pdf-reader 2.5.0 → 2.9.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +42 -0
  3. data/README.md +16 -1
  4. data/Rakefile +1 -1
  5. data/examples/extract_fonts.rb +12 -7
  6. data/examples/rspec.rb +1 -0
  7. data/lib/pdf/reader/aes_v2_security_handler.rb +41 -0
  8. data/lib/pdf/reader/aes_v3_security_handler.rb +38 -0
  9. data/lib/pdf/reader/bounding_rectangle_runs_filter.rb +16 -0
  10. data/lib/pdf/reader/buffer.rb +90 -46
  11. data/lib/pdf/reader/cid_widths.rb +1 -0
  12. data/lib/pdf/reader/cmap.rb +65 -50
  13. data/lib/pdf/reader/encoding.rb +3 -2
  14. data/lib/pdf/reader/error.rb +19 -3
  15. data/lib/pdf/reader/filter/ascii85.rb +7 -1
  16. data/lib/pdf/reader/filter/ascii_hex.rb +6 -1
  17. data/lib/pdf/reader/filter/depredict.rb +11 -9
  18. data/lib/pdf/reader/filter/flate.rb +4 -2
  19. data/lib/pdf/reader/filter/lzw.rb +2 -0
  20. data/lib/pdf/reader/filter/null.rb +1 -1
  21. data/lib/pdf/reader/filter/run_length.rb +19 -13
  22. data/lib/pdf/reader/filter.rb +2 -1
  23. data/lib/pdf/reader/font.rb +72 -16
  24. data/lib/pdf/reader/font_descriptor.rb +19 -17
  25. data/lib/pdf/reader/form_xobject.rb +15 -5
  26. data/lib/pdf/reader/glyph_hash.rb +16 -9
  27. data/lib/pdf/reader/glyphlist-zapfdingbats.txt +245 -0
  28. data/lib/pdf/reader/key_builder_v5.rb +138 -0
  29. data/lib/pdf/reader/lzw.rb +4 -2
  30. data/lib/pdf/reader/null_security_handler.rb +1 -4
  31. data/lib/pdf/reader/object_cache.rb +1 -0
  32. data/lib/pdf/reader/object_hash.rb +252 -44
  33. data/lib/pdf/reader/object_stream.rb +1 -0
  34. data/lib/pdf/reader/overlapping_runs_filter.rb +11 -4
  35. data/lib/pdf/reader/page.rb +99 -19
  36. data/lib/pdf/reader/page_layout.rb +36 -37
  37. data/lib/pdf/reader/page_state.rb +12 -11
  38. data/lib/pdf/reader/page_text_receiver.rb +57 -10
  39. data/lib/pdf/reader/pages_strategy.rb +1 -0
  40. data/lib/pdf/reader/parser.rb +23 -12
  41. data/lib/pdf/reader/point.rb +25 -0
  42. data/lib/pdf/reader/print_receiver.rb +1 -0
  43. data/lib/pdf/reader/rc4_security_handler.rb +38 -0
  44. data/lib/pdf/reader/rectangle.rb +113 -0
  45. data/lib/pdf/reader/reference.rb +1 -0
  46. data/lib/pdf/reader/register_receiver.rb +1 -0
  47. data/lib/pdf/reader/{resource_methods.rb → resources.rb} +16 -9
  48. data/lib/pdf/reader/security_handler_factory.rb +79 -0
  49. data/lib/pdf/reader/{standard_security_handler.rb → standard_key_builder.rb} +23 -94
  50. data/lib/pdf/reader/stream.rb +2 -1
  51. data/lib/pdf/reader/synchronized_cache.rb +1 -0
  52. data/lib/pdf/reader/text_run.rb +14 -6
  53. data/lib/pdf/reader/token.rb +1 -0
  54. data/lib/pdf/reader/transformation_matrix.rb +1 -0
  55. data/lib/pdf/reader/type_check.rb +52 -0
  56. data/lib/pdf/reader/unimplemented_security_handler.rb +1 -0
  57. data/lib/pdf/reader/validating_receiver.rb +262 -0
  58. data/lib/pdf/reader/width_calculator/built_in.rb +1 -0
  59. data/lib/pdf/reader/width_calculator/composite.rb +1 -0
  60. data/lib/pdf/reader/width_calculator/true_type.rb +2 -1
  61. data/lib/pdf/reader/width_calculator/type_one_or_three.rb +1 -0
  62. data/lib/pdf/reader/width_calculator/type_zero.rb +1 -0
  63. data/lib/pdf/reader/width_calculator.rb +1 -0
  64. data/lib/pdf/reader/xref.rb +27 -4
  65. data/lib/pdf/reader/zero_width_runs_filter.rb +13 -0
  66. data/lib/pdf/reader.rb +46 -15
  67. data/lib/pdf-reader.rb +1 -0
  68. data/rbi/pdf-reader.rbi +1978 -0
  69. metadata +21 -10
  70. data/lib/pdf/reader/orientation_detector.rb +0 -34
  71. data/lib/pdf/reader/standard_security_handler_v5.rb +0 -91
