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