updraft 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/updraft.rb +1010 -0
  2. data/updraft.gemspec +22 -0
  3. metadata +63 -0
data/lib/updraft.rb ADDED
@@ -0,0 +1,1010 @@
1
+ # =============================================================================
2
+ # updraft.rb: Umpteenth Portable Document Renderer And Formatting Tool
3
+ #
4
+ # Author: Steve Shreeve <steve.shreeve@gmail.com>
5
+ # Date: March 9, 2010
6
+ # Legal: Same license as Ruby.
7
+ # Props: Many ideas from FPDF.php and FPDF.rb
8
+ # =============================================================================
9
+ # TODO:
10
+ # * font - only emit when necessary (refer to how colors are handled)
11
+ # * disable auto-page break (in print() method)
12
+ # * wrap - turn wrapping on/off
13
+ # * feed - auto linefeed on/off
14
+ # =============================================================================
15
+
16
+ require 'date'
17
+ require 'enumerator'
18
+ require 'zlib'
19
+
20
+ class Updraft
21
+ attr_accessor :author, :creator, :keywords, :subject, :title
22
+
23
+ # ==[ Document ]=============================================================
24
+
25
+ def initialize(*args, &block)
26
+ @buffer = ''
27
+ @offsets = []
28
+ @pages = []
29
+ @flip = {}
30
+ @font = {}
31
+ @fonts = {}
32
+ @images = {}
33
+ @colors = {}
34
+ @page = 0
35
+
36
+ # defaults
37
+ @compress = false # compression
38
+ @orientation = 'P' # portrait
39
+ @scale = 72.0 # in
40
+ @format = [612, 792] # letter
41
+ @spacing = 1.0 # line spacing
42
+ @thick = 1.0 # line thickness
43
+ @dpi = 300.0 # for images
44
+ @tab = 18.0 # for indenting
45
+ @zoom = 'fullwidth' # page width
46
+ @layout = 'continuous' # one page
47
+ @path = File.dirname(__FILE__) # font path
48
+ name,type,size = 'helvetica', '', 12 # font
49
+ @margins = {
50
+ 'top' => 36.0,
51
+ 'right' => 36.0,
52
+ 'bottom' => 36.0,
53
+ 'left' => 36.0,
54
+ 'cell' => 1.5,
55
+ }
56
+
57
+ # process arguments
58
+ args.each do |old|
59
+ case arg = old.is_a?(String) ? old.downcase : old
60
+
61
+ # orientation
62
+ when 'portrait', 'p', 'landscape', 'l' then @orientation = arg[0,1].upcase
63
+
64
+ # scale
65
+ when 'pt' then @scale = 1.0
66
+ when 'mm' then @scale = 72.0 / 25.4
67
+ when 'cm' then @scale = 72.0 / 2.54
68
+ when 'in' then @scale = 72.0
69
+
70
+ # format
71
+ when 'a3' then @format = [841.89, 1190.55]
72
+ when 'a4' then @format = [595.28, 841.89]
73
+ when 'a5' then @format = [420.94, 595.28]
74
+ when 'letter' then @format = [612 , 792 ]
75
+ when 'legal' then @format = [612 , 1008 ]
76
+
77
+ # display
78
+ when 'fullpage', 'fullwidth', 'real' then @zoom = arg
79
+ when 'single', 'continuous', 'two' then @layout = arg
80
+
81
+ # font
82
+ when /[\/\\]/ then @path = old
83
+ when Numeric then size = arg
84
+ when /^[biu]*$/ then type = arg
85
+ when String then name = old
86
+
87
+ # compression
88
+ when true,false then @compress = arg
89
+
90
+ # hashes
91
+ when Hash
92
+ arg.each do |key,val|
93
+ case key = key.to_s.downcase
94
+ when 'top','right','bottom','left','cell' then @margins[key] = val.to_f * @scale
95
+ when 'spacing' then @spacing = val.to_f
96
+ when 'thick' then @thick = val.to_f * @scale
97
+ when 'dpi' then @dpi = val.to_f
98
+ when 'margins' then margins(val)
99
+ when 'colors' then colors(*val)
100
+ else raise "invalid hash key #{key.inspect}"
101
+ end
102
+ end
103
+
104
+ # syntactic sugar (may require @scale to be defined)
105
+ when Array
106
+ case arg.size
107
+ when 1 then @tab = arg[0].to_f * @scale # tab size for indent
108
+ when 2 then @format = [arg[0] * @scale, arg[1] * @scale]
109
+ end
110
+
111
+ else raise "invalid arg #{arg.inspect}"
112
+ end
113
+ end
114
+
115
+ # computed values
116
+ font(name, type, size)
117
+ @wide, @tall = *(@orientation == 'P' ? @format : @format.reverse)
118
+ @x = @margins['left']
119
+ @y = @margins['top']
120
+ @indents = [@x]
121
+
122
+ # invoke optional code block
123
+ instance_eval(&block) if block_given?
124
+ end
125
+
126
+ def finish
127
+ @state = 1 # in-document
128
+ out_header
129
+ out_info
130
+ out_catalog
131
+ out_pages
132
+ out_fonts
133
+ out_images
134
+ out_resources
135
+ out_xrefs
136
+ out_trailer
137
+ @state = 3 # finished
138
+ end
139
+
140
+ def save(path)
141
+ finish unless @state == 3 # finished
142
+ File.open(path, 'wb') {|f| f.puts(@buffer)}
143
+ path
144
+ end
145
+
146
+ def to_s
147
+ @buffer
148
+ end
149
+
150
+ # ==[ Output ]===============================================================
151
+
152
+ def out(str)
153
+ if @state == 2 # in-page
154
+ @pages[@page] << "#{str}\n"
155
+ else
156
+ @buffer << "#{str}\n"
157
+ end
158
+ end
159
+
160
+ def out_stream(str)
161
+ out("stream")
162
+ out(str)
163
+ out("endstream")
164
+ end
165
+
166
+ def out_line_color(color)
167
+ tag = color[' '] ? "RG" : "G"
168
+ out("#{color} #{tag}")
169
+ end
170
+
171
+ def out_fill_color(color)
172
+ tag = color[' '] ? "rg" : "g"
173
+ out("#{color} #{tag}")
174
+ end
175
+
176
+ def out_rect(x, y, w, h, type='d') # d=draw, f=fill, df=both
177
+ type = case type.downcase
178
+ when 'f' then 'f'
179
+ when 'df', 'fd' then 'B'
180
+ else 'S'
181
+ end
182
+ out("%.3f %.3f %.3f %.3f re %s" % [x, y, w, h, type])
183
+ end
184
+
185
+ def out_line(x1, y1, x2, y2)
186
+ out("%.3f %.3f m %.3f %.3f l S" % [x1, y1, x2, y2])
187
+ end
188
+
189
+ def out_text(x, y, str)
190
+ out("BT %.3f %.3f Td (%s) Tj ET" % [x, y, str])
191
+ end
192
+
193
+ def out_font(i, size)
194
+ out("BT /F%d %.3f Tf ET" % [i, size]) if @state == 2 # in-page
195
+ end
196
+
197
+ def out_image(w, h, x, y, i)
198
+ out("q %.3f 0 0 %.3f %.3f %.3f cm /I%d Do Q" % [w, h, x, y, i])
199
+ end
200
+
201
+ # ==[ Objects ]==============================================================
202
+
203
+ def new_obj
204
+ @offsets << @buffer.length
205
+ out("#{@offsets.size} 0 obj")
206
+ @offsets.size
207
+ end
208
+
209
+ def end_obj
210
+ out("endobj")
211
+ end
212
+
213
+ def out_header
214
+ out("%PDF-1.3")
215
+ end
216
+
217
+ def out_info
218
+ new_obj # 1
219
+ out("<<")
220
+ out("/Producer " << string('Ruby Updraft 1.0'))
221
+ out("/Title " << string(@title )) if @title
222
+ out("/Subject " << string(@subject )) if @subject
223
+ out("/Author " << string(@author )) if @author
224
+ out("/Keywords " << string(@keywords)) if @keywords
225
+ out("/Creator " << string(@creator )) if @creator
226
+ out("/CreationDate " << string("D: " + DateTime.now.to_s))
227
+ out(">>")
228
+ end_obj
229
+ end
230
+
231
+ def out_catalog
232
+ new_obj # 2
233
+ out("<<")
234
+ out("/Type /Catalog")
235
+ out("/Pages 3 0 R")
236
+ case @zoom
237
+ when "fullpage" then out("/OpenAction [4 0 R /Fit]")
238
+ when "fullwidth" then out("/OpenAction [4 0 R /FitH null]")
239
+ when "real" then out("/OpenAction [4 0 R /XYZ null null 1]")
240
+ when Numeric then out("/OpenAction [4 0 R /XYZ null null #{@zoom/100}]")
241
+ end
242
+ case @layout
243
+ when "single" then out("/PageLayout /SinglePage")
244
+ when "continuous" then out("/PageLayout /OneColumn")
245
+ when "two" then out("/PageLayout /TwoColumnLeft")
246
+ end
247
+ out(">>")
248
+ end_obj
249
+ end
250
+
251
+ def out_pages
252
+ new_obj # 3
253
+ out("<<")
254
+ out("/Type /Pages")
255
+ out("/Kids [")
256
+ out((1..@page).map {|page| "#{2 + 2 * page} 0 R"}.join("\n"))
257
+ out("]")
258
+ out("/Count #{@page}")
259
+ ref = 4 + @page * 2 # initial offset
260
+ @fonts.each {|k,v| ref += @core_fonts[k] ? 1 : 4} # with fonts
261
+ @images.each {|k,v| ref += v['pal'] ? 2 : 1} # with images
262
+ out("/Resources #{ref} 0 R")
263
+ out("/MediaBox [0 0 %.2f %.2f]" % [@wide, @tall])
264
+ out(">>")
265
+ end_obj
266
+
267
+ 1.upto(@page) do |page|
268
+ new_obj # 4, steps by 2
269
+ out("<<")
270
+ out("/Type /Page")
271
+ out("/Parent 3 0 R")
272
+ out("/MediaBox [0 0 %.2f %.2f]" % [@tall, @wide]) if @flip[page]
273
+ out("/Contents #{3 + page * 2} 0 R")
274
+ #!# handle annotations (links)...
275
+ out(">>")
276
+ end_obj
277
+
278
+ data = @pages[page].chomp
279
+ data = Zlib::Deflate.deflate(data) if @compress
280
+
281
+ new_obj
282
+ out("<<")
283
+ out("/Filter /FlateDecode") if @compress
284
+ out("/Length #{data.length}")
285
+ out(">>")
286
+ out_stream(data)
287
+ end_obj
288
+ end
289
+ end
290
+
291
+ def out_fonts
292
+ @fonts.values.sort{|a,b| a['i'] <=> b['i']}.each do |font|
293
+ font['type'] =~ /^Type1|TrueType$/ or raise "invalid font type #{font['type']}"
294
+
295
+ # font
296
+ font['n'] = ref = new_obj
297
+ out("<<")
298
+ out("/Type /Font")
299
+ out("/BaseFont /#{font['name']}")
300
+ out("/Subtype /#{font['type']}")
301
+ out("/Encoding /WinAnsiEncoding") if font['font'] !~ /^symbol|zapfdingbats$/i #!# unless "#{font['enc']}".empty? #!# what about "/Differences"???
302
+ if @core_fonts[font['font']]
303
+ out(">>")
304
+ end_obj
305
+ next
306
+ end
307
+ out("/FirstChar 32 /LastChar 255")
308
+ out("/FontDescriptor #{ref + 1} 0 R")
309
+ out("/Widths #{ref + 2} 0 R")
310
+ out(">>")
311
+ end_obj
312
+
313
+ # descriptor
314
+ new_obj
315
+ out("<<")
316
+ out("/Type /FontDescriptor")
317
+ out("/FontName /#{font['name']}")
318
+ out("/FontFile#{font['type'] == 'Type1' ? '' : '2'} #{ref + 3} 0 R") #!# make a lookup hash?
319
+ font['desc'].sort.each {|key, val| out("/#{key} #{val}")}
320
+ out(">>")
321
+ end_obj
322
+
323
+ # widths
324
+ new_obj
325
+ size = font['cw']
326
+ list = (32..255).map {|num| size[num]}
327
+ out("[ #{list.join(' ')} ]")
328
+ end_obj
329
+
330
+ # file
331
+ new_obj
332
+ path = File.join(@path, font['file'])
333
+ size = File.size(path)
334
+ out("<<")
335
+ out("/Filter /FlateDecode") if path[-2, 2] == '.z'
336
+ out("/Length #{size}")
337
+ out("/Length1 #{font['len1']}")
338
+ out("/Length2 #{font['len2']} /Length3 0") if font['len2']
339
+ out(">>")
340
+ out_stream(File.open(path, "rb") {|f| f.read})
341
+ end_obj
342
+ end
343
+ end
344
+
345
+ def out_images
346
+ @images.values.sort{|a,b| a['i'] <=> b['i']}.each do |info|
347
+ info['n'] = ref = new_obj
348
+
349
+ # image
350
+ out("<<")
351
+ out("/Type /XObject")
352
+ out("/Subtype /Image")
353
+ out("/Width #{info['w']}")
354
+ out("/Height #{info['h']}")
355
+ out("/ColorSpace [/Indexed /DeviceRGB #{info['pal'].length / 3 - 1} #{ref + 1} 0 R]") if info['cs'] == 'Indexed'
356
+ out("/ColorSpace /#{info['cs']}") if info['cs'] != 'Indexed'
357
+ out("/Decode [1 0 1 0 1 0 1 0]") if info['cs'] == 'DeviceCMYK'
358
+ out("/BitsPerComponent #{info['bpc']}")
359
+ case info['type']
360
+ when 'jpg'
361
+ out("/Filter /DCTDecode")
362
+ when 'png'
363
+ out("/Filter /FlateDecode")
364
+ out("/DecodeParms")
365
+ out("<<")
366
+ out("/Predictor 15")
367
+ out("/Colors %d" % (info['cs'] == 'DeviceRGB' ? 3 : 1))
368
+ out("/BitsPerComponent %d" % info['bpc'])
369
+ out("/Columns %d" % info['w'])
370
+ out(">>")
371
+ end
372
+ out("/Length #{info['data'].size}")
373
+ out(">>")
374
+ out_stream(info['data'])
375
+ end_obj
376
+
377
+ # palette
378
+ pal = info['pal'] or next
379
+ pal = Zlib::Deflate.deflate(pal) if @compress
380
+ new_obj
381
+ out("<<")
382
+ out("/Filter /FlateDecode") if @compress
383
+ out("/Length #{pal.size}")
384
+ out(">>")
385
+ out_stream(pal)
386
+ end_obj
387
+ end
388
+ end
389
+
390
+ def out_resources
391
+ new_obj
392
+ out("<<")
393
+ out("/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]")
394
+ unless @fonts.empty?
395
+ out("/Font")
396
+ out("<<")
397
+ @fonts.values.sort{|a,b| a['i'] <=> b['i']}.each do |font|
398
+ out("/F#{font['i']} #{font['n']} 0 R")
399
+ end
400
+ out(">>")
401
+ end
402
+ unless @images.empty?
403
+ out("/XObject")
404
+ out("<<")
405
+ @images.values.sort{|a,b| a['i'] <=> b['i']}.each do |image|
406
+ out("/I#{image['i']} #{image['n']} 0 R")
407
+ end
408
+ out(">>")
409
+ end
410
+ out(">>")
411
+ end_obj
412
+ end
413
+
414
+ def out_xrefs
415
+ @xref = @buffer.length
416
+ out("xref")
417
+ out("0 #{@offsets.size + 1}")
418
+ out("0000000000 65535 f ")
419
+ @offsets.each {|offset| out("%010d 00000 n " % offset)}
420
+ end
421
+
422
+ def out_trailer
423
+ @xref or raise "@xref not defined"
424
+ out("trailer")
425
+ out("<<")
426
+ out("/Info 1 0 R")
427
+ out("/Root 2 0 R")
428
+ out("/Size #{@offsets.size + 1}")
429
+ out(">>")
430
+ out("startxref")
431
+ out(@xref)
432
+ out("%%EOF")
433
+ end
434
+
435
+ # ==[ Pages ]================================================================
436
+
437
+ def page(orientation='')
438
+ @pages[@page += 1] = ''
439
+ @state = 2 # in-page
440
+ @colors['line'] = @colors['area'] = '0'
441
+ @x = @margins['left']
442
+ @y = @margins['top']
443
+
444
+ #!# handle orientations and changes
445
+ # out('2 J') # set line cap style to "projecting square cap" (if it's not 0)
446
+ # out('%.3f w' % (@thick * @scale)) # line width (if it's not 1pt)
447
+
448
+ out_font(@font['i'], @size)
449
+ end
450
+
451
+ def goto(x=nil,y=nil)
452
+ @x, x = @margins['left'], nil if x == '' # x='' moves to left margin
453
+ @y, y = @y + @size , nil if y == '' # y='' moves to top-align text
454
+ @x = (x < 0) ? (@wide - @scale * x) : (@scale * x) if x
455
+ @y = (y < 0) ? (@tall - @scale * y) : (@scale * y) if y
456
+ end
457
+
458
+ def x(x=nil)
459
+ @x = x if x
460
+ @x
461
+ end
462
+
463
+ def y(y=nil)
464
+ @y = y
465
+ @y
466
+ end
467
+
468
+ def margins(*list)
469
+ list = list.flatten.map {|num| num.to_f * @scale}
470
+ case list.size
471
+ when 0
472
+ return @margins
473
+ when 1
474
+ @margins['top'] = @margins['right'] = @margins['bottom'] = @margins['left'] = list[0]
475
+ when 2, 3
476
+ @margins['top'] = @margins['bottom'] = list[0]
477
+ @margins['right'] = @margins['left'] = list[1]
478
+ @margins['cell'] = list.last if list.size == 3
479
+ when 4, 5
480
+ @margins['top'], @margins['right'], @margins['bottom'], @margins['left'] = list
481
+ @margins['cell'] = list.last if list.size == 5
482
+ else raise "invalid margins specified #{list.inspect}"
483
+ end
484
+ nil
485
+ end
486
+
487
+ # ==[ Colors ]===============================================================
488
+
489
+ def float(val, fmt="%.3f")
490
+ case val
491
+ when 0,[0,0,0] then "0"
492
+ when 1, 255 then "1"
493
+ when 1..255 then fmt % (val / (val % 16 == 0 ? 256.0 : 255.0))
494
+ when 0..1 then fmt % val
495
+ when Array then val.size == 3 ? val.map {|x| float(x)}.join(' ') : raise
496
+ when /^[\da-f]{6}$/i then val.scan(/../).map {|x| float(( x ).hex)}.join(' ')
497
+ when /^[\da-f]{3}$/i then val.scan(/./ ).map {|x| float((x+x).hex)}.join(' ')
498
+ else raise
499
+ end
500
+ rescue
501
+ raise "unable to parse float value #{val.inspect}"
502
+ end
503
+
504
+ def colors(*args)
505
+ type = 'font'
506
+ args.each do |arg|
507
+ case arg
508
+ when 'draw', 'fill', 'font' then type = arg
509
+ when Symbol then type = arg.to_s; redo
510
+ when Numeric, Array, String then @colors[type] = float(arg)
511
+ when Hash then colors(arg.to_a.flatten)
512
+ else raise "unable to parse color #{arg.inspect}"
513
+ end
514
+ end
515
+ end
516
+
517
+ def drawcolor(val); @colors['draw'] = float(val); end
518
+ def fillcolor(val); @colors['fill'] = float(val); end
519
+ def fontcolor(val); @colors['font'] = float(val); end #!# should this be "textcolor"???
520
+
521
+ # ==[ Shapes ]===============================================================
522
+
523
+ def fill(x, y, w, h)
524
+ x = x ? x * @scale : @x
525
+ y = y ? @tall - y * @scale : @y
526
+ w = w * @scale
527
+ h = h * @scale
528
+
529
+ area, fill = @colors['area'], @colors['fill']
530
+ out_fill_color(@colors['area'] = fill) if fill && fill != area
531
+ out_rect(x, y, w, -h, 'f')
532
+ end
533
+
534
+ def draw(x, y, w, h)
535
+ line = @thick
536
+ half = 0.5 * line
537
+
538
+ x = (x ? x * @scale : @x) + half
539
+ y = (y ? @tall - y * @scale : @y) - half
540
+ w = ( w * @scale ) - line
541
+ h = ( h * @scale ) - line
542
+
543
+ line, draw = @colors['line'], @colors['draw']
544
+ out_line_color(@colors['line'] = draw) if draw && draw != line
545
+ out_rect(x, y, w, -h, 'd')
546
+ end
547
+
548
+ def line(*args)
549
+ line = @thick
550
+ half = 0.5 * line
551
+
552
+ case args.size
553
+ when 0
554
+ x1 = @margins['left'] + half
555
+ x2 = @wide - @margins['right'] - half
556
+ y1 = y2 = @tall - @y - half
557
+ when 1
558
+ if (w = args[0]) < 0
559
+ x2 = @wide - @margins['right'] - half
560
+ x1 = x2 - w + line
561
+ else
562
+ x1 = @x + half
563
+ x2 = x1 + args[0] * @scale - line
564
+ end
565
+ y1 = y2 = @tall - @y - half
566
+ when 4
567
+ x1, y1, x2, y2 = *args.map {|val| val * @scale}
568
+ y1 = @tall - y1
569
+ y2 = @tall - y2
570
+ else
571
+ raise "invalid line arg #{arg.inspect}"
572
+ end
573
+ line, draw = @colors['line'], @colors['draw']
574
+ out_line_color(@colors['line'] = draw) if draw && draw != line
575
+ out_line(x1, y1, x2, y2)
576
+ end
577
+
578
+ # ==[ Text ]=================================================================
579
+
580
+ def width(text)
581
+ size = @font['cw']
582
+ wide = text.unpack("C*").inject(0) {|wide, char| wide += size[char]}
583
+ wide * @size / 1000.0 # in pts
584
+ end
585
+
586
+ def height(val=nil)
587
+ @high = val if val
588
+ @high
589
+ end
590
+
591
+ def spacing(val=nil)
592
+ if val
593
+ @spacing = val
594
+ height(@size * @spacing * 1.2)
595
+ end
596
+ @spacing
597
+ end
598
+
599
+ def string(str)
600
+ "(#{escape(str)})"
601
+ end
602
+
603
+ def escape(str)
604
+ str.gsub("\\","\\\\").gsub("(","\\(").gsub(")","\\)")
605
+ end
606
+
607
+ def print(text, eols=0)
608
+ if @margins['bottom'] >= @tall - @y
609
+ page
610
+ header if respond_to?(:header)
611
+ end
612
+ if text != ''
613
+ line, draw = @colors['line'], @colors['draw']
614
+ area, font = @colors['area'], @colors['font']
615
+ out_line_color(@colors['line'] = draw) if draw && draw != line
616
+ out_fill_color(@colors['area'] = font) if font && font != area
617
+ out_text(@x, @tall - @y, escape(text))
618
+ if @underline
619
+ drop = @font['up']
620
+ line = @font['ut']
621
+ wide = width(text) #!# handle wordspacing => + @word_spacing * str.count(' ')
622
+ out_rect(@x, @tall - @y + drop / 1000.0 * @size, wide, -line / 1000.0 * @size, 'f')
623
+ end
624
+ end
625
+ if eols > 0
626
+ @y += @high * eols
627
+ @x = @margins['left']
628
+ else
629
+ @x += wide || width(text)
630
+ end
631
+ end
632
+
633
+ def puts(text='', eols=1)
634
+ print(text, eols)
635
+ end
636
+
637
+ def text(x=nil, y=nil, text='', eols=0)
638
+ goto(x, y) if x || y
639
+ print(text, eols)
640
+ end
641
+
642
+ def center(text, left=nil, right=nil, eols=0)
643
+ left ||= @margins['left']
644
+ right ||= @wide - @margins['right']
645
+ rows = text.split("\n",-1)
646
+ last = rows.size - 1
647
+ rows.each_with_index do |line, i|
648
+ goto((right + left - width(line)) / 2.0)
649
+ print(line, i == last ? eols : 1)
650
+ end
651
+ end
652
+
653
+ def wrap(text, eols=1)
654
+ side = @wide - @margins['right']
655
+ list = text.split(/[ \t]*\n/)
656
+ if list.empty?
657
+ print('', eols)
658
+ return
659
+ end
660
+ last = list.size - 1
661
+ list.each_with_index do |line, i|
662
+ line.gsub!("\t", " ") # HACK: tab expands to five spaces
663
+
664
+ # plenty of room
665
+ if (@x + width(line) <= side)
666
+ print(line, i == last ? eols : 1)
667
+ next
668
+ end
669
+
670
+ # requires word wrap
671
+ posn = @x
672
+ show = ""
673
+ line.scan(/\G(\s*)(\S+)/) do |fill, word|
674
+ lead = width(fill)
675
+ wide = width(word)
676
+ if (posn + lead + wide <= side)
677
+ posn += lead + wide
678
+ show += fill + word
679
+ else
680
+ print(show, i == last ? eols : 1)
681
+ posn = @x
682
+ if (posn + wide <= side)
683
+ posn += wide
684
+ show = word
685
+ else
686
+ raise "word split not yet implemented"
687
+ end
688
+ end
689
+ end
690
+ print(show, eols) unless show.empty?
691
+ end
692
+ end
693
+
694
+ def table(y, cols, rows)
695
+ wide = cols.size
696
+ bold = cols.map {|col| col.is_a?(Array)}
697
+ cols = cols.flatten
698
+ last = nil
699
+
700
+ n = 0
701
+ rows.each_slice(wide) do |list|
702
+ cols.each_with_index do |x, i|
703
+ item = list[i].to_s
704
+ next if item.empty?
705
+ if last != bold[i]
706
+ font(last ? '' : 'B')
707
+ last = !last
708
+ end
709
+ text(x, y + n * @high, item)
710
+ end
711
+ n += 1
712
+ end
713
+ font('')
714
+ end
715
+
716
+ def indent(far=[1])
717
+ far = case far
718
+ when false, nil then return block_given? ? yield : nil
719
+ when "@" then @x - @indents.last
720
+ when Numeric then far * @scale
721
+ when Array then far[0] * @tab
722
+ when String then far.to_f * @scale - @indents.last
723
+ when true then @tab
724
+ else raise "invalid indent value #{far.inspect}"
725
+ end
726
+ @x = @margins['left'] = @indents.last + far
727
+ @indents.push(@x)
728
+ if block_given?
729
+ yield
730
+ undent
731
+ end
732
+ end
733
+
734
+ def undent(num=1)
735
+ @indents.slice!(-num, num)
736
+ @x = @margins['left'] = @indents.last || raise("too many undents!")
737
+ end
738
+
739
+ # ==[ Fonts ]================================================================
740
+
741
+ def font(*args, &block)
742
+ define_core_fonts unless @core_fonts
743
+
744
+ # current font
745
+ font = @font['font'] || ''
746
+ orig = [@font, @size, @underline, @high]
747
+ name = font.sub(/[BI]+$/,'')
748
+ type = font[/[BI]*$/] + (@underline ? 'U' : '')
749
+
750
+ # grok request (may update @size and @underline)
751
+ path = nil
752
+ args.each do |arg|
753
+ case arg
754
+ when Numeric then @size = arg; spacing(@spacing)
755
+ when '' then type = arg
756
+ when /^[biu]+$/i then type = arg.upcase.split('').sort.uniq.join('')
757
+ when /[\/\\]/ then path = arg
758
+ when String then name = arg.downcase.delete(' ')
759
+ when Array then fontcolor(arg[0].is_a?(Array) ? arg[0] : arg)
760
+ else raise "unknown font argument #{arg.inspect}"
761
+ end
762
+ end
763
+ @underline = !!type.delete!('U')
764
+ font = "#{name}#{type}"
765
+
766
+ # pull font
767
+ @font = case
768
+ when @fonts[font]
769
+ @fonts[font]
770
+ when @core_fonts[font]
771
+ @fonts[font] = {
772
+ 'i' => @fonts.size + 1,
773
+ 'font' => font,
774
+ 'name' => @core_fonts[font],
775
+ 'type' => 'Type1',
776
+ 'up' => -100,
777
+ 'ut' => 50,
778
+ 'cw' => @char_width[font],
779
+ }
780
+ else
781
+ load(path ||= "#{font.downcase}.rb") #!# can we use a require instead of load?
782
+ @fonts[font] = {
783
+ 'i' => @fonts.size + 1,
784
+ 'font' => font,
785
+ 'name' => FontDef.name,
786
+ 'type' => FontDef.type,
787
+ 'up' => FontDef.up,
788
+ 'ut' => FontDef.ut,
789
+ 'cw' => FontDef.cw,
790
+ 'file' => FontDef.file,
791
+ 'enc' => FontDef.enc,
792
+ 'desc' => FontDef.desc,
793
+ 'len1' => FontDef.type == 'TrueType' ? FontDef.originalsize : FontDef.size1,
794
+ 'len2' => FontDef.type == 'TrueType' ? nil : FontDef.size2,
795
+ }
796
+ end
797
+
798
+ # change font
799
+ if [@font['i'], @size] != [orig[0]['i'], orig[1]]
800
+ out_font(@font['i'], @size)
801
+ changed = true
802
+ end
803
+
804
+ # call block and restore context
805
+ if block_given?
806
+ instance_eval(&block) #!# should this just be 'yield' ???
807
+ @font, @size, @underline, @high = orig
808
+ out_font(@font['i'], @size) if changed
809
+ end
810
+ end
811
+
812
+ def bold(*args, &block)
813
+ font(*args.push('b'), &block)
814
+ end
815
+
816
+ # ==[ Images ]===============================================================
817
+
818
+ def image(path, x=nil, y=nil, w=0, h=0)
819
+ if @images[path]
820
+ info = @images[path]
821
+ else
822
+ info = case path
823
+ when /\.jpe?g$/i then parse_jpg(path)
824
+ when /\.png$/i then parse_png(path)
825
+ else raise "unable to determine image type for #{path.inspect}"
826
+ end
827
+ info['i'] = @images.size + 1
828
+ @images[path] = info
829
+ end
830
+
831
+ # determine aspect ratio if needed
832
+ if w == 0 && h == 0
833
+ w = info['w'] / @dpi * 72.0 / @scale
834
+ h = info['h'] / @dpi * 72.0 / @scale
835
+ elsif w == 0
836
+ w = h.to_f * info['w'] / info['h']
837
+ elsif h == 0
838
+ h = w.to_f * info['h'] / info['w']
839
+ end
840
+
841
+ # (nil) for inline, (-x) for right-align, (-y) to top-align
842
+ x ||= @x / @scale
843
+ y ||= @y / @scale
844
+ y = h - y if y <= 0 # -0.0 will flush image to top of page
845
+ x =-x - w if x <= 0 # -0.0 seems a little pointless here
846
+
847
+ args = [w, h, x, -y].map {|val| val * @scale}; args[-1] += @tall
848
+ out_image(*args.push(info['i']))
849
+ end
850
+
851
+ def get_int(file)
852
+ file.read(4).unpack('N')[0]
853
+ end
854
+
855
+ def get_short(file)
856
+ file.read(2).unpack('n')[0]
857
+ end
858
+
859
+ def get_byte(file)
860
+ file.read(1).unpack('C')[0]
861
+ end
862
+
863
+ def get_mark(file)
864
+ # file.gets("\xFF") #!# Fixed in JRuby 1.5 => http://jira.codehaus.org/browse/JRUBY-4416
865
+ until (byte = get_byte(file)) == 255; end #!# Remove this for JRuby 1.5
866
+ until (byte = get_byte(file)) > 0; end
867
+ byte
868
+ end
869
+
870
+ def parse_jpg(path)
871
+ info = {}
872
+
873
+ open(path, "rb") do |file|
874
+ get_mark(file) == 0xd8 or raise "invalid JPG file #{path.inspect}" # SOI
875
+ loop do
876
+ case get_mark(file)
877
+ when 0xd9, 0xda then break # EOI, SOS
878
+ when 0xc0..0xc3, 0xc5..0xc7, 0xc9..0xcb, 0xcd..0xcf # SOF
879
+ size = get_short(file)
880
+ if info.empty?
881
+ info['bpc'] = get_byte(file)
882
+ info['h' ] = get_short(file)
883
+ info['w' ] = get_short(file)
884
+ info['cs' ] = case get_byte(file)
885
+ when 3 then 'DeviceRGB'
886
+ when 4 then 'DeviceCMYK'
887
+ else 'DeviceGray'
888
+ end
889
+ file.seek(size - 8, IO::SEEK_CUR)
890
+ else
891
+ file.seek(size - 2, IO::SEEK_CUR)
892
+ end
893
+ else
894
+ size = get_short(file)
895
+ file.seek(size - 2, IO::SEEK_CUR)
896
+ end
897
+ end
898
+ end
899
+
900
+ info.update('type' => 'jpg', 'data' => File.open(path, 'rb') {|f| f.read})
901
+ end
902
+
903
+ def parse_png(path)
904
+ info = {}
905
+
906
+ open(path, "rb") do |file|
907
+ file.read(8) == "\x89PNG\r\n\cZ\n" or raise "invalid PNG file #{path.inspect}"
908
+ file.read(4)
909
+ file.read(4) == "IHDR" or raise "invalid PNG file #{path.inspect}"
910
+
911
+ info['w' ] = get_int(file)
912
+ info['h' ] = get_int(file)
913
+ info['bpc'] = get_byte(file)
914
+ info['cs' ] = case (ct=get_byte(file))
915
+ when 0 then 'DeviceGray'
916
+ when 2 then 'DeviceRGB'
917
+ when 3 then 'Indexed'
918
+ else raise "unable to support PNG alpha channels"
919
+ end
920
+
921
+ info['bpc'] > 8 and raise "unable to support >8-bit color in file #{path.inspect}"
922
+ get_byte(file) == 0 or raise "unknown compression method in file #{path.inspect}"
923
+ get_byte(file) == 0 or raise "unknown filter method in file #{path.inspect}"
924
+ get_byte(file) == 0 or raise "unable to support interlacing in file #{path.inspect}"
925
+ file.read(4)
926
+
927
+ loop do
928
+ size = get_int(file)
929
+ type = file.read(4)
930
+ case type
931
+ when 'IEND' then break
932
+ when 'PLTE'
933
+ info['pal'] = file.read(size)
934
+ file.read(4)
935
+ when 'tRNS'
936
+ trns = file.read(size)
937
+ case ct
938
+ when 0 then info['trns'] = [trns[1]]
939
+ when 2 then info['trns'] = [trns[1], trns[3], trns[5]]
940
+ else info['trns'] = [trns.index(0)] #!# this may be wrong...
941
+ end
942
+ file.read(4)
943
+ when 'IDAT'
944
+ (info['data'] ||= "") << file.read(size)
945
+ file.read(4)
946
+ else
947
+ file.seek(size + 4, IO::SEEK_CUR)
948
+ end
949
+ end
950
+ end
951
+
952
+ raise "missing palette in file #{path.inspect}" if info['cs'] == 'Indexed' && !info['pal']
953
+
954
+ info.update('type' => 'png')
955
+ end
956
+
957
+ # ==[ Core fonts ]===========================================================
958
+
959
+ def define_core_fonts
960
+ @core_fonts = {
961
+ 'courier' => 'Courier',
962
+ 'courierB' => 'Courier-Bold',
963
+ 'courierBI' => 'Courier-BoldOblique',
964
+ 'courierI' => 'Courier-Oblique',
965
+ 'helvetica' => 'Helvetica',
966
+ 'helveticaB' => 'Helvetica-Bold',
967
+ 'helveticaBI' => 'Helvetica-BoldOblique',
968
+ 'helveticaI' => 'Helvetica-Oblique',
969
+ 'symbol' => 'Symbol',
970
+ 'times' => 'Times-Roman',
971
+ 'timesB' => 'Times-Bold',
972
+ 'timesBI' => 'Times-BoldItalic',
973
+ 'timesI' => 'Times-Italic',
974
+ 'zapfdingbats' => 'ZapfDingbats',
975
+ }
976
+
977
+ @char_width = {
978
+ 'courier' => [600] * 256,
979
+ 'courierB' => [600] * 256,
980
+ 'courierI' => [600] * 256,
981
+ 'courierBI' => [600] * 256,
982
+ 'helvetica' => [278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556, 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 350, 556, 350, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350, 350, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 350, 500, 667, 278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611, 667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278, 556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500],
983
+ 'helveticaB' => [278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556, 333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 350, 556, 350, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350, 350, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 350, 500, 667, 278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611, 722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278, 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556],
984
+ 'helveticaI' => [278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556, 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 350, 556, 350, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350, 350, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 350, 500, 667, 278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611, 667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278, 556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500],
985
+ 'helveticaBI' => [278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556, 333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 350, 556, 350, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 350, 611, 350, 350, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 350, 500, 667, 278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333, 400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611, 722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278, 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611, 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278, 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556],
986
+ 'times' => [250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 333, 408, 500, 500, 833, 778, 180, 333, 333, 500, 564, 250, 333, 250, 278, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444, 921, 722, 667, 667, 722, 611, 556, 722, 722, 333, 389, 722, 611, 889, 722, 722, 556, 722, 667, 556, 611, 722, 722, 944, 722, 722, 611, 333, 278, 333, 469, 500, 333, 444, 500, 444, 500, 444, 333, 500, 500, 278, 278, 500, 278, 778, 500, 500, 500, 500, 333, 389, 278, 500, 500, 722, 500, 500, 444, 480, 200, 480, 541, 350, 500, 350, 333, 500, 444, 1000, 500, 500, 333, 1000, 556, 333, 889, 350, 611, 350, 350, 333, 333, 444, 444, 350, 500, 1000, 333, 980, 389, 333, 722, 350, 444, 722, 250, 333, 500, 500, 500, 500, 200, 500, 333, 760, 276, 500, 564, 333, 760, 333, 400, 564, 300, 300, 333, 500, 453, 250, 333, 300, 310, 500, 750, 750, 750, 444, 722, 722, 722, 722, 722, 722, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333, 722, 722, 722, 722, 722, 722, 722, 564, 722, 722, 722, 722, 722, 722, 556, 500, 444, 444, 444, 444, 444, 444, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278, 500, 500, 500, 500, 500, 500, 500, 564, 500, 500, 500, 500, 500, 500, 500, 500],
987
+ 'timesB' => [250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 333, 555, 500, 500, 1000, 833, 278, 333, 333, 500, 570, 250, 333, 250, 278, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 930, 722, 667, 722, 722, 667, 611, 778, 778, 389, 500, 778, 667, 944, 722, 778, 611, 778, 722, 556, 667, 722, 722, 1000, 722, 722, 667, 333, 278, 333, 581, 500, 333, 500, 556, 444, 556, 444, 333, 500, 556, 278, 333, 556, 278, 833, 556, 500, 556, 556, 444, 389, 333, 556, 500, 722, 500, 500, 444, 394, 220, 394, 520, 350, 500, 350, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 1000, 350, 667, 350, 350, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 350, 444, 722, 250, 333, 500, 500, 500, 500, 220, 500, 333, 747, 300, 500, 570, 333, 747, 333, 400, 570, 300, 300, 333, 556, 540, 250, 333, 300, 330, 500, 750, 750, 750, 500, 722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 389, 389, 389, 389, 722, 722, 778, 778, 778, 778, 778, 570, 778, 722, 722, 722, 722, 722, 611, 556, 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278, 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 500, 556, 500],
988
+ 'timesI' => [250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 333, 420, 500, 500, 833, 778, 214, 333, 333, 500, 675, 250, 333, 250, 278, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 675, 675, 675, 500, 920, 611, 611, 667, 722, 611, 611, 722, 722, 333, 444, 667, 556, 833, 667, 722, 611, 722, 611, 500, 556, 722, 611, 833, 611, 556, 556, 389, 278, 389, 422, 500, 333, 500, 500, 444, 500, 444, 278, 500, 500, 278, 278, 444, 278, 722, 500, 500, 500, 500, 389, 389, 278, 500, 444, 667, 444, 444, 389, 400, 275, 400, 541, 350, 500, 350, 333, 500, 556, 889, 500, 500, 333, 1000, 500, 333, 944, 350, 556, 350, 350, 333, 333, 556, 556, 350, 500, 889, 333, 980, 389, 333, 667, 350, 389, 556, 250, 389, 500, 500, 500, 500, 275, 500, 333, 760, 276, 500, 675, 333, 760, 333, 400, 675, 300, 300, 333, 500, 523, 250, 333, 300, 310, 500, 750, 750, 750, 500, 611, 611, 611, 611, 611, 611, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333, 722, 667, 722, 722, 722, 722, 722, 675, 722, 722, 722, 722, 722, 556, 611, 500, 500, 500, 500, 500, 500, 500, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278, 500, 500, 500, 500, 500, 500, 500, 675, 500, 500, 500, 500, 500, 444, 500, 444],
989
+ 'timesBI' => [250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 389, 555, 500, 500, 833, 778, 278, 333, 333, 500, 570, 250, 333, 250, 278, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500, 832, 667, 667, 667, 722, 667, 667, 722, 778, 389, 500, 667, 611, 889, 722, 722, 611, 722, 667, 556, 611, 722, 667, 889, 667, 611, 611, 333, 278, 333, 570, 500, 333, 500, 500, 444, 500, 444, 333, 500, 556, 278, 278, 500, 278, 778, 556, 500, 500, 500, 389, 389, 278, 556, 444, 667, 500, 444, 389, 348, 220, 348, 570, 350, 500, 350, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 944, 350, 611, 350, 350, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 350, 389, 611, 250, 389, 500, 500, 500, 500, 220, 500, 333, 747, 266, 500, 606, 333, 747, 333, 400, 570, 300, 300, 333, 576, 500, 250, 333, 300, 300, 500, 750, 750, 750, 500, 667, 667, 667, 667, 667, 667, 944, 667, 667, 667, 667, 667, 389, 389, 389, 389, 722, 722, 722, 722, 722, 722, 722, 570, 722, 722, 722, 722, 722, 611, 611, 500, 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278, 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 444, 500, 444],
990
+ 'symbol' => [250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 333, 713, 500, 549, 833, 778, 439, 333, 333, 500, 549, 250, 549, 250, 278, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 549, 549, 549, 444, 549, 722, 667, 722, 612, 611, 763, 603, 722, 333, 631, 722, 686, 889, 722, 722, 768, 741, 556, 592, 611, 690, 439, 768, 645, 795, 611, 333, 863, 333, 658, 500, 500, 631, 549, 549, 494, 439, 521, 411, 603, 329, 603, 549, 549, 576, 521, 549, 549, 521, 549, 603, 439, 576, 713, 686, 493, 686, 494, 480, 200, 480, 549, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 750, 620, 247, 549, 167, 713, 500, 753, 753, 753, 753, 1042, 987, 603, 987, 603, 400, 549, 411, 549, 549, 713, 494, 460, 549, 549, 549, 549, 1000, 603, 1000, 658, 823, 686, 795, 987, 768, 768, 823, 768, 768, 713, 713, 713, 713, 713, 713, 713, 768, 713, 790, 790, 890, 823, 549, 250, 713, 603, 603, 1042, 987, 603, 987, 603, 494, 329, 790, 790, 786, 713, 384, 384, 384, 384, 384, 384, 494, 494, 494, 494, 0, 329, 274, 686, 686, 686, 384, 384, 384, 384, 384, 384, 494, 494, 494, 0],
991
+ 'zapfdingbats' => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 278, 974, 961, 974, 980, 719, 789, 790, 791, 690, 960, 939, 549, 855, 911, 933, 911, 945, 974, 755, 846, 762, 761, 571, 677, 763, 760, 759, 754, 494, 552, 537, 577, 692, 786, 788, 788, 790, 793, 794, 816, 823, 789, 841, 823, 833, 816, 831, 923, 744, 723, 749, 790, 792, 695, 776, 768, 792, 759, 707, 708, 682, 701, 826, 815, 789, 789, 707, 687, 696, 689, 786, 787, 713, 791, 785, 791, 873, 761, 762, 762, 759, 759, 892, 892, 788, 784, 438, 138, 277, 415, 392, 392, 668, 668, 0, 390, 390, 317, 317, 276, 276, 509, 509, 410, 410, 234, 234, 334, 334, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 732, 544, 544, 910, 667, 760, 760, 776, 595, 694, 626, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 894, 838, 1016, 458, 748, 924, 748, 918, 927, 928, 928, 834, 873, 828, 924, 924, 917, 930, 931, 463, 883, 836, 836, 867, 867, 696, 696, 874, 0, 874, 760, 946, 771, 865, 771, 888, 967, 888, 831, 873, 927, 970, 918, 0]
992
+ }
993
+ end
994
+
995
+ end
996
+
997
+ __END__
998
+
999
+ # Interesting...
1000
+ q % Save graphics state
1001
+ 1 0 0 1 100 200 cm % Translate
1002
+ 0.7071 0.7071 -0.7071 0.7071 0 0 cm % Rotate
1003
+ 150 0 0 80 0 0 cm % Scale
1004
+ /Image1 Do % Paint image
1005
+ Q % Restore graphics state
1006
+
1007
+ # Future...
1008
+ Link
1009
+ Cell
1010
+ private/public
data/updraft.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'updraft'
4
+ s.version = '0.5.0'
5
+ s.date = Time.now.strftime('%Y-%m-%d')
6
+
7
+ s.summary = 'PDF generation library'
8
+ s.description = 'Umpteenth Portable Document Renderer And Formatting Tool'
9
+
10
+ s.authors =['Steve Shreeve']
11
+ s.email = 'steve.shreeve@gmail.com'
12
+ s.homepage = 'http://github.com/shreeve/updraft'
13
+
14
+ s.files = %w[
15
+ updraft.gemspec
16
+ lib/updraft.rb
17
+ ]
18
+
19
+ s.require_paths =['lib']
20
+ s.rubyforge_project = ' '
21
+ s.rubygems_version = '1.3.5'
22
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: updraft
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 5
8
+ - 0
9
+ version: 0.5.0
10
+ platform: ruby
11
+ authors:
12
+ - Steve Shreeve
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-03-09 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: Umpteenth Portable Document Renderer And Formatting Tool
22
+ email: steve.shreeve@gmail.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - updraft.gemspec
31
+ - lib/updraft.rb
32
+ has_rdoc: true
33
+ homepage: http://github.com/shreeve/updraft
34
+ licenses: []
35
+
36
+ post_install_message:
37
+ rdoc_options: []
38
+
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ segments:
46
+ - 0
47
+ version: "0"
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ segments:
53
+ - 0
54
+ version: "0"
55
+ requirements: []
56
+
57
+ rubyforge_project: " "
58
+ rubygems_version: 1.3.6
59
+ signing_key:
60
+ specification_version: 3
61
+ summary: PDF generation library
62
+ test_files: []
63
+