pdf-writer 1.0.0

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