Font subsetting support. Only includes characters in embedded fonts that are actually used in the document. Please report bugs if you find them!

This commit is contained in:
Devon Govett 2011-07-17 01:13:44 -04:00
parent 84853322b8
commit 9d90e25664
22 changed files with 1077 additions and 126 deletions

Binary file not shown.

View File

@ -1,72 +1,60 @@
class Data
constructor: (@data) ->
constructor: (@data = []) ->
@pos = 0
@length = @data.length
readByte: ->
@data[@pos++]
writeByte: (byte) ->
@data[@pos++] = byte
byteAt: (index) ->
@data[index]
readBool: ->
return !!@readByte()
writeBool: (val) ->
@writeByte if val then 1 else 0
readUInt32: ->
b1 = @readByte() << 24
b2 = @readByte() << 16
b3 = @readByte() << 8
b4 = @readByte()
b1 | b2 | b3 | b4
writeUInt32: (val) ->
@writeByte (val >>> 24) & 0xff
@writeByte (val >> 16) & 0xff
@writeByte (val >> 8) & 0xff
@writeByte val & 0xff
readInt32: ->
int = @readUInt32()
if int >= 2147483648 then int - 4294967296 else int
if int >= 0x80000000 then int - 0x100000000 else int
writeInt32: (val) ->
val += 0x100000000 if val < 0
@writeUInt32 val
readUInt16: ->
b1 = @readByte() << 8
b2 = @readByte()
b1 | b2
writeUInt16: (val) ->
@writeByte (val >> 8) & 0xff
@writeByte val & 0xff
readInt16: ->
int = @readUInt16()
if int >= 32768 then int - 65536 else int
if int >= 0x8000 then int - 0x10000 else int
readFloat32: ->
b1 = @readByte()
b2 = @readByte()
b3 = @readByte()
b4 = @readByte()
sign = 1 - ((b1 >> 7) << 1) # sign = bit 0
exp = (((b1 << 1) & 0xFF) | (b2 >> 7)) - 127 # exponent = bits 1..8
sig = ((b2 & 0x7F) << 16) | (b3 << 8) | 4 # significand = bits 9..31
return 0.0 if sig is 0 and exp is -127
return sign * (1 + 2e-23 * sig) * Math.pow(2, exp)
readFloat64: ->
b1 = @readByte()
b2 = @readByte()
b3 = @readByte()
b4 = @readByte()
b5 = @readByte()
b6 = @readByte()
b7 = @readByte()
b8 = @readByte()
sign = 1 - ((b1 >> 7) << 1) # sign = bit 0
exp = (((b1 << 4) & 0x7FF) | (b2 >> 4)) - 0123 # exponent = bits 1..11
# This crazy toString() stuff works around the fact that js ints are
# only 32 bits and signed, giving us 31 bits to work with
sig = (((b2 & 0xF) << 16) | (b3 << 8) | b4).toString(2) +
(if b5 >> 7 then '1' else '0') +
(((b5 & 0x7F) << 24) | (b6 << 16) | (b7 << 8) | b8).toString(2) # significand = bits 12..63
sig = parseInt(sig, 2)
return 0.0 if sig is 0 and exp is -1023
return sign * (1.0 + 2e-52 * sig) * Math.pow(2, exp)
writeInt16: (val) ->
val += 0x10000 if val < 0
@writeUInt16 val
readString: (length) ->
ret = []
@ -75,22 +63,19 @@ class Data
return ret.join ''
writeString: (val) ->
for i in [0...val.length]
@writeByte val.charCodeAt(i)
stringAt: (@pos, length) ->
@readString length
readShort: ->
@readInt16()
readLong: ->
b1 = @readByte()
b2 = @readByte()
b3 = @readByte()
b4 = @readByte()
long = (((((b1 << 8) + b2) << 8) + b3) << 8) + b4
long += 4294967296 if long < 0
return long
writeShort: (val) ->
@writeInt16 val
readLongLong: ->
b1 = @readByte()
b2 = @readByte()
@ -100,16 +85,43 @@ class Data
b6 = @readByte()
b7 = @readByte()
b8 = @readByte()
b1 << 56 + b2 << 48 + b3 << 40 | b4 << 32 + b5 << 24 + b6 << 16 + b7 << 8 + b8
if b1 & 0x80 # sign -> avoid overflow
return ((b1 ^ 0xff) * 0x100000000000000 +
(b2 ^ 0xff) * 0x1000000000000 +
(b3 ^ 0xff) * 0x10000000000 +
(b4 ^ 0xff) * 0x100000000 +
(b5 ^ 0xff) * 0x1000000 +
(b6 ^ 0xff) * 0x10000 +
(b7 ^ 0xff) * 0x100 +
(b8 ^ 0xff) + 1) * -1
return b1 * 0x100000000000000 +
b2 * 0x1000000000000 +
b3 * 0x10000000000 +
b4 * 0x100000000 +
b5 * 0x1000000 +
b6 * 0x10000 +
b7 * 0x100 +
b8
writeLongLong: (val) ->
high = Math.floor(val / 0x100000000)
low = val & 0xffffffff
@writeByte (high >> 24) & 0xff
@writeByte (high >> 16) & 0xff
@writeByte (high >> 8) & 0xff
@writeByte high & 0xff
@writeByte (low >> 24) & 0xff
@writeByte (low >> 16) & 0xff
@writeByte (low >> 8) & 0xff
@writeByte low & 0xff
readInt: ->
@readInt32()
readFloat: ->
@readFloat32()
readDouble: ->
@readFloat64()
writeInt: (val) ->
@writeInt32 val
slice: (start, end) ->
@data.slice start, end
@ -121,4 +133,8 @@ class Data
return buf
write: (bytes) ->
for byte in bytes
@writeByte byte
module.exports = Data

View File

@ -97,6 +97,10 @@ class PDFDocument
for key, val of @info when typeof val is 'string'
@info[key] = PDFObject.s val
# embed the subsetted fonts
for family, font of @_fontFamilies
font.embed()
# finalize each page
for page in @pages
page.finalize()

View File

