Current Dev State

This commit is contained in:
Tim Lorsbach
2025-06-23 20:13:54 +02:00
parent b4f9bb277d
commit ded50edaa2
22617 changed files with 4345095 additions and 174 deletions

View File

@ -0,0 +1,304 @@
'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;

View File

@ -0,0 +1,195 @@
'use strict';
// See documentation here: http://www.microsoft.com/typography/otspec/glyf.htm
var _ = require('lodash');
var ByteBuffer = require('microbuffer');
function getFlags(glyph) {
var result = [];
_.forEach(glyph.ttfContours, function (contour) {
_.forEach(contour, function (point) {
var flag = point.onCurve ? 1 : 0;
if (point.x === 0) {
flag += 16;
} else {
if (-0xFF <= point.x && point.x <= 0xFF) {
flag += 2; // the corresponding x-coordinate is 1 byte long
}
if (point.x > 0 && point.x <= 0xFF) {
flag += 16; // If x-Short Vector is set, this bit describes the sign of the value, with 1 equalling positive and 0 negative
}
}
if (point.y === 0) {
flag += 32;
} else {
if (-0xFF <= point.y && point.y <= 0xFF) {
flag += 4; // the corresponding y-coordinate is 1 byte long
}
if (point.y > 0 && point.y <= 0xFF) {
flag += 32; // If y-Short Vector is set, this bit describes the sign of the value, with 1 equalling positive and 0 negative.
}
}
result.push(flag);
});
});
return result;
}
//repeating flags can be packed
function compactFlags(flags) {
var result = [];
var prevFlag = -1;
var firstRepeat = false;
_.forEach(flags, function (flag) {
if (prevFlag === flag) {
if (firstRepeat) {
result[result.length - 1] += 8; //current flag repeats previous one, need to set 3rd bit of previous flag and set 1 to the current one
result.push(1);
firstRepeat = false;
} else {
result[result.length - 1]++; //when flag is repeating second or more times, we need to increase the last flag value
}
} else {
firstRepeat = true;
prevFlag = flag;
result.push(flag);
}
});
return result;
}
function getCoords(glyph, coordName) {
var result = [];
_.forEach(glyph.ttfContours, function (contour) {
result.push.apply(result, _.map(contour, coordName));
});
return result;
}
function compactCoords(coords) {
return _.filter(coords, function (coord) {
return coord !== 0;
});
}
//calculates length of glyph data in GLYF table
function glyphDataSize(glyph) {
// Ignore glyphs without outlines. These will get a length of zero in the “loca” table
if (!glyph.contours.length) {
return 0;
}
var result = 12; //glyph fixed properties
result += glyph.contours.length * 2; //add contours
_.forEach(glyph.ttf_x, function (x) {
//add 1 or 2 bytes for each coordinate depending of its size
result += ((-0xFF <= x && x <= 0xFF)) ? 1 : 2;
});
_.forEach(glyph.ttf_y, function (y) {
//add 1 or 2 bytes for each coordinate depending of its size
result += ((-0xFF <= y && y <= 0xFF)) ? 1 : 2;
});
// Add flags length to glyph size.
result += glyph.ttf_flags.length;
if (result % 4 !== 0) { // glyph size must be divisible by 4.
result += 4 - result % 4;
}
return result;
}
function tableSize(font) {
var result = 0;
_.forEach(font.glyphs, function (glyph) {
glyph.ttf_size = glyphDataSize(glyph);
result += glyph.ttf_size;
});
font.ttf_glyph_size = result; //sum of all glyph lengths
return result;
}
function createGlyfTable(font) {
_.forEach(font.glyphs, function (glyph) {
glyph.ttf_flags = getFlags(glyph);
glyph.ttf_flags = compactFlags(glyph.ttf_flags);
glyph.ttf_x = getCoords(glyph, 'x');
glyph.ttf_x = compactCoords(glyph.ttf_x);
glyph.ttf_y = getCoords(glyph, 'y');
glyph.ttf_y = compactCoords(glyph.ttf_y);
});
var buf = new ByteBuffer(tableSize(font));
_.forEach(font.glyphs, function (glyph) {
// Ignore glyphs without outlines. These will get a length of zero in the “loca” table
if (!glyph.contours.length) {
return;
}
var offset = buf.tell();
buf.writeInt16(glyph.contours.length); // numberOfContours
buf.writeInt16(glyph.xMin); // xMin
buf.writeInt16(glyph.yMin); // yMin
buf.writeInt16(glyph.xMax); // xMax
buf.writeInt16(glyph.yMax); // yMax
// Array of end points
var endPtsOfContours = -1;
var ttfContours = glyph.ttfContours;
_.forEach(ttfContours, function (contour) {
endPtsOfContours += contour.length;
buf.writeInt16(endPtsOfContours);
});
buf.writeInt16(0); // instructionLength, is not used here
// Array of flags
_.forEach(glyph.ttf_flags, function (flag) {
buf.writeInt8(flag);
});
// Array of X relative coordinates
_.forEach(glyph.ttf_x, function (x) {
if (-0xFF <= x && x <= 0xFF) {
buf.writeUint8(Math.abs(x));
} else {
buf.writeInt16(x);
}
});
// Array of Y relative coordinates
_.forEach(glyph.ttf_y, function (y) {
if (-0xFF <= y && y <= 0xFF) {
buf.writeUint8(Math.abs(y));
} else {
buf.writeInt16(y);
}
});
var tail = (buf.tell() - offset) % 4;
if (tail !== 0) { // glyph size must be divisible by 4.
for (; tail < 4; tail++) {
buf.writeUint8(0);
}
}
});
return buf;
}
module.exports = createGlyfTable;

