Skip to content

Release v0.3.69 — language bindings, registry publishing, regression examples - #777

Merged
yfedoseev merged 88 commits into
mainfrom
release/v0.3.69
Jun 27, 2026
Merged

Release v0.3.69 — language bindings, registry publishing, regression examples#777
yfedoseev merged 88 commits into
mainfrom
release/v0.3.69

Conversation

@yfedoseev

@yfedoseev yfedoseev commented Jun 25, 2026

Copy link
Copy Markdown
Owner

v0.3.69 — Language-bindings release

Brings the 11 new language bindings to production readiness.

What's in it

  • 11 bindings over the C ABI: C++, Swift, Kotlin, Dart, R, Julia, Zig, Scala, Clojure, Objective-C, Elixir.
  • Package-registry publishing wired into release.yml: Maven Central (Kotlin/Scala), Clojars (Clojure), Hex.pm (Elixir), pub.dev (Dart, OIDC). Objective-C ships as a Trunk-free CocoaPods binary pod (xcframework + podspec as release assets) — CocoaPods Trunk goes read-only 2026-12-02. C++ (vcpkg/Conan), R (CRAN), Julia (General registry), Swift/Zig (git tag) documented in docs/RELEASING-bindings.md.
  • Cross-language regression examples: HTML extraction, words+geometry, table extraction — added to every binding and asserted in its CI workflow (on top of the existing basic example + api-coverage test suite).
  • Packaging fixes (Elixir files: so the NIF ships, objc podspec license, LICENSE for all 11, C++ CMake install/export + Conan), SOTA-2026 secondary linters (scalafix, cljfmt, dialyxir, dart analysis_options), version bump to 0.3.69 across all manifests, .gitignore fixes.

Verification

  • 9/9 runnable bindings smoke-tested locally (text + markdown + the 3 new scenarios) against a freshly built cdylib; Swift/ObjC are macOS-only (their CI).

Release gate

Do not merge/tag until: pub.dev OIDC enabled (+ first manual Dart publish), Clojars creds rotated, and CI fully green. See docs/RELEASING-bindings.md.

Closes

Delivers the idiomatic-binding requests:

yfedoseev added 30 commits June 20, 2026 00:35
2067 maturin build artifacts under target-wheel/ (the CARGO_TARGET_DIR used by
the python wheel build) were accidentally tracked and merged into main. Add
/target-wheel/ to .gitignore (alongside /target/) and remove the files from the
index (kept on disk). No source changes.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Idiomatic header-only C++17 RAII bindings over the C ABI
(include/pdf_oxide_c/pdf_oxide.h). Establishes the binding + CI pattern the
other v0.3.68 languages reuse.

- pdf_oxide::Document: open / open_from_bytes / open_with_password, page_count,
  version, is_encrypted, has_structure_tree, extract_text, to_plain_text,
  to_markdown, to_html, to_markdown_all, extract_structured_json.
- pdf_oxide::Pdf builder: from_markdown / from_html / from_text, save,
  save_to_bytes. C strings/buffers freed via free_string; errors -> Error.
- CMake build (links the default-feature cdylib, NOT the python wheel),
  runnable example (asserted), tests/test_api_coverage.cpp (one check per
  method), README, .clang-format.
- CI: .github/workflows/cpp.yml (Linux+macOS): build cdylib -> cmake -> ctest
  -> run example with assertion -> clang-format. Same verification set as the
  other bindings.

Verified locally: cmake build + ctest green (api-coverage 100% pass) against a
freshly built default-feature cdylib. Editor/builder/OCR/signatures/barcodes/
search/fonts/images/annotations/tables coverage to follow.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Idiomatic Dart bindings over the C ABI via dart:ffi (no C shim). Loads the
default-feature cdylib at runtime; NativeFinalizer + close() free handles;
free_string frees returned strings/buffers; errors -> PdfOxideError.

- PdfDocument: open / openFromBytes / openWithPassword, pageCount, version,
  isEncrypted, hasStructureTree, extractText, toPlainText, toMarkdown, toHtml,
  toMarkdownAll, extractStructuredJson, close.
- Pdf builder: fromMarkdown / fromHtml / fromText, save, saveToBytes, close.
- example/basic_extraction.dart (asserted in CI), test/api_coverage_test.dart
  (one test per method), README, pubspec.