@ -5,6 +5,7 @@ By Devon Govett
TTFFont = require './font/ttf'
AFMFont = require './font/afm'
Subset = require './font/subset'
zlib = require 'zlib'
class PDFFont
@ -14,20 +15,29 @@ class PDFFont
else if /\.(ttf|ttc)$/i.test @filename
@ttf = TTFFont.open @filename, @family
@embedTTF()
@subset = new Subset @ttf
@registerTTF()
else if /\.dfont$/i.test @filename
@ttf = TTFFont.fromDFont @filename, @family
@embedTTF()
@subset = new Subset @ttf
@registerTTF()
else
throw new Error 'Not a supported font format or standard PDF font.'
embedTTF: ->
use: (characters) ->
@subset?.use characters
embed: ->
@embedTTF() unless @isAFM
encode: (text) ->
@subset?.encodeText(text) or text
registerTTF: ->
@scaleFactor = 1000.0 / @ttf.head.unitsPerEm
@bbox = (Math.round e * @scaleFactor for e in @ttf.bbox)
@basename = @ttf.name.postscriptName
@stemV = 0 # not sure how to compute this for true-type fonts...
if @ttf.post.exists
@ -62,8 +72,14 @@ class PDFFont
@hmtx = @ttf.hmtx
@charWidths = (Math.round @hmtx.widths[gid] * @scaleFactor for i, gid of @cmap.codeMap when i >= 32)
data = @ttf.rawData
# Create a placeholder reference to be filled in embedTTF.
@ref = @document.ref
Type: 'Font'
Subtype: 'TrueType'
embedTTF: ->
data = @subset.encode()
compressedData = zlib.deflate(data)
@fontfile = @document.ref
@ -73,9 +89,13 @@ class PDFFont
@fontfile.add compressedData
cmap = @subset.cmap
widths = @subset.charWidths
charWidths = (Math.round widths[gid] * @scaleFactor for gid, i in cmap when i >= 32)
@descriptor = @document.ref
Type: 'FontDescriptor'
FontName: @basename
FontName: @subset.postscriptName
FontFile2: @fontfile
FontBBox: @bbox
Flags: @flags
@ -86,16 +106,19 @@ class PDFFont
CapHeight: @capHeight
XHeight: @xHeight
@ref = @document.ref
ref =
Type: 'Font'
BaseFont: @basename
BaseFont: @subset.postscriptName
Subtype: 'TrueType'
FontDescriptor: @descriptor
FirstChar: 32
LastChar: 255
Widths: @document.ref @charWidths
Widths: @document.ref charWidths
Encoding: 'MacRomanEncoding'
for key, val of ref
@ref.data[key] = val
embedStandard: ->
@isAFM = true
font = AFMFont.open __dirname + "/font/data/#{@filename}.afm"

View File

@ -1,3 +1,5 @@
Data = require '../data'
class Directory
constructor: (data) ->
@scalarType = data.readInt()
@ -15,5 +17,64 @@ class Directory
length: data.readInt()
@tables[entry.tag] = entry
encode: (tables) ->
tableCount = Object.keys(tables).length
log2 = Math.log(2)
searchRange = Math.floor(Math.log(tableCount) / log2) * 16
entrySelector = Math.floor searchRange / log2
rangeShift = tableCount * 16 - searchRange
directory = new Data
directory.writeInt @scalarType
directory.writeShort tableCount
directory.writeShort searchRange
directory.writeShort entrySelector
directory.writeShort rangeShift
directoryLength = tableCount * 16
offset = directory.pos + directoryLength
headOffset = null
tableData = []
# encode the font table directory
for tag, table of tables
directory.writeString tag
directory.writeInt checksum(table)
directory.writeInt offset
directory.writeInt table.length
tableData = tableData.concat(table)
headOffset = offset if tag is 'head'
offset += table.length
while offset % 4
tableData.push 0
offset++
# write the actual table data to the font
directory.write(tableData)
# calculate the font's checksum
sum = checksum(directory.data)
# set the checksum adjustment in the head table
adjustment = 0xB1B0AFBA - sum
directory.pos = headOffset + 8
directory.writeUInt32 adjustment
return new Buffer(directory.data)
checksum = ([data...]) ->
while data.length % 4
data.push 0
tmp = new Data(data)
sum = 0
for i in [0...data.length] by 4
sum += tmp.readUInt32()
return sum & 0xFFFFFFFF
module.exports = Directory

227
lib/font/macroman.coffee Normal file
View File

