combine_pdf 0.2.5 → 0.2.37

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