mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Fix #3964: Create Outlines, EPS, EMF/WMF and legacy .ai keep the ruby where the canvas draws it
This commit is contained in:
committed by
bflatastic
parent
7f8bc837bd
commit
cab62cbb18
@@ -153,7 +153,8 @@ fn laid_out(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::Bez
|
||||
let layout = vectorcraft_text::layout(db, t);
|
||||
let mut runs = vec![kurbo::BezPath::new(); t.runs.len()];
|
||||
let mut all = kurbo::BezPath::new();
|
||||
for g in &layout.glyphs {
|
||||
// Ruby (furigana) glyphs are painted with their base text's run, as the renderer paints them.
|
||||
for g in layout.glyphs.iter().chain(&layout.ruby) {
|
||||
if let Some(r) = runs.get_mut(g.run) {
|
||||
r.extend(g.outline.iter());
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ pub const AI_OPTIONS: &[FormatOption] = &[
|
||||
name: "outlineText",
|
||||
ty: "boolean",
|
||||
default: "false",
|
||||
description: "with aiVersion 7, type as its outlines instead of text (which apps reading the file set in the fonts it names: they need them installed); type 8-bit text can't hold (characters outside Windows Latin-1, vertical type, inline graphics, gradient or pattern fills) is always outlines",
|
||||
description: "with aiVersion 7, type as its outlines instead of text (which apps reading the file set in the fonts it names: they need them installed); type 8-bit text can't hold (characters outside Windows Latin-1, vertical type, ruby, inline graphics, gradient or pattern fills) is always outlines",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -580,6 +580,18 @@ fn create_outlines(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
node.appearance = st.appearance().into();
|
||||
children.push(Arc::new(node));
|
||||
}
|
||||
// Ruby (furigana) glyphs, where the layout put them, in their base text's run's paint,
|
||||
// as the canvas draws them (colour glyphs in ruby draw nothing there either).
|
||||
for g in &lay.ruby {
|
||||
let path = PathData::from_bezpath(&g.outline).transformed(t.xf);
|
||||
if path.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let st = t.runs.get(g.run).map(|r| r.style.clone()).unwrap_or_else(|| t.first_style());
|
||||
let mut node = shape_node(d, path, None);
|
||||
node.appearance = st.appearance().into();
|
||||
children.push(Arc::new(node));
|
||||
}
|
||||
// Underline and strikethrough bars become paths of their run's paint too (#847).
|
||||
for (run, bar) in vectorcraft_text::decorations(&lay, vectorcraft_text::FontDb::global(), t) {
|
||||
let st = t.runs.get(run).map(|r| r.style.clone()).unwrap_or_else(|| t.first_style());
|
||||
@@ -2179,6 +2191,33 @@ mod ruby_tests {
|
||||
assert!(text_of(&s, id).ruby.is_empty());
|
||||
}
|
||||
|
||||
/// Create Outlines keeps the ruby: one path per inked glyph of the base text and of the
|
||||
/// reading, each where the layout (and so the canvas) puts it.
|
||||
#[test]
|
||||
fn create_outlines_keeps_the_ruby_where_it_is_laid_out() {
|
||||
use kurbo::Shape;
|
||||
let (mut s, id) = session_with_text();
|
||||
s.execute("type.ruby", &json!({"text": "かんじ", "start": 0, "end": 6})).unwrap();
|
||||
let t = text_of(&s, id).clone();
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
|
||||
let inked = |gs: &[vectorcraft_text::PositionedGlyph]| -> Vec<kurbo::Rect> {
|
||||
gs.iter().filter(|g| !g.outline.elements().is_empty()).map(|g| t.xf.transform_rect_bbox(g.outline.bounding_box())).collect()
|
||||
};
|
||||
let (base, ruby) = (inked(&lay.glyphs), inked(&lay.ruby));
|
||||
assert_eq!(ruby.len(), 3, "the reading's three kana have ink");
|
||||
let g = s.execute("type.createOutlines", &json!({})).unwrap()["ids"][0].as_u64().unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let paths: Vec<kurbo::Rect> = d.node(NodeId(g)).unwrap().children().unwrap().iter().filter_map(|c| c.geometric_bounds()).collect();
|
||||
assert_eq!(paths.len(), base.len() + ruby.len(), "a path per glyph, the reading's included: {paths:?}");
|
||||
let near = |a: &kurbo::Rect, b: &kurbo::Rect| [a.x0 - b.x0, a.y0 - b.y0, a.x1 - b.x1, a.y1 - b.y1].iter().all(|v| v.abs() < 0.05);
|
||||
for r in &ruby {
|
||||
assert!(paths.iter().any(|p| near(p, r)), "the kana at {r:?} among {paths:?}");
|
||||
}
|
||||
// Undo brings the type back with its ruby.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(text_of(&s, id).ruby.len(), 1);
|
||||
}
|
||||
|
||||
/// Ruby is written to SVG and PDF with its text: SVG outlines the type where ruby is present
|
||||
/// (a reader's `<text>` can't place the reading), PDF differs when the ruby is there.
|
||||
#[test]
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
//! doesn't size), transparent image pixels over white. Hidden objects go to a hidden layer of
|
||||
//! their own above their layer, are left out or show, as [`HiddenObjects`] says (in a hidden
|
||||
//! layer they stay where they are). Type that 8-bit text can't hold (characters outside
|
||||
//! Windows-1252, vertical type, inline graphics, gradient or pattern fills) is written as its
|
||||
//! Windows-1252, vertical type, ruby, inline graphics, gradient or pattern fills) is written as its
|
||||
//! outlines, and all type with [`LegacyAiOptions::outline_text`]. Layer, spot color and gradient
|
||||
//! names and the title are written in Windows-1252 too, and a character outside it is written as
|
||||
//! `?`. Two spot colors or two gradients whose names become the same that way are numbered (`? 2`).
|
||||
@@ -82,7 +82,7 @@ const MAX_IMAGE_PIXELS: u64 = 1 << 26;
|
||||
const HEX_LINE: usize = 60;
|
||||
|
||||
pub const SUBLAYERS: &str = "sublayers are written as groups in their top-level layer (legacy .ai files have no sublayers)";
|
||||
pub const TYPE: &str = "type a legacy .ai can't hold as text (characters outside Windows Latin-1, vertical type, inline graphics, gradient or pattern fills) is written as outlines";
|
||||
pub const TYPE: &str = "type a legacy .ai can't hold as text (characters outside Windows Latin-1, vertical type, ruby, inline graphics, gradient or pattern fills) is written as outlines";
|
||||
pub const TYPE_ATTRIBUTES: &str = "type attributes legacy .ai files don't have (underline, strikethrough, small caps, superscript and subscript, character rotation, OpenType features, Variable Font settings, area type options, type on a path effects, space after paragraphs, tabs, bulleted lists) are left out";
|
||||
pub const HIDDEN: &str = "hidden objects are left out (hidden layers are kept, hidden)";
|
||||
pub const HIDDEN_LAYER: &str = "hidden objects are moved to a hidden layer above their own (legacy .ai files can hide only layers)";
|
||||
@@ -1268,7 +1268,8 @@ impl<'a> Writer<'a> {
|
||||
self.line("u");
|
||||
for (i, run) in t.runs.iter().enumerate() {
|
||||
let mut bp = BezPath::new();
|
||||
for g in layout.glyphs.iter().filter(|g| g.run == i) {
|
||||
// Ruby (furigana) glyphs in their base text's run's paint, as the canvas draws them.
|
||||
for g in layout.glyphs.iter().chain(&layout.ruby).filter(|g| g.run == i) {
|
||||
bp.extend(g.outline.iter());
|
||||
}
|
||||
if bp.elements().is_empty() {
|
||||
@@ -1371,11 +1372,13 @@ impl<'a> Writer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Can type `t` be written as text: horizontal, its runs plain characters (no inline graphics)
|
||||
/// that 8-bit text can hold, filled and stroked with colours?
|
||||
/// Can type `t` be written as text: horizontal, without ruby (which a text object can't hold), its
|
||||
/// runs plain characters (no inline graphics) that 8-bit text can hold, filled and stroked with
|
||||
/// colours?
|
||||
fn writable(t: &TextObject) -> bool {
|
||||
let solid = |p: &Paint| matches!(p, Paint::Solid { .. } | Paint::None);
|
||||
!t.vertical
|
||||
&& t.ruby.is_empty()
|
||||
&& t.runs.iter().any(|r| !r.text.is_empty())
|
||||
&& t.runs.iter().all(|r| {
|
||||
r.inline.is_none()
|
||||
|
||||
@@ -596,6 +596,10 @@ impl<'a> Scene<'a> {
|
||||
self.node(&art, true);
|
||||
}
|
||||
}
|
||||
// Ruby (furigana) glyphs in their base text's run's paint, as the canvas draws them.
|
||||
for g in layout.ruby.iter().filter(|g| g.run == i) {
|
||||
bp.extend(g.outline.iter());
|
||||
}
|
||||
if bp.elements().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -536,3 +536,25 @@ fn colr_glyphs_are_vector_art() {
|
||||
assert!(ps.contains("1 0 0 rg"), "the red layer: {ps}");
|
||||
assert!(ps.contains("0 0 1 rg"), "the foreground layer in the fill: {ps}");
|
||||
}
|
||||
|
||||
/// Ruby (furigana) is written with its base text, as the canvas draws it: one more `m` per contour
|
||||
/// of the reading's glyphs.
|
||||
#[test]
|
||||
fn ruby_is_written_with_its_type() {
|
||||
use vectorcraft_doc::{CharStyle, RubySpan, TextObject};
|
||||
let typed = |ruby: bool| {
|
||||
let mut t = TextObject::point(Point::new(20.0, 100.0), "AB", CharStyle { size: 30.0, ..CharStyle::default() });
|
||||
if ruby {
|
||||
t.ruby = vec![RubySpan::new(0, 2, "xyz")];
|
||||
}
|
||||
t
|
||||
};
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &typed(true));
|
||||
let reading = lay.ruby.iter().flat_map(|g| g.outline.elements()).filter(|e| matches!(e, kurbo::PathEl::MoveTo(_))).count();
|
||||
assert!(reading >= 3, "the reading has ink: {reading}");
|
||||
let moves = |ruby: bool| {
|
||||
let d = doc_with(|d| vec![Node::new(d.alloc_id(), NodeKind::Text(Box::new(typed(ruby))))]);
|
||||
page_of(&d, &opts(&d)).lines().filter(|l| l.ends_with(" m")).count()
|
||||
};
|
||||
assert_eq!(moves(true), moves(false) + reading, "the reading's contours are written");
|
||||
}
|
||||
|
||||
@@ -99,9 +99,9 @@ fn write_with(doc: &Document, o: LegacyAiOptions) -> LegacyAiOutput {
|
||||
/// hidden objects shown when `o` writes them visible, and its gradient fills and strokes painted
|
||||
/// in their first stop's color.
|
||||
fn as_printed(doc: &Document, o: &LegacyAiOptions) -> Document {
|
||||
// A run's glyphs, laid out, as one path with the run's fill (the renderer draws no type), and
|
||||
// its colour bitmap glyphs as images (their pixels added to `images`), its COLR glyphs as their
|
||||
// layers.
|
||||
// A run's glyphs, laid out, with its ruby's, as one path with the run's fill (the renderer
|
||||
// draws no type), and its colour bitmap glyphs as images (their pixels added to `images`), its
|
||||
// COLR glyphs as their layers.
|
||||
fn outlined(t: &vectorcraft_doc::TextObject, images: &mut std::collections::BTreeMap<String, ImageBlob>) -> Node {
|
||||
let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
|
||||
let mut nodes: Vec<Arc<Node>> = t
|
||||
@@ -110,7 +110,7 @@ fn as_printed(doc: &Document, o: &LegacyAiOptions) -> Document {
|
||||
.enumerate()
|
||||
.map(|(i, run)| {
|
||||
let mut bp = kurbo::BezPath::new();
|
||||
for g in layout.glyphs.iter().filter(|g| g.run == i) {
|
||||
for g in layout.glyphs.iter().chain(&layout.ruby).filter(|g| g.run == i) {
|
||||
bp.extend(g.outline.iter());
|
||||
}
|
||||
let path = PathData::from_bezpath(&bp).transformed(t.xf);
|
||||
@@ -1114,6 +1114,36 @@ fn type_8_bit_text_cant_hold_and_type_asked_for_as_outlines_are_outlines() {
|
||||
assert!(!out.warnings.iter().any(|w| w == TYPE), "{:?}", out.warnings);
|
||||
}
|
||||
|
||||
/// Ruby (furigana), which a text object can't hold, makes type 8-bit text could hold outlines,
|
||||
/// with the reading's glyphs among them, where the canvas draws them (the file prints as the
|
||||
/// document draws).
|
||||
#[test]
|
||||
fn type_with_ruby_is_outlines_with_the_reading() {
|
||||
use vectorcraft_doc::{RubySpan, TextObject};
|
||||
let typed = |ruby: bool| {
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let layer = doc.layers[0].id;
|
||||
let mut t = TextObject::point(vectorcraft_geom::Point::new(10.0, 60.0), "AB", style(30.0, red()));
|
||||
if ruby {
|
||||
t.ruby = vec![RubySpan::new(0, 2, "xyz")];
|
||||
}
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
|
||||
let reading = lay.ruby.iter().flat_map(|g| g.outline.elements()).filter(|e| matches!(e, kurbo::PathEl::MoveTo(_))).count();
|
||||
let n = Node::new(doc.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
(doc, reading)
|
||||
};
|
||||
let moves = |text: &str| text.lines().filter(|l| l.ends_with(" m")).count();
|
||||
let (doc, reading) = typed(true);
|
||||
assert!(reading >= 3, "the reading has ink: {reading}");
|
||||
let out = write(&doc);
|
||||
assert!(out.warnings.iter().any(|w| w == TYPE), "{:?}", out.warnings);
|
||||
let with_ruby = String::from_utf8(out.bytes).unwrap();
|
||||
assert!(!with_ruby.contains(" To\n"), "not a text object");
|
||||
let plain = String::from_utf8(write_with(&typed(false).0, LegacyAiOptions { outline_text: true, ..LegacyAiOptions::default() }).bytes).unwrap();
|
||||
assert_eq!(moves(&with_ruby), moves(&plain) + reading, "the reading's contours are written");
|
||||
}
|
||||
|
||||
/// A layer `Art` holding a group of a shown square and a hidden one; a hidden layer `Off` holding
|
||||
/// a hidden square.
|
||||
fn with_hidden() -> Document {
|
||||
|
||||
@@ -264,6 +264,29 @@ fn type_is_written_as_outlines() {
|
||||
assert!(out.warnings.iter().any(|w| w.contains("type is written as outlines")), "{:?}", out.warnings);
|
||||
}
|
||||
|
||||
/// Ruby (furigana) is written with its base text, as the canvas draws it: one more `MoveToEx`
|
||||
/// per contour of the reading's glyphs in EMF, more polygons in WMF.
|
||||
#[test]
|
||||
fn ruby_is_written_with_its_type() {
|
||||
let typed = |ruby: bool| {
|
||||
let mut t = TextObject::point(Point::new(10.0, 60.0), "AB", CharStyle { size: 30.0, fill: red(), ..CharStyle::default() });
|
||||
if ruby {
|
||||
t.ruby = vec![vectorcraft_doc::RubySpan::new(0, 2, "xyz")];
|
||||
}
|
||||
t
|
||||
};
|
||||
let contours = |gs: &[vectorcraft_text::PositionedGlyph]| {
|
||||
gs.iter().flat_map(|g| g.outline.elements()).filter(|e| matches!(e, kurbo::PathEl::MoveTo(_))).count()
|
||||
};
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &typed(true));
|
||||
let reading = contours(&lay.ruby);
|
||||
assert!(reading >= 3, "the reading has ink: {reading}");
|
||||
let doc = |ruby: bool| doc_with(100.0, 100.0, vec![Node::new(NodeId(0), NodeKind::Text(Box::new(typed(ruby))))]);
|
||||
let moves = |d: &Document| count(&kinds(&emf_records(&emf(d).bytes)), crate::emf::MOVETOEX);
|
||||
assert_eq!(moves(&doc(true)), moves(&doc(false)) + reading, "the reading's contours are written");
|
||||
assert!(wmf(&doc(true)).bytes.len() > wmf(&doc(false)).bytes.len(), "WMF writes the reading too");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wmf_placeable_header_checksum_and_record_sizes() {
|
||||
let d = doc_with(72.0, 144.0, vec![path(shapes::ellipse(Rect::new(10.0, 10.0, 60.0, 40.0)), red(), blue(), 1.0)]);
|
||||
|
||||
File diff suppressed because one or more lines are too long
+4
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user