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