From c27a80f840082da3604534af6f1519bede5e544d Mon Sep 17 00:00:00 2001 From: saqib mir Date: Mon, 8 Jun 2026 16:28:10 +0530 Subject: [PATCH] integration done and testing done --- bindings/python/pdfengine_py.cpp | 52 ++++++++ engine/include/pdfengine/pdf_document.hpp | 4 + engine/src/parser/pdfium_document.cpp | 31 ++++- frontend/package-lock.json | 70 ++++++++--- frontend/src/index.css | 66 +++++++--- frontend/src/lib/gatewayService.ts | 22 ++-- frontend/src/viewer/PDFViewer.tsx | 35 +++++- gateway/app/routers/documents.py | 142 ++++++++++++++++++++-- gateway/app/routers/render.py | 23 ---- model_test.json | Bin 0 -> 93702 bytes scripts/start_gateway.ps1 | 2 +- 11 files changed, 365 insertions(+), 82 deletions(-) create mode 100644 model_test.json diff --git a/bindings/python/pdfengine_py.cpp b/bindings/python/pdfengine_py.cpp index bc95e31..3295385 100644 --- a/bindings/python/pdfengine_py.cpp +++ b/bindings/python/pdfengine_py.cpp @@ -111,12 +111,64 @@ PYBIND11_MODULE(pdfengine, m) { return "FontInfo(font_name='" + self.fontName + "', type='" + self.type + "', is_embedded=" + (self.isEmbedded ? "True" : "False") + ")"; }); + py::class_(m, "Glyph") + .def_readonly("text", &pdfengine::Glyph::text) + .def_readonly("unicode", &pdfengine::Glyph::unicode) + .def_readonly("font_name", &pdfengine::Glyph::fontName) + .def_readonly("flags", &pdfengine::Glyph::flags) + .def_readonly("font_size", &pdfengine::Glyph::fontSize) + .def_readonly("origin_x", &pdfengine::Glyph::originX) + .def_readonly("origin_y", &pdfengine::Glyph::originY) + .def_readonly("bbox_x", &pdfengine::Glyph::bboxX) + .def_readonly("bbox_y", &pdfengine::Glyph::bboxY) + .def_readonly("bbox_w", &pdfengine::Glyph::bboxW) + .def_readonly("bbox_h", &pdfengine::Glyph::bboxH) + .def_readonly("angle", &pdfengine::Glyph::angle); + + py::class_(m, "TextRun") + .def_readonly("text", &pdfengine::TextRun::text) + .def_readonly("font_name", &pdfengine::TextRun::fontName) + .def_readonly("flags", &pdfengine::TextRun::flags) + .def_readonly("font_size", &pdfengine::TextRun::fontSize) + .def_readonly("internal_font_id", &pdfengine::TextRun::internalFontId) + .def_readonly("is_embedded", &pdfengine::TextRun::isEmbedded) + .def_readonly("type", &pdfengine::TextRun::type) + .def_readonly("glyphs", &pdfengine::TextRun::glyphs) + .def_readonly("x", &pdfengine::TextRun::x) + .def_readonly("y", &pdfengine::TextRun::y) + .def_readonly("w", &pdfengine::TextRun::w) + .def_readonly("h", &pdfengine::TextRun::h); + + py::class_(m, "TextLine") + .def_readonly("runs", &pdfengine::TextLine::runs) + .def_readonly("baseline_y", &pdfengine::TextLine::baselineY) + .def_readonly("x", &pdfengine::TextLine::x) + .def_readonly("y", &pdfengine::TextLine::y) + .def_readonly("w", &pdfengine::TextLine::w) + .def_readonly("h", &pdfengine::TextLine::h); + + py::class_(m, "Paragraph") + .def_readonly("lines", &pdfengine::Paragraph::lines) + .def_readonly("x", &pdfengine::Paragraph::x) + .def_readonly("y", &pdfengine::Paragraph::y) + .def_readonly("w", &pdfengine::Paragraph::w) + .def_readonly("h", &pdfengine::Paragraph::h); + + py::class_(m, "PageModel") + .def_readonly("paragraphs", &pdfengine::PageModel::paragraphs) + .def_readonly("width", &pdfengine::PageModel::width) + .def_readonly("height", &pdfengine::PageModel::height) + .def_readonly("page_index", &pdfengine::PageModel::pageIndex); + py::class_>(m, "PdfPage") .def_property_readonly("width", &pdfengine::PdfPage::width) .def_property_readonly("height", &pdfengine::PdfPage::height) .def("render", [](const pdfengine::PdfPage& self, int dpi) { return get_or_throw(self.render(dpi)); }, py::arg("dpi") = 96) + .def("extract_document_model", [](const pdfengine::PdfPage& self) { + return get_or_throw(self.extractDocumentModel()); + }) .def("extract_text", [](const pdfengine::PdfPage& self) { return get_or_throw(self.extractText()); }) diff --git a/engine/include/pdfengine/pdf_document.hpp b/engine/include/pdfengine/pdf_document.hpp index b624f8b..0cafffe 100644 --- a/engine/include/pdfengine/pdf_document.hpp +++ b/engine/include/pdfengine/pdf_document.hpp @@ -97,12 +97,16 @@ struct TextRun { uint32_t flags = 0; double fontSize = 0.0; std::string internalFontId; + bool isEmbedded = false; + std::string type; std::vector glyphs; double x = 0.0, y = 0.0, w = 0.0, h = 0.0; }; struct TextLine { std::vector runs; + std::vector glyphs; + double angle = 0.0; double baselineY = 0.0; double x = 0.0, y = 0.0, w = 0.0, h = 0.0; }; diff --git a/engine/src/parser/pdfium_document.cpp b/engine/src/parser/pdfium_document.cpp index 5d37ffd..04ee319 100644 --- a/engine/src/parser/pdfium_document.cpp +++ b/engine/src/parser/pdfium_document.cpp @@ -854,6 +854,13 @@ std::expected PdfiumPage::extractDocumentModel() const { return model; } + std::unordered_map fontMap; + if (auto fontsRes = getFonts()) { + for (const auto& f : *fontsRes) { + fontMap[f.fontName] = f; + } + } + std::vector documentGlyphs; documentGlyphs.reserve(charCount); @@ -969,7 +976,13 @@ std::expected PdfiumPage::extractDocumentModel() const { currentRun.fontName = firstG->fontName; currentRun.fontSize = firstG->fontSize; currentRun.flags = firstG->flags; + if (auto it = fontMap.find(currentRun.fontName); it != fontMap.end()) { + currentRun.internalFontId = it->second.internalFontId; + currentRun.isEmbedded = it->second.isEmbedded; + currentRun.type = it->second.type; + } currentRun.glyphs.push_back(*firstG); + currentRun.text += firstG->text; for (size_t i = 1; i < line.glyphs.size(); ++i) { const auto& prevG = line.glyphs[i-1]; @@ -1004,17 +1017,29 @@ std::expected PdfiumPage::extractDocumentModel() const { currentRun.fontName = currG.fontName; currentRun.fontSize = currG.fontSize; currentRun.flags = currG.flags; + if (auto it = fontMap.find(currentRun.fontName); it != fontMap.end()) { + currentRun.internalFontId = it->second.internalFontId; + currentRun.isEmbedded = it->second.isEmbedded; + currentRun.type = it->second.type; + } } currentRun.glyphs.push_back(spaceGlyph); + currentRun.text += spaceGlyph.text; } else if (breakRun) { line.runs.push_back(std::move(currentRun)); currentRun = TextRun(); currentRun.fontName = currG.fontName; currentRun.fontSize = currG.fontSize; currentRun.flags = currG.flags; + if (auto it = fontMap.find(currentRun.fontName); it != fontMap.end()) { + currentRun.internalFontId = it->second.internalFontId; + currentRun.isEmbedded = it->second.isEmbedded; + currentRun.type = it->second.type; + } } currentRun.glyphs.push_back(currG); + currentRun.text += currG.text; } if (!currentRun.glyphs.empty()) { line.runs.push_back(std::move(currentRun)); @@ -1965,10 +1990,12 @@ std::expected, EngineError> PdfiumDocument::getFontData(con // Always cache every font we encounter to avoid rescanning if (fontDataCache_.find(fontName) == fontDataCache_.end()) { - size_t buflen = FPDFFont_GetFontData(font, nullptr, 0); + size_t buflen = 0; + FPDFFont_GetFontData(font, nullptr, 0, &buflen); if (buflen > 0) { std::vector buffer(buflen); - if (FPDFFont_GetFontData(font, buffer.data(), buflen) > 0) { + size_t actual_len = 0; + if (FPDFFont_GetFontData(font, buffer.data(), buflen, &actual_len)) { fontDataCache_[fontName] = buffer; } else { fontDataCache_[fontName] = std::vector(); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8bccfe2..d6bbbb3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -59,7 +59,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -269,10 +268,31 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/core": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", + "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "license": "MIT", "optional": true, "dependencies": { @@ -755,6 +775,37 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", @@ -1087,7 +1138,6 @@ "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1098,7 +1148,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1158,7 +1207,6 @@ "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", @@ -1389,7 +1437,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1480,7 +1527,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1628,7 +1674,6 @@ "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -2524,7 +2569,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2585,7 +2629,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2757,7 +2800,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2843,7 +2885,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -2968,7 +3009,6 @@ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/src/index.css b/frontend/src/index.css index 6bfe9b0..e49d8e2 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -5,20 +5,23 @@ :root { --sans: 'Outfit', system-ui, -apple-system, sans-serif; --mono: 'JetBrains Mono', monospace; - + /* Color Palette - Premium Royal/Midnight */ - --bg-main: #020617; /* Slate 950 */ - --bg-card: #0f172a; /* Slate 900 */ - --bg-sidebar: #090d16; /* Deep Midnight */ + --bg-main: #020617; + /* Slate 950 */ + --bg-card: #0f172a; + /* Slate 900 */ + --bg-sidebar: #090d16; + /* Deep Midnight */ --bg-accent-indigo: #4f46e5; --bg-accent-indigo-hover: #4338ca; --border-main: #1e293b; --border-glow: rgba(99, 102, 241, 0.25); - + --text-main: #f8fafc; --text-muted: #94a3b8; --text-dim: #64748b; - + /* Status Colors */ --color-success: #10b981; --color-success-bg: rgba(16, 185, 129, 0.1); @@ -175,9 +178,18 @@ body { border-radius: 9999px; } -.status-checking .health-dot { background: var(--color-warning); animation: pulse 1.5s infinite; } -.status-healthy .health-dot { background: var(--color-success); } -.status-unhealthy .health-dot { background: var(--color-error); } +.status-checking .health-dot { + background: var(--color-warning); + animation: pulse 1.5s infinite; +} + +.status-healthy .health-dot { + background: var(--color-success); +} + +.status-unhealthy .health-dot { + background: var(--color-error); +} .wasm-badge { display: flex; @@ -422,7 +434,8 @@ body { padding: 18px; } -.doc-list-container, .annotations-list { +.doc-list-container, +.annotations-list { display: flex; flex-direction: column; gap: 12px; @@ -605,7 +618,8 @@ body { flex: 1; height: 100%; overflow: auto; - background: #0b0f19; /* Slightly darker midnight */ + background: #0b0f19; + /* Slightly darker midnight */ display: flex; justify-content: center; align-items: start; @@ -698,11 +712,13 @@ body { } .highlight-box.type-highlight { - background: #fde047; /* Yellow 300 */ + background: #fde047; + /* Yellow 300 */ } .highlight-box.type-comment { - background: #fecdd3; /* Rose 200 */ + background: #fecdd3; + /* Rose 200 */ border-bottom: 2px solid #f43f5e; } @@ -732,7 +748,7 @@ body { font-weight: 700; padding: 4px 10px; border-radius: 6px; - box-shadow: 0 4px 12px rgba(0,0,0,0.25); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); letter-spacing: 0.5px; } @@ -751,11 +767,23 @@ body { /* Animations */ @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } } @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } +} \ No newline at end of file diff --git a/frontend/src/lib/gatewayService.ts b/frontend/src/lib/gatewayService.ts index aab2eb8..c3806ae 100644 --- a/frontend/src/lib/gatewayService.ts +++ b/frontend/src/lib/gatewayService.ts @@ -118,7 +118,7 @@ export interface PageRotationData { rotation: 0 | 90 | 180 | 270; } -export interface PageDeletionData {} +export interface PageDeletionData { } export interface PageReorderData { destPageIndex: number; @@ -191,7 +191,7 @@ class GatewayService { method: 'POST', body: formData, }); - + if (response.status === 501) { // Simulate upload for Phase 0 scaffolding return new Promise((resolve) => { @@ -210,7 +210,7 @@ class GatewayService { }, 1000); }); } - + if (!response.ok) throw new Error(`Upload failed: ${response.statusText}`); return response.json(); } @@ -252,12 +252,12 @@ class GatewayService { }).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); @@ -266,6 +266,12 @@ class GatewayService { } } + 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 applyEdits(documentId: string, operations: EditOperation[]): Promise<{ success: boolean; newDocumentId: string }> { const response = await fetch(`${this.baseUrl}/edits/${documentId}`, { method: 'POST', @@ -283,15 +289,15 @@ class GatewayService { async searchDocument(documentId: string, query: string): Promise { if (!query) return []; - + const urlParams = new URLSearchParams({ q: query }); const response = await fetch(`${this.baseUrl}/documents/${documentId}/search?${urlParams.toString()}`); - + if (response.status === 501) { // Return mock empty results if backend isn't available return []; } - + if (!response.ok) throw new Error(`Failed to search document: ${response.statusText}`); return response.json(); } diff --git a/frontend/src/viewer/PDFViewer.tsx b/frontend/src/viewer/PDFViewer.tsx index 9a949f6..de12cb2 100644 --- a/frontend/src/viewer/PDFViewer.tsx +++ b/frontend/src/viewer/PDFViewer.tsx @@ -151,9 +151,9 @@ export const PDFViewer = React.forwardRef(({ currentVisiblePageIdx = layout.index; } - const isInside = (pageBottom >= viewportTop - (layout.height * buffer)) && - (pageTop <= viewportBottom + (layout.height * buffer)); - + const isInside = (pageBottom >= viewportTop - (layout.height * buffer)) && + (pageTop <= viewportBottom + (layout.height * buffer)); + if (isInside) { visible.push(layout); } @@ -201,6 +201,35 @@ export const PDFViewer = React.forwardRef(({ }; }, [visiblePages, documentId, zoom, renderedPages]); + useEffect(() => { + const verifyPageModel = async () => { + if (visiblePages.length > 0 && documentId && !renderedPages[visiblePages[0].index + '_verified']) { + const pageIndex = visiblePages[0].index; + try { + const model = await gatewayService.getPageModel(documentId, pageIndex); + console.log(`--- Verification for Page ${pageIndex} ---`); + model.paragraphs?.forEach((p: any) => { + p.lines?.forEach((l: any) => { + l.runs?.forEach((r: any) => { + const isBold = (r.flags & 262144) !== 0 || r.font_name.toLowerCase().includes('bold'); + const isItalic = (r.flags & 64) !== 0 || r.font_name.toLowerCase().includes('italic'); + console.log(`Run Text: "${r.text}", Font Name: ${r.font_name}, Font Size: ${r.font_size}, Bold: ${isBold}, Italic: ${isItalic}, Embedded: ${r.is_embedded}, Type: ${r.type}`); + }); + }); + }); + setRenderedPages((prev) => { + const next = [...prev]; + next[pageIndex + '_verified' as any] = 'true'; + return next; + }); + } catch (e) { + // ignore or log + } + } + }; + verifyPageModel(); + }, [visiblePages, documentId]); + const handleTextSelection = (text: string, bbox: Rect) => { if (activeTool === 'highlight') { const newAnno: Annotation = { diff --git a/gateway/app/routers/documents.py b/gateway/app/routers/documents.py index 8dcffb6..f9def6d 100644 --- a/gateway/app/routers/documents.py +++ b/gateway/app/routers/documents.py @@ -1,9 +1,4 @@ -<<<<<<< HEAD -from typing import List, Annotated -from fastapi import APIRouter, HTTPException, status, File, UploadFile, Query -======= from fastapi import APIRouter, File, HTTPException, UploadFile, status ->>>>>>> 2638dfe969874b954502b26656df48ad971e306d from pydantic import BaseModel from app.services import engine @@ -181,16 +176,10 @@ class FontInfoResponse(BaseModel): descent: float capHeight: float -<<<<<<< HEAD -@router.get("/{document_id}/fonts", response_model=List[FontInfoResponse]) -def get_document_fonts(document_id: str, start_page: Annotated[int, Query(ge=0)] = 0, end_page: Annotated[int, Query(ge=-1)] = -1) -> List[FontInfoResponse]: -======= - @router.get("/{document_id}/fonts", response_model=list[FontInfoResponse]) def get_document_fonts( document_id: str, start_page: int = 0, end_page: int = -1 ) -> list[FontInfoResponse]: ->>>>>>> 2638dfe969874b954502b26656df48ad971e306d if not engine.is_available(): raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, @@ -326,3 +315,134 @@ def search_document(document_id: str, q: str) -> list[SearchMatch]: except Exception as e: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) + +class GlyphModel(BaseModel): + text: str + unicode: int + font_name: str + flags: int + font_size: float + origin_x: float + origin_y: float + bbox_x: float + bbox_y: float + bbox_w: float + bbox_h: float + angle: float + +class TextRunModel(BaseModel): + text: str + font_name: str + flags: int + font_size: float + internal_font_id: str + is_embedded: bool + type: str + glyphs: list[GlyphModel] + x: float + y: float + w: float + h: float + +class TextLineModel(BaseModel): + runs: list[TextRunModel] + baseline_y: float + x: float + y: float + w: float + h: float + +class ParagraphModel(BaseModel): + lines: list[TextLineModel] + x: float + y: float + w: float + h: float + +class PageModelResponse(BaseModel): + paragraphs: list[ParagraphModel] + width: float + height: float + page_index: int + +@router.get("/{document_id}/pages/{page_index}/model", response_model=PageModelResponse) +def get_page_model(document_id: str, page_index: int) -> PageModelResponse: + if not engine.is_available(): + raise HTTPException( + status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail="Engine bridge (bindings/python) not yet available.", + ) + + doc_info = document_store.get_document(document_id) + if not doc_info: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found") + + try: + doc = doc_info["doc_instance"] + page = doc.get_page(page_index) + model = page.extract_document_model() + + paragraphs = [] + for p in model.paragraphs: + lines = [] + for l in p.lines: + runs = [] + for r in l.runs: + glyphs = [] + for g in r.glyphs: + glyphs.append(GlyphModel( + text=g.text, + unicode=g.unicode, + font_name=g.font_name, + flags=g.flags, + font_size=g.font_size, + origin_x=g.origin_x, + origin_y=g.origin_y, + bbox_x=g.bbox_x, + bbox_y=g.bbox_y, + bbox_w=g.bbox_w, + bbox_h=g.bbox_h, + angle=g.angle + )) + runs.append(TextRunModel( + text=r.text, + font_name=r.font_name, + flags=r.flags, + font_size=r.font_size, + internal_font_id=r.internal_font_id, + is_embedded=r.is_embedded, + type=r.type, + glyphs=glyphs, + x=r.x, + y=r.y, + w=r.w, + h=r.h + )) + lines.append(TextLineModel( + runs=runs, + baseline_y=l.baseline_y, + x=l.x, + y=l.y, + w=l.w, + h=l.h + )) + paragraphs.append(ParagraphModel( + lines=lines, + x=p.x, + y=p.y, + w=p.w, + h=p.h + )) + + return PageModelResponse( + paragraphs=paragraphs, + width=model.width, + height=model.height, + page_index=model.page_index + ) + except ValueError as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + except IndexError as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) diff --git a/gateway/app/routers/render.py b/gateway/app/routers/render.py index 053c3e2..9cd677b 100644 --- a/gateway/app/routers/render.py +++ b/gateway/app/routers/render.py @@ -1,9 +1,6 @@ -<<<<<<< HEAD from typing import List, Annotated from fastapi import APIRouter, HTTPException, status, Response, Path, Query -======= from fastapi import APIRouter, HTTPException, Response, status ->>>>>>> 2638dfe969874b954502b26656df48ad971e306d from app.routers.documents import FontInfoResponse from app.services import engine @@ -68,13 +65,9 @@ def extract_page_text(document_id: str, page_index: Annotated[int, Path(ge=0)]): @compat_router.get("/render/{document_id}") -<<<<<<< HEAD -def render_page_compat(document_id: str, page: Annotated[int, Query(ge=0)] = 0, zoom: float = 1.0, rotation: int = 0) -> Response: -======= def render_page_compat( document_id: str, page: int = 0, zoom: float = 1.0, rotation: int = 0 ) -> Response: ->>>>>>> 2638dfe969874b954502b26656df48ad971e306d dpi = int(96 * zoom) return render_page(document_id, page, dpi) @@ -104,9 +97,6 @@ def get_page_info(document_id: str, page_index: Annotated[int, Path(ge=0)]): @router.get("/{page_index}/transform/page-to-device") -<<<<<<< HEAD -def transform_page_to_device(document_id: str, page_index: Annotated[int, Path(ge=0)], x: float, y: float, device_width: int, device_height: int, rotate: int = 0): -======= def transform_page_to_device( document_id: str, page_index: int, @@ -116,7 +106,6 @@ def transform_page_to_device( device_height: int, rotate: int = 0, ): ->>>>>>> 2638dfe969874b954502b26656df48ad971e306d if not engine.is_available(): raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Engine unavailable" @@ -139,9 +128,6 @@ def transform_page_to_device( @router.get("/{page_index}/transform/device-to-page") -<<<<<<< HEAD -def transform_device_to_page(document_id: str, page_index: Annotated[int, Path(ge=0)], x: int, y: int, device_width: int, device_height: int, rotate: int = 0): -======= def transform_device_to_page( document_id: str, page_index: int, @@ -151,7 +137,6 @@ def transform_device_to_page( device_height: int, rotate: int = 0, ): ->>>>>>> 2638dfe969874b954502b26656df48ad971e306d if not engine.is_available(): raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Engine unavailable" @@ -171,15 +156,10 @@ def transform_device_to_page( return {"x": res.x, "y": res.y} except Exception as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) -<<<<<<< HEAD -@router.get("/{page_index}/fonts", response_model=List[FontInfoResponse]) -def get_page_fonts(document_id: str, page_index: Annotated[int, Path(ge=0)]) -> List[FontInfoResponse]: -======= @router.get("/{page_index}/fonts", response_model=list[FontInfoResponse]) def get_page_fonts(document_id: str, page_index: int) -> list[FontInfoResponse]: ->>>>>>> 2638dfe969874b954502b26656df48ad971e306d if not engine.is_available(): raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, @@ -222,7 +202,6 @@ def get_page_fonts(document_id: str, page_index: int) -> list[FontInfoResponse]: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Page index out of range") except Exception as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) -<<<<<<< HEAD @router.get("/{page_index}/fonts/glyph-width") @@ -247,5 +226,3 @@ def get_page_glyph_width(document_id: str, page_index: Annotated[int, Path(ge=0) except Exception as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) -======= ->>>>>>> 2638dfe969874b954502b26656df48ad971e306d diff --git a/model_test.json b/model_test.json new file mode 100644 index 0000000000000000000000000000000000000000..6cdd266f8a29ace390b7c9b2cd3cb122998d565a GIT binary patch literal 93702 zcmeHQQE%Kf4CeEI{fFRQw-rg(c5R0MTeqRefME~YTTldPlO-9FwCH7M+97{^8*RsD zTaqP;;@#}zL(%voQS#kKn&L;v_P_u9O`p?ev~~M$LGS30-t+&H`tLhBr6n!sdELH0 zkZt?KkHhifvTo~w&gqOU>A3JaxZ{S(CEqhNo?EwIJpba~Qii5Gs;<1C@91y3VqCBJ z?=F4I+Wmvx^0&L91O9tR7u>c>-}84qlulQr8@BDf{p;||>-CDhT=O{>jLspWb-cQM z4j8@mDsC`Z|980F_!&3<8$WFPH=a6YT#xnM7+YwW%`ezb*;((x^jcVur420D+YNtd zzJ701{-tF8!#tfg14yOw=V|Ti{EX+#$Mr0VxjFxelj^gQ!`!^7_`}jKOEhfZyCvtr z+$=$KyH0Ej7Ci2Rdr=@s-Lsz^2 zxo)Yw)7+c6h?OT40IT6}vMm?8qO(c17JE->-pp^)b=bz`JUZ>-U3qg?H`S zgWYF%S34}bku26W7) z+`EN-bu(j0J{+r}((5-*E`N3|zXJ%nms%Z_`K)%7*b%#zuzM+HAL%yiUP@!sNMN4E z?j@G)6?DmFnz69-p39- z_*aSa>gelXkMG>Co-A<2_wdK|1U=;^3k;u6&^=RNs&_3;|B7h*)Ly_(K`*|2m)A(I zkjB!xCu@v9Ph)4IcLGh{_V0@qvdy!muK{E=(imwZY_rOeZ(z%4WoO*fu5a6`qRi&8 z`XI55RtBpNEfSjJyzd&YSbacN!?hb}REn&Ii`+WPQmue1=(QllxX%?zpMW^s%#m$O zl-5dF)RZ0{Pg&Z!y%%}iZ2T@Wg>_+9vL8lf`}2F!Y!hMp?Bye4nc>*eW~b|G*JV{{ z%aG1_h%3@qj!8%{z8`VL^liPJ|^Wg7Ehy*d<%G22}&s$cvpDWl#S@x;azpO#e2cK#__542V;*U_DK3x zBAw}wk@T^X)<`i%MpBBXPA*>$$Vlpt?s_33*CAv1j6<+DgKD+N)ZhAM^qQaMN^BYPJ^1;2i_GKN!KQ9%E(CC z;5Zxw8Ob3YehC@LX}zBZPaN`Xw7dr1wH!l#1b9~;lVQyqy(==3LmjT8B;^svNb1p- zVvLNW9%D(*d_5o|DZw=+gN$Sh9eo=zl6vgip2$eLh-k|2uE?Yj@oH>nAz63aP z3bQ$K=8i)kc1`Iw)sEOTg^;ctv8xWZP_G-Z&C{3JyzNYFYbUmEH?o*Lb-bhG+4PJ0 z9qpHNOiR9N{y9|^wQp=et(BI1&vuO1ao)2t_8i7^t8J5V&}L=frT2y^Xhn;Bte;dX zUF-@y#PHO%Y4os1S9Dm<8gD4ZD4x0^hOHj&}K{Sz%*;o%m6JyBa)Wf@CA8wAl$YsIRxLQi9 ze zYI9jcVQWjav#{;js)c@aGh@kmC-%RTU)iA%@@Hp@`xYMZZu#vA?v}UpNFlt3b!9$l zI99l{px3;%zT~y_cK_G`eO%oaw_WRD-S-IC`quF89M;wIr{ZV@|%yQCiH?&HPa1mZx=B8|G=b70V?tPs_2AOIrAG z$=P(iUNUkCZ*N*vMPpU9ko&x0W19aONDeq_W`5DWt%ecWH;$5U070IDH-g&YZf! z#@q9wiMyPOpT4bkxeIXSajTZKfHTW*%9R3V&P8lo9-Mg`8+l9qEC=r@M>i}9?;6IX zv=-jA6v3`PylaPr{u8__GLm}hiSz&`BN?(0=}d==WC%T3ofP9P z6NZeW4B0%Xd_90O=OOG%gERY>Sk!>5HPw@w(-MKIhSQC%1pO)a824Qg&5cvoa3U7N5eBO_^p<8TyYB!_tT zr8#CK!I^zMTGvQtI&fw^@=}b!ne`Y;dgki^oLPcvOa`1ehK{}soLP^Z+Y_AGMMP7E zcLitG*iE_*ICC1Ke2L9*X4|Jamqiq|wq!dC<2mzN{058meHP2r+bs6U)`Hq=1dhs0w7kUT6@hu!orcS%eNXy58E7z?2Ma# z_a%6ML^E4d#(iyV>F($2