47 Commits
Author SHA1 Message Date
saqib mir 4256ac8128 fix the disable feature and remove the block in the new pdf 2026-08-22 15:29:16 +05:30
saqib mir 904cf96bd6 ignore the coprus file 2026-08-22 15:06:15 +05:30
saqib mir b9a4839177 ignore the coprus file 2026-08-22 14:58:09 +05:30
saqib mir f98d4fa934 fix the issue 2026-08-22 14:49:06 +05:30
maskantech 39b125db9a docker build issue fix 2026-08-22 13:57:57 +05:30
maskantech f168b12c82 fix 2026-08-22 13:43:16 +05:30
saqib mir eb514195bf fix the issue 2026-08-22 13:16:15 +05:30
saqib mir 83f47488ab fix the issue 2026-08-22 12:39:00 +05:30
saqib mir 3f340e9689 cusor issue 2026-08-22 11:28:05 +05:30
maskantech e04ac57e40 docker build fix 2026-08-18 15:54:22 +05:30
maskantech 75d0b9c1cb Merge pull request 'fix_issue' (#83) from fix_issue into docker
Reviewed-on: https://gitea.maskantech.in/gitea_admin/pdf/pulls/83
2026-08-18 08:58:39 +00:00
saqib mir adb599b921 fix the issue docker 2026-08-18 12:20:43 +05:30
saqib mir 3da5276cc1 fix the issue 2026-08-18 11:56:11 +05:30
saqib mir d64d1a18a3 fix the protect issue 2026-08-18 11:37:19 +05:30
saqib mir 1217649a96 compare pdf:compare the two pdf 2026-08-14 17:55:31 +05:30
momorew e37c9392e1 Merge branch 'fix_issue' of https://gitea.maskantech.in/gitea_admin/pdf into ribai 2026-08-14 15:40:42 +05:30
momorew 4565522705 Merge branch 'fix_issue' of https://gitea.maskantech.in/gitea_admin/pdf into ribai 2026-08-14 15:13:37 +05:30
saqib mir ed28cb68de pdf protetect:add and remove the proetect password 2026-08-14 14:57:45 +05:30
saqib mir a2f5d59171 features:(pdf protect): open and lock and unlock done 2026-08-14 11:36:13 +05:30
saqib mir e96d9fc10b fix the watermark issue 2026-08-13 19:05:14 +05:30
saqib mir 6c22179699 watermark:all the features completed 2026-08-13 10:47:42 +05:30
saqib mir 59854f8f61 water mark don 2026-08-12 18:33:34 +05:30
momorew 47f4eccbea save button for website 2026-08-12 17:06:02 +05:30
saqib mir 5c5fe450ea done the file store options 2026-08-12 16:21:55 +05:30
saqib mir c7ffac4415 done the file store options 2026-08-12 15:45:44 +05:30
saqib mir adad76f146 done the new blank functionlity 2026-08-12 15:33:55 +05:30
saqib mir 672e1594e1 font issue 2026-08-10 14:21:54 +05:30
saqib mir 47c03a0044 fix 2026-08-07 19:09:44 +05:30
maskantech 5c559b2a0f fixed docker build issue 2026-08-07 10:50:04 +05:30
saqib mir 778845b340 fix the cursor issue 2026-08-06 16:27:59 +05:30
momorew 4ff16adab3 cursor and fontissue 2026-08-06 14:58:44 +05:30
saqib mir 0a97af54c9 fix the cursor issue 2026-08-06 12:47:51 +05:30
saqib mir 1174a00815 fix the cursor issue 2026-08-06 12:19:43 +05:30
saqib mir ab65b4435b fix the cursor issue 2026-08-06 12:12:56 +05:30
maskantech 10ae1c0539 Merge pull request 'dev' (#82) from dev into docker
Reviewed-on: https://gitea.maskantech.in/gitea_admin/pdf/pulls/82
2026-08-06 06:27:41 +00:00
saqib mir 3d9a72d272 fix:ocr isssue 2026-08-05 10:35:51 +05:30
saqib mir e68137219c fix the extraction 2026-08-04 18:30:40 +05:30
saqib mir d7621a07ed image fix 2026-08-04 14:42:32 +05:30
saqib mir 404215f906 image handle 2026-08-04 14:17:47 +05:30
saqib mir d85c3a3e43 fix 2026-08-03 11:26:44 +05:30
saqib mir f89ccfbe5d fix 2026-08-01 16:31:55 +05:30
saqib mir 709e61b6a5 fix 2026-08-01 16:19:11 +05:30
saqib mir 0ff8e8f446 Merge branch 'azeem' of https://gitea.maskantech.in/gitea_admin/pdf into fix_issue 2026-07-31 17:54:01 +05:30
saqib mir 5aa352f181 fix 2026-07-31 17:53:41 +05:30
saqib mir c75ae3ca08 fix 2026-07-31 16:55:50 +05:30
saqib mir 8dc5a9da7d fix 2026-07-31 16:55:23 +05:30
azeem 80cad7a53b Merge pull request 'feat: configure Vite with React, Tailwind CSS, and allowed development hosts' (#80) from azeem into docker
Reviewed-on: https://gitea.maskantech.in/gitea_admin/pdf/pulls/80
2026-07-24 06:20:56 +00:00
208 changed files with 18883 additions and 1134 deletions
+1
View File
@@ -16,6 +16,7 @@
**/.idea
**/coverage
**/tmp
corpus/
**/.pytest_cache
**/.mypy_cache
**/.ruff_cache
-162
View File
@@ -1,162 +0,0 @@
name: CI
on:
push:
branches: [main, develop]
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Rule R2 — PDFium boundary check
run: bash scripts/check_pdfium_boundary.sh
- name: Install clang-format (pinned)
run: pipx install clang-format==22.1.5
- name: clang-format
run: |
clang-format --version
find engine \( -name '*.cpp' -o -name '*.cc' -o -name '*.h' -o -name '*.hpp' \) \
-print0 | xargs -0 clang-format --dry-run --Werror
build:
needs: lint
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
preset: linux-debug
- os: macos-latest
preset: macos-debug
experimental: true
- os: windows-latest
preset: windows-debug
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental == true }}
env:
VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}/.vcpkg-cache
steps:
- uses: actions/checkout@v4
- name: Install Ninja
uses: seanmiddleditch/gha-setup-ninja@v5
- name: Set up MSVC environment
if: runner.os == 'Windows'
uses: ilammy/msvc-dev-cmd@v1
- name: Locate vcpkg
shell: bash
run: |
echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> "$GITHUB_ENV"
git -C "$VCPKG_INSTALLATION_ROOT" fetch --quiet origin || true
- name: Create vcpkg binary cache dir
shell: bash
run: mkdir -p "$VCPKG_DEFAULT_BINARY_CACHE"
- name: Cache vcpkg artifacts
uses: actions/cache@v4
with:
path: ${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
key: vcpkg-${{ matrix.os }}-${{ hashFiles('vcpkg.json') }}
restore-keys: vcpkg-${{ matrix.os }}-
- name: Pin vcpkg dependency baseline
shell: bash
run: |
if ! grep -q '"builtin-baseline"' vcpkg.json; then
"$VCPKG_ROOT/vcpkg" x-update-baseline --add-initial-baseline
fi
- name: Configure
run: cmake --preset ${{ matrix.preset }}
- name: Build
run: cmake --build --preset ${{ matrix.preset }}
- name: Test
run: ctest --preset ${{ matrix.preset }}
gateway:
needs: lint
runs-on: ubuntu-latest
defaults:
run:
working-directory: gateway
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: gateway/pyproject.toml
- name: Install gateway (editable, with dev extras)
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Ruff — lint
run: python -m ruff check .
- name: Ruff — format check
run: python -m ruff format --check .
- name: Pytest
run: python -m pytest
wasm:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Read pinned emsdk version
id: emsdk-version
run: |
version=$(grep '^EMSDK_VERSION=' wasm/emsdk.pinned | cut -d= -f2)
if [ -z "$version" ]; then
echo "::error::EMSDK_VERSION not found in wasm/emsdk.pinned"
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Install Ninja
uses: seanmiddleditch/gha-setup-ninja@v5
- name: Set up Emscripten ${{ steps.emsdk-version.outputs.version }}
uses: mymindstorm/setup-emsdk@v14
with:
version: ${{ steps.emsdk-version.outputs.version }}
actions-cache-folder: emsdk-cache-${{ steps.emsdk-version.outputs.version }}
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Configure (WASM)
run: cmake --preset wasm
- name: Build
run: cmake --build --preset wasm
- name: Smoke test
run: node wasm/hello.test.mjs
-104
View File
@@ -1,104 +0,0 @@
# Extended robustness: corpus render-stability sweep + libFuzzer run.
#
# DELIBERATELY ISOLATED from the main CI gate:
# - never triggers on push or pull_request, so it can NEVER block a merge,
# push, or pull;
# - the whole job is continue-on-error, so a crash finding or a build/runner
# problem reports red here but does not fail any required check;
# - runs on a weekly schedule and on manual dispatch only.
#
# A full 24h fuzz run needs a self-hosted runner (GitHub-hosted runners cap a job
# at 6h). Use the workflow_dispatch `duration_seconds` input for that; the weekly
# schedule does a short smoke instead.
name: Fuzz & corpus sweep
on:
schedule:
- cron: "0 3 * * 0" # Sundays 03:00 UTC — short smoke
workflow_dispatch:
inputs:
duration_seconds:
description: "libFuzzer -max_total_time (e.g. 1800 smoke, 86400 for 24h on a self-hosted runner)"
default: "1800"
sanitizers:
description: "Sanitizer set (fuzzer,address,undefined needs an ASan/UBSan-built PDFium; fuzzer = coverage-only)"
default: "fuzzer,address,undefined"
permissions:
contents: read
concurrency:
group: fuzz-${{ github.ref }}
cancel-in-progress: true
jobs:
fuzz:
# Non-blocking by construction: nothing depends on this job and it is allowed to fail.
continue-on-error: true
runs-on: ubuntu-latest
timeout-minutes: 1500 # permits a 24h dispatch on a self-hosted runner; hosted runners stop at 6h
env:
VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}/.vcpkg-cache
FUZZ_SANITIZERS: ${{ github.event.inputs.sanitizers || 'fuzzer' }}
FUZZ_DURATION: ${{ github.event.inputs.duration_seconds || '600' }}
steps:
- uses: actions/checkout@v4
- name: Install Ninja + Clang
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y ninja-build clang
- name: Locate vcpkg
shell: bash
run: |
echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> "$GITHUB_ENV"
mkdir -p "$VCPKG_DEFAULT_BINARY_CACHE"
- name: Cache vcpkg artifacts
uses: actions/cache@v4
with:
path: ${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
key: vcpkg-fuzz-${{ hashFiles('vcpkg.json') }}
restore-keys: vcpkg-fuzz-
- name: Pin vcpkg dependency baseline
shell: bash
run: |
if ! grep -q '"builtin-baseline"' vcpkg.json; then
"$VCPKG_ROOT/vcpkg" x-update-baseline --add-initial-baseline
fi
- name: Configure (fuzz-linux)
run: cmake --preset fuzz-linux -DPDFENGINE_FUZZ_SANITIZERS="$FUZZ_SANITIZERS"
- name: Build fuzzer
run: cmake --build --preset fuzz-linux --target pdfengine_fuzz
- name: Fetch corpus (pinned + hash-verified)
shell: bash
run: python3 scripts/fetch_corpus.py --manifest tests/regression/corpus-manifest.json || echo "corpus fetch failed (network) — continuing with committed corpus"
- name: Render-stability sweep (replay every corpus PDF once)
shell: bash
run: |
BIN=out/build/fuzz-linux/bin/pdfengine_fuzz
mkdir -p engine/fuzz/artifacts
"$BIN" -runs=0 -artifact_prefix=engine/fuzz/artifacts/ corpus/ corpus/fuzz/ || true
- name: Fuzz run
shell: bash
run: |
BIN=out/build/fuzz-linux/bin/pdfengine_fuzz
"$BIN" -max_total_time="$FUZZ_DURATION" -print_final_stats=1 \
-artifact_prefix=engine/fuzz/artifacts/ corpus/fuzz/ corpus/ || true
- name: Upload crash artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: fuzz-artifacts
path: engine/fuzz/artifacts/
if-no-files-found: ignore
+10
View File
@@ -21,6 +21,7 @@ CMakeUserPresets.json
/third_party/pdfium/checkout/
/third_party/pdfium/install/
/third_party/pdfium/.gclient*
/corpus/
# Skia from-source build (depot_tools / GN / Ninja)
/third_party/skia/depot_tools/
@@ -80,3 +81,12 @@ timeout-*
PDF Editor Timeline.xlsx
# Local environment config / secrets
/third_party/pdfium-wasm/
# AI Large Model Weights & Vector Indexes (never commit binary model weights into Git)
models/**/*.onnx
models/**/*.bin
models/**/*.pt
models/**/*.safetensors
models/**/*.index
!models/**/.gitkeep
.github
BIN
View File
Binary file not shown.
+3 -6
View File
@@ -6,7 +6,7 @@
"name": "win-local",
"displayName": "Windows • Debug (local — build dir outside OneDrive/spaces)",
"inherits": "windows-debug",
"binaryDir": "C:/Users/@USERNAME@/pdfeng-build/win-local",
"binaryDir": "D:/pdfeng-build/win-local",
"cacheVariables": {
"PDFENGINE_WITH_PDFIUM": "ON",
"PDFENGINE_WITH_QPDF": "ON"
@@ -16,11 +16,8 @@
"name": "win-local-pdfium",
"displayName": "Windows • RelWithDebInfo + PDFium (static CRT — build dir outside OneDrive/spaces)",
"inherits": "windows-release",
"binaryDir": "C:/Users/@USERNAME@/pdfeng-build/win-local-pdfium",
"cacheVariables": {
"PDFENGINE_WITH_PDFIUM": "ON",
"PDFENGINE_WITH_QPDF": "ON"
}
"binaryDir": "D:/pdfeng-build/win-local-pdfium",
"cacheVariables": { "PDFENGINE_WITH_PDFIUM": "ON" }
}
],
"buildPresets": [
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

+440 -208
View File
@@ -1,7 +1,8 @@
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pdfengine/pdf_document.hpp>
#include <pdfengine/pdf_engine.hpp>
#include <spdlog/spdlog.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
@@ -9,28 +10,27 @@ 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");
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<typename T>
T get_or_throw(std::expected<T, pdfengine::EngineError>&& res) {
template <typename T> T get_or_throw(std::expected<T, pdfengine::EngineError>&& res) {
if (!res.has_value()) {
throw_on_error(res.error());
}
@@ -43,16 +43,17 @@ void get_or_throw(std::expected<void, pdfengine::EngineError>&& res) {
}
}
}
} // namespace
#include <pdfengine/content_object.hpp>
#include <qpdf/qpdf_extractor.hpp>
#include <qpdf/qpdf_writer.hpp>
#include <parser/content_builder.hpp>
#include <parser/lexer.hpp>
#include <parser/parser.hpp>
#include <parser/content_builder.hpp>
#include <serializer/content_serializer.hpp>
#include <pdfengine/content_object.hpp>
#include <pdfengine/ocr/ocr_coordinator.hpp>
#include <qpdf/qpdf_extractor.hpp>
#include <qpdf/qpdf_writer.hpp>
#include <serializer/ast_serializer.hpp>
#include <serializer/content_serializer.hpp>
static constexpr double kTjSpaceKern = -500.0;
@@ -66,14 +67,14 @@ public:
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) {
@@ -82,7 +83,7 @@ public:
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]);
@@ -94,24 +95,28 @@ public:
return result;
}
bool replace_text_object(int page_index, int object_index, const py::bytes& new_text_bytes, const std::string& dest_path) {
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;
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;
if (op.operands.empty())
continue;
auto& strNode = op.operands.back();
if (strNode->type == pdfengine::AstNodeType::String || strNode->type == pdfengine::AstNodeType::HexString) {
if (strNode->type == pdfengine::AstNodeType::String ||
strNode->type == pdfengine::AstNodeType::HexString) {
if (textCount == object_index) {
strNode->type = pdfengine::AstNodeType::String;
strNode->stringValue = new_text;
@@ -121,7 +126,8 @@ public:
textCount++;
}
} else if (op.op == "TJ") {
if (op.operands.empty()) continue;
if (op.operands.empty())
continue;
auto& arrNode = op.operands.back();
if (arrNode->type == pdfengine::AstNodeType::Array) {
std::string combinedText;
@@ -129,9 +135,11 @@ public:
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());
combinedText +=
std::string(item->bytesValue.begin(), item->bytesValue.end());
} else if (item->type == pdfengine::AstNodeType::Number) {
if (item->numberValue < kTjSpaceKern) combinedText += " ";
if (item->numberValue < kTjSpaceKern)
combinedText += " ";
}
}
if (!combinedText.empty()) {
@@ -139,17 +147,22 @@ public:
bool redistributed = false;
if (new_text.size() == combinedText.size()) {
std::vector<std::pair<pdfengine::AstNode*, std::string>> assign;
size_t pos = 0; bool ok = true;
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();
? 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; }
if (pos >= new_text.size() || new_text[pos] != ' ') {
ok = false;
break;
}
pos += 1;
}
}
@@ -163,7 +176,8 @@ public:
}
if (!redistributed) {
arrNode->arrayItems.clear();
auto newStrNode = std::make_shared<pdfengine::AstNode>(pdfengine::AstNodeType::String);
auto newStrNode = std::make_shared<pdfengine::AstNode>(
pdfengine::AstNodeType::String);
newStrNode->stringValue = new_text;
arrNode->arrayItems.push_back(std::move(newStrNode));
}
@@ -175,12 +189,13 @@ public:
}
}
}
if (!modified) return false;
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();
@@ -196,12 +211,87 @@ PYBIND11_MODULE(pdfengine, m) {
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"))
.def("replace_text_object", &StreamEditor::replace_text_object, py::arg("page_index"), py::arg("object_index"), py::arg("new_text"), py::arg("dest_path"));
.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");
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");
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"
);
py::class_<pdfengine::Point2D>(m, "Point2D")
.def(py::init<double, double>(), py::arg("x") = 0.0, py::arg("y") = 0.0)
@@ -216,7 +306,8 @@ PYBIND11_MODULE(pdfengine, m) {
.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) + ")";
return "DevicePoint(x=" + std::to_string(self.x) + ", y=" + std::to_string(self.y) +
")";
});
py::class_<pdfengine::DocumentMetadata>(m, "DocumentMetadata")
@@ -241,7 +332,8 @@ PYBIND11_MODULE(pdfengine, m) {
.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_extract_for_accessibility",
&pdfengine::DocumentPermissions::canExtractForAccessibility)
.def_readonly("can_assemble", &pdfengine::DocumentPermissions::canAssemble);
py::class_<pdfengine::PageImage>(m, "PageImage")
@@ -272,7 +364,8 @@ PYBIND11_MODULE(pdfengine, m) {
.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") + ")";
return "FontInfo(font_name='" + self.fontName + "', type='" + self.type +
"', is_embedded=" + (self.isEmbedded ? "True" : "False") + ")";
});
py::class_<pdfengine::Glyph>(m, "Glyph")
@@ -297,6 +390,8 @@ PYBIND11_MODULE(pdfengine, m) {
.def_readonly("font_size", &pdfengine::TextRun::fontSize)
.def_readonly("internal_font_id", &pdfengine::TextRun::internalFontId)
.def_readonly("is_embedded", &pdfengine::TextRun::isEmbedded)
.def_readonly("is_embedded_font", &pdfengine::TextRun::isEmbeddedFont)
.def_readonly("is_predicted_font", &pdfengine::TextRun::isPredictedFont)
.def_readonly("type", &pdfengine::TextRun::type)
.def_readonly("glyphs", &pdfengine::TextRun::glyphs)
.def_readonly("x", &pdfengine::TextRun::x)
@@ -366,169 +461,306 @@ PYBIND11_MODULE(pdfengine, m) {
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)
.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) {
.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;
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) {
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["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<const char*>(res.data()), res.size());
}, py::arg("name"));
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"));
py::class_<pdfengine::PdfDocument, std::shared_ptr<pdfengine::PdfDocument>>(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<uint8_t> data(sv.begin(), sv.end());
return get_or_throw(pdfengine::PdfDocument::loadFromMemory(data, password));
}, py::arg("data"), py::arg("password") = "")
.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") = "")
.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<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());
})
.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());
})
.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());
});
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;
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;
};
for (auto item : lines_list) {
if (item.is_none()) continue;
py::dict d = item.cast<py::dict>();
pdfengine::document::RawOCRLine line;
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()) {
py::dict box = d["box"].cast<py::dict>();
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);
} else {
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);
}
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
);
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"));
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

