eventioz-pdf-writer 1.0

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