-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Outlines: multilevel table of contents. #216
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
Changes from all commits
e2c9029
6d8b94d
0c1842c
2af94a5
1de1590
d54e84e
3410835
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) -> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The destination should always be the current page. Is there anything else it could be? If not, then why make the user pass the page everywhere themselves?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Destination is always set relatively to the page. Theoretically it can be any page, but in practice I agree - only current page is useful. Let's omit this parameter. |
||
|
|
||
| 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() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of the stuff you added to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you decide to implement the alternative API I described in the PR, then this isn't necessary since that would be a separate object anyway. |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure you meant |
||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should always be unicode, no need for an option. That's more of an implementation detail.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By default it is not unicode in 's'. We can make unicode default value in PDFOutline class, agree. |
||
| Parent: parent # dictionary | ||
| Dest: destination | ||
|
|
||
| module.exports = PDFOutline | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of an option, can these extra pieces of data be added automatically later once the first outline has been added?