Merge pull request #6 from storybored379/pr/svg-import-text-on-path
SVG import: <textPath> comes back as type on a path
This commit is contained in:
commit
cbf5dc99bb
|
|
@ -685,13 +685,34 @@ fn text_fallback(im: &mut Importer, svg: &str, w: f64, h: f64, layer_mode: bool)
|
||||||
if rb.runs.is_empty() {
|
if rb.runs.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Type on a path: a <textPath> naming a <path> (in the text's user space) by href.
|
||||||
|
let on_path = t.children().find(|c| c.is_element() && c.tag_name().name() == "textPath").and_then(|tp| {
|
||||||
|
let href = tp.attribute("href").or_else(|| tp.attribute(("http://www.w3.org/1999/xlink", "href")))?;
|
||||||
|
let id = href.strip_prefix('#')?;
|
||||||
|
let el = xml.descendants().find(|n| n.is_element() && n.tag_name().name() == "path" && n.attribute("id") == Some(id))?;
|
||||||
|
let mut bp = BezPath::from_svg(el.attribute("d")?).ok()?;
|
||||||
|
bp.apply_affine(acc * parse_transform(el.attribute("transform")));
|
||||||
|
let start = match tp.attribute("startOffset").map(str::trim) {
|
||||||
|
Some(o) if o.ends_with('%') => first_number(o.strip_suffix('%')) / 100.0,
|
||||||
|
Some(o) => {
|
||||||
|
let len: f64 = bp.segments().map(|s| kurbo::ParamCurveArclen::arclen(&s, 1e-3)).sum();
|
||||||
|
if len > 0.0 { first_number(Some(o)) / len } else { 0.0 }
|
||||||
|
}
|
||||||
|
None => 0.0,
|
||||||
|
};
|
||||||
|
Some((PathData::from_bezpath(&bp), start.clamp(0.0, 1.0)))
|
||||||
|
});
|
||||||
let justify = match ctx.prop(t, "text-anchor").as_deref() {
|
let justify = match ctx.prop(t, "text-anchor").as_deref() {
|
||||||
Some("middle") => Justify::Center,
|
Some("middle") => Justify::Center,
|
||||||
Some("end") => Justify::Right,
|
Some("end") => Justify::Right,
|
||||||
_ => Justify::Left,
|
_ => Justify::Left,
|
||||||
};
|
};
|
||||||
|
let (kind, xf) = match on_path {
|
||||||
|
Some((path, start)) => (TextKind::OnPath { path, start }, Affine::IDENTITY),
|
||||||
|
None => (TextKind::Point, xf),
|
||||||
|
};
|
||||||
let mut obj = TextObject {
|
let mut obj = TextObject {
|
||||||
kind: TextKind::Point,
|
kind,
|
||||||
xf,
|
xf,
|
||||||
runs: rb.runs,
|
runs: rb.runs,
|
||||||
para: Default::default(),
|
para: Default::default(),
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
//! * Masks and filters are ignored (reported as warnings); nested clip paths use the outer clip only.
|
//! * Masks and filters are ignored (reported as warnings); nested clip paths use the outer clip only.
|
||||||
//! * usvg only keeps `<text>` when fonts are loaded; we don't load a font database (too expensive and
|
//! * usvg only keeps `<text>` when fonts are loaded; we don't load a font database (too expensive and
|
||||||
//! unavailable on wasm), so `<text>` elements are read directly from the XML as live point-type
|
//! unavailable on wasm), so `<text>` elements are read directly from the XML as live point-type
|
||||||
//! [`TextObject`]s and placed on top of their layer.
|
//! [`TextObject`]s (type on a path for a `<textPath>`) and placed on top of their layer.
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
mod export;
|
mod export;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||||
use vectorcraft_color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, Paint};
|
use vectorcraft_color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, Paint};
|
||||||
use vectorcraft_doc::{
|
use vectorcraft_doc::{
|
||||||
Appearance, AppearanceItem, CharStyle, Dash, Document, FillLayer, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeId, NodeKind, StrokeAlign,
|
Appearance, AppearanceItem, CharStyle, Dash, Document, FillLayer, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeId, NodeKind, StrokeAlign,
|
||||||
StrokeLayer, TextObject,
|
StrokeLayer, TextKind, TextObject,
|
||||||
};
|
};
|
||||||
use vectorcraft_geom::{Affine, FillRule, PathData, Point, Rect, shapes};
|
use vectorcraft_geom::{Affine, FillRule, PathData, Point, Rect, shapes};
|
||||||
use vectorcraft_svg::{ExportOptions, Styling, export, import, import_with_report};
|
use vectorcraft_svg::{ExportOptions, Styling, export, import, import_with_report};
|
||||||
|
|
@ -374,6 +374,26 @@ fn text_roundtrip_and_escaping() {
|
||||||
assert!(t.xf.translation().to_point().distance(Point::new(20.0, 50.0)) < 0.01);
|
assert!(t.xf.translation().to_point().distance(Point::new(20.0, 50.0)) < 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_on_path_roundtrip() {
|
||||||
|
let mut d = Document::new(200.0, 200.0);
|
||||||
|
let mut curve = vectorcraft_geom::BezPath::new();
|
||||||
|
curve.move_to((20.0, 150.0));
|
||||||
|
curve.curve_to((60.0, 40.0), (140.0, 40.0), (180.0, 150.0));
|
||||||
|
let mut t = TextObject::point(Point::new(0.0, 0.0), "On a curve", CharStyle { size: 14.0, ..CharStyle::default() });
|
||||||
|
t.kind = TextKind::OnPath { path: PathData::from_bezpath(&curve), start: 0.25 };
|
||||||
|
let id = d.alloc_id();
|
||||||
|
let d = doc_with(vec![Node::new(id, NodeKind::Text(Box::new(t)))]);
|
||||||
|
let r = roundtrip(&d);
|
||||||
|
let a = art(&r);
|
||||||
|
let NodeKind::Text(t) = &a[0].kind else { panic!() };
|
||||||
|
assert_eq!(t.plain_text(), "On a curve");
|
||||||
|
let TextKind::OnPath { path, start } = &t.kind else { panic!("not type on a path: {:?}", t.kind) };
|
||||||
|
assert!((start - 0.25).abs() < 1e-6, "start {start}");
|
||||||
|
let doc_path = path.bounds().map(|b| t.xf.transform_rect_bbox(b)).unwrap();
|
||||||
|
assert!(close_rect(doc_path, PathData::from_bezpath(&curve).bounds().unwrap(), 0.01), "{doc_path:?}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn styling_modes_roundtrip() {
|
fn styling_modes_roundtrip() {
|
||||||
for styling in [Styling::PresentationAttributes, Styling::InlineStyle, Styling::InternalCss] {
|
for styling in [Styling::PresentationAttributes, Styling::InlineStyle, Styling::InternalCss] {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue