diff --git a/README.md b/README.md index bc613ebe..5a8dae3a 100644 --- a/README.md +++ b/README.md @@ -180,14 +180,14 @@ Fontkit includes several methods for accessing glyph metrics and performing layo Returns the advance width (described above) for a single glyph id. -#### `font.layout(string, features = [])` +#### `font.layout(string, features = [] | {})` This method returns a `GlyphRun` object, which includes an array of `Glyph`s and `GlyphPosition`s for the given string. `Glyph` objects are described below. `GlyphPosition` objects include 4 properties: `xAdvance`, `yAdvance`, `xOffset`, and `yOffset`. -The `features` parameter is an array of [OpenType feature tags](https://www.microsoft.com/typography/otspec/featuretags.htm) to be applied -in addition to the default set. If this is an AAT font, the OpenType feature tags are mapped to AAT features. +The `features` parameter is either an array of [OpenType feature tags](https://www.microsoft.com/typography/otspec/featuretags.htm) to be applied +in addition to the default set, or an object mapping OpenType features to a boolean enabling or disabling each. If this is an AAT font, the OpenType feature tags are mapped to AAT features. ### Variation fonts diff --git a/package.json b/package.json index a93e43c6..8d9c2c43 100644 --- a/package.json +++ b/package.json @@ -34,14 +34,14 @@ "babel-plugin-istanbul": "^4.1.3", "base64-arraybuffer": "^0.1.5", "buffer": "^5.6.0", - "clone": "^1.0.1", + "clone": "^1.0.4", "codepoints": "^1.2.0", - "concat-stream": "^1.4.6", + "concat-stream": "^1.6.2", "deep-equal": "^1.0.0", - "dfa": "^1.0.0", + "dfa": "^1.2.0", "esdoc": "^0.4.8", "esdoc-es7-plugin": "0.0.3", - "iconv-lite": "^0.4.13", + "iconv-lite": "^0.4.24", "mocha": "^2.0.1", "nyc": "^10.3.2", "rollup": "^2.10.2", diff --git a/src/TTFFont.js b/src/TTFFont.js index 939d96fa..209e4d22 100644 --- a/src/TTFFont.js +++ b/src/TTFFont.js @@ -25,6 +25,7 @@ export default class TTFFont { } constructor(stream, variationCoords = null) { + this.defaultLanguage = null; this.stream = stream; this.variationCoords = variationCoords; @@ -44,6 +45,10 @@ export default class TTFFont { } } + setDefaultLanguage(lang = null) { + this.defaultLanguage = lang; + } + _getTable(table) { if (!(table.tag in this._tables)) { try { @@ -83,34 +88,36 @@ export default class TTFFont { return result; } - /** - * The unique PostScript name for this font - * @type {string} - */ - get postscriptName() { - let name = this.name.records.postscriptName; - if (name) { - let lang = Object.keys(name)[0]; - return name[lang]; - } - - return null; - } - /** * Gets a string from the font's `name` table * `lang` is a BCP-47 language code. * @return {string} */ - getName(key, lang = 'en') { - let record = this.name.records[key]; + getName(key, lang = this.defaultLanguage || fontkit.defaultLanguage) { + let record = this.name && this.name.records[key]; if (record) { - return record[lang]; + // Attempt to retrieve the entry, depending on which translation is available: + return ( + record[lang] + || record[this.defaultLanguage] + || record[fontkit.defaultLanguage] + || record['en'] + || record[Object.keys(record)[0]] // Seriously, ANY language would be fine + || null + ); } return null; } + /** + * The unique PostScript name for this font, e.g. "Helvetica-Bold" + * @type {string} + */ + get postscriptName() { + return this.getName('postscriptName'); + } + /** * The font's full name, e.g. "Helvetica Bold" * @type {string} diff --git a/src/aat/AATMorxProcessor.js b/src/aat/AATMorxProcessor.js index 2fb3d258..94744e1a 100644 --- a/src/aat/AATMorxProcessor.js +++ b/src/aat/AATMorxProcessor.js @@ -44,7 +44,7 @@ export default class AATMorxProcessor { } // Processes an array of glyphs and applies the specified features - // Features should be in the form of {featureType:{featureSetting:true}} + // Features should be in the form of {featureType:{featureSetting:boolean}} process(glyphs, features = {}) { for (let chain of this.morx.chains) { let flags = chain.defaultFlags; @@ -52,9 +52,14 @@ export default class AATMorxProcessor { // enable/disable the requested features for (let feature of chain.features) { let f; - if ((f = features[feature.featureType]) && f[feature.featureSetting]) { - flags &= feature.disableFlags; - flags |= feature.enableFlags; + if (f = features[feature.featureType]) { + if (f[feature.featureSetting]) { + flags &= feature.disableFlags; + flags |= feature.enableFlags; + } else if (f[feature.featureSetting] === false) { + flags |= ~feature.disableFlags; + flags &= ~feature.enableFlags; + } } } diff --git a/src/base.js b/src/base.js index 4790b181..7e382b3f 100644 --- a/src/base.js +++ b/src/base.js @@ -25,4 +25,10 @@ const fontkit = { }, }; +fontkit.defaultLanguage = 'en'; +fontkit.setDefaultLanguage = function(lang = 'en') { + fontkit.defaultLanguage = lang; +}; + export default fontkit; + diff --git a/src/cff/CFFFont.js b/src/cff/CFFFont.js index fc54ecf7..0761ca26 100644 --- a/src/cff/CFFFont.js +++ b/src/cff/CFFFont.js @@ -127,7 +127,7 @@ class CFFFont { if (gid < ranges[mid].first) { high = mid - 1; - } else if (mid < high && gid > ranges[mid + 1].first) { + } else if (mid < high && gid >= ranges[mid + 1].first) { low = mid + 1; } else { return ranges[mid].fd; diff --git a/src/cff/CFFTop.js b/src/cff/CFFTop.js index 461063fb..24d4b5dc 100644 --- a/src/cff/CFFTop.js +++ b/src/cff/CFFTop.js @@ -156,7 +156,9 @@ class CFFPrivateOp { let FontDict = new CFFDict([ // key name type(s) default [18, 'Private', new CFFPrivateOp, null], - [[12, 38], 'FontName', 'sid', null] + [[12, 38], 'FontName', 'sid', null], + [[12, 7], 'FontMatrix', 'array', [0.001, 0, 0, 0.001, 0, 0]], + [[12, 5], 'PaintType', 'number', 0], ]); let CFFTopDict = new CFFDict([ diff --git a/src/glyph/CFFGlyph.js b/src/glyph/CFFGlyph.js index a9eb8a62..2b83bc49 100644 --- a/src/glyph/CFFGlyph.js +++ b/src/glyph/CFFGlyph.js @@ -24,10 +24,8 @@ export default class CFFGlyph extends Glyph { } _getPath() { - let { stream } = this._font; - let { pos } = stream; - let cff = this._font.CFF2 || this._font['CFF ']; + let { stream } = cff; let str = cff.topDict.CharStrings[this.id]; let end = str.offset + str.length; stream.pos = str.offset; @@ -49,7 +47,7 @@ export default class CFFGlyph extends Glyph { let gsubrs = cff.globalSubrIndex || []; let gsubrsBias = this.bias(gsubrs); - let privateDict = cff.privateDictForGlyph(this.id); + let privateDict = cff.privateDictForGlyph(this.id) || {}; let subrs = privateDict.Subrs || []; let subrsBias = this.bias(subrs); diff --git a/src/glyph/Path.js b/src/glyph/Path.js index d3ffdff2..236bc92d 100644 --- a/src/glyph/Path.js +++ b/src/glyph/Path.js @@ -27,7 +27,11 @@ export default class Path { * @return {string} */ toFunction() { - return (ctx) => this.commands.forEach((c) => ctx[c.command].apply(ctx, c.args)); + return ctx => { + this.commands.forEach(c => { + return ctx[c.command].apply(ctx, c.args) + }) + }; } /** diff --git a/src/opentype/GSUBProcessor.js b/src/opentype/GSUBProcessor.js index bea6b473..f5672d5d 100644 --- a/src/opentype/GSUBProcessor.js +++ b/src/opentype/GSUBProcessor.js @@ -28,6 +28,14 @@ export default class GSUBProcessor extends OTProcessor { let index = this.coverageIndex(table.coverage); if (index !== -1) { let sequence = table.sequences.get(index); + + if (sequence.length === 0) { + // If the sequence length is zero, delete the glyph. + // The OpenType spec disallows this, but seems like Harfbuzz and Uniscribe allow it. + this.glyphs.splice(this.glyphIterator.index, 1); + return true; + } + this.glyphIterator.cur.id = sequence[0]; this.glyphIterator.cur.ligatureComponent = 0; diff --git a/src/subset/CFFSubset.js b/src/subset/CFFSubset.js index 6b1da1ed..fdce7e61 100644 --- a/src/subset/CFFSubset.js +++ b/src/subset/CFFSubset.js @@ -156,7 +156,7 @@ export default class CFFSubset extends Subset { let top = { version: 1, hdrSize: this.cff.hdrSize, - offSize: this.cff.length, + offSize: 4, header: this.cff.header, nameIndex: [this.cff.postscriptName], topDictIndex: [topDict], diff --git a/src/subset/Subset.js b/src/subset/Subset.js index 069240e5..bfeb23c6 100644 --- a/src/subset/Subset.js +++ b/src/subset/Subset.js @@ -1,5 +1,7 @@ import r from '@pdf-lib/restructure'; +const resolved = Promise.resolve(); + export default class Subset { constructor(font) { this.font = font; @@ -26,7 +28,7 @@ export default class Subset { encodeStream() { let s = new r.EncodeStream(); - process.nextTick(() => { + resolved.then(() => { this.encode(s); return s.end(); }); diff --git a/src/subset/TTFSubset.js b/src/subset/TTFSubset.js index d6b26f62..b0a4b2dd 100644 --- a/src/subset/TTFSubset.js +++ b/src/subset/TTFSubset.js @@ -57,7 +57,8 @@ export default class TTFSubset extends Subset { this.glyf = []; this.offset = 0; this.loca = { - offsets: [] + offsets: [], + version: this.font.loca.version }; this.hmtx = { @@ -77,7 +78,6 @@ export default class TTFSubset extends Subset { maxp.numGlyphs = this.glyf.length; this.loca.offsets.push(this.offset); - Tables.loca.preEncode.call(this.loca); let head = cloneDeep(this.font.head); head.indexToLocFormat = this.loca.version; diff --git a/src/tables/loca.js b/src/tables/loca.js index 2f70154b..fc598cf1 100644 --- a/src/tables/loca.js +++ b/src/tables/loca.js @@ -18,11 +18,6 @@ loca.process = function() { }; loca.preEncode = function() { - if (this.version != null) return; - - // assume this.offsets is a sorted array - this.version = this.offsets[this.offsets.length - 1] > 0xffff ? 1 : 0; - if (this.version === 0) { for (let i = 0; i < this.offsets.length; i++) { this.offsets[i] >>>= 1; diff --git a/test/data/FiraSans/FiraSans-Regular.ttf b/test/data/FiraSans/FiraSans-Regular.ttf new file mode 100644 index 00000000..7544de9c Binary files /dev/null and b/test/data/FiraSans/FiraSans-Regular.ttf differ diff --git a/test/data/FiraSans/OFL.txt b/test/data/FiraSans/OFL.txt new file mode 100644 index 00000000..a2c1ae22 --- /dev/null +++ b/test/data/FiraSans/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/test/data/SourceSansPro/SourceSansPro-Regular.otf.woff b/test/data/SourceSansPro/SourceSansPro-Regular.otf.woff new file mode 100644 index 00000000..94659aeb Binary files /dev/null and b/test/data/SourceSansPro/SourceSansPro-Regular.otf.woff differ diff --git a/test/data/SourceSansPro/SourceSansPro-Regular.otf.woff2 b/test/data/SourceSansPro/SourceSansPro-Regular.otf.woff2 new file mode 100644 index 00000000..e002972a Binary files /dev/null and b/test/data/SourceSansPro/SourceSansPro-Regular.otf.woff2 differ diff --git a/test/data/SourceSansPro/SourceSansPro-Regular.ttf.woff b/test/data/SourceSansPro/SourceSansPro-Regular.ttf.woff new file mode 100644 index 00000000..5945501e Binary files /dev/null and b/test/data/SourceSansPro/SourceSansPro-Regular.ttf.woff differ diff --git a/test/data/SourceSansPro/SourceSansPro-Regular.ttf.woff2 b/test/data/SourceSansPro/SourceSansPro-Regular.ttf.woff2 new file mode 100644 index 00000000..df1d2115 Binary files /dev/null and b/test/data/SourceSansPro/SourceSansPro-Regular.ttf.woff2 differ diff --git a/test/glyph_mapping.js b/test/glyph_mapping.js index 8b80ed80..2e7e0ded 100644 --- a/test/glyph_mapping.js +++ b/test/glyph_mapping.js @@ -85,6 +85,13 @@ describe('character to glyph mapping', function() { return assert.deepEqual(glyphs.map(g => g.codePoints), [[102, 102, 105], [32], [49], [8260], [50]]); }); + it('should allow for disabling of default AAT morx features', function() { + let {glyphs} = font.layout('ffi 1⁄2', { 'liga': false }); + assert.equal(glyphs.length, 7); + assert.deepEqual(glyphs.map(g => g.id), [73, 73, 76, 3, 20, 645, 21]); + return assert.deepEqual(glyphs.map(g => g.codePoints), [[102], [102], [105], [32], [49], [8260], [50]]); + }); + it('should apply user specified features', function() { let {glyphs} = font.layout('ffi 1⁄2', [ 'numr' ]); assert.equal(glyphs.length, 3); diff --git a/test/glyphs.js b/test/glyphs.js index 8e25772f..aba6d092 100644 --- a/test/glyphs.js +++ b/test/glyphs.js @@ -13,11 +13,28 @@ describe('glyphs', function() { return assert.equal(glyph.constructor.name, 'TTFGlyph'); }); - it('should get a path for the glyph', function() { + it('should get a path for the glyph as SVG', function() { let glyph = font.getGlyph(39); return assert.equal(glyph.path.toSVG(), 'M1368 745Q1368 383 1171.5 191.5Q975 0 606 0L201 0L201 1462L649 1462Q990 1462 1179 1273Q1368 1084 1368 745ZM1188 739Q1188 1025 1044.5 1170Q901 1315 618 1315L371 1315L371 147L578 147Q882 147 1035 296.5Q1188 446 1188 739Z'); }); + it('should get a path for the glyph as function', function() { + let glyph = font.getGlyph(39); + let results = []; + let ctx = { + moveTo: (...args) => results.push('moveTo(' + args.join(', ') + ');'), + lineTo: (...args) => results.push('lineTo(' + args.join(', ') + ');'), + quadraticCurveTo: (...args) => results.push('quadraticCurveTo(' + args.join(', ') + ');'), + bezierCurveTo: (...args) => results.push('bezierCurveTo(' + args.join(', ') + ');'), + closePath: (...args) => results.push('closePath(' + args.join(', ') + ');') + }; + + let fn = glyph.path.toFunction(); + fn(ctx); + + return assert.equal(results.join('\n'), 'moveTo(1368, 745);\nquadraticCurveTo(1368, 383, 1171.5, 191.5);\nquadraticCurveTo(975, 0, 606, 0);\nlineTo(201, 0);\nlineTo(201, 1462);\nlineTo(649, 1462);\nquadraticCurveTo(990, 1462, 1179, 1273);\nquadraticCurveTo(1368, 1084, 1368, 745);\nclosePath();\nmoveTo(1188, 739);\nquadraticCurveTo(1188, 1025, 1044.5, 1170);\nquadraticCurveTo(901, 1315, 618, 1315);\nlineTo(371, 1315);\nlineTo(371, 147);\nlineTo(578, 147);\nquadraticCurveTo(882, 147, 1035, 296.5);\nquadraticCurveTo(1188, 446, 1188, 739);\nclosePath();'); + }); + it('should get a composite glyph', function() { let glyph = font.getGlyph(171); // é return assert.equal(glyph.path.toSVG(), 'M639 -20Q396 -20 255.5 128Q115 276 115 539Q115 804 245.5 960Q376 1116 596 1116Q802 1116 922 980.5Q1042 845 1042 623L1042 518L287 518Q292 325 384.5 225Q477 125 645 125Q822 125 995 199L995 51Q907 13 828.5 -3.5Q750 -20 639 -20ZM594 977Q462 977 383.5 891Q305 805 291 653L864 653Q864 810 794 893.5Q724 977 594 977ZM471 1266Q519 1328 574.5 1416Q630 1504 662 1569L864 1569L864 1548Q820 1483 733 1388Q646 1293 582 1241L471 1241Z'); @@ -72,11 +89,28 @@ describe('glyphs', function() { return assert.equal(glyph.constructor.name, 'CFFGlyph'); }); - it('should get a path for the glyph', function() { + it('should get a path for the glyph as SVG', function() { let glyph = font.getGlyph(5); return assert.equal(glyph.path.toSVG(), 'M90 0L258 0C456 0 564 122 564 331C564 539 456 656 254 656L90 656ZM173 68L173 588L248 588C401 588 478 496 478 331C478 165 401 68 248 68Z'); }); + it('should get a path for the glyph as function', function() { + let glyph = font.getGlyph(5); + let results = []; + let ctx = { + moveTo: (...args) => results.push('moveTo(' + args.join(', ') + ');'), + lineTo: (...args) => results.push('lineTo(' + args.join(', ') + ');'), + quadraticCurveTo: (...args) => results.push('quadraticCurveTo(' + args.join(', ') + ');'), + bezierCurveTo: (...args) => results.push('bezierCurveTo(' + args.join(', ') + ');'), + closePath: (...args) => results.push('closePath(' + args.join(', ') + ');') + }; + + let fn = glyph.path.toFunction(); + fn(ctx); + + return assert.equal(results.join('\n'), 'moveTo(90, 0);\nlineTo(258, 0);\nbezierCurveTo(456, 0, 564, 122, 564, 331);\nbezierCurveTo(564, 539, 456, 656, 254, 656);\nlineTo(90, 656);\nclosePath();\nmoveTo(173, 68);\nlineTo(173, 588);\nlineTo(248, 588);\nbezierCurveTo(401, 588, 478, 496, 478, 331);\nbezierCurveTo(478, 165, 401, 68, 248, 68);\nclosePath();'); + }); + it('should get the glyph cbox', function() { let glyph = font.getGlyph(5); return assert.deepEqual(glyph.cbox, new BBox(90, 0, 564, 656)); @@ -93,6 +127,49 @@ describe('glyphs', function() { }); }); + describe('CFF glyphs (CID font)', function() { + let font = fontkit.openSync(__dirname + '/data/NotoSansCJK/NotoSansCJKkr-Regular.otf'); + + it('should get a CFFGlyph', function() { + let glyph = font.getGlyph(27); // : + return assert.equal(glyph.constructor.name, 'CFFGlyph'); + }); + + it('should get a path for the glyph', function() { + let glyph = font.getGlyph(27); + return assert.equal(glyph.path.toSVG(), 'M139 390C175 390 205 419 205 459C205 501 175 530 139 530C103 530 73 501 73 459C73 419 103 390 139 390ZM139 -13C175 -13 205 15 205 56C205 97 175 127 139 127C103 127 73 97 73 56C73 15 103 -13 139 -13Z'); + }); + + it('should get the glyph cbox', function() { + let glyph = font.getGlyph(27); + return assert.deepEqual(glyph.cbox, new BBox(73, -13, 205, 530)); + }); + + it('should get the glyph bbox', function() { + let glyph = font.getGlyph(27); + return assert.deepEqual(glyph.bbox, new BBox(73, -13, 205, 530)); + }); + + it('should get the correct fd index', function() { + let cff = font['CFF ']; + // FDSelect ranges + // {first: 0, fd: 5 } + // {first: 1, fd: 15 } + // {first: 17, fd: 17 } + // {first: 27, fd: 15 } + // {first: 102, fd: 3 } + assert.equal(cff.fdForGlyph(0), 5); + assert.equal(cff.fdForGlyph(1), 15); + assert.equal(cff.fdForGlyph(10), 15); + assert.equal(cff.fdForGlyph(16), 15); + assert.equal(cff.fdForGlyph(17), 17); + assert.equal(cff.fdForGlyph(26), 17); + assert.equal(cff.fdForGlyph(27), 15); + assert.equal(cff.fdForGlyph(28), 15); + assert.equal(cff.fdForGlyph(102), 3); + }); + }); + describe('SBIX glyphs', function() { let font = fontkit.openSync(__dirname + '/data/ss-emoji/ss-emoji-apple.ttf'); @@ -156,56 +233,101 @@ describe('glyphs', function() { }); }); - describe('WOFF glyphs', function() { - let font = fontkit.openSync(__dirname + '/data/SourceSansPro/SourceSansPro-Regular.woff'); + describe('WOFF ttf glyphs', function() { + let font = fontkit.openSync(__dirname + '/data/SourceSansPro/SourceSansPro-Regular.ttf.woff'); + let glyph = font.glyphsForString('D')[0]; + + it('should get the glyph name', function() { + return assert.equal(glyph.name, 'D'); + }); it('should get a TTFGlyph', function() { - let glyph = font.glyphsForString('T')[0]; return assert.equal(glyph.constructor.name, 'TTFGlyph'); }); - it('should get a path for the glyph', function() { - let glyph = font.glyphsForString('T')[0]; - return assert.equal(glyph.path.toSVG(), 'M226 586L28 586L28 656L508 656L508 586L310 586L310 0L226 0Z'); + it('should get a quadratic path for the glyph', function() { + return assert.equal(glyph.path.toSVG(), 'M90 0L90 656L254 656Q406 656 485 571.5Q564 487 564 331Q564 174 485.5 87Q407 0 258 0ZM173 68L248 68Q363 68 420.5 137.5Q478 207 478 331Q478 455 420.5 521.5Q363 588 248 588L173 588Z'); }); + }); + + describe('WOFF otf glyphs', function() { + let font = fontkit.openSync(__dirname + '/data/SourceSansPro/SourceSansPro-Regular.otf.woff'); + let glyph = font.glyphsForString('D')[0]; it('should get the glyph name', function() { - let glyph = font.glyphsForString('T')[0]; - return assert.equal(glyph.name, 'T'); + return assert.equal(glyph.name, 'D'); + }); + + it('should get a CFFGlyph', function() { + return assert.equal(glyph.constructor.name, 'CFFGlyph'); + }); + + it('should get a cubic path for the glyph', function() { + return assert.equal(glyph.path.toSVG(), 'M90 0L258 0C456 0 564 122 564 331C564 539 456 656 254 656L90 656ZM173 68L173 588L248 588C401 588 478 496 478 331C478 165 401 68 248 68Z'); }); }); - describe('WOFF2 glyph', function() { - let font = fontkit.openSync(__dirname + '/data/SourceSansPro/SourceSansPro-Regular.woff2'); + describe('WOFF2 ttf glyph', function() { + let font = fontkit.openSync(__dirname + '/data/SourceSansPro/SourceSansPro-Regular.ttf.woff2'); + + let glyph = font.glyphsForString('D')[0]; + let expectedBox = new BBox(90, 0, 564, 656); + + it('should get the glyph name', function() { + return assert.equal(glyph.name, 'D'); + }); it('should get a WOFF2Glyph', function() { - let glyph = font.glyphsForString('T')[0]; return assert.equal(glyph.constructor.name, 'WOFF2Glyph'); }); it('should get a path for the glyph', function() { - let glyph = font.glyphsForString('T')[0]; - return assert.equal(glyph.path.toSVG(), 'M226 586L28 586L28 656L508 656L508 586L310 586L310 0L226 0Z'); + let tglyph = font.glyphsForString('T')[0]; + return assert.equal(tglyph.path.toSVG(), 'M226 0L226 586L28 586L28 656L508 656L508 586L310 586L310 0Z'); }); - it('should get a correct path for all contours', function() { - let glyph = font.glyphsForString('o')[0]; - return assert.equal(glyph.path.toSVG(), 'M271 -12Q226 -12 185.5 5Q145 22 114 54.5Q83 87 64.5 134.5Q46 182 46 242Q46 303 64.5 350.5Q83 398 114 431Q145 464 185.5 481Q226 498 271 498Q316 498 356.5 481Q397 464 428 431Q459 398 477.5 350.5Q496 303 496 242Q496 182 477.5 134.5Q459 87 428 54.5Q397 22 356.5 5Q316 -12 271 -12ZM271 56Q302 56 328 69.5Q354 83 372.5 107.5Q391 132 401 166Q411 200 411 242Q411 284 401 318.5Q391 353 372.5 378Q354 403 328 416.5Q302 430 271 430Q240 430 214 416.5Q188 403 169.5 378Q151 353 141 318.5Q131 284 131 242Q131 200 141 166Q151 132 169.5 107.5Q188 83 214 69.5Q240 56 271 56Z'); + it('should get a correct quadratic path for all contours', function() { + return assert.equal(glyph.path.toSVG(), 'M90 0L90 656L254 656Q406 656 485 571.5Q564 487 564 331Q564 174 485.5 87Q407 0 258 0ZM173 68L248 68Q363 68 420.5 137.5Q478 207 478 331Q478 455 420.5 521.5Q363 588 248 588L173 588Z'); }); - it('should get the glyph cbox', function() { - let glyph = font.glyphsForString('T')[0]; - return assert.deepEqual(glyph.cbox, new BBox(28, 0, 508, 656)); + it('should get the ttf glyph cbox', function() { + return assert.deepEqual(glyph.cbox, expectedBox); }); - it('should get the glyph bbox', function() { - let glyph = font.glyphsForString('T')[0]; - return assert.deepEqual(glyph.bbox, new BBox(28, 0, 508, 656)); + it('should get the ttf glyph bbox', function() { + return assert.deepEqual(glyph.bbox, expectedBox); }); + }); + + describe('WOFF2 otf glyph', function() { + let font = fontkit.openSync(__dirname + '/data/SourceSansPro/SourceSansPro-Regular.otf.woff2'); + + let glyph = font.glyphsForString('D')[0]; + let expectedBox = new BBox(90, 0, 564, 656); it('should get the glyph name', function() { - let glyph = font.glyphsForString('T')[0]; - return assert.equal(glyph.name, 'T'); + return assert.equal(glyph.name, 'D'); + }); + + it('should get a CFFGlyph', function() { + return assert.equal(glyph.constructor.name, 'CFFGlyph'); + }); + + it('should get a path for the glyph', function() { + let tglyph = font.glyphsForString('T')[0]; + return assert.equal(tglyph.path.toSVG(), 'M226 0L310 0L310 586L508 586L508 656L28 656L28 586L226 586Z'); + }); + + it('should get a correct cubic path for all contours', function() { + return assert.equal(glyph.path.toSVG(), 'M90 0L258 0C456 0 564 122 564 331C564 539 456 656 254 656L90 656ZM173 68L173 588L248 588C401 588 478 496 478 331C478 165 401 68 248 68Z'); + }); + + it('should get the otf glyph cbox', function() { + return assert.deepEqual(glyph.cbox, expectedBox); + }); + + it('should get the otf glyph bbox', function() { + return assert.deepEqual(glyph.bbox, expectedBox); }); }); }); diff --git a/test/i18n.js b/test/i18n.js new file mode 100644 index 00000000..c64d9dde --- /dev/null +++ b/test/i18n.js @@ -0,0 +1,110 @@ +import assert from 'assert'; +import fontkit from '../src'; + +describe('i18n', function() { + describe('fontkit.setDefaultLanguage', function () { + let font; + before('load Amiri font', function() { + font = fontkit.openSync(__dirname + '/data/amiri/amiri-regular.ttf'); + }); + + after('reset default language', function () { + fontkit.setDefaultLanguage(); + }); + + it('font has "en" metadata properties', function() { + assert.equal(font.fullName, 'Amiri'); + assert.equal(font.postscriptName, 'Amiri-Regular'); + assert.equal(font.familyName, 'Amiri'); + assert.equal(font.subfamilyName, 'Regular'); + assert.equal(font.copyright, 'Copyright (c) 2010-2017, Khaled Hosny .\nPortions copyright (c) 2010, Sebastian Kosch .'); + assert.equal(font.version, 'Version 000.110 '); + }); + + it('can set global default language to "ar"', function () { + fontkit.setDefaultLanguage('ar'); + assert.equal(fontkit.defaultLanguage, 'ar'); + }); + + it('font now has "ar" metadata properties', function() { + assert.equal(font.fullName, 'Amiri'); + assert.equal(font.postscriptName, 'Amiri-Regular'); + assert.equal(font.familyName, 'Amiri'); + assert.equal(font.subfamilyName, 'عادي'); + assert.equal(font.copyright, 'حقوق النشر 2010-2017، خالد حسني .'); + assert.equal(font.version, 'إصدارة 000٫110'); + }); + + it('can reset default language back to "en"', function () { + fontkit.setDefaultLanguage(); + assert.equal(fontkit.defaultLanguage, "en"); + }); + }); + + describe('font.setDefaultLanguage', function () { + let font; + before('load Amiri font', function () { + font = fontkit.openSync(__dirname + '/data/amiri/amiri-regular.ttf'); + }); + + it('font has "en" metadata properties', function() { + assert.equal(font.fullName, 'Amiri'); + assert.equal(font.postscriptName, 'Amiri-Regular'); + assert.equal(font.familyName, 'Amiri'); + assert.equal(font.subfamilyName, 'Regular'); + assert.equal(font.copyright, 'Copyright (c) 2010-2017, Khaled Hosny .\nPortions copyright (c) 2010, Sebastian Kosch .'); + assert.equal(font.version, 'Version 000.110 '); + }); + + it('can set font\'s default language to "ar"', function () { + font.setDefaultLanguage('ar'); + assert.equal(font.defaultLanguage, 'ar'); + }); + + it('font now has "ar" metadata properties', function() { + assert.equal(font.fullName, 'Amiri'); + assert.equal(font.postscriptName, 'Amiri-Regular'); + assert.equal(font.familyName, 'Amiri'); + assert.equal(font.subfamilyName, 'عادي'); + assert.equal(font.copyright, 'حقوق النشر 2010-2017، خالد حسني .'); + assert.equal(font.version, 'إصدارة 000٫110'); + }); + + it('the font\'s language should not change when the global changes', function () { + fontkit.setDefaultLanguage('en'); + + assert.equal(font.defaultLanguage, 'ar'); + assert.equal(font.subfamilyName, 'عادي'); + }); + + it('can reset default language back to "en"', function () { + font.setDefaultLanguage(); + assert.equal(font.defaultLanguage, null); + assert.equal(font.subfamilyName, 'Regular'); + }); + }); + + describe('backup languages', function () { + let font; + before('load Amiri font', function () { + font = fontkit.openSync(__dirname + '/data/amiri/amiri-regular.ttf'); + }); + + after('reset default language', function () { + fontkit.setDefaultLanguage(); + }); + + it('if the font\'s default language isn\'t found, use the global language', function () { + font.setDefaultLanguage('piglatin'); + fontkit.setDefaultLanguage('ar'); + + assert.equal(font.subfamilyName, 'عادي'); + }); + it('if the global language isn\'t found, use "en"', function () { + font.setDefaultLanguage('piglatin'); + fontkit.setDefaultLanguage('klingon'); + + assert.equal(font.subfamilyName, 'Regular'); + }); + }); +}); diff --git a/test/subset.js b/test/subset.js index f90d9519..95241672 100644 --- a/test/subset.js +++ b/test/subset.js @@ -57,6 +57,23 @@ describe('font subsetting', function() { done(); })); }); + + it('should handle fonts with long index to location format (indexToLocFormat = 1)', function(done) { + let font = fontkit.openSync(__dirname + '/data/FiraSans/FiraSans-Regular.ttf'); + let subset = font.createSubset(); + for (let glyph of font.glyphsForString('abcd')) { + subset.includeGlyph(glyph); + } + + subset.encodeStream().pipe(concat(function(buf) { + let f = fontkit.create(buf); + assert.equal(f.numGlyphs, 5); + assert.equal(f.getGlyph(1).path.toSVG(), font.glyphsForString('a')[0].path.toSVG()); + // must test also second glyph which has an odd loca index + assert.equal(f.getGlyph(2).path.toSVG(), font.glyphsForString('b')[0].path.toSVG()); + done(); + })); + }); }); describe('CFF subsetting', function() { @@ -64,7 +81,13 @@ describe('font subsetting', function() { it('should create a CFFSubset instance', function() { let subset = font.createSubset(); - return assert.equal(subset.constructor.name, 'CFFSubset'); + assert.equal(subset.constructor.name, 'CFFSubset'); + + if (fs.existsSync('/Library/Fonts/PingFang.ttc')) { + let pingfang = fontkit.openSync('/Library/Fonts/PingFang.ttc', 'PingFangTC-Regular'); + subset = pingfang.createSubset(); + assert.equal(subset.constructor.name, 'CFFSubset'); + } }); it('should produce a subset', function(done) { diff --git a/yarn.lock b/yarn.lock index 7492f3aa..d5ba9b95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1575,9 +1575,10 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -clone@^1.0.1: +clone@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= clone@~0.1.9: version "0.1.19" @@ -1673,9 +1674,10 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.4.4, concat-stream@^1.4.6: +concat-stream@^1.4.4, concat-stream@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== dependencies: buffer-from "^1.0.0" inherits "^2.0.3" @@ -1927,7 +1929,7 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" -dfa@^1.0.0: +dfa@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657" integrity sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q== @@ -2678,7 +2680,7 @@ ice-cap@0.0.4: cheerio "0.20.0" color-logger "0.0.3" -iconv-lite@^0.4.13: +iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==