use std::{
collections::btree_map::BTreeMap,
vec::Vec,
};
use serde_derive::{Deserialize, Serialize};
use crate::{
FontId,
};
#[cfg(feature = "text_layout")]
pub use azul_layout::{
PdfFontMetrics as FontMetrics, FontParseWarning as PdfFontParseWarning, FontType, OwnedGlyph,
};
#[cfg(feature = "text_layout")]
pub use azul_layout::ParsedFont as AzulParsedFont;
#[cfg(feature = "text_layout")]
pub use self::parsed_font::ParsedFont;
#[cfg(feature = "text_layout")]
mod parsed_font {
use std::{
ops::{Deref, DerefMut},
sync::Arc,
};
use super::{AzulParsedFont, PdfFontParseWarning};
const FONT_B64_START: &str = "data:font/ttf;base64,";
#[derive(Debug, Clone, PartialEq)]
pub struct ParsedFont(AzulParsedFont);
impl ParsedFont {
pub fn from_bytes(
bytes: &[u8],
font_index: usize,
warnings: &mut Vec<PdfFontParseWarning>,
) -> Option<Self> {
if let Some(wrapped) = crate::font::wrap_bare_cff_as_sfnt(bytes) {
let inner = AzulParsedFont::from_bytes(&wrapped, 0, warnings)?;
return Some(Self::attach_source_bytes(inner, &wrapped));
}
let inner = AzulParsedFont::from_bytes(bytes, font_index, warnings)?;
Some(Self::attach_source_bytes(inner, bytes))
}
pub fn from_azul(inner: AzulParsedFont) -> Self {
Self(inner)
}
fn attach_source_bytes(inner: AzulParsedFont, bytes: &[u8]) -> Self {
if inner.source_bytes_for_subset().is_some() {
return Self(inner);
}
Self(inner.with_source_bytes(Arc::new(rust_fontconfig::FontBytes::Owned(
Arc::from(bytes.to_vec()),
))))
}
pub fn source_bytes(&self) -> Option<Arc<rust_fontconfig::FontBytes>> {
self.0.source_bytes_for_subset()
}
pub fn has_source_bytes(&self) -> bool {
self.source_bytes().is_some()
}
pub fn as_azul(&self) -> &AzulParsedFont {
&self.0
}
pub fn into_inner(self) -> AzulParsedFont {
self.0
}
}
impl Deref for ParsedFont {
type Target = AzulParsedFont;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ParsedFont {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<AzulParsedFont> for ParsedFont {
fn from(inner: AzulParsedFont) -> Self {
Self::from_azul(inner)
}
}
impl serde::Serialize for ParsedFont {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use base64::Engine;
let encoded = match self.source_bytes() {
Some(b) => base64::prelude::BASE64_STANDARD.encode(b.as_slice()),
None => {
let bytes = self.0.to_bytes(None).map_err(|e| {
serde::ser::Error::custom(format!("font has no source bytes: {e}"))
})?;
base64::prelude::BASE64_STANDARD.encode(&bytes)
}
};
let s = format!("{FONT_B64_START}{encoded}");
s.serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for ParsedFont {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use base64::Engine;
let s = String::deserialize(deserializer)?;
let b64 = s.strip_prefix(FONT_B64_START).ok_or_else(|| {
serde::de::Error::custom(format!(
"font must be a {FONT_B64_START}… data URI, got {:.32?}",
s
))
})?;
let bytes = base64::prelude::BASE64_STANDARD
.decode(b64)
.map_err(serde::de::Error::custom)?;
let mut warnings = Vec::new();
ParsedFont::from_bytes(&bytes, 0, &mut warnings).ok_or_else(|| {
serde::de::Error::custom(format!("font deserialization error: {warnings:?}"))
})
}
}
}
#[cfg(not(feature = "text_layout"))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ParsedFont {
pub original_bytes: Vec<u8>,
pub font_index: u32,
pub font_name: Option<String>,
pub codepoint_to_glyph: BTreeMap<u32, u16>,
pub glyph_widths: BTreeMap<u16, u16>,
pub units_per_em: u16,
pub font_metrics: FontMetrics,
pub font_type: FontType,
pub pdf_font_metrics: PdfFontMetricsStub,
}
#[cfg(not(feature = "text_layout"))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PdfFontMetricsStub {
pub units_per_em: u16,
pub x_min: i16,
pub y_min: i16,
pub x_max: i16,
pub y_max: i16,
pub caret_slope_rise: i16,
pub caret_slope_run: i16,
pub us_weight_class: u16,
}
#[cfg(not(feature = "text_layout"))]
impl Default for PdfFontMetricsStub {
fn default() -> Self {
Self {
units_per_em: 1000,
x_min: 0,
y_min: -200,
x_max: 1000,
y_max: 800,
caret_slope_rise: 1,
caret_slope_run: 0,
us_weight_class: 0,
}
}
}
#[cfg(not(feature = "text_layout"))]
impl ParsedFont {
pub fn source_bytes(&self) -> Option<Vec<u8>> {
(!self.original_bytes.is_empty()).then(|| self.original_bytes.clone())
}
pub fn has_source_bytes(&self) -> bool {
!self.original_bytes.is_empty()
}
pub fn from_bytes(
bytes: &[u8],
index: usize,
warnings: &mut Vec<PdfFontParseWarning>,
) -> Option<Self> {
use allsorts::{
binary::read::ReadScope,
font_data::FontData,
tables::{cmap::Cmap, FontTableProvider, HeadTable, HheaTable, HmtxTable, MaxpTable},
tag,
};
let mut warn = |msg: &str| {
warnings.push(PdfFontParseWarning {
severity: FontParseSeverity::Warning,
message: msg.to_string(),
})
};
let font_file = ReadScope::new(bytes).read::<FontData<'_>>().ok()?;
let provider = font_file.table_provider(index).ok()?;
let head = provider
.read_table_data(tag::HEAD)
.ok()
.and_then(|d| ReadScope::new(&d).read::<HeadTable>().ok())?;
let maxp = provider
.read_table_data(tag::MAXP)
.ok()
.and_then(|d| ReadScope::new(&d).read::<MaxpTable>().ok())?;
let hhea = provider
.read_table_data(tag::HHEA)
.ok()
.and_then(|d| ReadScope::new(&d).read::<HheaTable>().ok())?;
let mut codepoint_to_glyph = BTreeMap::new();
let cmap_data = provider.read_table_data(tag::CMAP).ok();
match cmap_data
.as_deref()
.and_then(|d| ReadScope::new(d).read::<Cmap<'_>>().ok())
.and_then(|cmap| allsorts::font::read_cmap_subtable(&cmap).ok().flatten())
{
Some((_encoding, subtable)) => {
let _ = subtable.mappings_fn(|cp, gid| {
codepoint_to_glyph.insert(cp, gid);
});
}
None => warn("font has no usable Unicode cmap subtable; text will not map to glyphs"),
}
let mut glyph_widths = BTreeMap::new();
let hmtx_data = provider.read_table_data(tag::HMTX).ok();
match hmtx_data.as_deref().and_then(|d| {
ReadScope::new(d)
.read_dep::<HmtxTable<'_>>((
usize::from(maxp.num_glyphs),
usize::from(hhea.num_h_metrics),
))
.ok()
}) {
Some(hmtx) => {
for gid in 0..maxp.num_glyphs {
if let Ok(advance) = hmtx.horizontal_advance(gid) {
glyph_widths.insert(gid, advance);
}
}
}
None => warn("font has no hmtx table; glyph advances will be zero"),
}
let font_type = if provider.has_table(tag::CFF) {
FontType::OpenTypeCFF(())
} else {
FontType::TrueType
};
let os2_data = provider.read_table_data(tag::OS_2).ok();
let us_weight_class = os2_data
.as_deref()
.and_then(|d| {
ReadScope::new(d)
.read_dep::<allsorts::tables::os2::Os2>(d.len())
.ok()
})
.map(|os2| os2.us_weight_class)
.unwrap_or(0);
Some(ParsedFont {
original_bytes: bytes.to_vec(),
font_index: index as u32,
font_name: None,
codepoint_to_glyph,
glyph_widths,
units_per_em: head.units_per_em,
font_metrics: FontMetrics {
ascent: hhea.ascender,
descent: hhea.descender,
},
font_type,
pdf_font_metrics: PdfFontMetricsStub {
units_per_em: head.units_per_em,
x_min: head.x_min,
y_min: head.y_min,
x_max: head.x_max,
y_max: head.y_max,
caret_slope_rise: hhea.caret_slope_rise,
caret_slope_run: hhea.caret_slope_run,
us_weight_class,
},
})
}
pub fn with_glyph_data(
bytes: Vec<u8>,
index: u32,
font_name: Option<String>,
codepoint_to_glyph: BTreeMap<u32, u16>,
glyph_widths: BTreeMap<u16, u16>,
units_per_em: u16,
font_metrics: FontMetrics,
) -> Self {
ParsedFont {
original_bytes: bytes,
font_index: index,
font_name,
codepoint_to_glyph,
glyph_widths,
units_per_em,
font_metrics,
font_type: FontType::TrueType,
pdf_font_metrics: PdfFontMetricsStub { units_per_em, ..Default::default() },
}
}
pub fn set_codepoint_mapping(&mut self, codepoint: u32, gid: u16) {
self.codepoint_to_glyph.insert(codepoint, gid);
}
pub fn set_glyph_width(&mut self, gid: u16, width: u16) {
self.glyph_widths.insert(gid, width);
}
pub fn get_glyph_width(&self, gid: u16) -> Option<u16> {
self.glyph_widths.get(&gid).copied()
}
pub fn lookup_glyph_index(&self, codepoint: u32) -> Option<u16> {
self.codepoint_to_glyph.get(&codepoint).copied()
}
pub fn get_glyph_primary_char(&self, _gid: u16) -> Option<char> {
None
}
}
#[cfg(not(feature = "text_layout"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FontType {
TrueType,
OpenTypeCFF(()),
}
#[cfg(not(feature = "text_layout"))]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FontParseSeverity {
Info,
Warning,
Error,
}
#[cfg(not(feature = "text_layout"))]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FontParseWarning {
pub severity: FontParseSeverity,
pub message: String,
}
#[cfg(not(feature = "text_layout"))]
pub type PdfFontParseWarning = FontParseWarning;
#[cfg(not(feature = "text_layout"))]
pub type OwnedGlyph = ();
#[cfg(not(feature = "text_layout"))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FontMetrics {
pub ascent: i16,
pub descent: i16,
}
#[derive(Debug, Clone)]
pub struct SubsetFont {
pub bytes: Vec<u8>,
pub glyph_mapping: BTreeMap<u16, (u16, String)>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PrintpdfFontMeta {
pub original_gid_to_cid: Option<BTreeMap<u16, u16>>,
pub original_to_unicode_map: Option<String>,
pub embedding_mode: FontEmbeddingMode,
pub requires_subsetting: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FontEmbeddingMode {
Full,
Subset,
Reference,
}
impl Default for PrintpdfFontMeta {
fn default() -> Self {
Self {
original_gid_to_cid: None,
original_to_unicode_map: None,
embedding_mode: FontEmbeddingMode::Subset,
requires_subsetting: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PdfFont {
pub parsed_font: ParsedFont,
pub meta: PrintpdfFontMeta,
}
impl PdfFont {
pub fn new(parsed_font: ParsedFont) -> Self {
Self {
parsed_font,
meta: PrintpdfFontMeta::default(),
}
}
pub fn with_meta(parsed_font: ParsedFont, meta: PrintpdfFontMeta) -> Self {
Self { parsed_font, meta }
}
}
#[derive(Debug, Clone)]
pub enum Font {
BuiltinFont(BuiltinFont),
ExternalFont(ParsedFont, PrintpdfFontMeta),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BuiltinFont {
TimesRoman,
TimesBold,
TimesItalic,
TimesBoldItalic,
Helvetica,
HelveticaBold,
HelveticaOblique,
HelveticaBoldOblique,
Courier,
CourierOblique,
CourierBold,
CourierBoldOblique,
Symbol,
ZapfDingbats,
}
impl Default for BuiltinFont {
fn default() -> Self {
Self::TimesRoman }
}
include!("../defaultfonts/mapping.rs");
impl BuiltinFont {
pub fn check_if_matches(bytes: &[u8]) -> Option<Self> {
let matching_based_on_len = match_len(bytes)?;
if bytes == matching_based_on_len.get_subset_font().bytes.as_slice() {
Some(matching_based_on_len)
} else {
None
}
}
pub fn get_parsed_font(&self) -> Option<ParsedFont> {
let subset = self.get_subset_font();
ParsedFont::from_bytes(&subset.bytes, 0, &mut Vec::new())
}
pub fn get_svg_font_family(&self) -> &'static str {
match self {
BuiltinFont::TimesRoman => "Times New Roman, Times, serif",
BuiltinFont::TimesBold => "Times New Roman, Times, serif",
BuiltinFont::TimesItalic => "Times New Roman, Times, serif",
BuiltinFont::TimesBoldItalic => "Times New Roman, Times, serif",
BuiltinFont::Helvetica => "Helvetica, Arial, sans-serif",
BuiltinFont::HelveticaBold => "Helvetica, Arial, sans-serif",
BuiltinFont::HelveticaOblique => "Helvetica, Arial, sans-serif",
BuiltinFont::HelveticaBoldOblique => "Helvetica, Arial, sans-serif",
BuiltinFont::Courier => "Courier New, Courier, monospace",
BuiltinFont::CourierOblique => "Courier New, Courier, monospace",
BuiltinFont::CourierBold => "Courier New, Courier, monospace",
BuiltinFont::CourierBoldOblique => "Courier New, Courier, monospace",
BuiltinFont::Symbol => "Symbol",
BuiltinFont::ZapfDingbats => "Zapf Dingbats",
}
}
pub fn get_font_weight(&self) -> &'static str {
match self {
BuiltinFont::TimesRoman
| BuiltinFont::TimesItalic
| BuiltinFont::Helvetica
| BuiltinFont::HelveticaOblique
| BuiltinFont::Courier
| BuiltinFont::CourierOblique
| BuiltinFont::Symbol
| BuiltinFont::ZapfDingbats => "normal",
BuiltinFont::TimesBold
| BuiltinFont::TimesBoldItalic
| BuiltinFont::HelveticaBold
| BuiltinFont::HelveticaBoldOblique
| BuiltinFont::CourierBold
| BuiltinFont::CourierBoldOblique => "bold",
}
}
pub fn get_font_style(&self) -> &'static str {
match self {
BuiltinFont::TimesItalic
| BuiltinFont::TimesBoldItalic
| BuiltinFont::HelveticaOblique
| BuiltinFont::HelveticaBoldOblique
| BuiltinFont::CourierOblique
| BuiltinFont::CourierBoldOblique => "italic",
_ => "normal",
}
}
pub fn get_subset_font(&self) -> SubsetFont {
use self::BuiltinFont::*;
SubsetFont {
bytes: match self {
TimesRoman => crate::utils::uncompress(include_bytes!(
"../defaultfonts/Times-Roman.subset.ttf"
)),
TimesBold => crate::utils::uncompress(include_bytes!(
"../defaultfonts/Times-Bold.subset.ttf"
)),
TimesItalic => crate::utils::uncompress(include_bytes!(
"../defaultfonts/Times-Italic.subset.ttf"
)),
TimesBoldItalic => crate::utils::uncompress(include_bytes!(
"../defaultfonts/Times-BoldItalic.subset.ttf"
)),
Helvetica => {
crate::utils::uncompress(include_bytes!("../defaultfonts/Helvetica.subset.ttf"))
}
HelveticaBold => crate::utils::uncompress(include_bytes!(
"../defaultfonts/Helvetica-Bold.subset.ttf"
)),
HelveticaOblique => crate::utils::uncompress(include_bytes!(
"../defaultfonts/Helvetica-Oblique.subset.ttf"
)),
HelveticaBoldOblique => crate::utils::uncompress(include_bytes!(
"../defaultfonts/Helvetica-BoldOblique.subset.ttf"
)),
Courier => {
crate::utils::uncompress(include_bytes!("../defaultfonts/Courier.subset.ttf"))
}
CourierOblique => crate::utils::uncompress(include_bytes!(
"../defaultfonts/Courier-Oblique.subset.ttf"
)),
CourierBold => crate::utils::uncompress(include_bytes!(
"../defaultfonts/Courier-Bold.subset.ttf"
)),
CourierBoldOblique => crate::utils::uncompress(include_bytes!(
"../defaultfonts/Courier-BoldOblique.subset.ttf"
)),
Symbol => {
crate::utils::uncompress(include_bytes!("../defaultfonts/Symbol.subset.ttf"))
}
ZapfDingbats => crate::utils::uncompress(include_bytes!(
"../defaultfonts/ZapfDingbats.subset.ttf"
)),
},
glyph_mapping: FONTS
.iter()
.filter_map(|(font_id, old_gid, new_gid, char)| {
if *font_id == self.get_num() {
Some((*old_gid, (*new_gid, char.to_string())))
} else {
None
}
})
.collect(),
}
}
pub fn get_pdf_id(&self) -> &'static str {
use self::BuiltinFont::*;
match self {
TimesRoman => "F1",
TimesBold => "F2",
TimesItalic => "F3",
TimesBoldItalic => "F4",
Helvetica => "F5",
HelveticaBold => "F6",
HelveticaOblique => "F7",
HelveticaBoldOblique => "F8",
Courier => "F9",
CourierOblique => "F10",
CourierBold => "F11",
CourierBoldOblique => "F12",
Symbol => "F13",
ZapfDingbats => "F14",
}
}
pub fn get_num(&self) -> usize {
use self::BuiltinFont::*;
match self {
TimesRoman => 0,
TimesBold => 1,
TimesItalic => 2,
TimesBoldItalic => 3,
Helvetica => 4,
HelveticaBold => 5,
HelveticaOblique => 6,
HelveticaBoldOblique => 7,
Courier => 8,
CourierOblique => 9,
CourierBold => 10,
CourierBoldOblique => 11,
Symbol => 12,
ZapfDingbats => 13,
}
}
pub fn from_id(s: &str) -> Option<Self> {
use self::BuiltinFont::*;
match s {
"Times-Roman" | "F1" => Some(TimesRoman),
"Times-Bold" | "F2" => Some(TimesBold),
"Times-Italic" | "F3" => Some(TimesItalic),
"Times-BoldItalic" | "F4" => Some(TimesBoldItalic),
"Helvetica" | "F5" => Some(Helvetica),
"Helvetica-Bold" | "F6" => Some(HelveticaBold),
"Helvetica-Oblique" | "F7" => Some(HelveticaOblique),
"Helvetica-BoldOblique" | "F8" => Some(HelveticaBoldOblique),
"Courier" | "F9" => Some(Courier),
"Courier-Oblique" | "F10" => Some(CourierOblique),
"Courier-Bold" | "F11" => Some(CourierBold),
"Courier-BoldOblique" | "F12" => Some(CourierBoldOblique),
"Symbol" | "F13" => Some(Symbol),
"ZapfDingbats" | "F14" => Some(ZapfDingbats),
_ => None,
}
}
pub fn get_id(&self) -> &'static str {
use self::BuiltinFont::*;
match self {
TimesRoman => "Times-Roman",
TimesBold => "Times-Bold",
TimesItalic => "Times-Italic",
TimesBoldItalic => "Times-BoldItalic",
Helvetica => "Helvetica",
HelveticaBold => "Helvetica-Bold",
HelveticaOblique => "Helvetica-Oblique",
HelveticaBoldOblique => "Helvetica-BoldOblique",
Courier => "Courier",
CourierOblique => "Courier-Oblique",
CourierBold => "Courier-Bold",
CourierBoldOblique => "Courier-BoldOblique",
Symbol => "Symbol",
ZapfDingbats => "ZapfDingbats",
}
}
pub fn all_ids() -> [BuiltinFont; 14] {
use self::BuiltinFont::*;
[
TimesRoman,
TimesBold,
TimesItalic,
TimesBoldItalic,
Helvetica,
HelveticaBold,
HelveticaOblique,
HelveticaBoldOblique,
Courier,
CourierOblique,
CourierBold,
CourierBoldOblique,
Symbol,
ZapfDingbats,
]
}
}
impl Font {
pub fn get_parsed_font(&self) -> Option<&ParsedFont> {
match self {
Font::BuiltinFont(_) => None,
Font::ExternalFont(parsed, _) => Some(parsed),
}
}
pub fn get_parsed_font_mut(&mut self) -> Option<&mut ParsedFont> {
match self {
Font::BuiltinFont(_) => None,
Font::ExternalFont(parsed, _) => Some(parsed),
}
}
pub fn get_font_meta(&self) -> Option<&PrintpdfFontMeta> {
match self {
Font::BuiltinFont(_) => None,
Font::ExternalFont(_, meta) => Some(meta),
}
}
pub fn get_font_meta_mut(&mut self) -> Option<&mut PrintpdfFontMeta> {
match self {
Font::BuiltinFont(_) => None,
Font::ExternalFont(_, meta) => Some(meta),
}
}
}
#[cfg(feature = "text_layout")]
pub fn subset_font(font: &ParsedFont, glyph_ids: &BTreeMap<u16, String>) -> Result<SubsetFont, String> {
use allsorts::{binary::read::ReadScope, font_data::FontData, subset::CmapTarget};
let original_bytes = font
.source_bytes()
.ok_or_else(|| "ParsedFont has no source bytes to subset".to_string())?;
let scope = ReadScope::new(original_bytes.as_slice());
let font_file = scope.read::<FontData<'_>>().map_err(|e| e.to_string())?;
let provider = font_file
.table_provider(font.original_index)
.map_err(|e| e.to_string())?;
let ids: Vec<u16> = std::iter::once(0)
.chain(glyph_ids.keys().copied().filter(|gid| *gid != 0))
.collect();
let bytes = allsorts::subset::subset(
&provider,
&ids,
&allsorts::subset::SubsetProfile::Pdf,
CmapTarget::Unicode,
).map_err(|e| e.to_string())?;
let glyph_mapping: BTreeMap<u16, (u16, String)> = ids
.iter()
.enumerate()
.filter_map(|(idx, &original_gid)| {
glyph_ids.get(&original_gid).map(|ch| {
(original_gid, (idx as u16, ch.clone()))
})
})
.collect();
Ok(SubsetFont {
bytes,
glyph_mapping,
})
}
#[cfg(not(feature = "text_layout"))]
pub fn subset_font(font: &ParsedFont, _glyph_ids: &BTreeMap<u16, String>) -> Result<SubsetFont, String> {
Ok(SubsetFont {
bytes: font.original_bytes.clone(),
glyph_mapping: BTreeMap::new(),
})
}
pub fn generate_cmap_string(_font: &ParsedFont, font_id: &FontId, glyph_ids: &[(u16, String)]) -> String {
let mappings = glyph_ids
.iter()
.map(|(gid, unicode)| {
(*gid as u32, unicode.chars().map(|c| c as u32).collect())
})
.collect();
let cmap = crate::cmap::ToUnicodeCMap { mappings };
cmap.to_cmap_string(&font_id.0)
}
pub fn extract_collection_face(bytes: &[u8], index: usize) -> Option<Vec<u8>> {
use allsorts::{
binary::read::ReadScope, font_data::FontData, subset::whole_font,
tables::FontTableProvider,
};
if bytes.get(..4) != Some(b"ttcf") {
return None;
}
let font_file = ReadScope::new(bytes).read::<FontData<'_>>().ok()?;
let provider = font_file.table_provider(index).ok()?;
let tags = provider.table_tags()?;
whole_font(&provider, &tags).ok()
}
pub fn cff_charset_gid_to_cid_map(font_bytes: &[u8], index: usize) -> Option<BTreeMap<u16, u16>> {
use allsorts::{
binary::read::ReadScope, cff::CFF, font_data::FontData, tables::FontTableProvider, tag,
};
let cff_data: Vec<u8> = (|| -> Option<Vec<u8>> {
let font_file = ReadScope::new(font_bytes).read::<FontData<'_>>().ok()?;
let provider = font_file.table_provider(index).ok()?;
Some(provider.read_table_data(tag::CFF).ok()?.into_owned())
})()
.unwrap_or_else(|| font_bytes.to_vec());
let cff = ReadScope::new(&cff_data).read::<CFF<'_>>().ok()?;
let font = cff.fonts.first()?;
if !font.is_cid_keyed() {
return None;
}
let num_glyphs = font.char_strings_index.len() as u16;
Some(
(0..num_glyphs)
.filter_map(|gid| font.charset.id_for_glyph(gid).map(|cid| (gid, cid)))
.collect(),
)
}
pub fn extract_cid_keyed_cff(font_bytes: &[u8], index: usize) -> Option<Vec<u8>> {
use allsorts::{
binary::read::ReadScope, cff::CFF, font_data::FontData, tables::FontTableProvider, tag,
};
if !font_bytes.starts_with(b"OTTO") {
return None;
}
let font_file = ReadScope::new(font_bytes).read::<FontData<'_>>().ok()?;
let provider = font_file.table_provider(index).ok()?;
let cff_data = provider.read_table_data(tag::CFF).ok()?;
let cff = ReadScope::new(&cff_data).read::<CFF<'_>>().ok()?;
if !cff.fonts.first()?.is_cid_keyed() {
return None;
}
Some(cff_data.into_owned())
}
#[cfg(feature = "text_layout")]
pub(crate) fn wrap_bare_cff_as_sfnt(bytes: &[u8]) -> Option<Vec<u8>> {
use allsorts::{binary::read::ReadScope, cff::CFF};
let looks_like_sfnt = matches!(bytes.get(..4), Some(magic) if magic == b"OTTO"
|| magic == [0, 1, 0, 0]
|| magic == *b"true"
|| magic == *b"typ1"
|| magic == *b"ttcf"
|| magic == *b"wOFF"
|| magic == *b"wOF2");
if looks_like_sfnt {
return None;
}
let cff = ReadScope::new(bytes).read::<CFF<'_>>().ok()?;
let font = cff.fonts.first()?;
let num_glyphs = u16::try_from(font.char_strings_index.len()).ok()?;
let widths: Vec<u16> = (0..num_glyphs)
.map(|gid| cff_charstring_width(&cff, font, gid))
.collect();
let units_per_em = cff_units_per_em(font);
Some(build_minimal_sfnt(bytes, num_glyphs, &widths, units_per_em))
}
#[cfg(feature = "text_layout")]
fn cff_units_per_em(font: &allsorts::cff::Font<'_>) -> u16 {
use allsorts::cff::{Operand, Operator};
let scale = font
.top_dict
.get_with_default(Operator::FontMatrix)
.and_then(|ops| ops.first())
.and_then(|op| match op {
Operand::Integer(i) => Some(f64::from(*i)),
Operand::Offset(i) => Some(f64::from(*i)),
Operand::Real(r) => f64::try_from(r).ok(),
});
match scale {
Some(m0) if m0.is_finite() && m0 > 0.0 => (1.0 / m0).round().clamp(16.0, 16384.0) as u16,
_ => 1000,
}
}
#[cfg(feature = "text_layout")]
fn cff_charstring_width(
cff: &allsorts::cff::CFF<'_>,
font: &allsorts::cff::Font<'_>,
gid: u16,
) -> u16 {
use allsorts::cff::{CFFVariant, Operator};
let charstring = font.char_strings_index.read_object(usize::from(gid));
let (nominal_width, default_width, local_subrs) = match &font.data {
CFFVariant::CID(cid) => {
let fd = cid.fd_select.font_dict_index(gid).unwrap_or(0) as usize;
let pd = cid.private_dicts.get(fd);
(
pd.and_then(|d| d.get_i32(Operator::NominalWidthX))
.and_then(Result::ok)
.unwrap_or(0),
pd.and_then(|d| d.get_i32(Operator::DefaultWidthX))
.and_then(Result::ok)
.unwrap_or(0),
cid.local_subr_indices.get(fd).and_then(|s| s.as_ref()),
)
}
CFFVariant::Type1(t1) => (
t1.private_dict
.get_i32(Operator::NominalWidthX)
.and_then(Result::ok)
.unwrap_or(0),
t1.private_dict
.get_i32(Operator::DefaultWidthX)
.and_then(Result::ok)
.unwrap_or(0),
t1.local_subr_index.as_ref(),
),
};
let Some(charstring) = charstring else {
return default_width.max(0) as u16;
};
let mut stack: Vec<f64> = Vec::new();
let width = match cff_charstring_width_delta(
charstring,
local_subrs,
&cff.global_subr_index,
&mut stack,
0,
) {
Some(delta) => nominal_width as f64 + delta,
None => default_width as f64,
};
width.round().clamp(0.0, u16::MAX as f64) as u16
}
#[cfg(feature = "text_layout")]
fn cff_subr_bias(count: usize) -> i32 {
if count < 1240 {
107
} else if count < 33900 {
1131
} else {
32768
}
}
#[cfg(feature = "text_layout")]
fn cff_charstring_width_delta(
charstring: &[u8],
local_subrs: Option<&allsorts::cff::MaybeOwnedIndex<'_>>,
global_subrs: &allsorts::cff::MaybeOwnedIndex<'_>,
stack: &mut Vec<f64>,
depth: u8,
) -> Option<f64> {
if depth > 10 {
return None;
}
let mut i = 0usize;
while i < charstring.len() {
let b0 = charstring[i];
match b0 {
1 | 3 | 18 | 19 | 20 | 23 => {
return (stack.len() % 2 == 1).then(|| stack[0]);
}
21 => return (stack.len() > 2).then(|| stack[0]), 22 | 4 => return (stack.len() > 1).then(|| stack[0]), 14 => return (stack.len() == 1 || stack.len() == 5).then(|| stack[0]), 10 | 29 => {
let Some(idx) = stack.pop() else { return None };
let (subrs, bias) = if b0 == 10 {
let subrs = local_subrs?;
(subrs, cff_subr_bias(subrs.len()))
} else {
(global_subrs, cff_subr_bias(global_subrs.len()))
};
let subr_idx = idx as i32 + bias;
let sub_bytes = usize::try_from(subr_idx)
.ok()
.and_then(|si| subrs.read_object(si))?;
if let Some(w) =
cff_charstring_width_delta(sub_bytes, local_subrs, global_subrs, stack, depth + 1)
{
return Some(w);
}
i += 1;
}
11 => return None, 12 => {
if i + 2 > charstring.len() {
return None;
}
if !cff_apply_escape_op(charstring[i + 1], stack) {
return None;
}
i += 2;
}
28 => {
if i + 3 > charstring.len() {
return None;
}
let v = i16::from_be_bytes([charstring[i + 1], charstring[i + 2]]);
stack.push(f64::from(v));
i += 3;
}
32..=246 => {
stack.push(f64::from(b0) - 139.0);
i += 1;
}
247..=250 => {
if i + 2 > charstring.len() {
return None;
}
stack.push((f64::from(b0) - 247.0) * 256.0 + f64::from(charstring[i + 1]) + 108.0);
i += 2;
}
251..=254 => {
if i + 2 > charstring.len() {
return None;
}
stack.push(-(f64::from(b0) - 251.0) * 256.0 - f64::from(charstring[i + 1]) - 108.0);
i += 2;
}
255 => {
if i + 5 > charstring.len() {
return None;
}
let bits = i32::from_be_bytes([
charstring[i + 1],
charstring[i + 2],
charstring[i + 3],
charstring[i + 4],
]);
stack.push(f64::from(bits) / 65536.0);
i += 5;
}
_ => return None,
}
}
None
}
#[cfg(feature = "text_layout")]
fn cff_apply_escape_op(op: u8, stack: &mut Vec<f64>) -> bool {
fn un(stack: &mut Vec<f64>, f: impl Fn(f64) -> Option<f64>) -> bool {
let Some(a) = stack.pop() else { return false };
match f(a) {
Some(v) => {
stack.push(v);
true
}
None => false,
}
}
fn bin(stack: &mut Vec<f64>, f: impl Fn(f64, f64) -> Option<f64>) -> bool {
let Some(b) = stack.pop() else { return false };
let Some(a) = stack.pop() else { return false };
match f(a, b) {
Some(v) => {
stack.push(v);
true
}
None => false,
}
}
fn truth(v: bool) -> Option<f64> {
Some(if v { 1.0 } else { 0.0 })
}
match op {
3 => bin(stack, |a, b| truth(a != 0.0 && b != 0.0)), 4 => bin(stack, |a, b| truth(a != 0.0 || b != 0.0)), 5 => un(stack, |a| truth(a == 0.0)), 9 => un(stack, |a| Some(a.abs())), 10 => bin(stack, |a, b| Some(a + b)), 11 => bin(stack, |a, b| Some(a - b)), 12 => bin(stack, |a, b| (b != 0.0).then(|| a / b)), 14 => un(stack, |a| Some(-a)), 15 => bin(stack, |a, b| truth(a == b)), 18 => stack.pop().is_some(), 22 => {
let (Some(v2), Some(v1), Some(s2), Some(s1)) =
(stack.pop(), stack.pop(), stack.pop(), stack.pop())
else {
return false;
};
stack.push(if v1 <= v2 { s1 } else { s2 });
true
}
24 => bin(stack, |a, b| Some(a * b)), 26 => un(stack, |a| (a >= 0.0).then(|| a.sqrt())), 27 => match stack.last().copied() {
Some(a) => {
stack.push(a);
true
}
None => false,
},
28 => {
let len = stack.len();
if len < 2 {
return false;
}
stack.swap(len - 1, len - 2);
true
}
29 => {
let Some(n) = stack.pop() else { return false };
let idx = if n < 0.0 {
stack.len().checked_sub(1)
} else {
stack.len().checked_sub(1 + n as usize)
};
match idx.and_then(|i| stack.get(i)).copied() {
Some(v) => {
stack.push(v);
true
}
None => false,
}
}
30 => {
let (Some(j), Some(n)) = (stack.pop(), stack.pop()) else {
return false;
};
if n < 0.0 || n as usize > stack.len() {
return false;
}
let n = n as usize;
if n > 0 {
let start = stack.len() - n;
let shift = (j as i64).rem_euclid(n as i64) as usize;
stack[start..].rotate_right(shift);
}
true
}
_ => false,
}
}
#[cfg(feature = "text_layout")]
fn build_minimal_sfnt(
cff_bytes: &[u8],
num_glyphs: u16,
widths: &[u16],
units_per_em: u16,
) -> Vec<u8> {
let mut head = Vec::with_capacity(54);
head.extend_from_slice(&1u16.to_be_bytes()); head.extend_from_slice(&0u16.to_be_bytes()); head.extend_from_slice(&0x0001_0000u32.to_be_bytes()); head.extend_from_slice(&0u32.to_be_bytes()); head.extend_from_slice(&0x5F0F_3CF5u32.to_be_bytes()); head.extend_from_slice(&0u16.to_be_bytes()); head.extend_from_slice(&units_per_em.to_be_bytes());
head.extend_from_slice(&0i64.to_be_bytes()); head.extend_from_slice(&0i64.to_be_bytes()); head.extend_from_slice(&0i16.to_be_bytes()); head.extend_from_slice(&0i16.to_be_bytes()); head.extend_from_slice(&0i16.to_be_bytes()); head.extend_from_slice(&0i16.to_be_bytes()); head.extend_from_slice(&0u16.to_be_bytes()); head.extend_from_slice(&0u16.to_be_bytes()); head.extend_from_slice(&2i16.to_be_bytes()); head.extend_from_slice(&0i16.to_be_bytes()); head.extend_from_slice(&0i16.to_be_bytes());
let advance_width_max = widths.iter().copied().max().unwrap_or(0);
let mut hhea = Vec::with_capacity(36);
hhea.extend_from_slice(&1u16.to_be_bytes()); hhea.extend_from_slice(&0u16.to_be_bytes()); hhea.extend_from_slice(&(units_per_em as i16 * 8 / 10).to_be_bytes()); hhea.extend_from_slice(&(-(units_per_em as i16) * 2 / 10).to_be_bytes()); hhea.extend_from_slice(&0i16.to_be_bytes()); hhea.extend_from_slice(&advance_width_max.to_be_bytes());
hhea.extend_from_slice(&0i16.to_be_bytes()); hhea.extend_from_slice(&0i16.to_be_bytes()); hhea.extend_from_slice(&0i16.to_be_bytes()); hhea.extend_from_slice(&1i16.to_be_bytes()); hhea.extend_from_slice(&0i16.to_be_bytes()); hhea.extend_from_slice(&0i16.to_be_bytes()); hhea.extend_from_slice(&0i16.to_be_bytes()); hhea.extend_from_slice(&0i16.to_be_bytes());
hhea.extend_from_slice(&0i16.to_be_bytes());
hhea.extend_from_slice(&0i16.to_be_bytes());
hhea.extend_from_slice(&0i16.to_be_bytes()); hhea.extend_from_slice(&num_glyphs.to_be_bytes());
let mut maxp = Vec::with_capacity(6);
maxp.extend_from_slice(&0x0000_5000u32.to_be_bytes()); maxp.extend_from_slice(&num_glyphs.to_be_bytes());
let mut hmtx = Vec::with_capacity(widths.len() * 4);
for &w in widths {
hmtx.extend_from_slice(&w.to_be_bytes());
hmtx.extend_from_slice(&0i16.to_be_bytes()); }
let mut cmap_subtable = Vec::with_capacity(10);
cmap_subtable.extend_from_slice(&6u16.to_be_bytes()); cmap_subtable.extend_from_slice(&10u16.to_be_bytes()); cmap_subtable.extend_from_slice(&0u16.to_be_bytes()); cmap_subtable.extend_from_slice(&0u16.to_be_bytes()); cmap_subtable.extend_from_slice(&0u16.to_be_bytes());
let mut cmap = Vec::with_capacity(12 + cmap_subtable.len());
cmap.extend_from_slice(&0u16.to_be_bytes()); cmap.extend_from_slice(&1u16.to_be_bytes()); cmap.extend_from_slice(&3u16.to_be_bytes()); cmap.extend_from_slice(&1u16.to_be_bytes()); cmap.extend_from_slice(&12u32.to_be_bytes()); cmap.extend_from_slice(&cmap_subtable);
build_sfnt(&[
(*b"CFF ", cff_bytes),
(*b"cmap", &cmap),
(*b"head", &head),
(*b"hhea", &hhea),
(*b"hmtx", &hmtx),
(*b"maxp", &maxp),
])
}
#[cfg(feature = "text_layout")]
fn build_sfnt(tables: &[([u8; 4], &[u8])]) -> Vec<u8> {
fn checksum(data: &[u8]) -> u32 {
let mut sum: u32 = 0;
for chunk in data.chunks(4) {
let mut word = [0u8; 4];
word[..chunk.len()].copy_from_slice(chunk);
sum = sum.wrapping_add(u32::from_be_bytes(word));
}
sum
}
fn padded_len(len: usize) -> usize {
(len + 3) & !3
}
let num_tables = tables.len() as u16;
let mut search_range_pow2 = 1u16;
let mut entry_selector = 0u16;
while search_range_pow2 * 2 <= num_tables {
search_range_pow2 *= 2;
entry_selector += 1;
}
let search_range = search_range_pow2 * 16;
let range_shift = num_tables * 16 - search_range;
let mut offset = 12 + 16 * tables.len();
let mut directory = Vec::with_capacity(16 * tables.len());
let mut data_section = Vec::new();
for (tag, data) in tables {
directory.extend_from_slice(tag);
directory.extend_from_slice(&checksum(data).to_be_bytes());
directory.extend_from_slice(&(offset as u32).to_be_bytes());
directory.extend_from_slice(&(data.len() as u32).to_be_bytes());
data_section.extend_from_slice(data);
data_section.resize(data_section.len() + (padded_len(data.len()) - data.len()), 0);
offset += padded_len(data.len());
}
let mut out = Vec::with_capacity(offset);
out.extend_from_slice(b"OTTO");
out.extend_from_slice(&num_tables.to_be_bytes());
out.extend_from_slice(&search_range.to_be_bytes());
out.extend_from_slice(&entry_selector.to_be_bytes());
out.extend_from_slice(&range_shift.to_be_bytes());
out.extend_from_slice(&directory);
out.extend_from_slice(&data_section);
out
}
#[cfg(feature = "text_layout")]
fn get_glyph_width(font: &ParsedFont, gid: u16) -> Option<u16> {
font.get_or_decode_glyph(gid).map(|g| g.horz_advance)
}
#[cfg(feature = "text_layout")]
pub fn get_normalized_widths_ttf(font: &ParsedFont, glyph_ids: &[(u16, String)]) -> Vec<lopdf::Object> {
let mut widths_list = Vec::new();
let mut current_low_gid = 0;
let mut current_high_gid = 0;
let mut current_width_vec = Vec::new();
let percentage_font_scaling = 1000.0 / (font.pdf_font_metrics.units_per_em as f32);
for (gid, _) in glyph_ids {
let glyph_width = get_glyph_width(font, *gid)
.map(|w| (w as f32 * percentage_font_scaling) as i64)
.unwrap_or(0);
if current_width_vec.is_empty() {
current_low_gid = *gid;
current_high_gid = *gid;
current_width_vec.push(glyph_width);
} else if *gid == current_high_gid + 1 {
current_high_gid = *gid;
current_width_vec.push(glyph_width);
} else {
widths_list.push(lopdf::Object::Integer(current_low_gid as i64));
widths_list.push(lopdf::Object::Array(
current_width_vec.iter().map(|w| lopdf::Object::Integer(*w)).collect(),
));
current_low_gid = *gid;
current_high_gid = *gid;
current_width_vec = vec![glyph_width];
}
}
if !current_width_vec.is_empty() {
widths_list.push(lopdf::Object::Integer(current_low_gid as i64));
widths_list.push(lopdf::Object::Array(
current_width_vec.iter().map(|w| lopdf::Object::Integer(*w)).collect(),
));
}
widths_list
}
#[cfg(feature = "text_layout")]
pub fn get_normalized_widths_codes(
font: &ParsedFont,
entries: &[(u16, u16)],
) -> Vec<lopdf::Object> {
let percentage_font_scaling = 1000.0 / (font.pdf_font_metrics.units_per_em as f32);
let mut widths_list = Vec::new();
let mut current_low_code = 0u16;
let mut current_high_code = 0u16;
let mut current_width_vec: Vec<i64> = Vec::new();
for &(code, gid) in entries {
let glyph_width = get_glyph_width(font, gid)
.map(|w| (w as f32 * percentage_font_scaling) as i64)
.unwrap_or(0);
if current_width_vec.is_empty() {
current_low_code = code;
current_high_code = code;
current_width_vec.push(glyph_width);
} else if code == current_high_code + 1 {
current_high_code = code;
current_width_vec.push(glyph_width);
} else {
widths_list.push(lopdf::Object::Integer(current_low_code as i64));
widths_list.push(lopdf::Object::Array(
current_width_vec.iter().map(|w| lopdf::Object::Integer(*w)).collect(),
));
current_low_code = code;
current_high_code = code;
current_width_vec = vec![glyph_width];
}
}
if !current_width_vec.is_empty() {
widths_list.push(lopdf::Object::Integer(current_low_code as i64));
widths_list.push(lopdf::Object::Array(
current_width_vec.iter().map(|w| lopdf::Object::Integer(*w)).collect(),
));
}
widths_list
}
pub const FONT_B64_START: &str = "data:font/ttf;base64,";
#[cfg(all(test, feature = "text_layout"))]
mod test {
use std::collections::BTreeMap;
use crate::*;
pub const WIN_1252: &[char; 214] = &[
'!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2',
'3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D',
'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'{', '|', '}', '~', '€', '‚', 'ƒ', '„', '…', '†', '‡', 'ˆ', '‰', 'Š', '‹', 'Œ', 'Ž', '‘',
'’', '“', '•', '–', '—', '˜', '™', 'š', '›', 'œ', 'ž', 'Ÿ', '¡', '¢', '£', '¤', '¥', '¦',
'§', '¨', '©', 'ª', '«', '¬', '®', '¯', '°', '±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹',
'º', '»', '¼', '½', '¾', '¿', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë',
'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý',
'Þ', 'ß', 'à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï',
'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', '÷', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ',
];
const FONTS: &[(BuiltinFont, &[u8])] = &[
(
BuiltinFont::Courier,
include_bytes!("../examples/assets/fonts/Courier.ttf"),
),
(
BuiltinFont::CourierOblique,
include_bytes!("../examples/assets/fonts/Courier-Oblique.ttf"),
),
(
BuiltinFont::CourierBold,
include_bytes!("../examples/assets/fonts/Courier-Bold.ttf"),
),
(
BuiltinFont::CourierBoldOblique,
include_bytes!("../examples/assets/fonts/Courier-BoldOblique.ttf"),
),
(
BuiltinFont::Helvetica,
include_bytes!("../examples/assets/fonts/Helvetica.ttf"),
),
(
BuiltinFont::HelveticaBold,
include_bytes!("../examples/assets/fonts/Helvetica-Bold.ttf"),
),
(
BuiltinFont::HelveticaOblique,
include_bytes!("../examples/assets/fonts/Helvetica-Oblique.ttf"),
),
(
BuiltinFont::HelveticaBoldOblique,
include_bytes!("../examples/assets/fonts/Helvetica-BoldOblique.ttf"),
),
(
BuiltinFont::Symbol,
include_bytes!("../examples/assets/fonts/PDFASymbol.woff2"),
),
(
BuiltinFont::TimesRoman,
include_bytes!("../examples/assets/fonts/Times.ttf"),
),
(
BuiltinFont::TimesBold,
include_bytes!("../examples/assets/fonts/Times-Bold.ttf"),
),
(
BuiltinFont::TimesItalic,
include_bytes!("../examples/assets/fonts/Times-Oblique.ttf"),
),
(
BuiltinFont::TimesBoldItalic,
include_bytes!("../examples/assets/fonts/Times-BoldOblique.ttf"),
),
(
BuiltinFont::ZapfDingbats,
include_bytes!("../examples/assets/fonts/ZapfDingbats.ttf"),
),
];
#[test]
#[ignore = "regenerates bundled subset fonts + FONTS table; run manually with --ignored"]
fn subset_test() {
use std::collections::BTreeSet;
let charmap: BTreeSet<char> = WIN_1252.iter().copied().collect();
let mut target_map = vec![];
let mut tm2 = BTreeMap::new();
for (name, bytes) in FONTS {
let mut warnings = Vec::new();
let font = ParsedFont::from_bytes(bytes, 0, &mut warnings).unwrap();
let glyph_ids: Vec<(u16, char)> = charmap.iter()
.filter_map(|&ch| font.lookup_glyph_index(ch as u32).map(|gid| (gid, ch)))
.collect();
let (subset_bytes, glyph_mapping) = font.subset(&glyph_ids, azul_layout::CmapTarget::Unicode).unwrap();
let glyph_mapping = glyph_mapping
.into_iter()
.map(|(k, (g, c))| (k, (g, c.to_string())))
.collect();
let subset = crate::font::SubsetFont { bytes: subset_bytes, glyph_mapping };
tm2.insert(name.clone(), subset.bytes.len());
let _ = std::fs::write(
format!(
"{}/defaultfonts/{}.subset.ttf",
env!("CARGO_MANIFEST_DIR"),
name.get_id()
),
crate::utils::compress(&subset.bytes),
);
for (old_gid, (new_gid, char)) in subset.glyph_mapping.iter() {
let ch = char.chars().next().unwrap_or('\u{FFFD}');
target_map.push(format!(
" ({}, {old_gid}, {new_gid}, '{c}'),",
name.get_num(),
c = if ch == '\'' {
"\\'".to_string()
} else if ch == '\\' {
"\\\\".to_string()
} else {
ch.to_string()
}
));
}
}
let mut tm = vec![format!(
"const FONTS: &[(usize, u16, u16, char);{}] = &[",
target_map.len()
)];
tm.append(&mut target_map);
tm.push("];".to_string());
tm.push("fn match_len(bytes: &[u8]) -> Option<BuiltinFont> {".to_string());
tm.push("match bytes.len() {".to_string());
for (f, b) in tm2.iter() {
tm.push(format!("{b} => Some(BuiltinFont::{f:?}),"));
}
tm.push("_ => None,".to_string());
tm.push("}".to_string());
tm.push("}".to_string());
let _ = std::fs::write(
format!("{}/defaultfonts/mapping.rs", env!("CARGO_MANIFEST_DIR")),
tm.join("\r\n"),
);
}
}
#[cfg(all(test, feature = "text_layout"))]
mod bare_cff_wrap_test {
use super::{extract_cid_keyed_cff, wrap_bare_cff_as_sfnt};
const MOCK_CFF_CID: &[u8] = include_bytes!("../tests/assets/fonts/mock/mock_cff_cid.otf");
const MOCK_CFF_CID_FM: &[u8] =
include_bytes!("../tests/assets/fonts/mock/mock_cff_cid_fm.otf");
fn table(sfnt: &[u8], tag: &[u8; 4]) -> (usize, usize) {
let num_tables = u16::from_be_bytes([sfnt[4], sfnt[5]]) as usize;
for rec in 0..num_tables {
let off = 12 + 16 * rec;
if &sfnt[off..off + 4] == tag {
let start =
u32::from_be_bytes(sfnt[off + 8..off + 12].try_into().unwrap()) as usize;
let len =
u32::from_be_bytes(sfnt[off + 12..off + 16].try_into().unwrap()) as usize;
return (start, len);
}
}
panic!("table {} missing", String::from_utf8_lossy(tag));
}
fn wrap(font: &[u8]) -> Vec<u8> {
let bare = extract_cid_keyed_cff(font, 0).expect("mock font must be CID-keyed CFF");
wrap_bare_cff_as_sfnt(&bare).expect("bare CFF must wrap")
}
fn upm(sfnt: &[u8]) -> u16 {
let (off, _) = table(sfnt, b"head");
u16::from_be_bytes([sfnt[off + 18], sfnt[off + 19]])
}
fn advance(sfnt: &[u8], gid: usize) -> u16 {
let (off, len) = table(sfnt, b"hmtx");
assert!(4 * gid + 2 <= len, "gid {gid} beyond hmtx");
u16::from_be_bytes([sfnt[off + 4 * gid], sfnt[off + 4 * gid + 1]])
}
#[test]
fn synthetic_sfnt_upm_follows_font_matrix() {
assert_eq!(upm(&wrap(MOCK_CFF_CID)), 1000);
assert_eq!(upm(&wrap(MOCK_CFF_CID_FM)), 2000);
}
#[test]
fn width_scanner_evaluates_escape_arithmetic() {
let w = wrap(MOCK_CFF_CID_FM);
assert_eq!(advance(&w, 3), 700);
assert_eq!(advance(&w, 0), 1000); assert_eq!(advance(&w, 1), 500); assert_eq!(advance(&w, 2), 600); assert_eq!(advance(&w, 11), 1500); }
}