@@ -1,7 +1,9 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  require 'pdf/reader/overlapping_runs_filter'
6
+ require 'pdf/reader/zero_width_runs_filter'
5
7
 
6
8
  class PDF::Reader
7
9
 
@@ -15,14 +17,15 @@ class PDF::Reader
15
17
  DEFAULT_FONT_SIZE = 12
16
18
 
17
19
  def initialize(runs, mediabox)
18
- raise ArgumentError, "a mediabox must be provided" if mediabox.nil?
20
+ # mediabox is a 4-element array for now, but it'd be nice to switch to a
21
+ # PDF::Reader::Rectangle at some point
22
+ PDF::Reader::Error.validate_not_nil(mediabox, "mediabox")
19
23
 
20
- @runs = merge_runs(OverlappingRunsFilter.exclude_redundant_runs(runs))
24
+ @mediabox = process_mediabox(mediabox)
25
+ @runs = runs
21
26
  @mean_font_size = mean(@runs.map(&:font_size)) || DEFAULT_FONT_SIZE
22
27
  @mean_font_size = DEFAULT_FONT_SIZE if @mean_font_size == 0
23
- @mean_glyph_width = mean(@runs.map(&:mean_character_width)) || 0
24
- @page_width = (mediabox[2] - mediabox[0]).abs
25
- @page_height = (mediabox[3] - mediabox[1]).abs
28
+ @median_glyph_width = median(@runs.map(&:mean_character_width)) || 0
26
29
  @x_offset = @runs.map(&:x).sort.first || 0
27
30
  lowest_y = @runs.map(&:y).sort.first || 0
28
31
  @y_offset = lowest_y > 0 ? 0 : lowest_y
@@ -45,6 +48,14 @@ class PDF::Reader
45
48
 
46
49
  private
47
50
 
51
+ def page_width
52
+ @mediabox.width
53
+ end
54
+
55
+ def page_height
56
+ @mediabox.height
57
+ end
58
+
48
59
  # given an array of strings, return a new array with empty rows from the
49
60
  # beginning and end removed.
50
61
  #
@@ -63,19 +74,19 @@ class PDF::Reader
63
74
  end
64
75
 
65
76
  def row_count
66
- @row_count ||= (@page_height / @mean_font_size).floor
77
+ @row_count ||= (page_height / @mean_font_size).floor
67
78
  end
68
79
 
69
80
  def col_count
70
- @col_count ||= ((@page_width / @mean_glyph_width) * 1.05).floor
81
+ @col_count ||= ((page_width / @median_glyph_width) * 1.05).floor
71
82
  end
72
83
 
73
84
  def row_multiplier
74
- @row_multiplier ||= @page_height.to_f / row_count.to_f
85
+ @row_multiplier ||= page_height.to_f / row_count.to_f
75
86
  end
76
87
 
77
88
  def col_multiplier
78
- @col_multiplier ||= @page_width.to_f / col_count.to_f
89
+ @col_multiplier ||= page_width.to_f / col_count.to_f
79
90
  end
80
91
 
81
92
  def mean(collection)
@@ -86,40 +97,28 @@ class PDF::Reader
86
97
  end
87
98
  end
88
99
 
89
- def each_line(&block)
90
- @runs.sort.group_by { |run|
91
- run.y.to_i
92
- }.map { |y, collection|
93
- yield y, collection
94
- }
100
+ def median(collection)
101
+ if collection.size == 0
102
+ 0
103
+ else
104
+ collection.sort[(collection.size * 0.5).floor]
105
+ end
95
106
  end
