combine_pdf 0.2.5 → 0.2.37

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