diff --git a/README.md b/README.md index 7d53fd01..98e8ded6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# opentype.js + # opentype.js JavaScript parser and writer for TrueType and OpenType fonts. @@ -63,10 +63,11 @@ If you plan on improving or debugging opentype.js, you can: - clone your fork `git clone git://github.com/yourname/opentype.js.git` - move into the project `cd opentype.js` - install needed dependencies with `npm install` -- **option A:** for a simple build use `npm run build` -- **option B:** for a development server use `npm run start` and navigate to the `/docs` folder -- check if all still work fine with `npm run test` -- commit and pull requesting your changes. Thanks you ! +- make your changes + - **option A:** for a simple build, use `npm run build` + - **option B:** for a development server, use `npm run start` and navigate to the `/docs` folder +- check if all still works fine with `npm run test` +- commit and open a Pull Request with your changes. Thank you! ## Usage @@ -94,7 +95,7 @@ buffer.then(data => { ### Loading a WOFF2 font WOFF2 Brotli compression perform [29% better](https://www.w3.org/TR/WOFF20ER/#appendixB) than it WOFF predecessor. -But this compression is also more complex, and would result having a much heavier opentype.js library (~120KB => ~1400KB). +But this compression is also more complex, and would result in a much heavier (>10×!) opentype.js library (≈120KB => ≈1400KB). To solve this: Decompress the font beforehand (for example with [fontello/wawoff2](https://github.com/fontello/wawoff2)). @@ -106,7 +107,7 @@ const loadScript = (src) => new Promise((onload) => document.documentElement.app const buffer = //...same as previous example... -// load wawoff2 if needed and wait (!) for it to be ready +// load wawoff2 if needed, and wait (!) for it to be ready if (!window.Module) { const path = 'https://unpkg.com/wawoff2@2.0.1/build/decompress_binding.js' const init = new Promise((done) => window.Module = { onRuntimeInitialized: done}); @@ -118,7 +119,7 @@ const font = opentype.parse(Module.decompress(await buffer)); ### Loading a font (1.x style) -This example rely on the deprecated `.load()` method +This example relies on the deprecated `.load()` method ```js // case 1: from an URL @@ -131,7 +132,7 @@ console.log(font.supported); ``` ### Writing a font -Once you have a `Font` object (either by using `opentype.load` or by creating a new one from scratch) you can write it +Once you have a `Font` object (either by using `opentype.load()` or by creating a new one from scratch) you can write it back out as a binary file. In the browser, you can use `Font.download()` to instruct the browser to download a binary .OTF file. The name is based @@ -176,39 +177,39 @@ If you want to get an `ArrayBuffer`, use `font.toArrayBuffer()`. A Font represents a loaded OpenType font file. It contains a set of glyphs and methods to draw text on a drawing context, or to get a path representing the text. * `glyphs`: an indexed list of Glyph objects. -* `unitsPerEm`: X/Y coordinates in fonts are stored as integers. This value determines the size of the grid. Common values are 2048 and 4096. +* `unitsPerEm`: X/Y coordinates in fonts are stored as integers. This value determines the size of the grid. Common values are `2048` and `4096`. * `ascender`: Distance from baseline of highest ascender. In font units, not pixels. * `descender`: Distance from baseline of lowest descender. In font units, not pixels. #### `Font.getPath(text, x, y, fontSize, options)` Create a Path that represents the given text. -* `x`: Horizontal position of the beginning of the text. (default: 0) -* `y`: Vertical position of the *baseline* of the text. (default: 0) -* `fontSize`: Size of the text in pixels (default: 72). +* `x`: Horizontal position of the beginning of the text. (default: `0`) +* `y`: Vertical position of the *baseline* of the text. (default: `0`) +* `fontSize`: Size of the text in pixels (default: `72`). Options is an optional object containing: -* `kerning`: if true takes kerning information into account (default: true) +* `kerning`: if true takes kerning information into account (default: `true`) * `features`: an object with [OpenType feature tags](https://docs.microsoft.com/en-us/typography/opentype/spec/featuretags) as keys, and a boolean value to enable each feature. -Currently only ligature features "liga" and "rlig" are supported (default: true). -* `hinting`: if true uses TrueType font hinting if available (default: false). +Currently only ligature features `"liga"` and `"rlig"` are supported (default: `true`). +* `hinting`: if true uses TrueType font hinting if available (default: `false`). -_Note: there is also `Font.getPaths` with the same arguments which returns a list of Paths._ +_**Note:** there is also `Font.getPaths()` with the same arguments, which returns a list of Paths._ #### `Font.draw(ctx, text, x, y, fontSize, options)` Create a Path that represents the given text. * `ctx`: A 2D drawing context, like Canvas. -* `x`: Horizontal position of the beginning of the text. (default: 0) -* `y`: Vertical position of the *baseline* of the text. (default: 0) -* `fontSize`: Size of the text in pixels (default: 72). +* `x`: Horizontal position of the beginning of the text. (default: `0`) +* `y`: Vertical position of the *baseline* of the text. (default: `0`) +* `fontSize`: Size of the text in pixels (default: `72`). Options is an optional object containing: -* `kerning`: if true takes kerning information into account (default: true) +* `kerning`: if `true`, takes kerning information into account (default: `true`) * `features`: an object with [OpenType feature tags](https://docs.microsoft.com/en-us/typography/opentype/spec/featuretags) as keys, and a boolean value to enable each feature. -Currently only ligature features "liga" and "rlig" are supported (default: true). -* `hinting`: if true uses TrueType font hinting if available (default: false). +Currently only ligature features `"liga"` and `"rlig"` are supported (default: `true`). +* `hinting`: if true uses TrueType font hinting if available (default: `false`). #### `Font.drawPoints(ctx, text, x, y, fontSize, options)` -Draw the points of all glyphs in the text. On-curve points will be drawn in blue, off-curve points will be drawn in red. The arguments are the same as `Font.draw`. +Draw the points of all glyphs in the text. On-curve points will be drawn in blue, off-curve points will be drawn in red. The arguments are the same as `Font.draw()`. #### `Font.drawMetrics(ctx, text, x, y, fontSize, options)` Draw lines indicating important font measurements for all glyphs in the text. @@ -222,30 +223,30 @@ Note that there is no strict 1-to-1 correspondence between the string and glyph possible substitutions such as ligatures. The list of returned glyphs can be larger or smaller than the length of the given string. #### `Font.charToGlyph(char)` -Convert the character to a `Glyph` object. Returns null if the glyph could not be found. Note that this function assumes that there is a one-to-one mapping between the given character and a glyph; for complex scripts this might not be the case. +Convert the character to a Glyph object. Returns `null` if the glyph could not be found. Note that this function assumes that there is a one-to-one mapping between the given character and a glyph; for complex scripts, this might not be the case. #### `Font.getKerningValue(leftGlyph, rightGlyph)` -Retrieve the value of the [kerning pair](https://en.wikipedia.org/wiki/Kerning) between the left glyph (or its index) and the right glyph (or its index). If no kerning pair is found, return 0. The kerning value gets added to the advance width when calculating the spacing between glyphs. +Retrieve the value of the [kerning pair](https://en.wikipedia.org/wiki/Kerning) between the left glyph (or its index) and the right glyph (or its index). If no kerning pair is found, return `0`. The kerning value gets added to the advance width when calculating the spacing between glyphs. #### `Font.getAdvanceWidth(text, fontSize, options)` Returns the advance width of a text. -This is something different than Path.getBoundingBox() as for example a +This is something different than `Path.getBoundingBox()`; for example a suffixed whitespace increases the advancewidth but not the bounding box or an overhanging letter like a calligraphic 'f' might have a quite larger bounding box than its advance width. -This corresponds to canvas2dContext.measureText(text).width -* `fontSize`: Size of the text in pixels (default: 72). -* `options`: See Font.getPath +This corresponds to `canvas2dContext.measureText(text).width` +* `fontSize`: Size of the text in pixels (default: `72`). +* `options`: See `Font.getPath()` #### 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. -* `font`: A reference to the `Font` object. -* `name`: The glyph name (e.g. "Aring", "five") +* `font`: A reference to the Font object. +* `name`: The glyph name (e.g. `"Aring"`, `"five"`) * `unicode`: The primary unicode value of this glyph (can be `undefined`). -* `unicodes`: The list of unicode values for this glyph (most of the time this will be 1, can also be empty). +* `unicodes`: The list of unicode values for this glyph (most of the time this will be `1`, can also be empty). * `index`: The index number of the glyph. * `advanceWidth`: The width to advance the pen when drawing this glyph. * `leftSideBearing`: The horizontal distance from the previous character to the origin (`0, 0`); a negative value indicates an overhang @@ -253,82 +254,84 @@ A Glyph is an individual mark that often corresponds to a character. Some glyphs * `path`: The raw, unscaled path of the glyph. ##### `Glyph.getPath(x, y, fontSize)` -Get a scaled glyph Path object we can draw on a drawing context. -* `x`: Horizontal position of the glyph. (default: 0) -* `y`: Vertical position of the *baseline* of the glyph. (default: 0) -* `fontSize`: Font size in pixels (default: 72). +Get a scaled glyph Path object for use on a drawing context. +* `x`: Horizontal position of the glyph. (default: `0`) +* `y`: Vertical position of the *baseline* of the glyph. (default: `0`) +* `fontSize`: Font size in pixels (default: `72`). ##### `Glyph.getBoundingBox()` -Calculate the minimum bounding box for the unscaled path of the given glyph. Returns an `opentype.BoundingBox` object that contains x1/y1/x2/y2. +Calculate the minimum bounding box for the unscaled path of the given glyph. Returns an `opentype.BoundingBox` object that contains `x1`/`y1`/`x2`/`y2`. If the glyph has no points (e.g. a space character), all coordinates will be zero. ##### `Glyph.draw(ctx, x, y, fontSize)` Draw the glyph on the given context. * `ctx`: The drawing context. -* `x`: Horizontal position of the glyph. (default: 0) -* `y`: Vertical position of the *baseline* of the glyph. (default: 0) -* `fontSize`: Font size, in pixels (default: 72). +* `x`: Horizontal position of the glyph. (default: `0`) +* `y`: Vertical position of the *baseline* of the glyph. (default: `0`) +* `fontSize`: Font size, in pixels (default: `72`). ##### `Glyph.drawPoints(ctx, x, y, fontSize)` Draw the points of the glyph on the given context. On-curve points will be drawn in blue, off-curve points will be drawn in red. -The arguments are the same as `Glyph.draw`. +The arguments are the same as `Glyph.draw()`. ##### `Glyph.drawMetrics(ctx, x, y, fontSize)` Draw lines indicating important font measurements for all glyphs in the text. Black lines indicate the origin of the coordinate system (point 0,0). Blue lines indicate the glyph bounding box. Green line indicates the advance width of the glyph. -The arguments are the same as `Glyph.draw`. +The arguments are the same as `Glyph.draw()`. ##### `Glyph.toPathData(options)`, `Glyph.toDOMElement(options)`, `Glyph.toSVG(options)`, `Glyph.fromSVG(pathData, options)`, These are currently only wrapper functions for their counterparts on Path objects (see documentation there), but may be extended in the future to pass on Glyph data for automatic calculation. ### The Path object -Once you have a path through `Font.getPath` or `Glyph.getPath`, you can use it. +Once you have a path through `Font.getPath()` or `Glyph.getPath()`, you can use it. * `commands`: The path commands. Each command is a dictionary containing a type and coordinates. See below for examples. -* `fill`: The fill color of the `Path`. Color is a string representing a [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). (default: 'black') -* `stroke`: The stroke color of the `Path`. Color is a string representing a [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). (default: `null`: the path will not be stroked) -* `strokeWidth`: The line thickness of the `Path`. (default: 1, but since the `stroke` is null no stroke will be drawn) +* `fill`: The fill color of the Path. Color is a string representing a [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). (default: `'black'`) +* `stroke`: The stroke color of the `Path`. Color is a string representing a [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). (default: `null`; the path will not be stroked) +* `strokeWidth`: The line thickness of the `Path`. (default: `1`, but if `stroke` is `null` no stroke will be drawn) ##### `Path.draw(ctx)` -Draw the path on the given 2D context. This uses the `fill`, `stroke` and `strokeWidth` properties of the `Path` object. +Draw the path on the given 2D context. This uses the `fill`, `stroke`, and `strokeWidth` properties of the Path object. * `ctx`: The drawing context. ##### `Path.getBoundingBox()` -Calculate the minimum bounding box for the given path. Returns an `opentype.BoundingBox` object that contains x1/y1/x2/y2. +Calculate the minimum bounding box for the given path. Returns an `opentype.BoundingBox` object that contains `x1`/`y1`/`x2`/`y2`. If the path is empty (e.g. a space character), all coordinates will be zero. ##### `Path.toPathData(options)` Convert the Path to a string of path data instructions. See https://www.w3.org/TR/SVG/paths.html#PathData * `options`: - * `decimalPlaces`: The amount of decimal places for floating-point values. (default: 2) - * `optimize`: apply some optimizations to the path data, e.g. removing unnecessary/duplicate commands (true/false, default: true) - * `flipY`: whether to flip the Y axis of the path data, because SVG and font paths use inverted Y axes. (true: calculate from bounding box, false: disable; default: true) + * `decimalPlaces`: The amount of decimal places for floating-point values. (default: `2`) + * `optimize`: apply some optimizations to the path data, e.g. removing unnecessary/duplicate commands (true/false, default: `true`) + * `flipY`: whether to flip the Y axis of the path data, because SVG and font paths use inverted Y axes. (`true`: calculate from bounding box, `false`: disable; default: `true`) * `flipYBase`: Base value for the base flipping calculation. You'll probably want to calculate this from the font's ascender and descender values. (default: automatically calculate from the path data's bounding box) ##### `Path.toSVG(options)` -Convert the path to a SVG <path> element, as a string. -* `options`: see Path.toPathData +Convert the path to an SVG `` element, as a string. +* `options`: see `Path.toPathData()` ##### `Path.fromSVG(pathData, options)` -Retrieve path from SVG path data. Either overwriting the path data for an existing path +Retrieve path from SVG path data. + +Either overwriting the path data for an existing path: ```js const path = new Path(); path.fromSVG('M0 0'); ``` -or creating a new Path directly: +Or creating a new Path directly: ```js const path = Path.fromSVG('M0 0'); ``` * `pathData`: Either a string of SVG path commands, or (only in browser context) an `SVGPathElement` * `options`: - * `decimalPlaces`, `optimize`, `flipY`, `flipYBase`: see Path.toPathData - * `scale`: scaling value applied to all command coordinates (default: 1) - * `x`/`y`: offset applied to all command coordinates on the x or y axis (default: 0) + * `decimalPlaces`, `optimize`, `flipY`, `flipYBase`: see `Path.toPathData()` + * `scale`: scaling value applied to all command coordinates (default: `1`) + * `x`/`y`: offset applied to all command coordinates on the x or y axis (default: `0`) #### Path commands * **Move To**: Move to a new position. This creates a new contour. Example: `{type: 'M', x: 100, y: 200}` diff --git a/src/glyph.js b/src/glyph.js index b48d067b..f87e0400 100644 --- a/src/glyph.js +++ b/src/glyph.js @@ -74,7 +74,7 @@ Glyph.prototype.bindConstructorValues = function(options) { // These three values cannot be deferred for memory optimization: this.name = options.name || null; this.unicode = options.unicode; - this.unicodes = options.unicodes || options.unicode !== undefined ? [options.unicode] : []; + this.unicodes = options.unicodes || (options.unicode !== undefined ? [options.unicode] : []); // But by binding these values only when necessary, we reduce can // the memory requirements by almost 3% for larger fonts. diff --git a/src/opentype.js b/src/opentype.js index 56f49fab..3d14464f 100644 --- a/src/opentype.js +++ b/src/opentype.js @@ -33,6 +33,7 @@ import _name from './tables/name.js'; import os2 from './tables/os2.js'; import post from './tables/post.js'; import meta from './tables/meta.js'; +import gasp from './tables/gasp.js'; /** * The opentype library. * @namespace opentype @@ -338,6 +339,10 @@ function parseBuffer(buffer, opt={}) { case 'meta': metaTableEntry = tableEntry; break; + case 'gasp': + table = uncompressTable(data, tableEntry); + font.tables.gasp = gasp.parse(table.data, table.offset); + break; } } diff --git a/src/tables/gasp.js b/src/tables/gasp.js new file mode 100644 index 00000000..e0ffdcc9 --- /dev/null +++ b/src/tables/gasp.js @@ -0,0 +1,46 @@ +// The `gasp` table contains global information about the font. +// https://learn.microsoft.com/de-de/typography/opentype/spec/gasp + +import check from '../check.js'; +import parse from '../parse.js'; +import table from '../table.js'; + +//const GASP_SYMMETRIC_GRIDFIT = 0x0004 +//const GASP_SYMMETRIC_SMOOTHING = 0x0008 +//const GASP_DOGRAY = 0x0002 +//const GASP_GRIDFIT = 0x0001 + +// Parse the `gasp` table +function parseGaspTable(data, start) { + const gasp = {}; + const p = new parse.Parser(data, start); + gasp.version = p.parseUShort(); + check.argument(gasp.version <= 0x0001, 'Unsupported gasp table version.'); + gasp.numRanges = p.parseUShort(); + gasp.gaspRanges = []; + for (let i = 0; i < gasp.numRanges; i++) { + gasp.gaspRanges[i] = { + rangeMaxPPEM: p.parseUShort(), + rangeGaspBehavior: p.parseUShort(), + }; + } + return gasp; +} + + +function makeGaspTable(gasp) { + const result = new table.Table('gasp', [ + {name: 'version', type: 'USHORT', value: 0x0001}, + {name: 'numRanges', type: 'USHORT', value: gasp.numRanges}, + ]); + + for (let i in gasp.numRanges) { + result.fields.push({name: 'rangeMaxPPEM', type: 'USHORT', value: gasp.numRanges[i].rangeMaxPPEM}); + result.fields.push({name: 'rangeGaspBehavior', type: 'USHORT', value: gasp.numRanges[i].rangeGaspBehavior}); + } + + return result; +} + +export default { parse: parseGaspTable, make: makeGaspTable }; + diff --git a/src/tables/sfnt.js b/src/tables/sfnt.js index 41171291..d0f86dca 100644 --- a/src/tables/sfnt.js +++ b/src/tables/sfnt.js @@ -24,6 +24,7 @@ import cpal from './cpal.js'; import fvar from './fvar.js'; import stat from './stat.js'; import avar from './avar.js'; +import gasp from './gasp.js'; function log2(v) { return Math.log(v) / Math.log(2) | 0; @@ -318,6 +319,7 @@ function fontToSfntTable(font) { // we have to handle fvar before name, because it may modify name IDs const fvarTable = font.tables.fvar ? fvar.make(font.tables.fvar, font.names) : undefined; + const gaspTable = font.tables.gasp ? gasp.make(font.tables.gasp) : undefined; const languageTags = []; const nameTable = _name.make(names, languageTags); @@ -371,6 +373,10 @@ function fontToSfntTable(font) { tables.push(metaTable); } + if (gaspTable) { + tables.push(gaspTable); + } + const sfntTable = makeSfntTable(tables); // Compute the font's checkSum and store it in head.checkSumAdjustment. diff --git a/test/glyph.js b/test/glyph.js index a6e60e32..6bce6493 100644 --- a/test/glyph.js +++ b/test/glyph.js @@ -75,6 +75,7 @@ describe('glyph.js', function() { let glyph = new Glyph({ name: 'Test Glyph', unicode: 65, + unicodes: [65, 66], path: new Path(), advanceWidth: 400, leftSideBearing: -100 @@ -85,7 +86,7 @@ describe('glyph.js', function() { assert.equal(glyph.unicode, 65); assert.equal(glyph.advanceWidth, 400); assert.equal(glyph.leftSideBearing, -100); - assert.deepEqual(glyph.unicodes, [65]); + assert.deepEqual(glyph.unicodes, [65, 66]); }); }); diff --git a/test/tables/gasp.js b/test/tables/gasp.js new file mode 100644 index 00000000..f2aec720 --- /dev/null +++ b/test/tables/gasp.js @@ -0,0 +1,32 @@ +import assert from 'assert'; +import { Font, Path, Glyph, parse, load} from '../../src/opentype.js'; +import { readFileSync } from 'fs'; +const loadSync = (url, opt) => parse(readFileSync(url), opt); + +describe('tables/gasp.js', function () { + const font = loadSync('./test/fonts/Roboto-Black.ttf'); + + it('can parse gasp table version', function() { + assert.equal(font.tables.gasp.version, 1); + }); + it('can parse gasp table numRanges', function() { + assert.equal(font.tables.gasp.numRanges, 2); + }); + + it('can parse gasp table numRanges 0 rangeMaxPPEM', function() { + assert.equal(font.tables.gasp.gaspRanges[0].rangeMaxPPEM, 8); // default value + }); + + it('can parse gasp table numRanges 0 rangeGaspBehavior', function() { + assert.equal(font.tables.gasp.gaspRanges[0].rangeGaspBehavior, 0x0002); //GASP_DOGRAY = 0x0002 + }); + + it('can parse gasp table numRanges 1 rangeMaxPPEM', function() { + assert.equal(font.tables.gasp.gaspRanges[1].rangeMaxPPEM, 0xFFFF); // default value + }); + + it('can parse gasp table numRanges 1 rangeGaspBehavior', function() { + assert.equal(font.tables.gasp.gaspRanges[1].rangeGaspBehavior, 0x0001 + 0x0002 + 0x0004 + 0x0008); // all flags set = 15 + }); + +});