Commit Graph
91 Commits
Author SHA1 Message Date
aiongg 79d72038f1 refactor(fonts): move pdf.rs's tests to pdf/tests.rs 2026-10-26 20:51:19 +00:00
skylar taylor-barrick e90f7882f1 Migrate PdfCraft to egui 0.37 (#4591)
* Record successful PdfCraft web application check

* E21: bound startup requests and validate font and license inputs

* E21: separate pending control completion and gesture regression phases

* M-egui037: enforce control admission and per-family font notices

* M-egui037: qualify capture fixes and document migration behavior

* egui-037: identify current and historical qualification results
2026-10-25 14:30:31 -04:00
a047c6ba09 chore: resolve SonarCloud maintainability issues (#4356)
* chore: resolve SonarCloud maintainability issues

* fix(packaging): use verify_sha256 helper function

* style: cargo fmt

* fix: restore wildcard imports and fix method-reference type errors

Revert the glob-import expansions (they missed SomIndices and left unused
imports), revert char::is_ascii_digit where the iterator yields char by
value, and pass the content slice to prune_doomed_xobjects as &[Vec<u8>].

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

* Keep XObject decodability check without binding content in interp

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
2026-10-23 23:28:18 -04:00
aiongg cf0f80604f Allow OFL typefaces, Noto CJK and Source Han included (AGENTS.md §1.1) (#3908)
* Allow OFL typefaces whoever designed them (AGENTS.md §1.1)

Noto CJK and Source Han from their open-source releases are allowed like any
other OFL font. The fonts build no longer skips them, the asset check accepts
an OFL font credited to Adobe, and the docs describe the Chinese faces that
result. The ban on Adobe icons, artwork, UI graphics and the fonts Adobe
products install stays.

* Leave out whole CJK faces other than the Japanese ones for size

A Chinese or Korean face is embedded whole only under 4 MiB, so the 16.4 MB
Noto Sans CJK SC in craft-fonts stays out of the binary and the embedded
total against the pinned craft-fonts is unchanged (25,204,828 bytes).

* docs: replacement boxes for Chinese without craft-fonts only on the web or without an installed Chinese face

* xtask assets: the OFL font exemption needs at least one licence ID

* fonts build: count a CJK script tag with a region (Hant-HK) as CJK, shared with the embedding test

* AGENTS.md: the asset gate exempts OFL fonts from the Adobe-author check

* fonts build: count a Hangul-only face (Hang) as CJK for the size rule

* fonts: the CJK embedding test checks the size rule, sharing its constants with build.rs

* docs: the missing Hans face is a property of the pinned craft-fonts, not of every build
2026-10-23 13:02:56 -04:00
Dylan M. Taylor 8ee1ac0c58 Merge pull request #4117 from aiongg/fix/coderabbit-b-caps
fix(limits): bound the decoding and listing work that repeated references and small inputs multiply
2026-10-22 21:38:59 -04:00
aiongg d818e4a20a Merge upstream/main into fix/coderabbit-b-caps 2026-10-22 20:03:27 +00:00
aiongg a72845ad80 Merge upstream/main into fix/coderabbit-b-shared
Redaction reads page content and form XObjects strictly (upstream) and
within one page's decode budget (this branch): both now go through
DecodeBudget::decode_strict.
2026-10-22 19:44:33 +00:00
aiongg 3e968c8aa2 chore(a11y, fonts): satisfy clippy in the CID width ranges and a test helper 2026-10-22 14:15:50 -04:00
aiongg 5609bcbe37 fix(fonts): wrap text measuring each word once
Line wrapping built and measured the whole candidate line for every word,
so a long field value or annotation text at a tiny font size, with many
words on one line, took quadratic time and copying. The wrapper keeps the
line's width as it goes and measures each word, space and broken
character once.
2026-10-22 14:15:50 -04:00
aiongg 75c4275810 fix(fonts): look up CID and width ranges by binary search
Code-to-CID and /W range lookups scanned every record in declaration
order, once per glyph and once per explicit ToUnicode entry while a font
was read, so a few thousand records multiplied by as many codes took
billions of steps. Both tables are indexed once into stretches whose
first-declared covering record wins, the precedence the scan had, and the
/W ranges stop at the CMap entry limit.
2026-10-22 14:15:50 -04:00
aiongg 1cc392e5b0 fix(fonts): cut ToUnicode destinations to the CMap limit
A sequential bfrange stores one destination and builds every covered
glyph's text from all of it, so one huge destination and many repeats of
its codes made text extraction copy megabytes per glyph. Destinations keep
at most 256 UTF-16 units (the format's 512 bytes), and marked-text
extraction stops asking for a glyph's text once its id has all the text it
keeps.
2026-10-22 14:15:50 -04:00
aiongg 7eaa20799c fix(fonts): let auto-deref pass the candidate objects to the program match
Clippy's explicit_auto_deref rejects the explicit derefs.
2026-10-22 17:12:57 +00:00
aiongg 8da1164f7d fix(fonts): decode Encoding and ToUnicode CMaps within 16 MiB
Both CMap streams decoded under the general 1 GiB stream limit, so a
small Flate stream of whitespace inflated to a gigabyte that was then
lexed on every font load. They now decode within 16 MiB, like the
font programs beside them, and a larger CMap is not read.
2026-10-22 17:08:54 +00:00
aiongg 05df6ee4b7 fix(fonts): decode each candidate font program once, within its size
Looking for an already embedded full program decoded every candidate up
to 17 MiB, once per resource naming it, so a page whose PCFull resources
all alias one compressed stream, or many marked streams, made one text
edit inflate gigabytes. Candidates now decode within the program's own
length, each once per embedding, and at most 16 in all; the resource
check compares its CMap streams within their expected lengths too.
2026-10-22 17:08:54 +00:00
aiongg 7a33a85d8b fix(fonts): embedded sfnt reads can share one decode budget
embedded_sfnt decoded each program with a fresh 16 MiB allowance and
charged nothing when the program turned out unusable, so a page whose
font resources all named one bad program inflated it once per alias.
embedded_sfnt_within charges every decode to a caller's DecodeBudget.
2026-10-22 16:57:55 +00:00
aiongg b6c815672e Integration: cargo fmt 2026-10-22 02:52:29 +00:00
aiongg ad04768906 Merge r2 track t17: choice-pass takes
# Conflicts:
#	ATTRIBUTION.md
#	ATTRIBUTION.toml
#	apps/pdfcraft/src/main.rs
#	crates/engine/src/dates.rs
#	crates/fonts/src/lib.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/i18n/mod.rs
#	crates/ui-egui/src/system_fonts.rs
#	crates/ui-egui/src/theme.rs
#	crates/ui-egui/tests/content.rs
#	crates/ui-egui/tests/fonts.rs
2026-10-22 02:25:34 +00:00
aiongg c00a8b7616 Merge r2 track t14: signatures
# Conflicts:
#	ATTRIBUTION.toml
#	crates/annot/src/tests.rs
2026-10-22 02:22:02 +00:00
aiongg 69e755c439 Merge r2 track t08: fonts
# Conflicts:
#	crates/automation/src/tools.rs
#	vendor/README.md
2026-10-22 02:20:50 +00:00
aiongg 4d5025c6da Merge r2 track t07: text-extract-select
# Conflicts:
#	crates/edit/src/tests.rs
#	crates/render/src/raster.rs
#	vendor/README.md
2026-10-22 02:20:03 +00:00
aiongg ee373b1b42 Merge PR #2859: Treat CRLF as a single line break in fonts::wrap 2026-10-22 01:47:23 +00:00
aiongg 098462ec5d Merge PR #3717: Stop Std14::recognise mapping sans-serif font names to Times 2026-10-22 01:47:23 +00:00
aiongg 0965bc124d Merge PR #2900: Forms: measure a field's own simple font, not Helvetica, when laying out its text 2026-10-22 01:46:58 +00:00
aiongg e42ccbb440 Merge PR #1679: fix(sign): render Unicode characters in visible signatures using bundled Inter font
Conflicts:
- crates/sign/src/pdf.rs: details switch to Inter as the PR does; drawn labels keep main's
  Helvetica check and refusal, so the PR's label-driven switch to Inter is dropped. Doc comment
  merged.
- crates/sign/tests/pdf.rs: both tests kept.
2026-10-22 01:46:01 +00:00
aiongg a0e8c2faa2 Merge PR #2609: fix(edit): draw the Cyrillic the Japanese faces lack with the bundled Inter face (#2593)
parity/acrobat-features.toml: edit.edit-text evidence and notes take both sides' additions
2026-10-22 01:45:35 +00:00
NetterSilverandClaude Haiku 5.5 ab815b50a6 Stop Std14::recognise mapping sans-serif font names to Times
Found while testing crates/fonts/src/std14.rs:84

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 19:36:57 +02:00
sanches 21652973bb Merge remote-tracking branch 'origin/main' into i18n-id-vi 2026-10-20 14:30:10 +03:00
Claude b88c8d79ab M2: the two Identity-H ToUnicode rules point at each other
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
2026-10-19 18:07:05 +00:00
Claude 7f9d86c915 M2: doc comments for the Identity-H ToUnicode paths
Type0Font::new and char_code_to_unicode get doc comments, and
unicode_map's names the identity ToUnicode name. The comments and
vendor/README.md cite Acrobat's observed behaviour only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LymhVaGZCWwB9HNEbLzeMM
2026-10-19 17:07:36 +00:00
Lcoding15andClaude Opus 5.5 978c77c583 M2: the Identity-H ToUnicode map leaves out DEL and C1 controls
crates/fonts mapped codes U+007F and U+0080–U+009F to those controls while
the renderer's text layer (hayro patch 35) gives them no text. The generated
map now covers U+0020–U+007E and U+00A0–U+00FF in the first high byte, as
the text layer does; both tests check DEL and a C1 control.
(CodeRabbit review on #3222.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:20:28 +00:00
Lcoding15andClaude Opus 5.5 96db0e3bb5 M2: Type 0 fonts with /ToUnicode /Identity-H read codes as Unicode
WSCAD (and other CAD exporters) write `/ToUnicode /Identity-H` — a name,
not a CMap stream — on composite fonts whose two-byte codes are the UTF-16
code units. Acrobat, MuPDF and pdf.js read each code as its Unicode value.
hayro resolved the name to the Identity CMap, which maps codes to CIDs only,
so such pages had no text at all: nothing could be selected, searched,
copied or edited (147 of 147 pages of a WSCAD schematic).

- vendored hayro-interpret patch 35: `Type0Font::identity_to_unicode`
- crates/fonts: the same reading for Edit text, find and redaction
  (controls and lone surrogates map to nothing; simple fonts unaffected)

Tests fail without the fix: render `identity_to_unicode_names_give_text`,
fonts `identity_to_unicode_names_read_codes_as_unicode`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:20:28 +00:00
NetterSilverandClaude Haiku 5.5 61632e4899 Treat CRLF as a single line break in fonts::wrap
Found while testing crates/fonts/src/lib.rs:79 (wrap_with: `text.split(['\n', '\r'])`)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 16:01:48 +00:00
sanches 2b445950df Merge remote-tracking branch 'origin/main' into i18n-id-vi 2026-10-18 18:08:27 +03:00
aiongg b6b02a1b27 Merge upstream main 1952a8880 into next
ROADMAP.md, docs/roadmap.md: union of both sides' lines, each once.
parity/acrobat-features.toml: the XFA entry has both sides' evidence and notes (#2604 page areas, #2813 CLI layout).
2026-10-18 14:22:30 +00:00
mtlcl410andClaude Opus 5.5 f0c275669a M7: never encode typed text with CID 0 (#3223) (#3235)
Word's Type 0 subsets map CID 0 (.notdef) as well as the real space glyph
to U+0020 in ToUnicode and list a width for CID 0. encode keeps the lowest
code for each character, so an edited line wrote every space as <0000> and
drew a box where each space was. CID 0 is .notdef whatever ToUnicode says,
so has_glyph now refuses it and the space takes the real CID.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:48:14 -04:00
aiongg 7f718ecc62 Merge upstream main 50f261d70 into next
ROADMAP.md, docs/roadmap.md: union of both sides' log lines and milestone rows, each line once.
parity/acrobat-features.toml, crates/xfdf/README.md: next's text, a superset of main's (#1917, #2686 plus #2688).
crates/edit/src/tests.rs: both sides' new tests (#2558 and #3084).
crates/ui-egui/src/editing.rs: the picked picture is cleared on every edit, and a new drawing commits its raster (#3149).
2026-10-18 13:12:21 +00:00
Marco Arnulfo a61bd92fdd Fonts: measure a remapped standard-14 code by the glyph it draws
A standard-14 font with no /Widths was measured by code in the WinAnsi-indexed
table, so a /Differences remap took the width of the glyph WinAnsi gives that
code: Helvetica with [65 /W 87 /A] laid out "AAAA" four W's wide. The code is
now looked up through the font's encoding first; /Widths still go by code, and
Symbol, ZapfDingbats and glyphs WinAnsi has no code for keep the code itself.
2026-10-18 13:21:34 +02:00
Ridwan 5827afdcde M8: measure fonts without /Widths with their embedded program (#2130) (#2232)
Find and the renderer place glyphs of a simple font that has no /Widths
with the embedded font program's own advances, but redaction guessed the
widths from the font's name (a standard-14 face, Helvetica by default).
When the guess was wider than the real font, the end of a line under a
mark seemed to lie past it and was kept in the content stream, and the
verify pass, using the same guess, agreed.

Metrics now fills the width table from an embedded sfnt program
(FontFile2, or OpenType FontFile3) when /Widths is missing, looking the
glyph up as the renderer does. A subset's unmapped codes get width 0 so
has_glyph stays false for them. Fonts with /Widths, and bare CFF or
Type 1 programs, are unchanged.

Fixes #2130
2026-10-18 03:48:00 -04:00
sanches 6496dc6f47 Merge remote-tracking branch 'origin/main' into i18n-id-vi
# Conflicts:
#	ATTRIBUTION.md
#	ATTRIBUTION.toml
#	ROADMAP.md
#	crates/engine/src/dates.rs
#	crates/oracle/src/pixels.rs
#	crates/ui-egui/src/i18n/mod.rs
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/system_fonts.rs
#	crates/ui-egui/src/theme.rs
#	crates/ui-egui/tests/fonts.rs
#	docs/gaps.md
#	docs/localization-parity.md
#	docs/localization.md
#	docs/roadmap.md
#	docs/target-app-parity.md
2026-10-16 20:50:35 +03:00
aiongg 8898d0c9e5 Merge track r1-edit (fredapps-r1) 2026-10-16 14:11:53 +00:00
aiongg 62f3375297 Merge PR #2594: M7: write a full font program compressed and find it again on any page 2026-10-16 14:02:17 +00:00
NetterSilverandClaude Haiku 5.5 fdc8099ac2 Keep leading spaces and tabs when wrapping plain text into a PDF
Found while testing crates/fonts/src/lib.rs:79 (wrap_with), called from crates/create/src/lib.rs:set_text

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 04:08:29 +00:00
Tanish MankuandClaude Opus 5 13e3154ab3 fix(fonts): read the Annex D.2 alias codes as the ink they draw
#2305 fixed WinAnsi 0xAD in `crates/fonts`, and closed with two loose ends it named: the
user-visible extraction path goes through hayro, not that table, so it still reported the
invisible character; and the same Annex D.2 note makes 0xA0 the `space` glyph. This finishes
both, and makes the two tables in the product agree about the same byte.

WinAnsiEncoding gives codes 0xA0 and 0xAD the glyph names `space` and `hyphen` -- the names it
also gives 0x20 and 0x2D -- and MacRomanEncoding gives 0xCA `space`. The tables called them
`nbspace` and `sfthyphen`, whose Adobe Glyph List values U+00A0 and U+00AD are invisible
formatting characters, so extraction hid ink the reader can see: "well-known" drawn with the
0xAD byte matched no search and no redaction pattern, and a non-breaking space did not match the
space a reader typed.

Evidence. Two sources transcribe the table the same way (an Annex D implementation's docs, and
the WinAnsi.enc encoding vector R ships, which gives `/space` at octal 240 and `/hyphen` at
255), and a black-box check settles the rest: on a synthetic fixture pdftotext reads the
base-encoded codes as `a-b a b`, and keeps U+00AD for a code an explicit `/Differences` names
`sfthyphen`. We now do both. Over the 989-file pdf.js corpus the soft hyphens we extract fall
from 117 to 13 and the non-breaking spaces from 30 to 26; everything left comes from `/ToUnicode`
maps, which say what they mean and are not touched.

Scope. The base table only, in the text direction only.

- A `/Differences` entry naming `sfthyphen` or `nbspace` is a deliberate statement about one
  code and keeps its glyph list value, as pdftotext does.
- Glyph lookup is untouched, so a font program's own `sfthyphen` program is still found first.
  In hayro that is why the alias sits in the Unicode path rather than in the generated encoding
  table: `code_to_ps_name` is shared with lookup.
- Writing is unaffected: `encode` takes the lowest code showing a character, so ' ' and '-'
  still write 0x20 and 0x2D, and a caller holding a literal U+00A0 still writes 0xA0.

Tests. `fonts` pins both tables and the whole decode/encode path including the `/Differences`
case; `render` pins the extraction a reader actually gets, over WinAnsi, MacRoman and
`/Differences`. Each was watched failing with each cell and each match arm reverted in turn, so
no assertion is passing by accident. fonts 70, render 296+4, redact 49, a11y 43 and edit 156 are
green, with fmt, clippy, `xtask layers` (37 crates) and `xtask assets` clean.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 12:52:53 -07:00
fanyzp 963609d7d6 fix(edit): draw the Cyrillic the Japanese faces lack with the bundled Inter face (#2593) 2026-10-16 00:31:49 +08:00
komo1iddinandClaude Opus 5.5 8ebe4f7a76 M7: write a full font program compressed and find it again on any page
A text edit that needs glyphs the source font's embedding lacks writes
the face's full program once. It was written uncompressed, and the
document-wide search that lets another page reuse it compared the
stream's raw bytes, so it only matched an uncompressed copy.

Write the program with Flate, and compare its decoded bytes (after the
/Length1 check) both on the page and across the document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:21:02 +05:00
a790a046a5 M7: keep line breaks when editing a multi-line paragraph (#1443) (#1486)
Adding a word to a multi-line paragraph merged or reflowed its lines.
Two causes:

- Chrome/Skia subset fonts leave glyphs of the /DW width out of /W.
  has_glyph treated those CIDs as missing, so words containing them
  measured short, run calibration inflated, unchanged rows rewrapped
  and new letters fell back to Helvetica. A composite subset with an
  explicit /DW now counts a ToUnicode-mapped CID absent from /W as
  present.
- Text drawn turned on the page (landscape pages drawn a quarter turn
  round) was joined into lines and rows along the page's x axis, so the
  whole paragraph became one line. Lines, rows and blocks are now found
  along each text's own baseline (identical values for upright text), a
  turned paragraph is rewritten in its own text space, and that rewrite
  keeps explicit line breaks. Formatting part of turned text is refused
  instead of being misplaced.


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

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 03:10:46 -04:00
Tanish MankuandClaude Opus 5 41d4c15849 fix(fonts): read WinAnsi 0xAD as a hyphen, so search and redaction can see it (#2305)
A page that draws a discretionary hyphen through WinAnsiEncoding's 0xAD extracts, in our map, as
U+00AD SOFT HYPHEN. That character is invisible, so text the reader can plainly see does not match
a search for it, and `redact`'s pattern matching -- which runs on exactly this map -- does not
match it either. The same silent-failure shape as reading Symbol as Latin: the text is there, the
tool cannot see it.

Annex D.2 gives codes 0x2D and 0xAD the same glyph name, so the byte a Windows codepage calls a
soft hyphen draws, and should extract as, an ordinary hyphen. Changed in the reading table only:
writing is unaffected, because win_ansi_byte maps 0xA0..0xFF by identity, so '-' still takes the
canonical 0x2D and a caller holding a literal U+00AD still writes 0xAD.

The round-trip test could no longer hold for that code -- two codes now mean one character -- so
it states the aliasing instead of asserting a bijection, and a new test pins the behaviour and the
reason.

**This needs the owner's reading of the spec before merging.** The evidence is not unanimous: a
PDF library's documentation cites Annex D.2 for the glyph name `hyphen` at 0xAD, and Poppler
extracts U+002D, but the vendored hayro maps 0xAD to the glyph name `sfthyphen` and reports
U+00AD, normalising to `hyphen` only when looking the glyph up in the font program. Two
implementations disagree and I cannot read the normative table from here.

Note this does not change `pdfcraft-cli text`, which maps through hayro, not this table; the
user-visible extraction path carries the same question separately.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 02:53:46 -04:00
Marco Arnulfo c3e9609bd1 fix(fonts): keep a subset Type 0 font for glyphs /W leaves to /DW (part of #1388) (#2153) 2026-10-15 02:47:44 -04:00
a7fe7a20c4 M7: Hebrew in Add text (#128) (#2109)
Add text and page_add_text draw Hebrew with the craft-fonts Hebr face
(Noto Sans Hebrew) through the shaped path Arabic and Thai already use:

- pdfcraft-fonts: ShapedScript::Hebrew, shape_hebrew and hebrew_has.
  Grapheme clusters keep points and cantillation marks on their letter.
- pdfcraft-edit: Hebrew paragraphs are laid out with UAX #9 like Arabic;
  spaces and punctuation between Hebrew letters take the Hebrew face.
  first_undrawable refuses Hebrew the face can't draw (or without the
  face) instead of writing "?"; existing items stay movable without it.
- Type 3 glyphs with ToUnicode and per-line ActualText, so extraction,
  search and the editor get typing order back.

Regular style only, as for Arabic and Thai.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:45:12 -04:00
komo1iddinandClaude Opus 5.5 5098728078 M7: reuse a page's font resource and program when embedding again (#1779)
Every call to `embed` or `embed_mapping` wrote a new Type 0 font,
CIDFont, descriptor, ToUnicode and glyph map, under a new `PCFull{n}`
name. Editing a paragraph that needs a full face (a completed subset,
shaped Arabic, a loaded font) therefore added another set of font
objects to the page on every apply, even when the text was the same.
The program itself was only shared when it was among the first 8192
objects of the document, so on larger files each edit also copied the
whole font program, often several megabytes.

`embed_mapping` now looks at the page's own `PCFull` resources first:
- one with the same font name, program, glyph map (`CIDToGIDMap` or the
  CFF encoding CMap) and ToUnicode map is returned as it is, and nothing
  is written;
- otherwise a program one of them already uses is shared, wherever it
  sits in the document, before the document-wide search.

Programs are compared by the length they were written with before their
data is decoded, and streams are compared decoded, so a file another
writer compressed since is still recognised.

Pruning resources a rewrite no longer uses is left for later: a page's
/Resources can be shared with other pages, so it needs a check of every
content stream that uses it.

The test embeds after 9000 other objects, so the program lies beyond
the document-wide search. Embedding the same text twice returns the same
resource; two mappings share one program; after the streams are
compressed, embedding again writes no objects; a different program
under the same name gets its own resource. It fails without this change
("PCFull1" against "PCFull0").

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:24:09 -04:00