Embed, subset, and detect fonts in PDFs

This guide shows developers how to use PDFluent to manage fonts in PDF documents, ensuring compatibility and optimising file size.

Subset embedded fonts to reduce PDF size in Rust

Strip unused glyphs from embedded fonts so the PDF only carries the characters that actually appear in the document.

rust
use pdfluent::PdfDocument;

fn main() -> pdfluent::Result<()> {
    let mut doc = PdfDocument::open("report.pdf")?;
    let _report = doc.subset_fonts()?;
    doc.save("subset.pdf")?;
    Ok(())
}
  1. Open the PDF and inspect font usage

    Before subsetting, you can list embedded fonts and their sizes to understand what will change.

    rust
    use pdfluent::PdfDocument;
    
    let mut doc = PdfDocument::open("input.pdf")?;
    println!("{} pages", doc.page_count());
  2. Build subsetting options

    FontSubsetOptions controls which font types are processed. Type 1 fonts often have limited glyph sets already; focus on TrueType and OpenType where gains are largest.

    rust
    // subset_fonts() removes unused glyphs from embedded fonts.
    // It is core-tier and takes no configuration in 1.0.
  3. Run the subsetter

    PDFluent scans every page content stream, collects the Unicode codepoints actually used, then rewrites each embedded font to contain only those glyphs.

    rust
    let report = doc.subset_fonts()?;
    println!(
        "Subsetted {} of {} fonts, saved {} bytes",
        report.fonts_subsetted, report.fonts_processed, report.bytes_saved,
    );
  4. Compare sizes

    Check the font sizes again after subsetting to measure the reduction.

    rust
    // The FontSubsetReport summarises the result:
    //   fonts_processed - fonts_subsetted - bytes_saved
    println!("bytes saved: {}", report.bytes_saved);
  5. Save the output

    Write the subsetted file. Combine with compress_streams() for maximum size reduction.

    rust
    use pdfluent::CompressOptions;
    
    doc.compress(CompressOptions::strict())?;
    doc.save("subsetted.pdf")?;
  • A font with 80,000 glyphs used for two characters will be reduced from several MB to a few KB after subsetting.
  • Subsetting marks the font with a 6-character prefix tag (e.g. ABCDEF+FontName) per the PDF spec.
  • Subsetting is safe for archiving: PDF/A-3 conformance allows subset fonts.
  • Do not subset fonts in documents where end users may add text later; they would need the full font to type new characters.

Detect missing or unembedded fonts in a PDF in Rust

Scan every page resource dictionary and identify fonts that are referenced but not embedded in the file.

rust
use pdfluent::PdfDocument;

fn main() -> pdfluent::Result<()> {
    let doc = PdfDocument::open("input.pdf")?;

    let missing: Vec<_> = doc
        .fonts()
        .filter(|f| !f.is_embedded() && !f.is_standard_14())
        .collect();

    if missing.is_empty() {
        println!("All fonts are embedded.");
    } else {
        for font in &missing {
            println!("Missing: {} ({:?})", font.name(), font.font_type());
        }
        std::process::exit(1);
    }

    Ok(())
}
  1. Open the PDF in read mode

    For an audit task you only need a read-only Document.

    rust
    let doc = PdfDocument::open("input.pdf")?;
  2. Iterate over all font references

    doc.fonts() returns an iterator over all font dictionaries referenced from any page resource dictionary in the document.

    rust
    for font in doc.fonts() {
        println!(
            "name={} type={:?} embedded={} standard14={}",
            font.name(),
            font.font_type(),
            font.is_embedded(),
            font.is_standard_14(),
        );
    }
  3. Filter for unembedded non-standard fonts

    Standard 14 fonts (Helvetica, Times-Roman, Courier, etc.) are provided by PDF viewers and do not need embedding. All other fonts should be embedded for reliable rendering.

    rust
    let unembedded: Vec<_> = doc
        .fonts()
        .filter(|f| !f.is_embedded() && !f.is_standard_14())
        .collect();
  4. Print a per-page report

    To know which page each font appears on, iterate page by page.

    rust
    for (i, page) in doc.pages().enumerate() {
        for font in page.fonts() {
            if !font.is_embedded() && !font.is_standard_14() {
                println!("Page {}: unembedded font {}", i + 1, font.name());
            }
        }
    }
  5. Return a non-zero exit code for CI gating

    Use the missing font list to fail a CI pipeline when required fonts are absent.

    rust
    if !unembedded.is_empty() {
        eprintln!("{} unembedded font(s) found", unembedded.len());
        std::process::exit(1);
    }
  • A font can be present in the resource dictionary but have an empty or missing font program stream. is_embedded() checks for the stream, not just the dictionary entry.
  • Subset fonts are still considered embedded. The 6-character prefix tag does not affect the is_embedded check.
  • Type3 fonts (custom glyph shapes) are always "embedded" by definition since the glyph procedures are in the PDF itself.
  • Combine this check with the PDF/A validator for a full compliance audit.