mirror of
https://github.com/foliojs/pdfkit.git
synced 2025-12-08 20:15:54 +00:00
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:
parent
84853322b8
commit
9d90e25664
BIN
demo/out.pdf
BIN
demo/out.pdf
Binary file not shown.
124
lib/data.coffee
124
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
|
||||
@ -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()
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
227
lib/font/macroman.coffee
Normal 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
106
lib/font/subset.coffee
Normal 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
|
||||
@ -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
|
||||
@ -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
104
lib/font/tables/glyf.coffee
Normal 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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
44
lib/font/tables/loca.coffee
Normal file
44
lib/font/tables/loca.coffee
Normal 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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
67
lib/font/utils.coffee
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user