1#[cfg(not(feature = "std"))]
26use alloc::{
27 string::{String, ToString},
28 vec::Vec,
29};
30
31use crate::info::Rotation;
32
33#[derive(Debug, thiserror::Error)]
37pub enum TextError {
38 #[error("text layer data too short")]
40 TooShort,
41
42 #[error("text length overflows data")]
44 TextOverflow,
45
46 #[error("invalid UTF-8 in text layer")]
48 InvalidUtf8,
49
50 #[error("zone record truncated at offset {0}")]
52 ZoneTruncated(usize),
53
54 #[error("unknown zone type {0}")]
56 UnknownZoneType(u8),
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64pub enum TextZoneKind {
65 Page,
66 Column,
67 Region,
68 Para,
69 Line,
70 Word,
71 Character,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77pub struct Rect {
78 pub x: u32,
79 pub y: u32,
80 pub width: u32,
81 pub height: u32,
82}
83
84#[derive(Debug, Clone)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87pub struct TextZone {
88 pub kind: TextZoneKind,
90 pub rect: Rect,
92 pub text: String,
94 pub children: Vec<TextZone>,
96}
97
98#[derive(Debug, Clone)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub struct TextLayer {
102 pub text: String,
104 pub zones: Vec<TextZone>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
112#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
113pub struct Paragraph {
114 pub lines: Vec<String>,
117 pub text: String,
122}
123
124impl TextLayer {
127 pub fn transform(
138 &self,
139 page_w: u32,
140 page_h: u32,
141 rotation: Rotation,
142 render_w: u32,
143 render_h: u32,
144 ) -> Self {
145 let (disp_w, disp_h) = match rotation {
146 Rotation::Cw90 | Rotation::Ccw90 => (page_h, page_w),
147 _ => (page_w, page_h),
148 };
149 let t = ZoneTransform {
150 page_w,
151 page_h,
152 rotation,
153 disp_w,
154 disp_h,
155 render_w,
156 render_h,
157 };
158 let zones = self.zones.iter().map(|z| transform_zone(z, &t)).collect();
159 TextLayer {
160 text: self.text.clone(),
161 zones,
162 }
163 }
164
165 pub fn reflowable_text(&self) -> Vec<Paragraph> {
183 let mut out = Vec::new();
184 for chunk in self
185 .text
186 .split(['\u{0000}', '\u{000b}', '\u{001d}', '\u{001f}'])
187 {
188 let lines: Vec<String> = chunk
189 .split('\n')
190 .map(|l| l.trim().to_string())
191 .filter(|l| !l.is_empty())
192 .collect();
193 if lines.is_empty() {
194 continue;
195 }
196 let text = join_paragraph_lines(&lines);
197 out.push(Paragraph { lines, text });
198 }
199 out
200 }
201}
202
203fn join_paragraph_lines(lines: &[String]) -> String {
204 let mut out = String::new();
205 for (i, line) in lines.iter().enumerate() {
206 if i == 0 {
207 out.push_str(line);
208 continue;
209 }
210 let prev_hyphen =
211 out.ends_with('-') && line.chars().next().is_some_and(|c| c.is_ascii_lowercase());
212 if prev_hyphen {
213 out.pop();
214 out.push_str(line);
215 } else {
216 out.push(' ');
217 out.push_str(line);
218 }
219 }
220 out
221}
222
223impl Rect {
226 pub fn rotate(&self, page_w: u32, page_h: u32, rotation: Rotation) -> Self {
232 match rotation {
233 Rotation::None => self.clone(),
234 Rotation::Rot180 => Rect {
235 x: page_w.saturating_sub(self.x.saturating_add(self.width)),
236 y: page_h.saturating_sub(self.y.saturating_add(self.height)),
237 width: self.width,
238 height: self.height,
239 },
240 Rotation::Cw90 => Rect {
243 x: page_h.saturating_sub(self.y.saturating_add(self.height)),
244 y: self.x,
245 width: self.height,
246 height: self.width,
247 },
248 Rotation::Ccw90 => Rect {
251 x: self.y,
252 y: page_w.saturating_sub(self.x.saturating_add(self.width)),
253 width: self.height,
254 height: self.width,
255 },
256 }
257 }
258
259 pub fn scale(&self, from_w: u32, from_h: u32, to_w: u32, to_h: u32) -> Self {
261 if from_w == 0 || from_h == 0 {
262 return self.clone();
263 }
264 Rect {
265 x: (self.x as u64 * to_w as u64 / from_w as u64) as u32,
266 y: (self.y as u64 * to_h as u64 / from_h as u64) as u32,
267 width: (self.width as u64 * to_w as u64 / from_w as u64) as u32,
268 height: (self.height as u64 * to_h as u64 / from_h as u64) as u32,
269 }
270 }
271}
272
273struct ZoneTransform {
278 page_w: u32,
279 page_h: u32,
280 rotation: Rotation,
281 disp_w: u32,
282 disp_h: u32,
283 render_w: u32,
284 render_h: u32,
285}
286
287fn transform_zone(zone: &TextZone, t: &ZoneTransform) -> TextZone {
288 let rotated = zone.rect.rotate(t.page_w, t.page_h, t.rotation);
289 let scaled = rotated.scale(t.disp_w, t.disp_h, t.render_w, t.render_h);
290 let children = zone.children.iter().map(|c| transform_zone(c, t)).collect();
291 TextZone {
292 kind: zone.kind,
293 rect: scaled,
294 text: zone.text.clone(),
295 children,
296 }
297}
298
299pub fn parse_text_layer(data: &[u8], page_height: u32) -> Result<TextLayer, TextError> {
308 parse_text_layer_inner(data, page_height)
309}
310
311fn parse_text_layer_inner(data: &[u8], page_height: u32) -> Result<TextLayer, TextError> {
314 if data.len() < 3 {
315 return Err(TextError::TooShort);
316 }
317
318 let mut pos = 0usize;
319
320 let text_len = read_u24(data, &mut pos).ok_or(TextError::TooShort)?;
322
323 let text_end = pos.checked_add(text_len).ok_or(TextError::TextOverflow)?;
325 if text_end > data.len() {
326 return Err(TextError::TextOverflow);
327 }
328 let text = core::str::from_utf8(data.get(pos..text_end).ok_or(TextError::TextOverflow)?)
329 .map_err(|_| TextError::InvalidUtf8)?
330 .to_string();
331 pos = text_end;
332
333 if pos < data.len() {
335 pos += 1; }
337
338 let mut zones = Vec::new();
340 if pos < data.len() {
341 let zone = parse_zone(data, &mut pos, None, None, &text, page_height)?;
342 zones.push(zone);
343 }
344
345 Ok(TextLayer { text, zones })
346}
347
348#[derive(Clone)]
352struct ZoneCtx {
353 x: i32,
354 y: i32, width: i32,
356 height: i32,
357 text_start: i32,
358 text_len: i32,
359}
360
361fn parse_zone(
362 data: &[u8],
363 pos: &mut usize,
364 parent: Option<&ZoneCtx>,
365 prev: Option<&ZoneCtx>,
366 full_text: &str,
367 page_height: u32,
368) -> Result<TextZone, TextError> {
369 if *pos >= data.len() {
370 return Err(TextError::ZoneTruncated(*pos));
371 }
372
373 let type_byte = *data.get(*pos).ok_or(TextError::ZoneTruncated(*pos))?;
374 *pos += 1;
375
376 let kind = match type_byte {
377 1 => TextZoneKind::Page,
378 2 => TextZoneKind::Column,
379 3 => TextZoneKind::Region,
380 4 => TextZoneKind::Para,
381 5 => TextZoneKind::Line,
382 6 => TextZoneKind::Word,
383 7 => TextZoneKind::Character,
384 other => return Err(TextError::UnknownZoneType(other)),
385 };
386
387 let mut x = read_i16_biased(data, pos).ok_or(TextError::ZoneTruncated(*pos))?;
388 let mut y = read_i16_biased(data, pos).ok_or(TextError::ZoneTruncated(*pos))?;
389 let width = read_i16_biased(data, pos).ok_or(TextError::ZoneTruncated(*pos))?;
390 let height = read_i16_biased(data, pos).ok_or(TextError::ZoneTruncated(*pos))?;
391 let mut text_start = read_i16_biased(data, pos).ok_or(TextError::ZoneTruncated(*pos))?;
392 let text_len = read_i24(data, pos).ok_or(TextError::ZoneTruncated(*pos))?;
393
394 if let Some(prev) = prev {
396 match type_byte {
397 1 | 4 | 5 => {
398 x += prev.x;
400 y = prev.y - (y + height);
401 }
402 _ => {
403 x += prev.x + prev.width;
405 y += prev.y;
406 }
407 }
408 text_start += prev.text_start + prev.text_len;
409 } else if let Some(parent) = parent {
410 x += parent.x;
411 y = parent.y + parent.height - (y + height);
412 text_start += parent.text_start;
413 }
414
415 let tl_y = (page_height as i32)
418 .saturating_sub(y.saturating_add(height))
419 .max(0) as u32;
420 let tl_x = x.max(0) as u32;
421 let tl_w = width.max(0) as u32;
422 let tl_h = height.max(0) as u32;
423
424 let rect = Rect {
425 x: tl_x,
426 y: tl_y,
427 width: tl_w,
428 height: tl_h,
429 };
430
431 let ts = text_start.max(0) as usize;
433 let tl = text_len.max(0) as usize;
434 let zone_text = extract_text_slice(full_text, ts, tl);
435
436 let children_count = read_i24(data, pos)
437 .ok_or(TextError::ZoneTruncated(*pos))?
438 .max(0) as usize;
439
440 let ctx = ZoneCtx {
441 x,
442 y,
443 width,
444 height,
445 text_start,
446 text_len,
447 };
448
449 let mut children = Vec::with_capacity(children_count);
450 let mut prev_child: Option<ZoneCtx> = None;
451
452 for _ in 0..children_count {
453 let child = parse_zone(
454 data,
455 pos,
456 Some(&ctx),
457 prev_child.as_ref(),
458 full_text,
459 page_height,
460 )?;
461 prev_child = Some(ZoneCtx {
462 x: child.rect.x as i32,
463 y: {
464 (page_height as i32).saturating_sub(child.rect.y as i32 + child.rect.height as i32)
467 },
468 width: child.rect.width as i32,
469 height: child.rect.height as i32,
470 text_start: ts as i32,
471 text_len: tl as i32,
472 });
473 children.push(child);
474 }
475
476 Ok(TextZone {
477 kind,
478 rect,
479 text: zone_text,
480 children,
481 })
482}
483
484fn extract_text_slice(full_text: &str, start: usize, len: usize) -> String {
488 let end = start.saturating_add(len).min(full_text.len());
489 let start = start.min(end);
490 let safe_start = (0..=start)
492 .rev()
493 .find(|&i| full_text.is_char_boundary(i))
494 .unwrap_or(0);
495 let safe_end = (end..=full_text.len())
496 .find(|&i| full_text.is_char_boundary(i))
497 .unwrap_or(full_text.len());
498 full_text[safe_start..safe_end].to_string()
499}
500
501fn read_u24(data: &[u8], pos: &mut usize) -> Option<usize> {
505 let b0 = *data.get(*pos)?;
506 let b1 = *data.get(*pos + 1)?;
507 let b2 = *data.get(*pos + 2)?;
508 *pos += 3;
509 Some(((b0 as usize) << 16) | ((b1 as usize) << 8) | (b2 as usize))
510}
511
512fn read_i16_biased(data: &[u8], pos: &mut usize) -> Option<i32> {
514 let b0 = *data.get(*pos)?;
515 let b1 = *data.get(*pos + 1)?;
516 *pos += 2;
517 let raw = u16::from_be_bytes([b0, b1]);
518 Some(raw as i32 - 0x8000)
519}
520
521fn read_i24(data: &[u8], pos: &mut usize) -> Option<i32> {
523 let b0 = *data.get(*pos)? as i32;
524 let b1 = *data.get(*pos + 1)? as i32;
525 let b2 = *data.get(*pos + 2)? as i32;
526 *pos += 3;
527 Some((b0 << 16) | (b1 << 8) | b2)
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
537 fn test_read_u24() {
538 let data = [0x01, 0x02, 0x03];
539 let mut pos = 0;
540 assert_eq!(read_u24(&data, &mut pos), Some(0x010203));
541 assert_eq!(pos, 3);
542 }
543
544 #[test]
545 fn test_read_u24_truncated() {
546 let data = [0x01, 0x02];
547 let mut pos = 0;
548 assert_eq!(read_u24(&data, &mut pos), None);
549 }
550
551 #[test]
552 fn test_read_i16_biased() {
553 let data = [0x80, 0x00]; let mut pos = 0;
555 assert_eq!(read_i16_biased(&data, &mut pos), Some(0));
556 assert_eq!(pos, 2);
557 }
558
559 #[test]
560 fn test_read_i16_biased_negative() {
561 let data = [0x00, 0x00]; let mut pos = 0;
563 assert_eq!(read_i16_biased(&data, &mut pos), Some(-0x8000));
564 }
565
566 #[test]
567 fn test_read_i16_biased_truncated() {
568 let data = [0x80];
569 let mut pos = 0;
570 assert_eq!(read_i16_biased(&data, &mut pos), None);
571 }
572
573 #[test]
574 fn test_read_i24() {
575 let data = [0x00, 0x01, 0x00];
576 let mut pos = 0;
577 assert_eq!(read_i24(&data, &mut pos), Some(256));
578 }
579
580 #[test]
583 fn test_extract_text_slice_basic() {
584 assert_eq!(extract_text_slice("hello world", 0, 5), "hello");
585 assert_eq!(extract_text_slice("hello world", 6, 5), "world");
586 }
587
588 #[test]
589 fn test_extract_text_slice_out_of_bounds() {
590 assert_eq!(extract_text_slice("hello", 10, 5), "");
591 assert_eq!(extract_text_slice("hello", 0, 100), "hello");
592 }
593
594 #[test]
595 fn test_extract_text_slice_utf8_boundary() {
596 let s = "\u{00e9}\u{00e8}"; let result = extract_text_slice(s, 1, 2);
600 assert!(result.is_char_boundary(0));
601 }
602
603 #[test]
604 fn test_extract_text_slice_empty() {
605 assert_eq!(extract_text_slice("", 0, 0), "");
606 assert_eq!(extract_text_slice("abc", 1, 0), "");
607 }
608
609 #[test]
612 fn test_too_short_data() {
613 assert!(matches!(
614 parse_text_layer(&[0x00], 100),
615 Err(TextError::TooShort)
616 ));
617 assert!(matches!(
618 parse_text_layer(&[], 100),
619 Err(TextError::TooShort)
620 ));
621 }
622
623 #[test]
624 fn test_text_overflow() {
625 let data = [0x00, 0x00, 0xFF, 0x41];
627 assert!(matches!(
628 parse_text_layer(&data, 100),
629 Err(TextError::TextOverflow)
630 ));
631 }
632
633 #[test]
634 fn test_invalid_utf8() {
635 let data = [0x00, 0x00, 0x02, 0xFF, 0xFE];
637 assert!(matches!(
638 parse_text_layer(&data, 100),
639 Err(TextError::InvalidUtf8)
640 ));
641 }
642
643 #[test]
644 fn test_unknown_zone_type() {
645 let data = [
647 0x00, 0x00, 0x01, b'A', 0x00, 99, ];
652 assert!(matches!(
653 parse_text_layer(&data, 100),
654 Err(TextError::UnknownZoneType(99))
655 ));
656 }
657
658 #[test]
659 fn test_zone_truncated() {
660 let data = [
662 0x00, 0x00, 0x01, b'A', 0x00, 0x01, 0x80, 0x00, ];
668 assert!(matches!(
669 parse_text_layer(&data, 100),
670 Err(TextError::ZoneTruncated(_))
671 ));
672 }
673
674 #[test]
677 fn test_empty_text_no_zones() {
678 let data = [0x00, 0x00, 0x00];
680 let result = parse_text_layer(&data, 100).unwrap();
681 assert_eq!(result.text, "");
682 assert!(result.zones.is_empty());
683 }
684
685 #[test]
686 fn test_text_only_no_zones() {
687 let data = [
689 0x00, 0x00, 0x05, b'H', b'e', b'l', b'l', b'o', 0x00, ];
693 let result = parse_text_layer(&data, 100).unwrap();
694 assert_eq!(result.text, "Hello");
695 assert!(result.zones.is_empty());
696 }
697
698 fn make_layer(x: u32, y: u32, w: u32, h: u32) -> TextLayer {
701 TextLayer {
702 text: "test".to_string(),
703 zones: vec![TextZone {
704 kind: TextZoneKind::Page,
705 rect: Rect {
706 x,
707 y,
708 width: w,
709 height: h,
710 },
711 text: "test".to_string(),
712 children: vec![],
713 }],
714 }
715 }
716
717 fn rect0(layer: &TextLayer) -> &Rect {
718 &layer.zones[0].rect
719 }
720
721 #[test]
722 fn transform_none_identity() {
723 use crate::info::Rotation;
724 let layer = make_layer(10, 20, 30, 40);
726 let out = layer.transform(100, 200, Rotation::None, 100, 200);
727 assert_eq!(
728 *rect0(&out),
729 Rect {
730 x: 10,
731 y: 20,
732 width: 30,
733 height: 40
734 }
735 );
736 }
737
738 #[test]
739 fn transform_none_scale_2x() {
740 use crate::info::Rotation;
741 let layer = make_layer(10, 20, 30, 40);
742 let out = layer.transform(100, 200, Rotation::None, 200, 400);
743 assert_eq!(
744 *rect0(&out),
745 Rect {
746 x: 20,
747 y: 40,
748 width: 60,
749 height: 80
750 }
751 );
752 }
753
754 #[test]
755 fn transform_rot180() {
756 use crate::info::Rotation;
757 let layer = make_layer(10, 20, 30, 40);
761 let out = layer.transform(100, 200, Rotation::Rot180, 100, 200);
762 assert_eq!(
763 *rect0(&out),
764 Rect {
765 x: 60,
766 y: 140,
767 width: 30,
768 height: 40
769 }
770 );
771 }
772
773 #[test]
774 fn transform_cw90() {
775 use crate::info::Rotation;
776 let layer = make_layer(10, 20, 30, 40);
782 let out = layer.transform(100, 200, Rotation::Cw90, 200, 100);
783 assert_eq!(
784 *rect0(&out),
785 Rect {
786 x: 140,
787 y: 10,
788 width: 40,
789 height: 30
790 }
791 );
792 }
793
794 #[test]
795 fn transform_ccw90() {
796 use crate::info::Rotation;
797 let layer = make_layer(10, 20, 30, 40);
803 let out = layer.transform(100, 200, Rotation::Ccw90, 200, 100);
804 assert_eq!(
805 *rect0(&out),
806 Rect {
807 x: 20,
808 y: 60,
809 width: 40,
810 height: 30
811 }
812 );
813 }
814
815 #[test]
816 fn transform_cw90_then_scale() {
817 use crate::info::Rotation;
818 let layer = make_layer(10, 20, 30, 40);
822 let out = layer.transform(100, 200, Rotation::Cw90, 400, 200);
823 assert_eq!(
824 *rect0(&out),
825 Rect {
826 x: 280,
827 y: 20,
828 width: 80,
829 height: 60
830 }
831 );
832 }
833
834 #[test]
835 fn transform_text_preserved() {
836 use crate::info::Rotation;
837 let layer = make_layer(0, 0, 10, 10);
838 let out = layer.transform(100, 100, Rotation::Cw90, 100, 100);
839 assert_eq!(out.text, "test");
840 assert_eq!(out.zones[0].text, "test");
841 }
842
843 #[test]
844 fn test_single_word_zone() {
845 let text = b"Hi";
847 let mut data = Vec::new();
848 data.extend_from_slice(&[0x00, 0x00, 0x02]);
850 data.extend_from_slice(text);
851 data.push(0x00); data.push(0x01);
855 data.extend_from_slice(&0x8000u16.to_be_bytes()); data.extend_from_slice(&0x8000u16.to_be_bytes()); data.extend_from_slice(&(100u16 + 0x8000u16).wrapping_add(0).to_be_bytes()); let h_val = 50i32 + 0x8000;
860 data.extend_from_slice(&(h_val as u16).to_be_bytes()); data.extend_from_slice(&0x8000u16.to_be_bytes()); data.extend_from_slice(&[0x00, 0x00, 0x02]);
864 data.extend_from_slice(&[0x00, 0x00, 0x00]);
866
867 let result = parse_text_layer(&data, 100).unwrap();
868 assert_eq!(result.text, "Hi");
869 assert_eq!(result.zones.len(), 1);
870 assert_eq!(result.zones[0].kind, TextZoneKind::Page);
871 assert_eq!(result.zones[0].text, "Hi");
872 assert_eq!(result.zones[0].rect.width, 100);
873 assert_eq!(result.zones[0].rect.height, 50);
874 }
875
876 fn layer_with(text: &str) -> TextLayer {
879 TextLayer {
880 text: text.to_string(),
881 zones: Vec::new(),
882 }
883 }
884
885 #[test]
886 fn reflowable_text_splits_on_paragraph_separator() {
887 let layer = layer_with("first line\nsecond line\u{001f}third line\nfourth line");
889 let paras = layer.reflowable_text();
890 assert_eq!(paras.len(), 2);
891 assert_eq!(paras[0].lines, vec!["first line", "second line"]);
892 assert_eq!(paras[0].text, "first line second line");
893 assert_eq!(paras[1].lines, vec!["third line", "fourth line"]);
894 assert_eq!(paras[1].text, "third line fourth line");
895 }
896
897 #[test]
898 fn reflowable_text_joins_soft_hyphen() {
899 let layer = layer_with("a compre-\nhensive guide");
901 let paras = layer.reflowable_text();
902 assert_eq!(paras.len(), 1);
903 assert_eq!(paras[0].text, "a comprehensive guide");
904 }
905
906 #[test]
907 fn reflowable_text_keeps_hyphen_before_uppercase() {
908 let layer = layer_with("Anglo-\nSaxon roots");
912 let paras = layer.reflowable_text();
913 assert_eq!(paras.len(), 1);
914 assert_eq!(paras[0].text, "Anglo- Saxon roots");
915 }
916
917 #[test]
918 fn reflowable_text_treats_all_separator_codes_as_break() {
919 let layer = layer_with("a\u{0000}b\u{000b}c\u{001d}d\u{001f}e");
921 let paras = layer.reflowable_text();
922 assert_eq!(paras.len(), 5);
923 assert_eq!(paras[0].text, "a");
924 assert_eq!(paras[4].text, "e");
925 }
926
927 #[test]
928 fn reflowable_text_skips_empty_paragraphs() {
929 let layer = layer_with("\u{001f}\u{001f}only one\u{001f}");
930 let paras = layer.reflowable_text();
931 assert_eq!(paras.len(), 1);
932 assert_eq!(paras[0].text, "only one");
933 }
934
935 #[test]
936 fn reflowable_text_trims_per_line_whitespace() {
937 let layer = layer_with(" leading\n trailing \n middle ");
938 let paras = layer.reflowable_text();
939 assert_eq!(paras.len(), 1);
940 assert_eq!(paras[0].lines, vec!["leading", "trailing", "middle"]);
941 assert_eq!(paras[0].text, "leading trailing middle");
942 }
943
944 #[test]
947 fn parse_character_zone_type_7() {
948 let mut data = Vec::new();
950 data.extend_from_slice(&[0x00, 0x00, 0x01]); data.push(b'X'); data.push(0x00); data.push(0x07); data.extend_from_slice(&[0x80, 0x00]); data.extend_from_slice(&[0x80, 0x00]); data.extend_from_slice(&[0x80, 0x0A]); data.extend_from_slice(&[0x80, 0x14]); data.extend_from_slice(&[0x80, 0x00]); data.extend_from_slice(&[0x00, 0x00, 0x01]); data.extend_from_slice(&[0x00, 0x00, 0x00]); let result = parse_text_layer(&data, 100).unwrap();
962 assert_eq!(result.zones.len(), 1);
963 assert_eq!(result.zones[0].kind, TextZoneKind::Character);
964 }
965
966 #[test]
969 fn zone_truncated_when_child_data_missing() {
970 let mut data = Vec::new();
972 data.extend_from_slice(&[0x00, 0x00, 0x01]); data.push(b'A'); data.push(0x00); data.push(0x01); data.extend_from_slice(&[0x80, 0x00]); data.extend_from_slice(&[0x80, 0x00]); data.extend_from_slice(&[0x80, 0x64]); data.extend_from_slice(&[0x80, 0x32]); data.extend_from_slice(&[0x80, 0x00]); data.extend_from_slice(&[0x00, 0x00, 0x01]); data.extend_from_slice(&[0x00, 0x00, 0x01]); assert!(matches!(
985 parse_text_layer(&data, 100),
986 Err(TextError::ZoneTruncated(_))
987 ));
988 }
989
990 #[test]
993 fn rect_scale_zero_from_w_returns_clone() {
994 let r = Rect {
995 x: 5,
996 y: 10,
997 width: 20,
998 height: 30,
999 };
1000 assert_eq!(r.scale(0, 100, 200, 200), r);
1001 }
1002
1003 #[test]
1004 fn rect_scale_zero_from_h_returns_clone() {
1005 let r = Rect {
1006 x: 5,
1007 y: 10,
1008 width: 20,
1009 height: 30,
1010 };
1011 assert_eq!(r.scale(100, 0, 200, 200), r);
1012 }
1013}