diff --git a/crates/doc/src/connector.rs b/crates/doc/src/connector.rs index 2cfa5b277..e3ce8368f 100644 --- a/crates/doc/src/connector.rs +++ b/crates/doc/src/connector.rs @@ -100,6 +100,8 @@ pub fn sites(kind: ShapeKind, w: f32, h: f32) -> Vec<(f32, f32)> { vec![(cx, 0.0), (cx - dx, cy - dy), (0.0, cy), (cx - dx, cy + dy), (cx, h), (cx + dx, cy + dy), (w, cy), (cx + dx, cy - dy)] } ShapeKind::Triangle => vec![(cx, 0.0), (cx / 2.0, cy), (0.0, h), (cx, h), (w, h), (cx + cx / 2.0, cy)], + // The corners, the legs' midpoints and the hypotenuse's. + ShapeKind::RightTriangle => vec![(0.0, 0.0), (0.0, cy), (0.0, h), (cx, h), (w, h), (cx, cy)], ShapeKind::Line | ShapeKind::StraightConnector | ShapeKind::ElbowConnector | ShapeKind::CurvedConnector => vec![(0.0, 0.0), (w, h)], _ => vec![(cx, 0.0), (0.0, cy), (cx, h), (w, cy)], } diff --git a/crates/doc/src/para.rs b/crates/doc/src/para.rs index f1379bd35..aa6d84d32 100644 --- a/crates/doc/src/para.rs +++ b/crates/doc/src/para.rs @@ -289,6 +289,8 @@ pub enum ShapeKind { RoundedRectangle, Ellipse, Triangle, + /// A right triangle (DrawingML's `rtTriangle`): the right angle at the bottom left. + RightTriangle, Diamond, Line, Arrow, diff --git a/crates/doc/src/wrap.rs b/crates/doc/src/wrap.rs index 598286496..75fe8f7fb 100644 --- a/crates/doc/src/wrap.rs +++ b/crates/doc/src/wrap.rs @@ -88,6 +88,7 @@ pub fn shape_outline(kind: ShapeKind, w: f32, h: f32, corner: Option) -> Op pts } ShapeKind::Triangle => vec![(0.5, 0.0), (1.0, 1.0), (0.0, 1.0)], + ShapeKind::RightTriangle => vec![(0.0, 0.0), (1.0, 1.0), (0.0, 1.0)], ShapeKind::Diamond => vec![(0.5, 0.0), (1.0, 0.5), (0.5, 1.0), (0.0, 0.5)], ShapeKind::Arrow => { // As drawn: the head is half the height long, the shaft 40% of the height thick. @@ -217,6 +218,7 @@ mod tests { ShapeKind::Ellipse, ShapeKind::RoundedRectangle, ShapeKind::Triangle, + ShapeKind::RightTriangle, ShapeKind::Diamond, ShapeKind::Arrow, ShapeKind::Star, diff --git a/crates/docbin/src/drawing.rs b/crates/docbin/src/drawing.rs index c7b913be8..f2ecdb9a4 100644 --- a/crates/docbin/src/drawing.rs +++ b/crates/docbin/src/drawing.rs @@ -186,7 +186,8 @@ fn leaf(sp: &Sp, w: f32, h: f32, float: Float, b: &Build, pics: &mut Pictures) - 2 => ShapeKind::RoundedRectangle, 3 => ShapeKind::Ellipse, 4 => ShapeKind::Diamond, - 5 | 6 => ShapeKind::Triangle, + 5 => ShapeKind::Triangle, + 6 => ShapeKind::RightTriangle, 12 | 187 => ShapeKind::Star, 13 | 66 | 67 | 68 | 69 => ShapeKind::Arrow, 20 | 32 => ShapeKind::Line, diff --git a/crates/docx/src/read/shape_style.rs b/crates/docx/src/read/shape_style.rs index 09f47e93f..bb68c23fe 100644 --- a/crates/docx/src/read/shape_style.rs +++ b/crates/docx/src/read/shape_style.rs @@ -29,7 +29,8 @@ pub(crate) fn preset_kind(prst: &str) -> ShapeKind { "ellipse" | "wedgeEllipseCallout" | "cloudCallout" | "cloud" | "flowChartConnector" | "donut" | "flowChartOnlineStorage" => { ShapeKind::Ellipse } - "triangle" | "rtTriangle" | "flowChartExtract" | "flowChartMerge" => ShapeKind::Triangle, + "triangle" | "flowChartExtract" | "flowChartMerge" => ShapeKind::Triangle, + "rtTriangle" => ShapeKind::RightTriangle, "diamond" | "flowChartDecision" => ShapeKind::Diamond, "line" | "lineInv" => ShapeKind::Line, "straightConnector1" => ShapeKind::StraightConnector, diff --git a/crates/docx/src/write/story.rs b/crates/docx/src/write/story.rs index bd3dbfd3d..b9015edbf 100644 --- a/crates/docx/src/write/story.rs +++ b/crates/docx/src/write/story.rs @@ -986,6 +986,7 @@ impl Writer<'_> { ShapeKind::RoundedRectangle => "roundRect", ShapeKind::Ellipse => "ellipse", ShapeKind::Triangle => "triangle", + ShapeKind::RightTriangle => "rtTriangle", ShapeKind::Diamond => "diamond", ShapeKind::Line => "line", ShapeKind::StraightConnector => "straightConnector1", diff --git a/crates/docx/tests/fixtures.rs b/crates/docx/tests/fixtures.rs index b762041a7..2cbb81360 100644 --- a/crates/docx/tests/fixtures.rs +++ b/crates/docx/tests/fixtures.rs @@ -2567,6 +2567,21 @@ fn text_frames_read_and_round_trip() { assert!(q.iter().any(|x| x.props.drop_cap == Some(2) && x.props.frame.is_none())); } +/// A right triangle (`rtTriangle`) has its right angle at the bottom left: a cover page's white +/// corner cut-outs drawn as isosceles triangles pointed the wrong way. +#[test] +fn a_right_triangle_keeps_its_right_angle() { + let body = anchored_wsp("Corner", 1270000, 635000, r#""#, "", ""); + let d = read_body(&format!("{body}")); + let kind = |d: &Document| match ¶s(d)[0].objects[0] { + InlineObject::Shape { kind, .. } => *kind, + o => panic!("{o:?}"), + }; + assert_eq!(kind(&d), ShapeKind::RightTriangle); + let saved = wordcraft_docx::write(&d).unwrap(); + assert_eq!(kind(&wordcraft_docx::read(&saved).unwrap()), ShapeKind::RightTriangle); +} + #[test] fn hyperlink_with_empty_target_is_no_link() { // LibreOffice writes `Target=""` for the Hyperlink character style without an address (#1005). diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index e73ccb8e5..7f8ec1302 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -558,6 +558,7 @@ fn shape_path(kind: ShapeKind, r: &Rect, corner: Option) -> Option { } ShapeKind::Ellipse => ellipse(&mut pb, cx, cy, r.w / 2.0, r.h / 2.0), ShapeKind::Triangle => poly(&mut pb, &[(cx, y0), (x1, y1), (x0, y1)]), + ShapeKind::RightTriangle => poly(&mut pb, &[(x0, y0), (x1, y1), (x0, y1)]), ShapeKind::Diamond => poly(&mut pb, &[(cx, y0), (x1, cy), (cx, y1), (x0, cy)]), // DrawingML's `line`: top-left to bottom-right (flips give the other directions). ShapeKind::Line => { diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 972c3c638..ac79f73ae 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1039,6 +1039,7 @@ pub fn shape_path(kind: ShapeKind, r: kurbo::Rect, corner: Option) -> BezPa } ShapeKind::Ellipse => kurbo::Ellipse::from_rect(r).to_path(0.1), ShapeKind::Triangle => poly(&[(cx, r.y0), (r.x1, r.y1), (r.x0, r.y1)]), + ShapeKind::RightTriangle => poly(&[(r.x0, r.y0), (r.x1, r.y1), (r.x0, r.y1)]), ShapeKind::Diamond => poly(&[(cx, r.y0), (r.x1, cy), (cx, r.y1), (r.x0, cy)]), // DrawingML's `line`: top-left to bottom-right (flips give the other directions). ShapeKind::Line => { diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 7c227b811..143502d09 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity with Microsoft Word -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-14 · **Change:** trivial (legacy DOC DIB conversion honours the BLIP byte limit; related #1334 and #1554; previously trivial: section `w:vAlign` "both" round-trips as Justified, #1320; previously trivial: Word's text watermark written back to DOCX headers, #1290; previously trivial: VML preset shapes drawn, #1377; previously trivial: VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: equations as MathML in HTML export, copy and paste, #816; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-14 · **Change:** trivial (legacy DOC DIB conversion honours the BLIP byte limit; related #1334 and #1554; previously trivial: right triangles keep their right angle; previously trivial: section `w:vAlign` "both" round-trips as Justified, #1320; previously trivial: Word's text watermark written back to DOCX headers, #1290; previously trivial: VML preset shapes drawn, #1377; previously trivial: VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: equations as MathML in HTML export, copy and paste, #816; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats Every format Word opens or saves, what WordCraft does with it, and how it's tested. Fidelity is the share of a typical real-world file's content and formatting that survives (read: shows correctly; @@ -196,6 +196,7 @@ attributes inside stories and style or list definitions; a regenerated theme. Te | Date | Change | Summary | |---|---|---| | 2026-10-14 | minor | Legacy DOC DIB helper honours the supplied byte limit for its BMP header/copy, decoder and PNG output, with a bounded BMP fallback. Generated 1-pixel regression only; resource-exhaustion consequences unmeasured, fidelity/readiness estimates unchanged. Related: #1334 and #1554 | +| 2026-10-14 | trivial | Right triangles (`rtTriangle`) keep their right angle at the bottom left instead of becoming isosceles triangles, and save back as `rtTriangle` | | 2026-10-13 | trivial | Section `w:vAlign="both"` reads as Justified and writes back as `both` (#1320) | | 2026-10-13 | trivial | DOCX: the document's watermark is written as a VML text shape in every header, adding a header where pages had none (#1290) | | 2026-10-13 | trivial | DOCX: Word's VML text watermark in headers is read as the document's watermark (#1290) |