Skip to content

Commit

Permalink
COLR/CPAL color glyph rendering (#695)
Browse files Browse the repository at this point in the history
* fix incorrect colorRecordIndices in CPAL test data

* Implement drawing of COLR/CPAL layers

* font inspector: display CPAL data and palettes

* glyph inspector: implement drawing of CPAL/COLR glyphs

* docs demo page: fix typo causing JS error and not updating X/Y values next to range sliders

* docs demo page: implement drawing and snapping of COLR/CPAL glyphs

* font inspector: add update button

* font inspector: fix old font information not being overwritten if not present in newly loaded font

* docs: styling for color palettes

* update example code

* WIP: update documentation in README

* add rgba() and hsla() parsing and tests

* add test for hsl() turn unit parsing, less strict regex

* fix: don't draw the BaseGlyph itself, completele replace it with the color glyph

* ignore colr table if no cpal table is present

* log warnings on SVG output of color glyphs, add @todo comments

* fix: make sure defaultRenderOptions are actually used where needed

* fix wrong defaultRenderOptions

* Glyph Inspector: implement palette preview and selection, options.drawLayers checkbox

* add palettes.js tests

* use 'hexa' as the default color format

* update test to account for 'hexa' as the default color value

* implement setColor() and add tests

* implement palettes.delete()

* implement css color name parsing

* test css color name parsing

* initialize fullPath.layers, pass font to glyph.drawPoints()

* move layers to glyph, except for generated paths

* path drawing optimizations

* implement LayerManager, show layers in glyph inspector

* allow parsing of COLRv1/CPALv1 fonts, as they may provide color glyphs in v0 format

* test CPAL baseGlyphs order in Font.validate()

* have Font.validate() return the warnings array and log console warnings (fixes #607)

* implement Font.palettes.deleteColor()

* glyph inspector improvements regarding palette colors

* rename Path.layers to Path._layers so it's not confused with the Glyph.layers property's LayerManager

* WIP: README update, PaletteManager fixes and tests

* finish palette tests

* layer tests and fixes

* handle CHARARRAY null value

* update README for Font.layers

* add basic palette and color handling to glyph inspector
  • Loading branch information
Connum authored Apr 8, 2024
1 parent 6408975 commit 946f255
Show file tree
Hide file tree
Showing 26 changed files with 2,069 additions and 58 deletions.
117 changes: 112 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,17 @@ Create a Path that represents the given text.
* `x`: Horizontal position of the beginning of the text. (default: `0`)
* `y`: Vertical position of the *baseline* of the text. (default: `0`)
* `fontSize`: Size of the text in pixels (default: `72`).
* `options`: _{GlyphRenderOptions}_ passed to each glyph, see below

Options is an optional object containing:
Options is an optional _{GlyphRenderOptions}_ object containing:
* `script`: script used to determine which features to apply (default: `"DFLT"` or `"latn"`)
* `language`: language system used to determine which features to apply (default: `"dflt"`)
* `kerning`: if true takes kerning information into account (default: `true`)
* `features`: an object with [OpenType feature tags](https://docs.microsoft.com/en-us/typography/opentype/spec/featuretags) as keys, and a boolean value to enable each feature.
Currently only ligature features `"liga"` and `"rlig"` are supported (default: `true`).
* `hinting`: if true uses TrueType font hinting if available (default: `false`).
* `colorFormat`: the format colors are converted to for rendering (default: `"hexa"`). Can be `"rgb"`/`"rgba"` for `rgb()`/`rgba()` output, `"hex"`/`"hexa"` for 6/8 digit hex colors, or `"hsl"`/`"hsla"` for `hsl()`/`hsla()` output. `"bgra"` outputs an object with r, g, b, a keys (r/g/b from 0-255, a from 0-1). `"raw"` outputs an integer as used in the CPAL table.
* `fill`: font color, the color used to render each glyph (default: `"black"`)

_**Note:** there is also `Font.getPaths()` with the same arguments, which returns a list of Paths._

Expand All @@ -207,6 +212,7 @@ Create a Path that represents the given text.
* `x`: Horizontal position of the beginning of the text. (default: `0`)
* `y`: Vertical position of the *baseline* of the text. (default: `0`)
* `fontSize`: Size of the text in pixels (default: `72`).
* `options`: _{GlyphRenderOptions}_ passed to each glyph, see `Font.getPath()`

Options is an optional object containing:
* `kerning`: if `true`, takes kerning information into account (default: `true`)
Expand Down Expand Up @@ -244,7 +250,101 @@ bounding box than its advance width.

This corresponds to `canvas2dContext.measureText(text).width`
* `fontSize`: Size of the text in pixels (default: `72`).
* `options`: See `Font.getPath()`
* `options`: _{GlyphRenderOptions}_, see `Font.getPath()`

#### The `Font.palettes` object (`PaletteManager`)

This allows to manage the palettes and colors in the CPAL table, without having to modify the table manually.

###### `Font.palettes.add(colors)`
Add a new palette.
* `colors`: (optional) colors to add to the palette, differences to existing palettes will be filled with the defaultValue.

###### `Font.palettes.delete(paletteIndex)`
Deletes a palette by its zero-based index
* `paletteIndex`: zero-based palette index

###### `Font.palettes.deleteColor(colorIndex, replacementIndex)`
Deletes a specific color index in all palettes and updates all layers using that color with the color currently held in the replacement index
* `colorIndex`: index of the color that should be deleted
* `replacementIndex`: index (according to the palette before deletion) of the color to replace in layers using the color to be to deleted

###### `Font.palettes.cpal()`
Returns the font's cpal table, or false if it does not exist. Used internally.

###### `Font.palettes.ensureCPAL(colors)`
Mainly used internally. Makes sure that the CPAL table exists or is populated with default values.
* `colors`: (optional) colors to populate on creation
returns `true` if it was created, `false` if it already existed.

###### `Font.palettes.extend(num)`
Extend all existing palettes and the numPaletteEntries value by a number of color slots
* `num`: number of additional color slots to add to all palettes

###### `Font.palettes.fillPalette(palette, colors, colorCount)`
Fills a set of palette colors (from a palette index, or a provided array of CPAL color values) with a set of colors, falling back to the default color value, until a given count. *It does not modify the existing palette, returning a new array instead!* Use `Font.palettes.setColor()` instead if needed.
* `palette`: palette index or an Array of CPAL color values to fill the palette with, the rest will be filled with the default color
* `colors`: array of color values to fill the palette with, in a format supported as an output of `colorFormat` in _{GlyphRenderOptions}_, see `Font.getPath()`. CSS color names are also supported in browser context.
* `colorCount`: Number of colors to fill the palette with, defaults to the value of the numPaletteEntries field

###### `Font.palettes.getAll(colorFormat)`
Returns an array of arrays of color values for each palette, optionally in a specified color format
* `colorFormat`: (optional) See _{GlyphRenderOptions}_ at `Font.getPath()`, (default: `"hexa"`)

###### `Font.palettes.getColor(index, paletteIndex, colorFormat)`
Get a specific palette by its zero-based index
* `index`: zero-based index of the color in the palette
* `paletteIndex`: zero-based palette index (default: 0)
* `colorFormat`: (optional) See _{GlyphRenderOptions}_ at `Font.getPath()`, (default: `"hexa"`)

###### `Font.palettes.get(paletteIndex, colorFormat)`
Get a specific palette by its zero-based index
* `paletteIndex`: zero-based palette index
* `colorFormat`: (optional) See _{GlyphRenderOptions}_ at `Font.getPath()`, (default: `"hexa"`)

###### `Font.palettes.setColor(index, colors, paletteIndex)`
Set one or more colors on a specific palette by its zero-based index
* `index`: zero-based color index to start filling from
* `color`: color value or array of color values in a color notation supported as an output of `colorFormat` in _{GlyphRenderOptions}_, see `Font.getPath()`. CSS color names are also supported in browser context.
* `paletteIndex`: zero-based palette index (default: 0)

###### `Font.palettes.toCPALcolor(color)`
Converts a color value string to a CPAL integer color value
* `color`: string in a color notation supported as an output of `colorFormat` in _{GlyphRenderOptions}_, see `Font.getPath()`. CSS color names are also supported in browser context.


##### The `Font.layers` object (`LayerManager`)

This allows to manage the color glyph layers in the COLR table, without having to modify the table manually.

###### `Font.layers.add(glyphIndex, layers, position)`
Adds one or more layers to a glyph, at the end or at a specific position.
* `glyphIndex`: glyph index to add the layer(s) to.
* `layers`: layer object {glyph, paletteIndex}/{glyphID, paletteIndex} or array of layer objects.
* `position`: position to insert the layers at (will default to adding at the end).

###### `Font.layers.ensureCOLR()`
Mainly used internally. Ensures that the COLR table exists and is populated with default values.

###### `Font.layers.get(glyphIndex)`
Gets the layers for a specific glyph
* `glyphIndex`
Returns an array of `{glyph, paletteIndex}` layer objects.

###### `Font.layers.remove(glyphIndex, start, end = start)`
Removes one or more layers from a glyph.
* `glyphIndex`: glyph index to remove the layer(s) from
* `start`: index to remove the layer at
* `end`: (optional) if provided, removes all layers from start index to (and including) end index

###### `Font.layers.setPaletteIndex(glyphIndex, layerIndex, paletteIndex)`
Sets a color glyph layer's paletteIndex property to a new index
* `glyphIndex`: glyph in the font by zero-based glyph index
* `layerIndex`: layer in the glyph by zero-based layer index
* `paletteIndex`: new color to set for the layer by zero-based index in any palette

###### `Font.layers.updateColrTable(glyphIndex, layers)`
Mainly used internally. Updates the colr table, adding a baseGlyphRecord if needed, ensuring that it's inserted at the correct position, updating numLayers, and adjusting firstLayerIndex values for all baseGlyphRecords according to any deletions or insertions.

#### The Glyph object
A Glyph is an individual mark that often corresponds to a character. Some glyphs, such as ligatures, are a combination of many characters. Glyphs are the basic building blocks of a font.
Expand All @@ -259,24 +359,28 @@ A Glyph is an individual mark that often corresponds to a character. Some glyphs
* `xMin`, `yMin`, `xMax`, `yMax`: The bounding box of the glyph.
* `path`: The raw, unscaled path of the glyph.

##### `Glyph.getPath(x, y, fontSize)`
##### `Glyph.getPath(x, y, fontSize, options, font)`
Get a scaled glyph Path object for use on a drawing context.
* `x`: Horizontal position of the glyph. (default: `0`)
* `y`: Vertical position of the *baseline* of the glyph. (default: `0`)
* `fontSize`: Font size in pixels (default: `72`).
* `options`: _{GlyphRenderOptions}_, see `Font.getPath()`
* `font`: a font object, needed for rendering COLR/CPAL fonts to get the layers and colors

##### `Glyph.getBoundingBox()`
Calculate the minimum bounding box for the unscaled path of the given glyph. Returns an `opentype.BoundingBox` object that contains `x1`/`y1`/`x2`/`y2`.
If the glyph has no points (e.g. a space character), all coordinates will be zero.

##### `Glyph.draw(ctx, x, y, fontSize)`
##### `Glyph.draw(ctx, x, y, fontSize, options, font)`
Draw the glyph on the given context.
* `ctx`: The drawing context.
* `x`: Horizontal position of the glyph. (default: `0`)
* `y`: Vertical position of the *baseline* of the glyph. (default: `0`)
* `fontSize`: Font size, in pixels (default: `72`).
* `options`: _{GlyphRenderOptions}_, see `Font.getPath()`
* `font`: a font object, needed for rendering COLR/CPAL fonts to get the layers and colors

##### `Glyph.drawPoints(ctx, x, y, fontSize)`
##### `Glyph.drawPoints(ctx, x, y, fontSize, options, font)`
Draw the points of the glyph on the given context.
On-curve points will be drawn in blue, off-curve points will be drawn in red.
The arguments are the same as `Glyph.draw()`.
Expand All @@ -291,6 +395,9 @@ The arguments are the same as `Glyph.draw()`.
##### `Glyph.toPathData(options)`, `Glyph.toDOMElement(options)`, `Glyph.toSVG(options)`, `Glyph.fromSVG(pathData, options)`,
These are currently only wrapper functions for their counterparts on Path objects (see documentation there), but may be extended in the future to pass on Glyph data for automatic calculation.

##### `Glyph.getLayers(font)`
Gets the color glyph layers for this glyph from the specified font's COLR/CPAL tables

### The Path object
Once you have a path through `Font.getPath()` or `Glyph.getPath()`, you can use it.

Expand Down
2 changes: 1 addition & 1 deletion docs/examples/creating-fonts.html
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ <h1><output name="fontFamilyName"></output></h1>
var x = 50;
var y = 120;
var fontSize = 72;
glyph.draw(ctx, x, y, fontSize);
glyph.draw(ctx, x, y, fontSize, {}, font2);
glyph.drawPoints(ctx, x, y, fontSize);
glyph.drawMetrics(ctx, x, y, fontSize);
}
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/reading-writing.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ <h1><output name="fontFamilyName"></output></h1>
const x = 50;
const y = 120;
const fontSize = 72;
glyph.draw(ctx, x, y, fontSize);
glyph.draw(ctx, x, y, fontSize, {}, font2);
glyph.drawPoints(ctx, x, y, fontSize);
glyph.drawMetrics(ctx, x, y, fontSize);
}
Expand Down
33 changes: 31 additions & 2 deletions docs/font-inspector.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,17 @@ <h3 class="collapsed">Glyph Substitution Table <a href="https://www.microsoft.co

<h3 class="collapsed">Kerning <a href="https://www.microsoft.com/typography/otspec/kern.htm" target="_blank">kern</a></h3>
<dl id="kern-table"><dt>Undefined</dt></dl>

<h3 class="collapsed">CPAL color palettes</h3>
<dl id="cpal-table"><dt>Undefined</dt></dl>
</div>

<hr>

<button type="button" id="update">update</button> after modifying window.font

<hr>

<div class="explain">
<h1>Free Software</h1>
<p>opentype.js is available on <a href="https://github.com/opentypejs/opentype.js">GitHub</a> under the <a href="https://raw.github.com/opentypejs/opentype.js/master/LICENSE">MIT License</a>.</p>
Expand Down Expand Up @@ -127,7 +134,7 @@ <h1>Free Software</h1>
var previewCanvas = document.getElementById('preview');
var previewCtx = previewCanvas.getContext("2d");
previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height);
font.draw(previewCtx, textToRender, 0, 32, fontSize, {
var options = {
kerning: true,
features: [
/**
Expand All @@ -137,7 +144,9 @@ <h1>Free Software</h1>
{ script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] },
{ script: 'latn', tags: ['liga', 'rlig'] }
]
});
};
options = Object.assign({}, font.defaultRenderOptions, options);
font.draw(previewCtx, textToRender, 0, 32, fontSize, options);
}

function showErrorMessage(message) {
Expand Down Expand Up @@ -193,6 +202,9 @@ <h1>Free Software</h1>
function displayFontData(font) {
var html, tablename, table, property, value;

// reset all lists first
document.querySelectorAll('#font-data dl').forEach( dl => dl.innerHTML = '<dt>Undefined</dt>' );

for (tablename in font.tables) {
table = font.tables[tablename];
if (tablename == 'name') {
Expand Down Expand Up @@ -231,6 +243,21 @@ <h1>Free Software</h1>
element.innerHTML = '<dt>' + Object.keys(font.kerningPairs).length + ' Pairs</dt><dd>' + JSON.stringify(font.kerningPairs) + '</dd>';
}
}

if(font.tables.cpal) {
let markup = ' ';
const cpal = document.getElementById("cpal-table");
const dt = cpal.querySelector('dt');
if ( dt ) {
const palettes = font.palettes.getAll('hexa');
if ( palettes.length ) {
cpal.innerHTML += palettes.map((palette, idx) =>
`<dt><strong>↪ Palette ${idx}</strong></dt><dd><div class="color-swatches">`+
palette.map(color => `<span class="color-swatch" style="background:${color}" title="${color}"></span>`).join('')+
`</div></dd>`).join('');
}
}
}
}

function onFontLoaded(font) {
Expand All @@ -239,6 +266,8 @@ <h1>Free Software</h1>
displayFontData(font);
}

document.getElementById('update').addEventListener('click', () => displayFontData(window.font));

var tableHeaders = document.getElementById('font-data').getElementsByTagName('h3');
for(var i = tableHeaders.length; i--; ) {
tableHeaders[i].addEventListener('click', function(e) {
Expand Down
Loading

0 comments on commit 946f255

Please sign in to comment.