use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use super::cmap::{CMap, MapValue, utf16be_bytes_to_string};
use super::data::{
MAC_ROMAN, STANDARD, SYMBOL, WIN_ANSI, ZAPF_DINGBATS, encoding_by_name, font_basic_metrics,
glyph_unicode, has_standard14_metrics, is_serif_font, is_symbols_font, normalize_font_name,
standard14_default_width, standard14_glyph_width,
};
use super::filters::decode_stream;
use super::object::{Dict, Object, Ref, Stream};
use super::parser::Resolver;
use super::xref::XRef;
const DEFAULT_ASCENT: f64 = 0.88;
const DEFAULT_DESCENT: f64 = -0.12;
const PROVISIONAL_WIDTH: f64 = 500.0;
const FLAG_SYMBOLIC: i64 = 4;
const FLAG_NONSYMBOLIC: i64 = 32;
const FLAG_ITALIC: i64 = 64;
const FLAG_FORCE_BOLD: i64 = 262_144;
pub(crate) type VMetric = [f64; 3];
#[derive(Debug)]
pub(crate) struct LoadedFont {
name: String,
base_font: String,
widths: HashMap<u32, f64>,
default_width: f64,
vmetrics: HashMap<u32, VMetric>,
default_vmetric: Option<VMetric>,
to_unicode: ToUnicode,
encoding_cmap: Option<Arc<CMap>>,
ucs2_cmap: Option<Arc<CMap>>,
ascent: f64,
descent: f64,
bold: bool,
italic: bool,
uid: u64,
}
fn next_font_uid() -> u64 {
static NEXT: AtomicU64 = AtomicU64::new(0);
NEXT.fetch_add(1, Ordering::Relaxed)
}
#[derive(Debug, Clone)]
enum ToUnicode {
Identity,
Map(HashMap<u32, String>),
}
impl ToUnicode {
fn empty() -> Self {
ToUnicode::Map(HashMap::new())
}
}
impl LoadedFont {
pub fn stub(name: impl Into<String>) -> Self {
let name = name.into();
Self {
name,
base_font: String::new(),
widths: HashMap::new(),
default_width: PROVISIONAL_WIDTH,
vmetrics: HashMap::new(),
default_vmetric: None,
to_unicode: ToUnicode::empty(),
encoding_cmap: None,
ucs2_cmap: None,
ascent: DEFAULT_ASCENT,
descent: DEFAULT_DESCENT,
bold: false,
italic: false,
uid: next_font_uid(),
}
}
pub fn uid(&self) -> u64 {
self.uid
}
pub fn display_name(&self) -> &str {
if self.base_font.is_empty() {
&self.name
} else {
&self.base_font
}
}
pub fn text_font_name<'a>(&'a self, resource_name: &'a str) -> &'a str {
if self.base_font.is_empty() {
if resource_name.is_empty() {
self.display_name()
} else {
resource_name
}
} else {
self.display_name()
}
}
pub fn glyph_count(&self, bytes: &[u8]) -> usize {
if let Some(cmap) = &self.encoding_cmap {
let mut i = 0;
let mut n = 0;
while i < bytes.len() {
let (_, len) = cmap.read_char_code(bytes, i).unwrap_or((0, 1));
i += len;
n += 1;
}
n
} else {
bytes.len()
}
}
pub fn for_each_glyph<'a, F>(&'a self, bytes: &[u8], mut visit: F)
where
F: FnMut(Glyph<'a>),
{
if let Some(cmap) = &self.encoding_cmap {
let mut i = 0;
while i < bytes.len() {
let (charcode, len) = cmap.read_char_code(bytes, i).unwrap_or((0, 1));
let is_space = len == 1 && bytes[i] == 0x20;
i += len;
let cid = charcode_to_cid(cmap, charcode);
let width = self.widths.get(&cid).copied().unwrap_or(self.default_width);
visit(Glyph {
unicode: self.resolve_unicode(charcode, cid, true),
width,
is_space,
vmetric: self.vmetric_for(cid),
is_code_space: charcode == 0x20,
});
}
} else {
for &b in bytes {
let charcode = u32::from(b);
let width = self
.widths
.get(&charcode)
.copied()
.unwrap_or(self.default_width);
visit(Glyph {
unicode: self.resolve_unicode(charcode, charcode, false),
width,
is_space: b == 0x20,
vmetric: self.vmetric_for(charcode),
is_code_space: charcode == 0x20,
});
}
}
}
#[cfg(test)]
pub fn chars_to_glyphs(&self, bytes: &[u8]) -> Vec<Glyph<'_>> {
let mut out = Vec::with_capacity(self.glyph_count(bytes));
self.for_each_glyph(bytes, |g| out.push(g));
out
}
pub fn vmetric_for(&self, cid: u32) -> Option<VMetric> {
let default = self.default_vmetric?;
Some(self.vmetrics.get(&cid).copied().unwrap_or(default))
}
fn resolve_unicode(&self, charcode: u32, cid: u32, composite: bool) -> Cow<'_, str> {
match &self.to_unicode {
ToUnicode::Map(m) => {
if let Some(s) = m.get(&charcode) {
return Cow::Borrowed(s.as_str());
}
}
ToUnicode::Identity => return Cow::Owned(codepoint_to_string(charcode)),
}
if composite {
if let Some(ucs2) = &self.ucs2_cmap {
if let Some(MapValue::Bytes(b)) = ucs2.lookup(cid).as_deref() {
let s = utf16be_bytes_to_string(b);
if !s.is_empty() {
return Cow::Owned(s);
}
}
}
return Cow::Owned(codepoint_to_string(charcode));
}
if charcode <= 0xff {
return Cow::Owned(char::from(charcode as u8).to_string());
}
Cow::Owned(codepoint_to_string(charcode))
}
pub fn vertical(&self) -> bool {
self.encoding_cmap
.as_ref()
.map(|c| c.vertical())
.unwrap_or(false)
}
pub fn ascent(&self) -> f64 {
self.ascent
}
pub fn descent(&self) -> f64 {
self.descent
}
pub fn bold(&self) -> bool {
self.bold
}
pub fn italic(&self) -> bool {
self.italic
}
}
pub(crate) struct Glyph<'a> {
pub unicode: Cow<'a, str>,
pub width: f64,
pub is_space: bool,
pub vmetric: Option<VMetric>,
pub is_code_space: bool,
}
fn u32_or_zero(n: i64) -> u32 {
u32::try_from(n).unwrap_or(0)
}
fn charcode_to_cid(cmap: &CMap, charcode: u32) -> u32 {
match cmap.lookup(charcode).as_deref() {
Some(MapValue::Cid(cid)) => *cid,
Some(MapValue::Bytes(b)) => super::cmap::bytes_to_int(b),
None => charcode,
}
}
pub(crate) fn lookup_font(
resources: &Dict,
name: &str,
data: &[u8],
xref: Option<&XRef>,
) -> Arc<LoadedFont> {
let Some(font_res) = resources.get("Font") else {
return Arc::new(LoadedFont::stub(name));
};
let fonts = match resolve_obj(font_res, data, xref) {
Object::Dict(d) => d,
_ => return Arc::new(LoadedFont::stub(name)),
};
let Some(entry) = fonts.get(name) else {
return Arc::new(LoadedFont::stub(name));
};
if let (Object::Ref(r), Some(xref)) = (entry, xref) {
let key = *r;
if let Ok(guard) = xref.font_cache.lock() {
if let Some(cached) = guard.get(&key) {
return Arc::clone(cached);
}
}
let loaded = Arc::new(load_font_object(entry, name, data, Some(xref)));
if let Ok(mut guard) = xref.font_cache.lock() {
if let Some(cached) = guard.get(&key) {
return Arc::clone(cached);
}
guard.insert(key, Arc::clone(&loaded));
}
return loaded;
}
Arc::new(load_font_object(entry, name, data, xref))
}
fn load_font_object(entry: &Object, name: &str, data: &[u8], xref: Option<&XRef>) -> LoadedFont {
match resolve_obj(entry, data, xref) {
Object::Dict(dict) => load_font_dict(&dict, name, data, xref),
_ => LoadedFont::stub(name),
}
}
fn load_font_dict(dict: &Dict, name: &str, data: &[u8], xref: Option<&XRef>) -> LoadedFont {
let subtype = dict
.get("Subtype")
.and_then(|o| match resolve_obj(o, data, xref) {
Object::Name(n) => Some(n),
_ => None,
})
.unwrap_or_default();
match subtype.as_ref() {
"Type0" => load_type0(dict, name, data, xref),
"Type3" => load_type3(dict, name, data, xref),
_ => load_simple(dict, name, data, xref),
}
}
fn load_simple(dict: &Dict, name: &str, data: &[u8], xref: Option<&XRef>) -> LoadedFont {
let first_char = u32_or_zero(dict_int(dict, "FirstChar", data, xref).unwrap_or(0));
let descriptor = font_descriptor(dict, data, xref);
let base_font = base_font_name(dict, data, xref);
let subtype = dict
.get("Subtype")
.and_then(|o| match resolve_obj(o, data, xref) {
Object::Name(n) => Some(n),
_ => None,
})
.unwrap_or_else(|| "Type1".into());
let flags = descriptor
.get("Flags")
.and_then(|o| as_int(&resolve_obj(o, data, xref)))
.unwrap_or(0);
let embedded = has_embedded_font(&descriptor, data, xref);
let (mut default_encoding, differences, base_encoding_name) =
resolve_simple_encoding(dict, &subtype, flags, &base_font, embedded, data, xref);
if base_encoding_name.is_none() {
if let Some(builtin) = type1_builtin_encoding(&descriptor, data, xref) {
for (code, name) in builtin {
default_encoding[code as usize] = Cow::Owned(name);
}
}
}
let (mut widths, mut default_width) =
extract_simple_widths(dict, &descriptor, first_char, data, xref);
if dict.get("Widths").is_none() {
let metrics_name = resolve_metrics_font_name(&base_font);
if let Some(dw) = standard14_default_width(&metrics_name) {
widths = build_char_code_to_width(&metrics_name, &default_encoding, &differences);
default_width = descriptor
.get("MissingWidth")
.and_then(|o| as_number(&resolve_obj(o, data, xref)))
.unwrap_or(dw);
} else {
default_width = PROVISIONAL_WIDTH;
widths.clear();
}
}
let mut to_unicode = load_tounicode(dict, None, data, xref);
if let ToUnicode::Map(map) = &mut to_unicode {
let simple_tu = simple_font_to_unicode(
&default_encoding,
&differences,
base_encoding_name.as_deref(),
);
if map.is_empty() {
*map = simple_tu;
} else {
for (cc, s) in simple_tu {
map.entry(cc).or_insert(s);
}
}
}
let (ascent, descent) = resolve_ascent_descent(&descriptor, Some(&base_font), data, xref);
let (bold, italic) = resolve_font_style(&descriptor, &base_font, data, xref);
LoadedFont {
name: name.into(),
base_font,
widths,
default_width,
vmetrics: HashMap::new(),
default_vmetric: None,
to_unicode,
encoding_cmap: None,
ucs2_cmap: None,
ascent,
descent,
bold,
italic,
uid: next_font_uid(),
}
}
fn load_type3(dict: &Dict, name: &str, data: &[u8], xref: Option<&XRef>) -> LoadedFont {
let first_char = u32_or_zero(dict_int(dict, "FirstChar", data, xref).unwrap_or(0));
let descriptor = font_descriptor(dict, data, xref);
let (mut widths, mut default_width) =
extract_simple_widths(dict, &descriptor, first_char, data, xref);
let fm0 = font_matrix_a(dict, data, xref);
let scale = fm0 * 1000.0;
for w in widths.values_mut() {
*w *= scale;
}
default_width *= scale;
let base_font = base_font_name(dict, data, xref);
let flags = descriptor
.get("Flags")
.and_then(|o| as_int(&resolve_obj(o, data, xref)))
.unwrap_or(0);
let (default_encoding, differences, base_encoding_name) =
resolve_simple_encoding(dict, "Type3", flags, &base_font, false, data, xref);
let mut to_unicode = load_tounicode(dict, None, data, xref);
if let ToUnicode::Map(map) = &mut to_unicode {
let simple_tu = simple_font_to_unicode(
&default_encoding,
&differences,
base_encoding_name.as_deref(),
);
if map.is_empty() {
*map = simple_tu;
} else {
for (cc, s) in simple_tu {
map.entry(cc).or_insert(s);
}
}
}
let (ascent, descent) = resolve_ascent_descent(&descriptor, Some(&base_font), data, xref);
let (bold, italic) = resolve_font_style(&descriptor, &base_font, data, xref);
LoadedFont {
name: name.into(),
base_font,
widths,
default_width,
vmetrics: HashMap::new(),
default_vmetric: None,
to_unicode,
encoding_cmap: None,
ucs2_cmap: None,
ascent,
descent,
bold,
italic,
uid: next_font_uid(),
}
}
fn load_type0(dict: &Dict, name: &str, data: &[u8], xref: Option<&XRef>) -> LoadedFont {
let cid_dict = descendant_font(dict, data, xref);
let descriptor = font_descriptor(&cid_dict, data, xref);
let mut base_font = base_font_name(&cid_dict, data, xref);
if base_font.is_empty() {
base_font = base_font_name(dict, data, xref);
}
let (widths, default_width) = extract_cid_widths(&cid_dict, data, xref);
let (encoding_cmap, encoding_origin) = load_encoding_cmap(dict, data, xref);
let to_unicode = load_tounicode(dict, Some(dict), data, xref);
let ucs2_cmap = if matches!(&to_unicode, ToUnicode::Map(m) if m.is_empty()) {
load_ucs2_companion(&encoding_cmap, encoding_origin, &cid_dict, data, xref)
} else {
None
};
let (vmetrics, default_vmetric) = if encoding_cmap.vertical() {
extract_cid_vmetrics(&cid_dict, default_width, data, xref)
} else {
(HashMap::new(), None)
};
let (ascent, descent) = resolve_ascent_descent(&descriptor, Some(&base_font), data, xref);
let (bold, italic) = resolve_font_style(&descriptor, &base_font, data, xref);
LoadedFont {
name: name.into(),
base_font,
widths,
default_width,
vmetrics,
default_vmetric,
to_unicode,
encoding_cmap: Some(encoding_cmap),
ucs2_cmap,
ascent,
descent,
bold,
italic,
uid: next_font_uid(),
}
}
fn descendant_font(type0: &Dict, data: &[u8], xref: Option<&XRef>) -> Dict {
let Some(df) = type0.get("DescendantFonts") else {
return Dict::new();
};
let arr = match resolve_obj(df, data, xref) {
Object::Array(a) => a,
_ => return Dict::new(),
};
let Some(first) = arr.first() else {
return Dict::new();
};
match resolve_obj(first, data, xref) {
Object::Dict(d) => d,
_ => Dict::new(),
}
}
fn font_descriptor(dict: &Dict, data: &[u8], xref: Option<&XRef>) -> Dict {
match dict.get("FontDescriptor") {
Some(o) => match resolve_obj(o, data, xref) {
Object::Dict(d) => d,
_ => Dict::new(),
},
None => Dict::new(),
}
}
fn base_font_name(dict: &Dict, data: &[u8], xref: Option<&XRef>) -> String {
match dict.get("BaseFont") {
Some(o) => match resolve_obj(o, data, xref) {
Object::Name(n) => n.into_owned(),
Object::Str(s) => String::from_utf8_lossy(&s).into_owned(),
_ => String::new(),
},
None => String::new(),
}
}
fn resolve_font_style(
descriptor: &Dict,
base_font: &str,
data: &[u8],
xref: Option<&XRef>,
) -> (bool, bool) {
let (desc_bold, desc_italic) = style_from_descriptor(descriptor, data, xref);
let (name_bold, name_italic) = style_from_font_name(base_font);
(desc_bold || name_bold, desc_italic || name_italic)
}
fn style_from_descriptor(descriptor: &Dict, data: &[u8], xref: Option<&XRef>) -> (bool, bool) {
let flags = descriptor
.get("Flags")
.and_then(|o| as_int(&resolve_obj(o, data, xref)))
.unwrap_or(0);
let mut bold = flags & FLAG_FORCE_BOLD != 0;
let mut italic = flags & FLAG_ITALIC != 0;
if let Some(angle) = descriptor
.get("ItalicAngle")
.and_then(|o| as_number(&resolve_obj(o, data, xref)))
{
if angle.is_finite() && angle != 0.0 {
italic = true;
}
}
if let Some(weight) = descriptor
.get("FontWeight")
.and_then(|o| as_number(&resolve_obj(o, data, xref)))
{
if weight.is_finite() && weight >= 700.0 {
bold = true;
}
}
(bold, italic)
}
fn style_from_font_name(name: &str) -> (bool, bool) {
let key = style_name_key(name);
if key.is_empty() {
return (false, false);
}
let bold = name_key_is_bold(&key);
let italic = name_key_is_italic(&key);
(bold, italic)
}
fn style_name_key(name: &str) -> String {
let s = strip_subset_prefix(name);
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
',' | '_' | ' ' | '\t' | '-' => {
if !out.ends_with('-') {
out.push('-');
}
}
c => out.push(c.to_ascii_lowercase()),
}
}
out.trim_matches('-').to_owned()
}
fn strip_subset_prefix(name: &str) -> &str {
let bytes = name.as_bytes();
if bytes.len() > 7 && bytes[6] == b'+' && bytes[..6].iter().all(|b| b.is_ascii_uppercase()) {
&name[7..]
} else {
name
}
}
fn name_key_is_bold(key: &str) -> bool {
if key.contains("bold") || key.contains("black") || key.contains("heavy") {
return true;
}
if key.starts_with("cmbx") || key.starts_with("cmmib") {
return true;
}
for token in key.split('-') {
if token == "demi" || token.starts_with("demi") {
return true;
}
}
false
}
fn name_key_is_italic(key: &str) -> bool {
if key.contains("italic") || key.contains("oblique") || key.contains("boldit") {
return true;
}
if key.ends_with("-it") || key == "it" {
return true;
}
if key.starts_with("cmti") || key.starts_with("cmmi") {
return true;
}
false
}
fn extract_simple_widths(
dict: &Dict,
descriptor: &Dict,
first_char: u32,
data: &[u8],
xref: Option<&XRef>,
) -> (HashMap<u32, f64>, f64) {
let mut widths = HashMap::new();
let mut default_width = 0.0;
if let Some(wobj) = dict.get("Widths") {
if let Object::Array(arr) = resolve_obj(wobj, data, xref) {
let mut j = first_char;
for w in &arr {
let resolved = resolve_obj(w, data, xref);
if let Some(n) = as_number(&resolved) {
widths.insert(j, n);
}
j = j.saturating_add(1);
}
default_width = descriptor
.get("MissingWidth")
.and_then(|o| as_number(&resolve_obj(o, data, xref)))
.unwrap_or(0.0);
}
}
(widths, default_width)
}
fn extract_cid_widths(
cid_dict: &Dict,
data: &[u8],
xref: Option<&XRef>,
) -> (HashMap<u32, f64>, f64) {
let default_width = cid_dict
.get("DW")
.and_then(|o| as_number(&resolve_obj(o, data, xref)))
.map(|n| n.ceil())
.unwrap_or(1000.0);
let mut widths = HashMap::new();
let Some(wobj) = cid_dict.get("W") else {
return (widths, default_width);
};
let Object::Array(arr) = resolve_obj(wobj, data, xref) else {
return (widths, default_width);
};
let mut i = 0;
while i < arr.len() {
let start_obj = resolve_obj(&arr[i], data, xref);
let Some(start) = as_int(&start_obj) else {
break; };
i += 1;
if i >= arr.len() {
break;
}
let next = resolve_obj(&arr[i], data, xref);
if let Object::Array(ws) = next {
let mut c = u32_or_zero(start);
for w in &ws {
if let Some(n) = as_number(&resolve_obj(w, data, xref)) {
widths.insert(c, n);
}
c = c.saturating_add(1);
}
i += 1;
} else if let Some(end) = as_int(&next) {
i += 1;
if i >= arr.len() {
break;
}
let w_obj = resolve_obj(&arr[i], data, xref);
let Some(w) = as_number(&w_obj) else {
continue;
};
i += 1;
let s = u32_or_zero(start);
let e = u32_or_zero(end);
let e = e.min(65535);
if s <= e {
for c in s..=e {
widths.insert(c, w);
}
}
} else {
break;
}
}
(widths, default_width)
}
fn extract_cid_vmetrics(
cid_dict: &Dict,
default_width: f64,
data: &[u8],
xref: Option<&XRef>,
) -> (HashMap<u32, VMetric>, Option<VMetric>) {
let (vy, w1y) = match cid_dict.get("DW2") {
Some(o) => match resolve_obj(o, data, xref) {
Object::Array(arr) if arr.len() == 2 => {
let a = as_number(&resolve_obj(&arr[0], data, xref));
let b = as_number(&resolve_obj(&arr[1], data, xref));
match (a, b) {
(Some(a), Some(b)) => (a, b),
_ => (880.0, -1000.0),
}
}
_ => (880.0, -1000.0),
},
None => (880.0, -1000.0),
};
let default_vmetric: VMetric = [w1y, default_width * 0.5, vy];
let mut vmetrics = HashMap::new();
let Some(w2obj) = cid_dict.get("W2") else {
return (vmetrics, Some(default_vmetric));
};
let Object::Array(arr) = resolve_obj(w2obj, data, xref) else {
return (vmetrics, Some(default_vmetric));
};
let mut i = 0;
while i < arr.len() {
let start_obj = resolve_obj(&arr[i], data, xref);
let Some(start) = as_int(&start_obj) else {
break;
};
i += 1;
if i >= arr.len() {
break;
}
let next = resolve_obj(&arr[i], data, xref);
if let Object::Array(vals) = next {
let mut c = u32_or_zero(start);
let mut j = 0;
while j + 2 < vals.len() {
let w1 = as_number(&resolve_obj(&vals[j], data, xref));
let vx = as_number(&resolve_obj(&vals[j + 1], data, xref));
let vy = as_number(&resolve_obj(&vals[j + 2], data, xref));
if let (Some(w1), Some(vx), Some(vy)) = (w1, vx, vy) {
vmetrics.insert(c, [w1, vx, vy]);
}
c = c.saturating_add(1);
j += 3;
}
i += 1;
} else if let Some(end) = as_int(&next) {
i += 1;
if i + 2 >= arr.len() {
break;
}
let w1 = as_number(&resolve_obj(&arr[i], data, xref));
let vx = as_number(&resolve_obj(&arr[i + 1], data, xref));
let vy = as_number(&resolve_obj(&arr[i + 2], data, xref));
i += 3;
let Some(w1) = w1 else { continue };
let Some(vx) = vx else { continue };
let Some(vy) = vy else { continue };
let s = u32_or_zero(start);
let e = u32_or_zero(end);
let e = e.min(65535);
if s <= e {
for c in s..=e {
vmetrics.insert(c, [w1, vx, vy]);
}
}
} else {
break;
}
}
(vmetrics, Some(default_vmetric))
}
enum EncodingOrigin {
Predefined,
Embedded,
}
fn load_encoding_cmap(
type0: &Dict,
data: &[u8],
xref: Option<&XRef>,
) -> (Arc<CMap>, EncodingOrigin) {
let Some(enc) = type0.get("Encoding") else {
warn(xref, "Type0 font missing /Encoding; treating as Identity-H");
return (Arc::new(CMap::identity_h()), EncodingOrigin::Predefined);
};
match resolve_obj(enc, data, xref) {
Object::Name(n) => {
if let Some(c) = CMap::from_identity_name(&n) {
return (Arc::new(c), EncodingOrigin::Predefined);
}
match load_predefined_cmap(&n, xref) {
Ok(c) => (c, EncodingOrigin::Predefined),
Err(e) => {
warn(
xref,
format!(
"failed to load predefined CMap '{n}' ({e}); treating as Identity-H"
),
);
(Arc::new(CMap::identity_h()), EncodingOrigin::Predefined)
}
}
}
Object::Stream(s) => {
let decoded = decode_stream_bytes(data, &s, xref).unwrap_or_default();
let limit = cmap_entries_limit(xref);
let cmap = CMap::parse_embedded(&decoded, limit);
if cmap.truncated() {
warn(
xref,
"embedded /Encoding CMap exceeded entry limit; using Identity-H",
);
return (Arc::new(CMap::identity_h()), EncodingOrigin::Predefined);
}
(Arc::new(cmap), EncodingOrigin::Embedded)
}
_ => {
warn(xref, "invalid /Encoding; treating as Identity-H");
(Arc::new(CMap::identity_h()), EncodingOrigin::Predefined)
}
}
}
fn cmap_entries_limit(xref: Option<&XRef>) -> usize {
xref.map(|x| x.cmap_entries_limit())
.unwrap_or(crate::extract::DEFAULT_MAX_CMAP_ENTRIES)
}
fn load_ucs2_companion(
encoding_cmap: &CMap,
encoding_origin: EncodingOrigin,
cid_dict: &Dict,
data: &[u8],
xref: Option<&XRef>,
) -> Option<Arc<CMap>> {
let cid_info = read_cid_system_info(cid_dict, data, xref);
let use_ucs2 = (matches!(encoding_origin, EncodingOrigin::Predefined)
&& !encoding_cmap.is_identity())
|| cid_info
.as_ref()
.is_some_and(|(reg, ord)| reg == "Adobe" && is_cjk_ordering(ord));
if !use_ucs2 {
return None;
}
let (registry, ordering) = cid_info?;
let name = format!("{registry}-{ordering}-UCS2");
match load_predefined_cmap(&name, xref) {
Ok(c) => Some(c),
Err(e) => {
warn(xref, format!("failed to load UCS2 CMap '{name}' ({e})"));
None
}
}
}
fn load_predefined_cmap(name: &str, xref: Option<&XRef>) -> crate::error::Result<Arc<CMap>> {
let cache = xref.map(|x| x.cmap_cache.as_ref());
CMap::load_predefined_shared(name, cache)
}
fn is_cjk_ordering(ordering: &str) -> bool {
matches!(ordering, "GB1" | "CNS1" | "Japan1" | "Korea1")
}
fn read_cid_system_info(
cid_dict: &Dict,
data: &[u8],
xref: Option<&XRef>,
) -> Option<(String, String)> {
let info = cid_dict.get("CIDSystemInfo")?;
let Object::Dict(d) = resolve_obj(info, data, xref) else {
return None;
};
let registry = pdf_string_or_name(d.get("Registry")?, data, xref)?;
let ordering = pdf_string_or_name(d.get("Ordering")?, data, xref)?;
Some((registry, ordering))
}
fn pdf_string_or_name(obj: &Object, data: &[u8], xref: Option<&XRef>) -> Option<String> {
match resolve_obj(obj, data, xref) {
Object::Name(n) => Some(n.into_owned()),
Object::Str(s) => Some(String::from_utf8_lossy(&s).into_owned()),
_ => None,
}
}
fn load_tounicode(
dict: &Dict,
base_dict: Option<&Dict>,
data: &[u8],
xref: Option<&XRef>,
) -> ToUnicode {
let tu = dict
.get("ToUnicode")
.or_else(|| base_dict.and_then(|b| b.get("ToUnicode")));
let Some(tu) = tu else {
return ToUnicode::empty();
};
match resolve_obj(tu, data, xref) {
Object::Name(n) => {
if n == "Identity-H" || n == "Identity-V" || n == "Identity" {
return ToUnicode::Identity;
}
warn(xref, format!("named ToUnicode '{n}' not loaded yet"));
ToUnicode::empty()
}
Object::Stream(s) => {
let Some(decoded) = decode_stream_bytes(data, &s, xref) else {
return ToUnicode::empty();
};
let limit = cmap_entries_limit(xref);
let cmap = CMap::parse_embedded(&decoded, limit);
if cmap.truncated() {
warn(
xref,
"embedded ToUnicode CMap exceeded entry limit; discarding",
);
return ToUnicode::empty();
}
if cmap.is_identity() || cmap.name() == "Identity-H" || cmap.name() == "Identity-V" {
return ToUnicode::Identity;
}
let mut map = HashMap::new();
build_tounicode_from_cmap(&cmap, &mut map);
ToUnicode::Map(map)
}
_ => ToUnicode::empty(),
}
}
fn build_tounicode_from_cmap(cmap: &CMap, map: &mut HashMap<u32, String>) {
if cmap.is_identity() {
return;
}
cmap.for_each(|charcode, value| match value {
MapValue::Cid(cid) => {
map.insert(charcode, codepoint_to_string(*cid));
}
MapValue::Bytes(b) => {
map.insert(charcode, utf16be_bytes_to_string(b));
}
});
}
fn resolve_ascent_descent(
descriptor: &Dict,
base_font: Option<&str>,
data: &[u8],
xref: Option<&XRef>,
) -> (f64, f64) {
let mut ascent = descriptor
.get("Ascent")
.and_then(|o| as_number(&resolve_obj(o, data, xref)))
.map(|n| n / 1000.0);
let mut descent = descriptor
.get("Descent")
.and_then(|o| as_number(&resolve_obj(o, data, xref)))
.map(|n| n / 1000.0);
if let Some((a, d)) = try_truetype_metrics(descriptor, data, xref) {
ascent = Some(a);
descent = Some(d);
}
let need_a = ascent.map(|v| !v.is_finite() || v <= 0.0).unwrap_or(true);
let need_d = descent.map(|v| !v.is_finite() || v >= 0.0).unwrap_or(true);
if (need_a || need_d) && !has_embedded_font(descriptor, data, xref) {
if let Some(raw) = base_font {
let metrics_name = normalize_font_name(raw);
if let Some((ba, bd)) = font_basic_metrics(&metrics_name) {
if need_a {
if let Some(a) = ba {
ascent = Some(a / 1000.0);
}
}
if need_d {
if let Some(d) = bd {
descent = Some(d / 1000.0);
}
}
}
}
}
(correct_ascent(ascent), correct_descent(descent))
}
fn has_embedded_font(descriptor: &Dict, data: &[u8], xref: Option<&XRef>) -> bool {
for key in ["FontFile", "FontFile2", "FontFile3"] {
if let Some(o) = descriptor.get(key) {
match resolve_obj(o, data, xref) {
Object::Stream(_) | Object::Dict(_) => return true,
_ => {}
}
}
}
false
}
fn resolve_metrics_font_name(base_font: &str) -> String {
let normalized = normalize_font_name(base_font);
if has_standard14_metrics(&normalized) {
return normalized;
}
if is_serif_font(base_font) || is_serif_font(&normalized) {
"Times-Roman".into()
} else {
"Helvetica".into()
}
}
type SimpleEncoding = [Cow<'static, str>; 256];
fn resolve_simple_encoding(
dict: &Dict,
subtype: &str,
flags: i64,
base_font: &str,
embedded: bool,
data: &[u8],
xref: Option<&XRef>,
) -> (SimpleEncoding, HashMap<u32, String>, Option<String>) {
let mut differences: HashMap<u32, String> = HashMap::new();
let mut base_encoding_name: Option<String> = None;
if let Some(enc_obj) = dict.get("Encoding") {
match resolve_obj(enc_obj, data, xref) {
Object::Name(n) => {
if matches!(
n.as_ref(),
"MacRomanEncoding" | "MacExpertEncoding" | "WinAnsiEncoding"
) {
base_encoding_name = Some(n.into_owned());
}
}
Object::Dict(enc_dict) => {
if let Some(be) = enc_dict.get("BaseEncoding") {
if let Object::Name(n) = resolve_obj(be, data, xref) {
if matches!(
n.as_ref(),
"MacRomanEncoding"
| "MacExpertEncoding"
| "WinAnsiEncoding"
| "StandardEncoding"
| "ExpertEncoding"
| "SymbolSetEncoding"
| "ZapfDingbatsEncoding"
) {
base_encoding_name = Some(n.into_owned());
}
}
}
if let Some(diff) = enc_dict.get("Differences") {
if let Object::Array(arr) = resolve_obj(diff, data, xref) {
let mut code: i64 = 0;
for item in arr {
match resolve_obj(&item, data, xref) {
Object::Int(n) => code = n,
Object::Name(n) => {
if code >= 0 && code <= 255 {
differences.insert(code as u32, n.into_owned());
}
code += 1;
}
_ => {}
}
}
}
}
}
_ => {}
}
}
let norm = normalize_font_name(base_font);
if base_encoding_name.is_some() && !embedded && is_symbols_font(&norm) {
base_encoding_name = None;
}
let mut is_symbolic = (flags & FLAG_SYMBOLIC) != 0;
let is_nonsymbolic = (flags & FLAG_NONSYMBOLIC) != 0;
if subtype == "TrueType" && is_symbolic && is_nonsymbolic && !differences.is_empty() {
is_symbolic = false;
}
let table: &[&str; 256] = if let Some(ref n) = base_encoding_name {
encoding_by_name(n).unwrap_or(&STANDARD)
} else {
let mut enc: &[&str; 256] = &STANDARD;
if subtype == "TrueType" && !is_nonsymbolic {
enc = &WIN_ANSI;
}
if is_symbolic || is_symbols_font(&norm) {
enc = &MAC_ROMAN;
if !embedded {
if norm.contains("Symbol") || base_font.contains("Symbol") {
enc = &SYMBOL;
} else if norm.contains("Dingbats") || base_font.contains("Dingbats") {
enc = &ZAPF_DINGBATS;
} else if base_font.contains("Wingdings") {
enc = &WIN_ANSI;
}
}
}
enc
};
let default_encoding: SimpleEncoding = std::array::from_fn(|i| Cow::Borrowed(table[i]));
(default_encoding, differences, base_encoding_name)
}
fn build_char_code_to_width(
metrics_font: &str,
default_encoding: &SimpleEncoding,
differences: &HashMap<u32, String>,
) -> HashMap<u32, f64> {
if matches!(
metrics_font,
"Courier" | "Courier-Bold" | "Courier-BoldOblique" | "Courier-Oblique"
) {
return HashMap::new();
}
let mut widths = HashMap::new();
for char_code in 0u32..256 {
if let Some(gname) = differences.get(&char_code) {
if let Some(w) = standard14_glyph_width(metrics_font, gname) {
if w != 0.0 {
widths.insert(char_code, w);
continue;
}
}
}
let gname = &default_encoding[char_code as usize];
if gname.is_empty() {
continue;
}
if let Some(w) = standard14_glyph_width(metrics_font, gname) {
if w != 0.0 {
widths.insert(char_code, w);
}
}
}
widths
}
fn simple_font_to_unicode(
default_encoding: &SimpleEncoding,
differences: &HashMap<u32, String>,
base_encoding_name: Option<&str>,
) -> HashMap<u32, String> {
let mut encoding: SimpleEncoding = default_encoding.clone();
for (&cc, gname) in differences {
if gname == ".notdef" {
continue;
}
if (cc as usize) < 256 {
encoding[cc as usize] = Cow::Owned(gname.clone());
}
}
if let Some(map) = try_simple_to_unicode(&encoding, base_encoding_name, false) {
return map;
}
try_simple_to_unicode(&encoding, base_encoding_name, true).unwrap_or_default()
}
fn try_simple_to_unicode(
encoding: &SimpleEncoding,
base_encoding_name: Option<&str>,
force_glyphs: bool,
) -> Option<HashMap<u32, String>> {
let mut to_unicode = HashMap::new();
for charcode in 0u32..256 {
let glyph_name = &encoding[charcode as usize];
if glyph_name.is_empty() {
continue;
}
if let Some(s) = glyph_unicode(glyph_name) {
to_unicode.insert(charcode, s.to_string());
continue;
}
match glyph_name_to_codepoint(glyph_name, force_glyphs) {
GlyphNameCode::Code(code) => {
if code > 0 && code <= 0x10_ffff {
if let Some(ben) = base_encoding_name {
if code == charcode {
if let Some(table) = encoding_by_name(ben) {
let gn = table[charcode as usize];
if !gn.is_empty() {
if let Some(s) = glyph_unicode(gn) {
to_unicode.insert(charcode, s.to_string());
continue;
}
}
}
}
}
if let Some(c) = char::from_u32(code) {
to_unicode.insert(charcode, c.to_string());
}
}
}
GlyphNameCode::NotFound => {
if let Some(s) = ligature_fallback(glyph_name) {
to_unicode.insert(charcode, s);
}
}
GlyphNameCode::RetryAsHex => {
if !force_glyphs {
return None;
}
}
}
}
Some(to_unicode)
}
enum GlyphNameCode {
Code(u32),
NotFound,
RetryAsHex,
}
fn glyph_name_to_codepoint(glyph_name: &str, force_glyphs: bool) -> GlyphNameCode {
let bytes = glyph_name.as_bytes();
if bytes.is_empty() {
return GlyphNameCode::NotFound;
}
match bytes[0] {
b'G' if glyph_name.len() == 3 => {
if let Ok(code) = u32::from_str_radix(&glyph_name[1..], 16) {
return GlyphNameCode::Code(code);
}
}
b'g' if glyph_name.len() == 5 => {
if let Ok(code) = u32::from_str_radix(&glyph_name[1..], 16) {
return GlyphNameCode::Code(code);
}
}
b'C' | b'c' if (3..=4).contains(&glyph_name.len()) => {
let code_str = &glyph_name[1..];
if force_glyphs {
if let Ok(code) = u32::from_str_radix(code_str, 16) {
return GlyphNameCode::Code(code);
}
} else if let Ok(code) = code_str.parse::<u32>() {
return GlyphNameCode::Code(code);
} else if u32::from_str_radix(code_str, 16).is_ok() {
return GlyphNameCode::RetryAsHex;
}
}
b'u' => {
if let Some(code) = unicode_from_uni_name(glyph_name) {
return GlyphNameCode::Code(code);
}
}
_ => {}
}
GlyphNameCode::NotFound
}
fn unicode_from_uni_name(name: &str) -> Option<u32> {
let len = name.len();
let hex = if len == 7 && name.starts_with("uni") {
&name[3..]
} else if (5..=7).contains(&len) && name.starts_with('u') {
&name[1..]
} else {
return None;
};
if hex.chars().any(|c| c.is_ascii_lowercase()) {
return None;
}
if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
u32::from_str_radix(hex, 16).ok()
}
fn ligature_fallback(glyph_name: &str) -> Option<String> {
match glyph_name {
"f_h" => Some("fh".into()),
"f_t" => Some("ft".into()),
"T_h" => Some("Th".into()),
_ => None,
}
}
fn correct_ascent(a: Option<f64>) -> f64 {
match a {
Some(v) if v.is_finite() && v > 0.0 => v,
_ => DEFAULT_ASCENT,
}
}
fn correct_descent(d: Option<f64>) -> f64 {
match d {
Some(v) if v.is_finite() && v < 0.0 => v,
_ => DEFAULT_DESCENT,
}
}
fn find_sub(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|w| w == needle)
}
fn type1_builtin_encoding(
descriptor: &Dict,
data: &[u8],
xref: Option<&XRef>,
) -> Option<HashMap<u32, String>> {
let ff = descriptor.get("FontFile")?;
let stream = match resolve_obj(ff, data, xref) {
Object::Stream(s) => s,
_ => return None,
};
let bytes = decode_stream_bytes(data, &stream, xref)?;
let clear_end = find_sub(&bytes, b"eexec").unwrap_or(bytes.len());
let clear = &bytes[..clear_end];
let enc_pos = find_sub(clear, b"/Encoding")?;
let body = &clear[enc_pos + b"/Encoding".len()..];
if find_sub(&body[..body.len().min(64)], b"StandardEncoding").is_some() {
return None;
}
let end = find_sub(body, b" def").unwrap_or(body.len());
let body = &body[..end];
let mut map = HashMap::new();
let mut i = 0;
while i < body.len() {
let Some(rel) = find_sub(&body[i..], b"dup ") else {
break;
};
let mut p = i + rel + 4;
i = p;
while p < body.len() && body[p] == b' ' {
p += 1;
}
let num_start = p;
while p < body.len() && body[p].is_ascii_digit() {
p += 1;
}
let Some(code) = std::str::from_utf8(&body[num_start..p])
.ok()
.and_then(|s| s.parse::<u32>().ok())
.filter(|c| *c <= 255)
else {
continue;
};
while p < body.len() && body[p].is_ascii_whitespace() {
p += 1;
}
if body.get(p) != Some(&b'/') {
continue;
}
p += 1;
let name_start = p;
while p < body.len()
&& !body[p].is_ascii_whitespace()
&& !matches!(
body[p],
b'/' | b'(' | b')' | b'[' | b']' | b'<' | b'>' | b'{' | b'}' | b'%'
)
{
p += 1;
}
let name = String::from_utf8_lossy(&body[name_start..p]).into_owned();
while p < body.len() && body[p].is_ascii_whitespace() {
p += 1;
}
if body[p..].starts_with(b"put") && !name.is_empty() {
map.insert(code, name);
i = p;
}
}
if map.is_empty() { None } else { Some(map) }
}
fn try_truetype_metrics(descriptor: &Dict, data: &[u8], xref: Option<&XRef>) -> Option<(f64, f64)> {
let ff = descriptor.get("FontFile2")?;
let stream = match resolve_obj(ff, data, xref) {
Object::Stream(s) => s,
_ => return None,
};
let bytes = decode_stream_bytes(data, &stream, xref)?;
parse_ttf_ascent_descent(&bytes)
}
pub(crate) fn parse_ttf_ascent_descent(font: &[u8]) -> Option<(f64, f64)> {
if font.len() < 12 {
return None;
}
let num_tables = read_u16(font, 4)? as usize;
let mut head_off: Option<usize> = None;
let mut head_len: Option<usize> = None;
let mut hhea_off: Option<usize> = None;
let mut hhea_len: Option<usize> = None;
let mut pos = 12;
for _ in 0..num_tables {
if pos + 16 > font.len() {
break;
}
let tag = &font[pos..pos + 4];
let offset = read_u32(font, pos + 8)? as usize;
let length = read_u32(font, pos + 12)? as usize;
match tag {
b"head" => {
head_off = Some(offset);
head_len = Some(length);
}
b"hhea" => {
hhea_off = Some(offset);
hhea_len = Some(length);
}
_ => {}
}
pos += 16;
}
let head_off = head_off?;
let head_len = head_len?;
let hhea_off = hhea_off?;
let hhea_len = hhea_len?;
if head_len < 20 || hhea_len < 8 {
return None;
}
if head_off + 20 > font.len() || hhea_off + 8 > font.len() {
return None;
}
let units_per_em = read_u16(font, head_off + 18)? as f64;
if units_per_em <= 0.0 {
return None;
}
let ascent = read_i16(font, hhea_off + 4)? as f64 / units_per_em;
let descent = read_i16(font, hhea_off + 6)? as f64 / units_per_em;
Some((ascent, descent))
}
fn font_matrix_a(dict: &Dict, data: &[u8], xref: Option<&XRef>) -> f64 {
match dict.get("FontMatrix") {
Some(o) => match resolve_obj(o, data, xref) {
Object::Array(a) if !a.is_empty() => {
as_number(&resolve_obj(&a[0], data, xref)).unwrap_or(0.001)
}
_ => 0.001,
},
None => 0.001,
}
}
fn decode_stream_bytes(data: &[u8], stream: &Stream, xref: Option<&XRef>) -> Option<Vec<u8>> {
struct FetchResolver<'a> {
xref: Option<&'a XRef>,
data: &'a [u8],
}
impl Resolver for FetchResolver<'_> {
fn resolve(&self, r: Ref) -> crate::error::Result<Option<Object>> {
Ok(self.xref.and_then(|x| x.fetch(r, self.data).ok()))
}
}
decode_stream(
data,
stream,
&FetchResolver { xref, data },
xref.and_then(|x| x.cipher()),
xref.map_or(crate::extract::DEFAULT_MAX_DECODED_BYTES, |x| {
x.decode_limit()
}),
)
.ok()
}
fn resolve_obj(obj: &Object, data: &[u8], xref: Option<&XRef>) -> Object {
match obj {
Object::Ref(r) => {
if let Some(x) = xref {
x.fetch(*r, data).unwrap_or(Object::Null)
} else {
Object::Null
}
}
other => other.clone(),
}
}
fn dict_int(dict: &Dict, key: &str, data: &[u8], xref: Option<&XRef>) -> Option<i64> {
dict.get(key)
.and_then(|o| as_int(&resolve_obj(o, data, xref)))
}
fn as_number(obj: &Object) -> Option<f64> {
match obj {
Object::Int(n) => Some(*n as f64),
Object::Real(n) => Some(*n),
_ => None,
}
}
fn as_int(obj: &Object) -> Option<i64> {
match obj {
Object::Int(n) => Some(*n),
Object::Real(n) if n.fract() == 0.0 && n.is_finite() => Some(*n as i64),
_ => None,
}
}
fn codepoint_to_string(cp: u32) -> String {
char::from_u32(cp)
.map(|c| c.to_string())
.unwrap_or_default()
}
fn read_u16(data: &[u8], off: usize) -> Option<u16> {
if off + 2 > data.len() {
return None;
}
Some(u16::from_be_bytes([data[off], data[off + 1]]))
}
fn read_i16(data: &[u8], off: usize) -> Option<i16> {
if off + 2 > data.len() {
return None;
}
Some(i16::from_be_bytes([data[off], data[off + 1]]))
}
fn warn(xref: Option<&XRef>, msg: impl Into<String>) {
let msg = msg.into();
if let Some(x) = xref {
x.push_warning(format!("reader: {msg}"));
} else {
eprintln!("reader: {msg}");
}
}
fn read_u32(data: &[u8], off: usize) -> Option<u32> {
if off + 4 > data.len() {
return None;
}
Some(u32::from_be_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
]))
}
#[cfg(test)]
mod tests {
use super::super::object::Stream;
use super::*;
fn approx(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-9
}
fn minimal_ttf(units_per_em: u16, ascent: i16, descent: i16) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&0x0001_0000u32.to_be_bytes()); out.extend_from_slice(&2u16.to_be_bytes()); out.extend_from_slice(&32u16.to_be_bytes()); out.extend_from_slice(&1u16.to_be_bytes()); out.extend_from_slice(&0u16.to_be_bytes());
let head_offset = 12 + 32;
let head_len = 54usize;
let hhea_offset = head_offset + head_len;
let hhea_len = 36usize;
out.extend_from_slice(b"head");
out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&(head_offset as u32).to_be_bytes());
out.extend_from_slice(&(head_len as u32).to_be_bytes());
out.extend_from_slice(b"hhea");
out.extend_from_slice(&0u32.to_be_bytes());
out.extend_from_slice(&(hhea_offset as u32).to_be_bytes());
out.extend_from_slice(&(hhea_len as u32).to_be_bytes());
let mut head = vec![0u8; head_len];
head[18] = (units_per_em >> 8) as u8;
head[19] = (units_per_em & 0xff) as u8;
out.extend_from_slice(&head);
let mut hhea = vec![0u8; hhea_len];
let a = ascent.to_be_bytes();
let d = descent.to_be_bytes();
hhea[4] = a[0];
hhea[5] = a[1];
hhea[6] = d[0];
hhea[7] = d[1];
out.extend_from_slice(&hhea);
out
}
#[test]
fn stub_chars_latin1_and_space() {
let font = LoadedFont::stub("F1");
let gs = font.chars_to_glyphs(b"A \xff");
assert_eq!(gs.len(), 3);
assert_eq!(gs[0].unicode, "A");
assert!(approx(gs[0].width, 500.0));
assert!(!gs[0].is_space);
assert_eq!(gs[1].unicode, " ");
assert!(gs[1].is_space);
assert_eq!(gs[2].unicode, "\u{00ff}");
assert!(!gs[2].is_space);
}
#[test]
fn stub_metrics() {
let font = LoadedFont::stub("F1");
assert!(approx(font.ascent(), 0.88));
assert!(approx(font.descent(), -0.12));
}
#[test]
fn simple_widths_and_missing() {
let mut fd = Dict::new();
fd.set("MissingWidth", Object::Int(250));
fd.set("Ascent", Object::Int(800));
fd.set("Descent", Object::Int(-200));
let mut font_dict = Dict::new();
font_dict.set("Subtype", Object::Name("Type1".into()));
font_dict.set("FirstChar", Object::Int(65));
font_dict.set("LastChar", Object::Int(67));
font_dict.set(
"Widths",
Object::Array(vec![Object::Int(600), Object::Int(700), Object::Int(800)]),
);
font_dict.set("FontDescriptor", Object::Dict(fd));
let font = load_font_dict(&font_dict, "F1", &[], None);
let gs = font.chars_to_glyphs(b"ABC");
assert!(approx(gs[0].width, 600.0));
assert!(approx(gs[1].width, 700.0));
assert!(approx(gs[2].width, 800.0));
let out = font.chars_to_glyphs(&[0x20]);
assert!(approx(out[0].width, 250.0));
assert!(approx(font.ascent(), 0.8));
assert!(approx(font.descent(), -0.2));
}
#[test]
fn widths_ref_elements_and_non_numeric_skip() {
let mut fd = Dict::new();
fd.set("MissingWidth", Object::Int(100));
fd.set("Ascent", Object::Int(900));
fd.set("Descent", Object::Int(-100));
let mut font_dict = Dict::new();
font_dict.set("Subtype", Object::Name("TrueType".into()));
font_dict.set("FirstChar", Object::Int(0));
font_dict.set(
"Widths",
Object::Array(vec![
Object::Int(500),
Object::Name("skip".into()),
Object::Int(600),
]),
);
font_dict.set("FontDescriptor", Object::Dict(fd));
let font = load_font_dict(&font_dict, "F1", &[], None);
let gs = font.chars_to_glyphs(&[0, 1, 2]);
assert!(approx(gs[0].width, 500.0));
assert!(approx(gs[1].width, 100.0));
assert!(approx(gs[2].width, 600.0));
}
#[test]
fn cid_w_two_forms_and_dw() {
let mut cid = Dict::new();
cid.set("Subtype", Object::Name("CIDFontType2".into()));
cid.set("DW", Object::Int(1000));
cid.set(
"W",
Object::Array(vec![
Object::Int(1),
Object::Array(vec![Object::Int(100), Object::Int(200)]),
Object::Int(10),
Object::Int(12),
Object::Int(300),
]),
);
let mut type0 = Dict::new();
type0.set("Subtype", Object::Name("Type0".into()));
type0.set("Encoding", Object::Name("Identity-H".into()));
type0.set("DescendantFonts", Object::Array(vec![Object::Dict(cid)]));
let font = load_font_dict(&type0, "C0", &[], None);
let g = font.chars_to_glyphs(&[0x00, 0x01]);
assert_eq!(g.len(), 1);
assert!(approx(g[0].width, 100.0));
let g = font.chars_to_glyphs(&[0x00, 0x02]);
assert!(approx(g[0].width, 200.0));
let g = font.chars_to_glyphs(&[0x00, 0x0a]); assert!(approx(g[0].width, 300.0));
let g = font.chars_to_glyphs(&[0x00, 0x0c]); assert!(approx(g[0].width, 300.0));
let g = font.chars_to_glyphs(&[0x00, 0xff]);
assert!(approx(g[0].width, 1000.0));
}
#[test]
fn type3_width_normalization() {
let mut font_dict = Dict::new();
font_dict.set("Subtype", Object::Name("Type3".into()));
font_dict.set("FirstChar", Object::Int(65));
font_dict.set("LastChar", Object::Int(65));
font_dict.set("Widths", Object::Array(vec![Object::Real(0.6)]));
font_dict.set(
"FontMatrix",
Object::Array(vec![
Object::Real(1.0),
Object::Int(0),
Object::Int(0),
Object::Real(1.0),
Object::Int(0),
Object::Int(0),
]),
);
let font = load_font_dict(&font_dict, "T3", &[], None);
let g = font.chars_to_glyphs(b"A");
assert!(approx(g[0].width, 600.0), "got {}", g[0].width);
let mut font_dict2 = Dict::new();
font_dict2.set("Subtype", Object::Name("Type3".into()));
font_dict2.set("FirstChar", Object::Int(65));
font_dict2.set("Widths", Object::Array(vec![Object::Int(600)]));
font_dict2.set(
"FontMatrix",
Object::Array(vec![
Object::Real(0.001),
Object::Int(0),
Object::Int(0),
Object::Real(0.001),
Object::Int(0),
Object::Int(0),
]),
);
let font2 = load_font_dict(&font_dict2, "T3b", &[], None);
let g2 = font2.chars_to_glyphs(b"A");
assert!(approx(g2[0].width, 600.0), "got {}", g2[0].width);
}
#[test]
fn tounicode_stream_bfchar() {
let cmap_data = br#"
begincmap
1 begincodespacerange
<00> <FF>
endcodespacerange
1 beginbfchar
<41> <0042>
endbfchar
endcmap
"#;
let mut stream_dict = Dict::new();
stream_dict.set("Length", Object::Int(cmap_data.len() as i64));
let stream = Stream::new(stream_dict, 0, cmap_data.len());
let mut font_dict = Dict::new();
font_dict.set("Subtype", Object::Name("Type1".into()));
font_dict.set("FirstChar", Object::Int(65));
font_dict.set("Widths", Object::Array(vec![Object::Int(500)]));
font_dict.set("ToUnicode", Object::Stream(stream));
let font = load_font_dict(&font_dict, "F1", cmap_data, None);
let g = font.chars_to_glyphs(b"A");
assert_eq!(g[0].unicode, "B");
}
#[test]
fn ascent_descent_from_descriptor_and_correction() {
let font_dict = {
let mut d = Dict::new();
d.set("Subtype", Object::Name("Type1".into()));
d
};
let font = load_font_dict(&font_dict, "F1", &[], None);
assert!(approx(font.ascent(), 0.88));
assert!(approx(font.descent(), -0.12));
let mut fd = Dict::new();
fd.set("Ascent", Object::Int(900));
fd.set("Descent", Object::Int(100)); let mut d2 = Dict::new();
d2.set("Subtype", Object::Name("Type1".into()));
d2.set("FontDescriptor", Object::Dict(fd));
let font2 = load_font_dict(&d2, "F2", &[], None);
assert!(approx(font2.ascent(), 0.9));
assert!(approx(font2.descent(), -0.12));
let mut fd3 = Dict::new();
fd3.set("Ascent", Object::Int(0));
fd3.set("Descent", Object::Int(-200));
let mut d3 = Dict::new();
d3.set("Subtype", Object::Name("Type1".into()));
d3.set("FontDescriptor", Object::Dict(fd3));
let font3 = load_font_dict(&d3, "F3", &[], None);
assert!(approx(font3.ascent(), 0.88));
assert!(approx(font3.descent(), -0.2));
}
#[test]
fn truetype_hhea_override() {
let ttf = minimal_ttf(1000, 750, -250);
let (a, d) = parse_ttf_ascent_descent(&ttf).expect("parse ttf");
assert!(approx(a, 0.75), "ascent={a}");
assert!(approx(d, -0.25), "descent={d}");
let mut stream_dict = Dict::new();
stream_dict.set("Length", Object::Int(ttf.len() as i64));
let stream = Stream::new(stream_dict, 0, ttf.len());
let mut fd = Dict::new();
fd.set("Ascent", Object::Int(1000));
fd.set("Descent", Object::Int(-100));
fd.set("FontFile2", Object::Stream(stream));
let mut font_dict = Dict::new();
font_dict.set("Subtype", Object::Name("TrueType".into()));
font_dict.set("FontDescriptor", Object::Dict(fd));
let font = load_font_dict(&font_dict, "TT", &ttf, None);
assert!(approx(font.ascent(), 0.75), "got {}", font.ascent());
assert!(approx(font.descent(), -0.25), "got {}", font.descent());
}
#[test]
fn type1_builtin_encoding_ligatures() {
let pfa: &[u8] = b"%!PS-AdobeFont-1.0: TestFont\n\
/FontName /TestFont def\n\
/Encoding 256 array\n\
0 1 255 {1 index exch /.notdef put} for\n\
dup 11 /ff put\n\
dup 12 /fi put\n\
dup 39 /quoteright put\n\
readonly def\n\
currentfile eexec\n\
00000000";
let mut sd = Dict::new();
sd.set("Length", Object::Int(pfa.len() as i64));
let stream = Stream::new(sd, 0, pfa.len());
let mut fd = Dict::new();
fd.set("FontFile", Object::Stream(stream));
let mut d = Dict::new();
d.set("Subtype", Object::Name("Type1".into()));
d.set("BaseFont", Object::Name("TestFont".into()));
d.set("FontDescriptor", Object::Dict(fd));
let font = load_font_dict(&d, "T1", pfa, None);
let g = font.chars_to_glyphs(&[11, 12, 39]);
assert_eq!(
g[0].unicode, "\u{fb00}",
"ff ligature, got {:?}",
g[0].unicode
);
assert_eq!(
g[1].unicode, "\u{fb01}",
"fi ligature, got {:?}",
g[1].unicode
);
assert_eq!(
g[2].unicode, "\u{2019}",
"quoteright, got {:?}",
g[2].unicode
);
}
#[test]
fn composite_embedded_cmap_encoding() {
let cmap_data = br#"
begincmap
1 begincodespacerange
<00> <FF>
endcodespacerange
1 begincidrange
<41> <41> 200
endcidrange
endcmap
"#;
let mut stream_dict = Dict::new();
stream_dict.set("Length", Object::Int(cmap_data.len() as i64));
let stream = Stream::new(stream_dict, 0, cmap_data.len());
let mut cid = Dict::new();
cid.set("DW", Object::Int(500));
cid.set(
"W",
Object::Array(vec![
Object::Int(200),
Object::Array(vec![Object::Int(777)]),
]),
);
let mut type0 = Dict::new();
type0.set("Subtype", Object::Name("Type0".into()));
type0.set("Encoding", Object::Stream(stream));
type0.set("DescendantFonts", Object::Array(vec![Object::Dict(cid)]));
let font = load_font_dict(&type0, "C1", cmap_data, None);
let g = font.chars_to_glyphs(b"A");
assert_eq!(g.len(), 1);
assert!(approx(g[0].width, 777.0), "width={}", g[0].width);
}
#[test]
fn word_spacing_only_single_byte_space() {
let mut cid = Dict::new();
cid.set("DW", Object::Int(1000));
let mut type0 = Dict::new();
type0.set("Subtype", Object::Name("Type0".into()));
type0.set("Encoding", Object::Name("Identity-H".into()));
type0.set("DescendantFonts", Object::Array(vec![Object::Dict(cid)]));
let font = load_font_dict(&type0, "C0", &[], None);
let g = font.chars_to_glyphs(&[0x00, 0x20]);
assert_eq!(g.len(), 1);
assert!(!g[0].is_space);
let simple = LoadedFont::stub("S");
let g2 = simple.chars_to_glyphs(b" ");
assert!(g2[0].is_space);
}
#[test]
fn glyphlist_lookup_and_binary_search() {
assert_eq!(glyph_unicode("A"), Some("A"));
assert_eq!(glyph_unicode("alpha"), Some("α"));
assert_eq!(glyph_unicode("notarealglyph"), None);
let list = super::super::data::GLYPHS_UNICODE;
for w in list.windows(2) {
assert!(w[0].0 < w[1].0, "{} >= {}", w[0].0, w[1].0);
}
}
#[test]
fn winansi_slots() {
assert_eq!(WIN_ANSI[0x41], "A");
assert_eq!(WIN_ANSI[0x92], "quoteright");
}
#[test]
fn helvetica_and_courier_widths() {
assert_eq!(standard14_glyph_width("Helvetica", "A"), Some(667.0));
assert_eq!(standard14_glyph_width("Courier", "A"), Some(600.0));
assert_eq!(standard14_glyph_width("Courier-Bold", "Z"), Some(600.0));
assert_eq!(standard14_default_width("Courier"), Some(600.0));
assert_eq!(standard14_default_width("Helvetica"), Some(0.0));
}
#[test]
fn font_name_normalization() {
assert_eq!(normalize_font_name("Arial"), "Helvetica");
assert_eq!(normalize_font_name("ABCDEF+Times-Roman"), "Times-Roman");
assert_eq!(normalize_font_name("Arial-Bold"), "Helvetica-Bold");
}
#[test]
fn widths_from_standard14_without_widths_array() {
let mut font_dict = Dict::new();
font_dict.set("Subtype", Object::Name("Type1".into()));
font_dict.set("BaseFont", Object::Name("Helvetica".into()));
let mut fd = Dict::new();
fd.set("Flags", Object::Int(FLAG_NONSYMBOLIC));
font_dict.set("FontDescriptor", Object::Dict(fd));
let font = load_font_dict(&font_dict, "F1", &[], None);
let g = font.chars_to_glyphs(b"A");
assert!(approx(g[0].width, 667.0), "width={}", g[0].width);
assert_eq!(g[0].unicode, "A");
}
#[test]
fn differences_override_width_and_unicode() {
let mut enc = Dict::new();
enc.set("BaseEncoding", Object::Name("WinAnsiEncoding".into()));
enc.set(
"Differences",
Object::Array(vec![
Object::Int(65),
Object::Name("W".into()),
Object::Name("alpha".into()),
]),
);
let mut font_dict = Dict::new();
font_dict.set("Subtype", Object::Name("Type1".into()));
font_dict.set("BaseFont", Object::Name("Helvetica".into()));
font_dict.set("Encoding", Object::Dict(enc));
let mut fd = Dict::new();
fd.set("Flags", Object::Int(FLAG_NONSYMBOLIC));
font_dict.set("FontDescriptor", Object::Dict(fd));
let font = load_font_dict(&font_dict, "F1", &[], None);
let g = font.chars_to_glyphs(b"AB");
assert_eq!(g[0].unicode, "W");
assert!(approx(g[0].width, 944.0), "width={}", g[0].width);
assert_eq!(g[1].unicode, "α");
}
#[test]
fn times_roman_basic_metrics_ascent_descent() {
let mut font_dict = Dict::new();
font_dict.set("Subtype", Object::Name("Type1".into()));
font_dict.set("BaseFont", Object::Name("Times-Roman".into()));
font_dict.set("FontDescriptor", Object::Dict(Dict::new()));
let font = load_font_dict(&font_dict, "T", &[], None);
assert!(approx(font.ascent(), 0.683), "ascent={}", font.ascent());
assert!(approx(font.descent(), -0.217), "descent={}", font.descent());
}
#[cfg(feature = "cmap-japan1")]
fn japan1_cid_system_info() -> Dict {
let mut info = Dict::new();
info.set("Registry", Object::Str(b"Adobe".to_vec()));
info.set("Ordering", Object::Str(b"Japan1".to_vec()));
info.set("Supplement", Object::Int(0));
info
}
#[cfg(feature = "cmap-japan1")]
fn type0_rksj(encoding: &str, with_w: bool) -> Dict {
let mut cid = Dict::new();
cid.set("Subtype", Object::Name("CIDFontType0".into()));
cid.set("CIDSystemInfo", Object::Dict(japan1_cid_system_info()));
cid.set("DW", Object::Int(1000));
if with_w {
let rksj = CMap::load_predefined("90ms-RKSJ-H").expect("rksj");
let cid_val = match rksj.lookup(0x8140).as_deref() {
Some(MapValue::Cid(c)) => *c as i64,
_ => panic!("expected Cid"),
};
cid.set(
"W",
Object::Array(vec![
Object::Int(cid_val),
Object::Array(vec![Object::Int(500)]),
]),
);
}
let mut type0 = Dict::new();
type0.set("Subtype", Object::Name("Type0".into()));
type0.set("Encoding", Object::Name(encoding.to_string().into()));
type0.set("DescendantFonts", Object::Array(vec![Object::Dict(cid)]));
type0
}
#[cfg(feature = "cmap-japan1")]
#[test]
fn predefined_rksj_h_unicode_width_and_mixed() {
let type0 = type0_rksj("90ms-RKSJ-H", true);
let font = load_font_dict(&type0, "Cjk", &[], None);
assert!(!font.vertical());
let g = font.chars_to_glyphs(&[0x81, 0x40]);
assert_eq!(g.len(), 1);
assert_eq!(g[0].unicode, "\u{3000}");
assert!(approx(g[0].width, 500.0), "width={}", g[0].width);
let g2 = font.chars_to_glyphs(&[0x41, 0x81, 0x40]);
assert_eq!(g2.len(), 2);
assert!(!g2[0].unicode.is_empty());
assert_eq!(g2[1].unicode, "\u{3000}");
}
#[cfg(feature = "cmap-japan1")]
#[test]
fn predefined_rksj_v_vertical_same_unicode() {
let h = load_font_dict(&type0_rksj("90ms-RKSJ-H", false), "H", &[], None);
let v = load_font_dict(&type0_rksj("90ms-RKSJ-V", false), "V", &[], None);
assert!(v.vertical());
assert!(!h.vertical());
let gh = h.chars_to_glyphs(&[0x81, 0x40]);
let gv = v.chars_to_glyphs(&[0x81, 0x40]);
assert_eq!(gh[0].unicode, "\u{3000}");
assert_eq!(gv[0].unicode, gh[0].unicode);
}
#[cfg(feature = "cmap-japan1")]
#[test]
fn tounicode_takes_priority_over_ucs2() {
let cmap_data = br#"
begincmap
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
1 beginbfchar
<8140> <0058>
endbfchar
endcmap
"#;
let mut stream_dict = Dict::new();
stream_dict.set("Length", Object::Int(cmap_data.len() as i64));
let stream = Stream::new(stream_dict, 0, cmap_data.len());
let mut type0 = type0_rksj("90ms-RKSJ-H", false);
type0.set("ToUnicode", Object::Stream(stream));
let font = load_font_dict(&type0, "Cjk", cmap_data, None);
let g = font.chars_to_glyphs(&[0x81, 0x40]);
assert_eq!(g[0].unicode, "X");
}
#[cfg(feature = "cmap-japan1")]
#[test]
fn identity_h_japan1_ucs2_without_tounicode() {
let rksj = CMap::load_predefined("90ms-RKSJ-H").expect("rksj");
let cid = match rksj.lookup(0x8140).as_deref() {
Some(MapValue::Cid(c)) => *c,
_ => panic!("expected Cid"),
};
let mut cid_dict = Dict::new();
cid_dict.set("Subtype", Object::Name("CIDFontType2".into()));
cid_dict.set("CIDSystemInfo", Object::Dict(japan1_cid_system_info()));
cid_dict.set("DW", Object::Int(1000));
cid_dict.set(
"W",
Object::Array(vec![
Object::Int(cid as i64),
Object::Array(vec![Object::Int(600)]),
]),
);
let mut type0 = Dict::new();
type0.set("Subtype", Object::Name("Type0".into()));
type0.set("Encoding", Object::Name("Identity-H".into()));
type0.set(
"DescendantFonts",
Object::Array(vec![Object::Dict(cid_dict)]),
);
let font = load_font_dict(&type0, "Id", &[], None);
assert!(!font.vertical());
let hi = ((cid >> 8) & 0xff) as u8;
let lo = (cid & 0xff) as u8;
let g = font.chars_to_glyphs(&[hi, lo]);
assert_eq!(g.len(), 1);
assert_eq!(g[0].unicode, "\u{3000}");
assert!(approx(g[0].width, 600.0), "width={}", g[0].width);
assert!(g[0].vmetric.is_none());
}
fn type0_identity_v(with_w2: bool) -> Dict {
let mut cid = Dict::new();
cid.set("Subtype", Object::Name("CIDFontType2".into()));
cid.set("DW", Object::Int(1000));
cid.set(
"DW2",
Object::Array(vec![Object::Int(880), Object::Int(-1000)]),
);
if with_w2 {
cid.set(
"W2",
Object::Array(vec![
Object::Int(0x41),
Object::Array(vec![Object::Int(-800), Object::Int(400), Object::Int(700)]),
]),
);
}
let mut type0 = Dict::new();
type0.set("Subtype", Object::Name("Type0".into()));
type0.set("Encoding", Object::Name("Identity-V".into()));
type0.set("DescendantFonts", Object::Array(vec![Object::Dict(cid)]));
type0
}
#[test]
fn identity_v_default_vmetric_from_dw2() {
let font = load_font_dict(&type0_identity_v(false), "V", &[], None);
assert!(font.vertical());
let g = font.chars_to_glyphs(&[0x00, 0x42]);
assert_eq!(g.len(), 1);
let vm = g[0].vmetric.expect("vmetric");
assert!(approx(vm[0], -1000.0), "w1y={}", vm[0]);
assert!(approx(vm[1], 500.0), "v1x={}", vm[1]);
assert!(approx(vm[2], 880.0), "v1y={}", vm[2]);
}
#[test]
fn identity_v_w2_overrides_default() {
let font = load_font_dict(&type0_identity_v(true), "V", &[], None);
assert!(font.vertical());
let g = font.chars_to_glyphs(&[0x00, 0x41]);
let vm = g[0].vmetric.expect("vmetric");
assert!(approx(vm[0], -800.0));
assert!(approx(vm[1], 400.0));
assert!(approx(vm[2], 700.0));
let g2 = font.chars_to_glyphs(&[0x00, 0x42]);
let vm2 = g2[0].vmetric.expect("vmetric");
assert!(approx(vm2[0], -1000.0));
assert!(approx(vm2[1], 500.0));
assert!(approx(vm2[2], 880.0));
}
#[test]
fn identity_v_dw2_validation_and_fallback() {
let cases = [
None,
Some(vec![Object::Int(700), Object::Null]),
Some(vec![Object::Int(700), Object::Int(-900), Object::Int(0)]),
];
for dw2 in cases {
let mut cid = Dict::new();
if let Some(dw2) = dw2 {
cid.set("DW2", Object::Array(dw2));
}
let (_, default) = extract_cid_vmetrics(&cid, 1000.0, &[], None);
assert_eq!(default, Some([-1000.0, 500.0, 880.0]));
}
let mut cid = Dict::new();
cid.set(
"DW2",
Object::Array(vec![Object::Int(700), Object::Int(-900)]),
);
let (_, default) = extract_cid_vmetrics(&cid, 1000.0, &[], None);
assert_eq!(default, Some([-900.0, 500.0, 700.0]));
}
#[test]
fn identity_v_w2_range_form() {
let mut cid = Dict::new();
cid.set(
"W2",
Object::Array(vec![
Object::Int(0x41),
Object::Int(0x43),
Object::Int(-700),
Object::Int(300),
Object::Int(600),
]),
);
let (metrics, default) = extract_cid_vmetrics(&cid, 1000.0, &[], None);
for code in 0x41..=0x43 {
assert_eq!(metrics.get(&code), Some(&[-700.0, 300.0, 600.0]));
}
assert!(!metrics.contains_key(&0x40));
assert!(!metrics.contains_key(&0x44));
assert_eq!(default, Some([-1000.0, 500.0, 880.0]));
}
#[test]
fn identity_h_has_no_vmetric() {
let mut cid = Dict::new();
cid.set("Subtype", Object::Name("CIDFontType2".into()));
cid.set("DW", Object::Int(1000));
cid.set(
"DW2",
Object::Array(vec![Object::Int(880), Object::Int(-1000)]),
);
let mut type0 = Dict::new();
type0.set("Subtype", Object::Name("Type0".into()));
type0.set("Encoding", Object::Name("Identity-H".into()));
type0.set("DescendantFonts", Object::Array(vec![Object::Dict(cid)]));
let font = load_font_dict(&type0, "H", &[], None);
assert!(!font.vertical());
let g = font.chars_to_glyphs(&[0x00, 0x41]);
assert!(g[0].vmetric.is_none());
}
#[test]
fn stub_is_not_bold_or_italic() {
let font = LoadedFont::stub("F1");
assert!(!font.bold());
assert!(!font.italic());
}
#[test]
fn style_from_descriptor_flags_and_metrics() {
let mut d = Dict::new();
d.set("Flags", Object::Int(FLAG_ITALIC));
assert_eq!(style_from_descriptor(&d, &[], None), (false, true));
let mut d = Dict::new();
d.set("Flags", Object::Int(FLAG_FORCE_BOLD));
assert_eq!(style_from_descriptor(&d, &[], None), (true, false));
let mut d = Dict::new();
d.set("ItalicAngle", Object::Real(-12.0));
assert_eq!(style_from_descriptor(&d, &[], None), (false, true));
let mut d = Dict::new();
d.set("ItalicAngle", Object::Int(0));
assert_eq!(style_from_descriptor(&d, &[], None), (false, false));
let mut d = Dict::new();
d.set("FontWeight", Object::Int(700));
assert_eq!(style_from_descriptor(&d, &[], None), (true, false));
let mut d = Dict::new();
d.set("FontWeight", Object::Int(400));
assert_eq!(style_from_descriptor(&d, &[], None), (false, false));
let mut d = Dict::new();
d.set("Flags", Object::Int(FLAG_ITALIC | FLAG_FORCE_BOLD));
d.set("ItalicAngle", Object::Real(-10.0));
d.set("FontWeight", Object::Int(800));
assert_eq!(style_from_descriptor(&d, &[], None), (true, true));
}
#[test]
fn style_from_font_name_positive() {
assert_eq!(style_from_font_name("ABCDEF+Times-Bold"), (true, false));
assert_eq!(style_from_font_name("ABCDEF+Times-Italic"), (false, true));
assert_eq!(
style_from_font_name("ABCDEF+Times-BoldItalic"),
(true, true)
);
assert_eq!(style_from_font_name("Arial,Bold"), (true, false));
assert_eq!(style_from_font_name("Arial,BoldItalic"), (true, true));
assert_eq!(style_from_font_name("Foo-Semibold"), (true, false));
assert_eq!(style_from_font_name("Foo-Demi"), (true, false));
assert_eq!(style_from_font_name("Foo-Black"), (true, false));
assert_eq!(style_from_font_name("Foo-Heavy"), (true, false));
assert_eq!(style_from_font_name("Foo-Oblique"), (false, true));
assert_eq!(style_from_font_name("Foo-BoldIt"), (true, true));
assert_eq!(style_from_font_name("Bar-It"), (false, true));
assert_eq!(style_from_font_name("CMBX10"), (true, false));
assert_eq!(style_from_font_name("CMTI10"), (false, true));
assert_eq!(style_from_font_name("CMMI10"), (false, true));
assert_eq!(style_from_font_name("CMMIB10"), (true, true));
}
#[test]
fn style_from_font_name_negative() {
assert_eq!(style_from_font_name("Foo-Medium"), (false, false));
assert_eq!(style_from_font_name("Foo-Regular"), (false, false));
assert_eq!(style_from_font_name("Foo-Roman"), (false, false));
assert_eq!(style_from_font_name("Foo-Book"), (false, false));
assert_eq!(style_from_font_name("md"), (false, false));
assert_eq!(style_from_font_name("F1"), (false, false));
assert_eq!(style_from_font_name("Helvetica"), (false, false));
assert_eq!(style_from_font_name(""), (false, false));
}
#[test]
fn load_simple_applies_descriptor_and_name_style() {
let mut fd = Dict::new();
fd.set("Flags", Object::Int(FLAG_ITALIC));
fd.set("Ascent", Object::Int(800));
fd.set("Descent", Object::Int(-200));
let mut font_dict = Dict::new();
font_dict.set("Subtype", Object::Name("Type1".into()));
font_dict.set("BaseFont", Object::Name("Helvetica-Bold".into()));
font_dict.set("FontDescriptor", Object::Dict(fd));
font_dict.set("FirstChar", Object::Int(65));
font_dict.set("LastChar", Object::Int(65));
font_dict.set("Widths", Object::Array(vec![Object::Int(500)]));
let font = load_font_dict(&font_dict, "F", &[], None);
assert!(font.bold());
assert!(font.italic());
}
#[test]
fn load_type0_uses_descendant_style() {
let mut fd = Dict::new();
fd.set("FontWeight", Object::Int(700));
fd.set("ItalicAngle", Object::Real(-12.0));
let mut cid = Dict::new();
cid.set("Subtype", Object::Name("CIDFontType2".into()));
cid.set("BaseFont", Object::Name("SomeCIDFont".into()));
cid.set("FontDescriptor", Object::Dict(fd));
cid.set("DW", Object::Int(1000));
let mut type0 = Dict::new();
type0.set("Subtype", Object::Name("Type0".into()));
type0.set("Encoding", Object::Name("Identity-H".into()));
type0.set("DescendantFonts", Object::Array(vec![Object::Dict(cid)]));
let font = load_font_dict(&type0, "C", &[], None);
assert!(font.bold());
assert!(font.italic());
}
}