Shapes: a right triangle keeps its right angle (#1599)

DrawingML's rtTriangle was read as an isosceles triangle, so a cover
page's white corner cut-outs pointed up into a peak instead of cutting a
V into the picture above them. It is now its own shape kind, drawn with
the right angle at the bottom left, wrapped and glued along that outline,
and saved back as rtTriangle. The .doc reader maps msosptRightTriangle (6)
to it too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Fin Warman
2026-10-14 19:51:13 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 952eb57688
commit 90d8844e44
10 changed files with 30 additions and 3 deletions
+2
View File
@@ -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)],
}
+2
View File
@@ -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,
+2
View File
@@ -88,6 +88,7 @@ pub fn shape_outline(kind: ShapeKind, w: f32, h: f32, corner: Option<u32>) -> 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,
+2 -1
View File
@@ -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,
+2 -1
View File
@@ -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,
+1
View File
@@ -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",
+15
View File
@@ -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#"<a:prstGeom prst="rtTriangle"><a:avLst/></a:prstGeom>"#, "", "<wps:cNvSpPr/>");
let d = read_body(&format!("<w:p>{body}</w:p>"));
let kind = |d: &Document| match &paras(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).
+1
View File
@@ -558,6 +558,7 @@ fn shape_path(kind: ShapeKind, r: &Rect, corner: Option<u32>) -> Option<Path> {
}
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 => {
+1
View File
@@ -1039,6 +1039,7 @@ pub fn shape_path(kind: ShapeKind, r: kurbo::Rect, corner: Option<u32>) -> 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 => {
+2 -1
View File
@@ -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) |