pdf-reader 1.1.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +87 -2
  3. data/{README.rdoc → README.md} +43 -31
  4. data/Rakefile +21 -16
  5. data/bin/pdf_callbacks +1 -1
  6. data/bin/pdf_object +4 -1
  7. data/bin/pdf_text +1 -3
  8. data/examples/callbacks.rb +2 -1
  9. data/examples/extract_images.rb +11 -6
  10. data/examples/fuzzy_paragraphs.rb +24 -0
  11. data/lib/pdf/reader/afm/Courier-Bold.afm +342 -0
  12. data/lib/pdf/reader/afm/Courier-BoldOblique.afm +342 -0
  13. data/lib/pdf/reader/afm/Courier-Oblique.afm +342 -0
  14. data/lib/pdf/reader/afm/Courier.afm +342 -0
  15. data/lib/pdf/reader/afm/Helvetica-Bold.afm +2827 -0
  16. data/lib/pdf/reader/afm/Helvetica-BoldOblique.afm +2827 -0
  17. data/lib/pdf/reader/afm/Helvetica-Oblique.afm +3051 -0
  18. data/lib/pdf/reader/afm/Helvetica.afm +3051 -0
  19. data/lib/pdf/reader/afm/MustRead.html +19 -0
  20. data/lib/pdf/reader/afm/Symbol.afm +213 -0
  21. data/lib/pdf/reader/afm/Times-Bold.afm +2588 -0
  22. data/lib/pdf/reader/afm/Times-BoldItalic.afm +2384 -0
  23. data/lib/pdf/reader/afm/Times-Italic.afm +2667 -0
  24. data/lib/pdf/reader/afm/Times-Roman.afm +2419 -0
  25. data/lib/pdf/reader/afm/ZapfDingbats.afm +225 -0
  26. data/lib/pdf/reader/buffer.rb +90 -63
  27. data/lib/pdf/reader/cid_widths.rb +63 -0
  28. data/lib/pdf/reader/cmap.rb +69 -38
  29. data/lib/pdf/reader/encoding.rb +74 -48
  30. data/lib/pdf/reader/error.rb +24 -4
  31. data/lib/pdf/reader/filter/ascii85.rb +28 -0
  32. data/lib/pdf/reader/filter/ascii_hex.rb +30 -0
  33. data/lib/pdf/reader/filter/depredict.rb +141 -0
  34. data/lib/pdf/reader/filter/flate.rb +53 -0
  35. data/lib/pdf/reader/filter/lzw.rb +21 -0
  36. data/lib/pdf/reader/filter/null.rb +18 -0
  37. data/lib/pdf/reader/filter/run_length.rb +45 -0
  38. data/lib/pdf/reader/filter.rb +15 -234
  39. data/lib/pdf/reader/font.rb +107 -43
  40. data/lib/pdf/reader/font_descriptor.rb +80 -0
  41. data/lib/pdf/reader/form_xobject.rb +26 -4
  42. data/lib/pdf/reader/glyph_hash.rb +56 -18
  43. data/lib/pdf/reader/lzw.rb +6 -4
  44. data/lib/pdf/reader/null_security_handler.rb +17 -0
  45. data/lib/pdf/reader/object_cache.rb +40 -16
  46. data/lib/pdf/reader/object_hash.rb +94 -40
  47. data/lib/pdf/reader/object_stream.rb +1 -0
  48. data/lib/pdf/reader/orientation_detector.rb +34 -0
  49. data/lib/pdf/reader/overlapping_runs_filter.rb +65 -0
  50. data/lib/pdf/reader/page.rb +48 -3
  51. data/lib/pdf/reader/page_layout.rb +125 -0
  52. data/lib/pdf/reader/page_state.rb +185 -70
  53. data/lib/pdf/reader/page_text_receiver.rb +70 -20
  54. data/lib/pdf/reader/pages_strategy.rb +4 -293
  55. data/lib/pdf/reader/parser.rb +37 -61
  56. data/lib/pdf/reader/print_receiver.rb +6 -0
  57. data/lib/pdf/reader/reference.rb +4 -1
  58. data/lib/pdf/reader/register_receiver.rb +17 -31
  59. data/lib/pdf/reader/resource_methods.rb +1 -0
  60. data/lib/pdf/reader/standard_security_handler.rb +82 -42
  61. data/lib/pdf/reader/standard_security_handler_v5.rb +91 -0
  62. data/lib/pdf/reader/stream.rb +5 -2
  63. data/lib/pdf/reader/synchronized_cache.rb +33 -0
  64. data/lib/pdf/reader/text_run.rb +99 -0
  65. data/lib/pdf/reader/token.rb +4 -1
  66. data/lib/pdf/reader/transformation_matrix.rb +195 -0
  67. data/lib/pdf/reader/unimplemented_security_handler.rb +17 -0
  68. data/lib/pdf/reader/width_calculator/built_in.rb +67 -0
  69. data/lib/pdf/reader/width_calculator/composite.rb +28 -0
  70. data/lib/pdf/reader/width_calculator/true_type.rb +56 -0
  71. data/lib/pdf/reader/width_calculator/type_one_or_three.rb +33 -0
  72. data/lib/pdf/reader/width_calculator/type_zero.rb +25 -0
  73. data/lib/pdf/reader/width_calculator.rb +12 -0
  74. data/lib/pdf/reader/xref.rb +41 -9
  75. data/lib/pdf/reader.rb +45 -104
  76. data/lib/pdf-reader.rb +4 -1
  77. metadata +220 -101
  78. data/bin/pdf_list_callbacks +0 -17
  79. data/lib/pdf/hash.rb +0 -15
  80. data/lib/pdf/reader/abstract_strategy.rb +0 -81
  81. data/lib/pdf/reader/metadata_strategy.rb +0 -56
  82. data/lib/pdf/reader/text_receiver.rb +0 -264
