Merge pull request #1569 from switch87/feat/svg-flowroot-area-type
SVG import: Inkscape flowed text (flowRoot) becomes area type
This commit is contained in:
2 files changed
+82
-3
No files matched your search
@@ -175,7 +175,8 @@ pub(super) fn prepare<'s>(svg: &'s str, xml: &roxmltree::Document, dpi: f64, war
|
||||
let cx = Ctx::new(xml, dpi);
|
||||
let mut out = String::new();
|
||||
let mut copied = 0;
|
||||
for t in xml.descendants().filter(|n| n.is_element() && n.tag_name().name() == "text" && n.tag_name().namespace() == Some(SVG_NS)) {
|
||||
let is_text = |n: &XNode| n.is_element() && matches!(n.tag_name().name(), "text" | "flowRoot") && n.tag_name().namespace() == Some(SVG_NS);
|
||||
for t in xml.descendants().filter(is_text) {
|
||||
let label = t.attribute("id").map_or_else(|| "a text".to_string(), |id| format!("text '{id}'"));
|
||||
if t.ancestors().skip(1).any(|a| a.is_element() && a.tag_name().name() == "clipPath") {
|
||||
warnings.push(format!("{label} in a clip path ignored"));
|
||||
@@ -191,11 +192,13 @@ pub(super) fn prepare<'s>(svg: &'s str, xml: &roxmltree::Document, dpi: f64, war
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(q) = qname.strip_suffix("text").filter(|q| q.is_empty() || q.ends_with(':')) else { continue };
|
||||
let flowed = t.tag_name().name() == "flowRoot";
|
||||
let Some(q) = qname.strip_suffix(t.tag_name().name()).filter(|q| q.is_empty() || q.ends_with(':')) else { continue };
|
||||
if cx.css.own(t, "display").as_deref() == Some("none") || r.start < copied {
|
||||
continue;
|
||||
}
|
||||
let Some((text, paints)) = cx.build(xml, t, &label, warnings) else { continue };
|
||||
let built = if flowed { cx.build_flow(t, &label, warnings) } else { cx.build(xml, t, &label, warnings) };
|
||||
let Some((text, paints)) = built else { continue };
|
||||
out.push_str(&svg[copied..r.start]);
|
||||
placeholder(&mut out, q, &cx.css, t, &slots.prefix, slots.texts.len(), text.marker, &paints);
|
||||
copied = r.end;
|
||||
@@ -560,6 +563,47 @@ impl Ctx {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read one Inkscape `<flowRoot>` (SVG 1.2 flowed text, which browsers don't draw but
|
||||
/// Inkscape writes) as area type: its frame the first `<rect>` of its `<flowRegion>`, a
|
||||
/// paragraph per `<flowPara>`, styled as `<text>` is. Other regions are left out with a warning.
|
||||
fn build_flow(&self, t: XNode, label: &str, warnings: &mut Vec<String>) -> Option<(PendingText, Vec<String>)> {
|
||||
let (vw, vh) = self.viewport;
|
||||
let region = t.children().find(|c| c.is_element() && c.tag_name().name() == "flowRegion");
|
||||
let rect = region.and_then(|r| r.children().find(|c| c.is_element() && c.tag_name().name() == "rect"));
|
||||
let size = |n: XNode, a: &str, pct: f64| n.attribute(a).and_then(|v| self.length(v, DEFAULT_FONT_SIZE, pct));
|
||||
let frame = rect.and_then(|r| {
|
||||
let (w, h) = (size(r, "width", vw)?, size(r, "height", vh)?);
|
||||
(w > 0.0 && h > 0.0).then(|| (r, Rect::new(0.0, 0.0, w, h)))
|
||||
});
|
||||
let Some((r, frame)) = frame else {
|
||||
warnings.push(format!("{label}: flowed text (flowRoot) without a rectangle region was left out"));
|
||||
return None;
|
||||
};
|
||||
let origin = Affine::translate((size(r, "x", vw).unwrap_or(0.0), size(r, "y", vh).unwrap_or(0.0)));
|
||||
let paras: Vec<XNode> = t.descendants().filter(|n| n.is_element() && n.tag_name().name() == "flowPara").collect();
|
||||
let collapse = |n: XNode| {
|
||||
n.descendants().filter(XNode::is_text).filter_map(|c| c.text()).collect::<String>().split_whitespace().collect::<Vec<_>>().join(" ")
|
||||
};
|
||||
let text = paras.iter().map(|p| collapse(*p)).collect::<Vec<_>>().join("\n");
|
||||
if text.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let style = self.elem(paras.first().copied().unwrap_or(t)).style.clone();
|
||||
let mut obj = TextObject::point(Point::ZERO, &text, style);
|
||||
obj.kind = TextKind::Area { frame: vectorcraft_geom::shapes::rectangle(frame) };
|
||||
obj.xf = parse_transform(r.attribute("transform")) * origin;
|
||||
let servers = vec![(None, None); obj.runs.len()];
|
||||
let b = obj.xf.transform_rect_bbox(frame);
|
||||
// A size, place or transform past what numbers hold (`width="1e999"`) has no frame to set type in.
|
||||
if ![b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite()) {
|
||||
warnings.push(format!("{label}: flowed text (flowRoot) with a region too large to place was left out"));
|
||||
return None;
|
||||
}
|
||||
let marker = Rect::new(b.x0, b.y0, b.x1.max(b.x0 + 1.0), b.y1.max(b.y0 + 1.0));
|
||||
let name = t.attribute("id").unwrap_or("").to_string();
|
||||
Some((PendingText { name, obj, servers, paints: 0, non_scaling: false, under: None, marker }, Vec::new()))
|
||||
}
|
||||
|
||||
/// Read one `<text>`: the pending text and its `url(#…)` paints.
|
||||
fn build(&self, xml: &roxmltree::Document, t: XNode, label: &str, warnings: &mut Vec<String>) -> Option<(PendingText, Vec<String>)> {
|
||||
let preserve = t.ancestors().filter(|a| a.is_element()).find_map(|a| a.attribute((roxmltree::NS_XML_URI, "space"))) == Some("preserve");
|
||||
|
||||
@@ -594,3 +594,38 @@ fn the_font_shorthand_styles_type() {
|
||||
let style = st.font_style.to_ascii_lowercase();
|
||||
assert!(style.contains("bold") && style.contains("italic"), "{style}");
|
||||
}
|
||||
|
||||
/// Inkscape's flowed text (`<flowRoot>`) becomes area type in its region's rectangle, a paragraph
|
||||
/// per `<flowPara>`; another kind of region is left out with a warning, not silently.
|
||||
#[test]
|
||||
fn flowed_text_becomes_area_type() {
|
||||
let (d, w) = import_with_report(&svg(
|
||||
r#"<flowRoot style="font-size:16px;font-family:sans-serif"><flowRegion><rect x="20" y="30" width="100" height="160"/></flowRegion><flowPara>This is flowed text</flowPara><flowPara>Second <flowSpan>one</flowSpan></flowPara></flowRoot>
|
||||
<flowRoot><flowRegion><path d="M0 0 L10 0 L5 10 Z"/></flowRegion><flowPara>Lost</flowPara></flowRoot>"#,
|
||||
))
|
||||
.unwrap();
|
||||
let t = only_text(&d);
|
||||
assert_eq!(t.plain_text(), "This is flowed text\nSecond one");
|
||||
let TextKind::Area { frame } = &t.kind else { panic!("{:?}", t.kind) };
|
||||
let b = t.xf.transform_rect_bbox(frame.bounds().unwrap());
|
||||
assert!(close(b.x0, 20.0, 1e-6) && close(b.y0, 30.0, 1e-6) && close(b.x1, 120.0, 1e-6) && close(b.y1, 190.0, 1e-6), "{b:?}");
|
||||
assert!(close(t.runs[0].style.size * t.xf.determinant().abs().sqrt(), 16.0, 1e-6));
|
||||
assert!(w.iter().any(|w| w.contains("flowRoot")), "{w:?}");
|
||||
}
|
||||
|
||||
/// Flowed text that can't be read is left out with a warning each, and never stops the import: no
|
||||
/// region, a region too large for numbers, a flowRoot inside a flowRoot (read once, as the outer one).
|
||||
#[test]
|
||||
fn flowed_text_that_cannot_be_read_is_reported() {
|
||||
let (d, w) = import_with_report(&svg(
|
||||
r#"<flowRoot id="none"><flowPara>No region</flowPara></flowRoot>
|
||||
<flowRoot id="huge"><flowRegion><rect width="1e999" height="10"/></flowRegion><flowPara>Huge</flowPara></flowRoot>
|
||||
<flowRoot id="far"><flowRegion><rect x="1e308" width="1e308" height="10" transform="scale(1e308)"/></flowRegion><flowPara>Far</flowPara></flowRoot>
|
||||
<flowRoot id="outer"><flowRegion><rect width="50" height="50"/></flowRegion><flowPara>Outer</flowPara><flowRoot><flowRegion><rect width="5" height="5"/></flowRegion><flowPara>inner</flowPara></flowRoot></flowRoot>"#,
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(only_text(&d).plain_text(), "Outer\ninner");
|
||||
for id in ["none", "huge", "far"] {
|
||||
assert!(w.iter().any(|w| w.contains(id) && w.contains("flowRoot")), "{id}: {w:?}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user