| 9 |
lars |
1 |
/** @preserve
|
|
|
2 |
jsPDF fromHTML plugin. BETA stage. API subject to change. Needs browser, jQuery
|
|
|
3 |
Copyright (c) 2012 2012 Willow Systems Corporation, willow-systems.com
|
|
|
4 |
*/
|
|
|
5 |
/*
|
|
|
6 |
* Permission is hereby granted, free of charge, to any person obtaining
|
|
|
7 |
* a copy of this software and associated documentation files (the
|
|
|
8 |
* "Software"), to deal in the Software without restriction, including
|
|
|
9 |
* without limitation the rights to use, copy, modify, merge, publish,
|
|
|
10 |
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
|
11 |
* permit persons to whom the Software is furnished to do so, subject to
|
|
|
12 |
* the following conditions:
|
|
|
13 |
*
|
|
|
14 |
* The above copyright notice and this permission notice shall be
|
|
|
15 |
* included in all copies or substantial portions of the Software.
|
|
|
16 |
*
|
|
|
17 |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
|
18 |
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
|
19 |
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
|
20 |
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
|
21 |
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
|
22 |
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
|
23 |
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
24 |
* ====================================================================
|
|
|
25 |
*/
|
|
|
26 |
|
|
|
27 |
;(function(jsPDFAPI) {
|
|
|
28 |
'use strict'
|
|
|
29 |
|
|
|
30 |
|
|
|
31 |
if(!String.prototype.trim) {
|
|
|
32 |
String.prototype.trim = function () {
|
|
|
33 |
return this.replace(/^\s+|\s+$/g,'');
|
|
|
34 |
};
|
|
|
35 |
}
|
|
|
36 |
if(!String.prototype.trimLeft) {
|
|
|
37 |
String.prototype.trimLeft = function () {
|
|
|
38 |
return this.replace(/^\s+/g,'');
|
|
|
39 |
};
|
|
|
40 |
}
|
|
|
41 |
if(!String.prototype.trimRight) {
|
|
|
42 |
String.prototype.trimRight = function () {
|
|
|
43 |
return this.replace(/\s+$/g,'');
|
|
|
44 |
};
|
|
|
45 |
}
|
|
|
46 |
|
|
|
47 |
function PurgeWhiteSpace(array){
|
|
|
48 |
var i = 0, l = array.length, fragment
|
|
|
49 |
, lTrimmed = false
|
|
|
50 |
, rTrimmed = false
|
|
|
51 |
|
|
|
52 |
while (!lTrimmed && i !== l) {
|
|
|
53 |
fragment = array[i] = array[i].trimLeft()
|
|
|
54 |
if (fragment) {
|
|
|
55 |
// there is something left there.
|
|
|
56 |
lTrimmed = true
|
|
|
57 |
}
|
|
|
58 |
;i++;
|
|
|
59 |
}
|
|
|
60 |
|
|
|
61 |
i = l - 1
|
|
|
62 |
while (l && !rTrimmed && i !== -1) {
|
|
|
63 |
fragment = array[i] = array[i].trimRight()
|
|
|
64 |
if (fragment) {
|
|
|
65 |
// there is something left there.
|
|
|
66 |
rTrimmed = true
|
|
|
67 |
}
|
|
|
68 |
;i--;
|
|
|
69 |
}
|
|
|
70 |
|
|
|
71 |
var r = /\s+$/g
|
|
|
72 |
, trailingSpace = true // it's safe to assume we always trim start of display:block element's text.
|
|
|
73 |
|
|
|
74 |
for (i = 0; i !== l; i++) {
|
|
|
75 |
fragment = array[i].replace(/\s+/g, ' ')
|
|
|
76 |
// if (l > 1) {
|
|
|
77 |
// console.log(i, trailingSpace, fragment)
|
|
|
78 |
// }
|
|
|
79 |
if (trailingSpace) {
|
|
|
80 |
fragment = fragment.trimLeft()
|
|
|
81 |
}
|
|
|
82 |
if (fragment) {
|
|
|
83 |
// meaning, it was not reduced to ""
|
|
|
84 |
// if it was, we don't want to clear trailingSpace flag.
|
|
|
85 |
trailingSpace = r.test(fragment)
|
|
|
86 |
}
|
|
|
87 |
array[i] = fragment
|
|
|
88 |
}
|
|
|
89 |
|
|
|
90 |
return array
|
|
|
91 |
}
|
|
|
92 |
|
|
|
93 |
function Renderer(pdf, x, y, settings) {
|
|
|
94 |
this.pdf = pdf
|
|
|
95 |
this.x = x
|
|
|
96 |
this.y = y
|
|
|
97 |
this.settings = settings
|
|
|
98 |
|
|
|
99 |
this.init()
|
|
|
100 |
|
|
|
101 |
return this
|
|
|
102 |
}
|
|
|
103 |
|
|
|
104 |
Renderer.prototype.init = function(){
|
|
|
105 |
|
|
|
106 |
this.paragraph = {
|
|
|
107 |
'text': []
|
|
|
108 |
, 'style': []
|
|
|
109 |
}
|
|
|
110 |
|
|
|
111 |
this.pdf.internal.write(
|
|
|
112 |
'q'
|
|
|
113 |
)
|
|
|
114 |
}
|
|
|
115 |
|
|
|
116 |
Renderer.prototype.dispose = function(){
|
|
|
117 |
this.pdf.internal.write(
|
|
|
118 |
'Q' // closes the 'q' in init()
|
|
|
119 |
)
|
|
|
120 |
return {
|
|
|
121 |
'x':this.x, 'y':this.y // bottom left of last line. = upper left of what comes after us.
|
|
|
122 |
// TODO: we cannot traverse pages yet, but need to figure out how to communicate that when we do.
|
|
|
123 |
// TODO: add more stats: number of lines, paragraphs etc.
|
|
|
124 |
}
|
|
|
125 |
}
|
|
|
126 |
|
|
|
127 |
Renderer.prototype.splitFragmentsIntoLines = function(fragments, styles){
|
|
|
128 |
var defaultFontSize = 12 // points
|
|
|
129 |
, k = this.pdf.internal.scaleFactor // when multiplied by this, converts jsPDF instance units into 'points'
|
|
|
130 |
|
|
|
131 |
// var widths = options.widths ? options.widths : this.internal.getFont().metadata.Unicode.widths
|
|
|
132 |
// , kerning = options.kerning ? options.kerning : this.internal.getFont().metadata.Unicode.kerning
|
|
|
133 |
, fontMetricsCache = {}
|
|
|
134 |
, ff, fs
|
|
|
135 |
, fontMetrics
|
|
|
136 |
|
|
|
137 |
, fragment // string, element of `fragments`
|
|
|
138 |
, style // object with properties with names similar to CSS. Holds pertinent style info for given fragment
|
|
|
139 |
, fragmentSpecificMetrics // fontMetrics + some indent and sizing properties populated. We reuse it, hence the bother.
|
|
|
140 |
, fragmentLength // fragment's length in jsPDF units
|
|
|
141 |
, fragmentChopped // will be array - fragment split into "lines"
|
|
|
142 |
|
|
|
143 |
, line = [] // array of pairs of arrays [t,s], where t is text string, and s is style object for that t.
|
|
|
144 |
, lines = [line] // array of arrays of pairs of arrays
|
|
|
145 |
, currentLineLength = 0 // in jsPDF instance units (inches, cm etc)
|
|
|
146 |
, maxLineLength = this.settings.width // need to decide if this is the best way to know width of content.
|
|
|
147 |
|
|
|
148 |
// this loop sorts text fragments (and associated style)
|
|
|
149 |
// into lines. Some fragments will be chopped into smaller
|
|
|
150 |
// fragments to be spread over multiple lines.
|
|
|
151 |
while (fragments.length) {
|
|
|
152 |
|
|
|
153 |
fragment = fragments.shift()
|
|
|
154 |
style = styles.shift()
|
|
|
155 |
|
|
|
156 |
// if not empty string
|
|
|
157 |
if (fragment) {
|
|
|
158 |
|
|
|
159 |
ff = style['font-family']
|
|
|
160 |
fs = style['font-style']
|
|
|
161 |
|
|
|
162 |
fontMetrics = fontMetricsCache[ff+fs]
|
|
|
163 |
if (!fontMetrics) {
|
|
|
164 |
fontMetrics = this.pdf.internal.getFont(ff, fs).metadata.Unicode
|
|
|
165 |
fontMetricsCache[ff+fs] = fontMetrics
|
|
|
166 |
}
|
|
|
167 |
|
|
|
168 |
fragmentSpecificMetrics = {
|
|
|
169 |
'widths': fontMetrics.widths
|
|
|
170 |
, 'kerning': fontMetrics.kerning
|
|
|
171 |
|
|
|
172 |
// fontSize comes to us from CSS scraper as "proportion of normal" value
|
|
|
173 |
// , hence the multiplication
|
|
|
174 |
, 'fontSize': style['font-size'] * defaultFontSize
|
|
|
175 |
|
|
|
176 |
// // these should not matter as we provide the metrics manually
|
|
|
177 |
// // if we would not, we would need these:
|
|
|
178 |
// , 'fontName': style.fontName
|
|
|
179 |
// , 'fontStyle': style.fontStyle
|
|
|
180 |
|
|
|
181 |
// this is setting for "indent first line of paragraph", but we abuse it
|
|
|
182 |
// for continuing inline spans of text. Indent value = space in jsPDF instance units
|
|
|
183 |
// (whatever user passed to 'new jsPDF(orientation, units, size)
|
|
|
184 |
// already consumed on this line. May be zero, of course, for "start of line"
|
|
|
185 |
// it's used only on chopper, ignored in all "sizing" code
|
|
|
186 |
, 'textIndent': currentLineLength
|
|
|
187 |
}
|
|
|
188 |
|
|
|
189 |
// in user units (inch, cm etc.)
|
|
|
190 |
fragmentLength = this.pdf.getStringUnitWidth(
|
|
|
191 |
fragment
|
|
|
192 |
, fragmentSpecificMetrics
|
|
|
193 |
) * fragmentSpecificMetrics.fontSize / k
|
|
|
194 |
|
|
|
195 |
if (currentLineLength + fragmentLength > maxLineLength) {
|
|
|
196 |
// whatever is already on the line + this new fragment
|
|
|
197 |
// will be longer than max len for a line.
|
|
|
198 |
// Hence, chopping fragment into lines:
|
|
|
199 |
fragmentChopped = this.pdf.splitTextToSize(
|
|
|
200 |
fragment
|
|
|
201 |
, maxLineLength
|
|
|
202 |
, fragmentSpecificMetrics
|
|
|
203 |
)
|
|
|
204 |
|
|
|
205 |
line.push([fragmentChopped.shift(), style])
|
|
|
206 |
while (fragmentChopped.length){
|
|
|
207 |
line = [[fragmentChopped.shift(), style]]
|
|
|
208 |
lines.push(line)
|
|
|
209 |
}
|
|
|
210 |
|
|
|
211 |
currentLineLength = this.pdf.getStringUnitWidth(
|
|
|
212 |
// new line's first (and only) fragment's length is our new line length
|
|
|
213 |
line[0][0]
|
|
|
214 |
, fragmentSpecificMetrics
|
|
|
215 |
) * fragmentSpecificMetrics.fontSize / k
|
|
|
216 |
} else {
|
|
|
217 |
// nice, we can fit this fragment on current line. Less work for us...
|
|
|
218 |
line.push([fragment, style])
|
|
|
219 |
currentLineLength += fragmentLength
|
|
|
220 |
}
|
|
|
221 |
}
|
|
|
222 |
}
|
|
|
223 |
|
|
|
224 |
return lines
|
|
|
225 |
}
|
|
|
226 |
|
|
|
227 |
Renderer.prototype.RenderTextFragment = function(text, style) {
|
|
|
228 |
|
|
|
229 |
var defaultFontSize = 12
|
|
|
230 |
// , header = "/F1 16 Tf\n16 TL\n0 g"
|
|
|
231 |
, font = this.pdf.internal.getFont(style['font-family'], style['font-style'])
|
|
|
232 |
|
|
|
233 |
this.pdf.internal.write(
|
|
|
234 |
'/' + font.id // font key
|
|
|
235 |
, (defaultFontSize * style['font-size']).toFixed(2) // font size comes as float = proportion to normal.
|
|
|
236 |
, 'Tf' // font def marker
|
|
|
237 |
, '('+this.pdf.internal.pdfEscape(text)+') Tj'
|
|
|
238 |
)
|
|
|
239 |
}
|
|
|
240 |
|
|
|
241 |
Renderer.prototype.renderParagraph = function(){
|
|
|
242 |
|
|
|
243 |
var fragments = PurgeWhiteSpace( this.paragraph.text )
|
|
|
244 |
, styles = this.paragraph.style
|
|
|
245 |
, blockstyle = this.paragraph.blockstyle
|
|
|
246 |
, priorblockstype = this.paragraph.blockstyle || {}
|
|
|
247 |
this.paragraph = {'text':[], 'style':[], 'blockstyle':{}, 'priorblockstyle':blockstyle}
|
|
|
248 |
|
|
|
249 |
if (!fragments.join('').trim()) {
|
|
|
250 |
/* if it's empty string */
|
|
|
251 |
return
|
|
|
252 |
} // else there is something to draw
|
|
|
253 |
|
|
|
254 |
var lines = this.splitFragmentsIntoLines(fragments, styles)
|
|
|
255 |
, line // will be array of array pairs [[t,s],[t,s],[t,s]...] where t = text, s = style object
|
|
|
256 |
|
|
|
257 |
, maxLineHeight
|
|
|
258 |
, defaultFontSize = 12
|
|
|
259 |
, fontToUnitRatio = defaultFontSize / this.pdf.internal.scaleFactor
|
|
|
260 |
|
|
|
261 |
// these will be in pdf instance units
|
|
|
262 |
, paragraphspacing_before = (
|
|
|
263 |
// we only use margin-top potion that is larger than margin-bottom of previous elem
|
|
|
264 |
// because CSS margins don't stack, they overlap.
|
|
|
265 |
Math.max( ( blockstyle['margin-top'] || 0 ) - ( priorblockstype['margin-bottom'] || 0 ), 0 ) +
|
|
|
266 |
( blockstyle['padding-top'] || 0 )
|
|
|
267 |
) * fontToUnitRatio
|
|
|
268 |
, paragraphspacing_after = (
|
|
|
269 |
( blockstyle['margin-bottom'] || 0 ) + ( blockstyle['padding-bottom'] || 0 )
|
|
|
270 |
) * fontToUnitRatio
|
|
|
271 |
|
|
|
272 |
, out = this.pdf.internal.write
|
|
|
273 |
|
|
|
274 |
, i, l
|
|
|
275 |
|
|
|
276 |
this.y += paragraphspacing_before
|
|
|
277 |
|
|
|
278 |
out(
|
|
|
279 |
'q' // canning the scope
|
|
|
280 |
, 'BT' // Begin Text
|
|
|
281 |
// and this moves the text start to desired position.
|
|
|
282 |
, this.pdf.internal.getCoordinateString(this.x)
|
|
|
283 |
, this.pdf.internal.getVerticalCoordinateString(this.y)
|
|
|
284 |
, 'Td'
|
|
|
285 |
)
|
|
|
286 |
|
|
|
287 |
// looping through lines
|
|
|
288 |
while (lines.length) {
|
|
|
289 |
line = lines.shift()
|
|
|
290 |
|
|
|
291 |
maxLineHeight = 0
|
|
|
292 |
|
|
|
293 |
for (i = 0, l = line.length; i !== l; i++) {
|
|
|
294 |
if (line[i][0].trim()) {
|
|
|
295 |
maxLineHeight = Math.max(maxLineHeight, line[i][1]['line-height'], line[i][1]['font-size'])
|
|
|
296 |
}
|
|
|
297 |
}
|
|
|
298 |
|
|
|
299 |
// current coordinates are "top left" corner of text box. Text must start from "lower left"
|
|
|
300 |
// so, lowering the current coord one line height.
|
|
|
301 |
out(
|
|
|
302 |
|
|
|
303 |
, (-1 * defaultFontSize * maxLineHeight).toFixed(2) // shifting down a line in native `points' means reducing y coordinate
|
|
|
304 |
, 'Td'
|
|
|
305 |
// , (defaultFontSize * maxLineHeight).toFixed(2) // line height comes as float = proportion to normal.
|
|
|
306 |
// , 'TL' // line height marker. Not sure we need it with "Td", but...
|
|
|
307 |
)
|
|
|
308 |
|
|
|
309 |
for (i = 0, l = line.length; i !== l; i++) {
|
|
|
310 |
if (line[i][0]) {
|
|
|
311 |
this.RenderTextFragment(line[i][0], line[i][1])
|
|
|
312 |
}
|
|
|
313 |
}
|
|
|
314 |
|
|
|
315 |
// y is in user units (cm, inch etc)
|
|
|
316 |
// maxLineHeight is ratio of defaultFontSize
|
|
|
317 |
// defaultFontSize is in points always.
|
|
|
318 |
// this.internal.scaleFactor is ratio of user unit to points.
|
|
|
319 |
// Dividing by it converts points to user units.
|
|
|
320 |
// vertical offset will be in user units.
|
|
|
321 |
// this.y is in user units.
|
|
|
322 |
this.y += maxLineHeight * fontToUnitRatio
|
|
|
323 |
}
|
|
|
324 |
|
|
|
325 |
out(
|
|
|
326 |
'ET' // End Text
|
|
|
327 |
, 'Q' // restore scope
|
|
|
328 |
)
|
|
|
329 |
|
|
|
330 |
this.y += paragraphspacing_after
|
|
|
331 |
}
|
|
|
332 |
|
|
|
333 |
Renderer.prototype.setBlockBoundary = function(){
|
|
|
334 |
this.renderParagraph()
|
|
|
335 |
}
|
|
|
336 |
|
|
|
337 |
Renderer.prototype.setBlockStyle = function(css){
|
|
|
338 |
this.paragraph.blockstyle = css
|
|
|
339 |
}
|
|
|
340 |
|
|
|
341 |
Renderer.prototype.addText = function(text, css){
|
|
|
342 |
this.paragraph.text.push(text)
|
|
|
343 |
this.paragraph.style.push(css)
|
|
|
344 |
}
|
|
|
345 |
|
|
|
346 |
|
|
|
347 |
//=====================
|
|
|
348 |
// these are DrillForContent and friends
|
|
|
349 |
|
|
|
350 |
var FontNameDB = {
|
|
|
351 |
'helvetica':'helvetica'
|
|
|
352 |
, 'sans-serif':'helvetica'
|
|
|
353 |
, 'serif':'times'
|
|
|
354 |
, 'times':'times'
|
|
|
355 |
, 'times new roman':'times'
|
|
|
356 |
, 'monospace':'courier'
|
|
|
357 |
, 'courier':'courier'
|
|
|
358 |
}
|
|
|
359 |
, FontWeightMap = {"100":'normal',"200":'normal',"300":'normal',"400":'normal',"500":'bold',"600":'bold',"700":'bold',"800":'bold',"900":'bold',"normal":'normal',"bold":'bold',"bolder":'bold',"lighter":'normal'}
|
|
|
360 |
, FontStyleMap = {'normal':'normal','italic':'italic','oblique':'italic'}
|
|
|
361 |
, UnitedNumberMap = {'normal':1}
|
|
|
362 |
|
|
|
363 |
function ResolveFont(css_font_family_string){
|
|
|
364 |
var name
|
|
|
365 |
, parts = css_font_family_string.split(',') // yes, we don't care about , inside quotes
|
|
|
366 |
, part = parts.shift()
|
|
|
367 |
|
|
|
368 |
while (!name && part){
|
|
|
369 |
name = FontNameDB[ part.trim().toLowerCase() ]
|
|
|
370 |
part = parts.shift()
|
|
|
371 |
}
|
|
|
372 |
return name
|
|
|
373 |
}
|
|
|
374 |
|
|
|
375 |
// return ratio to "normal" font size. in other words, it's fraction of 16 pixels.
|
|
|
376 |
function ResolveUnitedNumber(css_line_height_string){
|
|
|
377 |
var undef
|
|
|
378 |
, normal = 16.00
|
|
|
379 |
, value = UnitedNumberMap[css_line_height_string]
|
|
|
380 |
if (value) {
|
|
|
381 |
return value
|
|
|
382 |
}
|
|
|
383 |
|
|
|
384 |
// not in cache, ok. need to parse it.
|
|
|
385 |
|
|
|
386 |
// Could it be a named value?
|
|
|
387 |
// we will use Windows 94dpi sizing with CSS2 suggested 1.2 step ratio
|
|
|
388 |
// where "normal" or "medium" is 16px
|
|
|
389 |
// see: http://style.cleverchimp.com/font_size_intervals/altintervals.html
|
|
|
390 |
value = ({
|
|
|
391 |
'xx-small':9
|
|
|
392 |
, 'x-small':11
|
|
|
393 |
, 'small':13
|
|
|
394 |
, 'medium':16
|
|
|
395 |
, 'large':19
|
|
|
396 |
, 'x-large':23
|
|
|
397 |
, 'xx-large':28
|
|
|
398 |
, 'auto':0
|
|
|
399 |
})[css_line_height_string]
|
|
|
400 |
if (value !== undef) {
|
|
|
401 |
// caching, returning
|
|
|
402 |
return UnitedNumberMap[css_line_height_string] = value / normal
|
|
|
403 |
}
|
|
|
404 |
|
|
|
405 |
// not in cache, ok. need to parse it.
|
|
|
406 |
// is it int?
|
|
|
407 |
if (value = parseFloat(css_line_height_string)) {
|
|
|
408 |
// caching, returning
|
|
|
409 |
return UnitedNumberMap[css_line_height_string] = value / normal
|
|
|
410 |
}
|
|
|
411 |
|
|
|
412 |
// must be a "united" value ('123em', '134px' etc.)
|
|
|
413 |
// most browsers convert it to px so, only handling the px
|
|
|
414 |
value = css_line_height_string.match( /([\d\.]+)(px)/ )
|
|
|
415 |
if (value.length === 3) {
|
|
|
416 |
// caching, returning
|
|
|
417 |
return UnitedNumberMap[css_line_height_string] = parseFloat( value[1] ) / normal
|
|
|
418 |
}
|
|
|
419 |
|
|
|
420 |
return UnitedNumberMap[css_line_height_string] = 1
|
|
|
421 |
}
|
|
|
422 |
|
|
|
423 |
function GetCSS(element){
|
|
|
424 |
var $e = $(element)
|
|
|
425 |
, css = {}
|
|
|
426 |
, tmp
|
|
|
427 |
|
|
|
428 |
css['font-family'] = ResolveFont( $e.css('font-family') ) || 'times'
|
|
|
429 |
css['font-style'] = FontStyleMap [ $e.css('font-style') ] || 'normal'
|
|
|
430 |
tmp = FontWeightMap[ $e.css('font-weight') ] || 'normal'
|
|
|
431 |
if (tmp === 'bold') {
|
|
|
432 |
if (css['font-style'] === 'normal') {
|
|
|
433 |
css['font-style'] = tmp
|
|
|
434 |
} else {
|
|
|
435 |
css['font-style'] = tmp + css['font-style'] // jsPDF's default fonts have it as "bolditalic"
|
|
|
436 |
}
|
|
|
437 |
}
|
|
|
438 |
|
|
|
439 |
css['font-size'] = ResolveUnitedNumber( $e.css('font-size') ) || 1 // ratio to "normal" size
|
|
|
440 |
css['line-height'] = ResolveUnitedNumber( $e.css('line-height') ) || 1 // ratio to "normal" size
|
|
|
441 |
|
|
|
442 |
css['display'] = $e.css('display') === 'inline' ? 'inline' : 'block'
|
|
|
443 |
|
|
|
444 |
if (css['display'] === 'block'){
|
|
|
445 |
css['margin-top'] = ResolveUnitedNumber( $e.css('margin-top') ) || 0
|
|
|
446 |
css['margin-bottom'] = ResolveUnitedNumber( $e.css('margin-bottom') ) || 0
|
|
|
447 |
css['padding-top'] = ResolveUnitedNumber( $e.css('padding-top') ) || 0
|
|
|
448 |
css['padding-bottom'] = ResolveUnitedNumber( $e.css('padding-bottom') ) || 0
|
|
|
449 |
}
|
|
|
450 |
|
|
|
451 |
return css
|
|
|
452 |
}
|
|
|
453 |
|
|
|
454 |
function elementHandledElsewhere(element, renderer, elementHandlers){
|
|
|
455 |
var isHandledElsewhere = false
|
|
|
456 |
|
|
|
457 |
var i, l, t
|
|
|
458 |
, handlers = elementHandlers['#'+element.id]
|
|
|
459 |
if (handlers) {
|
|
|
460 |
if (typeof handlers === 'function') {
|
|
|
461 |
isHandledElsewhere = handlers(element, renderer)
|
|
|
462 |
} else /* array */ {
|
|
|
463 |
i = 0
|
|
|
464 |
l = handlers.length
|
|
|
465 |
while (!isHandledElsewhere && i !== l){
|
|
|
466 |
isHandledElsewhere = handlers[i](element, renderer)
|
|
|
467 |
;i++;
|
|
|
468 |
}
|
|
|
469 |
}
|
|
|
470 |
}
|
|
|
471 |
|
|
|
472 |
handlers = elementHandlers[element.nodeName]
|
|
|
473 |
if (!isHandledElsewhere && handlers) {
|
|
|
474 |
if (typeof handlers === 'function') {
|
|
|
475 |
isHandledElsewhere = handlers(element, renderer)
|
|
|
476 |
} else /* array */ {
|
|
|
477 |
i = 0
|
|
|
478 |
l = handlers.length
|
|
|
479 |
while (!isHandledElsewhere && i !== l){
|
|
|
480 |
isHandledElsewhere = handlers[i](element, renderer)
|
|
|
481 |
;i++;
|
|
|
482 |
}
|
|
|
483 |
}
|
|
|
484 |
}
|
|
|
485 |
|
|
|
486 |
return isHandledElsewhere
|
|
|
487 |
}
|
|
|
488 |
|
|
|
489 |
function DrillForContent(element, renderer, elementHandlers){
|
|
|
490 |
var cns = element.childNodes
|
|
|
491 |
, cn
|
|
|
492 |
, fragmentCSS = GetCSS(element)
|
|
|
493 |
, isBlock = fragmentCSS.display === 'block'
|
|
|
494 |
|
|
|
495 |
if (isBlock) {
|
|
|
496 |
renderer.setBlockBoundary()
|
|
|
497 |
renderer.setBlockStyle(fragmentCSS)
|
|
|
498 |
}
|
|
|
499 |
|
|
|
500 |
for (var i = 0, l = cns.length; i < l ; i++){
|
|
|
501 |
cn = cns[i]
|
|
|
502 |
|
|
|
503 |
if (typeof cn === 'object') {
|
|
|
504 |
// Don't render the insides of script tags, they contain text nodes which then render
|
|
|
505 |
if (cn.nodeType === 1 && cn.nodeName != 'SCRIPT') {
|
|
|
506 |
if (!elementHandledElsewhere(cn, renderer, elementHandlers)) {
|
|
|
507 |
DrillForContent(cn, renderer, elementHandlers)
|
|
|
508 |
}
|
|
|
509 |
} else if (cn.nodeType === 3){
|
|
|
510 |
renderer.addText( cn.nodeValue, fragmentCSS )
|
|
|
511 |
}
|
|
|
512 |
} else if (typeof cn === 'string') {
|
|
|
513 |
renderer.addText( cn, fragmentCSS )
|
|
|
514 |
}
|
|
|
515 |
}
|
|
|
516 |
|
|
|
517 |
if (isBlock) {
|
|
|
518 |
renderer.setBlockBoundary()
|
|
|
519 |
}
|
|
|
520 |
}
|
|
|
521 |
|
|
|
522 |
function process(pdf, element, x, y, settings) {
|
|
|
523 |
|
|
|
524 |
// we operate on DOM elems. So HTML-formatted strings need to pushed into one
|
|
|
525 |
if (typeof element === 'string') {
|
|
|
526 |
element = (function(element) {
|
|
|
527 |
var framename = "jsPDFhtmlText" + Date.now().toString() + (Math.random() * 1000).toFixed(0)
|
|
|
528 |
, visuallyhidden = 'position: absolute !important;' +
|
|
|
529 |
'clip: rect(1px 1px 1px 1px); /* IE6, IE7 */' +
|
|
|
530 |
'clip: rect(1px, 1px, 1px, 1px);' +
|
|
|
531 |
'padding:0 !important;' +
|
|
|
532 |
'border:0 !important;' +
|
|
|
533 |
'height: 1px !important;' +
|
|
|
534 |
'width: 1px !important; ' +
|
|
|
535 |
'top:auto;' +
|
|
|
536 |
'left:-100px;' +
|
|
|
537 |
'overflow: hidden;'
|
|
|
538 |
// TODO: clean up hidden div
|
|
|
539 |
, $hiddendiv = $(
|
|
|
540 |
'<div style="'+visuallyhidden+'">' +
|
|
|
541 |
'<iframe style="height:1px;width:1px" name="'+framename+'" />' +
|
|
|
542 |
'</div>'
|
|
|
543 |
).appendTo(document.body)
|
|
|
544 |
, $frame = window.frames[framename]
|
|
|
545 |
return $($frame.document.body).html(element)[0]
|
|
|
546 |
})( element )
|
|
|
547 |
}
|
|
|
548 |
|
|
|
549 |
var r = new Renderer( pdf, x, y, settings )
|
|
|
550 |
, a = DrillForContent( element, r, settings.elementHandlers )
|
|
|
551 |
|
|
|
552 |
return r.dispose()
|
|
|
553 |
|
|
|
554 |
}
|
|
|
555 |
|
|
|
556 |
|
|
|
557 |
/**
|
|
|
558 |
Converts HTML-formatted text into formatted PDF text.
|
|
|
559 |
|
|
|
560 |
Notes:
|
|
|
561 |
2012-07-18
|
|
|
562 |
Plugin relies on having browser, DOM around. The HTML is pushed into dom and traversed.
|
|
|
563 |
Plugin relies on jQuery for CSS extraction.
|
|
|
564 |
Targeting HTML output from Markdown templating, which is a very simple
|
|
|
565 |
markup - div, span, em, strong, p. No br-based paragraph separation supported explicitly (but still may work.)
|
|
|
566 |
Images, tables are NOT supported.
|
|
|
567 |
|
|
|
568 |
@public
|
|
|
569 |
@function
|
|
|
570 |
@param HTML {String or DOM Element} HTML-formatted text, or pointer to DOM element that is to be rendered into PDF.
|
|
|
571 |
@param x {Number} starting X coordinate in jsPDF instance's declared units.
|
|
|
572 |
@param y {Number} starting Y coordinate in jsPDF instance's declared units.
|
|
|
573 |
@param settings {Object} Additional / optional variables controlling parsing, rendering.
|
|
|
574 |
@returns {Object} jsPDF instance
|
|
|
575 |
*/
|
|
|
576 |
jsPDFAPI.fromHTML = function(HTML, x, y, settings) {
|
|
|
577 |
'use strict'
|
|
|
578 |
// `this` is _jsPDF object returned when jsPDF is inited (new jsPDF())
|
|
|
579 |
// `this.internal` is a collection of useful, specific-to-raw-PDF-stream functions.
|
|
|
580 |
// for example, `this.internal.write` function allowing you to write directly to PDF stream.
|
|
|
581 |
// `this.line`, `this.text` etc are available directly.
|
|
|
582 |
// so if your plugin just wraps complex series of this.line or this.text or other public API calls,
|
|
|
583 |
// you don't need to look into `this.internal`
|
|
|
584 |
// See _jsPDF object in jspdf.js for complete list of what's available to you.
|
|
|
585 |
|
|
|
586 |
// it is good practice to return ref to jsPDF instance to make
|
|
|
587 |
// the calls chainable.
|
|
|
588 |
// return this
|
|
|
589 |
|
|
|
590 |
// but in this case it is more usefull to return some stats about what we rendered.
|
|
|
591 |
return process(this, HTML, x, y, settings)
|
|
|
592 |
}
|
|
|
593 |
|
|
|
594 |
})(jsPDF.API)
|