Commit Graph
4870 Commits
Author SHA1 Message Date
FredApps 784ce9212b Merge pull request #6497 from helenkwok/web/page-api
pdfcraft-web: window.pdfcraft, the page command API from craft-web-api, opt-in with ?control
2026-11-07 15:39:11 +01:00
FredApps a94e90da65 Merge pull request #6471 from ZIDAN44/fix/6440-add-text-cmyk-gray
Add text: gray or CMYK colour, painted with g or k (#6440)
2026-11-07 15:39:04 +01:00
FredApps 09a63d6dd6 Merge pull request #6197 from KenM76/feat/rich-copy
M3: copy keeps formatting as HTML (Copy with formatting)
2026-11-07 15:38:59 +01:00
FredApps 21747a4e58 Merge pull request #5802 from KenM76/perf/failed-object-cache
perf(cos): an object that can't be read is searched for once, not on every read
2026-11-07 15:38:52 +01:00
mtlcl410andClaude Opus 5.5 7de85d520c M1: a hybrid update without /Prev is rebuilt by the renderer as by cos (core.repair-broken-files)
The renderer (hayro-syntax (44)) counted the sections its chain reached, and
a hybrid section's /XRefStm stream (ISO 32000-2 §7.5.8.4) counted as one, so
a hybrid update without /Prev was rebuilt by cos but not by the renderer. It
now asks what cos asks: whether the newest section names an earlier one
through /Prev. The /XRefStm stream belongs to the same update.

Tests: a synthetic hybrid fixture (pdfcraft-testkit), with and without
/Prev, in cos and render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-07 22:29:49 +08:00
mtlcl410andClaude Opus 5.5 6847b1ea7e M1: a file whose last update has no /Prev opens with all its pages and a repair note (core.repair-broken-files)
An update whose cross-reference section has no /Prev (ISO 32000-2 §7.5.6,
§7.5.8.2) cuts the earlier revisions, and the page tree with them, off the
chain. The signed PDFBOX-2720 and PDFBOX-2942 files have this shape: the
document model found no page tree and the renderer showed 2 blank pages
found by scanning for page dictionaries.

When the chain reaches only the newest section, the file holds another
startxref and the catalog's /Pages can't be read, both readers now rebuild
the table from the file's objects, as for a damaged file:

- cos: the existing reconstruction (newest in-file definition wins), a
  repair-log line saying why, and `Document::rebuilt_unlinked_update`.
- hayro-syntax (44): `root_xref` repairs the table before the page tree is
  read; `Pdf::rebuilt_unlinked_update` says so.
- render: an open warning, and the lazy structure reader accepts such a
  rebuilt file, so links, fields and bookmarks are read too.

A save rewrites the file in full (main's rule for reconstructed files).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-07 22:22:47 +08:00
Grzegorz MartensandClaude Opus 5.5 0916bec6ef M2.6: a font's streams read /DecodeParms given by reference
The redaction proof now reads a font's /ToUnicode with its /DecodeParms given by reference, and
`Metrics::from_dict`, which redaction takes the removed text from, read it only with direct ones.
Behind a predictor so described, the two got different text for the same codes, so a copy the
redaction rightly left counted as a survivor and the proof failed a sound redaction
(`a_unicode_map_behind_parameters_given_by_reference_is_read_alike`).

Every stream `from_dict` reads now goes through `Document::with_direct_parms`: the /ToUnicode; a
composite font's /Encoding, which sets how its codes split
(`cmap_streams_read_their_parameters_given_by_reference`); a subset's program and /CIDToGIDMap,
which say which of its CIDs have a glyph
(`a_subsets_program_and_cid_map_read_their_parameters_given_by_reference`); and the program a
simple font without /Widths is measured with
(`a_font_without_widths_measures_with_its_embedded_program`). Read without them, as on `main`,
such a font was measured with widths guessed from its name, so redaction removed glyphs the mark
doesn't cover and left some it does, and the proof, which also guesses from the name, passed.
Redaction now measures such a font with its program. The proof still guesses, so it fails that
redaction, as it already does on `main` when the program's parameters are direct.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 13:49:52 +01:00
Grzegorz MartensandClaude Opus 5.5 1d0c303466 M2.6: the redaction proof reads /DecodeParms given by reference
The proof that came with #5557 decodes every stream it sweeps through `Ctx::decode`, which called
`decode_strict` with the stream alone. This PR's `decode_strict` also takes the document, to read
`/DecodeParms` given by reference, so the branch merged with main no longer built. `Ctx::decode`
now takes the document, and each of its callers passes the one it reads, so the proof reads such
a stream as redaction reads page content and forms. Before, it decoded the stream with the
parameters' defaults: a PNG predictor's rows kept their tag bytes, and a copy of the removed text
split by them went unreported (`a_survivor_behind_parameters_given_by_reference_is_found`). The
test helpers' streams are direct, so they pass an empty document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 13:15:45 +01:00
Josias Eli Rodriguez 65ebdab392 redact, annot: usable quad values on the f32 grid; appearances resolve indirect quads
Review round on #6525 (CodeRabbit, plus eliets2's first-pass finding):

- usable_quads returns usable values on the f32 grid, so readers folding
  f32 and f64 agree on the numbers as well as on the usable set
  (67108869 is 67108872 for every reader). New geom test pins it.
- The end-to-end redact agreement test gains the 4e38 case.
- Appearance drawing read /QuadPoints off the raw dictionary, dropping an
  indirect array or indirect entries the other readers keep.
  appearance::build now takes the document and resolves both, as redact's
  reader does; the restyle probes pass the document they already hold.
  New test pins both indirect shapes against the direct output.
- One shipped test block reformatted (rustfmt).

Drafted with AI assistance; verified by the contributor, fail-before
included.
2026-11-07 05:58:15 -06:00
Grzegorz MartensandClaude Opus 5.5 d1de0a4bad M2.6: autotagging's reason for content it can't decode reads as on main
Since autotagging reads page content through DecodeBudget::decode_strict, the reason it gave for
content that doesn't decode carried that error's own text, so it said twice that the data
couldn't be decoded: "content cannot be decoded strictly: stream data could not be decoded:
FlateDecode: …". It now gives the decoder's reason right after, as main does: "content cannot
be decoded strictly: FlateDecode: …".

Test: autotag_text_says_why_it_cannot_read_a_page now checks the decoder's reason follows the
prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 104e22904a M2.6: shorter wording for content in or after damage
Marking a figure decorative or a tag's content as an artifact refuses content that may be in a
damaged or unreadable content stream, or after the damage. The two errors now say so in one
clause, and that nothing was changed, which holds: both return before touching the document. The
mark-as-artifact tool's description names the case in a few words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 c3ca063373 M2.6: a test pins that pruning reads a long chain of forms only to the depth limit
A chain of 100,000 forms, whole or each damaged partway, is read only down to the depth limit, so the walk's recursion stays bounded: a_long_chain_of_forms_is_read_only_to_the_depth_limit. Nothing pinned it: with the depth step dropped in either of the two calls that go one form deeper (reading a form whole, and reading one that is damaged), or the depth check in the walk of damaged content made to apply only with the read-once guard, the walk follows the chain to its end and the stack overflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 79ea9154cf M2.6: autotagging says decoding takes more than 16 MiB, not that the content does
Read through DecodeBudget::decode_strict since cbc3fc221, a stream counts what each of its filter stages decodes against the 16 MiB, as a page's decode allowance does, where main bounded each stage alone. A page of 12 MiB behind ASCIIHex and Flate, with 9.1 MiB between them, was tagged on main and is refused now; the refusal said the decoded content exceeds 16 MiB, which it does not. It now says decoding the content takes more than that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 477f0abd49 M2.6: a test pins why autotagging refuses a page it cannot read
Content past 16 MiB says so, and content that does not decode strictly says that: autotag_text_says_why_it_cannot_read_a_page. Mutation testing found nothing pinned which message each gets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 d289038ca0 M2.6: autotagging reads page content with its /DecodeParms given by reference
Autotagging a text-only page rewrites its content, marked, from what it
read. It read each stream through its filters with /DecodeParms only
when direct, so content behind a predictor whose parameters are given
by reference was read with the predictor's tag bytes in it, and written
back so: with eight-byte rows, a \0 every eight bytes split `24 Tf` into
`2 4 Tf` and the text's string, and the tool returned ok. Found by the
consumers review of the CodeRabbit round; inherited from main.

It now reads through DecodeBudget::decode_strict, which reads the
parameters in the document and is strict on damage as before, within
the same 16 MiB. pdfcraft-a11y no longer uses pdfcraft-filters.

Test: autotag_text_reads_content_with_parameters_given_by_reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 084fa0927e M2.6: say why pruning reads a form's /Subtype only when direct
The sweep flags the read in Pruner::form, moved there by this change. A /Subtype given by reference reads as no form wherever the workspace tells forms from images; resolving it is a change across all of them, and the comment says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 75bf5ed264 M2.6: a test pins that pruning reads the forms of damaged content at the depth limit
Forms are read 8 deep. A damaged form that deep has every form it
names read as drawn, so one drawing through resources another page
shares keeps what it draws: a_damaged_form_at_the_depth_limit_has_its_forms_read.
Mutation testing found nothing pinned that boundary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 749a323342 M2.6: tests pin damage in an earlier filter stage, on another page, and in the saved file
- cos: a filter chain whose first stage stops at damage and whose
  second reads all it was given is damaged.
- optimize: a page damaged partway stops resampling for the whole
  document, so another page drawing the same image whole doesn't get
  it resampled.
- redact: after redacting content with /DecodeParms by reference, no
  stream in the saved file holds the text, read with its parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 8bf7412220 M2.6: pruning reads every form that damaged content may draw
Earlier in this change, a page or form damaged partway, like one that
doesn't decode, kept its own /XObject dictionary whole and was read no
further. So a form it draws was never read: when the form's resources
are shared with another page, the names the form draws there went, and
extracted or split pages lost those images. On main the part before the
damage was read, so forms drawn there kept theirs, and forms drawn past
it didn't.

Now every form the resources of such content name is read as drawn, as
any of them may be drawn before the damage or past it. Each set of
resources has its forms read once.

Test: extract_keeps_images_forms_drawn_around_damage_draw_through_shared_resources,
with a page and a form each damaged before and past the form they
draw, and each not decoding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 0a350f6809 M2.6: nothing after a damaged content stream, or in one that doesn't decode, is rewritten
A page's content streams are read as one stream, so what a stream
damaged partway hides past the damage is unknown to the streams after
it too. A string or comment the damage leaves open runs on into the
next stream, and operators the damage hides can change the graphics
state the later streams draw in. Marking a tag's content as an artifact
took what read as a BDC in a later stream for the tag's own, and could
rewrite bytes inside a string. Moving an image after the damage worked
the move out without a cm the damage may hide.

Now an edit refuses a change to any stream after one damaged partway
(rewritten_contents), and an image isn't arranged into a new stream
placed after one. Marking an artifact or a decorative figure refuses
content that reads as the tag's in a stream after the damage. Edits and
marking before the damage go on as before.

A content stream that doesn't decode at all was read as empty and
whole. A styled edit whose paragraph marker closes past the page's last
operator appended the close to it, so the edit replaced that stream
with one holding only the close. Edits now refuse a change to such a
stream, and still go on around it. Marking an artifact or a decorative
figure whose content isn't found takes it as possibly in such a stream,
as it does for content past the damage, and refuses rather than take it
for absent.

Tests: an_edit_never_rewrites_a_content_stream_damaged_partway (an edit
after the damage refused, one before it made),
text_a_damaged_stream_runs_on_into_is_not_edited,
images_after_a_content_stream_damaged_partway_are_not_moved,
a_content_stream_that_does_not_decode_is_never_written_over,
a_tags_content_after_a_damaged_stream_is_not_rewritten,
a_tags_content_not_found_beside_a_stream_that_does_not_decode_is_not_taken_for_absent,
and a_tags_content_in_a_stream_damaged_partway_is_not_rewritten now
checks the damaged stream itself and marks content before the damage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 ebf9c7a3ce M2.6: a damaged-stream refusal says the content may be past the damage
Since marking a figure decorative or a tag as an artifact refuses content
not found beside a damaged stream, the refusal also covers content that
only may be past the damage. Its message said the content is in a damaged
stream; it now says it is, or may be.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 2d672d9517 M2.6: redaction keeps an image the page still draws behind parameters by reference
Once a redaction removes an image or form under a mark, it takes the name
out of the page's resources unless the page's content still draws it
elsewhere. It read the page's streams again for that with `decoded`, which
reads `/DecodeParms` only when direct, so a stream behind a predictor whose
parameters are an indirect object seemed not to draw it, and the page lost
the image there.

It now looks in what it read for the redaction, with the parameters by
reference, and decodes again only the streams it has just written, which
have none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 91968610c3 M2.6: redaction reads content with its /DecodeParms given by reference
`DecodeBudget::decode_strict` read a stream's `/DecodeParms` only when
direct, so content behind a predictor whose parameters are an indirect
object decoded with the predictor's tag bytes left in. Redaction then found
no glyph under a mark, left the text in the file and reported the mark
applied. ISO 32000-2 lets those parameters be indirect.

`decode_strict` takes the document and reads through `decoding_view`, as
`decode_with_damage` does. Its other readers read the parameters too:
Remove Hidden Information's hidden text and layers, the audit's covered-text
check, and edit's backdrop scan.

The accessibility_mark_tag_artifact description says that content which is,
or may be, in a stream damaged partway is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 3bf6c6f027 M2.6: marking content as an artifact refuses what may be past the damage
A tag's content not found before the damage in a damaged stream may be
past it. Marking a figure decorative took it for absent: it succeeded,
took the figure out of the structure tree and left its content tagged.
Marking a tag as an artifact said its content wasn't on the page. Both now
refuse with `Damaged`, as when the content is before the damage, and change
nothing.

Clearing the parent tree read the page's `/StructParents` only when direct,
so with it given by reference (ISO 32000-2 lets it be) the tree went on
pointing the content at the retired tag. It is read through the reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 69e26167e4 M2.6: a page's content is read with its /DecodeParms given by reference
`decode_with_damage` and `decode_contents_with_damage` read a stream's
`/DecodeParms` only when direct, as `Stream::decoded` does, so content behind
a predictor whose parameters, or a number in them, are an indirect object
decoded with the predictor's tag bytes left in, and was taken as whole. Edit
then listed and wrote back the wrong bytes, and pruning dropped an image such
a form draws. ISO 32000-2 lets those values be indirect.

- `decode_with_damage` takes the document and reads the parameters through
  it, and so does `decode_contents_with_damage`, and `decode_contents` with
  it: printing, accessibility's reading of a page and a page copied as a form
  read them too. Code that calls `decode` on a stream itself doesn't yet.
- It reads through `Document::decoding_view`: `with_direct_parms`, but a copy
  of the stream holds only its `/Filter` and the parameters, its data shared,
  so a stream a page's `/Contents` lists again and again isn't copied whole
  for each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 3fec35e1ea M2.6: tests pin that content which doesn't decode is kept and edited around
A stream that doesn't decode at all is not damage to write over: edit and
accessibility keep it as stored and go on with the page's other streams,
and only running past the page's decode allowance refuses. Nothing pinned
either, nor that selecting objects reads a stream of a few MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 50d0fcc403 M2.6: pruning keeps what a page or form damaged partway may draw
Extracting, splitting and combining pages drops the `/XObject` entries no
page draws. A page or form damaged partway was read up to the damage as the
whole of it, so an image named only after the damage was dropped, though
the stream still names it and other readers may still read that far.

- `content_bytes` says whether a page's stream is damaged, and pruning keeps
  everything such a page could reach, as it does for content that doesn't
  decode. A form is read the same way.
- A page made a form XObject (stamps, watermarks, backgrounds from a PDF)
  still draws what decoded before the damage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 480fdb114a M2.6: Optimize leaves a page damaged partway as it is stored
Optimize read a page's content up to any damage in its streams as the whole
of it. Merging a page's image strips then wrote the page's content again as
one stream, without what followed the damage, and the image pass measured
each image by its uses before the damage only, so one drawn larger past it
could be resampled below that use.

- `DecodeBudget::decode_contents_with_damage` reads a page's `/Contents` as
  `decode_contents` does, each stream also saying whether it is damaged
  partway; `decode_contents` is it without the flag.
- `content_bytes` gives `None` for a page with a damaged stream, as for one
  that doesn't decode: the content is not all known. Its strips aren't merged
  (`merge_page` already leaves such a page alone), and no image is resampled
  (`effective_resolutions` gives every image `INFINITY`).
- A form damaged partway leaves the measuring incomplete in the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 712345d7a1 M2.6: an edit never rewrites a content stream from the part before its damage
A Flate stream damaged partway decodes to what came before the damage, and
`DecodeBudget::decode` gave that back as the stream's data: the decoder's
partial flag was dropped in `Stream::decoded_within_counted`. Editing a text
line, a block, an image or an added item in such a stream wrote it again from
that part, so everything after the damage was lost on save, though other
readers may still read it.

- `DecodeBudget::decode_with_damage` decodes as `decode_within` does and says
  whether the stream is damaged partway. A missing checksum or end-of-data
  marker is no damage, as before.
- Edit reads the page's streams with it (`ContentStream`), and
  `rewritten_contents` refuses, before adding anything, a change to a damaged
  stream. Changes in the page's other streams go on, and the damaged one stays
  as stored.
- Selecting objects (`editable_objects`, moving added items) refuses a page
  with a damaged stream, as it refuses one that doesn't decode: what follows
  the damage could change every later item's graphics state.
- Marking a tag as an artifact or a figure as decorative refuses when the
  tag's content is in a damaged stream (`ArtifactError::Damaged`,
  `AltError::Damaged`), and changes nothing, the structure tree included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
xullul 4b2a151012 M7: count each grouped placement once across preparation passes
Change:
- Read and count only the placements a preparation pass newly inlines. The
  others were read on an earlier pass in the context they still have, so a
  group refused where it is drawn (translucent, for example) is no longer
  counted again on every pass: 33 such placements beside one text group used
  to reach the 64-placement limit on the second pass and prepare nothing. A
  form refused in one place is still read where an expanded parent draws it
  anew. If the page doesn't parse back to the operators written, every
  placement is read again, as before.
- Read a group refused because its resources needed renaming again, without
  counting it, once names merged in for other groups make the renaming
  unnecessary (checked from its resource dictionaries alone, without decoding
  its content), and drop its earlier refusal when it is then prepared.

Validation: edit 470 and the 2 automation text_prepare tests passed;
formatting, strict Clippy for pdfcraft-edit, layers and assets passed. The
wasm check was not run locally (the wasm32 target is not installed), nor were
the engine suite and the workspace sweep. Each new regression failed with its
fix disabled.
2026-11-07 18:34:05 +07:00
vangel76andClaude Sonnet 5.5 81af7f1cd9 Merge upstream/main into feat/redact-mark-regex
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 11:54:30 +01:00
vangel76andClaude Sonnet 5.5 8fabc86f3a Merge upstream/main into perf/write-full-retain-parsed-objects
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 11:52:43 +01:00
mtlcl410andClaude Opus 5.5 3a3fe00f69 M2: without codespace ranges, a bfchar CID mapping matches only codes of its own source length (view.render-cid-fonts)
hayro-cmap patch 4: text CMaps record each bf entry's source length
(BfRange::code_len). Where no codespace range gives a code its length,
the bfchar fallback of lookup_cid_code takes the entry's own length, so
the first byte of <fe96> is no longer taken for <00fe>. CMaps with
codespace ranges are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-07 18:50:32 +08:00
mtlcl410andClaude Opus 5.5 2c1b903075 M2: a page whose /MediaBox has no area is US Letter and shows its content (view.render-pages)
- hayro-syntax patch 44: a /MediaBox without area counts as missing, so
  the page is sized, placed and clipped by the same Letter region and its
  content is drawn (MuPDF and PDFium both draw it on 612 x 792).
- pdfcraft-model: Page::crop falls back to Letter for such a box too.
- vendor/README.md: the raw-crop summary names the Letter fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-07 18:44:18 +08:00
vangel76andClaude Sonnet 5.5 e09f87ad55 Merge upstream/main into contributors-vangel76
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 11:36:39 +01:00
xullul a453112151 M7: read each grouped form once and check PUA alternate meanings
Change:
- Leave a form found to be artwork, or refused for a reason in the form
  itself, out of later preparation passes, so a page with many logos or
  unsupported groups no longer reaches the 64-placement limit and prepares
  nothing. ungroup_ops now marks each refusal as intrinsic or contextual;
  contextual ones (translucency, the resources it would merge into, pattern
  placement, the decode budget) are re-evaluated where the form appears, so
  a form refused in one place is still prepared where a parent draws it.
- Refuse a page-level Do with an operand too many for that placement only:
  the form found by its first operand may not be the one drawn. Check a
  form's own operand counts before the nested walk, as an intrinsic refusal.
- Use the Windows PUA alternates only when every kept alternate whose code
  the PDF's map names a Thai character names that alternate's base
  character, so alternates in another order that the map reveals are refused.

Chores:
- Say that the font program confirms each recovered glyph's advance class,
  not its identity, and document the alternate check.

Validation: with and without craft-fonts, fonts 105 (+3 build-input), edit
468, engine 482 (+28 integration) and automation 50 + 352 passed; editor flows
248 passed. Formatting, strict Clippy, layers, assets and parity passed; the
wasm check was not run locally. Each new regression failed with its fix
reverted. Real-PDF edits on Royal Gazette 26288 kept their source fonts.
2026-11-07 17:00:48 +07:00
mtlcl410andClaude Opus 5.5 cdb94e6611 M2: a page without a /MediaBox is US Letter (view.render-pages)
A page with no /MediaBox, its own or inherited, rendered as A4, while
PdfCraft's page model and inspector already sized it as US Letter and
other readers draw it at 612 x 792 pt (PDFBOX-4001-reduced.pdf).
ISO 32000-2 §7.7.3.3 Table 31 requires the box and gives no default;
follow the readers. `base_dimensions`' fallback for a visible region
without area uses the same Letter size, so a /MediaBox [0 0 0 0] page
is Letter too. Vendored hayro-syntax patch 44. The model test pins the
size it already gave.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-07 17:17:26 +08:00
Josias Eli Rodriguez 6e0d0fde3f redact: treat unusable /QuadPoints as absent in every reader
A quad counts only when all 8 entries are present, numeric and finite.
Anything else contributes nothing — never folded, shifted or zero-filled
into a box the document does not describe. With no usable quad the
existing /Rect fallback runs, and with neither the mark stays unreadable.

The rule lives in one helper, geom::quad_points::usable_quads, and redact,
render-inspect, annot summaries and annot appearance drawing all read
through it, so the outline shown is the area apply removes. Redact also
follows indirect numbers in /QuadPoints now, which it used to drop.
2026-11-07 03:04:51 -06:00
mtlcl410andClaude Opus 5.5 37e5708ed2 M2: a CMap's bfchar mappings read codes at their codespace length (view.render-cid-fonts)
A CMap that writes its CID mappings as `bfchar` was looked up without
the code's byte length, so the first byte of a two-byte code matched a
two-byte entry of the same value and drew as its own glyph: `<fe96>`
drew as "þ–" (PDFBOX-1282, Characters_Arial.pdf page 3). The bfchar
fallback of `lookup_cid_code` now applies only when the codespace
ranges give the code the length being tried (ISO 32000-2 §9.7.6.2),
byte by byte; a CMap without codespace ranges keeps the old fallback.
An unmapped code still advances one byte. Vendored hayro-cmap patch 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-07 17:03:56 +08:00
xullul 7634ac8064 M7: address review of grouped Thai text preparation
Add:
- Report text grouped more than eight levels deep instead of leaving it
  silently grouped, and refuse forms whose resource-naming operators carry
  surplus operands, which would otherwise lose nested artwork when inlined.
- Return the applied preparation's own counts from Session::prepare_text, so
  text_prepare no longer reports a separate preview's.
- Shape a THSarabunPSK program that has its own Thai mark positioning by that
  layout, in its own codes. A span the subsets can't show in place is shaped
  whole, Latin rows included, by a complete program of the same face and style,
  such as a CFF face from Load font.

Change:
- Expand only forms that show text, directly or through nested forms; image-
  and vector-only forms stay grouped and uncounted. Match a Do's raw resource
  name bytes, and refuse RGB-blended groups on pages naming no blending space.
- Check the recovered THSarabunPSK map and the Windows PUA alternates glyph by
  glyph against the subset's advances, leave dropped glyphs unmapped, and
  insert the repair after the last endcmap.
- Shape a Thai run split across same-face subsets cluster by cluster and skip
  subset glyphs that are empty. Take the legacy path only for programs in the
  legacy glyph order, needing only the glyphs the new text uses.
- Require GPOS mark/mkmk in the script and language system HarfRust selects.
- Match same-face candidates by weight and slant from the name, descriptor
  weight or a non-zero ItalicAngle; the Italic flag alone is ignored, as
  exporters set it on upright faces. Never complete bold or italic text with
  the Regular bundled Thai face.
- Name the missing character, or the unavailable builtin:thai choice, in errors.

Chores:
- Document text_prepare's fields, builtin:thai, the app's refusal reporting
  limits and the new routing; extend the text-edit parity evidence. Tests that
  need craft-fonts now say they skipped instead of returning silently.

Validation: with and without craft-fonts, fonts 105 (+3 build-input), edit
465, engine 482 (+28 integration) and automation 50 + 352 passed; editor flows
248 passed. Formatting, strict Clippy, layers, assets and parity passed; the
wasm check was not run locally. Each new regression failed with its fix
reverted. Real-PDF edits on Royal Gazette 26288 kept their source fonts.
2026-11-07 15:59:38 +07:00
Helen Kwok a9ccca5b71 pdfcraft-web: say ui.open answers with an error on the web 2026-11-07 15:30:00 +08:00
Zhihui Song 014b67915b M10: adopt file-backed loading and record review fixes in the progress log 2026-11-07 15:07:16 +08:00
Zhihui Song 596f8169e3 M10: use approved printing dependencies and retain the Direct2D crash fix 2026-11-07 14:49:08 +08:00
Zhihui Song d20f563ad7 M10: preserve existing LF endings in dependency update 2026-11-07 14:17:01 +08:00
Zhihui Song b56cb0ca9e M10: avoid Windows StorageFile access denial when printing 2026-11-07 14:15:26 +08:00
xullul c7e8d127af M7: edit grouped Thai text while preserving source formatting
Add:
- Prepare supported Form placements as original page operators through the
  existing-text UI and text_prepare, with shared-form isolation and undo/redo.
- Repair narrowly identified Thai exporter maps on copied page fonts, recording
  repairs without changing the embedded source glyph programs.

Change:
- Reuse legacy Thai Regular/Bold glyph codes and same-face punctuation subsets;
  retain point size, spacing, horizontal scale, rise, colour and paint settings.
- Keep cross-family Thai replacement explicit and write matching bundled Thai
  subsets through the existing bounded outline path rather than static embedding.
- Keep replacement-only menu choices out of the source-font preview atlas so
  opening an ordinary paragraph does not wait an extra frame for unused fonts.
- Refuse unsafe compositing, tagged or malformed groups, and missing contextual
  glyphs without losing the user's document or changing other placements.

Chores:
- Document the editing limits and headless workflow; keep text-edit parity partial.

Builds on dekkeng's Thai fallback work in #2811 (fba0a29a) and mtlcl410's Form
Ungroup implementation in #3067 (6eafca9c), adapted to the current module layout.
The Ungroup menu and older UI font-priority changes are not included.

Refs #3185, #2964 and #2142. Synthetic regressions cover source-font preservation,
shared forms, unsafe refusals, layout, undo/redo, rendering and save/reopen.

Validation: library suites 1,675 passed; automation integration 331 passed;
document-font integration 2 passed; editor flows 248 passed. Existing ignored
tests remain. Formatting, scoped strict Clippy, layers, assets, parity and wasm
checks passed. The unchanged opacity regression failed before the choice-only
atlas correction and passed afterwards.
2026-11-07 12:27:29 +07:00
xullul 77cebccda2 M7: adapt embedded Thai source fonts without replacing their programs
Add:
- Recover editor-only Unicode cmaps for Identity-H TrueType subsets and shape
  verified THSarabunPSK positional glyphs into their original PDF character codes.
- Recognize narrowly fingerprinted damaged Thai exporter maps without guessing
  mappings for unrelated fonts; callers must record and isolate any repairs.

Change:
- Preserve the original PDF font programs and refuse missing contextual glyphs.
- Allow modern Thai GPOS positioning without requiring optional GSUB, adapting
  that correction from dekkeng's PR #2811 at fba0a29a.

Refs #3185 and #2142. Font/edit tests: 535 passed. Formatting, scoped strict
Clippy, layers, assets, parity and the fonts/edit wasm check passed.
2026-11-07 12:26:35 +07:00
mtlcl410andClaude Opus 5.5 5c2f4b8383 M2: the oversized-/Rows fax test also checks white rows after the first (hayro-syntax patch 41)
Through /Indexed, samples past the decoded data read as black, so an
all-black image could not tell a decoder that stopped after one row
from one that decoded all four. A second Group 4 image striped black,
white, black, white (horizontal-mode rows, ITU-T T.6 §2.2.3.3) is now
rendered through both colour spaces with every /Rows value, and each
row is checked for its own colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
2026-11-07 12:54:37 +08:00
mtlcl410andClaude Opus 5.5 44428b36d3 M9: a signed number saved back as an integer is no change after signing (sign.changes-after-signing)
ISO 32000-2 §7.3.3: wherever a real number is expected, an integer may
be used instead. PdfCraft's own writer writes a whole real as an
integer, so a form fill that rewrites a signed widget turned
/Rect [20.0 20.0 120.0 40.0] into [20 20 120 40] and was refused as a
page content change. same_object now compares an integer and a real by
value (exactly, in i128), for the widget /Rect and every other signed
comparison, such as a page /MediaBox rewritten when a comment is added.

Also states the reordered page's verdict as None explicitly, in a test
of its own next to the one from #5962.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
2026-11-07 12:46:10 +08:00
mtlcl410andClaude Opus 5.5 ae79de8deb test(render): check a page whose box has no area on the render path too, and the inherited-box fallbacks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
2026-11-07 12:45:56 +08:00