Files
pdf/bindings/python/pdfengine_py.cpp
T

766 lines
36 KiB
C++
Raw Normal View History

2026-05-22 15:47:48 +05:30
#include <pdfengine/pdf_document.hpp>
#include <pdfengine/pdf_engine.hpp>
2026-08-10 14:21:54 +05:30
#include <spdlog/spdlog.h>
2026-08-06 12:12:56 +05:30
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
2026-05-22 15:47:48 +05:30
namespace py = pybind11;
namespace {
void throw_on_error(pdfengine::EngineError err) {
switch (err) {
2026-08-06 12:12:56 +05:30
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");
2026-05-22 15:47:48 +05:30
}
}
2026-08-06 12:12:56 +05:30
template <typename T> T get_or_throw(std::expected<T, pdfengine::EngineError>&& res) {
2026-05-22 15:47:48 +05:30
if (!res.has_value()) {
throw_on_error(res.error());
}
return std::move(res.value());
}
void get_or_throw(std::expected<void, pdfengine::EngineError>&& res) {
if (!res.has_value()) {
throw_on_error(res.error());
}
}
2026-08-06 12:12:56 +05:30
} // namespace
2026-05-22 15:47:48 +05:30
2026-08-06 12:12:56 +05:30
#include <parser/content_builder.hpp>
#include <parser/lexer.hpp>
#include <parser/parser.hpp>
2026-06-16 19:06:48 +05:30
#include <pdfengine/content_object.hpp>
2026-08-07 19:09:44 +05:30
#include <pdfengine/ocr/ocr_coordinator.hpp>
2026-06-16 19:06:48 +05:30
#include <qpdf/qpdf_extractor.hpp>
#include <qpdf/qpdf_writer.hpp>
#include <serializer/ast_serializer.hpp>
2026-08-06 12:12:56 +05:30
#include <serializer/content_serializer.hpp>
2026-06-16 19:06:48 +05:30
static constexpr double kTjSpaceKern = -500.0;
2026-06-16 19:06:48 +05:30
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");
}
2026-08-06 12:12:56 +05:30
2026-06-16 19:06:48 +05:30
pdfengine::Lexer lexer(stream->decodedContent);
auto tokens = lexer.tokenize();
pdfengine::ContentParser parser(tokens);
pdfengine::ContentBuilder builder;
2026-08-06 12:12:56 +05:30
2026-06-16 19:06:48 +05:30
auto objects = builder.build(parser.parse());
2026-08-06 12:12:56 +05:30
2026-06-16 19:06:48 +05:30
py::list result;
for (const auto& obj : objects) {
if (obj->getType() == pdfengine::ContentObjectType::Text) {
auto* textObj = static_cast<pdfengine::TextObject*>(obj.get());
py::dict d;
2026-06-16 19:27:32 +05:30
d["text"] = py::bytes(textObj->text);
2026-06-16 19:06:48 +05:30
d["fontName"] = textObj->fontName;
d["fontSize"] = textObj->fontSize;
2026-08-06 12:12:56 +05:30
2026-06-16 19:06:48 +05:30
py::list tm;
for (int i = 0; i < 6; ++i) {
tm.append(textObj->tm[i]);
}
d["tm"] = tm;
result.append(d);
}
}
return result;
}
2026-08-06 12:12:56 +05:30
bool replace_text_object(int page_index, int object_index, const py::bytes& new_text_bytes,
const std::string& dest_path) {
2026-06-16 19:27:32 +05:30
std::string new_text = new_text_bytes;
2026-06-16 19:06:48 +05:30
pdfengine::qpdf_layer::QpdfExtractor extractor;
auto stream = extractor.extractPageStream(filepath_, page_index);
2026-08-06 12:12:56 +05:30
if (!stream.has_value())
return false;
2026-06-16 19:06:48 +05:30
pdfengine::Lexer lexer(stream->decodedContent);
auto tokens = lexer.tokenize();
pdfengine::ContentParser parser(tokens);
2026-06-17 11:03:47 +05:30
auto operations = parser.parse();
2026-08-06 12:12:56 +05:30
2026-06-16 19:06:48 +05:30
int textCount = 0;
bool modified = false;
2026-06-17 11:03:47 +05:30
for (auto& op : operations) {
if (op.op == "Tj" || op.op == "'") {
2026-08-06 12:12:56 +05:30
if (op.operands.empty())
continue;
2026-06-17 11:03:47 +05:30
auto& strNode = op.operands.back();
2026-08-06 12:12:56 +05:30
if (strNode->type == pdfengine::AstNodeType::String ||
strNode->type == pdfengine::AstNodeType::HexString) {
2026-06-17 11:03:47 +05:30
if (textCount == object_index) {
strNode->type = pdfengine::AstNodeType::String;
strNode->stringValue = new_text;
modified = true;
break;
}
textCount++;
}
} else if (op.op == "TJ") {
2026-08-06 12:12:56 +05:30
if (op.operands.empty())
continue;
2026-06-17 11:03:47 +05:30
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) {
2026-08-06 12:12:56 +05:30
combinedText +=
std::string(item->bytesValue.begin(), item->bytesValue.end());
2026-06-17 11:03:47 +05:30
} else if (item->type == pdfengine::AstNodeType::Number) {
2026-08-06 12:12:56 +05:30
if (item->numberValue < kTjSpaceKern)
combinedText += " ";
2026-06-17 11:03:47 +05:30
}
}
if (!combinedText.empty()) {
if (textCount == object_index) {
bool redistributed = false;
if (new_text.size() == combinedText.size()) {
std::vector<std::pair<pdfengine::AstNode*, std::string>> assign;
2026-08-06 12:12:56 +05:30
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)
2026-08-06 12:12:56 +05:30
? 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) {
2026-08-06 12:12:56 +05:30
if (pos >= new_text.size() || new_text[pos] != ' ') {
ok = false;
break;
}
2026-06-18 16:48:29 +05:30
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();
2026-08-06 12:12:56 +05:30
auto newStrNode = std::make_shared<pdfengine::AstNode>(
pdfengine::AstNodeType::String);
newStrNode->stringValue = new_text;
arrNode->arrayItems.push_back(std::move(newStrNode));
}
2026-06-17 11:03:47 +05:30
modified = true;
break;
}
textCount++;
}
2026-06-16 19:06:48 +05:30
}
}
}
2026-08-06 12:12:56 +05:30
if (!modified)
return false;
2026-06-16 19:06:48 +05:30
pdfengine::AstSerializer astSerializer;
2026-06-17 11:03:47 +05:30
std::string newRawStream = astSerializer.serialize(operations);
2026-08-06 12:12:56 +05:30
2026-06-16 19:06:48 +05:30
pdfengine::qpdf_layer::QpdfWriter writer;
auto res = writer.replacePageStreamAndSave(filepath_, dest_path, page_index, newRawStream);
return res.has_value();
}
private:
std::string filepath_;
};
2026-05-22 15:47:48 +05:30
PYBIND11_MODULE(pdfengine, m) {
m.doc() = "Python bindings for the PdfEngine C++ Core SDK";
2026-06-16 19:06:48 +05:30
py::class_<StreamEditor>(m, "StreamEditor")
.def(py::init<const std::string&>(), py::arg("filepath"))
.def("extract_text_objects", &StreamEditor::extract_text_objects, py::arg("page_index"))
2026-08-06 12:12:56 +05:30
.def("replace_text_object", &StreamEditor::replace_text_object, py::arg("page_index"),
py::arg("object_index"), py::arg("new_text"), py::arg("dest_path"));
2026-06-16 19:06:48 +05:30
2026-05-22 15:47:48 +05:30
m.def("engine_version", &pdfengine::engineVersion, "Get the engine version string");
m.def("engine_build_info", &pdfengine::engineBuildInfo, "Get the engine build info string");
2026-08-06 12:12:56 +05:30
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");
2026-05-22 15:47:48 +05:30
m.def(
"protect_pdf",
[](const py::bytes& input_bytes,
const std::string& user_password,
const std::string& owner_password,
const py::dict& perms_dict) {
if (user_password.empty()) {
throw py::value_error("User password cannot be empty");
}
std::string_view sv = input_bytes;
std::vector<uint8_t> data(sv.begin(), sv.end());
pdfengine::qpdf_layer::PdfEncryptionOptions opts;
opts.userPassword = user_password;
opts.ownerPassword = owner_password.empty() ? user_password : owner_password;
auto get_bool = [&](const char* key, bool dflt) {
if (perms_dict.contains(key) && !perms_dict[key].is_none()) {
try { return perms_dict[key].cast<bool>(); } catch (...) {}
}
return dflt;
};
opts.allowPrint = get_bool("canPrint", true);
opts.allowPrintHighRes = get_bool("canPrintHighRes", true);
opts.allowModify = get_bool("canModify", true);
opts.allowCopy = get_bool("canCopy", true);
opts.allowAnnotate = get_bool("canAnnotate", true);
opts.allowFillForms = get_bool("canFillForms", true);
opts.allowAccessibility = get_bool("canExtractForAccessibility", true);
opts.allowAssemble = get_bool("canAssemble", true);
opts.keyLengthBits = 256;
pdfengine::qpdf_layer::QpdfWriter writer;
auto res = writer.encryptPdf(data, opts);
if (!res.has_value()) {
throw std::runtime_error("Encryption failed");
}
const auto& out_bytes = res.value();
return py::bytes(reinterpret_cast<const char*>(out_bytes.data()), out_bytes.size());
},
py::arg("input_bytes"),
py::arg("user_password"),
py::arg("owner_password") = "",
py::arg("permissions") = py::dict(),
"Encrypt raw PDF bytes with AES-256 and custom permissions"
);
m.def(
"unlock_pdf",
[](const py::bytes& input_bytes, const std::string& password) {
std::string_view sv = input_bytes;
std::vector<uint8_t> data(sv.begin(), sv.end());
pdfengine::qpdf_layer::QpdfWriter writer;
auto res = writer.unlockPdf(data, password);
if (!res.has_value()) {
throw std::runtime_error("Unlock failed");
}
const auto& out_bytes = res.value();
return py::bytes(reinterpret_cast<const char*>(out_bytes.data()), out_bytes.size());
},
py::arg("input_bytes"),
py::arg("password") = "",
"Decrypt raw PDF bytes with QPDF"
);
2026-05-22 15:47:48 +05:30
py::class_<pdfengine::Point2D>(m, "Point2D")
.def(py::init<double, double>(), 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_<pdfengine::DevicePoint>(m, "DevicePoint")
.def(py::init<int, int>(), 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) {
2026-08-06 12:12:56 +05:30
return "DevicePoint(x=" + std::to_string(self.x) + ", y=" + std::to_string(self.y) +
")";
2026-05-22 15:47:48 +05:30
});
py::class_<pdfengine::DocumentMetadata>(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_<pdfengine::DocumentPermissions>(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)
2026-08-06 12:12:56 +05:30
.def_readonly("can_extract_for_accessibility",
&pdfengine::DocumentPermissions::canExtractForAccessibility)
.def_readonly("can_assemble", &pdfengine::DocumentPermissions::canAssemble);
2026-05-22 15:47:48 +05:30
py::class_<pdfengine::PageImage>(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<const char*>(self.data.data()), self.data.size());
});
2026-05-26 15:55:48 +05:30
py::class_<pdfengine::FontInfo>(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) {
2026-08-06 12:12:56 +05:30
return "FontInfo(font_name='" + self.fontName + "', type='" + self.type +
"', is_embedded=" + (self.isEmbedded ? "True" : "False") + ")";
2026-05-26 15:55:48 +05:30
});
2026-06-08 16:28:10 +05:30
py::class_<pdfengine::Glyph>(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)
2026-06-11 18:19:26 +05:30
.def_readonly("angle", &pdfengine::Glyph::angle)
.def_readonly("page_object_index", &pdfengine::Glyph::pageObjectIndex);
2026-06-08 16:28:10 +05:30
py::class_<pdfengine::TextRun>(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)
2026-08-07 19:09:44 +05:30
.def_readonly("is_embedded_font", &pdfengine::TextRun::isEmbeddedFont)
.def_readonly("is_predicted_font", &pdfengine::TextRun::isPredictedFont)
2026-06-08 16:28:10 +05:30
.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)
2026-06-11 18:19:26 +05:30
.def_readonly("h", &pdfengine::TextRun::h)
.def_readonly("object_indices", &pdfengine::TextRun::objectIndices)
2026-06-18 16:48:29 +05:30
.def_readonly("fill_color", &pdfengine::TextRun::fillColor)
.def_readonly("para_id", &pdfengine::TextRun::paraId)
.def_readonly("font_fidelity", &pdfengine::TextRun::fontFidelity);
2026-06-08 16:28:10 +05:30
py::class_<pdfengine::TextLine>(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_<pdfengine::Paragraph>(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_<pdfengine::PageModel>(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);
2026-06-09 10:35:12 +05:30
py::class_<pdfengine::PdfPage::AnnotationInfo>(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)
2026-06-10 15:22:34 +05:30
.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;
});
2026-06-09 10:35:12 +05:30
2026-05-22 15:47:48 +05:30
py::class_<pdfengine::PdfPage, std::shared_ptr<pdfengine::PdfPage>>(m, "PdfPage")
.def_property_readonly("width", &pdfengine::PdfPage::width)
.def_property_readonly("height", &pdfengine::PdfPage::height)
2026-08-06 12:12:56 +05:30
.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<const char*>(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<const char*>(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;
2026-08-06 12:12:56 +05:30
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;
2026-08-06 12:12:56 +05:30
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<const char*>(res.data()), res.size());
},
py::arg("name"));
2026-05-22 15:47:48 +05:30
py::class_<pdfengine::PdfDocument, std::shared_ptr<pdfengine::PdfDocument>>(m, "PdfDocument")
2026-08-06 12:12:56 +05:30
.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<uint8_t> data(sv.begin(), sv.end());
return get_or_throw(pdfengine::PdfDocument::loadFromMemory(data, password));
},
py::arg("data"), py::arg("password") = "")
2026-05-22 15:47:48 +05:30
.def_property_readonly("page_count", &pdfengine::PdfDocument::pageCount)
.def_property_readonly("metadata", &pdfengine::PdfDocument::metadata)
.def_property_readonly("permissions", &pdfengine::PdfDocument::permissions)
2026-08-06 12:12:56 +05:30
.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<const char*>(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<const char*>(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<uint8_t> res = get_or_throw(self.saveIncremental());
return py::bytes(reinterpret_cast<const char*>(res.data()), res.size());
})
.def("save_full",
[](const pdfengine::PdfDocument& self) {
std::vector<uint8_t> res = get_or_throw(self.saveFull());
return py::bytes(reinterpret_cast<const char*>(res.data()), res.size());
})
2026-06-30 21:18:46 +05:30
.def("save_full_for_export", [](const pdfengine::PdfDocument& self) {
std::vector<uint8_t> res = get_or_throw(self.saveFullForExport());
return py::bytes(reinterpret_cast<const char*>(res.data()), res.size());
2026-05-22 15:47:48 +05:30
});
2026-08-07 19:09:44 +05:30
py::class_<pdfengine::ocr::OCRCoordinator>(m, "OCRCoordinator")
.def(py::init<>())
.def(
"process_document",
[](const pdfengine::ocr::OCRCoordinator& self, int pageIndex, double imgW, double imgH,
double pdfW, double pdfH, const py::list& lines_list) {
std::vector<pdfengine::document::RawOCRLine> cpp_lines;
2026-08-10 14:21:54 +05:30
auto get_str_safe = [](py::dict d, const char* key, const std::string& fallback = "") -> std::string {
if (d.contains(key) && !d[key].is_none()) {
try { return d[key].cast<std::string>(); } catch (...) {}
}
return fallback;
};
auto get_double_safe = [](py::dict d, const char* key, double fallback = 0.0) -> double {
if (d.contains(key) && !d[key].is_none()) {
try { return d[key].cast<double>(); } catch (...) {}
}
return fallback;
};
auto get_int_safe = [](py::dict d, const char* key, int fallback = 0) -> int {
if (d.contains(key) && !d[key].is_none()) {
try { return d[key].cast<int>(); } catch (...) {}
}
return fallback;
};
auto get_bool_safe = [](py::dict d, const char* key, bool fallback = false) -> bool {
if (d.contains(key) && !d[key].is_none()) {
try { return d[key].cast<bool>(); } catch (...) {}
}
return fallback;
};
2026-08-07 19:09:44 +05:30
for (auto item : lines_list) {
2026-08-10 14:21:54 +05:30
if (item.is_none()) continue;
2026-08-07 19:09:44 +05:30
py::dict d = item.cast<py::dict>();
pdfengine::document::RawOCRLine line;
2026-08-10 14:21:54 +05:30
line.text = get_str_safe(d, "text", "");
line.confidence = get_double_safe(d, "confidence", 0.0);
line.fontSize = get_double_safe(d, "fontSize", 12.0);
line.fontName = get_str_safe(d, "fontName", "Helvetica");
line.fontId = get_str_safe(d, "fontId", "");
line.fontFace = get_str_safe(d, "fontFace", "");
line.fontWeight = get_int_safe(d, "fontWeight", 400);
line.fontStyle = get_str_safe(d, "fontStyle", "normal");
line.isBold = get_bool_safe(d, "isBold", false);
line.isItalic = get_bool_safe(d, "isItalic", false);
line.lineSpacing = get_double_safe(d, "lineSpacing", 1.2);
line.letterSpacing = get_double_safe(d, "letterSpacing", 0.0);
if (d.contains("box") && !d["box"].is_none()) {
2026-08-07 19:09:44 +05:30
py::dict box = d["box"].cast<py::dict>();
2026-08-10 14:21:54 +05:30
line.x = get_double_safe(box, "x", 0.0);
line.y = get_double_safe(box, "y", 0.0);
line.width = get_double_safe(box, "width", 0.0);
line.height = get_double_safe(box, "height", 0.0);
2026-08-07 19:09:44 +05:30
} else {
2026-08-10 14:21:54 +05:30
line.x = get_double_safe(d, "x", 0.0);
line.y = get_double_safe(d, "y", 0.0);
line.width = get_double_safe(d, "width", 0.0);
line.height = get_double_safe(d, "height", 0.0);
2026-08-07 19:09:44 +05:30
}
2026-08-10 14:21:54 +05:30
spdlog::info(
"[OCR_CPP_RUN] text='{}' fontName='{}' fontId='{}' "
"fontFace='{}' fontWeight={} fontStyle='{}'",
line.text,
line.fontName.empty() ? "<empty>" : line.fontName,
line.fontId.empty() ? "<empty>" : line.fontId,
line.fontFace.empty() ? "<empty>" : line.fontFace,
line.fontWeight,
line.fontStyle.empty() ? "<empty>" : line.fontStyle
);
2026-08-07 19:09:44 +05:30
cpp_lines.push_back(line);
}
return self.processDocument(pageIndex, imgW, imgH, pdfW, pdfH, cpp_lines);
},
py::arg("page_index"), py::arg("img_w"), py::arg("img_h"), py::arg("pdf_w"),
py::arg("pdf_h"), py::arg("lines"));
2026-05-22 15:47:48 +05:30
}