pdf-reader 1.4.1 → 2.5.0

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.
Files changed (79) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG +53 -3
  3. data/{README.rdoc → README.md} +40 -23
  4. data/Rakefile +2 -2
  5. data/bin/pdf_callbacks +1 -1
  6. data/bin/pdf_object +4 -1
  7. data/bin/pdf_text +1 -1
  8. data/lib/pdf/reader/afm/Courier-Bold.afm +342 -342
  9. data/lib/pdf/reader/afm/Courier-BoldOblique.afm +342 -342
  10. data/lib/pdf/reader/afm/Courier-Oblique.afm +342 -342
  11. data/lib/pdf/reader/afm/Courier.afm +342 -342
  12. data/lib/pdf/reader/afm/Helvetica-Bold.afm +2827 -2827
  13. data/lib/pdf/reader/afm/Helvetica-BoldOblique.afm +2827 -2827
  14. data/lib/pdf/reader/afm/Helvetica-Oblique.afm +3051 -3051
  15. data/lib/pdf/reader/afm/Helvetica.afm +3051 -3051
  16. data/lib/pdf/reader/afm/MustRead.html +19 -0
  17. data/lib/pdf/reader/afm/Symbol.afm +213 -213
  18. data/lib/pdf/reader/afm/Times-Bold.afm +2588 -2588
  19. data/lib/pdf/reader/afm/Times-BoldItalic.afm +2384 -2384
  20. data/lib/pdf/reader/afm/Times-Italic.afm +2667 -2667
  21. data/lib/pdf/reader/afm/Times-Roman.afm +2419 -2419
  22. data/lib/pdf/reader/afm/ZapfDingbats.afm +225 -225
  23. data/lib/pdf/reader/buffer.rb +14 -12
  24. data/lib/pdf/reader/cid_widths.rb +2 -0
  25. data/lib/pdf/reader/cmap.rb +48 -36
  26. data/lib/pdf/reader/encoding.rb +16 -18
  27. data/lib/pdf/reader/error.rb +5 -0
  28. data/lib/pdf/reader/filter/ascii85.rb +1 -0
  29. data/lib/pdf/reader/filter/ascii_hex.rb +2 -0
  30. data/lib/pdf/reader/filter/depredict.rb +1 -0
  31. data/lib/pdf/reader/filter/flate.rb +29 -16
  32. data/lib/pdf/reader/filter/lzw.rb +2 -0
  33. data/lib/pdf/reader/filter/null.rb +2 -0
  34. data/lib/pdf/reader/filter/run_length.rb +4 -6
  35. data/lib/pdf/reader/filter.rb +2 -0
  36. data/lib/pdf/reader/font.rb +12 -13
  37. data/lib/pdf/reader/font_descriptor.rb +1 -0
  38. data/lib/pdf/reader/form_xobject.rb +1 -0
  39. data/lib/pdf/reader/glyph_hash.rb +7 -2
  40. data/lib/pdf/reader/lzw.rb +4 -4
  41. data/lib/pdf/reader/null_security_handler.rb +17 -0
  42. data/lib/pdf/reader/object_cache.rb +1 -0
  43. data/lib/pdf/reader/object_hash.rb +91 -37
  44. data/lib/pdf/reader/object_stream.rb +1 -0
  45. data/lib/pdf/reader/orientation_detector.rb +5 -4
  46. data/lib/pdf/reader/overlapping_runs_filter.rb +65 -0
  47. data/lib/pdf/reader/page.rb +30 -1
  48. data/lib/pdf/reader/page_layout.rb +19 -24
  49. data/lib/pdf/reader/page_state.rb +8 -5
  50. data/lib/pdf/reader/page_text_receiver.rb +23 -1
  51. data/lib/pdf/reader/pages_strategy.rb +2 -304
  52. data/lib/pdf/reader/parser.rb +10 -7
  53. data/lib/pdf/reader/print_receiver.rb +1 -0
  54. data/lib/pdf/reader/reference.rb +1 -0
  55. data/lib/pdf/reader/register_receiver.rb +1 -0
  56. data/lib/pdf/reader/resource_methods.rb +1 -0
  57. data/lib/pdf/reader/standard_security_handler.rb +80 -42
  58. data/lib/pdf/reader/standard_security_handler_v5.rb +91 -0
  59. data/lib/pdf/reader/stream.rb +1 -0
  60. data/lib/pdf/reader/synchronized_cache.rb +1 -0
  61. data/lib/pdf/reader/text_run.rb +28 -9
  62. data/lib/pdf/reader/token.rb +1 -0
  63. data/lib/pdf/reader/transformation_matrix.rb +1 -0
  64. data/lib/pdf/reader/unimplemented_security_handler.rb +17 -0
  65. data/lib/pdf/reader/width_calculator/built_in.rb +25 -16
  66. data/lib/pdf/reader/width_calculator/composite.rb +1 -0
  67. data/lib/pdf/reader/width_calculator/true_type.rb +2 -2
  68. data/lib/pdf/reader/width_calculator/type_one_or_three.rb +1 -0
  69. data/lib/pdf/reader/width_calculator/type_zero.rb +1 -0
  70. data/lib/pdf/reader/width_calculator.rb +1 -0
  71. data/lib/pdf/reader/xref.rb +11 -5
  72. data/lib/pdf/reader.rb +30 -119
  73. data/lib/pdf-reader.rb +1 -0
  74. metadata +35 -61
  75. data/bin/pdf_list_callbacks +0 -17
  76. data/lib/pdf/hash.rb +0 -19
  77. data/lib/pdf/reader/abstract_strategy.rb +0 -81
  78. data/lib/pdf/reader/metadata_strategy.rb +0 -56
  79. data/lib/pdf/reader/text_receiver.rb +0 -265
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  class PDF::Reader
4
5
  # Provides low level access to the objects in a PDF file via a hash-like
