Technical Article

Rendering Composite Glyphs and CFF seac Accents in PDF

HotPDF renders accented letters from embedded fonts correctly by handling two font mechanisms that build one glyph out of others: TrueType composite glyphs, whose component offsets must be applied in glyph units before the glyph-to-device matrix, and the CFF seac form of endchar, which stitches a base letter and an accent together by StandardEncoding code. Get either one wrong and é, Å, ü or a curly double quote comes out missing a part, while the plain letters around it look perfect

That last detail is what makes these bugs expensive. A report renders, the body text is fine, and only "München" has a hole in it, or a quotation opens with a single quote and a gap. Nobody suspects the renderer. The first theory is always a damaged font or a bad producer, and the ticket bounces around for a week. This article walks through what actually goes wrong in each mechanism, how the HotPDF Delphi component handles it since v2.768.10 and v2.768.11, and the related subsetting fix in v2.770.83 that keeps composite glyphs intact when HotPDF writes fonts instead of reading them

Why does only the accent disappear from a PDF glyph?

The accent disappears because it is the only part of the glyph that carries a non-zero offset. Both TrueType and CFF fonts save space by reusing outlines: é is not drawn as a new shape but as "the e glyph, plus the acute glyph shifted up and to the right". The base letter almost always sits at offset (0, 0), so any error in how offsets are applied leaves it untouched. The accent is the component that moves, so it is the component that lands in the wrong place or never gets drawn at all

The symptom pattern is a diagnostic in its own right. If the base letters of accented characters render and the diacritics do not, or if paired punctuation like “ and ” shows as one mark instead of two, stop looking at the font file. A broken font usually breaks whole glyphs, and it breaks them for every viewer. Missing pieces of composed glyphs in one renderer, while Acrobat shows the page fine, point at the code that assembles the components

How do TrueType composite glyphs place their components?

A TrueType composite glyph places each component with its own small affine transform, expressed in font units, and that transform has to act on the component outline before the glyph-to-device matrix does. In the OpenType glyf table a composite glyph has a negative contour count, followed by a list of component records. Each record holds a flags word, the component glyph index, two arguments that are either bytes or words (ARG_1_AND_2_ARE_WORDS, 0x0001), and an optional scale: one F2Dot14 value for WE_HAVE_A_SCALE (0x0008), two for WE_HAVE_AN_X_AND_Y_SCALE (0x0040), or a full 2x2 matrix for WE_HAVE_A_TWO_BY_TWO (0x0080). When ARGS_ARE_XY_VALUES (0x0002) is set, the two arguments are a dx/dy offset in font units. MORE_COMPONENTS (0x0020) says another record follows

So for each component you build a component matrix CM from the scale part and the offset, then combine it with M, the matrix that maps font units to device pixels. The order of that combination is the whole bug. HotPDF's internal matrix multiply uses the column-vector convention, so the product of A and B applies B first and then A. The correct composition applies CM first, in glyph space, and M second. Written the other way round, M runs first and CM's offset is added afterwards, already in device pixels and never scaled

// Illustration only: a 2D affine record with the column-vector convention,
// where Mul(A, B) maps a point through B first and then through A
type
  TAffine = record
    a, b, c, d, e, f: Double;
  end;

function Mul(const A, B: TAffine): TAffine;
begin
  Result.a := A.a * B.a + A.c * B.b;
  Result.b := A.b * B.a + A.d * B.b;
  Result.c := A.a * B.c + A.c * B.d;
  Result.d := A.b * B.c + A.d * B.d;
  Result.e := A.a * B.e + A.c * B.f + A.e;
  Result.f := A.b * B.e + A.d * B.f + A.f;
end;

// M  : font units -> device pixels (tiny scale, y flipped)
// CM : component transform; e/f hold dx/dy in font units
Sub := Mul(M, CM);   // correct: offset scaled down with the outline
// Sub := Mul(CM, M); // wrong: dx/dy land as raw device pixels

The numbers make the failure obvious once you see them. Arial uses a 2048-unit em, and the accent component of é carries its offset in those font units. With the correct order that offset shrinks along with the outline to a few pixels at normal zoom. With the reversed order the raw font-unit value is added as device pixels, and in the HotPDF test render the accent of Arial's é ended up 243 pixels away from its letter, far outside the glyph and usually outside anything visible. The left double quote in Arial is two copies of the single-quote shape, the second one shifted, so it rendered as a lone ‘ followed by empty space