- CI: .github/workflows/dart.yml (Linux+macOS): build cdylib -> pub get ->
  analyze -> format check -> dart test -> run example with assertion.

Verified locally (Dart 3.11.5): dart analyze clean, dart format clean,
`dart test` 16/16 pass against the default-feature cdylib. Editor/builder/OCR/
signatures/barcodes/search/fonts/images coverage to follow.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
…b/ rule)

The Python-artifacts `lib/` ignore rule also matched dart/lib/, so the Dart
binding's main wrapper file was omitted from the previous commit. Add a
`!dart/lib/` exception (alongside go/lib and ruby/lib) and commit the wrapper.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Idiomatic R package (pdfoxide) wrapping the C ABI via a small C shim and R's
.Call interface. Handles are external pointers freed by the GC; returned
strings/buffers copied into R and freed via free_string; C-ABI errors raised as
R errors. Page indices 0-based.

- Document: pdf_open / pdf_open_bytes (+ password), pdf_page_count, pdf_version,
  pdf_is_encrypted, pdf_has_structure_tree, pdf_extract_text, pdf_to_plain_text,
  pdf_to_markdown, pdf_to_html, pdf_to_markdown_all, pdf_extract_structured_json.
- Builder: pdf_from_markdown / pdf_from_html / pdf_from_text, pdf_save,
  pdf_save_to_bytes.
- inst/examples/basic_extraction.R (asserted in CI), inst/tinytest api-coverage
  (one test per function), DESCRIPTION/NAMESPACE/Makevars, README.
- CI: .github/workflows/r.yml (Linux+macOS): build cdylib -> R CMD INSTALL ->
  tinytest -> run example with assertion.

Verified locally (R 4.5.3): R CMD INSTALL clean, tinytest 15/15 pass against the
default-feature cdylib.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Kotlin/JVM (Android-ready) via JNA — pure Kotlin, no native compile. JNA loads
libpdf_oxide; AutoCloseable handles; free_string for returned strings/buffers;
errors -> PdfOxideException; suspending coroutine extensions on
Dispatchers.Default. KGP 2.2.20 (Gradle 9 + JDK 25 compat); jvmToolchain 17.
ApiCoverageTest (one test per method incl. coroutine helpers) green locally.

Julia via ccall (direct, no shim). Finalizer-freed handles; free_string;
errors -> PdfOxideError. test/runtests.jl api-coverage; example asserted in CI.

Both: wrapper + example (asserted) + api-coverage test + CI workflow
(kotlin.yml / julia.yml, Linux+macOS, same verification set) + README.
Also: author -> "Yury Fedoseev" in r/DESCRIPTION and julia/Project.toml.

Kotlin verified locally (gradle test BUILD SUCCESSFUL, all coverage tests pass).
Julia CI-verified (toolchain not installed locally).

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Idiomatic Zig bindings over the C ABI via @cImport of the cbindgen header (no
shim). Document/Pdf structs with deinit; returned strings/buffers copied into a
caller allocator and the C buffer freed via free_string; C-ABI errors map to
error.PdfOxide. Pinned to Zig 0.14.0 (pre-1.0 API drift).

- build.zig (links default-feature cdylib), build.zig.zon, lib/pdf_oxide.zig
  (wrapper + api-coverage tests, one per method), examples/basic_extraction.zig
  (asserted in CI), README.
- CI: .github/workflows/zig.yml (Linux+macOS, pinned Zig): build cdylib ->
  zig build test -> zig build example with assertion. Same verification set.

Zig toolchain not installed locally -> CI-verified.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
… rule)

The Python-artifacts `lib/` ignore rule also matched zig/lib/ (same as the
earlier dart/lib fix), omitting the Zig wrapper from the previous commit. Add a
`!zig/lib/` exception and commit the wrapper.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Idiomatic Scala 3 bindings over the C ABI via JNA (same proven mechanism as the
Kotlin binding). AutoCloseable handles (Using-friendly); free_string for
returned strings/buffers; errors -> PdfOxideException; case-class PdfVersion.

- src/main/scala/fyi/oxide/pdf/PdfOxide.scala (PdfDocument, Pdf), example
  (asserted in CI), ApiCoverageSpec (ScalaTest, one test per method), build.sbt,
  README.
