Section titles are drawn upper case through to_uppercase(), and tool names
inside sentences lowercased through to_lowercase(). Rust's case mapping knows
no language, so in Turkish "Gelişmiş" became "GELIŞMIŞ" and "İmza" became
"i̇mza" (i plus a combining dot). Turkish maps i ↔ İ and ı ↔ I.
upper_label() translates and upper-cases a title with the Turkish rule when
the label has a Turkish row (an untranslated label is still English and keeps
the English rule); in_sentence() lowercases with it. Other languages are
unchanged. The test failed before the fix ("GELIŞMIŞ").
An XFA form with no AcroForm fields was always classified as dynamic, so
opening it laid the template out into new pages and appended them as a
revision: the pages the user saw, and any later save, were PdfCraft's
layout rather than the file's own pages, with nothing saying so. Adobe
Designer writes <dynamicRender>forbidden</dynamicRender> in the config
packet of a static PDF form; such a form is now static and its pages are
kept. /NeedsRendering still makes a form dynamic, and with no word from
the config a form without fields stays dynamic as before.
The config packet is found in an /XFA array or a whole-XDP stream, read
with a 16 MiB decode cap, and scanned for the one element.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
- Remove Crop rebuilds the image's placement from its drawn CTM instead of
pattern-matching cm ops. After an Arrange under a translated CTM it used to
keep the absolute placement and draw the image displaced.
- Arrange refuses a move that would change which clips apply to the image
(moving it past a top-level clip it isn't under, or out from under one
without redrawing it), instead of silently cropping it.
- The exact-clip test now checks the moved copy carries its own clip and
hit-tests both images.
- Cargo.lock: add pdfcraft-edit's pdfcraft-render dev-dependency, which the
previous lockfile commit left out (CI failed --locked).
The test dragged a selection after a fixed four frames, but the page's text
layer arrives from the render pool in the background; under load it was
sometimes not in yet, the drag selected nothing and the assertion failed
(3 of 20 runs of the editing binary locally). Wait until DocView reports
the page's text instead, via a new has_page_text accessor.
Seven tests failed at random on slow or busy machines, main included
(FreeBSD run 38834256439 at 5fdd820b). They now count the work or wait
for the state they check; no assertion is loosened and nothing retries.
- spot_colour_images_run_their_tint_transform_once_per_sample_value
counts tint-transform runs (exactly 256, not one per pixel) through
vendored hayro-interpret patch (34), color::tint_transform_runs, a
thread-local counter, instead of a 10 s deadline.
- shadings_with_large_sampled_functions_paint_quickly: no counter tells
the flat table from the hash map (both read every sample per build),
so the build each paint does is timed against reading its samples on
the same thread, fastest of 8 rounds each, and must stay under 10x
(flat table 1.1x, hash map 109x), instead of a 10 s deadline.
- page_contents_and_nested_forms_share_one_budget: the budget is
checked by its warnings; the deadline is only a hang guard, now 600 s.
- The pen-stroke tests hold finished renders back from the view
(DocView::hold_rasters, for tests) while they look at the settling
stroke, then wait by state for its raster.
- files_and_quit_from_the_operating_system waits for the tab before
deleting the file the open worker reads.
- image_pixels_follow_the_frame_while_a_corner_is_dragged waits for the
page raster on screen before each drag.
Product fix (#1858): a released drawing stayed painted only until any
raster of its page arrived. In full screen its edit is applied a frame
later, so a raster already under way could end the preview first. A
settling drawing is now marked committed when its AddAnnotation lands,
only rasters after that end its preview, and a failed edit drops it.
image_pixels_follow_the_frame_while_a_corner_is_dragged drags before the
asynchronous page raster has arrived on a slow runner, and the preview
(drawn from that raster) then draws no picture: 'not only at []' on
windows-latest for #3070 and #3071 (a deny.toml-only change). With a
300 ms render delay added locally, main's test fails the same way and
this one passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
reading_lines grouped the joined operators by the stream their first token
is in and walked each group on its own. A marked-content sequence opened in
one stream (its /ActualText dictionary, or the whole BDC) and closed in the
next lost its replacement text: the export read "Glyphs" where every reader
of the joined streams reads "Replacement text".
The page's operators are now walked once. Each shown text still counts as
the stream its operator starts in, and text from another stream still starts
a new line, as before.
place_tagged wrapped the page's content in q/Q only when no Wrap stream
existed yet. Content appended after an earlier wrapper (by another tool, or
an older OCR layer) that left a cm, a clip or text state set then moved,
hid or distorted every later header, Bates number, watermark, added text,
flattened annotation and redaction cover.
Placing a mark on top now reads the page's actual operators instead of the
/PCMark tags. When the content parses cleanly with balanced q/Q and closed
text objects and paths, the stretch from the first stream that leaves state
set is wrapped, once, at a stream boundary no comment runs into. Content
that is already self-contained gets no new wrapper, so repeated marks don't
pile up.
Content that can't be read that precisely (stray bytes, unbalanced q/Q, an
unfinished text object or path, too large) is wrapped whole as before,
rather than refusing: redaction stamps its covers after removing the text
and can't back out. A q that content leaves open is closed after it, where
closing it changes nothing already drawn.
The wrapper's close now starts with a newline, so a comment at the end of
the content before it can't swallow the Q. Removing marks strips only
wrapper pairs that end the list, keeping one that later content still
relies on, and recognises the older close bytes too.
Pieces::join separated a page's content streams with a newline, which ended
a comment left open at the end of one stream. hayro, MuPDF and Poppler join
them with a space, so the comment runs on into the next stream and hides its
first line. Edit text, object_list, the redaction audit, redaction and the
Office/HTML export therefore saw text and boxes that nothing draws, and
redacting unrelated text on the page made the hidden content visible: the
rewritten stream dropped the comment.
- Pieces::join uses a space, so every reader of the joined streams agrees
with rendering.
- The parser records, per piece, whether a top-level comment runs into it
and out of it (boundaries only, not every comment). A redaction rewrite
of a piece ends an incoming comment before its first operator and leaves
an outgoing one open, so hidden content stays hidden.
- Reading for export parses the streams joined, instead of one by one.
- The redaction tag check compares the joined streams the same way.
- Pieces::piece_of uses a binary search.
RUSTSEC-2026-0370/0371/0372 were published today for zvariant, zbus_names and
zvariant_derive. All three are unmaintained notices rather than vulnerabilities,
and none has a fixed release to move to. They reach the tree as the D-Bus stack
behind accesskit_unix -> atspi (Linux accessibility) and as a dev-dependency of
pdfcraft-platform, so nothing ships in the other targets' binaries.
This is why main went red today; no dependency version changed.
The canvas asked can_paste_image() on every frame, which opens the OS
clipboard and decodes any image on it. With UI tests running in parallel,
many threads opened the Windows clipboard at once and the editing test
binary died with STATUS_HEAP_CORRUPTION.
- Ask whether there's an image to paste when a right-click opens a menu,
not on every frame. Pasting still reads the clipboard at paste time.
- Route every arboard call through os_clipboard::with, which holds a
process-wide lock across open, use and close.
- stale_clipboard_plain_text_does_not_paste_image no longer writes the
real clipboard; it gets its text from the paste event.
- Run CI tests in parallel again (revert --test-threads=1).