Skip to content

Font Subsetting

A fully embedded font program carries every glyph it ships with, which for a CJK or an emoji face can be several megabytes against a document that uses forty characters. Subsetting rewrites the embedded program so it keeps only the glyphs the document references. On text-heavy files with large embedded fonts this is usually the single largest saving available.

There are two entry points, and they operate at different scopes.

CallScope
optimize.Options{SubsetFonts: true}Every embedded font in the document being written.
Creator.EnableFontSubsetting(font)One font, registered on the creator that is producing the document.

SubsetFonts is the one to use when processing a file you did not create, since it works from the object list and needs no knowledge of how the document was built. EnableFontSubsetting is for creator output where you want a specific face subset and the others left alone, which matters when one of the fonts is intended to be reusable.

SubsetFonts implies CleanFonts: both flags append the same pass, and any font that cannot be subset still gets the table cleanup.

Doing it

pdfWriter, err := reader.ToWriter(nil)
if err != nil {
    return err
}

pdfWriter.SetOptimizer(optimize.New(optimize.Options{
    SubsetFonts: true,
}))

return pdfWriter.WriteToFile(outputPath)

Subsetting has to know which glyphs are in use before it can drop the rest. The pass finds that out by running text extraction over every page, plus annotation appearance streams, and collecting the glyphs each font was asked for. Which glyphs get kept depends on the font’s encoder: identity encoders contribute glyph indices directly, simple encoders contribute the runes their charcodes map to.

Combining it with the other options is fine, and font work always runs first in the chain. See optimizer options for the rest.

Limitations

Only fonts embedded through FontFile2 are subset, which in practice means TrueType and TrueType-flavored OpenType. A Type 1 or CFF program in FontFile or FontFile3 is skipped, as is an OpenType font with PostScript outlines, recognizable by a program starting with OTTO. Non-embedded fonts have nothing to subset.

If the rewritten program comes out larger than the original, the original is kept. A file already produced with subsetting will therefore report close to no saving rather than growing.

When no glyph usage is registered for a font, subsetting is skipped for that program rather than emptying it, and the generic font cleanup runs over it instead. This is what happens when text extraction cannot attribute any mark to the font.

Failures do not surface. Chain.Optimize logs an optimizer error at debug level and continues with the objects it was given, so a font that fails to parse leaves the write successful and the file larger than expected.

Subsetting is destructive with respect to later editing. The output font contains only the glyphs that were in use, so appending text in that font to the document afterwards can find the glyphs missing.

Run the example

pdf_font_subsetting.go reads a file, enables SubsetFonts alone, and prints the size before and after so the saving attributable to fonts is visible on its own.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/compress
go run pdf_font_subsetting.go <input.pdf> <output.pdf>

If this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.

View the full source

Sample output

Against the same input as the optimizer options example, fonts alone account for more than twice the saving of the whole structural chain, and take roughly seven times as long, since every page has to be run through text extraction.

Original file: input.pdf
Original size: 266845 bytes
Optimized file: output.pdf
Optimized size: 124868 bytes
Compression ratio: 53.21%
Processing time: 366.40 ms
Last updated on