@@ -45,6 +46,7 @@ class PDF::Reader
45
46
  @pdf_version = read_version
46
47
  @trailer = @xref.trailer
47
48
  @cache = opts[:cache] || PDF::Reader::ObjectCache.new
49
+ @sec_handler = NullSecurityHandler.new
48
50
  @sec_handler = build_security_handler(opts)
49
51
  end
50
52
 
@@ -76,16 +78,7 @@ class PDF::Reader
76
78
  key = PDF::Reader::Reference.new(key.to_i, 0)
77
79
  end
78
80
 
79
- if @cache.has_key?(key)
80
- @cache[key]
81
- elsif xref[key].is_a?(Integer)
82
- buf = new_buffer(xref[key])
83
- @cache[key] = decrypt(key, Parser.new(buf, self).object(key.id, key.gen))
84
- elsif xref[key].is_a?(PDF::Reader::Reference)
85
- container_key = xref[key]
86
- object_streams[container_key] ||= PDF::Reader::ObjectStream.new(object(container_key))
87
- @cache[key] = object_streams[container_key][key.id]
88
- end
81
+ @cache[key] ||= fetch_object(key) || fetch_object_stream(key)
89
82
  rescue InvalidObjectError
90
83
  return default
91
84
  end
@@ -102,21 +95,7 @@ class PDF::Reader
102
95
  # a PDF::Reader::Reference, the key is returned unchanged.
103
96
  #
104
97
  def deref!(key)
105
- case object = deref(key)
106
- when Hash
107
- {}.tap { |hash|
108
- object.each do |k, value|
109
- hash[k] = deref!(value)
110
- end
111
- }
112
- when PDF::Reader::Stream
113
- object.hash = deref!(object.hash)
114
- object
115
- when Array
116
- object.map { |value| deref!(value) }
117
- else
118
- object
119
- end
98
+ deref_internal!(key, {})
120
99
  end
121
100
 
122
101
  # Access an object from the PDF. key can be an int or a PDF::Reader::Reference
@@ -266,24 +245,95 @@ class PDF::Reader
266
245
 
267
246
  private
268
247
 
