diff --git a/README.md b/README.md index 812b2c17..95973891 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,92 @@ Sets a color glyph layer's paletteIndex property to a new index ###### `Font.layers.updateColrTable(glyphIndex, layers)` Mainly used internally. Updates the colr table, adding a baseGlyphRecord if needed, ensuring that it's inserted at the correct position, updating numLayers, and adjusting firstLayerIndex values for all baseGlyphRecords according to any deletions or insertions. + +##### The `Font.variation` object (`VariationManager`) +The `VariationManager` handles variable font properties using the OpenType font variation tables. + +###### `Font.variation.activateDefaultVariation()` +Activates the default variation by setting its variation data as the font's default render options. Uses the default instance if available; otherwise, it defaults to the coordinates of all axes. + +###### `Font.variation.getDefaultCoordinates()` +Returns the default coordinates for the font's variation axes. +* Returns: An object mapping axis tags to their default values. + +###### `Font.variation.getDefaultInstanceIndex()` +Determines and returns the index of the default variation instance. Returns `-1` if it cannot be determined. +* Returns: Integer representing the default instance index or `-1`. + +###### `Font.variation.getInstanceIndex(coordinates)` +Finds the index of the variation instance that matches the provided coordinates, or `-1` if none match. +* `coordinates`: Object with axis tags as keys and variation values as corresponding values. +* Returns: Integer of the matching instance index or `-1`. + +###### `Font.variation.getInstance(index)` +Retrieves a specific variation instance by its zero-based index. +* `index`: Zero-based index of the variation instance. +* Returns: Object representing the variation instance, or `null` if the index is invalid. + +###### `Font.variation.set(instanceIdOrObject)` +Sets the variation coordinates to be used by default for rendering in the font's default render options. +* `instanceIdOrObject`: Either the zero-based index of a variation instance or an object mapping axis tags to variation values. + +###### `Font.variation.get()` +Gets the current variation settings from the font's default render options. +* Returns: Object with the current variation settings. + + +##### The `Font.variation.process` object (`VariationProcessor`) +The `VariationProcessor` is a component of the `VariationManager`, used mainly internally for computing and applying variations to the glyphs in a variable font. It handles transformations and adjustments based on the font's variable axes and instances. + +###### `Font.variation.process.getNormalizedCoords()` +Returns normalized coordinates for the variation axes based on the current settings. +* Returns: Normalized coordinates as an object mapping axis tags to normalized values. + +###### `Font.variation.process.interpolatePoints(points, deltas, scalar)` +Interpolates points based on provided deltas and a scalar value. +* `points`: Array of original points. +* `deltas`: Array of point deltas. +* `scalar`: Scalar value for interpolation. +* Returns: Array of interpolated points. + +###### `Font.variation.process.deltaInterpolate(original, deltaValues, scalar)` +Calculates the interpolated value for a single point given original values, deltas, and a scalar. +* `original`: Original value of the point. +* `deltaValues`: Array of delta values for the point. +* `scalar`: Scalar value for interpolation. +* Returns: Interpolated value. + +###### `Font.variation.process.deltaShift(points, deltas)` +Applies delta values to shift points. +* `points`: Array of original points. +* `deltas`: Array of deltas to apply. +* Returns: Array of shifted points. + +###### `Font.variation.process.transformComponents(components, transformation)` +Transforms components of a glyph using a specified transformation matrix. +* `components`: Components of the glyph. +* `transformation`: Transformation matrix to apply. +* Returns: Transformed components. + +###### `Font.variation.process.getTransform()` +Computes the transformation matrix based on current variation settings. +* Returns: Transformation matrix for the current variation settings. + +###### `Font.variation.process.getVariableAdjustment(adjustment)` +Calculates the variable adjustment for a given adjustment parameter. +* `adjustment`: Adjustment parameter. +* Returns: Adjusted value based on current variation settings. + +###### `Font.variation.process.getDelta(deltas)` +Selects the appropriate delta values from a collection of deltas based on the current variation settings. +* `deltas`: Collection of delta values. +* Returns: Appropriate delta values for the current settings. + +###### `Font.variation.process.getBlendVector()` +Computes the blend vector for interpolations based on the current settings. +* Returns: Blend vector used for interpolation calculations. + + #### The Glyph object A Glyph is an individual mark that often corresponds to a character. Some glyphs, such as ligatures, are a combination of many characters. Glyphs are the basic building blocks of a font. @@ -466,12 +552,13 @@ MIT Thanks ====== -I would like to acknowledge the work of others without which opentype.js wouldn't be possible: +We would like to acknowledge the work of others without which opentype.js wouldn't be possible: * [pdf.js](https://mozilla.github.io/pdf.js/): for an awesome implementation of font parsing in the browser. * [FreeType](https://www.freetype.org/): for the nitty-gritty details and filling in the gaps when the spec was incomplete. * [ttf.js](https://ynakajima.github.io/ttf.js/demo/glyflist/): for hints about the TrueType parsing code. * [CFF-glyphlet-fonts](https://pomax.github.io/CFF-glyphlet-fonts/): for a great explanation/implementation of CFF font writing. +* [fontkit](https://github.com/foliojs/fontkit/): for a great implementation of CFF2 parsing and variable font features * [tiny-inflate](https://github.com/foliojs/tiny-inflate): for WOFF decompression. * [Microsoft Typography](https://docs.microsoft.com/en-us/typography/opentype/spec/otff): the go-to reference for all things OpenType. * [Adobe Compact Font Format spec](http://download.microsoft.com/download/8/0/1/801a191c-029d-4af3-9642-555f6fe514ee/cff.pdf) and the [Adobe Type 2 Charstring spec](http://download.microsoft.com/download/8/0/1/801a191c-029d-4af3-9642-555f6fe514ee/type2.pdf): explains the data structures and commands for the CFF glyph format. diff --git a/bin/test-render b/bin/test-render index f0ec59c1..d1d041a4 100755 --- a/bin/test-render +++ b/bin/test-render @@ -37,6 +37,7 @@ function printUsage() { let filename; let testcase; let textToRender; +let variation; for (let i = 0; i < process.argv.length; i++) { const arg = process.argv[i]; if (arg.startsWith('--font=')) { @@ -45,6 +46,12 @@ for (let i = 0; i < process.argv.length; i++) { testcase = arg.substring('--testcase='.length); } else if (arg.startsWith('--render=')) { textToRender = arg.substring('--render='.length); + } else if (arg.startsWith('--variation=')) { + variation = {}; + arg.substring('--variation='.length).split(';').forEach(function (setting) { + var parts = setting.split(':'); + variation[parts[0]] = parseFloat(parts[1]); + }); } } @@ -53,38 +60,147 @@ if (filename === undefined || testcase === undefined || textToRender === undefin process.exit(1); } +// getPath function from fontkit adapted to opentype.js +function getPath(points) { + const path = new opentype.Path(); + if (!points) { + return path; + } + + const contours = opentype.Glyph.prototype.getContours.call(this, points); + + for (let i = 0; i < contours.length; i++) { + let contour = contours[i]; + let firstPt = contour[0]; + let lastPt = contour[contour.length - 1]; + let start = 0; + + if (firstPt.onCurve) { + // The first point will be consumed by the moveTo command, so skip in the loop + var curvePt = null; + start = 1; + } else { + if (lastPt.onCurve) { + // Start at the last point if the first point is off curve and the last point is on curve + firstPt = lastPt; + } else { + // Start at the middle if both the first and last points are off curve + firstPt = {x: (firstPt.x + lastPt.x) / 2, y: (firstPt.y + lastPt.y) / 2, onCurve: falses}; + } + + var curvePt = firstPt; + } + + path.moveTo(firstPt.x, firstPt.y); + + for (let j = start; j < contour.length; j++) { + let pt = contour[j]; + let prevPt = j === 0 ? firstPt : contour[j - 1]; + + if (prevPt.onCurve && pt.onCurve) { + path.lineTo(pt.x, pt.y); + } else if (prevPt.onCurve && !pt.onCurve) { + var curvePt = pt; + } else if (!prevPt.onCurve && !pt.onCurve) { + let midX = (prevPt.x + pt.x) / 2; + let midY = (prevPt.y + pt.y) / 2; + path.quadraticCurveTo(prevPt.x, prevPt.y, midX, midY); + var curvePt = pt; + } else if (!prevPt.onCurve && pt.onCurve) { + path.quadraticCurveTo(curvePt.x, curvePt.y, pt.x, pt.y); + var curvePt = null; + } else { + throw new Error("Unknown TTF path state"); + } + } + + // Connect the first and last points + if (curvePt) { + path.quadraticCurveTo(curvePt.x, curvePt.y, firstPt.x, firstPt.y); + } + + path.closePath(); + } + + return path; +} + function renderSVG() { var font = opentype.loadSync(filename); let svgSymbols = []; let svgBody = []; - var glyphSet = new Set(); - let glyphData = []; + var symbolSet = new Set(); const fontSize = font.unitsPerEm; let minWidth = 0; + if(variation && font.variation) { + font.variation.set(variation); + } + + const scale = 1 / font.unitsPerEm * 1000; + font.forEachGlyph(textToRender, 0, 0, fontSize, {}, function(glyph, gX, gY, gFontSize) { - const glyphPath = glyph.getPath(gX, gY, gFontSize, {}, this); - const glyphWidth = glyph.getMetrics().xMax; - glyphData.push({glyph: glyph, path: glyphPath, gX, gY, w: glyphWidth}); - }); + let glyphName = glyph.name; + if(!glyphName || /^cid\d+$/.test(glyphName)) { + glyphName = `gid${glyph.index}`; + } + const + symbolId = testcase + '.' + glyphName; + + // TrueType paths from opentype.js are often visually the same as fontkit's, but because + // of minor differences in path construction, tests will fail. + // Therefore we re-calculate the path based on the points. + const pointsTransform = function(points) { + // Scale to 1000 + for (point of points) { + point.x *= scale; + point.y *= scale; + } + + const path = getPath(points); + return path; + } + + const pathTransform = function(path) { + // filter out lines back to the starting point of a contour + var startX = 0, startY = 0; + path.commands = path.commands.filter(function (c, i) { + path.commands[i] = c; + if (c.type === 'M') { + startX = c.x; + startY = c.y; + } else if (c.type === 'L' && (!path.commands[i + 1] || path.commands[i + 1].type === 'Z')) { + return Math.abs(c.x - startX) > 1 || Math.abs(c.y - startY) > 1; + } + + return true; + }); + + return path; + }; + + if (!symbolSet.has(symbolId)) { + const svgPath = glyph.toSVG({optimize: true, decimalPlaces: 2, flipY: false, pointsTransform, pathTransform}, font) + // normalize with other renderers + .replace(/(\d+) (-?\d+)/g, '$1,$2') + .replace(/(\d)(-)/g, '$1,$2') + .replace(/(\d)([A-Z])/g, '$1 $2') + .replace(/Z([^\s])/g, 'Z $1') + .replace(/\.\d+/g, ''); - for (let i = 0; i < glyphData.length; i++) { - const glyph = glyphData[i].glyph; - const path = glyphData[i].path; - const symbolId = testcase + '.' + glyph.name; - if (!glyphSet.has(glyph)) { - glyphSet.add(glyph); - const svgPath = glyph.path.toSVG({optimize: true, decimalPlaces: 0, flipY: false}); svgSymbols.push(` ${svgPath}`); + symbolSet.add(symbolId); } - svgBody.push(` `); - let xMax = glyphData[i].gX + glyph.advanceWidth; + svgBody.push(` `); + let xMax = gX + glyph.advanceWidth; if(xMax > minWidth) { minWidth = xMax; } - } + + const glyphWidth = glyph.getMetrics().xMax; + }); let minX = 0; let minY = Math.round(font.descender); @@ -93,7 +209,7 @@ function renderSVG() { `; + viewBox="${Math.round(minX * scale)} ${Math.round(minY * scale)} ${Math.round(minWidth * scale)} ${Math.round(height * scale)}">`; return svgHeader + svgSymbols.join('\n') + svgBody.join('\n') + SVG_FOOTER; } diff --git a/docs/font-inspector.html b/docs/font-inspector.html index e5ac87ff..c03175bf 100644 --- a/docs/font-inspector.html +++ b/docs/font-inspector.html @@ -4,6 +4,7 @@ +
@@ -102,7 +103,6 @@

Free Software

var font = null; const fontSize = 32; -const textToRender = 'Grumpy wizards make toxic brew for the evil Queen and Jack.'; const drawOptions = { kerning: true, features: [ @@ -142,6 +142,8 @@

Free Software

function renderText(font) { if (!font) return; + const textToRender = conditionalSampleText(font, 'Grumpy wizards make toxic brew for the evil Queen and Jack.'); + var previewCanvas = document.getElementById('preview'); var previewCtx = previewCanvas.getContext("2d"); previewCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height); diff --git a/docs/glyph-inspector.html b/docs/glyph-inspector.html index d020c471..401a3aec 100644 --- a/docs/glyph-inspector.html +++ b/docs/glyph-inspector.html @@ -4,6 +4,7 @@ +
@@ -32,6 +33,7 @@

Glyph Inspector


+