Skip to content

Commit 946f255

Browse files
authored
COLR/CPAL color glyph rendering (#695)
* 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
1 parent 6408975 commit 946f255

26 files changed

+2069
-58
lines changed

README.md

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,17 @@ Create a Path that represents the given text.
192192
* `x`: Horizontal position of the beginning of the text. (default: `0`)
193193
* `y`: Vertical position of the *baseline* of the text. (default: `0`)
194194
* `fontSize`: Size of the text in pixels (default: `72`).
195+
* `options`: _{GlyphRenderOptions}_ passed to each glyph, see below
195196

196-
Options is an optional object containing:
197+
Options is an optional _{GlyphRenderOptions}_ object containing:
198+
* `script`: script used to determine which features to apply (default: `"DFLT"` or `"latn"`)
199+
* `language`: language system used to determine which features to apply (default: `"dflt"`)
197200
* `kerning`: if true takes kerning information into account (default: `true`)
198201
* `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.
199202
Currently only ligature features `"liga"` and `"rlig"` are supported (default: `true`).
200203
* `hinting`: if true uses TrueType font hinting if available (default: `false`).
204+
* `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.
205+
* `fill`: font color, the color used to render each glyph (default: `"black"`)
201206

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

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

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

245251
This corresponds to `canvas2dContext.measureText(text).width`
246252
* `fontSize`: Size of the text in pixels (default: `72`).
247-
* `options`: See `Font.getPath()`
253+
* `options`: _{GlyphRenderOptions}_, see `Font.getPath()`
254+
255+
#### The `Font.palettes` object (`PaletteManager`)
256+
257+
This allows to manage the palettes and colors in the CPAL table, without having to modify the table manually.
258+
259+
###### `Font.palettes.add(colors)`
260+
Add a new palette.
261+
* `colors`: (optional) colors to add to the palette, differences to existing palettes will be filled with the defaultValue.
262+
263+
###### `Font.palettes.delete(paletteIndex)`
264+
Deletes a palette by its zero-based index
265+
* `paletteIndex`: zero-based palette index
266+
267+
###### `Font.palettes.deleteColor(colorIndex, replacementIndex)`
268+
Deletes a specific color index in all palettes and updates all layers using that color with the color currently held in the replacement index
269+
* `colorIndex`: index of the color that should be deleted
270+
* `replacementIndex`: index (according to the palette before deletion) of the color to replace in layers using the color to be to deleted
271+
272+
###### `Font.palettes.cpal()`
273+
Returns the font's cpal table, or false if it does not exist. Used internally.
274+
275+
###### `Font.palettes.ensureCPAL(colors)`
276+
Mainly used internally. Makes sure that the CPAL table exists or is populated with default values.
277+
* `colors`: (optional) colors to populate on creation
278+
returns `true` if it was created, `false` if it already existed.
279+
280+
###### `Font.palettes.extend(num)`
281+
Extend all existing palettes and the numPaletteEntries value by a number of color slots
282+
* `num`: number of additional color slots to add to all palettes
283+
284+
###### `Font.palettes.fillPalette(palette, colors, colorCount)`
285+
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.
286+
* `palette`: palette index or an Array of CPAL color values to fill the palette with, the rest will be filled with the default color
287+
* `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.
288+
* `colorCount`: Number of colors to fill the palette with, defaults to the value of the numPaletteEntries field
289+
290+
###### `Font.palettes.getAll(colorFormat)`
291+
Returns an array of arrays of color values for each palette, optionally in a specified color format
292+
* `colorFormat`: (optional) See _{GlyphRenderOptions}_ at `Font.getPath()`, (default: `"hexa"`)
293+
294+
###### `Font.palettes.getColor(index, paletteIndex, colorFormat)`
295+
Get a specific palette by its zero-based index
296+
* `index`: zero-based index of the color in the palette
297+
* `paletteIndex`: zero-based palette index (default: 0)
298+
* `colorFormat`: (optional) See _{GlyphRenderOptions}_ at `Font.getPath()`, (default: `"hexa"`)
299+
300+
###### `Font.palettes.get(paletteIndex, colorFormat)`
301+
Get a specific palette by its zero-based index
302+
* `paletteIndex`: zero-based palette index
303+
* `colorFormat`: (optional) See _{GlyphRenderOptions}_ at `Font.getPath()`, (default: `"hexa"`)
304+
305+
###### `Font.palettes.setColor(index, colors, paletteIndex)`
306+
Set one or more colors on a specific palette by its zero-based index
307+
* `index`: zero-based color index to start filling from
308+
* `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.
309+
* `paletteIndex`: zero-based palette index (default: 0)
310+
311+
###### `Font.palettes.toCPALcolor(color)`
312+
Converts a color value string to a CPAL integer color value
313+
* `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.
314+
315+
316+
##### The `Font.layers` object (`LayerManager`)
317+
318+
This allows to manage the color glyph layers in the COLR table, without having to modify the table manually.
319+
320+
###### `Font.layers.add(glyphIndex, layers, position)`
321+
Adds one or more layers to a glyph, at the end or at a specific position.
322+
* `glyphIndex`: glyph index to add the layer(s) to.
323+
* `layers`: layer object {glyph, paletteIndex}/{glyphID, paletteIndex} or array of layer objects.
324+
* `position`: position to insert the layers at (will default to adding at the end).
325+
326+
###### `Font.layers.ensureCOLR()`
327+
Mainly used internally. Ensures that the COLR table exists and is populated with default values.
328+
329+
###### `Font.layers.get(glyphIndex)`
330+
Gets the layers for a specific glyph
331+
* `glyphIndex`
332+
Returns an array of `{glyph, paletteIndex}` layer objects.
333+
334+
###### `Font.layers.remove(glyphIndex, start, end = start)`
335+
Removes one or more layers from a glyph.
336+
* `glyphIndex`: glyph index to remove the layer(s) from
337+
* `start`: index to remove the layer at
338+
* `end`: (optional) if provided, removes all layers from start index to (and including) end index
339+
340+
###### `Font.layers.setPaletteIndex(glyphIndex, layerIndex, paletteIndex)`
341+
Sets a color glyph layer's paletteIndex property to a new index
342+
* `glyphIndex`: glyph in the font by zero-based glyph index
343+
* `layerIndex`: layer in the glyph by zero-based layer index
344+
* `paletteIndex`: new color to set for the layer by zero-based index in any palette
345+
346+
###### `Font.layers.updateColrTable(glyphIndex, layers)`
347+
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.
248348

249349
#### The Glyph object
250350
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.
@@ -259,24 +359,28 @@ A Glyph is an individual mark that often corresponds to a character. Some glyphs
259359
* `xMin`, `yMin`, `xMax`, `yMax`: The bounding box of the glyph.
260360
* `path`: The raw, unscaled path of the glyph.
261361

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

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

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

279-
##### `Glyph.drawPoints(ctx, x, y, fontSize)`
383+
##### `Glyph.drawPoints(ctx, x, y, fontSize, options, font)`
280384
Draw the points of the glyph on the given context.
281385
On-curve points will be drawn in blue, off-curve points will be drawn in red.
282386
The arguments are the same as `Glyph.draw()`.
@@ -291,6 +395,9 @@ The arguments are the same as `Glyph.draw()`.
291395
##### `Glyph.toPathData(options)`, `Glyph.toDOMElement(options)`, `Glyph.toSVG(options)`, `Glyph.fromSVG(pathData, options)`,
292396
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.
293397

398+
##### `Glyph.getLayers(font)`
399+
Gets the color glyph layers for this glyph from the specified font's COLR/CPAL tables
400+
294401
### The Path object
295402
Once you have a path through `Font.getPath()` or `Glyph.getPath()`, you can use it.
296403

docs/examples/creating-fonts.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ <h1><output name="fontFamilyName"></output></h1>
139139
var x = 50;
140140
var y = 120;
141141
var fontSize = 72;
142-
glyph.draw(ctx, x, y, fontSize);
142+
glyph.draw(ctx, x, y, fontSize, {}, font2);
143143
glyph.drawPoints(ctx, x, y, fontSize);
144144
glyph.drawMetrics(ctx, x, y, fontSize);
145145
}

docs/examples/reading-writing.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ <h1><output name="fontFamilyName"></output></h1>
7171
const x = 50;
7272
const y = 120;
7373
const fontSize = 72;
74-
glyph.draw(ctx, x, y, fontSize);
74+
glyph.draw(ctx, x, y, fontSize, {}, font2);
7575
glyph.drawPoints(ctx, x, y, fontSize);
7676
glyph.drawMetrics(ctx, x, y, fontSize);
7777
}

docs/font-inspector.html

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,17 @@ <h3 class="collapsed">Glyph Substitution Table <a href="https://www.microsoft.co
7676

7777
<h3 class="collapsed">Kerning <a href="https://www.microsoft.com/typography/otspec/kern.htm" target="_blank">kern</a></h3>
7878
<dl id="kern-table"><dt>Undefined</dt></dl>
79+
80+
<h3 class="collapsed">CPAL color palettes</h3>
81+
<dl id="cpal-table"><dt>Undefined</dt></dl>
7982
</div>
8083

8184
<hr>
8285

86+
<button type="button" id="update">update</button> after modifying window.font
87+
88+
<hr>
89+
8390
<div class="explain">
8491
<h1>Free Software</h1>
8592
<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>
@@ -127,7 +134,7 @@ <h1>Free Software</h1>
127134
var previewCanvas = document.getElementById('preview');
128135
var previewCtx = previewCanvas.getContext("2d");
129136
previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height);
130-
font.draw(previewCtx, textToRender, 0, 32, fontSize, {
137+
var options = {
131138
kerning: true,
132139
features: [
133140
/**
@@ -137,7 +144,9 @@ <h1>Free Software</h1>
137144
{ script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] },
138145
{ script: 'latn', tags: ['liga', 'rlig'] }
139146
]
140-
});
147+
};
148+
options = Object.assign({}, font.defaultRenderOptions, options);
149+
font.draw(previewCtx, textToRender, 0, 32, fontSize, options);
141150
}
142151

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

205+
// reset all lists first
206+
document.querySelectorAll('#font-data dl').forEach( dl => dl.innerHTML = '<dt>Undefined</dt>' );
207+
196208
for (tablename in font.tables) {
197209
table = font.tables[tablename];
198210
if (tablename == 'name') {
@@ -231,6 +243,21 @@ <h1>Free Software</h1>
231243
element.innerHTML = '<dt>' + Object.keys(font.kerningPairs).length + ' Pairs</dt><dd>' + JSON.stringify(font.kerningPairs) + '</dd>';
232244
}
233245
}
246+
247+
if(font.tables.cpal) {
248+
let markup = ' ';
249+
const cpal = document.getElementById("cpal-table");
250+
const dt = cpal.querySelector('dt');
251+
if ( dt ) {
252+
const palettes = font.palettes.getAll('hexa');
253+
if ( palettes.length ) {
254+
cpal.innerHTML += palettes.map((palette, idx) =>
255+
`<dt><strong>↪ Palette ${idx}</strong></dt><dd><div class="color-swatches">`+
256+
palette.map(color => `<span class="color-swatch" style="background:${color}" title="${color}"></span>`).join('')+
257+
`</div></dd>`).join('');
258+
}
259+
}
260+
}
234261
}
235262

236263
function onFontLoaded(font) {
@@ -239,6 +266,8 @@ <h1>Free Software</h1>
239266
displayFontData(font);
240267
}
241268

269+
document.getElementById('update').addEventListener('click', () => displayFontData(window.font));
270+
242271
var tableHeaders = document.getElementById('font-data').getElementsByTagName('h3');
243272
for(var i = tableHeaders.length; i--; ) {
244273
tableHeaders[i].addEventListener('click', function(e) {

0 commit comments

Comments
 (0)