EPS import: spell out abbreviated styles, and keep a line the page draws in pieces as one text
- A PostScript font name's shortened style words (Md, Lt, Bd, XBd, Cn, It, ...)
weren't matched to installed faces, so AkzidenzGroteskPro-Md came in as
the condensed face. They are spelled out before the face is looked up.
- A line the page draws in pieces (around kerning) came in as several text
objects ("swe", "ep"); the pieces that add up to one line of the file's
text object now join into one, with the line's spaces between them.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
618b1cf94f
commit
2dc738b816
|
|
@ -730,6 +730,85 @@ fn same_type(n: &Node, lines: &[(String, Point)]) -> Option<f64> {
|
|||
.min_by(f64::total_cmp)
|
||||
}
|
||||
|
||||
/// The page draws a line in pieces where its kerning or styles change ("swe", "ep"), and the file
|
||||
/// has it as one line of one text object: the pieces that on one baseline add up to a line of
|
||||
/// `lines` become one text (each keeping its own style, the line's spaces between them).
|
||||
fn join_pieces(parts: &mut Vec<Arc<Node>>, lines: &[(String, Point)]) {
|
||||
let text_of = |n: &Arc<Node>| if let NodeKind::Text(t) = &n.kind { Some(t.plain_text()) } else { None };
|
||||
let texts: Vec<usize> = parts.iter().enumerate().filter(|(_, n)| matches!(n.kind, NodeKind::Text(_))).map(|(i, _)| i).collect();
|
||||
let mut taken: Vec<usize> = vec![];
|
||||
// The first piece of each line to join, the others, and the spaces to put after each piece.
|
||||
let mut joins: Vec<(usize, Vec<usize>, Vec<String>)> = vec![];
|
||||
for &i in &texts {
|
||||
if taken.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
let Some(line) = parts.get(i).and_then(|n| baseline(n)) else { continue };
|
||||
let group: Vec<usize> = texts
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|j| !taken.contains(j) && parts.get(*j).and_then(|n| baseline(n)).is_some_and(|b| (b - line).abs() <= SAME_LINE))
|
||||
.collect();
|
||||
taken.extend(&group);
|
||||
let pieces: Vec<String> = group.iter().filter_map(|j| parts.get(*j).and_then(text_of)).collect();
|
||||
let places: Vec<(f64, f64)> = group
|
||||
.iter()
|
||||
.filter_map(|j| parts.get(*j))
|
||||
.filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some((t.xf.as_coeffs()[4], t.xf.as_coeffs()[5])) } else { None })
|
||||
.collect();
|
||||
let distinct = places.iter().enumerate().all(|(a, p)| places.iter().skip(a + 1).all(|q| (p.0 - q.0).hypot(p.1 - q.1) > 0.01));
|
||||
let joined: String = pieces.iter().map(|p| letters(p)).collect();
|
||||
if group.len() < 2 || !distinct || joined.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Some((whole, _)) = lines.iter().find(|(text, _)| letters(text) == joined) else { continue };
|
||||
// Where the line has spaces after a piece's last character that the page doesn't draw.
|
||||
let line_chars: Vec<char> = whole.chars().collect();
|
||||
let mut at = 0;
|
||||
let mut gaps = vec![];
|
||||
for (k, piece) in pieces.iter().enumerate() {
|
||||
for _ in piece.chars().filter(|c| !c.is_whitespace()) {
|
||||
while line_chars.get(at).is_some_and(|c| c.is_whitespace()) {
|
||||
at += 1;
|
||||
}
|
||||
at += 1;
|
||||
}
|
||||
let space: String = line_chars.iter().skip(at).take_while(|c| c.is_whitespace()).collect();
|
||||
let next = pieces.get(k + 1);
|
||||
let drawn = piece.ends_with(char::is_whitespace) || next.is_none_or(|n| n.starts_with(char::is_whitespace));
|
||||
gaps.push(if drawn { String::new() } else { space });
|
||||
}
|
||||
if at <= line_chars.len() {
|
||||
joins.push((group[0], group[1..].to_vec(), gaps));
|
||||
}
|
||||
}
|
||||
for (first, rest, gaps) in joins.iter().rev() {
|
||||
let mut runs: Vec<Vec<vectorcraft_doc::TextRun>> = [first]
|
||||
.into_iter()
|
||||
.chain(rest)
|
||||
.filter_map(|j| parts.get(*j))
|
||||
.filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(t.runs.clone()) } else { None })
|
||||
.collect();
|
||||
for (r, gap) in runs.iter_mut().zip(gaps) {
|
||||
if let Some(last) = r.last_mut() {
|
||||
last.text.push_str(gap);
|
||||
}
|
||||
}
|
||||
if let Some(node) = parts.get_mut(*first).map(Arc::make_mut)
|
||||
&& let NodeKind::Text(t) = &mut node.kind
|
||||
{
|
||||
t.runs = runs.into_iter().flatten().collect();
|
||||
}
|
||||
let mut gone: Vec<usize> = rest.clone();
|
||||
gone.sort_unstable_by(|a, b| b.cmp(a));
|
||||
for j in gone {
|
||||
if j < parts.len() {
|
||||
parts.remove(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The box round `parts`.
|
||||
fn extent(parts: &[Arc<Node>]) -> Option<Rect> {
|
||||
parts.iter().filter_map(|n| n.visual_bounds()).reduce(|a, b| a.union(b))
|
||||
|
|
@ -787,6 +866,11 @@ fn plan(
|
|||
None => spare.push(object.clone()),
|
||||
}
|
||||
}
|
||||
for (k, c) in content.iter_mut().enumerate() {
|
||||
if let (Content::Page(parts), Some(Some(l))) = (c, lines.get(k)) {
|
||||
join_pieces(parts, l);
|
||||
}
|
||||
}
|
||||
// Type of the text document where the page has none: hidden type, and shown type off the page.
|
||||
for (k, c) in content.iter_mut().enumerate() {
|
||||
if matches!(c, Content::Empty)
|
||||
|
|
|
|||
|
|
@ -46,3 +46,13 @@ fn reencoded_copies_of_a_font_name_the_font() {
|
|||
assert_eq!(crate::family_style("Odd*Name").0, "Odd*Name");
|
||||
assert_eq!(crate::family_style("*1").0, "*1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abbreviated_styles_in_postscript_names_are_spelled_out() {
|
||||
// A style word PostScript names shorten is the style the installed font names in full.
|
||||
assert_eq!(crate::family_style("AkzidenzGroteskPro-Md"), ("Akzidenz Grotesk Pro".to_string(), "Medium".to_string()));
|
||||
assert_eq!(crate::family_style("AkzidenzGroteskPro-XBdCnIt").1, "Extra Bold Condensed Italic");
|
||||
assert_eq!(crate::family_style("AkzidenzGroteskPro-LightCn").1, "Light Condensed");
|
||||
// Whole words stay as they were.
|
||||
assert_eq!(crate::family_style("Helvetica-BoldOblique").1, "Bold Oblique");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -417,9 +417,12 @@ fn a_line_the_page_draws_in_pieces_goes_to_its_text_object_whole() {
|
|||
let art = native_with_text(&layer("Art", true, &text_object(0)), &doc);
|
||||
let page = "0 0 0 1 setcmykcolor /Helvetica findfont 14 scalefont setfont 58.5 61.5 moveto (Hi) show 70 61.5 moveto (there) show";
|
||||
let r = import(&eps(page, &art)).unwrap();
|
||||
// One text, with the line's space between the pieces.
|
||||
let group = &r.document.layers[0].children().unwrap()[0];
|
||||
assert_eq!(kinds(group), ["text", "text"], "{:?}", r.warnings);
|
||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
assert_eq!(kinds(group), ["text"], "{:?}", r.warnings);
|
||||
let texts: Vec<String> =
|
||||
group.children().unwrap().iter().filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(t.plain_text()) } else { None }).collect();
|
||||
assert_eq!(texts, ["Hi there"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -18,6 +18,27 @@ use super::obj::{Dict, DictRef, Key, Obj, Op, PsError, Res, ps_err};
|
|||
/// The font a program uses before it sets one.
|
||||
const DEFAULT_FONT: &str = "Helvetica";
|
||||
|
||||
/// A word of a style as PostScript names abbreviate it (`AkzidenzGroteskPro-BoldCnIt`), in full.
|
||||
fn style_word(w: &str) -> String {
|
||||
match w {
|
||||
"Lt" => "Light",
|
||||
"Md" | "Med" => "Medium",
|
||||
"Bd" => "Bold",
|
||||
"XBd" => "Extra Bold",
|
||||
"Blk" => "Black",
|
||||
"Hv" => "Heavy",
|
||||
"Th" => "Thin",
|
||||
"Rg" => "Regular",
|
||||
"Sb" | "Smbd" => "Semibold",
|
||||
"Cn" | "Cond" => "Condensed",
|
||||
"Ext" | "Ex" => "Extended",
|
||||
"It" => "Italic",
|
||||
"Obl" => "Oblique",
|
||||
w => w,
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// A font's family and style from its PostScript name: `Helvetica-BoldOblique` → (`Helvetica`,
|
||||
/// `Bold Oblique`), `TimesNewRomanPS-BoldMT` → (`Times New Roman`, `Bold`), without a subset
|
||||
/// prefix (`ABCDEF+`) or the `*1` of a re-encoded copy (`ABCDEF+RollerBabyBV*1`).
|
||||
|
|
@ -47,7 +68,7 @@ pub fn family_style(name: &str) -> (String, String) {
|
|||
};
|
||||
let style = match trim(style).as_str() {
|
||||
"" | "Roman" | "Regular" | "Book" | "Normal" => "Regular".to_string(),
|
||||
s => s.to_string(),
|
||||
s => s.split(' ').map(style_word).collect::<Vec<_>>().join(" "),
|
||||
};
|
||||
(trim(family), style)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue