Files
pdfcraft/docs/editing-existing-text.md
T
bflatastic ae7a21d2b0 Edit grouped text with one click instead of preparing pages on sight
Edit text queued Prepare grouped text for every page that came into view, after a full
preparation on a copy of the document on the UI thread to see whether it could. Scrolling cost
that work per page, marked the document changed, added undo steps and inlined shared forms
although nothing was edited.

Now nothing happens while pages are shown. A click (press and release, no drag) on grouped
artwork, routed to the picture by the page router as before, asks once whether that artwork
shows text preparation would open (text::forms::grouped_text_at, which reads the page without
copying or changing it). If it does, the page is prepared as one undoable step and the
paragraph at the point clicked opens for typing. A logo or a group preparation would refuse is
selected as before, with no message and no change. A drag still moves the selected group, and
while a paragraph is open or an edit is waiting the click selects as before. text_prepare is
unchanged.

The docs and the parity entry no longer say text is prepared on entering Edit text.
2026-11-08 22:31:14 +07:00

193 lines
15 KiB
Markdown

# Editing existing text and source subsets
In **Edit text and images**, text drawn inside a Form XObject (grouped artwork) is edited
with one click: clicking its text prepares that page in an undoable **Prepare grouped text**
step and opens the paragraph clicked for typing. Nothing is prepared while pages are only
shown or scrolled. A drag still moves the selected group as a whole; a click on grouped
artwork that preparation wouldn't open (a logo, a refused group) selects it as before, and
changes nothing. To select a text group for moving, right-click it. While a paragraph is open
or another edit is waiting, the click selects the group instead, as before; the next click
prepares it. The group's original operators, clip, placement and resources become page
content; shared forms on other pages stay unchanged. Ungroup (the artwork's right-click menu)
draws one placement in place with the same code. Headless callers use `text_prepare`, then
list `text_paragraphs` and call `text_edit`.
This edits existing glyph operators; it adds neither a cover rectangle nor an Add Text box.
Preparation refuses knockout/blended groups, groups that blend in a colour space other than
the page group's (DeviceRGB on a page that names none), forms whose default
colour spaces differ from the page's, tagged forms, forms drawn inside text objects or
unfinished paths, groups whose nested content can't be checked and unreadable content. Forms
that draw only images or vector artwork are left grouped. Decoding is capped at 16 MiB, 64
placements and eight expansion levels per page; text grouped deeper is reported. Unsupported
groups remain intact: `text_prepare` lists each with its reason, and the repair log records
them when the page is prepared. A narrowly fingerprinted legacy THSarabunPSK exporter map can
also be repaired in copied page fonts, only when every glyph the subset kept advances as its
assumed Thai character does; `text_prepare` repairs it also for text that was never grouped,
and the repair log records both preparation and map recovery.
Click a text region in **Edit a PDF → Edit text and images**. One editor contains its
source rows and mixed styles. Embedded TrueType and OpenType/CFF faces are used in memory;
source font programs, colour, size and baseline anchors stay intact. A textless preview
prevents a second copy underneath. The frame shows the live extent as text grows or shrinks.
Rows a text edit doesn't reach keep their original operators, so their kerning, `TJ` word
gaps and glyph widths stay exactly as they were. Only rows whose characters changed, or that
moved because an earlier row wrapped, are rewritten, and formatting a selection rewrites
only the rows it covers. Changing the whole paragraph's formatting, width or position rewrites
every row.
Add Text also opens existing text when it is clicked; a blank-space click starts a new item.
Both text tools retype an added text item with one click. Draft boxes grow to the page edge
before wrapping, and the saved item uses that width. A single visual source row grows across font/style boundaries;
multi-row paragraphs retain their existing wrap width. Retyping uses a preview with only
that added item's old drawing removed, so deleted draft characters do not show underneath.
A subset font contains only some of the original face's glyphs. A new character cannot be
reconstructed from a glyph the PDF does not contain. **Load font…** imports an openly licensed,
embeddable standalone `.ttf` or `.otf` face. **Installed fonts…** discovers eligible user-installed
faces on request, off the UI thread; the document-font menu selects resources already on the
page. Select **Keep source fonts** to preserve each run's original face. A matching full face
completes its subsets automatically, including new Chinese characters. When the PDF itself embeds
the complete program of a paragraph's font (not a subset) and its licence allows editable
embedding, a character the font's encoding can't show is drawn with that same program. A
character no available face has is refused with an error that names it, and a CJK character
without a craft-fonts build says so. Replacement text takes the craft-fonts Japanese faces when
they can draw all of it, and the bundled Inter face (`assets/fonts`) for the Latin, Greek and
Cyrillic letters they lack (Uzbek `қизил`, Kazakh `әже`): that face needs no build input, so
these work in every build, the web one included. Choosing another face
is explicit. Fonts with restricted embedding, unsupported collections, excluded families or
no open-licence declaration are refused. Interface fallback fonts are never exported.
The full selected program, glyph mappings, widths and Unicode text are embedded in the PDF.
Saving and reopening therefore needs no locally installed font. Editing the same text again
reuses the font resource the page already has, and every resource made from one program
shares a single copy of it. The Type 3 fonts generated for Japanese or Hebrew characters the
page's fonts lack are made anew by each edit; after an edit, those nothing on the page shows any
more leave the page's resources, unless some content that could name them can't be read.
Original subset bytes are
preserved. Optional matching faces from `CRAFT_FONTS_DIR` can also complete source subsets;
this works only when that build input actually contains the full face. This change does not
add a font binary or change the release font pin. Fonts shared by releases belong in
`storytold/craft-fonts` under the repository's asset rules.
**Keep source fonts** retains the existing Thai family, weight and point size. For legacy
THSarabunPSK subsets, PdfCraft reconstructs an editor-only Unicode cmap from the PDF's
Unicode and CID-to-glyph maps. HarfRust uses the retained Thai positional glyphs, and the
saved text refers to the original PDF font resources. Several subsets of the same exact
face can supply Thai and punctuation glyphs, including a Thai run whose clusters are split
across subsets, and a subset whose glyph for a character is empty is skipped; Regular and
Bold stay separate. Source font
programs are never modified or replaced. Text settings, including character/word spacing,
horizontal scale, rise, color and paint mode, remain in the PDF text state.
A source subset that lacks a needed contextual glyph produces an error asking for the
complete original face or an explicit replacement. Cross-family replacement requires an
explicit choice. The document-font menu offers **Noto Sans Thai**: pass its `text_fonts`
resource (normally `builtin:thai`) as `text_edit.font_resource` or a range's `font_resource`. This replacement
requires the optional craft-fonts Thai input and currently uses Regular. A matching Noto
subset can still be completed by that same bundled family when it is Regular and upright; a
subset its name or descriptor marks bold or italic needs an explicit replacement.
The bundled variable Thai face is written as bounded Type 3 glyph outlines, with shaped
vowel/tone positions and logical Unicode text, rather than embedded as a static font.
Saved paragraphs can be reopened and edited again without installing a font. Modern Thai
shaping uses the source's GPOS mark positioning (a `mark` or `mkmk` feature in the script and language system the shaper
selects for Thai, not just a GPOS table); GSUB substitution is optional, as specified by
[the OpenType Thai model](https://learn.microsoft.com/en-us/typography/script-development/thai).
Verified legacy Thai PUA fonts use positional substitutions instead of requiring GPOS; their
alternates are used only when each kept one has a mark's zero advance (or, for the two
consonant forms, an advance) and, where the PDF's own map names its character, the character
that alternate stands for. A THSarabunPSK program with its own Thai mark
positioning, such as a complete face loaded with Load font, is shaped by that layout instead,
still in its own codes. One without it takes the legacy path only when every Thai glyph it keeps
is where the legacy glyph order puts it, and is otherwise shaped as an ordinary source font. A
subset needs only the glyphs its new text uses. When no subset can show the new text in its
existing codes, a complete program of the same face and style (one added with Load font, CFF
included) shapes the whole span with its own layout, Latin rows too, rather than the edit being
refused; a Regular program never completes text the source marks bold or italic.
Mixed contextual style runs and dictionary word segmentation remain limited. No font binary
or release pin changes are needed.
When mixed-style text needs outline fallback, characters using the same actual fallback face
share Type 3 fonts, with at most 240 distinct glyphs per font. Adjacent characters with the same
font and source style are written as one run. Latin characters that the source font can encode
stay in it; Japanese serif/sans and weight choices remain separate. More than 240 missing glyphs
use additional fonts; a styled paragraph needing more than 15,360 distinct fallback glyphs is
refused with an error. Unsupported glyphs are refused before fallback objects are written.
This does not expand glyph coverage, shape mixed contextual runs or remove old unused fonts
when editing again. The separate single-style fallback path is unchanged.
Rewrapped Chinese and Japanese text, which has no spaces between words, may break between any
two characters. A row never begins with closing punctuation, a closing bracket or quote, a small
kana or the prolonged sound mark, and never ends with an opening bracket or quote or a currency
sign before an amount: the line-start and line-end rules of JIS X 4051, as in Unicode line
breaking (UAX #14). Latin words inside such text are not split. Korean and Thai still break only
at spaces.
Select text within the editor before changing size, colour, font, underline or spacing to
format that selection. Automation supports up to 1024 disjoint UTF-8 byte ranges with the
same properties; invalid or overlapping ranges fail atomically. Untouched source formatting
survives. OpenType shaping uses the selected/source face's substitution and positioning
rules, preserves marks and ligatures, and applies Unicode bidi ordering for a single face.
Each shaped run is one text object: its glyphs are shown in one `TJ` array whose adjustments
place each glyph where shaping put it, and a mark set above or below its letter is raised
with the text rise. Logical text remains available after save/reopen through ToUnicode and ActualText.
Added Arabic text records ActualText per shaped line for copying and searching in typing order.
ActualText already supplies typing order, so the editor preserves it without applying
a second bidi reversal; adjacent visual glyph runs still recover their own reading order.
Only right-to-left ActualText is kept apart this way: left-to-right spans, such as the Latin
ligatures (`fi`, `fl`) Word and Chrome wrap in ActualText mid-word, join their line as before.
A justified paragraph stretches each row but its last to the paragraph's width by widening its
spaces. Word spacing (`Tw`) widens only single-byte code 32, so where the font shows a space with
another code (a composite font, the generated Type 3 fallback) each space is followed by a `TJ`
adjustment instead. Justifying a paragraph with several formats in it is not supported yet: it is
left-aligned.
For headless editing, use `text_font_files` → `text_font_import` → `text_fonts` and pass a
resource to `text_edit.font_resource`. `text_edit.ranges` holds selected range formatting.
`text_edit_layout` accepts resource/ranges too and measures a draft without applying it.
All file inputs respect CLI root confinement; importing a font and applying text are undoable.
On a scanned page, recognised text (Scan & OCR) is an invisible layer over the picture. Edit
text groups it into phrases: words joined by a word space, with a table cell, a tab stop and each
scanned line kept apart. Applying an edit draws the new text visibly in the scan's ink colour (or
the one chosen) and first covers the old words with the paper colour sampled around them. Each
word keeps its position and width, and a longer phrase grows rightwards rather than wrapping. The
cover is one flat colour, and the text uses the layer's Helvetica, not the scanned typeface.
This applies to any invisible text (rendering mode 3), not just a PdfCraft OCR layer: editing
invisible text, such as another OCR tool's layer, makes the rewritten paragraph visible over a
painted cover in the sampled paper colour. Edit text also groups invisible text differently: its lines never
merge into multi-line paragraphs, and words split at gaps of 0.6 em or more.
Explicit marked paragraph/heading scopes prevent unrelated tagged regions from merging.
Compatible geometry groups untagged content, including runs split across content streams
or equivalent graphics transforms. Geometry cannot infer author intent in every untagged
PDF. Semantic structure-tree preservation, legacy Type 1/Type 3 inline font preview,
font collections/variable-font instances, mixed-font/style contextual runs and complete rotated
layout fidelity still need broader coverage. Do not interpret the passing real-file checks
as a guarantee for every PDF.
Private acceptance tests are opt-in and keep inputs unmodified. They exercise actual Chinese
keyboard insertion/deletion on all four pages, source-face/baseline checks and identical
in-memory renders before and after saving/reopening. Synthetic tests cover full TrueType
mapping, OpenType/CFF, Arabic marks/ligatures/mixed bidi, repeated edits, disjoint styles,
invalid ranges, font selection and root confinement. CLI acceptance repeats insertion/deletion
and two saves/reopens on four pages; independent PDFium checks extraction and rendering.
No private document, font or screenshot
is included in the repository.
## Paragraph numbers (automation)
A paragraph's number is its position in a list that is regrouped from geometry on every read,
so an edit that merges two paragraphs, or splits one, moves the others. After `text_paragraphs`
the numbers name the paragraphs *that listing showed*: `text_edit`, `text_edit_layout` and
`page_render`'s `hide_text_block` find the paragraph again by its text and box, so editing paragraph 3 then paragraph 1 still edits the
two the listing showed. A paragraph an edit merged into a neighbour (or changed) is refused, and so is the edited paragraph
itself when the edit merged it into one, with
a message to list the page again, instead of rewriting whichever paragraph took its number
(#2516). The result of an edit names the numbers that are gone in `stale_paragraphs`. Listing
a page again renumbers it. The listings kept are capped (256 pages); once the cap drops
them, a page of an affected document is refused until it is listed again, instead of reading its
numbers as positions. Without a listing, a number is a position in the page's current
paragraphs, as before. The inline editor holds the paragraph a switching click asked for the
same way. What is still missing is an identity that survives save and reopen (design: #2508).