Skip to content

Commit a6d7af4

Browse files
committed
Initial support for outlines (table of contents)
1 parent 970f0fd commit a6d7af4

3 files changed

Lines changed: 269 additions & 1 deletion

File tree

jspdf.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,17 @@ var jsPDF = (function(global) {
229229
out(objectNumber + ' 0 obj');
230230
return objectNumber;
231231
},
232+
// Does not output the object. The caller must call newObjectDeferredBegin(oid) before outputing any data
233+
newObjectDeferred = function() {
234+
objectNumber++;
235+
offsets[objectNumber] = function(){
236+
return content_length;
237+
};
238+
return objectNumber;
239+
},
240+
newObjectDeferredBegin = function(oid) {
241+
offsets[oid] = content_length;
242+
},
232243
putStream = function(str) {
233244
out('stream');
234245
out(str);
@@ -770,7 +781,12 @@ var jsPDF = (function(global) {
770781
out('0 ' + (objectNumber + 1));
771782
out(p+' 65535 f ');
772783
for (i = 1; i <= objectNumber; i++) {
773-
out((p + offsets[i]).slice(-10) + ' 00000 n ');
784+
var offset = offsets[i];
785+
if (typeof offset === 'function'){
786+
out((p + offsets[i]()).slice(-10) + ' 00000 n ');
787+
}else{
788+
out((p + offsets[i]).slice(-10) + ' 00000 n ');
789+
}
774790
}
775791
// Trailer
776792
out('trailer');
@@ -922,6 +938,8 @@ var jsPDF = (function(global) {
922938
},
923939
'collections' : {},
924940
'newObject' : newObject,
941+
'newObjectDeferred' : newObjectDeferred,
942+
'newObjectDeferredBegin' : newObjectDeferredBegin,
925943
'putStream' : putStream,
926944
'events' : events,
927945
// ratio that you use in multiplication of a given "size" number to arrive to 'point'

jspdf.plugin.outline.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* jsPDF Outline PlugIn
3+
* Copyright (c) 2014 Steven Spungin (TwelveTone LLC) steven@twelvetone.tv
4+
*
5+
* Licensed under the MIT License.
6+
* http://opensource.org/licenses/mit-license
7+
*/
8+
9+
/**
10+
* Generates a PDF Outline
11+
*/
12+
;
13+
(function(jsPDFAPI) {
14+
'use strict';
15+
16+
jsPDFAPI.initOutlinePlugin = function() {
17+
18+
this.outline = {};
19+
this.outline.root = {
20+
children : []
21+
};
22+
23+
this.internal.events.subscribe('postPutResources', function() {
24+
if (this.outline.root.children.length > 0) {
25+
var lines = this.outline.render().split(/\r\n/);
26+
for (var int = 0; int < lines.length; int++) {
27+
var line = lines[int];
28+
if (line.endsWith('obj')){
29+
var oid = line.split(' ')[0];
30+
this.internal.newObjectDeferredBegin(oid);
31+
}
32+
this.internal.write(line);
33+
}
34+
}
35+
});
36+
37+
this.internal.events.subscribe('putCatalog', function() {
38+
if (this.outline.root.children.length > 0) {
39+
this.internal.write("/Outlines", this.outline.makeRef(this.outline.root));
40+
}
41+
});
42+
43+
/**
44+
* Options: pageNumber
45+
*/
46+
this.outline.add = function(parent,title,options) {
47+
var item = {
48+
title : title,
49+
options : options,
50+
children : []
51+
};
52+
if (parent == null) {
53+
parent = this.root;
54+
}
55+
parent.children.push(item);
56+
return item;
57+
}
58+
59+
this.outline.render = function() {
60+
this.outline.ctx = {};
61+
this.outline.nextId = 1000;
62+
this.outline.ctx.val = '';
63+
this.outline.ctx.pdf = this;
64+
65+
this.outline.genIds_r(this.outline.root);
66+
this.outline.renderRoot(this.outline.root);
67+
this.outline.renderItems(this.outline.root);
68+
69+
return this.outline.ctx.val;
70+
}.bind(this);
71+
72+
this.outline.genIds_r = function(node) {
73+
node.id = this.internal.newObjectDeferred();
74+
for (var i = 0; i < node.children.length; i++) {
75+
this.outline.genIds_r(node.children[i]);
76+
}
77+
}.bind(this);
78+
79+
this.outline.renderRoot = function(node) {
80+
this.objStart(node);
81+
this.line('/Type /Outlines');
82+
if (node.children.length > 0) {
83+
this.line('/First ' + this.makeRef(node.children[0]));
84+
this.line('/Last ' + this.makeRef(node.children[node.children.length - 1]));
85+
}
86+
this.line('/Count ' + this.count_r({
87+
count : 0
88+
}, node));
89+
this.objEnd();
90+
}
91+
92+
this.outline.renderItems = function(node) {
93+
for (var i = 0; i < node.children.length; i++) {
94+
var item = node.children[i];
95+
this.objStart(item);
96+
97+
this.line('/Title ' + this.makeString(item.title));
98+
99+
this.line('/Parent ' + this.makeRef(node));
100+
if (i > 0) {
101+
this.line('/Prev ' + this.makeRef(node.children[i - 1]));
102+
}
103+
if (i < node.children.length - 1) {
104+
this.line('/Next ' + this.makeRef(node.children[i + 1]));
105+
}
106+
if (item.children.length > 0) {
107+
this.line('/First ' + this.makeRef(item.children[0]));
108+
this.line('/Last ' + this.makeRef(item.children[item.children.length - 1]));
109+
}
110+
111+
var count = this.count = this.count_r({
112+
count : 0
113+
}, item);
114+
if (count > 0) {
115+
this.line('/Count ' + count);
116+
}
117+
118+
if (item.options) {
119+
if (item.options.pageNumber) {
120+
this.line('/Dest ' + '[' + (item.options.pageNumber - 1) + ' /XYZ 0 ' + this.ctx.pdf.internal.pageSize.height + ' 0]');
121+
//this.line('/Dest ' + '[' + (item.options.pageNumber - 1) + ' /Fit]');
122+
}
123+
}
124+
this.objEnd();
125+
}
126+
for (var i = 0; i < node.children.length; i++) {
127+
var item = node.children[i];
128+
this.renderItems(item);
129+
}
130+
}
131+
132+
this.outline.line = function(text) {
133+
this.ctx.val += text + '\r\n';
134+
}
135+
136+
this.outline.makeRef = function(node) {
137+
return node.id + ' 0 R';
138+
}
139+
140+
this.outline.makeString = function(val) {
141+
return '(' + this.internal.pdfEscape(val) + ')';
142+
}.bind(this);
143+
144+
this.outline.objStart = function(node) {
145+
this.ctx.val += '\r\n' + node.id + ' 0 obj' + '\r\n<<\r\n';
146+
}
147+
148+
this.outline.objEnd = function(node) {
149+
this.ctx.val += '>> \r\n' + 'endobj' + '\r\n';
150+
}
151+
152+
this.outline.count_r = function(ctx,node) {
153+
for (var i = 0; i < node.children.length; i++) {
154+
ctx.count++;
155+
this.count_r(ctx, node.children[i]);
156+
}
157+
return ctx.count;
158+
}
159+
}
160+
161+
return this;
162+
})(jsPDF.API);

test/test_outline.html

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<!doctype html>
2+
<!--
3+
/**
4+
* jsPDF Outline PlugIn
5+
* Copyright (c) 2014 Steven Spungin (TwelveTone LLC) steven@twelvetone.tv
6+
*
7+
* Licensed under the MIT License.
8+
* http://opensource.org/licenses/mit-license
9+
*/
10+
-->
11+
12+
<html>
13+
<head>
14+
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
15+
16+
<title>Outline Test</title>
17+
18+
<script type="text/javascript" src="../jspdf.js"></script>
19+
<!-- the plugin to test -->
20+
<script type="text/javascript" src="../jspdf.plugin.outline.js"></script>
21+
22+
<script>
23+
window.onload = function() {
24+
25+
var pdf = new jsPDF('p', 'pt', 'letter');
26+
pdf.text(20, 20, 'Hello');
27+
pdf.addPage();
28+
pdf.text(20, 20, 'PDF');
29+
pdf.addPage();
30+
pdf.text(20, 20, 'World');
31+
32+
pdf.addPage();
33+
pdf.text(20, 20, 'More');
34+
35+
pdf.initOutlinePlugin();
36+
var node = pdf.outline.add(null, 'Test Pages', null);
37+
pdf.outline.add(node, 'Hello', {pageNumber:1});
38+
pdf.outline.add(node, 'PDF', {pageNumber:2});
39+
pdf.outline.add(node, 'World', {pageNumber:3});
40+
41+
var node = pdf.outline.add(null, 'More Pages', null);
42+
pdf.outline.add(node, 'More', {pageNumber:4});
43+
44+
// generate either the pdf or source code
45+
if (getParameterByName('src') != 'true'){
46+
var frame = document.getElementById('pdfframe');
47+
frame.src = pdf.output('datauristring');
48+
}
49+
else{
50+
var src = pdf.outline.render();
51+
var src = pdf.output();
52+
var content = document.getElementById('sourcecode');
53+
raw = escapeHtml(src);
54+
content.innerHTML = raw;
55+
}
56+
57+
}
58+
59+
var entityMap = {
60+
"&": "&amp;",
61+
"<": "&lt;",
62+
">": "&gt;",
63+
'"': '&quot;',
64+
"'": '&#39;',
65+
"/": '&#x2F;'
66+
};
67+
68+
function escapeHtml(string) {
69+
return String(string).replace(/[&<>"'\/]/g, function (s) {
70+
return entityMap[s];
71+
});
72+
}
73+
74+
function getParameterByName(name) {
75+
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
76+
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
77+
results = regex.exec(location.search);
78+
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
79+
}
80+
</script>
81+
82+
</head>
83+
84+
<body style='background-color: silver; margin: 0;'>
85+
<pre id='sourcecode'></pre>
86+
<iframe id='pdfframe' style='width: 100%; height: 100%; position: absolute'></iframe>
87+
</body>
88+
</html>

0 commit comments

Comments
 (0)