Technical Article

Embed CID Type 2 Fonts in PDF from Delphi

PDFium Component writes text into a PDF page at the glyph level with AddCidType2Text, embedding TrueType data you supply as a CID Type 2 font. It assigns each glyph instance a sequential CID, generates the explicit CID-to-GID map and the ToUnicode CMap, and merges glyphs on the same baseline into a single native text object. Optional subsetting keeps the file small, with an explicit policy for what happens when subsetting fails

Glyph-level writing is what you need when the text has already been shaped. Arabic, Devanagari and any script with contextual forms produce a sequence of glyph identifiers that no longer maps one-to-one onto characters, so an API that takes a string and a font name cannot express the result. Handing over glyphs and positions directly is the only way to put correctly shaped complex script text into a PDF

AddCidType2Text in Delphi assigns each shaped glyph instance a sequential CID, writes the CIDToGIDMap and ToUnicode CMap, and merges the run into one page text object that extracts cleanly
Every glyph instance takes its own CID, the font dictionary maps CIDs to glyph ids and back to Unicode, and the run lands on the page as one text object that extracts back to the intended text

Why every glyph instance gets its own CID

The tempting optimization is to deduplicate: one CID per distinct glyph identifier, reused wherever that glyph appears. It produces a smaller font and it breaks text extraction, because the same glyph can legitimately correspond to different Unicode content in different places

A ligature glyph is the clear case. The same "ffi" glyph may stand for the three characters in one word and, after a different shaping decision, for something else elsewhere. The ToUnicode map is keyed by CID, so a shared CID can only carry one mapping, and whichever text loses that contest becomes unextractable

So each glyph instance takes its own CID, and each mapping is free to be several UTF-16 code units. A glyph with no meaningful text — a decorative element, a positioning-only glyph — maps explicitly to U+200B, a zero-width space, so every CID has an observable extraction result rather than a gap

uses
  PDFium;

var
  Pdf: TPdf;
  Glyphs: TPdfCidGlyphs;
  Options: TPdfCidFontOptions;
  Report: TPdfCidFontReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'label.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 1;

    // One entry per shaped glyph: the glyph id, the text it represents,
    // and its advance and offsets in text space
    SetLength(Glyphs, 3);
    Glyphs[0].GlyphID := 402;  Glyphs[0].UnicodeText := 'ffi';
    Glyphs[0].Advance := 18.4;
    Glyphs[1].GlyphID := 71;  Glyphs[1].UnicodeText := 'c';
    Glyphs[1].Advance := 9.8;
    Glyphs[2].GlyphID := 74;   Glyphs[2].UnicodeText := 'e';
    Glyphs[2].Advance := 9.2;

    Options := TPdfCidFontOptions.Default;   // subset preferred
    Options.VerifyExtraction := True;

    if Pdf.AddCidType2Text(LoadFileBytes('NotoSans.ttf'), Glyphs,
         11, 72, 700, Options, Report) then
      Writeln(Format('%d glyphs, %d unique, font %d -> %d bytes, match=%s',
        [Report.GlyphCount, Report.UniqueGlyphCount,
         Report.OriginalFontBytes, Report.EmbeddedFontBytes,
         BoolToStr(Report.ExtractionMatches, True)]));
  finally
    Pdf.Free;
  end;
end;

Subsetting, and the checks a return code does not give you

Subsetting a font to the glyphs actually used is the difference between embedding 300 KB and embedding 12 KB, and on a document with several fonts that decides whether the file is emailable. The platform subsetting path accepts a glyph list directly, which fits this API perfectly since the caller already knows every glyph identifier in use

What it does not give you is confidence. A subsetting call can report success and return output that is unusable, so the component validates three properties before accepting the result: the output must be smaller than the original, it must reparse as a valid sfnt with a readable maxp table, and it must retain glyphs up to the highest original identifier requested. Any failure means the subset is rejected

What happens next is the caller's policy. Under pcfemSubsetPreferred, the default, a rejected subset falls back to embedding the full font, so the page is correct and merely larger. Under pcfemSubsetRequired, the operation fails before the page is modified, which is what a size-constrained pipeline wants. pcfemFull skips subsetting entirely. The report tells you which path was taken through SubsetAttempted, SubsetApplied, UsedFullFontFallback and SubsetErrorCode

CID Type 2 font subsetting in Delphi passes three validation checks for size, sfnt integrity and glyph coverage before acceptance, and a rejected subset follows the pcfem policy of full font fallback or hard failure
A subset is accepted only after the size, sfnt and glyph coverage checks pass, and a rejected subset then follows the caller policy — fall back to the full font or fail before the page is modified

Verification that cannot produce a false positive

With VerifyExtraction enabled, the component confirms that what it wrote can be read back. The naive way to do that is to extract the whole page and search for the expected string, and it is wrong: a page that already contained that text passes the check even when the new text was written incorrectly

Instead the text page is rebuilt and the object handles inserted by this call are read individually, in insertion order, and concatenated. The result is compared against the expected text, and the report exposes both strings along with the boolean, so a mismatch can be diagnosed rather than merely detected

Turn it on during development and in any pipeline where extractability is a requirement — searchable archives, accessibility compliance, downstream text mining. The cost is one text page rebuild per call, which is why it is not on by default in tight loops

Budgets and atomic failure

Font bytes, glyph instances and Unicode code units are each capped before allocation, and the font format, TTC index, glyph identifier range and geometry values are validated before anything is written. Geometry must be finite, which sounds obvious until a shaping engine hands you a NaN advance from a malformed font

Failure is atomic at the page level. If font loading, object writing, content generation or extraction verification fails, every object inserted by that call is removed in reverse order and the page content is regenerated. A failed call leaves the page as it was, not with half a text run on it

Where this fits in a text pipeline

The division of labor is worth stating plainly. Shaping — turning characters into positioned glyphs — is not this API's job; it belongs to a shaping engine, and the component's own bidirectional and complex-script support is covered in emoji, CJK and surrogate pair handling. This API is what you call afterward, with the glyph run the shaper produced

For ordinary Latin text with no contextual shaping, the simpler string-based text APIs are the right tool and produce smaller code. Reach for CID Type 2 writing when you have shaped output, when you need exact control of glyph identifiers, or when the font must be embedded from bytes you hold rather than resolved by name — the font resolution alternative being the provider mechanism described in controlling PDF font substitution

One deployment note: embedding a font is a licensing question as much as a technical one. Fonts vary in whether embedding is permitted at all, permitted for viewing only, or permitted for editing. The library will embed whatever bytes you give it, and checking the license is your responsibility, not the file format's

Glyph-level text writing, font provisioning and text extraction share the same page model in Delphi, C++Builder and Lazarus; the full API is described on the PDFium Component for Delphi page