96
107
 
97
- # take a collection of TextRun objects and merge any that are in close
98
- # proximity
99
- def merge_runs(runs)
100
- runs.group_by { |char|
101
- char.y.to_i
102
- }.map { |y, chars|
103
- group_chars_into_runs(chars.sort)
104
- }.flatten.sort
108
+ def local_string_insert(haystack, needle, index)
109
+ haystack[Range.new(index, index + needle.length - 1)] = String.new(needle)
105
110
  end
106
111
 
107
- def group_chars_into_runs(chars)
108
- runs = []
109
- while head = chars.shift
110
- if runs.empty?
111
- runs << head
112
- elsif runs.last.mergable?(head)
113
- runs[-1] = runs.last + head
114
- else
115
- runs << head
116
- end
112
+ def process_mediabox(mediabox)
113
+ if mediabox.is_a?(Array)
114
+ msg = "Passing the mediabox to PageLayout as an Array is deprecated," +
115
+ " please use a Rectangle instead"
116
+ $stderr.puts msg
117
+ PDF::Reader::Rectangle.from_array(mediabox)
118
+ else
119
+ mediabox
117
120
  end
118
- runs
119
121
  end
120
122
 
121
- def local_string_insert(haystack, needle, index)
122
- haystack[Range.new(index, index + needle.length - 1)] = String.new(needle)
123
- end
124
123
  end
125
124
  end
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  require 'pdf/reader/transformation_matrix'
@@ -312,7 +313,7 @@ class PDF::Reader
312
313
  # may need to be added
313
314
  #
314
315
  def process_glyph_displacement(w0, tj, word_boundary)
315
- fs = font_size # font size
316
+ fs = state[:text_font_size]
316
317
  tc = state[:char_spacing]
317
318
  if word_boundary
318
319
  tw = state[:word_spacing]
@@ -330,16 +331,16 @@ class PDF::Reader
330
331
  # apply horizontal scaling to spacing values but not font size
331
332
  tx = ((w0 * fs) + tc + tw) * th
332
333
  end
333
-
334
- # TODO: I'm pretty sure that tx shouldn't need to be divided by
335
- # ctm[0] here, but this gets my tests green and I'm out of
336
- # ideas for now
337
334
  # TODO: support ty > 0
338
- if ctm.a == 1 || ctm.a == 0
339
- @text_matrix.horizontal_displacement_multiply!(tx)
340
- else
341
- @text_matrix.horizontal_displacement_multiply!(tx/ctm.a)
342
- end
335
+ ty = 0
336
+ temp = TransformationMatrix.new(1, 0,
337
+ 0, 1,
338
+ tx, ty)
339
+ @text_matrix = temp.multiply!(
340
+ @text_matrix.a, @text_matrix.b,
341
+ @text_matrix.c, @text_matrix.d,
342
+ @text_matrix.e, @text_matrix.f
343
+ )
343
344
  @font_size = @text_rendering_matrix = nil # invalidate cached value
344
345
  end
345
346
 
@@ -383,7 +384,7 @@ class PDF::Reader
383
384
  #
384
385
  def build_fonts(raw_fonts)
385
386
  wrapped_fonts = raw_fonts.map { |label, font|
386
- [label, PDF::Reader::Font.new(@objects, @objects.deref(font))]
387
+ [label, PDF::Reader::Font.new(@objects, @objects.deref_hash(font) || {})]
387
388
  }
388
389
 
389
390
  ::Hash[wrapped_fonts]
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  require 'forwardable'
@@ -44,14 +45,34 @@ module PDF
44
45
  @page = page
45
46
  @content = []
46
47
  @characters = []
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]
51
48
  end
52
49
 
50
+ def runs(opts = {})
51
+ runs = @characters
52
+
53
+ if rect = opts.fetch(:rect, @page.rectangles[:CropBox])
54
+ runs = BoundingRectangleRunsFilter.runs_within_rect(runs, rect)
55
+ end
56
+
57
+ if opts.fetch(:skip_zero_width, true)
58
+ runs = ZeroWidthRunsFilter.exclude_zero_width_runs(runs)
59
+ end
60
+
61
+ if opts.fetch(:skip_overlapping, true)
62
+ runs = OverlappingRunsFilter.exclude_redundant_runs(runs)
63
+ end
64
+
65
+ if opts.fetch(:merge, true)
66
+ runs = merge_runs(runs)
67
+ end
68
+
69
+ runs
70
+ end
71
+
72
+ # deprecated
53
73
  def content
