Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A little more metrics for glyphs #307

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)`

Expand Down
53 changes: 50 additions & 3 deletions src/TTFFont.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -166,23 +204,23 @@ export default class TTFFont {
* @type {number}
*/
get ascent() {
return this.hhea.ascent;
return this._getMetrics().ascent;
}

/**
* The font’s [descender](https://en.wikipedia.org/wiki/Descender)
* @type {number}
*/
get descent() {
return this.hhea.descent;
return this._getMetrics().descent;
}

/**
* The amount of space that should be included between lines
* @type {number}
*/
get lineGap() {
return this.hhea.lineGap;
return this._getMetrics().lineGap;
}

/**
Expand All @@ -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.
Expand Down
88 changes: 74 additions & 14 deletions src/glyph/Glyph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}

/**
Expand Down Expand Up @@ -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}
Expand All @@ -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() {
Expand Down
16 changes: 16 additions & 0 deletions src/glyph/Path.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down
10 changes: 9 additions & 1 deletion src/glyph/TTFGlyph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/tables/maxp.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/tables/post.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/tables/vhea.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
36 changes: 36 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DecodeStream, EncodeStream } from 'restructure';

export function binarySearch(arr, cmp) {
let min = 0;
let max = arr.length - 1;
Expand Down Expand Up @@ -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();