- CI: .github/workflows/scala.yml (Linux+macOS): build cdylib -> JDK 17 + sbt ->
  sbt test -> run example with assertion. Same verification set.

Scala/sbt not installed locally -> CI-verified.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Idiomatic Clojure bindings over the C ABI via JNA (same mechanism as
Kotlin/Scala). Document/Pdf are Closeable deftypes (with-open friendly);
returned strings/buffers freed via free_string; C-ABI errors -> ex-info {:code}.

- src/pdf_oxide/core.clj (open/open-bytes/page-count/version/extract-text/
  to-markdown/to-html/to-markdown-all/extract-structured-json + builder
  from-markdown/from-html/from-text/save/save-to-bytes), example (asserted in
  CI), test/pdf_oxide/core_test.clj (clojure.test, one test per fn), deps.edn,
  README.
- CI: .github/workflows/clojure.yml (Linux+macOS): build cdylib -> JDK 17 +
  Clojure CLI -> clojure -M:test -> run example with assertion.

Clojure CLI not installed locally -> CI-verified.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Idiomatic Objective-C bindings over the C ABI (header is C-callable from ObjC).
POXDocument/POXPdf NSObject wrappers own the handles, free in -dealloc (ARC);
returned strings/buffers -> NSString/NSData freed via free_string; C-ABI errors
-> NSError (POXErrorDomain).

- include/POXPdfOxide.h + src/POXPdfOxide.m (open/openData/openPassword,
  pageCount, getVersion, isEncrypted, hasStructureTree, extractText/toPlainText/
  toMarkdown/toHtml/toMarkdownAll/extractStructuredJson + builder fromMarkdown/
  fromHtml/fromText/saveToPath/saveToBytes), example (asserted in CI),
  tests/test_api_coverage.m (one check per method, exit-code), Makefile, README.
- CI: .github/workflows/objc.yml (macOS): build cdylib -> make (clang/ARC) ->
  test_api_coverage -> run example with assertion.

ObjC toolchain not local -> CI-verified (macOS runner).

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Idiomatic Swift bindings over the C ABI. CPdfOxide system-library target exposes
the cbindgen header via a module map; PdfOxide is the Swift wrapper. Class-owned
handles freed in deinit; returned strings/buffers -> String/[UInt8] freed via
free_string; C-ABI errors -> PdfOxideError.

- Package.swift, Sources/CPdfOxide (module.modulemap + shim.h), Sources/PdfOxide
  (Document, Pdf), Sources/Example (asserted in CI), Tests/PdfOxideTests
  (XCTest, one test per method), README.
- CI: .github/workflows/swift.yml (macOS): build cdylib -> swift test
  (api-coverage) -> swift run example with assertion. Same verification set.

Swift toolchain not local -> CI-verified (macOS runner).

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Idiomatic Elixir bindings via a NIF over the C ABI. Every CPU-bound text NIF
runs on a dirty CPU scheduler (ERL_NIF_DIRTY_JOB_CPU_BOUND) so it never blocks
the BEAM. Handles are NIF resources freed by the GC; returned strings/buffers ->
binaries freed via free_string; results are {:ok, v}/{:error, code}.

- lib/pdf_oxide.ex (PdfOxide + Document/Pdf structs + Error), lib/pdf_oxide/
  native.ex (NIF loader), c_src/pdf_oxide_nif.c (dirty NIF), Makefile
  (elixir_make), example (asserted in CI), test/pdf_oxide_test.exs (ExUnit, one
  test per function), mix.exs, README.
- CI: .github/workflows/elixir.yml (Linux+macOS): build cdylib -> OTP/Elixir ->
  mix compile (builds NIF) -> mix test -> run example with assertion.

Elixir/OTP not installed locally -> CI-verified.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
…o 0.3.68

Centralize the release version in ONE place (Cargo.toml [package] version) and
propagate it everywhere:

- scripts/sync_version.py — reads the canonical Cargo.toml version and writes it
  into every binding manifest + version/parity assert (sub-crates cli/mcp/jni,
  pyproject, js/wasm package.json, csharp .csproj, java pom version+scm-tag+
  header, go fallbackVersion, ruby version.rb + 2 specs, php Pdf.php + installer
  (x2) + parity test, dart, r, julia, kotlin, scala, zig, cpp, elixir).
  `--check` verifies (exit 1 on drift), `--set X.Y.Z` bumps the source then
  syncs. First-match-only by default (so pom.xml dep versions are untouched);
  `multi` for legitimately-repeated strings. Idempotent.
