mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
M2.6: edits stop at a content stream that doesn't decode, as at damage
An edit was refused after a content stream damaged partway, since what the damage hides can change how the later streams read, the graphics state they draw in among it. A stream that doesn't decode at all hides all of its operators, and an edit after one went ahead. It now stops there too: text lines and blocks, and images (arrange, crop, transform, move), as selecting objects on `main` already does for a page with such a stream. An edit in a stream before it goes on, and the stream is kept as stored. A file may give a stream's /Filter by reference, which isn't read here, so the case needs no damage: `content_after_a_stream_that_does_not_decode_is_not_edited` has such a stream move the text and image after it with a `cm`. `a_content_stream_that_does_not_decode_is_kept_and_edited_around` becomes `..._kept_and_edited_before`, its edit now in the stream before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
1 parent
0916bec6ef
commit
86cb375869
2 files changed
+48
-15
No files matched your search
@@ -5191,7 +5191,7 @@ fn an_edit_never_rewrites_a_content_stream_damaged_partway() {
|
||||
assert!(refused.to_string().contains("is damaged partway"), "{refused}");
|
||||
assert_eq!(contents(&doc), listed);
|
||||
let refused = text::replace_line(&mut doc, 0, 1, "Edited").unwrap_err();
|
||||
assert!(refused.to_string().contains("comes after a content stream that is damaged partway"), "{refused}");
|
||||
assert!(refused.to_string().contains("comes after a content stream that is damaged or can't be decoded"), "{refused}");
|
||||
assert_eq!(contents(&doc), listed);
|
||||
|
||||
let mut doc = split_page("BT /F1 12 Tf 72 600 Td (Whole) Tj ET", "");
|
||||
@@ -5244,25 +5244,56 @@ fn a_content_stream_that_does_not_decode_is_never_written_over() {
|
||||
}
|
||||
|
||||
/// A content stream that doesn't decode at all is no damage to write over: it is kept as stored,
|
||||
/// and the page's other streams are read and edited around it. A stream of a few MB is read for
|
||||
/// and the page's streams before it are read and edited. A stream of a few MB is read for
|
||||
/// selecting objects.
|
||||
#[test]
|
||||
fn a_content_stream_that_does_not_decode_is_kept_and_edited_around() {
|
||||
let mut doc = split_page("", "BT /F1 12 Tf 72 600 Td (Whole) Tj ET");
|
||||
fn a_content_stream_that_does_not_decode_is_kept_and_edited_before() {
|
||||
let mut doc = split_page("BT /F1 12 Tf 72 600 Td (Whole) Tj ET", "");
|
||||
let mut dict = pdfcraft_cos::Dict::new();
|
||||
dict.set(b"Filter".to_vec(), Object::name("NoSuchFilter"));
|
||||
let unknown = pdfcraft_cos::Stream::from_raw(dict, b"BT /F1 12 Tf 72 700 Td (Unread) Tj ET".to_vec());
|
||||
doc.set(pdfcraft_cos::ObjRef::new(4, 0), unknown.clone());
|
||||
doc.set(pdfcraft_cos::ObjRef::new(5, 0), unknown.clone());
|
||||
text::replace_line(&mut doc, 0, 0, "Edited").unwrap();
|
||||
let doc = reopen(&doc);
|
||||
assert_eq!(text::text_lines(&doc, 0).unwrap().into_iter().map(|l| l.text).collect::<Vec<_>>(), ["Edited"]);
|
||||
let Object::Stream(kept) = &*doc.get(pdfcraft_cos::ObjRef::new(4, 0)) else { panic!("not a stream") };
|
||||
let Object::Stream(kept) = &*doc.get(pdfcraft_cos::ObjRef::new(5, 0)) else { panic!("not a stream") };
|
||||
assert_eq!(kept.raw, unknown.raw, "stored as it was");
|
||||
|
||||
let large = format!("{}0 0 m 10 10 l S", " ".repeat(2 << 20));
|
||||
assert!(editable_objects(&split_page(&large, "BT /F1 12 Tf 72 600 Td (Whole) Tj ET"), 0).is_ok());
|
||||
}
|
||||
|
||||
/// A content stream that doesn't decode hides its operators as damage does. Here its `/Filter` is
|
||||
/// given by reference, which a file may do and which isn't read here, and it holds a `cm` that
|
||||
/// moves what the page draws after it. Text and images there aren't edited, as the edit would be
|
||||
/// worked out without the `cm`.
|
||||
#[test]
|
||||
fn content_after_a_stream_that_does_not_decode_is_not_edited() {
|
||||
use pdfcraft_cos::{Dict, ObjRef, Stream};
|
||||
let hidden_move = |doc: &mut Document| {
|
||||
let filter = doc.add(Object::name("FlateDecode"));
|
||||
let mut s = Stream::flate(Dict::new(), b"1 0 0 1 0 -300 cm");
|
||||
s.dict.set(b"Filter".to_vec(), Object::Ref(filter));
|
||||
doc.set(ObjRef::new(4, 0), s);
|
||||
};
|
||||
let contents = |doc: &Document| pdfcraft_model::pages(doc)[0].dict.get(b"Contents").cloned();
|
||||
|
||||
let mut doc = split_page("", "BT /F1 12 Tf 72 600 Td (After) Tj ET");
|
||||
hidden_move(&mut doc);
|
||||
let listed = contents(&doc);
|
||||
let refused = text::replace_line(&mut doc, 0, 0, "Edited").unwrap_err();
|
||||
assert!(refused.to_string().contains("comes after a content stream that is damaged or can't be decoded"), "{refused}");
|
||||
assert_eq!(contents(&doc), listed);
|
||||
|
||||
let mut doc = two_stream_image_page("", "q 100 0 0 100 200 200 cm /Im1 Do Q");
|
||||
hidden_move(&mut doc);
|
||||
let listed = contents(&doc);
|
||||
let shift = images::ImageChange::Transform([1.0, 0.0, 0.0, 1.0, 10.0, 0.0]);
|
||||
let refused = images::change_image(&mut doc, 0, 0, &shift).unwrap_err();
|
||||
assert!(refused.to_string().contains("comes after"), "{refused}");
|
||||
assert_eq!(contents(&doc), listed);
|
||||
}
|
||||
|
||||
/// A content stream whose `/DecodeParms`, or a parameter in them, is given by reference is read
|
||||
/// through the reference: its text is listed and edited, not read with the predictor's tag bytes
|
||||
/// left in it.
|
||||
|
||||
@@ -148,29 +148,31 @@ pub(crate) fn splice(pieces: &Pieces, ops: &[Op], mut edit: impl FnMut(usize) ->
|
||||
}
|
||||
|
||||
/// An error when content at `/Contents` index `at` (a stream there, or one inserted there) is drawn
|
||||
/// after a stream damaged partway: what the damage hides can change how it reads.
|
||||
/// after a stream not read whole, damaged partway or not decoding at all: what wasn't read can
|
||||
/// change how it reads, the graphics state it draws in among it.
|
||||
pub(crate) fn drawn_before_damage(streams: &[ContentStream], at: usize) -> Result<(), EditError> {
|
||||
if at > first_damaged(streams) {
|
||||
return Err(EditError::Invalid(
|
||||
"this change comes after a content stream that is damaged partway, and what the damage hides can change how the rest of the page reads"
|
||||
"this change comes after a content stream that is damaged or can't be decoded, and what wasn't read can change how the rest of the page reads"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The index of the first stream damaged partway, or the number of streams.
|
||||
/// The index of the first stream not read whole, or the number of streams.
|
||||
fn first_damaged(streams: &[ContentStream]) -> usize {
|
||||
streams.iter().position(|s| s.read == Read::Damaged).unwrap_or(streams.len())
|
||||
streams.iter().position(|s| s.read != Read::Whole).unwrap_or(streams.len())
|
||||
}
|
||||
|
||||
/// The page's `/Contents` after a [`splice`]: pieces whose bytes changed become new streams
|
||||
/// (keeping their dictionaries), the others stay as they were. An error, before anything is
|
||||
/// added, when a piece that changed is a stream that didn't decode whole, or comes after one
|
||||
/// damaged partway. Written from what decoded, a damaged stream would lose everything after the
|
||||
/// damage, and one that doesn't decode all of it. What follows the damage is unknown, so a later
|
||||
/// stream may not read as it seems: a string or comment the damage left open runs on into it, and
|
||||
/// operators the damage hides can change the graphics state it draws in.
|
||||
/// added, when a piece that changed is a stream that didn't decode whole, or comes after one.
|
||||
/// Written from what decoded, a damaged stream would lose everything after the damage, and one
|
||||
/// that doesn't decode all of it. What follows the damage is unknown, so a later stream may not
|
||||
/// read as it seems: a string or comment the damage left open runs on into it, and operators the
|
||||
/// damage hides can change the graphics state it draws in. A stream that doesn't decode hides its
|
||||
/// operators all the same.
|
||||
pub(crate) fn rewritten_contents(doc: &mut Document, streams: &[ContentStream], new: Vec<Vec<u8>>) -> Result<Object, EditError> {
|
||||
// The last stream that changed: when it is drawn before the damage, so are the others.
|
||||
if let Some(i) = streams.iter().zip(&new).rposition(|(s, data)| s.data != *data) {
|
||||
|
||||
Reference in new issue
Block a user