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/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/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/glyphset.js b/src/glyphset.js index adddb395..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) { @@ -37,7 +38,6 @@ function GlyphSet(font, glyphs) { this.glyphs[i] = glyph; } } - this.length = (glyphs && glyphs.length) || 0; } @@ -64,13 +64,20 @@ 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') { + 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') { 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..3dfe110e --- /dev/null +++ b/src/logger.js @@ -0,0 +1,144 @@ +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()) { + 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) { + 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/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); 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 } }