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 Polygon {
374 vertices: Vec<ShapePoint>,
376 width: HwpUnit,
378 height: HwpUnit,
380 horz_offset: i32,
382 vert_offset: i32,
384 paragraphs: Vec<Paragraph>,
386 caption: Option<Caption>,
388 style: Option<ShapeStyle>,
390 },
391
392 Equation {
398 script: String,
400 width: HwpUnit,
402 height: HwpUnit,
404 base_line: u32,
406 text_color: Color,
408 font: String,
410 },
411
412 Chart {
417 chart_type: ChartType,
419 data: ChartData,
421 width: HwpUnit,
423 height: HwpUnit,
425 title: Option<String>,
427 legend: LegendPosition,
429 grouping: ChartGrouping,
431 bar_shape: Option<BarShape>,
433 explosion: Option<u32>,
435 of_pie_type: Option<OfPieType>,
437 radar_style: Option<RadarStyle>,
439 wireframe: Option<bool>,
441 bubble_3d: Option<bool>,
443 scatter_style: Option<ScatterStyle>,
445 show_markers: Option<bool>,
447 stock_variant: Option<StockVariant>,
452 },
453
454 Dutmal {
457 main_text: String,
459 sub_text: String,
461 position: DutmalPosition,
463 sz_ratio: u32,
465 align: DutmalAlign,
467 },
468
469 Compose {
472 compose_text: String,
474 circle_type: String,
476 char_sz: i32,
478 compose_type: String,
480 },
481
482 Arc {
485 arc_type: ArcType,
487 center: ShapePoint,
489 axis1: ShapePoint,
491 axis2: ShapePoint,
493 start1: ShapePoint,
495 end1: ShapePoint,
497 start2: ShapePoint,
499 end2: ShapePoint,
501 width: HwpUnit,
503 height: HwpUnit,
505 horz_offset: i32,
507 vert_offset: i32,
509 caption: Option<Caption>,
511 style: Option<ShapeStyle>,
513 },
514
515 Curve {
518 points: Vec<ShapePoint>,
520 segment_types: Vec<CurveSegmentType>,
522 width: HwpUnit,
524 height: HwpUnit,
526 horz_offset: i32,
528 vert_offset: i32,
530 caption: Option<Caption>,
532 style: Option<ShapeStyle>,
534 },
535
536 ConnectLine {
539 start: ShapePoint,
541 end: ShapePoint,
543 control_points: Vec<ShapePoint>,
545 connect_type: String,
547 width: HwpUnit,
549 height: HwpUnit,
551 horz_offset: i32,
553 vert_offset: i32,
555 caption: Option<Caption>,
557 style: Option<ShapeStyle>,
559 },
560
561 Bookmark {
564 name: String,
566 bookmark_type: BookmarkType,
568 },
569
570 CrossRef {
573 target_name: String,
575 ref_type: RefType,
577 content_type: RefContentType,
579 as_hyperlink: bool,
581 },
582
583 Field {
586 field_type: FieldType,
588 hint_text: Option<String>,
590 help_text: Option<String>,
592 },
593
594 Memo {
597 content: Vec<Paragraph>,
599 author: String,
601 date: String,
603 },
604
605 IndexMark {
608 primary: String,
610 secondary: Option<String>,
612 },
613
614 Unknown {
619 tag: String,
621 data: Option<String>,
623 },
624}
625
626#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
628#[non_exhaustive]
629pub enum DutmalPosition {
630 #[default]
632 Top,
633 Bottom,
635 Right,
637 Left,
639}
640
641#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
643#[non_exhaustive]
644pub enum DutmalAlign {
645 #[default]
647 Center,
648 Left,
650 Right,
652}
653
654impl Control {
655 pub fn is_text_box(&self) -> bool {
657 matches!(self, Self::TextBox { .. })
658 }
659
660 pub fn is_hyperlink(&self) -> bool {
662 matches!(self, Self::Hyperlink { .. })
663 }
664
665 pub fn is_footnote(&self) -> bool {
667 matches!(self, Self::Footnote { .. })
668 }
669
670 pub fn is_endnote(&self) -> bool {
672 matches!(self, Self::Endnote { .. })
673 }
674
675 pub fn is_line(&self) -> bool {
677 matches!(self, Self::Line { .. })
678 }
679
680 pub fn is_ellipse(&self) -> bool {
682 matches!(self, Self::Ellipse { .. })
683 }
684
685 pub fn is_polygon(&self) -> bool {
687 matches!(self, Self::Polygon { .. })
688 }
689
690 pub fn is_equation(&self) -> bool {
692 matches!(self, Self::Equation { .. })
693 }
694
695 pub fn is_chart(&self) -> bool {
697 matches!(self, Self::Chart { .. })
698 }
699
700 pub fn is_unknown(&self) -> bool {
702 matches!(self, Self::Unknown { .. })
703 }
704
705 pub fn is_dutmal(&self) -> bool {
707 matches!(self, Self::Dutmal { .. })
708 }
709
710 pub fn is_compose(&self) -> bool {
712 matches!(self, Self::Compose { .. })
713 }
714
715 pub fn is_arc(&self) -> bool {
717 matches!(self, Self::Arc { .. })
718 }
719
720 pub fn is_curve(&self) -> bool {
722 matches!(self, Self::Curve { .. })
723 }
724
725 pub fn is_connect_line(&self) -> bool {
727 matches!(self, Self::ConnectLine { .. })
728 }
729
730 pub fn is_bookmark(&self) -> bool {
732 matches!(self, Self::Bookmark { .. })
733 }
734
735 pub fn is_cross_ref(&self) -> bool {
737 matches!(self, Self::CrossRef { .. })
738 }
739
740 pub fn is_field(&self) -> bool {
742 matches!(self, Self::Field { .. })
743 }
744
745 pub fn is_memo(&self) -> bool {
747 matches!(self, Self::Memo { .. })
748 }
749
750 pub fn is_index_mark(&self) -> bool {
752 matches!(self, Self::IndexMark { .. })
753 }
754
755 pub fn bookmark(name: &str) -> Self {
766 Self::Bookmark { name: name.to_string(), bookmark_type: BookmarkType::Point }
767 }
768
769 pub fn field(hint: &str) -> Self {
780 Self::Field {
781 field_type: FieldType::ClickHere,
782 hint_text: Some(hint.to_string()),
783 help_text: None,
784 }
785 }
786
787 pub fn index_mark(primary: &str) -> Self {
798 Self::IndexMark { primary: primary.to_string(), secondary: None }
799 }
800
801 pub fn memo(content: Vec<Paragraph>, author: &str, date: &str) -> Self {
815 Self::Memo { content, author: author.to_string(), date: date.to_string() }
816 }
817
818 pub fn cross_ref(target: &str, ref_type: RefType, content_type: RefContentType) -> Self {
830 Self::CrossRef {
831 target_name: target.to_string(),
832 ref_type,
833 content_type,
834 as_hyperlink: false,
835 }
836 }
837
838 pub fn chart(chart_type: ChartType, data: ChartData) -> Self {
853 Self::Chart {
854 chart_type,
855 data,
856 width: HwpUnit::new(32250).expect("32250 is valid"),
857 height: HwpUnit::new(18750).expect("18750 is valid"),
858 title: None,
859 legend: LegendPosition::default(),
860 grouping: ChartGrouping::default(),
861 bar_shape: None,
862 explosion: None,
863 of_pie_type: None,
864 radar_style: None,
865 wireframe: None,
866 bubble_3d: None,
867 scatter_style: None,
868 show_markers: None,
869 stock_variant: None,
870 }
871 }
872
873 pub fn equation(script: &str) -> Self {
887 Self::Equation {
888 script: script.to_string(),
889 width: HwpUnit::new(8779).expect("8779 is valid"),
890 height: HwpUnit::new(2600).expect("2600 is valid"),
891 base_line: 71,
892 text_color: Color::BLACK,
893 font: "HancomEQN".to_string(),
894 }
895 }
896
897 pub fn text_box(paragraphs: Vec<Paragraph>, width: HwpUnit, height: HwpUnit) -> Self {
915 Self::TextBox {
916 paragraphs,
917 width,
918 height,
919 horz_offset: 0,
920 vert_offset: 0,
921 caption: None,
922 style: None,
923 }
924 }
925
926 pub fn footnote(paragraphs: Vec<Paragraph>) -> Self {
946 Self::Footnote { inst_id: None, paragraphs }
947 }
948
949 pub fn endnote(paragraphs: Vec<Paragraph>) -> Self {
969 Self::Endnote { inst_id: None, paragraphs }
970 }
971
972 pub fn footnote_with_id(inst_id: u32, paragraphs: Vec<Paragraph>) -> Self {
988 Self::Footnote { inst_id: Some(inst_id), paragraphs }
989 }
990
991 pub fn endnote_with_id(inst_id: u32, paragraphs: Vec<Paragraph>) -> Self {
1007 Self::Endnote { inst_id: Some(inst_id), paragraphs }
1008 }
1009
1010 pub fn ellipse(width: HwpUnit, height: HwpUnit) -> Self {
1027 let w = width.as_i32();
1028 let h = height.as_i32();
1029 Self::Ellipse {
1030 center: ShapePoint::new(w / 2, h / 2),
1031 axis1: ShapePoint::new(w, h / 2),
1032 axis2: ShapePoint::new(w / 2, h),
1033 width,
1034 height,
1035 horz_offset: 0,
1036 vert_offset: 0,
1037 paragraphs: vec![],
1038 caption: None,
1039 style: None,
1040 }
1041 }
1042
1043 pub fn ellipse_with_text(width: HwpUnit, height: HwpUnit, paragraphs: Vec<Paragraph>) -> Self {
1063 let w = width.as_i32();
1064 let h = height.as_i32();
1065 Self::Ellipse {
1066 center: ShapePoint::new(w / 2, h / 2),
1067 axis1: ShapePoint::new(w, h / 2),
1068 axis2: ShapePoint::new(w / 2, h),
1069 width,
1070 height,
1071 horz_offset: 0,
1072 vert_offset: 0,
1073 paragraphs,
1074 caption: None,
1075 style: None,
1076 }
1077 }
1078
1079 pub fn polygon(vertices: Vec<ShapePoint>) -> CoreResult<Self> {
1104 if vertices.len() < 3 {
1105 return Err(CoreError::InvalidStructure {
1106 context: "Control::polygon".to_string(),
1107 reason: format!("polygon requires at least 3 vertices, got {}", vertices.len()),
1108 });
1109 }
1110 let min_x = vertices.iter().map(|p| p.x as i64).min().unwrap_or(0);
1111 let max_x = vertices.iter().map(|p| p.x as i64).max().unwrap_or(0);
1112 let min_y = vertices.iter().map(|p| p.y as i64).min().unwrap_or(0);
1113 let max_y = vertices.iter().map(|p| p.y as i64).max().unwrap_or(0);
1114 let bbox_w = i32::try_from((max_x - min_x).max(0)).unwrap_or(i32::MAX);
1115 let bbox_h = i32::try_from((max_y - min_y).max(0)).unwrap_or(i32::MAX);
1116 let width = HwpUnit::new(bbox_w).map_err(|_| CoreError::InvalidStructure {
1117 context: "Control::polygon".into(),
1118 reason: format!("bounding box width {bbox_w} exceeds HwpUnit range"),
1119 })?;
1120 let height = HwpUnit::new(bbox_h).map_err(|_| CoreError::InvalidStructure {
1121 context: "Control::polygon".into(),
1122 reason: format!("bounding box height {bbox_h} exceeds HwpUnit range"),
1123 })?;
1124 Ok(Self::Polygon {
1125 vertices,
1126 width,
1127 height,
1128 horz_offset: 0,
1129 vert_offset: 0,
1130 paragraphs: vec![],
1131 caption: None,
1132 style: None,
1133 })
1134 }
1135
1136 pub fn line(start: ShapePoint, end: ShapePoint) -> CoreResult<Self> {
1159 if start == end {
1160 return Err(CoreError::InvalidStructure {
1161 context: "Control::line".to_string(),
1162 reason: "start and end points are identical (degenerate line)".to_string(),
1163 });
1164 }
1165 let min_x = start.x.min(end.x);
1168 let min_y = start.y.min(end.y);
1169 let norm_start =
1170 ShapePoint::new(start.x.saturating_sub(min_x), start.y.saturating_sub(min_y));
1171 let norm_end = ShapePoint::new(end.x.saturating_sub(min_x), end.y.saturating_sub(min_y));
1172
1173 let raw_w =
1174 i32::try_from(((end.x as i64) - (start.x as i64)).unsigned_abs()).unwrap_or(i32::MAX);
1175 let raw_h =
1176 i32::try_from(((end.y as i64) - (start.y as i64)).unsigned_abs()).unwrap_or(i32::MAX);
1177 let raw_w = raw_w.max(100);
1180 let raw_h = raw_h.max(100);
1181 let width = HwpUnit::new(raw_w).unwrap_or_else(|_| HwpUnit::new(100).expect("valid"));
1182 let height = HwpUnit::new(raw_h).unwrap_or_else(|_| HwpUnit::new(100).expect("valid"));
1183 Ok(Self::Line {
1184 start: norm_start,
1185 end: norm_end,
1186 width,
1187 height,
1188 horz_offset: 0,
1189 vert_offset: 0,
1190 caption: None,
1191 style: None,
1192 })
1193 }
1194
1195 pub fn horizontal_line(width: HwpUnit) -> Self {
1213 let w = width.as_i32();
1214 Self::Line {
1215 start: ShapePoint::new(0, 0),
1216 end: ShapePoint::new(w, 0),
1217 width,
1218 height: HwpUnit::new(100).expect("100 is valid"),
1219 horz_offset: 0,
1220 vert_offset: 0,
1221 caption: None,
1222 style: None,
1223 }
1224 }
1225
1226 pub fn dutmal(main_text: impl Into<String>, sub_text: impl Into<String>) -> Self {
1239 Self::Dutmal {
1240 main_text: main_text.into(),
1241 sub_text: sub_text.into(),
1242 position: DutmalPosition::Top,
1243 sz_ratio: 0,
1244 align: DutmalAlign::Center,
1245 }
1246 }
1247
1248 pub fn compose(text: impl Into<String>) -> Self {
1262 Self::Compose {
1263 compose_text: text.into(),
1264 circle_type: "SHAPE_REVERSAL_TIRANGLE".to_string(), char_sz: -3,
1266 compose_type: "SPREAD".to_string(),
1267 }
1268 }
1269
1270 pub fn arc(arc_type: ArcType, width: HwpUnit, height: HwpUnit) -> Self {
1287 let w = width.as_i32();
1288 let h = height.as_i32();
1289 Self::Arc {
1290 arc_type,
1291 center: ShapePoint::new(w / 2, h / 2),
1292 axis1: ShapePoint::new(w, h / 2),
1293 axis2: ShapePoint::new(w / 2, h),
1294 start1: ShapePoint::new(w, h / 2),
1295 end1: ShapePoint::new(w / 2, 0),
1296 start2: ShapePoint::new(w, h / 2),
1297 end2: ShapePoint::new(w / 2, 0),
1298 width,
1299 height,
1300 horz_offset: 0,
1301 vert_offset: 0,
1302 caption: None,
1303 style: None,
1304 }
1305 }
1306
1307 pub fn curve(points: Vec<ShapePoint>) -> CoreResult<Self> {
1332 if points.len() < 2 {
1333 return Err(CoreError::InvalidStructure {
1334 context: "Control::curve".to_string(),
1335 reason: format!("curve requires at least 2 points, got {}", points.len()),
1336 });
1337 }
1338 let min_x = points.iter().map(|p| p.x as i64).min().unwrap_or(0);
1339 let max_x = points.iter().map(|p| p.x as i64).max().unwrap_or(0);
1340 let min_y = points.iter().map(|p| p.y as i64).min().unwrap_or(0);
1341 let max_y = points.iter().map(|p| p.y as i64).max().unwrap_or(0);
1342 let bbox_w = i32::try_from((max_x - min_x).max(1)).unwrap_or(i32::MAX);
1343 let bbox_h = i32::try_from((max_y - min_y).max(1)).unwrap_or(i32::MAX);
1344 let width = HwpUnit::new(bbox_w).map_err(|_| CoreError::InvalidStructure {
1345 context: "Control::curve".into(),
1346 reason: format!("bounding box width {bbox_w} exceeds HwpUnit range"),
1347 })?;
1348 let height = HwpUnit::new(bbox_h).map_err(|_| CoreError::InvalidStructure {
1349 context: "Control::curve".into(),
1350 reason: format!("bounding box height {bbox_h} exceeds HwpUnit range"),
1351 })?;
1352 let seg_count = points.len().saturating_sub(1);
1353 Ok(Self::Curve {
1354 points,
1355 segment_types: vec![CurveSegmentType::Curve; seg_count],
1356 width,
1357 height,
1358 horz_offset: 0,
1359 vert_offset: 0,
1360 caption: None,
1361 style: None,
1362 })
1363 }
1364
1365 pub fn connect_line(start: ShapePoint, end: ShapePoint) -> CoreResult<Self> {
1387 if start == end {
1388 return Err(CoreError::InvalidStructure {
1389 context: "Control::connect_line".to_string(),
1390 reason: "start and end points are identical (degenerate line)".to_string(),
1391 });
1392 }
1393 let min_x = start.x.min(end.x);
1396 let min_y = start.y.min(end.y);
1397 let norm_start =
1398 ShapePoint::new(start.x.saturating_sub(min_x), start.y.saturating_sub(min_y));
1399 let norm_end = ShapePoint::new(end.x.saturating_sub(min_x), end.y.saturating_sub(min_y));
1400
1401 let raw_w =
1402 i32::try_from(((end.x as i64) - (start.x as i64)).unsigned_abs()).unwrap_or(i32::MAX);
1403 let raw_h =
1404 i32::try_from(((end.y as i64) - (start.y as i64)).unsigned_abs()).unwrap_or(i32::MAX);
1405 let raw_w = raw_w.max(100);
1406 let raw_h = raw_h.max(100);
1407 let width = HwpUnit::new(raw_w).unwrap_or_else(|_| HwpUnit::new(100).expect("valid"));
1408 let height = HwpUnit::new(raw_h).unwrap_or_else(|_| HwpUnit::new(100).expect("valid"));
1409 Ok(Self::ConnectLine {
1410 start: norm_start,
1411 end: norm_end,
1412 control_points: Vec::new(),
1413 connect_type: "STRAIGHT".to_string(),
1414 width,
1415 height,
1416 horz_offset: 0,
1417 vert_offset: 0,
1418 caption: None,
1419 style: None,
1420 })
1421 }
1422
1423 pub fn hyperlink(text: &str, url: &str) -> Self {
1434 Self::Hyperlink { text: text.to_string(), url: url.to_string() }
1435 }
1436}
1437
1438impl std::fmt::Display for Control {
1439 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1440 match self {
1441 Self::TextBox { paragraphs, .. } => {
1442 let n = paragraphs.len();
1443 let word = if n == 1 { "paragraph" } else { "paragraphs" };
1444 write!(f, "TextBox({n} {word})")
1445 }
1446 Self::Hyperlink { text, url } => {
1447 let preview: String =
1448 if text.len() > 30 { text.chars().take(30).collect() } else { text.clone() };
1449 write!(f, "Hyperlink(\"{preview}\" -> {url})")
1450 }
1451 Self::Footnote { paragraphs, .. } => {
1452 let n = paragraphs.len();
1453 let word = if n == 1 { "paragraph" } else { "paragraphs" };
1454 write!(f, "Footnote({n} {word})")
1455 }
1456 Self::Endnote { paragraphs, .. } => {
1457 let n = paragraphs.len();
1458 let word = if n == 1 { "paragraph" } else { "paragraphs" };
1459 write!(f, "Endnote({n} {word})")
1460 }
1461 Self::Line { .. } => {
1462 write!(f, "Line")
1463 }
1464 Self::Ellipse { paragraphs, .. } => {
1465 let n = paragraphs.len();
1466 let word = if n == 1 { "paragraph" } else { "paragraphs" };
1467 write!(f, "Ellipse({n} {word})")
1468 }
1469 Self::Polygon { vertices, paragraphs, .. } => {
1470 let nv = vertices.len();
1471 let np = paragraphs.len();
1472 let vw = if nv == 1 { "vertex" } else { "vertices" };
1473 let pw = if np == 1 { "paragraph" } else { "paragraphs" };
1474 write!(f, "Polygon({nv} {vw}, {np} {pw})")
1475 }
1476 Self::Chart { chart_type, data, .. } => {
1477 let series_count = match data {
1478 ChartData::Category { series, .. } => series.len(),
1479 ChartData::Xy { series } => series.len(),
1480 };
1481 write!(f, "Chart({chart_type:?}, {series_count} series)")
1482 }
1483 Self::Equation { script, .. } => {
1484 let preview: String = if script.len() > 30 {
1485 script.chars().take(30).collect()
1486 } else {
1487 script.clone()
1488 };
1489 write!(f, "Equation(\"{preview}\")")
1490 }
1491 Self::Dutmal { main_text, sub_text, .. } => {
1492 write!(f, "Dutmal(\"{main_text}\" / \"{sub_text}\")")
1493 }
1494 Self::Compose { compose_text, .. } => {
1495 write!(f, "Compose(\"{compose_text}\")")
1496 }
1497 Self::Arc { arc_type, .. } => {
1498 write!(f, "Arc({arc_type})")
1499 }
1500 Self::Curve { points, .. } => {
1501 write!(f, "Curve({} points)", points.len())
1502 }
1503 Self::ConnectLine { .. } => {
1504 write!(f, "ConnectLine")
1505 }
1506 Self::Bookmark { name, bookmark_type } => {
1507 write!(f, "Bookmark(\"{name}\", {bookmark_type})")
1508 }
1509 Self::CrossRef { target_name, ref_type, .. } => {
1510 write!(f, "CrossRef(\"{target_name}\", {ref_type})")
1511 }
1512 Self::Field { field_type, hint_text, .. } => {
1513 let hint = hint_text.as_deref().unwrap_or("");
1514 write!(f, "Field({field_type}, \"{hint}\")")
1515 }
1516 Self::Memo { content, author, .. } => {
1517 let n = content.len();
1518 let word = if n == 1 { "paragraph" } else { "paragraphs" };
1519 write!(f, "Memo({n} {word}, by {author})")
1520 }
1521 Self::IndexMark { primary, secondary } => {
1522 if let Some(sec) = secondary {
1523 write!(f, "IndexMark(\"{primary}\" / \"{sec}\")")
1524 } else {
1525 write!(f, "IndexMark(\"{primary}\")")
1526 }
1527 }
1528 Self::Unknown { tag, .. } => {
1529 write!(f, "Unknown({tag})")
1530 }
1531 }
1532 }
1533}
1534
1535#[cfg(test)]
1536mod tests {
1537 use super::*;
1538 use crate::run::Run;
1539 use hwpforge_foundation::{CharShapeIndex, Color, ParaShapeIndex};
1540
1541 fn simple_paragraph() -> Paragraph {
1542 Paragraph::with_runs(
1543 vec![Run::text("footnote text", CharShapeIndex::new(0))],
1544 ParaShapeIndex::new(0),
1545 )
1546 }
1547
1548 #[test]
1549 fn shape_style_default_all_none() {
1550 let s = ShapeStyle::default();
1551 assert!(s.line_color.is_none());
1552 assert!(s.fill_color.is_none());
1553 assert!(s.line_width.is_none());
1554 assert!(s.line_style.is_none());
1555 }
1556
1557 #[test]
1558 fn shape_style_with_typed_fields() {
1559 let s = ShapeStyle {
1560 line_color: Some(Color::from_rgb(255, 0, 0)),
1561 fill_color: Some(Color::from_rgb(0, 255, 0)),
1562 line_width: Some(100),
1563 line_style: Some(LineStyle::Dash),
1564 ..Default::default()
1565 };
1566 assert_eq!(s.line_color.unwrap(), Color::from_rgb(255, 0, 0));
1567 assert_eq!(s.fill_color.unwrap(), Color::from_rgb(0, 255, 0));
1568 assert_eq!(s.line_width.unwrap(), 100);
1569 assert_eq!(s.line_style.unwrap(), LineStyle::Dash);
1570 }
1571
1572 #[test]
1573 fn line_style_default() {
1574 assert_eq!(LineStyle::default(), LineStyle::Solid);
1575 }
1576
1577 #[test]
1578 fn line_style_display() {
1579 assert_eq!(LineStyle::Solid.to_string(), "SOLID");
1580 assert_eq!(LineStyle::Dash.to_string(), "DASH");
1581 assert_eq!(LineStyle::Dot.to_string(), "DOT");
1582 assert_eq!(LineStyle::DashDot.to_string(), "DASH_DOT");
1583 assert_eq!(LineStyle::DashDotDot.to_string(), "DASH_DOT_DOT");
1584 assert_eq!(LineStyle::None.to_string(), "NONE");
1585 }
1586
1587 #[test]
1588 fn line_style_from_str() {
1589 assert_eq!("SOLID".parse::<LineStyle>().unwrap(), LineStyle::Solid);
1590 assert_eq!("Dash".parse::<LineStyle>().unwrap(), LineStyle::Dash);
1591 assert_eq!("dot".parse::<LineStyle>().unwrap(), LineStyle::Dot);
1592 assert_eq!("DASH_DOT".parse::<LineStyle>().unwrap(), LineStyle::DashDot);
1593 assert_eq!("DashDotDot".parse::<LineStyle>().unwrap(), LineStyle::DashDotDot);
1594 assert_eq!("NONE".parse::<LineStyle>().unwrap(), LineStyle::None);
1595 assert!("INVALID".parse::<LineStyle>().is_err());
1596 }
1597
1598 #[test]
1599 fn line_style_serde_roundtrip() {
1600 for style in [
1601 LineStyle::Solid,
1602 LineStyle::Dash,
1603 LineStyle::Dot,
1604 LineStyle::DashDot,
1605 LineStyle::DashDotDot,
1606 LineStyle::None,
1607 ] {
1608 let json = serde_json::to_string(&style).unwrap();
1609 let back: LineStyle = serde_json::from_str(&json).unwrap();
1610 assert_eq!(style, back);
1611 }
1612 }
1613
1614 #[test]
1615 fn text_box_construction() {
1616 let ctrl = Control::TextBox {
1617 paragraphs: vec![simple_paragraph()],
1618 width: HwpUnit::from_mm(80.0).unwrap(),
1619 height: HwpUnit::from_mm(40.0).unwrap(),
1620 horz_offset: 0,
1621 vert_offset: 0,
1622 caption: None,
1623 style: None,
1624 };
1625 assert!(ctrl.is_text_box());
1626 assert!(!ctrl.is_hyperlink());
1627 assert!(!ctrl.is_footnote());
1628 assert!(!ctrl.is_endnote());
1629 assert!(!ctrl.is_unknown());
1630 }
1631
1632 #[test]
1633 fn hyperlink_construction() {
1634 let ctrl = Control::Hyperlink {
1635 text: "Click".to_string(),
1636 url: "https://example.com".to_string(),
1637 };
1638 assert!(ctrl.is_hyperlink());
1639 assert!(!ctrl.is_text_box());
1640 }
1641
1642 #[test]
1643 fn footnote_construction() {
1644 let ctrl = Control::Footnote { inst_id: None, paragraphs: vec![simple_paragraph()] };
1645 assert!(ctrl.is_footnote());
1646 assert!(!ctrl.is_text_box());
1647 assert!(!ctrl.is_endnote());
1648 }
1649
1650 #[test]
1651 fn endnote_construction() {
1652 let ctrl = Control::Endnote { inst_id: Some(123456), paragraphs: vec![simple_paragraph()] };
1653 assert!(ctrl.is_endnote());
1654 assert!(!ctrl.is_footnote());
1655 assert!(!ctrl.is_text_box());
1656 }
1657
1658 #[test]
1659 fn unknown_construction() {
1660 let ctrl = Control::Unknown {
1661 tag: "custom:widget".to_string(),
1662 data: Some("<data>value</data>".to_string()),
1663 };
1664 assert!(ctrl.is_unknown());
1665 }
1666
1667 #[test]
1668 fn unknown_without_data() {
1669 let ctrl = Control::Unknown { tag: "header".to_string(), data: None };
1670 assert!(ctrl.is_unknown());
1671 }
1672
1673 #[test]
1674 fn display_text_box() {
1675 let ctrl = Control::TextBox {
1676 paragraphs: vec![simple_paragraph(), simple_paragraph()],
1677 width: HwpUnit::from_mm(80.0).unwrap(),
1678 height: HwpUnit::from_mm(40.0).unwrap(),
1679 horz_offset: 0,
1680 vert_offset: 0,
1681 caption: None,
1682 style: None,
1683 };
1684 assert_eq!(ctrl.to_string(), "TextBox(2 paragraphs)");
1685 }
1686
1687 #[test]
1688 fn display_hyperlink() {
1689 let ctrl =
1690 Control::Hyperlink { text: "Short".to_string(), url: "https://x.com".to_string() };
1691 let s = ctrl.to_string();
1692 assert!(s.contains("Short"), "display: {s}");
1693 assert!(s.contains("https://x.com"), "display: {s}");
1694 }
1695
1696 #[test]
1697 fn display_hyperlink_long_text_truncated() {
1698 let ctrl =
1699 Control::Hyperlink { text: "A".repeat(100), url: "https://example.com".to_string() };
1700 let s = ctrl.to_string();
1701 assert!(s.contains(&"A".repeat(30)), "display: {s}");
1703 assert!(!s.contains(&"A".repeat(31)), "display: {s}");
1704 }
1705
1706 #[test]
1707 fn display_footnote() {
1708 let ctrl = Control::Footnote { inst_id: None, paragraphs: vec![simple_paragraph()] };
1709 assert_eq!(ctrl.to_string(), "Footnote(1 paragraph)");
1710 }
1711
1712 #[test]
1713 fn display_endnote() {
1714 let ctrl = Control::Endnote { inst_id: Some(999), paragraphs: vec![simple_paragraph()] };
1715 assert_eq!(ctrl.to_string(), "Endnote(1 paragraph)");
1716 }
1717
1718 #[test]
1719 fn display_unknown() {
1720 let ctrl = Control::Unknown { tag: "bookmark".to_string(), data: None };
1721 assert_eq!(ctrl.to_string(), "Unknown(bookmark)");
1722 }
1723
1724 #[test]
1725 fn equality() {
1726 let a = Control::Hyperlink { text: "A".to_string(), url: "B".to_string() };
1727 let b = Control::Hyperlink { text: "A".to_string(), url: "B".to_string() };
1728 let c = Control::Hyperlink { text: "A".to_string(), url: "C".to_string() };
1729 assert_eq!(a, b);
1730 assert_ne!(a, c);
1731 }
1732
1733 #[test]
1734 fn serde_roundtrip_text_box() {
1735 let ctrl = Control::TextBox {
1736 paragraphs: vec![simple_paragraph()],
1737 width: HwpUnit::from_mm(80.0).unwrap(),
1738 height: HwpUnit::from_mm(40.0).unwrap(),
1739 horz_offset: 0,
1740 vert_offset: 0,
1741 caption: None,
1742 style: None,
1743 };
1744 let json = serde_json::to_string(&ctrl).unwrap();
1745 let back: Control = serde_json::from_str(&json).unwrap();
1746 assert_eq!(ctrl, back);
1747 }
1748
1749 #[test]
1750 fn serde_roundtrip_hyperlink() {
1751 let ctrl = Control::Hyperlink {
1752 text: "link text".to_string(),
1753 url: "https://rust-lang.org".to_string(),
1754 };
1755 let json = serde_json::to_string(&ctrl).unwrap();
1756 let back: Control = serde_json::from_str(&json).unwrap();
1757 assert_eq!(ctrl, back);
1758 }
1759
1760 #[test]
1761 fn serde_roundtrip_footnote() {
1762 let ctrl = Control::Footnote { inst_id: Some(12345), paragraphs: vec![simple_paragraph()] };
1763 let json = serde_json::to_string(&ctrl).unwrap();
1764 let back: Control = serde_json::from_str(&json).unwrap();
1765 assert_eq!(ctrl, back);
1766 }
1767
1768 #[test]
1769 fn serde_roundtrip_endnote() {
1770 let ctrl = Control::Endnote { inst_id: None, paragraphs: vec![simple_paragraph()] };
1771 let json = serde_json::to_string(&ctrl).unwrap();
1772 let back: Control = serde_json::from_str(&json).unwrap();
1773 assert_eq!(ctrl, back);
1774 }
1775
1776 #[test]
1777 fn serde_roundtrip_unknown() {
1778 let ctrl = Control::Unknown { tag: "test".to_string(), data: Some("payload".to_string()) };
1779 let json = serde_json::to_string(&ctrl).unwrap();
1780 let back: Control = serde_json::from_str(&json).unwrap();
1781 assert_eq!(ctrl, back);
1782 }
1783
1784 #[test]
1787 fn line_construction() {
1788 let ctrl = Control::Line {
1789 start: ShapePoint { x: 0, y: 0 },
1790 end: ShapePoint { x: 1000, y: 500 },
1791 width: HwpUnit::from_mm(50.0).unwrap(),
1792 height: HwpUnit::from_mm(25.0).unwrap(),
1793 horz_offset: 0,
1794 vert_offset: 0,
1795 caption: None,
1796 style: None,
1797 };
1798 assert!(ctrl.is_line());
1799 assert!(!ctrl.is_text_box());
1800 assert!(!ctrl.is_ellipse());
1801 assert!(!ctrl.is_polygon());
1802 }
1803
1804 #[test]
1805 fn ellipse_construction() {
1806 let ctrl = Control::Ellipse {
1807 center: ShapePoint { x: 500, y: 500 },
1808 axis1: ShapePoint { x: 1000, y: 500 },
1809 axis2: ShapePoint { x: 500, y: 1000 },
1810 width: HwpUnit::from_mm(40.0).unwrap(),
1811 height: HwpUnit::from_mm(30.0).unwrap(),
1812 horz_offset: 0,
1813 vert_offset: 0,
1814 paragraphs: vec![],
1815 caption: None,
1816 style: None,
1817 };
1818 assert!(ctrl.is_ellipse());
1819 assert!(!ctrl.is_line());
1820 assert!(!ctrl.is_polygon());
1821 }
1822
1823 #[test]
1824 fn ellipse_with_paragraphs() {
1825 let ctrl = Control::Ellipse {
1826 center: ShapePoint { x: 500, y: 500 },
1827 axis1: ShapePoint { x: 1000, y: 500 },
1828 axis2: ShapePoint { x: 500, y: 1000 },
1829 width: HwpUnit::from_mm(40.0).unwrap(),
1830 height: HwpUnit::from_mm(30.0).unwrap(),
1831 horz_offset: 0,
1832 vert_offset: 0,
1833 paragraphs: vec![simple_paragraph()],
1834 caption: None,
1835 style: None,
1836 };
1837 assert!(ctrl.is_ellipse());
1838 assert_eq!(ctrl.to_string(), "Ellipse(1 paragraph)");
1839 }
1840
1841 #[test]
1842 fn polygon_construction() {
1843 let ctrl = Control::Polygon {
1844 vertices: vec![
1845 ShapePoint { x: 0, y: 0 },
1846 ShapePoint { x: 1000, y: 0 },
1847 ShapePoint { x: 500, y: 1000 },
1848 ],
1849 width: HwpUnit::from_mm(50.0).unwrap(),
1850 height: HwpUnit::from_mm(50.0).unwrap(),
1851 horz_offset: 0,
1852 vert_offset: 0,
1853 paragraphs: vec![],
1854 caption: None,
1855 style: None,
1856 };
1857 assert!(ctrl.is_polygon());
1858 assert!(!ctrl.is_line());
1859 assert!(!ctrl.is_ellipse());
1860 assert_eq!(ctrl.to_string(), "Polygon(3 vertices, 0 paragraphs)");
1861 }
1862
1863 #[test]
1864 fn display_line() {
1865 let ctrl = Control::Line {
1866 start: ShapePoint { x: 0, y: 0 },
1867 end: ShapePoint { x: 100, y: 200 },
1868 width: HwpUnit::from_mm(10.0).unwrap(),
1869 height: HwpUnit::from_mm(5.0).unwrap(),
1870 horz_offset: 0,
1871 vert_offset: 0,
1872 caption: None,
1873 style: None,
1874 };
1875 assert_eq!(ctrl.to_string(), "Line");
1876 }
1877
1878 #[test]
1879 fn serde_roundtrip_line() {
1880 let ctrl = Control::Line {
1881 start: ShapePoint { x: 100, y: 200 },
1882 end: ShapePoint { x: 300, y: 400 },
1883 width: HwpUnit::from_mm(20.0).unwrap(),
1884 height: HwpUnit::from_mm(10.0).unwrap(),
1885 horz_offset: 0,
1886 vert_offset: 0,
1887 caption: None,
1888 style: None,
1889 };
1890 let json = serde_json::to_string(&ctrl).unwrap();
1891 let back: Control = serde_json::from_str(&json).unwrap();
1892 assert_eq!(ctrl, back);
1893 }
1894
1895 #[test]
1896 fn serde_roundtrip_ellipse() {
1897 let ctrl = Control::Ellipse {
1898 center: ShapePoint { x: 500, y: 500 },
1899 axis1: ShapePoint { x: 1000, y: 500 },
1900 axis2: ShapePoint { x: 500, y: 1000 },
1901 width: HwpUnit::from_mm(40.0).unwrap(),
1902 height: HwpUnit::from_mm(30.0).unwrap(),
1903 horz_offset: 0,
1904 vert_offset: 0,
1905 paragraphs: vec![simple_paragraph()],
1906 caption: None,
1907 style: None,
1908 };
1909 let json = serde_json::to_string(&ctrl).unwrap();
1910 let back: Control = serde_json::from_str(&json).unwrap();
1911 assert_eq!(ctrl, back);
1912 }
1913
1914 #[test]
1915 fn serde_roundtrip_polygon() {
1916 let ctrl = Control::Polygon {
1917 vertices: vec![
1918 ShapePoint { x: 0, y: 0 },
1919 ShapePoint { x: 1000, y: 0 },
1920 ShapePoint { x: 500, y: 1000 },
1921 ],
1922 width: HwpUnit::from_mm(50.0).unwrap(),
1923 height: HwpUnit::from_mm(50.0).unwrap(),
1924 horz_offset: 0,
1925 vert_offset: 0,
1926 paragraphs: vec![],
1927 caption: None,
1928 style: None,
1929 };
1930 let json = serde_json::to_string(&ctrl).unwrap();
1931 let back: Control = serde_json::from_str(&json).unwrap();
1932 assert_eq!(ctrl, back);
1933 }
1934
1935 #[test]
1936 fn shape_point_equality() {
1937 let a = ShapePoint { x: 10, y: 20 };
1938 let b = ShapePoint { x: 10, y: 20 };
1939 let c = ShapePoint { x: 10, y: 30 };
1940 assert_eq!(a, b);
1941 assert_ne!(a, c);
1942 }
1943
1944 #[test]
1945 fn shape_point_new() {
1946 let pt = ShapePoint::new(100, 200);
1947 assert_eq!(pt.x, 100);
1948 assert_eq!(pt.y, 200);
1949 }
1950
1951 #[test]
1952 fn shape_point_serde_roundtrip() {
1953 let pt = ShapePoint::new(500, 750);
1954 let json = serde_json::to_string(&pt).unwrap();
1955 let back: ShapePoint = serde_json::from_str(&json).unwrap();
1956 assert_eq!(pt, back);
1957 }
1958
1959 #[test]
1962 fn equation_constructor_defaults() {
1963 let ctrl = Control::equation("{a+b} over {c+d}");
1964 assert!(ctrl.is_equation());
1965 match ctrl {
1966 Control::Equation { script, width, height, base_line, text_color, ref font } => {
1967 assert_eq!(script, "{a+b} over {c+d}");
1968 assert_eq!(width, HwpUnit::new(8779).unwrap());
1969 assert_eq!(height, HwpUnit::new(2600).unwrap());
1970 assert_eq!(base_line, 71);
1971 assert_eq!(text_color, Color::BLACK);
1972 assert_eq!(font, "HancomEQN");
1973 }
1974 _ => panic!("expected Equation"),
1975 }
1976 }
1977
1978 #[test]
1979 fn equation_constructor_empty_script() {
1980 let ctrl = Control::equation("");
1981 assert!(ctrl.is_equation());
1982 }
1983
1984 #[test]
1985 fn text_box_constructor_defaults() {
1986 let width = HwpUnit::from_mm(80.0).unwrap();
1987 let height = HwpUnit::from_mm(40.0).unwrap();
1988 let ctrl = Control::text_box(vec![simple_paragraph()], width, height);
1989 assert!(ctrl.is_text_box());
1990 match ctrl {
1991 Control::TextBox { paragraphs, horz_offset, vert_offset, caption, style, .. } => {
1992 assert_eq!(paragraphs.len(), 1);
1993 assert_eq!(horz_offset, 0);
1994 assert_eq!(vert_offset, 0);
1995 assert!(caption.is_none());
1996 assert!(style.is_none());
1997 }
1998 _ => panic!("expected TextBox"),
1999 }
2000 }
2001
2002 #[test]
2003 fn footnote_constructor_defaults() {
2004 let ctrl = Control::footnote(vec![simple_paragraph()]);
2005 assert!(ctrl.is_footnote());
2006 match ctrl {
2007 Control::Footnote { inst_id, paragraphs } => {
2008 assert!(inst_id.is_none());
2009 assert_eq!(paragraphs.len(), 1);
2010 }
2011 _ => panic!("expected Footnote"),
2012 }
2013 }
2014
2015 #[test]
2016 fn endnote_constructor_defaults() {
2017 let ctrl = Control::endnote(vec![simple_paragraph()]);
2018 assert!(ctrl.is_endnote());
2019 match ctrl {
2020 Control::Endnote { inst_id, paragraphs } => {
2021 assert!(inst_id.is_none());
2022 assert_eq!(paragraphs.len(), 1);
2023 }
2024 _ => panic!("expected Endnote"),
2025 }
2026 }
2027
2028 #[test]
2029 fn ellipse_constructor_geometry() {
2030 let width = HwpUnit::from_mm(40.0).unwrap();
2031 let height = HwpUnit::from_mm(30.0).unwrap();
2032 let ctrl = Control::ellipse(width, height);
2033 assert!(ctrl.is_ellipse());
2034 match &ctrl {
2035 Control::Ellipse {
2036 center,
2037 axis1,
2038 axis2,
2039 horz_offset,
2040 vert_offset,
2041 paragraphs,
2042 caption,
2043 style,
2044 ..
2045 } => {
2046 let w = width.as_i32();
2047 let h = height.as_i32();
2048 assert_eq!(*center, ShapePoint::new(w / 2, h / 2));
2049 assert_eq!(*axis1, ShapePoint::new(w, h / 2));
2050 assert_eq!(*axis2, ShapePoint::new(w / 2, h));
2051 assert_eq!(*horz_offset, 0);
2052 assert_eq!(*vert_offset, 0);
2053 assert!(paragraphs.is_empty());
2054 assert!(caption.is_none());
2055 assert!(style.is_none());
2056 }
2057 _ => panic!("expected Ellipse"),
2058 }
2059 }
2060
2061 #[test]
2062 fn polygon_constructor_triangle() {
2063 let vertices =
2064 vec![ShapePoint::new(0, 1000), ShapePoint::new(500, 0), ShapePoint::new(1000, 1000)];
2065 let ctrl = Control::polygon(vertices).unwrap();
2066 assert!(ctrl.is_polygon());
2067 match &ctrl {
2068 Control::Polygon {
2069 vertices,
2070 width,
2071 height,
2072 horz_offset,
2073 vert_offset,
2074 paragraphs,
2075 caption,
2076 style,
2077 } => {
2078 assert_eq!(vertices.len(), 3);
2079 assert_eq!(*width, HwpUnit::new(1000).unwrap());
2081 assert_eq!(*height, HwpUnit::new(1000).unwrap());
2082 assert_eq!(*horz_offset, 0);
2083 assert_eq!(*vert_offset, 0);
2084 assert!(paragraphs.is_empty());
2085 assert!(caption.is_none());
2086 assert!(style.is_none());
2087 }
2088 _ => panic!("expected Polygon"),
2089 }
2090 }
2091
2092 #[test]
2093 fn polygon_constructor_fewer_than_3_vertices_errors() {
2094 assert!(Control::polygon(vec![]).is_err());
2095 assert!(Control::polygon(vec![ShapePoint::new(0, 0)]).is_err());
2096 assert!(Control::polygon(vec![ShapePoint::new(0, 0), ShapePoint::new(1, 1)]).is_err());
2097 }
2098
2099 #[test]
2100 fn polygon_constructor_negative_coordinates() {
2101 let vertices =
2102 vec![ShapePoint::new(-500, -500), ShapePoint::new(500, -500), ShapePoint::new(0, 500)];
2103 let ctrl = Control::polygon(vertices).unwrap();
2104 assert!(ctrl.is_polygon());
2105 match ctrl {
2106 Control::Polygon { width, height, .. } => {
2107 assert_eq!(width, HwpUnit::new(1000).unwrap());
2109 assert_eq!(height, HwpUnit::new(1000).unwrap());
2110 }
2111 _ => panic!("expected Polygon"),
2112 }
2113 }
2114
2115 #[test]
2116 fn polygon_constructor_degenerate_collinear() {
2117 let vertices =
2119 vec![ShapePoint::new(0, 0), ShapePoint::new(500, 0), ShapePoint::new(1000, 0)];
2120 let ctrl = Control::polygon(vertices).unwrap();
2121 assert!(ctrl.is_polygon());
2122 match ctrl {
2123 Control::Polygon { width, height, .. } => {
2124 assert_eq!(width, HwpUnit::new(1000).unwrap());
2125 assert_eq!(height, HwpUnit::new(0).unwrap());
2126 }
2127 _ => panic!("expected Polygon"),
2128 }
2129 }
2130
2131 #[test]
2132 fn line_constructor_horizontal() {
2133 let ctrl = Control::line(ShapePoint::new(0, 0), ShapePoint::new(5000, 0)).unwrap();
2134 assert!(ctrl.is_line());
2135 match ctrl {
2136 Control::Line {
2137 start,
2138 end,
2139 width,
2140 height,
2141 horz_offset,
2142 vert_offset,
2143 caption,
2144 style,
2145 } => {
2146 assert_eq!(start, ShapePoint::new(0, 0));
2147 assert_eq!(end, ShapePoint::new(5000, 0));
2148 assert_eq!(width, HwpUnit::new(5000).unwrap());
2149 assert_eq!(height, HwpUnit::new(100).unwrap()); assert_eq!(horz_offset, 0);
2151 assert_eq!(vert_offset, 0);
2152 assert!(caption.is_none());
2153 assert!(style.is_none());
2154 }
2155 _ => panic!("expected Line"),
2156 }
2157 }
2158
2159 #[test]
2160 fn line_constructor_vertical() {
2161 let ctrl = Control::line(ShapePoint::new(0, 0), ShapePoint::new(0, 3000)).unwrap();
2162 assert!(ctrl.is_line());
2163 match ctrl {
2164 Control::Line { width, height, .. } => {
2165 assert_eq!(width, HwpUnit::new(100).unwrap()); assert_eq!(height, HwpUnit::new(3000).unwrap());
2167 }
2168 _ => panic!("expected Line"),
2169 }
2170 }
2171
2172 #[test]
2173 fn line_constructor_diagonal_bounding_box() {
2174 let ctrl = Control::line(ShapePoint::new(100, 200), ShapePoint::new(400, 500)).unwrap();
2175 match ctrl {
2176 Control::Line { width, height, .. } => {
2177 assert_eq!(width, HwpUnit::new(300).unwrap());
2178 assert_eq!(height, HwpUnit::new(300).unwrap());
2179 }
2180 _ => panic!("expected Line"),
2181 }
2182 }
2183
2184 #[test]
2185 fn line_constructor_same_point_errors() {
2186 let pt = ShapePoint::new(100, 200);
2187 assert!(Control::line(pt, pt).is_err());
2188 }
2189
2190 #[test]
2191 fn horizontal_line_constructor() {
2192 let width = HwpUnit::from_mm(100.0).unwrap();
2193 let ctrl = Control::horizontal_line(width);
2194 assert!(ctrl.is_line());
2195 match ctrl {
2196 Control::Line {
2197 start,
2198 end,
2199 width: w,
2200 height,
2201 horz_offset,
2202 vert_offset,
2203 caption,
2204 style,
2205 } => {
2206 assert_eq!(start, ShapePoint::new(0, 0));
2207 assert_eq!(end.y, 0);
2208 assert_eq!(end.x, width.as_i32());
2209 assert_eq!(w, width);
2210 assert_eq!(height, HwpUnit::new(100).unwrap()); assert_eq!(horz_offset, 0);
2212 assert_eq!(vert_offset, 0);
2213 assert!(caption.is_none());
2214 assert!(style.is_none());
2215 }
2216 _ => panic!("expected Line"),
2217 }
2218 }
2219
2220 #[test]
2221 fn hyperlink_constructor() {
2222 let ctrl = Control::hyperlink("Visit Rust", "https://rust-lang.org");
2223 assert!(ctrl.is_hyperlink());
2224 match ctrl {
2225 Control::Hyperlink { text, url } => {
2226 assert_eq!(text, "Visit Rust");
2227 assert_eq!(url, "https://rust-lang.org");
2228 }
2229 _ => panic!("expected Hyperlink"),
2230 }
2231 }
2232
2233 #[test]
2234 fn footnote_with_id_sets_inst_id() {
2235 let para = Paragraph::new(ParaShapeIndex::new(0));
2236 let ctrl = Control::footnote_with_id(42, vec![para]);
2237 assert!(ctrl.is_footnote());
2238 match ctrl {
2239 Control::Footnote { inst_id, paragraphs } => {
2240 assert_eq!(inst_id, Some(42));
2241 assert_eq!(paragraphs.len(), 1);
2242 }
2243 _ => panic!("expected Footnote"),
2244 }
2245 }
2246
2247 #[test]
2248 fn endnote_with_id_sets_inst_id() {
2249 let para = Paragraph::new(ParaShapeIndex::new(0));
2250 let ctrl = Control::endnote_with_id(7, vec![para]);
2251 assert!(ctrl.is_endnote());
2252 match ctrl {
2253 Control::Endnote { inst_id, paragraphs } => {
2254 assert_eq!(inst_id, Some(7));
2255 assert_eq!(paragraphs.len(), 1);
2256 }
2257 _ => panic!("expected Endnote"),
2258 }
2259 }
2260
2261 #[test]
2262 fn footnote_with_id_differs_from_plain_footnote() {
2263 let ctrl_plain = Control::footnote(vec![]);
2264 let ctrl_id = Control::footnote_with_id(1, vec![]);
2265 match ctrl_plain {
2266 Control::Footnote { inst_id, .. } => assert_eq!(inst_id, None),
2267 _ => panic!("expected Footnote"),
2268 }
2269 match ctrl_id {
2270 Control::Footnote { inst_id, .. } => assert_eq!(inst_id, Some(1)),
2271 _ => panic!("expected Footnote"),
2272 }
2273 }
2274
2275 #[test]
2276 fn ellipse_with_text_has_correct_geometry_and_paragraphs() {
2277 use hwpforge_foundation::HwpUnit;
2278 let width = HwpUnit::from_mm(40.0).unwrap();
2279 let height = HwpUnit::from_mm(30.0).unwrap();
2280 let para = Paragraph::new(ParaShapeIndex::new(0));
2281 let ctrl = Control::ellipse_with_text(width, height, vec![para]);
2282 assert!(ctrl.is_ellipse());
2283 match ctrl {
2284 Control::Ellipse {
2285 center,
2286 axis1,
2287 axis2,
2288 width: w,
2289 height: h,
2290 horz_offset,
2291 vert_offset,
2292 paragraphs,
2293 caption,
2294 style,
2295 } => {
2296 let wv = w.as_i32();
2297 let hv = h.as_i32();
2298 assert_eq!(center, ShapePoint::new(wv / 2, hv / 2));
2299 assert_eq!(axis1, ShapePoint::new(wv, hv / 2));
2300 assert_eq!(axis2, ShapePoint::new(wv / 2, hv));
2301 assert_eq!(horz_offset, 0);
2302 assert_eq!(vert_offset, 0);
2303 assert_eq!(paragraphs.len(), 1);
2304 assert!(caption.is_none());
2305 assert!(style.is_none());
2306 }
2307 _ => panic!("expected Ellipse"),
2308 }
2309 }
2310
2311 #[test]
2312 fn serde_roundtrip_chart() {
2313 use crate::chart::{ChartData, ChartGrouping, ChartType, LegendPosition};
2314 let ctrl = Control::Chart {
2315 chart_type: ChartType::Column,
2316 data: ChartData::category(&["A", "B"], &[("S1", &[1.0, 2.0])]),
2317 title: Some("Test Chart".to_string()),
2318 legend: LegendPosition::Bottom,
2319 grouping: ChartGrouping::Stacked,
2320 width: HwpUnit::from_mm(100.0).unwrap(),
2321 height: HwpUnit::from_mm(80.0).unwrap(),
2322 stock_variant: None,
2323 bar_shape: None,
2324 scatter_style: None,
2325 radar_style: None,
2326 of_pie_type: None,
2327 explosion: None,
2328 wireframe: None,
2329 bubble_3d: None,
2330 show_markers: None,
2331 };
2332 let json = serde_json::to_string(&ctrl).unwrap();
2333 let back: Control = serde_json::from_str(&json).unwrap();
2334 assert_eq!(ctrl, back);
2335 }
2336
2337 #[test]
2338 fn serde_roundtrip_equation() {
2339 let ctrl = Control::Equation {
2340 script: "{a+b} over {c+d}".to_string(),
2341 width: HwpUnit::new(8779).unwrap(),
2342 height: HwpUnit::new(2600).unwrap(),
2343 base_line: 71,
2344 text_color: Color::BLACK,
2345 font: "HancomEQN".to_string(),
2346 };
2347 let json = serde_json::to_string(&ctrl).unwrap();
2348 let back: Control = serde_json::from_str(&json).unwrap();
2349 assert_eq!(ctrl, back);
2350 }
2351
2352 #[test]
2353 fn ellipse_with_text_empty_paragraphs_matches_ellipse() {
2354 use hwpforge_foundation::HwpUnit;
2355 let width = HwpUnit::from_mm(20.0).unwrap();
2356 let height = HwpUnit::from_mm(10.0).unwrap();
2357 let plain = Control::ellipse(width, height);
2358 let with_text = Control::ellipse_with_text(width, height, vec![]);
2359 assert_eq!(plain, with_text);
2361 }
2362
2363 #[test]
2366 fn dutmal_constructor_defaults() {
2367 let ctrl = Control::dutmal("본문", "주석");
2368 assert!(ctrl.is_dutmal());
2369 match ctrl {
2370 Control::Dutmal { main_text, sub_text, position, sz_ratio, align } => {
2371 assert_eq!(main_text, "본문");
2372 assert_eq!(sub_text, "주석");
2373 assert_eq!(position, DutmalPosition::Top);
2374 assert_eq!(sz_ratio, 0);
2375 assert_eq!(align, DutmalAlign::Center);
2376 }
2377 _ => panic!("expected Dutmal"),
2378 }
2379 }
2380
2381 #[test]
2382 fn dutmal_is_dutmal_true() {
2383 assert!(Control::dutmal("a", "b").is_dutmal());
2384 }
2385
2386 #[test]
2387 fn dutmal_is_compose_false() {
2388 assert!(!Control::dutmal("a", "b").is_compose());
2389 }
2390
2391 #[test]
2392 fn dutmal_display() {
2393 let ctrl = Control::dutmal("hello", "world");
2394 assert_eq!(ctrl.to_string(), r#"Dutmal("hello" / "world")"#);
2395 }
2396
2397 #[test]
2398 fn dutmal_serde_roundtrip() {
2399 let ctrl = Control::Dutmal {
2400 main_text: "테스트".to_string(),
2401 sub_text: "test".to_string(),
2402 position: DutmalPosition::Bottom,
2403 sz_ratio: 50,
2404 align: DutmalAlign::Right,
2405 };
2406 let json = serde_json::to_string(&ctrl).unwrap();
2407 let decoded: Control = serde_json::from_str(&json).unwrap();
2408 assert_eq!(ctrl, decoded);
2409 }
2410
2411 #[test]
2412 fn dutmal_position_default_is_top() {
2413 assert_eq!(DutmalPosition::default(), DutmalPosition::Top);
2414 }
2415
2416 #[test]
2417 fn dutmal_align_default_is_center() {
2418 assert_eq!(DutmalAlign::default(), DutmalAlign::Center);
2419 }
2420
2421 #[test]
2424 fn compose_constructor_defaults() {
2425 let ctrl = Control::compose("가");
2426 assert!(ctrl.is_compose());
2427 match ctrl {
2428 Control::Compose { compose_text, circle_type, char_sz, compose_type } => {
2429 assert_eq!(compose_text, "가");
2430 assert_eq!(circle_type, "SHAPE_REVERSAL_TIRANGLE");
2431 assert_eq!(char_sz, -3);
2432 assert_eq!(compose_type, "SPREAD");
2433 }
2434 _ => panic!("expected Compose"),
2435 }
2436 }
2437
2438 #[test]
2439 fn compose_is_compose_true() {
2440 assert!(Control::compose("나").is_compose());
2441 }
2442
2443 #[test]
2444 fn compose_is_dutmal_false() {
2445 assert!(!Control::compose("나").is_dutmal());
2446 }
2447
2448 #[test]
2449 fn compose_display() {
2450 let ctrl = Control::compose("가나");
2451 assert_eq!(ctrl.to_string(), r#"Compose("가나")"#);
2452 }
2453
2454 #[test]
2455 fn compose_serde_roundtrip() {
2456 let ctrl = Control::Compose {
2457 compose_text: "①".to_string(),
2458 circle_type: "SHAPE_REVERSAL_TIRANGLE".to_string(),
2459 char_sz: -3,
2460 compose_type: "SPREAD".to_string(),
2461 };
2462 let json = serde_json::to_string(&ctrl).unwrap();
2463 let decoded: Control = serde_json::from_str(&json).unwrap();
2464 assert_eq!(ctrl, decoded);
2465 }
2466
2467 #[test]
2468 fn compose_spec_typo_preserved() {
2469 let ctrl = Control::compose("X");
2471 match ctrl {
2472 Control::Compose { circle_type, .. } => {
2473 assert_eq!(circle_type, "SHAPE_REVERSAL_TIRANGLE");
2474 assert!(!circle_type.contains("TRIANGLE")); }
2476 _ => panic!("expected Compose"),
2477 }
2478 }
2479
2480 #[test]
2485 fn line_extreme_coords_no_panic() {
2486 let start = ShapePoint::new(i32::MIN, i32::MIN);
2488 let end = ShapePoint::new(i32::MAX, i32::MAX);
2489 let ctrl = Control::line(start, end).unwrap();
2490 assert!(ctrl.is_line());
2491 }
2492
2493 #[test]
2494 fn connect_line_extreme_coords_no_panic() {
2495 let start = ShapePoint::new(i32::MIN, 0);
2496 let end = ShapePoint::new(i32::MAX, 0);
2497 let ctrl = Control::connect_line(start, end).unwrap();
2498 assert!(ctrl.is_connect_line());
2499 }
2500
2501 #[test]
2502 fn polygon_extreme_coords_no_panic() {
2503 let vertices = vec![
2505 ShapePoint::new(i32::MIN, 0),
2506 ShapePoint::new(i32::MAX, 0),
2507 ShapePoint::new(0, i32::MAX),
2508 ];
2509 let _ = Control::polygon(vertices);
2511 }
2512
2513 #[test]
2514 fn curve_extreme_coords_no_panic() {
2515 let points = vec![ShapePoint::new(i32::MIN, i32::MIN), ShapePoint::new(i32::MAX, i32::MAX)];
2516 let _ = Control::curve(points);
2517 }
2518}