From 02da9bbdc916906163475ed9dca689270f3cbd8a Mon Sep 17 00:00:00 2001 From: Hiroshi Inukai Date: Mon, 12 Oct 2026 00:07:28 +0900 Subject: [PATCH] PDF/.ai import: evenly tracked type on a path keeps its length Straight type takes the tracking that keeps its length as the file set it; type on a path took none, so letter-spaced type on a curve came back closed up to the font's advances (an "OFFICIAL ISSUED" badge sat to one side of its band, a condensed title ran off the end of its path). Type on a path now takes the same tracking, its length measured along the path from the first glyph to the end of the last. Co-Authored-By: Claude Opus 5.5 --- crates/pdf/src/import_text.rs | 14 ++++++++++++++ crates/pdf/src/tests_realtext.rs | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/crates/pdf/src/import_text.rs b/crates/pdf/src/import_text.rs index 8429aae4..1ccab798 100644 --- a/crates/pdf/src/import_text.rs +++ b/crates/pdf/src/import_text.rs @@ -350,6 +350,20 @@ impl TextLine { t.runs = tracked_each(std::mem::take(&mut t.runs), own, self.at.size); } if let Some(path) = path { + // Evenly spaced type keeps its length along the path: the path runs through the + // glyphs' origins, one curve each, to the end of the last glyph, and tracking makes up + // the difference from the advances. + if own.is_none() { + let length: f64 = path.segments().take(self.baseline.len()).map(|seg| kurbo::ParamCurveArclen::arclen(&seg, 1e-3)).sum(); + let laid = vectorcraft_text::layout(db, &t); + let natural: f64 = laid.glyphs.iter().map(|g| g.advance).sum(); + let chars: usize = t.runs.iter().map(|r| r.text.chars().count()).sum(); + let size = self.at.size; + if chars > 1 && length.is_finite() && (length - natural).abs() > size * 0.01 { + let (tracking, spaces) = spacing(&laid, &[], length - natural, chars, size); + t.runs = tracked(std::mem::take(&mut t.runs), tracking, &spaces); + } + } t.kind = TextKind::OnPath { path: vectorcraft_geom::PathData::from_bezpath(&path), start: 0.0, end: None }; t.xf = Affine::IDENTITY; t.cached_bounds = Some(vectorcraft_text::layout(db, &t).bounds); diff --git a/crates/pdf/src/tests_realtext.rs b/crates/pdf/src/tests_realtext.rs index f7e83f84..5186cb15 100644 --- a/crates/pdf/src/tests_realtext.rs +++ b/crates/pdf/src/tests_realtext.rs @@ -424,3 +424,22 @@ fn glyphs_spaced_each_their_own_way_keep_their_place() { } }); } + +/// Evenly tracked type on a path comes back with its tracking: each glyph where it was along the +/// curve, not closed up to the font's advances. +#[test] +fn evenly_tracked_type_on_a_path_keeps_its_length() { + let mut arc = BezPath::new(); + arc.move_to((40.0, 200.0)); + arc.curve_to((100.0, 80.0), (260.0, 80.0), (320.0, 200.0)); + let mut t = TextObject::point(Point::ORIGIN, "ARCHING TYPE", CharStyle { tracking: 80.0, ..style(24.0) }); + t.kind = TextKind::OnPath { path: PathData::from_bezpath(&arc), start: 0.0, end: None }; + let d = doc(vec![t]); + let want = origins(&d); + let back = import_as(&pdf(&d, false).bytes, TextAs::default()); + let got = origins(&back); + assert_eq!(got.len(), want.len(), "{:?}", texts(&back)); + for (g, w) in got.iter().zip(&want) { + assert!(g.distance(*w) < 0.3, "{got:?}\nvs {want:?}"); + } +}