ypdf-writer 1.3.2

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