View File

@ -0,0 +1,407 @@
'use strict';
// See documentation here: http://www.microsoft.com/typography/otspec/GSUB.htm
var _ = require('lodash');
var identifier = require('../utils.js').identifier;
var ByteBuffer = require('microbuffer');
function createScript() {
var scriptRecord = (0
+ 2 // Script DefaultLangSys Offset
+ 2 // Script[0] LangSysCount (0)
);
var langSys = (0
+ 2 // Script DefaultLangSys LookupOrder
+ 2 // Script DefaultLangSys ReqFeatureIndex
+ 2 // Script DefaultLangSys FeatureCount (0?)
+ 2 // Script Optional Feature Index[0]
);
var length = (0
+ scriptRecord
+ langSys
);
var buffer = new ByteBuffer(length);
// Script Record
// Offset to the start of langSys from the start of scriptRecord
buffer.writeUint16(scriptRecord); // DefaultLangSys
// Number of LangSys entries other than the default (none)
buffer.writeUint16(0);
// LangSys record (DefaultLangSys)
// LookupOrder
buffer.writeUint16(0);
// ReqFeatureIndex -> only one required feature: all ligatures
buffer.writeUint16(0);
// Number of FeatureIndex values for this language system (excludes the required feature)
buffer.writeUint16(1);
// FeatureIndex for the first optional feature
// Note: Adding the same feature to both the optional
// and the required features is a clear violation of the spec
// but it fixes IE not displaying the ligatures.
// See http://partners.adobe.com/public/developer/opentype/index_table_formats.html, Section “Language System Table”
// “FeatureCount: Number of FeatureIndex values for this language system-*excludes the required feature*” (emphasis added)
buffer.writeUint16(0);
return buffer;
}
function createScriptList() {
var scriptSize = (0
+ 4 // Tag
+ 2 // Offset
);
// tags should be arranged alphabetically
var scripts = [
[ 'DFLT', createScript() ],
[ 'latn', createScript() ]
];
var header = (0
+ 2 // Script count
+ scripts.length * scriptSize
);
var tableLengths = _.reduce(_.map(scripts, function (script) { return script[1].length; }), function (result, count) { return result + count; }, 0);
var length = (0
+ header
+ tableLengths
);
var buffer = new ByteBuffer(length);
// Script count
buffer.writeUint16(scripts.length);
// Write all ScriptRecords
var offset = header;
_.forEach(scripts, function (script) {
var name = script[0], table = script[1];
// Script identifier (DFLT/latn)
buffer.writeUint32(identifier(name));
// Offset to the ScriptRecord from start of the script list
buffer.writeUint16(offset);
// Increment offset by script table length
offset += table.length;
});
// Write all ScriptTables
_.forEach(scripts, function (script) {
var table = script[1];
buffer.writeBytes(table.buffer);
});
return buffer;
}
// Write one feature containing all ligatures
function createFeatureList() {
var header = (0
+ 2 // FeatureCount
+ 4 // FeatureTag[0]
+ 2 // Feature Offset[0]
);
var length = (0
+ header
+ 2 // FeatureParams[0]
+ 2 // LookupCount[0]
+ 2 // Lookup[0] LookupListIndex[0]
);
var buffer = new ByteBuffer(length);
// FeatureCount
buffer.writeUint16(1);
// FeatureTag[0]
buffer.writeUint32(identifier('liga'));
// Feature Offset[0]
buffer.writeUint16(header);
// FeatureParams[0]
buffer.writeUint16(0);
// LookupCount[0]
buffer.writeUint16(1);
// Index into lookup table. Since we only have ligatures, the index is always 0
buffer.writeUint16(0);
return buffer;
}
function createLigatureCoverage(font, ligatureGroups) {
var glyphCount = ligatureGroups.length;
var length = (0
+ 2 // CoverageFormat
+ 2 // GlyphCount
+ 2 * glyphCount // GlyphID[i]
);
var buffer = new ByteBuffer(length);
// CoverageFormat
buffer.writeUint16(1);
// Length
buffer.writeUint16(glyphCount);
_.forEach(ligatureGroups, function (group) {
buffer.writeUint16(group.startGlyph.id);
});
return buffer;
}
function createLigatureTable(font, ligature) {
var allCodePoints = font.codePoints;
var unicode = ligature.unicode;
var length = (0
+ 2 // LigGlyph
+ 2 // CompCount
+ 2 * (unicode.length - 1)
);
var buffer = new ByteBuffer(length);
// LigGlyph
var glyph = ligature.glyph;
buffer.writeUint16(glyph.id);
// CompCount
buffer.writeUint16(unicode.length);
// Compound glyphs (excluding first as its already in the coverage table)
for (var i = 1; i < unicode.length; i++) {
glyph = allCodePoints[unicode[i]];
buffer.writeUint16(glyph.id);
}
return buffer;
}
function createLigatureSet(font, codePoint, ligatures) {
var ligatureTables = [];
_.forEach(ligatures, function (ligature) {
ligatureTables.push(createLigatureTable(font, ligature));
});
var tableLengths = _.reduce(_.map(ligatureTables, 'length'), function (result, count) { return result + count; }, 0);
var offset = (0
+ 2 // LigatureCount
+ 2 * ligatures.length
);
var length = (0
+ offset
+ tableLengths
);
var buffer = new ByteBuffer(length);
// LigatureCount
buffer.writeUint16(ligatures.length);
// Ligature offsets
_.forEach(ligatureTables, function (table) {
// The offset to the current set, from SubstFormat
buffer.writeUint16(offset);
offset += table.length;
});
// Ligatures
_.forEach(ligatureTables, function (table) {
buffer.writeBytes(table.buffer);
});
return buffer;
}
function createLigatureList(font, ligatureGroups) {
var sets = [];
_.forEach(ligatureGroups, function (group) {
var set = createLigatureSet(font, group.codePoint, group.ligatures);
sets.push(set);
});
var setLengths = _.reduce(_.map(sets, 'length'), function (result, count) { return result + count; }, 0);
var coverage = createLigatureCoverage(font, ligatureGroups);
var tableOffset = (0
+ 2 // Lookup type
+ 2 // Lokup flag
+ 2 // SubTableCount
+ 2 // SubTable[0] Offset
);
var setOffset = (0
+ 2 // SubstFormat
+ 2 // Coverage offset
+ 2 // LigSetCount
+ 2 * sets.length // LigSet Offsets
);
var coverageOffset = setOffset + setLengths;
var length = (0
+ tableOffset
+ coverageOffset
+ coverage.length
);
var buffer = new ByteBuffer(length);
// Lookup type 4 ligatures
buffer.writeUint16(4);
// Lookup flag empty
buffer.writeUint16(0);
// Subtable count
buffer.writeUint16(1);
// Subtable[0] offset
buffer.writeUint16(tableOffset);
// SubstFormat
buffer.writeUint16(1);
// Coverage
buffer.writeUint16(coverageOffset);
// LigSetCount
buffer.writeUint16(sets.length);
_.forEach(sets, function (set) {
// The offset to the current set, from SubstFormat
buffer.writeUint16(setOffset);
setOffset += set.length;
});
_.forEach(sets, function (set) {
buffer.writeBytes(set.buffer);
});
buffer.writeBytes(coverage.buffer);
return buffer;
}
// Add a lookup for each ligature
function createLookupList(font) {
var ligatures = font.ligatures;
var groupedLigatures = {};
// Group ligatures by first code point
_.forEach(ligatures, function (ligature) {
var first = ligature.unicode[0];
if (!_.has(groupedLigatures, first)) {
groupedLigatures[first] = [];
}
groupedLigatures[first].push(ligature);
});
var ligatureGroups = [];
_.forEach(groupedLigatures, function (ligatures, codePoint) {
codePoint = parseInt(codePoint, 10);
// Order ligatures by length, descending
// “Ligatures with more components must be stored ahead of those with fewer components in order to be found”
// From: http://partners.adobe.com/public/developer/opentype/index_tag7.html#liga
ligatures.sort(function (ligA, ligB) {
return ligB.unicode.length - ligA.unicode.length;
});
ligatureGroups.push({
codePoint: codePoint,
ligatures: ligatures,
startGlyph: font.codePoints[codePoint]
});
});
ligatureGroups.sort(function (a, b) {
return a.startGlyph.id - b.startGlyph.id;
});
var offset = (0
+ 2 // Lookup count
+ 2 // Lookup[0] offset
);
var set = createLigatureList(font, ligatureGroups);
var length = (0
+ offset
+ set.length
);
var buffer = new ByteBuffer(length);
// Lookup count
buffer.writeUint16(1);
// Lookup[0] offset
buffer.writeUint16(offset);
// Lookup[0]
buffer.writeBytes(set.buffer);
return buffer;
}
function createGSUB(font) {
var scriptList = createScriptList();
var featureList = createFeatureList();
var lookupList = createLookupList(font);
var lists = [ scriptList, featureList, lookupList ];
var offset = (0
+ 4 // Version
+ 2 * lists.length // List offsets
);
// Calculate offsets
_.forEach(lists, function (list) {
list._listOffset = offset;
offset += list.length;
});
var length = offset;
var buffer = new ByteBuffer(length);
// Version
buffer.writeUint32(0x00010000);
// Offsets
_.forEach(lists, function (list) {
buffer.writeUint16(list._listOffset);
});
// List contents
_.forEach(lists, function (list) {
buffer.writeBytes(list.buffer);
});
return buffer;
}
module.exports = createGSUB;