@@ -0,0 +1,63 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+
6
+ require 'forwardable'
7
+
8
+ class PDF::Reader
9
+ # A Hash-like object that wraps the array of glyph widths in a CID font
10
+ # and gives us a nice way to query it for specific widths.
11
+ #
12
+ # there are two ways to calculate a cidfont_glyph_width, that are defined
13
+ # in Section 9.7.4.3 PDF 32000-1:2008 pp 271, the differences are remarked
14
+ # on below. because of these difference that may be contained within the
15
+ # same array, it is a bit difficult to parse this array.
16
+ class CidWidths
17
+ extend Forwardable
18
+
19
+ # Graphics State Operators
20
+ def_delegators :@widths, :[], :fetch
21
+
22
+ def initialize(default, array)
23
+ @widths = parse_array(default, array.dup)
24
+ end
25
+
26
+ private
27
+
28
+ def parse_array(default, array)
29
+ widths = Hash.new(default)
30
+ params = []
31
+ while array.size > 0
32
+ params << array.shift
33
+
34
+ if params.size == 2 && params.last.is_a?(Array)
35
+ widths.merge! parse_first_form(params.first, params.last)
36
+ params = []
37
+ elsif params.size == 3
38
+ widths.merge! parse_second_form(params[0], params[1], params[2])
39
+ params = []
40
+ end
41
+ end
42
+ widths
43
+ end
44
+
45
+ # this is the form 10 [234 63 234 346 47 234] where width of index 10 is
46
+ # 234, index 11 is 63, etc
47
+ def parse_first_form(first, widths)
48
+ widths.inject({}) { |accum, glyph_width|
49
+ accum[first + accum.size] = glyph_width
50
+ accum
51
+ }
52
+ end
53
+
54
+ # this is the form 10 20 123 where all index between 10 and 20 have width 123
55
+ def parse_second_form(first, final, width)
56
+ (first..final).inject({}) { |accum, index|
57
+ accum[index] = width
58
+ accum
59
+ }
60
+ end
61
+
62
+ end
63
+ end
@@ -1,3 +1,6 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  ################################################################################
2
5
  #
3
6
  # Copyright (C) 2008 James Healy (jimmy@deefa.com)
@@ -24,7 +27,22 @@
24
27
  ################################################################################
25
28
 
26
29
  class PDF::Reader
30
+
31
+ # wraps a string containing a PDF CMap and provides convenience methods for
32
+ # extracting various useful information.
33
+ #
27
34
  class CMap # :nodoc:
