mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
fix(ai-import): type from the text document takes its character style's attributes
Type made from an Illustrator file's text document took each character attribute its run doesn't set from the document's base attributes, not from the character style the run names ([Normal Character Style] or another of the file's). Where the two differ the type came in wrong: a file whose base fill is white and whose Normal Character Style is black came in with its text white, invisible, so the layers differed from the page wherever the page's own type didn't stand in for it. A run now takes its own attributes, then its character style's (and the styles that one is based on, up to 16), then the document's.
This commit is contained in:
@@ -235,6 +235,11 @@ fn read(bytes: &[u8]) -> Option<Val> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Most character styles read of a text document, and most styles a run's style is followed
|
||||
/// through to the ones they are based on.
|
||||
const MAX_SHEETS: usize = 4096;
|
||||
const MAX_SHEETS_BASED_ON: usize = 16;
|
||||
|
||||
/// Most frames read of one story, and most path segments of a frame.
|
||||
const MAX_FRAMES: usize = 4096;
|
||||
const MAX_SEGMENTS: usize = 1 << 16;
|
||||
@@ -309,6 +314,9 @@ pub(super) struct Texts {
|
||||
/// The character and paragraph styles a story starts from.
|
||||
defaults: Val,
|
||||
para_defaults: Val,
|
||||
/// 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>,
|
||||
frames: Vec<Val>,
|
||||
}
|
||||
|
||||
@@ -352,6 +360,16 @@ impl Texts {
|
||||
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(),
|
||||
frames: doc.get("0")?.get("8")?.get("0")?.list().to_vec(),
|
||||
})
|
||||
}
|
||||
@@ -440,8 +458,7 @@ impl Texts {
|
||||
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() {
|
||||
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("6"));
|
||||
runs.push((n, self.style(props)));
|
||||
runs.push((n, self.style(run.get("0").and_then(|r| r.get("0")))));
|
||||
}
|
||||
if runs.is_empty() {
|
||||
runs.push((text.chars().count(), self.style(None)));
|
||||
@@ -465,9 +482,19 @@ impl Texts {
|
||||
}
|
||||
}
|
||||
|
||||
/// A character style: its own keys, else the document's.
|
||||
fn style(&self, s: Option<&Val>) -> Style {
|
||||
let pick = |k: &str| s.and_then(|s| s.get(k)).or_else(|| self.defaults.get(k));
|
||||
/// 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.
|
||||
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![];
|
||||
let mut at = run;
|
||||
for _ in 0..=MAX_SHEETS_BASED_ON {
|
||||
let Some(style) = at else { break };
|
||||
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));
|
||||
}
|
||||
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)));
|
||||
let font = num("0").and_then(|i| self.fonts.get(i as usize)).filter(|f| !f.is_empty()).cloned();
|
||||
@@ -899,8 +926,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn right_to_left_paragraphs_swap_left_and_right_alignment() {
|
||||
let texts =
|
||||
Texts { stories: vec![], fonts: vec![], defaults: Val::Dict(BTreeMap::new()), para_defaults: read(b"/33 1").unwrap(), frames: vec![] };
|
||||
let texts = Texts {
|
||||
stories: vec![],
|
||||
fonts: vec![],
|
||||
defaults: Val::Dict(BTreeMap::new()),
|
||||
para_defaults: read(b"/33 1").unwrap(),
|
||||
char_sheets: vec![],
|
||||
frames: vec![],
|
||||
};
|
||||
let justify = |own: &[u8]| para_style(&texts.para(Some(&read(own).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);
|
||||
@@ -915,9 +948,15 @@ mod tests {
|
||||
/// dimension's is) lies in a tall box, not a wide one.
|
||||
#[test]
|
||||
fn point_type_lies_along_its_baseline() {
|
||||
let para =
|
||||
Texts { stories: vec![], fonts: vec![], defaults: Val::Dict(BTreeMap::new()), para_defaults: Val::Dict(BTreeMap::new()), frames: vec![] }
|
||||
.para(None);
|
||||
let para = Texts {
|
||||
stories: vec![],
|
||||
fonts: vec![],
|
||||
defaults: Val::Dict(BTreeMap::new()),
|
||||
para_defaults: Val::Dict(BTreeMap::new()),
|
||||
char_sheets: vec![],
|
||||
frames: vec![],
|
||||
}
|
||||
.para(None);
|
||||
let mut s = story(" 80 mm ", &[0.0], ¶);
|
||||
let size = |s: &Story| s.region((0.0, 0.0), Affine::IDENTITY).map(|r| (r.width(), r.height())).unwrap();
|
||||
let (w, h) = size(&s);
|
||||
@@ -933,8 +972,14 @@ mod tests {
|
||||
/// that doesn't say, goes by its letters.
|
||||
#[test]
|
||||
fn right_to_left_paragraphs_set_their_direction() {
|
||||
let texts =
|
||||
Texts { stories: vec![], fonts: vec![], defaults: Val::Dict(BTreeMap::new()), para_defaults: read(b"/33 1").unwrap(), frames: vec![] };
|
||||
let texts = Texts {
|
||||
stories: vec![],
|
||||
fonts: vec![],
|
||||
defaults: Val::Dict(BTreeMap::new()),
|
||||
para_defaults: read(b"/33 1").unwrap(),
|
||||
char_sheets: vec![],
|
||||
frames: vec![],
|
||||
};
|
||||
let direction = |own: &[u8]| para_style(&texts.para(Some(&read(own).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");
|
||||
@@ -973,6 +1018,7 @@ mod tests {
|
||||
fonts: vec![],
|
||||
defaults: Val::Dict(BTreeMap::new()),
|
||||
para_defaults: Val::Dict(BTreeMap::new()),
|
||||
char_sheets: vec![],
|
||||
frames: vec![read(doc.as_bytes()).unwrap()],
|
||||
};
|
||||
texts.frame(0)
|
||||
|
||||
@@ -841,6 +841,46 @@ fn type_on_a_hidden_layer_comes_from_the_text_document() {
|
||||
assert!(!r.warnings.iter().any(|w| w.contains("couldn't be read")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// A run that sets no colour of its own takes its character style's (here `[Normal Character
|
||||
/// Style]`, black), not the text document's base attributes (here white, which hid the type).
|
||||
/// A style takes what it doesn't set from the style it is based on, and a run's own setting wins.
|
||||
#[test]
|
||||
fn type_from_the_text_document_takes_its_character_styles_attributes() {
|
||||
let own_fill = "/53 << /99 /CAITextPaint /0 << /0 2 /1 [ 1.0 0.0 1.0 0.0 0.0 ] >> >> ";
|
||||
let run = |style: u8| format!("/5 {style} /6 << /0 0 /1 14.0 >>");
|
||||
let doc = text_document(&[
|
||||
("Normal\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0),
|
||||
("Based\r", 0, (8200.0, 8170.0), (0.0, 0.0), 0.0),
|
||||
("Own\r", 0, (8200.0, 8160.0), (0.0, 0.0), 0.0),
|
||||
])
|
||||
// The first two runs set no colour, the second of the style based on the normal one.
|
||||
.replacen(own_fill, "", 2)
|
||||
.replacen(&run(0), "/5 X", 1)
|
||||
.replacen(&run(0), &run(1), 1)
|
||||
.replacen("/5 X", &run(0), 1)
|
||||
// The document's base attributes: grey level 1, white, with auto leading.
|
||||
.replacen("/2 << /1 12.0 >>", "/2 << /1 12.0 /4 true /53 << /99 /SimplePaint /0 << /0 0 /1 [ 1.0 1.0 ] >> >> >>", 1)
|
||||
// Its character styles: the normal one black with 18-point leading, and one based on it in red.
|
||||
.replacen(
|
||||
"/0 << /1 <<",
|
||||
"/0 << /5 << /0 [ << /0 << /0 ([Normal Character Style]) /6 << /4 false /5 18.0 \
|
||||
/53 << /99 /SimplePaint /0 << /0 1 /1 [ 1.0 0.0 0.0 0.0 ] >> >> >> >> >> \
|
||||
<< /0 << /0 (Red) /5 0 /6 << /53 << /99 /SimplePaint /0 << /0 1 /1 [ 1.0 1.0 0.0 0.0 ] >> >> >> >> >> ] >> /1 <<",
|
||||
1,
|
||||
);
|
||||
let spare = text_object(0) + &text_object(1) + &text_object(2);
|
||||
let art = native_with_text(&(layer("Back", true, &square(10, 10)) + &layer("Spare", false, &spare)), &doc);
|
||||
let r = import(&eps(&page_square(10, 10), &art)).unwrap();
|
||||
let texts = text_of(&r.document.layers[1]);
|
||||
assert_eq!(texts.iter().map(|t| t.plain_text()).collect::<Vec<_>>(), ["Normal", "Based", "Own"], "{:?}", r.warnings);
|
||||
let style = |i: usize| (texts[i].runs[0].style.fill.clone(), texts[i].runs[0].style.leading);
|
||||
let solid = vectorcraft_color::Paint::solid;
|
||||
use vectorcraft_color::Color;
|
||||
assert_eq!(style(0), (solid(Color::rgb(0.0, 0.0, 0.0)), Some(18.0)), "the normal style's");
|
||||
assert_eq!(style(1), (solid(Color::rgb(1.0, 0.0, 0.0)), Some(18.0)), "its style's colour, the leading of the one it's based on");
|
||||
assert_eq!(style(2), (solid(Color::cmyk(0.0, 1.0, 0.0, 0.0)), Some(18.0)), "the run's own colour");
|
||||
}
|
||||
|
||||
#[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
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user