Commit Graph
73 Commits
Author SHA1 Message Date
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
sanches cfeaa8a996 Merge main: resolve ATTRIBUTION hashes and add Next/Previous tab
Upstream moved the Hungarian catalog and std14_metrics forward and added
the Next tab / Previous tab labels (document-tab cycling); the four new
catalogs translate both, and their ATTRIBUTION.toml hashes are
recomputed. The manifest check passes with 274 repository assets.
2026-10-15 03:59:00 +03:00
Tanish MankuandClaude d9607ec3be F3: one documented substitution chain, and report what it chose and why
A font the document does not embed was replaced by whatever the caller
happened to pick, and nothing told the user. `doc_info` said only
`embedded: false`, which is why "my Chinese PDF shows nothing" has been
a mystery rather than a sentence.

`crates/fonts/src/fallback.rs` applies one chain, in a documented order,
and returns what it chose with a reason:

1. embedded -> nothing substituted;
2. /BaseFont names a standard-14 face or a known alias (exact metrics);
3. craft-fonts holds that exact family;
4. /CIDSystemInfo /Ordering names a script we have a face for;
5. the ordering names a script we have no licensed face for -- report
   none, so nothing is drawn rather than the wrong glyphs;
6. otherwise the nearest of the fourteen by descriptor flags.

It delegates to the existing per-script selectors instead of restating
their order, so each keeps one source of truth. It answers the
dictionary question only; "which face has a glyph for this character?"
depends on the string being drawn and stays where the drawing happens.

`doc_info` gains `substitute` and `substitution` per font. Both null
means the document's own glyphs are shown; a null `substitute` with a
reason means no face was available at all. That is Acrobat's Actual
Font, minus the interface, which `parity/` now records as still missing.

Two findings on the way:

`Std14::from_base_font` matches loosely, so "SomeHouseSerif" resolved to
Times by its name containing "serif". The face is a fair guess but
reporting it as "names one of the standard 14" would have been untrue of
a font nobody has heard of. `Std14::recognise` now says whether a name
matched an alias or only a family hint, and the two report differently.
`from_base_font` is unchanged.

The generated table moved to `crates/fonts/src/std14_metrics.rs`, data
alone, and the ATTRIBUTION entry and AGENTS.md 1.1 line follow it. F1
put the hash on a file holding both the table and the code reading it,
so this commit's ordinary edits tripped the gate -- and a hash that
churns on unrelated changes gets re-attested reflexively, which is the
protection gone. Verified both directions after the split: editing
std14.rs passes the gate, changing one width in std14_metrics.rs fails
it.

Every new test was watched failing first: removing the script step
breaks the two unit tests and the doc_info integration test.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 16:42:10 -07:00
sanches 1ff8fe4800 i18n: add Indonesian and Vietnamese interfaces
Two new complete catalogs (2542 entries each, full coverage of the
uk.tsv key set): Indonesian (id) and Vietnamese (vi), both with a
single plural form per CLDR. Registered in LANGUAGES (22 → 24) with
locale-tag tests. Also adds Devanagari font support (for the upcoming
Hindi catalog) to the fonts crate, and fixes 19 duplicate keys in
ru.tsv that crept in during a previous merge.

Tests: 110 passed, 0 failed.
2026-10-14 00:07:07 +03:00
PINYO PATTANAWASANPORN bc6cfbd6cb fix(sign): render Unicode characters in visible signatures using bundled Inter font
Enables visible signature appearances in pdfcraft_sign to render
Unicode characters (specifically Latin Extended-A / Central European
characters such as č, ć, ž, š, đ) by embedding the bundled
Inter-Regular.ttf font with /Identity-H encoding and a /ToUnicode CMap
when the signer name, labels, reason, or location contain non-WinAnsi
text.

When all text is within WinAnsiEncoding, the existing zero-byte overhead
behavior (using standard /Helv) is preserved.

Closes #2207
2026-10-13 13:15:21 +07:00
Tanish MankuandClaude 0a09864833 F1, F2: adapt to main and re-pin the Arabic layout golden
Rebase onto 7163ba3 (396 commits on). Three adaptations, none of them
behavioural:

- `crates/fonts` renamed its `arabic` module to `shaping` and added
  Thai, so `Family::width`'s callers moved; the italic argument is
  re-applied to all six, including one new call site main added in
  `draw_laid_shaped`.
- `Metrics::text_of` now returns `Cow<str>`, so the F2 tests compare
  through `as_deref()`.
