From 388e6ae127cbefcb89eb5e03af41b2c327bac3d2 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Thu, 1 Dec 2022 21:43:59 +0000 Subject: [PATCH 01/22] Move DeltaSetIndexMap to variations --- src/tables/HVAR.js | 34 +--------------------------------- src/tables/variations.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 33 deletions(-) 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..aabdef6f 100644 --- a/src/tables/variations.js +++ b/src/tables/variations.js @@ -39,6 +39,43 @@ 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) +}); + +export let DeltaSetIndexMap = new r.Struct({ + entryFormat: r.uint16, + mapCount: r.uint16, + mapData: new r.Array(MapDataEntry, 'mapCount') +}); + + /********************** * Feature Variations * **********************/ From b577acb5fc590fb98ea05dd8ee821070b565e7c7 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 2 Dec 2022 08:22:32 +0000 Subject: [PATCH 02/22] DeltaSetIndexMap has two versions now --- src/tables/variations.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/tables/variations.js b/src/tables/variations.js index aabdef6f..c3bf016f 100644 --- a/src/tables/variations.js +++ b/src/tables/variations.js @@ -69,10 +69,17 @@ let MapDataEntry = new r.Struct({ innerIndex: t => t.entry & ((1 << ((t.parent.entryFormat & 0x000F) + 1)) - 1) }); -export let DeltaSetIndexMap = new r.Struct({ - entryFormat: r.uint16, - mapCount: r.uint16, - mapData: new r.Array(MapDataEntry, 'mapCount') +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') + } }); From ea4dae14423fd11e13d31d45df0c305842a44ff7 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Fri, 2 Dec 2022 15:23:07 +0000 Subject: [PATCH 03/22] COLRv1 first draft --- src/tables/COLR.js | 388 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 382 insertions(+), 6 deletions(-) diff --git a/src/tables/COLR.js b/src/tables/COLR.js index 6a4b1697..ee3e141c 100644 --- a/src/tables/COLR.js +++ b/src/tables/COLR.js @@ -1,4 +1,12 @@ 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). @@ -16,10 +24,378 @@ let BaseGlyphRecord = new r.Struct({ numLayers: r.uint16 }); -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 +// 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.uint16, { + 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.uint8, // Index for a CPAL palette entry. + alpha: F2DOT14 // Alpha value. + }, + // PaintVarSolid + 3: { + paletteIndex: r.uint8, // 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, 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. + 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, 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. + 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, 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. + 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. + dx: FWORD, // 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 5-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 5-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 5-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 5-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') +}); + +// "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, + }, + 2: { + varIndexBase: r.uint32 + } +}); + +var Clip = new r.Struct({ + startGlyphId: r.uint16, + endGlyphId: r.uint16, + clipBox: new r.Pointer(r.uint24, ClipBox) +}); + +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) // 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), + } }); From c2e3e2824e790f78f98e1e5b589989dc7b04319b Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Mon, 5 Dec 2022 14:55:35 +0000 Subject: [PATCH 04/22] Fix offset/version errors for Paint table --- src/tables/COLR.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/tables/COLR.js b/src/tables/COLR.js index ee3e141c..2401bdc9 100644 --- a/src/tables/COLR.js +++ b/src/tables/COLR.js @@ -106,7 +106,7 @@ export let CompositionMode = { // 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.uint16, { +var Paint = new r.VersionedStruct(r.uint8, { header: {}, // PaintColrLayers 1: { @@ -349,6 +349,7 @@ var ClipBox = new r.VersionedStruct(r.uint8, { xMax: r.int16, yMax: r.int16, }, + 1: {}, 2: { varIndexBase: r.uint32 } @@ -357,7 +358,7 @@ var ClipBox = new r.VersionedStruct(r.uint8, { var Clip = new r.Struct({ startGlyphId: r.uint16, endGlyphId: r.uint16, - clipBox: new r.Pointer(r.uint24, ClipBox) + clipBox: new r.Pointer(r.uint24, ClipBox, { type: 'parent' }) }); var ClipList = new r.Struct({ @@ -373,8 +374,8 @@ var ClipList = new r.Struct({ let BaseGlyphPaintRecord = new r.Struct({ gid: r.uint16, // Glyph ID of the base glyph. - paint: new r.Pointer(r.uint32, Paint) // Offset to a Paint table. -}); + paint: new r.Pointer(r.uint32, Paint, { type: 'parent' }) // Offset to a Paint table. + }); let BaseGlyphList = new r.Struct({ From f13ec56db7854dc4abc9a8efb898b8e98c74c212 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Mon, 5 Dec 2022 15:18:51 +0000 Subject: [PATCH 05/22] Use ClipList for bboxes in COLRv1 glyphs --- src/TTFFont.js | 8 ++++++-- src/glyph/COLRGlyph.js | 26 +++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/TTFFont.js b/src/TTFFont.js index 6aa0937a..147c344d 100644 --- a/src/TTFFont.js +++ b/src/TTFFont.js @@ -8,7 +8,7 @@ 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 GlyphVariationProcessor from './glyph/GlyphVariationProcessor'; import TTFSubset from './subset/TTFSubset'; import CFFSubset from './subset/CFFSubset'; @@ -412,7 +412,11 @@ 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); + if (this.COLR.version == 0) { + this._glyphs[glyph] = new COLRGlyph(glyph, characters, this); + } else { + this._glyphs[glyph] = new COLRv1Glyph(glyph, characters, this); + } } else { this._getBaseGlyph(glyph, characters); diff --git a/src/glyph/COLRGlyph.js b/src/glyph/COLRGlyph.js index 85d594b6..2512f235 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() { @@ -88,3 +88,27 @@ export default class COLRGlyph extends Glyph { return; } } + + +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() + } +} From 29c157c3412e17733d53feea9481effc2ec99e69 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Mon, 5 Dec 2022 15:19:29 +0000 Subject: [PATCH 06/22] Some tests (currently layers fails) --- .../COLRv1/noto_handwriting-glyf_colr_1.ttf | Bin 0 -> 5072 bytes test/glyphs.js | 36 +++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 test/data/COLRv1/noto_handwriting-glyf_colr_1.ttf 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 0000000000000000000000000000000000000000..513ea0ae2fedcc7eb75ff9e40a3fabb5e9d9f293 GIT binary patch literal 5072 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 Date: Tue, 6 Dec 2022 13:56:52 +0000 Subject: [PATCH 07/22] Fix palette index size --- src/tables/COLR.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tables/COLR.js b/src/tables/COLR.js index 2401bdc9..527bb9bd 100644 --- a/src/tables/COLR.js +++ b/src/tables/COLR.js @@ -115,12 +115,12 @@ var Paint = new r.VersionedStruct(r.uint8, { }, // PaintSolid 2: { - paletteIndex: r.uint8, // Index for a CPAL palette entry. + paletteIndex: r.uint16, // Index for a CPAL palette entry. alpha: F2DOT14 // Alpha value. }, // PaintVarSolid 3: { - paletteIndex: r.uint8, // Index for a CPAL palette entry. + paletteIndex: r.uint16, // Index for a CPAL palette entry. alpha: F2DOT14, // Alpha value. varIndexBase: r.uint32 // Base index into DeltaSetIndexMap. }, From ed63558ccc029c1c94eeac4eb357e61c45cc10ae Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 6 Dec 2022 14:04:10 +0000 Subject: [PATCH 08/22] Allow for constructing Paints while parsing a Paint --- src/tables/COLR.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tables/COLR.js b/src/tables/COLR.js index 527bb9bd..3161af99 100644 --- a/src/tables/COLR.js +++ b/src/tables/COLR.js @@ -106,7 +106,9 @@ export let CompositionMode = { // 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, { +var Paint = new r.VersionedStruct(r.uint8, {}); +// Declare first, then fill the version, to allow for self-use. +Paint.versions = { header: {}, // PaintColrLayers 1: { @@ -333,7 +335,7 @@ var Paint = new r.VersionedStruct(r.uint8, { 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, From 978b58995ffb2f2b4fb6ee312f203d40a7df92a2 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 6 Dec 2022 14:04:54 +0000 Subject: [PATCH 09/22] Return wrapped paint tree structures --- src/TTFFont.js | 23 ++- src/glyph/COLRGlyph.js | 13 +- src/glyph/PaintOperation.js | 366 ++++++++++++++++++++++++++++++++++++ 3 files changed, 393 insertions(+), 9 deletions(-) create mode 100644 src/glyph/PaintOperation.js diff --git a/src/TTFFont.js b/src/TTFFont.js index 147c344d..8d3efa1b 100644 --- a/src/TTFFont.js +++ b/src/TTFFont.js @@ -9,6 +9,7 @@ import TTFGlyph from './glyph/TTFGlyph'; import CFFGlyph from './glyph/CFFGlyph'; import SBIXGlyph from './glyph/SBIXGlyph'; 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'; @@ -397,6 +398,20 @@ export default class TTFFont { return this._glyphs[glyph] || null; } + _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,12 +427,8 @@ export default class TTFFont { this._glyphs[glyph] = new SBIXGlyph(glyph, characters, this); } else if ((this.directory.tables.COLR) && (this.directory.tables.CPAL)) { - if (this.COLR.version == 0) { - this._glyphs[glyph] = new COLRGlyph(glyph, characters, this); - } else { - this._glyphs[glyph] = new COLRv1Glyph(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 2512f235..533f1105 100644 --- a/src/glyph/COLRGlyph.js +++ b/src/glyph/COLRGlyph.js @@ -89,7 +89,11 @@ export class COLRGlyph extends Glyph { } } - +/** + * 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'; @@ -105,10 +109,13 @@ export class COLRv1Glyph extends COLRGlyph { box.yMin, box.xMax, box.yMax - ) + ); } } } - return super._getBBox() + return super._getBBox(); + } + render(ctx, size) { + this.paint.render(ctx, size); } } diff --git a/src/glyph/PaintOperation.js b/src/glyph/PaintOperation.js new file mode 100644 index 00000000..bbae7e50 --- /dev/null +++ b/src/glyph/PaintOperation.js @@ -0,0 +1,366 @@ +import * as fontkit from '../base'; + +class PaintOperation { + constructor(paint, font) { + this.paint = paint; + this.font = font; + this.cpal = font.CPAL; + this.layerList = font.COLR.layerList?.paint; + this.next = null; + this.layers = []; + } + render(_ctx, _size) {} + instantiate(location) { + this._instantiate_others(location); + return this; + } + _instantiate_others(location) { + if (this.next) { + this.next = this.next.instantiate(location); + } + for (let layer in this.layers) { + layer.instantiate(location); + } + } +} + +class VariablePaintOperation extends PaintOperation { + instantiate(location) { // Do something clever here + this._instantiate_others(location); + return this; + } + +} + +export var PAINT_OPERATIONS = [null]; + +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); +} + +// 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) { + let count = 0; + for (let layer of this.layers) { + ctx.save(); + layer.render(ctx, size); + ctx.restore(); + count = count + 1; + } + } +} + +PAINT_OPERATIONS.push(PaintColrLayersOperation); + +/* + * Fill-related paints + */ + +// PaintSolid +class PaintSolidOperation extends PaintOperation { + 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})`; + } +} +PAINT_OPERATIONS.push(PaintSolidOperation); + +// PaintVarSolid +class PaintVarSolidOperation extends VariablePaintOperation {} +PAINT_OPERATIONS.push(PaintVarSolidOperation); + +// PaintLinearGradient +class PaintGradientOperation extends PaintOperation { + _renderColorLine(gradient, colorline) { + for (let stop of colorline.colorStops) { + var color = this.cpal.colorRecords[stop.paletteIndex]; + var alpha = color.alpha / 255 * stop.alpha; + gradient.addColorStop(stop.stopOffset, `rgba(${color.red}, ${color.green}, ${color.blue}, ${alpha})`); + } + } +} +class PaintLinearGradientOperation extends PaintGradientOperation { + render(ctx, _size) { + let gradient = ctx.createLinearGradient(this.paint.x0, this.paint.y0, this.paint.x1, this.paint.y1); + // XXX This does not handle the x2,y2 (rotation point) + this._renderColorLine(gradient, this.paint.colorLine); + ctx.fillStyle = gradient; + } +} +PAINT_OPERATIONS.push(PaintLinearGradientOperation); + +// PaintVarLinearGradient +class PaintVarLinearGradientOperation extends VariablePaintOperation {} +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; + } +} +PAINT_OPERATIONS.push(PaintRadialGradientOperation); + +// PaintVarRadialGradient +class PaintVarRadialGradientOperation extends VariablePaintOperation {} +PAINT_OPERATIONS.push(PaintVarRadialGradientOperation); + +// PaintSweepGradient +class PaintSweepGradientOperation extends PaintOperation { + render(ctx, _size) { + const angle = this.paint.startAngle * Math.PI; + 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 {} +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) { + ctx.save(); + // Set fill, transform, etc. + ctx.beginPath(); + this.next.render(ctx, size); + const glyph = this.font._getBaseGlyph(this.paint.glyphID); + glyph.render(ctx, size); + ctx.restore(); + } +} +PAINT_OPERATIONS.push(PaintGlyphOperation); + +// PaintColrGlyph +class PaintColrGlyphOperation extends PaintOperation { + render(ctx, size) { + // We want a COLRGlyph or COLRv1Glyph here, not a base glyph + const glyph = this.font.getGlyph(this.paint.glyphID); + glyph.render(ctx, size); + } +} +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) { + 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); + } +} +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 {} +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, + }; + } +} +PAINT_OPERATIONS.push(PaintScaleOperation); + +// PaintVarScale +class PaintVarScaleOperation extends PaintVarTransformOperation {} +PAINT_OPERATIONS.push(PaintVarScaleOperation); + +// PaintScaleAroundCenter +class PaintScaleAroundCenterOperation extends PaintTransformOperation { + get affine() { + return; // XXX + } +} +PAINT_OPERATIONS.push(PaintScaleAroundCenterOperation); + +// PaintVarScaleAroundCenter +class PaintVarScaleAroundCenterOperation extends PaintVarTransformOperation {} +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, + }; + } +} +PAINT_OPERATIONS.push(PaintScaleUniformOperation); + +// PaintVarScale +class PaintVarScaleUniformOperation extends PaintVarTransformOperation {} +PAINT_OPERATIONS.push(PaintVarScaleUniformOperation); + +// PaintScaleUniformAroundCenter +class PaintScaleUniformAroundCenterOperation extends PaintTransformOperation { + get affine() { + return; // XXX + } +} +PAINT_OPERATIONS.push(PaintScaleUniformAroundCenterOperation); + +// PaintVarScaleUniformAroundCenter +class PaintVarScaleUniformAroundCenterOperation extends PaintVarTransformOperation {} +PAINT_OPERATIONS.push(PaintVarScaleUniformAroundCenterOperation); + +// PaintRotate +class PaintRotateOperation extends PaintTransformOperation { + get affine() { + return { + xx: this.paint.scale, + yx: 0, + xy: 0, + yy: this.paint.scale, + dx: 0, + dy: 0, + }; + } +} +PAINT_OPERATIONS.push(PaintRotateOperation); + +// PaintVarRotate +class PaintVarRotateOperation extends PaintVarTransformOperation {} +PAINT_OPERATIONS.push(PaintVarRotateOperation); + +// PaintRotateAroundCenter +class PaintRotateAroundCenterOperation extends PaintTransformOperation { + get affine() { + return; // XXX + } +} +PAINT_OPERATIONS.push(PaintRotateAroundCenterOperation); + +// PaintVarRotateAroundCenter +class PaintVarRotateAroundCenterOperation extends PaintVarTransformOperation {} +PAINT_OPERATIONS.push(PaintVarRotateAroundCenterOperation); + +// PaintRotate +class PaintSkewOperation extends PaintTransformOperation { + get affine() { + return; // XXX + } +} +PAINT_OPERATIONS.push(PaintSkewOperation); + +// PaintVarSkew +class PaintVarSkewOperation extends PaintVarTransformOperation {} +PAINT_OPERATIONS.push(PaintVarSkewOperation); + +// PaintSkewAroundCenter +class PaintSkewAroundCenterOperation extends PaintTransformOperation { + get affine() { + return; // XXX + } +} +PAINT_OPERATIONS.push(PaintSkewAroundCenterOperation); + +// PaintVarSkewAroundCenter +class PaintVarSkewAroundCenterOperation extends PaintVarTransformOperation {} +PAINT_OPERATIONS.push(PaintVarSkewAroundCenterOperation); + +/* And finally... */ +// PaintComposite +class PaintComposite extends PaintOperation { + source() { + return makePaintOperation(this.paint.source, this.font); + } + backdrop() { + return makePaintOperation(this.paint.backdrop, this.font); + } +} + +PAINT_OPERATIONS.push(PaintComposite); + +if (PAINT_OPERATIONS.length != 33) { + throw 'Not all paints registered'; +} From c5d402cb548d831110af7be2de26d1f2ec1fd05f Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 6 Dec 2022 14:19:26 +0000 Subject: [PATCH 10/22] Remove debugging code --- src/glyph/PaintOperation.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/glyph/PaintOperation.js b/src/glyph/PaintOperation.js index bbae7e50..de5cc879 100644 --- a/src/glyph/PaintOperation.js +++ b/src/glyph/PaintOperation.js @@ -58,12 +58,10 @@ class PaintColrLayersOperation extends PaintOperation { } render(ctx, size) { - let count = 0; for (let layer of this.layers) { ctx.save(); layer.render(ctx, size); ctx.restore(); - count = count + 1; } } } From f5b6e5c82a1dee7ad461f3a1d3291d7f13c7716d Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 6 Dec 2022 14:25:42 +0000 Subject: [PATCH 11/22] Reformatting --- src/tables/COLR.js | 77 ++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/src/tables/COLR.js b/src/tables/COLR.js index 3161af99..d1fd7692 100644 --- a/src/tables/COLR.js +++ b/src/tables/COLR.js @@ -9,19 +9,14 @@ 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 @@ -35,7 +30,7 @@ let Affine2x3 = new r.Struct({ 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. @@ -45,7 +40,7 @@ let VarAffine2x3 = new r.Struct({ 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({ @@ -65,12 +60,12 @@ 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 = { @@ -102,7 +97,7 @@ export let CompositionMode = { 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. @@ -187,7 +182,7 @@ Paint.versions = { }, // PaintGlyph 10: { - paint: new r.Pointer(r.uint24, Paint), // Paint table. + paint: new r.Pointer(r.uint24, Paint), // Paint table. glyphID: r.uint16 // Glyph ID for the source outline. }, // PaintColrGlyph @@ -206,41 +201,41 @@ Paint.versions = { }, // PaintTranslate 14: { - paint: new r.Pointer(r.uint24, Paint), // Paint table. + 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. + 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. + 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. + 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. + 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. + centerY: FWORD // y coordinate for the center of scaling. }, // PaintVarScaleAroundCenter 19: { - paint: new r.Pointer(r.uint24, Paint), // Paint table. + paint: new r.Pointer(r.uint24, Paint), // Paint table. dx: FWORD, // Scale factor in x direction. scaleY: F2DOT14, // Scale factor in y direction. centerX: FWORD, // x coordinate for the center of scaling. @@ -249,25 +244,25 @@ Paint.versions = { }, // PaintScaleUniform 20: { - paint: new r.Pointer(r.uint24, Paint), // Paint table. + 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. + 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. + 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. + 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. @@ -275,25 +270,25 @@ Paint.versions = { }, // PaintRotate 24: { - paint: new r.Pointer(r.uint24, Paint), // Paint table. + 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. + 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. + 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. + 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. @@ -301,30 +296,30 @@ Paint.versions = { }, // PaintSkew 28: { - paint: new r.Pointer(r.uint24, Paint), // Paint table. + 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 5-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. + 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 5-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. + 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 5-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. + 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 5-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. @@ -366,7 +361,7 @@ var Clip = new r.Struct({ var ClipList = new r.Struct({ format: r.uint8, numClips: r.uint32, - clips: new r.Array(Clip, "numClips") + clips: new r.Array(Clip, 'numClips') }); // "The BaseGlyphList table is, conceptually, similar to the baseGlyphRecords @@ -377,7 +372,7 @@ var ClipList = new r.Struct({ 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({ From ed00f7d64ed92d1a3984e76befd4847a942987ba Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 6 Dec 2022 14:26:15 +0000 Subject: [PATCH 12/22] Variable things use variable colour lines --- src/tables/COLR.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tables/COLR.js b/src/tables/COLR.js index d1fd7692..16eadd3e 100644 --- a/src/tables/COLR.js +++ b/src/tables/COLR.js @@ -133,7 +133,7 @@ Paint.versions = { }, // PaintVarLinearGradient 5: { - colorLine: new r.Pointer(r.uint24, ColorLine), // Offset to ColorLine table. + 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. @@ -154,7 +154,7 @@ Paint.versions = { }, // PaintVarRadialGradient 7: { - colorLine: new r.Pointer(r.uint24, ColorLine), // Offset to ColorLine table. + 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. @@ -173,7 +173,7 @@ Paint.versions = { }, // PaintVarSweepGradient 9: { - colorLine: new r.Pointer(r.uint24, ColorLine), // Offset to ColorLine table. + 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. From a187fa1658afd1d411f8a2b83c9e2edb041d2391 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 6 Dec 2022 15:08:12 +0000 Subject: [PATCH 13/22] Handle components in colour glyphs --- src/TTFFont.js | 15 +++++++++------ src/glyph/COLRGlyph.js | 5 +++++ src/glyph/PaintOperation.js | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/TTFFont.js b/src/TTFFont.js index 8d3efa1b..ff5a877c 100644 --- a/src/TTFFont.js +++ b/src/TTFFont.js @@ -387,17 +387,20 @@ 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) { diff --git a/src/glyph/COLRGlyph.js b/src/glyph/COLRGlyph.js index 533f1105..abda4fe3 100644 --- a/src/glyph/COLRGlyph.js +++ b/src/glyph/COLRGlyph.js @@ -87,6 +87,11 @@ export class COLRGlyph extends Glyph { return; } + _getContours() { + var base = this._font._getBaseGlyphUncached(this.id); + return base._getContours(); + } + } /** diff --git a/src/glyph/PaintOperation.js b/src/glyph/PaintOperation.js index de5cc879..eba6cb7e 100644 --- a/src/glyph/PaintOperation.js +++ b/src/glyph/PaintOperation.js @@ -159,7 +159,7 @@ class PaintGlyphOperation extends PaintOperation { // Set fill, transform, etc. ctx.beginPath(); this.next.render(ctx, size); - const glyph = this.font._getBaseGlyph(this.paint.glyphID); + const glyph = this.font._getBaseGlyphUncached(this.paint.glyphID); glyph.render(ctx, size); ctx.restore(); } From 8842816ce42557199f798347ed2e66787d0b1347 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 6 Dec 2022 16:02:18 +0000 Subject: [PATCH 14/22] There are now two types of delta set, with either 16/8 bit deltas or 32/16 bit deltas. --- src/tables/variations.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/tables/variations.js b/src/tables/variations.js index c3bf016f..4d57a3f9 100644 --- a/src/tables/variations.js +++ b/src/tables/variations.js @@ -18,9 +18,15 @@ 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) }); @@ -28,10 +34,17 @@ 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') + regionIndexes: new r.Array(r.uint16, 'regionIndexCount') }); +ItemVariationData.process = function(stream) { + var decoder = new r.Array( + (this.shortDeltaCount & 0x8000) ? longDeltaSet : shortDeltaSet, + this.itemCount + ); + this.deltaSets = decoder.decode(stream, this); +}; + export let ItemVariationStore = new r.Struct({ format: r.uint16, variationRegionList: new r.Pointer(r.uint32, VariationRegionList), From c8056fe90195af0fca605aa17f822bc357c310da Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Thu, 5 Jan 2023 12:58:21 +0000 Subject: [PATCH 15/22] Oops, bad naming --- src/tables/COLR.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tables/COLR.js b/src/tables/COLR.js index 16eadd3e..6f2b618f 100644 --- a/src/tables/COLR.js +++ b/src/tables/COLR.js @@ -236,7 +236,7 @@ Paint.versions = { // PaintVarScaleAroundCenter 19: { paint: new r.Pointer(r.uint24, Paint), // Paint table. - dx: FWORD, // Scale factor in x direction. + 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. From d55cdf49c0024ea3f1c69def336817ad96b0644c Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Thu, 5 Jan 2023 12:58:43 +0000 Subject: [PATCH 16/22] Instantiate some variable paint operations --- src/glyph/COLRGlyph.js | 7 +- src/glyph/PaintOperation.js | 280 ++++++++++++++++++++++++++++++------ 2 files changed, 243 insertions(+), 44 deletions(-) diff --git a/src/glyph/COLRGlyph.js b/src/glyph/COLRGlyph.js index abda4fe3..d0480720 100644 --- a/src/glyph/COLRGlyph.js +++ b/src/glyph/COLRGlyph.js @@ -121,6 +121,11 @@ export class COLRv1Glyph extends COLRGlyph { return super._getBBox(); } render(ctx, size) { - this.paint.render(ctx, size); + let paint = this.paint; + if (this._font.variationCoords) { + paint = paint.instantiate(this._font._variationProcessor); + } + + paint.render(ctx, size); } } diff --git a/src/glyph/PaintOperation.js b/src/glyph/PaintOperation.js index eba6cb7e..2f54c125 100644 --- a/src/glyph/PaintOperation.js +++ b/src/glyph/PaintOperation.js @@ -1,4 +1,5 @@ import * as fontkit from '../base'; +import GlyphVariationProcessor from './GlyphVariationProcessor'; class PaintOperation { constructor(paint, font) { @@ -6,30 +7,47 @@ class PaintOperation { 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) {} - instantiate(location) { - this._instantiate_others(location); - return this; - } - _instantiate_others(location) { + instantiate(processor) { + var instantiated = new (this.constructor)(this.paint, this.font); + instantiated = instantiated._instantiate_this(processor); if (this.next) { - this.next = this.next.instantiate(location); + instantiated.next = this.next.instantiate(processor); } - for (let layer in this.layers) { - layer.instantiate(location); + instantiated.layers = []; + for (let layer of this.layers) { + instantiated.layers.push(layer.instantiate(processor)); } + return instantiated; + } + _instantiate_this(_processor) { + return this; } } class VariablePaintOperation extends PaintOperation { - instantiate(location) { // Do something clever here - this._instantiate_others(location); + _instantiate_this(_processor) { // Do something clever here return this; } - + getDeltas(instantiator, count) { + let res = []; + let ix = this.paint.varIndexBase; + while (res.length < count) { + let {outerIndex, innerIndex} = this.font.COLR.varIndexMap.mapData[ix]; + let delta = 0; + try { + delta = instantiator.getDelta(this.ivs, outerIndex, innerIndex); + } catch { + } + res.push(delta); + ix += 1; + } + return res; + } } export var PAINT_OPERATIONS = [null]; @@ -88,10 +106,16 @@ PAINT_OPERATIONS.push(PaintVarSolidOperation); // PaintLinearGradient class PaintGradientOperation extends PaintOperation { _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; - gradient.addColorStop(stop.stopOffset, `rgba(${color.red}, ${color.green}, ${color.blue}, ${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})`); } } } @@ -127,13 +151,14 @@ class PaintVarRadialGradientOperation extends VariablePaintOperation {} PAINT_OPERATIONS.push(PaintVarRadialGradientOperation); // PaintSweepGradient -class PaintSweepGradientOperation extends PaintOperation { +class PaintSweepGradientOperation extends PaintGradientOperation { render(ctx, _size) { const angle = this.paint.startAngle * Math.PI; let gradient = ctx.createConicGradient( angle, this.paint.centerX, this.paint.centerY ); this._renderColorLine(gradient, this.colorLine); + // console.log(gradient); ctx.fillStyle = gradient; } @@ -141,7 +166,9 @@ class PaintSweepGradientOperation extends PaintOperation { PAINT_OPERATIONS.push(PaintSweepGradientOperation); // PaintVarSweepGradient -class PaintVarSweepGradientOperation extends VariablePaintOperation {} +class PaintVarSweepGradientOperation extends VariablePaintOperation { + +} PAINT_OPERATIONS.push(PaintVarSweepGradientOperation); /* @@ -207,6 +234,10 @@ class PaintVarTransformOperation extends VariablePaintOperation { super(paint, font); this.next = makePaintOperation(this.paint.paint, this.font); } + _instantiate_this(processor) { + return this; + // XXX + } } PAINT_OPERATIONS.push(PaintVarTransformOperation); @@ -222,11 +253,25 @@ class PaintTranslateOperation extends PaintTransformOperation { dy: this.paint.dy, }; } + } PAINT_OPERATIONS.push(PaintTranslateOperation); // PaintVarTranslate -class PaintVarTranslateOperation extends PaintVarTransformOperation {} +class PaintVarTranslateOperation extends PaintVarTransformOperation { + _instantiate_this(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 @@ -241,23 +286,59 @@ class PaintScaleOperation extends PaintTransformOperation { 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 {} +class PaintVarScaleOperation extends PaintVarTransformOperation { + _instantiate_this(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 { - get affine() { - return; // XXX + 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 {} +class PaintVarScaleAroundCenterOperation extends PaintVarTransformOperation { + _instantiate_this(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 @@ -272,54 +353,112 @@ class PaintScaleUniformOperation extends PaintTransformOperation { 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 {} +class PaintVarScaleUniformOperation extends PaintVarTransformOperation { + _instantiate_this(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 + ); + // console.log(rv); + return rv; + } +} PAINT_OPERATIONS.push(PaintVarScaleUniformOperation); // PaintScaleUniformAroundCenter class PaintScaleUniformAroundCenterOperation extends PaintTransformOperation { - get affine() { - return; // XXX + render(ctx, size) { + // Something not right here... + + // 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 {} +class PaintVarScaleUniformAroundCenterOperation extends PaintVarTransformOperation { + _instantiate_this(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 { - get affine() { - return { - xx: this.paint.scale, - yx: 0, - xy: 0, - yy: this.paint.scale, - dx: 0, - dy: 0, - }; + render(ctx, size) { + ctx.rotate(this.paint.angle * Math.PI); + this.next.render(ctx, size); } } PAINT_OPERATIONS.push(PaintRotateOperation); // PaintVarRotate -class PaintVarRotateOperation extends PaintVarTransformOperation {} -PAINT_OPERATIONS.push(PaintVarRotateOperation); +class PaintVarRotateOperation extends PaintVarTransformOperation { + _instantiate_this(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 { - get affine() { - return; // XXX - } -} + 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 {} +class PaintVarRotateAroundCenterOperation extends PaintVarTransformOperation { + _instantiate_this(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 @@ -348,12 +487,67 @@ 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 { - source() { - return makePaintOperation(this.paint.source, this.font); + 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]; } - backdrop() { - return makePaintOperation(this.paint.backdrop, this.font); + 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(); + ctx.globalCompositeOperation = CANVAS_COMPOSITING_MODES[this.paint.compositeMode]; + this.source.render(ctx, size); + + ctx.restore(); + } } } From 2e95a2e058d6d5dfe8106c45543c43aeaf63133f Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Mon, 22 May 2023 16:25:50 +0100 Subject: [PATCH 17/22] Implement a bunch more paints, fix scaling issues --- src/glyph/COLRGlyph.js | 6 + src/glyph/PaintOperation.js | 280 ++++++++++++++++++++++++++++-------- 2 files changed, 223 insertions(+), 63 deletions(-) diff --git a/src/glyph/COLRGlyph.js b/src/glyph/COLRGlyph.js index d0480720..75631665 100644 --- a/src/glyph/COLRGlyph.js +++ b/src/glyph/COLRGlyph.js @@ -121,11 +121,17 @@ export class COLRv1Glyph extends COLRGlyph { 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 index 2f54c125..76edcc47 100644 --- a/src/glyph/PaintOperation.js +++ b/src/glyph/PaintOperation.js @@ -1,5 +1,27 @@ import * as fontkit from '../base'; -import GlyphVariationProcessor from './GlyphVariationProcessor'; +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) { @@ -11,10 +33,18 @@ class PaintOperation { this.next = null; this.layers = []; } - render(_ctx, _size) {} + + 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_this(processor); + instantiated = instantiated._instantiate(processor); if (this.next) { instantiated.next = this.next.instantiate(processor); } @@ -24,45 +54,56 @@ class PaintOperation { } return instantiated; } - _instantiate_this(_processor) { + + // 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 { - _instantiate_this(_processor) { // Do something clever here - return this; + // ...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) { + + getDeltas(instantiator, count, thing) { let res = []; - let ix = this.paint.varIndexBase; + if (!thing) { thing = this.paint; } + let ix = thing.varIndexBase; while (res.length < count) { let {outerIndex, innerIndex} = this.font.COLR.varIndexMap.mapData[ix]; - let delta = 0; try { - delta = instantiator.getDelta(this.ivs, outerIndex, innerIndex); + res.push(instantiator.getDelta(this.ivs, outerIndex, innerIndex)); } catch { + res.push(0); } - res.push(delta); ix += 1; } return res; } } -export var PAINT_OPERATIONS = [null]; - -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); -} - // PaintColrLayers class PaintColrLayersOperation extends PaintOperation { constructor(paint, font) { @@ -89,59 +130,102 @@ 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 PaintOperation { - render(ctx, _size) { +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 {} +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 PaintOperation { +class PaintGradientOperation extends PaintFillOperation { _renderColorLine(gradient, colorline) { if (!colorline || !colorline.colorStops) { - return + 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) + 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) { + render(ctx, size) { let gradient = ctx.createLinearGradient(this.paint.x0, this.paint.y0, this.paint.x1, this.paint.y1); // XXX This does not handle the x2,y2 (rotation point) this._renderColorLine(gradient, this.paint.colorLine); ctx.fillStyle = gradient; + this.floodFill(ctx, size); } } PAINT_OPERATIONS.push(PaintLinearGradientOperation); // PaintVarLinearGradient -class PaintVarLinearGradientOperation extends VariablePaintOperation {} +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) { + 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); @@ -182,13 +266,23 @@ class PaintGlyphOperation extends PaintOperation { this.next = makePaintOperation(this.paint.paint, this.font); } render(ctx, size) { - ctx.save(); // Set fill, transform, etc. ctx.beginPath(); - this.next.render(ctx, size); const glyph = this.font._getBaseGlyphUncached(this.paint.glyphID); - glyph.render(ctx, size); - ctx.restore(); + let path = glyph.path; + path.commands.pop(); + let fn = glyph.path.toFunction(); + fn(ctx); + ctx.clip(); + + this.next.render(ctx, size); + // // Use this as a clipping mask + // let path = glyph.path; + // path.commands.pop(); // Remove the closepath + // let fn = path.toFunction(); + // fn(ctx); + // ctx.clip(); + // // ctx.fill(); } } PAINT_OPERATIONS.push(PaintGlyphOperation); @@ -197,8 +291,14 @@ PAINT_OPERATIONS.push(PaintGlyphOperation); 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); @@ -219,6 +319,7 @@ class PaintTransformOperation extends PaintOperation { } 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); @@ -234,9 +335,27 @@ class PaintVarTransformOperation extends VariablePaintOperation { super(paint, font); this.next = makePaintOperation(this.paint.paint, this.font); } - _instantiate_this(processor) { - return this; - // XXX + 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); @@ -259,7 +378,7 @@ PAINT_OPERATIONS.push(PaintTranslateOperation); // PaintVarTranslate class PaintVarTranslateOperation extends PaintVarTransformOperation { - _instantiate_this(processor) { + _instantiate(processor) { let [deltaX, deltaY] = this.getDeltas(processor, 2); let rv = new PaintTranslateOperation( { @@ -295,7 +414,7 @@ PAINT_OPERATIONS.push(PaintScaleOperation); // PaintVarScale class PaintVarScaleOperation extends PaintVarTransformOperation { - _instantiate_this(processor) { + _instantiate(processor) { let [scaleX, scaleY] = this.getDeltas(processor, 2); let rv = new PaintScaleOperation( { @@ -314,9 +433,9 @@ 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); + ctx.scale(this.paint.scaleX, this.paint.scaleY); + ctx.translate(-this.paint.centerX, -this.paint.centerY); this.next.render(ctx, size); } } @@ -324,7 +443,7 @@ PAINT_OPERATIONS.push(PaintScaleAroundCenterOperation); // PaintVarScaleAroundCenter class PaintVarScaleAroundCenterOperation extends PaintVarTransformOperation { - _instantiate_this(processor) { + _instantiate(processor) { let [scaleX, scaleY, centerX, centerY] = this.getDeltas(processor, 4); let rv = new PaintScaleAroundCenterOperation( { @@ -362,7 +481,7 @@ PAINT_OPERATIONS.push(PaintScaleUniformOperation); // PaintVarScale class PaintVarScaleUniformOperation extends PaintVarTransformOperation { - _instantiate_this(processor) { + _instantiate(processor) { let [deltaScale] = this.getDeltas(processor, 1); let rv = new PaintScaleUniformOperation( { @@ -371,7 +490,6 @@ class PaintVarScaleUniformOperation extends PaintVarTransformOperation { scale: this.paint.scale + deltaScale / (1<<14) }, this.font ); - // console.log(rv); return rv; } } @@ -380,11 +498,9 @@ PAINT_OPERATIONS.push(PaintVarScaleUniformOperation); // PaintScaleUniformAroundCenter class PaintScaleUniformAroundCenterOperation extends PaintTransformOperation { render(ctx, size) { - // Something not right here... - - // ctx.translate(this.paint.centerX, this.paint.centerY); - // ctx.scale(this.paint.scale, this.paint.scale); - // ctx.translate(-this.paint.centerX, -this.paint.centerY); + 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); } } @@ -392,7 +508,7 @@ PAINT_OPERATIONS.push(PaintScaleUniformAroundCenterOperation); // PaintVarScaleUniformAroundCenter class PaintVarScaleUniformAroundCenterOperation extends PaintVarTransformOperation { - _instantiate_this(processor) { + _instantiate(processor) { let [deltaScale, centerX, centerY] = this.getDeltas(processor, 3); let rv = new PaintScaleUniformAroundCenterOperation( { @@ -420,7 +536,7 @@ PAINT_OPERATIONS.push(PaintRotateOperation); // PaintVarRotate class PaintVarRotateOperation extends PaintVarTransformOperation { - _instantiate_this(processor) { + _instantiate(processor) { let [delta] = this.getDeltas(processor, 1); let rv = new PaintRotateOperation( { @@ -431,21 +547,22 @@ class PaintVarRotateOperation extends PaintVarTransformOperation { ); return rv; } -}PAINT_OPERATIONS.push(PaintVarRotateOperation); +} +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); + 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_this(processor) { + _instantiate(processor) { let [deltaAngle, deltaX, deltaY] = this.getDeltas(processor, 3); let rv = new PaintRotateAroundCenterOperation( { @@ -464,25 +581,62 @@ PAINT_OPERATIONS.push(PaintVarRotateAroundCenterOperation); // PaintRotate class PaintSkewOperation extends PaintTransformOperation { get affine() { - return; // XXX + 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 {} +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 { - get affine() { - return; // XXX + 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 {} +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... */ From 2302e2cbad2dd87ab1b8e2cdbdfa7c342be01440 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 7 Jun 2023 17:28:00 +0100 Subject: [PATCH 18/22] Handle rotation point of linear gradient --- src/glyph/PaintOperation.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/glyph/PaintOperation.js b/src/glyph/PaintOperation.js index 76edcc47..5e36aee9 100644 --- a/src/glyph/PaintOperation.js +++ b/src/glyph/PaintOperation.js @@ -183,8 +183,16 @@ class PaintGradientOperation extends PaintFillOperation { class PaintLinearGradientOperation extends PaintGradientOperation { render(ctx, size) { - let gradient = ctx.createLinearGradient(this.paint.x0, this.paint.y0, this.paint.x1, this.paint.y1); - // XXX This does not handle the x2,y2 (rotation point) + 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); From bfbb444f98b04be8714fd44098f3aaba39fb9494 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Thu, 8 Jun 2023 09:34:28 +0100 Subject: [PATCH 19/22] Instantiate var radial gradients --- src/glyph/PaintOperation.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/glyph/PaintOperation.js b/src/glyph/PaintOperation.js index 5e36aee9..1e66694a 100644 --- a/src/glyph/PaintOperation.js +++ b/src/glyph/PaintOperation.js @@ -239,7 +239,25 @@ class PaintRadialGradientOperation extends PaintGradientOperation { PAINT_OPERATIONS.push(PaintRadialGradientOperation); // PaintVarRadialGradient -class PaintVarRadialGradientOperation extends VariablePaintOperation {} +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 From d2cc0a8081388ded5e16f6db6bed0f16ee721117 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Thu, 8 Jun 2023 09:50:16 +0100 Subject: [PATCH 20/22] Clean up code --- src/glyph/PaintOperation.js | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/glyph/PaintOperation.js b/src/glyph/PaintOperation.js index 1e66694a..8be3b057 100644 --- a/src/glyph/PaintOperation.js +++ b/src/glyph/PaintOperation.js @@ -264,11 +264,12 @@ PAINT_OPERATIONS.push(PaintVarRadialGradientOperation); 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); - // console.log(gradient); ctx.fillStyle = gradient; } @@ -277,7 +278,21 @@ 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); @@ -302,13 +317,6 @@ class PaintGlyphOperation extends PaintOperation { ctx.clip(); this.next.render(ctx, size); - // // Use this as a clipping mask - // let path = glyph.path; - // path.commands.pop(); // Remove the closepath - // let fn = path.toFunction(); - // fn(ctx); - // ctx.clip(); - // // ctx.fill(); } } PAINT_OPERATIONS.push(PaintGlyphOperation); @@ -723,6 +731,9 @@ class PaintComposite extends PaintOperation { } 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); From c88ccb71167fcddcbe5e4788d441346c3d278a44 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Thu, 8 Jun 2023 09:54:14 +0100 Subject: [PATCH 21/22] Add COLRv1 test font from paintcompiler --- test/data/COLRv1/COLRv1-Test.ttf | Bin 0 -> 8868 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/data/COLRv1/COLRv1-Test.ttf diff --git a/test/data/COLRv1/COLRv1-Test.ttf b/test/data/COLRv1/COLRv1-Test.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e58043e8fbbb4b77ec572da887cf8d21a8e2998f GIT binary patch literal 8868 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 literal 0 HcmV?d00001 From 8c033c39dd369480aa8949ec4b45830191e6764d Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 12 Jul 2023 20:24:51 +0100 Subject: [PATCH 22/22] Bonus content! Allow for writing of variation data --- src/tables/variations.js | 98 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 8 deletions(-) diff --git a/src/tables/variations.js b/src/tables/variations.js index 4d57a3f9..c9997265 100644 --- a/src/tables/variations.js +++ b/src/tables/variations.js @@ -30,20 +30,52 @@ let longDeltaSet = new r.Struct({ 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') + regionIndexes: new r.Array(r.uint16, 'regionIndexCount'), + deltaSets: DeltaSets }); -ItemVariationData.process = function(stream) { - var decoder = new r.Array( - (this.shortDeltaCount & 0x8000) ? longDeltaSet : shortDeltaSet, - this.itemCount - ); - this.deltaSets = decoder.decode(stream, this); -}; +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, @@ -82,6 +114,21 @@ let MapDataEntry = new r.Struct({ 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, @@ -95,6 +142,41 @@ export let DeltaSetIndexMap = new r.VersionedStruct(r.uint8, { } }); +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 *