158 Commits
Author SHA1 Message Date
Dylan M. Taylor d46d9daea5 fix(edit): editor font enumeration reads a face name without building font metrics (#6086) 2026-11-11 14:46:47 -05:00
Grzegorz MartensandClaude Opus 5.5 5de168955b M7: the docs say which failed reads the allowance doesn't count
`Metrics::past_allowance`, `Reads::decode` and the Type 3 glyph-box read
said a stream that fails on its own data isn't counted as refused even when
it spends the last of the allowance. A stream past its own limit fails with
any allowance, yet with no more left than its stages' total it is counted,
as `Reads::decode` itself says just above. They now name the case that isn't:
a read that fails without the allowance refusing it, such as an empty stream
behind a filter nothing decodes. The program-list test's doc no longer says
more than it checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-10 06:59:28 +01:00
Grzegorz MartensandClaude Opus 5.5 973ddc17e9 M7: a test pins the editor's program list against a stream unread on its own
The editor's program list (`embedded_sfnt_within`) reads a legacy Thai
font's metrics after its program, and passes over the program when they are
past the allowance. With the program spending the allowance to the byte, an
empty ToUnicode behind a filter nothing decodes no longer passes it over;
a ToUnicode with data is still refused, and the program isn't listed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-10 06:46:04 +01:00
Grzegorz MartensandClaude Opus 5.5 7cea6dfead M7: a font's metrics are past their allowance only when it refused a read
`Reads::decode` and the Type 3 glyph-box read took any failed read, with
nothing left of the allowance after it, for one the allowance refused. The
Thai edit refuses on what the allowance itself counts
(`DecodeBudget::refused`), so the two could disagree: once the edit's
allowance was spent to the byte, an empty stream behind a filter nothing
decodes, which is unread with any allowance, made a font's metrics read
past it with no refusal counted. The edit then skipped that font of the
face and went through without it, which its refusal is there to prevent.

Both now take a read as refused only when `DecodeBudget::refused` grew
during it, so `Metrics::past_allowance` is set exactly when the allowance
counted a refusal, as its doc now says.

Tests:
- `an_empty_stream_reads_with_nothing_left` adds such a stream: it isn't
  past the allowance, and the flag agrees with the count in each case.
- `a_type3_description_unread_on_its_own_is_not_past_the_allowance`: the
  same for a glyph description after one that spends the allowance to the
  byte; a description with data after it is still refused.
- `a_font_of_the_face_unread_on_its_own_once_the_allowance_is_spent_still_refuses_the_edit`:
  the edit above is refused, where it went through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-10 06:19:08 +01:00
Grzegorz MartensandClaude Opus 5.5 4fca5783a7 M7: a Thai edit refuses only a read past its allowance, and reads its subsets' metrics within it
CodeRabbit's review of 674a20899:

- The Thai edit refused when nothing was left of its allowance, so an
  edit whose reads spent it to the byte was refused. It now refuses when
  a read was refused for want of it (`DecodeBudget::refused`).
- `LegacyThaiFont::from_pdf` read its metrics with `from_dict`, outside
  the edit's allowance, and so did the edit for each subset it measures.
  Both now read them within it (`from_dict_within`), and metrics read
  past it adapt nothing: `from_pdf` refuses the font before reading its
  program, and the edit leaves the subset unread and is refused. The
  edit's scan of every font on the page for its face name stays outside
  the allowance, as on `main`: charged, each alias of a font would pay
  again, and other faces would spend what the face's subsets need.
  `MAX_THAI_PROGRAMS` says what it pays for.
- When the `/CIDToGIDMap` a THSarabunPSK subset's Thai mappings are
  rebuilt from was left unread for want of the editor's allowance,
  `composite_glyph_maps` gave nothing, and a subset with a Latin-only
  cmap of its own was listed as it was, without its Thai mappings. The
  editor now lists no copy of it, as for metrics read past the
  allowance.

Tests: `a_thai_edit_that_spends_its_allowance_to_the_byte_goes_through`
(`pdfcraft-edit`: a subset costing exactly the 64 MiB is edited, and
one three bytes past it is refused),
`a_legacy_fonts_metrics_are_paid_for_from_the_edits_allowance` and
`a_map_left_unread_for_the_allowance_lists_no_copy` (`pdfcraft-fonts`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-09 21:45:29 +01:00
Grzegorz MartensandClaude Opus 5.5 8c5333bfe7 M7: font reads decode through a view of the stream, and refuse before resolving its parameters
CodeRabbit's review of 674a20899:

- Each budgeted read of a font's stream went through
  `Document::with_direct_parms`, which copies the whole stream when its
  /DecodeParms is given by reference: an unrelated value in the
  dictionary, however large, was copied again for every read.
  `DecodeBudget::decode_stages_within` now takes the document and reads
  through `Document::decoding_view`, as `decode_strict` and
  `decode_with_damage` do: the data is shared, and only /Filter and the
  parameters made direct are copied. `Reads::decode` and the editor's
  program read use it, a Type 3 font's glyph descriptions read through
  `decode_with_damage`, and so does Repair Thai maps' program read.
- A Type 3 font's glyph descriptions resolved one description's
  parameters a font before the spent allowance refused it. They now ask
  `DecodeBudget::refuses` first, as `decode_stages_within` does for the
  others: once the allowance is spent a stream with data is refused, and
  noted, before its parameters are resolved.
- `DecodeBudget::refused` counts the decodes refused for want of the
  allowance, where `remaining() == 0` can't tell a refusal from an
  allowance spent to the byte.

Test: `reading_a_fonts_streams_copies_no_dictionary` (`pdfcraft-fonts`,
a test binary of its own): a font whose program and ToUnicode hold an
unrelated array of a million numbers and /DecodeParms by reference is
read eight times by the editor's program read and the metrics read,
and the process's peak grows by less than half of what one array
holds. That a Type 3 font refuses before resolving parameters changes
no result, only the time it takes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-09 21:45:10 +01:00
Grzegorz MartensandClaude Opus 5.5 674a208990 M7: a test pins that Repair Thai maps refuses a map that isn't Identity
The Legal forms review of b7224cbc9..4d6ff291d:

- Nothing pinned that `recovered_thai_map`'s rewritten guard still
  refuses a /CIDToGIDMap that isn't Identity: deleting it left every
  Thai test passing. `recovered_thai_map_checks_every_kept_glyph_against_
  the_font_program` now asserts no map for a map stream that moves glyphs
  and for another name.
- The guard's comment and the test's cited Identity as the default of
  ISO 32000-2 Table 115. It is the default of PDF 1.x (ISO 32000-1);
  PDF 2.0 gives it none and requires the map with a /FontFile2. Both now
  say so, and that the renderer takes a missing map as Identity too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-09 17:18:51 +01:00
Grzegorz MartensandClaude Opus 5.5 4d6ff291d7 M7: Reads::decode says what a stage past its limit is charged
The Claims review of the description at b7224cbc9. `Reads::decode` said a
stream past its limit is unread, not refused, "when what is left can pay
for its stages". What `DecodeBudget::decode_stages_within` does, and
`a_stage_past_its_limit_charges_what_came_before_and_the_limit` pins:
with more left than what the stages before the stopped one made and the
limit, it is charged those two; otherwise, with more left than the
stages' total (behind one filter, the limit), it is charged that total;
with no more left than that total, it spends all of it and counts as
refused.

`MAX_THAI_PROGRAMS` also names the second read of a subset's program,
for which of its glyphs have outlines, among what it pays for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-09 16:52:47 +01:00
Grzegorz MartensandClaude Opus 5.5 92e8e6eff0 M7: Repair Thai maps reads a /CIDToGIDMap given by reference, and takes a missing one as Identity
The Consumers review of ba28458ad..b7224cbc9:

- `recovered_thai_map` read the descendant's /CIDToGIDMap with
  `Dict::name`, on the line after e9142b03b's /Encoding check, so a map
  given by reference made Repair Thai maps pass the font over, and so
  did a missing one, which is Identity (ISO 32000-2 Table 115), as
  `composite_glyph_maps`, `LegacyThaiFont` and the renderer read it. It
  now resolves the map and takes a missing one as Identity.
- `MAX_THAI_PROGRAMS` says what it pays for: the programs, and the CMaps
  and glyph maps read to adapt their cmaps. The metrics read to find and
  measure the subsets aren't charged to it. Its refusal ("MiB of font
  data") still says what it counts.

Test: `recovered_thai_map_checks_every_kept_glyph_against_the_font_program`
recovers the same map with the font's /CIDToGIDMap given by reference and
with none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-09 16:48:01 +01:00
Grzegorz MartensandClaude Opus 5.5 b7224cbc9a Merge main again into fix/fonts-decode-budget
A third merge, of ba28458ad, with nothing to resolve. main's commits since
b6cbf9bb3 add no font program, metrics or CMap read in pdfcraft-fonts or
the editor's font code for this PR's allowance to cover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-09 16:31:54 +01:00
Grzegorz MartensandClaude Opus 5.5 e9142b03b9 M7: a composite font's /Encoding is read through a reference
The Legal forms review of 8fb534ad4. A Type 0 font's /Encoding may be
given by reference (a name or a CMap stream, either indirect), and the
metrics read resolve it: an indirect /Identity-H reads two-byte codes.
The editor's check before reading a composite font's metrics, and
`main`'s `composite_glyph_maps` and `recovered_thai_map`, read it with
`Dict::name`, which doesn't resolve: such a font counted as another
encoding, so the editor adapted no copy of it and Repair Thai maps
passed it over. All three now read it through `identity_h`, which
resolves it as the metrics do.

Tests: `a_composite_fonts_encoding_given_by_reference_is_read` (the
editor adapts a copy of a composite subset with `/Encoding 5 0 R`, object
5 `/Identity-H`, that maps 'Ω', and its metrics read two-byte codes), and
`recovered_thai_map_checks_every_kept_glyph_against_the_font_program`
recovers the same map with the font's /Encoding by reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-09 16:27:49 +01:00
bflatastic 856d3fb0ac Merge main into merge/comments-annot2
# Conflicts:
#	crates/render/src/inspect.rs
#	crates/render/src/lib.rs
#	crates/ui-egui/src/canvas/page_pointer/tests.rs
#	crates/ui-egui/tests/it/comments.rs
2026-11-09 20:56:09 +07:00
Grzegorz MartensandClaude Opus 5.5 509cf9f9fd M7: the editor reads no metrics for a composite font it can't adapt
The Resources review of 8eead8ecd:

- `embedded_sfnt_within` read a composite font's metrics within the
  editor's allowance before `composite_glyph_maps` refused any `/Encoding`
  but Identity-H, so an Identity-V or UniJIS font spent its ToUnicode and
  `/CIDToGIDMap` for nothing. Its `/Encoding` is now checked first; a
  program with a cmap of its own is kept as it is, as when the rebuild
  finds nothing.
- The Thai edit's allowance, `MAX_THAI_PROGRAMS`, now also pays for the
  CMaps and glyph maps its subsets' metrics and codes are read from: its
  doc and its refusal say "font data", not "font programs".
- `a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names`
  also reads its font, whose `post` table names its glyphs, within an
  allowance: the program is paid for by the glyph-name reading, by the
  Mac OS Roman one and for the widths, and a byte short of the first the
  names go unread.

Tests: `a_composite_font_that_cant_be_adapted_spends_only_its_program`,
and `adapting_a_programs_cmap_is_paid_for` again expects a composite font
whose codes aren't Identity-H to pay for its program only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-09 13:36:44 +01:00
Grzegorz MartensandClaude Opus 5.5 063f8629e4 M7: each filter stage of a font's stream is held to its limit, as without an allowance
The Resources review of 8eead8ecd. The program read and the metrics reads
passed their limit times the number of filter stages as one total, which
any one stage could use: a FontFile2 of Flate making 128 MiB of
whitespace, under seven ASCIIHex stages (a 130 KB file), made all of it
before being refused, about 0.1 s a font where `main`'s bound stopped it
at 16 MiB. `edit`'s `matching_thai_outline` reads such a program for each
of a page's fonts with no total, and every `Metrics::from_dict` up to
three times.

Now each stage is held to the limit, as a decode without an allowance
holds it, and the stages together to the limit for each of them, so every
stream the stage scaling was for still reads: a full `/CIDToGIDMap` or a
12 MiB program behind ASCIIHex and Flate. The two bounds take an entry
point of their own: `pdfcraft_filters::decode_tolerant_counted_each`,
which `run` already supports, and `DecodeBudget::decode_stages_within`,
which `Reads::decode` and the editor's program read use. A stage past the
limit is charged what the stages before it decoded and the limit; past
the stages' total, or past what is left, it is charged as
`decode_within` charges. The program, the last stage, is so within the
limit without a check of its own.

Tests: `each_stage_is_held_to_its_own_bound_within_the_total` in
`pdfcraft-filters` (a Flate stage stopped at its bound hands the next
stage nothing), `each_stage_is_held_to_the_limit` in `pdfcraft-cos`, and
in `pdfcraft-fonts` `an_editors_program_read_holds_each_stage_to_its_limit`
and `each_stage_of_a_metrics_stream_is_held_to_its_limit`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-09 13:36:29 +01:00
Grzegorz MartensandClaude Opus 5.5 d4abe80671 Merge main again into fix/fonts-decode-budget
main's #3962 (symbolic TrueType glyph names, 63a6f8ace and nearby) reads a
symbolic TrueType font's codes by the `post` names of the glyphs its cmap
selects, before the Mac OS Roman reading:

- crates/fonts/src/pdf.rs, `unicode_map`: both readings stay, in main's
  order, each given the metrics read's allowance. The program read main
  added (`symbolic_glyph_texts`) goes through `Reads::decode`, as
  `symbolic_mac_roman_texts` does since the last merge: paid for from the
  allowance, `past_allowance` set when it can't be, behind several
  filters within the stream's own limit.
- crates/fonts/src/pdf/tests.rs: both sides added tests at the same place;
  all are kept.

Tests: `adapting_a_programs_cmap_is_paid_for` now expects the program to
be paid for a fourth time, and `the_symbolic_mac_roman_reading_is_paid_for`
becomes `the_symbolic_readings_of_a_program_are_paid_for`, paying for both
readings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-09 13:06:32 +01:00
Grzegorz MartensandClaude Opus 5.5 8eead8ecdd Merge main: the font reads main added are paid for from the allowance too
main's Thai work (#6249, #3492) and the Type 3 and symbolic TrueType
metrics changed the code this PR bounds. Each conflict, and each read main
added that this PR's allowance must cover:

- crates/fonts/src/pdf.rs, the editor's program read: this PR's read is
  kept, its stage-scaled limit and its check for an allowance already
  spent; main's read had /DecodeParms by reference too, which it has.
- The editor's loop: main rebuilds a cmap for a composite subset without
  one from its ToUnicode and /CIDToGIDMap, where this PR skipped Type 0.
  main's composite support stays, with the metrics read within the
  editor's allowance (`from_dict_within`) and no copy adapted from metrics
  read past it, as for a simple font. The /CIDToGIDMap the cmap is rebuilt
  from (`composite_glyph_maps`) reads as the font's other streams do
  (`Reads::decode`): within the allowance, behind several filters too.
- Reads main added to the metrics read: a Type 3 font's glyph
  descriptions (`type3_glyph_box`), and the program read that tells a
  symbolic TrueType font's codes as Mac OS Roman
  (`symbolic_mac_roman_texts`). Both are now paid for from the allowance
  the metrics are read within, and a refusal sets `past_allowance`. The
  glyph descriptions keep their own limits (64 KiB each, 4 MiB a font).
- Constants and helpers both sides added beside one another, and the
  tests both appended, are all kept. Callers main added of
  `unicode_map` pass a reading without a total, as `from_dict` does.
- crates/edit/src/rules.rs: the refusal of a page whose content didn't
  all decode stays, and the operators come from main's `text::joined`.

Tests: `adapting_a_programs_cmap_is_paid_for` now expects a composite
font's ToUnicode, and a simple font's program read again for the Mac OS
Roman reading, to be paid for. Four are new:
`adapting_a_composite_subsets_cmap_is_paid_for`,
`a_full_cid_map_rebuilds_a_composite_subsets_cmap_behind_two_filters`,
`type3_glyph_boxes_are_paid_for_from_the_allowance` and
`the_symbolic_mac_roman_reading_is_paid_for`. The fixture of main's test
of a composite subset without a cmap moves into `cmapless_composite`,
which both use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-09 12:00:17 +01:00
mtlcl410 c4db177d94 Merge upstream/main into fix/freetext-redact-appearance
props: a text box reports its drawn text style, other comments main's drawn_color
(grey, CMYK). Parity: main's evidence and notes with this branch's text-box and
redaction-outline sentences and tests.
2026-11-09 12:03:59 +08:00
mtlcl410andClaude Opus 5.5 d130c4f4de Merge upstream/main into fix/symbolic-truetype-glyph-names-3962
Main's #6569 (hayro-interpret patch 101) reads a symbolic TrueType font that
names no glyphs and keeps the Macintosh order as Mac OS Roman; this branch's
patch (79) reads one that names its glyphs by those names. Both are kept: the
renderer struct holds both fields, and the editor's unicode_map applies the
glyph-name reading only to a font whose post table names glyphs, then the Mac
OS Roman one, so each font takes the reading the renderer gives it. The ledger
keeps main's rows with (79) added; the editor reuses descriptor_flags and
MAX_SFNT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-09 11:15:40 +08:00
mtlcl410andClaude Opus 5.5 45c7222704 Merge upstream/main into fix/symbolic-truetype-glyph-names-3962
Keeps main's vendor ledger and adds hayro-interpret patch (79) to it; the
symbolic TrueType program is read through doc.with_direct_parms, as main now
reads every font stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-09 10:34:14 +08:00
bflatastic 8eb14f3892 Merge #6569: fix(render): a symbolic TrueType font in the Macintosh glyph order reads its codes as Mac Roman (hayro-interpret patch 101)
# Conflicts:
#	crates/render/src/raster/tests.rs
#	vendor/README.md
2026-11-09 08:13:36 +07:00
bflatastic e4cb041bdc Merge #6521: fix(render): glyph names read as text only in the glyph list's uni and u forms (hayro-interpret patch 92)
# Conflicts:
#	vendor/README.md
2026-11-09 08:10:37 +07:00
mtlcl410 83a6a8394e M7: Edit text lines set in a Type 3 font are as high as their glyphs (edit.text-edit) (#6571) 2026-11-09 08:57:56 +08:00
Fatih Kunak 2ef2e2b822 fix(fonts): a subset that gives the space no width can't encode one (#6131) (#6158) 2026-11-09 08:44:48 +08:00
mtlcl410andClaude Opus 5.5 0f5de52041 Merge upstream/main into fix/freetext-redact-appearance
view.render-annotation-appearances: main now draws Thai in text boxes from the Thai face
(Type 3 glyphs beside Helvetica). A text box keeps the standard face its /DA or /DS names for
the rest of its text: lines wrap and are measured in that face with Thai shaped
(appearance::text_width_in), its font resource sits beside the Thai fonts, and after a Thai run
the text goes back to that face rather than /Helv. set_contents keeps both main's Thai refusal
and the drawn style, and fitted_box sizes the box to the Thai ink in the drawn face.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-09 03:09:34 +08:00
BFlat 43f24aa169 Draw Thai in Fill & Sign, text boxes, form fields, headers, footers and watermarks (#6632)
Refs #2354. Thai typed into these tools is shaped and drawn from the Thai face through Add text's writer (now pdfcraft_fonts::shaped_text), searchable and copyable, instead of being written as ?????; a build without a Thai face refuses it with a message.
2026-11-09 02:56:34 +08:00
mtlcl410 b19be10a90 Merge remote-tracking branch 'upstream/main' into float-4365
# Conflicts:
#	crates/annot/src/appearance.rs
#	crates/annot/src/draw.rs
2026-11-09 02:50:44 +08:00
mtlcl410 2303f9e085 Merge remote-tracking branch 'upstream/main' into lane6-6569
# Conflicts:
#	crates/fonts/src/pdf/tests.rs
2026-11-09 02:26:52 +08:00
bflatastic 378ef84ea1 Keep a legacy Thai alternate out of the nominal mark's place
A legacy THSarabunPSK subset whose map gives a shifted alternate (glyphs
342-368) as the nominal mark, its nominal glyph dropped, shaped that
alternate wherever the nominal was needed, drawing the mark out of place.
The shaping copy now reaches an alternate only through its presentation
form, so text that needs the dropped nominal is refused instead.
2026-11-08 23:51:51 +07:00
bflatastic e8322a8fc2 Shape Thai digits and bare letters without mark positioning
The source shaper refused any Thai text in a program without GPOS mark
positioning, so a row of Thai digits failed although nothing in it is set
on a base. Require the positioning only for text with a vowel or tone
mark, or Sara Am.
2026-11-08 23:51:51 +07:00
bflatastic fbc2dc77a7 Read each THSarabunPSK subset of an edit once, within one budget
Every resource name of a page's Thai subsets decoded and kept its own copy
of the font program, so many names of one large program could hold about
2 GiB for a single edit. Names of one font object now share its program,
the subsets of an edit are decoded within a shared 64 MiB allowance, and
an edit that needs more is refused with the reason, changing nothing.
2026-11-08 23:51:51 +07:00
bflatastic dff6c2cc3e Reach a THSarabunPSK subset's Thai glyphs when its own cmap is Latin-only
A legacy subset can keep a cmap of its own that maps only Latin, so the
editor program returned it unchanged and an edit such as a consonant with a
tone mark failed although the subset keeps both outlines. Add the PDF's
verified Thai and alternate mappings to the editor-only cmap where the
program's own lacks them, keeping its existing mappings.
2026-11-08 23:51:51 +07:00
bflatastic d351aa2760 Read Thai font programs and CIDToGIDMaps with DecodeParms given by reference
A predictor whose parameters are an indirect object left its row tags in
the decoded font program and glyph map, so the Thai map repair, the glyph
codes and the shaper read the wrong bytes. Resolve the parameters as the
other font readers do, keeping the decode budgets.
2026-11-08 23:23:09 +07:00
bflatastic 62b85edef1 Write the Thai glyph check without a negated is_some_and
Clippy's nonminimal_bool asks for is_none_or: a glyph with no outline range, or an empty one, is skipped as before.
2026-11-08 22:31:14 +07:00
bflatastic 4444355446 Merge grouped Thai text editing (#6249)
# Conflicts:
#	parity/acrobat-features.toml
2026-11-08 22:31:13 +07:00
FredApps 6f5f361368 Refuse encoding unverified glyphs after a font decode allowance refusal 2026-11-08 14:39:32 +01:00
FredApps ef7f2f3116 Merge main and retain budgeted font decoding with indirect parameters 2026-11-08 13:56:26 +01:00
FredApps 8287ad0d06 Merge pull request #6289 from Piohrun/fix/partial-content-rewrite
M2.6: never rewrite a content stream from the part before its damage
2026-11-08 13:10:56 +01:00
mtlcl410andClaude Opus 5.5 11c7a02762 fix(render): the Mac OS Roman reading test covers ZapfDingbats and a /Differences naming .notdef (hayro-interpret patch 101)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-08 12:35:53 +08:00
mtlcl410andClaude Opus 5.5 15bc6159d1 fix(render): the Mac OS Roman reading needs no base encoding and no Symbol or ZapfDingbats font, tested (hayro-interpret patch 101)
Tests only: a named /Encoding or /BaseEncoding keeps its own reading, /Differences without a
base encoding still gets the Mac OS Roman reading under its own entries, and a Symbol font keeps
no implicit mapping (ISO 32000-2 §9.6.5.4, §9.6.6.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-08 12:04:29 +08:00
Chiranjeevi U adbf1dc01f M9: signed appearances show their real validation state (#79) (#2808)
* M9: redraw named-layer signature status layers

- /N drawing one Form that holds n1 and n3: n1, n3 and n4 redrawn for the state
- copies made once per original and state; originals never edited
- Lucide icons; captions in Helvetica, embedded Inter, or craft-fonts outlines

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

* M9: draw signature states in the display copy, exports and print

- marks follow the bytes shown; a changed verdict redraws after a metadata edit
- background opens pick up the trust and captions current at commit

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

* M9: status captions in the interface language

- the three captions in all 22 catalogues
- Signatures panel colours from sign::status_rgb

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

* M9: signature badge in the widget's corner

- a quarter of the shorter side, 48pt cap; outline only under 32pt
- dropped for a drawn valid state; bottom-right where the appearance draws one
- sign_list reports verdict_shown; parity and roadmap

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

* M9: keep Arabic signature captions in logical order

Pass raw RTL catalogue text to the signature appearance shaper so UAX #9 is applied exactly once. This keeps the rendered word order and exported /ActualText consistent.

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

* M2: read /ActualText over painted paths and pictures

- A replacement that marks no glyph reads where it paints, cut to what is visible and along the way it is drawn.
- A span the page content leaves open ends before the annotations.
- OCR and scan field detection still count such a page as having no text of its own.

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

* M8: redaction strips /ActualText around paint it clips

- A path or shading written again under a clip counts as changed, so a partly redacted word drawn as outlines leaves no name in the file.

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

* M9: recognise only layer forms that draw n1 and n3

- Settle on the one form holding both first, then read its content once per call.
- Skip an /N that is its own layer form; copies keep the layer's /OC.
- A verdict counts as drawn when any visible widget shows it.

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

* M9: keep the signature badge and redraw wherever a state changes

- The badge stays for every state; its mark sits bottom-right as the widget lies on the page.
- An edit that changes a verdict redraws every page; captions apply before startup files open.
- Print, the CLI's render and the repair log follow the display copy.

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

* M9: restore the attribution usage the rebase dropped

- Usage lines for the three icons, Inter and the craft-fonts input; ATTRIBUTION.md rebuilt from them.
- Roadmap and localization notes for this round.

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

* M9: pin signature field names on a nested form

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

* M9: bound the signature field walk

- kids share their parent's name and type instead of each copying them
- a widget stays listed under every field that claims it, as one shared copy
- a field written in place in a shared /Kids array is walked once
- caps of 20,000 fields, 100,000 entries and 4096-byte names; no states drawn past them

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

* M9: keep a document signed when the field walk stops early

- list_cached adds a signed row of unknown validity for the fields the walk didn't read
- Save As and actions then append, and Optimize, Sanitize and full rewrites are refused, as for any signed file

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

* M8: removing hidden content no longer strips the names of shadings

- With no regions to clip, a shading is left alone instead of wrapped and marked clipped
- clean_replacements skips scopes already marked, so deep nesting stays linear

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

* M2: read a picture's /ActualText over the whole picture

- draw_image's transform maps pixels, not a unit square, so the text sat on one pixel in the corner; the test now draws a 2x2 picture
- Painted grows in place instead of being copied through an Option

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

* M9: cap the bytes of names the signature field walk keeps

- A kid's name repeats its parent's, so one long name over 20,000 kids came to 80 MB; the walk now stops at 4 MiB of names and counts the rest as unread
- Cover the widget written into a shared /Kids array and the field cap among kids, which no test caught
- sig_fields is private again, and the cap docs say what happens past each cap

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

* M9: skip status-layer work that changes nothing; fix tests that could not fail

- show_status_layers and has_status_layers return before reading the form when nothing is signed, so an unsigned form no longer walks every field on each edit
- revalidate and draw_captions leave documents with no drawn state alone, and no longer copy the signature list to update it
- Scan form detection reads the page's text once; comment and badge tips share one hover rule
- Tests: drop a table row that repeated another, stop taking an expectation from the code under test, stop comparing an Arc with itself, give bare asserts messages

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

* M9: test the signature field name cap at the engine boundary

- A 4,096-byte name is read and blocks a full rewrite; at 4,097 bytes the list holds the unread-fields row and a full rewrite is still refused

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

* M9: test signature states beside fields the viewer draws

- One display copy holds both a field's drawn appearance and the signature's state.

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

* M9: draw Thai and Devanagari status captions in their own script

- script_of names Thai and Devanagari, so a craft-fonts face meant for either draws the caption as outlines; without one it stays English.
- Localization, parity and the craft-fonts usage line list the new scripts.

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

* M2: paint left out by an exclusion mixes its /ActualText replacement

- A replacement placed by outlines or pictures, some of them excluded, fails filtered extraction as one with excluded glyphs does.
- Glyphs still place a replacement that has them.

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

* M9: a long-named field that can't hold a signature leaves the walk whole

- A leaf of another type passed over for its name is no signature field and has none below it, so an unsigned form stays unsigned.
- A signature field, or any field with kids, still counts as unread.

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

* M9: status captions use document faces only

- Each script takes the face PdfCraft writes it into documents with, so Chinese stays English although the pinned craft-fonts' interface-only Chinese face is meant for Hani.
- The craft-fonts usage line names Devanagari: the pinned revision has a face for it, and Hindi captions are drawn in it.

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

* M9: refuse a new signature field the field walk can't read back

- Signing adds a field only when the walk reads the whole form with it: past a cap the signature would be listed as unread fields, and its name could repeat an unread field's.
- A field the walk already reads is signed as before.

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

* M9: refuse a new signature field with a blank name

- The walk lists no field without a name, so signing now also requires the walk to list the new field: one named only with spaces or NULs would be a signature nothing shows.

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

* M9: an unnamed signed field or an unreadable form counts as unread

- A signed field with no usable /T, or an /AcroForm or /Fields of the wrong type, cuts the field walk short: the document keeps its unread-fields row and stays signed.
- Signing refuses a blank new field name before it adds the field, so the refusal still says why.

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

* M9: no display copy when no signature state is drawn

- display_bytes_for makes a copy only when it added an appearance or drew a state, as the app does.

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

* M9: the verdict mark stays on the page

- A widget past the crop box takes its mark in the part on the page, or none when too little of it is there.

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

* M9: typed values stay drawn while a signature redraws every page

- A form edit that changes a signature's state puts its values back in unbaked after the pages are invalidated.

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

* M9: the action, Save As and shading tests check their effects

- The signed action and Save As tests reopen the output for the title and the comment; the shading test checks its clip.

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

* M9: values typed earlier stay drawn while a signature redraws every page

- A form edit that changes a signature's state keeps every value still waiting on its page, not only its own.

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

* M9: comment and content edits keep typed values drawn too

- An edit to one page's comments or content that changes a signature's state moves no page, so the values still waiting on their pages stay pending, as after a form edit.
- The pending-value test runs a field deletion and a comment after a fill, under a certification that allows the fill; its messages say what it checks.

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

* M9: the language-change test skips without catalogs

- Like the other language tests, it returns early when the build has no craft-i18n catalogs.

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

* M9: signed fields behind kids without a name count

- A kid with no /T but kids of its own is walked as a field node, so a signed field under it is listed by its full name.
- An unnamed kid claiming a signature its field doesn't have cuts the walk short, so the document stays signed and keeps its unread-fields row.

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-11-07 20:37:17 -06:00
mtlcl410andClaude Opus 5.5 210c2a65a4 M3: a symbolic TrueType font in the Macintosh glyph order reads its codes as Mac OS Roman (hayro-interpret patch 101)
An embedded symbolic TrueType font with no /Encoding, no /ToUnicode and
no glyph names (post 3.0) read its codes as WinAnsi in text extraction.
A code now reads as Mac OS Roman when two things agree: the font's
Macintosh Roman (1, 0) cmap subtable selects the glyph the code draws
(ISO 32000-2 9.6.5.4), and that glyph sits where the standard Macintosh
glyph order puts the code's Mac OS Roman character. pdf.js's
issue2017r.pdf now extracts its accented line as PDFium does.

Windows subsets in the same order whose codes are WinAnsi keep their
reading, as do fonts that name their glyphs, fonts without a (1, 0)
subtable and fonts whose (1, 0) subtable selects other glyphs.
pdfcraft-fonts reads the same codes the same way for the editor.
Glyph selection is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-07 23:40:16 +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
mtlcl410andClaude Opus 5.5 ea2e0a0e10 test(fonts): the u glyph-name form reads with four, five and six digits (hayro-interpret patch 92)
The test of the glyph list's hex forms now has well-formed `u` names too,
so a change that broke that branch of `glyph_text` would fail it. The rule
itself is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-07 19:17:57 +08:00
mtlcl410andClaude Opus 5.5 63a6f8ace6 M10: glyph names in a symbolic TrueType font's post table read as text only in the glyph list's uni and u forms (#3962)
The glyph list specification (ISO 32000-2 §9.10.2) writes the hex digits
of its `uniXXXX` and `uXXXX`-`uXXXXXX` forms in uppercase, `uni` taking
groups of exactly four and `u` four to six, outside the surrogate range.
`glyph_name_text` (hayro-interpret patch 79) took lowercase digits too, so
a glyph named `uni1eae` read as U+1EAE; it now names nothing and the code
reads as WinAnsi, as for any glyph without a name. The editor's reading of
the same `post` names in pdfcraft-fonts applies the same rule, so both
agree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
2026-11-07 18:11:55 +08: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
mtlcl410 9dfd49cc94 Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-07 18:00:33 +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
mtlcl410andClaude Opus 5.5 4a213e06b3 M3: glyph names read as text only in the glyph list specification's uni and u forms
`uni` takes groups of exactly four uppercase hex digits and `u` four to six,
outside the surrogate range, so `uni65`, `u65`, `uni20ac` and `uni+065` read
as nothing instead of `e` and the euro sign. hayro-interpret patch 92 reads a
glyph's text by `agl_unicode_from_name` and keeps upstream's lenient reading
for finding glyphs in a font, so drawing is unchanged; pdfcraft-fonts'
`glyph_text` stops accepting lowercase digits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
2026-11-07 15:39:37 +08: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
Grzegorz MartensandClaude Opus 5.5 fb01bbfe77 M7: the editor's program read takes its limit once for each filter stage
The editor's read of a font program bounded all its filter stages together by the program's
16 MiB limit, as main does: a 12 MiB program behind ASCIIHex and Flate, its Flate data about
8 MiB, was left unread, though behind Flate alone it reads. It now takes the limit once for each
stage, as the metrics reads do, which the stages share and any one of them may use, and then
holds the program itself to 16 MiB. Behind eight filters a read may so decode up to 128 MiB,
where main stopped at 16 MiB.

Once the allowance is spent, the metrics reads refuse a stream with data, and the editor's
program read passes a program over, before resolving its /DecodeParms, as the decode itself
would refuse it, so each font left doesn't pay for that work the allowance doesn't count. What
is read and what is refused are as before, an empty stream still read rather than refused.

Tests: an_editors_program_reads_behind_ascii_hex_and_flate_within_its_limit, and
an_empty_stream_reads_with_nothing_left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 04:37:45 +01:00