mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Merge pull request #4801 from mockydevs/fix/ai-text-character-style
fix(ai-import): type from the text document takes its character style's attributes
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
|
||||
|
||||
@@ -185,7 +185,7 @@ pub fn add_anchors(sp: &SubPath, which: SegmentKind, spacing: AnchorSpacing, cap
|
||||
};
|
||||
let c = sp.segment(seg);
|
||||
let len = c.arclen(ACCURACY);
|
||||
if !picked || !(len > TINY && len.is_finite()) {
|
||||
if !(picked && len > TINY && len.is_finite()) {
|
||||
continue;
|
||||
}
|
||||
let (count, step) = match spacing {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user