use super::*;
use crate::pdf::OpenTypeFont;
use crate::pdf::font::{CHANGED, Subset, SubsetOutput, has_code, mark_code};
use std::io::Write;
const BMP_BITMAP_BYTES: usize = 8192;
const MAX_PAGE_FONTS: usize = 128;
#[derive(Clone, Copy)]
enum GlyphText {
Character,
Decoration,
Replacement(char),
}
pub struct FontObject {
object: ObjectId,
slot: usize,
characters: Vec<u8>,
}
impl FontObject {
pub fn supports(&self, character: char) -> bool {
let code = character as usize;
code <= 0xffff && has_code(&self.characters, code)
}
}
pub(super) struct PendingFont {
ids: [ObjectId; 5],
mapping: Option<[ObjectId; 2]>,
name: String,
fingerprint: [u8; 32],
used: Vec<u8>,
embedded: bool,
}
struct FontStream<'w, 'a, W: Write, C: Cancellation> {
writer: &'w mut PdfWriter<'a, W, C>,
deflate: &'w mut Deflate,
}
impl<'w, 'a, W: Write, C: Cancellation> FontStream<'w, 'a, W, C> {
fn new(writer: &'w mut PdfWriter<'a, W, C>, deflate: &'w mut Deflate) -> Self {
Self { writer, deflate }
}
fn finish(self) -> Result<()> {
self.deflate.finish(self.writer)?;
self.writer.end_stream()
}
}
impl<W: Write, C: Cancellation> SubsetOutput for FontStream<'_, '_, W, C> {
fn put(&mut self, bytes: &[u8]) -> Result<()> {
self.deflate.put(self.writer, bytes)
}
}
pub(super) fn ensure_fonts_embedded(fonts: &[PendingFont]) -> Result<()> {
if fonts.iter().any(|font| !font.embedded) {
return Err(Error::invalid(
"PDF font was added but its subset was not embedded",
));
}
Ok(())
}
fn changed() -> Error {
Error::invalid(CHANGED)
}
impl<'a, W: Write, C: Cancellation> PdfDocument<'a, W, C> {
pub fn add_font<S: RangedSource>(&mut self, font: &OpenTypeFont<'_, S>) -> Result<FontObject> {
self.ensure_image_page_intact()?;
let name = font.postscript_name()?.replace('#', "#23");
let face = font.face()?;
let mut characters = self.bitmap("PDF font character bitmap")?;
for code in 0..65536_u32 {
if bmp_glyph(&face, code)?.is_some() {
mark_code(&mut characters, code as usize);
}
}
let used = self.bitmap("PDF font used-character bitmap")?;
let refused = self
.limits
.allocation_refused("PDF fonts", size_of::<PendingFont>() as u64);
reserve(&mut self.fonts, 1, refused)?;
let cff = font.is_cff();
self.writer.prepare_objects(5 + 2 * usize::from(!cff))?;
let mut next = || self.writer.reserve_object();
let ids = [next()?, next()?, next()?, next()?, next()?];
let mapping = if cff { None } else { Some([next()?, next()?]) };
self.fonts.push(PendingFont {
ids,
mapping,
name,
fingerprint: font.fingerprint(),
used,
embedded: false,
});
Ok(FontObject {
object: ids[4],
slot: self.fonts.len() - 1,
characters,
})
}
fn font(&self, handle: &FontObject) -> Result<&PendingFont> {
self.fonts
.get(handle.slot)
.ok_or(Error::invalid("PDF font was not added to this document"))
}
fn count_font_input(&mut self, bytes: u64) -> Result<()> {
let total = self.input_bytes_read.saturating_add(bytes);
self.limits.check_input_size(total)?;
self.input_bytes_read = total;
Ok(())
}
fn take_deflate(&mut self) -> Result<Deflate> {
self.deflate
.take()
.map_or_else(|| Deflate::new(self.limits), Ok)
}
fn bitmap(&self, resource: &'static str) -> Result<Vec<u8>> {
self.limits.check_allocation(BMP_BITMAP_BYTES as u64)?;
let mut bitmap = Vec::new();
let refused = self
.limits
.allocation_refused(resource, BMP_BITMAP_BYTES as u64);
reserve_exact(&mut bitmap, BMP_BITMAP_BYTES, refused)?;
bitmap.resize(BMP_BITMAP_BYTES, 0);
Ok(bitmap)
}
pub fn embed_font<S: RangedSource>(
&mut self,
handle: &FontObject,
font: &mut OpenTypeFont<'_, S>,
) -> Result<()> {
self.ensure_image_page_intact()?;
self.writer.ensure_idle()?;
let pending = self.font(handle)?;
if pending.embedded {
return Err(Error::invalid("PDF font subset is already embedded"));
}
if font.fingerprint() != pending.fingerprint {
return Err(changed());
}
let [file, file_length, descriptor, descendant, object] = pending.ids;
let mapping = pending.mapping;
let before = font.subset_bytes_read();
let plan = font.subset(
&pending.used,
MAX_PDF_INTEGER,
self.limits,
self.cancellation,
)?;
self.count_font_input(font.subset_bytes_read() - before)?;
let pending = &self.fonts[handle.slot];
let name = format!("{}+{}", plan.tag(&pending.name), pending.name);
let mut deflate = self.take_deflate()?;
let before = font.subset_bytes_read();
self.image_page_failed = true;
let unicode = self.unicode_cmap(&mut deflate)?;
let (program, file_key, subtype) = match &plan {
Subset::TrueType(_) => (
format!("/Length1 {}", plan.length()),
"FontFile2",
"CIDFontType2",
),
Subset::Cff(_) => (
"/Subtype /CIDFontType0C".to_owned(),
"FontFile3",
"CIDFontType0",
),
};
self.writer.begin_stream(
file,
file_length,
format!("{program}\n/Filter /FlateDecode").as_bytes(),
)?;
let mut stream = FontStream::new(&mut self.writer, &mut deflate);
font.write(&plan, &mut stream, self.limits, self.cancellation)?;
stream.finish()?;
self.count_font_input(font.subset_bytes_read() - before)?;
let face = font.face()?;
let mut cid_map = String::new();
if let (Subset::TrueType(plan), Some([mapping, mapping_length])) = (&plan, mapping) {
self.writer
.begin_stream(mapping, mapping_length, b"/Filter /FlateDecode")?;
let mut stream = FontStream::new(&mut self.writer, &mut deflate);
let used = &self.fonts[handle.slot].used;
let last = (0..65536).rev().find(|code| has_code(used, *code));
for code in last.map_or(0..0, |last| 0..last + 1) {
let glyph = char::from_u32(code as u32)
.filter(|_| has_code(used, code))
.map_or(0, |character| plan.glyph(&face, character));
stream.put(&glyph.to_be_bytes())?;
}
stream.finish()?;
cid_map = format!(" /CIDToGIDMap {} 0 R", mapping.number());
}
let scale = 1000.0 / f64::from(face.units_per_em());
let bbox = face.global_bounding_box();
let flags = 4 | u32::from(face.is_monospaced()) | if face.is_italic() { 64 } else { 0 };
let desc = format!(
"<< /Type /FontDescriptor /FontName /{name} /Flags {flags} /FontBBox [{} {} {} {}] /ItalicAngle {} /Ascent {} /Descent {} /CapHeight {} /StemV 0 /{file_key} {} 0 R >>",
f64::from(bbox.x_min) * scale,
f64::from(bbox.y_min) * scale,
f64::from(bbox.x_max) * scale,
f64::from(bbox.y_max) * scale,
face.italic_angle(),
f64::from(face.ascender()) * scale,
f64::from(face.descender()) * scale,
f64::from(face.capital_height().unwrap_or(face.ascender())) * scale,
file.number()
);
self.writer.write_object(descriptor, desc.as_bytes())?;
self.writer.begin_object(descendant)?;
self.writer.write_bytes(format!("<< /Type /Font /Subtype /{subtype} /BaseFont /{name} /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor {} 0 R{cid_map} /DW 1000 /W [", descriptor.number()).as_bytes())?;
let used = &self.fonts[handle.slot].used;
for first in (0..65536).step_by(256) {
let mut drawn = (first..first + 256).filter(|code| has_code(used, *code));
let Some(low) = drawn.next() else {
continue;
};
let high = drawn.next_back().unwrap_or(low);
let mut widths = format!(" {low} [");
for code in low..=high {
let glyph = has_code(used, code)
.then(|| bmp_glyph(&face, code as u32))
.transpose()?
.flatten();
let width = glyph.map_or(1000.0, |glyph| f64::from(glyph.advance) * scale);
widths += &format!(" {width}");
}
widths.push_str(" ]");
self.writer.write_bytes(widths.as_bytes())?;
}
self.writer.write_bytes(b" ] >>")?;
self.writer.end_object()?;
self.writer.write_object(object, format!("<< /Type /Font /Subtype /Type0 /BaseFont /{name} /Encoding /Identity-H /DescendantFonts [{} 0 R] /ToUnicode {} 0 R >>", descendant.number(), unicode.number()).as_bytes())?;
self.fonts[handle.slot].embedded = true;
self.deflate = Some(deflate);
self.image_page_failed = false;
Ok(())
}
fn unicode_cmap(&mut self, deflate: &mut Deflate) -> Result<ObjectId> {
if let Some(unicode) = self.to_unicode {
return Ok(unicode);
}
self.writer.prepare_objects(2)?;
let unicode = self.writer.reserve_object()?;
let length = self.writer.reserve_object()?;
self.writer
.begin_stream(unicode, length, b"/Filter /FlateDecode")?;
let mut stream = FontStream::new(&mut self.writer, deflate);
let mut cmap = String::from(
"/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n/CMapName /CajUnicode def\n/CMapType 2 def\n1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n",
);
for first in (0..256_u32).step_by(32) {
let blocks = (first..first + 32).filter(|high| !(0xd8..=0xdf).contains(high));
cmap += &format!("{} beginbfrange\n", blocks.clone().count());
for high in blocks {
cmap += &format!("<{high:02X}00> <{high:02X}FF> <{high:02X}00>\n");
}
cmap += "endbfrange\n";
}
cmap += "endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n";
stream.put(cmap.as_bytes())?;
stream.finish()?;
self.to_unicode = Some(unicode);
Ok(unicode)
}
pub fn begin_content_page<'d, 'r>(
&'d mut self,
page: PageSpec,
fonts: &'r [&'r FontObject],
images: &'r [ImageObject],
) -> Result<ContentPageWriter<'d, 'a, 'r, W, C>> {
self.ensure_image_page_intact()?;
self.writer.ensure_idle()?;
let width = pdf_page_number(page.width_points)?;
let height = pdf_page_number(page.height_points)?;
if fonts.len() > MAX_PAGE_FONTS || images.len() > MAX_PAGE_IMAGE_PLACEMENTS {
return Err(Error::invalid("too many resources for a PDF content page"));
}
for font in fonts {
if self.font(font)?.embedded {
return Err(Error::invalid("PDF font subset is already embedded"));
}
}
self.check_next_page()?;
let deflate = self.take_deflate()?;
self.reserve_page_index_slot()?;
self.prepare_page_objects()?;
self.image_page_failed = true;
self.ensure_leaf()?;
let content = self.writer.reserve_object()?;
let length = self.writer.reserve_object()?;
let page = self.writer.reserve_object()?;
self.writer
.begin_stream(content, length, b"/Filter /FlateDecode")?;
Ok(ContentPageWriter {
deflate,
document: self,
fonts,
images,
width,
height,
content,
page,
multiply_images: false,
failed: false,
})
}
}
fn bmp_glyph(
face: &xberg_ttf_parser::Face<'_>,
code: u32,
) -> Result<Option<crate::pdf::FontGlyph>> {
let Some(character) = char::from_u32(code) else {
return Ok(None);
};
let Some(id) = face.glyph_index(character).filter(|id| id.0 != 0) else {
return Ok(None);
};
if id.0 >= face.number_of_glyphs() {
return Err(Error::invalid(
"font character map references an invalid glyph",
));
}
let advance = face
.glyph_hor_advance(id)
.ok_or(Error::invalid("font glyph has no horizontal advance"))?;
Ok(Some(crate::pdf::FontGlyph { id: id.0, advance }))
}
pub struct ContentPageWriter<'d, 'a, 'r, W: Write, C: Cancellation> {
document: &'d mut PdfDocument<'a, W, C>,
fonts: &'r [&'r FontObject],
images: &'r [ImageObject],
width: String,
height: String,
content: ObjectId,
page: ObjectId,
deflate: Deflate,
multiply_images: bool,
failed: bool,
}
impl<W: Write, C: Cancellation> ContentPageWriter<'_, '_, '_, W, C> {
fn emit(&mut self, bytes: &[u8]) -> Result<()> {
self.deflate.put(&mut self.document.writer, bytes)
}
fn start_draw(&mut self) -> Result<()> {
if self.failed {
return Err(Error::invalid(
"PDF content page cannot continue after a failed draw",
));
}
self.failed = true;
self.document.writer.write_stream_bytes(&[])
}
pub fn glyph(&mut self, font: usize, character: char, transform: [f64; 6]) -> Result<()> {
self.draw_glyph(font, character, transform, None, None, GlyphText::Character)
}
pub fn glyph_with_gray(
&mut self,
font: usize,
character: char,
transform: [f64; 6],
gray: u8,
) -> Result<()> {
self.draw_glyph(
font,
character,
transform,
Some(gray),
None,
GlyphText::Character,
)
}
pub(crate) fn glyph_with_replacement_text(
&mut self,
font: usize,
alias: char,
transform: [f64; 6],
gray: u8,
character: char,
) -> Result<()> {
self.draw_glyph(
font,
alias,
transform,
Some(gray),
None,
GlyphText::Replacement(character),
)
}
pub fn glyph_with_clip(
&mut self,
font: usize,
character: char,
transform: [f64; 6],
clip: [f64; 4],
) -> Result<()> {
self.draw_glyph(
font,
character,
transform,
None,
Some(clip),
GlyphText::Character,
)
}
pub fn decoration_glyph(
&mut self,
font: usize,
character: char,
transform: [f64; 6],
clip: [f64; 4],
) -> Result<()> {
self.draw_glyph(
font,
character,
transform,
None,
Some(clip),
GlyphText::Decoration,
)
}
fn draw_glyph(
&mut self,
font: usize,
character: char,
transform: [f64; 6],
gray: Option<u8>,
clip: Option<[f64; 4]>,
text: GlyphText,
) -> Result<()> {
self.start_draw()?;
let resource = self
.fonts
.get(font)
.ok_or(Error::invalid("PDF page font index is out of range"))?;
if !resource.supports(character) {
return Err(Error::invalid(
"PDF font has no supported BMP glyph for the character",
));
}
mark_code(
&mut self.document.fonts[resource.slot].used,
character as usize,
);
let matrix = decimals(&transform)?;
if gray.is_some() || clip.is_some() {
self.emit(b"q ")?;
}
if let Some([left, bottom, width, height]) = clip {
decimals(&[left, bottom, width, height, left + width, bottom + height])?;
if width <= 0.0 || height <= 0.0 {
return Err(Error::invalid(
"PDF glyph clipping extents must be positive",
));
}
self.emit(format!("{left} {bottom} {width} {height} re W n\n").as_bytes())?;
}
if let Some(gray) = gray {
self.emit(format!("{:.6} g\n", f64::from(gray) / 255.0).as_bytes())?;
}
match text {
GlyphText::Character => (),
GlyphText::Decoration => {
self.emit(b"/Artifact BMC\n/Span << /ActualText () >> BDC\n")?
}
GlyphText::Replacement(character) => {
self.emit(b"/Span << /ActualText <FEFF")?;
for unit in character.encode_utf16(&mut [0; 2]) {
self.emit(format!("{unit:04X}").as_bytes())?;
}
self.emit(b"> >> BDC\n")?;
}
}
self.emit(format!("BT /F{font} 1 Tf\n").as_bytes())?;
self.emit(matrix.as_bytes())?;
self.emit(format!(" Tm <{:04X}> Tj ET\n", character as u32).as_bytes())?;
match text {
GlyphText::Character => (),
GlyphText::Decoration => self.emit(b"EMC\nEMC\n")?,
GlyphText::Replacement(_) => {
self.emit(b"EMC\n")?;
self.document.substituted_glyphs += 1;
}
}
if gray.is_some() || clip.is_some() {
self.emit(b"Q\n")?;
}
self.failed = false;
Ok(())
}
pub fn image(&mut self, index: usize, transform: [f64; 6]) -> Result<()> {
self.draw_image(index, transform, false)
}
pub(crate) fn image_is_bilevel(&self, index: usize) -> bool {
self.images.get(index).is_some_and(|image| image.bilevel)
}
pub(crate) fn bilevel_overlay(&mut self, index: usize, transform: [f64; 6]) -> Result<()> {
self.draw_image(index, transform, true)
}
fn draw_image(&mut self, index: usize, transform: [f64; 6], overlay: bool) -> Result<()> {
self.start_draw()?;
if index >= self.images.len() {
return Err(Error::invalid("PDF page image index is out of range"));
}
if overlay && !self.image_is_bilevel(index) {
return Err(Error::invalid(
"PDF image overlay requires a bilevel resource",
));
}
let matrix = decimals(&transform)?;
self.emit(b"q\n")?;
if overlay {
self.multiply_images = true;
self.emit(b"/BilevelOverlay gs\n")?;
}
self.emit(matrix.as_bytes())?;
self.emit(format!(" cm /Im{index} Do Q\n").as_bytes())?;
self.failed = false;
Ok(())
}
pub fn segment(&mut self, from: [f64; 2], to: [f64; 2], width: f64) -> Result<()> {
self.start_draw()?;
decimals(&[from[0], from[1], to[0], to[1], width, 0.0])?;
if width < 0.0 {
return Err(Error::invalid("PDF stroke width must be nonnegative"));
}
let command = format!(
"q {width} w {} {} m {} {} l S Q\n",
from[0], from[1], to[0], to[1]
);
self.emit(command.as_bytes())?;
self.failed = false;
Ok(())
}
pub(crate) fn stroke_polyline(
&mut self,
points: &[[f64; 2]],
width: f64,
gray: u8,
) -> Result<()> {
self.start_draw()?;
if !(2..=8).contains(&points.len()) || width < 0.0 {
return Err(Error::invalid(
"PDF polyline requires two to eight points and nonnegative width",
));
}
let width = decimals(&[width])?;
self.emit(b"q 0 J 0 j 10 M ")?;
self.emit(width.as_bytes())?;
self.emit(format!(" w {:.6} G\n", f64::from(gray) / 255.0).as_bytes())?;
for (index, point) in points.iter().enumerate() {
self.emit(decimals(point)?.as_bytes())?;
let operator = if index == 0 { b" m\n" } else { b" l\n" };
self.emit(operator)?;
}
self.emit(b"S Q\n")?;
self.failed = false;
Ok(())
}
pub fn fill_polygon(&mut self, points: &[[f64; 2]]) -> Result<()> {
self.start_draw()?;
if !(3..=8).contains(&points.len()) {
return Err(Error::invalid(
"PDF polygon must have three to eight vertices",
));
}
self.emit(b"q 0 g\n")?;
for (index, point) in points.iter().enumerate() {
self.emit(decimals(point)?.as_bytes())?;
let operator = if index == 0 { b" m\n" } else { b" l\n" };
self.emit(operator)?;
}
self.emit(b"h f Q\n")?;
self.failed = false;
Ok(())
}
pub fn finish(self) -> Result<u32> {
if self.failed {
return Err(Error::invalid(
"PDF content page cannot finish after a failed draw",
));
}
let document = self.document;
let mut deflate = self.deflate;
deflate.finish(&mut document.writer)?;
document.writer.end_stream()?;
document.deflate = Some(deflate);
let parent = document
.leaf
.as_ref()
.ok_or(Error::invalid("PDF page-tree leaf is missing"))?
.id;
document.writer.begin_object(self.page)?;
document.writer.write_bytes(
format!(
"<< /Type /Page /Parent {} 0 R /MediaBox [0 0 {} {}] /Resources << /Font <<",
parent.number(),
self.width,
self.height
)
.as_bytes(),
)?;
for (index, font) in self.fonts.iter().enumerate() {
document
.writer
.write_bytes(format!(" /F{index} {} 0 R", font.object.number()).as_bytes())?;
}
document.writer.write_bytes(b" >> /XObject <<")?;
for (index, image) in self.images.iter().enumerate() {
document
.writer
.write_bytes(format!(" /Im{index} {} 0 R", image.object.number()).as_bytes())?;
}
if self.multiply_images {
document.writer.write_bytes(
b" >> /ExtGState << /BilevelOverlay << /Type /ExtGState /BM /Multiply >>",
)?;
}
document
.writer
.write_bytes(format!(" >> >> /Contents {} 0 R >>", self.content.number()).as_bytes())?;
document.writer.end_object()?;
let index = document.register_page(self.page)?;
document.image_page_failed = false;
Ok(index)
}
}
#[cfg(test)]
mod tests;