From daa9b3adfffce04b6c8f807666eb51719e466379 Mon Sep 17 00:00:00 2001 From: Connum Date: Fri, 24 Nov 2023 17:14:14 +0100 Subject: [PATCH 1/9] implement graceful loading via logging severity levels --- docs/font-inspector.html | 17 +- docs/glyph-inspector.html | 64 +++- docs/index.html | 17 +- docs/site.css | 21 +- src/font.js | 19 +- src/font_REMOTE_153.js | 666 ++++++++++++++++++++++++++++++++++++++ src/glyphset.js | 5 +- src/logger.js | 141 ++++++++ src/opentype.js | 57 +++- src/parse.js | 27 +- src/tables/cff.js | 92 ++++-- src/types.js | 9 +- 12 files changed, 1050 insertions(+), 85 deletions(-) create mode 100644 src/font_REMOTE_153.js create mode 100644 src/logger.js diff --git a/docs/font-inspector.html b/docs/font-inspector.html index 52e1ea14..dea8fd6a 100644 --- a/docs/font-inspector.html +++ b/docs/font-inspector.html @@ -144,12 +144,24 @@

Free Software

var el = document.getElementById('message'); if (!message || message.trim().length === 0) { el.style.display = 'none'; + el.innerHTML = ''; } else { el.style.display = 'block'; + el.innerHTML = `

${message}

`; } - el.innerHTML = message; } +function appendErrorMessage(message, type) { + var el = document.getElementById('message'); + el.style.display = 'block'; + el.innerHTML += `

${message}

`; +} + +document.addEventListener('opentypejs:message', function(event) { + const message = event.detail.message; + appendErrorMessage(message.toString(), message.type); +}); + function sortKeys(dict) { var keys = []; for (var key in dict) { @@ -257,10 +269,11 @@

Free Software

} try { const data = await file.arrayBuffer(); - onFontLoaded(opentype.parse(isWoff2 ? Module.decompress(data) : data)); showErrorMessage(''); + onFontLoaded(opentype.parse(isWoff2 ? Module.decompress(data) : data)); } catch (err) { showErrorMessage(err.toString()); + throw err; } } form.file.onchange = function(e) { diff --git a/docs/glyph-inspector.html b/docs/glyph-inspector.html index 60732306..dd090554 100644 --- a/docs/glyph-inspector.html +++ b/docs/glyph-inspector.html @@ -92,12 +92,24 @@

Free Software

var el = document.getElementById('message'); if (!message || message.trim().length === 0) { el.style.display = 'none'; + el.innerHTML = ''; } else { el.style.display = 'block'; + el.innerHTML = `

${message}

`; } - el.innerHTML = message; } +function appendErrorMessage(message, type) { + var el = document.getElementById('message'); + el.style.display = 'block'; + el.innerHTML += `

${message}

`; +} + +document.addEventListener('opentypejs:message', function(event) { + const message = event.detail.message; + appendErrorMessage(message.toString(), message.type); +}); + function pathCommandToString(cmd) { var str = '' + cmd.type + ' ' + ((cmd.x !== undefined) ? 'x='+cmd.x+' y='+cmd.y+' ' : '') + @@ -273,13 +285,19 @@

Free Software

var cellMarkSize = 4; var ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, cellWidth, cellHeight); - if (glyphIndex >= window.font.numGlyphs) return; + const nGlyphs = window.font.numGlyphs || window.font.nGlyphs; + if (glyphIndex >= nGlyphs) return; ctx.fillStyle = '#606060'; ctx.font = '9px sans-serif'; ctx.fillText(glyphIndex, 1, cellHeight-1); - var glyph = window.font.glyphs.get(glyphIndex), - glyphWidth = glyph.advanceWidth * fontScale, + const glyph = window.font.glyphs.get(glyphIndex); + if (!glyph.advanceWidth) { + // force calculation of path data + glyph.getPath(); + } + const advanceWidth = glyph.advanceWidth; + let glyphWidth = glyph.advanceWidth * fontScale, xmin = (cellWidth - glyphWidth)/2, xmax = (cellWidth + glyphWidth)/2, x0 = xmin; @@ -314,7 +332,7 @@

Free Software

h = glyphBgCanvas.height / pixelRatio, glyphW = w - glyphMargin*2, glyphH = h - glyphMargin*2, - head = font.tables.head, + head = getFontDimensions(font), maxHeight = head.yMax - head.yMin, ctx = glyphBgCanvas.getContext('2d'); @@ -331,12 +349,23 @@

Free Software

ctx.clearRect(0, 0, w, h); ctx.fillStyle = '#a0a0a0'; hline('Baseline', 0); - hline('yMax', font.tables.head.yMax); - hline('yMin', font.tables.head.yMin); - hline('Ascender', font.tables.hhea.ascender); - hline('Descender', font.tables.hhea.descender); - hline('Typo Ascender', font.tables.os2.sTypoAscender); - hline('Typo Descender', font.tables.os2.sTypoDescender); + hline('yMax', head.yMax); + hline('yMin', head.yMin); + hline('Ascender', font.tables.hhea ? font.tables.hhea.ascender : font.ascender || head.yMax); + hline('Descender', font.tables.hhea ? font.tables.hhea.descender : font.descender || head.yMin); + if (font.tables.os2) { + hline('Typo Ascender', font.tables.os2.sTypoAscender); + hline('Typo Descender', font.tables.os2.sTypoDescender); + } +} + +function getFontDimensions(font) { + return font.isCFFFont ? { + xMin: font.tables.cff.topDict.fontBBox[0], + xMax: font.tables.cff.topDict.fontBBox[3] || 1000, + yMin: font.tables.cff.topDict.fontBBox[1] || -200, + yMax: font.tables.cff.topDict.fontBBox[2] || 1000 + } :font.tables.head; } function onFontLoaded(font) { @@ -344,7 +373,7 @@

Free Software

var w = cellWidth - cellMarginLeftRight * 2, h = cellHeight - cellMarginTop - cellMarginBottom, - head = font.tables.head, + head = getFontDimensions(font), maxHeight = head.yMax - head.yMin; fontScale = Math.min(w/(head.xMax - head.xMin), h/maxHeight); fontSize = fontScale * font.unitsPerEm; @@ -353,10 +382,11 @@

Free Software

var pagination = document.getElementById("pagination"); pagination.innerHTML = ''; var fragment = document.createDocumentFragment(); - var numPages = Math.ceil(font.numGlyphs / cellCount); + const nGlyphs = font.numGlyphs || font.nGlyphs; + var numPages = Math.ceil(nGlyphs / cellCount); for(var i = 0; i < numPages; i++) { var link = document.createElement('span'); - var lastIndex = Math.min(font.numGlyphs-1, (i+1)*cellCount-1); + var lastIndex = Math.min(nGlyphs-1, (i+1)*cellCount-1); link.textContent = i*cellCount + '-' + lastIndex; link.id = 'p' + i; link.addEventListener('click', pageSelect, false); @@ -378,7 +408,8 @@

Free Software

var firstGlyphIndex = pageSelected*cellCount, cellIndex = +event.target.id.substr(1), glyphIndex = firstGlyphIndex + cellIndex; - if (glyphIndex < window.font.numGlyphs) { + const nGlyphs = window.font.numGlyphs || window.font.nGlyphs; + if (glyphIndex < nGlyphs) { displayGlyph(glyphIndex); displayGlyphData(glyphIndex); } @@ -410,10 +441,11 @@

Free Software

} try { const data = await file.arrayBuffer(); - onFontLoaded(opentype.parse(isWoff2 ? Module.decompress(data) : data)); showErrorMessage(''); + onFontLoaded(opentype.parse(isWoff2 ? Module.decompress(data) : data)); } catch (err) { showErrorMessage(err.toString()); + throw err; } } diff --git a/docs/index.html b/docs/index.html index ed92e27e..34702b0a 100755 --- a/docs/index.html +++ b/docs/index.html @@ -169,12 +169,24 @@

Free Software

var el = document.getElementById('message'); if (!message || message.trim().length === 0) { el.style.display = 'none'; + el.innerHTML = ''; } else { el.style.display = 'block'; + el.innerHTML = `

${message}

`; } - el.innerHTML = message; } +function appendErrorMessage(message, type) { + var el = document.getElementById('message'); + el.style.display = 'block'; + el.innerHTML += `

${message}

`; +} + +document.addEventListener('opentypejs:message', function(event) { + const message = event.detail.message; + appendErrorMessage(message.toString(), message.type); +}); + function onFontLoaded(font) { window.font = font; @@ -217,10 +229,11 @@

Free Software

} try { const data = await file.arrayBuffer(); - onFontLoaded(opentype.parse(isWoff2 ? Module.decompress(data) : data)); showErrorMessage(''); + onFontLoaded(opentype.parse(isWoff2 ? Module.decompress(data) : data)); } catch (err) { showErrorMessage(err.toString()); + throw err; } } diff --git a/docs/site.css b/docs/site.css index 07693892..be42a0a7 100644 --- a/docs/site.css +++ b/docs/site.css @@ -107,14 +107,27 @@ canvas.text { #message { position: relative; - top: -3px; - background: red; color: white; - padding: 1px 5px; font-weight: bold; - border-radius: 2px; display: none; clear: both; + padding-top: 1px; +} + +#message p { + margin: 2px 0; + padding: 2px 5px; + border-radius: 0.25rem; + border: solid 1px; + background: #fff3cd; + color: #856404; + border-color: #ffeeba; +} + +#message p.message-type-1 { + background: #f8d7da; + color: #721c24; + border-color: #f5c6cb; } .message { diff --git a/src/font.js b/src/font.js index 09d2dddc..af1e7a15 100644 --- a/src/font.js +++ b/src/font.js @@ -9,14 +9,15 @@ import Substitution from './substitution.js'; import { isBrowser, checkArgument } from './util.js'; import HintingTrueType from './hintingtt.js'; import Bidi from './bidi.js'; +import { logger, ErrorTypes, MessageLogger } from './logger.js'; function createDefaultNamesInfo(options) { return { fontFamily: {en: options.familyName || ' '}, fontSubfamily: {en: options.styleName || ' '}, - fullName: {en: options.fullName || options.familyName + ' ' + options.styleName}, + fullName: {en: options.fullName || (options.familyName || '') + ' ' + (options.styleName || '')}, // postScriptName may not contain any whitespace - postScriptName: {en: options.postScriptName || (options.familyName + options.styleName).replace(/\s/g, '')}, + postScriptName: {en: options.postScriptName || ((options.familyName || '') + (options.styleName || '')).replace(/\s/g, '')}, designer: {en: options.designer || ' '}, designerURL: {en: options.designerURL || ' '}, manufacturer: {en: options.manufacturer || ' '}, @@ -502,14 +503,17 @@ Font.prototype.getEnglishName = function(name) { /** * Validate + * @type {MessageLogger} */ +Font.prototype.validation = new MessageLogger(); +Font.prototype.ErrorTypes = ErrorTypes; Font.prototype.validate = function() { - const warnings = []; + const validationMessages = []; const _this = this; function assert(predicate, message) { if (!predicate) { - warnings.push(message); + validationMessages.push(_this.validation.add(message, _this.ErrorTypes.WARNING)); } } @@ -528,6 +532,8 @@ Font.prototype.validate = function() { // Dimension information assert(this.unitsPerEm > 0, 'No unitsPerEm specified.'); + + return validationMessages; }; /** @@ -542,7 +548,7 @@ Font.prototype.toTables = function() { * @deprecated Font.toBuffer is deprecated. Use Font.toArrayBuffer instead. */ Font.prototype.toBuffer = function() { - console.warn('Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.'); + logger.add('Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.', this.ErrorTypes.DEPRECATED); return this.toArrayBuffer(); }; /** @@ -585,7 +591,7 @@ Font.prototype.download = function(fileName) { event.initEvent('click', true, false); link.dispatchEvent(event); } else { - console.warn('Font file could not be downloaded. Try using a different browser.'); + logger.add('Font file could not be downloaded. Try using a different browser.'); } } else { const fs = require('fs'); @@ -658,3 +664,4 @@ Font.prototype.usWeightClasses = { }; export default Font; +export { createDefaultNamesInfo }; \ No newline at end of file diff --git a/src/font_REMOTE_153.js b/src/font_REMOTE_153.js new file mode 100644 index 00000000..6f94f615 --- /dev/null +++ b/src/font_REMOTE_153.js @@ -0,0 +1,666 @@ +// The Font object + +import Path from './path.js'; +import sfnt from './tables/sfnt.js'; +import { DefaultEncoding } from './encoding.js'; +import glyphset from './glyphset.js'; +import Position from './position.js'; +import Substitution from './substitution.js'; +import { isBrowser, checkArgument } from './util.js'; +import HintingTrueType from './hintingtt.js'; +import Bidi from './bidi.js'; +import validation from './validation.js'; + +function createDefaultNamesInfo(options) { + return { + fontFamily: {en: options.familyName || ' '}, + fontSubfamily: {en: options.styleName || ' '}, + fullName: {en: options.fullName || options.familyName + ' ' + options.styleName}, + // postScriptName may not contain any whitespace + postScriptName: {en: options.postScriptName || (options.familyName + options.styleName).replace(/\s/g, '')}, + designer: {en: options.designer || ' '}, + designerURL: {en: options.designerURL || ' '}, + manufacturer: {en: options.manufacturer || ' '}, + manufacturerURL: {en: options.manufacturerURL || ' '}, + license: {en: options.license || ' '}, + licenseURL: {en: options.licenseURL || ' '}, + version: {en: options.version || 'Version 0.1'}, + description: {en: options.description || ' '}, + copyright: {en: options.copyright || ' '}, + trademark: {en: options.trademark || ' '} + }; +} + +/** + * @typedef FontOptions + * @type Object + * @property {Boolean} empty - whether to create a new empty font + * @property {string} familyName + * @property {string} styleName + * @property {string=} fullName + * @property {string=} postScriptName + * @property {string=} designer + * @property {string=} designerURL + * @property {string=} manufacturer + * @property {string=} manufacturerURL + * @property {string=} license + * @property {string=} licenseURL + * @property {string=} version + * @property {string=} description + * @property {string=} copyright + * @property {string=} trademark + * @property {Number} unitsPerEm + * @property {Number} ascender + * @property {Number} descender + * @property {Number} createdTimestamp + * @property {Number} weightClass + * @property {Number} italicAngle + * @property {string=} widthClass + * @property {string=} fsSelection + */ + +/** + * 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. + * @exports opentype.Font + * @class + * @param {FontOptions} + * @constructor + */ +function Font(options) { + options = options || {}; + options.tables = options.tables || {}; + + if (!options.empty) { + // Check that we've provided the minimum set of names. + checkArgument(options.familyName, 'When creating a new Font object, familyName is required.'); + checkArgument(options.styleName, 'When creating a new Font object, styleName is required.'); + checkArgument(options.unitsPerEm, 'When creating a new Font object, unitsPerEm is required.'); + checkArgument(options.ascender, 'When creating a new Font object, ascender is required.'); + checkArgument(options.descender <= 0, 'When creating a new Font object, negative descender value is required.'); + + // OS X will complain if the names are empty, so we put a single space everywhere by default. + this.names = {}; + this.names.unicode = createDefaultNamesInfo(options); + this.names.macintosh = createDefaultNamesInfo(options); + this.names.windows = createDefaultNamesInfo(options); + this.unitsPerEm = options.unitsPerEm || 1000; + this.ascender = options.ascender; + this.descender = options.descender; + this.createdTimestamp = options.createdTimestamp; + this.italicAngle = options.italicAngle || 0; + this.weightClass = options.weightClass || 0; + + let selection = 0; + if (options.fsSelection) { + selection = options.fsSelection; + } else { + if (this.italicAngle < 0) { + selection |= this.fsSelectionValues.ITALIC; + } else if (this.italicAngle > 0) { + selection |= this.fsSelectionValues.OBLIQUE; + } + if (this.weightClass >= 600) { + selection |= this.fsSelectionValues.BOLD; + } + if (selection == 0) { + selection = this.fsSelectionValues.REGULAR; + } + } + + if (!options.panose || !Array.isArray(options.panose)) { + options.panose = [0, 0, 0, 0, 0, 0, 0, 0, 0]; + } + + this.tables = Object.assign(options.tables, { + os2: Object.assign({ + usWeightClass: options.weightClass || this.usWeightClasses.MEDIUM, + usWidthClass: options.widthClass || this.usWidthClasses.MEDIUM, + bFamilyType: options.panose[0] || 0, + bSerifStyle: options.panose[1] || 0, + bWeight: options.panose[2] || 0, + bProportion: options.panose[3] || 0, + bContrast: options.panose[4] || 0, + bStrokeVariation: options.panose[5] || 0, + bArmStyle: options.panose[6] || 0, + bLetterform: options.panose[7] || 0, + bMidline: options.panose[8] || 0, + bXHeight: options.panose[9] || 0, + fsSelection: selection, + }, options.tables.os2) + }); + } + + this.supported = true; // Deprecated: parseBuffer will throw an error if font is not supported. + this.glyphs = new glyphset.GlyphSet(this, options.glyphs || []); + this.encoding = new DefaultEncoding(this); + this.position = new Position(this); + this.substitution = new Substitution(this); + this.tables = this.tables || {}; + + // needed for low memory mode only. + this._push = null; + this._hmtxTableData = {}; + + Object.defineProperty(this, 'hinting', { + get: function() { + if (this._hinting) return this._hinting; + if (this.outlinesFormat === 'truetype') { + return (this._hinting = new HintingTrueType(this)); + } + return null; + } + }); +} + +/** + * Check if the font has a glyph for the given character. + * @param {string} + * @return {Boolean} + */ +Font.prototype.hasChar = function(c) { + return this.encoding.charToGlyphIndex(c) > 0; +}; + +/** + * Convert the given character to a single glyph index. + * 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. + * @param {string} + * @return {Number} + */ +Font.prototype.charToGlyphIndex = function(s) { + return this.encoding.charToGlyphIndex(s); +}; + +/** + * Convert the given character to a single Glyph object. + * 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. + * @param {string} + * @return {opentype.Glyph} + */ +Font.prototype.charToGlyph = function(c) { + const glyphIndex = this.charToGlyphIndex(c); + let glyph = this.glyphs.get(glyphIndex); + if (!glyph) { + // .notdef + glyph = this.glyphs.get(0); + } + + return glyph; +}; + +/** + * Update features + * @param {any} options features options + */ +Font.prototype.updateFeatures = function (options) { + // TODO: update all features options not only 'latn'. + return this.defaultRenderOptions.features.map(feature => { + if (feature.script === 'latn') { + return { + script: 'latn', + tags: feature.tags.filter(tag => options[tag]) + }; + } else { + return feature; + } + }); +}; + +/** + * Convert the given text to a list of Glyph indexes. + * Note that there is no strict one-to-one mapping between characters and + * glyphs, so the list of returned glyph indexes can be larger or smaller than the + * length of the given string. + * @param {string} + * @param {GlyphRenderOptions} [options] + * @return {number[]} + */ +Font.prototype.stringToGlyphIndexes = function(s, options) { + const bidi = new Bidi(); + + // Create and register 'glyphIndex' state modifier + const charToGlyphIndexMod = token => this.charToGlyphIndex(token.char); + bidi.registerModifier('glyphIndex', null, charToGlyphIndexMod); + + // roll-back to default features + let features = options ? + this.updateFeatures(options.features) : + this.defaultRenderOptions.features; + + bidi.applyFeatures(this, features); + + return bidi.getTextGlyphs(s); +}; + +/** + * Convert the given text to a list of Glyph objects. + * Note that there is no strict one-to-one mapping between characters and + * glyphs, so the list of returned glyphs can be larger or smaller than the + * length of the given string. + * @param {string} + * @param {GlyphRenderOptions} [options] + * @return {opentype.Glyph[]} + */ +Font.prototype.stringToGlyphs = function(s, options) { + const indexes = this.stringToGlyphIndexes(s, options); + + let length = indexes.length; + + // convert glyph indexes to glyph objects + const glyphs = new Array(length); + const notdef = this.glyphs.get(0); + for (let i = 0; i < length; i += 1) { + glyphs[i] = this.glyphs.get(indexes[i]) || notdef; + } + return glyphs; +}; + +/** + * @param {string} + * @return {Number} + */ +Font.prototype.nameToGlyphIndex = function(name) { + return this.glyphNames.nameToGlyphIndex(name); +}; + +/** + * @param {string} + * @return {opentype.Glyph} + */ +Font.prototype.nameToGlyph = function(name) { + const glyphIndex = this.nameToGlyphIndex(name); + let glyph = this.glyphs.get(glyphIndex); + if (!glyph) { + // .notdef + glyph = this.glyphs.get(0); + } + + return glyph; +}; + +/** + * @param {Number} + * @return {String} + */ +Font.prototype.glyphIndexToName = function(gid) { + if (!this.glyphNames.glyphIndexToName) { + return ''; + } + + return this.glyphNames.glyphIndexToName(gid); +}; + +/** + * Retrieve the value of the kerning pair 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. + * For GPOS kerning, this method uses the default script and language, which covers + * most use cases. To have greater control, use font.position.getKerningValue . + * @param {opentype.Glyph} leftGlyph + * @param {opentype.Glyph} rightGlyph + * @return {Number} + */ +Font.prototype.getKerningValue = function(leftGlyph, rightGlyph) { + leftGlyph = leftGlyph.index || leftGlyph; + rightGlyph = rightGlyph.index || rightGlyph; + const gposKerning = this.position.defaultKerningTables; + if (gposKerning) { + return this.position.getKerningValue(gposKerning, leftGlyph, rightGlyph); + } + // "kern" table + return this.kerningPairs[leftGlyph + ',' + rightGlyph] || 0; +}; + +/** + * @typedef GlyphRenderOptions + * @type Object + * @property {string} [script] - script used to determine which features to apply. By default, 'DFLT' or 'latn' is used. + * See https://www.microsoft.com/typography/otspec/scripttags.htm + * @property {string} [language='dflt'] - language system used to determine which features to apply. + * See https://www.microsoft.com/typography/developers/opentype/languagetags.aspx + * @property {boolean} [kerning=true] - whether to include kerning values + * @property {object} [features] - OpenType Layout feature tags. Used to enable or disable the features of the given script/language system. + * See https://www.microsoft.com/typography/otspec/featuretags.htm + */ +Font.prototype.defaultRenderOptions = { + kerning: true, + features: [ + /** + * these 4 features are required to render Arabic text properly + * and shouldn't be turned off when rendering arabic text. + */ + { script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] }, + { script: 'latn', tags: ['liga', 'rlig'] }, + { script: 'thai', tags: ['liga', 'rlig', 'ccmp'] }, + ] +}; + +/** + * Helper function that invokes the given callback for each glyph in the given text. + * The callback gets `(glyph, x, y, fontSize, options)`.* @param {string} text + * @param {string} text - The text to apply. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + * @param {Function} callback + */ +Font.prototype.forEachGlyph = function(text, x, y, fontSize, options, callback) { + x = x !== undefined ? x : 0; + y = y !== undefined ? y : 0; + fontSize = fontSize !== undefined ? fontSize : 72; + options = Object.assign({}, this.defaultRenderOptions, options); + const fontScale = 1 / this.unitsPerEm * fontSize; + const glyphs = this.stringToGlyphs(text, options); + let kerningLookups; + if (options.kerning) { + const script = options.script || this.position.getDefaultScriptName(); + kerningLookups = this.position.getKerningTables(script, options.language); + } + for (let i = 0; i < glyphs.length; i += 1) { + const glyph = glyphs[i]; + callback.call(this, glyph, x, y, fontSize, options); + if (glyph.advanceWidth) { + x += glyph.advanceWidth * fontScale; + } + + if (options.kerning && i < glyphs.length - 1) { + // We should apply position adjustment lookups in a more generic way. + // Here we only use the xAdvance value. + const kerningValue = kerningLookups ? + this.position.getKerningValue(kerningLookups, glyph.index, glyphs[i + 1].index) : + this.getKerningValue(glyph, glyphs[i + 1]); + x += kerningValue * fontScale; + } + + if (options.letterSpacing) { + x += options.letterSpacing * fontSize; + } else if (options.tracking) { + x += (options.tracking / 1000) * fontSize; + } + } + return x; +}; + +/** + * Create a Path object that represents the given text. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + * @return {opentype.Path} + */ +Font.prototype.getPath = function(text, x, y, fontSize, options) { + const fullPath = new Path(); + this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { + const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this); + fullPath.extend(glyphPath); + }); + return fullPath; +}; + +/** + * Create an array of Path objects that represent the glyphs of a given text. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + * @return {opentype.Path[]} + */ +Font.prototype.getPaths = function(text, x, y, fontSize, options) { + const glyphPaths = []; + this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { + const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this); + glyphPaths.push(glyphPath); + }); + + return glyphPaths; +}; + +/** + * Returns the advance width of a text. + * + * This is something different than Path.getBoundingBox() as 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 + * + * @param {string} text - The text to create. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + * @return advance width + */ +Font.prototype.getAdvanceWidth = function(text, fontSize, options) { + return this.forEachGlyph(text, 0, 0, fontSize, options, function() {}); +}; + +/** + * Draw the text on the given drawing context. + * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + */ +Font.prototype.draw = function(ctx, text, x, y, fontSize, options) { + this.getPath(text, x, y, fontSize, options).draw(ctx); +}; + +/** + * 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. + * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + */ +Font.prototype.drawPoints = function(ctx, text, x, y, fontSize, options) { + this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { + glyph.drawPoints(ctx, gX, gY, gFontSize); + }); +}; + +/** + * 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. + * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + */ +Font.prototype.drawMetrics = function(ctx, text, x, y, fontSize, options) { + this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { + glyph.drawMetrics(ctx, gX, gY, gFontSize); + }); +}; + +/** + * @param {string} + * @return {string} + */ +Font.prototype.getEnglishName = function(name) { + const translations = (this.names.unicode || this.names.macintosh || this.names.windows)[name]; + if (translations) { + return translations.en; + } +}; + +/** + * Validate + */ +Font.prototype.validation = new validation.MessageStack(); +Font.prototype.validate = function() { + const validationMessages = []; + const _this = this; + + function assert(predicate, message) { + if (!predicate) { + validationMessages.push(_this.validation.add(message, validation.errorTypes.WARNING)); + } + } + + function assertNamePresent(name) { + const englishName = _this.getEnglishName(name); + assert(englishName && englishName.trim().length > 0, + 'No English ' + name + ' specified.'); + } + + // Identification information + assertNamePresent('fontFamily'); + assertNamePresent('weightName'); + assertNamePresent('manufacturer'); + assertNamePresent('copyright'); + assertNamePresent('version'); + + // Dimension information + assert(this.unitsPerEm > 0, 'No unitsPerEm specified.'); + + this.validation.logMessages(); + + return validationMessages; +}; + +/** + * Convert the font object to a SFNT data structure. + * This structure contains all the necessary tables and metadata to create a binary OTF file. + * @return {opentype.Table} + */ +Font.prototype.toTables = function() { + return sfnt.fontToTable(this); +}; +/** + * @deprecated Font.toBuffer is deprecated. Use Font.toArrayBuffer instead. + */ +Font.prototype.toBuffer = function() { + this.validation.add('Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.', validation.errorTypes.DEPRECATED); + return this.toArrayBuffer(); +}; +/** + * Converts a `opentype.Font` into an `ArrayBuffer` + * @return {ArrayBuffer} + */ +Font.prototype.toArrayBuffer = function() { + const sfntTable = this.toTables(); + const bytes = sfntTable.encode(); + const buffer = new ArrayBuffer(bytes.length); + const intArray = new Uint8Array(buffer); + for (let i = 0; i < bytes.length; i++) { + intArray[i] = bytes[i]; + } + + return buffer; +}; + +/** + * Initiate a download of the OpenType font. + */ +Font.prototype.download = function(fileName) { + const familyName = this.getEnglishName('fontFamily'); + const styleName = this.getEnglishName('fontSubfamily'); + fileName = fileName || familyName.replace(/\s/g, '') + '-' + styleName + '.otf'; + const arrayBuffer = this.toArrayBuffer(); + + if (isBrowser()) { + window.URL = window.URL || window.webkitURL; + + if (window.URL) { + const dataView = new DataView(arrayBuffer); + const blob = new Blob([dataView], {type: 'font/opentype'}); + + let link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = fileName; + + let event = document.createEvent('MouseEvents'); + event.initEvent('click', true, false); + link.dispatchEvent(event); + } else { + validation.add('Font file could not be downloaded. Try using a different browser.'); + } + } else { + const fs = require('fs'); + const buffer = Buffer.alloc(arrayBuffer.byteLength); + const view = new Uint8Array(arrayBuffer); + for (let i = 0; i < buffer.length; ++i) { + buffer[i] = view[i]; + } + fs.writeFileSync(fileName, buffer); + } +}; + +/** + * @private + */ +Font.prototype.fsSelectionValues = { + ITALIC: 0x001, //1 + UNDERSCORE: 0x002, //2 + NEGATIVE: 0x004, //4 + OUTLINED: 0x008, //8 + STRIKEOUT: 0x010, //16 + BOLD: 0x020, //32 + REGULAR: 0x040, //64 + USER_TYPO_METRICS: 0x080, //128 + WWS: 0x100, //256 + OBLIQUE: 0x200 //512 +}; + +/** + * @private + */ +Font.prototype.macStyleValues = { + BOLD: 0x001, //1 + ITALIC: 0x002, //2 + UNDERLINE: 0x004, //4 + OUTLINED: 0x008, //8 + SHADOW: 0x010, //16 + CONDENSED: 0x020, //32 + EXTENDED: 0x040, //64 +}; + +/** + * @private + */ +Font.prototype.usWidthClasses = { + ULTRA_CONDENSED: 1, + EXTRA_CONDENSED: 2, + CONDENSED: 3, + SEMI_CONDENSED: 4, + MEDIUM: 5, + SEMI_EXPANDED: 6, + EXPANDED: 7, + EXTRA_EXPANDED: 8, + ULTRA_EXPANDED: 9 +}; + +/** + * @private + */ +Font.prototype.usWeightClasses = { + THIN: 100, + EXTRA_LIGHT: 200, + LIGHT: 300, + NORMAL: 400, + MEDIUM: 500, + SEMI_BOLD: 600, + BOLD: 700, + EXTRA_BOLD: 800, + BLACK: 900 +}; + +export default Font; diff --git a/src/glyphset.js b/src/glyphset.js index adddb395..1f889fba 100644 --- a/src/glyphset.js +++ b/src/glyphset.js @@ -37,7 +37,6 @@ function GlyphSet(font, glyphs) { this.glyphs[i] = glyph; } } - this.length = (glyphs && glyphs.length) || 0; } @@ -64,13 +63,15 @@ if(typeof Symbol !== 'undefined' && Symbol.iterator) { GlyphSet.prototype.get = function(index) { // this.glyphs[index] is 'undefined' when low memory mode is on. glyph is pushed on request only. if (this.glyphs[index] === undefined) { + if (typeof this.font._push !== 'function') return; + this.font._push(index); if (typeof this.glyphs[index] === 'function') { this.glyphs[index] = this.glyphs[index](); } let glyph = this.glyphs[index]; - let unicodeObj = this.font._IndexToUnicodeMap[index]; + let unicodeObj = this.font._IndexToUnicodeMap && this.font._IndexToUnicodeMap[index]; if (unicodeObj) { for (let j = 0; j < unicodeObj.unicodes.length; j++) diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 00000000..66d74326 --- /dev/null +++ b/src/logger.js @@ -0,0 +1,141 @@ +import { isBrowser } from './util.js'; + +/** + * @typedef {number} ErrorTypes + */ + +/** + * @enum {ErrorTypes} + */ +const ErrorTypes = { + ERROR: 1, + WARNING: 2, + DEPRECATED: 4, + ALL: 32767 +}; +Object.freeze && Object.freeze(ErrorTypes); + +/** + * @enum {ErrorStrings} + */ +const errorStrings = { + 1: 'ERROR', + 2: 'WARNING', + 4: 'DEPRECATED' +}; + +const logMethods = { + 1: 'error', + 2: 'warn', + 4: 'info' +}; + +/** + * @property {string} string - message string + * @property {keyof ErrorTypes} type - error type + */ +class Message { + constructor(string, type = ErrorTypes.ERROR) { + if (!errorStrings[type]) { + throw new Error( 'Invalid error type ' + type + ' for message: ' + string ); + } + + this.string = string; + this.type = type; + } + + toString() { + return errorStrings[this.type] + ': ' + this.string; + } +} + +class MessageLogger { + + constructor() { + this.logLevel = ErrorTypes.ALL; + this.throwLevel = ErrorTypes.ERROR; + this.ErrorTypes = ErrorTypes; + } + + /** + * Logs a message and fires the opentypejs:message Event. + * @property {String|Message} string + * @property {keyof ErrorTypes} type + * + * @returns {Message} + */ + add(stringOrMessage, type = ErrorTypes.ERROR) { + let message; + if (stringOrMessage instanceof Message) { + message = stringOrMessage; + type = message.type; + } else { + message = new Message(stringOrMessage, type); + } + + let doLog = !!(this.logLevel & type); + + if (isBrowser()) { + document.dispatchEvent( + new CustomEvent('opentypejs:message', { + detail: { + message, + logged: doLog, + logger: this.logLevel + } + }) + ); + } + + if (doLog) { + this.logMessage(message); + } + + return message; + } + + /** + * adds an array of messages + */ + adds(messageArray) { + for (let i = 0; i < messageArray.length; i++) { + this.add(messageArray[i]); + } + } + + /** + * Logs a message to the console or throws it, + * depending on the throwLevel setting. + * @param {Message} message + */ + logMessage(message) { + const type = message.type || ErrorTypes.ERROR; + const logMethod = console[logMethods[type] || 'log'] || console.log; + const logMessage = '[opentype.js] ' + message.toString(); + if ( this.throwLevel & type ) { + throw new Error(logMessage); + } + logMethod(logMessage); + } + + getLogLevel() { + return this.logLevel; + } + + setLogLevel(newLevel) { + this.logLevel = newLevel; + } + + getThrowLevel() { + return this.throwLevel; + } + + setThrowLevel(newLevel) { + this.throwLevel = newLevel; + } + +} + +const globalLogger = new MessageLogger(); + +export { ErrorTypes, Message, MessageLogger, globalLogger as logger }; \ No newline at end of file diff --git a/src/opentype.js b/src/opentype.js index 0fd00854..71f013b2 100644 --- a/src/opentype.js +++ b/src/opentype.js @@ -35,6 +35,9 @@ import os2 from './tables/os2.js'; import post from './tables/post.js'; import meta from './tables/meta.js'; import gasp from './tables/gasp.js'; +import { createDefaultNamesInfo } from './font.js'; +import { sizeOf } from './types.js'; +import { ErrorTypes, logger } from './logger.js'; /** * The opentype library. * @namespace opentype @@ -187,17 +190,18 @@ function parseWOFFTableEntries(data, numTables) { */ /** + * @param {opentype.Font} * @param {DataView} * @param {Object} * @return {TableData} */ -function uncompressTable(data, tableEntry) { +function uncompressTable(data, tableEntry) { if (tableEntry.compression === 'WOFF') { const inBuffer = new Uint8Array(data.buffer, tableEntry.offset + 2, tableEntry.compressedLength - 2); const outBuffer = new Uint8Array(tableEntry.length); inflate(inBuffer, outBuffer); if (outBuffer.byteLength !== tableEntry.length) { - throw new Error('Decompression error: ' + tableEntry.tag + ' decompressed length doesn\'t match recorded length'); + logger.add('Decompression error: ' + tableEntry.tag + ' decompressed length doesn\'t match recorded length'); } const view = new DataView(outBuffer.buffer, 0); @@ -249,16 +253,26 @@ function parseBuffer(buffer, opt={}) { } else if (flavor === 'OTTO') { font.outlinesFormat = 'cff'; } else { - throw new Error('Unsupported OpenType flavor ' + signature); + logger.add('Unsupported OpenType flavor ' + signature); } numTables = parse.getUShort(data, 12); tableEntries = parseWOFFTableEntries(data, numTables); } else if (signature === 'wOF2') { var issue = 'https://github.com/opentypejs/opentype.js/issues/183#issuecomment-1147228025'; - throw new Error('WOFF2 require an external decompressor library, see examples at: ' + issue); + logger.add('WOFF2 require an external decompressor library, see examples at: ' + issue); + } else if (signature.substring(0,2) === '%!') { + // https://adobe-type-tools.github.io/font-tech-notes/pdfs/T1_SPEC.pdf + // https://personal.math.ubc.ca/~cass/piscript/type1.pdf + logger.add('PostScript/PS1/T1/Adobe Type 1 fonts are not supported'); + } else if (data.buffer.byteLength > (3 * sizeOf.Card8() + sizeOf.OffSize()) && parse.getByte(data, 0) === 0x01) { + // this could be a CFF1 file, we will try to parse it like a CCF table below + // https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf + font.isCFFFont = true; + tableEntries.push({tag:'CFF ',offset:0}); + numTables = 1; } else { - throw new Error('Unsupported OpenType signature ' + signature); + logger.add('Unsupported OpenType signature ' + signature); } let cffTableEntry; @@ -393,9 +407,16 @@ function parseBuffer(buffer, opt={}) { } } - const nameTable = uncompressTable(data, nameTableEntry); - font.tables.name = _name.parse(nameTable.data, nameTable.offset, ltagTable); - font.names = font.tables.name; + if ( nameTableEntry ) { + const nameTable = uncompressTable(data, nameTableEntry); + font.tables.name = _name.parse(nameTable.data, nameTable.offset, ltagTable); + font.names = font.tables.name; + } else { + font.names = {}; + font.names.unicode = createDefaultNamesInfo({}); + font.names.macintosh = createDefaultNamesInfo({}); + font.names.windows = createDefaultNamesInfo({}); + } if (glyfTableEntry && locaTableEntry) { const shortVersion = indexToLocFormat === 0; @@ -410,12 +431,21 @@ function parseBuffer(buffer, opt={}) { const cffTable2 = uncompressTable(data, cff2TableEntry); cff.parse(cffTable2.data, cffTable2.offset, font, opt); } else { - throw new Error('Font doesn\'t contain TrueType, CFF or CFF2 outlines.'); + logger.add('Font doesn\'t contain TrueType, CFF or CFF2 outlines.'); } - const hmtxTable = uncompressTable(data, hmtxTableEntry); - hmtx.parse(font, hmtxTable.data, hmtxTable.offset, font.numberOfHMetrics, font.numGlyphs, font.glyphs, opt); - addGlyphNames(font, opt); + if (hmtxTableEntry) { + const hmtxTable = uncompressTable(data, hmtxTableEntry); + hmtx.parse(font, hmtxTable.data, hmtxTable.offset, font.numberOfHMetrics, font.numGlyphs, font.glyphs, opt); + } + + if (!font.tables.cmap) { + if (!font.isCFFFont) { + logger.add('Font doesn\'t contain required cmap table', ErrorTypes.WARNING); + } + } else { + addGlyphNames(font, opt); + } if (kernTableEntry) { const kernTable = uncompressTable(data, kernTableEntry); @@ -540,5 +570,6 @@ export { parse as _parse, parseBuffer as parse, load, - loadSync + loadSync, + ErrorTypes }; diff --git a/src/parse.js b/src/parse.js index d6770721..e0bd9cb4 100644 --- a/src/parse.js +++ b/src/parse.js @@ -37,15 +37,29 @@ function getFixed(dataView, offset) { return decimal + fraction / 65535; } +// Retrieve a string with a specific byte length from the DataView. +function getString(dataView, offset, length) { + let string = ''; + + if (!offset) { + offset = 0; + } + + if (length === undefined) { + length = dataView.byteLength - offset; + } + + for (let i = offset; i < offset + length; i += 1) { + string += String.fromCharCode(dataView.getInt8(i)); + } + + return string; +} + // Retrieve a 4-character tag from the DataView. // Tags are used to identify tables. function getTag(dataView, offset) { - let tag = ''; - for (let i = offset; i < offset + 4; i += 1) { - tag += String.fromCharCode(dataView.getInt8(i)); - } - - return tag; + return getString(dataView, offset, 4); } // Retrieve an offset from the DataView. @@ -721,6 +735,7 @@ export default { getUInt24, getULong, getFixed, + getString, getTag, getOffset, getBytes, diff --git a/src/tables/cff.js b/src/tables/cff.js index 51a82986..58aaad18 100755 --- a/src/tables/cff.js +++ b/src/tables/cff.js @@ -10,6 +10,8 @@ import glyphset from '../glyphset.js'; import parse from '../parse.js'; import Path from '../path.js'; import table from '../table.js'; +import { createDefaultNamesInfo } from '../font.js'; +import { logger, ErrorTypes } from '../logger.js'; // Custom equals function that can also check lists. function equals(a, b) { @@ -309,7 +311,7 @@ function interpretDict(dict, meta, strings) { } // Parse the CFF header. -function parseCFFHeader(data, start) { +function parseCFFHeader(data, start, isCFFFont) { const header = {}; header.formatMajor = parse.getCard8(data, start); header.formatMinor = parse.getCard8(data, start + 1); @@ -323,7 +325,7 @@ function parseCFFHeader(data, start) { if (header.formatMajor < 2) { header.offsetSize = parse.getCard8(data, start + 3); header.startOffset = start; - header.endOffset = start + 4; + header.endOffset = start + (isCFFFont ? header.size : 4); } else { header.topDictLength = parse.getCard16(data, start + 3); header.endOffset = start + 8; @@ -1101,17 +1103,17 @@ function parseCFFCharstring(font, glyph, code, version) { return p; } -function parseCFFFDSelect(data, start, nGlyphs, fdArrayCount, version) { +function parseCFFFDSelect(data, start, font, fdArrayCount, version) { const fdSelect = []; let fdIndex; const parser = new parse.Parser(data, start); const format = parser.parseCard8(); if (format === 0) { // Simple list of nGlyphs elements - for (let iGid = 0; iGid < nGlyphs; iGid++) { + for (let iGid = 0; iGid < font.nGlyphs; iGid++) { fdIndex = parser.parseCard8(); if (fdIndex >= fdArrayCount) { - throw new Error('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')'); + logger.add('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')'); } fdSelect.push(fdIndex); } @@ -1120,36 +1122,43 @@ function parseCFFFDSelect(data, start, nGlyphs, fdArrayCount, version) { const nRanges = format === 4 ? parser.parseULong() : parser.parseCard16(); let first = format === 4 ? parser.parseULong() : parser.parseCard16(); if (first !== 0) { - throw new Error(`CFF Table CID Font FDSelect format ${format} range has bad initial GID ${first}`); + logger.add(`CFF Table CID Font FDSelect format ${format} range has bad initial GID ${first}`); } let next; for (let iRange = 0; iRange < nRanges; iRange++) { fdIndex = format === 4 ? parser.parseUShort() : parser.parseCard8(); next = format === 4 ? parser.parseULong() : parser.parseCard16(); if (fdIndex >= fdArrayCount) { - throw new Error('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')'); + logger.add('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')'); } - if (next > nGlyphs) { - throw new Error(`CFF Table CID Font FDSelect format ${version} range has bad GID ${next}`); + if (next > font.nGlyphs) { + logger.add(`CFF Table CID Font FDSelect format ${version} range has bad GID ${next}`); } for (; first < next; first++) { fdSelect.push(fdIndex); } first = next; } - if (next !== nGlyphs) { - throw new Error('CFF Table CID Font FDSelect format 3 range has bad final (Sentinal) GID ' + next); + if (next !== font.nGlyphs) { + logger.add('CFF Table CID Font FDSelect format 3 range has bad final (Sentinel) GID ' + next, ErrorTypes.WARNING); } } else { - throw new Error('CFF Table CID Font FDSelect table has unsupported format ' + format); + logger.add('CFF Table CID Font FDSelect table has unsupported format ' + format); } + return fdSelect; } -// Parse the `CFF` table, which contains the glyph outlines in PostScript format. +/** + * Parse the `CFF` table, which contains the glyph outlines in PostScript format. + * @param {DataView} data + * @param {Number} start + * @param {Font} font + * @param {Object} opt + */ function parseCFFTable(data, start, font, opt) { let resultTable; - const header = parseCFFHeader(data, start); + const header = parseCFFHeader(data, start, !!font.isCFFFont); if (header.formatMajor === 2) { resultTable = font.tables.cff2 = {}; } else { @@ -1170,13 +1179,14 @@ function parseCFFTable(data, start, font, opt) { } else { const topDictArray = gatherCFFTopDicts(data, start, topDictIndex.objects, stringIndex.objects, header.formatMajor); if (topDictArray.length !== 1) { - throw new Error('CFF table has too many fonts in \'FontSet\' - count of fonts NameIndex.length = ' + topDictArray.length); + logger.add('CFF table has too many fonts in \'FontSet\' - count of fonts NameIndex.length = ' + topDictArray.length); } topDict = topDictArray[0]; } resultTable.topDict = topDict; + resultTable.nameIndex = nameIndex; if (topDict._privateDict) { font.defaultWidthX = topDict._privateDict.defaultWidthX; @@ -1187,11 +1197,22 @@ function parseCFFTable(data, start, font, opt) { font.isCIDFont = true; } + // CharStrings must be parsed before FDSelect, because we need the nGlyphs value for parsing FDSelect + // Offsets in the top dict are relative to the beginning of the CFF data, so add the CFF start offset. + let charStringsIndex; + if (opt.lowMemory) { + charStringsIndex = parseCFFIndexLowMemory(data, start + topDict.charStrings, header.formatMajor); + font.nGlyphs = charStringsIndex.offsets.length - (header.formatMajor > 1 ? 1 : 0); // number of elements is count + 1 + } else { + charStringsIndex = parseCFFIndex(data, start + topDict.charStrings, null, header.formatMajor); + font.nGlyphs = charStringsIndex.objects.length; + } + if ( header.formatMajor > 1 ) { let fdArrayIndexOffset = topDict.fdArray; let fdSelectOffset = topDict.fdSelect; if (!fdArrayIndexOffset) { - throw new Error('This is a CFF2 font, but FDArray information is missing'); + logger.add('This is a CFF2 font, but FDArray information is missing'); } const fdArrayIndex = parseCFFIndex(data, start + fdArrayIndexOffset, null, header.formatMajor); @@ -1199,21 +1220,21 @@ function parseCFFTable(data, start, font, opt) { const fdArray = gatherCFF2FontDicts(data, start, fdArrayIndex.objects); topDict._fdArray = fdArray; if (fdSelectOffset) { - topDict._fdSelect = parseCFFFDSelect(data, start + fdSelectOffset, font.numGlyphs, fdArray.length, header.formatMajor); + topDict._fdSelect = parseCFFFDSelect(data, start + fdSelectOffset, font, fdArray.length, header.formatMajor); } } else if (font.isCIDFont) { let fdArrayOffset = topDict.fdArray; let fdSelectOffset = topDict.fdSelect; if (fdArrayOffset === 0 || fdSelectOffset === 0) { - throw new Error('Font is marked as a CID font, but FDArray and/or FDSelect information is missing'); + logger.add('Font is marked as a CID font, but FDArray and/or FDSelect information is missing'); } fdArrayOffset += start; const fdArrayIndex = parseCFFIndex(data, fdArrayOffset); const fdArray = gatherCFFTopDicts(data, start, fdArrayIndex.objects, stringIndex.objects, header.formatMajor); topDict._fdArray = fdArray; fdSelectOffset += start; - topDict._fdSelect = parseCFFFDSelect(data, fdSelectOffset, font.numGlyphs, fdArray.length, header.formatMajor); + topDict._fdSelect = parseCFFFDSelect(data, fdSelectOffset, font, fdArray.length, header.formatMajor); } if (header.formatMajor < 2) { @@ -1233,18 +1254,8 @@ function parseCFFTable(data, start, font, opt) { } } - // Offsets in the top dict are relative to the beginning of the CFF data, so add the CFF start offset. - let charStringsIndex; - if (opt.lowMemory) { - charStringsIndex = parseCFFIndexLowMemory(data, start + topDict.charStrings, header.formatMajor); - font.nGlyphs = charStringsIndex.offsets.length - (header.formatMajor > 1 ? 1 : 0); // number of elements is count + 1 - } else { - charStringsIndex = parseCFFIndex(data, start + topDict.charStrings, null, header.formatMajor); - font.nGlyphs = charStringsIndex.objects.length; - } - if ( header.formatMajor > 1 && font.tables.maxp && font.nGlyphs !== font.tables.maxp.numGlyphs ) { - console.error(`Glyph count in the CFF2 table (${font.nGlyphs}) must correspond to the glyph count in the maxp table (${font.tables.maxp.numGlyphs})`); + logger.add(`Glyph count in the CFF2 table (${font.nGlyphs}) must correspond to the glyph count in the maxp table (${font.tables.maxp.numGlyphs})`, ErrorTypes.WARNING); } if (header.formatMajor < 2) { @@ -1280,6 +1291,27 @@ function parseCFFTable(data, start, font, opt) { const p = new parse.Parser(data, start + topDict.vstore); topDict._vstore = p.parseVariationStore(); } + + if (font.isCFFFont) { + logger.add('CFF Type1 fonts are not fully supported, but you can use this to extract glyph outlines and metadata for example.', ErrorTypes.WARNING); + const topDict = font.tables.cff.topDict; + const psName = font.tables.cff.nameIndex && font.tables.cff.nameIndex.objects.length && font.tables.cff.nameIndex.objects[0] || ''; + const metaData = { + copyright: topDict.copyright || topDict.notice, + fullName: topDict.fullName, + version: topDict.version, + postScriptName: psName + }; + font.names.unicode = createDefaultNamesInfo(metaData); + font.names.macintosh = createDefaultNamesInfo(metaData); + font.names.windows = createDefaultNamesInfo(metaData); + + const bBox = topDict.fontBBox; + const fMatrix = topDict.fontMatrix; + font.ascender = bBox && bBox.length > 2 && bBox[2] || 0; + font.descender = bBox && bBox.length > 1 && bBox[1] || 0; + font.unitsPerEm = fMatrix && fMatrix.length && (1/fMatrix[0]) || 1000; + } } // Convert a string to a String ID (SID). diff --git a/src/types.js b/src/types.js index d8e5e637..94ced906 100644 --- a/src/types.js +++ b/src/types.js @@ -2,6 +2,7 @@ // All OpenType fonts use Motorola-style byte ordering (Big Endian) import check from './check.js'; +import { logger } from './logger.js'; const LIMIT16 = 32768; // The limit at which a 16-bit number switches signs == 2^15 const LIMIT32 = 2147483648; // The limit at which a 32-bit number switches signs == 2 ^ 31 @@ -70,9 +71,9 @@ sizeOf.CHAR = constant(1); * @returns {Array} */ encode.CHARARRAY = function(v) { - if (typeof v === 'undefined') { + if (v == null) { // catches undefined and null v = ''; - console.warn('Undefined CHARARRAY encountered and treated as an empty string. This is probably caused by a missing glyph name.'); + logger.add('Undefined CHARARRAY encountered and treated as an empty string. This is probably caused by a missing glyph name.', logger.ErrorTypes.WARNING); } const b = []; for (let i = 0; i < v.length; i += 1) { @@ -183,7 +184,7 @@ sizeOf.LONG = constant(4); */ encode.FLOAT = function(v) { if (v > MAX_16_16 || v < MIN_16_16) { - throw new Error(`Value ${v} is outside the range of representable values in 16.16 format`); + logger.add(`Value ${v} is outside the range of representable values in 16.16 format`, logger.ErrorTypes.ERROR); } const fixedValue = Math.round(v * (1 << 16)) << 0; // Round to nearest multiple of 1/(1<<16) return encode.ULONG(fixedValue); @@ -856,7 +857,7 @@ encode.OPERAND = function(v, type) { d.push(enc1[j]); } } else { - throw new Error('Unknown operand type ' + type); + logger.add('Unknown operand type ' + type, logger.ErrorTypes.ERROR); // FIXME Add support for booleans } } From 30bb7d95ff77a78e26d687c537be2ea477b33747 Mon Sep 17 00:00:00 2001 From: Connum Date: Fri, 24 Nov 2023 21:07:46 +0100 Subject: [PATCH 2/9] remove accidentally committed temp file --- src/font_REMOTE_153.js | 666 ----------------------------------------- 1 file changed, 666 deletions(-) delete mode 100644 src/font_REMOTE_153.js diff --git a/src/font_REMOTE_153.js b/src/font_REMOTE_153.js deleted file mode 100644 index 6f94f615..00000000 --- a/src/font_REMOTE_153.js +++ /dev/null @@ -1,666 +0,0 @@ -// The Font object - -import Path from './path.js'; -import sfnt from './tables/sfnt.js'; -import { DefaultEncoding } from './encoding.js'; -import glyphset from './glyphset.js'; -import Position from './position.js'; -import Substitution from './substitution.js'; -import { isBrowser, checkArgument } from './util.js'; -import HintingTrueType from './hintingtt.js'; -import Bidi from './bidi.js'; -import validation from './validation.js'; - -function createDefaultNamesInfo(options) { - return { - fontFamily: {en: options.familyName || ' '}, - fontSubfamily: {en: options.styleName || ' '}, - fullName: {en: options.fullName || options.familyName + ' ' + options.styleName}, - // postScriptName may not contain any whitespace - postScriptName: {en: options.postScriptName || (options.familyName + options.styleName).replace(/\s/g, '')}, - designer: {en: options.designer || ' '}, - designerURL: {en: options.designerURL || ' '}, - manufacturer: {en: options.manufacturer || ' '}, - manufacturerURL: {en: options.manufacturerURL || ' '}, - license: {en: options.license || ' '}, - licenseURL: {en: options.licenseURL || ' '}, - version: {en: options.version || 'Version 0.1'}, - description: {en: options.description || ' '}, - copyright: {en: options.copyright || ' '}, - trademark: {en: options.trademark || ' '} - }; -} - -/** - * @typedef FontOptions - * @type Object - * @property {Boolean} empty - whether to create a new empty font - * @property {string} familyName - * @property {string} styleName - * @property {string=} fullName - * @property {string=} postScriptName - * @property {string=} designer - * @property {string=} designerURL - * @property {string=} manufacturer - * @property {string=} manufacturerURL - * @property {string=} license - * @property {string=} licenseURL - * @property {string=} version - * @property {string=} description - * @property {string=} copyright - * @property {string=} trademark - * @property {Number} unitsPerEm - * @property {Number} ascender - * @property {Number} descender - * @property {Number} createdTimestamp - * @property {Number} weightClass - * @property {Number} italicAngle - * @property {string=} widthClass - * @property {string=} fsSelection - */ - -/** - * 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. - * @exports opentype.Font - * @class - * @param {FontOptions} - * @constructor - */ -function Font(options) { - options = options || {}; - options.tables = options.tables || {}; - - if (!options.empty) { - // Check that we've provided the minimum set of names. - checkArgument(options.familyName, 'When creating a new Font object, familyName is required.'); - checkArgument(options.styleName, 'When creating a new Font object, styleName is required.'); - checkArgument(options.unitsPerEm, 'When creating a new Font object, unitsPerEm is required.'); - checkArgument(options.ascender, 'When creating a new Font object, ascender is required.'); - checkArgument(options.descender <= 0, 'When creating a new Font object, negative descender value is required.'); - - // OS X will complain if the names are empty, so we put a single space everywhere by default. - this.names = {}; - this.names.unicode = createDefaultNamesInfo(options); - this.names.macintosh = createDefaultNamesInfo(options); - this.names.windows = createDefaultNamesInfo(options); - this.unitsPerEm = options.unitsPerEm || 1000; - this.ascender = options.ascender; - this.descender = options.descender; - this.createdTimestamp = options.createdTimestamp; - this.italicAngle = options.italicAngle || 0; - this.weightClass = options.weightClass || 0; - - let selection = 0; - if (options.fsSelection) { - selection = options.fsSelection; - } else { - if (this.italicAngle < 0) { - selection |= this.fsSelectionValues.ITALIC; - } else if (this.italicAngle > 0) { - selection |= this.fsSelectionValues.OBLIQUE; - } - if (this.weightClass >= 600) { - selection |= this.fsSelectionValues.BOLD; - } - if (selection == 0) { - selection = this.fsSelectionValues.REGULAR; - } - } - - if (!options.panose || !Array.isArray(options.panose)) { - options.panose = [0, 0, 0, 0, 0, 0, 0, 0, 0]; - } - - this.tables = Object.assign(options.tables, { - os2: Object.assign({ - usWeightClass: options.weightClass || this.usWeightClasses.MEDIUM, - usWidthClass: options.widthClass || this.usWidthClasses.MEDIUM, - bFamilyType: options.panose[0] || 0, - bSerifStyle: options.panose[1] || 0, - bWeight: options.panose[2] || 0, - bProportion: options.panose[3] || 0, - bContrast: options.panose[4] || 0, - bStrokeVariation: options.panose[5] || 0, - bArmStyle: options.panose[6] || 0, - bLetterform: options.panose[7] || 0, - bMidline: options.panose[8] || 0, - bXHeight: options.panose[9] || 0, - fsSelection: selection, - }, options.tables.os2) - }); - } - - this.supported = true; // Deprecated: parseBuffer will throw an error if font is not supported. - this.glyphs = new glyphset.GlyphSet(this, options.glyphs || []); - this.encoding = new DefaultEncoding(this); - this.position = new Position(this); - this.substitution = new Substitution(this); - this.tables = this.tables || {}; - - // needed for low memory mode only. - this._push = null; - this._hmtxTableData = {}; - - Object.defineProperty(this, 'hinting', { - get: function() { - if (this._hinting) return this._hinting; - if (this.outlinesFormat === 'truetype') { - return (this._hinting = new HintingTrueType(this)); - } - return null; - } - }); -} - -/** - * Check if the font has a glyph for the given character. - * @param {string} - * @return {Boolean} - */ -Font.prototype.hasChar = function(c) { - return this.encoding.charToGlyphIndex(c) > 0; -}; - -/** - * Convert the given character to a single glyph index. - * 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. - * @param {string} - * @return {Number} - */ -Font.prototype.charToGlyphIndex = function(s) { - return this.encoding.charToGlyphIndex(s); -}; - -/** - * Convert the given character to a single Glyph object. - * 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. - * @param {string} - * @return {opentype.Glyph} - */ -Font.prototype.charToGlyph = function(c) { - const glyphIndex = this.charToGlyphIndex(c); - let glyph = this.glyphs.get(glyphIndex); - if (!glyph) { - // .notdef - glyph = this.glyphs.get(0); - } - - return glyph; -}; - -/** - * Update features - * @param {any} options features options - */ -Font.prototype.updateFeatures = function (options) { - // TODO: update all features options not only 'latn'. - return this.defaultRenderOptions.features.map(feature => { - if (feature.script === 'latn') { - return { - script: 'latn', - tags: feature.tags.filter(tag => options[tag]) - }; - } else { - return feature; - } - }); -}; - -/** - * Convert the given text to a list of Glyph indexes. - * Note that there is no strict one-to-one mapping between characters and - * glyphs, so the list of returned glyph indexes can be larger or smaller than the - * length of the given string. - * @param {string} - * @param {GlyphRenderOptions} [options] - * @return {number[]} - */ -Font.prototype.stringToGlyphIndexes = function(s, options) { - const bidi = new Bidi(); - - // Create and register 'glyphIndex' state modifier - const charToGlyphIndexMod = token => this.charToGlyphIndex(token.char); - bidi.registerModifier('glyphIndex', null, charToGlyphIndexMod); - - // roll-back to default features - let features = options ? - this.updateFeatures(options.features) : - this.defaultRenderOptions.features; - - bidi.applyFeatures(this, features); - - return bidi.getTextGlyphs(s); -}; - -/** - * Convert the given text to a list of Glyph objects. - * Note that there is no strict one-to-one mapping between characters and - * glyphs, so the list of returned glyphs can be larger or smaller than the - * length of the given string. - * @param {string} - * @param {GlyphRenderOptions} [options] - * @return {opentype.Glyph[]} - */ -Font.prototype.stringToGlyphs = function(s, options) { - const indexes = this.stringToGlyphIndexes(s, options); - - let length = indexes.length; - - // convert glyph indexes to glyph objects - const glyphs = new Array(length); - const notdef = this.glyphs.get(0); - for (let i = 0; i < length; i += 1) { - glyphs[i] = this.glyphs.get(indexes[i]) || notdef; - } - return glyphs; -}; - -/** - * @param {string} - * @return {Number} - */ -Font.prototype.nameToGlyphIndex = function(name) { - return this.glyphNames.nameToGlyphIndex(name); -}; - -/** - * @param {string} - * @return {opentype.Glyph} - */ -Font.prototype.nameToGlyph = function(name) { - const glyphIndex = this.nameToGlyphIndex(name); - let glyph = this.glyphs.get(glyphIndex); - if (!glyph) { - // .notdef - glyph = this.glyphs.get(0); - } - - return glyph; -}; - -/** - * @param {Number} - * @return {String} - */ -Font.prototype.glyphIndexToName = function(gid) { - if (!this.glyphNames.glyphIndexToName) { - return ''; - } - - return this.glyphNames.glyphIndexToName(gid); -}; - -/** - * Retrieve the value of the kerning pair 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. - * For GPOS kerning, this method uses the default script and language, which covers - * most use cases. To have greater control, use font.position.getKerningValue . - * @param {opentype.Glyph} leftGlyph - * @param {opentype.Glyph} rightGlyph - * @return {Number} - */ -Font.prototype.getKerningValue = function(leftGlyph, rightGlyph) { - leftGlyph = leftGlyph.index || leftGlyph; - rightGlyph = rightGlyph.index || rightGlyph; - const gposKerning = this.position.defaultKerningTables; - if (gposKerning) { - return this.position.getKerningValue(gposKerning, leftGlyph, rightGlyph); - } - // "kern" table - return this.kerningPairs[leftGlyph + ',' + rightGlyph] || 0; -}; - -/** - * @typedef GlyphRenderOptions - * @type Object - * @property {string} [script] - script used to determine which features to apply. By default, 'DFLT' or 'latn' is used. - * See https://www.microsoft.com/typography/otspec/scripttags.htm - * @property {string} [language='dflt'] - language system used to determine which features to apply. - * See https://www.microsoft.com/typography/developers/opentype/languagetags.aspx - * @property {boolean} [kerning=true] - whether to include kerning values - * @property {object} [features] - OpenType Layout feature tags. Used to enable or disable the features of the given script/language system. - * See https://www.microsoft.com/typography/otspec/featuretags.htm - */ -Font.prototype.defaultRenderOptions = { - kerning: true, - features: [ - /** - * these 4 features are required to render Arabic text properly - * and shouldn't be turned off when rendering arabic text. - */ - { script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] }, - { script: 'latn', tags: ['liga', 'rlig'] }, - { script: 'thai', tags: ['liga', 'rlig', 'ccmp'] }, - ] -}; - -/** - * Helper function that invokes the given callback for each glyph in the given text. - * The callback gets `(glyph, x, y, fontSize, options)`.* @param {string} text - * @param {string} text - The text to apply. - * @param {number} [x=0] - Horizontal position of the beginning of the text. - * @param {number} [y=0] - Vertical position of the *baseline* of the text. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - * @param {Function} callback - */ -Font.prototype.forEachGlyph = function(text, x, y, fontSize, options, callback) { - x = x !== undefined ? x : 0; - y = y !== undefined ? y : 0; - fontSize = fontSize !== undefined ? fontSize : 72; - options = Object.assign({}, this.defaultRenderOptions, options); - const fontScale = 1 / this.unitsPerEm * fontSize; - const glyphs = this.stringToGlyphs(text, options); - let kerningLookups; - if (options.kerning) { - const script = options.script || this.position.getDefaultScriptName(); - kerningLookups = this.position.getKerningTables(script, options.language); - } - for (let i = 0; i < glyphs.length; i += 1) { - const glyph = glyphs[i]; - callback.call(this, glyph, x, y, fontSize, options); - if (glyph.advanceWidth) { - x += glyph.advanceWidth * fontScale; - } - - if (options.kerning && i < glyphs.length - 1) { - // We should apply position adjustment lookups in a more generic way. - // Here we only use the xAdvance value. - const kerningValue = kerningLookups ? - this.position.getKerningValue(kerningLookups, glyph.index, glyphs[i + 1].index) : - this.getKerningValue(glyph, glyphs[i + 1]); - x += kerningValue * fontScale; - } - - if (options.letterSpacing) { - x += options.letterSpacing * fontSize; - } else if (options.tracking) { - x += (options.tracking / 1000) * fontSize; - } - } - return x; -}; - -/** - * Create a Path object that represents the given text. - * @param {string} text - The text to create. - * @param {number} [x=0] - Horizontal position of the beginning of the text. - * @param {number} [y=0] - Vertical position of the *baseline* of the text. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - * @return {opentype.Path} - */ -Font.prototype.getPath = function(text, x, y, fontSize, options) { - const fullPath = new Path(); - this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { - const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this); - fullPath.extend(glyphPath); - }); - return fullPath; -}; - -/** - * Create an array of Path objects that represent the glyphs of a given text. - * @param {string} text - The text to create. - * @param {number} [x=0] - Horizontal position of the beginning of the text. - * @param {number} [y=0] - Vertical position of the *baseline* of the text. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - * @return {opentype.Path[]} - */ -Font.prototype.getPaths = function(text, x, y, fontSize, options) { - const glyphPaths = []; - this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { - const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this); - glyphPaths.push(glyphPath); - }); - - return glyphPaths; -}; - -/** - * Returns the advance width of a text. - * - * This is something different than Path.getBoundingBox() as 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 - * - * @param {string} text - The text to create. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - * @return advance width - */ -Font.prototype.getAdvanceWidth = function(text, fontSize, options) { - return this.forEachGlyph(text, 0, 0, fontSize, options, function() {}); -}; - -/** - * Draw the text on the given drawing context. - * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. - * @param {string} text - The text to create. - * @param {number} [x=0] - Horizontal position of the beginning of the text. - * @param {number} [y=0] - Vertical position of the *baseline* of the text. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - */ -Font.prototype.draw = function(ctx, text, x, y, fontSize, options) { - this.getPath(text, x, y, fontSize, options).draw(ctx); -}; - -/** - * 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. - * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. - * @param {string} text - The text to create. - * @param {number} [x=0] - Horizontal position of the beginning of the text. - * @param {number} [y=0] - Vertical position of the *baseline* of the text. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - */ -Font.prototype.drawPoints = function(ctx, text, x, y, fontSize, options) { - this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { - glyph.drawPoints(ctx, gX, gY, gFontSize); - }); -}; - -/** - * 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. - * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. - * @param {string} text - The text to create. - * @param {number} [x=0] - Horizontal position of the beginning of the text. - * @param {number} [y=0] - Vertical position of the *baseline* of the text. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - */ -Font.prototype.drawMetrics = function(ctx, text, x, y, fontSize, options) { - this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { - glyph.drawMetrics(ctx, gX, gY, gFontSize); - }); -}; - -/** - * @param {string} - * @return {string} - */ -Font.prototype.getEnglishName = function(name) { - const translations = (this.names.unicode || this.names.macintosh || this.names.windows)[name]; - if (translations) { - return translations.en; - } -}; - -/** - * Validate - */ -Font.prototype.validation = new validation.MessageStack(); -Font.prototype.validate = function() { - const validationMessages = []; - const _this = this; - - function assert(predicate, message) { - if (!predicate) { - validationMessages.push(_this.validation.add(message, validation.errorTypes.WARNING)); - } - } - - function assertNamePresent(name) { - const englishName = _this.getEnglishName(name); - assert(englishName && englishName.trim().length > 0, - 'No English ' + name + ' specified.'); - } - - // Identification information - assertNamePresent('fontFamily'); - assertNamePresent('weightName'); - assertNamePresent('manufacturer'); - assertNamePresent('copyright'); - assertNamePresent('version'); - - // Dimension information - assert(this.unitsPerEm > 0, 'No unitsPerEm specified.'); - - this.validation.logMessages(); - - return validationMessages; -}; - -/** - * Convert the font object to a SFNT data structure. - * This structure contains all the necessary tables and metadata to create a binary OTF file. - * @return {opentype.Table} - */ -Font.prototype.toTables = function() { - return sfnt.fontToTable(this); -}; -/** - * @deprecated Font.toBuffer is deprecated. Use Font.toArrayBuffer instead. - */ -Font.prototype.toBuffer = function() { - this.validation.add('Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.', validation.errorTypes.DEPRECATED); - return this.toArrayBuffer(); -}; -/** - * Converts a `opentype.Font` into an `ArrayBuffer` - * @return {ArrayBuffer} - */ -Font.prototype.toArrayBuffer = function() { - const sfntTable = this.toTables(); - const bytes = sfntTable.encode(); - const buffer = new ArrayBuffer(bytes.length); - const intArray = new Uint8Array(buffer); - for (let i = 0; i < bytes.length; i++) { - intArray[i] = bytes[i]; - } - - return buffer; -}; - -/** - * Initiate a download of the OpenType font. - */ -Font.prototype.download = function(fileName) { - const familyName = this.getEnglishName('fontFamily'); - const styleName = this.getEnglishName('fontSubfamily'); - fileName = fileName || familyName.replace(/\s/g, '') + '-' + styleName + '.otf'; - const arrayBuffer = this.toArrayBuffer(); - - if (isBrowser()) { - window.URL = window.URL || window.webkitURL; - - if (window.URL) { - const dataView = new DataView(arrayBuffer); - const blob = new Blob([dataView], {type: 'font/opentype'}); - - let link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = fileName; - - let event = document.createEvent('MouseEvents'); - event.initEvent('click', true, false); - link.dispatchEvent(event); - } else { - validation.add('Font file could not be downloaded. Try using a different browser.'); - } - } else { - const fs = require('fs'); - const buffer = Buffer.alloc(arrayBuffer.byteLength); - const view = new Uint8Array(arrayBuffer); - for (let i = 0; i < buffer.length; ++i) { - buffer[i] = view[i]; - } - fs.writeFileSync(fileName, buffer); - } -}; - -/** - * @private - */ -Font.prototype.fsSelectionValues = { - ITALIC: 0x001, //1 - UNDERSCORE: 0x002, //2 - NEGATIVE: 0x004, //4 - OUTLINED: 0x008, //8 - STRIKEOUT: 0x010, //16 - BOLD: 0x020, //32 - REGULAR: 0x040, //64 - USER_TYPO_METRICS: 0x080, //128 - WWS: 0x100, //256 - OBLIQUE: 0x200 //512 -}; - -/** - * @private - */ -Font.prototype.macStyleValues = { - BOLD: 0x001, //1 - ITALIC: 0x002, //2 - UNDERLINE: 0x004, //4 - OUTLINED: 0x008, //8 - SHADOW: 0x010, //16 - CONDENSED: 0x020, //32 - EXTENDED: 0x040, //64 -}; - -/** - * @private - */ -Font.prototype.usWidthClasses = { - ULTRA_CONDENSED: 1, - EXTRA_CONDENSED: 2, - CONDENSED: 3, - SEMI_CONDENSED: 4, - MEDIUM: 5, - SEMI_EXPANDED: 6, - EXPANDED: 7, - EXTRA_EXPANDED: 8, - ULTRA_EXPANDED: 9 -}; - -/** - * @private - */ -Font.prototype.usWeightClasses = { - THIN: 100, - EXTRA_LIGHT: 200, - LIGHT: 300, - NORMAL: 400, - MEDIUM: 500, - SEMI_BOLD: 600, - BOLD: 700, - EXTRA_BOLD: 800, - BLACK: 900 -}; - -export default Font; From 347b79215fccbdc93f5cd1c7bae0f9866197bf08 Mon Sep 17 00:00:00 2001 From: Connum Date: Fri, 24 Nov 2023 21:19:17 +0100 Subject: [PATCH 3/9] catch another error that can occur with subset font files --- src/encoding.js | 4 +++- src/glyphset.js | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/encoding.js b/src/encoding.js index 865fad9f..eed40ba1 100644 --- a/src/encoding.js +++ b/src/encoding.js @@ -252,7 +252,9 @@ function addGlyphNamesAll(font) { const c = charCodes[i]; const glyphIndex = glyphIndexMap[c]; glyph = font.glyphs.get(glyphIndex); - glyph.addUnicode(parseInt(c)); + if(glyph) { + glyph.addUnicode(parseInt(c)); + } } for (let i = 0; i < font.glyphs.length; i += 1) { diff --git a/src/glyphset.js b/src/glyphset.js index 1f889fba..f70d466c 100644 --- a/src/glyphset.js +++ b/src/glyphset.js @@ -1,6 +1,7 @@ // The GlyphSet object import Glyph from './glyph.js'; +import { logger } from './logger.js'; // Define a property on the glyph that depends on the path being loaded. function defineDependentProperty(glyph, externalName, internalName) { @@ -63,7 +64,12 @@ if(typeof Symbol !== 'undefined' && Symbol.iterator) { GlyphSet.prototype.get = function(index) { // this.glyphs[index] is 'undefined' when low memory mode is on. glyph is pushed on request only. if (this.glyphs[index] === undefined) { - if (typeof this.font._push !== 'function') return; + if (typeof this.font._push !== 'function') { + if (index !== null) { + logger.add(`Trying to access unknown glyph at index ${index}`, logger.ErrorTypes.WARNING); + } + return; + } this.font._push(index); if (typeof this.glyphs[index] === 'function') { From 517f2f099aa8dfda47aba5fce13667ed549e8b69 Mon Sep 17 00:00:00 2001 From: Connum Date: Fri, 24 Nov 2023 21:30:49 +0100 Subject: [PATCH 4/9] make message event cancelable --- src/logger.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/logger.js b/src/logger.js index 66d74326..3dfe110e 100644 --- a/src/logger.js +++ b/src/logger.js @@ -76,15 +76,18 @@ class MessageLogger { let doLog = !!(this.logLevel & type); if (isBrowser()) { - document.dispatchEvent( - new CustomEvent('opentypejs:message', { - detail: { - message, - logged: doLog, - logger: this.logLevel - } - }) - ); + const messageEvent = new CustomEvent('opentypejs:message', { + cancelable: true, + detail: { + message, + doLog: doLog, + logger: this.logLevel + } + }); + const cancelled = document.dispatchEvent(messageEvent); + if (cancelled) { + doLog = false; + } } if (doLog) { From f9dc7cc787293dbfc85a85808506a6e81ac4c6e2 Mon Sep 17 00:00:00 2001 From: Connum Date: Fri, 24 Nov 2023 17:14:14 +0100 Subject: [PATCH 5/9] implement graceful loading via logging severity levels --- docs/font-inspector.html | 17 +- docs/glyph-inspector.html | 64 +++- docs/index.html | 17 +- docs/site.css | 21 +- src/font.js | 19 +- src/font_REMOTE_153.js | 666 ++++++++++++++++++++++++++++++++++++++ src/glyphset.js | 5 +- src/logger.js | 141 ++++++++ src/opentype.js | 57 +++- src/parse.js | 27 +- src/tables/cff.js | 92 ++++-- src/types.js | 9 +- 12 files changed, 1050 insertions(+), 85 deletions(-) create mode 100644 src/font_REMOTE_153.js create mode 100644 src/logger.js diff --git a/docs/font-inspector.html b/docs/font-inspector.html index 52e1ea14..dea8fd6a 100644 --- a/docs/font-inspector.html +++ b/docs/font-inspector.html @@ -144,12 +144,24 @@

Free Software

var el = document.getElementById('message'); if (!message || message.trim().length === 0) { el.style.display = 'none'; + el.innerHTML = ''; } else { el.style.display = 'block'; + el.innerHTML = `

${message}

`; } - el.innerHTML = message; } +function appendErrorMessage(message, type) { + var el = document.getElementById('message'); + el.style.display = 'block'; + el.innerHTML += `

${message}

`; +} + +document.addEventListener('opentypejs:message', function(event) { + const message = event.detail.message; + appendErrorMessage(message.toString(), message.type); +}); + function sortKeys(dict) { var keys = []; for (var key in dict) { @@ -257,10 +269,11 @@

Free Software

} try { const data = await file.arrayBuffer(); - onFontLoaded(opentype.parse(isWoff2 ? Module.decompress(data) : data)); showErrorMessage(''); + onFontLoaded(opentype.parse(isWoff2 ? Module.decompress(data) : data)); } catch (err) { showErrorMessage(err.toString()); + throw err; } } form.file.onchange = function(e) { diff --git a/docs/glyph-inspector.html b/docs/glyph-inspector.html index 60732306..dd090554 100644 --- a/docs/glyph-inspector.html +++ b/docs/glyph-inspector.html @@ -92,12 +92,24 @@

Free Software

var el = document.getElementById('message'); if (!message || message.trim().length === 0) { el.style.display = 'none'; + el.innerHTML = ''; } else { el.style.display = 'block'; + el.innerHTML = `

${message}

`; } - el.innerHTML = message; } +function appendErrorMessage(message, type) { + var el = document.getElementById('message'); + el.style.display = 'block'; + el.innerHTML += `

${message}

`; +} + +document.addEventListener('opentypejs:message', function(event) { + const message = event.detail.message; + appendErrorMessage(message.toString(), message.type); +}); + function pathCommandToString(cmd) { var str = '' + cmd.type + ' ' + ((cmd.x !== undefined) ? 'x='+cmd.x+' y='+cmd.y+' ' : '') + @@ -273,13 +285,19 @@

Free Software

var cellMarkSize = 4; var ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, cellWidth, cellHeight); - if (glyphIndex >= window.font.numGlyphs) return; + const nGlyphs = window.font.numGlyphs || window.font.nGlyphs; + if (glyphIndex >= nGlyphs) return; ctx.fillStyle = '#606060'; ctx.font = '9px sans-serif'; ctx.fillText(glyphIndex, 1, cellHeight-1); - var glyph = window.font.glyphs.get(glyphIndex), - glyphWidth = glyph.advanceWidth * fontScale, + const glyph = window.font.glyphs.get(glyphIndex); + if (!glyph.advanceWidth) { + // force calculation of path data + glyph.getPath(); + } + const advanceWidth = glyph.advanceWidth; + let glyphWidth = glyph.advanceWidth * fontScale, xmin = (cellWidth - glyphWidth)/2, xmax = (cellWidth + glyphWidth)/2, x0 = xmin; @@ -314,7 +332,7 @@

Free Software

h = glyphBgCanvas.height / pixelRatio, glyphW = w - glyphMargin*2, glyphH = h - glyphMargin*2, - head = font.tables.head, + head = getFontDimensions(font), maxHeight = head.yMax - head.yMin, ctx = glyphBgCanvas.getContext('2d'); @@ -331,12 +349,23 @@

Free Software

ctx.clearRect(0, 0, w, h); ctx.fillStyle = '#a0a0a0'; hline('Baseline', 0); - hline('yMax', font.tables.head.yMax); - hline('yMin', font.tables.head.yMin); - hline('Ascender', font.tables.hhea.ascender); - hline('Descender', font.tables.hhea.descender); - hline('Typo Ascender', font.tables.os2.sTypoAscender); - hline('Typo Descender', font.tables.os2.sTypoDescender); + hline('yMax', head.yMax); + hline('yMin', head.yMin); + hline('Ascender', font.tables.hhea ? font.tables.hhea.ascender : font.ascender || head.yMax); + hline('Descender', font.tables.hhea ? font.tables.hhea.descender : font.descender || head.yMin); + if (font.tables.os2) { + hline('Typo Ascender', font.tables.os2.sTypoAscender); + hline('Typo Descender', font.tables.os2.sTypoDescender); + } +} + +function getFontDimensions(font) { + return font.isCFFFont ? { + xMin: font.tables.cff.topDict.fontBBox[0], + xMax: font.tables.cff.topDict.fontBBox[3] || 1000, + yMin: font.tables.cff.topDict.fontBBox[1] || -200, + yMax: font.tables.cff.topDict.fontBBox[2] || 1000 + } :font.tables.head; } function onFontLoaded(font) { @@ -344,7 +373,7 @@

Free Software

var w = cellWidth - cellMarginLeftRight * 2, h = cellHeight - cellMarginTop - cellMarginBottom, - head = font.tables.head, + head = getFontDimensions(font), maxHeight = head.yMax - head.yMin; fontScale = Math.min(w/(head.xMax - head.xMin), h/maxHeight); fontSize = fontScale * font.unitsPerEm; @@ -353,10 +382,11 @@

Free Software

var pagination = document.getElementById("pagination"); pagination.innerHTML = ''; var fragment = document.createDocumentFragment(); - var numPages = Math.ceil(font.numGlyphs / cellCount); + const nGlyphs = font.numGlyphs || font.nGlyphs; + var numPages = Math.ceil(nGlyphs / cellCount); for(var i = 0; i < numPages; i++) { var link = document.createElement('span'); - var lastIndex = Math.min(font.numGlyphs-1, (i+1)*cellCount-1); + var lastIndex = Math.min(nGlyphs-1, (i+1)*cellCount-1); link.textContent = i*cellCount + '-' + lastIndex; link.id = 'p' + i; link.addEventListener('click', pageSelect, false); @@ -378,7 +408,8 @@

Free Software

var firstGlyphIndex = pageSelected*cellCount, cellIndex = +event.target.id.substr(1), glyphIndex = firstGlyphIndex + cellIndex; - if (glyphIndex < window.font.numGlyphs) { + const nGlyphs = window.font.numGlyphs || window.font.nGlyphs; + if (glyphIndex < nGlyphs) { displayGlyph(glyphIndex); displayGlyphData(glyphIndex); } @@ -410,10 +441,11 @@

Free Software

} try { const data = await file.arrayBuffer(); - onFontLoaded(opentype.parse(isWoff2 ? Module.decompress(data) : data)); showErrorMessage(''); + onFontLoaded(opentype.parse(isWoff2 ? Module.decompress(data) : data)); } catch (err) { showErrorMessage(err.toString()); + throw err; } } diff --git a/docs/index.html b/docs/index.html index ed92e27e..34702b0a 100755 --- a/docs/index.html +++ b/docs/index.html @@ -169,12 +169,24 @@

Free Software

var el = document.getElementById('message'); if (!message || message.trim().length === 0) { el.style.display = 'none'; + el.innerHTML = ''; } else { el.style.display = 'block'; + el.innerHTML = `

${message}

`; } - el.innerHTML = message; } +function appendErrorMessage(message, type) { + var el = document.getElementById('message'); + el.style.display = 'block'; + el.innerHTML += `

${message}

`; +} + +document.addEventListener('opentypejs:message', function(event) { + const message = event.detail.message; + appendErrorMessage(message.toString(), message.type); +}); + function onFontLoaded(font) { window.font = font; @@ -217,10 +229,11 @@

Free Software

} try { const data = await file.arrayBuffer(); - onFontLoaded(opentype.parse(isWoff2 ? Module.decompress(data) : data)); showErrorMessage(''); + onFontLoaded(opentype.parse(isWoff2 ? Module.decompress(data) : data)); } catch (err) { showErrorMessage(err.toString()); + throw err; } } diff --git a/docs/site.css b/docs/site.css index 07693892..be42a0a7 100644 --- a/docs/site.css +++ b/docs/site.css @@ -107,14 +107,27 @@ canvas.text { #message { position: relative; - top: -3px; - background: red; color: white; - padding: 1px 5px; font-weight: bold; - border-radius: 2px; display: none; clear: both; + padding-top: 1px; +} + +#message p { + margin: 2px 0; + padding: 2px 5px; + border-radius: 0.25rem; + border: solid 1px; + background: #fff3cd; + color: #856404; + border-color: #ffeeba; +} + +#message p.message-type-1 { + background: #f8d7da; + color: #721c24; + border-color: #f5c6cb; } .message { diff --git a/src/font.js b/src/font.js index 09d2dddc..af1e7a15 100644 --- a/src/font.js +++ b/src/font.js @@ -9,14 +9,15 @@ import Substitution from './substitution.js'; import { isBrowser, checkArgument } from './util.js'; import HintingTrueType from './hintingtt.js'; import Bidi from './bidi.js'; +import { logger, ErrorTypes, MessageLogger } from './logger.js'; function createDefaultNamesInfo(options) { return { fontFamily: {en: options.familyName || ' '}, fontSubfamily: {en: options.styleName || ' '}, - fullName: {en: options.fullName || options.familyName + ' ' + options.styleName}, + fullName: {en: options.fullName || (options.familyName || '') + ' ' + (options.styleName || '')}, // postScriptName may not contain any whitespace - postScriptName: {en: options.postScriptName || (options.familyName + options.styleName).replace(/\s/g, '')}, + postScriptName: {en: options.postScriptName || ((options.familyName || '') + (options.styleName || '')).replace(/\s/g, '')}, designer: {en: options.designer || ' '}, designerURL: {en: options.designerURL || ' '}, manufacturer: {en: options.manufacturer || ' '}, @@ -502,14 +503,17 @@ Font.prototype.getEnglishName = function(name) { /** * Validate + * @type {MessageLogger} */ +Font.prototype.validation = new MessageLogger(); +Font.prototype.ErrorTypes = ErrorTypes; Font.prototype.validate = function() { - const warnings = []; + const validationMessages = []; const _this = this; function assert(predicate, message) { if (!predicate) { - warnings.push(message); + validationMessages.push(_this.validation.add(message, _this.ErrorTypes.WARNING)); } } @@ -528,6 +532,8 @@ Font.prototype.validate = function() { // Dimension information assert(this.unitsPerEm > 0, 'No unitsPerEm specified.'); + + return validationMessages; }; /** @@ -542,7 +548,7 @@ Font.prototype.toTables = function() { * @deprecated Font.toBuffer is deprecated. Use Font.toArrayBuffer instead. */ Font.prototype.toBuffer = function() { - console.warn('Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.'); + logger.add('Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.', this.ErrorTypes.DEPRECATED); return this.toArrayBuffer(); }; /** @@ -585,7 +591,7 @@ Font.prototype.download = function(fileName) { event.initEvent('click', true, false); link.dispatchEvent(event); } else { - console.warn('Font file could not be downloaded. Try using a different browser.'); + logger.add('Font file could not be downloaded. Try using a different browser.'); } } else { const fs = require('fs'); @@ -658,3 +664,4 @@ Font.prototype.usWeightClasses = { }; export default Font; +export { createDefaultNamesInfo }; \ No newline at end of file diff --git a/src/font_REMOTE_153.js b/src/font_REMOTE_153.js new file mode 100644 index 00000000..6f94f615 --- /dev/null +++ b/src/font_REMOTE_153.js @@ -0,0 +1,666 @@ +// The Font object + +import Path from './path.js'; +import sfnt from './tables/sfnt.js'; +import { DefaultEncoding } from './encoding.js'; +import glyphset from './glyphset.js'; +import Position from './position.js'; +import Substitution from './substitution.js'; +import { isBrowser, checkArgument } from './util.js'; +import HintingTrueType from './hintingtt.js'; +import Bidi from './bidi.js'; +import validation from './validation.js'; + +function createDefaultNamesInfo(options) { + return { + fontFamily: {en: options.familyName || ' '}, + fontSubfamily: {en: options.styleName || ' '}, + fullName: {en: options.fullName || options.familyName + ' ' + options.styleName}, + // postScriptName may not contain any whitespace + postScriptName: {en: options.postScriptName || (options.familyName + options.styleName).replace(/\s/g, '')}, + designer: {en: options.designer || ' '}, + designerURL: {en: options.designerURL || ' '}, + manufacturer: {en: options.manufacturer || ' '}, + manufacturerURL: {en: options.manufacturerURL || ' '}, + license: {en: options.license || ' '}, + licenseURL: {en: options.licenseURL || ' '}, + version: {en: options.version || 'Version 0.1'}, + description: {en: options.description || ' '}, + copyright: {en: options.copyright || ' '}, + trademark: {en: options.trademark || ' '} + }; +} + +/** + * @typedef FontOptions + * @type Object + * @property {Boolean} empty - whether to create a new empty font + * @property {string} familyName + * @property {string} styleName + * @property {string=} fullName + * @property {string=} postScriptName + * @property {string=} designer + * @property {string=} designerURL + * @property {string=} manufacturer + * @property {string=} manufacturerURL + * @property {string=} license + * @property {string=} licenseURL + * @property {string=} version + * @property {string=} description + * @property {string=} copyright + * @property {string=} trademark + * @property {Number} unitsPerEm + * @property {Number} ascender + * @property {Number} descender + * @property {Number} createdTimestamp + * @property {Number} weightClass + * @property {Number} italicAngle + * @property {string=} widthClass + * @property {string=} fsSelection + */ + +/** + * 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. + * @exports opentype.Font + * @class + * @param {FontOptions} + * @constructor + */ +function Font(options) { + options = options || {}; + options.tables = options.tables || {}; + + if (!options.empty) { + // Check that we've provided the minimum set of names. + checkArgument(options.familyName, 'When creating a new Font object, familyName is required.'); + checkArgument(options.styleName, 'When creating a new Font object, styleName is required.'); + checkArgument(options.unitsPerEm, 'When creating a new Font object, unitsPerEm is required.'); + checkArgument(options.ascender, 'When creating a new Font object, ascender is required.'); + checkArgument(options.descender <= 0, 'When creating a new Font object, negative descender value is required.'); + + // OS X will complain if the names are empty, so we put a single space everywhere by default. + this.names = {}; + this.names.unicode = createDefaultNamesInfo(options); + this.names.macintosh = createDefaultNamesInfo(options); + this.names.windows = createDefaultNamesInfo(options); + this.unitsPerEm = options.unitsPerEm || 1000; + this.ascender = options.ascender; + this.descender = options.descender; + this.createdTimestamp = options.createdTimestamp; + this.italicAngle = options.italicAngle || 0; + this.weightClass = options.weightClass || 0; + + let selection = 0; + if (options.fsSelection) { + selection = options.fsSelection; + } else { + if (this.italicAngle < 0) { + selection |= this.fsSelectionValues.ITALIC; + } else if (this.italicAngle > 0) { + selection |= this.fsSelectionValues.OBLIQUE; + } + if (this.weightClass >= 600) { + selection |= this.fsSelectionValues.BOLD; + } + if (selection == 0) { + selection = this.fsSelectionValues.REGULAR; + } + } + + if (!options.panose || !Array.isArray(options.panose)) { + options.panose = [0, 0, 0, 0, 0, 0, 0, 0, 0]; + } + + this.tables = Object.assign(options.tables, { + os2: Object.assign({ + usWeightClass: options.weightClass || this.usWeightClasses.MEDIUM, + usWidthClass: options.widthClass || this.usWidthClasses.MEDIUM, + bFamilyType: options.panose[0] || 0, + bSerifStyle: options.panose[1] || 0, + bWeight: options.panose[2] || 0, + bProportion: options.panose[3] || 0, + bContrast: options.panose[4] || 0, + bStrokeVariation: options.panose[5] || 0, + bArmStyle: options.panose[6] || 0, + bLetterform: options.panose[7] || 0, + bMidline: options.panose[8] || 0, + bXHeight: options.panose[9] || 0, + fsSelection: selection, + }, options.tables.os2) + }); + } + + this.supported = true; // Deprecated: parseBuffer will throw an error if font is not supported. + this.glyphs = new glyphset.GlyphSet(this, options.glyphs || []); + this.encoding = new DefaultEncoding(this); + this.position = new Position(this); + this.substitution = new Substitution(this); + this.tables = this.tables || {}; + + // needed for low memory mode only. + this._push = null; + this._hmtxTableData = {}; + + Object.defineProperty(this, 'hinting', { + get: function() { + if (this._hinting) return this._hinting; + if (this.outlinesFormat === 'truetype') { + return (this._hinting = new HintingTrueType(this)); + } + return null; + } + }); +} + +/** + * Check if the font has a glyph for the given character. + * @param {string} + * @return {Boolean} + */ +Font.prototype.hasChar = function(c) { + return this.encoding.charToGlyphIndex(c) > 0; +}; + +/** + * Convert the given character to a single glyph index. + * 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. + * @param {string} + * @return {Number} + */ +Font.prototype.charToGlyphIndex = function(s) { + return this.encoding.charToGlyphIndex(s); +}; + +/** + * Convert the given character to a single Glyph object. + * 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. + * @param {string} + * @return {opentype.Glyph} + */ +Font.prototype.charToGlyph = function(c) { + const glyphIndex = this.charToGlyphIndex(c); + let glyph = this.glyphs.get(glyphIndex); + if (!glyph) { + // .notdef + glyph = this.glyphs.get(0); + } + + return glyph; +}; + +/** + * Update features + * @param {any} options features options + */ +Font.prototype.updateFeatures = function (options) { + // TODO: update all features options not only 'latn'. + return this.defaultRenderOptions.features.map(feature => { + if (feature.script === 'latn') { + return { + script: 'latn', + tags: feature.tags.filter(tag => options[tag]) + }; + } else { + return feature; + } + }); +}; + +/** + * Convert the given text to a list of Glyph indexes. + * Note that there is no strict one-to-one mapping between characters and + * glyphs, so the list of returned glyph indexes can be larger or smaller than the + * length of the given string. + * @param {string} + * @param {GlyphRenderOptions} [options] + * @return {number[]} + */ +Font.prototype.stringToGlyphIndexes = function(s, options) { + const bidi = new Bidi(); + + // Create and register 'glyphIndex' state modifier + const charToGlyphIndexMod = token => this.charToGlyphIndex(token.char); + bidi.registerModifier('glyphIndex', null, charToGlyphIndexMod); + + // roll-back to default features + let features = options ? + this.updateFeatures(options.features) : + this.defaultRenderOptions.features; + + bidi.applyFeatures(this, features); + + return bidi.getTextGlyphs(s); +}; + +/** + * Convert the given text to a list of Glyph objects. + * Note that there is no strict one-to-one mapping between characters and + * glyphs, so the list of returned glyphs can be larger or smaller than the + * length of the given string. + * @param {string} + * @param {GlyphRenderOptions} [options] + * @return {opentype.Glyph[]} + */ +Font.prototype.stringToGlyphs = function(s, options) { + const indexes = this.stringToGlyphIndexes(s, options); + + let length = indexes.length; + + // convert glyph indexes to glyph objects + const glyphs = new Array(length); + const notdef = this.glyphs.get(0); + for (let i = 0; i < length; i += 1) { + glyphs[i] = this.glyphs.get(indexes[i]) || notdef; + } + return glyphs; +}; + +/** + * @param {string} + * @return {Number} + */ +Font.prototype.nameToGlyphIndex = function(name) { + return this.glyphNames.nameToGlyphIndex(name); +}; + +/** + * @param {string} + * @return {opentype.Glyph} + */ +Font.prototype.nameToGlyph = function(name) { + const glyphIndex = this.nameToGlyphIndex(name); + let glyph = this.glyphs.get(glyphIndex); + if (!glyph) { + // .notdef + glyph = this.glyphs.get(0); + } + + return glyph; +}; + +/** + * @param {Number} + * @return {String} + */ +Font.prototype.glyphIndexToName = function(gid) { + if (!this.glyphNames.glyphIndexToName) { + return ''; + } + + return this.glyphNames.glyphIndexToName(gid); +}; + +/** + * Retrieve the value of the kerning pair 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. + * For GPOS kerning, this method uses the default script and language, which covers + * most use cases. To have greater control, use font.position.getKerningValue . + * @param {opentype.Glyph} leftGlyph + * @param {opentype.Glyph} rightGlyph + * @return {Number} + */ +Font.prototype.getKerningValue = function(leftGlyph, rightGlyph) { + leftGlyph = leftGlyph.index || leftGlyph; + rightGlyph = rightGlyph.index || rightGlyph; + const gposKerning = this.position.defaultKerningTables; + if (gposKerning) { + return this.position.getKerningValue(gposKerning, leftGlyph, rightGlyph); + } + // "kern" table + return this.kerningPairs[leftGlyph + ',' + rightGlyph] || 0; +}; + +/** + * @typedef GlyphRenderOptions + * @type Object + * @property {string} [script] - script used to determine which features to apply. By default, 'DFLT' or 'latn' is used. + * See https://www.microsoft.com/typography/otspec/scripttags.htm + * @property {string} [language='dflt'] - language system used to determine which features to apply. + * See https://www.microsoft.com/typography/developers/opentype/languagetags.aspx + * @property {boolean} [kerning=true] - whether to include kerning values + * @property {object} [features] - OpenType Layout feature tags. Used to enable or disable the features of the given script/language system. + * See https://www.microsoft.com/typography/otspec/featuretags.htm + */ +Font.prototype.defaultRenderOptions = { + kerning: true, + features: [ + /** + * these 4 features are required to render Arabic text properly + * and shouldn't be turned off when rendering arabic text. + */ + { script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] }, + { script: 'latn', tags: ['liga', 'rlig'] }, + { script: 'thai', tags: ['liga', 'rlig', 'ccmp'] }, + ] +}; + +/** + * Helper function that invokes the given callback for each glyph in the given text. + * The callback gets `(glyph, x, y, fontSize, options)`.* @param {string} text + * @param {string} text - The text to apply. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + * @param {Function} callback + */ +Font.prototype.forEachGlyph = function(text, x, y, fontSize, options, callback) { + x = x !== undefined ? x : 0; + y = y !== undefined ? y : 0; + fontSize = fontSize !== undefined ? fontSize : 72; + options = Object.assign({}, this.defaultRenderOptions, options); + const fontScale = 1 / this.unitsPerEm * fontSize; + const glyphs = this.stringToGlyphs(text, options); + let kerningLookups; + if (options.kerning) { + const script = options.script || this.position.getDefaultScriptName(); + kerningLookups = this.position.getKerningTables(script, options.language); + } + for (let i = 0; i < glyphs.length; i += 1) { + const glyph = glyphs[i]; + callback.call(this, glyph, x, y, fontSize, options); + if (glyph.advanceWidth) { + x += glyph.advanceWidth * fontScale; + } + + if (options.kerning && i < glyphs.length - 1) { + // We should apply position adjustment lookups in a more generic way. + // Here we only use the xAdvance value. + const kerningValue = kerningLookups ? + this.position.getKerningValue(kerningLookups, glyph.index, glyphs[i + 1].index) : + this.getKerningValue(glyph, glyphs[i + 1]); + x += kerningValue * fontScale; + } + + if (options.letterSpacing) { + x += options.letterSpacing * fontSize; + } else if (options.tracking) { + x += (options.tracking / 1000) * fontSize; + } + } + return x; +}; + +/** + * Create a Path object that represents the given text. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + * @return {opentype.Path} + */ +Font.prototype.getPath = function(text, x, y, fontSize, options) { + const fullPath = new Path(); + this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { + const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this); + fullPath.extend(glyphPath); + }); + return fullPath; +}; + +/** + * Create an array of Path objects that represent the glyphs of a given text. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + * @return {opentype.Path[]} + */ +Font.prototype.getPaths = function(text, x, y, fontSize, options) { + const glyphPaths = []; + this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { + const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this); + glyphPaths.push(glyphPath); + }); + + return glyphPaths; +}; + +/** + * Returns the advance width of a text. + * + * This is something different than Path.getBoundingBox() as 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 + * + * @param {string} text - The text to create. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + * @return advance width + */ +Font.prototype.getAdvanceWidth = function(text, fontSize, options) { + return this.forEachGlyph(text, 0, 0, fontSize, options, function() {}); +}; + +/** + * Draw the text on the given drawing context. + * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + */ +Font.prototype.draw = function(ctx, text, x, y, fontSize, options) { + this.getPath(text, x, y, fontSize, options).draw(ctx); +}; + +/** + * 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. + * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + */ +Font.prototype.drawPoints = function(ctx, text, x, y, fontSize, options) { + this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { + glyph.drawPoints(ctx, gX, gY, gFontSize); + }); +}; + +/** + * 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. + * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + */ +Font.prototype.drawMetrics = function(ctx, text, x, y, fontSize, options) { + this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { + glyph.drawMetrics(ctx, gX, gY, gFontSize); + }); +}; + +/** + * @param {string} + * @return {string} + */ +Font.prototype.getEnglishName = function(name) { + const translations = (this.names.unicode || this.names.macintosh || this.names.windows)[name]; + if (translations) { + return translations.en; + } +}; + +/** + * Validate + */ +Font.prototype.validation = new validation.MessageStack(); +Font.prototype.validate = function() { + const validationMessages = []; + const _this = this; + + function assert(predicate, message) { + if (!predicate) { + validationMessages.push(_this.validation.add(message, validation.errorTypes.WARNING)); + } + } + + function assertNamePresent(name) { + const englishName = _this.getEnglishName(name); + assert(englishName && englishName.trim().length > 0, + 'No English ' + name + ' specified.'); + } + + // Identification information + assertNamePresent('fontFamily'); + assertNamePresent('weightName'); + assertNamePresent('manufacturer'); + assertNamePresent('copyright'); + assertNamePresent('version'); + + // Dimension information + assert(this.unitsPerEm > 0, 'No unitsPerEm specified.'); + + this.validation.logMessages(); + + return validationMessages; +}; + +/** + * Convert the font object to a SFNT data structure. + * This structure contains all the necessary tables and metadata to create a binary OTF file. + * @return {opentype.Table} + */ +Font.prototype.toTables = function() { + return sfnt.fontToTable(this); +}; +/** + * @deprecated Font.toBuffer is deprecated. Use Font.toArrayBuffer instead. + */ +Font.prototype.toBuffer = function() { + this.validation.add('Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.', validation.errorTypes.DEPRECATED); + return this.toArrayBuffer(); +}; +/** + * Converts a `opentype.Font` into an `ArrayBuffer` + * @return {ArrayBuffer} + */ +Font.prototype.toArrayBuffer = function() { + const sfntTable = this.toTables(); + const bytes = sfntTable.encode(); + const buffer = new ArrayBuffer(bytes.length); + const intArray = new Uint8Array(buffer); + for (let i = 0; i < bytes.length; i++) { + intArray[i] = bytes[i]; + } + + return buffer; +}; + +/** + * Initiate a download of the OpenType font. + */ +Font.prototype.download = function(fileName) { + const familyName = this.getEnglishName('fontFamily'); + const styleName = this.getEnglishName('fontSubfamily'); + fileName = fileName || familyName.replace(/\s/g, '') + '-' + styleName + '.otf'; + const arrayBuffer = this.toArrayBuffer(); + + if (isBrowser()) { + window.URL = window.URL || window.webkitURL; + + if (window.URL) { + const dataView = new DataView(arrayBuffer); + const blob = new Blob([dataView], {type: 'font/opentype'}); + + let link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = fileName; + + let event = document.createEvent('MouseEvents'); + event.initEvent('click', true, false); + link.dispatchEvent(event); + } else { + validation.add('Font file could not be downloaded. Try using a different browser.'); + } + } else { + const fs = require('fs'); + const buffer = Buffer.alloc(arrayBuffer.byteLength); + const view = new Uint8Array(arrayBuffer); + for (let i = 0; i < buffer.length; ++i) { + buffer[i] = view[i]; + } + fs.writeFileSync(fileName, buffer); + } +}; + +/** + * @private + */ +Font.prototype.fsSelectionValues = { + ITALIC: 0x001, //1 + UNDERSCORE: 0x002, //2 + NEGATIVE: 0x004, //4 + OUTLINED: 0x008, //8 + STRIKEOUT: 0x010, //16 + BOLD: 0x020, //32 + REGULAR: 0x040, //64 + USER_TYPO_METRICS: 0x080, //128 + WWS: 0x100, //256 + OBLIQUE: 0x200 //512 +}; + +/** + * @private + */ +Font.prototype.macStyleValues = { + BOLD: 0x001, //1 + ITALIC: 0x002, //2 + UNDERLINE: 0x004, //4 + OUTLINED: 0x008, //8 + SHADOW: 0x010, //16 + CONDENSED: 0x020, //32 + EXTENDED: 0x040, //64 +}; + +/** + * @private + */ +Font.prototype.usWidthClasses = { + ULTRA_CONDENSED: 1, + EXTRA_CONDENSED: 2, + CONDENSED: 3, + SEMI_CONDENSED: 4, + MEDIUM: 5, + SEMI_EXPANDED: 6, + EXPANDED: 7, + EXTRA_EXPANDED: 8, + ULTRA_EXPANDED: 9 +}; + +/** + * @private + */ +Font.prototype.usWeightClasses = { + THIN: 100, + EXTRA_LIGHT: 200, + LIGHT: 300, + NORMAL: 400, + MEDIUM: 500, + SEMI_BOLD: 600, + BOLD: 700, + EXTRA_BOLD: 800, + BLACK: 900 +}; + +export default Font; diff --git a/src/glyphset.js b/src/glyphset.js index adddb395..1f889fba 100644 --- a/src/glyphset.js +++ b/src/glyphset.js @@ -37,7 +37,6 @@ function GlyphSet(font, glyphs) { this.glyphs[i] = glyph; } } - this.length = (glyphs && glyphs.length) || 0; } @@ -64,13 +63,15 @@ if(typeof Symbol !== 'undefined' && Symbol.iterator) { GlyphSet.prototype.get = function(index) { // this.glyphs[index] is 'undefined' when low memory mode is on. glyph is pushed on request only. if (this.glyphs[index] === undefined) { + if (typeof this.font._push !== 'function') return; + this.font._push(index); if (typeof this.glyphs[index] === 'function') { this.glyphs[index] = this.glyphs[index](); } let glyph = this.glyphs[index]; - let unicodeObj = this.font._IndexToUnicodeMap[index]; + let unicodeObj = this.font._IndexToUnicodeMap && this.font._IndexToUnicodeMap[index]; if (unicodeObj) { for (let j = 0; j < unicodeObj.unicodes.length; j++) diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 00000000..66d74326 --- /dev/null +++ b/src/logger.js @@ -0,0 +1,141 @@ +import { isBrowser } from './util.js'; + +/** + * @typedef {number} ErrorTypes + */ + +/** + * @enum {ErrorTypes} + */ +const ErrorTypes = { + ERROR: 1, + WARNING: 2, + DEPRECATED: 4, + ALL: 32767 +}; +Object.freeze && Object.freeze(ErrorTypes); + +/** + * @enum {ErrorStrings} + */ +const errorStrings = { + 1: 'ERROR', + 2: 'WARNING', + 4: 'DEPRECATED' +}; + +const logMethods = { + 1: 'error', + 2: 'warn', + 4: 'info' +}; + +/** + * @property {string} string - message string + * @property {keyof ErrorTypes} type - error type + */ +class Message { + constructor(string, type = ErrorTypes.ERROR) { + if (!errorStrings[type]) { + throw new Error( 'Invalid error type ' + type + ' for message: ' + string ); + } + + this.string = string; + this.type = type; + } + + toString() { + return errorStrings[this.type] + ': ' + this.string; + } +} + +class MessageLogger { + + constructor() { + this.logLevel = ErrorTypes.ALL; + this.throwLevel = ErrorTypes.ERROR; + this.ErrorTypes = ErrorTypes; + } + + /** + * Logs a message and fires the opentypejs:message Event. + * @property {String|Message} string + * @property {keyof ErrorTypes} type + * + * @returns {Message} + */ + add(stringOrMessage, type = ErrorTypes.ERROR) { + let message; + if (stringOrMessage instanceof Message) { + message = stringOrMessage; + type = message.type; + } else { + message = new Message(stringOrMessage, type); + } + + let doLog = !!(this.logLevel & type); + + if (isBrowser()) { + document.dispatchEvent( + new CustomEvent('opentypejs:message', { + detail: { + message, + logged: doLog, + logger: this.logLevel + } + }) + ); + } + + if (doLog) { + this.logMessage(message); + } + + return message; + } + + /** + * adds an array of messages + */ + adds(messageArray) { + for (let i = 0; i < messageArray.length; i++) { + this.add(messageArray[i]); + } + } + + /** + * Logs a message to the console or throws it, + * depending on the throwLevel setting. + * @param {Message} message + */ + logMessage(message) { + const type = message.type || ErrorTypes.ERROR; + const logMethod = console[logMethods[type] || 'log'] || console.log; + const logMessage = '[opentype.js] ' + message.toString(); + if ( this.throwLevel & type ) { + throw new Error(logMessage); + } + logMethod(logMessage); + } + + getLogLevel() { + return this.logLevel; + } + + setLogLevel(newLevel) { + this.logLevel = newLevel; + } + + getThrowLevel() { + return this.throwLevel; + } + + setThrowLevel(newLevel) { + this.throwLevel = newLevel; + } + +} + +const globalLogger = new MessageLogger(); + +export { ErrorTypes, Message, MessageLogger, globalLogger as logger }; \ No newline at end of file diff --git a/src/opentype.js b/src/opentype.js index 0fd00854..71f013b2 100644 --- a/src/opentype.js +++ b/src/opentype.js @@ -35,6 +35,9 @@ import os2 from './tables/os2.js'; import post from './tables/post.js'; import meta from './tables/meta.js'; import gasp from './tables/gasp.js'; +import { createDefaultNamesInfo } from './font.js'; +import { sizeOf } from './types.js'; +import { ErrorTypes, logger } from './logger.js'; /** * The opentype library. * @namespace opentype @@ -187,17 +190,18 @@ function parseWOFFTableEntries(data, numTables) { */ /** + * @param {opentype.Font} * @param {DataView} * @param {Object} * @return {TableData} */ -function uncompressTable(data, tableEntry) { +function uncompressTable(data, tableEntry) { if (tableEntry.compression === 'WOFF') { const inBuffer = new Uint8Array(data.buffer, tableEntry.offset + 2, tableEntry.compressedLength - 2); const outBuffer = new Uint8Array(tableEntry.length); inflate(inBuffer, outBuffer); if (outBuffer.byteLength !== tableEntry.length) { - throw new Error('Decompression error: ' + tableEntry.tag + ' decompressed length doesn\'t match recorded length'); + logger.add('Decompression error: ' + tableEntry.tag + ' decompressed length doesn\'t match recorded length'); } const view = new DataView(outBuffer.buffer, 0); @@ -249,16 +253,26 @@ function parseBuffer(buffer, opt={}) { } else if (flavor === 'OTTO') { font.outlinesFormat = 'cff'; } else { - throw new Error('Unsupported OpenType flavor ' + signature); + logger.add('Unsupported OpenType flavor ' + signature); } numTables = parse.getUShort(data, 12); tableEntries = parseWOFFTableEntries(data, numTables); } else if (signature === 'wOF2') { var issue = 'https://github.com/opentypejs/opentype.js/issues/183#issuecomment-1147228025'; - throw new Error('WOFF2 require an external decompressor library, see examples at: ' + issue); + logger.add('WOFF2 require an external decompressor library, see examples at: ' + issue); + } else if (signature.substring(0,2) === '%!') { + // https://adobe-type-tools.github.io/font-tech-notes/pdfs/T1_SPEC.pdf + // https://personal.math.ubc.ca/~cass/piscript/type1.pdf + logger.add('PostScript/PS1/T1/Adobe Type 1 fonts are not supported'); + } else if (data.buffer.byteLength > (3 * sizeOf.Card8() + sizeOf.OffSize()) && parse.getByte(data, 0) === 0x01) { + // this could be a CFF1 file, we will try to parse it like a CCF table below + // https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf + font.isCFFFont = true; + tableEntries.push({tag:'CFF ',offset:0}); + numTables = 1; } else { - throw new Error('Unsupported OpenType signature ' + signature); + logger.add('Unsupported OpenType signature ' + signature); } let cffTableEntry; @@ -393,9 +407,16 @@ function parseBuffer(buffer, opt={}) { } } - const nameTable = uncompressTable(data, nameTableEntry); - font.tables.name = _name.parse(nameTable.data, nameTable.offset, ltagTable); - font.names = font.tables.name; + if ( nameTableEntry ) { + const nameTable = uncompressTable(data, nameTableEntry); + font.tables.name = _name.parse(nameTable.data, nameTable.offset, ltagTable); + font.names = font.tables.name; + } else { + font.names = {}; + font.names.unicode = createDefaultNamesInfo({}); + font.names.macintosh = createDefaultNamesInfo({}); + font.names.windows = createDefaultNamesInfo({}); + } if (glyfTableEntry && locaTableEntry) { const shortVersion = indexToLocFormat === 0; @@ -410,12 +431,21 @@ function parseBuffer(buffer, opt={}) { const cffTable2 = uncompressTable(data, cff2TableEntry); cff.parse(cffTable2.data, cffTable2.offset, font, opt); } else { - throw new Error('Font doesn\'t contain TrueType, CFF or CFF2 outlines.'); + logger.add('Font doesn\'t contain TrueType, CFF or CFF2 outlines.'); } - const hmtxTable = uncompressTable(data, hmtxTableEntry); - hmtx.parse(font, hmtxTable.data, hmtxTable.offset, font.numberOfHMetrics, font.numGlyphs, font.glyphs, opt); - addGlyphNames(font, opt); + if (hmtxTableEntry) { + const hmtxTable = uncompressTable(data, hmtxTableEntry); + hmtx.parse(font, hmtxTable.data, hmtxTable.offset, font.numberOfHMetrics, font.numGlyphs, font.glyphs, opt); + } + + if (!font.tables.cmap) { + if (!font.isCFFFont) { + logger.add('Font doesn\'t contain required cmap table', ErrorTypes.WARNING); + } + } else { + addGlyphNames(font, opt); + } if (kernTableEntry) { const kernTable = uncompressTable(data, kernTableEntry); @@ -540,5 +570,6 @@ export { parse as _parse, parseBuffer as parse, load, - loadSync + loadSync, + ErrorTypes }; diff --git a/src/parse.js b/src/parse.js index d6770721..e0bd9cb4 100644 --- a/src/parse.js +++ b/src/parse.js @@ -37,15 +37,29 @@ function getFixed(dataView, offset) { return decimal + fraction / 65535; } +// Retrieve a string with a specific byte length from the DataView. +function getString(dataView, offset, length) { + let string = ''; + + if (!offset) { + offset = 0; + } + + if (length === undefined) { + length = dataView.byteLength - offset; + } + + for (let i = offset; i < offset + length; i += 1) { + string += String.fromCharCode(dataView.getInt8(i)); + } + + return string; +} + // Retrieve a 4-character tag from the DataView. // Tags are used to identify tables. function getTag(dataView, offset) { - let tag = ''; - for (let i = offset; i < offset + 4; i += 1) { - tag += String.fromCharCode(dataView.getInt8(i)); - } - - return tag; + return getString(dataView, offset, 4); } // Retrieve an offset from the DataView. @@ -721,6 +735,7 @@ export default { getUInt24, getULong, getFixed, + getString, getTag, getOffset, getBytes, diff --git a/src/tables/cff.js b/src/tables/cff.js index 51a82986..58aaad18 100755 --- a/src/tables/cff.js +++ b/src/tables/cff.js @@ -10,6 +10,8 @@ import glyphset from '../glyphset.js'; import parse from '../parse.js'; import Path from '../path.js'; import table from '../table.js'; +import { createDefaultNamesInfo } from '../font.js'; +import { logger, ErrorTypes } from '../logger.js'; // Custom equals function that can also check lists. function equals(a, b) { @@ -309,7 +311,7 @@ function interpretDict(dict, meta, strings) { } // Parse the CFF header. -function parseCFFHeader(data, start) { +function parseCFFHeader(data, start, isCFFFont) { const header = {}; header.formatMajor = parse.getCard8(data, start); header.formatMinor = parse.getCard8(data, start + 1); @@ -323,7 +325,7 @@ function parseCFFHeader(data, start) { if (header.formatMajor < 2) { header.offsetSize = parse.getCard8(data, start + 3); header.startOffset = start; - header.endOffset = start + 4; + header.endOffset = start + (isCFFFont ? header.size : 4); } else { header.topDictLength = parse.getCard16(data, start + 3); header.endOffset = start + 8; @@ -1101,17 +1103,17 @@ function parseCFFCharstring(font, glyph, code, version) { return p; } -function parseCFFFDSelect(data, start, nGlyphs, fdArrayCount, version) { +function parseCFFFDSelect(data, start, font, fdArrayCount, version) { const fdSelect = []; let fdIndex; const parser = new parse.Parser(data, start); const format = parser.parseCard8(); if (format === 0) { // Simple list of nGlyphs elements - for (let iGid = 0; iGid < nGlyphs; iGid++) { + for (let iGid = 0; iGid < font.nGlyphs; iGid++) { fdIndex = parser.parseCard8(); if (fdIndex >= fdArrayCount) { - throw new Error('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')'); + logger.add('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')'); } fdSelect.push(fdIndex); } @@ -1120,36 +1122,43 @@ function parseCFFFDSelect(data, start, nGlyphs, fdArrayCount, version) { const nRanges = format === 4 ? parser.parseULong() : parser.parseCard16(); let first = format === 4 ? parser.parseULong() : parser.parseCard16(); if (first !== 0) { - throw new Error(`CFF Table CID Font FDSelect format ${format} range has bad initial GID ${first}`); + logger.add(`CFF Table CID Font FDSelect format ${format} range has bad initial GID ${first}`); } let next; for (let iRange = 0; iRange < nRanges; iRange++) { fdIndex = format === 4 ? parser.parseUShort() : parser.parseCard8(); next = format === 4 ? parser.parseULong() : parser.parseCard16(); if (fdIndex >= fdArrayCount) { - throw new Error('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')'); + logger.add('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')'); } - if (next > nGlyphs) { - throw new Error(`CFF Table CID Font FDSelect format ${version} range has bad GID ${next}`); + if (next > font.nGlyphs) { + logger.add(`CFF Table CID Font FDSelect format ${version} range has bad GID ${next}`); } for (; first < next; first++) { fdSelect.push(fdIndex); } first = next; } - if (next !== nGlyphs) { - throw new Error('CFF Table CID Font FDSelect format 3 range has bad final (Sentinal) GID ' + next); + if (next !== font.nGlyphs) { + logger.add('CFF Table CID Font FDSelect format 3 range has bad final (Sentinel) GID ' + next, ErrorTypes.WARNING); } } else { - throw new Error('CFF Table CID Font FDSelect table has unsupported format ' + format); + logger.add('CFF Table CID Font FDSelect table has unsupported format ' + format); } + return fdSelect; } -// Parse the `CFF` table, which contains the glyph outlines in PostScript format. +/** + * Parse the `CFF` table, which contains the glyph outlines in PostScript format. + * @param {DataView} data + * @param {Number} start + * @param {Font} font + * @param {Object} opt + */ function parseCFFTable(data, start, font, opt) { let resultTable; - const header = parseCFFHeader(data, start); + const header = parseCFFHeader(data, start, !!font.isCFFFont); if (header.formatMajor === 2) { resultTable = font.tables.cff2 = {}; } else { @@ -1170,13 +1179,14 @@ function parseCFFTable(data, start, font, opt) { } else { const topDictArray = gatherCFFTopDicts(data, start, topDictIndex.objects, stringIndex.objects, header.formatMajor); if (topDictArray.length !== 1) { - throw new Error('CFF table has too many fonts in \'FontSet\' - count of fonts NameIndex.length = ' + topDictArray.length); + logger.add('CFF table has too many fonts in \'FontSet\' - count of fonts NameIndex.length = ' + topDictArray.length); } topDict = topDictArray[0]; } resultTable.topDict = topDict; + resultTable.nameIndex = nameIndex; if (topDict._privateDict) { font.defaultWidthX = topDict._privateDict.defaultWidthX; @@ -1187,11 +1197,22 @@ function parseCFFTable(data, start, font, opt) { font.isCIDFont = true; } + // CharStrings must be parsed before FDSelect, because we need the nGlyphs value for parsing FDSelect + // Offsets in the top dict are relative to the beginning of the CFF data, so add the CFF start offset. + let charStringsIndex; + if (opt.lowMemory) { + charStringsIndex = parseCFFIndexLowMemory(data, start + topDict.charStrings, header.formatMajor); + font.nGlyphs = charStringsIndex.offsets.length - (header.formatMajor > 1 ? 1 : 0); // number of elements is count + 1 + } else { + charStringsIndex = parseCFFIndex(data, start + topDict.charStrings, null, header.formatMajor); + font.nGlyphs = charStringsIndex.objects.length; + } + if ( header.formatMajor > 1 ) { let fdArrayIndexOffset = topDict.fdArray; let fdSelectOffset = topDict.fdSelect; if (!fdArrayIndexOffset) { - throw new Error('This is a CFF2 font, but FDArray information is missing'); + logger.add('This is a CFF2 font, but FDArray information is missing'); } const fdArrayIndex = parseCFFIndex(data, start + fdArrayIndexOffset, null, header.formatMajor); @@ -1199,21 +1220,21 @@ function parseCFFTable(data, start, font, opt) { const fdArray = gatherCFF2FontDicts(data, start, fdArrayIndex.objects); topDict._fdArray = fdArray; if (fdSelectOffset) { - topDict._fdSelect = parseCFFFDSelect(data, start + fdSelectOffset, font.numGlyphs, fdArray.length, header.formatMajor); + topDict._fdSelect = parseCFFFDSelect(data, start + fdSelectOffset, font, fdArray.length, header.formatMajor); } } else if (font.isCIDFont) { let fdArrayOffset = topDict.fdArray; let fdSelectOffset = topDict.fdSelect; if (fdArrayOffset === 0 || fdSelectOffset === 0) { - throw new Error('Font is marked as a CID font, but FDArray and/or FDSelect information is missing'); + logger.add('Font is marked as a CID font, but FDArray and/or FDSelect information is missing'); } fdArrayOffset += start; const fdArrayIndex = parseCFFIndex(data, fdArrayOffset); const fdArray = gatherCFFTopDicts(data, start, fdArrayIndex.objects, stringIndex.objects, header.formatMajor); topDict._fdArray = fdArray; fdSelectOffset += start; - topDict._fdSelect = parseCFFFDSelect(data, fdSelectOffset, font.numGlyphs, fdArray.length, header.formatMajor); + topDict._fdSelect = parseCFFFDSelect(data, fdSelectOffset, font, fdArray.length, header.formatMajor); } if (header.formatMajor < 2) { @@ -1233,18 +1254,8 @@ function parseCFFTable(data, start, font, opt) { } } - // Offsets in the top dict are relative to the beginning of the CFF data, so add the CFF start offset. - let charStringsIndex; - if (opt.lowMemory) { - charStringsIndex = parseCFFIndexLowMemory(data, start + topDict.charStrings, header.formatMajor); - font.nGlyphs = charStringsIndex.offsets.length - (header.formatMajor > 1 ? 1 : 0); // number of elements is count + 1 - } else { - charStringsIndex = parseCFFIndex(data, start + topDict.charStrings, null, header.formatMajor); - font.nGlyphs = charStringsIndex.objects.length; - } - if ( header.formatMajor > 1 && font.tables.maxp && font.nGlyphs !== font.tables.maxp.numGlyphs ) { - console.error(`Glyph count in the CFF2 table (${font.nGlyphs}) must correspond to the glyph count in the maxp table (${font.tables.maxp.numGlyphs})`); + logger.add(`Glyph count in the CFF2 table (${font.nGlyphs}) must correspond to the glyph count in the maxp table (${font.tables.maxp.numGlyphs})`, ErrorTypes.WARNING); } if (header.formatMajor < 2) { @@ -1280,6 +1291,27 @@ function parseCFFTable(data, start, font, opt) { const p = new parse.Parser(data, start + topDict.vstore); topDict._vstore = p.parseVariationStore(); } + + if (font.isCFFFont) { + logger.add('CFF Type1 fonts are not fully supported, but you can use this to extract glyph outlines and metadata for example.', ErrorTypes.WARNING); + const topDict = font.tables.cff.topDict; + const psName = font.tables.cff.nameIndex && font.tables.cff.nameIndex.objects.length && font.tables.cff.nameIndex.objects[0] || ''; + const metaData = { + copyright: topDict.copyright || topDict.notice, + fullName: topDict.fullName, + version: topDict.version, + postScriptName: psName + }; + font.names.unicode = createDefaultNamesInfo(metaData); + font.names.macintosh = createDefaultNamesInfo(metaData); + font.names.windows = createDefaultNamesInfo(metaData); + + const bBox = topDict.fontBBox; + const fMatrix = topDict.fontMatrix; + font.ascender = bBox && bBox.length > 2 && bBox[2] || 0; + font.descender = bBox && bBox.length > 1 && bBox[1] || 0; + font.unitsPerEm = fMatrix && fMatrix.length && (1/fMatrix[0]) || 1000; + } } // Convert a string to a String ID (SID). diff --git a/src/types.js b/src/types.js index d8e5e637..94ced906 100644 --- a/src/types.js +++ b/src/types.js @@ -2,6 +2,7 @@ // All OpenType fonts use Motorola-style byte ordering (Big Endian) import check from './check.js'; +import { logger } from './logger.js'; const LIMIT16 = 32768; // The limit at which a 16-bit number switches signs == 2^15 const LIMIT32 = 2147483648; // The limit at which a 32-bit number switches signs == 2 ^ 31 @@ -70,9 +71,9 @@ sizeOf.CHAR = constant(1); * @returns {Array} */ encode.CHARARRAY = function(v) { - if (typeof v === 'undefined') { + if (v == null) { // catches undefined and null v = ''; - console.warn('Undefined CHARARRAY encountered and treated as an empty string. This is probably caused by a missing glyph name.'); + logger.add('Undefined CHARARRAY encountered and treated as an empty string. This is probably caused by a missing glyph name.', logger.ErrorTypes.WARNING); } const b = []; for (let i = 0; i < v.length; i += 1) { @@ -183,7 +184,7 @@ sizeOf.LONG = constant(4); */ encode.FLOAT = function(v) { if (v > MAX_16_16 || v < MIN_16_16) { - throw new Error(`Value ${v} is outside the range of representable values in 16.16 format`); + logger.add(`Value ${v} is outside the range of representable values in 16.16 format`, logger.ErrorTypes.ERROR); } const fixedValue = Math.round(v * (1 << 16)) << 0; // Round to nearest multiple of 1/(1<<16) return encode.ULONG(fixedValue); @@ -856,7 +857,7 @@ encode.OPERAND = function(v, type) { d.push(enc1[j]); } } else { - throw new Error('Unknown operand type ' + type); + logger.add('Unknown operand type ' + type, logger.ErrorTypes.ERROR); // FIXME Add support for booleans } } From 240c3a73ca717cafa3dcac038a5553ced7470416 Mon Sep 17 00:00:00 2001 From: Connum Date: Fri, 24 Nov 2023 21:07:46 +0100 Subject: [PATCH 6/9] remove accidentally committed temp file --- src/font_REMOTE_153.js | 666 ----------------------------------------- 1 file changed, 666 deletions(-) delete mode 100644 src/font_REMOTE_153.js diff --git a/src/font_REMOTE_153.js b/src/font_REMOTE_153.js deleted file mode 100644 index 6f94f615..00000000 --- a/src/font_REMOTE_153.js +++ /dev/null @@ -1,666 +0,0 @@ -// The Font object - -import Path from './path.js'; -import sfnt from './tables/sfnt.js'; -import { DefaultEncoding } from './encoding.js'; -import glyphset from './glyphset.js'; -import Position from './position.js'; -import Substitution from './substitution.js'; -import { isBrowser, checkArgument } from './util.js'; -import HintingTrueType from './hintingtt.js'; -import Bidi from './bidi.js'; -import validation from './validation.js'; - -function createDefaultNamesInfo(options) { - return { - fontFamily: {en: options.familyName || ' '}, - fontSubfamily: {en: options.styleName || ' '}, - fullName: {en: options.fullName || options.familyName + ' ' + options.styleName}, - // postScriptName may not contain any whitespace - postScriptName: {en: options.postScriptName || (options.familyName + options.styleName).replace(/\s/g, '')}, - designer: {en: options.designer || ' '}, - designerURL: {en: options.designerURL || ' '}, - manufacturer: {en: options.manufacturer || ' '}, - manufacturerURL: {en: options.manufacturerURL || ' '}, - license: {en: options.license || ' '}, - licenseURL: {en: options.licenseURL || ' '}, - version: {en: options.version || 'Version 0.1'}, - description: {en: options.description || ' '}, - copyright: {en: options.copyright || ' '}, - trademark: {en: options.trademark || ' '} - }; -} - -/** - * @typedef FontOptions - * @type Object - * @property {Boolean} empty - whether to create a new empty font - * @property {string} familyName - * @property {string} styleName - * @property {string=} fullName - * @property {string=} postScriptName - * @property {string=} designer - * @property {string=} designerURL - * @property {string=} manufacturer - * @property {string=} manufacturerURL - * @property {string=} license - * @property {string=} licenseURL - * @property {string=} version - * @property {string=} description - * @property {string=} copyright - * @property {string=} trademark - * @property {Number} unitsPerEm - * @property {Number} ascender - * @property {Number} descender - * @property {Number} createdTimestamp - * @property {Number} weightClass - * @property {Number} italicAngle - * @property {string=} widthClass - * @property {string=} fsSelection - */ - -/** - * 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. - * @exports opentype.Font - * @class - * @param {FontOptions} - * @constructor - */ -function Font(options) { - options = options || {}; - options.tables = options.tables || {}; - - if (!options.empty) { - // Check that we've provided the minimum set of names. - checkArgument(options.familyName, 'When creating a new Font object, familyName is required.'); - checkArgument(options.styleName, 'When creating a new Font object, styleName is required.'); - checkArgument(options.unitsPerEm, 'When creating a new Font object, unitsPerEm is required.'); - checkArgument(options.ascender, 'When creating a new Font object, ascender is required.'); - checkArgument(options.descender <= 0, 'When creating a new Font object, negative descender value is required.'); - - // OS X will complain if the names are empty, so we put a single space everywhere by default. - this.names = {}; - this.names.unicode = createDefaultNamesInfo(options); - this.names.macintosh = createDefaultNamesInfo(options); - this.names.windows = createDefaultNamesInfo(options); - this.unitsPerEm = options.unitsPerEm || 1000; - this.ascender = options.ascender; - this.descender = options.descender; - this.createdTimestamp = options.createdTimestamp; - this.italicAngle = options.italicAngle || 0; - this.weightClass = options.weightClass || 0; - - let selection = 0; - if (options.fsSelection) { - selection = options.fsSelection; - } else { - if (this.italicAngle < 0) { - selection |= this.fsSelectionValues.ITALIC; - } else if (this.italicAngle > 0) { - selection |= this.fsSelectionValues.OBLIQUE; - } - if (this.weightClass >= 600) { - selection |= this.fsSelectionValues.BOLD; - } - if (selection == 0) { - selection = this.fsSelectionValues.REGULAR; - } - } - - if (!options.panose || !Array.isArray(options.panose)) { - options.panose = [0, 0, 0, 0, 0, 0, 0, 0, 0]; - } - - this.tables = Object.assign(options.tables, { - os2: Object.assign({ - usWeightClass: options.weightClass || this.usWeightClasses.MEDIUM, - usWidthClass: options.widthClass || this.usWidthClasses.MEDIUM, - bFamilyType: options.panose[0] || 0, - bSerifStyle: options.panose[1] || 0, - bWeight: options.panose[2] || 0, - bProportion: options.panose[3] || 0, - bContrast: options.panose[4] || 0, - bStrokeVariation: options.panose[5] || 0, - bArmStyle: options.panose[6] || 0, - bLetterform: options.panose[7] || 0, - bMidline: options.panose[8] || 0, - bXHeight: options.panose[9] || 0, - fsSelection: selection, - }, options.tables.os2) - }); - } - - this.supported = true; // Deprecated: parseBuffer will throw an error if font is not supported. - this.glyphs = new glyphset.GlyphSet(this, options.glyphs || []); - this.encoding = new DefaultEncoding(this); - this.position = new Position(this); - this.substitution = new Substitution(this); - this.tables = this.tables || {}; - - // needed for low memory mode only. - this._push = null; - this._hmtxTableData = {}; - - Object.defineProperty(this, 'hinting', { - get: function() { - if (this._hinting) return this._hinting; - if (this.outlinesFormat === 'truetype') { - return (this._hinting = new HintingTrueType(this)); - } - return null; - } - }); -} - -/** - * Check if the font has a glyph for the given character. - * @param {string} - * @return {Boolean} - */ -Font.prototype.hasChar = function(c) { - return this.encoding.charToGlyphIndex(c) > 0; -}; - -/** - * Convert the given character to a single glyph index. - * 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. - * @param {string} - * @return {Number} - */ -Font.prototype.charToGlyphIndex = function(s) { - return this.encoding.charToGlyphIndex(s); -}; - -/** - * Convert the given character to a single Glyph object. - * 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. - * @param {string} - * @return {opentype.Glyph} - */ -Font.prototype.charToGlyph = function(c) { - const glyphIndex = this.charToGlyphIndex(c); - let glyph = this.glyphs.get(glyphIndex); - if (!glyph) { - // .notdef - glyph = this.glyphs.get(0); - } - - return glyph; -}; - -/** - * Update features - * @param {any} options features options - */ -Font.prototype.updateFeatures = function (options) { - // TODO: update all features options not only 'latn'. - return this.defaultRenderOptions.features.map(feature => { - if (feature.script === 'latn') { - return { - script: 'latn', - tags: feature.tags.filter(tag => options[tag]) - }; - } else { - return feature; - } - }); -}; - -/** - * Convert the given text to a list of Glyph indexes. - * Note that there is no strict one-to-one mapping between characters and - * glyphs, so the list of returned glyph indexes can be larger or smaller than the - * length of the given string. - * @param {string} - * @param {GlyphRenderOptions} [options] - * @return {number[]} - */ -Font.prototype.stringToGlyphIndexes = function(s, options) { - const bidi = new Bidi(); - - // Create and register 'glyphIndex' state modifier - const charToGlyphIndexMod = token => this.charToGlyphIndex(token.char); - bidi.registerModifier('glyphIndex', null, charToGlyphIndexMod); - - // roll-back to default features - let features = options ? - this.updateFeatures(options.features) : - this.defaultRenderOptions.features; - - bidi.applyFeatures(this, features); - - return bidi.getTextGlyphs(s); -}; - -/** - * Convert the given text to a list of Glyph objects. - * Note that there is no strict one-to-one mapping between characters and - * glyphs, so the list of returned glyphs can be larger or smaller than the - * length of the given string. - * @param {string} - * @param {GlyphRenderOptions} [options] - * @return {opentype.Glyph[]} - */ -Font.prototype.stringToGlyphs = function(s, options) { - const indexes = this.stringToGlyphIndexes(s, options); - - let length = indexes.length; - - // convert glyph indexes to glyph objects - const glyphs = new Array(length); - const notdef = this.glyphs.get(0); - for (let i = 0; i < length; i += 1) { - glyphs[i] = this.glyphs.get(indexes[i]) || notdef; - } - return glyphs; -}; - -/** - * @param {string} - * @return {Number} - */ -Font.prototype.nameToGlyphIndex = function(name) { - return this.glyphNames.nameToGlyphIndex(name); -}; - -/** - * @param {string} - * @return {opentype.Glyph} - */ -Font.prototype.nameToGlyph = function(name) { - const glyphIndex = this.nameToGlyphIndex(name); - let glyph = this.glyphs.get(glyphIndex); - if (!glyph) { - // .notdef - glyph = this.glyphs.get(0); - } - - return glyph; -}; - -/** - * @param {Number} - * @return {String} - */ -Font.prototype.glyphIndexToName = function(gid) { - if (!this.glyphNames.glyphIndexToName) { - return ''; - } - - return this.glyphNames.glyphIndexToName(gid); -}; - -/** - * Retrieve the value of the kerning pair 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. - * For GPOS kerning, this method uses the default script and language, which covers - * most use cases. To have greater control, use font.position.getKerningValue . - * @param {opentype.Glyph} leftGlyph - * @param {opentype.Glyph} rightGlyph - * @return {Number} - */ -Font.prototype.getKerningValue = function(leftGlyph, rightGlyph) { - leftGlyph = leftGlyph.index || leftGlyph; - rightGlyph = rightGlyph.index || rightGlyph; - const gposKerning = this.position.defaultKerningTables; - if (gposKerning) { - return this.position.getKerningValue(gposKerning, leftGlyph, rightGlyph); - } - // "kern" table - return this.kerningPairs[leftGlyph + ',' + rightGlyph] || 0; -}; - -/** - * @typedef GlyphRenderOptions - * @type Object - * @property {string} [script] - script used to determine which features to apply. By default, 'DFLT' or 'latn' is used. - * See https://www.microsoft.com/typography/otspec/scripttags.htm - * @property {string} [language='dflt'] - language system used to determine which features to apply. - * See https://www.microsoft.com/typography/developers/opentype/languagetags.aspx - * @property {boolean} [kerning=true] - whether to include kerning values - * @property {object} [features] - OpenType Layout feature tags. Used to enable or disable the features of the given script/language system. - * See https://www.microsoft.com/typography/otspec/featuretags.htm - */ -Font.prototype.defaultRenderOptions = { - kerning: true, - features: [ - /** - * these 4 features are required to render Arabic text properly - * and shouldn't be turned off when rendering arabic text. - */ - { script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] }, - { script: 'latn', tags: ['liga', 'rlig'] }, - { script: 'thai', tags: ['liga', 'rlig', 'ccmp'] }, - ] -}; - -/** - * Helper function that invokes the given callback for each glyph in the given text. - * The callback gets `(glyph, x, y, fontSize, options)`.* @param {string} text - * @param {string} text - The text to apply. - * @param {number} [x=0] - Horizontal position of the beginning of the text. - * @param {number} [y=0] - Vertical position of the *baseline* of the text. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - * @param {Function} callback - */ -Font.prototype.forEachGlyph = function(text, x, y, fontSize, options, callback) { - x = x !== undefined ? x : 0; - y = y !== undefined ? y : 0; - fontSize = fontSize !== undefined ? fontSize : 72; - options = Object.assign({}, this.defaultRenderOptions, options); - const fontScale = 1 / this.unitsPerEm * fontSize; - const glyphs = this.stringToGlyphs(text, options); - let kerningLookups; - if (options.kerning) { - const script = options.script || this.position.getDefaultScriptName(); - kerningLookups = this.position.getKerningTables(script, options.language); - } - for (let i = 0; i < glyphs.length; i += 1) { - const glyph = glyphs[i]; - callback.call(this, glyph, x, y, fontSize, options); - if (glyph.advanceWidth) { - x += glyph.advanceWidth * fontScale; - } - - if (options.kerning && i < glyphs.length - 1) { - // We should apply position adjustment lookups in a more generic way. - // Here we only use the xAdvance value. - const kerningValue = kerningLookups ? - this.position.getKerningValue(kerningLookups, glyph.index, glyphs[i + 1].index) : - this.getKerningValue(glyph, glyphs[i + 1]); - x += kerningValue * fontScale; - } - - if (options.letterSpacing) { - x += options.letterSpacing * fontSize; - } else if (options.tracking) { - x += (options.tracking / 1000) * fontSize; - } - } - return x; -}; - -/** - * Create a Path object that represents the given text. - * @param {string} text - The text to create. - * @param {number} [x=0] - Horizontal position of the beginning of the text. - * @param {number} [y=0] - Vertical position of the *baseline* of the text. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - * @return {opentype.Path} - */ -Font.prototype.getPath = function(text, x, y, fontSize, options) { - const fullPath = new Path(); - this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { - const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this); - fullPath.extend(glyphPath); - }); - return fullPath; -}; - -/** - * Create an array of Path objects that represent the glyphs of a given text. - * @param {string} text - The text to create. - * @param {number} [x=0] - Horizontal position of the beginning of the text. - * @param {number} [y=0] - Vertical position of the *baseline* of the text. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - * @return {opentype.Path[]} - */ -Font.prototype.getPaths = function(text, x, y, fontSize, options) { - const glyphPaths = []; - this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { - const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this); - glyphPaths.push(glyphPath); - }); - - return glyphPaths; -}; - -/** - * Returns the advance width of a text. - * - * This is something different than Path.getBoundingBox() as 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 - * - * @param {string} text - The text to create. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - * @return advance width - */ -Font.prototype.getAdvanceWidth = function(text, fontSize, options) { - return this.forEachGlyph(text, 0, 0, fontSize, options, function() {}); -}; - -/** - * Draw the text on the given drawing context. - * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. - * @param {string} text - The text to create. - * @param {number} [x=0] - Horizontal position of the beginning of the text. - * @param {number} [y=0] - Vertical position of the *baseline* of the text. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - */ -Font.prototype.draw = function(ctx, text, x, y, fontSize, options) { - this.getPath(text, x, y, fontSize, options).draw(ctx); -}; - -/** - * 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. - * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. - * @param {string} text - The text to create. - * @param {number} [x=0] - Horizontal position of the beginning of the text. - * @param {number} [y=0] - Vertical position of the *baseline* of the text. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - */ -Font.prototype.drawPoints = function(ctx, text, x, y, fontSize, options) { - this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { - glyph.drawPoints(ctx, gX, gY, gFontSize); - }); -}; - -/** - * 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. - * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. - * @param {string} text - The text to create. - * @param {number} [x=0] - Horizontal position of the beginning of the text. - * @param {number} [y=0] - Vertical position of the *baseline* of the text. - * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. - * @param {GlyphRenderOptions=} options - */ -Font.prototype.drawMetrics = function(ctx, text, x, y, fontSize, options) { - this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { - glyph.drawMetrics(ctx, gX, gY, gFontSize); - }); -}; - -/** - * @param {string} - * @return {string} - */ -Font.prototype.getEnglishName = function(name) { - const translations = (this.names.unicode || this.names.macintosh || this.names.windows)[name]; - if (translations) { - return translations.en; - } -}; - -/** - * Validate - */ -Font.prototype.validation = new validation.MessageStack(); -Font.prototype.validate = function() { - const validationMessages = []; - const _this = this; - - function assert(predicate, message) { - if (!predicate) { - validationMessages.push(_this.validation.add(message, validation.errorTypes.WARNING)); - } - } - - function assertNamePresent(name) { - const englishName = _this.getEnglishName(name); - assert(englishName && englishName.trim().length > 0, - 'No English ' + name + ' specified.'); - } - - // Identification information - assertNamePresent('fontFamily'); - assertNamePresent('weightName'); - assertNamePresent('manufacturer'); - assertNamePresent('copyright'); - assertNamePresent('version'); - - // Dimension information - assert(this.unitsPerEm > 0, 'No unitsPerEm specified.'); - - this.validation.logMessages(); - - return validationMessages; -}; - -/** - * Convert the font object to a SFNT data structure. - * This structure contains all the necessary tables and metadata to create a binary OTF file. - * @return {opentype.Table} - */ -Font.prototype.toTables = function() { - return sfnt.fontToTable(this); -}; -/** - * @deprecated Font.toBuffer is deprecated. Use Font.toArrayBuffer instead. - */ -Font.prototype.toBuffer = function() { - this.validation.add('Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.', validation.errorTypes.DEPRECATED); - return this.toArrayBuffer(); -}; -/** - * Converts a `opentype.Font` into an `ArrayBuffer` - * @return {ArrayBuffer} - */ -Font.prototype.toArrayBuffer = function() { - const sfntTable = this.toTables(); - const bytes = sfntTable.encode(); - const buffer = new ArrayBuffer(bytes.length); - const intArray = new Uint8Array(buffer); - for (let i = 0; i < bytes.length; i++) { - intArray[i] = bytes[i]; - } - - return buffer; -}; - -/** - * Initiate a download of the OpenType font. - */ -Font.prototype.download = function(fileName) { - const familyName = this.getEnglishName('fontFamily'); - const styleName = this.getEnglishName('fontSubfamily'); - fileName = fileName || familyName.replace(/\s/g, '') + '-' + styleName + '.otf'; - const arrayBuffer = this.toArrayBuffer(); - - if (isBrowser()) { - window.URL = window.URL || window.webkitURL; - - if (window.URL) { - const dataView = new DataView(arrayBuffer); - const blob = new Blob([dataView], {type: 'font/opentype'}); - - let link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = fileName; - - let event = document.createEvent('MouseEvents'); - event.initEvent('click', true, false); - link.dispatchEvent(event); - } else { - validation.add('Font file could not be downloaded. Try using a different browser.'); - } - } else { - const fs = require('fs'); - const buffer = Buffer.alloc(arrayBuffer.byteLength); - const view = new Uint8Array(arrayBuffer); - for (let i = 0; i < buffer.length; ++i) { - buffer[i] = view[i]; - } - fs.writeFileSync(fileName, buffer); - } -}; - -/** - * @private - */ -Font.prototype.fsSelectionValues = { - ITALIC: 0x001, //1 - UNDERSCORE: 0x002, //2 - NEGATIVE: 0x004, //4 - OUTLINED: 0x008, //8 - STRIKEOUT: 0x010, //16 - BOLD: 0x020, //32 - REGULAR: 0x040, //64 - USER_TYPO_METRICS: 0x080, //128 - WWS: 0x100, //256 - OBLIQUE: 0x200 //512 -}; - -/** - * @private - */ -Font.prototype.macStyleValues = { - BOLD: 0x001, //1 - ITALIC: 0x002, //2 - UNDERLINE: 0x004, //4 - OUTLINED: 0x008, //8 - SHADOW: 0x010, //16 - CONDENSED: 0x020, //32 - EXTENDED: 0x040, //64 -}; - -/** - * @private - */ -Font.prototype.usWidthClasses = { - ULTRA_CONDENSED: 1, - EXTRA_CONDENSED: 2, - CONDENSED: 3, - SEMI_CONDENSED: 4, - MEDIUM: 5, - SEMI_EXPANDED: 6, - EXPANDED: 7, - EXTRA_EXPANDED: 8, - ULTRA_EXPANDED: 9 -}; - -/** - * @private - */ -Font.prototype.usWeightClasses = { - THIN: 100, - EXTRA_LIGHT: 200, - LIGHT: 300, - NORMAL: 400, - MEDIUM: 500, - SEMI_BOLD: 600, - BOLD: 700, - EXTRA_BOLD: 800, - BLACK: 900 -}; - -export default Font; From e2a1d83afa4ccf259e0e238730af383094f4a6c9 Mon Sep 17 00:00:00 2001 From: Connum Date: Fri, 24 Nov 2023 21:19:17 +0100 Subject: [PATCH 7/9] catch another error that can occur with subset font files --- src/encoding.js | 4 +++- src/glyphset.js | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/encoding.js b/src/encoding.js index 865fad9f..eed40ba1 100644 --- a/src/encoding.js +++ b/src/encoding.js @@ -252,7 +252,9 @@ function addGlyphNamesAll(font) { const c = charCodes[i]; const glyphIndex = glyphIndexMap[c]; glyph = font.glyphs.get(glyphIndex); - glyph.addUnicode(parseInt(c)); + if(glyph) { + glyph.addUnicode(parseInt(c)); + } } for (let i = 0; i < font.glyphs.length; i += 1) { diff --git a/src/glyphset.js b/src/glyphset.js index 1f889fba..f70d466c 100644 --- a/src/glyphset.js +++ b/src/glyphset.js @@ -1,6 +1,7 @@ // The GlyphSet object import Glyph from './glyph.js'; +import { logger } from './logger.js'; // Define a property on the glyph that depends on the path being loaded. function defineDependentProperty(glyph, externalName, internalName) { @@ -63,7 +64,12 @@ if(typeof Symbol !== 'undefined' && Symbol.iterator) { GlyphSet.prototype.get = function(index) { // this.glyphs[index] is 'undefined' when low memory mode is on. glyph is pushed on request only. if (this.glyphs[index] === undefined) { - if (typeof this.font._push !== 'function') return; + if (typeof this.font._push !== 'function') { + if (index !== null) { + logger.add(`Trying to access unknown glyph at index ${index}`, logger.ErrorTypes.WARNING); + } + return; + } this.font._push(index); if (typeof this.glyphs[index] === 'function') { From d4ac90bccc128f953ff806e9640449ee2641307f Mon Sep 17 00:00:00 2001 From: Connum Date: Fri, 24 Nov 2023 21:30:49 +0100 Subject: [PATCH 8/9] make message event cancelable --- src/logger.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/logger.js b/src/logger.js index 66d74326..3dfe110e 100644 --- a/src/logger.js +++ b/src/logger.js @@ -76,15 +76,18 @@ class MessageLogger { let doLog = !!(this.logLevel & type); if (isBrowser()) { - document.dispatchEvent( - new CustomEvent('opentypejs:message', { - detail: { - message, - logged: doLog, - logger: this.logLevel - } - }) - ); + const messageEvent = new CustomEvent('opentypejs:message', { + cancelable: true, + detail: { + message, + doLog: doLog, + logger: this.logLevel + } + }); + const cancelled = document.dispatchEvent(messageEvent); + if (cancelled) { + doLog = false; + } } if (doLog) { From 94ce748fa2446d9d30c364e4e39e1ec573a7394a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Constantin=20Gro=C3=9F?= Date: Mon, 27 Nov 2023 00:13:05 +0000 Subject: [PATCH 9/9] return undefined cmap instead of throwing --- src/tables/cmap.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tables/cmap.js b/src/tables/cmap.js index 3130b051..6dfbae15 100644 --- a/src/tables/cmap.js +++ b/src/tables/cmap.js @@ -6,7 +6,7 @@ import parse from '../parse.js'; import table from '../table.js'; import { eightBitMacEncodings } from '../types.js'; import { getEncoding } from '../tables/name.js'; - + function parseCmapTableFormat0(cmap, p, platformID, encodingID) { // Length in bytes of the index map cmap.length = p.parseUShort(); @@ -199,7 +199,8 @@ function parseCmapTable(data, start) { if (offset === -1) { // There is no cmap table in the font that we support. - throw new Error('No valid cmap sub-tables found.'); + // logging will be handled down the line if return now + return; } const p = new parse.Parser(data, start + offset);