54
- PageLayout.new(@characters, @device_mediabox).to_s
74
+ mediabox = @page.rectangles[:MediaBox]
75
+ PageLayout.new(runs, mediabox).to_s
55
76
  end
56
77
 
57
78
  #####################################################
@@ -66,8 +87,10 @@ module PDF
66
87
  params.each do |arg|
67
88
  if arg.is_a?(String)
68
89
  internal_show_text(arg)
69
- else
90
+ elsif arg.is_a?(Numeric)
70
91
  @state.process_glyph_displacement(0, arg, false)
92
+ else
93
+ # skip it
71
94
  end
72
95
  end
73
96
  end
@@ -98,6 +121,7 @@ module PDF
98
121
  private
99
122
 
100
123
  def internal_show_text(string)
124
+ PDF::Reader::Error.validate_type_as_malformed(string, "string", String)
101
125
  if @state.current_font.nil?
102
126
  raise PDF::Reader::MalformedPDFError, "current font is invalid"
103
127
  end
@@ -111,7 +135,7 @@ module PDF
111
135
 
112
136
  # apply to glyph displacment for the current glyph so the next
113
137
  # glyph will appear in the correct position
114
- glyph_width = @state.current_font.glyph_width(glyph_code) / 1000.0
138
+ glyph_width = @state.current_font.glyph_width_in_text_space(glyph_code)
115
139
  th = 1
116
140
  scaled_glyph_width = glyph_width * @state.font_size * th
117
141
  unless utf8_chars == SPACE
@@ -128,14 +152,37 @@ module PDF
128
152
  y = tmp * -1
129
153
  elsif @page.rotate == 180
130
154
  y *= -1
155
+ x *= -1
131
156
  elsif @page.rotate == 270
132
- tmp = x
133
- x = y * -1
134
- y = tmp * -1
157
+ tmp = y
158
+ y = x
159
+ x = tmp * -1
135
160
  end
136
161
  return x, y
137
162
  end
138
163
 
164
+ # take a collection of TextRun objects and merge any that are in close
165
+ # proximity
166
+ def merge_runs(runs)
167
+ runs.group_by { |char|
168
+ char.y.to_i
169
+ }.map { |y, chars|
170
+ group_chars_into_runs(chars.sort)
171
+ }.flatten.sort
172
+ end
173
+
174
+ def group_chars_into_runs(chars)
175
+ chars.each_with_object([]) do |char, runs|
176
+ if runs.empty?
177
+ runs << char
178
+ elsif runs.last.mergable?(char)
179
+ runs[-1] = runs.last + char
180
+ else
181
+ runs << char
182
+ end
183
+ end
184
+ end
185
+
139
186
  end
140
187
  end
141
188
  end
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  ################################################################################
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  ################################################################################
@@ -79,8 +80,8 @@ class PDF::Reader
79
80
  token
80
81
  elsif operators.has_key? token
81
82
  Token.new(token)
82
- elsif token.respond_to?(:to_token)
83
- token.to_token
83
+ elsif token.frozen?
84
+ token
84
85
  elsif token =~ /\d*\.\d/
85
86
  token.to_f
86
87
  else
@@ -102,7 +103,7 @@ class PDF::Reader
102
103
  obj = parse_token
103
104
  post_obj = parse_token
104
105
 
105
- if post_obj == "stream"
106
+ if obj.is_a?(Hash) && post_obj == "stream"
106
107
  stream(obj)
107
108
  else
108
109
  obj
@@ -120,7 +121,7 @@ class PDF::Reader
120
121
  key = parse_token
121
122
  break if key.kind_of?(Token) and key == ">>"
122
123
  raise MalformedPDFError, "unterminated dict" if @buffer.empty?
123
- raise MalformedPDFError, "Dictionary key (#{key.inspect}) is not a name" unless key.kind_of?(Symbol)
124
+ PDF::Reader::Error.validate_type_as_malformed(key, "Dictionary key", Symbol)
124
125
 
125
126
  value = parse_token
126
127
  value.kind_of?(Token) and Error.str_assert_not(value, ">>")
