use std::collections::HashMap;
use std::sync::Arc;
use skrifa::MetadataProvider;
use stet_fonts::cff_parser::{CffFont, parse_cff};
use stet_fonts::charstring::{execute_charstring, execute_charstring_mm};
use stet_fonts::encoding::{MACROMAN_ENCODING, STANDARD_ENCODING, WINANSI_ENCODING};
use stet_fonts::geometry::PathSegment;
use stet_fonts::geometry::{Matrix, PsPath};
use stet_fonts::truetype::{
get_glyf_data, get_units_per_em, parse_cmap, parse_cmap_with_info, parse_glyf_to_path,
};
use stet_fonts::type1_parser::parse_type1;
use stet_fonts::type2_charstring::execute_type2_charstring;
use crate::FontProvider;
use crate::error::PdfError;
use crate::objects::{PdfDict, PdfObj};
use crate::resolver::Resolver;
pub enum PdfFont {
Type1(Type1PdfFont),
TrueType(TrueTypePdfFont),
Cff(CffPdfFont),
CidTrueType(CidTrueTypePdfFont),
CidCff(CidCffPdfFont),
Type3(Type3PdfFont),
}
pub struct Type1PdfFont {
pub font: stet_fonts::type1_parser::Type1Font,
pub encoding: [Option<String>; 256],
pub widths: [f64; 256],
pub font_matrix: Matrix,
pub weight_vector: Option<Vec<f64>>,
pub builtin_fallback: bool,
pub per_char_width_scale: bool,
}
pub struct TrueTypePdfFont {
pub data: Vec<u8>,
pub encoding: [Option<String>; 256],
pub widths: [f64; 256],
pub cmap: HashMap<u32, u16>,
pub cmap_is_unicode: bool,
pub post_name_to_gid: HashMap<String, u16>,
pub units_per_em: f64,
pub to_unicode: HashMap<u16, u32>,
pub identity_gid: bool,
pub gid_hex: bool,
}
pub struct CffPdfFont {
pub font: CffFont,
pub encoding: [Option<String>; 256],
pub widths: [f64; 256],
pub font_matrix: Matrix,
}
pub struct CidTrueTypePdfFont {
pub data: Vec<u8>,
pub default_width: f64,
pub cid_widths: HashMap<u16, f64>,
pub cmap: HashMap<u32, u16>,
pub units_per_em: f64,
pub identity_cid_to_gid: bool,
pub substituted: bool,
pub cid_to_gid_map: Option<Vec<u16>>,
pub to_unicode: HashMap<u16, u32>,
pub ordering: Vec<u8>,
pub ucs2_encoding: bool,
pub code_lengths: [u8; 256],
pub code_to_cid: HashMap<u32, u32>,
pub wmode: u8,
pub dw2: [f64; 2],
pub w2: HashMap<u16, [f64; 3]>,
}
pub struct CidCffPdfFont {
pub font: CffFont,
pub default_width: f64,
pub cid_widths: HashMap<u16, f64>,
pub cmap: Option<HashMap<u32, u16>>,
pub pdf_cid_to_gid: Option<Vec<u16>>,
pub identity_cid_to_gid: bool,
pub ordering: Vec<u8>,
pub font_matrix: Matrix,
pub code_lengths: [u8; 256],
pub code_to_cid: HashMap<u32, u32>,
pub wmode: u8,
pub dw2: [f64; 2],
pub w2: HashMap<u16, [f64; 3]>,
pub type1_paths: Option<HashMap<u16, PsPath>>,
}
pub struct Type3PdfFont {
pub char_procs: HashMap<u8, Vec<u8>>,
pub resources: PdfDict,
pub widths: [f64; 256],
pub font_matrix: Matrix,
pub font_bbox: [f64; 4],
}
pub type FontCache = HashMap<Vec<u8>, Arc<PdfFont>>;
pub fn resolve_font(
resolver: &Resolver,
font_ref: &PdfObj,
font_provider: Option<&FontProvider>,
) -> Result<PdfFont, PdfError> {
let font_obj = resolver.deref(font_ref)?;
let font_dict = font_obj
.as_dict()
.ok_or(PdfError::Other("Font is not a dict".into()))?;
let subtype = font_dict.get_name(b"Subtype").unwrap_or(b"Type1");
if subtype == b"Type0" {
return resolve_type0(resolver, font_dict);
}
if subtype == b"Type3" {
return resolve_type3(resolver, font_dict);
}
let first_char = font_dict.get_int(b"FirstChar").unwrap_or(0) as usize;
let last_char = font_dict.get_int(b"LastChar").unwrap_or(255) as usize;
let mut widths = [0.0f64; 256];
let mut has_pdf_widths = false;
let widths_obj = font_dict.get(b"Widths").and_then(|obj| {
if obj.as_array().is_some() {
Some(obj.clone())
} else {
resolver.deref(obj).ok()
}
});
if let Some(PdfObj::Array(w_arr)) = &widths_obj {
for (i, obj) in w_arr.iter().enumerate() {
let code = first_char + i;
if code < 256 {
let val = if obj.as_f64().is_some() {
obj.as_f64().unwrap()
} else if let Ok(resolved) = resolver.deref(obj) {
resolved.as_f64().unwrap_or(0.0)
} else {
0.0
};
widths[code] = val / 1000.0;
}
}
has_pdf_widths = true;
let descriptor = get_font_descriptor(font_dict, resolver)?;
if let Some(ref desc) = descriptor {
let missing_w = desc.get_f64(b"MissingWidth").unwrap_or(0.0) / 1000.0;
if missing_w != 0.0 {
for (code, width) in widths.iter_mut().enumerate() {
if code < first_char || code > last_char {
*width = missing_w;
}
}
}
}
}
let (encoding, has_valid_encoding, differences, no_base_encoding) =
resolve_encoding(font_dict, resolver)?;
let has_explicit_encoding = has_valid_encoding;
let descriptor = get_font_descriptor(font_dict, resolver)?;
let desc_flags = descriptor
.as_ref()
.and_then(|d| d.get_int(b"Flags"))
.unwrap_or(0) as u32;
let base_font_name = font_dict
.get_name(b"BaseFont")
.map(|n| String::from_utf8_lossy(n).to_string())
.unwrap_or_default();
if let Some(ref desc) = descriptor {
if desc.get(b"FontFile3").is_some() {
match resolve_cff(
resolver,
&descriptor,
encoding.clone(),
widths,
has_explicit_encoding,
has_pdf_widths,
&differences,
no_base_encoding,
) {
Ok(font) => return Ok(font),
Err(_) => {
if let Some(font) = substitute_font(
&base_font_name,
encoding.clone(),
widths,
has_pdf_widths,
font_provider,
desc_flags,
first_char,
last_char,
) {
return Ok(font);
}
}
}
}
if desc.get(b"FontFile2").is_some() {
match resolve_truetype(resolver, &descriptor, encoding.clone(), widths, font_dict) {
Ok(font) => return Ok(font),
Err(_) => {
if let Some(font) = substitute_font(
&base_font_name,
encoding.clone(),
widths,
has_pdf_widths,
font_provider,
desc_flags,
first_char,
last_char,
) {
return Ok(font);
}
}
}
}
if desc.get(b"FontFile").is_some() {
match resolve_type1(
resolver,
&descriptor,
encoding.clone(),
widths,
has_explicit_encoding,
has_pdf_widths,
&differences,
no_base_encoding,
) {
Ok(font) => return Ok(font),
Err(_) => {
if let Some(font) = substitute_font(
&base_font_name,
encoding.clone(),
widths,
has_pdf_widths,
font_provider,
desc_flags,
first_char,
last_char,
) {
return Ok(font);
}
}
}
}
}
if let Some(font) = substitute_font(
&base_font_name,
encoding.clone(),
widths,
has_pdf_widths,
font_provider,
desc_flags,
first_char,
last_char,
) {
return Ok(font);
}
if subtype == b"TrueType"
&& let Ok(data) = load_system_truetype_font(&base_font_name)
{
let units_per_em = get_units_per_em(&data) as f64;
let (cmap, cmap_is_unicode) = parse_cmap_with_info(&data);
let post_name_to_gid = stet_fonts::system_fonts::parse_post_table(&data)
.map(|gid_to_name| {
gid_to_name
.into_iter()
.map(|(gid, name)| (name, gid))
.collect()
})
.unwrap_or_default();
let to_unicode = if let Some(tu_obj) = font_dict.get(b"ToUnicode") {
resolver
.stream_data_from_obj(tu_obj)
.map(|d| parse_to_unicode(&d))
.unwrap_or_default()
} else {
HashMap::new()
};
let gid_hex = TrueTypePdfFont::detect_gid_hex(&encoding);
return Ok(PdfFont::TrueType(TrueTypePdfFont {
data,
encoding,
widths,
cmap,
cmap_is_unicode,
post_name_to_gid,
units_per_em,
to_unicode,
identity_gid: false, gid_hex,
}));
}
match subtype {
b"TrueType" => resolve_truetype(resolver, &descriptor, encoding, widths, font_dict),
_ => resolve_type1(
resolver,
&descriptor,
encoding,
widths,
has_explicit_encoding,
has_pdf_widths,
&differences,
no_base_encoding,
),
}
}
fn get_font_descriptor(
font_dict: &PdfDict,
resolver: &Resolver,
) -> Result<Option<PdfDict>, PdfError> {
if let Some(fd_ref) = font_dict.get(b"FontDescriptor") {
let fd_obj = resolver.deref(fd_ref)?;
if let Some(d) = fd_obj.as_dict() {
return Ok(Some(d.clone()));
}
}
Ok(None)
}
fn resolve_encoding(
font_dict: &PdfDict,
resolver: &Resolver,
) -> Result<([Option<String>; 256], bool, Vec<(usize, String)>, bool), PdfError> {
let mut encoding: [Option<String>; 256] = std::array::from_fn(|_| None);
let mut differences: Vec<(usize, String)> = Vec::new();
let base_font = font_dict.get_name(b"BaseFont").unwrap_or(b"");
let clean_base = if base_font.len() > 7 && base_font.get(6) == Some(&b'+') {
&base_font[7..]
} else {
base_font
};
let is_symbol_font = clean_base == b"ZapfDingbats" || clean_base == b"Symbol";
let mut base_table: &[&str; 256] = if clean_base == b"ZapfDingbats" {
&stet_fonts::encoding::ZAPFDINGBATS_ENCODING
} else if clean_base == b"Symbol" {
&stet_fonts::encoding::SYMBOL_ENCODING
} else {
&STANDARD_ENCODING
};
let mut has_valid_encoding = is_symbol_font; if let Some(enc_obj) = font_dict.get(b"Encoding") {
let enc_resolved = resolver.deref(enc_obj)?;
match &enc_resolved {
PdfObj::Name(name) => {
if !is_symbol_font {
if let Some(table) = encoding_table_by_name(name) {
base_table = table;
has_valid_encoding = true;
}
}
}
PdfObj::Dict(enc_dict) => {
has_valid_encoding = true;
let mut has_base_encoding = false;
if !is_symbol_font {
if let Some(base_name) = enc_dict.get_name(b"BaseEncoding") {
if let Some(table) = encoding_table_by_name(base_name) {
base_table = table;
has_base_encoding = true;
}
}
}
for (i, &name) in base_table.iter().enumerate() {
if name != ".notdef" {
encoding[i] = Some(name.to_string());
}
}
if let Some(diffs_obj) = enc_dict.get(b"Differences") {
let diffs_resolved = resolver.deref(diffs_obj)?;
if let Some(diffs) = diffs_resolved.as_array() {
let mut code = 0usize;
for obj in diffs {
let obj = resolver.deref(obj).unwrap_or(obj.clone());
match &obj {
PdfObj::Int(n) => code = *n as usize,
PdfObj::Name(name) => {
if code < 256 {
let name_str = String::from_utf8_lossy(name).to_string();
encoding[code] = Some(name_str.clone());
if !has_base_encoding && !is_symbol_font {
differences.push((code, name_str));
}
code += 1;
}
}
_ => {}
}
}
}
}
return Ok((
encoding,
has_valid_encoding,
differences,
!has_base_encoding && !is_symbol_font,
));
}
_ => {}
}
}
for (i, &name) in base_table.iter().enumerate() {
if name != ".notdef" {
encoding[i] = Some(name.to_string());
}
}
Ok((encoding, has_valid_encoding, differences, false))
}
fn encoding_table_by_name(name: &[u8]) -> Option<&'static [&'static str; 256]> {
match name {
b"WinAnsiEncoding" => Some(&WINANSI_ENCODING),
b"MacRomanEncoding" => Some(&MACROMAN_ENCODING),
b"StandardEncoding" => Some(&STANDARD_ENCODING),
_ => None,
}
}
pub fn fallback_font(font_provider: Option<&FontProvider>) -> Option<PdfFont> {
let encoding: [Option<String>; 256] = std::array::from_fn(|i| {
WINANSI_ENCODING.get(i).and_then(|&s| {
if s.is_empty() {
None
} else {
Some(s.to_string())
}
})
});
let widths = super::standard_fonts::standard_font_widths(b"Helvetica").unwrap_or([0.0f64; 256]);
substitute_font(
"Helvetica",
encoding,
widths,
false,
font_provider,
0,
0,
255,
)
}
fn load_predefined_cmap(name: &[u8]) -> Option<Vec<u8>> {
let name_str = std::str::from_utf8(name).ok()?;
if let Ok(dir) = std::env::var("STET_CMAP_DIR") {
let path = format!("{}/{}", dir, name_str);
if let Ok(data) = std::fs::read(&path) {
return Some(data);
}
}
if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
let path = std::path::Path::new(&home)
.join(".local/share/stet/CMap")
.join(name_str);
if let Ok(data) = std::fs::read(&path) {
return Some(data);
}
}
let poppler_dirs = [
"/usr/share/poppler/cMap",
"/usr/local/share/poppler/cMap",
"/opt/homebrew/share/poppler/cMap", "/usr/local/opt/poppler-data/share/poppler/cMap", ];
let collections = [
"Adobe-GB1",
"Adobe-CNS1",
"Adobe-Japan1",
"Adobe-Japan2",
"Adobe-Korea1",
"Adobe-KR",
];
for base in &poppler_dirs {
for collection in &collections {
let path = format!("{}/{}/{}", base, collection, name_str);
if let Ok(data) = std::fs::read(&path) {
return Some(data);
}
}
}
let gs_dirs = [
"/var/lib/ghostscript/CMap",
"/usr/share/ghostscript/Resource/CMap",
"/usr/local/share/ghostscript/Resource/CMap",
];
for dir in &gs_dirs {
let path = format!("{}/{}", dir, name_str);
if let Ok(data) = std::fs::read(&path) {
return Some(data);
}
}
None
}
fn substitute_font(
base_font: &str,
encoding: [Option<String>; 256],
widths: [f64; 256],
has_pdf_widths: bool,
font_provider: Option<&FontProvider>,
descriptor_flags: u32,
first_char: usize,
last_char: usize,
) -> Option<PdfFont> {
use stet_fonts::FONT_SUBSTITUTIONS;
let mut clean_name: &str = base_font;
if clean_name.len() > 7 && clean_name.as_bytes().get(6) == Some(&b'+') {
clean_name = &clean_name[7..];
}
if let Some(star_pos) = clean_name.rfind('*') {
clean_name = &clean_name[..star_pos];
}
let urw_name = FONT_SUBSTITUTIONS
.iter()
.find(|&&(ps, _)| ps == clean_name)
.map(|&(_, urw)| urw)
.or_else(|| fuzzy_font_match(clean_name));
let font_file_name = urw_name.unwrap_or(clean_name);
let font_data = if let Some(provider) = font_provider {
provider(font_file_name)
} else {
None
};
let font_data = font_data.or_else(|| {
let cache = stet_fonts::system_fonts::get_system_font_cache();
let path = cache.get_font_path(font_file_name)?;
read_font_file(path, font_file_name).ok()
});
let font_data = font_data.or_else(|| embedded_font(font_file_name));
let font_data = font_data.or_else(|| {
let lower = clean_name.to_ascii_lowercase();
let is_bold = lower.contains("bold")
|| lower.contains("demi")
|| lower.contains("black")
|| lower.contains("heavy");
let is_italic = lower.contains("italic") || lower.contains("oblique");
let is_serif = descriptor_flags & 2 != 0; let default_name = if is_serif {
match (is_bold, is_italic) {
(true, true) => "NimbusRoman-BoldItalic",
(true, false) => "NimbusRoman-Bold",
(false, true) => "NimbusRoman-Italic",
(false, false) => "NimbusRoman-Regular",
}
} else {
match (is_bold, is_italic) {
(true, true) => "NimbusSans-BoldItalic",
(true, false) => "NimbusSans-Bold",
(false, true) => "NimbusSans-Italic",
(false, false) => "NimbusSans-Regular",
}
};
if let Some(provider) = font_provider {
if let Some(data) = provider(default_name) {
return Some(data);
}
}
embedded_font(default_name)
})?;
let font = parse_type1(&font_data).ok()?;
let fm = font.font_matrix;
let mut font_matrix = Matrix::new(fm[0], fm[1], fm[2], fm[3], fm[4], fm[5]);
let mut per_char_scale = false;
let widths = if !has_pdf_widths {
let mut derived = [0.0f64; 256];
let notdef_width = font
.charstrings
.get(".notdef")
.and_then(|cs| execute_charstring(cs, &font.subrs, font.len_iv, false).ok())
.map(|r| r.width_x * fm[0])
.unwrap_or(0.0);
for code in 0..256usize {
let glyph_name = encoding[code].as_deref().unwrap_or(".notdef");
if let Some(cs) = font.charstrings.get(glyph_name) {
if let Ok(result) = execute_charstring(cs, &font.subrs, font.len_iv, false) {
derived[code] = result.width_x * fm[0];
}
} else {
derived[code] = notdef_width;
}
}
derived
} else {
let is_symbol_font = {
let lower = clean_name.to_ascii_lowercase();
lower.contains("wingding") || lower.contains("webding") || lower.contains("dingbat")
};
if !is_symbol_font {
let mut pdf_sum = 0.0;
let mut sub_sum = 0.0;
let mut count = 0;
for code in first_char..=last_char.min(255) {
let pdf_w = widths[code];
if pdf_w <= 0.0 {
continue;
}
let glyph_name = match encoding[code].as_deref() {
Some(n) if n != ".notdef" && n != "space" => n,
_ => continue,
};
if let Some(cs) = font.charstrings.get(glyph_name)
&& let Ok(result) = execute_charstring(cs, &font.subrs, font.len_iv, false)
{
let sub_w = result.width_x * fm[0];
if sub_w > 0.0 {
let ratio = pdf_w / sub_w;
if ratio > 0.5 && ratio < 2.0 {
pdf_sum += pdf_w;
sub_sum += sub_w;
count += 1;
}
}
}
}
if count >= 3 && sub_sum > 0.0 && !is_standard_14_alias(clean_name) {
let ratio = pdf_sum / sub_sum;
if (ratio - 1.0).abs() > 0.03 {
font_matrix.a *= ratio;
per_char_scale = true;
}
}
}
widths
};
let weight_vector = font.weight_vector.clone();
Some(PdfFont::Type1(Type1PdfFont {
font,
encoding,
widths,
font_matrix,
builtin_fallback: false,
weight_vector,
per_char_width_scale: per_char_scale,
}))
}
fn is_standard_14_alias(name: &str) -> bool {
let normalized = name.replace(',', "-");
matches!(
normalized.as_str(),
"Times-Roman"
| "Times-Bold"
| "Times-Italic"
| "Times-BoldItalic"
| "Helvetica"
| "Helvetica-Bold"
| "Helvetica-Oblique"
| "Helvetica-BoldOblique"
| "Courier"
| "Courier-Bold"
| "Courier-Oblique"
| "Courier-BoldOblique"
| "Symbol"
| "ZapfDingbats"
)
}
fn fuzzy_font_match(name: &str) -> Option<&'static str> {
let lower = name.to_ascii_lowercase();
let is_bold = lower.contains("bold") || lower.contains("demi");
let is_italic = lower.contains("italic") || lower.contains("oblique");
let family = strip_style_suffix(&lower);
if family.contains("times") || family.contains("serif") {
return Some(match (is_bold, is_italic) {
(true, true) => "NimbusRoman-BoldItalic",
(true, false) => "NimbusRoman-Bold",
(false, true) => "NimbusRoman-Italic",
(false, false) => "NimbusRoman-Regular",
});
}
if family.contains("helvetica")
|| family.contains("arial")
|| family.contains("sans")
|| family.contains("calibri")
|| family.contains("verdana")
|| family.contains("tahoma")
{
return Some(match (is_bold, is_italic) {
(true, true) => "NimbusSans-BoldItalic",
(true, false) => "NimbusSans-Bold",
(false, true) => "NimbusSans-Italic",
(false, false) => "NimbusSans-Regular",
});
}
if family.contains("courier") || family.contains("mono") {
return Some(match (is_bold, is_italic) {
(true, true) => "NimbusMonoPS-BoldItalic",
(true, false) => "NimbusMonoPS-Bold",
(false, true) => "NimbusMonoPS-Italic",
(false, false) => "NimbusMonoPS-Regular",
});
}
None
}
fn strip_style_suffix(lower: &str) -> &str {
const SUFFIXES: &[&str] = &[
"-roman", " roman", "-regular", " regular", "-medium", " medium", "-book", " book",
"-normal", " normal", "-light", " light",
];
for suffix in SUFFIXES {
if let Some(prefix) = lower.strip_suffix(suffix) {
return prefix;
}
}
lower
}
const CID_FONT_SUBSTITUTIONS: &[(&str, &str)] = &[
("ArialUnicodeMS", "DejaVuSans"),
("Arial", "LiberationSans"),
("Arial,Bold", "LiberationSans-Bold"),
("Arial,BoldItalic", "LiberationSans-BoldItalic"),
("Arial,Italic", "LiberationSans-Italic"),
("Arial-BoldMT", "LiberationSans-Bold"),
("Arial-BoldItalicMT", "LiberationSans-BoldItalic"),
("Arial-ItalicMT", "LiberationSans-Italic"),
("Arial-ItalicMT,Italic", "LiberationSans-Italic"),
("ArialMT", "LiberationSans"),
("ArialBlack", "LiberationSans-Bold"),
("ArialBlack,Bold", "LiberationSans-Bold"),
("ArialBlack,Italic", "LiberationSans-BoldItalic"),
("ArialBlack,BoldItalic", "LiberationSans-BoldItalic"),
("Arial-BlackMT", "LiberationSans-Bold"),
("CourierNew", "LiberationMono"),
("CourierNew,Bold", "LiberationMono-Bold"),
("CourierNew,BoldItalic", "LiberationMono-BoldItalic"),
("CourierNew,Italic", "LiberationMono-Italic"),
("CourierNewPS-BoldMT", "LiberationMono-Bold"),
("CourierNewPS-BoldItalicMT", "LiberationMono-BoldItalic"),
("CourierNewPS-ItalicMT", "LiberationMono-Italic"),
("CourierNewPSMT", "LiberationMono"),
("LucidaConsole", "LiberationMono"),
("LucidaConsole,Bold", "LiberationMono-Bold"),
("Calibri", "LiberationSans"),
("Calibri,Bold", "LiberationSans-Bold"),
("Calibri,BoldItalic", "LiberationSans-BoldItalic"),
("Calibri,Italic", "LiberationSans-Italic"),
("CenturyGothic", "LiberationSans"),
("CenturyGothic,Bold", "LiberationSans-Bold"),
("CenturyGothic,BoldItalic", "LiberationSans-BoldItalic"),
("CenturyGothic,Italic", "LiberationSans-Italic"),
("TimesNewRoman", "LiberationSerif"),
("TimesNewRoman,Bold", "LiberationSerif-Bold"),
("TimesNewRoman,BoldItalic", "LiberationSerif-BoldItalic"),
("TimesNewRoman,Italic", "LiberationSerif-Italic"),
("TimesNewRomanPS-BoldMT", "LiberationSerif-Bold"),
("TimesNewRomanPS-BoldItalicMT", "LiberationSerif-BoldItalic"),
("TimesNewRomanPS-ItalicMT", "LiberationSerif-Italic"),
("TimesNewRomanPSMT", "LiberationSerif"),
("HeiseiMin-W3", "NotoSansCJKjp-Regular"),
("HeiseiKakuGo-W5", "NotoSansCJKjp-Regular"),
("KozMinPr6N-Regular", "NotoSansCJKjp-Regular"),
("KozGoPr6N-Medium", "NotoSansCJKjp-Regular"),
("MS-Gothic", "NotoSansCJKjp-Regular"),
("MS-Gothic,Bold", "NotoSansCJKjp-Bold"),
("MS-Gothic,Italic", "NotoSansCJKjp-Regular"),
("MS-Gothic,BoldItalic", "NotoSansCJKjp-Bold"),
("MS-PGothic", "NotoSansCJKjp-Regular"),
("MS-PGothic,Bold", "NotoSansCJKjp-Bold"),
("MS-PGothic,Italic", "NotoSansCJKjp-Regular"),
("MS-PGothic,BoldItalic", "NotoSansCJKjp-Bold"),
("MS-Mincho", "NotoSansCJKjp-Regular"),
("MS-Mincho,Bold", "NotoSansCJKjp-Bold"),
("MS-Mincho,Italic", "NotoSansCJKjp-Regular"),
("MS-Mincho,BoldItalic", "NotoSansCJKjp-Bold"),
("MS-PMincho", "NotoSansCJKjp-Regular"),
("MS-PMincho,Bold", "NotoSansCJKjp-Bold"),
("MS-PMincho,Italic", "NotoSansCJKjp-Regular"),
("MS-PMincho,BoldItalic", "NotoSansCJKjp-Bold"),
("MSGothic", "NotoSansCJKjp-Regular"),
("MSPGothic", "NotoSansCJKjp-Regular"),
("MSMincho", "NotoSansCJKjp-Regular"),
("MSPMincho", "NotoSansCJKjp-Regular"),
("Batang", "NotoSansCJKkr-Regular"),
("BatangChe", "NotoSansCJKkr-Regular"),
("Dotum", "NotoSansCJKkr-Regular"),
("DotumChe", "NotoSansCJKkr-Regular"),
("Gulim", "NotoSansCJKkr-Regular"),
("GulimChe", "NotoSansCJKkr-Regular"),
("STSongStd-Light", "NotoSerifCJKjp-Regular"),
("STSong-Light", "NotoSerifCJKjp-Regular"),
("AdobeSongStd-Light", "NotoSerifCJKjp-Regular"),
("STFangsong-Light", "NotoSerifCJKjp-Regular"),
("STHeiti-Regular", "NotoSansCJKjp-Regular"),
("STKaiti-Regular", "NotoSansCJKjp-Regular"),
("SimSun", "NotoSerifCJKjp-Regular"),
("SimSunBold", "NotoSerifCJKjp-Bold"),
("SimHei", "NotoSansCJKjp-Regular"),
("FangSong", "NotoSerifCJKjp-Regular"),
("KaiTi", "NotoSansCJKjp-Regular"),
("MSungStd-Light", "NotoSerifCJKjp-Regular"),
("MSung-Light", "NotoSerifCJKjp-Regular"),
("AdobeMingStd-Light", "NotoSerifCJKjp-Regular"),
("MHei-Medium", "NotoSansCJKjp-Regular"),
("MingLiU", "NotoSerifCJKjp-Regular"),
("PMingLiU", "NotoSerifCJKjp-Regular"),
];
fn cjk_fullwidth_alternative(unicode: u32) -> Option<u32> {
match unicode {
0x00B7 => Some(0x30FB),
_ => None,
}
}
fn is_cff_cid_keyed(otf_data: &[u8]) -> bool {
use stet_fonts::truetype::find_table;
let Some((cff_off, cff_len)) = find_table(otf_data, b"CFF ") else {
return false;
};
let cff_data = &otf_data[cff_off..cff_off + cff_len];
match parse_cff(cff_data) {
Ok(fonts) => fonts.first().map_or(false, |f| f.is_cid),
Err(_) => false,
}
}
fn create_cid_cff_from_otf(
otf_data: &[u8],
default_width: f64,
cid_widths: HashMap<u16, f64>,
ordering: &[u8],
pdf_cid_to_gid: Option<Vec<u16>>,
identity_cid_to_gid: bool,
code_lengths: [u8; 256],
code_to_cid: HashMap<u32, u32>,
wmode: u8,
dw2: [f64; 2],
w2: HashMap<u16, [f64; 3]>,
) -> Result<PdfFont, PdfError> {
use stet_fonts::truetype::find_table;
let (cff_off, cff_len) = find_table(otf_data, b"CFF ")
.ok_or(PdfError::Other("OpenType font has no CFF table".into()))?;
let cff_data = &otf_data[cff_off..cff_off + cff_len];
let fonts =
parse_cff(cff_data).map_err(|e| PdfError::Other(format!("CFF parse error: {e}")))?;
let font = fonts
.into_iter()
.next()
.ok_or(PdfError::Other("CFF contains no fonts".into()))?;
let fm = font.font_matrix;
let font_matrix = Matrix::new(fm[0], fm[1], fm[2], fm[3], fm[4], fm[5]);
let otf_cmap = parse_cmap(otf_data);
let cmap = if otf_cmap.is_empty() {
None
} else {
Some(otf_cmap)
};
Ok(PdfFont::CidCff(CidCffPdfFont {
font,
default_width,
cid_widths,
font_matrix,
cmap,
pdf_cid_to_gid,
identity_cid_to_gid,
ordering: ordering.to_vec(),
code_lengths,
code_to_cid,
wmode,
dw2,
w2,
type1_paths: None,
}))
}
fn is_raw_cff(data: &[u8]) -> bool {
data.len() > 4 && data[0] == 1 && data[1] == 0 && data[2] >= 4 && (1..=4).contains(&data[3])
}
fn create_cid_cff_from_raw(
cff_data: &[u8],
default_width: f64,
cid_widths: HashMap<u16, f64>,
ordering: &[u8],
pdf_cid_to_gid: Option<Vec<u16>>,
identity_cid_to_gid: bool,
code_lengths: [u8; 256],
code_to_cid: HashMap<u32, u32>,
wmode: u8,
dw2: [f64; 2],
w2: HashMap<u16, [f64; 3]>,
) -> Result<PdfFont, PdfError> {
let fonts =
parse_cff(cff_data).map_err(|e| PdfError::Other(format!("CFF parse error: {e}")))?;
let font = fonts
.into_iter()
.next()
.ok_or(PdfError::Other("CFF contains no fonts".into()))?;
let fm = font.font_matrix;
let font_matrix = Matrix::new(fm[0], fm[1], fm[2], fm[3], fm[4], fm[5]);
Ok(PdfFont::CidCff(CidCffPdfFont {
font,
default_width,
cid_widths,
font_matrix,
cmap: None, pdf_cid_to_gid,
identity_cid_to_gid,
ordering: ordering.to_vec(),
code_lengths,
code_to_cid,
wmode,
dw2,
w2,
type1_paths: None,
}))
}
const MAX_OFFSET_BYTES: usize = 8;
fn create_cid_from_ps_cidfont(
font_data: &[u8],
default_width: f64,
cid_widths: HashMap<u16, f64>,
code_lengths: [u8; 256],
code_to_cid: HashMap<u32, u32>,
wmode: u8,
dw2: [f64; 2],
w2: HashMap<u16, [f64; 3]>,
) -> Result<PdfFont, PdfError> {
let text = String::from_utf8_lossy(font_data);
let get_int = |key: &str| -> Option<usize> {
let pat = format!("/{key}");
let idx = text.find(&pat)?;
let rest = &text[idx + pat.len()..];
rest.split_whitespace().next()?.parse().ok()
};
let cid_count = get_int("CIDCount").unwrap_or(0);
let fd_bytes = get_int("FDBytes").unwrap_or(0);
let gd_bytes = get_int("GDBytes").unwrap_or(4);
let subr_map_offset = get_int("SubrMapOffset").unwrap_or(0);
let sd_bytes = get_int("SDBytes").unwrap_or(4);
let subr_count = get_int("SubrCount").unwrap_or(0);
if fd_bytes > MAX_OFFSET_BYTES || gd_bytes > MAX_OFFSET_BYTES || sd_bytes > MAX_OFFSET_BYTES {
return Err(PdfError::Other(
"PS CIDFont: implausible FDBytes/GDBytes/SDBytes".into(),
));
}
let len_iv = get_int("lenIV").unwrap_or(4) as u16;
let font_matrix = if let Some(fm_idx) = text.find("/FontMatrix") {
let rest = &text[fm_idx..];
if let Some(start) = rest.find('[') {
let end_bracket = rest[start..].find(']').unwrap_or(50) + start;
let vals: Vec<f64> = rest[start + 1..end_bracket]
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
if vals.len() == 6 {
Matrix::new(vals[0], vals[1], vals[2], vals[3], vals[4], vals[5])
} else {
Matrix::new(0.001, 0.0, 0.0, 0.001, 0.0, 0.0)
}
} else {
Matrix::new(0.001, 0.0, 0.0, 0.001, 0.0, 0.0)
}
} else {
Matrix::new(0.001, 0.0, 0.0, 0.001, 0.0, 0.0)
};
let binary_data = {
let sd_marker = b"StartData";
let pos = font_data
.windows(sd_marker.len())
.position(|w| w == sd_marker)
.ok_or(PdfError::Other("PS CIDFont: no StartData found".into()))?;
let after = &font_data[pos + sd_marker.len()..];
let skip = after
.iter()
.position(|&b| !matches!(b, b' ' | b'\t' | b'\r' | b'\n'))
.unwrap_or(0);
&font_data[pos + sd_marker.len() + skip..]
};
let entry_size = fd_bytes + gd_bytes;
if entry_size == 0 {
return Err(PdfError::Other(
"PS CIDFont: FDBytes + GDBytes is zero".into(),
));
}
let Some(cid_map_size) = cid_count.checked_mul(entry_size) else {
return Err(PdfError::Other("PS CIDFont: CID map size overflows".into()));
};
if binary_data.len() < cid_map_size {
return Err(PdfError::Other(
"PS CIDFont: binary data too short for CID map".into(),
));
}
let read_be = |data: &[u8], off: usize, n: usize| -> usize {
let mut val = 0usize;
for i in 0..n {
if off + i < data.len() {
val = (val << 8) | data[off + i] as usize;
}
}
val
};
let mut cid_offsets: Vec<usize> = Vec::with_capacity(cid_count + 1);
for c in 0..cid_count {
let entry_off = c * entry_size + fd_bytes;
let offset = read_be(binary_data, entry_off, gd_bytes);
cid_offsets.push(offset);
}
cid_offsets.push(subr_map_offset);
let subr_map_fits = sd_bytes > 0
&& subr_count
.checked_add(1)
.and_then(|n| n.checked_mul(sd_bytes))
.and_then(|n| n.checked_add(subr_map_offset))
.is_some_and(|end| end <= binary_data.len());
let mut subrs: Vec<Vec<u8>> = Vec::new();
if subr_count > 0 && subr_map_fits {
subrs.reserve(subr_count);
let mut sub_offsets: Vec<usize> = Vec::with_capacity(subr_count + 1);
for i in 0..=subr_count {
let off = read_be(binary_data, subr_map_offset + i * sd_bytes, sd_bytes);
sub_offsets.push(off);
}
for i in 0..subr_count {
let start = sub_offsets[i];
let end = sub_offsets[i + 1];
if start < end && end <= binary_data.len() {
subrs.push(binary_data[start..end].to_vec());
} else {
subrs.push(Vec::new());
}
}
}
let mut paths = HashMap::new();
for &cid in cid_widths.keys() {
let c = cid as usize;
if c >= cid_count {
continue;
}
let cs_start = cid_offsets[c];
let cs_end = cid_offsets[c + 1];
if cs_start >= cs_end || cs_end > binary_data.len() {
continue;
}
let charstring = &binary_data[cs_start..cs_end];
if let Ok(result) = execute_charstring(charstring, &subrs, len_iv.into(), false) {
let path = result.path.transform(&font_matrix);
paths.insert(cid, path);
}
}
let dummy_cff = stet_fonts::cff_parser::CffFont {
name: String::new(),
font_matrix: [
font_matrix.a,
font_matrix.b,
font_matrix.c,
font_matrix.d,
font_matrix.tx,
font_matrix.ty,
],
font_bbox: [0.0; 4],
char_strings: Vec::new(),
global_subrs: Vec::new(),
local_subrs: Vec::new(),
charset: Vec::new(),
encoding: Vec::new(),
default_width_x: 0.0,
nominal_width_x: 0.0,
is_cid: true,
fd_array: Vec::new(),
fd_select: Vec::new(),
ros: None,
cid_to_gid: Vec::new(),
};
Ok(PdfFont::CidCff(CidCffPdfFont {
font: dummy_cff,
default_width,
cid_widths,
font_matrix,
cmap: None,
pdf_cid_to_gid: None,
identity_cid_to_gid: true,
ordering: Vec::new(),
code_lengths,
code_to_cid,
wmode,
dw2,
w2,
type1_paths: Some(paths),
}))
}
fn create_cid_from_type1(
font_data: &[u8],
default_width: f64,
cid_widths: HashMap<u16, f64>,
_to_unicode: &HashMap<u16, u32>,
code_lengths: [u8; 256],
code_to_cid: HashMap<u32, u32>,
wmode: u8,
dw2: [f64; 2],
w2: HashMap<u16, [f64; 3]>,
) -> Result<PdfFont, PdfError> {
let font =
parse_type1(font_data).map_err(|e| PdfError::Other(format!("Type1 parse error: {e}")))?;
let fm = font.font_matrix;
let font_matrix = Matrix::new(fm[0], fm[1], fm[2], fm[3], fm[4], fm[5]);
let mut paths = HashMap::new();
for (&cid, _) in &cid_widths {
let glyph_name = if (cid as usize) < font.encoding.len() {
font.encoding[cid as usize].as_str()
} else {
".notdef"
};
if let Some(cs) = font.charstrings.get(glyph_name) {
if let Ok(result) = execute_charstring(cs, &font.subrs, font.len_iv, false) {
let path = result.path.transform(&font_matrix);
paths.insert(cid, path);
}
}
}
for (code, name) in font.encoding.iter().enumerate() {
let cid = code as u16;
if paths.contains_key(&cid) {
continue;
}
{
let name = name.as_str();
if name != ".notdef" {
if let Some(cs) = font.charstrings.get(name) {
if let Ok(result) = execute_charstring(cs, &font.subrs, font.len_iv, false) {
let path = result.path.transform(&font_matrix);
paths.insert(cid, path);
}
}
}
}
}
let dummy_cff = stet_fonts::cff_parser::CffFont {
name: font.font_name.clone(),
font_matrix: fm,
font_bbox: [0.0; 4],
char_strings: Vec::new(),
global_subrs: Vec::new(),
local_subrs: Vec::new(),
charset: Vec::new(),
encoding: Vec::new(),
default_width_x: 0.0,
nominal_width_x: 0.0,
is_cid: false,
fd_array: Vec::new(),
fd_select: Vec::new(),
ros: None,
cid_to_gid: Vec::new(),
};
Ok(PdfFont::CidCff(CidCffPdfFont {
font: dummy_cff,
default_width,
cid_widths,
font_matrix,
cmap: None,
pdf_cid_to_gid: None,
identity_cid_to_gid: true,
ordering: Vec::new(),
code_lengths,
code_to_cid,
wmode,
dw2,
w2,
type1_paths: Some(paths),
}))
}
fn sanitize_index_to_loc_format(font_data: &mut [u8]) {
use stet_fonts::truetype::{find_table, read_i16, read_u16};
let head = find_table(font_data, b"head");
let loca = find_table(font_data, b"loca");
let maxp = find_table(font_data, b"maxp");
let (head_off, _) = match head {
Some(h) => h,
None => return,
};
if head_off + 52 > font_data.len() {
return;
}
let format = read_i16(font_data, head_off + 50);
if format == 0 || format == 1 {
return; }
let correct = if let (Some((_, loca_len)), Some((maxp_off, _))) = (loca, maxp) {
if maxp_off + 6 <= font_data.len() {
let num_glyphs = read_u16(font_data, maxp_off + 4) as usize;
if loca_len == (num_glyphs + 1) * 4 {
1i16 } else {
0i16 }
} else {
if format != 0 { 1 } else { 0 }
}
} else {
if format != 0 { 1 } else { 0 }
};
font_data[head_off + 50] = (correct >> 8) as u8;
font_data[head_off + 51] = correct as u8;
}
fn load_system_truetype_font(base_font: &str) -> Result<Vec<u8>, PdfError> {
use stet_fonts::system_fonts::get_system_font_cache;
let cache = get_system_font_cache();
let mut clean_name = base_font;
if clean_name.len() > 7 && clean_name.as_bytes().get(6) == Some(&b'+') {
clean_name = &clean_name[7..];
}
if let Some(path) = cache.get_font_path(clean_name)
&& let Ok(data) = read_font_file(path, clean_name)
{
return Ok(data);
}
for &(from, to) in CID_FONT_SUBSTITUTIONS {
if from == clean_name
&& let Some(path) = cache.get_font_path(to)
&& let Ok(data) = read_font_file(path, to)
{
return Ok(data);
}
}
let lower = clean_name.to_ascii_lowercase();
let is_bold = lower.contains("bold") || lower.contains("demi");
let is_italic = lower.contains("italic") || lower.contains("oblique");
for (ps_name, path) in cache.iter() {
let ps_lower = ps_name.to_ascii_lowercase();
let family = lower.split(&['-', ','][..]).next().unwrap_or(&lower);
if ps_lower.contains(family) || family.contains(ps_lower.split('-').next().unwrap_or("")) {
let name_bold = ps_lower.contains("bold") || ps_lower.contains("demi");
let name_italic = ps_lower.contains("italic") || ps_lower.contains("oblique");
if name_bold == is_bold
&& name_italic == is_italic
&& let Ok(data) = read_font_file(path, ps_name)
{
return Ok(data);
}
}
}
Err(PdfError::Other(format!(
"font '{}' not found on system",
clean_name
)))
}
fn load_cjk_fallback_font(ordering: &[u8], base_font: &str) -> Result<Vec<u8>, PdfError> {
use stet_fonts::system_fonts::get_system_font_cache;
if ordering.is_empty() {
return Err(PdfError::Other("no CJK ordering for fallback".into()));
}
let cache = get_system_font_cache();
let lower = base_font.to_ascii_lowercase();
let is_bold = lower.contains("bold") || lower.contains("demi") || lower.contains("black");
let has_cjk_gothic = {
if let Some(pos) = lower.find("gothic") {
pos == 0 || !lower.as_bytes()[pos - 1].is_ascii_alphabetic()
} else {
false
}
};
let is_cjk_name = has_cjk_gothic
|| [
"cn", "sc", "jp", "kr", "tc", "hk", "cjk", "ming", "song", "hei", "kai", "fang", "han",
]
.iter()
.any(|kw| lower.contains(kw));
if ordering == b"Identity" && !is_cjk_name {
let latin_targets: &[&str] = if is_bold {
&["LiberationSans-Bold", "DejaVuSans-Bold"]
} else {
&["LiberationSans", "DejaVuSans"]
};
for &target in latin_targets {
if let Some(path) = cache.get_font_path(target)
&& let Ok(data) = read_font_file(path, target)
{
return Ok(data);
}
}
return Err(PdfError::Other(format!(
"Latin fallback font not found for '{}'",
base_font
)));
}
let lang = if lower.contains("cn") || lower.contains("sc") || ordering == b"GB1" {
"sc"
} else if lower.contains("tw") || lower.contains("tc") || ordering == b"CNS1" {
"tc"
} else if lower.contains("kr") || ordering == b"Korea1" {
"kr"
} else if lower.contains("hk") {
"hk"
} else {
"jp" };
let heavy = lower.contains("heavy") || lower.contains("black");
let weight_suffix = if heavy {
"Black"
} else if is_bold {
"Bold"
} else {
"Regular"
};
let targets = [
format!("NotoSansCJK{lang}-{weight_suffix}"),
if is_bold || heavy {
format!("NotoSansCJK{lang}-Bold")
} else {
format!("NotoSansCJK{lang}-Regular")
},
format!("NotoSansCJKjp-{weight_suffix}"),
];
for target in &targets {
if let Some(path) = cache.get_font_path(target)
&& let Ok(data) = read_font_file(path, target)
{
return Ok(data);
}
}
Err(PdfError::Other(format!(
"CJK fallback font not found on system for '{}'",
base_font
)))
}
const EMBEDDED_FONTS: &[(&str, &[u8])] = &[
(
"NimbusRoman-Regular",
include_bytes!("../../fonts/NimbusRoman-Regular.t1"),
),
(
"NimbusRoman-Bold",
include_bytes!("../../fonts/NimbusRoman-Bold.t1"),
),
(
"NimbusRoman-Italic",
include_bytes!("../../fonts/NimbusRoman-Italic.t1"),
),
(
"NimbusRoman-BoldItalic",
include_bytes!("../../fonts/NimbusRoman-BoldItalic.t1"),
),
(
"NimbusSans-Regular",
include_bytes!("../../fonts/NimbusSans-Regular.t1"),
),
(
"NimbusSans-Bold",
include_bytes!("../../fonts/NimbusSans-Bold.t1"),
),
(
"NimbusSans-Italic",
include_bytes!("../../fonts/NimbusSans-Italic.t1"),
),
(
"NimbusSans-BoldItalic",
include_bytes!("../../fonts/NimbusSans-BoldItalic.t1"),
),
(
"NimbusSansNarrow-Regular",
include_bytes!("../../fonts/NimbusSansNarrow-Regular.t1"),
),
(
"NimbusSansNarrow-Bold",
include_bytes!("../../fonts/NimbusSansNarrow-Bold.t1"),
),
(
"NimbusSansNarrow-Oblique",
include_bytes!("../../fonts/NimbusSansNarrow-Oblique.t1"),
),
(
"NimbusSansNarrow-BoldOblique",
include_bytes!("../../fonts/NimbusSansNarrow-BoldOblique.t1"),
),
(
"NimbusMonoPS-Regular",
include_bytes!("../../fonts/NimbusMonoPS-Regular.t1"),
),
(
"NimbusMonoPS-Bold",
include_bytes!("../../fonts/NimbusMonoPS-Bold.t1"),
),
(
"NimbusMonoPS-Italic",
include_bytes!("../../fonts/NimbusMonoPS-Italic.t1"),
),
(
"NimbusMonoPS-BoldItalic",
include_bytes!("../../fonts/NimbusMonoPS-BoldItalic.t1"),
),
("P052-Roman", include_bytes!("../../fonts/P052-Roman.t1")),
("P052-Bold", include_bytes!("../../fonts/P052-Bold.t1")),
("P052-Italic", include_bytes!("../../fonts/P052-Italic.t1")),
(
"P052-BoldItalic",
include_bytes!("../../fonts/P052-BoldItalic.t1"),
),
("C059-Roman", include_bytes!("../../fonts/C059-Roman.t1")),
("C059-Bold", include_bytes!("../../fonts/C059-Bold.t1")),
("C059-Italic", include_bytes!("../../fonts/C059-Italic.t1")),
("C059-BdIta", include_bytes!("../../fonts/C059-BdIta.t1")),
(
"URWBookman-Light",
include_bytes!("../../fonts/URWBookman-Light.t1"),
),
(
"URWBookman-Demi",
include_bytes!("../../fonts/URWBookman-Demi.t1"),
),
(
"URWBookman-LightItalic",
include_bytes!("../../fonts/URWBookman-LightItalic.t1"),
),
(
"URWBookman-DemiItalic",
include_bytes!("../../fonts/URWBookman-DemiItalic.t1"),
),
(
"URWGothic-Book",
include_bytes!("../../fonts/URWGothic-Book.t1"),
),
(
"URWGothic-Demi",
include_bytes!("../../fonts/URWGothic-Demi.t1"),
),
(
"URWGothic-BookOblique",
include_bytes!("../../fonts/URWGothic-BookOblique.t1"),
),
(
"URWGothic-DemiOblique",
include_bytes!("../../fonts/URWGothic-DemiOblique.t1"),
),
(
"StandardSymbolsPS",
include_bytes!("../../fonts/StandardSymbolsPS.t1"),
),
("D050000L", include_bytes!("../../fonts/D050000L.t1")),
(
"Z003-MediumItalic",
include_bytes!("../../fonts/Z003-MediumItalic.t1"),
),
];
fn embedded_font(name: &str) -> Option<Vec<u8>> {
EMBEDDED_FONTS
.iter()
.find(|(n, _)| *n == name)
.map(|(_, data)| data.to_vec())
}
fn read_font_file(path: &std::path::Path, ps_name: &str) -> std::io::Result<Vec<u8>> {
let data = std::fs::read(path)?;
if data.len() > 12 && &data[0..4] == b"ttcf" {
let num_fonts = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize;
let mut best_offset = if num_fonts > 0 {
u32::from_be_bytes([data[12], data[13], data[14], data[15]]) as usize
} else {
0
};
for i in 0..num_fonts {
let off_pos = 12 + i * 4;
if off_pos + 4 > data.len() {
break;
}
let font_offset = u32::from_be_bytes([
data[off_pos],
data[off_pos + 1],
data[off_pos + 2],
data[off_pos + 3],
]) as usize;
if let Some(name) = extract_ps_name_at_offset(&data, font_offset)
&& name == ps_name
{
best_offset = font_offset;
break;
}
}
extract_ttf_from_ttc(&data, best_offset)
} else {
Ok(data)
}
}
fn extract_ps_name_at_offset(data: &[u8], offset: usize) -> Option<String> {
use stet_fonts::truetype::read_u16;
if offset + 12 > data.len() {
return None;
}
let num_tables = read_u16(data, offset + 4) as usize;
let mut name_off = 0usize;
let mut name_len = 0usize;
for i in 0..num_tables {
let entry = offset + 12 + i * 16;
if entry + 16 > data.len() {
break;
}
if &data[entry..entry + 4] == b"name" {
name_off = u32::from_be_bytes([
data[entry + 8],
data[entry + 9],
data[entry + 10],
data[entry + 11],
]) as usize;
name_len = u32::from_be_bytes([
data[entry + 12],
data[entry + 13],
data[entry + 14],
data[entry + 15],
]) as usize;
break;
}
}
if name_off == 0 || name_off + name_len > data.len() {
return None;
}
let nd = &data[name_off..name_off + name_len];
let count = read_u16(nd, 2) as usize;
let string_offset = read_u16(nd, 4) as usize;
for i in 0..count {
let rec = 6 + i * 12;
if rec + 12 > nd.len() {
break;
}
let pid = read_u16(nd, rec);
let name_id = read_u16(nd, rec + 6);
let length = read_u16(nd, rec + 8) as usize;
let str_off = read_u16(nd, rec + 10) as usize;
if name_id == 6 {
let start = string_offset + str_off;
if start + length <= nd.len() {
let raw = &nd[start..start + length];
if pid == 3 {
let s: String = raw
.chunks(2)
.filter_map(|c| {
if c.len() == 2 {
Some(u16::from_be_bytes([c[0], c[1]]) as u8 as char)
} else {
None
}
})
.collect();
return Some(s);
} else {
return Some(String::from_utf8_lossy(raw).to_string());
}
}
}
}
None
}
fn extract_ttf_from_ttc(ttc_data: &[u8], font_offset: usize) -> std::io::Result<Vec<u8>> {
use stet_fonts::truetype::{read_u16, read_u32};
if font_offset + 12 > ttc_data.len() {
return Err(std::io::Error::other("TTC font offset out of range"));
}
let num_tables = read_u16(ttc_data, font_offset + 4) as usize;
let header_size = 12 + num_tables * 16;
let mut tables = Vec::with_capacity(num_tables);
for i in 0..num_tables {
let entry = font_offset + 12 + i * 16;
if entry + 16 > ttc_data.len() {
break;
}
let tag = &ttc_data[entry..entry + 4];
let offset = read_u32(ttc_data, entry + 8) as usize;
let length = read_u32(ttc_data, entry + 12) as usize;
tables.push((tag.to_vec(), offset, length));
}
let mut result = Vec::with_capacity(
header_size + tables.iter().map(|(_, _, l)| (l + 3) & !3).sum::<usize>(),
);
result.extend_from_slice(&ttc_data[font_offset..font_offset + 12]);
let mut data_offset = header_size as u32;
let mut new_offsets = Vec::with_capacity(num_tables);
for (_, _, length) in &tables {
new_offsets.push(data_offset);
data_offset += ((*length as u32) + 3) & !3; }
for (i, (tag, _, length)) in tables.iter().enumerate() {
let entry = font_offset + 12 + i * 16;
result.extend_from_slice(tag); result.extend_from_slice(&ttc_data[entry + 4..entry + 8]); result.extend_from_slice(&new_offsets[i].to_be_bytes()); result.extend_from_slice(&(*length as u32).to_be_bytes()); }
for (_, ttc_offset, length) in &tables {
let end = (*ttc_offset + *length).min(ttc_data.len());
if *ttc_offset < ttc_data.len() {
result.extend_from_slice(&ttc_data[*ttc_offset..end]);
let pad = (4 - (length % 4)) % 4;
result.extend(std::iter::repeat_n(0u8, pad));
}
}
Ok(result)
}
fn resolve_type3(resolver: &Resolver, font_dict: &PdfDict) -> Result<PdfFont, PdfError> {
let first_char = font_dict.get_int(b"FirstChar").unwrap_or(0) as usize;
let mut widths = [0.0f64; 256];
let widths_resolved = font_dict
.get(b"Widths")
.and_then(|obj| resolver.deref(obj).ok());
if let Some(ref w_obj) = widths_resolved
&& let Some(w_arr) = w_obj.as_array()
{
for (i, obj) in w_arr.iter().enumerate() {
let code = first_char + i;
if code < 256 {
let val = if obj.as_f64().is_some() {
obj.as_f64().unwrap()
} else if let Ok(resolved) = resolver.deref(obj) {
resolved.as_f64().unwrap_or(0.0)
} else {
0.0
};
widths[code] = val;
}
}
}
let font_matrix = font_dict
.get_array(b"FontMatrix")
.map(|a| {
let v: Vec<f64> = a.iter().filter_map(|o| o.as_f64()).collect();
if v.len() >= 6 {
Matrix::new(v[0], v[1], v[2], v[3], v[4], v[5])
} else {
Matrix::new(0.001, 0.0, 0.0, 0.001, 0.0, 0.0)
}
})
.unwrap_or_else(|| Matrix::new(0.001, 0.0, 0.0, 0.001, 0.0, 0.0));
let font_bbox = font_dict
.get_array(b"FontBBox")
.map(|a| {
let v: Vec<f64> = a.iter().filter_map(|o| o.as_f64()).collect();
if v.len() >= 4 {
[v[0], v[1], v[2], v[3]]
} else {
[0.0, 0.0, 1.0, 1.0]
}
})
.unwrap_or([0.0, 0.0, 1.0, 1.0]);
let (encoding, _, _, _) = resolve_encoding(font_dict, resolver)?;
let char_procs_dict = if let Some(obj) = font_dict.get(b"CharProcs") {
match resolver.deref(obj)? {
PdfObj::Dict(d) => d,
_ => return Err(PdfError::Other("Type3 CharProcs is not a dict".into())),
}
} else {
return Err(PdfError::Other("Type3 font missing CharProcs".into()));
};
let resources = if let Some(res_ref) = font_dict.get(b"Resources") {
match resolver.deref(res_ref)? {
PdfObj::Dict(d) => d,
_ => PdfDict::new(),
}
} else {
PdfDict::new()
};
let mut char_procs = HashMap::new();
for code in 0..256u16 {
if let Some(glyph_name) = &encoding[code as usize]
&& let Some(proc_ref) = char_procs_dict.get(glyph_name.as_bytes())
&& let Ok(data) = resolver.stream_data_from_obj(proc_ref)
{
char_procs.insert(code as u8, data);
}
}
Ok(PdfFont::Type3(Type3PdfFont {
char_procs,
resources,
widths,
font_matrix,
font_bbox,
}))
}
fn resolve_type1(
resolver: &Resolver,
descriptor: &Option<PdfDict>,
encoding: [Option<String>; 256],
widths: [f64; 256],
has_explicit_encoding: bool,
has_pdf_widths: bool,
differences: &[(usize, String)],
no_base_encoding: bool,
) -> Result<PdfFont, PdfError> {
let desc = descriptor
.as_ref()
.ok_or(PdfError::Other("Type1 font missing FontDescriptor".into()))?;
if let Some(ff3_ref) = desc.get(b"FontFile3") {
let ff3_obj = resolver.deref(ff3_ref)?;
let ff3_dict = ff3_obj.as_dict();
let subtype = ff3_dict.and_then(|d| d.get_name(b"Subtype")).unwrap_or(b"");
if subtype == b"Type1C" || subtype == b"CIDFontType0C" || subtype == b"OpenType" {
let raw_data = resolver.stream_data_from_obj(ff3_ref)?;
let font_data = if raw_data.starts_with(b"OTTO") {
use stet_fonts::truetype::find_table;
let (offset, length) = find_table(&raw_data, b"CFF ")
.ok_or(PdfError::Other("OpenType font has no CFF table".into()))?;
raw_data[offset..offset + length].to_vec()
} else {
raw_data
};
let fonts = parse_cff(&font_data)
.map_err(|e| PdfError::Other(format!("CFF parse error: {e}")))?;
let font = fonts
.into_iter()
.next()
.ok_or(PdfError::Other("CFF contains no fonts".into()))?;
let fm = font.font_matrix;
let font_matrix = Matrix::new(fm[0], fm[1], fm[2], fm[3], fm[4], fm[5]);
return Ok(PdfFont::Cff(CffPdfFont {
font,
encoding,
widths,
font_matrix,
}));
}
}
let ff_ref = desc
.get(b"FontFile")
.or_else(|| desc.get(b"FontFile3"))
.ok_or(PdfError::Other("Type1 font missing FontFile".into()))?;
let font_data = resolver.stream_data_from_obj(ff_ref)?;
let font_data = strip_pfb(&font_data);
let font =
parse_type1(&font_data).map_err(|e| PdfError::Other(format!("Type1 parse error: {e}")))?;
let encoding = if no_base_encoding && font.encoding.len() == 256 {
let mut builtin: [Option<String>; 256] = std::array::from_fn(|_| None);
for (i, name) in font.encoding.iter().enumerate() {
if name != ".notdef" {
builtin[i] = Some(name.clone());
}
}
for (code, name) in differences {
if *code < 256 {
builtin[*code] = Some(name.clone());
}
}
builtin
} else if !has_explicit_encoding {
let flags = desc.get_int(b"Flags").unwrap_or(0) as u32;
let is_symbolic = flags & 4 != 0;
if is_symbolic && font.encoding.len() == 256 {
let mut builtin: [Option<String>; 256] = std::array::from_fn(|_| None);
for (i, name) in font.encoding.iter().enumerate() {
if name != ".notdef" {
builtin[i] = Some(name.clone());
}
}
builtin
} else {
encoding
}
} else {
encoding
};
let builtin_fallback = {
let flags = desc.get_int(b"Flags").unwrap_or(0) as u32;
let is_sym = flags & 4 != 0;
let builtin_useful =
is_sym && font.encoding.len() == 256 && font.encoding.iter().any(|n| n != ".notdef");
if builtin_useful {
!encoding[32..127].iter().any(|slot| {
slot.as_ref()
.is_some_and(|name| font.charstrings.contains_key(name.as_str()))
})
} else {
false
}
};
let fm = font.font_matrix;
let font_matrix = Matrix::new(fm[0], fm[1], fm[2], fm[3], fm[4], fm[5]);
let widths = if !has_pdf_widths {
let mut derived = [0.0f64; 256];
for code in 0..256usize {
let glyph_name = encoding[code].as_deref().unwrap_or(".notdef");
if glyph_name == ".notdef" {
continue;
}
if let Some(charstring) = font.charstrings.get(glyph_name) {
let cs_lookup =
|name: &str| -> Option<Vec<u8>> { font.charstrings.get(name).cloned() };
if let Ok(result) = execute_charstring_mm(
charstring,
&font.subrs,
font.len_iv,
false,
Some(&cs_lookup),
font.weight_vector.as_deref(),
) {
derived[code] = result.width_x * fm[0];
}
}
}
derived
} else {
widths
};
let weight_vector = font.weight_vector.clone();
Ok(PdfFont::Type1(Type1PdfFont {
font,
encoding,
widths,
font_matrix,
weight_vector,
builtin_fallback,
per_char_width_scale: false,
}))
}
fn try_raw_deflate_if_truncated(resolver: &Resolver, ff_ref: &PdfObj, data: Vec<u8>) -> Vec<u8> {
if data.len() < 12 {
return data;
}
let num_tables = u16::from_be_bytes([data[4], data[5]]) as usize;
let mut max_end = 0usize;
for i in 0..num_tables {
let e = 12 + i * 16;
if e + 16 > data.len() {
break;
}
let off =
u32::from_be_bytes([data[e + 8], data[e + 9], data[e + 10], data[e + 11]]) as usize;
let len =
u32::from_be_bytes([data[e + 12], data[e + 13], data[e + 14], data[e + 15]]) as usize;
max_end = max_end.max(off.saturating_add(len));
}
if max_end <= data.len() {
return data; }
let raw_bytes = match resolver.raw_stream_bytes(ff_ref) {
Some(b) if b.len() > 2 => b,
_ => return data,
};
let cinfo = raw_bytes[0] >> 4;
let cm = raw_bytes[0] & 0xF;
if cm != 8 || cinfo >= 7 {
return data;
}
let mut decoder = flate2::Decompress::new(false);
let mut output = Vec::with_capacity(data.len() * 2);
let mut buf = [0u8; 8192];
let input = &raw_bytes[2..];
let mut input_offset = 0;
loop {
let before_in = decoder.total_in() as usize;
let before_out = decoder.total_out() as usize;
let result = decoder.decompress(
&input[input_offset..],
&mut buf,
flate2::FlushDecompress::None,
);
let consumed = decoder.total_in() as usize - before_in;
let produced = decoder.total_out() as usize - before_out;
input_offset += consumed;
output.extend_from_slice(&buf[..produced]);
match result {
Ok(flate2::Status::StreamEnd) => break,
Ok(_) => {
if consumed == 0 && produced == 0 {
break;
}
}
Err(_) => break,
}
}
if output.len() <= data.len() {
return data;
}
let mut raw_max_end = 0usize;
for i in 0..num_tables {
let e = 12 + i * 16;
if e + 16 > output.len() {
return data;
}
let off = u32::from_be_bytes([output[e + 8], output[e + 9], output[e + 10], output[e + 11]])
as usize;
let len = u32::from_be_bytes([
output[e + 12],
output[e + 13],
output[e + 14],
output[e + 15],
]) as usize;
raw_max_end = raw_max_end.max(off.saturating_add(len));
}
if raw_max_end > output.len() {
return data; }
if stet_fonts::truetype::get_units_per_em(&output) == 0 {
return data;
}
output
}
fn resolve_truetype(
resolver: &Resolver,
descriptor: &Option<PdfDict>,
encoding: [Option<String>; 256],
widths: [f64; 256],
font_dict: &PdfDict,
) -> Result<PdfFont, PdfError> {
let desc = descriptor.as_ref().ok_or(PdfError::Other(
"TrueType font missing FontDescriptor".into(),
))?;
let ff_ref = desc
.get(b"FontFile2")
.ok_or(PdfError::Other("TrueType font missing FontFile2".into()))?;
let data = resolver.stream_data_from_obj(ff_ref)?;
let data = try_raw_deflate_if_truncated(resolver, ff_ref, data);
use stet_fonts::truetype::find_table;
let has_glyf = find_table(&data, b"glyf").is_some();
let has_usable_glyx = if let Some((off, len)) = find_table(&data, b"glyx") {
off + len <= data.len()
} else {
false
};
if !has_glyf && !has_usable_glyx {
let is_otf = data.starts_with(b"OTTO");
let is_cff = is_raw_cff(&data);
if is_otf || is_cff {
let has_explicit_encoding = font_dict.get(b"Encoding").is_some();
let has_pdf_widths = font_dict.get(b"Widths").is_some();
return build_cff_font(
data,
encoding,
widths,
has_explicit_encoding,
has_pdf_widths,
&[],
false,
);
}
return Err(PdfError::Other(
"TrueType font has no usable glyph outline data".into(),
));
}
if let Some((off, _)) = find_table(&data, b"head") {
if off + 54 > data.len() {
return Err(PdfError::Other(
"TrueType font head table is out of bounds (truncated data)".into(),
));
}
}
let units_per_em = get_units_per_em(&data) as f64;
if units_per_em < 16.0 {
return Err(PdfError::Other(
"TrueType font has degenerate unitsPerEm (placeholder outlines)".into(),
));
}
let (cmap, cmap_is_unicode) = parse_cmap_with_info(&data);
let post_name_to_gid = stet_fonts::system_fonts::parse_post_table(&data)
.map(|gid_to_name| {
gid_to_name
.into_iter()
.map(|(gid, name)| (name, gid))
.collect()
})
.unwrap_or_default();
let flags = desc.get_int(b"Flags").unwrap_or(0) as u32;
let is_symbolic = flags & 4 != 0;
let has_encoding = font_dict.get(b"Encoding").is_some();
let identity_gid = is_symbolic && !has_encoding && cmap_is_unicode;
let gid_hex = TrueTypePdfFont::detect_gid_hex(&encoding);
Ok(PdfFont::TrueType(TrueTypePdfFont {
data,
encoding,
widths,
cmap,
cmap_is_unicode,
post_name_to_gid,
units_per_em,
to_unicode: if let Some(tu_obj) = font_dict.get(b"ToUnicode") {
resolver
.stream_data_from_obj(tu_obj)
.map(|d| parse_to_unicode(&d))
.unwrap_or_default()
} else {
HashMap::new()
},
identity_gid,
gid_hex,
}))
}
fn resolve_cff(
resolver: &Resolver,
descriptor: &Option<PdfDict>,
encoding: [Option<String>; 256],
widths: [f64; 256],
has_explicit_encoding: bool,
has_pdf_widths: bool,
differences: &[(usize, String)],
no_base_encoding: bool,
) -> Result<PdfFont, PdfError> {
let desc = descriptor
.as_ref()
.ok_or(PdfError::Other("CFF font missing FontDescriptor".into()))?;
let ff_ref = desc
.get(b"FontFile3")
.ok_or(PdfError::Other("CFF font missing FontFile3".into()))?;
let raw_data = resolver.stream_data_from_obj(ff_ref)?;
build_cff_font(
raw_data,
encoding,
widths,
has_explicit_encoding,
has_pdf_widths,
differences,
no_base_encoding,
)
}
fn build_cff_font(
raw_data: Vec<u8>,
encoding: [Option<String>; 256],
widths: [f64; 256],
has_explicit_encoding: bool,
has_pdf_widths: bool,
differences: &[(usize, String)],
no_base_encoding: bool,
) -> Result<PdfFont, PdfError> {
let font_data = if raw_data.starts_with(b"OTTO") {
use stet_fonts::truetype::find_table;
let (offset, length) = find_table(&raw_data, b"CFF ")
.ok_or(PdfError::Other("OpenType font has no CFF table".into()))?;
raw_data[offset..offset + length].to_vec()
} else {
raw_data
};
let fonts =
parse_cff(&font_data).map_err(|e| PdfError::Other(format!("CFF parse error: {e}")))?;
let font = fonts
.into_iter()
.next()
.ok_or(PdfError::Other("CFF contains no fonts".into()))?;
let build_cff_encoding = |font: &stet_fonts::cff_parser::CffFont| -> [Option<String>; 256] {
let mut enc: [Option<String>; 256] = std::array::from_fn(|_| None);
let name_to_gid: std::collections::HashMap<&str, u16> = font
.charset
.iter()
.enumerate()
.map(|(gid, name)| (name.as_str(), gid as u16))
.collect();
#[allow(clippy::needless_range_loop)]
for code in 0..256 {
let gid = font.encoding[code] as usize;
if gid > 0 && gid < font.charset.len() && font.charset[gid] != ".notdef" {
enc[code] = Some(font.charset[gid].clone());
}
}
if name_to_gid.contains_key("Asmall") {
for &(code, sid) in &stet_fonts::cff_parser::EXPERT_ENCODING_MAP {
if enc[code as usize].is_none() {
let name = stet_fonts::cff_parser::get_sid_string(sid, &[]);
if let Some(&gid) = name_to_gid.get(name.as_str()) {
if gid > 0 {
enc[code as usize] = Some(font.charset[gid as usize].clone());
}
}
}
}
for code in b'a'..=b'z' {
if enc[code as usize].is_none() {
let small_name = format!("{}small", (code - b'a' + b'A') as char);
if name_to_gid.contains_key(small_name.as_str()) {
enc[code as usize] = Some(small_name);
}
}
}
}
enc
};
let encoding = if no_base_encoding || !differences.is_empty() {
let mut enc = build_cff_encoding(&font);
for (code, name) in differences {
if *code < 256 {
enc[*code] = Some(name.clone());
}
}
enc
} else if !has_explicit_encoding {
build_cff_encoding(&font)
} else {
encoding
};
let fm = font.font_matrix;
let font_matrix = Matrix::new(fm[0], fm[1], fm[2], fm[3], fm[4], fm[5]);
let widths = if !has_pdf_widths {
use stet_fonts::type2_charstring::execute_type2_charstring;
let mut derived = [0.0f64; 256];
for code in 0..256usize {
let glyph_name = encoding[code].as_deref().unwrap_or(".notdef");
let gid = font
.charset
.iter()
.position(|name| name == glyph_name)
.unwrap_or(0);
if gid > 0 && gid < font.char_strings.len() {
if let Ok(result) = execute_type2_charstring(
&font.char_strings[gid],
&font.local_subrs,
&font.global_subrs,
font.default_width_x,
font.nominal_width_x,
true, ) {
derived[code] = result.width_x * fm[0];
}
}
}
derived
} else {
widths
};
Ok(PdfFont::Cff(CffPdfFont {
font,
encoding,
widths,
font_matrix,
}))
}
fn resolve_type0(resolver: &Resolver, font_dict: &PdfDict) -> Result<PdfFont, PdfError> {
let encoding_obj = font_dict.get(b"Encoding");
let encoding_name = font_dict.get_name(b"Encoding").unwrap_or(b"");
let ucs2_encoding = encoding_name.windows(4).any(|w| w == b"UCS2");
let (code_lengths, code_to_cid, mut wmode) = if let Some(enc_obj) = encoding_obj {
if let Ok(cmap_data) = resolver.stream_data_from_obj(enc_obj) {
let cmap = super::cmap::CMap::parse_with_loader(
&cmap_data,
Some(&|name| load_predefined_cmap(name)),
);
(cmap.code_lengths, cmap.code_to_cid, cmap.wmode)
} else if !encoding_name.is_empty() && !encoding_name.starts_with(b"Identity") {
if let Some(cmap_data) = load_predefined_cmap(encoding_name) {
let cmap = super::cmap::CMap::parse_with_loader(
&cmap_data,
Some(&|name| load_predefined_cmap(name)),
);
(cmap.code_lengths, cmap.code_to_cid, cmap.wmode)
} else {
eprintln!(
"warning: predefined CMap '{}' not found; \
set STET_CMAP_DIR or install poppler-data for CJK support",
String::from_utf8_lossy(encoding_name)
);
([2u8; 256], HashMap::new(), 0)
}
} else {
([2u8; 256], HashMap::new(), 0) }
} else {
([2u8; 256], HashMap::new(), 0)
};
if encoding_name.ends_with(b"-V") {
wmode = 1;
} else if encoding_name.ends_with(b"-H") {
wmode = 0;
}
let descendants_obj = font_dict
.get(b"DescendantFonts")
.ok_or(PdfError::Other("Type0 font missing DescendantFonts".into()))?;
let descendants_resolved = resolver.deref(descendants_obj)?;
let descendants = descendants_resolved
.as_array()
.ok_or(PdfError::Other("DescendantFonts is not an array".into()))?;
let cid_font_ref = descendants
.first()
.ok_or(PdfError::Other("DescendantFonts is empty".into()))?;
let cid_font_obj = resolver.deref(cid_font_ref)?;
let cid_font_dict = cid_font_obj
.as_dict()
.ok_or(PdfError::Other("CIDFont is not a dict".into()))?;
let cid_subtype = cid_font_dict.get_name(b"Subtype").unwrap_or(b"");
let descriptor = get_font_descriptor(cid_font_dict, resolver)?;
let desc = descriptor
.as_ref()
.ok_or(PdfError::Other("CIDFont missing FontDescriptor".into()))?;
let default_width = cid_font_dict.get_f64(b"DW").unwrap_or(1000.0) / 1000.0;
let dw2 = cid_font_dict
.get_array(b"DW2")
.and_then(|arr| {
let v: Vec<f64> = arr.iter().filter_map(|o| o.as_f64()).collect();
if v.len() >= 2 {
Some([v[0], v[1]])
} else {
None
}
})
.unwrap_or([880.0, -1000.0]);
let cid_widths = parse_cid_widths(cid_font_dict, resolver);
let w2 = parse_cid_w2(cid_font_dict, resolver);
let code_to_cid = if code_to_cid.is_empty()
&& code_lengths[0] == 2
&& encoding_name.windows(4).any(|w| w == b"UCS2")
{
let mut map = HashMap::new();
for unicode in 0x0020u32..=0x007Eu32 {
let cid = unicode - 0x001F;
map.insert(unicode, cid);
}
map
} else {
code_to_cid
};
let to_unicode = if let Some(tu_obj) = font_dict.get(b"ToUnicode") {
match resolver.stream_data_from_obj(tu_obj) {
Ok(data) => parse_to_unicode(&data),
Err(_) => HashMap::new(),
}
} else {
HashMap::new()
};
let ordering = {
let si_dict = cid_font_dict
.get_dict(b"CIDSystemInfo")
.cloned()
.or_else(|| {
cid_font_dict
.get(b"CIDSystemInfo")
.and_then(|obj| resolver.deref(obj).ok())
.and_then(|obj| obj.as_dict().cloned())
});
si_dict
.and_then(|d| {
d.get(b"Ordering").and_then(|v| match v {
PdfObj::Str(s) => Some(s.clone()),
PdfObj::Name(n) => Some(n.clone()),
_ => None,
})
})
.unwrap_or_default()
};
match cid_subtype {
b"CIDFontType2" => {
let mut substituted;
let mut data = if let Some(ff_ref) = desc
.get(b"FontFile2")
.or_else(|| {
desc.get(b"FontFile").filter(|obj| {
resolver
.stream_data_from_obj(obj)
.ok()
.is_some_and(|d| d.len() > 4 && d[..4] == [0, 1, 0, 0])
})
})
.or_else(|| {
desc.get(b"FontFile3").filter(|obj| {
resolver.stream_data_from_obj(obj).ok().is_some_and(|d| {
d.len() > 4
&& (d[..4] == [0, 1, 0, 0]
|| &d[..4] == b"true"
|| &d[..4] == b"OTTO")
})
})
}) {
substituted = false;
let mut font_data = resolver.stream_data_from_obj(ff_ref)?;
sanitize_index_to_loc_format(&mut font_data);
let is_otf_cff = font_data.len() > 4 && &font_data[0..4] == b"OTTO";
let is_raw = is_raw_cff(&font_data);
if is_otf_cff || is_raw {
let cid_to_gid_map = if let Some(map_obj) = cid_font_dict.get(b"CIDToGIDMap") {
if cid_font_dict.get_name(b"CIDToGIDMap") != Some(b"Identity") {
resolver.stream_data_from_obj(map_obj).ok().map(|d| {
d.chunks_exact(2)
.map(|p| u16::from_be_bytes([p[0], p[1]]))
.collect()
})
} else {
None
}
} else {
None
};
let is_cid_keyed = {
use stet_fonts::truetype::find_table;
let cff_range = if is_otf_cff {
find_table(&font_data, b"CFF ")
} else {
Some((0, font_data.len()))
};
cff_range
.and_then(|(off, len)| parse_cff(&font_data[off..off + len]).ok())
.and_then(|fonts| fonts.into_iter().next())
.is_some_and(|f| f.is_cid)
};
let (cid_to_gid_map, identity) = if is_cid_keyed {
(None, true) } else {
let id = cid_to_gid_map.is_none();
(cid_to_gid_map, id)
};
if is_otf_cff {
return create_cid_cff_from_otf(
&font_data,
default_width,
cid_widths,
&ordering,
cid_to_gid_map,
identity,
code_lengths,
code_to_cid.clone(),
wmode,
dw2,
w2.clone(),
);
} else {
return create_cid_cff_from_raw(
&font_data,
default_width,
cid_widths,
&ordering,
cid_to_gid_map,
identity,
code_lengths,
code_to_cid.clone(),
wmode,
dw2,
w2.clone(),
);
}
}
font_data
} else {
substituted = true;
let base_font = cid_font_dict
.get_name(b"BaseFont")
.map(|n| {
let s = String::from_utf8_lossy(n);
if s.len() > 7 && s.as_bytes().get(6) == Some(&b'+') {
s[7..].to_string()
} else {
s.to_string()
}
})
.unwrap_or_default();
let sys_data = load_system_truetype_font(&base_font)
.or_else(|_| load_cjk_fallback_font(&ordering, &base_font))?;
if sys_data.len() > 4 && &sys_data[0..4] == b"OTTO" {
return create_cid_cff_from_otf(
&sys_data,
default_width,
cid_widths,
&ordering,
None,
false, code_lengths,
code_to_cid.clone(),
wmode,
dw2,
w2.clone(),
);
}
sys_data
};
let has_cid_to_gid_map = cid_font_dict
.get(b"CIDToGIDMap")
.is_some_and(|v| v.as_name().is_none_or(|n| n != b"Identity"));
if !substituted && !cid_widths.is_empty() && !has_cid_to_gid_map {
let upm_f = get_units_per_em(&data) as f64;
let any_glyph = cid_widths
.keys()
.any(|&cid| skrifa_glyph_path(&data, cid, upm_f).is_some());
if !any_glyph {
let base_font = cid_font_dict
.get_name(b"BaseFont")
.map(|n| {
let s = String::from_utf8_lossy(n);
if s.len() > 7 && s.as_bytes().get(6) == Some(&b'+') {
s[7..].to_string()
} else {
s.to_string()
}
})
.unwrap_or_default();
if let Ok(sys_data) = load_system_truetype_font(&base_font) {
data = sys_data;
substituted = true;
}
}
}
let units_per_em = get_units_per_em(&data) as f64;
let cmap = parse_cmap(&data);
let (identity_cid_to_gid, cid_to_gid_map) =
if let Some(name) = cid_font_dict.get_name(b"CIDToGIDMap") {
(name == b"Identity", None)
} else if let Some(map_obj) = cid_font_dict.get(b"CIDToGIDMap") {
match resolver.stream_data_from_obj(map_obj) {
Ok(stream_data) => {
let mut gid_map = Vec::with_capacity(stream_data.len() / 2);
for pair in stream_data.chunks_exact(2) {
gid_map.push(u16::from_be_bytes([pair[0], pair[1]]));
}
(false, Some(gid_map))
}
Err(_) => (true, None), }
} else {
(true, None) };
let cid_to_gid_map = if substituted && cid_to_gid_map.is_some() {
None
} else {
cid_to_gid_map
};
let to_unicode = if substituted
&& identity_cid_to_gid
&& to_unicode.is_empty()
&& encoding_name.starts_with(b"Identity")
{
let base_name = cid_font_dict.get_name(b"BaseFont").unwrap_or(b"");
let name_str = String::from_utf8_lossy(base_name);
let clean = if name_str.len() > 7 && name_str.as_bytes().get(6) == Some(&b'+') {
&name_str[7..]
} else {
&name_str
};
let mut family = clean
.split(&[',', '-'][..])
.next()
.unwrap_or(clean)
.to_ascii_lowercase();
for suffix in &["psmt", "ps", "mt"] {
if family.len() > suffix.len() && family.ends_with(suffix) {
family.truncate(family.len() - suffix.len());
break;
}
}
super::gid_maps::get_gid_to_unicode_map(&family).unwrap_or(to_unicode)
} else {
to_unicode
};
Ok(PdfFont::CidTrueType(CidTrueTypePdfFont {
data,
default_width,
cid_widths,
cmap,
units_per_em,
identity_cid_to_gid,
substituted,
cid_to_gid_map,
to_unicode,
ordering: ordering.clone(),
ucs2_encoding,
code_lengths,
code_to_cid: code_to_cid.clone(),
wmode,
dw2,
w2: w2.clone(),
}))
}
b"CIDFontType0" => {
if let Some(ff_ref) = desc.get(b"FontFile3").or_else(|| desc.get(b"FontFile")) {
let font_data = resolver.stream_data_from_obj(ff_ref)?;
let is_truetype = font_data.len() > 4 && &font_data[0..4] == b"\x00\x01\x00\x00";
if is_truetype {
let mut font_data = font_data;
sanitize_index_to_loc_format(&mut font_data);
let units_per_em = get_units_per_em(&font_data) as f64;
let cmap = parse_cmap(&font_data);
let (identity_cid_to_gid, cid_to_gid_map) =
if let Some(name) = cid_font_dict.get_name(b"CIDToGIDMap") {
(name == b"Identity", None)
} else {
(true, None)
};
return Ok(PdfFont::CidTrueType(CidTrueTypePdfFont {
data: font_data,
default_width,
cid_widths,
cmap,
units_per_em,
identity_cid_to_gid,
substituted: false,
cid_to_gid_map,
to_unicode,
ordering: ordering.clone(),
ucs2_encoding,
code_lengths,
code_to_cid: code_to_cid.clone(),
wmode,
dw2,
w2: w2.clone(),
}));
}
if font_data.len() > 4 && &font_data[0..4] == b"OTTO" {
let pdf_cid_to_gid = if let Some(map_obj) = cid_font_dict.get(b"CIDToGIDMap") {
match resolver.stream_data_from_obj(map_obj) {
Ok(stream_data) => {
let mut gid_map = Vec::with_capacity(stream_data.len() / 2);
for pair in stream_data.chunks_exact(2) {
gid_map.push(u16::from_be_bytes([pair[0], pair[1]]));
}
Some(gid_map)
}
Err(_) => None,
}
} else {
None
};
let cff_is_cid = is_cff_cid_keyed(&font_data);
return create_cid_cff_from_otf(
&font_data,
default_width,
cid_widths,
&ordering,
pdf_cid_to_gid,
!cff_is_cid,
code_lengths,
code_to_cid.clone(),
wmode,
dw2,
w2.clone(),
);
}
if font_data.starts_with(b"%!")
&& font_data.windows(16).any(|w| w == b"Resource-CIDFont")
{
return create_cid_from_ps_cidfont(
&font_data,
default_width,
cid_widths,
code_lengths,
code_to_cid.clone(),
wmode,
dw2,
w2.clone(),
);
}
let is_type1 = font_data.starts_with(b"%!") || font_data.first() == Some(&0x80);
if is_type1 {
return create_cid_from_type1(
&font_data,
default_width,
cid_widths,
&to_unicode,
code_lengths,
code_to_cid.clone(),
wmode,
dw2,
w2.clone(),
);
}
let fonts = parse_cff(&font_data)
.map_err(|e| PdfError::Other(format!("CFF parse error: {e}")))?;
let font = fonts
.into_iter()
.next()
.ok_or(PdfError::Other("CFF contains no fonts".into()))?;
let cs_count = font.char_strings.len();
let is_adobe_cjk_registry = matches!(
ordering.as_slice(),
b"GB1" | b"CNS1" | b"Japan1" | b"Japan2" | b"Korea1" | b"KR"
);
if cs_count > 0 && cid_widths.len() > cs_count * 4 && is_adobe_cjk_registry {
} else {
let fm = font.font_matrix;
let font_matrix = Matrix::new(fm[0], fm[1], fm[2], fm[3], fm[4], fm[5]);
return Ok(PdfFont::CidCff(CidCffPdfFont {
font,
default_width,
cid_widths,
font_matrix,
cmap: None,
pdf_cid_to_gid: None,
identity_cid_to_gid: false,
ordering: ordering.clone(),
code_lengths,
code_to_cid: code_to_cid.clone(),
wmode,
dw2,
w2: w2.clone(),
type1_paths: None,
}));
}
}
{
let base_font = cid_font_dict
.get_name(b"BaseFont")
.map(|n| String::from_utf8_lossy(n).to_string())
.unwrap_or_default();
let sys_data = if ucs2_encoding {
load_system_truetype_font(&base_font)
.or_else(|_| load_cjk_fallback_font(&ordering, &base_font))
.or_else(|_| load_system_truetype_font("DejaVuSans"))
.or_else(|_| load_system_truetype_font("LiberationSans"))
.or_else(|_| load_system_truetype_font("NimbusSans"))?
} else {
load_system_truetype_font(&base_font)
.or_else(|_| load_cjk_fallback_font(&ordering, &base_font))?
};
let identity = ordering == b"Identity";
let is_otto = sys_data.len() > 4 && &sys_data[0..4] == b"OTTO";
let is_ttc_cff = sys_data.len() > 16 && &sys_data[0..4] == b"ttcf" && {
let off = u32::from_be_bytes([
sys_data[12],
sys_data[13],
sys_data[14],
sys_data[15],
]) as usize;
off + 4 <= sys_data.len() && &sys_data[off..off + 4] == b"OTTO"
};
if is_otto || is_ttc_cff {
return create_cid_cff_from_otf(
&sys_data,
default_width,
cid_widths,
&ordering,
None,
identity,
code_lengths,
code_to_cid.clone(),
wmode,
dw2,
w2.clone(),
);
}
let data = sys_data;
let units_per_em = get_units_per_em(&data) as f64;
let cmap = parse_cmap(&data);
Ok(PdfFont::CidTrueType(CidTrueTypePdfFont {
data,
default_width,
cid_widths,
cmap,
units_per_em,
identity_cid_to_gid: false,
substituted: true,
cid_to_gid_map: None,
to_unicode,
ordering: ordering.clone(),
ucs2_encoding,
code_lengths,
code_to_cid: code_to_cid.clone(),
wmode,
dw2,
w2,
}))
}
}
_ => Err(PdfError::Other(format!(
"Unsupported CIDFont subtype: {}",
String::from_utf8_lossy(cid_subtype)
))),
}
}
fn extract_hex_tokens(s: &str) -> Vec<&str> {
let mut tokens = Vec::new();
let mut rest = s;
while let Some(start) = rest.find('<') {
rest = &rest[start + 1..];
if let Some(end) = rest.find('>') {
let hex = rest[..end].trim();
if !hex.is_empty() {
tokens.push(hex);
}
rest = &rest[end + 1..];
} else {
break;
}
}
tokens
}
fn hex_to_unicode(hex: &str) -> Option<u32> {
if hex.len() <= 4 {
u32::from_str_radix(hex, 16).ok()
} else {
match hex {
"00660066" => Some(0xFB00), "00660069" => Some(0xFB01), "0066006C" => Some(0xFB02), "006600660069" => Some(0xFB03), "00660066006C" => Some(0xFB04), "017F0074" => Some(0xFB05), "00730074" => Some(0xFB06), _ => {
u32::from_str_radix(&hex[..hex.len().min(4)], 16).ok()
}
}
}
}
fn parse_to_unicode(data: &[u8]) -> HashMap<u16, u32> {
let mut map = HashMap::new();
let text = String::from_utf8_lossy(data);
let mut in_bfchar = false;
let mut in_bfrange = false;
let mut range_tokens: Vec<&str> = Vec::new();
for line in text.lines() {
let trimmed = line.trim();
if trimmed.ends_with("beginbfchar") {
in_bfchar = true;
continue;
}
if trimmed == "endbfchar" {
in_bfchar = false;
continue;
}
if trimmed.ends_with("beginbfrange") {
in_bfrange = true;
range_tokens.clear();
continue;
}
if trimmed == "endbfrange" {
in_bfrange = false;
range_tokens.clear();
continue;
}
if in_bfchar {
let tokens = extract_hex_tokens(trimmed);
if tokens.len() >= 2
&& let Ok(cid) = u32::from_str_radix(tokens[0], 16)
&& let Some(unicode) = hex_to_unicode(tokens[1])
{
map.insert(cid as u16, unicode);
}
}
if in_bfrange {
let line_tokens = extract_hex_tokens(trimmed);
if trimmed.contains('[') {
let all_before_bracket: Vec<&str> = {
let before = trimmed.split('[').next().unwrap_or("");
extract_hex_tokens(before)
};
let in_bracket = {
let after_open = trimmed.split('[').nth(1).unwrap_or("");
let before_close = after_open.split(']').next().unwrap_or(after_open);
extract_hex_tokens(before_close)
};
if all_before_bracket.len() >= 2
&& let (Some(start), Some(end)) = (
u32::from_str_radix(all_before_bracket[0], 16).ok(),
u32::from_str_radix(all_before_bracket[1], 16).ok(),
)
{
for (j, cid) in (start..=end).enumerate() {
if j < in_bracket.len()
&& let Some(u) = hex_to_unicode(in_bracket[j])
{
map.insert(cid as u16, u);
}
}
}
} else if line_tokens.len() >= 3 {
if let (Some(start), Some(end), Some(mut dst)) = (
u32::from_str_radix(line_tokens[0], 16).ok(),
u32::from_str_radix(line_tokens[1], 16).ok(),
hex_to_unicode(line_tokens[2]),
) {
for cid in start..=end {
map.insert(cid as u16, dst);
dst += 1;
}
}
}
}
}
map
}
fn parse_cid_widths(cid_font_dict: &PdfDict, resolver: &Resolver) -> HashMap<u16, f64> {
let mut widths = HashMap::new();
let w_obj = match cid_font_dict.get(b"W") {
Some(obj) => match resolver.deref(obj) {
Ok(resolved) => resolved,
Err(_) => return widths,
},
None => return widths,
};
let w_arr = match w_obj.as_array() {
Some(arr) => arr,
None => return widths,
};
let mut i = 0;
while i < w_arr.len() {
let first_cid = match &w_arr[i] {
PdfObj::Int(n) => *n as u16,
_ => break,
};
i += 1;
if i >= w_arr.len() {
break;
}
let next = resolver.deref(&w_arr[i]).unwrap_or(w_arr[i].clone());
match &next {
PdfObj::Array(arr) => {
for (j, w_obj) in arr.iter().enumerate() {
let w_val = w_obj
.as_f64()
.or_else(|| resolver.deref(w_obj).ok().and_then(|r| r.as_f64()))
.unwrap_or(0.0);
widths.insert(first_cid + j as u16, w_val / 1000.0);
}
i += 1;
}
_ => {
let last_cid = match &next {
PdfObj::Int(n) => *n as u16,
_ => first_cid,
};
i += 1;
let w = if i < w_arr.len() {
let obj = &w_arr[i];
obj.as_f64()
.or_else(|| resolver.deref(obj).ok().and_then(|r| r.as_f64()))
.unwrap_or(0.0)
/ 1000.0
} else {
0.0
};
i += 1;
for cid in first_cid..=last_cid {
widths.insert(cid, w);
}
}
}
}
widths
}
fn parse_cid_w2(cid_font_dict: &PdfDict, resolver: &Resolver) -> HashMap<u16, [f64; 3]> {
let mut metrics = HashMap::new();
let w2_obj = match cid_font_dict.get(b"W2") {
Some(obj) => match resolver.deref(obj) {
Ok(resolved) => resolved,
Err(_) => return metrics,
},
None => return metrics,
};
let arr = match w2_obj.as_array() {
Some(a) => a,
None => return metrics,
};
let mut i = 0;
while i < arr.len() {
let first_cid = match &arr[i] {
PdfObj::Int(n) => *n as u16,
_ => break,
};
i += 1;
if i >= arr.len() {
break;
}
let next = resolver.deref(&arr[i]).unwrap_or(arr[i].clone());
match &next {
PdfObj::Array(sub) => {
let vals: Vec<f64> = sub.iter().filter_map(|o| o.as_f64()).collect();
for (j, chunk) in vals.chunks(3).enumerate() {
if chunk.len() == 3 {
metrics.insert(first_cid + j as u16, [chunk[0], chunk[1], chunk[2]]);
}
}
i += 1;
}
_ => {
let last_cid = match &next {
PdfObj::Int(n) => *n as u16,
_ => first_cid,
};
i += 1;
if i + 2 < arr.len() {
let w1 = arr[i].as_f64().unwrap_or(-1000.0);
let vx = arr[i + 1].as_f64().unwrap_or(0.0);
let vy = arr[i + 2].as_f64().unwrap_or(880.0);
i += 3;
for cid in first_cid..=last_cid {
metrics.insert(cid, [w1, vx, vy]);
}
} else {
break;
}
}
}
}
metrics
}
fn strip_pfb(data: &[u8]) -> Vec<u8> {
if data.len() < 2 || data[0] != 0x80 {
return data.to_vec();
}
let mut result = Vec::with_capacity(data.len());
let mut pos = 0;
while pos + 6 <= data.len() && data[pos] == 0x80 {
let segment_type = data[pos + 1];
if segment_type == 3 {
break; }
let len = u32::from_le_bytes([data[pos + 2], data[pos + 3], data[pos + 4], data[pos + 5]])
as usize;
pos += 6;
let end = (pos + len).min(data.len());
result.extend_from_slice(&data[pos..end]);
pos = end;
}
result
}
impl PdfFont {
pub fn glyph_path(&self, char_code: u8) -> Option<PsPath> {
match self {
PdfFont::Type1(f) => f.glyph_path(char_code),
PdfFont::TrueType(f) => f.glyph_path(char_code),
PdfFont::Cff(f) => f.glyph_path(char_code),
PdfFont::CidTrueType(f) => f.glyph_path_cid(char_code as u16),
PdfFont::CidCff(f) => f.glyph_path_cid(char_code as u16),
PdfFont::Type3(_) => None,
}
}
pub fn glyph_path_cid(&self, cid: u16) -> Option<PsPath> {
match self {
PdfFont::CidTrueType(f) => f.glyph_path_cid(cid),
PdfFont::CidCff(f) => f.glyph_path_cid(cid),
_ => self.glyph_path(cid as u8),
}
}
pub fn glyph_path_unicode(&self, unicode: u16) -> Option<PsPath> {
match self {
PdfFont::CidTrueType(f) => f.glyph_path_unicode(unicode),
PdfFont::CidCff(f) => f.glyph_path_unicode(unicode),
_ => None,
}
}
pub fn glyph_width_unicode(&self, unicode: u16) -> f64 {
match self {
PdfFont::CidTrueType(f) => f.glyph_width_unicode(unicode),
_ => 0.0,
}
}
pub fn glyph_width(&self, char_code: u8) -> f64 {
match self {
PdfFont::Type1(f) => f.widths[char_code as usize],
PdfFont::TrueType(f) => f.widths[char_code as usize],
PdfFont::Cff(f) => f.widths[char_code as usize],
PdfFont::CidTrueType(f) => f.glyph_width_cid(char_code as u16),
PdfFont::CidCff(f) => f.glyph_width_cid(char_code as u16),
PdfFont::Type3(f) => f.widths[char_code as usize],
}
}
pub fn glyph_width_cid(&self, cid: u16) -> f64 {
match self {
PdfFont::CidTrueType(f) => f.glyph_width_cid(cid),
PdfFont::CidCff(f) => f.glyph_width_cid(cid),
_ => self.glyph_width(cid as u8),
}
}
pub fn font_matrix(&self) -> Matrix {
match self {
PdfFont::Type1(f) => f.font_matrix,
PdfFont::TrueType(_) | PdfFont::CidTrueType(_) => Matrix::identity(),
PdfFont::Cff(f) => f.font_matrix,
PdfFont::CidCff(_) => Matrix::identity(),
PdfFont::Type3(f) => f.font_matrix,
}
}
pub fn is_composite(&self) -> bool {
matches!(self, PdfFont::CidTrueType(_) | PdfFont::CidCff(_))
}
pub fn wmode(&self) -> u8 {
match self {
PdfFont::CidTrueType(f) => f.wmode,
PdfFont::CidCff(f) => f.wmode,
_ => 0,
}
}
pub fn dw2(&self) -> [f64; 2] {
match self {
PdfFont::CidTrueType(f) => f.dw2,
PdfFont::CidCff(f) => f.dw2,
_ => [880.0, -1000.0],
}
}
pub fn vertical_metrics_cid(&self, cid: u16) -> [f64; 3] {
match self {
PdfFont::CidTrueType(f) => {
if let Some(&m) = f.w2.get(&cid) {
m
} else {
let w0 = f.cid_widths.get(&cid).copied().unwrap_or(f.default_width) * 1000.0;
[f.dw2[1], w0 / 2.0, f.dw2[0]]
}
}
PdfFont::CidCff(f) => {
if let Some(&m) = f.w2.get(&cid) {
m
} else {
let w0 = f.cid_widths.get(&cid).copied().unwrap_or(f.default_width) * 1000.0;
[f.dw2[1], w0 / 2.0, f.dw2[0]]
}
}
_ => [-1000.0, 500.0, 880.0],
}
}
pub fn has_cid_glyph(&self, cid: u16) -> bool {
match self {
PdfFont::CidTrueType(f) => f.has_glyph(cid),
PdfFont::CidCff(_) => true, _ => false,
}
}
pub fn resolve_code_to_cid(&self, code: u32) -> u32 {
match self {
PdfFont::CidTrueType(f) => f.code_to_cid.get(&code).copied().unwrap_or(code),
PdfFont::CidCff(f) => f.code_to_cid.get(&code).copied().unwrap_or(code),
_ => code,
}
}
pub fn code_width(&self, first_byte: u8) -> usize {
match self {
PdfFont::CidTrueType(f) => {
let w = f.code_lengths[first_byte as usize];
if w == 0 { 2 } else { w as usize }
}
PdfFont::CidCff(f) => {
let w = f.code_lengths[first_byte as usize];
if w == 0 { 2 } else { w as usize }
}
_ => 1,
}
}
pub fn is_type3(&self) -> bool {
matches!(self, PdfFont::Type3(_))
}
pub fn type3_char_proc(&self, char_code: u8) -> Option<&[u8]> {
match self {
PdfFont::Type3(f) => f.char_procs.get(&char_code).map(|v| v.as_slice()),
_ => None,
}
}
pub fn type3_resources(&self) -> Option<&PdfDict> {
match self {
PdfFont::Type3(f) => Some(&f.resources),
_ => None,
}
}
}
impl Type1PdfFont {
fn glyph_path(&self, char_code: u8) -> Option<PsPath> {
let glyph_name = self.encoding[char_code as usize].as_deref();
let charstring = glyph_name
.and_then(|name| self.font.charstrings.get(name))
.or_else(|| {
if !self.builtin_fallback {
return None;
}
let builtin = self.font.encoding.get(char_code as usize)?;
if builtin != ".notdef" && glyph_name.map_or(true, |n| n != builtin) {
self.font.charstrings.get(builtin.as_str())
} else {
None
}
})?;
let cs_lookup =
|name: &str| -> Option<Vec<u8>> { self.font.charstrings.get(name).cloned() };
let result = execute_charstring_mm(
charstring,
&self.font.subrs,
self.font.len_iv,
false,
Some(&cs_lookup),
self.weight_vector.as_deref(),
)
.ok()?;
if self.per_char_width_scale {
let pdf_w = self.widths[char_code as usize];
let font_w = result.width_x * self.font_matrix.a;
if font_w.abs() > 0.001 && pdf_w > 0.001 && (pdf_w / font_w - 1.0).abs() > 0.01 {
return Some(result.path.transform(&Matrix::scale(pdf_w / font_w, 1.0)));
}
}
Some(result.path)
}
}
impl TrueTypePdfFont {
fn detect_gid_hex(encoding: &[Option<String>; 256]) -> bool {
encoding.iter().any(|name| {
if let Some(n) = name {
n.starts_with('g')
&& n.len() > 1
&& n[1..].bytes().all(|b| b.is_ascii_hexdigit())
&& n[1..]
.bytes()
.any(|b| b.is_ascii_hexdigit() && !b.is_ascii_digit())
} else {
false
}
})
}
fn glyph_path(&self, char_code: u8) -> Option<PsPath> {
let gid = self.char_code_to_gid(char_code);
let gid = gid?;
let path = skrifa_glyph_path(&self.data, gid, self.units_per_em).or_else(|| {
let glyf_data = get_glyf_data(&self.data, gid)?;
let data_ref = &self.data;
let p = parse_glyf_to_path(&glyf_data, &|cid| get_glyf_data(data_ref, cid));
if p.is_empty() { None } else { Some(p) }
})?;
let scale = 1.0 / self.units_per_em;
let m = Matrix::scale(scale, scale);
Some(path.transform(&m))
}
fn char_code_to_gid(&self, char_code: u8) -> Option<u16> {
if self.identity_gid {
if let Some(&gid) = self.cmap.get(&(char_code as u32)) {
return Some(gid);
}
if let Some(&gid) = self.cmap.get(&(0xF000 + char_code as u32)) {
return Some(gid);
}
return Some(char_code as u16);
}
if let Some(glyph_name) = &self.encoding[char_code as usize] {
if self.cmap_is_unicode {
if let Some(unicode) = stet_fonts::agl::glyph_name_to_unicode(glyph_name)
&& let Some(&gid) = self.cmap.get(&(unicode as u32))
{
return Some(gid);
}
}
}
if self.cmap_is_unicode {
if let Some(&unicode) = self.to_unicode.get(&(char_code as u16))
&& let Some(&gid) = self.cmap.get(&unicode)
{
return Some(gid);
}
}
if let Some(glyph_name) = &self.encoding[char_code as usize] {
if glyph_name.starts_with('g')
&& glyph_name.len() > 1
&& glyph_name[1..].bytes().all(|b| b.is_ascii_hexdigit())
{
let suffix = &glyph_name[1..];
let gid = if self.gid_hex {
u16::from_str_radix(suffix, 16).ok()
} else {
suffix.parse::<u16>().ok()
};
if let Some(gid) = gid {
return Some(gid);
}
}
}
if let Some(&gid) = self.cmap.get(&(char_code as u32)) {
return Some(gid);
}
if let Some(glyph_name) = &self.encoding[char_code as usize] {
if let Some(&gid) = self.post_name_to_gid.get(glyph_name.as_str()) {
return Some(gid);
}
}
if let Some(&gid) = self.cmap.get(&(0xF000 + char_code as u32)) {
return Some(gid);
}
if let Some(&unicode) = self.to_unicode.get(&(char_code as u16)) {
if let Some(name) = stet_fonts::system_fonts::unicode_to_glyph_name(unicode) {
if let Some(&gid) = self.post_name_to_gid.get(name) {
return Some(gid);
}
}
if let Ok(font_ref) = skrifa::FontRef::new(&self.data) {
let charmap = font_ref.charmap();
if let Some(gid) = charmap.map(unicode) {
return Some(gid.to_u32() as u16);
}
}
if !self.cmap.is_empty() {
use stet_fonts::truetype::{find_table, read_u16};
let mapped: std::collections::HashSet<u16> = self.cmap.values().copied().collect();
let num_glyphs = find_table(&self.data, b"maxp")
.map(|(off, _)| read_u16(&self.data, off + 4))
.unwrap_or(0);
for gid in 0..num_glyphs {
if mapped.contains(&gid) {
continue;
}
if let Some(glyf_data) = get_glyf_data(&self.data, gid) {
if glyf_data.len() >= 2 {
let num_contours = stet_fonts::truetype::read_i16(&glyf_data, 0);
if num_contours < 0 {
return Some(gid);
}
}
}
}
}
}
if self.cmap.is_empty() {
Some(char_code as u16)
} else {
None
}
}
}
impl CidTrueTypePdfFont {
fn resolve_cid(&self, code: u16) -> u16 {
if self.ucs2_encoding && !self.ordering.is_empty() && self.code_to_cid.is_empty() {
super::cid_unicode::unicode_to_cid(&self.ordering, code as u32).unwrap_or(code)
} else {
code
}
}
fn glyph_path_cid(&self, cid: u16) -> Option<PsPath> {
if std::env::var("STET_DEBUG_TEXT").is_ok() {
eprintln!(
"[cid_tt] cid={} sub={} ordering={} identity={} to_unicode={} cmap={} cid_to_gid_map={}",
cid,
self.substituted,
String::from_utf8_lossy(&self.ordering),
self.identity_cid_to_gid,
!self.to_unicode.is_empty(),
!self.cmap.is_empty(),
self.cid_to_gid_map.is_some()
);
}
let gid = if self.ucs2_encoding && !self.cmap.is_empty() && self.code_to_cid.is_empty() {
if let Some(&g) = self.cmap.get(&(cid as u32)) {
g
} else {
return None;
}
} else if self.ucs2_encoding && self.substituted && !self.ordering.is_empty() {
let unicode = super::cid_unicode::cid_to_unicode(&self.ordering, cid)?;
*self.cmap.get(&unicode)?
} else if let Some(ref map) = self.cid_to_gid_map {
*map.get(cid as usize).unwrap_or(&0)
} else if self.substituted && !self.to_unicode.is_empty() {
if let Some(&unicode) = self.to_unicode.get(&cid) {
*self.cmap.get(&unicode)?
} else {
cid
}
} else if self.substituted && !self.ordering.is_empty() && self.ordering != b"Identity" {
let unicode = super::cid_unicode::cid_to_unicode(&self.ordering, cid)?;
*self.cmap.get(&unicode)?
} else if self.identity_cid_to_gid {
cid
} else if self.substituted && !self.cmap.is_empty() {
if let Some(&g) = self.cmap.get(&(cid as u32)) {
g
} else {
cid
}
} else if !self.cmap.is_empty() {
*self.cmap.get(&(cid as u32))?
} else {
cid
};
let path = skrifa_glyph_path(&self.data, gid, self.units_per_em).or_else(|| {
let glyf_data = get_glyf_data(&self.data, gid)?;
let data_ref = &self.data;
let p = parse_glyf_to_path(&glyf_data, &|cid| get_glyf_data(data_ref, cid));
if p.is_empty() || p.segments.len() > 10_000 {
None
} else {
Some(p)
}
});
let path = path?;
let scale = 1.0 / self.units_per_em;
let m = if self.substituted {
let pdf_w = self.cid_widths.get(&cid).copied();
let font_w =
hmtx_advance_width(&self.data, gid, self.units_per_em).unwrap_or(0.0) / 1000.0;
if let Some(pw) = pdf_w {
if font_w > 0.001 && pw > 0.001 {
Matrix::new(scale * pw / font_w, 0.0, 0.0, scale, 0.0, 0.0)
} else {
Matrix::scale(scale, scale)
}
} else {
Matrix::scale(scale, scale)
}
} else {
Matrix::scale(scale, scale)
};
Some(path.transform(&m))
}
fn has_glyph(&self, cid: u16) -> bool {
if self.cid_widths.contains_key(&cid) {
return true;
}
let gid = if let Some(ref map) = self.cid_to_gid_map {
*map.get(cid as usize).unwrap_or(&0)
} else if self.substituted && !self.to_unicode.is_empty() {
if let Some(&unicode) = self.to_unicode.get(&cid) {
if let Some(&g) = self.cmap.get(&unicode) {
g
} else {
return false;
}
} else {
return false;
}
} else if self.identity_cid_to_gid {
cid
} else {
return true; };
let num_glyphs = stet_fonts::truetype::get_num_glyphs(&self.data);
(gid as u32) < num_glyphs
}
fn glyph_width_cid(&self, cid: u16) -> f64 {
let resolved = self.resolve_cid(cid);
if let Some(&w) = self.cid_widths.get(&resolved) {
return w;
}
if self.substituted && !self.to_unicode.is_empty() {
if let Some(&unicode) = self.to_unicode.get(&cid) {
if let Some(&gid) = self.cmap.get(&unicode) {
if let Some(w) = hmtx_advance_width(&self.data, gid, self.units_per_em) {
return w / 1000.0;
}
}
}
}
self.default_width
}
fn glyph_path_unicode(&self, unicode: u16) -> Option<PsPath> {
let &gid = self.cmap.get(&(unicode as u32))?;
let path = skrifa_glyph_path(&self.data, gid, self.units_per_em)?;
let scale = 1.0 / self.units_per_em;
let m = Matrix::scale(scale, scale);
Some(path.transform(&m))
}
fn glyph_width_unicode(&self, unicode: u16) -> f64 {
if let Some(&gid) = self.cmap.get(&(unicode as u32)) {
hmtx_advance_width(&self.data, gid, self.units_per_em)
.map(|w| w / 1000.0)
.unwrap_or(self.default_width)
} else {
self.default_width
}
}
}
impl CidCffPdfFont {
fn glyph_path_at_gid(&self, gid: usize) -> Option<PsPath> {
if gid >= self.font.char_strings.len() {
return None;
}
let (default_width_x, nominal_width_x, local_subrs, fd_font_matrix) = if self.font.is_cid
&& !self.font.fd_select.is_empty()
&& !self.font.fd_array.is_empty()
{
let fd_idx = *self.font.fd_select.get(gid).unwrap_or(&0) as usize;
if let Some(fd) = self.font.fd_array.get(fd_idx) {
(
fd.default_width_x,
fd.nominal_width_x,
&fd.local_subrs,
fd.font_matrix,
)
} else {
(
self.font.default_width_x,
self.font.nominal_width_x,
&self.font.local_subrs,
None,
)
}
} else {
(
self.font.default_width_x,
self.font.nominal_width_x,
&self.font.local_subrs,
None,
)
};
let result = execute_type2_charstring(
&self.font.char_strings[gid],
local_subrs,
&self.font.global_subrs,
default_width_x,
nominal_width_x,
false,
)
.ok()?;
let effective_fm = if let Some(fd_fm) = fd_font_matrix {
let fd = Matrix::new(fd_fm[0], fd_fm[1], fd_fm[2], fd_fm[3], fd_fm[4], fd_fm[5]);
if fd.a.abs() < 0.01 || fd.d.abs() < 0.01 {
fd
} else {
self.font_matrix.concat(&fd)
}
} else {
self.font_matrix
};
Some(result.path.transform(&effective_fm))
}
fn glyph_path_unicode(&self, unicode: u16) -> Option<PsPath> {
let cmap = self.cmap.as_ref()?;
let &gid = cmap.get(&(unicode as u32))?;
self.glyph_path_at_gid(gid as usize)
}
fn glyph_path_cid(&self, cid: u16) -> Option<PsPath> {
if let Some(ref paths) = self.type1_paths {
return paths.get(&cid).cloned();
}
let gid = if let Some(ref map) = self.pdf_cid_to_gid {
*map.get(cid as usize).unwrap_or(&0) as usize
} else if self.identity_cid_to_gid {
cid as usize
} else if let Some(ref cmap) = self.cmap {
if !self.ordering.is_empty() && self.ordering != b"Identity" {
let unicode = super::cid_unicode::cid_to_unicode(&self.ordering, cid)?;
let gid_opt = cjk_fullwidth_alternative(unicode)
.and_then(|alt| cmap.get(&alt))
.or_else(|| cmap.get(&unicode));
*gid_opt? as usize
} else {
*cmap.get(&(cid as u32))? as usize
}
} else if !self.font.cid_to_gid.is_empty() {
let g = *self.font.cid_to_gid.get(cid as usize)?;
if g == 0xFFFF {
return None;
}
g as usize
} else {
cid as usize
};
self.glyph_path_at_gid(gid)
}
fn glyph_width_cid(&self, cid: u16) -> f64 {
self.cid_widths
.get(&cid)
.copied()
.unwrap_or(self.default_width)
}
}
impl CffPdfFont {
fn glyph_path(&self, char_code: u8) -> Option<PsPath> {
let glyph_name = self.encoding[char_code as usize].as_deref()?;
let gid = self
.font
.charset
.iter()
.position(|name| name == glyph_name)
.or_else(|| {
let cff_gid = self
.font
.encoding
.get(char_code as usize)
.copied()
.unwrap_or(0) as usize;
if cff_gid > 0 && cff_gid < self.font.char_strings.len() {
Some(cff_gid)
} else {
None
}
});
let gid = gid?;
if gid >= self.font.char_strings.len() {
return None;
}
let result = execute_type2_charstring(
&self.font.char_strings[gid],
&self.font.local_subrs,
&self.font.global_subrs,
self.font.default_width_x,
self.font.nominal_width_x,
false,
)
.ok()?;
if let Some((adx, ady, bchar, achar)) = result.seac {
return self.compose_seac(adx, ady, bchar, achar);
}
Some(result.path)
}
fn compose_seac(&self, adx: f64, ady: f64, bchar: u8, achar: u8) -> Option<PsPath> {
use stet_fonts::encoding::STANDARD_ENCODING;
let base_name = STANDARD_ENCODING.get(bchar as usize).copied().unwrap_or("");
let accent_name = STANDARD_ENCODING.get(achar as usize).copied().unwrap_or("");
let base_gid = self.font.charset.iter().position(|n| n == base_name)?;
let accent_gid = self.font.charset.iter().position(|n| n == accent_name)?;
let base_result = execute_type2_charstring(
&self.font.char_strings[base_gid],
&self.font.local_subrs,
&self.font.global_subrs,
self.font.default_width_x,
self.font.nominal_width_x,
false,
)
.ok()?;
let accent_result = execute_type2_charstring(
&self.font.char_strings[accent_gid],
&self.font.local_subrs,
&self.font.global_subrs,
self.font.default_width_x,
self.font.nominal_width_x,
false,
)
.ok()?;
let mut combined = base_result.path;
let offset = Matrix::translate(adx, ady);
let shifted_accent = accent_result.path.transform(&offset);
combined
.segments
.extend_from_slice(&shifted_accent.segments);
Some(combined)
}
}
struct PsPathPen {
path: PsPath,
cur_x: f64,
cur_y: f64,
}
impl skrifa::outline::OutlinePen for PsPathPen {
fn move_to(&mut self, x: f32, y: f32) {
self.cur_x = x as f64;
self.cur_y = y as f64;
self.path
.segments
.push(PathSegment::MoveTo(self.cur_x, self.cur_y));
}
fn line_to(&mut self, x: f32, y: f32) {
self.cur_x = x as f64;
self.cur_y = y as f64;
self.path
.segments
.push(PathSegment::LineTo(self.cur_x, self.cur_y));
}
fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
let cx = cx as f64;
let cy = cy as f64;
let ex = x as f64;
let ey = y as f64;
let cp1x = self.cur_x + 2.0 / 3.0 * (cx - self.cur_x);
let cp1y = self.cur_y + 2.0 / 3.0 * (cy - self.cur_y);
let cp2x = ex + 2.0 / 3.0 * (cx - ex);
let cp2y = ey + 2.0 / 3.0 * (cy - ey);
self.cur_x = ex;
self.cur_y = ey;
self.path.segments.push(PathSegment::CurveTo {
x1: cp1x,
y1: cp1y,
x2: cp2x,
y2: cp2y,
x3: ex,
y3: ey,
});
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.cur_x = x as f64;
self.cur_y = y as f64;
self.path.segments.push(PathSegment::CurveTo {
x1: cx0 as f64,
y1: cy0 as f64,
x2: cx1 as f64,
y2: cy1 as f64,
x3: self.cur_x,
y3: self.cur_y,
});
}
fn close(&mut self) {
self.path.segments.push(PathSegment::ClosePath);
}
}
pub(crate) fn winansi_byte_to_unicode(byte: u8) -> u16 {
match byte {
0x80 => 0x20AC, 0x82 => 0x201A, 0x83 => 0x0192, 0x84 => 0x201E, 0x85 => 0x2026, 0x86 => 0x2020, 0x87 => 0x2021, 0x88 => 0x02C6, 0x89 => 0x2030, 0x8A => 0x0160, 0x8B => 0x2039, 0x8C => 0x0152, 0x8E => 0x017D, 0x91 => 0x2018, 0x92 => 0x2019, 0x93 => 0x201C, 0x94 => 0x201D, 0x95 => 0x2022, 0x96 => 0x2013, 0x97 => 0x2014, 0x98 => 0x02DC, 0x99 => 0x2122, 0x9A => 0x0161, 0x9B => 0x203A, 0x9C => 0x0153, 0x9E => 0x017E, 0x9F => 0x0178, _ => byte as u16,
}
}
fn hmtx_advance_width(font_data: &[u8], gid: u16, units_per_em: f64) -> Option<f64> {
use stet_fonts::truetype::{find_table, read_u16};
let (hhea_off, _) = find_table(font_data, b"hhea")?;
let (hmtx_off, _) = find_table(font_data, b"hmtx")?;
if hhea_off + 36 > font_data.len() {
return None;
}
let num_h_metrics = read_u16(font_data, hhea_off + 34) as usize;
let gid = gid as usize;
let advance = if gid < num_h_metrics {
let offset = hmtx_off + gid * 4;
if offset + 2 > font_data.len() {
return None;
}
read_u16(font_data, offset)
} else {
if num_h_metrics == 0 {
return None;
}
let offset = hmtx_off + (num_h_metrics - 1) * 4;
if offset + 2 > font_data.len() {
return None;
}
read_u16(font_data, offset)
};
Some(advance as f64 / units_per_em * 1000.0)
}
fn skrifa_glyph_path(font_data: &[u8], gid: u16, units_per_em: f64) -> Option<PsPath> {
let font_ref = skrifa::FontRef::from_index(font_data, 0).ok()?;
let outlines = font_ref.outline_glyphs();
let glyph = outlines.get(skrifa::GlyphId::new(gid as u32))?;
let hinting = skrifa::outline::HintingInstance::new(
&outlines,
skrifa::prelude::Size::new(units_per_em as f32),
skrifa::instance::LocationRef::default(),
skrifa::outline::HintingOptions {
engine: skrifa::outline::Engine::Interpreter,
target: skrifa::outline::Target::Mono,
},
)
.ok();
let mut pen = PsPathPen {
path: PsPath::new(),
cur_x: 0.0,
cur_y: 0.0,
};
let result = if let Some(ref instance) = hinting {
glyph.draw(instance, &mut pen)
} else {
glyph.draw(
skrifa::outline::DrawSettings::unhinted(
skrifa::prelude::Size::new(units_per_em as f32),
skrifa::instance::LocationRef::default(),
),
&mut pen,
)
};
result.ok()?;
if pen.path.is_empty() {
None
} else {
Some(pen.path)
}
}