269
- def build_security_handler(opts = {})
270
- return nil if trailer[:Encrypt].nil?
248
+ # parse a traditional object from the PDF, starting from the byte offset indicated
249
+ # in the xref table
250
+ #
251
+ def fetch_object(key)
252
+ if xref[key].is_a?(Integer)
253
+ buf = new_buffer(xref[key])
254
+ decrypt(key, Parser.new(buf, self).object(key.id, key.gen))
255
+ end
256
+ end
257
+
258
+ # parse a object that's embedded in an object stream in the PDF
259
+ #
260
+ def fetch_object_stream(key)
261
+ if xref[key].is_a?(PDF::Reader::Reference)
262
+ container_key = xref[key]
263
+ object_streams[container_key] ||= PDF::Reader::ObjectStream.new(object(container_key))
264
+ object_streams[container_key][key.id]
265
+ end
266
+ end
267
+
268
+ # Private implementation of deref!, which exists to ensure the `seen` argument
269
+ # isn't publicly available. It's used to avoid endless loops in the recursion, and
270
+ # doesn't need to be part of the public API.
271
+ #
272
+ def deref_internal!(key, seen)
273
+ seen_key = key.is_a?(PDF::Reader::Reference) ? key : key.object_id
274
+
275
+ return seen[seen_key] if seen.key?(seen_key)
271
276
 
272
- enc = deref(trailer[:Encrypt])
273
- case enc[:Filter]
274
- when :Standard
275
- StandardSecurityHandler.new(enc, deref(trailer[:ID]), opts[:password])
277
+ case object = deref(key)
278
+ when Hash
279
+ seen[seen_key] ||= {}
280
+ object.each do |k, value|
281
+ seen[seen_key][k] = deref_internal!(value, seen)
282
+ end
283
+ seen[seen_key]
284
+ when PDF::Reader::Stream
285
+ seen[seen_key] ||= PDF::Reader::Stream.new({}, object.data)
286
+ object.hash.each do |k,value|
287
+ seen[seen_key].hash[k] = deref_internal!(value, seen)
288
+ end
289
+ seen[seen_key]
290
+ when Array
291
+ seen[seen_key] ||= []
292
+ object.each do |value|
293
+ seen[seen_key] << deref_internal!(value, seen)
294
+ end
295
+ seen[seen_key]
276
296
  else
277
- raise PDF::Reader::EncryptedPDFError, "Unsupported encryption method (#{enc[:Filter]})"
297
+ object
278
298
  end
279
299
  end
280
300
 
281
- def decrypt(ref, obj)
282
- return obj unless sec_handler?
301
+ def build_security_handler(opts = {})
302
+ encrypt = deref(trailer[:Encrypt])
303
+ if NullSecurityHandler.supports?(encrypt)
304
+ NullSecurityHandler.new
305
+ elsif StandardSecurityHandler.supports?(encrypt)
306
+ encmeta = !encrypt.has_key?(:EncryptMetadata) || encrypt[:EncryptMetadata].to_s == "true"
307
+ StandardSecurityHandler.new(
308
+ key_length: (encrypt[:Length] || 40).to_i,
309
+ revision: encrypt[:R],
310
+ owner_key: encrypt[:O],
311
+ user_key: encrypt[:U],
312
+ permissions: encrypt[:P].to_i,
313
+ encrypted_metadata: encmeta,
314
+ file_id: (deref(trailer[:ID]) || []).first,
315
+ password: opts[:password],
316
+ cfm: encrypt.fetch(:CF, {}).fetch(encrypt[:StmF], {}).fetch(:CFM, nil)
317
+ )
318
+ elsif StandardSecurityHandlerV5.supports?(encrypt)
319
+ StandardSecurityHandlerV5.new(
320
+ O: encrypt[:O],
321
+ U: encrypt[:U],
322
+ OE: encrypt[:OE],
323
+ UE: encrypt[:UE],
324
+ password: opts[:password]
325
+ )
326
+ else
327
+ UnimplementedSecurityHandler.new
328
+ end
329
+ end
283
330
 
331
+ def decrypt(ref, obj)
284
332
  case obj
285
333
  when PDF::Reader::Stream then
286
- obj.data = sec_handler.decrypt(obj.data, ref)
334
+ # PDF 32000-1:2008 7.5.8.2: "The cross-reference stream shall not be encrypted [...]."
335
+ # Therefore we shouldn't try to decrypt it.
336
+ obj.data = sec_handler.decrypt(obj.data, ref) unless obj.hash[:Type] == :XRef
287
337
  obj
288
338
  when Hash then
289
339
  arr = obj.map { |key,val| [key, decrypt(ref, val)] }.flatten(1)
