mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
* Edit text: refuse a rewrite of text that is a clipping path Editing text in a Photoshop-exported PDF destroyed the artwork on the page. Such exports draw text in `7 Tr`: the glyphs are not ink but a mask, and a fill or image is painted through them. Rewriting the run removed the glyphs and so the mask, the fill behind flooded its own rectangle, and the file still opened without a warning (#3429). Measured on a page whose only text is a `7 Tr` mask over a red rectangle: the fill covered 3.4% of the page before the edit and 100% after it. `clear_block` already refuses these runs -- "text used as a clipping path cannot be previewed for editing" -- but `replace_line` and `rewrite_block` did not, so the destructive path was the one the editor offered. Both refuse modes 4 to 7 now, and the document is left untouched. Modes 0 to 2 paint ink and are rewritten as before. This does not keep the design: preserving it means emitting the replacement inside the same `BT … ET` with the mode intact, which needs the writer to place glyphs in a clipping object, as both reporters noted. Refusing turns silent data loss into an error while that is written. Found and analysed by @RavenCrowe666 and @mtlcl410, who converged on this fix. * Check every run the rewrite removes, not the line's first one The guard read `target.origin.state.render_mode`, the mode the line's first run set. Two paths got a clipping run past it: A line's runs need not share a rendering mode. Nothing in the join in `text_lines` requires one -- it asks for the same stream piece, scope, text object or neighbouring word, font, size, weight, slant and fill colour -- so a line that starts as ink can continue as a mask, and the whole line's operators are what the rewrite removes: BT /F1 48 Tf 40 60 Td 0 Tr (TITLE) Tj 7 Tr (MASK) Tj ET reads as one line "TITLEMASK" whose origin is `0 Tr`. Replacing it passed the guard and took the mask away with the ink. `coincident_ops` drops another drawing of the line on the same baseline, so that the replacement is all that shows -- a fake-bold second copy, an invisible OCR layer. It matches on text, overlap and baseline, not on rendering mode, so that copy can be the mask while the line being replaced is ink. Both rewrite paths could drop it. `TextLine` now carries whether any of its runs clips, and the guard reads every line whose operators are in the removal set, the way `clear_block` already checks every run it drops. In `replace_line_with_limit` the removal set is computed before the replacement font is generated, so a refusal still leaves the document untouched. Both cases have a test; on the previous commit each returns `Ok(LineEdit { substituted: None })`. * Edit text: keep existing fixtures on text_page; build the clipping fixture on it 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>