1use std::fmt;
26use std::ops::{Add, Div, Mul, Neg, Sub};
27
28use serde::{Deserialize, Deserializer, Serialize};
29
30use crate::error::{FoundationError, FoundationResult};
31
32#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
57#[repr(transparent)]
58pub struct HwpUnit(i32);
59
60const _: () = assert!(std::mem::size_of::<HwpUnit>() == 4);
62
63const HWPUNIT_PER_PT: f64 = 100.0;
65
66const HWPUNIT_PER_INCH: f64 = 7200.0;
68
69const HWPUNIT_PER_MM: f64 = 7200.0 / 25.4;
71
72impl HwpUnit {
73 pub const MIN_VALUE: i32 = -100_000_000;
75 pub const MAX_VALUE: i32 = 100_000_000;
77
78 pub const ZERO: Self = Self(0);
80 pub const ONE_PT: Self = Self(100);
82
83 pub fn new(value: i32) -> FoundationResult<Self> {
99 if !(Self::MIN_VALUE..=Self::MAX_VALUE).contains(&value) {
100 return Err(FoundationError::InvalidHwpUnit {
101 value: value as i64, min: Self::MIN_VALUE,
103 max: Self::MAX_VALUE,
104 });
105 }
106 Ok(Self(value))
107 }
108
109 pub(crate) const fn new_unchecked(value: i32) -> Self {
114 Self(value)
115 }
116
117 pub const fn as_i32(self) -> i32 {
119 self.0
120 }
121
122 pub const fn is_zero(&self) -> bool {
126 self.0 == 0
127 }
128
129 pub fn from_pt(pt: f64) -> FoundationResult<Self> {
145 Self::from_f64(pt, HWPUNIT_PER_PT, "pt")
146 }
147
148 pub fn from_mm(mm: f64) -> FoundationResult<Self> {
155 Self::from_f64(mm, HWPUNIT_PER_MM, "mm")
156 }
157
158 pub fn from_inch(inch: f64) -> FoundationResult<Self> {
165 Self::from_f64(inch, HWPUNIT_PER_INCH, "inch")
166 }
167
168 pub fn to_pt(self) -> f64 {
170 self.0 as f64 / HWPUNIT_PER_PT
171 }
172
173 pub fn to_mm(self) -> f64 {
175 self.0 as f64 / HWPUNIT_PER_MM
176 }
177
178 pub fn to_inch(self) -> f64 {
180 self.0 as f64 / HWPUNIT_PER_INCH
181 }
182
183 fn from_f64(value: f64, scale: f64, unit_name: &str) -> FoundationResult<Self> {
185 if !value.is_finite() {
186 return Err(FoundationError::InvalidField {
187 field: unit_name.to_string(),
188 reason: format!("{value} is not finite"),
189 });
190 }
191 let raw = (value * scale).round() as i64;
192 if raw < Self::MIN_VALUE as i64 || raw > Self::MAX_VALUE as i64 {
193 return Err(FoundationError::InvalidHwpUnit {
194 value: raw, min: Self::MIN_VALUE,
196 max: Self::MAX_VALUE,
197 });
198 }
199 Ok(Self(raw as i32))
200 }
201}
202
203impl Default for HwpUnit {
204 fn default() -> Self {
205 Self::ZERO
206 }
207}
208
209impl<'de> Deserialize<'de> for HwpUnit {
210 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
211 let raw = i32::deserialize(deserializer)?;
212 HwpUnit::new(raw).map_err(|_| {
213 serde::de::Error::custom(format!(
214 "HwpUnit out of range: {raw} (must be in [{}, {}])",
215 HwpUnit::MIN_VALUE,
216 HwpUnit::MAX_VALUE
217 ))
218 })
219 }
220}
221
222impl fmt::Debug for HwpUnit {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 write!(f, "HwpUnit({})", self.0)
225 }
226}
227
228impl fmt::Display for HwpUnit {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 write!(f, "{} hwp", self.0)
231 }
232}
233
234impl Add for HwpUnit {
235 type Output = Self;
236 fn add(self, rhs: Self) -> Self {
237 Self(self.0.saturating_add(rhs.0))
238 }
239}
240
241impl Sub for HwpUnit {
242 type Output = Self;
243 fn sub(self, rhs: Self) -> Self {
244 Self(self.0.saturating_sub(rhs.0))
245 }
246}
247
248impl Neg for HwpUnit {
249 type Output = Self;
250 fn neg(self) -> Self {
251 Self(self.0.saturating_neg())
252 }
253}
254
255impl Mul<i32> for HwpUnit {
256 type Output = Self;
257 fn mul(self, rhs: i32) -> Self {
258 Self(self.0.saturating_mul(rhs))
259 }
260}
261
262impl Div<i32> for HwpUnit {
263 type Output = Self;
264 fn div(self, rhs: i32) -> Self {
265 if rhs == 0 {
266 return Self::ZERO;
267 }
268 Self(self.0.saturating_div(rhs))
269 }
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
288pub struct Size {
289 pub width: HwpUnit,
291 pub height: HwpUnit,
293}
294
295const _: () = assert!(std::mem::size_of::<Size>() == 8);
296
297impl Size {
298 pub const A4: Self = Self {
300 width: HwpUnit::new_unchecked(59528), height: HwpUnit::new_unchecked(84188), };
303
304 pub const LETTER: Self = Self {
306 width: HwpUnit::new_unchecked(61200), height: HwpUnit::new_unchecked(79200), };
309
310 pub const B5: Self = Self {
312 width: HwpUnit::new_unchecked(51591), height: HwpUnit::new_unchecked(72850), };
315
316 pub const fn new(width: HwpUnit, height: HwpUnit) -> Self {
318 Self { width, height }
319 }
320}
321
322impl fmt::Display for Size {
323 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324 write!(f, "{} x {}", self.width, self.height)
325 }
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
339pub struct Point {
340 pub x: HwpUnit,
342 pub y: HwpUnit,
344}
345
346const _: () = assert!(std::mem::size_of::<Point>() == 8);
347
348impl Point {
349 pub const ORIGIN: Self = Self { x: HwpUnit::ZERO, y: HwpUnit::ZERO };
351
352 pub const fn new(x: HwpUnit, y: HwpUnit) -> Self {
354 Self { x, y }
355 }
356}
357
358impl fmt::Display for Point {
359 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360 write!(f, "({}, {})", self.x, self.y)
361 }
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
375pub struct Rect {
376 pub origin: Point,
378 pub size: Size,
380}
381
382const _: () = assert!(std::mem::size_of::<Rect>() == 16);
383
384impl Rect {
385 pub const fn new(origin: Point, size: Size) -> Self {
387 Self { origin, size }
388 }
389}
390
391impl fmt::Display for Rect {
392 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393 write!(f, "{} @ {}", self.size, self.origin)
394 }
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
408pub struct Insets {
409 pub top: HwpUnit,
411 pub bottom: HwpUnit,
413 pub left: HwpUnit,
415 pub right: HwpUnit,
417}
418
419const _: () = assert!(std::mem::size_of::<Insets>() == 16);
420
421impl Insets {
422 pub const fn uniform(value: HwpUnit) -> Self {
424 Self { top: value, bottom: value, left: value, right: value }
425 }
426
427 pub const fn symmetric(horizontal: HwpUnit, vertical: HwpUnit) -> Self {
429 Self { top: vertical, bottom: vertical, left: horizontal, right: horizontal }
430 }
431
432 pub const fn new(top: HwpUnit, bottom: HwpUnit, left: HwpUnit, right: HwpUnit) -> Self {
434 Self { top, bottom, left, right }
435 }
436}
437
438impl fmt::Display for Insets {
439 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440 write!(
441 f,
442 "Insets(top={}, bottom={}, left={}, right={})",
443 self.top, self.bottom, self.left, self.right
444 )
445 }
446}
447
448impl schemars::JsonSchema for HwpUnit {
453 fn schema_name() -> std::borrow::Cow<'static, str> {
454 "HwpUnit".into()
455 }
456
457 fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
458 gen.subschema_for::<i32>()
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 #[test]
472 fn hwpunit_zero() {
473 let u = HwpUnit::new(0).unwrap();
474 assert_eq!(u.as_i32(), 0);
475 assert_eq!(u, HwpUnit::ZERO);
476 }
477
478 #[test]
480 fn hwpunit_min_valid() {
481 let u = HwpUnit::new(HwpUnit::MIN_VALUE).unwrap();
482 assert_eq!(u.as_i32(), -100_000_000);
483 }
484
485 #[test]
487 fn hwpunit_max_valid() {
488 let u = HwpUnit::new(HwpUnit::MAX_VALUE).unwrap();
489 assert_eq!(u.as_i32(), 100_000_000);
490 }
491
492 #[test]
494 fn hwpunit_below_min_is_error() {
495 assert!(HwpUnit::new(HwpUnit::MIN_VALUE - 1).is_err());
496 assert!(HwpUnit::new(i32::MIN).is_err());
497 }
498
499 #[test]
501 fn hwpunit_above_max_is_error() {
502 assert!(HwpUnit::new(HwpUnit::MAX_VALUE + 1).is_err());
503 assert!(HwpUnit::new(i32::MAX).is_err());
504 }
505
506 #[test]
508 fn hwpunit_from_pt_infinity_is_error() {
509 assert!(HwpUnit::from_pt(f64::INFINITY).is_err());
510 assert!(HwpUnit::from_pt(f64::NEG_INFINITY).is_err());
511 }
512
513 #[test]
515 fn hwpunit_from_pt_nan_is_error() {
516 assert!(HwpUnit::from_pt(f64::NAN).is_err());
517 }
518
519 #[test]
521 fn hwpunit_from_pt_negative_zero() {
522 let u = HwpUnit::from_pt(-0.0).unwrap();
523 assert_eq!(u.as_i32(), 0);
524 }
525
526 #[test]
528 fn hwpunit_roundtrip_pt() {
529 let u = HwpUnit::from_pt(12.5).unwrap();
530 assert!((u.to_pt() - 12.5).abs() < 0.01);
531 }
532
533 #[test]
535 fn hwpunit_roundtrip_mm() {
536 let u = HwpUnit::from_mm(25.4).unwrap();
537 assert_eq!(u.as_i32(), 7200);
539 assert!((u.to_mm() - 25.4).abs() < 0.01);
540 }
541
542 #[test]
544 fn hwpunit_roundtrip_inch() {
545 let u = HwpUnit::from_inch(1.0).unwrap();
546 assert_eq!(u.as_i32(), 7200);
547 assert!((u.to_inch() - 1.0).abs() < f64::EPSILON);
548 }
549
550 #[test]
553 fn hwpunit_add() {
554 let a = HwpUnit::new(100).unwrap();
555 let b = HwpUnit::new(200).unwrap();
556 assert_eq!((a + b).as_i32(), 300);
557 }
558
559 #[test]
560 fn hwpunit_sub() {
561 let a = HwpUnit::new(300).unwrap();
562 let b = HwpUnit::new(100).unwrap();
563 assert_eq!((a - b).as_i32(), 200);
564 }
565
566 #[test]
567 fn hwpunit_neg() {
568 let a = HwpUnit::new(100).unwrap();
569 assert_eq!((-a).as_i32(), -100);
570 }
571
572 #[test]
573 fn hwpunit_mul_scalar() {
574 let a = HwpUnit::new(100).unwrap();
575 assert_eq!((a * 3).as_i32(), 300);
576 }
577
578 #[test]
579 fn hwpunit_div_scalar() {
580 let a = HwpUnit::new(300).unwrap();
581 assert_eq!((a / 3).as_i32(), 100);
582 }
583
584 #[test]
585 fn hwpunit_add_saturates_on_overflow() {
586 let a = HwpUnit::new_unchecked(i32::MAX);
587 let b = HwpUnit::new_unchecked(1);
588 assert_eq!((a + b).as_i32(), i32::MAX);
589 }
590
591 #[test]
592 fn hwpunit_display() {
593 let u = HwpUnit::new(7200).unwrap();
594 assert_eq!(u.to_string(), "7200 hwp");
595 }
596
597 #[test]
598 fn hwpunit_debug() {
599 let u = HwpUnit::new(100).unwrap();
600 assert_eq!(format!("{u:?}"), "HwpUnit(100)");
601 }
602
603 #[test]
604 fn hwpunit_default_is_zero() {
605 assert_eq!(HwpUnit::default(), HwpUnit::ZERO);
606 }
607
608 #[test]
609 fn hwpunit_ord() {
610 let a = HwpUnit::new(100).unwrap();
611 let b = HwpUnit::new(200).unwrap();
612 assert!(a < b);
613 }
614
615 #[test]
616 fn hwpunit_serde_roundtrip() {
617 let u = HwpUnit::new(1200).unwrap();
618 let json = serde_json::to_string(&u).unwrap();
619 assert_eq!(json, "1200");
620 let back: HwpUnit = serde_json::from_str(&json).unwrap();
621 assert_eq!(back, u);
622 }
623
624 #[test]
629 fn size_a4_dimensions() {
630 let a4 = Size::A4;
632 assert!((HwpUnit(a4.width.as_i32()).to_mm() - 210.0).abs() < 0.1);
633 assert!((HwpUnit(a4.height.as_i32()).to_mm() - 297.0).abs() < 0.1);
634 }
635
636 #[test]
637 fn size_letter_dimensions() {
638 let letter = Size::LETTER;
639 assert_eq!(letter.width.as_i32(), 61200);
640 assert_eq!(letter.height.as_i32(), 79200);
641 }
642
643 #[test]
644 fn size_display() {
645 let s = Size::new(HwpUnit::new_unchecked(100), HwpUnit::new_unchecked(200));
646 assert_eq!(s.to_string(), "100 hwp x 200 hwp");
647 }
648
649 #[test]
650 fn point_origin() {
651 let o = Point::ORIGIN;
652 assert_eq!(o.x, HwpUnit::ZERO);
653 assert_eq!(o.y, HwpUnit::ZERO);
654 }
655
656 #[test]
657 fn point_display() {
658 let p = Point::new(HwpUnit::new_unchecked(10), HwpUnit::new_unchecked(20));
659 assert_eq!(p.to_string(), "(10 hwp, 20 hwp)");
660 }
661
662 #[test]
663 fn rect_construction() {
664 let r = Rect::new(Point::ORIGIN, Size::A4);
665 assert_eq!(r.origin, Point::ORIGIN);
666 assert_eq!(r.size, Size::A4);
667 }
668
669 #[test]
670 fn rect_display() {
671 let r = Rect::new(
672 Point::new(HwpUnit::new_unchecked(1), HwpUnit::new_unchecked(2)),
673 Size::new(HwpUnit::new_unchecked(3), HwpUnit::new_unchecked(4)),
674 );
675 assert_eq!(r.to_string(), "3 hwp x 4 hwp @ (1 hwp, 2 hwp)");
676 }
677
678 #[test]
679 fn insets_uniform() {
680 let ins = Insets::uniform(HwpUnit::ONE_PT);
681 assert_eq!(ins.top, HwpUnit::ONE_PT);
682 assert_eq!(ins.bottom, HwpUnit::ONE_PT);
683 assert_eq!(ins.left, HwpUnit::ONE_PT);
684 assert_eq!(ins.right, HwpUnit::ONE_PT);
685 }
686
687 #[test]
688 fn insets_symmetric() {
689 let h = HwpUnit::new(10).unwrap();
690 let v = HwpUnit::new(20).unwrap();
691 let ins = Insets::symmetric(h, v);
692 assert_eq!(ins.left, h);
693 assert_eq!(ins.right, h);
694 assert_eq!(ins.top, v);
695 assert_eq!(ins.bottom, v);
696 }
697
698 #[test]
699 fn geometry_serde_roundtrip() {
700 let size = Size::A4;
701 let json = serde_json::to_string(&size).unwrap();
702 let back: Size = serde_json::from_str(&json).unwrap();
703 assert_eq!(back, size);
704
705 let rect = Rect::new(Point::ORIGIN, Size::A4);
706 let json = serde_json::to_string(&rect).unwrap();
707 let back: Rect = serde_json::from_str(&json).unwrap();
708 assert_eq!(back, rect);
709 }
710
711 #[test]
712 fn geometry_default_is_zero() {
713 assert_eq!(Size::default(), Size::new(HwpUnit::ZERO, HwpUnit::ZERO));
714 assert_eq!(Point::default(), Point::ORIGIN);
715 }
716
717 use proptest::prelude::*;
722
723 proptest! {
724 #[test]
725 fn prop_hwpunit_pt_roundtrip(pt in -1_000_000.0f64..1_000_000.0f64) {
726 if let Ok(u) = HwpUnit::from_pt(pt) {
727 let back = u.to_pt();
728 prop_assert!((back - pt).abs() < 0.01,
729 "pt={pt}, back={back}, diff={}", (back - pt).abs());
730 }
731 }
732
733 #[test]
734 fn prop_hwpunit_mm_roundtrip(mm in -350.0f64..350.0f64) {
735 if let Ok(u) = HwpUnit::from_mm(mm) {
736 let back = u.to_mm();
737 prop_assert!((back - mm).abs() < 0.01,
738 "mm={mm}, back={back}, diff={}", (back - mm).abs());
739 }
740 }
741
742 #[test]
743 fn prop_hwpunit_inch_roundtrip(inch in -14.0f64..14.0f64) {
744 if let Ok(u) = HwpUnit::from_inch(inch) {
745 let back = u.to_inch();
746 prop_assert!((back - inch).abs() < 0.001,
747 "inch={inch}, back={back}");
748 }
749 }
750 }
751
752 #[test]
757 fn hwpunit_div_by_zero_returns_zero() {
758 let u = HwpUnit::new(300).unwrap();
759 assert_eq!((u / 0).as_i32(), 0);
760 }
761
762 #[test]
763 fn hwpunit_div_min_by_neg_one_saturates() {
764 let u = HwpUnit::new_unchecked(i32::MIN);
766 let result = u / -1;
767 assert_eq!(result.as_i32(), i32::MAX);
768 }
769
770 #[test]
771 fn hwpunit_div_normal_works() {
772 let u = HwpUnit::new(600).unwrap();
773 assert_eq!((u / 2).as_i32(), 300);
774 }
775
776 #[test]
781 fn hwpunit_deser_valid_roundtrip() {
782 let u = HwpUnit::new(42000).unwrap();
783 let json = serde_json::to_string(&u).unwrap();
784 let back: HwpUnit = serde_json::from_str(&json).unwrap();
785 assert_eq!(back, u);
786 }
787
788 #[test]
789 fn hwpunit_deser_out_of_range_is_error() {
790 let err = serde_json::from_str::<HwpUnit>("200000000");
792 assert!(err.is_err(), "expected error for out-of-range value");
793 }
794
795 #[test]
796 fn hwpunit_deser_i32_min_is_error() {
797 let json = format!("{}", i32::MIN);
798 let err = serde_json::from_str::<HwpUnit>(&json);
799 assert!(err.is_err(), "i32::MIN should be rejected");
800 }
801
802 #[test]
803 fn hwpunit_deser_max_valid_boundary() {
804 let json = format!("{}", HwpUnit::MAX_VALUE);
805 let u: HwpUnit = serde_json::from_str(&json).unwrap();
806 assert_eq!(u.as_i32(), HwpUnit::MAX_VALUE);
807 }
808
809 #[test]
810 fn hwpunit_deser_min_valid_boundary() {
811 let json = format!("{}", HwpUnit::MIN_VALUE);
812 let u: HwpUnit = serde_json::from_str(&json).unwrap();
813 assert_eq!(u.as_i32(), HwpUnit::MIN_VALUE);
814 }
815}