@ -0,0 +1,227 @@
exports.TO_UNICODE =
0x20: 0x0020 # SPACE
0x21: 0x0021 # EXCLAMATION MARK
0x22: 0x0022 # QUOTATION MARK
0x23: 0x0023 # NUMBER SIGN
0x24: 0x0024 # DOLLAR SIGN
0x25: 0x0025 # PERCENT SIGN
0x26: 0x0026 # AMPERSAND
0x27: 0x0027 # APOSTROPHE
0x28: 0x0028 # LEFT PARENTHESIS
0x29: 0x0029 # RIGHT PARENTHESIS
0x2A: 0x002A # ASTERISK
0x2B: 0x002B # PLUS SIGN
0x2C: 0x002C # COMMA
0x2D: 0x002D # HYPHEN-MINUS
0x2E: 0x002E # FULL STOP
0x2F: 0x002F # SOLIDUS
0x30: 0x0030 # DIGIT ZERO
0x31: 0x0031 # DIGIT ONE
0x32: 0x0032 # DIGIT TWO
0x33: 0x0033 # DIGIT THREE
0x34: 0x0034 # DIGIT FOUR
0x35: 0x0035 # DIGIT FIVE
0x36: 0x0036 # DIGIT SIX
0x37: 0x0037 # DIGIT SEVEN
0x38: 0x0038 # DIGIT EIGHT
0x39: 0x0039 # DIGIT NINE
0x3A: 0x003A # COLON
0x3B: 0x003B # SEMICOLON
0x3C: 0x003C # LESS-THAN SIGN
0x3D: 0x003D # EQUALS SIGN
0x3E: 0x003E # GREATER-THAN SIGN
0x3F: 0x003F # QUESTION MARK
0x40: 0x0040 # COMMERCIAL AT
0x41: 0x0041 # LATIN CAPITAL LETTER A
0x42: 0x0042 # LATIN CAPITAL LETTER B
0x43: 0x0043 # LATIN CAPITAL LETTER C
0x44: 0x0044 # LATIN CAPITAL LETTER D
0x45: 0x0045 # LATIN CAPITAL LETTER E
0x46: 0x0046 # LATIN CAPITAL LETTER F
0x47: 0x0047 # LATIN CAPITAL LETTER G
0x48: 0x0048 # LATIN CAPITAL LETTER H
0x49: 0x0049 # LATIN CAPITAL LETTER I
0x4A: 0x004A # LATIN CAPITAL LETTER J
0x4B: 0x004B # LATIN CAPITAL LETTER K
0x4C: 0x004C # LATIN CAPITAL LETTER L
0x4D: 0x004D # LATIN CAPITAL LETTER M
0x4E: 0x004E # LATIN CAPITAL LETTER N
0x4F: 0x004F # LATIN CAPITAL LETTER O
0x50: 0x0050 # LATIN CAPITAL LETTER P
0x51: 0x0051 # LATIN CAPITAL LETTER Q
0x52: 0x0052 # LATIN CAPITAL LETTER R
0x53: 0x0053 # LATIN CAPITAL LETTER S
0x54: 0x0054 # LATIN CAPITAL LETTER T
0x55: 0x0055 # LATIN CAPITAL LETTER U
0x56: 0x0056 # LATIN CAPITAL LETTER V
0x57: 0x0057 # LATIN CAPITAL LETTER W
0x58: 0x0058 # LATIN CAPITAL LETTER X
0x59: 0x0059 # LATIN CAPITAL LETTER Y
0x5A: 0x005A # LATIN CAPITAL LETTER Z
0x5B: 0x005B # LEFT SQUARE BRACKET
0x5C: 0x005C # REVERSE SOLIDUS
0x5D: 0x005D # RIGHT SQUARE BRACKET
0x5E: 0x005E # CIRCUMFLEX ACCENT
0x5F: 0x005F # LOW LINE
0x60: 0x0060 # GRAVE ACCENT
0x61: 0x0061 # LATIN SMALL LETTER A
0x62: 0x0062 # LATIN SMALL LETTER B
0x63: 0x0063 # LATIN SMALL LETTER C
0x64: 0x0064 # LATIN SMALL LETTER D
0x65: 0x0065 # LATIN SMALL LETTER E
0x66: 0x0066 # LATIN SMALL LETTER F
0x67: 0x0067 # LATIN SMALL LETTER G
0x68: 0x0068 # LATIN SMALL LETTER H
0x69: 0x0069 # LATIN SMALL LETTER I
0x6A: 0x006A # LATIN SMALL LETTER J
0x6B: 0x006B # LATIN SMALL LETTER K
0x6C: 0x006C # LATIN SMALL LETTER L
0x6D: 0x006D # LATIN SMALL LETTER M
0x6E: 0x006E # LATIN SMALL LETTER N
0x6F: 0x006F # LATIN SMALL LETTER O
0x70: 0x0070 # LATIN SMALL LETTER P
0x71: 0x0071 # LATIN SMALL LETTER Q
0x72: 0x0072 # LATIN SMALL LETTER R
0x73: 0x0073 # LATIN SMALL LETTER S
0x74: 0x0074 # LATIN SMALL LETTER T
0x75: 0x0075 # LATIN SMALL LETTER U
0x76: 0x0076 # LATIN SMALL LETTER V
0x77: 0x0077 # LATIN SMALL LETTER W
0x78: 0x0078 # LATIN SMALL LETTER X
0x79: 0x0079 # LATIN SMALL LETTER Y
0x7A: 0x007A # LATIN SMALL LETTER Z
0x7B: 0x007B # LEFT CURLY BRACKET
0x7C: 0x007C # VERTICAL LINE
0x7D: 0x007D # RIGHT CURLY BRACKET
0x7E: 0x007E # TILDE
#
0x80: 0x00C4 # LATIN CAPITAL LETTER A WITH DIAERESIS
0x81: 0x00C5 # LATIN CAPITAL LETTER A WITH RING ABOVE
0x82: 0x00C7 # LATIN CAPITAL LETTER C WITH CEDILLA
0x83: 0x00C9 # LATIN CAPITAL LETTER E WITH ACUTE
0x84: 0x00D1 # LATIN CAPITAL LETTER N WITH TILDE
0x85: 0x00D6 # LATIN CAPITAL LETTER O WITH DIAERESIS
0x86: 0x00DC # LATIN CAPITAL LETTER U WITH DIAERESIS
0x87: 0x00E1 # LATIN SMALL LETTER A WITH ACUTE
0x88: 0x00E0 # LATIN SMALL LETTER A WITH GRAVE
0x89: 0x00E2 # LATIN SMALL LETTER A WITH CIRCUMFLEX
0x8A: 0x00E4 # LATIN SMALL LETTER A WITH DIAERESIS
0x8B: 0x00E3 # LATIN SMALL LETTER A WITH TILDE
0x8C: 0x00E5 # LATIN SMALL LETTER A WITH RING ABOVE
0x8D: 0x00E7 # LATIN SMALL LETTER C WITH CEDILLA
0x8E: 0x00E9 # LATIN SMALL LETTER E WITH ACUTE
0x8F: 0x00E8 # LATIN SMALL LETTER E WITH GRAVE
0x90: 0x00EA # LATIN SMALL LETTER E WITH CIRCUMFLEX
0x91: 0x00EB # LATIN SMALL LETTER E WITH DIAERESIS
0x92: 0x00ED # LATIN SMALL LETTER I WITH ACUTE
0x93: 0x00EC # LATIN SMALL LETTER I WITH GRAVE
0x94: 0x00EE # LATIN SMALL LETTER I WITH CIRCUMFLEX
0x95: 0x00EF # LATIN SMALL LETTER I WITH DIAERESIS
0x96: 0x00F1 # LATIN SMALL LETTER N WITH TILDE
0x97: 0x00F3 # LATIN SMALL LETTER O WITH ACUTE
0x98: 0x00F2 # LATIN SMALL LETTER O WITH GRAVE
0x99: 0x00F4 # LATIN SMALL LETTER O WITH CIRCUMFLEX
0x9A: 0x00F6 # LATIN SMALL LETTER O WITH DIAERESIS
0x9B: 0x00F5 # LATIN SMALL LETTER O WITH TILDE
0x9C: 0x00FA # LATIN SMALL LETTER U WITH ACUTE
0x9D: 0x00F9 # LATIN SMALL LETTER U WITH GRAVE
0x9E: 0x00FB # LATIN SMALL LETTER U WITH CIRCUMFLEX
0x9F: 0x00FC # LATIN SMALL LETTER U WITH DIAERESIS
0xA0: 0x2020 # DAGGER
0xA1: 0x00B0 # DEGREE SIGN
0xA2: 0x00A2 # CENT SIGN
0xA3: 0x00A3 # POUND SIGN
0xA4: 0x00A7 # SECTION SIGN
0xA5: 0x2022 # BULLET
0xA6: 0x00B6 # PILCROW SIGN
0xA7: 0x00DF # LATIN SMALL LETTER SHARP S
0xA8: 0x00AE # REGISTERED SIGN
0xA9: 0x00A9 # COPYRIGHT SIGN
0xAA: 0x2122 # TRADE MARK SIGN
0xAB: 0x00B4 # ACUTE ACCENT
0xAC: 0x00A8 # DIAERESIS
0xAD: 0x2260 # NOT EQUAL TO
0xAE: 0x00C6 # LATIN CAPITAL LETTER AE
0xAF: 0x00D8 # LATIN CAPITAL LETTER O WITH STROKE
0xB0: 0x221E # INFINITY
0xB1: 0x00B1 # PLUS-MINUS SIGN
0xB2: 0x2264 # LESS-THAN OR EQUAL TO
0xB3: 0x2265 # GREATER-THAN OR EQUAL TO
0xB4: 0x00A5 # YEN SIGN
0xB5: 0x00B5 # MICRO SIGN
0xB6: 0x2202 # PARTIAL DIFFERENTIAL
0xB7: 0x2211 # N-ARY SUMMATION
0xB8: 0x220F # N-ARY PRODUCT
0xB9: 0x03C0 # GREEK SMALL LETTER PI
0xBA: 0x222B # INTEGRAL
0xBB: 0x00AA # FEMININE ORDINAL INDICATOR
0xBC: 0x00BA # MASCULINE ORDINAL INDICATOR
0xBD: 0x03A9 # GREEK CAPITAL LETTER OMEGA
0xBE: 0x00E6 # LATIN SMALL LETTER AE
0xBF: 0x00F8 # LATIN SMALL LETTER O WITH STROKE
0xC0: 0x00BF # INVERTED QUESTION MARK
0xC1: 0x00A1 # INVERTED EXCLAMATION MARK
0xC2: 0x00AC # NOT SIGN
0xC3: 0x221A # SQUARE ROOT
0xC4: 0x0192 # LATIN SMALL LETTER F WITH HOOK
0xC5: 0x2248 # ALMOST EQUAL TO
0xC6: 0x2206 # INCREMENT
0xC7: 0x00AB # LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
0xC8: 0x00BB # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
0xC9: 0x2026 # HORIZONTAL ELLIPSIS
0xCA: 0x00A0 # NO-BREAK SPACE
0xCB: 0x00C0 # LATIN CAPITAL LETTER A WITH GRAVE
0xCC: 0x00C3 # LATIN CAPITAL LETTER A WITH TILDE
0xCD: 0x00D5 # LATIN CAPITAL LETTER O WITH TILDE
0xCE: 0x0152 # LATIN CAPITAL LIGATURE OE
0xCF: 0x0153 # LATIN SMALL LIGATURE OE
0xD0: 0x2013 # EN DASH
0xD1: 0x2014 # EM DASH
0xD2: 0x201C # LEFT DOUBLE QUOTATION MARK
0xD3: 0x201D # RIGHT DOUBLE QUOTATION MARK
0xD4: 0x2018 # LEFT SINGLE QUOTATION MARK
0xD5: 0x2019 # RIGHT SINGLE QUOTATION MARK
0xD6: 0x00F7 # DIVISION SIGN
0xD7: 0x25CA # LOZENGE
0xD8: 0x00FF # LATIN SMALL LETTER Y WITH DIAERESIS
0xD9: 0x0178 # LATIN CAPITAL LETTER Y WITH DIAERESIS
0xDA: 0x2044 # FRACTION SLASH
0xDB: 0x20AC # EURO SIGN
0xDC: 0x2039 # SINGLE LEFT-POINTING ANGLE QUOTATION MARK
0xDD: 0x203A # SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
0xDE: 0xFB01 # LATIN SMALL LIGATURE FI
0xDF: 0xFB02 # LATIN SMALL LIGATURE FL
0xE0: 0x2021 # DOUBLE DAGGER
0xE1: 0x00B7 # MIDDLE DOT
0xE2: 0x201A # SINGLE LOW-9 QUOTATION MARK
0xE3: 0x201E # DOUBLE LOW-9 QUOTATION MARK
0xE4: 0x2030 # PER MILLE SIGN
0xE5: 0x00C2 # LATIN CAPITAL LETTER A WITH CIRCUMFLEX
0xE6: 0x00CA # LATIN CAPITAL LETTER E WITH CIRCUMFLEX
0xE7: 0x00C1 # LATIN CAPITAL LETTER A WITH ACUTE
0xE8: 0x00CB # LATIN CAPITAL LETTER E WITH DIAERESIS
0xE9: 0x00C8 # LATIN CAPITAL LETTER E WITH GRAVE
0xEA: 0x00CD # LATIN CAPITAL LETTER I WITH ACUTE
0xEB: 0x00CE # LATIN CAPITAL LETTER I WITH CIRCUMFLEX
0xEC: 0x00CF # LATIN CAPITAL LETTER I WITH DIAERESIS
0xED: 0x00CC # LATIN CAPITAL LETTER I WITH GRAVE
0xEE: 0x00D3 # LATIN CAPITAL LETTER O WITH ACUTE
0xEF: 0x00D4 # LATIN CAPITAL LETTER O WITH CIRCUMFLEX
0xF0: 0xF8FF # Apple logo
0xF1: 0x00D2 # LATIN CAPITAL LETTER O WITH GRAVE
0xF2: 0x00DA # LATIN CAPITAL LETTER U WITH ACUTE
0xF3: 0x00DB # LATIN CAPITAL LETTER U WITH CIRCUMFLEX
0xF4: 0x00D9 # LATIN CAPITAL LETTER U WITH GRAVE
0xF5: 0x0131 # LATIN SMALL LETTER DOTLESS I
0xF6: 0x02C6 # MODIFIER LETTER CIRCUMFLEX ACCENT
0xF7: 0x02DC # SMALL TILDE
0xF8: 0x00AF # MACRON
0xF9: 0x02D8 # BREVE
0xFA: 0x02D9 # DOT ABOVE
0xFB: 0x02DA # RING ABOVE
0xFC: 0x00B8 # CEDILLA
0xFD: 0x02DD # DOUBLE ACUTE ACCENT
0xFE: 0x02DB # OGONEK
0xFF: 0x02C7 # CARON
exports.FROM_UNICODE = require('./utils').invert(exports.TO_UNICODE)

