1use crate::error::Result;
7use crate::parser::content::{ContentOperation, ContentParser};
8use std::collections::VecDeque;
9
10#[derive(Debug, Clone)]
12pub struct TextChunk {
13 pub text: String,
15 pub x: f64,
17 pub y: f64,
19 pub font_size: f64,
21 pub font_name: Option<String>,
23}
24
25#[derive(Debug, Clone)]
27pub struct TextStreamOptions {
28 pub min_font_size: f64,
30 pub max_buffer_size: usize,
32 pub preserve_formatting: bool,
34 pub sort_by_position: bool,
36}
37
38impl Default for TextStreamOptions {
39 fn default() -> Self {
40 Self {
41 min_font_size: 0.0,
42 max_buffer_size: 1024 * 1024, preserve_formatting: true,
44 sort_by_position: true,
45 }
46 }
47}
48
49pub struct TextStreamer {
51 options: TextStreamOptions,
52 buffer: VecDeque<TextChunk>,
53 current_font: Option<String>,
54 current_font_size: f64,
55 current_x: f64,
56 current_y: f64,
57 current_leading: f64,
60}
61
62impl TextStreamer {
63 pub fn new(options: TextStreamOptions) -> Self {
65 Self {
66 options,
67 buffer: VecDeque::new(),
68 current_font: None,
69 current_font_size: 12.0,
70 current_x: 0.0,
71 current_y: 0.0,
72 current_leading: 0.0,
73 }
74 }
75
76 fn emit_text(&self, bytes: &[u8], chunks: &mut Vec<TextChunk>) {
79 if self.current_font_size < self.options.min_font_size {
80 return;
81 }
82 chunks.push(TextChunk {
83 text: String::from_utf8_lossy(bytes).to_string(),
84 x: self.current_x,
85 y: self.current_y,
86 font_size: self.current_font_size,
87 font_name: self.current_font.clone(),
88 });
89 }
90
91 pub fn process_chunk(&mut self, data: &[u8]) -> Result<Vec<TextChunk>> {
93 let operations = ContentParser::parse(data)
94 .map_err(|e| crate::error::PdfError::ParseError(e.to_string()))?;
95
96 let mut chunks = Vec::new();
97
98 for op in operations {
99 match op {
100 ContentOperation::SetFont(name, size) => {
101 self.current_font = Some(name);
102 self.current_font_size = size as f64;
103 }
104 ContentOperation::MoveText(x, y) => {
107 self.current_x += x as f64;
108 self.current_y += y as f64;
109 }
110 ContentOperation::MoveTextSetLeading(x, y) => {
114 self.current_leading = -(y as f64);
115 self.current_x += x as f64;
116 self.current_y += y as f64;
117 }
118 ContentOperation::SetLeading(leading) => {
120 self.current_leading = leading as f64;
121 }
122 ContentOperation::NextLine => {
124 self.current_y -= self.current_leading;
125 }
126 ContentOperation::SetTextMatrix(_a, _b, _c, _d, e, f) => {
129 self.current_x = e as f64;
130 self.current_y = f as f64;
131 }
132 ContentOperation::ShowText(bytes) => {
133 self.emit_text(&bytes, &mut chunks);
134 }
135 ContentOperation::ShowTextArray(elements) => {
140 let mut text = Vec::new();
141 for el in elements {
142 if let crate::parser::content::TextElement::Text(bytes) = el {
143 text.extend_from_slice(&bytes);
144 }
145 }
146 self.emit_text(&text, &mut chunks);
147 }
148 ContentOperation::NextLineShowText(bytes) => {
150 self.current_y -= self.current_leading;
151 self.emit_text(&bytes, &mut chunks);
152 }
153 ContentOperation::SetSpacingNextLineShowText(_aw, _ac, bytes) => {
157 self.current_y -= self.current_leading;
158 self.emit_text(&bytes, &mut chunks);
159 }
160 ContentOperation::BeginText => {
161 self.current_x = 0.0;
162 self.current_y = 0.0;
163 }
164 _ => {} }
166 }
167
168 for chunk in &chunks {
170 self.buffer.push_back(chunk.clone());
171 }
172
173 self.check_buffer_size();
175
176 Ok(chunks)
177 }
178
179 pub fn get_buffered_chunks(&self) -> Vec<TextChunk> {
181 self.buffer.iter().cloned().collect()
182 }
183
184 pub fn clear_buffer(&mut self) {
186 self.buffer.clear();
187 }
188
189 pub fn extract_text(&self) -> String {
191 let mut chunks = self.get_buffered_chunks();
192
193 if self.options.sort_by_position {
194 chunks.sort_by(|a, b| b.y.total_cmp(&a.y).then(a.x.total_cmp(&b.x)));
196 }
197
198 chunks
199 .into_iter()
200 .map(|chunk| chunk.text)
201 .collect::<Vec<_>>()
202 .join(" ")
203 }
204
205 fn check_buffer_size(&mut self) {
206 let total_size: usize = self.buffer.iter().map(|chunk| chunk.text.len()).sum();
207
208 while total_size > self.options.max_buffer_size && !self.buffer.is_empty() {
210 self.buffer.pop_front();
211 }
212 }
213}
214
215pub fn stream_text<F>(content_streams: Vec<Vec<u8>>, mut callback: F) -> Result<()>
217where
218 F: FnMut(TextChunk) -> Result<()>,
219{
220 let mut streamer = TextStreamer::new(TextStreamOptions::default());
221
222 for stream in content_streams {
223 let chunks = streamer.process_chunk(&stream)?;
224 for chunk in chunks {
225 callback(chunk)?;
226 }
227 }
228
229 Ok(())
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn test_text_chunk() {
238 let chunk = TextChunk {
239 text: "Hello".to_string(),
240 x: 100.0,
241 y: 700.0,
242 font_size: 12.0,
243 font_name: Some("Helvetica".to_string()),
244 };
245
246 assert_eq!(chunk.text, "Hello");
247 assert_eq!(chunk.x, 100.0);
248 assert_eq!(chunk.y, 700.0);
249 assert_eq!(chunk.font_size, 12.0);
250 assert_eq!(chunk.font_name, Some("Helvetica".to_string()));
251 }
252
253 #[test]
254 fn test_text_stream_options_default() {
255 let options = TextStreamOptions::default();
256 assert_eq!(options.min_font_size, 0.0);
257 assert_eq!(options.max_buffer_size, 1024 * 1024);
258 assert!(options.preserve_formatting);
259 assert!(options.sort_by_position);
260 }
261
262 #[test]
263 fn test_text_streamer_creation() {
264 let options = TextStreamOptions::default();
265 let streamer = TextStreamer::new(options);
266
267 assert!(streamer.buffer.is_empty());
268 assert_eq!(streamer.current_font_size, 12.0);
269 assert_eq!(streamer.current_x, 0.0);
270 assert_eq!(streamer.current_y, 0.0);
271 }
272
273 #[test]
274 fn test_process_chunk_text() {
275 let mut streamer = TextStreamer::new(TextStreamOptions::default());
276
277 let content = b"BT /F1 14 Tf 100 700 Td (Hello World) Tj ET";
279 let chunks = streamer.process_chunk(content).unwrap();
280
281 assert!(!chunks.is_empty());
282 assert_eq!(chunks[0].text, "Hello World");
283 assert_eq!(chunks[0].font_size, 14.0);
284 }
285
286 #[test]
287 fn test_min_font_size_filter() {
288 let mut options = TextStreamOptions::default();
289 options.min_font_size = 10.0;
290 let mut streamer = TextStreamer::new(options);
291
292 let content = b"BT /F1 8 Tf 100 700 Td (Small Text) Tj ET";
294 let chunks = streamer.process_chunk(content).unwrap();
295 assert!(chunks.is_empty());
296
297 let content = b"BT /F1 12 Tf 100 650 Td (Large Text) Tj ET";
299 let chunks = streamer.process_chunk(content).unwrap();
300 assert_eq!(chunks.len(), 1);
301 assert_eq!(chunks[0].text, "Large Text");
302 }
303
304 #[test]
305 fn test_extract_text_sorted() {
306 let mut streamer = TextStreamer::new(TextStreamOptions::default());
307
308 streamer.buffer.push_back(TextChunk {
310 text: "Bottom".to_string(),
311 x: 100.0,
312 y: 100.0,
313 font_size: 12.0,
314 font_name: None,
315 });
316
317 streamer.buffer.push_back(TextChunk {
318 text: "Top".to_string(),
319 x: 100.0,
320 y: 700.0,
321 font_size: 12.0,
322 font_name: None,
323 });
324
325 streamer.buffer.push_back(TextChunk {
326 text: "Middle".to_string(),
327 x: 100.0,
328 y: 400.0,
329 font_size: 12.0,
330 font_name: None,
331 });
332
333 let text = streamer.extract_text();
334 assert_eq!(text, "Top Middle Bottom");
335 }
336
337 #[test]
338 fn test_buffer_management() {
339 let mut options = TextStreamOptions::default();
340 options.max_buffer_size = 10; let mut streamer = TextStreamer::new(options);
342
343 for i in 0..5 {
345 streamer.buffer.push_back(TextChunk {
346 text: format!("Text{i}"),
347 x: 0.0,
348 y: 0.0,
349 font_size: 12.0,
350 font_name: None,
351 });
352 }
353
354 streamer.check_buffer_size();
355
356 assert!(streamer.buffer.len() < 5);
358 }
359
360 #[test]
361 fn test_stream_text_function() {
362 let content1 = b"BT /F1 12 Tf 100 700 Td (Page 1) Tj ET".to_vec();
363 let content2 = b"BT /F1 12 Tf 100 650 Td (Page 2) Tj ET".to_vec();
364 let streams = vec![content1, content2];
365
366 let mut collected = Vec::new();
367 stream_text(streams, |chunk| {
368 collected.push(chunk.text);
369 Ok(())
370 })
371 .unwrap();
372
373 assert_eq!(collected.len(), 2);
374 assert_eq!(collected[0], "Page 1");
375 assert_eq!(collected[1], "Page 2");
376 }
377
378 #[test]
379 fn test_text_chunk_debug_clone() {
380 let chunk = TextChunk {
381 text: "Test".to_string(),
382 x: 50.0,
383 y: 100.0,
384 font_size: 10.0,
385 font_name: Some("Arial".to_string()),
386 };
387
388 let debug_str = format!("{chunk:?}");
389 assert!(debug_str.contains("TextChunk"));
390 assert!(debug_str.contains("Test"));
391
392 let cloned = chunk.clone();
393 assert_eq!(cloned.text, chunk.text);
394 assert_eq!(cloned.x, chunk.x);
395 assert_eq!(cloned.y, chunk.y);
396 assert_eq!(cloned.font_size, chunk.font_size);
397 assert_eq!(cloned.font_name, chunk.font_name);
398 }
399
400 #[test]
401 fn test_text_stream_options_custom() {
402 let options = TextStreamOptions {
403 min_font_size: 8.0,
404 max_buffer_size: 2048,
405 preserve_formatting: false,
406 sort_by_position: false,
407 };
408
409 assert_eq!(options.min_font_size, 8.0);
410 assert_eq!(options.max_buffer_size, 2048);
411 assert!(!options.preserve_formatting);
412 assert!(!options.sort_by_position);
413 }
414
415 #[test]
416 fn test_text_stream_options_debug_clone() {
417 let options = TextStreamOptions::default();
418
419 let debug_str = format!("{options:?}");
420 assert!(debug_str.contains("TextStreamOptions"));
421
422 let cloned = options.clone();
423 assert_eq!(cloned.min_font_size, options.min_font_size);
424 assert_eq!(cloned.max_buffer_size, options.max_buffer_size);
425 assert_eq!(cloned.preserve_formatting, options.preserve_formatting);
426 assert_eq!(cloned.sort_by_position, options.sort_by_position);
427 }
428
429 #[test]
430 fn test_text_streamer_process_empty_chunk() {
431 let mut streamer = TextStreamer::new(TextStreamOptions::default());
432 let chunks = streamer.process_chunk(b"").unwrap();
433 assert!(chunks.is_empty());
434 }
435
436 #[test]
437 fn test_text_streamer_process_invalid_content() {
438 let mut streamer = TextStreamer::new(TextStreamOptions::default());
439 let content = b"Not valid PDF content";
441 let result = streamer.process_chunk(content);
442 match result {
444 Ok(chunks) => assert!(chunks.is_empty()),
445 Err(_) => {} }
447 }
448
449 #[test]
450 fn test_text_streamer_font_tracking() {
451 let mut streamer = TextStreamer::new(TextStreamOptions::default());
452
453 let content = b"BT /Helvetica-Bold 16 Tf ET";
455 let _ = streamer.process_chunk(content).unwrap();
456
457 assert_eq!(streamer.current_font, Some("Helvetica-Bold".to_string()));
458 assert_eq!(streamer.current_font_size, 16.0);
459 }
460
461 #[test]
462 fn test_text_streamer_position_tracking() {
463 let mut streamer = TextStreamer::new(TextStreamOptions::default());
464
465 let content = b"BT 50 100 Td ET";
467 let _ = streamer.process_chunk(content).unwrap();
468
469 assert_eq!(streamer.current_x, 50.0);
470 assert_eq!(streamer.current_y, 100.0);
471 }
472
473 #[test]
474 fn test_text_streamer_begin_text_resets_position() {
475 let mut streamer = TextStreamer::new(TextStreamOptions::default());
476
477 streamer.current_x = 100.0;
479 streamer.current_y = 200.0;
480
481 let content = b"BT ET";
483 let _ = streamer.process_chunk(content).unwrap();
484
485 assert_eq!(streamer.current_x, 0.0);
486 assert_eq!(streamer.current_y, 0.0);
487 }
488
489 #[test]
490 fn test_text_streamer_clear_buffer() {
491 let mut streamer = TextStreamer::new(TextStreamOptions::default());
492
493 streamer.buffer.push_back(TextChunk {
495 text: "Chunk1".to_string(),
496 x: 0.0,
497 y: 0.0,
498 font_size: 12.0,
499 font_name: None,
500 });
501 streamer.buffer.push_back(TextChunk {
502 text: "Chunk2".to_string(),
503 x: 0.0,
504 y: 0.0,
505 font_size: 12.0,
506 font_name: None,
507 });
508
509 assert_eq!(streamer.buffer.len(), 2);
510
511 streamer.clear_buffer();
512 assert!(streamer.buffer.is_empty());
513 }
514
515 #[test]
516 fn test_text_streamer_get_buffered_chunks() {
517 let mut streamer = TextStreamer::new(TextStreamOptions::default());
518
519 let chunk1 = TextChunk {
520 text: "First".to_string(),
521 x: 10.0,
522 y: 20.0,
523 font_size: 14.0,
524 font_name: Some("Times".to_string()),
525 };
526 let chunk2 = TextChunk {
527 text: "Second".to_string(),
528 x: 30.0,
529 y: 40.0,
530 font_size: 16.0,
531 font_name: Some("Arial".to_string()),
532 };
533
534 streamer.buffer.push_back(chunk1);
535 streamer.buffer.push_back(chunk2);
536
537 let chunks = streamer.get_buffered_chunks();
538 assert_eq!(chunks.len(), 2);
539 assert_eq!(chunks[0].text, "First");
540 assert_eq!(chunks[1].text, "Second");
541 }
542
543 #[test]
544 fn test_extract_text_no_sorting() {
545 let mut options = TextStreamOptions::default();
546 options.sort_by_position = false;
547 let mut streamer = TextStreamer::new(options);
548
549 streamer.buffer.push_back(TextChunk {
551 text: "First".to_string(),
552 x: 200.0,
553 y: 100.0,
554 font_size: 12.0,
555 font_name: None,
556 });
557 streamer.buffer.push_back(TextChunk {
558 text: "Second".to_string(),
559 x: 100.0,
560 y: 200.0,
561 font_size: 12.0,
562 font_name: None,
563 });
564
565 let text = streamer.extract_text();
566 assert_eq!(text, "First Second"); }
568
569 #[test]
570 fn test_extract_text_horizontal_sorting() {
571 let mut streamer = TextStreamer::new(TextStreamOptions::default());
572
573 streamer.buffer.push_back(TextChunk {
575 text: "Right".to_string(),
576 x: 300.0,
577 y: 500.0,
578 font_size: 12.0,
579 font_name: None,
580 });
581 streamer.buffer.push_back(TextChunk {
582 text: "Left".to_string(),
583 x: 100.0,
584 y: 500.0,
585 font_size: 12.0,
586 font_name: None,
587 });
588 streamer.buffer.push_back(TextChunk {
589 text: "Middle".to_string(),
590 x: 200.0,
591 y: 500.0,
592 font_size: 12.0,
593 font_name: None,
594 });
595
596 let text = streamer.extract_text();
597 assert_eq!(text, "Left Middle Right");
598 }
599
600 #[test]
601 fn test_check_buffer_size_edge_cases() {
602 let mut options = TextStreamOptions::default();
603 options.max_buffer_size = 20;
604 let mut streamer = TextStreamer::new(options);
605
606 streamer.buffer.push_back(TextChunk {
608 text: "a".repeat(20),
609 x: 0.0,
610 y: 0.0,
611 font_size: 12.0,
612 font_name: None,
613 });
614
615 streamer.check_buffer_size();
616 assert_eq!(streamer.buffer.len(), 1); streamer.buffer.push_back(TextChunk {
620 text: "b".to_string(),
621 x: 0.0,
622 y: 0.0,
623 font_size: 12.0,
624 font_name: None,
625 });
626
627 streamer.check_buffer_size();
628 assert!(streamer.buffer.len() <= 1);
630 }
631
632 #[test]
633 fn test_stream_text_with_error_callback() {
634 let content = b"BT /F1 12 Tf 100 700 Td (Test) Tj ET".to_vec();
635 let streams = vec![content];
636
637 let result = stream_text(streams, |_chunk| {
638 Err(crate::error::PdfError::ParseError("Test error".to_string()))
639 });
640
641 assert!(result.is_err());
642 }
643
644 #[test]
645 fn test_stream_text_empty_streams() {
646 let streams: Vec<Vec<u8>> = vec![];
647
648 let mut collected = Vec::new();
649 stream_text(streams, |chunk| {
650 collected.push(chunk);
651 Ok(())
652 })
653 .unwrap();
654
655 assert!(collected.is_empty());
656 }
657
658 #[test]
659 fn test_text_chunk_without_font_name() {
660 let chunk = TextChunk {
661 text: "No Font".to_string(),
662 x: 0.0,
663 y: 0.0,
664 font_size: 12.0,
665 font_name: None,
666 };
667
668 assert_eq!(chunk.font_name, None);
669 }
670
671 #[test]
672 fn test_process_chunk_multiple_operations() {
673 let mut streamer = TextStreamer::new(TextStreamOptions::default());
674
675 let content = b"BT /F1 10 Tf 100 700 Td (First) Tj 50 0 Td (Second) Tj ET";
677 let chunks = streamer.process_chunk(content).unwrap();
678
679 assert_eq!(chunks.len(), 2);
680 assert_eq!(chunks[0].text, "First");
681 assert_eq!(chunks[1].text, "Second");
682 assert_eq!(chunks[0].x, 100.0);
683 assert_eq!(chunks[1].x, 150.0); }
685
686 #[test]
687 fn test_buffer_size_calculation() {
688 let mut options = TextStreamOptions::default();
689 options.max_buffer_size = 100;
690 let mut streamer = TextStreamer::new(options);
691
692 for _i in 0..10 {
694 streamer.buffer.push_back(TextChunk {
695 text: "1234567890".to_string(), x: 0.0,
697 y: 0.0,
698 font_size: 12.0,
699 font_name: None,
700 });
701 }
702
703 streamer.check_buffer_size();
705
706 streamer.buffer.push_back(TextChunk {
708 text: "x".to_string(),
709 x: 0.0,
710 y: 0.0,
711 font_size: 12.0,
712 font_name: None,
713 });
714
715 streamer.check_buffer_size();
716
717 let total_size: usize = streamer.buffer.iter().map(|c| c.text.len()).sum();
719 assert!(total_size <= 100);
720 }
721
722 #[test]
723 fn test_text_chunk_extreme_positions() {
724 let chunk = TextChunk {
725 text: "Extreme".to_string(),
726 x: f64::MAX,
727 y: f64::MIN,
728 font_size: 0.1,
729 font_name: Some("TinyFont".to_string()),
730 };
731
732 assert_eq!(chunk.x, f64::MAX);
733 assert_eq!(chunk.y, f64::MIN);
734 assert_eq!(chunk.font_size, 0.1);
735 }
736
737 #[test]
738 fn test_text_streamer_accumulated_position() {
739 let mut streamer = TextStreamer::new(TextStreamOptions::default());
740
741 let content = b"BT 10 20 Td 5 10 Td 15 -5 Td ET";
743 let _ = streamer.process_chunk(content).unwrap();
744
745 assert_eq!(streamer.current_x, 30.0); assert_eq!(streamer.current_y, 25.0); }
748
749 #[test]
750 fn test_process_chunk_with_multiple_font_changes() {
751 let mut streamer = TextStreamer::new(TextStreamOptions::default());
752
753 let content = b"BT /F1 10 Tf (Small) Tj /F2 24 Tf (Large) Tj /F3 16 Tf (Medium) Tj ET";
754 let chunks = streamer.process_chunk(content).unwrap();
755
756 assert_eq!(chunks.len(), 3);
757 assert_eq!(chunks[0].font_size, 10.0);
758 assert_eq!(chunks[1].font_size, 24.0);
759 assert_eq!(chunks[2].font_size, 16.0);
760 }
761
762 #[test]
763 fn test_empty_text_operations() {
764 let mut streamer = TextStreamer::new(TextStreamOptions::default());
765
766 let content = b"BT /F1 12 Tf () Tj ( ) Tj ET";
768 let chunks = streamer.process_chunk(content).unwrap();
769
770 assert_eq!(chunks.len(), 2);
771 assert!(chunks[0].text.is_empty());
772 assert_eq!(chunks[1].text, " ");
773 }
774
775 #[test]
776 fn test_text_with_special_characters() {
777 let mut streamer = TextStreamer::new(TextStreamOptions::default());
778
779 let content = b"BT /F1 12 Tf (\xC3\xA9\xC3\xA0\xC3\xB1) Tj ET"; let chunks = streamer.process_chunk(content).unwrap();
781
782 assert!(!chunks.is_empty());
783 assert!(!chunks[0].text.is_empty());
785 }
786
787 #[test]
788 fn test_sorting_with_equal_positions() {
789 let mut streamer = TextStreamer::new(TextStreamOptions::default());
790
791 for i in 0..3 {
793 streamer.buffer.push_back(TextChunk {
794 text: format!("Text{i}"),
795 x: 100.0,
796 y: 100.0,
797 font_size: 12.0,
798 font_name: None,
799 });
800 }
801
802 let text = streamer.extract_text();
803 assert!(text.contains("Text0"));
805 assert!(text.contains("Text1"));
806 assert!(text.contains("Text2"));
807 }
808
809 #[test]
810 fn test_max_buffer_size_zero() {
811 let mut options = TextStreamOptions::default();
812 options.max_buffer_size = 0;
813 let mut streamer = TextStreamer::new(options);
814
815 streamer.buffer.push_back(TextChunk {
816 text: "Should be removed".to_string(),
817 x: 0.0,
818 y: 0.0,
819 font_size: 12.0,
820 font_name: None,
821 });
822
823 streamer.check_buffer_size();
824 assert!(streamer.buffer.is_empty());
825 }
826
827 #[test]
828 fn test_font_name_with_spaces() {
829 let mut streamer = TextStreamer::new(TextStreamOptions::default());
830
831 let content = b"BT /Times New Roman 14 Tf ET";
832 let result = streamer.process_chunk(content);
833
834 assert!(result.is_ok());
839
840 assert_eq!(streamer.current_font, None);
843 assert_eq!(streamer.current_font_size, 12.0);
844 }
845
846 #[test]
847 fn test_stream_text_with_mixed_content() {
848 let content1 = b"BT /F1 8 Tf (Small) Tj ET".to_vec();
849 let content2 = b"Invalid content".to_vec();
850 let content3 = b"BT /F2 16 Tf (Large) Tj ET".to_vec();
851 let streams = vec![content1, content2, content3];
852
853 let mut collected = Vec::new();
854 let result = stream_text(streams, |chunk| {
855 collected.push(chunk.text);
856 Ok(())
857 });
858
859 assert!(result.is_ok() || result.is_err());
861 }
863
864 #[test]
865 fn test_preserve_formatting_option() {
866 let mut options = TextStreamOptions::default();
867 options.preserve_formatting = false;
868 let streamer = TextStreamer::new(options.clone());
869
870 assert!(!streamer.options.preserve_formatting);
871 assert_eq!(streamer.options.min_font_size, options.min_font_size);
872 }
873
874 #[test]
875 fn test_very_large_font_size() {
876 let mut streamer = TextStreamer::new(TextStreamOptions::default());
877
878 let content = b"BT /F1 9999 Tf (Huge) Tj ET";
879 let chunks = streamer.process_chunk(content).unwrap();
880
881 assert!(!chunks.is_empty());
882 assert_eq!(chunks[0].font_size, 9999.0);
883 assert_eq!(chunks[0].text, "Huge");
884 }
885
886 #[test]
887 fn test_negative_font_size() {
888 let mut options = TextStreamOptions::default();
889 options.min_font_size = -10.0; let mut streamer = TextStreamer::new(options);
891
892 streamer.current_font_size = -5.0;
893 let content = b"BT (Negative) Tj ET";
894 let chunks = streamer.process_chunk(content).unwrap();
895
896 assert!(!chunks.is_empty());
897 assert_eq!(chunks[0].font_size, -5.0);
898 }
899
900 #[test]
901 fn test_text_position_nan_handling() {
902 let mut streamer = TextStreamer::new(TextStreamOptions::default());
903
904 let chunk1 = TextChunk {
906 text: "NaN X".to_string(),
907 x: f64::NAN,
908 y: 100.0,
909 font_size: 12.0,
910 font_name: None,
911 };
912 let chunk2 = TextChunk {
913 text: "NaN Y".to_string(),
914 x: 100.0,
915 y: f64::NAN,
916 font_size: 12.0,
917 font_name: None,
918 };
919
920 streamer.buffer.push_back(chunk1);
921 streamer.buffer.push_back(chunk2);
922
923 let text = streamer.extract_text();
925 assert!(text.contains("NaN"));
926 }
927
928 #[test]
929 fn test_buffer_with_different_font_names() {
930 let mut streamer = TextStreamer::new(TextStreamOptions::default());
931
932 let fonts = ["Arial", "Times", "Courier", "Helvetica"];
933 for (i, font) in fonts.iter().enumerate() {
934 streamer.buffer.push_back(TextChunk {
935 text: format!("Font{i}"),
936 x: 0.0,
937 y: 0.0,
938 font_size: 12.0,
939 font_name: Some((*font).to_string()),
940 });
941 }
942
943 let chunks = streamer.get_buffered_chunks();
944 assert_eq!(chunks.len(), 4);
945 for (i, chunk) in chunks.iter().enumerate() {
946 assert_eq!(chunk.font_name, Some(fonts[i].to_string()));
947 }
948 }
949
950 #[test]
951 fn test_process_chunk_error_propagation() {
952 let mut streamer = TextStreamer::new(TextStreamOptions::default());
953
954 let content = b"\xFF\xFE\xFD\xFC"; let result = streamer.process_chunk(content);
957
958 assert!(result.is_ok() || result.is_err());
960 }
961
962 #[test]
963 fn test_extract_text_empty_buffer() {
964 let streamer = TextStreamer::new(TextStreamOptions::default());
965 let text = streamer.extract_text();
966 assert!(text.is_empty());
967 }
968
969 #[test]
970 fn test_extract_text_single_chunk() {
971 let mut streamer = TextStreamer::new(TextStreamOptions::default());
972
973 streamer.buffer.push_back(TextChunk {
974 text: "Single".to_string(),
975 x: 0.0,
976 y: 0.0,
977 font_size: 12.0,
978 font_name: None,
979 });
980
981 let text = streamer.extract_text();
982 assert_eq!(text, "Single");
983 }
984
985 #[test]
986 fn test_check_buffer_size_empty() {
987 let mut streamer = TextStreamer::new(TextStreamOptions::default());
988 streamer.check_buffer_size(); assert!(streamer.buffer.is_empty());
990 }
991
992 #[test]
993 fn test_complex_content_operations() {
994 let mut streamer = TextStreamer::new(TextStreamOptions::default());
995
996 let content = b"BT /F1 12 Tf 0 0 Td (Start) Tj ET q Q BT 50 50 Td (End) Tj ET";
998 let chunks = streamer.process_chunk(content).unwrap();
999
1000 assert_eq!(chunks.len(), 2);
1001 assert_eq!(chunks[0].text, "Start");
1002 assert_eq!(chunks[1].text, "End");
1003 assert_eq!(chunks[0].x, 0.0);
1004 assert_eq!(chunks[1].x, 50.0);
1005 }
1006
1007 #[test]
1008 fn test_stream_text_callback_state() {
1009 let content = b"BT /F1 12 Tf (Test) Tj ET".to_vec();
1010 let streams = vec![content; 3]; let mut count = 0;
1013 stream_text(streams, |_chunk| {
1014 count += 1;
1015 Ok(())
1016 })
1017 .unwrap();
1018
1019 assert_eq!(count, 3);
1020 }
1021}