View File

@ -0,0 +1,42 @@
'use strict';
// See documentation here: http://www.microsoft.com/typography/otspec/head.htm
var ByteBuffer = require('microbuffer');
function dateToUInt64(date) {
var startDate = new Date('1904-01-01T00:00:00.000Z');
return Math.floor((date - startDate) / 1000);
}
function createHeadTable(font) {
var buf = new ByteBuffer(54); // fixed table length
buf.writeInt32(0x10000); // version
buf.writeInt32(font.revision * 0x10000); // fontRevision
buf.writeUint32(0); // checkSumAdjustment
buf.writeUint32(0x5F0F3CF5); // magicNumber
// FLag meanings:
// Bit 0: Baseline for font at y=0;
// Bit 1: Left sidebearing point at x=0;
// Bit 3: Force ppem to integer values for all internal scaler math; may use fractional ppem sizes if this bit is clear;
buf.writeUint16(0x000B); // flags
buf.writeUint16(font.unitsPerEm); // unitsPerEm
buf.writeUint64(dateToUInt64(font.createdDate)); // created
buf.writeUint64(dateToUInt64(font.modifiedDate)); // modified
buf.writeInt16(font.xMin); // xMin
buf.writeInt16(font.yMin); // yMin
buf.writeInt16(font.xMax); // xMax
buf.writeInt16(font.yMax); // yMax
buf.writeUint16(font.macStyle); //macStyle
buf.writeUint16(font.lowestRecPPEM); // lowestRecPPEM
buf.writeInt16(2); // fontDirectionHint
buf.writeInt16(font.ttf_glyph_size < 0x20000 ? 0 : 1); // indexToLocFormat, 0 for short offsets, 1 for long offsets
buf.writeInt16(0); // glyphDataFormat
return buf;
}
module.exports = createHeadTable;