@@ -312,11 +362,15 @@ class PDF::Reader
312
362
  # returns a nested array of object references for all pages in this object store.
313
363
  #
314
364
  def get_page_objects(ref)
315
- obj = fetch(ref)
365
+ obj = deref(ref)
366
+
367
+ unless obj.kind_of?(::Hash)
368
+ raise MalformedPDFError, "Dereferenced page object must be a dict"
369
+ end
316
370
 
317
371
  if obj[:Type] == :Page
318
372
  ref
319
- elsif obj[:Type] == :Pages
373
+ elsif obj[:Kids]
320
374
  deref(obj[:Kids]).map { |kid| get_page_objects(kid) }
321
375
  end
322
376
  end
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  class PDF::Reader
4
5
 
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  class PDF::Reader
4
5
  # Small util class for detecting the orientation of a single PDF page. Accounts
@@ -21,12 +22,12 @@ class PDF::Reader
21
22
  def detect_orientation
22
23
  llx,lly,urx,ury = @attributes[:MediaBox]
23
24
  rotation = @attributes[:Rotate].to_i
24
- width = urx.to_i - llx.to_i
25
- height = ury.to_i - lly.to_i
25
+ width = (urx.to_i - llx.to_i).abs
26
+ height = (ury.to_i - lly.to_i).abs
26
27
  if width > height
27
- [0,180].include?(rotation) ? 'landscape' : 'portrait'
28
+ (rotation % 180).zero? ? 'landscape' : 'portrait'
28
29
  else
29
- [0,180].include?(rotation) ? 'portrait' : 'landscape'
30
+ (rotation % 180).zero? ? 'portrait' : 'landscape'
30
31
  end
31
32
  end
32
33
  end
@@ -0,0 +1,65 @@
1
+ # coding: utf-8
2
+
3
+ class PDF::Reader
4
+ # remove duplicates from a collection of TextRun objects. This can be helpful when a PDF
5
+ # uses slightly offset overlapping characters to achieve a fake 'bold' effect.
6
+ class OverlappingRunsFilter
7
+
8
+ # This should be between 0 and 1. If TextRun B obscures this much of TextRun A (and they
9
+ # have identical characters) then one will be discarded
10
+ OVERLAPPING_THRESHOLD = 0.5
11
+
12
+ def self.exclude_redundant_runs(runs)
13
+ sweep_line_status = Array.new
14
+ event_point_schedule = Array.new
15
+ to_exclude = []
16
+
17
+ runs.each do |run|
18
+ event_point_schedule << EventPoint.new(run.x, run)
19
+ event_point_schedule << EventPoint.new(run.endx, run)
20
+ end
21
+
22
+ event_point_schedule.sort! { |a,b| a.x <=> b.x }
23
+
24
+ event_point_schedule.each do |event_point|
25
+ run = event_point.run
26
+
27
+ if event_point.start?
28
+ if detect_intersection(sweep_line_status, event_point)
29
+ to_exclude << run
30
+ end
31
+ sweep_line_status.push(run)
32
+ else
33
+ sweep_line_status.delete(run)
34
+ end
35
+ end
36
+ runs - to_exclude
37
+ end
38
+
39
+ def self.detect_intersection(sweep_line_status, event_point)
40
+ sweep_line_status.each do |open_text_run|
41
+ if event_point.x >= open_text_run.x &&
42
+ event_point.x <= open_text_run.endx &&
43
+ open_text_run.intersection_area_percent(event_point.run) >= OVERLAPPING_THRESHOLD
44
+ return true
45
+ end
46
+ end
47
+ return false
48
+ end
49
+ end
50
+
51
+ # Utility class used to avoid modifying the underlying TextRun objects while we're
52
+ # looking for duplicates
53
+ class EventPoint
54
+ attr_reader :x, :run
55
+
56
+ def initialize x, run
57
+ @x, @run = x, run
58
+ end
59
+
60
+ def start?
61
+ @x == @run.x
62
+ end
63
+ end
64
+
65
+ end
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  module PDF
4
5
  class Reader
@@ -36,7 +37,7 @@ module PDF
36
37
  @cache = options[:cache] || {}
37
38
 
38
39
  unless @page_object.is_a?(::Hash)