@@ -166,7 +167,9 @@ class PDF::Reader
166
167
 
167
168
  # add a missing digit if required, as required by the spec
168
169
  str << "0" unless str.size % 2 == 0
169
- str.scan(/../).map {|i| i.hex.chr}.join.force_encoding("binary")
170
+ str.chars.each_slice(2).map { |nibbles|
171
+ nibbles.join("").hex.chr
172
+ }.join.force_encoding("binary")
170
173
  end
171
174
  ################################################################################
172
175
  # Reads a PDF String from the buffer and converts it to a Ruby String
@@ -175,15 +178,18 @@ class PDF::Reader
175
178
  return "".dup.force_encoding("binary") if str == ")"
176
179
  Error.assert_equal(parse_token, ")")
177
180
 
178
- str.gsub!(/\\([nrtbf()\\\n]|\d{1,3})?|\r\n?|\n\r/m) do |match|
179
- MAPPING[match] || "".dup
181
+ str.gsub!(/\\(\r\n|[nrtbf()\\\n\r]|([0-7]{1,3}))?|\r\n?/m) do |match|
182
+ if $2.nil? # not octal digits
183
+ MAPPING[match] || "".dup
184
+ else # must be octal digits
185
+ ($2.oct & 0xff).chr # ignore high level overflow
186
+ end
180
187
  end
181
188
  str.force_encoding("binary")
182
189
  end
183
190
 
184
191
  MAPPING = {
185
192
  "\r" => "\n",
186
- "\n\r" => "\n",
187
193
  "\r\n" => "\n",
188
194
  "\\n" => "\n",
189
195
  "\\r" => "\r",
@@ -194,20 +200,25 @@ class PDF::Reader
194
200
  "\\)" => ")",
195
201
  "\\\\" => "\\",
196
202
  "\\\n" => "",
203
+ "\\\r" => "",
204
+ "\\\r\n" => "",
197
205
  }
198
- 0.upto(9) { |n| MAPPING["\\00"+n.to_s] = ("00"+n.to_s).oct.chr }
199
- 0.upto(99) { |n| MAPPING["\\0"+n.to_s] = ("0"+n.to_s).oct.chr }
200
- 0.upto(377) { |n| MAPPING["\\"+n.to_s] = n.to_s.oct.chr }
201
206
 
202
207
  ################################################################################
203
208
  # Decodes the contents of a PDF Stream and returns it as a Ruby String.
204
209
  def stream(dict)
205
210
  raise MalformedPDFError, "PDF malformed, missing stream length" unless dict.has_key?(:Length)
206
211
  if @objects
207
- length = @objects.deref(dict[:Length])
212
+ length = @objects.deref_integer(dict[:Length])
213
+ if dict[:Filter]
214
+ dict[:Filter] = @objects.deref_name_or_array(dict[:Filter])
215
+ end
208
216
  else
209
217
  length = dict[:Length] || 0
210
218
  end
219
+
220
+ PDF::Reader::Error.validate_type_as_malformed(length, "length", Numeric)
221
+
211
222
  data = @buffer.read(length, :skip_eol => true)
212
223
 
213
224
  Error.str_assert(parse_token, "endstream")
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ # typed: strict
3
+ # frozen_string_literal: true
4
+
5
+ module PDF
6
+ class Reader
7
+
8
+ # PDFs are all about positioning content on a page, so there's lots of need to
9
+ # work with a set of X,Y coordinates.
10
+ #
11
+ class Point
12
+
13
+ attr_reader :x, :y
14
+
15
+ def initialize(x, y)
16
+ @x, @y = x, y
17
+ end
18
+
19
+ def ==(other)
20
+ other.respond_to?(:x) && other.respond_to?(:y) && x == other.x && y == other.y
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: strict
2
3
  # frozen_string_literal: true
3
4
 