View File

@ -0,0 +1,31 @@
'use strict';
// See documentation here: http://www.microsoft.com/typography/otspec/hhea.htm
var ByteBuffer = require('microbuffer');
function createHHeadTable(font) {
var buf = new ByteBuffer(36); // fixed table length
buf.writeInt32(0x10000); // version
buf.writeInt16(font.ascent); // ascent
buf.writeInt16(font.descent); // descend
// Non zero lineGap causes offset in IE, https://github.com/fontello/svg2ttf/issues/37
buf.writeInt16(0); // lineGap
buf.writeUint16(font.maxWidth); // advanceWidthMax
buf.writeInt16(font.minLsb); // minLeftSideBearing
buf.writeInt16(font.minRsb); // minRightSideBearing
buf.writeInt16(font.maxExtent); // xMaxExtent
buf.writeInt16(1); // caretSlopeRise
buf.writeInt16(0); // caretSlopeRun
buf.writeUint32(0); // reserved1
buf.writeUint32(0); // reserved2
buf.writeUint16(0); // reserved3
buf.writeInt16(0); // metricDataFormat
buf.writeUint16(font.glyphs.length); // numberOfHMetrics
return buf;
}
module.exports = createHHeadTable;

View File

@ -0,0 +1,19 @@
'use strict';
// See documentation here: http://www.microsoft.com/typography/otspec/hmtx.htm
var _ = require('lodash');
var ByteBuffer = require('microbuffer');
function createHtmxTable(font) {
var buf = new ByteBuffer(font.glyphs.length * 4);
_.forEach(font.glyphs, function (glyph) {
buf.writeUint16(glyph.width); //advanceWidth
buf.writeInt16(glyph.xMin); //lsb
});
return buf;
}
module.exports = createHtmxTable;