35
+ CMAP_KEYWORDS = {
36
+ "begincodespacerange" => 1,
37
+ "endcodespacerange" => 1,
38
+ "beginbfchar" => 1,
39
+ "endbfchar" => 1,
40
+ "beginbfrange" => 1,
41
+ "endbfrange" => 1,
42
+ "begin" => 1,
43
+ "begincmap" => 1,
44
+ "def" => 1
45
+ }
28
46
 
29
47
  attr_reader :map
30
48
 
@@ -34,24 +52,25 @@ class PDF::Reader
34
52
  end
35
53
 
36
54
  def process_data(data)
55
+ parser = build_parser(data)
37
56
  mode = nil
38
- instructions = ""
57
+ instructions = []
39
58
 
40
- data.each_line do |l|
41
- if l.include?("beginbfchar")
59
+ while token = parser.parse_token(CMAP_KEYWORDS)
60
+ if token == "beginbfchar"
42
61
  mode = :char
43
- elsif l.include?("endbfchar")
62
+ elsif token == "endbfchar"
44
63
  process_bfchar_instructions(instructions)
45
- instructions = ""
64
+ instructions = []
46
65
  mode = nil
47
- elsif l.include?("beginbfrange")
66
+ elsif token == "beginbfrange"
48
67
  mode = :range
49
- elsif l.include?("endbfrange")
68
+ elsif token == "endbfrange"
50
69
  process_bfrange_instructions(instructions)
51
- instructions = ""
70
+ instructions = []
52
71
  mode = nil
53
72
  elsif mode == :char || mode == :range
54
- instructions << l
73
+ instructions << token
55
74
  end
56
75
  end
57
76
  end
@@ -60,9 +79,13 @@ class PDF::Reader
60
79
  @map.size
61
80
  end
62
81
 
82
+ # Convert a glyph code into one or more Codepoints.
83
+ #
84
+ # Returns an array of Integers.
85
+ #
63
86
  def decode(c)
64
87
  # TODO: implement the conversion
65
- return c unless c.class == Fixnum
88
+ return c unless Integer === c
66
89
  @map[c]
67
90
  end
68
91
 
@@ -73,33 +96,46 @@ class PDF::Reader
73
96
  Parser.new(buffer)
74
97
  end
75
98
 
99
+ # The following includes some manual decoding of UTF-16BE strings into unicode codepoints. In
100
+ # theory we could replace all the UTF-16 code with something based on Ruby's encoding support:
101
+ #
102
+ # str.dup.force_encoding("utf-16be").encode!("utf-8").unpack("U*")
103
+ #
104
+ # However, some cmaps contain broken surrogate pairs and the ruby encoding support raises an
105
+ # exception when we try converting broken UTF-16 to UTF-8
106
+ #
76
107
  def str_to_int(str)
77
- return nil if str.nil? || str.size == 0 || str.size >= 3
78
-
79
- if str.size == 1
80
- str.unpack("C*")[0]
81
- else
82
- str.unpack("n*")[0]
108
+ return nil if str.nil? || str.size == 0
109
+ unpacked_string = if str.bytesize == 1 # UTF-8
110
+ str.unpack("C*")
111
+ else # UTF-16
112
+ str.unpack("n*")
113
+ end
114
+ result = []
115
+ while unpacked_string.any? do
116
+ if unpacked_string.size >= 2 && unpacked_string[0] > 0xD800 && unpacked_string[0] < 0xDBFF
117
+ # this is a Unicode UTF-16 "Surrogate Pair" see Unicode Spec. Chapter 3.7
118
+ # lets convert to a UTF-32. (the high bit is between 0xD800-0xDBFF, the
119
+ # low bit is between 0xDC00-0xDFFF) for example: U+1D44E (U+D835 U+DC4E)
120
+ points = [unpacked_string.shift, unpacked_string.shift]
121
+ result << (points[0] - 0xD800) * 0x400 + (points[1] - 0xDC00) + 0x10000
122
+ else
123
+ result << unpacked_string.shift
124
+ end
83
125
  end
126
+ result
84
127
  end
85
128
 
86
129
  def process_bfchar_instructions(instructions)
