diff --git a/demo/out.pdf b/demo/out.pdf index 86b68ac..40c9a91 100644 Binary files a/demo/out.pdf and b/demo/out.pdf differ diff --git a/lib/data.coffee b/lib/data.coffee index 9416a87..0fdfe14 100644 --- a/lib/data.coffee +++ b/lib/data.coffee @@ -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 \ No newline at end of file diff --git a/lib/document.coffee b/lib/document.coffee index 53bb38e..250aa03 100644 --- a/lib/document.coffee +++ b/lib/document.coffee @@ -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() diff --git a/lib/font.coffee b/lib/font.coffee index 4b9eacc..bfebc02 100644 --- a/lib/font.coffee +++ b/lib/font.coffee @@ -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" diff --git a/lib/font/directory.coffee b/lib/font/directory.coffee index 0e7a709..8e87be3 100644 --- a/lib/font/directory.coffee +++ b/lib/font/directory.coffee @@ -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 \ No newline at end of file diff --git a/lib/font/macroman.coffee b/lib/font/macroman.coffee new file mode 100644 index 0000000..e78ed65 --- /dev/null +++ b/lib/font/macroman.coffee @@ -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) \ No newline at end of file diff --git a/lib/font/subset.coffee b/lib/font/subset.coffee new file mode 100644 index 0000000..cec4478 --- /dev/null +++ b/lib/font/subset.coffee @@ -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 \ No newline at end of file diff --git a/lib/font/table.coffee b/lib/font/table.coffee index 762787e..4871944 100644 --- a/lib/font/table.coffee +++ b/lib/font/table.coffee @@ -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 \ No newline at end of file diff --git a/lib/font/tables/cmap.coffee b/lib/font/tables/cmap.coffee index b791b78..06e401c 100644 --- a/lib/font/tables/cmap.coffee +++ b/lib/font/tables/cmap.coffee @@ -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 \ No newline at end of file diff --git a/lib/font/tables/glyf.coffee b/lib/font/tables/glyf.coffee new file mode 100644 index 0000000..526954f --- /dev/null +++ b/lib/font/tables/glyf.coffee @@ -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 \ No newline at end of file diff --git a/lib/font/tables/head.coffee b/lib/font/tables/head.coffee index ddf1726..35b16cf 100644 --- a/lib/font/tables/head.coffee +++ b/lib/font/tables/head.coffee @@ -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 \ No newline at end of file diff --git a/lib/font/tables/hhea.coffee b/lib/font/tables/hhea.coffee index 82d7f83..065671f 100644 --- a/lib/font/tables/hhea.coffee +++ b/lib/font/tables/hhea.coffee @@ -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 \ No newline at end of file diff --git a/lib/font/tables/hmtx.coffee b/lib/font/tables/hmtx.coffee index 5fc912a..f3c7f7d 100644 --- a/lib/font/tables/hmtx.coffee +++ b/lib/font/tables/hmtx.coffee @@ -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 \ No newline at end of file diff --git a/lib/font/tables/loca.coffee b/lib/font/tables/loca.coffee new file mode 100644 index 0000000..f41edda --- /dev/null +++ b/lib/font/tables/loca.coffee @@ -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 \ No newline at end of file diff --git a/lib/font/tables/maxp.coffee b/lib/font/tables/maxp.coffee index b095c67..32f6e18 100644 --- a/lib/font/tables/maxp.coffee +++ b/lib/font/tables/maxp.coffee @@ -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 \ No newline at end of file diff --git a/lib/font/tables/name.coffee b/lib/font/tables/name.coffee index 8dfd56d..b685eb8 100644 --- a/lib/font/tables/name.coffee +++ b/lib/font/tables/name.coffee @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/lib/font/tables/os2.coffee b/lib/font/tables/os2.coffee index d585407..844a0cc 100644 --- a/lib/font/tables/os2.coffee +++ b/lib/font/tables/os2.coffee @@ -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 \ No newline at end of file diff --git a/lib/font/tables/post.coffee b/lib/font/tables/post.coffee index 1a29d91..3e9f8fc 100644 --- a/lib/font/tables/post.coffee +++ b/lib/font/tables/post.coffee @@ -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 \ No newline at end of file diff --git a/lib/font/ttf.coffee b/lib/font/ttf.coffee index e01e472..7fdb708 100644 --- a/lib/font/ttf.coffee +++ b/lib/font/ttf.coffee @@ -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 diff --git a/lib/font/utils.coffee b/lib/font/utils.coffee new file mode 100644 index 0000000..a19595f --- /dev/null +++ b/lib/font/utils.coffee @@ -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("<>") == "<>" +# 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 \ No newline at end of file diff --git a/lib/mixins/text.coffee b/lib/mixins/text.coffee index 707593c..994942b 100644 --- a/lib/mixins/text.coffee +++ b/lib/mixins/text.coffee @@ -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 diff --git a/lib/reference.coffee b/lib/reference.coffee index 51567a5..72b149a 100644 --- a/lib/reference.coffee +++ b/lib/reference.coffee @@ -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