View File

@ -0,0 +1,43 @@
'use strict';
// See documentation here: http://www.microsoft.com/typography/otspec/loca.htm
var _ = require('lodash');
var ByteBuffer = require('microbuffer');
function tableSize(font, isShortFormat) {
var result = (font.glyphs.length + 1) * (isShortFormat ? 2 : 4); // by glyph count + tail
return result;
}
function createLocaTable(font) {
var isShortFormat = font.ttf_glyph_size < 0x20000;
var buf = new ByteBuffer(tableSize(font, isShortFormat));
var location = 0;
// Array of offsets in GLYF table for each glyph
_.forEach(font.glyphs, function (glyph) {
if (isShortFormat) {
buf.writeUint16(location);
location += glyph.ttf_size / 2; // actual location must be divided to 2 in short format
} else {
buf.writeUint32(location);
location += glyph.ttf_size; //actual location is stored as is in long format
}
});
// The last glyph location is stored to get last glyph length
if (isShortFormat) {
buf.writeUint16(location);
} else {
buf.writeUint32(location);
}
return buf;
}
module.exports = createLocaTable;

View File

@ -0,0 +1,46 @@
'use strict';
// See documentation here: http://www.microsoft.com/typography/otspec/maxp.htm
var _ = require('lodash');
var ByteBuffer = require('microbuffer');
// Find max points in glyph TTF contours.
function getMaxPoints(font) {
return _.max(_.map(font.glyphs, function (glyph) {
return _.reduce(glyph.ttfContours, function (sum, ctr) { return sum + ctr.length; }, 0);
}));
}
function getMaxContours(font) {
return _.max(_.map(font.glyphs, function (glyph) {
return glyph.ttfContours.length;
}));
}
function createMaxpTable(font) {
var buf = new ByteBuffer(32);
buf.writeInt32(0x10000); // version
buf.writeUint16(font.glyphs.length); // numGlyphs
buf.writeUint16(getMaxPoints(font)); // maxPoints
buf.writeUint16(getMaxContours(font)); // maxContours
buf.writeUint16(0); // maxCompositePoints
buf.writeUint16(0); // maxCompositeContours
buf.writeUint16(2); // maxZones
buf.writeUint16(0); // maxTwilightPoints
// It is unclear how to calculate maxStorage, maxFunctionDefs and maxInstructionDefs.
// These are magic constants now, with values exceeding values from FontForge
buf.writeUint16(10); // maxStorage
buf.writeUint16(10); // maxFunctionDefs
buf.writeUint16(0); // maxInstructionDefs
buf.writeUint16(255); // maxStackElements
buf.writeUint16(0); // maxSizeOfInstructions
buf.writeUint16(0); // maxComponentElements
buf.writeUint16(0); // maxComponentDepth
return buf;
}
module.exports = createMaxpTable;