4
5
  class PDF::Reader
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+ # typed: strict
3
+ # frozen_string_literal: true
4
+
5
+ require 'digest/md5'
6
+ require 'rc4'
7
+
8
+ class PDF::Reader
9
+
10
+ # Decrypts data using the RC4 algorithim defined in the PDF spec. Requires
11
+ # a decryption key, which is usually generated by PDF::Reader::StandardKeyBuilder
12
+ #
13
+ class Rc4SecurityHandler
14
+
15
+ def initialize(key)
16
+ @encrypt_key = key
17
+ end
18
+
19
+ ##7.6.2 General Encryption Algorithm
20
+ #
21
+ # Algorithm 1: Encryption of data using the RC4 algorithm
22
+ #
23
+ # version <=3 or (version == 4 and CFM == V2)
24
+ #
25
+ # buf - a string to decrypt
26
+ # ref - a PDF::Reader::Reference for the object to decrypt
27
+ #
28
+ def decrypt( buf, ref )
29
+ objKey = @encrypt_key.dup
30
+ (0..2).each { |e| objKey << (ref.id >> e*8 & 0xFF ) }
31
+ (0..1).each { |e| objKey << (ref.gen >> e*8 & 0xFF ) }
32
+ length = objKey.length < 16 ? objKey.length : 16
33
+ rc4 = RC4.new( Digest::MD5.digest(objKey)[0,length] )
34
+ rc4.decrypt(buf)
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,113 @@
1
+ # coding: utf-8
2
+ # typed: strict
3
+ # frozen_string_literal: true
4
+
5
+ module PDF
6
+ class Reader
7
+
8
+ # PDFs represent rectangles all over the place. They're 4 element arrays, like this:
9
+ #
10
+ # [A, B, C, D]
11
+ #
12
+ # Four element arrays are yucky to work with though, so here's a class that's better.
13
+ # Initialize it with the 4 elements, and get utility functions (width, height, etc)
14
+ # for free.
15
+ #
16
+ # By convention the first two elements are x1, y1, the co-ords for the bottom left corner
17
+ # of the rectangle. The third and fourth elements are x2, y2, the co-ords for the top left
18
+ # corner of the rectangle. It's valid for the alternative corners to be used though, so
19
+ # we don't assume which is which.
20
+ #
21
+ class Rectangle
22
+
23
+ attr_reader :bottom_left, :bottom_right, :top_left, :top_right
24
+
25
+ def initialize(x1, y1, x2, y2)
26
+ set_corners(x1, y1, x2, y2)
27
+ end
28
+
29
+ def self.from_array(arr)
30
+ if arr.size != 4
31
+ raise ArgumentError, "Only 4-element Arrays can be converted to a Rectangle"
32
+ end
33
+
34
+ PDF::Reader::Rectangle.new(
35
+ arr[0].to_f,
36
+ arr[1].to_f,
37
+ arr[2].to_f,
38
+ arr[3].to_f,
39
+ )
40
+ end
41
+
42
+ def ==(other)
43
+ to_a == other.to_a
44
+ end
45
+
46
+ def height
47
+ top_right.y - bottom_right.y
48
+ end
49
+
50
+ def width
51
+ bottom_right.x - bottom_left.x
52
+ end
53
+
54
+ def contains?(point)
55
+ point.x >= bottom_left.x && point.x <= top_right.x &&
56
+ point.y >= bottom_left.y && point.y <= top_right.y
57
+ end
58
+
59
+ # A pdf-style 4-number array
60
+ def to_a
61
+ [
62
+ bottom_left.x,
63
+ bottom_left.y,
64
+ top_right.x,
65
+ top_right.y,
66
+ ]
67
+ end
68
+
69
+ def apply_rotation(degrees)
70
+ return if degrees != 90 && degrees != 180 && degrees != 270
71
+
72
+ if degrees == 90
73
+ new_x1 = bottom_left.x
74
+ new_y1 = bottom_left.y - width
75
+ new_x2 = bottom_left.x + height
76
+ new_y2 = bottom_left.y
77
+ elsif degrees == 180
78
+ new_x1 = bottom_left.x - width
79
+ new_y1 = bottom_left.y - height
80
+ new_x2 = bottom_left.x
81
+ new_y2 = bottom_left.y
82
+ elsif degrees == 270
83
+ new_x1 = bottom_left.x - height
84
+ new_y1 = bottom_left.y
85
+ new_x2 = bottom_left.x
86
+ new_y2 = bottom_left.y + width
87
+ end
88
+ set_corners(new_x1 || 0, new_y1 || 0, new_x2 || 0, new_y2 || 0)
89
+ end
90
+
91
+ private
92
+
93
+ def set_corners(x1, y1, x2, y2)
94
+ @bottom_left = PDF::Reader::Point.new(
95
+ [x1, x2].min,
96
+ [y1, y2].min,
97
+ )
98
+ @bottom_right = PDF::Reader::Point.new(
99
+ [x1, x2].max,
100
+ [y1, y2].min,
101
+ )
102
+ @top_left = PDF::Reader::Point.new(
103
+ [x1, x2].min,
104
+ [y1, y2].max,
105
+ )
106
+ @top_right = PDF::Reader::Point.new(
107
+ [x1, x2].max,
108
+ [y1, y2].max,
109
+ )
110
+ end
111
+ end
112
+ end
113
+ end
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  ################################################################################
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: strict
2
3
  # frozen_string_literal: true
