* M7: keep embedded CMap ranges compact instead of truncating expansion
A ToUnicode CMap's sequential bfrange used to expand into one HashMap
entry per code, and the parser stopped at 2^20 expanded codes. A valid
CMap with more codes than that lost every mapping past the cap.
Each sequential range is now one record. Codes below 65 536 find their
covering record in a table, higher codes by binary search over a cover
index; the later declaration wins where ranges and explicit entries
overlap. Explicit entries keep their strings. The budget counts an
explicit entry as one and a range as two, since a range covers at least
two codes, so every CMap with at most 2^20 codes is kept whole. encode
finds the lowest code per character from the same records.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
* ToUnicode: size the low-code tables to the codes a font covers
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Export to Word/HTML/RTF: read text and images drawn by form XObjects (fixes#314)
The office export built its paragraphs from text_blocks and its images from
page_images. Both read only the page's own content streams (they back Edit
text and Edit image, which rewrite those streams), so a page drawn through
`/Fm1 Do`, as many print-to-PDF tools emit, exported as an empty document.
Add read-only reading_blocks and reading_images to pdfcraft-edit. They
follow Do into form XObjects (nested ones too), apply each form's /Matrix,
use its own /Resources (or the caller's when it has none), and start it
from the graphics state at the Do. Depth is capped at MAX_FORM_DEPTH and a
form already being read is skipped, so self-referencing and mutually
recursive forms are read once. export_pages uses them; editing is
unchanged and still offers only the page's own text and images.
* Export: a page's walk through form XObjects has a total visit budget, so forms that fan out can't hang it
* Export: keep the image walk within clippy's argument limit
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A page's /Contents array is one content stream in pieces that writers may
split between any two tokens (ISO 32000-2 §7.8.2). Edit text parsed each
piece on its own, so an operator whose operands ended one piece and whose
keyword started the next arrived with no operands: a `[(After) -20 (wards)]`
/ `TJ` split made the line vanish from text_lines, text_paragraphs and
text_edit, and from the Edit text tool.
The joined-parse logic the redaction interpreter used (#153) moves into
pdfcraft-content as `Pieces`: join the pieces, parse them as one stream,
map positions back to pieces, and splice joined byte ranges back into
per-piece bytes (an edit replacing or removing an operator that spans
pieces rewrites every piece it touches; untouched pieces keep their
objects). text_lines now interprets the joined operators, and replace_line
and rewrite_block splice through `Pieces`. Redaction uses the same helper.
Lines and paragraphs are grouped by the piece their first operator starts
in, as before, so pages whose pieces are separate drawings don't merge.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Replacement text that the line's own (subset) font can't show and WinAnsi can't either goes
to the Type 3 fallback. For serif sources that was always Shippori Mincho, which has no
Cyrillic or Greek, so e.g. retyping a Bulgarian word in a Garamond line failed with
"Japanese fallback font has no glyph for U+0421". BIZ UDMincho and BIZ UDPGothic have the
letters. document_font_for_text keeps the style's face when it shows every character and
otherwise picks the closest Japanese face that does (Mincho first for serif, then weight).
Those faces draw Cyrillic and Greek full-width (1 em, JIS X 0208) with no palt metrics, so
fallback glyphs in those blocks are respaced to their ink width plus a 0.05 em side bearing.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
type3_path wrote `wx 0 0 0 0 1000 1000 d1`: seven operands where d1 takes six
(wx wy llx lly urx ury). Lenient renderers (pdfium, Quartz, MuPDF, poppler) ignore the
extra one, but Acrobat treats the glyph as unusable and draws a bullet for every
character of edited Japanese (or other fallback) text. The box is now the glyph's own
bounds, rounded outward; empty glyphs (spaces) get a zero box.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
pdfcraft_fonts::win_ansi, edit's is_win_ansi_char and the xtask demo
encoder each carried a hand-written subset of WinAnsiEncoding's 0x80-0x9F
row (quotes, dashes, euro, trademark) and left out Š š Ž ž Œ œ Ÿ ƒ † ‡ ˆ
˜ ‰ ‹ ›. Text editing therefore treated š/ž as non-WinAnsi and sent them
to the Type3 fallback font (or refused them without craft-fonts), and
every other generator (forms, annotations, OCR layer, redaction overlay,
signatures, create) wrote them as '?'.
win_ansi now goes through a new win_ansi_byte, which reads the 0x80-0x9F
codes from the same ISO 32000-2 Annex D table the text extractor decodes
with, and is_win_ansi_char reuses it. xtask doesn't depend on
pdfcraft-fonts, so its demo encoder gets the full row with a pointer to
the shared table. č/ć are not in WinAnsi and still use the fallback.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Replacing text in a serif line (Times New Roman, say) with Cyrillic failed
with "Japanese fallback font has no glyph for U+0444". The Type 3 fallback
took only the style's first face, Shippori Mincho, which has no Cyrillic,
although BIZ UDMincho and BIZ UDPGothic ship alongside it.
`document_japanese_fonts_for_style` lists every Japanese face in the
existing order of preference, and `type3_font` uses the first one that has
a glyph for every character. If none has them all, the preferred face still
names the missing character, as before.
* M7: select real Japanese fallback faces by family and weight
Refs #184. Keep Type 3 font-name reporting separate in #387; missing weights and the small web font input remain explicit limitations.
Assisted-by: OpenAI GPT-6.1 (gpt-6.1-sol)
* Test the Japanese fallback name for a sans source: the Gothic face since #394 matches the source style
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A page's /Contents array is one content stream in pieces, and a writer
may split it between any two tokens (ISO 32000-2 §7.8.2): a TJ array
can end one piece with its operator starting the next, and a
marked-content dictionary can close at the start of the next piece.
Replacing a line or rewriting a paragraph re-serialized the parsed
operators of the line's piece, and the parser keeps neither operands
left at the end of a piece nor a dictionary's closing >> at its start.
The edit dropped them: the text after the edited line vanished, and the
page was left with an operator without operands (TJ, BDC), which other
readers reject.
The piece is now rewritten by splicing: only the replaced, inserted and
removed operators change, and every other byte is copied as it was.
Co-authored-by: EthanSuquet <292885762+EthanSuquet@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A page's /Contents array is one content stream in pieces (ISO 32000-2
§7.8.2), so a cm, a q that is still open, the font and the colour set in
one piece still apply in the next. Edit text read every piece with a
fresh state. AutoCAD scales the whole page in its first stream
(0.12 0 0 0.12 0 0 cm) and draws in the following ones, so the text
boxes of its plans landed far off the page and no label could be edited.
Co-authored-by: Oliver Schiemann <228913879+Checkov23@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now
PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the
PrintCraft settings and crash-recovery folders to the new name, and both apps read settings
saved under the old storage key, so upgrades keep recent files, preferences and unsaved work.
The signing test fixtures keep their PrintCraft subject names (they are binary).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Shippori Mincho (and its OFL file) leaves assets/fonts/: the Japanese fonts now
live in storytold/craft-fonts. printcraft-fonts gets craft-fonts' build.rs recipe:
with CRAFT_FONTS_DIR=<checkout> it embeds the manifest's fonts as CRAFT_FONTS
(wasm32: only BIZ UDPGothic Regular); without it CRAFT_FONTS is empty and the
app builds, tests and runs as before, minus the Japanese face.
- SHIPPORI_MINCHO is now Option<&[u8]> from CRAFT_FONTS; japanese_glyph
(was shippori_glyph) uses document_japanese_font() (Shippori Mincho, then
BIZ UDMincho, then any Jpan face) and returns GlyphError::NoFont without one.
- Editing PDF text into Japanese without craft-fonts is a clear EditError that
leaves the page untouched.
- UI: every Jpan craft font (BIZ UDPGothic first) is the last fallback of
every egui family, sharing the same static bytes.
- Tests: Japanese glyph/edit tests skip without craft-fonts; new UI tests for
no-tofu Japanese with craft-fonts and working fonts without it.
- CI: the Linux test job checks out craft-fonts at a pinned SHA and runs the
tests with CRAFT_FONTS_DIR. Releases build every platform with it
(CRAFT_FONTS_REQUIRED=1) and ship each font's OFL as OFL-<family>.txt.
- ATTRIBUTION.toml gains [[build_input]] (checked by xtask assets); NOTICE,
AGENTS.md, CLAUDE.md, README and docs say fonts live in craft-fonts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In Edit a PDF ▸ Edit text, dragging a paragraph's box moves it, and dragging
the handle on its right edge rewraps it to a new width; a click still opens
it for typing.
- edit: BlockStyle gains offset (user space) and width. rewrite_block puts the
move inside the paragraph's own q … Q as a cm, taken back through the CTM's
linear part so a page-space drag lands right in scaled/rotated content
(the CTM is now recorded at each line's origin). Non-finite numbers are
refused; widths are clamped to one character .. 14 400 pt.
- ui: BlockDrag on DocView; the box follows the pointer (or its right edge
does) and the release applies one undoable Edit text step. The width handle
shows on upright pages only (on a quarter-turned page the screen's
horizontal is the text's vertical).
- automation: text_edit takes dx, dy and width.
- Tests: engine (move, move under a scaling cm, narrower/wider rewrap,
non-finite and zero inputs), kittest pointer drags, automation tool.
Parity: new edit.text-move-resize (shipped).
Requested by @TCB13.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Comments: a text box's rectangle follows its edited text
Editing a text box (or typewriter or callout) rewrote /Contents but
kept the old /Rect: the appearance clipped the new text to the old box
and silently dropped lines that no longer fit, while shorter text left
an oversized box. set_contents now re-measures the text with the same
Helvetica widths as at creation and re-fits the box: the wrap width and
top edge stay, the height fits the wrapped lines. A callout's leader
line keeps its place and re-attaches to the moved box (that geometry
moved from set_rect into a shared apply_text_box). A locked box keeps
its text editable, so it re-fits too.
Covered by annot tests (grow and shrink with nothing clipped, callout
leader re-attach, locked box) and a comment_edit round trip through the
automation tools; parity entries cite them.
Co-Authored-By: GLM <noreply@z.ai>
* Polish edit text and images tool
* feat(edit-text): select source font style
* fix(edit-text): preserve detected PDF font styles
* feat(edit-text): support Japanese replacements
* fix(forms): escape non-ASCII format script strings
* Forms: unwrap_or instead of map_or with identity (clippy map_or_identity)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: GLM <noreply@z.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- edit: the substitute font name is a `&str` constant, so there's no `from_utf8(..).expect`.
Paragraph grouping uses `let … else` / `if let` chains, and the reuse-font path takes the
metrics with `filter` instead of `expect("checked")`.
- xfdf: a comment key that vanished between listing and lookup is skipped.
Both crates now deny the panic lints. Adds the root clippy.toml (identical to #11) so tests may
still unwrap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Edit text: a rewritten paragraph keeps its place in the text object it shares
A rewritten paragraph was drawn again just before the BT of its text object. When earlier
paragraphs shared that BT … ET (as in text-only pages), the paragraph moved ahead of them in
the content: reading order changed (screen readers, copy, `pdftotext -raw`) and text_paragraphs
renumbered, so after text_edit paragraph 2 a following text_edit paragraph 1 rewrote the
already edited paragraph instead of the first one. The text object is now split where the
paragraph was, and the second half resumes from the same line matrix and text state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Edit text: read the text before a paragraph without slicing by input positions (never-panic rule)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Edit text: test a recoloured paragraph in a shared text object (split + q/Q together)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Rewriting a paragraph (text_edit, Format text) draws it in its own text object, then sets the
text state back for what follows, but not the fill colour. Changing one paragraph's colour
therefore also coloured every paragraph after it in the same content stream (recolour the
first paragraph of a page red and the rest of the page turns red). The rewritten paragraph is
now wrapped in q … Q, so its colour and other graphics state stay with it.
Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>