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>
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>
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>
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>
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>
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>
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>
* 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>
* 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>
- 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>
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>
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>
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>
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).
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>
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.
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>
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>
* 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>
`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>
* 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>
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>
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>
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>
* 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>
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>
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>
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>
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
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>
* 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>
* 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
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>
* 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.
* 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>
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.
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>