export interface PageInfo { index: number; width: number; height: number; } export class PasswordError extends Error { detail: string; constructor(detail: string) { super(detail); this.name = 'PasswordError'; this.detail = detail; } } export interface PDFPermissions { isEncrypted: boolean; encryption: string; securityRevision: number; ownerUnlocked: boolean; canPrint: boolean; canPrintHighRes: boolean; canModify: boolean; canCopy: boolean; canAnnotate: boolean; canFillForms: boolean; canExtractForAccessibility: boolean; canAssemble: boolean; } export interface DocumentInfo { id: string; filename: string; sizeBytes: number; totalPages: number; pageWidth: number; pageHeight: number; uploadedAt: string; status: 'processing' | 'ready' | 'error'; pages?: PageInfo[]; permissions?: PDFPermissions; } export interface RenderParams { documentId: string; pageIndex: number; zoom: number; rotation: number; } export interface Annotation { id: string; pageIndex: number; type: string; x: number; y: number; width: number; height: number; fieldName?: string; fieldValue?: string; fieldType?: string; fieldFlags?: number; fieldOptions?: string[]; } import type { Point } from './coordinateMapping'; export interface SearchRect { x: number; y: number; w: number; h: number; } export interface SearchResult { pageIndex: number; rects: SearchRect[]; text: string; } export interface Glyph { text: string; x: number; y: number; w: number; h: number; fontSize?: number; } export interface PageText { text: string; glyphs: Glyph[]; } export interface DocumentMetadata { title?: string; author?: string; subject?: string; keywords?: string; creator?: string; producer?: string; creation_date?: string; modification_date?: string; } export interface FontInfo { fontName: string; type?: string; isEmbedded?: boolean; isSubset?: boolean; isVertical?: boolean; encoding?: string; hasToUnicode?: boolean; cmapName?: string; cidSystemInfo?: string; subsetTag?: string; sourceType?: string; substitutedFrom?: string; substitutedTo?: string; normalizedFamily?: string; internalFontId?: string; flags?: number; ascent?: number; descent?: number; capHeight?: number; } export interface TextOverlayData { text: string; x: number; y: number; width: number; height: number; fontSize: number; fontFamily: string; color: string; } export interface RedactionData { x: number; y: number; width: number; height: number; fillColor: string; } export interface ImageOverlayData { x: number; y: number; width: number; height: number; imageData: string; } export interface HighlightQuadPoint { x1: number; y1: number; x2: number; y2: number; x3: number; y3: number; x4: number; y4: number; } export interface HighlightData { quadPoints: HighlightQuadPoint[]; color: string; opacity: number; author: string; content?: string; } export interface FreeTextData { x: number; y: number; width: number; height: number; text: string; fontSize: number; color: string; } export interface StickyNoteData { x: number; y: number; author: string; content: string; timestamp?: string; } export interface FreehandData { paths: Point[][]; color: string; thickness: number; } export interface PageRotationData { rotation: 0 | 90 | 180 | 270; } export interface PageDeletionData { } export interface PageReorderData { destPageIndex: number; } export type EditOperationDataMap = { text_overlay: TextOverlayData; redaction: RedactionData; image_overlay: ImageOverlayData; highlight: HighlightData; free_text: FreeTextData; comment: StickyNoteData; freehand: FreehandData; page_rotation: PageRotationData; page_deletion: PageDeletionData; page_reorder: PageReorderData; delete_annotation: DeleteAnnotationData; update_annotation: UpdateAnnotationData; replace_text: ReplaceTextData; reflow_paragraph: ReflowParagraphData; underline: DecorationData; strikeout: DecorationData; squiggly: DecorationData; }; export interface DecorationData { quadPoints: HighlightQuadPoint[]; color: string; author?: string; content?: string; } export interface ReplaceTextData { objectIndices: number[]; text: string; internalFontId: string; fontSize: number; disableJustify?: boolean; } export interface ReflowFragment { text: string; internalFontId: string; fontSize: number; color: string; advances?: number[]; } export interface ReflowParagraphData { objectIndices: number[]; runs: ReflowFragment[]; columnLeft: number; columnRight: number; firstBaselineY: number; leading: number; oldLineCount: number; align: 'left' | 'justify' | 'center' | 'right'; pushColumnLeft?: number; paraId?: string; lines?: ReflowFragment[][]; lineBaselineY?: number[]; lineX?: number[]; } export interface DeleteAnnotationData { annotationId: string; } export interface UpdateAnnotationData { annotationId: string; x?: number; y?: number; width?: number; height?: number; color?: string; thickness?: number; text?: string; } export interface OutlineItem { title: string; pageIndex: number; level: number; } export type EditOperationType = keyof EditOperationDataMap; export interface EditOperationBase { id: string; type: T; pageIndex: number; data: EditOperationDataMap[T]; } export type EditOperation = { [T in EditOperationType]: EditOperationBase; }[EditOperationType]; export interface EditOperationEnvelope { version: '1.0'; operations: EditOperation[]; } export interface TextObjectResponse { text: string; fontName: string; fontSize: number; tm: number[]; } class GatewayService { public baseUrl: string; constructor() { this.baseUrl = import.meta.env.VITE_GATEWAY_URL || 'http://127.0.0.1:8000'; } async getHealth(): Promise<{ status: string; version: string; engine_available: boolean }> { const response = await fetch(`${this.baseUrl}/health`); if (!response.ok) throw new Error(`Health check failed: ${response.statusText}`); return response.json(); } async listDocuments(): Promise { try { const response = await fetch(`${this.baseUrl}/documents`); if (response.status === 501) { return this.getMockDocuments(); } if (!response.ok) throw new Error(`Failed to list documents: ${response.statusText}`); return response.json(); } catch (err) { return this.getMockDocuments(); } } async uploadDocument(file: File, password = ''): Promise { const formData = new FormData(); formData.append('file', file); const url = password ? `${this.baseUrl}/documents?password=${encodeURIComponent(password)}` : `${this.baseUrl}/documents`; const response = await fetch(url, { method: 'POST', body: formData, }); if (response.status === 401) { const body = await response.json().catch(() => ({ detail: 'Password required' })); throw new PasswordError(body.detail || 'Password required'); } if (response.status === 501) { return new Promise((resolve) => { setTimeout(() => { resolve({ id: `doc_${Math.random().toString(36).substr(2, 9)}`, filename: file.name, sizeBytes: file.size, totalPages: 5, pageWidth: 612, pageHeight: 792, uploadedAt: new Date().toISOString(), status: 'ready', pages: Array.from({ length: 5 }, (_, i) => ({ index: i, width: 612, height: 792 })), }); }, 1000); }); } if (!response.ok) throw new Error(`Upload failed: ${response.statusText}`); return response.json(); } async getDocument(id: string): Promise { try { const response = await fetch(`${this.baseUrl}/documents/${id}`); if (response.status === 501) { const mock = this.getMockDocuments().find(d => d.id === id); if (!mock) throw new Error('Document not found'); return mock; } if (!response.ok) throw new Error(`Failed to fetch document metadata: ${response.statusText}`); return response.json(); } catch (err) { const mock = this.getMockDocuments().find(d => d.id === id); if (!mock) throw new Error('Document not found'); return mock; } } async deleteDocument(id: string): Promise<{ success: boolean }> { const response = await fetch(`${this.baseUrl}/documents/${id}`, { method: 'DELETE', }); if (response.status === 501) { return { success: true }; } if (!response.ok) throw new Error(`Failed to delete document: ${response.statusText}`); return response.json(); } async getDocumentAnnotations(id: string): Promise { try { const response = await fetch(`${this.baseUrl}/documents/${id}/annotations`); if (response.status === 501) { return []; } if (!response.ok) throw new Error(`Failed to fetch document annotations: ${response.statusText}`); return response.json(); } catch (err) { return []; } } async renderPage(params: RenderParams): Promise { try { const query = new URLSearchParams({ page: params.pageIndex.toString(), zoom: params.zoom.toString(), rotation: params.rotation.toString(), }).toString(); const url = `${this.baseUrl}/render/${params.documentId}?${query}`; const response = await fetch(url); if (response.status === 501) { return this.generateMockPage(params.pageIndex); } if (!response.ok) throw new Error(`Page render failed: ${response.statusText}`); const blob = await response.blob(); return URL.createObjectURL(blob); } catch (err) { return this.generateMockPage(params.pageIndex); } } async getPageModel(documentId: string, pageIndex: number): Promise { const response = await fetch(`${this.baseUrl}/documents/${documentId}/pages/${pageIndex}/model`); if (!response.ok) throw new Error(`Failed to get page model: ${response.statusText}`); return response.json(); } async getDocumentRaw(documentId: string): Promise { try { const r = await fetch(`${this.baseUrl}/documents/${documentId}/raw`); if (!r.ok) return null; return await r.arrayBuffer(); } catch { return null; } } async getFontData(documentId: string, internalFontId: string): Promise { try { const url = `${this.baseUrl}/documents/${documentId}/font?internal_font_id=${encodeURIComponent(internalFontId)}`; const response = await fetch(url); if (!response.ok) return null; return await response.arrayBuffer(); } catch { return null; } } async getReconstructedFontData(documentId: string, internalFontId: string): Promise { try { const url = `${this.baseUrl}/documents/${documentId}/font-reconstructed?internal_font_id=${encodeURIComponent(internalFontId)}`; const response = await fetch(url); if (!response.ok || response.status === 204) return null; const buf = await response.arrayBuffer(); return buf.byteLength ? buf : null; } catch { return null; } } async applyEdits(documentId: string, operations: EditOperation[]): Promise<{ success: boolean; newDocumentId: string }> { let response: Response; try { response = await fetch(`${this.baseUrl}/documents/${documentId}/edits`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: '1.0', operations } as EditOperationEnvelope), }); } catch { return { success: true, newDocumentId: `${documentId}_edited` }; } if (response.status === 501) { return { success: true, newDocumentId: `${documentId}_edited` }; } if (!response.ok) { let detail = response.statusText; try { const j = await response.json(); if (j?.detail) detail = typeof j.detail === 'string' ? j.detail : JSON.stringify(j.detail); } catch { } throw new Error(detail); } return response.json(); } async searchDocument(documentId: string, query: string, caseSensitive: boolean = false, wholeWords: boolean = false): Promise { if (!query) return []; const urlParams = new URLSearchParams({ q: query, ...(caseSensitive && { case_sensitive: 'true' }), ...(wholeWords && { whole_words: 'true' }) }); const response = await fetch(`${this.baseUrl}/documents/${documentId}/search?${urlParams.toString()}`); if (response.status === 501) { return []; } if (!response.ok) throw new Error(`Failed to search document: ${response.statusText}`); return response.json(); } async getTextObjects(documentId: string, pageIndex: number): Promise { const response = await fetch(`${this.baseUrl}/documents/${documentId}/pages/${pageIndex}/text_objects`); if (!response.ok) throw new Error(`Failed to get text objects: ${response.statusText}`); return response.json(); } async updateTextObject(documentId: string, pageIndex: number, objectIndex: number, newText: string): Promise<{ success: boolean; newDocumentId?: string }> { const response = await fetch(`${this.baseUrl}/documents/${documentId}/pages/${pageIndex}/text_objects/${objectIndex}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ new_text: newText }), }); if (!response.ok) { let detail = response.statusText; try { const j = await response.json(); if (j?.detail) detail = j.detail; } catch { } throw new Error(detail); } return response.json(); } async getPageDisplayList(documentId: string, pageIndex: number): Promise { const response = await fetch(`${this.baseUrl}/documents/${documentId}/pages/${pageIndex}/display_list`); if (!response.ok) throw new Error(`Failed to get display list: ${response.statusText}`); return response.json(); } getImageXObjectUrl(documentId: string, pageIndex: number, name: string): string { return `${this.baseUrl}/documents/${documentId}/pages/${pageIndex}/xobjects/${encodeURIComponent(name)}`; } private getMockDocuments(): DocumentInfo[] { return [ { id: 'sample-doc-1', filename: 'Quarterly_Financial_Report.pdf', sizeBytes: 1024 * 1024 * 3.4, totalPages: 12, pageWidth: 612, pageHeight: 792, uploadedAt: new Date(Date.now() - 1000 * 60 * 60 * 3).toISOString(), status: 'ready', pages: Array.from({ length: 12 }, (_, i) => ({ index: i, width: 612, height: 792 })), }, { id: 'sample-doc-2', filename: 'Engineering_Specification_v4.pdf', sizeBytes: 1024 * 1024 * 18.2, totalPages: 54, pageWidth: 612, pageHeight: 792, uploadedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2).toISOString(), status: 'ready', pages: Array.from({ length: 54 }, (_, i) => ({ index: i, width: 612, height: 792 })), }, { id: 'sample-doc-3', filename: 'Tenant_Lease_Agreement_Final.pdf', sizeBytes: 1024 * 245, totalPages: 4, pageWidth: 612, pageHeight: 792, uploadedAt: new Date(Date.now() - 1000 * 60 * 45).toISOString(), status: 'ready', pages: Array.from({ length: 4 }, (_, i) => ({ index: i, width: 612, height: 792 })), } ]; } private generateMockPage(pageIndex: number): string { const width = 800; const height = 1100; const svg = ` PDF Editor PAGE ${pageIndex + 1} OF THE SPECIFICATION SECTION ${pageIndex * 2 + 1}.1 High-Performance Rendering Architecture ${pageIndex % 2 === 0 ? ` Engine Performance Vectors (Mock Data) ` : ` #include <pdfengine/core.hpp> int main() { pdf::Engine engine; engine.load_document("quarterly.pdf"); return engine.render_page(1, 2.0); } `} PDF Editor — preview render PAGE ${pageIndex + 1} `; return `data:image/svg+xml;utf8,${encodeURIComponent(svg.trim())}`; } async getPageText(documentId: string, pageIndex: number): Promise { try { const response = await fetch(`${this.baseUrl}/documents/${documentId}/pages/${pageIndex}/text`); if (!response.ok) return { text: '', glyphs: [] }; const data = await response.json(); return { text: data.text || '', glyphs: Array.isArray(data.glyphs) ? data.glyphs : [] }; } catch { return { text: '', glyphs: [] }; } } async getDocumentMetadata(documentId: string): Promise { try { const response = await fetch(`${this.baseUrl}/documents/${documentId}/metadata`); if (!response.ok) return {}; return response.json(); } catch { return {}; } } async getDocumentFonts(documentId: string): Promise { try { const response = await fetch(`${this.baseUrl}/documents/${documentId}/fonts`); if (!response.ok) return []; const data = await response.json(); return Array.isArray(data) ? data : []; } catch { return []; } } async getOutline(documentId: string): Promise { try { const response = await fetch(`${this.baseUrl}/documents/${documentId}/outline`); if (!response.ok) return []; const data = await response.json(); return Array.isArray(data) ? data : []; } catch { return []; } } async exportDocument(documentId: string, filename: string): Promise { const response = await fetch(`${this.baseUrl}/documents/${documentId}/export`); if (!response.ok) throw new Error(`Export failed: ${response.statusText}`); const blob = await response.blob(); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } async fetchDocumentBytes(documentId: string): Promise { const response = await fetch(`${this.baseUrl}/documents/${documentId}/export`); if (!response.ok) throw new Error(`Failed to fetch document bytes: ${response.statusText}`); return response.arrayBuffer(); } } export const gatewayService = new GatewayService();