39
- raise ArgumentError, "invalid page: #{pagenum}"
40
+ raise InvalidPageError, "Invalid page: #{pagenum}"
40
41
  end
41
42
  end
42
43
 
@@ -123,6 +124,34 @@ module PDF
123
124
  }.join(" ")
124
125
  end
125
126
 
127
+ # returns the angle to rotate the page clockwise. Always 0, 90, 180 or 270
128
+ #
129
+ def rotate
130
+ value = attributes[:Rotate].to_i
131
+ case value
132
+ when 0, 90, 180, 270
133
+ value
134
+ else
135
+ 0
136
+ end
137
+ end
138
+
139
+ # returns the "boxes" that define the page object.
140
+ # values are defaulted according to section 7.7.3.3 of the PDF Spec 1.7
141
+ #
142
+ def boxes
143
+ mediabox = attributes[:MediaBox]
144
+ cropbox = attributes[:Cropbox] || mediabox
145
+
146
+ {
147
+ MediaBox: objects.deref!(mediabox),
148
+ CropBox: objects.deref!(cropbox),
149
+ BleedBox: objects.deref!(attributes[:BleedBox] || cropbox),
150
+ TrimBox: objects.deref!(attributes[:TrimBox] || cropbox),
151
+ ArtBox: objects.deref!(attributes[:ArtBox] || cropbox)
152
+ }
153
+ end
154
+
126
155
  private
127
156
 
128
157
  def root
@@ -1,4 +1,7 @@
1
1
  # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'pdf/reader/overlapping_runs_filter'
2
5
 
3
6
  class PDF::Reader
4
7
 
@@ -8,28 +11,33 @@ class PDF::Reader
8
11
  # media box should be a 4 number array that describes the dimensions of the
9
12
  # page to be rendered as described by the page's MediaBox attribute
10
13
  class PageLayout
14
+
15
+ DEFAULT_FONT_SIZE = 12
16
+
11
17
  def initialize(runs, mediabox)
12
18
  raise ArgumentError, "a mediabox must be provided" if mediabox.nil?
13
19
 
14
- @runs = merge_runs(runs)
15
- @mean_font_size = mean(@runs.map(&:font_size)) || 0
20
+ @runs = merge_runs(OverlappingRunsFilter.exclude_redundant_runs(runs))
21
+ @mean_font_size = mean(@runs.map(&:font_size)) || DEFAULT_FONT_SIZE
22
+ @mean_font_size = DEFAULT_FONT_SIZE if @mean_font_size == 0
16
23
  @mean_glyph_width = mean(@runs.map(&:mean_character_width)) || 0
17
- @page_width = mediabox[2] - mediabox[0]
18
- @page_height = mediabox[3] - mediabox[1]
19
- @x_offset = @runs.map(&:x).sort.first
20
- @current_platform_is_rbx_19 = RUBY_DESCRIPTION =~ /\Arubinius 2.0.0/ &&
21
- RUBY_VERSION >= "1.9.0"
24
+ @page_width = (mediabox[2] - mediabox[0]).abs
25
+ @page_height = (mediabox[3] - mediabox[1]).abs
26
+ @x_offset = @runs.map(&:x).sort.first || 0
27
+ lowest_y = @runs.map(&:y).sort.first || 0
28
+ @y_offset = lowest_y > 0 ? 0 : lowest_y
22
29
  end
23
30
 
24
31
  def to_s
25
32
  return "" if @runs.empty?
33
+ return "" if row_count == 0
26
34
 
27
35
  page = row_count.times.map { |i| " " * col_count }
28
36
  @runs.each do |run|
29
37
  x_pos = ((run.x - @x_offset) / col_multiplier).round
30
- y_pos = row_count - (run.y / row_multiplier).round
31
- if y_pos < row_count && y_pos >= 0 && x_pos < col_count && x_pos >= 0
32
- local_string_insert(page[y_pos], run.text, x_pos)
38
+ y_pos = row_count - ((run.y - @y_offset) / row_multiplier).round
39
+ if y_pos <= row_count && y_pos >= 0 && x_pos <= col_count && x_pos >= 0
40
+ local_string_insert(page[y_pos-1], run.text, x_pos)
33
41
  end
34
42
  end
35
43
  interesting_rows(page).map(&:rstrip).join("\n")