View File

@ -0,0 +1,106 @@
'use strict';
// See documentation here: http://www.microsoft.com/typography/otspec/name.htm
var _ = require('lodash');
var ByteBuffer = require('microbuffer');
var Str = require('../../str');
var TTF_NAMES = {
COPYRIGHT: 0,
FONT_FAMILY: 1,
ID: 3,
DESCRIPTION: 10,
URL_VENDOR: 11
};
function tableSize(names) {
var result = 6; // table header
_.forEach(names, function (name) {
result += 12 + name.data.length; //name header and data
});
return result;
}
function getStrings(name, id) {
var result = [];
var str = new Str(name);
result.push({ data: str.toUTF8Bytes(), id: id, platformID : 1, encodingID : 0, languageID : 0 }); //mac standard
result.push({ data: str.toUCS2Bytes(), id: id, platformID : 3, encodingID : 1, languageID : 0x409 }); //windows standard
return result;
}
// Collect font names
function getNames(font) {
var result = [];
if (font.copyright) {
result.push.apply(result, getStrings(font.copyright, TTF_NAMES.COPYRIGHT));
}
if (font.familyName) {
result.push.apply(result, getStrings(font.familyName, TTF_NAMES.FONT_FAMILY));
}
if (font.id) {
result.push.apply(result, getStrings(font.id, TTF_NAMES.ID));
}
result.push.apply(result, getStrings('Generated by svg2ttf from Fontello project.', TTF_NAMES.DESCRIPTION));
result.push.apply(result, getStrings('http://fontello.com', TTF_NAMES.URL_VENDOR));
_.forEach(font.sfntNames, function (sfntName) {
result.push.apply(result, getStrings(sfntName.value, sfntName.id));
});
result.sort(function (a, b) {
var orderFields = [ 'platformID', 'encodingID', 'languageID', 'id' ];
var i;
for (i = 0; i < orderFields.length; i++) {
if (a[orderFields[i]] !== b[orderFields[i]]) {
return a[orderFields[i]] < b[orderFields[i]] ? -1 : 1;
}
}
return 0;
});
return result;
}
function createNameTable(font) {
var names = getNames(font);
var buf = new ByteBuffer(tableSize(names));
buf.writeUint16(0); // formatSelector
buf.writeUint16(names.length); // nameRecordsCount
var offsetPosition = buf.tell();
buf.writeUint16(0); // offset, will be filled later
var nameOffset = 0;
_.forEach(names, function (name) {
buf.writeUint16(name.platformID); // platformID
buf.writeUint16(name.encodingID); // platEncID
buf.writeUint16(name.languageID); // languageID, English (USA)
buf.writeUint16(name.id); // nameID
buf.writeUint16(name.data.length); // reclength
buf.writeUint16(nameOffset); // offset
nameOffset += name.data.length;
});
var actualStringDataOffset = buf.tell();
//Array of bytes with actual string data
_.forEach(names, function (name) {
buf.writeBytes(name.data);
});
//write actual string data offset
buf.seek(offsetPosition);
buf.writeUint16(actualStringDataOffset); // offset
return buf;
}
module.exports = createNameTable;

View File

