From c4a882018a80038e556e3bb4b7674f829856c0ea Mon Sep 17 00:00:00 2001 From: Vsevolod Volkov Date: Sun, 26 Mar 2023 20:50:41 +0300 Subject: [PATCH 1/8] Fix `VHEA` table structure Signed-off-by: Vsevolod Volkov --- src/tables/vhea.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tables/vhea.js b/src/tables/vhea.js index 004cdf93..c0f4ddd4 100644 --- a/src/tables/vhea.js +++ b/src/tables/vhea.js @@ -2,7 +2,8 @@ import * as r from 'restructure'; // Vertical Header Table export default new r.Struct({ - version: r.uint16, // Version number of the Vertical Header Table + majorVersion: r.uint16, // Major version number of the Vertical Header Table + minorVersion: r.uint16, // Minor version number of the Vertical Header Table ascent: r.int16, // The vertical typographic ascender for this font descent: r.int16, // The vertical typographic descender for this font lineGap: r.int16, // The vertical typographic line gap for this font From 7c7e6ac889056bb09e39b46e2950a906df3f9fc7 Mon Sep 17 00:00:00 2001 From: Vsevolod Volkov Date: Fri, 7 Apr 2023 18:34:02 +0300 Subject: [PATCH 2/8] Fix loading non-existent data from `glyf` table Signed-off-by: Vsevolod Volkov --- src/glyph/TTFGlyph.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/glyph/TTFGlyph.js b/src/glyph/TTFGlyph.js index 8dc1c75b..4a001be7 100644 --- a/src/glyph/TTFGlyph.js +++ b/src/glyph/TTFGlyph.js @@ -74,8 +74,16 @@ export default class TTFGlyph extends Glyph { return this.path.cbox; } + let glyfPos = this._font.loca.offsets[this.id]; + let nextPos = this._font.loca.offsets[this.id + 1]; + + // No data for this glyph (space?) + if (glyfPos === nextPos) { + return super._getCBox(); + } + let stream = this._font._getTableStream('glyf'); - stream.pos += this._font.loca.offsets[this.id]; + stream.pos += glyfPos; let glyph = GlyfHeader.decode(stream); let cbox = new BBox(glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax); From 6b9538a158af66758590c5d028b9d9a6669d5730 Mon Sep 17 00:00:00 2001 From: Vsevolod Volkov Date: Fri, 7 Apr 2023 18:35:25 +0300 Subject: [PATCH 3/8] Setting zero coordinates for empty glyphs (space) Signed-off-by: Vsevolod Volkov --- src/glyph/Path.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/glyph/Path.js b/src/glyph/Path.js index a119fb28..5396b088 100644 --- a/src/glyph/Path.js +++ b/src/glyph/Path.js @@ -63,6 +63,14 @@ export default class Path { } } + if (this.commands.length === 0) { + // No content, put 0 instead of Infinity + cbox.minX = 0; + cbox.minY = 0; + cbox.maxX = 0; + cbox.maxY = 0; + } + this._cbox = Object.freeze(cbox); } @@ -172,6 +180,14 @@ export default class Path { } } + if (this.commands.length === 0) { + // No content, put 0 instead of Infinity + bbox.minX = 0; + bbox.minY = 0; + bbox.maxX = 0; + bbox.maxY = 0; + } + return this._bbox = Object.freeze(bbox); } From 30acdf8424eb3b1dfc659f7a9f4bb130ddfc94dd Mon Sep 17 00:00:00 2001 From: Vsevolod Volkov Date: Mon, 10 Apr 2023 16:32:22 +0300 Subject: [PATCH 4/8] Handling `maxp`, `post, `vhea` versions as Version16Dot16 Signed-off-by: Vsevolod Volkov --- src/tables/maxp.js | 3 ++- src/tables/post.js | 3 ++- src/tables/vhea.js | 4 ++-- src/utils.js | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/tables/maxp.js b/src/tables/maxp.js index 3e211388..b85f17aa 100644 --- a/src/tables/maxp.js +++ b/src/tables/maxp.js @@ -1,8 +1,9 @@ import * as r from 'restructure'; +import { version16Dot16 } from '../utils'; // maxiumum profile export default new r.Struct({ - version: r.int32, + version: version16Dot16, numGlyphs: r.uint16, // The number of glyphs in the font maxPoints: r.uint16, // Maximum points in a non-composite glyph maxContours: r.uint16, // Maximum contours in a non-composite glyph diff --git a/src/tables/post.js b/src/tables/post.js index 04fdeabe..d03da1db 100644 --- a/src/tables/post.js +++ b/src/tables/post.js @@ -1,7 +1,8 @@ import * as r from 'restructure'; +import { version16Dot16 } from '../utils'; // PostScript information -export default new r.VersionedStruct(r.fixed32, { +export default new r.VersionedStruct(version16Dot16, { header: { // these fields exist at the top of all versions italicAngle: r.fixed32, // Italic angle in counter-clockwise degrees from the vertical. underlinePosition: r.int16, // Suggested distance of the top of the underline from the baseline diff --git a/src/tables/vhea.js b/src/tables/vhea.js index c0f4ddd4..ba6c7428 100644 --- a/src/tables/vhea.js +++ b/src/tables/vhea.js @@ -1,9 +1,9 @@ import * as r from 'restructure'; +import { version16Dot16 } from '../utils'; // Vertical Header Table export default new r.Struct({ - majorVersion: r.uint16, // Major version number of the Vertical Header Table - minorVersion: r.uint16, // Minor version number of the Vertical Header Table + version: version16Dot16, ascent: r.int16, // The vertical typographic ascender for this font descent: r.int16, // The vertical typographic descender for this font lineGap: r.int16, // The vertical typographic line gap for this font diff --git a/src/utils.js b/src/utils.js index 72db5760..2389c9d2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,3 +1,5 @@ +import { DecodeStream, EncodeStream } from 'restructure'; + export function binarySearch(arr, cmp) { let min = 0; let max = arr.length - 1; @@ -60,3 +62,37 @@ export function decodeBase64(base64) { return bytes; } + +export class Version16Dot16 { + fromBuffer(buffer) { + let stream = new DecodeStream(buffer); + return this.decode(stream); + } + + toBuffer(value) { + let size = this.size(value); + let buffer = new Uint8Array(size); + let stream = new EncodeStream(buffer); + this.encode(stream, value); + return buffer; + } + + size() { + return 4; + } + + decode(stream) { + let major = stream.readUInt16BE(); + let minor = stream.readUInt16BE() >> 12; + return major + minor / 10; + } + + encode(stream, val) { + let major = Math.trunc(val); + let minor = (val - major) * 10; + stream.writeUInt16BE(major); + return stream.writeUInt16BE(minor << 12); + } +} + +export const version16Dot16 = new Version16Dot16(); From 393f3ccccc40547bd3db9ecb7897dc7f1c9662de Mon Sep 17 00:00:00 2001 From: Vsevolod Volkov Date: Fri, 7 Apr 2023 00:33:03 +0300 Subject: [PATCH 5/8] Added respect for the "useTypoMetrics" flag Signed-off-by: Vsevolod Volkov --- README.md | 1 + src/TTFFont.js | 53 +++++++++++++++++++++++++++++++++++++++++++--- src/glyph/Glyph.js | 17 +++------------ 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 027b61ce..248bf3e5 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ The following properties describe the general metrics of the font. See [here](ht * `underlinePosition` - the offset from the normal underline position that should be used * `underlineThickness` - the weight of the underline that should be used * `italicAngle` - if this is an italic font, the angle the cursor should be drawn at to match the font design +* `lineHeight` - is the vertical space between adjacent lines (their baselines) of text, also known as leading. See [here](https://en.wikipedia.org/wiki/Leading) for more details. * `capHeight` - the height of capital letters above the baseline. See [here](http://en.wikipedia.org/wiki/Cap_height) for more details. * `xHeight`- the height of lower case letters. See [here](http://en.wikipedia.org/wiki/X-height) for more details. * `bbox` - the font’s bounding box, i.e. the box that encloses all glyphs in the font diff --git a/src/TTFFont.js b/src/TTFFont.js index 6aa0937a..08483b4f 100644 --- a/src/TTFFont.js +++ b/src/TTFFont.js @@ -91,6 +91,44 @@ export default class TTFFont { return result; } + _getMetrics() { + if (this._metrics) { return this._metrics; } + + let { 'OS/2': os2, hhea } = this; + let useTypoMetrics = os2.fsSelection.useTypoMetrics; + let ascent, descent, lineGap, lineHeight; + + // Use the same approach as FreeType + // https://gitlab.freedesktop.org/freetype/freetype/-/blob/4d8db130ea4342317581bab65fc96365ce806b77/src/sfnt/sfobjs.c#L1310 + + if (useTypoMetrics) { + ascent = os2.typoAscender; + descent = os2.typoDescender; + lineGap = os2.typoLineGap; + lineHeight = ascent - descent + lineGap; + } else { + ascent = hhea.ascent; + descent = hhea.descent; + lineGap = hhea.lineGap; + lineHeight = ascent - descent + lineGap; + } + + if (!ascent || !descent) { + if (os2.typoAscender || os2.typoDescender) { + ascent = os2.typoAscender; + descent = os2.typoDescender; + lineGap = os2.typoLineGap; + lineHeight = ascent - descent + lineGap; + } else { + ascent = os2.winAscent; + descent = -os2.winDescent; + lineHeight = ascent - descent; + } + } + + return this._metrics = {ascent, descent, lineGap, lineHeight}; + } + /** * Gets a string from the font's `name` table * `lang` is a BCP-47 language code. @@ -166,7 +204,7 @@ export default class TTFFont { * @type {number} */ get ascent() { - return this.hhea.ascent; + return this._getMetrics().ascent; } /** @@ -174,7 +212,7 @@ export default class TTFFont { * @type {number} */ get descent() { - return this.hhea.descent; + return this._getMetrics().descent; } /** @@ -182,7 +220,7 @@ export default class TTFFont { * @type {number} */ get lineGap() { - return this.hhea.lineGap; + return this._getMetrics().lineGap; } /** @@ -209,6 +247,15 @@ export default class TTFFont { return this.post.italicAngle; } + /** + * The vertical space between adjacent lines (their baselines) of text. + * See [here](https://en.wikipedia.org/wiki/Leading) for more details. + * @type {number} + */ + get lineHeight() { + return this._getMetrics().lineHeight; + } + /** * The height of capital letters above the baseline. * See [here](https://en.wikipedia.org/wiki/Cap_height) for more details. diff --git a/src/glyph/Glyph.js b/src/glyph/Glyph.js index 3592150f..15e26c0e 100644 --- a/src/glyph/Glyph.js +++ b/src/glyph/Glyph.js @@ -64,23 +64,12 @@ export default class Glyph { let {advance:advanceWidth, bearing:leftBearing} = this._getTableMetrics(this._font.hmtx); - // For vertical metrics, use vmtx if available, or fall back to global data from OS/2 or hhea + // For vertical metrics, use vmtx if available, or fall back to global data if (this._font.vmtx) { var {advance:advanceHeight, bearing:topBearing} = this._getTableMetrics(this._font.vmtx); - } else { - let os2; - if (typeof cbox === 'undefined' || cbox === null) { ({ cbox } = this); } - - if ((os2 = this._font['OS/2']) && os2.version > 0) { - var advanceHeight = Math.abs(os2.typoAscender - os2.typoDescender); - var topBearing = os2.typoAscender - cbox.maxY; - - } else { - let { hhea } = this._font; - var advanceHeight = Math.abs(hhea.ascent - hhea.descent); - var topBearing = hhea.ascent - cbox.maxY; - } + var advanceHeight = Math.abs(this._font.ascent - this._font.descent); + var topBearing = this._font.ascent - cbox.maxY; } if (this._font._variationProcessor && this._font.HVAR) { From 0575fe36a52d27f89318f6a3707e1f10bbe076bd Mon Sep 17 00:00:00 2001 From: Vsevolod Volkov Date: Mon, 27 Mar 2023 02:14:10 +0300 Subject: [PATCH 6/8] Add new metrics for glyphs (cbox's width / height and bearing) Signed-off-by: Vsevolod Volkov --- README.md | 7 +++++ src/glyph/Glyph.js | 72 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 248bf3e5..1c20f075 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,14 @@ You do not create glyph objects directly. They are created by various methods on * `path` - a vector Path object representing the glyph * `bbox` - the glyph’s bounding box, i.e. the rectangle that encloses the glyph outline as tightly as possible. * `cbox` - the glyph’s control box. This is often the same as the bounding box, but is faster to compute. Because of the way bezier curves are defined, some of the control points can be outside of the bounding box. Where `bbox` takes this into account, `cbox` does not. Thus, `cbox` is less accurate, but faster to compute. See [here](http://www.freetype.org/freetype2/docs/glyphs/glyphs-6.html#section-2) for a more detailed description. +* `width` - the glyph’s width. +* `weight` - the glyph’s height. * `advanceWidth` - the glyph’s advance width. +* `advanceHeight` - the glyph’s advance height. +* `leftBearing` - the glyph’s left side bearing. +* `topBearing` - the glyph’s top side bearing. +* `rightBearing` - the glyph’s right side bearing. +* `bottomBearing` - the glyph’s bottom side bearing. ### `glyph.render(ctx, size)` diff --git a/src/glyph/Glyph.js b/src/glyph/Glyph.js index 15e26c0e..3ec5deda 100644 --- a/src/glyph/Glyph.js +++ b/src/glyph/Glyph.js @@ -61,6 +61,7 @@ export default class Glyph { _getMetrics(cbox) { if (this._metrics) { return this._metrics; } + if (typeof cbox === 'undefined' || cbox === null) { ({ cbox } = this); } let {advance:advanceWidth, bearing:leftBearing} = this._getTableMetrics(this._font.hmtx); @@ -76,7 +77,21 @@ export default class Glyph { advanceWidth += this._font._variationProcessor.getAdvanceAdjustment(this.id, this._font.HVAR); } - return this._metrics = { advanceWidth, advanceHeight, leftBearing, topBearing }; + let width = cbox.width; + let height = cbox.height; + let rightBearing = advanceWidth - leftBearing - width; + let bottomBearing = advanceHeight - topBearing - height; + + return this._metrics = { + width, + height, + advanceWidth, + advanceHeight, + leftBearing, + topBearing, + rightBearing, + bottomBearing + }; } /** @@ -126,6 +141,24 @@ export default class Glyph { return this.path.scale(scale); } + /** + * The glyph's width. + * @type {number} + */ + @cache + get width() { + return this._getMetrics().width; + } + + /** + * The glyph's height. + * @type {number} + */ + @cache + get height() { + return this._getMetrics().height; + } + /** * The glyph's advance width. * @type {number} @@ -144,6 +177,43 @@ export default class Glyph { return this._getMetrics().advanceHeight; } + /** + * The glyph's left side bearing. + * @type {number} + */ + @cache + get leftBearing() { + return this._getMetrics().leftBearing; + } + + + /** + * The glyph's top side bearing. + * @type {number} + */ + @cache + get topBearing() { + return this._getMetrics().topBearing; + } + + /** + * The glyph's right side bearing. + * @type {number} + */ + @cache + get rightBearing() { + return this._getMetrics().rightBearing; + } + + /** + * The glyph's bottom side bearing. + * @type {number} + */ + @cache + get bottomBearing() { + return this._getMetrics().bottomBearing; + } + get ligatureCaretPositions() {} _getName() { From ac9199efc38bc6cfd931780bbcb82e7a76145e67 Mon Sep 17 00:00:00 2001 From: Vsevolod Volkov Date: Fri, 7 Apr 2023 19:57:26 +0300 Subject: [PATCH 7/8] Correcting a typo in the README: weight -> height Signed-off-by: Vsevolod Volkov --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1c20f075..6908300d 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ You do not create glyph objects directly. They are created by various methods on * `bbox` - the glyph’s bounding box, i.e. the rectangle that encloses the glyph outline as tightly as possible. * `cbox` - the glyph’s control box. This is often the same as the bounding box, but is faster to compute. Because of the way bezier curves are defined, some of the control points can be outside of the bounding box. Where `bbox` takes this into account, `cbox` does not. Thus, `cbox` is less accurate, but faster to compute. See [here](http://www.freetype.org/freetype2/docs/glyphs/glyphs-6.html#section-2) for a more detailed description. * `width` - the glyph’s width. -* `weight` - the glyph’s height. +* `height` - the glyph’s height. * `advanceWidth` - the glyph’s advance width. * `advanceHeight` - the glyph’s advance height. * `leftBearing` - the glyph’s left side bearing. From dfdde326cf14ede6f9ed4c909664f86e4435e3c9 Mon Sep 17 00:00:00 2001 From: Vsevolod Volkov Date: Tue, 19 Nov 2024 17:26:01 +0200 Subject: [PATCH 8/8] Fix undefined `cbox` in `_getMetrics` Signed-off-by: Vsevolod Volkov --- src/glyph/Glyph.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/glyph/Glyph.js b/src/glyph/Glyph.js index 3ec5deda..0f84b2b5 100644 --- a/src/glyph/Glyph.js +++ b/src/glyph/Glyph.js @@ -69,6 +69,7 @@ export default class Glyph { if (this._font.vmtx) { var {advance:advanceHeight, bearing:topBearing} = this._getTableMetrics(this._font.vmtx); } else { + if (typeof cbox === 'undefined' || cbox === null) { ({ cbox } = this); } var advanceHeight = Math.abs(this._font.ascent - this._font.descent); var topBearing = this._font.ascent - cbox.maxY; }