The linear part rarely exposes the bug. Accent components usually carry no scale at all, and a uniform scale commutes with M anyway, so only the translation reveals the reversed product. That is also why a quick test with plain Latin text passes: the reversal had sat in the code across many releases before v2.768.10 corrected it, and only components with offsets ever showed it

Recursion and cycles

Composite glyphs may reference other composite glyphs, so a renderer walks them recursively and must defend itself. HotPDF caps nesting depth at 5 levels and the total component count per glyph at 1024, skips a component that names its own glyph index, and treats the rarely used point-matching mode (arguments that are point numbers rather than offsets) as a zero offset. A hostile or corrupted font can therefore lose a component, but it cannot hang the page render

What is the seac form of CFF endchar?

The seac form of endchar is a deprecated way for a CFF Type 2 charstring to say "this glyph is a base letter plus an accent": when endchar has four arguments, they are adx ady bchar achar. Adobe Technical Note #5177 describes it in its appendix on compatibility with Type 1. The name comes from the Type 1 seac operator (standard encoding accented character), and CFF subsets converted from Type 1 fonts still use it heavily for accented Latin letters

Three details trip up implementers. First, bchar and achar are not glyph IDs. They are codes in Adobe StandardEncoding, so the renderer maps each code to a glyph name and then looks that name up through the font's charset to find the GID. Second, Type 2 has no asb argument, unlike Type 1 seac, which takes asb adx ady bchar achar. In Type 2 the accent's origin goes directly to (adx, ady) relative to the base glyph's origin, with no side-bearing correction. Third, CID-keyed CFF fonts may not use seac at all, because there is no glyph-name charset to resolve StandardEncoding codes through

Then there is the width. Every Type 2 charstring may start its first stack-clearing operator with an optional advance width argument, and endchar is one of those operators. A plain endchar with one argument carries a width; a seac endchar with five arguments carries a width followed by the four seac values. The rule is to count arguments against the expected fixed count, which is four for seac. Before v2.768.11, HotPDF treated any endchar as "take the width and stop", so seac glyphs produced an empty outline: no base, no accent, just a correctly sized gap in the line

// Illustration of the endchar decision in a Type 2 interpreter.
// Stack holds the operands that precede endchar; Count is how many.
if (Count >= 4) and not IsCIDFont then
begin
  // 5 operands: the first one is the advance width, the rest are seac
  if Count = 5 then
    TakeWidthAndShift;                 // drop the width, keep 4 operands
  if StackDepth = 4 then
  begin
    BaseGID   := GIDForName(StdEncName(Round(Stack[2])));  // bchar
    AccentGID := GIDForName(StdEncName(Round(Stack[3])));  // achar
    DrawGlyph(BaseGID, Identity);
    DrawGlyph(AccentGID, Translate(Stack[0], Stack[1]));   // adx, ady
    Exit;
  end;
end;
// otherwise: ordinary endchar, optional width, close the path

In the real interpreter both component glyphs are executed with the same charstring engine, the accent's matrix is the current glyph matrix composed with a translation to (adx, ady), and the matrix is restored afterwards so later glyphs on the line are unaffected. If either name does not resolve to a GID, that component is skipped rather than failing the whole glyph, which is the forgiving behavior you want when a subset has been trimmed a little too aggressively

Checking the fix from your own Delphi code

The quickest way to verify composite glyph and seac handling is to render a page with accented text from embedded fonts through both public output paths: THotPDF.RenderLoadedPageToBitmap for raster output and THotPDF.BuildLoadedPageSVG with EmbedFonts set to True for vector output. Both share the same glyph outline code, so both received the v2.768.10 and v2.768.11 fixes. The pipeline as a whole is covered in how HotPDF renders embedded TrueType, CFF and Type 3 glyphs, and bitmap options in rendering PDF pages to bitmaps in Delphi

uses
  System.Classes, Vcl.Graphics, HPDFDoc;

