hexapdf 0.21.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -1
  3. data/Rakefile +1 -1
  4. data/lib/hexapdf/cli/form.rb +30 -3
  5. data/lib/hexapdf/cli/inspect.rb +18 -5
  6. data/lib/hexapdf/cli/modify.rb +23 -3
  7. data/lib/hexapdf/composer.rb +24 -2
  8. data/lib/hexapdf/dictionary_fields.rb +1 -1
  9. data/lib/hexapdf/document/destinations.rb +396 -0
  10. data/lib/hexapdf/document.rb +38 -89
  11. data/lib/hexapdf/encryption/aes.rb +9 -5
  12. data/lib/hexapdf/layout/frame.rb +8 -9
  13. data/lib/hexapdf/layout/style.rb +280 -7
  14. data/lib/hexapdf/layout/text_box.rb +10 -2
  15. data/lib/hexapdf/layout/text_layouter.rb +6 -1
  16. data/lib/hexapdf/revision.rb +8 -1
  17. data/lib/hexapdf/revisions.rb +151 -50
  18. data/lib/hexapdf/task/optimize.rb +21 -11
  19. data/lib/hexapdf/type/acro_form/form.rb +11 -5
  20. data/lib/hexapdf/type/acro_form/text_field.rb +8 -0
  21. data/lib/hexapdf/type/catalog.rb +9 -1
  22. data/lib/hexapdf/type/image.rb +47 -3
  23. data/lib/hexapdf/type/names.rb +13 -0
  24. data/lib/hexapdf/type/xref_stream.rb +2 -1
  25. data/lib/hexapdf/utils/sorted_tree_node.rb +3 -1
  26. data/lib/hexapdf/version.rb +1 -1
  27. data/lib/hexapdf/writer.rb +15 -2
  28. data/test/hexapdf/document/test_destinations.rb +338 -0
  29. data/test/hexapdf/encryption/test_aes.rb +8 -0
  30. data/test/hexapdf/encryption/test_security_handler.rb +2 -2
  31. data/test/hexapdf/layout/test_frame.rb +15 -1
  32. data/test/hexapdf/layout/test_text_box.rb +16 -0
  33. data/test/hexapdf/layout/test_text_layouter.rb +7 -0
  34. data/test/hexapdf/task/test_optimize.rb +17 -4
  35. data/test/hexapdf/test_composer.rb +24 -1
  36. data/test/hexapdf/test_dictionary_fields.rb +1 -1
  37. data/test/hexapdf/test_document.rb +30 -133
  38. data/test/hexapdf/test_parser.rb +1 -1
  39. data/test/hexapdf/test_revision.rb +14 -0
  40. data/test/hexapdf/test_revisions.rb +137 -29
  41. data/test/hexapdf/test_writer.rb +43 -14
  42. data/test/hexapdf/type/acro_form/test_form.rb +2 -1
  43. data/test/hexapdf/type/acro_form/test_text_field.rb +17 -0
  44. data/test/hexapdf/type/test_catalog.rb +8 -0
  45. data/test/hexapdf/type/test_image.rb +45 -9
  46. data/test/hexapdf/type/test_names.rb +20 -0
  47. data/test/hexapdf/type/test_xref_stream.rb +2 -1
  48. data/test/hexapdf/utils/test_sorted_tree_node.rb +11 -1
  49. metadata +6 -3