106
lib/font/subset.coffee Normal file
View File

@ -0,0 +1,106 @@
CmapTable = require './tables/cmap'
MacRoman = require './macroman'
utils = require './utils'
class Subset
constructor: (@font) ->
@subset = {}
use: (character) ->
# if given a string, add each character
if typeof character is 'string'
for i in [0...character.length]
@use character.charCodeAt(i)
else
@subset[MacRoman.FROM_UNICODE[character]] = character
encodeText: (text) ->
string = ''
for i in [0...text.length]
char = MacRoman.FROM_UNICODE[text.charCodeAt(i)]
string += String.fromCharCode(char)
return string
cmap: ->
# generate the cmap table for this subset
unicodeCmap = @font.cmap.unicode.codeMap
mapping = {}
for roman, unicode of @subset
mapping[roman] = unicodeCmap[unicode]
return mapping
glyphIDs: ->
# collect glyph ids for this subset
unicodeCmap = @font.cmap.unicode.codeMap
ret = [0]
for roman, unicode of @subset
val = unicodeCmap[unicode]
ret.push val if val? and val not in ret
return ret.sort()
glyphsFor: (glyphIDs) ->
# collect the actual glyph data for this subset
glyphs = {}
for id in glyphIDs
glyphs[id] = @font.glyf.glyphFor(id)
# collect additional glyphs referenced from compound glyphs
additionalIDs = []
for id, glyph of glyphs when glyph?.compound
additionalIDs.push glyph.glyphIDs...
if additionalIDs.length > 0
for id, glyph of @glyphsFor(additionalIDs)
glyphs[id] = glyph
return glyphs
encode: ->
# generate the Cmap for this subset
cmap = CmapTable.encode @cmap()
glyphs = @glyphsFor @glyphIDs()
# compute old2new and new2old mapping tables
old2new = { 0: 0 }
for code, ids of cmap.charMap
old2new[ids.old] = ids.new
nextGlyphID = cmap.maxGlyphID
for oldID of glyphs when oldID not of old2new
old2new[oldID] = nextGlyphID++
new2old = utils.invert(old2new)
newIDs = Object.keys(new2old).sort (a, b) -> a - b
oldIDs = (new2old[id] for id in newIDs)
# encode the font tables
glyf = @font.glyf.encode(glyphs, oldIDs, old2new)
loca = @font.loca.encode(glyf.offsets)
name = @font.name.encode()
# store for use later
@cmap = cmap.indexes
@postscriptName = name.postscriptName
@charWidths = (@font.hmtx.forGlyph(id).advance for id in oldIDs)
tables =
cmap: cmap.table
glyf: glyf.table
loca: loca.table
hmtx: @font.hmtx.encode(oldIDs)
hhea: @font.hhea.encode(oldIDs)
maxp: @font.maxp.encode(oldIDs)
post: @font.post.encode(oldIDs)
name: name.table
head: @font.head.encode(loca)
# just copy over the OS/2 table if it exists
tables['OS/2'] = @font.os2.raw() if @font.os2.exists
# encode the font directory
@font.directory.encode(tables)
module.exports = Subset

