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 <[email protected]>
On macOS sync_all is F_FULLFSYNC, which SMB and FUSE volumes such as a mounted Cryptomator vault refuse with ENOTSUP or ENOTTY, so every save there failed. Those two errors now fall back to fsync; any other flush error still fails the save.
* 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)
Two follow-ups the maintainer asked for after #1355.
The script session's worker now says when the engine is ready for the
script it was sent (its JavaScript context built; FormCalc at once),
and the script's time limit runs from then, with the engine's own
start limit before it, as the one-shot path always did: building a
context is not charged to the script, which matters on a busy machine.
A field inside an area shares its data node with a field of the same
name outside it (areas are left out of data paths). The in-place
refresh of the live form now follows the data path through the tree
and gives every field bound to the node the value the tree builder
would, each by its own template field (an exclusion group there means
a rebuild), and the engine's value setter, with an index of widgets by
data node, gives every widget bound to the node the value by its own
kind, hidden fields included, writing the data whenever the node's
text differs. The script's time now also starts after its host and
globals are set up. A stale parity note (choosing a picture for an
image field, delivered in #1175) is removed.
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 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
* fix(audit): a rectangle drawn as an m/l path is a cover like re (fixes#1510)
* docs(parity): a rectangle drawn as an m/l path counts as a cover (#1510)
* Allow mouse-wheel scrolling during text selection
* Keep wheel-scrolling a selection drag over the page gap and the next page (#721)
Move the wheel handling out of the per-page drag branch to after the page
loop, gated on the pointer being over the document area (like #759), so it
no longer stops once the pointer leaves the selection's page.
Co-authored-by: Waleed <[email protected]>
---------
Co-authored-by: Waleed <[email protected]>
* fix(ui): dragged added text follows the pointer before release (fixes#1488)
* fix(ui): keep a moved item in place until the page is redrawn (fixes#1488)
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.
Follow up #1237 by deriving Bradford scaling ratios from the source and
D65 white points in LMS. Preserve CalRGB and the merged Indexed/Range
handling. Add independent neutral/chromatic, identity, asymmetric Range,
and degenerate-cone render regressions.
Co-authored-by: Codex <[email protected]>
* M8: Retire inherited redaction resources
* Redaction: pin inherited resources only on redacted pages, shared by reference
localize_resources rewrote every page in the document on every apply and copied a direct
inherited /Resources dictionary into each one, so a large document grew by N x the dict.
Now only redacted pages that inherit resources are pinned; a direct inherited dictionary is
made one indirect object and shared by reference. The parent's /Resources is retired when no
other page still inherits it (the leak fix), and kept (through the shared reference) when an
unredacted page still draws with it. A /Parent chain that can't be followed or disagrees with
the page tree retires nothing. Tests: an unredacted page is left untouched and the rewrite is
bounded; redacting every inheriting page retires the parent's resources. ROADMAP entry moved
into the revision-history table.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Rebase onto 7163ba3 (396 commits on). Three adaptations, none of them
behavioural:
- `crates/fonts` renamed its `arabic` module to `shaping` and added
Thai, so `Family::width`'s callers moved; the italic argument is
re-applied to all six, including one new call site main added in
`draw_laid_shaped`.
- `Metrics::text_of` now returns `Cow<str>`, so the F2 tests compare
through `as_deref()`.
- `unicode_map` writes into a struct field now, so `/Differences`
inserts go to `out.encoding`.
`SMALL_ITEM_FINGERPRINT` in added/arabic_tests.rs is re-pinned, and this
is the part worth reviewing. That golden is the appearance-stream pin
F1's acceptance criteria asked for, which landed from the Arabic work
after F1 was written; it caught the change correctly. The item's Latin
run is "Hi " in Helvetica 12pt. The old character-class guess called H
680, i 260 and space 260 (14.4pt); the published metrics are 722, 222
and 278 (14.664pt), verified against the table. The Arabic clusters that
follow start 0.264pt further right, which is the intended effect: the
glyphs now land where the font says they do. The Arabic path itself is
untouched.
Also dropped from the rebase: a re-added ATTRIBUTION entry for
vendor/hayro-interpret/assets/LAB.icc, which main deleted in favour of
CGATS001Compat-v2-micro.icc. Carrying it would have attributed a file
that no longer exists.
Co-Authored-By: Claude <[email protected]>
Two gaps in code-to-Unicode, both found by probing the current
behaviour rather than by reading the code.
A font with no /Widths and no /ToUnicode fell back to StandardEncoding
whatever it was, so /Symbol code 0x61 -- `alpha` in Symbol's own
built-in encoding -- decoded as "a". ZapfDingbats and MacExpert were
read as Latin the same way. That map is what `redact` matches patterns
against, so a wrong character there is a silent redaction failure, and
a wrong character is worse than no character. Symbol, ZapfDingbats and
MacExpert now yield no mapping at all unless the font names a base
encoding, which is always honoured as the deliberate statement it is.
This keys on the base font *name*, not the /FontDescriptor symbolic
flag. A great many ordinary Latin TrueType subsets set that flag and
still rely on a Latin encoding, so suppressing on the flag would lose
real text across a large share of real documents. A test pins that
restraint so it is not "fixed" into a regression later.
Glyph names now follow the published naming algorithm instead of two
of its cases: a name is cut at its first `.` (`a.sc` is `a`), split on
`_` into components (`f_i` is "fi"), and `uniXXXX` carries any number
of four-digit values (`uni00660069` is "fi"). Names are resolved whole
or not at all, so half a ligature is never returned. `glyph_text`
returns a String for this; `glyph_unicode` stays as the single-character
view. Hex parsing rejects the leading `+` that `from_str_radix` would
otherwise accept, and a lone surrogate half.
Both halves were verified by reverting each fix and watching the
matching tests fail. The parity note on protect.redact-patterns now
says Symbol-encoded text is skipped rather than mis-decoded.
Still open, and an owner decision: Symbol and ZapfDingbats could be
mapped properly with a glyph-name-to-Unicode table, but the vendored
Adobe Glyph List is not on the AGENTS.md 1.1 closed list.
Co-Authored-By: Claude <[email protected]>
`Family::width` chose its standard-14 face from `bold` alone and passed
`italic: false`, while `Family::base_font` writes the item out as the
italic or oblique face. Every italic item was therefore laid out with
upright metrics: Times-Italic `A` is 611 against Times-Roman's 722, so
boxes, wrapping and centring were all sized from the wrong face.
The measure now takes `italic` and the five call sites pass the item's
own flag, which they already had in scope. The test walks all twelve
style combinations and asserts the written face and the measured face
are the same one, so the two can't drift apart again.
Co-Authored-By: Claude <[email protected]>
Isolated so it can be reverted on its own.
AGENTS.md 1.1 keeps a closed list of Adobe-authored non-visual technical data,
and adding to it needs the owner's explicit approval, which was given on
2026-10-08. The item itself -- the standard-14 font metrics and encoding tables
from the PDF specification -- was already on that list; what is new is holding
a copy first-party rather than only through hayro, which is what lets generated
appearances measure with it and what survives hayro being retired in M2.8.
The gate only understood the list for dependency-bundled files, and rejected
adobe_data on a repository file outright. It now accepts it for a closed list
of first-party paths, so crates/fonts/src/std14.rs carries an ATTRIBUTION entry
with its SHA-256: regenerating the table fails the gate until the entry is
updated, which makes a change to Adobe-derived data a deliberate act rather
than an ordinary edit.
Worth noting that .rs is not an asset extension, so the table would never have
been scanned at all. Entering it on the list is a choice to be checked, not a
requirement the gate imposed.
Co-Authored-By: Claude Opus 5 <[email protected]>
pdfcraft_fonts::helvetica_width was a fifteen-line guess by character class,
and seven crates generated appearance streams through it: annot, edit, forms,
ocr, redact, sign and xfa. Times was Helvetica x 0.9, bold was x 1.05, Courier
a flat 0.6. Against the published metrics a Times capital A is 722, so that
estimate said 600 -- a 17% error on a letter, compounding across a line.
Std14 carries the real advance widths for all fourteen faces, indexed by
character code: WinAnsiEncoding for the twelve text fonts, which is what we
write into the appearance streams we generate, and their own built-in
encodings for Symbol and ZapfDingbats. The tables are generated by joining the
published encodings with the per-glyph widths.
helvetica_width now delegates, so every call site gained exact Helvetica with
no change. The three places that faked another face by scaling it -- Family
in edit::added, the paragraph measure in edit::text, and the redaction overlay
fonts -- now select the right face instead. So does Metrics::width in
fonts::pdf, which measures existing document text when a font carries no
/Widths; that path feeds redaction's glyph removal, where a wrong width means
cutting the wrong glyphs.
Tests spot-check the table against values typed in from the specification by
hand, so it is verified against the spec rather than against whatever produced
it; plus Courier being fixed pitch everywhere, full printable-ASCII and Latin-1
coverage, Symbol and ZapfDingbats not being WinAnsi, /BaseFont aliases and
subset prefixes resolving, and hostile text staying finite.
Co-Authored-By: Claude Opus 5 <[email protected]>
Replaces this PR's earlier contents. It previously added a committed
`docs/plan/` tree -- a status baseline, an architecture doc and an
execution plan. Since then `main` has grown `docs/gaps.md`,
`docs/target-app-parity.md`, `docs/roadmap.md` and `docs/architecture.md`,
which are actively maintained (gaps.md was reviewed 2026-10-10 and
updated 2026-10-11) and cover the same ground better, because they sit in
the flow contributors already touch. Keeping a parallel plan would have
meant two sources of truth and a document that rots; it had already gone
conflicting twice in three days. So the plan is dropped and only the
parts with no equivalent anywhere remain.
`.github/pull_request_template.md` is new -- the repository had none,
with 167 PRs open and 66 of them conflicting. It leads with the two
things a contributor cannot be expected to know and that cannot be undone
after a merge: provenance (no Adobe asset, no committed font, no copied
GPL/AGPL code, behaviour from the spec rather than from reading another
implementation) and the never-crash rules. It also asks for a test that
has been *seen to fail* before the fix, and says plainly that per-crate
verification is preferred because CI runs the workspace anyway.
CLAUDE.md gains three things it did not record: the four-zoom review
habit (clause, function, file, project) with the tests that mirror it;
the external-PR bar; and a note that builds and tests should be scoped
per crate, because a full workspace build has locked up a development
machine and CI covers it. The commit rule now also says that `main` moves
fast enough for PRs to rot -- 396 commits in three days -- and that
conflict risk tracks how contested the files are rather than how large
the change is.
Co-Authored-By: Claude <[email protected]>
The previous release build failed in craft-common-actions, not in this
repo: riscv64 packaging got --formats deb,rpm,tar (fixed in
craft-common-actions#3), Linux jobs lacked the ALSA headers
(craft-common-actions#5), and Windows signing needed the AZURE_* secrets
listed in the caller.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since the release workflow moved to storytold/craft-common-actions, it has
passed only the Apple and WINDOWS_CERTIFICATE secrets. The signing job reads
values from the release environment, but only for names the caller lists, so
the AZURE_* names that Trusted Signing uses came through empty and Windows
builds would ship unsigned. Same fix as storytold/craft-launcher#174.
Co-authored-by: Claude Opus 5.5 <[email protected]>
An unchanged classic-table file used to append xref immediately followed by trailer, which qpdf treats as a damaged file. A zero-length subsection keeps trailer-only updates without rewriting any object entry.
Co-authored-by: Brandon Thomas <[email protected]>
* M14: add Lithuanian interface localization
Register Lithuanian UI and date languages with three integer plural forms
and a 2,572-entry contributor-original catalog. Cover dynamic dialog labels,
contextual calendar and certificate wording, commands, tools, and every key
in the other bundled catalogs. Add real-shell calendar, inspector and
password-dialog regressions; preserve PDF values and document state.
Update localization documentation and asset attribution. Native-speaker/VLKK
terminology review remains outstanding.
Validation: cargo xtask ci passed; cargo-deny was skipped because it is not
installed. Five rendered dialogs and contributor manual testing checked UI.
Co-Authored-By: OpenAI gpt-6.1-sol <[email protected]>
* M14: preserve multilingual localization documentation
Restore the existing language descriptions instead of replacing general
localization guidance with Lithuanian-specific coverage. List every
registered language and control-channel code; keep Lithuanian details
alongside the existing catalog descriptions.
Include the previously omitted Kazakh, Polish, Slovenian, Greek and Turkish
catalogs in the shipped-language table without changing the historical
coverage snapshot or existing assessments. Keep Lithuanian roadmap notes
concise and additive.
Validation: all 22 registered languages appear in documentation; every
existing descriptive paragraph is retained. cargo xtask ci passed, with
cargo-deny skipped because it is not installed.
Co-Authored-By: OpenAI gpt-6.1-sol <[email protected]>
* M14: record completed Lithuanian native-speaker review
The contributor confirms they are a native Lithuanian speaker and have
reviewed the translation. Remove obsolete pending-review labels from the
asset manifest and documentation; regenerate ATTRIBUTION.md. Remove the
redundant generated-message disclaimer from the Lithuanian description,
keeping the existing general localization documentation unchanged.
Validation: cargo xtask ci passed; cargo-deny was skipped because it is
not installed. Catalog content and its SHA-256 are unchanged.
Co-Authored-By: OpenAI gpt-6.1-sol <[email protected]>
* Lithuanian: cover main's Copy Image labels, list lt last, check its glyphs
Adds the six Copy Image rows from #1363, moves lt to the end of LANGUAGES
and DATE_LANGUAGES (after tr), adds lt.tsv to the primary-font coverage
test, writes the JavaScript preferences path with '>' as the other
catalogs do (Inter has no ▸), and refreshes counts and hashes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: OpenAI gpt-6.1-sol <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* feat(preflight): report image resolution and bleed without rewriting the file
* feat(preflight): report image resolution and bleed without rewriting the file
* feat(preflight): report image resolution and bleed without rewriting the file
* feat(preflight): report image resolution and bleed without rewriting the file
* feat(preflight): report image resolution and bleed without rewriting the file
* feat(preflight): report image resolution and bleed without rewriting the file
* feat(preflight): report image resolution and bleed without rewriting the file
* feat(preflight): report image resolution and bleed without rewriting the file
* feat(preflight): report image resolution and bleed without rewriting the file
* feat(preflight): report image resolution and bleed without rewriting the file
* feat(preflight): report image resolution and bleed without rewriting the file
* feat(preflight): report image resolution and bleed without rewriting the file
* test upload session tests only
* Include the print-readiness module from the OCR module so the crate compiles.
* Record the one-line print-readiness patch so the dispatch can be restored exactly.
* Restore the print-readiness dispatch from the original sources.
* M3: select an image on the page and copy it (#1363)
* M3: select an image on the page and copy it
Select tool: a click on a raster image selects it (also one drawn through Form artwork, or with
text over it such as an OCR layer); ⌘C copies it as a file in its original format (a JPEG as
stored in the PDF, otherwise a lossless PNG). Right-click ▸ Copy Image copies its pixels at full
resolution, and Copy Image in Original Format the file; Edit text & images has both for the
selected image. Both respect the document's copy permission.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWUuTDPKL5wivnSJuCkC8Q
* M3: translate the copy-image strings into Czech, Kazakh, Slovene and Turkish
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GWUuTDPKL5wivnSJuCkC8Q
* Copy image: keep the clipboard file in a private per-user folder
Copy Image in Original Format wrote into the shared temporary folder
(/tmp/pdfcraft-clipboard) and deleted everything in it first, so another
user on the machine could plant a link or folder there and have PdfCraft
delete or overwrite their target. The file now goes in PdfCraft's
per-user cache folder (or the portable data folder), which is checked to
be a real folder and set to 0700 on Unix (refused when it isn't ours).
Only earlier copies (regular files named '<stem> p<N> img<M>.jpg|png')
are removed, links are never followed, and the new file is created with
create_new so it can't be redirected.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Tests: escape Windows paths in synthetic GoToR file specifications
canonicalize() returns \\?\C:\... on Windows; unescaped backslashes in a PDF literal string become control characters, so link_open refused the target and main's Windows CI failed. Taken from #1359's helper fix.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* fix(cli): edit refuses move/insert destinations past the end (#1330) (#1400)
Co-authored-by: umesh429 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix(sign): refuse to sign a field whose /V is not a signature (#1328) (#1402)
Follow-up to #1374. Signing an existing field refused a field whose /V
is a signature dictionary and signed one whose /V is missing or null,
but a /V that is neither (an empty string, a number, a name, an array)
was treated as unsigned and overwritten with the new signature.
Signing such a field by name now fails with "the value of the field
... is not a signature" and leaves the document as it is. A missing or
null /V, direct or indirect, still signs, and a signed field is still
refused as already signed. The signature list is unchanged: it still
shows such a field as unsigned, since it carries no signature.
Adds a_field_whose_value_is_not_a_signature_is_refused_by_name, which
fails before this change.
* fix(forms): keep a whitespace-only value in a text field without a format (#1323) (#1403)
The commit-time Keystroke check returned an empty value for any
whitespace-only text before looking at the field's format, so setting a
space, a tab or a non-breaking space in a plain text field reported
success and removed the field's value. Text and XFDF imports go through
the same setter and lost the value the same way.
Blank input now clears only a field with a format (number, percent,
date, time, special or mask), as before; a field without one stores the
text as given. An empty value still clears any field.
Adds tests::whitespace_only_text_is_kept_in_a_field_without_a_format
and a keystroke assertion in af::tests::numbers_format_like_acrobat,
both of which fail before this change.
* fix(content): skip an inline image with an unreadable key to its EI (#1318) (#1404)
When a key in an inline image dictionary was not a name (`BI /W 4 H 4
ID ... EI`), the parser ended the image at the end of the stream and
counted nothing as skipped, so every operator after the image was lost:
text_lines found no text on a page whose text follows such an image.
An image whose parameters can't be read is now skipped up to the EI
that ends it, the skipped bytes are counted, and parsing resumes after
it. Without an EI the image still runs to the end of the stream, as
before.
Adds tests::an_unreadable_inline_image_is_skipped_to_its_end, which
fails before this change.
* fix(automation): doc_save reports a required full rewrite as not incremental (#1329) (#1405)
doc_save reported "incremental": !full, from the arguments alone. When
an in-place save had to be a full rewrite (after doc_protect adds or
removes encryption, or after Sanitize), write_incremental fell back to
write_full, but the result still said the save was incremental.
The result now says whether the written file starts with the file it
was saved over, read from the new Document::saved_bytes. An ordinary
in-place save still reports true and an explicit full save false.
Adds an_in_place_save_reports_a_required_rewrite_as_not_incremental,
which fails before this change for both doc_protect and
doc_remove_hidden.
* fix(audit): an identity cm between re and fill no longer hides a cover (#1333) (#1410)
Co-authored-by: umesh429 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* fix(ui): Current page prints the viewed page, not a page labelled with its number (#1308) (#1411)
The Print dialog turned Current page into the range "N" for the page on
screen, and ranges read numbers as page labels first. In a document
labelled i, ii, iii, 1, 2, viewing the first page printed the fourth,
whose label is "1".
Current page is now passed as a position, the way Selected pages is,
through print::select_listed. A typed range still reads labels first.
Adds print_ui::tests::current_page_is_the_viewed_page_not_a_label,
which fails before this change.
* fix(render): draw Turkish Ğ İ Ş ğ ş in non-embedded fonts (vendored hayro-interpret patch 26) (#1409)
A PDF whose fonts aren't embedded is drawn with PDFium's Foxit stand-ins
for the standard 14 fonts. Those carry only the 230-odd glyphs of the
Standard, WinAnsi and Mac Roman encodings, so a letter a font names beyond
them in its /Differences mapped to .notdef and drew nothing. iText 2
writes Turkish exactly this way (Times New Roman left unembedded,
/Gbreve /Idotaccent /Scedilla /gbreve /scedilla over Ð Ý Þ ð þ), and such
letters showed blanks for Ğ, İ, Ş, ğ and ş while ı, ç, ö and ü drew.
StandardKind now builds a Latin Extended-A letter the stand-in lacks from
the base letter and accent it does have, as Type 1 seac composites and
the standard-14 AFM composites are built: the accent is centred on the
letter, raised over capitals and letters with ascenders (and moved along
the slant in italics), cedillas hang below and ogoneks from the right
edge. Composite glyph ids sit above any font's glyph count, one per
character code; their width comes from the standard metrics of the name.
Test: raster::tests::accented_letters_the_standard_stand_ins_lack_are_built_from_their_parts
(fails without the patch: Gbreve drew no pixel).
Co-authored-by: Claude Opus 5.5 <[email protected]>
* M10: edit OCR text on scanned pages so the change shows (#1397)
* M10: edit OCR text on scanned pages so the change shows
Editing a recognised word on a scan rewrote it as invisible OCR text, so
the page still showed the old word, and the whole OCR layer grouped into
one paragraph that rewrapped when one word changed.
- An edited invisible paragraph is drawn visibly, over a cover in the
paper colour sampled around it, in the scan's sampled ink colour
(or the chosen colour).
- OCR words group into phrases: a table cell or a tab stop splits them
and each scanned line stays apart; a phrase grows rightwards.
- Styled rewrites measure with the text matrix's horizontal scale, so
stretched OCR words keep their widths instead of overlapping.
- The inline editor covers the scanned word and grows like the result.
* M10: size OCR text from each word's letters
A word box from recognition is tight around the ink, but the text layer
assumed every box spans Helvetica's ascender-descender height. A
capitals-only word such as "DT" spans only the cap height, so its text
was set about 23% too small and its baseline floated up; editing it
then drew the new word visibly smaller than the scanned one.
The em and baseline now come from the word's letters: cap height or
x-height on top, descender or baseline at the bottom. On a real scanned
form, a rewritten "DT" -> "DSI" now matches the scanned word's height
and baseline. The engine OCR-edit tests build their words the way a scan
would box them.
* Edit OCR text: sample colours only for invisible blocks, from the current bytes
Session::apply looked up every edited block with a fresh text_blocks pass and
re-serialized the whole document to render the sampling tile. Text blocks are
now cached per page on the editor (keyed on the unmodified document's source
identity, so any edit, undo or save invalidates them), visible blocks skip
sampling entirely, and the tile renders from the document's current bytes.
Adds a visible stretched-text regression test and documents that editing any
invisible text makes it visible over a painted cover.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Edit OCR text: name the cached text-blocks tuple (clippy), rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Edit text: support per-font visual-order Indic text and nukta reordering (#1380)
* Edit text: support per-font visual-order Indic text and nukta reordering
- Detect legacy visual-order Indic fonts via Metrics.visual_order (fonts with
Indic codepoints without OpenType GSUB tables, or legacy visual font names).
- Implement Indic visual-to-logical and logical-to-visual pre-base vowel
reordering in crates/edit/src/indic.rs, with support for decomposed/precomposed
nuktas and conjuncts.
- Apply reordering during extraction and source font reuse only when visual_order
is set, leaving modern logical-order text untouched.
- Add unit tests for nuktas, conjuncts, and round-trips in crates/edit/src/indic.rs
and font visual_order tests in crates/edit/src/tests.rs.
* Visual-order Indic: positive evidence only, ActualText kept logical, no font decoding
- A font is visual order only when it is a known legacy Indic family (subset tag
allowed; aps-/dv- as prefixes) with an Indic Unicode mapping. A missing GSUB is
no evidence (bare CFF and subset fonts drop it), so Metrics::from_dict no longer
decodes every embedded font program on every call.
- Lines with /ActualText text are already logical: skip indic::to_logical, so
"बढ़िया" no longer becomes "बढ़यिा".
- reading_lines no longer runs bidi recovery on every line (it changed RTL
reading_blocks output for all documents); only the Indic recovery remains.
- indic.rs indexes through get().
- Tests use a ToUnicode fixture (not visual-order ActualText), cover an edit
round-trip, logical ActualText in a legacy font, and a subset Noto font.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* M12: preview text structure proposals for auto-tagging (#1383)
* M12: preview text structure proposals for auto-tagging
* Auto-tag preview: no unregistered command, allowed on copy-restricted files like a11y.check
The preview is read-only with no UI command yet, like render_preview, so it no
longer names the Planned a11y.autotag catalog id (tool_table_is_well_formed).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* M12: preview text structure proposals for auto-tagging
* Keep read-only auto-tag preview separate from unavailable command
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* M10: Test current-page print previews and saved output (#1415)
Extend the coverage of #1411 with preview raster selection and a dialog-driven PDF save test. Reopen and render the saved PDF to verify the viewed physical page when labels restart. Cover duplicate labels and selection guards without changing production code.
Refs #1308, #1411
Co-authored-by: LabDennis <[email protected]>
* fix(content): count an unreadable inline image with no EI as skipped (#1318) (#1418)
Follow-up to #1404. An inline image whose parameters can't be read and that has no EI after them ran to the end of the stream as a BI op with no data, and those bytes were not counted. It is now skipped like one with an EI, its bytes counted in Parsed::skipped. A stream that ends inside the parameters (BI /W 1) still keeps its BI op.
* feat(ui): add command palette arrow-key navigation (#1419)
* M4: Compare and cache actual Combine file duplicates (#1420)
Different equal-sized PDFs with the same name no longer receive duplicate warnings. Share exact-content detection between Grid and List, and cache the result using the existing list revision to remove the per-redraw all-pairs scan.
Cover distinct contents, actual duplicates, source parts, cache reuse, removal, undo, redo and closing the tab. Both UI regressions fail before the fix.
Fixes#1303
Co-authored-by: LabDennis <[email protected]>
* feat(preflight): verify image resolution and bleed without rewriting the file (#1408)
Squashed from the PR branch (its history added and removed a workflow file;
the final tree has no .github changes).
* printcheck: proper engine module, one TrimBox note, count unmeasurable images
Declares printcheck in engine/src/lib.rs instead of a #[path] module inside
ocr.rs. Pages without a TrimBox get one informational document-level
finding instead of a bleed warning per page; bleed is only measured
against a real TrimBox. Placements with a non-finite or degenerate matrix
are counted as resolution unknown (info finding, text line and JSON
field) rather than passing every check. Adds a print_readiness test
through Automation::call.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Francesco Clemeno <[email protected]>
Co-authored-by: Claude <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: umesh429 <[email protected]>
Co-authored-by: umesh429 <[email protected]>
Co-authored-by: Marco Arnulfo <[email protected]>
Co-authored-by: TheLastRoadRunner <[email protected]>
Co-authored-by: Tangu Achilis <[email protected]>
Co-authored-by: krinry <[email protected]>
Co-authored-by: Earl Co <[email protected]>
Co-authored-by: LabDennis <[email protected]>
Co-authored-by: LabDennis <[email protected]>
Co-authored-by: Pratham Bhanushali <[email protected]>
Different equal-sized PDFs with the same name no longer receive duplicate warnings. Share exact-content detection between Grid and List, and cache the result using the existing list revision to remove the per-redraw all-pairs scan.
Cover distinct contents, actual duplicates, source parts, cache reuse, removal, undo, redo and closing the tab. Both UI regressions fail before the fix.
Fixes#1303
Co-authored-by: LabDennis <[email protected]>
Follow-up to #1404. An inline image whose parameters can't be read and that has no EI after them ran to the end of the stream as a BI op with no data, and those bytes were not counted. It is now skipped like one with an EI, its bytes counted in Parsed::skipped. A stream that ends inside the parameters (BI /W 1) still keeps its BI op.
Extend the coverage of #1411 with preview raster selection and a dialog-driven PDF save test. Reopen and render the saved PDF to verify the viewed physical page when labels restart. Cover duplicate labels and selection guards without changing production code.
Refs #1308, #1411
Co-authored-by: LabDennis <[email protected]>
* M12: preview text structure proposals for auto-tagging
* Auto-tag preview: no unregistered command, allowed on copy-restricted files like a11y.check
The preview is read-only with no UI command yet, like render_preview, so it no
longer names the Planned a11y.autotag catalog id (tool_table_is_well_formed).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* M12: preview text structure proposals for auto-tagging
* Keep read-only auto-tag preview separate from unavailable command
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Edit text: support per-font visual-order Indic text and nukta reordering
- Detect legacy visual-order Indic fonts via Metrics.visual_order (fonts with
Indic codepoints without OpenType GSUB tables, or legacy visual font names).
- Implement Indic visual-to-logical and logical-to-visual pre-base vowel
reordering in crates/edit/src/indic.rs, with support for decomposed/precomposed
nuktas and conjuncts.
- Apply reordering during extraction and source font reuse only when visual_order
is set, leaving modern logical-order text untouched.
- Add unit tests for nuktas, conjuncts, and round-trips in crates/edit/src/indic.rs
and font visual_order tests in crates/edit/src/tests.rs.
* Visual-order Indic: positive evidence only, ActualText kept logical, no font decoding
- A font is visual order only when it is a known legacy Indic family (subset tag
allowed; aps-/dv- as prefixes) with an Indic Unicode mapping. A missing GSUB is
no evidence (bare CFF and subset fonts drop it), so Metrics::from_dict no longer
decodes every embedded font program on every call.
- Lines with /ActualText text are already logical: skip indic::to_logical, so
"बढ़िया" no longer becomes "बढ़यिा".
- reading_lines no longer runs bidi recovery on every line (it changed RTL
reading_blocks output for all documents); only the Indic recovery remains.
- indic.rs indexes through get().
- Tests use a ToUnicode fixture (not visual-order ActualText), cover an edit
round-trip, logical ActualText in a legacy font, and a subset Noto font.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* M10: edit OCR text on scanned pages so the change shows
Editing a recognised word on a scan rewrote it as invisible OCR text, so
the page still showed the old word, and the whole OCR layer grouped into
one paragraph that rewrapped when one word changed.
- An edited invisible paragraph is drawn visibly, over a cover in the
paper colour sampled around it, in the scan's sampled ink colour
(or the chosen colour).
- OCR words group into phrases: a table cell or a tab stop splits them
and each scanned line stays apart; a phrase grows rightwards.
- Styled rewrites measure with the text matrix's horizontal scale, so
stretched OCR words keep their widths instead of overlapping.
- The inline editor covers the scanned word and grows like the result.
* M10: size OCR text from each word's letters
A word box from recognition is tight around the ink, but the text layer
assumed every box spans Helvetica's ascender-descender height. A
capitals-only word such as "DT" spans only the cap height, so its text
was set about 23% too small and its baseline floated up; editing it
then drew the new word visibly smaller than the scanned one.
The em and baseline now come from the word's letters: cap height or
x-height on top, descender or baseline at the bottom. On a real scanned
form, a rewritten "DT" -> "DSI" now matches the scanned word's height
and baseline. The engine OCR-edit tests build their words the way a scan
would box them.
* Edit OCR text: sample colours only for invisible blocks, from the current bytes
Session::apply looked up every edited block with a fresh text_blocks pass and
re-serialized the whole document to render the sampling tile. Text blocks are
now cached per page on the editor (keyed on the unmodified document's source
identity, so any edit, undo or save invalidates them), visible blocks skip
sampling entirely, and the tile renders from the document's current bytes.
Adds a visible stretched-text regression test and documents that editing any
invisible text makes it visible over a painted cover.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Edit OCR text: name the cached text-blocks tuple (clippy), rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
A PDF whose fonts aren't embedded is drawn with PDFium's Foxit stand-ins
for the standard 14 fonts. Those carry only the 230-odd glyphs of the
Standard, WinAnsi and Mac Roman encodings, so a letter a font names beyond
them in its /Differences mapped to .notdef and drew nothing. iText 2
writes Turkish exactly this way (Times New Roman left unembedded,
/Gbreve /Idotaccent /Scedilla /gbreve /scedilla over Ð Ý Þ ð þ), and such
letters showed blanks for Ğ, İ, Ş, ğ and ş while ı, ç, ö and ü drew.
StandardKind now builds a Latin Extended-A letter the stand-in lacks from
the base letter and accent it does have, as Type 1 seac composites and
the standard-14 AFM composites are built: the accent is centred on the
letter, raised over capitals and letters with ascenders (and moved along
the slant in italics), cedillas hang below and ogoneks from the right
edge. Composite glyph ids sit above any font's glyph count, one per
character code; their width comes from the standard metrics of the name.
Test: raster::tests::accented_letters_the_standard_stand_ins_lack_are_built_from_their_parts
(fails without the patch: Gbreve drew no pixel).
Co-authored-by: Claude Opus 5.5 <[email protected]>
The Print dialog turned Current page into the range "N" for the page on
screen, and ranges read numbers as page labels first. In a document
labelled i, ii, iii, 1, 2, viewing the first page printed the fourth,
whose label is "1".
Current page is now passed as a position, the way Selected pages is,
through print::select_listed. A typed range still reads labels first.
Adds print_ui::tests::current_page_is_the_viewed_page_not_a_label,
which fails before this change.
doc_save reported "incremental": !full, from the arguments alone. When
an in-place save had to be a full rewrite (after doc_protect adds or
removes encryption, or after Sanitize), write_incremental fell back to
write_full, but the result still said the save was incremental.
The result now says whether the written file starts with the file it
was saved over, read from the new Document::saved_bytes. An ordinary
in-place save still reports true and an explicit full save false.
Adds an_in_place_save_reports_a_required_rewrite_as_not_incremental,
which fails before this change for both doc_protect and
doc_remove_hidden.