diff --git a/lib/document.coffee b/lib/document.coffee index 6b994f1..2eab1e4 100644 --- a/lib/document.coffee +++ b/lib/document.coffee @@ -3,14 +3,16 @@ PDFDocument - represents an entire PDF document By Devon Govett ### +stream = require 'stream' fs = require 'fs' -PDFObjectStore = require './store' PDFObject = require './object' PDFReference = require './reference' PDFPage = require './page' -class PDFDocument +class PDFDocument extends stream.Readable constructor: (@options = {}) -> + super + # PDF version @version = 1.3 @@ -18,10 +20,17 @@ class PDFDocument @compress = yes # The PDF object store - @store = new PDFObjectStore - - # A list of pages in this document - @pages = [] + @_objects = [] + @_waiting = 0 + @_ended = false + @_offset = 0 + + @_root = @ref + Type: 'Catalog' + Pages: @ref + Type: 'Pages' + Count: 0 + Kids: [] # The current page @page = null @@ -33,16 +42,22 @@ class PDFDocument @initText() @initImages() - # Create the metadata - @_info = @ref + # Initialize the metadata + @info = Producer: 'PDFKit' Creator: 'PDFKit' CreationDate: new Date() - - @info = @_info.data + if @options.info - @info[key] = val for key, val of @options.info - delete @options.info + for key, val of @options.info + @info[key] = val + + # Write the header + # PDF version + @_write "%PDF-#{@version}" + + # 4 binary chars, as recommended by the spec + @_write "%\xFF\xFF\xFF\xFF" # Add the first page @addPage() @@ -61,12 +76,16 @@ class PDFDocument mixin 'annotations' addPage: (options = @options) -> + # end the current page if needed + @page?.end() + # create a page object @page = new PDFPage(this, options) # add the page to the object store - @store.addPage @page - @pages.push @page + pages = @_root.data.Pages.data + pages.Kids.push @page.dictionary + pages.Count++ # reset x and y coordinates @x = @page.margins.left @@ -80,90 +99,97 @@ class PDFDocument return this ref: (data) -> - @store.ref(data) + ref = new PDFReference(this, @_objects.length + 1, data) + @_objects.push ref + @_waiting++ + return ref - addContent: (str) -> - @page.content.add str - return this # make chaining possible + _read: -> + # do nothing, but this method is required by node + + _write: (data) -> + unless Buffer.isBuffer(data) + data = new Buffer(data + '\n', 'binary') + + @push data + @_offset += data.length + + addContent: (data) -> + @page.write data + return this + + _refEnd: -> + if --@_waiting is 0 and @_ended + @_finalize() + @_ended = false write: (filename, fn) -> - @output (out) -> - fs.writeFile filename, out, 'binary', fn + # print a deprecation warning with a stacktrace + err = new Error ' + PDFDocument#write is deprecated, and will be removed in a future version of PDFKit. + Please pipe the document into a Node stream. + ' + + console.warn err.stack + + @pipe fs.createWriteStream(filename) + @end() + @once 'end', fn output: (fn) -> - @finalize => - out = [] - @generateHeader out - @generateBody out, => - @generateXRef out - @generateTrailer out + # more difficult to support this. It would involve concatenating all the buffers together + throw new Error ' + PDFDocument#output is deprecated, and has been removed from PDFKit. + Please pipe the document into a Node stream. + ' - ret = [] - for k in out - ret.push(k + '\n') - - fn new Buffer(ret.join(''),'binary') + end: -> + @page.end() - 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, true + @_info = @ref() + for key, val of @info + if typeof val is 'string' + val = PDFObject.s val, true + + @_info.data[key] = val + + @_info.end() - # embed the subsetted fonts - @embedFonts => - # embed the images - @embedImages => - done = 0 - cb = => fn() if ++done is @pages.length - - # finalize each page - for page in @pages - page.finalize(cb) + for name, font of @_fontFamilies + font.embed() + + @_root.end() + @_root.data.Pages.end() - generateHeader: (out) -> - # PDF version - out.push "%PDF-#{@version}" - - # 4 binary chars, as recommended by the spec - out.push "%\xFF\xFF\xFF\xFF\n" - return out + if @_waiting is 0 + @_finalize() + else + @_ended = true - generateBody: (out, fn) -> - offset = out.join('\n').length + 1 + _finalize: (fn) -> + # generate xref + xRefOffset = @_offset + @_write "xref" + @_write "0 #{@_objects.length + 1}" + @_write "0000000000 65535 f " - 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 # plus one for newline - proceed() - else - @xref_offset = offset - fn() - - generateXRef: (out) -> - len = @store.length + 1 - out.push "xref" - out.push "0 #{len}" - out.push "0000000000 65535 f " - - for id, ref of @store.objects + for ref in @_objects offset = ('0000000000' + ref.offset).slice(-10) - out.push offset + ' 00000 n ' - - generateTrailer: (out) -> - trailer = PDFObject.convert - Size: @store.length + 1 - Root: @store.root + @_write offset + ' 00000 n ' + + # trailer + @_write 'trailer' + @_write PDFObject.convert + Size: @_objects.length + Root: @_root Info: @_info + + @_write 'startxref' + @_write "#{xRefOffset}" + @_write '%%EOF' - out.push 'trailer' - out.push trailer - out.push 'startxref' - out.push @xref_offset - out.push '%%EOF' + # end the stream + @push null toString: -> "[object PDFDocument]" diff --git a/lib/font.coffee b/lib/font.coffee index 1a8947b..9fbf44b 100644 --- a/lib/font.coffee +++ b/lib/font.coffee @@ -10,10 +10,12 @@ zlib = require 'zlib' class PDFFont constructor: (@document, @filename, @family, @id) -> + @ref = @document.ref() + if @filename in @_standardFonts @isAFM = true @font = AFMFont.open __dirname + "/font/data/#{@filename}.afm" - @registerStandard() + @registerAFM() else if /\.(ttf|ttc)$/i.test @filename @font = TTFFont.open @filename, @family @@ -31,9 +33,11 @@ class PDFFont use: (characters) -> @subset?.use characters - embed: (fn) -> - return fn() if @isAFM - @embedTTF fn + embed: -> + if @isAFM + @embedAFM() + else + @embedTTF() encode: (text) -> if @isAFM @@ -74,59 +78,49 @@ class PDFFont @flags |= 1 << 5 # assume the font is nonsymbolic... throw new Error 'No unicode cmap for font' if not @font.cmap.unicode + + embedTTF: -> + data = @subset.encode() + fontfile = @document.ref() + fontfile.write data - # Create a placeholder reference to be filled in embedTTF. - @ref = @document.ref + fontfile.data.Length1 = fontfile.uncompressedLength + fontfile.end() + + 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.end() + + firstChar = +Object.keys(@subset.cmap)[0] + charWidths = for code, glyph of @subset.cmap + Math.round @font.widthOfGlyph(glyph) + + cmap = @document.ref() + cmap.end toUnicodeCmap(@subset.subset) + + @ref.data = Type: 'Font' + BaseFont: @subset.postscriptName Subtype: 'TrueType' - - embedTTF: (fn) -> - data = @subset.encode() - zlib.deflate data, (err, compressedData) => - throw err if err + FontDescriptor: descriptor + FirstChar: firstChar + LastChar: firstChar + charWidths.length - 1 + Widths: charWidths + Encoding: 'MacRomanEncoding' + ToUnicode: cmap - @fontfile = @document.ref - Length: compressedData.length - Length1: data.length - Filter: 'FlateDecode' - - @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 - - firstChar = +Object.keys(@subset.cmap)[0] - charWidths = for code, glyph of @subset.cmap - Math.round @font.widthOfGlyph(glyph) - - 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 - - for key, val of ref - @ref.data[key] = val - - cmap.finalize(@document.compress, fn) # compress it + @ref.end() toUnicodeCmap = (map) -> unicodeMap = ''' @@ -164,15 +158,18 @@ class PDFFont end ''' - registerStandard: -> + registerAFM: -> {@ascender,@decender,@bbox,@lineGap} = @font - @ref = @document.ref + embedAFM: -> + @ref.data = Type: 'Font' BaseFont: @filename Subtype: 'Type1' Encoding: 'WinAnsiEncoding' + @ref.end() + _standardFonts: [ "Courier" "Courier-Bold" diff --git a/lib/gradient.coffee b/lib/gradient.coffee index 335ba64..cdddd1f 100644 --- a/lib/gradient.coffee +++ b/lib/gradient.coffee @@ -3,6 +3,7 @@ class PDFGradient @stops = [] @embedded = no @transform = [1, 0, 0, 1, 0, 0] + @_colorSpace = 'DeviceRGB' stop: (pos, color, opacity = 1) -> opacity = Math.max(0, Math.min(1, opacity)) @@ -35,6 +36,7 @@ class PDFGradient N: 1 stops.push fn + fn.end() # if there are only two stops, we don't need a stitching function if stops.length is 1 @@ -47,6 +49,8 @@ class PDFGradient Bounds: bounds Encode: encode + fn.end() + @id = 'Sh' + (++@doc._gradCount) # apply gradient transform to existing document ctm @@ -60,26 +64,40 @@ class PDFGradient m[4] = m0 * dx + m2 * dy + m4 m[5] = m1 * dx + m3 * dy + m5 + shader = @shader fn + shader.end() + pattern = @doc.ref Type: 'Pattern' PatternType: 2 - Shading: @shader fn + Shading: shader Matrix: (+v.toFixed(5) for v in m) @doc.page.patterns[@id] = pattern + pattern.end() if (@stops.some (stop) -> stop[2] < 1) grad = @opacityGradient() + grad._colorSpace = 'DeviceGray' + for stop in @stops grad.stop stop[0], [stop[2]] grad = grad.embed() - grad.data.Shading.data.ColorSpace = 'DeviceGray' group = @doc.ref Type: 'Group' S: 'Transparency' CS: 'DeviceGray' + + group.end() + + resources = @doc.ref + ProcSet: ['PDF', 'Text', 'ImageB', 'ImageC', 'ImageI'] + Shading: + Sh1: grad.data.Shading + + resources.end() form = @doc.ref Type: 'XObject' @@ -87,17 +105,16 @@ class PDFGradient FormType: 1 BBox: [0, 0, @doc.page.width, @doc.page.height] Group: group - Resources: @doc.ref - ProcSet: ['PDF', 'Text', 'ImageB', 'ImageC', 'ImageI'] - Shading: - Sh1: grad.data.Shading + Resources: resources - form.add "/Sh1 sh" + form.end "/Sh1 sh" sMask = @doc.ref Type: 'Mask' S: 'Luminosity' G: form + + sMask.end() gstate = @doc.ref Type: 'ExtGState' @@ -107,6 +124,7 @@ class PDFGradient name = "Gs#{@opacity_id}" @doc.page.ext_gstates[name] = gstate + gstate.end() return pattern @@ -115,7 +133,6 @@ class PDFGradient @doc.addContent "/#{@id} #{op}" if @opacity_id - @doc.save() @doc.addContent "/Gs#{@opacity_id} gs" @doc._sMasked = true @@ -126,7 +143,7 @@ class PDFLinearGradient extends PDFGradient shader: (fn) -> @doc.ref ShadingType: 2 - ColorSpace: 'DeviceRGB' + ColorSpace: @_colorSpace Coords: [@x1, @y1, @x2, @y2] Function: fn Extend: [true, true] @@ -141,7 +158,7 @@ class PDFRadialGradient extends PDFGradient shader: (fn) -> @doc.ref ShadingType: 3 - ColorSpace: 'DeviceRGB' + ColorSpace: @_colorSpace Coords: [@x1, @y1, @r1, @x2, @y2, @r2] Function: fn Extend: [true, true] diff --git a/lib/image.coffee b/lib/image.coffee index e438899..d4af108 100644 --- a/lib/image.coffee +++ b/lib/image.coffee @@ -9,7 +9,7 @@ JPEG = require './image/jpeg' PNG = require './image/png' class PDFImage - @open: (filenameOrBuffer) -> + @open: (filenameOrBuffer, label) -> if typeof filenameOrBuffer is 'object' and filenameOrBuffer instanceof Buffer @contents = filenameOrBuffer else @@ -24,10 +24,10 @@ class PDFImage firstByte = data.byteAt(0) if firstByte is 0xFF and data.byteAt(1) is 0xD8 - return new JPEG(data) + return new JPEG(data, label) else if firstByte is 0x89 and data.stringAt(1, 3) is "PNG" - return new PNG(data) + return new PNG(data, label) else throw new Error 'Unknown image format.' diff --git a/lib/image/jpeg.coffee b/lib/image/jpeg.coffee index f2f0ec4..143bf80 100644 --- a/lib/image/jpeg.coffee +++ b/lib/image/jpeg.coffee @@ -1,9 +1,8 @@ fs = require 'fs' Data = '../data' -setImmediate = global.setImmediate ? process.nextTick # backfill for node <0.10 class JPEG - constructor: (@data) -> + constructor: (@data, @label) -> len = data.length if data.readUInt16() isnt 0xFFD8 @@ -30,10 +29,8 @@ class JPEG when 3 then 'DeviceRGB' when 4 then 'DeviceCMYK' - @imgData = @data - - object: (document, fn) -> - obj = document.ref + embed: (document) -> + @obj = document.ref Type: 'XObject' Subtype: 'Image' BitsPerComponent: @bits @@ -47,9 +44,8 @@ class JPEG # min and max values from the default, we invert the colors. See # section 4.8.4 of the spec. if @colorSpace is 'DeviceCMYK' - obj.data['Decode'] = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0] + @obj.data['Decode'] = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0] - obj.add @data.data - setImmediate -> fn(obj) + @obj.end @data.data module.exports = JPEG diff --git a/lib/image/png.coffee b/lib/image/png.coffee index 0cdbfff..0699b18 100644 --- a/lib/image/png.coffee +++ b/lib/image/png.coffee @@ -1,31 +1,15 @@ zlib = require 'zlib' PNG = require 'png-js' -setImmediate = global.setImmediate ? process.nextTick # backfill for node <0.10 class PNGImage - constructor: (data) -> + constructor: (data, @label) -> @image = new PNG(data.data) @width = @image.width @height = @image.height @imgData = @image.imgData - object: (document, fn) -> - # get the async stuff out of the way first - if not @alphaChannel - if @image.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 @image.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 + embed: (@document) -> + @obj = document.ref Type: 'XObject' Subtype: 'Image' BitsPerComponent: @image.bits @@ -33,25 +17,28 @@ class PNGImage Height: @height Length: @imgData.length Filter: 'FlateDecode' - + unless @image.hasAlphaChannel - obj.data['DecodeParms'] = document.ref + params = document.ref Predictor: 15 Colors: @image.colors BitsPerComponent: @image.bits Columns: @width + @obj.data['DecodeParms'] = params + params.end() + if @image.palette.length is 0 - obj.data['ColorSpace'] = @image.colorSpace + @obj.data['ColorSpace'] = @image.colorSpace else # embed the color palette in the PDF as an object stream palette = document.ref Length: @image.palette.length - palette.add new Buffer(@image.palette) + palette.end @image.palette # build the color space array for the image - obj.data['ColorSpace'] = ['Indexed', 'DeviceRGB', (@image.palette.length / 3) - 1, palette] + @obj.data['ColorSpace'] = ['Indexed', 'DeviceRGB', (@image.palette.length / 3) - 1, palette] # For PNG color types 0, 2 and 3, the transparency data is stored in # a dedicated PNG chunk. @@ -59,7 +46,7 @@ class PNGImage # Use Color Key Masking (spec section 4.8.5) # An array with N elements, where N is two times the number of color components. val = @image.transparency.greyscale - obj.data['Mask'] = [val, val] + @obj.data['Mask'] = [val, val] else if @image.transparency.rgb # Use Color Key Masking (spec section 4.8.5) @@ -69,10 +56,25 @@ class PNGImage for x in rgb mask.push x, x - obj.data['Mask'] = mask + @obj.data['Mask'] = mask + else if @image.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() + + else if @image.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() + + else + @finalize() + + finalize: -> if @alphaChannel - sMask = document.ref + sMask = @document.ref Type: 'XObject' Subtype: 'Image' Height: @height @@ -83,14 +85,13 @@ class PNGImage ColorSpace: 'DeviceGray' Decode: [0, 1] - sMask.add @alphaChannel - obj.data['SMask'] = sMask + sMask.end @alphaChannel + @obj.data['SMask'] = sMask - # add the actual image data - obj.add @imgData - setImmediate -> fn(obj) + # add the actual image data + @obj.end @imgData - splitAlphaChannel: (fn) -> + splitAlphaChannel: -> @image.decodePixels (pixels) => colorByteSize = @image.colors * @image.bits / 8 pixelCount = @width * @height @@ -108,11 +109,11 @@ class PNGImage done = 0 zlib.deflate imgData, (err, @imgData) => throw err if err - fn() if ++done is 2 + @finalize() if ++done is 2 zlib.deflate alphaChannel, (err, @alphaChannel) => throw err if err - fn() if ++done is 2 + @finalize() if ++done is 2 loadIndexedAlphaChannel: (fn) -> transparency = @image.transparency.indexed @@ -125,6 +126,6 @@ class PNGImage zlib.deflate alphaChannel, (err, @alphaChannel) => throw err if err - fn() - + @finalize() + module.exports = PNGImage diff --git a/lib/mixins/annotations.coffee b/lib/mixins/annotations.coffee index 03a1e14..bfb756e 100644 --- a/lib/mixins/annotations.coffee +++ b/lib/mixins/annotations.coffee @@ -15,7 +15,9 @@ module.exports = for key, val of options options[key[0].toUpperCase() + key.slice(1)] = val - @page.annotations.push @ref options + ref = @ref options + @page.annotations.push ref + ref.end() return this note: (x, y, w, h, contents, options = {}) -> @@ -31,6 +33,7 @@ module.exports = S: 'URI' URI: PDFObject.s url + options.A.end() @annotate x, y, w, h, options _markup: (x, y, w, h, options = {}) -> diff --git a/lib/mixins/color.coffee b/lib/mixins/color.coffee index 5f816fb..1bd8198 100644 --- a/lib/mixins/color.coffee +++ b/lib/mixins/color.coffee @@ -43,9 +43,11 @@ module.exports = Type: 'ExtGState' SMask: 'None' + gstate.end() name = "Gs#{++@_opacityCount}" @page.ext_gstates[name] = gstate @addContent "/#{name} gs" + @_sMasked = false op = if stroke then 'SCN' else 'scn' @@ -108,6 +110,7 @@ module.exports = dictionary.CA = strokeOpacity if strokeOpacity? dictionary = @ref dictionary + dictionary.end() id = ++@_opacityCount name = "Gs#{id}" @_opacityRegistry[key] = [dictionary, name] diff --git a/lib/mixins/fonts.coffee b/lib/mixins/fonts.coffee index fcf2da9..b56872f 100644 --- a/lib/mixins/fonts.coffee +++ b/lib/mixins/fonts.coffee @@ -45,11 +45,6 @@ module.exports = registerFont: (name, path, family) -> @_registeredFonts[name] = filename: path - family: family - return this + family: family - embedFonts: (fn) -> - fonts = (font for family, font of @_fontFamilies) - do proceed = => - return fn() if fonts.length is 0 - fonts.shift().embed(proceed) + return this diff --git a/lib/mixins/images.coffee b/lib/mixins/images.coffee index 8d9825c..a78e09f 100644 --- a/lib/mixins/images.coffee +++ b/lib/mixins/images.coffee @@ -13,14 +13,13 @@ module.exports = x = x ? options.x ? @x y = y ? options.y ? @y - if @_imageRegistry[src] - [image, label, pages] = @_imageRegistry[src] - pages.push @page unless @page in pages - - else - image = PDFImage.open(src) - label = "I" + (++@_imageCount) - @_imageRegistry[src] = [image, label, [@page]] + image = @_imageRegistry[src] + if not image + image = PDFImage.open src, 'I' + (++@_imageCount) + image.embed this + @_imageRegistry[src] = image unless Buffer.isBuffer(src) + + @page.xobjects[image.label] ?= image.obj w = options.width or image.width h = options.height or image.height @@ -60,20 +59,7 @@ module.exports = @save() @transform w, 0, 0, -h, x, y + h - @addContent "/#{label} Do" + @addContent "/#{image.label} Do" @restore() return this - - embedImages: (fn) -> - images = (item for src, item of @_imageRegistry) - do proceed = => - if images.length - [image, label, pages] = images.shift() - image.object this, (obj) -> - for page in pages - page.xobjects[label] ?= obj - - proceed() - else - fn() \ No newline at end of file diff --git a/lib/page.coffee b/lib/page.coffee index c84ddda..2584b3b 100644 --- a/lib/page.coffee +++ b/lib/page.coffee @@ -8,7 +8,7 @@ class PDFPage @size = options.size or 'letter' @layout = options.layout or 'portrait' - # if margin was passed as a single number + # process margins if typeof options.margin is 'number' @margins = top: options.margin @@ -19,47 +19,50 @@ class PDFPage # default to 1 inch margins else @margins = options.margins or DEFAULT_MARGINS - + + # calculate page dimensions dimensions = if Array.isArray(@size) then @size else SIZES[@size.toUpperCase()] @width = dimensions[if @layout is 'portrait' then 0 else 1] @height = dimensions[if @layout is 'portrait' then 1 else 0] - # A reference to the content of this page - @content = @document.ref() - - # The page dictionary - @dictionary = @document.ref - Type: 'Page' - Parent: @document.store.pages - MediaBox: [0, 0, @width, @height] - Contents: @content + @content = @document.ref() - # The resource dictionary - @dictionary.data['Resources'] = @document.ref + # Initialize the Font, XObject, and ExtGState dictionaries + @resources = @document.ref ProcSet: ['PDF', 'Text', 'ImageB', 'ImageC', 'ImageI'] - # Initialize the Font, XObject, and ExtGState dictionaries - @resources = @dictionary.data['Resources'].data - # Lazily create these dictionaries Object.defineProperties this, fonts: - get: => @resources['Font'] ?= {} + get: => @resources.data.Font ?= {} xobjects: - get: => @resources['XObject'] ?= {} + get: => @resources.data.XObject ?= {} ext_gstates: - get: => @resources['ExtGState'] ?= {} + get: => @resources.data.ExtGState ?= {} patterns: - get: => @resources['Pattern'] ?= {} + get: => @resources.data.Pattern ?= {} annotations: - get: => @dictionary.data['Annots'] ?= [] - + get: => @dictionary.data.Annots ?= [] + + # The page dictionary + @dictionary = @document.ref + Type: 'Page' + Parent: @document._root.data.Pages + MediaBox: [0, 0, @width, @height] + Contents: @content + Resources: @resources + maxY: -> @height - @margins.bottom - - finalize: (fn) -> - @content.finalize(@document.compress, fn) - + + write: (chunk) -> + @content.write chunk + + end: -> + @dictionary.end() + @resources.end() + @content.end() + DEFAULT_MARGINS = top: 72 left: 72 @@ -117,5 +120,5 @@ class PDFPage LEGAL: [612.00, 1008.00] LETTER: [612.00, 792.00] TABLOID: [792.00, 1224.00] - + module.exports = PDFPage \ No newline at end of file diff --git a/lib/reference.coffee b/lib/reference.coffee index 05646b9..6fc0dfd 100644 --- a/lib/reference.coffee +++ b/lib/reference.coffee @@ -4,57 +4,67 @@ By Devon Govett ### zlib = require 'zlib' -setImmediate = global.setImmediate ? process.nextTick # backfill for node < 0.10 class PDFReference - constructor: (@id, @data = {}) -> + constructor: (@document, @id, @data = {}) -> @gen = 0 - @stream = null - @finalizedStream = null + @deflate = null + @compress = @document.compress and not @data.Filter + @uncompressedLength = 0 + @chunks = [] - object: (compress, fn) -> - unless @finalizedStream? - return @finalize compress, => @object compress, fn + initDeflate: -> + @data.Filter = 'FlateDecode' - out = ["#{@id} #{@gen} obj"] - out.push PDFObject.convert(@data) + @deflate = zlib.createDeflate() + @deflate.on 'data', (chunk) => + @chunks.push chunk + @data.Length += chunk.length + + @deflate.on 'end', @finalize + + write: (chunk) -> + unless Buffer.isBuffer(chunk) + chunk = new Buffer(chunk + '\n', 'binary') + + @uncompressedLength += chunk.length + @data.Length ?= 0 - if @stream - out.push "stream" - out.push @finalizedStream - out.push "endstream" - - out.push "endobj" - fn out.join '\n' - - add: (s) -> - @stream ?= [] - @stream.push if Buffer.isBuffer(s) then s.toString('binary') else s - - finalize: (compress = false, fn) -> - # cache the finalized stream - if @stream - data = @stream.join '\n' - 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]) - 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 - setImmediate fn + if @compress + @initDeflate() if not @deflate + @deflate.write chunk else - @finalizedStream = '' - setImmediate fn + @chunks.push chunk + @data.Length += chunk.length + + end: (chunk) -> + if typeof chunk is 'string' or Buffer.isBuffer(chunk) + @write chunk + + if @deflate + @deflate.end() + else + @finalize() + + finalize: => + @offset = @document._offset + + @document._write "#{@id} #{@gen} obj" + @document._write PDFObject.convert(@data) + + if @chunks.length + @document._write 'stream' + for chunk in @chunks + @document._write chunk + + @chunks.length = 0 # free up memory + @document._write '\nendstream' + + @document._write 'endobj' + @document._refEnd() toString: -> - "#{@id} #{@gen} R" - + return "#{@id} #{@gen} R" + module.exports = PDFReference PDFObject = require './object' diff --git a/lib/store.coffee b/lib/store.coffee deleted file mode 100644 index e840bda..0000000 --- a/lib/store.coffee +++ /dev/null @@ -1,34 +0,0 @@ -### -PDFObjectStore - stores the object heirarchy for the PDF document -By Devon Govett -### - -PDFReference = require './reference' - -class PDFObjectStore - constructor: -> - @objects = {} - @length = 0 - - @root = @ref - Type: 'Catalog' - Pages: @ref - Type: 'Pages' - Count: 0 - Kids: [] - - @pages = @root.data['Pages'] - - ref: (data) -> - @push ++@length, data - - push: (id, data) -> - ref = new PDFReference(id, data) - @objects[id] = ref - return ref - - addPage: (page) -> - @pages.data['Kids'].push(page.dictionary) - @pages.data['Count']++ - -module.exports = PDFObjectStore \ No newline at end of file