hexapdf 0.22.0 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
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)