diff --git a/lib/document.coffee b/lib/document.coffee index 59be08d..98fa3b8 100644 --- a/lib/document.coffee +++ b/lib/document.coffee @@ -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 diff --git a/lib/font.coffee b/lib/font.coffee index aa3f47f..92af2df 100644 --- a/lib/font.coffee +++ b/lib/font.coffee @@ -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 = ''' diff --git a/lib/image/jpeg.coffee b/lib/image/jpeg.coffee index a21933c..7354e77 100644 --- a/lib/image/jpeg.coffee +++ b/lib/image/jpeg.coffee @@ -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 \ No newline at end of file diff --git a/lib/image/png.coffee b/lib/image/png.coffee index bf5f6dd..ccdd8bd 100644 --- a/lib/image/png.coffee +++ b/lib/image/png.coffee @@ -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 \ No newline at end of file diff --git a/lib/mixins/fonts.coffee b/lib/mixins/fonts.coffee index 654c3fa..67a9fa1 100644 --- a/lib/mixins/fonts.coffee +++ b/lib/mixins/fonts.coffee @@ -48,4 +48,10 @@ module.exports = registerFont: (name, path, family) -> @_registeredFonts[name] = filename: path - family: family \ No newline at end of file + family: family + + embedFonts: (fn) -> + fonts = (font for family, font of @_fontFamilies) + do proceed = => + return fn() if fonts.length is 0 + fonts.shift().embed(proceed) \ No newline at end of file diff --git a/lib/mixins/images.coffee b/lib/mixins/images.coffee index da617af..d5540df 100644 --- a/lib/mixins/images.coffee +++ b/lib/mixins/images.coffee @@ -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 \ No newline at end of file + 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() \ No newline at end of file diff --git a/lib/object.coffee b/lib/object.coffee index 083b720..e2fed0c 100644 --- a/lib/object.coffee +++ b/lib/object.coffee @@ -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(' ') diff --git a/lib/page.coffee b/lib/page.coffee index 41e2230..af192c7 100644 --- a/lib/page.coffee +++ b/lib/page.coffee @@ -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 diff --git a/lib/reference.coffee b/lib/reference.coffee index 48cc240..dd4282e 100644 --- a/lib/reference.coffee +++ b/lib/reference.coffee @@ -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" diff --git a/lib/store.coffee b/lib/store.coffee index 5e91f79..080f3fc 100644 --- a/lib/store.coffee +++ b/lib/store.coffee @@ -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']