svg2ttf
Version:
Converts SVG font to TTF font
305 lines (244 loc) • 7.74 kB
JavaScript
// 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