use std::collections::{BTreeMap, BTreeSet};
use pdf_writer::types::{CidFontType, FontFlags};
use pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str};
use pdf_writer::types::{SystemInfo, UnicodeCmap};
use rustyfi_backend::{
Color, DocExtras, FontKey, GraphicsElem, ImageResource, Page, PageGeometry, PureHorzBox,
};
use ttf_parser::{Face, GlyphId};
use crate::ttf::TtfFontStore;
use crate::{
form_res_name, image_res_name, place_embedded_block, place_form, place_graphics, place_image,
place_math, set_fill_color, used_images, write_annotations, write_document_info,
write_form_xobjects, write_image_xobjects, write_named_dests, write_outline, PdfError,
};
pub(crate) fn font_res_name(file_idx: usize) -> String {
format!("F{file_idx}")
}
#[derive(Default)]
struct FontUsage {
glyphs: BTreeMap<u16, char>,
missing: BTreeSet<char>,
}
fn report_missing_glyphs(usage: &BTreeMap<usize, FontUsage>, store: &TtfFontStore) {
for (&file_idx, file_usage) in usage {
if file_usage.missing.is_empty() {
continue;
}
let label = store.file_label(file_idx);
for &c in file_usage.missing.iter().take(MAX_MISSING_REPORTED) {
eprintln!(
" [Warning] No glyph is provided for U+{:04X} ({}) by font `{label}`; \
it is drawn as .notdef, which this face may render as nothing at all.",
c as u32,
c.escape_debug(),
);
}
let rest = file_usage.missing.len().saturating_sub(MAX_MISSING_REPORTED);
if rest > 0 {
eprintln!(
" [Warning] ... and {rest} more character(s) with no glyph in font \
`{label}` ({} distinct in all); the font is probably wrong for this text.",
file_usage.missing.len(),
);
}
}
}
const MAX_MISSING_REPORTED: usize = 20;
pub fn render_pdf_ttf(
geometry: &PageGeometry,
pages: &[Page],
store: &TtfFontStore,
images: &[ImageResource],
) -> Result<Vec<u8>, PdfError> {
render_pdf_ttf_with(geometry, pages, store, images, &DocExtras::default())
}
pub fn render_pdf_ttf_with(
geometry: &PageGeometry,
pages: &[Page],
store: &TtfFontStore,
images: &[ImageResource],
extras: &DocExtras,
) -> Result<Vec<u8>, PdfError> {
let paper_h = geometry.paper_height.0 as f32;
let mut usage: BTreeMap<usize, FontUsage> = BTreeMap::new();
for (i, page) in pages.iter().enumerate() {
let overlay = extras.page_graphics.get(i).map(|v| v.as_slice()).unwrap_or(&[]);
let _ = page_content(page, paper_h, store, &mut usage, overlay, images, &BTreeMap::new())?;
}
let mut cff_subsets: BTreeMap<usize, (Vec<u8>, subsetter::GlyphRemapper)> = BTreeMap::new();
for (&file_idx, file_usage) in &usage {
let Some(face) = store.face_by_file(file_idx) else {
continue;
};
let tables = face.tables();
if tables.glyf.is_none() && tables.cff.is_some() {
let glyphs: Vec<u16> = file_usage.glyphs.keys().copied().collect();
let remapper = subsetter::GlyphRemapper::new_from_glyphs_sorted(&glyphs);
if let Ok(bytes) = subsetter::subset(store.file_bytes(file_idx), 0, &remapper) {
cff_subsets.insert(file_idx, (bytes, remapper));
}
}
}
let cid_remaps: BTreeMap<usize, &subsetter::GlyphRemapper> =
cff_subsets.iter().map(|(&idx, (_, r))| (idx, r)).collect();
report_missing_glyphs(&usage, store);
let mut usage: BTreeMap<usize, FontUsage> = BTreeMap::new();
let mut page_contents = Vec::with_capacity(pages.len());
for (i, page) in pages.iter().enumerate() {
let overlay = extras.page_graphics.get(i).map(|v| v.as_slice()).unwrap_or(&[]);
page_contents.push(page_content(page, paper_h, store, &mut usage, overlay, images, &cid_remaps)?);
}
let mut pdf = Pdf::new();
let mut alloc: i32 = 1;
let next_ref = |alloc: &mut i32| {
let r = Ref::new(*alloc);
*alloc += 1;
r
};
let catalog_id = next_ref(&mut alloc);
let page_tree_id = next_ref(&mut alloc);
let mut type0_ids: BTreeMap<usize, Ref> = BTreeMap::new();
for &file_idx in usage.keys() {
type0_ids.insert(file_idx, next_ref(&mut alloc));
}
let used = used_images(pages, &extras.page_graphics);
let img_refs = write_image_xobjects(&mut pdf, || next_ref(&mut alloc), images, &used);
let form_refs = write_form_xobjects(&mut pdf, || next_ref(&mut alloc), images, &used);
let page_ids: Vec<Ref> = pages.iter().map(|_| next_ref(&mut alloc)).collect();
let content_ids: Vec<Ref> = pages.iter().map(|_| next_ref(&mut alloc)).collect();
let annot_refs =
write_annotations(&mut pdf, || next_ref(&mut alloc), &extras.annotations, pages.len());
let dests_id =
write_named_dests(&mut pdf, || next_ref(&mut alloc), &extras.destinations, &page_ids);
let outline_id = write_outline(&mut pdf, || next_ref(&mut alloc), &extras.outline);
if let Some(info) = &extras.doc_info {
let info_id = next_ref(&mut alloc);
write_document_info(&mut pdf, info_id, info);
}
{
let mut cat = pdf.catalog(catalog_id);
cat.pages(page_tree_id);
if let Some(d) = dests_id {
cat.destinations(d);
}
if let Some(o) = outline_id {
cat.outlines(o);
}
}
{
let mut tree = pdf.pages(page_tree_id);
tree.kids(page_ids.iter().copied());
tree.count(page_ids.len() as i32);
}
for (&file_idx, file_usage) in &usage {
write_font(
&mut pdf,
&mut alloc,
type0_ids[&file_idx],
store,
file_idx,
file_usage,
cff_subsets.get(&file_idx),
)?;
}
let media_box = Rect::new(0.0, 0.0, geometry.paper_width.0 as f32, paper_h);
for (i, ((&page_id, &content_id), content_bytes)) in
page_ids.iter().zip(&content_ids).zip(&page_contents).enumerate()
{
pdf.stream(content_id, content_bytes);
let mut p = pdf.page(page_id);
p.media_box(media_box);
p.parent(page_tree_id);
p.contents(content_id);
if let Some(refs) = annot_refs.get(&i) {
p.annotations(refs.iter().copied());
}
let mut resources = p.resources();
let mut fonts = resources.fonts();
let names: Vec<(String, Ref)> = type0_ids
.iter()
.map(|(&file_idx, &font_ref)| (font_res_name(file_idx), font_ref))
.collect();
for (name, font_ref) in &names {
fonts.pair(Name(name.as_bytes()), *font_ref);
}
fonts.finish();
if !img_refs.is_empty() || !form_refs.is_empty() {
let mut x_objects = resources.x_objects();
for (&id, &r) in &img_refs {
x_objects.pair(Name(image_res_name(id).as_bytes()), r);
}
for (&id, &r) in &form_refs {
x_objects.pair(Name(form_res_name(id).as_bytes()), r);
}
x_objects.finish();
}
resources.finish();
p.finish();
}
Ok(pdf.finish())
}
fn page_content(
page: &Page,
paper_h: f32,
store: &TtfFontStore,
usage: &mut BTreeMap<usize, FontUsage>,
overlay: &[GraphicsElem],
images: &[ImageResource],
cid_remaps: &BTreeMap<usize, &subsetter::GlyphRemapper>,
) -> Result<Vec<u8>, PdfError> {
let mut content = Content::new();
if !overlay.is_empty() {
place_graphics(&mut content, overlay, 0.0, 0.0, &mut |c, bx, x, y| {
emit_box(c, bx, x, y, store, usage, images, cid_remaps)
})?;
}
for line in &page.lines {
let y = paper_h - line.baseline_y.0 as f32;
for (dx, bx) in &line.contents {
emit_box(&mut content, bx, (line.x + *dx).0 as f32, y, store, usage, images, cid_remaps)?;
}
}
Ok(content.finish().into_vec())
}
fn emit_box(
content: &mut Content,
bx: &PureHorzBox,
tx: f32,
ty: f32,
store: &TtfFontStore,
usage: &mut BTreeMap<usize, FontUsage>,
images: &[ImageResource],
cid_remaps: &BTreeMap<usize, &subsetter::GlyphRemapper>,
) -> Result<(), PdfError> {
match bx {
PureHorzBox::InnerString { info, text, .. } => {
let file_idx = store.file_index(info.font);
let face = store
.face_by_file(file_idx)
.ok_or_else(|| PdfError::NoGlyph(text.chars().next().unwrap_or('\u{FFFD}')))?;
let file_usage = usage.entry(file_idx).or_default();
let encoded =
encode_glyph_run(&face, text, file_usage, cid_remaps.get(&file_idx).copied())?;
let colored = info.color != Color::Gray(0.0);
if colored {
content.save_state();
set_fill_color(content, info.color);
}
content.begin_text();
content.set_font(
Name(font_res_name(file_idx).as_bytes()),
info.size.0 as f32,
);
content.next_line(tx, ty + info.rising.0 as f32);
content.show(Str(&encoded));
content.end_text();
if colored {
content.restore_state();
}
}
PureHorzBox::Image {
width,
height,
image,
} => {
match images.get(image.0).and_then(|im| im.pdf.as_ref()) {
Some(pdf_res) => place_form(
content,
image.0,
tx,
ty,
width.0 as f32,
height.0 as f32,
pdf_res.media_box,
),
None => place_image(content, image.0, tx, ty, width.0 as f32, height.0 as f32),
}
}
PureHorzBox::Graphics { elems, origin_independent, .. } => {
let (ax, ay) = if *origin_independent { (0.0, 0.0) } else { (tx, ty) };
place_graphics(content, elems, ax, ay, &mut |c, bx, x, y| {
emit_box(c, bx, x, y, store, usage, images, cid_remaps)
})?;
}
PureHorzBox::Math { glyphs, rules, .. } => {
let name_for = |k: FontKey| font_res_name(store.file_index(k));
place_math(content, glyphs, tx, ty, &name_for, |g| {
let file_idx = store.file_index(g.info.font);
let remap = cid_remaps.get(&file_idx).copied();
let file_usage = usage.entry(file_idx).or_default();
match g.gid {
Some(gid) => {
file_usage
.glyphs
.entry(gid)
.or_insert(g.text.chars().next().unwrap_or('\u{FFFD}'));
let cid = remap.and_then(|r| r.get(gid)).unwrap_or(gid);
Ok(cid.to_be_bytes().to_vec())
}
None => {
let face = store.face_by_file(file_idx).ok_or_else(|| {
PdfError::NoGlyph(g.text.chars().next().unwrap_or('\u{FFFD}'))
})?;
encode_glyph_run(&face, &g.text, file_usage, remap)
}
}
})?;
place_graphics(content, rules, tx, ty, &mut |c, bx, x, y| {
emit_box(c, bx, x, y, store, usage, images, cid_remaps)
})?;
}
PureHorzBox::Tabular(tab) => {
for cell in &tab.cells {
for (cdx, cbx) in &cell.contents {
emit_box(
content,
cbx,
tx + (cell.x + *cdx).0 as f32,
ty + cell.baseline_y.0 as f32,
store,
usage,
images,
cid_remaps,
)?;
}
}
place_graphics(content, &tab.rules, tx, ty, &mut |c, bx, x, y| {
emit_box(c, bx, x, y, store, usage, images, cid_remaps)
})?;
}
PureHorzBox::EmbeddedBlock { block, anchor_last, .. } => {
place_embedded_block(block, tx, ty, *anchor_last, |cbx, x, y| {
emit_box(content, cbx, x, y, store, usage, images, cid_remaps)
})?;
}
PureHorzBox::Frame { contents, .. } => {
for (dx, cbx) in contents {
emit_box(content, cbx, tx + dx.0 as f32, ty, store, usage, images, cid_remaps)?;
}
}
_ => {}
}
Ok(())
}
fn encode_glyph_run(
face: &Face<'_>,
text: &str,
usage: &mut FontUsage,
remap: Option<&subsetter::GlyphRemapper>,
) -> Result<Vec<u8>, PdfError> {
let mut out = Vec::with_capacity(text.len() * 2);
for c in text.chars() {
let gid = face.glyph_index(c).unwrap_or_else(|| {
usage.missing.insert(c);
GlyphId(0)
});
usage.glyphs.entry(gid.0).or_insert(c);
let cid = remap.and_then(|r| r.get(gid.0)).unwrap_or(gid.0);
out.extend_from_slice(&cid.to_be_bytes());
}
Ok(out)
}
fn write_font(
pdf: &mut Pdf,
alloc: &mut i32,
type0_ref: Ref,
store: &TtfFontStore,
file_idx: usize,
usage: &FontUsage,
cff_subset: Option<&(Vec<u8>, subsetter::GlyphRemapper)>,
) -> Result<(), PdfError> {
let mut next_ref = || {
let r = Ref::new(*alloc);
*alloc += 1;
r
};
let cid_font_ref = next_ref();
let descriptor_ref = next_ref();
let font_file_ref = next_ref();
let to_unicode_ref = next_ref();
let c2g_ref = next_ref();
let face = store
.face_by_file(file_idx)
.expect("file_idx came from a successfully-loaded TtfFontStore");
let tables = face.tables();
if tables.glyf.is_none() && tables.cff.is_some() {
return write_font_cff(
pdf,
type0_ref,
cid_font_ref,
descriptor_ref,
font_file_ref,
to_unicode_ref,
store,
file_idx,
usage,
&face,
cff_subset,
);
}
let units_per_em = face.units_per_em() as f64;
let scale = |v: f64| (v * 1000.0 / units_per_em) as f32;
let glyphs: Vec<u16> = usage.glyphs.keys().copied().collect();
let subset: Option<(Vec<u8>, subsetter::GlyphRemapper)> = if face.tables().glyf.is_some() {
let remapper = subsetter::GlyphRemapper::new_from_glyphs_sorted(&glyphs);
subsetter::subset(store.file_bytes(file_idx), 0, &remapper)
.ok()
.map(|bytes| (bytes, remapper))
} else {
None
};
let base_name = match &subset {
Some(_) => format!("{}+{}", subset_tag(&glyphs), base_font_name(&face, file_idx)),
None => base_font_name(&face, file_idx),
};
let mut cmap = UnicodeCmap::new(
Name(b"Custom-UCS"),
SystemInfo {
registry: Str(b"Adobe"),
ordering: Str(b"UCS"),
supplement: 0,
},
);
for (&gid, &ch) in &usage.glyphs {
cmap.pair(gid, ch);
}
let cmap_bytes = cmap.finish();
pdf.cmap(to_unicode_ref, &cmap_bytes);
{
let mut t0 = pdf.type0_font(type0_ref);
t0.base_font(Name(base_name.as_bytes()));
t0.encoding_predefined(Name(b"Identity-H"));
t0.descendant_font(cid_font_ref);
t0.to_unicode(to_unicode_ref);
}
let widths: BTreeMap<u16, f32> = usage
.glyphs
.keys()
.map(|&gid| {
let advance = face.glyph_hor_advance(GlyphId(gid)).unwrap_or(0) as f64;
(gid, scale(advance))
})
.collect();
let default_width = if widths.is_empty() {
1000.0
} else {
widths.values().sum::<f32>() / widths.len() as f32
};
{
let mut cid = pdf.cid_font(cid_font_ref);
cid.subtype(CidFontType::Type2);
cid.base_font(Name(base_name.as_bytes()));
cid.system_info(SystemInfo {
registry: Str(b"Adobe"),
ordering: Str(b"Identity"),
supplement: 0,
});
cid.font_descriptor(descriptor_ref);
cid.default_width(default_width);
if subset.is_some() {
cid.cid_to_gid_map_stream(c2g_ref);
} else {
cid.cid_to_gid_map_predefined(Name(b"Identity"));
}
if !widths.is_empty() {
let mut w = cid.widths();
write_width_runs(&mut w, &widths);
w.finish();
}
}
{
let bbox_units = face.global_bounding_box();
let bbox = Rect::new(
scale(bbox_units.x_min as f64),
scale(bbox_units.y_min as f64),
scale(bbox_units.x_max as f64),
scale(bbox_units.y_max as f64),
);
let mut flags = FontFlags::empty();
if face.is_italic() {
flags |= FontFlags::ITALIC;
}
if face.is_monospaced() {
flags |= FontFlags::FIXED_PITCH;
}
flags |= FontFlags::SYMBOLIC;
let mut fd = pdf.font_descriptor(descriptor_ref);
fd.name(Name(base_name.as_bytes()));
fd.flags(flags);
fd.bbox(bbox);
fd.italic_angle(face.italic_angle());
fd.ascent(scale(face.ascender() as f64));
fd.descent(scale(face.descender() as f64));
let cap_height = face.capital_height().unwrap_or(face.ascender());
fd.cap_height(scale(cap_height as f64));
fd.stem_v(if face.is_bold() { 120.0 } else { 80.0 });
fd.font_file2(font_file_ref);
}
match &subset {
Some((bytes, _)) => pdf.stream(font_file_ref, bytes),
None => pdf.stream(font_file_ref, store.file_bytes(file_idx)),
};
if let Some((_, remapper)) = &subset {
let max_cid = usage.glyphs.keys().copied().max().unwrap_or(0);
let mut map_bytes = vec![0u8; 2 * (max_cid as usize + 1)];
for cid in 0..=max_cid {
let new_gid = remapper.get(cid).unwrap_or(0);
let at = 2 * cid as usize;
map_bytes[at..at + 2].copy_from_slice(&new_gid.to_be_bytes());
}
pdf.stream(c2g_ref, &map_bytes);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn write_font_cff(
pdf: &mut Pdf,
type0_ref: Ref,
cid_font_ref: Ref,
descriptor_ref: Ref,
font_file_ref: Ref,
to_unicode_ref: Ref,
store: &TtfFontStore,
file_idx: usize,
usage: &FontUsage,
face: &Face<'_>,
subset: Option<&(Vec<u8>, subsetter::GlyphRemapper)>,
) -> Result<(), PdfError> {
let units_per_em = face.units_per_em() as f64;
let scale = |v: f64| (v * 1000.0 / units_per_em) as f32;
let cid_of = |gid: u16| -> u16 { subset.and_then(|(_, r)| r.get(gid)).unwrap_or(gid) };
let glyphs: Vec<u16> = usage.glyphs.keys().copied().collect();
let base_name = match subset {
Some(_) => format!("{}+{}", subset_tag(&glyphs), base_font_name(face, file_idx)),
None => base_font_name(face, file_idx),
};
let mut cmap = UnicodeCmap::new(
Name(b"Custom-UCS"),
SystemInfo {
registry: Str(b"Adobe"),
ordering: Str(b"UCS"),
supplement: 0,
},
);
for (&gid, &ch) in &usage.glyphs {
cmap.pair(cid_of(gid), ch);
}
let cmap_bytes = cmap.finish();
pdf.cmap(to_unicode_ref, &cmap_bytes);
{
let mut t0 = pdf.type0_font(type0_ref);
t0.base_font(Name(base_name.as_bytes()));
t0.encoding_predefined(Name(b"Identity-H"));
t0.descendant_font(cid_font_ref);
t0.to_unicode(to_unicode_ref);
}
let widths: BTreeMap<u16, f32> = usage
.glyphs
.keys()
.map(|&gid| {
let advance = face.glyph_hor_advance(GlyphId(gid)).unwrap_or(0) as f64;
(cid_of(gid), scale(advance))
})
.collect();
let default_width = if widths.is_empty() {
1000.0
} else {
widths.values().sum::<f32>() / widths.len() as f32
};
{
let mut cid = pdf.cid_font(cid_font_ref);
cid.subtype(CidFontType::Type0);
cid.base_font(Name(base_name.as_bytes()));
cid.system_info(SystemInfo {
registry: Str(b"Adobe"),
ordering: Str(b"Identity"),
supplement: 0,
});
cid.font_descriptor(descriptor_ref);
cid.default_width(default_width);
if !widths.is_empty() {
let mut w = cid.widths();
write_width_runs(&mut w, &widths);
w.finish();
}
}
{
let bbox_units = face.global_bounding_box();
let bbox = Rect::new(
scale(bbox_units.x_min as f64),
scale(bbox_units.y_min as f64),
scale(bbox_units.x_max as f64),
scale(bbox_units.y_max as f64),
);
let mut flags = FontFlags::empty();
if face.is_italic() {
flags |= FontFlags::ITALIC;
}
if face.is_monospaced() {
flags |= FontFlags::FIXED_PITCH;
}
flags |= FontFlags::SYMBOLIC;
let mut fd = pdf.font_descriptor(descriptor_ref);
fd.name(Name(base_name.as_bytes()));
fd.flags(flags);
fd.bbox(bbox);
fd.italic_angle(face.italic_angle());
fd.ascent(scale(face.ascender() as f64));
fd.descent(scale(face.descender() as f64));
let cap_height = face.capital_height().unwrap_or(face.ascender());
fd.cap_height(scale(cap_height as f64));
fd.stem_v(if face.is_bold() { 120.0 } else { 80.0 });
fd.font_file3(font_file_ref);
}
match subset {
Some((bytes, _)) => {
pdf.stream(font_file_ref, bytes).pair(Name(b"Subtype"), Name(b"OpenType"));
}
None => {
pdf.stream(font_file_ref, store.file_bytes(file_idx))
.pair(Name(b"Subtype"), Name(b"OpenType"));
}
}
Ok(())
}
fn subset_tag(glyphs: &[u16]) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
glyphs.hash(&mut hasher);
let mut h = Hasher::finish(&hasher);
let mut tag = String::with_capacity(6);
for _ in 0..6 {
tag.push((b'A' + (h % 26) as u8) as char);
h /= 26;
}
tag
}
fn write_width_runs(w: &mut pdf_writer::writers::Widths<'_>, widths: &BTreeMap<u16, f32>) {
let mut iter = widths.iter().peekable();
while let Some((&start, &start_w)) = iter.next() {
let mut run = vec![start_w];
let mut prev = start;
while let Some(&(&next_gid, &next_w)) = iter.peek() {
if next_gid == prev + 1 {
run.push(next_w);
prev = next_gid;
iter.next();
} else {
break;
}
}
w.consecutive(start, run);
}
}
fn base_font_name(face: &Face<'_>, file_idx: usize) -> String {
for name in face.names() {
if name.is_unicode() {
if let Some(s) = name.to_string() {
if !s.is_empty() {
return s;
}
}
}
}
format!("EmbeddedTTF{file_idx}")
}