- .github/workflows/version-consistency.yml — CI fails if any binding drifts.
- Bumped workspace + every binding 0.3.67 -> 0.3.68 (incl. Cargo.lock, uv.lock).
- CHANGELOG: 0.3.68 language-bindings release entry.

Verified: sync_version.py --check passes; cargo metadata resolves; full-repo
sweep shows no stray 0.3.67.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Every binding now has wrapper + example + api-coverage test + README + CI
workflow. (Zig's api-coverage tests are inline test blocks in lib/pdf_oxide.zig
per Zig convention.)

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
…ig build)

The fingerprint's high 32 bits must match the package-name hash; the made-up
value was rejected by Zig 0.14. Verified locally: zig build test passes, example
prints expected output.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
…cal Pkg.test)

Julia's ccall forbids a variable function name; the _str_page/_from helpers
passed the C symbol as an argument, failing precompile. Generate the page
extractors and builders with @eval so each ccall references its C function as a
literal symbol. Verified locally: Pkg.test 15/15 pass against the cdylib.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
…quality bar)

Apply JuliaFormatter and add a format-check step to julia.yml (mirrors the
gofmt/rubocop/cs-fixer format gates the existing bindings enforce). Verified
locally: format check clean, Pkg.test 15/15 still pass.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Match the existing-binding quality bar (gofmt+golangci-lint, php-cs-fixer+phpstan,
spotless+spotbugs): add blocking clang-format checks and, for C++, clang-tidy
static analysis (bugprone/performance/modernize/readability, WarningsAsErrors).
Applied clang-format to all C++/ObjC sources; added cpp/.clang-tidy and
objc/.clang-format.

Verified locally: clang-format clean, clang-tidy exit 0, C++ rebuild + ctest
pass after reformat.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Match the binding quality bar: add .formatter.exs, Credo (.credo.exs + dep), and
CI steps for mix format --check-formatted, mix credo --strict, and compile
--warnings-as-errors. Verified locally: NIF compiles clean (warnings-as-errors),
8/8 tests pass, format clean, credo no issues.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Local clojure -M:test caught two real bugs: (1) definterface can't extend
com.sun.jna.Library (JNA rejected it) -> rewrote to JNA NativeLibrary + Function
direct invocation (no interface); (2) deftype mutable fields aren't readable
externally -> store the handle in an atom field (Closeable resets it).

Also add clj-kondo lint (config-free, 0 warnings) to clojure.yml — matches the
binding quality bar. Verified locally: 15 assertions pass, clj-kondo clean.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Add .scalafmt.conf + sbt-scalafmt plugin and a scalafmtCheckAll CI gate. Local
sbt run caught a scalafmt/Scala-3 significant-indentation pitfall (the Native_
object body got de-indented) — converted that object to braces. Verified
locally: scalafmt check clean, 8/8 ScalaTest pass against the cdylib.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
Apply ktlint (autofix) and add an .editorconfig that disables only
function-naming + class-naming (the JNA CLib interface must use snake_case to
map C symbols; the Native_ object mirrors the C namespace) — all other ktlint
standard rules enforced. CI installs ktlint 1.8.0 and runs the check before
tests. Verified locally: ktlint clean, gradle test BUILD SUCCESSFUL.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
…rity hardening

Bring every new binding's CI to the existing-binding quality bar (format check +
lint/static analysis), and harden the workflows:

- R: add lintr (r/.lintr) + lint step in r.yml. Verified locally: "No lints found".
- Swift: add .swift-format + `swift format lint --strict` step (macOS CI; the
  local brew Swift has a glibc incompatibility so Swift is CI-verified).
- Security (flagged by commit review on kotlin.yml): the ktlint download is now
  SHA256-pinned + verified (sha256sum -c) instead of an unverified curl.
- Least privilege: add `permissions: contents: read` to all 12 new binding/
  version workflows.

Lint/format gate now per binding: cpp clang-format+clang-tidy, dart analyze+
format, r lintr, julia JuliaFormatter, zig fmt, scala scalafmt, clojure
clj-kondo, objc clang-format, swift swift-format, kotlin ktlint, elixir mix
format+credo+warnings-as-errors. All verified locally except swift/objc
(macOS-only).

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
…tion + Julia Aqua

