hexapdf 0.22.0 → 0.23.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/lib/hexapdf/cli/form.rb +26 -3
  4. data/lib/hexapdf/cli/inspect.rb +12 -3
  5. data/lib/hexapdf/cli/modify.rb +23 -3
  6. data/lib/hexapdf/composer.rb +24 -2
  7. data/lib/hexapdf/document/destinations.rb +396 -0
  8. data/lib/hexapdf/document.rb +38 -89
  9. data/lib/hexapdf/layout/frame.rb +8 -9
  10. data/lib/hexapdf/layout/style.rb +280 -7
  11. data/lib/hexapdf/layout/text_box.rb +10 -2
  12. data/lib/hexapdf/layout/text_layouter.rb +6 -1
  13. data/lib/hexapdf/revision.rb +8 -1
  14. data/lib/hexapdf/revisions.rb +151 -50
  15. data/lib/hexapdf/task/optimize.rb +21 -11
  16. data/lib/hexapdf/type/acro_form/text_field.rb +8 -0
  17. data/lib/hexapdf/type/catalog.rb +9 -1
  18. data/lib/hexapdf/type/names.rb +13 -0
  19. data/lib/hexapdf/type/xref_stream.rb +2 -1
  20. data/lib/hexapdf/utils/sorted_tree_node.rb +3 -1
  21. data/lib/hexapdf/version.rb +1 -1
  22. data/lib/hexapdf/writer.rb +15 -2
  23. data/test/hexapdf/document/test_destinations.rb +338 -0
  24. data/test/hexapdf/encryption/test_security_handler.rb +2 -2
  25. data/test/hexapdf/layout/test_frame.rb +15 -1
  26. data/test/hexapdf/layout/test_text_box.rb +16 -0
  27. data/test/hexapdf/layout/test_text_layouter.rb +7 -0
  28. data/test/hexapdf/task/test_optimize.rb +17 -4
  29. data/test/hexapdf/test_composer.rb +24 -1
  30. data/test/hexapdf/test_document.rb +30 -133
  31. data/test/hexapdf/test_parser.rb +1 -1
  32. data/test/hexapdf/test_revision.rb +14 -0
  33. data/test/hexapdf/test_revisions.rb +137 -29
  34. data/test/hexapdf/test_writer.rb +43 -14
  35. data/test/hexapdf/type/acro_form/test_text_field.rb +17 -0
  36. data/test/hexapdf/type/test_catalog.rb +8 -0
  37. data/test/hexapdf/type/test_names.rb +20 -0
  38. data/test/hexapdf/type/test_xref_stream.rb +2 -1
  39. data/test/hexapdf/utils/test_sorted_tree_node.rb +11 -1
  40. metadata +5 -2
@@ -106,6 +106,7 @@ module HexaPDF
106
106
  autoload(:Images, 'hexapdf/document/images')
107
107
  autoload(:Files, 'hexapdf/document/files')
108
108
  autoload(:Signatures, 'hexapdf/document/signatures')
109
+ autoload(:Destinations, 'hexapdf/document/destinations')
109
110
 
110
111
  # :call-seq:
111
112
  # Document.open(filename, **docargs) -> doc
@@ -184,22 +185,9 @@ module HexaPDF
184
185
  # For references to unknown objects, +nil+ is returned but free objects are represented by a
185
186
  # PDF Null object, not by +nil+!
186
187
  #
187
- # See: PDF1.7 s7.3.9
188
+ # See: Revisions#object
188
189
  def object(ref)
189
- i = @revisions.size - 1
190
- while i >= 0
191
- return @revisions[i].object(ref) if @revisions[i].object?(ref)
192
- i -= 1
193
- end
194
- nil
195
- end
196
-
197
- # Dereferences the given object.
198
- #
199
- # Return the object itself if it is not a reference, or the indirect object specified by the
200
- # reference.
201
- def deref(obj)
202
- obj.kind_of?(Reference) ? object(obj) : obj
190
+ @revisions.object(ref)
203
191
  end
204
192
 
205
193
  # :call-seq:
@@ -212,74 +200,51 @@ module HexaPDF
212
200
  # Even though this method might return +true+ for some references, #object may return +nil+
