UNPKG

opentype.js

Version:
336 lines (291 loc) 11.9 kB
// The `sfnt` wrapper provides organization for the tables in the font. // It is the top-level data structure in a font. // https://www.microsoft.com/typography/OTSPEC/otff.htm // Recommendations for creating OpenType Fonts: // http://www.microsoft.com/typography/otspec140/recom.htm import check from '../check'; import table from '../table'; import cmap from './cmap'; import cff from './cff'; import head from './head'; import hhea from './hhea'; import hmtx from './hmtx'; import ltag from './ltag'; import maxp from './maxp'; import _name from './name'; import os2 from './os2'; import post from './post'; import gsub from './gsub'; import meta from './meta'; function log2(v) { return Math.log(v) / Math.log(2) | 0; } function computeCheckSum(bytes) { while (bytes.length % 4 !== 0) { bytes.push(0); } let sum = 0; for (let i = 0; i < bytes.length; i += 4) { sum += (bytes[i] << 24) + (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + (bytes[i + 3]); } sum %= Math.pow(2, 32); return sum; } function makeTableRecord(tag, checkSum, offset, length) { return new table.Record('Table Record', [ {name: 'tag', type: 'TAG', value: tag !== undefined ? tag : ''}, {name: 'checkSum', type: 'ULONG', value: checkSum !== undefined ? checkSum : 0}, {name: 'offset', type: 'ULONG', value: offset !== undefined ? offset : 0}, {name: 'length', type: 'ULONG', value: length !== undefined ? length : 0} ]); } function makeSfntTable(tables) { const sfnt = new table.Table('sfnt', [ {name: 'version', type: 'TAG', value: 'OTTO'}, {name: 'numTables', type: 'USHORT', value: 0}, {name: 'searchRange', type: 'USHORT', value: 0}, {name: 'entrySelector', type: 'USHORT', value: 0}, {name: 'rangeShift', type: 'USHORT', value: 0} ]); sfnt.tables = tables; sfnt.numTables = tables.length; const highestPowerOf2 = Math.pow(2, log2(sfnt.numTables)); sfnt.searchRange = 16 * highestPowerOf2; sfnt.entrySelector = log2(highestPowerOf2); sfnt.rangeShift = sfnt.numTables * 16 - sfnt.searchRange; const recordFields = []; const tableFields = []; let offset = sfnt.sizeOf() + (makeTableRecord().sizeOf() * sfnt.numTables); while (offset % 4 !== 0) { offset += 1; tableFields.push({name: 'padding', type: 'BYTE', value: 0}); } for (let i = 0; i < tables.length; i += 1) { const t = tables[i]; check.argument(t.tableName.length === 4, 'Table name' + t.tableName + ' is invalid.'); const tableLength = t.sizeOf(); const tableRecord = makeTableRecord(t.tableName, computeCheckSum(t.encode()), offset, tableLength); recordFields.push({name: tableRecord.tag + ' Table Record', type: 'RECORD', value: tableRecord}); tableFields.push({name: t.tableName + ' table', type: 'RECORD', value: t}); offset += tableLength; check.argument(!isNaN(offset), 'Something went wrong calculating the offset.'); while (offset % 4 !== 0) { offset += 1; tableFields.push({name: 'padding', type: 'BYTE', value: 0}); } } // Table records need to be sorted alphabetically. recordFields.sort(function(r1, r2) { if (r1.value.tag > r2.value.tag) { return 1; } else { return -1; } }); sfnt.fields = sfnt.fields.concat(recordFields); sfnt.fields = sfnt.fields.concat(tableFields); return sfnt; } // Get the metrics for a character. If the string has more than one character // this function returns metrics for the first available character. // You can provide optional fallback metrics if no characters are available. function metricsForChar(font, chars, notFoundMetrics) { for (let i = 0; i < chars.length; i += 1) { const glyphIndex = font.charToGlyphIndex(chars[i]); if (glyphIndex > 0) { const glyph = font.glyphs.get(glyphIndex); return glyph.getMetrics(); } } return notFoundMetrics; } function average(vs) { let sum = 0; for (let i = 0; i < vs.length; i += 1) { sum += vs[i]; } return sum / vs.length; } // Convert the font object to a SFNT data structure. // This structure contains all the necessary tables and metadata to create a binary OTF file. function fontToSfntTable(font) { const xMins = []; const yMins = []; const xMaxs = []; const yMaxs = []; const advanceWidths = []; const leftSideBearings = []; const rightSideBearings = []; let firstCharIndex; let lastCharIndex = 0; let ulUnicodeRange1 = 0; let ulUnicodeRange2 = 0; let ulUnicodeRange3 = 0; let ulUnicodeRange4 = 0; for (let i = 0; i < font.glyphs.length; i += 1) { const glyph = font.glyphs.get(i); const unicode = glyph.unicode | 0; if (isNaN(glyph.advanceWidth)) { throw new Error('Glyph ' + glyph.name + ' (' + i + '): advanceWidth is not a number.'); } if (firstCharIndex > unicode || firstCharIndex === undefined) { // ignore .notdef char if (unicode > 0) { firstCharIndex = unicode; } } if (lastCharIndex < unicode) { lastCharIndex = unicode; } const position = os2.getUnicodeRange(unicode); if (position < 32) { ulUnicodeRange1 |= 1 << position; } else if (position < 64) { ulUnicodeRange2 |= 1 << position - 32; } else if (position < 96) { ulUnicodeRange3 |= 1 << position - 64; } else if (position < 123) { ulUnicodeRange4 |= 1 << position - 96; } else { throw new Error('Unicode ranges bits > 123 are reserved for internal usage'); } // Skip non-important characters. if (glyph.name === '.notdef') continue; const metrics = glyph.getMetrics(); xMins.push(metrics.xMin); yMins.push(metrics.yMin); xMaxs.push(metrics.xMax); yMaxs.push(metrics.yMax); leftSideBearings.push(metrics.leftSideBearing); rightSideBearings.push(metrics.rightSideBearing); advanceWidths.push(glyph.advanceWidth); } const globals = { xMin: Math.min.apply(null, xMins), yMin: Math.min.apply(null, yMins), xMax: Math.max.apply(null, xMaxs), yMax: Math.max.apply(null, yMaxs), advanceWidthMax: Math.max.apply(null, advanceWidths), advanceWidthAvg: average(advanceWidths), minLeftSideBearing: Math.min.apply(null, leftSideBearings), maxLeftSideBearing: Math.max.apply(null, leftSideBearings), minRightSideBearing: Math.min.apply(null, rightSideBearings) }; globals.ascender = font.ascender; globals.descender = font.descender; const headTable = head.make({ flags: 3, // 00000011 (baseline for font at y=0; left sidebearing point at x=0) unitsPerEm: font.unitsPerEm, xMin: globals.xMin, yMin: globals.yMin, xMax: globals.xMax, yMax: globals.yMax, lowestRecPPEM: 3, createdTimestamp: font.createdTimestamp }); const hheaTable = hhea.make({ ascender: globals.ascender, descender: globals.descender, advanceWidthMax: globals.advanceWidthMax, minLeftSideBearing: globals.minLeftSideBearing, minRightSideBearing: globals.minRightSideBearing, xMaxExtent: globals.maxLeftSideBearing + (globals.xMax - globals.xMin), numberOfHMetrics: font.glyphs.length }); const maxpTable = maxp.make(font.glyphs.length); const os2Table = os2.make(Object.assign({ xAvgCharWidth: Math.round(globals.advanceWidthAvg), usFirstCharIndex: firstCharIndex, usLastCharIndex: lastCharIndex, ulUnicodeRange1: ulUnicodeRange1, ulUnicodeRange2: ulUnicodeRange2, ulUnicodeRange3: ulUnicodeRange3, ulUnicodeRange4: ulUnicodeRange4, // See http://typophile.com/node/13081 for more info on vertical metrics. // We get metrics for typical characters (such as "x" for xHeight). // We provide some fallback characters if characters are unavailable: their // ordering was chosen experimentally. sTypoAscender: globals.ascender, sTypoDescender: globals.descender, sTypoLineGap: 0, usWinAscent: globals.yMax, usWinDescent: Math.abs(globals.yMin), ulCodePageRange1: 1, // FIXME: hard-code Latin 1 support for now sxHeight: metricsForChar(font, 'xyvw', {yMax: Math.round(globals.ascender / 2)}).yMax, sCapHeight: metricsForChar(font, 'HIKLEFJMNTZBDPRAGOQSUVWXY', globals).yMax, usDefaultChar: font.hasChar(' ') ? 32 : 0, // Use space as the default character, if available. usBreakChar: font.hasChar(' ') ? 32 : 0, // Use space as the break character, if available. }, font.tables.os2)); const hmtxTable = hmtx.make(font.glyphs); const cmapTable = cmap.make(font.glyphs); const englishFamilyName = font.getEnglishName('fontFamily'); const englishStyleName = font.getEnglishName('fontSubfamily'); const englishFullName = englishFamilyName + ' ' + englishStyleName; let postScriptName = font.getEnglishName('postScriptName'); if (!postScriptName) { postScriptName = englishFamilyName.replace(/\s/g, '') + '-' + englishStyleName; } const names = {}; for (let n in font.names) { names[n] = font.names[n]; } if (!names.uniqueID) { names.uniqueID = {en: font.getEnglishName('manufacturer') + ':' + englishFullName}; } if (!names.postScriptName) { names.postScriptName = {en: postScriptName}; } if (!names.preferredFamily) { names.preferredFamily = font.names.fontFamily; } if (!names.preferredSubfamily) { names.preferredSubfamily = font.names.fontSubfamily; } const languageTags = []; const nameTable = _name.make(names, languageTags); const ltagTable = (languageTags.length > 0 ? ltag.make(languageTags) : undefined); const postTable = post.make(); const cffTable = cff.make(font.glyphs, { version: font.getEnglishName('version'), fullName: englishFullName, familyName: englishFamilyName, weightName: englishStyleName, postScriptName: postScriptName, unitsPerEm: font.unitsPerEm, fontBBox: [0, globals.yMin, globals.ascender, globals.advanceWidthMax] }); const metaTable = (font.metas && Object.keys(font.metas).length > 0) ? meta.make(font.metas) : undefined; // The order does not matter because makeSfntTable() will sort them. const tables = [headTable, hheaTable, maxpTable, os2Table, nameTable, cmapTable, postTable, cffTable, hmtxTable]; if (ltagTable) { tables.push(ltagTable); } // Optional tables if (font.tables.gsub) { tables.push(gsub.make(font.tables.gsub)); } if (metaTable) { tables.push(metaTable); } const sfntTable = makeSfntTable(tables); // Compute the font's checkSum and store it in head.checkSumAdjustment. const bytes = sfntTable.encode(); const checkSum = computeCheckSum(bytes); const tableFields = sfntTable.fields; let checkSumAdjusted = false; for (let i = 0; i < tableFields.length; i += 1) { if (tableFields[i].name === 'head table') { tableFields[i].value.checkSumAdjustment = 0xB1B0AFBA - checkSum; checkSumAdjusted = true; break; } } if (!checkSumAdjusted) { throw new Error('Could not find head table with checkSum to adjust.'); } return sfntTable; } export default { make: makeSfntTable, fontToTable: fontToSfntTable, computeCheckSum };