View File

@ -1,5 +1,5 @@
class Table
constructor: (@file) ->
constructor: (@file, @tag) ->
@tag ?= @constructor.name.replace('Table', '').toLowerCase()
info = @file.directory.tables[@tag]
@exists = !!info
@ -11,4 +11,13 @@ class Table
parse: ->
# implemented by subclasses
encode: ->
# implemented by subclasses
raw: ->
return null unless @exists
@file.contents.pos = @offset
@file.contents.read @length
module.exports = Table

View File

@ -1,4 +1,5 @@
Table = require '../table'
Data = require '../../data'
class CmapTable extends Table
parse: (data) ->
@ -15,6 +16,16 @@ class CmapTable extends Table
@unicode ?= entry if entry.isUnicode
return true
@encode: (charmap, encoding = 0) ->
result = CmapEntry.encode(charmap, encoding)
table = new Data
table.writeUInt16 0 # version
table.writeUInt16 1 # tableCount
result.table = table.data.concat(result.subtable)
return result
class CmapEntry
constructor: (data, offset) ->
@ -32,7 +43,8 @@ class CmapEntry
@codeMap = {}
switch @format
when 0
@codeMap[i] = data.readByte() for i in [0...256]
for i in [0...256]
@codeMap[i] = data.readByte()
when 4
segCountX2 = data.readUInt16()
@ -60,5 +72,39 @@ class CmapEntry
glyphId += idDelta[i] if glyphId isnt 0
@codeMap[code] = glyphId & 0xFFFF
@encode: (charmap, format) ->
subtable = new Data
switch format
when 0 # Mac Roman
id = 0
indexes = (0 for i in [0...256])
map = { 0: 0 }
codeMap = {}
for code in Object.keys(charmap).sort()
map[charmap[code]] ?= ++id
codeMap[code] =
old: charmap[code]
new: map[charmap[code]]
indexes[code] = map[charmap[code]]
subtable.writeUInt16 1 # platformID
subtable.writeUInt16 0 # encodingID
subtable.writeUInt32 12 # offset
subtable.writeUInt16 0 # format
subtable.writeUInt16 262 # length
subtable.writeUInt16 0 # language
subtable.write indexes # glyph indexes
result =
charMap: codeMap
indexes: indexes
subtable: subtable.data
maxGlyphID: id + 1
when 4 # Unicode - TODO: implement
return
module.exports = CmapTable

104
lib/font/tables/glyf.coffee Normal file
View File

