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