mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 23:05:05 +00:00
Merge pull request #6906 from ruchirguitar/fix/pdf-indic-text-5235
PDF import: Indic type that would not draw as the file does keeps the file's glyphs (#5235)
This commit is contained in:
@@ -35,6 +35,7 @@ mod builder;
|
||||
mod cid;
|
||||
mod device;
|
||||
mod entry;
|
||||
mod reshaped;
|
||||
mod state;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -42,8 +43,8 @@ pub(crate) use cid::{CidText, font_chars, identify_glyph, unify_radical};
|
||||
#[cfg(any(test, feature = "read-count"))]
|
||||
pub use entry::imports_of;
|
||||
pub use entry::{
|
||||
INVISIBLE_TEXT_NOTE, OFF_ARTBOARD_NOTE, OUTLINED_TEXT_NOTE, PAST_PAGE_NOTE, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE, holds_outlines,
|
||||
import, import_opened, import_with_report, is_glyph_outlines, is_outlines_note,
|
||||
INVISIBLE_TEXT_NOTE, OFF_ARTBOARD_NOTE, OUTLINED_TEXT_NOTE, PAST_PAGE_NOTE, RESHAPED_TEXT_NOTE, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE,
|
||||
holds_outlines, import, import_opened, import_with_report, is_glyph_outlines, is_outlines_note,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use entry::{TEST_MAX_OBJECTS, import_with_reader_options, read};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::cid::{CidText, FontInfo, comparable, font_chars, identify_glyph, matching_face, replacement, unify_radical};
|
||||
use super::entry::{
|
||||
MAX_NESTED, OVERPRINT_UNREAD, OVERPRINT_ZERO_MODE, PATTERN_REUSE, TEXT_OUTLINES, TOO_MANY_OBJECTS, UNMATCHED_GLYPHS_NOTE,
|
||||
MAX_NESTED, OVERPRINT_UNREAD, OVERPRINT_ZERO_MODE, PATTERN_REUSE, RESHAPED_TEXT_NOTE, TEXT_OUTLINES, TOO_MANY_OBJECTS, UNMATCHED_GLYPHS_NOTE,
|
||||
UNNAMED_CHARACTERS_NOTE, max_objects,
|
||||
};
|
||||
use super::state::{Builder, Frame, FrameKind, Saved, bounds, grouped, line_width, mean_scale, paints, round3, stroke_layer};
|
||||
use super::reshaped;
|
||||
use super::state::{Builder, Frame, FrameKind, GlyphRun, Saved, bounds, grouped, line_width, mean_scale, paints, round3, stroke_layer};
|
||||
use super::{
|
||||
Affine, Appearance, AppearanceItem, Arc, BezPath, BfString, BlendMode, CacheKey, Color, Colors, Families, FillLayer, FillRule, FunctionBudget,
|
||||
FunctionPaint, HashMap, HashSet, ImageBlob, ImageObject, Knockout, Look, MaskSpec, MaskType, Native, Node, NodeId, NodeKind, OpacityMask, Paint,
|
||||
@@ -22,6 +23,7 @@ impl<'p> Builder<'p> {
|
||||
page: Rect::ZERO,
|
||||
glyphs: None,
|
||||
text: None,
|
||||
text_glyphs: vec![],
|
||||
text_as,
|
||||
actual: Default::default(),
|
||||
images: HashMap::new(),
|
||||
@@ -101,6 +103,7 @@ impl<'p> Builder<'p> {
|
||||
self.blend = BlendMode::Normal;
|
||||
self.glyphs = None;
|
||||
self.text = None;
|
||||
self.text_glyphs.clear();
|
||||
self.mask = None;
|
||||
self.masked.clear();
|
||||
self.marked.clear();
|
||||
@@ -376,18 +379,56 @@ impl<'p> Builder<'p> {
|
||||
}
|
||||
|
||||
pub(super) fn flush_text(&mut self) {
|
||||
let Some(mut line) = self.text.take() else { return };
|
||||
if self.end_text() {
|
||||
self.flush_glyphs();
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish the open line of type. `true`: it was kept as outlines instead ([`reshaped`]), which
|
||||
/// are left open as the glyph run for an outline drawn next to join.
|
||||
pub(super) fn end_text(&mut self) -> bool {
|
||||
let glyphs = std::mem::take(&mut self.text_glyphs);
|
||||
let Some(mut line) = self.text.take() else { return false };
|
||||
let held = std::mem::take(&mut line.held);
|
||||
// Type the file names itself (ActualText) is in reading order as it stands.
|
||||
let checked = !glyphs.is_empty() && line.mapped();
|
||||
let mut outlined = false;
|
||||
if let Some((t, opacity, facts)) = line.finish() {
|
||||
let mut n = Node::new(self.id(), NodeKind::Text(Box::new(t)));
|
||||
n.opacity = opacity;
|
||||
self.lines.insert(n.id, facts);
|
||||
self.push_node(n);
|
||||
if checked && reshaped::reshapes(&t.plain_text()) && !reshaped::draws_as(&t, &facts.ink) {
|
||||
outlined = true;
|
||||
for g in glyphs {
|
||||
let same = self.glyphs.as_ref().is_some_and(|r| {
|
||||
r.paint == g.paint
|
||||
&& r.opacity == g.opacity
|
||||
&& r.overprint == g.overprint
|
||||
&& r.stroke.is_some() == g.stroke.is_some()
|
||||
&& r.stroke.as_ref().map(|s| s.line_width) == g.stroke.as_ref().map(|s| s.line_width)
|
||||
&& (r.scale - g.scale).abs() < 1e-9
|
||||
});
|
||||
match &mut self.glyphs {
|
||||
Some(r) if same => r.path.extend(g.path.iter()),
|
||||
_ => {
|
||||
self.flush_glyphs();
|
||||
self.glyphs = Some(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.warn(RESHAPED_TEXT_NOTE);
|
||||
} else {
|
||||
let mut n = Node::new(self.id(), NodeKind::Text(Box::new(t)));
|
||||
n.opacity = opacity;
|
||||
self.lines.insert(n.id, facts);
|
||||
self.push_node(n);
|
||||
}
|
||||
}
|
||||
if outlined && !held.is_empty() {
|
||||
self.flush_glyphs();
|
||||
}
|
||||
// The strokes of its glyphs' outlines, right after it ([`import_lines`] makes them its stroke).
|
||||
for n in held {
|
||||
self.push_node(n);
|
||||
}
|
||||
outlined
|
||||
}
|
||||
|
||||
/// A stroke of the outline of the glyph just drawn, in the glyph's own paint, while every glyph
|
||||
@@ -737,6 +778,27 @@ impl<'p> Builder<'p> {
|
||||
n
|
||||
}
|
||||
|
||||
/// Glyph `o` of the open line of type, placed by `m` and drawn with `drawn` (its paint, and its
|
||||
/// stroke when it is stroked), as the outline it is kept as if the line is
|
||||
/// ([`Self::end_text`]). Nothing for a glyph that shows nothing.
|
||||
fn keep_glyph(
|
||||
&mut self,
|
||||
o: &craft_pdf::interpret::font::OutlineGlyph,
|
||||
m: Affine,
|
||||
drawn: Option<(Paint, Option<StrokeProps>)>,
|
||||
opacity: f32,
|
||||
scale: f64,
|
||||
overprint: bool,
|
||||
) {
|
||||
let Some((paint, stroke)) = drawn else { return };
|
||||
let mut path = o.outline();
|
||||
if path.elements().is_empty() {
|
||||
return;
|
||||
}
|
||||
path.apply_affine(m);
|
||||
self.text_glyphs.push(GlyphRun { path, paint, opacity, stroke, scale, overprint });
|
||||
}
|
||||
|
||||
/// Glyph `o` as type: added to the line being gathered (or starting one). `false`: it keeps
|
||||
/// its outline (no Unicode text, mirrored, or not type mode). No `paint`: invisible text,
|
||||
/// type with neither fill nor stroke.
|
||||
@@ -794,8 +856,11 @@ impl<'p> Builder<'p> {
|
||||
return false;
|
||||
}
|
||||
let width = o.advance_width().filter(|w| w.is_finite());
|
||||
// Set vertically (WMode 1): no horizontal advance.
|
||||
let upright = width.is_some_and(|w| w.abs() <= 1.0);
|
||||
// Set vertically (WMode 1): no horizontal advance. Not in a font the file says is written
|
||||
// horizontally: there a glyph without an advance is a mark over the letter before it (a
|
||||
// vowel sign, an anusvara), which stays on its line (#5235).
|
||||
let horizontal = self.fonts.get(&key).is_some_and(|f| f.vertical.is_none());
|
||||
let upright = !horizontal && width.is_some_and(|w| w.abs() <= 1.0);
|
||||
let mut info = self.font_name(key, o);
|
||||
// A font the file doesn't carry is drawn with a stand-in: its glyphs can't tell which
|
||||
// style of the installed family it is, so the name decides.
|
||||
@@ -841,6 +906,8 @@ impl<'p> Builder<'p> {
|
||||
}
|
||||
None => (None, 1.0),
|
||||
};
|
||||
// What the glyph is drawn with as an outline, should its line be kept so.
|
||||
let drawn = paint.clone().map(|p| (p, stroke.cloned()));
|
||||
let stroke = stroke.zip(paint.clone()).map(|(p, paint)| (paint, line_width(p, scale)));
|
||||
let look = Look {
|
||||
font: key,
|
||||
@@ -879,6 +946,7 @@ impl<'p> Builder<'p> {
|
||||
&& look.fill.is_none()
|
||||
&& line.stroke_last(key, at, s.clone(), overprint)
|
||||
{
|
||||
self.keep_glyph(o, m, drawn, opacity, scale, overprint);
|
||||
return true;
|
||||
}
|
||||
if !self.text.as_mut().is_some_and(place) {
|
||||
@@ -889,6 +957,9 @@ impl<'p> Builder<'p> {
|
||||
}
|
||||
// Its ink: a stroke the file draws as outlines of the glyphs is told by it.
|
||||
let mut ink = o.outline();
|
||||
if top.is_none() {
|
||||
self.keep_glyph(o, m, drawn, opacity, scale, overprint);
|
||||
}
|
||||
if let Some(line) = &mut self.text
|
||||
&& !ink.elements().is_empty()
|
||||
{
|
||||
|
||||
@@ -191,7 +191,8 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||
if self.type_glyph(o, transform * glyph_transform, scale, Some(paint), stroke.as_ref(), overprint) {
|
||||
return;
|
||||
}
|
||||
self.flush_text();
|
||||
// A line kept as outlines leaves them open: this glyph joins them.
|
||||
self.end_text();
|
||||
let mut bp = o.outline();
|
||||
bp.apply_affine(transform * glyph_transform);
|
||||
let (paint, opacity) = self.paint(paint, stroke.is_some(), self.walked(true));
|
||||
|
||||
@@ -171,14 +171,22 @@ pub const INVISIBLE_TEXT_NOTE: &str = "invisible text (such as a scanned page's
|
||||
/// Warning: glyphs that differ from the installed font of their name were kept as outlines.
|
||||
pub const UNMATCHED_GLYPHS_NOTE: &str = "text whose glyphs differ from the installed font of the same name was kept as outlines";
|
||||
|
||||
/// Warning: type in a script whose letters reorder and join (Indic scripts) that, set again from
|
||||
/// its characters, wouldn't draw the glyphs where the file drew them was kept as outlines (#5235).
|
||||
pub const RESHAPED_TEXT_NOTE: &str =
|
||||
"text that would not look as it does in the file if set as type (a script whose letters join) was kept as outlines";
|
||||
|
||||
/// The end of the warning that glyphs whose characters the file doesn't name were kept as
|
||||
/// outlines, after "text in" and the font's name.
|
||||
pub const UNNAMED_CHARACTERS_NOTE: &str = "whose characters the file doesn't name (U+FFFD) was kept as outlines";
|
||||
|
||||
/// Is `note` one of the warnings that glyphs were kept as outlines ([`OUTLINED_TEXT_NOTE`],
|
||||
/// [`UNMATCHED_GLYPHS_NOTE`], [`UNNAMED_CHARACTERS_NOTE`])?
|
||||
/// [`UNMATCHED_GLYPHS_NOTE`], [`RESHAPED_TEXT_NOTE`], [`UNNAMED_CHARACTERS_NOTE`])?
|
||||
pub fn is_outlines_note(note: &str) -> bool {
|
||||
note == OUTLINED_TEXT_NOTE || note == UNMATCHED_GLYPHS_NOTE || note.starts_with("text in ") && note.ends_with(UNNAMED_CHARACTERS_NOTE)
|
||||
note == OUTLINED_TEXT_NOTE
|
||||
|| note == UNMATCHED_GLYPHS_NOTE
|
||||
|| note == RESHAPED_TEXT_NOTE
|
||||
|| note.starts_with("text in ") && note.ends_with(UNNAMED_CHARACTERS_NOTE)
|
||||
}
|
||||
|
||||
/// Is `n` glyphs kept as outlines (a path the import names `<Text Outlines>`)?
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Type in a script whose letters reorder and join as they are set (#5235). A PDF draws such text
|
||||
//! as glyphs already shaped: a vowel sign before the consonant it follows in reading order, a
|
||||
//! conjunct as one glyph that the file may not name, a mark placed over its base. Read glyph by
|
||||
//! glyph, the characters don't always make the text that was set, and type made from them draws
|
||||
//! other glyphs in other places. A line of such type is therefore set again and compared with what
|
||||
//! the file drew; one that would look different keeps the file's glyphs as outlines.
|
||||
|
||||
use kurbo::{Rect, Shape};
|
||||
use vectorcraft_doc::TextObject;
|
||||
|
||||
/// Does `text` have a character of a script whose letters reorder and join into conjuncts: the
|
||||
/// Brahmic scripts of U+0900 to U+0DFF (Devanagari, Bengali, Gurmukhi, Gujarati, Oriya, Tamil,
|
||||
/// Telugu, Kannada, Malayalam, Sinhala)?
|
||||
pub(super) fn reshapes(text: &str) -> bool {
|
||||
text.chars().any(|c| ('\u{0900}'..='\u{0DFF}').contains(&c))
|
||||
}
|
||||
|
||||
/// Would type `t` draw its glyphs where the file drew them (`ink`: each glyph's ink box in
|
||||
/// document space)? As many glyphs with ink, each within 3 % of the type's size of one of the
|
||||
/// file's.
|
||||
pub(super) fn draws_as(t: &TextObject, ink: &[Rect]) -> bool {
|
||||
let laid = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
|
||||
let mut set: Vec<Rect> =
|
||||
laid.glyphs.iter().filter(|g| !g.outline.elements().is_empty()).map(|g| (t.xf * g.outline.clone()).bounding_box()).collect();
|
||||
if set.len() != ink.len() {
|
||||
return false;
|
||||
}
|
||||
let size = t.runs.iter().map(|r| r.style.size).fold(0.0, f64::max);
|
||||
let tol = size * 0.03;
|
||||
if !tol.is_finite() || set.iter().chain(ink).any(|r| !(r.x0.is_finite() && r.y0.is_finite() && r.x1.is_finite() && r.y1.is_finite())) {
|
||||
return false;
|
||||
}
|
||||
set.sort_by(|a, b| a.x0.total_cmp(&b.x0));
|
||||
let mut used = vec![false; set.len()];
|
||||
// Each of the file's glyphs takes one the type draws in its place: looked for among those
|
||||
// whose left edge is as near.
|
||||
ink.iter().all(|r| {
|
||||
let from = set.partition_point(|s| s.x0 < r.x0 - tol);
|
||||
let found = set
|
||||
.iter()
|
||||
.zip(used.iter_mut())
|
||||
.skip(from)
|
||||
.take_while(|(s, _)| s.x0 <= r.x0 + tol)
|
||||
.find(|(s, used)| !**used && (s.y0 - r.y0).abs() <= tol && (s.x1 - r.x1).abs() <= tol && (s.y1 - r.y1).abs() <= tol);
|
||||
match found {
|
||||
Some((_, used)) => {
|
||||
*used = true;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -81,6 +81,10 @@ pub(super) struct Builder<'p> {
|
||||
pub(super) page: Rect,
|
||||
pub(super) glyphs: Option<GlyphRun>,
|
||||
pub(super) text: Option<TextLine>,
|
||||
/// The glyphs of the open line of type (`text`) that show, each as the outline it would be
|
||||
/// kept as: what the line becomes when its type wouldn't draw as the file does
|
||||
/// ([`super::reshaped`]).
|
||||
pub(super) text_glyphs: Vec<GlyphRun>,
|
||||
pub(super) text_as: TextAs,
|
||||
pub(super) actual: super::actual_text::ActualText,
|
||||
pub(super) images: HashMap<String, ImageBlob>,
|
||||
|
||||
@@ -269,6 +269,12 @@ impl TextLine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Horizontal type whose characters are all read glyph by glyph from the font's mappings (none
|
||||
/// from the file's own ActualText).
|
||||
pub fn mapped(&self) -> bool {
|
||||
self.upright.is_none() && !self.column && !self.authored.contains(&true)
|
||||
}
|
||||
|
||||
/// The paint the last glyph is filled with.
|
||||
pub fn last_fill(&self) -> Option<&Paint> {
|
||||
self.runs.last()?.0.fill.as_ref()
|
||||
|
||||
@@ -80,8 +80,8 @@ pub use file_info::{FileInfo, file_info};
|
||||
#[cfg(any(test, feature = "read-count"))]
|
||||
pub use import::imports_of;
|
||||
pub use import::{
|
||||
INVISIBLE_TEXT_NOTE, OFF_ARTBOARD_NOTE, OUTLINED_TEXT_NOTE, PAST_PAGE_NOTE, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE, holds_outlines,
|
||||
import, import_opened, import_with_report, is_glyph_outlines, is_outlines_note,
|
||||
INVISIBLE_TEXT_NOTE, OFF_ARTBOARD_NOTE, OUTLINED_TEXT_NOTE, PAST_PAGE_NOTE, RESHAPED_TEXT_NOTE, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE,
|
||||
holds_outlines, import, import_opened, import_with_report, is_glyph_outlines, is_outlines_note,
|
||||
};
|
||||
#[cfg(any(test, feature = "read-count"))]
|
||||
pub use pages::reads_of;
|
||||
|
||||
@@ -44,6 +44,7 @@ mod printadvanced;
|
||||
mod reader_compat;
|
||||
mod reader_streaming;
|
||||
mod realtext;
|
||||
mod reshaped;
|
||||
mod settings;
|
||||
mod spread;
|
||||
pub(super) mod stroke;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
//! #5235: type in a script whose letters reorder as they are set (here Devanagari) opens as type
|
||||
//! only when that type draws what the file drew; otherwise the file's glyphs stay, as outlines.
|
||||
//! The file is written from scratch from the PDF specification (ISO 32000-1 §9.7, composite
|
||||
//! fonts), its font program a test font built in code over Source Sans 3
|
||||
//! ([`vectorcraft_text::test_fonts::matra_font`]): Ka draws Source Sans 3's `a` and the vowel sign
|
||||
//! I, set before its consonant, draws its `i`.
|
||||
|
||||
use vectorcraft_doc::{Document, NodeKind};
|
||||
use vectorcraft_testkit::pdf::{PdfPage, first_extra, pdf_with};
|
||||
use vectorcraft_text::FontDb;
|
||||
use vectorcraft_text::test_fonts::{MATRA_FAMILY, matra_font};
|
||||
|
||||
use crate::tests_import_fidelity::stream;
|
||||
use crate::*;
|
||||
|
||||
const KA: char = 'क';
|
||||
const SIGN_I: char = '\u{093F}';
|
||||
|
||||
/// The test font's glyph of `c` (Source Sans 3's glyph of the Latin letter that draws it) and its
|
||||
/// advance in thousandths of an em.
|
||||
fn glyph(c: char) -> (u32, f64) {
|
||||
let face = FontDb::global().face(MATRA_FAMILY, "Regular").expect("the test font is added");
|
||||
let g = face.glyph_for(c);
|
||||
(g, (face.advance(g) * 1000.0 / face.units_per_em()).round())
|
||||
}
|
||||
|
||||
/// A 300 × 200 pt page showing the glyphs of `drawn`, in that order, in the test font at 40 pt
|
||||
/// from (20, 100): a Type 0 font (Identity-H, two-byte glyph ids) with the font program embedded
|
||||
/// and a ToUnicode map naming each glyph's character. `sign_width`: the advance the file gives
|
||||
/// the vowel sign, in thousandths of an em (`None`: the font's).
|
||||
fn page(drawn: &[char], sign_width: Option<f64>) -> Vec<u8> {
|
||||
FontDb::global().add_font(matra_font().expect("the test font builds"));
|
||||
let program = matra_font().unwrap();
|
||||
let hex: String = program.iter().map(|b| format!("{b:02X}")).collect::<String>() + ">";
|
||||
let n = first_extra(1);
|
||||
let rows: Vec<String> = [KA, SIGN_I].iter().map(|&c| format!("<{:04X}> <{:04X}>", glyph(c).0, c as u32)).collect();
|
||||
let width = |c| sign_width.filter(|_| c == SIGN_I).unwrap_or(glyph(c).1);
|
||||
let widths: Vec<String> = [KA, SIGN_I].iter().map(|&c| format!("{} [{}]", glyph(c).0, width(c))).collect();
|
||||
let objs = [
|
||||
stream(&format!("/Length1 {} /Filter /ASCIIHexDecode", program.len()), &hex),
|
||||
format!(
|
||||
"<< /Type /FontDescriptor /FontName /MatratestSans-Regular /Flags 4 /FontBBox [-500 -300 1500 1000] /ItalicAngle 0 /Ascent 1024 /Descent -400 /CapHeight 660 /StemV 80 /FontFile2 {n} 0 R >>"
|
||||
),
|
||||
format!(
|
||||
"<< /Type /Font /Subtype /CIDFontType2 /BaseFont /MatratestSans-Regular /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor {} 0 R /CIDToGIDMap /Identity /DW 1000 /W [{}] >>",
|
||||
n + 1,
|
||||
widths.join(" ")
|
||||
),
|
||||
stream(
|
||||
"",
|
||||
&format!(
|
||||
"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def /CMapName /Test-UCS def /CMapType 2 def 1 begincodespacerange <0000> <FFFF> endcodespacerange {} beginbfchar\n{}\nendbfchar endcmap CMapName currentdict /CMap defineresource pop end end",
|
||||
rows.len(),
|
||||
rows.join("\n")
|
||||
),
|
||||
),
|
||||
];
|
||||
let refs: Vec<&str> = objs.iter().map(String::as_str).collect();
|
||||
let resources = format!(
|
||||
"/Font << /F1 << /Type /Font /Subtype /Type0 /BaseFont /MatratestSans-Regular /Encoding /Identity-H /DescendantFonts [{} 0 R] /ToUnicode {} 0 R >> >>",
|
||||
n + 2,
|
||||
n + 3
|
||||
);
|
||||
let shown: String = drawn.iter().map(|&c| format!("{:04X}", glyph(c).0)).collect();
|
||||
let content = format!("BT /F1 40 Tf 20 100 Td <{shown}> Tj ET");
|
||||
pdf_with(&[PdfPage { resources, ..PdfPage::new(300.0, 200.0, &content) }], &refs, None)
|
||||
}
|
||||
|
||||
fn open(bytes: &[u8]) -> ImportReport {
|
||||
import_with_report(bytes, &ImportOptions { text_as: TextAs::Text, ..Default::default() }).unwrap()
|
||||
}
|
||||
|
||||
fn texts(d: &Document) -> Vec<String> {
|
||||
let mut v = vec![];
|
||||
d.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
v.push(t.plain_text());
|
||||
}
|
||||
});
|
||||
v
|
||||
}
|
||||
|
||||
fn outlines(d: &Document) -> usize {
|
||||
let mut n = 0;
|
||||
d.walk(|node| n += usize::from(is_glyph_outlines(node)));
|
||||
n
|
||||
}
|
||||
|
||||
/// The file draws ककि as it is set: Ka, then the vowel sign before the Ka it follows in reading
|
||||
/// order. Read glyph by glyph that is Ka, the sign, Ka, which reads किक: set as type, the sign
|
||||
/// would move in front of the first Ka. The file's three glyphs stay as they are drawn.
|
||||
#[test]
|
||||
fn a_syllable_drawn_out_of_reading_order_keeps_the_files_glyphs() {
|
||||
let report = open(&page(&[KA, SIGN_I, KA], None));
|
||||
assert_eq!(texts(&report.document), [""; 0], "no type that draws something else");
|
||||
assert_eq!(outlines(&report.document), 1, "the glyphs, as one path");
|
||||
assert!(report.warnings.iter().any(|w| w == RESHAPED_TEXT_NOTE), "{:?}", report.warnings);
|
||||
assert!(report.warnings.iter().all(|w| !is_outlines_note(w) || w == RESHAPED_TEXT_NOTE), "{:?}", report.warnings);
|
||||
}
|
||||
|
||||
/// Letters the file draws in reading order make type that draws the same: they stay type.
|
||||
#[test]
|
||||
fn letters_drawn_in_reading_order_stay_type() {
|
||||
let report = open(&page(&[KA, KA], None));
|
||||
assert_eq!(texts(&report.document), [format!("{KA}{KA}")]);
|
||||
assert_eq!(outlines(&report.document), 0);
|
||||
assert!(!report.warnings.iter().any(|w| is_outlines_note(w)), "{:?}", report.warnings);
|
||||
}
|
||||
|
||||
/// A mark the file gives no advance (as fonts do for the signs they set over a letter) belongs to
|
||||
/// the line of its letter in a font written horizontally: it is not a glyph of vertical type, set
|
||||
/// as an object of its own. Here the line it stays on then reads कि, whose sign type would set
|
||||
/// before the Ka, so the line keeps the file's glyphs, the mark with them.
|
||||
#[test]
|
||||
fn a_mark_without_an_advance_stays_with_its_line() {
|
||||
let report = open(&page(&[KA, SIGN_I], Some(0.0)));
|
||||
assert_eq!(texts(&report.document), [""; 0], "the mark is no type of its own");
|
||||
assert_eq!(outlines(&report.document), 1, "both glyphs, as one path");
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (newly imported SVG/SVGZ titles are filtered and bounded without changing restored titles, #3626; `.ai` and EPS import, and what every export and `.ai` save draw: type's strokes in an envelope paint at their own weight along its bent outlines, Distort Appearance on or off, #6644; `.ai` and EPS import: centred and right-aligned lines of tracked type are aligned without the tracking after their last character, as the file's page aligns them, #6610; `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art, #6570; `.ai` save and open: layer colour presets Yellow, Magenta and Cyan keep their colour, as the `Lb` colour index now follows the file's numbering, #6502; `.ai` save: stroked type keeps its stroke's cap, join and miter limit in the text document, #6472; `.ai` and EPS import, PDF import: tracking and manual kerning in horizontally scaled type take the Horizontal Scale, as the file's page spaces such type; `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke, #3179; `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid, #3179; `.ai` and EPS import: stroked type keeps its stroke's cap, join and miter limit from the text document, #6472; `.ai` save: type in a variable font's named instance names its design-space setting, #6388; `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page, #3179; `.ai` and EPS import: a note for type in an envelope, #3179; `.ai` and EPS import: the live envelope and warped type check sees white art; PDF import: DeviceN images of one ink or of process plates keep their inks, #6348; `.ai` import: images in a spot ink's Separation space keep their ink, #6350; PDF import: Separation images keep their ink, #6314; PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; PDF import: image masks open colorized by the color they are painted in, #6313; EPS and `.ai` editing data: a ligature the page names by a private-use glyph matches its text object; EPS import: CIDFonts only named, over a Unicode CMap, set as live type; EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-12 · **Change:** minor (PDF import: Indic type keeps its marks on their line, and a line whose type would not draw as the file does keeps the file's glyphs as outlines, #5235; previously: newly imported SVG/SVGZ titles are filtered and bounded without changing restored titles, #3626; `.ai` and EPS import, and what every export and `.ai` save draw: type's strokes in an envelope paint at their own weight along its bent outlines, Distort Appearance on or off, #6644; `.ai` and EPS import: centred and right-aligned lines of tracked type are aligned without the tracking after their last character, as the file's page aligns them, #6610; `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art, #6570; `.ai` save and open: layer colour presets Yellow, Magenta and Cyan keep their colour, as the `Lb` colour index now follows the file's numbering, #6502; `.ai` save: stroked type keeps its stroke's cap, join and miter limit in the text document, #6472; `.ai` and EPS import, PDF import: tracking and manual kerning in horizontally scaled type take the Horizontal Scale, as the file's page spaces such type; `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke, #3179; `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid, #3179; `.ai` and EPS import: stroked type keeps its stroke's cap, join and miter limit from the text document, #6472; `.ai` save: type in a variable font's named instance names its design-space setting, #6388; `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page, #3179; `.ai` and EPS import: a note for type in an envelope, #3179; `.ai` and EPS import: the live envelope and warped type check sees white art; PDF import: DeviceN images of one ink or of process plates keep their inks, #6348; `.ai` import: images in a spot ink's Separation space keep their ink, #6350; PDF import: Separation images keep their ink, #6314; PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; PDF import: image masks open colorized by the color they are painted in, #6313; EPS and `.ai` editing data: a ligature the page names by a private-use glyph matches its text object; EPS import: CIDFonts only named, over a Unicode CMap, set as live type; EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`,
|
||||
`OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/formats.rs`). Illustrator's list comes from its public
|
||||
@@ -343,6 +343,7 @@ Stop opacity on translated artboards remains outside this regression coverage.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-12 | minor | PDF import (#5235): a glyph with no advance in a font the file writes horizontally (a vowel sign or anusvara set over its letter) stays on its line instead of opening as vertical type of its own. A line of type in an Indic script (U+0900 to U+0DFF) is set again after it is read and compared with the glyphs the file drew: when it would draw other glyphs or put them elsewhere (a vowel sign the file draws before its consonant, a conjunct the file doesn't name), the line keeps the file's glyphs as outlines, with a note. Indic type in a font that isn't installed never draws as the file does, so it keeps the file's glyphs too; other type in such a font still shows in the fallback font (#4568). Tests: `crates/pdf/src/tests/reshaped.rs`. Scores unchanged. |
|
||||
| 2026-11-12 | minor | Save refusal follow-up (#6146): changed format opens its Save options; unchanged format retains original options. Focused UI regressions passed, including the format-change case that failed before the fix; native panel acceptance pending. Scores and estimates unchanged. |
|
||||
| 2026-11-11 | minor | Newly imported SVG/SVGZ titles use the imported-text rules: generated Untitled names and control-only titles give the filename fallback, other control characters are removed, and titles are limited to 65,536 characters. Native and Preserve Editing titles remain unchanged (#3626). |
|
||||
| 2026-11-11 | minor | Native and Preserve Editing restores keep an existing File Info title exactly, including repeated spaces; imported-text fallback titles alone are normalized (#3626). |
|
||||
|
||||
Reference in New Issue
Block a user