@@ -110,21 +118,8 @@ class PDF::Reader
110
118
  runs
111
119
  end
112
120
 
113
- # This is a simple alternative to String#[]=. We can't use the string
114
- # method as it's buggy on rubinius 2.0rc1 (in 1.9 mode)
115
- #
116
- # See my bug report at https://github.com/rubinius/rubinius/issues/1985
117
121
  def local_string_insert(haystack, needle, index)
118
- if @current_platform_is_rbx_19
119
- char_count = needle.length
120
- haystack.replace(
121
- (haystack[0,index] || "") +
122
- needle +
123
- (haystack[index+char_count,500] || "")
124
- )
125
- else
126
- haystack[Range.new(index, index + needle.length - 1)] = String.new(needle)
127
- end
122
+ haystack[Range.new(index, index + needle.length - 1)] = String.new(needle)
128
123
  end
129
124
  end
130
125
  end
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'pdf/reader/transformation_matrix'
4
5
 
@@ -29,7 +30,7 @@ class PDF::Reader
29
30
  @xobject_stack = [page.xobjects]
30
31
  @cs_stack = [page.color_spaces]
31
32
  @stack = [DEFAULT_GRAPHICS_STATE.dup]
32
- state[:ctm] = identity_matrix
33
+ state[:ctm] = identity_matrix
33
34
  end
34
35
 
35
36
  #####################################################
@@ -321,11 +322,13 @@ class PDF::Reader
321
322
  th = state[:h_scaling]
322
323
  # optimise the common path to reduce Float allocations
323
324
  if th == 1 && tj == 0 && tc == 0 && tw == 0
324
- glyph_width = w0 * fs
325
- tx = glyph_width
325
+ tx = w0 * fs
326
+ elsif tj != 0
327
+ # don't apply spacing to TJ displacement
328
+ tx = (w0 - (tj/1000.0)) * fs * th
326
329
  else
327
- glyph_width = ((w0 - (tj/1000.0)) * fs) * th
328
- tx = glyph_width + ((tc + tw) * th)
330
+ # apply horizontal scaling to spacing values but not font size
331
+ tx = ((w0 * fs) + tc + tw) * th
329
332
  end
330
333
 
331
334
  # TODO: I'm pretty sure that tx shouldn't need to be divided by
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'forwardable'
4
5
  require 'pdf/reader/page_layout'
@@ -40,13 +41,17 @@ module PDF
40
41
  # starting a new page
41
42
  def page=(page)
42
43
  @state = PageState.new(page)
44
+ @page = page
43
45
  @content = []
44
46
  @characters = []
45
47
  @mediabox = page.objects.deref(page.attributes[:MediaBox])
48
+ device_bl = @state.ctm_transform(@mediabox[0], @mediabox[1])
49
+ device_tr = @state.ctm_transform(@mediabox[2], @mediabox[3])
50
+ @device_mediabox = [ device_bl.first, device_bl.last, device_tr.first, device_tr.last]
46
51
  end
47
52
 
48
53
  def content
49
- PageLayout.new(@characters, @mediabox).to_s
54
+ PageLayout.new(@characters, @device_mediabox).to_s
50
55
  end
51
56
 
52
57
  #####################################################
@@ -100,6 +105,8 @@ module PDF
100
105
  glyphs.each_with_index do |glyph_code, index|
101
106
  # paint the current glyph
102
107
  newx, newy = @state.trm_transform(0,0)
108
+ newx, newy = apply_rotation(newx, newy)
109
+
103
110
  utf8_chars = @state.current_font.to_utf8(glyph_code)
104
111
 
105
112
  # apply to glyph displacment for the current glyph so the next
@@ -114,6 +121,21 @@ module PDF
114
121
  end
115
122
  end
116
123
 
124
+ def apply_rotation(x, y)
125
+ if @page.rotate == 90
126
+ tmp = x
127
+ x = y
128
+ y = tmp * -1
129
+ elsif @page.rotate == 180
130
+ y *= -1
131
+ elsif @page.rotate == 270
132
+ tmp = x
133
+ x = y * -1
134
+ y = tmp * -1
135
+ end
136
+ return x, y
137
+ end
138
+
117
139
  end
118
140
  end
119
141
  end