attachment_add compared every attachment name, including files attached to pages as comments, so a page comment with the same file name made the reported key empty. It now compares only the document-level name-tree keys. Deleting the last document attachment when the catalog's /Names is an indirect dictionary left an empty /Names behind; it is now removed from the catalog, as the direct case already did.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A font naming a Latvian comma-below letter by its code point (/uni0136) or by
the glyph list's name for it (/Kcedilla, /Gcedilla...) still drew nothing:
the name resolves to the cedilla name the glyph list prefers, which the
composite table does not hold. The thirteen comma-below letters are now
looked up by character, which also replaces the special case for U+021A and
U+021B. T and t with a cedilla (U+0162, U+0163) are not in that table and
keep their cedilla.
Raised by CodeRabbit on the PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`[(FIRSTARR)] [(LASTARR)] TJ` is malformed (TJ takes one array
operand), but the operator still shows the one immediately before it,
as the renderer, MuPDF and pdftotext agree. Three readers here counted
operands from the front instead and showed the discarded first array:
- crates/edit/src/text.rs (text_lines/text_paragraphs, and the Office
export through reading_lines)
- crates/redact/src/interp.rs (search-based redaction's content
interpreter)
- crates/a11y/src/headings.rs (autotagged heading detection)
So the editor could offer a word the page never draws, a redaction
search could miss it, and a heading could read as text nobody sees.
Take the last array for TJ in all three, matching the `_ =>
op.operands.last()` arm already used for Tj/'/" alongside it.
Added a regression test per crate (`a_tj_with_a_surplus_array_reads_
the_last_one`, `a_tj_surplus_arrays_undrawn_glyphs_are_not_seen_
under_a_mark`, `a_heading_with_a_tj_surplus_array_reads_the_one_the_
page_draws`), confirmed each fails without its fix and passes with it.
cargo test -p pdfcraft-edit (196 passed) -p pdfcraft-redact (81
passed) -p pdfcraft-a11y (44 passed), fmt, clippy -D warnings, xtask
layers and xtask assets all clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
load_certificates now reads PKCS #7 certificate bundles (degenerate
SignedData, RFC 5652 §5.2) in DER and as PEM PKCS7/CMS blocks, the way
Windows, Acrobat and certificate authorities export chains. PEM END
lines must match their BEGIN label.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The stand-ins for the standard fonts (PDFium's Foxit fonts) have neither the
comma-below letters nor a commaaccent glyph, and the composite-letter patch
left comma accents out. A non-embedded font naming /scommaaccent therefore
drew nothing, which is the blank "si" left in Romanian XFA forms, and
/tcommaaccent drew a cedilla through the glyph list's alias to U+0163.
Romanian s/S/t/T and Latvian k/K/l/L/n/N/r/R/G with a comma below are now
built from the letter and the font's comma, scaled to three quarters and set
just under the baseline, moved back along the slant in italics. The uni021A and
uni021B names of T and t with a comma below, which the glyph list
does not name, resolve by code point. Lowercase g with its turned comma above
is still not built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New settings start with Highlight fields on. A stored choice is kept; a missing or
malformed value falls back to on. Preferences > Appearance gets the checkbox.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The harness window is live, so open_path hands the read to the background
worker and run_steps(2) could not promise the worker had finished: under
load the file could be deleted before it was read, failing the test with
"no second tab". wait_until polls for the tab the way the drop tests
already do. Proven by delaying the worker 300 ms: the old test fails at
views.len() == 0, this one passes.
Conflicts: crates/automation/tests/automation.rs (code resolved by hand: crates/automation/tests/automation.rs). Catalogs keep upstream's rows, then this branch's rows upstream lacks; ATTRIBUTION.toml is upstream's with every sha256 recomputed; progress lines keep both sides, upstream's first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A content-type attribute value or an eContentType that was not an OID
was read as missing, which skipped the check that the two match. Such a
CMS is now not read, and neither is one without an eContentType.
A PDF signature's eContentType must also be data. With a content-type
attribute this already follows from the checks on the attribute.
Without one, a signature whose eContentType named another type could
still validate as a signature of the document. It is now invalid.
The PDF test now covers a wrong eContentType and a repeated
content-type attribute, and the README says that a missing
content-type attribute is read anyway, with a note.
RFC 5652 allows one content-type and one message-digest attribute in
the signed attributes, each with one value. The parser kept the last
attribute and the first value, so a CMS with two of either could mean
one thing here and another in a different reader. Such a CMS is now
not read.
The content-type attribute must also name the type of the encapsulated
content. The signature covers the attribute but not eContentType, so a
CMS where the two differ is not read either.
CodeRabbit: Read mode leaves the Edit tool picked, so the recognition Edit
starts could run in Read mode and change a scanned document; and it could run
while a dialog or prompt was open, which PdfCraftApp::modal_open says nothing
may act beneath. It now waits for Edit to be on outside Read mode with no modal
open, and is tried again once they are gone.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A button was given its content area (inside its margins) as its widget
rectangle, though its border belongs to the field: DA Form 31's
Instructions button, with a 1.25 pt top margin, came out shorter than
the Attachments Menu button beside it. Its box is now the whole field;
the margins pad the caption.
Appearance text in a simple font was measured with Helvetica's widths,
whatever the font: a bold or Times caption or value came out narrower
than drawn, so centered and right-aligned text slid right of where it
belongs. Text is now measured with the font's /Widths, or a standard
font's published widths.
Button appearances always drew the caption in regular Helvetica, sized
from the button's height, whatever the field's /DA asked for: DA Form
31's 'Attachments Menu' (9 pt bold) came out at 4 pt. The caption now
takes the /DA's font from the form's /DR, its size (an automatic size
still fits the button) and its color, as text fields already do.
A button with no height was sized like a text field in its field font,
with text-field padding, so DA Form 31's Instructions button (10 pt
field font, 8 pt caption) came out about 17 pt tall and over the form
below it. It is now as tall as its caption, never below its minimum.
A draw or a button with no width took all the width available, so a
right-aligned label slid to the far side of its subform and a button
spread over what lay beside it: DA Form 31's Instructions button covered
its header and the email line, and its PERSONNEL/FINANCE line ran into
the field after it. They now grow to their text, unwrapped, within what
is available and never below their minimum width, as containers do.
A word of spaces alone draws nothing, but after a word whose own
trailing space ran past the box it failed the fit test and opened a
blank line, so the text measured a line taller than it draws. Such
words now always stay on the line, keeping their width.
Review on #2715: reverting only the content_list guard still passed. The fixture now has
text added by PdfCraft (`/PCAdded`) on page 6, and content_list must report it at its
unchanged rect [70 70 150 100]. form_set_props must refuse a rect for the field on page 6
with "name is on page 6, which could not be read" and leave form_fields unchanged.
Reverting any one of the link, content or form guards now fails the test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The layout draws a non-interactive field's text itself, condensed, but
measured its height at full width, so a condensed value reserved lines
it didn't use. Fields now measure and draw their text in one face:
scaled when the layout draws it, as designed when the forms layer does
(the over-page fallback included).
CodeRabbit review on #3471: even with the gap >= 0.0 overlap guard, two
equal-size rows with no horizontal overlap (not just overlapping ones)
could still land inside the widened 50% baseline tolerance at a 5pt
shift that the original 20% tolerance would have rejected, merging
distinct body-text lines into one row.
The wider tolerance is meant for a run distinctly smaller or larger
than its neighbour (a super/subscript), not two ordinary same-size
rows. Require prev.size.min(line.size) < prev.size.max(line.size) * 0.9
before taking it; equal-size runs keep the original 20% tolerance
regardless of overlap.
Added equal_size_rows_need_the_tighter_tolerance_even_without_overlap,
confirmed it fails without this change (merges into one row/block) and
passes with it, alongside the two existing regression tests from the
earlier rounds of this fix. cargo test -p pdfcraft-edit (198 passed),
fmt, clippy -D warnings, xtask layers and xtask assets all clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Words keep the space after them, and the line-fit test counted it, so a
label written with a trailing space wrapped when its box was a fraction
of a point narrower than the label plus that space: DA Form 31's 'a.
DATE ' and 'b. TIME ' put their second word on top of the field below.
A line now fits by what it draws, and alignment ignores every trailing
space, not just one.
* 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>
When the current page's label is not its page number (front matter
numbered i, ii, then 1, 2 …), the rail shows "3/180" under the page box
instead of only the page count, with "Page 3 of 180" on hover. It falls
back to the count when the text would not fit the rail. The page box
itself already shows and accepts labels since #1667 (0.8.0 showed the
physical index).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`link_list` and `form_set_props` indexed the viewer's pages with a page number from the
editing layer. On a file whose page tree has a dangling `/Kids` entry (#2660) the viewer
sees three pages and the editing layer six, so `link_list` panicked ("the len is 3 but
the index is 3") and so did moving a field on one of the later pages.
Rectangles on a page the viewer could not read are now reported unchanged, as `doc_info`
already reports them (`link_list`, `comment_list`, `content_list`); `comment_list` no
longer borrows the last page's geometry for them. Placing a rect on such a page is an
error (`form_set_props`, and `sign` when the viewer has no page at all).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.125 × 9.5 in (297 × 684 pt) in the print dialog's Paper list and
doc_print, fed to CUPS as Env10. Appended after the 4 × 6 label so
LABEL_PAPER keeps its index.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a handle of a Fill & Sign line, check, cross, dot, typed signature
or comment stamp was dragged, only the selection frame followed the
pointer; the stamp kept its old size (or place) until release. Image
signatures already had a live layer. Every stamp now gets one: the page
without the stamp, and the stamp drawn alone on a transparent page,
whose pixels are stretched with the frame. Resizing a stamp only changes
its /Rect, which its appearance is stretched to fit, so the preview is
what the release produces.
- render: RenderConfig.transparent clears the page to transparent.
- annot: isolate_annotation leaves a page showing one annotation alone.
- engine: Document::stamp_preview builds the two read-only layers; the
page beneath gets the appearances drawn for comments without one.
- ui-egui: the signature drag layer covers every stamp, rendering the
stamp's own tile sharp at the current zoom.
Review follow-up on #3497: the editing map in crates/fonts and the vendored
renderer each read an identity /ToUnicode name as code = Unicode without
controls or lone surrogates; each now names the other, so a change to one
is made to both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LymhVaGZCWwB9HNEbLzeMM
fontHorizontalScale on a font, and xfa-font-horizontal-scale in rich text,
condense the glyphs; PdfCraft ignored both, so condensed labels were laid
out at full width and overflowed or wrapped where the form fits them on
one line. Text is now measured at its scale and written with Tz, in a
graphics state of its own. Field values keep full width: the forms layer
draws them, and its appearances have no horizontal scaling.
CodeRabbit review on #3471: the widened baseline tolerance (half the
larger run's size, for a same-row footnote marker) could also let two
genuinely stacked rows of ordinary body text join into one row, if
horizontal compression (Tz) narrows the first row enough to put the
second row's start within it — a negative `gap` inside the existing
overlap allowance, with a baseline shift inside the new 50% band but
outside the original 20% one.
Only take the wider tolerance when the two runs don't overlap
horizontally (`gap >= 0.0`), which is true for a footnote marker drawn
right after the text it follows. Overlapping runs keep the original,
tighter 20% tolerance.
Added stacked_rows_that_merely_overlap_do_not_join_into_one_row,
confirmed it fails without this change (merges into one row/block) and
passes with it. cargo test -p pdfcraft-edit (197 passed), fmt, clippy
-D warnings, xtask layers and xtask assets all clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CodeRabbit: the recognition Edit starts used the same notifications as a run the
user asked for, so turning Edit on for a page with text of its own (skipped by
the run) showed "Recognizing text..." and then "Recognized 0 words on 0 pages;
1 page already had text". The run Edit starts is now marked automatic: it shows
no progress toast, and no completion toast when it added no words. A run the
user asks for, and an automatic run that did read words, notify as before.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>