- `unicode_map` writes into a struct field now, so `/Differences`
  inserts go to `out.encoding`.

`SMALL_ITEM_FINGERPRINT` in added/arabic_tests.rs is re-pinned, and this
is the part worth reviewing. That golden is the appearance-stream pin
F1's acceptance criteria asked for, which landed from the Arabic work
after F1 was written; it caught the change correctly. The item's Latin
run is "Hi " in Helvetica 12pt. The old character-class guess called H
680, i 260 and space 260 (14.4pt); the published metrics are 722, 222
and 278 (14.664pt), verified against the table. The Arabic clusters that
follow start 0.264pt further right, which is the intended effect: the
glyphs now land where the font says they do. The Arabic path itself is
untouched.

Also dropped from the rebase: a re-added ATTRIBUTION entry for
vendor/hayro-interpret/assets/LAB.icc, which main deleted in favour of
CGATS001Compat-v2-micro.icc. Carrying it would have attributed a file
that no longer exists.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-12 13:57:43 -07:00
Tanish MankuandClaude ce9a1d1014 F2: read glyph names by the naming algorithm, and stop reading Symbol as Latin
Two gaps in code-to-Unicode, both found by probing the current
behaviour rather than by reading the code.

A font with no /Widths and no /ToUnicode fell back to StandardEncoding
whatever it was, so /Symbol code 0x61 -- `alpha` in Symbol's own
built-in encoding -- decoded as "a". ZapfDingbats and MacExpert were
read as Latin the same way. That map is what `redact` matches patterns
against, so a wrong character there is a silent redaction failure, and
a wrong character is worse than no character. Symbol, ZapfDingbats and
MacExpert now yield no mapping at all unless the font names a base
encoding, which is always honoured as the deliberate statement it is.

This keys on the base font *name*, not the /FontDescriptor symbolic
flag. A great many ordinary Latin TrueType subsets set that flag and
still rely on a Latin encoding, so suppressing on the flag would lose
real text across a large share of real documents. A test pins that
restraint so it is not "fixed" into a regression later.

Glyph names now follow the published naming algorithm instead of two
of its cases: a name is cut at its first `.` (`a.sc` is `a`), split on
`_` into components (`f_i` is "fi"), and `uniXXXX` carries any number
of four-digit values (`uni00660069` is "fi"). Names are resolved whole
or not at all, so half a ligature is never returned. `glyph_text`
returns a String for this; `glyph_unicode` stays as the single-character
view. Hex parsing rejects the leading `+` that `from_str_radix` would
otherwise accept, and a lone surrogate half.

Both halves were verified by reverting each fix and watching the
matching tests fail. The parity note on protect.redact-patterns now
says Symbol-encoded text is skipped rather than mis-decoded.

Still open, and an owner decision: Symbol and ZapfDingbats could be
mapped properly with a glyph-name-to-Unicode table, but the vendored
Adobe Glyph List is not on the AGENTS.md 1.1 closed list.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-12 13:57:43 -07:00
Tanish MankuandClaude Opus 5 d0d1ad8192 F1: exact standard-14 metrics
pdfcraft_fonts::helvetica_width was a fifteen-line guess by character class,
and seven crates generated appearance streams through it: annot, edit, forms,
ocr, redact, sign and xfa. Times was Helvetica x 0.9, bold was x 1.05, Courier
a flat 0.6. Against the published metrics a Times capital A is 722, so that
estimate said 600 -- a 17% error on a letter, compounding across a line.

Std14 carries the real advance widths for all fourteen faces, indexed by
character code: WinAnsiEncoding for the twelve text fonts, which is what we
write into the appearance streams we generate, and their own built-in
encodings for Symbol and ZapfDingbats. The tables are generated by joining the
published encodings with the per-glyph widths.

helvetica_width now delegates, so every call site gained exact Helvetica with
no change. The three places that faked another face by scaling it -- Family
in edit::added, the paragraph measure in edit::text, and the redaction overlay
fonts -- now select the right face instead. So does Metrics::width in
fonts::pdf, which measures existing document text when a font carries no
/Widths; that path feeds redaction's glyph removal, where a wrong width means
cutting the wrong glyphs.

Tests spot-check the table against values typed in from the specification by
hand, so it is verified against the spec rather than against whatever produced
it; plus Courier being fixed pitch everywhere, full printable-ASCII and Latin-1
coverage, Symbol and ZapfDingbats not being WinAnsi, /BaseFont aliases and
subset prefixes resolving, and hostile text staying finite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-12 13:57:43 -07:00
2839152af1 M12: preview text structure proposals for auto-tagging (#1383)
* M12: preview text structure proposals for auto-tagging

* Auto-tag preview: no unregistered command, allowed on copy-restricted files like a11y.check

The preview is read-only with no UI command yet, like render_preview, so it no
longer names the Planned a11y.autotag catalog id (tool_table_is_well_formed).

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

* M12: preview text structure proposals for auto-tagging

* Keep read-only auto-tag preview separate from unavailable command

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:20:13 -04:00
e33b66e9b5 Edit text: support per-font visual-order Indic text and nukta reordering (#1380)
* Edit text: support per-font visual-order Indic text and nukta reordering

- Detect legacy visual-order Indic fonts via Metrics.visual_order (fonts with
  Indic codepoints without OpenType GSUB tables, or legacy visual font names).
- Implement Indic visual-to-logical and logical-to-visual pre-base vowel
  reordering in crates/edit/src/indic.rs, with support for decomposed/precomposed
  nuktas and conjuncts.
- Apply reordering during extraction and source font reuse only when visual_order
  is set, leaving modern logical-order text untouched.
- Add unit tests for nuktas, conjuncts, and round-trips in crates/edit/src/indic.rs
  and font visual_order tests in crates/edit/src/tests.rs.

* Visual-order Indic: positive evidence only, ActualText kept logical, no font decoding

- A font is visual order only when it is a known legacy Indic family (subset tag
  allowed; aps-/dv- as prefixes) with an Indic Unicode mapping. A missing GSUB is
  no evidence (bare CFF and subset fonts drop it), so Metrics::from_dict no longer
  decodes every embedded font program on every call.
- Lines with /ActualText text are already logical: skip indic::to_logical, so
  "बढ़िया" no longer becomes "बढ़यिा".
- reading_lines no longer runs bidi recovery on every line (it changed RTL
  reading_blocks output for all documents); only the Indic recovery remains.
- indic.rs indexes through get().
- Tests use a ToUnicode fixture (not visual-order ActualText), cover an edit
  round-trip, logical ActualText in a legacy font, and a subset Noto font.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:18:37 -04:00
Marco Arnulfo a0575f2478 fix(fonts): a /Differences code past 255 no longer encodes as its last byte (#1314) (#1394)
A simple font whose /Differences puts Euro at code 300 encoded "€" as
the single byte 0x2C, which the same font decodes as a comma. Simple
fonts show one-byte codes, so a larger code now has no glyph to encode
with: encode returns None and the caller falls back to another font.
The code stays in the decoding table as before.

Adds pdf::tests::differences_codes_past_one_byte_are_not_encoded,
which fails before this change.
2026-10-12 07:20:48 -04:00
Marco Arnulfo af1c997aff fix(fonts): a composite /W run past the last CID stops there (#1313) (#1389)
A /W entry such as [4294967295 [100 200]] added the run index to its
first CID unchecked, so the second width overflowed: a panic with
overflow checks on, and with them off a width given to CID 0, which the
entry does not name. The run now ends at the last CID it can address;
/W [0 [100 200]] reads as before.

Adds pdf::tests::composite_width_run_past_the_last_cid_is_dropped,
which fails before this change.
2026-10-12 07:17:59 -04:00
6d843a8e8b Preserve source fonts in text editing and resize clipped artwork (#1116)
* M0: remove upstream trademark artwork from contributor fork

Use the existing document icon and a plain project label. Remove trademark
asset entries and regenerate attribution as required for modified forks.

Co-authored-by: OpenAI Codex <codex@openai.com>

* M7: group compatible PDF words and hide source text during editing

Join compatible words across text objects and subset-font spaces, recognize
component colours, and reuse subset fonts with numeric space positioning.
Render a read-only background with the selected glyphs removed while the
inline editor is open. Keep one frame and source line breaks, and reset
the layout width when switching paragraphs. Expose the same preview through page_render.

Add synthetic extraction, save/reopen, preview and UI regressions. Record
the remaining mixed-style paragraph and installed-font limitations.

Co-authored-by: OpenAI Codex <codex@openai.com>

* M7: preserve rich PDF regions and preview text edits in place

Group mixed source runs into complete visual regions, retain run styles and source encodings, and use validated embedded source faces in the inline editor. Preserve source baselines and show a single live frame as text grows or shrinks, backed by a read-only draft geometry tool.

Validation: strict workspace Clippy, workspace tests, all 30 WASM crates, asset and parity gates, licensed synthetic edit/save/reopen and desktop checks, and an opt-in four-page private-document regression.

Co-authored-by: OpenAI Codex <codex@openai.com>

* M7: keep cropped artwork from blocking text editing

Track page/path clipping separately from full image placement, use visible selection bounds, and prioritize text hit testing over overlapping artwork. Preserve fixed clips during image transformations and expose visible bounds through the headless tools.

Validation: strict workspace Clippy, workspace tests, 30 WASM crates, asset/parity gates, synthetic engine/tool/UI and native desktop checks, both private-document UI regressions, and all-page local edit/save/reopen/undo on a four-page cropped-artwork PDF.

Co-authored-by: OpenAI Codex <codex@openai.com>

* M7: resize cropped images with their clipping paths

Keep the visible frame and image pixels under the same transform. Preserve the
opposite corner and respond to diagonal, horizontal-only and vertical-only
corner drags without jumping from an off-centre grip. Refuse shared clipping
scopes that would move unrelated content.

Add synthetic engine/tool/UI regressions and opt-in private-file coverage
for 80 corner gestures and 10 grow/shrink save/reopen/undo render checks.

Co-authored-by: OpenAI Codex <codex@openai.com>

* M7: complete source font subsets and preserve clipped editing

Add explicit document fonts, CFF embedding, range formatting and bounded contextual shaping; isolate shared image clips and verify private Chinese edits on all pages.

Co-authored-by: OpenAI Codex <codex@openai.com>

* Revert "M0: remove upstream trademark artwork from contributor fork"

This reverts commit 3aa539b5b6.

* Test: give the text layer up to 4 s before copying (it flaked on a loaded machine)

---------

Co-authored-by: Kamoliddin Fatkhullaev <komoliddinfatxullayev@MacBook-Pro-Komoliddin.local>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:27:55 -04:00
8ee3673bd4 M7: support shaped Thai text in Add Text and UI fallback fonts (#767)
* M7: support shaped Thai text in Add Text and UI fallback fonts

Share the bounded shaping and Type 3 writer with Thai, preserving vowel and tone clusters and original Unicode through save/reopen. Keep the Thai face optional and update UI notices and their translations.

Constraint: Thai font assets remain in the companion craft-fonts repository.

Confidence: high

Scope-risk: moderate

Co-authored-by: Codex <noreply@openai.com>

* M7: show Thai Add Text before and after the fix

Document the rejected input on the base commit and the saved Thai text on the fixed build using genuine UI captures.

Constraint: Synthetic content and licensed fonts only; system font fallback disabled.

Co-authored-by: Codex <noreply@openai.com>

* M7: demonstrate Thai stress cases in the comparison screenshot

Show nine lines of stacked marks, both Sara Am encodings, Thai digits and mixed Latin text. Verify all 198 Unicode code points survive saving and extraction with both PdfCraft and Poppler.

Co-authored-by: Codex <noreply@openai.com>

* CI and releases: craft-fonts pin to 3d9ab3a (adds Noto Sans Thai)

ci.yml, freebsd.yml and the craft-fonts-ref release.yml passes to craft-common-actions move
from 8dcdacd to storytold/craft-fonts 3d9ab3a, the revision with Noto Sans Thai (#18), so
Thai Add Text is built in and tested.

* ATTRIBUTION: drop the screenshot entry again (the image isn't committed)

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 18:07:36 -04:00
Zeyu LiandBrandon Thomas b91d38c073 M14: text the fonts can't draw is kept and reported, never written as ? silently (#1112)
* M14: text the fonts can't draw is kept and reported, never written as ? silently

Form values and captions, text boxes, callouts, Fill & Sign text, measurement labels and stamp by-lines keep text the standard fonts can't draw, draw ? for it, mark the annotation (PCUndrawn) and report the edit (interface notice, tools' warnings); forms set /NeedAppearances (not in PDF/A). Flattening, redaction overlay text, typed and visible signatures and create-from-text refuse instead. Reports escape such characters as [U+XXXX].

* Catalogs: the two new can't-draw messages in Czech, Slovene, Kazakh, Turkish and Greek; refresh hashes

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 17:53:55 -04:00
tzagimandBrandon Thomas e609722b36 M7: Hebrew in Edit text (reading order, writing order, Hebrew glyphs) (#521)
* Hebrew fix

* Tests: the faux-redaction audit gets its own work folder (it shared "audit" with the space audit, so the two raced)

* Arabic golden test: compare bytes only in a build without an Arabic face (CI's Linux job builds with craft-fonts)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 17:41:06 -04:00