* M14: Italian catalog: complete the missing entries
it.tsv lacked 265 keys that the other catalogs carry. The 201 the code
looks up are added in one dated section at the end of the catalog: the
sentences the accessibility checker, preflight, signature validation and
page-range parser word (their WORDINGS), the certificate rows and calendar
weekday and month labels (their tl_ctx contexts), digital-ID, docking and
print messages, and five plurals (one|other). Italian now has 2,528
entries.
The new rows follow the wording the catalog already uses: history labels
say "Modifica ..." like "Change {key}", and the Creator and Producer keys
that label fills in say "Applicazione" and "Produttore PDF", as the
Document Properties dialog names those fields. The Optimize PDF summary
says "eliminato" like its Discard options, the checker and preflight
sentences say "ha rilevato" and "Viene usato", the trust messages say
"nel tuo elenco di certificati attendibili", the password and certificate
rows say "non corrispondono" and "rilasciato" like the existing "The
passwords don't match." and "Issued by" rows, and the print hint quotes
the "Disattivato" option as the dialog shows it.
The other 64 are left out because no non-test code looks them up:
- 10 tooltip texts with a fixed macOS shortcut ("Zoom in (⌘+)"); the code
fills the shortcut in through "{key}" rows Italian already has;
- 9 older wordings the code no longer uses: the five Combine folder
messages, "Save (M4)", " on page {label}", the owner-only security
message and the old "Text recognition isn't installed" message (the
code now says "its model files are missing");
- 45 generic labels and plurals only the Lithuanian catalog has. The code
says "{n} file|{n} files" as two plain strings, "1 file" and
"{n} files", which Italian has; "{n} comment|..." and "{n} result|..."
are used nowhere.
The entries are AI-assisted translations written from the meaning of the
English (clean-room; no proprietary localisation consulted), and keep
their placeholders and trailing ellipsis. The catalog's SHA-256 in
ATTRIBUTION.toml is updated by hand, as the other catalog changes do.
localization-parity.md, the ROADMAP progress log, the M14 rows of
docs/roadmap.md and docs/localization.md record the new count, and
parity/acrobat-features.toml now says Italian translates the engine's
sentences too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refresh catalog hashes after merging main
---------
Co-authored-by: kcobelli-pixel <257471167+kcobelli-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Holding Shift while dragging a corner handle now preserves the annotation's
current proportions, unless the annotation has its own fixed aspect ratio
(image signatures). This matches Acrobat behaviour for comments, stamps,
and form fields.
- Added helper to compute the effective ratio from fixed
aspect, current proportions with Shift held, or none.
- Updated in comments.rs and in prepare.rs to
pass the Shift modifier.
- Updated signature_drag.rs to read and pass Shift.
- Added tests for the new Shift behaviour in comments.rs and prepare.rs.
- Updated docs/ui-parity.md and parity/acrobat-features.toml.
Fixes#2199
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M6: sort multi-select choice indices
Write ascending /I option indices without reordering /V selections.
Add forms and engine regressions for reverse selection and save/reopen.
Co-authored-by: Codex <codex@openai.com>
* M0: update rustls to 0.23.46
Update only the resolved rustls version and registry checksum.
Keep the existing choice-index feature changes and advisory policy.
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>
Add text and page_add_text draw Hebrew with the craft-fonts Hebr face
(Noto Sans Hebrew) through the shaped path Arabic and Thai already use:
- pdfcraft-fonts: ShapedScript::Hebrew, shape_hebrew and hebrew_has.
Grapheme clusters keep points and cantillation marks on their letter.
- pdfcraft-edit: Hebrew paragraphs are laid out with UAX #9 like Arabic;
spaces and punctuation between Hebrew letters take the Hebrew face.
first_undrawable refuses Hebrew the face can't draw (or without the
face) instead of writing "?"; existing items stay movable without it.
- Type 3 glyphs with ToUnicode and per-line ActualText, so extraction,
search and the editor get typing order back.
Regular style only, as for Arabic and Thai.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Pair coordinate press and release after hover warmup so frame delays do not turn clicks into holds. Preserve text multi-click selection for coalesced pointer events.
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>
Each page's presentation settings of ISO 32000-2 s12.4.4 are read with the
spec's defaults for what is missing or out of range: the transition style,
duration, dimension, motion, direction, Fly scale and backdrop, and the
/Dur display time. doc_info lists them; pdfcraft-cli info counts them.
This is the data full screen needs to play transitions and auto-advance;
playing and editing them stay planned.
Refs #379, #2070.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Apply the minimum after page-unit scaling without inflating small raw crops. Retain the existing unit policy and nearly-zero fallback, with regression coverage for origins, rotations and reciprocal rounding.
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>
Every way into the page grid (toolbar, tool panel, page.organize, --organize,
Create multiple) goes through DocView::open_organize, which drops a selection
left from earlier that does not include the page being read and scrolls that
page's cell into view, computed from the row so virtualized rows need not be
laid out. Grid clicks navigate the document as Pages-panel clicks do, so the
viewer no longer resets the current page from its scroll position when the
grid closes.
Regression: crates/ui-egui/tests/editing.rs::organize_opens_on_the_current_page
(middle page, last page, reopening after a selection elsewhere).
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Check admission before each prepared result so a modal or queued picker keeps its document recipient. Wake retained replies once after picker consumption or worker abandonment.
Preserve merged #1914 continuation/startup behavior and the three worker regressions from upstream 3f044b62 (jcl-bit and its recorded coauthors). Keep the inherited printing/dependency changes intact; add only the remaining ownership barrier, wakes and focused regressions.
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>
Inserting an annex into a guide dropped the annex's bookmarks: the pages
came in without them, while Combine Files keeps every file's bookmarks
under one named after the file.
Insert pages from file now does the same: the inserted file's bookmarks,
destinations mapped to the copied pages (positions kept, named ones
resolved), go under one new bookmark titled with the file's name at its
first inserted page, collapsed. That bookmark is placed among the
document's top-level bookmarks before the first one pointing after the
inserted pages. A file without bookmarks adds none.
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>
INSTALLPDFCONTEXTMENU defaulted to 1 on every install, so an upgrade without
INSTALLPDFCONTEXTMENU=0 brought "Edit with PdfCraft" back. It now works like the desktop
shortcut since #1570: no static default, and a SetProperty after AppSearch turns it on for a
first install and, for an update, only while the menu's key is still in HKLM. A value on the
command line still wins.
"Create PDF with PdfCraft..." for the ten image types moves out of PdfcraftApp into its own
PdfcraftImageContextMenu component, gated by a new INSTALLIMAGECONTEXTMENU that defaults and is
remembered the same way. Moving the rows is safe because MajorUpgrade removes the old version
before installing (afterInstallValidate).
The PDF menu first shipped in 0.7.1 and the image menu in 0.4.0, so a missing key can also mean
the installed release never had it. OnlyDetect Upgrade rows (PDFCONTEXTMENURELEASE,
IMAGECONTEXTMENURELEASE) tell those apart, so updating from 0.6.0 still adds the PDF menu.
test-msi.ps1 evaluates the SetProperty and component conditions for the desktop shortcut and
both menus (first install, =0, update with and without the item, =1, and an update from a
release without the menu), and checks the registry searches, the Upgrade rows, that no image row
is left in PdfcraftApp, and the RemoveExistingProducts scheduling.
README documents the three properties, pinning the SHA-256 because same-version MSIs replace
each other silently, and the signing identities (checked against the v0.7.1 files).
docs/releasing.md and the misc.installers parity note follow.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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>
* 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>
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>
* 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>
* 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>
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.
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>
Carry selected tool actions through native file preparation, password retries and conversion, and defer startup intake until pending native opens finish.
Await observable completion in native opening, password, recovery and linked-PDF tests while retaining their content, identity, dirty-state, cancellation and destination checks. Verify the expected validation error and preserve the original document after refused consent targets.
M3; refs #1914.
GNOME's Wayland compositor draws no window frames (it offers no
zxdg_decoration_manager_v1), so winit drew its own fallback title bar,
which ignores GNOME's theme, dark mode and button layout. PdfCraft now
asks the compositor at launch (apps/pdfcraft/src/decorations.rs) and,
where it draws no frames, turns winit's frame off and makes the tab strip
the header bar, as in GNOME's own apps
(crates/ui-egui/src/window_controls.rs):
- Minimize, maximize/restore and close in Adwaita's style (round buttons,
symbolic glyphs drawn in code, no new assets), in the order and on the
side named by org.gnome.desktop.wm.preferences button-layout, read
through gsettings; Fedora's default `appmenu:close` shows only close.
Hidden in full screen.
- Resize zones along the window's edges and L-shaped corners, above the
rest of the UI so a press there resizes instead of reaching a
scrollbar; a hairline outlines the window while it isn't maximized.
- Preferences > Use system title bar (shown only where the app draws the
frame) gives the window back winit's frame at once, without a restart
(part of #1001). Named as in VectorCraft and PhotoCraft, but GNOME draws
no title bars for apps, so on GNOME this is winit's basic frame, not
GNOME's own; the tooltip says so. Saved as `system_title_bar`;
`system-title-bar` on the control channel.
- PDFCRAFT_TITLEBAR=app|system overrides the detection.
KDE, Sway, Hyprland, COSMIC and X11 offer server-side decorations and
keep the frame their desktop draws; macOS and Windows are unchanged.
Not done: right-clicking the header bar doesn't open GNOME's window menu
(egui has no command for it), the window has no shadow or rounded
corners, the resize zones lie just inside the window rather than outside
it, and Windows still uses its system frame (the rest of #1001).
The new labels (Minimize, Maximize, Restore, Use system title bar and its
tooltip) were added to all 21 translation catalogs. These translations
have not been checked by anyone fluent in those languages and should be
reviewed. The catalogs' hashes in ATTRIBUTION.toml are updated.
Made with Claude Code (Claude Opus 5.5, medium effort). It passes the
workspace quality gates (fmt, clippy, cargo test --workspace, xtask
assets) and was checked by hand on Fedora 44 GNOME (light and dark,
maximized and not, the preference toggled live), but it has not been
exhaustively regression tested.
Co-authored-by: bmh129 <11478682+bmh129@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A stamp placed on a page with /Rotate was drawn in user space, so the
label or picture turned with the page. Counterrotate the appearance so
it reads upright as the page is displayed.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- Create PDF detects BigTIFF (II+/MM+) as well as classic TIFF; the tiff
decoder already reads it, so these scans now convert instead of being
refused.
- Image detection accepts only GIF87a/GIF89a, the headers conversion
decodes, so a malformed GIF header is no longer called an image and then
rejected with a misleading error.
- file-format-parity.md: XDP split into XFA data import/export (shipped
under form.xfa-data) and opening .xdp as a document (none).
Tests: bigtiff_images_become_pages; malformed GIF headers in
source_kind_tells_pdfs_images_and_text_apart. Both fail without the fix.
Claude-Session: https://claude.ai/code/session_01F8DFYWkDyTnEcin9PNkUW5
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Flattening deleted a cloudy comment it could not draw, and a refused text-box restyle or resize left the new properties in the file. Arrow defaults were stored on plain lines. A custom match ending on a separator included the next glyph, and the vertical-text redaction error claimed an overlap it had not checked.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
`set_info` changed only `/Info`. Files that carry XMP (LibreOffice and Word
exports, PDF/A) kept the old dc:title and dc:creator, so viewers that read
XMP first kept showing the old title, and PDF/A preflight reported the two
as different (6.6.3).
The matching XMP property (Title dc:title, Author dc:creator, Subject
dc:description, Keywords pdf:Keywords, Creator xmp:CreatorTool, Producer
pdf:Producer) is now replaced where it stands, added in a description of
its own, or removed with an empty value. The packet is edited as text and
the rest of it stays byte for byte; files without XMP get none, and a
packet that isn't UTF-8 is left as it was.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
wgpu validates indirect draw and dispatch arguments with a compute shader it
compiles when the device is created. Metal on an Intel Iris Pro (MacBookPro11,3,
macOS 12.7.6) fails to compile it ("Compiler encountered an internal error" in
MTLCompilerService), the device is lost, and the window falls back to OpenGL.
PdfCraft draws and dispatches nothing indirectly, so leave the check out of the
instance flags. WGPU_VALIDATION_INDIRECT_CALL=1 turns it back on. VectorCraft
does the same (storytold/vectorcraft#651).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(edit): move existing text by its frame, and selections with the arrow keys (fixes#1520)
* docs(parity): arrow-key nudging and the paragraph frame are no longer gaps (#1520)
The launcher installs and updates PdfCraft with `msiexec /passive` and no properties, so
INSTALLDESKTOPSHORTCUT always took its default of 1 and every update put the shortcut back,
even for users who had deleted it. The property no longer has a static default: a SetProperty
after AppSearch turns it on for a first install, and for an update only while PdfCraft.lnk is
still on the all-users desktop. A value on the command line still wins, and the full-UI
checkbox still starts ticked on a first install.
test-msi.ps1 checks the action runs after AppSearch in both sequences and evaluates its
condition for first installs, updates with and without the shortcut, and
INSTALLDESKTOPSHORTCUT=0.
A build compiled with PDFCRAFT_BUILD_LABEL=<label> shows "0.7.1 (<label>)"
in `pdfcraft --version`, `pdfcraft-cli --version`, Help > About and the
first log line (pdfcraft_engine::build_info). The version itself is
unchanged, so the update check, the MCP server info and the User-Agent
still carry the release version.
packaging/linux/package.sh puts the label in the file names
(pdfcraft-0.7.1-<label>-linux-x86_64.rpm) and makes the .rpm/.deb release
`1.<label>` (nfpm `release: ${RELEASE}`, 1 when unlabelled). rpm sorts
0.7.1-1.<label> after 0.7.1-1 and before 0.7.2-1, so a locally built
package upgrades over the installed release and the next release replaces
it. packaging/env.sh accepts only letters, digits and inner dots, which
are valid in both rpm and deb release fields.
Unlabelled builds are unchanged.
Made with Claude Code (Claude Opus 5.5, medium effort). It passes the
workspace quality gates (fmt, clippy, cargo test --workspace, xtask assets)
and was checked by hand on Fedora 44, but it has not been exhaustively
regression tested.
Co-authored-by: bmh129 <11478682+bmh129@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Two language additions each edited the "Auto follows the system
language" paragraph, and both versions ended up in the file: one
mentioned Turkish, the other Slovenian, and neither mentioned both.
Keep one paragraph that lists both.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Glyph boxes of tightly spaced lines overlap, so replacement treated the next line as a duplicate and removed it. A second drawing is removed only when it also shares the edited line's baseline.