87
- parser = build_parser(instructions)
88
- find = str_to_int(parser.parse_token)
89
- replace = str_to_int(parser.parse_token)
90
- while find && replace
91
- @map[find] = replace
92
- find = str_to_int(parser.parse_token)
93
- replace = str_to_int(parser.parse_token)
130
+ instructions.each_slice(2) do |one, two|
131
+ find = str_to_int(one)
132
+ replace = str_to_int(two)
133
+ @map[find.first] = replace
94
134
  end
95
135
  end
96
136
 
97
137
  def process_bfrange_instructions(instructions)
98
- parser = build_parser(instructions)
99
- start = parser.parse_token
100
- finish = parser.parse_token
101
- to = parser.parse_token
102
- while start && finish && to
138
+ instructions.each_slice(3) do |start, finish, to|
103
139
  if start.kind_of?(String) && finish.kind_of?(String) && to.kind_of?(String)
104
140
  bfrange_type_one(start, finish, to)
105
141
  elsif start.kind_of?(String) && finish.kind_of?(String) && to.kind_of?(Array)
@@ -107,28 +143,23 @@ class PDF::Reader
107
143
  else
108
144
  raise "invalid bfrange section"
109
145
  end
110
- start = parser.parse_token
111
- finish = parser.parse_token
112
- to = parser.parse_token
113
146
  end
114
147
  end
115
148
 
116
149
  def bfrange_type_one(start_code, end_code, dst)
117
- start_code = str_to_int(start_code)
118
- end_code = str_to_int(end_code)
150
+ start_code = str_to_int(start_code)[0]
151
+ end_code = str_to_int(end_code)[0]
119
152
  dst = str_to_int(dst)
120
153
 
121
154
  # add all values in the range to our mapping
122
155
  (start_code..end_code).each_with_index do |val, idx|
123
- @map[val] = dst + idx
124
- # ensure a single range does not exceed 255 chars
125
- raise PDF::Reader::MalformedPDFError, "a CMap bfrange cann't exceed 255 chars" if idx > 255
156
+ @map[val] = dst.length == 1 ? [dst[0] + idx] : [dst[0], dst[1] + 1]
126
157
  end
127
158
  end
128
159
 
129
160
  def bfrange_type_two(start_code, end_code, dst)
130
- start_code = str_to_int(start_code)
131
- end_code = str_to_int(end_code)
161
+ start_code = str_to_int(start_code)[0]
162
+ end_code = str_to_int(end_code)[0]
132
163
  from_range = (start_code..end_code)
133
164
 
134
165
  # add all values in the range to our mapping
@@ -1,3 +1,6 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  ################################################################################
2
5
  #
3
6
  # Copyright (C) 2008 James Healy (jimmy@deefa.com)
@@ -24,6 +27,8 @@
24
27
  ################################################################################
25
28
 
26
29
  class PDF::Reader
30
+ # Util class for working with string encodings in PDF files. Mostly used to
31
+ # convert strings of various PDF-dialect encodings into UTF-8.
27
32
  class Encoding # :nodoc:
28
33
  CONTROL_CHARS = [0,1,2,3,4,5,6,7,8,11,12,14,15,16,17,18,19,20,21,22,23,
29
34
  24,25,26,27,28,29,30,31]
@@ -32,19 +37,25 @@ class PDF::Reader
32
37
  attr_reader :unpack
33
38
 
34
39
  def initialize(enc)
35
- if enc.kind_of?(Hash)
36
- self.differences = enc[:Differences] if enc[:Differences]
37
- enc = enc[:Encoding] || enc[:BaseEncoding]
38
- elsif enc != nil
39
- enc = enc.to_sym
40
+ @mapping = default_mapping # maps from character codes to Unicode codepoints
41
+ @string_cache = {} # maps from character codes to UTF-8 strings.
42
+
43
+ @enc_name = if enc.kind_of?(Hash)
44
+ enc[:Encoding] || enc[:BaseEncoding]
45
+ elsif enc && enc.respond_to?(:to_sym)
46
+ enc.to_sym
40
47
  else
41
- enc = nil
48
+ :StandardEncoding
42
49
  end
43
50
 
44
- @enc_name = enc
45
- @unpack = get_unpack(enc)
46
- @map_file = get_mapping_file(enc)
51
+ @unpack = get_unpack(@enc_name)
52
+ @map_file = get_mapping_file(@enc_name)
53
+
47
54
  load_mapping(@map_file) if @map_file
55
+
56
+ if enc.is_a?(Hash) && enc[:Differences]
57
+ self.differences = enc[:Differences]
58
+ end
48
59
  end
49
60
 
50
61
  # set the differences table for this encoding. should be an array in the following format:
@@ -66,6 +77,7 @@ class PDF::Reader
66
77
  byte = val.to_i
67
78
  else
68
79
  @differences[byte] = val
80
+ @mapping[byte] = glyphlist.name_to_unicode(val)
69
81
  byte += 1
70
82
  end
71
83
  end
@@ -73,6 +85,7 @@ class PDF::Reader
73
85
  end
74
86
 
75
87
  def differences
88
+ # this method is only used by the spec tests
76
89
  @differences ||= {}
77
90
  end
78
91
 
@@ -95,8 +108,52 @@ class PDF::Reader
95
108
  end
96
109
  end
97
110
 
111
+ def int_to_utf8_string(glyph_code)
112
+ @string_cache[glyph_code] ||= internal_int_to_utf8_string(glyph_code)
113
+ end
114
+
115
+ # convert an integer glyph code into an Adobe glyph name.
116
+ #
117
+ # int_to_name(65)
118
+ # => [:A]
119
+ #
120
+ def int_to_name(glyph_code)
121
+ if @enc_name == "Identity-H" || @enc_name == "Identity-V"
122
+ []
123
+ elsif differences[glyph_code]
124
+ [differences[glyph_code]]
125
+ elsif @mapping[glyph_code]
126
+ glyphlist.unicode_to_name(@mapping[glyph_code])
127
+ else
128
+ []
129
+ end
130
+ end
131
+
98
132
  private
99
133
 
134
+ # returns a hash that:
135
+ # - maps control chars and nil to the unicode "unknown character"
136
+ # - leaves all other bytes <= 255 unchaged
137
+ #
138
+ # Each specific encoding will change this default as required for their glyphs
139
+ def default_mapping
140
+ all_bytes = (0..255).to_a
141
+ tuples = all_bytes.map {|i|
142
+ CONTROL_CHARS.include?(i) ? [i, UNKNOWN_CHAR] : [i,i]
143
+ }
144
+ mapping = Hash[tuples]
145
+ mapping[nil] = UNKNOWN_CHAR
146
+ mapping
147
+ end
148
+
149
+ def internal_int_to_utf8_string(glyph_code)
150
+ ret = [
151
+ @mapping[glyph_code.to_i] || glyph_code.to_i
152
+ ].pack("U*")
153
+ ret.force_encoding("UTF-8")
154
+ ret
155
+ end
156
+
100
157
  def utf8_conversion_impossible?
101
158
  @enc_name == :"Identity-H" || @enc_name == :"Identity-V"
102
159
  end
@@ -104,33 +161,13 @@ class PDF::Reader
104
161
  def little_boxes(times)
105
162
  codepoints = [ PDF::Reader::Encoding::UNKNOWN_CHAR ] * times
106
163
  ret = codepoints.pack("U*")
107
- ret.force_encoding("UTF-8") if ret.respond_to?(:force_encoding)
164
+ ret.force_encoding("UTF-8")
108
165
  ret
109
166
  end
110
167
 
111
168
  def convert_to_utf8(str)
112
- ret = str.unpack(unpack).map { |c|
113
- differences[c] || c
114
- }.map { |c|
115
- mapping[c] || c
116
- }.map { |c|
117
- names_to_unicode[c] || c
118
- }.map { |c|
119
- if PDF::Reader::Encoding::CONTROL_CHARS.include?(c)
120
- PDF::Reader::Encoding::UNKNOWN_CHAR
121
- else
122
- c
123
- end
124
- }.map { |c|
125
- if c.nil? || !c.is_a?(Fixnum)
126
- PDF::Reader::Encoding::UNKNOWN_CHAR
127
- else
128
- c
129
- end
130
- }.pack("U*")
131
-
132
- ret.force_encoding("UTF-8") if ret.respond_to?(:force_encoding)
133
-
169
+ ret = str.unpack(unpack).map! { |c| @mapping[c] || c }.pack("U*")
170
+ ret.force_encoding("UTF-8")
134
171
  ret
135
172
  end
136
173
 
@@ -164,26 +201,15 @@ class PDF::Reader
164
201
  end
165
202
  end
166
203
 
167
- def mapping
168
- @mapping ||= {}
169
- end
170
-
171
- def has_mapping?
172
- mapping.size > 0
173
- end
174
-
175
- def names_to_unicode
176
- @names_to_unicode ||= PDF::Reader::GlyphHash.new
204
+ def glyphlist
205
+ @glyphlist ||= PDF::Reader::GlyphHash.new
177
206
  end
178
207
 
179
208
  def load_mapping(file)
180
- return if has_mapping?
181
-
182
- RUBY_VERSION >= "1.9" ? mode = "r:BINARY" : mode = "r"
183
- File.open(file, mode) do |f|
209
+ File.open(file, "r:BINARY") do |f|
184
210
  f.each do |l|
185
- m, single_byte, unicode = *l.match(/([0-9A-Za-z]+);([0-9A-F]{4})/)
186
- mapping["0x#{single_byte}".hex] = "0x#{unicode}".hex if single_byte
211
+ _m, single_byte, unicode = *l.match(/([0-9A-Za-z]+);([0-9A-F]{4})/)
212
+ @mapping["0x#{single_byte}".hex] = "0x#{unicode}".hex if single_byte
187
213
  end
188
214
  end
189
215
  end
@@ -1,3 +1,6 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  ################################################################################
2
5
  #
3
6
  # Copyright (C) 2006 Peter J Jones (pjones@pmade.com)
@@ -21,34 +24,51 @@
21
24
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
25
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
26
  #
24
-
25
27
  class PDF::Reader
26
28
  ################################################################################
27
29
  # An internal PDF::Reader class that helps to verify various parts of the PDF file
28
30
  # are valid
29
31
  class Error # :nodoc:
30
32
  ################################################################################
31
- def self.str_assert (lvalue, rvalue, chars=nil)
33
+ def self.str_assert(lvalue, rvalue, chars=nil)
32
34
  raise MalformedPDFError, "PDF malformed, expected string but found #{lvalue.class} instead" if chars and !lvalue.kind_of?(String)
33
35
  lvalue = lvalue[0,chars] if chars
34
36
  raise MalformedPDFError, "PDF malformed, expected '#{rvalue}' but found #{lvalue} instead" if lvalue != rvalue
35
37
  end
36
38
  ################################################################################
37
- def self.str_assert_not (lvalue, rvalue, chars=nil)
39
+ def self.str_assert_not(lvalue, rvalue, chars=nil)
38
40
  raise MalformedPDFError, "PDF malformed, expected string but found #{lvalue.class} instead" if chars and !lvalue.kind_of?(String)
39
41
  lvalue = lvalue[0,chars] if chars
40
42
  raise MalformedPDFError, "PDF malformed, expected '#{rvalue}' but found #{lvalue} instead" if lvalue == rvalue
41
43
  end
42
44
  ################################################################################
43
- def self.assert_equal (lvalue, rvalue)
45
+ def self.assert_equal(lvalue, rvalue)
44
46
  raise MalformedPDFError, "PDF malformed, expected #{rvalue} but found #{lvalue} instead" if lvalue != rvalue
45
47
  end
46
48
  ################################################################################
47
49
  end
50
+
48
51
  ################################################################################
52
+ # an exception that is raised when we believe the current PDF is not following
53
+ # the PDF spec and cannot be recovered
49
54
  class MalformedPDFError < RuntimeError; end
55
+
56
+ ################################################################################
57
+ # an exception that is raised when an invalid page number is used
58
+ class InvalidPageError < ArgumentError; end
59
+
60
+ ################################################################################
61
+ # an exception that is raised when a PDF object appears to be invalid
50
62
  class InvalidObjectError < MalformedPDFError; end
63
+
64
+ ################################################################################
65
+ # an exception that is raised when a PDF follows the specs but uses a feature
66
+ # that we don't support just yet
51
67
  class UnsupportedFeatureError < RuntimeError; end
68
+
69
+ ################################################################################
70
+ # an exception that is raised when a PDF is encrypted and we don't have the
71
+ # necessary data to decrypt it
52
72
  class EncryptedPDFError < UnsupportedFeatureError; end
53
73
  end
54
74
  ################################################################################