Gap 19: ODT rotated and mirrored shapes and pictures

Shapes and pictures turned by `draw:transform` (rotate, translate and
scale, applied in turn to the object's own coordinates) open turned: the
file's anticlockwise turn about the object's top left corner, followed by
a move, becomes WordCraft's clockwise turn about the centre of the
unrotated box at its offset, as Word places them. They are written back
the same way, in line as well as floating. LibreOffice writes no
`svg:x`/`svg:y` for a turned shape, so before, a turned shape lost both
its turn and its position.

Custom shapes keep their mirroring (`draw:mirror-horizontal`/`-vertical`
on their geometry), and a turned line is written by its turned ends. The
flow keeps the placement of an in-line object that is turned or mirrored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
This commit is contained in:
Claude
2026-11-01 06:32:22 +00:00
parent 836bf6553b
commit f9d13f06dd
7 changed files with 269 additions and 19 deletions
+8 -2
View File
@@ -291,7 +291,7 @@ fn rich_inlines(doc: &Document, o: &InlineObject, f: &Fmt, opts: FlowOpts) -> Op
let inner = FlowOpts { notes: false, comments: false, boxes: false, ..opts };
let part = story.and_then(|id| doc.parts.get(&id));
let blocks = part.map(|p| flow_blocks(doc, &p.blocks, 0, inner)).unwrap_or_default();
let float = (float.wrap != wordcraft_doc::para::Wrap::Inline).then(|| Box::new(float.clone()));
let float = kept_float(float);
let look = BoxLook {
fill: fill.as_ref().filter(|_| kind != ShapeKind::Line).map(wordcraft_doc::fill::Fill::main_color),
line: stroke.filter(|_| stroke_width.is_finite() && *stroke_width > 0.0).map(|c| (c, stroke_width.min(12.0))),
@@ -309,6 +309,12 @@ fn rich_inlines(doc: &Document, o: &InlineObject, f: &Fmt, opts: FlowOpts) -> Op
/// Comment `c` (a reply to `parent`) of `doc` as a flow comment: its text holds no comments or
/// notes of its own.
/// The placement a flow keeps for a drawing: when it floats, or turns or mirrors it in line.
fn kept_float(f: &wordcraft_doc::para::Float) -> Option<Box<wordcraft_doc::para::Float>> {
let turned = f.rot.is_finite() && f.rot.rem_euclid(360.0) > 0.001 && f.rot.rem_euclid(360.0) < 359.999;
(f.wrap != wordcraft_doc::para::Wrap::Inline || turned || f.flip_h || f.flip_v).then(|| Box::new(f.clone()))
}
fn flow_comment(doc: &Document, c: &wordcraft_doc::Comment, parent: Option<u32>, opts: FlowOpts) -> FComment {
let story = doc.parts.get(&c.part).map(|p| &p.blocks);
let blocks = story.map(|b| flow_blocks(doc, b, 0, FlowOpts { notes: false, comments: false, ..opts })).unwrap_or_default();
@@ -440,7 +446,7 @@ fn flow_paras_in(doc: &Document, p: &Paragraph, opts: FlowOpts) -> Vec<Para> {
w: *w,
h: *h,
alt: alt.to_string(),
float: (opts.paras && float.wrap != wordcraft_doc::para::Wrap::Inline).then(|| Box::new(float.clone())),
float: kept_float(float).filter(|_| opts.paras),
})),
None if !alt.is_empty() => buf.push_str(alt),
None => {}
+26 -4
View File
@@ -289,6 +289,8 @@ pub(super) struct Body<'a> {
frame_look: model::BoxLook,
/// The kind of the drawing shape being read (its text is read as a text box's).
shape_kind: Option<wordcraft_doc::para::ShapeKind>,
/// A custom shape's mirroring (left to right, top to bottom), from its geometry.
shape_flip: (bool, bool),
/// The depth of the `draw:g` group being read: its shapes are placed by it, not anchored.
group_at: Option<usize>,
/// The note or comment being read.
@@ -405,6 +407,7 @@ impl<'a> Body<'a> {
frame_box: None,
frame_look: model::BoxLook::default(),
shape_kind: None,
shape_flip: (false, false),
group_at: None,
note: None,
comment_names: HashMap::new(),
@@ -801,6 +804,9 @@ impl Body<'_> {
if let Some(t) = get(a, "draw:type") {
self.shape_kind = Some(super::shapes::kind_of(t));
}
// Mirrored (§20.112 `draw:mirror-horizontal`, §20.113 `draw:mirror-vertical`).
let on = |k: &str| get(a, k).is_some_and(|v| v.trim() == "true");
self.shape_flip = (on("draw:mirror-horizontal"), on("draw:mirror-vertical"));
self.skip = 1;
return;
}
@@ -917,7 +923,7 @@ impl Body<'_> {
let h = get(a, "svg:height").and_then(length);
self.frame = Some((w, h, None, String::new(), None));
let g = get(a, "draw:style-name").map(|s| self.styles.graphic(s)).unwrap_or_default();
self.frame_float = Some(super::frames::float_of(get(a, "text:anchor-type"), get(a, "svg:x"), get(a, "svg:y"), &g));
self.frame_float = Some(super::frames::place(a, w.unwrap_or(0.0), h.unwrap_or(0.0), &g));
// Without a height, a text box in it grows to its text (`fo:min-height` below).
self.frame_look = g.look(h.is_none());
}
@@ -1415,7 +1421,17 @@ impl Body<'_> {
fn end_frame(&mut self) {
if let Some((w, h, img, alt, eq)) = self.frame.take() {
let boxed = self.frame_box.take();
let float = self.frame_float.take().filter(|f| f.wrap != wordcraft_doc::para::Wrap::Inline);
let mut float = self.frame_float.take();
if boxed.is_some()
&& self.shape_kind.is_some()
&& let Some(f) = &mut float
{
let (h, v) = std::mem::take(&mut self.shape_flip);
f.flip_h |= h;
f.flip_v ^= v;
}
// Kept when it floats, or turns or mirrors the object in line.
let float = float.filter(|f| f.wrap != wordcraft_doc::para::Wrap::Inline || super::frames::turned(f) || f.flip_h || f.flip_v);
let inline = match (boxed, eq) {
(Some(blocks), _) => {
let look = std::mem::take(&mut self.frame_look);
@@ -1447,9 +1463,15 @@ impl Body<'_> {
let kind = super::shapes::element_kind(local(q));
let g = self.styles.shape_graphic(get(a, "draw:style-name"));
let (w, h, x, y, rising) = super::shapes::geometry(kind, a);
let at = |v: Option<f32>| v.map(super::writer::pt);
let mut float = super::frames::float_of(get(a, "text:anchor-type"), at(x).as_deref(), at(y).as_deref(), &g);
let mut float = if kind == wordcraft_doc::para::ShapeKind::Line {
// A line is placed by its ends.
let at = |v: Option<f32>| v.map(super::writer::pt);
super::frames::float_of(get(a, "text:anchor-type"), at(x).as_deref(), at(y).as_deref(), &g)
} else {
super::frames::place(a, w, h, &g)
};
float.flip_v = rising;
self.shape_flip = (false, false);
self.frame_look = g.shape_look(kind);
self.shape_kind = Some(kind);
let frame = (Some((Some(w), Some(h), None, String::new(), None)), Some(float), std::mem::take(&mut self.frame_look));
+103 -2
View File
@@ -217,9 +217,105 @@ pub(super) fn float_of(anchor: Option<&str>, x: Option<&str>, y: Option<&str>, g
}
}
/// Where a frame or shape `w` × `h` (points) sits: anchored as its `text:anchor-type` says, at
/// its `svg:x`/`svg:y`, or turned and moved by its `draw:transform`, in graphic style `g`.
pub(super) fn place(a: &Attrs, w: f32, h: f32, g: &Graphic) -> Float {
let turned = get(a, "draw:transform").and_then(|t| transform(t, w, h));
let (x, y) = match turned {
Some((x, y, _)) => (Some(pt(x)), Some(pt(y))),
None => (get(a, "svg:x").map(String::from), get(a, "svg:y").map(String::from)),
};
let mut f = float_of(get(a, "text:anchor-type"), x.as_deref(), y.as_deref(), g);
if let Some((.., rot)) = turned {
f.rot = rot;
}
f
}
/// Most operations of a `draw:transform` read.
const MAX_TRANSFORMS: usize = 16;
/// A `draw:transform` (ODF 1.3 §19.228) of an object `w` × `h`, as WordCraft places it: the
/// offset of its unrotated box and its clockwise turn about the box's centre, in degrees
/// (0–360). The file's operations apply in turn to the object's own coordinates, whose origin
/// is its top left corner: `rotate` turns it anticlockwise (radians), `translate` moves it and
/// `scale` scales it; `skewX`, `skewY` and `matrix` are left out. `None` when none reads.
pub(super) fn transform(v: &str, w: f32, h: f32) -> Option<(f32, f32, f32)> {
// x' = m0·x + m2·y + m4, y' = m1·x + m3·y + m5.
let mut m = [1.0f64, 0.0, 0.0, 1.0, 0.0, 0.0];
let mut angle = 0.0f64;
let mut any = false;
let mut rest = v;
for _ in 0..MAX_TRANSFORMS {
let (Some(open), Some(close)) = (rest.find('('), rest.find(')')) else { break };
let (Some(name), Some(args)) = (rest.get(..open), rest.get(open + 1..close)) else { break };
let args: Vec<&str> = args.split(|c: char| c.is_whitespace() || c == ',').filter(|s| !s.is_empty()).collect();
rest = rest.get(close + 1..).unwrap_or("");
let num = |i: usize| args.get(i).and_then(|a| a.parse::<f64>().ok()).filter(|v| v.is_finite());
let len = |i: usize| args.get(i).and_then(|a| length(a)).filter(|v| v.is_finite()).map(|v| f64::from(v.clamp(-MAX_LEN, MAX_LEN)));
match name.trim() {
"rotate" => {
let Some(a) = num(0) else { continue };
let a = a % std::f64::consts::TAU;
let (sin, cos) = a.sin_cos();
m = [
cos * m[0] + sin * m[1],
-sin * m[0] + cos * m[1],
cos * m[2] + sin * m[3],
-sin * m[2] + cos * m[3],
cos * m[4] + sin * m[5],
-sin * m[4] + cos * m[5],
];
angle += a;
}
"translate" => {
let Some(tx) = len(0) else { continue };
m[4] += tx;
m[5] += len(1).unwrap_or(0.0);
}
"scale" => {
let Some(sx) = num(0).map(|v| v.clamp(-100.0, 100.0)) else { continue };
let sy = num(1).map_or(sx, |v| v.clamp(-100.0, 100.0));
m = [m[0] * sx, m[1] * sy, m[2] * sx, m[3] * sy, m[4] * sx, m[5] * sy];
}
_ => continue,
}
any = true;
}
if !any {
return None;
}
let (hw, hh) = (f64::from(w) / 2.0, f64::from(h) / 2.0);
let centre = (m[0] * hw + m[2] * hh + m[4], m[1] * hw + m[3] * hh + m[5]);
let fit = |v: f64| if v.is_finite() { v.clamp(-f64::from(MAX_LEN), f64::from(MAX_LEN)) as f32 } else { 0.0 };
let rot = (-angle.to_degrees()).rem_euclid(360.0);
// Rounding can leave a whole turn.
let rot = if rot.is_finite() && rot < 359.9999 { rot as f32 } else { 0.0 };
Some((fit(centre.0 - hw), fit(centre.1 - hh), rot))
}
/// The `draw:transform` of an object `w` × `h` whose unrotated box is at `x`, `y` and turned `rot`
/// degrees clockwise about its centre (the reverse of [`transform`]).
pub(super) fn transform_attr(x: f32, y: f32, w: f32, h: f32, rot: f32) -> String {
let fin = |v: f32| if v.is_finite() { f64::from(v.clamp(-MAX_LEN, MAX_LEN)) } else { 0.0 };
let a = -fin(rot).to_radians();
let (sin, cos) = a.sin_cos();
let (hw, hh) = (fin(w) / 2.0, fin(h) / 2.0);
// The corner that ends up where the centre stays put.
let tx = fin(x) + hw - (cos * hw + sin * hh);
let ty = fin(y) + hh - (-sin * hw + cos * hh);
format!(" draw:transform=\"rotate ({a}) translate ({} {})\"", pt(tx as f32), pt(ty as f32))
}
/// Whether `f` turns its object.
pub(super) fn turned(f: &Float) -> bool {
f.rot.is_finite() && f.rot.rem_euclid(360.0) > 0.001 && f.rot.rem_euclid(360.0) < 359.999
}
/// How a floating picture is written: its `text:anchor-type`, its graphic style's
/// `style:graphic-properties` attributes, and the frame's `svg:x`/`svg:y` attributes.
pub(super) fn frame_of(f: &Float) -> (&'static str, String, String) {
/// `style:graphic-properties` attributes, and the frame's `svg:x`/`svg:y` attributes, or its
/// `draw:transform` when it is turned (`w` × `h` its size).
pub(super) fn frame_of(f: &Float, w: f32, h: f32) -> (&'static str, String, String) {
let anchor = if f.h_rel == Anchor::Page && f.v_rel == Anchor::Page {
"page"
} else if f.v_rel == Anchor::Line {
@@ -275,6 +371,11 @@ pub(super) fn frame_of(f: &Float) -> (&'static str, String, String) {
);
let mut xy = String::new();
let p = |v: f32| pt(if v.is_finite() { v.clamp(-MAX_LEN, MAX_LEN) } else { 0.0 });
if turned(f) {
let x = if f.h_align.is_none() { f.x } else { 0.0 };
let y = if f.v_align.is_none() { f.y } else { 0.0 };
return (anchor, props, transform_attr(x, y, w, h, f.rot));
}
if f.h_align.is_none() {
xy.push_str(&format!(" svg:x=\"{}\"", p(f.x)));
}
+1
View File
@@ -13,6 +13,7 @@ mod named_styles;
mod notes;
mod outline;
mod para_borders;
mod rotation;
mod rtl;
mod settings;
mod shapes;
@@ -0,0 +1,99 @@
//! Rotated and mirrored shapes and pictures (ODF 1.3 §19.228 `draw:transform`, §20.112
//! `draw:mirror-horizontal`/`-vertical` of a custom shape's geometry): a turn in the file
//! (anticlockwise, about the shape's top left corner, then moved) opens as WordCraft's
//! (clockwise about the centre of the unrotated box at its offset), and is written back.
use crate::odt::tests::{NS_T, content_of, package, zip_of};
use crate::odt::{export, import};
use wordcraft_doc::Document;
use wordcraft_doc::para::{Float, InlineObject, Wrap};
const NS_D: &str = r#"xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:xlink="http://www.w3.org/1999/xlink""#;
/// Each object's (offset, rotation, flips, floats), rounded.
fn placed(d: &Document) -> Vec<((f32, f32), f32, (bool, bool), bool)> {
let r = |v: f32| (v * 10.0).round() / 10.0;
d.body
.iter()
.filter_map(|b| b.as_para())
.flat_map(|p| p.objects.iter())
.filter_map(|o| match o {
InlineObject::Shape { float, .. } | InlineObject::Image { float, .. } => Some(float),
_ => None,
})
.map(|f: &Float| ((r(f.x), r(f.y)), r(f.rot), (f.flip_h, f.flip_v), f.wrap != Wrap::Inline))
.collect()
}
/// LibreOffice's own: a rectangle turned 30° and an arrow turned 90°, both anticlockwise, whose
/// unrotated boxes sit at (0.3937in, 0.1969in) and (2.756in, 0.1969in); an arrow mirrored.
fn open() -> Document {
let content = format!(
r#"<office:document-content {NS_T} {NS_D}><office:body><office:text><text:p>
<draw:custom-shape text:anchor-type="paragraph" svg:width="1.5752in" svg:height="0.5906in" draw:transform="rotate (0.523598775598299) translate (0.351388888888889in 0.629861111111111in)"><draw:enhanced-geometry draw:type="rectangle"/></draw:custom-shape>
<draw:custom-shape text:anchor-type="paragraph" svg:width="1.1815in" svg:height="0.5906in" draw:transform="rotate (1.5707963267949) translate (3.05138888888889in 1.08194444444444in)"><draw:enhanced-geometry draw:type="right-arrow"/></draw:custom-shape>
<draw:custom-shape text:anchor-type="paragraph" svg:width="1in" svg:height="0.5in" svg:x="1in" svg:y="2in"><draw:enhanced-geometry draw:type="right-arrow" draw:mirror-horizontal="true"/></draw:custom-shape>
<draw:custom-shape text:anchor-type="as-char" svg:width="1in" svg:height="0.5in" draw:transform="rotate (-0.785398163397448) translate (0in 0in)"><draw:enhanced-geometry draw:type="diamond" draw:mirror-vertical="true"/></draw:custom-shape>
text</text:p></office:text></office:body></office:document-content>"#
);
import(&package(&content, None)).unwrap()
}
#[test]
fn turned_shapes_open_turned_about_their_centre() {
assert_eq!(
placed(&open()),
[
((28.3, 14.2), 330.0, (false, false), true),
((198.4, 14.1), 270.0, (false, false), true),
((72.0, 144.0), 0.0, (true, false), true),
// In line with the text: turned 45° clockwise, mirrored, still in line.
((0.0, 0.0), 45.0, (false, true), false),
]
);
}
#[test]
fn turned_shapes_round_trip() {
let d = open();
let bytes = export(&d).unwrap();
let c = content_of(&bytes);
assert!(c.contains("draw:transform=\"rotate (") && c.contains("draw:mirror-horizontal=\"true\""), "{c}");
assert_eq!(placed(&import(&bytes).unwrap()), placed(&d));
}
#[test]
fn a_turned_picture_opens_and_saves_turned() {
let png = [b"\x89PNG\r\n\x1a\n".as_slice(), &[7u8; 64]].concat();
let content = format!(
r#"<office:document-content {NS_T} {NS_D}><office:body><office:text><text:p><draw:frame text:anchor-type="paragraph" svg:width="2in" svg:height="1in" draw:transform="rotate (1.5707963267949) translate (1in 3in)"><draw:image xlink:href="Pictures/a.png"/></draw:frame>Text.</text:p></office:text></office:body></office:document-content>"#
);
let d = import(&zip_of(&[("content.xml", content.as_bytes()), ("Pictures/a.png", &png)])).unwrap();
// Centre: (1in, 3in) + the half size turned a quarter anticlockwise, (0.5in, -1in): the
// unrotated box's top left is half its size back from (1.5in, 2in).
assert_eq!(placed(&d), [((36.0, 108.0), 270.0, (false, false), true)]);
assert_eq!(placed(&import(&export(&d).unwrap()).unwrap()), placed(&d));
}
#[test]
fn hostile_transforms_do_not_panic() {
for t in [
"rotate (NaN) translate (1e40in 2in)",
"rotate",
"rotate (1",
"translate (1in)",
"scale (2 2) rotate (1e30) skewX (3)",
"matrix (1 0 0 1 0 0)",
"rotate ((((",
"",
] {
let content = format!(
r#"<office:document-content {NS_T} {NS_D}><office:body><office:text><text:p><draw:custom-shape text:anchor-type="paragraph" svg:width="1in" svg:height="1in" draw:transform="{t}"><draw:enhanced-geometry draw:type="star5"/></draw:custom-shape></text:p></office:text></office:body></office:document-content>"#
);
let d = import(&package(&content, None)).unwrap();
for ((x, y), rot, ..) in placed(&d) {
assert!(x.is_finite() && y.is_finite() && rot.is_finite() && (0.0..360.0).contains(&rot), "{t}: {x} {y} {rot}");
}
let _ = export(&d).unwrap();
}
}
+30 -10
View File
@@ -442,12 +442,16 @@ fn box_look_attrs(look: &model::BoxLook) -> String {
a
}
/// How a frame or shape is anchored and placed: its `text:anchor-type`, its graphic style's
/// placement and its `svg:x`/`svg:y`; in line with the text when it doesn't float.
fn placement(float: Option<&wordcraft_doc::para::Float>) -> (&'static str, String, String) {
/// How a frame or shape `w` × `h` is anchored and placed: its `text:anchor-type`, its graphic
/// style's placement and its `svg:x`/`svg:y` (or `draw:transform` when it is turned); in line
/// with the text when it doesn't float.
fn placement(float: Option<&wordcraft_doc::para::Float>, w: f32, h: f32) -> (&'static str, String, String) {
match float {
Some(f) => super::frames::frame_of(f),
None => ("as-char", " style:vertical-pos=\"top\" style:vertical-rel=\"baseline\"".to_string(), String::new()),
Some(f) if f.wrap != wordcraft_doc::para::Wrap::Inline => super::frames::frame_of(f, w, h),
f => {
let turn = f.filter(|f| super::frames::turned(f)).map(|f| super::frames::transform_attr(0.0, 0.0, w, h, f.rot));
("as-char", " style:vertical-pos=\"top\" style:vertical-rel=\"baseline\"".to_string(), turn.unwrap_or_default())
}
}
}
@@ -853,7 +857,7 @@ impl Writer {
// A floating picture: anchored, in a graphic style that places and wraps it.
let (anchor, place) = match img.float.as_deref() {
Some(f) => {
let (anchor, props, xy) = super::frames::frame_of(f);
let (anchor, props, xy) = placement(Some(f), img.w, img.h);
(anchor, format!(" draw:style-name=\"{}\"{xy}", self.graphic_style(props)))
}
None => ("as-char", String::new()),
@@ -954,7 +958,7 @@ impl Writer {
let reopen = self.open_ins.as_ref().map(|(m, _)| m.clone());
self.close_ins(out);
let frame = box_look_attrs(look);
let (anchor, props, xy) = placement(float);
let (anchor, props, xy) = placement(float, size.0, size.1);
let style = self.graphic_style(format!("{props}{frame}"));
let mut body = self.story(blocks);
if body.is_empty() {
@@ -993,7 +997,7 @@ impl Writer {
) {
let reopen = self.open_ins.as_ref().map(|(m, _)| m.clone());
self.close_ins(out);
let (anchor, props, xy) = placement(float);
let (anchor, props, xy) = placement(float, size.0, size.1);
let style = self.graphic_style(format!("{props}{}", shape_look_attrs(look)));
let body = self.story(blocks);
self.boxes += 1;
@@ -1008,12 +1012,28 @@ impl Writer {
let (x, y) = float.map_or((0.0, 0.0), |f| (off(f.x, f.h_align.is_some()), off(f.y, f.v_align.is_some())));
let rises = float.is_some_and(|f| f.flip_v != f.flip_h);
let (y1, y2) = if rises { (y + h, y) } else { (y, y + h) };
format!(" svg:x1=\"{}\" svg:y1=\"{}\" svg:x2=\"{}\" svg:y2=\"{}\"", pt(x), pt(y1), pt(x + w), pt(y2))
// Turned about its centre: its ends turned with it.
let (cx, cy) = (x + w / 2.0, y + h / 2.0);
let (sin, cos) = float.filter(|f| super::frames::turned(f)).map_or(0.0, |f| f.rot.to_radians()).sin_cos();
let turn = |px: f32, py: f32| (cx + (px - cx) * cos - (py - cy) * sin, cy + (px - cx) * sin + (py - cy) * cos);
let ((x1, y1), (x2, y2)) = (turn(x, y1), turn(x + w, y2));
format!(" svg:x1=\"{}\" svg:y1=\"{}\" svg:x2=\"{}\" svg:y2=\"{}\"", pt(x1), pt(y1), pt(x2), pt(y2))
}
} else {
format!(" svg:width=\"{}\" svg:height=\"{}\"{xy}", pt(len(size.0, 1.0)), pt(len(size.1, 1.0)))
};
let geometry = geometry.map(|t| format!("<draw:enhanced-geometry svg:viewBox=\"0 0 21600 21600\" draw:type=\"{t}\"/>")).unwrap_or_default();
// Mirrored, as a custom shape's geometry says (§20.112, §20.113).
let mirror = |on: bool, k: &str| if on { format!(" draw:mirror-{k}=\"true\"") } else { String::new() };
let (fh, fv) = float.map_or((false, false), |f| (f.flip_h, f.flip_v));
let geometry = geometry
.map(|t| {
format!(
"<draw:enhanced-geometry svg:viewBox=\"0 0 21600 21600\" draw:type=\"{t}\"{}{}/>",
mirror(fh, "horizontal"),
mirror(fv, "vertical")
)
})
.unwrap_or_default();
out.push_str(&format!(
"<{element} draw:name=\"{}Shape{}\" draw:style-name=\"{style}\" text:anchor-type=\"{anchor}\"{at} draw:z-index=\"0\">{body}{geometry}</{element}>",
self.prefix, self.boxes,
File diff suppressed because one or more lines are too long