Commit Graph
332 Commits
Author SHA1 Message Date
PerchAnton 42a31db308 M7: retain product notes while integrating current main 2026-10-18 17:51:48 +03:00
alehelandClaude Opus 5.5 eca16e6bd5 fix(edit): backdrop copies keep the stream dictionary and every slot
Review of #2815 (CodeRabbit):

- A content stream whose bytes live in an external file (`/F`) draws
  those, not its inline bytes, so it is never taken for a backdrop.
- The copy keeps the original stream dictionary, unknown entries
  included, less `Length` and `DL` (Flate re-encodes it); it had started
  from an empty dictionary.
- Every `/Contents` slot that lists the original stream gets the copy: a
  page listing the same stream twice painted the white fill again over
  the background.
- The Background dialog starts with replacing on each time it opens.
  Update restores backdrops before re-applying, so a replaced page shows
  no fill and no checkbox, and a choice left off from an earlier dialog
  hid the background again unseen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:24:14 +02:00
aiongg b6b02a1b27 Merge upstream main 1952a8880 into next
ROADMAP.md, docs/roadmap.md: union of both sides' lines, each once.
parity/acrobat-features.toml: the XFA entry has both sides' evidence and notes (#2604 page areas, #2813 CLI layout).
2026-10-18 14:22:30 +00:00
alehel 911147126c Merge remote-tracking branch 'origin/main' into fix/background-hidden-by-page-backdrop
# Conflicts:
#	ATTRIBUTION.toml
2026-10-18 16:05:19 +02:00
mtlcl410andClaude Opus 5.5 2bd6d1357d M5: flattening comments leaves 3D, multimedia and print-production annotations (#3087) (#3089)
doc_flatten with comments chose every non-widget annotation except Link
and Popup, so a comments-only flatten baked in and deleted 3D, Screen,
Movie, RichMedia, PrinterMark, TrapNet and Watermark annotations with
their data (a 3D annotation's U3D model and default view). The comments
branch now uses pdfcraft_annot::is_comment_subtype, the classifier the
comment list uses; widgets follow `fields` only.


Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:48:26 -04:00
aiongg 7f718ecc62 Merge upstream main 50f261d70 into next
ROADMAP.md, docs/roadmap.md: union of both sides' log lines and milestone rows, each line once.
parity/acrobat-features.toml, crates/xfdf/README.md: next's text, a superset of main's (#1917, #2686 plus #2688).
crates/edit/src/tests.rs: both sides' new tests (#2558 and #3084).
crates/ui-egui/src/editing.rs: the picked picture is cleared on every edit, and a new drawing commits its raster (#3149).
2026-10-18 13:12:21 +00:00
PerchAnton caaae7a8c7 M7: retain product notes while integrating current main 2026-10-18 15:50:34 +03:00
MarekSpodBiedry 7420af6751 Draw a note without an appearance as a fixed-size icon at the corner of its Rect (#3175) 2026-10-18 13:53:26 +02:00
komo1iddinandClaude Opus 5.5 36d239d2b1 M7: fold repeated group moves of a cropped image into one clip cm (#2476)
Moving an image whose crop is its own (`q re W n cm Do Q`) as part of a
group put a fresh `cm` before the clip path on every move, so twenty
moves left twenty `cm` operators in front of one `re`. It never nested
deeper, but the content stream grew with every drag.

When a `cm` already sits right before that clip (one an earlier move
added, or the file's own), `translate_images` now replaces it with the
product of the new and old matrices. The CTM at the clip is
`c · prev · outer` either way (ISO 32000-1 8.3.4, 8.4.4). A previous
`cm` that is not finite, or a product larger than 1e12, keeps the old
behaviour and inserts a separate `cm`.

The new test moves an image cropped smaller than itself twenty times and
checks that the image and its visible crop both moved by the full offset,
that the operator count stays at its value after the first move, and that
the caption after it is untouched. It fails without this change
(32 operators against 13).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:47:08 -04:00
PerchAnton b05d0cce4a chore(pr-3000): integrate main d8b27058 2026-10-18 08:33:10 +03:00
skylar taylor-barrick d36a2ccbf7 Image Arrange: keep the image's clipping and position through later edits
- Remove Crop rebuilds the image's placement from its drawn CTM instead of
  pattern-matching cm ops. After an Arrange under a translated CTM it used to
  keep the absolute placement and draw the image displaced.
- Arrange refuses a move that would change which clips apply to the image
  (moving it past a top-level clip it isn't under, or out from under one
  without redrawing it), instead of silently cropping it.
- The exact-clip test now checks the moved copy carries its own clip and
  hit-tests both images.
- Cargo.lock: add pdfcraft-edit's pdfcraft-render dev-dependency, which the
  previous lockfile commit left out (CI failed --locked).
2026-10-17 22:04:17 -06:00
skylar taylor-barrick 4938bbf272 Merge storytold/pdfcraft main (picks up the zbus advisory ignores) 2026-10-17 20:44:19 -06:00
alehel a21faed4e0 Merge remote-tracking branch 'origin/main' into fix/background-hidden-by-page-backdrop
# Conflicts:
#	crates/edit/src/lib.rs
2026-10-18 02:06:28 +02:00
skylar taylor-barrick 9b4af15b21 fix(edit): read marked content that spans content streams in one pass (#3081)
reading_lines grouped the joined operators by the stream their first token
is in and walked each group on its own. A marked-content sequence opened in
one stream (its /ActualText dictionary, or the whole BDC) and closed in the
next lost its replacement text: the export read "Glyphs" where every reader
of the joined streams reads "Replacement text".

The page's operators are now walked once. Each shown text still counts as
the stream its operator starts in, and text from another stream still starts
a new line, as before.
2026-10-18 00:27:50 +02:00
skylar taylor-barrick 43bb98eb88 fix(edit): isolate new marks from state left by content after an earlier wrapper (#3084)
place_tagged wrapped the page's content in q/Q only when no Wrap stream
existed yet. Content appended after an earlier wrapper (by another tool, or
an older OCR layer) that left a cm, a clip or text state set then moved,
hid or distorted every later header, Bates number, watermark, added text,
flattened annotation and redaction cover.

Placing a mark on top now reads the page's actual operators instead of the
/PCMark tags. When the content parses cleanly with balanced q/Q and closed
text objects and paths, the stretch from the first stream that leaves state
set is wrapped, once, at a stream boundary no comment runs into. Content
that is already self-contained gets no new wrapper, so repeated marks don't
pile up.

Content that can't be read that precisely (stray bytes, unbalanced q/Q, an
unfinished text object or path, too large) is wrapped whole as before,
rather than refusing: redaction stamps its covers after removing the text
and can't back out. A q that content leaves open is closed after it, where
closing it changes nothing already drawn.

The wrapper's close now starts with a newline, so a comment at the end of
the content before it can't swallow the Q. Removing marks strips only
wrapper pairs that end the list, keeping one that later content still
relies on, and recognises the older close bytes too.
2026-10-17 21:50:58 +02:00
skylar taylor-barrick 392406eb4a fix(content): read a comment at the end of a content stream as the renderer does (#3081)
Pieces::join separated a page's content streams with a newline, which ended
a comment left open at the end of one stream. hayro, MuPDF and Poppler join
them with a space, so the comment runs on into the next stream and hides its
first line. Edit text, object_list, the redaction audit, redaction and the
Office/HTML export therefore saw text and boxes that nothing draws, and
redacting unrelated text on the page made the hidden content visible: the
rewritten stream dropped the comment.

- Pieces::join uses a space, so every reader of the joined streams agrees
  with rendering.
- The parser records, per piece, whether a top-level comment runs into it
  and out of it (boundaries only, not every comment). A redaction rewrite
  of a piece ends an incoming comment before its first operator and leaves
  an outgoing one open, so hidden content stays hidden.
- Reading for export parses the streams joined, instead of one by one.
- The redaction tag check compares the joined streams the same way.
- Pieces::piece_of uses a binary search.
2026-10-17 21:50:58 +02:00
PerchAnton 1d477655b5 test(edit): cover fallback budget and weight cache (#2592) 2026-10-17 15:23:19 +03:00
PerchAnton 9283d2fb22 fix(edit): batch styled fallback glyphs (#2592) 2026-10-17 11:56:29 +03:00
alehelandClaude Opus 5.5 0b63f5a4ba fix(edit): check page backdrops with one page-tree walk per batch
Review of #2815: `page_of` built every page to return one, and the Add
Background dialog checked 24 pages a frame that way, so a frame cost
24 page-tree walks (93 ms at 5,000 pages, 385 ms at 20,000).

- `page_backdrops(doc, pages)` walks the tree once for a batch; the
  dialog checks one batch a frame and `doc_backdrops` one batch in all.
  A page past the end answers None without hiding the others (the
  viewer's page count can disagree with the page tree's).
- `scan` and `replace` take the page; `add_background` no longer walks
  the tree twice more per page to replace a backdrop.
- Tests: 4,000 pages checked in one batch under a generous bound (17.6 s
  with the old lookup), batch order and missing pages, and a background
  over several pages sharing streams, replaced and restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 05:43:46 +02:00
Combiler01 035183777a fix:implement 8 point non destructive crop, image resize and Arrange extraction bugs 2026-10-16 21:24:49 +05:30
aiongg 2366b0c513 Merge PR #2710: fix(edit): wrap single turned rows at the page margin (#2578) 2026-10-16 14:02:18 +00:00
aiongg d77066be2b Merge PR #2577: M7: rewrap a paragraph at a slant at the width its rows have 2026-10-16 14:02:18 +00:00
aiongg 28942c4c7d Merge PR #2558: fix(edit): emit rewritten paragraph after backgrounds between its rows (#2377) 2026-10-16 14:02:18 +00:00
mtlcl410andClaude Opus 5.5 b97e59190f M5: year-first slash dates (yyyy/mm/dd, yyyy/m/d) for Fill & Sign and headers
Taiwan, Japan and China write dates year first with slashes (Windows'
zh-TW short date is yyyy/M/d), and neither the Fill & Sign presets nor
the header and footer date tokens offered it. Both now have yyyy/mm/dd
and yyyy/m/d. The header and footer list gains them at its end, since
its selection is an index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-16 19:20:48 +08:00
alehelandClaude Opus 5.5 a9b0db12c6 fix(edit): show backgrounds on pages that paint their own white backdrop
Add Background draws behind the page content, so on a page that first
fills itself with opaque white (browsers' Save as PDF, print drivers) the
background was hidden completely and the tool looked broken.

- pdfcraft-edit: new `backdrop` module. A strictly detected backdrop (one
  axis-aligned `re`, opaque white in a device colour space, painted before
  anything else and covering the crop box) is replaced by turning that one
  fill into `n` in a per-page copy of its stream; the original stream is
  left untouched for other pages. Remove background restores it, unless
  the copy was edited since. Page-sized pictures (scans) are reported only.
- Background gains `replace_backdrop` (default on); the default colour is
  a visible light yellow instead of near-white.
- Dialog: offers to replace the backdrop, warns about scanned pages,
  checks pages a few per frame, preview draws text over the colour.
- Automation: `doc_background` takes `replace_backdrop`; new read-only
  `doc_backdrops` lists pages with a backdrop.

Acrobat's behaviour on such pages is unobserved; recorded as our choice
in parity/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 10:46:02 +02:00
wstockandClaude Fable 5.1 6e3a16f96a Issue #2368: flattening keeps a NoRotate widget upright on a rotated page
A widget (or any annotation) with the NoRotate flag is shown upright on a
page with /Rotate: its appearance turns with the page about the rectangle's
upper-left corner (ISO 32000-2 12.5.3), which is how MuPDF and Poppler draw it.
Flattening drew the appearance onto the rectangle as it lies in user space
(Algorithm 8.1 alone), so on a /Rotate 90 page the flattened widget turned
sideways and moved while its neighbours without the flag stayed put.

The placement matrix of a NoRotate annotation is now followed by that turn
(`upright`), for 90, 180 and 270.

Regression test: tests::flattening_keeps_a_no_rotate_widget_upright_on_a_rotated_page
(a plain and a NoRotate widget on pages rotated 0/90/180/270; the plain one's
cm is unchanged, the NoRotate one's is the turned matrix). Verified on qpdf's
manual-appearances.pdf page 2 (/Rotate 90): Poppler's render of the flattened
page differs from the original's by 80 pixels at 36 dpi (anti-aliasing),
against 648 before, when the NoRotate widget's box moved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
2026-10-16 06:19:15 +00:00
Krzysztof Cieślik ad9e5d2ba9 fix(edit): keep a one-row centred title centred when it grows (#2581)
- Detect a one-row centred title through a new `is_centred` helper in
  `text_regions.rs`, shared by both rewrite paths, instead of the multi-row
  ragged-centres rule that can never fire on a single row.
- A lone row counts as centred when its centre sits within one font size of
  the page centre and its left edge is not the page's dominant left edge, so a
  full-width left-aligned line at the margin is not mistaken for a title.
- In the single-style path, a centred block with no explicit width or
  alignment no longer has its width stretched to the page margin; the line is
  centred in the box it already sits in, so a grown row keeps its centre and
  every wrapped row shares it.
- Gate the new rule behind main's `upright` flag: `is_centred` measures
  page-space rects, which are not meaningful across a turned baseline; frame
  coordinates (#1443) are the tool for that, not this change.
- Add `a_one_row_title_keeps_its_centre_when_it_grows_and_when_it_wraps`,
  built from the issue's fixture; red before the fix (centre 398.025 vs
  expected 306), green after.
2026-10-16 02:49:20 +02:00
jonathanwxh-cell 9229e7af36 fix(edit): wrap single turned rows at the page margin (#2578) 2026-10-16 08:13:01 +08:00
Krzysztof Cieślik a3cbb85536 M7: let a list marker open an item and a hanging indent continue it (#2564) 2026-10-16 01:30:11 +02:00
jonathanwxh-cell 6f348589bf fix(edit): emit rewritten paragraph after backgrounds between its rows (#2377) 2026-10-16 07:24:05 +08:00
vangel76andClaude Sonnet 5.5 0871543fd8 M7: rotate an existing page image by any angle (#1940)
A selected image gets a rotate knob (Shift: 15 degree steps); image_edit
gains rotate_by (degrees). The turn is one extra cm about the visible centre.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 23:14:59 +02:00
fanyzp 963609d7d6 fix(edit): draw the Cyrillic the Japanese faces lack with the bundled Inter face (#2593) 2026-10-16 00:31:49 +08:00
jonathanwxh-cell 279712769d fix(edit): keep overlapping watermarks and distinct paragraphs on text edit (#1756) 2026-10-15 23:22:11 +08:00
komo1iddinandClaude Opus 5.5 398ad77077 M7: rewrap a paragraph at a slant at the width its rows have
A turned paragraph's width came from its rows' upright bounding boxes,
turned into the baseline's frame. On a slant that box is wider than the
row by the row's height times |sin 2θ| (about 12 pt for 12 pt text at
30°), so a rewrite let a word that doesn't fit stay on its row.

Each run now also records its box along and across its baseline, from
the same corners as its upright box, and a line unions those. Grouping
and the rewrite's width read that box. Upright and quarter-turned text
are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:51:57 +05:00
Combiler01 b31e6b49db merge: reconcile upstream/main with local changes 2026-10-15 19:36:23 +05:30
komo1iddinandClaude Opus 5.5 e0ef098381 M7: drop the fallback fonts a text edit leaves unused
Each edit that draws Japanese or Hebrew characters the page's fonts lack
generates new Type 3 fonts with the outlines of every character it draws.
Editing the paragraph again generated new ones and left the old ones in
the page's resources, so every edit added a set of glyph outlines to the
file.

After replace_line and rewrite_block, the page's generated fallback fonts
(Type 3, /Name /PCJapanese or /PCHebrew) that no Tf selects any more are
removed from its own font dictionary. The content read for this is the
page's streams, every form XObject it can reach that has no resources of
its own (those use the page's, ISO 32000-1 7.8.3), and the glyph
procedures of Type 3 fonts without resources. If any of those can't be
decoded strictly, nothing is removed. Registered fonts (PCFull) and the
file's own fonts are never touched.

Tests: a page with an unused generated font, one its content selects,
one only a resource-less form selects and a file's Type 3 font that only
shares the name prefix keeps all but the first after a rewrite, and keeps
everything when such a form can't be read. Rewriting Japanese text twice
(with craft-fonts) leaves only the second rewrite's fonts. The first test
fails without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 15:13:34 +05:00
komo1iddinandClaude Opus 5.5 2109c6785e M7: show a shaped run in one text object instead of one per glyph
Shaped text (Arabic, Indic, combining marks) was written as one text
object per glyph: BT, the whole text state (Tf Tc Tw Tz TL Ts and the
colour), Tm, Tj and ET. A 15-glyph row took 15 text objects and 392
tokens (the whole stream), a long Arabic paragraph thousands of operators, and other
viewers saw every glyph as a separate piece of text.

Each run is now one text object: the state once, one Tm, and the glyphs
in TJ arrays. A glyph's adjustment moves it from where the glyph before
it ends (its width in the embedded font) to where shaping put it, and a
mark shaped above or below its letter is raised with Ts, then reset
(ISO 32000-1 9.4.3, 9.3.7). Character and word spacing are part of those
positions, so Tc and Tw are 0 in the run. The same row is now 1 text
object (90 tokens for the stream), and renders the same at 300 dpi (138 of 33
million bytes differ, by 1 level, from rounding).

The new test shapes marks beside and above their letters with Inter,
reads each glyph's origin back from the TJ displacements and checks it
against the shaped position. It fails without this change (15 text
objects in the run against 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 14:58:32 +05:00
d1bc200115 M7: grow text boxes and edit existing text (#1539)
* 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>
2026-10-15 03:52:33 -04:00
a790a046a5 M7: keep line breaks when editing a multi-line paragraph (#1443) (#1486)
Adding a word to a multi-line paragraph merged or reflowed its lines.
Two causes:

- Chrome/Skia subset fonts leave glyphs of the /DW width out of /W.
  has_glyph treated those CIDs as missing, so words containing them
  measured short, run calibration inflated, unchanged rows rewrapped
  and new letters fell back to Helvetica. A composite subset with an
  explicit /DW now counts a ToUnicode-mapped CID absent from /W as
  present.
- Text drawn turned on the page (landscape pages drawn a quarter turn
  round) was joined into lines and rows along the page's x axis, so the
  whole paragraph became one line. Lines, rows and blocks are now found
  along each text's own baseline (identical values for upright text), a
  turned paragraph is rewritten in its own text space, and that rewrite
  keeps explicit line breaks. Formatting part of turned text is refused
  instead of being misplaced.


Claude-Session: https://claude.ai/code/session_01VFaMKaTDYNrroco63W74EQ

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 03:10:46 -04:00
2a130abf82 Refuse header/footer Bates numbers that would overflow u64 (#2253)
Found while testing crates/edit/src/lib.rs:445

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:52:17 -04:00
Marco Arnulfo dfdb31b25d fix(edit): keep the runs of untouched text between two edits (fixes #2198) (#2209) 2026-10-15 02:48:54 -04:00
Marco Arnulfo c3e9609bd1 fix(fonts): keep a subset Type 0 font for glyphs /W leaves to /DW (part of #1388) (#2153) 2026-10-15 02:47:44 -04:00
a7fe7a20c4 M7: Hebrew in Add text (#128) (#2109)
Add text and page_add_text draw Hebrew with the craft-fonts Hebr face
(Noto Sans Hebrew) through the shaped path Arabic and Thai already use:

- pdfcraft-fonts: ShapedScript::Hebrew, shape_hebrew and hebrew_has.
  Grapheme clusters keep points and cantillation marks on their letter.
- pdfcraft-edit: Hebrew paragraphs are laid out with UAX #9 like Arabic;
  spaces and punctuation between Hebrew letters take the Hebrew face.
  first_undrawable refuses Hebrew the face can't draw (or without the
  face) instead of writing "?"; existing items stay movable without it.
- Type 3 glyphs with ToUnicode and per-line ActualText, so extraction,
  search and the editor get typing order back.

Regular style only, as for Arabic and Thai.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:45:12 -04:00
jonathanwxh-cellandClaude Opus 5.5 b655208aaf fix(ui): header, footer and watermark dialogs refuse undrawable text before OK (#2013) (#2107)
The dialog previews in the interface font, which draws Cyrillic, while
applying draws with the standard fonts and refused it after OK.
HeaderFooter::first_undrawable and Watermark::first_undrawable are now
the one check: applying refuses through them, and the dialog shows the
Edit Text warning and disables OK on the same answer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:44:49 -04:00
6c22c78a72 Edit: crop an existing page image with its handles (#2002)
* Edit: crop an existing page image with its handles

ImageChange::Crop(rect) keeps the part of a page image inside rect (user space)
with a clipping path placed just inside the scope that holds the image (or a
new q … Q around its Do); a clipping path already at the scope's start, the
picture's earlier crop, is replaced, so a crop can grow back. The pixels are
never scaled, and a later move or resize wraps the scope, so the crop moves
with the picture. ImageEdit::Crop (undo label "Crop image"), image_edit
action crop. In the window, right-click ▸ Crop Image puts the selected
picture in crop mode: eight handles trim (the inside pans), the part that goes
is dimmed, Enter or Done applies, Esc or Cancel leaves; Remove Crop shows the
whole picture again. Rows in every language, their checksums in
ATTRIBUTION.toml, parity edit.image-crop shipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Image crop: Bulgarian hint without a character the monospace UI font lacks

JetBrains Mono has no 'ѝ', so primary_ui_fonts_cover_latin_and_cyrillic_catalogs
failed; the hint is reworded ("with its handles" -> "with the handles").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Refresh catalog hashes after merging main

---------

Co-authored-by: Osvaldo Duran <338149330+regendorf-ux@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:37:56 -04:00
mtlcl410andClaude Fable 5.1 6b7abc61d1 fix(edit): flattening keeps a widget it cannot draw and an undrawn reply (#1794) (#1934)
A widget with no appearance to draw (none stored, or none for its selected
state) was removed from the page and the field tree with nothing in its
place, so the field's value was lost; a reply kept because it could not be
drawn was then deleted anyway as a reply of the flattened comment. Both now
stay as they are. A widget whose appearance draws nothing is still removed,
as is a reply note's empty appearance with its comment.


Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 02:30:42 -04:00
56ef1f1c4e fix(audit): an applied redaction at the end of a line is not a faux redaction (#1898)
* fix(audit): an applied redaction at the end of a line is not a faux redaction

After Apply Redactions removes text at the end of a line, the line keeps a
TJ adjustment where the text was, and the box of the line still spans it.
The audit compared the redaction box with that box: at the end of a line
it covered more than 40% of it and the words left beside it were reported
as covered text, on a correctly redacted file. In the middle of a line the
gap was usually too small to cross the threshold.

Text lines now also give the boxes of the words they draw (`ink`: glyphs,
without spaces and TJ adjustments), and the audit measures how much of
those words a cover hides. A real cover over text that is still there is
found as before.

doc_audit_redactions also reports its rectangles in top-left-origin points
like comment_list and doc_info; they were in PDF user space.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(audit): leave the rectangles of doc_audit_redactions in user space here

Reporting doc_audit_redactions rectangles in top-left-origin points is a
separate subject from the false alarm this branch fixes, and on its own it
made the tool and `pdfcraft-cli audit` disagree on the same file: the CLI
still prints PDF user space. Take it out of this change; the tool and the
CLI can move together in their own change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(edit): word boxes span whole words when glyphs run backwards

With a negative font size or a negative horizontal scale (Tz), each glyph
advances backwards along the text matrix, and the box of a word kept only
its first glyph: the end of the word was compared with a start that had
already moved past it. A word box is now the union of its glyphs whichever
way they go, and a TJ adjustment ends a word past 0.2 em of the scaled size,
whatever its sign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(audit): a cover over a short value after a long label is still found

Measuring a cover on the words of a line alone missed faux redactions main
found: a long label followed by a short value hidden under the box is less
than 40% of the line's words. A cover now also counts, as before, when it
hides 40% of the line's box and at least half of one word still drawn; a
cover over the gap an applied redaction left, with no word under it, still
does not. The audit README states both rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(audit): retain partial-value cover detections

Co-Authored-By: OpenAI Codex <noreply@openai.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
2026-10-15 02:27:55 -04:00
komo1iddinandClaude Opus 5.5 6aa9de485c M7: justify paragraphs whose font shows the space with another code (#1832)
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>
2026-10-15 02:25:53 -04:00
b18639c242 M7: rewrap Chinese and Japanese text between characters (#1827)
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>
2026-10-15 02:25:42 -04:00
72a46facb8 M7: read the page once per keystroke when measuring a text draft (#1781)
`text_edit_layout`, which the inline editor calls on every keystroke to
size the frame, interpreted the page's content four times: `text_blocks`
for the paragraphs, `text_lines` again for the untouched runs, once more
inside `rewrite_block` on the still unchanged clone, and finally for the
rewritten draft. The first three read the same, unchanged page.

It now reads the page's lines once, groups the paragraphs from them, and
hands them to the rewrite through `rewrite_block_from`, which
`rewrite_block_with_limit` also uses after reading the lines itself. The
draft is still read back once after the rewrite. On a page of 120 runs
in 80 paragraphs, 50 keystrokes in a release build took 0.87 ms each,
against 1.25 ms before (the fastest of three runs each).

No behaviour changes. The new test checks that the measured draft frame
is the extent an applied edit actually draws, for a longer row, a
shorter one and an added row, so sharing the lines can't drift from a
fresh read.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:24:55 -04:00