combine_pdf 0.2.21 → 0.2.27

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.
@@ -5,882 +5,869 @@
5
5
  ## is subject to the same license.
6
6
  ########################################################
7
7
 
8
-
9
-
10
-
11
8
  module CombinePDF
12
-
13
- # This module injects page editing methods into existing page objects and the PDFWriter objects.
14
- module Page_Methods
15
- include Renderer
16
-
17
- # holds the string that starts a PDF graphic state container - used for wrapping malformed PDF content streams.
18
- CONTENT_CONTAINER_START = 'q'
19
- # holds the string that ends a PDF graphic state container - used for wrapping malformed PDF content streams.
20
- CONTENT_CONTAINER_MIDDLE = "Q\nq"
21
- # holds the string that ends a PDF graphic state container - used for wrapping malformed PDF content streams.
22
- CONTENT_CONTAINER_END = 'Q'
23
-
24
- # accessor (getter) for the secure_injection setting
25
- def secure_injection
26
- warn "**Deprecation Warning**: the `Page_Methods#secure_injection`, `Page_Methods#make_unsecure` and `Page_Methods#make_secure` methods are deprecated. Use `Page_Methods#copy(true)` for safeguarding against font/resource conflicts when 'stamping' one PDF page over another."
27
- @secure_injection
28
- end
29
- # accessor (setter) for the secure_injection setting
30
- def secure_injection= safe
31
- warn "**Deprecation Warning**: the `Page_Methods#secure_injection`, `Page_Methods#make_unsecure` and `Page_Methods#make_secure` methods are deprecated. Use `Page_Methods#copy(true)` for safeguarding against font/resource conflicts when 'stamping' one PDF page over another."
32
- @secure_injection = safe
33
- end
34
- # sets secure_injection to `true` and returns self, allowing for chaining methods
35
- def make_secure
36
- warn "**Deprecation Warning**: the `Page_Methods#secure_injection`, `Page_Methods#make_unsecure` and `Page_Methods#make_secure` methods are deprecated. Use `Page_Methods#copy(true)` for safeguarding against font/resource conflicts when 'stamping' one PDF page over another."
37
- @secure_injection = true
38
- self
39
- end
40
- # sets secure_injection to `false` and returns self, allowing for chaining methods
41
- def make_unsecure
42
- warn "**Deprecation Warning**: the `Page_Methods#secure_injection`, `Page_Methods#make_unsecure` and `Page_Methods#make_secure` methods are deprecated. Use `Page_Methods#copy(true)` for safeguarding against font/resource conflicts when 'stamping' one PDF page over another."
43
- @secure_injection = false
44
- self
45
- end
46
-
47
- # the injection method
48
- def << obj
49
- inject_page obj, true
50
- end
51
- def >> obj
52
- inject_page obj, false
53
- end
54
- def inject_page obj, top = true
55
-
56
- raise TypeError, "couldn't inject data, expecting a PDF page (Hash type)" unless obj.is_a?(Page_Methods)
57
-
58
- obj = obj.copy( should_secure?(obj) ) #obj.copy(secure_injection)
59
-
60
- # following the reference chain and assigning a pointer to the correct Resouces object.
61
- # (assignments of Strings, Arrays and Hashes are pointers in Ruby, unless the .dup method is called)
62
-
63
- # injecting each of the values in the injected Page
64
- res = resources
65
- obj.resources.each do |key, new_val|
66
- unless PDF::PRIVATE_HASH_KEYS.include? key # keep CombinePDF structual data intact.
67
- if res[key].nil?
68
- res[key] = new_val
69
- elsif res[key].is_a?(Hash) && new_val.is_a?(Hash)
70
- new_val = new_val[:referenced_object] || new_val
71
- new_val.update (res[key][:referenced_object] || res[key]) # make sure the old values are respected
72
- (res[key][:referenced_object] || res[key]).update new_val # transfer old and new values to the injected page
73
- end #Do nothing if array - ot is the PROC array, which is an issue
74
- end
75
- end
76
- resources[:ProcSet] = [:PDF, :Text, :ImageB, :ImageC, :ImageI] # this was recommended by the ISO. 32000-1:2008
77
-
78
- if top # if this is a stamp (overlay)
79
- insert_content CONTENT_CONTAINER_START, 0
80
- insert_content CONTENT_CONTAINER_MIDDLE
81
- self[:Contents].concat obj[:Contents]
82
- insert_content CONTENT_CONTAINER_END
83
- else #if this was a watermark (underlay? would be lost if the page was scanned, as white might not be transparent)
84
- insert_content CONTENT_CONTAINER_MIDDLE, 0
85
- insert_content CONTENT_CONTAINER_START, 0
86
- self[:Contents].insert 1, *obj[:Contents]
87
- insert_content CONTENT_CONTAINER_END
88
- end
89
- init_contents
90
-
91
- self
92
- end
93
-
94
- # accessor (setter) for the :MediaBox element of the page
95
- # dimensions:: an Array consisting of four numbers (can be floats) setting the size of the media box.
96
- def mediabox=(dimensions = [0.0, 0.0, 612.0, 792.0])
97
- self[:MediaBox] = dimensions
98
- end
99
-
100
- # accessor (getter) for the :MediaBox element of the page
101
- def mediabox
102
- actual_object self[:MediaBox]
103
- end
104
-
105
- # accessor (setter) for the :CropBox element of the page
106
- # dimensions:: an Array consisting of four numbers (can be floats) setting the size of the media box.
107
- def cropbox=(dimensions = [0.0, 0.0, 612.0, 792.0])
108
- self[:CropBox] = dimensions
109
- end
110
-
111
- # accessor (getter) for the :CropBox element of the page
112
- def cropbox
113
- actual_object self[:CropBox]
114
- end
115
-
116
- # get page size
117
- def page_size
118
- cropbox || mediabox
119
- end
120
-
121
- # accessor (getter) for the :Resources element of the page
122
- def resources
123
- self[:Resources] ||= {}
124
- self[:Resources][:referenced_object] || self[:Resources]
125
- end
126
-
127
-
128
- # This method adds a simple text box to the Page represented by the PDFWriter class.
129
- # This function takes two values:
130
- # text:: the text to potin the box.
131
- # properties:: a Hash of box properties.
132
- # the symbols and values in the properties Hash could be any or all of the following:
133
- # x:: the left position of the box.
134
- # y:: the BUTTOM position of the box.
135
- # width:: the width/length of the box. negative values will be computed from edge of page. defaults to 0 (end of page).
136
- # height:: the height of the box. negative values will be computed from edge of page. defaults to 0 (end of page).
137
- # text_align:: symbol for horizontal text alignment, can be ":center" (default), ":right", ":left"
138
- # text_valign:: symbol for vertical text alignment, can be ":center" (default), ":top", ":buttom"
139
- # text_padding:: a Float between 0 and 1, setting the padding for the text. defaults to 0.05 (5%).
140
- # font:: a registered font name or an Array of names. defaults to ":Helvetica". The 14 standard fonts names are:
141
- # - :"Times-Roman"
142
- # - :"Times-Bold"
143
- # - :"Times-Italic"
144
- # - :"Times-BoldItalic"
145
- # - :Helvetica
146
- # - :"Helvetica-Bold"
147
- # - :"Helvetica-BoldOblique"
148
- # - :"Helvetica- Oblique"
149
- # - :Courier
150
- # - :"Courier-Bold"
151
- # - :"Courier-Oblique"
152
- # - :"Courier-BoldOblique"
153
- # - :Symbol
154
- # - :ZapfDingbats
155
- # font_size:: a Fixnum for the font size, or :fit_text to fit the text in the box. defaults to ":fit_text"
156
- # max_font_size:: if font_size is set to :fit_text, this will be the maximum font size. defaults to nil (no maximum)
157
- # 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.
158
- # 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).
159
- # stroke_width:: text stroke width in PDF units. defaults to 0 (none).
160
- # 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).
161
- # 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).
162
- # border_width:: border width in PDF units. defaults to nil (none).
163
- # box_radius:: border radius in PDF units. defaults to 0 (no corner rounding).
164
- # opacity:: textbox opacity, a float between 0 (transparent) and 1 (opaque)
165
- def textbox(text, properties = {})
166
- options = {
167
- x: 0,
168
- y: 0,
169
- width: 0,
170
- height: -1,
171
- text_align: :center,
172
- text_valign: :center,
173
- text_padding: 0.1,
174
- font: nil,
175
- font_size: :fit_text,
176
- max_font_size: nil,
177
- font_color: [0,0,0],
178
- stroke_color: nil,
179
- stroke_width: 0,
180
- box_color: nil,
181
- border_color: nil,
182
- border_width: 0,
183
- box_radius: 0,
184
- opacity: 1,
185
- ctm: nil # ~= [1,0,0,1,0,0]
186
- }
187
- options.update properties
188
- # reset the length and height to meaningful values, if negative
189
- options[:width] = mediabox[2] - options[:x] + options[:width] if options[:width] <= 0
190
- options[:height] = mediabox[3] - options[:y] + options[:height] if options[:height] <= 0
191
-
192
- # reset the padding value
193
- options[:text_padding] = 0 if options[:text_padding].to_f >= 1
194
-
195
- # create box stream
196
- box_stream = ""
197
- # set graphic state for box
198
- if options[:box_color] || (options[:border_width].to_i > 0 && options[:border_color])
199
- # compute x and y position for text
200
- x = options[:x]
201
- y = options[:y]
202
-
203
- # set graphic state for the box
204
- box_stream << "q\n"
205
- box_stream << "#{options[:ctm].join ' '} cm\n" if options[:ctm]
206
- box_graphic_state = { ca: options[:opacity], CA: options[:opacity], LW: options[:border_width], LC: 0, LJ: 0, LD: 0}
207
- if options[:box_radius] != 0 # if the text box has rounded corners
208
- box_graphic_state[:LC], box_graphic_state[:LJ] = 2, 1
209
- end
210
- box_graphic_state = graphic_state box_graphic_state # adds the graphic state to Resources and gets the reference
211
- box_stream << "#{object_to_pdf box_graphic_state} gs\n"
212
-
213
- # the following line was removed for Acrobat Reader compatability
214
- # box_stream << "DeviceRGB CS\nDeviceRGB cs\n"
215
-
216
- if options[:box_color]
217
- box_stream << "#{options[:box_color].join(' ')} rg\n"
218
- end
219
- if options[:border_width].to_i > 0 && options[:border_color]
220
- box_stream << "#{options[:border_color].join(' ')} RG\n"
221
- end
222
- # create the path
223
- radius = options[:box_radius]
224
- half_radius = (radius.to_f / 2).round 4
225
- ## set starting point
226
- box_stream << "#{options[:x] + radius} #{options[:y]} m\n"
227
- ## buttom and right corner - first line and first corner
228
- box_stream << "#{options[:x] + options[:width] - radius} #{options[:y]} l\n" #buttom
229
- if options[:box_radius] != 0 # make first corner, if not straight.
230
- box_stream << "#{options[:x] + options[:width] - half_radius} #{options[:y]} "
231
- box_stream << "#{options[:x] + options[:width]} #{options[:y] + half_radius} "
232
- box_stream << "#{options[:x] + options[:width]} #{options[:y] + radius} c\n"
233
- end
234
- ## right and top-right corner
235
- box_stream << "#{options[:x] + options[:width]} #{options[:y] + options[:height] - radius} l\n"
236
- if options[:box_radius] != 0
237
- box_stream << "#{options[:x] + options[:width]} #{options[:y] + options[:height] - half_radius} "
238
- box_stream << "#{options[:x] + options[:width] - half_radius} #{options[:y] + options[:height]} "
239
- box_stream << "#{options[:x] + options[:width] - radius} #{options[:y] + options[:height]} c\n"
240
- end
241
- ## top and top-left corner
242
- box_stream << "#{options[:x] + radius} #{options[:y] + options[:height]} l\n"
243
- if options[:box_radius] != 0
244
- box_stream << "#{options[:x] + half_radius} #{options[:y] + options[:height]} "
245
- box_stream << "#{options[:x]} #{options[:y] + options[:height] - half_radius} "
246
- box_stream << "#{options[:x]} #{options[:y] + options[:height] - radius} c\n"
247
- end
248
- ## left and buttom-left corner
249
- box_stream << "#{options[:x]} #{options[:y] + radius} l\n"
250
- if options[:box_radius] != 0
251
- box_stream << "#{options[:x]} #{options[:y] + half_radius} "
252
- box_stream << "#{options[:x] + half_radius} #{options[:y]} "
253
- box_stream << "#{options[:x] + radius} #{options[:y]} c\n"
254
- end
255
- # fill / stroke path
256
- box_stream << "h\n"
257
- if options[:box_color] && options[:border_width].to_i > 0 && options[:border_color]
258
- box_stream << "B\n"
259
- elsif options[:box_color] # fill if fill color is set
260
- box_stream << "f\n"
261
- elsif options[:border_width].to_i > 0 && options[:border_color] # stroke if border is set
262
- box_stream << "S\n"
263
- end
264
-
265
- # exit graphic state for the box
266
- box_stream << "Q\n"
267
- end
268
- contents << box_stream
269
-
270
- # reset x,y by text alignment - x,y are calculated from the buttom left
271
- # each unit (1) is 1/72 Inch
272
- # create text stream
273
- text_stream = ""
274
- if !text.to_s.empty? && options[:font_size] != 0 && (options[:font_color] || options[:stroke_color])
275
- # compute x and y position for text
276
- x = options[:x] + (options[:width]*options[:text_padding])
277
- y = options[:y] + (options[:height]*options[:text_padding])
278
-
279
- # set the fonts (fonts array, with :Helvetica as fallback).
280
- fonts = [*options[:font], :Helvetica]
281
- # fit text in box, if requested
282
- font_size = options[:font_size]
283
- if options[:font_size] == :fit_text
284
- font_size = self.fit_text text, fonts, (options[:width]*(1-options[:text_padding])), (options[:height]*(1-options[:text_padding]))
285
- font_size = options[:max_font_size] if options[:max_font_size] && font_size > options[:max_font_size]
286
- end
287
-
288
- text_size = dimensions_of text, fonts, font_size
289
-
290
- if options[:text_align] == :center
291
- x = ( ( options[:width]*(1-(2*options[:text_padding])) ) - text_size[0] )/2 + x
292
- elsif options[:text_align] == :right
293
- x = ( ( options[:width]*(1-(1.5*options[:text_padding])) ) - text_size[0] ) + x
294
- end
295
- if options[:text_valign] == :center
296
- y = ( ( options[:height]*(1-(2*options[:text_padding])) ) - text_size[1] )/2 + y
297
- elsif options[:text_valign] == :top
298
- y = ( options[:height]*(1-(1.5*options[:text_padding])) ) - text_size[1] + y
299
- end
300
-
301
- # set graphic state for text
302
- text_stream << "q\n"
303
- text_stream << "#{options[:ctm].join ' '} cm\n" if options[:ctm]
304
- text_graphic_state = graphic_state({ca: options[:opacity], CA: options[:opacity], LW: options[:stroke_width].to_f, LC: 2, LJ: 1, LD: 0 })
305
- text_stream << "#{object_to_pdf text_graphic_state} gs\n"
306
-
307
- # the following line was removed for Acrobat Reader compatability
308
- # text_stream << "DeviceRGB CS\nDeviceRGB cs\n"
309
-
310
- # set text render mode
311
- if options[:font_color]
312
- text_stream << "#{options[:font_color].join(' ')} rg\n"
313
- end
314
- if options[:stroke_width].to_i > 0 && options[:stroke_color]
315
- text_stream << "#{options[:stroke_color].join(' ')} RG\n"
316
- if options[:font_color]
317
- text_stream << "2 Tr\n"
318
- else
319
- final_stream << "1 Tr\n"
320
- end
321
- elsif options[:font_color]
322
- text_stream << "0 Tr\n"
323
- else
324
- text_stream << "3 Tr\n"
325
- end
326
- # format text object(s)
327
- # text_stream << "#{options[:font_color].join(' ')} rg\n" # sets the color state
328
- encode_text(text, fonts).each do |encoded|
329
- text_stream << "BT\n" # the Begine Text marker
330
- text_stream << format_name_to_pdf(set_font encoded[0]) # Set font name
331
- text_stream << " #{font_size.round 3} Tf\n" # set font size and add font operator
332
- text_stream << "#{x.round 4} #{y.round 4} Td\n" # set location for text object
333
- text_stream << ( encoded[1] ) # insert the encoded string to the stream
334
- text_stream << " Tj\n" # the Text object operator and the End Text marker
335
- text_stream << "ET\n" # the Text object operator and the End Text marker
336
- x += encoded[2]/1000*font_size #update text starting point
337
- y -= encoded[3]/1000*font_size #update text starting point
338
- end
339
- # exit graphic state for text
340
- text_stream << "Q\n"
341
- end
342
- contents << text_stream
343
-
344
- self
345
- end
346
- # gets the dimentions (width and height) of the text, as it will be printed in the PDF.
347
- #
348
- # text:: the text to measure
349
- # 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.
350
- # size:: the size of the font (defaults to 1000 points).
351
- def dimensions_of(text, fonts, size = 1000)
352
- Fonts.dimensions_of text, fonts, size
353
- end
354
- # this method returns the size for which the text fits the requested metrices
355
- # the size is type Float and is rather exact
356
- # if the text cannot fit such a small place, returns zero (0).
357
- # maximum font size possible is set to 100,000 - which should be big enough for anything
358
- # text:: the text to fit
359
- # font:: the font name. @see font
360
- # length:: the length to fit
361
- # height:: the height to fit (optional - normally length is the issue)
362
- def fit_text(text, font, length, height = 10000000)
363
- size = 100000
364
- size_array = [size]
365
- metrics = Fonts.dimensions_of text, font, size
366
- if metrics[0] > length
367
- size_array << size * length/metrics[0]
368
- end
369
- if metrics[1] > height
370
- size_array << size * height/metrics[1]
371
- end
372
- size_array.min
373
- end
374
-
375
-
376
- # This method moves the Page[:Rotate] property into the page's data stream, so that
377
- # "what you see is what you get".
378
- #
379
- # This is usful in cases where there might be less control over the source PDF files,
380
- # and the user assums that the PDF page's data is the same as the PDF's pages
381
- # on screen display (Rotate rotates a page but leaves the data in the original orientation).
382
- #
383
- # The method returns the page object, thus allowing method chaining (i.e. `page[:Rotate] = 90; page.textbox('hello!').fix_rotation.textbox('hello!')`)
384
- def fix_rotation
385
- return self if self[:Rotate].to_f == 0.0 || mediabox.nil?
386
- # calculate the rotation
387
- r = self[:Rotate].to_f * Math::PI / 180
388
- s = Math.sin(r).round 6
389
- c = Math.cos(r).round 6
390
- ctm = [c, s, -s, c]
391
- # calculate the translation (move the origin of x,y to the new origin).
392
- x = mediabox[2] - mediabox[0]
393
- y = mediabox[3] - mediabox[1]
394
- ctm.push( ( (x*c).abs - x*c + (y*s).abs + y*s )/2 , ( (x*s).abs - x*s + (y*c).abs - y*c )/2 )
395
-
396
- # insert the rotation stream into the current content stream
397
- insert_content "q\n#{ctm.join ' '} cm\n", 0
398
- # close the rotation stream
399
- insert_content CONTENT_CONTAINER_END
400
- # reset the mediabox and cropbox values - THIS IS ONLY FOR ORIENTATION CHANGE...
401
- if ((self[:Rotate].to_f / 90)%2) != 0
402
- self[:MediaBox] = self[:MediaBox].values_at(1,0,3,2)
403
- self[:CropBox] = self[:CropBox].values_at(1,0,3,2) if self[:CropBox]
404
- end
405
- # reset the Rotate property
406
- self.delete :Rotate
407
- # disconnect the content stream, so that future inserts aren't rotated
408
- @contents = false #init_contents
409
-
410
- # always return self, for chaining.
411
- self
412
- end
413
-
414
- # resizes the page relative to it's current viewport (either the cropbox or the mediabox), setting the new viewport to the requested size.
415
- #
416
- # accepts:
417
- # new_size:: an Array with four elements: [X0, Y0, X_max, Y_max]. For example, A4: `[0, 0, 595, 842]`. It is important that the first two numbers are 0 unless a special effect is attempted. If the first two numbers change, the final result might not be the size requested, but the nearest possible transformation (calling the method again will allow a better resizing).
418
- # conserve_aspect_ratio:: whether to keep the current content in the same aspect ratio or to allow streaching. Defaults to true - so that although the content is resized, it might not fill the new size completely.
419
- def resize new_size = nil, conserve_aspect_ratio = true
420
- return page_size unless new_size
421
- c_mediabox = mediabox
422
- c_cropbox = cropbox
423
- c_size = c_cropbox || c_mediabox
424
- x_ratio = 1.0 * (new_size[2]-new_size[0]) / (c_size[2])#-c_size[0])
425
- y_ratio = 1.0 * (new_size[3]-new_size[1]) / (c_size[3])#-c_size[1])
426
- x_move = new_size[0] - c_size[0]
427
- y_move = new_size[1] - c_size[1]
428
- puts "ctm will be: #{x_ratio.round(4).to_s} 0 0 #{y_ratio.round(4).to_s} #{x_move} #{y_move}"
429
- self[:MediaBox] = [(c_mediabox[0] + x_move), (c_mediabox[1] + y_move), ((c_mediabox[2] * x_ratio) + x_move ), ((c_mediabox[3] * y_ratio) + y_move)]
430
- self[:CropBox] = [(c_cropbox[0] + x_move), (c_cropbox[1] + y_move), ((c_cropbox[2] * x_ratio) + x_move), ((c_cropbox[3] * y_ratio) + y_move)] if c_cropbox
431
- x_ratio = y_ratio = [x_ratio, y_ratio].min if conserve_aspect_ratio
432
- # insert the rotation stream into the current content stream
433
- # insert_content "q\n#{x_ratio.round(4).to_s} 0 0 #{y_ratio.round(4).to_s} 0 0 cm\n1 0 0 1 #{x_move} #{y_move} cm\n", 0
434
- insert_content "q\n#{x_ratio.round(4).to_s} 0 0 #{y_ratio.round(4).to_s} #{x_move} #{y_move} cm\n", 0
435
- # close the rotation stream
436
- insert_content CONTENT_CONTAINER_END
437
- # disconnect the content stream, so that future inserts aren't rotated
438
- @contents = false #init_contents
439
-
440
- # always return self, for chaining.
441
- self
442
- end
443
-
444
- # crops the page using a <b>relative</b> size.
445
- #
446
- # `crop` will crop the page by updating it's MediaBox property using a <b>relative</b> crop box. i.e.,
447
- # when cropping a page with {#page_size} of [10,10,900,900] to [5,5,500,500], the resulting page size should be [15, 15, 510, 510] - allowing you to ignore a page's initial XY starting point when cropping.
448
- #
449
- # for an absolute cropping, simpy use the {#mediabox=} or {#cropbox=} methods, setting their value to the new {page_size}.
450
- #
451
- # accepts:
452
- # new_size:: an Array with four elements: [X0, Y0, X_max, Y_max]. For example, inch4(width)x6(length): `[200, 200, 488, 632]`
453
- def crop(new_size=nil)
454
- # no crop box? clear any cropping.
455
- return page_size if !new_size
456
- # type safety
457
- raise TypeError, "pdf.page\#crop expeceted an Array (or nil)" unless Array === new_size
458
-
459
- # set the MediaBox to the existing page size
460
- self[:MediaBox] = page_size
461
- # clear the CropBox
462
- self[:CropBox] = nil
463
- # update X0
464
- self[:MediaBox][0] += new_size[0]
465
- # update Y0
466
- self[:MediaBox][1] += new_size[1]
467
- # update X max IF the value is smaller then the existing value
468
- self[:MediaBox][2] = (self[:MediaBox][0] + new_size[2] - new_size[0]) if ((self[:MediaBox][0] + new_size[2] - new_size[0]) < self[:MediaBox][2])
469
- # update Y max IF the value is smaller then the existing value
470
- self[:MediaBox][3] = (self[:MediaBox][1] + new_size[3] - new_size[1]) if ((self[:MediaBox][1] + new_size[3] - new_size[1]) < self[:MediaBox][3])
471
- # return self for chaining
472
- self
473
- end
474
-
475
-
476
- # rotate the page 90 degrees counter clockwise
477
- def rotate_left
478
- self[:Rotate] = self[:Rotate].to_f + 90
479
- fix_rotation
480
- end
481
- # rotate the page 90 degrees clockwise
482
- def rotate_right
483
- self[:Rotate] = self[:Rotate].to_f - 90
484
- fix_rotation
485
- end
486
- # rotate the page by 180 degrees
487
- def rotate_180
488
- self[:Rotate] = self[:Rotate].to_f +180
489
- fix_rotation
490
- end
491
- # get or set (by clockwise rotation) the page's orientation
492
- #
493
- # accepts one optional parameter:
494
- # force:: to get the orientation, pass nil. to set the orientatiom, set fource to either :portrait or :landscape. defaults to nil (get orientation).
495
- # clockwise:: sets the rotation directions. defaults to true (clockwise rotation).
496
- #
497
- # returns the current orientation (:portrait or :landscape) if used to get the orientation.
498
- # otherwise, if used to set the orientation, returns the page object to allow method chaining.
499
- #
500
- # * Notice: a square page always returns the :portrait value and is ignored when trying to set the orientation.
501
- def orientation force = nil, clockwise = true
502
- a = page_size
503
- unless force
504
- return (a[2] - a[0] > a[3] - a[1]) ? :landscape : :portrait
505
- end
506
- unless orientation == force || (a[2] - a[0] == a[3] - a[1])
507
- self[:Rotate] = 0;
508
- clockwise ? rotate_right : rotate_left
509
- end
510
- self
511
- end
512
-
513
-
514
- # Writes a table to the current page, removing(!) the written rows from the table_data Array.
515
- #
516
- # since the table_data Array is updated, it is possible to call this method a few times,
517
- # each time creating or moving to the next page, until table_data.empty? returns true.
518
- #
519
- # accepts a Hash with any of the following keys as well as any of the PDFWriter#textbox options:
520
- # headers:: an Array of strings with the headers (will be repeated every page).
521
- # table_data:: as Array of Arrays, each containing a string for each column. the first row sets the number of columns. extra columns will be ignored.
522
- # font:: a registered or standard font name (see PDFWriter). defaults to nil (:Helvetica).
523
- # header_font:: a registered or standard font name for the headers (see PDFWriter). defaults to nil (the font for all the table rows).
524
- # max_font_size:: the maximum font size. if the string doesn't fit, it will be resized. defaults to 14.
525
- # column_widths:: an array of relative column widths ([1,2] will display only the first two columns, the second twice as big as the first). defaults to nil (even widths).
526
- # header_color:: the header color. defaults to [0.8, 0.8, 0.8] (light gray).
527
- # main_color:: main row color. defaults to nil (transparent / white).
528
- # alternate_color:: alternate row color. defaults to [0.95, 0.95, 0.95] (very light gray).
529
- # font_color:: font color. defaults to [0,0,0] (black).
530
- # border_color:: border color. defaults to [0,0,0] (black).
531
- # border_width:: border width in PDF units. defaults to 1.
532
- # header_align:: the header text alignment within each column (:right, :left, :center). defaults to :center.
533
- # row_align:: the row text alignment within each column. defaults to :left (:right for RTL table).
534
- # direction:: the table's writing direction (:ltr or :rtl). this reffers to the direction of the columns and doesn't effect text (rtl text is automatically recognized). defaults to :ltr.
535
- # max_rows:: the maximum number of rows to actually draw, INCLUDING the header row. deafults to 25.
536
- # xy:: an Array specifying the top-left corner of the table. defaulte to [page_width*0.1, page_height*0.9].
537
- # size:: an Array specifying the height and the width of the table. defaulte to [page_width*0.8, page_height*0.8].
538
- def write_table(options = {})
539
- defaults = {
540
- headers: nil,
541
- table_data: [[]],
542
- font: nil,
543
- header_font: nil,
544
- max_font_size: 14,
545
- column_widths: nil,
546
- header_color: [0.8, 0.8, 0.8],
547
- main_color: nil,
548
- alternate_color: [0.95, 0.95, 0.95],
549
- font_color: [0,0,0],
550
- border_color: [0,0,0],
551
- border_width: 1,
552
- header_align: :center,
553
- row_align: nil,
554
- direction: :ltr,
555
- max_rows: 25,
556
- xy: nil,
557
- size: nil
558
- }
559
- options = defaults.merge options
560
- raise "method call error! not enough rows allowed to create table" if (options[:max_rows].to_i < 1 && options[:headers]) || (options[:max_rows].to_i <= 0)
561
- options[:header_font] ||= options[:font]
562
- options[:row_align] ||= ( (options[:direction] == :rtl) ? :right : :left )
563
- options[:xy] ||= [( (page_size[2]-page_size[0])*0.1 ), ( (page_size[3]-page_size[1])*0.9 )]
564
- options[:size] ||= [( (page_size[2]-page_size[0])*0.8 ), ( (page_size[3]-page_size[1])*0.8 )]
565
- # assert table_data is an array of arrays
566
- return false unless (options[:table_data].select {|r| !r.is_a?(Array) }).empty?
567
- # compute sizes
568
- top = options[:xy][1]
569
- height = options[:size][1] / options[:max_rows]
570
- from_side = options[:xy][0]
571
- width = options[:size][0]
572
- columns = options[:table_data][0].length
573
- column_widths = []
574
- columns.times {|i| column_widths << (width/columns) }
575
- if options[:column_widths]
576
- scale = 0
577
- options[:column_widths].each {|w| scale += w}
578
- column_widths = []
579
- options[:column_widths].each { |w| column_widths << (width*w/scale) }
580
- end
581
- column_widths = column_widths.reverse if options[:direction] == :rtl
582
- # set count and start writing the data
583
- row_number = 1
584
-
585
- until (options[:table_data].empty? || row_number > options[:max_rows])
586
- # add headers
587
- if options[:headers] && row_number == 1
588
- x = from_side
589
- headers = options[:headers]
590
- headers = headers.reverse if options[:direction] == :rtl
591
- column_widths.each_index do |i|
592
- text = headers[i].to_s
593
- textbox text, {x: x, y: (top - (height*row_number)), width: column_widths[i], height: height, box_color: options[:header_color], text_align: options[:header_align] }.merge(options).merge({font: options[:header_font]})
594
- x += column_widths[i]
595
- end
596
- row_number += 1
597
- end
598
- x = from_side
599
- row_data = options[:table_data].shift
600
- row_data = row_data.reverse if options[:direction] == :rtl
601
- column_widths.each_index do |i|
602
- text = row_data[i].to_s
603
- box_color = (options[:alternate_color] && ( (row_number.odd? && options[:headers]) || row_number.even? ) ) ? options[:alternate_color] : options[:main_color]
604
- textbox text, {x: x, y: (top - (height*row_number)), width: column_widths[i], height: height, box_color: box_color, text_align: options[:row_align]}.merge(options)
605
- x += column_widths[i]
606
- end
607
- row_number += 1
608
- end
609
- self
610
- end
611
-
612
- # creates a copy of the page. if the :secure flag is set to true, the resource indentifiers (fonts etc') will be renamed in order to secure their uniqueness.
613
- def copy(secure = false)
614
- # since only the Content streams are modified (Resource hashes are created anew),
615
- # it should be safe (and a lot faster) to create a deep copy only for the content hashes and streams.
616
- delete :Parent
617
- prep_content_array
618
- page_copy = self.clone
619
- page_copy[:Contents] = page_copy[:Contents].map do |obj|
620
- obj = obj.dup
621
- obj[:referenced_object] = obj[:referenced_object].dup if obj[:referenced_object]
622
- obj[:referenced_object][:raw_stream_content] = obj[:referenced_object][:raw_stream_content].dup if obj[:referenced_object] && obj[:referenced_object][:raw_stream_content]
623
- obj
624
- end
625
- if page_copy[:Resources]
626
- page_res = page_copy[:Resources] = page_copy[:Resources].dup
627
- page_res = page_copy[:Resources][:referenced_object] = page_copy[:Resources][:referenced_object].dup if page_copy[:Resources][:referenced_object]
628
- page_res.each do |k, v|
629
- v = page_res[k] = v.dup if v.is_a?(Array) || v.is_a?(Hash)
630
- v = v[:referenced_object] = v[:referenced_object].dup if v.is_a?(Hash) && v[:referenced_object]
631
- v = v[:referenced_object] = v[:referenced_object].dup if v.is_a?(Hash) && v[:referenced_object]
632
- end
633
- end
634
- return page_copy.instance_exec(secure || @secure_injection) { |s| secure_for_copy if s ; init_contents; self }
635
- end
636
-
637
- ###################################
638
- # protected methods
639
-
640
- protected
641
-
642
- # accessor (getter) for the stream in the :Contents element of the page
643
- # after getting the string object, you can operate on it but not replace it (use << or other String methods).
644
- def contents
645
- @contents ||= init_contents
646
- end
647
- #initializes the content stream in case it was not initialized before
648
- def init_contents
649
- self[:Contents] = self[:Contents][:referenced_object][:indirect_without_dictionary] if self[:Contents].is_a?(Hash) && self[:Contents][:referenced_object] && self[:Contents][:referenced_object].is_a?(Hash) && self[:Contents][:referenced_object][:indirect_without_dictionary]
650
- self[:Contents] = [self[:Contents]] unless self[:Contents].is_a?(Array)
651
- self[:Contents].delete({ is_reference_only: true , referenced_object: {indirect_reference_id: 0, raw_stream_content: ''} })
652
- # un-nest any referenced arrays
653
- self[:Contents].map! {|s| actual_value(s).is_a?(Array) ? actual_value(s) : s}
654
- self[:Contents].flatten!
655
- self[:Contents].compact!
656
- # wrap content streams
657
- insert_content 'q', 0
658
- insert_content 'Q'
659
-
660
- # Prep content
661
- @contents = ''
662
- insert_content @contents
663
- @contents
664
- end
665
-
666
- # adds a string or an object to the content stream, at the location indicated
667
- #
668
- # accepts:
669
- # object:: can be a string or a hash object
670
- # location:: can be any numeral related to the possition in the :Contents array. defaults to -1 == insert at the end.
671
- def insert_content object, location = -1
672
- object = { is_reference_only: true , referenced_object: {indirect_reference_id: 0, raw_stream_content: object} } if object.is_a?(String)
673
- raise TypeError, "expected a String or Hash object." unless object.is_a?(Hash)
674
- prep_content_array
675
- self[:Contents].insert location, object
676
- self[:Contents].flatten!
677
- self
678
- end
679
-
680
- def prep_content_array
681
- return self if self[:Contents].is_a?(Array)
682
- init_contents
683
- # self[:Contents] = self[:Contents][:referenced_object] if self[:Contents].is_a?(Hash) && self[:Contents][:referenced_object] && self[:Contents][:referenced_object].is_a?(Array)
684
- # self[:Contents] = self[:Contents][:indirect_without_dictionary] if self[:Contents].is_a?(Hash) && self[:Contents][:indirect_without_dictionary] && self[:Contents][:indirect_without_dictionary].is_a?(Array)
685
- # self[:Contents] = [self[:Contents]] unless self[:Contents].is_a?(Array)
686
- # self[:Contents].compact!
687
- self
688
- end
689
-
690
- #returns the basic font name used internally
691
- def base_font_name
692
- @base_font_name ||= "Writer" + SecureRandom.hex(7) + "PDF"
693
- end
694
- # creates a font object and adds the font to the resources dictionary
695
- # returns the name of the font for the content stream.
696
- # font:: a Symbol of one of the fonts registered in the library, or:
697
- # - :"Times-Roman"
698
- # - :"Times-Bold"
699
- # - :"Times-Italic"
700
- # - :"Times-BoldItalic"
701
- # - :Helvetica
702
- # - :"Helvetica-Bold"
703
- # - :"Helvetica-BoldOblique"
704
- # - :"Helvetica- Oblique"
705
- # - :Courier
706
- # - :"Courier-Bold"
707
- # - :"Courier-Oblique"
708
- # - :"Courier-BoldOblique"
709
- # - :Symbol
710
- # - :ZapfDingbats
711
- def set_font(font = :Helvetica)
712
- # if the font exists, return it's name
713
- resources[:Font] ||= {}
714
- fonts_res = resources[:Font][:referenced_object] || resources[:Font]
715
- fonts_res.each do |k,v|
716
- if v.is_a?(Fonts::Font) && v.name && v.name == font
717
- return k
718
- end
719
- end
720
- # set a secure name for the font
721
- name = (base_font_name + (fonts_res.length + 1).to_s).to_sym
722
- # get font object
723
- font_object = Fonts.get_font(font)
724
- # return false if the font wan't found in the library.
725
- return false unless font_object
726
- # add object to reasource
727
- fonts_res[name] = font_object
728
- #return name
729
- name
730
- end
731
- # register or get a registered graphic state dictionary.
732
- # the method returns the name of the graphos state, for use in a content stream.
733
- def graphic_state(graphic_state_dictionary = {})
734
- # if the graphic state exists, return it's name
735
- resources[:ExtGState] ||= {}
736
- gs_res = resources[:ExtGState][:referenced_object] || resources[:ExtGState]
737
- gs_res.each do |k,v|
738
- if v.is_a?(Hash) && v == graphic_state_dictionary
739
- return k
740
- end
741
- end
742
- # set graphic state type
743
- graphic_state_dictionary[:Type] = :ExtGState
744
- # set a secure name for the graphic state
745
- name = (SecureRandom.hex(9)).to_sym
746
- # add object to reasource
747
- gs_res[name] = graphic_state_dictionary
748
- #return name
749
- name
750
- end
751
-
752
- # encodes the text in an array of [:font_name, <PDFHexString>] for use in textbox
753
- def encode_text text, fonts
754
- # text must be a unicode string and fonts must be an array.
755
- # this is an internal method, don't perform tests.
756
- fonts_array = []
757
- fonts.each do |name|
758
- f = Fonts.get_font name
759
- fonts_array << f if f
760
- end
761
-
762
- # before starting, we should reorder any RTL content in the string
763
- text = reorder_rtl_content text
764
-
765
- out = []
766
- text.chars.each do |c|
767
- fonts_array.each_index do |i|
768
- if fonts_array[i].cmap.nil? || (fonts_array[i].cmap && fonts_array[i].cmap[c])
769
- #add to array
770
- if out.last.nil? || out.last[0] != fonts[i]
771
- out.last[1] << ">" unless out.last.nil?
772
- out << [fonts[i], "<" , 0, 0]
773
- end
774
- out.last[1] << ( fonts_array[i].cmap.nil? ? ( c.unpack("H*")[0] ) : (fonts_array[i].cmap[c]) )
775
- if fonts_array[i].metrics[c]
776
- out.last[2] += fonts_array[i].metrics[c][:wx].to_f
777
- out.last[3] += fonts_array[i].metrics[c][:wy].to_f
778
- end
779
- break
780
- end
781
- end
782
- end
783
- out.last[1] << ">" if out.last
784
- out
785
- end
786
-
787
- # a very primitive text reordering algorithm... I was lazy...
788
- # ...still, it works (I think).
789
- def reorder_rtl_content text
790
- rtl_characters = "\u05d0-\u05ea\u05f0-\u05f4\u0600-\u06ff\u0750-\u077f"
791
- rtl_replaces = { '(' => ')', ')' => '(',
792
- '[' => ']', ']'=>'[',
793
- '{' => '}', '}'=>'{',
794
- '<' => '>', '>'=>'<',
795
- }
796
- return text unless text =~ /[#{rtl_characters}]/
797
-
798
- out = []
799
- scanner = StringScanner.new text
800
- until scanner.eos? do
801
- if scanner.scan /[#{rtl_characters} ]/
802
- out.unshift scanner.matched
803
- elsif scanner.scan /[^#{rtl_characters}]+/
804
- if out.empty? && scanner.matched.match(/[\s]$/) && !scanner.eos?
805
- white_space_to_move = scanner.matched.match(/[\s]+$/).to_s
806
- out.unshift scanner.matched[0..-1-white_space_to_move.length]
807
- out.unshift white_space_to_move
808
- elsif scanner.matched.match /^[\(\)\[\]\{\}\<\>]$/
809
- out.unshift rtl_replaces[scanner.matched]
810
- else
811
- out.unshift scanner.matched
812
- end
813
- end
814
- end
815
- out.join.strip
816
- end
817
-
818
-
819
- # copy_and_secure_for_injection(page)
820
- # - page is a page in the pages array, i.e.
821
- # pdf.pages[0]
822
- # takes a page object and:
823
- #
824
- # makes a deep copy of the page (Ruby defaults to pointers, so this will copy the memory).
825
- #
826
- # then it will rewrite the content stream with renamed resources, so as to avoid name conflicts.
827
- def secure_for_copy
828
- # initiate dictionary from old names to new names
829
- names_dictionary = {}
830
-
831
- # travel every dictionary to pick up names (keys), change them and add them to the dictionary
832
- res = self.resources
833
- res.each do |k,v|
834
- if actual_value(v).is_a?(Hash)
835
- # if k == :XObject
836
- # self[:Resources][k] = v.dup
837
- # next
838
- # end
839
- new_dictionary = {}
840
- new_name = "Combine" + SecureRandom.hex(7) + "PDF"
841
- i = 1
842
- actual_value(v).each do |old_key, value|
843
- new_key = (new_name + i.to_s).to_sym
844
- names_dictionary[old_key] = new_key
845
- new_dictionary[new_key] = value
846
- i += 1
847
- end
848
- res[k] = new_dictionary
849
- end
850
- end
851
-
852
- # now that we have replaced the names in the resources dictionaries,
853
- # it is time to replace the names inside the stream
854
- # we will need to make sure we have access to the stream injected
855
- # we will user PDFFilter.inflate_object
856
- self[:Contents].each do |c|
857
- stream = actual_value(c)
858
- PDFFilter.inflate_object stream
859
- names_dictionary.each do |old_key, new_key|
860
- stream[:raw_stream_content].gsub! object_to_pdf(old_key), object_to_pdf(new_key) ##### PRAY(!) that the parsed datawill be correctly reproduced!
861
- end
862
- # # # the following code isn't needed now that we wrap both the existing and incoming content streams.
863
- # # patch back to PDF defaults, for OCRed PDF files.
864
- # stream[:raw_stream_content] = "q\n0 0 0 rg\n0 0 0 RG\n0 Tr\n1 0 0 1 0 0 cm\n%s\nQ\n" % stream[:raw_stream_content]
865
- end
866
- self
867
- end
868
-
869
- # @return [true, false] returns true if there are two different resources sharing the same named reference.
870
- def should_secure?(page)
871
- # travel every dictionary to pick up names (keys), change them and add them to the dictionary
872
- res = actual_value(self.resources)
873
- foreign_res = actual_value(page.resources)
874
- tmp = nil
875
- res.each do |k,v|
876
- if ((v = actual_value(v)).is_a?(Hash) && (tmp = actual_value(foreign_res[k])).is_a?(Hash) )
877
- v.keys.each do |name| return true if tmp[name] && tmp[name] != v[name]
878
- end # else # Do nothing, this is taken care of elseware
879
- end
880
- end
881
- false
882
- end
883
-
884
- end
885
-
9
+ # This module injects page editing methods into existing page objects and the PDFWriter objects.
10
+ module Page_Methods
11
+ include Renderer
12
+
13
+ # holds the string that starts a PDF graphic state container - used for wrapping malformed PDF content streams.
14
+ CONTENT_CONTAINER_START = 'q'.freeze
15
+ # holds the string that ends a PDF graphic state container - used for wrapping malformed PDF content streams.
16
+ CONTENT_CONTAINER_MIDDLE = "Q\nq".freeze
17
+ # holds the string that ends a PDF graphic state container - used for wrapping malformed PDF content streams.
18
+ CONTENT_CONTAINER_END = 'Q'.freeze
19
+
20
+ # accessor (getter) for the secure_injection setting
21
+ def secure_injection
22
+ warn "**Deprecation Warning**: the `Page_Methods#secure_injection`, `Page_Methods#make_unsecure` and `Page_Methods#make_secure` methods are deprecated. Use `Page_Methods#copy(true)` for safeguarding against font/resource conflicts when 'stamping' one PDF page over another."
23
+ @secure_injection
24
+ end
25
+
26
+ # accessor (setter) for the secure_injection setting
27
+ def secure_injection=(safe)
28
+ warn "**Deprecation Warning**: the `Page_Methods#secure_injection`, `Page_Methods#make_unsecure` and `Page_Methods#make_secure` methods are deprecated. Use `Page_Methods#copy(true)` for safeguarding against font/resource conflicts when 'stamping' one PDF page over another."
29
+ @secure_injection = safe
30
+ end
31
+
32
+ # sets secure_injection to `true` and returns self, allowing for chaining methods
33
+ def make_secure
34
+ warn "**Deprecation Warning**: the `Page_Methods#secure_injection`, `Page_Methods#make_unsecure` and `Page_Methods#make_secure` methods are deprecated. Use `Page_Methods#copy(true)` for safeguarding against font/resource conflicts when 'stamping' one PDF page over another."
35
+ @secure_injection = true
36
+ self
37
+ end
38
+
39
+ # sets secure_injection to `false` and returns self, allowing for chaining methods
40
+ def make_unsecure
41
+ warn "**Deprecation Warning**: the `Page_Methods#secure_injection`, `Page_Methods#make_unsecure` and `Page_Methods#make_secure` methods are deprecated. Use `Page_Methods#copy(true)` for safeguarding against font/resource conflicts when 'stamping' one PDF page over another."
42
+ @secure_injection = false
43
+ self
44
+ end
45
+
46
+ # the injection method
47
+ def <<(obj)
48
+ inject_page obj, true
49
+ end
50
+
51
+ def >>(obj)
52
+ inject_page obj, false
53
+ end
54
+
55
+ def inject_page(obj, top = true)
56
+ raise TypeError, "couldn't inject data, expecting a PDF page (Hash type)" unless obj.is_a?(Page_Methods)
57
+
58
+ obj = obj.copy(should_secure?(obj)) # obj.copy(secure_injection)
59
+
60
+ # following the reference chain and assigning a pointer to the correct Resouces object.
61
+ # (assignments of Strings, Arrays and Hashes are pointers in Ruby, unless the .dup method is called)
62
+
63
+ # injecting each of the values in the injected Page
64
+ res = resources
65
+ obj.resources.each do |key, new_val|
66
+ if !PDF::PRIVATE_HASH_KEYS.include?(key) # keep CombinePDF structual data intact.
67
+ next unless res[key].nil?
68
+ res[key] = new_val
69
+ elsif res[key].is_a?(Hash) && new_val.is_a?(Hash)
70
+ new_val = new_val[:referenced_object] || new_val
71
+ new_val.update (res[key][:referenced_object] || res[key]) # make sure the old values are respected
72
+ (res[key][:referenced_object] || res[key]).update new_val # transfer old and new values to the injected page
73
+ # Do nothing if array - ot is the PROC array, which is an issue
74
+ end
75
+ end
76
+ resources[:ProcSet] = [:PDF, :Text, :ImageB, :ImageC, :ImageI] # this was recommended by the ISO. 32000-1:2008
77
+
78
+ if top # if this is a stamp (overlay)
79
+ insert_content CONTENT_CONTAINER_START, 0
80
+ insert_content CONTENT_CONTAINER_MIDDLE
81
+ self[:Contents].concat obj[:Contents]
82
+ insert_content CONTENT_CONTAINER_END
83
+ else # if this was a watermark (underlay? would be lost if the page was scanned, as white might not be transparent)
84
+ insert_content CONTENT_CONTAINER_MIDDLE, 0
85
+ insert_content CONTENT_CONTAINER_START, 0
86
+ self[:Contents].insert 1, *obj[:Contents]
87
+ insert_content CONTENT_CONTAINER_END
88
+ end
89
+ init_contents
90
+
91
+ self
92
+ end
93
+
94
+ # accessor (setter) for the :MediaBox element of the page
95
+ # dimensions:: an Array consisting of four numbers (can be floats) setting the size of the media box.
96
+ def mediabox=(dimensions = [0.0, 0.0, 612.0, 792.0])
97
+ self[:MediaBox] = dimensions
98
+ end
99
+
100
+ # accessor (getter) for the :MediaBox element of the page
101
+ def mediabox
102
+ actual_object self[:MediaBox]
103
+ end
104
+
105
+ # accessor (setter) for the :CropBox element of the page
106
+ # dimensions:: an Array consisting of four numbers (can be floats) setting the size of the media box.
107
+ def cropbox=(dimensions = [0.0, 0.0, 612.0, 792.0])
108
+ self[:CropBox] = dimensions
109
+ end
110
+
111
+ # accessor (getter) for the :CropBox element of the page
112
+ def cropbox
113
+ actual_object self[:CropBox]
114
+ end
115
+
116
+ # get page size
117
+ def page_size
118
+ cropbox || mediabox
119
+ end
120
+
121
+ # accessor (getter) for the :Resources element of the page
122
+ def resources
123
+ self[:Resources] ||= {}
124
+ self[:Resources][:referenced_object] || self[:Resources]
125
+ end
126
+
127
+ # This method adds a simple text box to the Page represented by the PDFWriter class.
128
+ # This function takes two values:
129
+ # text:: the text to potin the box.
130
+ # properties:: a Hash of box properties.
131
+ # the symbols and values in the properties Hash could be any or all of the following:
132
+ # x:: the left position of the box.
133
+ # y:: the BUTTOM position of the box.
134
+ # width:: the width/length of the box. negative values will be computed from edge of page. defaults to 0 (end of page).
135
+ # height:: the height of the box. negative values will be computed from edge of page. defaults to 0 (end of page).
136
+ # text_align:: symbol for horizontal text alignment, can be ":center" (default), ":right", ":left"
137
+ # text_valign:: symbol for vertical text alignment, can be ":center" (default), ":top", ":buttom"
138
+ # text_padding:: a Float between 0 and 1, setting the padding for the text. defaults to 0.05 (5%).
139
+ # font:: a registered font name or an Array of names. defaults to ":Helvetica". The 14 standard fonts names are:
140
+ # - :"Times-Roman"
141
+ # - :"Times-Bold"
142
+ # - :"Times-Italic"
143
+ # - :"Times-BoldItalic"
144
+ # - :Helvetica
145
+ # - :"Helvetica-Bold"
146
+ # - :"Helvetica-BoldOblique"
147
+ # - :"Helvetica- Oblique"
148
+ # - :Courier
149
+ # - :"Courier-Bold"
150
+ # - :"Courier-Oblique"
151
+ # - :"Courier-BoldOblique"
152
+ # - :Symbol
153
+ # - :ZapfDingbats
154
+ # font_size:: a Fixnum for the font size, or :fit_text to fit the text in the box. defaults to ":fit_text"
155
+ # max_font_size:: if font_size is set to :fit_text, this will be the maximum font size. defaults to nil (no maximum)
156
+ # 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.
157
+ # 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).
158
+ # stroke_width:: text stroke width in PDF units. defaults to 0 (none).
159
+ # 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).
160
+ # 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).
161
+ # border_width:: border width in PDF units. defaults to nil (none).
162
+ # box_radius:: border radius in PDF units. defaults to 0 (no corner rounding).
163
+ # opacity:: textbox opacity, a float between 0 (transparent) and 1 (opaque)
164
+ def textbox(text, properties = {})
165
+ options = {
166
+ x: 0,
167
+ y: 0,
168
+ width: 0,
169
+ height: -1,
170
+ text_align: :center,
171
+ text_valign: :center,
172
+ text_padding: 0.1,
173
+ font: nil,
174
+ font_size: :fit_text,
175
+ max_font_size: nil,
176
+ font_color: [0, 0, 0],
177
+ stroke_color: nil,
178
+ stroke_width: 0,
179
+ box_color: nil,
180
+ border_color: nil,
181
+ border_width: 0,
182
+ box_radius: 0,
183
+ opacity: 1,
184
+ ctm: nil # ~= [1,0,0,1,0,0]
185
+ }
186
+ options.update properties
187
+ # reset the length and height to meaningful values, if negative
188
+ options[:width] = mediabox[2] - options[:x] + options[:width] if options[:width] <= 0
189
+ options[:height] = mediabox[3] - options[:y] + options[:height] if options[:height] <= 0
190
+
191
+ # reset the padding value
192
+ options[:text_padding] = 0 if options[:text_padding].to_f >= 1
193
+
194
+ # create box stream
195
+ box_stream = ''
196
+ # set graphic state for box
197
+ if options[:box_color] || (options[:border_width].to_i > 0 && options[:border_color])
198
+ # compute x and y position for text
199
+ x = options[:x]
200
+ y = options[:y]
201
+
202
+ # set graphic state for the box
203
+ box_stream << "q\n"
204
+ box_stream << "#{options[:ctm].join ' '} cm\n" if options[:ctm]
205
+ box_graphic_state = { ca: options[:opacity], CA: options[:opacity], LW: options[:border_width], LC: 0, LJ: 0, LD: 0 }
206
+ if options[:box_radius] != 0 # if the text box has rounded corners
207
+ box_graphic_state[:LC] = 2
208
+ box_graphic_state[:LJ] = 1
209
+ end
210
+ box_graphic_state = graphic_state box_graphic_state # adds the graphic state to Resources and gets the reference
211
+ box_stream << "#{object_to_pdf box_graphic_state} gs\n"
212
+
213
+ # the following line was removed for Acrobat Reader compatability
214
+ # box_stream << "DeviceRGB CS\nDeviceRGB cs\n"
215
+
216
+ box_stream << "#{options[:box_color].join(' ')} rg\n" if options[:box_color]
217
+ if options[:border_width].to_i > 0 && options[:border_color]
218
+ box_stream << "#{options[:border_color].join(' ')} RG\n"
219
+ end
220
+ # create the path
221
+ radius = options[:box_radius]
222
+ half_radius = (radius.to_f / 2).round 4
223
+ ## set starting point
224
+ box_stream << "#{options[:x] + radius} #{options[:y]} m\n"
225
+ ## buttom and right corner - first line and first corner
226
+ box_stream << "#{options[:x] + options[:width] - radius} #{options[:y]} l\n" # buttom
227
+ if options[:box_radius] != 0 # make first corner, if not straight.
228
+ box_stream << "#{options[:x] + options[:width] - half_radius} #{options[:y]} "
229
+ box_stream << "#{options[:x] + options[:width]} #{options[:y] + half_radius} "
230
+ box_stream << "#{options[:x] + options[:width]} #{options[:y] + radius} c\n"
231
+ end
232
+ ## right and top-right corner
233
+ box_stream << "#{options[:x] + options[:width]} #{options[:y] + options[:height] - radius} l\n"
234
+ if options[:box_radius] != 0
235
+ box_stream << "#{options[:x] + options[:width]} #{options[:y] + options[:height] - half_radius} "
236
+ box_stream << "#{options[:x] + options[:width] - half_radius} #{options[:y] + options[:height]} "
237
+ box_stream << "#{options[:x] + options[:width] - radius} #{options[:y] + options[:height]} c\n"
238
+ end
239
+ ## top and top-left corner
240
+ box_stream << "#{options[:x] + radius} #{options[:y] + options[:height]} l\n"
241
+ if options[:box_radius] != 0
242
+ box_stream << "#{options[:x] + half_radius} #{options[:y] + options[:height]} "
243
+ box_stream << "#{options[:x]} #{options[:y] + options[:height] - half_radius} "
244
+ box_stream << "#{options[:x]} #{options[:y] + options[:height] - radius} c\n"
245
+ end
246
+ ## left and buttom-left corner
247
+ box_stream << "#{options[:x]} #{options[:y] + radius} l\n"
248
+ if options[:box_radius] != 0
249
+ box_stream << "#{options[:x]} #{options[:y] + half_radius} "
250
+ box_stream << "#{options[:x] + half_radius} #{options[:y]} "
251
+ box_stream << "#{options[:x] + radius} #{options[:y]} c\n"
252
+ end
253
+ # fill / stroke path
254
+ box_stream << "h\n"
255
+ if options[:box_color] && options[:border_width].to_i > 0 && options[:border_color]
256
+ box_stream << "B\n"
257
+ elsif options[:box_color] # fill if fill color is set
258
+ box_stream << "f\n"
259
+ elsif options[:border_width].to_i > 0 && options[:border_color] # stroke if border is set
260
+ box_stream << "S\n"
261
+ end
262
+
263
+ # exit graphic state for the box
264
+ box_stream << "Q\n"
265
+ end
266
+ contents << box_stream
267
+
268
+ # reset x,y by text alignment - x,y are calculated from the buttom left
269
+ # each unit (1) is 1/72 Inch
270
+ # create text stream
271
+ text_stream = ''
272
+ if !text.to_s.empty? && options[:font_size] != 0 && (options[:font_color] || options[:stroke_color])
273
+ # compute x and y position for text
274
+ x = options[:x] + (options[:width] * options[:text_padding])
275
+ y = options[:y] + (options[:height] * options[:text_padding])
276
+
277
+ # set the fonts (fonts array, with :Helvetica as fallback).
278
+ fonts = [*options[:font], :Helvetica]
279
+ # fit text in box, if requested
280
+ font_size = options[:font_size]
281
+ if options[:font_size] == :fit_text
282
+ font_size = fit_text text, fonts, (options[:width] * (1 - options[:text_padding])), (options[:height] * (1 - options[:text_padding]))
283
+ font_size = options[:max_font_size] if options[:max_font_size] && font_size > options[:max_font_size]
284
+ end
285
+
286
+ text_size = dimensions_of text, fonts, font_size
287
+
288
+ if options[:text_align] == :center
289
+ x = ((options[:width] * (1 - (2 * options[:text_padding]))) - text_size[0]) / 2 + x
290
+ elsif options[:text_align] == :right
291
+ x = ((options[:width] * (1 - (1.5 * options[:text_padding]))) - text_size[0]) + x
292
+ end
293
+ if options[:text_valign] == :center
294
+ y = ((options[:height] * (1 - (2 * options[:text_padding]))) - text_size[1]) / 2 + y
295
+ elsif options[:text_valign] == :top
296
+ y = (options[:height] * (1 - (1.5 * options[:text_padding]))) - text_size[1] + y
297
+ end
298
+
299
+ # set graphic state for text
300
+ text_stream << "q\n"
301
+ text_stream << "#{options[:ctm].join ' '} cm\n" if options[:ctm]
302
+ text_graphic_state = graphic_state(ca: options[:opacity], CA: options[:opacity], LW: options[:stroke_width].to_f, LC: 2, LJ: 1, LD: 0)
303
+ text_stream << "#{object_to_pdf text_graphic_state} gs\n"
304
+
305
+ # the following line was removed for Acrobat Reader compatability
306
+ # text_stream << "DeviceRGB CS\nDeviceRGB cs\n"
307
+
308
+ # set text render mode
309
+ if options[:font_color]
310
+ text_stream << "#{options[:font_color].join(' ')} rg\n"
311
+ end
312
+ if options[:stroke_width].to_i > 0 && options[:stroke_color]
313
+ text_stream << "#{options[:stroke_color].join(' ')} RG\n"
314
+ if options[:font_color]
315
+ text_stream << "2 Tr\n"
316
+ else
317
+ final_stream << "1 Tr\n"
318
+ end
319
+ elsif options[:font_color]
320
+ text_stream << "0 Tr\n"
321
+ else
322
+ text_stream << "3 Tr\n"
323
+ end
324
+ # format text object(s)
325
+ # text_stream << "#{options[:font_color].join(' ')} rg\n" # sets the color state
326
+ encode_text(text, fonts).each do |encoded|
327
+ text_stream << "BT\n" # the Begine Text marker
328
+ text_stream << format_name_to_pdf(set_font(encoded[0])) # Set font name
329
+ text_stream << " #{font_size.round 3} Tf\n" # set font size and add font operator
330
+ text_stream << "#{x.round 4} #{y.round 4} Td\n" # set location for text object
331
+ text_stream << (encoded[1]) # insert the encoded string to the stream
332
+ text_stream << " Tj\n" # the Text object operator and the End Text marker
333
+ text_stream << "ET\n" # the Text object operator and the End Text marker
334
+ x += encoded[2] / 1000 * font_size # update text starting point
335
+ y -= encoded[3] / 1000 * font_size # update text starting point
336
+ end
337
+ # exit graphic state for text
338
+ text_stream << "Q\n"
339
+ end
340
+ contents << text_stream
341
+
342
+ self
343
+ end
344
+
345
+ # gets the dimentions (width and height) of the text, as it will be printed in the PDF.
346
+ #
347
+ # text:: the text to measure
348
+ # 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.
349
+ # size:: the size of the font (defaults to 1000 points).
350
+ def dimensions_of(text, fonts, size = 1000)
351
+ Fonts.dimensions_of text, fonts, size
352
+ end
353
+
354
+ # this method returns the size for which the text fits the requested metrices
355
+ # the size is type Float and is rather exact
356
+ # if the text cannot fit such a small place, returns zero (0).
357
+ # maximum font size possible is set to 100,000 - which should be big enough for anything
358
+ # text:: the text to fit
359
+ # font:: the font name. @see font
360
+ # length:: the length to fit
361
+ # height:: the height to fit (optional - normally length is the issue)
362
+ def fit_text(text, font, length, height = 10_000_000)
363
+ size = 100_000
364
+ size_array = [size]
365
+ metrics = Fonts.dimensions_of text, font, size
366
+ size_array << size * length / metrics[0] if metrics[0] > length
367
+ size_array << size * height / metrics[1] if metrics[1] > height
368
+ size_array.min
369
+ end
370
+
371
+ # This method moves the Page[:Rotate] property into the page's data stream, so that
372
+ # "what you see is what you get".
373
+ #
374
+ # This is usful in cases where there might be less control over the source PDF files,
375
+ # and the user assums that the PDF page's data is the same as the PDF's pages
376
+ # on screen display (Rotate rotates a page but leaves the data in the original orientation).
377
+ #
378
+ # The method returns the page object, thus allowing method chaining (i.e. `page[:Rotate] = 90; page.textbox('hello!').fix_rotation.textbox('hello!')`)
379
+ def fix_rotation
380
+ return self if self[:Rotate].to_f == 0.0 || mediabox.nil?
381
+ # calculate the rotation
382
+ r = self[:Rotate].to_f * Math::PI / 180
383
+ s = Math.sin(r).round 6
384
+ c = Math.cos(r).round 6
385
+ ctm = [c, s, -s, c]
386
+ # calculate the translation (move the origin of x,y to the new origin).
387
+ x = mediabox[2] - mediabox[0]
388
+ y = mediabox[3] - mediabox[1]
389
+ ctm.push(((x * c).abs - x * c + (y * s).abs + y * s) / 2, ((x * s).abs - x * s + (y * c).abs - y * c) / 2)
390
+
391
+ # insert the rotation stream into the current content stream
392
+ insert_content "q\n#{ctm.join ' '} cm\n", 0
393
+ # close the rotation stream
394
+ insert_content CONTENT_CONTAINER_END
395
+ # reset the mediabox and cropbox values - THIS IS ONLY FOR ORIENTATION CHANGE...
396
+ if (self[:Rotate].to_f / 90).odd?
397
+ self[:MediaBox] = self[:MediaBox].values_at(1, 0, 3, 2)
398
+ self[:CropBox] = self[:CropBox].values_at(1, 0, 3, 2) if self[:CropBox]
399
+ end
400
+ # reset the Rotate property
401
+ delete :Rotate
402
+ # disconnect the content stream, so that future inserts aren't rotated
403
+ @contents = false # init_contents
404
+
405
+ # always return self, for chaining.
406
+ self
407
+ end
408
+
409
+ # resizes the page relative to it's current viewport (either the cropbox or the mediabox), setting the new viewport to the requested size.
410
+ #
411
+ # accepts:
412
+ # new_size:: an Array with four elements: [X0, Y0, X_max, Y_max]. For example, A4: `[0, 0, 595, 842]`. It is important that the first two numbers are 0 unless a special effect is attempted. If the first two numbers change, the final result might not be the size requested, but the nearest possible transformation (calling the method again will allow a better resizing).
413
+ # conserve_aspect_ratio:: whether to keep the current content in the same aspect ratio or to allow streaching. Defaults to true - so that although the content is resized, it might not fill the new size completely.
414
+ def resize(new_size = nil, conserve_aspect_ratio = true)
415
+ return page_size unless new_size
416
+ c_mediabox = mediabox
417
+ c_cropbox = cropbox
418
+ c_size = c_cropbox || c_mediabox
419
+ x_ratio = 1.0 * (new_size[2] - new_size[0]) / (c_size[2]) #-c_size[0])
420
+ y_ratio = 1.0 * (new_size[3] - new_size[1]) / (c_size[3]) #-c_size[1])
421
+ x_move = new_size[0] - c_size[0]
422
+ y_move = new_size[1] - c_size[1]
423
+ puts "ctm will be: #{x_ratio.round(4)} 0 0 #{y_ratio.round(4)} #{x_move} #{y_move}"
424
+ self[:MediaBox] = [(c_mediabox[0] + x_move), (c_mediabox[1] + y_move), ((c_mediabox[2] * x_ratio) + x_move), ((c_mediabox[3] * y_ratio) + y_move)]
425
+ self[:CropBox] = [(c_cropbox[0] + x_move), (c_cropbox[1] + y_move), ((c_cropbox[2] * x_ratio) + x_move), ((c_cropbox[3] * y_ratio) + y_move)] if c_cropbox
426
+ x_ratio = y_ratio = [x_ratio, y_ratio].min if conserve_aspect_ratio
427
+ # insert the rotation stream into the current content stream
428
+ # insert_content "q\n#{x_ratio.round(4).to_s} 0 0 #{y_ratio.round(4).to_s} 0 0 cm\n1 0 0 1 #{x_move} #{y_move} cm\n", 0
429
+ insert_content "q\n#{x_ratio.round(4)} 0 0 #{y_ratio.round(4)} #{x_move} #{y_move} cm\n", 0
430
+ # close the rotation stream
431
+ insert_content CONTENT_CONTAINER_END
432
+ # disconnect the content stream, so that future inserts aren't rotated
433
+ @contents = false # init_contents
434
+
435
+ # always return self, for chaining.
436
+ self
437
+ end
438
+
439
+ # crops the page using a <b>relative</b> size.
440
+ #
441
+ # `crop` will crop the page by updating it's MediaBox property using a <b>relative</b> crop box. i.e.,
442
+ # when cropping a page with {#page_size} of [10,10,900,900] to [5,5,500,500], the resulting page size should be [15, 15, 510, 510] - allowing you to ignore a page's initial XY starting point when cropping.
443
+ #
444
+ # for an absolute cropping, simpy use the {#mediabox=} or {#cropbox=} methods, setting their value to the new {page_size}.
445
+ #
446
+ # accepts:
447
+ # new_size:: an Array with four elements: [X0, Y0, X_max, Y_max]. For example, inch4(width)x6(length): `[200, 200, 488, 632]`
448
+ def crop(new_size = nil)
449
+ # no crop box? clear any cropping.
450
+ return page_size unless new_size
451
+ # type safety
452
+ raise TypeError, "pdf.page\#crop expeceted an Array (or nil)" unless Array === new_size
453
+
454
+ # set the MediaBox to the existing page size
455
+ self[:MediaBox] = page_size
456
+ # clear the CropBox
457
+ self[:CropBox] = nil
458
+ # update X0
459
+ self[:MediaBox][0] += new_size[0]
460
+ # update Y0
461
+ self[:MediaBox][1] += new_size[1]
462
+ # update X max IF the value is smaller then the existing value
463
+ self[:MediaBox][2] = (self[:MediaBox][0] + new_size[2] - new_size[0]) if (self[:MediaBox][0] + new_size[2] - new_size[0]) < self[:MediaBox][2]
464
+ # update Y max IF the value is smaller then the existing value
465
+ self[:MediaBox][3] = (self[:MediaBox][1] + new_size[3] - new_size[1]) if (self[:MediaBox][1] + new_size[3] - new_size[1]) < self[:MediaBox][3]
466
+ # return self for chaining
467
+ self
468
+ end
469
+
470
+ # rotate the page 90 degrees counter clockwise
471
+ def rotate_left
472
+ self[:Rotate] = self[:Rotate].to_f + 90
473
+ fix_rotation
474
+ end
475
+
476
+ # rotate the page 90 degrees clockwise
477
+ def rotate_right
478
+ self[:Rotate] = self[:Rotate].to_f - 90
479
+ fix_rotation
480
+ end
481
+
482
+ # rotate the page by 180 degrees
483
+ def rotate_180
484
+ self[:Rotate] = self[:Rotate].to_f +180
485
+ fix_rotation
486
+ end
487
+
488
+ # get or set (by clockwise rotation) the page's orientation
489
+ #
490
+ # accepts one optional parameter:
491
+ # force:: to get the orientation, pass nil. to set the orientatiom, set fource to either :portrait or :landscape. defaults to nil (get orientation).
492
+ # clockwise:: sets the rotation directions. defaults to true (clockwise rotation).
493
+ #
494
+ # returns the current orientation (:portrait or :landscape) if used to get the orientation.
495
+ # otherwise, if used to set the orientation, returns the page object to allow method chaining.
496
+ #
497
+ # * Notice: a square page always returns the :portrait value and is ignored when trying to set the orientation.
498
+ def orientation(force = nil, clockwise = true)
499
+ a = page_size
500
+ return (a[2] - a[0] > a[3] - a[1]) ? :landscape : :portrait unless force
501
+ unless orientation == force || (a[2] - a[0] == a[3] - a[1])
502
+ self[:Rotate] = 0
503
+ clockwise ? rotate_right : rotate_left
504
+ end
505
+ self
506
+ end
507
+
508
+ # Writes a table to the current page, removing(!) the written rows from the table_data Array.
509
+ #
510
+ # since the table_data Array is updated, it is possible to call this method a few times,
511
+ # each time creating or moving to the next page, until table_data.empty? returns true.
512
+ #
513
+ # accepts a Hash with any of the following keys as well as any of the PDFWriter#textbox options:
514
+ # headers:: an Array of strings with the headers (will be repeated every page).
515
+ # table_data:: as Array of Arrays, each containing a string for each column. the first row sets the number of columns. extra columns will be ignored.
516
+ # font:: a registered or standard font name (see PDFWriter). defaults to nil (:Helvetica).
517
+ # header_font:: a registered or standard font name for the headers (see PDFWriter). defaults to nil (the font for all the table rows).
518
+ # max_font_size:: the maximum font size. if the string doesn't fit, it will be resized. defaults to 14.
519
+ # column_widths:: an array of relative column widths ([1,2] will display only the first two columns, the second twice as big as the first). defaults to nil (even widths).
520
+ # header_color:: the header color. defaults to [0.8, 0.8, 0.8] (light gray).
521
+ # main_color:: main row color. defaults to nil (transparent / white).
522
+ # alternate_color:: alternate row color. defaults to [0.95, 0.95, 0.95] (very light gray).
523
+ # font_color:: font color. defaults to [0,0,0] (black).
524
+ # border_color:: border color. defaults to [0,0,0] (black).
525
+ # border_width:: border width in PDF units. defaults to 1.
526
+ # header_align:: the header text alignment within each column (:right, :left, :center). defaults to :center.
527
+ # row_align:: the row text alignment within each column. defaults to :left (:right for RTL table).
528
+ # direction:: the table's writing direction (:ltr or :rtl). this reffers to the direction of the columns and doesn't effect text (rtl text is automatically recognized). defaults to :ltr.
529
+ # max_rows:: the maximum number of rows to actually draw, INCLUDING the header row. deafults to 25.
530
+ # xy:: an Array specifying the top-left corner of the table. defaulte to [page_width*0.1, page_height*0.9].
531
+ # size:: an Array specifying the height and the width of the table. defaulte to [page_width*0.8, page_height*0.8].
532
+ def write_table(options = {})
533
+ defaults = {
534
+ headers: nil,
535
+ table_data: [[]],
536
+ font: nil,
537
+ header_font: nil,
538
+ max_font_size: 14,
539
+ column_widths: nil,
540
+ header_color: [0.8, 0.8, 0.8],
541
+ main_color: nil,
542
+ alternate_color: [0.95, 0.95, 0.95],
543
+ font_color: [0, 0, 0],
544
+ border_color: [0, 0, 0],
545
+ border_width: 1,
546
+ header_align: :center,
547
+ row_align: nil,
548
+ direction: :ltr,
549
+ max_rows: 25,
550
+ xy: nil,
551
+ size: nil
552
+ }
553
+ options = defaults.merge options
554
+ raise 'method call error! not enough rows allowed to create table' if (options[:max_rows].to_i < 1 && options[:headers]) || (options[:max_rows].to_i <= 0)
555
+ options[:header_font] ||= options[:font]
556
+ options[:row_align] ||= ((options[:direction] == :rtl) ? :right : :left)
557
+ options[:xy] ||= [((page_size[2] - page_size[0]) * 0.1), ((page_size[3] - page_size[1]) * 0.9)]
558
+ options[:size] ||= [((page_size[2] - page_size[0]) * 0.8), ((page_size[3] - page_size[1]) * 0.8)]
559
+ # assert table_data is an array of arrays
560
+ return false unless (options[:table_data].select { |r| !r.is_a?(Array) }).empty?
561
+ # compute sizes
562
+ top = options[:xy][1]
563
+ height = options[:size][1] / options[:max_rows]
564
+ from_side = options[:xy][0]
565
+ width = options[:size][0]
566
+ columns = options[:table_data][0].length
567
+ column_widths = []
568
+ columns.times { |_i| column_widths << (width / columns) }
569
+ if options[:column_widths]
570
+ scale = 0
571
+ options[:column_widths].each { |w| scale += w }
572
+ column_widths = []
573
+ options[:column_widths].each { |w| column_widths << (width * w / scale) }
574
+ end
575
+ column_widths = column_widths.reverse if options[:direction] == :rtl
576
+ # set count and start writing the data
577
+ row_number = 1
578
+
579
+ until options[:table_data].empty? || row_number > options[:max_rows]
580
+ # add headers
581
+ if options[:headers] && row_number == 1
582
+ x = from_side
583
+ headers = options[:headers]
584
+ headers = headers.reverse if options[:direction] == :rtl
585
+ column_widths.each_index do |i|
586
+ text = headers[i].to_s
587
+ textbox text, { x: x, y: (top - (height * row_number)), width: column_widths[i], height: height, box_color: options[:header_color], text_align: options[:header_align] }.merge(options).merge(font: options[:header_font])
588
+ x += column_widths[i]
589
+ end
590
+ row_number += 1
591
+ end
592
+ x = from_side
593
+ row_data = options[:table_data].shift
594
+ row_data = row_data.reverse if options[:direction] == :rtl
595
+ column_widths.each_index do |i|
596
+ text = row_data[i].to_s
597
+ box_color = (options[:alternate_color] && ((row_number.odd? && options[:headers]) || row_number.even?)) ? options[:alternate_color] : options[:main_color]
598
+ textbox text, { x: x, y: (top - (height * row_number)), width: column_widths[i], height: height, box_color: box_color, text_align: options[:row_align] }.merge(options)
599
+ x += column_widths[i]
600
+ end
601
+ row_number += 1
602
+ end
603
+ self
604
+ end
605
+
606
+ # creates a copy of the page. if the :secure flag is set to true, the resource indentifiers (fonts etc') will be renamed in order to secure their uniqueness.
607
+ def copy(secure = false)
608
+ # since only the Content streams are modified (Resource hashes are created anew),
609
+ # it should be safe (and a lot faster) to create a deep copy only for the content hashes and streams.
610
+ delete :Parent
611
+ prep_content_array
612
+ page_copy = clone
613
+ page_copy[:Contents] = page_copy[:Contents].map do |obj|
614
+ obj = obj.dup
615
+ obj[:referenced_object] = obj[:referenced_object].dup if obj[:referenced_object]
616
+ obj[:referenced_object][:raw_stream_content] = obj[:referenced_object][:raw_stream_content].dup if obj[:referenced_object] && obj[:referenced_object][:raw_stream_content]
617
+ obj
618
+ end
619
+ if page_copy[:Resources]
620
+ page_res = page_copy[:Resources] = page_copy[:Resources].dup
621
+ page_res = page_copy[:Resources][:referenced_object] = page_copy[:Resources][:referenced_object].dup if page_copy[:Resources][:referenced_object]
622
+ page_res.each do |k, v|
623
+ v = page_res[k] = v.dup if v.is_a?(Array) || v.is_a?(Hash)
624
+ v = v[:referenced_object] = v[:referenced_object].dup if v.is_a?(Hash) && v[:referenced_object]
625
+ v = v[:referenced_object] = v[:referenced_object].dup if v.is_a?(Hash) && v[:referenced_object]
626
+ end
627
+ end
628
+ page_copy.instance_exec(secure || @secure_injection) { |s| secure_for_copy if s; init_contents; self }
629
+ end
630
+
631
+ ###################################
632
+ # protected methods
633
+
634
+ protected
635
+
636
+ # accessor (getter) for the stream in the :Contents element of the page
637
+ # after getting the string object, you can operate on it but not replace it (use << or other String methods).
638
+ def contents
639
+ @contents ||= init_contents
640
+ end
641
+
642
+ # initializes the content stream in case it was not initialized before
643
+ def init_contents
644
+ self[:Contents] = self[:Contents][:referenced_object][:indirect_without_dictionary] if self[:Contents].is_a?(Hash) && self[:Contents][:referenced_object] && self[:Contents][:referenced_object].is_a?(Hash) && self[:Contents][:referenced_object][:indirect_without_dictionary]
645
+ self[:Contents] = [self[:Contents]] unless self[:Contents].is_a?(Array)
646
+ self[:Contents].delete(is_reference_only: true, referenced_object: { indirect_reference_id: 0, raw_stream_content: '' })
647
+ # un-nest any referenced arrays
648
+ self[:Contents].map! { |s| actual_value(s).is_a?(Array) ? actual_value(s) : s }
649
+ self[:Contents].flatten!
650
+ self[:Contents].compact!
651
+ # wrap content streams
652
+ insert_content 'q', 0
653
+ insert_content 'Q'
654
+
655
+ # Prep content
656
+ @contents = ''
657
+ insert_content @contents
658
+ @contents
659
+ end
660
+
661
+ # adds a string or an object to the content stream, at the location indicated
662
+ #
663
+ # accepts:
664
+ # object:: can be a string or a hash object
665
+ # location:: can be any numeral related to the possition in the :Contents array. defaults to -1 == insert at the end.
666
+ def insert_content(object, location = -1)
667
+ object = { is_reference_only: true, referenced_object: { indirect_reference_id: 0, raw_stream_content: object } } if object.is_a?(String)
668
+ raise TypeError, 'expected a String or Hash object.' unless object.is_a?(Hash)
669
+ prep_content_array
670
+ self[:Contents].insert location, object
671
+ self[:Contents].flatten!
672
+ self
673
+ end
674
+
675
+ def prep_content_array
676
+ return self if self[:Contents].is_a?(Array)
677
+ init_contents
678
+ # self[:Contents] = self[:Contents][:referenced_object] if self[:Contents].is_a?(Hash) && self[:Contents][:referenced_object] && self[:Contents][:referenced_object].is_a?(Array)
679
+ # self[:Contents] = self[:Contents][:indirect_without_dictionary] if self[:Contents].is_a?(Hash) && self[:Contents][:indirect_without_dictionary] && self[:Contents][:indirect_without_dictionary].is_a?(Array)
680
+ # self[:Contents] = [self[:Contents]] unless self[:Contents].is_a?(Array)
681
+ # self[:Contents].compact!
682
+ self
683
+ end
684
+
685
+ # returns the basic font name used internally
686
+ def base_font_name
687
+ @base_font_name ||= 'Writer' + SecureRandom.hex(7) + 'PDF'
688
+ end
689
+
690
+ # creates a font object and adds the font to the resources dictionary
691
+ # returns the name of the font for the content stream.
692
+ # font:: a Symbol of one of the fonts registered in the library, or:
693
+ # - :"Times-Roman"
694
+ # - :"Times-Bold"
695
+ # - :"Times-Italic"
696
+ # - :"Times-BoldItalic"
697
+ # - :Helvetica
698
+ # - :"Helvetica-Bold"
699
+ # - :"Helvetica-BoldOblique"
700
+ # - :"Helvetica- Oblique"
701
+ # - :Courier
702
+ # - :"Courier-Bold"
703
+ # - :"Courier-Oblique"
704
+ # - :"Courier-BoldOblique"
705
+ # - :Symbol
706
+ # - :ZapfDingbats
707
+ def set_font(font = :Helvetica)
708
+ # if the font exists, return it's name
709
+ resources[:Font] ||= {}
710
+ fonts_res = resources[:Font][:referenced_object] || resources[:Font]
711
+ fonts_res.each do |k, v|
712
+ return k if v.is_a?(Fonts::Font) && v.name && v.name == font
713
+ end
714
+ # set a secure name for the font
715
+ name = (base_font_name + (fonts_res.length + 1).to_s).to_sym
716
+ # get font object
717
+ font_object = Fonts.get_font(font)
718
+ # return false if the font wan't found in the library.
719
+ return false unless font_object
720
+ # add object to reasource
721
+ fonts_res[name] = font_object
722
+ # return name
723
+ name
724
+ end
725
+
726
+ # register or get a registered graphic state dictionary.
727
+ # the method returns the name of the graphos state, for use in a content stream.
728
+ def graphic_state(graphic_state_dictionary = {})
729
+ # if the graphic state exists, return it's name
730
+ resources[:ExtGState] ||= {}
731
+ gs_res = resources[:ExtGState][:referenced_object] || resources[:ExtGState]
732
+ gs_res.each do |k, v|
733
+ return k if v.is_a?(Hash) && v == graphic_state_dictionary
734
+ end
735
+ # set graphic state type
736
+ graphic_state_dictionary[:Type] = :ExtGState
737
+ # set a secure name for the graphic state
738
+ name = SecureRandom.hex(9).to_sym
739
+ # add object to reasource
740
+ gs_res[name] = graphic_state_dictionary
741
+ # return name
742
+ name
743
+ end
744
+
745
+ # encodes the text in an array of [:font_name, <PDFHexString>] for use in textbox
746
+ def encode_text(text, fonts)
747
+ # text must be a unicode string and fonts must be an array.
748
+ # this is an internal method, don't perform tests.
749
+ fonts_array = []
750
+ fonts.each do |name|
751
+ f = Fonts.get_font name
752
+ fonts_array << f if f
753
+ end
754
+
755
+ # before starting, we should reorder any RTL content in the string
756
+ text = reorder_rtl_content text
757
+
758
+ out = []
759
+ text.chars.each do |c|
760
+ fonts_array.each_index do |i|
761
+ next unless fonts_array[i].cmap.nil? || (fonts_array[i].cmap && fonts_array[i].cmap[c])
762
+ # add to array
763
+ if out.last.nil? || out.last[0] != fonts[i]
764
+ out.last[1] << '>' unless out.last.nil?
765
+ out << [fonts[i], '<', 0, 0]
766
+ end
767
+ out.last[1] << (fonts_array[i].cmap.nil? ? (c.unpack('H*')[0]) : fonts_array[i].cmap[c])
768
+ if fonts_array[i].metrics[c]
769
+ out.last[2] += fonts_array[i].metrics[c][:wx].to_f
770
+ out.last[3] += fonts_array[i].metrics[c][:wy].to_f
771
+ end
772
+ break
773
+ end
774
+ end
775
+ out.last[1] << '>' if out.last
776
+ out
777
+ end
778
+
779
+ # a very primitive text reordering algorithm... I was lazy...
780
+ # ...still, it works (I think).
781
+ def reorder_rtl_content(text)
782
+ rtl_characters = "\u05d0-\u05ea\u05f0-\u05f4\u0600-\u06ff\u0750-\u077f"
783
+ rtl_replaces = { '(' => ')', ')' => '(',
784
+ '[' => ']', ']' => '[',
785
+ '{' => '}', '}' => '{',
786
+ '<' => '>', '>' => '<' }
787
+ return text unless text =~ /[#{rtl_characters}]/
788
+
789
+ out = []
790
+ scanner = StringScanner.new text
791
+ until scanner.eos?
792
+ if scanner.scan /[#{rtl_characters} ]/
793
+ out.unshift scanner.matched
794
+ elsif scanner.scan /[^#{rtl_characters}]+/
795
+ if out.empty? && scanner.matched.match(/[\s]$/) && !scanner.eos?
796
+ white_space_to_move = scanner.matched.match(/[\s]+$/).to_s
797
+ out.unshift scanner.matched[0..-1 - white_space_to_move.length]
798
+ out.unshift white_space_to_move
799
+ elsif scanner.matched =~ /^[\(\)\[\]\{\}\<\>]$/
800
+ out.unshift rtl_replaces[scanner.matched]
801
+ else
802
+ out.unshift scanner.matched
803
+ end
804
+ end
805
+ end
806
+ out.join.strip
807
+ end
808
+
809
+ # copy_and_secure_for_injection(page)
810
+ # - page is a page in the pages array, i.e.
811
+ # pdf.pages[0]
812
+ # takes a page object and:
813
+ #
814
+ # makes a deep copy of the page (Ruby defaults to pointers, so this will copy the memory).
815
+ #
816
+ # then it will rewrite the content stream with renamed resources, so as to avoid name conflicts.
817
+ def secure_for_copy
818
+ # initiate dictionary from old names to new names
819
+ names_dictionary = {}
820
+
821
+ # travel every dictionary to pick up names (keys), change them and add them to the dictionary
822
+ res = resources
823
+ res.each do |k, v|
824
+ next unless actual_value(v).is_a?(Hash)
825
+ # if k == :XObject
826
+ # self[:Resources][k] = v.dup
827
+ # next
828
+ # end
829
+ new_dictionary = {}
830
+ new_name = 'Combine' + SecureRandom.hex(7) + 'PDF'
831
+ i = 1
832
+ actual_value(v).each do |old_key, value|
833
+ new_key = (new_name + i.to_s).to_sym
834
+ names_dictionary[old_key] = new_key
835
+ new_dictionary[new_key] = value
836
+ i += 1
837
+ end
838
+ res[k] = new_dictionary
839
+ end
840
+
841
+ # now that we have replaced the names in the resources dictionaries,
842
+ # it is time to replace the names inside the stream
843
+ # we will need to make sure we have access to the stream injected
844
+ # we will user PDFFilter.inflate_object
845
+ self[:Contents].each do |c|
846
+ stream = actual_value(c)
847
+ PDFFilter.inflate_object stream
848
+ names_dictionary.each do |old_key, new_key|
849
+ stream[:raw_stream_content].gsub! object_to_pdf(old_key), object_to_pdf(new_key) ##### PRAY(!) that the parsed datawill be correctly reproduced!
850
+ end
851
+ # # # the following code isn't needed now that we wrap both the existing and incoming content streams.
852
+ # # patch back to PDF defaults, for OCRed PDF files.
853
+ # stream[:raw_stream_content] = "q\n0 0 0 rg\n0 0 0 RG\n0 Tr\n1 0 0 1 0 0 cm\n%s\nQ\n" % stream[:raw_stream_content]
854
+ end
855
+ self
856
+ end
857
+
858
+ # @return [true, false] returns true if there are two different resources sharing the same named reference.
859
+ def should_secure?(page)
860
+ # travel every dictionary to pick up names (keys), change them and add them to the dictionary
861
+ res = actual_value(resources)
862
+ foreign_res = actual_value(page.resources)
863
+ tmp = nil
864
+ res.each do |k, v|
865
+ next unless (v = actual_value(v)).is_a?(Hash) && (tmp = actual_value(foreign_res[k])).is_a?(Hash)
866
+ v.keys.each do |name|
867
+ return true if tmp[name] && tmp[name] != v[name]
868
+ end # else # Do nothing, this is taken care of elseware
869
+ end
870
+ false
871
+ end
872
+ end
886
873
  end