@ -0,0 +1,74 @@
'use strict';
// See documentation here: http://www.microsoft.com/typography/otspec/os2.htm
var _ = require('lodash');
var identifier = require('../utils.js').identifier;
var ByteBuffer = require('microbuffer');
//get first glyph unicode
function getFirstCharIndex(font) {
return Math.max(0, Math.min(0xffff, Math.abs(_.minBy(Object.keys(font.codePoints), function (point) {
return parseInt(point, 10);
}))));
}
//get last glyph unicode
function getLastCharIndex(font) {
return Math.max(0, Math.min(0xffff, Math.abs(_.maxBy(Object.keys(font.codePoints), function (point) {
return parseInt(point, 10);
}))));
}
function createOS2Table(font) {
var buf = new ByteBuffer(86);
buf.writeUint16(1); //version
buf.writeInt16(font.avgWidth); // xAvgCharWidth
buf.writeUint16(font.weightClass); // usWeightClass
buf.writeUint16(font.widthClass); // usWidthClass
buf.writeInt16(font.fsType); // fsType
buf.writeInt16(font.ySubscriptXSize); // ySubscriptXSize
buf.writeInt16(font.ySubscriptYSize); //ySubscriptYSize
buf.writeInt16(font.ySubscriptXOffset); // ySubscriptXOffset
buf.writeInt16(font.ySubscriptYOffset); // ySubscriptYOffset
buf.writeInt16(font.ySuperscriptXSize); // ySuperscriptXSize
buf.writeInt16(font.ySuperscriptYSize); // ySuperscriptYSize
buf.writeInt16(font.ySuperscriptXOffset); // ySuperscriptXOffset
buf.writeInt16(font.ySuperscriptYOffset); // ySuperscriptYOffset
buf.writeInt16(font.yStrikeoutSize); // yStrikeoutSize
buf.writeInt16(font.yStrikeoutPosition); // yStrikeoutPosition
buf.writeInt16(font.familyClass); // sFamilyClass
buf.writeUint8(font.panose.familyType); // panose.bFamilyType
buf.writeUint8(font.panose.serifStyle); // panose.bSerifStyle
buf.writeUint8(font.panose.weight); // panose.bWeight
buf.writeUint8(font.panose.proportion); // panose.bProportion
buf.writeUint8(font.panose.contrast); // panose.bContrast
buf.writeUint8(font.panose.strokeVariation); // panose.bStrokeVariation
buf.writeUint8(font.panose.armStyle); // panose.bArmStyle
buf.writeUint8(font.panose.letterform); // panose.bLetterform
buf.writeUint8(font.panose.midline); // panose.bMidline
buf.writeUint8(font.panose.xHeight); // panose.bXHeight
// TODO: This field is used to specify the Unicode blocks or ranges based on the 'cmap' table.
buf.writeUint32(0); // ulUnicodeRange1
buf.writeUint32(0); // ulUnicodeRange2
buf.writeUint32(0); // ulUnicodeRange3
buf.writeUint32(0); // ulUnicodeRange4
buf.writeUint32(identifier('PfEd')); // achVendID, equal to PfEd
buf.writeUint16(font.fsSelection); // fsSelection
buf.writeUint16(getFirstCharIndex(font)); // usFirstCharIndex
buf.writeUint16(getLastCharIndex(font)); // usLastCharIndex
buf.writeInt16(font.ascent); // sTypoAscender
buf.writeInt16(font.descent); // sTypoDescender
buf.writeInt16(font.lineGap); // lineGap
// Enlarge win acscent/descent to avoid clipping
buf.writeInt16(Math.max(font.yMax, font.ascent)); // usWinAscent
buf.writeInt16(-Math.min(font.yMin, font.descent)); // usWinDescent
buf.writeInt32(1); // ulCodePageRange1, Latin 1
buf.writeInt32(0); // ulCodePageRange2
return buf;
}
module.exports = createOS2Table;

View File

@ -0,0 +1,73 @@
'use strict';
// See documentation here: http://www.microsoft.com/typography/otspec/post.htm
var _ = require('lodash');
var ByteBuffer = require('microbuffer');
function tableSize(font, names) {
var result = 36; // table header
result += font.glyphs.length * 2; // name declarations
_.forEach(names, function (name) {
result += name.length;
});
return result;
}
function pascalString(str) {
var bytes = [];
var len = str ? (str.length < 256 ? str.length : 255) : 0; //length in Pascal string is limited with 255
bytes.push(len);
for (var i = 0; i < len; i++) {
var char = str.charCodeAt(i);
bytes.push(char < 128 ? char : 95); //non-ASCII characters are substituted with '_'
}
return bytes;
}
function createPostTable(font) {
var names = [];
_.forEach(font.glyphs, function (glyph) {
if (glyph.unicode !== 0) {
names.push(pascalString(glyph.name));
}
});
var buf = new ByteBuffer(tableSize(font, names));
buf.writeInt32(0x20000); // formatType, version 2.0
buf.writeInt32(font.italicAngle); // italicAngle
buf.writeInt16(font.underlinePosition); // underlinePosition
buf.writeInt16(font.underlineThickness); // underlineThickness
buf.writeUint32(font.isFixedPitch); // isFixedPitch
buf.writeUint32(0); // minMemType42
buf.writeUint32(0); // maxMemType42
buf.writeUint32(0); // minMemType1
buf.writeUint32(0); // maxMemType1
buf.writeUint16(font.glyphs.length); // numberOfGlyphs
// Array of glyph name indexes
var index = 258; // first index of custom glyph name, it is calculated as glyph name index + 258
_.forEach(font.glyphs, function (glyph) {
if (glyph.unicode === 0) {
buf.writeUint16(0);// missed element should have .notDef name in the Macintosh standard order.
} else {
buf.writeUint16(index++);
}
});
// Array of glyph name indexes
_.forEach(names, function (name) {
buf.writeBytes(name);
});
return buf;
}
module.exports = createPostTable;

