diff --git a/lib/document.coffee b/lib/document.coffee index b9dff020a..6b29eefd7 100644 --- a/lib/document.coffee +++ b/lib/document.coffee @@ -12,13 +12,13 @@ PDFPage = require './page' class PDFDocument extends stream.Readable constructor: (@options = {}) -> super - + # PDF version @version = 1.3 - + # Whether streams should be compressed @compress = @options.compress ? yes - + @_pageBuffer = [] @_pageBufferStart = 0 @@ -27,24 +27,24 @@ class PDFDocument extends stream.Readable @_waiting = 0 @_ended = false @_offset = 0 - + @_root = @ref Type: 'Catalog' Pages: @ref Type: 'Pages' Count: 0 Kids: [] - + # The current page @page = null - + # Initialize mixins @initColor() @initVector() @initFonts() @initText() @initImages() - + # Initialize the metadata @info = Producer: 'PDFKit' @@ -54,22 +54,22 @@ class PDFDocument extends stream.Readable if @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 if @options.autoFirstPage isnt false @addPage() - + mixin = (methods) => for name, method of methods this::[name] = method - + # Load mixins mixin require './mixins/color' mixin require './mixins/vector' @@ -77,7 +77,8 @@ class PDFDocument extends stream.Readable mixin require './mixins/text' mixin require './mixins/images' mixin require './mixins/annotations' - + mixin require './mixins/tables' + addPage: (options = @options) -> # end the current page if needed @flushPages() unless @options.bufferPages @@ -90,25 +91,25 @@ class PDFDocument extends stream.Readable pages = @_root.data.Pages.data pages.Kids.push @page.dictionary pages.Count++ - + # reset x and y coordinates @x = @page.margins.left @y = @page.margins.top - + # flip PDF coordinate system so that the origin is in # the top left rather than the bottom left @_ctm = [1, 0, 0, 1, 0, 0] @transform 1, 0, 0, -1, 0, @page.height - + return this - bufferedPageRange: -> + bufferedPageRange: -> return { start: @_pageBufferStart, count: @_pageBuffer.length } switchToPage: (n) -> unless page = @_pageBuffer[n - @_pageBufferStart] throw new Error "switchToPage(#{n}) out of bounds, current buffer covers pages #{@_pageBufferStart} to #{@_pageBufferStart + @_pageBuffer.length - 1}" - + @page = page flushPages: -> @@ -119,7 +120,7 @@ class PDFDocument extends stream.Readable @_pageBufferStart += pages.length for page in pages page.end() - + return ref: (data) -> @@ -127,95 +128,95 @@ class PDFDocument extends stream.Readable @_offsets.push null # placeholder for this object's offset once it is finalized @_waiting++ return ref - + _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: (ref) -> @_offsets[ref.id - 1] = ref.offset if --@_waiting is 0 and @_ended @_finalize() @_ended = false - + write: (filename, 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) -> # 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. ' - + end: -> @flushPages() @_info = @ref() for key, val of @info if typeof val is 'string' val = new String val - + @_info.data[key] = val - + @_info.end() - + for name, font of @_fontFamilies font.embed() - + @_root.end() @_root.data.Pages.end() - + if @_waiting is 0 @_finalize() else @_ended = true - - _finalize: (fn) -> + + _finalize: (fn) -> # generate xref xRefOffset = @_offset @_write "xref" @_write "0 #{@_offsets.length + 1}" @_write "0000000000 65535 f " - + for offset in @_offsets offset = ('0000000000' + offset).slice(-10) @_write offset + ' 00000 n ' - + # trailer @_write 'trailer' @_write PDFObject.convert Size: @_offsets.length + 1 Root: @_root Info: @_info - + @_write 'startxref' @_write "#{xRefOffset}" @_write '%%EOF' # end the stream @push null - + toString: -> "[object PDFDocument]" - + module.exports = PDFDocument diff --git a/lib/mixins/tables.coffee b/lib/mixins/tables.coffee new file mode 100644 index 000000000..33a60ff86 --- /dev/null +++ b/lib/mixins/tables.coffee @@ -0,0 +1,143 @@ +#TODO check if the options are correct +#TODO bottom padding is heigher than the padding specified in the options +#TODO use underscore for private functions +#TODO generalize the font of data and header and provide the possibility to pass them throw the options +#TODO update the y position after all table is rendered + +LineWrapper = require '../line_wrapper' + +module.exports = + initTables: -> + console.log 'init tables' + + table: (data, options) -> + @tableOptions = options + @rowY = @tableOptions.y or @page.margins.top + @tableOptions.margins.top + @printHeaderRow() + @printRow rowIndex, row for row, rowIndex in data + @y += @tableOptions.padding.bottom + @tableOptions.margins.bottom + @x = @tableOptions.margins.left + +# print a row + printRow: (rowIndex, row) -> + @rowHeight = @getRowHeight(row) + + if @rowY + @rowHeight > @page.height - @page.margins.bottom + @addPage() + @rowY = @page.margins.top + @tableOptions.margins.top + @printHeaderRow() + @rowHeight = @getRowHeight(row) + + # print the data in each column + for col, colIndex in @tableOptions.columns + @printCol rowIndex, row, colIndex, col + + # print the borer of the row + # @moveTo(@tableOptions.margins.left, @rowY) + # .lineTo(@tableOptions.margins.left + @getWidth(), @rowY) + # .stroke() + @moveTo(@tableOptions.margins.left + @getWidth(), @rowY + @rowHeight) + .lineTo(@tableOptions.margins.left, @rowY + @rowHeight) + .stroke() + + if @needsVerticalLines() + for col, colIndex in @tableOptions.columns + # print the line that divide each column + @moveTo(@getXOfColumn(colIndex), @rowY) + .lineTo(@getXOfColumn(colIndex), @rowY + @rowHeight) + .stroke() + + @moveTo(@tableOptions.margins.left + @getWidth(), @rowY) + .lineTo(@tableOptions.margins.left + @getWidth(), @rowY + @rowHeight) + .stroke() + + @rowY += @rowHeight + +# print a column + printCol: (rowIndex, row, colIndex, col) -> + colOpt = @tableOptions.columns[colIndex]; + text = row[col.id] or '' + text = colOpt.renderer(text) if colOpt.renderer + @font(@tableOptions.font) + .text( + text, + @getXOfColumn(colIndex) + @tableOptions.padding.left, + @rowY + @tableOptions.padding.top, + width: @getColWidth(colIndex) + ) + +# print a row + printHeaderRow: -> + @rowHeight = 30 + + # print the data in each column + for col, colIndex in @tableOptions.columns + @printHeaderCol colIndex + + # print the borer of the row + @moveTo(@tableOptions.margins.left, @rowY) + .lineTo(@tableOptions.margins.left + @getWidth(), @rowY) + .lineTo(@tableOptions.margins.left + @getWidth(), @rowY+1) + .lineTo(@tableOptions.margins.left, @rowY+1) + .stroke() + @moveTo(@tableOptions.margins.left + @getWidth(), @rowY + @rowHeight) + .lineTo(@tableOptions.margins.left, @rowY + @rowHeight) + .lineTo(@tableOptions.margins.left + @getWidth(), @rowY + @rowHeight + 1) + .lineTo(@tableOptions.margins.left, @rowY + @rowHeight + 1) + .stroke() + + if @needsVerticalLines() + for col, colIndex in @tableOptions.columns + # print the line that divide each column + @moveTo(@getXOfColumn(colIndex), @rowY) + .lineTo(@getXOfColumn(colIndex), @rowY + @rowHeight) + .stroke() + + @moveTo(@tableOptions.margins.left + @getWidth(), @rowY) + .lineTo(@tableOptions.margins.left + @getWidth(), @rowY + @rowHeight) + .stroke() + + @rowY += @rowHeight + +# print an header column + printHeaderCol: (colIndex) -> + @font(@tableOptions.boldFont) + .text( + @tableOptions.columns[colIndex].name or '', + @getXOfColumn(colIndex) + @tableOptions.padding.left, + @rowY + @tableOptions.padding.top, + width: @getColWidth(colIndex) + ) + + getColWidth : (colIndex) -> + @getXOfColumn(colIndex+1) - @getXOfColumn(colIndex) - @tableOptions.padding.left + + getRowHeight: (row) -> + maxHeight = 0; + + for col, colIndex in @tableOptions.columns + height = 0 + line = () -> height += @currentLineHeight(true) + height = height += @currentLineHeight(true) + wrapper = new LineWrapper(this, {}) + wrapper.on 'line', line.bind(this) + wrapper.wrap(row[col.id]+'' or '', { width: @getColWidth(colIndex) }) + if height > maxHeight + maxHeight = height + + return maxHeight + @tableOptions.padding.bottom + 8 # TODO use font height instead of 8 + +# get the x position of a column + getXOfColumn: (colIndex) -> + perc = 0 + for col, i in @tableOptions.columns when i < colIndex + perc += col.width + + return @tableOptions.margins.left + (@getWidth() * perc / 100) + + needsVerticalLines: -> + not @tableOptions.noVerticalLines or @tableOptions.noVerticalLines is false + +# return the width of the table + getWidth: -> + @page.width - (@tableOptions.margins.left + @tableOptions.margins.right)