Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table support #394

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 41 additions & 40 deletions lib/document.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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'
Expand All @@ -54,30 +54,31 @@ 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'
mixin require './mixins/fonts'
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
Expand All @@ -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: ->
Expand All @@ -119,103 +120,103 @@ class PDFDocument extends stream.Readable
@_pageBufferStart += pages.length
for page in pages
page.end()

return

ref: (data) ->
ref = new PDFReference(this, @_offsets.length + 1, data)
@_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
143 changes: 143 additions & 0 deletions lib/mixins/tables.coffee
Original file line number Diff line number Diff line change
@@ -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)