first commit

This commit is contained in:
Furqan-14
2026-05-15 10:39:16 +05:30
commit 43eba01201
38 changed files with 2021 additions and 0 deletions
+24
View File
@@ -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
+49
View File
@@ -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
+30
View File
@@ -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
+39
View File
@@ -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
+106
View File
@@ -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 }}
+60
View File
@@ -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
+88
View File
@@ -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 <platform>-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}")
+141
View File
@@ -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$<$<CONFIG:Debug>: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" }
]
}
+84
View File
@@ -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\<you>\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`.
+13
View File
@@ -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<T, E>`; the C ABI surface translates
that to `int error_code` (engine blueprint §16.2).
+52
View File
@@ -0,0 +1,52 @@
# Reusable warning configuration.
# Usage: pdfengine_set_warnings(<target> [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()
+28
View File
@@ -0,0 +1,28 @@
# AddressSanitizer / UndefinedBehaviorSanitizer wiring.
# Enabled per build via -DPDFENGINE_ENABLE_SANITIZERS=ON (see the *-asan presets).
# Usage: pdfengine_enable_sanitizers(<target>)
#
# 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()
+63
View File
@@ -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}")
+31
View File
@@ -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")
+25
View File
@@ -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.
+273
View File
@@ -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$<$<CONFIG:Debug>: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/<you>/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<T, E>` 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/<you>/pdfeng-build/...`); do the same for
any non-PDFium preset by overriding `binaryDir`:
```powershell
cmake --preset windows-release -B C:/Users/<you>/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\<you>\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.
+51
View File
@@ -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
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
"$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/generated>"
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()
+26
View File
@@ -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
+25
View File
@@ -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 <pdfengine/version.hpp>
#include <string_view>
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
+14
View File
@@ -0,0 +1,14 @@
// Generated by CMake from version.hpp.in — do not edit the generated copy.
#pragma once
#include <string_view>
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
+31
View File
@@ -0,0 +1,31 @@
#include "parser/pdfium_loader.hpp"
#include <pdfengine/pdf_engine.hpp>
#include <spdlog/spdlog.h>
#include <string>
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
+31
View File
@@ -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 <fpdfview.h>
#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
+20
View File
@@ -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
+18
View File
@@ -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)
+27
View File
@@ -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 <gtest/gtest.h>
#include <pdfengine/pdf_engine.hpp>
#include <string_view>
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());
}
+24
View File
@@ -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/
```
+24
View File
@@ -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/
```
+60
View File
@@ -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'
+55
View File
@@ -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"
+42
View File
@@ -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/)'
+36
View File
@@ -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/)"
+93
View File
@@ -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 `<build root>/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.
+37
View File
@@ -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
+144
View File
@@ -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<YEAR>_install env var or a fixed
# path (...\Microsoft Visual Studio\<YEAR>). 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<YEAR>_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'
+103
View File
@@ -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"
+13
View File
@@ -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
+19
View File
@@ -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"
}
+22
View File
@@ -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.