pdf-labels 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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