1use crate::error::{ManipError, Result};
14use lopdf::content::{Content, Operation};
15use lopdf::{Document, Object, ObjectId, Stream};
16use std::io::Write;
17use std::ops::Range;
18
19#[derive(Debug, Clone)]
24pub struct ContentEditor {
25 operations: Vec<Operation>,
26}
27
28const MAX_CONTENT_ARRAY_DEPTH: usize = 64;
35
36pub(crate) fn content_stream_too_deeply_nested(stream: &[u8]) -> bool {
39 let mut depth: usize = 0;
40 for &b in stream {
41 match b {
42 b'[' => {
43 depth += 1;
44 if depth > MAX_CONTENT_ARRAY_DEPTH {
45 return true;
46 }
47 }
48 b']' => {
49 depth = depth.saturating_sub(1);
50 }
51 _ => {}
52 }
53 }
54 false
55}
56
57impl ContentEditor {
58 pub fn from_stream(stream: &[u8]) -> Result<Self> {
64 let content = crate::inline_image::decode(stream)?;
65 Ok(Self {
66 operations: content.operations,
67 })
68 }
69
70 #[cfg(feature = "pdfa-convert")]
72 pub(crate) fn from_stream_strict(stream: &[u8]) -> Result<Self> {
73 let content = crate::inline_image::decode_strict(stream)?;
74 Ok(Self {
75 operations: content.operations,
76 })
77 }
78
79 pub fn from_operations(operations: Vec<Operation>) -> Self {
81 Self { operations }
82 }
83
84 pub fn new() -> Self {
86 Self {
87 operations: Vec::new(),
88 }
89 }
90
91 pub fn operations(&self) -> &[Operation] {
93 &self.operations
94 }
95
96 pub fn operations_mut(&mut self) -> &mut Vec<Operation> {
98 &mut self.operations
99 }
100
101 pub fn len(&self) -> usize {
103 self.operations.len()
104 }
105
106 pub fn is_empty(&self) -> bool {
108 self.operations.is_empty()
109 }
110
111 pub fn remove_range(&mut self, range: Range<usize>) {
113 if range.start < self.operations.len() {
114 let end = range.end.min(self.operations.len());
115 self.operations.drain(range.start..end);
116 }
117 }
118
119 pub fn replace_operation(&mut self, index: usize, ops: Vec<Operation>) {
121 if index < self.operations.len() {
122 self.operations.splice(index..=index, ops);
123 }
124 }
125
126 pub fn insert_operations(&mut self, at: usize, ops: Vec<Operation>) {
128 let at = at.min(self.operations.len());
129 self.operations.splice(at..at, ops);
130 }
131
132 pub fn push(&mut self, op: Operation) {
134 self.operations.push(op);
135 }
136
137 pub fn retain<F>(&mut self, f: F)
139 where
140 F: FnMut(&Operation) -> bool,
141 {
142 self.operations.retain(f);
143 }
144
145 pub fn encode(&self) -> Result<Vec<u8>> {
151 if self.operations.iter().any(|op| op.operator == "BI") {
152 self.encode_with_inline_images()
153 } else {
154 let content = Content {
155 operations: self.operations.clone(),
156 };
157 content
158 .encode()
159 .map_err(|e| ManipError::Other(format!("content encode: {e}")))
160 }
161 }
162
163 fn encode_with_inline_images(&self) -> Result<Vec<u8>> {
165 let mut buf = Vec::new();
168 let mut segment: Vec<Operation> = Vec::new();
169
170 for op in &self.operations {
171 if op.operator == "BI" {
172 if !segment.is_empty() {
174 let content = Content {
175 operations: std::mem::take(&mut segment),
176 };
177 let encoded = content
178 .encode()
179 .map_err(|e| ManipError::Other(format!("content encode: {e}")))?;
180 if !buf.is_empty() {
181 buf.push(b'\n');
182 }
183 buf.extend_from_slice(&encoded);
184 }
185
186 if let Some(Object::Stream(ref stream)) = op.operands.first() {
188 if !buf.is_empty() {
189 buf.push(b'\n');
190 }
191 buf.extend_from_slice(b"BI\n");
192 for (key, val) in &stream.dict {
193 if key == b"Length" {
195 continue;
196 }
197 write_inline_value(&mut buf, &Object::Name(key.clone()))?;
198 buf.push(b' ');
199 write_inline_value(&mut buf, val)?;
200 buf.push(b'\n');
201 }
202 buf.extend_from_slice(b"ID ");
203 buf.extend_from_slice(&stream.content);
204 buf.extend_from_slice(b"\nEI");
205 } else {
206 return Err(ManipError::Other(
207 "inline image operation has no payload".into(),
208 ));
209 }
210 } else {
211 segment.push(op.clone());
212 }
213 }
214
215 if !segment.is_empty() {
217 let content = Content {
218 operations: segment,
219 };
220 let encoded = content
221 .encode()
222 .map_err(|e| ManipError::Other(format!("content encode: {e}")))?;
223 if !buf.is_empty() {
224 buf.push(b'\n');
225 }
226 buf.extend_from_slice(&encoded);
227 }
228
229 Ok(buf)
230 }
231
232 pub fn encode_compressed(&self) -> Result<Vec<u8>> {
234 let raw = self.encode()?;
235 let compressed = compress_flate(&raw);
236 if compressed.len() < raw.len() {
237 Ok(compressed)
238 } else {
239 Ok(raw)
240 }
241 }
242
243 pub fn track_state(&self) -> GraphicsStateTracker {
246 GraphicsStateTracker::from_operations(&self.operations)
247 }
248}
249
250impl Default for ContentEditor {
251 fn default() -> Self {
252 Self::new()
253 }
254}
255
256#[derive(Debug, Clone)]
262pub struct GraphicsSnapshot {
263 pub ctm: [f64; 6],
265 pub fill_color: [f64; 3],
267 pub stroke_color: [f64; 3],
269 pub font_name: String,
271 pub font_size: f64,
273 pub text_matrix: [f64; 6],
275 pub text_line_matrix: [f64; 6],
277 pub char_spacing: f64,
279 pub word_spacing: f64,
281 pub horiz_scaling: f64,
283 pub leading: f64,
285 pub text_rise: f64,
287 pub in_text_object: bool,
289}
290
291impl Default for GraphicsSnapshot {
292 fn default() -> Self {
293 Self {
294 ctm: IDENTITY,
295 fill_color: [0.0; 3],
296 stroke_color: [0.0; 3],
297 font_name: String::new(),
298 font_size: 0.0,
299 text_matrix: IDENTITY,
300 text_line_matrix: IDENTITY,
301 char_spacing: 0.0,
302 word_spacing: 0.0,
303 horiz_scaling: 100.0,
304 leading: 0.0,
305 text_rise: 0.0,
306 in_text_object: false,
307 }
308 }
309}
310
311const IDENTITY: [f64; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
312
313#[derive(Debug, Clone)]
318pub struct GraphicsStateTracker {
319 snapshots: Vec<GraphicsSnapshot>,
320}
321
322impl GraphicsStateTracker {
323 pub fn from_operations(ops: &[Operation]) -> Self {
325 let mut snapshots = Vec::with_capacity(ops.len());
326 let mut state = GraphicsSnapshot::default();
327 let mut gs_stack: Vec<SavedState> = Vec::new();
328
329 for op in ops {
330 snapshots.push(state.clone());
331
332 match op.operator.as_str() {
333 "q" => {
334 gs_stack.push(SavedState {
335 ctm: state.ctm,
336 fill_color: state.fill_color,
337 stroke_color: state.stroke_color,
338 font_name: state.font_name.clone(),
339 font_size: state.font_size,
340 char_spacing: state.char_spacing,
341 word_spacing: state.word_spacing,
342 horiz_scaling: state.horiz_scaling,
343 leading: state.leading,
344 text_rise: state.text_rise,
345 });
346 }
347 "Q" => {
348 if let Some(saved) = gs_stack.pop() {
349 state.ctm = saved.ctm;
350 state.fill_color = saved.fill_color;
351 state.stroke_color = saved.stroke_color;
352 state.font_name = saved.font_name;
353 state.font_size = saved.font_size;
354 state.char_spacing = saved.char_spacing;
355 state.word_spacing = saved.word_spacing;
356 state.horiz_scaling = saved.horiz_scaling;
357 state.leading = saved.leading;
358 state.text_rise = saved.text_rise;
359 }
360 }
361 "cm" => {
362 if let Some(m) = extract_matrix(&op.operands) {
363 state.ctm = concat_cm(&m, &state.ctm);
366 }
367 }
368 "BT" => {
369 state.in_text_object = true;
370 state.text_matrix = IDENTITY;
371 state.text_line_matrix = IDENTITY;
372 }
373 "ET" => {
374 state.in_text_object = false;
375 }
376 "Tf" if op.operands.len() >= 2 => {
377 if let Object::Name(ref name) = op.operands[0] {
378 state.font_name = String::from_utf8_lossy(name).to_string();
379 }
380 if let Some(size) = as_number(&op.operands[1]) {
381 state.font_size = size;
382 }
383 }
384 "Tm" => {
385 if let Some(m) = extract_matrix(&op.operands) {
386 state.text_matrix = m;
387 state.text_line_matrix = m;
388 }
389 }
390 "Td" if op.operands.len() >= 2 => {
391 let tx = as_number(&op.operands[0]).unwrap_or(0.0);
392 let ty = as_number(&op.operands[1]).unwrap_or(0.0);
393 let new_tlm = offset_text_line(&state.text_line_matrix, tx, ty);
394 state.text_line_matrix = new_tlm;
395 state.text_matrix = new_tlm;
396 }
397 "TD" if op.operands.len() >= 2 => {
398 let tx = as_number(&op.operands[0]).unwrap_or(0.0);
399 let ty = as_number(&op.operands[1]).unwrap_or(0.0);
400 state.leading = -ty;
401 let new_tlm = offset_text_line(&state.text_line_matrix, tx, ty);
402 state.text_line_matrix = new_tlm;
403 state.text_matrix = new_tlm;
404 }
405 "T*" => {
406 let new_tlm = offset_text_line(&state.text_line_matrix, 0.0, -state.leading);
407 state.text_line_matrix = new_tlm;
408 state.text_matrix = new_tlm;
409 }
410 "Tc" => {
411 if let Some(v) = op.operands.first().and_then(as_number) {
412 state.char_spacing = v;
413 }
414 }
415 "Tw" => {
416 if let Some(v) = op.operands.first().and_then(as_number) {
417 state.word_spacing = v;
418 }
419 }
420 "Tz" => {
421 if let Some(v) = op.operands.first().and_then(as_number) {
422 state.horiz_scaling = v;
423 }
424 }
425 "TL" => {
426 if let Some(v) = op.operands.first().and_then(as_number) {
427 state.leading = v;
428 }
429 }
430 "Ts" => {
431 if let Some(v) = op.operands.first().and_then(as_number) {
432 state.text_rise = v;
433 }
434 }
435 "rg" if op.operands.len() >= 3 => {
436 state.fill_color = [
437 as_number(&op.operands[0]).unwrap_or(0.0),
438 as_number(&op.operands[1]).unwrap_or(0.0),
439 as_number(&op.operands[2]).unwrap_or(0.0),
440 ];
441 }
442 "g" => {
443 if let Some(v) = op.operands.first().and_then(as_number) {
444 state.fill_color = [v, v, v];
445 }
446 }
447 "RG" if op.operands.len() >= 3 => {
448 state.stroke_color = [
449 as_number(&op.operands[0]).unwrap_or(0.0),
450 as_number(&op.operands[1]).unwrap_or(0.0),
451 as_number(&op.operands[2]).unwrap_or(0.0),
452 ];
453 }
454 "G" => {
455 if let Some(v) = op.operands.first().and_then(as_number) {
456 state.stroke_color = [v, v, v];
457 }
458 }
459 _ => {}
460 }
461 }
462
463 Self { snapshots }
464 }
465
466 pub fn state_at(&self, index: usize) -> Option<&GraphicsSnapshot> {
468 self.snapshots.get(index)
469 }
470
471 pub fn len(&self) -> usize {
473 self.snapshots.len()
474 }
475
476 pub fn is_empty(&self) -> bool {
478 self.snapshots.is_empty()
479 }
480}
481
482pub fn editor_for_page(doc: &Document, page_num: u32) -> Result<ContentEditor> {
488 let pages = doc.get_pages();
489 let total = pages.len() as u32;
490 let &page_id = pages.get(&page_num).ok_or(ManipError::PageOutOfRange(
491 page_num as usize,
492 total as usize,
493 ))?;
494
495 let content_bytes = get_page_content_with_separators(doc, page_id);
501
502 ContentEditor::from_stream(&content_bytes)
503}
504
505fn get_page_content_with_separators(doc: &Document, page_id: ObjectId) -> Vec<u8> {
507 let ids = get_content_stream_ids(doc, page_id);
508 if ids.is_empty() {
509 return Vec::new();
510 }
511 let mut combined = Vec::new();
512 for &id in &ids {
513 if let Ok(Object::Stream(ref s)) = doc.get_object(id) {
514 let mut stream = s.clone();
515 let _ = stream.decompress();
516 if !combined.is_empty() {
517 combined.push(b'\n');
518 }
519 combined.extend_from_slice(&stream.content);
520 }
521 }
522 combined
523}
524
525pub fn write_editor_to_page(
527 doc: &mut Document,
528 page_num: u32,
529 editor: &ContentEditor,
530) -> Result<()> {
531 let pages = doc.get_pages();
532 let total = pages.len() as u32;
533 let &page_id = pages.get(&page_num).ok_or(ManipError::PageOutOfRange(
534 page_num as usize,
535 total as usize,
536 ))?;
537
538 let encoded = editor.encode()?;
539 let compressed = compress_flate(&encoded);
540
541 let (stream_bytes, use_flate) = if compressed.len() < encoded.len() {
542 (compressed, true)
543 } else {
544 (encoded, false)
545 };
546
547 let content_ids = get_content_stream_ids(doc, page_id);
548
549 if let Some(&first_id) = content_ids.first() {
550 if let Ok(Object::Stream(ref mut s)) = doc.get_object_mut(first_id) {
551 s.content = stream_bytes;
552 if use_flate {
553 s.dict.set("Filter", Object::Name(b"FlateDecode".to_vec()));
554 } else {
555 s.dict.remove(b"Filter");
556 }
557 s.dict
558 .set("Length", Object::Integer(s.content.len() as i64));
559 }
560
561 if content_ids.len() > 1 {
562 if let Ok(Object::Dictionary(ref mut page_dict)) = doc.get_object_mut(page_id) {
563 page_dict.set("Contents", Object::Reference(first_id));
564 }
565 }
566 } else {
567 let mut dict = lopdf::Dictionary::new();
568 if use_flate {
569 dict.set("Filter", Object::Name(b"FlateDecode".to_vec()));
570 }
571 dict.set("Length", Object::Integer(stream_bytes.len() as i64));
572 let stream = Stream::new(dict, stream_bytes);
573 let new_id = doc.add_object(Object::Stream(stream));
574
575 if let Ok(Object::Dictionary(ref mut page_dict)) = doc.get_object_mut(page_id) {
576 page_dict.set("Contents", Object::Reference(new_id));
577 }
578 }
579
580 Ok(())
581}
582
583#[derive(Debug, Clone)]
588struct SavedState {
589 ctm: [f64; 6],
590 fill_color: [f64; 3],
591 stroke_color: [f64; 3],
592 font_name: String,
593 font_size: f64,
594 char_spacing: f64,
595 word_spacing: f64,
596 horiz_scaling: f64,
597 leading: f64,
598 text_rise: f64,
599}
600
601pub fn get_content_stream_ids(doc: &Document, page_id: ObjectId) -> Vec<ObjectId> {
603 let page_obj = match doc.get_object(page_id) {
604 Ok(obj) => obj,
605 Err(_) => return Vec::new(),
606 };
607 let page_dict = match page_obj {
608 Object::Dictionary(ref d) => d,
609 _ => return Vec::new(),
610 };
611 match page_dict.get(b"Contents") {
612 Ok(Object::Reference(id)) => {
613 match doc.get_object(*id) {
616 Ok(Object::Array(arr)) => arr
617 .iter()
618 .filter_map(|obj| {
619 if let Object::Reference(ref_id) = obj {
620 Some(*ref_id)
621 } else {
622 None
623 }
624 })
625 .collect(),
626 _ => vec![*id],
627 }
628 }
629 Ok(Object::Array(arr)) => arr
630 .iter()
631 .filter_map(|obj| {
632 if let Object::Reference(id) = obj {
633 Some(*id)
634 } else {
635 None
636 }
637 })
638 .collect(),
639 _ => Vec::new(),
640 }
641}
642
643pub(crate) fn as_number(obj: &Object) -> Option<f64> {
645 match obj {
646 Object::Integer(i) => Some(*i as f64),
647 Object::Real(f) => Some(*f as f64),
648 _ => None,
649 }
650}
651
652fn extract_matrix(operands: &[Object]) -> Option<[f64; 6]> {
654 if operands.len() < 6 {
655 return None;
656 }
657 Some([
658 as_number(&operands[0])?,
659 as_number(&operands[1])?,
660 as_number(&operands[2])?,
661 as_number(&operands[3])?,
662 as_number(&operands[4])?,
663 as_number(&operands[5])?,
664 ])
665}
666
667pub(crate) fn multiply_matrix(m1: &[f64; 6], m2: &[f64; 6]) -> [f64; 6] {
669 [
670 m1[0] * m2[0] + m1[1] * m2[2],
671 m1[0] * m2[1] + m1[1] * m2[3],
672 m1[2] * m2[0] + m1[3] * m2[2],
673 m1[2] * m2[1] + m1[3] * m2[3],
674 m1[4] * m2[0] + m1[5] * m2[2] + m2[4],
675 m1[4] * m2[1] + m1[5] * m2[3] + m2[5],
676 ]
677}
678
679pub(crate) fn concat_cm(m: &[f64; 6], ctm: &[f64; 6]) -> [f64; 6] {
681 multiply_matrix(m, ctm)
682}
683
684pub(crate) fn offset_text_line(tlm: &[f64; 6], tx: f64, ty: f64) -> [f64; 6] {
687 multiply_matrix(&[1.0, 0.0, 0.0, 1.0, tx, ty], tlm)
688}
689
690fn write_inline_value(buf: &mut Vec<u8>, obj: &Object) -> Result<()> {
692 let content = Content {
696 operations: vec![Operation::new("", vec![obj.clone()])],
697 };
698 let mut encoded = content
699 .encode()
700 .map_err(|e| ManipError::Other(format!("inline image value encode: {e}")))?;
701 encoded.pop(); buf.extend_from_slice(&encoded);
703 Ok(())
704}
705
706pub fn strip_inline_images(content: &[u8]) -> (Vec<u8>, Vec<Vec<u8>>) {
717 let mut stripped = Vec::with_capacity(content.len());
718 let mut images: Vec<Vec<u8>> = Vec::new();
719 let mut i = 0;
720
721 while i < content.len() {
722 if i + 2 <= content.len()
724 && content[i] == b'B'
725 && content[i + 1] == b'I'
726 && (i == 0 || is_ws_or_delim(content[i - 1]))
727 && (i + 2 >= content.len() || is_ws_or_delim(content[i + 2]))
728 {
729 if let Some(ei_end) = find_ei_end(&content[i..]) {
730 images.push(content[i..i + ei_end].to_vec());
731 stripped.push(b'\n');
732 i += ei_end;
733 continue;
734 }
735 }
736 stripped.push(content[i]);
737 i += 1;
738 }
739
740 (stripped, images)
741}
742
743fn find_ei_end(content: &[u8]) -> Option<usize> {
746 let mut j = 2; while j + 2 <= content.len() {
749 if content[j] == b'I'
750 && content[j + 1] == b'D'
751 && (j == 0 || is_ws_or_delim(content[j - 1]))
752 && (j + 2 >= content.len()
753 || content[j + 2] == b'\r'
754 || content[j + 2] == b'\n'
755 || is_ws_or_delim(content[j + 2]))
756 {
757 let mut k = j + 2;
759 if k < content.len() && content[k] == b'\r' {
760 k += 1;
761 }
762 if k < content.len() && content[k] == b'\n' {
763 k += 1;
764 }
765 while k + 2 <= content.len() {
767 if content[k] == b'E'
768 && content[k + 1] == b'I'
769 && k > 0
770 && is_ws_or_delim(content[k - 1])
771 && (k + 2 >= content.len() || is_ws_or_delim(content[k + 2]))
772 {
773 return Some(k + 2);
774 }
775 k += 1;
776 }
777 return None;
778 }
779 j += 1;
780 }
781 None
782}
783
784fn is_ws_or_delim(b: u8) -> bool {
785 matches!(
786 b,
787 b' ' | b'\t'
788 | b'\n'
789 | b'\r'
790 | b'\x0C'
791 | b'\x00'
792 | b'('
793 | b')'
794 | b'<'
795 | b'>'
796 | b'['
797 | b']'
798 | b'{'
799 | b'}'
800 | b'/'
801 | b'%'
802 )
803}
804
805fn compress_flate(data: &[u8]) -> Vec<u8> {
807 let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
808 if encoder.write_all(data).is_ok() {
809 encoder.finish().unwrap_or_else(|_| data.to_vec())
810 } else {
811 data.to_vec()
812 }
813}
814
815#[cfg(test)]
816mod tests {
817 use super::*;
818 use lopdf::{dictionary, Document, Object, Stream};
819
820 fn make_doc_with_content(content: &[u8]) -> Document {
821 let mut doc = Document::with_version("1.7");
822 let content_stream = Stream::new(dictionary! {}, content.to_vec());
823 let content_id = doc.add_object(Object::Stream(content_stream));
824 let page_dict = dictionary! {
825 "Type" => "Page",
826 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
827 "Contents" => Object::Reference(content_id),
828 };
829 let page_id = doc.add_object(Object::Dictionary(page_dict));
830 let pages_dict = dictionary! {
831 "Type" => "Pages",
832 "Kids" => vec![Object::Reference(page_id)],
833 "Count" => 1_i64,
834 };
835 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
836 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
837 d.set("Parent", Object::Reference(pages_id));
838 }
839 let catalog = dictionary! {
840 "Type" => "Catalog",
841 "Pages" => Object::Reference(pages_id),
842 };
843 let catalog_id = doc.add_object(Object::Dictionary(catalog));
844 doc.trailer.set("Root", Object::Reference(catalog_id));
845 doc
846 }
847
848 #[test]
849 fn round_trip_preserves_operators() {
850 let input = b"q 1 0 0 1 100 200 cm BT /F1 12 Tf (Hello) Tj ET Q";
851 let editor = ContentEditor::from_stream(input).unwrap();
852 assert!(!editor.is_empty());
853
854 let encoded = editor.encode().unwrap();
855 let editor2 = ContentEditor::from_stream(&encoded).unwrap();
856 assert_eq!(editor.len(), editor2.len());
857
858 for (a, b) in editor.operations().iter().zip(editor2.operations().iter()) {
859 assert_eq!(a.operator, b.operator);
860 assert_eq!(a.operands.len(), b.operands.len());
861 }
862 }
863
864 #[test]
865 fn remove_range() {
866 let input = b"q 1 0 0 RG 0 0 100 100 re S Q";
867 let mut editor = ContentEditor::from_stream(input).unwrap();
868 let original_len = editor.len();
869 editor.remove_range(1..2);
870 assert_eq!(editor.len(), original_len - 1);
871 assert_eq!(editor.operations()[0].operator, "q");
872 assert_eq!(editor.operations()[1].operator, "re");
873 }
874
875 #[test]
876 fn replace_operation() {
877 let input = b"1 0 0 rg 0 0 100 100 re f";
878 let mut editor = ContentEditor::from_stream(input).unwrap();
879 editor.replace_operation(
880 0,
881 vec![Operation::new(
882 "rg",
883 vec![Object::Real(0.0), Object::Real(0.0), Object::Real(1.0)],
884 )],
885 );
886 assert_eq!(editor.operations()[0].operator, "rg");
887 }
888
889 #[test]
890 fn insert_operations() {
891 let input = b"q Q";
892 let mut editor = ContentEditor::from_stream(input).unwrap();
893 assert_eq!(editor.len(), 2);
894 editor.insert_operations(
895 1,
896 vec![
897 Operation::new("BT", vec![]),
898 Operation::new("Tf", vec![Object::Name(b"F1".to_vec()), Object::Real(12.0)]),
899 Operation::new(
900 "Tj",
901 vec![Object::String(
902 b"Hello".to_vec(),
903 lopdf::StringFormat::Literal,
904 )],
905 ),
906 Operation::new("ET", vec![]),
907 ],
908 );
909 assert_eq!(editor.len(), 6);
910 assert_eq!(editor.operations()[0].operator, "q");
911 assert_eq!(editor.operations()[1].operator, "BT");
912 assert_eq!(editor.operations()[5].operator, "Q");
913 }
914
915 #[test]
916 fn retain_filter() {
917 let input = b"q BT /F1 12 Tf (Hello) Tj ET Q";
918 let mut editor = ContentEditor::from_stream(input).unwrap();
919 editor.retain(|op| !matches!(op.operator.as_str(), "Tj" | "TJ" | "'" | "\""));
920 assert!(editor.operations().iter().all(|op| op.operator != "Tj"));
921 }
922
923 #[test]
924 fn state_tracking_font() {
925 let input = b"BT /F1 12 Tf (Hello) Tj /F2 24 Tf (World) Tj ET";
926 let editor = ContentEditor::from_stream(input).unwrap();
927 let tracker = editor.track_state();
928
929 let s = tracker.state_at(2).unwrap();
930 assert_eq!(s.font_name, "F1");
931 assert_eq!(s.font_size, 12.0);
932 assert!(s.in_text_object);
933
934 let s = tracker.state_at(4).unwrap();
935 assert_eq!(s.font_name, "F2");
936 assert_eq!(s.font_size, 24.0);
937 }
938
939 #[test]
940 fn state_tracking_ctm() {
941 let input = b"q 2 0 0 2 10 20 cm Q";
942 let editor = ContentEditor::from_stream(input).unwrap();
943 let tracker = editor.track_state();
944 let s = tracker.state_at(2).unwrap();
945 assert!((s.ctm[0] - 2.0).abs() < 1e-9);
946 assert!((s.ctm[3] - 2.0).abs() < 1e-9);
947 assert!((s.ctm[4] - 10.0).abs() < 1e-9);
948 assert!((s.ctm[5] - 20.0).abs() < 1e-9);
949 }
950
951 fn assert_matrix(actual: &[f64; 6], expected: [f64; 6], what: &str) {
952 for (i, (a, e)) in actual.iter().zip(expected).enumerate() {
953 assert!(
954 (a - e).abs() < 1e-9,
955 "{what}: element {i}: {a} != {e} ({actual:?})"
956 );
957 }
958 }
959
960 #[test]
965 fn nested_cm_with_q_q_compose_in_pdf_order() {
966 let input = b"q 1 0 0 1 100 100 cm q 2 0 0 2 0 0 cm n Q 0.5 0 0 0.5 0 0 cm n Q n";
967 let tracker = ContentEditor::from_stream(input).unwrap().track_state();
969
970 assert_matrix(
972 &tracker.state_at(4).unwrap().ctm,
973 [2.0, 0.0, 0.0, 2.0, 100.0, 100.0],
974 "inner cm",
975 );
976 assert_matrix(
978 &tracker.state_at(6).unwrap().ctm,
979 [1.0, 0.0, 0.0, 1.0, 100.0, 100.0],
980 "after inner Q",
981 );
982 assert_matrix(
984 &tracker.state_at(7).unwrap().ctm,
985 [0.5, 0.0, 0.0, 0.5, 100.0, 100.0],
986 "second inner cm",
987 );
988 assert_matrix(
990 &tracker.state_at(9).unwrap().ctm,
991 [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
992 "after outer Q",
993 );
994 }
995
996 #[test]
1000 fn nested_cm_rotation_then_translation() {
1001 let input = b"0 1 -1 0 0 0 cm 1 0 0 1 10 0 cm n";
1002 let tracker = ContentEditor::from_stream(input).unwrap().track_state();
1003 let ctm = tracker.state_at(2).unwrap().ctm;
1004 assert_matrix(&ctm, [0.0, 1.0, -1.0, 0.0, 0.0, 10.0], "rotate x translate");
1005 let (px, py) = (ctm[4], ctm[5]); assert!((px - 0.0).abs() < 1e-9 && (py - 10.0).abs() < 1e-9);
1007 }
1008
1009 #[test]
1012 fn td_offset_is_scaled_by_the_text_matrix() {
1013 let input = b"BT 2 0 0 2 100 100 Tm 10 5 Td ET";
1014 let tracker = ContentEditor::from_stream(input).unwrap().track_state();
1015 let s = tracker.state_at(3).unwrap();
1016 assert_matrix(
1017 &s.text_matrix,
1018 [2.0, 0.0, 0.0, 2.0, 120.0, 110.0],
1019 "Td after scaled Tm",
1020 );
1021 assert_matrix(
1022 &s.text_line_matrix,
1023 [2.0, 0.0, 0.0, 2.0, 120.0, 110.0],
1024 "Tlm after Td",
1025 );
1026 }
1027
1028 #[test]
1029 fn state_tracking_text_position() {
1030 let input = b"BT 1 0 0 1 100 700 Tm 50 -20 Td ET";
1031 let editor = ContentEditor::from_stream(input).unwrap();
1032 let tracker = editor.track_state();
1033
1034 let s = tracker.state_at(2).unwrap();
1035 assert!((s.text_matrix[4] - 100.0).abs() < 1e-9);
1036 assert!((s.text_matrix[5] - 700.0).abs() < 1e-9);
1037
1038 let s = tracker.state_at(3).unwrap();
1039 assert!((s.text_matrix[4] - 150.0).abs() < 1e-9);
1040 assert!((s.text_matrix[5] - 680.0).abs() < 1e-9);
1041 }
1042
1043 #[test]
1044 fn state_tracking_color() {
1045 let input = b"1 0 0 rg 0 1 0 RG";
1046 let editor = ContentEditor::from_stream(input).unwrap();
1047 let tracker = editor.track_state();
1048 let s = tracker.state_at(1).unwrap();
1049 assert!((s.fill_color[0] - 1.0).abs() < 1e-9);
1050 assert!((s.fill_color[1]).abs() < 1e-9);
1051 }
1052
1053 #[test]
1054 fn state_tracking_save_restore() {
1055 let input = b"q 1 0 0 rg q 0 1 0 rg Q Q";
1056 let editor = ContentEditor::from_stream(input).unwrap();
1057 let tracker = editor.track_state();
1058 let s = tracker.state_at(4).unwrap();
1059 assert!((s.fill_color[1] - 1.0).abs() < 1e-9);
1060 }
1061
1062 #[test]
1063 fn editor_for_page_works() {
1064 let doc = make_doc_with_content(b"BT /F1 12 Tf (Hello) Tj ET");
1065 let editor = editor_for_page(&doc, 1).unwrap();
1066 assert!(!editor.is_empty());
1067 }
1068
1069 #[test]
1070 fn write_editor_to_page_roundtrip() {
1071 let mut doc = make_doc_with_content(b"BT /F1 12 Tf (Hello) Tj ET");
1072 let editor = editor_for_page(&doc, 1).unwrap();
1073 let original_len = editor.len();
1074 write_editor_to_page(&mut doc, 1, &editor).unwrap();
1075 let editor2 = editor_for_page(&doc, 1).unwrap();
1076 assert_eq!(editor2.len(), original_len);
1077 }
1078
1079 #[test]
1080 fn write_modified_content() {
1081 let mut doc = make_doc_with_content(b"BT /F1 12 Tf (Hello) Tj ET");
1082 let mut editor = editor_for_page(&doc, 1).unwrap();
1083 editor.insert_operations(0, vec![Operation::new("q", vec![])]);
1084 editor.push(Operation::new("Q", vec![]));
1085 write_editor_to_page(&mut doc, 1, &editor).unwrap();
1086 let editor2 = editor_for_page(&doc, 1).unwrap();
1087 assert_eq!(editor2.operations()[0].operator, "q");
1088 assert_eq!(editor2.operations()[editor2.len() - 1].operator, "Q");
1089 }
1090
1091 #[test]
1092 fn empty_content_stream() {
1093 let editor = ContentEditor::from_stream(b"").unwrap();
1094 assert!(editor.is_empty());
1095 assert_eq!(editor.len(), 0);
1096 }
1097
1098 #[test]
1099 fn minimal_inline_image_roundtrip() {
1100 let input =
1102 b"q 1 0 0 1 0 0 cm BI /W 4 /H 4 /BPC 1 /CS /DeviceGray ID \xF0\xA0\x50\x0F EI Q";
1103 let editor = ContentEditor::from_stream(input).unwrap();
1104 let encoded = editor.encode().unwrap();
1105 let editor2 = ContentEditor::from_stream(&encoded).unwrap();
1106 assert_eq!(editor.len(), editor2.len());
1107 }
1108
1109 #[test]
1110 fn inline_image_with_text_roundtrip() {
1111 let input = b"q 1 0 0 1 0 0 cm BI /W 4 /H 4 /BPC 1 /CS /DeviceGray ID \xF0\xA0\x50\x0F EI Q BT /F0 12 Tf (Hello World) Tj ET q 1 0 0 1 0 0 cm BI /W 4 /H 4 /BPC 1 /CS /DeviceGray ID \xAA\xBB\xCC\xDD EI Q";
1113 let editor = ContentEditor::from_stream(input).unwrap();
1114 let encoded = editor.encode().unwrap();
1115 let editor2 = ContentEditor::from_stream(&encoded).unwrap();
1116 assert_eq!(editor.len(), editor2.len(), "op count mismatch");
1117 }
1118
1119 #[test]
1120 fn roundtrip_regression_0119() {
1121 let pdf_data = std::fs::read(concat!(
1123 env!("CARGO_MANIFEST_DIR"),
1124 "/../../tests/regression-388/roundtrip_0119.pdf"
1125 ));
1126 let pdf_data = match pdf_data {
1127 Ok(d) => d,
1128 Err(_) => return, };
1130 let doc = Document::load_mem(&pdf_data).unwrap();
1131 let pages = doc.get_pages();
1132 let mut tested = 0;
1133 for (&page_num, &page_id) in &pages {
1134 let content = doc.get_page_content(page_id);
1135 if content.is_empty() {
1136 continue;
1137 }
1138 let editor = match ContentEditor::from_stream(&content) {
1139 Ok(e) => e,
1140 Err(_) => continue, };
1142 let orig_count = editor.len();
1143 let encoded = editor.encode().unwrap();
1144 let editor2 = ContentEditor::from_stream(&encoded).unwrap();
1145 assert_eq!(
1146 orig_count,
1147 editor2.len(),
1148 "page {page_num}: op count mismatch {orig_count} → {}",
1149 editor2.len()
1150 );
1151 tested += 1;
1152 }
1153 assert!(tested > 0, "should test at least one page");
1154 }
1155
1156 #[test]
1157 fn encode_compressed_smaller() {
1158 let mut editor = ContentEditor::new();
1159 for _ in 0..100 {
1160 editor.push(Operation::new(
1161 "rg",
1162 vec![Object::Real(1.0), Object::Real(0.0), Object::Real(0.0)],
1163 ));
1164 }
1165 let raw = editor.encode().unwrap();
1166 let compressed = editor.encode_compressed().unwrap();
1167 assert!(compressed.len() < raw.len());
1168 }
1169}