@@ -0,0 +1,396 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2022 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ require 'hexapdf/dictionary'
38
+ require 'hexapdf/error'
39
+
40
+ module HexaPDF
41
+ class Document
42
+
43
+ # This class provides methods for creating and managing the destinations of a PDF file.
44
+ #
45
+ # A destination describes a particular view of a PDF document, consisting of the page, the view
46
+ # location and a magnification factor. See Destination for details.
47
+ #
48
+ # Such destinations may be directly specified where needed, e.g. for link annotations, or they
49
+ # may be named and later referenced through the name. This class allows to create destinations
50
+ # with or without a name.
51
+ #
52
+ # See: PDF1.7 s12.3.2
53
+ class Destinations
54
+
55
+ # Wraps an explicit destination array to allow easy access to query its properties.
56
+ #
57
+ # A *destination array* has the form
58
+ #
59
+ # [page, type, *arguments]
60
+ #
61
+ # where +page+ is either a page object or a page number (in case of a destination to a page in
62
+ # a remote PDF document), +type+ is the destination type (see below) and +arguments+ are the
63
+ # required arguments for the specific type of destination.
64
+ #
65
+ # == Destination Types
66
+ #
67
+ # There are eight different types of destinations, each taking different arguments. The
68
+ # arguments are marked up in the list below and are in the correct order for use in the
69
+ # destination array. The first name in the list is the PDF internal name, the second one the
70
+ # explicit, more descriptive one used by HexaPDF:
71
+ #
72
+ # :XYZ, :xyz::
73
+ # Display the page with the given (+left+, +top+) coordinate at the upper-left corner of
74
+ # the window and the specified magnification (+zoom+) factor. A +nil+ value for any
75
+ # argument means not changing it from the current value.
76
+ #
77
+ # :Fit, :fit_page::
78
+ # Display the page so that it fits horizontally and vertically within the window.
79
+ #
80
+ # :FitH, :fit_page_horizontal::
81
+ # Display the page so that it fits horizontally within the window, with the given +top+
82
+ # coordinate being at the top of the window. A +nil+ value for +top+ means not changing it
83
+ # from the current value.
84
+ #
85
+ # :FitV, :fit_page_vertical::
86
+ # Display the page so that it fits vertically within the window, with the given +left+
87
+ # coordinate being at the left of the window. A +nil+ value for +left+ means not changing
88
+ # it from the current value.
89
+ #
90
+ # :FitR, :fit_rectangle::
91
+ # Display the page so that the rectangle specified by (+left+, +bottom+)-(+right+, +top+)
92
+ # fits horizontally and vertically within the window.
93
+ #
94
+ # :FitB, :fit_bounding_box::
95
+ # Display the page so that its bounding box fits horizontally and vertically within the
96
+ # window.
97
+ #
98
+ # :FitBH, :fit_bounding_box_horizontal::
99
+ # Display the page so that its bounding box fits horizontally within the window, with the
100
+ # given +top+ coordinate being at the top of the window. A +nil+ value for +top+ means not
101
+ # changing it from the current value.
102
+ #
103
+ # :FitBV, :fit_bounding_box_vertical::
104
+ # Display the page so that its bounding box fits vertically within the window, with the
105
+ # given +left+ coordinate being at the left of the window. A +nil+ value for +left+ means
106
+ # not changing it from the current value.
107
+ class Destination
108
+
109
+ # :nodoc:
110
+ TYPE_MAPPING = {
111
+ XYZ: :xyz,
112
+ Fit: :fit_page,
113
+ FitH: :fit_page_horizontal,
114
+ FitV: :fit_page_vertical,
115
+ FitR: :fit_rectangle,
116
+ FitB: :fit_bounding_box,
117
+ FitBH: :fit_bounding_box_horizontal,
118
+ FitBV: :fit_bounding_box_vertical,
119
+ }
120
+
121
+ # :nodoc:
122
+ REVERSE_TYPE_MAPPING = Hash[*TYPE_MAPPING.flatten.reverse]
123
+
124
+ # Creates a new Destination for the given +destination+ which may be an explicit destination
125
+ # array or a dictionary with a /D entry (as allowed for a named destination).
126
+ def initialize(destination)
127
+ @destination = (destination.kind_of?(HexaPDF::Dictionary) ? destination[:D] : destination)
128
+ end
129
+
130
+ # Returns +true+ if the destination references a destination in a remote document.
131
+ def remote?
132
+ @destination[0].kind_of?(Numeric)
133
+ end
134
+
135
+ # Returns the referenced page.
136
+ #
137
+ # The return value is either a page object or, in case of a destination to a remote
138
+ # document, a page number.
139
+ def page
140
+ @destination[0]
141
+ end
142
+
143
+ # Returns the type of destination.
144
+ def type
145
+ TYPE_MAPPING[@destination[1]]
146
+ end
147
+
148
+ # Returns the argument +left+ if used by the destination, raises an error otherwise.
149
+ def left
150
+ case type
151
+ when :xyz, :fit_page_vertical, :fit_rectangle, :fit_bounding_box_vertical
152
+ @destination[2]
153
+ else
154
+ raise HexaPDF::Error, "No such argument for destination type #{type}"
155
+ end
156
+ end
157
+
158
+ # Returns the argument +top+ if used by the destination, raises an error otherwise.
159
+ def top
160
+ case type
161
+ when :xyz
162
+ @destination[3]
163
+ when :fit_page_horizontal, :fit_bounding_box_horizontal
164
+ @destination[2]
165
+ when :fit_rectangle
166
+ @destination[5]
167
+ else
168
+ raise HexaPDF::Error, "No such argument for destination type #{type}"
169
+ end
170
+ end
171
+
172
+ # Returns the argument +right+ if used by the destination, raises an error otherwise.
173
+ def right
174
+ case type
175
+ when :fit_rectangle
176
+ @destination[4]
177
+ else
178
+ raise HexaPDF::Error, "No such argument for destination type #{type}"
179
+ end
180
+ end
181
+
182
+ # Returns the argument +bottom+ if used by the destination, raises an error otherwise.
183
+ def bottom
184
+ case type
185
+ when :fit_rectangle
186
+ @destination[3]
187
+ else
188
+ raise HexaPDF::Error, "No such argument for destination type #{type}"
189
+ end
190
+ end
191
+
192
+ # Returns the argument +zoom+ if used by the destination, raises an error otherwise.
193
+ def zoom
194
+ case type
195
+ when :xyz
196
+ @destination[4]
197
+ else
198
+ raise HexaPDF::Error, "No such argument for destination type #{type}"
199
+ end
200
+ end
201
+
202
+ end
203
+
204
+ include Enumerable
205
+
206
+ # Creates a new Destinations object for the given PDF document.
207
+ def initialize(document)
208
+ @document = document
209
+ end
210
+
211
+ # :call-seq:
212
+ # destinations.create_xyz(page, left: nil, top: nil, zoom: nil) -> dest
213
+ # destinations.create_xyz(page, name: nil, left: nil, top: nil, zoom: nil) -> name
214
+ #
215
+ # Creates a new xyz destination array for the given arguments and returns it or, in case
216
+ # a name is given, the name.
217
+ #
218
+ # The arguments +page+, +left+, +top+ and +zoom+ are described in detail in the Destination
219
+ # class description.
220
+ #
221
+ # If the argument +name+ is given, the created destination array is added to the destinations
222
+ # name tree under that name for reuse later, overwriting an existing entry if there is one.
223
+ def create_xyz(page, name: nil, left: nil, top: nil, zoom: nil)
224
+ destination = [page, Destination::REVERSE_TYPE_MAPPING.fetch(:xyz), left, top, zoom]
225
+ name ? (add(name, destination); name) : destination
226
+ end
227
+
228
+ # :call-seq:
229
+ # destinations.create_fit_page(page) -> dest
230
+ # destinations.create_fit_page(page, name: nil) -> name
231
+ #
232
+ # Creates a new fit to page destination array for the given arguments and returns it or, in
233
+ # case a name is given, the name.
234
+ #
235
+ # The argument +page+ is described in detail in the Destination class description.
236
+ #
237
+ # If the argument +name+ is given, the created destination array is added to the destinations
238
+ # name tree under that name for reuse later, overwriting an existing entry if there is one.
239
+ def create_fit_page(page, name: nil)
240
+ destination = [page, Destination::REVERSE_TYPE_MAPPING.fetch(:fit_page)]
241
+ name ? (add(name, destination); name) : destination
242
+ end
243
+
244
+ # :call-seq:
245
+ # destinations.create_fit_page_horizontal(page, top: nil) -> dest
246
+ # destinations.create_fit_page_horizontal(page, name: nil, top: nil) -> name
247
+ #
248
+ # Creates a new fit page horizontal destination array for the given arguments and returns it
249
+ # or, in case a name is given, the name.
250
+ #
251
+ # The arguments +page and +top+ are described in detail in the Destination class description.
252
+ #
253
+ # If the argument +name+ is given, the created destination array is added to the destinations
254
+ # name tree under that name for reuse later, overwriting an existing entry if there is one.
255
+ def create_fit_page_horizontal(page, name: nil, top: nil)
256
+ destination = [page, Destination::REVERSE_TYPE_MAPPING.fetch(:fit_page_horizontal), top]
257
+ name ? (add(name, destination); name) : destination
258
+ end
259
+
260
+ # :call-seq:
261
+ # destinations.create_fit_page_vertical(page, left: nil) -> dest
262
+ # destinations.create_fit_page_vertical(page, name: nil, left: nil) -> name
263
+ #
264
+ # Creates a new fit page vertical destination array for the given arguments and returns it or,
265
+ # in case a name is given, the name.
266
+ #
267
+ # The arguments +page and +left+ are described in detail in the Destination class description.
268
+ #
269
+ # If the argument +name+ is given, the created destination array is added to the destinations
270
+ # name tree under that name for reuse later, overwriting an existing entry if there is one.
271
+ def create_fit_page_vertical(page, name: nil, left: nil)
272
+ destination = [page, Destination::REVERSE_TYPE_MAPPING.fetch(:fit_page_vertical), left]
273
+ name ? (add(name, destination); name) : destination
274
+ end
275
+
276
+ # :call-seq:
277
+ # destinations.create_fit_rectangle(page, left:, bottom:, right:, top:) -> dest
278
+ # destinations.create_fit_rectangle(page, name: nil, left:, bottom:, right:, top:) -> name
279
+ #
280
+ # Creates a new fit to rectangle destination array for the given arguments and returns it or,
281
+ # in case a name is given, the name.
282
+ #
283
+ # The arguments +page+, +left+, +bottom+, +right+ and +top+ are described in detail in the
284
+ # Destination class description.
285
+ #
286
+ # If the argument +name+ is given, the created destination array is added to the destinations
287
+ # name tree under that name for reuse later, overwriting an existing entry if there is one.
288
+ def create_fit_rectangle(page, left:, bottom:, right:, top:, name: nil)
289
+ destination = [page, Destination::REVERSE_TYPE_MAPPING.fetch(:fit_rectangle),
290
+ left, bottom, right, top]
291
+ name ? (add(name, destination); name) : destination
292
+ end
293
+
294
+ # :call-seq:
295
+ # destinations.create_fit_bounding_box(page) -> dest
296
+ # destinations.create_fit_bounding_box(page, name: nil) -> name
297
+ #
298
+ # Creates a new fit to bounding box destination array for the given arguments and returns it
299
+ # or, in case a name is given, the name.
300
+ #
301
+ # The argument +page+ is described in detail in the Destination class description.
302
+ #
303
+ # If the argument +name+ is given, the created destination array is added to the destinations
304
+ # name tree under that name for reuse later, overwriting an existing entry if there is one.
305
+ def create_fit_bounding_box(page, name: nil)
306
+ destination = [page, Destination::REVERSE_TYPE_MAPPING.fetch(:fit_bounding_box)]
307
+ name ? (add(name, destination); name) : destination
308
+ end
309
+
310
+ # :call-seq:
311
+ # destinations.create_fit_bounding_box_horizontal(page, top: nil) -> dest
312
+ # destinations.create_fit_bounding_box_horizontal(page, name: nil, top: nil) -> name
313
+ #
314
+ # Creates a new fit bounding box horizontal destination array for the given arguments and
315
+ # returns it or, in case a name is given, the name.
316
+ #
317
+ # The arguments +page and +top+ are described in detail in the Destination class description.
318
+ #
319
+ # If the argument +name+ is given, the created destination array is added to the destinations
320
+ # name tree under that name for reuse later, overwriting an existing entry if there is one.
321
+ def create_fit_bounding_box_horizontal(page, name: nil, top: nil)
322
+ destination = [page, Destination::REVERSE_TYPE_MAPPING.fetch(:fit_bounding_box_horizontal), top]
323
+ name ? (add(name, destination); name) : destination
324
+ end
325
+
326
+ # :call-seq:
327
+ # destinations.create_fit_bounding_box_vertical(page, left: nil) -> dest
328
+ # destinations.create_fit_bounding_box_vertical(page, name: nil, left: nil) -> name
329
+ #
330
+ # Creates a new fit bounding box vertical destination array for the given arguments and
331
+ # returns it or, in case a name is given, the name.
332
+ #
333
+ # The arguments +page and +left+ are described in detail in the Destination class description.
334
+ #
335
+ # If the argument +name+ is given, the created destination array is added to the destinations
336
+ # name tree under that name for reuse later, overwriting an existing entry if there is one.
337
+ def create_fit_bounding_box_vertical(page, name: nil, left: nil)
338
+ destination = [page, Destination::REVERSE_TYPE_MAPPING.fetch(:fit_bounding_box_vertical), left]
339
+ name ? (add(name, destination); name) : destination
340
+ end
341
+
342
+ # :call-seq:
343
+ # destinations.add(name, destination)
344
+ #
345
+ # Adds the given +destination+ under +name+ to the destinations name tree.
346
+ #
347
+ # If the name does already exist, an error is raised.
348
+ def add(name, destination)
349
+ destinations.add_entry(name, destination)
350
+ end
351
+
352
+ # :call-seq:
353
+ # destinations.delete(name) -> destination
354
+ #
355
+ # Deletes the given destination from the destinations name tree and returns it or +nil+ if no
356
+ # destination was registered under that name.
357
+ def delete(name)
358
+ destinations.delete_entry(name)
359
+ end
360
+
361
+ # :call-seq:
362
+ # destinations[name] -> destination
363
+ #
364
+ # Returns the destination registered under the given +name+ or +nil+ if no destination was
365
+ # registered under that name.
366
+ def [](name)
367
+ destinations.find_entry(name)
368
+ end
369
+
370
+ # :call-seq:
371
+ # destinations.each {|name, dest| block } -> destinations
372
+ # destinations.each -> Enumerator
373
+ #
374
+ # Iterates over all named destinations of the PDF, yielding the name and the destination
375
+ # wrapped into a Destination object.
376
+ def each
377
+ return to_enum(__method__) unless block_given?
378
+
379
+ destinations.each_entry do |name, dest|
380
+ yield(name, Destination.new(dest))
381
+ end
382
+
383
+ self
384
+ end
385
+
386
+ private
387
+
388
+ # Returns the root of the destinations name tree.
389
+ def destinations
390
+ @document.catalog.names.destinations
391
+ end
392
+
393
+ end
394
+
395
+ end
396
+ end
@@ -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.
@@ -114,10 +114,12 @@ module HexaPDF
114
114
  #