@ -0,0 +1,104 @@
Table = require '../table'
Data = require '../../data'
class GlyfTable extends Table
parse: (data) ->
# We're not going to parse the whole glyf table, just the glyfs we need. See below.
@cache = {}
glyphFor: (id) ->
return @cache[id] if id of @cache
loca = @file.loca
data = @file.contents
index = loca.indexOf(id)
length = loca.lengthOf(id)
if length is 0
return @cache[id] = null
data.pos = @offset + index
raw = new Data data.read(length)
numberOfContours = raw.readShort()
xMin = raw.readShort()
yMin = raw.readShort()
xMax = raw.readShort()
yMax = raw.readShort()
if numberOfContours is -1
@cache[id] = new CompoundGlyph(raw, xMin, yMin, xMax, yMax)
else
@cache[id] = new SimpleGlyph(raw, numberOfContours, xMin, yMin, xMax, yMax)
return @cache[id]
encode: (glyphs, mapping, old2new) ->
table = []
offsets = []
for id in mapping
glyph = glyphs[id]
offsets.push table.length
table = table.concat glyph.encode(old2new) if glyph
# include an offset at the end of the table, for use in computing the
# size of the last glyph
offsets.push table.length
return { table, offsets }
class SimpleGlyph
constructor: (@raw, @numberOfContours, @xMin, @yMin, @xMax, @yMax) ->
@compound = false
encode: ->
return @raw.data
# a compound glyph is one that is comprised of 2 or more simple glyphs,
# for example a letter with an accent
class CompoundGlyph
ARG_1_AND_2_ARE_WORDS = 0x0001
WE_HAVE_A_SCALE = 0x0008
MORE_COMPONENTS = 0x0020
WE_HAVE_AN_X_AND_Y_SCALE = 0x0040
WE_HAVE_A_TWO_BY_TWO = 0x0080
WE_HAVE_INSTRUCTIONS = 0x0100
constructor: (@raw, @xMin, @yMin, @xMax, @yMax) ->
@compound = true
@glyphIDs = []
@glyphOffsets = []
data = @raw
loop
flags = data.readShort()
@glyphOffsets.push data.pos
@glyphIDs.push data.readShort()
break unless flags & MORE_COMPONENTS
if flags & ARG_1_AND_2_ARE_WORDS
data.pos += 4
else
data.pos += 2
if flags & WE_HAVE_A_TWO_BY_TWO
data.pos += 8
else if flags & WE_HAVE_AN_X_AND_Y_SCALE
data.pos += 4
else if flags & WE_HAVE_A_SCALE
data.pos += 2
encode: (mapping) ->
result = new Data [@raw.data...]
# update glyph offsets
for id, i in @glyphIDs
result.pos = @glyphOffsets[i]
result.writeShort mapping[id]
return result.data
module.exports = GlyfTable

View File

@ -1,4 +1,5 @@
Table = require '../table'
Data = require '../../data'
class HeadTable extends Table
parse: (data) ->
@ -22,5 +23,29 @@ class HeadTable extends Table
@fontDirectionHint = data.readShort()
@indexToLocFormat = data.readShort()
@glyphDataFormat = data.readShort()
encode: (loca) ->
table = new Data
table.writeInt @version
table.writeInt @revision
table.writeInt @checkSumAdjustment
table.writeInt @magicNumber
table.writeShort @flags
table.writeShort @unitsPerEm
table.writeLongLong @created
table.writeLongLong @modified
table.writeShort @xMin
table.writeShort @yMin
table.writeShort @xMax
table.writeShort @yMax
table.writeShort @macStyle
table.writeShort @lowestRecPPEM
table.writeShort @fontDirectionHint
table.writeShort loca.type
table.writeShort @glyphDataFormat
return table.data
module.exports = HeadTable

View File

@ -1,4 +1,5 @@
Table = require '../table'
Data = require '../../data'
class HheaTable extends Table
parse: (data) ->
@ -21,4 +22,26 @@ class HheaTable extends Table
@metricDataFormat = data.readShort()
@numberOfMetrics = data.readUInt16()
encode: (ids) ->
table = new Data
table.writeInt @version
table.writeShort @ascender
table.writeShort @decender
table.writeShort @lineGap
table.writeShort @advanceWidthMax
table.writeShort @minLeftSideBearing
table.writeShort @minRightSideBearing
table.writeShort @xMaxExtent
table.writeShort @caretSlopeRise
table.writeShort @caretSlopeRun
table.writeShort @caretOffset
table.writeByte(0) for i in [0...4 * 2] # skip 4 reserved int16 slots
table.writeShort @metricDataFormat
table.writeUInt16 ids.length # numberOfMetrics
return table.data
module.exports = HheaTable

View File

@ -1,4 +1,5 @@
Table = require '../table'
Data = require '../../data'
class HmtxTable extends Table
parse: (data) ->
@ -17,4 +18,20 @@ class HmtxTable extends Table
last = @widths[@widths.length - 1]
@widths.push(last) for i in [0...lsbCount]
forGlyph: (id) ->
return @metrics[id] if id of @metrics
metrics =
advance: @metrics[@metrics.length - 1].advance
lsb: @leftSideBearings[id - @metrics.length]
encode: (mapping) ->
table = new Data
for id in mapping
metric = @forGlyph id
table.writeUInt16 metric.advance
table.writeUInt16 metric.lsb
return table.data
module.exports = HmtxTable

View File

@ -0,0 +1,44 @@
Table = require '../table'
Data = require '../../Data'
class LocaTable extends Table
parse: (data) ->
data.pos = @offset
format = @file.head.indexToLocFormat
# short format
if format is 0
@offsets = (data.readUInt16() * 2 for i in [0...@length] by 2)
# long format
else
@offsets = (data.readUInt32() for i in [0...@length] by 4)
indexOf: (id) ->
@offsets[id]
lengthOf: (id) ->
@offsets[id + 1] - @offsets[id]
encode: (offsets) ->
table = new Data
# long format
for offset in offsets when offset > 0xFFFF
for o in @offsets
table.writeUInt32 o
return ret =
format: 1
table: table.data
# short format
for o in offsets
table.writeUInt16 o / 2
ret =
format: 0
table: table.data
module.exports = LocaTable

View File

@ -1,4 +1,5 @@
Table = require '../table'
Data = require '../../data'
class MaxpTable extends Table
parse: (data) ->
@ -20,4 +21,25 @@ class MaxpTable extends Table
@maxComponentElements = data.readUInt16()
@maxComponentDepth = data.readUInt16()
encode: (ids) ->
table = new Data
table.writeInt @version
table.writeUInt16 ids.length # numGlyphs
table.writeUInt16 @maxPoints
table.writeUInt16 @maxContours
table.writeUInt16 @maxCompositePoints
table.writeUInt16 @maxComponentContours
table.writeUInt16 @maxZones
table.writeUInt16 @maxTwilightPoints
table.writeUInt16 @maxStorage
table.writeUInt16 @maxFunctionDefs
table.writeUInt16 @maxInstructionDefs
table.writeUInt16 @maxStackElements
table.writeUInt16 @maxSizeOfInstructions
table.writeUInt16 @maxComponentElements
table.writeUInt16 @maxComponentDepth
return table.data
module.exports = MaxpTable

View File

