Edit text: a paragraph's new colour no longer spills into the text after it (#6)

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>
This commit is contained in:
storybored379 2026-10-04 10:24:17 -07:00 committed by GitHub
parent 096b515719
commit 24f0a8928e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 25 additions and 1 deletions

View File

@ -449,6 +449,28 @@ fn paragraphs_take_new_formatting() {
}
}
#[test]
fn a_new_paragraph_colour_does_not_spill_into_the_text_after_it() {
let mut doc = text_page("BT /F1 10 Tf 72 700 Td (First paragraph) Tj 0 -40 Td (Second paragraph) Tj ET");
let style = text::BlockStyle { color: Some([1.0, 0.0, 0.0]), ..Default::default() };
text::rewrite_block(&mut doc, 0, 0, None, &style).unwrap();
let doc = reopen(&doc);
// The fill colour in force where the second paragraph is shown (q/Q nest it).
let ops = printcraft_content::parse(&page_content_bytes(&doc, 0)).ops;
let (mut fill, mut stack) = (String::from("0 g"), Vec::new());
for op in &ops {
match op.op.as_slice() {
b"q" => stack.push(fill.clone()),
b"Q" => fill = stack.pop().unwrap_or_default(),
b"g" | b"rg" | b"k" => fill = String::from_utf8_lossy(&printcraft_content::serialize_ops(std::slice::from_ref(op))).trim().to_string(),
b"Tj" if op.operands.first().and_then(|o| o.as_string()).is_some_and(|s| s.to_text() == "Second paragraph") => break,
_ => {}
}
}
assert_eq!(fill, "0 g", "{ops:?}");
assert_eq!(text::text_blocks(&doc, 0).unwrap()[1].text, "Second paragraph");
}
#[test]
fn justify_underline_and_spacing() {
let src = "BT /F1 10 Tf 12 TL 100 700 Td (One two three four five six seven) Tj T* (eight nine ten eleven twelve) Tj T* (end) Tj ET";

View File

@ -667,7 +667,8 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option
None => size * 1.2,
};
let n = printcraft_content::num;
let mut block_ops = vec![Op::new("BT", vec![])];
// In its own graphics state, so a new colour (or anything else) stops at the paragraph.
let mut block_ops = vec![Op::new("q", vec![]), Op::new("BT", vec![])];
let mut state = o_state.clone();
state.word_spacing = 0.0;
state.font = Some((show_font, size));
@ -735,6 +736,7 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option
block_ops.push(Op::new("S", vec![]));
block_ops.push(Op::new("Q", vec![]));
}
block_ops.push(Op::new("Q", vec![]));
// What the text after the paragraph expects: the state at its BT.
block_ops.extend(o.bt_state.ops().into_iter().filter(|op| !op.is("Tf") || o.bt_state.font.is_some()));
let drop: std::collections::HashSet<usize> = members.iter().flat_map(|l| l.ops.iter().copied()).collect();