Review: copying an added JPEG 2000 image failed even within its own
document, because the exporter can't write JPEG 2000 yet. Add Image stores
such a picture as the file it was given, so that file is the copy: it
pastes into the same document as the same object and into another as one
embedded image. The notes now also say that a CMYK picture stored
losslessly turns to RGB in another document.
Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
Edit::PasteContent pastes an added text box or image copied in the same
document or another, as it was (a text box's style; an image's picture,
turns, flips and crop), as one undo step. A pasted image draws its picture
from the copied object while that is still the picture in the target, else
from an added image there that already holds it (an earlier paste from
another document), else the file is embedded. Document::added_image_file
gives a copied image's picture as a file and honours the copy permission.
pdfcraft_edit::added::paste_rect / paste_place place a paste where the item
was, 18 pt down and to the right past each copy of it on the page (an image
with the same box and transforms counts whichever object holds its
picture), or at a point, kept on the page.
The content_copy tool drives it headlessly, across pages and documents.
The desktop keys and menus come in follow-up PRs.
Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
The tag on a page's backdrop copy, `/PCBackdrop << /Original ref /At /Len >>`, kept the
page's original content stream reachable. After Add background, then Redact (or Remove
Hidden Information, or Edit text) and a full save, the text taken off the page was still in
the file, in the original stream the tag pointed at. Text extraction didn't show it; reading
the file's bytes did. Restore also gave up after any edit, so the fill stayed off without a
word.
The tag is now `/PCBackdrop << /Index n /Op (f) /Before <SHA-256> /Was [num gen] >>`: how many
of the copy's operators come before the `n`, the fill it stands in for, a hash of those
operators by value (redaction writes the stream out again with other spacing), and the
original's number as two integers, which keep nothing alive.
Remove background turns the fill back on in the copy when the operators before it still
hash the same and a scan with the fill restored finds it painted first, covering the page.
Edits and redactions after the fill are kept. When the original stream is still there with
the same bytes, the page points at it again. Otherwise the copy stays as it is, and
Remove background says which pages keep their own backdrop off: `EditNotice::BackdropsLeftOff`
(engine), a toast in the app, `warnings` from `doc_remove_marks`. `edit_notices` holds
`EditNotice` now, with the undrawn-text notice as one of its kinds. The two new UI strings
are in all 33 catalogues.
`sha2` (already in the lock file) becomes a dependency of `pdfcraft-edit`.
Tests, each seen failing with its piece of the fix reverted:
- `the_tag_holds_no_reference_and_a_full_save_keeps_no_second_copy_of_the_page` and
`removed_text_leaves_the_file_when_a_background_replaced_the_pages_backdrop` (automation,
redaction and Remove Hidden Information through a full save): fail with `/Original` back.
- `a_fill_that_would_cover_content_painted_before_it_stays_off`: fails without the re-scan.
- `a_copy_written_out_again_with_other_spacing_still_comes_back` and the automation test:
fail when the raw bytes are hashed instead of the operators.
- `an_edit_after_the_fill_is_kept_and_the_backdrop_still_comes_back`: fails under the old
rule (only an unchanged copy comes back).
- `remove_background_names_the_pages_whose_own_backdrop_stays_off` (engine),
`remove_background_warns_about_pages_whose_own_backdrop_stays_off` (automation),
`remove_background_says_which_pages_keep_their_own_backdrop_off` (UI): fail without the
notice.
- `a_change_before_the_fill_leaves_it_off_and_names_the_page`,
`a_tag_that_does_not_add_up_is_left_alone` (hostile and old-format tags, never a panic),
`operators_hash_by_value_not_by_spelling`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit text set a paragraph whose font lacks a typed character (an accent
missing from a subset) in a standard font, always under the resource
name PCEdHelv, whatever face it stood for. Rewriting a bold paragraph
after a regular one replaced PCEdHelv with Times-Bold, so every
paragraph set in the earlier stand-in turned bold, and the editor's
draft measurement saw those paragraphs change and grew its frame over
them.
The stand-in is now named after its face (PCEdTimesBold, ...), as the
line and region rewrites already name theirs.
Fixes#4280
tabbing_back_to_the_same_field_then_saving_keeps_the_committed_value (its
same-frame case) and typing_and_saving_in_the_same_frame_saves_the_last_keystrokes
both asked scratch() for "same-frame". The tests of the it binary share one
process id and run in parallel, so they got the same folder, and one could
clear it while the other was saving into it: the save then failed to read
back with NotFound. scratch() now adds a number taken per call.
Delete the translation coverage tests and the tests of one language's
wording. Keep catalog validation (format, duplicates, placeholders, plural
forms, command ids) and the tests that use a language (registration,
plural rules, RTL order, glyphs, casing, font fallback).
cargo xtask i18n-report runs i18n::tests::translation_gaps_report, which
prints each language's missing count and first few keys and always
passes; xtask ci runs it after the tests.
The signing saves wrote with default options, so a redaction mark added
and cleared in the session (or a comment added and deleted) reached the
file through a signature, a timestamped signature, embedded LTV evidence
or a document timestamp, with its QuadPoints, overlay text and authors.
A signed revision can't be changed afterwards, so nothing could take it
out again.
`sign`, `sign_with_timestamp` and `dss::embed` now save with
`drop_unlinked`: everything they add is reachable (the signature through
the field's `/V`, the store from the catalog), and the ByteRange
placeholder is found in the written bytes.
`timestamp_document` can't: its `/DocTimeStamp` dictionary has no field
pointing to it, so the option would drop it and every document timestamp
would fail with "ByteRange placeholder not found". It frees the leftovers
first with the new `Document::free_unlinked_additions`, which frees
exactly what the option leaves out, and then adds the dictionary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The actions chained after a trigger's first one (/Next) are kept
whatever replaces it, not only another script, and the row reads
"…, then N more actions". Edit is disabled on every row while a
script is open, so it can't be overwritten.
Found while testing crates/js/src/formcalc.rs:2328-2330 ("format" number-picture branch, `if num < 0.0 { text = format!("-{text}") }`)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A /XYZ destination with a null (or unusable) left or top went to the top of the
target page. ISO 32000-2 §12.3.2.2 says a null value keeps the current one, and
Acrobat Reader, observed on a synthetic two-page fixture at 400 %, keeps the
user-space point at the window's top-left on the new page at the same zoom.
CAD exports (WSCAD) write every cross-reference as [page /XYZ null null 0], so
following one zoomed in landed on the sheet's title row instead of where the
reader was looking.
go_to_dest now reads the place at the window's top-left before the jump and
uses it for the null coordinates, along each axis the page overflows; a page
that fits keeps the layout's own placement. Null /FitH top and /FitV left
still go to the page edge.
Tests: a unit test (zoomed, partly given, view turned a quarter, page that
fits) and an end-to-end click on such a link at 400 %, both seen failing with
the fix reverted. The malformed-bookmark case in
bookmark_destinations_scroll_to_their_position now expects the kept place, as
DestView documents unusable operands as null.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"A picture covers this page (a scan?)" didn't say which page, so on a
range of several pages the user couldn't tell where the background
would stay hidden. Both messages now list the pages as 1-based runs
("Page 3", "Pages 1-3, 6"), stopping with "…" after eight runs.
The four strings are updated in every catalogue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit on a Run a JavaScript action loads its script into the editor;
Update replaces it on the same trigger, Discard leaves it. Saving the
tab now keeps actions that did not change as they are, and an edited
script keeps the actions chained after it (/Next).
* test(automation): give sign_document a real digital ID and document in the hostile-arguments sweep
sign_id_create, earlier in the sweep, wrote a digital ID to the in.pdf
placeholder that sign_document then took as its id. Almost every
sign_document call failed to decrypt it and never reached the hostile
value under test, and the decrypts took most of the test's time.
Each tool now starts from an empty root, and sign_document gets a test
ID with its password and a fresh blank document per call. 370 of its
440 calls get past the ID (1 before) and 340 past the document (0
before); the sweep takes 7 s instead of 89 s.
* test(automation): empty the sweep's root through its canonical path
form_merge_data, given the hostile name " ", writes a file of that name
into the root through the canonical \\?\ path, where Windows takes it
literally. Emptying the root through the plain path dropped the trailing
space, so the entry named the root itself and removing it deleted the
root. Entries are now reached through the canonical path, their type is
read from the directory entry, and a failed removal is reported.
* test(automation): fail the sweep when the root can't be emptied
Listing, typing or removing an entry left in the root now fails the test
with the entry's path, so no tool runs against an earlier tool's files.
The page counts the spacing after every character, the last of a line
included; epaint only puts it between characters, so a line could fit in
the editor and wrap in the saved text.
* Allow OFL typefaces whoever designed them (AGENTS.md §1.1)
Noto CJK and Source Han from their open-source releases are allowed like any
other OFL font. The fonts build no longer skips them, the asset check accepts
an OFL font credited to Adobe, and the docs describe the Chinese faces that
result. The ban on Adobe icons, artwork, UI graphics and the fonts Adobe
products install stays.
* Leave out whole CJK faces other than the Japanese ones for size
A Chinese or Korean face is embedded whole only under 4 MiB, so the 16.4 MB
Noto Sans CJK SC in craft-fonts stays out of the binary and the embedded
total against the pinned craft-fonts is unchanged (25,204,828 bytes).
* docs: replacement boxes for Chinese without craft-fonts only on the web or without an installed Chinese face
* xtask assets: the OFL font exemption needs at least one licence ID
* fonts build: count a CJK script tag with a region (Hant-HK) as CJK, shared with the embedding test
* AGENTS.md: the asset gate exempts OFL fonts from the Adobe-author check
* fonts build: count a Hangul-only face (Hang) as CJK for the size rule
* fonts: the CJK embedding test checks the size rule, sharing its constants with build.rs
* docs: the missing Hans face is a property of the pinned craft-fonts, not of every build
* fix(links): use resolve_absolute for remote document paths
* fix(ui): refuse output file names that end in a dot or a space
* ci: check that git dependencies come from storytold
* ci: release only with git dependencies from storytold
* ci: check git dependency sources on a locked, parsed Cargo.lock
The git-sources step in ci.yml and release.yml fails when Cargo.lock is
missing, when .cargo/config.toml replaces or patches a source, when
Cargo.lock is out of date with the manifests (cargo metadata --locked),
and when any package source parsed from Cargo.lock as TOML is a git
source outside github.com/storytold/. A grep on Cargo's usual spelling
missed single-quoted strings and stale lockfiles.
* fix(links): a Windows path on Unix is refused, not reported as needing a source
resolve_remote_document_path reported MissingSourcePath for any path that
resolved but not to an absolute one. With an absolute source, such a path
is one this platform cannot open (C:\x.pdf on Unix): it is now
NotLocal(Invalid). A relative target from a missing or relative source
still gives MissingSourcePath.
* ci: check the legacy .cargo/config for source replacement too
Cargo reads .cargo/config as well as .cargo/config.toml, so the
git-sources step greps both; grep -s keeps a missing file from
failing the step.
* ci: parse cargo config for source replacement, patches and path overrides