mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 09:29:44 -06:00
Merge pull request #5125 from dotCooCoo/fix/ai-os22-g11
.ai import: legacy type keeps its spacing, hanging punctuation and hyphenation
This commit is contained in:
@@ -3310,6 +3310,13 @@ fn arb_ai_token() -> impl Strategy<Value = String> {
|
||||
"9 Ta",
|
||||
"1e308 -1e308 1e308 Ti",
|
||||
"0 1 Tk",
|
||||
"-1e308 0 1e308 TC",
|
||||
"1e308 -1 5 TW",
|
||||
"/x 100 200 TW",
|
||||
"1 Tq",
|
||||
"7 Tq",
|
||||
"1 1 1e308 -1e308 4e9 Th",
|
||||
"1 1 (2) 3 4 Th",
|
||||
"[39/quotesingle 128/Euro/eacute/nosuch 300/x TE",
|
||||
"[1e308/a -1/b TE",
|
||||
"\n%AI3_BeginEncoding: _Helvetica Helvetica\n[/_Helvetica/Helvetica 0 0 1 TZ\n%AI3_EndEncoding AdobeType\n",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Type in the legacy format (version 7 specification §15): text objects `type To` … `TO`, their
|
||||
//! text path (`a b c d tx ty startPt Tp`, an area's frame or the path type runs along, `TP`), the
|
||||
//! modal character and paragraph attributes (`Tr Tf Ts Tz Tt TA Tl Ta Ti`), the paint style, and
|
||||
//! modal character and paragraph attributes (`Tr Tf Ts Tz Tt TA Tl Ta Ti`, and §15.4.7 and §22's
|
||||
//! letter and word spacing `TC TW`, hanging punctuation `Tq` and hyphenation `Th`), the paint style, and
|
||||
//! the characters (`Tx`, `Tj`, overflow `TX`; `\r` ends a paragraph). Final-form operators (the
|
||||
//! computed positions, §15.1) are skipped: the type is laid out again. Strings are decoded by the
|
||||
//! file's `TE` encoding, else as Windows-1252; a re-encoded font `/_name` is the font its
|
||||
@@ -78,8 +79,23 @@ impl TypeState {
|
||||
}
|
||||
}
|
||||
|
||||
fn num(vals: &[V], i: usize) -> Option<f64> {
|
||||
vals.iter().filter_map(V::num).nth(i).filter(|v| v.is_finite())
|
||||
/// An operator's `N` operands, the last `N` values before it, when each is a finite number.
|
||||
fn nums<const N: usize>(vals: &[V]) -> Option<[f64; N]> {
|
||||
let operands = vals.get(vals.len().checked_sub(N)?..)?;
|
||||
let mut out = [0.0; N];
|
||||
for (o, v) in out.iter_mut().zip(operands) {
|
||||
*o = v.num().filter(|n| n.is_finite())?;
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// A flag operand: 0 or 1.
|
||||
fn flag(v: f64) -> Option<bool> {
|
||||
match v {
|
||||
0.0 => Some(false),
|
||||
1.0 => Some(true),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl Reader<'_> {
|
||||
@@ -110,23 +126,28 @@ impl Reader<'_> {
|
||||
/// `type To`: a text object begins.
|
||||
pub(super) fn begin_text(&mut self, vals: &[V], hidden: bool) {
|
||||
self.end_path_quietly();
|
||||
let kind = num(vals, 0).map_or(0, |k| k as i64);
|
||||
let kind = nums::<1>(vals).map_or(0, |[k]| k as i64);
|
||||
self.text = Some(LegacyText { kind, gs: self.gs.clone(), hidden, ..LegacyText::default() });
|
||||
}
|
||||
|
||||
/// `a b c d tx ty startPt Tp`: the text path begins.
|
||||
pub(super) fn begin_text_path(&mut self, vals: &[V]) {
|
||||
self.end_path_quietly();
|
||||
let n: Vec<f64> = vals.iter().filter_map(V::num).collect();
|
||||
let Some(t) = self.text.as_mut() else { return };
|
||||
if let Some(m) = n.get(..6).and_then(|m| <[f64; 6]>::try_from(m).ok()) {
|
||||
// Seven operands, or the matrix alone.
|
||||
let (m, start) = match vals.len() {
|
||||
7 => (vals.get(..6).and_then(nums::<6>), vals.get(6).and_then(V::num)),
|
||||
6 => (nums::<6>(vals), None),
|
||||
_ => (None, None),
|
||||
};
|
||||
if let Some(m) = m {
|
||||
let m = Affine::new(m);
|
||||
let det = m.determinant();
|
||||
if m.as_coeffs().iter().all(|v| v.is_finite()) && det.is_finite() && det.abs() > 1e-12 {
|
||||
if det.is_finite() && det.abs() > 1e-12 {
|
||||
t.matrix = Some(m);
|
||||
}
|
||||
}
|
||||
t.start = n.get(6).copied().filter(|s| s.is_finite() && *s >= 0.0).unwrap_or(0.0);
|
||||
t.start = start.filter(|s| s.is_finite() && *s >= 0.0).unwrap_or(0.0);
|
||||
t.in_path = true;
|
||||
}
|
||||
|
||||
@@ -150,56 +171,122 @@ impl Reader<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A character or paragraph attribute.
|
||||
/// A character or paragraph attribute. An attribute whose operands are missing, are not
|
||||
/// numbers, or are flags other than 0 and 1 stays as it was.
|
||||
pub(super) fn text_attr(&mut self, w: &str, vals: &[V]) {
|
||||
let st = &mut self.type_state;
|
||||
let n0 = num(vals, 0);
|
||||
let n0 = nums::<1>(vals).map(|[v]| v);
|
||||
match w {
|
||||
"Tr" => st.render = n0.map_or(0, |v| v as i64),
|
||||
"Tr" => {
|
||||
if let Some(v) = n0 {
|
||||
st.render = v as i64;
|
||||
}
|
||||
}
|
||||
"Tf" => {
|
||||
if let Some(name) = vals.iter().find_map(|v| if let V::Name(n) = v { Some(n.clone()) } else { None }) {
|
||||
let name = st
|
||||
.fonts
|
||||
.iter()
|
||||
.find(|(new, _)| *new == name)
|
||||
.map_or_else(|| name.trim_start_matches('_').to_string(), |(_, old)| old.clone());
|
||||
// `/name size Tf`, or `/name size ascent descent Tf`.
|
||||
let Some(at) = vals.iter().position(|v| matches!(v, V::Name(_))) else { return };
|
||||
if let Some(V::Name(name)) = vals.get(at) {
|
||||
let name =
|
||||
st.fonts.iter().find(|(new, _)| new == name).map_or_else(|| name.trim_start_matches('_').to_string(), |(_, old)| old.clone());
|
||||
let (family, style) = super::super::super::text::family_style(&name);
|
||||
st.style.font_family = family;
|
||||
st.style.font_style = style;
|
||||
st.style.font_version = None;
|
||||
}
|
||||
if let Some(size) = num(vals, 0).filter(|s| *s > 0.0) {
|
||||
if let Some(size) = vals.get(at + 1).and_then(V::num).filter(|s| s.is_finite() && *s > 0.0) {
|
||||
st.style.size = size.min(MAX_SIZE);
|
||||
}
|
||||
}
|
||||
"Ts" => st.style.baseline_shift = n0.unwrap_or(0.0).clamp(-MAX_SIZE, MAX_SIZE),
|
||||
"Ts" => {
|
||||
if let Some(v) = n0 {
|
||||
st.style.baseline_shift = v.clamp(-MAX_SIZE, MAX_SIZE);
|
||||
}
|
||||
}
|
||||
"Tz" => {
|
||||
// `parallelScale perpendicularScale Tz`; before version 7, `scale Tz`.
|
||||
let scale = |v: f64| v.clamp(1.0, 10_000.0);
|
||||
st.style.h_scale = n0.map_or(100.0, scale);
|
||||
st.style.v_scale = num(vals, 1).map_or(100.0, scale);
|
||||
let scales = match vals.len() {
|
||||
2 => nums::<2>(vals),
|
||||
1 => n0.map(|h| [h, 100.0]),
|
||||
_ => None,
|
||||
};
|
||||
if let Some([h, v]) = scales {
|
||||
(st.style.h_scale, st.style.v_scale) = (scale(h), scale(v));
|
||||
}
|
||||
}
|
||||
"Tt" => {
|
||||
if let Some(v) = n0 {
|
||||
st.style.tracking = v.clamp(-10_000.0, 10_000.0);
|
||||
}
|
||||
}
|
||||
"TA" => {
|
||||
if let Some(auto) = n0.and_then(flag) {
|
||||
st.style.kerning = if auto { None } else { Some(0.0) };
|
||||
}
|
||||
}
|
||||
"Tt" => st.style.tracking = n0.unwrap_or(0.0).clamp(-10_000.0, 10_000.0),
|
||||
"TA" => st.style.kerning = if n0 == Some(0.0) { Some(0.0) } else { None },
|
||||
"Tl" => {
|
||||
if let Some(l) = n0 {
|
||||
// `leading paragraphLeading Tl`; before version 7, `leading Tl`.
|
||||
let leading = match vals.len() {
|
||||
2 => nums::<2>(vals),
|
||||
1 => n0.map(|l| [l, 0.0]),
|
||||
_ => None,
|
||||
};
|
||||
if let Some([l, before]) = leading {
|
||||
let l = l.clamp(0.0, MAX_SIZE);
|
||||
// The automatic leading, 120% of the size, stays automatic.
|
||||
st.style.leading = if (l - st.style.size * 1.2).abs() < 1e-3 { None } else { Some(l) };
|
||||
st.para.space_before = before.clamp(-MAX_SIZE, MAX_SIZE);
|
||||
}
|
||||
st.para.space_before = num(vals, 1).unwrap_or(0.0).clamp(-MAX_SIZE, MAX_SIZE);
|
||||
}
|
||||
"Ta" => {
|
||||
st.para.justify = match n0.map(|v| v as i64) {
|
||||
Some(1) => Justify::Center,
|
||||
Some(2) => Justify::Right,
|
||||
Some(3) => Justify::JustifyLeft,
|
||||
Some(4) => Justify::JustifyAll,
|
||||
_ => Justify::Left,
|
||||
let justify = match n0 {
|
||||
Some(0.0) => Some(Justify::Left),
|
||||
Some(1.0) => Some(Justify::Center),
|
||||
Some(2.0) => Some(Justify::Right),
|
||||
Some(3.0) => Some(Justify::JustifyLeft),
|
||||
Some(4.0) => Some(Justify::JustifyAll),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(j) = justify {
|
||||
st.para.justify = j;
|
||||
}
|
||||
}
|
||||
"Ti" => {
|
||||
let indent = |i: usize| num(vals, i).unwrap_or(0.0).clamp(-MAX_SIZE, MAX_SIZE);
|
||||
(st.para.first_line_indent, st.para.left_indent, st.para.right_indent) = (indent(0), indent(1), indent(2));
|
||||
if let Some(i) = nums::<3>(vals) {
|
||||
let [first, left, right] = i.map(|v| v.clamp(-MAX_SIZE, MAX_SIZE));
|
||||
(st.para.first_line_indent, st.para.left_indent, st.para.right_indent) = (first, left, right);
|
||||
}
|
||||
}
|
||||
// `min opt max TC` (letter spacing) and `min opt max TW` (word spacing), in percent of
|
||||
// a space's width, as the paragraph's Justification options take them.
|
||||
"TC" | "TW" => {
|
||||
if let Some(v) = nums::<3>(vals) {
|
||||
let j = &mut st.para.justification;
|
||||
if w == "TC" {
|
||||
j.letter_spacing = v;
|
||||
} else {
|
||||
j.word_spacing = v;
|
||||
}
|
||||
j.keep_in_range();
|
||||
}
|
||||
}
|
||||
// `hangingQuotes Tq`.
|
||||
"Tq" => {
|
||||
if let Some(on) = n0.and_then(flag) {
|
||||
st.para.roman_hanging = on;
|
||||
}
|
||||
}
|
||||
// `hyphenate limitHyphLines minLeadHyphen minTailHyphen maxHyphLines Th`, kept to the
|
||||
// ranges `text.setFormat` takes.
|
||||
"Th" => {
|
||||
let Some([on, limit, lead, tail, max]) = nums::<5>(vals) else { return };
|
||||
let (Some(on), Some(limit)) = (flag(on), flag(limit)) else { return };
|
||||
let letters = |v: f64, lo: f64, hi: f64| v.round().clamp(lo, hi) as u32;
|
||||
st.para.hyphenate = on;
|
||||
let h = &mut st.para.hyphenation;
|
||||
h.after_first = letters(lead, 1.0, 15.0);
|
||||
h.before_last = letters(tail, 1.0, 15.0);
|
||||
h.hyphen_limit = if limit { letters(max, 0.0, 25.0) } else { 0 };
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -222,9 +309,12 @@ impl Reader<'_> {
|
||||
t.pending_break = true;
|
||||
return;
|
||||
}
|
||||
if st.chars >= MAX_CHARS {
|
||||
// As many characters as the file's allowance has left.
|
||||
let room = MAX_CHARS.saturating_sub(st.chars);
|
||||
if room == 0 {
|
||||
return;
|
||||
}
|
||||
let decoded: String = decoded.chars().take(room).collect();
|
||||
st.chars += decoded.chars().count();
|
||||
let gs = &self.gs;
|
||||
let mut style = st.style.clone();
|
||||
|
||||
@@ -276,15 +276,13 @@ impl<'a> Reader<'a> {
|
||||
"TP" => self.end_text_path(),
|
||||
"TO" => self.end_text(hidden)?,
|
||||
"Tx" | "Tj" | "TX" => self.text_body(w, &vals),
|
||||
"Tr" | "Tf" | "Ts" | "Tz" | "Tt" | "TA" | "Tl" | "Ta" | "Ti" => self.text_attr(w, &vals),
|
||||
// Attributes the reader doesn't keep (kerning pairs, spacing, hanging punctuation,
|
||||
// Far-Eastern type) and final-form positions (§15.3).
|
||||
"Tk" | "TK" | "TC" | "TW" | "Tq" | "Tc" | "Tw" | "Tm" | "Td" | "T*" | "TR" | "T+" | "T-" | "TV" | "Tv" | "Ty" | "TY" | "TG" | "Tg"
|
||||
| "Tu" | "Xu" => {}
|
||||
// Hyphenation and tab-ruler attributes the reader doesn't keep either (§22): `Th`
|
||||
// hyphen control, `XL` hyphenation language, `Xb`/`XB` a tab definition (and the `Xe`
|
||||
// tab descriptions between them).
|
||||
"Th" | "XL" | "Xb" | "XB" | "Xe" => {}
|
||||
"Tr" | "Tf" | "Ts" | "Tz" | "Tt" | "TA" | "Tl" | "Ta" | "Ti" | "TC" | "TW" | "Tq" | "Th" => self.text_attr(w, &vals),
|
||||
// Attributes the reader doesn't keep (kerning pairs, Far-Eastern type) and final-form
|
||||
// positions (§15.3).
|
||||
"Tk" | "TK" | "Tc" | "Tw" | "Tm" | "Td" | "T*" | "TR" | "T+" | "T-" | "TV" | "Tv" | "Ty" | "TY" | "TG" | "Tg" | "Tu" | "Xu" => {}
|
||||
// The hyphenation language and tab rulers, which the reader doesn't keep either (§22): `XL`,
|
||||
// `Xb`/`XB` a tab definition (and the `Xe` tab descriptions between them).
|
||||
"XL" | "Xb" | "XB" | "Xe" => {}
|
||||
"TE" => self.te(&vals),
|
||||
// An image whose samples follow in comment lines (read at `%AI5_EndRaster`).
|
||||
"XI" if self.raster.is_some() => {
|
||||
|
||||
@@ -1183,6 +1183,113 @@ fn text_objects_are_slots_named_after_their_story() {
|
||||
assert_eq!((a.len(), a[0].children().map(Vec::len), slot_of(a[1].name.as_deref())), (2, Some(1), Some(Some(7))));
|
||||
}
|
||||
|
||||
/// A file in the legacy format (version 7) whose layer holds `art`, crop marks 0 0 200 100.
|
||||
fn type_stream7(art: &str) -> String {
|
||||
format!(
|
||||
"%!PS-Adobe-3.0\n%%Creator: test\n%AI5_FileFormat 3.0\n%AI3_Cropmarks: 0 0 200 100\n%%EndComments\n%%BeginProlog\n%%EndProlog\n%%BeginSetup\n%%EndSetup\n{}\n%%PageTrailer\ngsave annotatepage grestore showpage\n%%Trailer\n%%EOF\n",
|
||||
layer(art)
|
||||
)
|
||||
}
|
||||
|
||||
/// The text objects of a legacy-format file whose layer holds `art`.
|
||||
fn legacy_texts(art: &str) -> Vec<vectorcraft_doc::TextObject> {
|
||||
let doc = match read(type_stream7(art).as_bytes()) {
|
||||
Ok(r) => r.doc,
|
||||
Err(e) => panic!("{e}"),
|
||||
};
|
||||
art_texts(&doc)
|
||||
}
|
||||
|
||||
fn art_texts(doc: &Document) -> Vec<vectorcraft_doc::TextObject> {
|
||||
art(doc).iter().filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some((**t).clone()) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// Point type at (10, 50): `attrs`, then the characters `text`.
|
||||
fn point_type(attrs: &str, text: &str) -> String {
|
||||
format!("0 To\n1 0 0 1 10 50 0 Tp\nTP\n0 Tr\n0 0 0 1 k\n/_Helvetica 12 Tf\n{attrs}\n({text}) Tx\nTO\n")
|
||||
}
|
||||
|
||||
/// `TC` and `TW` (§15.4.7) set the paragraph's letter and word spacing, `Tq` its hanging
|
||||
/// punctuation and `Th` (§22) its hyphenation; each paragraph keeps the values in force at its
|
||||
/// first character, and they last into the next text object.
|
||||
#[test]
|
||||
fn legacy_type_keeps_its_spacing_hanging_punctuation_and_hyphenation() {
|
||||
let two_paragraphs =
|
||||
point_type("10 15 20 TC\n70 110 140 TW\n1 Tq\n1 1 2 3 4 Th", "One\\r").replace("TO\n", "0 0 5 TC\n0 Tq\n0 0 4 5 6 Th\n(Two) Tx\nTO\n");
|
||||
let texts = legacy_texts(&format!("{two_paragraphs}{}", point_type("", "Three")));
|
||||
assert_eq!(texts.len(), 2);
|
||||
let one = texts[0].para_at(0);
|
||||
assert_eq!(one.justification.letter_spacing, [10.0, 15.0, 20.0]);
|
||||
assert_eq!(one.justification.word_spacing, [70.0, 110.0, 140.0]);
|
||||
assert!(one.roman_hanging && one.hyphenate);
|
||||
let h = &one.hyphenation;
|
||||
assert_eq!((h.after_first, h.before_last, h.hyphen_limit), (2, 3, 4));
|
||||
assert_eq!(h.words_longer_than, vectorcraft_doc::text::HyphenationOptions::default().words_longer_than, "no operand sets it");
|
||||
let two = texts[0].para_at(1);
|
||||
assert_eq!(two.justification.letter_spacing, [0.0, 0.0, 5.0]);
|
||||
assert_eq!(two.justification.word_spacing, [70.0, 110.0, 140.0], "word spacing stays");
|
||||
assert!(!two.roman_hanging && !two.hyphenate);
|
||||
// Without the limit flag, any number of hyphens in a row.
|
||||
assert_eq!((two.hyphenation.after_first, two.hyphenation.before_last, two.hyphenation.hyphen_limit), (4, 5, 0));
|
||||
// The next text object keeps them.
|
||||
assert_eq!(texts[1].para_at(0), two);
|
||||
}
|
||||
|
||||
/// A legacy attribute whose operands are missing, are not numbers, or are flags other than 0 and
|
||||
/// 1 leaves the attribute as it was, and operands are read by their place.
|
||||
#[test]
|
||||
fn legacy_type_attributes_with_bad_operands_stay_as_they_were() {
|
||||
let set = "10 15 20 TC\n70 110 140 TW\n1 Tq\n1 1 2 3 4 Th\n2 Ta\n0 TA\n5 6 7 Ti";
|
||||
let bad = "/x 15 20 TC\n110 140 TW\n2 Tq\n1 2 2 3 4 Th\n1 1 (2) 3 4 Th\n/x Ta\n2 TA\n5 /x 7 Ti";
|
||||
let texts = legacy_texts(&point_type(&format!("{set}\n{bad}"), "Hi"));
|
||||
let p = texts[0].para_at(0);
|
||||
assert_eq!(p.justification.letter_spacing, [10.0, 15.0, 20.0]);
|
||||
assert_eq!(p.justification.word_spacing, [70.0, 110.0, 140.0]);
|
||||
assert!(p.roman_hanging && p.hyphenate);
|
||||
assert_eq!((p.hyphenation.after_first, p.hyphenation.before_last, p.hyphenation.hyphen_limit), (2, 3, 4));
|
||||
assert_eq!(p.justify, vectorcraft_doc::Justify::Right);
|
||||
assert_eq!((p.first_line_indent, p.left_indent, p.right_indent), (5.0, 6.0, 7.0));
|
||||
assert_eq!(texts[0].runs[0].style.kerning, Some(0.0), "kerning stays off");
|
||||
// A scale or leading pair with a bad operand leaves both as they were; one operand is the
|
||||
// horizontal scale (or the leading) of the format before version 7.
|
||||
let texts = legacy_texts(&point_type("50 80 Tz\n14 3 Tl\n/x 120 Tz\n/x 20 Tl", "Hi"));
|
||||
let st = &texts[0].runs[0].style;
|
||||
assert_eq!((st.h_scale, st.v_scale, st.leading), (50.0, 80.0, Some(14.0)));
|
||||
assert_eq!(texts[0].para_at(0).space_before, 3.0);
|
||||
let st = &legacy_texts(&point_type("50 80 Tz\n120 Tz", "Hi"))[0].runs[0].style;
|
||||
assert_eq!((st.h_scale, st.v_scale), (120.0, 100.0));
|
||||
// A start point that isn't a number leaves the matrix.
|
||||
let good = &legacy_texts(&point_type("", "Hi"))[0];
|
||||
let bad = &legacy_texts(&point_type("", "Hi").replace("10 50 0 Tp", "10 50 /x Tp"))[0];
|
||||
assert_eq!(bad.xf, good.xf);
|
||||
}
|
||||
|
||||
/// Spacing is kept to the ranges the Justification options take, a Minimum, Desired and Maximum
|
||||
/// out of order go back to their defaults, and hyphenation's letter counts and limit to the ranges
|
||||
/// `text.setFormat` takes.
|
||||
#[test]
|
||||
fn legacy_type_spacing_and_hyphenation_are_kept_in_range() {
|
||||
let texts = legacy_texts(&point_type("-500 0 900 TC\n0 2000 5000 TW\n1 1 0 40 99 Th", "Hi"));
|
||||
let p = texts[0].para_at(0);
|
||||
assert_eq!(p.justification.letter_spacing, [-100.0, 0.0, 500.0]);
|
||||
assert_eq!(p.justification.word_spacing, [0.0, 1000.0, 1000.0]);
|
||||
assert_eq!((p.hyphenation.after_first, p.hyphenation.before_last, p.hyphenation.hyphen_limit), (1, 15, 25));
|
||||
let texts = legacy_texts(&point_type("5 0 0 TC\n200 100 80 TW", "Hi"));
|
||||
let j = &texts[0].para_at(0).justification;
|
||||
let d = vectorcraft_doc::text::JustificationOptions::default();
|
||||
assert_eq!((j.letter_spacing, j.word_spacing), (d.letter_spacing, d.word_spacing));
|
||||
}
|
||||
|
||||
/// A file's legacy type is read up to its allowance of characters, a single long run included.
|
||||
#[test]
|
||||
fn one_long_run_of_legacy_type_stops_at_the_allowance() {
|
||||
// The reader's allowance, `MAX_CHARS`.
|
||||
const ALLOWANCE: usize = 1 << 22;
|
||||
let long = "a".repeat(ALLOWANCE + 10);
|
||||
let texts = legacy_texts(&point_type("", &long));
|
||||
assert_eq!(texts[0].plain_text().chars().count(), ALLOWANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_it_doesnt_read_is_an_error_where_it_shows() {
|
||||
let e = read(stream(&layer("1 2 frobnicate\n")).as_bytes()).unwrap_err();
|
||||
|
||||
@@ -147,6 +147,17 @@ fn text_with_hyphenation_and_tab_attributes_opens_instead_of_failing() {
|
||||
let r = import(src.as_bytes()).unwrap();
|
||||
assert!(!r.preview, "read through the layers, not a fallback preview");
|
||||
assert!(!r.warnings.iter().any(|w| w.contains("doesn't know") || w.contains("couldn't be read")), "{:?}", r.warnings);
|
||||
// `Th` sets the paragraph's hyphenation: off, 2 letters before and after a hyphen, and the
|
||||
// limit of 99 hyphens in a row kept to the 25 `text.setFormat` takes.
|
||||
let mut texts = vec![];
|
||||
r.document.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
texts.push(t.para_at(0).clone());
|
||||
}
|
||||
});
|
||||
let [p] = texts.as_slice() else { panic!("{texts:?}") };
|
||||
assert!(!p.hyphenate);
|
||||
assert_eq!((p.hyphenation.after_first, p.hyphenation.before_last, p.hyphenation.hyphen_limit), (2, 2, 25));
|
||||
}
|
||||
|
||||
/// An Illustrator 3 file of paths alone, as image editors export their paths (storytold/photocraft#6036):
|
||||
|
||||
@@ -81,7 +81,8 @@ private editing data. VectorCraft:
|
||||
14; #868: round trip with Illustrator 2018 loses layers and groups), guides and non-printing construction
|
||||
lines lost (#779), some files 4× slower to open since the editing data is read whole (#758), live effects,
|
||||
brushes and symbols come in as their drawn look (#637; pattern fills are read since #1025); legacy `.ai`
|
||||
without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868.
|
||||
without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868, with its paragraphs' letter
|
||||
and word spacing (`TC`, `TW`), hanging punctuation (`Tq`) and hyphenation (`Th`).
|
||||
- Estimate: **~65%** for real exchange with Illustrator users (open: ~75%; save: PDF-compatible, or legacy
|
||||
version 7 with layers and groups; not yet tested by users who hand files back).
|
||||
Closing the open-side issues: 10–20 h with users' files.
|
||||
|
||||
Reference in New Issue
Block a user