@ -1,4 +1,6 @@
Table = require '../table'
Data = require '../../data'
utils = require '../utils'
class NameTable extends Table
parse: (data) ->
@ -18,20 +20,23 @@ class NameTable extends Table
length: data.readShort()
offset: @offset + stringOffset + data.readShort()
strings = []
strings = {}
for entry, i in entries
data.pos = entry.offset
text = data.readString(entry.length)
name = new NameEntry text, entry
strings[entry.nameID] ?= []
strings[entry.nameID].push strip(text)
strings[entry.nameID].push name
@strings = strings
@copyright = strings[0]
@fontFamily = strings[1]
@fontSubfamily = strings[2]
@uniqueSubfamily = strings[3]
@fontName = strings[4]
@version = strings[5]
@postscriptName = strings[6][0] # should only be ONE postscript name
@postscriptName = strings[6][0].raw.replace(/[\x00-\x19\x80-\xff]/g, "") # should only be ONE postscript name
@trademark = strings[7]
@manufacturer = strings[8]
@designer = strings[9]
@ -45,9 +50,53 @@ class NameTable extends Table
@compatibleFull = strings[18]
@sampleText = strings[19]
strip = (string) ->
stripped = string.replace(/[\x00-\x19\x80-\xff]/g, "")
stripped = "[not-postscript]" if stripped.length is 0
return stripped
subsetTag = "AAAAAA"
encode: ->
strings = {}
strings[id] = val for id, val of @strings
# generate a new postscript name for this subset
postscriptName = new NameEntry "#{subsetTag}+#{@postscriptName}",
platformID: 1
encodingID: 0
languageID: 0
module.exports = NameTable
strings[6] = [postscriptName]
subsetTag = utils.successorOf(subsetTag)
# count the number of strings in the table
strCount = 0
strCount += list.length for id, list of strings when list?
table = new Data
strTable = new Data
table.writeShort 0 # format
table.writeShort strCount # count
table.writeShort 6 + 12 * strCount # stringOffset
# write the strings
for nameID, list of strings when list?
for string in list
table.writeShort string.platformID
table.writeShort string.encodingID
table.writeShort string.languageID
table.writeShort nameID
table.writeShort string.length
table.writeShort strTable.pos
# write the actual string
strTable.writeString string.raw
nameTable =
postscriptName: postscriptName.raw
table: table.data.concat(strTable.data)
module.exports = NameTable
class NameEntry
constructor: (@raw, entry) ->
@length = raw.length
@platformID = entry.platformID
@encodingID = entry.encodingID
@languageID = entry.languageID

View File

@ -6,46 +6,49 @@ class OS2Table extends Table
super
parse: (data) ->
data.pos = @offset
data.pos = @offset
@version = data.readUInt16()
@averageCharWidth = data.readShort()
@weightClass = data.readUInt16()
@widthClass = data.readUInt16()
@type = data.readShort()
@ySubscriptXSize = data.readShort()
@ySubscriptYSize = data.readShort()
@ySubscriptXOffset = data.readShort()
@ySubscriptYOffset = data.readShort()
@ySuperscriptXSize = data.readShort()
@ySuperscriptYSize = data.readShort()
@ySuperscriptXOffset = data.readShort()
@ySuperscriptYOffset = data.readShort()
@yStrikeoutSize = data.readShort()
@yStrikeoutPosition = data.readShort()
@familyClass = data.readShort()
@panose = (data.readByte() for i in [0...10])
@charRange = (data.readInt() for i in [0...4])
@vendorID = data.readString(4)
@selection = data.readShort()
@firstCharIndex = data.readShort()
@lastCharIndex = data.readShort()
if @version > 0
@ascent = data.readShort()
@descent = data.readShort()
@lineGap = data.readShort()
@winAscent = data.readShort()
@winDescent = data.readShort()
@codePageRange = (data.readInt() for i in [0...2])
@version = data.readUInt16()
@averageCharWidth = data.readShort()
@weightClass = data.readUInt16()
@widthClass = data.readUInt16()
@type = data.readShort()
@ySubscriptXSize = data.readShort()
@ySubscriptYSize = data.readShort()
@ySubscriptXOffset = data.readShort()
@ySubscriptYOffset = data.readShort()
@ySuperscriptXSize = data.readShort()
@ySuperscriptYSize = data.readShort()
@ySuperscriptXOffset = data.readShort()
@ySuperscriptYOffset = data.readShort()
@yStrikeoutSize = data.readShort()
@yStrikeoutPosition = data.readShort()
@familyClass = data.readShort()
@panose = (data.readByte() for i in [0...10])
@charRange = (data.readInt() for i in [0...4])
@vendorID = data.readString(4)
@selection = data.readShort()
@firstCharIndex = data.readShort()
@lastCharIndex = data.readShort()
if @version > 0
@ascent = data.readShort()
@descent = data.readShort()
@lineGap = data.readShort()
@winAscent = data.readShort()
@winDescent = data.readShort()
@codePageRange = (data.readInt() for i in [0...2])
if @version > 1
@xHeight = data.readShort()
@capHeight = data.readShort()
@defaultChar = data.readShort()
@breakChar = data.readShort()
@maxContext = data.readShort()
if @version > 1
@xHeight = data.readShort()
@capHeight = data.readShort()
@defaultChar = data.readShort()
@breakChar = data.readShort()
@maxContext = data.readShort()
encode: ->
return @raw()
module.exports = OS2Table

View File