213
201
  # because this method takes *all* revisions into account. Also see the discussion on #each for
214
202
  # more information.
203
+ #
204
+ # See: Revisions#object?
215
205
  def object?(ref)
216
- @revisions.any? {|rev| rev.object?(ref) }
206
+ @revisions.object?(ref)
207
+ end
208
+
209
+ # Dereferences the given object.
210
+ #
211
+ # Return the object itself if it is not a reference, or the indirect object specified by the
212
+ # reference.
213
+ def deref(obj)
214
+ obj.kind_of?(Reference) ? object(obj) : obj
217
215
  end
218
216
 
219
217
  # :call-seq:
220
- # doc.add(obj, revision: :current, **wrap_opts) -> indirect_object
218
+ # doc.add(obj, **wrap_opts) -> indirect_object
221
219
  #
222
- # Adds the object to the specified revision of the document and returns the wrapped indirect
223
- # object.
220
+ # Adds the object to the document and returns the wrapped indirect object.
224
221
  #
225
222
  # The object can either be a native Ruby object (Hash, Array, Integer, ...) or a
226
223
  # HexaPDF::Object. If it is not the latter, #wrap is called with the object and the
227
224
  # additional keyword arguments.
228
225
  #
229
- # If the +revision+ option is +:current+, the current revision is used. Otherwise +revision+
230
- # should be a revision index.
231
- def add(obj, revision: :current, **wrap_opts)
226
+ # See: Revisions#add_object
227
+ def add(obj, **wrap_opts)
232
228
  obj = wrap(obj, **wrap_opts) unless obj.kind_of?(HexaPDF::Object)
233
229
 
234
- revision = (revision == :current ? @revisions.current : @revisions.revision(revision))
235
- if revision.nil?
236
- raise ArgumentError, "Invalid revision index specified"
237
- end
238
-
239
230
  if obj.document? && obj.document != self
240
231
  raise HexaPDF::Error, "Can't add object that is already attached to another document"
241
232
  end
242
233
  obj.document = self
243
234
 
244
- if obj.indirect? && (rev_obj = revision.object(obj.oid))
245
- if rev_obj.equal?(obj)
246
- return obj
247
- else
248
- raise HexaPDF::Error, "Can't add object because the specified revision already has " \
249
- "an object with object number #{obj.oid}"
250
- end
251
- end
252
-
253
- obj.oid = @revisions.map(&:next_free_oid).max unless obj.indirect?
254
-
255
- revision.add(obj)
235
+ @revisions.add_object(obj)
256
236
  end
257
237
 
258
238
  # :call-seq:
259
- # doc.delete(ref, revision: :all)
260
- # doc.delete(oid, revision: :all)
239
+ # doc.delete(ref)
240
+ # doc.delete(oid)
261
241
  #
262
242
  # Deletes the indirect object specified by an exact reference or by an object number from the
263
243
  # document.
264
244
  #
265
- # Options:
266
- #
267
- # revision:: Specifies from which revisions the object should be deleted:
268
- #
269
- # :all:: Delete the object from all revisions.
270
- # :current:: Delete the object only from the current revision.
271
- #
272
- # mark_as_free:: If +true+, objects are only marked as free objects instead of being actually
273
- # deleted.
274
- def delete(ref, revision: :all, mark_as_free: true)
275
- case revision
276
- when :current
277
- @revisions.current.delete(ref, mark_as_free: mark_as_free)
278
- when :all
279
- @revisions.each {|rev| rev.delete(ref, mark_as_free: mark_as_free) }
280
- else
281
- raise ArgumentError, "Unsupported option revision: #{revision}"
282
- end
245
+ # See: Revisions#delete_object
246
+ def delete(ref)
247
+ @revisions.delete_object(ref)
283
248
  end
284
249
 
285
250
  # :call-seq:
@@ -414,42 +379,20 @@ module HexaPDF
414
379
  end
415
380
 
416
381
  # :call-seq:
417
- # doc.each(only_current: true, only_loaded: false) {|obj| block } -> doc
418
- # doc.each(only_current: true, only_loaded: false) {|obj, rev| block } -> doc
382
+ # doc.each(only_current: true, only_loaded: false) {|obj| block }
383
+ # doc.each(only_current: true, only_loaded: false) {|obj, rev| block }
419
384
  # doc.each(only_current: true, only_loaded: false) -> Enumerator
