hexapdf 0.24.2 → 0.25.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 +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
|