You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
305 lines
7.7 KiB
305 lines
7.7 KiB
2 years ago
|
'use strict';
|
||
|
|
||
|
// See documentation here: http://www.microsoft.com/typography/otspec/cmap.htm
|
||
|
|
||
|
var _ = require('lodash');
|
||
|
var ByteBuffer = require('microbuffer');
|
||
|
|
||
|
function getIDByUnicode(font, unicode) {
|
||
|
return font.codePoints[unicode] ? font.codePoints[unicode].id : 0;
|
||
|
}
|
||
|
|
||
|
// Calculate character segments with non-interruptable chains of unicodes
|
||
|
function getSegments(font, bounds) {
|
||
|
bounds = bounds || Number.MAX_VALUE;
|
||
|
|
||
|
var result = [];
|
||
|
var segment;
|
||
|
|
||
|
// prevEndCode only changes when a segment closes
|
||
|
_.forEach(font.codePoints, function (glyph, unicode) {
|
||
|
unicode = parseInt(unicode, 10);
|
||
|
if (unicode >= bounds) {
|
||
|
return false;
|
||
|
}
|
||
|
// Initialize first segment or add new segment if code "hole" is found
|
||
|
if (!segment || unicode !== (segment.end + 1)) {
|
||
|
if (segment) {
|
||
|
result.push(segment);
|
||
|
}
|
||
|
segment = {
|
||
|
start: unicode
|
||
|
};
|
||
|
}
|
||
|
segment.end = unicode;
|
||
|
});
|
||
|
|
||
|
// Need to finish the last segment
|
||
|
if (segment) {
|
||
|
result.push(segment);
|
||
|
}
|
||
|
|
||
|
_.forEach(result, function (segment) {
|
||
|
segment.length = segment.end - segment.start + 1;
|
||
|
});
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
// Returns an array of {unicode, glyph} sets for all valid code points up to bounds
|
||
|
function getCodePoints(codePoints, bounds) {
|
||
|
bounds = bounds || Number.MAX_VALUE;
|
||
|
|
||
|
var result = [];
|
||
|
|
||
|
_.forEach(codePoints, function (glyph, unicode) {
|
||
|
unicode = parseInt(unicode, 10);
|
||
|
// Since this is a sparse array, iterating will only yield the valid code points
|
||
|
if (unicode > bounds) {
|
||
|
return false;
|
||
|
}
|
||
|
result.push({
|
||
|
unicode: unicode,
|
||
|
glyph: glyph
|
||
|
});
|
||
|
});
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
function bufferForTable(format, length) {
|
||
|
var fieldWidth = format === 8 || format === 10 || format === 12 || format === 13 ? 4 : 2;
|
||
|
|
||
|
length += (0
|
||
|
+ fieldWidth // Format
|
||
|
+ fieldWidth // Length
|
||
|
+ fieldWidth // Language
|
||
|
);
|
||
|
|
||
|
var LANGUAGE = 0;
|
||
|
var buffer = new ByteBuffer(length);
|
||
|
|
||
|
var writer = fieldWidth === 4 ? buffer.writeUint32 : buffer.writeUint16;
|
||
|
|
||
|
// Format specifier
|
||
|
buffer.writeUint16(format);
|
||
|
if (fieldWidth === 4) {
|
||
|
// In case of formats 8.…, 10.…, 12.… and 13.…, this is the decimal part of the format number
|
||
|
// But since have not been any point releases, this can be zero in that case as well
|
||
|
buffer.writeUint16(0);
|
||
|
}
|
||
|
// Length
|
||
|
writer.call(buffer, length);
|
||
|
// Language code (0, only used for legacy quickdraw tables)
|
||
|
writer.call(buffer, LANGUAGE);
|
||
|
|
||
|
return buffer;
|
||
|
}
|
||
|
|
||
|
function createFormat0Table(font) {
|
||
|
var FORMAT = 0;
|
||
|
|
||
|
var i;
|
||
|
|
||
|
var length = 0xff + 1; //Format 0 maps only single-byte code points
|
||
|
|
||
|
var buffer = bufferForTable(FORMAT, length);
|
||
|
|
||
|
for (i = 0; i < length; i++) {
|
||
|
buffer.writeUint8(getIDByUnicode(font, i)); // existing char in table 0..255
|
||
|
}
|
||
|
return buffer;
|
||
|
}
|
||
|
|
||
|
function createFormat4Table(font) {
|
||
|
var FORMAT = 4;
|
||
|
|
||
|
var i;
|
||
|
|
||
|
var segments = getSegments(font, 0xFFFF);
|
||
|
var glyphIndexArrays = [];
|
||
|
|
||
|
_.forEach(segments, function (segment) {
|
||
|
var glyphIndexArray = [];
|
||
|
|
||
|
for (var unicode = segment.start; unicode <= segment.end; unicode++) {
|
||
|
glyphIndexArray.push(getIDByUnicode(font, unicode));
|
||
|
}
|
||
|
glyphIndexArrays.push(glyphIndexArray);
|
||
|
});
|
||
|
|
||
|
var segCount = segments.length + 1; // + 1 for the 0xFFFF section
|
||
|
var glyphIndexArrayLength = _.reduce(_.map(glyphIndexArrays, 'length'), function (result, count) { return result + count; }, 0);
|
||
|
|
||
|
var length = (0
|
||
|
+ 2 // segCountX2
|
||
|
+ 2 // searchRange
|
||
|
+ 2 // entrySelector
|
||
|
+ 2 // rangeShift
|
||
|
+ 2 * segCount // endCodes
|
||
|
+ 2 // Padding
|
||
|
+ 2 * segCount //startCodes
|
||
|
+ 2 * segCount //idDeltas
|
||
|
+ 2 * segCount //idRangeOffsets
|
||
|
+ 2 * glyphIndexArrayLength
|
||
|
);
|
||
|
|
||
|
var buffer = bufferForTable(FORMAT, length);
|
||
|
|
||
|
buffer.writeUint16(segCount * 2); // segCountX2
|
||
|
var maxExponent = Math.floor(Math.log(segCount) / Math.LN2);
|
||
|
var searchRange = 2 * Math.pow(2, maxExponent);
|
||
|
|
||
|
buffer.writeUint16(searchRange); // searchRange
|
||
|
buffer.writeUint16(maxExponent); // entrySelector
|
||
|
buffer.writeUint16(2 * segCount - searchRange); // rangeShift
|
||
|
|
||
|
// Array of end counts
|
||
|
_.forEach(segments, function (segment) {
|
||
|
buffer.writeUint16(segment.end);
|
||
|
});
|
||
|
buffer.writeUint16(0xFFFF); // endCountArray should be finished with 0xFFFF
|
||
|
|
||
|
buffer.writeUint16(0); // reservedPad
|
||
|
|
||
|
// Array of start counts
|
||
|
_.forEach(segments, function (segment) {
|
||
|
buffer.writeUint16(segment.start); //startCountArray
|
||
|
});
|
||
|
buffer.writeUint16(0xFFFF); // startCountArray should be finished with 0xFFFF
|
||
|
|
||
|
// Array of deltas. Leave it zero to not complicate things when using the glyph index array
|
||
|
for (i = 0; i < segments.length; i++) {
|
||
|
buffer.writeUint16(0); // delta is always zero because we use the glyph array
|
||
|
}
|
||
|
buffer.writeUint16(1); // idDeltaArray should be finished with 1
|
||
|
|
||
|
// Array of range offsets
|
||
|
var offset = 0;
|
||
|
|
||
|
for (i = 0; i < segments.length; i++) {
|
||
|
buffer.writeUint16(2 * ((segments.length - i + 1) + offset));
|
||
|
offset += glyphIndexArrays[i].length;
|
||
|
}
|
||
|
buffer.writeUint16(0); // rangeOffsetArray should be finished with 0
|
||
|
|
||
|
_.forEach(glyphIndexArrays, function (glyphIndexArray) {
|
||
|
_.forEach(glyphIndexArray, function (glyphId) {
|
||
|
buffer.writeUint16(glyphId);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
return buffer;
|
||
|
}
|
||
|
|
||
|
function createFormat12Table(font) {
|
||
|
var FORMAT = 12;
|
||
|
|
||
|
var codePoints = getCodePoints(font.codePoints);
|
||
|
|
||
|
var length = (0
|
||
|
+ 4 // nGroups
|
||
|
+ 4 * codePoints.length // startCharCode
|
||
|
+ 4 * codePoints.length // endCharCode
|
||
|
+ 4 * codePoints.length // startGlyphCode
|
||
|
);
|
||
|
|
||
|
var buffer = bufferForTable(FORMAT, length);
|
||
|
|
||
|
buffer.writeUint32(codePoints.length); // nGroups
|
||
|
_.forEach(codePoints, function (codePoint) {
|
||
|
buffer.writeUint32(codePoint.unicode); // startCharCode
|
||
|
buffer.writeUint32(codePoint.unicode); // endCharCode
|
||
|
buffer.writeUint32(codePoint.glyph.id); // startGlyphCode
|
||
|
});
|
||
|
|
||
|
return buffer;
|
||
|
}
|
||
|
|
||
|
function createCMapTable(font) {
|
||
|
var TABLE_HEAD = (0
|
||
|
+ 2 // platform
|
||
|
+ 2 // encoding
|
||
|
+ 4 // offset
|
||
|
);
|
||
|
|
||
|
var singleByteTable = createFormat0Table(font);
|
||
|
var twoByteTable = createFormat4Table(font);
|
||
|
var fourByteTable = createFormat12Table(font);
|
||
|
|
||
|
// Subtable headers must be sorted by platformID, encodingID
|
||
|
var tableHeaders = [
|
||
|
// subtable 4, unicode
|
||
|
{
|
||
|
platformID: 0,
|
||
|
encodingID: 3,
|
||
|
table: twoByteTable
|
||
|
},
|
||
|
// subtable 12, unicode
|
||
|
{
|
||
|
platformID: 0,
|
||
|
encodingID: 4,
|
||
|
table: fourByteTable
|
||
|
},
|
||
|
// subtable 0, mac standard
|
||
|
{
|
||
|
platformID: 1,
|
||
|
encodingID: 0,
|
||
|
table: singleByteTable
|
||
|
},
|
||
|
// subtable 4, windows standard, identical to the unicode table
|
||
|
{
|
||
|
platformID: 3,
|
||
|
encodingID: 1,
|
||
|
table: twoByteTable
|
||
|
},
|
||
|
// subtable 12, windows ucs4
|
||
|
{
|
||
|
platformID: 3,
|
||
|
encodingID: 10,
|
||
|
table: fourByteTable
|
||
|
}
|
||
|
];
|
||
|
|
||
|
var tables = [
|
||
|
twoByteTable,
|
||
|
singleByteTable,
|
||
|
fourByteTable
|
||
|
];
|
||
|
|
||
|
var tableOffset = (0
|
||
|
+ 2 // version
|
||
|
+ 2 // number of subtable headers
|
||
|
+ tableHeaders.length * TABLE_HEAD
|
||
|
);
|
||
|
|
||
|
// Calculate offsets for each table
|
||
|
_.forEach(tables, function (table) {
|
||
|
table._tableOffset = tableOffset;
|
||
|
tableOffset += table.length;
|
||
|
});
|
||
|
|
||
|
var length = tableOffset;
|
||
|
|
||
|
var buffer = new ByteBuffer(length);
|
||
|
|
||
|
// Write table header.
|
||
|
buffer.writeUint16(0); // version
|
||
|
buffer.writeUint16(tableHeaders.length); // count
|
||
|
|
||
|
// Write subtable headers
|
||
|
_.forEach(tableHeaders, function (header) {
|
||
|
buffer.writeUint16(header.platformID); // platform
|
||
|
buffer.writeUint16(header.encodingID); // encoding
|
||
|
buffer.writeUint32(header.table._tableOffset); // offset
|
||
|
});
|
||
|
|
||
|
// Write subtables
|
||
|
_.forEach(tables, function (table) {
|
||
|
buffer.writeBytes(table.buffer);
|
||
|
});
|
||
|
|
||
|
return buffer;
|
||
|
}
|
||
|
|
||
|
module.exports = createCMapTable;
|