Switch to using Node's built in zlib bindings. Fixes #48.

This commit is contained in:
Devon Govett 2012-04-03 19:28:59 -07:00
parent a39c7d0995
commit b9da8ebedd
10 changed files with 258 additions and 229 deletions

View File

@ -15,7 +15,7 @@ class PDFDocument
@version = 1.3
# Whether streams should be compressed
@compress = true
@compress = yes
# The PDF object store
@store = new PDFObjectStore
@ -80,30 +80,34 @@ class PDFDocument
@page.content.add str
return this # make chaining possible
write: (filename, callback) ->
fs.writeFile filename, @output(), 'binary', callback
write: (filename, fn) ->
@output (out) ->
fs.writeFile filename, out, 'binary', fn
output: ->
out = []
@finalize()
@generateHeader out
@generateBody out
@generateXRef out
@generateTrailer out
return out.join('\n')
output: (fn) ->
@finalize =>
out = []
@generateHeader out
@generateBody out, =>
@generateXRef out
@generateTrailer out
fn out.join('\n')
finalize: ->
finalize: (fn) ->
# convert strings in the info dictionary to literals
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()
@embedFonts =>
# embed the images
@embedImages =>
done = 0
cb = => fn() if ++done is @pages.length
# finalize each page
for page in @pages
page.finalize(cb)
generateHeader: (out) ->
# PDF version
@ -113,17 +117,20 @@ class PDFDocument
out.push "%\xFF\xFF\xFF\xFF\n"
return out
generateBody: (out) ->
generateBody: (out, fn) ->
offset = out.join('\n').length
for id, ref of @store.objects
object = ref.object()
ref.offset = offset
out.push object
offset += object.length + 1
@xref_offset = offset
refs = (ref for id, ref of @store.objects)
do proceed = =>
if ref = refs.shift()
ref.object @compress, (object) ->
ref.offset = offset
out.push object
offset += object.length + 1
proceed()
else
@xref_offset = offset
fn()
generateXRef: (out) ->
len = @store.length + 1

View File

