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
PdfCraft > About PdfCraft is AppKit's standard panel (winit's default menu),
which takes Credits.rtf from Contents/Resources. Ship one with the lines the
in-app Help > About has: the one-sentence description, the licence, and links
to getartcraft.com and the Discord.
Co-authored-by: Tristan von Kries <tristan@MacBook-Pro-von-Tristan.local>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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>
* M4: keep the last saved revision across undo (#1310, #1311, #1312)
Undo treated a return to the opened file as unsaved work, and after a save it followed the restored snapshot's older base. Revert and the next save now stay on the last saved file, so that revision remains in the PDF.
* Run a tool after a background open of the file it asked for
A tool chosen with no document opened the picked file on the worker, but only ran when that open finished inside the picker callback. Remember the command on the pending open, and let the command test wait for the worker.
* Wait for background opens in the command tests
Opening a picked file or a recent file finishes on the worker, so a fixed handful of frames can assert before the tab exists.
* Compare batch copies to the PDF that was written
A new text PDF stamps the current second, so a later pdf() call is a different file. The numbered copy and the source must match the bytes that were saved.
* Drop the unused batch fixture left by the merge
Main already keeps the written PDF in original, so the older source binding is unread and fails -D warnings.
Consolidate the reviewed change and its follow-ups on current upstream.
Original change notes:
M8: Bound and reuse repeated redaction overlays
Prepare appearances within a shared 8 MiB budget before changing the document. Reuse one measured and encoded row, reject excessive geometry, and preserve document and undo state on refusal.
Fixes#1326
M8: Preserve upstream font metrics in bounded redaction overlays
M8: reuse ordered redaction mark groups (#1326)
Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com>
The print-ready PDF that `pdfcraft_print::impose` builds starts from a new
catalog with only /Type and /Pages, so the document's /OCProperties was not
carried over. The pages' marked content still refers to its optional-content
groups, but nothing said a group was off, so a layer that is switched off in
the document (and not drawn on screen) was printed anyway.
Keep /OCProperties in the new catalog, so the sheets hide the same layers the
document hides. Test: a one-page document whose red square is in a layer; with
the layer off by default it is not drawn on screen and, now, not on the
printed sheet; with the layer on it is drawn in both.
Printing still follows the layers' default state only; applying the Print
event of /AS (a layer shown only on screen or only in print) is not done here.
Keep RGBA decoding unchanged while satisfying the current Clippy lint.
Include the separately verified Rustls 0.23.46 lock prerequisite.
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
A PDF can forbid copying its text. `pdfcraft-cli text` honours that and refuses, naming the owner
password as the way through. pdftotext ignores the flag and extracts anyway, so the oracle was
comparing our refusal against its text and scoring 0.000 -- measuring obedience, not quality.
bug1815476.pdf is the case: a one-page Spanish form we render perfectly, whose text we decline to
extract and pdftotext hands over regardless. It sat at the bottom of the worst list looking like a
font bug, which is what sent me looking at it.
Those files are now skipped and counted, not scored, and the count is printed rather than passed
over in silence, on the same rule the oracle crate exists for: a comparison that did not happen
must not look like one that succeeded. Over the first 250 corpus files that moves the mean from
0.811 to 0.854 and drops one file from the sample, with the median unchanged at 0.991.
Permissions come from `Document::permissions`, so this is not a match on our own error text. A
file that cannot be opened at all counts as permitted, because that is a different problem and
dropping it quietly would hide it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
F1 replaced the character-class approximation with the published standard-14 metrics, and
`helvetica_width` has delegated to `Std14::Helvetica` ever since. The README kept describing it as
PdfCraft's own estimate and, more importantly, said "No metrics file or font program from any
vendor is bundled" -- which stopped being true when the generated tables landed.
They are Adobe-authored non-visual technical data, the item AGENTS.md 1.1 permits, carried in
std14_metrics.rs with adobe_data = true in ATTRIBUTION.toml. A contributor reading the README
would have concluded there was no vendor data here at all, which is the one kind of error that
matters in a provenance claim. Says what is bundled now, and what still is not: widths and
encodings, never a font program or a typeface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The render oracle put four JBIG2 fixtures at the top of its disagreement list against Poppler, and
the obvious reading was that hayro-jbig2, which carries local patches, had a bug. Investigating it
found the reverse.
The 96 `bitmap-*.pdf` corpus files are a conformance suite: one small drawing encoded through every
corner of JBIG2 -- each generic-region template, custom AT pixels, TPGDON and TPGRON, refinement,
halftone with and without skip, symbol dictionaries with Huffman tables, MMR, the composition
operators, random access, unknown-height segments. Those features change how the bytes are
compressed, not what the picture is, so a correct decoder renders all 96 identically. That is a
real test with no reference image and no external oracle: the suite checks itself.
We render 1 distinct image across all 96. Poppler renders 8: it draws a blank page for the seven
halftone-skip and symbol fixtures, and its output changes when TPGRON is switched on, which a
correct decoder's cannot. So roughly a fifth of Poppler's JBIG2 output is wrong, the top of the
oracle's worst list is its bug rather than ours, and Poppler is not a usable oracle for JBIG2.
Watched failing by admitting one unrelated file to the set: the test fails and names it as the odd
one out rather than merely reporting a count.
The expectation holds of the pinned corpus. Whoever re-pins it to a newer pdf.js should re-check
this rather than assume it still holds, which the test's own documentation says.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Investigating the worst file of the first 250 turned the headline finding around. bug1552113.pdf
scores 84%, and the cause is Poppler: it paints the link's `/Border [0 0 112]` as a 112-unit band
across a 250x50 page, hiding the two lines of text. The fixture is named "this text should be
visible", and in our rendering it is. The oracle is not ground truth, and the top of its worst
list needs a person to look at both images before it is read as a bug of ours.
That led to a plausible change that the measurement rejected. Poppler synthesises appearances for
annotations that have none, where we draw only what the document carries -- `/AP`-less link borders
are already recorded as not covered under view.render-annotation-appearances -- so hiding
annotations looked like the way to measure page content alone. It is worse: it then scores Poppler
for every appearance we do draw. Over the same 250 files, shown gives mean 2.16%, p90 5.35% and 25
files within 0.1%; hidden gives 2.18%, 5.55% and 22, with annotation-tx3.pdf jumping to 45.64% and
annotation-tx2.pdf to 18.79%. The two biases nearly cancel.
So the default is unchanged and `--hide-annotations` is an opt-in for explaining a single file,
with the numbers written down so the experiment is not repeated. Verified that the default
reproduces the merged baseline exactly and that the flag moves it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gap 12 of docs/gaps.md: until rendering is measured, every claim about its quality is a judgement.
Text extraction had an oracle and written output had qpdf; rendering had nothing.
`cargo xtask render-oracle` renders page 1 of every corpus file with our CLI and with Poppler's
pdftoppm and reports how far apart they are. The metric is the point. A raw pixel comparison is
useless here -- two correct renderers disagree on every glyph edge, so it calls a perfect page
badly wrong and says nothing about whether a shading is missing. `pixels::compare` counts a pixel
as differing only when it differs from every pixel in the other image's 3x3 neighbourhood by more
than a tolerance, in both directions, taking the worse. A one-pixel shift is forgiven; absent or
miscoloured content is not.
First reading, 250 files at 72 dpi, tolerance 8: 229 compared, median 0.89%, mean 2.16%, p90
5.35%, 25 within 0.1%. It reports rather than gates -- the corpus is not on CI, and a first
reading of a borrowed renderer is a baseline to improve on, not a bar to clear. The worst results
cluster on JBIG2 `bitmap-*` files and on ZapfDingbats, which aims the renderer work at something
specific for the first time. 15 files could not be compared because the two renderers round the
page box to different pixel sizes; they are reported as unmeasurable rather than scored.
Verified against the knob's bounds end to end: tolerance 255 scores every file 0.00%, tolerance 0
takes the median to 0.95% and p90 to 26.36%.
Also turns on `extract_type3_shared_xobjects_matches_poppler_pixels`, which had never run anywhere.
Poppler is now installed on the Linux runner, and the test is version-robust by construction --
both sides of its comparison go through the same pdftoppm, so it is about our two documents rather
than about Poppler's output. Verified in all three states: runs and passes with pdftoppm present,
skips without it, fails without it when PDFCRAFT_ORACLES_REQUIRED is set.
`run_capture` keeps its exact behaviour, including leaving the buffer empty on non-UTF-8 output;
the byte-oriented capture an image needs is a second entry point on the same timeout logic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
55 files hand-rolled the same serialiser -- number the objects, record each one's offset, emit a
classic cross-reference table and a trailer -- 111 times over. The object bodies are properly the
business of the crate under test; the envelope around them is not, and every copy is a place for a
fixture to be quietly wrong in a way that makes a test prove less than it appears to.
`pdfcraft-testkit` holds that envelope. It has no workspace dependencies on purpose, so the crates
at the bottom of the layering can use it without a cycle, and it returns bytes rather than a parsed
document for the same reason.
The builder is byte-identical to the serialiser it replaces, asserted against a verbatim copy of
the old code across versions and trailer variations, because a migration that changes a fixture's
bytes silently changes what the test using it proves. Watched failing by breaking the free-entry
generation: three tests caught it. The one intended difference is that an empty stream dictionary
gives `<< /Length N >>` rather than a doubled space.
Migrates cos, measure and redact: 111 copies in 55 files becomes 108 in 53. The rest are
follow-ups rather than one sweep -- ui-egui alone is 27 of those files and is the most contested
area in the repository. Some copies should stay: a test about a malformed cross-reference table
needs to write its own bytes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>