diff --git a/README.md b/README.md index 027b61ce..6908300d 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 @@ -187,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. +* `height` - 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/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..0f84b2b5 100644 --- a/src/glyph/Glyph.js +++ b/src/glyph/Glyph.js @@ -61,33 +61,38 @@ 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); - // 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) { 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 + }; } /** @@ -137,6 +142,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} @@ -155,6 +178,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() { 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); } 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); 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 004cdf93..ba6c7428 100644 --- a/src/tables/vhea.js +++ b/src/tables/vhea.js @@ -1,8 +1,9 @@ import * as r from 'restructure'; +import { version16Dot16 } from '../utils'; // Vertical Header Table export default new r.Struct({ - version: r.uint16, // 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();