@ -6,7 +6,7 @@ By Devon Govett
TTFFont = require './font/ttf'
AFMFont = require './font/afm'
Subset = require './font/subset'
zlib = require 'flate'
zlib = require 'zlib'
class PDFFont
constructor: (@document, @filename, @family, @id) ->
@ -29,8 +29,9 @@ class PDFFont
use: (characters) ->
@subset?.use characters
embed: ->
@embedTTF() unless @isAFM
embed: (fn) ->
return fn() if @isAFM
@embedTTF fn
encode: (text) ->
@subset?.encodeText(text) or text
@ -78,52 +79,54 @@ class PDFFont
Type: 'Font'
Subtype: 'TrueType'
embedTTF: ->
embedTTF: (fn) ->
data = @subset.encode()
compressedData = zlib.deflate(data)
zlib.deflate data, (err, compressedData) =>
throw err if err
@fontfile = @document.ref
Length: compressedData.length
Length1: data.length
Filter: 'FlateDecode'
@fontfile = @document.ref
Length: compressedData.length
Length1: data.length
Filter: 'FlateDecode'
@fontfile.add compressedData
@fontfile.add compressedData
@descriptor = @document.ref
Type: 'FontDescriptor'
FontName: @subset.postscriptName
FontFile2: @fontfile
FontBBox: @bbox
Flags: @flags
StemV: @stemV
ItalicAngle: @italicAngle
Ascent: @ascender
Descent: @decender
CapHeight: @capHeight
XHeight: @xHeight
@descriptor = @document.ref
Type: 'FontDescriptor'
FontName: @subset.postscriptName
FontFile2: @fontfile
FontBBox: @bbox
Flags: @flags
StemV: @stemV
ItalicAngle: @italicAngle
Ascent: @ascender
Descent: @decender
CapHeight: @capHeight
XHeight: @xHeight
firstChar = +Object.keys(@subset.cmap)[0]
charWidths = for code, glyph of @subset.cmap
Math.round @ttf.hmtx.forGlyph(glyph).advance * @scaleFactor
firstChar = +Object.keys(@subset.cmap)[0]
charWidths = for code, glyph of @subset.cmap
Math.round @ttf.hmtx.forGlyph(glyph).advance * @scaleFactor
cmap = @document.ref()
cmap.add toUnicodeCmap(@subset.subset)
cmap.finalize(true) # compress it
cmap = @document.ref()
cmap.add toUnicodeCmap(@subset.subset)
ref =
Type: 'Font'
BaseFont: @subset.postscriptName
Subtype: 'TrueType'
FontDescriptor: @descriptor
FirstChar: firstChar
LastChar: firstChar + charWidths.length - 1
Widths: @document.ref charWidths
Encoding: 'MacRomanEncoding'
ToUnicode: cmap
ref =
Type: 'Font'
BaseFont: @subset.postscriptName
Subtype: 'TrueType'
FontDescriptor: @descriptor
FirstChar: firstChar
LastChar: firstChar + charWidths.length - 1
Widths: @document.ref charWidths
Encoding: 'MacRomanEncoding'
ToUnicode: cmap
for key, val of ref
@ref.data[key] = val
for key, val of ref
@ref.data[key] = val
cmap.finalize(@document.compress, fn) # compress it
toUnicodeCmap = (map) ->
unicodeMap = '''

View File

@ -2,11 +2,6 @@ fs = require 'fs'
Data = '../data'
class JPEG
@open: (filename) ->
contents = fs.readFileSync filename
data = new Data(contents)
new JPEG(data)
constructor: (@data) ->
len = data.length
@ -36,7 +31,7 @@ class JPEG
@imgData = @data
object: (document) ->
object: (document, fn) ->
obj = document.ref
Type: 'XObject'
Subtype: 'Image'
@ -54,6 +49,6 @@ class JPEG
obj.data['Decode'] = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0]
obj.add @data.data
return obj
fn obj
module.exports = JPEG

View File

@ -1,13 +1,8 @@
fs = require 'fs'
Data = '../data'
zlib = require 'flate'
zlib = require 'zlib'
class PNG
@open: (filename) ->
contents = fs.readFileSync filename
data = new Data(contents)
new PNG(data)
constructor: (@data) ->
data.pos = 8 # Skip the default header
@ -84,7 +79,22 @@ class PNG
return
object: (document) ->
object: (document, fn) ->
# get the async stuff out of the way first
if not @alphaChannel
if @transparency.indexed
# Create a transparency SMask for the image based on the data
# in the PLTE and tRNS sections. See below for details on SMasks.
@loadIndexedAlphaChannel => @object document, fn
return
else if @hasAlphaChannel
# For PNG color types 4 and 6, the transparency data is stored as a alpha
# channel mixed in with the main image data. Separate this data out into an
# SMask object and store it separately in the PDF.
@splitAlphaChannel => @object document, fn
return
obj = document.ref
Type: 'XObject'
Subtype: 'Image'
@ -130,18 +140,6 @@ class PNG
mask.push x, x
obj.data['Mask'] = mask
else if @transparency.indexed
# Create a transparency SMask for the image based on the data
# in the PLTE and tRNS sections. See below for details on SMasks.
@loadIndexedAlphaChannel()
# For PNG color types 4 and 6, the transparency data is stored as a alpha
# channel mixed in with the main image data. Separate this data out into an
# SMask object and store it separately in the PDF.
if @hasAlphaChannel
@splitAlphaChannel()
obj.data['Length'] = @imgData.length
if @alphaChannel
sMask = document.ref
@ -160,105 +158,112 @@ class PNG
# add the actual image data
obj.add @imgData
return obj
fn obj
decodePixels: ->
data = zlib.inflate @imgData
pixelBytes = @pixelBitlength / 8
scanlineLength = pixelBytes * @width
row = 0
pixels = []
length = data.length
pos = 0
while pos < length
filter = data[pos++]
i = 0
rowData = []
switch filter
when 0 # None
while i < scanlineLength
rowData[i++] = data[pos++]
when 1 # Sub
while i < scanlineLength
byte = data[pos++]
left = if i < pixelBytes then 0 else rowData[i - pixelBytes]
rowData[i++] = (byte + left) % 256
when 2 # Up
while i < scanlineLength
byte = data[pos++]
col = (i - (i % pixelBytes)) / pixelBytes
upper = if row is 0 then 0 else pixels[row - 1][col][i % pixelBytes]
rowData[i++] = (upper + byte) % 256
when 3 # Average
while i < scanlineLength
byte = data[pos++]
col = (i - (i % pixelBytes)) / pixelBytes
left = if i < pixelBytes then 0 else rowData[i - pixelBytes]
upper = if row is 0 then 0 else pixels[row - 1][col][i % pixelBytes]
rowData[i++] = (byte + Math.floor((left + upper) / 2)) % 256
when 4 # Paeth
while i < scanlineLength
byte = data[pos++]
col = (i - (i % pixelBytes)) / pixelBytes
left = if i < pixelBytes then 0 else rowData[i - pixelBytes]
if row is 0
upper = upperLeft = 0
else
upper = pixels[row - 1][col][i % pixelBytes]
upperLeft = if col is 0 then 0 else pixels[row - 1][col - 1][i % pixelBytes]
p = left + upper - upperLeft
pa = Math.abs(p - left)
pb = Math.abs(p - upper)
pc = Math.abs(p - upperLeft)
if pa <= pb and pa <= pc
paeth = left
else if pb <= pc
paeth = upper
else
paeth = upperLeft
rowData[i++] = (byte + paeth) % 256
else
throw new Error "Invalid filter algorithm: " + filter
s = []
for i in [0...rowData.length] by pixelBytes
s.push rowData.slice(i, i + pixelBytes)
pixels.push(s)
row += 1
decodePixels: (fn) ->
zlib.inflate @imgData, (err, data) =>
throw err if err
return pixels
splitAlphaChannel: ->
pixels = @decodePixels()
pixelBytes = @pixelBitlength / 8
scanlineLength = pixelBytes * @width
colorByteSize = @colors * @bits / 8
alphaByteSize = 1
row = 0
pixels = []
length = data.length
pos = 0
pixelCount = @width * @height
imgData = new Buffer(pixelCount * colorByteSize)
alphaChannel = new Buffer(pixelCount)
while pos < length
filter = data[pos++]
i = 0
rowData = []
switch filter
when 0 # None
while i < scanlineLength
rowData[i++] = data[pos++]
when 1 # Sub
while i < scanlineLength
byte = data[pos++]
left = if i < pixelBytes then 0 else rowData[i - pixelBytes]
rowData[i++] = (byte + left) % 256
when 2 # Up
while i < scanlineLength
byte = data[pos++]
col = (i - (i % pixelBytes)) / pixelBytes
upper = if row is 0 then 0 else pixels[row - 1][col][i % pixelBytes]
rowData[i++] = (upper + byte) % 256
when 3 # Average
while i < scanlineLength
byte = data[pos++]
col = (i - (i % pixelBytes)) / pixelBytes
left = if i < pixelBytes then 0 else rowData[i - pixelBytes]
upper = if row is 0 then 0 else pixels[row - 1][col][i % pixelBytes]
rowData[i++] = (byte + Math.floor((left + upper) / 2)) % 256
when 4 # Paeth
while i < scanlineLength
byte = data[pos++]
col = (i - (i % pixelBytes)) / pixelBytes
left = if i < pixelBytes then 0 else rowData[i - pixelBytes]
if row is 0
upper = upperLeft = 0
else
upper = pixels[row - 1][col][i % pixelBytes]
upperLeft = if col is 0 then 0 else pixels[row - 1][col - 1][i % pixelBytes]
p = left + upper - upperLeft
pa = Math.abs(p - left)
pb = Math.abs(p - upper)
pc = Math.abs(p - upperLeft)
if pa <= pb and pa <= pc
paeth = left
else if pb <= pc
paeth = upper
else
paeth = upperLeft
rowData[i++] = (byte + paeth) % 256
else
throw new Error "Invalid filter algorithm: " + filter
s = []
for i in [0...rowData.length] by pixelBytes
s.push rowData.slice(i, i + pixelBytes)
pixels.push(s)
row += 1
fn pixels
p = a = 0
for row in pixels
for pixel in row
imgData[p++] = pixel[i] for i in [0...colorByteSize]
alphaChannel[a++] = pixel[colorByteSize]
splitAlphaChannel: (fn) ->
@decodePixels (pixels) =>
colorByteSize = @colors * @bits / 8
alphaByteSize = 1
pixelCount = @width * @height
imgData = new Buffer(pixelCount * colorByteSize)
alphaChannel = new Buffer(pixelCount)
@imgData = zlib.deflate imgData
@alphaChannel = zlib.deflate alphaChannel
p = a = 0
for row in pixels
for pixel in row
imgData[p++] = pixel[i] for i in [0...colorByteSize]
alphaChannel[a++] = pixel[colorByteSize]
done = 0
zlib.deflate imgData, (err, @imgData) =>
throw err if err
fn() if ++done is 2
zlib.deflate alphaChannel, (err, @alphaChannel) =>
throw err if err
fn() if ++done is 2
decodePalette: ->
palette = @palette
@ -273,19 +278,20 @@ class PNG
return decodingMap
loadIndexedAlphaChannel: ->
loadIndexedAlphaChannel: (fn) ->
palette = @decodePalette()
pixels = @decodePixels()
@decodePixels (pixels) =>
pixelCount = @width * @height
alphaChannel = new Buffer(pixelCount)
pixelCount = @width * @height
alphaChannel = new Buffer(pixelCount)
i = 0
for row in pixels
for pixel in row
pixel = pixel[0]
alphaChannel[i++] = palette[pixel][3]
i = 0
for row in pixels
for pixel in row
pixel = pixel[0]
alphaChannel[i++] = palette[pixel][3]
@alphaChannel = zlib.deflate alphaChannel
zlib.deflate alphaChannel, (err, @alphaChannel) =>
throw err if err
fn()
module.exports = PNG

View File

@ -48,4 +48,10 @@ module.exports =
registerFont: (name, path, family) ->
@_registeredFonts[name] =
filename: path
family: family
family: family
embedFonts: (fn) ->
fonts = (font for family, font of @_fontFamilies)
do proceed = =>
return fn() if fonts.length is 0
fonts.shift().embed(proceed)

View File

@ -14,13 +14,12 @@ module.exports =
y = y ? options.y ? @y
if @_imageRegistry[src]
[image, obj, label] = @_imageRegistry[src]
[image, @page, label] = @_imageRegistry[src]
else
image = PDFImage.open(src)
obj = image.object(this)
label = "I" + (++@_imageCount)
@_imageRegistry[src] = [image, obj, label]
@_imageRegistry[src] = [image, @page, label]
w = options.width or image.width
h = options.height or image.height
@ -52,13 +51,22 @@ module.exports =
# Set the current y position to below the image if it is in the document flow
@y += h if @y is y
y = @page.height - y - h
@page.xobjects[label] ?= obj
@save()
@addContent "#{w} 0 0 #{h} #{x} #{y} cm"
@addContent "/#{label} Do"
@restore()
return this
return this
embedImages: (fn) ->
images = (item for src, item of @_imageRegistry)
do proceed = =>
if images.length
[image, page, label] = images.shift()
image.object this, (obj) ->
page.xobjects[label] ?= obj
proceed()
else
fn()

View File

@ -3,10 +3,10 @@ PDFObject - converts JavaScript types into their corrisponding PDF types.
By Devon Govett
###
pad = (str, length) ->
(Array(length + 1).join('0') + str).slice(-length)
class PDFObject
pad = (str, length) ->
(Array(length + 1).join('0') + str).slice(-length)
@convert: (object) ->
if Array.isArray object
items = (PDFObject.convert e for e in object).join(' ')

View File

@ -5,8 +5,8 @@ By Devon Govett
class PDFPage
constructor: (@document, options = {}) ->
@size = options.size or "letter"
@layout = options.layout or "portrait"
@size = options.size or 'letter'
@layout = options.layout or 'portrait'
# if margin was passed as a single number
if typeof options.margin is 'number'
@ -55,8 +55,8 @@ class PDFPage
maxY: ->
@height - @margins.bottom
finalize: ->
@content.finalize(@document.compress)
finalize: (fn) ->
@content.finalize(@document.compress, fn)
DEFAULT_MARGINS =
top: 72

View File

@ -3,7 +3,7 @@ PDFReference - represents a reference to another object in the PDF object heirar
By Devon Govett
###
zlib = require 'flate'
zlib = require 'zlib'
class PDFReference
constructor: (@id, @data = {}) ->
@ -11,8 +11,10 @@ class PDFReference
@stream = null
@finalizedStream = null
object: ->
@finalize() if not @finalizedStream
object: (compress, fn) ->
unless @finalizedStream?
return @finalize compress, => @object compress, fn
out = ["#{@id} #{@gen} obj"]
out.push PDFObject.convert(@data)
@ -22,30 +24,33 @@ class PDFReference
out.push "endstream"
out.push "endobj"
return out.join '\n'
fn out.join '\n'
add: (s) ->
@stream ?= []
@stream.push if Buffer.isBuffer(s) then s.toString('binary') else s
finalize: (compress = false) ->
finalize: (compress = false, fn) ->
# cache the finalized stream
if @stream
data = @stream.join '\n'
if compress
if compress and not @data.Filter
# 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'
@data.Filter = 'FlateDecode'
zlib.deflate data, (err, compressedData) =>
throw err if err
@finalizedStream = compressedData.toString 'binary'
@data.Filter = 'FlateDecode'
@data.Length = @finalizedStream.length
fn()
else
@finalizedStream = data
@data.Length ?= @finalizedStream.length
@data.Length = @finalizedStream.length
fn()
else
@finalizedStream = ''
fn()
toString: ->
"#{@id} #{@gen} R"

View File

@ -9,14 +9,13 @@ class PDFObjectStore
constructor: ->
@objects = {}
@length = 0
@root = @ref
Type: 'Catalog'
@root.data['Pages'] = @ref
Type: 'Pages'
Count: 0
Kids: []
Pages: @ref
Type: 'Pages'
Count: 0
Kids: []
@pages = @root.data['Pages']