From the 2026 SOTA tooling research workflow (adversarially verified):

- R (real defect): useDynLib(.registration = TRUE) had NO R_registerRoutines /
  R_init_pdfoxide in the C shim — registration was absent (R CMD check NOTE).
  Add an R_CallMethodDef table + R_init_pdfoxide (R_useDynamicSymbols FALSE,
  R_forceSymbols TRUE); NAMESPACE .fixes = "C_"; switch all 18 .Call sites to
  the registered symbol objects (C_*). Verified locally: reinstall + tinytest
  15/15 pass.

- Julia (real gap): Project.toml only bounded julia, not Test. Add [compat] for
  Aqua + Test and an Aqua.test_all(PdfOxide; persistent_tasks=false) testset
  (stale-deps / compat-coverage / undefined-exports / piracy QA). Verified
  locally: Aqua 10/10 + api coverage pass.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
…es (all bindings)

The C++ ASan/UBSan sanitizer lane (added per the SOTA research) caught a
heap-buffer-overflow: save_to_bytes freed the raw byte buffer returned by
pdf_save_to_bytes with free_string(), which runs strlen() on a buffer that is
NOT NUL-terminated → read past the allocation. The C header explicitly mandates
free_bytes() for uint8_t* buffers.

This latent bug was present in EVERY binding's save_to_bytes (tests passed
without a sanitizer because the over-read was usually benign). Fixed in all 11:
C++, Dart (added free_bytes ffi binding), R, Julia, Zig, Scala (+CLib decl),
Kotlin (+CLib decl), Clojure (+helper), Objective-C, Elixir, Swift.

Re-verified locally after the fix: C++ ASan clean, Dart 16/16, R 15/15, Julia
Aqua+15, Zig, Scala 8/8, Kotlin, Clojure 15/15, Elixir 8/8. ObjC/Swift CI-only.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
…OTA research)

Top-rated findings from the 2026 tooling research workflow (high value/low risk):
- cpp.yml: a Linux ASan+UBSan job that compiles the api-coverage test with
  -fsanitize=address,undefined and runs it — this is what caught the free_bytes
  heap-overflow. Memory/lifetime coverage at the C-ABI boundary that clang-tidy
  can't provide.
- objc.yml: Clang Static Analyzer via `scan-build --status-bugs make build`
  (exit 1 on any potential bug) — zero new deps, high value for the ARC/pointer
  wrapper layer.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
…s gen

SOTA research recommended R CMD check as the R gate. Running it locally caught a
real defect: DESCRIPTION lacked Author/Maintainer (Authors@R alone didn't expand
under R CMD check). Fixed. Plus:
- .Rbuildignore for dev files (.lintr, .Rcheck, build artifacts).
- @useDynLib gains .fixes = "C_" so roxygen regenerates the symbol-based NAMESPACE.
- r.yml: install roxygen2/rcmdcheck, generate man/ docs (roxygenise), then
  rcmdcheck(error_on="warning") + lintr + tinytest + example. (Air formatter
  deferred — pending a SHA-pinned setup action.)

Verified locally: DESCRIPTION ERROR cleared (R CMD check), tinytest 15/15.
man/ docs generated in CI (local roxygen2 install is broken on this box —
pkgload C-compile failure, environment not code).

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
…ned toolchain)

Workflow research flagged the 0.14-only pin as outdated (0.15.x is current stable
in 2026). Migrated to Zig 0.15.1:
- build.zig: new module-based API (b.createModule + addTest/addExecutable with
  .root_module; include/link settings moved onto Module).
- example: Zig 0.15 "Writergate" — stdout via std.fs.File.stdout().writer(&buf)
  + .interface + flush(); page_allocator (GeneralPurposeAllocator renamed).
- build.zig.zon minimum_zig_version 0.14.0 → 0.15.0; CI setup-zig pinned 0.15.1;
  README updated.

Verified locally against Zig 0.15.1: zig build test passes, example prints
expected output, zig fmt --check clean.

Claude-Session: https://claude.ai/code/session_01XhP8c4j3bAPsXKme22WX4Q
yfedoseev added 28 commits June 24, 2026 17:22
- clojure.yml: the pinned DeLaGuardo/setup-clojure SHA did not exist in the
  action repo (jobs failed at 'Set up job'); repin to the real 13.4 commit.
