mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 16:52:47 +00:00
Merge pull request #5777 from dotCooCoo/fix/large-canvas-eps-type
.ai/EPS import: type that names no style takes the normal styles
This commit is contained in:
@@ -1023,6 +1023,46 @@ fn an_empty_page_with_type_as_outlines_outlines_the_editing_datas_type() {
|
||||
assert_eq!(names(&s.doc().unwrap().doc.layers), ["Art"], "the editing data's layers");
|
||||
}
|
||||
|
||||
/// A story of a `.ai` file's text document with no character or paragraph runs is set in
|
||||
/// `[Normal Character Style]` and `[Normal Paragraph Style]`; the base attributes give only what
|
||||
/// those styles leave out.
|
||||
#[test]
|
||||
fn an_ai_story_without_runs_takes_the_normal_styles() {
|
||||
let story = "<< /0 << /0 (Hi\r) >> \
|
||||
/1 << /0 [ << /0 0 >> ] /2 [ << /99 /PC /6 [ << /99 /F /0 << /0 [ 8191.5 8191.5 ] >> /6 [ << /99 /R /6 [ << /99 /R /6 [ << /99 /L /6 [ \
|
||||
<< /99 /S /0 << /0 [ 0.0 0.0 ] >> /15 << /0 3 >> >> ] >> ] >> ] >> ] >> ] >> ] >> >>";
|
||||
let char_sheets = "/5 << /0 [ << /0 << /0 ([Normal Character Style]) /6 << /0 0 /1 9.0 \
|
||||
/53 << /99 /SimplePaint /0 << /0 1 /1 [ 1.0 0.0 0.0 1.0 ] >> >> >> >> >> ] >>";
|
||||
let para_sheets = "/6 << /0 [ << /0 << /0 ([Normal Paragraph Style]) /5 << /1 6.0 /9 false >> >> >> ] >>";
|
||||
let document = format!(
|
||||
"/0 << /1 << /0 [ << /0 << /0 << /0 (Helvetica) >> >> >> ] >> {char_sheets} {para_sheets} \
|
||||
/8 << /0 [ << /0 << /0 [ 0 0 ] /2 << /2 [ 1 0 0 1 0 0 ] >> >> >> ] >> >>\n\
|
||||
/1 << /1 [ {story} ] /2 << /1 12.0 /53 << /99 /SimplePaint /0 << /0 0 /1 [ 1.0 1.0 ] >> >> >> /3 << /1 2.0 /9 true >> >>\n"
|
||||
);
|
||||
let lines: Vec<String> = ascii85(document.as_bytes()).as_bytes().chunks(60).map(|c| format!("%{}", String::from_utf8_lossy(c))).collect();
|
||||
let text = "/AI11Text :\n0 /FreeUndo ,\n0 /FrameIndex ,\n0 /StoryIndex ,\n2 /TextAntialiasing ,\n;\n";
|
||||
let data = format!(
|
||||
"%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 100 100\n%%HiResBoundingBox: 0 0 100 100\n%AI3_Cropmarks: 0 0 100 100\n%AI3_TemplateBox: 50 50 50 50\n\
|
||||
%AI5_NumLayers: 1\n{}%AI11_BeginTextDocument\n/AI11TextDocument : /ASCII85Decode ,\n{}\n%AI11_EndTextDocument\n%%Trailer\n",
|
||||
ai::layer("Art", text),
|
||||
lines.join("\n")
|
||||
);
|
||||
let mut s = Session::new();
|
||||
let r = open(&mut s, "plain.ai", &ai::ai(&compressed(data.as_bytes()), ""), json!({})).unwrap();
|
||||
let mut texts = vec![];
|
||||
s.doc().unwrap().doc.walk(|c| {
|
||||
if let NodeKind::Text(t) = &c.kind {
|
||||
texts.push((**t).clone());
|
||||
}
|
||||
});
|
||||
let [t] = texts.as_slice() else { panic!("one text object: {r}") };
|
||||
assert_eq!(t.plain_text(), "Hi");
|
||||
let style = &t.runs[0].style;
|
||||
assert_eq!(style.size, 9.0, "{r}");
|
||||
assert_eq!(style.fill, vectorcraft_color::Paint::solid(vectorcraft_color::Color::rgb(0.0, 0.0, 1.0)));
|
||||
assert_eq!((t.para.first_line_indent, t.para.hyphenate), (6.0, false));
|
||||
}
|
||||
|
||||
/// Type asked for as outlines in a `.ai` saved without PDF compatibility (its page only a
|
||||
/// placeholder): the editing data's type, outlined, as for an empty page.
|
||||
#[test]
|
||||
|
||||
@@ -396,6 +396,9 @@ pub(super) struct Texts {
|
||||
/// The document's character styles (`[Normal Character Style]` first), each a name, the
|
||||
/// character style it is based on (key 5, an index into these) and its attributes (key 6).
|
||||
char_sheets: Vec<Val>,
|
||||
/// The document's paragraph styles (`[Normal Paragraph Style]` first), each a name, its
|
||||
/// attributes (key 5) and the paragraph style it is based on (key 6, an index into these).
|
||||
para_sheets: Vec<Val>,
|
||||
frames: Vec<Val>,
|
||||
}
|
||||
|
||||
@@ -434,21 +437,17 @@ impl Texts {
|
||||
.map(|f| f.get("0").and_then(|f| f.get("0")).and_then(|f| f.get("0")).and_then(Val::str).unwrap_or_default().to_string())
|
||||
.collect();
|
||||
let main = doc.get("1")?;
|
||||
let sheets = |key: &str| -> Vec<Val> {
|
||||
let list = doc.get("0").and_then(|d| d.get(key)).and_then(|s| s.get("0")).map(Val::list).unwrap_or_default();
|
||||
list.iter().take(MAX_SHEETS).map(|s| s.get("0").cloned().unwrap_or(Val::Dict(BTreeMap::new()))).collect()
|
||||
};
|
||||
Some(Texts {
|
||||
stories: main.get("1")?.list().to_vec(),
|
||||
fonts,
|
||||
defaults: main.get("2")?.clone(),
|
||||
para_defaults: main.get("3").cloned().unwrap_or(Val::Dict(BTreeMap::new())),
|
||||
char_sheets: doc
|
||||
.get("0")?
|
||||
.get("5")
|
||||
.and_then(|s| s.get("0"))
|
||||
.map(Val::list)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.take(MAX_SHEETS)
|
||||
.map(|s| s.get("0").cloned().unwrap_or(Val::Dict(BTreeMap::new())))
|
||||
.collect(),
|
||||
char_sheets: sheets("5"),
|
||||
para_sheets: sheets("6"),
|
||||
frames: doc.get("0")?.get("8")?.get("0")?.list().to_vec(),
|
||||
})
|
||||
}
|
||||
@@ -541,8 +540,10 @@ impl Texts {
|
||||
let mut paras = vec![];
|
||||
for run in s.get("0")?.get("5").map(|p| p.get("0").map(Val::list).unwrap_or_default()).unwrap_or_default() {
|
||||
let n = run.get("1").and_then(Val::num)? as usize;
|
||||
let props = run.get("0").and_then(|r| r.get("0")).and_then(|r| r.get("5"));
|
||||
paras.push((n, self.para(props)));
|
||||
paras.push((n, self.para(run.get("0").and_then(|r| r.get("0")))));
|
||||
}
|
||||
if paras.is_empty() {
|
||||
paras.push((text.chars().count(), self.para(None)));
|
||||
}
|
||||
let mut runs = vec![];
|
||||
for run in s.get("0")?.get("6").map(|p| p.get("0").map(Val::list).unwrap_or_default()).unwrap_or_default() {
|
||||
@@ -555,9 +556,19 @@ impl Texts {
|
||||
Some(Story { text, anchor, lines, frames, paras, runs, path_spacing, vertical })
|
||||
}
|
||||
|
||||
/// A paragraph's attributes: its own keys, else the document's.
|
||||
fn para(&self, p: Option<&Val>) -> Para {
|
||||
let pick = |k: &str| p.and_then(|p| p.get(k)).or_else(|| self.para_defaults.get(k));
|
||||
/// A paragraph's attributes (`run`: its attributes, key 5, and the paragraph style it is based
|
||||
/// on, key 6): its own keys, else those of its paragraph style and the ones that is based on,
|
||||
/// else those of `[Normal Paragraph Style]`, else the document's.
|
||||
fn para(&self, run: Option<&Val>) -> Para {
|
||||
let mut chain = vec![];
|
||||
let mut at = run;
|
||||
for _ in 0..=MAX_SHEETS_BASED_ON {
|
||||
let Some(style) = at else { break };
|
||||
chain.extend(style.get("5"));
|
||||
at = style.get("6").and_then(Val::num).filter(|i| i.is_finite() && *i >= 0.0).and_then(|i| self.para_sheets.get(i as usize));
|
||||
}
|
||||
chain.extend(self.para_sheets.first().and_then(|s| s.get("5")));
|
||||
let pick = |k: &str| chain.iter().find_map(|s| s.get(k)).or_else(|| self.para_defaults.get(k));
|
||||
let num = |k: &str| pick(k).and_then(Val::num).filter(|v| v.abs() < 1e5).unwrap_or(0.0);
|
||||
Para {
|
||||
justify: pick("0").and_then(Val::num).map_or(0, |v| v.clamp(0.0, 6.0) as u8),
|
||||
@@ -573,7 +584,7 @@ impl Texts {
|
||||
|
||||
/// A run's character style (`run`: its attributes, key 6, and the character style it is based
|
||||
/// on, key 5): its own keys, else those of its character style and the ones that is based on,
|
||||
/// else the document's.
|
||||
/// else those of `[Normal Character Style]`, else the document's.
|
||||
fn style(&self, run: Option<&Val>) -> Style {
|
||||
// The run's attributes, then its character style's, then the styles each is based on.
|
||||
let mut chain = vec![];
|
||||
@@ -583,6 +594,7 @@ impl Texts {
|
||||
chain.extend(style.get("6"));
|
||||
at = style.get("5").and_then(Val::num).filter(|i| i.is_finite() && *i >= 0.0).and_then(|i| self.char_sheets.get(i as usize));
|
||||
}
|
||||
chain.extend(self.char_sheets.first().and_then(|s| s.get("6")));
|
||||
let pick = |k: &str| chain.iter().find_map(|s| s.get(k)).or_else(|| self.defaults.get(k));
|
||||
let num = |k: &str| pick(k).and_then(Val::num);
|
||||
let on = |k: &str| !matches!(pick(k), Some(Val::Bool(false)));
|
||||
@@ -1074,9 +1086,10 @@ mod tests {
|
||||
defaults: Val::Dict(BTreeMap::new()),
|
||||
para_defaults: read(b"/33 1").unwrap(),
|
||||
char_sheets: vec![],
|
||||
para_sheets: vec![],
|
||||
frames: vec![],
|
||||
};
|
||||
let justify = |own: &[u8]| para_style(&texts.para(Some(&read(own).unwrap())), 1.0).justify;
|
||||
let justify = |own: &[u8]| para_style(&texts.para(Some(&read(&[b"/5 << ", own, b" >>"].concat()).unwrap())), 1.0).justify;
|
||||
// Right to left from the document's defaults, or the paragraph's own.
|
||||
assert_eq!(justify(b"/0 0"), Justify::Right);
|
||||
assert_eq!(justify(b"/0 1 /33 1"), Justify::Left);
|
||||
@@ -1096,6 +1109,7 @@ mod tests {
|
||||
defaults: Val::Dict(BTreeMap::new()),
|
||||
para_defaults: Val::Dict(BTreeMap::new()),
|
||||
char_sheets: vec![],
|
||||
para_sheets: vec![],
|
||||
frames: vec![],
|
||||
}
|
||||
.para(None);
|
||||
@@ -1142,6 +1156,7 @@ mod tests {
|
||||
defaults: Val::Dict(BTreeMap::new()),
|
||||
para_defaults: Val::Dict(BTreeMap::new()),
|
||||
char_sheets: vec![],
|
||||
para_sheets: vec![],
|
||||
frames: cases.iter().map(|c| frame(c)).collect(),
|
||||
};
|
||||
let vertical: Vec<bool> = (0..cases.len() + 1).map(|i| texts.frame_vertical(i)).collect();
|
||||
@@ -1157,13 +1172,14 @@ mod tests {
|
||||
fonts: vec![],
|
||||
defaults: read(b"/35 1").unwrap(),
|
||||
para_defaults: Val::Dict(BTreeMap::new()),
|
||||
char_sheets: vec![read(b"/0 (Block) /6 << /35 3 >>").unwrap()],
|
||||
char_sheets: vec![read(b"/0 ([Normal Character Style]) /6 << >>").unwrap(), read(b"/0 (Block) /6 << /35 3 >>").unwrap()],
|
||||
para_sheets: vec![],
|
||||
frames: vec![],
|
||||
};
|
||||
let tcy = |run: &[u8]| texts.style(Some(&read(run).unwrap())).tate_chu_yoko;
|
||||
assert!(tcy(b"/6 << /35 3 >>"), "the run's own");
|
||||
assert!(tcy(b"/5 0 /6 << >>"), "its character style's");
|
||||
assert!(!tcy(b"/5 0 /6 << /35 1 >>"), "the run's own wins");
|
||||
assert!(tcy(b"/5 1 /6 << >>"), "its character style's");
|
||||
assert!(!tcy(b"/5 1 /6 << /35 1 >>"), "the run's own wins");
|
||||
for other in [&b"/6 << >>"[..], b"/6 << /35 2 >>", b"/6 << /35 3.5 >>", b"/6 << /35 (3) >>", b"/6 << /35 -3 >>", b"/6 << /35 1e308 >>"] {
|
||||
assert!(!tcy(other), "{}", String::from_utf8_lossy(other));
|
||||
}
|
||||
@@ -1180,9 +1196,10 @@ mod tests {
|
||||
defaults: Val::Dict(BTreeMap::new()),
|
||||
para_defaults: read(b"/33 1").unwrap(),
|
||||
char_sheets: vec![],
|
||||
para_sheets: vec![],
|
||||
frames: vec![],
|
||||
};
|
||||
let direction = |own: &[u8]| para_style(&texts.para(Some(&read(own).unwrap())), 1.0).direction;
|
||||
let direction = |own: &[u8]| para_style(&texts.para(Some(&read(&[b"/5 << ", own, b" >>"].concat()).unwrap())), 1.0).direction;
|
||||
assert_eq!(direction(b"/0 2"), Some(ParaDirection::RightToLeft), "from the document's defaults");
|
||||
assert_eq!(direction(b"/0 2 /33 0"), None, "the paragraph's own");
|
||||
let plain = Texts { para_defaults: Val::Dict(BTreeMap::new()), ..texts };
|
||||
@@ -1221,6 +1238,7 @@ mod tests {
|
||||
defaults: Val::Dict(BTreeMap::new()),
|
||||
para_defaults: Val::Dict(BTreeMap::new()),
|
||||
char_sheets: vec![],
|
||||
para_sheets: vec![],
|
||||
frames: vec![read(doc.as_bytes()).unwrap()],
|
||||
};
|
||||
texts.frame(0)
|
||||
@@ -1520,6 +1538,7 @@ mod tests {
|
||||
defaults: Val::Dict(BTreeMap::new()),
|
||||
para_defaults: Val::Dict(BTreeMap::new()),
|
||||
char_sheets: vec![],
|
||||
para_sheets: vec![],
|
||||
frames: vec![read(doc.as_bytes()).unwrap()],
|
||||
};
|
||||
texts.frame(0).unwrap()
|
||||
|
||||
@@ -2358,6 +2358,53 @@ fn type_from_the_text_document_takes_its_character_styles_attributes() {
|
||||
assert_eq!(style(2), (solid(Color::cmyk(0.0, 1.0, 0.0, 0.0)), Some(18.0)), "the run's own colour");
|
||||
}
|
||||
|
||||
/// A story with no runs, and a run or paragraph that names no style, is set in `[Normal Character
|
||||
/// Style]` and `[Normal Paragraph Style]` before the text document's base attributes. A Large
|
||||
/// Canvas copy (1:10) writes the styles at the art's scale (1.2 for 12 points, 0.6 for a 6-point
|
||||
/// indent) and the base attributes at the page's, so the type comes in at 12 points with a 6-point
|
||||
/// indent.
|
||||
#[test]
|
||||
fn a_story_without_runs_takes_the_normal_styles() {
|
||||
let own_style = "/5 0 /6 << /0 0 /1 14.0 /53 << /99 /CAITextPaint /0 << /0 2 /1 [ 1.0 0.0 1.0 0.0 0.0 ] >> >> >>";
|
||||
let doc = text_document(&[("Plain\r", 0, (8191.5, 8191.5), (0.0, 0.0), 0.0), ("Runs\r", 0, (8191.5, 8190.5), (0.0, -1.0), 0.0)])
|
||||
// The first story has no runs.
|
||||
.replacen(&format!("/6 << /0 [ << /0 << /0 << /0 () {own_style} >> >> /1 6 >> ] >> "), "", 1)
|
||||
.replacen("/5 << /0 [ << /0 << /0 << /0 () /5 << /0 0 >> /6 0 >> >> /1 6 >> ] >> ", "", 1)
|
||||
// The second story's runs name no style.
|
||||
.replacen(own_style, "/6 << /0 0 >>", 1)
|
||||
.replacen("/5 << /0 0 >> /6 0", "/5 << /0 0 >>", 1)
|
||||
// The document's base attributes: 12 points, white, a 60-point indent, hyphenated.
|
||||
.replacen("/2 << /1 12.0 >>", "/2 << /1 12.0 /53 << /99 /SimplePaint /0 << /0 0 /1 [ 1.0 1.0 ] >> >> >> /3 << /1 60.0 /9 true >>", 1)
|
||||
// The normal styles: 1.2 art units, blue; a 0.6 art unit indent, not hyphenated.
|
||||
.replacen(
|
||||
"/0 << /1 <<",
|
||||
"/0 << /5 << /0 [ << /0 << /0 ([Normal Character Style]) /6 << /0 0 /1 1.2 \
|
||||
/53 << /99 /SimplePaint /0 << /0 1 /1 [ 1.0 0.0 0.0 1.0 ] >> >> >> >> >> ] >> \
|
||||
/6 << /0 [ << /0 << /0 ([Normal Paragraph Style]) /5 << /1 0.6 /9 false >> >> >> ] >> /1 <<",
|
||||
1,
|
||||
);
|
||||
assert!(!doc.contains("/1 14.0") && !doc.contains("/6 0 "), "{doc}");
|
||||
// A 10 × 10 unit copy of the page's 100 × 100 points, its template box centered at (5, 5).
|
||||
let art = native_with_text(
|
||||
&(layer("Back", true, "0 0 1 0 k\n5 5 m\n5.4 5 L\n5.4 5.4 L\n5 5.4 L\nf") + &layer("Spare", false, &(text_object(0) + &text_object(1)))),
|
||||
&doc,
|
||||
)
|
||||
.replace("0 0 100 100", "0 0 10 10")
|
||||
.replace("%AI3_TemplateBox: 50 50 50 50\n", "%AI3_TemplateBox: 5 5 5 5\n%AI24_LargeCanvasScale: 10\n");
|
||||
let r = import(&eps(&page_square(50, 50), &art)).unwrap();
|
||||
assert_eq!(names(&r.document), ["Back", "Spare"], "{:?}", r.warnings);
|
||||
let texts = text_of(&r.document.layers[1]);
|
||||
assert_eq!(texts.iter().map(|t| t.plain_text()).collect::<Vec<_>>(), ["Plain", "Runs"], "{:?}", r.warnings);
|
||||
let [.., e, f] = texts[0].xf.as_coeffs();
|
||||
assert!((e - 50.0).abs() < 1e-6 && (f - 50.0).abs() < 1e-6, "{:?}", texts[0].xf);
|
||||
for t in texts {
|
||||
let style = &t.runs[0].style;
|
||||
assert!((style.size - 12.0).abs() < 1e-9, "{}: {}", t.plain_text(), style.size);
|
||||
assert_eq!(style.fill, vectorcraft_color::Paint::solid(vectorcraft_color::Color::rgb(0.0, 0.0, 1.0)), "{}", t.plain_text());
|
||||
assert!((t.para.first_line_indent - 6.0).abs() < 1e-9 && !t.para.hyphenate, "{}: {:?}", t.plain_text(), t.para);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_from_the_text_document_keeps_its_case_setting() {
|
||||
// Key 12 of a character style is its case: 1 Small Caps, 2 All Caps. The characters stay as
|
||||
|
||||
Reference in New Issue
Block a user