@ -1,4 +1,5 @@
Table = require '../table'
Data = require '../../data'
class PostTable extends Table
parse: (data) ->
@ -8,19 +9,113 @@ class PostTable extends Table
@italicAngle = data.readInt()
@underlinePosition = data.readShort()
@underlineThickness = data.readShort()
@isFixedPitch = data.readBool()
@isFixedPitch = data.readInt()
@minMemType42 = data.readInt()
@maxMemType42 = data.readInt()
@minMemType1 = data.readInt()
@maxMemType1 = data.readInt()
###
switch @format
when 0x00010000 then
when 0x00020000 then
when 0x00025000 then
when 0x00030000 then
when 0x00040000 then
###
when 0x00010000 then break
when 0x00020000
numberOfGlyphs = data.readUInt16()
@glyphNameIndex = []
for i in [0...numberOfGlyphs]
@glyphNameIndex.push data.readUInt16()
@names = []
while data.pos < @offset + @length
length = data.readByte()
@names.push data.readString(length)
when 0x00025000
numberOfGlyphs = data.readUInt16()
@offsets = data.read(numberOfGlyphs)
when 0x00030000 then break
when 0x00040000
@map = (data.readUInt32() for i in [0...@file.maxp.numGlyphs])
glyphFor: (code) ->
switch @format
when 0x00010000
POSTSCRIPT_GLYPHS[code] or '.notdef'
when 0x00020000
index = @glyphNameIndex[code]
if index <= 257
POSTSCRIPT_GLYPHS[index]
else
@names[index - 258] or '.notdef'
when 0x00025000
POSTSCRIPT_GLYPHS[code + @offsets[code]] or '.notdef'
when 0x00030000
'.notdef'
when 0x00040000
@map[code] or 0xFFFF
encode: (mapping) ->
return null unless @exists
raw = @raw()
return raw if @format is 0x00030000
table = new Data raw[0...32]
table.writeUInt32 0x00020000 # set format
table.pos = 32
indexes = []
strings = []
for id in mapping
post = @glyphFor id
position = POSTSCRIPT_GLYPHS.indexOf post
if position isnt -1
indexes.push position
else
indexes.push 257 + strings.length
strings.push post
table.writeUInt16 Object.keys(mapping).length
for index in indexes
table.writeUInt16 index
for string in strings
table.writeByte string.length
table.writeString string
return table.data
POSTSCRIPT_GLYPHS = '''
.notdef .null nonmarkingreturn space exclam quotedbl numbersign dollar percent
ampersand quotesingle parenleft parenright asterisk plus comma hyphen period slash
zero one two three four five six seven eight nine colon semicolon less equal greater
question at A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
bracketleft backslash bracketright asciicircum underscore grave
a b c d e f g h i j k l m n o p q r s t u v w x y z
braceleft bar braceright asciitilde Adieresis Aring Ccedilla Eacute Ntilde Odieresis
Udieresis aacute agrave acircumflex adieresis atilde aring ccedilla eacute egrave
ecircumflex edieresis iacute igrave icircumflex idieresis ntilde oacute ograve
ocircumflex odieresis otilde uacute ugrave ucircumflex udieresis dagger degree cent
sterling section bullet paragraph germandbls registered copyright trademark acute
dieresis notequal AE Oslash infinity plusminus lessequal greaterequal yen mu
partialdiff summation product pi integral ordfeminine ordmasculine Omega ae oslash
questiondown exclamdown logicalnot radical florin approxequal Delta guillemotleft
guillemotright ellipsis nonbreakingspace Agrave Atilde Otilde OE oe endash emdash
quotedblleft quotedblright quoteleft quoteright divide lozenge ydieresis Ydieresis
fraction currency guilsinglleft guilsinglright fi fl daggerdbl periodcentered
quotesinglbase quotedblbase perthousand Acircumflex Ecircumflex Aacute Edieresis
Egrave Iacute Icircumflex Idieresis Igrave Oacute Ocircumflex apple Ograve Uacute
Ucircumflex Ugrave dotlessi circumflex tilde macron breve dotaccent ring cedilla
hungarumlaut ogonek caron Lslash lslash Scaron scaron Zcaron zcaron brokenbar Eth
eth Yacute yacute Thorn thorn minus multiply onesuperior twosuperior threesuperior
onehalf onequarter threequarters franc Gbreve gbreve Idotaccent Scedilla scedilla
Cacute cacute Ccaron ccaron dcroat
'''.split(/\s+/g)
module.exports = PostTable

View File

@ -11,6 +11,8 @@ HheaTable = require './tables/hhea'
MaxpTable = require './tables/maxp'
PostTable = require './tables/post'
OS2Table = require './tables/os2'
LocaTable = require './tables/loca'
GlyfTable = require './tables/glyf'
class TTFFont
@open: (filename, name) ->
@ -56,7 +58,8 @@ class TTFFont
@hmtx = new HmtxTable(this)
@post = new PostTable(this)
@os2 = new OS2Table(this)
#kern, loca, glyf, etc.
@loca = new LocaTable(this)
@glyf = new GlyfTable(this)
@ascender = (@os2.exists and @os2.ascender) or @hhea.ascender
@decender = (@os2.exists and @os2.decender) or @hhea.decender

67
lib/font/utils.coffee Normal file
View File

@ -0,0 +1,67 @@
###
# An implementation of Ruby's string.succ method.
# By Devon Govett
#
# Returns the successor to str. The successor is calculated by incrementing characters starting
# from the rightmost alphanumeric (or the rightmost character if there are no alphanumerics) in the
# string. Incrementing a digit always results in another digit, and incrementing a letter results in
# another letter of the same case.
#
# If the increment generates a carry, the character to the left of it is incremented. This
# process repeats until there is no carry, adding an additional character if necessary.
#
# succ("abcd") == "abce"
# succ("THX1138") == "THX1139"
# succ("<<koala>>") == "<<koalb>>"
# succ("1999zzz") == "2000aaa"
# succ("ZZZ9999") == "AAAA0000"
###
exports.successorOf = (input) ->
alphabet = 'abcdefghijklmnopqrstuvwxyz'
length = alphabet.length
result = input
i = input.length
while i >= 0
last = input.charAt(--i)
if isNaN(last)
index = alphabet.indexOf(last.toLowerCase())
if index is -1
next = last
carry = true
else
next = alphabet.charAt((index + 1) % length)
isUpperCase = last is last.toUpperCase()
if isUpperCase
next = next.toUpperCase()
carry = index + 1 >= length
if carry and i is 0
added = if isUpperCase then 'A' else 'a'
result = added + next + result.slice(1)
break
else
next = +last + 1
carry = next > 9
next = 0 if carry
if carry and i is 0
result = '1' + next + result.slice(1)
break
result = result.slice(0, i) + next + result.slice(i + 1)
break unless carry
return result
# Swaps the properties and values of an object and returns the result
exports.invert = (object) ->
ret = {}
for key, val of object
ret[val] = key
return ret

View File

@ -34,6 +34,9 @@ module.exports =
# add current font to page if necessary
@page.fonts[@_font.id] ?= @_font.ref
# tell the font subset to use the characters
@_font.use text
# if the wordSpacing option is specified, remove multiple consecutive spaces
if options.wordSpacing
text = text.replace(/\s+/g, ' ')
@ -130,7 +133,8 @@ module.exports =
# flip coordinate system
y = @page.height - y - (@_font.ascender / 1000 * @_fontSize)
# escape the text for inclusion in PDF
# encode and escape the text for inclusion in PDF
text = @_font.encode text
text = @_escape text
# begin the text object

View File

@ -33,7 +33,10 @@ class PDFReference
if @stream
data = @stream.join '\n'
if compress
compressedData = zlib.deflate new Buffer(data)
# create a byte array instead of passing a string to the Buffer
# fixes a weird unicode bug.
data = new Buffer(data.charCodeAt(i) for i in [0...data.length])
compressedData = zlib.deflate(data)
@finalizedStream = compressedData.toString 'binary'
else
@finalizedStream = data