- cpp.yml: clang-format is apt-installed on Linux only, but the format-check
  step ran on all OSes -> exit 127 on macOS. Gate it to Linux (matches the
  Linux-only clang-tidy step; formatting is platform-independent).
- cpp/pdf_oxide.hpp: drop no-op std::move on trivially-copyable Path
  (performance-move-const-arg); make discarded unique_ptr::release() an
  explicit assignment (bugprone-unused-return-value). Verified clang-tidy +
  clang-format clean locally.
- objc examples: clang-format the new example sources (were unformatted).
- objc.yml: install clang-format on the macOS runner (was exiting 127).
roxygen2 (and other deps) were silently failing to compile from source on
the Ubuntu runner, leaving them absent for the 'Generate man/ docs' step
('there is no package called roxygen2'). Enable use-public-rspm for prebuilt
binaries and verify all packages installed (install.packages only warns).
PdfOxide.swift and ApiCoverageTests.swift had never been run through
swift-format; the swift.yml strict format check flagged 265 violations on
first CI run. Auto-formatted with swift-format 6.0.3 and moved 3 over-long
trailing comments to their own lines. Verified 0 violations locally.
PHPStan 2.2.2 flags assertNotNull() on a non-null FFI\CData as always-true
(the FFI stub types the render handle as non-null). Assert the instance type
instead — meaningful and version-robust. Verified: phpstan 2.2.2 reports
'No errors'. (Pre-existing; surfaced by the unpinned phpstan minor bump.)
…s, UselessAssignment)

RuboCop 1.88.0 flagged 3 pre-existing offenses (surfaced by the unpinned
rubocop minor bump): use preserve.zero?, omit ternary-condition parens, and
drop a redundant 'str_ptrs = []' pre-init (the functional str_ptrs that
anchors the FFI string pointers is unaffected). Verified: no offenses.
- ruby: my earlier rubocop autocorrect changed 'preserve == 0' to
  'preserve.zero?', which raises NoMethodError on a Boolean arg
  (true.zero?). Use [false,nil,0].include?(preserve) — type-safe AND
  rubocop-clean. Verified: rubocop clean + phase4 spec passes.
- cpp tests: 4 intentional empty catches (tolerating optional-feature
  errors) tripped bugprone-empty-catch on first CI run; suppress with
  NOLINT/NOLINTNEXTLINE. Verified clang-tidy + clang-format clean.
- r/.lintr: disable object_usage_linter — it false-positives on the
  useDynLib(.registration) native symbols (C_r_*).
