Skip to main content

hwpforge_core/
placement.rs

1//! Object-placement metadata shared by every floating drawing object.
2//!
3//! [`ObjectPlacement`] models the HWPX `<hp:pos>` block plus the owning
4//! shape's `textWrap`/`textFlow` attributes: where an object anchors, how
5//! surrounding text wraps around it, and whether it behaves like an inline
6//! character. It is carried by [`Image`](crate::image::Image) and by every
7//! [`Control`](crate::control::Control) drawing-object variant (text boxes,
8//! rectangles, lines, ellipses, polygons, arcs, curves, connect lines,
9//! groups, textart, embedded charts).
10//!
11//! Historically these types lived in `image.rs` under `Image*` names and only
12//! images carried them; the shape variants stored two loose `i32` offsets and
13//! silently dropped the rest of `<hp:pos>`. Promoting the type to a shared
14//! `Object*` vocabulary lets the shape encoders/decoders carry the full
15//! placement instead.
16//!
17//! # Examples
18//!
19//! ```
20//! use hwpforge_core::placement::{ObjectPlacement, ObjectTextWrap};
21//!
22//! let inline = ObjectPlacement::legacy_inline_defaults();
23//! assert!(inline.treat_as_char);
24//! assert_eq!(inline.text_wrap, ObjectTextWrap::TopAndBottom);
25//! ```
26
27use std::borrow::Cow;
28
29use hwpforge_foundation::HwpUnit;
30use schemars::JsonSchema;
31use serde::{Deserialize, Serialize};
32
33/// Optional object-placement metadata for a floating drawing object.
34///
35/// Mirrors the HWPX `<hp:pos>` block (anchor references + offsets +
36/// treat-as-char / overlap / flow flags) together with the owning shape's
37/// `textWrap`/`textFlow` attributes.
38///
39/// [`ObjectPlacement::legacy_inline_defaults`] is the canonical "plain inline
40/// object" value: an image or shape that behaves like a character with no
41/// floating offset. Decoders collapse a placement equal to this default back
42/// to `None`, so the encoder's untouched legacy path emits the historical
43/// inline bytes (mirrors the `Option<ShapeStyle>` collapse pattern in the
44/// HWPX shape-style decoder).
45///
46/// Offsets are signed: Hancom persists negative offsets in HWPX `hp:pos`
47/// as **u32 wrap-around decimal strings** (e.g. `horzOffset="4294965029"`
48/// = −2267, dialog-authored fixture measurement), and in HWP5 as plain
49/// signed `i32`. Codecs must round-trip both encodings losslessly.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
51pub struct ObjectPlacement {
52    /// Text wrapping mode around the object.
53    pub text_wrap: ObjectTextWrap,
54    /// Side flow policy around the wrapped object.
55    pub text_flow: ObjectTextFlow,
56    /// Whether the object behaves like an inline character.
57    pub treat_as_char: bool,
58    /// Whether surrounding text should flow with the object.
59    pub flow_with_text: bool,
60    /// Whether overlapping other objects is allowed.
61    pub allow_overlap: bool,
62    /// Vertical anchor reference for `vert_offset`.
63    pub vert_rel_to: ObjectRelativeTo,
64    /// Horizontal anchor reference for `horz_offset`.
65    pub horz_rel_to: ObjectRelativeTo,
66    /// Vertical offset from `vert_rel_to`.
67    pub vert_offset: HwpUnit,
68    /// Horizontal offset from `horz_rel_to`.
69    pub horz_offset: HwpUnit,
70}
71
72impl ObjectPlacement {
73    /// The canonical inline-object placement: treat-as-char, paragraph-anchored,
74    /// zero offset, no overlap, top-and-bottom wrap.
75    ///
76    /// Used by the pre-placement HWPX image path and by shape decoders as the
77    /// sentinel that collapses to `None`.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use hwpforge_core::placement::{ObjectPlacement, ObjectRelativeTo, ObjectTextFlow};
83    /// use hwpforge_foundation::HwpUnit;
84    ///
85    /// let p = ObjectPlacement::legacy_inline_defaults();
86    /// assert!(p.treat_as_char);
87    /// assert!(!p.allow_overlap);
88    /// assert_eq!(p.text_flow, ObjectTextFlow::BothSides);
89    /// assert_eq!(p.vert_rel_to, ObjectRelativeTo::Para);
90    /// assert_eq!(p.horz_offset, HwpUnit::ZERO);
91    /// ```
92    #[must_use]
93    pub fn legacy_inline_defaults() -> Self {
94        Self {
95            text_wrap: ObjectTextWrap::TopAndBottom,
96            text_flow: ObjectTextFlow::BothSides,
97            treat_as_char: true,
98            flow_with_text: false,
99            allow_overlap: false,
100            vert_rel_to: ObjectRelativeTo::Para,
101            horz_rel_to: ObjectRelativeTo::Para,
102            vert_offset: HwpUnit::ZERO,
103            horz_offset: HwpUnit::ZERO,
104        }
105    }
106}
107
108/// Text wrapping mode for placed objects.
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
110#[non_exhaustive]
111pub enum ObjectTextWrap {
112    /// Place text above and below the object.
113    TopAndBottom,
114    /// Wrap text on the object's sides.
115    Square,
116    /// Place the object behind text.
117    BehindText,
118    /// Place the object in front of text.
119    InFrontOfText,
120    /// Tight text wrapping around the object.
121    Tight,
122    /// Through-style wrapping.
123    Through,
124    /// Any wrap value not modeled explicitly.
125    Other(String),
126}
127
128impl ObjectTextWrap {
129    /// Converts a raw HWPX wrap string into a typed value.
130    pub fn from_hwpx(value: &str) -> Self {
131        match value {
132            "TOP_AND_BOTTOM" => Self::TopAndBottom,
133            "SQUARE" => Self::Square,
134            "BEHIND_TEXT" => Self::BehindText,
135            "IN_FRONT_OF_TEXT" => Self::InFrontOfText,
136            "TIGHT" => Self::Tight,
137            "THROUGH" => Self::Through,
138            other => Self::Other(other.to_string()),
139        }
140    }
141
142    /// Returns the HWPX serialization string for this wrap mode.
143    pub fn as_hwpx_str(&self) -> Cow<'_, str> {
144        match self {
145            Self::TopAndBottom => Cow::Borrowed("TOP_AND_BOTTOM"),
146            Self::Square => Cow::Borrowed("SQUARE"),
147            Self::BehindText => Cow::Borrowed("BEHIND_TEXT"),
148            Self::InFrontOfText => Cow::Borrowed("IN_FRONT_OF_TEXT"),
149            Self::Tight => Cow::Borrowed("TIGHT"),
150            Self::Through => Cow::Borrowed("THROUGH"),
151            Self::Other(value) => Cow::Borrowed(value.as_str()),
152        }
153    }
154}
155
156/// Text flow mode for placed objects.
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
158#[non_exhaustive]
159pub enum ObjectTextFlow {
160    /// Text can flow on both sides.
161    BothSides,
162    /// Text can flow only on the left side.
163    LeftOnly,
164    /// Text can flow only on the right side.
165    RightOnly,
166    /// Use the side with the larger available space.
167    LargestOnly,
168    /// Any flow value not modeled explicitly.
169    Other(String),
170}
171
172impl ObjectTextFlow {
173    /// Converts a raw HWPX flow string into a typed value.
174    pub fn from_hwpx(value: &str) -> Self {
175        match value {
176            "BOTH_SIDES" => Self::BothSides,
177            "LEFT_ONLY" => Self::LeftOnly,
178            "RIGHT_ONLY" => Self::RightOnly,
179            "LARGEST_ONLY" => Self::LargestOnly,
180            other => Self::Other(other.to_string()),
181        }
182    }
183
184    /// Returns the HWPX serialization string for this flow mode.
185    pub fn as_hwpx_str(&self) -> Cow<'_, str> {
186        match self {
187            Self::BothSides => Cow::Borrowed("BOTH_SIDES"),
188            Self::LeftOnly => Cow::Borrowed("LEFT_ONLY"),
189            Self::RightOnly => Cow::Borrowed("RIGHT_ONLY"),
190            Self::LargestOnly => Cow::Borrowed("LARGEST_ONLY"),
191            Self::Other(value) => Cow::Borrowed(value.as_str()),
192        }
193    }
194}
195
196/// Anchor target for object placement offsets.
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
198#[non_exhaustive]
199pub enum ObjectRelativeTo {
200    /// Anchor offsets to the paper.
201    Paper,
202    /// Anchor offsets to the page.
203    Page,
204    /// Anchor offsets to the paragraph.
205    Para,
206    /// Anchor offsets to the column.
207    Column,
208    /// Anchor offsets to the character box.
209    Character,
210    /// Anchor offsets to the line box.
211    Line,
212    /// Any anchor value not modeled explicitly.
213    Other(String),
214}
215
216impl ObjectRelativeTo {
217    /// Converts a raw HWPX anchor string into a typed value.
218    pub fn from_hwpx(value: &str) -> Self {
219        match value {
220            "PAPER" => Self::Paper,
221            "PAGE" => Self::Page,
222            "PARA" => Self::Para,
223            "COLUMN" => Self::Column,
224            "CHAR" => Self::Character,
225            "LINE" => Self::Line,
226            other => Self::Other(other.to_string()),
227        }
228    }
229
230    /// Returns the HWPX serialization string for this anchor mode.
231    pub fn as_hwpx_str(&self) -> Cow<'_, str> {
232        match self {
233            Self::Paper => Cow::Borrowed("PAPER"),
234            Self::Page => Cow::Borrowed("PAGE"),
235            Self::Para => Cow::Borrowed("PARA"),
236            Self::Column => Cow::Borrowed("COLUMN"),
237            Self::Character => Cow::Borrowed("CHAR"),
238            Self::Line => Cow::Borrowed("LINE"),
239            Self::Other(value) => Cow::Borrowed(value.as_str()),
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn legacy_inline_defaults_are_inline() {
250        let p = ObjectPlacement::legacy_inline_defaults();
251        assert!(p.treat_as_char);
252        assert!(!p.flow_with_text);
253        assert!(!p.allow_overlap);
254        assert_eq!(p.text_wrap, ObjectTextWrap::TopAndBottom);
255        assert_eq!(p.text_flow, ObjectTextFlow::BothSides);
256        assert_eq!(p.vert_rel_to, ObjectRelativeTo::Para);
257        assert_eq!(p.horz_rel_to, ObjectRelativeTo::Para);
258        assert_eq!(p.vert_offset, HwpUnit::ZERO);
259        assert_eq!(p.horz_offset, HwpUnit::ZERO);
260    }
261
262    #[test]
263    fn placement_serde_roundtrip() {
264        let p = ObjectPlacement {
265            text_wrap: ObjectTextWrap::Square,
266            text_flow: ObjectTextFlow::RightOnly,
267            treat_as_char: false,
268            flow_with_text: true,
269            allow_overlap: true,
270            vert_rel_to: ObjectRelativeTo::Paper,
271            horz_rel_to: ObjectRelativeTo::Page,
272            vert_offset: HwpUnit::new(1200).unwrap(),
273            horz_offset: HwpUnit::new(3400).unwrap(),
274        };
275        let json = serde_json::to_string(&p).unwrap();
276        let back: ObjectPlacement = serde_json::from_str(&json).unwrap();
277        assert_eq!(p, back);
278    }
279
280    #[test]
281    fn text_wrap_hwpx_roundtrip() {
282        for (s, v) in [
283            ("TOP_AND_BOTTOM", ObjectTextWrap::TopAndBottom),
284            ("SQUARE", ObjectTextWrap::Square),
285            ("BEHIND_TEXT", ObjectTextWrap::BehindText),
286            ("IN_FRONT_OF_TEXT", ObjectTextWrap::InFrontOfText),
287            ("TIGHT", ObjectTextWrap::Tight),
288            ("THROUGH", ObjectTextWrap::Through),
289        ] {
290            assert_eq!(ObjectTextWrap::from_hwpx(s), v);
291            assert_eq!(v.as_hwpx_str(), s);
292        }
293        assert_eq!(ObjectTextWrap::from_hwpx("WEIRD"), ObjectTextWrap::Other("WEIRD".to_string()));
294        assert_eq!(ObjectTextWrap::Other("WEIRD".to_string()).as_hwpx_str(), "WEIRD");
295    }
296
297    #[test]
298    fn text_flow_hwpx_roundtrip() {
299        for (s, v) in [
300            ("BOTH_SIDES", ObjectTextFlow::BothSides),
301            ("LEFT_ONLY", ObjectTextFlow::LeftOnly),
302            ("RIGHT_ONLY", ObjectTextFlow::RightOnly),
303            ("LARGEST_ONLY", ObjectTextFlow::LargestOnly),
304        ] {
305            assert_eq!(ObjectTextFlow::from_hwpx(s), v);
306            assert_eq!(v.as_hwpx_str(), s);
307        }
308        assert_eq!(ObjectTextFlow::from_hwpx("X"), ObjectTextFlow::Other("X".to_string()));
309        assert_eq!(ObjectTextFlow::Other("X".to_string()).as_hwpx_str(), "X");
310    }
311
312    #[test]
313    fn relative_to_hwpx_roundtrip() {
314        for (s, v) in [
315            ("PAPER", ObjectRelativeTo::Paper),
316            ("PAGE", ObjectRelativeTo::Page),
317            ("PARA", ObjectRelativeTo::Para),
318            ("COLUMN", ObjectRelativeTo::Column),
319            ("CHAR", ObjectRelativeTo::Character),
320            ("LINE", ObjectRelativeTo::Line),
321        ] {
322            assert_eq!(ObjectRelativeTo::from_hwpx(s), v);
323            assert_eq!(v.as_hwpx_str(), s);
324        }
325        assert_eq!(ObjectRelativeTo::from_hwpx("Z"), ObjectRelativeTo::Other("Z".to_string()));
326        assert_eq!(ObjectRelativeTo::Other("Z".to_string()).as_hwpx_str(), "Z");
327    }
328}