diff --git a/demo/out_outlines.pdf b/demo/out_outlines.pdf new file mode 100644 index 000000000..367b5a4b0 Binary files /dev/null and b/demo/out_outlines.pdf differ diff --git a/demo/test_outlines.coffee b/demo/test_outlines.coffee new file mode 100644 index 000000000..95fb7c9a5 --- /dev/null +++ b/demo/test_outlines.coffee @@ -0,0 +1,82 @@ +PDFDocument = require 'pdfkit' +fs = require 'fs' + +docOptions = { + hasOutlines: true, # required for outlines + margins: { top : 40, left : 40, bottom : 40, right : 30 } } + + +# Create a new PDFDocument +doc = new PDFDocument(docOptions) +doc.pipe fs.createWriteStream('out_outlines.pdf') + +doc.fontSize(16) +title = 'Test Document With Outlines' + +# add outline on current level (0), with outline options: +# 'FitH' - top coordinate is positioned at the top edge of the window, +# document fits the entire width. +# Can be any type according to PDF 1.3 specification, 7.2.1 Destinations (Table 7.2) +doc.addOutline( title, doc.page, { type: 'FitH', top : 0 } ) + +# text to point by outline should be added next +doc.text(title, { paragraphGap : 8, align : 'center' } ) + +doc.fontSize(12) # usual text + +doc.info['Title'] = title +doc.info['Author'] = 'Alexey Voytenko' + +loremIpsum_0_1 = 'LOREM IPSUM DOLOR. + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam in suscipit purus. + Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; + Vivamus nec hendrerit felis. Morbi aliquam facilisis risus eu lacinia. Sed eu leo in turpis fringilla hendrerit. Ut nec accumsan nisl. + Suspendisse rhoncus nisl posuere tortor tempus et dapibus elit porta.' + +loremIpsum_1_1 = 'CRAS LEO NEQUE. + Cras leo neque, elementum a rhoncus ut, vestibulum non nibh. Phasellus pretium justo turpis. + Etiam vulputate, odio vitae tincidunt ultricies, eros odio dapibus nisi, + ut tincidunt lacus arcu eu elit.' + +loremIpsum_1_2 = 'AENEAN VELIT ERAT. +Aenean velit erat, vehicula eget lacinia ut, dignissim non tellus. +Aliquam nec lacus mi, sed vestibulum nunc. Suspendisse potenti. Curabitur vitae sem turpis. +Vestibulum sed neque eget dolor dapibus porttitor at sit amet sem. Fusce a turpis lorem. +Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; +Mauris at ante tellus. Vestibulum a metus lectus. Praesent tempor purus a lacus blandit eget +gravida ante hendrerit.' + +loremIpsum_0_2 = 'CRAS ET EROS METUS. +Cras et eros metus. Sed commodo malesuada eros, vitae interdum augue semper quis. +Fusce id magna nunc. Curabitur sollicitudin placerat semper. Cras et mi neque, a dignissim risus. +Nulla venenatis porta lacus, vel rhoncus lectus tempor vitae. Duis sagittis venenatis rutrum. +Curabitur tempor massa tortor.' + +# add outline on current level (0) with default options - 'XYZ', positioning on text +doc.addOutline('Lorem ipsum dolor', doc.page); + +doc.text( loremIpsum_0_1, + { paragraphGap : 2, indent : 30, align : 'justify' } ) + +# create next level (1) of table of contents and add outline +doc.addSublevelOutline('Cras leo neque', doc.page); +doc.text( loremIpsum_1_1, { paragraphGap : 2, indent : 30, align : 'justify' } ); + +# add outline on the same level 1 with positioning on text +doc.addOutline('Aenean velit erat', doc.page ); + +doc.text( loremIpsum_1_2, { paragraphGap : 2, indent : 30, align : 'justify' } ); + +# go up in outlines levels +doc.endOutlineSublevel(); + +outl_options = { type: 'XYZ', unicode: true } + +# add outline on the level 0 with positioning on text ('XYZ' is by default), +# using non-ASCII characters in outline title: set unicode in this case +doc.addOutline('Тестовый заголовок', doc.page, outl_options); + +doc.text( loremIpsum_0_2, { paragraphGap : 2, indent : 30, align : 'justify' } ); + +doc.end() +console.log "out_outlines.pdf created" diff --git a/lib/document.coffee b/lib/document.coffee index c17979c44..bf5696cb9 100644 --- a/lib/document.coffee +++ b/lib/document.coffee @@ -8,6 +8,7 @@ fs = require 'fs' PDFObject = require './object' PDFReference = require './reference' PDFPage = require './page' +PDFOutline = require './outline' class PDFDocument extends stream.Readable constructor: (@options = {}) -> @@ -31,9 +32,24 @@ class PDFDocument extends stream.Readable Type: 'Pages' Count: 0 Kids: [] + + if @options.hasOutlines + @_root.data['PageMode'] = 'UseOutlines' + @_root.data['Outlines'] = @ref + Type: 'Outlines' + Count: 0 + + @root_outlines = @_root.data['Outlines'] # The current page @page = null + + # Outlines data structures + @outlines = [] # a multitier tree, represented by lists ( [] ) + @cur = @outlines # current level list + @levelHead = @root_outlines # head of current level list + @levs = [] # stack of sublevel lists heads + @prevs = [] # stack of sublevel lists # Initialize mixins @initColor() @@ -97,10 +113,100 @@ class PDFDocument extends stream.Readable @transform 1, 0, 0, -1, 0, @page.height return this + + isRoot: () -> + return @prevs.length == 0 + + addOutline: (title, dest, options) -> + + if not @options.hasOutlines + return this + + prevNode = null + if @isRoot() + parent = @root_outlines + else + parent = @levelHead.dictionary + + if @cur.length > 0 # can be on Root level + prevNode = @cur[@cur.length-1] + if prevNode instanceof Array # prev is a sublevel list + prevNode = @cur[@cur.length-2] # there *must* be an element before + + # create an outline object + outline = new PDFOutline(this, parent, title, dest, options) + + # update list header info + if @outlines.length == 0 # init root level list + @root_outlines.data['First'] = outline.dictionary + + parent.data['Last'] = outline.dictionary # can be root level + parent.data['Count']++ + + if prevNode != null # not first in the list + outline.dictionary.data['Prev'] = prevNode.dictionary + prevNode.dictionary.data['Next'] = outline.dictionary + + # add to the current level list + @cur.push outline + + return this + + addSublevelOutline: (title, dest, options) -> + + if not @options.hasOutlines + return this + + if @cur.length == 0 or ( @cur[@cur.length-1] instanceof Array ) + throw new Error ' + Cannot start sublevel with empty current level: + add current level outline first.' + + @levs.push @levelHead + @levelHead = @cur[@cur.length-1] # head of sublevel list + @prevs.push @cur # remember up level container + @cur.push [] # add sublevel container + @cur = @cur[@cur.length-1] + + outline = new PDFOutline(this, @levelHead.dictionary, title, dest, options) + @cur.push outline + @levelHead.dictionary.data['First'] = outline.dictionary + @levelHead.dictionary.data['Last'] = outline.dictionary + @levelHead.dictionary.data['Count'] = 1 # important: not ++ (field is absent now) + + return this + + endOutlineSublevel: () -> + + if not @options.hasOutlines + return this + + if @isRoot() + throw new Error 'cannot end root outlines level' + + # restore previous list as current + @cur = @prevs.pop() + count = @levelHead.dictionary.data['Count'] + @levelHead = @levs.pop() + + # add child sublist counter (will contain all child counters) + if @isRoot() + @levelHead.data['Count'] += count + else + @levelHead.dictionary.data['Count'] += count + + return this + + endOutlines: (outlines) -> + for outline in outlines + if outline instanceof Array + @endOutlines(outline) + else + outline.dictionary.end() ref: (data) -> ref = new PDFReference(this, @_offsets.length + 1, data) - @_offsets.push null # placeholder for this object's offset once it is finalized + @_offsets.push null # placeholder for this object''s offset once it is finalized @_waiting++ return ref @@ -161,6 +267,8 @@ class PDFDocument extends stream.Readable @_root.end() @_root.data.Pages.end() + @_root.data.Outlines.end() + @endOutlines(@outlines) if @_waiting is 0 @_finalize() diff --git a/lib/outline.coffee b/lib/outline.coffee new file mode 100644 index 000000000..4a924f2d9 --- /dev/null +++ b/lib/outline.coffee @@ -0,0 +1,54 @@ +### +PDFOutline - represents a table of contents in the PDF 1.3 document +By Alexey Voytenko +see also methods addOutline, addSublevelOutline, endOutlineSublevel +of PDFDocument class +### + +PDFObject = require './object' +PDFPage = require './page' + +class PDFOutline + constructor: ( @document, parent, @title, @dest, @options = {} ) -> + + @type = @options.type or 'XYZ' + @unicode = @options.unicode or false + + # to convert library y coordinate to requred user space coordinate + height = @document.page.height + + # see PDFReference 1.3, 7.2.1 Destinations + switch @type + when 'Fit', 'FitB' + destination = [@dest.dictionary, @type] + + when 'FitH', 'FitBH' + top = height - ( @options.top or 0 ) + destination = [@dest.dictionary, @type, top] + + when 'FitV', 'FitBV' + top = height - ( @options.left or 0 ) + destination = [@dest.dictionary, @type, left] + + when 'FitR' + left = @options.left or 0 + bottom = height - ( @options.bottom or 0 ) + right = @options.right or 0 + top = height - ( @options.top or 0 ) + destination = [@dest.dictionary, @type, left, bottom, right, top] + + else # assuming 'XYZ' - most common + # XY - coordinates of page top left in user space system, + # Z - zoom factor (null for X,Y,Z - previous value unchanged) + X = @options.X or 0 + Y = height - ( @options.Y or @document.y ) + Z = @options.Z or null + destination = [@dest.dictionary, @type, X, Y, Z] + + # The outline dictionary + @dictionary = @document.ref + Title: PDFObject.s @title, @unicode # need to specify if unicode + Parent: parent # dictionary + Dest: destination + +module.exports = PDFOutline diff --git a/lib/page.coffee b/lib/page.coffee index 2584b3ba7..fbe6be57d 100644 --- a/lib/page.coffee +++ b/lib/page.coffee @@ -121,4 +121,4 @@ class PDFPage LETTER: [612.00, 792.00] TABLOID: [792.00, 1224.00] -module.exports = PDFPage \ No newline at end of file +module.exports = PDFPage