UNPKG

opentype.features.js

Version:

Library to list opentype fonts optional features.

252 lines (251 loc) 11.5 kB
"use strict"; // The `cmap` table stores the mappings from characters to glyphs. // https://www.microsoft.com/typography/OTSPEC/cmap.htm var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var check_1 = __importDefault(require("../check")); var parse_1 = __importDefault(require("../parse")); var table_1 = __importDefault(require("../table")); function parseCmapTableFormat12(cmap, p) { //Skip reserved. p.parseUShort(); // Length in bytes of the sub-tables. cmap.length = p.parseULong(); cmap.language = p.parseULong(); var groupCount; cmap.groupCount = groupCount = p.parseULong(); cmap.glyphIndexMap = {}; for (var i = 0; i < groupCount; i += 1) { var startCharCode = p.parseULong(); var endCharCode = p.parseULong(); var startGlyphId = p.parseULong(); for (var c = startCharCode; c <= endCharCode; c += 1) { cmap.glyphIndexMap[c] = startGlyphId; startGlyphId++; } } } function parseCmapTableFormat4(cmap, p, data, start, offset) { // Length in bytes of the sub-tables. cmap.length = p.parseUShort(); cmap.language = p.parseUShort(); // segCount is stored x 2. var segCount; cmap.segCount = segCount = p.parseUShort() >> 1; // Skip searchRange, entrySelector, rangeShift. p.skip('uShort', 3); // The "unrolled" mapping from character codes to glyph indices. cmap.glyphIndexMap = {}; var endCountParser = new parse_1.default.Parser(data, start + offset + 14); var startCountParser = new parse_1.default.Parser(data, start + offset + 16 + segCount * 2); var idDeltaParser = new parse_1.default.Parser(data, start + offset + 16 + segCount * 4); var idRangeOffsetParser = new parse_1.default.Parser(data, start + offset + 16 + segCount * 6); var glyphIndexOffset = start + offset + 16 + segCount * 8; for (var i = 0; i < segCount - 1; i += 1) { var glyphIndex = void 0; var endCount = endCountParser.parseUShort(); var startCount = startCountParser.parseUShort(); var idDelta = idDeltaParser.parseShort(); var idRangeOffset = idRangeOffsetParser.parseUShort(); for (var c = startCount; c <= endCount; c += 1) { if (idRangeOffset !== 0) { // The idRangeOffset is relative to the current position in the idRangeOffset array. // Take the current offset in the idRangeOffset array. glyphIndexOffset = (idRangeOffsetParser.offset + idRangeOffsetParser.relativeOffset - 2); // Add the value of the idRangeOffset, which will move us into the glyphIndex array. glyphIndexOffset += idRangeOffset; // Then add the character index of the current segment, multiplied by 2 for USHORTs. glyphIndexOffset += (c - startCount) * 2; glyphIndex = parse_1.default.getUShort(data, glyphIndexOffset); if (glyphIndex !== 0) { glyphIndex = (glyphIndex + idDelta) & 0xFFFF; } } else { glyphIndex = (c + idDelta) & 0xFFFF; } cmap.glyphIndexMap[c] = glyphIndex; } } } // Parse the `cmap` table. This table stores the mappings from characters to glyphs. // There are many available formats, but we only support the Windows format 4 and 12. // This function returns a `CmapEncoding` object or null if no supported format could be found. function parseCmapTable(data, start) { var cmap = {}; cmap.version = parse_1.default.getUShort(data, start); check_1.default.argument(cmap.version === 0, 'cmap table version should be 0.'); // The cmap table can contain many sub-tables, each with their own format. // We're only interested in a "platform 0" (Unicode format) and "platform 3" (Windows format) table. cmap.numTables = parse_1.default.getUShort(data, start + 2); var offset = -1; for (var i = cmap.numTables - 1; i >= 0; i -= 1) { var platformId = parse_1.default.getUShort(data, start + 4 + (i * 8)); var encodingId = parse_1.default.getUShort(data, start + 4 + (i * 8) + 2); if ((platformId === 3 && (encodingId === 0 || encodingId === 1 || encodingId === 10)) || (platformId === 0 && (encodingId === 0 || encodingId === 1 || encodingId === 2 || encodingId === 3 || encodingId === 4))) { offset = parse_1.default.getULong(data, start + 4 + (i * 8) + 4); break; } } if (offset === -1) { // There is no cmap table in the font that we support. throw new Error('No valid cmap sub-tables found.'); } var p = new parse_1.default.Parser(data, start + offset); cmap.format = p.parseUShort(); if (cmap.format === 12) { parseCmapTableFormat12(cmap, p); } else if (cmap.format === 4) { parseCmapTableFormat4(cmap, p, data, start, offset); } else { throw new Error('Only format 4 and 12 cmap tables are supported (found format ' + cmap.format + ').'); } return cmap; } function addSegment(t, code, glyphIndex) { t.segments.push({ end: code, start: code, delta: -(code - glyphIndex), offset: 0, glyphIndex: glyphIndex }); } function addTerminatorSegment(t) { t.segments.push({ end: 0xFFFF, start: 0xFFFF, delta: 1, offset: 0 }); } // Make cmap table, format 4 by default, 12 if needed only function makeCmapTable(glyphs) { // Plan 0 is the base Unicode Plan but emojis, for example are on another plan, and needs cmap 12 format (with 32bit) var isPlan0Only = true; var i; // Check if we need to add cmap format 12 or if format 4 only is fine for (i = glyphs.length - 1; i > 0; i -= 1) { var g = glyphs.get(i); if (g.unicode > 65535) { console.log('Adding CMAP format 12 (needed!)'); isPlan0Only = false; break; } } var cmapTable = [ { name: 'version', type: 'USHORT', value: 0 }, { name: 'numTables', type: 'USHORT', value: isPlan0Only ? 1 : 2 }, // CMAP 4 header { name: 'platformID', type: 'USHORT', value: 3 }, { name: 'encodingID', type: 'USHORT', value: 1 }, { name: 'offset', type: 'ULONG', value: isPlan0Only ? 12 : (12 + 8) } ]; if (!isPlan0Only) cmapTable = cmapTable.concat([ // CMAP 12 header { name: 'cmap12PlatformID', type: 'USHORT', value: 3 }, { name: 'cmap12EncodingID', type: 'USHORT', value: 10 }, { name: 'cmap12Offset', type: 'ULONG', value: 0 } ]); cmapTable = cmapTable.concat([ // CMAP 4 Subtable { name: 'format', type: 'USHORT', value: 4 }, { name: 'cmap4Length', type: 'USHORT', value: 0 }, { name: 'language', type: 'USHORT', value: 0 }, { name: 'segCountX2', type: 'USHORT', value: 0 }, { name: 'searchRange', type: 'USHORT', value: 0 }, { name: 'entrySelector', type: 'USHORT', value: 0 }, { name: 'rangeShift', type: 'USHORT', value: 0 } ]); var t = new table_1.default.Table('cmap', cmapTable); t.segments = []; for (i = 0; i < glyphs.length; i += 1) { var glyph = glyphs.get(i); for (var j = 0; j < glyph.unicodes.length; j += 1) { addSegment(t, glyph.unicodes[j], i); } t.segments = t.segments.sort(function (a, b) { return a.start - b.start; }); } addTerminatorSegment(t); var segCount = t.segments.length; var segCountToRemove = 0; // CMAP 4 // Set up parallel segment arrays. var endCounts = []; var startCounts = []; var idDeltas = []; var idRangeOffsets = []; var glyphIds = []; // CMAP 12 var cmap12Groups = []; // Reminder this loop is not following the specification at 100% // The specification -> find suites of characters and make a group // Here we're doing one group for each letter // Doing as the spec can save 8 times (or more) space for (i = 0; i < segCount; i += 1) { var segment = t.segments[i]; // CMAP 4 if (segment.end <= 65535 && segment.start <= 65535) { endCounts = endCounts.concat({ name: 'end_' + i, type: 'USHORT', value: segment.end }); startCounts = startCounts.concat({ name: 'start_' + i, type: 'USHORT', value: segment.start }); idDeltas = idDeltas.concat({ name: 'idDelta_' + i, type: 'SHORT', value: segment.delta }); idRangeOffsets = idRangeOffsets.concat({ name: 'idRangeOffset_' + i, type: 'USHORT', value: segment.offset }); if (segment.glyphId !== undefined) { glyphIds = glyphIds.concat({ name: 'glyph_' + i, type: 'USHORT', value: segment.glyphId }); } } else { // Skip Unicode > 65535 (16bit unsigned max) for CMAP 4, will be added in CMAP 12 segCountToRemove += 1; } // CMAP 12 // Skip Terminator Segment if (!isPlan0Only && segment.glyphIndex !== undefined) { cmap12Groups = cmap12Groups.concat({ name: 'cmap12Start_' + i, type: 'ULONG', value: segment.start }); cmap12Groups = cmap12Groups.concat({ name: 'cmap12End_' + i, type: 'ULONG', value: segment.end }); cmap12Groups = cmap12Groups.concat({ name: 'cmap12Glyph_' + i, type: 'ULONG', value: segment.glyphIndex }); } } // CMAP 4 Subtable t.segCountX2 = (segCount - segCountToRemove) * 2; t.searchRange = Math.pow(2, Math.floor(Math.log((segCount - segCountToRemove)) / Math.log(2))) * 2; t.entrySelector = Math.log(t.searchRange / 2) / Math.log(2); t.rangeShift = t.segCountX2 - t.searchRange; t.fields = t.fields.concat(endCounts); t.fields.push({ name: 'reservedPad', type: 'USHORT', value: 0 }); t.fields = t.fields.concat(startCounts); t.fields = t.fields.concat(idDeltas); t.fields = t.fields.concat(idRangeOffsets); t.fields = t.fields.concat(glyphIds); t.cmap4Length = 14 + // Subtable header endCounts.length * 2 + 2 + // reservedPad startCounts.length * 2 + idDeltas.length * 2 + idRangeOffsets.length * 2 + glyphIds.length * 2; if (!isPlan0Only) { // CMAP 12 Subtable var cmap12Length = 16 + // Subtable header cmap12Groups.length * 4; t.cmap12Offset = 12 + (2 * 2) + 4 + t.cmap4Length; t.fields = t.fields.concat([ { name: 'cmap12Format', type: 'USHORT', value: 12 }, { name: 'cmap12Reserved', type: 'USHORT', value: 0 }, { name: 'cmap12Length', type: 'ULONG', value: cmap12Length }, { name: 'cmap12Language', type: 'ULONG', value: 0 }, { name: 'cmap12nGroups', type: 'ULONG', value: cmap12Groups.length / 3 } ]); t.fields = t.fields.concat(cmap12Groups); } return t; } exports.default = { parse: parseCmapTable, make: makeCmapTable };