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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1388a344a539c7273603549e014c635c4864af4de8665a260b15ac66d19ac461
4
- data.tar.gz: 471e0f4933ac37348ac5ed217446031e742099de26773b25bb23c49fc8480a05
3
+ metadata.gz: d6f11a38da2472389966c1d103b86e46ba8cd858d2927b1a65f441f56e2e7dd5
4
+ data.tar.gz: 74ff92dcb6ede9f137303afc7c8b9d08e0baa42b0ab0b7d4436777e6db617ea4
5
5
  SHA512:
6
- metadata.gz: 0ab8c16c85475e68f12faea47f8dbf7e167ebdc864d11fb1151e107af1d3678b867ad0d7a6184155e1b55ed1469d3e7443f39184e0250974f5a7df9a920adb99
7
- data.tar.gz: 43c80b555018068baf314d6ed7d6a03ae0a359f2e8be498341985ecb75879fd414d066d7c356b47fb896ec0e4aa74c2044f6a5e7965922de1136ddacf409765f
6
+ metadata.gz: 468e0a5a24e06574b4a802408daafbdb59b3a1dc460ad2b6bf9f543088d1210d8295b843847819a872436ea4c380df85240822e28b548dd88b55f74b30ff8ab3
7
+ data.tar.gz: 29e95def1e7ccf0d55bca3cf5ad4d95a070d5910a0e1b8645d2884e0e5022367c63e14e2df3ecc0bac7cdfc329d88698c01060dc8005e1c1025a0c8337367afc
data/CHANGELOG.md CHANGED
@@ -1,3 +1,53 @@
1
+ ## 0.23.0 - 2022-05-26
2
+
3
+ ### Added
4
+
5
+ - [HexaPDF::Composer#create_stamp] for creating a form Xobject
6
+ - [HexaPDF::Revision#reset_objects] for deleting all live loaded and added
7
+ objects
8
+ - Support for removing or flattening annotations to the `hexapdf modify` command
9
+ - Option to CLI command `hexapdf form` to allow generation of a template file
10
+ - Support for centering a floating box in [HexaPDF::Layout::Frame]
11
+ - [HexaPDF::Type::Catalog#names] for easier access to the name dictionary
12
+ - [HexaPDF::Type::Names#destinations] for easier access to the destinations name
13
+ tree
14
+ - [HexaPDF::Document::Destinations], accessible via
15
+ [HexaPDF::Document#destinations], as convenience interface for working with
16
+ destination arrays
17
+
18
+ ### Changed
19
+
20
+ - **Breaking change**: Refactored the [HexaPDF::Document] interface for working
21
+ with objects and move parts into [HexaPDF::Revisions]
22
+ - **Breaking change**: [HexaPDF::Layout::TextBox] to use whole available width
23
+ when aligning to the center or right
24
+ - **Breaking change**: [HexaPDF::Layout::TextBox] to use whole available height
25
+ when vertically aligning to the center or bottom
26
+ - CLI command `hexapdf inspect` to show the type of revisions, as well as the
27
+ number of objects per revision
28
+ - [HexaPDF::Task::Optimize] to allow skipping invalid content stream operations
29
+ - [HexaPDF::Composer#image] to allow using a form xobject in place of the image
30
+
31
+ ### Fixed
32
+
33
+ - [HexaPDF::Writer#write] to write modified objects into the correct revision
34
+ - [HexaPDF::Revisions::from_io] to correctly handle hybrid-reference files
35
+ - [HexaPDF::Writer] to assign a valid object number to a created cross-reference
36
+ stream in all cases
37
+ * [HexaPDF::Type::AcroForm::TextField] to validate the existence of a /MaxLen
38
+ value for comb text fields
39
+ * [HexaPDF::Type::AcroForm::TextField#field_value=] to check for the existence
40
+ of /MaxLen when setting a value for a comb text field
41
+ * [HexaPDF::Type::AcroForm::TextField#field_value=] to check the value against
42
+ /MaxLen
43
+ * [HexaPDF::Layout::TextLayouter#fit] to not use style valign when doing
44
+ variable width layouting
45
+ * [HexaPDF::Utils::SortedTreeNode#find_entry] to work in case of a node without
46
+ a container name or kids key
47
+ * CLI command `hexapdf form` to allow setting array values when using a template
48
+ * CLI command `hexapdf form` to allow setting file select fields
49
+
50
+
1
51
  ## 0.22.0 - 2022-03-26
2
52
 
3
53
  ### Added
@@ -70,6 +70,9 @@ module HexaPDF
70
70
  @template = template
71
71
  @fill = true
72
72
  end
73
+ options.on('--generate-template', 'Print a template for use with --template') do
74
+ @generate_template = true
75
+ end
73
76
  options.on('--flatten', 'Flatten the form fields') do
74
77
  @flatten = true
75
78
  end
@@ -85,6 +88,7 @@ module HexaPDF
85
88
  @password = nil
86
89
  @fill = false
87
90
  @flatten = false
91
+ @generate_template = false
88
92
  @template = nil
89
93
  @need_appearances = nil
90
94
  @incremental = true
@@ -117,6 +121,15 @@ module HexaPDF
117
121
  doc.catalog.delete(:AcroForm)
118
122
  doc.delete(doc.acro_form)
119
123
  end
124
+ elsif @generate_template
125
+ unsupported_fields = [:signature_field, :password_field]
126
+ each_field(doc) do |_, _, field, _|
127
+ next if unsupported_fields.include?(field.concrete_field_type)
128
+ name = field.full_field_name.gsub(':', "\\:")
129
+ Array(field.field_value).each do |val|
130
+ puts "#{name}: #{val.to_s.gsub(/(\r|\r\n|\n)/, '\1 ')}"
131
+ end
132
+ end
120
133
  else
121
134
  list_form_fields(doc)
122
135
  end
@@ -220,8 +233,16 @@ module HexaPDF
220
233
  field_name = scanner.scan(/(\\:|[^:])*?:/)
221
234
  break unless field_name
222
235
  field_name.gsub!(/\\:/, ':')
236
+ field_name.chop!
223
237
  field_value = scanner.scan(/.*?(?=^\S|\z)/m)
224
- data[field_name.chop] = field_value.strip.gsub(/^\s*/, '') if field_value
238
+ next unless field_value
239
+ field_value = field_value.strip.gsub(/^\s*/, '')
240
+ if data.key?(field_name)
241
+ data[field_name] = [data[field_name]] unless data[field_name].kind_of?(Array)
242
+ data[field_name] << field_value
243
+ else
244
+ data[field_name] = field_value
245
+ end
225
246
  end
226
247
  if !scanner.eos? && command_parser.verbosity_warning?
227
248
  $stderr.puts "Warning: Some template could not be parsed"
@@ -232,8 +253,8 @@ module HexaPDF
232
253
  # Applies the given value to the field.
233
254
  def apply_field_value(field, value)
234
255
  case field.concrete_field_type
235
- when :single_line_text_field, :multiline_text_field, :comb_text_field, :combo_box,
236
- :list_box, :editable_combo_box
256
+ when :single_line_text_field, :multiline_text_field, :comb_text_field, :file_select_field,
257
+ :combo_box, :list_box, :editable_combo_box
237
258
  field.field_value = value
238
259
  when :check_box
239
260
  field.field_value = case value
@@ -249,6 +270,8 @@ module HexaPDF
249
270
  else
250
271
  raise "Field type #{field.concrete_field_type} not yet supported"
251
272
  end
273
+ rescue
274
+ raise "Error while setting '#{field.full_field_name}': #{$!.message}"
252
275
  end
253
276
 
254
277
  # Iterates over all non-push button fields in page order. If a field appears on multiple
@@ -229,10 +229,19 @@ module HexaPDF
229
229
  end
230
230
  IO.copy_stream(@doc.revisions.parser.io, $stdout, length, 0)
231
231
  else
232
- puts "Document has #{@doc.revisions.size} revision#{@doc.revisions.size == 1 ? '' : 's'}"
233
- revision_information do |_, index, count, signature, end_offset|
232
+ puts "Document has #{@doc.revisions.count} revision#{@doc.revisions.count == 1 ? '' : 's'}"
233
+ revision_information do |rev, index, count, signature, end_offset|
234
+ type = if rev.trailer[:XRefStm]
235
+ "xref table + stream"
236
+ elsif rev.trailer[:Type] == :XRef
237
+ "xref stream"
238
+ else
239
+ "xref table"
240
+ end
234
241
  puts "Revision #{index + 1}"
242
+ puts " Type : #{type}"
235
243
  puts " Objects : #{count}"
244
+ puts " Size : #{rev.trailer[:Size]}"
236
245
  puts " Signed : yes" if signature
237
246
  puts " Byte range: 0-#{end_offset}"
238
247
  end
@@ -352,7 +361,7 @@ module HexaPDF
352
361
  buffer = buffer[-20..-1]
353
362
  end
354
363
  end
355
- yield(rev, index, rev.next_free_oid - 1, sig, end_index)
364
+ yield(rev, index, rev.each.count, sig, end_index)
356
365
  end
357
366
  end
358
367
 
@@ -53,14 +53,15 @@ module HexaPDF
53
53
  super('modify', takes_commands: false)
54
54
  short_desc("Modify a PDF file")
55
55
  long_desc(<<~EOF)
56
- This command modifies a PDF file. It can be used to select pages that should appear in
57
- the output file and/or rotate them. The output file can also be encrypted/decrypted and
58
- optimized in various ways.
56
+ This command modifies a PDF file. It can be used, for example, to select pages that should
57
+ appear in the output file and/or rotate them. The output file can also be
58
+ encrypted/decrypted and optimized in various ways.
59
59
  EOF
60
60
 
61
61
  @password = nil
62
62
  @pages = '1-e'
63
63
  @embed_files = []
64
+ @annotation_mode = nil
64
65
 
65
66
  options.on("--password PASSWORD", "-p", String,
66
67
  "The password for decryption. Use - for reading from standard input.") do |pwd|
@@ -74,6 +75,10 @@ module HexaPDF
74
75
  "used multiple times)") do |file|
75
76
  @embed_files << file
76
77
  end
78
+ options.on("--annotations MODE", [:remove, :flatten], "Handling of annotations (either " \
79
+ "remove or flatten)") do |mode|
80
+ @annotation_mode = mode
81
+ end
77
82
  define_optimization_options
78
83
  define_encryption_options
79
84
  end
@@ -82,6 +87,7 @@ module HexaPDF
82
87
  maybe_raise_on_existing_file(out_file)
83
88
  with_document(in_file, password: @password, out_file: out_file) do |doc|
84
89
  arrange_pages(doc) unless @pages == '1-e'
90
+ handle_annotations(doc)
85
91
  @embed_files.each {|file| doc.files.add(file, embed: true) }
86
92
  apply_encryption_options(doc)
87
93
  apply_optimization_options(doc)
@@ -109,6 +115,20 @@ module HexaPDF
109
115
  doc.pages.add unless doc.pages.count > 0
110
116
  end
111
117
 
118
+ # Handles the annotations of all selected pages by doing nothing, removing them or flattening
119
+ # them.
120
+ def handle_annotations(doc)
121
+ return unless @annotation_mode
122
+
123
+ doc.pages.each do |page|
124
+ if @annotation_mode == :remove
125
+ page.delete(:Annots)
126
+ else
127
+ page.flatten_annotations
128
+ end
129
+ end
130
+ end
131
+
112
132
  end
113
133
 
114
134
  end
@@ -313,7 +313,8 @@ module HexaPDF
313
313
 
314
314
  # Draws the given image at the current position.
315
315
  #
316
- # The +file+ argument can be anything that is accepted by HexaPDF::Document::Images#add.
316
+ # The +file+ argument can be anything that is accepted by HexaPDF::Document::Images#add or a
317
+ # HexaPDF::Type::Form object.
317
318
  #
318
319
  # See #text for details on +width+, +height+, +style+ and +style_properties+.
319
320
  #
@@ -324,7 +325,7 @@ module HexaPDF
324
325
  # composer.image(machu_picchu, height: 30)
325
326
  def image(file, width: 0, height: 0, style: nil, **style_properties)
326
327
  style = retrieve_style(style, style_properties)
327
- image = document.images.add(file)
328
+ image = file.kind_of?(HexaPDF::Stream) ? file : document.images.add(file)
328
329
  draw_box(Layout::ImageBox.new(image, width: width, height: height, style: style))
329
330
  end
330
331
 
@@ -361,6 +362,27 @@ module HexaPDF
361
362
  end
362
363
  end
363
364
 
365
+ # Creates a stamp (Form XObject) which can be used like an image multiple times on a single page
366
+ # or on multiple pages.
367
+ #
368
+ # The width and the height of the stamp need to be set (frame.width/height or
369
+ # page.box.width/height might be good choices).
370
+ #
371
+ # Examples:
372
+ #
373
+ # #>pdf-composer
374
+ # stamp = composer.create_stamp(50, 50) do |canvas|
375
+ # canvas.fill_color("red").line_width(5).
376
+ # rectangle(10, 10, 30, 30).fill_stroke
377
+ # end
378
+ # composer.image(stamp, width: 20, height: 20)
379
+ # composer.image(stamp, width: 50)
380
+ def create_stamp(width, height) # :yield: canvas
381
+ stamp = @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height]})
382
+ yield(stamp.canvas) if block_given?
383
+ stamp
384
+ end
385
+
364
386
  private
365
387
 
366
388
  # Creates the frame into which boxes are layed out when a new page is created.
@@ -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