730 lines
22 KiB
TypeScript
730 lines
22 KiB
TypeScript
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<T extends EditOperationType> {
|
|
id: string;
|
|
type: T;
|
|
pageIndex: number;
|
|
data: EditOperationDataMap[T];
|
|
}
|
|
|
|
export type EditOperation = {
|
|
[T in EditOperationType]: EditOperationBase<T>;
|
|
}[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<DocumentInfo[]> {
|
|
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<DocumentInfo> {
|
|
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<DocumentInfo> {
|
|
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<any[]> {
|
|
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<string> {
|
|
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<any> {
|
|
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<ArrayBuffer | null> {
|
|
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<ArrayBuffer | null> {
|
|
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<ArrayBuffer | null> {
|
|
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<SearchResult[]> {
|
|
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<TextObjectResponse[]> {
|
|
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<any[]> {
|
|
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 = `
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
|
|
<rect width="${width}" height="${height}" fill="#fafbfc" />
|
|
<!-- Shadow border effect -->
|
|
<rect x="5" y="5" width="${width - 10}" height="${height - 10}" fill="white" rx="8" filter="drop-shadow(0 4px 6px rgba(0,0,0,0.05))" />
|
|
|
|
<!-- Header -->
|
|
<text x="60" y="80" font-family="system-ui, sans-serif" font-size="28" font-weight="800" fill="#1e293b">PDF Editor</text>
|
|
<text x="60" y="110" font-family="system-ui, sans-serif" font-size="12" font-weight="500" fill="#64748b" letter-spacing="1">PAGE ${pageIndex + 1} OF THE SPECIFICATION</text>
|
|
<line x1="60" y1="130" x2="${width - 60}" y2="130" stroke="#f1f5f9" stroke-width="2" />
|
|
|
|
<!-- Document Content Simulation -->
|
|
<rect x="60" y="160" width="120" height="24" fill="#eff6ff" rx="4" />
|
|
<text x="70" y="176" font-family="system-ui, sans-serif" font-size="12" font-weight="700" fill="#3b82f6">SECTION ${pageIndex * 2 + 1}.1</text>
|
|
|
|
<text x="60" y="210" font-family="system-ui, sans-serif" font-size="20" font-weight="700" fill="#0f172a">High-Performance Rendering Architecture</text>
|
|
|
|
<!-- Multiline mock text -->
|
|
<rect x="60" y="235" width="${width - 120}" height="10" fill="#e2e8f0" rx="3" />
|
|
<rect x="60" y="255" width="${width - 160}" height="10" fill="#e2e8f0" rx="3" />
|
|
<rect x="60" y="275" width="${width - 120}" height="10" fill="#e2e8f0" rx="3" />
|
|
<rect x="60" y="295" width="${width - 240}" height="10" fill="#e2e8f0" rx="3" />
|
|
|
|
<!-- Draw some abstract blueprints / charts on even pages to make them look distinct -->
|
|
${pageIndex % 2 === 0 ? `
|
|
<!-- Mock Chart -->
|
|
<rect x="60" y="340" width="${width - 120}" height="280" fill="#f8fafc" rx="12" stroke="#e2e8f0" stroke-width="1.5" />
|
|
<path d="M 100 580 Q 200 420 300 480 T 500 380 T 700 420" fill="none" stroke="url(#chartGrad)" stroke-width="4" stroke-linecap="round" />
|
|
<circle cx="300" cy="480" r="6" fill="#3b82f6" stroke="white" stroke-width="2" />
|
|
<circle cx="500" cy="380" r="6" fill="#10b981" stroke="white" stroke-width="2" />
|
|
<text x="80" y="375" font-family="system-ui, sans-serif" font-size="14" font-weight="700" fill="#334155">Engine Performance Vectors (Mock Data)</text>
|
|
|
|
<defs>
|
|
<linearGradient id="chartGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" stop-color="#3b82f6" />
|
|
<stop offset="100%" stop-color="#10b981" />
|
|
</linearGradient>
|
|
</defs>
|
|
` : `
|
|
<!-- Structured List -->
|
|
<circle cx="70" cy="360" r="4" fill="#3b82f6" />
|
|
<rect x="90" y="355" width="${width - 160}" height="10" fill="#cbd5e1" rx="3" />
|
|
|
|
<circle cx="70" cy="390" r="4" fill="#3b82f6" />
|
|
<rect x="90" y="385" width="${width - 200}" height="10" fill="#cbd5e1" rx="3" />
|
|
|
|
<circle cx="70" cy="420" r="4" fill="#3b82f6" />
|
|
<rect x="90" y="415" width="${width - 140}" height="10" fill="#cbd5e1" rx="3" />
|
|
|
|
<!-- A block of code/preformatted text -->
|
|
<rect x="60" y="470" width="${width - 120}" height="180" fill="#0f172a" rx="12" />
|
|
<text x="90" y="510" font-family="monospace" font-size="13" fill="#38bdf8">#include <pdfengine/core.hpp></text>
|
|
<text x="90" y="535" font-family="monospace" font-size="13" fill="#f87171">int main() {</text>
|
|
<text x="120" y="560" font-family="monospace" font-size="13" fill="#a7f3d0"> pdf::Engine engine;</text>
|
|
<text x="120" y="585" font-family="monospace" font-size="13" fill="#a7f3d0"> engine.load_document("quarterly.pdf");</text>
|
|
<text x="120" y="610" font-family="monospace" font-size="13" fill="#fbbf24"> return engine.render_page(1, 2.0);</text>
|
|
<text x="90" y="635" font-family="monospace" font-size="13" fill="#f87171">}</text>
|
|
`}
|
|
|
|
<!-- Footer -->
|
|
<line x1="60" y1="1020" x2="${width - 60}" y2="1020" stroke="#f1f5f9" stroke-width="1.5" />
|
|
<text x="60" y="1045" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#94a3b8">PDF Editor — preview render</text>
|
|
<text x="${width - 100}" y="1045" font-family="system-ui, sans-serif" font-size="11" font-weight="700" fill="#94a3b8">PAGE ${pageIndex + 1}</text>
|
|
</svg>
|
|
`;
|
|
return `data:image/svg+xml;utf8,${encodeURIComponent(svg.trim())}`;
|
|
}
|
|
|
|
async getPageText(documentId: string, pageIndex: number): Promise<PageText> {
|
|
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<DocumentMetadata> {
|
|
try {
|
|
const response = await fetch(`${this.baseUrl}/documents/${documentId}/metadata`);
|
|
if (!response.ok) return {};
|
|
return response.json();
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
async getDocumentFonts(documentId: string): Promise<FontInfo[]> {
|
|
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<OutlineItem[]> {
|
|
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<void> {
|
|
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<ArrayBuffer> {
|
|
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();
|