diff --git a/bindings/python/pdfengine_py.cpp b/bindings/python/pdfengine_py.cpp index 0005166..27bcee7 100644 --- a/bindings/python/pdfengine_py.cpp +++ b/bindings/python/pdfengine_py.cpp @@ -428,7 +428,14 @@ PYBIND11_MODULE(pdfengine, m) { .def("page_to_device", &pdfengine::PdfPage::pageToDevice, py::arg("page_point"), py::arg("device_width"), py::arg("device_height"), py::arg("rotate") = 0) .def("device_to_page", &pdfengine::PdfPage::deviceToPage, - py::arg("device_point"), py::arg("device_width"), py::arg("device_height"), py::arg("rotate") = 0); + py::arg("device_point"), py::arg("device_width"), py::arg("device_height"), py::arg("rotate") = 0) + .def("extract_display_list", [](const pdfengine::PdfPage& self) { + return get_or_throw(self.extractDisplayListJson()); + }) + .def("extract_image_xobject", [](const pdfengine::PdfPage& self, const std::string& name) { + auto res = get_or_throw(self.extractImageXObject(name)); + return py::bytes(reinterpret_cast(res.data()), res.size()); + }, py::arg("name")); py::class_>(m, "PdfDocument") .def_static("load_from_file", [](const std::string& path, const std::string& password) { diff --git a/engine/include/pdfengine/pdf_document.hpp b/engine/include/pdfengine/pdf_document.hpp index c44c709..5bb2c0b 100644 --- a/engine/include/pdfengine/pdf_document.hpp +++ b/engine/include/pdfengine/pdf_document.hpp @@ -209,6 +209,9 @@ public: [[nodiscard]] virtual DevicePoint pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept = 0; [[nodiscard]] virtual Point2D deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept = 0; + + [[nodiscard]] virtual std::expected extractDisplayListJson() const = 0; + [[nodiscard]] virtual std::expected, EngineError> extractImageXObject(const std::string& name) const = 0; }; namespace fonts::pdf_fonts { class Font; } diff --git a/engine/src/parser/pdfium_document.cpp b/engine/src/parser/pdfium_document.cpp index fba417b..46ccddb 100644 --- a/engine/src/parser/pdfium_document.cpp +++ b/engine/src/parser/pdfium_document.cpp @@ -19,6 +19,9 @@ #include "fonts/shaping/hb_shaper.hpp" #include "fonts/face/font_face.hpp" #include "decoration_builder.hpp" +#include "qpdf/qpdf_extractor.hpp" +#include "parser/lexer.hpp" +#include "parser/parser.hpp" #include #include @@ -4328,6 +4331,51 @@ std::expected, std::string> PdfiumDocume #endif } +std::expected PdfiumPage::extractDisplayListJson() const { +#ifdef PDFENGINE_WITH_PDFIUM + if (!ownerDoc_) return std::unexpected(EngineError::Unknown); + const auto& buffer = ownerDoc_->getMemoryBuffer(); + if (buffer.empty()) return std::unexpected(EngineError::FileNotFound); + + qpdf_layer::QpdfExtractor extractor; + auto stream = extractor.extractPageStreamFromMemory(buffer, pageIndex_); + if (!stream) return std::unexpected(EngineError::InvalidFormat); + + Lexer lexer(stream->decodedContent); + auto tokens = lexer.tokenize(); + ContentParser parser(tokens); + auto operations = parser.parse(); + + nlohmann::json j_array = nlohmann::json::array(); + for (const auto& op : operations) { + nlohmann::json j_op = nlohmann::json::object(); + j_op["op"] = op.op; + if (!op.operands.empty()) { + nlohmann::json j_args = nlohmann::json::array(); + for (const auto& arg : op.operands) { + if (arg->type == AstNodeType::Number) { + j_args.push_back(arg->numberValue); + } else if (arg->type == AstNodeType::Name || arg->type == AstNodeType::String) { + j_args.push_back(arg->stringValue); + } else { + j_args.push_back(""); + } + } + j_op["args"] = j_args; + } + j_array.push_back(j_op); + } + return j_array.dump(); +#else + return std::unexpected(EngineError::Unknown); +#endif +} + +std::expected, EngineError> PdfiumPage::extractImageXObject(const std::string& name) const { + (void)name; + return std::unexpected(EngineError::Unknown); // Stub for now +} + void PdfiumDocument::invalidateCaches() { { std::lock_guard lock(fontsMutex_); diff --git a/engine/src/parser/pdfium_document.hpp b/engine/src/parser/pdfium_document.hpp index d458d3f..af08d35 100644 --- a/engine/src/parser/pdfium_document.hpp +++ b/engine/src/parser/pdfium_document.hpp @@ -55,6 +55,9 @@ public: std::expected getGlyphWidth(const std::string& fontName, uint32_t charcode, double fontSize) const override; + std::expected extractDisplayListJson() const override; + std::expected, EngineError> extractImageXObject(const std::string& name) const override; + DevicePoint pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override; Point2D deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override; @@ -102,6 +105,8 @@ public: std::string lastReflowLayout() const { return lastReflowLayout_; } bool lastReflowOverflowed() const { return lastReflowOverflowed_; } + const std::vector& getMemoryBuffer() const { return memoryBuffer_; } + private: mutable std::string lastReflowLayout_; mutable bool lastReflowOverflowed_ = false; diff --git a/frontend/src/lib/gatewayService.ts b/frontend/src/lib/gatewayService.ts index 49078f2..beedd58 100644 --- a/frontend/src/lib/gatewayService.ts +++ b/frontend/src/lib/gatewayService.ts @@ -530,6 +530,16 @@ class GatewayService { 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 [ { diff --git a/frontend/src/viewer/CanvasLayer.tsx b/frontend/src/viewer/CanvasLayer.tsx index ecc3f61..c184e12 100644 --- a/frontend/src/viewer/CanvasLayer.tsx +++ b/frontend/src/viewer/CanvasLayer.tsx @@ -1,6 +1,8 @@ -import React, { useRef, useEffect } from 'react'; +import React, { useRef, useEffect, useState } from 'react'; +import { gatewayService } from '../lib/gatewayService'; interface CanvasLayerProps { + documentId: string; pageIndex: number; imageUrl: string; zoom: number; @@ -11,6 +13,7 @@ interface CanvasLayerProps { } export const CanvasLayer: React.FC = ({ + documentId, pageIndex, imageUrl, zoom, @@ -20,6 +23,13 @@ export const CanvasLayer: React.FC = ({ onRenderComplete, }) => { const canvasRef = useRef(null); + const [vectorOps, setVectorOps] = useState(null); + + useEffect(() => { + gatewayService.getPageDisplayList(documentId, pageIndex) + .then(ops => setVectorOps(ops)) + .catch(console.error); + }, [documentId, pageIndex]); useEffect(() => { const canvas = canvasRef.current; @@ -50,11 +60,49 @@ export const CanvasLayer: React.FC = ({ ctx.drawImage(img, 0, 0, width, height); ctx.restore(); + // Render vector ops on top + if (vectorOps && vectorOps.length > 0) { + ctx.save(); + ctx.translate(0, height); + ctx.scale(1, -1); + ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)'; + ctx.fillStyle = 'rgba(0, 0, 255, 0.2)'; + ctx.lineWidth = 1; + + ctx.beginPath(); + for (const op of vectorOps) { + if (op.op === 'm' && op.args && op.args.length === 2) { + ctx.moveTo(op.args[0], op.args[1]); + } else if (op.op === 'l' && op.args && op.args.length === 2) { + ctx.lineTo(op.args[0], op.args[1]); + } else if (op.op === 'c' && op.args && op.args.length === 6) { + ctx.bezierCurveTo(op.args[0], op.args[1], op.args[2], op.args[3], op.args[4], op.args[5]); + } else if (op.op === 're' && op.args && op.args.length === 4) { + ctx.rect(op.args[0], op.args[1], op.args[2], op.args[3]); + } else if (op.op === 'h') { + ctx.closePath(); + } else if (op.op === 'S' || op.op === 's') { + ctx.stroke(); + ctx.beginPath(); + } else if (op.op === 'f' || op.op === 'F') { + ctx.fill(); + ctx.beginPath(); + } else if (op.op === 'B' || op.op === 'b') { + ctx.fill(); + ctx.stroke(); + ctx.beginPath(); + } else if (op.op === 'n') { + ctx.beginPath(); + } + } + ctx.restore(); + } + onRenderComplete?.(pageIndex); }; img.src = imageUrl; - }, [imageUrl, zoom, rotation, width, height, onRenderComplete]); + }, [imageUrl, zoom, rotation, width, height, onRenderComplete, vectorOps]); return ( (({ {imageUrl ? ( <>