commit 43eba01201a7ae6ee418656d01186d8ce5bb3c3c Author: Furqan-14 Date: Fri May 15 10:39:16 2026 +0530 first commit diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..84290e1 --- /dev/null +++ b/.clang-format @@ -0,0 +1,24 @@ +# PDF Engine C++ formatting. Applied to engine/ and bindings/. +--- +Language: Cpp +BasedOnStyle: LLVM +Standard: c++20 +ColumnLimit: 100 +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +AccessModifierOffset: -4 +NamespaceIndentation: None +PointerAlignment: Left +ReferenceAlignment: Left +AlignAfterOpenBracket: Align +AllowShortFunctionsOnASingleLine: InlineOnly +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +BreakBeforeBraces: Attach +FixNamespaceComments: true +IncludeBlocks: Regroup +SortIncludes: CaseInsensitive +SeparateDefinitionBlocks: Always +SpaceAfterCStyleCast: true +EmptyLineBeforeAccessModifier: LogicalBlock diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..c360ddb --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,49 @@ +# PDF Engine static analysis + naming enforcement. +# Naming rules mirror the engineering blueprint section 16.1: +# PascalCase -> types, camelCase -> functions, snake_case -> file names. +# clang-tidy enforces identifier casing below; file-name casing is a convention +# checked in code review. +--- +Checks: > + -*, + bugprone-*, + performance-*, + modernize-*, + readability-identifier-naming, + readability-redundant-*, + cppcoreguidelines-pro-type-member-init, + -modernize-use-trailing-return-type, + -bugprone-easily-swappable-parameters + +WarningsAsErrors: '' +HeaderFilterRegex: 'engine/(include|src)/.*' + +CheckOptions: + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.StructCase + value: CamelCase + - key: readability-identifier-naming.EnumCase + value: CamelCase + - key: readability-identifier-naming.TypeAliasCase + value: CamelCase + - key: readability-identifier-naming.FunctionCase + value: camelBack + - key: readability-identifier-naming.MethodCase + value: camelBack + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.ParameterCase + value: lower_case + - key: readability-identifier-naming.PrivateMemberCase + value: lower_case + - key: readability-identifier-naming.PrivateMemberSuffix + value: '_' + - key: readability-identifier-naming.ConstantCase + value: lower_case + - key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE + - key: readability-identifier-naming.EnumConstantCase + value: CamelCase diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c5df56b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,30 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space + +# C++ engine + CMake +[*.{c,cc,cpp,h,hpp}] +indent_size = 4 + +[{CMakeLists.txt,*.cmake}] +indent_size = 2 + +# Web / config +[*.{ts,tsx,js,jsx,json,yml,yaml}] +indent_size = 2 + +# Python gateway (PEP 8) +[*.py] +indent_size = 4 + +# Windows scripts keep CRLF +[*.{ps1,bat,cmd}] +end_of_line = crlf + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7061f77 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,39 @@ +# Normalize line endings: let git decide on commit, check out native. +* text=auto + +# Scripts that MUST keep a specific ending regardless of platform. +*.sh text eol=lf +*.bash text eol=lf +*.ps1 text eol=crlf +*.bat text eol=crlf +*.cmd text eol=crlf + +# Source files - always LF in the repo. +*.c text eol=lf +*.cc text eol=lf +*.cpp text eol=lf +*.h text eol=lf +*.hpp text eol=lf +*.cmake text eol=lf +*.py text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.json text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.md text eol=lf +CMakeLists.txt text eol=lf + +# Binary assets - never touch. +*.pdf binary +*.png binary +*.jpg binary +*.webp binary +*.ttf binary +*.otf binary +*.wasm binary +*.lib binary +*.a binary +*.dll binary +*.so binary +*.dylib binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8a66881 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,106 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + # --------------------------------------------------------------------------- + # Static checks: Rule R2 boundary + formatting. Fast, gates the build matrix. + # --------------------------------------------------------------------------- + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Rule R2 — PDFium boundary check + run: bash scripts/check_pdfium_boundary.sh + + # Pinned so CI matches the version the tree was formatted with — clang-format + # output drifts between releases, and an unpinned runner version would cause + # spurious lint failures. + - 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 + test on all three platforms (Gate G0: compiles clean everywhere). + # --------------------------------------------------------------------------- + build: + needs: lint + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + preset: linux-debug + - os: macos-latest + preset: macos-debug + - os: windows-latest + preset: windows-debug + runs-on: ${{ matrix.os }} + 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 + + # GitHub-hosted runners ship vcpkg preinstalled. VCPKG_INSTALLATION_ROOT is + # a runner OS env var, so it must be promoted to GITHUB_ENV here — it is + # NOT visible via the ${{ env.* }} context in a job-level env: block. + - name: Locate vcpkg + shell: bash + run: | + echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> "$GITHUB_ENV" + # The runner's preinstalled vcpkg may predate our pinned builtin-baseline; + # fetch so manifest resolution can find that commit's versions tree. + 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 }}- + + # vcpkg.json ships without a builtin-baseline; the local bootstrap script + # normally pins it. In CI we pin it on the fly (idempotent) so the + # CMakeLists dependency-pinning guard is satisfied. + - 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 }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39fcb9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# Build output +/build/ +/out/ +**/build/ +**/cmake-build-*/ +CMakeUserPresets.json + +# vcpkg +/vcpkg_installed/ +**/vcpkg_installed/ +/vcpkg/ +# vcpkg binary cache (CI sets VCPKG_DEFAULT_BINARY_CACHE here; may appear locally too) +/.vcpkg-cache/ + +# CMake compile database — lives in the build dir, but tooling often drops a copy +# or symlink at the repo root. +/compile_commands.json + +# PDFium from-source build (depot_tools / GN / Ninja) +/third_party/pdfium/depot_tools/ +/third_party/pdfium/checkout/ +/third_party/pdfium/install/ +/third_party/pdfium/.gclient* + +# IDE / editor +/.vs/ +/.vscode/ +!/.vscode/extensions.json +!/.vscode/settings.shared.json +/.idea/ +*.user +*.suo + +# OS +Thumbs.db +.DS_Store +desktop.ini + +# Office lock files (the "~$" prefix Word/Excel/PowerPoint write while a doc is open) +~$* + +# Python (gateway) +__pycache__/ +*.pyc +.venv/ +/gateway/.venv/ + +# Node (frontend) +node_modules/ +/frontend/dist/ +/frontend/.vite/ + +# WASM artifacts +*.wasm +*.wasm.map + +# Logs / misc +*.log + +PDF Editor Timeline.xlsx \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2116882 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,88 @@ +cmake_minimum_required(VERSION 3.25) + +# --------------------------------------------------------------------------- +# Dependency-pinning guard (Rule: pin all dependency versions on Day 1). +# We refuse to configure until vcpkg.json carries a 'builtin-baseline', which +# scripts/bootstrap adds via `vcpkg x-update-baseline --add-initial-baseline`. +# --------------------------------------------------------------------------- +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg.json" _pdfengine_vcpkg_json) +string(JSON _pdfengine_baseline ERROR_VARIABLE _pdfengine_baseline_err + GET "${_pdfengine_vcpkg_json}" "builtin-baseline") +if(_pdfengine_baseline_err OR NOT _pdfengine_baseline) + message(FATAL_ERROR + "vcpkg.json has no 'builtin-baseline' — the dependency registry is not " + "pinned.\n" + "Run the bootstrap script first:\n" + " Windows: pwsh scripts/bootstrap.ps1\n" + " Unix: ./scripts/bootstrap.sh\n" + "It installs vcpkg and runs 'vcpkg x-update-baseline --add-initial-baseline'.") +endif() + +project(PdfEngine + VERSION 0.1.0 + DESCRIPTION "Cross-platform PDF rendering and editing SDK" + HOMEPAGE_URL "https://example.invalid/pdf-engine" + LANGUAGES CXX) + +# --------------------------------------------------------------------------- +# Global C++ settings — C++23, no compiler extensions (engine blueprint §1.3). +# --------------------------------------------------------------------------- +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# compile_commands.json — consumed by clang-tidy and editors. +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Single output dirs so test binaries find imported shared libs at runtime. +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +# Only the top-level project drives testing/install defaults. +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) + message(FATAL_ERROR "In-source builds are not allowed. Use a preset: " + "cmake --preset -debug") +endif() + +# --------------------------------------------------------------------------- +# Build options. +# --------------------------------------------------------------------------- +option(PDFENGINE_BUILD_TESTS "Build engine unit/smoke tests" ON) +option(PDFENGINE_ENABLE_SANITIZERS "Build with AddressSanitizer/UBSan" OFF) +option(PDFENGINE_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF) +option(PDFENGINE_WITH_PDFIUM "Link the PDFium static lib (build it first)" OFF) + +include(CompilerWarnings) +include(Sanitizers) +include(pdfium) # defines pdfium::pdfium when PDFENGINE_WITH_PDFIUM is ON + +# --------------------------------------------------------------------------- +# Third-party dependencies (resolved by vcpkg via the manifest). +# --------------------------------------------------------------------------- +find_package(freetype CONFIG REQUIRED) +find_package(harfbuzz CONFIG REQUIRED) +find_package(spdlog CONFIG REQUIRED) + +if(PDFENGINE_BUILD_TESTS) + find_package(GTest CONFIG REQUIRED) + enable_testing() + include(GoogleTest) +endif() + +# --------------------------------------------------------------------------- +# Subprojects. +# --------------------------------------------------------------------------- +add_subdirectory(engine) + +# bindings/, gateway/, frontend/, wasm/ are placeholders in Phase 0 and are not +# wired into the build yet. + +message(STATUS "PdfEngine ${PROJECT_VERSION} configured") +message(STATUS " C++ standard ......... ${CMAKE_CXX_STANDARD}") +message(STATUS " Build tests .......... ${PDFENGINE_BUILD_TESTS}") +message(STATUS " Sanitizers ........... ${PDFENGINE_ENABLE_SANITIZERS}") +message(STATUS " Warnings as errors ... ${PDFENGINE_WARNINGS_AS_ERRORS}") +message(STATUS " Link PDFium .......... ${PDFENGINE_WITH_PDFIUM}") diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..7781da9 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,141 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "installDir": "${sourceDir}/out/install/${presetName}", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "PDFENGINE_BUILD_TESTS": "ON" + } + }, + { + "name": "debug", + "hidden": true, + "inherits": "base", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "release", + "hidden": true, + "inherits": "base", + "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } + }, + { + "name": "asan", + "hidden": true, + "inherits": "debug", + "cacheVariables": { "PDFENGINE_ENABLE_SANITIZERS": "ON" } + }, + + { + "name": "windows-base", + "hidden": true, + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, + "architecture": { "value": "x64", "strategy": "external" }, + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "x64-windows-static", + "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded$<$:Debug>" + } + }, + { + "name": "windows-debug", + "displayName": "Windows • Debug", + "inherits": ["debug", "windows-base"] + }, + { + "name": "windows-release", + "displayName": "Windows • Release", + "inherits": ["release", "windows-base"] + }, + { + "name": "windows-asan", + "displayName": "Windows • Debug + ASan", + "inherits": ["asan", "windows-base"] + }, + + { + "name": "linux-debug", + "displayName": "Linux • Debug", + "inherits": "debug", + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" } + }, + { + "name": "linux-release", + "displayName": "Linux • Release", + "inherits": "release", + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" } + }, + { + "name": "linux-asan", + "displayName": "Linux • Debug + ASan/UBSan", + "inherits": "asan", + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" } + }, + + { + "name": "macos-debug", + "displayName": "macOS • Debug", + "inherits": "debug", + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" } + }, + { + "name": "macos-release", + "displayName": "macOS • Release", + "inherits": "release", + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" } + }, + { + "name": "macos-asan", + "displayName": "macOS • Debug + ASan/UBSan", + "inherits": "asan", + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" } + }, + + { + "name": "wasm", + "displayName": "WASM • Emscripten (Phase 0 stub — validation only)", + "generator": "Ninja", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "toolchainFile": "${sourceDir}/cmake/toolchains/wasm.cmake", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "PDFENGINE_BUILD_TESTS": "OFF", + "PDFENGINE_WITH_PDFIUM": "OFF" + } + } + ], + + "buildPresets": [ + { "name": "windows-debug", "configurePreset": "windows-debug" }, + { "name": "windows-release", "configurePreset": "windows-release" }, + { "name": "windows-asan", "configurePreset": "windows-asan" }, + { "name": "linux-debug", "configurePreset": "linux-debug" }, + { "name": "linux-release", "configurePreset": "linux-release" }, + { "name": "linux-asan", "configurePreset": "linux-asan" }, + { "name": "macos-debug", "configurePreset": "macos-debug" }, + { "name": "macos-release", "configurePreset": "macos-release" }, + { "name": "macos-asan", "configurePreset": "macos-asan" }, + { "name": "wasm", "configurePreset": "wasm" } + ], + + "testPresets": [ + { + "name": "common", + "hidden": true, + "output": { "outputOnFailure": true }, + "execution": { "noTestsAction": "error", "stopOnFailure": false } + }, + { "name": "windows-debug", "inherits": "common", "configurePreset": "windows-debug" }, + { "name": "windows-asan", "inherits": "common", "configurePreset": "windows-asan" }, + { "name": "linux-debug", "inherits": "common", "configurePreset": "linux-debug" }, + { "name": "linux-asan", "inherits": "common", "configurePreset": "linux-asan" }, + { "name": "macos-debug", "inherits": "common", "configurePreset": "macos-debug" }, + { "name": "macos-asan", "inherits": "common", "configurePreset": "macos-asan" } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae84232 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# PDF Engine + +Cross-platform, high-performance, license-safe PDF rendering and editing SDK. +A single C++ core (PDFium + Skia + FreeType + HarfBuzz) drives native desktop, +server-side, and browser (WASM) rendering, with a FastAPI gateway and a +React + TypeScript viewer on top. + +> **Status:** Phase 0 — Infrastructure. The stack must compile clean on +> Linux, macOS, and Windows before any feature work begins. See +> [`docs/phase0.md`](docs/phase0.md). + +## Repository layout + +| Path | Purpose | +|------------------|----------------------------------------------------------------------| +| `engine/` | C++23 PDF SDK core. The only code that links PDFium/Skia/FreeType. | +| `engine/src/parser/` | **Only** place raw `FPDF_*` PDFium APIs may be used (Rule R2). | +| `bindings/` | Language bindings — C ABI (.NET/ctypes) and pybind11 (Python). | +| `gateway/` | FastAPI service: auth, metadata, storage, job queue. *(placeholder)* | +| `frontend/` | React + TypeScript layered PDF viewer. *(placeholder)* | +| `wasm/` | Emscripten build of the engine for in-browser rendering. *(placeholder)* | +| `third_party/` | Vendored dependencies built from source (PDFium via depot_tools/GN). | +| `cmake/` | Reusable CMake modules (`pdfium.cmake`, warnings, sanitizers). | +| `corpus/` | Test PDF corpus for rendering/regression gates. | +| `scripts/` | Bootstrap, lint, and developer tooling. | +| `docs/` | Engineering docs and phase plans. | + +## Prerequisites + +- **CMake** >= 3.25 and **Ninja** +- A **C++23** compiler: MSVC 19.36+ (VS 2022 17.6+ **or** VS 2026 / Build + Tools 2026 — IDE not required), GCC 13+, or Clang 16+ +- **vcpkg** (set `VCPKG_ROOT`; `scripts/bootstrap` can install it). On + Windows, `setx VCPKG_ROOT ...` does not affect already-open shells — + set `$env:VCPKG_ROOT` in that shell or open a new terminal. +- **clang-format 22.1.5** (CI is pinned to this exact version) — + `pip install clang-format==22.1.5` on Windows +- **Python 3.11+** and **Node 20+** (for gateway/frontend, later phases) +- For building PDFium from source: **depot_tools** (handled by + `third_party/pdfium/build_pdfium.*` — do **not** add it to your + persistent `PATH`; its `ninja.bat` shadows real Ninja and breaks the + engine build, see [`docs/phase0.md`](docs/phase0.md)) + +## Quick start + +```sh +# 1. One-time setup: checks tools, installs vcpkg, pins the dependency baseline. +# Windows: pwsh scripts/bootstrap.ps1 +# Unix: ./scripts/bootstrap.sh + +# 2. Build PDFium from source (slow, one-time — see third_party/pdfium/README.md). +# Windows: pwsh third_party/pdfium/build_pdfium.ps1 +# Unix: ./third_party/pdfium/build_pdfium.sh + +# 3. Configure + build + test via CMake presets. +cmake --preset windows-debug # or linux-debug / macos-debug +cmake --build --preset windows-debug +ctest --preset windows-debug +``` + +> **Windows + PDFium:** the engine-linked-against-PDFium build is +> RelWithDebInfo + static CRT (not the Debug preset). Drop the +> `win-local-pdfium` preset into a local `CMakeUserPresets.json` and use +> that — see [`docs/phase0.md`](docs/phase0.md#windows--pdfium) for the +> exact JSON and the reason (CRT alignment). + +> **OneDrive note:** this checkout lives in a OneDrive-synced folder whose +> path also contains a space. On **Windows** this is not optional — put +> your build directory outside OneDrive on a space-free path +> (e.g. `C:\Users\\pdfeng-build\...`), or vcpkg/meson and depot_tools +> will fail. On Linux/macOS it is just a sync nuisance — exclude `out/` +> and `vcpkg/` from sync, or pause sync while building. See +> [`docs/phase0.md`](docs/phase0.md#onedrive-warning) for details. + +## Engineering rules (non-negotiable) + +- **R1** PDFium stays the parser core — no custom parser. +- **R2** Only `engine/src/parser/` may call raw `FPDF_*` APIs. CI enforces this. +- **R3** No custom graphics interpreter in Phase 1/2 — use PDFium's renderer. +- **R4** Saving is incremental (append-only xref) by default. +- **R5** WASM never blocks shipping — server-side render is the fallback. + +Dependency versions are **pinned** (vcpkg baseline + a pinned PDFium ref). +Never track rolling `HEAD`. diff --git a/bindings/README.md b/bindings/README.md new file mode 100644 index 0000000..86e781d --- /dev/null +++ b/bindings/README.md @@ -0,0 +1,13 @@ +# Bindings — placeholder + +Language bindings over the C++ engine. Not implemented yet. + +- `c_abi/` — stable C ABI (`int error_code` returns, opaque handles). Consumed + by .NET and Python `ctypes`. This ABI is **frozen in Phase 0** alongside the + `PdfDocument` / `PdfPage` interface contracts (Gate G0b) and must not change + without a version bump. Phase 3 ships `.so` / `.dylib` / `.dll` packages from it. +- `python/` — **pybind11** module used by the FastAPI gateway. Exposes + `render_page()`, `apply_edits()`, `extract_text()`. + +Internal engine code uses `std::expected`; the C ABI surface translates +that to `int error_code` (engine blueprint §16.2). diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake new file mode 100644 index 0000000..c4a7871 --- /dev/null +++ b/cmake/CompilerWarnings.cmake @@ -0,0 +1,52 @@ +# Reusable warning configuration. +# Usage: pdfengine_set_warnings( [PRIVATE|INTERFACE|PUBLIC]) + +function(pdfengine_set_warnings target) + set(_scope PRIVATE) + if(ARGV1) + set(_scope ${ARGV1}) + endif() + + set(_msvc_warnings + /W4 + /permissive- # strict standard conformance + /w14242 /w14254 /w14263 /w14265 /w14287 + /w14296 /w14311 /w14545 /w14546 /w14547 + /w14549 /w14555 /w14619 /w14640 /w14826 + /w14905 /w14906 /w14928 + /EHsc + ) + + set(_gnu_clang_warnings + -Wall + -Wextra + -Wpedantic + -Wshadow + -Wnon-virtual-dtor + -Wcast-align + -Wunused + -Woverloaded-virtual + -Wconversion + -Wsign-conversion + -Wnull-dereference + -Wdouble-promotion + -Wformat=2 + -Wimplicit-fallthrough + ) + + if(MSVC) + set(_warnings ${_msvc_warnings}) + else() + set(_warnings ${_gnu_clang_warnings}) + endif() + + if(PDFENGINE_WARNINGS_AS_ERRORS) + if(MSVC) + list(APPEND _warnings /WX) + else() + list(APPEND _warnings -Werror) + endif() + endif() + + target_compile_options(${target} ${_scope} ${_warnings}) +endfunction() diff --git a/cmake/Sanitizers.cmake b/cmake/Sanitizers.cmake new file mode 100644 index 0000000..278395c --- /dev/null +++ b/cmake/Sanitizers.cmake @@ -0,0 +1,28 @@ +# AddressSanitizer / UndefinedBehaviorSanitizer wiring. +# Enabled per build via -DPDFENGINE_ENABLE_SANITIZERS=ON (see the *-asan presets). +# Usage: pdfengine_enable_sanitizers() +# +# Coverage by platform: +# MSVC -> ASan only (/fsanitize=address). UBSan/TSan unsupported. +# GCC/Clang -> ASan + UBSan. +# TSan is intentionally not wired here yet; it conflicts with ASan and is only +# relevant once the engine is multithreaded (Phase 3+). + +function(pdfengine_enable_sanitizers target) + if(NOT PDFENGINE_ENABLE_SANITIZERS) + return() + endif() + + if(MSVC) + message(STATUS "Sanitizers: AddressSanitizer enabled for '${target}' (MSVC)") + target_compile_options(${target} PRIVATE /fsanitize=address) + # ASan on MSVC is incompatible with the /RTC runtime checks and incremental link. + target_compile_options(${target} PRIVATE /Zi) + target_link_options(${target} PRIVATE /INCREMENTAL:NO) + else() + message(STATUS "Sanitizers: Address + UndefinedBehavior enabled for '${target}'") + set(_flags -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all) + target_compile_options(${target} PRIVATE ${_flags} -g) + target_link_options(${target} PRIVATE ${_flags}) + endif() +endfunction() diff --git a/cmake/pdfium.cmake b/cmake/pdfium.cmake new file mode 100644 index 0000000..440a391 --- /dev/null +++ b/cmake/pdfium.cmake @@ -0,0 +1,63 @@ +# PDFium integration. +# +# PDFium is NOT a vcpkg package — it is built from source via depot_tools/GN/Ninja +# (see third_party/pdfium/). That build installs into: +# +# third_party/pdfium/install/ +# include/ public PDFium headers (fpdfview.h, fpdf_*.h, ...) +# lib/ the static library (pdfium.lib / libpdfium.a) +# +# This module turns that install tree into an imported target: pdfium::pdfium +# +# Behaviour: +# PDFENGINE_WITH_PDFIUM = OFF (default) +# No target is created. The engine compiles with PDFium code paths +# #ifdef-ed out. This keeps Phase 0 unblocked before PDFium is built. +# PDFENGINE_WITH_PDFIUM = ON +# The install tree MUST exist; otherwise this is a hard error directing +# the developer to the build script. + +if(NOT PDFENGINE_WITH_PDFIUM) + message(STATUS "PDFium: disabled (PDFENGINE_WITH_PDFIUM=OFF). " + "Engine builds without raw PDFium linkage.") + return() +endif() + +set(PDFIUM_INSTALL_DIR "${CMAKE_SOURCE_DIR}/third_party/pdfium/install" + CACHE PATH "Root of the PDFium install tree produced by build_pdfium.*") + +find_path(PDFIUM_INCLUDE_DIR + NAMES fpdfview.h + PATHS "${PDFIUM_INSTALL_DIR}/include" + NO_DEFAULT_PATH) + +find_library(PDFIUM_LIBRARY + NAMES pdfium libpdfium + PATHS "${PDFIUM_INSTALL_DIR}/lib" + NO_DEFAULT_PATH) + +if(NOT PDFIUM_INCLUDE_DIR OR NOT PDFIUM_LIBRARY) + message(FATAL_ERROR + "PDFENGINE_WITH_PDFIUM=ON but no PDFium install tree was found under:\n" + " ${PDFIUM_INSTALL_DIR}\n" + "Build PDFium first (one-time, slow):\n" + " Windows: pwsh third_party/pdfium/build_pdfium.ps1\n" + " Unix: ./third_party/pdfium/build_pdfium.sh\n" + "See third_party/pdfium/README.md.") +endif() + +add_library(pdfium::pdfium STATIC IMPORTED GLOBAL) +set_target_properties(pdfium::pdfium PROPERTIES + IMPORTED_LOCATION "${PDFIUM_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${PDFIUM_INCLUDE_DIR}") + +# PDFium is a C++ static lib; consumers on Linux also need the C++ runtime and +# pthreads. These are no-ops where irrelevant. +if(UNIX AND NOT APPLE) + set_property(TARGET pdfium::pdfium APPEND PROPERTY + INTERFACE_LINK_LIBRARIES pthread dl) +endif() + +message(STATUS "PDFium: found") +message(STATUS " include .. ${PDFIUM_INCLUDE_DIR}") +message(STATUS " library .. ${PDFIUM_LIBRARY}") diff --git a/cmake/toolchains/wasm.cmake b/cmake/toolchains/wasm.cmake new file mode 100644 index 0000000..60265a5 --- /dev/null +++ b/cmake/toolchains/wasm.cmake @@ -0,0 +1,31 @@ +# WebAssembly (Emscripten) toolchain hook — STUB for Phase 0. +# +# The Phase 0 "WASM hello-world" task is validation-only and must NOT block +# Phase 1 (Rule R5). This file exists so the integration point is real; the +# full WASM build (PDFium + Skia + FreeType + HarfBuzz compiled together) is +# Phase 2 work. +# +# It chain-loads the real Emscripten toolchain from the active EMSDK, then lets +# vcpkg layer on top. Used by the `wasm` configure preset. + +if(NOT DEFINED ENV{EMSDK}) + message(FATAL_ERROR + "WASM build requested but EMSDK is not set in the environment.\n" + "Install and activate the Emscripten SDK:\n" + " git clone https://github.com/emscripten-core/emsdk\n" + " ./emsdk install latest && ./emsdk activate latest\n" + " source ./emsdk_env.sh (or emsdk_env.bat on Windows)\n" + "Then re-run: cmake --preset wasm") +endif() + +set(_emscripten_toolchain + "$ENV{EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake") + +if(NOT EXISTS "${_emscripten_toolchain}") + message(FATAL_ERROR "Emscripten toolchain not found at: ${_emscripten_toolchain}") +endif() + +include("${_emscripten_toolchain}") + +# Engine blueprint §6.3: large PDFs can exceed the default WASM heap. +set(CMAKE_EXE_LINKER_FLAGS_INIT "-sALLOW_MEMORY_GROWTH=1") diff --git a/corpus/README.md b/corpus/README.md new file mode 100644 index 0000000..cdfab1f --- /dev/null +++ b/corpus/README.md @@ -0,0 +1,25 @@ +# Test PDF corpus + +Reference PDFs used by rendering, text-extraction, and regression gates. + +**Do not commit large or confidential PDFs here.** Binary PDFs are tracked by +git but the corpus is expected to grow — once it does, move it to Git LFS or an +external store and keep only a manifest here. + +Structure (populated during Phase 1): + +``` +corpus/ + basic/ # clean, simple PDFs — smoke tests + fonts/ # embedded fonts, CID/CJK, Arabic/RTL + edge-cases/ # broken xrefs, scanned, PDF/A, encrypted + golden/ # reference PNG renders for pixel-diff tests + manifest.json # per-file metadata + expected results +``` + +**Risk-register rule:** every client PDF that surfaces a bug gets added to the +corpus immediately. + +Gates that consume this corpus: +- **G1/G2/G3** (Phase 1): 30-PDF corpus, render + text extraction. +- **G7** (Phase 3): 500-PDF regression corpus, SSIM > 0.95. diff --git a/docs/phase0.md b/docs/phase0.md new file mode 100644 index 0000000..1ab5fe7 --- /dev/null +++ b/docs/phase0.md @@ -0,0 +1,273 @@ +# Phase 0 — Infrastructure + +> Goal of Phase 0: **make the entire stack compile and run on Linux, macOS, and +> Windows.** No editing, no rendering features. Success = the engine library +> builds clean on all three platforms and the smoke test passes. Phase 1 does +> not begin until Gate G0 and G0b are reached. + +## Phase 0 task board + +| # | Task | Owner | This session | +|---|------|-------|--------------| +| 1 | CMake root + vcpkg + CI/CD pipeline | Dev 1 | **Done — scaffolded** | +| 2 | PDFium build (depot_tools + GN + Ninja) | Dev 1 | Build scripts staged; not yet run | +| 3 | Skia build integration | Dev 2 | Not started | +| 4 | FreeType + HarfBuzz vcpkg integration | Dev 3 | In manifest; wrappers not started | +| 5 | FastAPI service scaffolding | Dev 1 | Placeholder dir only | +| 6 | React + TypeScript frontend scaffolding | Dev 2 | Placeholder dir only | +| 7 | WASM hello-world build (Emscripten) | Dev 1 | Toolchain hook + preset stubbed | +| 8 | Frozen interface contracts (Gate G0b) | All | Placeholder header; **not designed** | + +This session delivered **Task 1 in full** plus the repository structure for +everything else. Scope was deliberately limited to the build pipeline — see +"What is intentionally not done" below. + +## What was built this session + +``` +Code/ +├── CMakeLists.txt root build; refuses to configure without a pinned baseline +├── CMakePresets.json debug/release/asan per platform + a wasm stub +├── vcpkg.json dependency manifest (freetype, harfbuzz, spdlog, gtest) +├── .clang-format .clang-tidy style + naming rules from blueprint §16.1 +├── .gitignore .gitattributes .editorconfig +├── cmake/ +│ ├── pdfium.cmake turns the PDFium install tree into pdfium::pdfium +│ ├── CompilerWarnings.cmake high warning levels per compiler +│ ├── Sanitizers.cmake ASan/UBSan wiring +│ └── toolchains/wasm.cmake Emscripten hook (stub) +├── engine/ +│ ├── CMakeLists.txt +│ ├── include/pdfengine/ public headers (version, umbrella, pdf_document placeholder) +│ ├── src/core/ engine_info.cpp — version/build introspection +│ ├── src/parser/ pdfium_loader — the ONLY FPDF_-allowed dir (Rule R2) +│ └── tests/ gtest smoke test backing Gate G0 +├── third_party/pdfium/ from-source build scripts + pinned-ref file + args.gn +├── scripts/ +│ ├── bootstrap.{sh,ps1} installs vcpkg, pins the dependency baseline +│ └── check_pdfium_boundary.{sh,ps1} Rule R2 enforcement +├── .github/workflows/ci.yml Linux/macOS/Windows build matrix + lint jobs +├── bindings/ gateway/ frontend/ wasm/ corpus/ placeholder dirs with READMEs +└── docs/phase0.md this file +``` + +## How to build (developer onboarding) + +### Prerequisites + +| Tool | Version | Notes | +|------|---------|-------| +| CMake | >= 3.25 | presets v6 | +| Ninja | any recent | the only generator used | +| C++ compiler | MSVC 19.36+ / GCC 13+ / Clang 16+ | needs C++23 | +| vcpkg | — | `scripts/bootstrap` installs it if `VCPKG_ROOT` is unset | +| Git | any recent | | +| clang-format | 22.1.5 | not natively packaged on Windows — `pip install clang-format==22.1.5` (CI is pinned to this exact version) | + +> **Windows:** either Visual Studio 2022/2026 (with the *"Desktop development +> with C++"* workload) or **Build Tools 2026** (no IDE — installer product +> `Microsoft.VisualStudio.Product.BuildTools`) is supported. `cl.exe` is not on +> `PATH` by default — run builds from a *Developer PowerShell* or import +> `VC\Auxiliary\Build\vcvars64.bat` first. Build Tools is not a default +> `vswhere` product, so detection scripts need `vswhere -products *` (the +> PDFium build script already does this). CI uses `ilammy/msvc-dev-cmd`. +> +> `VCPKG_ROOT` set via `setx` (or the bootstrap script's persistent install) +> does **not** propagate into already-open shells — set `$env:VCPKG_ROOT` +> explicitly in that shell, or open a new terminal. PowerShell 5.1 is fine; +> `pwsh` (7+) is not required by anything in this repo. + +### Steps + +```sh +# 1. One-time setup — checks tools, installs vcpkg, pins the dependency baseline. +pwsh scripts/bootstrap.ps1 # Windows +./scripts/bootstrap.sh # Linux / macOS + +# 2. Configure + build + test. +cmake --preset windows-debug # linux-debug | macos-debug +cmake --build --preset windows-debug +ctest --preset windows-debug +``` + +The first configure compiles the vcpkg dependencies (freetype, harfbuzz, +spdlog, gtest) — slow once, cached after. + +### Building with PDFium + +PDFium is built separately from source (Task 2): + +```sh +# Pin the revision first — edit third_party/pdfium/pdfium.pinned (see its README). +pwsh third_party/pdfium/build_pdfium.ps1 # or .sh +``` + +Until then the engine builds with PDFium code paths `#ifdef`-ed out, which is +the correct Phase 0 default — it keeps the pipeline green while Task 2 runs. + +On Linux/macOS, enabling PDFium is a single flag added to a debug build: + +```sh +cmake --preset linux-debug -DPDFENGINE_WITH_PDFIUM=ON +``` + +On Windows there is more to it — see the next section. + +### Windows + PDFium + +PDFium's static-lib GN build forces the static CRT (`/MT`, `is_debug=false`) +and offers no knob for "static lib + dynamic CRT". The engine, vcpkg deps, +and PDFium must therefore all use the same static CRT, or the link dies with +`LNK2038: 'RuntimeLibrary' mismatch`. The repo is wired for this ("Option A, +all static CRT, release-flavored"): + +- The hidden `windows-base` preset in [CMakePresets.json](../CMakePresets.json) + sets `VCPKG_TARGET_TRIPLET=x64-windows-static` (vcpkg deps as static lib + + static CRT — first configure rebuilds them, ~7 min one-time) and + `CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded$<$:Debug>`. +- PDFium's own [args.gn](../third_party/pdfium/args.gn) keeps `is_debug=false` + (i.e. `/MT`). +- The PDFium-linked engine build must be **RelWithDebInfo, not Debug** — a + Debug engine is `/MTd` and still mismatches PDFium's `/MT`. The plain + `windows-debug -DPDFENGINE_WITH_PDFIUM=ON` recipe will not link; use the + `win-local-pdfium` user preset below. + +#### `win-local-pdfium` user preset + +`CMakeUserPresets.json` is git-ignored (build dirs are per-developer and live +outside OneDrive). Drop this preset in at `Code/CMakeUserPresets.json`, with +your own username in `binaryDir`: + +```json +{ + "version": 6, + "configurePresets": [ + { + "name": "win-local-pdfium", + "inherits": "windows-release", + "binaryDir": "C:/Users//pdfeng-build/win-local-pdfium", + "cacheVariables": { "PDFENGINE_WITH_PDFIUM": "ON" } + } + ], + "buildPresets": [ + { "name": "win-local-pdfium", "configurePreset": "win-local-pdfium" } + ], + "testPresets": [ + { "name": "win-local-pdfium", "inherits": "common", "configurePreset": "win-local-pdfium" } + ] +} +``` + +Then, from a shell with `vcvars64.bat` imported: + +```powershell +cmake --preset win-local-pdfium +cmake --build --preset win-local-pdfium +ctest --preset win-local-pdfium +``` + +Success looks like `pdfengine_smoke.exe` linking cleanly and logging +`pdfium=on`. The build dir is intentionally outside OneDrive and on a +space-free path — see the OneDrive section below for why. + +#### Gotcha: depot_tools shadows `ninja` + +The PDFium build adds `depot_tools` to `PATH` and the depot_tools `ninja` / +`ninja.bat` are not real Ninja — they fail with: + +``` +Running ninja --version failed with unknown error +... CMAKE_CXX_COMPILER not set, after EnableLanguage +``` + +If `depot_tools` ended up on your persistent `PATH`, CMake will pick its +broken `ninja` for the engine build. Two fixes: + +- **Short-term:** point CMake at the real Ninja explicitly, e.g. + `cmake --preset win-local-pdfium -D CMAKE_MAKE_PROGRAM=C:/path/to/real/ninja.exe`. + This caches, so only the first configure needs the flag. +- **Long-term (recommended):** keep `depot_tools` **off** the persistent + `PATH`. `third_party/pdfium/build_pdfium.ps1` already prepends it + per-run, so the PDFium build still works. + +## Dependency pinning + +The blueprint rule is *"pin all dependency versions on Day 1, never track +rolling HEAD."* Two mechanisms: + +- **vcpkg deps** — `scripts/bootstrap` runs `vcpkg x-update-baseline + --add-initial-baseline`, which writes a `builtin-baseline` commit into + `vcpkg.json`. That pins the entire dependency registry to one commit. The + root `CMakeLists.txt` **refuses to configure** until this is present. + → **The first commit to the repo must include the bootstrapped `vcpkg.json`**, + otherwise CI fails at the configure step (by design). +- **PDFium** — `third_party/pdfium/pdfium.pinned` holds an exact commit SHA. + The build script refuses to run while it is the placeholder. Rebases are a + deliberate, scheduled (quarterly) action. + +## Engineering conventions + +- **Naming** (`.clang-tidy`): `CamelCase` types, `camelBack` functions, + `snake_case` file names. +- **Errors**: `std::expected` internally; `int error_code` across the C ABI. +- **Branches**: `main`, `develop`, `feature/*`, `release/*`. +- **Rule R2**: only `engine/src/parser/` may use raw `FPDF_*` APIs — + enforced by `scripts/check_pdfium_boundary.*` locally and in CI. + +## What is intentionally NOT done this session + +- **PDFium is not actually built** — scripts are staged; the revision needs to + be pinned and the (long) build run as the second half of Task 2. +- **No interface contracts** — `engine/include/pdfengine/pdf_document.hpp` is a + placeholder. `PdfDocument` / `PdfPage` are designed and frozen at **Gate G0b** + in an all-devs session; nothing proceeds until it is signed off. +- **Skia / FreeType / HarfBuzz wrappers** — FreeType + HarfBuzz are in the vcpkg + manifest and link-tested, but the actual wrappers are Dev 3's Phase 0/1 work. + Skia is Dev 2's task. +- **FastAPI / React / WASM** — placeholder directories only. WASM has a toolchain + hook and preset stub so the integration point exists (Rule R5: WASM never + blocks shipping). + +## Gates ahead + +| Gate | Criterion | Unblocks | +|------|-----------|----------| +| **G0** | All platforms build clean; PDFium + Skia + FreeType + HarfBuzz compile | Phase 1 | +| **G0b** | `PdfDocument` / `PdfPage` contracts locked by all 3 devs | Coding begins | + +The CI `build` matrix is the automated half of G0. The smoke test +(`engine/tests/smoke_test.cpp`) is what it runs. + +## OneDrive warning + +This checkout lives under `OneDrive\Work\Maskan\PDF Editor\Code`. The path is +both OneDrive-synced **and** contains a space (`PDF Editor`). Both bite C++ +builds: + +1. **Sync churn** — build output is thousands of `.obj`/`.o` files. `out/` is + git-ignored, but OneDrive still tries to upload it. +2. **File locks** — OneDrive can hold a handle on a file mid-sync, causing + intermittent "permission denied" errors during compile or link. +3. **Spaces in build paths break tooling** — vcpkg/meson (harfbuzz) fail + with `LNK1181` when `vcpkg_installed` is under the spaced path, and + `depot_tools` / GN / Ninja `.bat` wrappers cannot handle a space in their + own path at all. + +**On Windows, building inside the repo path is not viable** — put the build +dir outside OneDrive on a space-free path. The `win-local-pdfium` preset +above already does this (`C:/Users//pdfeng-build/...`); do the same for +any non-PDFium preset by overriding `binaryDir`: + +```powershell +cmake --preset windows-release -B C:/Users//pdfeng-build/windows-release +``` + +The PDFium build is even stricter: `third_party/pdfium/build_pdfium.ps1` +takes a `PDFIUM_BUILD_ROOT` env var and hard-errors if it contains a space. +Use e.g. `C:\Users\\pdfium-build`. + +On Linux/macOS the OneDrive path is still a sync nuisance but the toolchain +itself is fine. Either point the build dir outside OneDrive, or exclude +`out/` and `vcpkg/` from sync, or pause sync while building. + +Long term, the repository should live outside OneDrive on a real Git remote. diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt new file mode 100644 index 0000000..2734760 --- /dev/null +++ b/engine/CMakeLists.txt @@ -0,0 +1,51 @@ +# pdfengine — the C++23 PDF SDK core. +# +# Phase 0 scope: a minimal-but-real static library that compiles, links against +# its vcpkg dependencies, and exposes version/build introspection. Feature +# modules (parser, render, text, core) are filled in from Phase 1 onward. + +# Generate the version header from the project version. +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/include/pdfengine/version.hpp.in" + "${CMAKE_CURRENT_BINARY_DIR}/generated/pdfengine/version.hpp" + @ONLY) + +add_library(pdfengine STATIC + src/core/engine_info.cpp + src/parser/pdfium_loader.cpp +) +add_library(pdfengine::pdfengine ALIAS pdfengine) + +target_include_directories(pdfengine + PUBLIC + "$" + "$" + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src" +) + +# Dependencies resolved by vcpkg. +# spdlog - logging, used now (engine blueprint §13.1). +# freetype/harfbuzz - linked now to prove the vcpkg toolchain end to end; +# actually exercised by Dev 3 from Phase 1 onward. +target_link_libraries(pdfengine + PUBLIC + spdlog::spdlog + PRIVATE + freetype + harfbuzz::harfbuzz +) + +# PDFium is optional in Phase 0 (built from source separately). When enabled, +# only this target gets the macro + link — and only src/parser/ uses it (R2). +if(PDFENGINE_WITH_PDFIUM) + target_link_libraries(pdfengine PRIVATE pdfium::pdfium) + target_compile_definitions(pdfengine PRIVATE PDFENGINE_WITH_PDFIUM) +endif() + +pdfengine_set_warnings(pdfengine) +pdfengine_enable_sanitizers(pdfengine) + +if(PDFENGINE_BUILD_TESTS) + add_subdirectory(tests) +endif() diff --git a/engine/include/pdfengine/pdf_document.hpp b/engine/include/pdfengine/pdf_document.hpp new file mode 100644 index 0000000..16d1cfb --- /dev/null +++ b/engine/include/pdfengine/pdf_document.hpp @@ -0,0 +1,26 @@ +// pdfengine — document API. +// +// ┌─────────────────────────────────────────────────────────────────────────┐ +// │ PLACEHOLDER. This header is the deliverable of Phase 0 Gate G0b: │ +// │ "Frozen interface contracts" — the PdfDocument / PdfPage API structs │ +// │ reviewed and locked by all three developers before Phase 1 coding. │ +// │ │ +// │ Do NOT fill in method signatures yet. Nothing proceeds until the │ +// │ contract is signed off (see docs/phase0.md, Gate G0b). │ +// │ │ +// │ Rule R2: only engine/src/parser/ may touch raw FPDF_* PDFium APIs. │ +// │ Everything else in the codebase goes through these types. │ +// └─────────────────────────────────────────────────────────────────────────┘ +#pragma once + +namespace pdfengine { + +// Opaque, owning handle to a parsed PDF document. +// Full definition + API land at Gate G0b. +class PdfDocument; + +// Opaque view onto a single page of a PdfDocument. +// Full definition + API land at Gate G0b. +class PdfPage; + +} // namespace pdfengine diff --git a/engine/include/pdfengine/pdf_engine.hpp b/engine/include/pdfengine/pdf_engine.hpp new file mode 100644 index 0000000..b4e4366 --- /dev/null +++ b/engine/include/pdfengine/pdf_engine.hpp @@ -0,0 +1,25 @@ +// pdfengine — public umbrella header for the PDF SDK core. +// +// Phase 0 surface only: version + build introspection. The real document API +// (PdfDocument / PdfPage) is frozen at Gate G0b and added in Phase 1 — see +// pdf_document.hpp. +#pragma once + +#include +#include + +namespace pdfengine { + +// Human-readable engine version, e.g. "0.1.0". +[[nodiscard]] std::string_view engineVersion() noexcept; + +// One-line build descriptor, e.g. "pdfengine 0.1.0 (pdfium=off)". +[[nodiscard]] std::string_view engineBuildInfo() noexcept; + +// True if this build was compiled and linked against the PDFium parser core. +[[nodiscard]] bool engineHasPdfium() noexcept; + +// Emits engineBuildInfo() through spdlog at info level. +void engineLogBuildInfo(); + +} // namespace pdfengine diff --git a/engine/include/pdfengine/version.hpp.in b/engine/include/pdfengine/version.hpp.in new file mode 100644 index 0000000..0712ef2 --- /dev/null +++ b/engine/include/pdfengine/version.hpp.in @@ -0,0 +1,14 @@ +// Generated by CMake from version.hpp.in — do not edit the generated copy. +#pragma once + +#include + +namespace pdfengine { + +inline constexpr int version_major = @PROJECT_VERSION_MAJOR@; +inline constexpr int version_minor = @PROJECT_VERSION_MINOR@; +inline constexpr int version_patch = @PROJECT_VERSION_PATCH@; + +inline constexpr std::string_view version_string = "@PROJECT_VERSION@"; + +} // namespace pdfengine diff --git a/engine/src/core/engine_info.cpp b/engine/src/core/engine_info.cpp new file mode 100644 index 0000000..1e52f1a --- /dev/null +++ b/engine/src/core/engine_info.cpp @@ -0,0 +1,31 @@ +#include "parser/pdfium_loader.hpp" + +#include +#include +#include + +namespace pdfengine { + +std::string_view engineVersion() noexcept { + return version_string; +} + +bool engineHasPdfium() noexcept { + return parser::pdfiumAvailable(); +} + +std::string_view engineBuildInfo() noexcept { + static const std::string info = [] { + std::string s = "pdfengine "; + s += version_string; + s += parser::pdfiumAvailable() ? " (pdfium=on)" : " (pdfium=off)"; + return s; + }(); + return info; +} + +void engineLogBuildInfo() { + spdlog::info("{}", engineBuildInfo()); +} + +} // namespace pdfengine diff --git a/engine/src/parser/pdfium_loader.cpp b/engine/src/parser/pdfium_loader.cpp new file mode 100644 index 0000000..db5480e --- /dev/null +++ b/engine/src/parser/pdfium_loader.cpp @@ -0,0 +1,31 @@ +#include "parser/pdfium_loader.hpp" + +#ifdef PDFENGINE_WITH_PDFIUM +// The ONLY place in the codebase a raw PDFium header may be included (Rule R2). +#include +#endif + +namespace pdfengine::parser { + +bool pdfiumAvailable() noexcept { +#ifdef PDFENGINE_WITH_PDFIUM + return true; +#else + return false; +#endif +} + +void pdfiumInitLibrary() { +#ifdef PDFENGINE_WITH_PDFIUM + // PDFium expects this once per process before any document is opened. + FPDF_InitLibrary(); +#endif +} + +void pdfiumDestroyLibrary() { +#ifdef PDFENGINE_WITH_PDFIUM + FPDF_DestroyLibrary(); +#endif +} + +} // namespace pdfengine::parser diff --git a/engine/src/parser/pdfium_loader.hpp b/engine/src/parser/pdfium_loader.hpp new file mode 100644 index 0000000..ec9a23a --- /dev/null +++ b/engine/src/parser/pdfium_loader.hpp @@ -0,0 +1,20 @@ +// pdfengine::parser — the PDFium boundary. +// +// Rule R2: this directory (engine/src/parser/) is the ONLY place in the entire +// codebase permitted to include PDFium headers or call raw FPDF_* APIs. CI +// enforces this with scripts/check_pdfium_boundary.* — keep it that way. +#pragma once + +namespace pdfengine::parser { + +// True if the engine was compiled with PDFENGINE_WITH_PDFIUM, i.e. linked +// against the PDFium static library. False in Phase 0 default builds. +[[nodiscard]] bool pdfiumAvailable() noexcept; + +// Global one-time initialisation / teardown of the PDFium library. +// No-ops when built without PDFium. Init must be called before, and destroy +// after, any document parsing. +void pdfiumInitLibrary(); +void pdfiumDestroyLibrary(); + +} // namespace pdfengine::parser diff --git a/engine/tests/CMakeLists.txt b/engine/tests/CMakeLists.txt new file mode 100644 index 0000000..3dfb952 --- /dev/null +++ b/engine/tests/CMakeLists.txt @@ -0,0 +1,18 @@ +# Engine test suite. Phase 0: a single smoke test that backs Gate G0. + +add_executable(pdfengine_smoke + smoke_test.cpp +) + +target_link_libraries(pdfengine_smoke + PRIVATE + pdfengine::pdfengine + GTest::gtest + GTest::gtest_main +) + +pdfengine_set_warnings(pdfengine_smoke) +pdfengine_enable_sanitizers(pdfengine_smoke) + +# Registers each TEST() with CTest so `ctest --preset ...` runs them. +gtest_discover_tests(pdfengine_smoke) diff --git a/engine/tests/smoke_test.cpp b/engine/tests/smoke_test.cpp new file mode 100644 index 0000000..3aa268e --- /dev/null +++ b/engine/tests/smoke_test.cpp @@ -0,0 +1,27 @@ +// Phase 0 smoke test: proves the engine library compiles, links against its +// vcpkg dependencies, and is callable. This is what Gate G0 ("infrastructure +// compiles on all platforms") checks in CI. +#include +#include +#include + +TEST(EngineSmoke, VersionIsReported) { + EXPECT_FALSE(pdfengine::engineVersion().empty()); + EXPECT_EQ(pdfengine::engineVersion(), pdfengine::version_string); +} + +TEST(EngineSmoke, BuildInfoMentionsVersion) { + const std::string_view info = pdfengine::engineBuildInfo(); + EXPECT_NE(info.find(pdfengine::version_string), std::string_view::npos); +} + +TEST(EngineSmoke, BuildInfoConsistentWithPdfiumLinkage) { + const std::string_view info = pdfengine::engineBuildInfo(); + const bool says_on = info.find("pdfium=on") != std::string_view::npos; + EXPECT_EQ(says_on, pdfengine::engineHasPdfium()); +} + +TEST(EngineSmoke, LogBuildInfoDoesNotThrow) { + // Exercises the spdlog dependency end to end (compile + link + call). + EXPECT_NO_THROW(pdfengine::engineLogBuildInfo()); +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..420a2e5 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,24 @@ +# Frontend (React + TypeScript) — placeholder + +Phase 0 task **"React + TypeScript frontend scaffolding"** (Dev 2) and all later +viewer/editor UI lives here. Not implemented yet — this session scaffolds the +C++ build pipeline only. + +Planned responsibilities (see the engine blueprint §9): + +- Layered viewer: canvas layer, selection layer, annotation layer, overlay layer. +- PDF <-> canvas <-> zoomed coordinate mapping. +- Virtualized page rendering (only visible pages). +- Talks to the gateway over HTTP; later, to the engine directly via WASM. + +Setup when implemented: Vite + React + **TypeScript strict mode** from day one. + +``` +frontend/ + package.json + tsconfig.json # "strict": true + src/ + components/ + viewer/ + lib/ +``` diff --git a/gateway/README.md b/gateway/README.md new file mode 100644 index 0000000..477a6b7 --- /dev/null +++ b/gateway/README.md @@ -0,0 +1,24 @@ +# Gateway (FastAPI) — placeholder + +Phase 0 task **"FastAPI service scaffolding"** (Dev 1) and all later API work +lives here. Not implemented yet — this session scaffolds the C++ build pipeline +only. + +Planned responsibilities (see `docs/phase0.md` and the engine blueprint §8): + +- Auth, metadata, storage (S3 / MinIO), async job queue. +- Bridges to the C++ engine via **pybind11** (`bindings/python/`). +- Placeholder routes + a `/health` endpoint returning `200` — **no PDF logic** + until the engine wrappers exist. + +When implemented: + +``` +gateway/ + pyproject.toml + app/ + main.py # FastAPI app + /health + routers/ + services/ + tests/ +``` diff --git a/scripts/bootstrap.ps1 b/scripts/bootstrap.ps1 new file mode 100644 index 0000000..be8a696 --- /dev/null +++ b/scripts/bootstrap.ps1 @@ -0,0 +1,60 @@ +#requires -Version 5.1 +# One-time developer setup (Windows): +# - verifies required tools +# - installs vcpkg if VCPKG_ROOT is not set +# - pins the vcpkg dependency baseline in vcpkg.json +$ErrorActionPreference = 'Stop' + +$RepoRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +Set-Location $RepoRoot + +Write-Host '>> Checking required tools' +$missing = $false +foreach ($tool in @('git', 'cmake', 'ninja')) { + if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { + Write-Host " MISSING: $tool" + $missing = $true + } +} +if ($missing) { Write-Error 'Install the missing tools and re-run.' } + +# MSVC is only on PATH inside a Developer prompt; detect the install instead. +$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' +if (Test-Path $vswhere) { + $vc = & $vswhere -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if ($vc) { Write-Host " MSVC C++ toolchain: $vc" } + else { Write-Host ' WARNING: Visual Studio found but the C++ workload (VC.Tools) is not installed.' } +} else { + Write-Host ' WARNING: Visual Studio not detected. Install VS 2022 with the' + Write-Host ' "Desktop development with C++" workload (MSVC 19.36+ for C++23).' +} + +Write-Host '>> Setting up vcpkg' +if (-not $env:VCPKG_ROOT) { + $vcpkgDir = Join-Path $RepoRoot 'vcpkg' + if (-not (Test-Path $vcpkgDir)) { + Write-Host ' Cloning vcpkg into .\vcpkg' + git clone https://github.com/microsoft/vcpkg.git $vcpkgDir + } + & (Join-Path $vcpkgDir 'bootstrap-vcpkg.bat') -disableMetrics + $env:VCPKG_ROOT = $vcpkgDir + Write-Host ' VCPKG_ROOT is not set in your environment. Set it permanently with:' + Write-Host " setx VCPKG_ROOT `"$vcpkgDir`"" +} else { + Write-Host " Using VCPKG_ROOT=$env:VCPKG_ROOT" +} +$vcpkgExe = Join-Path $env:VCPKG_ROOT 'vcpkg.exe' + +Write-Host '>> Pinning the vcpkg dependency baseline' +if (Select-String -Path 'vcpkg.json' -Pattern '"builtin-baseline"' -Quiet) { + Write-Host ' builtin-baseline already present — leaving it pinned.' +} else { + & $vcpkgExe x-update-baseline --add-initial-baseline + Write-Host ' Added builtin-baseline to vcpkg.json. Commit this change.' +} + +Write-Host '' +Write-Host '>> Bootstrap complete. Next steps:' +Write-Host ' cmake --preset windows-debug' +Write-Host ' cmake --build --preset windows-debug' +Write-Host ' ctest --preset windows-debug' diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100644 index 0000000..2930c8e --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# One-time developer setup (Linux / macOS): +# - verifies required tools +# - installs vcpkg if VCPKG_ROOT is not set +# - pins the vcpkg dependency baseline in vcpkg.json +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${REPO_ROOT}" + +echo ">> Checking required tools" +missing=0 +for tool in git cmake ninja; do + if ! command -v "${tool}" >/dev/null 2>&1; then + echo " MISSING: ${tool}" >&2 + missing=1 + fi +done +if [[ "${missing}" -ne 0 ]]; then + echo "ERROR: install the missing tools and re-run." >&2 + exit 1 +fi +if ! command -v c++ >/dev/null 2>&1 && ! command -v clang++ >/dev/null 2>&1; then + echo "WARNING: no C++ compiler found on PATH (need GCC 13+ or Clang 16+ for C++23)." >&2 +fi + +echo ">> Setting up vcpkg" +if [[ -z "${VCPKG_ROOT:-}" ]]; then + if [[ ! -d "${REPO_ROOT}/vcpkg" ]]; then + echo " Cloning vcpkg into ./vcpkg" + git clone https://github.com/microsoft/vcpkg.git "${REPO_ROOT}/vcpkg" + fi + "${REPO_ROOT}/vcpkg/bootstrap-vcpkg.sh" -disableMetrics + export VCPKG_ROOT="${REPO_ROOT}/vcpkg" + echo " VCPKG_ROOT is not set in your environment." + echo " Add this to your shell profile (~/.bashrc, ~/.zshrc):" + echo " export VCPKG_ROOT=\"${REPO_ROOT}/vcpkg\"" +else + echo " Using VCPKG_ROOT=${VCPKG_ROOT}" +fi +VCPKG_EXE="${VCPKG_ROOT}/vcpkg" + +echo ">> Pinning the vcpkg dependency baseline" +if grep -q '"builtin-baseline"' vcpkg.json; then + echo " builtin-baseline already present — leaving it pinned." +else + "${VCPKG_EXE}" x-update-baseline --add-initial-baseline + echo " Added builtin-baseline to vcpkg.json. Commit this change." +fi + +echo "" +echo ">> Bootstrap complete. Next steps:" +echo " cmake --preset linux-debug # or macos-debug" +echo " cmake --build --preset linux-debug" +echo " ctest --preset linux-debug" diff --git a/scripts/check_pdfium_boundary.ps1 b/scripts/check_pdfium_boundary.ps1 new file mode 100644 index 0000000..edbc659 --- /dev/null +++ b/scripts/check_pdfium_boundary.ps1 @@ -0,0 +1,42 @@ +#requires -Version 5.1 +# Rule R2 enforcement: raw PDFium APIs (FPDF_*) and PDFium headers (fpdf*.h) may +# only appear under engine/src/parser/. Run locally and in CI. +# +# C/C++ comments are stripped before matching, so headers that *document* the +# rule (e.g. engine/include/pdfengine/pdf_document.hpp) don't trip it — only +# real code usage counts. +$ErrorActionPreference = 'Stop' + +$RepoRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +Set-Location $RepoRoot + +$sep = [IO.Path]::DirectorySeparatorChar +$allowed = "engine${sep}src${sep}parser${sep}" +$pattern = 'FPDF_|#\s*include\s*[<"]fpdf' + +$sources = Get-ChildItem -Path 'engine', 'bindings' -Recurse -File -ErrorAction SilentlyContinue ` + -Include '*.cpp', '*.cc', '*.h', '*.hpp' | + Where-Object { + $rel = $_.FullName.Substring($RepoRoot.Length + 1) + -not $rel.StartsWith($allowed) + } + +$violations = @() +foreach ($file in $sources) { + $text = Get-Content -Raw -LiteralPath $file.FullName + if (-not $text) { continue } + # Strip // line comments and /* ... */ block comments before matching. + $stripped = [regex]::Replace($text, '//[^\r\n]*|/\*[\s\S]*?\*/', '') + if ($stripped -match $pattern) { + $violations += $file.FullName.Substring($RepoRoot.Length + 1) + } +} + +if ($violations) { + Write-Host 'ERROR: Rule R2 violation - raw PDFium usage outside engine/src/parser/:' + $violations | ForEach-Object { Write-Host " $_" } + Write-Host 'All PDFium access must go through the parser boundary.' + exit 1 +} + +Write-Host 'R2 boundary check: OK (no raw PDFium usage outside engine/src/parser/)' diff --git a/scripts/check_pdfium_boundary.sh b/scripts/check_pdfium_boundary.sh new file mode 100644 index 0000000..d209519 --- /dev/null +++ b/scripts/check_pdfium_boundary.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Rule R2 enforcement: raw PDFium APIs (FPDF_*) and PDFium headers (fpdf*.h) may +# only appear under engine/src/parser/. Run locally and in CI. +# +# C/C++ comments are stripped before matching, so headers that *document* the +# rule (e.g. engine/include/pdfengine/pdf_document.hpp) don't trip it — only +# real code usage counts. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${REPO_ROOT}" + +pattern='FPDF_|#[[:space:]]*include[[:space:]]*[<"]fpdf' +violations="" + +while IFS= read -r -d '' file; do + # engine/src/parser/ is the one allowed home of raw PDFium usage. + case "${file}" in + engine/src/parser/*) continue ;; + esac + # Strip // line comments and /* ... */ block comments, then search. + if perl -0777 -pe 's{//[^\n]*|/\*.*?\*/}{}gs' "${file}" | grep -Eq "${pattern}"; then + violations+=" ${file}"$'\n' + fi +done < <(find engine bindings \ + \( -name '*.cpp' -o -name '*.cc' -o -name '*.h' -o -name '*.hpp' \) \ + -print0 2>/dev/null) + +if [[ -n "${violations}" ]]; then + echo "ERROR: Rule R2 violation — raw PDFium usage outside engine/src/parser/:" >&2 + printf '%s' "${violations}" >&2 + echo "All PDFium access must go through the parser boundary." >&2 + exit 1 +fi + +echo "R2 boundary check: OK (no raw PDFium usage outside engine/src/parser/)" diff --git a/third_party/pdfium/README.md b/third_party/pdfium/README.md new file mode 100644 index 0000000..8281a85 --- /dev/null +++ b/third_party/pdfium/README.md @@ -0,0 +1,93 @@ +# PDFium — built from source + +PDFium is the parser + base-rendering core of the engine (Rule R1). It is **not** +a vcpkg package; Google ships it only as source built with their own toolchain +(**depot_tools + GN + Ninja**). The timeline flags this as *"the single biggest +Day-1 risk — budget a full day for depot_tools quirks."* + +## What the build scripts do + +`build_pdfium.ps1` (Windows) and `build_pdfium.sh` (Linux/macOS) automate: + +1. Read the **pinned revision** from `pdfium.pinned` (refuses to run if it is + still the placeholder — Rule: never track rolling HEAD). +2. Clone `depot_tools` under the **build root** and run its Windows bootstrap + (fetches bundled git + python via CIPD). +3. `gclient config` + `gclient sync` the PDFium tree into `/checkout/`. +4. Check out the exact pinned commit and re-sync its DEPS. +5. Write `args.gn` for a **static, standalone, monolithic, embed-friendly** build: + - `is_component_build = false` — one static lib, not many DLLs + - `pdf_is_standalone = true` + - `pdf_enable_v8 = false`, `pdf_enable_xfa = false` — no JS / XFA in v1 + - `pdf_use_skia = false` — we drive Skia ourselves later + - `use_custom_libcxx = false` — link the system C++ runtime so PDFium is + ABI-compatible with the rest of the engine *(critical for embedding)* + - `pdf_use_partition_alloc = false` +6. `gn gen` + `ninja -C out/Release pdfium`. +7. Copy `public/*.h` → `install/include/` and the static lib → `install/lib/`. + +`cmake/pdfium.cmake` then turns `install/` into the `pdfium::pdfium` imported +target. Build the engine with `-DPDFENGINE_WITH_PDFIUM=ON` to link it. + +## The build root — paths with spaces + +**depot_tools, GN and Ninja do not support a space anywhere in their own path.** +By default the scripts build under this directory (`third_party/pdfium/`), which +is fine when the repo lives on a space-free path. + +If the repo path contains a space (e.g. `OneDrive\...\PDF Editor\...`), the +scripts **hard-error** and you must point the build root somewhere space-free: + +```powershell +# Windows +$env:PDFIUM_BUILD_ROOT = 'C:\pdfium-build' +pwsh third_party\pdfium\build_pdfium.ps1 +``` +```sh +# Linux / macOS +PDFIUM_BUILD_ROOT=/tmp/pdfium-build ./third_party/pdfium/build_pdfium.sh +``` + +`depot_tools/` and `checkout/` then live under the build root; the finished +`install/` is **always** written into `third_party/pdfium/install/` (git-ignored) +so `cmake/pdfium.cmake` finds it in the same place regardless. + +## Pinning the revision + +`pdfium.pinned` holds the pinned commit SHA. It is currently pinned to a +specific `main`-branch commit. To re-pin (a deliberate, scheduled action — the +risk register calls for *quarterly* rebases): + +1. Pick a commit from https://pdfium.googlesource.com/pdfium/+log/main (or the + tip of a recent `chromium/NNNN` release branch). +2. Update `PDFIUM_COMMIT=` in `pdfium.pinned` and commit it. +3. Re-run the build script. + +## Usage + +Prerequisites: Git, Python 3, and a C++ toolchain (Windows: Visual Studio 2022+ +with the "Desktop development with C++" workload). + +```powershell +# Windows — set PDFIUM_BUILD_ROOT first if the repo path has a space (see above) +pwsh third_party\pdfium\build_pdfium.ps1 +``` +```sh +# Linux / macOS +./third_party/pdfium/build_pdfium.sh +``` + +Expect the first run to take a long time — the `gclient sync` alone pulls +several GB, and the compile is lengthy. `depot_tools/`, `checkout/`, and +`install/` are all git-ignored. + +## Troubleshooting + +- **`'C:\...\PDF' is not recognized` / GN or Ninja path errors** — a space in + the build-root path. Set `PDFIUM_BUILD_ROOT` to a space-free location. +- **`gclient sync` aborts with "uncommitted changes"** — git's `core.autocrlf` + rewrote a dependency checkout. The scripts already inject `core.autocrlf=false` + per-process; if you bypass them, set it yourself. +- **depot_tools `git`/`python` not found** — depot_tools was not bootstrapped. + The scripts run `bootstrap\win_tools.bat`; do not set `DEPOT_TOOLS_UPDATE=0`, + which suppresses that bootstrap. diff --git a/third_party/pdfium/args.gn b/third_party/pdfium/args.gn new file mode 100644 index 0000000..a606c78 --- /dev/null +++ b/third_party/pdfium/args.gn @@ -0,0 +1,37 @@ +# GN build args for PDFium — static, standalone, monolithic, embed-friendly. +# Copied to out/Release/args.gn by the build scripts. See README.md for rationale. + +is_debug = false + +# One self-contained static library, not a component (DLL) build. +is_component_build = false + +# Bundle PDFium *and all its dependencies* into a single static lib +# (out/Release/obj/pdfium.lib). Without this the `pdfium` target is just a +# GN group — it builds the component libs but never archives them together, +# leaving nothing for the engine to link against. +pdf_is_complete_lib = true + +# Standalone embedder build (no full Chromium tree). +pdf_is_standalone = true + +# v1 scope: no JavaScript engine, no XFA forms. +pdf_enable_v8 = false +pdf_enable_xfa = false + +# We integrate Skia ourselves at the engine layer — PDFium uses its built-in +# AGG rasterizer here. +pdf_use_skia = false + +# Critical for embedding: link the system C++ runtime instead of Chromium's +# bundled libc++, so PDFium is ABI-compatible with the rest of the engine. +use_custom_libcxx = false + +# Avoid Chromium's PartitionAlloc — keeps the embed surface simple. +pdf_use_partition_alloc = false + +# Don't require the Chromium clang plugins for a standalone build. +clang_use_chrome_plugins = false + +# Treat warnings as warnings — PDFium upstream, not our code. +treat_warnings_as_errors = false diff --git a/third_party/pdfium/build_pdfium.ps1 b/third_party/pdfium/build_pdfium.ps1 new file mode 100644 index 0000000..9975b78 --- /dev/null +++ b/third_party/pdfium/build_pdfium.ps1 @@ -0,0 +1,144 @@ +#requires -Version 5.1 +# Build PDFium from source (Windows) into third_party\pdfium\install\. +# +# depot_tools, the PDFium checkout, and the GN/Ninja build all happen under a +# BUILD ROOT. depot_tools, GN and Ninja do NOT support spaces anywhere in their +# own path. If this repo lives under a path containing a space (for example +# OneDrive\...\PDF Editor\...), you MUST point the build root somewhere +# space-free: +# +# $env:PDFIUM_BUILD_ROOT = 'C:\pdfium-build' +# pwsh third_party\pdfium\build_pdfium.ps1 +# +# The finished static lib + public headers are always installed into +# third_party\pdfium\install\ (git-ignored) regardless of the build root, so +# cmake/pdfium.cmake finds them in the same place either way. +# +# One-time and slow: a multi-GB gclient sync plus a long compile. See README.md. +$ErrorActionPreference = 'Stop' + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$Install = Join-Path $ScriptDir 'install' +$PinnedFile = Join-Path $ScriptDir 'pdfium.pinned' + +# Build root: depot_tools + the checkout live here. Defaults to this directory +# (fine for repos on a space-free path), but MUST be overridden to a space-free +# location otherwise. +$BuildRoot = if ($env:PDFIUM_BUILD_ROOT) { $env:PDFIUM_BUILD_ROOT } else { $ScriptDir } +if ($BuildRoot -match '\s') { + Write-Error @" +PDFium build root contains a space: $BuildRoot +depot_tools, GN and Ninja cannot build under a path with spaces. Set a +space-free build root and re-run, e.g.: + `$env:PDFIUM_BUILD_ROOT = 'C:\pdfium-build' + pwsh third_party\pdfium\build_pdfium.ps1 +"@ +} +$DepotTools = Join-Path $BuildRoot 'depot_tools' +$Checkout = Join-Path $BuildRoot 'checkout' +Write-Host ">> Build root: $BuildRoot" + +# --- 1. Read and validate the pinned revision ------------------------------- +$pinned = Get-Content $PinnedFile | Where-Object { $_ -match '^\s*PDFIUM_' } +$repo = ($pinned | Where-Object { $_ -match '^PDFIUM_REPO=' }) -replace '^PDFIUM_REPO=', '' +$commit = ($pinned | Where-Object { $_ -match '^PDFIUM_COMMIT=' }) -replace '^PDFIUM_COMMIT=', '' +if (-not $commit -or $commit -eq 'REPLACE_WITH_PINNED_COMMIT_SHA') { + Write-Error 'PDFium revision is not pinned. Edit pdfium.pinned first (see README.md).' +} +Write-Host ">> PDFium pinned at $commit" + +# --- 2. depot_tools --------------------------------------------------------- +New-Item -ItemType Directory -Force -Path $BuildRoot | Out-Null +if (-not (Test-Path $DepotTools)) { + Write-Host '>> Cloning depot_tools' + git clone --depth 1 https://chromium.googlesource.com/chromium/tools/depot_tools.git $DepotTools +} +$env:PATH = "$DepotTools;$env:PATH" +# Use the locally installed Visual Studio toolchain, not Google's internal one. +$env:DEPOT_TOOLS_WIN_TOOLCHAIN = '0' + +# Git settings Chromium requires on Windows, injected per-process via GIT_CONFIG_* +# so the user's global git config is never touched. core.autocrlf=false is the +# critical one: without it, gclient sees CRLF-converted dependency checkouts as +# "uncommitted changes" and aborts the sync. +$env:GIT_CONFIG_COUNT = '4' +$env:GIT_CONFIG_KEY_0 = 'core.autocrlf'; $env:GIT_CONFIG_VALUE_0 = 'false' +$env:GIT_CONFIG_KEY_1 = 'core.filemode'; $env:GIT_CONFIG_VALUE_1 = 'false' +$env:GIT_CONFIG_KEY_2 = 'core.fscache'; $env:GIT_CONFIG_VALUE_2 = 'true' +$env:GIT_CONFIG_KEY_3 = 'core.preloadindex'; $env:GIT_CONFIG_VALUE_3 = 'true' + +# Bootstrap depot_tools. On Windows it must fetch its bundled git + python via +# CIPD and generate the git.bat / python3.bat wrappers before gclient can run. +# NOTE: do NOT set DEPOT_TOOLS_UPDATE=0 here — that suppresses this initial +# bootstrap, not just self-updates. Reproducibility comes from the pinned PDFium +# revision; depot_tools itself is designed to self-manage. +Write-Host '>> Bootstrapping depot_tools (fetches bundled git + python; one-time, slow)' +& cmd /c "`"$DepotTools\bootstrap\win_tools.bat`"" +if ($LASTEXITCODE -ne 0) { Write-Error 'depot_tools bootstrap failed.' } + +# --- 2b. Locate Visual Studio for the GN build ----------------------------- +# gn's vs_toolchain.py finds VS via the vs_install env var or a fixed +# path (...\Microsoft Visual Studio\). VS *Build Tools* installs under a +# major-version path (e.g. \18\BuildTools), not the year, so the fixed-path +# probe misses it — resolve VS with vswhere and set vs_install. vswhere +# needs `-products *` to see Build Tools at all. +$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' +if (-not (Test-Path $vswhere)) { + Write-Error 'vswhere.exe not found. Install Visual Studio 2022+ (or Build Tools) with the C++ workload.' +} +$vsPath = (& $vswhere -latest -prerelease -products * -property installationPath | Select-Object -First 1) +$vsVer = (& $vswhere -latest -prerelease -products * -property installationVersion | Select-Object -First 1) +if (-not $vsPath -or -not (Test-Path (Join-Path $vsPath 'VC\Tools\MSVC'))) { + Write-Error 'No Visual Studio with the C++ toolchain (VC.Tools) found — install the "Desktop development with C++" workload.' +} +$vsYear = @{ '18' = '2026'; '17' = '2022'; '16' = '2019'; '15' = '2017' }[$vsVer.Split('.')[0]] +if (-not $vsYear) { Write-Error "Unsupported Visual Studio major version: $vsVer (need 15/16/17/18)." } +Write-Host ">> Visual Studio ${vsYear}: $vsPath" +Set-Item -Path "env:vs${vsYear}_install" -Value $vsPath +$env:GYP_MSVS_VERSION = $vsYear + +# --- 3. Fetch / sync the PDFium tree ---------------------------------------- +New-Item -ItemType Directory -Force -Path $Checkout | Out-Null +Push-Location $Checkout +if (-not (Test-Path (Join-Path $Checkout 'pdfium'))) { + Write-Host '>> gclient config (unmanaged)' + & gclient config --unmanaged $repo + if ($LASTEXITCODE -ne 0) { Write-Error 'gclient config failed.' } +} +Write-Host '>> gclient sync (pulls several GB; slow)' +& gclient sync --no-history --shallow --reset --force +if ($LASTEXITCODE -ne 0) { Write-Error 'gclient sync failed.' } + +# --- 4. Pin to the exact commit + sync its DEPS ----------------------------- +Push-Location (Join-Path $Checkout 'pdfium') +& git fetch origin $commit +& git checkout --detach $commit +if ($LASTEXITCODE -ne 0) { Write-Error "git checkout $commit failed." } +& gclient sync --no-history --shallow --reset --force -D +if ($LASTEXITCODE -ne 0) { Write-Error 'gclient sync (pinned DEPS) failed.' } + +# --- 5. GN args: static, standalone, monolithic, embed-friendly ------------- +New-Item -ItemType Directory -Force -Path 'out\Release' | Out-Null +Copy-Item (Join-Path $ScriptDir 'args.gn') 'out\Release\args.gn' -Force + +# --- 6. Generate + build ---------------------------------------------------- +Write-Host '>> gn gen' +& gn gen out/Release +if ($LASTEXITCODE -ne 0) { Write-Error 'gn gen failed.' } +Write-Host '>> ninja (long compile)' +& ninja -C out/Release pdfium +if ($LASTEXITCODE -ne 0) { Write-Error 'ninja build failed.' } + +# --- 7. Install: public headers + static lib -------------------------------- +Write-Host ">> Installing into $Install" +if (Test-Path $Install) { Remove-Item -Recurse -Force $Install } +New-Item -ItemType Directory -Force -Path "$Install\include", "$Install\lib" | Out-Null +Copy-Item 'public\*.h' "$Install\include\" -Force +if (Test-Path 'public\cpp') { Copy-Item 'public\cpp' "$Install\include\" -Recurse -Force } +$lib = if (Test-Path 'out\Release\obj\pdfium.lib') { 'out\Release\obj\pdfium.lib' } + else { 'out\Release\pdfium.lib' } +Copy-Item $lib "$Install\lib\" -Force + +Pop-Location +Pop-Location +Write-Host '>> Done. Configure the engine with -DPDFENGINE_WITH_PDFIUM=ON' diff --git a/third_party/pdfium/build_pdfium.sh b/third_party/pdfium/build_pdfium.sh new file mode 100644 index 0000000..aebdbba --- /dev/null +++ b/third_party/pdfium/build_pdfium.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# Build PDFium from source (Linux / macOS) into third_party/pdfium/install/. +# +# depot_tools, the PDFium checkout, and the GN/Ninja build all happen under a +# BUILD ROOT. depot_tools, GN and Ninja do NOT support spaces anywhere in their +# own path. If this repo lives under a path containing a space, you MUST point +# the build root somewhere space-free: +# +# PDFIUM_BUILD_ROOT=/tmp/pdfium-build ./third_party/pdfium/build_pdfium.sh +# +# The finished static lib + public headers are always installed into +# third_party/pdfium/install/ (git-ignored) regardless of the build root, so +# cmake/pdfium.cmake finds them in the same place either way. +# +# One-time and slow: a multi-GB gclient sync plus a long compile. See README.md. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_DIR="${SCRIPT_DIR}/install" +PINNED_FILE="${SCRIPT_DIR}/pdfium.pinned" + +# Build root: depot_tools + the checkout live here. Defaults to this directory +# (fine for repos on a space-free path), but MUST be overridden otherwise. +BUILD_ROOT="${PDFIUM_BUILD_ROOT:-${SCRIPT_DIR}}" +case "${BUILD_ROOT}" in + *' '*) + echo "ERROR: PDFium build root contains a space: ${BUILD_ROOT}" >&2 + echo "depot_tools, GN and Ninja cannot build under a path with spaces." >&2 + echo "Re-run with a space-free build root, e.g.:" >&2 + echo " PDFIUM_BUILD_ROOT=/tmp/pdfium-build $0" >&2 + exit 1 ;; +esac +DEPOT_TOOLS_DIR="${BUILD_ROOT}/depot_tools" +CHECKOUT_DIR="${BUILD_ROOT}/checkout" +echo ">> Build root: ${BUILD_ROOT}" + +# --- 1. Read and validate the pinned revision ------------------------------- +PDFIUM_REPO="$(grep -E '^PDFIUM_REPO=' "${PINNED_FILE}" | cut -d= -f2-)" +PDFIUM_COMMIT="$(grep -E '^PDFIUM_COMMIT=' "${PINNED_FILE}" | cut -d= -f2-)" +if [[ -z "${PDFIUM_COMMIT}" || "${PDFIUM_COMMIT}" == "REPLACE_WITH_PINNED_COMMIT_SHA" ]]; then + echo "ERROR: PDFium revision is not pinned. Edit pdfium.pinned first (see README.md)." >&2 + exit 1 +fi +echo ">> PDFium pinned at ${PDFIUM_COMMIT}" + +# --- 2. depot_tools --------------------------------------------------------- +mkdir -p "${BUILD_ROOT}" +if [[ ! -d "${DEPOT_TOOLS_DIR}" ]]; then + echo ">> Cloning depot_tools" + git clone --depth 1 https://chromium.googlesource.com/chromium/tools/depot_tools.git \ + "${DEPOT_TOOLS_DIR}" +fi +export PATH="${DEPOT_TOOLS_DIR}:${PATH}" +# Do NOT set DEPOT_TOOLS_UPDATE=0 — depot_tools is designed to self-manage, and +# on first use it must bootstrap. Reproducibility comes from the pinned PDFium +# revision below, not from freezing depot_tools. + +# Git settings injected per-process via GIT_CONFIG_* so the user's global git +# config is never touched. core.autocrlf=false avoids gclient seeing dependency +# checkouts as "uncommitted changes" on platforms where autocrlf is enabled. +export GIT_CONFIG_COUNT=2 +export GIT_CONFIG_KEY_0=core.autocrlf GIT_CONFIG_VALUE_0=false +export GIT_CONFIG_KEY_1=core.filemode GIT_CONFIG_VALUE_1=false + +# --- 3. Fetch / sync the PDFium tree ---------------------------------------- +mkdir -p "${CHECKOUT_DIR}" +cd "${CHECKOUT_DIR}" +if [[ ! -d "${CHECKOUT_DIR}/pdfium" ]]; then + echo ">> gclient config (unmanaged)" + gclient config --unmanaged "${PDFIUM_REPO}" +fi +echo ">> gclient sync (pulls several GB; slow)" +gclient sync --no-history --shallow --reset --force + +# --- 4. Pin to the exact commit + sync its DEPS ----------------------------- +cd "${CHECKOUT_DIR}/pdfium" +git fetch origin "${PDFIUM_COMMIT}" +git checkout --detach "${PDFIUM_COMMIT}" +gclient sync --no-history --shallow --reset --force -D + +# --- 5. GN args: static, standalone, monolithic, embed-friendly ------------- +mkdir -p out/Release +cp "${SCRIPT_DIR}/args.gn" out/Release/args.gn + +# --- 6. Generate + build ---------------------------------------------------- +echo ">> gn gen + ninja" +gn gen out/Release +ninja -C out/Release pdfium + +# --- 7. Install: public headers + static lib -------------------------------- +echo ">> Installing into ${INSTALL_DIR}" +rm -rf "${INSTALL_DIR}" +mkdir -p "${INSTALL_DIR}/include" "${INSTALL_DIR}/lib" +cp public/*.h "${INSTALL_DIR}/include/" +cp -r public/cpp "${INSTALL_DIR}/include/" 2>/dev/null || true +if [[ -f out/Release/obj/libpdfium.a ]]; then + cp out/Release/obj/libpdfium.a "${INSTALL_DIR}/lib/" +else + # Fallback: some configurations emit the lib at the out-dir root. + cp out/Release/libpdfium.a "${INSTALL_DIR}/lib/" +fi + +echo ">> Done. Configure the engine with -DPDFENGINE_WITH_PDFIUM=ON" diff --git a/third_party/pdfium/pdfium.pinned b/third_party/pdfium/pdfium.pinned new file mode 100644 index 0000000..63a6e3e --- /dev/null +++ b/third_party/pdfium/pdfium.pinned @@ -0,0 +1,13 @@ +# PDFium pinned revision — Rule: never track rolling HEAD. +# +# Pinned to a specific main-branch commit (a clean dependency-roll commit). +# Rebasing is a deliberate, scheduled (quarterly) action — bump the SHA below, +# commit it, and re-run the build scripts. +# +# To re-pin: pick a commit from https://pdfium.googlesource.com/pdfium/+log/main +# (or the tip of a recent chromium/NNNN release branch) and update PDFIUM_COMMIT. +# The build scripts refuse to run while PDFIUM_COMMIT is the placeholder string. + +PDFIUM_REPO=https://pdfium.googlesource.com/pdfium.git +# main @ 2026-05-13 — "Roll third_party/cpu_features/src/ ..." +PDFIUM_COMMIT=423b6b376015e9458d588bd9a8f7b5c4ae21f8a2 diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..7f5988a --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "name": "pdf-engine", + "version": "0.1.0", + "description": [ + "Cross-platform PDF rendering and editing SDK core.", + "'builtin-baseline' pins the entire dependency registry to one vcpkg commit", + "(Rule: pin versions on Day 1, never track rolling HEAD). It was added by", + "'vcpkg x-update-baseline --add-initial-baseline' via scripts/bootstrap; the", + "root CMakeLists.txt refuses to configure if it is ever removed." + ], + "dependencies": [ + "freetype", + "harfbuzz", + "spdlog", + "gtest" + ], + "builtin-baseline": "495848814af4cc2760e70f7440c2dbe66d3ff196" +} diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 0000000..d665e03 --- /dev/null +++ b/wasm/README.md @@ -0,0 +1,22 @@ +# WASM build — placeholder + +Phase 0 task **"WASM hello-world build (Emscripten)"** and the Phase 2 task +**"WASM rendering path"** live here. Not implemented yet. + +**Rule R5: WASM never blocks shipping.** Server-side rendering is always the +fallback. The Phase 0 WASM task is *validation only* — prove that Emscripten can +compile a C++ function callable from JS. Do not gate Phase 1 on it. + +Planned pipeline (engine blueprint §6): + +```sh +emcmake cmake --preset wasm +emmake cmake --build --preset wasm +# outputs: engine.wasm + engine.js +``` + +Key build flags to carry forward: `ALLOW_MEMORY_GROWTH=1`, Web Worker + +OffscreenCanvas for off-main-thread rendering. + +The `cmake/toolchains/wasm.cmake` hook and a `wasm` CMake preset are stubbed in +this session so the integration point exists.