diff --git a/src/TTFFont.js b/src/TTFFont.js index 6aa0937a..ff5a877c 100644 --- a/src/TTFFont.js +++ b/src/TTFFont.js @@ -8,7 +8,8 @@ import LayoutEngine from './layout/LayoutEngine'; import TTFGlyph from './glyph/TTFGlyph'; import CFFGlyph from './glyph/CFFGlyph'; import SBIXGlyph from './glyph/SBIXGlyph'; -import COLRGlyph from './glyph/COLRGlyph'; +import { COLRGlyph, COLRv1Glyph } from './glyph/COLRGlyph'; +import { PAINT_OPERATIONS } from './glyph/PaintOperation'; import GlyphVariationProcessor from './glyph/GlyphVariationProcessor'; import TTFSubset from './subset/TTFSubset'; import CFFSubset from './subset/CFFSubset'; @@ -386,17 +387,34 @@ export default class TTFFont { _getBaseGlyph(glyph, characters = []) { if (!this._glyphs[glyph]) { - if (this.directory.tables.glyf) { - this._glyphs[glyph] = new TTFGlyph(glyph, characters, this); - - } else if (this.directory.tables['CFF '] || this.directory.tables.CFF2) { - this._glyphs[glyph] = new CFFGlyph(glyph, characters, this); - } + this._glyphs[glyph] = this._getBaseGlyphUncached(glyph, characters) } return this._glyphs[glyph] || null; } + _getBaseGlyphUncached(glyph, characters = []) { + if (this.directory.tables.glyf) { + return new TTFGlyph(glyph, characters, this); + } else if (this.directory.tables['CFF '] || this.directory.tables.CFF2) { + return new CFFGlyph(glyph, characters, this); + } + } + + _getColrGlyph(glyph, characters = []) { + if (this.COLR.version == 1 && this.COLR.baseGlyphList) { + for (let baseGlyph of this.COLR.baseGlyphList.baseGlyphPaintRecords) { + if (baseGlyph.gid == glyph) { + let colorGlyph = new COLRv1Glyph(glyph, characters, this) + colorGlyph.paint = (new PAINT_OPERATIONS[baseGlyph.paint.version])(baseGlyph.paint, this); + return colorGlyph + } + } + } + // Either v0 format, no BaseGlyphList, or not found -> treat as COLRv0 + return new COLRGlyph(glyph, characters, this); + } + /** * Returns a glyph object for the given glyph id. * You can pass the array of code points this glyph represents for @@ -412,8 +430,8 @@ export default class TTFFont { this._glyphs[glyph] = new SBIXGlyph(glyph, characters, this); } else if ((this.directory.tables.COLR) && (this.directory.tables.CPAL)) { - this._glyphs[glyph] = new COLRGlyph(glyph, characters, this); - + // Each glyph may be either COLRv0 (layers) or COLRv1 (paint tree) + this._glyphs[glyph] = this._getColrGlyph(glyph, characters); } else { this._getBaseGlyph(glyph, characters); } diff --git a/src/glyph/COLRGlyph.js b/src/glyph/COLRGlyph.js index 85d594b6..75631665 100644 --- a/src/glyph/COLRGlyph.js +++ b/src/glyph/COLRGlyph.js @@ -13,7 +13,7 @@ class COLRLayer { * Each glyph in this format contain a list of colored layers, each * of which is another vector glyph. */ -export default class COLRGlyph extends Glyph { +export class COLRGlyph extends Glyph { type = 'COLR'; _getBBox() { @@ -87,4 +87,51 @@ export default class COLRGlyph extends Glyph { return; } + _getContours() { + var base = this._font._getBaseGlyphUncached(this.id); + return base._getContours(); + } + +} + +/** + * Represents a color (e.g. emoji) glyph in Microsoft/Google's COLRv1 + * format. Each glyph in this format contains a directed acyclic graph + * of Paint structures. + */ +export class COLRv1Glyph extends COLRGlyph { + type = 'COLRv1'; + + _getBBox() { + // If we have a clip list item, use that + let colr = this._font.COLR; + if (colr.clipList) { + for (var clip of colr.clipList.clips) { + if (clip.startGlyphId <= this.id && this.id <= clip.endGlyphId) { + let box = clip.clipBox; + return new BBox( + box.xMin, + box.yMin, + box.xMax, + box.yMax + ); + } + } + } + return super._getBBox(); + } + render(ctx, size) { + // One scale only. + ctx.save(); + let scale = 1 / this._font.unitsPerEm * size; + ctx.scale(scale, scale); + + let paint = this.paint; + if (this._font.variationCoords) { + paint = paint.instantiate(this._font._variationProcessor); + } + + paint.render(ctx, size); + ctx.restore(); + } } diff --git a/src/glyph/PaintOperation.js b/src/glyph/PaintOperation.js new file mode 100644 index 00000000..8be3b057 --- /dev/null +++ b/src/glyph/PaintOperation.js @@ -0,0 +1,749 @@ +import * as fontkit from '../base'; +import { ColorLine, ColorStop } from "../tables/COLR"; +export var PAINT_OPERATIONS = [null]; + +// Variation deltas of values in a variable paint table come +// back from instantiator.getDelta as an integer, but sometimes +// they're actually intended to be interpreted as float values. +// This simple function just helps the code document that fact. +function deltaToFloat(f2dot14) { + return f2dot14 / (1 << 14); +} + +// Wrap the raw paint tree data into a JS object of the +// appropriate class +function makePaintOperation(paint, font) { + if (paint.version < 1 || paint.version >= PAINT_OPERATIONS.length) { + if (fontkit.logErrors) { + console.error(`Unknown paint table ${paint.version}`); + } + return; + } + let thisPaint = PAINT_OPERATIONS[paint.version]; + return new thisPaint(paint, font); +} + +class PaintOperation { + constructor(paint, font) { + this.paint = paint; + this.font = font; + this.cpal = font.CPAL; + this.layerList = font.COLR.layerList?.paint; + this.ivs = font.COLR.itemVariationStore; + this.next = null; + this.layers = []; + } + + render(_ctx, _size) { + throw new Error('Unimplemented abstract method'); + } + + // Convert a paint tree to a "static" paint tree (containing + // no PaintVar* paints) so that it can be rendered at a given + // location. This is done by walking the tree and calling + // `_instantiate` on all paints. + instantiate(processor) { + var instantiated = new (this.constructor)(this.paint, this.font); + instantiated = instantiated._instantiate(processor); + if (this.next) { + instantiated.next = this.next.instantiate(processor); + } + instantiated.layers = []; + for (let layer of this.layers) { + instantiated.layers.push(layer.instantiate(processor)); + } + return instantiated; + } + + // Convert a single variable paint to its static equivalent. + // In this case, static paint operations don't need any + // instantiating, so we just return them... + _instantiate(_processor) { + return this; + } +} + +class VariablePaintOperation extends PaintOperation { + // ...but variable paint operations require some operation-specific + // processing to turn them into a static equivalent. + _instantiate(_processor) { + throw new Error('Unimplemented abstract method'); + } + + _instantiateColorLine(varcolorline, instantiator) { + let stops = []; + for (var stop of varcolorline.colorStops) { + let [posDelta, alphaDelta] = this.getDeltas(instantiator, 2, stop); + stops.push({ + stopOffset: stop.stopOffset + posDelta / (1 << 14), + paletteIndex: stop.paletteIndex, + alpha: stop.alpha + alphaDelta / (1 << 14), + }); + } + return { + extend: varcolorline.extend, + numStops: varcolorline.numStops, + colorStops: stops + }; + } + + getDeltas(instantiator, count, thing) { + let res = []; + if (!thing) { thing = this.paint; } + let ix = thing.varIndexBase; + while (res.length < count) { + let {outerIndex, innerIndex} = this.font.COLR.varIndexMap.mapData[ix]; + try { + res.push(instantiator.getDelta(this.ivs, outerIndex, innerIndex)); + } catch { + res.push(0); + } + ix += 1; + } + return res; + } +} + +// PaintColrLayers +class PaintColrLayersOperation extends PaintOperation { + constructor(paint, font) { + super(paint, font); + for (let layer of this.layerList.slice( + this.paint.firstLayerIndex, + this.paint.firstLayerIndex + this.paint.numLayers + )) { + this.layers.push(makePaintOperation(layer, this.font)); + } + } + + render(ctx, size) { + for (let layer of this.layers) { + ctx.save(); + layer.render(ctx, size); + ctx.restore(); + } + } +} + +PAINT_OPERATIONS.push(PaintColrLayersOperation); + +/* + * Fill-related paints + */ +class PaintFillOperation extends PaintOperation { + floodFill(ctx, size) { + ctx.fillRect(0, 0, ctx.canvas.width * 2, ctx.canvas.height * 2); + } +} + +// PaintSolid +class PaintSolidOperation extends PaintFillOperation { + render(ctx, size) { + var color = this.cpal.colorRecords[this.paint.paletteIndex]; + ctx.fillStyle = `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.alpha / 255 * this.paint.alpha})`; + this.floodFill(ctx, size); + } +} +PAINT_OPERATIONS.push(PaintSolidOperation); + +// PaintVarSolid +class PaintVarSolidOperation extends VariablePaintOperation { + _instantiate(processor) { + let [deltaAlpha] = this.getDeltas(processor, 1); + let rv = new PaintSolidOperation( + { + version: 2, + paint: this.paint.paint, + paletteIndex: this.paint.paletteIndex, + alpha: this.paint.alpha + deltaToFloat(deltaAlpha), + }, this.font + ); + return rv; + } +} +PAINT_OPERATIONS.push(PaintVarSolidOperation); + +// PaintLinearGradient +class PaintGradientOperation extends PaintFillOperation { + _renderColorLine(gradient, colorline) { + if (!colorline || !colorline.colorStops) { + return; + } + for (let stop of colorline.colorStops) { + var color = this.cpal.colorRecords[stop.paletteIndex]; + var alpha = color.alpha / 255 * stop.alpha; + // If the stop offset > 1 or < 0 we should interpolate, + // not clamp. But we're going to clamp for now. + let stopOffset = stop.stopOffset > 1.0 ? 1.0 : (stop.stopOffset < 0.0 ? 0.0 : stop.stopOffset); + gradient.addColorStop(stopOffset, `rgba(${color.red}, ${color.green}, ${color.blue}, ${alpha})`); + } + } +} + + +class PaintLinearGradientOperation extends PaintGradientOperation { + render(ctx, size) { + const d1x = this.paint.x1 - this.paint.x0; + const d1y = this.paint.y1 - this.paint.y0; + const d2x = this.paint.x2 - this.paint.x0; + const d2y = this.paint.y2 - this.paint.y0; + const dotProd = d1x*d2x + d1y*d2y; + const rotLengthSquared = d2x*d2x + d2y*d2y; + const magnitude = dotProd / rotLengthSquared; + let finalX = this.paint.x1 - magnitude * d2x; + let finalY = this.paint.y1 - magnitude * d2y; + let gradient = ctx.createLinearGradient(this.paint.x0, this.paint.y0, finalX, finalY); + this._renderColorLine(gradient, this.paint.colorLine); + ctx.fillStyle = gradient; + this.floodFill(ctx, size); + } +} +PAINT_OPERATIONS.push(PaintLinearGradientOperation); + +// PaintVarLinearGradient +class PaintVarLinearGradientOperation extends VariablePaintOperation { + _instantiate(processor) { + let [deltaX0, deltaY0, deltaX1, deltaY1, deltaX2, deltaY2] = this.getDeltas(processor, 6); + let rv = new PaintLinearGradientOperation( + { + version: 4, + paint: this.paint.paint, + x0: this.paint.x0 + deltaX0, + y0: this.paint.y0 + deltaY0, + x1: this.paint.x1 + deltaX1, + y1: this.paint.y1 + deltaY1, + x2: this.paint.x2 + deltaX2, + y2: this.paint.y2 + deltaY2, + colorLine: this._instantiateColorLine(this.paint.colorLine, processor), + }, this.font + ); + return rv; + } +} + + +PAINT_OPERATIONS.push(PaintVarLinearGradientOperation); + +// PaintRadialGradient +class PaintRadialGradientOperation extends PaintGradientOperation { + render(ctx, size) { + let gradient = ctx.createRadialGradient( + this.paint.x0, this.paint.y0, this.paint.radius0, + this.paint.x1, this.paint.y1, this.paint.radius1 + ); + this._renderColorLine(gradient, this.paint.colorLine); + ctx.fillStyle = gradient; + this.floodFill(ctx, size); + } +} +PAINT_OPERATIONS.push(PaintRadialGradientOperation); + +// PaintVarRadialGradient +class PaintVarRadialGradientOperation extends VariablePaintOperation { + _instantiate(processor) { + let [deltaX0, deltaY0, deltaR0, deltaX1, deltaY1, deltaR1] = this.getDeltas(processor, 6); + let rv = new PaintRadialGradientOperation( + { + version: 6, + paint: this.paint.paint, + x0: this.paint.x0 + deltaX0, + y0: this.paint.y0 + deltaY0, + radius0: this.paint.radius0 + deltaR0, + x1: this.paint.x1 + deltaX1, + y1: this.paint.y1 + deltaY1, + radius1: this.paint.radius1 + deltaR1, + colorLine: this._instantiateColorLine(this.paint.colorLine, processor), + }, this.font + ); + return rv; + } +} +PAINT_OPERATIONS.push(PaintVarRadialGradientOperation); + +// PaintSweepGradient +class PaintSweepGradientOperation extends PaintGradientOperation { + render(ctx, _size) { + const angle = this.paint.startAngle * Math.PI; + // This is clearly wrong, but HTML Canvas doesn't support + // sweep gradients. + let gradient = ctx.createConicGradient( + angle, this.paint.centerX, this.paint.centerY + ); + this._renderColorLine(gradient, this.colorLine); + ctx.fillStyle = gradient; + } + +} +PAINT_OPERATIONS.push(PaintSweepGradientOperation); + +// PaintVarSweepGradient +class PaintVarSweepGradientOperation extends VariablePaintOperation { + _instantiate(processor) { + let [deltaX0, deltaY0, deltaStart, deltaEnd] = this.getDeltas(processor, 4); + let rv = new PaintSweepGradientOperation( + { + version: 8, + paint: this.paint.paint, + centerX: this.paint.centerX + deltaX0, + centerY: this.paint.centerY + deltaY0, + startAngle: this.paint.startAngle + deltaStart / (1<< 14), + endAngle: this.paint.endAngle + deltaEnd / (1<< 14), + colorLine: this._instantiateColorLine(this.paint.colorLine, processor), + }, this.font + ); + return rv; + } +} +PAINT_OPERATIONS.push(PaintVarSweepGradientOperation); + +/* + * Glyph painting + */ + +// PaintGlyph +class PaintGlyphOperation extends PaintOperation { + constructor(paint, font) { + super(paint, font); + this.next = makePaintOperation(this.paint.paint, this.font); + } + render(ctx, size) { + // Set fill, transform, etc. + ctx.beginPath(); + const glyph = this.font._getBaseGlyphUncached(this.paint.glyphID); + let path = glyph.path; + path.commands.pop(); + let fn = glyph.path.toFunction(); + fn(ctx); + ctx.clip(); + + this.next.render(ctx, size); + } +} +PAINT_OPERATIONS.push(PaintGlyphOperation); + +// PaintColrGlyph +class PaintColrGlyphOperation extends PaintOperation { + render(ctx, size) { + // We want a COLRGlyph or COLRv1Glyph here, not a base glyph + // We also want to undo the scaling operation, else it will get + // done twice. + ctx.save(); + let scale = 1 / this.font.unitsPerEm * size; + ctx.scale(1 / scale, 1 / scale); + const glyph = this.font.getGlyph(this.paint.glyphID); + glyph.render(ctx, size); + ctx.restore(); + } +} +PAINT_OPERATIONS.push(PaintColrGlyphOperation); + +/* + * Transformation-related paints + */ + +// PaintTransform +class PaintTransformOperation extends PaintOperation { + constructor(paint, font) { + super(paint, font); + this.next = makePaintOperation(this.paint.paint, this.font); + } + + get affine() { + return this.paint.transform; + } + + render(ctx, size) { + console.log(this); + if (this.affine) { + let { xx, yx, xy, yy, dx, dy } = this.affine; + ctx.transform(xx, yx, xy, yy, dx, dy); + } + this.next.render(ctx, size); + } +} +PAINT_OPERATIONS.push(PaintTransformOperation); + +// PaintVarTransform +class PaintVarTransformOperation extends VariablePaintOperation { + constructor(paint, font) { + super(paint, font); + this.next = makePaintOperation(this.paint.paint, this.font); + } + newAffine(processor) { + let deltas = this.getDeltas(processor, 6, this.paint.transform); + let { xx, yx, xy, yy, dx, dy } = this.paint.transform; + return { + xx: xx + deltas[0] / (1<<16), + yx: yx + deltas[1] / (1<<16), + xy: xy + deltas[2] / (1<<16), + yy: yy + deltas[3] / (1<<16), + dx: dx + deltas[4] / (1<<16), + dy: dy + deltas[5] / (1<<16), + }; + } + + _instantiate(processor) { + return new PaintTransformOperation( + { + version: 12, + paint: this.paint.paint, + transform: this.newAffine(processor) + }, this.font + ); + } +} +PAINT_OPERATIONS.push(PaintVarTransformOperation); + +// PaintTranslate +class PaintTranslateOperation extends PaintTransformOperation { + get affine() { + return { + xx: 1, + yx: 0, + xy: 0, + yy: 1, + dx: this.paint.dx, + dy: this.paint.dy, + }; + } + +} +PAINT_OPERATIONS.push(PaintTranslateOperation); + +// PaintVarTranslate +class PaintVarTranslateOperation extends PaintVarTransformOperation { + _instantiate(processor) { + let [deltaX, deltaY] = this.getDeltas(processor, 2); + let rv = new PaintTranslateOperation( + { + version: 14, + paint: this.paint.paint, + dx: this.paint.dx + deltaX, + dy: this.paint.dy + deltaY, + }, this.font + ); + return rv; + } +} +PAINT_OPERATIONS.push(PaintVarTranslateOperation); + +// PaintScale +class PaintScaleOperation extends PaintTransformOperation { + get affine() { + return { + xx: this.paint.scaleX, + yx: 0, + xy: 0, + yy: this.paint.scaleY, + dx: 0, + dy: 0, + }; + } + render(ctx, size) { + ctx.scale(this.paint.scaleX, this.paint.scaleY); + this.next.render(ctx, size); + } +} +PAINT_OPERATIONS.push(PaintScaleOperation); + +// PaintVarScale +class PaintVarScaleOperation extends PaintVarTransformOperation { + _instantiate(processor) { + let [scaleX, scaleY] = this.getDeltas(processor, 2); + let rv = new PaintScaleOperation( + { + version: 16, + paint: this.paint.paint, + scaleX: this.paint.scaleX + scaleX / (1<<14), + scaleY: this.paint.scaleY + scaleY / (1<<14), + }, this.font + ); + // console.log(rv); + return rv; + } +} +PAINT_OPERATIONS.push(PaintVarScaleOperation); + +// PaintScaleAroundCenter +class PaintScaleAroundCenterOperation extends PaintTransformOperation { + render(ctx, size) { + ctx.translate(this.paint.centerX, this.paint.centerY); + ctx.scale(this.paint.scaleX, this.paint.scaleY); + ctx.translate(-this.paint.centerX, -this.paint.centerY); + this.next.render(ctx, size); + } +} +PAINT_OPERATIONS.push(PaintScaleAroundCenterOperation); + +// PaintVarScaleAroundCenter +class PaintVarScaleAroundCenterOperation extends PaintVarTransformOperation { + _instantiate(processor) { + let [scaleX, scaleY, centerX, centerY] = this.getDeltas(processor, 4); + let rv = new PaintScaleAroundCenterOperation( + { + version: 18, + paint: this.paint.paint, + scaleX: this.paint.scaleX + scaleX / (1 << 14), + scaleY: this.paint.scaleY + scaleY / (1 << 14), + centerX: this.paint.centerX + centerX, + centerY: this.paint.centerY + centerY, + }, this.font + ); + return rv; + } +} +PAINT_OPERATIONS.push(PaintVarScaleAroundCenterOperation); + +// PaintScale +class PaintScaleUniformOperation extends PaintTransformOperation { + get affine() { + return { + xx: this.paint.scale, + yx: 0, + xy: 0, + yy: this.paint.scale, + dx: 0, + dy: 0, + }; + } + render(ctx, size) { + ctx.scale(this.paint.scale, this.paint.scale); + this.next.render(ctx, size); + } +} +PAINT_OPERATIONS.push(PaintScaleUniformOperation); + +// PaintVarScale +class PaintVarScaleUniformOperation extends PaintVarTransformOperation { + _instantiate(processor) { + let [deltaScale] = this.getDeltas(processor, 1); + let rv = new PaintScaleUniformOperation( + { + version: 20, + paint: this.paint.paint, + scale: this.paint.scale + deltaScale / (1<<14) + }, this.font + ); + return rv; + } +} +PAINT_OPERATIONS.push(PaintVarScaleUniformOperation); + +// PaintScaleUniformAroundCenter +class PaintScaleUniformAroundCenterOperation extends PaintTransformOperation { + render(ctx, size) { + ctx.translate(this.paint.centerX, this.paint.centerY); + ctx.scale(this.paint.scale, this.paint.scale); + ctx.translate(-this.paint.centerX, -this.paint.centerY); + this.next.render(ctx, size); + } +} +PAINT_OPERATIONS.push(PaintScaleUniformAroundCenterOperation); + +// PaintVarScaleUniformAroundCenter +class PaintVarScaleUniformAroundCenterOperation extends PaintVarTransformOperation { + _instantiate(processor) { + let [deltaScale, centerX, centerY] = this.getDeltas(processor, 3); + let rv = new PaintScaleUniformAroundCenterOperation( + { + version: 22, + paint: this.paint.paint, + scale: this.paint.scale + deltaScale / (1<< 14), + centerX: this.paint.centerX + centerX, + centerY: this.paint.centerY + centerY, + }, this.font + ); + // console.log(rv) + return rv; + } +} +PAINT_OPERATIONS.push(PaintVarScaleUniformAroundCenterOperation); + +// PaintRotate +class PaintRotateOperation extends PaintTransformOperation { + render(ctx, size) { + ctx.rotate(this.paint.angle * Math.PI); + this.next.render(ctx, size); + } +} +PAINT_OPERATIONS.push(PaintRotateOperation); + +// PaintVarRotate +class PaintVarRotateOperation extends PaintVarTransformOperation { + _instantiate(processor) { + let [delta] = this.getDeltas(processor, 1); + let rv = new PaintRotateOperation( + { + version: 24, + paint: this.paint.paint, + angle: this.paint.angle + delta / (1<< 14) + }, this.font + ); + return rv; + } +} +PAINT_OPERATIONS.push(PaintVarRotateOperation); + +// PaintRotateAroundCenter +class PaintRotateAroundCenterOperation extends PaintTransformOperation { + render(ctx, size) { + ctx.translate(this.paint.centerX, this.paint.centerY); + ctx.rotate(this.paint.angle * Math.PI); + ctx.translate(-this.paint.centerX, -this.paint.centerY); + this.next.render(ctx, size); + }} +PAINT_OPERATIONS.push(PaintRotateAroundCenterOperation); + +// PaintVarRotateAroundCenter +class PaintVarRotateAroundCenterOperation extends PaintVarTransformOperation { + _instantiate(processor) { + let [deltaAngle, deltaX, deltaY] = this.getDeltas(processor, 3); + let rv = new PaintRotateAroundCenterOperation( + { + version: 26, + paint: this.paint.paint, + angle: this.paint.angle + deltaAngle / (1<< 14), + centerX: this.paint.centerX + deltaX, + centerY: this.paint.centerY + deltaY + }, this.font + ); + return rv; + } +} +PAINT_OPERATIONS.push(PaintVarRotateAroundCenterOperation); + +// PaintRotate +class PaintSkewOperation extends PaintTransformOperation { + get affine() { + return { + xx: 1, + yx: Math.tan(this.paint.ySkewAngle * Math.PI), + xy: -Math.tan(this.paint.xSkewAngle * Math.PI), + yy: 1, + dx: 0, + dy: 0, + }; + } +} +PAINT_OPERATIONS.push(PaintSkewOperation); + +// PaintVarSkew +class PaintVarSkewOperation extends PaintVarTransformOperation { + _instantiate(processor) { + let [xDelta, yDelta] = this.getDeltas(processor, 2); + return new PaintSkewOperation( + { + version: 28, + paint: this.paint.paint, + xSkewAngle: this.paint.xSkewAngle + xDelta / (1<< 14), + ySkewAngle: this.paint.ySkewAngle + yDelta / (1<< 14) + }, this.font + ); + } +} +PAINT_OPERATIONS.push(PaintVarSkewOperation); + +// PaintSkewAroundCenter +class PaintSkewAroundCenterOperation extends PaintTransformOperation { + render(ctx, size) { + ctx.translate(this.paint.centerX, this.paint.centerY); + ctx.transform(1.0, Math.tan(this.paint.ySkewAngle * Math.PI), -Math.tan(this.paint.xSkewAngle * Math.PI), 1.0, 0.0, 0.0); + ctx.translate(-this.paint.centerX, -this.paint.centerY); + this.next.render(ctx, size); + } +} + +PAINT_OPERATIONS.push(PaintSkewAroundCenterOperation); + +// PaintVarSkewAroundCenter +class PaintVarSkewAroundCenterOperation extends PaintVarTransformOperation { + _instantiate(processor) { + let [xDelta, yDelta, cxDelta, cyDelta] = this.getDeltas(processor, 4); + return new PaintSkewAroundCenterOperation( + { + version: 30, + paint: this.paint.paint, + xSkewAngle: this.paint.xSkewAngle + xDelta / (1<< 14), + ySkewAngle: this.paint.ySkewAngle + yDelta / (1<< 14), + centerX: this.paint.centerX + cxDelta, + centerY: this.paint.centerY + cyDelta, + }, this.font + ); + } +} +PAINT_OPERATIONS.push(PaintVarSkewAroundCenterOperation); + +/* And finally... */ +// PaintComposite +let CANVAS_COMPOSITING_MODES = [ + 'source-over', + 'source-over', + 'source-over', + 'source-over', + 'dest-over', + 'source-in', + 'dest-in', + 'source-out', + 'dest-out', + 'source-atop', + 'dest-atop', + 'xor', + 'lighter', + 'screen', + 'overlay', + 'lighten', + 'darken', + 'color-dodge', + 'color-burn', + 'hard-light', + 'soft-light', + 'difference', + 'exclusion', + 'multiply', + 'hue', + 'saturation', + 'color', + 'luminosity' +]; + +class PaintComposite extends PaintOperation { + constructor(paint, font) { + super(paint, font); + this.layers = [ + makePaintOperation(this.paint.sourcePaint, this.font), + makePaintOperation(this.paint.backdropPaint, this.font) + ]; + } + + get source() { + return this.layers[0]; + } + get backdrop() { + return this.layers[1]; + } + + render(ctx, size) { + // console.log(this.paint.compositeMode); + if (this.paint.compositeMode == 1 || this.paint.compositeMode > 2) { + ctx.save(); + this.backdrop.render(ctx,size); + ctx.restore(); + } + if (this.paint.compositeMode > 1) { + ctx.save(); + // There is an issue here when composite paints are nested + // inside other composite paints, which I am not clever enough + // to solve. + ctx.globalCompositeOperation = CANVAS_COMPOSITING_MODES[this.paint.compositeMode]; + this.source.render(ctx, size); + + ctx.restore(); + } + } +} + +PAINT_OPERATIONS.push(PaintComposite); + +if (PAINT_OPERATIONS.length != 33) { + throw 'Not all paints registered'; +} diff --git a/src/tables/COLR.js b/src/tables/COLR.js index 6a4b1697..6f2b618f 100644 --- a/src/tables/COLR.js +++ b/src/tables/COLR.js @@ -1,25 +1,399 @@ import * as r from 'restructure'; +import { ItemVariationStore, DeltaSetIndexMap } from '../tables/variations'; + +let F2DOT14 = new r.Fixed(16, 'BE', 14); +let Fixed = new r.Fixed(32, 'BE', 16); +let FWORD = r.int16; +let UFWORD = r.uint16; + +// COLRv0 let LayerRecord = new r.Struct({ - gid: r.uint16, // Glyph ID of layer glyph (must be in z-order from bottom to top). - paletteIndex: r.uint16 // Index value to use in the appropriate palette. This value must -}); // be less than numPaletteEntries in the CPAL table, except for - // the special case noted below. Each palette entry is 16 bits. - // A palette index of 0xFFFF is a special case indicating that - // the text foreground color should be used. + gid: r.uint16, // Glyph ID of layer glyph (must be in z-order from bottom to top). + paletteIndex: r.uint16 // Index value to use in the appropriate palette. +}); let BaseGlyphRecord = new r.Struct({ - gid: r.uint16, // Glyph ID of reference glyph. This glyph is for reference only - // and is not rendered for color. + gid: r.uint16, // Glyph ID of reference glyph. This glyph is for reference only and is not rendered for color. firstLayerIndex: r.uint16, // Index (from beginning of the Layer Records) to the layer record. - // There will be numLayers consecutive entries for this base glyph. - numLayers: r.uint16 + numLayers: r.uint16 // There will be numLayers consecutive entries for this base glyph. +}); + +// COLRv1 + +// Affine transforms + +let Affine2x3 = new r.Struct({ + xx: Fixed, // x-component of transformed x-basis vector. + yx: Fixed, // y-component of transformed x-basis vector. + xy: Fixed, // x-component of transformed y-basis vector. + yy: Fixed, // y-component of transformed y-basis vector. + dx: Fixed, // Translation in x direction. + dy: Fixed // Translation in y direction. +}); + +let VarAffine2x3 = new r.Struct({ + xx: Fixed, // x-component of transformed x-basis vector. + yx: Fixed, // y-component of transformed x-basis vector. + xy: Fixed, // x-component of transformed y-basis vector. + yy: Fixed, // y-component of transformed y-basis vector. + dx: Fixed, // Translation in x direction. + dy: Fixed, // Translation in y direction. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. +}); + +// Color lines for gradients +let ColorStop = new r.Struct({ + stopOffset: F2DOT14, // Position on a color line. + paletteIndex: r.uint16, // Index for a CPAL palette entry. + alpha: F2DOT14 // Alpha value. +}); + +let VarColorStop = new r.Struct({ + stopOffset: F2DOT14, // Position on a color line. + paletteIndex: r.uint16, // Index for a CPAL palette entry. + alpha: F2DOT14, // Alpha value. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. +}); + +let ColorLine = new r.Struct({ + extend: r.uint8, // An Extend enum value + numStops: r.uint16, // Number of ColorStop records. + colorStops: new r.Array(ColorStop, 'numStops') +}); +let VarColorLine = new r.Struct({ + extend: r.uint8, // An Extend enum value + numStops: r.uint16, // Number of ColorStop records. + colorStops: new r.Array(VarColorStop, 'numStops') +}); + +// Porter-Duff Composition modes, used in PaintComposite +export let CompositionMode = { + CLEAR: 0, + SRC: 1, + DEST: 2, + SRC_OVER: 3, + DEST_OVER: 4, + SRC_IN: 5, + DEST_IN: 6, + SRC_OUT: 7, + DEST_OUT: 8, + SRC_ATOP: 9, + DEST_ATOP: 10, + XOR: 11, + PLUS: 12, + SCREEN: 13, + OVERLAY: 14, + DARKEN: 15, + LIGHTEN: 16, + COLOR_DODGE: 17, + COLOR_BURN: 18, + HARD_LIGHT: 19, + SOFT_LIGHT: 20, + DIFFERENCE: 21, + EXCLUSION: 22, + MULTIPLY: 23, + HSL_HUE: 24, + HSL_SATURATION: 25, + HSL_COLOR: 26, + HSL_LUMINOSITY: 27 +}; + +// The Paint table is format-switching rather than version-switching, but +// we use the VersionedStruct functionality to achieve what we want. +var Paint = new r.VersionedStruct(r.uint8, {}); +// Declare first, then fill the version, to allow for self-use. +Paint.versions = { + header: {}, + // PaintColrLayers + 1: { + numLayers: r.uint8, // Number of offsets to paint tables to read from LayerList. + firstLayerIndex: r.uint32 // Index (base 0) into the LayerList. + }, + // PaintSolid + 2: { + paletteIndex: r.uint16, // Index for a CPAL palette entry. + alpha: F2DOT14 // Alpha value. + }, + // PaintVarSolid + 3: { + paletteIndex: r.uint16, // Index for a CPAL palette entry. + alpha: F2DOT14, // Alpha value. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. + }, + // PaintLinearGradient + 4: { + colorLine: new r.Pointer(r.uint24, ColorLine), // Offset to ColorLine table. + x0: FWORD, // Start point x coordinate. + y0: FWORD, // Start point y coordinate. + x1: FWORD, // End point x coordinate. + y1: FWORD, // End point y coordinate. + x2: FWORD, // Rotation point x coordinate. + y2: FWORD, // Rotation point y coordinate. + }, + // PaintVarLinearGradient + 5: { + colorLine: new r.Pointer(r.uint24, VarColorLine), // Offset to ColorLine table. + x0: FWORD, // Start point x coordinate. + y0: FWORD, // Start point y coordinate. + x1: FWORD, // End point x coordinate. + y1: FWORD, // End point y coordinate. + x2: FWORD, // Rotation point x coordinate. + y2: FWORD, // Rotation point y coordinate. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. + }, + // PaintRadialGradient + 6: { + colorLine: new r.Pointer(r.uint24, ColorLine), // Offset to ColorLine table. + x0: FWORD, // Start circle center x coordinate. + y0: FWORD, // Start circle center y coordinate. + radius0: UFWORD, // Start circle radius. + x1: FWORD, // End circle center x coordinate. + y1: FWORD, // End circle center y coordinate. + radius1: UFWORD // End circle radius. + }, + // PaintVarRadialGradient + 7: { + colorLine: new r.Pointer(r.uint24, VarColorLine), // Offset to ColorLine table. + x0: FWORD, // Start circle center x coordinate. + y0: FWORD, // Start circle center y coordinate. + radius0: UFWORD, // Start circle radius. + x1: FWORD, // End circle center x coordinate. + y1: FWORD, // End circle center y coordinate. + radius1: UFWORD, // End circle radius. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. + }, + // PaintSweepGradient + 8: { + colorLine: new r.Pointer(r.uint24, ColorLine), // Offset to ColorLine table. + centerX: FWORD, // Center x coordinate. + centerY: FWORD, // Center y coordinate. + startAngle: F2DOT14, // Start of the angular range of the gradient, 180° in counter-clockwise degrees per 1.0 of value. + endAngle: F2DOT14 // End of the angular range of the gradient, 180° in counter-clockwise degrees per 1.0 of value. + }, + // PaintVarSweepGradient + 9: { + colorLine: new r.Pointer(r.uint24, VarColorLine), // Offset to ColorLine table. + centerX: FWORD, // Center x coordinate. + centerY: FWORD, // Center y coordinate. + startAngle: F2DOT14, // Start of the angular range of the gradient, 180° in counter-clockwise degrees per 1.0 of value. + endAngle: F2DOT14, // End of the angular range of the gradient, 180° in counter-clockwise degrees per 1.0 of value. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. + }, + // PaintGlyph + 10: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + glyphID: r.uint16 // Glyph ID for the source outline. + }, + // PaintColrGlyph + 11: { + glyphID: r.uint16 // Glyph ID for a BaseGlyphList base glyph. + }, + // PaintTransform + 12: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + transform: new r.Pointer(r.uint24, Affine2x3) // Transformation. + }, + // PaintVarTransform + 13: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + transform: new r.Pointer(r.uint24, VarAffine2x3) // Variable transformation. + }, + // PaintTranslate + 14: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + dx: FWORD, // Translation in x direction. + dy: FWORD // Translation in y direction. + }, + // PaintVarTranslate + 15: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + dx: FWORD, // Translation in x direction. + dy: FWORD, // Translation in y direction. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. + }, + // PaintScale + 16: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + scaleX: F2DOT14, // Scale factor in x direction. + scaleY: F2DOT14 // Scale factor in y direction. + }, + // PaintVarScale + 17: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + scaleX: F2DOT14, // Scale factor in x direction. + scaleY: F2DOT14, // Scale factor in y direction. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. + }, + // PaintScaleAroundCenter + 18: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + scaleX: F2DOT14, // Scale factor in x direction. + scaleY: F2DOT14, // Scale factor in y direction. + centerX: FWORD, // x coordinate for the center of scaling. + centerY: FWORD // y coordinate for the center of scaling. + }, + // PaintVarScaleAroundCenter + 19: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + scaleX: F2DOT14, // Scale factor in x direction. + scaleY: F2DOT14, // Scale factor in y direction. + centerX: FWORD, // x coordinate for the center of scaling. + centerY: FWORD, // y coordinate for the center of scaling. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. + }, + // PaintScaleUniform + 20: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + scale: F2DOT14 // Scale factor in x and y directions. + }, + // PaintVarScaleUniform + 21: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + scale: F2DOT14, // Scale factor in x and y directions. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. + }, + // PaintScaleUniformAroundCenter + 22: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + scale: F2DOT14, // Scale factor in x and y directions. + centerX: FWORD, // x coordinate for the center of scaling. + centerY: FWORD // y coordinate for the center of scaling. + }, + // PaintVarScaleUniformAroundCenter + 23: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + scale: F2DOT14, // Scale factor in x and y directions. + centerX: FWORD, // x coordinate for the center of scaling. + centerY: FWORD, // y coordinate for the center of scaling. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. + }, + // PaintRotate + 24: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + angle: F2DOT14 // Rotation angle, 180° in counter-clockwise degrees per 1.0 of value + }, + // PaintVarRotate + 25: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + angle: F2DOT14, // Rotation angle, 180° in counter-clockwise degrees per 1.0 of value + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. + }, + // PaintRotateAroundCenter + 26: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + angle: F2DOT14, // Rotation angle, 180° in counter-clockwise degrees per 1.0 of value + centerX: FWORD, // x coordinate for the center of scaling. + centerY: FWORD // y coordinate for the center of scaling. + }, + // PaintVarRotateAroundCenter + 27: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + angle: F2DOT14, // Rotation angle, 180° in counter-clockwise degrees per 1.0 of value + centerX: FWORD, // x coordinate for the center of scaling. + centerY: FWORD, // y coordinate for the center of scaling. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. + }, + // PaintSkew + 28: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + xSkewAngle: F2DOT14, // Angle of skew in the direction of the x-axis, 180° in counter-clockwise degrees per 1.0 of value. + ySkewAngle: F2DOT14 // Angle of skew in the direction of the y-axis, 180° in counter-clockwise degrees per 1.0 of value. + }, + // PaintVarSkew + 29: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + xSkewAngle: F2DOT14, // Angle of skew in the direction of the x-axis, 180° in counter-clockwise degrees per 1.0 of value. + ySkewAngle: F2DOT14, // Angle of skew in the direction of the y-axis, 180° in counter-clockwise degrees per 1.0 of value. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. + }, + // PaintSkewAroundCenter + 30: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + xSkewAngle: F2DOT14, // Angle of skew in the direction of the x-axis, 180° in counter-clockwise degrees per 1.0 of value. + ySkewAngle: F2DOT14, // Angle of skew in the direction of the y-axis, 180° in counter-clockwise degrees per 1.0 of value. + centerX: FWORD, // x coordinate for the center of scaling. + centerY: FWORD // y coordinate for the center of scaling. + }, + // PaintVarSkewAroundCenter + 31: { + paint: new r.Pointer(r.uint24, Paint), // Paint table. + xSkewAngle: F2DOT14, // Angle of skew in the direction of the x-axis, 180° in counter-clockwise degrees per 1.0 of value. + ySkewAngle: F2DOT14, // Angle of skew in the direction of the y-axis, 180° in counter-clockwise degrees per 1.0 of value. + centerX: FWORD, // x coordinate for the center of scaling. + centerY: FWORD, // y coordinate for the center of scaling. + varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. + }, + // PaintComposite + 32: { + sourcePaint: new r.Pointer(r.uint24, Paint), // Source paint table. + compositeMode: r.uint8, // A CompositeMode enumeration value. + backdropPaint: new r.Pointer(r.uint24, Paint), // Backdrop paint table. + }, +}; + +var LayerList = new r.Struct({ + numLayers: r.uint32, + paint: new r.Array(new r.Pointer(r.uint32, Paint), 'numLayers') }); -export default new r.Struct({ - version: r.uint16, - numBaseGlyphRecords: r.uint16, - baseGlyphRecord: new r.Pointer(r.uint32, new r.Array(BaseGlyphRecord, 'numBaseGlyphRecords')), - layerRecords: new r.Pointer(r.uint32, new r.Array(LayerRecord, 'numLayerRecords'), { lazy: true }), - numLayerRecords: r.uint16 +// "A ClipList table is used to provide precomputed clip boxes for color glyphs." + +var ClipBox = new r.VersionedStruct(r.uint8, { + header: { + xMin: r.int16, + yMin: r.int16, + xMax: r.int16, + yMax: r.int16, + }, + 1: {}, + 2: { + varIndexBase: r.uint32 + } +}); + +var Clip = new r.Struct({ + startGlyphId: r.uint16, + endGlyphId: r.uint16, + clipBox: new r.Pointer(r.uint24, ClipBox, { type: 'parent' }) +}); + +var ClipList = new r.Struct({ + format: r.uint8, + numClips: r.uint32, + clips: new r.Array(Clip, 'numClips') +}); + +// "The BaseGlyphList table is, conceptually, similar to the baseGlyphRecords +// array in COLR version 0, providing records that map a base glyph to a +// color glyph definition. The color glyph definitions that each refer to are significantly +// different, however." + +let BaseGlyphPaintRecord = new r.Struct({ + gid: r.uint16, // Glyph ID of the base glyph. + paint: new r.Pointer(r.uint32, Paint, { type: 'parent' }) // Offset to a Paint table. +}); + + +let BaseGlyphList = new r.Struct({ + numBaseGlyphPaintRecords: r.uint32, + baseGlyphPaintRecords: new r.Array(BaseGlyphPaintRecord, 'numBaseGlyphPaintRecords') +}); + + +export default new r.VersionedStruct(r.uint16, { + header: { + numBaseGlyphRecords: r.uint16, + baseGlyphRecord: new r.Pointer(r.uint32, new r.Array(BaseGlyphRecord, 'numBaseGlyphRecords')), + layerRecords: new r.Pointer(r.uint32, new r.Array(LayerRecord, 'numLayerRecords'), { lazy: true }), + numLayerRecords: r.uint16 + }, + 0: {}, + 1: { + baseGlyphList: new r.Pointer(r.uint32, BaseGlyphList), + layerList: new r.Pointer(r.uint32, LayerList), + clipList: new r.Pointer(r.uint32, ClipList), + varIndexMap: new r.Pointer(r.uint32, DeltaSetIndexMap), + itemVariationStore: new r.Pointer(r.uint32, ItemVariationStore), + } }); diff --git a/src/tables/HVAR.js b/src/tables/HVAR.js index 0b45e958..840732e3 100644 --- a/src/tables/HVAR.js +++ b/src/tables/HVAR.js @@ -1,38 +1,6 @@ import * as r from 'restructure'; import { resolveLength } from 'restructure'; -import { ItemVariationStore } from './variations'; - -// TODO: add this to restructure -class VariableSizeNumber { - constructor(size) { - this._size = size; - } - - decode(stream, parent) { - switch (this.size(0, parent)) { - case 1: return stream.readUInt8(); - case 2: return stream.readUInt16BE(); - case 3: return stream.readUInt24BE(); - case 4: return stream.readUInt32BE(); - } - } - - size(val, parent) { - return resolveLength(this._size, null, parent); - } -} - -let MapDataEntry = new r.Struct({ - entry: new VariableSizeNumber(t => ((t.parent.entryFormat & 0x0030) >> 4) + 1), - outerIndex: t => t.entry >> ((t.parent.entryFormat & 0x000F) + 1), - innerIndex: t => t.entry & ((1 << ((t.parent.entryFormat & 0x000F) + 1)) - 1) -}); - -let DeltaSetIndexMap = new r.Struct({ - entryFormat: r.uint16, - mapCount: r.uint16, - mapData: new r.Array(MapDataEntry, 'mapCount') -}); +import { ItemVariationStore, DeltaSetIndexMap } from './variations'; export default new r.Struct({ majorVersion: r.uint16, diff --git a/src/tables/variations.js b/src/tables/variations.js index 0aae61c3..c9997265 100644 --- a/src/tables/variations.js +++ b/src/tables/variations.js @@ -18,20 +18,65 @@ let VariationRegionList = new r.Struct({ variationRegions: new r.Array(new r.Array(RegionAxisCoordinates, 'axisCount'), 'regionCount') }); -let DeltaSet = new r.Struct({ - shortDeltas: new r.Array(r.int16, t => t.parent.shortDeltaCount), - regionDeltas: new r.Array(r.int8, t => t.parent.regionIndexCount - t.parent.shortDeltaCount), +let shortDeltaSet = new r.Struct({ + shortDeltas: new r.Array(r.int16, t => t.parent.shortDeltaCount & 0x7FFF), + regionDeltas: new r.Array(r.int8, t => t.parent.regionIndexCount - (t.parent.shortDeltaCount & 0x7FFF)), deltas: t => t.shortDeltas.concat(t.regionDeltas) }); +let longDeltaSet = new r.Struct({ + shortDeltas: new r.Array(r.int32, t => t.parent.shortDeltaCount & 0x7FFF), + regionDeltas: new r.Array(r.int16, t => t.parent.regionIndexCount - (t.parent.shortDeltaCount & 0x7FFF)), + deltas: t => t.shortDeltas.concat(t.regionDeltas) +}); + +var DeltaSets = new r.Struct({}) + +DeltaSets.decode = function(stream, parent) { + var decoder = new r.Array( + (parent.shortDeltaCount & 0x8000) ? longDeltaSet : shortDeltaSet, + parent.itemCount + ); + return decoder.decode(stream, parent); +}; + +DeltaSets.encode = function(stream, array, parent) { + for (var deltaset of array) { + // Split deltas into short and long, if this hasn't been done already + let shortDeltaCount = parent.val.shortDeltaCount & 0x7FFF; + deltaset.shortDeltas = deltaset.deltas.slice(0, shortDeltaCount); + deltaset.regionDeltas = deltaset.deltas.slice(shortDeltaCount); + if (parent.val.shortDeltaCount & 0x8000) { + longDeltaSet.encode(stream, deltaset, parent) + } else { + shortDeltaSet.encode(stream, deltaset, parent) + } + } +} + let ItemVariationData = new r.Struct({ itemCount: r.uint16, shortDeltaCount: r.uint16, regionIndexCount: r.uint16, regionIndexes: new r.Array(r.uint16, 'regionIndexCount'), - deltaSets: new r.Array(DeltaSet, 'itemCount') + deltaSets: DeltaSets }); +ItemVariationData.size = function(array, ctx) { + let headersize = 6 + 2 * array.regionIndexCount; + let shortDeltaCount = array.shortDeltaCount; + let deltasize = 0; + for (var deltaset of array.deltaSets) { + var shortDeltas = deltaset.deltas.slice(0, shortDeltaCount & 0x7FFF); + var regionDeltas = deltaset.deltas.slice(shortDeltaCount & 0x7FFF); + deltasize += shortDeltas.length * 2 + regionDeltas.length; + } + if (shortDeltaCount & 0x8000) { + deltasize *= 2; + } + return headersize + deltasize; +} + export let ItemVariationStore = new r.Struct({ format: r.uint16, variationRegionList: new r.Pointer(r.uint32, VariationRegionList), @@ -39,6 +84,100 @@ export let ItemVariationStore = new r.Struct({ itemVariationData: new r.Array(new r.Pointer(r.uint32, ItemVariationData), 'variationDataCount') }); +/*********************** + * Delta Set Index Map * + ***********************/ + +// TODO: add this to restructure +class VariableSizeNumber { + constructor(size) { + this._size = size; + } + + decode(stream, parent) { + switch (this.size(0, parent)) { + case 1: return stream.readUInt8(); + case 2: return stream.readUInt16BE(); + case 3: return stream.readUInt24BE(); + case 4: return stream.readUInt32BE(); + } + } + + size(val, parent) { + return r.resolveLength(this._size, null, parent); + } +} + +let MapDataEntry = new r.Struct({ + entry: new VariableSizeNumber(t => ((t.parent.entryFormat & 0x0030) >> 4) + 1), + outerIndex: t => t.entry >> ((t.parent.entryFormat & 0x000F) + 1), + innerIndex: t => t.entry & ((1 << ((t.parent.entryFormat & 0x000F) + 1)) - 1) +}); + +MapDataEntry.encode = function (stream, val, parent) { + let fmt = (parent.val.entryFormat & 0x0030) + let innerBits = 1 + (fmt & 0x000F); + let innerMask = (1 << innerBits) - 1; + let outerShift = 16 - innerBits; + let entrySize = 1 + ((fmt & 0x0030) >> 4); + let packed = (((val.entry & 0xFFFF0000) >> outerShift) | (val.entry & innerMask)) + switch(entrySize) { + case 1: return stream.writeUInt8(packed); + case 2: return stream.writeUInt16BE(packed); + case 3: return stream.writeUInt24BE(packed); + case 4: return stream.writeUInt32BE(packed); + } +} + +export let DeltaSetIndexMap = new r.VersionedStruct(r.uint8, { + 0: { + entryFormat: r.uint8, + mapCount: r.uint16, + mapData: new r.Array(MapDataEntry, 'mapCount') + }, + 1: { + entryFormat: r.uint8, + mapCount: r.uint32, + mapData: new r.Array(MapDataEntry, 'mapCount') + } +}); + +DeltaSetIndexMap.preEncode = function (val, stream) { + // Compute correct version and entry format + let ored = 0; + for (var idx of val.mapData) { + ored |= idx.entry + } + let inner = ored & 0xFFFF + let innerBits = 0 + while (inner) { + innerBits += 1 + inner >>= 1 + } + innerBits = Math.max(innerBits, 1) + console.assert(innerBits <= 16) + + ored = (ored >> (16 - innerBits)) | (ored & ((1 << innerBits) - 1)) + let entrySize = 1; + if (ored <= 0x000000FF) { + entrySize = 1 + } else if (ored <= 0x0000FFFF) { + entrySize = 2 + } else if (ored <= 0x00FFFFFF) { + entrySize = 3 + } else { + entrySize = 4 + } + + val.entryFormat = ((entrySize - 1) << 4) | (innerBits - 1) + val.mapCount = val.mapData.length + if (val.mapCount > 0xFFFF) { + val.version = 1 + } else { + val.version = 0 + } +} + /********************** * Feature Variations * **********************/ diff --git a/test/data/COLRv1/COLRv1-Test.ttf b/test/data/COLRv1/COLRv1-Test.ttf new file mode 100644 index 00000000..e58043e8 Binary files /dev/null and b/test/data/COLRv1/COLRv1-Test.ttf differ diff --git a/test/data/COLRv1/noto_handwriting-glyf_colr_1.ttf b/test/data/COLRv1/noto_handwriting-glyf_colr_1.ttf new file mode 100644 index 00000000..513ea0ae Binary files /dev/null and b/test/data/COLRv1/noto_handwriting-glyf_colr_1.ttf differ diff --git a/test/glyphs.js b/test/glyphs.js index 498004c1..902e4ce6 100644 --- a/test/glyphs.js +++ b/test/glyphs.js @@ -219,10 +219,10 @@ describe('glyphs', function () { }); }); - describe('COLR glyphs', function () { + describe('COLRv0 glyphs', function () { let font = fontkit.openSync(new URL('data/ss-emoji/ss-emoji-microsoft.ttf', import.meta.url)); - it('should get an SBIXGlyph', function () { + it('should get an COLRGlyph', function () { let glyph = font.glyphsForString('😜')[0]; return assert.equal(glyph.type, 'COLR'); }); @@ -255,6 +255,38 @@ describe('glyphs', function () { }); }); + + describe('COLRv1 glyphs', function () { + let font = fontkit.openSync(new URL('data/COLRv1/noto_handwriting-glyf_colr_1.ttf', import.meta.url)); + + it('should get an COLRv1Glyph', function () { + let glyph = font.glyphsForString('✍')[0]; + return assert.equal(glyph.type, 'COLRv1'); + }); + + it('should get layers', function () { + let glyph = font.glyphsForString('✍')[0]; + return assert.deepEqual(glyph.layers, [ + { glyph: font.getGlyph(247), color: { red: 252, green: 194, blue: 0, alpha: 255 } }, + { glyph: font.getGlyph(248), color: { red: 159, green: 79, blue: 0, alpha: 255 } }, + { glyph: font.getGlyph(249), color: { red: 229, green: 65, blue: 65, alpha: 255 } } + ]); + }); + + it('should get empty path', function () { + let glyph = font.glyphsForString('✍')[0]; + return assert.equal(glyph.path.toSVG(), ''); + }); + + it('should get bbox', function () { + let glyph = font.glyphsForString('✍')[0]; + assert.deepEqual(glyph.bbox.minX, 64); + assert.deepEqual(glyph.bbox.minY, -224); + assert.deepEqual(glyph.bbox.maxX, 1216); + assert.deepEqual(glyph.bbox.maxY, 928); + }); + }); + describe('WOFF ttf glyphs', function () { let font = fontkit.openSync(new URL('data/SourceSansPro/SourceSansPro-Regular.ttf.woff', import.meta.url)); let glyph = font.glyphsForString('D')[0];