procedure RenderAccentProbe(const FileName: string);
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  Svg: AnsiString;
  Lines: TStringList;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(FileName) <= 0 then
      Exit;

    // Raster path: 144 DPI makes misplaced accents easy to spot
    Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
    if Bmp <> nil then
    try
      Bmp.SaveToFile('accent-probe.bmp');
    finally
      Bmp.Free;
    end;

    // Vector path: EmbedFonts = True writes glyph outlines, so the
    // composite and seac assembly is visible in the SVG paths themselves
    Svg := Pdf.BuildLoadedPageSVG(0, True);
    if Svg <> '' then
    begin
      Lines := TStringList.Create;
      try
        Lines.Text := string(Svg);
        Lines.SaveToFile('accent-probe.svg');
      finally
        Lines.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Pick test documents deliberately. You want one PDF with an embedded TrueType subset containing é, Å and curly double quotes, and one with an embedded CFF (FontFile3, Type1C) subset produced from a Type 1 font, which is where seac still lives, typically in older typesetting output and some print workflows. Text set in fonts the producer did not embed tells you nothing here, because those glyphs come from installed Windows fonts rather than the embedded font program described in ISO 32000-1 §9.9

Why did composite glyphs break during font subsetting?

Composite glyphs broke during subsetting because the subsetter stepped through component records with the wrong stride whenever a component carried a scale or 2x2 transform, then misread the next component's glyph index. HotPDF fixed this in v2.770.83. Subsetting is the write-side twin of the rendering bug: when HotPDF embeds a subset of an installed TrueType font, it has to keep every glyph a composite references and renumber those references to the new, compact glyph IDs

The old walker only looked at the argument-size flag and the more-components flag, so it advanced 6 or 8 bytes per record. It ignored the 2, 4 or 8 bytes of transform data that the scale flags add. After a scaled component, the next "glyph index" it read was really transform bytes, so the real component could be left out of the subset, and the renumbering step could write a new glyph ID straight into the transform. The result was an accented letter that rendered in some viewers with the wrong accent, a distorted accent, or none at all, and only for fonts that scale their components, which is why most tests never caught it

Two more problems sat next to the stride bug. Renumbering a glyph index is not idempotent, and nested composites were being renumbered twice: once by the pass over every marked composite and again by a recursive descent into it. HotPDF now renumbers each composite exactly once and drives the "which glyphs does this composite need" step from a work queue, so a cycle of composites that reference each other terminates, glyph indexes beyond the font's maxp glyph count are rejected, and empty glyphs are never parsed as composites

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'accents-subset.pdf';
    Pdf.FontEmbedding := True;          // embed the glyphs that are used
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 14);
    // é, Å, ü and curly quotes are all composite glyphs in Arial
    Pdf.CurrentPage.UnicodeTextOut(50, 720, 0,
      'Café, Ångström, München, “quoted”');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Round-trip that file through the probe above and through a second viewer. Since v2.770.83 every component of every composite glyph survives subsetting, including components behind a scale or 2x2 transform, so the accents render the same in HotPDF, Acrobat and browser viewers

What the composite glyph handling does not cover

The composite glyph fix covers offset-based placement, which is what nearly every real font uses, and approximates the rest conservatively. TrueType point-matching composites, where the two arguments name points to align instead of giving an offset, are drawn with a zero offset. Seac in CID-keyed CFF fonts is ignored by design because the specification forbids it there. A seac whose base or accent code has no glyph in a trimmed subset draws whatever component it can find. None of these cases crash the renderer, but if you produce PDFs for archival or PDF/A use, the safer move is to embed fonts that use offset composites and Type 2 charstrings without seac, so every consumer, not just one renderer, sees the same letters

FAQ

Why do accented letters render without their accents in my PDF renderer?

Accented letters are usually composite glyphs or CFF seac glyphs, and the accent is the only component with an offset. A renderer that applies the component offset after the device matrix, or that ignores seac in endchar, drops or misplaces the accent while the base letter renders normally

Does HotPDF support the CFF seac form of endchar?

Yes. Since v2.768.11 HotPDF draws endchar with four arguments, or five including the width, as a base glyph plus an accent shifted to (adx, ady), resolving both through StandardEncoding names and the font charset, for both bitmap rendering and SVG export with embedded fonts

Are composite glyphs preserved when HotPDF subsets TrueType fonts?

Yes. Since v2.770.83 HotPDF steps over component transforms of 2, 4 or 8 bytes correctly, keeps every referenced component in the subset, renumbers nested composites once, and stops safely on cyclic or out-of-range glyph references

Composite glyph rendering, CFF seac support and safe TrueType subsetting ship as part of the HotPDF Delphi PDF Component, a native VCL library for Delphi and C++Builder that creates, edits, extracts and renders PDF documents without external DLLs