pdf-writer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/ChangeLog +44 -0
  2. data/LICENCE +118 -0
  3. data/README +32 -0
  4. data/bin/loader +54 -0
  5. data/bin/manual +22 -0
  6. data/bin/manual.bat +2 -0
  7. data/demo/chunkybacon.rb +28 -0
  8. data/demo/code.rb +63 -0
  9. data/demo/colornames.rb +843 -0
  10. data/demo/demo.rb +65 -0
  11. data/demo/gettysburg.rb +58 -0
  12. data/demo/hello.rb +18 -0
  13. data/demo/individual-i.rb +81 -0
  14. data/demo/pac.rb +62 -0
  15. data/demo/pagenumber.rb +67 -0
  16. data/demo/qr-language.rb +573 -0
  17. data/demo/qr-library.rb +371 -0
  18. data/images/chunkybacon.jpg +0 -0
  19. data/images/chunkybacon.png +0 -0
  20. data/images/pdfwriter-icon.jpg +0 -0
  21. data/images/pdfwriter-small.jpg +0 -0
  22. data/lib/pdf/charts.rb +13 -0
  23. data/lib/pdf/charts/stddev.rb +431 -0
  24. data/lib/pdf/grid.rb +135 -0
  25. data/lib/pdf/math.rb +108 -0
  26. data/lib/pdf/quickref.rb +330 -0
  27. data/lib/pdf/simpletable.rb +946 -0
  28. data/lib/pdf/techbook.rb +890 -0
  29. data/lib/pdf/writer.rb +2661 -0
  30. data/lib/pdf/writer/arc4.rb +63 -0
  31. data/lib/pdf/writer/fontmetrics.rb +201 -0
  32. data/lib/pdf/writer/fonts/Courier-Bold.afm +342 -0
  33. data/lib/pdf/writer/fonts/Courier-BoldOblique.afm +342 -0
  34. data/lib/pdf/writer/fonts/Courier-Oblique.afm +342 -0
  35. data/lib/pdf/writer/fonts/Courier.afm +342 -0
  36. data/lib/pdf/writer/fonts/Helvetica-Bold.afm +2827 -0
  37. data/lib/pdf/writer/fonts/Helvetica-BoldOblique.afm +2827 -0
  38. data/lib/pdf/writer/fonts/Helvetica-Oblique.afm +3051 -0
  39. data/lib/pdf/writer/fonts/Helvetica.afm +3051 -0
  40. data/lib/pdf/writer/fonts/MustRead.html +1 -0
  41. data/lib/pdf/writer/fonts/Symbol.afm +213 -0
  42. data/lib/pdf/writer/fonts/Times-Bold.afm +2588 -0
  43. data/lib/pdf/writer/fonts/Times-BoldItalic.afm +2384 -0
  44. data/lib/pdf/writer/fonts/Times-Italic.afm +2667 -0
  45. data/lib/pdf/writer/fonts/Times-Roman.afm +2419 -0
  46. data/lib/pdf/writer/fonts/ZapfDingbats.afm +225 -0
  47. data/lib/pdf/writer/graphics.rb +727 -0
  48. data/lib/pdf/writer/graphics/imageinfo.rb +365 -0
  49. data/lib/pdf/writer/lang.rb +43 -0
  50. data/lib/pdf/writer/lang/en.rb +77 -0
  51. data/lib/pdf/writer/object.rb +23 -0
  52. data/lib/pdf/writer/object/action.rb +40 -0
  53. data/lib/pdf/writer/object/annotation.rb +42 -0
  54. data/lib/pdf/writer/object/catalog.rb +39 -0
  55. data/lib/pdf/writer/object/contents.rb +68 -0
  56. data/lib/pdf/writer/object/destination.rb +40 -0
  57. data/lib/pdf/writer/object/encryption.rb +53 -0
  58. data/lib/pdf/writer/object/font.rb +76 -0
  59. data/lib/pdf/writer/object/fontdescriptor.rb +34 -0
  60. data/lib/pdf/writer/object/fontencoding.rb +39 -0
  61. data/lib/pdf/writer/object/image.rb +168 -0
  62. data/lib/pdf/writer/object/info.rb +55 -0
  63. data/lib/pdf/writer/object/outline.rb +30 -0
  64. data/lib/pdf/writer/object/outlines.rb +30 -0
  65. data/lib/pdf/writer/object/page.rb +195 -0
  66. data/lib/pdf/writer/object/pages.rb +115 -0
  67. data/lib/pdf/writer/object/procset.rb +46 -0
  68. data/lib/pdf/writer/object/viewerpreferences.rb +74 -0
  69. data/lib/pdf/writer/ohash.rb +58 -0
  70. data/lib/pdf/writer/oreader.rb +25 -0
  71. data/lib/pdf/writer/state.rb +48 -0
  72. data/lib/pdf/writer/strokestyle.rb +138 -0
  73. data/manual.pwd +5151 -0
  74. metadata +147 -0
@@ -0,0 +1,2661 @@
1
+ #--
2
+ # PDF::Writer for Ruby.
3
+ # http://rubyforge.org/projects/ruby-pdf/
4
+ # Copyright 2003 - 2005 Austin Ziegler.
5
+ #
6
+ # Licensed under a MIT-style licence. See LICENCE in the main distribution
7
+ # for full licensing information.
8
+ #
9
+ # $Id: writer.rb,v 1.31 2005/06/09 11:10:18 austin Exp $
10
+ #++
11
+ require 'thread'
12
+ require 'open-uri'
13
+
14
+ require 'transaction/simple'
15
+ require 'color'
16
+
17
+ # A class to provide the core functionality to create a PDF document
18
+ # without any requirement for additional modules.
19
+ module PDF
20
+ class Writer
21
+ # The version of PDF::Writer.
22
+ VERSION = '1.0.0'
23
+
24
+ # Escape the text so that it's safe for insertion into the PDF
25
+ # document.
26
+ def self.escape(text)
27
+ text.gsub(/\\/, '\\\\\\\\').
28
+ gsub(/\(/, '\\(').
29
+ gsub(/\)/, '\\)').
30
+ gsub(/&lt;/, '<').
31
+ gsub(/&gt;/, '>').
32
+ gsub(/&amp;/, '&')
33
+ end
34
+ end
35
+ end
36
+
37
+ require 'pdf/math'
38
+ require 'pdf/writer/lang'
39
+ require 'pdf/writer/lang/en'
40
+
41
+ begin
42
+ require 'zlib'
43
+ PDF::Writer::Compression = true
44
+ rescue LoadError
45
+ warn PDF::Writer::Lang[:no_zlib_no_compress]
46
+ PDF::Writer::Compression = false
47
+ end
48
+
49
+ require 'pdf/writer/arc4'
50
+ require 'pdf/writer/fontmetrics'
51
+ require 'pdf/writer/object'
52
+ require 'pdf/writer/object/action'
53
+ require 'pdf/writer/object/annotation'
54
+ require 'pdf/writer/object/catalog'
55
+ require 'pdf/writer/object/contents'
56
+ require 'pdf/writer/object/destination'
57
+ require 'pdf/writer/object/encryption'
58
+ require 'pdf/writer/object/font'
59
+ require 'pdf/writer/object/fontdescriptor'
60
+ require 'pdf/writer/object/fontencoding'
61
+ require 'pdf/writer/object/image'
62
+ require 'pdf/writer/object/info'
63
+ require 'pdf/writer/object/outlines'
64
+ require 'pdf/writer/object/outline'
65
+ require 'pdf/writer/object/page'
66
+ require 'pdf/writer/object/pages'
67
+ require 'pdf/writer/object/procset'
68
+ require 'pdf/writer/object/viewerpreferences'
69
+
70
+ require 'pdf/writer/ohash'
71
+ require 'pdf/writer/strokestyle'
72
+ require 'pdf/writer/graphics'
73
+ require 'pdf/writer/graphics/imageinfo'
74
+ require 'pdf/writer/state'
75
+
76
+ class PDF::Writer
77
+ # The system font path. The sytem font path will be determined
78
+ # differently for each operating system.
79
+ #
80
+ # Win32:: Uses ENV['SystemRoot']/Fonts as the system font path. There is
81
+ # an extension that will handle this better, but until and
82
+ # unless it is distributed with the standard Ruby Windows
83
+ # installer, PDF::Writer will not depend upon it.
84
+ # OS X:: The fonts are found in /System/Library/Fonts.
85
+ # Linux:: The font path list will be found (usually) in
86
+ # /etc/fonts/fonts.conf or /usr/etc/fonts/fonts.conf. This XML
87
+ # file will be parsed (using REXML) to provide the value for
88
+ # FONT_PATH.
89
+ FONT_PATH = []
90
+
91
+ class << self
92
+ require 'rexml/document'
93
+ # Parse the fonts.conf XML file.
94
+ def parse_fonts_conf(filename)
95
+ doc = REXML::Document.new(File.open(filename, "rb")).root rescue nil
96
+
97
+ if doc
98
+ path = REXML::XPath.match(doc, '//dir').map do |el|
99
+ el.text.gsub($/, '')
100
+ end
101
+ doc = nil
102
+ else
103
+ path = []
104
+ end
105
+ path
106
+ end
107
+ private :parse_fonts_conf
108
+ end
109
+
110
+ case RUBY_PLATFORM
111
+ when /mswin32/o
112
+ # Windows font path. This is not the most reliable method.
113
+ FONT_PATH << File.join(ENV['SystemRoot'], 'Fonts')
114
+ when /darwin/o
115
+ # Macintosh font path.
116
+ FONT_PATH << '/System/Library/Fonts'
117
+ else
118
+ FONT_PATH.push(*parse_fonts_conf('/etc/fonts/fonts.conf'))
119
+ FONT_PATH.push(*parse_fonts_conf('//usr/etc/fonts/fonts.conf'))
120
+ end
121
+
122
+ FONT_PATH.uniq!
123
+
124
+ include PDF::Writer::Graphics
125
+
126
+ # Contains all of the PDF objects, ready for final assembly. This is of
127
+ # no interest to external consumers.
128
+ attr_reader :objects #:nodoc:
129
+
130
+ # The ARC4 encryption object. This is of no interest to external
131
+ # consumers.
132
+ attr_reader :arc4 #:nodoc:
133
+ # The string that will be used to encrypt this PDF document.
134
+ attr_accessor :encryption_key
135
+
136
+ # The number of PDF objects in the document
137
+ def size
138
+ @objects.size
139
+ end
140
+
141
+ # Generate an ID for a new PDF object.
142
+ def generate_id
143
+ @mutex.synchronize { @current_id += 1 }
144
+ end
145
+ private :generate_id
146
+
147
+ # Generate a new font ID.
148
+ def generate_font_id
149
+ @mutex.synchronize { @current_font_id += 1 }
150
+ end
151
+ private :generate_font_id
152
+
153
+ class << self
154
+ # Create the document with prepress options. Uses the same options as
155
+ # PDF::Writer.new (<tt>:paper</tt>, <tt>:orientation</tt>, and
156
+ # <tt>:version</tt>). It also supports the following options:
157
+ #
158
+ # <tt>:left_margin</tt>:: The left margin.
159
+ # <tt>:right_margin</tt>:: The right margin.
160
+ # <tt>:top_margin</tt>:: The top margin.
161
+ # <tt>:bottom_margin</tt>:: The bottom margin.
162
+ # <tt>:bleed_size</tt>:: The size of the bleed area in points.
163
+ # Default 12.
164
+ # <tt>:mark_length</tt>:: The length of the prepress marks in
165
+ # points. Default 18.
166
+ #
167
+ # The prepress marks are added to the loose objects and will appear on
168
+ # all pages.
169
+ def prepress(options = { })
170
+ pdf = self.new(options)
171
+
172
+ bleed_size = options[:bleed_size] || 12
173
+ mark_length = options[:mark_length] || 18
174
+
175
+ pdf.left_margin = options[:left_margin] if options[:left_margin]
176
+ pdf.right_margin = options[:right_margin] if options[:right_margin]
177
+ pdf.top_margin = options[:top_margin] if options[:top_margin]
178
+ pdf.bottom_margin = options[:bottom_margin] if options[:bottom_margin]
179
+
180
+ # This is in an "odd" order because the y-coordinate system in PDF
181
+ # is from bottom to top.
182
+ tx0 = pdf.pages.media_box[0] + pdf.left_margin
183
+ ty0 = pdf.pages.media_box[3] - pdf.top_margin
184
+ tx1 = pdf.pages.media_box[2] - pdf.right_margin
185
+ ty1 = pdf.pages.media_box[1] + pdf.bottom_margin
186
+
187
+ bx0 = tx0 - bleed_size
188
+ by0 = ty0 - bleed_size
189
+ bx1 = tx1 + bleed_size
190
+ by1 = ty1 + bleed_size
191
+
192
+ pdf.pages.trim_box = [ tx0, ty0, tx1, ty1 ]
193
+ pdf.pages.bleed_box = [ bx0, by0, bx1, by1 ]
194
+
195
+ all = pdf.open_object
196
+ pdf.save_state
197
+ k = Color::CMYK.new(0, 0, 0, 100)
198
+ pdf.stroke_color! k
199
+ pdf.fill_color! k
200
+ pdf.stroke_style! StrokeStyle.new(0.3)
201
+
202
+ pdf.prepress_clip_mark(tx1, ty0, 0, mark_length, bleed_size) # Upper Right
203
+ pdf.prepress_clip_mark(tx0, ty0, 90, mark_length, bleed_size) # Upper Left
204
+ pdf.prepress_clip_mark(tx0, ty1, 180, mark_length, bleed_size) # Lower Left
205
+ pdf.prepress_clip_mark(tx1, ty1, -90, mark_length, bleed_size) # Lower Right
206
+
207
+ mid_x = pdf.pages.media_box[2] / 2.0
208
+ mid_y = pdf.pages.media_box[3] / 2.0
209
+
210
+ pdf.prepress_center_mark(mid_x, ty0, 0, mark_length, bleed_size) # Centre Top
211
+ pdf.prepress_center_mark(tx0, mid_y, 90, mark_length, bleed_size) # Centre Left
212
+ pdf.prepress_center_mark(mid_x, ty1, 180, mark_length, bleed_size) # Centre Bottom
213
+ pdf.prepress_center_mark(tx1, mid_y, -90, mark_length, bleed_size) # Centre Right
214
+
215
+ pdf.restore_state
216
+ pdf.close_object
217
+ pdf.add_object(all, :all)
218
+
219
+ yield pdf if block_given?
220
+
221
+ pdf
222
+ end
223
+
224
+ # Convert a measurement in centimetres to points, which are the
225
+ # default PDF userspace units.
226
+ def cm2pts(x)
227
+ (x / 2.54) * 72
228
+ end
229
+
230
+ # Convert a measurement in millimetres to points, which are the
231
+ # default PDF userspace units.
232
+ def mm2pts(x)
233
+ (x / 25.4) * 72
234
+ end
235
+
236
+ # Convert a measurement in inches to points, which are the default PDF
237
+ # userspace units.
238
+ def in2pts(x)
239
+ x * 72
240
+ end
241
+ end
242
+
243
+ # Convert a measurement in centimetres to points, which are the default
244
+ # PDF userspace units.
245
+ def cm2pts(x)
246
+ PDF::Writer.cm2pts(x)
247
+ end
248
+
249
+ # Convert a measurement in millimetres to points, which are the default
250
+ # PDF userspace units.
251
+ def mm2pts(x)
252
+ PDF::Writer.mm2pts(x)
253
+ end
254
+
255
+ # Convert a measurement in inches to points, which are the default PDF
256
+ # userspace units.
257
+ def in2pts(x)
258
+ PDF::Writer.in2pts(x)
259
+ end
260
+
261
+ # Standard page size names. One of these may be provided to
262
+ # PDF::Writer.new as the <tt>:paper</tt> parameter.
263
+ #
264
+ # Page sizes supported are:
265
+ #
266
+ # * 4A0, 2A0
267
+ # * A0, A1 A2, A3, A4, A5, A6, A7, A8, A9, A10
268
+ # * B0, B1, B2, B3, B4, B5, B6, B7, B8, B9, B10
269
+ # * C0, C1, C2, C3, C4, C5, C6, C7, C8, C9, C10
270
+ # * RA0, RA1, RA2, RA3, RA4
271
+ # * SRA0, SRA1, SRA2, SRA3, SRA4
272
+ # * LETTER
273
+ # * LEGAL
274
+ # * FOLIO
275
+ # * EXECUTIVE
276
+ PAGE_SIZES = { # :value {...}:
277
+ "4A0" => [0, 0, 4767.87, 6740.79], "2A0" => [0, 0, 3370.39, 4767.87],
278
+ "A0" => [0, 0, 2383.94, 3370.39], "A1" => [0, 0, 1683.78, 2383.94],
279
+ "A2" => [0, 0, 1190.55, 1683.78], "A3" => [0, 0, 841.89, 1190.55],
280
+ "A4" => [0, 0, 595.28, 841.89], "A5" => [0, 0, 419.53, 595.28],
281
+ "A6" => [0, 0, 297.64, 419.53], "A7" => [0, 0, 209.76, 297.64],
282
+ "A8" => [0, 0, 147.40, 209.76], "A9" => [0, 0, 104.88, 147.40],
283
+ "A10" => [0, 0, 73.70, 104.88], "B0" => [0, 0, 2834.65, 4008.19],
284
+ "B1" => [0, 0, 2004.09, 2834.65], "B2" => [0, 0, 1417.32, 2004.09],
285
+ "B3" => [0, 0, 1000.63, 1417.32], "B4" => [0, 0, 708.66, 1000.63],
286
+ "B5" => [0, 0, 498.90, 708.66], "B6" => [0, 0, 354.33, 498.90],
287
+ "B7" => [0, 0, 249.45, 354.33], "B8" => [0, 0, 175.75, 249.45],
288
+ "B9" => [0, 0, 124.72, 175.75], "B10" => [0, 0, 87.87, 124.72],
289
+ "C0" => [0, 0, 2599.37, 3676.54], "C1" => [0, 0, 1836.85, 2599.37],
290
+ "C2" => [0, 0, 1298.27, 1836.85], "C3" => [0, 0, 918.43, 1298.27],
291
+ "C4" => [0, 0, 649.13, 918.43], "C5" => [0, 0, 459.21, 649.13],
292
+ "C6" => [0, 0, 323.15, 459.21], "C7" => [0, 0, 229.61, 323.15],
293
+ "C8" => [0, 0, 161.57, 229.61], "C9" => [0, 0, 113.39, 161.57],
294
+ "C10" => [0, 0, 79.37, 113.39], "RA0" => [0, 0, 2437.80, 3458.27],
295
+ "RA1" => [0, 0, 1729.13, 2437.80], "RA2" => [0, 0, 1218.90, 1729.13],
296
+ "RA3" => [0, 0, 864.57, 1218.90], "RA4" => [0, 0, 609.45, 864.57],
297
+ "SRA0" => [0, 0, 2551.18, 3628.35], "SRA1" => [0, 0, 1814.17, 2551.18],
298
+ "SRA2" => [0, 0, 1275.59, 1814.17], "SRA3" => [0, 0, 907.09, 1275.59],
299
+ "SRA4" => [0, 0, 637.80, 907.09], "LETTER" => [0, 0, 612.00, 792.00],
300
+ "LEGAL" => [0, 0, 612.00, 1008.00], "FOLIO" => [0, 0, 612.00, 936.00],
301
+ "EXECUTIVE" => [0, 0, 521.86, 756.00]
302
+ }
303
+
304
+ # Creates a new PDF document as a writing canvas. It accepts three named
305
+ # parameters:
306
+ #
307
+ # <tt>:paper</tt>:: Specifies the size of the default page in
308
+ # PDF::Writer. This may be a four-element array
309
+ # of coordinates specifying the lower-left
310
+ # <tt>(xll, yll)</tt> and upper-right <tt>(xur,
311
+ # yur)</tt> corners, a two-element array of
312
+ # width and height in centimetres, or a page
313
+ # name as defined in PAGE_SIZES.
314
+ # <tt>:orientation</tt>:: The orientation of the page, either long
315
+ # (:portrait) or wide (:landscape). This may be
316
+ # used to swap the width and the height of the
317
+ # page.
318
+ # <tt>:version</tt>:: The feature set available to the document is
319
+ # limited by the PDF version. Setting this
320
+ # version restricts the feature set available to
321
+ # PDF::Writer. PDF::Writer currently supports
322
+ # PDF version 1.3 features and does not yet
323
+ # support advanced features from PDF 1.4, 1.5,
324
+ # or 1.6.
325
+ def initialize(options = {})
326
+ paper = options[:paper] || "LETTER"
327
+ orientation = options[:orientation] || :portrait
328
+ version = options[:version] || PDF_VERSION_13
329
+
330
+ @mutex = Mutex.new
331
+ @current_id = @current_font_id = 0
332
+
333
+ # Start the document
334
+ @objects = []
335
+ @callbacks = []
336
+ @font_families = {}
337
+ @fonts = {}
338
+ @stack = []
339
+ @state_stack = StateStack.new
340
+ @loose_objects = []
341
+ @current_text_state = ""
342
+ @options = {}
343
+ @destinations = {}
344
+ @add_loose_objects = {}
345
+ @images = []
346
+ @word_space_adjust = nil
347
+ @current_stroke_style = PDF::Writer::StrokeStyle.new(1)
348
+ @page_numbering = nil
349
+ @arc4 = nil
350
+ @encryption = nil
351
+ @file_identifier = nil
352
+
353
+ @columns = {}
354
+ @columns_on = false
355
+ @insert_mode = nil
356
+
357
+ @catalog = PDF::Writer::Object::Catalog.new(self)
358
+ @outlines = PDF::Writer::Object::Outlines.new(self)
359
+ @pages = PDF::Writer::Object::Pages.new(self)
360
+
361
+ @current_node = @pages
362
+
363
+ @current_text_render_style = 0
364
+
365
+ @procset = PDF::Writer::Object::Procset.new(self)
366
+ @info = PDF::Writer::Object::Info.new(self)
367
+ @page = PDF::Writer::Object::Page.new(self)
368
+
369
+ @first_page = @page
370
+
371
+ @version = version
372
+
373
+ # Initialize the default font families.
374
+ init_font_families
375
+
376
+ # Items formerly in EZWriter
377
+ @font_size = 10
378
+ @pageset = []
379
+
380
+ if paper.kind_of?(Array)
381
+ if paper.size == 4
382
+ size = paper # Coordinate Array
383
+ else
384
+ size = [0, 0, PDF::Writer.cm2pts(paper[0]), PDF::Writer.cm2pts(paper[1])]
385
+ # Paper size in centimeters has been passed
386
+ end
387
+ else
388
+ size = PAGE_SIZES[paper.upcase].dup
389
+ end
390
+ size[3], size[2] = size[2], size[3] if orientation == :landscape
391
+
392
+ @pages.media_box = size
393
+
394
+ @page_width = size[2] - size[0]
395
+ @page_height = size[3] - size[1]
396
+ @y = @page_height
397
+
398
+ # Also set the margins to some reasonable defaults -- 1.27 cm, 36pt,
399
+ # or 0.5 inches.
400
+ margins_pt(36)
401
+
402
+ # Set the current writing position to the top of the first page
403
+ @y = absolute_top_margin
404
+ # Get the ID of the page that was created during the instantiation
405
+ # process.
406
+ @pageset[1] = @pages.first_page
407
+
408
+ fill_color! Color::Black
409
+ stroke_color! Color::Black
410
+
411
+ yield self if block_given?
412
+ end
413
+
414
+ PDF_VERSION_13 = '1.3'
415
+ PDF_VERSION_14 = '1.4'
416
+ PDF_VERSION_15 = '1.5'
417
+ PDF_VERSION_16 = '1.6'
418
+
419
+ # The version of PDF to which this document conforms. Should be one of
420
+ # PDF_VERSION_13, PDF_VERSION_14, PDF_VERSION_15, or PDF_VERSION_16.
421
+ attr_reader :version
422
+ # The document catalog object (PDF::Writer::Object::Catalog). The
423
+ # options in the catalog should be set with PDF::Writer#open_here,
424
+ # PDF::Writer#viewer_preferences, and PDF::Writer#page_mode.
425
+ #
426
+ # This is of little interest to external clients.
427
+ attr_accessor :catalog #:nodoc:
428
+ # The PDF::Writer::Object::Pages object. This is of little interest to
429
+ # external clients.
430
+ attr_accessor :pages #:nodoc:
431
+
432
+ # The PDF::Writer::Object::Procset object. This is of little interest to
433
+ # external clients.
434
+ attr_accessor :procset #:nodoc:
435
+ # Sets the document to compressed (+true+) or uncompressed (+false+).
436
+ # Defaults to uncompressed.
437
+ attr_accessor :compressed
438
+ # Returns +true+ if the document is compressed.
439
+ def compressed?
440
+ @compressed == true
441
+ end
442
+ # The set of known labelled destinations. All destinations are of class
443
+ # PDF::Writer::Object::Destination. This is of little interest to
444
+ # external clients.
445
+ attr_reader :destinations #:nodoc:
446
+ # The PDF::Writer::Object::Info info object. This is used to provide
447
+ # certain metadata.
448
+ attr_reader :info
449
+ # The current page for writing. This is of little interest to external
450
+ # clients.
451
+ attr_accessor :current_page #:nodoc:
452
+ # Returns the current contents object to which raw PDF instructions may
453
+ # be written.
454
+ attr_reader :current_contents
455
+ # The PDF::Writer::Object::Outlines object. This is currently used very
456
+ # little. This is of little interest to external clients.
457
+ attr_reader :outlines #:nodoc:
458
+
459
+ # The complete set of page objects. This is of little interest to
460
+ # external consumers.
461
+ attr_reader :pageset #:nodoc:
462
+
463
+ attr_accessor :left_margin
464
+ attr_accessor :right_margin
465
+ attr_accessor :top_margin
466
+ attr_accessor :bottom_margin
467
+ attr_reader :page_width
468
+ attr_reader :page_height
469
+
470
+ # The absolute x position of the left margin.
471
+ attr_reader :absolute_left_margin
472
+ def absolute_left_margin #:nodoc:
473
+ @left_margin
474
+ end
475
+ # The absolute x position of the right margin.
476
+ attr_reader :absolute_right_margin
477
+ def absolute_right_margin #:nodoc:
478
+ @page_width - @right_margin
479
+ end
480
+ # Returns the absolute y position of the top margin.
481
+ attr_reader :absolute_top_margin
482
+ def absolute_top_margin #:nodoc:
483
+ @page_height - @top_margin
484
+ end
485
+ # Returns the absolute y position of the bottom margin.
486
+ attr_reader :absolute_bottom_margin
487
+ def absolute_bottom_margin #:nodoc:
488
+ @bottom_margin
489
+ end
490
+
491
+ # The height of the margin area.
492
+ attr_reader :margin_height
493
+ def margin_height #:nodoc:
494
+ absolute_top_margin - absolute_bottom_margin
495
+ end
496
+ # The width of the margin area.
497
+ attr_reader :margin_width
498
+ def margin_width #:nodoc:
499
+ absolute_right_margin - absolute_left_margin
500
+ end
501
+ # The absolute x middle position.
502
+ attr_reader :absolute_x_middle
503
+ def absolute_x_middle #:nodoc:
504
+ @page_width / 2.0
505
+ end
506
+ # The absolute y middle position.
507
+ attr_reader :absolute_y_middle
508
+ def absolute_y_middle #:nodoc:
509
+ @page_height / 2.0
510
+ end
511
+ # The middle of the writing area between the left and right margins.
512
+ attr_reader :margin_x_middle
513
+ def margin_x_middle #:nodoc:
514
+ (absolute_right_margin + absolute_left_margin) / 2.0
515
+ end
516
+ # The middle of the writing area between the top and bottom margins.
517
+ attr_reader :margin_y_middle
518
+ def margin_y_middle #:nodoc:
519
+ (absolute_top_margin + absolute_bottom_margin) / 2.0
520
+ end
521
+
522
+ # The vertical position of the writing point. The vertical position is
523
+ # constrained between the top and bottom margins. Any attempt to set it
524
+ # outside of those margins will cause the y pointer to be placed
525
+ # absolutely at the margins.
526
+ attr_accessor :y
527
+ def y=(yy) #:nodoc:
528
+ @y = yy
529
+ @y = absolute_top_margin if @y > absolute_top_margin
530
+ @y = @bottom_margin if @y < @bottom_margin
531
+ end
532
+
533
+ # The vertical position of the writing point. If the vertical position
534
+ # is outside of the bottom margin, a new page will be created.
535
+ attr_accessor :pointer
536
+ def pointer=(y) #:nodoc:
537
+ @y = y
538
+ start_new_page if @y < @bottom_margin
539
+ end
540
+
541
+ # Used to change the vertical position of the writing point. The pointer
542
+ # is moved *down* the page by +dy+ (that is, #y is reduced by +dy+), so
543
+ # if the pointer is to be moved up, a negative number must be used.
544
+ # Moving up the page will not move to the previous page because of
545
+ # limitations in the way that PDF::Writer works. The writing point will
546
+ # be limited to the top margin position.
547
+ #
548
+ # If +make_space+ is true and a new page is forced, then the pointer
549
+ # will be moved down on the new page. This will allow space to be
550
+ # reserved for graphics.
551
+ def move_pointer(dy, make_space = false)
552
+ @y -= dy
553
+ if @y < @bottom_margin
554
+ start_new_page
555
+ @y -= dy if make_space
556
+ elsif @y > absolute_top_margin
557
+ @y = absolute_top_margin
558
+ end
559
+ end
560
+
561
+ # Define the margins in millimetres.
562
+ def margins_mm(top, left = top, bottom = top, right = left)
563
+ margins_pt(mm2pts(top), mm2pts(bottom), mm2pts(left), mm2pts(right))
564
+ end
565
+
566
+ # Define the margins in centimetres.
567
+ def margins_cm(top, left = top, bottom = top, right = left)
568
+ margins_pt(cm2pts(top), cm2pts(bottom), cm2pts(left), cm2pts(right))
569
+ end
570
+
571
+ # Define the margins in inches.
572
+ def margins_in(top, left = top, bottom = top, right = left)
573
+ margins_pt(in2pts(top), in2pts(bottom), in2pts(left), in2pts(right))
574
+ end
575
+
576
+ # Define the margins in points. This will move the #y pointer
577
+ #
578
+ # # T L B R
579
+ # pdf.margins_pt(36) # 36 36 36 36
580
+ # pdf.margins_pt(36, 54) # 36 54 36 54
581
+ # pdf.margins_pt(36, 54, 72) # 36 54 72 54
582
+ # pdf.margins_pt(36, 54, 72, 90) # 36 54 72 90
583
+ def margins_pt(top, left = top, bottom = top, right = left)
584
+ # Set the margins to new values
585
+ @top_margin = top
586
+ @bottom_margin = bottom
587
+ @left_margin = left
588
+ @right_margin = right
589
+ # Check to see if this means that the current writing position is
590
+ # outside the writable area
591
+ if @y > (@page_height - top)
592
+ # Move y down
593
+ @y = @page_height - top
594
+ end
595
+
596
+ start_new_page if @y < bottom # Make a new page
597
+ end
598
+
599
+ # Add a new translation table for a font family. A font family will be
600
+ # used to associate a single name and font styles with multiple fonts.
601
+ # A style will be identified with a single-character style identifier or
602
+ # a series of style identifiers. The only styles currently recognised
603
+ # are:
604
+ #
605
+ # +b+:: Bold (or heavy) fonts. Examples: Helvetica-Bold, Courier-Bold,
606
+ # Times-Bold.
607
+ # +i+:: Italic (or oblique) fonts. Examples: Helvetica-Oblique,
608
+ # Courier-Oblique, Times-Italic.
609
+ # +bi+:: Bold italic fonts. Examples Helvetica-BoldOblique,
610
+ # Courier-BoldOblique, Times-BoldItalic.
611
+ # +ib+:: Italic bold fonts. Generally defined the same as +bi+ font
612
+ # styles. Examples: Helvetica-BoldOblique, Courier-BoldOblique,
613
+ # Times-BoldItalic.
614
+ #
615
+ # Each font family key is the base name for the font.
616
+ attr_reader :font_families
617
+
618
+ # Allows the user to find out what the ID is of the first page that was
619
+ # created during startup - useful if they wish to add something to it
620
+ # later.
621
+ attr_reader :first_page
622
+
623
+ # Initialize the font families for the default fonts.
624
+ def init_font_families
625
+ # Set the known family groups. These font families will be used to
626
+ # enable bold and italic markers to be included within text
627
+ # streams. HTML forms will be used... <b></b> <i></i>
628
+ @font_families["Helvetica"] =
629
+ {
630
+ "b" => 'Helvetica-Bold',
631
+ "i" => 'Helvetica-Oblique',
632
+ "bi" => 'Helvetica-BoldOblique',
633
+ "ib" => 'Helvetica-BoldOblique'
634
+ }
635
+ @font_families['Courier'] =
636
+ {
637
+ "b" => 'Courier-Bold',
638
+ "i" => 'Courier-Oblique',
639
+ "bi" => 'Courier-BoldOblique',
640
+ "ib" => 'Courier-BoldOblique'
641
+ }
642
+ @font_families['Times-Roman'] =
643
+ {
644
+ "b" => 'Times-Bold',
645
+ "i" => 'Times-Italic',
646
+ "bi" => 'Times-BoldItalic',
647
+ "ib" => 'Times-BoldItalic'
648
+ }
649
+ end
650
+ private :init_font_families
651
+
652
+ # Sets the trim box area.
653
+ def trim_box(x0, y0, x1, y1)
654
+ @pages.trim_box = [ x0, y0, x1, y1 ]
655
+ end
656
+
657
+ # Sets the bleed box area.
658
+ def bleed_box(x0, y0, x1, y1)
659
+ @pages.bleed_box = [ x0, y0, x1, y1 ]
660
+ end
661
+
662
+ # set the viewer preferences of the document, it is up to the browser to
663
+ # obey these.
664
+ def viewer_preferences(label, value = 0)
665
+ @catalog.viewer_preferences ||= PDF::Writer::Object::ViewerPreferences.new(self)
666
+
667
+ # This will only work if the label is one of the valid ones.
668
+ if label.kind_of?(Hash)
669
+ label.each { |kk, vv| @catalog.viewer_preferences.__send__("#{kk.downcase}=".intern, vv) }
670
+ else
671
+ @catalog.viewer_preferences.__send__("#{label.downcase}=".intern, value)
672
+ end
673
+ end
674
+
675
+ # Add a link in the document to an external URL.
676
+ def add_link(uri, x0, y0, x1, y1)
677
+ PDF::Writer::Object::Annotation.new(self, :link, [x0, y0, x1, y1], uri)
678
+ end
679
+
680
+ # Add a link in the document to an internal destination (ie. within the
681
+ # document)
682
+ def add_internal_link(label, x0, y0, x1, y1)
683
+ PDF::Writer::Object::Annotation.new(self, :ilink, [x0, y0, x1, y1], label)
684
+ end
685
+
686
+ # Add an outline item (Bookmark).
687
+ def add_outline_item(label, title = label)
688
+ PDF::Writer::Object::Outline.new(self, label, title)
689
+ end
690
+
691
+ # Standard encryption/DRM options.
692
+ ENCRYPT_OPTIONS = { #:nodoc:
693
+ :print => 4,
694
+ :modify => 8,
695
+ :copy => 16,
696
+ :add => 32
697
+ }
698
+
699
+ # Encrypts the document. This will set the user and owner passwords that
700
+ # will be used to access the document and set the permissions the user
701
+ # has with the document. The passwords are limited to 32 characters.
702
+ #
703
+ # The permissions provided are an array of symbols, allowing identified
704
+ # users to perform particular actions:
705
+ # <tt>:print</tt>:: Print.
706
+ # <tt>:modify</tt>:: Modify text or objects.
707
+ # <tt>:copy</tt>:: Copy text or objects.
708
+ # <tt>:add</tt>:: Add text or objects.
709
+ def encrypt(user_pass = nil, owner_pass = nil, permissions = [])
710
+ perms = ["11000000"].pack("B8")
711
+
712
+ permissions.each do |perm|
713
+ perms += ENCRYPT_OPTIONS[perm] if ENCRYPT_OPTIONS[perm]
714
+ end
715
+
716
+ @arc4 ||= PDF::ARC4.new
717
+ owner_pass ||= user_pass
718
+
719
+ options = {
720
+ :owner_pass => owner_pass,
721
+ :user_pass => user_pass,
722
+ :permissions => perms,
723
+ }
724
+ @encryption = PDF::Writer::Object::Encryption.new(self, options)
725
+ end
726
+
727
+ def encrypted?
728
+ not @encryption.nil?
729
+ end
730
+
731
+ # should be used for internal checks, not implemented as yet
732
+ def check_all_here
733
+ end
734
+
735
+ # Return the PDF stream as a string.
736
+ def render(debug = false)
737
+ clean_up
738
+ @compression = false if $DEBUG or debug
739
+ @arc4.init(@encryption_key) unless @arc4.nil?
740
+
741
+ check_all_here
742
+
743
+ xref = []
744
+
745
+ content = "%PDF-#{@version}\n%����\n"
746
+ pos = content.size
747
+
748
+ objects.each do |oo|
749
+ cont = oo.to_s
750
+ content << cont
751
+ xref << pos
752
+ pos += cont.size
753
+ end
754
+
755
+ # pos += 1 # Newline character before XREF
756
+
757
+ content << "\nxref\n0 #{xref.size + 1}\n0000000000 65535 f \n"
758
+ xref.each { |xx| content << "#{'%010d' % [xx]} 00000 n \n" }
759
+ content << "\ntrailer\n"
760
+ content << " << /Size #{xref.size + 1}\n"
761
+ content << " /Root 1 0 R\n /Info #{@info.oid} 0 R\n"
762
+ # If encryption has been applied to this document, then add the marker
763
+ # for this dictionary
764
+ if @arc4 and @encryption
765
+ content << "/Encrypt #{@encryption.oid} 0 R\n"
766
+ end
767
+
768
+ if @file_identifier
769
+ content << "/ID[<#{@file_identifier}><#{@file_identifier}>]\n"
770
+ end
771
+ content << " >>\nstartxref\n#{pos}\n%%EOF\n"
772
+ content
773
+ end
774
+ alias :to_s :render
775
+
776
+ # Loads the font metrics. This is now thread-safe.
777
+ def load_font_metrics(font)
778
+ metrics = PDF::Writer::FontMetrics.open(font)
779
+ @mutex.synchronize do
780
+ @fonts[font] = metrics
781
+ @fonts[font].font_num = @fonts.size
782
+ end
783
+ metrics
784
+ end
785
+ private :load_font_metrics
786
+
787
+ def find_font(fontname)
788
+ name = File.basename(fontname, ".afm")
789
+ @objects.detect do |oo|
790
+ oo.kind_of?(PDF::Writer::Object::Font) and /#{oo.basefont}$/ =~ name
791
+ end
792
+ end
793
+ private :find_font
794
+
795
+ def font_file(fontfile)
796
+ path = "#{fontfile}.pfb"
797
+ return path if File.exists?(path)
798
+ path = "#{fontfile}.ttf"
799
+ return path if File.exists?(path)
800
+ nil
801
+ end
802
+ private :font_file
803
+
804
+ def load_font(font, encoding = nil)
805
+ metrics = load_font_metrics(font)
806
+
807
+ name = File.basename(font).gsub(/\.afm$/o, "")
808
+
809
+ encoding_diff = nil
810
+ case encoding
811
+ when Hash
812
+ encoding_name = encoding[:encoding]
813
+ encoding_diff = encoding[:differences]
814
+ when NilClass
815
+ encoding_name = 'WinAnsiEncoding'
816
+ else
817
+ encoding_name = encoding
818
+ end
819
+
820
+ wfo = PDF::Writer::Object::Font.new(self, name, encoding_name)
821
+
822
+ # We have an Adobe Font Metrics (.afm) file. We need to find the
823
+ # associated Type1 (.pfb) or TrueType (.ttf) files (we do not yet
824
+ # support OpenType fonts); we need to load it into a
825
+ # PDF::Writer::Object and put the references into the metrics object.
826
+ base = metrics.path.sub(/\.afm$/o, "")
827
+ fontfile = font_file(base)
828
+ unless fontfile
829
+ base = File.basename(base)
830
+ FONT_PATH.each do |path|
831
+ fontfile = font_file(File.join(path, base))
832
+ break if fontfile
833
+ end
834
+ end
835
+
836
+ if font =~ /afm/o and fontfile
837
+ # Find the array of font widths, and put that into an object.
838
+ first_char = -1
839
+ last_char = 0
840
+
841
+ widths = {}
842
+ metrics.c.each_value do |details|
843
+ num = details["C"]
844
+
845
+ if num >= 0
846
+ # warn "Multiple definitions of #{num}" if widths.has_key?(num)
847
+ widths[num] = details['WX']
848
+ first_char = num if num < first_char or first_char < 0
849
+ last_char = num if num > last_char
850
+ end
851
+ end
852
+
853
+ # Adjust the widths for the differences array.
854
+ if encoding_diff
855
+ encoding_diff.each do |cnum, cname|
856
+ # warn "Differences is ignored for now."
857
+ (cnum - last_char).times { widths << 0 } if cnum > last_char
858
+ last_char = cnum
859
+ widths[cnum - firstchar] = fonts.c[cname]['WX'] if metrics.c[cname]
860
+ end
861
+ end
862
+
863
+ widthid = PDF::Writer::Object::Contents.new(self, :raw)
864
+ widthid << "["
865
+ (first_char .. last_char).each do |ii|
866
+ if widths.has_key?(ii)
867
+ widthid << " #{widths[ii].to_i}"
868
+ else
869
+ widthid << " 0"
870
+ end
871
+ end
872
+ widthid << "]"
873
+
874
+ # Load the pfb file, and put that into an object too. Note that PDF
875
+ # supports only binary format Type 1 font files, though there is a
876
+ # simple utility to convert them from pfa to pfb.
877
+ data = nil
878
+ File.open(fbfile, "rb") { |ff| data = ff.read }
879
+
880
+ # Create the font descriptor.
881
+ fdsc = PDF::Writer::Object::FontDescriptor.new(self)
882
+ # Raw contents causes problems with Acrobat Reader.
883
+ pfbc = PDF::Writer::Object::Contents.new(self)
884
+
885
+ # Determine flags (more than a little flakey, hopefully will not
886
+ # matter much).
887
+ flags = 0
888
+ flags += 2 ** 6 if metrics.italicangle.nonzero?
889
+ flags += 1 if metrics.isfixedpitch == "true"
890
+ flags += 2 ** 5 # Assume a non-symbolic font
891
+
892
+ list = {
893
+ 'Ascent' => 'Ascender',
894
+ 'CapHeight' => 'CapHeight',
895
+ 'Descent' => 'Descender',
896
+ 'FontBBox' => 'FontBBox',
897
+ 'ItalicAngle' => 'ItalicAngle'
898
+ }
899
+ fdopt = {
900
+ 'Flags' => flags,
901
+ 'FontName' => metrics.fontname,
902
+ 'StemV' => 100 # Don't know what the value for this should be!
903
+ }
904
+
905
+ list.each do |kk, vv|
906
+ zz = metrics.__send__(vv.downcase.intern)
907
+ fdopt[kk] = zz if zz
908
+ end
909
+
910
+ # Determine the cruicial lengths within this file
911
+ if fbtype =~ /\.pfb$/o
912
+ fdopt['FontFile'] = pfbc.oid
913
+ i1 = data.index('eexec') + 6
914
+ i2 = data.index('00000000') - i1
915
+ i3 = data.size - i2 - i1
916
+ pfbc.add('Length1' => i1, 'Length2' => i2, 'Length3' => i3)
917
+ elsif fbtype =~ /\.ttf$/o
918
+ fdopt['FontFile2'] = pfbc.oid
919
+ pfbc.add('Length1' => data.size)
920
+ end
921
+
922
+ fdsc.options = fdopt
923
+ # Embed the font program
924
+ pfbc << data
925
+
926
+ # Tell the font object about all this new stuff
927
+ tmp = {
928
+ 'BaseFont' => metrics.fontname,
929
+ 'Widths' => widthid.oid,
930
+ 'FirstChar' => first_char,
931
+ 'LastChar' => last_char,
932
+ 'FontDescriptor' => fdsc.oid
933
+ }
934
+ tmp['SubType'] = 'TrueType' if fbtype == "ttf"
935
+
936
+ tmp.each { |kk, vv| wfo.__send__("#{kk.downcase}=".intern, vv) }
937
+ end
938
+
939
+ # Also set the differences here. Note that this means that these will
940
+ # take effect only the first time that a font is selected, else they
941
+ # are ignored.
942
+ metrics.differences = encoding_diff unless encoding_diff.nil?
943
+ metrics.encoding = encoding_name
944
+ metrics
945
+ end
946
+ private :load_font
947
+
948
+ # If the named +font+ is not loaded, then load it and make the required
949
+ # PDF objects to represent the font. If the font is already loaded, then
950
+ # make it the current font.
951
+ #
952
+ # The parameter +encoding+ applies only when the font is first being
953
+ # loaded; it may not be applied later. It may either be an encoding name
954
+ # or a hash. The Hash must contain two keys:
955
+ #
956
+ # <tt>:encoding</tt>:: The name of the encoding. Either *none*,
957
+ # *WinAnsiEncoding*, *MacRomanEncoding*, or
958
+ # *MacExpertEncoding*. For symbolic fonts, an
959
+ # encoding of *none* is recommended with a
960
+ # differences Hash.
961
+ # <tt>:differences</tt>:: This Hash value is a mapping between character
962
+ # byte values (0 .. 255) and character names
963
+ # from the AFM file for the font.
964
+ #
965
+ # The standard PDF encodings are detailed fully in the PDF Reference
966
+ # version 1.6, Appendix D.
967
+ #
968
+ # Note that WinAnsiEncoding is not the same as Windows code page 1252
969
+ # (roughly equivalent to latin-1), Most characters map, but not all. The
970
+ # encoding value currently defaults to WinAnsiEncoding.
971
+ #
972
+ # If the font's "natural" encoding is desired, then it is necessary to
973
+ # specify the +encoding+ parameter as <tt>{ :encoding => nil }</tt>.
974
+ def select_font(font, encoding = nil)
975
+ load_font(font, encoding) unless @fonts[font]
976
+
977
+ @current_base_font = font
978
+ current_font!
979
+ @current_base_font
980
+ end
981
+
982
+ # Selects the current font based on defined font families and the
983
+ # current text state. As noted in #font_families, a "bi" font can be
984
+ # defined differently than an "ib" font. It should not be possible to
985
+ # have a "bb" text state, but if one were to show up, an entry for the
986
+ # #font_families would have to be defined to select anything other than
987
+ # the default font. This function is to be called whenever the current
988
+ # text state is changed; it will update the current font to whatever the
989
+ # appropriate font defined in the font family.
990
+ #
991
+ # When the user calls #select_font, both the current base font and the
992
+ # current font will be reset; this function only changes the current
993
+ # font, not the current base font.
994
+ #
995
+ # This will probably not be needed by end users.
996
+ def current_font!
997
+ select_font("Helvetica") unless @current_base_font
998
+
999
+ font = File.basename(@current_base_font)
1000
+ if @font_families[font] and @font_families[font][@current_text_state]
1001
+ # Then we are in some state or another and this font has a family,
1002
+ # and the current setting exists within it select the font, then
1003
+ # return it.
1004
+ if File.dirname(@current_base_font) != '.'
1005
+ nf = File.join(File.dirname(@current_base_font), @font_families[font][@current_text_state])
1006
+ else
1007
+ nf = @font_families[font][@current_text_state]
1008
+ end
1009
+
1010
+ unless @fonts[nf]
1011
+ enc = {
1012
+ :encoding => @fonts[font].encoding,
1013
+ :differences => @fonts[font].differences
1014
+ }
1015
+ load_font(nf, enc)
1016
+ end
1017
+ @current_font = nf
1018
+ else
1019
+ @current_font = @current_base_font
1020
+ end
1021
+ end
1022
+
1023
+ attr_reader :current_font
1024
+ attr_reader :current_base_font
1025
+ attr_accessor :font_size
1026
+
1027
+ # add content to the currently active object
1028
+ def add_content(cc)
1029
+ @current_contents << cc
1030
+ end
1031
+
1032
+ # Return the height in units of the current font in the given size.
1033
+ def font_height(size)
1034
+ select_font("Helvetica") if @fonts.empty?
1035
+ hh = @fonts[@current_font].fontbbox[3].to_f - @fonts[@current_font].fontbbox[1].to_f
1036
+ (size * hh / 1000.0)
1037
+ end
1038
+
1039
+ # Return the font descender, this will normally return a negative
1040
+ # number. If you add this number to the baseline, you get the level of
1041
+ # the bottom of the font it is in the PDF user units.
1042
+ def font_descender(size)
1043
+ select_font("Helvetica") if @fonts.empty?
1044
+ hi = @fonts[@current_font].fontbbox[1].to_f
1045
+ (size * hi / 1000.0)
1046
+ end
1047
+
1048
+ # Given a start position and information about how text is to be laid
1049
+ # out, calculate where on the page the text will end.
1050
+ def text_position(x, y, angle, size, wa, text)
1051
+ width = text_width(size, text)
1052
+ width += wa * (text.count(" "))
1053
+ rad = PDF::Math.deg2rad(angle)
1054
+ [Math.cos(rad) * width + x, ((-Math.sin(rad)) * width + y)]
1055
+ end
1056
+ private :text_position
1057
+
1058
+ # Wrapper function for #text_tags
1059
+ def quick_text_tags(text, ii, font_change)
1060
+ ret = text_tags(text, ii, font_change)
1061
+ [ret[0], ret[1], ret[2]]
1062
+ end
1063
+ private :quick_text_tags
1064
+
1065
+ # Matches tags.
1066
+ MATCH_TAG_REPLACE_RE = %r{^r:(\w+)(?: (.*?))? */} #:nodoc:
1067
+ MATCH_TAG_DRAW_ONE_RE = %r{^C:(\w+)(?: (.*?))? */} #:nodoc:
1068
+ MATCH_TAG_DRAW_PAIR_RE = %r{^c:(\w+)(?: (.*))? *} #:nodoc:
1069
+
1070
+ # Checks if +text+ contains a control tag at +pos+. Control tags are
1071
+ # XML-like tags that contain tag information.
1072
+ #
1073
+ # === Supported Tag Formats
1074
+ # <tt>&lt;b></tt>:: Adds +b+ to the end of the current
1075
+ # text state. If this is the closing
1076
+ # tag, <tt>&lt;/b></tt>, +b+ is removed
1077
+ # from the end of the current text
1078
+ # state.
1079
+ # <tt>&lt;i></tt>:: Adds +i+ to the end of the current
1080
+ # text state. If this is the closing
1081
+ # tag, <tt>&lt;/i</tt>, +i+ is removed
1082
+ # from the end of the current text
1083
+ # state.
1084
+ # <tt>&lt;r:TAG[ PARAMS]/></tt>:: Calls a stand-alone replace callback
1085
+ # method of the form tag_TAG_replace.
1086
+ # PARAMS must be separated from the TAG
1087
+ # name by a single space. The PARAMS, if
1088
+ # present, are passed to the replace
1089
+ # callback unmodified, whose
1090
+ # responsibility it is to interpret the
1091
+ # parameters. The replace callback is
1092
+ # expected to return text that will be
1093
+ # used in the place of the tag.
1094
+ # #text_tags is called again immediately
1095
+ # so that if the replacement text has
1096
+ # tags, they will be dealt with
1097
+ # properly.
1098
+ # <tt>&lt;C:TAG[ PARAMS]/></tt>:: Calls a stand-alone drawing callback
1099
+ # method. The method will be provided an
1100
+ # information hash (see below for the
1101
+ # data provided). It is expected to use
1102
+ # this information to perform whatever
1103
+ # drawing tasks are needed to perform
1104
+ # its task.
1105
+ # <tt>&lt;c:TAG[ PARAMS]></tt>:: Calls a paired drawing callback
1106
+ # method. The method will be provided an
1107
+ # information hash (see below for the
1108
+ # data provided). It is expected to use
1109
+ # this information to perform whatever
1110
+ # drawing tasks are needed to perform
1111
+ # its task. It must have a corresponding
1112
+ # &lt;/c:TAG> closing tag. Paired
1113
+ # callback behaviours will be preserved
1114
+ # over page breaks and line changes.
1115
+ #
1116
+ # Drawing callback tags will be provided an information hash that tells
1117
+ # the callback method where it must perform its drawing tasks.
1118
+ #
1119
+ # === Drawing Callback Parameters
1120
+ # <tt>:x</tt>:: The current X position of the text.
1121
+ # <tt>:y</tt>:: The current y position of the text.
1122
+ # <tt>:angle</tt>:: The current text drawing angle.
1123
+ # <tt>:params</tt>:: Any parameters that may be important to the
1124
+ # callback. This value is only guaranteed to have
1125
+ # meaning when a stand-alone callback is made or the
1126
+ # opening tag is processed.
1127
+ # <tt>:status</tt>:: :start, :end, :start_line, :end_line
1128
+ # <tt>:cbid</tt>:: The identifier of this callback. This may be
1129
+ # used as a key into a different variable where
1130
+ # state may be kept.
1131
+ # <tt>:callback</tt>:: The name of the callback function. Only set for
1132
+ # stand-alone or opening callback tags.
1133
+ # <tt>:height</tt>:: The font height.
1134
+ # <tt>:descender</tt>:: The font descender size.
1135
+ #
1136
+ # ==== <tt>:status</tt> Values and Meanings
1137
+ # <tt>:start</tt>:: The callback has been started. This applies
1138
+ # either when the callback is a stand-alone
1139
+ # callback (<tt>&lt;C:TAG/></tt>) or the opening
1140
+ # tag of a paired tag (<tt>&lt;c:TAG></tt>).
1141
+ # <tt>:end</tt>:: The callback has been manually terminated with
1142
+ # a closing tag (<tt>&lt;/c:TAG></tt>).
1143
+ # <tt>:start_line</tt>:: Called when a new line is to be drawn. This
1144
+ # allows the callback to perform any updates
1145
+ # necessary to permit paired callbacks to cross
1146
+ # line boundaries. This will usually involve
1147
+ # updating x, y positions.
1148
+ # <tt>:end_line</tt>:: Called when the end of a line is reached. This
1149
+ # permits the callback to perform any drawing
1150
+ # necessary to permit paired callbacks to cross
1151
+ # line boundaries.
1152
+ #
1153
+ # Drawing callback methods may return a hash of the <tt>:x</tt> and
1154
+ # <tt>:y</tt> position that the drawing pointer should take after the
1155
+ # callback is complete.
1156
+ #
1157
+ # === Known Callback Tags
1158
+ # <tt>&lt;c:alink URI></tt>:: makes an external link around text
1159
+ # between the opening and closing tags of
1160
+ # this callback. The URI may be any URL,
1161
+ # including http://, ftp://, and mailto:,
1162
+ # as long as there is a URL handler
1163
+ # registered. URI is of the form
1164
+ # uri="URI".
1165
+ # <tt>&lt;c:ilink DEST></tt>:: makes an internal link within the
1166
+ # document. The DEST must refer to a known
1167
+ # named destination within the document.
1168
+ # DEST is of the form dest="DEST".
1169
+ # <tt>&lt;c:uline></tt>:: underlines the specified text.
1170
+ # <tt>&lt;C:bullet></tt>:: Draws a solid bullet at the tag
1171
+ # position.
1172
+ # <tt>&lt;C:disc></tt>:: Draws a disc bullet at the tag position.
1173
+ def text_tags(text, pos, font_change, final = false, x = 0, y = 0, size = 0, angle = 0, word_space_adjust = 0)
1174
+ tag_size = 0
1175
+
1176
+ tag_match = %r!^<(/)?([^>]+)>!.match(text[pos..-1])
1177
+
1178
+ if tag_match
1179
+ closed, tag_name = tag_match.captures
1180
+ cts = @current_text_state # Alias for shorter lines.
1181
+ tag_size = tag_name.size + 2 + (closed ? 1 : 0)
1182
+
1183
+ case tag_name
1184
+ when %r{^(?:b|strong)$}o
1185
+ if closed
1186
+ cts.slice!(-1, 1) if ?b == cts[-1]
1187
+ else
1188
+ cts << ?b
1189
+ end
1190
+ when %r{^(?:i|em)$}o
1191
+ if closed
1192
+ cts.slice!(-1, 1) if ?i == cts[-1]
1193
+ else
1194
+ cts << ?i
1195
+ end
1196
+ when %r{^r:}o
1197
+ _match = MATCH_TAG_REPLACE_RE.match(tag_name)
1198
+ if _match.nil?
1199
+ warn PDF::Writer::Lang[:callback_warning] % [ 'r:', tag_name ]
1200
+ tag_size = 0
1201
+ else
1202
+ func = _match.captures[0]
1203
+ params = parse_tag_params(_match.captures[1] || "")
1204
+ tag = TAGS[:replace][func]
1205
+
1206
+ if tag
1207
+ text[pos, tag_size] = tag[self, params]
1208
+ tag_size, text, font_change, x, y = text_tags(text, pos, font_change,
1209
+ final, x, y, size,
1210
+ angle,
1211
+ word_space_adjust)
1212
+ else
1213
+ warn PDF::Writer::Lang[:callback_warning] % [ 'r:', func ]
1214
+ tag_size = 0
1215
+ end
1216
+ end
1217
+ when %r{^C:}o
1218
+ _match = MATCH_TAG_DRAW_ONE_RE.match(tag_name)
1219
+ if _match.nil?
1220
+ warn PDF::Writer::Lang[:callback_warning] % [ 'C:', tag_name ]
1221
+ tag_size = 0
1222
+ else
1223
+ func = _match.captures[0]
1224
+ params = parse_tag_params(_match.captures[1] || "")
1225
+ tag = TAGS[:single][func]
1226
+
1227
+ if tag
1228
+ font_change = false
1229
+
1230
+ if final
1231
+ # Only call the function if this is the "final" call. Assess
1232
+ # the text position. Calculate the text width to this point.
1233
+ x, y = text_position(x, y, angle, size, word_space_adjust,
1234
+ text[0, pos])
1235
+ info = {
1236
+ :x => x,
1237
+ :y => y,
1238
+ :angle => angle,
1239
+ :params => params,
1240
+ :status => :start,
1241
+ :cbid => @callbacks.size + 1,
1242
+ :callback => func,
1243
+ :height => font_height(size),
1244
+ :descender => font_descender(size)
1245
+ }
1246
+
1247
+ ret = tag[self, info]
1248
+ if ret.kind_of?(Hash)
1249
+ ret.each do |rk, rv|
1250
+ x = rv if rk == :x
1251
+ y = rv if rk == :y
1252
+ font_change = rv if rk == :font_change
1253
+ end
1254
+ end
1255
+ end
1256
+ else
1257
+ warn PDF::Writer::Lang[:callback_Warning] % [ 'C:', func ]
1258
+ tag_size = 0
1259
+ end
1260
+ end
1261
+ when %r{^c:}o
1262
+ _match = MATCH_TAG_DRAW_PAIR_RE.match(tag_name)
1263
+
1264
+ if _match.nil?
1265
+ warn PDF::Writer::Lang[:callback_warning] % [ 'c:', tag_name ]
1266
+ tag_size = 0
1267
+ else
1268
+ func = _match.captures[0]
1269
+ params = parse_tag_params(_match.captures[1] || "")
1270
+ tag = TAGS[:pair][func]
1271
+
1272
+ if tag
1273
+ font_change = false
1274
+
1275
+ if final
1276
+ # Only call the function if this is the "final" call. Assess
1277
+ # the text position. Calculate the text width to this point.
1278
+ x, y = text_position(x, y, angle, size, word_space_adjust,
1279
+ text[0, pos])
1280
+ info = {
1281
+ :x => x,
1282
+ :y => y,
1283
+ :angle => angle,
1284
+ :params => params,
1285
+ }
1286
+
1287
+ if closed
1288
+ info[:status] = :end
1289
+ info[:cbid] = @callbacks.size
1290
+
1291
+ ret = tag[self, info]
1292
+
1293
+ if ret.kind_of?(Hash)
1294
+ ret.each do |rk, rv|
1295
+ x = rv if rk == :x
1296
+ y = rv if rk == :y
1297
+ font_change = rv if rk == :font_change
1298
+ end
1299
+ end
1300
+
1301
+ @callbacks.pop
1302
+ else
1303
+ info[:status] = :start
1304
+ info[:cbid] = @callbacks.size + 1
1305
+ info[:tag] = tag
1306
+ info[:callback] = func
1307
+ info[:height] = font_height(size)
1308
+ info[:descender] = font_descender(size)
1309
+
1310
+ @callbacks << info
1311
+
1312
+ ret = tag[self, info]
1313
+
1314
+ if ret.kind_of?(Hash)
1315
+ ret.each do |rk, rv|
1316
+ x = rv if rk == :x
1317
+ y = rv if rk == :y
1318
+ font_change = rv if rk == :font_change
1319
+ end
1320
+ end
1321
+ end
1322
+ end
1323
+ else
1324
+ warn PDF::Writer::Lang[:callback_warning] % [ 'c:', func ]
1325
+ tag_size = 0
1326
+ end
1327
+ end
1328
+ else
1329
+ tag_size = 0
1330
+ end
1331
+ end
1332
+ [ tag_size, text, font_change, x, y ]
1333
+ end
1334
+ private :text_tags
1335
+
1336
+ TAG_PARAM_RE = %r{(\w+)=(?:"([^"]+)"|'([^']+)'|(\w+))} #:nodoc:
1337
+
1338
+ def parse_tag_params(params)
1339
+ params ||= ""
1340
+ ph = {}
1341
+ params.scan(TAG_PARAM_RE) do |param|
1342
+ ph[param[0]] = param[1] || param[2] || param[3]
1343
+ end
1344
+ ph
1345
+ end
1346
+ private :parse_tag_params
1347
+
1348
+ # Add text to the document at the x, y location with the text size at
1349
+ # the specified angle.
1350
+ def add_text(x, y, size, text, angle = 0, word_space_adjust = 0)
1351
+ select_font("Helvetica") if @fonts.empty?
1352
+
1353
+ text = text.to_s
1354
+
1355
+ # If there are any open callbacks, then they should be called, to show
1356
+ # the start of the line
1357
+ @callbacks.reverse_each do |ii|
1358
+ info = ii.dup
1359
+ info[:x] = x
1360
+ info[:y] = y
1361
+ info[:angle] = angle
1362
+ info[:status] = :start_line
1363
+
1364
+ info[:tag][self, info]
1365
+ end
1366
+ if angle == 0
1367
+ add_content("\nBT %.3f %.3f Td" % [x, y])
1368
+ else
1369
+ rad = PDF::Math.deg2rad(angle)
1370
+ tt = "\nBT %.3f %.3f %.3f %.3f %.3f %.3f Tm"
1371
+ tt = tt % [Math.cos(rad), -1 * Math.sin(rad), Math.sin(rad), Math.cos(rad), x, y]
1372
+ add_content(tt)
1373
+ end
1374
+
1375
+ if (word_space_adjust != 0) or not ((@word_space_adjust.nil?) and (@word_space_adjust != word_space_adjust))
1376
+ @word_space_adjust = word_space_adjust
1377
+ add_content(" %.3f Tw" % word_space_adjust)
1378
+ end
1379
+
1380
+ pos = -1
1381
+ start = 0
1382
+ loop do
1383
+ pos += 1
1384
+ break if pos == text.size
1385
+ font_change = true
1386
+ tag_size, text, font_change = quick_text_tags(text, pos, font_change)
1387
+
1388
+ if tag_size != 0
1389
+ if pos > start
1390
+ part = text[start, pos - start]
1391
+ tt = " /F#{find_font(@current_font).font_id}"
1392
+ tt << " %.1f Tf %d Tr" % [ size, @current_text_render_style ]
1393
+ tt << " (#{PDF::Writer.escape(part)}) Tj"
1394
+ add_content(tt)
1395
+ end
1396
+
1397
+ if font_change
1398
+ current_font!
1399
+ else
1400
+ add_content(" ET")
1401
+ xp = x
1402
+ yp = y
1403
+ tag_size, text, font_change, xp, yp = text_tags(text, pos, font_change, true, xp, yp, size, angle, word_space_adjust)
1404
+
1405
+ # Restart the text object
1406
+ if angle.zero?
1407
+ add_content("\nBT %.3f %.3f Td" % [xp, yp])
1408
+ else
1409
+ rad = PDF::Math.deg2rad(angle)
1410
+ tt = "\nBT %.3f %.3f %.3f %.3f %.3f %.3f Tm"
1411
+ tt = tt % [Math.cos(rad), -1 * Math.sin(rad), Math.sin(rad), Math.cos(rad), xp, yp]
1412
+ add_content(tt)
1413
+ end
1414
+
1415
+ if (word_space_adjust != 0) or (word_space_adjust != @word_space_adjust)
1416
+ @word_space_adjust = word_space_adjust
1417
+ add_content(" %.3f Tw" % [word_space_adjust])
1418
+ end
1419
+ end
1420
+
1421
+ pos += tag_size - 1
1422
+ start = pos + 1
1423
+ end
1424
+ end
1425
+
1426
+ if start < text.size
1427
+ part = text[start..-1]
1428
+
1429
+ tt = " /F#{find_font(@current_font).font_id}"
1430
+ tt << " %.1f Tf %d Tr" % [ size, @current_text_render_style ]
1431
+ tt << " (#{PDF::Writer.escape(part)}) Tj"
1432
+ add_content(tt)
1433
+ end
1434
+ add_content(" ET")
1435
+
1436
+ # XXX: Experimental fix.
1437
+ @callbacks.reverse_each do |ii|
1438
+ info = ii.dup
1439
+ info[:x] = x
1440
+ info[:y] = y
1441
+ info[:angle] = angle
1442
+ info[:status] = :end_line
1443
+ info[:tag][self, info]
1444
+ end
1445
+ end
1446
+
1447
+ def char_width(font, char)
1448
+ char = char[0] unless @fonts[font].c[char]
1449
+
1450
+ if @fonts[font].differences and @fonts[font].c[char].nil?
1451
+ name = @fonts[font].differences[char] || 'M'
1452
+ width = @fonts[font].c[name]['WX'] if @fonts[font].c[name]['WX']
1453
+ elsif @fonts[font].c[char]
1454
+ width = @fonts[font].c[char]['WX']
1455
+ else
1456
+ width = @fonts[font].c['M']['WX']
1457
+ end
1458
+ width
1459
+ end
1460
+ private :char_width
1461
+
1462
+ # Calculate how wide a given text string will be on a page, at a given
1463
+ # size. This can be called externally, but is alse used by the other
1464
+ # class functions.
1465
+ def text_line_width(size, text)
1466
+ # This function should not change any of the settings, though it will
1467
+ # need to track any tag which change during calculation, so copy them
1468
+ # at the start and put them back at the end.
1469
+ t_CTS = @current_text_state.dup
1470
+
1471
+ select_font("Helvetica") if @fonts.empty?
1472
+ # converts a number or a float to a string so it can get the width
1473
+ tt = text.to_s
1474
+ # hmm, this is where it all starts to get tricky - use the font
1475
+ # information to calculate the width of each character, add them up
1476
+ # and convert to user units
1477
+ width = 0
1478
+ font = @current_font
1479
+
1480
+ pos = -1
1481
+ loop do
1482
+ pos += 1
1483
+ break if pos == tt.size
1484
+ font_change = true
1485
+ tag_size, text, font_change = quick_text_tags(text, pos, font_change)
1486
+ if tag_size != 0
1487
+ if font_change
1488
+ current_font!
1489
+ font = @current_font
1490
+ end
1491
+ pos += tag_size - 1
1492
+ else
1493
+ if "&lt;" == tt[pos, 4]
1494
+ width += char_width(font, '<')
1495
+ pos += 3
1496
+ elsif "&gt;" == tt[pos, 4]
1497
+ width += char_width(font, '>')
1498
+ pos += 3
1499
+ elsif "&amp;" == tt[pos, 5]
1500
+ width += char_width(font, '&')
1501
+ pos += 4
1502
+ else
1503
+ width += char_width(font, tt[pos, 1])
1504
+ end
1505
+ end
1506
+ end
1507
+
1508
+ @current_text_state = t_CTS.dup
1509
+ current_font!
1510
+
1511
+ (width * size / 1000.0)
1512
+ end
1513
+
1514
+ # Will calculate the maximum width, taking into account that the text
1515
+ # may be broken by line breaks.
1516
+ def text_width(size, text)
1517
+ max = 0
1518
+
1519
+ text.to_s.each do |line|
1520
+ width = text_line_width(size, line)
1521
+ max = width if width > max
1522
+ end
1523
+ max
1524
+ end
1525
+
1526
+ # Partially calculate the values necessary to sort out the justification
1527
+ # of text.
1528
+ def adjust_wrapped_text(text, actual, width, x, just)
1529
+ adjust = 0
1530
+
1531
+ case just
1532
+ when :left
1533
+ nil
1534
+ when :right
1535
+ x += (width - actual)
1536
+ when :center
1537
+ x += (width - actual) / 2.0
1538
+ when :full
1539
+ spaces = text.count(" ")
1540
+ adjust = (width - actual) / spaces.to_f if spaces > 0
1541
+ end
1542
+
1543
+ [x, adjust]
1544
+ end
1545
+ private :adjust_wrapped_text
1546
+
1547
+ # Add text to the page, but ensure that it fits within a certain width.
1548
+ # If it does not fit then put in as much as possible, breaking at word
1549
+ # boundaries; return the remainder. +justification+ and +angle+ can also
1550
+ # be specified for the text.
1551
+ #
1552
+ # This will display the text; if it goes beyond the width +width+, it
1553
+ # will backttrack to the previous space or hyphen and return the
1554
+ # remainder of the text.
1555
+ #
1556
+ # +justification+:: :left, :right, :center, or :full
1557
+ def add_text_wrap(x, y, width, size, text, justification = :left, angle = 0, test = false)
1558
+ # Need to store the initial text state, as this will change during the
1559
+ # width calculation, but will need to be re-set before printing, so
1560
+ # that the chars work out right
1561
+ t_CTS = @current_text_state.dup
1562
+
1563
+ select_font("Helvetica") if @fonts.empty?
1564
+ return "" if width <= 0
1565
+
1566
+ w = brk = brkw = 0
1567
+ font = @current_font
1568
+ tw = width / size.to_f * 1000
1569
+
1570
+ pos = -1
1571
+ loop do
1572
+ pos += 1
1573
+ break if pos == text.size
1574
+ font_change = true
1575
+ tag_size, text, font_change = quick_text_tags(text, pos, font_change)
1576
+ if tag_size != 0
1577
+ if font_change
1578
+ current_font!
1579
+ font = @current_font
1580
+ end
1581
+ pos += (tag_size - 1)
1582
+ else
1583
+ w += char_width(font, text[pos, 1])
1584
+
1585
+ if w > tw # We need to truncate this line
1586
+ if brk > 0 # There is somewhere to break the line.
1587
+ if text[brk] == " "
1588
+ tmp = text[0, brk]
1589
+ else
1590
+ tmp = text[0, brk + 1]
1591
+ end
1592
+ x, adjust = adjust_wrapped_text(tmp, brkw, width, x, justification)
1593
+
1594
+ # Reset the text state
1595
+ @current_text_state = t_CTS.dup
1596
+ current_font!
1597
+ add_text(x, y, size, tmp, angle, adjust) unless test
1598
+ return text[brk + 1..-1]
1599
+ else # just break before the current character
1600
+ tmp = text[0, pos]
1601
+ # tmpw = (w - char_width(font, text[pos, 1])) * size / 1000.0
1602
+ x, adjust = adjust_wrapped_text(tmp, brkw, width, x, justification)
1603
+
1604
+ # Reset the text state
1605
+ @current_text_state = t_CTS.dup
1606
+ current_font!
1607
+ add_text(x, y, size, tmp, angle, adjust) unless test
1608
+ return text[pos..-1]
1609
+ end
1610
+ end
1611
+
1612
+ if text[pos] == ?-
1613
+ brk = pos
1614
+ brkw = w * size / 1000.0
1615
+ end
1616
+
1617
+ if text[pos, 1] == " "
1618
+ brk = pos
1619
+ ctmp = text[pos]
1620
+ ctmp = @fonts[font].differences[ctmp] unless @fonts[font].differences.nil?
1621
+ z = @fonts[font].c[tmp].nil? ? 0 : @fonts[font].c[tmp]['WX']
1622
+ brkw = (w - z) * size / 1000.0
1623
+ end
1624
+ end
1625
+ end
1626
+
1627
+ # There was no need to break this line.
1628
+ justification = :left if justification == :full
1629
+ tmpw = (w * size) / 1000.0
1630
+ x, adjust = adjust_wrapped_text(text, tmpw, width, x, justification)
1631
+ # reset the text state
1632
+ @current_text_state = t_CTS.dup
1633
+ current_font!
1634
+ add_text(x, y, size, text, angle, adjust) unless test
1635
+ return ""
1636
+ end
1637
+
1638
+ # Saves the state.
1639
+ def save_state
1640
+ PDF::Writer::State.new do |state|
1641
+ state.fill_color = @current_fill_color
1642
+ state.stroke_color = @current_stroke_color
1643
+ state.text_render_style = @current_text_render_style
1644
+ state.stroke_style = @current_stroke_style
1645
+ @state_stack.push state
1646
+ end
1647
+ add_content("\nq")
1648
+ end
1649
+
1650
+ # This will be called at a new page to return the state to what it was
1651
+ # on the end of the previous page, before the stack was closed down.
1652
+ # This is to get around not being able to have open 'q' across pages.
1653
+ def reset_state_at_page_start
1654
+ @state_stack.each do |state|
1655
+ fill_color! state.fill_color
1656
+ stroke_color! state.stroke_color
1657
+ text_render_style! state.text_render_style
1658
+ stroke_style! state.stroke_style
1659
+ add_content("\nq")
1660
+ end
1661
+ end
1662
+ private :reset_state_at_page_start
1663
+
1664
+ # Restore a previously saved state.
1665
+ def restore_state
1666
+ unless @state_stack.empty?
1667
+ state = @state_stack.pop
1668
+ @current_fill_color = state.fill_color
1669
+ @current_stroke_color = state.stroke_color
1670
+ @current_text_render_style = state.text_render_style
1671
+ @current_stroke_style = state.stroke_style
1672
+ stroke_style!
1673
+ end
1674
+ add_content("\nQ")
1675
+ end
1676
+
1677
+ # Restore the state at the end of a page.
1678
+ def reset_state_at_page_finish
1679
+ add_content("\nQ" * @state_stack.size)
1680
+ end
1681
+ private :reset_state_at_page_finish
1682
+
1683
+ # Make a loose object. The output will go into this object, until it is
1684
+ # closed, then will revert to the current one. This object will not
1685
+ # appear until it is included within a page. The function will return
1686
+ # the object reference.
1687
+ def open_object
1688
+ @stack << { :contents => @current_contents, :page => @current_page }
1689
+ @current_contents = PDF::Writer::Object::Contents.new(self)
1690
+ @loose_objects << @current_contents
1691
+ yield @current_contents if block_given?
1692
+ @current_contents
1693
+ end
1694
+
1695
+ # Opens an existing object for editing.
1696
+ def reopen_object(id)
1697
+ @stack << { :contents => @current_contents, :page => @current_page }
1698
+ @current_contents = id
1699
+ # if this object is the primary contents for a page, then set the
1700
+ # current page to its parent
1701
+ @current_page = @current_contents.on_page unless @current_contents.on_page.nil?
1702
+ @current_contents
1703
+ end
1704
+
1705
+ # Close an object for writing.
1706
+ def close_object
1707
+ unless @stack.empty?
1708
+ obj = @stack.pop
1709
+ @current_contents = obj[:contents]
1710
+ @current_page = obj[:page]
1711
+ end
1712
+ end
1713
+
1714
+ # Stop an object from appearing on pages from this point on.
1715
+ def stop_object(id)
1716
+ obj = @loose_objects.detect { |ii| ii.oid == id.oid }
1717
+ @add_loose_objects[obj] = nil
1718
+ end
1719
+
1720
+ # After an object has been created, it will only show if it has been
1721
+ # added, using this method.
1722
+ def add_object(id, where = :this_page)
1723
+ obj = @loose_objects.detect { |ii| ii == id }
1724
+
1725
+ if obj and @current_contents != obj
1726
+ case where
1727
+ when :all_pages, :this_page
1728
+ @add_loose_objects[obj] = where if where == :all_pages
1729
+ @current_contents.on_page.contents << obj if @current_contents.on_page
1730
+ when :even_pages
1731
+ @add_loose_objects[obj] = where
1732
+ page = @current_contents.on_page
1733
+ add_object(id) if (page.info.page_number % 2) == 0
1734
+ when :odd_pages
1735
+ @add_loose_objects[obj] = where
1736
+ page = @current_contents.on_page
1737
+ add_object(id) if (page.info.page_number % 2) == 1
1738
+ when :all_following_pages
1739
+ @add_loose_objects[obj] = :all_pages
1740
+ when :following_even_pages
1741
+ @add_loose_objects[obj] = :even_pages
1742
+ when :following_odd_pages
1743
+ @add_loose_objects[obj] = :odd_pages
1744
+ end
1745
+ end
1746
+ end
1747
+
1748
+ # Add content to the documents info object.
1749
+ def add_info(label, value = 0)
1750
+ # This will only work if the label is one of the valid ones. Modify
1751
+ # this so that arrays can be passed as well. If @label is an array
1752
+ # then assume that it is key => value pairs else assume that they are
1753
+ # both scalar, anything else will probably error.
1754
+ if label.kind_of?(Hash)
1755
+ label.each { |kk, vv| @info.__send__(kk.downcase.intern, vv) }
1756
+ else
1757
+ @info.__send__(label.downcase.intern, value)
1758
+ end
1759
+ end
1760
+
1761
+ # Specify the Destination object where the document should open when it
1762
+ # first starts. +style+ must be one of the values detailed for
1763
+ # #destinations. The value of +style+ affects the interpretation of
1764
+ # +params+. Uses the current page as the starting location.
1765
+ def open_here(style, *params)
1766
+ open_at(@current_page, style, *params)
1767
+ end
1768
+
1769
+ # Specify the Destination object where the document should open when it
1770
+ # first starts. +style+ must be one of the following values. The value
1771
+ # of +style+ affects the interpretation of +params+. Uses +page+ as the
1772
+ # starting location.
1773
+ def open_at(page, style, *params)
1774
+ d = PDF::Writer::Object::Destination.new(self, page, style, *params)
1775
+ @catalog.open_here = d
1776
+ end
1777
+
1778
+ # Create a labelled destination within the document. The label is the
1779
+ # name which will be used for <c:ilink> destinations.
1780
+ #
1781
+ # XYZ:: The viewport will be opened at position <tt>(left, top)</tt>
1782
+ # with +zoom+ percentage. +params+ must have three values
1783
+ # representing +left+, +top+, and +zoom+, respectively. If the
1784
+ # values are "null", the current parameter values are unchanged.
1785
+ # Fit:: Fit the page to the viewport (horizontal and vertical).
1786
+ # +params+ will be ignored.
1787
+ # FitH:: Fit the page horizontally to the viewport. The top of the
1788
+ # viewport is set to the first value in +params+.
1789
+ # FitV:: Fit the page vertically to the viewport. The left of the
1790
+ # viewport is set to the first value in +params+.
1791
+ # FitR:: Fits the page to the provided rectangle. +params+ must have
1792
+ # four values representing the +left+, +bottom+, +right+, and
1793
+ # +top+ positions, respectively.
1794
+ # FitB:: Fits the page to the bounding box of the page. +params+ is
1795
+ # ignored.
1796
+ # FitBH:: Fits the page horizontally to the bounding box of the page.
1797
+ # The top position is defined by the first value in +params+.
1798
+ # FitBV:: Fits the page vertically to the bounding box of the page. The
1799
+ # left position is defined by the first value in +params+.
1800
+ def add_destination(label, style, *params)
1801
+ @destinations[label] = PDF::Writer::Object::Destination.new(self, @current_page, style, *params)
1802
+ end
1803
+
1804
+ # Set the page mode of the catalog. Must be one of the following:
1805
+ # UseNone:: Neither document outline nor thumbnail images are
1806
+ # visible.
1807
+ # UseOutlines:: Document outline visible.
1808
+ # UseThumbs:: Thumbnail images visible.
1809
+ # FullScreen:: Full-screen mode, with no menu bar, window controls, or
1810
+ # any other window visible.
1811
+ # UseOC:: Optional content group panel is visible.
1812
+ #
1813
+ def page_mode=(mode)
1814
+ @catalog.page_mode = value
1815
+ end
1816
+
1817
+ include Transaction::Simple
1818
+
1819
+ # The width of the currently active column. This will return zero (0) if
1820
+ # columns are off.
1821
+ attr_reader :column_width
1822
+ def column_width #:nodoc:
1823
+ return 0 unless @columns_on
1824
+ @columns[:width]
1825
+ end
1826
+ # The gutter between columns. This will return zero (0) if columns are
1827
+ # off.
1828
+ attr_reader :column_gutter
1829
+ def column_gutter #:nodoc:
1830
+ return 0 unless @columns_on
1831
+ @columns[:gutter]
1832
+ end
1833
+ # The current column number. Returns zero (0) if columns are off.
1834
+ attr_reader :column_number
1835
+ def column_number #:nodoc:
1836
+ return 0 unless @columns_on
1837
+ @columns[:current]
1838
+ end
1839
+ # The total number of columns. Returns zero (0) if columns are off.
1840
+ attr_reader :column_count
1841
+ def column_count #:nodoc:
1842
+ return 0 unless @columns_on
1843
+ @columns[:size]
1844
+ end
1845
+ # Indicates if columns are currently on.
1846
+ def columns?
1847
+ @columns_on
1848
+ end
1849
+
1850
+ # Starts multi-column output. Creates +size+ number of columns with a
1851
+ # +gutter+ PDF unit space between each column.
1852
+ #
1853
+ # If columns are already started, this will return +false+.
1854
+ def start_columns(size = 2, gutter = 10)
1855
+ # Start from the current y-position; make the set number of columns.
1856
+ return false if @columns_on
1857
+
1858
+ @columns = {
1859
+ :current => 1,
1860
+ :bot_y => @y
1861
+ }
1862
+ @columns_on = true
1863
+ # store the current margins
1864
+ @columns[:left] = @left_margin
1865
+ @columns[:right] = @right_margin
1866
+ @columns[:top] = @top_margin
1867
+ @columns[:bottom] = @bottom_margin
1868
+ # Reset the margins to suit the new columns. Safe enough to assume the
1869
+ # first column here, but start from the current y-position.
1870
+ @top_margin = @page_height - @y
1871
+ @columns[:size] = size || 2
1872
+ @columns[:gutter] = gutter || 10
1873
+ w = absolute_right_margin - absolute_left_margin
1874
+ @columns[:width] = (w - ((size - 1) * gutter)) / size.to_f
1875
+ @right_margin = @page_width - (@left_margin + @columns[:width])
1876
+ end
1877
+
1878
+ def restore_margins_after_columns
1879
+ @left_margin = @columns[:left]
1880
+ @right_margin = @columns[:right]
1881
+ @top_margin = @columns[:top]
1882
+ @bottom_margin = @columns[:bottom]
1883
+ end
1884
+ private :restore_margins_after_columns
1885
+
1886
+ # Turns off multi-column output. If we are in the first column, or the
1887
+ # lowest point at which columns were written is higher than the bottom
1888
+ # of the page, then the writing pointer will be placed at the lowest
1889
+ # point. Otherwise, a new page will be started.
1890
+ def stop_columns
1891
+ return false unless @columns_on
1892
+ @columns_on = false
1893
+
1894
+ @columns[:bot_y] = @y if @y < @columns[:bot_y]
1895
+
1896
+ if (@columns[:bot_y] > @bottom_margin) or @column_number == 1
1897
+ @y = @columns[:bot_y]
1898
+ else
1899
+ start_new_page
1900
+ end
1901
+ restore_margins_after_columns
1902
+ @columns = {}
1903
+ true
1904
+ end
1905
+
1906
+ # Changes page insert mode. May be called as follows:
1907
+ #
1908
+ # pdf.insert_mode # => current insert mode
1909
+ # # The following four affect the insert mode without changing the
1910
+ # # insert page or insert position.
1911
+ # pdf.insert_mode(:on) # enables insert mode
1912
+ # pdf.insert_mode(true) # enables insert mode
1913
+ # pdf.insert_mode(:off) # disables insert mode
1914
+ # pdf.insert_mode(false) # disables insert mode
1915
+ #
1916
+ # # Changes the insert mode, the insert page, and the insert
1917
+ # # position at the same time.
1918
+ # opts = {
1919
+ # :on => true,
1920
+ # :page => :last,
1921
+ # :position => :before
1922
+ # }
1923
+ # pdf.insert_mode(opts)
1924
+ def insert_mode(options = {})
1925
+ case options
1926
+ when :on, true
1927
+ @insert_mode = true
1928
+ when :off, false
1929
+ @insert_mode = false
1930
+ else
1931
+ return @insert_mode unless options
1932
+
1933
+ @insert_mode = options[:on] unless options[:on].nil?
1934
+
1935
+ unless options[:page].nil?
1936
+ if @pageset[options[:page]].nil? or options[:page] == :last
1937
+ @insert_page = @pageset[-1]
1938
+ else
1939
+ @insert_page = @pageset[options[:page]]
1940
+ end
1941
+ end
1942
+
1943
+ @insert_position = options[:position] if options[:position]
1944
+ end
1945
+ end
1946
+ # Returns or changes the insert page property.
1947
+ #
1948
+ # pdf.insert_page # => current insert page
1949
+ # pdf.insert_page(35) # insert at page 35
1950
+ # pdf.insert_page(:last) # insert at the last page
1951
+ def insert_page(page = nil)
1952
+ return @insert_page unless page
1953
+ if page == :last
1954
+ @insert_page = @pageset[-1]
1955
+ else
1956
+ @insert_page = @pageset[page]
1957
+ end
1958
+ end
1959
+ # Changes the #insert_page property to append to the page set.
1960
+ def append_page
1961
+ insert_mode(:last)
1962
+ end
1963
+ # Returns or changes the insert position to be before or after the
1964
+ # specified page.
1965
+ #
1966
+ # pdf.insert_position # => current insert position
1967
+ # pdf.insert_position(:before) # insert before #insert_page
1968
+ # pdf.insert_position(:after) # insert before #insert_page
1969
+ def insert_position(position = nil)
1970
+ return @insert_position unless position
1971
+ @insert_position = position
1972
+ end
1973
+
1974
+ # Creates a new page. If multi-column output is turned on, this will
1975
+ # change the column to the next greater or create a new page as
1976
+ # necessary. If +force+ is true, then a new page will be created even if
1977
+ # multi-column output is on.
1978
+ def start_new_page(force = false)
1979
+ page_required = true
1980
+
1981
+ if @columns_on
1982
+ # Check if this is just going to a new column. Increment the column
1983
+ # number.
1984
+ @columns[:current] += 1
1985
+
1986
+ if @columns[:current] <= @columns[:size] and not force
1987
+ page_required = false
1988
+ @columns[:bot_y] = @y if @y < @columns[:bot_y]
1989
+ else
1990
+ @columns[:current] = 1
1991
+ @top_margin = @columns[:top]
1992
+ @columns[:bot_y] = absolute_top_margin
1993
+ end
1994
+
1995
+ w = @columns[:width]
1996
+ g = @columns[:gutter]
1997
+ n = @columns[:current] - 1
1998
+ @left_margin = @columns[:left] + n * (g + w)
1999
+ @right_margin = @page_width - (@left_margin + w)
2000
+ end
2001
+
2002
+ if page_required or force
2003
+ # make a new page, setting the writing point back to the top.
2004
+ @y = absolute_top_margin
2005
+ # make the new page with a call to the basic class
2006
+ if @insert_mode
2007
+ id = new_page(true, @insert_page, @insert_position)
2008
+ @pageset << id
2009
+ # Manipulate the insert options so that inserted pages follow each
2010
+ # other
2011
+ @insert_page = id
2012
+ @insert_position = :after
2013
+ else
2014
+ @pageset << new_page
2015
+ end
2016
+
2017
+ else
2018
+ @y = absolute_top_margin
2019
+ end
2020
+ @pageset
2021
+ end
2022
+
2023
+ # Add a new page to the document. This also makes the new page the
2024
+ # current active object. This allows for mandatory page creation
2025
+ # regardless of multi-column output.
2026
+ #
2027
+ # For most purposes, #start_new_page is preferred.
2028
+ def new_page(insert = false, page = nil, pos = :after)
2029
+ reset_state_at_page_finish
2030
+
2031
+ if insert
2032
+ # The id from the PDF::Writer class is the id of the contents of the
2033
+ # page, not the page object itself. Query that object to find the
2034
+ # parent.
2035
+ _new_page = PDF::Writer::Object::Page.new(self, { :rpage => page, :pos => pos })
2036
+ else
2037
+ _new_page = PDF::Writer::Object::Page.new(self)
2038
+ end
2039
+
2040
+ reset_state_at_page_start
2041
+
2042
+ # If there has been a stroke or fill color set, transfer them.
2043
+ fill_color!
2044
+ stroke_color!
2045
+ stroke_style!
2046
+
2047
+ # the call to the page object set @current_contents to the present page,
2048
+ # so this can be returned as the page id
2049
+ # @current_contents
2050
+ _new_page
2051
+ end
2052
+
2053
+ # Returns the current generic page number. This is based exclusively on
2054
+ # the size of the page set.
2055
+ def current_page_number
2056
+ @pageset.size
2057
+ end
2058
+
2059
+ # Put page numbers on the pages from the current page. Place them
2060
+ # relative to the coordinates <tt>(x, y)</tt> with the text horizontally
2061
+ # relative according to +pos+, which may be <tt>:left</tt>,
2062
+ # <tt>:right</tt>, or <tt>:center</tt>. The page numbers will be written
2063
+ # on each page using +pattern+.
2064
+ #
2065
+ # When +pattern+ is rendered, <PAGENUM> will be replaced with the
2066
+ # current page number; <TOTALPAGENUM> will be replaced with the total
2067
+ # number of pages in the page numbering scheme. The default +pattern+ is
2068
+ # "<PAGENUM> of <TOTALPAGENUM>".
2069
+ #
2070
+ # If +starting+ is non-nil, this is the first page number. The number of
2071
+ # total pages will be adjusted to account for this.
2072
+ #
2073
+ # Each time page numbers are started, a new page number scheme will be
2074
+ # started. The scheme number will be returned.
2075
+ def start_page_numbering(x, y, size, pos = nil, pattern = nil, starting = nil)
2076
+ pos ||= :left
2077
+ pattern ||= "<PAGENUM> of <TOTALPAGENUM>"
2078
+
2079
+ @page_numbering ||= []
2080
+ @page_numbering << (o = {})
2081
+
2082
+ page = @pageset.size
2083
+ o[page] = {
2084
+ :x => x,
2085
+ :y => y,
2086
+ :pos => pos,
2087
+ :pattern => pattern,
2088
+ :starting => starting,
2089
+ :size => size
2090
+ }
2091
+ @page_numbering.index(o)
2092
+ end
2093
+
2094
+ # Given a particular generic page number +page_num+ (numbered
2095
+ # sequentially from the beginning of the page set), return the page
2096
+ # number under a particular page numbering +scheme+. Returns +nil+ if
2097
+ # page numbering is not turned on.
2098
+ def which_page_number(page_num, scheme = 0)
2099
+ return nil unless @page_numbering
2100
+
2101
+ num = 0
2102
+ start = start_num = 1
2103
+
2104
+ @page_numbering[scheme].each do |kk, vv|
2105
+ if kk <= page_num
2106
+ if vv.kind_of?(Hash)
2107
+ unless vv[:starting].nil?
2108
+ start = vv[:starting]
2109
+ start_num = kk
2110
+ num = page_num - start_num + start
2111
+ end
2112
+ else
2113
+ num = 0
2114
+ end
2115
+ end
2116
+ end
2117
+ num
2118
+ end
2119
+
2120
+ # Stop page numbering. Returns +false+ if page numbering is off.
2121
+ #
2122
+ # If +stop_total+ is true, then then the totaling of pages for this page
2123
+ # numbering +scheme+ will be stopped as well. If +stop_at+ is
2124
+ # <tt>:current</tt>, then the page numbering will stop at this page;
2125
+ # otherwise, it will stop at the next page.
2126
+ def stop_page_numbering(stop_total = false, stop_at = :current, scheme = 0)
2127
+ return false unless @page_numbering
2128
+
2129
+ page = @pageset.size
2130
+
2131
+ if stop_at != :current and @page_numbering[scheme][page].kind_of?(Hash)
2132
+ if stop_total
2133
+ @page_numbering[scheme][page]["stoptn"] = true
2134
+ else
2135
+ @page_numbering[scheme][page]["stopn"] = true
2136
+ end
2137
+ else
2138
+ if stop_total
2139
+ @page_numbering[scheme][page] = "stopt"
2140
+ else
2141
+ @page_numbering[scheme][page] = "stop"
2142
+ end
2143
+
2144
+ @page_numbering[scheme][page] << "n" unless stop_at == :current
2145
+ end
2146
+ end
2147
+
2148
+ def page_number_search(label, tmp)
2149
+ tmp.each do |scheme, v|
2150
+ if v.kind_of?(Hash)
2151
+ return scheme unless v[label].nil?
2152
+ else
2153
+ return scheme if v == label
2154
+ end
2155
+ end
2156
+ 0
2157
+ end
2158
+ private :page_number_search
2159
+
2160
+ def add_page_numbers
2161
+ # This will go through the @page_numbering array and add the page
2162
+ # numbers are required.
2163
+ unless @page_numbering.nil?
2164
+ total_pages1 = @pageset.size
2165
+ tmp1 = @page_numbering
2166
+ status = 0
2167
+ info = {}
2168
+ tmp1.each do |tmp|
2169
+ # Do each of the page numbering systems. First, find the total
2170
+ # pages for this one.
2171
+ k = page_number_search("stopt", tmp)
2172
+ if k and k > 0
2173
+ total_pages = k - 1
2174
+ else
2175
+ l = page_number_search("stoptn", tmp)
2176
+ if l and l > 0
2177
+ total_pages = l
2178
+ else
2179
+ total_pages = total_pages1
2180
+ end
2181
+ end
2182
+ @pageset.each_with_index do |id, page_num|
2183
+ next if page_num == 0
2184
+ if tmp[page_num].kind_of?(Hash) # This must be the starting page #s
2185
+ status = 1
2186
+ info = tmp[page_num]
2187
+ info[:delta] = info[:starting] - page_num
2188
+ # Also check for the special case of the numbering stopping
2189
+ # and starting on the same page.
2190
+ status = 2 if info["stopn"] or info["stoptn"]
2191
+ elsif tmp[page_num] == "stop" or tmp[page_num] == "stopt"
2192
+ status = 0 # we are stopping page numbers
2193
+ elsif status == 1 and (tmp[page_num] == "stoptn" or tmp[page_num] == "stopn")
2194
+ status = 2
2195
+ end
2196
+
2197
+ if status != 0
2198
+ # Add the page numbering to this page
2199
+ unless info[:delta]
2200
+ num = page_num
2201
+ else
2202
+ num = page_num + info[:delta]
2203
+ end
2204
+
2205
+ total = total_pages + num - page_num
2206
+ pat = info[:pattern].gsub(/<PAGENUM>/, num.to_s).gsub(/<TOTALPAGENUM>/, total.to_s)
2207
+ reopen_object(id.contents.first)
2208
+ case info[:pos]
2209
+ when :right
2210
+ w = 0
2211
+ when :left
2212
+ w = text_width(info[:size], pat)
2213
+ when :center
2214
+ w = text_width(info[:size], pat) / 2.0
2215
+ end
2216
+ add_text(info[:x] + w, info[:y], info[:size], pat)
2217
+ close_object
2218
+ status = 0 if status == 2
2219
+ end
2220
+ end
2221
+ end
2222
+ end
2223
+ end
2224
+ private :add_page_numbers
2225
+
2226
+ def clean_up
2227
+ add_page_numbers
2228
+ end
2229
+ private :clean_up
2230
+
2231
+ def preprocess_text(text)
2232
+ text
2233
+ end
2234
+ private :preprocess_text
2235
+
2236
+ # This will add a string of +text+ to the document, starting at the
2237
+ # current drawing position. It will wrap to keep within the margins,
2238
+ # including optional offsets from the left and the right. The text will
2239
+ # go to the start of the next line when a return code "\n" is found.
2240
+ #
2241
+ # Possible +options+ are:
2242
+ # <tt>:font_size</tt>:: The font size to be used. If not
2243
+ # specified, is either the last font size or
2244
+ # the default font size of 12 points.
2245
+ # <tt>:left</tt>:: number, gap to leave from the left margin
2246
+ # <tt>:right</tt>:: number, gap to leave from the right margin
2247
+ # <tt>:absolute_left</tt>:: number, absolute left position (overrides
2248
+ # <tt>:left</tt>)
2249
+ # <tt>:absolute_right</tt>:: number, absolute right position (overrides
2250
+ # <tt>:right</tt>)
2251
+ # <tt>:justification</tt>:: <tt>:left</tt>, <tt>:right</tt>,
2252
+ # <tt>:center</tt>, <tt>:full</tt>
2253
+ # <tt>:leading</tt>:: number, defines the total height taken by
2254
+ # the line, independent of the font height.
2255
+ # <tt>:spacing</tt>:: a Floating point number, though usually
2256
+ # set to one of 1, 1.5, 2 (line spacing as
2257
+ # used in word processing)
2258
+ #
2259
+ # Only one of <tt>:leading</tt> or <tt>:spacing</tt> should be specified
2260
+ # (leading overrides spacing).
2261
+ #
2262
+ # If the <tt>:test</tt> option is +true+, then this should just check to
2263
+ # see if the text is flowing onto a new page or not; returns +true+ or
2264
+ # +false+. Note that the new page test is only sensitive to exceeding
2265
+ # the bottom margin of the page. It is not known whether the writing of
2266
+ # the text will require a new physical page or whether it will require a
2267
+ # new column.
2268
+ def text(text, options = {})
2269
+ # Apply the filtering which will make underlining (and other items)
2270
+ # function.
2271
+ text = preprocess_text(text)
2272
+
2273
+ options ||= {}
2274
+
2275
+ new_page_required = false
2276
+ __y = @y
2277
+
2278
+ if options[:absolute_left]
2279
+ left = options[:absolute_left]
2280
+ else
2281
+ left = @left_margin
2282
+ left += options[:left] if options[:left]
2283
+ end
2284
+
2285
+ if options[:absolute_right]
2286
+ right = options[:absolute_right]
2287
+ else
2288
+ right = absolute_right_margin
2289
+ right -= options[:right] if options[:right]
2290
+ end
2291
+
2292
+ size = options[:font_size] || 0
2293
+ if size <= 0
2294
+ size = @font_size
2295
+ else
2296
+ @font_size = size
2297
+ end
2298
+
2299
+ just = options[:justification] || :left
2300
+
2301
+ if options[:leading] # leading instead of spacing
2302
+ height = options[:leading]
2303
+ elsif options[:spacing]
2304
+ height = options[:spacing] * font_height(size)
2305
+ else
2306
+ height = font_height(size)
2307
+ end
2308
+
2309
+ text.each do |line|
2310
+ start = true
2311
+ loop do # while not line.empty? or start
2312
+ break if (line.nil? or line.empty?) and not start
2313
+
2314
+ start = false
2315
+
2316
+ @y -= height
2317
+
2318
+ if @y < @bottom_margin
2319
+ if options[:test]
2320
+ new_page_required = true
2321
+ else
2322
+ # and then re-calc the left and right, in case they have
2323
+ # changed due to columns
2324
+ start_new_page
2325
+ @y -= height
2326
+
2327
+ if options[:absolute_left]
2328
+ left = options[:absolute_left]
2329
+ else
2330
+ left = @left_margin
2331
+ left += options[:left] if options[:left]
2332
+ end
2333
+
2334
+ if options[:absolute_right]
2335
+ right = options[:absolute_right]
2336
+ else
2337
+ right = absolute_right_margin
2338
+ right -= options[:right] if options[:right]
2339
+ end
2340
+ end
2341
+ end
2342
+
2343
+ line = add_text_wrap(left, @y, right - left, size, line, just, 0, options[:test])
2344
+ end
2345
+ end
2346
+
2347
+ if options[:test]
2348
+ @y = __y
2349
+ new_page_required
2350
+ else
2351
+ @y
2352
+ end
2353
+ end
2354
+
2355
+ def prepress_clip_mark(x, y, angle, mark_length = 18, bleed_size = 12) #:nodoc:
2356
+ save_state
2357
+ translate_axis(x, y)
2358
+ rotate_axis(angle)
2359
+ line(0, bleed_size, 0, bleed_size + mark_length).stroke
2360
+ line(bleed_size, 0, bleed_size + mark_length, 0).stroke
2361
+ restore_state
2362
+ end
2363
+
2364
+ def prepress_center_mark(x, y, angle, mark_length = 18, bleed_size = 12) #:nodoc:
2365
+ save_state
2366
+ translate_axis(x, y)
2367
+ rotate_axis(angle)
2368
+ half_mark = mark_length / 2.0
2369
+ c_x = 0
2370
+ c_y = bleed_size + half_mark
2371
+ line((c_x - half_mark), c_y, (c_x + half_mark), c_y).stroke
2372
+ line(c_x, (c_y - half_mark), c_x, (c_y + half_mark)).stroke
2373
+ rad = (mark_length * 0.50) / 2.0
2374
+ circle_at(c_x, c_y, rad).stroke
2375
+ restore_state
2376
+ end
2377
+
2378
+ # Returns the estimated number of lines remaining given the default or
2379
+ # specified font size.
2380
+ def lines_remaining(font_size = nil)
2381
+ font_size ||= @font_size
2382
+ remaining = @y - @bottom_margin
2383
+ remaining / font_height(font_size).to_f
2384
+ end
2385
+
2386
+ # Callback tag relationships. All relationships are of the form
2387
+ # "tagname" => CallbackClass.
2388
+ #
2389
+ # There are three types of tag callbacks:
2390
+ # <tt>:pair</tt>:: Paired callbacks, e.g., <c:alink></c:alink>.
2391
+ # <tt>:single</tt>:: Single-tag callbacks, e.g., <C:bullet>.
2392
+ # <tt>:replace</tt>:: Single-tag replacement callbacks, e.g., <r:xref>.
2393
+ TAGS = {
2394
+ :pair => { },
2395
+ :single => { },
2396
+ :replace => { }
2397
+ }
2398
+ TAGS.freeze
2399
+
2400
+ # A callback to support the formation of clickable links to external
2401
+ # locations.
2402
+ class TagAlink
2403
+ # The default anchored link style.
2404
+ DEFAULT_STYLE = {
2405
+ :color => Color::Blue,
2406
+ :text_color => Color::Blue,
2407
+ :draw_line => true,
2408
+ :line_style => { :dash => PDF::Writer::StrokeStyle::SOLID_LINE },
2409
+ :factor => 0.05
2410
+ }
2411
+
2412
+ class << self
2413
+ # Sets the style for <c:alink> callback underlines that follow. This
2414
+ # is expected to be a hash with the following keys:
2415
+ #
2416
+ # <tt>:color</tt>:: The colour to be applied to the link
2417
+ # underline. Default is Color::Blue.
2418
+ # <tt>:text_color</tt>:: The colour to be applied to the link text.
2419
+ # Default is Color::Blue.
2420
+ # <tt>:factor</tt>:: The size of the line, as a multiple of the
2421
+ # text height. Default is 0.05.
2422
+ # <tt>:draw_line</tt>:: Whether to draw the underline as part of
2423
+ # the link or not. Default is +true+.
2424
+ # <tt>:line_style</tt>:: The style modification hash supplied to
2425
+ # PDF::Writer::StrokeStyle.new. The default
2426
+ # is a solid line with normal cap, join, and
2427
+ # miter limit values.
2428
+ #
2429
+ # Set this to +nil+ to get the default style.
2430
+ attr_accessor :style
2431
+
2432
+ def [](pdf, info)
2433
+ @style ||= DEFAULT_STYLE.dup
2434
+
2435
+ case info[:status]
2436
+ when :start, :start_line
2437
+ # The beginning of the link. This should contain the URI for the
2438
+ # link as the :params entry, and will also contain the value of
2439
+ # :cbid.
2440
+ @links ||= {}
2441
+
2442
+ @links[info[:cbid]] = {
2443
+ :x => info[:x],
2444
+ :y => info[:y],
2445
+ :angle => info[:angle],
2446
+ :descender => info[:descender],
2447
+ :height => info[:height],
2448
+ :uri => info[:params]["uri"]
2449
+ }
2450
+
2451
+ pdf.save_state
2452
+ pdf.fill_color @style[:text_color] if @style[:text_color]
2453
+ if @style[:draw_line]
2454
+ pdf.stroke_color @style[:color] if @style[:color]
2455
+ sz = info[:height] * @style[:factor]
2456
+ pdf.stroke_style! StrokeStyle.new(sz, @style[:line_style])
2457
+ end
2458
+ when :end, :end_line
2459
+ # The end of the link. Assume that it is the most recent opening
2460
+ # which has closed.
2461
+ start = @links[info[:cbid]]
2462
+ # Add underlining.
2463
+ theta = PDF::Math.deg2rad(start[:angle] - 90.0)
2464
+ if @style[:draw_line]
2465
+ drop = start[:height] * @style[:factor] * 1.5
2466
+ drop_x = Math.cos(theta) * drop
2467
+ drop_y = -Math.sin(theta) * drop
2468
+ pdf.move_to(start[:x] - drop_x, start[:y] - drop_y)
2469
+ pdf.line_to(info[:x] - drop_x, info[:y] - drop_y).stroke
2470
+ end
2471
+ pdf.add_link(start[:uri], start[:x], start[:y] +
2472
+ start[:descender], info[:x], start[:y] +
2473
+ start[:descender] + start[:height])
2474
+ pdf.restore_state
2475
+ end
2476
+ end
2477
+ end
2478
+ end
2479
+ TAGS[:pair]["alink"] = TagAlink
2480
+
2481
+ # A callback for creating and managing links internal to the document.
2482
+ class TagIlink
2483
+ def self.[](pdf, info)
2484
+ case info[:status]
2485
+ when :start, :start_line
2486
+ @links ||= {}
2487
+ @links[info[:cbid]] = {
2488
+ :x => info[:x],
2489
+ :y => info[:y],
2490
+ :angle => info[:angle],
2491
+ :descender => info[:descender],
2492
+ :height => info[:height],
2493
+ :uri => info[:params]["dest"]
2494
+ }
2495
+ when :end, :end_line
2496
+ # The end of the link. Assume that it is the most recent opening
2497
+ # which has closed.
2498
+ start = @links[info[:cbid]]
2499
+ pdf.add_internal_link(start[:uri], start[:x],
2500
+ start[:y] + start[:descender], info[:x],
2501
+ start[:y] + start[:descender] +
2502
+ start[:height])
2503
+ end
2504
+ end
2505
+ end
2506
+ TAGS[:pair]["ilink"] = TagIlink
2507
+
2508
+ # A callback to support underlining.
2509
+ class TagUline
2510
+ # The default underline style.
2511
+ DEFAULT_STYLE = {
2512
+ :color => nil,
2513
+ :line_style => { :dash => PDF::Writer::StrokeStyle::SOLID_LINE },
2514
+ :factor => 0.05
2515
+ }
2516
+
2517
+ class << self
2518
+ # Sets the style for <c:uline> callback underlines that follow. This
2519
+ # is expected to be a hash with the following keys:
2520
+ #
2521
+ # <tt>:factor</tt>:: The size of the line, as a multiple of the
2522
+ # text height. Default is 0.05.
2523
+ #
2524
+ # Set this to +nil+ to get the default style.
2525
+ attr_accessor :style
2526
+
2527
+ def [](pdf, info)
2528
+ @style ||= DEFAULT_STYLE.dup
2529
+
2530
+ case info[:status]
2531
+ when :start, :start_line
2532
+ @links ||= {}
2533
+
2534
+ @links[info[:cbid]] = {
2535
+ :x => info[:x],
2536
+ :y => info[:y],
2537
+ :angle => info[:angle],
2538
+ :descender => info[:descender],
2539
+ :height => info[:height],
2540
+ :uri => nil
2541
+ }
2542
+
2543
+ pdf.save_state
2544
+ pdf.stroke_color @style[:color] if @style[:color]
2545
+ sz = info[:height] * @style[:factor]
2546
+ pdf.stroke_style! StrokeStyle.new(sz, @style[:line_style])
2547
+ when :end, :end_line
2548
+ start = @links[info[:cbid]]
2549
+ theta = PDF::Math.deg2rad(start[:angle] - 90.0)
2550
+ drop = start[:height] * @style[:factor] * 1.5
2551
+ drop_x = Math.cos(theta) * drop
2552
+ drop_y = -Math.sin(theta) * drop
2553
+ pdf.move_to(start[:x] - drop_x, start[:y] - drop_y)
2554
+ pdf.line_to(info[:x] - drop_x, info[:y] - drop_y).stroke
2555
+ pdf.restore_state
2556
+ end
2557
+ end
2558
+ end
2559
+ end
2560
+ TAGS[:pair]["uline"] = TagUline
2561
+
2562
+ # A callback function to support drawing of a solid bullet style. Use
2563
+ # with <C:bullet>.
2564
+ class TagBullet
2565
+ # The default bullet color.
2566
+ DEFAULT_COLOR = Color::Black
2567
+
2568
+ class << self
2569
+ # Sets the style for <C:bullet> callback bullets that follow.
2570
+ # Default is Color::Black.
2571
+ #
2572
+ # Set this to +nil+ to get the default colour.
2573
+ attr_accessor :color
2574
+ def [](pdf, info)
2575
+ @color ||= DEFAULT_COLOR
2576
+
2577
+ desc = info[:descender].abs
2578
+ xpos = info[:x] - (desc * 2.00)
2579
+ ypos = info[:y] + (desc * 1.05)
2580
+
2581
+ pdf.save_state
2582
+ ss = StrokeStyle.new(desc)
2583
+ ss.cap = :butt
2584
+ ss.join = :miter
2585
+ pdf.stroke_style! ss
2586
+ pdf.stroke_color @style
2587
+ pdf.circle_at(xpos, ypos, 1).stroke
2588
+ pdf.restore_state
2589
+ end
2590
+ end
2591
+ end
2592
+ TAGS[:single]["bullet"] = TagBullet
2593
+
2594
+ # A callback function to support drawing of a disc bullet style.
2595
+ class TagDisc
2596
+ # The default disc bullet foreground.
2597
+ DEFAULT_FOREGROUND = Color::Black
2598
+ # The default disc bullet background.
2599
+ DEFAULT_BACKGROUND = Color::White
2600
+ class << self
2601
+ # The foreground color for <C:disc> bullets. Default is
2602
+ # Color::Black.
2603
+ #
2604
+ # Set to +nil+ to get the default color.
2605
+ attr_accessor :foreground
2606
+ # The background color for <C:disc> bullets. Default is
2607
+ # Color::White.
2608
+ #
2609
+ # Set to +nil+ to get the default color.
2610
+ attr_accessor :background
2611
+ def [](pdf, info)
2612
+ @foreground ||= DEFAULT_FOREGROUND
2613
+ @background ||= DEFAULT_BACKGROUND
2614
+
2615
+ desc = info[:descender].abs
2616
+ xpos = info[:x] - (desc * 2.00)
2617
+ ypos = info[:y] + (desc * 1.05)
2618
+
2619
+ ss = StrokeStyle.new(desc)
2620
+ ss.cap = :butt
2621
+ ss.join = :miter
2622
+ pdf.stroke_style! ss
2623
+ pdf.stroke_color @foreground
2624
+ pdf.circle_at(xpos, ypos, 1).stroke
2625
+ pdf.stroke_color @background
2626
+ pdf.circle_at(xpos, ypos, 0.5).stroke
2627
+ end
2628
+ end
2629
+ end
2630
+ TAGS[:single]["disc"] = TagDisc
2631
+
2632
+ # Opens a new PDF object for operating against. Returns the object's
2633
+ # identifier. To close the object, you'll need to do:
2634
+ # ob = open_new_object # Opens the object
2635
+ # # do stuff here
2636
+ # close_object # Closes the PDF document
2637
+ # # do stuff here
2638
+ # reopen_object(ob) # Reopens the custom object.
2639
+ # close_object # Closes it.
2640
+ # restore_state # Returns full control to the PDF document.
2641
+ #
2642
+ # ... I think. I haven't examined the full details to be sure of what
2643
+ # this is doing, but the code works.
2644
+ def open_new_object
2645
+ save_state
2646
+ oid = open_object
2647
+ close_object
2648
+ add_object(oid)
2649
+ reopen_object(oid)
2650
+ oid
2651
+ end
2652
+
2653
+ # Save the PDF as a file to disk.
2654
+ def save_as(name, compressed = false)
2655
+ old_compressed = self.compressed
2656
+ self.compressed = compressed
2657
+ File.open(name, "wb") { |f| f.write self.render }
2658
+ ensure
2659
+ self.compressed = old_compressed
2660
+ end
2661
+ end