mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 20:01:37 +00:00
Merge pull request #5186 from dotCooCoo/fix/legacy-ai-character-rotation
Legacy .ai: type with rotated characters is written as outlines that keep the rotation
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
//! layer's hidden objects go to a hidden layer of their own just above it, with `omit` they are
|
||||
//! left out, with `visible` they show where they are. Type is written as text the format's reader
|
||||
//! lays out again, in the fonts it names; `outlineText` writes its outlines instead (as type that
|
||||
//! 8-bit text can't hold always is).
|
||||
//! 8-bit text can't hold and type with rotated characters always are).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -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, ruby, 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) and type with rotated characters are always outlines",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
//! 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, ruby, inline graphics, gradient or pattern fills) is written as its
|
||||
//! outlines, and all type with [`LegacyAiOptions::outline_text`]. Layer, spot color and gradient
|
||||
//! outlines, and so is type with rotated characters (a text object has no character rotation), 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`).
|
||||
//!
|
||||
@@ -76,9 +77,9 @@ mod encoding;
|
||||
mod prolog;
|
||||
mod writer;
|
||||
|
||||
#[cfg(test)]
|
||||
use encoding::writable;
|
||||
use encoding::{gradient_def, latin_string};
|
||||
#[cfg(test)]
|
||||
use encoding::{rotated, writable};
|
||||
pub(crate) use prolog::PROCSET;
|
||||
use prolog::{PROLOG, PS_HEADER, fonts};
|
||||
use writer::Writer;
|
||||
@@ -94,7 +95,8 @@ 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, 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 and numbered lists) are left out";
|
||||
pub const ROTATED_TYPE: &str = "type with rotated characters is written as outlines, which keep the rotation, and is no longer editable text (legacy .ai text can't rotate characters)";
|
||||
pub const TYPE_ATTRIBUTES: &str = "type attributes legacy .ai files don't have (underline, strikethrough, small caps, superscript and subscript, OpenType features, Variable Font settings, area type options, type on a path effects, space after paragraphs, tabs, bulleted and numbered 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)";
|
||||
pub const HIDDEN_SHOWN: &str = "hidden objects are written visible (legacy .ai files can hide only layers)";
|
||||
|
||||
@@ -80,10 +80,25 @@ pub(super) fn latin_string(name: &str) -> String {
|
||||
ps::bytes(&bytes)
|
||||
}
|
||||
|
||||
/// Can type `t` be written as text: horizontal, without ruby (which a text object can't hold), its
|
||||
/// Can type `t` be written as text: 8-bit text can hold it ([`holds_as_text`]) and no character
|
||||
/// is rotated ([`rotated`])?
|
||||
pub(super) fn writable(t: &TextObject) -> bool {
|
||||
holds_as_text(t) && !rotated(t)
|
||||
}
|
||||
|
||||
/// Has type `t` visible characters with Character Rotation? A legacy `.ai` text object's character
|
||||
/// attributes have none that turns a character (§15), so such type is written as its outlines.
|
||||
pub(super) fn rotated(t: &TextObject) -> bool {
|
||||
t.runs.iter().any(|r| {
|
||||
let turn = r.style.rotation;
|
||||
turn.is_finite() && turn.rem_euclid(360.0) != 0.0 && r.text.chars().any(|c| !c.is_whitespace())
|
||||
})
|
||||
}
|
||||
|
||||
/// Can 8-bit text hold type `t`: 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?
|
||||
pub(super) fn writable(t: &TextObject) -> bool {
|
||||
pub(super) fn holds_as_text(t: &TextObject) -> bool {
|
||||
let solid = |p: &Paint| matches!(p, Paint::Solid { .. } | Paint::None);
|
||||
!t.vertical
|
||||
&& t.ruby.is_empty()
|
||||
@@ -103,7 +118,6 @@ pub(super) fn lost_attributes(t: &TextObject) -> bool {
|
||||
|| s.strikethrough
|
||||
|| s.small_caps.is_some()
|
||||
|| s.position != CharPosition::Normal
|
||||
|| s.rotation != 0.0
|
||||
|| !s.features.is_empty()
|
||||
|| !s.font_axes.is_empty()
|
||||
};
|
||||
|
||||
@@ -1150,6 +1150,72 @@ 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);
|
||||
}
|
||||
|
||||
/// A legacy `.ai` text object has no character rotation: point type and area type with a rotated
|
||||
/// character between ordinary ones (turned either way, shifted off the baseline and scaled) are
|
||||
/// written as their outlines, with the warning, and read back as paths where the laid-out glyphs
|
||||
/// are, the rotated character turned. A full turn writes text.
|
||||
#[test]
|
||||
fn type_with_rotated_characters_is_outlines_that_keep_the_rotation() {
|
||||
use vectorcraft_doc::{CharStyle, TextKind, TextObject, TextRun};
|
||||
let typed = |turn: f64, area: bool| {
|
||||
let mut t = TextObject::point(vectorcraft_geom::Point::new(20.0, 80.0), "HE", style(30.0, red()));
|
||||
let turned = CharStyle { rotation: turn, baseline_shift: 4.0, h_scale: 120.0, v_scale: 80.0, ..style(30.0, red()) };
|
||||
t.runs.push(TextRun::new("L", turned));
|
||||
t.runs.push(TextRun::new("TH", style(30.0, red())));
|
||||
if area {
|
||||
t.kind = TextKind::Area { frame: shapes::rectangle(Rect::new(0.0, 0.0, 200.0, 100.0)) };
|
||||
t.xf = Affine::translate((20.0, 40.0));
|
||||
}
|
||||
t
|
||||
};
|
||||
// Each run's laid-out glyphs' box, in the document.
|
||||
let boxes = |t: &TextObject| -> Vec<kurbo::Rect> {
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
|
||||
(0..t.runs.len())
|
||||
.map(|i| {
|
||||
let mut bp = kurbo::BezPath::new();
|
||||
for g in lay.glyphs.iter().filter(|g| g.run == i) {
|
||||
bp.extend(g.outline.iter());
|
||||
}
|
||||
bp.apply_affine(t.xf);
|
||||
bp.bounding_box()
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let near = |a: kurbo::Rect, b: kurbo::Rect| (a.x0 - b.x0).abs().max((a.y0 - b.y0).abs()).max((a.x1 - b.x1).abs()).max((a.y1 - b.y1).abs());
|
||||
for (turn, area) in [(30.0, false), (-45.0, true)] {
|
||||
let t = typed(turn, area);
|
||||
assert!(rotated(&t));
|
||||
let want = boxes(&t);
|
||||
let unturned = boxes(&typed(0.0, area));
|
||||
assert!(near(want[1], unturned[1]) > 1.0, "the turn moves the glyph: {:?} {:?}", want[1], unturned[1]);
|
||||
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 out = write(&doc);
|
||||
assert!(out.warnings.iter().any(|w| w == ROTATED_TYPE), "{:?}", out.warnings);
|
||||
assert!(!out.warnings.iter().any(|w| w == TYPE || w == TYPE_ATTRIBUTES), "{:?}", out.warnings);
|
||||
assert!(!String::from_utf8_lossy(&out.bytes).contains(" To\n"), "not a text object");
|
||||
let back = crate::import(&out.bytes).unwrap().document;
|
||||
assert!(texts(&back).is_empty(), "no type reads back");
|
||||
let parts = children(&children(&back.layers[0])[0]);
|
||||
assert_eq!(parts.len(), 3, "one compound path for each run");
|
||||
for (i, part) in parts.iter().enumerate() {
|
||||
let b = part.visual_bounds().unwrap();
|
||||
assert!(near(b, want[i]) < 0.05, "run {i} at {turn}°: {b:?}, laid out at {:?}", want[i]);
|
||||
}
|
||||
}
|
||||
// A full turn draws the character upright: text.
|
||||
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(typed(360.0, false))));
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
let out = write(&doc);
|
||||
assert!(String::from_utf8_lossy(&out.bytes).contains(" To\n"), "a text object");
|
||||
assert!(!out.warnings.iter().any(|w| w == ROTATED_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).
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use super::encoding::{
|
||||
Ink, average, direct_fill, direct_stroke, finite, font_of, invertible, latin_string, lost_attributes, segment_point, unit, writable,
|
||||
Ink, average, direct_fill, direct_stroke, finite, font_of, holds_as_text, invertible, latin_string, lost_attributes, rotated, segment_point,
|
||||
unit, writable,
|
||||
};
|
||||
use super::{
|
||||
Affine, AnchorKind, AppearanceItem, Arc, BezPath, BlendMode, CharStyle, Cms, Color, Document, ELLIPTICAL, FREEFORM, FillLayer, FillRule,
|
||||
Gradient, GradientKind, GradientPaint, HEX_LINE, HIDDEN, HIDDEN_LAYER, HIDDEN_SHOWN, HiddenObjects, IMAGE_ALPHA, ImageBlob, Justify, LARGE_IMAGE,
|
||||
LAYER_COLORS, LAYER_NAME, LIVE, LayerColor, LegacyAiOptions, LineCap, LineJoin, MAX_IMAGE_PIXELS, MAX_NEST, NAMES, Node, NodeKind, PATTERNS,
|
||||
Paint, ParaDirection, ParaStyle, PathData, Point, Rect, STROKES, SUBLAYERS, Shape, StrokeLayer, SubPath, TITLE_AND_COLOR_NAMES, TRANSPARENCY,
|
||||
TYPE, TYPE_ATTRIBUTES, TextKind, TextObject, UNDECODED, Vec2, Write, latin, ps, stroke,
|
||||
Paint, ParaDirection, ParaStyle, PathData, Point, ROTATED_TYPE, Rect, STROKES, SUBLAYERS, Shape, StrokeLayer, SubPath, TITLE_AND_COLOR_NAMES,
|
||||
TRANSPARENCY, TYPE, TYPE_ATTRIBUTES, TextKind, TextObject, UNDECODED, Vec2, Write, latin, ps, stroke,
|
||||
};
|
||||
|
||||
pub(super) struct Writer<'a> {
|
||||
@@ -719,7 +720,8 @@ impl<'a> Writer<'a> {
|
||||
|
||||
// ---------- type ----------
|
||||
|
||||
/// Type: a text object when the format can hold it as text, else its outlines. A run whose
|
||||
/// 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`]).
|
||||
fn text(&mut self, t: &TextObject) {
|
||||
@@ -732,7 +734,12 @@ impl<'a> Writer<'a> {
|
||||
if writable(t) {
|
||||
return self.text_object(t);
|
||||
}
|
||||
self.warn(TYPE);
|
||||
if !holds_as_text(t) {
|
||||
self.warn(TYPE);
|
||||
}
|
||||
if rotated(t) {
|
||||
self.warn(ROTATED_TYPE);
|
||||
}
|
||||
}
|
||||
self.outlines(t);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,9 @@ private editing data. VectorCraft:
|
||||
`transparentImages: "flatten"|"omit"`). Type is written as editable text objects (point type, area type and type
|
||||
on a path, with their fonts' PostScript names, sizes, colours, alignment, indents, leading, tracking and
|
||||
scaling), which the reading app sets in those fonts; characters outside Windows-1252, vertical type, ruby, inline
|
||||
graphics and gradient or pattern fills make that type outlines, and Text: Outlines (`outlineText: true`) writes
|
||||
graphics and gradient or pattern fills make that type outlines, and so do rotated characters (Character Rotation,
|
||||
which a version 7 text object can't hold: the outlines keep the rotation, and the type is no longer editable text
|
||||
in the file), and Text: Outlines (`outlineText: true`) writes
|
||||
all type as outlines. The format can hide only layers: hidden objects go to a hidden layer of their own above
|
||||
their layer, are omitted, or are made visible (Hidden Objects, `hiddenObjects: "layer"|"omit"|"visible"`).
|
||||
Sublayers become groups; appearances, effects and brushes are expanded; object names are left out; layer
|
||||
|
||||
+4
-1
@@ -843,7 +843,10 @@ PostScript form, which keeps layers, groups, compound paths and clipping groups
|
||||
of `range` (default all; `{name}-{artboard}.ai` when several, listed in the result's `files`), with
|
||||
`transparentImages: "flatten"` (default) or `"omit"` for images with see-through pixels, opacity or a blending mode,
|
||||
`hiddenObjects: "layer"` (default: a hidden layer of each layer's hidden objects), `"omit"` or `"visible"`, and
|
||||
`outlineText: true` to write type as outlines instead of text objects. An image that can't be decoded is left out of
|
||||
`outlineText: true` to write type as outlines instead of text objects. Type with rotated characters (Character
|
||||
Rotation) is always written as outlines, which keep the rotation; without `outlineText: true` this gives the warning "type with rotated characters is
|
||||
written as outlines, which keep the rotation, and is no longer editable text (legacy .ai text can't rotate
|
||||
characters)". An image that can't be decoded is left out of
|
||||
the file and does not show in art that is flattened or whose raster effects are expanded; `warnings` carries "an image
|
||||
couldn't be decoded and is missing from the picture" once for all such images. From the CLI: `vectorcraft-cli convert in.svg
|
||||
out.ai --ai-version 7 [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] [--outline-text]
|
||||
|
||||
@@ -19,6 +19,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-02 | minor | Legacy `.ai` saves (version 7) write type with rotated characters as outlines that keep each character's rotation, with a warning unless `outlineText` is on (#5186); scores unchanged. |
|
||||
| 2026-10-31 | minor | Object › Path › Smooth smooths a path along its length before refitting it, so Auto-Smooth takes a shaky line to one smooth curve instead of leaving its jags, and with anchor points direct-selected it smooths only that section; scores unchanged. |
|
||||
| 2026-10-31 | minor | New Document: More Settings has the Save Preset button beside the name too, saving its settings (artboard layout included) as a preset (#4893); scores unchanged. |
|
||||
| 2026-10-31 | minor | Object › Rasterize in a CMYK document with its CMYK colour model makes a CMYK image of the art's ink amounts (as CMYK exports draw them), so the Properties panel says CMYK and CMYK exports keep its inks; a transparent background becomes the image's opacity mask (#4882); scores unchanged. |
|
||||
@@ -1064,6 +1065,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
A `.ai` or EPS opened from its editing data whose text document makes the type of every text object that shows takes all its type from the text document: each text object stays in its own group, frame and layer, the page's type isn't matched with them, a file of more than 5,000 text objects still opens with its layers, and type the page draws differently (in another font) keeps the layers, with a warning. A line break inside a paragraph stays a line break in it (`crates/eps/src/import/tests_native.rs`, `crates/eps/src/import/ate.rs`).
|
||||
In SVG export without Fewer `<tspan>` Elements, each character of live type that justification or Mojikumi moves (the letter gaps of a Justify All line without word spaces, the gaps between the characters of a justified Japanese line, closed-up punctuation, the space between Japanese and Latin letters) starts its own positioned `<tspan>`, and type scaled horizontally on a justified line is stretched to its own width (`crates/svg/tests/it/text_layout.rs`).
|
||||
SVG export (live text), legacy `.ai` saves (text objects), DXF export (`TEXT` entities) and the CSS Properties panel write area type that Shrink Text to Fit scales down at the size it draws at, and scale its tracking, kerning, leading and baseline shifts the same way where the format holds them.
|
||||
Legacy `.ai` saves (version 7) write type with rotated characters (Character Rotation, Touch Type turns) as its outlines, with a warning that it is no longer editable text (unless `outlineText` already asks for every type object as outlines): a version 7 text object has no character rotation ([Adobe Illustrator File Format Specification 7.0](https://www.nationalarchives.gov.uk/PRONOM/fmt/423), §15), and the outlines keep each character's rotation, baseline shift and scale (`legacy_ai::tests::type_with_rotated_characters_is_outlines_that_keep_the_rotation`).
|
||||
A PDF saved with Illustrator's editing data opens through it as a PDF-compatible `.ai` does, with its layers and groups and with art reaching past its page unclipped; such a PDF holds the data in `AIPDFPrivateData` streams (part of #868). A shape a PDF or `.ai` paints with a gradient (a clip path round a shading) opens as one path filled with the gradient, not a clip group round a rectangle, so Shape Builder, Live Paint and Pathfinder work on its outline (#1304).
|
||||
In a `.ai` opened from its editing data, a text object on a layer that shows and doesn't print stays on that layer, made from the file's text document, even where the page doesn't draw it (`crates/eps/src/import/tests_native.rs`). Such a `.ai` opens without the PDF part's warnings that text was kept as outlines when no outlined glyphs come in, and a warning counts the text objects that hold the page's type next to glyphs the page has only as outlines, which don't come in (`crates/engine/src/cmd/fileio/tests_aiimport.rs`). Where the art of a `.ai` or EPS opened from its editing data differs a little from the file's page, the warning gives the share of the page that differs away from type. When that share is 0.2% or less, the warning names the type and gives the share of the page that differs where the type is; when all the type is made from the text document, the type's share has a warning of its own beside the art's (`crates/eps/src/import/tests_native.rs`).
|
||||
PDF text opens with every glyph where the file draws it (#722): the font's kerning or none, whichever the file followed, word and character spacing, `TJ` adjustments and horizontal scaling kept per character as tracking, text rise as baseline shift within its line, vertical glyphs narrower than an em centred on their column (`/W2`, `/W`, `/DW`), justified text boxes with word spacing back as justified area type (`tests_text_placement`, fixtures written from ISO 32000-1 §9).
|
||||
|
||||
Reference in New Issue
Block a user