Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74c79f83e9 | ||
|
|
4256ac8128 | ||
|
|
904cf96bd6 | ||
|
|
b9a4839177 | ||
|
|
2cef7b12c0 | ||
|
|
f98d4fa934 | ||
|
|
84d215ebd7 | ||
|
|
39b125db9a | ||
|
|
f168b12c82 | ||
|
|
eb514195bf | ||
|
|
c94ffbf73d | ||
|
|
83f47488ab | ||
|
|
3f340e9689 | ||
|
|
e04ac57e40 | ||
|
|
75d0b9c1cb | ||
|
|
adb599b921 | ||
|
|
3da5276cc1 | ||
|
|
d64d1a18a3 | ||
|
|
1217649a96 | ||
|
|
e37c9392e1 | ||
|
|
4565522705 | ||
|
|
ed28cb68de | ||
|
|
a2f5d59171 | ||
|
|
e96d9fc10b | ||
|
|
6c22179699 | ||
|
|
59854f8f61 | ||
|
|
47f4eccbea | ||
|
|
5c5fe450ea | ||
|
|
c7ffac4415 | ||
|
|
adad76f146 | ||
|
|
672e1594e1 | ||
|
|
47c03a0044 | ||
|
|
5c559b2a0f | ||
|
|
778845b340 | ||
|
|
4ff16adab3 | ||
|
|
0a97af54c9 | ||
|
|
1174a00815 | ||
|
|
ab65b4435b | ||
|
|
10ae1c0539 | ||
|
|
3d9a72d272 | ||
|
|
e68137219c | ||
|
|
d7621a07ed | ||
|
|
404215f906 | ||
|
|
d85c3a3e43 | ||
|
|
f89ccfbe5d | ||
|
|
709e61b6a5 | ||
|
|
0ff8e8f446 | ||
|
|
5aa352f181 | ||
|
|
c75ae3ca08 | ||
|
|
8dc5a9da7d | ||
|
|
80cad7a53b |
@@ -16,6 +16,7 @@
|
|||||||
**/.idea
|
**/.idea
|
||||||
**/coverage
|
**/coverage
|
||||||
**/tmp
|
**/tmp
|
||||||
|
corpus/
|
||||||
**/.pytest_cache
|
**/.pytest_cache
|
||||||
**/.mypy_cache
|
**/.mypy_cache
|
||||||
**/.ruff_cache
|
**/.ruff_cache
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
@@ -21,6 +21,7 @@ CMakeUserPresets.json
|
|||||||
/third_party/pdfium/checkout/
|
/third_party/pdfium/checkout/
|
||||||
/third_party/pdfium/install/
|
/third_party/pdfium/install/
|
||||||
/third_party/pdfium/.gclient*
|
/third_party/pdfium/.gclient*
|
||||||
|
/corpus/
|
||||||
|
|
||||||
# Skia from-source build (depot_tools / GN / Ninja)
|
# Skia from-source build (depot_tools / GN / Ninja)
|
||||||
/third_party/skia/depot_tools/
|
/third_party/skia/depot_tools/
|
||||||
@@ -80,3 +81,12 @@ timeout-*
|
|||||||
PDF Editor Timeline.xlsx
|
PDF Editor Timeline.xlsx
|
||||||
# Local environment config / secrets
|
# Local environment config / secrets
|
||||||
/third_party/pdfium-wasm/
|
/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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"name": "win-local",
|
"name": "win-local",
|
||||||
"displayName": "Windows • Debug (local — build dir outside OneDrive/spaces)",
|
"displayName": "Windows • Debug (local — build dir outside OneDrive/spaces)",
|
||||||
"inherits": "windows-debug",
|
"inherits": "windows-debug",
|
||||||
"binaryDir": "C:/Users/@USERNAME@/pdfeng-build/win-local",
|
"binaryDir": "D:/pdfeng-build/win-local",
|
||||||
"cacheVariables": {
|
"cacheVariables": {
|
||||||
"PDFENGINE_WITH_PDFIUM": "ON",
|
"PDFENGINE_WITH_PDFIUM": "ON",
|
||||||
"PDFENGINE_WITH_QPDF": "ON"
|
"PDFENGINE_WITH_QPDF": "ON"
|
||||||
@@ -16,11 +16,8 @@
|
|||||||
"name": "win-local-pdfium",
|
"name": "win-local-pdfium",
|
||||||
"displayName": "Windows • RelWithDebInfo + PDFium (static CRT — build dir outside OneDrive/spaces)",
|
"displayName": "Windows • RelWithDebInfo + PDFium (static CRT — build dir outside OneDrive/spaces)",
|
||||||
"inherits": "windows-release",
|
"inherits": "windows-release",
|
||||||
"binaryDir": "C:/Users/@USERNAME@/pdfeng-build/win-local-pdfium",
|
"binaryDir": "D:/pdfeng-build/win-local-pdfium",
|
||||||
"cacheVariables": {
|
"cacheVariables": { "PDFENGINE_WITH_PDFIUM": "ON" }
|
||||||
"PDFENGINE_WITH_PDFIUM": "ON",
|
|
||||||
"PDFENGINE_WITH_QPDF": "ON"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"buildPresets": [
|
"buildPresets": [
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB |
+440
-208
@@ -1,7 +1,8 @@
|
|||||||
#include <pybind11/pybind11.h>
|
|
||||||
#include <pybind11/stl.h>
|
|
||||||
#include <pdfengine/pdf_document.hpp>
|
#include <pdfengine/pdf_document.hpp>
|
||||||
#include <pdfengine/pdf_engine.hpp>
|
#include <pdfengine/pdf_engine.hpp>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
#include <pybind11/pybind11.h>
|
||||||
|
#include <pybind11/stl.h>
|
||||||
|
|
||||||
namespace py = pybind11;
|
namespace py = pybind11;
|
||||||
|
|
||||||
@@ -9,28 +10,27 @@ namespace {
|
|||||||
|
|
||||||
void throw_on_error(pdfengine::EngineError err) {
|
void throw_on_error(pdfengine::EngineError err) {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
case pdfengine::EngineError::FileNotFound:
|
case pdfengine::EngineError::FileNotFound:
|
||||||
PyErr_SetString(PyExc_FileNotFoundError, "PDF file not found");
|
PyErr_SetString(PyExc_FileNotFoundError, "PDF file not found");
|
||||||
throw py::error_already_set();
|
throw py::error_already_set();
|
||||||
case pdfengine::EngineError::InvalidFormat:
|
case pdfengine::EngineError::InvalidFormat:
|
||||||
throw py::value_error("Invalid PDF format");
|
throw py::value_error("Invalid PDF format");
|
||||||
case pdfengine::EngineError::PasswordRequired:
|
case pdfengine::EngineError::PasswordRequired:
|
||||||
throw py::value_error("Password required to open this PDF");
|
throw py::value_error("Password required to open this PDF");
|
||||||
case pdfengine::EngineError::InvalidPassword:
|
case pdfengine::EngineError::InvalidPassword:
|
||||||
throw py::value_error("Invalid password provided for this PDF");
|
throw py::value_error("Invalid password provided for this PDF");
|
||||||
case pdfengine::EngineError::PageOutOfBounds:
|
case pdfengine::EngineError::PageOutOfBounds:
|
||||||
throw py::index_error("Page index out of bounds");
|
throw py::index_error("Page index out of bounds");
|
||||||
case pdfengine::EngineError::RenderFailed:
|
case pdfengine::EngineError::RenderFailed:
|
||||||
throw std::runtime_error("Failed to render PDF page");
|
throw std::runtime_error("Failed to render PDF page");
|
||||||
case pdfengine::EngineError::WriteFailed:
|
case pdfengine::EngineError::WriteFailed:
|
||||||
throw std::runtime_error("Failed to write PDF data");
|
throw std::runtime_error("Failed to write PDF data");
|
||||||
default:
|
default:
|
||||||
throw std::runtime_error("Unknown PDF engine error");
|
throw std::runtime_error("Unknown PDF engine error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T>
|
template <typename T> T get_or_throw(std::expected<T, pdfengine::EngineError>&& res) {
|
||||||
T get_or_throw(std::expected<T, pdfengine::EngineError>&& res) {
|
|
||||||
if (!res.has_value()) {
|
if (!res.has_value()) {
|
||||||
throw_on_error(res.error());
|
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 <parser/content_builder.hpp>
|
||||||
#include <qpdf/qpdf_extractor.hpp>
|
|
||||||
#include <qpdf/qpdf_writer.hpp>
|
|
||||||
#include <parser/lexer.hpp>
|
#include <parser/lexer.hpp>
|
||||||
#include <parser/parser.hpp>
|
#include <parser/parser.hpp>
|
||||||
#include <parser/content_builder.hpp>
|
#include <pdfengine/content_object.hpp>
|
||||||
#include <serializer/content_serializer.hpp>
|
#include <pdfengine/ocr/ocr_coordinator.hpp>
|
||||||
|
#include <qpdf/qpdf_extractor.hpp>
|
||||||
|
#include <qpdf/qpdf_writer.hpp>
|
||||||
#include <serializer/ast_serializer.hpp>
|
#include <serializer/ast_serializer.hpp>
|
||||||
|
#include <serializer/content_serializer.hpp>
|
||||||
|
|
||||||
static constexpr double kTjSpaceKern = -500.0;
|
static constexpr double kTjSpaceKern = -500.0;
|
||||||
|
|
||||||
@@ -66,14 +67,14 @@ public:
|
|||||||
if (!stream.has_value()) {
|
if (!stream.has_value()) {
|
||||||
throw std::runtime_error("Failed to extract page stream");
|
throw std::runtime_error("Failed to extract page stream");
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfengine::Lexer lexer(stream->decodedContent);
|
pdfengine::Lexer lexer(stream->decodedContent);
|
||||||
auto tokens = lexer.tokenize();
|
auto tokens = lexer.tokenize();
|
||||||
pdfengine::ContentParser parser(tokens);
|
pdfengine::ContentParser parser(tokens);
|
||||||
pdfengine::ContentBuilder builder;
|
pdfengine::ContentBuilder builder;
|
||||||
|
|
||||||
auto objects = builder.build(parser.parse());
|
auto objects = builder.build(parser.parse());
|
||||||
|
|
||||||
py::list result;
|
py::list result;
|
||||||
for (const auto& obj : objects) {
|
for (const auto& obj : objects) {
|
||||||
if (obj->getType() == pdfengine::ContentObjectType::Text) {
|
if (obj->getType() == pdfengine::ContentObjectType::Text) {
|
||||||
@@ -82,7 +83,7 @@ public:
|
|||||||
d["text"] = py::bytes(textObj->text);
|
d["text"] = py::bytes(textObj->text);
|
||||||
d["fontName"] = textObj->fontName;
|
d["fontName"] = textObj->fontName;
|
||||||
d["fontSize"] = textObj->fontSize;
|
d["fontSize"] = textObj->fontSize;
|
||||||
|
|
||||||
py::list tm;
|
py::list tm;
|
||||||
for (int i = 0; i < 6; ++i) {
|
for (int i = 0; i < 6; ++i) {
|
||||||
tm.append(textObj->tm[i]);
|
tm.append(textObj->tm[i]);
|
||||||
@@ -94,24 +95,28 @@ public:
|
|||||||
return result;
|
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;
|
std::string new_text = new_text_bytes;
|
||||||
pdfengine::qpdf_layer::QpdfExtractor extractor;
|
pdfengine::qpdf_layer::QpdfExtractor extractor;
|
||||||
auto stream = extractor.extractPageStream(filepath_, page_index);
|
auto stream = extractor.extractPageStream(filepath_, page_index);
|
||||||
if (!stream.has_value()) return false;
|
if (!stream.has_value())
|
||||||
|
return false;
|
||||||
|
|
||||||
pdfengine::Lexer lexer(stream->decodedContent);
|
pdfengine::Lexer lexer(stream->decodedContent);
|
||||||
auto tokens = lexer.tokenize();
|
auto tokens = lexer.tokenize();
|
||||||
pdfengine::ContentParser parser(tokens);
|
pdfengine::ContentParser parser(tokens);
|
||||||
auto operations = parser.parse();
|
auto operations = parser.parse();
|
||||||
|
|
||||||
int textCount = 0;
|
int textCount = 0;
|
||||||
bool modified = false;
|
bool modified = false;
|
||||||
for (auto& op : operations) {
|
for (auto& op : operations) {
|
||||||
if (op.op == "Tj" || op.op == "'") {
|
if (op.op == "Tj" || op.op == "'") {
|
||||||
if (op.operands.empty()) continue;
|
if (op.operands.empty())
|
||||||
|
continue;
|
||||||
auto& strNode = op.operands.back();
|
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) {
|
if (textCount == object_index) {
|
||||||
strNode->type = pdfengine::AstNodeType::String;
|
strNode->type = pdfengine::AstNodeType::String;
|
||||||
strNode->stringValue = new_text;
|
strNode->stringValue = new_text;
|
||||||
@@ -121,7 +126,8 @@ public:
|
|||||||
textCount++;
|
textCount++;
|
||||||
}
|
}
|
||||||
} else if (op.op == "TJ") {
|
} else if (op.op == "TJ") {
|
||||||
if (op.operands.empty()) continue;
|
if (op.operands.empty())
|
||||||
|
continue;
|
||||||
auto& arrNode = op.operands.back();
|
auto& arrNode = op.operands.back();
|
||||||
if (arrNode->type == pdfengine::AstNodeType::Array) {
|
if (arrNode->type == pdfengine::AstNodeType::Array) {
|
||||||
std::string combinedText;
|
std::string combinedText;
|
||||||
@@ -129,9 +135,11 @@ public:
|
|||||||
if (item->type == pdfengine::AstNodeType::String) {
|
if (item->type == pdfengine::AstNodeType::String) {
|
||||||
combinedText += item->stringValue;
|
combinedText += item->stringValue;
|
||||||
} else if (item->type == pdfengine::AstNodeType::HexString) {
|
} 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) {
|
} else if (item->type == pdfengine::AstNodeType::Number) {
|
||||||
if (item->numberValue < kTjSpaceKern) combinedText += " ";
|
if (item->numberValue < kTjSpaceKern)
|
||||||
|
combinedText += " ";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!combinedText.empty()) {
|
if (!combinedText.empty()) {
|
||||||
@@ -139,17 +147,22 @@ public:
|
|||||||
bool redistributed = false;
|
bool redistributed = false;
|
||||||
if (new_text.size() == combinedText.size()) {
|
if (new_text.size() == combinedText.size()) {
|
||||||
std::vector<std::pair<pdfengine::AstNode*, std::string>> assign;
|
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) {
|
for (const auto& item : arrNode->arrayItems) {
|
||||||
if (item->type == pdfengine::AstNodeType::String ||
|
if (item->type == pdfengine::AstNodeType::String ||
|
||||||
item->type == pdfengine::AstNodeType::HexString) {
|
item->type == pdfengine::AstNodeType::HexString) {
|
||||||
size_t L = (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));
|
assign.emplace_back(item.get(), new_text.substr(pos, L));
|
||||||
pos += L;
|
pos += L;
|
||||||
} else if (item->type == pdfengine::AstNodeType::Number &&
|
} else if (item->type == pdfengine::AstNodeType::Number &&
|
||||||
item->numberValue < kTjSpaceKern) {
|
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;
|
pos += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +176,8 @@ public:
|
|||||||
}
|
}
|
||||||
if (!redistributed) {
|
if (!redistributed) {
|
||||||
arrNode->arrayItems.clear();
|
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;
|
newStrNode->stringValue = new_text;
|
||||||
arrNode->arrayItems.push_back(std::move(newStrNode));
|
arrNode->arrayItems.push_back(std::move(newStrNode));
|
||||||
}
|
}
|
||||||
@@ -175,12 +189,13 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!modified) return false;
|
if (!modified)
|
||||||
|
return false;
|
||||||
|
|
||||||
pdfengine::AstSerializer astSerializer;
|
pdfengine::AstSerializer astSerializer;
|
||||||
std::string newRawStream = astSerializer.serialize(operations);
|
std::string newRawStream = astSerializer.serialize(operations);
|
||||||
|
|
||||||
pdfengine::qpdf_layer::QpdfWriter writer;
|
pdfengine::qpdf_layer::QpdfWriter writer;
|
||||||
auto res = writer.replacePageStreamAndSave(filepath_, dest_path, page_index, newRawStream);
|
auto res = writer.replacePageStreamAndSave(filepath_, dest_path, page_index, newRawStream);
|
||||||
return res.has_value();
|
return res.has_value();
|
||||||
@@ -196,12 +211,87 @@ PYBIND11_MODULE(pdfengine, m) {
|
|||||||
py::class_<StreamEditor>(m, "StreamEditor")
|
py::class_<StreamEditor>(m, "StreamEditor")
|
||||||
.def(py::init<const std::string&>(), py::arg("filepath"))
|
.def(py::init<const std::string&>(), py::arg("filepath"))
|
||||||
.def("extract_text_objects", &StreamEditor::extract_text_objects, py::arg("page_index"))
|
.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_version", &pdfengine::engineVersion, "Get the engine version string");
|
||||||
m.def("engine_build_info", &pdfengine::engineBuildInfo, "Get the engine build info 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_pdfium", &pdfengine::engineHasPdfium,
|
||||||
m.def("engine_has_skia", &pdfengine::engineHasSkia, "Check if the engine was built with Skia support");
|
"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")
|
py::class_<pdfengine::Point2D>(m, "Point2D")
|
||||||
.def(py::init<double, double>(), py::arg("x") = 0.0, py::arg("y") = 0.0)
|
.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("x", &pdfengine::DevicePoint::x)
|
||||||
.def_readwrite("y", &pdfengine::DevicePoint::y)
|
.def_readwrite("y", &pdfengine::DevicePoint::y)
|
||||||
.def("__repr__", [](const pdfengine::DevicePoint& self) {
|
.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")
|
py::class_<pdfengine::DocumentMetadata>(m, "DocumentMetadata")
|
||||||
@@ -241,7 +332,8 @@ PYBIND11_MODULE(pdfengine, m) {
|
|||||||
.def_readonly("can_copy", &pdfengine::DocumentPermissions::canCopy)
|
.def_readonly("can_copy", &pdfengine::DocumentPermissions::canCopy)
|
||||||
.def_readonly("can_annotate", &pdfengine::DocumentPermissions::canAnnotate)
|
.def_readonly("can_annotate", &pdfengine::DocumentPermissions::canAnnotate)
|
||||||
.def_readonly("can_fill_forms", &pdfengine::DocumentPermissions::canFillForms)
|
.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);
|
.def_readonly("can_assemble", &pdfengine::DocumentPermissions::canAssemble);
|
||||||
|
|
||||||
py::class_<pdfengine::PageImage>(m, "PageImage")
|
py::class_<pdfengine::PageImage>(m, "PageImage")
|
||||||
@@ -272,7 +364,8 @@ PYBIND11_MODULE(pdfengine, m) {
|
|||||||
.def_readonly("descent", &pdfengine::FontInfo::descent)
|
.def_readonly("descent", &pdfengine::FontInfo::descent)
|
||||||
.def_readonly("cap_height", &pdfengine::FontInfo::capHeight)
|
.def_readonly("cap_height", &pdfengine::FontInfo::capHeight)
|
||||||
.def("__repr__", [](const pdfengine::FontInfo& self) {
|
.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")
|
py::class_<pdfengine::Glyph>(m, "Glyph")
|
||||||
@@ -297,6 +390,8 @@ PYBIND11_MODULE(pdfengine, m) {
|
|||||||
.def_readonly("font_size", &pdfengine::TextRun::fontSize)
|
.def_readonly("font_size", &pdfengine::TextRun::fontSize)
|
||||||
.def_readonly("internal_font_id", &pdfengine::TextRun::internalFontId)
|
.def_readonly("internal_font_id", &pdfengine::TextRun::internalFontId)
|
||||||
.def_readonly("is_embedded", &pdfengine::TextRun::isEmbedded)
|
.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("type", &pdfengine::TextRun::type)
|
||||||
.def_readonly("glyphs", &pdfengine::TextRun::glyphs)
|
.def_readonly("glyphs", &pdfengine::TextRun::glyphs)
|
||||||
.def_readonly("x", &pdfengine::TextRun::x)
|
.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")
|
py::class_<pdfengine::PdfPage, std::shared_ptr<pdfengine::PdfPage>>(m, "PdfPage")
|
||||||
.def_property_readonly("width", &pdfengine::PdfPage::width)
|
.def_property_readonly("width", &pdfengine::PdfPage::width)
|
||||||
.def_property_readonly("height", &pdfengine::PdfPage::height)
|
.def_property_readonly("height", &pdfengine::PdfPage::height)
|
||||||
.def("render", [](const pdfengine::PdfPage& self, int dpi) {
|
.def(
|
||||||
return get_or_throw(self.render(dpi));
|
"render",
|
||||||
}, py::arg("dpi") = 96)
|
[](const pdfengine::PdfPage& self, int dpi) { return get_or_throw(self.render(dpi)); },
|
||||||
.def("render_region_raw", [](const pdfengine::PdfPage& self, int dpi, double y_top_pt, double height_pt) {
|
py::arg("dpi") = 96)
|
||||||
auto img = get_or_throw(self.renderRegionRaw(dpi, y_top_pt, height_pt));
|
.def(
|
||||||
return py::make_tuple(img.width, img.height,
|
"render_region_raw",
|
||||||
py::bytes(reinterpret_cast<const char*>(img.data.data()), img.data.size()));
|
[](const pdfengine::PdfPage& self, int dpi, double y_top_pt, double height_pt) {
|
||||||
}, py::arg("dpi"), py::arg("y_top_pt"), py::arg("height_pt") = 0.0)
|
auto img = get_or_throw(self.renderRegionRaw(dpi, y_top_pt, height_pt));
|
||||||
.def("render_tile", [](const pdfengine::PdfPage& self, int dpi, double xPt, double yPt, double wPt, double hPt) {
|
return py::make_tuple(
|
||||||
auto img = get_or_throw(self.renderTile(dpi, xPt, yPt, wPt, hPt));
|
img.width, img.height,
|
||||||
return py::make_tuple(img.width, img.height,
|
py::bytes(reinterpret_cast<const char*>(img.data.data()), img.data.size()));
|
||||||
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"))
|
py::arg("dpi"), py::arg("y_top_pt"), py::arg("height_pt") = 0.0)
|
||||||
.def("extract_document_model", [](const pdfengine::PdfPage& self) {
|
.def(
|
||||||
return get_or_throw(self.extractDocumentModel());
|
"render_tile",
|
||||||
})
|
[](const pdfengine::PdfPage& self, int dpi, double xPt, double yPt, double wPt,
|
||||||
.def("extract_text", [](const pdfengine::PdfPage& self) {
|
double hPt) {
|
||||||
return get_or_throw(self.extractText());
|
auto img = get_or_throw(self.renderTile(dpi, xPt, yPt, wPt, hPt));
|
||||||
})
|
return py::make_tuple(
|
||||||
.def("extract_annotations_text", [](const pdfengine::PdfPage& self) {
|
img.width, img.height,
|
||||||
return get_or_throw(self.extractAnnotationsText());
|
py::bytes(reinterpret_cast<const char*>(img.data.data()), img.data.size()));
|
||||||
})
|
},
|
||||||
.def("extract_annotations", [](const pdfengine::PdfPage& self) {
|
py::arg("dpi"), py::arg("xPt"), py::arg("yPt"), py::arg("wPt"), py::arg("hPt"))
|
||||||
return get_or_throw(self.extractAnnotations());
|
.def("extract_document_model",
|
||||||
})
|
[](const pdfengine::PdfPage& self) {
|
||||||
.def("extract_text_with_bounds", [](const pdfengine::PdfPage& self) {
|
return get_or_throw(self.extractDocumentModel());
|
||||||
auto res = get_or_throw(self.extractTextWithBounds());
|
})
|
||||||
py::list py_list;
|
.def("extract_text",
|
||||||
for (const auto& glyph : res) {
|
[](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;
|
py::dict d;
|
||||||
d["text"] = glyph.text;
|
d["glyphIndex"] = hit.glyphIndex;
|
||||||
d["x"] = glyph.x;
|
d["caret"] = hit.caret;
|
||||||
d["y"] = glyph.y;
|
d["line"] = hit.line;
|
||||||
d["w"] = glyph.w;
|
return d;
|
||||||
d["h"] = glyph.h;
|
},
|
||||||
d["fontSize"] = glyph.fontSize;
|
py::arg("x"), py::arg("y"))
|
||||||
py_list.append(d);
|
.def(
|
||||||
}
|
"select_range",
|
||||||
return py_list;
|
[](const pdfengine::PdfPage& self, double ax, double ay, double bx, double by) {
|
||||||
})
|
auto sel = get_or_throw(self.selectRange(ax, ay, bx, by));
|
||||||
.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;
|
py::dict d;
|
||||||
d["text"] = g.text; d["x"] = g.x; d["y"] = g.y;
|
d["startGlyph"] = sel.startGlyph;
|
||||||
d["w"] = g.w; d["h"] = g.h; d["fontSize"] = g.fontSize;
|
d["endGlyph"] = sel.endGlyph;
|
||||||
py_list.append(d);
|
d["text"] = sel.text;
|
||||||
}
|
py::list rects;
|
||||||
return py_list;
|
for (const auto& r : sel.rects) {
|
||||||
})
|
py::dict rd;
|
||||||
.def("hit_glyph", [](const pdfengine::PdfPage& self, double x, double y) {
|
rd["x"] = r.x;
|
||||||
auto hit = get_or_throw(self.hitGlyph(x, y));
|
rd["y"] = r.y;
|
||||||
py::dict d;
|
rd["w"] = r.w;
|
||||||
d["glyphIndex"] = hit.glyphIndex;
|
rd["h"] = r.h;
|
||||||
d["caret"] = hit.caret;
|
rects.append(rd);
|
||||||
d["line"] = hit.line;
|
}
|
||||||
return d;
|
d["rects"] = rects;
|
||||||
}, py::arg("x"), py::arg("y"))
|
return d;
|
||||||
.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::arg("ax"), py::arg("ay"), py::arg("bx"), py::arg("by"))
|
||||||
py::dict d;
|
.def("get_fonts",
|
||||||
d["startGlyph"] = sel.startGlyph;
|
[](const pdfengine::PdfPage& self) { return get_or_throw(self.getFonts()); })
|
||||||
d["endGlyph"] = sel.endGlyph;
|
.def(
|
||||||
d["text"] = sel.text;
|
"get_glyph_width",
|
||||||
py::list rects;
|
[](const pdfengine::PdfPage& self, const std::string& fontName, uint32_t charcode,
|
||||||
for (const auto& r : sel.rects) {
|
double fontSize) {
|
||||||
py::dict rd;
|
return get_or_throw(self.getGlyphWidth(fontName, charcode, fontSize));
|
||||||
rd["x"] = r.x; rd["y"] = r.y; rd["w"] = r.w; rd["h"] = r.h;
|
},
|
||||||
rects.append(rd);
|
py::arg("font_name"), py::arg("charcode"), py::arg("font_size"))
|
||||||
}
|
.def("page_to_device", &pdfengine::PdfPage::pageToDevice, py::arg("page_point"),
|
||||||
d["rects"] = rects;
|
py::arg("device_width"), py::arg("device_height"), py::arg("rotate") = 0)
|
||||||
return d;
|
.def("device_to_page", &pdfengine::PdfPage::deviceToPage, py::arg("device_point"),
|
||||||
}, py::arg("ax"), py::arg("ay"), py::arg("bx"), py::arg("by"))
|
py::arg("device_width"), py::arg("device_height"), py::arg("rotate") = 0)
|
||||||
.def("get_fonts", [](const pdfengine::PdfPage& self) {
|
.def("extract_display_list",
|
||||||
return get_or_throw(self.getFonts());
|
[](const pdfengine::PdfPage& self) {
|
||||||
})
|
return get_or_throw(self.extractDisplayListJson());
|
||||||
.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));
|
.def(
|
||||||
}, py::arg("font_name"), py::arg("charcode"), py::arg("font_size"))
|
"extract_image_xobject",
|
||||||
.def("page_to_device", &pdfengine::PdfPage::pageToDevice,
|
[](const pdfengine::PdfPage& self, const std::string& name) {
|
||||||
py::arg("page_point"), py::arg("device_width"), py::arg("device_height"), py::arg("rotate") = 0)
|
auto res = get_or_throw(self.extractImageXObject(name));
|
||||||
.def("device_to_page", &pdfengine::PdfPage::deviceToPage,
|
return py::bytes(reinterpret_cast<const char*>(res.data()), res.size());
|
||||||
py::arg("device_point"), py::arg("device_width"), py::arg("device_height"), py::arg("rotate") = 0)
|
},
|
||||||
.def("extract_display_list", [](const pdfengine::PdfPage& self) {
|
py::arg("name"));
|
||||||
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")
|
py::class_<pdfengine::PdfDocument, std::shared_ptr<pdfengine::PdfDocument>>(m, "PdfDocument")
|
||||||
.def_static("load_from_file", [](const std::string& path, const std::string& password) {
|
.def_static(
|
||||||
return get_or_throw(pdfengine::PdfDocument::loadFromFile(path, password));
|
"load_from_file",
|
||||||
}, py::arg("path"), py::arg("password") = "")
|
[](const std::string& path, const std::string& password) {
|
||||||
.def_static("load_from_memory", [](const py::bytes& bytes, const std::string& password) {
|
return get_or_throw(pdfengine::PdfDocument::loadFromFile(path, password));
|
||||||
std::string_view sv = bytes;
|
},
|
||||||
std::vector<uint8_t> data(sv.begin(), sv.end());
|
py::arg("path"), py::arg("password") = "")
|
||||||
return get_or_throw(pdfengine::PdfDocument::loadFromMemory(data, password));
|
.def_static(
|
||||||
}, py::arg("data"), py::arg("password") = "")
|
"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("page_count", &pdfengine::PdfDocument::pageCount)
|
||||||
.def_property_readonly("metadata", &pdfengine::PdfDocument::metadata)
|
.def_property_readonly("metadata", &pdfengine::PdfDocument::metadata)
|
||||||
.def_property_readonly("permissions", &pdfengine::PdfDocument::permissions)
|
.def_property_readonly("permissions", &pdfengine::PdfDocument::permissions)
|
||||||
.def("extract_outline", [](const pdfengine::PdfDocument& self) {
|
.def("extract_outline",
|
||||||
auto res = get_or_throw(self.extractOutline());
|
[](const pdfengine::PdfDocument& self) {
|
||||||
py::list out;
|
auto res = get_or_throw(self.extractOutline());
|
||||||
for (const auto& item : res) {
|
py::list out;
|
||||||
py::dict d;
|
for (const auto& item : res) {
|
||||||
d["title"] = item.title;
|
py::dict d;
|
||||||
d["pageIndex"] = item.pageIndex;
|
d["title"] = item.title;
|
||||||
d["level"] = item.level;
|
d["pageIndex"] = item.pageIndex;
|
||||||
out.append(d);
|
d["level"] = item.level;
|
||||||
}
|
out.append(d);
|
||||||
return out;
|
}
|
||||||
})
|
return out;
|
||||||
.def("get_page", [](pdfengine::PdfDocument& self, int pageIndex) {
|
})
|
||||||
return get_or_throw(self.getPage(pageIndex));
|
.def(
|
||||||
}, py::arg("page_index"))
|
"get_page",
|
||||||
.def("get_fonts", [](const pdfengine::PdfDocument& self, int start_page, int end_page) {
|
[](pdfengine::PdfDocument& self, int pageIndex) {
|
||||||
return get_or_throw(self.getFonts(start_page, end_page));
|
return get_or_throw(self.getPage(pageIndex));
|
||||||
}, py::arg("start_page") = 0, py::arg("end_page") = -1)
|
},
|
||||||
.def("get_font_data", [](const pdfengine::PdfDocument& self, const std::string& internal_font_id) {
|
py::arg("page_index"))
|
||||||
auto res = self.getFontData(internal_font_id);
|
.def(
|
||||||
if (!res || res->empty()) {
|
"get_fonts",
|
||||||
return py::bytes();
|
[](const pdfengine::PdfDocument& self, int start_page, int end_page) {
|
||||||
}
|
return get_or_throw(self.getFonts(start_page, end_page));
|
||||||
return py::bytes(reinterpret_cast<const char*>(res->data()), res->size());
|
},
|
||||||
}, py::arg("internal_font_id"))
|
py::arg("start_page") = 0, py::arg("end_page") = -1)
|
||||||
.def("get_reconstructed_font_data", [](pdfengine::PdfDocument& self, const std::string& internal_font_id) {
|
.def(
|
||||||
auto res = self.getReconstructedFontData(internal_font_id);
|
"get_font_data",
|
||||||
if (!res || res->empty()) {
|
[](const pdfengine::PdfDocument& self, const std::string& internal_font_id) {
|
||||||
return py::bytes();
|
auto res = self.getFontData(internal_font_id);
|
||||||
}
|
if (!res || res->empty()) {
|
||||||
return py::bytes(reinterpret_cast<const char*>(res->data()), res->size());
|
return py::bytes();
|
||||||
}, py::arg("internal_font_id"))
|
}
|
||||||
.def("apply_edits", [](pdfengine::PdfDocument& self, const std::string& editsJson) {
|
return py::bytes(reinterpret_cast<const char*>(res->data()), res->size());
|
||||||
auto regions = get_or_throw(self.applyEdits(editsJson));
|
},
|
||||||
py::list py_regions;
|
py::arg("internal_font_id"))
|
||||||
for (const auto& r : regions) {
|
.def(
|
||||||
py::dict d;
|
"get_reconstructed_font_data",
|
||||||
d["pageIndex"] = r.pageIndex;
|
[](pdfengine::PdfDocument& self, const std::string& internal_font_id) {
|
||||||
d["x"] = r.x;
|
auto res = self.getReconstructedFontData(internal_font_id);
|
||||||
d["y"] = r.y;
|
if (!res || res->empty()) {
|
||||||
d["width"] = r.width;
|
return py::bytes();
|
||||||
d["height"] = r.height;
|
}
|
||||||
py_regions.append(d);
|
return py::bytes(reinterpret_cast<const char*>(res->data()), res->size());
|
||||||
}
|
},
|
||||||
return py_regions;
|
py::arg("internal_font_id"))
|
||||||
}, py::arg("edits_json"))
|
.def(
|
||||||
.def("last_reflow_layout", [](const pdfengine::PdfDocument& self) {
|
"apply_edits",
|
||||||
return self.lastReflowLayout();
|
[](pdfengine::PdfDocument& self, const std::string& editsJson) {
|
||||||
})
|
auto regions = get_or_throw(self.applyEdits(editsJson));
|
||||||
.def("save_incremental", [](const pdfengine::PdfDocument& self) {
|
py::list py_regions;
|
||||||
std::vector<uint8_t> res = get_or_throw(self.saveIncremental());
|
for (const auto& r : regions) {
|
||||||
return py::bytes(reinterpret_cast<const char*>(res.data()), res.size());
|
py::dict d;
|
||||||
})
|
d["pageIndex"] = r.pageIndex;
|
||||||
.def("save_full", [](const pdfengine::PdfDocument& self) {
|
d["x"] = r.x;
|
||||||
std::vector<uint8_t> res = get_or_throw(self.saveFull());
|
d["y"] = r.y;
|
||||||
return py::bytes(reinterpret_cast<const char*>(res.data()), res.size());
|
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) {
|
.def("save_full_for_export", [](const pdfengine::PdfDocument& self) {
|
||||||
std::vector<uint8_t> res = get_or_throw(self.saveFullForExport());
|
std::vector<uint8_t> res = get_or_throw(self.saveFullForExport());
|
||||||
return py::bytes(reinterpret_cast<const char*>(res.data()), res.size());
|
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
@@ -30,7 +30,7 @@ services:
|
|||||||
image: pdf-engine-frontend:dev
|
image: pdf-engine-frontend:dev
|
||||||
container_name: pdf-engine-frontend
|
container_name: pdf-engine-frontend
|
||||||
environment:
|
environment:
|
||||||
VITE_GATEWAY_URL: http://localhost:8765
|
VITE_GATEWAY_URL: https://pdfapi-dev.maskantech.in
|
||||||
ports:
|
ports:
|
||||||
- "5173:5173"
|
- "5173:5173"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -13,6 +13,9 @@ add_library(pdfengine OBJECT
|
|||||||
src/core/skia_renderer.cpp
|
src/core/skia_renderer.cpp
|
||||||
src/parser/content_stream_parser.cpp
|
src/parser/content_stream_parser.cpp
|
||||||
src/parser/decoration_builder.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/selection.cpp
|
||||||
src/text/text_layout_engine.cpp
|
src/text/text_layout_engine.cpp
|
||||||
src/fonts/face/font_face.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_fallback.cpp
|
||||||
src/fonts/pdf_fonts/font_subset.cpp
|
src/fonts/pdf_fonts/font_subset.cpp
|
||||||
src/fonts/pdf_fonts/font_cmap_builder.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/embedded_font_reconstructor.cpp
|
||||||
src/fonts/pdf_fonts/encoding/encoding.cpp
|
src/fonts/pdf_fonts/encoding/encoding.cpp
|
||||||
src/fonts/pdf_fonts/encoding/tounicode_parser.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_writer.cpp
|
||||||
src/qpdf/qpdf_resource_resolver.cpp
|
src/qpdf/qpdf_resource_resolver.cpp
|
||||||
src/core/image_decoder.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/lexer.cpp
|
||||||
src/parser/parser.cpp
|
src/parser/parser.cpp
|
||||||
src/parser/content_builder.cpp
|
src/parser/content_builder.cpp
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <pdfengine/graphics_state.hpp>
|
#include <pdfengine/graphics_state.hpp>
|
||||||
#include <pdfengine/path.hpp>
|
#include <pdfengine/path.hpp>
|
||||||
|
#include <pdfengine/image_object.hpp>
|
||||||
|
|
||||||
namespace pdfengine {
|
namespace pdfengine {
|
||||||
|
|
||||||
@@ -39,10 +40,14 @@ public:
|
|||||||
double tm[6] = {1.0, 0.0, 0.0, 1.0, 0.0, 0.0};
|
double tm[6] = {1.0, 0.0, 0.0, 1.0, 0.0, 0.0};
|
||||||
};
|
};
|
||||||
|
|
||||||
class ImageObject : public ContentObject {
|
class ImageContentObject : public ContentObject {
|
||||||
public:
|
public:
|
||||||
ContentObjectType getType() const override { return ContentObjectType::Image; }
|
ContentObjectType getType() const override { return ContentObjectType::Image; }
|
||||||
|
|
||||||
|
std::shared_ptr<const ImageObject> image;
|
||||||
|
Matrix transform;
|
||||||
|
|
||||||
|
// Helper compatibility properties
|
||||||
std::string name;
|
std::string name;
|
||||||
int width = 0;
|
int width = 0;
|
||||||
int height = 0;
|
int height = 0;
|
||||||
@@ -50,12 +55,11 @@ public:
|
|||||||
std::string filter;
|
std::string filter;
|
||||||
int bitsPerComponent = 8;
|
int bitsPerComponent = 8;
|
||||||
bool hasSoftMask = false;
|
bool hasSoftMask = false;
|
||||||
|
|
||||||
std::vector<uint8_t> pixelData;
|
std::vector<uint8_t> pixelData;
|
||||||
|
|
||||||
Matrix transform;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
using LegacyImageObject = ImageContentObject;
|
||||||
|
|
||||||
enum class PathPaintOp {
|
enum class PathPaintOp {
|
||||||
Stroke,
|
Stroke,
|
||||||
Fill,
|
Fill,
|
||||||
@@ -72,4 +76,4 @@ public:
|
|||||||
Matrix transform;
|
Matrix transform;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
} // namespace pdfengine
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <pdfengine/graphics_state.hpp>
|
#include <pdfengine/graphics_state.hpp>
|
||||||
#include <pdfengine/path.hpp>
|
#include <pdfengine/path.hpp>
|
||||||
#include <pdfengine/image.hpp>
|
#include <pdfengine/image.hpp>
|
||||||
|
#include <pdfengine/image_object.hpp>
|
||||||
namespace pdfengine {
|
namespace pdfengine {
|
||||||
|
|
||||||
class CommandVisitor;
|
class CommandVisitor;
|
||||||
@@ -63,13 +64,48 @@ struct FillStrokePathCommand : public Command {
|
|||||||
void accept(CommandVisitor& visitor) const override;
|
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 {
|
struct DrawImageCommand : public Command {
|
||||||
|
std::shared_ptr<const ImageObject> imageObject;
|
||||||
ImageInfo image;
|
ImageInfo image;
|
||||||
Matrix matrix;
|
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) {}
|
: image(std::move(img)), matrix(m), opacity(op) {}
|
||||||
|
|
||||||
void accept(CommandVisitor& visitor) const override;
|
void accept(CommandVisitor& visitor) const override;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,6 +143,7 @@ public:
|
|||||||
void strokePath(const Path& path);
|
void strokePath(const Path& path);
|
||||||
void fillStrokePath(const Path& path, FillRule rule = FillRule::NonZero);
|
void fillStrokePath(const Path& path, FillRule rule = FillRule::NonZero);
|
||||||
void drawImage(const ImageInfo& image, const Matrix& m, float opacity = 1.0f);
|
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(); }
|
[[nodiscard]] size_t size() const noexcept { return m_commands.size(); }
|
||||||
void clear() { m_commands.clear(); }
|
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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 {
|
struct TextRun {
|
||||||
std::string text;
|
std::string text;
|
||||||
std::string fontName;
|
std::string fontName;
|
||||||
|
std::string fontFace;
|
||||||
|
int fontWeight = 400;
|
||||||
|
std::string fontStyle = "normal";
|
||||||
uint32_t flags = 0;
|
uint32_t flags = 0;
|
||||||
double fontSize = 0.0;
|
double fontSize = 0.0;
|
||||||
std::string internalFontId;
|
std::string internalFontId;
|
||||||
bool isEmbedded = false;
|
bool isEmbedded = false;
|
||||||
|
bool isEmbeddedFont = false;
|
||||||
|
bool isPredictedFont = false;
|
||||||
std::string type;
|
std::string type;
|
||||||
std::vector<Glyph> glyphs;
|
std::vector<Glyph> glyphs;
|
||||||
std::vector<int> objectIndices;
|
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
|
||||||
@@ -60,4 +60,8 @@ void DisplayList::drawImage(const ImageInfo& image, const Matrix& m, float opaci
|
|||||||
addCommand(std::make_unique<DrawImageCommand>(image, m, opacity));
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 "fonts/pdf_fonts/embedded_font_reconstructor.hpp"
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include "fonts/pdf_fonts/font_cmap_builder.hpp"
|
#include "fonts/pdf_fonts/font_cmap_builder.hpp"
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ ReconstructedFont EmbeddedFontReconstructor::reconstruct(
|
|||||||
if (cmap.empty()) return out;
|
if (cmap.empty()) return out;
|
||||||
std::vector<uint8_t> sfnt = FontCmapBuilder::spliceCmapIntoSfnt(program, cmap);
|
std::vector<uint8_t> sfnt = FontCmapBuilder::spliceCmapIntoSfnt(program, cmap);
|
||||||
if (sfnt.empty()) return out;
|
if (sfnt.empty()) return out;
|
||||||
|
spdlog::info("[RECONSTRUCTOR] Input={} Output={}", program.size(), sfnt.size());
|
||||||
out.sfnt = std::move(sfnt);
|
out.sfnt = std::move(sfnt);
|
||||||
for (const auto& [uni, gid] : unicodeToGid) out.coveredUnicode.insert(uni);
|
for (const auto& [uni, gid] : unicodeToGid) out.coveredUnicode.insert(uni);
|
||||||
out.ok = true;
|
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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "content_builder.hpp"
|
#include "content_builder.hpp"
|
||||||
#include "../core/image_decoder.hpp"
|
#include <pdfengine/image_manager.hpp>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
namespace pdfengine {
|
namespace pdfengine {
|
||||||
@@ -176,48 +176,31 @@ void ContentBuilder::handleDo(const Operation& op, std::vector<std::unique_ptr<C
|
|||||||
if (nameNode->type != AstNodeType::Name) return;
|
if (nameNode->type != AstNodeType::Name) return;
|
||||||
|
|
||||||
std::string name = nameNode->stringValue;
|
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);
|
ResolvedXObject resolved = resolver_->resolveXObject(name);
|
||||||
|
if (resolved.type == XObjectType::Form) {
|
||||||
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) {
|
|
||||||
std::cerr << "Form XObject not fully supported yet.\n";
|
std::cerr << "Form XObject not fully supported yet.\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,12 @@
|
|||||||
|
|
||||||
namespace pdfengine {
|
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) {
|
PdfDocument::loadFromFile(const std::string& path, const std::string& password) {
|
||||||
#ifdef PDFENGINE_WITH_PDFIUM
|
#ifdef PDFENGINE_WITH_PDFIUM
|
||||||
parser::ensure_pdfium_initialized();
|
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) {
|
if (!doc) {
|
||||||
auto err = FPDF_GetLastError();
|
auto err = FPDF_GetLastError();
|
||||||
spdlog::error("Failed to load PDF file from path: {} (error code: {})", path, err);
|
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);
|
return std::make_shared<parser::PdfiumDocument>(doc);
|
||||||
#else
|
#else
|
||||||
(void)path;
|
(void) path;
|
||||||
(void)password;
|
(void) password;
|
||||||
spdlog::error("loadFromFile failed: PDFEngine compiled without PDFium support.");
|
spdlog::error("loadFromFile failed: PDFEngine compiled without PDFium support.");
|
||||||
return std::unexpected(EngineError::Unknown);
|
return std::unexpected(EngineError::Unknown);
|
||||||
#endif
|
#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) {
|
PdfDocument::loadFromMemory(const std::vector<uint8_t>& data, const std::string& password) {
|
||||||
#ifdef PDFENGINE_WITH_PDFIUM
|
#ifdef PDFENGINE_WITH_PDFIUM
|
||||||
parser::ensure_pdfium_initialized();
|
parser::ensure_pdfium_initialized();
|
||||||
@@ -38,8 +39,9 @@ PdfDocument::loadFromMemory(const std::vector<uint8_t>& data, const std::string&
|
|||||||
return std::unexpected(EngineError::InvalidFormat);
|
return std::unexpected(EngineError::InvalidFormat);
|
||||||
}
|
}
|
||||||
std::vector<uint8_t> buffer_copy = data;
|
std::vector<uint8_t> buffer_copy = data;
|
||||||
FPDF_DOCUMENT doc = FPDF_LoadMemDocument(buffer_copy.data(), static_cast<int>(buffer_copy.size()),
|
FPDF_DOCUMENT doc =
|
||||||
password.empty() ? nullptr : password.c_str());
|
FPDF_LoadMemDocument(buffer_copy.data(), static_cast<int>(buffer_copy.size()),
|
||||||
|
password.empty() ? nullptr : password.c_str());
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
auto err = FPDF_GetLastError();
|
auto err = FPDF_GetLastError();
|
||||||
spdlog::error("Failed to load PDF from memory (error code: {})", err);
|
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));
|
return std::make_shared<parser::PdfiumDocument>(doc, std::move(buffer_copy));
|
||||||
#else
|
#else
|
||||||
(void)data;
|
(void) data;
|
||||||
(void)password;
|
(void) password;
|
||||||
spdlog::error("loadFromMemory failed: PDFEngine compiled without PDFium support.");
|
spdlog::error("loadFromMemory failed: PDFEngine compiled without PDFium support.");
|
||||||
return std::unexpected(EngineError::Unknown);
|
return std::unexpected(EngineError::Unknown);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
} // namespace pdfengine
|
||||||
|
|
||||||
|
|
||||||
namespace pdfengine::parser {
|
namespace pdfengine::parser {
|
||||||
|
|
||||||
PdfiumDocument::PdfiumDocument(NativeDocHandle docHandle)
|
PdfiumDocument::PdfiumDocument(NativeDocHandle docHandle) : doc_(docHandle) {
|
||||||
: doc_(docHandle) {}
|
}
|
||||||
|
|
||||||
PdfiumDocument::PdfiumDocument(NativeDocHandle docHandle, std::vector<uint8_t> memoryBuffer)
|
PdfiumDocument::PdfiumDocument(NativeDocHandle docHandle, std::vector<uint8_t> memoryBuffer)
|
||||||
: doc_(docHandle), memoryBuffer_(std::move(memoryBuffer)) {}
|
: doc_(docHandle), memoryBuffer_(std::move(memoryBuffer)) {
|
||||||
|
}
|
||||||
|
|
||||||
PdfiumDocument::~PdfiumDocument() {
|
PdfiumDocument::~PdfiumDocument() {
|
||||||
#ifdef PDFENGINE_WITH_PDFIUM
|
#ifdef PDFENGINE_WITH_PDFIUM
|
||||||
@@ -86,7 +88,8 @@ PdfiumDocument::PdfiumDocument(PdfiumDocument&& other) noexcept {
|
|||||||
PdfiumDocument& PdfiumDocument::operator=(PdfiumDocument&& other) noexcept {
|
PdfiumDocument& PdfiumDocument::operator=(PdfiumDocument&& other) noexcept {
|
||||||
if (this != &other) {
|
if (this != &other) {
|
||||||
#ifdef PDFENGINE_WITH_PDFIUM
|
#ifdef PDFENGINE_WITH_PDFIUM
|
||||||
if (doc_) FPDF_CloseDocument(doc_);
|
if (doc_)
|
||||||
|
FPDF_CloseDocument(doc_);
|
||||||
#endif
|
#endif
|
||||||
doc_ = other.doc_;
|
doc_ = other.doc_;
|
||||||
other.doc_ = nullptr;
|
other.doc_ = nullptr;
|
||||||
@@ -106,11 +109,13 @@ int PdfiumDocument::pageCount() const noexcept {
|
|||||||
DocumentMetadata PdfiumDocument::metadata() const noexcept {
|
DocumentMetadata PdfiumDocument::metadata() const noexcept {
|
||||||
DocumentMetadata meta;
|
DocumentMetadata meta;
|
||||||
#ifdef PDFENGINE_WITH_PDFIUM
|
#ifdef PDFENGINE_WITH_PDFIUM
|
||||||
if (!doc_) return meta;
|
if (!doc_)
|
||||||
|
return meta;
|
||||||
|
|
||||||
auto fetchMeta = [this](const char* key) -> std::string {
|
auto fetchMeta = [this](const char* key) -> std::string {
|
||||||
unsigned long len = FPDF_GetMetaText(doc_, key, nullptr, 0);
|
unsigned long len = FPDF_GetMetaText(doc_, key, nullptr, 0);
|
||||||
if (len <= 2) return "";
|
if (len <= 2)
|
||||||
|
return "";
|
||||||
std::vector<unsigned short> buf(len / 2);
|
std::vector<unsigned short> buf(len / 2);
|
||||||
FPDF_GetMetaText(doc_, key, buf.data(), len);
|
FPDF_GetMetaText(doc_, key, buf.data(), len);
|
||||||
return utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), buf.size());
|
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 PdfiumDocument::permissions() const noexcept {
|
||||||
DocumentPermissions perms;
|
DocumentPermissions perms;
|
||||||
#ifdef PDFENGINE_WITH_PDFIUM
|
#ifdef PDFENGINE_WITH_PDFIUM
|
||||||
if (!doc_) return perms;
|
if (!doc_)
|
||||||
|
return perms;
|
||||||
|
|
||||||
const int rev = FPDF_GetSecurityHandlerRevision(doc_);
|
const int rev = FPDF_GetSecurityHandlerRevision(doc_);
|
||||||
perms.securityRevision = rev;
|
perms.securityRevision = rev;
|
||||||
@@ -139,12 +145,22 @@ DocumentPermissions PdfiumDocument::permissions() const noexcept {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (rev) {
|
switch (rev) {
|
||||||
case 2: perms.encryption = "RC4-40"; break;
|
case 2:
|
||||||
case 3: perms.encryption = "RC4-128"; break;
|
perms.encryption = "RC4-40";
|
||||||
case 4: perms.encryption = "AES-128"; break;
|
break;
|
||||||
case 5:
|
case 3:
|
||||||
case 6: perms.encryption = "AES-256"; break;
|
perms.encryption = "RC4-128";
|
||||||
default: perms.encryption = "Unknown"; break;
|
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_);
|
const unsigned long p = FPDF_GetDocPermissions(doc_);
|
||||||
@@ -164,7 +180,8 @@ DocumentPermissions PdfiumDocument::permissions() const noexcept {
|
|||||||
return perms;
|
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
|
#ifdef PDFENGINE_WITH_PDFIUM
|
||||||
std::vector<PdfDocument::OutlineItem> result;
|
std::vector<PdfDocument::OutlineItem> result;
|
||||||
if (doc_) {
|
if (doc_) {
|
||||||
@@ -201,15 +218,14 @@ std::expected<std::shared_ptr<PdfPage>, EngineError> PdfiumDocument::getPage(int
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto pageObj = std::make_shared<PdfiumPage>(
|
auto pageObj = std::make_shared<PdfiumPage>(
|
||||||
doc_, pageHandle, pageIndex,
|
doc_, pageHandle, pageIndex, std::static_pointer_cast<PdfiumDocument>(shared_from_this()));
|
||||||
std::static_pointer_cast<PdfiumDocument>(shared_from_this()));
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(pageCacheMutex_);
|
std::lock_guard<std::mutex> lock(pageCacheMutex_);
|
||||||
pageCache_[pageIndex] = pageObj;
|
pageCache_[pageIndex] = pageObj;
|
||||||
}
|
}
|
||||||
return pageObj;
|
return pageObj;
|
||||||
#else
|
#else
|
||||||
(void)pageIndex;
|
(void) pageIndex;
|
||||||
return std::unexpected(EngineError::Unknown);
|
return std::unexpected(EngineError::Unknown);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -250,7 +266,8 @@ std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::saveFullForExpo
|
|||||||
if (bytes) {
|
if (bytes) {
|
||||||
qpdf_layer::QpdfWriter writer;
|
qpdf_layer::QpdfWriter writer;
|
||||||
auto withAppearances = writer.setNeedAppearances(*bytes);
|
auto withAppearances = writer.setNeedAppearances(*bytes);
|
||||||
if (withAppearances) return *withAppearances;
|
if (withAppearances)
|
||||||
|
return *withAppearances;
|
||||||
spdlog::warn("saveFullForExport: NeedAppearances pass failed ({}); returning plain save",
|
spdlog::warn("saveFullForExport: NeedAppearances pass failed ({}); returning plain save",
|
||||||
withAppearances.error());
|
withAppearances.error());
|
||||||
}
|
}
|
||||||
@@ -282,5 +299,4 @@ void PdfiumDocument::invalidateCaches() {
|
|||||||
spdlog::info("Document caches have been invalidated.");
|
spdlog::info("Document caches have been invalidated.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace pdfengine::parser
|
||||||
}
|
|
||||||
@@ -169,6 +169,7 @@ private:
|
|||||||
std::expected<void, EngineError> applyOp_reflow(const nlohmann::json& op, int pageIndex);
|
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_textOverlay(const nlohmann::json& op, int pageIndex);
|
||||||
std::expected<void, EngineError> applyOp_stamp(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_decoration(const nlohmann::json& op, int pageIndex);
|
||||||
std::expected<void, EngineError> applyOp_redaction(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);
|
std::expected<void, EngineError> applyOp_updateField(const nlohmann::json& op, int pageIndex);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ std::expected<std::vector<InvalidatedRegion>, EngineError> PdfiumDocument::apply
|
|||||||
}
|
}
|
||||||
|
|
||||||
static const std::set<std::string> kContentOps = {
|
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",
|
"underline", "strikeout", "squiggly", "redaction",
|
||||||
"image_overlay", "highlight", "free_text", "comment", "freehand"};
|
"image_overlay", "highlight", "free_text", "comment", "freehand"};
|
||||||
if (kContentOps.count(type)) markEdited(pageIndex);
|
if (kContentOps.count(type)) markEdited(pageIndex);
|
||||||
@@ -58,6 +58,8 @@ std::expected<std::vector<InvalidatedRegion>, EngineError> PdfiumDocument::apply
|
|||||||
r = applyOp_textOverlay(op, pageIndex);
|
r = applyOp_textOverlay(op, pageIndex);
|
||||||
} else if (type == "stamp") {
|
} else if (type == "stamp") {
|
||||||
r = applyOp_stamp(op, pageIndex);
|
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") {
|
} else if (type == "underline" || type == "strikeout" || type == "squiggly") {
|
||||||
r = applyOp_decoration(op, pageIndex);
|
r = applyOp_decoration(op, pageIndex);
|
||||||
} else if (type == "redaction") {
|
} else if (type == "redaction") {
|
||||||
|
|||||||
@@ -676,4 +676,107 @@ std::expected<void, EngineError> PdfiumDocument::applyOp_updateAnnotation(const
|
|||||||
#endif
|
#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
Reference in New Issue
Block a user