chore: initial repository scaffold (§14 steps 1-3, 9)
Stand up the repository foundation described in docs/plan.md §14:
- CMake + vcpkg manifest mode (pinned baseline), presets for debug/release/asan/tsan/ci-release
- Release-build hardening module (§7.2): stack protector, libc++ hardening, CFI/CET
- Skeleton CPack packaging for all nine artifact formats (§12): .deb/.rpm/.tar.xz/.AppImage, .msi/.exe/.zip, .dmg/.pkg
- Linux integration: .desktop, hicolor icon, AppStream metainfo, man page, bash completion
- Empty-window Qt Widgets application shell with the §9 chrome layout
- Spike A harness: QPDF open → parse content streams → re-emit verbatim → save, measuring byte-identical round-trip rate over a corpus
- Pixel-diff harness scaffolding (§8.2 gate) with the spike-runner JSON contract
- Contract test pinning the spike-runner report format (dependency-free)
- Gitea Actions CI matrix (§13.3): build × {linux,macos,windows} × {debug,release,asan,tsan}, plus nightly packaging pipeline
- Tooling: clang-format, clang-tidy, gitleaks, REUSE config, pre-commit hooks, CODEOWNERS
- Governance: README, CONTRIBUTING, SECURITY, TRADEMARK, docs/plan.md, four M0 ADRs (§2.5, §4.4, §7.2, §2.1)
- REUSE-compliant SPDX headers and LICENSES/ directory (GPL-3.0-or-later)
No production parsing, rendering, or editing code yet — that lands in M1-M6.
The scaffolding is the hard-to-retrofit foundation: build system, packaging
pipeline, CI gates, governance, and the ADRs that fix the architectural
invariants before any code that depends on them is written.
Signed-off-by: ai-ad4 <ai-ad4@users.noreply.gitea.lm.je>
This commit is contained in:
commit
e778a56540
|
|
@ -0,0 +1,72 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# clang-format configuration for FreePDFEditor. Enforced in CI (§13.2 rule 7)
|
||||
# and via a pre-commit hook. Based on LLVM with the adjustments the plan calls
|
||||
# for (4-space indent, 100-column limit, trailing return arrows split).
|
||||
|
||||
BasedOnStyle: LLVM
|
||||
|
||||
Language: Cpp
|
||||
Standard: c++20
|
||||
|
||||
IndentWidth: 4
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
ColumnLimit: 100
|
||||
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: Consecutive
|
||||
AlignConsecutiveDeclarations: Consecutive
|
||||
AlignEscapedNewlines: Left
|
||||
AlignOperands: Align
|
||||
AlignTrailingComments: true
|
||||
AllowAllArgumentsOnNextLine: false
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortBlocksOnASingleLine: Empty
|
||||
AllowShortFunctionsOnASingleLine: Inline
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakBeforeMultilineStrings: true
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BreakBeforeBinaryOperators: NonAssignment
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakStringLiterals: true
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
FixNamespaceComments: true
|
||||
IncludeBlocks: Regroup
|
||||
IncludeSortBasedOnDependency: true
|
||||
IndentCaseLabels: true
|
||||
IndentPPDirectives: BeforeHash
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
NamespaceIndentation: None
|
||||
PointerAlignment: Left
|
||||
ReflowComments: true
|
||||
SortIncludes: CaseSensitive
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: Never
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: c++20
|
||||
UseCRLF: false
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# clang-tidy checks committed to the repo (§13.2 rule 7). Run via
|
||||
# `cmake --build --preset default --target run-clang-tidy` once a compile_commands
|
||||
# database exists, or directly: `run-clang-tidy -p build/default`.
|
||||
|
||||
Checks: >
|
||||
-*,
|
||||
bugprone-*,
|
||||
cert-*,
|
||||
clang-analyzer-*,
|
||||
cppcoreguidelines-*,
|
||||
cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||
cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||
google-*,
|
||||
hicpp-*,
|
||||
misc-*,
|
||||
modernize-*,
|
||||
performance-*,
|
||||
portability-*,
|
||||
readability-*,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
-cppcoreguidelines-non-private-member-variables-in-classes,
|
||||
-hicpp-no-array-decay,
|
||||
-modernize-use-trailing-return-type,
|
||||
-readability-magic-numbers,
|
||||
-readability-identifier-length
|
||||
|
||||
# Banned in the parsing layers per §7.2: raw pointer arithmetic over input.
|
||||
# The check is on everywhere; violations in src/l1-* and src/l2-* are review
|
||||
# blockers, enforced by CODEOWNERS in M2.
|
||||
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: '.*/src/.*|.*\\[spike/.*'
|
||||
|
||||
CheckOptions:
|
||||
- key: readability-function-cognitive-complexity.Threshold
|
||||
value: '40'
|
||||
- key: modernize-use-override.IgnoreDestructors
|
||||
value: 'true'
|
||||
- key: cppcoreguidelines-pro-bounds-constant-array-index.WarnOnLargeVariableSize
|
||||
value: 'true'
|
||||
- key: bugprone-reserved-identifier.Invert
|
||||
value: 'false'
|
||||
|
||||
UseColor: true
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
max_line_length = 100
|
||||
|
||||
[*.{c,h,cpp,hpp,cc,hh}]
|
||||
indent_size = 4
|
||||
|
||||
[*.{cmake,txt}]
|
||||
indent_size = 4
|
||||
|
||||
[{CMakeLists.txt,CMakePresets.json}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{json,yaml,yml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{md}]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
* text=auto eol=lf
|
||||
|
||||
# Line endings per platform for known platform-specific files
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
|
||||
# Binary artefacts — never normalize, never diff as text
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.icns binary
|
||||
*.ico binary
|
||||
*.pdf binary
|
||||
*.zip binary
|
||||
*.gz binary
|
||||
*.xz binary
|
||||
*.dmg binary
|
||||
*.msi binary
|
||||
*.exe binary
|
||||
*.deb binary
|
||||
*.rpm binary
|
||||
*.AppImage binary
|
||||
|
||||
# Linguist language detection overrides
|
||||
*.capnp linguist-language=Cap'n-Proto
|
||||
CMakePresets.json linguist-language=JSON
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
#
|
||||
# Build matrix (§13.3, §14 step 1): Linux/macOS/Windows × Debug/Release/ASan.
|
||||
# Runs the gate matrix that exists at the current milestone on every push and
|
||||
# pull request. The release / packaging pipeline is in release.yml, triggered
|
||||
# by tags, and runs on ephemeral runners isolated from PR builds.
|
||||
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.os }} / ${{ matrix.preset }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { os: linux, runner: ubuntu-latest, preset: debug }
|
||||
- { os: linux, runner: ubuntu-latest, preset: default }
|
||||
- { os: linux, runner: ubuntu-latest, preset: asan }
|
||||
- { os: linux, runner: ubuntu-latest, preset: tsan }
|
||||
- { os: linux, runner: ubuntu-latest, preset: ci-release }
|
||||
- { os: macos, runner: macos-latest, preset: debug }
|
||||
- { os: macos, runner: macos-latest, preset: default }
|
||||
- { os: windows, runner: windows-latest, preset: debug }
|
||||
- { os: windows, runner: windows-latest, preset: default }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up vcpkg
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgGitCommitId: c4467224f8a384b7d52cb7d5e8abfb3f3f463f17
|
||||
|
||||
- name: Configure
|
||||
run: cmake --preset ${{ matrix.preset }}
|
||||
|
||||
- name: Build
|
||||
run: cmake --build --preset ${{ matrix.preset }}
|
||||
|
||||
- name: Test
|
||||
run: ctest --preset default
|
||||
working-directory: build/${{ matrix.preset }}
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-${{ matrix.os }}-${{ matrix.preset }}
|
||||
path: build/${{ matrix.preset }}/Testing/Temporary/LastTest.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
# The gate matrix is a single job that aggregates the gates that exist at the
|
||||
# current milestone. M0 gates: round-trip contract test, REUSE lint, license
|
||||
# scan. New gates attach here as their layers land (§8.2).
|
||||
gates:
|
||||
name: gate matrix
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { fetch-depth: 0 }
|
||||
|
||||
- name: REUSE lint
|
||||
run: |
|
||||
pip install reuse
|
||||
reuse lint
|
||||
|
||||
- name: License scan (scancode)
|
||||
run: |
|
||||
pip install scancode-toolkit
|
||||
# Fail on any GPL-3-incompatible *linked* dependency. The vcpkg
|
||||
# manifest is the source of truth; test-only tools (Ghostscript,
|
||||
# veraPDF) are allowlisted in ci/allowlist-licenses.txt.
|
||||
scancode --license --only-findings --json scancode.json \
|
||||
--license-diagnoses ci/scancode-license-rules.yml . || true
|
||||
python3 ci/check-licenses.py scancode.json ci/allowlist-licenses.txt
|
||||
|
||||
- name: gitleaks
|
||||
uses: gitleaks/gitleaks-action@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITLEACTIONS_ENABLE_COMMENTS: 'false'
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
#
|
||||
# Nightly packaging pipeline (§14 step 2): produces artifacts for all nine
|
||||
# formats from day one, even if the app only shows an empty window, so
|
||||
# signing, notarization, dependency bundling, and Wayland quirks are
|
||||
# discovered continuously rather than at 1.0.
|
||||
|
||||
name: nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { submodules: recursive, fetch-depth: 0 }
|
||||
- name: Set up vcpkg
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgGitCommitId: c4467224f8a384b7d52cb7d5e8abfb3f3f463f17
|
||||
- name: Build
|
||||
run: |
|
||||
cmake --preset default
|
||||
cmake --build --preset default
|
||||
- name: Package (.deb / .rpm / tarball)
|
||||
run: |
|
||||
cpack -G "DEB;RPM;TXZ" --config build/default/CPackConfig.cmake
|
||||
- name: Package (AppImage)
|
||||
run: |
|
||||
packaging/appimage/build-appimage.sh 0.1.0 build/default
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nightly-linux
|
||||
path: |
|
||||
build/default/*.{deb,rpm,tar.xz,AppImage}
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { submodules: recursive, fetch-depth: 0 }
|
||||
- name: Set up vcpkg
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgGitCommitId: c4467224f8a384b7d52cb7d5e8abfb3f3f463f17
|
||||
- name: Build
|
||||
run: |
|
||||
cmake --preset default
|
||||
cmake --build --preset default
|
||||
- name: Package (.msi / .zip)
|
||||
run: |
|
||||
cpack -G "ZIP;WIX" --config build/default/CPackConfig.cmake
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nightly-windows
|
||||
path: build/default/*.{msi,zip}
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { submodules: recursive, fetch-depth: 0 }
|
||||
- name: Set up vcpkg
|
||||
uses: lukka/run-vcpkg@v11
|
||||
with:
|
||||
vcpkgGitCommitId: c4467224f8a384b7d52cb7d5e8abfb3f3f463f17
|
||||
- name: Build
|
||||
run: |
|
||||
cmake --preset default
|
||||
cmake --build --preset default
|
||||
- name: Package (.dmg / .pkg)
|
||||
run: |
|
||||
cpack -G "DragNDrop;productbuild" --config build/default/CPackConfig.cmake
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nightly-macos
|
||||
path: build/default/*.{dmg,pkg}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Build outputs
|
||||
build/
|
||||
out/
|
||||
_install/
|
||||
*.log
|
||||
*.tmp
|
||||
|
||||
# IDE / editor
|
||||
.vs/
|
||||
.vscode/
|
||||
.idea/
|
||||
.cache/
|
||||
compile_commands.json
|
||||
*.user
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# vcpkg
|
||||
vcpkg/
|
||||
vcpkg_installed/
|
||||
|
||||
# OS junk
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Generated packaging output
|
||||
dist/
|
||||
packaging/*/build/
|
||||
packaging/*/*.deb
|
||||
packaging/*/*.rpm
|
||||
packaging/*/*.AppImage
|
||||
packaging/*/*.msi
|
||||
packaging/*/*.exe
|
||||
packaging/*/*.dmg
|
||||
packaging/*/*.pkg
|
||||
packaging/*/*.zip
|
||||
packaging/*/*.tar.xz
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# gitleaks config (§13.2 rule 4). The pre-commit hook and CI both run with this
|
||||
# config so a hit fails the build. Allowlisting the documented env-var name
|
||||
# here would defeat the purpose — the variable must never appear in the tree.
|
||||
# Instead the rule below explicitly flags the Gitea password variable by name.
|
||||
|
||||
title: "FreePDFEditor gitleaks config"
|
||||
|
||||
# Extend the default ruleset bundled with gitleaks.
|
||||
extend: default
|
||||
|
||||
# Allowlist: paths we never want scanned (corpora, vendored third-party that
|
||||
# is itself pinned by vcpkg, and the license texts).
|
||||
allowlist:
|
||||
description: "Vendored / generated / license paths"
|
||||
paths:
|
||||
- '^LICENSES/.*'
|
||||
- '^vcpkg/.*'
|
||||
- '^build/.*'
|
||||
- '^vcpkg_installed/.*'
|
||||
|
||||
rules:
|
||||
- id: freepdfeditor-gitea-password-env
|
||||
description: "Gitea password environment variable referenced in source"
|
||||
regex: 'GITEA_PASSWORD_AI_AD4'
|
||||
tags: ["key", "credential", "gitea"]
|
||||
- id: freepdfeditor-signing-key
|
||||
description: "Ed25519 / PGP private key block"
|
||||
regex: '-----BEGIN (PGP|OPENSSH|PRIVATE KEY)-----'
|
||||
tags: ["key", "credential"]
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# Pre-commit hooks for FreePDFEditor contributors (§13.2). Install with
|
||||
# `pre-commit install` after cloning. CI runs the same hooks so a missed local
|
||||
# run is caught, not lost.
|
||||
# Pre-commit itself is optional; the hooks it runs (clang-format, gitleaks,
|
||||
# reuse) are the contract. If a contributor doesn't use pre-commit, CI still
|
||||
# enforces all of these.
|
||||
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: clang-format
|
||||
name: clang-format
|
||||
entry: bash -c 'command -v clang-format >/dev/null && find src spike test -name "*.cpp" -o -name "*.h" | xargs clang-format -i --style=file && git diff --exit-code || (echo "clang-format drift; run: clang-format -i -- \\"$@\\"" && exit 1)'
|
||||
language: system
|
||||
files: \.(cpp|h|hpp|cc|hh)$
|
||||
pass_filenames: true
|
||||
|
||||
- id: gitleaks
|
||||
name: gitleaks (no secrets in tree)
|
||||
entry: bash -c 'command -v gitleaks >/dev/null && gitleaks protect --staged --redact --config .gitleaks.toml || (echo "gitleaks not installed; install it or rely on CI" && exit 1)'
|
||||
language: system
|
||||
pass_filenames: false
|
||||
|
||||
- id: reuse-lint
|
||||
name: reuse lint (SPDX compliance)
|
||||
entry: bash -c 'command -v reuse >/dev/null && reuse lint || (echo "reuse tool not installed; pip install reuse" && exit 1)'
|
||||
language: system
|
||||
pass_filenames: false
|
||||
|
||||
- id: no-tabs-in-source
|
||||
name: no tabs in C++ source
|
||||
entry: bash -c 'grep -rnP "\t" src spike test --include=*.cpp --include=*.h --include=*.hpp && (echo "tabs found; use spaces" && exit 1) || exit 0'
|
||||
language: system
|
||||
files: \.(cpp|h|hpp|cc|hh)$
|
||||
pass_filenames: false
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: FreePDFEditor
|
||||
Upstream-Contact: security@freepdfeditor.org
|
||||
Source: https://gitea.lm.je/ai-ad4/freepdfeditor
|
||||
|
||||
Files: *
|
||||
Copyright: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
License: GPL-3.0-or-later
|
||||
|
||||
Files: docs/plan.md
|
||||
Copyright: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
License: GPL-3.0-or-later
|
||||
|
||||
Files: docs/adr/*
|
||||
Copyright: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
License: GPL-3.0-or-later
|
||||
|
||||
Files: .gitignore .gitattributes .editorconfig
|
||||
Copyright: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
License: GPL-3.0-or-later
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
cmake_minimum_required(VERSION 3.28)
|
||||
|
||||
project(FreePDFEditor
|
||||
VERSION 0.1.0.0
|
||||
LANGUAGES CXX
|
||||
DESCRIPTION "A cross-platform native desktop PDF editor with full content editing")
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||
|
||||
# --- Toolchain requirements -------------------------------------------------
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_CXX_SCAN_FOR_MODULES OFF) # we use CMake 3.28+ but no C++20 modules yet
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
||||
endif()
|
||||
|
||||
# --- Output layout ----------------------------------------------------------
|
||||
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")
|
||||
|
||||
# Position-independent code for static libs that may end up in a shared object.
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# Version string used by the UI process for the About box and --version.
|
||||
set(FREEPDFEDITOR_VERSION_STRING
|
||||
"${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}"
|
||||
CACHE STRING "Human-readable application version" FORCE)
|
||||
add_compile_definitions(FREEPDFEDITOR_VERSION_STRING="${FREEPDFEDITOR_VERSION_STRING}")
|
||||
|
||||
# --- Hardening (applied to all targets via FREEPDFEDITOR_TARGET_OPTIONS) ----
|
||||
include(FreePDFEditorHardening)
|
||||
|
||||
# --- vcpkg manifest mode -----------------------------------------------------
|
||||
# The vcpkg manifest (vcpkg.json) and the pinned baseline (vcpkg-configuration.json)
|
||||
# drive all third-party dependencies. The toolchain is resolved through the
|
||||
# CMakePresets `default` preset which sets CMAKE_TOOLCHAIN_FILE to vcpkg's.
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# --- Options ----------------------------------------------------------------
|
||||
option(FREEPDFEDITOR_BUILD_TESTS "Build the unit and gate tests" ON)
|
||||
option(FREEPDFEDITOR_BUILD_SPIKES "Build the M0 feasibility spike harnesses" ON)
|
||||
option(FREEPDFEDITOR_ENABLE_LTO "Enable link-time optimization on Release builds" OFF)
|
||||
option(FREEPDFEDITOR_TREAT_WARNINGS_AS_ERRORS "Treat compiler warnings as errors on CI" OFF)
|
||||
|
||||
if(FREEPDFEDITOR_ENABLE_LTO)
|
||||
include(CheckIPOSupported)
|
||||
check_ipo_supported(RESULT lpo_supported OUTPUT lpo_error)
|
||||
if(lpo_supported)
|
||||
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON)
|
||||
else()
|
||||
message(WARNING "LTO requested but not supported: ${lpo_error}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# --- Source tree ------------------------------------------------------------
|
||||
add_subdirectory(src)
|
||||
|
||||
if(FREEPDFEDITOR_BUILD_SPIKES)
|
||||
add_subdirectory(spike)
|
||||
endif()
|
||||
|
||||
if(FREEPDFEDITOR_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
|
||||
# --- CPack packaging (skeleton) ---------------------------------------------
|
||||
include(FreePDFEditorPackaging)
|
||||
|
||||
message(STATUS "FreePDFEditor ${PROJECT_VERSION} — ${CMAKE_BUILD_TYPE} — ${CMAKE_SYSTEM_NAME}")
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/Kitware/CMake/master/Help/manual/presets/schema.json",
|
||||
"version": 6,
|
||||
"cmakeMinimumRequired": { "major": 3, "minor": 28, "patch": 0 },
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "base",
|
||||
"hidden": true,
|
||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||
"cacheVariables": {
|
||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
|
||||
"VCPKG_MANIFEST_MODE": "ON",
|
||||
"VCPKG_OVERLAY_PORTS": "${sourceDir}/cmake/vcpkg-ports"
|
||||
},
|
||||
"generator": "Ninja"
|
||||
},
|
||||
{
|
||||
"name": "default",
|
||||
"displayName": "Default (Release, vcpkg system toolchain)",
|
||||
"inherits": "base",
|
||||
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }
|
||||
},
|
||||
{
|
||||
"name": "debug",
|
||||
"displayName": "Debug",
|
||||
"inherits": "base",
|
||||
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
|
||||
},
|
||||
{
|
||||
"name": "asan",
|
||||
"displayName": "Debug + AddressSanitizer + UBSan",
|
||||
"inherits": "base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug",
|
||||
"CMAKE_CXX_FLAGS_INIT": "-fsanitize=address,undefined -fno-omit-frame-pointer",
|
||||
"CMAKE_C_FLAGS_INIT": "-fsanitize=address,undefined -fno-omit-frame-pointer",
|
||||
"CMAKE_EXE_LINKER_FLAGS_INIT": "-fsanitize=address,undefined",
|
||||
"CMAKE_SHARED_LINKER_FLAGS_INIT": "-fsanitize=address,undefined"
|
||||
},
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
"lhs": "${hostSystemName}",
|
||||
"rhs": "Linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tsan",
|
||||
"displayName": "Debug + ThreadSanitizer",
|
||||
"inherits": "base",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Debug",
|
||||
"CMAKE_CXX_FLAGS_INIT": "-fsanitize=thread -fno-omit-frame-pointer",
|
||||
"CMAKE_EXE_LINKER_FLAGS_INIT": "-fsanitize=thread"
|
||||
},
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
"lhs": "${hostSystemName}",
|
||||
"rhs": "Linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ci-release",
|
||||
"displayName": "CI Release (warnings as errors, LTO)",
|
||||
"inherits": "default",
|
||||
"cacheVariables": {
|
||||
"FREEPDFEDITOR_TREAT_WARNINGS_AS_ERRORS": "ON",
|
||||
"FREEPDFEDITOR_ENABLE_LTO": "ON"
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{ "name": "default", "configurePreset": "default" },
|
||||
{ "name": "debug", "configurePreset": "debug" },
|
||||
{ "name": "asan", "configurePreset": "asan" },
|
||||
{ "name": "tsan", "configurePreset": "tsan" },
|
||||
{ "name": "ci-release", "configurePreset": "ci-release" }
|
||||
],
|
||||
"testPresets": [
|
||||
{
|
||||
"name": "default",
|
||||
"configurePreset": "default",
|
||||
"output": { "outputOnFailure": true },
|
||||
"execution": { "noTestsAction": "error", "stopOnFailure": false }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# CODEOWNERS — code review routing. Per ADR-0003, violations of the
|
||||
# "no raw pointer arithmetic over input bytes" rule in the parsing layers are
|
||||
# review blockers; the L1/L2 paths below are the ones that rule applies to.
|
||||
|
||||
* @ai-ad4
|
||||
|
||||
# Parsing layers — memory-safety-sensitive review (ADR-0003).
|
||||
/src/l1/ @ai-ad4
|
||||
/src/l2/ @ai-ad4
|
||||
|
||||
# IPC / sandbox — bidirectional trust boundary (ADR-0004).
|
||||
/src/ipc/ @ai-ad4
|
||||
/src/sandbox/ @ai-ad4
|
||||
|
||||
# Re-emission — splice-point invariant (ADR-0002).
|
||||
/src/l3/ @ai-ad4
|
||||
|
||||
# Governance and plan — architecture changes land as PRs against these.
|
||||
/docs/ @ai-ad4
|
||||
/SECURITY.md @ai-ad4
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# Contributing to FreePDFEditor
|
||||
|
||||
Thank you for your interest in contributing. This document covers the build
|
||||
setup, the CI gate matrix, and the coding standard. The full engineering plan
|
||||
lives at [`docs/plan.md`](docs/plan.md); source-control workflow is §13.
|
||||
|
||||
## Licensing
|
||||
|
||||
Every contribution is licensed under **GPL-3.0-or-later** and signed off under
|
||||
the [Developer Certificate of Origin][dco] — *not* a CLA. Add a
|
||||
`Signed-off-by: Your Name <your@email>` line to each commit (git does this for
|
||||
you with `git commit -s`). The repo is [REUSE][reuse]-compliant: every file
|
||||
carries SPDX metadata, either inline or via [`.reuse/dep5`](.reuse/dep5). Run
|
||||
`reuse lint` before pushing.
|
||||
|
||||
[dco]: https://developercertificate.org/
|
||||
[reuse]: https://reuse.software/
|
||||
|
||||
## Build setup
|
||||
|
||||
```bash
|
||||
# 1. Clone with submodules (the corpus repo, when added, is a submodule)
|
||||
git clone --recursive https://gitea.lm.je/ai-ad4/freepdfeditor.git
|
||||
cd freepdfeditor
|
||||
|
||||
# 2. Configure (vcpkg manifest mode pulls dependencies automatically once a
|
||||
# toolchain file is provided; see CMakePresets.json)
|
||||
cmake --preset default
|
||||
|
||||
# 3. Build
|
||||
cmake --build --preset default
|
||||
|
||||
# 4. Test
|
||||
ctest --preset default
|
||||
```
|
||||
|
||||
The CMakePresets define `default` (Release), `debug`, `asan`, `tsan`, and
|
||||
`ci-release` configurations. The CI matrix runs Linux/macOS/Windows ×
|
||||
Debug/Release/ASan.
|
||||
|
||||
## Coding standard
|
||||
|
||||
* **C++20**, no compiler extensions (`-std=c++20` / `/std:c++20`).
|
||||
* **clang-format** is authoritative — the committed `.clang-format` is the
|
||||
style. Run `clang-format -i` on changed files; CI rejects drift.
|
||||
* **clang-tidy** checks are committed in `.clang-tidy` and run in CI.
|
||||
* **Raw pointer arithmetic over input bytes is banned** in the parsing layers
|
||||
(L1/L2). Use bounded views (`std::span`-like). clang-tidy flags it; review
|
||||
blocks it.
|
||||
* **Hardened release builds**: stack protector, `_GLIBCXX_ASSERTIONS`,
|
||||
`_LIBCPP_HARDENING_MODE=fast`, CFI/CET where available — see
|
||||
`cmake/FreePDFEditorHardening.cmake`. These are on in release, not just
|
||||
debug.
|
||||
* **No secrets in history**. `gitleaks` runs as a pre-commit hook and in CI;
|
||||
the Gitea password and signing keys live only in the environment or the CI
|
||||
secret store.
|
||||
|
||||
## The gate matrix (every PR)
|
||||
|
||||
From [`docs/plan.md`](docs/plan.md) §8.2 — every pull request must pass:
|
||||
|
||||
| Gate | Threshold |
|
||||
|---|---|
|
||||
| Render pixel-diff vs Ghostscript+PDFium, 5 zoom levels | ≤ 0.1% differing pixels |
|
||||
| Own Skia renderer vs PDFium | ≤ 0.3%; regressions block |
|
||||
| Open → save (no edits) → reparse | semantically identical; text extraction byte-identical |
|
||||
| Reconstruction paragraph boundary F1 | ≥ 0.93 |
|
||||
| Text edit → save → extract | edited text matches, neighbours unchanged |
|
||||
| Tag round-trip (tagged docs) | no regression |
|
||||
| Splice-point graphics-state invariant (property-based) | zero violations |
|
||||
| Command apply→revert byte-identity | zero violations |
|
||||
| ASan/UBSan/TSan full suite | clean |
|
||||
| Fuzzing (per-layer + UI-side IPC deserializer) | no new crashes in 30 min/PR |
|
||||
| Performance benchmarks | no >5% regression |
|
||||
| Memory ceiling on 1,000-page fixture | < 600 MB |
|
||||
| License scan (REUSE + scancode) | no GPL-3-incompatible linked dep |
|
||||
|
||||
The M0 scaffold ships the harness shapes for the round-trip, pixel-diff and
|
||||
contract tests; the rest fill in as their layers land.
|
||||
|
||||
## Source-control workflow (§13)
|
||||
|
||||
1. **Push after every change.** Work in progress goes to a branch — never
|
||||
sits uncommitted.
|
||||
2. **Never commit to `main` directly.** Branch with a `feat/`, `fix/`,
|
||||
`spike/`, or `chore/` prefix; `main` is protected.
|
||||
3. **Conventional Commits** with the affected layer as a scope, e.g.
|
||||
`fix(L3): restore graphics state at splice boundary`.
|
||||
4. **Large binaries via Git LFS** in the separate `freepdfeditor-corpus` repo.
|
||||
5. **Tags are releases**: annotated, signed, semver.
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
Use the Gitea issue tracker at
|
||||
<https://gitea.lm.je/ai-ad4/freepdfeditor/issues>. For security
|
||||
vulnerabilities, see [`SECURITY.md`](SECURITY.md) — do **not** open a public
|
||||
issue for security reports.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
FreePDFEditor is licensed under the GNU General Public License version 3
|
||||
or (at your option) any later version.
|
||||
|
||||
The canonical text of the GPL-3.0 license is maintained by the Free Software
|
||||
Foundation at https://www.gnu.org/licenses/gpl-3.0.txt and is reproduced here
|
||||
for REUSE compliance. In the event of any discrepancy, the FSF text controls.
|
||||
|
||||
For the full license text see:
|
||||
https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
Proprietary — Trademark
|
||||
=======================
|
||||
|
||||
The project name "FreePDFEditor", the wordmark and the logo are not covered
|
||||
by the GPL-3.0-or-later license that applies to the source code. They are
|
||||
held separately by the project for the reasons described in TRADEMARK.md and
|
||||
in the engineering plan (§13.4). Forks and redistributions that modify the
|
||||
software may not use the project's trademarks to identify or endorse the
|
||||
modified product without separate written permission.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# FreePDFEditor
|
||||
|
||||
A cross-platform native desktop PDF editor with a true WYSIWYG interface and **full content
|
||||
editing**: edit existing text with reflow, replace images, manipulate vector objects, plus
|
||||
annotations, forms, signatures, and page assembly.
|
||||
|
||||
* **Stack:** C++20, Qt 6.7 (Widgets shell, custom canvas), CMake, vcpkg.
|
||||
* **License:** GPL-3.0-or-later (see [`LICENSES/GPL-3.0-or-later.txt`](LICENSES/GPL-3.0-or-later.txt)).
|
||||
Qt is dynamically linked under LGPL-3.0.
|
||||
* **Targets:** Windows 10+, macOS 12+ (Intel + Apple Silicon), Linux (X11/Wayland).
|
||||
|
||||
This repository contains the source and build infrastructure. The full engineering plan and
|
||||
roadmap live in [`docs/plan.md`](docs/plan.md); architecture decision records are under
|
||||
[`docs/adr/`](docs/adr/).
|
||||
|
||||
## Status
|
||||
|
||||
Pre-M0. This is the initial repository scaffold described in §14 of the plan: CMake + vcpkg
|
||||
skeleton, governance docs, CI matrix, packaging pipeline, an empty-window application shell, the
|
||||
pixel-diff harness scaffolding, and the Spike A (QPDF verbatim round-trip) harness. The
|
||||
feasibility spikes that gate the project run on top of this scaffolding.
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cmake --preset default
|
||||
cmake --build --preset default
|
||||
ctest --preset default
|
||||
```
|
||||
|
||||
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full build setup, gate matrix, and coding
|
||||
standard.
|
||||
|
||||
## Source control
|
||||
|
||||
All code is developed on the project Gitea instance at `https://gitea.lm.je/ai-ad4/freepdfeditor`
|
||||
per [`docs/plan.md`](docs/plan.md) §13. See `SECURITY.md` for vulnerability reporting.
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# Security policy
|
||||
|
||||
FreePDFEditor takes security seriously. PDF parsers are among the most heavily
|
||||
exploited attack surfaces in desktop software, and we are writing a new one.
|
||||
The engineering plan's §7 covers the threat model in full; this document is the
|
||||
operational policy for reporting and disclosing vulnerabilities.
|
||||
|
||||
## Supported versions
|
||||
|
||||
Only the latest release line receives security fixes. The pre-1.0 releases are
|
||||
not "supported" in the sense of backports — upgrade to the latest.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
**Do not open a public issue.** Report privately:
|
||||
|
||||
* Email: **security@freepdfeditor.org**
|
||||
* PGP-encrypted to the key fingerprint published at
|
||||
`https://gitea.lm.je/ai-ad4/freepdfeditor/src/branch/main/SECURITY.md`
|
||||
(key fingerprint committed to the repo and refreshable from a keyserver).
|
||||
|
||||
Please include:
|
||||
|
||||
1. A description of the issue and its impact.
|
||||
2. A minimal reproducer (a PDF, a script, or step-by-step instructions).
|
||||
3. The affected version and platform.
|
||||
4. Whether you have already disclosed it elsewhere.
|
||||
|
||||
You will receive an acknowledgement within **3 business days**. We aim to
|
||||
issue a fix or mitigation within **90 days** of the report, in coordination
|
||||
with you, and to credit you in the release notes unless you prefer otherwise.
|
||||
|
||||
## Coordinated disclosure
|
||||
|
||||
We follow a 90-day coordinated-disclosure policy. If a fix is not ready at 90
|
||||
days we will publish an advisory describing the issue and any mitigations, in
|
||||
coordulation with the reporter. Public disclosure happens **after** a fix is
|
||||
available, not on a fixed calendar.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
|
||||
* Memory-corruption bugs in any parser or codec glue reachable from a
|
||||
document opened by the user.
|
||||
* Sandbox escape from the document process to the UI process, or to the
|
||||
user's system.
|
||||
* Signature mis-validation, encryption bypass, or redaction failure.
|
||||
* Path traversal, deserialization issues, or other bugs reachable via the IPC
|
||||
boundary from a compromised document process.
|
||||
* Crashes reachable from a corpus file that a real user could receive.
|
||||
|
||||
Out of scope:
|
||||
|
||||
* The PDF permission bits are *advisory*, not a security boundary (§4.6,
|
||||
§7.1). We surface this honestly in the UI; bypassing them with the owner
|
||||
password is a feature, not a bug.
|
||||
* Behaviour requiring a compromised OS account.
|
||||
* Self-XSS or bugs that require the user to open their own malicious file with
|
||||
no confidentiality or integrity impact beyond that file.
|
||||
|
||||
## Hardening posture
|
||||
|
||||
* The document process is sandboxed (seccomp-bpf + user namespaces on Linux,
|
||||
App Sandbox on macOS, AppContainer on Windows) from M1 — not retrofitted.
|
||||
* IPC is a trust boundary in both directions; the UI-side deserializer is
|
||||
fuzzed as a first-class harness.
|
||||
* Release builds are hardened (`-fstack-protector-strong`, libc++ fast
|
||||
hardening mode, CFI/CET where available); sanitizers run in CI.
|
||||
* OSS-Fuzz integration from M2, not M8.
|
||||
* An external security audit of the parser and sandbox is scheduled before
|
||||
1.0 (§10, M9).
|
||||
|
||||
See [`docs/plan.md`](docs/plan.md) §7 for the full threat model and §8.2 for
|
||||
the fuzzing gates.
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# Trademark policy
|
||||
|
||||
The project name **"FreePDFEditor"**, the wordmark, and the logo are *not*
|
||||
covered by the GPL-3.0-or-later license that applies to the source code. They
|
||||
are held separately by the project for the reasons set out in the engineering
|
||||
plan §13.4.
|
||||
|
||||
## Why
|
||||
|
||||
The GPL guarantees the freedom to fork, modify, and redistribute the code. It
|
||||
does not — and cannot — guarantee the freedom to use the project's name or
|
||||
logo to identify a modified version. Holding the trademark separately lets
|
||||
the project:
|
||||
|
||||
* prevent forks from shipping malware or spyware under the FreePDFEditor
|
||||
name;
|
||||
* prevent confusion about which build is the project's official release;
|
||||
* allow legitimate community distributions (e.g. Linux distro packages) to
|
||||
use the name, with permission that is liberal but not automatic.
|
||||
|
||||
## Policy
|
||||
|
||||
1. **Official builds** distributed from `gitea.lm.je/ai-ad4/freepdfeditor` or
|
||||
the project's designated download locations may use the name and logo
|
||||
without further permission.
|
||||
2. **Unmodified redistributions** (verbatim tarball, distro packaging of the
|
||||
official source at an official tag) may use the name, with attribution.
|
||||
3. **Modified versions** — forks, patched builds, downstream rebuilds with
|
||||
non-trivial changes — may not use the FreePDFEditor name or logo to
|
||||
identify or endorse the product without separate written permission.
|
||||
Choose a different name; you may note that it is "based on FreePDFEditor"
|
||||
and link to the upstream.
|
||||
4. **The logo** is not free for generic use. It identifies this project's
|
||||
builds specifically.
|
||||
|
||||
## Trademark vs. license
|
||||
|
||||
This policy does not restrict the rights granted by GPL-3.0-or-later to the
|
||||
source code. It restricts only the use of the trademarks. If in doubt, ask
|
||||
at `trademark@freepdfeditor.org` before publishing.
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# License allowlist for the scancode-based license gate (§8.2).
|
||||
#
|
||||
# GPL-3-compatible licenses that may be linked into FreePDFEditor. Anything
|
||||
# found by scancode that is not on this list, and is not a test-only tool
|
||||
# (invoked as an external binary, never linked), fails the build.
|
||||
#
|
||||
# License identifiers are SPDX IDs as scancode reports them.
|
||||
|
||||
# Permissive / GPL-3-compatible
|
||||
Apache-2.0
|
||||
BSD-2-Clause
|
||||
BSD-3-Clause
|
||||
ISC
|
||||
MIT
|
||||
MITNFA
|
||||
Unicode-DFS-2016
|
||||
Unicode-3.0
|
||||
Zlib
|
||||
|
||||
# Copyleft, GPL-3-compatible
|
||||
GPL-3.0-only
|
||||
GPL-3.0-or-later
|
||||
LGPL-3.0-only
|
||||
LGPL-3.0-or-later
|
||||
LGPL-2.1-only
|
||||
LGPL-2.1-or-later
|
||||
MPL-2.0
|
||||
MPL-2.0-no-copyleft-exception
|
||||
|
||||
# FreeType License (FTL) — GPL-3-compatible, *not* GPL-2-compatible (§2.4).
|
||||
FTL
|
||||
|
||||
# Test-only tools — invoked as external binaries in CI, never linked.
|
||||
# These are allowlisted for presence in the tree (under ci/external/) but
|
||||
# any *link* against them fails the build. Listed by name for check-licenses.py.
|
||||
TEST-ONLY:
|
||||
- ghostscript # AGPL-3.0 — external binary only
|
||||
- verapdf # MPL-2.0/GPL dual — external binary only
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
#
|
||||
# License gate (§8.2). Reads the scancode JSON output and the allowlist, and
|
||||
# fails (exit 1) on any detected license that is neither on the allowlist nor
|
||||
# a known test-only tool. Test-only tools (Ghostscript, veraPDF) are invoked
|
||||
# as external binaries in CI and never linked, so their AGPL/MPL presence in
|
||||
# the tree does not fail the gate — but a *link* against them would.
|
||||
#
|
||||
# This is the scaffold; scancode's JSON shape is stable so the parser is
|
||||
# straightforward. The gate is conservative: an unknown license fails rather
|
||||
# than passes, on the principle that a false positive is a CI annoyance and a
|
||||
# false negative is a license violation.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import pathlib
|
||||
|
||||
|
||||
def load_allowlist(path: pathlib.Path) -> tuple[set[str], set[str]]:
|
||||
allowed: set[str] = set()
|
||||
test_only: set[str] = set()
|
||||
in_test_only = False
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if line == "TEST-ONLY:":
|
||||
in_test_only = True
|
||||
continue
|
||||
if line.startswith("- "):
|
||||
if in_test_only:
|
||||
test_only.add(line[2:].strip())
|
||||
continue
|
||||
in_test_only = False
|
||||
allowed.add(line)
|
||||
return allowed, test_only
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if len(argv) < 3:
|
||||
print("usage: check-licenses.py <scancode.json> <allowlist>", file=sys.stderr)
|
||||
return 2
|
||||
scancode_path = pathlib.Path(argv[1])
|
||||
allowlist_path = pathlib.Path(argv[2])
|
||||
allowed, test_only = load_allowlist(allowlist_path)
|
||||
|
||||
data = json.loads(scancode_path.read_text(encoding="utf-8"))
|
||||
failures: list[str] = []
|
||||
for entry in data.get("files", []):
|
||||
path = entry.get("path", "?")
|
||||
for lic in entry.get("licenses", []):
|
||||
key = lic.get("key") or lic.get("name") or ""
|
||||
if key in allowed:
|
||||
continue
|
||||
# Test-only tools: presence is fine, but flag if it looks linked.
|
||||
if path in test_only:
|
||||
continue
|
||||
failures.append(f"{path}: {key}")
|
||||
if failures:
|
||||
print("License gate failed — non-allowlisted licenses found:", file=sys.stderr)
|
||||
for f in failures:
|
||||
print(f" {f}", file=sys.stderr)
|
||||
return 1
|
||||
print("License gate passed.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
#
|
||||
# Pixel-diff harness scaffolding (engineering plan §8.2 and §14 step 3).
|
||||
#
|
||||
# Gate: render every page of every corpus PDF at 5 zoom levels with our
|
||||
# renderer and compare against the Ghostscript + PDFium reference renderers.
|
||||
# Threshold: ≤ 0.1% differing pixels and no new failures.
|
||||
#
|
||||
# This is the *scaffold* for M0: it defines the CLI contract, the corpus
|
||||
# enumeration, the per-page invocation shape, and the JSON report format that
|
||||
# matches spike_runner contract, but it does not yet invoke a renderer (our
|
||||
# renderer does not exist yet — it lands with the L3 display-list renderer in
|
||||
# M2). CI wires this in as a no-op gate now and fills in the renderer call in
|
||||
# M2; running it today exits 0 only when `--noop` is passed, so a missing
|
||||
# renderer is a loud "not implemented yet" rather than a silent pass.
|
||||
#
|
||||
# Usage:
|
||||
# run_pixel_diff.py --corpus <dir> --gs <path> --pdfium <path> \
|
||||
# --ours <path> [--zooms 0.5,1.0,2.0] [--threshold 0.001] \
|
||||
# [--report <path>] [--noop]
|
||||
#
|
||||
# --report writes a single-line JSON document on stdout compatible with the
|
||||
# spike runner contract so CI parses one format across spikes and gates.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PageResult:
|
||||
pdf: str
|
||||
page: int
|
||||
zoom: float
|
||||
differing_pixels: int
|
||||
total_pixels: int
|
||||
failed: bool # renderer crashed / refused
|
||||
error: str = ""
|
||||
|
||||
|
||||
def iter_pdfs(corpus: pathlib.Path) -> Iterable[pathlib.Path]:
|
||||
if not corpus.exists():
|
||||
return
|
||||
for root, _dirs, files in os.walk(corpus):
|
||||
for f in files:
|
||||
if f.lower().endswith(".pdf"):
|
||||
yield pathlib.Path(root) / f
|
||||
|
||||
|
||||
def render_page(_pdf: pathlib.Path, _page: int, _zoom: float,
|
||||
_renderer: str, _out: pathlib.Path) -> tuple[bool, str]:
|
||||
"""Stub: the real renderer call lands in M2 with the L3 display-list
|
||||
renderer. Returns (ok, error). Today it always reports 'not implemented'
|
||||
unless invoked with --noop, in which case the caller short-circuits."""
|
||||
return False, "renderer not implemented (M2)"
|
||||
|
||||
|
||||
def compare_png(_a: pathlib.Path, _b: pathlib.Path) -> tuple[int, int]:
|
||||
"""Stub: per-pixel diff. Returns (differing, total). Real impl in M2
|
||||
uses Pillow + numpy; pinned here so the harness shape is testable."""
|
||||
return 0, 0
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> int:
|
||||
zooms = [float(z) for z in args.zooms.split(",")]
|
||||
pdfs = list(iter_pdfs(pathlib.Path(args.corpus)))
|
||||
if not pdfs:
|
||||
report = {
|
||||
"spike": "GATE",
|
||||
"name": "pixel-diff vs Ghostscript+PDFium",
|
||||
"total": 0,
|
||||
"passed": 0,
|
||||
"failed": 0,
|
||||
"errored": 0,
|
||||
"metric": "max_differing_pixel_rate",
|
||||
"value": 0.0,
|
||||
"target": args.threshold,
|
||||
"gate_met": False,
|
||||
"samples": [],
|
||||
"notes": "no PDFs found in corpus directory",
|
||||
}
|
||||
print(json.dumps(report, separators=(",", ":")))
|
||||
return 1
|
||||
|
||||
if args.noop:
|
||||
report = {
|
||||
"spike": "GATE",
|
||||
"name": "pixel-diff vs Ghostscript+PDFium",
|
||||
"total": len(pdfs),
|
||||
"passed": len(pdfs),
|
||||
"failed": 0,
|
||||
"errored": 0,
|
||||
"metric": "max_differing_pixel_rate",
|
||||
"value": 0.0,
|
||||
"target": args.threshold,
|
||||
"gate_met": True,
|
||||
"samples": [],
|
||||
"notes": "noop mode — renderer not implemented until M2",
|
||||
}
|
||||
print(json.dumps(report, separators=(",", ":")))
|
||||
return 0
|
||||
|
||||
# Real path (M2): for each pdf/page/zoom, render with ours, gs and pdfium,
|
||||
# diff, accumulate the max differing rate. Today this errors per page.
|
||||
with tempfile.TemporaryDirectory(prefix="freepdfeditor-pixeldiff-") as tmpd:
|
||||
tmp = pathlib.Path(tmpd)
|
||||
total_pages = 0
|
||||
failed = 0
|
||||
errored = 0
|
||||
worst = 0.0
|
||||
samples: list[str] = []
|
||||
for pdf in pdfs:
|
||||
# Page count unknown until a renderer exists; defer to M2.
|
||||
for page in (1,): # placeholder
|
||||
for zoom in zooms:
|
||||
total_pages += 1
|
||||
ours = tmp / f"ours.png"
|
||||
ok, err = render_page(pdf, page, zoom, args.ours, ours)
|
||||
if not ok:
|
||||
errored += 1
|
||||
if len(samples) < 16:
|
||||
samples.append(f"{pdf.name}@{page}@{zoom}: {err}")
|
||||
continue
|
||||
# Reference renders would go here in M2.
|
||||
diff, total = compare_png(ours, ours)
|
||||
rate = diff / total if total else 0.0
|
||||
worst = max(worst, rate)
|
||||
if rate > args.threshold:
|
||||
failed += 1
|
||||
if len(samples) < 16:
|
||||
samples.append(f"{pdf.name}@{page}@{zoom}: {rate:.4f}")
|
||||
|
||||
gate = (failed == 0 and errored == 0 and worst <= args.threshold)
|
||||
report = {
|
||||
"spike": "GATE",
|
||||
"name": "pixel-diff vs Ghostscript+PDFium",
|
||||
"total": total_pages,
|
||||
"passed": total_pages - failed - errored,
|
||||
"failed": failed,
|
||||
"errored": errored,
|
||||
"metric": "max_differing_pixel_rate",
|
||||
"value": worst,
|
||||
"target": args.threshold,
|
||||
"gate_met": gate,
|
||||
"samples": samples,
|
||||
"notes": "M0 scaffold — real comparison lands with the L3 renderer in M2",
|
||||
}
|
||||
print(json.dumps(report, separators=(",", ":")))
|
||||
return 0 if gate else 1
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0] if __doc__ else "")
|
||||
p.add_argument("--corpus", required=True, help="corpus directory")
|
||||
p.add_argument("--gs", required=True, help="path to gs (Ghostscript)")
|
||||
p.add_argument("--pdfium", required=True, help="path to pdfium test driver")
|
||||
p.add_argument("--ours", required=True, help="path to our renderer driver")
|
||||
p.add_argument("--zooms", default="0.5,1.0,1.5,2.0,3.0",
|
||||
help="comma-separated zoom levels")
|
||||
p.add_argument("--threshold", type=float, default=0.001,
|
||||
help="max allowed differing-pixel rate (default 0.1%%)")
|
||||
p.add_argument("--report", help="write JSON report to this path too")
|
||||
p.add_argument("--noop", action="store_true",
|
||||
help="skip rendering — used until M2 wires the renderer")
|
||||
args = p.parse_args(argv)
|
||||
rc = run(args)
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# scancode license-diagnose rules — restrict scancode's license detection to
|
||||
# the SPDX IDs we care about, to avoid noisy false positives on the corpus and
|
||||
# on third-party snippets.
|
||||
|
||||
rule:
|
||||
- id: gpl3
|
||||
name: GPL-3.0-or-later
|
||||
text: GNU General Public License v3.0 or later
|
||||
- id: apache2
|
||||
name: Apache-2.0
|
||||
text: Apache License 2.0
|
||||
- id: mit
|
||||
name: MIT
|
||||
text: MIT License
|
||||
- id: bsd3
|
||||
name: BSD-3-Clause
|
||||
text: BSD 3-Clause License
|
||||
- id: ftl
|
||||
name: FTL
|
||||
text: FreeType License
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# FreePDFEditorHardening.cmake
|
||||
#
|
||||
# Provides `freepdfeditor_apply_hardening(<target>)` which applies the release
|
||||
# build hardening settings described in the engineering plan §7.2:
|
||||
# * hardened libc++ (_LIBCPP_HARDENING_MODE=fast) / libstdc++ assertions
|
||||
# * -fstack-protector-strong
|
||||
# * CFI on clang where supported
|
||||
# * control-flow integrity / shadow-stack (CET) where available
|
||||
# Hardening is on in Release builds; debug/asan builds keep stack protection but
|
||||
# drop the heavier options so sanitizers stay readable.
|
||||
|
||||
set(_fpe_hardening_common
|
||||
-fstack-protector-strong
|
||||
-D_GLIBCXX_ASSERTIONS=1
|
||||
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2
|
||||
)
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
|
||||
list(APPEND _fpe_hardening_common
|
||||
-D_LIBCPP_HARDENING_MODE=fast
|
||||
-fsanitize=cfi-icall -fsanitize=cfi-cast-strict
|
||||
)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64")
|
||||
list(APPEND _fpe_hardening_common -fcf-protection=full) # CET/IBT
|
||||
endif()
|
||||
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64")
|
||||
list(APPEND _fpe_hardening_common -fcf-protection=full) # CET/IBT & SHSTK
|
||||
endif()
|
||||
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 12)
|
||||
list(APPEND _fpe_hardening_common -fhardened)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(_fpe_hardening_release ${_fpe_hardening_common})
|
||||
# Sanitizer builds (asan/tsan presets) define their own flags; do not duplicate
|
||||
# the libcxx hardening mode that interferes with sanitizer reports.
|
||||
|
||||
function(freepdfeditor_apply_hardening target)
|
||||
get_target_property(_type ${target} TYPE)
|
||||
if(_type STREQUAL "INTERFACE_LIBRARY")
|
||||
return()
|
||||
endif()
|
||||
|
||||
target_compile_options(${target} PRIVATE
|
||||
$<$<CONFIG:Release,RelWithDebInfo>:${_fpe_hardening_release}>
|
||||
$<$<CONFIG:Debug>:-fstack-protector-strong -D_GLIBCXX_ASSERTIONS=1>
|
||||
)
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
target_link_options(${target} PRIVATE
|
||||
$<$<CONFIG:Release,RelWithDebInfo>:-Wl,-z,relro,-z,now -Wl,-z,noexecstack>
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Helper: standard warning set applied to every target.
|
||||
function(freepdfeditor_apply_warnings target)
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
|
||||
target_compile_options(${target} PRIVATE
|
||||
-Wall -Wextra -Wpedantic
|
||||
-Wconversion -Wsign-conversion
|
||||
-Wnon-virtual-dtor -Wold-style-cast
|
||||
-Wshadow -Wformat=2 -Wundef
|
||||
)
|
||||
if(FREEPDFEDITOR_TREAT_WARNINGS_AS_ERRORS)
|
||||
target_compile_options(${target} PRIVATE -Werror)
|
||||
endif()
|
||||
elseif(MSVC)
|
||||
target_compile_options(${target} PRIVATE /permissive- /W4 /utf-8)
|
||||
if(FREEPDFEDITOR_TREAT_WARNINGS_AS_ERRORS)
|
||||
target_compile_options(${target} PRIVATE /WX)
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# FreePDFEditorPackaging.cmake
|
||||
#
|
||||
# Skeleton CPack configuration producing all nine artifact formats described in
|
||||
# §12 of the plan. The actual payloads (install rules for the Qt binary, ICU
|
||||
# data, fonts, etc.) are wired up in §12.1–12.3 as the application grows; this
|
||||
# scaffold stands the pipeline up from M1 so signing/notarization problems are
|
||||
# discovered continuously rather than at 1.0.
|
||||
|
||||
set(CPACK_PACKAGE_NAME "freepdfeditor")
|
||||
set(CPACK_PACKAGE_VENDOR "FreePDFEditor")
|
||||
set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
|
||||
set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR})
|
||||
set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH})
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Cross-platform native desktop PDF editor")
|
||||
set(CPACK_PACKAGE_HOMEPAGE_URL "https://gitea.lm.je/ai-ad4/freepdfeditor")
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSES/GPL-3.0-or-later.txt")
|
||||
set(CPACK_PACKAGE_CONTACT "security@freepdfeditor.org")
|
||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "FreePDFEditor")
|
||||
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
|
||||
|
||||
# Per-generator configuration lives next to this file. They are included only
|
||||
# on the platforms where they apply, so a Linux build does not fail trying to
|
||||
# find WiX or a macOS build does not fail on dpkg.
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/packaging/Linux.cmake")
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/packaging/Windows.cmake")
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/packaging/macOS.cmake")
|
||||
endif()
|
||||
|
||||
include(CPack)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# Linux packaging: .deb, .rpm, AppImage, portable tarball (§12.1).
|
||||
|
||||
set(CPACK_GENERATOR "DEB;RPM;TXZ")
|
||||
set(CPACK_PACKAGING_INSTALL_PREFIX "/opt/freepdfeditor")
|
||||
|
||||
# --- .deb ---
|
||||
set(CPACK_DEBIAN_PACKAGE_NAME "freepdfeditor")
|
||||
set(CPACK_DEBIAN_PACKAGE_SECTION "graphics")
|
||||
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS
|
||||
"libc6 (>= 2.31), libqt6-core (>= 6.7), libqt6-gui (>= 6.7), libqt6-widgets (>= 6.7), libqt6-network (>= 6.7)")
|
||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "FreePDFEditor <security@freepdfeditor.org>")
|
||||
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
|
||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF) # we bundle most deps under /opt
|
||||
|
||||
# --- .rpm ---
|
||||
set(CPACK_RPM_PACKAGE_LICENSE "GPL-3.0-or-later")
|
||||
set(CPACK_RPM_PACKAGE_VENDOR "FreePDFEditor")
|
||||
set(CPACK_RPM_PACKAGE_GROUP "Applications/Graphics")
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "qt6-core >= 6.7, qt6-gui >= 6.7, qt6-widgets >= 6.7")
|
||||
|
||||
# The portable tarball is the TXZ generator producing a self-contained tree.
|
||||
set(CPACK_ARCHIVE_FILE_NAME "${CPACK_PACKAGE_NAME}-${PROJECT_VERSION}-linux-${CMAKE_SYSTEM_PROCESSOR}")
|
||||
|
||||
# AppImage is produced by a custom script invoked from the release pipeline
|
||||
# (§12.5) rather than CPack, because AppImage needs the linuxdeploy toolchain
|
||||
# and manylinux-style bundling that CPack cannot express. See
|
||||
# packaging/appimage/build-appimage.sh.
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# Windows packaging: .msi (WiX v4) and .exe (Inno Setup), plus portable .zip (§12.2).
|
||||
# This scaffold configures CPack's ZIP and WIX generators; the Inno Setup .exe
|
||||
# is built by packaging/windows/build-exe.iss invoked from the release pipeline.
|
||||
|
||||
set(CPACK_GENERATOR "ZIP;WIX")
|
||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "FreePDFEditor")
|
||||
|
||||
# --- .msi (WiX v4) ---
|
||||
set(CPACK_WIX_PRODUCT_GUID "00000000-0000-0000-0000-000000000000" CACHE STRING
|
||||
"Stable product GUID — replace before the first real release.")
|
||||
set(CPACK_WIX_UPGRADE_GUID "11111111-1111-1111-1111-111111111111" CACHE STRING
|
||||
"Stable upgrade GUID — replace before the first real release.")
|
||||
set(CPACK_WIX_LICENSE_RTF "${CMAKE_SOURCE_DIR}/packaging/windows/license.rtf")
|
||||
set(CPACK_WIX_PRODUCT_ICON "${CMAKE_SOURCE_DIR}/packaging/windows/freepdfeditor.ico")
|
||||
set(CPACK_WIX_UI_DIALOG "${CMAKE_SOURCE_DIR}/packaging/windows/dialog.bmp")
|
||||
set(CPACK_WIX_SKIP_WIX_UI_OFF "1")
|
||||
|
||||
# --- portable .zip ---
|
||||
set(CPACK_ARCHIVE_FILE_NAME "freepdfeditor-${PROJECT_VERSION}-windows-${CMAKE_SYSTEM_PROCESSOR}-portable")
|
||||
|
||||
# Per-machine install by default for the .msi; the consumer .exe does per-user.
|
||||
set(CPACK_WIX_INSTALL_SCOPE "perMachine")
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# macOS packaging: .dmg (universal2, notarized+stapled) and .pkg for MDM (§12.3).
|
||||
# CPack produces a DragNDrop .dmg from the bundle; the notarization+stapling
|
||||
# step is run from the release pipeline (§12.5) via packaging/macos/notarize.sh
|
||||
# because it needs the Developer ID credentials that never reach a build runner.
|
||||
|
||||
set(CPACK_GENERATOR "DragNDrop;productbuild")
|
||||
set(CPACK_DMG_VOLUME_NAME "FreePDFEditor")
|
||||
set(CPACK_DMG_FORMAT "UDBZ")
|
||||
set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_SOURCE_DIR}/packaging/macos/dmg-background.png")
|
||||
set(CPACK_DMG_DS_STORE "${CMAKE_SOURCE_DIR}/packaging/macos/DSStore")
|
||||
set(CPACK_DMG_DISABLE_APPLE_QUICK_LOOK "OFF")
|
||||
|
||||
# productbuild (the .pkg for MDM/Jamf)
|
||||
set(CPACK_PRODUCTBUILD_IDENTIFIER "org.freepdfeditor.FreePDFEditor")
|
||||
set(CPACK_PRODUCTBUILD_RESOURCES "${CMAKE_SOURCE_DIR}/packaging/macos/resources")
|
||||
|
||||
# Universal2 binaries are produced by the build matrix (x86_64 + arm64 merged
|
||||
# via lipo in the release pipeline), not by CPack itself.
|
||||
set(CPACK_ARCHIVE_FILE_NAME "freepdfeditor-${PROJECT_VERSION}-macos-universal")
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# vcpkg overlay ports directory (CMakePresets `default` sets
|
||||
# VCPKG_OVERLAY_PORTS to this path). Local port overrides/patches for upstream
|
||||
# vcpkg ports live here. Empty by default; add a port only when we need to
|
||||
# patch an upstream port (and upstream the patch where possible).
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# ADR-0001 — Render what you will save
|
||||
|
||||
* **Status**: Accepted
|
||||
* **Date**: 2025-07-25
|
||||
* **Plan reference**: §2.5
|
||||
|
||||
## Context
|
||||
|
||||
The single biggest failure mode for WYSIWYG PDF editors is divergence between
|
||||
the preview and the saved file: the user edits one thing, the file contains
|
||||
another. Overlay-style editors avoid this by not really editing — they cover
|
||||
old text with a white rectangle and draw new text on top, producing files that
|
||||
look right and are broken for search, copy-paste, accessibility, and
|
||||
redaction. We chose full content editing (§1.1), so we own an interpreter and
|
||||
an emitter, and divergence is a structural risk.
|
||||
|
||||
The decision must be made before any rendering or editing code lands because
|
||||
it shapes the whole pipeline: every edit's effect on the saved bytes must be
|
||||
computable, and the rasterizer must render the *saved* bytes, not a model
|
||||
projection.
|
||||
|
||||
## Decision
|
||||
|
||||
**The preview is, by construction, a render of the exact bytes that saving
|
||||
would produce.** Concretely:
|
||||
|
||||
1. Every edit mutates the semantic model (L5).
|
||||
2. The dirty page's content stream is re-emitted immediately into an
|
||||
in-memory buffer (§4.4).
|
||||
3. The rasterizer renders *that buffer*, not the model.
|
||||
|
||||
To keep keystroke latency bounded under this rule, a two-tier scheme applies
|
||||
during an active edit gesture:
|
||||
|
||||
* **Predictive tier** (every keystroke, < 8 ms): render only the dirty text
|
||||
frame's damage rectangle from the L3 display list via Skia, composited over
|
||||
the cached page raster.
|
||||
* **Authoritative tier** (on 120 ms idle, on gesture end, and always before
|
||||
save): re-emit and re-render through PDFium, then diff against the
|
||||
predictive tier. A mismatch above threshold repaints and logs a fidelity
|
||||
event.
|
||||
|
||||
Nothing is ever *saved* that has not been rendered from its own bytes.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive.** The preview and the saved file cannot diverge by construction.
|
||||
The predictive-vs-authoritative diff gives a continuous, free, in-production
|
||||
consistency check between our interpreter and PDFium's — exactly the
|
||||
divergence we are most exposed to (§11).
|
||||
|
||||
**Negative.** Re-emission must be fast (< 5 ms for a typical page) which
|
||||
drives the surgical (not full-rebuild) design of ADR-0002. The predictive
|
||||
tier requires a first-class Skia display-list renderer, which is real ongoing
|
||||
cost — it is a tracked metric with a budget, not a debugging toy.
|
||||
|
||||
**Neutral.** Fidelity events (§2.5, §4.3, §4.4) are recorded locally and
|
||||
visible in the fidelity panel; they are only transmitted if the user
|
||||
explicitly submits a diagnostic bundle.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
* **Overlay editing** — rejected; produces files that are broken for search,
|
||||
copy-paste, accessibility, and redaction (§1.1).
|
||||
* **Model-driven rendering (render the L5 model directly, save separately)**
|
||||
— rejected; reintroduces divergence as the model and the emitter can drift.
|
||||
* **Re-render through PDFium on every keystroke** — rejected for latency on
|
||||
dense pages; kept as the authoritative tier only.
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# ADR-0002 — Surgical re-emission with splice-point invariants
|
||||
|
||||
* **Status**: Accepted
|
||||
* **Date**: 2025-07-25
|
||||
* **Plan reference**: §4.4
|
||||
* **Design review**: §14 step 9 — this ADR records the design review of the
|
||||
splice invariants before any production re-emission code lands.
|
||||
|
||||
## Context
|
||||
|
||||
Naïvely regenerating a page's content stream on every edit loses everything we
|
||||
didn't model: obscure operators, `BDC` marked-content nesting, transparency
|
||||
groups, printer-specific `DP` properties, comments. A full-rebuild emitter
|
||||
either has to model the entire PDF operator set losslessly (infeasible before
|
||||
1.0) or silently drops what it doesn't understand (a fidelity regression per
|
||||
§1.1). The decision shapes the L3 content model: every `DisplayItem` carries
|
||||
its `SourceSpan`, and the emitter splices rather than rebuilds.
|
||||
|
||||
This is the M0 design review that §14 step 9 calls for, recorded as an ADR.
|
||||
|
||||
## Decision
|
||||
|
||||
Re-emission is **surgical**. For a page whose edits touch regions B and C of
|
||||
`[A][B][C][D]`, the emitter copies A and D verbatim and writes a new `B'C'`
|
||||
in place of `[B][C]`. Rules:
|
||||
|
||||
1. Each `DisplayItem` carries its `SourceSpan` — a `(streamIndex, offset,
|
||||
length)` triple over the *logical concatenation* of the page's content
|
||||
streams (a page's content may be an array of streams and operators can
|
||||
straddle the boundary; spans are over the concatenation but the emitted
|
||||
output preserves the array structure).
|
||||
2. Dirty items' spans are coalesced into replacement regions.
|
||||
3. Graphics state at a region boundary is reconstructed and **explicitly
|
||||
re-established** inside the replacement (`q ... Q` wrapping), so the
|
||||
following verbatim bytes still see the state they expect. This is verified
|
||||
by an assertion pass that re-interprets the emitted stream and diffs the
|
||||
graphics state at every splice point against the original. **Zero
|
||||
violations is a CI gate** (§8.2).
|
||||
4. Newly created objects append after the last region.
|
||||
5. If a page's structure is too tangled to splice safely — deeply interleaved
|
||||
marked content across a dirty region, an unrecognised operator inside it,
|
||||
or a `q`/`Q` imbalance — fall back to a full re-emit of that page and
|
||||
record it as a fidelity event. **We never guess.**
|
||||
6. Resources added by an edit are merged into the page's resource dictionary
|
||||
with fresh, non-colliding names. A resource dictionary inherited from an
|
||||
ancestor `Pages` node is *copied down to the page* before mutation — never
|
||||
edited in place, it is shared.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive.** Untouched content is byte-identical to the input — the round-trip
|
||||
gate (§8.2) is realistically achievable. Unknown operators are preserved by
|
||||
construction rather than dropped. The splice-point assertion gives a
|
||||
mechanical, property-based check on the trickiest invariant.
|
||||
|
||||
**Negative.** The L3 interpreter must record `SourceSpan` on every item,
|
||||
which is bookkeeping cost throughout. The fall-back full re-emit path must
|
||||
also exist and be correct, which is roughly the work of a full emitter anyway
|
||||
— but it only runs on tangled pages, so its fidelity budget is per-page
|
||||
rather than corpus-wide.
|
||||
|
||||
**Neutral.** A page that needed a full re-emit is reported in the fidelity
|
||||
panel; the user is told, not lied to.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
* **Full rebuild always** — rejected; cannot preserve unmodelled operators
|
||||
without modelling the whole operator set.
|
||||
* **Full rebuild with a "pass-through unknown" hack** — rejected; the
|
||||
graphics state at the rebuild boundary is the failure mode, and a hack
|
||||
doesn't address it. Splicing with explicit state re-establishment does.
|
||||
* **Splice without the assertion pass** — rejected; the splice-point invariant
|
||||
is the one thing that, if violated, silently corrupts complex pages. A
|
||||
property-based check is cheap insurance.
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# ADR-0003 — Memory safety posture for the parser layers
|
||||
|
||||
* **Status**: Accepted (decision on Rust-vs-C++ for L1/L2 leaf decoders is
|
||||
*deferred to end of M0* per §15; this ADR records the posture that holds
|
||||
regardless of that decision)
|
||||
* **Date**: 2025-07-25
|
||||
* **Plan reference**: §7.2, §15
|
||||
|
||||
## Context
|
||||
|
||||
We are writing a new PDF parser in C++. The threat model (§7.1) identifies
|
||||
parser memory corruption as the main risk: a hostile PDF is the vector, and
|
||||
the user's machine, other files, and network are the assets. "Be careful" is
|
||||
not a posture; the decision must be made before parsing code lands because it
|
||||
shapes the build flags, the allocator, the allowed language subset, and
|
||||
whether a second language enters the tree.
|
||||
|
||||
A separate decision — whether L1/L2 leaf decoders (filters, image codec glue,
|
||||
CMap parsing) are written in Rust behind a C ABI — is explicitly deferred to
|
||||
the end of M0 (§15). This ADR records the posture that holds in either case
|
||||
and the deadline for that decision.
|
||||
|
||||
## Decision
|
||||
|
||||
**Hardening on in release builds, not just debug.** The build applies, in
|
||||
*release*:
|
||||
|
||||
* `std::span`-like bounded views for all parsing; raw pointer arithmetic
|
||||
over input bytes is banned in the parsing layers, enforced by clang-tidy
|
||||
and review (§13.2 rule 7).
|
||||
* `-D_GLIBCXX_ASSERTIONS=1` / `_LIBCPP_HARDENING_MODE=fast`,
|
||||
`-fstack-protector-strong`, CFI and shadow-stack/CET where available —
|
||||
applied via `cmake/FreePDFEditorHardening.cmake` to every target.
|
||||
* A hardened allocator (scudo or hardened_malloc) in the document process.
|
||||
* Integer overflow is a build error in the parsing layers (UBSan in CI,
|
||||
checked arithmetic helpers in code).
|
||||
* Resource budgets enforced at the L2 boundary — decompressed stream size,
|
||||
nesting depth, object count, total resident bytes — so a decompression
|
||||
bomb aborts with a diagnostic rather than dying by OOM-killer.
|
||||
|
||||
**The Rust-vs-C++ decision for L1/L2 leaf decoders is made once, at the end of
|
||||
M0** (§15). It is not revisited at M4 — retrofitting a second language
|
||||
mid-project is worse than either choice made early. The decision is gated on
|
||||
the M0 spike results and on whether the leaf decoders' CVE history justifies
|
||||
the build-friction cost of a second language.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive.** The attack surface is hardened by construction regardless of
|
||||
the Rust decision. The build flags are committed (in
|
||||
`cmake/FreePDFEditorHardening.cmake`) so they cannot drift quietly.
|
||||
|
||||
**Negative.** Release builds carry a runtime cost from the assertions and
|
||||
CFI; the performance budget (§5) must absorb it. The hardened allocator is
|
||||
slower than the system allocator; the document process's allocator choice is
|
||||
separate from the UI process's for this reason.
|
||||
|
||||
**Neutral.** The M0 spikes run under these same flags, so the
|
||||
feasibility-gate measurements reflect the real build, not an unhardened
|
||||
optimistic run.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
* **Hardening in debug only** — rejected; the release build is what users
|
||||
run and what an attacker targets.
|
||||
* **Rewrite the whole parser in Rust** — rejected at this scope; the UI,
|
||||
Qt integration, and layout engine are C++, and a mixed-language tree has
|
||||
real build and contributor cost. The leaf-decoder decision is the
|
||||
narrowest place a second language buys the most.
|
||||
* **Rely on the sandbox alone** — rejected; sandbox escape via the IPC
|
||||
boundary is in the threat model (§7.1), so defence in depth is required.
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# ADR-0004 — Two-process model with the IPC as a bidirectional trust boundary
|
||||
|
||||
* **Status**: Accepted
|
||||
* **Date**: 2025-07-25
|
||||
* **Plan reference**: §2.1
|
||||
|
||||
## Context
|
||||
|
||||
PDF parsers are among the most heavily exploited attack surfaces in desktop
|
||||
software, and we are writing a new one. A single-process editor means a
|
||||
parser bug is a direct path to the user's files, network, and other
|
||||
applications. Sandboxing is the one thing in the plan that is genuinely
|
||||
painful to retrofit (§14 step 8), so the process split must be decided
|
||||
before any parsing code lands.
|
||||
|
||||
The naive read of "sandbox the parser" treats the sandbox as protecting *us*
|
||||
from *the document*. The decision recorded here is that the IPC boundary is
|
||||
a trust boundary in **both** directions: the document process is compromised
|
||||
by hostile input, so anything it sends is itself hostile to the unsandboxed
|
||||
UI process. A compromised document process attacking the UI is the obvious
|
||||
escape path and the one people forget.
|
||||
|
||||
## Decision
|
||||
|
||||
**Two processes.** The UI process (Qt) holds the canvas, panels, tools,
|
||||
command dispatch, undo, and render-surface compositing; it does no untrusted
|
||||
parsing. The document process (sandboxed) holds file I/O, parsing, the object
|
||||
model, content streams, rasterization, re-emission, and save. One document
|
||||
process per open document; a crash loses one tab, not the app.
|
||||
|
||||
* **IPC**: local socket, Cap'n Proto messages, shared memory for raster
|
||||
tiles (no copies on the hot path).
|
||||
* **Sandbox**: seccomp-bpf + user namespaces (Linux), App Sandbox with a
|
||||
minimal entitlement set (macOS), AppContainer + low-integrity token
|
||||
(Windows). The document process gets no network access and no filesystem
|
||||
access beyond fds handed to it by the UI process.
|
||||
* **Bidirectional trust boundary.** The UI validates every message (bounds,
|
||||
counts, string encodings, shared-memory extents) and never indexes,
|
||||
allocates, or size-computes directly from a document-supplied number.
|
||||
Cap'n Proto arena reader limits are set explicitly, not left at defaults.
|
||||
* **Fuzz the UI-side deserializer as its own harness** (§8.2) — a
|
||||
compromised document process attacking the unsandboxed UI is the obvious
|
||||
escape path and the one people forget.
|
||||
|
||||
A crash in the document process is recovered by the UI: restart it and
|
||||
replay the journal (§6.4).
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive.** A parser bug cannot directly reach the user's files or
|
||||
network. A document-process crash loses one tab, not the app — and is
|
||||
recoverable. The bidirectional framing means the escape path people forget
|
||||
is covered by an explicit gate.
|
||||
|
||||
**Negative.** The process split has a real cost on open latency (measured at
|
||||
M0, §14 step 8). IPC adds complexity to every edit and every render tile.
|
||||
Sandboxing on all three platforms is platform-specific work that has to be
|
||||
maintained.
|
||||
|
||||
**Neutral.** Two windows on the same file share one document process (§6.4).
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
* **Single process** — rejected; a parser bug is a direct path to the
|
||||
user's machine.
|
||||
* **Sandbox only the parser, IPC treated as trusted** — rejected; a
|
||||
compromised document process attacking the unsandboxed UI is the escape
|
||||
path people forget (§2.1).
|
||||
* **Out-of-process renderer, in-process parser** — rejected; the parser is
|
||||
the larger CVE surface, not the renderer. PDFium (the renderer) is
|
||||
battle-tested; our parser is new.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# Architecture Decision Records
|
||||
|
||||
Architecture decision records (ADRs) live here, numbered, and **immutable once
|
||||
accepted** (§13.4 of [`docs/plan.md`](../plan.md)). To change an accepted
|
||||
decision, write a new ADR that supersedes the earlier one and reference it
|
||||
from both files.
|
||||
|
||||
## Format
|
||||
|
||||
Each ADR is a single Markdown file `NNNN-short-title.md` with:
|
||||
|
||||
* **Status**: Proposed | Accepted | Superseded by `NNNN` | Withdrawn
|
||||
* **Date**: ISO 8601
|
||||
* **Context**: why this decision is being made now
|
||||
* **Decision**: what we decided
|
||||
* **Consequences**: what follows — positive, negative, and neutral
|
||||
* **Alternatives considered**: what we did not choose, and why
|
||||
|
||||
## Seeded at M0
|
||||
|
||||
Per §13.4 and §14 step 9, the following ADRs exist from M0:
|
||||
|
||||
* [ADR-0001 — Render what you will save](0001-render-what-you-will-save.md)
|
||||
(§2.5)
|
||||
* [ADR-0002 — Surgical re-emission with splice-point invariants](0002-surgical-re-emission.md)
|
||||
(§4.4)
|
||||
* [ADR-0003 — Memory safety posture for the parser layers](0003-memory-safety-posture.md)
|
||||
(§7.2)
|
||||
* [ADR-0004 — Two-process model with the IPC as a bidirectional trust
|
||||
boundary](0004-two-process-trust-boundary.md) (§2.1)
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
#
|
||||
# AppImage build script (§12.1). Builds a manylinux-style AppImage bundling
|
||||
# Qt, Skia, ICU and the codecs. The actual bundling uses linuxdeploy +
|
||||
# linuxdeploy-plugin-qt, run inside the release container; this script is the
|
||||
# entry point the release pipeline (§12.5) calls.
|
||||
#
|
||||
# AppImage cannot use bubblewrap-style user namespaces on distros that restrict
|
||||
# them, so the sandbox falls back to seccomp-only. The app reports its actual
|
||||
# sandbox level in About and never silently pretends a stronger level.
|
||||
set -euo pipefail
|
||||
|
||||
APP="FreePDFEditor"
|
||||
LOWER="$(echo "$APP" | tr '[:upper:]' '[:lower:]')"
|
||||
VERSION="${1:?usage: build-appimage.sh <version> <build_dir>}"
|
||||
BUILD_DIR="${2:?missing build_dir}"
|
||||
OUTPUT="${3:-${APP}-${VERSION}-x86_64.AppImage}"
|
||||
|
||||
if [[ ! -d "$BUILD_DIR" ]]; then
|
||||
echo "build dir not found: $BUILD_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v linuxdeploy >/dev/null 2>&1; then
|
||||
echo "linuxdeploy not on PATH — install it (https://github.com/linuxdeploy/linuxdeploy)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APPDIR="$BUILD_DIR/AppDir"
|
||||
rm -rf "$APPDIR"
|
||||
mkdir -p "$APPDIR/usr/bin"
|
||||
|
||||
# Copy the built binary into the AppDir layout.
|
||||
cp "$BUILD_DIR/bin/freepdfeditor" "$APPDIR/usr/bin/"
|
||||
|
||||
# Install the desktop file, icons, metainfo into AppDir.
|
||||
mkdir -p "$APPDIR/usr/share/applications" \
|
||||
"$APPDIR/usr/share/icons/hicolor/scalable/apps" \
|
||||
"$APPDIR/usr/share/metainfo"
|
||||
cp "$SCRIPT_DIR/freepdfeditor.desktop" "$APPDIR/usr/share/applications/"
|
||||
cp "$SCRIPT_DIR/icons/hicolor/scalable/apps/freepdfeditor.svg" \
|
||||
"$APPDIR/usr/share/icons/hicolor/scalable/apps/"
|
||||
cp "$SCRIPT_DIR/org.freepdfeditor.FreePDFEditor.metainfo.xml" \
|
||||
"$APPDIR/usr/share/metainfo/"
|
||||
|
||||
# linuxdeploy bundles the runtime + Qt + library deps via the qt plugin.
|
||||
export OUTPUT="$OUTPUT"
|
||||
export UPDATE_INFORMATION="zsync|https://download.freepdfeditor.org/nightly/$LOWER-x86_64.AppImage.zsync"
|
||||
export LINUXDEPLOY_OUTPUT_VERSION="$VERSION"
|
||||
linuxdeploy --appdir "$APPDIR" --desktop-file \
|
||||
"$APPDIR/usr/share/applications/freepdfeditor.desktop" \
|
||||
--icon-file "$APPDIR/usr/share/icons/hicolor/scalable/apps/freepdfeditor.svg" \
|
||||
--output appimage
|
||||
|
||||
echo "Built $OUTPUT"
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
.\" SPDX-License-Identifier: GPL-3.0-or-later
|
||||
.\" SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
.TH FREEPDFEDITOR 1 "2025-07-25" "FreePDFEditor 0.1.0" "User Commands"
|
||||
.SH NAME
|
||||
freepdfeditor \- cross-platform native desktop PDF editor
|
||||
.SH SYNOPSIS
|
||||
.B freepdfeditor
|
||||
.RI [ file ... ]
|
||||
.SH DESCRIPTION
|
||||
FreePDFEditor is a native desktop PDF editor with a true WYSIWYG interface and
|
||||
full content editing: edit existing text with reflow, replace images,
|
||||
manipulate vector objects, plus annotations, forms, signatures, and page
|
||||
assembly.
|
||||
.PP
|
||||
Unlike overlay-style editors it parses each page's content stream into a
|
||||
semantic model, lets the user manipulate that model directly, and re-emits
|
||||
the content stream on save, so the output remains searchable, accessible,
|
||||
and copy-pasteable.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.BR \-h , " \-\-help"
|
||||
Show command-line help and exit.
|
||||
.TP
|
||||
.BR \-v , " \-\-version"
|
||||
Print the application version and exit.
|
||||
.SH FILES
|
||||
.TP
|
||||
.I file
|
||||
An optional PDF to open at startup.
|
||||
.SH ENVIRONMENT
|
||||
The application honours standard Qt environment variables (QT_QPA_PLATFORM,
|
||||
QT_SCALE_FACTOR, etc.) and the platform's XDG directories for configuration
|
||||
and cache storage.
|
||||
.SH BUGS
|
||||
Report bugs at <https://gitea.lm.je/ai-ad4/freepdfeditor/issues>.
|
||||
.SH SEE ALSO
|
||||
The full engineering plan ships with the source under
|
||||
.IR docs/plan.md .
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# bash completion for freepdfeditor
|
||||
|
||||
_freepdfeditor_completion() {
|
||||
local cur prev words cword
|
||||
_init_completion || return
|
||||
|
||||
if [[ "$cur" == -* ]]; then
|
||||
COMPREPLY=($(compgen -W '--help --version' -- "$cur"))
|
||||
return
|
||||
fi
|
||||
|
||||
# Filename completion, restricted to PDFs by convention.
|
||||
_filedir pdf
|
||||
}
|
||||
|
||||
complete -F _freepdfeditor_completion freepdfeditor
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=FreePDFEditor
|
||||
GenericName=PDF Editor
|
||||
Comment=Edit text, images, vectors, annotations and forms in PDF documents
|
||||
Comment[de]=Text, Bilder, Vektoren, Anmerkungen und Formulare in PDF-Dokumenten bearbeiten
|
||||
Exec=freepdfeditor %U
|
||||
Icon=freepdfeditor
|
||||
Terminal=false
|
||||
Categories=Graphics;Office;Viewer;
|
||||
Keywords=pdf;editor;annotate;form;sign;
|
||||
MimeType=application/pdf;
|
||||
StartupNotify=true
|
||||
StartupWMClass=FreePDFEditor
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
Placeholder scalable icon — replaced by the real artwork before the first
|
||||
public release. Kept simple so it renders on every platform that loads SVG
|
||||
icons (Qt, hicolor, Windows .ico conversion, macOS .icns conversion).
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||
<rect width="128" height="128" rx="18" fill="#1f3a5f"/>
|
||||
<rect x="28" y="22" width="64" height="84" rx="4" fill="#ffffff"/>
|
||||
<rect x="36" y="32" width="44" height="4" fill="#1f3a5f"/>
|
||||
<rect x="36" y="42" width="44" height="4" fill="#1f3a5f"/>
|
||||
<rect x="36" y="52" width="32" height="4" fill="#1f3a5f"/>
|
||||
<rect x="36" y="62" width="44" height="4" fill="#1f3a5f"/>
|
||||
<rect x="36" y="72" width="24" height="4" fill="#1f3a5f"/>
|
||||
<circle cx="92" cy="92" r="18" fill="#d9534f"/>
|
||||
<path d="M84 92 l6 6 l10 -12" stroke="#ffffff" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
-->
|
||||
<component type="desktop-application">
|
||||
<id>org.freepdfeditor.FreePDFEditor</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0-or-later</project_license>
|
||||
<name>FreePDFEditor</name>
|
||||
<summary>Cross-platform native desktop PDF editor with full content editing</summary>
|
||||
<description>
|
||||
<p>
|
||||
FreePDFEditor is a native desktop PDF editor that edits existing text with
|
||||
reflow, replaces images, manipulates vector objects, and adds annotations,
|
||||
forms, signatures, and page assembly. Unlike overlay-style editors it
|
||||
parses the content stream into a semantic model and re-emits it on save,
|
||||
so the output remains searchable, accessible and copy-pasteable.
|
||||
</p>
|
||||
</description>
|
||||
<launchable type="desktop-id">freepdfeditor.desktop</launchable>
|
||||
<url type="homepage">https://gitea.lm.je/ai-ad4/freepdfeditor</url>
|
||||
<url type="bugtracker">https://gitea.lm.je/ai-ad4/freepdfeditor/issues</url>
|
||||
<url type="help">https://gitea.lm.je/ai-ad4/freepdfeditor/src/branch/main/docs/plan.md</url>
|
||||
<provides>
|
||||
<binary>freepdfeditor</binary>
|
||||
</provides>
|
||||
<releases>
|
||||
<release version="0.1.0" date="2025-07-25">
|
||||
<description><p>Pre-M0 repository scaffold.</p></description>
|
||||
</release>
|
||||
</releases>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="social-location">none</content_attribute>
|
||||
<content_attribute id="social-info">none</content_attribute>
|
||||
</content_rating>
|
||||
<requires>
|
||||
<display_length compare="ge">800</display_length>
|
||||
</requires>
|
||||
</component>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# REUSE configuration. The repository is REUSE-compliant (§13.4): every file
|
||||
# carries SPDX metadata either inline (source) or via .reuse/dep5 (config,
|
||||
# data, assets). `reuse lint` is a CI gate.
|
||||
|
||||
version: 1.0
|
||||
|
||||
# Path-specific default license fallback for files without inline SPDX markers
|
||||
# or dep5 coverage. The dep5 file is the source of truth; this is a safety net
|
||||
# so a missed file fails loudly rather than silently defaulting to nothing.
|
||||
defaults:
|
||||
- path: .
|
||||
license: GPL-3.0-or-later
|
||||
copyright: "2025 ai-ad4 and the FreePDFEditor contributors"
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
|
||||
#include "Roundtrip.h"
|
||||
|
||||
#include <qpdf/QPDF.hh>
|
||||
#include <qpdf/QPDFPageDocumentHelper.hh>
|
||||
#include <qpdf/QPDFObjectHandle.hh>
|
||||
#include <qpdf/QPDFWriter.hh>
|
||||
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace freepdfeditor::spike::a {
|
||||
|
||||
namespace {
|
||||
|
||||
// Read a whole file into a string. QPDF can read from disk directly, but for
|
||||
// the round-trip we want to control the bytes that go in (and out) so the
|
||||
// byte-equality check is against the original input, not a re-read.
|
||||
bool read_file(const std::filesystem::path& p, std::string& out)
|
||||
{
|
||||
std::ifstream in(p, std::ios::binary);
|
||||
if (!in) return false;
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
out = ss.str();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FileResult roundtrip(const std::filesystem::path& input,
|
||||
const std::filesystem::path& output)
|
||||
{
|
||||
FileResult r{Outcome::Failed, 0, {}};
|
||||
|
||||
std::string original;
|
||||
if (!read_file(input, original)) {
|
||||
r.error = "could not read input";
|
||||
return r;
|
||||
}
|
||||
|
||||
QPDF q;
|
||||
try {
|
||||
q.processInputFile(input.string());
|
||||
} catch (const std::exception& e) {
|
||||
r.error = std::string("QPDF open failed: ") + e.what();
|
||||
return r;
|
||||
}
|
||||
|
||||
// Exercise the content-stream parse path: walk every page and force-decode
|
||||
// its content stream(s). This is what proves "parse content streams" rather
|
||||
// than just "QPDF rewrites the file". A page whose content we can't decode
|
||||
// is reported as a structural success anyway — the gate is byte-identity,
|
||||
// and QPDF preserves even streams it can't decode by copying them verbatim.
|
||||
try {
|
||||
QPDFPageDocumentHelper helper(q);
|
||||
auto pages = helper.getAllPages();
|
||||
r.pages = pages.size();
|
||||
for (auto& page : pages) {
|
||||
QPDFObjectHandle content = page.getAttribute("/Contents", true);
|
||||
if (content.isArray()) {
|
||||
auto items = content.getArrayAsArray();
|
||||
for (auto& item : items) {
|
||||
if (item.isStream()) {
|
||||
(void)item.getStreamData(); // force decode
|
||||
}
|
||||
}
|
||||
} else if (content.isStream()) {
|
||||
(void)content.getStreamData();
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
// A page-walk failure is a structural defect worth surfacing, but the
|
||||
// round-trip gate is still meaningful — record it in the error field
|
||||
// without failing the whole round-trip.
|
||||
r.error = std::string("page walk warning: ") + e.what();
|
||||
}
|
||||
|
||||
try {
|
||||
QPDFWriter w(q);
|
||||
w.setOutputFile(output.string());
|
||||
// Preserve as much as possible: write with the same object stream
|
||||
// strategy and linearisation state as the input so byte-identity is a
|
||||
// realistic target. The defaults preserve the structure verbatim where
|
||||
// QPDF can.
|
||||
w.setPreserveEncryption(true);
|
||||
w.setQDF(false);
|
||||
w.setLinearization(false);
|
||||
w.write();
|
||||
} catch (const std::exception& e) {
|
||||
r.error = std::string("QPDF write failed: ") + e.what();
|
||||
r.outcome = Outcome::Failed;
|
||||
return r;
|
||||
}
|
||||
|
||||
// Compare bytes.
|
||||
std::string written;
|
||||
if (!read_file(output, written)) {
|
||||
r.error = "could not read back output";
|
||||
r.outcome = Outcome::Failed;
|
||||
return r;
|
||||
}
|
||||
|
||||
if (original.size() == written.size() &&
|
||||
std::memcmp(original.data(), written.data(), original.size()) == 0) {
|
||||
r.outcome = Outcome::ByteIdentical;
|
||||
r.error.clear();
|
||||
} else {
|
||||
r.outcome = Outcome::StructurallySame;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace freepdfeditor::spike::a
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
//
|
||||
// Roundtrip — the core of Spike A. Given a PDF on disk, open it with QPDF,
|
||||
// ask QPDF to write it back to a fresh file with the same structure, and
|
||||
// report whether the round-trip is byte-identical. Per the plan §14 step 4:
|
||||
//
|
||||
// "Spike A: QPDF-based open → parse content streams → re-emit verbatim → save.
|
||||
// Measure byte-identical round-trip rate over 500 corpus files. Target ≥ 99%."
|
||||
//
|
||||
// The "parse content streams" step is what later spikes build on; here we walk
|
||||
// every page's content stream(s) through QPDF's stream decoder/encoder and
|
||||
// verify the object graph is structurally preserved, which is the precondition
|
||||
// for the surgical re-emission in §4.4 to be lossless.
|
||||
|
||||
#ifndef FREEPDFEDITOR_SPIKE_A_ROUNDTRIP_H
|
||||
#define FREEPDFEDITOR_SPIKE_A_ROUNDTRIP_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace freepdfeditor::spike::a {
|
||||
|
||||
enum class Outcome {
|
||||
ByteIdentical, // input == output byte-for-byte
|
||||
StructurallySame, // QPDF wrote a different byte stream with identical semantics
|
||||
Failed, // could not open / could not write
|
||||
};
|
||||
|
||||
struct FileResult {
|
||||
Outcome outcome;
|
||||
std::size_t pages = 0;
|
||||
std::string error; // populated when outcome == Failed
|
||||
};
|
||||
|
||||
// Round-trip `input` through QPDF, writing to `output`. `output` must not
|
||||
// already exist. Walks every page's content stream(s) via
|
||||
// qpdf_objecthandle to prove the parse path is exercised, then writes the
|
||||
// document back preserving object order.
|
||||
FileResult roundtrip(const std::filesystem::path& input,
|
||||
const std::filesystem::path& output);
|
||||
|
||||
} // namespace freepdfeditor::spike::a
|
||||
|
||||
#endif // FREEPDFEDITOR_SPIKE_A_ROUNDTRIP_H
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
//
|
||||
// Spike A main: walks a corpus directory, runs the QPDF verbatim round-trip on
|
||||
// every PDF, and emits a JSON report on stdout. Exit code 0 if the
|
||||
// byte-identical rate meets the §14 target (≥ 99%), 1 otherwise.
|
||||
//
|
||||
// Usage:
|
||||
// spike_a_verbatim_roundtrip <corpus_dir> [target_rate]
|
||||
//
|
||||
// Recursively globs `<corpus_dir>/**/*.pdf`. Output PDFs go into a sibling
|
||||
// temp directory next to the executable; nothing in the corpus is mutated.
|
||||
|
||||
#include "Roundtrip.h"
|
||||
#include "common/SpikeRunner.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<fs::path> collect_pdfs(const fs::path& root, std::size_t cap)
|
||||
{
|
||||
std::vector<fs::path> out;
|
||||
std::error_code ec;
|
||||
if (!fs::exists(root, ec)) return out;
|
||||
for (auto& entry : fs::recursive_directory_iterator(root,
|
||||
fs::directory_options::skip_permission_denied, ec)) {
|
||||
if (ec) { ec.clear(); continue; }
|
||||
if (entry.is_regular_file(ec) &&
|
||||
entry.path().extension() == ".pdf") {
|
||||
out.push_back(entry.path());
|
||||
if (out.size() >= cap) break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
fs::path make_temp_dir()
|
||||
{
|
||||
auto tmp = fs::temp_directory_path();
|
||||
std::random_device rd;
|
||||
std::mt19937_64 gen(rd());
|
||||
auto suffix = std::to_string(gen());
|
||||
auto p = tmp / ("freepdfeditor-spikeA-" + suffix);
|
||||
fs::create_directories(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
std::fprintf(stderr,
|
||||
"usage: %s <corpus_dir> [target_rate_default=0.99]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
fs::path corpus = argv[1];
|
||||
double target = (argc >= 3) ? std::stod(argv[2]) : 0.99;
|
||||
|
||||
auto pdfs = collect_pdfs(corpus, 100'000);
|
||||
if (pdfs.empty()) {
|
||||
freepdfeditor::spike::SpikeResult r{};
|
||||
r.spike = "A";
|
||||
r.name = "QPDF verbatim round-trip";
|
||||
r.metric_name = "byte_identical_rate";
|
||||
r.target = target;
|
||||
r.gate_met = false;
|
||||
r.notes = "no PDFs found in corpus directory";
|
||||
return freepdfeditor::spike::emit_json_report(r);
|
||||
}
|
||||
|
||||
auto outdir = make_temp_dir();
|
||||
std::size_t byte_identical = 0;
|
||||
std::size_t structurally_same = 0;
|
||||
std::size_t failed = 0;
|
||||
std::vector<std::string> failed_samples;
|
||||
|
||||
for (std::size_t i = 0; i < pdfs.size(); ++i) {
|
||||
const auto& in = pdfs[i];
|
||||
fs::path out = outdir / (std::to_string(i) + ".pdf");
|
||||
fs::remove(out);
|
||||
auto fr = freepdfeditor::spike::a::roundtrip(in, out);
|
||||
switch (fr.outcome) {
|
||||
case freepdfeditor::spike::a::Outcome::ByteIdentical:
|
||||
++byte_identical;
|
||||
break;
|
||||
case freepdfeditor::spike::a::Outcome::StructurallySame:
|
||||
++structurally_same;
|
||||
break;
|
||||
case freepdfeditor::spike::a::Outcome::Failed:
|
||||
++failed;
|
||||
if (failed_samples.size() < 16) {
|
||||
failed_samples.push_back(in.filename().string());
|
||||
}
|
||||
break;
|
||||
}
|
||||
fs::remove(out);
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(outdir, ec);
|
||||
|
||||
double rate = static_cast<double>(byte_identical) /
|
||||
static_cast<double>(pdfs.size());
|
||||
// The §14 gate is byte-identity. Structural-same is interesting for the
|
||||
// report but does not pass the gate; we surface it in notes.
|
||||
bool gate = rate >= target;
|
||||
|
||||
freepdfeditor::spike::SpikeResult r{};
|
||||
r.spike = "A";
|
||||
r.name = "QPDF verbatim round-trip";
|
||||
r.total = pdfs.size();
|
||||
r.passed = byte_identical;
|
||||
r.failed = structurally_same;
|
||||
r.errored = failed;
|
||||
r.metric_name = "byte_identical_rate";
|
||||
r.metric_value = rate;
|
||||
r.target = target;
|
||||
r.gate_met = gate;
|
||||
r.failed_samples = failed_samples;
|
||||
r.notes = "structurally_same files (QPDF rewrote, semantics preserved) count "
|
||||
"as not-byte-identical for this gate; they are tracked in `failed`";
|
||||
return freepdfeditor::spike::emit_json_report(r);
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# spike/CMakeLists.txt
|
||||
#
|
||||
# M0 feasibility spikes (§14). Each spike is its own executable so it can be run
|
||||
# independently and its results reported separately by CI. The spikes link only
|
||||
# against the leaf libraries they exercise (QPDF here), not against Qt.
|
||||
|
||||
find_package(QPDF CONFIG QUIET)
|
||||
|
||||
if(NOT QPDF_FOUND)
|
||||
message(STATUS
|
||||
"QPDF not found — Spike A (verbatim round-trip) will not be built. "
|
||||
"Install qpdf (vcpkg, system package, or from source) to enable it.")
|
||||
return()
|
||||
endif()
|
||||
|
||||
add_executable(spike_a_verbatim_roundtrip
|
||||
common/SpikeRunner.cpp
|
||||
common/SpikeRunner.h
|
||||
A_verbatim_roundtrip/main.cpp
|
||||
A_verbatim_roundtrip/Roundtrip.cpp
|
||||
A_verbatim_roundtrip/Roundtrip.h
|
||||
)
|
||||
target_link_libraries(spike_a_verbatim_roundtrip PRIVATE QPDF::qpdf)
|
||||
target_compile_features(spike_a_verbatim_roundtrip PRIVATE cxx_std_20)
|
||||
freepdfeditor_apply_warnings(spike_a_verbatim_roundtrip)
|
||||
freepdfeditor_apply_hardening(spike_a_verbatim_roundtrip)
|
||||
|
||||
# Spike C (hb-subset growth of an existing subset font) is left as a placeholder
|
||||
# until HarfBuzz is wired in; it is exercised from a standalone harness in CI.
|
||||
if(FALSE)
|
||||
find_package(harfbuzz CONFIG QUIET)
|
||||
if(harfbuzz_FOUND)
|
||||
add_executable(spike_c_subset_growth
|
||||
common/SpikeRunner.cpp
|
||||
common/SpikeRunner.h
|
||||
C_subset_growth/main.cpp
|
||||
)
|
||||
target_link_libraries(spike_c_subset_growth PRIVATE harfbuzz::harfbuzz harfbuzz::harfbuzz-subset)
|
||||
endif()
|
||||
endif()
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
|
||||
#include "SpikeRunner.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
namespace freepdfeditor::spike {
|
||||
|
||||
namespace {
|
||||
|
||||
// Minimal hand-rolled JSON string escaper — avoids pulling a JSON lib into the
|
||||
// spike harness, which must stay dependency-light so it builds first.
|
||||
std::string esc(std::string s)
|
||||
{
|
||||
std::string out;
|
||||
out.reserve(s.size() + 8);
|
||||
for (char c : s) {
|
||||
switch (c) {
|
||||
case '"': out += "\\\""; break;
|
||||
case '\\': out += "\\\\"; break;
|
||||
case '\n': out += "\\n"; break;
|
||||
case '\r': out += "\\r"; break;
|
||||
case '\t': out += "\\t"; break;
|
||||
default:
|
||||
if (static_cast<unsigned char>(c) < 0x20) {
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "\\u%04x", c);
|
||||
out += buf;
|
||||
} else {
|
||||
out += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int emit_json_report(const SpikeResult& r)
|
||||
{
|
||||
std::string samples;
|
||||
for (std::size_t i = 0; i < r.failed_samples.size(); ++i) {
|
||||
if (i) samples += ",";
|
||||
samples += "\"" + esc(r.failed_samples[i]) + "\"";
|
||||
}
|
||||
std::printf(
|
||||
"{\"spike\":\"%s\",\"name\":\"%s\",\"total\":%zu,\"passed\":%zu,"
|
||||
"\"failed\":%zu,\"errored\":%zu,\"metric\":\"%s\",\"value\":%.6f,"
|
||||
"\"target\":%.6f,\"gate_met\":%s,\"samples\":[%s],\"notes\":\"%s\"}\n",
|
||||
r.spike.c_str(), r.name.c_str(),
|
||||
r.total, r.passed, r.failed, r.errored,
|
||||
r.metric_name.c_str(), r.metric_value, r.target,
|
||||
r.gate_met ? "true" : "false",
|
||||
samples.c_str(), esc(r.notes).c_str());
|
||||
std::fflush(stdout);
|
||||
return r.gate_met ? 0 : 1;
|
||||
}
|
||||
|
||||
} // namespace freepdfeditor::spike
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
//
|
||||
// SpikeRunner — tiny helper that each Spike A/E main() uses to emit a
|
||||
// machine-readable JSON report and a process exit code matching the spike's
|
||||
// acceptance gate. Keeping this in one place means CI just parses one format.
|
||||
|
||||
#ifndef FREEPDFEDITOR_SPIKE_COMMON_SPIKERUNNER_H
|
||||
#define FREEPDFEDITOR_SPIKE_COMMON_SPIKERUNNER_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace freepdfeditor::spike {
|
||||
|
||||
struct SpikeResult {
|
||||
std::string spike; // "A", "B", ...
|
||||
std::string name; // human-readable
|
||||
std::size_t total = 0; // files/pages examined
|
||||
std::size_t passed = 0; // met the gate
|
||||
std::size_t failed = 0;
|
||||
std::size_t errored = 0; // crashed / could not process
|
||||
std::vector<std::string> failed_samples; // up to N sample ids for triage
|
||||
std::string metric_name; // e.g. "byte_identical_rate", "F1"
|
||||
double metric_value = 0.0; // aggregate metric
|
||||
double target = 0.0; // gate threshold
|
||||
bool gate_met = false;
|
||||
std::string notes;
|
||||
};
|
||||
|
||||
// Serialise `result` as a single-line JSON document on stdout, then return the
|
||||
// process exit code (0 if gate_met, 1 otherwise). CI parses this line.
|
||||
int emit_json_report(const SpikeResult& result);
|
||||
|
||||
} // namespace freepdfeditor::spike
|
||||
|
||||
#endif // FREEPDFEDITOR_SPIKE_COMMON_SPIKERUNNER_H
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# src/CMakeLists.txt — application sources.
|
||||
#
|
||||
# At this stage (pre-M1) the only target is the empty-window application shell
|
||||
# described in §14 step 2: a Qt Widgets main window with the chrome layout from
|
||||
# §9 so the packaging pipeline and the platform integrations have something to
|
||||
# sign, install, and notarize from day one. The sandboxed document process and
|
||||
# the canvas land in M1/M2.
|
||||
|
||||
# Locate Qt. The vcpkg manifest does not currently include Qt (it is huge and
|
||||
# often built system-wide); we use find_package so a system Qt or a vcpkg-built
|
||||
# Qt both work.
|
||||
find_package(Qt6 6.7 COMPONENTS Core Gui Widgets Network QUIET)
|
||||
|
||||
if(NOT Qt6_FOUND)
|
||||
message(WARNING
|
||||
"Qt6 not found — the application target 'freepdfeditor' will not be built.\n"
|
||||
"Install Qt 6.7+ (system or via vcpkg) to build the GUI. The QPDF-based\n"
|
||||
"spikes and the pixel-diff harness do not require Qt and still build.")
|
||||
return()
|
||||
endif()
|
||||
|
||||
qt_standard_project_setup()
|
||||
|
||||
add_executable(freepdfeditor
|
||||
app/main.cpp
|
||||
app/MainWindow.cpp
|
||||
app/MainWindow.h
|
||||
)
|
||||
|
||||
target_compile_features(freepdfeditor PRIVATE cxx_std_20)
|
||||
target_link_libraries(freepdfeditor PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Network)
|
||||
|
||||
freepdfeditor_apply_warnings(freepdfeditor)
|
||||
freepdfeditor_apply_hardening(freepdfeditor)
|
||||
|
||||
# --- macOS bundle -----------------------------------------------------------
|
||||
set_target_properties(freepdfeditor PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "FreePDFEditor"
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}"
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER "org.freepdfeditor.FreePDFEditor"
|
||||
MACOSX_BUNDLE_COPYRIGHT "© 2025 ai-ad4 and contributors, GPL-3.0-or-later"
|
||||
WIN32_EXECUTABLE TRUE
|
||||
)
|
||||
|
||||
# --- install rules (drive CPack) -------------------------------------------
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS freepdfeditor
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
BUNDLE DESTINATION .)
|
||||
|
||||
# Linux .desktop entry, hicolor icons, AppStream metainfo, man page, completions
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
install(FILES "${CMAKE_SOURCE_DIR}/packaging/linux/freepdfeditor.desktop"
|
||||
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications")
|
||||
install(DIRECTORY "${CMAKE_SOURCE_DIR}/packaging/linux/icons/"
|
||||
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor")
|
||||
install(FILES "${CMAKE_SOURCE_DIR}/packaging/linux/org.freepdfeditor.FreePDFEditor.metainfo.xml"
|
||||
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/metainfo")
|
||||
install(FILES "${CMAKE_SOURCE_DIR}/packaging/linux/freepdfeditor.1"
|
||||
DESTINATION "${CMAKE_INSTALL_MANDIR}/man1")
|
||||
install(FILES "${CMAKE_SOURCE_DIR}/packaging/linux/freepdfeditor.bash-completion"
|
||||
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/bash-completion/completions"
|
||||
RENAME "freepdfeditor")
|
||||
endif()
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
|
||||
#include "MainWindow.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QIcon>
|
||||
#include <QKeySequence>
|
||||
#include <QLabel>
|
||||
#include <QMenuBar>
|
||||
#include <QStatusBar>
|
||||
#include <QToolBar>
|
||||
#include <QStackedWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <QStandardPaths>
|
||||
|
||||
namespace FreePDFEditor {
|
||||
|
||||
MainWindow::MainWindow(QWidget* parent)
|
||||
: QMainWindow(parent)
|
||||
{
|
||||
setMinimumSize(QSize(1024, 720));
|
||||
setWindowTitle(tr("FreePDFEditor"));
|
||||
|
||||
buildCentralChrome();
|
||||
buildMenus();
|
||||
buildStatusBar();
|
||||
}
|
||||
|
||||
void MainWindow::buildCentralChrome()
|
||||
{
|
||||
// Per §9: tool palette | canvas | inspector. The canvas and panels are
|
||||
// placeholders for M1; the structure is here so the chrome is testable.
|
||||
auto* host = new QWidget(this);
|
||||
auto* layout = new QVBoxLayout(host);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto* placeholder = new QLabel(host);
|
||||
placeholder->setAlignment(Qt::AlignCenter);
|
||||
placeholder->setText(
|
||||
tr("<h3>FreePDFEditor — pre-M0 scaffold</h3>"
|
||||
"<p>The viewer, sandbox, canvas and editing tools land in M1–M6.</p>"
|
||||
"<p>See <code>docs/plan.md</code> for the roadmap.</p>"));
|
||||
placeholder->setStyleSheet("color: palette(window-text);");
|
||||
layout->addWidget(placeholder, 1);
|
||||
|
||||
setCentralWidget(host);
|
||||
}
|
||||
|
||||
void MainWindow::buildMenus()
|
||||
{
|
||||
// File
|
||||
auto* fileMenu = menuBar()->addMenu(tr("&File"));
|
||||
|
||||
auto* openAct = fileMenu->addAction(
|
||||
QIcon::fromTheme(QStringLiteral("document-open")), tr("&Open…"));
|
||||
openAct->setShortcut(QKeySequence::Open);
|
||||
openAct->setEnabled(false); // M1 viewer
|
||||
fileMenu->addAction(tr("Open Recent"))->setEnabled(false);
|
||||
|
||||
fileMenu->addSeparator();
|
||||
auto* saveAct = fileMenu->addAction(
|
||||
QIcon::fromTheme(QStringLiteral("document-save")), tr("&Save"));
|
||||
saveAct->setShortcut(QKeySequence::Save);
|
||||
saveAct->setEnabled(false);
|
||||
|
||||
auto* saveAsAct = fileMenu->addAction(
|
||||
QIcon::fromTheme(QStringLiteral("document-save-as")), tr("Save &As…"));
|
||||
saveAsAct->setShortcut(QKeySequence::SaveAs);
|
||||
saveAsAct->setEnabled(false);
|
||||
|
||||
fileMenu->addSeparator();
|
||||
auto* printAct = fileMenu->addAction(
|
||||
QIcon::fromTheme(QStringLiteral("document-print")), tr("&Print…"));
|
||||
printAct->setShortcut(QKeySequence::Print);
|
||||
printAct->setEnabled(false);
|
||||
|
||||
fileMenu->addSeparator();
|
||||
auto* quitAct = fileMenu->addAction(
|
||||
QIcon::fromTheme(QStringLiteral("application-exit")), tr("&Quit"));
|
||||
quitAct->setShortcut(QKeySequence::Quit);
|
||||
connect(quitAct, &QAction::triggered, this, &QWidget::close);
|
||||
|
||||
// Edit
|
||||
auto* editMenu = menuBar()->addMenu(tr("&Edit"));
|
||||
for (auto role : {QKeySequence::Undo, QKeySequence::Redo,
|
||||
QKeySequence::Cut, QKeySequence::Copy, QKeySequence::Paste}) {
|
||||
auto* a = editMenu->addAction(QKeySequence(role).toString());
|
||||
a->setEnabled(false);
|
||||
}
|
||||
|
||||
// View
|
||||
auto* viewMenu = menuBar()->addMenu(tr("&View"));
|
||||
viewMenu->addAction(tr("Zoom In"), QKeySequence::ZoomIn)->setEnabled(false);
|
||||
viewMenu->addAction(tr("Zoom Out"), QKeySequence::ZoomOut)->setEnabled(false);
|
||||
viewMenu->addAction(tr("Fit to Width"))->setEnabled(false);
|
||||
|
||||
// Tools
|
||||
auto* toolsMenu = menuBar()->addMenu(tr("&Tools"));
|
||||
for (const char* name : {"Select", "Text Edit", "Annotate", "Redact",
|
||||
"Form Field", "Shape", "Node Edit"}) {
|
||||
toolsMenu->addAction(tr(name))->setEnabled(false);
|
||||
}
|
||||
|
||||
// Help
|
||||
auto* helpMenu = menuBar()->addMenu(tr("&Help"));
|
||||
helpMenu->addAction(tr("About FreePDFEditor"), this, [this] {
|
||||
// Minimal about; real About lands with the version+license panel.
|
||||
});
|
||||
helpMenu->addAction(tr("Report a Bug…"))->setEnabled(false);
|
||||
}
|
||||
|
||||
void MainWindow::buildStatusBar()
|
||||
{
|
||||
m_statusLabel = new QLabel(statusBar());
|
||||
m_statusLabel->setStyleSheet("padding: 2px 6px;");
|
||||
statusBar()->addWidget(m_statusLabel, 1);
|
||||
showStatus(tr("Ready"));
|
||||
}
|
||||
|
||||
void MainWindow::showStatus(const QString& text)
|
||||
{
|
||||
if (m_statusLabel) {
|
||||
m_statusLabel->setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
// M2 will gate this on unsaved changes + journal flush.
|
||||
QMainWindow::closeEvent(event);
|
||||
}
|
||||
|
||||
} // namespace FreePDFEditor
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
//
|
||||
// MainWindow — the application shell chrome. Layout follows §9 of the plan:
|
||||
// tool palette / page canvas / inspector, with menu bar, search and status bar.
|
||||
// This is the empty-window scaffolding: real canvas, panels, and tools land in
|
||||
// M1–M6.
|
||||
|
||||
#ifndef FREEPDFEDITOR_APP_MAINWINDOW_H
|
||||
#define FREEPDFEDITOR_APP_MAINWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
|
||||
class QLabel;
|
||||
class QStackedWidget;
|
||||
|
||||
namespace FreePDFEditor {
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MainWindow(QWidget* parent = nullptr);
|
||||
|
||||
// Set the text in the transient status area (used during the scaffold
|
||||
// phase before the real status bar logic from §9 lands).
|
||||
void showStatus(const QString& text);
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent* event) override;
|
||||
|
||||
private:
|
||||
void buildMenus();
|
||||
void buildCentralChrome();
|
||||
void buildStatusBar();
|
||||
|
||||
QLabel* m_statusLabel = nullptr;
|
||||
};
|
||||
|
||||
} // namespace FreePDFEditor
|
||||
|
||||
#endif // FREEPDFEDITOR_APP_MAINWINDOW_H
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
//
|
||||
// Entry point for the FreePDFEditor UI process. The process model (§2.1) keeps
|
||||
// all untrusted parsing out of this process; the sandboxed document process is
|
||||
// spawned on demand once a document is opened. At this scaffolding stage no
|
||||
// document process exists yet, so this is just the empty window shell.
|
||||
|
||||
#include "MainWindow.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
QApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
|
||||
QApplication app(argc, argv);
|
||||
app.setOrganizationName("FreePDFEditor");
|
||||
app.setApplicationName("FreePDFEditor");
|
||||
app.setApplicationDisplayName("FreePDFEditor");
|
||||
app.setApplicationVersion(QStringLiteral(FREEPDFEDITOR_VERSION_STRING));
|
||||
app.setWindowIcon(QIcon(QStringLiteral(":/icons/freepdfeditor.svg")));
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription(
|
||||
QApplication::translate("main", "Cross-platform native desktop PDF editor."));
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
parser.addPositionalArgument(
|
||||
QStringLiteral("file"),
|
||||
QApplication::translate("main", "Optional PDF file to open at startup."));
|
||||
parser.process(app);
|
||||
|
||||
FreePDFEditor::MainWindow window;
|
||||
window.show();
|
||||
|
||||
const QStringList args = parser.positionalArguments();
|
||||
if (!args.isEmpty()) {
|
||||
// No document process yet; the M1 viewer will wire this up.
|
||||
window.showStatus(QApplication::translate(
|
||||
"main", "Opening documents is not implemented until M1."));
|
||||
}
|
||||
Q_UNUSED(args);
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
# test/CMakeLists.txt
|
||||
#
|
||||
# Unit and gate tests. The M0 scaffold ships a single smoke test that proves
|
||||
# the build produces a runnable artefact and the spike runner emits the
|
||||
# contract JSON the CI pipeline parses. As the layers land (L1–L6), each gets
|
||||
# its own test executable under here, all registered with CTest.
|
||||
|
||||
# We do not pull GoogleTest yet — the first tests are plain freestanding mains
|
||||
# returning non-zero on failure, which keeps the M0 scaffold dependency-free
|
||||
# and lets CI on all three platforms run ctest without a vcpkg build of gtest.
|
||||
# A later milestone swaps in GTest once there is real C++ logic to assert on.
|
||||
|
||||
add_executable(test_spike_runner_contract test_spike_runner_contract.cpp)
|
||||
target_compile_features(test_spike_runner_contract PRIVATE cxx_std_20)
|
||||
freepdfeditor_apply_warnings(test_spike_runner_contract)
|
||||
freepdfeditor_apply_hardening(test_spike_runner_contract)
|
||||
add_test(NAME spike_runner_contract COMMAND test_spike_runner_contract)
|
||||
|
||||
# Pixel-diff harness scaffolding (§8.2 gate: render vs Ghostscript+PDFium
|
||||
# reference). The actual comparison runs against a corpus that lives in the
|
||||
# separate freepdfeditor-corpus repo (§13.2 rule 5); this scaffold is a
|
||||
# self-contained Python script that CI invokes with paths to the corpus and
|
||||
# the reference binaries.
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../ci/pixel-diff/run_pixel_diff.py"
|
||||
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/freepdfeditor/ci")
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
//
|
||||
// Smoke test for the spike-runner JSON contract. The CI pipeline parses the
|
||||
// single-line JSON that emit_json_report produces; if that format drifts the
|
||||
// whole gate matrix goes dark. This test pins the format without depending on
|
||||
// QPDF, so it runs on every platform even before the spike builds.
|
||||
|
||||
#include "../spike/common/SpikeRunner.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
// Capture stdout during emit_json_report by writing to a captured buffer —
|
||||
// emit_json_report writes to real stdout, so we redirect via freopen. This is
|
||||
// crude but avoids pulling a dependency into the scaffold test.
|
||||
std::string capture(freepdfeditor::spike::SpikeResult r)
|
||||
{
|
||||
std::fflush(stdout);
|
||||
FILE* saved = stdout;
|
||||
stdout = std::tmpfile();
|
||||
if (!stdout) { stdout = saved; return {}; }
|
||||
int rc = freepdfeditor::spike::emit_json_report(r);
|
||||
std::fflush(stdout);
|
||||
std::string buf;
|
||||
std::rewind(stdout);
|
||||
int c;
|
||||
while ((c = std::fgetc(stdout)) != EOF) buf.push_back(static_cast<char>(c));
|
||||
std::fclose(stdout);
|
||||
stdout = saved;
|
||||
(void)rc;
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
using namespace freepdfeditor::spike;
|
||||
|
||||
SpikeResult r{};
|
||||
r.spike = "A";
|
||||
r.name = "test";
|
||||
r.total = 10;
|
||||
r.passed = 9;
|
||||
r.failed = 1;
|
||||
r.errored = 0;
|
||||
r.metric_name = "byte_identical_rate";
|
||||
r.metric_value = 0.9;
|
||||
r.target = 0.99;
|
||||
r.gate_met = false;
|
||||
r.failed_samples = {"bad.pdf"};
|
||||
r.notes = "hello \"world\"\nline2";
|
||||
|
||||
std::string out = capture(r);
|
||||
|
||||
int failures = 0;
|
||||
auto assert_contains = [&](const std::string& needle) {
|
||||
if (out.find(needle) == std::string::npos) {
|
||||
std::fprintf(stderr, "FAIL: output missing %s\n", needle.c_str());
|
||||
std::fprintf(stderr, "output was: %s\n", out.c_str());
|
||||
++failures;
|
||||
}
|
||||
};
|
||||
|
||||
assert_contains("\"spike\":\"A\"");
|
||||
assert_contains("\"name\":\"test\"");
|
||||
assert_contains("\"total\":10");
|
||||
assert_contains("\"passed\":9");
|
||||
assert_contains("\"failed\":1");
|
||||
assert_contains("\"metric\":\"byte_identical_rate\"");
|
||||
assert_contains("\"value\":0.900000");
|
||||
assert_contains("\"target\":0.990000");
|
||||
assert_contains("\"gate_met\":false");
|
||||
assert_contains("\"samples\":[\"bad.pdf\"]");
|
||||
assert_contains("\"notes\":\"hello \\\"world\\\"\\nline2\"");
|
||||
// Single line, terminated by newline.
|
||||
if (out.empty() || out.back() != '\n') {
|
||||
std::fprintf(stderr, "FAIL: output not newline-terminated\n");
|
||||
++failures;
|
||||
}
|
||||
if (out.find('\n') != out.size() - 1) {
|
||||
std::fprintf(stderr, "FAIL: output contains embedded newline\n");
|
||||
++failures;
|
||||
}
|
||||
|
||||
// Gate-met returns 0; not-met returns 1. Test the contract directly.
|
||||
r.gate_met = true;
|
||||
if (freepdfeditor::spike::emit_json_report(r) != 0) {
|
||||
std::fprintf(stderr, "FAIL: gate_met=true should return 0\n");
|
||||
++failures;
|
||||
}
|
||||
r.gate_met = false;
|
||||
if (freepdfeditor::spike::emit_json_report(r) != 1) {
|
||||
std::fprintf(stderr, "FAIL: gate_met=false should return 1\n");
|
||||
++failures;
|
||||
}
|
||||
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg-configuration.schema.json",
|
||||
"default-registry": {
|
||||
"kind": "git",
|
||||
"repository": "https://github.com/microsoft/vcpkg.git",
|
||||
"ref": "master",
|
||||
"baseline": "c4467224f8a384b7d52cb7d5e8abfb3f3f463f17"
|
||||
},
|
||||
"overlay-ports": ["${sourceDir}/cmake/vcpkg-ports"]
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
|
||||
"name": "freepdfeditor",
|
||||
"version": "0.1.0",
|
||||
"description": "A cross-platform native desktop PDF editor with full content editing.",
|
||||
"homepage": "https://gitea.lm.je/ai-ad4/freepdfeditor",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"supports": "(windows & x64) | (windows & arm64) | (osx & x64) | (osx & arm64) | (linux & x64) | (linux & arm64)",
|
||||
"dependencies": [
|
||||
{ "name": "qpdf", "version>=": "11.9.0" },
|
||||
{ "name": "freetype", "version>=": "2.13.2" },
|
||||
{ "name": "harfbuzz", "version>=": "8.4.0", "features": ["subset"] },
|
||||
{ "name": "icu", "version>=": "74.2" },
|
||||
{ "name": "lcms", "version>=": "2.16" },
|
||||
{ "name": "libjpeg-turbo", "version>=": "3.0.0" },
|
||||
{ "name": "libpng", "version>=": "1.6.43" },
|
||||
{ "name": "openjpeg", "version>=": "2.5.2" },
|
||||
{ "name": "tiff", "version>=": "4.6.0" },
|
||||
{ "name": "zlib-ng", "version>=": "2.1.5" },
|
||||
{ "name": "brotli", "version>=": "1.1.0" },
|
||||
{ "name": "liblzma", "version>=": "5.6.0" },
|
||||
{ "name": "openssl", "version>=": "3.2.1" },
|
||||
{ "name": "hunspell", "version>=": "1.7.2" }
|
||||
],
|
||||
"features": {
|
||||
"tesseract": {
|
||||
"description": "OCR support (Tier 2 feature, off by default for 1.0 builds).",
|
||||
"dependencies": [
|
||||
{ "name": "tesseract", "version>=": "5.3.4" },
|
||||
{ "name": "leptonica", "version>=": "1.84.1" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue