Commit Graph

1189 Commits

Author SHA1 Message Date
mtlcl410 af62daeaf8 fix(organize): bound how deep an import follows the object graph (#1800) (#1921)
The page Copier recursed once per reference and once per nested array or
dictionary with no bound, so a source whose page reached a chain of thousands
of shallow indirect objects overflowed the stack in Insert Pages, Combine,
Extract, Split and Duplicate; the memo map only cut cycles. The copy now keeps
a depth of at most 256, refuses with an OrganizeError beyond it, and
import_pages_with frees the objects a refused copy added, so the destination
is as it was.


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

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 02:29:36 -04:00
mtlcl410 668f491771 feat(ui): a middle click closes a tab (#1853) (#1920)
As in Acrobat and browsers. A document tab with unsaved changes asks first,
through the same close request as its close button; the Combine files tab
closes too.


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

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 02:29:26 -04:00
mtlcl410 8248379152 fix(forms): new text and date fields take Acrobat's Auto font size (#1888) (#1912)
add_field gave text and date fields a fixed /Helv 12 Tf, which is clipped in
a box shorter than 12 pt, and a script can't see the clipping. They now get
/Helv 0 Tf (Auto), as Acrobat's new text fields do: 12 pt where there is
room, smaller where there isn't. An explicit size set afterwards still wins;
choice fields keep 12 pt.


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

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 02:29:16 -04:00
mtlcl410 1ae12090d0 fix(redact): the tail of a longer number is not a phone number (#1884) (#1911)
The phone pattern tried every position, so the last groups of a French
social security number, an IBAN or a SIRET matched as a trunk-0 or NANP
number once the groups before them were skipped, and the start of the
identifier stayed visible. A phone number is now a whole run of digit groups:
one reached through separators from a digit before it, or going on past the
28-character cap, is not one.


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

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 02:29:06 -04:00
mlkonrad 6882275ee5 M3: touchpad swipes glide on after the fingers lift on Wayland (#531) (#1909)
Wayland compositors leave momentum to the client: libinput reports the
swipe as pixel deltas and the lift as an axis stop (TouchPhase::End), and
the page view stopped dead there (#1708). It now measures the gesture's
speed over its last 0.1 s and keeps scrolling through the #759 1:1 path,
slowing with a 0.35 s time constant.

- No glide for a slow drag, a pause before lifting, zoom or Shift swipes,
  single-page paging, or with Animation effects off.
- Any wheel input, click, key, zoom, dialog or the document's edge ends it.
- Off on macOS (it sends its own momentum) and the web; Windows and X11
  report no touch phases through winit, so nothing starts a glide there.

Not yet: fingers resting on the touchpad don't catch the glide; that needs
Wayland hold gestures, which the vendored winit doesn't bind.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:28:56 -04:00
mtlcl410 aed3afdc20 fix(automation): a missing path under --root says where it was looked for (#1890) (#1907)
A relative path that doesn't exist below the root was refused with the path
as typed and the system's localised message, so a script written for
another working directory ("sub/a.pdf" with --root sub) gave nothing to
correct from. The refusal now names the resolved path and the root; paths
outside the root keep their uniform refusal.


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

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 02:28:46 -04:00
Eugene Hinderer 16d9198077 fix: render CCITT Group 4 images that declare /Rows 0 (#1903)
ISO 32000-2 Table 11 defines /Rows 0 (the default) as "height not
predetermined", but the vendored hayro-syntax decoder took it as zero
rows, so scanned pages written that way rendered blank. Treat 0 like an
absent /Rows and fall back to the image height.

Adds a synthetic regression test: an 8x4 all-black Group 4 stream,
hand-encoded from ITU-T T.6, rendered with /Rows absent, 4 and 0. It
fails without the patch.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:28:36 -04:00
Alex b5dc84df2a fix(audit): report faux-redaction rectangles in top-left page points (#1900)
doc_audit_redactions and `pdfcraft-cli audit` returned rectangles in PDF
user space (origin at the bottom-left of the media box), while comment_list,
doc_info, text_lines and the redaction tools give points from the top-left
of the page as displayed. The same mark read [144.14, 91.2, 265.95, 102.2]
in comment_list and [144.138, 739.8, 265.952, 750.8] in the audit.

Both surfaces now convert with the helper doc_info uses (view_rect, made
public for the CLI), so they report the same rectangles, after /Rotate.
The CLI reads the page geometry only when there is a finding to place.
AuditFinding::rect stays in user space; the audit README says which space
each surface uses.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:28:25 -04:00
Alex b9f0f84ff8 fix(a11y): bookmarks from structure keep the words of glyph-by-glyph headings (#1899)
* fix(a11y): bookmarks from structure keep the words of glyph-by-glyph headings

Chromium (Skia) draws text one glyph at a time, each moved into place with
its own Td. The marked text of a heading added a space at every Td, TD, T*
and Tm, so New Bookmarks from Structure titled them "C h a p t e r T w o",
and the real spaces between words were lost. On a 20-page tagged guide
printed by Chromium, 25 of 61 titles came out spaced.

mcid_texts now follows the text matrix (and the CTM, q/Q, Tc, Tw, Tz, TL)
and decides word breaks from where the glyphs are: a glyph that starts on
another line, more than 0.2 em after the previous glyph of the same marked
content, or back over more than an em, starts a new word. Kerning and
glyph-by-glyph placement no longer split words.

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

* fix(a11y): headings set with a negative font size keep their words

The em that decides word breaks was the font size times |Tz|, negative with
a negative font size: every glyph then started a new word, and a heading in
one Tj read "C h a p t e r O n e". The em is now |size x Tz|, and "ahead"
follows the way glyphs advance in text space, backwards when the size or Tz
is negative, so a TJ word space still splits words there as on main.

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

* fix(a11y): keep wrapped headings separate under horizontal scaling

Co-Authored-By: OpenAI Codex <noreply@openai.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
2026-10-15 02:28:01 -04:00
Alex 56ef1f1c4e fix(audit): an applied redaction at the end of a line is not a faux redaction (#1898)
* fix(audit): an applied redaction at the end of a line is not a faux redaction

After Apply Redactions removes text at the end of a line, the line keeps a
TJ adjustment where the text was, and the box of the line still spans it.
The audit compared the redaction box with that box: at the end of a line
it covered more than 40% of it and the words left beside it were reported
as covered text, on a correctly redacted file. In the middle of a line the
gap was usually too small to cross the threshold.

Text lines now also give the boxes of the words they draw (`ink`: glyphs,
without spaces and TJ adjustments), and the audit measures how much of
those words a cover hides. A real cover over text that is still there is
found as before.

doc_audit_redactions also reports its rectangles in top-left-origin points
like comment_list and doc_info; they were in PDF user space.

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

* fix(audit): leave the rectangles of doc_audit_redactions in user space here

Reporting doc_audit_redactions rectangles in top-left-origin points is a
separate subject from the false alarm this branch fixes, and on its own it
made the tool and `pdfcraft-cli audit` disagree on the same file: the CLI
still prints PDF user space. Take it out of this change; the tool and the
CLI can move together in their own change.

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

* fix(edit): word boxes span whole words when glyphs run backwards

With a negative font size or a negative horizontal scale (Tz), each glyph
advances backwards along the text matrix, and the box of a word kept only
its first glyph: the end of the word was compared with a start that had
already moved past it. A word box is now the union of its glyphs whichever
way they go, and a TJ adjustment ends a word past 0.2 em of the scaled size,
whatever its sign.

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

* fix(audit): a cover over a short value after a long label is still found

Measuring a cover on the words of a line alone missed faux redactions main
found: a long label followed by a short value hidden under the box is less
than 40% of the line's words. A cover now also counts, as before, when it
hides 40% of the line's box and at least half of one word still drawn; a
cover over the gap an applied redaction left, with no word under it, still
does not. The audit README states both rules.

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

* fix(audit): retain partial-value cover detections

Co-Authored-By: OpenAI Codex <noreply@openai.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
2026-10-15 02:27:55 -04:00
ejbarraza f0c877ca23 audit: harden finding classes with golden tests and docs (#1892)
- Pin FindingKind stable IDs (covered-text, unapplied-mark) with a test
  and document the no-rename contract on id()
- Golden tests pinning the full AuditFinding struct for both finding
  classes (page, kind, exact rect, covered text)
- Depth-limit boundary tests: 32 nested form XObjects audit clean,
  33 fail closed with TooDeep
- Fix stale crate docs: m/l axis-aligned rectangles count as covers (#1510)
- Document AuditFinding.rect normalization and covered_text joining

Tests + docs only, no behavior change.

Co-authored-by: Erick Barraza <ejbarraza1994@gmail.com>
2026-10-15 02:27:44 -04:00
mtlcl410 c14602993f fix(sign): an unreadable extendedKeyUsage allows no purpose (#1807) (#1866)
A malformed EKU left extended_key_usage at None, which reads as "no
restriction", so an issuer-signed certificate with a broken EKU could reach a
Valid verdict; EKU being on the processed-critical list let a critical one
through as well. Like keyUsage, a present EKU now restricts the key even when
it doesn't parse: it must be a SEQUENCE whose members are all OIDs, otherwise
it allows no purpose.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:27:33 -04:00
mtlcl410 424459f386 fix(audit): find rectangle covers closed with h or painted with b/b* (#1817) (#1865)
The redaction audit turned an re rectangle into an unknown path on a
redundant h, and its fill branch left out the close-fill-stroke operators b
and b*, so those opaque covers over extractable text produced no finding.
Keep a closed rectangle closed under h and treat b/b* like the other fills.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:27:23 -04:00
mtlcl410 81982b092b fix(cos): a nonpositive crypt-filter /Length no longer overflows (#1816) (#1864)
encrypt_dict multiplied any V4 StdCF /Length up to 32 by 8, so a large
negative value overflowed (a panic with overflow checks, a wrapped length
without). A zero or negative length is meaningless; read it as if absent, the
128-bit default, which also fixes small negatives that used to clamp to a
40-bit key and fail to open.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:27:12 -04:00
pauliusiv123-rgb 4d358d4fa4 M2: a /Rotate that isn't a quarter turn counts as 0, as the page is drawn (#1863)
Page::rotation rounded a /Rotate that isn't a multiple of 90 down to a
quarter turn (135 became 90), while the renderer draws such a page
upright. Display size and the display-to-user-space matrix (header and
footer, watermarks, measuring, printing, image signatures) then
disagreed with the page on screen. ISO 32000-2 §7.7.3.3 requires a
multiple of 90; read any other value as 0, as the renderer does.

Page import and image stamps, which read /Rotate themselves, already
treat such a value as 0. Test: rotate_off_a_quarter_turn_counts_as_upright
(/Rotate 135, 30, -45, 359).
2026-10-15 02:27:02 -04:00
mtlcl410 e337a49631 fix(a11y): mark_decorative no longer panics on an unpaired ParentTree key (#1809) (#1862)
clear_parent_tree walked /Nums with chunks_mut(2) and indexed pair[1], so a
trailing key without a value that matched the page's StructParents panicked
after the page content had already been replaced. Walk whole pairs only and
leave an unpaired key as it is: it maps nothing, so there is nothing to clear.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:26:51 -04:00
Alexis 0a054a425c M7: Add / Update / Remove menu on Header and footer, Watermark, Background (#1192) (#1857)
The Update and Remove commands existed and were enabled only when the
document had that kind of mark, but no menu listed them. A tool entry in
the catalogue can now list further commands (ToolItem::actions); the tool
panel shows a caret and opens a menu, each command enabled by its own
needs. Add stays usable with no document open (it asks for a file first).

Tests: the catalogue's menus name registered commands starting with the
entry's own, and every edit.*.update / edit.*.remove is reachable from a
tool entry.
2026-10-15 02:26:28 -04:00
Marco Arnulfo 9c2fc4f8e9 M10: A6 paper in the print dialog and doc_print (fixes #1838) (#1849) 2026-10-15 02:26:16 -04:00
Marco Arnulfo e017ac93aa Edit image: give each flip button the icon of its own mirror axis (fixes #1834) (#1841) 2026-10-15 02:26:05 -04:00
komo1iddin 6aa9de485c M7: justify paragraphs whose font shows the space with another code (#1832)
Word spacing reaches only single-byte code 32 (ISO 32000-1 §9.3.3).
Justify was therefore turned off for a composite source font, so the
paragraph came out left-aligned. And the generated Type 3 fallback, whose
codes are numbered from 1, had its 32nd character widened instead of its
spaces.

Where word spacing doesn't reach the space, each space of a stretched row
is now followed by a TJ adjustment of the same amount, and word spacing
stays 0. Fonts whose space is code 32 keep using Tw as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:25:53 -04:00
komo1iddin b18639c242 M7: rewrap Chinese and Japanese text between characters (#1827)
Chinese and Japanese are written without spaces between words, so wrapping
only at spaces left a paragraph of them as a single overlong row. A row may
now also end between two ideographs or kana (UAX #14 class ID), except
before closing punctuation, closing brackets, small kana and similar marks,
or after opening brackets and currency signs (JIS X 4051 line-start and
line-end rules). Latin words inside such text stay whole; Korean and Thai
still break only at spaces.

Both the plain and the styled rewrite paths use the same rule.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:25:42 -04:00
magdale76 df0bc0ffbc M11: merge image fragments, and the Standard and Mobile optimizer presets (closes #1783) (#1784)
* M11: merge image fragments, and the Standard and Mobile optimizer presets

Some PDF printers draw a page picture as thousands of images one pixel row
high (one real page: 3,300 strips, 4.3 MB). The optimizer works image by
image, a one-row image can't be resampled, and every JPEG strip carries its
own headers, so Reduce File Size saved 2 bytes on it.

- pdfcraft-optimize `fragments` (Settings::merge_fragments, on by default):
  runs of `q … cm /Im Do Q` drawing upright 8-bit grey or RGB images in one
  colour space, sharing their left and right edges and stacked edge to edge
  (at least four, nothing else drawn between them; or, failing that, the
  rows drawn one under the other up to a gap) become one lossless image at
  their finest resolution. The image settings then resample it, and Clean
  Up drops the strips. A page whose content can't be read back exactly is
  left alone.
- Presets as in Acrobat's Settings menu: Standard (Reduce File Size's
  choices) and Mobile (images above 144 ppi to 96 ppi, JPEG high). The PDF
  Optimizer dialog has a Settings menu (Custom once an option changes) and
  a Merge image fragments box; doc_optimize takes preset and
  merge_fragments and reports merged_images and fragments_merged.
- A progress stage for the merge; four new strings in all 21 catalogs.

On the real page: Standard/Reduce 4.3 MB → 231 KB, Mobile → 161 KB, with
a rendering closer to the original than Acrobat's Mobile result (147 KB).

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

* Refresh catalog hashes after merging main

---------

Co-authored-by: magdale76 <16279778+magdale76@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:25:19 -04:00
komo1iddin 72a46facb8 M7: read the page once per keystroke when measuring a text draft (#1781)
`text_edit_layout`, which the inline editor calls on every keystroke to
size the frame, interpreted the page's content four times: `text_blocks`
for the paragraphs, `text_lines` again for the untouched runs, once more
inside `rewrite_block` on the still unchanged clone, and finally for the
rewritten draft. The first three read the same, unchanged page.

It now reads the page's lines once, groups the paragraphs from them, and
hands them to the rewrite through `rewrite_block_from`, which
`rewrite_block_with_limit` also uses after reading the lines itself. The
draft is still read back once after the rewrite. On a page of 120 runs
in 80 paragraphs, 50 keystrokes in a release build took 0.87 ms each,
against 1.25 ms before (the fastest of three runs each).

No behaviour changes. The new test checks that the measured draft frame
is the extent an applied edit actually draws, for a longer row, a
shorter one and an added row, so sharing the lines can't drift from a
fresh read.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:24:55 -04:00
Sir Clangington III. c5c5d0d661 M2: Add word and character counts with optional content exclusions (#1780)
* M2: Add word and character counts with optional content exclusions

Retain the complete reviewed change, including prior integration resolutions,
on current upstream.

M2: Add document word counts and semantic exclusions

* Refresh catalog hashes after merging main

---------

Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:24:33 -04:00
komo1iddin 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
Marco Arnulfo 58e125ccf1 Automation: tell a tool name or an unknown id from a command without a tool in command_run (fixes #1759) (#1765) 2026-10-15 02:23:58 -04:00
komo1iddin 432d3172d4 M7: draw new characters with the source font's own embedded program (#1760)
A PDF that embeds the complete TrueType/OpenType program of a simple
WinAnsi font can only encode 256 characters through that font, but the
program usually has many more glyphs. Typing one the encoding lacks
(ő, or Arabic in a face that has it) ignored the program: the edit went
to the Type 3 fallback, so official builds drew the character with the
Japanese face's Latin glyph, and builds without craft-fonts refused it
with "this text needs PdfCraft's Japanese fallback font", even for a
Latin letter.

The row editor now looks at the paragraph font's own program after the
source encoding, same-face resources and matching craft-fonts faces, and
before the fallback. When it has the glyph, the program is embedded once
per edit as a Unicode Type 0 font (the same embedding a loaded font
gets), so the new character keeps the source face. A paragraph whose new
characters only that program can draw takes the row editor's path.

The program is used only when:
- the font is not a subset (no `ABCDEF+` tag), since a subsetter can
  keep a character's mapping after dropping its outline;
- `validate` accepts it, which includes the OS/2 fsType check, so a font
  licensed for preview & print or restricted embedding is not reused for
  editable text.

When no face has a non-Japanese character, the error now names it and
points to Load font / Installed fonts; Japanese text keeps its message.

Tests: Inter embedded as a simple WinAnsi TrueType font, edited in a
one-row paragraph (the single-style path) and in a row of a two-row one,
draws ő from a Type 0 font whose program is Inter's bytes, with no Type
3 font and the word read back whole. The same program under a subset
name is not used, and without craft-fonts the edit fails, naming
"ő (U+0151)", and leaves the page unchanged. Both fail without this
change.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:23:47 -04:00
komo1iddin 8955ed8379 M7: keep the source operators of rows a text edit doesn't reach (#1754)
Changing one character in a paragraph rewrote every row of it from the
text state. A row whose width came from TJ kerning or justification gaps
was then squeezed back to that width with a `Tz` stretch, and the TJ
gaps became literal double spaces, so copying and searching saw
"justified  text". A typo fix in the last line of a justified paragraph
stretched the glyphs of all the lines above it.

`rewrite_styled_block` now compares each output row with the source row
at the same position. When every character in it comes from the source
unchanged, no style range covers it and the paragraph-wide formatting,
width and position are untouched, that row keeps its own operators and
only the other rows are drawn again. The new drawing goes where the
first rewritten row was, so the existing ET/BT split restores the text
matrix for the rows kept after it.

Rows are rewritten as before when:
- an earlier row wrapped and pushed them down (they no longer line up
  with a source row);
- a rewritten row used `'` or `"`, whose move to the next line the rows
  after it depend on;
- the first rewritten row is drawn in a different space from the
  paragraph's first row;
- a source run contains a line break, so editor rows can't be paired
  with visual rows.

Tests: a justified paragraph edited in its first, middle and last row
keeps the other two rows' TJ operators byte for byte and every row's
position; colouring a word in the last row keeps the rows above. The
first test fails without this change. A second test pins the `'` case
(a row-leading `'` that is rewritten would lift the kept rows by one
line) and checks that a wrapping row shows each word once.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:23:25 -04:00
mtlcl410 7c2ca46d2f i18n: Taiwan vocabulary and the missing entries in zh-hant (#1750)
* i18n: Taiwan vocabulary and the missing entries in zh-hant

The Traditional Chinese catalog says it uses the vocabulary of Taiwan,
but 113 entries used mainland terms, often next to the Taiwan term for
the same word elsewhere in the file (對話框 and 對話方塊, 應用 and
套用 for Apply, 列表 and 清單). They now read 套用, 捨棄, 對話方塊,
另存新檔為, 自訂, 目前, 清單/清單方塊, 置中, 平面化, 選取, 送出 (submit),
浮水印, 醒目提示, 一般, 進階, 重設, 存取, 全螢幕, 範例, 標題列,
工具列, 收件者 and 字型大小, and quotations use 「」 instead of “”.
Application (應用程式) and Commit (提交) keep their words.

The 80 entries the Simplified Chinese catalog has and this one lacked
(About ▸ Contributors and Models, link and email confirmations,
preferences defaults, Create PDF from images, print scale notes, Fill &
Sign) are translated from the English, so those labels no longer show
in English. Reviewed by a native Taiwanese speaker.

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

* i18n: zh-hant Grab bag reads 精簡

The contributor list's compact view keeps the chosen sort order, so it
is named for its look (精簡, against 表格), not as an unordered pile.

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

* Refresh catalog hashes after merging main

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:23:03 -04:00
komo1iddin 0f86ba905f M7: fold repeated image transforms into one cm instead of nesting q (#1746)
Moving, resizing, rotating or flipping a figure wrapped it in a fresh
`q cm … Q` every time, so twenty drags left twenty nested graphics
states and twenty `cm` operators around one `Do`. Each edit made the
content stream longer and pushed the figure closer to the nesting limit
viewers enforce (PDF 32000-1 Annex C recommends 28 levels).

When the placement already sits alone in a `q cm … Q` (its own
placement, or the wrapper an earlier edit added), `transform_image` and
`translate_images` now change that `cm` in place: the CTM inside is
`local · cm · outer` either way (8.4.4, 8.3.4), so the result is
identical. A bare `cm Do` without a `q`, a shared clip or any other
content in the scope still gets the wrapper, once.

The new test runs twenty resizes and twenty group moves on a plain
placement, one with a crop exclusive to the image, and a bare `cm Do`,
and checks the visible rectangle after each step, that the `q` depth
stays at its first-edit value, and that the following text is untouched.
It fails without this change ("twenty resizes nest no deeper than one").

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:22:31 -04:00
mtlcl410 8e74a4850c Issue #1676: put fallback faces on the interface baseline (#1743)
Chinese next to Latin in one label sat a few percent of the font size
above or below the Latin, depending on the installed Chinese face. egui
centres a fallback face's line box on the primary face's instead of
lining up their baselines, so the fallback's baseline moves by half the
difference of ascent + descent (less half the difference of the line
gaps): +0.047 em for Microsoft JhengHei, +0.034 em for Microsoft YaHei,
-0.075 em for SimSun under Inter.

Fallback faces (the installed system face and the craft-fonts faces) now
get a FontTweak::y_offset_factor that cancels that shift, computed from
their own metrics with the skrifa egui uses, so any face lands on Inter's
baseline. skrifa moves from the native-only dependencies to all targets
(egui already brings it to wasm).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:22:15 -04:00
umesh429 b5039ca603 fix(ui): clicking a theme card in Preferences picks that theme (#1727)
Preferences > Appearance drew a Light and a Dark theme card, each with a
radio row, but neither the card nor the radio responded to a click: the
radio's response was discarded and the frame never sensed clicks. Only
the Appearance mode combo box changed the theme.

The whole card and its radio rows now run view.theme.light /
view.theme.dark, the same commands as the combo box, so the combo box
shows the choice. A card click is a manual choice that OS theme changes
leave alone, and Use system setting afterwards follows the OS again.

Co-authored-by: umesh429 <3474974+umesh429@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:22:03 -04:00
Sir Clangington III. 2048a18e53 M2: Bound literal text search with prefix matching (#1725)
Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com>
2026-10-15 02:21:53 -04:00
alexanderalber 5b220eb886 M6: detect form fields on scanned pages (#1721) (#1722)
Field detection read only vector data: text-layer words and drawn
rectangles and lines. A scanned form is a single picture, so it found
nothing there.

A page that an image covers at least half of, and whose content draws
no lines, is now rendered at 144 dpi and its shapes are read from the
pixels (pdfcraft_forms::detect::raster): ink against a local mean,
long thin runs as lines, enclosed rectangular patches of paper as
boxes, the empty band of a box with a small printed label as its
writing area, and the open-ended horizontal lines as rules. Labels come
from the text layer, else from OCR when the models are installed;
without either the fields get default names. Detection works in the
displayed frame, so a scan stored sideways reads like any other, and
fields on a turned page get /MK /R so typing reads upright.

generated with Claude Code
2026-10-15 02:21:42 -04:00
Tristan von Kries 4e06a01631 Edit text: the four-way arrow wherever a drag moves, and a sharp page while editing (#1717)
Moving a paragraph or image showed the text cursor over a closed paragraph
and a closed hand while dragging, so the drag by its box (and, since #1600,
by the frame of the open one) went unnoticed. Now the four-way arrow is
offered over a closed paragraph, on the frame band of the open one, over a
selected object group and throughout a move; edges keep the rewrap cursor and
the open paragraph's text keeps the text cursor.

While a paragraph is open, the page around the editor is drawn from one
raster capped at the 2048-pixel backdrop side, which went soft past about
1.2x on a Retina display. It is now asked for at the tiling threshold (4096),
sharp to about 2.5x on Letter or A4; a GPU with a smaller texture limit still
caps it.

Co-authored-by: Tristan von Kries <tristan@MacBook-Pro-von-Tristan.local>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 02:21:21 -04:00
Trần Nhật Duy db5b9b4364 M14: add Vietnamese interface and date localization (#1712)
* G14: Vietnamese catalog — menus, file and shell chrome

Add the Vietnamese (vi) TSV catalog header and translations for menus,
File/Edit/Help chrome, clipboard and common shell labels. PhotoCraft and
VectorCraft catalogs used only as translation memory when English source
and context matched. ATTRIBUTION records the asset hash.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: Vietnamese catalog — forms, signatures and pages

Translate form/Fill & Sign/certificate wording and page organize/rotate/
extract/split labels. Keep product and format names unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: Vietnamese catalog — comments, view, a11y and contexts

Translate comments/markup, view/zoom/preferences tools, accessibility and
preflight sentences, plus contextual rows (certificate, calendar, keys,
scroll speed, signature pad, action wizard, @plural one-form).

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: Vietnamese catalog — remaining dialogs and notices

Complete the Vietnamese catalog to every (context, English) key in the
bundled union (2,580 entries). No unapproved English UI fallbacks remain
outside the documented VI_KEEP_AS_IS list.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: restore complete Vietnamese catalog key order

Replace the domain-appended catalog with the verified 2,580-entry union
in stable key order so coverage matches every bundled reference key.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: register Vietnamese interface and date language

Add `vi` / Tiếng Việt to LANGUAGES and DATE_LANGUAGES with plural_none
and rtl false. Locale tags vi, vi-VN and vi_VN.UTF-8 resolve. Calendar
uses Tháng Một / tháng Một; Fill & Sign refuses non-WinAnsi month names.
Coverage tests require full command, literal, key and credits coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: document Vietnamese localization

Record Vietnamese at 100% catalog coverage in localization-parity and
ROADMAP, list Tiếng Việt in Preferences/docs, and note the G14 catalog
in the progress log. Engine/OS details, document contents, filenames,
CLI/MCP and external help remain out of scope by design.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: use ASCII > in Vietnamese preference paths

Inter lacks U+25B8 (▸). Match other Latin catalogs and write preference
paths as "Tùy chọn > …" so the glyph coverage test stays green.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: finalize Vietnamese terminology and current coverage

Disambiguate theme Appearance (Giao diện) from object look (Diện mạo),
normalize form Field to Trường, make redaction apply wording permanent,
use numeric calendar months, close the GoToR/digital-ID key gap, and
pin regression assertions. Refresh the vi.tsv attribution hash.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: align localization docs with Vietnamese coverage

Recompute the twelve-language mean (≈59% → ≈68%), document 23 interface
languages, and remove stale claims that Vietnamese is missing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: cover Wayland title-bar strings in Vietnamese

Translate Minimize/Maximize/Restore and the system title-bar preference
added on upstream main, and refresh the catalog attribution hash and counts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: correct current union key count in localization docs

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: prepare Vietnamese localization for review

Trim the vi.tsv header to maintainer-oriented notes, record native-speaker
community review in attribution and localization docs, and refresh the
weighted readiness arithmetic (localization ≈68% → ≈50%).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: DUY Tran Nhat (SAE-KIDS) <duytn@sanancorp.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 02:21:11 -04:00
alexanderalber 00957b9f21 M5: Fill & Sign text reads across on rotated pages (#1710)
* M5: Fill & Sign text reads across on rotated pages

Typed text, dates and text boxes ignored the page's /Rotate, so on a
page stored sideways (common for scanned forms) the text came out
sideways. The box now follows the displayed axes, the annotation
records the turn in /PCTextRotate and its appearance is turned back
with a form /Matrix, as stamps and image signatures already do.
Callouts keep user-space axes. The headless fill_sign_add tool gets
the same layout.

generated with Claude Code

* chore: credit alexanderalber in the About window

generated with Claude Code
2026-10-15 02:21:01 -04:00
manchitmr 089e13c074 Sinhala and Tamil interface: draw with an installed font (part of #1688) (#1706)
Sinhala and Tamil file names showed as boxes on the home screen, in Recent
and in document tabs: no embedded face covers either script, and the
installed-font fallback did not look for one.

Add sinhala_fallback() and tamil_fallback(), each probing for its script's
letter ka, and end every interface family with them:
- macOS: Sinhala Sangam MN / Sinhala MN, Tamil Sangam MN / Tamil MN
- Windows: Nirmala UI, then Iskoola Pota (Sinhala) or Latha (Tamil)
- Linux: Noto Sans Sinhala / Noto Sans Tamil, Lohit Tamil

The faces are read at runtime, never embedded or shipped, and
PDFCRAFT_SYSTEM_FONTS=0 still turns them off.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 02:20:51 -04:00
null 00000111 b558ef7447 i18n(pt-br): fix 6 Brazilian Portuguese strings (#1694)
* i18n(pt-br): fix 5 Brazilian Portuguese strings

A subject-verb agreement slip, a hyphenated colour, and three terms
that disagreed with the catalog's own wording.

* i18n(pt-br): call the comb option "Caixas", as its warning does

Keeps the warning's "As caixas de caracteres" and renames the option
from the literal "Pente" to match it.
2026-10-15 02:20:41 -04:00
Piohrun 9c875656de fix(render, ui): form-heavy documents draw and fill fast (spot-colour images, widget placement, Fields panel) (#1691)
* fix(render): convert spot-colour images once per pixel value (vendored hayro-interpret patch 28)

An image whose pixels hold at most 16 bits of samples (an 8-bit Separation or
DeviceN image, a 1-, 2- or 4-bit gray or indexed one) is now converted to RGB
through a table of every possible pixel value instead of pixel by pixel.

A Separation or DeviceN tint transform is a PDF function, often a PostScript
calculator program, and the image decoder ran it once per pixel. Fillable
character sheets draw their background art as an 8-bit DeviceN image: one with
a 3304 x 4105 background took 3.8 s and 430 MB to draw each page (Poppler: 0.27 s),
when opening it and again after every field was filled. It now draws in 0.11 s
and 100 MB, pixel for pixel the same.

The table decodes each sample through the same `decode_sample` as the per-pixel
path, keeps `fix_image_length`'s rules for short and overlong data (the pixels
past the data are converted as before), and is used only when the image has
more pixels than the table has entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8

* fix(render): place widgets without /P in one pass over the pages

Opening a document, the inspector finds the page of a form field whose widget
has no /P by searching every page's /Annots for it, once per field: O(fields x
annotations). A form of 8,000 such fields took 1.17 s to open instead of
0.27 s. The pages' /Annots are now read once, on the first such field, into a
map from annotation to page.

A widget that several pages list is now placed on the first of them, as
pdfcraft-forms places it; the search took whichever page a HashMap visited
first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8

* fix(ui): the Fields panel keeps up with large forms

Every frame the Fields panel searched the form for each row by name, once to
sort the rows into tab order and again to find what a click selects: O(fields²)
a frame. It also laid out, drew and built the tooltip of every row, scrolled
into view or not. With 8,000 fields the app used 13.4 s of CPU for 20 pointer
moves with the panel open (0.2 s with it closed).

The rows are now worked out by `field_rows`, which pairs each row with the
first form field of its name through one map a frame, as the engine pairs
them, and a row out of view is allocated and keeps its clicks, keyboard focus
and accessibility label but isn't drawn and builds no tooltip. The same 20
moves now take 0.86 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8

* docs: roadmap entries for faster form-heavy documents

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8

* fix(render): convert each lookup-table entry on its own (vendored hayro-interpret patch 28)

Patch 28 converted the whole table of possible pixel values in one batch.
Separation and DeviceN `convert_f32` concatenate each tint result, and a Type 4
program that returns the wrong number of components is only warned about, so a
malformed result for a value the image never uses shifted every later entry. A
32 x 32 image whose only sample (255) is red drew black because tint 0 returned
one component (PR #1691 review).

`convert_each` now converts the table one entry at a time, so a malformed entry
changes only itself. The largest table (65,536 entries, a two-ink DeviceN image
over DeviceCMYK) takes about 2 ms longer.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:20:31 -04:00
Marco Arnulfo f48b76c146 Text selection: a drag after a double- or triple-click extends by whole words or lines (fixes #1685) (#1689)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:20:09 -04:00
Marco Arnulfo 44fbacfffd Edit text & images: a dragged image's pixels follow its frame instead of waiting for the release (part of #1671) (#1677) 2026-10-15 02:19:45 -04:00
Marco Arnulfo 740117620d Text selection: dragging to the top or bottom edge of the view scrolls it (part of #1670) (#1675) 2026-10-15 02:19:34 -04:00
FredApps 760cde5ade M3: keep the full final spread reachable with unequal page heights (#1672)
Related #187: this corrects the reproduced unreachable tall-left content, not the reported backward-page flash.

AI-assisted by Codex (GPT-6).
2026-10-15 02:19:24 -04:00
sanyamsingh001 9bbd765ea4 Keep page labels and focused navigation consistent across reloads (#1667)
* M3: await remote-link refusal in the UI regression

Wait for the asynchronous notification before checking the existing no-tab and refusal-message assertions.

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

* M3: keep page label input consistent across reloads

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

---------

Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:19:13 -04:00
FredApps 2fee7af241 Clamp extracted Indexed image samples to the declared palette range (#1581)
* M10: clamp extracted Indexed image samples to hival

Validate Indexed hival as an integer in 0..=255 and clamp raw palette samples before lookup. Preserve existing Decode, mask, and short-palette behavior.

Refs #1201 (item 9 only).

AI-generated by Codex (GPT-6).

* ci: re-run (unrelated flaky test)

* test: make reduce-size no-op regression deterministic
2026-10-15 02:18:44 -04:00
tylerstoltz 5105eb732c feat(ui): drop files on the Pages panel to insert pages; track the pointer during drags on Windows (#1547)
Files dragged from outside onto the Pages panel are inserted at the gap under the pointer (with a drop indicator) instead of opening as new documents. Files dropped beside the panel still open as before.

On Windows, winit sends no pointer moves while files are dragged over the window, so the cursor is read with GetPhysicalCursorPos (winsafe "user" feature). This also makes the page grid show and use the gap under the pointer during a drag on Windows. Linux/BSD still have no pointer during a drag and keep the existing fallback.

Tests: two new kittest cases in tests/editing.rs (drop on the panel inserts at the gap / at the end; drop beside the panel opens a document). The Windows cursor path was checked by hand.
2026-10-15 02:18:34 -04:00
cyfung1031 e770c564a8 COS: resolve xref entries on demand (#1505)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-15 02:18:23 -04:00
Earl Co 0b732e90ff M12: mark leaf tags as artifacts (#1475)
* M12: mark leaf tags as artifacts

* chore: refresh translation asset hashes

* fix(a11y): retain text autotag exports after artifact merge
2026-10-15 02:18:13 -04:00
Luchsen 3d7336fc41 M5: add comment and reply submission shortcuts (#1457)
Submit highlight comment text and replies with Ctrl+Enter, retain Command+Enter
for floating composers, and let Shift+Enter insert reply newlines. Keep ordinary
Enter-to-post for replies and restrict submission to the focused editor.

Add UI regressions and parity evidence. Normalize existing lint and formatting
issues and pin the reload test's English labels so the required checks pass on
this Windows machine.

Co-authored-by: Armin Reisgys <Armin.Reisgys@de.bosch.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Luchsen <210128845+Luchsen@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:18:03 -04:00