From cc12a1ab2deb498a73464dadc5852da15d20b1e1 Mon Sep 17 00:00:00 2001 From: Mathis Mensing Date: Fri, 16 Oct 2026 20:12:19 +0000 Subject: [PATCH] renderText: GSUB shaping, emoji sequences, lazy fallbacks, glyph cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/render/gsub.ts applies the default features (ccmp, rlig, liga, clig, calt): single, multiple, ligature, contextual and chained contextual substitutions (formats 1-3) and extension lookups, with lookup flags from GDEF and nested lookups. A memo per lookup skips glyphs no subtable starts at. - Paragraphs are shaped: graphemes (extending characters, joined sequences, regional indicator pairs) go to the first font that has all of their visible characters, preferring color glyphs before FE0F and others before FE0E; each font's run is substituted. Invisible characters that weren't substituted show nothing. - Fallback fonts load when the text first needs them; glyphs, including decoded and scaled emoji bitmaps, are kept per font, size and hinting across calls (a few sizes per font). - Tests: skin tones, flags, keycaps and families are single glyphs, ❤️ picks the emoji font's red heart; emoji targets regenerated. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ --- CHANGELOG.md | 10 +- README.md | 2 +- src/render/font.ts | 4 +- src/render/gsub.ts | 359 +++++++++++++++++++++++++++++++++++ src/render/text.ts | 241 ++++++++++++++++------- tests/font.js | 15 +- tests/targets/emoji-cbdt.png | Bin 12143 -> 22066 bytes tests/targets/emoji-colr.png | Bin 6438 -> 11987 bytes 8 files changed, 553 insertions(+), 78 deletions(-) create mode 100644 src/render/gsub.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ea6d2d2..6672e8b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,14 @@ - `Image.renderText` takes several fonts by priority (`[text, emoji]`): every character comes from the first font that has it, the first font sets the line height. Color glyphs keep their colors: COLR version 0 layers with CPAL palettes (e.g. Twemoji) and CBDT/CBLC bitmaps (e.g. Noto Color Emoji, which now loads at all, it has no outlines). - Invisible characters like joiners and variation selectors take no room, as in browsers. Emoji sequences (skin tones, - flags, families) still show as their parts. + Invisible characters like joiners and variation selectors take no room, as in browsers. +- `Image.renderText` applies the fonts' glyph substitutions (GSUB) that browsers apply by default (`ccmp`, `rlig`, + `liga`, `clig`, `calt`): emoji sequences (skin tones, flags, keycaps, families and other joined ones) become one + glyph, and text fonts use their ligatures (fi, ffi, ...) and contextual alternates. Emoji sequences come from one + font, the first that has all of their characters, and the emoji variation selector picks a color glyph (❤️ in an + emoji font rather than the text font's ❤). +- Fallback fonts are only loaded when the text needs them, and glyphs (also decoded and scaled emoji bitmaps) are + kept per font and size across calls. ### Performance diff --git a/README.md b/README.md index df8c0d4e..799403c2 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ deno add jsr:@matmen/imagescript # Deno (npm:imagescript works too) - [Rendering SVGs](https://imagescript.matmen.dev/classes/Image.html#rendersvg) (shapes, paths, gradients, strokes, clip paths, masks, CSS; no text, images or filters) - [Rendering vector fonts](https://imagescript.matmen.dev/classes/Image.html#rendertext) (TrueType and OpenType, with kerning, - outlines, TrueType hinting, fallback fonts and color emoji) + ligatures, outlines, TrueType hinting, fallback fonts and color emoji) - [Resizing](https://imagescript.matmen.dev/classes/Image.html#resize) (bilinear, bicubic, area averaging and nearest neighbor) - Image manipulation functions ([crop](https://imagescript.matmen.dev/classes/Image.html#crop) , [rotate](https://imagescript.matmen.dev/classes/Image.html#rotate) diff --git a/src/render/font.ts b/src/render/font.ts index 617b0d63..6475941f 100644 --- a/src/render/font.ts +++ b/src/render/font.ts @@ -598,7 +598,7 @@ function parsePairPos(font: Font, table: number): PairLookup | null { } // returns the coverage index of a glyph, -1 if it isn't covered -function parseCoverage(font: Font, table: number): (glyph: number) => number { +export function parseCoverage(font: Font, table: number): (glyph: number) => number { const format = font.u16(table), count = font.u16(table + 2); return glyph => { @@ -621,7 +621,7 @@ function parseCoverage(font: Font, table: number): (glyph: number) => number { }; } -function parseClassDef(font: Font, table: number): (glyph: number) => number { +export function parseClassDef(font: Font, table: number): (glyph: number) => number { const format = font.u16(table); if (format === 1) { diff --git a/src/render/gsub.ts b/src/render/gsub.ts new file mode 100644 index 00000000..a9f7b5dd --- /dev/null +++ b/src/render/gsub.ts @@ -0,0 +1,359 @@ +// glyph substitution (GSUB) with the features browsers apply by default: composition (emoji sequences, flags, +// keycaps), ligatures and contextual alternates. Script and language systems aren't told apart, every record of these +// features applies (like the kerning of GPOS) + +import { Font, parseCoverage, parseClassDef } from './font.ts'; + +const FEATURES = new Set(['ccmp', 'rlig', 'liga', 'clig', 'calt']); + +/** glyphs being shaped, with the index of the character each one came from (a ligature: its first) */ +export interface GlyphBuffer { + glyphs: number[]; + clusters: number[]; +} + +// a subtable applied at index i: the index to continue at, -1 if it doesn't apply there +type Subtable = (buffer: GlyphBuffer, i: number) => number; + +interface Lookup { + subtables: Subtable[]; + /** whether the lookup flags skip the glyph */ + skip: (glyph: number) => boolean; + /** whether a subtable can start at the glyph */ + starts: (glyph: number) => boolean; +} + +// a glyph test in a context: a glyph id, a class or a coverage +type Matcher = (glyph: number) => boolean; + +/** + * The font's substitutions as a function that rewrites a glyph buffer, null if the font has none + */ +export function parseGSUB(font: Font): ((buffer: GlyphBuffer) => void) | null { + const gsub = font.tables.GSUB?.offset; + if (gsub === undefined) return null; + const featureList = gsub + font.u16(gsub + 6), lookupList = gsub + font.u16(gsub + 8); + const lookupCount = font.u16(lookupList); + + const indices = new Set(); + for (let i = 0, count = font.u16(featureList); i < count; i++) { + const record = featureList + 2 + i * 6; + const tag = String.fromCharCode(font.u8(record), font.u8(record + 1), font.u8(record + 2), font.u8(record + 3)); + if (!FEATURES.has(tag)) continue; + const feature = featureList + font.u16(record + 4); + for (let j = 0, n = font.u16(feature + 2); j < n; j++) { + const index = font.u16(feature + 4 + j * 2); + if (index < lookupCount) indices.add(index); + } + } + if (!indices.size) return null; + + // glyph classes (1 base, 2 ligature, 3 mark) and mark attachment classes for the lookup flags + const gdef = font.tables.GDEF?.offset; + const glyphClass = gdef !== undefined && font.u16(gdef + 4) ? parseClassDef(font, gdef + font.u16(gdef + 4)) : () => 0; + const markClass = gdef !== undefined && font.u16(gdef + 10) ? parseClassDef(font, gdef + font.u16(gdef + 10)) : () => 0; + + // lookups are parsed when first used, contextual ones can call any lookup + const lookups = new Map(); + const lookup = (index: number): Lookup | null => { + if (index >= lookupCount) return null; + let l = lookups.get(index); + if (!l) { + const offset = lookupList + font.u16(lookupList + 2 + index * 2); + const type = font.u16(offset), flag = font.u16(offset + 2); + const skip = skipper(flag, glyphClass, markClass); + // a glyph no subtable covers first is passed over without trying them (memoized per glyph: 0 unknown, + // 1 no, 2 yes) + const covers: Matcher[] = [], memo = new Uint8Array(font.numGlyphs); + const starts = (glyph: number): boolean => { + if (glyph >= memo.length) return covers.some(c => c(glyph)); + if (!memo[glyph]) memo[glyph] = covers.some(c => c(glyph)) ? 2 : 1; + return memo[glyph] === 2; + }; + l = { subtables: [], skip, starts }; + lookups.set(index, l); + for (let i = 0, count = font.u16(offset + 4); i < count; i++) { + const subtableOffset = offset + font.u16(offset + 6 + i * 2); + const subtable = parseSubtable(font, type, subtableOffset, l, applyNested); + if (!subtable) continue; + l.subtables.push(subtable); + covers.push(firstCoverage(font, type, subtableOffset)); + } + } + return l; + }; + + // a lookup called from a context applies once, at the given index + function applyNested(buffer: GlyphBuffer, index: number, i: number): void { + const l = lookup(index); + if (!l || i >= buffer.glyphs.length || l.skip(buffer.glyphs[i]) || !l.starts(buffer.glyphs[i])) return; + for (const subtable of l.subtables) if (subtable(buffer, i) >= 0) return; + } + + const order = [...indices].sort((a, b) => a - b); + return buffer => { + // every lookup goes over the whole buffer, in the order of the lookup list + for (const index of order) { + const l = lookup(index); + if (!l) continue; + for (let i = 0; i < buffer.glyphs.length;) { + let next = -1; + const glyph = buffer.glyphs[i]; + if (l.starts(glyph) && !l.skip(glyph)) for (const subtable of l.subtables) if ((next = subtable(buffer, i)) >= 0) break; + i = next >= 0 ? next : i + 1; + } + } + }; +} + +// which glyphs a lookup's flags skip: bases, ligatures, marks (all or those of another attachment class) +function skipper(flag: number, glyphClass: (glyph: number) => number, markClass: (glyph: number) => number): (glyph: number) => boolean { + const attachment = flag >> 8; + if (!(flag & 0xe) && !attachment) return () => false; + return glyph => { + const c = glyphClass(glyph); + if (c === 1) return (flag & 2) !== 0; + if (c === 2) return (flag & 4) !== 0; + if (c === 3) return (flag & 8) !== 0 || attachment !== 0 && markClass(glyph) !== attachment; + return false; + }; +} + +// the next / previous index the lookup doesn't skip, -1 at the end +function next(buffer: GlyphBuffer, i: number, l: Lookup): number { + for (i++; i < buffer.glyphs.length; i++) if (!l.skip(buffer.glyphs[i])) return i; + return -1; +} + +function previous(buffer: GlyphBuffer, i: number, l: Lookup): number { + for (i--; i >= 0; i--) if (!l.skip(buffer.glyphs[i])) return i; + return -1; +} + +// the glyphs a subtable can start at: its coverage, or the first input coverage of format 3 contexts +function firstCoverage(font: Font, type: number, offset: number): Matcher { + if (type === 7) return firstCoverage(font, font.u16(offset + 2), offset + font.u32(offset + 4)); + let coverage = offset + font.u16(offset + 2); + if (type === 5 && font.u16(offset) === 3) coverage = offset + font.u16(offset + 6); + else if (type === 6 && font.u16(offset) === 3) coverage = offset + font.u16(offset + 6 + font.u16(offset + 2) * 2); + const covered = parseCoverage(font, coverage); + return glyph => covered(glyph) >= 0; +} + +function parseSubtable(font: Font, type: number, offset: number, l: Lookup, nested: (buffer: GlyphBuffer, lookup: number, i: number) => void): Subtable | null { + const format = font.u16(offset); + switch (type) { + // single: a glyph for another + case 1: { + const coverage = parseCoverage(font, offset + font.u16(offset + 2)); + if (format === 1) { + const delta = font.i16(offset + 4); + return (buffer, i) => { + if (coverage(buffer.glyphs[i]) < 0) return -1; + buffer.glyphs[i] = buffer.glyphs[i] + delta & 0xffff; + return i + 1; + }; + } + return (buffer, i) => { + const k = coverage(buffer.glyphs[i]); + if (k < 0) return -1; + buffer.glyphs[i] = font.u16(offset + 6 + k * 2); + return i + 1; + }; + } + + // multiple: a glyph for a sequence (empty ones delete it) + case 2: { + const coverage = parseCoverage(font, offset + font.u16(offset + 2)); + return (buffer, i) => { + const k = coverage(buffer.glyphs[i]); + if (k < 0) return -1; + const sequence = offset + font.u16(offset + 6 + k * 2), count = font.u16(sequence); + const glyphs = Array.from({ length: count }, (_, j) => font.u16(sequence + 2 + j * 2)); + buffer.glyphs.splice(i, 1, ...glyphs); + buffer.clusters.splice(i, 1, ...glyphs.map(() => buffer.clusters[i])); + return i + count; + }; + } + + // ligature: a sequence for a glyph, the first ligature of the set that matches + case 4: { + const coverage = parseCoverage(font, offset + font.u16(offset + 2)); + return (buffer, i) => { + const k = coverage(buffer.glyphs[i]); + if (k < 0) return -1; + const set = offset + font.u16(offset + 6 + k * 2); + for (let j = 0, count = font.u16(set); j < count; j++) { + const ligature = set + font.u16(set + 2 + j * 2), components = font.u16(ligature + 2); + const positions = [i]; + for (let c = 1, p = i; c < components; c++) { + p = next(buffer, p, l); + if (p < 0 || buffer.glyphs[p] !== font.u16(ligature + 2 + c * 2)) break; + positions.push(p); + } + if (positions.length !== components) continue; + + buffer.glyphs[i] = font.u16(ligature); + for (let c = positions.length - 1; c > 0; c--) { + buffer.glyphs.splice(positions[c], 1); + buffer.clusters.splice(positions[c], 1); + } + return i + 1; + } + return -1; + }; + } + + // contextual and chained contextual: lookups applied to a matched sequence + case 5: + case 6: + return parseContext(font, type === 6, offset, l, nested); + + // extension: a subtable of another type behind a 32-bit offset + case 7: + return parseSubtable(font, font.u16(offset + 2), offset + font.u32(offset + 4), l, nested); + + default: + return null; + } +} + +interface Rule { + backtrack: Matcher[]; + input: Matcher[]; + lookahead: Matcher[]; + /** (sequence index, lookup index) pairs */ + records: number[]; +} + +function parseContext(font: Font, chained: boolean, offset: number, l: Lookup, nested: (buffer: GlyphBuffer, lookup: number, i: number) => void): Subtable | null { + const format = font.u16(offset); + const records = (o: number, count: number): number[] => Array.from({ length: count * 2 }, (_, j) => font.u16(o + j * 2)); + const glyphs = (o: number, count: number): Matcher[] => Array.from({ length: count }, (_, j) => { + const id = font.u16(o + j * 2); + return (glyph: number) => glyph === id; + }); + + // rules of format 1 (glyphs) and 2 (classes) sets: [backtrack,] input after the first, [lookahead,] records + const rule = (o: number, matchers: (o: number, count: number, part: number) => Matcher[]): Rule => { + if (!chained) { + const inputCount = font.u16(o), recordCount = font.u16(o + 2); + return { backtrack: [], input: matchers(o + 4, inputCount - 1, 1), lookahead: [], records: records(o + 4 + (inputCount - 1) * 2, recordCount) }; + } + const backtrackCount = font.u16(o); + const backtrack = matchers(o + 2, backtrackCount, 0); + let p = o + 2 + backtrackCount * 2; + const inputCount = font.u16(p); + const input = matchers(p + 2, inputCount - 1, 1); + p += 2 + (inputCount - 1) * 2; + const lookaheadCount = font.u16(p); + const lookahead = matchers(p + 2, lookaheadCount, 2); + p += 2 + lookaheadCount * 2; + return { backtrack, input, lookahead, records: records(p + 2, font.u16(p)) }; + }; + const ruleSet = (set: number, matchers: (o: number, count: number, part: number) => Matcher[]): Rule[] => + Array.from({ length: font.u16(set) }, (_, j) => rule(set + font.u16(set + 2 + j * 2), matchers)); + + if (format === 1 || format === 2) { + const coverage = parseCoverage(font, offset + font.u16(offset + 2)); + let classes: Array<(glyph: number) => number> = []; + let setCount: number, sets: number; + if (format === 1) { + setCount = font.u16(offset + 4); + sets = offset + 6; + } else if (!chained) { + const classDef = parseClassDef(font, offset + font.u16(offset + 4)); + classes = [classDef, classDef, classDef]; + setCount = font.u16(offset + 6); + sets = offset + 8; + } else { + classes = [0, 1, 2].map(k => parseClassDef(font, offset + font.u16(offset + 4 + k * 2))); + setCount = font.u16(offset + 10); + sets = offset + 12; + } + // format 2 rules hold classes of the backtrack (0), input (1) or lookahead (2) class definition + const matchers = format === 1 ? glyphs : (o: number, count: number, part: number): Matcher[] => Array.from({ length: count }, (_, j) => { + const value = font.u16(o + j * 2), classOf = classes[part]; + return (glyph: number) => classOf(glyph) === value; + }); + + const cache = new Map(); + return (buffer, i) => { + const glyph = buffer.glyphs[i]; + if (coverage(glyph) < 0) return -1; + // sets by coverage index (format 1) or by the input class of the first glyph (format 2) + const k = format === 1 ? coverage(glyph) : classes[1](glyph); + if (k >= setCount || !font.u16(sets + k * 2)) return -1; + let rules = cache.get(k); + if (!rules) cache.set(k, rules = ruleSet(offset + font.u16(sets + k * 2), matchers)); + for (const r of rules) { + const end = applyRule(buffer, i, r, l, nested); + if (end >= 0) return end; + } + return -1; + }; + } + + if (format === 3) { + const coverages = (o: number, count: number): Matcher[] => Array.from({ length: count }, (_, j) => { + const coverage = parseCoverage(font, offset + font.u16(o + j * 2)); + return (glyph: number) => coverage(glyph) >= 0; + }); + let r: Rule; + if (!chained) { + const inputCount = font.u16(offset + 2), recordCount = font.u16(offset + 4); + const input = coverages(offset + 6, inputCount); + r = { backtrack: [], input: input.slice(1), lookahead: [], records: records(offset + 6 + inputCount * 2, recordCount) }; + const first = input[0]; + return (buffer, i) => first(buffer.glyphs[i]) ? applyRule(buffer, i, r, l, nested) : -1; + } + const backtrackCount = font.u16(offset + 2); + const backtrack = coverages(offset + 4, backtrackCount); + let p = offset + 4 + backtrackCount * 2; + const inputCount = font.u16(p); + const input = coverages(p + 2, inputCount); + p += 2 + inputCount * 2; + const lookaheadCount = font.u16(p); + const lookahead = coverages(p + 2, lookaheadCount); + p += 2 + lookaheadCount * 2; + r = { backtrack, input: input.slice(1), lookahead, records: records(p + 2, font.u16(p)) }; + const first = input[0]; + return (buffer, i) => first(buffer.glyphs[i]) ? applyRule(buffer, i, r, l, nested) : -1; + } + + return null; +} + +// matches a rule at i (its first input glyph already matched) and applies its lookups: the index after the input +// sequence, -1 if the rule doesn't match +function applyRule(buffer: GlyphBuffer, i: number, r: Rule, l: Lookup, nested: (buffer: GlyphBuffer, lookup: number, i: number) => void): number { + const positions = [i]; + for (let k = 0, p = i; k < r.input.length; k++) { + p = next(buffer, p, l); + if (p < 0 || !r.input[k](buffer.glyphs[p])) return -1; + positions.push(p); + } + for (let k = 0, p = i; k < r.backtrack.length; k++) { + p = previous(buffer, p, l); + if (p < 0 || !r.backtrack[k](buffer.glyphs[p])) return -1; + } + for (let k = 0, p = positions[positions.length - 1]; k < r.lookahead.length; k++) { + p = next(buffer, p, l); + if (p < 0 || !r.lookahead[k](buffer.glyphs[p])) return -1; + } + + // the lookups can change the length, the positions after the changed one move along + let end = positions[positions.length - 1] + 1; + for (let k = 0; k < r.records.length; k += 2) { + const sequenceIndex = r.records[k]; + if (sequenceIndex >= positions.length) continue; + const at = positions[sequenceIndex], before = buffer.glyphs.length; + nested(buffer, r.records[k + 1], at); + const delta = buffer.glyphs.length - before; + if (delta) { + for (let j = sequenceIndex + 1; j < positions.length; j++) positions[j] += delta; + end += delta; + } + } + return end; +} diff --git a/src/render/text.ts b/src/render/text.ts index 2fa60e8b..192d4c7e 100644 --- a/src/render/text.ts +++ b/src/render/text.ts @@ -6,6 +6,7 @@ import { Font, PathCommand } from './font.ts'; import { Hinter } from './hinting.ts'; import { ColorLayer, BitmapGlyph, parseColorLayers, parseBitmaps } from './color.ts'; +import { GlyphBuffer, parseGSUB } from './gsub.ts'; import { decode as decodePNG } from '../codecs/png.ts'; import { resample, TRIANGLE } from '../util/resample.ts'; @@ -37,7 +38,7 @@ interface ColorBitmap { top: number; } -/** a font with what's derived from it for one size */ +/** a font with what's derived from it for one size, and its glyphs (kept across calls) */ interface SizedFont { font: Font; /** pixels per font unit */ @@ -45,6 +46,8 @@ interface SizedFont { hinter: Hinter | null; layers: ((glyph: number) => ColorLayer[] | null) | null; bitmaps: ((glyph: number) => BitmapGlyph | null) | null; + gsub: ((buffer: GlyphBuffer) => void) | null; + glyphs: Map; } interface Glyph { @@ -61,8 +64,14 @@ interface Glyph { bitmaps: Array; /** the layers or the bitmap of color glyphs */ color: { layers: ColorLayer[] } | { bitmap: BitmapGlyph } | null; - /** color bitmaps per subpixel offset (bitmap glyphs only have the one for offset 0) */ - colors: Array; + /** color bitmaps by subpixel offset and text color (bitmap glyphs only have the one for offset 0) */ + colors: Map; +} + +/** a glyph of a shaped paragraph, and whether it is a space (a line can break after it) */ +interface ShapedGlyph { + glyph: Glyph; + space: boolean; } interface Line { @@ -77,6 +86,10 @@ const SUBPIXELS = 4; // parsed fonts are cached per memory region, so new views of the same font data (e.g. from node Buffers) hit the cache const fonts = new WeakMap>(); const hinters = new WeakMap(); +const substitutions = new WeakMap void) | null, layers: ((glyph: number) => ColorLayer[] | null) | null }>(); +// sizes (and hinting) per font, a few of them +const sizes = new WeakMap>(); +const SIZES = 16; function loadFont(data: Uint8Array): Font { let regions = fonts.get(data.buffer); @@ -88,12 +101,63 @@ function loadFont(data: Uint8Array): Font { return font; } +function sizedFont(data: Uint8Array, size: number, hinting: boolean): SizedFont { + const font = loadFont(data); + let bySize = sizes.get(font); + if (!bySize) sizes.set(font, bySize = new Map()); + const key = `${size}:${hinting}`; + let sized = bySize.get(key); + if (sized) return sized; + + let hinter: Hinter | null = null; + if (hinting && font.outlines === 'glyf') { + hinter = hinters.get(font) ?? null; + if (!hinter) hinters.set(font, hinter = new Hinter(font)); + if (!hinter.hasInstructions) hinter = null; + } + let shared = substitutions.get(font); + if (!shared) substitutions.set(font, shared = { gsub: parseGSUB(font), layers: parseColorLayers(font) }); + + if (bySize.size >= SIZES) bySize.clear(); + bySize.set(key, sized = { font, scale: size / font.unitsPerEm, hinter, layers: shared.layers, bitmaps: parseBitmaps(font, size), gsub: shared.gsub, glyphs: new Map() }); + return sized; +} + +// a glyph of a font, with its outline or color glyph data (hinted glyphs advance by whole pixels) +function glyphOf(f: SizedFont, index: number, size: number): Glyph { + let g = f.glyphs.get(index); + if (g) return g; + + const { font, scale, hinter } = f; + const bitmap = f.bitmaps?.(index) ?? null, layers = bitmap ? null : f.layers?.(index) ?? null; + const advance = font.advance(index) * scale; + g = { index, font: f, path: [], scale, advance: hinter ? Math.round(advance) : advance, ink: null, bitmaps: [], color: null, colors: new Map() }; + if (bitmap) { + const s = size / bitmap.ppem; + g.color = { bitmap }; + g.ink = { left: bitmap.left * s, right: (bitmap.left + bitmap.width) * s }; + } else if (layers) { + g.color = { layers }; + g.ink = inkExtent(layers.flatMap(layer => font.outline(layer.glyph)), scale); + } else { + const hinted = hinter && hinter.glyph(index, size); + if (hinted) Object.assign(g, { path: hinted.path, scale: 1, advance: hinted.advance, ink: inkExtent(hinted.path, 1) }); + else { + g.path = font.outline(index); + g.ink = inkExtent(g.path, scale); + } + } + f.glyphs.set(index, g); + return g; +} + /** * Lays out and rasterizes text - * Lines start at ceil(ascender) and advance by ceil(ascender - descender + lineGap) of the first font; every character - * comes from the first font that has a glyph for it. Glyphs are placed with subpixel precision using their advance - * widths and the font's kerning; the canvas spans from the origin to the furthest glyph edge - * @param fonts TrueType/OpenType fonts, by priority + * Lines start at ceil(ascender) and advance by ceil(ascender - descender + lineGap) of the first font. Every grapheme + * (a character with its joined, modifying and combining ones, like an emoji sequence) comes from the first font that + * has all of it; runs of a font are shaped with its substitutions (GSUB). Glyphs are placed with subpixel precision + * using their advance widths and the font's kerning; the canvas spans from the origin to the furthest glyph edge + * @param fonts TrueType/OpenType fonts, by priority (later ones are only loaded when the text needs them) * @param size font size in pixels per em * @param text * @param layout @@ -103,64 +167,23 @@ function loadFont(data: Uint8Array): Font { * @return the coverage of the outline glyphs, and the premultiplied RGBA pixels of color glyphs (null without any) */ export async function render(fonts: Uint8Array[], size: number, text: string, layout: Layout, hinting = false, foreground = 0xffffffff): Promise<{ width: number, height: number, coverage: Uint8Array, color: Uint8ClampedArray | null }> { - const sized: SizedFont[] = fonts.map(data => { - const font = loadFont(data); - let hinter: Hinter | null = null; - if (hinting && font.outlines === 'glyf') { - hinter = hinters.get(font) ?? null; - if (!hinter) hinters.set(font, hinter = new Hinter(font)); - if (!hinter.hasInstructions) hinter = null; - } - return { font, scale: size / font.unitsPerEm, hinter, layers: parseColorLayers(font), bitmaps: parseBitmaps(font, size) }; - }); - const primary = sized[0]; + const sized: SizedFont[] = []; + const font = (i: number): SizedFont => sized[i] ??= sizedFont(fonts[i], size, hinting); + const primary = font(0); const ascent = Math.ceil(primary.font.ascender * primary.scale); const lineHeight = Math.ceil((primary.font.ascender - primary.font.descender + primary.font.lineGap) * primary.scale); - const glyphs = new Map(); - const make = (f: SizedFont, index: number): Glyph => { - const { font, scale, hinter } = f; - const bitmap = f.bitmaps?.(index) ?? null, layers = bitmap ? null : f.layers?.(index) ?? null; - // hinted glyphs go on whole pixels (it only makes sense for hinted outlines) - const advance = font.advance(index) * scale; - const g: Glyph = { index, font: f, path: [], scale, advance: hinter ? Math.round(advance) : advance, ink: null, bitmaps: [], color: null, colors: [] }; - if (bitmap) { - const s = size / bitmap.ppem; - g.color = { bitmap }; - g.ink = { left: bitmap.left * s, right: (bitmap.left + bitmap.width) * s }; - } else if (layers) { - g.color = { layers }; - g.ink = inkExtent(layers.flatMap(layer => font.outline(layer.glyph)), scale); - } else { - const hinted = hinter && hinter.glyph(index, size); - if (hinted) Object.assign(g, { path: hinted.path, scale: 1, advance: hinted.advance, ink: inkExtent(hinted.path, 1) }); - else { - g.path = font.outline(index); - g.ink = inkExtent(g.path, scale); - } - } - return g; - }; - const glyph = (codepoint: number): Glyph => { - let g = glyphs.get(codepoint); - if (!g) { - const f = sized.find(f => f.font.glyphIndex(codepoint)); - // invisible characters (joiners, variation selectors, ...) show nothing even where fonts have glyphs for - // them (as HarfBuzz does), characters no font has show the first one's .notdef - if (defaultIgnorable(codepoint)) g = { index: 0, font: primary, path: [], scale: primary.scale, advance: 0, ink: null, bitmaps: [], color: null, colors: [] }; - else g = f ? make(f, f.font.glyphIndex(codepoint)) : make(primary, 0); - glyphs.set(codepoint, g); - } - return g; - }; + const invisible: Glyph = { index: 0, font: primary, path: [], scale: primary.scale, advance: 0, ink: null, bitmaps: [], color: null, colors: new Map() }; + const paragraphs = layout.wrapHardBreaks ? text.split(/\r\n|\r|\n/) : [text.replace(/\r\n|\r|\n/g, '')]; + const shaped = paragraphs.map(paragraph => shape(paragraph, fonts.length, font, size, invisible)); const kern = (left: Glyph, right: Glyph): number => { - const kerning = left.font === right.font && left.font.font.kerning; + const kerning = left.font === right.font && left !== invisible && right !== invisible && left.font.font.kerning; if (!kerning) return 0; const k = kerning(left.index, right.index) * left.font.scale; return left.font.hinter ? Math.round(k) : k; }; - const lines = wrap(text, layout, glyph, kern); + const lines = wrap(shaped, layout, kern); // lines that fit into maxHeight, chosen by the vertical alignment let visible = lines; @@ -195,10 +218,11 @@ export async function render(fonts: Uint8Array[], size: number, text: string, la if (g.color) { // bitmaps are placed on whole pixels if ('bitmap' in g.color) subpixel = 0; - if (g.colors[subpixel] === undefined) g.colors[subpixel] = 'bitmap' in g.color + const key = 'bitmap' in g.color ? '0' : `${subpixel}:${foreground}`; + let bitmap = g.colors.get(key); + if (bitmap === undefined) g.colors.set(key, bitmap = 'bitmap' in g.color ? await scaledBitmap(g.color.bitmap, size / g.color.bitmap.ppem) - : layeredGlyph(g, g.color.layers, subpixel / SUBPIXELS, foreground); - const bitmap = g.colors[subpixel]; + : layeredGlyph(g, g.color.layers, subpixel / SUBPIXELS, foreground)); if (bitmap) blitColor(color ??= new Uint8ClampedArray(width * height * 4), width, height, bitmap, pixel + bitmap.left, baseline - bitmap.top); continue; } @@ -212,14 +236,94 @@ export async function render(fonts: Uint8Array[], size: number, text: string, la return { width, height, coverage, color }; } +/** + * Turns a paragraph into glyphs: graphemes go to the first font that has all of their (visible) characters, + * preferring color glyphs before an emoji variation selector and others before a text one; each run of a font is + * mapped to glyphs and substituted. Invisible characters that weren't substituted show nothing + */ +function shape(paragraph: string, count: number, font: (i: number) => SizedFont, size: number, invisible: Glyph): ShapedGlyph[] { + const codepoints = Array.from(paragraph, char => char.codePointAt(0) as number); + const has = (f: SizedFont, codepoint: number): boolean => f.font.glyphIndex(codepoint) !== 0; + const isColor = (f: SizedFont, codepoint: number): boolean => { + const index = f.font.glyphIndex(codepoint); + return !!(f.bitmaps?.(index) || f.layers?.(index)); + }; + + // the font of a grapheme (later fonts are only loaded when the earlier ones miss a character) + const choose = (start: number, end: number): number => { + const needed = codepoints.slice(start, end).filter(c => !defaultIgnorable(c)); + const variation = codepoints.slice(start, end).find(c => c === 0xfe0f || c === 0xfe0e); + let first = -1; + for (let i = 0; i < count; i++) { + const f = font(i); + if (!needed.every(c => has(f, c))) continue; + if (first < 0) first = i; + if (!variation || !needed.length || (variation === 0xfe0f) === isColor(f, needed[0])) return i; + } + return first; + }; + + // fonts per character: whole graphemes where a font has all of them, else character by character + const fontOf = new Int32Array(codepoints.length); + for (const [start, end] of graphemes(codepoints)) { + const chosen = choose(start, end); + for (let k = start; k < end; k++) fontOf[k] = chosen >= 0 ? chosen : Math.max(0, choose(k, k + 1)); + } + + const shaped: ShapedGlyph[] = []; + for (let start = 0; start < codepoints.length;) { + let end = start + 1; + while (end < codepoints.length && fontOf[end] === fontOf[start]) end++; + const f = font(fontOf[start]); + + // invisible characters the font has no glyph for are left out of the run + const buffer: GlyphBuffer = { glyphs: [], clusters: [] }; + for (let k = start; k < end; k++) { + const index = f.font.glyphIndex(codepoints[k]); + if (!index && defaultIgnorable(codepoints[k])) continue; + buffer.glyphs.push(index); + buffer.clusters.push(k); + } + f.gsub?.(buffer); + + for (let j = 0; j < buffer.glyphs.length; j++) { + const codepoint = codepoints[buffer.clusters[j]], index = buffer.glyphs[j]; + const hidden = defaultIgnorable(codepoint) && index === f.font.glyphIndex(codepoint); + shaped.push({ glyph: hidden ? invisible : glyphOf(f, index, size), space: codepoint === 0x20 || codepoint === 0x09 }); + } + start = end; + } + return shaped; +} + +// characters that extend the grapheme before them: combining marks, variation selectors, emoji modifiers, the keycap, +// tags and joiners (which also join the next character) +const EXTENDING = [0x300, 0x36f, 0x1ab0, 0x1aff, 0x1dc0, 0x1dff, 0x200d, 0x200d, 0x20d0, 0x20ff, 0xfe00, 0xfe0f, 0xfe20, 0xfe2f, + 0x1f3fb, 0x1f3ff, 0xe0020, 0xe007f, 0xe0100, 0xe01ef]; +const inRanges = (ranges: number[], codepoint: number): boolean => { + for (let i = 0; i < ranges.length; i += 2) if (codepoint >= ranges[i] && codepoint <= ranges[i + 1]) return true; + return false; +}; +const regionalIndicator = (codepoint: number): boolean => codepoint >= 0x1f1e6 && codepoint <= 0x1f1ff; + +// grapheme ranges (simplified UAX #29 for emoji): extensions, joined sequences and pairs of regional indicators +function graphemes(codepoints: number[]): Array<[number, number]> { + const ranges: Array<[number, number]> = []; + for (let start = 0; start < codepoints.length;) { + let end = start + 1; + if (regionalIndicator(codepoints[start]) && end < codepoints.length && regionalIndicator(codepoints[end])) end++; + while (end < codepoints.length && (inRanges(EXTENDING, codepoints[end]) || codepoints[end - 1] === 0x200d)) end++; + ranges.push([start, end]); + start = end; + } + return ranges; +} + // default ignorable code points (Unicode's DerivedCoreProperties), which render as nothing const IGNORABLE = [0xad, 0xad, 0x34f, 0x34f, 0x61c, 0x61c, 0x115f, 0x1160, 0x17b4, 0x17b5, 0x180b, 0x180f, 0x200b, 0x200f, 0x202a, 0x202e, 0x2060, 0x206f, 0x3164, 0x3164, 0xfe00, 0xfe0f, 0xfeff, 0xfeff, 0xffa0, 0xffa0, 0xfff0, 0xfff8, 0x1bca0, 0x1bca3, 0x1d173, 0x1d17a, 0xe0000, 0xe0fff]; -function defaultIgnorable(codepoint: number): boolean { - for (let i = 0; i < IGNORABLE.length; i += 2) if (codepoint >= IGNORABLE[i] && codepoint <= IGNORABLE[i + 1]) return true; - return false; -} +const defaultIgnorable = (codepoint: number): boolean => inRanges(IGNORABLE, codepoint); // a bitmap glyph's PNG decoded and scaled from its strike to the font size, premultiplied async function scaledBitmap(glyph: BitmapGlyph, scale: number): Promise { @@ -289,10 +393,9 @@ function inkExtent(path: PathCommand[], scale: number): Glyph['ink'] { return right > left ? { left, right } : null; } -// breaks text into lines of positioned glyphs -function wrap(text: string, layout: Layout, glyph: (codepoint: number) => Glyph, kern: (left: Glyph, right: Glyph) => number): Line[] { +// breaks shaped paragraphs into lines of positioned glyphs +function wrap(paragraphs: ShapedGlyph[][], layout: Layout, kern: (left: Glyph, right: Glyph) => number): Line[] { const lines: Line[] = []; - const paragraphs = layout.wrapHardBreaks ? text.split(/\r\n|\r|\n/) : [text.replace(/\r\n|\r|\n/g, '')]; // right edge of a glyph placed at x (glyphs without ink don't extend the line) const edge = (x: number, g: Glyph): number => g.ink ? x + g.ink.right : 0; @@ -320,11 +423,7 @@ function wrap(text: string, layout: Layout, glyph: (codepoint: number) => Glyph, breakAt = -1; }; - for (const char of paragraph) { - const codepoint = char.codePointAt(0) as number; - const g = glyph(codepoint); - const space = codepoint === 0x20 || codepoint === 0x09; - + for (const { glyph: g, space } of paragraph) { if (!space && line.glyphs.length && edge(next(g), g) > layout.maxWidth) { if (layout.wrapStyle === 'word' && breakAt > 0) { // move the current word to the next line diff --git a/tests/font.js b/tests/font.js index eb37a289..d404af46 100644 --- a/tests/font.js +++ b/tests/font.js @@ -155,11 +155,11 @@ const panic = message => { } { - // color glyphs from fallback fonts: CBDT bitmaps (Noto Color Emoji) and COLR layers (Twemoji) + // color glyphs from fallback fonts: CBDT bitmaps (Noto Color Emoji) and COLR layers (Twemoji), emoji sequences const sans = await fs.readFile('./tests/fonts/sourcesans3 regular.otf'); for (const [file, target] of [['noto color emoji subset.ttf', 'emoji-cbdt'], ['twemoji mozilla subset.ttf', 'emoji-colr']]) { const emoji = await fs.readFile(`./tests/fonts/${file}`); - const image = await Image.renderText([sans, emoji], 48, 'Hi 😀🔥 👍', {color: 0x000000ff}); + const image = await Image.renderText([sans, emoji], 48, 'Hi 😀🔥 👍🏽 🇩🇪 1️⃣ ❤️ 👨‍👩‍👧', {color: 0x000000ff}); const encoded = await image.encode(1, {creationTime: 0, software: ''}); if (process.env.OVERWRITE_TEST) await fs.writeFile(`./tests/targets/${target}.png`, encoded); @@ -174,6 +174,17 @@ const panic = message => { const plain = await Image.renderText([sans, emoji], 48, '😀😀'), joined = await Image.renderText([sans, emoji], 48, '😀\u200d\ufe0f😀'); if (plain.width !== joined.width) panic(`${file}: invisible characters should take no room (${plain.width} vs ${joined.width})`); + // sequences are one glyph (GSUB ligatures): skin tones, flags, keycaps, joined ones + const one = (await Image.renderText(emoji, 48, '👍')).width; + for (const sequence of ['👍🏽', '🇩🇪', '1️⃣', '👨‍👩‍👧']) + if ((await Image.renderText([sans, emoji], 48, sequence)).width > one * 1.2) panic(`${file}: ${sequence} should be a single glyph`); + + // the emoji variation selector picks the color glyph over the text font's heart + const heart = await Image.renderText([sans, emoji], 48, '❤️', {color: 0x000000ff}); + let red = 0; + for (let i = 0; i < heart.bitmap.length; i += 4) if (heart.bitmap[i + 3] > 128 && heart.bitmap[i] > 150 && heart.bitmap[i + 1] < 100) red++; + if (red < 100) panic(`${file}: ❤️ should be the red emoji`); + // characters the emoji font doesn't have come from the next font (the first one still sets the line height) const fallback = await Image.renderText([emoji, sans], 48, 'Hi'), direct = await Image.renderText(sans, 48, 'Hi'); const ink = image => image.bitmap.reduce((sum, v, i) => (i & 3) === 3 ? sum + v : sum, 0); diff --git a/tests/targets/emoji-cbdt.png b/tests/targets/emoji-cbdt.png index caa07d02d7496020c5db417e5ef365918c0cc67d..60b2234a81178acfbd7e4e4825914eb5f4b3ef64 100644 GIT binary patch literal 22066 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 Y1CSO~BRn|#IRF3v07*qoM6N<$g8$_59{>OV literal 12143 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 diff --git a/tests/targets/emoji-colr.png b/tests/targets/emoji-colr.png index edc21c7d430773e288bcbef494ad9b7b39e8755b..4a8ea876b6cb04ff9768d4fcbdeda9a4326471be 100644 GIT binary patch literal 11987 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 literal 6438 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