Subversion-Projekte lars-tiefland.cienc

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
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)