* M7: grow text boxes and edit existing text
Text boxes now grow to the page edge and vertically as needed, retaining their size when saved. Clicking native or added text in either text tool edits the existing item instead of layering a duplicate over it.\n\nCo-authored-by: OpenAI gpt-6.1-sol <noreply@openai.com>
* #1539: build against main's Document::added() accessor and draft-aware prepare
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>
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>
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>
* 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 <noreply@anthropic.com>
* Edit OCR text: name the cached text-blocks tuple (clippy), rustfmt
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>
* M7: preserve Arabic text order when copying and editing
Record per-line ActualText for shaped added text and preserve its logical order in Edit text. Cover mixed scripts, marks, copy/search, retyping and save/reopen; retain the drawing golden.
Assisted-by: Codex
* Edit text: keep only right-to-left ActualText out of line joining and bidi recovery
Word and Chrome wrap Latin ligatures in /ActualText spans mid-word; splitting
lines at every span broke such words into pieces in Edit Text. Only spans
whose ActualText has strong right-to-left characters keep their own line and
typing order now. Regression: a Latin fi ligature span stays on its line.
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>
* M0: remove upstream trademark artwork from contributor fork
Use the existing document icon and a plain project label. Remove trademark
asset entries and regenerate attribution as required for modified forks.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: group compatible PDF words and hide source text during editing
Join compatible words across text objects and subset-font spaces, recognize
component colours, and reuse subset fonts with numeric space positioning.
Render a read-only background with the selected glyphs removed while the
inline editor is open. Keep one frame and source line breaks, and reset
the layout width when switching paragraphs. Expose the same preview through page_render.
Add synthetic extraction, save/reopen, preview and UI regressions. Record
the remaining mixed-style paragraph and installed-font limitations.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: preserve rich PDF regions and preview text edits in place
Group mixed source runs into complete visual regions, retain run styles and source encodings, and use validated embedded source faces in the inline editor. Preserve source baselines and show a single live frame as text grows or shrinks, backed by a read-only draft geometry tool.
Validation: strict workspace Clippy, workspace tests, all 30 WASM crates, asset and parity gates, licensed synthetic edit/save/reopen and desktop checks, and an opt-in four-page private-document regression.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: keep cropped artwork from blocking text editing
Track page/path clipping separately from full image placement, use visible selection bounds, and prioritize text hit testing over overlapping artwork. Preserve fixed clips during image transformations and expose visible bounds through the headless tools.
Validation: strict workspace Clippy, workspace tests, 30 WASM crates, asset/parity gates, synthetic engine/tool/UI and native desktop checks, both private-document UI regressions, and all-page local edit/save/reopen/undo on a four-page cropped-artwork PDF.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: resize cropped images with their clipping paths
Keep the visible frame and image pixels under the same transform. Preserve the
opposite corner and respond to diagonal, horizontal-only and vertical-only
corner drags without jumping from an off-centre grip. Refuse shared clipping
scopes that would move unrelated content.
Add synthetic engine/tool/UI regressions and opt-in private-file coverage
for 80 corner gestures and 10 grow/shrink save/reopen/undo render checks.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: complete source font subsets and preserve clipped editing
Add explicit document fonts, CFF embedding, range formatting and bounded contextual shaping; isolate shared image clips and verify private Chinese edits on all pages.
Co-authored-by: OpenAI Codex <codex@openai.com>
* Revert "M0: remove upstream trademark artwork from contributor fork"
This reverts commit 3aa539b5b6.
* Test: give the text layer up to 4 s before copying (it flaked on a loaded machine)
---------
Co-authored-by: Kamoliddin Fatkhullaev <komoliddinfatxullayev@MacBook-Pro-Komoliddin.local>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>