115
115
  # See: PDF1.7 s7.6.2.
116
116
  def decrypt(key, data)
117
- if data.length % BLOCK_SIZE != 0 || data.length < 2 * BLOCK_SIZE
117
+ if data.length % BLOCK_SIZE != 0 || data.length < BLOCK_SIZE
118
118
  raise HexaPDF::EncryptionError, "Invalid data for decryption, need 32 + 16*n bytes"
119
119
  end
120
- unpad(new(key, data.slice!(0, BLOCK_SIZE), :decrypt).process(data))
120
+ iv = data.slice!(0, BLOCK_SIZE)
121
+ # Handle invalid files with missing padding
122
+ data.empty? ? data : unpad(new(key, iv, :decrypt).process(data))
121
123
  end
122
124
 
123
125
  # Returns a Fiber object that decrypts the data from the given source fiber with the
@@ -140,11 +142,13 @@ module HexaPDF
140
142
  Fiber.yield(algorithm.process(new_data))
141
143
  end
142
144
 
143
- if data.length < BLOCK_SIZE || data.length % BLOCK_SIZE != 0
145
+ if data.length % BLOCK_SIZE != 0
144
146
  raise HexaPDF::EncryptionError, "Invalid data for decryption, need 32 + 16*n bytes"
147
+ elsif data.empty?
148
+ data # Handle invalid files with missing padding
149
+ else
150
+ unpad(algorithm.process(data))
145
151
  end
146
-
147
- unpad(algorithm.process(data))
148
152
  end
149
153
  end
150
154
 
@@ -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)