The README still advertised only the original ~7 bindings ('Java is the 8th
binding', Ruby/PHP/Swift as 'roadmap'). Refresh the title, add a v0.3.69
callout for the 11 new bindings, list all 19 bindings + install snippets in
'Other languages' (incl. Ruby/PHP which already shipped), and update the
feature line, citation, and footer. Links to docs/RELEASING-bindings.md.
erlef/setup-beam failed on macos-latest (now macos26): 'failed to map a
target OS from ImageOS macos26'. Pin to macos-15 until setup-beam supports
the newer image.
uv.lock still pinned the editable pdf-oxide package at 0.3.68 (sync_version
didn't track it). Regenerated via 'uv lock' and added a sync_version rule so
the Version Consistency CI catches uv.lock drift on future bumps.
Version-numbered test/binary names (coverage_v0368, binding_parity_v0350/
v0351, api_coverage_phase4, debug_v0356, v0365_targets_and_locks,
phase1-xfa-forms, test_phase2_results) read as noise and don't say what they
test. Rename by subject:
- *_v0368 / phase4 coverage  -> api_coverage_extended
- binding/purego parity v0350 -> *_sanitize_sign  (doc sanitization + PAdES reader)
- binding/purego parity v0351 -> *_ocr_models     (OCR model provisioning)
- debug_v0356                 -> debug_extract_text
- v0365_targets_and_locks     -> regression_targets_and_locks
- phase1-xfa-forms            -> xfa-forms
- test_phase2_results         -> check_span_spacing
Renames the C#/Go/JS internal symbols to match and de-versions the header
comments. Pure renames — no test logic changed; filenames keep their
test-runner patterns; no references elsewhere (grep-verified).
- r/.lintr: disable indentation_linter (opinionated; flags the conventional
  structure(\n list(...)) 2-space style); split an over-100-char roxygen
  line into one @PARAM per line (also fixes params that weren't documented).
- zig.yml: pin macos-15 — Zig 0.15.1's build runner fails to link against
  the macos26 SDK (macos-latest advanced; undefined libSystem symbols).
The Swift wrapper never compiled — swift.yml's build never ran on a real
macOS runner, so a systematic set of C-ABI type mismatches went unnoticed:
- 43× Int->UInt: every size_t/UInt C arg was passed a Swift Int (.count,
  page indices, etc.); wrapped each in UInt(...).
- 1× OpaquePointer: pdf_document_get_dss returns UnsafeMutableRawPointer but
  Dss.init takes OpaquePointer -> Dss(OpaquePointer(h)).
- Removed 6 dead 'try' on rethrows withByteArrayArray calls (no nested throw).
Verified with swift 6.0.3: 'swift build' = Build complete (0 errors/warnings),
'swift format lint' = 0 violations.
…FIPS wheel)

- lint: sort imports + ruff-format scripts/sync_version.py (I001)
- R: split packed `@param`/`@return` roxygen tags onto their own lines —
  roxygen2 only parses a tag at line start, so trailing tags became
  description text and left ~50 functions' args "undocumented" (R CMD
  check WARNING, error_on="warning"). Fixes both R on ubuntu + macos.
- Swift: pass the C header dir to the Clang module importer via
  `-Xcc -I` (swiftc's plain `-I` is a Swift-module path only), on every
  target that imports PdfOxide → fixes "pdf_oxide_c/pdf_oxide.h not found".
- ObjC: install + invoke scan-build from the keg-only `llvm` formula
  (not on the macOS runner PATH) → fixes "scan-build: command not found".
- FIPS python wheel: bump the cargo cache key to v2 so the stale
  macOS-26-SDK aws-lc-fips crypto dylib is rebuilt under
  MACOSX_DEPLOYMENT_TARGET=11.0 → fixes delocate's 11.0-floor rejection.
…ngs)

- lint: ruff-format cpp/conanfile.py (ruff format --check . runs repo-wide;
  the prior commit only covered scripts/).
- ObjC scan-build (now that it actually runs): fix the two analyzer bugs it
  surfaced.
  * POXPdfOxide.m pageTable: guard `cells` on (nRows && nCols) — the exact
    conditions under which the fill loops run — so the analyzer can prove it
    non-NULL (decoupled `nCells` left a spurious null path). Behaviour-identical.
  * test_api_coverage.m: the bogus cert loaders always yield nil, so the
    signing calls deliberately pass nil to a `nonnull` `certificate:` to test
    graceful failure. Exclude that block from the analyzer via
    `#ifndef __clang_analyzer__`; it still builds and runs.

(The Python 3.8 Windows failure was a transient `curl`/schannel error fetching
the flate2 crate — a fresh run re-attempts the download.)
…e crash

The binding build/config fixes unblocked these api-coverage tests, which now
RUN for the first time and surface two pre-existing issues:

- Swift `testPdfPageCount`: `XCTAssertGreaterThanOrEqual(try pdf.pageCount(), 1)`
  swallows the thrown PdfOxideError and records its own failure, so the
  `expectReturnOrPdfError` wrapper (which is meant to tolerate code-1 on a
  freshly-built Pdf) never sees it. Pull the `try` out of the assert so the
  throw propagates to the wrapper. (52 tests, was the only failure.)

- ObjC `test_api_coverage` segfaults (exit 139) on first run. Add a
  SIGSEGV/SIGABRT/SIGBUS backtrace handler + per-section `g_phase` markers so
  the next CI run reports which API faults instead of a bare 139. Diagnostic
  only — kept as a permanent aid; the actual fix follows once the frame is known.
The crash diagnostic localised the ObjC `test_api_coverage` segfault to
`-[POXDocument pageCountAliasError:]`, which called the Pdf-*builder* C ABI
`pdf_get_page_count((Pdf*)_handle, …)` with a `PdfDocument*` handle. Opaque
handles are indistinguishable to the C ABI, so it read the PdfDocument as a
`Pdf` and dereferenced garbage → SIGSEGV.

The method was redundant as well as wrong: `pdf_get_page_count` is already
covered correctly via `POXPdf.pageCountError:` (a real `Pdf*`), and document
page count via `POXDocument.pageCountError:` (`pdf_document_get_page_count`).
Remove the broken alias (header + impl) and its test call — zero coverage loss.

Keep the SIGSEGV/SIGABRT/SIGBUS backtrace handler added for diagnosis as a
permanent aid.
…774)

