#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()); } } } PYBIND11_MODULE(pdfengine, m) { m.doc() = "Python bindings for the PdfEngine C++ Core SDK"; 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); 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("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); 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()); }) .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); 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) { // Raw embedded font bytes for the given run font; empty bytes when the font // is not embedded / not found (the gateway maps that to a 404 + CSS fallback). 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("apply_edits", [](pdfengine::PdfDocument& self, const std::string& editsJson) { get_or_throw(self.applyEdits(editsJson)); }, py::arg("edits_json")) .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()); }); }