mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 21:17:48 +00:00
EPS import: charpath, cshow, kshow and glyphshow read named CIDFont type by its CMap
- `charpath` in a CIDFont only named, over a predefined CMap, adds the outlines of the characters its codes stand for, placed as `show` sets them, and moves the point as `show` does. It added no outlines. - `cshow` gives its procedure each code of such type, two bytes for a two-byte code, with the code's width. It split two-byte codes into single bytes. - `glyphshow` sets the named character itself. It set the character's UTF-8 bytes as Latin-1 letters. - `kshow` runs its procedure between each two codes of such type, with the two codes' values. It didn't run it. The procedure sees the font the file set as current, and a font it sets stays current. - A name that ends in the row-and-cell CMap `H` or `V` splits off the CMap only when the file defines the CIDFont of that name (as a resource or with `definefont`), or the name has a hyphen and an installed face that covers Japanese has it. `Foo-Bold-H` stays a font of the whole name. - In a CMap that can't be read (`GBT-H`), each code moves the point one em, along the line or down the column; a tab or carriage return byte is a code of its own. - One parser reads the names of the predefined Unicode CMaps, for EPS and legacy `.ai` type. - Outlines added to a path by `charpath` stop at the path's point limit. - docs/file-format-parity.md, docs/gaps.md and docs/mcp.md describe type in named CIDFonts over the predefined CMaps.
This commit is contained in:
@@ -18,7 +18,6 @@ use vectorcraft_geom::BezPath;
|
||||
|
||||
use super::interp::{Interp, matrix_obj};
|
||||
use super::obj::{Dict, DictRef, Key, Obj, Op, PsError, Res, Shared, ps_err};
|
||||
use super::predefined_cmap::Unicode;
|
||||
|
||||
/// Most codespace ranges, character and notdef mappings a CMap may hold.
|
||||
const MAX_RANGES: usize = 1 << 16;
|
||||
@@ -211,32 +210,6 @@ fn number(data: &[u8], at: usize, n: usize) -> Option<u64> {
|
||||
(n <= 8).then(|| bytes.iter().fold(0u64, |v, b| v << 8 | u64::from(*b)))
|
||||
}
|
||||
|
||||
/// The encoding of predefined Unicode CMap `name` and whether it is vertical: one of Adobe's
|
||||
/// public CMap resources whose name says its codes are Unicode: `Uni` and a character
|
||||
/// collection, then `-UCS2`, `-UTF8`, `-UTF16` or `-UTF32` (and `-HW` for half-width forms),
|
||||
/// then `-H` or `-V` (`UniJIS-UCS2-H`, `UniGB-UTF16-V`:
|
||||
/// <https://github.com/adobe-type-tools/cmap-resources>).
|
||||
pub(super) fn unicode_cmap(name: &str) -> Option<(Unicode, bool)> {
|
||||
let (rest, dir) = name.rsplit_once('-')?;
|
||||
let vertical = match dir {
|
||||
"H" => false,
|
||||
"V" => true,
|
||||
_ => return None,
|
||||
};
|
||||
let rest = rest.strip_suffix("-HW").unwrap_or(rest);
|
||||
let (collection, form) = rest.rsplit_once('-')?;
|
||||
if !(collection.len() > 3 && collection.starts_with("Uni") && !collection.contains('-')) {
|
||||
return None;
|
||||
}
|
||||
let form = match form {
|
||||
"UCS2" | "UTF16" => Unicode::Utf16,
|
||||
"UTF8" => Unicode::Utf8,
|
||||
"UTF32" => Unicode::Utf32,
|
||||
_ => return None,
|
||||
};
|
||||
Some((form, vertical))
|
||||
}
|
||||
|
||||
/// A predefined Identity CMap: two-byte codes, each the CID of its own value; vertical for
|
||||
/// `Identity-V`.
|
||||
pub(super) fn identity(name: &str, vertical: bool) -> Obj {
|
||||
@@ -396,11 +369,9 @@ impl Interp<'_> {
|
||||
return Ok(d.clone());
|
||||
}
|
||||
let text = name.text().map(|t| t.to_string()).unwrap_or_default();
|
||||
// Another predefined CMap the file doesn't define, without its mappings: its codes are
|
||||
// read by its name when type is set (see `text`).
|
||||
let predefined = unicode_cmap(&text)
|
||||
.map(|(_, vertical)| vertical)
|
||||
.or_else(|| super::predefined_cmap::known(&text).then(|| super::predefined_cmap::vertical(&text)));
|
||||
// A predefined CMap the file doesn't define, without its mappings: its codes are read by
|
||||
// its name when type is set (see `text`).
|
||||
let predefined = super::predefined_cmap::known(&text).then(|| super::predefined_cmap::vertical(&text));
|
||||
match predefined {
|
||||
Some(vertical) => {
|
||||
let mut d = Dict::new();
|
||||
|
||||
@@ -95,6 +95,9 @@ pub(crate) struct Interp<'a> {
|
||||
/// The predefined CMaps type was set in so far, by name, with how their strings decode
|
||||
/// (`None` when one can't be read; see `text`).
|
||||
pub predefined_cmaps: Vec<(Rc<str>, Option<Rc<super::predefined_cmap::CMapCodes>>)>,
|
||||
/// The font a `kshow` procedure sees as current while type in a CIDFont only named is set in
|
||||
/// a font of its own (see `text`).
|
||||
pub kshow_font: Option<DictRef>,
|
||||
pub g: GState,
|
||||
pub saved: Vec<GState>,
|
||||
pub out: Out,
|
||||
@@ -236,6 +239,7 @@ impl<'a> Interp<'a> {
|
||||
cid_subrs: vec![],
|
||||
cmap_font: 0,
|
||||
predefined_cmaps: vec![],
|
||||
kshow_font: None,
|
||||
type42_counted: HashSet::new(),
|
||||
g,
|
||||
saved: vec![],
|
||||
|
||||
@@ -157,7 +157,7 @@ pub(super) enum Unicode {
|
||||
/// The encoding of predefined Unicode CMap `name`: `Uni` and a character collection, then `-UCS2`,
|
||||
/// `-UTF8`, `-UTF16` or `-UTF32` (and `-HW` for half-width forms), then `-H` or `-V`
|
||||
/// (`UniJIS-UCS2-H`, `UniGB-UTF16-V`: <https://github.com/adobe-type-tools/cmap-resources>).
|
||||
fn unicode_cmap(name: &str) -> Option<Unicode> {
|
||||
pub(super) fn unicode_cmap(name: &str) -> Option<Unicode> {
|
||||
let rest = name.strip_suffix("-H").or_else(|| name.strip_suffix("-V"))?;
|
||||
let rest = rest.strip_suffix("-HW").unwrap_or(rest);
|
||||
let (collection, form) = rest.rsplit_once('-')?;
|
||||
@@ -325,6 +325,28 @@ pub(super) fn known(cmap: &str) -> bool {
|
||||
unicode_cmap(cmap).is_some() || predefined(cmap).is_some() || vertical_of_predefined() || other()
|
||||
}
|
||||
|
||||
/// The codes of string `bytes` in predefined CMap `cmap` when its mappings can't be read, each
|
||||
/// standing for no character: as long as the code space range it falls in ([`code_space`]; a byte
|
||||
/// that starts no code is a code of its own), or two bytes when the code space isn't listed (the
|
||||
/// row-and-cell codes of `GBT-H` and `CNS2-H`). A 0x0D or 0x09 byte is always a code of its own.
|
||||
pub(super) fn unread_codes(cmap: &str, bytes: &[u8]) -> Decoded {
|
||||
let space = code_space(cmap);
|
||||
let mut out = Decoded::default();
|
||||
let mut at = 0;
|
||||
while let Some(rest) = bytes.get(at..).filter(|r| !r.is_empty()) {
|
||||
let len = match space {
|
||||
// A 0x0D or 0x09 byte is a code of its own, as [`CMapCodes::each_code`] reads it.
|
||||
_ if rest.first().and_then(|&b| control(b)).is_some() => 1,
|
||||
Some(space) => code_len(space, rest).map_or(1, usize::from),
|
||||
None => 2,
|
||||
};
|
||||
let code = rest.get(..len).unwrap_or(rest);
|
||||
out.push(code.iter().fold(0u32, |c, &b| c << 8 | u32::from(b)), None);
|
||||
at += code.len();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Whether predefined CMap `cmap` is a vertical CMap (`-V`, or the row-and-cell `V`).
|
||||
pub(super) fn vertical(cmap: &str) -> bool {
|
||||
cmap == "V" || cmap.ends_with("-V")
|
||||
|
||||
@@ -426,8 +426,6 @@ fn named_cidfonts_over_predefined_cmaps_set_their_characters_as_type() {
|
||||
("83pv-RKSJ-H", "93fa967b41", "日本A"),
|
||||
("90ms-RKSJ-H", "93fa", "日"),
|
||||
("EUC-H", "c6fc", "日"),
|
||||
// JIS row and cell: 日 is 0x467C.
|
||||
("H", "467c", "日"),
|
||||
("GBK-EUC-H", "d6d0", "中"),
|
||||
("B5pc-H", "a4a4", "中"),
|
||||
("ETen-B5-H", "a4a4", "中"),
|
||||
@@ -448,6 +446,9 @@ fn named_cidfonts_over_predefined_cmaps_set_their_characters_as_type() {
|
||||
}
|
||||
let r = super::tests::read("/X /GBK-EUC-H [/NotInFile] composefont 20 scalefont setfont 10 20 moveto <d6d041> show");
|
||||
assert_eq!(texts(&r).iter().map(|t| t.plain_text()).collect::<String>(), "中A");
|
||||
// JIS row and cell: 日 is 0x467C.
|
||||
let r = super::tests::read("/X /H [/Ryumin-Light] composefont 20 scalefont setfont 10 20 moveto <467c> show");
|
||||
assert_eq!(texts(&r).iter().map(|t| t.plain_text()).collect::<String>(), "日");
|
||||
// The point moves past the type, along the line.
|
||||
check("/Ryumin-Light-90ms-RKSJ-H findfont 20 scalefont setfont <93fa967b> stringwidth abs 0.01 lt exch dup 5 gt exch 60 lt and and");
|
||||
}
|
||||
@@ -478,8 +479,34 @@ fn named_cidfonts_over_predefined_cmaps_take_the_show_operators_spacing() {
|
||||
measured(&format!(
|
||||
"{gb} 10 20 moveto <56505650> show currentpoint pop 10 20 moveto 7 0 16#5650 <56505650> widthshow currentpoint pop sub -14 sub abs 0.01 lt"
|
||||
));
|
||||
// `xshow` moves 30 a code.
|
||||
// `awidthshow` adds 5 to each code and 7 more to the space.
|
||||
measured(&format!(
|
||||
"{set} 10 20 moveto <93fa20967b> show currentpoint pop 10 20 moveto 7 0 32 5 0 <93fa20967b> awidthshow currentpoint pop sub -22 sub abs 0.01 lt"
|
||||
));
|
||||
// `xshow` moves 30 a code, `yshow` 10 up a code, `xyshow` both.
|
||||
measured(&format!("{set} 10 20 moveto <93fa967b> [30 30] xshow currentpoint pop 70 sub abs 0.01 lt"));
|
||||
measured(&format!("{set} 10 20 moveto <93fa967b> [10 10] yshow currentpoint 40 sub abs 0.01 lt exch 10 sub abs 0.01 lt and"));
|
||||
measured(&format!("{set} 10 20 moveto <93fa967b> [30 10 30 10] xyshow currentpoint 40 sub abs 0.01 lt exch 70 sub abs 0.01 lt and"));
|
||||
// `kshow` runs its procedure between each two codes: it moves the point 100 more here, and a
|
||||
// font it sets stays current.
|
||||
measured(&format!(
|
||||
"{set} 10 20 moveto <93fa967b> show currentpoint 10 20 moveto {{pop pop 100 0 rmoveto}} <93fa967b> kshow currentpoint \
|
||||
3 -1 roll sub abs 0.01 lt 3 1 roll sub -100 sub abs 0.01 lt and"
|
||||
));
|
||||
measured(&format!(
|
||||
"{set} 10 20 moveto {{pop pop /Helvetica findfont 10 scalefont setfont}} <93fa967b> kshow currentfont /FontName get /Helvetica eq"
|
||||
));
|
||||
// The procedure sees the font the file set as current, and the next one the font an earlier
|
||||
// one set.
|
||||
measured(&format!("{set} 10 20 moveto {{pop pop currentfont /FontName get /seen exch def}} <93fa967b> kshow seen /Ryumin-Light-90ms-RKSJ-H eq"));
|
||||
measured(&format!(
|
||||
"{set} 10 20 moveto /n 0 def {{pop pop /n n 1 add def n 1 eq {{/Helvetica findfont 10 scalefont setfont}} {{currentfont /FontName get /seen exch def}} ifelse}} \
|
||||
<93fa967b41> kshow seen /Helvetica eq"
|
||||
));
|
||||
// The procedure gets the two codes' values (left on the stack here).
|
||||
measured(&format!(
|
||||
"{set} 10 20 moveto [ {{}} <93fa967b41> kshow ] dup length 4 eq exch aload pop 65 eq exch 16#967b eq and exch 16#967b eq and exch 16#93fa eq and and"
|
||||
));
|
||||
// Down the column: `ashow`'s 5 down each code.
|
||||
let set = "/Ryumin-Light-90ms-RKSJ-V findfont 20 scalefont setfont";
|
||||
measured(&format!(
|
||||
@@ -530,10 +557,15 @@ fn named_cid_type_is_set_alike_by_every_show_operator() {
|
||||
let r = super::tests::read(&format!("{set} <00660069> {show}"));
|
||||
let t = texts(&r);
|
||||
assert_eq!(t.iter().map(|t| t.plain_text()).collect::<Vec<_>>(), ["fi"], "{show}");
|
||||
t[0].runs.iter().map(|r| r.style.features.clone()).collect::<Vec<_>>()
|
||||
// Spacing that differs from the advances splits the type into runs of the same features.
|
||||
let mut features = t[0].runs.iter().map(|r| r.style.features.clone()).collect::<Vec<_>>();
|
||||
features.dedup();
|
||||
features
|
||||
};
|
||||
let plain = features("show");
|
||||
assert_eq!(plain, features("0 0 3 -1 roll ashow"));
|
||||
for show in ["0 0 3 -1 roll ashow", "0 0 32 4 -1 roll widthshow", "0 0 32 0 0 6 -1 roll awidthshow", "[25 25] xshow", "{pop pop} exch kshow"] {
|
||||
assert_eq!(plain, features(show), "{show}");
|
||||
}
|
||||
assert!(plain.iter().all(|f| f.iter().any(|f| f == "-liga")), "{plain:?}");
|
||||
check(&format!(
|
||||
"gsave nulldevice {set} <00660069> stringwidth 10 20 moveto 0 0 <00660069> ashow currentpoint grestore \
|
||||
@@ -541,17 +573,77 @@ fn named_cid_type_is_set_alike_by_every_show_operator() {
|
||||
));
|
||||
}
|
||||
|
||||
/// A font name ending in the row-and-cell CMap `H` after a name without a hyphen stays a font of
|
||||
/// the whole name, its strings read byte by byte. Type in a predefined CMap whose mappings aren't
|
||||
/// available is left out with a warning, and a long string in a CID-keyed font over a predefined
|
||||
/// CMap shown in a loop runs out of the budget.
|
||||
/// `charpath`, `cshow` and `glyphshow` in a CID-keyed font over a predefined CMap, its CIDFont only
|
||||
/// named: `charpath` adds the outlines of the characters the codes stand for and moves the point
|
||||
/// as `show` does; `cshow`'s procedure gets each code (two bytes for a two-byte code) with its
|
||||
/// width; `glyphshow` sets the named character itself.
|
||||
#[test]
|
||||
fn named_cid_type_in_charpath_cshow_and_glyphshow() {
|
||||
let set = "/Ryumin-Light-90ms-RKSJ-H findfont 20 scalefont setfont";
|
||||
let r = super::tests::read(&format!("{set} 10 20 moveto <4142> true charpath fill"));
|
||||
assert!(texts(&r).is_empty(), "{:?}", all(&r.document));
|
||||
assert!(all(&r.document).iter().any(|n| matches!(n.kind, NodeKind::Path { .. })), "{:?}", all(&r.document));
|
||||
check(&format!(
|
||||
"gsave nulldevice {set} 10 20 moveto <93fa41> show currentpoint newpath 10 20 moveto <93fa41> false charpath currentpoint grestore \
|
||||
3 -1 roll sub abs 0.01 lt 3 1 roll sub abs 0.01 lt and"
|
||||
));
|
||||
// Down a column a code that stands for no character (0xFD) moves the outlines one em, as it
|
||||
// moves the type `show` sets.
|
||||
let top = |s: &str| format!("newpath 10 100 moveto <{s}> false charpath pathbbox 4 1 roll pop pop pop");
|
||||
check(&format!(
|
||||
"gsave nulldevice /Ryumin-Light-90ms-RKSJ-V findfont 20 scalefont setfont {} {} grestore sub -20 sub abs 0.5 lt",
|
||||
top("fd41"),
|
||||
top("41")
|
||||
));
|
||||
// The procedure leaves each code: 日 (0x93FA), then A.
|
||||
check(&format!("{set} [ {{pop pop}} <93fa41> cshow ] dup length 2 eq exch dup 0 get 16#93fa eq exch 1 get 65 eq and and"));
|
||||
check(&format!("{set} 0 {{pop exch pop add}} <93fa967b41> cshow <93fa967b41> stringwidth pop sub abs 0.01 lt"));
|
||||
let r = super::tests::read(&format!("{set} 10 20 moveto /uni00E9 glyphshow"));
|
||||
assert_eq!(texts(&r).iter().map(|t| t.plain_text()).collect::<Vec<_>>(), ["é"]);
|
||||
}
|
||||
|
||||
/// A font name ending in the row-and-cell CMap `H` or `V` stays a font of the whole name, its
|
||||
/// strings read byte by byte, unless the part before the CMap names a CIDFont the file defines (or
|
||||
/// an installed face that covers Japanese). Type in a predefined CMap whose mappings aren't
|
||||
/// available is left out with a warning, each code moving the point one em, and a long string in a
|
||||
/// CID-keyed font over a predefined CMap shown in a loop runs out of the budget.
|
||||
#[test]
|
||||
fn latin_names_ending_in_h_and_cmaps_that_cannot_be_read() {
|
||||
let r = super::tests::read("/Futura-H findfont 20 scalefont setfont 10 20 moveto (AB) show");
|
||||
assert_eq!(texts(&r).iter().map(|t| t.plain_text()).collect::<Vec<_>>(), ["AB"]);
|
||||
for (font, family, style) in [("Futura-H", "Futura", "H"), ("Foo-Bold-H", "Foo", "Bold-H"), ("Foo-Condensed-V", "Foo", "Condensed-V")] {
|
||||
let r = super::tests::read(&format!("/{font} findfont 20 scalefont setfont 10 20 moveto (AB) show"));
|
||||
let t = texts(&r);
|
||||
assert_eq!(t.iter().map(|t| t.plain_text()).collect::<Vec<_>>(), ["AB"], "{font}");
|
||||
let s = &t[0].runs[0].style;
|
||||
assert_eq!((s.font_family.as_str(), s.font_style.as_str()), (family, style), "{font}");
|
||||
assert!(!t[0].vertical, "{font}");
|
||||
}
|
||||
// The file defines the CIDFont `Ryumin-Light`, as a resource or as a font: `Ryumin-Light-H` is
|
||||
// it over the CMap `H`.
|
||||
// A CIDFont name without a hyphen too, when the file defines it.
|
||||
let cid_font = |name: &str| format!("/{name} << /CIDFontName /{name} /CIDFontType 0 /FontType 9 /FontMatrix [0.001 0 0 0.001 0 0] >>");
|
||||
let defines = [
|
||||
format!("{} /CIDFont defineresource pop /Ryumin-Light-H", cid_font("Ryumin-Light")),
|
||||
format!("{} definefont pop /Ryumin-Light-H", cid_font("Ryumin-Light")),
|
||||
format!("{} /CIDFont defineresource pop /Foo-H", cid_font("Foo")),
|
||||
];
|
||||
for define in defines {
|
||||
let r = super::tests::read(&format!("{define} findfont 20 scalefont setfont 10 20 moveto <467c> show"));
|
||||
assert_eq!(texts(&r).iter().map(|t| t.plain_text()).collect::<Vec<_>>(), ["日"], "{define}: {:?}", r.warnings);
|
||||
}
|
||||
let r = super::tests::read("/Ryumin-Light-GBT-H findfont 20 scalefont setfont 10 20 moveto (AB) show 0 0 10 10 rectfill");
|
||||
assert!(texts(&r).is_empty(), "{:?}", all(&r.document));
|
||||
assert_eq!(super::tests::program_warnings(&r.warnings), [super::text::UNREAD_CMAP], "{:?}", r.warnings);
|
||||
// Two two-byte codes: two ems along the line, or down the column (a square drawn when the
|
||||
// point moved that far).
|
||||
for cond in [
|
||||
"/Ryumin-Light-GBT-H findfont 20 scalefont setfont <21212121> stringwidth abs 0.01 lt exch 40 sub abs 0.01 lt and",
|
||||
"/Ryumin-Light-GBT-V findfont 20 scalefont setfont 10 100 moveto <21212121> show currentpoint 60 sub abs 0.01 lt exch 10 sub abs 0.01 lt and",
|
||||
// A tab is a code of its own: three codes.
|
||||
"/Ryumin-Light-GBT-H findfont 20 scalefont setfont <09092121> stringwidth abs 0.01 lt exch 60 sub abs 0.01 lt and",
|
||||
] {
|
||||
let r = super::tests::read(&format!("{cond} {{ 0 0 10 10 rectfill }} if"));
|
||||
assert!(all(&r.document).iter().any(|n| matches!(n.kind, NodeKind::Path { .. })), "{cond}");
|
||||
}
|
||||
super::tests::runs_out_of_budget("/Ryumin-Light-90ms-RKSJ-H findfont 20 scalefont setfont { 100000 string stringwidth pop pop } loop");
|
||||
}
|
||||
|
||||
|
||||
+213
-79
@@ -171,8 +171,8 @@ impl Interp<'_> {
|
||||
ResourceForAll => self.resource_for_all()?,
|
||||
Show | AShow | WidthShow | AWidthShow | XShow | YShow | XYShow | KShow => {
|
||||
let (s, spacing) = self.show_operands(op)?;
|
||||
if let Some((codes, font, vertical)) = self.named_cid_type(&s)? {
|
||||
self.show_named_cid(&codes, &font, vertical, &spacing)?;
|
||||
if let Some(named) = self.named_cid_type(&s)? {
|
||||
self.show_named_cid(&named, &spacing, Draw::Fill)?;
|
||||
} else if let Some(glyphs) = self.composite_glyphs(&s)? {
|
||||
self.show_composite(&glyphs, &spacing)?;
|
||||
} else {
|
||||
@@ -186,6 +186,23 @@ impl Interp<'_> {
|
||||
// The procedure shows or places each character: it gets its code and width.
|
||||
let s = self.pop_str()?.to_vec();
|
||||
let proc = self.pop_proc()?;
|
||||
// In a CIDFont only named, a code may be several bytes: the procedure gets each
|
||||
// code's value.
|
||||
if let Some(named) = self.named_cid_type(&s)? {
|
||||
let mut chars = named.codes.text.chars();
|
||||
for &(code, n) in &named.codes.codes {
|
||||
let mut one = Decoded::default();
|
||||
one.push(code, chars.by_ref().take(n));
|
||||
let v = self.named_cid_width(&named, &one)?;
|
||||
self.push(Obj::Int(i64::from(code)))?;
|
||||
self.push_num(v.x)?;
|
||||
self.push_num(v.y)?;
|
||||
if !self.body(&proc)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
for b in s {
|
||||
let v = self.width_of(&[b])?;
|
||||
self.push(Obj::Int(i64::from(b)))?;
|
||||
@@ -208,7 +225,14 @@ impl Interp<'_> {
|
||||
}
|
||||
None => {
|
||||
let ch = glyph_char(&name);
|
||||
self.show(ch.encode_utf8(&mut [0; 4]).as_bytes())?;
|
||||
// In a CIDFont only named, the character is set as it is, not read through
|
||||
// the CMap.
|
||||
if let Some(mut named) = self.named_cid_font()? {
|
||||
named.codes.push(0, Some(ch).filter(|c| !c.is_control()));
|
||||
self.show_named_cid(&named, &Spacing::Plain, Draw::Fill)?;
|
||||
} else {
|
||||
self.show(ch.encode_utf8(&mut [0; 4]).as_bytes())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,11 +307,8 @@ impl Interp<'_> {
|
||||
|
||||
/// How far `s` moves the current point (user space).
|
||||
fn width_of(&mut self, s: &[u8]) -> Res<Vec2> {
|
||||
if let Some((codes, font, vertical)) = self.named_cid_type(s)? {
|
||||
let saved = self.g.font.replace(font);
|
||||
let r = self.named_cid_width(&codes, vertical);
|
||||
self.g.font = saved;
|
||||
return r;
|
||||
if let Some(named) = self.named_cid_type(s)? {
|
||||
return self.named_cid_width(&named, &named.codes);
|
||||
}
|
||||
if let Some(glyphs) = self.composite_glyphs(s)? {
|
||||
let mut w = Vec2::ZERO;
|
||||
@@ -399,25 +420,23 @@ impl Interp<'_> {
|
||||
/// is a predefined CMap, the CID-keyed font of those two (the usual name of a CID-keyed font,
|
||||
/// as `composefont` makes them: PLRM 3rd ed., chapter 8; the CMaps are Adobe's public CMap
|
||||
/// resources, <https://github.com/adobe-type-tools/cmap-resources>); else a font of that name.
|
||||
/// A name an installed face has is that face's, and a name split at a bare `H` or `V` (the
|
||||
/// row-and-cell CMaps) needs a CIDFont name with a hyphen in it (`Ryumin-Light-H`), so
|
||||
/// `Futura-H` stays a font of that name.
|
||||
/// A name that ends in a predefined CMap other than a Unicode CMap and that an installed face
|
||||
/// has is that face's ([`Self::predefined_split`]).
|
||||
fn find_font(&mut self, k: &Obj) -> Res<DictRef> {
|
||||
let key = k.key().ok_or(PsError::Ps("typecheck", "findfont".into()))?;
|
||||
if let Some(Obj::Dict(d)) = self.fonts.borrow().get(&key) {
|
||||
return Ok(d.clone());
|
||||
}
|
||||
let text = k.text().map(|t| t.to_string()).unwrap_or_default();
|
||||
let split = text
|
||||
let unicode = text
|
||||
.match_indices('-')
|
||||
.map(|(i, _)| i)
|
||||
.find(|&i| i > 0 && super::cid::unicode_cmap(text.get(i + 1..).unwrap_or_default()).is_some())
|
||||
.and_then(|i| Some((text.get(..i)?, text.get(i + 1..)?)))
|
||||
.or_else(|| {
|
||||
let (cid_font, cmap) = super::predefined_cmap::composite_font(&text)?;
|
||||
let installed = vectorcraft_text::FontDb::global().find_postscript(&text).is_some();
|
||||
(!installed && !(matches!(cmap, "H" | "V") && !cid_font.contains('-'))).then_some((cid_font, cmap))
|
||||
});
|
||||
.find(|&i| i > 0 && super::predefined_cmap::unicode_cmap(text.get(i + 1..).unwrap_or_default()).is_some())
|
||||
.and_then(|i| Some((text.get(..i)?, text.get(i + 1..)?)));
|
||||
let split = match unicode {
|
||||
Some(split) => Some(split),
|
||||
None => self.predefined_split(&text),
|
||||
};
|
||||
if let Some((cid_font, cmap)) = split {
|
||||
let cmap = self.find_cmap(&Obj::name(cmap))?;
|
||||
let dep = self.find_cid_font(&Obj::name(cid_font))?;
|
||||
@@ -434,6 +453,32 @@ impl Interp<'_> {
|
||||
Ok(font)
|
||||
}
|
||||
|
||||
/// Font name `name` as a CIDFont's name and the name of a predefined CMap other than a Unicode
|
||||
/// CMap ([`super::predefined_cmap::composite_font`]), unless an installed face has the whole
|
||||
/// name. A name that ends in the row-and-cell CMap `H` or `V` splits only when the part before
|
||||
/// it names a CIDFont the file defines, or has a hyphen and names an installed face that covers
|
||||
/// Japanese; otherwise it stays a font of the whole name (`Futura-H`, `Foo-Bold-H`), its
|
||||
/// strings read byte by byte.
|
||||
fn predefined_split<'a>(&mut self, name: &'a str) -> Option<(&'a str, &'a str)> {
|
||||
let (cid_font, cmap) = super::predefined_cmap::composite_font(name)?;
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
if db.find_postscript(name).is_some() {
|
||||
return None;
|
||||
}
|
||||
if matches!(cmap, "H" | "V") {
|
||||
let key = Key::name(cid_font);
|
||||
// As a CIDFont resource, or as a font (`definefont`), as `composefont` finds it.
|
||||
let cid_font_in = |d: &DictRef| matches!(d.borrow().get(&key), Some(Obj::Dict(f)) if f.borrow().contains_key(&Key::name("CIDFontType")));
|
||||
let defined = self.instances(&Obj::name("CIDFont")).is_ok_and(|d| d.borrow().contains_key(&key)) || cid_font_in(&self.fonts);
|
||||
// 日 is in the row-and-cell CMaps' character set (JIS X 0208).
|
||||
let installed = || db.find_postscript(cid_font).is_some_and(|face| face.covers('日'));
|
||||
if !(defined || (cid_font.contains('-') && installed())) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some((cid_font, cmap))
|
||||
}
|
||||
|
||||
/// The current font's name and its matrix for a font of one unit (user space).
|
||||
fn font(&mut self) -> Res<(String, Affine)> {
|
||||
let font = match self.g.font.clone() {
|
||||
@@ -575,15 +620,25 @@ impl Interp<'_> {
|
||||
codes.push(u32::from(code), c.filter(|c| !c.is_control()));
|
||||
}
|
||||
let widths: Option<Vec<f64>> = plain.map(|(_, glyphs)| glyphs.iter().map(|g| g.width).collect());
|
||||
self.show_codes(&codes, widths.as_deref(), spacing, false, s)
|
||||
self.show_codes(&codes, widths.as_deref(), spacing, false, s, Draw::Fill)
|
||||
}
|
||||
|
||||
/// The codes `codes` (each standing for some of its characters, or none) shown in the current
|
||||
/// font as [`Self::show_plain`] shows a string's, spaced as `spacing` says, each code `widths`
|
||||
/// wide when given (in glyph space, one unit an em), else as wide as its characters' advances;
|
||||
/// as vertical type when `vertical`, the codes moving the point down the column. `kern` is the
|
||||
/// string `kshow` gives its procedure the codes of.
|
||||
fn show_codes(&mut self, codes: &Decoded, widths: Option<&[f64]>, spacing: &Spacing, vertical: bool, kern: &[u8]) -> Res {
|
||||
/// as vertical type when `vertical`, the codes moving the point down the column. `kern` holds
|
||||
/// the codes `kshow` gives its procedure, one for each code. With `draw` [`Draw::Path`] the
|
||||
/// type's glyph outlines are added to the current path, placed as the type would be
|
||||
/// (`charpath`).
|
||||
fn show_codes<C: Copy + Into<i64>>(
|
||||
&mut self,
|
||||
codes: &Decoded,
|
||||
widths: Option<&[f64]>,
|
||||
spacing: &Spacing,
|
||||
vertical: bool,
|
||||
kern: &[C],
|
||||
draw: Draw,
|
||||
) -> Res {
|
||||
let at = self.user_point()?;
|
||||
let (_, f1) = self.font()?;
|
||||
let text = &codes.text;
|
||||
@@ -627,8 +682,9 @@ impl Interp<'_> {
|
||||
return ps_err("undefinedresult", "a show");
|
||||
}
|
||||
let across = moves.iter().any(|m| m.2.abs() > size * 1e-3);
|
||||
// Nothing shows on the null device (`nulldevice`, a Type 3 glyph only measured).
|
||||
let painted = !self.g.paint.is_none() && !self.g.null;
|
||||
// Nothing shows on the null device (`nulldevice`, a Type 3 glyph only measured);
|
||||
// `charpath` adds the outlines there too.
|
||||
let painted = draw == Draw::Path || (!self.g.paint.is_none() && !self.g.null);
|
||||
if across {
|
||||
// Moves across the baseline (`yshow`, `xyshow`): each character where the file puts it.
|
||||
let mut pen = at;
|
||||
@@ -637,8 +693,12 @@ impl Interp<'_> {
|
||||
let c: String = chars.by_ref().take(n).collect();
|
||||
if painted && !c.trim().is_empty() {
|
||||
let (t, ..) = self.set_text_in(&c, pen, vertical)?;
|
||||
let clips = self.g.clips.clone();
|
||||
self.out.push(Node::new(NodeId(0), NodeKind::Text(Box::new(t))), &clips);
|
||||
if draw == Draw::Path {
|
||||
self.type_path(&t)?;
|
||||
} else {
|
||||
let clips = self.g.clips.clone();
|
||||
self.out.push(Node::new(NodeId(0), NodeKind::Text(Box::new(t))), &clips);
|
||||
}
|
||||
}
|
||||
pen += *d;
|
||||
}
|
||||
@@ -686,27 +746,64 @@ impl Interp<'_> {
|
||||
if lead != Vec2::ZERO {
|
||||
t.xf = Affine::translate(self.xf() * Point::new(lead.x, lead.y) - self.xf() * Point::ZERO) * t.xf;
|
||||
}
|
||||
let clips = self.g.clips.clone();
|
||||
if self.out.push(Node::new(NodeId(0), NodeKind::Text(Box::new(t))), &clips).is_some()
|
||||
&& let Some(after) = self.out.drawn.get_mut(start..)
|
||||
{
|
||||
after.rotate_right(1);
|
||||
if draw == Draw::Path {
|
||||
self.type_path(&t)?;
|
||||
} else {
|
||||
let clips = self.g.clips.clone();
|
||||
if self.out.push(Node::new(NodeId(0), NodeKind::Text(Box::new(t))), &clips).is_some()
|
||||
&& let Some(after) = self.out.drawn.get_mut(start..)
|
||||
{
|
||||
after.rotate_right(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.g.cur = Some(self.xf() * (at + total));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `kshow` between character `i` of `s` and the next: its procedure run with the two codes
|
||||
/// and the current point at user point `pen` (PLRM 3rd ed., `kshow`: the procedure may
|
||||
/// change the current point, as kerning does) → how far it moved the point (user space).
|
||||
/// The glyph outlines of point type `t` added to the current path (`charpath`).
|
||||
fn type_path(&mut self, t: &TextObject) -> Res {
|
||||
let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
|
||||
for g in &layout.glyphs {
|
||||
let mut o = g.outline.clone();
|
||||
o.apply_affine(t.xf);
|
||||
self.extend_path(o)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Glyph outline `o` added to the current path, unless the path would pass its most points.
|
||||
fn extend_path(&mut self, o: vectorcraft_geom::BezPath) -> Res {
|
||||
if self.g.path.elements().len().saturating_add(o.elements().len()) > super::graphics::MAX_PATH {
|
||||
return Err(PsError::Limit("a path has too many points"));
|
||||
}
|
||||
self.g.path.extend(o);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `kshow` between character `i` of `s` (its codes) and the next: its procedure run with the
|
||||
/// two codes and the current point at user point `pen` (PLRM 3rd ed., `kshow`: the procedure
|
||||
/// may change the current point, as kerning does) → how far it moved the point (user space).
|
||||
/// Nothing for other show operators or after the last character.
|
||||
fn kern(&mut self, spacing: &Spacing, s: &[u8], i: usize, pen: Point) -> Res<Vec2> {
|
||||
fn kern<C: Copy + Into<i64>>(&mut self, spacing: &Spacing, s: &[C], i: usize, pen: Point) -> Res<Vec2> {
|
||||
let (Spacing::Kern(p), Some(&code), Some(&next)) = (spacing, s.get(i), s.get(i + 1)) else { return Ok(Vec2::ZERO) };
|
||||
self.g.cur = Some(self.xf() * pen);
|
||||
self.push(Obj::Int(i64::from(code)))?;
|
||||
self.push(Obj::Int(i64::from(next)))?;
|
||||
self.call(p.clone())?;
|
||||
self.push(Obj::Int(code.into()))?;
|
||||
self.push(Obj::Int(next.into()))?;
|
||||
// The procedure sees the font the file set, not the one type in a CIDFont only named is
|
||||
// set in; a font it sets stays current.
|
||||
let Some(outer) = self.kshow_font.take() else {
|
||||
self.call(p.clone())?;
|
||||
return Ok(self.user_point()? - pen);
|
||||
};
|
||||
let inner = self.g.font.replace(outer.clone());
|
||||
let r = self.call(p.clone());
|
||||
// Once the procedure sets a font, it and the later procedures see that one.
|
||||
if self.g.font.as_ref().is_some_and(|f| f.same(&outer)) {
|
||||
self.g.font = inner;
|
||||
self.kshow_font = Some(outer);
|
||||
}
|
||||
r?;
|
||||
Ok(self.user_point()? - pen)
|
||||
}
|
||||
|
||||
@@ -757,15 +854,14 @@ impl Interp<'_> {
|
||||
if paint == Draw::Fill {
|
||||
return self.fill_path(outline, vectorcraft_geom::FillRule::NonZero);
|
||||
}
|
||||
if self.g.path.elements().len().saturating_add(outline.elements().len()) > super::graphics::MAX_PATH {
|
||||
return Err(PsError::Limit("a path has too many points"));
|
||||
}
|
||||
self.g.path.extend(outline);
|
||||
Ok(())
|
||||
self.extend_path(outline)
|
||||
}
|
||||
|
||||
/// `charpath`: the glyph outlines of `s` added to the path.
|
||||
fn char_path(&mut self, s: &[u8]) -> Res {
|
||||
if let Some(named) = self.named_cid_type(s)? {
|
||||
return self.show_named_cid(&named, &Spacing::Plain, Draw::Path);
|
||||
}
|
||||
if let Some(glyphs) = self.composite_glyphs(s)? {
|
||||
for g in &glyphs {
|
||||
let at = self.user_point()?;
|
||||
@@ -791,10 +887,9 @@ impl Interp<'_> {
|
||||
let at = self.user_point()?;
|
||||
let (t, layout, adv) = self.set_type(s, at)?;
|
||||
for g in &layout.glyphs {
|
||||
self.grow()?;
|
||||
let mut o = g.outline.clone();
|
||||
o.apply_affine(t.xf);
|
||||
self.g.path.extend(o);
|
||||
self.extend_path(o)?;
|
||||
}
|
||||
self.g.cur = Some(self.xf() * (at + adv));
|
||||
Ok(())
|
||||
@@ -855,6 +950,16 @@ struct Mapped {
|
||||
vertical: bool,
|
||||
}
|
||||
|
||||
/// Type in a CIDFont only named, over a predefined CMap ([`Interp::named_cid_type`]): its codes
|
||||
/// with their characters, the font it is set in, whether it runs down a column, and whether each
|
||||
/// code moves the point one em (down a column, or in a CMap VectorCraft can't read).
|
||||
struct NamedCid {
|
||||
codes: Decoded,
|
||||
font: DictRef,
|
||||
vertical: bool,
|
||||
ems: bool,
|
||||
}
|
||||
|
||||
/// What drawing a glyph does with it.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Draw {
|
||||
@@ -986,9 +1091,9 @@ impl Interp<'_> {
|
||||
/// of that name and its character collection's CID → Unicode CMap (a code it doesn't map
|
||||
/// stands for none), set as type in the font of its CIDFont's name, vertical over a vertical
|
||||
/// CMap (`WMode` 1). Control codes stand for no character. Type in a CMap VectorCraft can't
|
||||
/// read is left out, with a warning.
|
||||
fn named_cid_type(&mut self, s: &[u8]) -> Res<Option<(Decoded, DictRef, bool)>> {
|
||||
let Some((cmap, font, name, vertical)) = self.named_cid_parts() else { return Ok(None) };
|
||||
/// read is left out, with a warning, each of its codes moving the point one em.
|
||||
fn named_cid_type(&mut self, s: &[u8]) -> Res<Option<NamedCid>> {
|
||||
let Some(cmap) = self.named_cid_parts().map(|(cmap, ..)| cmap) else { return Ok(None) };
|
||||
// Each code is looked up in the CMap's mappings.
|
||||
self.spend(s.len() as u64)?;
|
||||
// The decoded codes and characters are held while the string is shown: checked against
|
||||
@@ -996,25 +1101,33 @@ impl Interp<'_> {
|
||||
let held = s.len().saturating_mul(DECODED_BYTES);
|
||||
self.alloc(held)?;
|
||||
self.unalloc(held);
|
||||
let decoded = match self.predefined_codes(&cmap) {
|
||||
Some(codes) => codes.codes(s),
|
||||
let (decoded, read) = match self.predefined_codes(&cmap) {
|
||||
Some(codes) => (codes.codes(s), true),
|
||||
None => {
|
||||
self.out.warn(UNREAD_CMAP);
|
||||
Decoded::default()
|
||||
(super::predefined_cmap::unread_codes(&cmap, s), false)
|
||||
}
|
||||
};
|
||||
let mut codes = Decoded::default();
|
||||
let Some(mut named) = self.named_cid_font()? else { return Ok(None) };
|
||||
let mut chars = decoded.text.chars();
|
||||
for &(code, n) in &decoded.codes {
|
||||
codes.push(code, chars.by_ref().take(n).filter(|c| !c.is_control()));
|
||||
named.codes.push(code, chars.by_ref().take(n).filter(|c| !c.is_control()));
|
||||
}
|
||||
named.ems |= !read;
|
||||
Ok(Some(named))
|
||||
}
|
||||
|
||||
/// The font [`Self::named_cid_type`] sets type in, with no codes yet, when the current font is
|
||||
/// a CID-keyed font over a predefined CMap the file doesn't define.
|
||||
fn named_cid_font(&mut self) -> Res<Option<NamedCid>> {
|
||||
let Some((_, font, name, vertical)) = self.named_cid_parts() else { return Ok(None) };
|
||||
// The composite font is copied under the CIDFont's name for every string: charged by
|
||||
// its size, so a huge font dictionary shown in a loop runs out of the budget.
|
||||
let entries = font.borrow().len();
|
||||
self.spend(entries as u64)?;
|
||||
let mut named = font.borrow().clone();
|
||||
named.insert(Key::name("FontName"), name);
|
||||
Ok(Some((codes, DictRef::new(named), vertical)))
|
||||
Ok(Some(NamedCid { codes: Decoded::default(), font: DictRef::new(named), vertical, ems: vertical }))
|
||||
}
|
||||
|
||||
/// The CMap's name, the composite font, the CIDFont's name and the writing mode of
|
||||
@@ -1032,7 +1145,7 @@ impl Interp<'_> {
|
||||
return None;
|
||||
}
|
||||
let cmap_name = cmap.get(&Key::name("CMapName")).and_then(Obj::text)?.to_string();
|
||||
if super::cid::unicode_cmap(&cmap_name).is_none() && !super::predefined_cmap::known(&cmap_name) {
|
||||
if !super::predefined_cmap::known(&cmap_name) {
|
||||
return None;
|
||||
}
|
||||
let wmode = f.get(&Key::name("WMode")).or_else(|| cmap.get(&Key::name("WMode"))).and_then(Obj::as_num);
|
||||
@@ -1047,7 +1160,7 @@ impl Interp<'_> {
|
||||
/// How strings in predefined CMap `cmap` decode (a CMap other than a Unicode CMap read once a
|
||||
/// file); `None` when VectorCraft can't read it.
|
||||
fn predefined_codes(&mut self, cmap: &str) -> Option<Rc<super::predefined_cmap::CMapCodes>> {
|
||||
if let Some((form, _)) = super::cid::unicode_cmap(cmap) {
|
||||
if let Some(form) = super::predefined_cmap::unicode_cmap(cmap) {
|
||||
return Some(Rc::new(super::predefined_cmap::CMapCodes::Unicode(form)));
|
||||
}
|
||||
if let Some((_, codes)) = self.predefined_cmaps.iter().find(|(n, _)| **n == *cmap) {
|
||||
@@ -1060,32 +1173,53 @@ impl Interp<'_> {
|
||||
codes
|
||||
}
|
||||
|
||||
/// The codes `codes` (with their characters) set as point type at the current point in font
|
||||
/// `font` (see [`Self::named_cid_type`]), running down a column when `vertical`, which moves
|
||||
/// past them, spaced as `spacing` says, as [`Self::show_codes`] shows them. `widthshow` and
|
||||
/// `awidthshow` match a code by its value (font number 0 times 256 plus the code: PLRM 3rd ed.,
|
||||
/// `widthshow` in chapter 8); `kshow`'s procedure isn't run between them (a composite font's
|
||||
/// codes aren't single bytes). Down a column each code moves the point one em, the default
|
||||
/// vertical displacement of a CIDFont whose metrics the file doesn't give (PDF Reference 1.7,
|
||||
/// §5.6.3, `DW2`).
|
||||
fn show_named_cid(&mut self, codes: &Decoded, font: &DictRef, vertical: bool, spacing: &Spacing) -> Res {
|
||||
let widths = vertical.then(|| vec![1.0; codes.codes.len()]);
|
||||
let saved = self.g.font.replace(font.clone());
|
||||
let r = self.show_codes(codes, widths.as_deref(), spacing, vertical, &[]);
|
||||
/// Type `named` (see [`Self::named_cid_type`]) set as point type at the current point in its
|
||||
/// font, running down a column when it is vertical, which moves past it, spaced as `spacing`
|
||||
/// says, as [`Self::show_codes`] shows codes. `widthshow` and `awidthshow` match a code by its
|
||||
/// value (font number 0 times 256 plus the code: PLRM 3rd ed., `widthshow` in chapter 8), and
|
||||
/// `kshow`'s procedure gets the values of the two codes it runs between. Down a column, and in
|
||||
/// a CMap VectorCraft can't read, each code moves the point one em, the default displacement
|
||||
/// of a CIDFont whose metrics the file doesn't give (PDF Reference 1.7, §5.6.3, `DW` and
|
||||
/// `DW2`). With `draw` [`Draw::Path`] the outlines go on the current path (`charpath`).
|
||||
fn show_named_cid(&mut self, named: &NamedCid, spacing: &Spacing, draw: Draw) -> Res {
|
||||
let widths = named.ems.then(|| vec![1.0; named.codes.codes.len()]);
|
||||
let kern: Vec<u32> = match spacing {
|
||||
Spacing::Kern(_) => named.codes.codes.iter().map(|&(code, _)| code).collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let saved = self.g.font.replace(named.font.clone());
|
||||
let before = std::mem::replace(&mut self.kshow_font, saved.clone());
|
||||
let r = self.show_codes(&named.codes, widths.as_deref(), spacing, named.vertical, &kern, draw);
|
||||
self.kshow_font = before;
|
||||
// A font the `kshow` procedure set stays current.
|
||||
if self.g.font.as_ref().is_none_or(|f| f.same(&named.font)) {
|
||||
self.g.font = saved;
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// How far codes `codes` of type `named` move the current point (user space) when
|
||||
/// [`Self::show_named_cid`] shows them with no extra spacing.
|
||||
fn named_cid_width(&mut self, named: &NamedCid, codes: &Decoded) -> Res<Vec2> {
|
||||
let saved = self.g.font.replace(named.font.clone());
|
||||
let r = self.named_cid_moved(codes, named.vertical, named.ems);
|
||||
self.g.font = saved;
|
||||
r
|
||||
}
|
||||
|
||||
/// How far the codes `codes` move the current point (user space) when [`Self::show_named_cid`]
|
||||
/// shows them with no extra spacing.
|
||||
fn named_cid_width(&mut self, codes: &Decoded, vertical: bool) -> Res<Vec2> {
|
||||
/// [`Self::named_cid_width`] in the current font: codes `codes`, vertical when `vertical`,
|
||||
/// each one em when `ems`.
|
||||
fn named_cid_moved(&mut self, codes: &Decoded, vertical: bool, ems: bool) -> Res<Vec2> {
|
||||
let (_, f1) = self.font()?;
|
||||
let moved = if vertical {
|
||||
Point::new(0.0, -(codes.codes.len() as f64))
|
||||
} else {
|
||||
let (t, layout) = self.set_placed_text(&codes.text, Point::ZERO, false)?;
|
||||
let size = t.runs.first().map_or(1.0, |r| r.style.size);
|
||||
Point::new(layout.glyphs.iter().map(|g| g.advance).sum::<f64>() / size, 0.0)
|
||||
let n = codes.codes.len() as f64;
|
||||
let moved = match (vertical, ems) {
|
||||
(true, _) => Point::new(0.0, -n),
|
||||
(false, true) => Point::new(n, 0.0),
|
||||
(false, false) => {
|
||||
let (t, layout) = self.set_placed_text(&codes.text, Point::ZERO, false)?;
|
||||
let size = t.runs.first().map_or(1.0, |r| r.style.size);
|
||||
Point::new(layout.glyphs.iter().map(|g| g.advance).sum::<f64>() / size, 0.0)
|
||||
}
|
||||
};
|
||||
Ok(f1 * moved - f1 * Point::ZERO)
|
||||
}
|
||||
@@ -1393,7 +1527,7 @@ const MISSING_CID_FONT: &str = "CID-keyed type in a font the file doesn't carry
|
||||
const DECODED_BYTES: usize = 64;
|
||||
|
||||
/// The warning for type in a predefined CMap whose mappings VectorCraft doesn't have.
|
||||
pub(super) const UNREAD_CMAP: &str = "Type in a CMap that couldn't be read was left out";
|
||||
pub(super) const UNREAD_CMAP: &str = "type in a CMap that couldn't be read was left out";
|
||||
|
||||
/// Most Type 42 fonts whose TrueType data is kept between glyphs.
|
||||
const MAX_TYPE42: usize = 16;
|
||||
|
||||
@@ -288,7 +288,7 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors,
|
||||
| Illustrator template `.ait` | open, save | yes | PDF-compatible only, from Save as Template (`.vctemplate` stays its default) | New from Template opens a saved `.ait` untitled with its layers and art (`tests_pdfedit.rs`) |
|
||||
| Illustrator `.ai` | open, save | yes (PDF-compatible + editing-data structure with template layers and ruler guides, File Info) | PDF-compatible, or legacy version 7 (layers and groups) | see above; no real-file corpus |
|
||||
| PDF | open, place, save | yes (authored ActualText in editable import, #6165; layers, masks, editable text, security, overprint, File Info from the XMP packet and the document information dictionary; Crop To › Trim takes the bleed box as the document's bleed) | yes (presets, PDF/X-1a/3/4, layers, marks and bleed, ICC, fast web view, 1.3+; objects' URLs as link annotations, not in PDF/X) | fuzzed import; shared guarded writer and windowed reader ([pdf-backend.md](pdf-backend.md)), metadata, incomplete-content and dependency-source regressions; PDF text opens where the file draws it, glyph by glyph (#722: kerning or none, spacing per character, text rise as baseline shift, vertical writing, justified text boxes); alpha soft masks drawn as images import with the mask (#1551); a clip only counts as covering the page when its shape does (#1542); a PDF another app copied pastes as editable vector art (#1499); letter-spaced type on a path keeps its length and its glyph-by-glyph stroke comes in as one text object (#1586, #1585); a crafted name tree with shared nodes no longer takes exponential time on open (#1781); fonts named with hyphens (`Noto-Sans-Bold`) or with a composite font's CMap (`Name-Identity-H`) find the installed family; invisible text (rendering mode 3: a scanned page's recognized text) opens as type with no fill or stroke and saves back as invisible, searchable text (#2675); PDF/X-1a and X-3 flattening in a CMYK document keeps CMYK inks where it rasterizes (#2160); an image mask (`/ImageMask true`) painted in a solid color opens as a grayscale image colorized by that color, a spot color keeping its swatch and tint, so Fill and Recolor Artwork reach it, and the same mask painted again in another color keeps that color (#6313); an image in a Separation space opens as a grayscale image of its tints colorized by its ink at 100 % (a spot swatch, a process plate or Registration), its soft mask as its opacity mask (#6314); a DeviceN image of one colorant opens the same way, and one of process plates only (the ink `None` may be among them) opens in CMYK with each colorant's tint in its plate (#6348) PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import preserves authored ActualText separately (#6165), with cross-line/style layout limits; separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. |
|
||||
| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CIDFonts the file doesn't carry not drawn, but for those over a predefined Unicode CMap (`UniJIS-UCS2-H`), set as type in the font of the CIDFont's name; name-keyed fonts in a FontSet set as type by name; #505; images encoded with `CCITTFaxDecode` (ITU-T T.4 Group 3 and T.6 Group 4, with `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1`) draw, and damaged data keeps the rows decoded before its first damaged row; Type 42 (TrueType) fonts set type at its size (#1553); an embedded Type 1 font's encrypted part is read for its glyphs, which space its type, outline it and draw glyphs without a character, in plain and composite fonts; a Type 42 font that carries its TrueType font gives each code its character by its glyph name or, for names that say nothing (`/g12`), by the glyph's entry in the font's own character map, spaces its type by its own widths and draws glyphs that stand for no character from its outlines; the show operators' spacing along the baseline (`ashow`, `widthshow`, `xshow`, `yshow`, `xyshow`, and the moves of `kshow`'s procedure, run between each two characters) is kept on live type as tracking, so type in a font that isn't installed keeps the file's line lengths (spacing that moves characters off the baseline, as `yshow`, `xyshow` or a `kshow` procedure can, places each character as its own text object); composite (Type 0) fonts with the 8/8 mapping, as PDF-to-PostScript converters write two-byte fonts, draw a Type 42 descendant's glyphs from the TrueType outlines it carries and an embedded Type 1 descendant's from its charstrings, and set other descendants' characters as type at the composite font's size (other mappings are still set as type in the font of their name); CID-keyed fonts (`composefont`, `FMapType` 9 over a CMap built with the CIDInit operators or `Identity-H`/`Identity-V`; `bfchar` and `bfrange` mappings give a base-font descendant its codes or glyph names) draw the glyphs of the Type 0 CIDFonts (Type 1 charstrings in their `StartData` binary data or `GlyphDirectory`) and Type 2 CIDFonts (TrueType glyphs through `CIDMap`) they carry, and the CID-keyed fonts of a FontSet's Compact Font Format data (Type 2 charstrings); a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499); a stitched shading keeps its hard colour changes on import (#1654); sampled functions (type 0) interpolate between their samples, multilinearly or by cubics with `Order 3`, and a sampled axial or radial gradient has a stop at each sample; a gradient the page paints as a row of abutting one-colour strips (as Level 2 writers draw smooth shades) imports as one linear gradient, without the seams the strips leave when each is drawn alone; CMYK images, CMYK JPEGs included, keep their ink amounts on import, as from PDF, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask; ruzstd 0.9.1 for compressed editing data (#1807, RUSTSEC-2026-0344) |
|
||||
| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CIDFonts the file doesn't carry not drawn, but for those over a predefined CMap (`UniJIS-UCS2-H`, `90ms-RKSJ-H`, `GBK-EUC-H`, `KSCms-UHC-H`, `ETen-B5-H`), set as type in the font of the CIDFont's name, vertical over a `-V` CMap; a name that ends in the row-and-cell CMap `H` or `V` splits off the CMap only when the file defines the CIDFont of that name, or the name has a hyphen and an installed face that covers Japanese has it; name-keyed fonts in a FontSet set as type by name; #505; images encoded with `CCITTFaxDecode` (ITU-T T.4 Group 3 and T.6 Group 4, with `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1`) draw, and damaged data keeps the rows decoded before its first damaged row; Type 42 (TrueType) fonts set type at its size (#1553); an embedded Type 1 font's encrypted part is read for its glyphs, which space its type, outline it and draw glyphs without a character, in plain and composite fonts; a Type 42 font that carries its TrueType font gives each code its character by its glyph name or, for names that say nothing (`/g12`), by the glyph's entry in the font's own character map, spaces its type by its own widths and draws glyphs that stand for no character from its outlines; the show operators' spacing along the baseline (`ashow`, `widthshow`, `xshow`, `yshow`, `xyshow`, and the moves of `kshow`'s procedure, run between each two characters) is kept on live type as tracking, so type in a font that isn't installed keeps the file's line lengths (spacing that moves characters off the baseline, as `yshow`, `xyshow` or a `kshow` procedure can, places each character as its own text object); composite (Type 0) fonts with the 8/8 mapping, as PDF-to-PostScript converters write two-byte fonts, draw a Type 42 descendant's glyphs from the TrueType outlines it carries and an embedded Type 1 descendant's from its charstrings, and set other descendants' characters as type at the composite font's size (other mappings are still set as type in the font of their name); CID-keyed fonts (`composefont`, `FMapType` 9 over a CMap built with the CIDInit operators or `Identity-H`/`Identity-V`; `bfchar` and `bfrange` mappings give a base-font descendant its codes or glyph names) draw the glyphs of the Type 0 CIDFonts (Type 1 charstrings in their `StartData` binary data or `GlyphDirectory`) and Type 2 CIDFonts (TrueType glyphs through `CIDMap`) they carry, and the CID-keyed fonts of a FontSet's Compact Font Format data (Type 2 charstrings); a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499); a stitched shading keeps its hard colour changes on import (#1654); sampled functions (type 0) interpolate between their samples, multilinearly or by cubics with `Order 3`, and a sampled axial or radial gradient has a stop at each sample; a gradient the page paints as a row of abutting one-colour strips (as Level 2 writers draw smooth shades) imports as one linear gradient, without the seams the strips leave when each is drawn alone; CMYK images, CMYK JPEGs included, keep their ink amounts on import, as from PDF, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask; ruzstd 0.9.1 for compressed editing data (#1807, RUSTSEC-2026-0344) |
|
||||
| PostScript `.ps` | open, place | yes (the EPS reader reads its first page; `.epsf` files open and place as EPS) | via Print to PostScript | `tests_epsimport.rs` |
|
||||
| SVG / SVGZ | open, place, save | yes | yes (SVG Options, Preserve Editing, symbols, filters, rich text) | strongest format; tests in `crates/svg/tests`; text on a path follows basic shapes as their SVG 2 equivalent paths (#1527); CSS transforms and the `font` shorthand (#1565), percentage lengths in symbols (#1596), Inkscape flowed text as area type (#1568) and Inkscape pages as artboards (#1601) import; an unsupported filter is named in its warning (#1598); an artboard exported on its own (Export As with Use Artboards, Export for Screens, Asset Export) has the artboard's name as `<title>`, Save the document's (#3544); attributes in other applications' XML namespaces (`plotter:pen="2"`) kept on their objects from import to export, as `data-*` attributes are (`crates/svg/tests/it/foreign_attributes.rs`); simple shapes export as `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polygon>` and `<polyline>` where they draw the same as the path (#4652, `crates/svg/tests/it/basic_shapes.rs`); the regions of exported masks and filters (opacity masks, inverted masks, knockout groups) cover the art they apply to, and over type keep a region that covers any artwork (#6401, `crates/svg/tests/it/blend_export.rs`) |
|
||||
| DXF | open, export | yes | yes (R12–2018) | R13–2018 closed SPLINE flags and control-point closure on import verified (#1299); circular cubic paths export as native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes remain POLYLINE while non-circular curves keep the existing fallback (`crates/cad/src/tests.rs`); [Autodesk group 70](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DXF/files/GUID-E1F884F8-AA90-4864-A215-3182D47A9C74.htm). DXF 2000–2018 LAYOUT objects write the paper image origin as 148/149 (was a stray 158), the shade plot codes (2004+) and the model type flag, and the plot style placeholder carries no subclass marker (#3085; structure test `r2000_and_later_files_have_the_structure_cad_apps_require`, checked with ezdxf's audit; not yet confirmed in Autodesk Fusion). DXF Options' Preset menu and `document.exportDxf`'s `dxfPreset: "cnc"` set the CNC / CAM options (#5500); VCarve QA remains open |
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi
|
||||
|
||||
- **Narrowed (#6165, 2026-11-09):** editable PDF import preserves explicit ActualText once, with empty/nested spans, authored order and glyph advances covered by `crates/pdf/src/tests/actual_text.rs`. One replacement uses the first placed glyph's line/style; exact redistribution across lines/styles and general mixed-direction layout remain. Input is bounded to depth 128 and 64 KiB encoded per accepted replacement. This does not close G9's broader `.ai`/EPS gaps or change its estimate.
|
||||
|
||||
- **Done (2026-11-09):** the fonts an EPS carries draw their glyphs instead of being set as type in an installed font: composite (Type 0) fonts with the 8/8 mapping, plain Type 42 fonts (characters from their glyph names or character map, widths from the font), CID-keyed fonts over CMaps (Type 0 and Type 2 CIDFonts, CIDFonts in a FontSet's Compact Font Format data, base-font CMaps, named CIDFonts over predefined Unicode CMaps set as type) and embedded Type 1 fonts; the show operators' spacing along the baseline is kept as tracking, `kshow` included. Each step is charged to the interpreter's budgets. Hand-written test files only (no corpus file uses these fonts); scores and estimates unchanged. This narrows G9.
|
||||
- **Done (2026-11-09):** the fonts an EPS carries draw their glyphs instead of being set as type in an installed font: composite (Type 0) fonts with the 8/8 mapping, plain Type 42 fonts (characters from their glyph names or character map, widths from the font), CID-keyed fonts over CMaps (Type 0 and Type 2 CIDFonts, CIDFonts in a FontSet's Compact Font Format data, base-font CMaps, named CIDFonts over predefined CMaps (Unicode, Shift-JIS, EUC, GBK, Big Five, UHC) set as type, vertical over `-V` CMaps) and embedded Type 1 fonts; the show operators' spacing along the baseline is kept as tracking, `kshow` included. Each step is charged to the interpreter's budgets. Hand-written test files only (no corpus file uses these fonts); scores and estimates unchanged. This narrows G9.
|
||||
- **Done (2026-11-06):** an EPS page that paints its gradients as rows of abutting one-colour strips imports them as linear gradients, so the seams between strips no longer show and an Illustrator EPS drawn that way keeps its layers instead of opening as its page. This narrows G9; scores and estimates unchanged.
|
||||
- **Done (2026-10-30):** from an audit of a user's `.ai`/EPS files: All Caps and Small Caps; placed files at their box's corner; pages cropped to the art; gradient mesh corners with handle weights of 0 and the transparency written after a mesh, a masked mesh and a blend; a layer's state block with the stroke style; type on a path spaced as the file lays it out (the distance from the path under Align to Path is measured from the top of each character's em box, half the height of the top of its H or the bottom of its font's bounding box, and the fit under Gravity does not reproduce the file's layout); EPS10 legacy-format type; a PDF of some artboards compared on the artboard its page shows. Comparison limits remain unchanged. This narrows G9; scores and estimates unchanged.
|
||||
|
||||
|
||||
+1
-1
@@ -6872,7 +6872,7 @@ dictionaries (the standard ones are values in `systemdict`; `dictstack`, `intern
|
||||
intervals share storage, resources (categories made from `Generic`, `resourceforall`), executable filters (`cvx exec`
|
||||
runs their data; `flushfile` skips it), axial and radial shadings and shading patterns (gradients), images and image
|
||||
masks (data in the file through ASCII85, hex, run-length, Flate, LZW or DCT filters, or from procedures), and type as
|
||||
point type in the font the file names (embedded Type 1 and Type 42 fonts' characters and widths come from the font program they carry, and the show operators' spacing along the baseline is kept as tracking, while spacing that moves characters off it (`yshow`, `xyshow`) makes a text object per character; CID-keyed fonts draw the glyphs of the Type 0 and Type 2 CIDFonts the file carries, CIDFonts in a FontSet's Compact Font Format data included). In a file in Illustrator's own format
|
||||
point type in the font the file names (embedded Type 1 and Type 42 fonts' characters and widths come from the font program they carry, and the show operators' spacing along the baseline is kept as tracking, while spacing that moves characters off it (`yshow`, `xyshow`) makes a text object per character; CID-keyed fonts draw the glyphs of the Type 0 and Type 2 CIDFonts the file carries, CIDFonts in a FontSet's Compact Font Format data included; a CID-keyed font whose CIDFont the file only names, over a predefined CMap such as `UniJIS-UCS2-H`, `90ms-RKSJ-H` or `GBK-EUC-H`, sets the characters its codes stand for as type in the font of the CIDFont's name, as vertical type over a `-V` CMap, and type in a predefined CMap that can't be read is left out with a warning). In a file in Illustrator's own format
|
||||
(Illustrator 3–8 `.ai` and their EPS, written with the prolog that defines its operators) the groups it writes (`u` …
|
||||
`U`) come in as groups, nested as they were; clips and groups nest at most 128 deep. The artboard is the `%%HiResBoundingBox`
|
||||
(else `%%BoundingBox`; a letter page without one). A program the interpreter can't run (an operator it doesn't know,
|
||||
|
||||
Reference in New Issue
Block a user