1use hwpforge_foundation::{
26 ArcType, ArrowSize, ArrowType, BookmarkType, Color, CurveSegmentType, DropCapStyle, FieldType,
27 Flip, GradientType, HwpUnit, ImageFillMode, PatternType, RefContentType, RefType,
28};
29use schemars::JsonSchema;
30use serde::{Deserialize, Serialize};
31
32use crate::caption::Caption;
33use crate::chart::{
34 BarShape, ChartData, ChartGrouping, ChartType, LegendPosition, OfPieType, RadarStyle,
35 ScatterStyle, StockVariant,
36};
37use crate::error::{CoreError, CoreResult};
38use crate::paragraph::Paragraph;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
45pub struct ShapePoint {
46 pub x: i32,
48 pub y: i32,
50}
51
52impl ShapePoint {
53 pub fn new(x: i32, y: i32) -> Self {
65 Self { x, y }
66 }
67}
68
69#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
84#[non_exhaustive]
85pub enum LineStyle {
86 #[default]
88 Solid,
89 Dash,
91 Dot,
93 DashDot,
95 DashDotDot,
97 None,
99}
100
101impl std::fmt::Display for LineStyle {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 match self {
104 Self::Solid => f.write_str("SOLID"),
105 Self::Dash => f.write_str("DASH"),
106 Self::Dot => f.write_str("DOT"),
107 Self::DashDot => f.write_str("DASH_DOT"),
108 Self::DashDotDot => f.write_str("DASH_DOT_DOT"),
109 Self::None => f.write_str("NONE"),
110 }
111 }
112}
113
114impl std::str::FromStr for LineStyle {
115 type Err = CoreError;
116
117 fn from_str(s: &str) -> Result<Self, Self::Err> {
118 match s {
119 "SOLID" | "Solid" | "solid" => Ok(Self::Solid),
120 "DASH" | "Dash" | "dash" => Ok(Self::Dash),
121 "DOT" | "Dot" | "dot" => Ok(Self::Dot),
122 "DASH_DOT" | "DashDot" | "dash_dot" => Ok(Self::DashDot),
123 "DASH_DOT_DOT" | "DashDotDot" | "dash_dot_dot" => Ok(Self::DashDotDot),
124 "NONE" | "None" | "none" => Ok(Self::None),
125 _ => Err(CoreError::InvalidStructure {
126 context: "LineStyle".to_string(),
127 reason: format!(
128 "unknown line style '{s}', valid: SOLID, DASH, DOT, DASH_DOT, DASH_DOT_DOT, NONE"
129 ),
130 }),
131 }
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
150pub struct ArrowStyle {
151 pub arrow_type: ArrowType,
153 pub size: ArrowSize,
155 pub filled: bool,
157}
158
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
172#[non_exhaustive]
173pub enum Fill {
174 Solid {
176 color: Color,
178 },
179 Gradient {
181 gradient_type: GradientType,
183 angle: i32,
185 colors: Vec<(Color, u32)>,
187 },
188 Pattern {
190 pattern_type: PatternType,
192 fg_color: Color,
194 bg_color: Color,
196 },
197 Image {
199 image_id: String,
201 mode: ImageFillMode,
203 },
204}
205
206#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
226pub struct ShapeStyle {
227 pub line_color: Option<Color>,
229 pub fill_color: Option<Color>,
232 pub line_width: Option<u32>,
234 pub line_style: Option<LineStyle>,
236 pub rotation: Option<f32>,
238 pub flip: Option<Flip>,
240 pub head_arrow: Option<ArrowStyle>,
242 pub tail_arrow: Option<ArrowStyle>,
244 pub fill: Option<Fill>,
246 #[serde(default)]
249 pub drop_cap_style: DropCapStyle,
250}
251
252#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
278#[non_exhaustive]
279pub enum Control {
280 TextBox {
283 paragraphs: Vec<Paragraph>,
285 width: HwpUnit,
287 height: HwpUnit,
289 horz_offset: i32,
291 vert_offset: i32,
293 caption: Option<Caption>,
295 style: Option<ShapeStyle>,
297 },
298
299 Hyperlink {
301 text: String,
303 url: String,
305 },
306
307 Footnote {
310 inst_id: Option<u32>,
312 paragraphs: Vec<Paragraph>,
314 },
315
316 Endnote {
319 inst_id: Option<u32>,
321 paragraphs: Vec<Paragraph>,
323 },
324
325 Line {
328 start: ShapePoint,
330 end: ShapePoint,
332 width: HwpUnit,
334 height: HwpUnit,
336 horz_offset: i32,
338 vert_offset: i32,
340 caption: Option<Caption>,
342 style: Option<ShapeStyle>,
344 },
345
346 Ellipse {
349 center: ShapePoint,
351 axis1: ShapePoint,
353 axis2: ShapePoint,
355 width: HwpUnit,
357 height: HwpUnit,
359 horz_offset: i32,
361 vert_offset: i32,
363 paragraphs: Vec<Paragraph>,
365 caption: Option<Caption>,
367 style: Option<ShapeStyle>,
369 },
370
371 EmbeddedChart {
385 chart_xml: String,
387 ole_bytes: Vec<u8>,
389 width: HwpUnit,
391 height: HwpUnit,
393 horz_offset: i32,
395 vert_offset: i32,
397 },
398
399 Rect {
406 width: HwpUnit,
408 height: HwpUnit,
410 horz_offset: i32,
412 vert_offset: i32,
414 caption: Option<Caption>,
416 style: Option<ShapeStyle>,
418 },
419
420 Polygon {
423 vertices: Vec<ShapePoint>,
425 width: HwpUnit,
427 height: HwpUnit,
429 horz_offset: i32,
431 vert_offset: i32,
433 paragraphs: Vec<Paragraph>,
435 caption: Option<Caption>,
437 style: Option<ShapeStyle>,
439 },
440
441 Equation {
447 script: String,
449 width: HwpUnit,
451 height: HwpUnit,
453 base_line: u32,
455 text_color: Color,
457 font: String,
459 },
460
461 Chart {
466 chart_type: ChartType,
468 data: ChartData,
470 width: HwpUnit,
472 height: HwpUnit,
474 title: Option<String>,
476 legend: LegendPosition,
478 grouping: ChartGrouping,
480 bar_shape: Option<BarShape>,
482 explosion: Option<u32>,
484 of_pie_type: Option<OfPieType>,
486 radar_style: Option<RadarStyle>,
488 wireframe: Option<bool>,
490 bubble_3d: Option<bool>,
492 scatter_style: Option<ScatterStyle>,
494 show_markers: Option<bool>,
496 stock_variant: Option<StockVariant>,
501 },
502
503 Dutmal {
506 main_text: String,
508 sub_text: String,
510 position: DutmalPosition,
512 sz_ratio: u32,
514 align: DutmalAlign,
516 },
517
518 Compose {
521 compose_text: String,
523 circle_type: String,
525 char_sz: i32,
527 compose_type: String,
529 },
530
531 Arc {
534 arc_type: ArcType,
536 center: ShapePoint,
538 axis1: ShapePoint,
540 axis2: ShapePoint,
542 start1: ShapePoint,
544 end1: ShapePoint,
546 start2: ShapePoint,
548 end2: ShapePoint,
550 width: HwpUnit,
552 height: HwpUnit,
554 horz_offset: i32,
556 vert_offset: i32,
558 caption: Option<Caption>,
560 style: Option<ShapeStyle>,
562 },
563
564 Curve {
567 points: Vec<ShapePoint>,
569 segment_types: Vec<CurveSegmentType>,
571 width: HwpUnit,
573 height: HwpUnit,
575 horz_offset: i32,
577 vert_offset: i32,
579 caption: Option<Caption>,
581 style: Option<ShapeStyle>,
583 },
584
585 ConnectLine {
588 start: ShapePoint,
590 end: ShapePoint,
592 control_points: Vec<ShapePoint>,
594 connect_type: String,
596 width: HwpUnit,
598 height: HwpUnit,
600 horz_offset: i32,
602 vert_offset: i32,
604 caption: Option<Caption>,
606 style: Option<ShapeStyle>,
608 },
609
610 Bookmark {
613 name: String,
615 bookmark_type: BookmarkType,
617 },
618
619 CrossRef {
622 target_name: String,
624 ref_type: RefType,
626 content_type: RefContentType,
628 as_hyperlink: bool,
630 },
631
632 Field {
635 field_type: FieldType,
637 hint_text: Option<String>,
639 help_text: Option<String>,
641 },
642
643 Memo {
646 content: Vec<Paragraph>,
648 author: String,
650 date: String,
652 },
653
654 IndexMark {
657 primary: String,
659 secondary: Option<String>,
661 },
662
663 Unknown {
668 tag: String,
670 data: Option<String>,
672 },
673}
674
675#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
677#[non_exhaustive]
678pub enum DutmalPosition {
679 #[default]
681 Top,
682 Bottom,
684 Right,
686 Left,
688}
689
690#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
692#[non_exhaustive]
693pub enum DutmalAlign {
694 #[default]
696 Center,
697 Left,
699 Right,
701}
702
703impl Control {
704 pub fn is_text_box(&self) -> bool {
706 matches!(self, Self::TextBox { .. })
707 }
708
709 pub fn is_hyperlink(&self) -> bool {
711 matches!(self, Self::Hyperlink { .. })
712 }
713
714 pub fn is_footnote(&self) -> bool {
716 matches!(self, Self::Footnote { .. })
717 }
718
719 pub fn is_endnote(&self) -> bool {
721 matches!(self, Self::Endnote { .. })
722 }
723
724 pub fn is_line(&self) -> bool {
726 matches!(self, Self::Line { .. })
727 }
728
729 pub fn is_ellipse(&self) -> bool {
731 matches!(self, Self::Ellipse { .. })
732 }
733
734 pub fn is_rect(&self) -> bool {
736 matches!(self, Self::Rect { .. })
737 }
738
739 pub fn is_polygon(&self) -> bool {
741 matches!(self, Self::Polygon { .. })
742 }
743
744 pub fn is_equation(&self) -> bool {
746 matches!(self, Self::Equation { .. })
747 }
748
749 pub fn is_chart(&self) -> bool {
751 matches!(self, Self::Chart { .. })
752 }
753
754 pub fn is_embedded_chart(&self) -> bool {
756 matches!(self, Self::EmbeddedChart { .. })
757 }
758
759 pub fn is_unknown(&self) -> bool {
761 matches!(self, Self::Unknown { .. })
762 }
763
764 pub fn is_dutmal(&self) -> bool {
766 matches!(self, Self::Dutmal { .. })
767 }
768
769 pub fn is_compose(&self) -> bool {
771 matches!(self, Self::Compose { .. })
772 }
773
774 pub fn is_arc(&self) -> bool {
776 matches!(self, Self::Arc { .. })
777 }
778
779 pub fn is_curve(&self) -> bool {
781 matches!(self, Self::Curve { .. })
782 }
783
784 pub fn is_connect_line(&self) -> bool {
786 matches!(self, Self::ConnectLine { .. })
787 }
788
789 pub fn is_bookmark(&self) -> bool {
791 matches!(self, Self::Bookmark { .. })
792 }
793
794 pub fn is_cross_ref(&self) -> bool {
796 matches!(self, Self::CrossRef { .. })
797 }
798
799 pub fn is_field(&self) -> bool {
801 matches!(self, Self::Field { .. })
802 }
803
804 pub fn is_memo(&self) -> bool {
806 matches!(self, Self::Memo { .. })
807 }
808
809 pub fn is_index_mark(&self) -> bool {
811 matches!(self, Self::IndexMark { .. })
812 }
813
814 pub fn bookmark(name: &str) -> Self {
825 Self::Bookmark { name: name.to_string(), bookmark_type: BookmarkType::Point }
826 }
827
828 pub fn field(hint: &str) -> Self {
839 Self::Field {
840 field_type: FieldType::ClickHere,
841 hint_text: Some(hint.to_string()),
842 help_text: None,
843 }
844 }
845
846 pub fn index_mark(primary: &str) -> Self {
857 Self::IndexMark { primary: primary.to_string(), secondary: None }
858 }
859
860 pub fn memo(content: Vec<Paragraph>, author: &str, date: &str) -> Self {
874 Self::Memo { content, author: author.to_string(), date: date.to_string() }
875 }
876
877 pub fn cross_ref(target: &str, ref_type: RefType, content_type: RefContentType) -> Self {
889 Self::CrossRef {
890 target_name: target.to_string(),
891 ref_type,
892 content_type,
893 as_hyperlink: false,
894 }
895 }
896
897 pub fn chart(chart_type: ChartType, data: ChartData) -> Self {
912 Self::Chart {
913 chart_type,
914 data,
915 width: HwpUnit::new(32250).expect("32250 is valid"),
916 height: HwpUnit::new(18750).expect("18750 is valid"),
917 title: None,
918 legend: LegendPosition::default(),
919 grouping: ChartGrouping::default(),
920 bar_shape: None,
921 explosion: None,
922 of_pie_type: None,
923 radar_style: None,
924 wireframe: None,
925 bubble_3d: None,
926 scatter_style: None,
927 show_markers: None,
928 stock_variant: None,
929 }
930 }
931
932 pub fn equation(script: &str) -> Self {
946 Self::Equation {
947 script: script.to_string(),
948 width: HwpUnit::new(8779).expect("8779 is valid"),
949 height: HwpUnit::new(2600).expect("2600 is valid"),
950 base_line: 71,
951 text_color: Color::BLACK,
952 font: "HancomEQN".to_string(),
953 }
954 }
955
956 pub fn text_box(paragraphs: Vec<Paragraph>, width: HwpUnit, height: HwpUnit) -> Self {
974 Self::TextBox {
975 paragraphs,
976 width,
977 height,
978 horz_offset: 0,
979 vert_offset: 0,
980 caption: None,
981 style: None,
982 }
983 }
984
985 pub fn footnote(paragraphs: Vec<Paragraph>) -> Self {
1005 Self::Footnote { inst_id: None, paragraphs }
1006 }
1007
1008 pub fn endnote(paragraphs: Vec<Paragraph>) -> Self {
1028 Self::Endnote { inst_id: None, paragraphs }
1029 }
1030
1031 pub fn footnote_with_id(inst_id: u32, paragraphs: Vec<Paragraph>) -> Self {
1047 Self::Footnote { inst_id: Some(inst_id), paragraphs }
1048 }
1049
1050 pub fn endnote_with_id(inst_id: u32, paragraphs: Vec<Paragraph>) -> Self {
1066 Self::Endnote { inst_id: Some(inst_id), paragraphs }
1067 }
1068
1069 pub fn ellipse(width: HwpUnit, height: HwpUnit) -> Self {
1086 let w = width.as_i32();
1087 let h = height.as_i32();
1088 Self::Ellipse {
1089 center: ShapePoint::new(w / 2, h / 2),
1090 axis1: ShapePoint::new(w, h / 2),
1091 axis2: ShapePoint::new(w / 2, h),
1092 width,
1093 height,
1094 horz_offset: 0,
1095 vert_offset: 0,
1096 paragraphs: vec![],
1097 caption: None,
1098 style: None,
1099 }
1100 }
1101
1102 pub fn ellipse_with_text(width: HwpUnit, height: HwpUnit, paragraphs: Vec<Paragraph>) -> Self {
1122 let w = width.as_i32();
1123 let h = height.as_i32();
1124 Self::Ellipse {
1125 center: ShapePoint::new(w / 2, h / 2),
1126 axis1: ShapePoint::new(w, h / 2),
1127 axis2: ShapePoint::new(w / 2, h),
1128 width,
1129 height,
1130 horz_offset: 0,
1131 vert_offset: 0,
1132 paragraphs,
1133 caption: None,
1134 style: None,
1135 }
1136 }
1137
1138 pub fn rect(width: HwpUnit, height: HwpUnit) -> CoreResult<Self> {
1160 if width.as_i32() == 0 || height.as_i32() == 0 {
1161 return Err(CoreError::InvalidStructure {
1162 context: "Control::rect".to_string(),
1163 reason: format!(
1164 "rectangle requires non-zero dimensions, got {}x{}",
1165 width.as_i32(),
1166 height.as_i32()
1167 ),
1168 });
1169 }
1170 Ok(Self::Rect { width, height, horz_offset: 0, vert_offset: 0, caption: None, style: None })
1171 }
1172
1173 pub fn polygon(vertices: Vec<ShapePoint>) -> CoreResult<Self> {
1198 if vertices.len() < 3 {
1199 return Err(CoreError::InvalidStructure {
1200 context: "Control::polygon".to_string(),
1201 reason: format!("polygon requires at least 3 vertices, got {}", vertices.len()),
1202 });
1203 }
1204 let min_x = vertices.iter().map(|p| p.x as i64).min().unwrap_or(0);
1205 let max_x = vertices.iter().map(|p| p.x as i64).max().unwrap_or(0);
1206 let min_y = vertices.iter().map(|p| p.y as i64).min().unwrap_or(0);
1207 let max_y = vertices.iter().map(|p| p.y as i64).max().unwrap_or(0);
1208 let bbox_w = i32::try_from((max_x - min_x).max(0)).unwrap_or(i32::MAX);
1209 let bbox_h = i32::try_from((max_y - min_y).max(0)).unwrap_or(i32::MAX);
1210 let width = HwpUnit::new(bbox_w).map_err(|_| CoreError::InvalidStructure {
1211 context: "Control::polygon".into(),
1212 reason: format!("bounding box width {bbox_w} exceeds HwpUnit range"),
1213 })?;
1214 let height = HwpUnit::new(bbox_h).map_err(|_| CoreError::InvalidStructure {
1215 context: "Control::polygon".into(),
1216 reason: format!("bounding box height {bbox_h} exceeds HwpUnit range"),
1217 })?;
1218 Ok(Self::Polygon {
1219 vertices,
1220 width,
1221 height,
1222 horz_offset: 0,
1223 vert_offset: 0,
1224 paragraphs: vec![],
1225 caption: None,
1226 style: None,
1227 })
1228 }
1229
1230 pub fn line(start: ShapePoint, end: ShapePoint) -> CoreResult<Self> {
1253 if start == end {
1254 return Err(CoreError::InvalidStructure {
1255 context: "Control::line".to_string(),
1256 reason: "start and end points are identical (degenerate line)".to_string(),
1257 });
1258 }
1259 let min_x = start.x.min(end.x);
1262 let min_y = start.y.min(end.y);
1263 let norm_start =
1264 ShapePoint::new(start.x.saturating_sub(min_x), start.y.saturating_sub(min_y));
1265 let norm_end = ShapePoint::new(end.x.saturating_sub(min_x), end.y.saturating_sub(min_y));
1266
1267 let raw_w =
1268 i32::try_from(((end.x as i64) - (start.x as i64)).unsigned_abs()).unwrap_or(i32::MAX);
1269 let raw_h =
1270 i32::try_from(((end.y as i64) - (start.y as i64)).unsigned_abs()).unwrap_or(i32::MAX);
1271 let raw_w = raw_w.max(100);
1274 let raw_h = raw_h.max(100);
1275 let width = HwpUnit::new(raw_w).unwrap_or_else(|_| HwpUnit::new(100).expect("valid"));
1276 let height = HwpUnit::new(raw_h).unwrap_or_else(|_| HwpUnit::new(100).expect("valid"));
1277 Ok(Self::Line {
1278 start: norm_start,
1279 end: norm_end,
1280 width,
1281 height,
1282 horz_offset: 0,
1283 vert_offset: 0,
1284 caption: None,
1285 style: None,
1286 })
1287 }
1288
1289 pub fn horizontal_line(width: HwpUnit) -> Self {
1307 let w = width.as_i32();
1308 Self::Line {
1309 start: ShapePoint::new(0, 0),
1310 end: ShapePoint::new(w, 0),
1311 width,
1312 height: HwpUnit::new(100).expect("100 is valid"),
1313 horz_offset: 0,
1314 vert_offset: 0,
1315 caption: None,
1316 style: None,
1317 }
1318 }
1319
1320 pub fn dutmal(main_text: impl Into<String>, sub_text: impl Into<String>) -> Self {
1333 Self::Dutmal {
1334 main_text: main_text.into(),
1335 sub_text: sub_text.into(),
1336 position: DutmalPosition::Top,
1337 sz_ratio: 0,
1338 align: DutmalAlign::Center,
1339 }
1340 }
1341
1342 pub fn compose(text: impl Into<String>) -> Self {
1356 Self::Compose {
1357 compose_text: text.into(),
1358 circle_type: "SHAPE_REVERSAL_TIRANGLE".to_string(), char_sz: -3,
1360 compose_type: "SPREAD".to_string(),
1361 }
1362 }
1363
1364 pub fn arc(arc_type: ArcType, width: HwpUnit, height: HwpUnit) -> Self {
1381 let w = width.as_i32();
1382 let h = height.as_i32();
1383 Self::Arc {
1384 arc_type,
1385 center: ShapePoint::new(w / 2, h / 2),
1386 axis1: ShapePoint::new(w, h / 2),
1387 axis2: ShapePoint::new(w / 2, h),
1388 start1: ShapePoint::new(w, h / 2),
1389 end1: ShapePoint::new(w / 2, 0),
1390 start2: ShapePoint::new(w, h / 2),
1391 end2: ShapePoint::new(w / 2, 0),
1392 width,
1393 height,
1394 horz_offset: 0,
1395 vert_offset: 0,
1396 caption: None,
1397 style: None,
1398 }
1399 }
1400
1401 pub fn curve(points: Vec<ShapePoint>) -> CoreResult<Self> {
1426 if points.len() < 2 {
1427 return Err(CoreError::InvalidStructure {
1428 context: "Control::curve".to_string(),
1429 reason: format!("curve requires at least 2 points, got {}", points.len()),
1430 });
1431 }
1432 let min_x = points.iter().map(|p| p.x as i64).min().unwrap_or(0);
1433 let max_x = points.iter().map(|p| p.x as i64).max().unwrap_or(0);
1434 let min_y = points.iter().map(|p| p.y as i64).min().unwrap_or(0);
1435 let max_y = points.iter().map(|p| p.y as i64).max().unwrap_or(0);
1436 let bbox_w = i32::try_from((max_x - min_x).max(1)).unwrap_or(i32::MAX);
1437 let bbox_h = i32::try_from((max_y - min_y).max(1)).unwrap_or(i32::MAX);
1438 let width = HwpUnit::new(bbox_w).map_err(|_| CoreError::InvalidStructure {
1439 context: "Control::curve".into(),
1440 reason: format!("bounding box width {bbox_w} exceeds HwpUnit range"),
1441 })?;
1442 let height = HwpUnit::new(bbox_h).map_err(|_| CoreError::InvalidStructure {
1443 context: "Control::curve".into(),
1444 reason: format!("bounding box height {bbox_h} exceeds HwpUnit range"),
1445 })?;
1446 let seg_count = points.len().saturating_sub(1);
1447 Ok(Self::Curve {
1448 points,
1449 segment_types: vec![CurveSegmentType::Curve; seg_count],
1450 width,
1451 height,
1452 horz_offset: 0,
1453 vert_offset: 0,
1454 caption: None,
1455 style: None,
1456 })
1457 }
1458
1459 pub fn connect_line(start: ShapePoint, end: ShapePoint) -> CoreResult<Self> {
1481 if start == end {
1482 return Err(CoreError::InvalidStructure {
1483 context: "Control::connect_line".to_string(),
1484 reason: "start and end points are identical (degenerate line)".to_string(),
1485 });
1486 }
1487 let min_x = start.x.min(end.x);
1490 let min_y = start.y.min(end.y);
1491 let norm_start =
1492 ShapePoint::new(start.x.saturating_sub(min_x), start.y.saturating_sub(min_y));
1493 let norm_end = ShapePoint::new(end.x.saturating_sub(min_x), end.y.saturating_sub(min_y));
1494
1495 let raw_w =
1496 i32::try_from(((end.x as i64) - (start.x as i64)).unsigned_abs()).unwrap_or(i32::MAX);
1497 let raw_h =
1498 i32::try_from(((end.y as i64) - (start.y as i64)).unsigned_abs()).unwrap_or(i32::MAX);
1499 let raw_w = raw_w.max(100);
1500 let raw_h = raw_h.max(100);
1501 let width = HwpUnit::new(raw_w).unwrap_or_else(|_| HwpUnit::new(100).expect("valid"));
1502 let height = HwpUnit::new(raw_h).unwrap_or_else(|_| HwpUnit::new(100).expect("valid"));
1503 Ok(Self::ConnectLine {
1504 start: norm_start,
1505 end: norm_end,
1506 control_points: Vec::new(),
1507 connect_type: "STRAIGHT".to_string(),
1508 width,
1509 height,
1510 horz_offset: 0,
1511 vert_offset: 0,
1512 caption: None,
1513 style: None,
1514 })
1515 }
1516
1517 pub fn hyperlink(text: &str, url: &str) -> Self {
1528 Self::Hyperlink { text: text.to_string(), url: url.to_string() }
1529 }
1530}
1531
1532impl std::fmt::Display for Control {
1533 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1534 match self {
1535 Self::TextBox { paragraphs, .. } => {
1536 let n = paragraphs.len();
1537 let word = if n == 1 { "paragraph" } else { "paragraphs" };
1538 write!(f, "TextBox({n} {word})")
1539 }
1540 Self::Hyperlink { text, url } => {
1541 let preview: String =
1542 if text.len() > 30 { text.chars().take(30).collect() } else { text.clone() };
1543 write!(f, "Hyperlink(\"{preview}\" -> {url})")
1544 }
1545 Self::Footnote { paragraphs, .. } => {
1546 let n = paragraphs.len();
1547 let word = if n == 1 { "paragraph" } else { "paragraphs" };
1548 write!(f, "Footnote({n} {word})")
1549 }
1550 Self::Endnote { paragraphs, .. } => {
1551 let n = paragraphs.len();
1552 let word = if n == 1 { "paragraph" } else { "paragraphs" };
1553 write!(f, "Endnote({n} {word})")
1554 }
1555 Self::Line { .. } => {
1556 write!(f, "Line")
1557 }
1558 Self::Ellipse { paragraphs, .. } => {
1559 let n = paragraphs.len();
1560 let word = if n == 1 { "paragraph" } else { "paragraphs" };
1561 write!(f, "Ellipse({n} {word})")
1562 }
1563 Self::Rect { width, height, .. } => {
1564 write!(f, "Rect({}x{})", width.as_i32(), height.as_i32())
1565 }
1566 Self::Polygon { vertices, paragraphs, .. } => {
1567 let nv = vertices.len();
1568 let np = paragraphs.len();
1569 let vw = if nv == 1 { "vertex" } else { "vertices" };
1570 let pw = if np == 1 { "paragraph" } else { "paragraphs" };
1571 write!(f, "Polygon({nv} {vw}, {np} {pw})")
1572 }
1573 Self::Chart { chart_type, data, .. } => {
1574 let series_count = match data {
1575 ChartData::Category { series, .. } => series.len(),
1576 ChartData::Xy { series } => series.len(),
1577 };
1578 write!(f, "Chart({chart_type:?}, {series_count} series)")
1579 }
1580 Self::EmbeddedChart { chart_xml, ole_bytes, width, height, .. } => {
1581 write!(
1582 f,
1583 "EmbeddedChart(xml={} bytes, ole={} bytes, {}x{})",
1584 chart_xml.len(),
1585 ole_bytes.len(),
1586 width.as_i32(),
1587 height.as_i32()
1588 )
1589 }
1590 Self::Equation { script, .. } => {
1591 let preview: String = if script.len() > 30 {
1592 script.chars().take(30).collect()
1593 } else {
1594 script.clone()
1595 };
1596 write!(f, "Equation(\"{preview}\")")
1597 }
1598 Self::Dutmal { main_text, sub_text, .. } => {
1599 write!(f, "Dutmal(\"{main_text}\" / \"{sub_text}\")")
1600 }
1601 Self::Compose { compose_text, .. } => {
1602 write!(f, "Compose(\"{compose_text}\")")
1603 }
1604 Self::Arc { arc_type, .. } => {
1605 write!(f, "Arc({arc_type})")
1606 }
1607 Self::Curve { points, .. } => {
1608 write!(f, "Curve({} points)", points.len())
1609 }
1610 Self::ConnectLine { .. } => {
1611 write!(f, "ConnectLine")
1612 }
1613 Self::Bookmark { name, bookmark_type } => {
1614 write!(f, "Bookmark(\"{name}\", {bookmark_type})")
1615 }
1616 Self::CrossRef { target_name, ref_type, .. } => {
1617 write!(f, "CrossRef(\"{target_name}\", {ref_type})")
1618 }
1619 Self::Field { field_type, hint_text, .. } => {
1620 let hint = hint_text.as_deref().unwrap_or("");
1621 write!(f, "Field({field_type}, \"{hint}\")")
1622 }
1623 Self::Memo { content, author, .. } => {
1624 let n = content.len();
1625 let word = if n == 1 { "paragraph" } else { "paragraphs" };
1626 write!(f, "Memo({n} {word}, by {author})")
1627 }
1628 Self::IndexMark { primary, secondary } => {
1629 if let Some(sec) = secondary {
1630 write!(f, "IndexMark(\"{primary}\" / \"{sec}\")")
1631 } else {
1632 write!(f, "IndexMark(\"{primary}\")")
1633 }
1634 }
1635 Self::Unknown { tag, .. } => {
1636 write!(f, "Unknown({tag})")
1637 }
1638 }
1639 }
1640}
1641
1642#[cfg(test)]
1643mod tests {
1644 use super::*;
1645 use crate::run::Run;
1646 use hwpforge_foundation::{CharShapeIndex, Color, ParaShapeIndex};
1647
1648 fn simple_paragraph() -> Paragraph {
1649 Paragraph::with_runs(
1650 vec![Run::text("footnote text", CharShapeIndex::new(0))],
1651 ParaShapeIndex::new(0),
1652 )
1653 }
1654
1655 #[test]
1656 fn shape_style_default_all_none() {
1657 let s = ShapeStyle::default();
1658 assert!(s.line_color.is_none());
1659 assert!(s.fill_color.is_none());
1660 assert!(s.line_width.is_none());
1661 assert!(s.line_style.is_none());
1662 }
1663
1664 #[test]
1665 fn shape_style_with_typed_fields() {
1666 let s = ShapeStyle {
1667 line_color: Some(Color::from_rgb(255, 0, 0)),
1668 fill_color: Some(Color::from_rgb(0, 255, 0)),
1669 line_width: Some(100),
1670 line_style: Some(LineStyle::Dash),
1671 ..Default::default()
1672 };
1673 assert_eq!(s.line_color.unwrap(), Color::from_rgb(255, 0, 0));
1674 assert_eq!(s.fill_color.unwrap(), Color::from_rgb(0, 255, 0));
1675 assert_eq!(s.line_width.unwrap(), 100);
1676 assert_eq!(s.line_style.unwrap(), LineStyle::Dash);
1677 }
1678
1679 #[test]
1680 fn line_style_default() {
1681 assert_eq!(LineStyle::default(), LineStyle::Solid);
1682 }
1683
1684 #[test]
1685 fn line_style_display() {
1686 assert_eq!(LineStyle::Solid.to_string(), "SOLID");
1687 assert_eq!(LineStyle::Dash.to_string(), "DASH");
1688 assert_eq!(LineStyle::Dot.to_string(), "DOT");
1689 assert_eq!(LineStyle::DashDot.to_string(), "DASH_DOT");
1690 assert_eq!(LineStyle::DashDotDot.to_string(), "DASH_DOT_DOT");
1691 assert_eq!(LineStyle::None.to_string(), "NONE");
1692 }
1693
1694 #[test]
1695 fn line_style_from_str() {
1696 assert_eq!("SOLID".parse::<LineStyle>().unwrap(), LineStyle::Solid);
1697 assert_eq!("Dash".parse::<LineStyle>().unwrap(), LineStyle::Dash);
1698 assert_eq!("dot".parse::<LineStyle>().unwrap(), LineStyle::Dot);
1699 assert_eq!("DASH_DOT".parse::<LineStyle>().unwrap(), LineStyle::DashDot);
1700 assert_eq!("DashDotDot".parse::<LineStyle>().unwrap(), LineStyle::DashDotDot);
1701 assert_eq!("NONE".parse::<LineStyle>().unwrap(), LineStyle::None);
1702 assert!("INVALID".parse::<LineStyle>().is_err());
1703 }
1704
1705 #[test]
1706 fn line_style_serde_roundtrip() {
1707 for style in [
1708 LineStyle::Solid,
1709 LineStyle::Dash,
1710 LineStyle::Dot,
1711 LineStyle::DashDot,
1712 LineStyle::DashDotDot,
1713 LineStyle::None,
1714 ] {
1715 let json = serde_json::to_string(&style).unwrap();
1716 let back: LineStyle = serde_json::from_str(&json).unwrap();
1717 assert_eq!(style, back);
1718 }
1719 }
1720
1721 #[test]
1722 fn text_box_construction() {
1723 let ctrl = Control::TextBox {
1724 paragraphs: vec![simple_paragraph()],
1725 width: HwpUnit::from_mm(80.0).unwrap(),
1726 height: HwpUnit::from_mm(40.0).unwrap(),
1727 horz_offset: 0,
1728 vert_offset: 0,
1729 caption: None,
1730 style: None,
1731 };
1732 assert!(ctrl.is_text_box());
1733 assert!(!ctrl.is_hyperlink());
1734 assert!(!ctrl.is_footnote());
1735 assert!(!ctrl.is_endnote());
1736 assert!(!ctrl.is_unknown());
1737 }
1738
1739 #[test]
1740 fn hyperlink_construction() {
1741 let ctrl = Control::Hyperlink {
1742 text: "Click".to_string(),
1743 url: "https://example.com".to_string(),
1744 };
1745 assert!(ctrl.is_hyperlink());
1746 assert!(!ctrl.is_text_box());
1747 }
1748
1749 #[test]
1750 fn footnote_construction() {
1751 let ctrl = Control::Footnote { inst_id: None, paragraphs: vec![simple_paragraph()] };
1752 assert!(ctrl.is_footnote());
1753 assert!(!ctrl.is_text_box());
1754 assert!(!ctrl.is_endnote());
1755 }
1756
1757 #[test]
1758 fn endnote_construction() {
1759 let ctrl = Control::Endnote { inst_id: Some(123456), paragraphs: vec![simple_paragraph()] };
1760 assert!(ctrl.is_endnote());
1761 assert!(!ctrl.is_footnote());
1762 assert!(!ctrl.is_text_box());
1763 }
1764
1765 #[test]
1766 fn unknown_construction() {
1767 let ctrl = Control::Unknown {
1768 tag: "custom:widget".to_string(),
1769 data: Some("<data>value</data>".to_string()),
1770 };
1771 assert!(ctrl.is_unknown());
1772 }
1773
1774 #[test]
1775 fn unknown_without_data() {
1776 let ctrl = Control::Unknown { tag: "header".to_string(), data: None };
1777 assert!(ctrl.is_unknown());
1778 }
1779
1780 #[test]
1781 fn display_text_box() {
1782 let ctrl = Control::TextBox {
1783 paragraphs: vec![simple_paragraph(), simple_paragraph()],
1784 width: HwpUnit::from_mm(80.0).unwrap(),
1785 height: HwpUnit::from_mm(40.0).unwrap(),
1786 horz_offset: 0,
1787 vert_offset: 0,
1788 caption: None,
1789 style: None,
1790 };
1791 assert_eq!(ctrl.to_string(), "TextBox(2 paragraphs)");
1792 }
1793
1794 #[test]
1795 fn display_hyperlink() {
1796 let ctrl =
1797 Control::Hyperlink { text: "Short".to_string(), url: "https://x.com".to_string() };
1798 let s = ctrl.to_string();
1799 assert!(s.contains("Short"), "display: {s}");
1800 assert!(s.contains("https://x.com"), "display: {s}");
1801 }
1802
1803 #[test]
1804 fn display_hyperlink_long_text_truncated() {
1805 let ctrl =
1806 Control::Hyperlink { text: "A".repeat(100), url: "https://example.com".to_string() };
1807 let s = ctrl.to_string();
1808 assert!(s.contains(&"A".repeat(30)), "display: {s}");
1810 assert!(!s.contains(&"A".repeat(31)), "display: {s}");
1811 }
1812
1813 #[test]
1814 fn display_footnote() {
1815 let ctrl = Control::Footnote { inst_id: None, paragraphs: vec![simple_paragraph()] };
1816 assert_eq!(ctrl.to_string(), "Footnote(1 paragraph)");
1817 }
1818
1819 #[test]
1820 fn display_endnote() {
1821 let ctrl = Control::Endnote { inst_id: Some(999), paragraphs: vec![simple_paragraph()] };
1822 assert_eq!(ctrl.to_string(), "Endnote(1 paragraph)");
1823 }
1824
1825 #[test]
1826 fn display_unknown() {
1827 let ctrl = Control::Unknown { tag: "bookmark".to_string(), data: None };
1828 assert_eq!(ctrl.to_string(), "Unknown(bookmark)");
1829 }
1830
1831 #[test]
1832 fn equality() {
1833 let a = Control::Hyperlink { text: "A".to_string(), url: "B".to_string() };
1834 let b = Control::Hyperlink { text: "A".to_string(), url: "B".to_string() };
1835 let c = Control::Hyperlink { text: "A".to_string(), url: "C".to_string() };
1836 assert_eq!(a, b);
1837 assert_ne!(a, c);
1838 }
1839
1840 #[test]
1841 fn serde_roundtrip_text_box() {
1842 let ctrl = Control::TextBox {
1843 paragraphs: vec![simple_paragraph()],
1844 width: HwpUnit::from_mm(80.0).unwrap(),
1845 height: HwpUnit::from_mm(40.0).unwrap(),
1846 horz_offset: 0,
1847 vert_offset: 0,
1848 caption: None,
1849 style: None,
1850 };
1851 let json = serde_json::to_string(&ctrl).unwrap();
1852 let back: Control = serde_json::from_str(&json).unwrap();
1853 assert_eq!(ctrl, back);
1854 }
1855
1856 #[test]
1857 fn serde_roundtrip_hyperlink() {
1858 let ctrl = Control::Hyperlink {
1859 text: "link text".to_string(),
1860 url: "https://rust-lang.org".to_string(),
1861 };
1862 let json = serde_json::to_string(&ctrl).unwrap();
1863 let back: Control = serde_json::from_str(&json).unwrap();
1864 assert_eq!(ctrl, back);
1865 }
1866
1867 #[test]
1868 fn serde_roundtrip_footnote() {
1869 let ctrl = Control::Footnote { inst_id: Some(12345), paragraphs: vec![simple_paragraph()] };
1870 let json = serde_json::to_string(&ctrl).unwrap();
1871 let back: Control = serde_json::from_str(&json).unwrap();
1872 assert_eq!(ctrl, back);
1873 }
1874
1875 #[test]
1876 fn serde_roundtrip_endnote() {
1877 let ctrl = Control::Endnote { inst_id: None, paragraphs: vec![simple_paragraph()] };
1878 let json = serde_json::to_string(&ctrl).unwrap();
1879 let back: Control = serde_json::from_str(&json).unwrap();
1880 assert_eq!(ctrl, back);
1881 }
1882
1883 #[test]
1884 fn serde_roundtrip_unknown() {
1885 let ctrl = Control::Unknown { tag: "test".to_string(), data: Some("payload".to_string()) };
1886 let json = serde_json::to_string(&ctrl).unwrap();
1887 let back: Control = serde_json::from_str(&json).unwrap();
1888 assert_eq!(ctrl, back);
1889 }
1890
1891 #[test]
1894 fn line_construction() {
1895 let ctrl = Control::Line {
1896 start: ShapePoint { x: 0, y: 0 },
1897 end: ShapePoint { x: 1000, y: 500 },
1898 width: HwpUnit::from_mm(50.0).unwrap(),
1899 height: HwpUnit::from_mm(25.0).unwrap(),
1900 horz_offset: 0,
1901 vert_offset: 0,
1902 caption: None,
1903 style: None,
1904 };
1905 assert!(ctrl.is_line());
1906 assert!(!ctrl.is_text_box());
1907 assert!(!ctrl.is_ellipse());
1908 assert!(!ctrl.is_polygon());
1909 }
1910
1911 #[test]
1912 fn ellipse_construction() {
1913 let ctrl = Control::Ellipse {
1914 center: ShapePoint { x: 500, y: 500 },
1915 axis1: ShapePoint { x: 1000, y: 500 },
1916 axis2: ShapePoint { x: 500, y: 1000 },
1917 width: HwpUnit::from_mm(40.0).unwrap(),
1918 height: HwpUnit::from_mm(30.0).unwrap(),
1919 horz_offset: 0,
1920 vert_offset: 0,
1921 paragraphs: vec![],
1922 caption: None,
1923 style: None,
1924 };
1925 assert!(ctrl.is_ellipse());
1926 assert!(!ctrl.is_line());
1927 assert!(!ctrl.is_polygon());
1928 }
1929
1930 #[test]
1931 fn ellipse_with_paragraphs() {
1932 let ctrl = Control::Ellipse {
1933 center: ShapePoint { x: 500, y: 500 },
1934 axis1: ShapePoint { x: 1000, y: 500 },
1935 axis2: ShapePoint { x: 500, y: 1000 },
1936 width: HwpUnit::from_mm(40.0).unwrap(),
1937 height: HwpUnit::from_mm(30.0).unwrap(),
1938 horz_offset: 0,
1939 vert_offset: 0,
1940 paragraphs: vec![simple_paragraph()],
1941 caption: None,
1942 style: None,
1943 };
1944 assert!(ctrl.is_ellipse());
1945 assert_eq!(ctrl.to_string(), "Ellipse(1 paragraph)");
1946 }
1947
1948 #[test]
1949 fn polygon_construction() {
1950 let ctrl = Control::Polygon {
1951 vertices: vec![
1952 ShapePoint { x: 0, y: 0 },
1953 ShapePoint { x: 1000, y: 0 },
1954 ShapePoint { x: 500, y: 1000 },
1955 ],
1956 width: HwpUnit::from_mm(50.0).unwrap(),
1957 height: HwpUnit::from_mm(50.0).unwrap(),
1958 horz_offset: 0,
1959 vert_offset: 0,
1960 paragraphs: vec![],
1961 caption: None,
1962 style: None,
1963 };
1964 assert!(ctrl.is_polygon());
1965 assert!(!ctrl.is_line());
1966 assert!(!ctrl.is_ellipse());
1967 assert_eq!(ctrl.to_string(), "Polygon(3 vertices, 0 paragraphs)");
1968 }
1969
1970 #[test]
1971 fn display_line() {
1972 let ctrl = Control::Line {
1973 start: ShapePoint { x: 0, y: 0 },
1974 end: ShapePoint { x: 100, y: 200 },
1975 width: HwpUnit::from_mm(10.0).unwrap(),
1976 height: HwpUnit::from_mm(5.0).unwrap(),
1977 horz_offset: 0,
1978 vert_offset: 0,
1979 caption: None,
1980 style: None,
1981 };
1982 assert_eq!(ctrl.to_string(), "Line");
1983 }
1984
1985 #[test]
1986 fn serde_roundtrip_line() {
1987 let ctrl = Control::Line {
1988 start: ShapePoint { x: 100, y: 200 },
1989 end: ShapePoint { x: 300, y: 400 },
1990 width: HwpUnit::from_mm(20.0).unwrap(),
1991 height: HwpUnit::from_mm(10.0).unwrap(),
1992 horz_offset: 0,
1993 vert_offset: 0,
1994 caption: None,
1995 style: None,
1996 };
1997 let json = serde_json::to_string(&ctrl).unwrap();
1998 let back: Control = serde_json::from_str(&json).unwrap();
1999 assert_eq!(ctrl, back);
2000 }
2001
2002 #[test]
2003 fn serde_roundtrip_ellipse() {
2004 let ctrl = Control::Ellipse {
2005 center: ShapePoint { x: 500, y: 500 },
2006 axis1: ShapePoint { x: 1000, y: 500 },
2007 axis2: ShapePoint { x: 500, y: 1000 },
2008 width: HwpUnit::from_mm(40.0).unwrap(),
2009 height: HwpUnit::from_mm(30.0).unwrap(),
2010 horz_offset: 0,
2011 vert_offset: 0,
2012 paragraphs: vec![simple_paragraph()],
2013 caption: None,
2014 style: None,
2015 };
2016 let json = serde_json::to_string(&ctrl).unwrap();
2017 let back: Control = serde_json::from_str(&json).unwrap();
2018 assert_eq!(ctrl, back);
2019 }
2020
2021 #[test]
2022 fn serde_roundtrip_polygon() {
2023 let ctrl = Control::Polygon {
2024 vertices: vec![
2025 ShapePoint { x: 0, y: 0 },
2026 ShapePoint { x: 1000, y: 0 },
2027 ShapePoint { x: 500, y: 1000 },
2028 ],
2029 width: HwpUnit::from_mm(50.0).unwrap(),
2030 height: HwpUnit::from_mm(50.0).unwrap(),
2031 horz_offset: 0,
2032 vert_offset: 0,
2033 paragraphs: vec![],
2034 caption: None,
2035 style: None,
2036 };
2037 let json = serde_json::to_string(&ctrl).unwrap();
2038 let back: Control = serde_json::from_str(&json).unwrap();
2039 assert_eq!(ctrl, back);
2040 }
2041
2042 #[test]
2043 fn shape_point_equality() {
2044 let a = ShapePoint { x: 10, y: 20 };
2045 let b = ShapePoint { x: 10, y: 20 };
2046 let c = ShapePoint { x: 10, y: 30 };
2047 assert_eq!(a, b);
2048 assert_ne!(a, c);
2049 }
2050
2051 #[test]
2052 fn shape_point_new() {
2053 let pt = ShapePoint::new(100, 200);
2054 assert_eq!(pt.x, 100);
2055 assert_eq!(pt.y, 200);
2056 }
2057
2058 #[test]
2059 fn shape_point_serde_roundtrip() {
2060 let pt = ShapePoint::new(500, 750);
2061 let json = serde_json::to_string(&pt).unwrap();
2062 let back: ShapePoint = serde_json::from_str(&json).unwrap();
2063 assert_eq!(pt, back);
2064 }
2065
2066 #[test]
2069 fn equation_constructor_defaults() {
2070 let ctrl = Control::equation("{a+b} over {c+d}");
2071 assert!(ctrl.is_equation());
2072 match ctrl {
2073 Control::Equation { script, width, height, base_line, text_color, ref font } => {
2074 assert_eq!(script, "{a+b} over {c+d}");
2075 assert_eq!(width, HwpUnit::new(8779).unwrap());
2076 assert_eq!(height, HwpUnit::new(2600).unwrap());
2077 assert_eq!(base_line, 71);
2078 assert_eq!(text_color, Color::BLACK);
2079 assert_eq!(font, "HancomEQN");
2080 }
2081 _ => panic!("expected Equation"),
2082 }
2083 }
2084
2085 #[test]
2086 fn equation_constructor_empty_script() {
2087 let ctrl = Control::equation("");
2088 assert!(ctrl.is_equation());
2089 }
2090
2091 #[test]
2092 fn text_box_constructor_defaults() {
2093 let width = HwpUnit::from_mm(80.0).unwrap();
2094 let height = HwpUnit::from_mm(40.0).unwrap();
2095 let ctrl = Control::text_box(vec![simple_paragraph()], width, height);
2096 assert!(ctrl.is_text_box());
2097 match ctrl {
2098 Control::TextBox { paragraphs, horz_offset, vert_offset, caption, style, .. } => {
2099 assert_eq!(paragraphs.len(), 1);
2100 assert_eq!(horz_offset, 0);
2101 assert_eq!(vert_offset, 0);
2102 assert!(caption.is_none());
2103 assert!(style.is_none());
2104 }
2105 _ => panic!("expected TextBox"),
2106 }
2107 }
2108
2109 #[test]
2110 fn footnote_constructor_defaults() {
2111 let ctrl = Control::footnote(vec![simple_paragraph()]);
2112 assert!(ctrl.is_footnote());
2113 match ctrl {
2114 Control::Footnote { inst_id, paragraphs } => {
2115 assert!(inst_id.is_none());
2116 assert_eq!(paragraphs.len(), 1);
2117 }
2118 _ => panic!("expected Footnote"),
2119 }
2120 }
2121
2122 #[test]
2123 fn endnote_constructor_defaults() {
2124 let ctrl = Control::endnote(vec![simple_paragraph()]);
2125 assert!(ctrl.is_endnote());
2126 match ctrl {
2127 Control::Endnote { inst_id, paragraphs } => {
2128 assert!(inst_id.is_none());
2129 assert_eq!(paragraphs.len(), 1);
2130 }
2131 _ => panic!("expected Endnote"),
2132 }
2133 }
2134
2135 #[test]
2136 fn ellipse_constructor_geometry() {
2137 let width = HwpUnit::from_mm(40.0).unwrap();
2138 let height = HwpUnit::from_mm(30.0).unwrap();
2139 let ctrl = Control::ellipse(width, height);
2140 assert!(ctrl.is_ellipse());
2141 match &ctrl {
2142 Control::Ellipse {
2143 center,
2144 axis1,
2145 axis2,
2146 horz_offset,
2147 vert_offset,
2148 paragraphs,
2149 caption,
2150 style,
2151 ..
2152 } => {
2153 let w = width.as_i32();
2154 let h = height.as_i32();
2155 assert_eq!(*center, ShapePoint::new(w / 2, h / 2));
2156 assert_eq!(*axis1, ShapePoint::new(w, h / 2));
2157 assert_eq!(*axis2, ShapePoint::new(w / 2, h));
2158 assert_eq!(*horz_offset, 0);
2159 assert_eq!(*vert_offset, 0);
2160 assert!(paragraphs.is_empty());
2161 assert!(caption.is_none());
2162 assert!(style.is_none());
2163 }
2164 _ => panic!("expected Ellipse"),
2165 }
2166 }
2167
2168 #[test]
2169 fn rect_constructor_basic_geometry() {
2170 let width = HwpUnit::from_mm(40.0).unwrap();
2171 let height = HwpUnit::from_mm(20.0).unwrap();
2172 let ctrl = Control::rect(width, height).unwrap();
2173 assert!(ctrl.is_rect());
2174 match ctrl {
2175 Control::Rect { width: w, height: h, horz_offset, vert_offset, caption, style } => {
2176 assert_eq!(w, width);
2177 assert_eq!(h, height);
2178 assert_eq!(horz_offset, 0);
2179 assert_eq!(vert_offset, 0);
2180 assert!(caption.is_none());
2181 assert!(style.is_none());
2182 }
2183 _ => panic!("expected Rect"),
2184 }
2185 }
2186
2187 #[test]
2188 fn rect_constructor_zero_dimension_errors() {
2189 let zero = HwpUnit::new(0).unwrap();
2190 let nonzero = HwpUnit::from_mm(10.0).unwrap();
2191 assert!(Control::rect(zero, nonzero).is_err());
2192 assert!(Control::rect(nonzero, zero).is_err());
2193 }
2194
2195 #[test]
2196 fn polygon_constructor_triangle() {
2197 let vertices =
2198 vec![ShapePoint::new(0, 1000), ShapePoint::new(500, 0), ShapePoint::new(1000, 1000)];
2199 let ctrl = Control::polygon(vertices).unwrap();
2200 assert!(ctrl.is_polygon());
2201 match &ctrl {
2202 Control::Polygon {
2203 vertices,
2204 width,
2205 height,
2206 horz_offset,
2207 vert_offset,
2208 paragraphs,
2209 caption,
2210 style,
2211 } => {
2212 assert_eq!(vertices.len(), 3);
2213 assert_eq!(*width, HwpUnit::new(1000).unwrap());
2215 assert_eq!(*height, HwpUnit::new(1000).unwrap());
2216 assert_eq!(*horz_offset, 0);
2217 assert_eq!(*vert_offset, 0);
2218 assert!(paragraphs.is_empty());
2219 assert!(caption.is_none());
2220 assert!(style.is_none());
2221 }
2222 _ => panic!("expected Polygon"),
2223 }
2224 }
2225
2226 #[test]
2227 fn polygon_constructor_fewer_than_3_vertices_errors() {
2228 assert!(Control::polygon(vec![]).is_err());
2229 assert!(Control::polygon(vec![ShapePoint::new(0, 0)]).is_err());
2230 assert!(Control::polygon(vec![ShapePoint::new(0, 0), ShapePoint::new(1, 1)]).is_err());
2231 }
2232
2233 #[test]
2234 fn polygon_constructor_negative_coordinates() {
2235 let vertices =
2236 vec![ShapePoint::new(-500, -500), ShapePoint::new(500, -500), ShapePoint::new(0, 500)];
2237 let ctrl = Control::polygon(vertices).unwrap();
2238 assert!(ctrl.is_polygon());
2239 match ctrl {
2240 Control::Polygon { width, height, .. } => {
2241 assert_eq!(width, HwpUnit::new(1000).unwrap());
2243 assert_eq!(height, HwpUnit::new(1000).unwrap());
2244 }
2245 _ => panic!("expected Polygon"),
2246 }
2247 }
2248
2249 #[test]
2250 fn polygon_constructor_degenerate_collinear() {
2251 let vertices =
2253 vec![ShapePoint::new(0, 0), ShapePoint::new(500, 0), ShapePoint::new(1000, 0)];
2254 let ctrl = Control::polygon(vertices).unwrap();
2255 assert!(ctrl.is_polygon());
2256 match ctrl {
2257 Control::Polygon { width, height, .. } => {
2258 assert_eq!(width, HwpUnit::new(1000).unwrap());
2259 assert_eq!(height, HwpUnit::new(0).unwrap());
2260 }
2261 _ => panic!("expected Polygon"),
2262 }
2263 }
2264
2265 #[test]
2266 fn line_constructor_horizontal() {
2267 let ctrl = Control::line(ShapePoint::new(0, 0), ShapePoint::new(5000, 0)).unwrap();
2268 assert!(ctrl.is_line());
2269 match ctrl {
2270 Control::Line {
2271 start,
2272 end,
2273 width,
2274 height,
2275 horz_offset,
2276 vert_offset,
2277 caption,
2278 style,
2279 } => {
2280 assert_eq!(start, ShapePoint::new(0, 0));
2281 assert_eq!(end, ShapePoint::new(5000, 0));
2282 assert_eq!(width, HwpUnit::new(5000).unwrap());
2283 assert_eq!(height, HwpUnit::new(100).unwrap()); assert_eq!(horz_offset, 0);
2285 assert_eq!(vert_offset, 0);
2286 assert!(caption.is_none());
2287 assert!(style.is_none());
2288 }
2289 _ => panic!("expected Line"),
2290 }
2291 }
2292
2293 #[test]
2294 fn line_constructor_vertical() {
2295 let ctrl = Control::line(ShapePoint::new(0, 0), ShapePoint::new(0, 3000)).unwrap();
2296 assert!(ctrl.is_line());
2297 match ctrl {
2298 Control::Line { width, height, .. } => {
2299 assert_eq!(width, HwpUnit::new(100).unwrap()); assert_eq!(height, HwpUnit::new(3000).unwrap());
2301 }
2302 _ => panic!("expected Line"),
2303 }
2304 }
2305
2306 #[test]
2307 fn line_constructor_diagonal_bounding_box() {
2308 let ctrl = Control::line(ShapePoint::new(100, 200), ShapePoint::new(400, 500)).unwrap();
2309 match ctrl {
2310 Control::Line { width, height, .. } => {
2311 assert_eq!(width, HwpUnit::new(300).unwrap());
2312 assert_eq!(height, HwpUnit::new(300).unwrap());
2313 }
2314 _ => panic!("expected Line"),
2315 }
2316 }
2317
2318 #[test]
2319 fn line_constructor_same_point_errors() {
2320 let pt = ShapePoint::new(100, 200);
2321 assert!(Control::line(pt, pt).is_err());
2322 }
2323
2324 #[test]
2325 fn horizontal_line_constructor() {
2326 let width = HwpUnit::from_mm(100.0).unwrap();
2327 let ctrl = Control::horizontal_line(width);
2328 assert!(ctrl.is_line());
2329 match ctrl {
2330 Control::Line {
2331 start,
2332 end,
2333 width: w,
2334 height,
2335 horz_offset,
2336 vert_offset,
2337 caption,
2338 style,
2339 } => {
2340 assert_eq!(start, ShapePoint::new(0, 0));
2341 assert_eq!(end.y, 0);
2342 assert_eq!(end.x, width.as_i32());
2343 assert_eq!(w, width);
2344 assert_eq!(height, HwpUnit::new(100).unwrap()); assert_eq!(horz_offset, 0);
2346 assert_eq!(vert_offset, 0);
2347 assert!(caption.is_none());
2348 assert!(style.is_none());
2349 }
2350 _ => panic!("expected Line"),
2351 }
2352 }
2353
2354 #[test]
2355 fn hyperlink_constructor() {
2356 let ctrl = Control::hyperlink("Visit Rust", "https://rust-lang.org");
2357 assert!(ctrl.is_hyperlink());
2358 match ctrl {
2359 Control::Hyperlink { text, url } => {
2360 assert_eq!(text, "Visit Rust");
2361 assert_eq!(url, "https://rust-lang.org");
2362 }
2363 _ => panic!("expected Hyperlink"),
2364 }
2365 }
2366
2367 #[test]
2368 fn footnote_with_id_sets_inst_id() {
2369 let para = Paragraph::new(ParaShapeIndex::new(0));
2370 let ctrl = Control::footnote_with_id(42, vec![para]);
2371 assert!(ctrl.is_footnote());
2372 match ctrl {
2373 Control::Footnote { inst_id, paragraphs } => {
2374 assert_eq!(inst_id, Some(42));
2375 assert_eq!(paragraphs.len(), 1);
2376 }
2377 _ => panic!("expected Footnote"),
2378 }
2379 }
2380
2381 #[test]
2382 fn endnote_with_id_sets_inst_id() {
2383 let para = Paragraph::new(ParaShapeIndex::new(0));
2384 let ctrl = Control::endnote_with_id(7, vec![para]);
2385 assert!(ctrl.is_endnote());
2386 match ctrl {
2387 Control::Endnote { inst_id, paragraphs } => {
2388 assert_eq!(inst_id, Some(7));
2389 assert_eq!(paragraphs.len(), 1);
2390 }
2391 _ => panic!("expected Endnote"),
2392 }
2393 }
2394
2395 #[test]
2396 fn footnote_with_id_differs_from_plain_footnote() {
2397 let ctrl_plain = Control::footnote(vec![]);
2398 let ctrl_id = Control::footnote_with_id(1, vec![]);
2399 match ctrl_plain {
2400 Control::Footnote { inst_id, .. } => assert_eq!(inst_id, None),
2401 _ => panic!("expected Footnote"),
2402 }
2403 match ctrl_id {
2404 Control::Footnote { inst_id, .. } => assert_eq!(inst_id, Some(1)),
2405 _ => panic!("expected Footnote"),
2406 }
2407 }
2408
2409 #[test]
2410 fn ellipse_with_text_has_correct_geometry_and_paragraphs() {
2411 use hwpforge_foundation::HwpUnit;
2412 let width = HwpUnit::from_mm(40.0).unwrap();
2413 let height = HwpUnit::from_mm(30.0).unwrap();
2414 let para = Paragraph::new(ParaShapeIndex::new(0));
2415 let ctrl = Control::ellipse_with_text(width, height, vec![para]);
2416 assert!(ctrl.is_ellipse());
2417 match ctrl {
2418 Control::Ellipse {
2419 center,
2420 axis1,
2421 axis2,
2422 width: w,
2423 height: h,
2424 horz_offset,
2425 vert_offset,
2426 paragraphs,
2427 caption,
2428 style,
2429 } => {
2430 let wv = w.as_i32();
2431 let hv = h.as_i32();
2432 assert_eq!(center, ShapePoint::new(wv / 2, hv / 2));
2433 assert_eq!(axis1, ShapePoint::new(wv, hv / 2));
2434 assert_eq!(axis2, ShapePoint::new(wv / 2, hv));
2435 assert_eq!(horz_offset, 0);
2436 assert_eq!(vert_offset, 0);
2437 assert_eq!(paragraphs.len(), 1);
2438 assert!(caption.is_none());
2439 assert!(style.is_none());
2440 }
2441 _ => panic!("expected Ellipse"),
2442 }
2443 }
2444
2445 #[test]
2446 fn serde_roundtrip_chart() {
2447 use crate::chart::{ChartData, ChartGrouping, ChartType, LegendPosition};
2448 let ctrl = Control::Chart {
2449 chart_type: ChartType::Column,
2450 data: ChartData::category(&["A", "B"], &[("S1", &[1.0, 2.0])]),
2451 title: Some("Test Chart".to_string()),
2452 legend: LegendPosition::Bottom,
2453 grouping: ChartGrouping::Stacked,
2454 width: HwpUnit::from_mm(100.0).unwrap(),
2455 height: HwpUnit::from_mm(80.0).unwrap(),
2456 stock_variant: None,
2457 bar_shape: None,
2458 scatter_style: None,
2459 radar_style: None,
2460 of_pie_type: None,
2461 explosion: None,
2462 wireframe: None,
2463 bubble_3d: None,
2464 show_markers: None,
2465 };
2466 let json = serde_json::to_string(&ctrl).unwrap();
2467 let back: Control = serde_json::from_str(&json).unwrap();
2468 assert_eq!(ctrl, back);
2469 }
2470
2471 #[test]
2472 fn serde_roundtrip_equation() {
2473 let ctrl = Control::Equation {
2474 script: "{a+b} over {c+d}".to_string(),
2475 width: HwpUnit::new(8779).unwrap(),
2476 height: HwpUnit::new(2600).unwrap(),
2477 base_line: 71,
2478 text_color: Color::BLACK,
2479 font: "HancomEQN".to_string(),
2480 };
2481 let json = serde_json::to_string(&ctrl).unwrap();
2482 let back: Control = serde_json::from_str(&json).unwrap();
2483 assert_eq!(ctrl, back);
2484 }
2485
2486 #[test]
2487 fn ellipse_with_text_empty_paragraphs_matches_ellipse() {
2488 use hwpforge_foundation::HwpUnit;
2489 let width = HwpUnit::from_mm(20.0).unwrap();
2490 let height = HwpUnit::from_mm(10.0).unwrap();
2491 let plain = Control::ellipse(width, height);
2492 let with_text = Control::ellipse_with_text(width, height, vec![]);
2493 assert_eq!(plain, with_text);
2495 }
2496
2497 #[test]
2500 fn dutmal_constructor_defaults() {
2501 let ctrl = Control::dutmal("본문", "주석");
2502 assert!(ctrl.is_dutmal());
2503 match ctrl {
2504 Control::Dutmal { main_text, sub_text, position, sz_ratio, align } => {
2505 assert_eq!(main_text, "본문");
2506 assert_eq!(sub_text, "주석");
2507 assert_eq!(position, DutmalPosition::Top);
2508 assert_eq!(sz_ratio, 0);
2509 assert_eq!(align, DutmalAlign::Center);
2510 }
2511 _ => panic!("expected Dutmal"),
2512 }
2513 }
2514
2515 #[test]
2516 fn dutmal_is_dutmal_true() {
2517 assert!(Control::dutmal("a", "b").is_dutmal());
2518 }
2519
2520 #[test]
2521 fn dutmal_is_compose_false() {
2522 assert!(!Control::dutmal("a", "b").is_compose());
2523 }
2524
2525 #[test]
2526 fn dutmal_display() {
2527 let ctrl = Control::dutmal("hello", "world");
2528 assert_eq!(ctrl.to_string(), r#"Dutmal("hello" / "world")"#);
2529 }
2530
2531 #[test]
2532 fn dutmal_serde_roundtrip() {
2533 let ctrl = Control::Dutmal {
2534 main_text: "테스트".to_string(),
2535 sub_text: "test".to_string(),
2536 position: DutmalPosition::Bottom,
2537 sz_ratio: 50,
2538 align: DutmalAlign::Right,
2539 };
2540 let json = serde_json::to_string(&ctrl).unwrap();
2541 let decoded: Control = serde_json::from_str(&json).unwrap();
2542 assert_eq!(ctrl, decoded);
2543 }
2544
2545 #[test]
2546 fn dutmal_position_default_is_top() {
2547 assert_eq!(DutmalPosition::default(), DutmalPosition::Top);
2548 }
2549
2550 #[test]
2551 fn dutmal_align_default_is_center() {
2552 assert_eq!(DutmalAlign::default(), DutmalAlign::Center);
2553 }
2554
2555 #[test]
2558 fn compose_constructor_defaults() {
2559 let ctrl = Control::compose("가");
2560 assert!(ctrl.is_compose());
2561 match ctrl {
2562 Control::Compose { compose_text, circle_type, char_sz, compose_type } => {
2563 assert_eq!(compose_text, "가");
2564 assert_eq!(circle_type, "SHAPE_REVERSAL_TIRANGLE");
2565 assert_eq!(char_sz, -3);
2566 assert_eq!(compose_type, "SPREAD");
2567 }
2568 _ => panic!("expected Compose"),
2569 }
2570 }
2571
2572 #[test]
2573 fn compose_is_compose_true() {
2574 assert!(Control::compose("나").is_compose());
2575 }
2576
2577 #[test]
2578 fn compose_is_dutmal_false() {
2579 assert!(!Control::compose("나").is_dutmal());
2580 }
2581
2582 #[test]
2583 fn compose_display() {
2584 let ctrl = Control::compose("가나");
2585 assert_eq!(ctrl.to_string(), r#"Compose("가나")"#);
2586 }
2587
2588 #[test]
2589 fn compose_serde_roundtrip() {
2590 let ctrl = Control::Compose {
2591 compose_text: "①".to_string(),
2592 circle_type: "SHAPE_REVERSAL_TIRANGLE".to_string(),
2593 char_sz: -3,
2594 compose_type: "SPREAD".to_string(),
2595 };
2596 let json = serde_json::to_string(&ctrl).unwrap();
2597 let decoded: Control = serde_json::from_str(&json).unwrap();
2598 assert_eq!(ctrl, decoded);
2599 }
2600
2601 #[test]
2602 fn compose_spec_typo_preserved() {
2603 let ctrl = Control::compose("X");
2605 match ctrl {
2606 Control::Compose { circle_type, .. } => {
2607 assert_eq!(circle_type, "SHAPE_REVERSAL_TIRANGLE");
2608 assert!(!circle_type.contains("TRIANGLE")); }
2610 _ => panic!("expected Compose"),
2611 }
2612 }
2613
2614 #[test]
2619 fn line_extreme_coords_no_panic() {
2620 let start = ShapePoint::new(i32::MIN, i32::MIN);
2622 let end = ShapePoint::new(i32::MAX, i32::MAX);
2623 let ctrl = Control::line(start, end).unwrap();
2624 assert!(ctrl.is_line());
2625 }
2626
2627 #[test]
2628 fn connect_line_extreme_coords_no_panic() {
2629 let start = ShapePoint::new(i32::MIN, 0);
2630 let end = ShapePoint::new(i32::MAX, 0);
2631 let ctrl = Control::connect_line(start, end).unwrap();
2632 assert!(ctrl.is_connect_line());
2633 }
2634
2635 #[test]
2636 fn polygon_extreme_coords_no_panic() {
2637 let vertices = vec![
2639 ShapePoint::new(i32::MIN, 0),
2640 ShapePoint::new(i32::MAX, 0),
2641 ShapePoint::new(0, i32::MAX),
2642 ];
2643 let _ = Control::polygon(vertices);
2645 }
2646
2647 #[test]
2648 fn curve_extreme_coords_no_panic() {
2649 let points = vec![ShapePoint::new(i32::MIN, i32::MIN), ShapePoint::new(i32::MAX, i32::MAX)];
2650 let _ = Control::curve(points);
2651 }
2652}