420
385
  #
421
- # Calls the given block once for every object, or, if +only_loaded+ is +true+, for every loaded
422
- # object in the PDF document. The block may either accept only the object or the object and the
423
- # revision it is in.
424
- #
425
- # By default, only the current version of each object is returned which implies that each object
426
- # number is yielded exactly once. If the +only_current+ option is +false+, all stored objects
427
- # from newest to oldest are returned, not only the current version of each object.
386
+ # Yields every object and the revision it is in.
428
387
  #
429
- # The +only_current+ option can make a difference because the document can contain multiple
430
- # revisions:
388
+ # If +only_current+ is +true+, only the current version of each object is yielded, otherwise
389
+ # all objects from all revisions.
431
390
  #
432
- # * Multiple revisions may contain objects with the same object and generation numbers, e.g.
433
- # two (different) objects with oid/gen [3,0].
391
+ # If +only_loaded+ is +true+, only the already loaded objects are yielded.
434
392
  #
435
- # * Additionally, there may also be objects with the same object number but different
436
- # generation numbers in different revisions, e.g. one object with oid/gen [3,0] and one with
437
- # oid/gen [3,1].
393
+ # For details see Revisions#each_object
438
394
  def each(only_current: true, only_loaded: false, &block)
439
- unless block_given?
440
- return to_enum(__method__, only_current: only_current, only_loaded: only_loaded)
441
- end
442
-
443
- yield_rev = (block.arity == 2)
444
- oids = {}
445
- @revisions.reverse_each do |rev|
446
- rev.each(only_loaded: only_loaded) do |obj|
447
- next if only_current && oids.include?(obj.oid)
448
- (yield_rev ? yield(obj, rev) : yield(obj))
449
- oids[obj.oid] = true
450
- end
451
- end
452
- self
395
+ @revisions.each_object(only_current: only_current, only_loaded: only_loaded, &block)
453
396
  end
454
397
 
455
398
  # :call-seq:
@@ -529,6 +472,12 @@ module HexaPDF
529
472
  @fonts ||= Fonts.new(self)
530
473
  end
531
474
 
475
+ # Returns the Destinations object that provides convenience methods for working with destination
476
+ # objects.
477
+ def destinations
478
+ @destinations ||= Destinations.new(self)
479
+ end
480
+
532
481
  # Returns the main AcroForm object for dealing with interactive forms.
533
482
  #
534
483
  # See HexaPDF::Type::Catalog#acro_form for details on the arguments.
@@ -252,14 +252,6 @@ module HexaPDF
252
252
  else
253
253
  create_rectangle(x, y, x + width, y + height)
254
254
  end
255
- when :float
256
- x = @x + @fit_data.margin_left
257
- x += @fit_data.available_width - width if box.style.position_hint == :right
258
- y = @y - height - @fit_data.margin_top
259
- # We use the real margins from the box because they either have the desired effect or just
260
- # extend the rectangle outside the frame.
261
- rectangle = create_rectangle(x - (margin&.left || 0), y - (margin&.bottom || 0),
262
- x + width + (margin&.right || 0), @y)
263
255
  when :flow
264
256
  x = 0
265
257
  y = @y - height
@@ -280,7 +272,14 @@ module HexaPDF
280
272
  @x + @fit_data.margin_left
281
273
  end
282
274
  y = @y - height - @fit_data.margin_top
283
- rectangle = create_rectangle(left, y - (margin&.bottom || 0), left + self.width, @y)
275
+ rectangle = if box.style.position == :float
276
+ # We use the real margins from the box because they either have the desired
277
+ # effect or just extend the rectangle outside the frame.
278
+ create_rectangle(x - (margin&.left || 0), y - (margin&.bottom || 0),
279
+ x + width + (margin&.right || 0), @y)
280
+ else
281
+ create_rectangle(left, y - (margin&.bottom || 0), left + self.width, @y)
282
+ end
284
283
  end
285
284
 
286
285
  box.draw(canvas, x, y)