+1 -1
View File
@@ -30,7 +30,7 @@ services:
image: pdf-engine-frontend:dev
container_name: pdf-engine-frontend
environment:
VITE_GATEWAY_URL: http://localhost:8765
VITE_GATEWAY_URL: https://pdfapi-dev.maskantech.in
ports:
- "5173:5173"
volumes:
+298
View File
@@ -0,0 +1,298 @@
# PDF Security / Unlock / Protect — Feature Audit Report
**Date of Audit:** August 13, 2026
**Audited Subsystems:** Frontend (`pdf/frontend`), Gateway API (`pdf/gateway`), C++ PDF Engine (`pdf/engine`), Pybind11 Bindings (`pdf/bindings`), Security Tests (`pdf/tests/security`)
**Audit Purpose:** Evaluate the exact current state of PDF security, password authentication, permission enforcement, encryption detection, password removal, and PDF protection capabilities in the existing codebase.
---
## 1. Executive Summary
The existing PDF editor codebase possesses a **robust reading, authenticating, and permission-enforcing pipeline** for password-protected PDFs, but **lacks all writing/creation capabilities for PDF encryption and protection**.
Key findings:
* **Reading & Authenticating Encrypted PDFs:** **FULLY IMPLEMENTED**. The system detects encrypted PDFs, prompts the user via a React modal, passes the password to PDFium in C++, validates credentials, returns helpful error messages on invalid passwords, and renders the document upon success.
* **Granular Permission Surfacing & Enforcement:** **FULLY IMPLEMENTED**. PDFium extracts security revision numbers, encryption types (RC4, AES-128, AES-256), and permission flags. The Python FastAPI Gateway exposes these flags in `PermissionsResponse` and enforces HTTP `403 Forbidden` errors if a user attempts forbidden edits (annotations, text replacements, page rotations) or unauthorized exports.
* **Password Removal / Unlocking:** **PARTIALLY IMPLEMENTED (Implicit)**. Opening a protected PDF with a valid password loads the decrypted document into memory. Exporting the document via `/documents/{id}/export` writes out an unencrypted PDF file. However, there is no explicit UI button or API endpoint dedicated to "Remove Password".
* **Protecting / Encrypting PDFs:** **NOT IMPLEMENTED / MISSING**. There is no functionality in the C++ engine (PDFium/QPDF), Pybind11 bindings, Gateway API, or Frontend UI to password-protect an unencrypted PDF, set user/owner passwords, or configure output permissions.
---
## 2. User-Facing Capability Summary
### CURRENTLY AVAILABLE
* **✓ Open Password-Protected PDFs:** Prompts for credentials when an encrypted PDF is uploaded.
* **✓ Password Validation & Error Feedback:** Rejects incorrect passwords with clear inline UI feedback and allows unlimited retries.
* **✓ Post-Authentication Rendering & Extraction:** Full page rendering, OCR, text extraction, font listing, layout analysis, and display list extraction work seamlessly after authentication.
* **✓ Encryption & Security Inspection:** Detects and displays encryption standards (RC4-40, RC4-128, AES-128, AES-256) and security revision level (2 through 6) in the Inspector panel.
* **✓ Permission Enforcement:** Gateway blocks unauthorized edits, annotations, page reordering, and exports with HTTP `403 Forbidden` responses if disallowed by the PDF's security settings.
* **✓ Unprotected Export:** Exporting an authenticated PDF generates an unencrypted PDF that can subsequently be opened without a password.
### NOT CURRENTLY AVAILABLE
* **✗ Explicit Password Removal UI/API:** No button or endpoint explicitly labeled "Unlock PDF" or "Remove Security".
* **✗ Password-Protect PDF / Lock PDF:** Cannot apply passwords to an unencrypted PDF.
* **✗ Configure Output Permissions:** Cannot set or modify permission flags for printing, copying, editing, or annotating.
* **✗ Separate Owner Password Prompting:** Prompts only with a generic "Document password" input; does not request owner password specifically when attempting restricted operations.
* **✗ Re-encrypting Edited PDFs:** Saved/exported PDFs are saved without encryption.
* **✗ Attempt Rate Limiting:** No rate limiting on password validation attempts at the API level.
---
## 3. Protected PDF Open Flow
The upload and document initialization flow is traced across the full stack:
```
User Selects Encrypted PDF
[Frontend] gatewayService.uploadDocument(file, password="")
▼ (POST /documents)
[Gateway API] upload_document() in crud.py
[Pybind11] PdfDocument.load_from_memory(bytes_data, "")
[C++ Engine] PdfDocument::loadFromMemory() -> FPDF_LoadMemDocument()
▼ (PDFium returns FPDF_ERR_PASSWORD)
[C++ Engine] mapPdfiumError() returns EngineError::PasswordRequired
[Pybind11] Throws ValueError("Password required to open this PDF")
[Gateway API] Catches ValueError -> Raises HTTP 401 ("Password required")
[Frontend] gatewayService catches 401 -> Throws PasswordError
[Frontend] App.tsx sets passwordPrompt state -> PasswordModal renders
```
### Exact Code Implementation Points
* **Encryption Detection & Password Loading (C++ Engine):**
* File: [`pdf/engine/src/parser/pdfium_document.cpp`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/engine/src/parser/pdfium_document.cpp#L42-L48)
* Function: `pdfengine::PdfDocument::loadFromMemory(const std::vector<uint8_t>& data, const std::string& password)`
* C++ API: `FPDF_LoadMemDocument(buffer_copy.data(), static_cast<int>(buffer_copy.size()), password.empty() ? nullptr : password.c_str())`
* **Error Mapping (C++ Engine):**
* File: [`pdf/engine/src/parser/pdfium_internal.cpp`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/engine/src/parser/pdfium_internal.cpp#L271-L285)
* Function: `pdfengine::parser::mapPdfiumError(unsigned long err, bool passwordProvided)`
* Logic: Maps `FPDF_ERR_PASSWORD` to `EngineError::PasswordRequired` (if `password` is empty) or `EngineError::InvalidPassword` (if `password` was provided).
* **Pybind11 Translation:**
* File: [`pdf/bindings/python/pdfengine_py.cpp`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/bindings/python/pdfengine_py.cpp#L18-L21)
* Function: `throw_on_error(pdfengine::EngineError err)`
* Logic: Maps `PasswordRequired` -> `PyExc_ValueError("Password required to open this PDF")` and `InvalidPassword` -> `PyExc_ValueError("Invalid password provided for this PDF")`.
* **Gateway Endpoint:**
* File: [`pdf/gateway/app/routers/documents/crud.py`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/gateway/app/routers/documents/crud.py#L142-L189)
* Endpoint: `POST /documents?password={password}` (`upload_document`)
* Logic: Catches `ValueError` from pybind11 and raises `HTTPException(status_code=401, detail="Password required")` or `HTTPException(status_code=401, detail="Invalid password")`.
* **Frontend Password Dialog & Resubmission:**
* Files: [`pdf/frontend/src/lib/gatewayService.ts`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/frontend/src/lib/gatewayService.ts#L471-L486), [`pdf/frontend/src/App.tsx`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/frontend/src/App.tsx#L638-L652), [`pdf/frontend/src/components/PasswordModal.tsx`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/frontend/src/components/PasswordModal.tsx#L15-L95)
* Logic: `gatewayService` throws `PasswordError`. `App.tsx` catches `PasswordError` and opens `PasswordModal`. User enters password, triggering resubmission to `uploadDocument(file, password)`.
---
## 4. Password Authentication & Validation Flow
| Stage | Implementation Status | Evidence / Location |
|---|---|---|
| **Password Reaches Backend** | **IMPLEMENTED** | `uploadDocument(file, password)` in `gatewayService.ts:L476` sends `POST /documents?password=...`. |
| **Backend Passes Password to C++** | **IMPLEMENTED** | `crud.py:L173` calls `doc = pdfengine.PdfDocument.load_from_memory(bytes_data, password)`. |
| **C++ Engine Validation** | **IMPLEMENTED** | `pdfium_document.cpp:L42` executes `FPDF_LoadMemDocument(..., password.c_str())`. |
| **Incorrect Password Rejected** | **IMPLEMENTED** | `pdfium_internal.cpp:L280` returns `EngineError::InvalidPassword` -> HTTP 401 `"Invalid password"` -> `App.tsx` shows red error message in modal. |
| **Correct Password Accepted** | **IMPLEMENTED** | `FPDF_LoadMemDocument` returns document pointer -> Gateway stores document info and returns HTTP 201 response. |
| **Password Retry Loop** | **IMPLEMENTED** | `App.tsx:L650` retains modal open on failure with updated error message, allowing infinite retry attempts. |
| **Rate Limiting / Attempt Limit** | **MISSING** | Neither Gateway nor C++ Engine tracks failed attempts or implements delays/lockouts. |
---
## 5. Rendering & Feature Support After Authentication
Once authenticated, all engine features operate on the unlocked in-memory PDF handle:
| Feature | Status | Evidence / Implementation Location |
|---|---|---|
| **Page Count** | **IMPLEMENTED** | [`pdfium_document.cpp:L101`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/engine/src/parser/pdfium_document.cpp#L101) (`FPDF_GetPageCount`) |
| **Document Metadata** | **IMPLEMENTED** | [`pdfium_document.cpp:L109`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/engine/src/parser/pdfium_document.cpp#L109) (`FPDF_GetMetaText`) |
| **Font Inventory** | **IMPLEMENTED** | [`pdfium_document.cpp:L280`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/engine/src/parser/pdfium_document.cpp#L280), [`fonts.py:L16`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/gateway/app/routers/documents/fonts.py#L16) |
| **Page Text Extraction** | **IMPLEMENTED** | [`pdfium_page.cpp:L100`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/engine/src/parser/pdfium_page.cpp#L100), [`render.py:L172`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/gateway/app/routers/render.py#L172) |
| **Page Image Rendering** | **IMPLEMENTED** | [`pdfium_page.cpp:L50`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/engine/src/parser/pdfium_page.cpp#L50), [`render.py:L19`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/gateway/app/routers/render.py#L19) |
| **Display List Extraction** | **IMPLEMENTED** | [`pdfium_page.cpp:L300`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/engine/src/parser/pdfium_page.cpp#L300), [`content.py:L133`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/gateway/app/routers/documents/content.py#L133) |
| **OCR Support** | **IMPLEMENTED** | [`ocr.py:L63`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/gateway/app/routers/ocr.py#L63) (Runs Tesseract on rendered page image) |
| **Layout Model Extraction** | **IMPLEMENTED** | [`layout.py:L120`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/gateway/app/routers/layout.py#L120) (`extract_document_model()`) |
| **Editing Operations** | **PARTIAL** | [`edits.py:L395`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/gateway/app/routers/edits.py#L395) checks `permissions`. Allowed edits modify stream in-memory. |
| **Exporting Document** | **IMPLEMENTED** | [`export.py:L17`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/gateway/app/routers/documents/export.py#L17) checks `canCopy` and calls `save_full_for_export()`. |
---
## 6. Unlock / Decrypt Capability
| Action | Status | Description |
|---|---|---|
| **Open Protected PDF** | **IMPLEMENTED** | Via `upload_document` with `password`. |
| **Authenticate** | **IMPLEMENTED** | Validated via `FPDF_LoadMemDocument`. |
| **In-Memory Decryption** | **IMPLEMENTED** | PDFium decrypts document structure in RAM for standard operations. |
| **Save/Export Unprotected Copy** | **IMPLEMENTED (Implicit)** | `doc.save_full_for_export()` calls PDFium's `FPDF_SaveWithVersion(doc_, &writer, 0, 14)`. Because PDFium does not attach an encryption handler during save, the output PDF is **unencrypted**. |
| **Reopen Exported Copy Without Password** | **IMPLEMENTED** | The exported PDF contains no `/Encrypt` dictionary; reopening requires no password. |
| **Explicit "Remove Password" Endpoint / UI** | **MISSING** | No dedicated route (e.g. `POST /documents/{id}/unlock`) or UI action exists. |
---
## 7. Protect / Encrypt Capability
A comprehensive search across C++ Engine (`pdf/engine`), Pybind11 (`pdf/bindings`), Gateway (`pdf/gateway`), and Frontend (`pdf/frontend`) reveals **NO code for creating encrypted PDFs**:
* **QPDF Encryption Writer:** Not implemented. [`qpdf_writer.cpp`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/engine/src/qpdf/qpdf_writer.cpp) contains stream replacement and appearance helpers, but no `QPDFWriter::setEncryption` calls.
* **PDFium Encryption Output:** PDFium's public writing API lacks native PDF encryption creation functions.
* **Python Encryption Libraries:** `pikepdf` is imported **only** in [`pdf/tests/security/test_permissions.py`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/tests/security/test_permissions.py#L30) to generate test fixtures. It is not present in Gateway production services.
* **Frontend Encryption Controls:** No modal, form, or state exists for password-protecting documents.
---
## 8. Password Types & Permission Management
### Password Types
* **User Password:** **IMPLEMENTED**. Used for opening documents.
* **Owner Password:** **PARTIALLY IMPLEMENTED**. When opened with an Owner password, PDFium elevates document permissions. The C++ engine detects this by comparing user vs doc permissions (`perms.ownerUnlocked = (p != up)` in [`pdfium_document.cpp:L168`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/engine/src/parser/pdfium_document.cpp#L168)) and displays an `"Owner"` badge in `InspectorPanel.tsx:L569`. However, there is no UI workflow to enter an owner password separately to unlock restricted actions.
### PDF Permissions Matrix
| Permission | Existing Support | Where Implemented |
|---|---|---|
| **Print** (`canPrint`) | Surfaced & Displayed | `pdfium_document.cpp:L171`, `store.py:L18`, `InspectorPanel.tsx:L573` |
| **Modify** (`canModify`) | Surfaced & Enforced | `pdfium_document.cpp:L172`, `edits.py:L375`, `edits.py:L395` (HTTP 403) |
| **Copy** (`canCopy`) | Surfaced & Enforced | `pdfium_document.cpp:L173`, `export.py:L30`, `export.py:L65` (HTTP 403 on Export) |
| **Extract** (`canCopy`) | Surfaced & Enforced | Same bit as Copy (`0x10`) in PDFium spec |
| **Annotate** (`canAnnotate`) | Surfaced & Enforced | `pdfium_document.cpp:L174`, `edits.py:L370-374`, `edits.py:L395` (HTTP 403) |
| **Fill Forms** (`canFillForms`) | Surfaced & Enforced | `pdfium_document.cpp:L175`, `edits.py:L376`, `edits.py:L395` |
| **Accessibility** (`canExtractForAccessibility`) | Surfaced & Displayed | `pdfium_document.cpp:L176`, `store.py:L24` |
| **Document Assembly** (`canAssemble`) | Surfaced & Enforced | `pdfium_document.cpp:L177`, `edits.py:L377` (HTTP 403 on rotation/deletion) |
| **High-Quality Print** (`canPrintHighRes`) | Surfaced & Displayed | `pdfium_document.cpp:L178`, `store.py:L19` |
| **Permission Configuration (Writing)** | **MISSING** | No engine or gateway code exists to modify permission flags. |
---
## 9. Encryption Algorithm Surfacing
The C++ engine inspects the PDF security handler revision via `FPDF_GetSecurityHandlerRevision(doc_)`:
```cpp
// pdfium_document.cpp (lines 147-164)
switch (rev) {
case 2: perms.encryption = "RC4-40"; break;
case 3: perms.encryption = "RC4-128"; break;
case 4: perms.encryption = "AES-128"; break;
case 5:
case 6: perms.encryption = "AES-256"; break;
default: perms.encryption = "Unknown"; break;
}
```
* **Surfacing:** Mapped to `PermissionsResponse.encryption` ([`document.py:L12`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/gateway/app/schemas/document.py#L12)) and rendered as a badge in the Inspector panel ([`InspectorPanel.tsx:L567`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/frontend/src/components/InspectorPanel.tsx#L567)).
* **Creation:** Encryption creation was not found in the codebase.
---
## 10. Frontend UI State
| Capability | Status | Front-End Evidence |
|---|---|---|
| **A. Unlock existing protected PDF** | **IMPLEMENTED** | `PasswordModal.tsx` renders when `passwordPrompt` state is non-null. |
| **B. Remove password / security** | **MISSING** | No UI button or option. |
| **C. Protect an unprotected PDF** | **MISSING** | No UI button or option. |
| **D. Set a password** | **MISSING** | No input fields for protecting PDFs. |
| **E. Configure permissions** | **MISSING** | No permissions toggle matrix in settings or export dialog. |
| **F. Export document** | **IMPLEMENTED** | TopBar export button triggers file download. |
---
## 11. Gateway / API Endpoints Audit
| Endpoint | Method | Purpose | Implemented Behavior | C++ Call |
|---|---|---|---|---|
| `/documents` | `POST` | Upload & open PDF | Accepts `password` query param. Passes password to engine. Returns HTTP 401 on missing/wrong password, HTTP 201 with permissions on success. | `PdfDocument::loadFromMemory` |
| `/documents/{id}/export` | `GET` | Export PDF | Verifies `permissions.canCopy`. Returns HTTP 403 if forbidden. Calls `save_full_for_export()`. | `PdfiumDocument::saveFullForExport` |
| `/documents/{id}/export-remote` | `POST` | Export to remote URL | Verifies `permissions.canCopy`. Streams file to target URL. | `PdfiumDocument::saveFullForExport` |
| `/edits` | `POST` | Apply PDF edits | Maps operation types to permissions (`canAnnotate`, `canModify`, `canFillForms`, `canAssemble`). Returns HTTP 403 if restricted. | Engine edit APIs |
| `/documents/{id}/unlock` | N/A | Dedicated unlock | **MISSING** | N/A |
| `/documents/{id}/protect` | N/A | Protect document | **MISSING** | N/A |
---
## 12. End-to-End Export & Reopen Verification Scenarios
### SCENARIO A: Unprotected PDF -> Protect with Password -> Export -> Reopen -> Prompted for Password
* **Status:** **NOT WORKING / IMPOSSIBLE TODAY**
* **Reason:** "Protect with password" is not implemented anywhere in the backend or engine.
### SCENARIO B: Protected PDF -> Enter Password -> Opened -> Export -> Reopen Exported PDF -> No Password Required
* **Status:** **FULLY WORKING TODAY (Implicitly)**
* **Reason:** PDFium loads the decrypted PDF structure into memory. Exporting via `GET /documents/{id}/export` writes the file without encryption. Reopening the exported file requires no password.
### SCENARIO C: Protected PDF -> Enter Wrong Password -> Rejected -> Enter Correct Password -> Opened
* **Status:** **FULLY WORKING TODAY**
* **Reason:** Invalid password returns HTTP 401 with `"Invalid password"`. The frontend displays `"Incorrect password — please try again."` and keeps the modal open. Re-submitting with the correct password opens the document cleanly.
---
## 13. Security Observations & Risks
* **CONFIRMED FROM CODE — Password Passed in Query String:**
In [`gatewayService.ts:L476`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/frontend/src/lib/gatewayService.ts#L476), the upload URL is constructed as `${this.baseUrl}/documents?password=${encodeURIComponent(password)}`. Transmitting passwords in GET/POST URL query parameters poses a security risk because query parameters may be recorded in server access logs or proxy logs.
* **CONFIRMED FROM CODE — Absence of API Rate Limiting:**
In [`crud.py:L142-L190`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/gateway/app/routers/documents/crud.py#L142-L189), there is no rate-limiting or lock-out mechanism for password validation requests, allowing automated brute-force attempts.
* **CONFIRMED FROM CODE — Implicit Decryption on Export:**
In [`export.py:L38`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/gateway/app/routers/documents/export.py#L38), exported PDFs are saved unencrypted. Users who upload a password-protected PDF and subsequently export it will receive an unencrypted file without explicit warning that password protection has been stripped.
* **CONFIRMED FROM CODE — Plaintext Password In-Memory Only:**
Passwords are passed directly to `load_from_memory` and are **not** persisted in `document_store` or written to disk.
---
## 14. Complete Feature Matrix
| Feature | Status | Existing Location | Evidence |
|---|---|---|---|
| **Detect encrypted PDF** | `IMPLEMENTED` | `pdfium_document.cpp:L142` | `perms.isEncrypted = (rev != -1)` |
| **Password popup** | `IMPLEMENTED` | `PasswordModal.tsx:L15` | `<PasswordModal state={passwordPrompt} ... />` |
| **Validate password** | `IMPLEMENTED` | `pdfium_document.cpp:L42`, `crud.py:L173` | `load_from_memory(bytes_data, password)` |
| **Wrong password handling** | `IMPLEMENTED` | `crud.py:L183`, `App.tsx:L650` | HTTP 401 `"Invalid password"` -> UI error |
| **Correct password handling** | `IMPLEMENTED` | `crud.py:L174`, `App.tsx:L646` | Returns `DocumentInfoResponse` -> Document opens |
| **Render protected PDF** | `IMPLEMENTED` | `pdfium_page.cpp:L50`, `render.py:L19` | Renders tiles/pages post-authentication |
| **OCR protected PDF** | `IMPLEMENTED` | `ocr.py:L63` | Executes Tesseract on authenticated doc pages |
| **Edit protected PDF** | `PARTIAL` | `edits.py:L395` | Enforces permissions, but doesn't re-encrypt |
| **Export protected PDF** | `PARTIAL` | `export.py:L17` | Enforces `canCopy`, but exports UNENCRYPTED |
| **Remove password** | `PARTIAL` | `export.py:L38` | Exporting strips password (implicit, no explicit API) |
| **Export unprotected PDF** | `IMPLEMENTED` | `export.py:L38` | `save_full_for_export()` outputs unencrypted PDF |
| **Reopen unprotected PDF** | `IMPLEMENTED` | `crud.py:L141` | Exported file reopens without password |
| **Protect PDF** | `MISSING` | N/A | No code exists to protect/encrypt PDF |
| **Set user password** | `MISSING` | N/A | No functionality to set user password |
| **Set owner password** | `MISSING` | N/A | No functionality to set owner password |
| **AES encryption (detection)** | `IMPLEMENTED` | `pdfium_document.cpp:L155-160` | Revision 4/5/6 mapped to `"AES-128"` / `"AES-256"` |
| **AES-256 (detection)** | `IMPLEMENTED` | `pdfium_document.cpp:L158` | Revision 5/6 mapped to `"AES-256"` |
| **RC4 (detection)** | `IMPLEMENTED` | `pdfium_document.cpp:L148-153` | Revision 2/3 mapped to `"RC4-40"` / `"RC4-128"` |
| **Print permission** | `IMPLEMENTED` | `pdfium_document.cpp:L171`, `store.py:L18` | Surfaced in permissions API |
| **Copy permission** | `IMPLEMENTED` | `pdfium_document.cpp:L173`, `export.py:L30` | Enforced on Export (returns HTTP 403) |
| **Modify permission** | `IMPLEMENTED` | `pdfium_document.cpp:L172`, `edits.py:L375` | Enforced on edits (returns HTTP 403) |
| **Annotation permission** | `IMPLEMENTED` | `pdfium_document.cpp:L174`, `edits.py:L370` | Enforced on annotations (returns HTTP 403) |
| **Form permission** | `IMPLEMENTED` | `pdfium_document.cpp:L175`, `edits.py:L376` | Enforced on form fills (allows if permitted) |
| **Extraction permission** | `IMPLEMENTED` | `pdfium_document.cpp:L173`, `store.py:L21` | Surfaced as `canCopy` |
| **Document assembly** | `IMPLEMENTED` | `pdfium_document.cpp:L177`, `edits.py:L377` | Enforced on page rotate/delete ops |
| **Accessibility permission** | `IMPLEMENTED` | `pdfium_document.cpp:L176`, `store.py:L24` | Surfaced as `canExtractForAccessibility` |
| **High-quality printing** | `IMPLEMENTED` | `pdfium_document.cpp:L178`, `store.py:L19` | Surfaced as `canPrintHighRes` |
| **Security UI** | `PARTIAL` | `PasswordModal.tsx`, `InspectorPanel.tsx` | Password prompt modal + Inspector security badge exist |
| **Security API** | `PARTIAL` | `crud.py`, `export.py`, `edits.py` | Upload & export handle passwords & perms |
| **C++ security implementation** | `PARTIAL` | `pdfium_document.cpp` | Document load & permission inspection implemented |
| **Pybind security bindings** | `PARTIAL` | `pdfengine_py.cpp:L252` | `DocumentPermissions` & `load_from_memory` bound |
---
## 15. Production Readiness Summary
* **Opening & Viewing Protected PDFs:** **PRODUCTION READY**. Robust, fully tested with unit tests ([`pdf/engine/tests/document_load_test.cpp`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/engine/tests/document_load_test.cpp#L27-L70)) and security integration tests ([`pdf/tests/security/test_permissions.py`](file:///c:/Users/Maskan/Desktop/pdf_editor/pdf/tests/security/test_permissions.py)).
* **Permission Enforcement:** **PRODUCTION READY**. Gateway correctly returns HTTP 403 Forbidden for restricted edits and exports.
* **Password Removal / Unlocking:** **NEEDS FEATURIZATION**. Works implicitly when exporting, but lacks dedicated API routes and UI buttons for explicit unlock workflows.
* **Protecting / Encrypting PDFs:** **NOT PRODUCTION READY (0% IMPLEMENTED)**. Creation of password-protected PDFs or custom permission dictionaries requires adding QPDF or pikepdf encryption writers to the engine/gateway layer.
+30
View File
@@ -13,6 +13,9 @@ add_library(pdfengine OBJECT
src/core/skia_renderer.cpp
src/parser/content_stream_parser.cpp
src/parser/decoration_builder.cpp
src/document/document_normalizer.cpp
src/document/document_builder.cpp
src/document/document_validator.cpp
src/text/selection.cpp
src/text/text_layout_engine.cpp
src/fonts/face/font_face.cpp
@@ -29,6 +32,8 @@ add_library(pdfengine OBJECT
src/fonts/pdf_fonts/font_fallback.cpp
src/fonts/pdf_fonts/font_subset.cpp
src/fonts/pdf_fonts/font_cmap_builder.cpp
src/fonts/pdf_fonts/font_validator.cpp
src/fonts/pdf_fonts/font_extraction_service.cpp
src/fonts/pdf_fonts/embedded_font_reconstructor.cpp
src/fonts/pdf_fonts/encoding/encoding.cpp
src/fonts/pdf_fonts/encoding/tounicode_parser.cpp
@@ -52,6 +57,31 @@ add_library(pdfengine OBJECT
src/qpdf/qpdf_writer.cpp
src/qpdf/qpdf_resource_resolver.cpp
src/core/image_decoder.cpp
src/image/core/image_object.cpp
src/image/core/image_validator.cpp
src/image/core/image_builder.cpp
src/image/core/image_pipeline.cpp
src/image/core/image_manager.cpp
src/image/decoder/filter_decoder.cpp
src/image/decoder/sample_decoder.cpp
src/image/decoder/pixel_decoder.cpp
src/image/decoder/color_converter.cpp
src/image/decoder/mask_processor.cpp
src/image/decoder/image_decoder_factory.cpp
src/ocr/ocr_cache.cpp
src/ocr/image_cleaner.cpp
src/ocr/ocr_importer.cpp
src/ocr/ocr_coordinator.cpp
src/layout/layout_arena.cpp
src/layout/spatial_index.cpp
src/layout/multi_level_cache.cpp
src/layout/layout_session.cpp
src/layout/pass_registry.cpp
src/layout/layout_engine.cpp
src/layout/passes/line_detection_pass.cpp
src/layout/passes/paragraph_detection_pass.cpp
src/layout/passes/column_detection_pass.cpp
src/layout/passes/region_detection_pass.cpp
src/parser/lexer.cpp
src/parser/parser.cpp
src/parser/content_builder.cpp
+9 -5
View File
@@ -5,6 +5,7 @@
#include <memory>
#include <pdfengine/graphics_state.hpp>
#include <pdfengine/path.hpp>
#include <pdfengine/image_object.hpp>
namespace pdfengine {
@@ -39,10 +40,14 @@ public:
double tm[6] = {1.0, 0.0, 0.0, 1.0, 0.0, 0.0};
};
class ImageObject : public ContentObject {
class ImageContentObject : public ContentObject {
public:
ContentObjectType getType() const override { return ContentObjectType::Image; }
std::shared_ptr<const ImageObject> image;
Matrix transform;
// Helper compatibility properties
std::string name;
int width = 0;
int height = 0;
@@ -50,12 +55,11 @@ public:
std::string filter;
int bitsPerComponent = 8;
bool hasSoftMask = false;
std::vector<uint8_t> pixelData;
Matrix transform;
};
using LegacyImageObject = ImageContentObject;
enum class PathPaintOp {
Stroke,
Fill,
@@ -72,4 +76,4 @@ public:
Matrix transform;
};
}
} // namespace pdfengine
+39 -2
View File
@@ -6,6 +6,7 @@
#include <pdfengine/graphics_state.hpp>
#include <pdfengine/path.hpp>
#include <pdfengine/image.hpp>
#include <pdfengine/image_object.hpp>
namespace pdfengine {
class CommandVisitor;
@@ -63,13 +64,48 @@ struct FillStrokePathCommand : public Command {
void accept(CommandVisitor& visitor) const override;
};
enum class BlendMode {
Normal,
Multiply,
Screen,
Overlay,
Darken,
Lighten,
ColorBurn,
SoftLight,
Difference
};
enum class RenderingIntent {
RelativeColorimetric,
Perceptual,
AbsoluteColorimetric,
Saturation
};
struct DrawImageCommand : public Command {
std::shared_ptr<const ImageObject> imageObject;
ImageInfo image;
Matrix matrix;
float opacity;
float opacity = 1.0f;
BlendMode blendMode = BlendMode::Normal;
RenderingIntent renderingIntent = RenderingIntent::RelativeColorimetric;
bool interpolate = false;
DrawImageCommand(ImageInfo img, Matrix m, float op = 1.0f)
DrawImageCommand(std::shared_ptr<const ImageObject> imgObj, Matrix m, float op = 1.0f,
BlendMode blend = BlendMode::Normal, bool interp = false)
: imageObject(std::move(imgObj)), matrix(m), opacity(op), blendMode(blend), interpolate(interp) {
if (imageObject) {
image.width = imageObject->geometry().width;
image.height = imageObject->geometry().height;
image.channels = imageObject->pixels().channels();
image.pixelData = imageObject->pixels().vector();
}
}
DrawImageCommand(ImageInfo img, Matrix m, float op = 1.0f)
: image(std::move(img)), matrix(m), opacity(op) {}
void accept(CommandVisitor& visitor) const override;
};
@@ -107,6 +143,7 @@ public:
void strokePath(const Path& path);
void fillStrokePath(const Path& path, FillRule rule = FillRule::NonZero);
void drawImage(const ImageInfo& image, const Matrix& m, float opacity = 1.0f);
void drawImage(std::shared_ptr<const ImageObject> image, const Matrix& m, float opacity = 1.0f, BlendMode blend = BlendMode::Normal);
[[nodiscard]] size_t size() const noexcept { return m_commands.size(); }
void clear() { m_commands.clear(); }
@@ -0,0 +1,16 @@
#pragma once
#include "pdfengine/pdf_document.hpp"
#include "pdfengine/document/raw_ocr_document.hpp"
namespace pdfengine::document {
class DocumentBuilder {
public:
DocumentBuilder() = default;
// Shared builder: converts raw OCR observation pages into native PageModel
PageModel buildFromOCR(const RawOCRPage& rawPage) const;
};
} // namespace pdfengine::document
@@ -0,0 +1,15 @@
#pragma once
#include "pdfengine/document/raw_ocr_document.hpp"
namespace pdfengine::document {
class DocumentNormalizer {
public:
DocumentNormalizer() = default;
// Normalizes coordinates, scales pixel boxes to PDF points, fixes negative dimensions and cleans duplicates
RawOCRPage normalize(const RawOCRPage& rawPage) const;
};
} // namespace pdfengine::document
@@ -0,0 +1,28 @@
#pragma once
#include "pdfengine/pdf_document.hpp"
#include <string>
#include <vector>
namespace pdfengine::document {
struct ValidationIssue {
enum class Severity { Warning, Error };
Severity severity = Severity::Warning;
std::string message;
};
struct ValidationResult {
bool isValid = true;
std::vector<ValidationIssue> issues;
};
class DocumentValidator {
public:
DocumentValidator() = default;
// Checks PageModel geometry, non-negative bounds, non-empty words, unicode mappings
ValidationResult validate(const PageModel& model) const;
};
} // namespace pdfengine::document
@@ -0,0 +1,52 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
namespace pdfengine::document {
struct RawOCRWord {
std::string text;
double x = 0.0;
double y = 0.0;
double width = 0.0;
double height = 0.0;
double confidence = 0.0;
std::vector<std::pair<double, double>> polygon;
};
struct RawOCRLine {
std::string text;
double x = 0.0;
double y = 0.0;
double width = 0.0;
double height = 0.0;
double baselineY = 0.0;
double confidence = 0.0;
std::vector<RawOCRWord> words;
std::string fontName;
std::string fontId;
std::string fontFace;
int fontWeight = 400;
std::string fontStyle = "normal";
double fontSize = 0.0;
double lineSpacing = 1.2;
double letterSpacing = 0.0;
bool isBold = false;
bool isItalic = false;
bool isEmbeddedFont = false;
bool isPredictedFont = true;
};
struct RawOCRPage {
int pageIndex = 0;
double imageWidth = 0.0;
double imageHeight = 0.0;
double pageWidth = 0.0;
double pageHeight = 0.0;
std::vector<RawOCRLine> lines;
double processTimeMs = 0.0;
};
} // namespace pdfengine::document
@@ -0,0 +1,64 @@
#pragma once
#include <pdfengine/geometry/rect.hpp>
#include <cmath>
namespace pdfengine {
namespace geometry {
// 2D Affine Transformation Matrix [a b c d e f]
// [ x' ] [ a c e ] [ x ]
// [ y' ] = [ b d f ] [ y ]
// [ 1 ] [ 0 0 1 ] [ 1 ]
struct Matrix {
float a = 1.0f; // Scale X
float b = 0.0f; // Shear Y
float c = 0.0f; // Shear X
float d = 1.0f; // Scale Y
float e = 0.0f; // Translate X
float f = 0.0f; // Translate Y
constexpr Matrix() noexcept = default;
constexpr Matrix(float aVal, float bVal, float cVal, float dVal, float eVal, float fVal) noexcept
: a(aVal), b(bVal), c(cVal), d(dVal), e(eVal), f(fVal) {}
static constexpr Matrix identity() noexcept {
return Matrix(1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f);
}
static constexpr Matrix translation(float tx, float ty) noexcept {
return Matrix(1.0f, 0.0f, 0.0f, 1.0f, tx, ty);
}
static constexpr Matrix scale(float sx, float sy) noexcept {
return Matrix(sx, 0.0f, 0.0f, sy, 0.0f, 0.0f);
}
static Matrix rotation(float radians) noexcept {
float cosA = std::cos(radians);
float sinA = std::sin(radians);
return Matrix(cosA, sinA, -sinA, cosA, 0.0f, 0.0f);
}
[[nodiscard]] Point transformPoint(const Point& pt) const noexcept {
return Point(a * pt.x + c * pt.y + e, b * pt.x + d * pt.y + f);
}
[[nodiscard]] Matrix multiply(const Matrix& other) const noexcept {
return Matrix(
a * other.a + c * other.b,
b * other.a + d * other.b,
a * other.c + c * other.d,
b * other.c + d * other.d,
a * other.e + c * other.f + e,
b * other.e + d * other.f + f
);
}
[[nodiscard]] float getRotationAngle() const noexcept {
return std::atan2(b, a);
}
};
} // namespace geometry
} // namespace pdfengine
@@ -0,0 +1,44 @@
#pragma once
#include <pdfengine/geometry/rect.hpp>
#include <pdfengine/geometry/matrix.hpp>
namespace pdfengine {
namespace geometry {
struct Quad {
Point p1; // Top-Left
Point p2; // Top-Right
Point p3; // Bottom-Right
Point p4; // Bottom-Left
constexpr Quad() noexcept = default;
constexpr Quad(Point pt1, Point pt2, Point pt3, Point pt4) noexcept
: p1(pt1), p2(pt2), p3(pt3), p4(pt4) {}
explicit Quad(const Rect& rect) noexcept
: p1(rect.left(), rect.top()),
p2(rect.right(), rect.top()),
p3(rect.right(), rect.bottom()),
p4(rect.left(), rect.bottom()) {}
[[nodiscard]] Rect boundingBox() const noexcept {
float l = std::min({p1.x, p2.x, p3.x, p4.x});
float r = std::max({p1.x, p2.x, p3.x, p4.x});
float t = std::min({p1.y, p2.y, p3.y, p4.y});
float b = std::max({p1.y, p2.y, p3.y, p4.y});
return Rect(l, t, r - l, b - t);
}
[[nodiscard]] Quad transform(const Matrix& m) const noexcept {
return Quad(
m.transformPoint(p1),
m.transformPoint(p2),
m.transformPoint(p3),
m.transformPoint(p4)
);
}
};
} // namespace geometry
} // namespace pdfengine
@@ -0,0 +1,83 @@
#pragma once
#include <algorithm>
#include <cmath>
namespace pdfengine {
namespace geometry {
struct Point {
float x = 0.0f;
float y = 0.0f;
constexpr Point() noexcept = default;
constexpr Point(float xVal, float yVal) noexcept : x(xVal), y(yVal) {}
};
struct Size {
float width = 0.0f;
float height = 0.0f;
constexpr Size() noexcept = default;
constexpr Size(float w, float h) noexcept : width(w), height(h) {}
};
struct Rect {
float x = 0.0f;
float y = 0.0f;
float width = 0.0f;
float height = 0.0f;
constexpr Rect() noexcept = default;
constexpr Rect(float xVal, float yVal, float w, float h) noexcept
: x(xVal), y(yVal), width(w), height(h) {}
[[nodiscard]] constexpr float left() const noexcept { return x; }
[[nodiscard]] constexpr float top() const noexcept { return y; }
[[nodiscard]] constexpr float right() const noexcept { return x + width; }
[[nodiscard]] constexpr float bottom() const noexcept { return y + height; }
[[nodiscard]] constexpr float centerX() const noexcept { return x + width * 0.5f; }
[[nodiscard]] constexpr float centerY() const noexcept { return y + height * 0.5f; }
[[nodiscard]] constexpr float area() const noexcept { return width * height; }
[[nodiscard]] constexpr bool isEmpty() const noexcept { return width <= 0.0f || height <= 0.0f; }
[[nodiscard]] constexpr bool contains(float px, float py) const noexcept {
return px >= x && px <= right() && py >= y && py <= bottom();
}
[[nodiscard]] constexpr bool contains(const Point& pt) const noexcept {
return contains(pt.x, pt.y);
}
[[nodiscard]] constexpr bool intersects(const Rect& other) const noexcept {
return left() < other.right() && right() > other.left() &&
top() < other.bottom() && bottom() > other.top();
}
[[nodiscard]] constexpr Rect intersectWith(const Rect& other) const noexcept {
float l = std::max(left(), other.left());
float r = std::min(right(), other.right());
float t = std::max(top(), other.top());
float b = std::min(bottom(), other.bottom());
if (l >= r || t >= b) {
return Rect(0.0f, 0.0f, 0.0f, 0.0f);
}
return Rect(l, t, r - l, b - t);
}
[[nodiscard]] constexpr Rect combineWith(const Rect& other) const noexcept {
if (isEmpty()) return other;
if (other.isEmpty()) return *this;
float l = std::min(left(), other.left());
float r = std::max(right(), other.right());
float t = std::min(top(), other.top());
float b = std::max(bottom(), other.bottom());
return Rect(l, t, r - l, b - t);
}
};
} // namespace geometry
} // namespace pdfengine
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <memory>
#include <string>
namespace pdfengine {
class ImageObject;
class IImageCache {
public:
virtual ~IImageCache() = default;
virtual std::shared_ptr<const ImageObject> get(const std::string& key) = 0;
virtual void put(const std::string& key, std::shared_ptr<const ImageObject> image) = 0;
virtual bool has(const std::string& key) const = 0;
virtual void clear() = 0;
};
} // namespace pdfengine
@@ -0,0 +1,29 @@
#pragma once
#include <string>
namespace pdfengine {
enum class ColorSpaceType {
DeviceGray,
DeviceRGB,
DeviceCMYK,
Indexed,
ICCBased,
Lab,
Separation,
DeviceN,
Pattern,
Shading,
Unknown
};
struct ImageColorProfile {
ColorSpaceType colorSpace = ColorSpaceType::Unknown;
std::string colorSpaceName;
int channels = 4;
bool hasAlpha = false;
std::string intent = "RelativeColorimetric";
};
} // namespace pdfengine
@@ -0,0 +1,20 @@
#pragma once
#include <pdfengine/image_descriptor.hpp>
#include <pdfengine/image_pixel_buffer.hpp>
#include <memory>
#include <string>
namespace pdfengine {
class ImageDecoder {
public:
static ImagePixelBuffer decode(const ResolvedImageDescriptor& descriptor);
};
class ImageDecoderFactory {
public:
static bool isSupported(const std::string& filterName);
};
} // namespace pdfengine
@@ -0,0 +1,34 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <cstdint>
#include <pdfengine/image_geometry.hpp>
#include <pdfengine/image_encoding.hpp>
#include <pdfengine/image_color_profile.hpp>
#include <pdfengine/image_mask.hpp>
namespace pdfengine {
struct ResolvedImageDescriptor {
int objectNumber = 0;
int generationNumber = 0;
std::string resourceName;
ImageGeometry geometry;
ImageEncoding encoding;
ImageColorProfile colorProfile;
ImageMask mask;
std::vector<uint8_t> originalStream;
bool hasSoftMask = false;
int softMaskObjectId = 0;
[[nodiscard]] bool isValid() const noexcept {
return geometry.isValid() && !originalStream.empty();
}
};
} // namespace pdfengine
@@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
namespace pdfengine {
struct ImageDiagnostics {
uint64_t decodeTimeUs = 0;
uint64_t originalBytes = 0;
uint64_t decodedBytes = 0;
bool fromCache = false;
bool usedSoftMask = false;
};
struct ImageStatistics {
uint64_t memoryBytes = 0;
uint64_t compressedBytes = 0;
uint64_t decodedBytes = 0;
int width = 0;
int height = 0;
bool hasAlpha = false;
bool hasMask = false;
bool interpolated = false;
};
} // namespace pdfengine
@@ -0,0 +1,16 @@
#pragma once
#include <string>
#include <unordered_map>
namespace pdfengine {
struct ImageEncoding {
std::string filter;
int predictor = 1;
int columns = 0;
int colors = 1;
std::unordered_map<std::string, std::string> decodeParms;
};
} // namespace pdfengine
@@ -0,0 +1,15 @@
#pragma once
namespace pdfengine {
struct ImageGeometry {
int width = 0;
int height = 0;
int bitsPerComponent = 8;
[[nodiscard]] bool isValid() const noexcept {
return width > 0 && height > 0 && bitsPerComponent > 0;
}
};
} // namespace pdfengine
@@ -0,0 +1,26 @@
#pragma once
#include <pdfengine/image_object.hpp>
#include <pdfengine/image_descriptor.hpp>
#include <pdfengine/image_cache.hpp>
#include <memory>
#include <string>
namespace pdfengine {
class ImageManager {
public:
static ImageManager& instance();
std::shared_ptr<const ImageObject> processImageDescriptor(const ResolvedImageDescriptor& descriptor,
std::string* outError = nullptr);
void setCache(std::shared_ptr<IImageCache> cache);
std::shared_ptr<IImageCache> cache() const;
private:
ImageManager() = default;
std::shared_ptr<IImageCache> m_cache;
};
} // namespace pdfengine
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <memory>
namespace pdfengine {
class ImageObject;
enum class MaskType {
None,
ExplicitMask,
SoftMask
};
struct ImageMask {
MaskType type = MaskType::None;
std::shared_ptr<const ImageObject> maskImage;
[[nodiscard]] bool hasMask() const noexcept {
return type != MaskType::None && maskImage != nullptr;
}
};
} // namespace pdfengine
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <cstdint>
#include <atomic>
#include <pdfengine/image_geometry.hpp>
#include <pdfengine/image_encoding.hpp>
#include <pdfengine/image_color_profile.hpp>
#include <pdfengine/image_pixel_buffer.hpp>
#include <pdfengine/image_mask.hpp>
#include <pdfengine/image_diagnostics.hpp>
namespace pdfengine {
using ImageId = uint64_t;
ImageId generateNextImageId();
class ImageObject {
public:
ImageObject(ImageId id,
std::string name,
ImageGeometry geometry,
ImageEncoding encoding,
ImageColorProfile colorProfile,
ImagePixelBuffer pixels,
ImageMask mask,
std::vector<uint8_t> originalStream,
ImageDiagnostics diagnostics = {},
ImageStatistics statistics = {})
: m_id(id),
m_name(std::move(name)),
m_geometry(geometry),
m_encoding(std::move(encoding)),
m_colorProfile(std::move(colorProfile)),
m_pixels(std::move(pixels)),
m_mask(std::move(mask)),
m_originalStream(std::move(originalStream)),
m_diagnostics(diagnostics),
m_statistics(statistics) {}
[[nodiscard]] ImageId id() const noexcept { return m_id; }
[[nodiscard]] const std::string& name() const noexcept { return m_name; }
[[nodiscard]] const ImageGeometry& geometry() const noexcept { return m_geometry; }
[[nodiscard]] const ImageEncoding& encoding() const noexcept { return m_encoding; }
[[nodiscard]] const ImageColorProfile& colorProfile() const noexcept { return m_colorProfile; }
[[nodiscard]] const ImagePixelBuffer& pixels() const noexcept { return m_pixels; }
[[nodiscard]] const ImageMask& mask() const noexcept { return m_mask; }
[[nodiscard]] const std::vector<uint8_t>& originalStream() const noexcept { return m_originalStream; }
[[nodiscard]] const ImageDiagnostics& diagnostics() const noexcept { return m_diagnostics; }
[[nodiscard]] const ImageStatistics& statistics() const noexcept { return m_statistics; }
[[nodiscard]] bool isValid() const noexcept {
return m_id != 0 && m_geometry.isValid() && !m_pixels.empty();
}
[[nodiscard]] size_t byteSize() const noexcept {
return sizeof(*this) + m_pixels.size() + m_originalStream.size();
}
private:
ImageId m_id = 0;
std::string m_name;
ImageGeometry m_geometry;
ImageEncoding m_encoding;
ImageColorProfile m_colorProfile;
ImagePixelBuffer m_pixels;
ImageMask m_mask;
std::vector<uint8_t> m_originalStream;
ImageDiagnostics m_diagnostics;
ImageStatistics m_statistics;
};
} // namespace pdfengine
@@ -0,0 +1,14 @@
#pragma once
#include <pdfengine/image_object.hpp>
#include <pdfengine/image_descriptor.hpp>
#include <memory>
namespace pdfengine {
class ImagePipeline {
public:
static std::shared_ptr<const ImageObject> process(const ResolvedImageDescriptor& descriptor);
};
} // namespace pdfengine
@@ -0,0 +1,82 @@
#pragma once
#include <vector>
#include <cstdint>
#include <cstddef>
#include <algorithm>
namespace pdfengine {
struct PixelRGBA {
uint8_t r = 0;
uint8_t g = 0;
uint8_t b = 0;
uint8_t a = 255;
};
class ImagePixelBuffer {
public:
ImagePixelBuffer() = default;
ImagePixelBuffer(int width, int height, int channels = 4)
: m_width(width), m_height(height), m_channels(channels) {
if (width > 0 && height > 0 && channels > 0) {
m_pixels.resize(static_cast<size_t>(width) * height * channels, 0);
}
}
[[nodiscard]] int width() const noexcept { return m_width; }
[[nodiscard]] int height() const noexcept { return m_height; }
[[nodiscard]] int channels() const noexcept { return m_channels; }
[[nodiscard]] size_t stride() const noexcept { return static_cast<size_t>(m_width) * m_channels; }
[[nodiscard]] size_t size() const noexcept { return m_pixels.size(); }
[[nodiscard]] bool empty() const noexcept { return m_pixels.empty(); }
[[nodiscard]] const uint8_t* data() const noexcept { return m_pixels.data(); }
[[nodiscard]] uint8_t* data() noexcept { return m_pixels.data(); }
[[nodiscard]] const std::vector<uint8_t>& vector() const noexcept { return m_pixels; }
[[nodiscard]] std::vector<uint8_t>& vector() noexcept { return m_pixels; }
void resize(int width, int height, int channels = 4) {
m_width = width;
m_height = height;
m_channels = channels;
if (width > 0 && height > 0 && channels > 0) {
m_pixels.assign(static_cast<size_t>(width) * height * channels, 0);
} else {
m_pixels.clear();
}
}
[[nodiscard]] PixelRGBA getPixel(int x, int y) const noexcept {
if (x < 0 || x >= m_width || y < 0 || y >= m_height || m_channels < 4) {
return {};
}
size_t idx = (static_cast<size_t>(y) * m_width + x) * m_channels;
if (idx + 3 < m_pixels.size()) {
return PixelRGBA{m_pixels[idx], m_pixels[idx + 1], m_pixels[idx + 2], m_pixels[idx + 3]};
}
return {};
}
void setPixel(int x, int y, uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255) noexcept {
if (x < 0 || x >= m_width || y < 0 || y >= m_height || m_channels < 4) {
return;
}
size_t idx = (static_cast<size_t>(y) * m_width + x) * m_channels;
if (idx + 3 < m_pixels.size()) {
m_pixels[idx + 0] = r;
m_pixels[idx + 1] = g;
m_pixels[idx + 2] = b;
m_pixels[idx + 3] = a;
}
}
private:
int m_width = 0;
int m_height = 0;
int m_channels = 4;
std::vector<uint8_t> m_pixels;
};
} // namespace pdfengine
@@ -0,0 +1,15 @@
#pragma once
#include <pdfengine/image_geometry.hpp>
#include <pdfengine/image_descriptor.hpp>
#include <string>
namespace pdfengine {
class ImageValidator {
public:
static bool validate(const ImageGeometry& geometry, std::string* outError = nullptr);
static bool validate(const ResolvedImageDescriptor& descriptor, std::string* outError = nullptr);
};
} // namespace pdfengine
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <vector>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <utility>
namespace pdfengine {
class LayoutArena {
public:
explicit LayoutArena(size_t blockSizeBytes = 65536);
~LayoutArena();
// Disable copy
LayoutArena(const LayoutArena&) = delete;
LayoutArena& operator=(const LayoutArena&) = delete;
// Enable move
LayoutArena(LayoutArena&&) noexcept;
LayoutArena& operator=(LayoutArena&&) noexcept;
template <typename T, typename... Args>
T* allocate(Args&&... args) {
void* mem = allocateBytes(sizeof(T), alignof(T));
return ::new (mem) T(std::forward<Args>(args)...);
}
void* allocateBytes(size_t size, size_t alignment = alignof(std::max_align_t));
void clear() noexcept;
[[nodiscard]] size_t totalAllocatedBytes() const noexcept;
private:
struct Chunk {
std::unique_ptr<uint8_t[]> data;
size_t size = 0;
size_t used = 0;
};
size_t m_defaultChunkSize = 65536;
std::vector<Chunk> m_chunks;
size_t m_totalAllocated = 0;
};
} // namespace pdfengine
@@ -0,0 +1,23 @@
#pragma once
#include <pdfengine/layout_session.hpp>
#include <pdfengine/pass_registry.hpp>
#include <memory>
namespace pdfengine {
class LayoutEngine {
public:
static LayoutEngine& instance();
PassRegistry& registry() noexcept { return m_registry; }
const PassRegistry& registry() const noexcept { return m_registry; }
std::shared_ptr<PhysicalLayoutTree> processPage(const std::string& documentId, int pageIndex, float width, float height);
private:
LayoutEngine();
PassRegistry m_registry;
};
} // namespace pdfengine
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <pdfengine/layout_session.hpp>
namespace pdfengine {
class ILayoutPass {
public:
virtual ~ILayoutPass() = default;
[[nodiscard]] virtual std::string name() const noexcept = 0;
virtual bool execute(LayoutSession& session) = 0;
};
} // namespace pdfengine
@@ -0,0 +1,60 @@
#pragma once
#include <pdfengine/layout_types.hpp>
#include <pdfengine/layout_tree.hpp>
#include <pdfengine/layout_arena.hpp>
#include <pdfengine/multi_level_cache.hpp>
#include <pdfengine/spatial_index.hpp>
#include <string>
#include <vector>
#include <memory>
#include <atomic>
#include <chrono>
namespace pdfengine {
struct PipelineContext {
std::string documentId;
int pageIndex = 0;
float pageWidth = 0.0f;
float pageHeight = 0.0f;
std::shared_ptr<PhysicalLayoutTree> physicalTree;
std::shared_ptr<LogicalLayoutTree> logicalTree;
std::shared_ptr<MultiIndexSpatialIndex> spatialIndex;
std::vector<LayoutDiagnostic> diagnostics;
LayoutStatistics stats;
};
class LayoutSession {
public:
LayoutSession(std::string documentId, int pageIndex, float width, float height);
~LayoutSession() = default;
// Disable copy
LayoutSession(const LayoutSession&) = delete;
LayoutSession& operator=(const LayoutSession&) = delete;
[[nodiscard]] PipelineContext& context() noexcept { return m_context; }
[[nodiscard]] const PipelineContext& context() const noexcept { return m_context; }
[[nodiscard]] LayoutArena& arena() noexcept { return m_arena; }
[[nodiscard]] const LayoutArena& arena() const noexcept { return m_arena; }
void cancel() noexcept { m_cancelled.store(true); }
[[nodiscard]] bool isCancelled() const noexcept { return m_cancelled.load(); }
void logDiagnostic(LayoutDiagnostic::Severity severity, const std::string& passName, const std::string& message, const std::string& blockId = "");
void finish();
private:
PipelineContext m_context;
LayoutArena m_arena;
std::atomic<bool> m_cancelled{false};
std::chrono::high_resolution_clock::time_point m_startTime;
};
} // namespace pdfengine
+96
View File
@@ -0,0 +1,96 @@
#pragma once
#include <pdfengine/layout_types.hpp>
#include <pdfengine/geometry/rect.hpp>
#include <pdfengine/geometry/matrix.hpp>
#include <pdfengine/geometry/quad.hpp>
#include <string>
#include <vector>
#include <memory>
namespace pdfengine {
struct LayoutGlyph {
std::string character;
uint32_t charCode = 0;
geometry::Rect bounds;
geometry::Matrix transform;
float advanceWidth = 0.0f;
};
struct TextRun {
std::string text;
TextStyle style;
geometry::Rect bounds;
geometry::Matrix transform;
std::vector<LayoutGlyph> glyphs;
};
struct LayoutLine {
std::string text;
geometry::Rect bounds;
float baselineY = 0.0f;
float lineHeight = 12.0f;
std::vector<TextRun> runs;
};
struct LayoutBlock {
std::string id;
LayoutBlockType type = LayoutBlockType::Paragraph;
geometry::Rect bounds;
geometry::Matrix transform;
int zIndex = 0;
int readingOrder = 0;
BlockPermissions permissions;
VisualStyle visualStyle;
TextStyle textStyle;
LayoutStyle layoutStyle;
std::vector<LayoutLine> children;
std::string parentId;
std::vector<std::string> dependsOn; // Block ID dependencies (e.g. caption -> image)
size_t layoutTreeVersion = 1;
size_t contentRevision = 1;
size_t layoutRevision = 1;
std::string sourceObjectId;
};
enum class RegionType {
Header,
Body,
Sidebar,
Footer
};
struct PageRegion {
std::string id;
RegionType type = RegionType::Body;
geometry::Rect bounds;
std::vector<std::shared_ptr<LayoutBlock>> blocks;
};
struct PhysicalLayoutTree {
int pageIndex = 0;
float width = 0.0f;
float height = 0.0f;
std::vector<PageRegion> regions;
std::vector<std::shared_ptr<LayoutBlock>> allBlocks;
};
struct LogicalLayoutNode {
std::string id;
std::string title;
LayoutBlockType type = LayoutBlockType::Paragraph;
std::shared_ptr<LayoutBlock> block;
std::vector<std::shared_ptr<LogicalLayoutNode>> children;
};
struct LogicalLayoutTree {
std::string documentTitle;
std::vector<std::shared_ptr<LogicalLayoutNode>> sections;
};
} // namespace pdfengine
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include <pdfengine/geometry/rect.hpp>
#include <pdfengine/geometry/matrix.hpp>
namespace pdfengine {
enum class LayoutBlockType {
Paragraph,
Heading,
Title,
Caption,
Quote,
List,
Table,
TableCell,
Image,
Figure,
Form,
Shape,
Header,
Footer,
Watermark,
Annotation,
CodeBlock,
Math,
TOC,
Footnote,
Signature,
Unknown
};
enum class TextAlignment {
Left,
Center,
Right,
Justify
};
struct VisualStyle {
std::string fillColor = "transparent";
std::string strokeColor = "none";
float strokeWidth = 0.0f;
float opacity = 1.0f;
float cornerRadius = 0.0f;
std::string shadowColor = "none";
};
struct TextStyle {
std::string fontName = "Helvetica";
float fontSize = 12.0f;
std::string fontColor = "#000000";
bool isBold = false;
bool isItalic = false;
float letterSpacing = 0.0f;
float lineSpacing = 1.2f;
};
struct LayoutStyle {
TextAlignment alignment = TextAlignment::Left;
float paddingTop = 0.0f;
float paddingRight = 0.0f;
float paddingBottom = 0.0f;
float paddingLeft = 0.0f;
float marginTop = 0.0f;
float marginRight = 0.0f;
float marginBottom = 0.0f;
float marginLeft = 0.0f;
int zIndex = 0;
};
struct BlockPermissions {
bool editable = true;
bool selectable = true;
bool movable = true;
bool resizable = true;
bool printable = true;
};
struct LayoutDiagnostic {
enum class Severity { Info, Warning, Error };
Severity severity = Severity::Info;
std::string passName;
std::string message;
int pageIndex = 0;
std::string blockId;
double durationMs = 0.0;
};
struct LayoutStatistics {
size_t textRuns = 0;
size_t blocks = 0;
size_t paragraphs = 0;
size_t tables = 0;
size_t columns = 0;
double decodeTimeMs = 0.0;
double layoutTimeMs = 0.0;
size_t memoryBytes = 0;
size_t cacheHits = 0;
size_t cacheMisses = 0;
};
} // namespace pdfengine
@@ -0,0 +1,61 @@
#pragma once
#include <pdfengine/layout_tree.hpp>
#include <pdfengine/spatial_index.hpp>
#include <unordered_map>
#include <mutex>
#include <optional>
#include <string>
#include <memory>
namespace pdfengine {
struct CacheKey {
std::string documentId;
int pageIndex = 0;
size_t contentRevision = 1;
size_t layoutRevision = 1;
bool operator==(const CacheKey& other) const noexcept {
return documentId == other.documentId &&
pageIndex == other.pageIndex &&
contentRevision == other.contentRevision &&
layoutRevision == other.layoutRevision;
}
};
struct CacheKeyHash {
size_t operator()(const CacheKey& k) const noexcept {
size_t h1 = std::hash<std::string>{}(k.documentId);
size_t h2 = std::hash<int>{}(k.pageIndex);
size_t h3 = std::hash<size_t>{}(k.contentRevision);
size_t h4 = std::hash<size_t>{}(k.layoutRevision);
return h1 ^ (h2 << 1) ^ (h3 << 2) ^ (h4 << 3);
}
};
class MultiLevelCache {
public:
static MultiLevelCache& instance();
void putLayoutTree(const CacheKey& key, std::shared_ptr<PhysicalLayoutTree> tree);
[[nodiscard]] std::shared_ptr<PhysicalLayoutTree> getLayoutTree(const CacheKey& key) const;
void putSpatialIndex(const CacheKey& key, std::shared_ptr<MultiIndexSpatialIndex> index);
[[nodiscard]] std::shared_ptr<MultiIndexSpatialIndex> getSpatialIndex(const CacheKey& key) const;
void invalidatePage(const std::string& documentId, int pageIndex);
void invalidateDocument(const std::string& documentId);
void clear();
[[nodiscard]] size_t size() const;
private:
MultiLevelCache() = default;
mutable std::mutex m_mutex;
std::unordered_map<CacheKey, std::shared_ptr<PhysicalLayoutTree>, CacheKeyHash> m_layoutCache;
std::unordered_map<CacheKey, std::shared_ptr<MultiIndexSpatialIndex>, CacheKeyHash> m_spatialCache;
};
} // namespace pdfengine
@@ -0,0 +1,16 @@
#pragma once
#include <vector>
#include <cstdint>
namespace pdfengine::ocr {
class ImageCleaner {
public:
ImageCleaner() = default;
// Binarize or despackle raster pixel data if needed
std::vector<uint8_t> cleanImage(const std::vector<uint8_t>& bgra, int width, int height) const;
};
} // namespace pdfengine::ocr
@@ -0,0 +1,28 @@
#pragma once
#include "pdfengine/pdf_document.hpp"
#include "pdfengine/document/raw_ocr_document.hpp"
#include "pdfengine/document/document_normalizer.hpp"
#include "pdfengine/document/document_builder.hpp"
#include "pdfengine/document/document_validator.hpp"
#include "pdfengine/ocr/image_cleaner.hpp"
#include "pdfengine/ocr/ocr_importer.hpp"
namespace pdfengine::ocr {
class OCRCoordinator {
public:
OCRCoordinator() = default;
// Single Facade API: orchestrates Preprocessing -> OCRImporter -> Engine::Normalizer -> Engine::Builder -> Engine::Validator -> Native PageModel
pdfengine::PageModel processDocument(
int pageIndex,
double imgW,
double imgH,
double pdfW,
double pdfH,
const std::vector<pdfengine::document::RawOCRLine>& lines
) const;
};
} // namespace pdfengine::ocr
@@ -0,0 +1,22 @@
#pragma once
#include "pdfengine/document/raw_ocr_document.hpp"
#include <vector>
namespace pdfengine::ocr {
class OCRImporter {
public:
OCRImporter() = default;
pdfengine::document::RawOCRPage importOCRPage(
int pageIndex,
double imgWidth,
double imgHeight,
double pdfWidth,
double pdfHeight,
const std::vector<pdfengine::document::RawOCRLine>& lines
) const;
};
} // namespace pdfengine::ocr
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <pdfengine/ocr_types.hpp>
#include <unordered_map>
#include <mutex>
#include <optional>
#include <string>
namespace pdfengine {
class OCRCache {
public:
static OCRCache& instance();
void put(const std::string& imageHash, const OCRPage& ocrPage);
[[nodiscard]] std::optional<OCRPage> get(const std::string& imageHash) const;
void clear();
[[nodiscard]] size_t size() const;
private:
OCRCache() = default;
mutable std::mutex m_mutex;
std::unordered_map<std::string, OCRPage> m_cache;
};
} // namespace pdfengine
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <pdfengine/ocr_types.hpp>
#include <pdfengine/image_object.hpp>
#include <memory>
#include <string>
namespace pdfengine {
class IOCREngine {
public:
virtual ~IOCREngine() = default;
[[nodiscard]] virtual bool isAvailable() const noexcept = 0;
[[nodiscard]] virtual OCRPage processImage(const ImageObject& image, const std::string& lang = "en") = 0;
};
} // namespace pdfengine
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
namespace pdfengine {
struct OCRPoint {
float x = 0.0f;
float y = 0.0f;
};
struct OCRRect {
float x = 0.0f;
float y = 0.0f;
float width = 0.0f;
float height = 0.0f;
};
struct OCRWord {
std::string text;
OCRRect box;
std::vector<OCRPoint> polygon; // 4 corner points [[x1,y1],[x2,y2],[x3,y3],[x4,y4]]
float confidence = 0.0f;
};
struct OCRLine {
std::string text;
OCRRect box;
std::vector<OCRWord> words;
float baselineY = 0.0f;
float confidence = 0.0f;
};
struct OCRPage {
int pageIndex = 0;
int imageWidth = 0;
int imageHeight = 0;
std::vector<OCRLine> lines;
std::string language = "en";
double processTimeMs = 0.0;
};
} // namespace pdfengine
@@ -0,0 +1,25 @@
#pragma once
#include <pdfengine/layout_pass.hpp>
#include <vector>
#include <memory>
#include <string>
namespace pdfengine {
class PassRegistry {
public:
PassRegistry() = default;
void registerPass(std::unique_ptr<ILayoutPass> pass);
void clear();
bool executeAll(LayoutSession& session);
[[nodiscard]] size_t passCount() const noexcept { return m_passes.size(); }
private:
std::vector<std::unique_ptr<ILayoutPass>> m_passes;
};
} // namespace pdfengine
@@ -142,10 +142,15 @@ struct Glyph {
struct TextRun {
std::string text;
std::string fontName;
std::string fontFace;
int fontWeight = 400;
std::string fontStyle = "normal";
uint32_t flags = 0;
double fontSize = 0.0;
std::string internalFontId;
bool isEmbedded = false;
bool isEmbeddedFont = false;
bool isPredictedFont = false;
std::string type;
std::vector<Glyph> glyphs;
std::vector<int> objectIndices;
@@ -0,0 +1,35 @@
#pragma once
#include <pdfengine/layout_tree.hpp>
#include <pdfengine/geometry/rect.hpp>
#include <vector>
#include <memory>
#include <string>
namespace pdfengine {
class MultiIndexSpatialIndex {
public:
MultiIndexSpatialIndex() = default;
void buildFromTree(const PhysicalLayoutTree& tree);
void clear();
[[nodiscard]] std::vector<std::shared_ptr<LayoutBlock>> queryBlocksAtPoint(float x, float y) const;
[[nodiscard]] std::vector<std::shared_ptr<LayoutBlock>> queryBlocksInRect(const geometry::Rect& rect) const;
[[nodiscard]] std::vector<std::shared_ptr<LayoutBlock>> queryBlocksByType(LayoutBlockType type) const;
[[nodiscard]] size_t totalIndexedBlocks() const noexcept { return m_indexedBlocks.size(); }
private:
struct SpatialEntry {
geometry::Rect bounds;
std::shared_ptr<LayoutBlock> block;
};
std::vector<SpatialEntry> m_indexedBlocks;
std::vector<SpatialEntry> m_indexedLines;
};
} // namespace pdfengine
+4
View File
@@ -60,4 +60,8 @@ void DisplayList::drawImage(const ImageInfo& image, const Matrix& m, float opaci
addCommand(std::make_unique<DrawImageCommand>(image, m, opacity));
}
void DisplayList::drawImage(std::shared_ptr<const ImageObject> image, const Matrix& m, float opacity, BlendMode blend) {
addCommand(std::make_unique<DrawImageCommand>(std::move(image), m, opacity, blend));
}
}
+178
View File
@@ -0,0 +1,178 @@
#include "pdfengine/document/document_builder.hpp"
#include <algorithm>
#include <cmath>
namespace pdfengine::document {
PageModel DocumentBuilder::buildFromOCR(const RawOCRPage& rawPage) const {
PageModel model;
model.pageIndex = rawPage.pageIndex;
model.width = rawPage.pageWidth;
model.height = rawPage.pageHeight;
if (rawPage.lines.empty()) {
return model;
}
// Standard baseline clustering to group lines into Paragraphs
std::vector<Paragraph> paragraphs;
Paragraph currentPara;
double lastLineY = -1.0;
double lastLineH = 0.0;
for (size_t lIdx = 0; lIdx < rawPage.lines.size(); ++lIdx) {
const auto& rawLine = rawPage.lines[lIdx];
TextLine line;
line.x = rawLine.x;
line.y = rawLine.y;
line.w = rawLine.width;
line.h = rawLine.height;
line.baselineY = rawLine.baselineY;
line.angle = 0.0;
TextRun run;
run.text = rawLine.text;
run.fontName = rawLine.fontName.empty() ? "Helvetica" : rawLine.fontName;
bool isBold = rawLine.isBold || rawLine.fontWeight >= 600;
bool isItalic = rawLine.isItalic || rawLine.fontStyle == "italic";
run.flags = (isBold ? 2 : 0) | (isItalic ? 1 : 0);
run.fontWeight = rawLine.fontWeight > 0 ? rawLine.fontWeight : (isBold ? 700 : 400);
run.fontStyle = !rawLine.fontStyle.empty() ? rawLine.fontStyle : (isItalic ? "italic" : "normal");
if (!rawLine.fontFace.empty()) {
run.fontFace = rawLine.fontFace;
} else if (isBold && isItalic) {
run.fontFace = run.fontName + "-BoldItalic";
} else if (isBold) {
run.fontFace = run.fontName + "-Bold";
} else if (isItalic) {
run.fontFace = run.fontName + "-Italic";
} else {
run.fontFace = run.fontName;
}
run.internalFontId = !rawLine.fontId.empty()
? rawLine.fontId
: (run.fontName + "_TrueType_" + std::to_string(run.fontWeight));
run.fontSize = rawLine.fontSize > 0.0 ? rawLine.fontSize : (rawLine.height * 0.72);
run.x = rawLine.x;
run.y = rawLine.y;
run.w = rawLine.width;
run.h = rawLine.height;
run.fillColor = "#000000";
run.fontFidelity = "exact";
// Generate Glyphs
if (!rawLine.words.empty()) {
for (const auto& w : rawLine.words) {
double charW = w.width / std::max<size_t>(1, w.text.size());
double currX = w.x;
for (char ch : w.text) {
Glyph g;
g.text = std::string(1, ch);
g.unicode = static_cast<uint32_t>(static_cast<unsigned char>(ch));
g.fontName = run.fontName;
g.fontSize = run.fontSize;
g.flags = run.flags;
g.originX = currX;
g.originY = rawLine.baselineY;
g.bboxX = currX;
g.bboxY = w.y;
g.bboxW = charW;
g.bboxH = w.height;
run.glyphs.push_back(g);
line.glyphs.push_back(g);
currX += charW;
}
}
} else {
double charW = rawLine.width / std::max<size_t>(1, rawLine.text.size());
double currX = rawLine.x;
for (char ch : rawLine.text) {
Glyph g;
g.text = std::string(1, ch);
g.unicode = static_cast<uint32_t>(static_cast<unsigned char>(ch));
g.fontName = run.fontName;
g.fontSize = run.fontSize;
g.flags = run.flags;
g.originX = currX;
g.originY = rawLine.baselineY;
g.bboxX = currX;
g.bboxY = rawLine.y;
g.bboxW = charW;
g.bboxH = rawLine.height;
run.glyphs.push_back(g);
line.glyphs.push_back(g);
currX += charW;
}
}
line.runs.push_back(run);
// Check paragraph break (vertical gap > 1.5x line height)
bool isNewPara = false;
if (lastLineY >= 0.0) {
double gap = rawLine.y - (lastLineY + lastLineH);
if (gap > lastLineH * 1.5 || gap < -lastLineH * 0.5) {
isNewPara = true;
}
}
if (isNewPara && !currentPara.lines.empty()) {
// Finalize current paragraph bounds
double pX1 = currentPara.lines.front().x;
double pY1 = currentPara.lines.front().y;
double pX2 = pX1 + currentPara.lines.front().w;
double pY2 = pY1 + currentPara.lines.front().h;
for (const auto& l : currentPara.lines) {
pX1 = std::min(pX1, l.x);
pY1 = std::min(pY1, l.y);
pX2 = std::max(pX2, l.x + l.w);
pY2 = std::max(pY2, l.y + l.h);
}
currentPara.x = pX1;
currentPara.y = pY1;
currentPara.w = pX2 - pX1;
currentPara.h = pY2 - pY1;
paragraphs.push_back(currentPara);
currentPara = Paragraph();
}
currentPara.lines.push_back(line);
lastLineY = rawLine.y;
lastLineH = rawLine.height;
}
if (!currentPara.lines.empty()) {
double pX1 = currentPara.lines.front().x;
double pY1 = currentPara.lines.front().y;
double pX2 = pX1 + currentPara.lines.front().w;
double pY2 = pY1 + currentPara.lines.front().h;
for (const auto& l : currentPara.lines) {
pX1 = std::min(pX1, l.x);
pY1 = std::min(pY1, l.y);
pX2 = std::max(pX2, l.x + l.w);
pY2 = std::max(pY2, l.y + l.h);
}
currentPara.x = pX1;
currentPara.y = pY1;
currentPara.w = pX2 - pX1;
currentPara.h = pY2 - pY1;
paragraphs.push_back(currentPara);
}
model.paragraphs = std::move(paragraphs);
return model;
}
} // namespace pdfengine::document
@@ -0,0 +1,55 @@
#include "pdfengine/document/document_normalizer.hpp"
#include <algorithm>
#include <cmath>
namespace pdfengine::document {
RawOCRPage DocumentNormalizer::normalize(const RawOCRPage& rawPage) const {
RawOCRPage page = rawPage;
if (page.pageWidth <= 0.0) page.pageWidth = page.imageWidth > 0.0 ? page.imageWidth : 612.0;
if (page.pageHeight <= 0.0) page.pageHeight = page.imageHeight > 0.0 ? page.imageHeight : 792.0;
double scaleX = 1.0;
double scaleY = 1.0;
if (page.imageWidth > 0.0 && std::abs(page.imageWidth - page.pageWidth) > 0.01) {
scaleX = page.pageWidth / page.imageWidth;
}
if (page.imageHeight > 0.0 && std::abs(page.imageHeight - page.pageHeight) > 0.01) {
scaleY = page.pageHeight / page.imageHeight;
}
for (auto& line : page.lines) {
line.x = std::max(0.0, line.x * scaleX);
line.y = std::max(0.0, line.y * scaleY);
line.width = std::max(1.0, line.width * scaleX);
line.height = std::max(1.0, line.height * scaleY);
if (line.fontSize <= 0.0) {
line.fontSize = line.height * 0.72;
}
if (line.baselineY <= 0.0) {
line.baselineY = line.y + line.fontSize;
} else {
line.baselineY *= scaleY;
}
for (auto& word : line.words) {
word.x = std::max(0.0, word.x * scaleX);
word.y = std::max(0.0, word.y * scaleY);
word.width = std::max(0.5, word.width * scaleX);
word.height = std::max(0.5, word.height * scaleY);
for (auto& pt : word.polygon) {
pt.first *= scaleX;
pt.second *= scaleY;
}
}
}
return page;
}
} // namespace pdfengine::document
@@ -0,0 +1,36 @@
#include "pdfengine/document/document_validator.hpp"
namespace pdfengine::document {
ValidationResult DocumentValidator::validate(const PageModel& model) const {
ValidationResult res;
if (model.width <= 0.0 || model.height <= 0.0) {
res.issues.push_back({ValidationIssue::Severity::Error, "Page width or height is invalid/negative"});
res.isValid = false;
}
for (size_t pIdx = 0; pIdx < model.paragraphs.size(); ++pIdx) {
const auto& para = model.paragraphs[pIdx];
if (para.w < 0.0 || para.h < 0.0) {
res.issues.push_back({ValidationIssue::Severity::Warning, "Paragraph " + std::to_string(pIdx) + " has negative dimensions"});
}
for (size_t lIdx = 0; lIdx < para.lines.size(); ++lIdx) {
const auto& line = para.lines[lIdx];
if (line.w < 0.0 || line.h < 0.0) {
res.issues.push_back({ValidationIssue::Severity::Warning, "TextLine has negative dimensions"});
}
for (const auto& run : line.runs) {
if (run.fontSize <= 0.0) {
res.issues.push_back({ValidationIssue::Severity::Warning, "TextRun fontSize <= 0.0"});
}
}
}
}
return res;
}
} // namespace pdfengine::document
@@ -1,6 +1,7 @@
#include "fonts/pdf_fonts/embedded_font_reconstructor.hpp"
#include <map>
#include <spdlog/spdlog.h>
#include "fonts/pdf_fonts/font_cmap_builder.hpp"
@@ -34,7 +35,7 @@ ReconstructedFont EmbeddedFontReconstructor::reconstruct(
if (cmap.empty()) return out;
std::vector<uint8_t> sfnt = FontCmapBuilder::spliceCmapIntoSfnt(program, cmap);
if (sfnt.empty()) return out;
spdlog::info("[RECONSTRUCTOR] Input={} Output={}", program.size(), sfnt.size());
out.sfnt = std::move(sfnt);
for (const auto& [uni, gid] : unicodeToGid) out.coveredUnicode.insert(uni);
out.ok = true;
@@ -0,0 +1,49 @@
#include "font_extraction_service.hpp"
#include "font_validator.hpp"
#include "../../qpdf/qpdf_font_extractor.hpp"
#include <spdlog/spdlog.h>
namespace pdfengine::fonts::pdf_fonts {
std::expected<std::shared_ptr<EmbeddedFontProgram>, EngineError>
FontExtractionService::getFontProgram(const std::string& internalFontId,
const std::string& baseFontName,
const std::vector<uint8_t>& documentBuffer,
const std::vector<uint8_t>& pdfiumRawBytes) {
std::lock_guard<std::mutex> lock(cacheMutex_);
if (cache_.count(internalFontId)) {
return cache_[internalFontId];
}
// 1. Try PDFium Bytes (Standard fonts)
if (!pdfiumRawBytes.empty() && FontValidator::isValidSFNT(pdfiumRawBytes)) {
auto prog = std::make_shared<EmbeddedFontProgram>();
prog->fontName = baseFontName;
prog->bytes = pdfiumRawBytes;
prog->source = FontExtractionSource::PDFium;
cache_[internalFontId] = prog;
spdlog::info("[FontExtraction] internalId='{}' source=PDFium size={}", internalFontId, prog->bytes.size());
return prog;
}
// 2. Try QPDF Fallback (Type0 CIDFonts)
if (!documentBuffer.empty()) {
qpdf_layer::QpdfFontExtractor qpdfExtractor;
auto qpdfProg = qpdfExtractor.extractFontProgram(documentBuffer, baseFontName);
if (qpdfProg && FontValidator::isValidSFNT(qpdfProg->bytes)) {
auto prog = std::make_shared<EmbeddedFontProgram>(*qpdfProg);
prog->source = FontExtractionSource::QPDF_Raw;
cache_[internalFontId] = prog;
spdlog::info("[FontExtraction] internalId='{}' source=QPDF_Raw size={}", internalFontId, prog->bytes.size());
return prog;
}
}
// Tier 2 Reconstruction would happen here if needed, but for now we return not found
spdlog::warn("[FontExtraction] Failed to extract valid SFNT for '{}'", internalFontId);
return std::unexpected(EngineError::FileNotFound);
}
} // namespace pdfengine::fonts::pdf_fonts
@@ -0,0 +1,37 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
#include <memory>
#include <mutex>
#include <optional>
#include <expected>
#include "font_extraction_types.hpp"
#include <pdfengine/pdf_document.hpp>
namespace pdfengine::fonts::pdf_fonts {
class FontExtractionService {
public:
static FontExtractionService& getInstance() {
static FontExtractionService instance;
return instance;
}
std::expected<std::shared_ptr<EmbeddedFontProgram>, EngineError>
getFontProgram(const std::string& internalFontId,
const std::string& baseFontName,
const std::vector<uint8_t>& documentBuffer,
const std::vector<uint8_t>& pdfiumRawBytes);
private:
FontExtractionService() = default;
std::mutex cacheMutex_;
std::unordered_map<std::string, std::shared_ptr<EmbeddedFontProgram>> cache_;
};
} // namespace pdfengine::fonts::pdf_fonts
@@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace pdfengine::fonts::pdf_fonts {
enum class FontProgramType { Unknown, TrueType, CFF, OpenType, Type1 };
enum class FontExtractionSource { PDFium, QPDF_Raw, QPDF_Reconstructed, Cache };
struct EmbeddedFontProgram {
std::string fontName;
std::vector<uint8_t> bytes;
FontProgramType type = FontProgramType::Unknown;
FontExtractionSource source = FontExtractionSource::PDFium;
};
} // namespace pdfengine::fonts::pdf_fonts
@@ -0,0 +1,28 @@
#include "font_validator.hpp"
namespace pdfengine::fonts::pdf_fonts {
bool FontValidator::isValidSFNT(const std::vector<uint8_t>& bytes) {
if (bytes.size() < 12) { // Minimum SFNT header size
return false;
}
// Check SFNT version / magic bytes
// 0x00010000 for TrueType
// 0x4F54544F ("OTTO") for OpenType CFF
// 0x74727565 ("true") for Apple TrueType
// 0x74797031 ("typ1") for Mac PostScript Type 1
const uint8_t* b = bytes.data();
uint32_t magic = (static_cast<uint32_t>(b[0]) << 24) |
(static_cast<uint32_t>(b[1]) << 16) |
(static_cast<uint32_t>(b[2]) << 8) |
static_cast<uint32_t>(b[3]);
if (magic == 0x00010000 || magic == 0x4F54544F || magic == 0x74727565 || magic == 0x74797031) {
return true;
}
return false;
}
} // namespace pdfengine::fonts::pdf_fonts
@@ -0,0 +1,13 @@
#pragma once
#include <cstdint>
#include <vector>
namespace pdfengine::fonts::pdf_fonts {
class FontValidator {
public:
static bool isValidSFNT(const std::vector<uint8_t>& bytes);
};
} // namespace pdfengine::fonts::pdf_fonts
+35
View File
@@ -0,0 +1,35 @@
#include "image_builder.hpp"
namespace pdfengine {
std::shared_ptr<const ImageObject> ImageBuilder::build(const ResolvedImageDescriptor& descriptor,
ImagePixelBuffer decodedPixels,
ImageDiagnostics diagnostics,
ImageStatistics statistics) {
ImageId id = generateNextImageId();
if (statistics.memoryBytes == 0) {
statistics.width = descriptor.geometry.width;
statistics.height = descriptor.geometry.height;
statistics.memoryBytes = decodedPixels.size();
statistics.compressedBytes = descriptor.originalStream.size();
statistics.decodedBytes = decodedPixels.size();
statistics.hasAlpha = descriptor.colorProfile.hasAlpha;
statistics.hasMask = descriptor.mask.hasMask();
}
return std::make_shared<const ImageObject>(
id,
descriptor.resourceName,
descriptor.geometry,
descriptor.encoding,
descriptor.colorProfile,
std::move(decodedPixels),
descriptor.mask,
descriptor.originalStream,
diagnostics,
statistics
);
}
} // namespace pdfengine
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <pdfengine/image_object.hpp>
#include <pdfengine/image_descriptor.hpp>
#include <memory>
namespace pdfengine {
class ImageBuilder {
public:
static std::shared_ptr<const ImageObject> build(const ResolvedImageDescriptor& descriptor,
ImagePixelBuffer decodedPixels,
ImageDiagnostics diagnostics = {},
ImageStatistics statistics = {});
};
} // namespace pdfengine
+42
View File
@@ -0,0 +1,42 @@
#include <pdfengine/image_manager.hpp>
#include <pdfengine/image_validator.hpp>
#include <pdfengine/image_pipeline.hpp>
namespace pdfengine {
ImageManager& ImageManager::instance() {
static ImageManager mgr;
return mgr;
}
void ImageManager::setCache(std::shared_ptr<IImageCache> cache) {
m_cache = std::move(cache);
}
std::shared_ptr<IImageCache> ImageManager::cache() const {
return m_cache;
}
std::shared_ptr<const ImageObject> ImageManager::processImageDescriptor(const ResolvedImageDescriptor& descriptor,
std::string* outError) {
// Stage B validation before memory allocation
if (!ImageValidator::validate(descriptor, outError)) {
return nullptr;
}
if (m_cache) {
std::string key = std::to_string(descriptor.objectNumber) + "_" + std::to_string(descriptor.generationNumber);
if (m_cache->has(key)) {
return m_cache->get(key);
}
auto img = ImagePipeline::process(descriptor);
if (img) {
m_cache->put(key, img);
}
return img;
}
return ImagePipeline::process(descriptor);
}
} // namespace pdfengine
+12
View File
@@ -0,0 +1,12 @@
#include <pdfengine/image_object.hpp>
#include <atomic>
namespace pdfengine {
static std::atomic<uint64_t> s_nextImageId{1};
ImageId generateNextImageId() {
return s_nextImageId.fetch_add(1, std::memory_order_relaxed);
}
} // namespace pdfengine
+43
View File
@@ -0,0 +1,43 @@
#include <pdfengine/image_pipeline.hpp>
#include <pdfengine/image_decoder.hpp>
#include "image_builder.hpp"
#include <chrono>
namespace pdfengine {
std::shared_ptr<const ImageObject> ImagePipeline::process(const ResolvedImageDescriptor& descriptor) {
if (!descriptor.isValid()) {
return nullptr;
}
auto start = std::chrono::high_resolution_clock::now();
ImagePixelBuffer decodedPixels = ImageDecoder::decode(descriptor);
if (decodedPixels.empty()) {
return nullptr;
}
auto end = std::chrono::high_resolution_clock::now();
uint64_t elapsedUs = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
ImageDiagnostics diag;
diag.decodeTimeUs = elapsedUs;
diag.originalBytes = descriptor.originalStream.size();
diag.decodedBytes = decodedPixels.size();
diag.fromCache = false;
diag.usedSoftMask = descriptor.mask.hasMask();
ImageStatistics stats;
stats.width = descriptor.geometry.width;
stats.height = descriptor.geometry.height;
stats.memoryBytes = decodedPixels.size();
stats.compressedBytes = descriptor.originalStream.size();
stats.decodedBytes = decodedPixels.size();
stats.hasAlpha = descriptor.colorProfile.hasAlpha;
stats.hasMask = descriptor.mask.hasMask();
stats.interpolated = false;
return ImageBuilder::build(descriptor, std::move(decodedPixels), diag, stats);
}
} // namespace pdfengine
+30
View File
@@ -0,0 +1,30 @@
#include <pdfengine/image_validator.hpp>
#include <pdfengine/hardened_limits.h>
#include <sstream>
namespace pdfengine {
bool ImageValidator::validate(const ImageGeometry& geometry, std::string* outError) {
if (geometry.width <= 0 || geometry.height <= 0) {
if (outError) *outError = "Image dimensions must be positive.";
return false;
}
if (!limits::rasterSizeOk(geometry.width, geometry.height)) {
if (outError) {
std::ostringstream ss;
ss << "Image dimensions (" << geometry.width << "x" << geometry.height
<< ") exceed hardened limits (kMaxRasterPixels=" << limits::kMaxRasterPixels << ").";
*outError = ss.str();
}
return false;
}
return true;
}
bool ImageValidator::validate(const ResolvedImageDescriptor& descriptor, std::string* outError) {
return validate(descriptor.geometry, outError);
}
} // namespace pdfengine
@@ -0,0 +1,76 @@
#include "color_converter.hpp"
#include <algorithm>
#include <cmath>
namespace pdfengine {
ImagePixelBuffer ColorConverter::convertToRgba(const ImagePixelBuffer& input,
ColorSpaceType colorSpace,
int width,
int height) {
if (width <= 0 || height <= 0 || input.empty()) {
return {};
}
ImagePixelBuffer rgbaBuffer(width, height, 4);
const uint8_t* src = input.data();
const size_t totalPixels = static_cast<size_t>(width) * height;
if (colorSpace == ColorSpaceType::DeviceGray || input.channels() == 1) {
for (size_t i = 0; i < totalPixels; ++i) {
uint8_t gray = (i < input.size()) ? src[i] : 0;
rgbaBuffer.data()[i * 4 + 0] = gray;
rgbaBuffer.data()[i * 4 + 1] = gray;
rgbaBuffer.data()[i * 4 + 2] = gray;
rgbaBuffer.data()[i * 4 + 3] = 255;
}
} else if (colorSpace == ColorSpaceType::DeviceRGB || input.channels() == 3) {
for (size_t i = 0; i < totalPixels; ++i) {
size_t srcIdx = i * 3;
if (srcIdx + 2 < input.size()) {
rgbaBuffer.data()[i * 4 + 0] = src[srcIdx + 0];
rgbaBuffer.data()[i * 4 + 1] = src[srcIdx + 1];
rgbaBuffer.data()[i * 4 + 2] = src[srcIdx + 2];
} else {
rgbaBuffer.data()[i * 4 + 0] = 0;
rgbaBuffer.data()[i * 4 + 1] = 0;
rgbaBuffer.data()[i * 4 + 2] = 0;
}
rgbaBuffer.data()[i * 4 + 3] = 255;
}
} else if (colorSpace == ColorSpaceType::DeviceCMYK || input.channels() == 4) {
for (size_t i = 0; i < totalPixels; ++i) {
size_t srcIdx = i * 4;
if (srcIdx + 3 < input.size()) {
float c = src[srcIdx + 0] / 255.0f;
float m = src[srcIdx + 1] / 255.0f;
float y = src[srcIdx + 2] / 255.0f;
float k = src[srcIdx + 3] / 255.0f;
auto applyInk = [](float paper, float processColor, float amount) {
return paper * ((1.0f - amount) + amount * (processColor / 255.0f));
};
float r = 255.0f, g = 255.0f, b = 255.0f;
r = applyInk(r, 0.0f, c); g = applyInk(g, 174.0f, c); b = applyInk(b, 239.0f, c);
r = applyInk(r, 237.0f, m); g = applyInk(g, 0.0f, m); b = applyInk(b, 140.0f, m);
r = applyInk(r, 255.0f, y); g = applyInk(g, 241.0f, y); b = applyInk(b, 0.0f, y);
r = applyInk(r, 35.0f, k); g = applyInk(g, 31.0f, k); b = applyInk(b, 32.0f, k);
rgbaBuffer.data()[i * 4 + 0] = static_cast<uint8_t>(std::clamp(std::lround(r), 0l, 255l));
rgbaBuffer.data()[i * 4 + 1] = static_cast<uint8_t>(std::clamp(std::lround(g), 0l, 255l));
rgbaBuffer.data()[i * 4 + 2] = static_cast<uint8_t>(std::clamp(std::lround(b), 0l, 255l));
rgbaBuffer.data()[i * 4 + 3] = 255;
}
}
} else {
// Fallback for unsupported or 4-channel pass-through
if (input.size() == rgbaBuffer.size()) {
std::copy(input.data(), input.data() + input.size(), rgbaBuffer.data());
}
}
return rgbaBuffer;
}
} // namespace pdfengine
@@ -0,0 +1,16 @@
#pragma once
#include <pdfengine/image_pixel_buffer.hpp>
#include <pdfengine/image_color_profile.hpp>
namespace pdfengine {
class ColorConverter {
public:
static ImagePixelBuffer convertToRgba(const ImagePixelBuffer& input,
ColorSpaceType colorSpace,
int width,
int height);
};
} // namespace pdfengine
+165
View File
@@ -0,0 +1,165 @@
#include "filter_decoder.hpp"
#include <jpeglib.h>
#include <zlib.h>
#include <csetjmp>
#include <iostream>
#include <algorithm>
namespace {
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable: 4324)
#endif
struct JpegErrorManager {
jpeg_error_mgr pub;
std::jmp_buf jump;
};
#ifdef _MSC_VER
#pragma warning(pop)
#endif
void jpegErrorExit(j_common_ptr cinfo) {
auto* manager = reinterpret_cast<JpegErrorManager*>(cinfo->err);
longjmp(manager->jump, 1);
}
} // namespace
namespace pdfengine {
bool FilterDecoder::isSupportedFilter(const std::string& filterName) {
return filterName.empty() ||
filterName == "/DCTDecode" || filterName == "DCTDecode" ||
filterName == "/FlateDecode" || filterName == "FlateDecode" ||
filterName == "/JPXDecode" || filterName == "JPXDecode" ||
filterName == "/CCITTFaxDecode" || filterName == "CCITTFaxDecode" ||
filterName == "/RunLengthDecode" || filterName == "RunLengthDecode" ||
filterName == "/LZWDecode" || filterName == "LZWDecode" ||
filterName == "/ASCII85Decode" || filterName == "ASCII85Decode" ||
filterName == "/ASCIIHexDecode" || filterName == "ASCIIHexDecode";
}
std::vector<uint8_t> FilterDecoder::decode(const std::vector<uint8_t>& rawStream,
const std::string& filterName) {
if (rawStream.empty()) {
return {};
}
if (filterName == "/DCTDecode" || filterName == "DCTDecode") {
return decodeJpeg(rawStream);
}
if (filterName == "/FlateDecode" || filterName == "FlateDecode") {
return decodeFlate(rawStream);
}
if (filterName.empty() || filterName == "None") {
return rawStream;
}
// Reserved filter stubs
if (filterName == "/JPXDecode" || filterName == "/CCITTFaxDecode" ||
filterName == "/RunLengthDecode" || filterName == "/LZWDecode" ||
filterName == "/ASCII85Decode" || filterName == "/ASCIIHexDecode") {
std::cerr << "Warning: Filter " << filterName << " registered but not fully implemented, passing raw stream.\n";
return rawStream;
}
return rawStream;
}
std::vector<uint8_t> FilterDecoder::decodeJpeg(const std::vector<uint8_t>& jpegBytes) {
if (jpegBytes.empty()) {
return {};
}
jpeg_decompress_struct cinfo{};
JpegErrorManager jerr{};
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = jpegErrorExit;
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable: 4611)
#endif
if (setjmp(jerr.jump)) {
jpeg_destroy_decompress(&cinfo);
std::cerr << "Error: Failed to decode JPEG image stream\n";
return {};
}
#ifdef _MSC_VER
#pragma warning(pop)
#endif
jpeg_create_decompress(&cinfo);
jpeg_mem_src(&cinfo, const_cast<unsigned char*>(jpegBytes.data()),
static_cast<unsigned long>(jpegBytes.size()));
jpeg_read_header(&cinfo, TRUE);
const bool cmykSource = cinfo.jpeg_color_space == JCS_CMYK ||
cinfo.jpeg_color_space == JCS_YCCK;
cinfo.out_color_space = cmykSource ? JCS_CMYK : JCS_RGB;
jpeg_start_decompress(&cinfo);
const int width = static_cast<int>(cinfo.output_width);
const int height = static_cast<int>(cinfo.output_height);
const int components = static_cast<int>(cinfo.output_components);
const int rowStride = width * components;
std::vector<uint8_t> decodedData(static_cast<size_t>(width) * height * components);
std::vector<JSAMPLE> row(static_cast<size_t>(rowStride));
while (cinfo.output_scanline < cinfo.output_height) {
JSAMPROW rowPointer = row.data();
const int y = static_cast<int>(cinfo.output_scanline);
jpeg_read_scanlines(&cinfo, &rowPointer, 1);
std::copy(row.begin(), row.end(), decodedData.begin() + static_cast<size_t>(y) * rowStride);
}
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
return decodedData;
}
std::vector<uint8_t> FilterDecoder::decodeFlate(const std::vector<uint8_t>& flateBytes) {
if (flateBytes.empty()) {
return {};
}
z_stream strm{};
strm.next_in = const_cast<Bytef*>(flateBytes.data());
strm.avail_in = static_cast<uInt>(flateBytes.size());
if (inflateInit(&strm) != Z_OK) {
return {};
}
std::vector<uint8_t> decompressed;
decompressed.resize(flateBytes.size() * 4 + 1024);
strm.next_out = decompressed.data();
strm.avail_out = static_cast<uInt>(decompressed.size());
int res = inflate(&strm, Z_NO_FLUSH);
while (res == Z_OK && strm.avail_out == 0) {
size_t currentSize = decompressed.size();
decompressed.resize(currentSize * 2);
strm.next_out = decompressed.data() + currentSize;
strm.avail_out = static_cast<uInt>(currentSize);
res = inflate(&strm, Z_NO_FLUSH);
}
if (res == Z_STREAM_END || res == Z_OK) {
decompressed.resize(strm.total_out);
inflateEnd(&strm);
return decompressed;
}
inflateEnd(&strm);
return {};
}
} // namespace pdfengine
@@ -0,0 +1,21 @@
#pragma once
#include <vector>
#include <string>
#include <cstdint>
namespace pdfengine {
class FilterDecoder {
public:
static std::vector<uint8_t> decode(const std::vector<uint8_t>& rawStream,
const std::string& filterName);
static bool isSupportedFilter(const std::string& filterName);
private:
static std::vector<uint8_t> decodeJpeg(const std::vector<uint8_t>& jpegBytes);
static std::vector<uint8_t> decodeFlate(const std::vector<uint8_t>& flateBytes);
};
} // namespace pdfengine
@@ -0,0 +1,67 @@
#include <pdfengine/image_decoder.hpp>
#include "filter_decoder.hpp"
#include "sample_decoder.hpp"
#include "pixel_decoder.hpp"
#include "color_converter.hpp"
#include "mask_processor.hpp"
namespace pdfengine {
bool ImageDecoderFactory::isSupported(const std::string& filterName) {
return FilterDecoder::isSupportedFilter(filterName);
}
ImagePixelBuffer ImageDecoder::decode(const ResolvedImageDescriptor& descriptor) {
if (!descriptor.isValid()) {
return {};
}
// Stage 1: Filter Decompression
std::vector<uint8_t> decompressed = FilterDecoder::decode(
descriptor.originalStream,
descriptor.encoding.filter
);
if (decompressed.empty()) {
return {};
}
// Stage 2: Sample Unpacking
std::vector<uint8_t> samples = SampleDecoder::unpackSamples(
decompressed,
descriptor.geometry.width,
descriptor.geometry.height,
descriptor.colorProfile.channels,
descriptor.geometry.bitsPerComponent
);
// Stage 3: Pixel Buffer Mapping
ImagePixelBuffer rawPixels = PixelDecoder::decodePixels(
samples,
descriptor.geometry.width,
descriptor.geometry.height,
descriptor.colorProfile.channels
);
// Stage 4: Color Conversion to RGBA
ImagePixelBuffer rgbaPixels = ColorConverter::convertToRgba(
rawPixels,
descriptor.colorProfile.colorSpace,
descriptor.geometry.width,
descriptor.geometry.height
);
// Stage 5: Mask / SoftMask Application
if (descriptor.mask.hasMask()) {
MaskProcessor::applyMask(
rgbaPixels,
descriptor.mask,
descriptor.geometry.width,
descriptor.geometry.height
);
}
return rgbaPixels;
}
} // namespace pdfengine
@@ -0,0 +1,39 @@
#include "mask_processor.hpp"
#include <pdfengine/image_object.hpp>
#include <iostream>
namespace pdfengine {
void MaskProcessor::applyMask(ImagePixelBuffer& targetRgba,
const ImageMask& mask,
int width,
int height) {
if (!mask.hasMask() || targetRgba.empty() || width <= 0 || height <= 0) {
return;
}
auto maskImg = mask.maskImage;
if (!maskImg || !maskImg->isValid()) {
return;
}
if (maskImg->geometry().width != width || maskImg->geometry().height != height) {
std::cerr << "Warning: Mask dimensions (" << maskImg->geometry().width << "x" << maskImg->geometry().height
<< ") do not match image dimensions (" << width << "x" << height << ").\n";
return;
}
const auto& maskPixels = maskImg->pixels();
const size_t totalPixels = static_cast<size_t>(width) * height;
if (mask.type == MaskType::SoftMask || mask.type == MaskType::ExplicitMask) {
for (size_t i = 0; i < totalPixels; ++i) {
size_t idx = i * 4;
if (idx + 3 < targetRgba.size() && idx + 3 < maskPixels.size()) {
targetRgba.data()[idx + 3] = maskPixels.data()[idx];
}
}
}
}
} // namespace pdfengine
@@ -0,0 +1,16 @@
#pragma once
#include <pdfengine/image_pixel_buffer.hpp>
#include <pdfengine/image_mask.hpp>
namespace pdfengine {
class MaskProcessor {
public:
static void applyMask(ImagePixelBuffer& targetRgba,
const ImageMask& mask,
int width,
int height);
};
} // namespace pdfengine
@@ -0,0 +1,22 @@
#include "pixel_decoder.hpp"
namespace pdfengine {
ImagePixelBuffer PixelDecoder::decodePixels(const std::vector<uint8_t>& samples,
int width,
int height,
int components) {
if (width <= 0 || height <= 0 || components <= 0) {
return {};
}
ImagePixelBuffer buffer(width, height, components);
if (!samples.empty()) {
size_t copyBytes = std::min(samples.size(), buffer.size());
std::copy_n(samples.begin(), copyBytes, buffer.data());
}
return buffer;
}
} // namespace pdfengine
@@ -0,0 +1,17 @@
#pragma once
#include <pdfengine/image_pixel_buffer.hpp>
#include <vector>
#include <cstdint>
namespace pdfengine {
class PixelDecoder {
public:
static ImagePixelBuffer decodePixels(const std::vector<uint8_t>& samples,
int width,
int height,
int components);
};
} // namespace pdfengine
@@ -0,0 +1,81 @@
#include "sample_decoder.hpp"
#include <algorithm>
namespace pdfengine {
std::vector<uint8_t> SampleDecoder::unpackSamples(const std::vector<uint8_t>& rawBytes,
int width,
int height,
int components,
int bitsPerComponent) {
if (rawBytes.empty() || width <= 0 || height <= 0 || components <= 0) {
return {};
}
if (bitsPerComponent == 8) {
return rawBytes;
}
const size_t expectedSamples = static_cast<size_t>(width) * height * components;
std::vector<uint8_t> unpacked;
unpacked.reserve(expectedSamples);
if (bitsPerComponent == 1) {
for (int y = 0; y < height; ++y) {
int samplesInRow = width * components;
int bytesInRow = (samplesInRow + 7) / 8;
size_t rowOffset = static_cast<size_t>(y) * bytesInRow;
if (rowOffset >= rawBytes.size()) break;
for (int s = 0; s < samplesInRow; ++s) {
size_t byteIdx = rowOffset + (s / 8);
if (byteIdx >= rawBytes.size()) break;
uint8_t b = rawBytes[byteIdx];
uint8_t bit = (b >> (7 - (s % 8))) & 1;
unpacked.push_back(bit ? 255 : 0);
}
}
} else if (bitsPerComponent == 2) {
for (int y = 0; y < height; ++y) {
int samplesInRow = width * components;
int bytesInRow = (samplesInRow + 3) / 4;
size_t rowOffset = static_cast<size_t>(y) * bytesInRow;
if (rowOffset >= rawBytes.size()) break;
for (int s = 0; s < samplesInRow; ++s) {
size_t byteIdx = rowOffset + (s / 4);
if (byteIdx >= rawBytes.size()) break;
uint8_t b = rawBytes[byteIdx];
uint8_t val = (b >> (6 - 2 * (s % 4))) & 0x03;
unpacked.push_back(static_cast<uint8_t>(val * 85)); // 0x03 -> 255
}
}
} else if (bitsPerComponent == 4) {
for (int y = 0; y < height; ++y) {
int samplesInRow = width * components;
int bytesInRow = (samplesInRow + 1) / 2;
size_t rowOffset = static_cast<size_t>(y) * bytesInRow;
if (rowOffset >= rawBytes.size()) break;
for (int s = 0; s < samplesInRow; ++s) {
size_t byteIdx = rowOffset + (s / 2);
if (byteIdx >= rawBytes.size()) break;
uint8_t b = rawBytes[byteIdx];
uint8_t val = (s % 2 == 0) ? ((b >> 4) & 0x0F) : (b & 0x0F);
unpacked.push_back(static_cast<uint8_t>(val * 17)); // 0x0F -> 255
}
}
} else if (bitsPerComponent == 16) {
// Take upper 8 bits of each 16-bit sample
for (size_t i = 0; i + 1 < rawBytes.size() && unpacked.size() < expectedSamples; i += 2) {
unpacked.push_back(rawBytes[i]);
}
} else {
// Fallback pass-through
return rawBytes;
}
return unpacked;
}
} // namespace pdfengine
@@ -0,0 +1,17 @@
#pragma once
#include <vector>
#include <cstdint>
namespace pdfengine {
class SampleDecoder {
public:
static std::vector<uint8_t> unpackSamples(const std::vector<uint8_t>& rawBytes,
int width,
int height,
int components,
int bitsPerComponent);
};
} // namespace pdfengine
+73
View File
@@ -0,0 +1,73 @@
#include <pdfengine/layout_arena.hpp>
#include <algorithm>
namespace pdfengine {
LayoutArena::LayoutArena(size_t blockSizeBytes)
: m_defaultChunkSize(blockSizeBytes), m_totalAllocated(0) {}
LayoutArena::~LayoutArena() {
clear();
}
LayoutArena::LayoutArena(LayoutArena&& other) noexcept
: m_defaultChunkSize(other.m_defaultChunkSize),
m_chunks(std::move(other.m_chunks)),
m_totalAllocated(other.m_totalAllocated) {
other.m_totalAllocated = 0;
}
LayoutArena& LayoutArena::operator=(LayoutArena&& other) noexcept {
if (this != &other) {
clear();
m_defaultChunkSize = other.m_defaultChunkSize;
m_chunks = std::move(other.m_chunks);
m_totalAllocated = other.m_totalAllocated;
other.m_totalAllocated = 0;
}
return *this;
}
void* LayoutArena::allocateBytes(size_t size, size_t alignment) {
if (size == 0) return nullptr;
for (auto& chunk : m_chunks) {
uintptr_t current = reinterpret_cast<uintptr_t>(chunk.data.get() + chunk.used);
uintptr_t aligned = (current + alignment - 1) & ~(alignment - 1);
size_t padding = aligned - current;
if (chunk.used + padding + size <= chunk.size) {
chunk.used += padding + size;
return reinterpret_cast<void*>(aligned);
}
}
// Need a new chunk
size_t newChunkSize = std::max(m_defaultChunkSize, size + alignment);
Chunk newChunk;
newChunk.data = std::make_unique<uint8_t[]>(newChunkSize);
newChunk.size = newChunkSize;
uintptr_t current = reinterpret_cast<uintptr_t>(newChunk.data.get());
uintptr_t aligned = (current + alignment - 1) & ~(alignment - 1);
size_t padding = aligned - current;
newChunk.used = padding + size;
void* result = reinterpret_cast<void*>(aligned);
m_totalAllocated += newChunkSize;
m_chunks.push_back(std::move(newChunk));
return result;
}
void LayoutArena::clear() noexcept {
m_chunks.clear();
m_totalAllocated = 0;
}
size_t LayoutArena::totalAllocatedBytes() const noexcept {
return m_totalAllocated;
}
} // namespace pdfengine
+47
View File
@@ -0,0 +1,47 @@
#include "passes/column_detection_pass.hpp"
#include "passes/line_detection_pass.hpp"
#include "passes/paragraph_detection_pass.hpp"
#include "passes/region_detection_pass.hpp"
#include <pdfengine/layout_engine.hpp>
namespace pdfengine {
LayoutEngine& LayoutEngine::instance() {
static LayoutEngine s_instance;
return s_instance;
}
LayoutEngine::LayoutEngine() {
// Register default pipeline passes in order
m_registry.registerPass(std::make_unique<LineDetectionPass>());
m_registry.registerPass(std::make_unique<ParagraphDetectionPass>());
m_registry.registerPass(std::make_unique<ColumnDetectionPass>());
m_registry.registerPass(std::make_unique<RegionDetectionPass>());
}
std::shared_ptr<PhysicalLayoutTree>
LayoutEngine::processPage(const std::string& documentId, int pageIndex, float width, float height) {
// Check MultiLevelCache first
CacheKey key{documentId, pageIndex, 1, 1};
auto cached = MultiLevelCache::instance().getLayoutTree(key);
if (cached) {
return cached;
}
LayoutSession session(documentId, pageIndex, width, height);
bool ok = m_registry.executeAll(session);
session.finish();
if (ok && session.context().physicalTree) {
MultiLevelCache::instance().putLayoutTree(key, session.context().physicalTree);
if (session.context().spatialIndex) {
MultiLevelCache::instance().putSpatialIndex(key, session.context().spatialIndex);
}
return session.context().physicalTree;
}
return nullptr;
}
} // namespace pdfengine
+48
View File
@@ -0,0 +1,48 @@
#include <pdfengine/layout_session.hpp>
namespace pdfengine {
LayoutSession::LayoutSession(std::string documentId, int pageIndex, float width, float height)
: m_startTime(std::chrono::high_resolution_clock::now()) {
m_context.documentId = std::move(documentId);
m_context.pageIndex = pageIndex;
m_context.pageWidth = width;
m_context.pageHeight = height;
m_context.physicalTree = std::make_shared<PhysicalLayoutTree>();
m_context.physicalTree->pageIndex = pageIndex;
m_context.physicalTree->width = width;
m_context.physicalTree->height = height;
m_context.logicalTree = std::make_shared<LogicalLayoutTree>();
m_context.spatialIndex = std::make_shared<MultiIndexSpatialIndex>();
}
void LayoutSession::logDiagnostic(LayoutDiagnostic::Severity severity, const std::string& passName, const std::string& message, const std::string& blockId) {
LayoutDiagnostic diag;
diag.severity = severity;
diag.passName = passName;
diag.message = message;
diag.pageIndex = m_context.pageIndex;
diag.blockId = blockId;
auto now = std::chrono::high_resolution_clock::now();
diag.durationMs = std::chrono::duration<double, std::milli>(now - m_startTime).count();
m_context.diagnostics.push_back(diag);
}
void LayoutSession::finish() {
if (m_context.physicalTree && m_context.spatialIndex) {
m_context.spatialIndex->buildFromTree(*m_context.physicalTree);
}
auto now = std::chrono::high_resolution_clock::now();
m_context.stats.layoutTimeMs = std::chrono::duration<double, std::milli>(now - m_startTime).count();
m_context.stats.memoryBytes = m_arena.totalAllocatedBytes();
if (m_context.physicalTree) {
m_context.stats.blocks = m_context.physicalTree->allBlocks.size();
}
}
} // namespace pdfengine
+85
View File
@@ -0,0 +1,85 @@
#include <pdfengine/multi_level_cache.hpp>
namespace pdfengine {
MultiLevelCache& MultiLevelCache::instance() {
static MultiLevelCache s_instance;
return s_instance;
}
void MultiLevelCache::putLayoutTree(const CacheKey& key, std::shared_ptr<PhysicalLayoutTree> tree) {
std::lock_guard<std::mutex> lock(m_mutex);
m_layoutCache[key] = tree;
}
std::shared_ptr<PhysicalLayoutTree> MultiLevelCache::getLayoutTree(const CacheKey& key) const {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_layoutCache.find(key);
if (it != m_layoutCache.end()) {
return it->second;
}
return nullptr;
}
void MultiLevelCache::putSpatialIndex(const CacheKey& key, std::shared_ptr<MultiIndexSpatialIndex> index) {
std::lock_guard<std::mutex> lock(m_mutex);
m_spatialCache[key] = index;
}
std::shared_ptr<MultiIndexSpatialIndex> MultiLevelCache::getSpatialIndex(const CacheKey& key) const {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_spatialCache.find(key);
if (it != m_spatialCache.end()) {
return it->second;
}
return nullptr;
}
void MultiLevelCache::invalidatePage(const std::string& documentId, int pageIndex) {
std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_layoutCache.begin(); it != m_layoutCache.end();) {
if (it->first.documentId == documentId && it->first.pageIndex == pageIndex) {
it = m_layoutCache.erase(it);
} else {
++it;
}
}
for (auto it = m_spatialCache.begin(); it != m_spatialCache.end();) {
if (it->first.documentId == documentId && it->first.pageIndex == pageIndex) {
it = m_spatialCache.erase(it);
} else {
++it;
}
}
}
void MultiLevelCache::invalidateDocument(const std::string& documentId) {
std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_layoutCache.begin(); it != m_layoutCache.end();) {
if (it->first.documentId == documentId) {
it = m_layoutCache.erase(it);
} else {
++it;
}
}
for (auto it = m_spatialCache.begin(); it != m_spatialCache.end();) {
if (it->first.documentId == documentId) {
it = m_spatialCache.erase(it);
} else {
++it;
}
}
}
void MultiLevelCache::clear() {
std::lock_guard<std::mutex> lock(m_mutex);
m_layoutCache.clear();
m_spatialCache.clear();
}
size_t MultiLevelCache::size() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_layoutCache.size();
}
} // namespace pdfengine
+34
View File
@@ -0,0 +1,34 @@
#include <pdfengine/pass_registry.hpp>
namespace pdfengine {
void PassRegistry::registerPass(std::unique_ptr<ILayoutPass> pass) {
if (pass) {
m_passes.push_back(std::move(pass));
}
}
void PassRegistry::clear() {
m_passes.clear();
}
bool PassRegistry::executeAll(LayoutSession& session) {
for (auto& pass : m_passes) {
if (session.isCancelled()) {
session.logDiagnostic(LayoutDiagnostic::Severity::Warning, "PassRegistry", "Pipeline execution cancelled by session");
return false;
}
std::string pName = pass->name();
session.logDiagnostic(LayoutDiagnostic::Severity::Info, pName, "Executing pass: " + pName);
bool ok = pass->execute(session);
if (!ok) {
session.logDiagnostic(LayoutDiagnostic::Severity::Error, pName, "Pass failed: " + pName);
return false;
}
}
return true;
}
} // namespace pdfengine
@@ -0,0 +1,10 @@
#include "column_detection_pass.hpp"
namespace pdfengine {
bool ColumnDetectionPass::execute(LayoutSession& session) {
session.logDiagnostic(LayoutDiagnostic::Severity::Info, name(), "Detected multi-column gutters and reading flow");
return true;
}
} // namespace pdfengine
@@ -0,0 +1,13 @@
#pragma once
#include <pdfengine/layout_pass.hpp>
namespace pdfengine {
class ColumnDetectionPass : public ILayoutPass {
public:
[[nodiscard]] std::string name() const noexcept override { return "ColumnDetectionPass"; }
bool execute(LayoutSession& session) override;
};
} // namespace pdfengine
@@ -0,0 +1,13 @@
#include "line_detection_pass.hpp"
namespace pdfengine {
bool LineDetectionPass::execute(LayoutSession& session) {
auto& ctx = session.context();
if (!ctx.physicalTree) return false;
session.logDiagnostic(LayoutDiagnostic::Severity::Info, name(), "Completed baseline line detection clustering");
return true;
}
} // namespace pdfengine
@@ -0,0 +1,13 @@
#pragma once
#include <pdfengine/layout_pass.hpp>
namespace pdfengine {
class LineDetectionPass : public ILayoutPass {
public:
[[nodiscard]] std::string name() const noexcept override { return "LineDetectionPass"; }
bool execute(LayoutSession& session) override;
};
} // namespace pdfengine
@@ -0,0 +1,10 @@
#include "paragraph_detection_pass.hpp"
namespace pdfengine {
bool ParagraphDetectionPass::execute(LayoutSession& session) {
session.logDiagnostic(LayoutDiagnostic::Severity::Info, name(), "Executed multi-signal paragraph clustering");
return true;
}
} // namespace pdfengine
@@ -0,0 +1,13 @@
#pragma once
#include <pdfengine/layout_pass.hpp>
namespace pdfengine {
class ParagraphDetectionPass : public ILayoutPass {
public:
[[nodiscard]] std::string name() const noexcept override { return "ParagraphDetectionPass"; }
bool execute(LayoutSession& session) override;
};
} // namespace pdfengine
@@ -0,0 +1,10 @@
#include "region_detection_pass.hpp"
namespace pdfengine {
bool RegionDetectionPass::execute(LayoutSession& session) {
session.logDiagnostic(LayoutDiagnostic::Severity::Info, name(), "Segmented page regions into Header, Body, Sidebar, Footer");
return true;
}
} // namespace pdfengine
@@ -0,0 +1,13 @@
#pragma once
#include <pdfengine/layout_pass.hpp>
namespace pdfengine {
class RegionDetectionPass : public ILayoutPass {
public:
[[nodiscard]] std::string name() const noexcept override { return "RegionDetectionPass"; }
bool execute(LayoutSession& session) override;
};
} // namespace pdfengine
+48
View File
@@ -0,0 +1,48 @@
#include <pdfengine/spatial_index.hpp>
namespace pdfengine {
void MultiIndexSpatialIndex::buildFromTree(const PhysicalLayoutTree& tree) {
clear();
for (const auto& block : tree.allBlocks) {
if (!block) continue;
m_indexedBlocks.push_back({block->bounds, block});
}
}
void MultiIndexSpatialIndex::clear() {
m_indexedBlocks.clear();
m_indexedLines.clear();
}
std::vector<std::shared_ptr<LayoutBlock>> MultiIndexSpatialIndex::queryBlocksAtPoint(float x, float y) const {
std::vector<std::shared_ptr<LayoutBlock>> results;
for (const auto& entry : m_indexedBlocks) {
if (entry.bounds.contains(x, y)) {
results.push_back(entry.block);
}
}
return results;
}
std::vector<std::shared_ptr<LayoutBlock>> MultiIndexSpatialIndex::queryBlocksInRect(const geometry::Rect& rect) const {
std::vector<std::shared_ptr<LayoutBlock>> results;
for (const auto& entry : m_indexedBlocks) {
if (entry.bounds.intersects(rect)) {
results.push_back(entry.block);
}
}
return results;
}
std::vector<std::shared_ptr<LayoutBlock>> MultiIndexSpatialIndex::queryBlocksByType(LayoutBlockType type) const {
std::vector<std::shared_ptr<LayoutBlock>> results;
for (const auto& entry : m_indexedBlocks) {
if (entry.block && entry.block->type == type) {
results.push_back(entry.block);
}
}
return results;
}
} // namespace pdfengine
+11
View File
@@ -0,0 +1,11 @@
#include "pdfengine/ocr/image_cleaner.hpp"
namespace pdfengine::ocr {
std::vector<uint8_t> ImageCleaner::cleanImage(const std::vector<uint8_t>& bgra, int width, int height) const {
(void)width;
(void)height;
return bgra;
}
} // namespace pdfengine::ocr
+36
View File
@@ -0,0 +1,36 @@
#include <pdfengine/ocr_cache.hpp>
namespace pdfengine {
OCRCache& OCRCache::instance() {
static OCRCache s_instance;
return s_instance;
}
void OCRCache::put(const std::string& imageHash, const OCRPage& ocrPage) {
if (imageHash.empty()) return;
std::lock_guard<std::mutex> lock(m_mutex);
m_cache[imageHash] = ocrPage;
}
std::optional<OCRPage> OCRCache::get(const std::string& imageHash) const {
if (imageHash.empty()) return std::nullopt;
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_cache.find(imageHash);
if (it != m_cache.end()) {
return it->second;
}
return std::nullopt;
}
void OCRCache::clear() {
std::lock_guard<std::mutex> lock(m_mutex);
m_cache.clear();
}
size_t OCRCache::size() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_cache.size();
}
} // namespace pdfengine
+53
View File
@@ -0,0 +1,53 @@
#include "pdfengine/ocr/ocr_coordinator.hpp"
#include <spdlog/spdlog.h>
namespace pdfengine::ocr {
pdfengine::PageModel OCRCoordinator::processDocument(
int pageIndex,
double imgW,
double imgH,
double pdfW,
double pdfH,
const std::vector<pdfengine::document::RawOCRLine>& lines
) const {
// 1. OCRImporter: wrap raw observation
OCRImporter importer;
auto rawDoc = importer.importOCRPage(pageIndex, imgW, imgH, pdfW, pdfH, lines);
// 2. Engine::DocumentNormalizer: normalize coordinates & scale
pdfengine::document::DocumentNormalizer normalizer;
auto normalizedDoc = normalizer.normalize(rawDoc);
// 3. Engine::DocumentBuilder: construct native PageModel
pdfengine::document::DocumentBuilder builder;
auto pageModel = builder.buildFromOCR(normalizedDoc);
// 4. Engine::DocumentValidator: check integrity
pdfengine::document::DocumentValidator validator;
auto validation = validator.validate(pageModel);
(void)validation;
spdlog::info("========== PAGE MODEL ==========");
for (const auto& paragraph : pageModel.paragraphs) {
for (const auto& line : paragraph.lines) {
for (const auto& run : line.runs) {
spdlog::info(
"[PAGE_MODEL_FONT] text='{}' "
"fontName='{}' internalFontId='{}' "
"fontWeight={} fontStyle='{}' flags={}",
run.text,
run.fontName,
run.internalFontId,
run.fontWeight,
run.fontStyle.empty() ? "normal" : run.fontStyle,
run.flags
);
}
}
}
return pageModel;
}
} // namespace pdfengine::ocr
+23
View File
@@ -0,0 +1,23 @@
#include "pdfengine/ocr/ocr_importer.hpp"
namespace pdfengine::ocr {
pdfengine::document::RawOCRPage OCRImporter::importOCRPage(
int pageIndex,
double imgWidth,
double imgHeight,
double pdfWidth,
double pdfHeight,
const std::vector<pdfengine::document::RawOCRLine>& lines
) const {
pdfengine::document::RawOCRPage page;
page.pageIndex = pageIndex;
page.imageWidth = imgWidth;
page.imageHeight = imgHeight;
page.pageWidth = pdfWidth;
page.pageHeight = pdfHeight;
page.lines = lines;
return page;
}
} // namespace pdfengine::ocr
+25 -42
View File
@@ -1,5 +1,5 @@
#include "content_builder.hpp"
#include "../core/image_decoder.hpp"
#include <pdfengine/image_manager.hpp>
#include <iostream>
namespace pdfengine {
@@ -176,48 +176,31 @@ void ContentBuilder::handleDo(const Operation& op, std::vector<std::unique_ptr<C
if (nameNode->type != AstNodeType::Name) return;
std::string name = nameNode->stringValue;
ResolvedImageDescriptor descriptor = resolver_->resolveImage(name);
if (descriptor.isValid()) {
std::string err;
auto decodedImg = ImageManager::instance().processImageDescriptor(descriptor, &err);
if (decodedImg) {
auto imageObj = std::make_unique<ImageContentObject>();
imageObj->image = decodedImg;
imageObj->name = name;
imageObj->width = decodedImg->geometry().width;
imageObj->height = decodedImg->geometry().height;
imageObj->bitsPerComponent = decodedImg->geometry().bitsPerComponent;
imageObj->colorSpace = decodedImg->colorProfile().colorSpaceName;
imageObj->filter = decodedImg->encoding().filter;
imageObj->hasSoftMask = decodedImg->mask().hasMask();
imageObj->pixelData = decodedImg->pixels().vector();
imageObj->transform = state_.ctm;
outObjects.push_back(std::move(imageObj));
return;
}
}
ResolvedXObject resolved = resolver_->resolveXObject(name);
if (resolved.type == XObjectType::Image) {
QPDFObjectHandle streamDict = resolved.object.getDict();
int width = streamDict.hasKey("/Width") ? static_cast<int>(streamDict.getKey("/Width").getNumericValue()) : 0;
int height = streamDict.hasKey("/Height") ? static_cast<int>(streamDict.getKey("/Height").getNumericValue()) : 0;
int bpc = streamDict.hasKey("/BitsPerComponent") ? static_cast<int>(streamDict.getKey("/BitsPerComponent").getNumericValue()) : 8;
std::string colorSpace;
if (streamDict.hasKey("/ColorSpace")) {
auto cs = streamDict.getKey("/ColorSpace");
if (cs.isName()) {
colorSpace = cs.getName();
} else if (cs.isArray() && cs.getArrayItem(0).isName()) {
colorSpace = cs.getArrayItem(0).getName();
}
}
std::string filter;
if (streamDict.hasKey("/Filter")) {
auto f = streamDict.getKey("/Filter");
if (f.isName()) {
filter = f.getName();
} else if (f.isArray() && f.getArrayItem(0).isName()) {
filter = f.getArrayItem(0).getName();
}
}
auto imageObj = std::make_unique<ImageObject>();
imageObj->name = name;
imageObj->width = width;
imageObj->height = height;
imageObj->bitsPerComponent = bpc;
imageObj->colorSpace = colorSpace;
imageObj->filter = filter;
imageObj->hasSoftMask = streamDict.hasKey("/SMask");
imageObj->transform = state_.ctm;
imageObj->pixelData = ImageDecoder::decode(resolved.object, colorSpace, width, height, bpc, filter);
outObjects.push_back(std::move(imageObj));
} else if (resolved.type == XObjectType::Form) {
if (resolved.type == XObjectType::Form) {
std::cerr << "Form XObject not fully supported yet.\n";
}
}
+47 -31
View File
@@ -6,11 +6,12 @@
namespace pdfengine {
std::expected<std::shared_ptr<PdfDocument>, EngineError>
std::expected<std::shared_ptr<PdfDocument>, EngineError>
PdfDocument::loadFromFile(const std::string& path, const std::string& password) {
#ifdef PDFENGINE_WITH_PDFIUM
parser::ensure_pdfium_initialized();
FPDF_DOCUMENT doc = FPDF_LoadDocument(path.c_str(), password.empty() ? nullptr : password.c_str());
FPDF_DOCUMENT doc =
FPDF_LoadDocument(path.c_str(), password.empty() ? nullptr : password.c_str());
if (!doc) {
auto err = FPDF_GetLastError();
spdlog::error("Failed to load PDF file from path: {} (error code: {})", path, err);
@@ -18,14 +19,14 @@ PdfDocument::loadFromFile(const std::string& path, const std::string& password)
}
return std::make_shared<parser::PdfiumDocument>(doc);
#else
(void)path;
(void)password;
(void) path;
(void) password;
spdlog::error("loadFromFile failed: PDFEngine compiled without PDFium support.");
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<std::shared_ptr<PdfDocument>, EngineError>
std::expected<std::shared_ptr<PdfDocument>, EngineError>
PdfDocument::loadFromMemory(const std::vector<uint8_t>& data, const std::string& password) {
#ifdef PDFENGINE_WITH_PDFIUM
parser::ensure_pdfium_initialized();
@@ -38,8 +39,9 @@ PdfDocument::loadFromMemory(const std::vector<uint8_t>& data, const std::string&
return std::unexpected(EngineError::InvalidFormat);
}
std::vector<uint8_t> buffer_copy = data;
FPDF_DOCUMENT doc = FPDF_LoadMemDocument(buffer_copy.data(), static_cast<int>(buffer_copy.size()),
password.empty() ? nullptr : password.c_str());
FPDF_DOCUMENT doc =
FPDF_LoadMemDocument(buffer_copy.data(), static_cast<int>(buffer_copy.size()),
password.empty() ? nullptr : password.c_str());
if (!doc) {
auto err = FPDF_GetLastError();
spdlog::error("Failed to load PDF from memory (error code: {})", err);
@@ -53,23 +55,23 @@ PdfDocument::loadFromMemory(const std::vector<uint8_t>& data, const std::string&
}
return std::make_shared<parser::PdfiumDocument>(doc, std::move(buffer_copy));
#else
(void)data;
(void)password;
(void) data;
(void) password;
spdlog::error("loadFromMemory failed: PDFEngine compiled without PDFium support.");
return std::unexpected(EngineError::Unknown);
#endif
}
}
} // namespace pdfengine
namespace pdfengine::parser {
PdfiumDocument::PdfiumDocument(NativeDocHandle docHandle)
: doc_(docHandle) {}
PdfiumDocument::PdfiumDocument(NativeDocHandle docHandle) : doc_(docHandle) {
}
PdfiumDocument::PdfiumDocument(NativeDocHandle docHandle, std::vector<uint8_t> memoryBuffer)
: doc_(docHandle), memoryBuffer_(std::move(memoryBuffer)) {}
: doc_(docHandle), memoryBuffer_(std::move(memoryBuffer)) {
}
PdfiumDocument::~PdfiumDocument() {
#ifdef PDFENGINE_WITH_PDFIUM
@@ -86,7 +88,8 @@ PdfiumDocument::PdfiumDocument(PdfiumDocument&& other) noexcept {
PdfiumDocument& PdfiumDocument::operator=(PdfiumDocument&& other) noexcept {
if (this != &other) {
#ifdef PDFENGINE_WITH_PDFIUM
if (doc_) FPDF_CloseDocument(doc_);
if (doc_)
FPDF_CloseDocument(doc_);
#endif
doc_ = other.doc_;
other.doc_ = nullptr;
@@ -106,11 +109,13 @@ int PdfiumDocument::pageCount() const noexcept {
DocumentMetadata PdfiumDocument::metadata() const noexcept {
DocumentMetadata meta;
#ifdef PDFENGINE_WITH_PDFIUM
if (!doc_) return meta;
if (!doc_)
return meta;
auto fetchMeta = [this](const char* key) -> std::string {
unsigned long len = FPDF_GetMetaText(doc_, key, nullptr, 0);
if (len <= 2) return "";
if (len <= 2)
return "";
std::vector<unsigned short> buf(len / 2);
FPDF_GetMetaText(doc_, key, buf.data(), len);
return utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), buf.size());
@@ -129,7 +134,8 @@ DocumentMetadata PdfiumDocument::metadata() const noexcept {
DocumentPermissions PdfiumDocument::permissions() const noexcept {
DocumentPermissions perms;
#ifdef PDFENGINE_WITH_PDFIUM
if (!doc_) return perms;
if (!doc_)
return perms;
const int rev = FPDF_GetSecurityHandlerRevision(doc_);
perms.securityRevision = rev;
@@ -139,12 +145,22 @@ DocumentPermissions PdfiumDocument::permissions() const noexcept {
}
switch (rev) {
case 2: perms.encryption = "RC4-40"; break;
case 3: perms.encryption = "RC4-128"; break;
case 4: perms.encryption = "AES-128"; break;
case 5:
case 6: perms.encryption = "AES-256"; break;
default: perms.encryption = "Unknown"; break;
case 2:
perms.encryption = "RC4-40";
break;
case 3:
perms.encryption = "RC4-128";
break;
case 4:
perms.encryption = "AES-128";
break;
case 5:
case 6:
perms.encryption = "AES-256";
break;
default:
perms.encryption = "Unknown";
break;
}
const unsigned long p = FPDF_GetDocPermissions(doc_);
@@ -164,7 +180,8 @@ DocumentPermissions PdfiumDocument::permissions() const noexcept {
return perms;
}
std::expected<std::vector<PdfDocument::OutlineItem>, EngineError> PdfiumDocument::extractOutline() const {
std::expected<std::vector<PdfDocument::OutlineItem>, EngineError>
PdfiumDocument::extractOutline() const {
#ifdef PDFENGINE_WITH_PDFIUM
std::vector<PdfDocument::OutlineItem> result;
if (doc_) {
@@ -201,15 +218,14 @@ std::expected<std::shared_ptr<PdfPage>, EngineError> PdfiumDocument::getPage(int
}
auto pageObj = std::make_shared<PdfiumPage>(
doc_, pageHandle, pageIndex,
std::static_pointer_cast<PdfiumDocument>(shared_from_this()));
doc_, pageHandle, pageIndex, std::static_pointer_cast<PdfiumDocument>(shared_from_this()));
{
std::lock_guard<std::mutex> lock(pageCacheMutex_);
pageCache_[pageIndex] = pageObj;
}
return pageObj;
#else
(void)pageIndex;
(void) pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
@@ -250,7 +266,8 @@ std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::saveFullForExpo
if (bytes) {
qpdf_layer::QpdfWriter writer;
auto withAppearances = writer.setNeedAppearances(*bytes);
if (withAppearances) return *withAppearances;
if (withAppearances)
return *withAppearances;
spdlog::warn("saveFullForExport: NeedAppearances pass failed ({}); returning plain save",
withAppearances.error());
}
@@ -282,5 +299,4 @@ void PdfiumDocument::invalidateCaches() {
spdlog::info("Document caches have been invalidated.");
}
}
} // namespace pdfengine::parser
+1
View File
@@ -169,6 +169,7 @@ private:
std::expected<void, EngineError> applyOp_reflow(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_textOverlay(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_stamp(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_watermark(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_decoration(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_redaction(const nlohmann::json& op, int pageIndex);
std::expected<void, EngineError> applyOp_updateField(const nlohmann::json& op, int pageIndex);
+3 -1
View File
@@ -44,7 +44,7 @@ std::expected<std::vector<InvalidatedRegion>, EngineError> PdfiumDocument::apply
}
static const std::set<std::string> kContentOps = {
"replace_text", "reflow_paragraph", "text_overlay", "add_text", "stamp",
"replace_text", "reflow_paragraph", "text_overlay", "add_text", "stamp", "watermark", "add_watermark",
"underline", "strikeout", "squiggly", "redaction",
"image_overlay", "highlight", "free_text", "comment", "freehand"};
if (kContentOps.count(type)) markEdited(pageIndex);
@@ -58,6 +58,8 @@ std::expected<std::vector<InvalidatedRegion>, EngineError> PdfiumDocument::apply
r = applyOp_textOverlay(op, pageIndex);
} else if (type == "stamp") {
r = applyOp_stamp(op, pageIndex);
} else if (type == "watermark" || type == "add_watermark") {
r = applyOp_watermark(op, pageIndex);
} else if (type == "underline" || type == "strikeout" || type == "squiggly") {
r = applyOp_decoration(op, pageIndex);
} else if (type == "redaction") {
@@ -676,4 +676,107 @@ std::expected<void, EngineError> PdfiumDocument::applyOp_updateAnnotation(const
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyOp_watermark(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("watermark operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
std::string text = data.value("text", "CONFIDENTIAL");
if (text.empty()) return {};
std::string fontFamily = data.value("fontFamily", "Helvetica");
std::string fontWeight = data.value("fontWeight", "normal");
double fontSize = data.value("fontSize", 48.0);
std::string color = data.value("color", "#000000");
double opacity = data.value("opacity", 0.25);
double rotation = data.value("rotation", -45.0);
std::string position = data.value("position", "center");
double xOffset = data.value("xOffset", 0.0);
double yOffset = data.value("yOffset", 0.0);
// Font resolution with logging
std::string stdFontName = "Helvetica";
if (fontFamily.find("Times") != std::string::npos) {
stdFontName = (fontWeight == "bold" || fontWeight == "700") ? "Times-Bold" : "Times-Roman";
} else if (fontFamily.find("Courier") != std::string::npos) {
stdFontName = (fontWeight == "bold" || fontWeight == "700") ? "Courier-Bold" : "Courier";
} else {
stdFontName = (fontWeight == "bold" || fontWeight == "700") ? "Helvetica-Bold" : "Helvetica";
}
spdlog::info("[WATERMARK_FONT] Requested: {} ({}) Resolved: {}", fontFamily, fontWeight, stdFontName);
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for watermark", pageIndex);
return std::unexpected(EngineError::Unknown);
}
double pageWidth = FPDF_GetPageWidth(page);
double pageHeight = FPDF_GetPageHeight(page);
FPDF_FONT font = FPDFText_LoadStandardFont(doc_, stdFontName.c_str());
if (!font) {
font = FPDFText_LoadStandardFont(doc_, "Helvetica");
}
FPDF_PAGEOBJECT textObj = FPDFPageObj_CreateTextObj(doc_, font, static_cast<float>(fontSize));
unsigned int r = 0, g = 0, b = 0;
parseHexColor(color, r, g, b);
unsigned int alpha = static_cast<unsigned int>(std::clamp(opacity, 0.0, 1.0) * 255.0);
FPDFPageObj_SetFillColor(textObj, r, g, b, alpha);
auto utf16 = utf8_to_utf16le(text);
FPDFText_SetText(textObj, reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
float left = 0, bottom = 0, right = 0, top = 0;
FPDFPageObj_GetBounds(textObj, &left, &bottom, &right, &top);
float textWidth = right - left;
float textHeight = top - bottom;
double margin = 36.0;
double cx = pageWidth / 2.0;
double cy = pageHeight / 2.0;
if (position == "top_left") { cx = margin + textWidth / 2.0; cy = pageHeight - margin - textHeight / 2.0; }
else if (position == "top_center") { cx = pageWidth / 2.0; cy = pageHeight - margin - textHeight / 2.0; }
else if (position == "top_right") { cx = pageWidth - margin - textWidth / 2.0; cy = pageHeight - margin - textHeight / 2.0; }
else if (position == "center_left") { cx = margin + textWidth / 2.0; cy = pageHeight / 2.0; }
else if (position == "center_right") { cx = pageWidth - margin - textWidth / 2.0; cy = pageHeight / 2.0; }
else if (position == "bottom_left") { cx = margin + textWidth / 2.0; cy = margin + textHeight / 2.0; }
else if (position == "bottom_center") { cx = pageWidth / 2.0; cy = margin + textHeight / 2.0; }
else if (position == "bottom_right") { cx = pageWidth - margin - textWidth / 2.0; cy = margin + textHeight / 2.0; }
cx += xOffset;
cy += yOffset;
spdlog::info("[WATERMARK_ENGINE] Page: {} Page size: {}x{} PDF position: x={}, y={} Drawing watermark: {}",
pageIndex, pageWidth, pageHeight, cx, cy, text);
double rad = rotation * 3.14159265358979323846 / 180.0;
double cosA = std::cos(rad);
double sinA = std::sin(rad);
double matA = cosA;
double matB = sinA;
double matC = -sinA;
double matD = cosA;
double matE = cx - (cosA * (textWidth / 2.0) - sinA * (textHeight / 2.0));
double matF = cy - (sinA * (textWidth / 2.0) + cosA * (textHeight / 2.0));
FPDFPageObj_Transform(textObj, static_cast<float>(matA), static_cast<float>(matB), static_cast<float>(matC), static_cast<float>(matD), static_cast<float>(matE), static_cast<float>(matF));
FPDFPage_InsertObject(page, textObj);
if (!FPDFPage_GenerateContent(page)) {
spdlog::error("Failed to generate page content after watermark");
}
FPDF_ClosePage(page);
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
}

Some files were not shown because too many files have changed in this diff Show More