hwpforge_core/
page.rs

1//! Page settings for document sections.
2//!
3//! [`PageSettings`] defines the physical dimensions of a page: width,
4//! height, and margins. Each section in a document can have its own
5//! page settings (e.g. landscape pages mixed with portrait).
6//!
7//! All measurements use [`HwpUnit`] from Foundation.
8//!
9//! # Examples
10//!
11//! ```
12//! use hwpforge_core::PageSettings;
13//!
14//! let a4 = PageSettings::a4();
15//! assert!(a4.width.to_mm() > 209.0);
16//! assert!(a4.width.to_mm() < 211.0);
17//!
18//! let letter = PageSettings::letter();
19//! assert!(letter.width.to_inch() > 8.4);
20//! ```
21
22use hwpforge_foundation::{GutterType, HwpUnit};
23use schemars::JsonSchema;
24use serde::{Deserialize, Serialize};
25
26/// Physical page dimensions and margins for a section.
27///
28/// Contains 8 [`HwpUnit`] fields covering all geometry a page needs.
29/// `Copy` because it is 32 bytes -- small enough to pass by value.
30///
31/// # Presets
32///
33/// - [`PageSettings::a4()`] -- A4 (210 mm x 297 mm) with 20 mm margins
34/// - [`PageSettings::letter()`] -- US Letter (8.5" x 11") with 1" margins
35///
36/// # Examples
37///
38/// ```
39/// use hwpforge_core::PageSettings;
40/// use hwpforge_foundation::HwpUnit;
41///
42/// let custom = PageSettings {
43///     width: HwpUnit::from_mm(148.0).unwrap(),
44///     height: HwpUnit::from_mm(210.0).unwrap(),
45///     ..PageSettings::a4()
46/// };
47/// assert!(custom.width.to_mm() < 149.0);
48/// ```
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
50pub struct PageSettings {
51    /// Page width.
52    pub width: HwpUnit,
53    /// Page height.
54    pub height: HwpUnit,
55    /// Left margin.
56    pub margin_left: HwpUnit,
57    /// Right margin.
58    pub margin_right: HwpUnit,
59    /// Top margin.
60    pub margin_top: HwpUnit,
61    /// Bottom margin.
62    pub margin_bottom: HwpUnit,
63    /// Header margin (distance from page top to header baseline).
64    pub header_margin: HwpUnit,
65    /// Footer margin (distance from page bottom to footer baseline).
66    pub footer_margin: HwpUnit,
67    /// Binding gutter width. Defaults to 0 (no gutter).
68    #[serde(default, skip_serializing_if = "HwpUnit::is_zero")]
69    pub gutter: HwpUnit,
70    /// Gutter position type. Defaults to `LeftOnly`.
71    #[serde(default)]
72    pub gutter_type: GutterType,
73    /// Whether to mirror left/right margins on even pages (for bound documents).
74    #[serde(default)]
75    pub mirror_margins: bool,
76    /// Whether this section uses landscape orientation.
77    /// When `true`, the encoder outputs `landscape="NARROWLY"` (한글's reversed convention).
78    /// Width/height should remain in portrait order (e.g. 210x297 for A4);
79    /// 한글 handles the rotation internally.
80    #[serde(default)]
81    pub landscape: bool,
82}
83
84// 9 x HwpUnit(i32=4) + GutterType(1) + bool(1) + bool(1) + padding(1) = 40 bytes
85const _: () = assert!(std::mem::size_of::<PageSettings>() == 40);
86
87impl PageSettings {
88    /// A4 paper (210 mm x 297 mm) with 20 mm margins, 10 mm header/footer.
89    ///
90    /// These are the de-facto default settings for Korean government
91    /// documents and the HWP editor's default.
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use hwpforge_core::PageSettings;
97    ///
98    /// let a4 = PageSettings::a4();
99    /// assert!((a4.width.to_mm() - 210.0).abs() < 0.1);
100    /// assert!((a4.height.to_mm() - 297.0).abs() < 0.1);
101    /// ```
102    pub fn a4() -> Self {
103        // round(210 * 7200/25.4) = 59528
104        // round(297 * 7200/25.4) = 84188
105        // round(20 * 7200/25.4)  = 5669
106        // round(10 * 7200/25.4)  = 2835
107        Self {
108            width: HwpUnit::from_mm(210.0).unwrap(),
109            height: HwpUnit::from_mm(297.0).unwrap(),
110            margin_left: HwpUnit::from_mm(20.0).unwrap(),
111            margin_right: HwpUnit::from_mm(20.0).unwrap(),
112            margin_top: HwpUnit::from_mm(20.0).unwrap(),
113            margin_bottom: HwpUnit::from_mm(20.0).unwrap(),
114            header_margin: HwpUnit::from_mm(10.0).unwrap(),
115            footer_margin: HwpUnit::from_mm(10.0).unwrap(),
116            gutter: HwpUnit::ZERO,
117            gutter_type: GutterType::LeftOnly,
118            mirror_margins: false,
119            landscape: false,
120        }
121    }
122
123    /// US Letter (8.5" x 11") with 1" margins, 0.5" header/footer.
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use hwpforge_core::PageSettings;
129    ///
130    /// let letter = PageSettings::letter();
131    /// assert_eq!(letter.width.as_i32(), 61200); // 8.5 * 7200
132    /// assert_eq!(letter.height.as_i32(), 79200); // 11 * 7200
133    /// ```
134    pub fn letter() -> Self {
135        Self {
136            width: HwpUnit::from_inch(8.5).unwrap(),
137            height: HwpUnit::from_inch(11.0).unwrap(),
138            margin_left: HwpUnit::from_inch(1.0).unwrap(),
139            margin_right: HwpUnit::from_inch(1.0).unwrap(),
140            margin_top: HwpUnit::from_inch(1.0).unwrap(),
141            margin_bottom: HwpUnit::from_inch(1.0).unwrap(),
142            header_margin: HwpUnit::from_inch(0.5).unwrap(),
143            footer_margin: HwpUnit::from_inch(0.5).unwrap(),
144            gutter: HwpUnit::ZERO,
145            gutter_type: GutterType::LeftOnly,
146            mirror_margins: false,
147            landscape: false,
148        }
149    }
150
151    /// Returns the printable width (page width minus left and right margins).
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// use hwpforge_core::PageSettings;
157    ///
158    /// let a4 = PageSettings::a4();
159    /// let printable = a4.printable_width();
160    /// // 210mm - 20mm - 20mm = 170mm
161    /// assert!((printable.to_mm() - 170.0).abs() < 0.5);
162    /// ```
163    pub fn printable_width(&self) -> HwpUnit {
164        self.width - self.margin_left - self.margin_right
165    }
166
167    /// Returns the printable height (page height minus top and bottom margins).
168    pub fn printable_height(&self) -> HwpUnit {
169        self.height - self.margin_top - self.margin_bottom
170    }
171}
172
173impl Default for PageSettings {
174    /// Default page settings are A4 with 20 mm margins.
175    fn default() -> Self {
176        Self::a4()
177    }
178}
179
180impl std::fmt::Display for PageSettings {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        write!(f, "PageSettings({:.1}mm x {:.1}mm)", self.width.to_mm(), self.height.to_mm())
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn a4_dimensions() {
192        let a4 = PageSettings::a4();
193        assert!((a4.width.to_mm() - 210.0).abs() < 0.1, "width: {}", a4.width.to_mm());
194        assert!((a4.height.to_mm() - 297.0).abs() < 0.1, "height: {}", a4.height.to_mm());
195    }
196
197    #[test]
198    fn a4_margins() {
199        let a4 = PageSettings::a4();
200        assert!((a4.margin_left.to_mm() - 20.0).abs() < 0.1);
201        assert!((a4.margin_right.to_mm() - 20.0).abs() < 0.1);
202        assert!((a4.margin_top.to_mm() - 20.0).abs() < 0.1);
203        assert!((a4.margin_bottom.to_mm() - 20.0).abs() < 0.1);
204    }
205
206    #[test]
207    fn a4_header_footer_margins() {
208        let a4 = PageSettings::a4();
209        assert!((a4.header_margin.to_mm() - 10.0).abs() < 0.1);
210        assert!((a4.footer_margin.to_mm() - 10.0).abs() < 0.1);
211    }
212
213    #[test]
214    fn letter_dimensions() {
215        let letter = PageSettings::letter();
216        assert_eq!(letter.width.as_i32(), 61200);
217        assert_eq!(letter.height.as_i32(), 79200);
218    }
219
220    #[test]
221    fn letter_margins() {
222        let letter = PageSettings::letter();
223        assert_eq!(letter.margin_left.as_i32(), 7200);
224        assert_eq!(letter.margin_right.as_i32(), 7200);
225        assert_eq!(letter.margin_top.as_i32(), 7200);
226        assert_eq!(letter.margin_bottom.as_i32(), 7200);
227    }
228
229    #[test]
230    fn default_is_a4() {
231        assert_eq!(PageSettings::default(), PageSettings::a4());
232    }
233
234    #[test]
235    fn printable_width() {
236        let a4 = PageSettings::a4();
237        let pw = a4.printable_width();
238        // 210 - 20 - 20 = 170mm
239        assert!((pw.to_mm() - 170.0).abs() < 0.5, "printable width: {}mm", pw.to_mm());
240    }
241
242    #[test]
243    fn printable_height() {
244        let a4 = PageSettings::a4();
245        let ph = a4.printable_height();
246        // 297 - 20 - 20 = 257mm
247        assert!((ph.to_mm() - 257.0).abs() < 0.5, "printable height: {}mm", ph.to_mm());
248    }
249
250    #[test]
251    fn custom_page_with_struct_update() {
252        let custom = PageSettings {
253            width: HwpUnit::from_mm(148.0).unwrap(),
254            height: HwpUnit::from_mm(210.0).unwrap(),
255            ..PageSettings::a4()
256        };
257        assert!((custom.width.to_mm() - 148.0).abs() < 0.1);
258        assert!((custom.height.to_mm() - 210.0).abs() < 0.1);
259        // margins inherited from A4
260        assert!((custom.margin_left.to_mm() - 20.0).abs() < 0.1);
261    }
262
263    #[test]
264    fn size_assertion() {
265        assert_eq!(std::mem::size_of::<PageSettings>(), 40);
266    }
267
268    #[test]
269    fn display_format() {
270        let a4 = PageSettings::a4();
271        let s = a4.to_string();
272        assert!(s.contains("210.0"), "display: {s}");
273        assert!(s.contains("297.0"), "display: {s}");
274    }
275
276    #[test]
277    fn copy_semantics() {
278        let a = PageSettings::a4();
279        let b = a; // Copy
280        assert_eq!(a, b);
281    }
282
283    #[test]
284    fn serde_roundtrip() {
285        let ps = PageSettings::a4();
286        let json = serde_json::to_string(&ps).unwrap();
287        let back: PageSettings = serde_json::from_str(&json).unwrap();
288        assert_eq!(ps, back);
289    }
290
291    #[test]
292    fn letter_serde_roundtrip() {
293        let ps = PageSettings::letter();
294        let json = serde_json::to_string(&ps).unwrap();
295        let back: PageSettings = serde_json::from_str(&json).unwrap();
296        assert_eq!(ps, back);
297    }
298}