mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-15 14:52:26 +00:00
WordArt: keep ordinary shapes with textNoShape; print the text outline
textNoShape is the standard no-warp value, so it alone no longer makes a shape WordArt: only a rectangle that is a text box (as the writer saves WordArt) or has no fill and no outline reads as WordArt. A filled ellipse with an explicit textNoShape now reads and saves as the same ellipse. The PDF/print path drew WordArt text without its outline, so Mint Outline (white text, green outline) printed as a blank page. The outline is now drawn like on screen: the text in the outline colour, offset in eight directions, under the filled text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
032b280b04
commit
14aae7cee1
4 files changed
+70
-6
No files matched your search
@@ -208,6 +208,16 @@ fn draw_shape(pg: &mut Page, wb: &Workbook, shp: &gridcraft_model::Shape, b: &Bl
|
||||
let off = (size * 0.06).max(0.5);
|
||||
pg.text(lx + off, yy + off, size, font, sc, l);
|
||||
}
|
||||
// WordArt's text outline, as on screen: the line in the outline colour, offset in eight
|
||||
// directions by half the outline width, under the filled text.
|
||||
if let Some((c, ow)) = fm.text_outline
|
||||
&& let Some(oc) = rgb_of(c, wb)
|
||||
{
|
||||
let d = (ow * b.scale / 2.0).clamp(0.25, 20.0);
|
||||
for (dx, dy) in [(-1.0, -1.0), (0.0, -1.0), (1.0, -1.0), (-1.0, 0.0), (1.0, 0.0), (-1.0, 1.0), (0.0, 1.0), (1.0, 1.0)] {
|
||||
pg.text(lx + dx * d, yy + dy * d, size, font, oc, l);
|
||||
}
|
||||
}
|
||||
pg.text(lx, yy, size, font, color, l);
|
||||
yy += size * 1.2;
|
||||
}
|
||||
|
||||
@@ -587,6 +587,25 @@ fn word_art_survives_xlsx_roundtrip() {
|
||||
assert!(after.format.no_line);
|
||||
}
|
||||
|
||||
/// White text with a green outline (Mint Outline) prints its outline, so it is not invisible on a
|
||||
/// white page.
|
||||
#[test]
|
||||
fn word_art_outline_prints() {
|
||||
let mut s = s();
|
||||
s.execute("insert.wordArt", json!({"style": "Mint Outline", "text": "VISIBLE", "at": "A1"})).unwrap();
|
||||
let pdf = s.execute("file.exportPdf", json!({"range": "A1:G10"})).unwrap();
|
||||
let data = crate::io::base64_decode(pdf["base64"].as_str().unwrap()).unwrap();
|
||||
let text = String::from_utf8_lossy(&data);
|
||||
let outline = text.matches("0.18 0.608 0.416 rg").count();
|
||||
assert!(outline >= 1, "the outline colour (2E9B6A) is drawn");
|
||||
assert_eq!(text.matches("(VISIBLE) Tj").count(), 9, "eight outline copies under the white text");
|
||||
assert!(text.contains("1 1 1 rg"), "the white fill is drawn on top");
|
||||
// The last copy drawn is the filled text, in white; every outline copy comes before it.
|
||||
let before_fill = &text[..text.rfind("(VISIBLE) Tj").unwrap()];
|
||||
assert!(before_fill.rfind("1 1 1 rg") > before_fill.rfind("0.18 0.608 0.416 rg"), "the white fill is drawn last");
|
||||
assert_eq!(before_fill.matches("(VISIBLE) Tj").count(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_border_clears_only_shared_edges_and_undo_restores_them() {
|
||||
use gridcraft_core::RangeRef;
|
||||
|
||||
@@ -221,9 +221,14 @@ fn read_shape(obj: &El, anchor: Anchor) -> Option<Shape> {
|
||||
let tx_box = obj.path(&["nvSpPr", "cNvSpPr"]).is_some_and(|c| c.flag("txBox", false));
|
||||
let ln = sp.child("ln");
|
||||
let arrow = ln.is_some_and(|l| l.child("tailEnd").or_else(|| l.child("headEnd")).is_some_and(|t| t.attr("type").is_some_and(|v| v != "none")));
|
||||
// WordArt: our marker, a text body with the non-warping text warp `textNoShape`. Any other warp
|
||||
// (Excel puts one on ordinary filled shapes too) leaves the shape what its geometry says.
|
||||
let word_art = obj.path(&["txBody", "bodyPr", "prstTxWarp"]).is_some_and(|w| w.attr("prst") == Some("textNoShape"));
|
||||
// WordArt: our marker, a text body with the non-warping text warp `textNoShape`, on a rectangle
|
||||
// that is a text box (as the writer saves it) or has no fill and no outline of its own (as
|
||||
// WordArt usually comes). `textNoShape` is the standard "no warp" value, so on an ordinary
|
||||
// shape (an ellipse, a filled rectangle) it, like any other warp, leaves the shape what its
|
||||
// geometry says.
|
||||
let word_art = prst == "rect"
|
||||
&& (tx_box || (sp.child("noFill").is_some() && !ln.is_some_and(|l| l.child("solidFill").is_some())))
|
||||
&& obj.path(&["txBody", "bodyPr", "prstTxWarp"]).is_some_and(|w| w.attr("prst") == Some("textNoShape"));
|
||||
let kind = match prst {
|
||||
_ if word_art => ShapeKind::WordArt,
|
||||
_ if tx_box => ShapeKind::TextBox,
|
||||
|
||||
@@ -117,7 +117,37 @@ fn a_text_warp_on_a_filled_ellipse_does_not_make_it_word_art() {
|
||||
.replace(r#"anchor="t"/>"#, r#"anchor="t"><a:prstTxWarp prst="textTriangle"><a:avLst/></a:prstTxWarp></a:bodyPr>"#);
|
||||
let wb = read_drawing(&drawing);
|
||||
assert_eq!(wb.sheet(0).unwrap().shapes[0].kind, ShapeKind::Ellipse);
|
||||
// The marker the writer uses still reads back as WordArt.
|
||||
let marked = drawing.replace("textTriangle", "textNoShape");
|
||||
assert_eq!(read_drawing(&marked).sheet(0).unwrap().shapes[0].kind, ShapeKind::WordArt);
|
||||
}
|
||||
|
||||
/// `textNoShape` is the standard "no text warp" value, so on its own it does not make an ordinary
|
||||
/// shape WordArt: a filled ellipse that carries it reads and saves as the same filled ellipse.
|
||||
#[test]
|
||||
fn an_explicit_no_warp_on_a_filled_ellipse_stays_an_ellipse() {
|
||||
let drawing = ARABIC_TEXT_BOX
|
||||
.replace(r#"<xdr:cNvSpPr txBox="1"/>"#, "<xdr:cNvSpPr/>")
|
||||
.replace(
|
||||
r#"<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>"#,
|
||||
r#"<a:prstGeom prst="ellipse"><a:avLst/></a:prstGeom><a:solidFill><a:srgbClr val="FF0000"/></a:solidFill>"#,
|
||||
)
|
||||
.replace(r#"anchor="t"/>"#, r#"anchor="t"><a:prstTxWarp prst="textNoShape"><a:avLst/></a:prstTxWarp></a:bodyPr>"#);
|
||||
let wb = read_drawing(&drawing);
|
||||
let shape = &wb.sheet(0).unwrap().shapes[0];
|
||||
assert_eq!(shape.kind, ShapeKind::Ellipse);
|
||||
assert_eq!(shape.fill, Color::Rgb(0xFF0000));
|
||||
let xml = written_drawing(&wb);
|
||||
assert!(xml.contains(r#"<a:prstGeom prst="ellipse">"#), "{xml}");
|
||||
assert!(xml.contains(r#"<a:solidFill><a:srgbClr val="FF0000"/></a:solidFill>"#), "{xml}");
|
||||
assert!(!xml.contains(r#"txBox="1""#), "{xml}");
|
||||
let (reopened, _) = read_xlsx(&write_xlsx(&wb).unwrap()).unwrap();
|
||||
assert_eq!(reopened.sheet(0).unwrap().shapes[0].kind, ShapeKind::Ellipse);
|
||||
// The same warp on a filled rectangle that is not a text box keeps it a rectangle too.
|
||||
let rect = drawing.replace(r#"prst="ellipse""#, r#"prst="rect""#);
|
||||
assert_eq!(read_drawing(&rect).sheet(0).unwrap().shapes[0].kind, ShapeKind::Rectangle);
|
||||
// WordArt as written (a text box) or as a fill-less, line-less rectangle still reads as WordArt.
|
||||
let text_box = ARABIC_TEXT_BOX.replace(r#"anchor="t"/>"#, r#"anchor="t"><a:prstTxWarp prst="textNoShape"><a:avLst/></a:prstTxWarp></a:bodyPr>"#);
|
||||
assert_eq!(read_drawing(&text_box).sheet(0).unwrap().shapes[0].kind, ShapeKind::WordArt);
|
||||
let bare = text_box
|
||||
.replace(r#"<xdr:cNvSpPr txBox="1"/>"#, "<xdr:cNvSpPr/>")
|
||||
.replace(r#"<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>"#, r#"<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/>"#);
|
||||
assert_eq!(read_drawing(&bare).sheet(0).unwrap().shapes[0].kind, ShapeKind::WordArt);
|
||||
}
|
||||
Reference in new issue
Block a user