mod glyph;
mod gsub;
mod transform;
pub use transform::{CidTransform, cid_transform_to_float, japan1_transform};
use crate::descriptor::{self, FontDescriptor};
use crate::glyphs::{Charmap, Face, GlyphSource};
use crate::subst::{self, CodePage, FontRequest, SubstFont, SubstitutionOptions};
use crate::tounicode::ToUnicode;
use crate::widths::CidWidths;
use crate::{CharCode, CharItem, Cid, Error, FontCache, FontId, Gid, names, widths};
use pdfrum_cmap::{CMap, CidCoding, CidSet};
use pdfrum_common::kurbo::Rect;
use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
use pdfrum_object::{Dict, Object, Resolve};
use smallvec::SmallVec;
pub use crate::widths::VerticalMetrics;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CidToGid {
Identity,
Stream(Box<[u8]>),
ViaCharmap,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CidFontKind {
Type1,
TrueType,
}
#[derive(Debug)]
pub struct Type0Font {
pub(crate) id: FontId,
pub(crate) cmap: CMap,
pub(crate) glyphs: GlyphSource,
pub(crate) charset: CidSet,
pub(crate) cid_to_gid: CidToGid,
pub(crate) widths: CidWidths,
pub(crate) vertical: Option<VerticalMetrics>,
pub(crate) to_unicode: Option<ToUnicode>,
pub(crate) descriptor: FontDescriptor,
pub(crate) subst: Option<SubstFont>,
pub(crate) kind: CidFontKind,
pub(crate) embedded: bool,
pub(crate) base_font_name: Vec<u8>,
pub(crate) adobe_courier_std: bool,
#[cfg(test)]
pub(crate) ansi_widths_fixed: bool,
gsub: gsub::VerticalSubst,
}
impl Type0Font {
#[must_use]
pub(crate) fn cid_from_charcode(&self, code: CharCode) -> Cid {
self.cmap.cid(code)
}
#[must_use]
pub(crate) fn glyph_from_charcode(&self, code: CharCode) -> (Option<Gid>, bool) {
glyph::resolve(self, code)
}
#[must_use]
pub(crate) fn char_width(&self, code: CharCode) -> f32 {
self.widths.width(code, self.cid_from_charcode(code))
}
#[must_use]
pub(crate) fn vert_width(&self, code: CharCode) -> f32 {
match &self.vertical {
Some(v) => v.width(self.cid_from_charcode(code)),
None => -1000.0,
}
}
#[must_use]
pub(crate) fn vert_origin(&self, code: CharCode) -> (f32, f32) {
let cid = self.cid_from_charcode(code);
match &self.vertical {
Some(v) => v.origin(cid, &self.widths),
None => ((self.widths.width(code, cid) / 2.0).trunc(), 880.0),
}
}
#[must_use]
pub(crate) fn unicode_from_charcode(&self, code: CharCode) -> SmallVec<[char; 2]> {
if let Some(tu) = &self.to_unicode {
let chars = tu.lookup(code);
if !chars.is_empty() {
return chars;
}
}
match self.scalar_unicode(code) {
0 => SmallVec::new(),
u => char::from_u32(u32::from(u))
.map(|c| SmallVec::from_slice(&[c]))
.unwrap_or_default(),
}
}
#[must_use]
pub(crate) fn scalar_unicode(&self, code: CharCode) -> u16 {
match self.cmap.coding() {
CidCoding::Ucs2 | CidCoding::Utf16 => return (code.0 & 0xffff) as u16,
CidCoding::Cid => {
if !pdfrum_cmap::has_cid2unicode(self.charset) {
return 0;
}
let cid = Cid((code.0 & 0xffff) as u16);
return pdfrum_cmap::unicode_from_cid(self.charset, cid).map_or(0, |c| c as u16);
}
_ => {}
}
if pdfrum_cmap::has_cid2unicode(self.charset) && self.cmap.is_loaded() {
let cid = self.cid_from_charcode(code);
return pdfrum_cmap::unicode_from_cid(self.charset, cid).map_or(0, |c| c as u16);
}
if !self.cmap.has_static_map() {
return 0;
}
let cid = self.cid_from_charcode(code);
if cid.0 == 0 {
return 0;
}
pdfrum_cmap::unicode_from_cid(self.charset, cid).map_or(0, |c| c as u16)
}
#[must_use]
pub(crate) fn charcode_from_unicode(&self, unicode: char) -> CharCode {
if let Some(tu) = &self.to_unicode {
let c = tu.reverse(unicode);
if c.0 != 0 {
return c;
}
}
match self.cmap.coding() {
CidCoding::Unknown => return CharCode(0),
CidCoding::Ucs2 | CidCoding::Utf16 => return CharCode(unicode as u32),
CidCoding::Cid => {
if !pdfrum_cmap::has_cid2unicode(self.charset) {
return CharCode(0);
}
for cid in 0..=u16::MAX {
if pdfrum_cmap::unicode_from_cid(self.charset, Cid(cid)) == Some(unicode) {
return CharCode(u32::from(cid));
}
}
}
_ => {}
}
if (unicode as u32) < 0x80 {
return CharCode(unicode as u32);
}
if self.cmap.coding() == CidCoding::Cid {
return CharCode(0);
}
pdfrum_cmap::charcode_from_unicode(&self.cmap, unicode)
}
#[must_use]
pub(crate) fn is_unicode_compatible(&self) -> bool {
if pdfrum_cmap::has_cid2unicode(self.charset) && self.cmap.is_loaded() {
return true;
}
self.cmap.coding() != CidCoding::Unknown
}
#[must_use]
pub(crate) fn is_vertical(&self) -> bool {
self.cmap.is_vertical()
}
#[must_use]
pub(crate) fn char_bbox(&self, code: CharCode) -> Rect {
let (gid, vertical) = self.glyph_from_charcode(code);
let Some(gid) = gid else { return Rect::ZERO };
let Some(bbox) = self.glyphs.glyph_bbox(gid) else {
return Rect::ZERO;
};
if vertical {
return bbox;
}
match self.japan1_transform(code) {
Some(t) => transform::apply(t, bbox),
None => bbox,
}
}
#[must_use]
pub(crate) fn japan1_transform(&self, code: CharCode) -> Option<CidTransform> {
if self.charset != CidSet::Japan1 || self.embedded {
return None;
}
japan1_transform(self.cid_from_charcode(code))
}
pub(crate) fn char_item(&self, code: CharCode) -> CharItem {
let (gid, vertical_glyph) = self.glyph_from_charcode(code);
CharItem {
code,
cid: Some(self.cid_from_charcode(code)),
gid,
unicode: self.unicode_from_charcode(code),
width: if self.is_vertical() {
self.vert_width(code)
} else {
self.char_width(code)
},
vertical_glyph,
}
}
fn gsub(&self) -> &gsub::VerticalSubst {
&self.gsub
}
}
fn use_cmap_parent(
dict: &Dict,
r: &impl Resolve,
limits: &Limits,
diags: &mut Diagnostics,
) -> Option<CMap> {
if let Some(stream) = dict.stream(names::USE_CMAP, r) {
let bytes = pdfrum_filters::decode_chain(&stream, 0, r, limits, diags).data;
return Some(pdfrum_cmap::parse_embedded(&bytes, limits, diags));
}
let resolved = dict.get(names::USE_CMAP, r)?;
let name = resolved.as_name()?;
Some(pdfrum_cmap::from_encoding_name(name, diags))
}
pub(crate) fn load(
dict: &Dict,
r: &impl Resolve,
cache: &FontCache,
opts: &SubstitutionOptions,
limits: &Limits,
diags: &mut Diagnostics,
) -> Result<Type0Font, Error> {
let descendants = dict
.array(names::DESCENDANT_FONTS, r)
.ok_or(Error::BadDescendantFonts)?;
if descendants.len() != 1 {
return Err(Error::BadDescendantFonts);
}
let cid_dict = descendants.dict_at(0, r).ok_or(Error::BadDescendantFonts)?;
let base_font_name = cid_dict
.name(names::BASE_FONT)
.map(|n| n.as_bytes().to_vec())
.unwrap_or_default();
let encoding = dict.raw(names::ENCODING).ok_or(Error::BadCidEncoding)?;
let kind = match cid_dict.name(names::SUBTYPE).map(|n| n.as_bytes().to_vec()) {
Some(s) if s == b"CIDFontType0" => CidFontKind::Type1,
_ => CidFontKind::TrueType,
};
let cmap = match encoding {
Object::Name(name) => pdfrum_cmap::from_encoding_name(name, diags),
Object::Stream(_) | Object::Ref(_) => {
if let Some(stream) = dict.stream(names::ENCODING, r) {
let bytes = pdfrum_filters::decode_chain(&stream, 0, r, limits, diags).data;
let cmap = pdfrum_cmap::parse_embedded(&bytes, limits, diags);
match use_cmap_parent(&stream.dict, r, limits, diags) {
Some(parent) => pdfrum_cmap::inherit_from(cmap, parent, 0, limits, diags),
None => cmap,
}
} else {
let resolved = dict.get(names::ENCODING, r);
match resolved.as_ref().and_then(|o| o.as_name()) {
Some(name) => pdfrum_cmap::from_encoding_name(name, diags),
None => return Err(Error::BadCidEncoding),
}
}
}
_ => return Err(Error::BadCidEncoding),
};
Ok(build(
&cid_dict,
dict,
cmap,
kind,
base_font_name,
r,
cache,
opts,
limits,
diags,
false,
))
}
pub(crate) fn load_gb2312(
dict: &Dict,
r: &impl Resolve,
cache: &FontCache,
opts: &SubstitutionOptions,
limits: &Limits,
diags: &mut Diagnostics,
) -> Result<Type0Font, Error> {
let cmap = pdfrum_cmap::predefined(&pdfrum_object::Name::from("GBK-EUC-H"))
.ok_or(Error::BadCidEncoding)?;
let base_font_name = dict
.name(names::BASE_FONT)
.map(|n| n.as_bytes().to_vec())
.unwrap_or_default();
Ok(build(
dict,
dict,
cmap,
CidFontKind::TrueType,
base_font_name,
r,
cache,
opts,
limits,
diags,
true,
))
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
fn build(
cid_dict: &Dict,
font_dict: &Dict,
cmap: CMap,
kind: CidFontKind,
base_font_name: Vec<u8>,
r: &impl Resolve,
cache: &FontCache,
opts: &SubstitutionOptions,
limits: &Limits,
diags: &mut Diagnostics,
gb2312: bool,
) -> Type0Font {
let adobe_courier_std = matches!(
base_font_name.as_slice(),
b"CourierStd" | b"CourierStd-Bold" | b"CourierStd-BoldOblique" | b"CourierStd-Oblique"
);
let desc = cid_dict.dict(names::FONT_DESCRIPTOR, r);
let mut descriptor = FontDescriptor::default();
if let Some(d) = &desc {
descriptor = descriptor::load(d, r);
}
let (mut glyphs, mut embedded) =
crate::simple::load_font_program(desc.as_ref(), r, limits, diags);
let mut charset = if gb2312 { CidSet::Gb1 } else { cmap.charset() };
if charset == CidSet::Unknown
&& let Some(info) = cid_dict.dict(names::CID_SYSTEM_INFO, r)
&& let Some(ordering) = info.byte_string(names::ORDERING, r)
{
charset = pdfrum_cmap::charset_from_ordering(&ordering);
}
let mut widths_table = CidWidths::load(cid_dict, r, diags);
if gb2312 {
widths_table.set_ansi_widths_fixed();
}
let mut subst_font = None;
if !embedded {
let request = FontRequest {
name: base_font_name.clone(),
is_truetype: kind == CidFontKind::TrueType,
flags: descriptor.flags,
weight: descriptor
.stem_v
.checked_mul(5)
.filter(|w| *w > 0)
.unwrap_or(400),
italic_angle: descriptor.italic_angle,
code_page: CodePage::for_cid_set(charset),
vertical: cmap.is_vertical(),
};
let s = substitute(&request, opts, diags);
glyphs = s.glyphs;
subst_font = Some(s.subst);
}
if !glyphs.is_some() {
embedded = false;
}
let cid_to_gid = match cid_dict.raw(names::CID_TO_GID_MAP) {
Some(Object::Name(n)) if n.as_bytes() == b"Identity" && embedded => CidToGid::Identity,
Some(Object::Stream(_) | Object::Ref(_)) => {
match cid_dict.stream(names::CID_TO_GID_MAP, r) {
Some(s) => {
let bytes = pdfrum_filters::decode_chain(&s, 0, r, limits, diags).data;
if bytes.len() < glyphs.num_glyphs() as usize * 2 {
diags.record(Severity::Suspicious, DiagKind::CidToGidStreamShort, None);
}
CidToGid::Stream(bytes.into_boxed_slice())
}
None => CidToGid::ViaCharmap,
}
}
_ => CidToGid::ViaCharmap,
};
let vertical = if cmap.is_vertical() {
Some(widths::VerticalMetrics::load(cid_dict, r, diags))
} else {
None
};
let to_unicode = crate::simple::load_to_unicode(font_dict, r, limits, diags);
let metrics = match &glyphs {
GlyphSource::Fontations(f) => f.metrics(),
GlyphSource::Type1(f) => Some(descriptor::FaceMetrics {
upem: f.units_per_em(),
bbox_left: f.bbox().x0 as i64,
bbox_top: f.bbox().y1 as i64,
bbox_right: f.bbox().x1 as i64,
bbox_bottom: f.bbox().y0 as i64,
ascender: f.bbox().y1 as i64,
descender: f.bbox().y0 as i64,
}),
GlyphSource::None => None,
};
descriptor::check_font_metrics(&mut descriptor, metrics, |_| Rect::ZERO);
let gsub = if cmap.is_vertical() {
gsub::VerticalSubst::parse(&glyphs, diags)
} else {
gsub::VerticalSubst::none()
};
Type0Font {
id: cache.next_id(),
cmap,
glyphs,
charset,
cid_to_gid,
widths: widths_table,
vertical,
to_unicode,
descriptor,
subst: subst_font,
kind,
embedded,
base_font_name,
adobe_courier_std,
#[cfg(test)]
ansi_widths_fixed: gb2312,
gsub,
}
}
fn substitute(
request: &FontRequest,
opts: &SubstitutionOptions,
diags: &mut Diagnostics,
) -> subst::Substitution {
subst::resolve_with_options(request, opts, diags)
}
pub(crate) fn cid_charmap(glyphs: &GlyphSource, coding: CidCoding) -> Charmap {
let charmaps = glyphs.charmaps();
if let Some(wanted) = legacy_encoding_id(coding)
&& let Some(i) = charmaps
.iter()
.position(|c| c.platform == 3 && c.encoding == wanted)
{
return Charmap::Index(i);
}
if let Some(i) = charmaps.iter().position(|c| c.is_unicode()) {
return Charmap::Index(i);
}
if charmaps.is_empty() {
Charmap::None
} else {
Charmap::Index(0)
}
}
fn legacy_encoding_id(coding: CidCoding) -> Option<u16> {
Some(match coding {
CidCoding::Gb => 3,
CidCoding::Big5 => 4,
CidCoding::Jis => 2,
CidCoding::Korea => 6,
CidCoding::Unknown | CidCoding::Ucs2 | CidCoding::Cid | CidCoding::Utf16 => return None,
})
}
pub(crate) fn face_of(glyphs: &GlyphSource) -> Option<&Face> {
match glyphs {
GlyphSource::Fontations(f) => Some(f),
GlyphSource::Type1(_) | GlyphSource::None => None,
}
}
#[cfg(test)]
#[path = "cid_tests.rs"]
mod tests;