Commit Graph
1451 Commits
Author SHA1 Message Date
Dylan M. Taylor ee9a4e6bbb fix: add generic font-family fallbacks in demo (SonarCloud css:S4649)
Each font-family in assets/demo/showcase.html named only a specific font with
no generic fallback. Add an appropriate generic (serif/sans-serif/monospace/
cursive/math) so text still renders if the specific face is unavailable.
2026-10-21 17:20:39 -04:00
Dylan M. Taylor b1bf1cf01f ci: add sonar-project.properties for CI-based analysis
Marks the project ineligible for SonarCloud Automatic Analysis, resolving the
CI-vs-automatic-analysis conflict so the SonarCloud workflow succeeds.
2026-10-21 16:00:45 -04:00
Dylan M. Taylor 30b8c60b55 ci: add SonarCloud scan via shared reusable workflow
Calls storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main
with secrets: inherit. Adds SonarCloud badge to README where missing and
removes the redundant sonar-project.properties (config now passed by the
reusable workflow).
2026-10-21 15:47:03 -04:00
Brandon ThomasandClaude Opus 5.5 3e3064532f Release: PdfCraft v0.11.0 (#3893)
* Release: PdfCraft v0.11.0

- Bump the workspace to 0.11.0.
- Fix main's red CI (cargo-deny): update rustls-webpki 0.103.15 -> 0.103.16
  (RUSTSEC-2026-0382) and webpki-root-certs 1.0.9 -> 1.0.10 (RUSTSEC-2026-0383),
  both reached through winprint's sigstore build dependency.
- `cargo xtask version set` now moves the workspace crates' pins in
  [workspace.dependencies] with the version; before, `cargo update --workspace`
  refused the bump and the pins had to be edited by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: a healthy private retry may answer during the watchdog pass that scheduled it

shared_timeouts_retry_healthy_pages_privately_before_blacklisting failed on
CI (PR #3893): the try_recv that runs the watchdog spawns the private retries,
and page 1's retry has no gate, so on a loaded runner its healthy result
arrived before that same call read the channel. Accept that early answer
(page 1, no error) and use it as the healthy result; an error still fails.
Reproduced by sleeping 300 ms after the watchdog in try_recv: the old test
fails every time, the new one passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v0.11.0
2026-10-21 15:27:33 -04:00
Dylan M. Taylor 4164951453 Merge pull request #2753 from dylanmtaylor/ci/shared-nix-workflow
ci: switch to shared nix workflow from craft-common-actions
2026-10-21 03:59:40 -04:00
Dylan M. Taylor d7481cca6e Merge remote-tracking branch 'origin/main' into pr-2753
# Conflicts:
#	.github/workflows/nix.yml
2026-10-21 03:41:36 -04:00
skylar taylor-barrick d695bc5654 fix(redact): evaluate OCMD visibility during sanitization (#3522)
* fix(redact): evaluate OCMD visibility during sanitization

Fixes #3499 by evaluating OCMD policies and bounded visibility expressions against the default optional-content configuration before removing hidden groups. Unknown or malformed visibility data is handled conservatively.

* fix(redact): preserve OCMD visibility during sanitization

* fix(redact): count hidden XObjects during verification

* test(redact): cover hidden form depth limit
2026-10-20 18:17:34 -06:00
Brandon ThomasandClaude Opus 5.5 22bedbf2af Skip CodeRabbit reviews on Dependabot PRs (#3692)
Adds .coderabbit.yaml so CodeRabbit no longer reviews or comments on PRs opened by dependabot[bot].

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:38:59 -04:00
Brandon ThomasandClaude Opus 5.5 b77d57000c Release 0.10.1: rebuild with the shared workflow's exact Linux tarball pick
0.10.0's Arch Linux and Flatpak aarch64 jobs picked up the
pdfcraft-cli tarball (storytold/craft-common-actions#35).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.10.1
2026-10-20 11:38:32 -04:00
Dylan M. Taylor 59a5a83774 docs: add SonarCloud and CodeRabbit badges to README (#3271)
* docs: add SonarCloud and CodeRabbit badges and standardize to flat-square

* docs: put SonarCloud and CodeRabbit badges on their own line
2026-10-20 11:29:59 -04:00
Brandon ThomasandClaude Opus 5.5 6fb6b7cf6f Release 0.10.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:21:10 -04:00
ec0a965074 M5.6: preserve nested annotation values in FDF exports (#1971)
* M5.6: preserve nested annotation values in FDF exports

Copy and renumber retained object graphs without expanding aliases or following
indirect cycles recursively. Refuse excessive work and propagate export errors
before the destination is written. Cover saved geometry, colour and borders,
shared graphs, streams, stale references and refusal through automation tools.

Co-Authored-By: Codex <noreply@openai.com>

* Bound FDF annotation copying before allocation

Keep selected aliases shared, budget retained keys and values before copying, and add direct-payload and alias-count regressions. Retain copied-graph limits and document source-memory scope.

* M5.6: bound FDF cache-eviction discovery and field values

Extend the concurrent annotation-budget correction while retaining its direct-payload and alias regressions. Record annotation positions instead of resolved bodies so cache eviction cannot pin repeated parsed copies; borrow direct values and budget retained entries before cloning. Count every emitted multi-select field value before allocating its output array.

Observed pre-fix allocation-cap and field-limit failures, then fixed tests passed. Final validation: 549 scoped XFDF/engine/automation tests passed, three existing ignored; strict all-targets Clippy, WASM, fmt, layers, assets, parity and diff checks passed.

AI assistance: OpenAI Codex for @MMexicann.

* M5.6: record bounded FDF export regression evidence

Record comment cache-eviction discovery and emitted-field-value budget regressions without changing feature scores or status. Describe copied-graph limits and existing source-parser limits precisely. Final 549 scoped tests, strict Clippy, WASM, fmt, layers, assets and parity passed.

AI assistance: OpenAI Codex for @MMexicann.

* M5.6: align FDF progress with budgeted discovery

Document lazy annotation positions, cache-eviction coverage and copied-graph limits while retaining the original nested-value progress entry and all upstream notes. Estimates unchanged.

AI assistance: OpenAI Codex for @MMexicann.

* M5.6: describe FDF discovery and copied-graph limits

Align exchange documentation with the tested lazy-position discovery implementation and its copied-graph limits. Source parsing/page discovery keep existing limits.

AI assistance: OpenAI Codex for @MMexicann.

* M5.6: align milestone notes with FDF discovery limits

Retain upstream milestone history and describe the tested discovery/copy budgets without increasing estimates or claiming a total source-memory bound.

AI assistance: OpenAI Codex for @MMexicann.

* M5.6: keep nested page links out of FDF exports

Preserve graph aliases and existing budgets; exclude direct and indirect Page/Pages type names. Add saved-PDF regression and bounded type-resolution controls. Both pre-fix failures observed; 46 scoped tests, strict Clippy, WASM and formatting passed.

* M5.6: document bounded nested page exclusion

Address maintainer feedback on the existing PR. Preserve upstream work and document the tested scope and remaining validation limits.

* M5.6: record nested page-reference regression evidence

Address maintainer feedback on the existing PR. Preserve upstream work and document the tested scope and remaining validation limits.

* M5.6: retain FDF limits in milestone evidence

Address maintainer feedback on the existing PR. Preserve upstream work and document the tested scope and remaining validation limits.

* M5.6: cite nested page-reference exclusion tests

Address maintainer feedback on the existing PR. Preserve upstream work and document the tested scope and remaining validation limits.

* M5.6: respect generations while identifying page references

Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.

* XFDF: document stale Type preservation regression

Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.

* M5.6: record generation-aware page exclusion evidence

Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.

* M5.6: propagate FDF export errors in usage example

Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.

* M5.6: consolidate roadmap change and target fields

Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
2026-10-19 12:21:37 -06:00
459379ef9b Edit text: refuse a rewrite of text that is a clipping path (#3447)
* Edit text: refuse a rewrite of text that is a clipping path

Editing text in a Photoshop-exported PDF destroyed the artwork on the page.
Such exports draw text in `7 Tr`: the glyphs are not ink but a mask, and a fill
or image is painted through them. Rewriting the run removed the glyphs and so
the mask, the fill behind flooded its own rectangle, and the file still opened
without a warning (#3429).

Measured on a page whose only text is a `7 Tr` mask over a red rectangle: the
fill covered 3.4% of the page before the edit and 100% after it.

`clear_block` already refuses these runs -- "text used as a clipping path
cannot be previewed for editing" -- but `replace_line` and `rewrite_block` did
not, so the destructive path was the one the editor offered. Both refuse modes
4 to 7 now, and the document is left untouched. Modes 0 to 2 paint ink and are
rewritten as before.

This does not keep the design: preserving it means emitting the replacement
inside the same `BT … ET` with the mode intact, which needs the writer to place
glyphs in a clipping object, as both reporters noted. Refusing turns silent
data loss into an error while that is written.

Found and analysed by @RavenCrowe666 and @mtlcl410, who converged on this fix.

* Check every run the rewrite removes, not the line's first one

The guard read `target.origin.state.render_mode`, the mode the line's first run
set. Two paths got a clipping run past it:

A line's runs need not share a rendering mode. Nothing in the join in
`text_lines` requires one -- it asks for the same stream piece, scope, text
object or neighbouring word, font, size, weight, slant and fill colour -- so a
line that starts as ink can continue as a mask, and the whole line's operators
are what the rewrite removes:

  BT /F1 48 Tf 40 60 Td 0 Tr (TITLE) Tj 7 Tr (MASK) Tj ET

reads as one line "TITLEMASK" whose origin is `0 Tr`. Replacing it passed the
guard and took the mask away with the ink.

`coincident_ops` drops another drawing of the line on the same baseline, so that
the replacement is all that shows -- a fake-bold second copy, an invisible OCR
layer. It matches on text, overlap and baseline, not on rendering mode, so that
copy can be the mask while the line being replaced is ink. Both rewrite paths
could drop it.

`TextLine` now carries whether any of its runs clips, and the guard reads every
line whose operators are in the removal set, the way `clear_block` already
checks every run it drops. In `replace_line_with_limit` the removal set is
computed before the replacement font is generated, so a refusal still leaves the
document untouched.

Both cases have a test; on the previous commit each returns
`Ok(LineEdit { substituted: None })`.

* Edit text: keep existing fixtures on text_page; build the clipping fixture on it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:03:54 -04:00
Earl Co e5033243f4 Add fills and border widths to text boxes and callouts (#3366)
* Support fills and border widths for text comments

* Allow borderless text comment defaults
2026-10-19 13:03:27 -04:00
FabianandClaude e45d5dca5c Tests: the parser-cancel test guards against a hang, not a 2 s limit (#1970) (#3462)
waiting_worker_can_cancel_before_initializer_returns gave its threads
2 s to start, and a busy FreeBSD VM in CI took longer. Its waits only
guard against a hang: the initializer is held until the end of the
test, so a waiter that can't cancel never returns, however long the
guard is. They now allow 600 s, like the hang guard in
page_contents_and_nested_forms_share_one_budget (#2812), which already
covers the second test named in the issue. The test polls with a 1 ms
sleep instead of yield_now, and the held initializer no longer panics
a second time when the test fails.

Checked by hand: with a 3 s delay at both threads' start the old test
fails at its deadline and the new one passes; with the waiter's
condvar wait made untimed (a real stall) the new test fails at its
guard.

Fixes #1970

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 13:03:13 -04:00
鍾承翰andClaude Sonnet 5.5 aebb333582 docs: base the XFA page area Acrobat observations on a synthetic form (#2574) (#3431)
The earlier notes cited an observation made on a public real-world form,
which the Privacy rule does not allow. Re-observed in Acrobat Pro on
synthetic forms (three breakBefore pageArea sections, then a page area with
occur min=1 max=1, with and without relevant="-print"):

- the once-only page comes last in both;
- when printing it is kept in both, blank only with relevant="-print";
- a breakBefore on the first section, with or without startNew, makes
  Acrobat open an empty body page first (5 pages); with no break on the
  first section it lays out 4, as PdfCraft does. Recorded as a gap.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 13:02:58 -04:00
5602fbba12 M10: Office export follows forms 50 deep and up to 50 000 a page and says where it stops (#3120) (#3426)
* M10: Office export reads forms as far as the renderer and says where it stops (#3120)

Export to Word, PowerPoint, HTML and RTF stopped at 12 nested forms and
4 096 forms a page and left the rest out silently. It now follows forms
50 deep, as the renderer does, and up to 50 000 a page (the renderer's
budget for nested paints), still bounded on hostile files.
doc_export_office lists pages past those limits in truncated_pages, with
a warning. New: reading_blocks_checked and export_office_checked;
reading_blocks and export_office keep their signatures.

Fixes #3120

Co-Authored-By: Claude <noreply@anthropic.com>

* Tests: pin the form depth limit to the renderer's 50 (#3120)

The depth test built its chain from MAX_FORM_DEPTH, so it moved with the
constant. Write the renderer's limit out: depth 50 is read, depth 51 is
left out and reported. Fails with the constant at 49 and at 51.

Co-Authored-By: Claude <noreply@anthropic.com>

* Export: say which forms count toward the page limit (#3120)

The page budget also counts forms the page draws itself, which the
renderer does not charge, so a page can be reported as cut although the
viewer shows it. The warning and the tool description now say so.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:02:44 -04:00
84bd488db3 M3: keep save shortcuts behind other dialogs (#3387)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:01:58 -04:00
Earl Co 8b88a2d14b M2: fix shading bounds in Type 3 glyphs (#3178) (#3412) 2026-10-19 13:01:14 -04:00
Giuseppe Silano b113896461 Never-crash gate on the oracle and testkit crates; oracle find() no longer panics on an empty needle (#3435) 2026-10-19 13:01:03 -04:00
Eric KadenandBrandon Thomas 0831d911b4 M6: a date typed without separators fills a yyyymmdd field (#3388)
* M6: a date typed without separators fills a yyyymmdd field

parse_date read each run of digits as one number, so 20261018 in a
yyyymmdd field was a single number with nothing left for the month and
the day, and the keystroke check refused it. Every date field on forms
that use the YYYYMMDD date format (US Army forms such as DA Form 31 among
them) refused every valid date.

order() now records each field's fixed width (4 for yyyy; 2 for yy, mm,
dd, HH, MM and ss) and whether the next field follows with nothing in
between. parse_date keeps runs of digits as text and splits a run between
such joined fields by those widths. Formats with separators, month names,
literal digits (dd.mm.26) and two-digit years behave as before.

A run of digits too long for a u32 used to be dropped silently, which let
"2026 99999999999" pass for yyyy; it is now read like any other number and
refused when it is left over or does not fit.

* chore(contributors): credit Eric Kaden by real name

* M6: separated runs of digits stay whole in a joined date format

With separators each run of digits is one field's number, as before this
branch: "2026-10-18" in a yymmdd field reads the year 2026, where the
first commit split the run into 20 and 26 and refused the date. A run is
now split between joined fields only when fewer runs were typed than the
fields still to fill, so "2610" and "261018" still split.

* M6: what is left of a split run of digits fills its field exactly

"202610018" in a yyyymmdd field split off 2026 and 10 and read the rest,
018, as day 18, so a nine-digit date passed. The rest of a split run now
has to be its field's width; a number typed on its own ("2026 10 018")
keeps its leeway.

* M6: a number typed on its own is split only when its rest fills the next field

The optional time fields of a format like yyyymmddHHMM counted as fields
still to fill, so "2026 10 018" split the day 018 into 01 and 8 and was
refused; on main the time defaults to 00:00. A run typed on its own is
now split only when what is left can fill the next field; the rest of a
run that was already split is split as before.

* M6: describe the undelimited-date tests without naming one form

The tests and their comments now say what they check (fields written side by side, such as yyyymmdd) instead of naming the form that showed the problem, and use a generic field label.

* M6: count date and time fields apart when deciding to split a run

Optional time fields counted towards the fields still to fill, so
"2026-10-18" in a yymmddHHMM field split the year into 20 and 26 and was
refused, where main reads it with the time left out. A run typed on its
own is now matched against the fields of its own kind: date runs against
date fields, time runs against time fields. What is left of a split run
goes on being split along the fields written side by side, so
"20261018143005" still fills yyyymmddHHMMss. A mix such as
"2026 1018 1430" (year apart, month and day joined, then a time) is still
refused, as on main.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:00:52 -04:00
Eric Kaden a30dde2225 M6: the date picker follows a whole date typed while it is open (#3393)
The calendar took its month once, when it opened, so a date typed while
it was open showed only after leaving the field and coming back. It now
moves to a date as soon as the text becomes one, and stays on the month
it shows while the text is half typed or unchanged, so its arrows still
turn the months.
2026-10-19 12:58:15 -04:00
Eric Kaden caaf058815 M6: a secured form's notice bar still says it is a form (#3404) 2026-10-19 12:58:03 -04:00
32abf71049 Text: tracked glyphs retain their word boundaries (#3477)
* Text: tracked glyphs retain their word boundaries

* M7: cap inferred tracking at half an em so one-glyph words and cells keep their spaces

Adds the regression test from the #3477 review (fails without the cap).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:57:52 -04:00
KnottyDyes 1bb3e916fe Docs: install the CLI and register it with MCP clients (#3478) 2026-10-19 12:57:29 -04:00
skylar taylor-barrick e397efa854 Merge pull request #3285 from mtlcl410/fix/struct-refs-parent-tree-2532
M8: redaction's structure-tree walks start from the parent and ID trees too (#2447, #3302)
2026-10-19 10:05:04 -06:00
43550c448d M2: a JP2 image in a colour space the decoder refuses is drawn by its component count (#3306) (#3363)
* M2: a JP2 image in a colour space the decoder refuses is drawn by its component count (#3306)

The veraPDF fixture's JPX header names CIEJab (enumerated colour space 19),
which hayro-jpeg2000 refuses, so the image failed and the page drew blank.
When the decoder refuses the image and its colr boxes name such a space, it is
decoded again from a copy naming greyscale, sRGB or CMYK by component count,
as MuPDF and Poppler draw it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* M2: copy the JP2 stream only when a colour enum needs replacing (#3306)

Review follow-up on #3363: a stream the decoder refused for another reason was
copied for nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* Renumber the JP2 colour-space patch (21): #3378 takes hayro-syntax (20)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 02:50:58 -04:00
cbbeb61650 fix(parse): accept spaces between stream and its line break (#3378)
* fix(parse): accept spaces between stream and its line break

Microsoft Reporting Services writes `stream \r\n`, a space before the line break.
hayro-syntax and pdfcraft-cos took the stream data to start at the space, so the
content stream failed to inflate: the page rendered blank (white, or black with
inverted colours) and an edited save corrupted the stream. Spaces and tabs are now
skipped, but only when a line break follows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Number the stream end-of-line patch (20), as vendor/README does

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 02:50:21 -04:00
mtlcl410andClaude Opus 5.5 b955c2eefb M2: a tiling pattern drawn once shows its cell at full scale (#3349) (#3356)
* M2: a tiling pattern drawn once shows its cell at full scale (#3349)

PowerPoint writes a picture fill as a tiling pattern with /XStep 2147480100.
Fitting that step into the 3000 px cell pixmap shrank the picture below a
pixel. When what is painted can only show the first cell, the cell's /BBox is
drawn at full scale with a transparent border and padded instead of tiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* M2: a tiling cell whose scale isn't finite keeps the old path (#3349)

Review follow-up on #3356: a /BBox 1e-40 wide passes the zero-area check and
gives an infinite cell scale, which would send non-finite geometry to the cell
renderer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* M2: a stroke's miter counts in the one-cell check (#3349)

Review follow-up on #3356: shows_one_cell took the line width as a stroke's
reach; a miter join reaches half the width times the miter limit. The reach
formula patch (9) uses is now one function, stroke_reach, used by both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* M2: a skewed stroke's reach uses the transform's largest stretch (#3349)

Review on #3356: under a skew, a direction between the axes stretches
further than either axis, so the larger axis stretch could place a miter
tip short of the next cell when it reached it, and the one-cell path left
that cell unpainted. The reach now uses the spectral norm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:50:09 -04:00
TomCatTandClaude Opus 5.5 4d42b48d20 fix(sign): offer only certificates that may sign documents from the Windows store (#3365)
A CAC puts its encryption, logon and signing certificates in the store
under the same name, so the digital ID list showed several identical
entries and only one of them makes a signature that validates. Grey out
the ones whose key usage or extended key usage doesn't allow document
signing, with the reason, the same way an ID without a usable key is
already treated.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:49:57 -04:00
mtlcl410andClaude Opus 5.5 2f21f920a0 fix(windows): name the single-instance socket by the session id, so a launch without SESSIONNAME finds its session's window (#3338) (#3340)
* fix(app): a launch without SESSIONNAME hands its files to the console session's window (#3338)

Task Scheduler, services and some remote shells start PdfCraft without
SESSIONNAME, so the launch looked for instance.sock while the window opened
from Explorer listens on instance-Console.sock, and a second window opened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* fix(windows): name the single-instance socket by the session id, not SESSIONNAME (#3338)

Review on #3340 (earlvanze): falling back to the console's socket when
SESSIONNAME is unset could hand a file from a Remote Desktop session to a
console window of the same user in another session. The socket is now named
by the session id in the process token (winsafe, no unsafe): a launch from
Task Scheduler runs in the user's session and finds its window, and
different sessions never share a socket. SESSIONNAME names it only if the id
can't be read.

Tested with a real scheduled task: on main the task's launch (no
SESSIONNAME, session 1) opened a second window; with this change the file
went to the running window in session 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:49:46 -04:00
6733e073ef Keep long menus usable in short windows (#3374)
* M14: keep long menus usable in short windows

* Menus: scroll each top-level menu once, from its caller, so View's list isn't nested in a second scroll area

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:49:34 -04:00
7d9adcb8f1 fix(preflight): identify PDF standards by XML namespace (#3364)
* fix(preflight): identify standards by XML namespace

* fix(preflight): bound XMP attribute parsing work

* fix(preflight): cap XMP during stream decoding

* Preflight: read the PDF/A and PDF/UA identification by its usual prefix when the XMP can't be parsed as XML

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:48:40 -04:00
Earl Co 236ce9a064 Clear image pixels intersected by redaction marks (#3368) 2026-10-19 02:48:29 -04:00
Earl Co 66f1085688 Keep document scroll position across viewer modes (#3362) 2026-10-19 02:48:18 -04:00
MarekSpodBiedry 9d23505dd7 Clamp the zoom-anchor scroll to the content so zooming out no longer jolts the page (#3266) (#3350) 2026-10-19 02:48:06 -04:00
Earl Co 6e61b50503 Fix clock-dependent form properties test (#3367) 2026-10-19 02:47:55 -04:00
skylar taylor-barrick cece9f5ccc M8: document discard-on-error for public redaction calls (#3302) 2026-10-18 16:45:35 -06:00
Chiranjeevi U 4db6ac4ae2 M3: Accelerate coarse wheel scrolling (#1094)
* Viewer: accelerate coarse wheel scrolling (#1072)

Line/Page notches moved a fixed eased step, so a fast spin felt slow.
Accumulate one frame of coarse input per axis, scale by arrival rate
(1.0 idle, up to 6.0 spinning), and apply it directly like precise
touchpad input.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* Viewer: add only the fast-spin surplus on top of egui smoothing

Single notches flow through egui untouched, so isolated input matches
the scroll speed preference exactly; page units are never boosted.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

---------

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
2026-10-18 16:15:39 -06:00
dab3149110 M2: a fax image with fewer /Rows than its /Height is decoded to its height (#3308) (#3316)
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 17:08:32 -04:00
6fd49d6565 M1: a saved file keeps its creation date (#2658) (#3291)
* M1: a saved file keeps its creation date (#2658)

Every save writes a staging file and renames it over the target, so the
file's creation date became the save time. Before the rename the staging
file now takes the replaced file's creation date, where the system lets
a program set one (Windows and macOS, through std's FileTimesExt;
best effort). A new file keeps its own date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* Staging: compile the creation-date step only where it applies; say so in write_atomically's doc

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 17:08:02 -04:00
NetterSilverandClaude Haiku 5.5 c00ee14be8 Find and edit images whose Do or cm spans two content streams (#3294)
Fixes #3113

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 17:07:51 -04:00
NetterSilverandClaude Haiku 5.5 e2d4814089 Read /ActualText from named property lists in edit text and export (#3301)
Fixes #3110

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 17:07:39 -04:00
mtlcl410andClaude Opus 5.5 16f36810cc M7: work out each font's space width once per paragraph rewrite (#2616) (#3297)
* M7: work out each font's space width once per paragraph rewrite (#2616)

Each keystroke in Edit text rebuilds the paragraph's draft. When a source
font can't show a space (LaTeX and Sphinx subsets space words with TJ
kerning instead), source_space reads every font on the page, building
Metrics for each. The draft did that once per character, so a keystroke
in a 380-character paragraph of NVIDIA's vGPU guide took ~460 ms. The
widths are now worked out once per font before the per-character pass:
~71 ms there, ~16 ms for a 200-character paragraph (was ~255 ms), with
identical layouts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* M2: work out space widths only for the fonts a paragraph uses (#2616)

Review follow-up: the eager map scanned the page once per page font even for a
short edit on a page of many fonts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:07:29 -04:00
oracola91 628f43e17a fix(ui): the recovery folder is owner-only on Unix (#3320)
Recovery snapshots are unsaved copies of open documents. The folder was
made with the default mode, so on a shared Linux or macOS machine other
accounts could list it and read the snapshots of documents that are not
encrypted. The folder is now made owner-only (0o700), and a folder an
earlier version made is tightened when an instance starts or writes a
snapshot, as image_pick does for its folder. A volume that can't change
modes (FAT, some network shares) works as before, with one warning in
the log.
2026-10-18 17:07:17 -04:00
mtlcl410andClaude Opus 5.5 6b099bef72 docs: vendor/README keeps one hayro-interpret row, with the patches lost to number collisions (#3312)
Merges left four hayro-interpret rows; the current one lacked the
transparency-group, Type 3 rendering-mode and Lab white-point patches (each
numbered 25, 27 or 28 on a branch where another patch took that number).
They are now (41), (42) and (43) in the one row, with their tests.


Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:07:05 -04:00
104411dc7a Read doubled-sign integers like --280 in the COS lexer (#3299)
* Read doubled-sign integers like --280 in the COS lexer

Fixes #3111

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* COS lexer: strip the doubled sign without allocating

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 17:06:53 -04:00
Morten Husted Lochmann f280ca8e6d M10: exported TIFF and JPEG pages record the resolution they were drawn at (#3332)
* M10: exported TIFF and JPEG pages record the resolution they were drawn at

A page exported as TIFF said 1 x 1 with no unit, whatever the dpi, and
a JPEG export said nothing, so other programs showed and printed them at
a default (a 300-dpi page shown at 72 dpi is four times too big).
encode_image now takes the dpi: TIFF X/YResolution in inches, the JFIF
density in dots per inch.

PNG stays without pHYs. It counts pixels per metre, so 72 dpi would read
back as 72.009, and a page made from an exported PNG (doc_create from
images, as enhance_camera_image_through_tools does) would come out 0.01%
small.

Exporter records the resolution a page was actually drawn at (dpi_used),
so a page too large for the renderer says the lower one. render records
--dpi, since it refuses oversized pages. The TIFF is written with the
tiff crate, already in the graph, because image's TiffEncoder can't set
a resolution.

* M10: TIFF page exports are LZW-compressed, as documented

encode_tiff's comment said "LZW-compressed by the encoder's default",
but the TIFF was written uncompressed: a Letter page at 300 dpi came to
25 MB. It is now LZW without a predictor, the compression TIFF readers
support most widely after none.

* Contributors: add my name for the About window
2026-10-18 17:06:41 -04:00
58795e9188 cli: info names the standard a document declares (#3328)
* cli: info names the standard a document declares

A PDF/A file was summarised with no mention of PDF/A. `info` prints two dozen
fields -- tagged, xfa, javascript, layers -- and said nothing about the
standard, while the app's Standards panel reads the same declaration from the
catalog's XMP and shows "PDF/A-2b" (#3319).

`info` gains `pdfa` (the part and conformance together, e.g. "2B"), `pdfua` and
`output_intents`, from `pdfcraft_preflight::declared` -- the call the Standards
panel makes, so the two agree by construction rather than by a second reading
of the metadata. A document that declares nothing leaves them null and empty.

Read in the CLI rather than in `pdfcraft_render::Inspect`: render is layer 3
and preflight layer 4, so the summary struct can't reach it, while an app may
depend on any layer.

* cli: read the declaration from the bytes already in hand

`Document::open_with_password` takes an `Arc<Vec<u8>>` and `info` already holds
one, so `bytes.to_vec()` copied the whole file for every call -- a file-sized
allocation on a large PDF, for nothing. Clone the `Arc` instead.

* info: name the declared PDF/A level as the automation tools do (PDF/A-2b)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:06:29 -04:00
NetterSilverandClaude Haiku 5.5 8d6254aa0b Render weekday letters in FormCalc date pictures (#3321)
Found while testing crates/js/src/formcalc.rs:1929-1957 (format_date), with DateFmt returning "EEEE, MMMM D, YYYY" at :2333

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 17:06:18 -04:00