combine_pdf 0.1.13 → 0.1.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/lib/combine_pdf.rb +2 -0
- data/lib/combine_pdf/combine_pdf_basic_writer.rb +391 -386
- data/lib/combine_pdf/combine_pdf_methods.rb +30 -0
- data/lib/combine_pdf/combine_pdf_page.rb +545 -0
- data/lib/combine_pdf/combine_pdf_pdf.rb +19 -16
- data/lib/combine_pdf/version.rb +1 -1
- metadata +4 -3
@@ -150,6 +150,36 @@ module CombinePDF
|
|
150
150
|
create_table options
|
151
151
|
end
|
152
152
|
|
153
|
+
# calculate a CTM value for a specific transformation.
|
154
|
+
#
|
155
|
+
# this could be used to apply transformation in #textbox and to convert visual
|
156
|
+
# rotation values into actual rotation transformation.
|
157
|
+
#
|
158
|
+
# this method accepts a Hash containing any of the following parameters:
|
159
|
+
#
|
160
|
+
# deg:: the clockwise rotation to be applied, in degrees
|
161
|
+
# tx:: the x translation to be applied.
|
162
|
+
# ty:: the y translation to be applied.
|
163
|
+
# sx:: the x scaling to be applied.
|
164
|
+
# sy:: the y scaling to be applied.
|
165
|
+
#
|
166
|
+
# * scaling will be applied after the transformation is applied.
|
167
|
+
#
|
168
|
+
def calc_ctm parameters
|
169
|
+
p = {deg: 0, tx: 0, ty: 0, sx: 1, sy: 1}.merge parameters
|
170
|
+
r = p[:deg] * Math::PI / 180
|
171
|
+
s = Math.sin(r)
|
172
|
+
c = Math.cos(r)
|
173
|
+
# start with tranlation matrix
|
174
|
+
m = Matrix[ [1,0,0], [0,1,0], [ p[:tx], p[:ty], 1] ]
|
175
|
+
# then rotate
|
176
|
+
m = m * Matrix[ [c, s, 0], [-s, c, 0], [0, 0, 1]] if parameters[:deg]
|
177
|
+
# then scale
|
178
|
+
m = m * Matrix[ [p[:sx], 0, 0], [0, p[:sy], 0], [0,0,1] ] if parameters[:sx] || parameters[:sy]
|
179
|
+
# flaten array and round to 6 digits
|
180
|
+
m.to_a.flatten.values_at(0,1,3,4,6,7).map! {|f| f.round 6}
|
181
|
+
end
|
182
|
+
|
153
183
|
# adds a correctly formatted font object to the font library.
|
154
184
|
#
|
155
185
|
# registered fonts will remain in the library and will only be embeded in
|
@@ -0,0 +1,545 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
########################################################
|
3
|
+
## Thoughts from reading the ISO 32000-1:2008
|
4
|
+
## this file is part of the CombinePDF library and the code
|
5
|
+
## is subject to the same license.
|
6
|
+
########################################################
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
module CombinePDF
|
12
|
+
|
13
|
+
# This module injects methods into existing page objects
|
14
|
+
module Page_Methods
|
15
|
+
|
16
|
+
# accessor (getter) for the secure_injection setting
|
17
|
+
def secure_injection
|
18
|
+
@secure_injection
|
19
|
+
end
|
20
|
+
# accessor (setter) for the secure_injection setting
|
21
|
+
def secure_injection= safe
|
22
|
+
@secure_injection = safe
|
23
|
+
end
|
24
|
+
|
25
|
+
# the injection method
|
26
|
+
def << obj
|
27
|
+
obj = secure_injection ? PDFOperations.copy_and_secure_for_injection(obj) : PDFOperations.create_deep_copy(obj)
|
28
|
+
PDFOperations.inject_to_page self, obj
|
29
|
+
# should add new referenced objects to the main PDF objects array,
|
30
|
+
# but isn't done because the container is unknown.
|
31
|
+
# This should be resolved once the container is rendered and references are renewed.
|
32
|
+
# holder.add_referenced self
|
33
|
+
self
|
34
|
+
end
|
35
|
+
# accessor (setter) for the :MediaBox element of the page
|
36
|
+
# dimensions:: an Array consisting of four numbers (can be floats) setting the size of the media box.
|
37
|
+
def mediabox=(dimensions = [0.0, 0.0, 612.0, 792.0])
|
38
|
+
self[:MediaBox] = dimensions
|
39
|
+
end
|
40
|
+
|
41
|
+
# accessor (getter) for the :MediaBox element of the page
|
42
|
+
def mediabox
|
43
|
+
self[:MediaBox]
|
44
|
+
end
|
45
|
+
# accessor (setter) for the :MediaBox element of the page
|
46
|
+
# dimensions:: an Array consisting of four numbers (can be floats) setting the size of the media box.
|
47
|
+
def mediabox=(dimensions = [0.0, 0.0, 612.0, 792.0])
|
48
|
+
self[:MediaBox] = dimensions
|
49
|
+
end
|
50
|
+
|
51
|
+
# This method adds a simple text box to the Page represented by the PDFWriter class.
|
52
|
+
# This function takes two values:
|
53
|
+
# text:: the text to potin the box.
|
54
|
+
# properties:: a Hash of box properties.
|
55
|
+
# the symbols and values in the properties Hash could be any or all of the following:
|
56
|
+
# x:: the left position of the box.
|
57
|
+
# y:: the BUTTOM position of the box.
|
58
|
+
# width:: the width/length of the box. negative values will be computed from edge of page. defaults to 0 (end of page).
|
59
|
+
# height:: the height of the box. negative values will be computed from edge of page. defaults to 0 (end of page).
|
60
|
+
# text_align:: symbol for horizontal text alignment, can be ":center" (default), ":right", ":left"
|
61
|
+
# text_valign:: symbol for vertical text alignment, can be ":center" (default), ":top", ":buttom"
|
62
|
+
# text_padding:: a Float between 0 and 1, setting the padding for the text. defaults to 0.05 (5%).
|
63
|
+
# font:: a registered font name or an Array of names. defaults to ":Helvetica". The 14 standard fonts names are:
|
64
|
+
# - :"Times-Roman"
|
65
|
+
# - :"Times-Bold"
|
66
|
+
# - :"Times-Italic"
|
67
|
+
# - :"Times-BoldItalic"
|
68
|
+
# - :Helvetica
|
69
|
+
# - :"Helvetica-Bold"
|
70
|
+
# - :"Helvetica-BoldOblique"
|
71
|
+
# - :"Helvetica- Oblique"
|
72
|
+
# - :Courier
|
73
|
+
# - :"Courier-Bold"
|
74
|
+
# - :"Courier-Oblique"
|
75
|
+
# - :"Courier-BoldOblique"
|
76
|
+
# - :Symbol
|
77
|
+
# - :ZapfDingbats
|
78
|
+
# font_size:: a Fixnum for the font size, or :fit_text to fit the text in the box. defaults to ":fit_text"
|
79
|
+
# max_font_size:: if font_size is set to :fit_text, this will be the maximum font size. defaults to nil (no maximum)
|
80
|
+
# font_color:: text color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defaults to black.
|
81
|
+
# stroke_color:: text stroke color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defounlts to nil (no stroke).
|
82
|
+
# stroke_width:: text stroke width in PDF units. defaults to 0 (none).
|
83
|
+
# box_color:: box fill color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defaults to nil (none).
|
84
|
+
# border_color:: box border color in [R, G, B], an array with three floats, each in a value between 0 to 1 (gray will be "[0.5, 0.5, 0.5]"). defaults to nil (none).
|
85
|
+
# border_width:: border width in PDF units. defaults to nil (none).
|
86
|
+
# box_radius:: border radius in PDF units. defaults to 0 (no corner rounding).
|
87
|
+
# opacity:: textbox opacity, a float between 0 (transparent) and 1 (opaque)
|
88
|
+
def textbox(text, properties = {})
|
89
|
+
options = {
|
90
|
+
x: 0,
|
91
|
+
y: 0,
|
92
|
+
width: 0,
|
93
|
+
height: -1,
|
94
|
+
text_align: :center,
|
95
|
+
text_valign: :center,
|
96
|
+
text_padding: 0.1,
|
97
|
+
font: nil,
|
98
|
+
font_size: :fit_text,
|
99
|
+
max_font_size: nil,
|
100
|
+
font_color: [0,0,0],
|
101
|
+
stroke_color: nil,
|
102
|
+
stroke_width: 0,
|
103
|
+
box_color: nil,
|
104
|
+
border_color: nil,
|
105
|
+
border_width: 0,
|
106
|
+
box_radius: 0,
|
107
|
+
opacity: 1,
|
108
|
+
ctm: nil # ~= [1,0,0,1,0,0]
|
109
|
+
}
|
110
|
+
options.update properties
|
111
|
+
# reset the length and height to meaningful values, if negative
|
112
|
+
options[:width] = mediabox[2] - options[:x] + options[:width] if options[:width] <= 0
|
113
|
+
options[:height] = mediabox[3] - options[:y] + options[:height] if options[:height] <= 0
|
114
|
+
|
115
|
+
# reset the padding value
|
116
|
+
options[:text_padding] = 0 if options[:text_padding].to_f >= 1
|
117
|
+
|
118
|
+
# create box stream
|
119
|
+
box_stream = ""
|
120
|
+
# set graphic state for box
|
121
|
+
if options[:box_color] || (options[:border_width].to_i > 0 && options[:border_color])
|
122
|
+
# compute x and y position for text
|
123
|
+
x = options[:x]
|
124
|
+
y = options[:y]
|
125
|
+
|
126
|
+
# set graphic state for the box
|
127
|
+
box_stream << "q\n"
|
128
|
+
box_stream << "#{options[:ctm].join ' '} cm\n" if options[:ctm]
|
129
|
+
box_graphic_state = { ca: options[:opacity], CA: options[:opacity], LW: options[:border_width], LC: 0, LJ: 0, LD: 0}
|
130
|
+
if options[:box_radius] != 0 # if the text box has rounded corners
|
131
|
+
box_graphic_state[:LC], box_graphic_state[:LJ] = 2, 1
|
132
|
+
end
|
133
|
+
box_graphic_state = graphic_state box_graphic_state # adds the graphic state to Resources and gets the reference
|
134
|
+
box_stream << "#{PDFOperations._object_to_pdf box_graphic_state} gs\n"
|
135
|
+
|
136
|
+
# the following line was removed for Acrobat Reader compatability
|
137
|
+
# box_stream << "DeviceRGB CS\nDeviceRGB cs\n"
|
138
|
+
|
139
|
+
if options[:box_color]
|
140
|
+
box_stream << "#{options[:box_color].join(' ')} rg\n"
|
141
|
+
end
|
142
|
+
if options[:border_width].to_i > 0 && options[:border_color]
|
143
|
+
box_stream << "#{options[:border_color].join(' ')} RG\n"
|
144
|
+
end
|
145
|
+
# create the path
|
146
|
+
radius = options[:box_radius]
|
147
|
+
half_radius = (radius.to_f / 2).round 4
|
148
|
+
## set starting point
|
149
|
+
box_stream << "#{options[:x] + radius} #{options[:y]} m\n"
|
150
|
+
## buttom and right corner - first line and first corner
|
151
|
+
box_stream << "#{options[:x] + options[:width] - radius} #{options[:y]} l\n" #buttom
|
152
|
+
if options[:box_radius] != 0 # make first corner, if not straight.
|
153
|
+
box_stream << "#{options[:x] + options[:width] - half_radius} #{options[:y]} "
|
154
|
+
box_stream << "#{options[:x] + options[:width]} #{options[:y] + half_radius} "
|
155
|
+
box_stream << "#{options[:x] + options[:width]} #{options[:y] + radius} c\n"
|
156
|
+
end
|
157
|
+
## right and top-right corner
|
158
|
+
box_stream << "#{options[:x] + options[:width]} #{options[:y] + options[:height] - radius} l\n"
|
159
|
+
if options[:box_radius] != 0
|
160
|
+
box_stream << "#{options[:x] + options[:width]} #{options[:y] + options[:height] - half_radius} "
|
161
|
+
box_stream << "#{options[:x] + options[:width] - half_radius} #{options[:y] + options[:height]} "
|
162
|
+
box_stream << "#{options[:x] + options[:width] - radius} #{options[:y] + options[:height]} c\n"
|
163
|
+
end
|
164
|
+
## top and top-left corner
|
165
|
+
box_stream << "#{options[:x] + radius} #{options[:y] + options[:height]} l\n"
|
166
|
+
if options[:box_radius] != 0
|
167
|
+
box_stream << "#{options[:x] + half_radius} #{options[:y] + options[:height]} "
|
168
|
+
box_stream << "#{options[:x]} #{options[:y] + options[:height] - half_radius} "
|
169
|
+
box_stream << "#{options[:x]} #{options[:y] + options[:height] - radius} c\n"
|
170
|
+
end
|
171
|
+
## left and buttom-left corner
|
172
|
+
box_stream << "#{options[:x]} #{options[:y] + radius} l\n"
|
173
|
+
if options[:box_radius] != 0
|
174
|
+
box_stream << "#{options[:x]} #{options[:y] + half_radius} "
|
175
|
+
box_stream << "#{options[:x] + half_radius} #{options[:y]} "
|
176
|
+
box_stream << "#{options[:x] + radius} #{options[:y]} c\n"
|
177
|
+
end
|
178
|
+
# fill / stroke path
|
179
|
+
box_stream << "h\n"
|
180
|
+
if options[:box_color] && options[:border_width].to_i > 0 && options[:border_color]
|
181
|
+
box_stream << "B\n"
|
182
|
+
elsif options[:box_color] # fill if fill color is set
|
183
|
+
box_stream << "f\n"
|
184
|
+
elsif options[:border_width].to_i > 0 && options[:border_color] # stroke if border is set
|
185
|
+
box_stream << "S\n"
|
186
|
+
end
|
187
|
+
|
188
|
+
# exit graphic state for the box
|
189
|
+
box_stream << "Q\n"
|
190
|
+
end
|
191
|
+
contents << box_stream
|
192
|
+
|
193
|
+
# reset x,y by text alignment - x,y are calculated from the buttom left
|
194
|
+
# each unit (1) is 1/72 Inch
|
195
|
+
# create text stream
|
196
|
+
text_stream = ""
|
197
|
+
if text.to_s != "" && options[:font_size] != 0 && (options[:font_color] || options[:stroke_color])
|
198
|
+
# compute x and y position for text
|
199
|
+
x = options[:x] + (options[:width]*options[:text_padding])
|
200
|
+
y = options[:y] + (options[:height]*options[:text_padding])
|
201
|
+
|
202
|
+
# set the fonts (fonts array, with :Helvetica as fallback).
|
203
|
+
fonts = [*options[:font], :Helvetica]
|
204
|
+
# fit text in box, if requested
|
205
|
+
font_size = options[:font_size]
|
206
|
+
if options[:font_size] == :fit_text
|
207
|
+
font_size = self.fit_text text, fonts, (options[:width]*(1-options[:text_padding])), (options[:height]*(1-options[:text_padding]))
|
208
|
+
font_size = options[:max_font_size] if options[:max_font_size] && font_size > options[:max_font_size]
|
209
|
+
end
|
210
|
+
|
211
|
+
text_size = dimensions_of text, fonts, font_size
|
212
|
+
|
213
|
+
if options[:text_align] == :center
|
214
|
+
x = ( ( options[:width]*(1-(2*options[:text_padding])) ) - text_size[0] )/2 + x
|
215
|
+
elsif options[:text_align] == :right
|
216
|
+
x = ( ( options[:width]*(1-(1.5*options[:text_padding])) ) - text_size[0] ) + x
|
217
|
+
end
|
218
|
+
if options[:text_valign] == :center
|
219
|
+
y = ( ( options[:height]*(1-(2*options[:text_padding])) ) - text_size[1] )/2 + y
|
220
|
+
elsif options[:text_valign] == :top
|
221
|
+
y = ( options[:height]*(1-(1.5*options[:text_padding])) ) - text_size[1] + y
|
222
|
+
end
|
223
|
+
|
224
|
+
# set graphic state for text
|
225
|
+
text_stream << "q\n"
|
226
|
+
text_stream << "#{options[:ctm].join ' '} cm\n" if options[:ctm]
|
227
|
+
text_graphic_state = graphic_state({ca: options[:opacity], CA: options[:opacity], LW: options[:stroke_width].to_f, LC: 2, LJ: 1, LD: 0 })
|
228
|
+
text_stream << "#{PDFOperations._object_to_pdf text_graphic_state} gs\n"
|
229
|
+
|
230
|
+
# the following line was removed for Acrobat Reader compatability
|
231
|
+
# text_stream << "DeviceRGB CS\nDeviceRGB cs\n"
|
232
|
+
|
233
|
+
# set text render mode
|
234
|
+
if options[:font_color]
|
235
|
+
text_stream << "#{options[:font_color].join(' ')} rg\n"
|
236
|
+
end
|
237
|
+
if options[:stroke_width].to_i > 0 && options[:stroke_color]
|
238
|
+
text_stream << "#{options[:stroke_color].join(' ')} RG\n"
|
239
|
+
if options[:font_color]
|
240
|
+
text_stream << "2 Tr\n"
|
241
|
+
else
|
242
|
+
final_stream << "1 Tr\n"
|
243
|
+
end
|
244
|
+
elsif options[:font_color]
|
245
|
+
text_stream << "0 Tr\n"
|
246
|
+
else
|
247
|
+
text_stream << "3 Tr\n"
|
248
|
+
end
|
249
|
+
# format text object(s)
|
250
|
+
# text_stream << "#{options[:font_color].join(' ')} rg\n" # sets the color state
|
251
|
+
encode(text, fonts).each do |encoded|
|
252
|
+
text_stream << "BT\n" # the Begine Text marker
|
253
|
+
text_stream << PDFOperations._format_name_to_pdf(set_font encoded[0]) # Set font name
|
254
|
+
text_stream << " #{font_size.round 3} Tf\n" # set font size and add font operator
|
255
|
+
text_stream << "#{x.round 4} #{y.round 4} Td\n" # set location for text object
|
256
|
+
text_stream << ( encoded[1] ) # insert the encoded string to the stream
|
257
|
+
text_stream << " Tj\n" # the Text object operator and the End Text marker
|
258
|
+
text_stream << "ET\n" # the Text object operator and the End Text marker
|
259
|
+
x += encoded[2]/1000*font_size #update text starting point
|
260
|
+
y -= encoded[3]/1000*font_size #update text starting point
|
261
|
+
end
|
262
|
+
# exit graphic state for text
|
263
|
+
text_stream << "Q\n"
|
264
|
+
end
|
265
|
+
contents << text_stream
|
266
|
+
|
267
|
+
self
|
268
|
+
end
|
269
|
+
# gets the dimentions (width and height) of the text, as it will be printed in the PDF.
|
270
|
+
#
|
271
|
+
# text:: the text to measure
|
272
|
+
# font:: a font name or an Array of font names. Font names should be registered fonts. The 14 standard fonts are pre regitered with the font library.
|
273
|
+
# size:: the size of the font (defaults to 1000 points).
|
274
|
+
def dimensions_of(text, fonts, size = 1000)
|
275
|
+
Fonts.dimensions_of text, fonts, size
|
276
|
+
end
|
277
|
+
# this method returns the size for which the text fits the requested metrices
|
278
|
+
# the size is type Float and is rather exact
|
279
|
+
# if the text cannot fit such a small place, returns zero (0).
|
280
|
+
# maximum font size possible is set to 100,000 - which should be big enough for anything
|
281
|
+
# text:: the text to fit
|
282
|
+
# font:: the font name. @see font
|
283
|
+
# length:: the length to fit
|
284
|
+
# height:: the height to fit (optional - normally length is the issue)
|
285
|
+
def fit_text(text, font, length, height = 10000000)
|
286
|
+
size = 100000
|
287
|
+
size_array = [size]
|
288
|
+
metrics = Fonts.dimensions_of text, font, size
|
289
|
+
if metrics[0] > length
|
290
|
+
size_array << size * length/metrics[0]
|
291
|
+
end
|
292
|
+
if metrics[1] > height
|
293
|
+
size_array << size * height/metrics[1]
|
294
|
+
end
|
295
|
+
size_array.min
|
296
|
+
end
|
297
|
+
|
298
|
+
|
299
|
+
# accessor (getter) for the :Resources element of the page
|
300
|
+
def resources
|
301
|
+
self[:Resources]
|
302
|
+
end
|
303
|
+
|
304
|
+
# This method moves the Page[:Rotate] property into the page's data stream, so that
|
305
|
+
# "what you see is what you get".
|
306
|
+
#
|
307
|
+
# This is usful in cases where there might be less control over the source PDF files,
|
308
|
+
# and the user assums that the PDF page's data is the same as the PDF's pages
|
309
|
+
# on screen display (Rotate rotates a page but leaves the data in the original orientation).
|
310
|
+
#
|
311
|
+
# The method returns the page object, thus allowing method chaining (i.e. `page[:Rotate] = 90; page.textbox('hello!').fix_rotation.textbox('hello!')`)
|
312
|
+
def fix_rotation
|
313
|
+
return self if self[:Rotate].to_f == 0.0 || mediabox.nil?
|
314
|
+
# calculate the rotation
|
315
|
+
r = self[:Rotate].to_f * Math::PI / 180
|
316
|
+
s = Math.sin(r).round 6
|
317
|
+
c = Math.cos(r).round 6
|
318
|
+
ctm = [c, s, -s, c]
|
319
|
+
# calculate the translation (move the origin of x,y to the new origin).
|
320
|
+
x = mediabox[2] - mediabox[0]
|
321
|
+
y = mediabox[3] - mediabox[1]
|
322
|
+
ctm.push( ( (x*c).abs - x*c + (y*s).abs + y*s )/2 , ( (x*s).abs - x*s + (y*c).abs - y*c )/2 )
|
323
|
+
|
324
|
+
# insert the rotation stream into the current content stream
|
325
|
+
insert_object "q\n#{ctm.join ' '} cm\n", 0
|
326
|
+
# close the rotation stream
|
327
|
+
insert_object CONTENT_CONTAINER_END
|
328
|
+
# reset the mediabox and cropbox values - THIS IS ONLY FOR ORIENTATION CHANGE...
|
329
|
+
if ((self[:Rotate].to_f / 90)%2) != 0
|
330
|
+
self[:MediaBox] = self[:MediaBox].values_at(1,0,3,2)
|
331
|
+
self[:CropBox] = self[:CropBox].values_at(1,0,3,2) if self[:CropBox]
|
332
|
+
end
|
333
|
+
# reset the Rotate property
|
334
|
+
self.delete :Rotate
|
335
|
+
# re-initialize the content stream, so that future inserts aren't rotated
|
336
|
+
init_contents
|
337
|
+
|
338
|
+
# always return self, for chaining.
|
339
|
+
self
|
340
|
+
end
|
341
|
+
|
342
|
+
# rotate the page 90 degrees counter clockwise
|
343
|
+
def rotate_left
|
344
|
+
self[:Rotate] = self[:Rotate].to_f + 90
|
345
|
+
fix_rotation
|
346
|
+
end
|
347
|
+
# rotate the page 90 degrees clockwise
|
348
|
+
def rotate_right
|
349
|
+
self[:Rotate] = self[:Rotate].to_f - 90
|
350
|
+
fix_rotation
|
351
|
+
end
|
352
|
+
# rotate the page by 180 degrees
|
353
|
+
def rotate_180
|
354
|
+
self[:Rotate] = self[:Rotate].to_f +180
|
355
|
+
fix_rotation
|
356
|
+
end
|
357
|
+
# get or set (by clockwise rotation) the page's orientation
|
358
|
+
#
|
359
|
+
# accepts one optional parameter:
|
360
|
+
# force:: to get the orientation, pass nil. to set the orientatiom, set fource to either :portrait or :landscape. defaults to nil (get orientation).
|
361
|
+
# clockwise:: sets the rotation directions. defaults to true (clockwise rotation).
|
362
|
+
#
|
363
|
+
# returns the current orientation (:portrait or :landscape) if used to get the orientation.
|
364
|
+
# otherwise, if used to set the orientation, returns the page object to allow method chaining.
|
365
|
+
#
|
366
|
+
# * Notice: a square page always returns the :portrait value and is ignored when trying to set the orientation.
|
367
|
+
def orientation force = nil, clockwise = true
|
368
|
+
a = self[:CropBox] || self[:MediaBox]
|
369
|
+
unless force
|
370
|
+
return (a[2] - a[0] > a[3] - a[1]) ? :landscape : :portrait
|
371
|
+
end
|
372
|
+
unless orientation == force || (a[2] - a[0] == a[3] - a[1])
|
373
|
+
self[:Rotate] = 0;
|
374
|
+
clockwise ? rotate_right : rotate_left
|
375
|
+
end
|
376
|
+
self
|
377
|
+
end
|
378
|
+
|
379
|
+
|
380
|
+
|
381
|
+
###################################
|
382
|
+
# protected methods
|
383
|
+
|
384
|
+
protected
|
385
|
+
|
386
|
+
# accessor (getter) for the stream in the :Contents element of the page
|
387
|
+
# after getting the string object, you can operate on it but not replace it (use << or other String methods).
|
388
|
+
def contents
|
389
|
+
@contents ||= init_contents
|
390
|
+
end
|
391
|
+
#initializes the content stream in case it was not initialized before
|
392
|
+
def init_contents
|
393
|
+
@contents = ''
|
394
|
+
insert_object @contents
|
395
|
+
@contents
|
396
|
+
end
|
397
|
+
|
398
|
+
# adds a string or an object to the content stream, at the location indicated
|
399
|
+
#
|
400
|
+
# accepts:
|
401
|
+
# object:: can be a string or a hash object
|
402
|
+
# location:: can be any numeral related to the possition in the :Contents array. defaults to -1 == insert at the end.
|
403
|
+
def insert_object object, location = -1
|
404
|
+
object = { is_reference_only: true , referenced_object: {indirect_reference_id: 0, raw_stream_content: object} } if object.is_a?(String)
|
405
|
+
raise TypeError, "expected a String or Hash object." unless object.is_a?(Hash)
|
406
|
+
unless self[:Contents].is_a?(Array)
|
407
|
+
self[:Contents] = [ self[:Contents] ].compact
|
408
|
+
end
|
409
|
+
self[:Contents].insert location, object
|
410
|
+
self
|
411
|
+
end
|
412
|
+
|
413
|
+
#returns the basic font name used internally
|
414
|
+
def base_font_name
|
415
|
+
@base_font_name ||= "Writer" + SecureRandom.hex(7) + "PDF"
|
416
|
+
end
|
417
|
+
# creates a font object and adds the font to the resources dictionary
|
418
|
+
# returns the name of the font for the content stream.
|
419
|
+
# font:: a Symbol of one of the fonts registered in the library, or:
|
420
|
+
# - :"Times-Roman"
|
421
|
+
# - :"Times-Bold"
|
422
|
+
# - :"Times-Italic"
|
423
|
+
# - :"Times-BoldItalic"
|
424
|
+
# - :Helvetica
|
425
|
+
# - :"Helvetica-Bold"
|
426
|
+
# - :"Helvetica-BoldOblique"
|
427
|
+
# - :"Helvetica- Oblique"
|
428
|
+
# - :Courier
|
429
|
+
# - :"Courier-Bold"
|
430
|
+
# - :"Courier-Oblique"
|
431
|
+
# - :"Courier-BoldOblique"
|
432
|
+
# - :Symbol
|
433
|
+
# - :ZapfDingbats
|
434
|
+
def set_font(font = :Helvetica)
|
435
|
+
# if the font exists, return it's name
|
436
|
+
resources[:Font] ||= {}
|
437
|
+
resources[:Font].each do |k,v|
|
438
|
+
if v.is_a?(Fonts::Font) && v.name && v.name == font
|
439
|
+
return k
|
440
|
+
end
|
441
|
+
end
|
442
|
+
# set a secure name for the font
|
443
|
+
name = (base_font_name + (resources[:Font].length + 1).to_s).to_sym
|
444
|
+
# get font object
|
445
|
+
font_object = Fonts.get_font(font)
|
446
|
+
# return false if the font wan't found in the library.
|
447
|
+
return false unless font_object
|
448
|
+
# add object to reasource
|
449
|
+
resources[:Font][name] = font_object
|
450
|
+
#return name
|
451
|
+
name
|
452
|
+
end
|
453
|
+
# register or get a registered graphic state dictionary.
|
454
|
+
# the method returns the name of the graphos state, for use in a content stream.
|
455
|
+
def graphic_state(graphic_state_dictionary = {})
|
456
|
+
# if the graphic state exists, return it's name
|
457
|
+
resources[:ExtGState] ||= {}
|
458
|
+
resources[:ExtGState].each do |k,v|
|
459
|
+
if v.is_a?(Hash) && v == graphic_state_dictionary
|
460
|
+
return k
|
461
|
+
end
|
462
|
+
end
|
463
|
+
# set graphic state type
|
464
|
+
graphic_state_dictionary[:Type] = :ExtGState
|
465
|
+
# set a secure name for the graphic state
|
466
|
+
name = (SecureRandom.hex(9)).to_sym
|
467
|
+
# add object to reasource
|
468
|
+
resources[:ExtGState][name] = graphic_state_dictionary
|
469
|
+
#return name
|
470
|
+
name
|
471
|
+
end
|
472
|
+
|
473
|
+
# encodes the text in an array of [:font_name, <PDFHexString>] for use in textbox
|
474
|
+
def encode text, fonts
|
475
|
+
# text must be a unicode string and fonts must be an array.
|
476
|
+
# this is an internal method, don't perform tests.
|
477
|
+
fonts_array = []
|
478
|
+
fonts.each do |name|
|
479
|
+
f = Fonts.get_font name
|
480
|
+
fonts_array << f if f
|
481
|
+
end
|
482
|
+
|
483
|
+
# before starting, we should reorder any RTL content in the string
|
484
|
+
text = reorder_rtl_content text
|
485
|
+
|
486
|
+
out = []
|
487
|
+
text.chars.each do |c|
|
488
|
+
fonts_array.each_index do |i|
|
489
|
+
if fonts_array[i].cmap.nil? || (fonts_array[i].cmap && fonts_array[i].cmap[c])
|
490
|
+
#add to array
|
491
|
+
if out.last.nil? || out.last[0] != fonts[i]
|
492
|
+
out.last[1] << ">" unless out.last.nil?
|
493
|
+
out << [fonts[i], "<" , 0, 0]
|
494
|
+
end
|
495
|
+
out.last[1] << ( fonts_array[i].cmap.nil? ? ( c.unpack("H*")[0] ) : (fonts_array[i].cmap[c]) )
|
496
|
+
if fonts_array[i].metrics[c]
|
497
|
+
out.last[2] += fonts_array[i].metrics[c][:wx].to_f
|
498
|
+
out.last[3] += fonts_array[i].metrics[c][:wy].to_f
|
499
|
+
end
|
500
|
+
break
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
out.last[1] << ">" if out.last
|
505
|
+
out
|
506
|
+
end
|
507
|
+
|
508
|
+
# a very primitive text reordering algorithm... I was lazy...
|
509
|
+
# ...still, it works (I think).
|
510
|
+
def reorder_rtl_content text
|
511
|
+
rtl_characters = "\u05d0-\u05ea\u05f0-\u05f4\u0600-\u06ff\u0750-\u077f"
|
512
|
+
rtl_replaces = { '(' => ')', ')' => '(',
|
513
|
+
'[' => ']', ']'=>'[',
|
514
|
+
'{' => '}', '}'=>'{',
|
515
|
+
'<' => '>', '>'=>'<',
|
516
|
+
}
|
517
|
+
return text unless text =~ /[#{rtl_characters}]/
|
518
|
+
|
519
|
+
out = []
|
520
|
+
scanner = StringScanner.new text
|
521
|
+
until scanner.eos? do
|
522
|
+
if scanner.scan /[#{rtl_characters} ]/
|
523
|
+
out.unshift scanner.matched
|
524
|
+
elsif scanner.scan /[^#{rtl_characters}]+/
|
525
|
+
if out.empty? && scanner.matched.match(/[\s]$/) && !scanner.eos?
|
526
|
+
white_space_to_move = scanner.matched.match(/[\s]+$/).to_s
|
527
|
+
out.unshift scanner.matched[0..-1-white_space_to_move.length]
|
528
|
+
out.unshift white_space_to_move
|
529
|
+
elsif scanner.matched.match /^[\(\)\[\]\{\}\<\>]$/
|
530
|
+
out.unshift rtl_replaces[scanner.matched]
|
531
|
+
else
|
532
|
+
out.unshift scanner.matched
|
533
|
+
end
|
534
|
+
end
|
535
|
+
end
|
536
|
+
out.join.strip
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
end
|
541
|
+
|
542
|
+
|
543
|
+
|
544
|
+
|
545
|
+
|