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:
Brandon Thomas 2026-10-03 09:01:10 -04:00
parent d0df520bc1
commit 740faf1c1f
5 changed files with 530 additions and 64 deletions

View File

@ -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;

View File

@ -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)
{

View File

@ -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)
});
}
}

View File

@ -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));

View File

@ -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 {