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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +79 -1
- data/Rakefile +1 -1
- data/lib/hexapdf/cli/form.rb +30 -3
- data/lib/hexapdf/cli/inspect.rb +18 -5
- data/lib/hexapdf/cli/modify.rb +23 -3
- data/lib/hexapdf/composer.rb +24 -2
- data/lib/hexapdf/dictionary_fields.rb +1 -1
- data/lib/hexapdf/document/destinations.rb +396 -0
- data/lib/hexapdf/document.rb +38 -89
- data/lib/hexapdf/encryption/aes.rb +9 -5
- data/lib/hexapdf/layout/frame.rb +8 -9
- data/lib/hexapdf/layout/style.rb +280 -7
- data/lib/hexapdf/layout/text_box.rb +10 -2
- data/lib/hexapdf/layout/text_layouter.rb +6 -1
- data/lib/hexapdf/revision.rb +8 -1
- data/lib/hexapdf/revisions.rb +151 -50
- data/lib/hexapdf/task/optimize.rb +21 -11
- data/lib/hexapdf/type/acro_form/form.rb +11 -5
- data/lib/hexapdf/type/acro_form/text_field.rb +8 -0
- data/lib/hexapdf/type/catalog.rb +9 -1
- data/lib/hexapdf/type/image.rb +47 -3
- data/lib/hexapdf/type/names.rb +13 -0
- data/lib/hexapdf/type/xref_stream.rb +2 -1
- data/lib/hexapdf/utils/sorted_tree_node.rb +3 -1
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +15 -2
- data/test/hexapdf/document/test_destinations.rb +338 -0
- data/test/hexapdf/encryption/test_aes.rb +8 -0
- data/test/hexapdf/encryption/test_security_handler.rb +2 -2
- data/test/hexapdf/layout/test_frame.rb +15 -1
- data/test/hexapdf/layout/test_text_box.rb +16 -0
- data/test/hexapdf/layout/test_text_layouter.rb +7 -0
- data/test/hexapdf/task/test_optimize.rb +17 -4
- data/test/hexapdf/test_composer.rb +24 -1
- data/test/hexapdf/test_dictionary_fields.rb +1 -1
- data/test/hexapdf/test_document.rb +30 -133
- data/test/hexapdf/test_parser.rb +1 -1
- data/test/hexapdf/test_revision.rb +14 -0
- data/test/hexapdf/test_revisions.rb +137 -29
- data/test/hexapdf/test_writer.rb +43 -14
- data/test/hexapdf/type/acro_form/test_form.rb +2 -1
- data/test/hexapdf/type/acro_form/test_text_field.rb +17 -0
- data/test/hexapdf/type/test_catalog.rb +8 -0
- data/test/hexapdf/type/test_image.rb +45 -9
- data/test/hexapdf/type/test_names.rb +20 -0
- data/test/hexapdf/type/test_xref_stream.rb +2 -1
- data/test/hexapdf/utils/test_sorted_tree_node.rb +11 -1
- 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
|
data/lib/hexapdf/document.rb
CHANGED
@@ -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:
|
188
|
+
# See: Revisions#object
|
188
189
|
def object(ref)
|
189
|
-
|
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.
|
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,
|
218
|
+
# doc.add(obj, **wrap_opts) -> indirect_object
|
221
219
|
#
|
222
|
-
# Adds the object to the
|
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
|
-
#
|
230
|
-
|
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
|
-
|
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
|
260
|
-
# doc.delete(oid
|
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
|
-
#
|
266
|
-
|
267
|
-
|
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 }
|
418
|
-
# doc.each(only_current: true, only_loaded: false) {|obj, rev| block }
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
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 <
|
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
|
-
|
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
|
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
|
|
data/lib/hexapdf/layout/frame.rb
CHANGED
@@ -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 =
|
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)
|