combine_pdf 0.1.13 → 0.1.15

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