M13.12: EPS text (show variants, charpath, stringwidth, embedded Type 1/CFF, fallback fonts)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d0df520bc1
commit
740faf1c1f
|
|
@ -366,6 +366,14 @@ pub struct Cff {
|
|||
pub encoding: [u16; 256],
|
||||
}
|
||||
|
||||
/// The first font name in a CFF program's Name INDEX.
|
||||
pub fn font_name(data: &[u8]) -> Option<String> {
|
||||
let hdr = *data.get(2)? as usize;
|
||||
let (names, _) = index(data, hdr)?;
|
||||
let r = names.first()?.clone();
|
||||
Some(String::from_utf8_lossy(data.get(r)?).into_owned())
|
||||
}
|
||||
|
||||
impl Cff {
|
||||
pub fn parse(data: &[u8]) -> Option<Cff> {
|
||||
let d = data;
|
||||
|
|
|
|||
|
|
@ -311,11 +311,9 @@ fn parse_cmap(data: &[u8]) -> CMap {
|
|||
|
||||
impl Font {
|
||||
/// Read a font dictionary. `skipped` collects what could not be read.
|
||||
pub fn load(file: &File, d: &Dict, skipped: &mut Vec<String>) -> Font {
|
||||
let subtype = file.get(d, "Subtype").and_then(Obj::name).unwrap_or("Type1").to_string();
|
||||
let base_name = file.get(d, "BaseFont").and_then(Obj::name).unwrap_or("").to_string();
|
||||
let mut f = Font {
|
||||
composite: subtype == "Type0",
|
||||
fn blank(base_name: &str, composite: bool) -> Font {
|
||||
Font {
|
||||
composite,
|
||||
program: Program::None,
|
||||
matrix: Affine::scale(0.001),
|
||||
first_char: 0,
|
||||
|
|
@ -333,8 +331,44 @@ impl Font {
|
|||
dw2: (0.88, -1.0),
|
||||
cache: RefCell::new(HashMap::new()),
|
||||
has_widths: false,
|
||||
base_name: base_name.clone(),
|
||||
base_name: base_name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple font outside a PDF (EPS text): `program` is an embedded font program (CFF when
|
||||
/// the flag is set, else Type 1), otherwise the bundled stand-in for `name` is used;
|
||||
/// `names` is the encoding (code → glyph name), else the program's built-in encoding or
|
||||
/// StandardEncoding. Advances are the program's.
|
||||
pub fn standalone(name: &str, program: Option<(&[u8], bool)>, names: Option<Vec<Option<String>>>) -> Font {
|
||||
let mut f = Font::blank(name, false);
|
||||
f.program = match program {
|
||||
Some((data, true)) => Cff::parse(data).map(|c| Program::Cff(Box::new(c))),
|
||||
Some((data, false)) => Type1::parse(data).map(|t| Program::Type1(Box::new(t))),
|
||||
None => None,
|
||||
}
|
||||
.unwrap_or_else(|| Program::Fallback(fallback(name)));
|
||||
match &f.program {
|
||||
Program::Cff(c) => f.matrix = Affine::new(c.font_matrix),
|
||||
Program::Type1(t) => f.matrix = t.font_matrix.map(Affine::new).unwrap_or(f.matrix),
|
||||
_ => {}
|
||||
}
|
||||
let symbol = matches!(f.program, Program::Fallback(_)) && (name.contains("Symbol") || name.contains("Dingbats"));
|
||||
f.names = match names {
|
||||
// Unencoded codes draw .notdef (not the built-in encoding).
|
||||
Some(n) => (0..256).map(|c| Some(n.get(c).cloned().flatten().unwrap_or_else(|| ".notdef".into()))).collect(),
|
||||
None => match &f.program {
|
||||
Program::Type1(t) if !t.encoding.is_empty() => (0..256).map(|c| t.encoding.get(&(c as u8)).cloned()).collect(),
|
||||
_ if symbol => vec![None; 256],
|
||||
_ => (0..256).map(|c| Base::Standard.name(c as u8)).collect(),
|
||||
},
|
||||
};
|
||||
f
|
||||
}
|
||||
|
||||
pub fn load(file: &File, d: &Dict, skipped: &mut Vec<String>) -> Font {
|
||||
let subtype = file.get(d, "Subtype").and_then(Obj::name).unwrap_or("Type1").to_string();
|
||||
let base_name = file.get(d, "BaseFont").and_then(Obj::name).unwrap_or("").to_string();
|
||||
let mut f = Font::blank(&base_name, subtype == "Type0");
|
||||
if let Some(Obj::Stream(sd, raw)) = file.get(d, "ToUnicode")
|
||||
&& let Some(data) = decode_stream(file, sd, raw)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
//! edition, and the EPSF 3.0 specification) covering what exported vector EPS files use: the
|
||||
//! operand and dictionary stacks, procedures and `def`, arithmetic, control flow, arrays and
|
||||
//! dictionaries, the graphics state, path construction (including arcs) and painting, clipping
|
||||
//! and device colours. Text is skipped, as are images (their inline data is stepped over) and
|
||||
//! and device colours, and text (`findfont` / `selectfont`, `show` and its variants, `charpath`,
|
||||
//! `stringwidth`, re-encoded fonts) with embedded Type 1 and CFF programs or the bundled
|
||||
//! stand-in fonts. Images are skipped (their inline data is stepped over), as is
|
||||
//! Level 3 smooth shading (`shfill`).
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
|
@ -13,6 +15,7 @@ use effectcraft_svg::{Affine, BezPath, Cap, FillRule, Join, Paint, Stroke};
|
|||
use kurbo::{PathEl, Point};
|
||||
|
||||
use crate::build::Builder;
|
||||
use crate::font::Font;
|
||||
use crate::object::{ascii85, is_white, parse_num};
|
||||
|
||||
type DictRef = Rc<RefCell<HashMap<String, V>>>;
|
||||
|
|
@ -79,6 +82,8 @@ struct GState {
|
|||
cur: Option<Point>,
|
||||
start: Option<Point>,
|
||||
depth: usize,
|
||||
/// The current font dictionary.
|
||||
font: Option<DictRef>,
|
||||
}
|
||||
|
||||
/// Source tokenizer (the data source for `currentfile`).
|
||||
|
|
@ -311,6 +316,12 @@ pub(crate) struct Interp {
|
|||
budget: usize,
|
||||
/// Nested procedure calls (recursion is cut off at [`MAX_CALL_DEPTH`]).
|
||||
calls: usize,
|
||||
/// Fonts by name: `definefont`d dictionaries.
|
||||
font_dir: HashMap<String, DictRef>,
|
||||
/// Embedded font programs found in the file (name → (program, CFF)).
|
||||
embedded: HashMap<String, (Rc<Vec<u8>>, bool)>,
|
||||
/// Fonts built for drawing, by base font and encoding.
|
||||
font_cache: HashMap<String, Rc<Font>>,
|
||||
page: [f64; 4],
|
||||
pub unknown: Vec<String>,
|
||||
}
|
||||
|
|
@ -353,16 +364,21 @@ impl Interp {
|
|||
cur: None,
|
||||
start: None,
|
||||
depth: 1,
|
||||
font: None,
|
||||
},
|
||||
saved: vec![],
|
||||
budget: 8_000_000,
|
||||
calls: 0,
|
||||
font_dir: HashMap::new(),
|
||||
embedded: HashMap::new(),
|
||||
font_cache: HashMap::new(),
|
||||
page,
|
||||
unknown: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&mut self, data: &[u8]) {
|
||||
self.embedded = embedded_fonts(data);
|
||||
let mut src = Src { d: data, pos: 0 };
|
||||
while let Some(t) = src.token(0) {
|
||||
if let Err(Flow::Quit) = self.exec_token(t, &mut src) {
|
||||
|
|
@ -924,16 +940,24 @@ impl Interp {
|
|||
"countdictstack" => self.push(V::Num(self.dicts.len() as f64)),
|
||||
"cleardictstack" => self.dicts.truncate(1),
|
||||
"findresource" | "findfont" | "findencoding" => {
|
||||
if n == "findresource" {
|
||||
self.pop();
|
||||
let category = if n == "findresource" { self.pop().key() } else { String::new() };
|
||||
let key = self.pop().key();
|
||||
match (n, category.as_str()) {
|
||||
("findfont", _) | (_, "Font") => {
|
||||
let f = self.find_font(&key);
|
||||
self.push(V::Dict(f));
|
||||
}
|
||||
("findencoding", _) | (_, "Encoding") => self.push(encoding_array(&key)),
|
||||
_ => self.push(V::Dict(new_dict())),
|
||||
}
|
||||
self.pop();
|
||||
self.push(V::Dict(new_dict()));
|
||||
}
|
||||
"defineresource" => {
|
||||
self.pop();
|
||||
let category = self.pop().key();
|
||||
let v = self.pop();
|
||||
self.pop();
|
||||
let key = self.pop().key();
|
||||
if let (V::Dict(d), "Font") = (&v, category.as_str()) {
|
||||
self.font_dir.insert(key, d.clone());
|
||||
}
|
||||
self.push(v);
|
||||
}
|
||||
"resourcestatus" => {
|
||||
|
|
@ -1355,55 +1379,10 @@ impl Interp {
|
|||
self.pop();
|
||||
self.b.skip("smooth shading (shfill)");
|
||||
}
|
||||
// Text (skipped).
|
||||
"scalefont" | "makefont" | "selectfont" | "setfont" | "show" | "charpath" | "glyphshow" | "rootfont" | "setcachedevice" | "setcharwidth" => {
|
||||
match n {
|
||||
"scalefont" | "makefont" => {
|
||||
self.pop();
|
||||
}
|
||||
"selectfont" => {
|
||||
self.pop();
|
||||
self.pop();
|
||||
}
|
||||
"charpath" => {
|
||||
self.pop();
|
||||
self.pop();
|
||||
}
|
||||
"setcachedevice" => {
|
||||
for _ in 0..6 {
|
||||
self.pop();
|
||||
}
|
||||
}
|
||||
"setcharwidth" => {
|
||||
self.pop();
|
||||
self.pop();
|
||||
}
|
||||
"rootfont" => self.push(V::Dict(new_dict())),
|
||||
_ => {
|
||||
self.pop();
|
||||
}
|
||||
}
|
||||
if matches!(n, "show" | "charpath" | "glyphshow") {
|
||||
self.b.skip("text");
|
||||
}
|
||||
}
|
||||
"currentfont" => self.push(V::Dict(new_dict())),
|
||||
"ashow" | "widthshow" | "awidthshow" | "xshow" | "yshow" | "xyshow" | "kshow" | "cshow" => {
|
||||
let k = match n {
|
||||
"ashow" | "xshow" | "yshow" | "xyshow" | "kshow" | "cshow" => 3,
|
||||
"widthshow" => 4,
|
||||
_ => 6,
|
||||
};
|
||||
for _ in 0..k.min(self.st.len()) {
|
||||
self.pop();
|
||||
}
|
||||
self.b.skip("text");
|
||||
}
|
||||
"stringwidth" => {
|
||||
self.pop();
|
||||
self.push(V::Num(0.0));
|
||||
self.push(V::Num(0.0));
|
||||
}
|
||||
// Text (see `text_op`).
|
||||
"scalefont" | "makefont" | "selectfont" | "setfont" | "show" | "charpath" | "glyphshow" | "rootfont" | "currentfont" | "setcachedevice"
|
||||
| "setcharwidth" | "ashow" | "widthshow" | "awidthshow" | "xshow" | "yshow" | "xyshow" | "kshow" | "cshow" | "stringwidth" | "definefont"
|
||||
| "StandardEncoding" | "ISOLatin1Encoding" | "eexec" | "StartData" | "undefinefont" => self.text_op(n, src)?,
|
||||
// Images: operands popped, inline data skipped.
|
||||
"image" | "imagemask" | "colorimage" => {
|
||||
let mut reads_file = false;
|
||||
|
|
@ -1478,3 +1457,364 @@ pub fn eps_parts(bytes: &[u8]) -> Option<(&[u8], [f64; 4])> {
|
|||
let bbox = find_box(b"%%HiResBoundingBox:").or_else(|| find_box(b"%%BoundingBox:"))?;
|
||||
Some((ps, bbox))
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ text
|
||||
|
||||
/// An encoding array of glyph names (`StandardEncoding`; `ISOLatin1Encoding` and others are
|
||||
/// approximated by WinAnsi, which agrees with ISO Latin-1 on the printable codes).
|
||||
fn encoding_array(name: &str) -> V {
|
||||
let base = if name == "StandardEncoding" { crate::encoding::Base::Standard } else { crate::encoding::Base::WinAnsi };
|
||||
array((0..256).map(|c| V::Lit(base.name(c as u8).unwrap_or_else(|| ".notdef".into()).into())).collect())
|
||||
}
|
||||
|
||||
fn find_from(hay: &[u8], needle: &[u8], from: usize) -> Option<usize> {
|
||||
hay.get(from..)?.windows(needle.len()).position(|w| w == needle).map(|p| p + from)
|
||||
}
|
||||
|
||||
fn rfind_before(hay: &[u8], needle: &[u8], to: usize) -> Option<usize> {
|
||||
hay.get(..to)?.windows(needle.len()).rposition(|w| w == needle)
|
||||
}
|
||||
|
||||
/// The name after `key` (`/FontName /Name`).
|
||||
fn name_after(b: &[u8], key: &[u8]) -> Option<String> {
|
||||
let p = find_from(b, key, 0)? + key.len();
|
||||
let rest = &b[p..];
|
||||
let s = rest.iter().position(|c| *c == b'/')? + 1;
|
||||
let e = rest[s..].iter().position(|c| is_white(*c) || matches!(c, b'/' | b'[' | b'(' | b'{')).map_or(rest.len(), |q| s + q);
|
||||
(e > s).then(|| String::from_utf8_lossy(&rest[s..e]).into_owned())
|
||||
}
|
||||
|
||||
/// Embedded font programs of an EPS file: Type 1 fonts (`… /FontName /X … currentfile eexec
|
||||
/// … cleartomark`, hexadecimal or binary) and CFF font sets (`… <length> StartData <bytes>`).
|
||||
fn embedded_fonts(ps: &[u8]) -> HashMap<String, (Rc<Vec<u8>>, bool)> {
|
||||
let mut out = HashMap::new();
|
||||
let mut from = 0;
|
||||
while let Some(ee) = find_from(ps, b"eexec", from) {
|
||||
from = ee + 5;
|
||||
let start = [b"%!PS-AdobeFont".as_slice(), b"%!FontType1"]
|
||||
.iter()
|
||||
.filter_map(|h| rfind_before(ps, h, ee))
|
||||
.max()
|
||||
.or_else(|| rfind_before(ps, b"/FontName", ee).map(|p| p.saturating_sub(512)));
|
||||
let Some(start) = start else { continue };
|
||||
let end = find_from(ps, b"cleartomark", ee).map_or(ps.len(), |p| p + 11);
|
||||
if let Some(name) = name_after(&ps[start..ee], b"/FontName") {
|
||||
out.insert(name, (Rc::new(ps[start..end].to_vec()), false));
|
||||
}
|
||||
from = end;
|
||||
}
|
||||
let mut from = 0;
|
||||
while let Some(sd) = find_from(ps, b"StartData", from) {
|
||||
from = sd + 9;
|
||||
let head = String::from_utf8_lossy(&ps[sd.saturating_sub(64)..sd]).into_owned();
|
||||
let Some(len) = head.split_whitespace().last().and_then(|t| t.parse::<usize>().ok()) else { continue };
|
||||
let s = sd + 10;
|
||||
let Some(data) = ps.get(s..s.saturating_add(len)) else { continue };
|
||||
if let Some(name) = crate::cff::font_name(data) {
|
||||
out.insert(name, (Rc::new(data.to_vec()), true));
|
||||
}
|
||||
from = s + len;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn dict_matrix(d: &DictRef, key: &str) -> Affine {
|
||||
d.borrow().get(key).and_then(matrix_of).unwrap_or(Affine::IDENTITY)
|
||||
}
|
||||
|
||||
/// A copy of a font dictionary with `m` applied after its matrix (`scalefont`, `makefont`).
|
||||
fn transformed(font: &DictRef, m: Affine) -> DictRef {
|
||||
let copy: HashMap<String, V> = font.borrow().clone();
|
||||
let d = Rc::new(RefCell::new(copy));
|
||||
let old = dict_matrix(&d, "__m");
|
||||
d.borrow_mut().insert("__m".into(), matrix_val(m * old));
|
||||
d
|
||||
}
|
||||
|
||||
impl Interp {
|
||||
/// A font dictionary by name: one `definefont` made, else a new dictionary for an
|
||||
/// embedded program or a bundled stand-in.
|
||||
fn find_font(&mut self, key: &str) -> DictRef {
|
||||
if let Some(d) = self.font_dir.get(key) {
|
||||
return d.clone();
|
||||
}
|
||||
let d = new_dict();
|
||||
{
|
||||
let mut m = d.borrow_mut();
|
||||
m.insert("FontName".into(), V::Lit(key.into()));
|
||||
m.insert("FontType".into(), V::Num(1.0));
|
||||
m.insert("FontMatrix".into(), matrix_val(Affine::scale(0.001)));
|
||||
m.insert("FID".into(), V::Null);
|
||||
m.insert("__base".into(), V::Lit(key.into()));
|
||||
m.insert("__m".into(), matrix_val(Affine::IDENTITY));
|
||||
}
|
||||
self.font_dir.insert(key.to_string(), d.clone());
|
||||
d
|
||||
}
|
||||
|
||||
/// The drawable font of a font dictionary (its base program and encoding) and the matrix
|
||||
/// from text space (font size 1) to user space.
|
||||
fn font_of(&mut self, d: &DictRef) -> Option<(Rc<Font>, Affine)> {
|
||||
let (base, names) = {
|
||||
let m = d.borrow();
|
||||
let base = m.get("__base").or_else(|| m.get("FontName")).map(V::key)?;
|
||||
let names: Option<Vec<Option<String>>> = m.get("Encoding").and_then(V::items).map(|a| {
|
||||
a.iter()
|
||||
.map(|v| match v {
|
||||
V::Lit(n) | V::Name(n) if &**n != ".notdef" => Some(n.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
(base, names)
|
||||
};
|
||||
let key = match &names {
|
||||
Some(n) => format!("{base}\u{0}{}", n.iter().map(|x| x.as_deref().unwrap_or("")).collect::<Vec<_>>().join(" ")),
|
||||
None => base.clone(),
|
||||
};
|
||||
let font = match self.font_cache.get(&key) {
|
||||
Some(f) => f.clone(),
|
||||
None => {
|
||||
// Not embedded: a bundled stand-in, as for PDF.
|
||||
let prog = self.embedded.get(&base).cloned();
|
||||
let f = Rc::new(Font::standalone(&base, prog.as_ref().map(|(d, cff)| (d.as_slice(), *cff)), names));
|
||||
self.font_cache.insert(key, f.clone());
|
||||
f
|
||||
}
|
||||
};
|
||||
Some((font, dict_matrix(d, "__m")))
|
||||
}
|
||||
|
||||
/// A string's glyph outlines (device space) from the current point, with an extra
|
||||
/// displacement `extra(index, code)` (user space) after each glyph; moves the current
|
||||
/// point.
|
||||
fn text_path(&mut self, bytes: &[u8], mut extra: impl FnMut(usize, u32) -> (f64, f64)) -> Option<(BezPath, String)> {
|
||||
let d = self.gs.font.clone()?;
|
||||
let (font, m) = self.font_of(&d)?;
|
||||
let mut p = self.current_user();
|
||||
let mut path = BezPath::new();
|
||||
let mut text = String::new();
|
||||
let ctm = self.gs.ctm;
|
||||
for (i, &code) in bytes.iter().enumerate() {
|
||||
let code = code as u32;
|
||||
if let Some(g) = font.glyph(code) {
|
||||
path.extend(ctm * Affine::translate(p.to_vec2()) * m * (*g).clone());
|
||||
}
|
||||
text.push_str(&font.unicode(code));
|
||||
let adv = m * Point::new(font.width(code), 0.0) - m * Point::ZERO;
|
||||
let (ex, ey) = extra(i, code);
|
||||
p += adv + kurbo::Vec2::new(ex, ey);
|
||||
}
|
||||
self.move_to(p);
|
||||
Some((path, text))
|
||||
}
|
||||
|
||||
fn fill_text(&mut self, path: BezPath, text: &str) {
|
||||
let label: String = text.chars().filter(|c| !c.is_control()).take(40).collect();
|
||||
let name = if label.trim().is_empty() { "Text".to_string() } else { format!("Text: {}", label.trim()) };
|
||||
self.b.fill_stroke_named(&name, path, Affine::IDENTITY, Some((Paint::Color(self.gs.color), 1.0, FillRule::NonZero)), None);
|
||||
}
|
||||
|
||||
fn pop_string(&mut self) -> Vec<u8> {
|
||||
match self.pop() {
|
||||
V::Str(s) => s.borrow().clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn show_with(&mut self, s: &[u8], extra: impl FnMut(usize, u32) -> (f64, f64)) {
|
||||
match self.text_path(s, extra) {
|
||||
Some((path, text)) => self.fill_text(path, &text),
|
||||
None => self.b.skip("text (no font)"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Text and font operators (PostScript Language Reference §5.1, §8.2).
|
||||
fn text_op(&mut self, n: &str, src: &mut Src) -> Result<(), Flow> {
|
||||
match n {
|
||||
"scalefont" | "makefont" => {
|
||||
let m = if n == "scalefont" {
|
||||
Affine::scale(self.popn())
|
||||
} else {
|
||||
let v = self.pop();
|
||||
matrix_of(&v).unwrap_or(Affine::IDENTITY)
|
||||
};
|
||||
let font = match self.pop() {
|
||||
V::Dict(d) => d,
|
||||
_ => self.find_font("Helvetica"),
|
||||
};
|
||||
self.push(V::Dict(transformed(&font, m)));
|
||||
}
|
||||
"selectfont" => {
|
||||
let size = self.pop();
|
||||
let key = self.pop().key();
|
||||
let font = self.find_font(&key);
|
||||
let m = match &size {
|
||||
V::Num(s) => Affine::scale(*s),
|
||||
other => matrix_of(other).unwrap_or(Affine::IDENTITY),
|
||||
};
|
||||
self.gs.font = Some(transformed(&font, m));
|
||||
}
|
||||
"setfont" => {
|
||||
if let V::Dict(d) = self.pop() {
|
||||
self.gs.font = Some(d);
|
||||
}
|
||||
}
|
||||
"currentfont" | "rootfont" => {
|
||||
let d = self.gs.font.clone().unwrap_or_else(new_dict);
|
||||
self.push(V::Dict(d));
|
||||
}
|
||||
"definefont" => {
|
||||
let v = self.pop();
|
||||
let key = self.pop().key();
|
||||
if let V::Dict(d) = &v {
|
||||
if !d.borrow().contains_key("__base") {
|
||||
// A font the file defines itself: its embedded program, by name.
|
||||
d.borrow_mut().insert("__base".into(), V::Lit(key.as_str().into()));
|
||||
d.borrow_mut().insert("__m".into(), matrix_val(Affine::IDENTITY));
|
||||
}
|
||||
self.font_dir.insert(key, d.clone());
|
||||
}
|
||||
self.push(v);
|
||||
}
|
||||
"undefinefont" => {
|
||||
let key = self.pop().key();
|
||||
self.font_dir.remove(&key);
|
||||
}
|
||||
"StandardEncoding" | "ISOLatin1Encoding" => self.push(encoding_array(n)),
|
||||
"show" => {
|
||||
let s = self.pop_string();
|
||||
self.show_with(&s, |_, _| (0.0, 0.0));
|
||||
}
|
||||
"ashow" => {
|
||||
let s = self.pop_string();
|
||||
let ay = self.popn();
|
||||
let ax = self.popn();
|
||||
self.show_with(&s, |_, _| (ax, ay));
|
||||
}
|
||||
"widthshow" => {
|
||||
let s = self.pop_string();
|
||||
let ch = self.popn() as u32;
|
||||
let cy = self.popn();
|
||||
let cx = self.popn();
|
||||
self.show_with(&s, |_, c| if c == ch { (cx, cy) } else { (0.0, 0.0) });
|
||||
}
|
||||
"awidthshow" => {
|
||||
let s = self.pop_string();
|
||||
let ay = self.popn();
|
||||
let ax = self.popn();
|
||||
let ch = self.popn() as u32;
|
||||
let cy = self.popn();
|
||||
let cx = self.popn();
|
||||
self.show_with(&s, |_, c| if c == ch { (cx + ax, cy + ay) } else { (ax, ay) });
|
||||
}
|
||||
"kshow" | "cshow" => {
|
||||
// The procedure between characters is not run.
|
||||
let s = self.pop_string();
|
||||
self.pop();
|
||||
self.show_with(&s, |_, _| (0.0, 0.0));
|
||||
}
|
||||
"xshow" | "yshow" | "xyshow" => {
|
||||
let disp: Vec<f64> = self.pop().items().unwrap_or_default().iter().filter_map(V::num).collect();
|
||||
let s = self.pop_string();
|
||||
self.explicit_show(&s, &disp, n);
|
||||
}
|
||||
"glyphshow" => {
|
||||
let name = self.pop().key();
|
||||
let Some(d) = self.gs.font.clone() else { return Ok(()) };
|
||||
// Drawn through a one-glyph encoding.
|
||||
let mut e: Vec<V> = (0..256).map(|_| V::Lit(".notdef".into())).collect();
|
||||
e[65] = V::Lit(name.as_str().into());
|
||||
let tmp = transformed(&d, Affine::IDENTITY);
|
||||
tmp.borrow_mut().insert("Encoding".into(), array(e));
|
||||
self.gs.font = Some(tmp);
|
||||
self.show_with(b"A", |_, _| (0.0, 0.0));
|
||||
self.gs.font = Some(d);
|
||||
}
|
||||
"charpath" => {
|
||||
self.pop();
|
||||
let s = self.pop_string();
|
||||
let saved = std::mem::take(&mut self.gs.path);
|
||||
let start = self.gs.start;
|
||||
match self.text_path(&s, |_, _| (0.0, 0.0)) {
|
||||
Some((glyphs, _)) => {
|
||||
let mut p = saved;
|
||||
p.extend(glyphs);
|
||||
if let Some(c) = self.gs.cur {
|
||||
p.move_to(c);
|
||||
}
|
||||
self.gs.path = p;
|
||||
self.gs.start = start;
|
||||
}
|
||||
None => self.gs.path = saved,
|
||||
}
|
||||
}
|
||||
"stringwidth" => {
|
||||
let s = self.pop_string();
|
||||
let (mut w, mut h) = (0.0, 0.0);
|
||||
if let Some(d) = self.gs.font.clone()
|
||||
&& let Some((font, m)) = self.font_of(&d)
|
||||
{
|
||||
for &c in &s {
|
||||
let v = m * Point::new(font.width(c as u32), 0.0) - m * Point::ZERO;
|
||||
w += v.x;
|
||||
h += v.y;
|
||||
}
|
||||
}
|
||||
self.push(V::Num(w));
|
||||
self.push(V::Num(h));
|
||||
}
|
||||
"setcachedevice" => {
|
||||
for _ in 0..6 {
|
||||
self.pop();
|
||||
}
|
||||
}
|
||||
"setcharwidth" => {
|
||||
self.pop();
|
||||
self.pop();
|
||||
}
|
||||
// An embedded Type 1 program's encrypted part (read by `embedded_fonts`).
|
||||
"eexec" => {
|
||||
if matches!(self.pop(), V::File) {
|
||||
let rest = &src.d[src.pos.min(src.d.len())..];
|
||||
src.pos += rest.windows(11).position(|w| w == b"cleartomark").map_or(rest.len(), |p| p + 11);
|
||||
}
|
||||
}
|
||||
// A CFF font set's binary data (read by `embedded_fonts`).
|
||||
"StartData" => {
|
||||
let len = self.popn().max(0.0) as usize;
|
||||
self.pop();
|
||||
src.pos = src.pos.saturating_add(1).saturating_add(len).min(src.d.len());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `xshow` / `yshow` / `xyshow`: glyph displacements from a number array instead of the
|
||||
/// advances.
|
||||
fn explicit_show(&mut self, s: &[u8], disp: &[f64], n: &str) {
|
||||
let step = if n == "xyshow" { 2 } else { 1 };
|
||||
let d: Vec<(f64, f64)> = (0..s.len())
|
||||
.map(|k| {
|
||||
let i = k * step;
|
||||
let v = |j: usize| disp.get(j).copied().unwrap_or(0.0);
|
||||
match n {
|
||||
"xshow" => (v(i), 0.0),
|
||||
"yshow" => (0.0, v(i)),
|
||||
_ => (v(i), v(i + 1)),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let Some(fd) = self.gs.font.clone() else {
|
||||
self.b.skip("text (no font)");
|
||||
return;
|
||||
};
|
||||
let Some((font, m)) = self.font_of(&fd) else { return };
|
||||
self.show_with(s, move |k, c| {
|
||||
let adv = m * Point::new(font.width(c), 0.0) - m * Point::ZERO;
|
||||
let (dx, dy) = d.get(k).copied().unwrap_or((0.0, 0.0));
|
||||
(dx - adv.x, dy - adv.y)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,8 @@ showpage\n%%EOF\n";
|
|||
fn eps_postscript_subset() {
|
||||
let doc = parse(EPS.as_bytes()).unwrap();
|
||||
assert_eq!((doc.width, doc.height), (100.0, 100.0));
|
||||
assert!(doc.skipped.contains(&"text".to_string()));
|
||||
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
|
||||
assert!(shape_names(&doc).contains(&"Text: text".to_string()), "EPS text is drawn: {:?}", shape_names(&doc));
|
||||
let at = |x, y| px(&doc, x, y);
|
||||
assert!(at(25, 75)[0] > 0.99 && at(25, 75)[3] > 0.99, "red box {:?}", at(25, 75));
|
||||
assert!(at(70, 50)[2] > 0.99, "blue disc {:?}", at(70, 50));
|
||||
|
|
|
|||
|
|
@ -2,7 +2,90 @@
|
|||
//! corrupt inputs (generated fixtures).
|
||||
|
||||
use super::*;
|
||||
use crate::tests::{page_pdf, render};
|
||||
use crate::tests::{coverage, page_pdf, render, shape_names};
|
||||
|
||||
fn eps(body: &[u8]) -> Doc {
|
||||
let mut b = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 200 100\n".to_vec();
|
||||
b.extend_from_slice(body);
|
||||
let doc = parse(&b).unwrap();
|
||||
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
|
||||
doc
|
||||
}
|
||||
|
||||
/// The ink columns `(x0, x1)` of a render (alpha > 0.5).
|
||||
fn ink_x(img: &effectcraft_raster::Image) -> (i64, i64) {
|
||||
let (mut x0, mut x1) = (i64::MAX, i64::MIN);
|
||||
for y in 0..img.height as i64 {
|
||||
for x in 0..img.width as i64 {
|
||||
if img.get(x, y)[3] > 0.5 {
|
||||
(x0, x1) = (x0.min(x), x1.max(x));
|
||||
}
|
||||
}
|
||||
}
|
||||
(x0, x1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_text_with_bundled_fonts_and_show_variants() {
|
||||
// Not embedded: drawn with the bundled stand-in, like the same text in a PDF.
|
||||
let doc = eps(b"/Helvetica findfont 40 scalefont setfont 10 30 moveto (Hi) show\n");
|
||||
assert_eq!(shape_names(&doc), vec!["Text: Hi"]);
|
||||
let pdf = parse(&page_pdf("BT /F1 40 Tf 10 30 Td (Hi) Tj ET", "/Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >>", vec![])).unwrap();
|
||||
let (a, b) = (render(&doc), render(&pdf));
|
||||
let (ia, ib) = (coverage(&a, 0, 0, 200, 100), coverage(&b, 0, 0, 200, 100));
|
||||
assert!(ia > 100.0 && (ia - ib).abs() < 1.0, "eps {ia} vs pdf {ib}");
|
||||
assert_eq!(ink_x(&a), ink_x(&b));
|
||||
// ashow spreads the letters; widthshow widens spaces only; selectfont; stringwidth centres.
|
||||
let plain = ink_x(&render(&eps(b"/Helvetica 30 selectfont 10 30 moveto (HHH) show\n")));
|
||||
let spread = ink_x(&render(&eps(b"/Helvetica 30 selectfont 10 30 moveto 10 0 (HHH) ashow\n")));
|
||||
assert_eq!(spread.0, plain.0);
|
||||
assert!(((spread.1 - plain.1) - 20).abs() <= 1, "{plain:?} {spread:?}");
|
||||
let ws = ink_x(&render(&eps(b"/Helvetica 30 selectfont 10 30 moveto 15 0 72 (HHH) widthshow\n")));
|
||||
assert!(((ws.1 - plain.1) - 30).abs() <= 1, "{plain:?} {ws:?}");
|
||||
let centred = ink_x(&render(&eps(b"/Helvetica 30 selectfont 100 (HHH) stringwidth pop 2 div sub 30 moveto (HHH) show\n")));
|
||||
let mid = (centred.0 + centred.1) as f64 / 2.0;
|
||||
assert!((mid - 100.0).abs() < 3.0, "{centred:?}");
|
||||
// xshow: explicit advances.
|
||||
let xs = ink_x(&render(&eps(b"/Helvetica 30 selectfont 10 30 moveto (HH) [50 0] xshow\n")));
|
||||
let one = ink_x(&render(&eps(b"/Helvetica 30 selectfont 10 30 moveto (H) show\n")));
|
||||
assert!(((xs.1 - one.1) - 50).abs() <= 1, "{one:?} {xs:?}");
|
||||
// makefont mirrors; charpath clips; colour applies.
|
||||
let doc = eps(b"1 0 0 setrgbcolor /Helvetica findfont [40 0 0 40 0 0] makefont setfont 10 30 moveto (I) false charpath clip 0 0 200 100 rectfill\n");
|
||||
let img = render(&doc);
|
||||
assert!(coverage(&img, 0, 0, 200, 100) > 50.0);
|
||||
assert!(coverage(&img, 60, 0, 200, 100) < 1.0, "only the glyph is painted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_text_with_an_embedded_type1_font_and_reencoding() {
|
||||
// hsbw 50 600; 0 0 rmoveto; 500 hlineto 500 vlineto -500 hlineto closepath endchar
|
||||
let cs = vec![50 + 139, 248, 236, 13, 139, 139, 21, 248, 136, 6, 248, 136, 7, 252, 136, 6, 9, 14];
|
||||
let raw = crate::type1::write_test_type1(&[(".notdef", vec![139, 139, 13, 14]), ("box", cs)], &[(66, "box")]);
|
||||
// Give the program a name.
|
||||
let mut font = vec![];
|
||||
let nl = raw.iter().position(|c| *c == b'\n').unwrap() + 1;
|
||||
font.extend_from_slice(&raw[..nl]);
|
||||
font.extend_from_slice(b"12 dict begin /FontName /BoxFont def /FontType 1 def\n");
|
||||
font.extend_from_slice(&raw[nl..]);
|
||||
let mut body = font.clone();
|
||||
body.extend_from_slice(b"\n/BoxFont findfont 100 scalefont setfont 0 0 moveto (BB) show\n");
|
||||
let doc = eps(&body);
|
||||
let img = render(&doc);
|
||||
// Boxes 50..550 / 650..1150 font units at size 100: x 5..55 and 65..115, y 0..50.
|
||||
for (x, on) in [(30, true), (60, false), (90, true), (120, false)] {
|
||||
assert_eq!(img.get(x, 75)[3] > 0.99, on, "x = {x}");
|
||||
}
|
||||
// Re-encoded copy (`forall` / `definefont`): code 65 → box.
|
||||
let mut body = font;
|
||||
body.extend_from_slice(
|
||||
b"\n/BoxFont findfont dup length dict begin { 1 index /FID ne { def } { pop pop } ifelse } forall \
|
||||
/Encoding 256 array def 0 1 255 { Encoding exch /.notdef put } for Encoding 65 /box put currentdict end \
|
||||
/BoxA exch definefont pop /BoxA findfont 100 scalefont setfont 0 0 moveto (AB) show\n",
|
||||
);
|
||||
let img = render(&eps(&body));
|
||||
assert!(img.get(30, 75)[3] > 0.99, "A is the box now");
|
||||
assert!(img.get(90, 75)[3] < 0.01, "B is unencoded");
|
||||
}
|
||||
|
||||
/// An image XObject of `img` (black = true) CCITT-encoded with `k`, drawn over the page.
|
||||
fn ccitt_page(img: &[Vec<bool>], k: i64, mask: bool) -> Doc {
|
||||
|
|
|
|||
Loading…
Reference in New Issue