Skip to content

fontMap text rendering draws one character at a time, dropping the font's OpenType kerning #2728

Description

@nilvng

Describe the bug

When rendering a text layer via LottieAnimationView#setFontMap / LottieDrawable#setFontMap (the path taken whenever the layer isn't exported as glyph shapes), text is drawn one character at a time: TextLayer#drawFontTextLine calls fillPaint.measureText(charString) on a single character, draws it, then manually canvas.translate()s by that character's width before drawing the next one.

Because each character is measured and drawn independently, none of the mapped font's OpenType pair kerning is ever applied — kerning is inherently a property of adjacent-character pairs, and it can't be produced by measuring/drawing one character in isolation. This holds regardless of tracking (tr), which is a separate, uniform per-character offset applied on top — it can't reproduce specific pair kerning either.

This makes any custom font mapped in via fontMap render measurably looser than the same font rendered normally by the OS (e.g. in a TextView/Compose Text).

Minimal repro

Using stock Roboto Bold (Apache-2.0, ships on every Android device / available at https://github.com/googlefonts/roboto) — no custom font needed to reproduce this.

repro.json:

{
  "v": "5.9.6",
  "fr": 24,
  "ip": 0,
  "op": 48,
  "w": 500,
  "h": 200,
  "nm": "fontMap-kerning-repro",
  "ddd": 0,
  "assets": [],
  "fonts": {
    "list": [
      { "origin": 0, "fPath": "", "fClass": "", "fFamily": "Roboto", "fWeight": "", "fStyle": "Bold", "fName": "Roboto-Bold", "ascent": 75 }
    ]
  },
  "layers": [
    {
      "ddd": 0, "ind": 1, "ty": 5, "nm": "Total", "sr": 1,
      "ks": {
        "o": { "a": 0, "k": 100 }, "r": { "a": 0, "k": 0 },
        "p": { "a": 0, "k": [40, 120, 0] }, "a": { "a": 0, "k": [0, 0, 0] },
        "s": { "a": 0, "k": [100, 100, 100] }
      },
      "ao": 0,
      "t": {
        "d": { "k": [ { "s": { "s": 60, "f": "Roboto-Bold", "t": "Total", "j": 0, "tr": 0, "lh": 72, "ls": 0, "fc": [0, 0, 0] }, "t": 0 } ] },
        "p": {}, "m": { "g": 1, "a": { "a": 0, "k": [0, 0] } }, "a": []
      },
      "ip": 0, "op": 48, "st": 0, "bm": 0
    }
  ]
}
val fontMap = mapOf("Roboto-Bold" to Typeface.create("sans-serif", Typeface.BOLD))
// Load repro.json via LottieCompositionFactory, set this fontMap on the LottieDrawable/LottieAnimationView.
  • The font's own To kern pair is −208/2048 em (Roboto Bold, verified via fontTools) — at 60px that's −6.09px that a correctly-kerned renderer would subtract between T and o.
  • Rendered through fontMap, the full unkerned advance is used instead — the word comes out ~6px wider/looser than the same text drawn with canvas.drawText("Total", x, y, paint) (or a plain TextView/Compose Text using the same font/size), which does apply the kerning.

Expected behavior

For a text layer resolved via fontMap/getTypeface (i.e. rendered with a real Android Typeface, not baked glyph shapes), the whole line should be measured and drawn as one run — e.g. fillPaint.measureText(fullLine) / canvas.drawText(fullLine, x, y, paint) — so Android's normal text shaping (which does apply the font's kerning) can take effect, with per-character tracking/coloring/animation applied on top only where actually needed (e.g. TextRangeUnits/character-level color or position keyframes).

Environment

  • lottie-android: 6.7.1
  • Affected: any text layer using fontMap/FontAssetDelegate-resolved Typeface (the non-glyph path); glyph-shape text layers (drawTextWithGlyphs/drawCharacterAsGlyph) are unaffected since they don't rely on font kerning at all.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions