metaskills-pdf-writer 1.2.2

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