hexapdf 0.12.0 → 0.14.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 +126 -0
- data/examples/019-acro_form.rb +41 -4
- data/lib/hexapdf/cli/command.rb +4 -2
- data/lib/hexapdf/cli/image2pdf.rb +2 -1
- data/lib/hexapdf/cli/info.rb +51 -2
- data/lib/hexapdf/cli/inspect.rb +30 -8
- data/lib/hexapdf/cli/merge.rb +1 -1
- data/lib/hexapdf/cli/split.rb +74 -14
- data/lib/hexapdf/configuration.rb +15 -0
- data/lib/hexapdf/content/graphic_object/arc.rb +3 -3
- data/lib/hexapdf/content/parser.rb +1 -1
- data/lib/hexapdf/dictionary.rb +4 -4
- data/lib/hexapdf/dictionary_fields.rb +1 -9
- data/lib/hexapdf/document.rb +41 -16
- data/lib/hexapdf/document/files.rb +0 -1
- data/lib/hexapdf/encryption/fast_arc4.rb +1 -1
- data/lib/hexapdf/encryption/security_handler.rb +1 -0
- data/lib/hexapdf/encryption/standard_security_handler.rb +1 -0
- data/lib/hexapdf/font/cmap.rb +1 -4
- data/lib/hexapdf/font/encoding/base.rb +8 -0
- data/lib/hexapdf/font/encoding/difference_encoding.rb +6 -0
- data/lib/hexapdf/font/true_type/table/head.rb +1 -0
- data/lib/hexapdf/font/true_type/table/os2.rb +2 -0
- data/lib/hexapdf/font/type1_wrapper.rb +1 -1
- data/lib/hexapdf/image_loader/png.rb +3 -2
- data/lib/hexapdf/layout/line.rb +1 -1
- data/lib/hexapdf/layout/style.rb +23 -23
- data/lib/hexapdf/layout/text_layouter.rb +2 -2
- data/lib/hexapdf/layout/text_shaper.rb +3 -2
- data/lib/hexapdf/object.rb +52 -25
- data/lib/hexapdf/parser.rb +87 -3
- data/lib/hexapdf/pdf_array.rb +11 -4
- data/lib/hexapdf/revisions.rb +29 -21
- data/lib/hexapdf/serializer.rb +1 -1
- data/lib/hexapdf/task/optimize.rb +6 -4
- data/lib/hexapdf/tokenizer.rb +4 -3
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +132 -28
- data/lib/hexapdf/type/acro_form/button_field.rb +21 -13
- data/lib/hexapdf/type/acro_form/choice_field.rb +68 -14
- data/lib/hexapdf/type/acro_form/field.rb +35 -5
- data/lib/hexapdf/type/acro_form/form.rb +139 -14
- data/lib/hexapdf/type/acro_form/text_field.rb +70 -4
- data/lib/hexapdf/type/actions/uri.rb +3 -2
- data/lib/hexapdf/type/annotations/widget.rb +3 -4
- data/lib/hexapdf/type/catalog.rb +2 -2
- data/lib/hexapdf/type/cid_font.rb +1 -1
- data/lib/hexapdf/type/file_specification.rb +1 -1
- data/lib/hexapdf/type/font.rb +1 -1
- data/lib/hexapdf/type/font_simple.rb +4 -2
- data/lib/hexapdf/type/font_true_type.rb +6 -2
- data/lib/hexapdf/type/font_type0.rb +4 -4
- data/lib/hexapdf/type/form.rb +15 -2
- data/lib/hexapdf/type/image.rb +2 -2
- data/lib/hexapdf/type/page.rb +37 -13
- data/lib/hexapdf/type/page_tree_node.rb +29 -5
- data/lib/hexapdf/type/resources.rb +1 -0
- data/lib/hexapdf/type/trailer.rb +2 -3
- data/lib/hexapdf/utils/object_hash.rb +0 -1
- data/lib/hexapdf/utils/sorted_tree_node.rb +18 -15
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/common_tokenizer_tests.rb +6 -1
- data/test/hexapdf/content/graphic_object/test_arc.rb +4 -4
- data/test/hexapdf/content/test_canvas.rb +3 -3
- data/test/hexapdf/content/test_color_space.rb +1 -1
- data/test/hexapdf/encryption/test_aes.rb +4 -4
- data/test/hexapdf/encryption/test_standard_security_handler.rb +11 -11
- data/test/hexapdf/filter/test_ascii85_decode.rb +1 -1
- data/test/hexapdf/filter/test_ascii_hex_decode.rb +1 -1
- data/test/hexapdf/font/encoding/test_base.rb +10 -0
- data/test/hexapdf/font/encoding/test_difference_encoding.rb +8 -0
- data/test/hexapdf/font/test_type1_wrapper.rb +4 -3
- data/test/hexapdf/layout/test_style.rb +1 -1
- data/test/hexapdf/layout/test_text_layouter.rb +12 -5
- data/test/hexapdf/test_configuration.rb +2 -2
- data/test/hexapdf/test_dictionary.rb +3 -1
- data/test/hexapdf/test_dictionary_fields.rb +2 -2
- data/test/hexapdf/test_document.rb +18 -10
- data/test/hexapdf/test_object.rb +71 -26
- data/test/hexapdf/test_parser.rb +159 -53
- data/test/hexapdf/test_pdf_array.rb +8 -1
- data/test/hexapdf/test_revisions.rb +35 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +296 -38
- data/test/hexapdf/type/acro_form/test_button_field.rb +22 -2
- data/test/hexapdf/type/acro_form/test_choice_field.rb +92 -9
- data/test/hexapdf/type/acro_form/test_field.rb +39 -0
- data/test/hexapdf/type/acro_form/test_form.rb +87 -15
- data/test/hexapdf/type/acro_form/test_text_field.rb +77 -1
- data/test/hexapdf/type/test_font_simple.rb +2 -1
- data/test/hexapdf/type/test_font_true_type.rb +6 -0
- data/test/hexapdf/type/test_form.rb +26 -1
- data/test/hexapdf/type/test_page.rb +45 -7
- data/test/hexapdf/type/test_page_tree_node.rb +42 -0
- data/test/hexapdf/utils/test_bit_field.rb +2 -0
- data/test/hexapdf/utils/test_object_hash.rb +5 -0
- data/test/hexapdf/utils/test_sorted_tree_node.rb +10 -9
- data/test/test_helper.rb +2 -0
- metadata +6 -11
|
@@ -44,6 +44,9 @@ module HexaPDF
|
|
|
44
44
|
# AcroForm text fields provide a box or space to fill-in data entered from keyboard. The text
|
|
45
45
|
# may be restricted to a single line or can span multiple lines.
|
|
46
46
|
#
|
|
47
|
+
# A special type of single-line text field is the comb text field. This type of field divides
|
|
48
|
+
# the existing space into /MaxLen equally spaced positions.
|
|
49
|
+
#
|
|
47
50
|
# == Type Specific Field Flags
|
|
48
51
|
#
|
|
49
52
|
# :multiline:: If set, the text field may contain multiple lines.
|
|
@@ -88,6 +91,63 @@ module HexaPDF
|
|
|
88
91
|
}
|
|
89
92
|
).freeze
|
|
90
93
|
|
|
94
|
+
# Initializes the text field to be a multiline text field.
|
|
95
|
+
#
|
|
96
|
+
# This method should only be called directly after creating a new text field because it
|
|
97
|
+
# doesn't completely reset the object.
|
|
98
|
+
def initialize_as_multiline_text_field
|
|
99
|
+
flag(:multiline)
|
|
100
|
+
unflag(:file_select, :comb, :password)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Initializes the text field to be a comb text field.
|
|
104
|
+
#
|
|
105
|
+
# This method should only be called directly after creating a new text field because it
|
|
106
|
+
# doesn't completely reset the object.
|
|
107
|
+
def initialize_as_comb_text_field
|
|
108
|
+
flag(:comb)
|
|
109
|
+
unflag(:file_select, :multiline, :password)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Initializes the text field to be a password field.
|
|
113
|
+
#
|
|
114
|
+
# This method should only be called directly after creating a new text field because it
|
|
115
|
+
# doesn't completely reset the object.
|
|
116
|
+
def initialize_as_password_field
|
|
117
|
+
delete(:V)
|
|
118
|
+
flag(:password)
|
|
119
|
+
unflag(:comb, :multiline, :file_select)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Initializes the text field to be a file select field.
|
|
123
|
+
#
|
|
124
|
+
# This method should only be called directly after creating a new text field because it
|
|
125
|
+
# doesn't completely reset the object.
|
|
126
|
+
def initialize_as_file_select_field
|
|
127
|
+
flag(:file_select)
|
|
128
|
+
unflag(:comb, :multiline, :password)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns +true+ if this field is a multiline text field.
|
|
132
|
+
def multiline_text_field?
|
|
133
|
+
flagged?(:multiline) && !(flagged?(:file_select) || flagged?(:comb) || flagged?(:password))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns +true+ if this field is a comb text field.
|
|
137
|
+
def comb_text_field?
|
|
138
|
+
flagged?(:comb) && !(flagged?(:file_select) || flagged?(:multiline) || flagged?(:password))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns +true+ if this field is a password field.
|
|
142
|
+
def password_field?
|
|
143
|
+
flagged?(:password) && !(flagged?(:file_select) || flagged?(:multiline) || flagged?(:comb))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Returns +true+ if this field is a file select field.
|
|
147
|
+
def file_select_field?
|
|
148
|
+
flagged?(:file_select) && !(flagged?(:password) || flagged?(:multiline) || flagged?(:comb))
|
|
149
|
+
end
|
|
150
|
+
|
|
91
151
|
# Returns the field value, i.e. the text contents of the field, or +nil+ if no value is set.
|
|
92
152
|
#
|
|
93
153
|
# Note that modifying the returned value *might not* modify the text contents in case it is
|
|
@@ -147,11 +207,16 @@ module HexaPDF
|
|
|
147
207
|
#
|
|
148
208
|
# For information on how this is done see AppearanceGenerator.
|
|
149
209
|
#
|
|
150
|
-
# Note that
|
|
151
|
-
#
|
|
152
|
-
|
|
210
|
+
# Note that no new appearances are created if the field value hasn't changed between
|
|
211
|
+
# invocations.
|
|
212
|
+
#
|
|
213
|
+
# By setting +force+ to +true+ the creation of the appearances can be forced.
|
|
214
|
+
def create_appearances(force: false)
|
|
215
|
+
current_value = field_value
|
|
153
216
|
appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
|
|
154
217
|
each_widget do |widget|
|
|
218
|
+
next if !force && widget.cached?(:last_value) && widget.cache(:last_value) == current_value
|
|
219
|
+
widget.cache(:last_value, current_value, update: true)
|
|
155
220
|
appearance_generator_class.new(widget).create_text_appearances
|
|
156
221
|
end
|
|
157
222
|
end
|
|
@@ -173,8 +238,9 @@ module HexaPDF
|
|
|
173
238
|
|
|
174
239
|
if self[:V] && !(self[:V].kind_of?(String) || self[:V].kind_of?(HexaPDF::Stream))
|
|
175
240
|
yield("Text field doesn't contain text but #{self[:V].class} object")
|
|
241
|
+
return
|
|
176
242
|
end
|
|
177
|
-
if (max_len = self[:MaxLen]) && field_value.length > max_len
|
|
243
|
+
if (max_len = self[:MaxLen]) && field_value && field_value.length > max_len
|
|
178
244
|
yield("Text contents of field '#{full_field_name}' is too long")
|
|
179
245
|
end
|
|
180
246
|
end
|
|
@@ -53,9 +53,10 @@ module HexaPDF
|
|
|
53
53
|
|
|
54
54
|
def perform_validation #:nodoc:
|
|
55
55
|
super
|
|
56
|
-
|
|
56
|
+
uri = self[:URI]
|
|
57
|
+
if uri && !uri.ascii_only?
|
|
57
58
|
yield("URIs have to contain ASCII characters only", true)
|
|
58
|
-
uri =
|
|
59
|
+
uri = uri.dup.force_encoding(Encoding::BINARY)
|
|
59
60
|
uri.encode!(Encoding::US_ASCII, fallback: lambda {|c| "%#{c.ord.to_s(16).upcase}" })
|
|
60
61
|
self[:URI] = uri
|
|
61
62
|
end
|
|
@@ -307,10 +307,9 @@ module HexaPDF
|
|
|
307
307
|
color = [0]
|
|
308
308
|
if (da = self[:DA] || field[:DA])
|
|
309
309
|
HexaPDF::Content::Parser.parse(da) do |obj, params|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
size = params[1]
|
|
310
|
+
case obj
|
|
311
|
+
when :rg, :g, :k then color = params.dup
|
|
312
|
+
when :Tf then size = params[1]
|
|
314
313
|
end
|
|
315
314
|
end
|
|
316
315
|
end
|
data/lib/hexapdf/type/catalog.rb
CHANGED
|
@@ -120,12 +120,12 @@ module HexaPDF
|
|
|
120
120
|
private
|
|
121
121
|
|
|
122
122
|
# Ensures that there is a valid page tree.
|
|
123
|
-
def perform_validation
|
|
123
|
+
def perform_validation(&block)
|
|
124
124
|
super
|
|
125
125
|
unless key?(:Pages)
|
|
126
126
|
yield("A PDF document needs a page tree", true)
|
|
127
127
|
value[:Pages] = document.add({Type: :Pages})
|
|
128
|
-
value[:Pages].validate
|
|
128
|
+
value[:Pages].validate(&block)
|
|
129
129
|
end
|
|
130
130
|
end
|
|
131
131
|
|
|
@@ -220,7 +220,7 @@ module HexaPDF
|
|
|
220
220
|
|
|
221
221
|
if document.catalog.key?(:Names) && document.catalog[:Names].key?(:EmbeddedFiles)
|
|
222
222
|
tree = document.catalog[:Names][:EmbeddedFiles]
|
|
223
|
-
tree.each_entry.find_all {|_, spec|
|
|
223
|
+
tree.each_entry.find_all {|_, spec| spec == self }.each do |(name, _)|
|
|
224
224
|
tree.delete_entry(name)
|
|
225
225
|
end
|
|
226
226
|
end
|
data/lib/hexapdf/type/font.rb
CHANGED
|
@@ -102,7 +102,7 @@ module HexaPDF
|
|
|
102
102
|
|
|
103
103
|
# Parses and caches the ToUnicode CMap.
|
|
104
104
|
def to_unicode_cmap
|
|
105
|
-
|
|
105
|
+
cache(:to_unicode_cmap) do
|
|
106
106
|
if key?(:ToUnicode)
|
|
107
107
|
HexaPDF::Font::CMap.parse(self[:ToUnicode].stream)
|
|
108
108
|
else
|
|
@@ -57,7 +57,7 @@ module HexaPDF
|
|
|
57
57
|
#
|
|
58
58
|
# Note that the encoding is cached internally when accessed the first time.
|
|
59
59
|
def encoding
|
|
60
|
-
|
|
60
|
+
cache(:encoding) do
|
|
61
61
|
case (val = self[:Encoding])
|
|
62
62
|
when Symbol
|
|
63
63
|
encoding = HexaPDF::Font::Encoding.for_name(val)
|
|
@@ -170,7 +170,9 @@ module HexaPDF
|
|
|
170
170
|
[:FirstChar, :LastChar, :Widths].each do |field|
|
|
171
171
|
yield("Required field #{field} is not set", false) if self[field].nil?
|
|
172
172
|
end
|
|
173
|
-
|
|
173
|
+
|
|
174
|
+
if key?(:Widths) && key?(:LastChar) && key?(:FirstChar) &&
|
|
175
|
+
self[:Widths].length != (self[:LastChar] - self[:FirstChar] + 1)
|
|
174
176
|
yield("Invalid number of entries in field Widths", false)
|
|
175
177
|
end
|
|
176
178
|
end
|
|
@@ -48,8 +48,12 @@ module HexaPDF
|
|
|
48
48
|
private
|
|
49
49
|
|
|
50
50
|
def perform_validation
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
std_font = FontType1::StandardFonts.standard_font?(self[:BaseFont])
|
|
52
|
+
super(ignore_missing_font_fields: std_font)
|
|
53
|
+
|
|
54
|
+
if self[:FontDescriptor].nil? && !std_font
|
|
55
|
+
yield("Required field FontDescriptor is not set", false)
|
|
56
|
+
end
|
|
53
57
|
end
|
|
54
58
|
|
|
55
59
|
end
|
|
@@ -58,8 +58,8 @@ module HexaPDF
|
|
|
58
58
|
|
|
59
59
|
# Returns the CID font of this type 0 font.
|
|
60
60
|
def descendant_font
|
|
61
|
-
|
|
62
|
-
document.wrap(
|
|
61
|
+
cache(:descendant_font) do
|
|
62
|
+
document.wrap(self[:DescendantFonts][0])
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
@@ -116,7 +116,7 @@ module HexaPDF
|
|
|
116
116
|
#
|
|
117
117
|
# Note that the CMap is cached internally when accessed the first time.
|
|
118
118
|
def cmap
|
|
119
|
-
|
|
119
|
+
cache(:cmap) do
|
|
120
120
|
val = self[:Encoding]
|
|
121
121
|
if val.kind_of?(Symbol)
|
|
122
122
|
HexaPDF::Font::CMap.for_name(val.to_s)
|
|
@@ -135,7 +135,7 @@ module HexaPDF
|
|
|
135
135
|
#
|
|
136
136
|
# See: PDF1.7 s9.10.2
|
|
137
137
|
def ucs2_cmap
|
|
138
|
-
|
|
138
|
+
cache(:ucs2_cmap) do
|
|
139
139
|
encoding = self[:Encoding]
|
|
140
140
|
system_info = descendant_font[:CIDSystemInfo]
|
|
141
141
|
registry = system_info[:Registry]
|
data/lib/hexapdf/type/form.rb
CHANGED
|
@@ -94,14 +94,18 @@ module HexaPDF
|
|
|
94
94
|
|
|
95
95
|
# Replaces the contents of the form XObject with the given string.
|
|
96
96
|
#
|
|
97
|
+
# This also clears the cache to avoid returning invalid objects.
|
|
98
|
+
#
|
|
97
99
|
# Note: This is the same as #stream= but here for interface compatibility with Page.
|
|
98
100
|
def contents=(data)
|
|
99
101
|
self.stream = data
|
|
102
|
+
clear_cache
|
|
100
103
|
end
|
|
101
104
|
|
|
102
105
|
# Returns the resource dictionary which is automatically created if it doesn't exist.
|
|
103
106
|
def resources
|
|
104
|
-
self[:Resources] ||= document.wrap({
|
|
107
|
+
self[:Resources] ||= document.wrap({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]},
|
|
108
|
+
type: :XXResources)
|
|
105
109
|
end
|
|
106
110
|
|
|
107
111
|
# Processes the content stream of the form XObject with the given processor object.
|
|
@@ -126,13 +130,22 @@ module HexaPDF
|
|
|
126
130
|
# The canvas object is cached once it is created so that its graphics state is correctly
|
|
127
131
|
# retained without the need for parsing its contents.
|
|
128
132
|
#
|
|
133
|
+
# If the bounding box of the form XObject doesn't have its origin at (0, 0), the canvas origin
|
|
134
|
+
# is translated into the bottom left corner so that this detail doesn't matter when using the
|
|
135
|
+
# canvas. This means that the canvas' origin is always at the bottom left corner of the
|
|
136
|
+
# bounding box.
|
|
137
|
+
#
|
|
129
138
|
# *Note* that a canvas can only be retrieved for initially empty form XObjects!
|
|
130
139
|
def canvas
|
|
131
|
-
|
|
140
|
+
cache(:canvas) do
|
|
132
141
|
unless stream.empty?
|
|
133
142
|
raise HexaPDF::Error, "Cannot create a canvas for a form XObjects with contents"
|
|
134
143
|
end
|
|
144
|
+
|
|
135
145
|
canvas = Content::Canvas.new(self)
|
|
146
|
+
if box.left != 0 || box.bottom != 0
|
|
147
|
+
canvas.save_graphics_state.translate(box.left, box.bottom)
|
|
148
|
+
end
|
|
136
149
|
self.stream = canvas.stream_data
|
|
137
150
|
set_filter(:FlateDecode)
|
|
138
151
|
canvas
|
data/lib/hexapdf/type/image.rb
CHANGED
|
@@ -150,7 +150,7 @@ module HexaPDF
|
|
|
150
150
|
color_space, = *self[:ColorSpace]
|
|
151
151
|
if color_space == :Indexed
|
|
152
152
|
result.indexed = true
|
|
153
|
-
color_space, = *
|
|
153
|
+
color_space, = *self[:ColorSpace][1]
|
|
154
154
|
end
|
|
155
155
|
case color_space
|
|
156
156
|
when :DeviceRGB, :CalRGB
|
|
@@ -240,7 +240,7 @@ module HexaPDF
|
|
|
240
240
|
end
|
|
241
241
|
|
|
242
242
|
if color_type == ImageLoader::PNG::INDEXED
|
|
243
|
-
palette_data =
|
|
243
|
+
palette_data = self[:ColorSpace][3]
|
|
244
244
|
palette_data = palette_data.stream unless palette_data.kind_of?(String)
|
|
245
245
|
palette = ''.b
|
|
246
246
|
if info.color_space == :rgb
|
data/lib/hexapdf/type/page.rb
CHANGED
|
@@ -304,7 +304,7 @@ module HexaPDF
|
|
|
304
304
|
def contents
|
|
305
305
|
Array(self[:Contents]).each_with_object("".b) do |content_stream, content|
|
|
306
306
|
content << " " unless content.empty?
|
|
307
|
-
content <<
|
|
307
|
+
content << content_stream.stream
|
|
308
308
|
end
|
|
309
309
|
end
|
|
310
310
|
|
|
@@ -323,10 +323,11 @@ module HexaPDF
|
|
|
323
323
|
end
|
|
324
324
|
end
|
|
325
325
|
|
|
326
|
-
# Returns the possibly inherited resource dictionary which is automatically created if it
|
|
326
|
+
# Returns the, possibly inherited, resource dictionary which is automatically created if it
|
|
327
327
|
# doesn't exist.
|
|
328
328
|
def resources
|
|
329
|
-
self[:Resources] ||= document.wrap({
|
|
329
|
+
self[:Resources] ||= document.wrap({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]},
|
|
330
|
+
type: :XXResources)
|
|
330
331
|
end
|
|
331
332
|
|
|
332
333
|
# Processes the content streams associated with the page with the given processor object.
|
|
@@ -344,8 +345,7 @@ module HexaPDF
|
|
|
344
345
|
node = self
|
|
345
346
|
while (parent_node = node[:Parent])
|
|
346
347
|
parent_node[:Kids].each do |kid|
|
|
347
|
-
kid
|
|
348
|
-
break if kid.data == node.data
|
|
348
|
+
break if kid == node
|
|
349
349
|
idx += (kid.type == :Page ? 1 : kid[:Count])
|
|
350
350
|
end
|
|
351
351
|
node = parent_node
|
|
@@ -353,11 +353,26 @@ module HexaPDF
|
|
|
353
353
|
idx
|
|
354
354
|
end
|
|
355
355
|
|
|
356
|
+
# Returns all parent nodes of the page up to the root of the page tree.
|
|
357
|
+
#
|
|
358
|
+
# The direct parent is the first node in the array and the root node the last.
|
|
359
|
+
def ancestor_nodes
|
|
360
|
+
parent = self[:Parent]
|
|
361
|
+
result = [parent]
|
|
362
|
+
result << parent while (parent = parent[:Parent])
|
|
363
|
+
result
|
|
364
|
+
end
|
|
365
|
+
|
|
356
366
|
# Returns the requested type of canvas for the page.
|
|
357
367
|
#
|
|
358
368
|
# The canvas object is cached once it is created so that its graphics state is correctly
|
|
359
369
|
# retained without the need for parsing its contents.
|
|
360
370
|
#
|
|
371
|
+
# If the media box of the page doesn't have its origin at (0, 0), the canvas origin is
|
|
372
|
+
# translated into the bottom left corner so that this detail doesn't matter when using the
|
|
373
|
+
# canvas. This means that the canvas' origin is always at the bottom left corner of the media
|
|
374
|
+
# box.
|
|
375
|
+
#
|
|
361
376
|
# type::
|
|
362
377
|
# Can either be
|
|
363
378
|
# * :page for getting the canvas for the page itself (only valid for initially empty pages)
|
|
@@ -368,22 +383,31 @@ module HexaPDF
|
|
|
368
383
|
raise ArgumentError, "Invalid value for 'type', expected: :page, :underlay or :overlay"
|
|
369
384
|
end
|
|
370
385
|
cache_key = "#{type}_canvas".intern
|
|
371
|
-
return
|
|
386
|
+
return cache(cache_key) if cached?(cache_key)
|
|
372
387
|
|
|
373
388
|
if type == :page && key?(:Contents)
|
|
374
389
|
raise HexaPDF::Error, "Cannot get the canvas for a page with contents"
|
|
375
390
|
end
|
|
376
391
|
|
|
392
|
+
create_canvas = lambda do
|
|
393
|
+
Content::Canvas.new(self).tap do |canvas|
|
|
394
|
+
media_box = box(:media)
|
|
395
|
+
if media_box.left != 0 || media_box.bottom != 0
|
|
396
|
+
canvas.translate(media_box.left, media_box.bottom)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
377
401
|
contents = self[:Contents]
|
|
378
402
|
if contents.nil?
|
|
379
|
-
page_canvas =
|
|
403
|
+
page_canvas = cache(:page_canvas, create_canvas.call)
|
|
380
404
|
self[:Contents] = document.add({Filter: :FlateDecode},
|
|
381
405
|
stream: page_canvas.stream_data)
|
|
382
406
|
end
|
|
383
407
|
|
|
384
408
|
if type == :overlay || type == :underlay
|
|
385
|
-
underlay_canvas =
|
|
386
|
-
overlay_canvas =
|
|
409
|
+
underlay_canvas = cache(:underlay_canvas, create_canvas.call)
|
|
410
|
+
overlay_canvas = cache(:overlay_canvas, create_canvas.call)
|
|
387
411
|
|
|
388
412
|
stream = HexaPDF::StreamData.new do
|
|
389
413
|
Fiber.yield(" q ")
|
|
@@ -396,18 +420,19 @@ module HexaPDF
|
|
|
396
420
|
underlay = document.add({Filter: :FlateDecode}, stream: stream)
|
|
397
421
|
|
|
398
422
|
stream = HexaPDF::StreamData.new do
|
|
399
|
-
Fiber.yield(" Q ")
|
|
423
|
+
Fiber.yield(" Q q ")
|
|
400
424
|
fiber = overlay_canvas.stream_data.fiber
|
|
401
425
|
while fiber.alive? && (data = fiber.resume)
|
|
402
426
|
Fiber.yield(data)
|
|
403
427
|
end
|
|
428
|
+
" Q "
|
|
404
429
|
end
|
|
405
430
|
overlay = document.add({Filter: :FlateDecode}, stream: stream)
|
|
406
431
|
|
|
407
432
|
self[:Contents] = [underlay, *self[:Contents], overlay]
|
|
408
433
|
end
|
|
409
434
|
|
|
410
|
-
|
|
435
|
+
cache(cache_key)
|
|
411
436
|
end
|
|
412
437
|
|
|
413
438
|
# Creates a Form XObject from the page's dictionary and contents for the given PDF document.
|
|
@@ -448,8 +473,7 @@ module HexaPDF
|
|
|
448
473
|
REQUIRED_INHERITABLE_FIELDS.each do |name|
|
|
449
474
|
next if self[name]
|
|
450
475
|
yield("Inheritable page field #{name} not set", name == :Resources)
|
|
451
|
-
|
|
452
|
-
self[:Resources].validate(&block)
|
|
476
|
+
resources.validate(&block) if name == :Ressources
|
|
453
477
|
end
|
|
454
478
|
end
|
|
455
479
|
|
|
@@ -172,11 +172,10 @@ module HexaPDF
|
|
|
172
172
|
return unless page && !page.null? && page[:Parent]
|
|
173
173
|
|
|
174
174
|
parent = page[:Parent]
|
|
175
|
-
index = parent[:Kids].index
|
|
175
|
+
index = parent[:Kids].index(page)
|
|
176
176
|
|
|
177
177
|
if index
|
|
178
|
-
ancestors =
|
|
179
|
-
ancestors << parent while (parent = parent[:Parent])
|
|
178
|
+
ancestors = page.ancestor_nodes
|
|
180
179
|
return nil unless ancestors.include?(self)
|
|
181
180
|
|
|
182
181
|
page[:Parent][:Kids].delete_at(index)
|
|
@@ -188,6 +187,31 @@ module HexaPDF
|
|
|
188
187
|
end
|
|
189
188
|
end
|
|
190
189
|
|
|
190
|
+
# :call-seq:
|
|
191
|
+
# pages.move_page(page, to_index)
|
|
192
|
+
# pages.move_page(index, to_index)
|
|
193
|
+
#
|
|
194
|
+
# Moves the given page or the page at the position specified by the zero-based index to the
|
|
195
|
+
# +to_index+ position.
|
|
196
|
+
#
|
|
197
|
+
# If the page that should be moved, doesn't exist or is invalid, an error is raised.
|
|
198
|
+
#
|
|
199
|
+
# Negative indices count backwards from the end, i.e. -1 is the last page. When using a
|
|
200
|
+
# negative index, the page will be moved after that element. So using an index of -1 will
|
|
201
|
+
# move the page after the last page.
|
|
202
|
+
def move_page(page, to_index)
|
|
203
|
+
page = self.page(page) if page.kind_of?(Integer)
|
|
204
|
+
if page.nil? || page.null? || !page[:Parent] ||
|
|
205
|
+
!(ancestors = page.ancestor_nodes).include?(self)
|
|
206
|
+
raise HexaPDF::Error, "The page to be moved doesn't exist in this page tree"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
parent = page[:Parent]
|
|
210
|
+
insert_page(to_index, page)
|
|
211
|
+
ancestors.each {|node| node[:Count] -= 1 }
|
|
212
|
+
parent[:Kids].delete(page)
|
|
213
|
+
end
|
|
214
|
+
|
|
191
215
|
# :call-seq:
|
|
192
216
|
# pages.each_page {|page| block } -> pages
|
|
193
217
|
# pages.each_page -> Enumerator
|
|
@@ -222,7 +246,7 @@ module HexaPDF
|
|
|
222
246
|
# Ensures that the /Count and /Parent fields of the whole page tree are set up correctly and
|
|
223
247
|
# that there is at least one page node. This is therefore only done for the root node of the
|
|
224
248
|
# page tree!
|
|
225
|
-
def perform_validation
|
|
249
|
+
def perform_validation(&block)
|
|
226
250
|
super
|
|
227
251
|
return if key?(:Parent)
|
|
228
252
|
|
|
@@ -255,7 +279,7 @@ module HexaPDF
|
|
|
255
279
|
|
|
256
280
|
if self[:Count] == 0
|
|
257
281
|
yield("A PDF document needs at least one page", true)
|
|
258
|
-
add_page.validate
|
|
282
|
+
add_page.validate(&block)
|
|
259
283
|
end
|
|
260
284
|
end
|
|
261
285
|
|