View File

@ -0,0 +1,129 @@
'use strict';
var _ = require('lodash');
var math = require('../math');
// Remove points, that looks like straight line
function simplify(contours, accuracy) {
return _.map(contours, function (contour) {
var i, curr, prev, next;
var p, pPrev, pNext;
// run from the end, to simplify array elements removal
for (i = contour.length - 2; i > 1; i--) {
prev = contour[i - 1];
next = contour[i + 1];
curr = contour[i];
// skip point (both oncurve & offcurve),
// if [prev,next] is straight line
if (prev.onCurve && next.onCurve) {
p = new math.Point(curr.x, curr.y);
pPrev = new math.Point(prev.x, prev.y);
pNext = new math.Point(next.x, next.y);
if (math.isInLine(pPrev, p, pNext, accuracy)) {
contour.splice(i, 1);
}
}
}
return contour;
});
}
// Remove interpolateable oncurve points
// Those should be in the middle of nebor offcurve points
function interpolate(contours, accuracy) {
return _.map(contours, function (contour) {
var resContour = [];
_.forEach(contour, function (point, idx) {
// Never skip first and last points
if (idx === 0 || idx === (contour.length - 1)) {
resContour.push(point);
return;
}
var prev = contour[idx - 1];
var next = contour[idx + 1];
var p, pPrev, pNext;
// skip interpolateable oncurve points (if exactly between previous and next offcurves)
if (!prev.onCurve && point.onCurve && !next.onCurve) {
p = new math.Point(point.x, point.y);
pPrev = new math.Point(prev.x, prev.y);
pNext = new math.Point(next.x, next.y);
if (pPrev.add(pNext).div(2).sub(p).dist() < accuracy) {
return;
}
}
// keep the rest
resContour.push(point);
});
return resContour;
});
}
function roundPoints(contours) {
return _.map(contours, function (contour) {
return _.map(contour, function (point) {
return { x: Math.round(point.x), y: Math.round(point.y), onCurve: point.onCurve };
});
});
}
// Remove closing point if it is the same as first point of contour.
// TTF doesn't need this point when drawing contours.
function removeClosingReturnPoints(contours) {
return _.map(contours, function (contour) {
var length = contour.length;
if (length > 1 &&
contour[0].x === contour[length - 1].x &&
contour[0].y === contour[length - 1].y) {
contour.splice(length - 1);
}
return contour;
});
}
function toRelative(contours) {
var prevPoint = { x: 0, y: 0 };
var resContours = [];
var resContour;
_.forEach(contours, function (contour) {
resContour = [];
resContours.push(resContour);
_.forEach(contour, function (point) {
resContour.push({
x: point.x - prevPoint.x,
y: point.y - prevPoint.y,
onCurve: point.onCurve
});
prevPoint = point;
});
});
return resContours;
}
function identifier(string, littleEndian) {
var result = 0;
for (var i = 0; i < string.length; i++) {
result = result << 8;
var index = littleEndian ? string.length - i - 1 : i;
result += string.charCodeAt(index);
}
return result;
}
module.exports.interpolate = interpolate;
module.exports.simplify = simplify;
module.exports.roundPoints = roundPoints;
module.exports.removeClosingReturnPoints = removeClosingReturnPoints;
module.exports.toRelative = toRelative;
module.exports.identifier = identifier;