combine_pdf 0.1.13 → 0.1.15

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