3
4
 
4
5
  # Copyright (C) 2010 James Healy (jimmy@deefa.com)
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ # typed: true
2
3
  # frozen_string_literal: true
3
4
 
4
5
  module PDF
@@ -6,7 +7,13 @@ module PDF
6
7
 
7
8
  # mixin for common methods in Page and FormXobjects
8
9
  #
9
- module ResourceMethods
10
+ class Resources
11
+
12
+ def initialize(objects, resources)
13
+ @objects = objects
14
+ @resources = resources
15
+ end
16
+
10
17
  # Returns a Hash of color spaces that are available to this page
11
18
  #
12
19
  # NOTE: this method de-serialise objects from the underlying PDF
@@ -14,7 +21,7 @@ module PDF
14
21
  # of calling it over and over.
15
22
  #
16
23
  def color_spaces
17
- @objects.deref!(resources[:ColorSpace]) || {}
24
+ @objects.deref_hash!(@resources[:ColorSpace]) || {}
18
25
  end
19
26
 
20
27
  # Returns a Hash of fonts that are available to this page
@@ -24,7 +31,7 @@ module PDF
24
31
  # of calling it over and over.
25
32
  #
26
33
  def fonts
27
- @objects.deref!(resources[:Font]) || {}
34
+ @objects.deref_hash!(@resources[:Font]) || {}
28
35
  end
29
36
 
30
37
  # Returns a Hash of external graphic states that are available to this
@@ -35,7 +42,7 @@ module PDF
35
42
  # of calling it over and over.
36
43
  #
37
44
  def graphic_states
38
- @objects.deref!(resources[:ExtGState]) || {}
45
+ @objects.deref_hash!(@resources[:ExtGState]) || {}
39
46
  end
40
47
 
41
48
  # Returns a Hash of patterns that are available to this page
@@ -45,7 +52,7 @@ module PDF
45
52
  # of calling it over and over.
46
53
  #
47
54
  def patterns
48
- @objects.deref!(resources[:Pattern]) || {}
55
+ @objects.deref_hash!(@resources[:Pattern]) || {}
49
56
  end
50
57
 
51
58
  # Returns an Array of procedure sets that are available to this page
@@ -55,7 +62,7 @@ module PDF
55
62
  # of calling it over and over.
56
63
  #
57
64
  def procedure_sets
58
- @objects.deref!(resources[:ProcSet]) || []
65
+ @objects.deref_array!(@resources[:ProcSet]) || []
59
66
  end
60
67
 
61
68
  # Returns a Hash of properties sets that are available to this page
@@ -65,7 +72,7 @@ module PDF
65
72
  # of calling it over and over.
66
73
  #
67
74
  def properties
68
- @objects.deref!(resources[:Properties]) || {}
75
+ @objects.deref_hash!(@resources[:Properties]) || {}
69
76
  end
70
77
 
71
78
  # Returns a Hash of shadings that are available to this page
@@ -75,7 +82,7 @@ module PDF
75
82
  # of calling it over and over.
76
83
  #
77
84
  def shadings
78
- @objects.deref!(resources[:Shading]) || {}
85
+ @objects.deref_hash!(@resources[:Shading]) || {}
79
86
  end
80
87
 
81
88
  # Returns a Hash of XObjects that are available to this page
@@ -85,7 +92,7 @@ module PDF
85
92
  # of calling it over and over.
86
93
  #
87
94
  def xobjects
88
- @objects.deref!(resources[:XObject]) || {}
95
+ @objects.deref_hash!(@resources[:XObject]) || {}
89
96
  end
90
97
 
91
98
  end