mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 23:05:05 +00:00
PDF/.ai export: fit Dynamic Text before resolving font heights
The legacy .ai writer resolved cap-height and other font height references to em sizes before fitting, so Dynamic Text stepped and limited the em size instead of the size the Character panel shows, and the type could be written smaller than it is drawn. It now fits first, and outlines come from the type's own layout. The editing data now also warns that Dynamic Text is not written when the type is written as outlines.
This commit is contained in:
@@ -4366,6 +4366,19 @@ fn dynamic_text_is_written_at_its_drawn_sizes() {
|
||||
assert!(!texts[0].dynamic_text);
|
||||
let k = texts[0].xf.as_coeffs()[0];
|
||||
assert_eq!(r3(texts[0].runs[0].style.size * k), r3(drawn));
|
||||
// Type written as outlines (here on an open path whose ends meet) loses the setting too, and
|
||||
// says so.
|
||||
let mut doc = Document::new(600.0, 300.0);
|
||||
let layer = doc.layers[0].id;
|
||||
let mut t = TextObject::point(vectorcraft_geom::Point::ZERO, "Grow", style(12.0, Color::BLACK));
|
||||
let ends_meet = [(50.0, 200.0).into(), (550.0, 200.0).into(), (300.0, 50.0).into(), (50.0, 200.0).into()];
|
||||
t.kind = TextKind::OnPath { path: PathData::new(vec![vectorcraft_geom::SubPath::polyline(&ends_meet, false)]), start: 0.0, end: None };
|
||||
t.xf = Affine::IDENTITY;
|
||||
t.dynamic_text = true;
|
||||
let node = text_node(&mut doc, t);
|
||||
doc.insert(Some(layer), usize::MAX, node).unwrap();
|
||||
let out = export_ai_editing(&doc, &opts()).unwrap();
|
||||
assert!(out.warnings.iter().any(|w| w == writer::TYPE) && out.warnings.iter().any(|w| w == writer::DYNAMIC_TEXT), "{:?}", out.warnings);
|
||||
}
|
||||
|
||||
/// See-through type with the knockout shape flag comes back with the flag and writes no warning.
|
||||
|
||||
@@ -1257,6 +1257,10 @@ impl<'a> Writer<'a> {
|
||||
return;
|
||||
}
|
||||
self.warn(TYPE);
|
||||
// The outlines keep the sizes Dynamic Text draws the type at, but not the setting.
|
||||
if matches!(&n.kind, NodeKind::Text(t) if t.dynamic_text) {
|
||||
self.warn(DYNAMIC_TEXT);
|
||||
}
|
||||
let doc = self.doc;
|
||||
let resolved = match &n.kind {
|
||||
NodeKind::Text(t) => Some(doc.inline_resolved(t).into_owned()),
|
||||
|
||||
@@ -1263,6 +1263,44 @@ fn dynamic_text_is_written_at_its_drawn_sizes_with_a_warning() {
|
||||
assert!(out.warnings.iter().any(|w| w == DYNAMIC_TEXT), "{:?}", out.warnings);
|
||||
}
|
||||
|
||||
/// Dynamic Text fits type whose size measures the cap height at the size the Character panel
|
||||
/// shows: 1,000 pt measuring a 0.7 em cap height on a long path grows to the 1,296 pt limit and is
|
||||
/// written at the em size drawn there, as text and as outlines.
|
||||
#[test]
|
||||
fn dynamic_text_with_a_font_height_reference_is_written_at_its_drawn_size() {
|
||||
use vectorcraft_doc::{TextKind, TextObject};
|
||||
use vectorcraft_text::test_fonts::{HEIGHT_FAMILY, height_font};
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
db.add_font(height_font(true).unwrap());
|
||||
let mut t = TextObject::point(vectorcraft_geom::Point::ZERO, "HE", style(1000.0, red()));
|
||||
let st = &mut t.runs[0].style;
|
||||
st.font_family = HEIGHT_FAMILY.into();
|
||||
st.font_height_reference = vectorcraft_doc::FontHeightReference::CapHeight;
|
||||
let mut line = PathData::new(vec![]);
|
||||
line.subpaths.push(vectorcraft_geom::SubPath::polyline(&[(0.0, 2000.0).into(), (100_000.0, 2000.0).into()], false));
|
||||
t.kind = TextKind::OnPath { path: line, start: 0.0, end: None };
|
||||
t.xf = Affine::IDENTITY;
|
||||
t.dynamic_text = true;
|
||||
let lay = vectorcraft_text::layout(db, &t);
|
||||
assert!((lay.fit_scale - 1.296).abs() < 1e-9, "{}", lay.fit_scale);
|
||||
let em = 1296.0 / 0.7;
|
||||
let mut glyphs = kurbo::BezPath::new();
|
||||
for g in &lay.glyphs {
|
||||
glyphs.extend(g.outline.iter());
|
||||
}
|
||||
let want = glyphs.bounding_box();
|
||||
let mut doc = Document::new(300.0, 200.0);
|
||||
let layer = doc.layers[0].id;
|
||||
let n = Node::new(doc.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
let back = crate::import(&write(&doc).bytes).unwrap().document;
|
||||
let size = texts(&back)[0].1.runs[0].style.size;
|
||||
assert!((size - em).abs() < 1e-3, "{size} vs {em}");
|
||||
let back = crate::import(&write_with(&doc, LegacyAiOptions { outline_text: true, ..LegacyAiOptions::default() }).bytes).unwrap().document;
|
||||
let b = children(&children(&back.layers[0])[0])[0].visual_bounds().unwrap();
|
||||
assert!((b.height() - want.height()).abs() < 0.05 && (b.width() - want.width()).abs() < 0.05, "{b:?}, laid out at {want:?}");
|
||||
}
|
||||
|
||||
/// Characters with Auto leading are written with their paragraph's Auto Leading share of the
|
||||
/// size: 18 pt type in paragraphs at 150% is written with 27 pt leading.
|
||||
#[test]
|
||||
|
||||
@@ -657,15 +657,11 @@ impl<'a> Writer<'a> {
|
||||
// ---------- type ----------
|
||||
|
||||
/// Type: a text object when the format can hold it as text, else its outlines (type with
|
||||
/// rotated characters among them, which keeps their rotation). A run whose
|
||||
/// font size measures a height other than the em box is written at the em size it is drawn
|
||||
/// at ([`vectorcraft_text::FontDb::font_heights_resolved`]).
|
||||
/// rotated characters among them, which keeps their rotation).
|
||||
fn text(&mut self, t: &TextObject) {
|
||||
if !invertible(t.xf) {
|
||||
return;
|
||||
}
|
||||
let resolved = vectorcraft_text::FontDb::global().font_heights_resolved(t);
|
||||
let t = &*resolved;
|
||||
// Written as text or as outlines, the type keeps its drawn sizes but not the setting.
|
||||
if t.dynamic_text && matches!(t.kind, TextKind::OnPath { .. }) {
|
||||
self.warn(DYNAMIC_TEXT);
|
||||
@@ -687,14 +683,18 @@ impl<'a> Writer<'a> {
|
||||
/// A text object (§15): `type To`, the text path (`a b c d tx ty startPt Tp`, an area's frame
|
||||
/// or the path type runs along, `TP`), then each run's style and characters, `TO`. The matrix
|
||||
/// takes text space (y up, the first baseline's start for point type) to art space; the frame
|
||||
/// and path are in art space, as the specification's examples have them (§15.5).
|
||||
/// and path are in art space, as the specification's examples have them (§15.5). Type that
|
||||
/// Shrink Text to Fit or Dynamic Text scales is written in the styles it draws in, fitted at
|
||||
/// the sizes the Character panel shows; then a run whose font size measures a height other
|
||||
/// than the em box is written at the em size it is drawn at
|
||||
/// ([`vectorcraft_text::FontDb::font_heights_resolved`]).
|
||||
fn text_object(&mut self, t: &TextObject) {
|
||||
if lost_attributes(t) {
|
||||
self.warn(TYPE_ATTRIBUTES);
|
||||
}
|
||||
// Type that Shrink Text to Fit or Dynamic Text scales is written in the styles it draws in.
|
||||
let fitted = vectorcraft_text::drawn(t);
|
||||
let t = &*fitted;
|
||||
let resolved = vectorcraft_text::FontDb::global().font_heights_resolved(&fitted);
|
||||
let t = &*resolved;
|
||||
let m = self.to_art * t.xf * Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 0.0]);
|
||||
let (kind, path, start) = match &t.kind {
|
||||
TextKind::Point => (0, None, 0.0),
|
||||
|
||||
Reference in New Issue
Block a user