#include #include #include #include namespace py = pybind11; namespace { void throw_on_error(pdfengine::EngineError err) { switch (err) { case pdfengine::EngineError::FileNotFound: PyErr_SetString(PyExc_FileNotFoundError, "PDF file not found"); throw py::error_already_set(); case pdfengine::EngineError::InvalidFormat: throw py::value_error("Invalid PDF format"); case pdfengine::EngineError::PasswordRequired: throw py::value_error("Password required to open this PDF"); case pdfengine::EngineError::InvalidPassword: throw py::value_error("Invalid password provided for this PDF"); case pdfengine::EngineError::PageOutOfBounds: throw py::index_error("Page index out of bounds"); case pdfengine::EngineError::RenderFailed: throw std::runtime_error("Failed to render PDF page"); case pdfengine::EngineError::WriteFailed: throw std::runtime_error("Failed to write PDF data"); default: throw std::runtime_error("Unknown PDF engine error"); } } template T get_or_throw(std::expected&& res) { if (!res.has_value()) { throw_on_error(res.error()); } return std::move(res.value()); } void get_or_throw(std::expected&& res) { if (!res.has_value()) { throw_on_error(res.error()); } } } #include #include #include #include #include #include #include #include static constexpr double kTjSpaceKern = -500.0; class StreamEditor { public: StreamEditor(const std::string& filepath) : filepath_(filepath) {} py::list extract_text_objects(int page_index) { pdfengine::qpdf_layer::QpdfExtractor extractor; auto stream = extractor.extractPageStream(filepath_, page_index); if (!stream.has_value()) { throw std::runtime_error("Failed to extract page stream"); } pdfengine::Lexer lexer(stream->decodedContent); auto tokens = lexer.tokenize(); pdfengine::ContentParser parser(tokens); pdfengine::ContentBuilder builder; auto objects = builder.build(parser.parse()); py::list result; for (const auto& obj : objects) { if (obj->getType() == pdfengine::ContentObjectType::Text) { auto* textObj = static_cast(obj.get()); py::dict d; d["text"] = py::bytes(textObj->text); d["fontName"] = textObj->fontName; d["fontSize"] = textObj->fontSize; py::list tm; for (int i = 0; i < 6; ++i) { tm.append(textObj->tm[i]); } d["tm"] = tm; result.append(d); } } return result; } bool replace_text_object(int page_index, int object_index, const py::bytes& new_text_bytes, const std::string& dest_path) { std::string new_text = new_text_bytes; pdfengine::qpdf_layer::QpdfExtractor extractor; auto stream = extractor.extractPageStream(filepath_, page_index); if (!stream.has_value()) return false; pdfengine::Lexer lexer(stream->decodedContent); auto tokens = lexer.tokenize(); pdfengine::ContentParser parser(tokens); auto operations = parser.parse(); int textCount = 0; bool modified = false; for (auto& op : operations) { if (op.op == "Tj" || op.op == "'") { if (op.operands.empty()) continue; auto& strNode = op.operands.back(); if (strNode->type == pdfengine::AstNodeType::String || strNode->type == pdfengine::AstNodeType::HexString) { if (textCount == object_index) { strNode->type = pdfengine::AstNodeType::String; strNode->stringValue = new_text; modified = true; break; } textCount++; } } else if (op.op == "TJ") { if (op.operands.empty()) continue; auto& arrNode = op.operands.back(); if (arrNode->type == pdfengine::AstNodeType::Array) { std::string combinedText; for (const auto& item : arrNode->arrayItems) { if (item->type == pdfengine::AstNodeType::String) { combinedText += item->stringValue; } else if (item->type == pdfengine::AstNodeType::HexString) { combinedText += std::string(item->bytesValue.begin(), item->bytesValue.end()); } else if (item->type == pdfengine::AstNodeType::Number) { if (item->numberValue < kTjSpaceKern) combinedText += " "; } } if (!combinedText.empty()) { if (textCount == object_index) { bool redistributed = false; if (new_text.size() == combinedText.size()) { std::vector> assign; size_t pos = 0; bool ok = true; for (const auto& item : arrNode->arrayItems) { if (item->type == pdfengine::AstNodeType::String || item->type == pdfengine::AstNodeType::HexString) { size_t L = (item->type == pdfengine::AstNodeType::HexString) ? item->bytesValue.size() : item->stringValue.size(); assign.emplace_back(item.get(), new_text.substr(pos, L)); pos += L; } else if (item->type == pdfengine::AstNodeType::Number && item->numberValue < kTjSpaceKern) { if (pos >= new_text.size() || new_text[pos] != ' ') { ok = false; break; } pos += 1; } } if (ok && pos == new_text.size()) { for (auto& [node, content] : assign) { node->type = pdfengine::AstNodeType::String; node->stringValue = content; } redistributed = true; } } if (!redistributed) { arrNode->arrayItems.clear(); auto newStrNode = std::make_shared(pdfengine::AstNodeType::String); newStrNode->stringValue = new_text; arrNode->arrayItems.push_back(std::move(newStrNode)); } modified = true; break; } textCount++; } } } } if (!modified) return false; pdfengine::AstSerializer astSerializer; std::string newRawStream = astSerializer.serialize(operations); pdfengine::qpdf_layer::QpdfWriter writer; auto res = writer.replacePageStreamAndSave(filepath_, dest_path, page_index, newRawStream); return res.has_value(); } private: std::string filepath_; }; PYBIND11_MODULE(pdfengine, m) { m.doc() = "Python bindings for the PdfEngine C++ Core SDK"; py::class_(m, "StreamEditor") .def(py::init(), py::arg("filepath")) .def("extract_text_objects", &StreamEditor::extract_text_objects, py::arg("page_index")) .def("replace_text_object", &StreamEditor::replace_text_object, py::arg("page_index"), py::arg("object_index"), py::arg("new_text"), py::arg("dest_path")); m.def("engine_version", &pdfengine::engineVersion, "Get the engine version string"); m.def("engine_build_info", &pdfengine::engineBuildInfo, "Get the engine build info string"); m.def("engine_has_pdfium", &pdfengine::engineHasPdfium, "Check if the engine was built with PDFium support"); m.def("engine_has_skia", &pdfengine::engineHasSkia, "Check if the engine was built with Skia support"); py::class_(m, "Point2D") .def(py::init(), py::arg("x") = 0.0, py::arg("y") = 0.0) .def_readwrite("x", &pdfengine::Point2D::x) .def_readwrite("y", &pdfengine::Point2D::y) .def("__repr__", [](const pdfengine::Point2D& self) { return "Point2D(x=" + std::to_string(self.x) + ", y=" + std::to_string(self.y) + ")"; }); py::class_(m, "DevicePoint") .def(py::init(), py::arg("x") = 0, py::arg("y") = 0) .def_readwrite("x", &pdfengine::DevicePoint::x) .def_readwrite("y", &pdfengine::DevicePoint::y) .def("__repr__", [](const pdfengine::DevicePoint& self) { return "DevicePoint(x=" + std::to_string(self.x) + ", y=" + std::to_string(self.y) + ")"; }); py::class_(m, "DocumentMetadata") .def_readonly("title", &pdfengine::DocumentMetadata::title) .def_readonly("author", &pdfengine::DocumentMetadata::author) .def_readonly("creator", &pdfengine::DocumentMetadata::creator) .def_readonly("producer", &pdfengine::DocumentMetadata::producer) .def_readonly("creation_date", &pdfengine::DocumentMetadata::creationDate) .def_readonly("modification_date", &pdfengine::DocumentMetadata::modificationDate) .def("__repr__", [](const pdfengine::DocumentMetadata& self) { return "DocumentMetadata(title='" + self.title + "', author='" + self.author + "')"; }); py::class_(m, "DocumentPermissions") .def_readonly("is_encrypted", &pdfengine::DocumentPermissions::isEncrypted) .def_readonly("encryption", &pdfengine::DocumentPermissions::encryption) .def_readonly("security_revision", &pdfengine::DocumentPermissions::securityRevision) .def_readonly("owner_unlocked", &pdfengine::DocumentPermissions::ownerUnlocked) .def_readonly("can_print", &pdfengine::DocumentPermissions::canPrint) .def_readonly("can_print_high_res", &pdfengine::DocumentPermissions::canPrintHighRes) .def_readonly("can_modify", &pdfengine::DocumentPermissions::canModify) .def_readonly("can_copy", &pdfengine::DocumentPermissions::canCopy) .def_readonly("can_annotate", &pdfengine::DocumentPermissions::canAnnotate) .def_readonly("can_fill_forms", &pdfengine::DocumentPermissions::canFillForms) .def_readonly("can_extract_for_accessibility", &pdfengine::DocumentPermissions::canExtractForAccessibility) .def_readonly("can_assemble", &pdfengine::DocumentPermissions::canAssemble); py::class_(m, "PageImage") .def_readonly("width", &pdfengine::PageImage::width) .def_readonly("height", &pdfengine::PageImage::height) .def_property_readonly("data", [](const pdfengine::PageImage& self) { return py::bytes(reinterpret_cast(self.data.data()), self.data.size()); }); py::class_(m, "FontInfo") .def_readonly("font_name", &pdfengine::FontInfo::fontName) .def_readonly("type", &pdfengine::FontInfo::type) .def_readonly("is_embedded", &pdfengine::FontInfo::isEmbedded) .def_readonly("is_subset", &pdfengine::FontInfo::isSubset) .def_readonly("is_vertical", &pdfengine::FontInfo::isVertical) .def_readonly("encoding", &pdfengine::FontInfo::encoding) .def_readonly("has_to_unicode", &pdfengine::FontInfo::hasToUnicode) .def_readonly("cmap_name", &pdfengine::FontInfo::cmapName) .def_readonly("cid_system_info", &pdfengine::FontInfo::cidSystemInfo) .def_readonly("subset_tag", &pdfengine::FontInfo::subsetTag) .def_readonly("source_type", &pdfengine::FontInfo::sourceType) .def_readonly("substituted_from", &pdfengine::FontInfo::substitutedFrom) .def_readonly("substituted_to", &pdfengine::FontInfo::substitutedTo) .def_readonly("normalized_family", &pdfengine::FontInfo::normalizedFamily) .def_readonly("internal_font_id", &pdfengine::FontInfo::internalFontId) .def_readonly("flags", &pdfengine::FontInfo::flags) .def_readonly("ascent", &pdfengine::FontInfo::ascent) .def_readonly("descent", &pdfengine::FontInfo::descent) .def_readonly("cap_height", &pdfengine::FontInfo::capHeight) .def("__repr__", [](const pdfengine::FontInfo& self) { 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) .def_readonly("page_object_index", &pdfengine::Glyph::pageObjectIndex); 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) .def_readonly("object_indices", &pdfengine::TextRun::objectIndices) .def_readonly("fill_color", &pdfengine::TextRun::fillColor) .def_readonly("para_id", &pdfengine::TextRun::paraId) .def_readonly("font_fidelity", &pdfengine::TextRun::fontFidelity); 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, "AnnotationInfo") .def_readonly("id", &pdfengine::PdfPage::AnnotationInfo::id) .def_readonly("type", &pdfengine::PdfPage::AnnotationInfo::type) .def_readonly("x", &pdfengine::PdfPage::AnnotationInfo::x) .def_readonly("y", &pdfengine::PdfPage::AnnotationInfo::y) .def_readonly("width", &pdfengine::PdfPage::AnnotationInfo::width) .def_readonly("height", &pdfengine::PdfPage::AnnotationInfo::height) .def_readonly("color", &pdfengine::PdfPage::AnnotationInfo::color) .def_readonly("author", &pdfengine::PdfPage::AnnotationInfo::author) .def_readonly("content", &pdfengine::PdfPage::AnnotationInfo::content) .def_readonly("timestamp", &pdfengine::PdfPage::AnnotationInfo::timestamp) .def_readonly("page_index", &pdfengine::PdfPage::AnnotationInfo::pageIndex) .def_readonly("thickness", &pdfengine::PdfPage::AnnotationInfo::thickness) .def_readonly("paths", &pdfengine::PdfPage::AnnotationInfo::paths) .def_readonly("field_name", &pdfengine::PdfPage::AnnotationInfo::fieldName) .def_readonly("field_value", &pdfengine::PdfPage::AnnotationInfo::fieldValue) .def_readonly("field_type", &pdfengine::PdfPage::AnnotationInfo::fieldType) .def_readonly("field_flags", &pdfengine::PdfPage::AnnotationInfo::fieldFlags) .def_readonly("field_options", &pdfengine::PdfPage::AnnotationInfo::fieldOptions) .def_property_readonly("quad_points", [](const pdfengine::PdfPage::AnnotationInfo& self) { py::list out; for (const auto& quad : self.quadPoints) { py::list quad_list; for (const auto& pt : quad) { py::dict d; d["x"] = pt.x; d["y"] = pt.y; quad_list.append(d); } out.append(quad_list); } return out; }); 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("render_region_raw", [](const pdfengine::PdfPage& self, int dpi, double y_top_pt, double height_pt) { auto img = get_or_throw(self.renderRegionRaw(dpi, y_top_pt, height_pt)); return py::make_tuple(img.width, img.height, py::bytes(reinterpret_cast(img.data.data()), img.data.size())); }, py::arg("dpi"), py::arg("y_top_pt"), py::arg("height_pt") = 0.0) .def("render_tile", [](const pdfengine::PdfPage& self, int dpi, double xPt, double yPt, double wPt, double hPt) { auto img = get_or_throw(self.renderTile(dpi, xPt, yPt, wPt, hPt)); return py::make_tuple(img.width, img.height, py::bytes(reinterpret_cast(img.data.data()), img.data.size())); }, py::arg("dpi"), py::arg("xPt"), py::arg("yPt"), py::arg("wPt"), py::arg("hPt")) .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()); }) .def("extract_annotations_text", [](const pdfengine::PdfPage& self) { return get_or_throw(self.extractAnnotationsText()); }) .def("extract_annotations", [](const pdfengine::PdfPage& self) { return get_or_throw(self.extractAnnotations()); }) .def("extract_text_with_bounds", [](const pdfengine::PdfPage& self) { auto res = get_or_throw(self.extractTextWithBounds()); py::list py_list; for (const auto& glyph : res) { py::dict d; d["text"] = glyph.text; d["x"] = glyph.x; d["y"] = glyph.y; d["w"] = glyph.w; d["h"] = glyph.h; d["fontSize"] = glyph.fontSize; py_list.append(d); } return py_list; }) .def("ordered_glyphs", [](const pdfengine::PdfPage& self) { auto res = get_or_throw(self.orderedGlyphs()); py::list py_list; for (const auto& g : res) { py::dict d; d["text"] = g.text; d["x"] = g.x; d["y"] = g.y; d["w"] = g.w; d["h"] = g.h; d["fontSize"] = g.fontSize; py_list.append(d); } return py_list; }) .def("hit_glyph", [](const pdfengine::PdfPage& self, double x, double y) { auto hit = get_or_throw(self.hitGlyph(x, y)); py::dict d; d["glyphIndex"] = hit.glyphIndex; d["caret"] = hit.caret; d["line"] = hit.line; return d; }, py::arg("x"), py::arg("y")) .def("select_range", [](const pdfengine::PdfPage& self, double ax, double ay, double bx, double by) { auto sel = get_or_throw(self.selectRange(ax, ay, bx, by)); py::dict d; d["startGlyph"] = sel.startGlyph; d["endGlyph"] = sel.endGlyph; d["text"] = sel.text; py::list rects; for (const auto& r : sel.rects) { py::dict rd; rd["x"] = r.x; rd["y"] = r.y; rd["w"] = r.w; rd["h"] = r.h; rects.append(rd); } d["rects"] = rects; return d; }, py::arg("ax"), py::arg("ay"), py::arg("bx"), py::arg("by")) .def("get_fonts", [](const pdfengine::PdfPage& self) { return get_or_throw(self.getFonts()); }) .def("get_glyph_width", [](const pdfengine::PdfPage& self, const std::string& fontName, uint32_t charcode, double fontSize) { return get_or_throw(self.getGlyphWidth(fontName, charcode, fontSize)); }, py::arg("font_name"), py::arg("charcode"), py::arg("font_size")) .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) .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) { return get_or_throw(pdfengine::PdfDocument::loadFromFile(path, password)); }, py::arg("path"), py::arg("password") = "") .def_static("load_from_memory", [](const py::bytes& bytes, const std::string& password) { std::string_view sv = bytes; std::vector data(sv.begin(), sv.end()); return get_or_throw(pdfengine::PdfDocument::loadFromMemory(data, password)); }, py::arg("data"), py::arg("password") = "") .def_property_readonly("page_count", &pdfengine::PdfDocument::pageCount) .def_property_readonly("metadata", &pdfengine::PdfDocument::metadata) .def_property_readonly("permissions", &pdfengine::PdfDocument::permissions) .def("extract_outline", [](const pdfengine::PdfDocument& self) { auto res = get_or_throw(self.extractOutline()); py::list out; for (const auto& item : res) { py::dict d; d["title"] = item.title; d["pageIndex"] = item.pageIndex; d["level"] = item.level; out.append(d); } return out; }) .def("get_page", [](pdfengine::PdfDocument& self, int pageIndex) { return get_or_throw(self.getPage(pageIndex)); }, py::arg("page_index")) .def("get_fonts", [](const pdfengine::PdfDocument& self, int start_page, int end_page) { return get_or_throw(self.getFonts(start_page, end_page)); }, py::arg("start_page") = 0, py::arg("end_page") = -1) .def("get_font_data", [](const pdfengine::PdfDocument& self, const std::string& internal_font_id) { auto res = self.getFontData(internal_font_id); if (!res || res->empty()) { return py::bytes(); } return py::bytes(reinterpret_cast(res->data()), res->size()); }, py::arg("internal_font_id")) .def("get_reconstructed_font_data", [](pdfengine::PdfDocument& self, const std::string& internal_font_id) { auto res = self.getReconstructedFontData(internal_font_id); if (!res || res->empty()) { return py::bytes(); } return py::bytes(reinterpret_cast(res->data()), res->size()); }, py::arg("internal_font_id")) .def("apply_edits", [](pdfengine::PdfDocument& self, const std::string& editsJson) { auto regions = get_or_throw(self.applyEdits(editsJson)); py::list py_regions; for (const auto& r : regions) { py::dict d; d["pageIndex"] = r.pageIndex; d["x"] = r.x; d["y"] = r.y; d["width"] = r.width; d["height"] = r.height; py_regions.append(d); } return py_regions; }, py::arg("edits_json")) .def("last_reflow_layout", [](const pdfengine::PdfDocument& self) { return self.lastReflowLayout(); }) .def("save_incremental", [](const pdfengine::PdfDocument& self) { std::vector res = get_or_throw(self.saveIncremental()); return py::bytes(reinterpret_cast(res.data()), res.size()); }) .def("save_full", [](const pdfengine::PdfDocument& self) { std::vector res = get_or_throw(self.saveFull()); return py::bytes(reinterpret_cast(res.data()), res.size()); }) .def("save_full_for_export", [](const pdfengine::PdfDocument& self) { std::vector res = get_or_throw(self.saveFullForExport()); return py::bytes(reinterpret_cast(res.data()), res.size()); }); }