hexapdf 0.24.2 → 0.25.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +63 -14
- data/examples/022-outline.rb +30 -0
- data/lib/hexapdf/configuration.rb +3 -0
- data/lib/hexapdf/document/destinations.rb +90 -1
- data/lib/hexapdf/document.rb +7 -0
- data/lib/hexapdf/encryption/security_handler.rb +4 -1
- data/lib/hexapdf/layout/style.rb +41 -12
- data/lib/hexapdf/layout/text_layouter.rb +5 -0
- data/lib/hexapdf/revision.rb +3 -0
- data/lib/hexapdf/revisions.rb +11 -1
- data/lib/hexapdf/type/acro_form/form.rb +3 -12
- data/lib/hexapdf/type/annotation.rb +5 -16
- data/lib/hexapdf/type/catalog.rb +7 -0
- data/lib/hexapdf/type/font_descriptor.rb +4 -13
- data/lib/hexapdf/type/object_stream.rb +9 -1
- data/lib/hexapdf/type/outline.rb +122 -0
- data/lib/hexapdf/type/outline_item.rb +373 -0
- data/lib/hexapdf/type.rb +2 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/document/test_destinations.rb +87 -0
- data/test/hexapdf/encryption/test_security_handler.rb +4 -2
- data/test/hexapdf/layout/test_style.rb +1 -0
- data/test/hexapdf/layout/test_text_layouter.rb +6 -0
- data/test/hexapdf/test_document.rb +4 -0
- data/test/hexapdf/test_revisions.rb +47 -0
- data/test/hexapdf/test_writer.rb +3 -3
- data/test/hexapdf/type/test_catalog.rb +7 -0
- data/test/hexapdf/type/test_object_stream.rb +11 -10
- data/test/hexapdf/type/test_outline.rb +69 -0
- data/test/hexapdf/type/test_outline_item.rb +292 -0
- metadata +8 -3
@@ -0,0 +1,122 @@
|
|
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
|
+
|
39
|
+
module HexaPDF
|
40
|
+
module Type
|
41
|
+
|
42
|
+
# Represents the root of the PDF's document outline containing a hierarchy of outline items
|
43
|
+
# (sometimes called bookmarks) in a linked list.
|
44
|
+
#
|
45
|
+
# The document outline usually contains items for the sections of the document, so that clicking
|
46
|
+
# on an item opens the page where the section starts (the section header is). Most PDF viewers
|
47
|
+
# are able to display the outline to aid in navigation, though not all apply the optional
|
48
|
+
# attributes like the text color.
|
49
|
+
#
|
50
|
+
# The outline dictionary is linked via the /Outlines entry from the Type::Catalog and can
|
51
|
+
# directly be accessed via HexaPDF::Document#outline.
|
52
|
+
#
|
53
|
+
# Here is an example for creating an outline:
|
54
|
+
#
|
55
|
+
# doc = HexaPDF::Document.new
|
56
|
+
# 5.times { doc.pages.add }
|
57
|
+
# doc.outline.add_item("Section 1", destination: 0) do |sec1|
|
58
|
+
# sec1.add_item("Page 2", destination: doc.pages[1])
|
59
|
+
# sec1.add_item("Page 3", destination: 2)
|
60
|
+
# sec1.add_item("Section 1.1", text_color: "red", flags: [:bold]) do |sec11|
|
61
|
+
# sec11.add_item("Page 4", destination: 3)
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# See: PDF1.7 s12.3.3
|
66
|
+
class Outline < Dictionary
|
67
|
+
|
68
|
+
define_type :Outlines
|
69
|
+
|
70
|
+
define_field :Type, type: Symbol, default: type
|
71
|
+
define_field :First, type: :XXOutlineItem, indirect: true
|
72
|
+
define_field :Last, type: :XXOutlineItem, indirect: true
|
73
|
+
define_field :Count, type: Integer
|
74
|
+
|
75
|
+
# Adds a new top-level outline item.
|
76
|
+
#
|
77
|
+
# See OutlineItem#add_item for details on the available options since this method just passes
|
78
|
+
# all arguments through to it.
|
79
|
+
def add_item(title, **options, &block)
|
80
|
+
self[:Count] ||= 0
|
81
|
+
self_as_item.add_item(title, **options, &block)
|
82
|
+
end
|
83
|
+
|
84
|
+
# :call-seq:
|
85
|
+
# outline.each_item {|item| block } -> item
|
86
|
+
# outline.each_item -> Enumerator
|
87
|
+
#
|
88
|
+
# Iterates over all items of the outline.
|
89
|
+
#
|
90
|
+
# The items are yielded in-order, yielding first the item itself and then its descendants.
|
91
|
+
def each_item(&block)
|
92
|
+
self_as_item.each_item(&block)
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Represents the outline dictionary as an outline item dictionary to make use of some of its
|
98
|
+
# methods.
|
99
|
+
def self_as_item
|
100
|
+
@self_as_item ||= document.wrap(self, type: :XXOutlineItem)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Makes sure the required values are set.
|
104
|
+
def perform_validation
|
105
|
+
super
|
106
|
+
first = self[:First]
|
107
|
+
last = self[:Last]
|
108
|
+
if (first && !last) || (!first && last)
|
109
|
+
yield('Outline dictionary is missing an endpoint reference', true)
|
110
|
+
node, dir = first ? [first, :Next] : [last, :Prev]
|
111
|
+
node = node[dir] while node.key?(dir)
|
112
|
+
self[dir == :Next ? :Last : :First] = node
|
113
|
+
elsif !first && !last && self[:Count]
|
114
|
+
yield('Outline dictionary key /Count set but no items exist', true)
|
115
|
+
delete(:Count)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,373 @@
|
|
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/utils/bit_field'
|
39
|
+
require 'hexapdf/content/color_space'
|
40
|
+
|
41
|
+
module HexaPDF
|
42
|
+
module Type
|
43
|
+
|
44
|
+
# Represents an outline item dictionary.
|
45
|
+
#
|
46
|
+
# An item has a title and some optional attributes: the action that is activated when clicking
|
47
|
+
# (either a simple destination or an explicit action object), the text color, and flags (whether
|
48
|
+
# the text should appear bold and/or italic).
|
49
|
+
#
|
50
|
+
# Additionally, items may have child items which makes it possible to create a hierarchy of
|
51
|
+
# items.
|
52
|
+
#
|
53
|
+
# If no destination/action is set, the item just acts as kind of a header. It usually only makes
|
54
|
+
# sense to do this when the item has children.
|
55
|
+
#
|
56
|
+
# Outline item dictionaries are connected together in the form of a linked list using the /Next
|
57
|
+
# and /Prev keys. Each item may have descendant items. If so, the /First and /Last keys point to
|
58
|
+
# respectively the first and last descendant items.
|
59
|
+
#
|
60
|
+
# Since many dictionary keys need to be kept up-to-date when manipulating the outline item tree,
|
61
|
+
# it is not recommended to manually do this but to rely on the provided convenience methods.
|
62
|
+
#
|
63
|
+
# See: PDF1.7 s12.3.3
|
64
|
+
class OutlineItem < Dictionary
|
65
|
+
|
66
|
+
extend Utils::BitField
|
67
|
+
|
68
|
+
define_type :XXOutlineItem
|
69
|
+
|
70
|
+
define_field :Title, type: String, required: true
|
71
|
+
define_field :Parent, type: Dictionary, required: true, indirect: true
|
72
|
+
define_field :Prev, type: :XXOutlineItem, indirect: true
|
73
|
+
define_field :Next, type: :XXOutlineItem, indirect: true
|
74
|
+
define_field :First, type: :XXOutlineItem, indirect: true
|
75
|
+
define_field :Last, type: :XXOutlineItem, indirect: true
|
76
|
+
define_field :Count, type: Integer
|
77
|
+
define_field :Dest, type: [Symbol, PDFByteString, PDFArray]
|
78
|
+
define_field :A, type: :Action, version: '1.1'
|
79
|
+
define_field :SE, type: Dictionary, indirect: true
|
80
|
+
define_field :C, type: PDFArray, default: [0, 0, 0], version: '1.4'
|
81
|
+
define_field :F, type: Integer, default: 0, version: '1.4'
|
82
|
+
|
83
|
+
##
|
84
|
+
# :method: flags
|
85
|
+
#
|
86
|
+
# Returns an array of flag names representing the set bit flags for /F.
|
87
|
+
#
|
88
|
+
# The available flags are:
|
89
|
+
#
|
90
|
+
# :italic or 0:: The text is displayed in italic.
|
91
|
+
# :bold or 1:: The text is displayed in bold.
|
92
|
+
#
|
93
|
+
|
94
|
+
##
|
95
|
+
# :method: flagged?
|
96
|
+
# :call-seq:
|
97
|
+
# flagged?(flag)
|
98
|
+
#
|
99
|
+
# Returns +true+ if the given flag is set on /F. The argument can either be the flag name or
|
100
|
+
# the bit index.
|
101
|
+
#
|
102
|
+
# See #flags for the list of available flags.
|
103
|
+
#
|
104
|
+
|
105
|
+
##
|
106
|
+
# :method: flag
|
107
|
+
# :call-seq:
|
108
|
+
# flag(*flags, clear_existing: false)
|
109
|
+
#
|
110
|
+
# Sets the given flags on /F, given as flag names or bit indices. If +clear_existing+ is
|
111
|
+
# +true+, all prior flags will be cleared.
|
112
|
+
#
|
113
|
+
# See #flags for the list of available flags.
|
114
|
+
#
|
115
|
+
|
116
|
+
##
|
117
|
+
# :method: unflag
|
118
|
+
# :call-seq:
|
119
|
+
# flag(*flags)
|
120
|
+
#
|
121
|
+
# Clears the given flags from /F, given as flag names or bit indices.
|
122
|
+
#
|
123
|
+
# See #flags for the list of available flags.
|
124
|
+
#
|
125
|
+
bit_field(:flags, {italic: 0, bold: 1},
|
126
|
+
lister: "flags", getter: "flagged?", setter: "flag", unsetter: "unflag",
|
127
|
+
value_getter: "self[:F]", value_setter: "self[:F]")
|
128
|
+
|
129
|
+
# :call-seq:
|
130
|
+
# item.title -> title
|
131
|
+
# item.title(value) -> title
|
132
|
+
#
|
133
|
+
# Returns the item's title if no argument is given. Otherwise sets the title to the given
|
134
|
+
# value.
|
135
|
+
def title(value = nil)
|
136
|
+
if value
|
137
|
+
self[:Title] = value
|
138
|
+
else
|
139
|
+
self[:Title]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# :call-seq:
|
144
|
+
# item.text_color -> color
|
145
|
+
# item.text_color(color) -> color
|
146
|
+
#
|
147
|
+
# Returns the item's text color as HexaPDF::Content::ColorSpace::DeviceRGB::Color object if no
|
148
|
+
# argument is given. Otherwise sets the text color, see
|
149
|
+
# HexaPDF::Content::ColorSpace.device_color_from_specification for possible +color+ values.
|
150
|
+
#
|
151
|
+
# Note: The color *has* to be an RGB color.
|
152
|
+
def text_color(color = nil)
|
153
|
+
if color
|
154
|
+
color = HexaPDF::Content::ColorSpace.device_color_from_specification(color)
|
155
|
+
unless color.color_space.family == :DeviceRGB
|
156
|
+
raise ArgumentError, "The given argument is not a valid RGB color"
|
157
|
+
end
|
158
|
+
self[:C] = color.components
|
159
|
+
else
|
160
|
+
Content::ColorSpace.prenormalized_device_color(self[:C])
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# :call-seq:
|
165
|
+
# item.destination -> destination
|
166
|
+
# item.destination(value) -> destination
|
167
|
+
#
|
168
|
+
# Returns the item's destination if no argument is given. Otherwise sets the destination to
|
169
|
+
# the given value (see HexaPDF::Document::Destinations#use_or_create for the posssible
|
170
|
+
# values).
|
171
|
+
#
|
172
|
+
# If an action is set, the destination has to be unset; and vice versa. So when setting a
|
173
|
+
# destination value, the action is automatically deleted.
|
174
|
+
def destination(value = nil)
|
175
|
+
if value
|
176
|
+
delete(:A)
|
177
|
+
self[:Dest] = document.destinations.use_or_create(value)
|
178
|
+
else
|
179
|
+
self[:Dest]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# :call-seq:
|
184
|
+
# item.action -> action
|
185
|
+
# item.action(value) -> action
|
186
|
+
#
|
187
|
+
# Returns the item's action if no argument is given. Otherwise sets the action to
|
188
|
+
# the given value (needs to be a valid HexaPDF::Type::Action dictionary).
|
189
|
+
#
|
190
|
+
# If an action is set, the destination has to be unset; and vice versa. So when setting an
|
191
|
+
# action value, the destination is automatically deleted.
|
192
|
+
def action(value = nil)
|
193
|
+
if value
|
194
|
+
delete(:Dest)
|
195
|
+
self[:A] = value
|
196
|
+
else
|
197
|
+
self[:A]
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Adds, as child to this item, a new outline item with the given title that performs the
|
202
|
+
# provided action on clicking. Returns the newly added item.
|
203
|
+
#
|
204
|
+
# If neither :destination nor :action is specified, the outline item has no associated action.
|
205
|
+
# This is only meaningful if the new item will have children as it then acts just as a
|
206
|
+
# container.
|
207
|
+
#
|
208
|
+
# If a block is specified, the newly created item is yielded.
|
209
|
+
#
|
210
|
+
# destination::
|
211
|
+
#
|
212
|
+
# Specifies the destination that should be activated when clicking on the outline item.
|
213
|
+
# See HexaPDF::Document::Destinations#use_or_create for details. The argument :action
|
214
|
+
# takes precedence if it is also specified,
|
215
|
+
#
|
216
|
+
# action::
|
217
|
+
#
|
218
|
+
# Specifies the action that should be taken when clicking on the outline item. See
|
219
|
+
# HexaPDF::Type::Action for details. If the argument :destination is also specified, the
|
220
|
+
# :action argument takes precedence.
|
221
|
+
#
|
222
|
+
# position::
|
223
|
+
#
|
224
|
+
# The position where the new child item should be inserted. Can either be:
|
225
|
+
#
|
226
|
+
# +:first+:: Insert as first item
|
227
|
+
# +:last+:: Insert as last item (default)
|
228
|
+
# Integer:: When non-negative inserts before, otherwise after, the item at the given
|
229
|
+
# zero-based index.
|
230
|
+
#
|
231
|
+
# open::
|
232
|
+
#
|
233
|
+
# Specifies whether the outline item should be open (i.e. one or more children are shown)
|
234
|
+
# or closed. Default: +true+.
|
235
|
+
#
|
236
|
+
# text_color::
|
237
|
+
#
|
238
|
+
# The text color of the outline item text which needs to be a valid RGB color (see
|
239
|
+
# #text_color for details). If not set, the text appears in black.
|
240
|
+
#
|
241
|
+
# flags::
|
242
|
+
#
|
243
|
+
# An array of font variants (possible values are :bold and :italic) to set for the outline
|
244
|
+
# item text, see #flags for detail. Default is to use no variant.
|
245
|
+
#
|
246
|
+
# Examples:
|
247
|
+
#
|
248
|
+
# doc.destinations.add("Title") do |item| # no action, just container
|
249
|
+
# item.add("Second subitem", destination: doc.pages[1]) # links to page 2
|
250
|
+
# item.add("First subitem", position: :first, destination: doc.pages[0])
|
251
|
+
# end
|
252
|
+
def add_item(title, destination: nil, action: nil, position: :last, open: true,
|
253
|
+
text_color: nil, flags: nil) # :yield: item
|
254
|
+
item = document.add({Parent: self}, type: :XXOutlineItem)
|
255
|
+
item.title(title)
|
256
|
+
if action
|
257
|
+
item.action(action)
|
258
|
+
else
|
259
|
+
item.destination(destination)
|
260
|
+
end
|
261
|
+
item.text_color(text_color) if text_color
|
262
|
+
item.flag(*flags) if flags
|
263
|
+
item[:Count] = 0 if open # Count=0 means open if items are later added
|
264
|
+
|
265
|
+
unless position == :last || position == :first || position.kind_of?(Integer)
|
266
|
+
raise ArgumentError, "position must be :first, :last, or an integer"
|
267
|
+
end
|
268
|
+
if self[:First]
|
269
|
+
case position
|
270
|
+
when :last, -1
|
271
|
+
item[:Prev] = self[:Last]
|
272
|
+
self[:Last][:Next] = item
|
273
|
+
self[:Last] = item
|
274
|
+
when :first, 0
|
275
|
+
item[:Next] = self[:First]
|
276
|
+
self[:First][:Prev] = item
|
277
|
+
self[:First] = item
|
278
|
+
when Integer
|
279
|
+
temp, direction = if position > 0
|
280
|
+
[self[:First], :Next]
|
281
|
+
else
|
282
|
+
position = -position - 2
|
283
|
+
[self[:Last], :Prev]
|
284
|
+
end
|
285
|
+
position.times { temp &&= temp[direction] }
|
286
|
+
raise ArgumentError, "position out of bounds" if temp.nil?
|
287
|
+
item[:Prev] = temp[:Prev]
|
288
|
+
item[:Next] = temp
|
289
|
+
temp[:Prev] = item
|
290
|
+
item[:Prev][:Next] = item
|
291
|
+
end
|
292
|
+
else
|
293
|
+
self[:First] = self[:Last] = item
|
294
|
+
end
|
295
|
+
|
296
|
+
# Re-calculate /Count entries
|
297
|
+
temp = self
|
298
|
+
while temp
|
299
|
+
if !temp.key?(:Count) || temp[:Count] < 0
|
300
|
+
temp[:Count] = (temp[:Count] || 0) - 1
|
301
|
+
break
|
302
|
+
else
|
303
|
+
temp[:Count] += 1
|
304
|
+
end
|
305
|
+
temp = temp[:Parent]
|
306
|
+
end
|
307
|
+
|
308
|
+
yield(item) if block_given?
|
309
|
+
|
310
|
+
item
|
311
|
+
end
|
312
|
+
|
313
|
+
# :call-seq:
|
314
|
+
# item.each_item {|descendant_item| block } -> item
|
315
|
+
# item.each_item -> Enumerator
|
316
|
+
#
|
317
|
+
# Iterates over all descendant items of this one.
|
318
|
+
#
|
319
|
+
# The items are yielded in-order, yielding first the item itself and then its descendants.
|
320
|
+
def each_item(&block)
|
321
|
+
return to_enum(__method__) unless block_given?
|
322
|
+
|
323
|
+
item = self[:First]
|
324
|
+
while item
|
325
|
+
yield(item)
|
326
|
+
item.each_item(&block)
|
327
|
+
item = item[:Next]
|
328
|
+
end
|
329
|
+
|
330
|
+
self
|
331
|
+
end
|
332
|
+
|
333
|
+
private
|
334
|
+
|
335
|
+
def perform_validation # :nodoc:
|
336
|
+
super
|
337
|
+
first = self[:First]
|
338
|
+
last = self[:Last]
|
339
|
+
if (first && !last) || (!first && last)
|
340
|
+
yield('Outline item dictionary is missing an endpoint reference', true)
|
341
|
+
node, dir = first ? [first, :Next] : [last, :Prev]
|
342
|
+
node = node[dir] while node.key?(dir)
|
343
|
+
self[dir == :Next ? :Last : :First] = node
|
344
|
+
elsif !first && !last && self[:Count] != 0
|
345
|
+
yield('Outline item dictionary key /Count set but no descendants exist', true)
|
346
|
+
delete(:Count)
|
347
|
+
end
|
348
|
+
|
349
|
+
prev_item = self[:Prev]
|
350
|
+
if prev_item && (prev_item_next = prev_item[:Next]) != self
|
351
|
+
if prev_item_next
|
352
|
+
yield('Outline item /Prev points to item whose /Next points somewhere else', false)
|
353
|
+
else
|
354
|
+
yield('Outline item /Prev points to item without /Next', true)
|
355
|
+
prev_item[:Next] = self
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
next_item = self[:Next]
|
360
|
+
if next_item && (next_item_prev = next_item[:Prev]) != self
|
361
|
+
if next_item_prev
|
362
|
+
yield('Outline item /Next points to item whose /Prev points somewhere else', false)
|
363
|
+
else
|
364
|
+
yield('Outline item /Next points to item without /Prev', true)
|
365
|
+
next_item[:Prev] = self
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
end
|
371
|
+
|
372
|
+
end
|
373
|
+
end
|
data/lib/hexapdf/type.rb
CHANGED
@@ -73,6 +73,8 @@ module HexaPDF
|
|
73
73
|
autoload(:IconFit, 'hexapdf/type/icon_fit')
|
74
74
|
autoload(:AcroForm, 'hexapdf/type/acro_form')
|
75
75
|
autoload(:Signature, 'hexapdf/type/signature')
|
76
|
+
autoload(:Outline, 'hexapdf/type/outline')
|
77
|
+
autoload(:OutlineItem, 'hexapdf/type/outline_item')
|
76
78
|
|
77
79
|
end
|
78
80
|
|
data/lib/hexapdf/version.rb
CHANGED
@@ -8,6 +8,27 @@ describe HexaPDF::Document::Destinations::Destination do
|
|
8
8
|
HexaPDF::Document::Destinations::Destination.new(dest)
|
9
9
|
end
|
10
10
|
|
11
|
+
describe "self.valid?" do
|
12
|
+
before do
|
13
|
+
@klass = HexaPDF::Document::Destinations::Destination
|
14
|
+
end
|
15
|
+
|
16
|
+
it "validates the type" do
|
17
|
+
assert(@klass.valid?([5, :Fit]))
|
18
|
+
refute(@klass.valid?([5, :FitNone]))
|
19
|
+
end
|
20
|
+
|
21
|
+
it "validates the page entry" do
|
22
|
+
assert(@klass.valid?([5, :Fit]))
|
23
|
+
refute(@klass.valid?([HexaPDF::Dictionary.new({Type: :Page}), :FitNone]))
|
24
|
+
end
|
25
|
+
|
26
|
+
it "validates the arguments" do
|
27
|
+
assert(@klass.valid?([5, :FitH, 5]))
|
28
|
+
refute(@klass.valid?([5, :FitH, :other]))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
11
32
|
it "can be asked whether the referenced page is in a remote document" do
|
12
33
|
assert(destination([5, :Fit]).remote?)
|
13
34
|
refute(destination([HexaPDF::Dictionary.new({}), :Fit]).remote?)
|
@@ -17,6 +38,10 @@ describe HexaPDF::Document::Destinations::Destination do
|
|
17
38
|
assert_equal(:page, destination([:page, :Fit]).page)
|
18
39
|
end
|
19
40
|
|
41
|
+
it "can validate a destination" do
|
42
|
+
assert(destination([5, :Fit]).valid?)
|
43
|
+
end
|
44
|
+
|
20
45
|
describe "type :xyz" do
|
21
46
|
before do
|
22
47
|
@dest = destination([:page, :XYZ, :left, :top, :zoom])
|
@@ -201,6 +226,68 @@ describe HexaPDF::Document::Destinations do
|
|
201
226
|
@page = @doc.pages.add
|
202
227
|
end
|
203
228
|
|
229
|
+
describe "use_or_create" do
|
230
|
+
it "uses the given destination name if it exists" do
|
231
|
+
@doc.destinations.create(:fit_page, @page, name: "test")
|
232
|
+
assert_equal("test", @doc.destinations.use_or_create("test"))
|
233
|
+
end
|
234
|
+
|
235
|
+
it "fails if the given destination name doesn't exist" do
|
236
|
+
assert_raises(HexaPDF::Error) { @doc.destinations.use_or_create("test") }
|
237
|
+
end
|
238
|
+
|
239
|
+
it "uses the given destination array" do
|
240
|
+
dest = [@page, :Fit]
|
241
|
+
assert_same(dest, @doc.destinations.use_or_create(dest))
|
242
|
+
end
|
243
|
+
|
244
|
+
it "fails if the given destination array is not valid" do
|
245
|
+
assert_raises(HexaPDF::Error) { @doc.destinations.use_or_create([@page, :FitNone]) }
|
246
|
+
end
|
247
|
+
|
248
|
+
it "creates a fit page destination for a given page" do
|
249
|
+
assert_equal([@page, :Fit], @doc.destinations.use_or_create(@page))
|
250
|
+
end
|
251
|
+
|
252
|
+
it "fails if the given dictionary object is not a page object" do
|
253
|
+
assert_raises(HexaPDF::Error) { @doc.destinations.use_or_create(@doc.catalog) }
|
254
|
+
end
|
255
|
+
|
256
|
+
it "creates a fit page destination for a given page index" do
|
257
|
+
assert_equal([@page, :Fit], @doc.destinations.use_or_create(0))
|
258
|
+
end
|
259
|
+
|
260
|
+
it "fails if the given index is no a valid page index" do
|
261
|
+
assert_raises(ArgumentError) { @doc.destinations.use_or_create(-1) }
|
262
|
+
assert_raises(ArgumentError) { @doc.destinations.use_or_create(1) }
|
263
|
+
end
|
264
|
+
|
265
|
+
it "creates the destination using the provided details" do
|
266
|
+
dest = @doc.destinations.use_or_create(type: :fit_page_horizontal, page: @page, top: 10)
|
267
|
+
assert_equal([@page, :FitH, 10], dest)
|
268
|
+
end
|
269
|
+
|
270
|
+
it "fails creating a destination if the :type key is missing" do
|
271
|
+
assert_raises(ArgumentError) { @doc.destinations.use_or_create(page: @page) }
|
272
|
+
end
|
273
|
+
|
274
|
+
it "fails creating a destination if the :page key is missing" do
|
275
|
+
assert_raises(ArgumentError) { @doc.destinations.use_or_create(type: :fit_page) }
|
276
|
+
end
|
277
|
+
|
278
|
+
it "fails if the provided argument has an invalid type" do
|
279
|
+
assert_raises(ArgumentError) { @doc.destinations.use_or_create(:value) }
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
describe "create" do
|
284
|
+
it "creates the destination based on the given type" do
|
285
|
+
@doc.destinations.stub(:create_fit_page, [5, :Fit]) do
|
286
|
+
assert_equal([5, :Fit], @doc.destinations.create(:fit_page, 5))
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
204
291
|
describe "create_xyz" do
|
205
292
|
it "creates the destination" do
|
206
293
|
dest = @doc.destinations.create_xyz(@page, left: 1, top: 2, zoom: 3)
|
@@ -351,8 +351,10 @@ describe HexaPDF::Encryption::SecurityHandler do
|
|
351
351
|
|
352
352
|
it "doesn't encrypt strings in a document's Encrypt dictionary" do
|
353
353
|
@document.trailer[:Encrypt] = @handler.dict
|
354
|
-
|
355
|
-
|
354
|
+
str = 'string'
|
355
|
+
result = @handler.encrypt_string(str, @document.trailer[:Encrypt])
|
356
|
+
assert_equal('string', result)
|
357
|
+
refute_same(str, result)
|
356
358
|
end
|
357
359
|
|
358
360
|
it "doesn't encrypt XRef streams" do
|
@@ -671,6 +671,7 @@ describe HexaPDF::Layout::Style do
|
|
671
671
|
@style = HexaPDF::Layout::Style.new
|
672
672
|
assert_raises(HexaPDF::Error) { @style.font }
|
673
673
|
assert_equal(10, @style.font_size)
|
674
|
+
assert_nil(@style.line_height)
|
674
675
|
assert_equal(0, @style.character_spacing)
|
675
676
|
assert_equal(0, @style.word_spacing)
|
676
677
|
assert_equal(100, @style.horizontal_scaling)
|
@@ -461,6 +461,12 @@ describe HexaPDF::Layout::TextLayouter do
|
|
461
461
|
assert_equal(20 + 20 + 9 + 20 + 9, result.height)
|
462
462
|
end
|
463
463
|
|
464
|
+
it "handles penalties with non-zero width at end of line if they don't fit" do
|
465
|
+
items = boxes([17.21, 9]) + [penalty(50, boxes([3.33]).first)] + boxes([30, 9])
|
466
|
+
result = @layouter.fit(items, 20.5, 100)
|
467
|
+
assert_equal(3, result.remaining_items.size)
|
468
|
+
end
|
469
|
+
|
464
470
|
it "handles line breaks in combination with multiple parts per line" do
|
465
471
|
items = boxes([20, 20]) + [penalty(-5000)] +
|
466
472
|
boxes([20, 20], [20, 20], [20, 20]) + [penalty(-5000)] +
|
@@ -562,6 +562,10 @@ describe HexaPDF::Document do
|
|
562
562
|
end
|
563
563
|
end
|
564
564
|
|
565
|
+
it "returns the document outline" do
|
566
|
+
assert_kind_of(HexaPDF::Type::Outline, @doc.outline)
|
567
|
+
end
|
568
|
+
|
565
569
|
it "can be inspected and the output is not too large" do
|
566
570
|
assert_match(/HexaPDF::Document:\d+/, @doc.inspect)
|
567
571
|
end
|