mack-pdf_writer 0.8.1 → 0.8.2

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