Brings the #774 fix (non-Identity-ordered Type0 fonts emit U+FFFD for CIDs
missing from /ToUnicode instead of a wrong char) into the release, plus its
tests. Moves its CHANGELOG entry under [0.3.69] → Fixed and credits @schelip
(reporter of #773 and author of the fix).

Claude-Session: https://claude.ai/code/session_01UvsYzR5sjaMeamU35eGFsC
Mirrors Dependabot #764 onto the release branch so release/v0.3.69 and main
stay in sync on the workflow action pins (identical SHA bump). CI-tooling only;
no effect on shipped artifacts.

Claude-Session: https://claude.ai/code/session_01UvsYzR5sjaMeamU35eGFsC
The blockquote announced Go/JS/C# as new in v0.3.24 and linked only 6 language
guides — obsolete at v0.3.69 (20 languages, already listed in the intro line).

Claude-Session: https://claude.ai/code/session_01UvsYzR5sjaMeamU35eGFsC
Match the project's identity (PDFOxide), as used in the CHANGELOG. The
`pdf_oxide` package/crate name is unchanged everywhere it's an actual identifier.

Claude-Session: https://claude.ai/code/session_01UvsYzR5sjaMeamU35eGFsC
Brand/product mentions (H1, intro, comparison tables, citation title, license
prose) now read PDFOxide, matching the CHANGELOG and our identity. URLs,
package/crate names, imports, and file paths keep the pdf_oxide identifier.

Claude-Session: https://claude.ai/code/session_01UvsYzR5sjaMeamU35eGFsC
The release-fips Python wheel job tags wheels macosx_11_0 via
MACOSX_DEPLOYMENT_TARGET but reused a cached aws-lc-fips dylib built
against the macOS-26 SDK (min target 15.0). cargo/cmake don't rebuild
the dylib on an env-var change, so delocate rejected the 15.0 dylib in
the 11.0 wheel.

- Bump the FIPS cargo cache key v2 -> v3 to force a clean rebuild that
  honors the 11.0 deployment target.
- Set CMAKE_OSX_DEPLOYMENT_TARGET=11.0 so aws-lc-fips's CMake build
  back-deploys its dylib to 11.0 explicitly.
- Mirror the same pins in ci-fips.yml so CI builds/delocates the same
  11.0 wheel the release ships instead of masking the regression.

Also bump the 0.3.69 CHANGELOG date to 2026-06-27.
The Identity-ordered Type0 default (U+FFFD for uncovered CIDs) also
resolves schelip's opt-in-flag feature request #775 by making the
behaviour the default rather than a configuration flag.
aws-lc-fips-sys ignores MACOSX_DEPLOYMENT_TARGET/CMAKE_OSX_DEPLOYMENT_TARGET
and links its arm64 dylib against the runner SDK (min macOS 15.0), so a
macosx_11_0-tagged wheel bundling that dylib is rejected by delocate. The
earlier 11.0 pin + cache bust could not lower the dylib. Tag the arm64
FIPS wheel macosx_15_0 to match the bundled dylib; x86_64 still builds at
the portable 11.0 floor (it passes). Drop the inert CMAKE_OSX pin.
aws-lc-fips links its arm64 dylib against the runner's build SDK (now
macOS 26) and ignores MACOSX_/CMAKE_OSX_DEPLOYMENT_TARGET, so the bundled
dylib's minimum keeps rising with the runner image and delocate rejects
any portable wheel tag. Drop the aarch64-apple-darwin entry from the FIPS
Python wheel matrix (release-fips.yml + ci-fips.yml); ship Linux x86_64/
aarch64, macOS x86_64, and Windows FIPS wheels. FIPS is predominantly
Linux/x86_64; macOS arm64 FIPS wheel tracked as a follow-up.
@yfedoseev
yfedoseev merged commit 911270a into main Jun 27, 2026
420 of 424 checks passed
@yfedoseev
yfedoseev deleted the release/v0.3.69 branch June 27, 2026 04:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment