updraft 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+