Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/render/font.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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) {
Expand Down
359 changes: 359 additions & 0 deletions src/render/gsub.ts

Large diffs are not rendered by default.

241 changes: 170 additions & 71 deletions src/render/text.ts

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions tests/font.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Binary file modified tests/targets/emoji-cbdt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/targets/emoji-colr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading