hexapdf 0.9.3 → 0.10.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/CONTRIBUTERS +2 -1
  4. data/VERSION +1 -1
  5. data/lib/hexapdf/cli/command.rb +9 -5
  6. data/lib/hexapdf/cli/images.rb +68 -13
  7. data/lib/hexapdf/cli/inspect.rb +201 -71
  8. data/lib/hexapdf/content/canvas.rb +1 -1
  9. data/lib/hexapdf/dictionary.rb +15 -1
  10. data/lib/hexapdf/dictionary_fields.rb +5 -4
  11. data/lib/hexapdf/document.rb +15 -6
  12. data/lib/hexapdf/encryption/security_handler.rb +3 -2
  13. data/lib/hexapdf/font/encoding/mac_expert_encoding.rb +165 -165
  14. data/lib/hexapdf/font/true_type_wrapper.rb +2 -2
  15. data/lib/hexapdf/font/type1/afm_parser.rb +1 -1
  16. data/lib/hexapdf/image_loader/jpeg.rb +13 -11
  17. data/lib/hexapdf/reference.rb +5 -0
  18. data/lib/hexapdf/revision.rb +14 -0
  19. data/lib/hexapdf/serializer.rb +6 -6
  20. data/lib/hexapdf/type/annotations/markup_annotation.rb +1 -1
  21. data/lib/hexapdf/type/image.rb +3 -1
  22. data/lib/hexapdf/version.rb +1 -1
  23. data/lib/hexapdf/xref_section.rb +8 -0
  24. data/man/man1/hexapdf.1 +88 -20
  25. data/test/data/images/truecolour-alpha-8bit.png +0 -0
  26. data/test/data/images/ycck.jpg +0 -0
  27. data/test/hexapdf/content/test_canvas.rb +2 -2
  28. data/test/hexapdf/document/test_images.rb +7 -5
  29. data/test/hexapdf/encryption/test_security_handler.rb +13 -0
  30. data/test/hexapdf/image_loader/test_jpeg.rb +10 -0
  31. data/test/hexapdf/image_loader/test_png.rb +3 -2
  32. data/test/hexapdf/test_dictionary.rb +9 -0
  33. data/test/hexapdf/test_dictionary_fields.rb +15 -0
  34. data/test/hexapdf/test_document.rb +5 -2
  35. data/test/hexapdf/test_reference.rb +4 -0
  36. data/test/hexapdf/test_revision.rb +12 -0
  37. data/test/hexapdf/test_writer.rb +5 -5
  38. data/test/hexapdf/test_xref_section.rb +11 -0
  39. data/test/hexapdf/type/test_form.rb +1 -1
  40. data/test/hexapdf/type/test_image.rb +28 -19
  41. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1011a745720b76eaa963d0a54b6807e732fe6ecd3bcb029870309e42296bbe9
4
- data.tar.gz: 0aed16b283825785a7039ebca11a61da853c7fb01e8d2fadc9e5d7a722e56754
3
+ metadata.gz: e7d800154c0525b724aa0b1a91e360aff2d443bd56e260b5b051a06d605cdb9c
4
+ data.tar.gz: d658fa5ff09f12a169156ee091f66b189874ae4b847cb4effa8d2c60b5f836b1
5
5
  SHA512:
6
- metadata.gz: 70bee8b172f67ab59559ec1b0d740d767dad0db0a5d30476bdbfd71f80f443b31fc8b98bfd62a6d632411c246aa04af9c4203c443f4affc7b0307694bd5b725b
7
- data.tar.gz: 04002b0d6728a37c499c1453a4068161b6c3c2631e318981559e52207681ae268de39b6a5b9127b39ee009be079c06e8ea761c2633d1f586efb0294a923e7a61
6
+ metadata.gz: b600fb0c600a0093e08d5ed3a4d16c2bfee637b0d338783c663f664aa7b4eb5384b20f064137c26d83f4269ed213b5d46c390421946d747f16be9d7e5600f381
7
+ data.tar.gz: 31f7c75f80db9b467f0fe8582616a9d136b9da86995cd6c57c76678a4e916fef0e73a4ad22ef53d8e8c36ce7ab620e303d6d05268d3ce6d551bbc33b67c7f959
@@ -1,3 +1,38 @@
1
+ ## 0.10.0 - 2019-10-02
2
+
3
+ ### Added
4
+
5
+ * [HexaPDF::Reference#to_s] to return the serialized form of the PDF reference
6
+ * [HexaPDF::Revision#xref] for getting cross-reference entries
7
+ * HexaPDF::XRefSection::Entry#to_s to return a description of the
8
+ cross-reference entry
9
+
10
+ ### Changed
11
+
12
+ * Enhanced the `hexapdf images` command to also show information on PPI (pixels
13
+ per inch) and size
14
+ * Completely revamped the `hexapdf inspect` command with an interactive mode,
15
+ structure output, cross-reference entry output and object search
16
+ * Output of validation problem messages for `hexapdf` command to include more
17
+ information
18
+ * The Validation feature to automatically correct String-for-Symbol and
19
+ Symbol-for-String problems
20
+
21
+ ### Fixed
22
+
23
+ * [HexaPDF::Document#wrap] to better handle subtype mappings in case of unknown
24
+ type information
25
+ * [HexaPDF::DictionaryFields::DictionaryConverter] to not allow conversion to a
26
+ [HexaPDF::Stream] subclass from objects without stream data
27
+ * Import of JPEG images with YCCK color encoding
28
+ * Export of images without `/FlateDecode` filter or `/DecodeParms` to PNG files
29
+ * Mistyped name of field type for field `/Popup` of
30
+ [HexaPDF::Type::Annotations::MarkupAnnotation]
31
+ * Loading and saving of encrypted and signed PDFs
32
+ * CLI commands that optimize font data structures won't crash when encountering
33
+ invalid font objects
34
+
35
+
1
36
  ## 0.9.3 - 2019-06-13
2
37
 
3
38
  ### Changed
@@ -1,4 +1,5 @@
1
1
  Count Name
2
2
  ======= ====
3
- 1041 Thomas Leitner <t_leitner@gmx.at>
3
+ 1069 Thomas Leitner <t_leitner@gmx.at>
4
+ 1 Stanislav (Stas) Katkov <sk@skylup.com>
4
5
  1 Daniel Kraus <bovender@bovender.de>
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.3
1
+ 0.10.0
@@ -120,7 +120,7 @@ module HexaPDF
120
120
  if out_file
121
121
  doc.validate(auto_correct: true) do |object, msg, correctable|
122
122
  if command_parser.strict && !correctable
123
- raise "Validation error: #{msg}"
123
+ raise "Validation error for object (#{object.oid},#{object.gen}): #{msg}"
124
124
  elsif command_parser.verbosity_info?
125
125
  $stderr.puts "#{correctable ? 'Corrected' : 'Ignored'} validation problem " \
126
126
  "for object (#{object.oid},#{object.gen}): #{msg}"
@@ -247,7 +247,7 @@ module HexaPDF
247
247
  # Applies the chosen stream mode to the given object.
248
248
  def optimize_stream(obj)
249
249
  return if @out_options.streams == :preserve || !obj.respond_to?(:set_filter) ||
250
- Array(obj[:Filter]).any? {|f| IGNORED_FILTERS[f] }
250
+ Array(obj[:Filter]).any? {|f| IGNORED_FILTERS[f] }
251
251
 
252
252
  obj.set_filter(@out_options.streams == :compress ? :FlateDecode : nil)
253
253
  end
@@ -255,14 +255,18 @@ module HexaPDF
255
255
  # Optimize the object if it is a font object.
256
256
  def optimize_font(obj)
257
257
  return unless @out_options.optimize_fonts && obj.kind_of?(HexaPDF::Type::Font) &&
258
- (obj[:Subtype] == :TrueType ||
259
- (obj[:Subtype] == :Type0 && obj.descendant_font[:Subtype] == :CIDFontType2)) &&
260
- obj.embedded?
258
+ (obj[:Subtype] == :TrueType ||
259
+ (obj[:Subtype] == :Type0 && obj.descendant_font[:Subtype] == :CIDFontType2)) &&
260
+ obj.embedded?
261
261
 
262
262
  font = HexaPDF::Font::TrueType::Font.new(StringIO.new(obj.font_file.stream))
263
263
  data = HexaPDF::Font::TrueType::Optimizer.build_for_pdf(font)
264
264
  obj.font_file.stream = data
265
265
  obj.font_file[:Length1] = data.size
266
+ rescue StandardError => e
267
+ if command_parser.verbosity_info?
268
+ $stderr.puts "Error optimizing font object (#{obj.oid},#{obj.gen}): #{e.message}"
269
+ end
266
270
  end
267
271
 
268
272
  # Applies the encryption related options to the given HexaPDF::Document instance.
@@ -45,6 +45,41 @@ module HexaPDF
45
45
  # See: HexaPDF::Type::Image
46
46
  class Images < Command
47
47
 
48
+ # Extracts the PPI (pixel per inch) information for each image of a content stream.
49
+ class ImageLocationProcessor < HexaPDF::Content::Processor
50
+
51
+ # The mapping of XObject name to [x_ppi, y_ppi].
52
+ attr_reader :result
53
+
54
+ # Initialize the processor with the names of the images for which the PPI should be
55
+ # determined.
56
+ def initialize(names, user_unit)
57
+ super()
58
+ @names = names
59
+ @user_unit = user_unit
60
+ @result = {}
61
+ end
62
+
63
+ # Determine the PPI in x- and y-directions of the specified images.
64
+ def paint_xobject(name)
65
+ super
66
+ return unless @names.delete(name)
67
+ xobject = resources.xobject(name)
68
+ return unless xobject[:Subtype] == :Image
69
+
70
+ w, h = xobject.width, xobject.height
71
+ llx, lly = graphics_state.ctm.evaluate(0, 0).map {|i| i * @user_unit }
72
+ lrx, lry = graphics_state.ctm.evaluate(1, 0).map {|i| i * @user_unit }
73
+ ulx, uly = graphics_state.ctm.evaluate(0, 1).map {|i| i * @user_unit }
74
+
75
+ x_ppi = 72.0 * w / Math.sqrt((lrx - llx)**2 + (lry - lly)**2)
76
+ y_ppi = 72.0 * h / Math.sqrt((ulx - llx)**2 + (uly - lly)**2)
77
+ @result[name] = [x_ppi.round, y_ppi.round]
78
+ raise StopIteration if @names.empty?
79
+ end
80
+
81
+ end
82
+
48
83
  def initialize #:nodoc:
49
84
  super('images', takes_commands: false)
50
85
  short_desc("List or extract images from a PDF file")
@@ -94,16 +129,17 @@ module HexaPDF
94
129
 
95
130
  # Outputs a table with the images of the PDF document.
96
131
  def list_images(doc)
97
- printf("%5s %5s %9s %6s %6s %5s %4s %3s %5s %8s\n",
98
- "index", "page", "oid", "width", "height", "color", "comp", "bpc", "type",
99
- "writable")
100
- puts("-" * 65)
101
- each_image(doc) do |image, index, pindex|
132
+ printf("%5s %5s %9s %6s %6s %5s %4s %3s %5s %5s %6s %5s %8s\n",
133
+ "index", "page", "oid", "width", "height", "color", "comp", "bpc",
134
+ "x-ppi", "y-ppi", "size", "type", "writable")
135
+ puts("-" * 77)
136
+ each_image(doc) do |image, index, pindex, (x_ppi, y_ppi)|
102
137
  info = image.info
103
- printf("%5i %5s %9s %6i %6i %5s %4i %3i %5s %8s\n",
138
+ size = human_readable_file_size(image[:Length] + image[:SMask]&.[](:Length).to_i)
139
+ printf("%5i %5s %9s %6i %6i %5s %4i %3i %5s %5s %6s %5s %8s\n",
104
140
  index, pindex || '-', "#{image.oid},#{image.gen}", info.width, info.height,
105
- info.color_space, info.components, info.bits_per_component, info.type,
106
- info.writable)
141
+ info.color_space, info.components, info.bits_per_component, x_ppi, y_ppi,
142
+ size, info.type, info.writable)
107
143
  end
108
144
  end
109
145
 
@@ -131,11 +167,21 @@ module HexaPDF
131
167
  seen = {}
132
168
 
133
169
  doc.pages.each_with_index do |page, pindex|
134
- page.resources[:XObject]&.each do |_name, xobject|
170
+ image_names = []
171
+ xobjects = page.resources[:XObject]
172
+
173
+ xobjects&.each&.map do |name, xobject|
174
+ image_names << name if xobject[:Subtype] == :Image && !xobject[:ImageMask]
175
+ end
176
+
177
+ processor = ImageLocationProcessor.new(image_names, page[:UserUnit] || 1)
178
+ page.process_contents(processor)
179
+ processor.result.each do |name, ppi|
180
+ xobject = xobjects[name]
135
181
  if seen[xobject]
136
- yield(xobject, seen[xobject], pindex + 1)
137
- elsif xobject[:Subtype] == :Image && !xobject[:ImageMask]
138
- yield(xobject, index, pindex + 1)
182
+ yield(xobject, seen[xobject], pindex + 1, ppi)
183
+ else
184
+ yield(xobject, index, pindex + 1, ppi)
139
185
  seen[xobject] = index
140
186
  index += 1
141
187
  end
@@ -145,12 +191,21 @@ module HexaPDF
145
191
  if @search
146
192
  doc.images.each do |image|
147
193
  next if seen[image]
148
- yield(image, index, nil)
194
+ yield(image, index, nil, nil)
149
195
  index += 1
150
196
  end
151
197
  end
152
198
  end
153
199
 
200
+ # Returns the human readable file size.
201
+ def human_readable_file_size(size)
202
+ case size
203
+ when 0..9999 then "#{size}B"
204
+ when 10_000..999_999 then "#{(size / 1024.to_f).round(1)}K"
205
+ else "#{(size.to_f / 1024 / 1024).round(1)}M"
206
+ end
207
+ end
208
+
154
209
  end
155
210
 
156
211
  end
@@ -50,98 +50,148 @@ module HexaPDF
50
50
  needs to inspect the internal object structure or a stream of a PDF file. A PDF object is
51
51
  always shown in the PDF syntax.
52
52
 
53
- If no option is given, the PDF trailer is shown. Otherwise the various, mutually exclusive
54
- display options define the shown content. If multiple such options are specified only the
55
- last is respected.
56
- EOF
53
+ If no arguments are given, the interactive mode is started. Otherwise the arguments are
54
+ interpreted as interactive mode commands and executed. It is possible to specify more than
55
+ one command in this way by separating them with semicolons, or whitespace in case the
56
+ number of command arguments is fixed.
57
57
 
58
- options.on("--catalog", "Show the PDF catalog dictionary.") do
59
- @exec = :catalog
60
- end
61
- options.on("-c", "--page-count", "Print the number of pages.") do
62
- @exec = :page_count
63
- end
64
- options.on("--pages [PAGES]", "Show the pages with their object and generation numbers " \
65
- "and their associated content streams. If the optional argument PAGES is " \
66
- "specified, only the specified pages are listed.") do |range|
67
- @exec = :pages
68
- @param = range || '1-e'
69
- end
70
- options.on("-o", "--object OID[,GEN]", "Show the object with the given object and " \
71
- "generation numbers. The generation number defaults to 0 if not given.") do |str|
72
- @exec = :object
73
- @param = str
74
- end
75
- options.on("-s", "--stream OID[,GEN]", "Show the filtered stream data (add --raw to get " \
76
- "the raw stream data) of the object with the given object and generation " \
77
- "numbers. The generation number defaults to 0 if not given.") do |str|
78
- @exec = :stream
79
- @param = str
80
- end
81
- options.on("--raw", "Modifies --stream to show the raw stream data instead of the " \
82
- "filtered one.") do
83
- @raw = true
84
- end
58
+ EOF
85
59
 
86
- options.separator("")
87
60
  options.on("--password PASSWORD", "-p", String,
88
61
  "The password for decryption. Use - for reading from standard input.") do |pwd|
89
62
  @password = (pwd == '-' ? read_password : pwd)
90
63
  end
91
64
 
92
65
  @password = nil
93
- @exec = :trailer
94
- @param = nil
95
- @raw = nil
66
+ @serializer = HexaPDF::Serializer.new
96
67
  end
97
68
 
98
- def execute(file) #:nodoc:
99
- with_document(file, password: @password) {|doc| send("do_#{@exec}", doc) }
69
+ def execute(file, *commands) #:nodoc:
70
+ with_document(file, password: @password) do |doc|
71
+ @doc = doc
72
+ if commands.empty?
73
+ while true
74
+ print "cmd> "
75
+ input = $stdin.gets
76
+ (puts; break) unless input
77
+ commands = input.scan(/(["'])(.+?)\1|(\S+)/).map {|a| a[1] || a[2] }
78
+ break if execute_commands(commands)
79
+ end
80
+ else
81
+ execute_commands(commands)
82
+ end
83
+ end
100
84
  end
101
85
 
102
86
  private
103
87
 
104
- def do_catalog(doc) #:nodoc:
105
- puts HexaPDF::Serializer.new.serialize(doc.catalog)
106
- end
88
+ def execute_commands(data) #:nodoc:
89
+ data.map! {|item| item == ";" ? nil : item }
90
+ until data.empty?
91
+ command = data.shift || next
92
+ case command
93
+ when /^\d+(,\d+)?$/, 'o', 'object'
94
+ arg = (command.start_with?('o') ? data.shift : command)
95
+ obj = pdf_object_from_string_reference(arg) rescue puts($!.message)
96
+ if obj.data.stream && command_parser.verbosity_info?
97
+ $stderr.puts("Note: Object also has stream data")
98
+ end
99
+ serialize(obj.value, recursive: false) if obj
107
100
 
108
- def do_trailer(doc) #:nodoc:
109
- puts HexaPDF::Serializer.new.serialize(doc.trailer)
110
- end
101
+ when 'r', 'recursive'
102
+ obj = if (obj = data.shift)
103
+ pdf_object_from_string_reference(obj) rescue puts($!.message)
104
+ else
105
+ @doc.trailer
106
+ end
107
+ serialize(obj.value, recursive: true) if obj
111
108
 
112
- def do_page_count(doc) #:nodoc:
113
- puts doc.pages.count
114
- end
109
+ when 's', 'stream', 'raw', 'raw-stream'
110
+ if (obj = pdf_object_from_string_reference(data.shift) rescue puts($!.message)) &&
111
+ obj.kind_of?(HexaPDF::Stream)
112
+ source = (command.start_with?('raw') ? obj.stream_source : obj.stream_decoder)
113
+ while source.alive? && (stream_data = source.resume)
114
+ $stdout.write(stream_data)
115
+ end
116
+ elsif command_parser.verbosity_info?
117
+ $stderr.puts("Note: Object has no stream data")
118
+ end
115
119
 
116
- def do_pages(doc) #:nodoc:
117
- pages = parse_pages_specification(@param, doc.pages.count)
118
- page_list = doc.pages.to_a
119
- pages.each do |index, _|
120
- page = page_list[index]
121
- str = +"page #{index + 1} (#{page.oid},#{page.gen}): "
122
- str << Array(page[:Contents]).map {|c| "#{c.oid},#{c.gen}" }.join(" ")
123
- puts str
124
- end
125
- end
120
+ when 'x', 'xref'
121
+ if (obj = pdf_object_from_string_reference(data.shift) rescue puts($!.message))
122
+ @doc.revisions.reverse_each do |rev|
123
+ if (xref = rev.xref(obj))
124
+ puts xref
125
+ break
126
+ end
127
+ end
128
+ end
129
+
130
+ when 'c', 'catalog'
131
+ serialize(@doc.catalog.value, recursive: false)
132
+
133
+ when 't', 'trailer'
134
+ serialize(@doc.trailer.value, recursive: false)
135
+
136
+ when 'p', 'pages'
137
+ begin
138
+ pages = parse_pages_specification(data.shift || '1-e', @doc.pages.count)
139
+ rescue StandardError
140
+ $stderr.puts("Error: Invalid page range argument")
141
+ next
142
+ end
143
+ page_list = @doc.pages.to_a
144
+ pages.each do |index, _|
145
+ page = page_list[index]
146
+ str = +"page #{index + 1} (#{page.oid},#{page.gen}): "
147
+ str << Array(page[:Contents]).map {|c| "#{c.oid},#{c.gen}" }.join(" ")
148
+ puts str
149
+ end
150
+
151
+ when 'pc', 'page-count'
152
+ puts @doc.pages.count
126
153
 
127
- def do_object(doc) #:nodoc:
128
- object = doc.object(pdf_reference_from_string(@param))
129
- return unless object
130
- if object.data.stream && command_parser.verbosity_info?
131
- $stderr.puts("Note: Object also has stream data")
154
+ when 'search'
155
+ regexp = data.shift
156
+ unless regexp
157
+ $stderr.puts("Error: Missing argument regexp")
158
+ next
159
+ end
160
+ re = Regexp.new(regexp, Regexp::IGNORECASE)
161
+ @doc.each do |object|
162
+ if @serializer.serialize(object.value).match?(re)
163
+ puts "#{object.oid} #{object.gen} obj"
164
+ serialize(object.value, recursive: false)
165
+ puts "endobj"
166
+ end
167
+ end
168
+
169
+ when 'q', 'quit'
170
+ return true
171
+
172
+ when 'h', 'help'
173
+ puts COMMAND_DESCRIPTIONS.map {|cmd, desc| cmd.ljust(35) << desc }.join("\n")
174
+
175
+ else
176
+ if command
177
+ $stderr.puts("Error: Unknown command '#{command}' - enter 'h' for a list of commands")
178
+ end
179
+ end
132
180
  end
133
- puts HexaPDF::Serializer.new.serialize(object.value)
181
+
182
+ false
134
183
  end
135
184
 
136
- def do_stream(doc) #:nodoc:
137
- object = doc.object(pdf_reference_from_string(@param))
138
- if object.kind_of?(HexaPDF::Stream)
139
- source = (@raw ? object.stream_source : object.stream_decoder)
140
- while source.alive? && (data = source.resume)
141
- $stdout.write(data)
142
- end
143
- elsif command_parser.verbosity_info?
144
- $stderr.puts("Note: Object has no stream data")
185
+ # Resolves the PDF object from the given string reference and returns it.
186
+ def pdf_object_from_string_reference(str)
187
+ if str.nil?
188
+ raise "Error: Missing argument object identifier OID[,GEN]"
189
+ elsif !str.match?(/^\d+(,\d+)?$/)
190
+ raise "Error: Invalid argument: Must be of form OID[,GEN]"
191
+ elsif !(obj = @doc.object(pdf_reference_from_string(str)))
192
+ raise "Error: No object with the given object identifier found"
193
+ else
194
+ obj
145
195
  end
146
196
  end
147
197
 
@@ -151,6 +201,86 @@ module HexaPDF
151
201
  HexaPDF::Reference.new(oid, gen || 0)
152
202
  end
153
203
 
204
+ # Prints the serialized value to the standard output. If +recursive+ is +true+, then the whole
205
+ # object tree is printed, with object references to already printed objects replaced by
206
+ # specially generated PDF references.
207
+ def serialize(val, recursive: true, seen: {}, indent: 0) #:nodoc:
208
+ case val
209
+ when Hash
210
+ puts "<<"
211
+ (recursive ? val.sort : val).each do |k, v|
212
+ next if v.nil? || (v.respond_to?(:null?) && v.null?)
213
+ print ' ' * (indent + 1) + @serializer.serialize_symbol(k) + " "
214
+ serialize(v, recursive: recursive, seen: seen, indent: indent + 1)
215
+ puts
216
+ end
217
+ print "#{' ' * indent}>>"
218
+ when Array
219
+ print "["
220
+ val.each do |v|
221
+ serialize(v, recursive: recursive, seen: seen, indent: indent)
222
+ print " "
223
+ end
224
+ print "]"
225
+ when HexaPDF::Reference
226
+ serialize(@doc.object(val), recursive: recursive, seen: seen, indent: indent)
227
+ when HexaPDF::Object
228
+ if !recursive
229
+ if val.indirect?
230
+ print "#{val.oid} #{val.gen} R"
231
+ else
232
+ serialize(val.value, recursive: recursive, seen: seen, indent: indent)
233
+ end
234
+ elsif val.nil? || seen.key?(val.data)
235
+ print "{ref #{seen[val.data]}}"
236
+ else
237
+ seen[val.data] = (val.type == :Page ? "page #{val.index + 1}" : seen.length + 1)
238
+ print "{obj #{seen[val.data]}} "
239
+ serialize(val.value, recursive: recursive, seen: seen, indent: indent)
240
+ end
241
+ else
242
+ print @serializer.serialize(val)
243
+ end
244
+ puts if indent == 0
245
+ end
246
+
247
+ COMMAND_DESCRIPTIONS = [ #:nodoc:
248
+ ["OID[,GEN] | o[bject] OID[,GEN]", "Print object"],
249
+ ["r[ecursive] OID[,GEN]", "Print object recursively"],
250
+ ["s[tream] OID[,GEN]", "Print filtered stream"],
251
+ ["raw[-stream] OID[,GEN]", "Print raw stream"],
252
+ ["x[ref] OID[,GEN]", "Print the cross-reference entry"],
253
+ ["c[atalog]", "Print the catalog dictionary"],
254
+ ["t[railer]", "Print the trailer dictionary"],
255
+ ["p[ages] [RANGE]", "Print information about pages"],
256
+ ["pc | page-count", "Print the number of pages"],
257
+ ["search REGEXP", "Print objects matching the pattern"],
258
+ ["h[elp]", "Show the help"],
259
+ ["q[uit]", "Quit"],
260
+ ]
261
+
262
+ def help_long_desc #:nodoc:
263
+ output = super
264
+ summary_width = command_parser.main_options.summary_width
265
+ data = <<~HELP
266
+ If a command or an argument is OID[,GEN], object and generation numbers are expected. The
267
+ generation number defaults to 0 if not given. The available commands are:
268
+ HELP
269
+ content = format(data, indent: 0,
270
+ width: command_parser.help_line_width - command_parser.help_indent)
271
+ content << "\n\n"
272
+ COMMAND_DESCRIPTIONS.each do |cmd, desc|
273
+ content << format(cmd.ljust(summary_width + 1) << desc,
274
+ width: command_parser.help_line_width - command_parser.help_indent,
275
+ indent: summary_width + 1, indent_first_line: false) << "\n"
276
+ end
277
+ output << cond_format_help_section("Interactive Mode Commands", content, preformatted: true)
278
+ end
279
+
280
+ def usage_arguments #:nodoc:
281
+ "FILE [[CMD [ARGS]]...]"
282
+ end
283
+
154
284
  end
155
285
 
156
286
  end