hexapdf 0.30.0 → 0.31.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 +28 -1
- data/lib/hexapdf/cli/command.rb +16 -1
- data/lib/hexapdf/cli/info.rb +9 -1
- data/lib/hexapdf/cli/inspect.rb +2 -2
- data/lib/hexapdf/composer.rb +76 -28
- data/lib/hexapdf/configuration.rb +17 -4
- data/lib/hexapdf/document/pages.rb +1 -5
- data/lib/hexapdf/document.rb +8 -1
- data/lib/hexapdf/filter/flate_decode.rb +20 -8
- data/lib/hexapdf/layout/page_style.rb +144 -0
- data/lib/hexapdf/layout.rb +1 -0
- data/lib/hexapdf/task/optimize.rb +8 -6
- data/lib/hexapdf/type/object_stream.rb +7 -2
- data/lib/hexapdf/type/outline.rb +1 -1
- data/lib/hexapdf/type/outline_item.rb +1 -1
- data/lib/hexapdf/type/page.rb +29 -8
- data/lib/hexapdf/type/xref_stream.rb +11 -4
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +1 -1
- data/test/hexapdf/filter/test_flate_decode.rb +19 -5
- data/test/hexapdf/layout/test_page_style.rb +70 -0
- data/test/hexapdf/task/test_optimize.rb +11 -9
- data/test/hexapdf/test_composer.rb +35 -10
- data/test/hexapdf/test_writer.rb +5 -5
- data/test/hexapdf/type/test_object_stream.rb +16 -7
- data/test/hexapdf/type/test_outline.rb +3 -1
- data/test/hexapdf/type/test_outline_item.rb +3 -1
- data/test/hexapdf/type/test_page.rb +42 -11
- data/test/hexapdf/type/test_xref_stream.rb +6 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a32d8f7558ea14ae5dc40849c690b79f5ed4a797cffb85253e7fd6d00c54736
|
|
4
|
+
data.tar.gz: 92713168ee64efc59f86ff1d4f8989054ab02653ccaf84795aed3fea02a4811d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 43b05643d9a59e9656a464eda221fa28e04bb4fe8ba1f97d0e31e434fbce64ffb9ed551689ce68957c8b4a7b82fbc85ecd91c1eac6d177b0109d4b9b5d275a6c
|
|
7
|
+
data.tar.gz: 780e7051327b0463e800ca4ea3b9f1ac1c3723d960154a2ac27c9e9bed867d0a257c48c20762cd0947f8638dc1f72b598a33ff76e1f0d1d2b0f1ff843497eb29
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
## 0.
|
|
1
|
+
## 0.31.0 - 2023-02-22
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
* [HexaPDF::Layout::PageStyle] for collecting all styling information for pages
|
|
6
|
+
* [HexaPDF::Composer#page_style] for configuring different page styles
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
* **Breaking change**: [HexaPDF::Composer] uses page styles underneath
|
|
11
|
+
* **Breaking change**: Configuration options `filter.flate_compression` and
|
|
12
|
+
`filter.flate_memory` are changed to `filter.flate.compression` and
|
|
13
|
+
`filter.flate.memory`
|
|
14
|
+
* **Breaking change**: [HexaPDF::Document#wrap] handles cross-reference and
|
|
15
|
+
object stream specially to avoid problems with invalid PDFs
|
|
16
|
+
* [HexaPDF::Composer::new] to allow skipping the initial page creation
|
|
17
|
+
* CLI command `hexapdf info --check` to process streams to reveal stream errors
|
|
18
|
+
* CLI commands to output the name of created PDF files in verbose mode
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
* Validation of document outline items in case the first or last item got
|
|
23
|
+
deleted
|
|
24
|
+
* `HexaPDF::Type::Page#perform_validation` to set a /MediaBox for invalid pages
|
|
25
|
+
that don't have one
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## 0.30.0 - 2023-02-13
|
|
2
29
|
|
|
3
30
|
### Added
|
|
4
31
|
|
data/lib/hexapdf/cli/command.rb
CHANGED
|
@@ -101,6 +101,17 @@ module HexaPDF
|
|
|
101
101
|
def pdf_options(password)
|
|
102
102
|
hash = {decryption_opts: {password: password}, config: {}}
|
|
103
103
|
HexaPDF::GlobalConfiguration['filter.predictor.strict'] = command_parser.strict
|
|
104
|
+
HexaPDF::GlobalConfiguration['filter.flate.on_error'] =
|
|
105
|
+
if command_parser.strict
|
|
106
|
+
proc { true }
|
|
107
|
+
else
|
|
108
|
+
proc do |_, error|
|
|
109
|
+
if command_parser.verbosity_info?
|
|
110
|
+
$stderr.puts "Ignoring error in flate encoded stream: #{error}"
|
|
111
|
+
end
|
|
112
|
+
false
|
|
113
|
+
end
|
|
114
|
+
end
|
|
104
115
|
hash[:config]['parser.try_xref_reconstruction'] = !command_parser.strict
|
|
105
116
|
hash[:config]['parser.on_correctable_error'] =
|
|
106
117
|
if command_parser.strict
|
|
@@ -120,6 +131,7 @@ module HexaPDF
|
|
|
120
131
|
# Writes the document to the given file or does nothing if +out_file+ is +nil+.
|
|
121
132
|
def write_document(doc, out_file, incremental: false)
|
|
122
133
|
if out_file
|
|
134
|
+
doc.trailer.update_id
|
|
123
135
|
doc.validate(auto_correct: true) do |msg, correctable, object|
|
|
124
136
|
if command_parser.strict && !correctable
|
|
125
137
|
raise "Validation error for object (#{object.oid},#{object.gen}): #{msg}"
|
|
@@ -128,6 +140,9 @@ module HexaPDF
|
|
|
128
140
|
"for object (#{object.oid},#{object.gen}): #{msg}"
|
|
129
141
|
end
|
|
130
142
|
end
|
|
143
|
+
if command_parser.verbosity_info?
|
|
144
|
+
puts "Creating output document #{out_file}"
|
|
145
|
+
end
|
|
131
146
|
doc.write(out_file, validate: false, incremental: incremental)
|
|
132
147
|
end
|
|
133
148
|
end
|
|
@@ -331,7 +346,7 @@ module HexaPDF
|
|
|
331
346
|
rotation = ROTATE_MAP[$4]
|
|
332
347
|
start_nr.step(to: end_nr, by: step) {|n| arr << [n, rotation] }
|
|
333
348
|
else
|
|
334
|
-
raise OptionParser::InvalidArgument, "invalid page range format: #{str}"
|
|
349
|
+
raise OptionParser::InvalidArgument, "invalid page range format: #{str.inspect}"
|
|
335
350
|
end
|
|
336
351
|
end
|
|
337
352
|
end
|
data/lib/hexapdf/cli/info.rb
CHANGED
|
@@ -106,6 +106,13 @@ module HexaPDF
|
|
|
106
106
|
doc.each(only_loaded: false) do |obj|
|
|
107
107
|
indirect_object = obj
|
|
108
108
|
obj.validate(auto_correct: true, &validation_block)
|
|
109
|
+
if obj.data.stream
|
|
110
|
+
begin
|
|
111
|
+
obj.stream
|
|
112
|
+
rescue StandardError
|
|
113
|
+
puts "ERROR: Stream of object (#{obj.oid},#{obj.gen}) invalid: #{$!.message}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
109
116
|
end
|
|
110
117
|
end
|
|
111
118
|
|
|
@@ -177,7 +184,8 @@ module HexaPDF
|
|
|
177
184
|
def pdf_options(password)
|
|
178
185
|
if @check_file
|
|
179
186
|
options = {decryption_opts: {password: password}, config: {}}
|
|
180
|
-
HexaPDF::GlobalConfiguration['filter.predictor.strict'] =
|
|
187
|
+
HexaPDF::GlobalConfiguration['filter.predictor.strict'] = true
|
|
188
|
+
HexaPDF::GlobalConfiguration['filter.flate.on_error'] = proc { true }
|
|
181
189
|
options[:config]['parser.try_xref_reconstruction'] = true
|
|
182
190
|
options[:config]['parser.on_correctable_error'] = lambda do |_, msg, pos|
|
|
183
191
|
puts "WARNING: Parse error at position #{pos}: #{msg}"
|
data/lib/hexapdf/cli/inspect.rb
CHANGED
|
@@ -166,8 +166,8 @@ module HexaPDF
|
|
|
166
166
|
when 'p', 'pages'
|
|
167
167
|
begin
|
|
168
168
|
pages = parse_pages_specification(data.shift || '1-e', @doc.pages.count)
|
|
169
|
-
rescue StandardError
|
|
170
|
-
$stderr.puts("Error:
|
|
169
|
+
rescue StandardError => e
|
|
170
|
+
$stderr.puts("Error: #{e}")
|
|
171
171
|
next
|
|
172
172
|
end
|
|
173
173
|
page_list = @doc.pages.to_a
|
data/lib/hexapdf/composer.rb
CHANGED
|
@@ -106,6 +106,14 @@ module HexaPDF
|
|
|
106
106
|
|
|
107
107
|
# Creates a new Composer object and optionally yields it to the given block.
|
|
108
108
|
#
|
|
109
|
+
# skip_page_creation::
|
|
110
|
+
# If this argument is +true+ (the default), the arguments +page_size+, +page_orientation+
|
|
111
|
+
# and +margin+ are used to create a page style with the name :default and an initial page is
|
|
112
|
+
# created as well.
|
|
113
|
+
#
|
|
114
|
+
# Otherwise, i.e. when this argument is +false+, no initial page or default page style is
|
|
115
|
+
# created. This has to be done manually using the #page_style and #new_page methods.
|
|
116
|
+
#
|
|
109
117
|
# page_size::
|
|
110
118
|
# Can be any valid predefined page size (see Type::Page::PAPER_SIZE) or an array [llx, lly,
|
|
111
119
|
# urx, ury] specifying a custom page size.
|
|
@@ -120,36 +128,53 @@ module HexaPDF
|
|
|
120
128
|
# Example:
|
|
121
129
|
#
|
|
122
130
|
# composer = HexaPDF::Composer.new # uses the default values
|
|
131
|
+
#
|
|
123
132
|
# HexaPDF::Composer.new(page_size: :Letter, margin: 72) do |composer|
|
|
124
133
|
# #...
|
|
125
134
|
# end
|
|
126
|
-
|
|
135
|
+
#
|
|
136
|
+
# HexaPDF::Composer.new(skip_page_creation: true) do |composer|
|
|
137
|
+
# page_template = lambda {|canvas, style| style.create_frame(canvas.context, 36) }
|
|
138
|
+
# page_style(:default, template: page_template)
|
|
139
|
+
# new_page
|
|
140
|
+
# # ...
|
|
141
|
+
# end
|
|
142
|
+
def initialize(skip_page_creation: false, page_size: :A4, page_orientation: :portrait,
|
|
143
|
+
margin: 36) #:yields: composer
|
|
127
144
|
@document = HexaPDF::Document.new
|
|
128
|
-
@
|
|
129
|
-
@
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
145
|
+
@page_styles = {}
|
|
146
|
+
@page_style = :default
|
|
147
|
+
unless skip_page_creation
|
|
148
|
+
page_style(:default, page_size: page_size, orientation: page_orientation) do |canvas, style|
|
|
149
|
+
style.frame = style.create_frame(canvas.context, margin)
|
|
150
|
+
end
|
|
151
|
+
new_page
|
|
152
|
+
end
|
|
133
153
|
yield(self) if block_given?
|
|
134
154
|
end
|
|
135
155
|
|
|
136
156
|
# Creates a new page, making it the current one.
|
|
137
157
|
#
|
|
138
|
-
#
|
|
139
|
-
# the
|
|
158
|
+
# The page style to use for the new page can be set via the +style+ argument. If not provided,
|
|
159
|
+
# the currently set page style is used.
|
|
160
|
+
#
|
|
161
|
+
# The used page style determines the page style that should be used for the following new pages.
|
|
162
|
+
# If this information is not provided, the used page style is used again.
|
|
140
163
|
#
|
|
141
164
|
# Examples:
|
|
142
165
|
#
|
|
143
|
-
# composer.
|
|
144
|
-
# composer.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
# composer.page_style(:cover, page_size: :A4).next_style = :content
|
|
167
|
+
# composer.page_style(:content, page_size: :A4)
|
|
168
|
+
# composer.new_page(:cover) # uses the :cover style, set next style to :content
|
|
169
|
+
# composer.new_page # uses the :content style, next style again :content
|
|
170
|
+
def new_page(style = @page_style)
|
|
171
|
+
page_style = @page_styles.fetch(style) do |key|
|
|
172
|
+
raise ArgumentError, "Page style #{key} has not been defined"
|
|
173
|
+
end
|
|
174
|
+
@page = @document.pages.add(page_style.create_page(@document))
|
|
151
175
|
@canvas = @page.canvas
|
|
152
|
-
|
|
176
|
+
@frame = page_style.frame
|
|
177
|
+
@page_style = page_style.next_style || style
|
|
153
178
|
end
|
|
154
179
|
|
|
155
180
|
# The x-position of the cursor inside the current frame.
|
|
@@ -190,6 +215,40 @@ module HexaPDF
|
|
|
190
215
|
@document.layout.style(name, base: base, **properties)
|
|
191
216
|
end
|
|
192
217
|
|
|
218
|
+
# :call-seq:
|
|
219
|
+
# composer.page_style(name) -> page_style
|
|
220
|
+
# composer.page_style(name, **attributes, &template_block) -> page_style
|
|
221
|
+
#
|
|
222
|
+
# Creates and/or returns the page style +name+.
|
|
223
|
+
#
|
|
224
|
+
# If no attributes are given, the page style +name+ is returned. In case it does not exist,
|
|
225
|
+
# +nil+ is returned.
|
|
226
|
+
#
|
|
227
|
+
# If one or more page style attributes are given, a new HexaPDF::Layout::PageStyle object with
|
|
228
|
+
# those attribute values is created, stored under +name+ and returned. If a block is provided,
|
|
229
|
+
# it is used to define the page template.
|
|
230
|
+
#
|
|
231
|
+
# Example:
|
|
232
|
+
#
|
|
233
|
+
# composer.page_style(:default)
|
|
234
|
+
# composer.page_style(:cover, page_size: :A4) do |canvas, style|
|
|
235
|
+
# page_box = canvas.context.box
|
|
236
|
+
# canvas.fill_color("fd0") do
|
|
237
|
+
# canvas.rectangle(0, 0, page_box.width, page_box.height).
|
|
238
|
+
# fill
|
|
239
|
+
# end
|
|
240
|
+
# style.frame = style.create_frame(canvas.context, 36)
|
|
241
|
+
# end
|
|
242
|
+
#
|
|
243
|
+
# See: HexaPDF::Layout::PageStyle
|
|
244
|
+
def page_style(name, **attributes, &block)
|
|
245
|
+
if attributes.empty?
|
|
246
|
+
@page_styles[name]
|
|
247
|
+
else
|
|
248
|
+
@page_styles[name] = HexaPDF::Layout::PageStyle.new(**attributes, &block)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
193
252
|
# Draws the given text at the current position into the current frame.
|
|
194
253
|
#
|
|
195
254
|
# The text will be positioned at the current position if possible. Otherwise the next best
|
|
@@ -325,17 +384,6 @@ module HexaPDF
|
|
|
325
384
|
stamp
|
|
326
385
|
end
|
|
327
386
|
|
|
328
|
-
private
|
|
329
|
-
|
|
330
|
-
# Creates the frame into which boxes are layed out when a new page is created.
|
|
331
|
-
def create_frame
|
|
332
|
-
media_box = @page.box
|
|
333
|
-
@frame = Layout::Frame.new(media_box.left + @margin.left,
|
|
334
|
-
media_box.bottom + @margin.bottom,
|
|
335
|
-
media_box.width - @margin.left - @margin.right,
|
|
336
|
-
media_box.height - @margin.bottom - @margin.top)
|
|
337
|
-
end
|
|
338
|
-
|
|
339
387
|
end
|
|
340
388
|
|
|
341
389
|
end
|
|
@@ -350,6 +350,9 @@ module HexaPDF
|
|
|
350
350
|
# The media box that is used for new pages that don't define a media box. Default value is
|
|
351
351
|
# A4. See HexaPDF::Type::Page::PAPER_SIZE for a list of predefined paper sizes.
|
|
352
352
|
#
|
|
353
|
+
# This configuration option (together with 'page.default_media_orientation') is also used when
|
|
354
|
+
# validating pages and a page without a media box is found.
|
|
355
|
+
#
|
|
353
356
|
# The value can either be a rectangle defining the paper size or a Symbol referencing one of
|
|
354
357
|
# the predefined paper sizes.
|
|
355
358
|
#
|
|
@@ -511,11 +514,20 @@ module HexaPDF
|
|
|
511
514
|
#
|
|
512
515
|
# See PDF1.7 s8.6
|
|
513
516
|
#
|
|
514
|
-
# filter.
|
|
517
|
+
# filter.flate.compression::
|
|
515
518
|
# Specifies the compression level that should be used with the FlateDecode filter. The level
|
|
516
519
|
# can range from 0 (no compression), 1 (best speed) to 9 (best compression, default).
|
|
517
520
|
#
|
|
518
|
-
# filter.
|
|
521
|
+
# filter.flate.on_error::
|
|
522
|
+
# Callback hook when a potentially recoverable Zlib error occurs in the FlateDecode filter.
|
|
523
|
+
#
|
|
524
|
+
# The value needs to be an object that responds to \#call(stream, error) where stream is the
|
|
525
|
+
# Zlib stream object and error is the thrown error. The method needs to return +true+ if an
|
|
526
|
+
# error should be raised.
|
|
527
|
+
#
|
|
528
|
+
# The default implementation prevents errors from being raised.
|
|
529
|
+
#
|
|
530
|
+
# filter.flate.memory::
|
|
519
531
|
# Specifies the memory level that should be used with the FlateDecode filter. The level can
|
|
520
532
|
# range from 1 (minimum memory usage; slow, reduces compression) to 9 (maximum memory usage).
|
|
521
533
|
#
|
|
@@ -543,8 +555,9 @@ module HexaPDF
|
|
|
543
555
|
# This mapping is used to provide automatic wrapping of objects in the HexaPDF::Document#wrap
|
|
544
556
|
# method.
|
|
545
557
|
GlobalConfiguration =
|
|
546
|
-
Configuration.new('filter.
|
|
547
|
-
'filter.
|
|
558
|
+
Configuration.new('filter.flate.compression' => 9,
|
|
559
|
+
'filter.flate.on_error' => proc { false },
|
|
560
|
+
'filter.flate.memory' => 6,
|
|
548
561
|
'filter.predictor.strict' => false,
|
|
549
562
|
'color_space.map' => {
|
|
550
563
|
DeviceRGB: 'HexaPDF::Content::ColorSpace::DeviceRGB',
|
|
@@ -94,11 +94,7 @@ module HexaPDF
|
|
|
94
94
|
def create(media_box: nil, orientation: nil)
|
|
95
95
|
media_box ||= @document.config['page.default_media_box']
|
|
96
96
|
orientation ||= @document.config['page.default_media_orientation']
|
|
97
|
-
box =
|
|
98
|
-
media_box
|
|
99
|
-
else
|
|
100
|
-
Type::Page.media_box(media_box, orientation: orientation)
|
|
101
|
-
end
|
|
97
|
+
box = Type::Page.media_box(media_box, orientation: orientation)
|
|
102
98
|
@document.add({Type: :Page, MediaBox: box})
|
|
103
99
|
end
|
|
104
100
|
|
data/lib/hexapdf/document.rb
CHANGED
|
@@ -325,7 +325,14 @@ module HexaPDF
|
|
|
325
325
|
type = (klass <= HexaPDF::Dictionary ? klass.type : nil)
|
|
326
326
|
else
|
|
327
327
|
type ||= deref(data.value[:Type]) if data.value.kind_of?(Hash)
|
|
328
|
-
|
|
328
|
+
if type
|
|
329
|
+
klass = GlobalConfiguration.constantize('object.type_map', type) { nil }
|
|
330
|
+
if (type == :ObjStm || type == :XRef) &&
|
|
331
|
+
klass.each_field.any? {|name, field| field.required? && !data.value.key?(name) }
|
|
332
|
+
data.value.delete(:Type)
|
|
333
|
+
klass = nil
|
|
334
|
+
end
|
|
335
|
+
end
|
|
329
336
|
end
|
|
330
337
|
|
|
331
338
|
if data.value.kind_of?(Hash)
|
|
@@ -55,21 +55,33 @@ module HexaPDF
|
|
|
55
55
|
def self.decoder(source, options = nil)
|
|
56
56
|
fib = Fiber.new do
|
|
57
57
|
inflater = Zlib::Inflate.new
|
|
58
|
+
error_raised = nil
|
|
59
|
+
|
|
58
60
|
while source.alive? && (data = source.resume)
|
|
59
61
|
next if data.empty?
|
|
60
62
|
begin
|
|
61
|
-
|
|
62
|
-
rescue
|
|
63
|
-
|
|
63
|
+
Fiber.yield(inflater.inflate(data))
|
|
64
|
+
rescue Zlib::DataError, Zlib::BufError => e
|
|
65
|
+
# Only swallow the error if it appears at the end of the stream
|
|
66
|
+
if error_raised || HexaPDF::GlobalConfiguration['filter.flate.on_error'].call(inflater, e)
|
|
67
|
+
raise FilterError, "Problem while decoding Flate encoded stream: #{e}"
|
|
68
|
+
else
|
|
69
|
+
Fiber.yield(inflater.flush_next_out)
|
|
70
|
+
error_raised = e
|
|
71
|
+
end
|
|
64
72
|
end
|
|
65
|
-
Fiber.yield(data)
|
|
66
73
|
end
|
|
74
|
+
|
|
67
75
|
begin
|
|
68
76
|
data = inflater.total_in == 0 || (data = inflater.finish).empty? ? nil : data
|
|
69
77
|
inflater.close
|
|
70
78
|
data
|
|
71
|
-
rescue
|
|
72
|
-
|
|
79
|
+
rescue Zlib::DataError, Zlib::BufError => e
|
|
80
|
+
if HexaPDF::GlobalConfiguration['filter.flate.on_error'].call(inflater, e)
|
|
81
|
+
raise FilterError, "Problem while decoding Flate encoded stream: #{e}"
|
|
82
|
+
else
|
|
83
|
+
Fiber.yield(inflater.flush_next_out)
|
|
84
|
+
end
|
|
73
85
|
end
|
|
74
86
|
end
|
|
75
87
|
|
|
@@ -87,9 +99,9 @@ module HexaPDF
|
|
|
87
99
|
end
|
|
88
100
|
|
|
89
101
|
Fiber.new do
|
|
90
|
-
deflater = Zlib::Deflate.new(HexaPDF::GlobalConfiguration['filter.
|
|
102
|
+
deflater = Zlib::Deflate.new(HexaPDF::GlobalConfiguration['filter.flate.compression'],
|
|
91
103
|
Zlib::MAX_WBITS,
|
|
92
|
-
HexaPDF::GlobalConfiguration['filter.
|
|
104
|
+
HexaPDF::GlobalConfiguration['filter.flate.memory'])
|
|
93
105
|
while source.alive? && (data = source.resume)
|
|
94
106
|
data = deflater.deflate(data)
|
|
95
107
|
Fiber.yield(data)
|
|
@@ -0,0 +1,144 @@
|
|
|
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-2023 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/error'
|
|
38
|
+
require 'hexapdf/layout/style'
|
|
39
|
+
require 'hexapdf/layout/frame'
|
|
40
|
+
|
|
41
|
+
module HexaPDF
|
|
42
|
+
module Layout
|
|
43
|
+
|
|
44
|
+
# A PageStyle defines the initial look of a page and the placement of one or more frames.
|
|
45
|
+
class PageStyle
|
|
46
|
+
|
|
47
|
+
# The page size.
|
|
48
|
+
#
|
|
49
|
+
# Can be any valid predefined page size (see HexaPDF::Type::Page::PAPER_SIZE) or an array
|
|
50
|
+
# [llx, lly, urx, ury] specifying a custom page size.
|
|
51
|
+
#
|
|
52
|
+
# Example:
|
|
53
|
+
#
|
|
54
|
+
# style.page_size = :A4
|
|
55
|
+
# style.page_size = [0, 0, 200, 200]
|
|
56
|
+
attr_accessor :page_size
|
|
57
|
+
|
|
58
|
+
# The page orientation, either +:portrait+ or +:landscape+.
|
|
59
|
+
#
|
|
60
|
+
# Only used if #page_size is one of the predefined page sizes and not an array.
|
|
61
|
+
attr_accessor :orientation
|
|
62
|
+
|
|
63
|
+
# A callable object that defines the initial content of a page created with #create_page.
|
|
64
|
+
#
|
|
65
|
+
# The callable object is given a canvas and the page style as arguments. It needs to draw the
|
|
66
|
+
# initial content of the page. Note that the graphics state of the canvas is *not* saved
|
|
67
|
+
# before executing the template code and restored afterwards. If this is needed, the object
|
|
68
|
+
# needs to do it itself.
|
|
69
|
+
#
|
|
70
|
+
# Furthermore it should set the #frame and #next_style attributes appropriately, if not done
|
|
71
|
+
# beforehand. The #create_frame method can be used for easily creating a rectangular frame.
|
|
72
|
+
#
|
|
73
|
+
# Example:
|
|
74
|
+
#
|
|
75
|
+
# page_style.template = lambda do |canvas, style
|
|
76
|
+
# box = canvas.context.box
|
|
77
|
+
# canvas.fill_color("fd0") do
|
|
78
|
+
# canvas.rectangle(0, 0, box.width, box.height).fill
|
|
79
|
+
# end
|
|
80
|
+
# style.frame = style.create_frame(canvas.context, 72)
|
|
81
|
+
# end
|
|
82
|
+
attr_accessor :template
|
|
83
|
+
|
|
84
|
+
# The HexaPDF::Layout::Frame object that defines the area on the page where content should be
|
|
85
|
+
# placed.
|
|
86
|
+
#
|
|
87
|
+
# This can either be set beforehand or during execution of the #template.
|
|
88
|
+
#
|
|
89
|
+
# If no frame has been set, a frame covering the page except for a default margin on all sides
|
|
90
|
+
# is set during #create_page.
|
|
91
|
+
attr_accessor :frame
|
|
92
|
+
|
|
93
|
+
# Defines the name of the page style that should be used for the next page.
|
|
94
|
+
#
|
|
95
|
+
# If this attribute is +nil+ (the default), it means that this style should be used again.
|
|
96
|
+
attr_accessor :next_style
|
|
97
|
+
|
|
98
|
+
# Creates a new page style instance for the given page size and orientation. If a block is
|
|
99
|
+
# given, it is used as template for defining the initial content.
|
|
100
|
+
#
|
|
101
|
+
# Example:
|
|
102
|
+
#
|
|
103
|
+
# PageStyle.new(page_size: :Letter) do |canvas, style|
|
|
104
|
+
# style.frame = style.create_frame(canvas.context, 72)
|
|
105
|
+
# style.next_style = :other
|
|
106
|
+
# canvas.fill_color("fd0") { canvas.circle(100, 100, 50).fill }
|
|
107
|
+
# end
|
|
108
|
+
def initialize(page_size: :A4, orientation: :portrait, &block)
|
|
109
|
+
@page_size = page_size
|
|
110
|
+
@orientation = orientation
|
|
111
|
+
@template = block
|
|
112
|
+
@frame = nil
|
|
113
|
+
@next_style = nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Creates a new page in the given document with this page style and returns it.
|
|
117
|
+
#
|
|
118
|
+
# If #frame has not been set beforehand or during execution of the #template, a default frame
|
|
119
|
+
# covering the whole page except a margin of 36 is created.
|
|
120
|
+
def create_page(document)
|
|
121
|
+
page = document.pages.create(media_box: page_size, orientation: orientation)
|
|
122
|
+
template&.call(page.canvas, self)
|
|
123
|
+
self.frame ||= create_frame(page, 36)
|
|
124
|
+
page
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Creates a frame based on the given page's box and margin.
|
|
128
|
+
#
|
|
129
|
+
# The +margin+ can be any value allowed by HexaPDF::Layout::Style::Quad#set.
|
|
130
|
+
#
|
|
131
|
+
# *Note*: This is a helper method for use inside the #template callable.
|
|
132
|
+
def create_frame(page, margin = 36)
|
|
133
|
+
box = page.box
|
|
134
|
+
margin = Layout::Style::Quad.new(margin)
|
|
135
|
+
Layout::Frame.new(box.left + margin.left,
|
|
136
|
+
box.bottom + margin.bottom,
|
|
137
|
+
box.width - margin.left - margin.right,
|
|
138
|
+
box.height - margin.bottom - margin.top)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
end
|
|
144
|
+
end
|
data/lib/hexapdf/layout.rb
CHANGED
|
@@ -38,6 +38,8 @@ require 'set'
|
|
|
38
38
|
require 'hexapdf/serializer'
|
|
39
39
|
require 'hexapdf/content/parser'
|
|
40
40
|
require 'hexapdf/content/operator'
|
|
41
|
+
require 'hexapdf/type/xref_stream'
|
|
42
|
+
require 'hexapdf/type/object_stream'
|
|
41
43
|
|
|
42
44
|
module HexaPDF
|
|
43
45
|
module Task
|
|
@@ -124,7 +126,7 @@ module HexaPDF
|
|
|
124
126
|
if object_streams == :generate
|
|
125
127
|
process_object_streams(doc, :generate, xref_streams)
|
|
126
128
|
elsif xref_streams == :generate
|
|
127
|
-
doc.add({
|
|
129
|
+
doc.add({}, type: Type::XRefStream)
|
|
128
130
|
end
|
|
129
131
|
end
|
|
130
132
|
|
|
@@ -150,14 +152,14 @@ module HexaPDF
|
|
|
150
152
|
end
|
|
151
153
|
objects_to_delete.each {|obj| rev.delete(obj) }
|
|
152
154
|
if xref_streams == :generate && !xref_stream
|
|
153
|
-
rev.add(doc.wrap({
|
|
155
|
+
rev.add(doc.wrap({}, type: Type::XRefStream, oid: doc.revisions.next_oid))
|
|
154
156
|
end
|
|
155
157
|
end
|
|
156
158
|
when :generate
|
|
157
159
|
doc.revisions.each do |rev|
|
|
158
160
|
xref_stream = false
|
|
159
161
|
count = 0
|
|
160
|
-
objstms = [doc.wrap({
|
|
162
|
+
objstms = [doc.wrap({}, type: Type::ObjectStream)]
|
|
161
163
|
old_objstms = []
|
|
162
164
|
rev.each do |obj|
|
|
163
165
|
case obj.type
|
|
@@ -173,7 +175,7 @@ module HexaPDF
|
|
|
173
175
|
objstms[-1].add_object(obj)
|
|
174
176
|
count += 1
|
|
175
177
|
if count == 200
|
|
176
|
-
objstms << doc.wrap({
|
|
178
|
+
objstms << doc.wrap({}, type: Type::ObjectStream)
|
|
177
179
|
count = 0
|
|
178
180
|
end
|
|
179
181
|
end
|
|
@@ -182,7 +184,7 @@ module HexaPDF
|
|
|
182
184
|
objstm.data.oid = doc.revisions.next_oid
|
|
183
185
|
rev.add(objstm)
|
|
184
186
|
end
|
|
185
|
-
rev.add(doc.wrap({
|
|
187
|
+
rev.add(doc.wrap({}, type: Type::XRefStream, oid: doc.revisions.next_oid)) unless xref_stream
|
|
186
188
|
end
|
|
187
189
|
end
|
|
188
190
|
end
|
|
@@ -207,7 +209,7 @@ module HexaPDF
|
|
|
207
209
|
xref_stream = true if obj.type == :XRef
|
|
208
210
|
delete_fields_with_defaults(obj)
|
|
209
211
|
end
|
|
210
|
-
rev.add(doc.wrap({
|
|
212
|
+
rev.add(doc.wrap({}, type: Type::XRefStream, oid: doc.revisions.next_oid)) unless xref_stream
|
|
211
213
|
end
|
|
212
214
|
end
|
|
213
215
|
end
|
|
@@ -101,8 +101,8 @@ module HexaPDF
|
|
|
101
101
|
define_type :ObjStm
|
|
102
102
|
|
|
103
103
|
define_field :Type, type: Symbol, required: true, default: type, version: '1.5'
|
|
104
|
-
define_field :N, type: Integer
|
|
105
|
-
define_field :First, type: Integer
|
|
104
|
+
define_field :N, type: Integer, required: true
|
|
105
|
+
define_field :First, type: Integer, required: true
|
|
106
106
|
define_field :Extends, type: Stream
|
|
107
107
|
|
|
108
108
|
# Parses the stream and returns an ObjectStream::Data object that can be used for retrieving
|
|
@@ -230,6 +230,11 @@ module HexaPDF
|
|
|
230
230
|
|
|
231
231
|
# Validates that the generation number of the object stream is zero.
|
|
232
232
|
def perform_validation
|
|
233
|
+
# Assign dummy values so that the validation for required values works since those values
|
|
234
|
+
# are only set on #write_objects
|
|
235
|
+
self[:N] ||= 0
|
|
236
|
+
self[:First] ||= 0
|
|
237
|
+
|
|
233
238
|
super
|
|
234
239
|
yield("Object stream has invalid generation number > 0", false) if gen != 0
|
|
235
240
|
end
|
data/lib/hexapdf/type/outline.rb
CHANGED
|
@@ -126,7 +126,7 @@ module HexaPDF
|
|
|
126
126
|
if (first && !last) || (!first && last)
|
|
127
127
|
yield('Outline dictionary is missing an endpoint reference', true)
|
|
128
128
|
node, dir = first ? [first, :Next] : [last, :Prev]
|
|
129
|
-
node = node[dir] while node
|
|
129
|
+
node = node[dir] while node[dir]
|
|
130
130
|
self[dir == :Next ? :Last : :First] = node
|
|
131
131
|
elsif !first && !last && self[:Count] && self[:Count] != 0
|
|
132
132
|
yield('Outline dictionary key /Count set but no items exist', true)
|
|
@@ -397,7 +397,7 @@ module HexaPDF
|
|
|
397
397
|
if (first && !last) || (!first && last)
|
|
398
398
|
yield('Outline item dictionary is missing an endpoint reference', true)
|
|
399
399
|
node, dir = first ? [first, :Next] : [last, :Prev]
|
|
400
|
-
node = node[dir] while node
|
|
400
|
+
node = node[dir] while node[dir]
|
|
401
401
|
self[dir == :Next ? :Last : :First] = node
|
|
402
402
|
elsif !first && !last && self[:Count] && self[:Count] != 0
|
|
403
403
|
yield('Outline item dictionary key /Count set but no descendants exist', true)
|
data/lib/hexapdf/type/page.rb
CHANGED
|
@@ -104,8 +104,16 @@ module HexaPDF
|
|
|
104
104
|
Executive: [0, 0, 522, 756].freeze,
|
|
105
105
|
}.freeze
|
|
106
106
|
|
|
107
|
-
# Returns the media box for the given paper size
|
|
107
|
+
# Returns the media box for the given paper size or array.
|
|
108
|
+
#
|
|
109
|
+
# If an array is specified, it needs to contain exactly four numbers. The +orientation+
|
|
110
|
+
# argument is not used in this case.
|
|
111
|
+
#
|
|
112
|
+
# See PAPER_SIZE for the defined paper sizes.
|
|
108
113
|
def self.media_box(paper_size, orientation: :portrait)
|
|
114
|
+
return paper_size if paper_size.kind_of?(Array) && paper_size.size == 4 &&
|
|
115
|
+
paper_size.all?(Numeric)
|
|
116
|
+
|
|
109
117
|
unless PAPER_SIZE.key?(paper_size)
|
|
110
118
|
raise HexaPDF::Error, "Invalid paper size specified: #{paper_size}"
|
|
111
119
|
end
|
|
@@ -118,9 +126,6 @@ module HexaPDF
|
|
|
118
126
|
# The inheritable fields.
|
|
119
127
|
INHERITABLE_FIELDS = [:Resources, :MediaBox, :CropBox, :Rotate].freeze
|
|
120
128
|
|
|
121
|
-
# The required inheritable fields.
|
|
122
|
-
REQUIRED_INHERITABLE_FIELDS = [:Resources, :MediaBox].freeze
|
|
123
|
-
|
|
124
129
|
define_type :Page
|
|
125
130
|
|
|
126
131
|
define_field :Type, type: Symbol, required: true, default: type
|
|
@@ -609,10 +614,26 @@ module HexaPDF
|
|
|
609
614
|
return unless parent_node
|
|
610
615
|
|
|
611
616
|
super
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
yield("
|
|
615
|
-
resources.validate(&block)
|
|
617
|
+
|
|
618
|
+
unless self[:Resources]
|
|
619
|
+
yield("Required inheritable page field Resources not set", true)
|
|
620
|
+
resources.validate(&block)
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
unless self[:MediaBox]
|
|
624
|
+
yield("Required inheritable page field MediaBox not set", true)
|
|
625
|
+
index = self.index
|
|
626
|
+
box_before = index == 0 ? nil : document.pages[index - 1][:MediaBox]
|
|
627
|
+
box_after = index == document.pages.count - 1 ? nil : document.pages[index + 1]&.[](:MediaBox)
|
|
628
|
+
self[:MediaBox] =
|
|
629
|
+
if box_before && (box_before&.value == box_after&.value || box_after.nil?)
|
|
630
|
+
box_before.dup
|
|
631
|
+
elsif box_after && box_before.nil?
|
|
632
|
+
box_after
|
|
633
|
+
else
|
|
634
|
+
self.class.media_box(document.config['page.default_media_box'],
|
|
635
|
+
orientation: document.config['page.default_media_orientation'])
|
|
636
|
+
end
|
|
616
637
|
end
|
|
617
638
|
end
|
|
618
639
|
|
|
@@ -72,12 +72,10 @@ module HexaPDF
|
|
|
72
72
|
|
|
73
73
|
define_field :Type, type: Symbol, default: type, required: true, indirect: false,
|
|
74
74
|
version: '1.5'
|
|
75
|
-
|
|
76
|
-
define_field :Size, type: Integer, indirect: false
|
|
75
|
+
define_field :Size, type: Integer, indirect: false, required: true
|
|
77
76
|
define_field :Index, type: PDFArray, indirect: false
|
|
78
77
|
define_field :Prev, type: Integer, indirect: false
|
|
79
|
-
|
|
80
|
-
define_field :W, type: PDFArray, indirect: false
|
|
78
|
+
define_field :W, type: PDFArray, indirect: false, required: true
|
|
81
79
|
|
|
82
80
|
# Returns an XRefSection that represents the content of this cross-reference stream.
|
|
83
81
|
#
|
|
@@ -219,6 +217,15 @@ module HexaPDF
|
|
|
219
217
|
[[1, middle, 2], pack_string]
|
|
220
218
|
end
|
|
221
219
|
|
|
220
|
+
def perform_validation #:nodoc
|
|
221
|
+
# Size is not required because it will be auto-filled before the object is written
|
|
222
|
+
# W is not required because it will be auto-filled on #update_with_xref_section_and_trailer
|
|
223
|
+
# Set both here to dummy values to make validation work for the required values
|
|
224
|
+
self[:Size] ||= 1
|
|
225
|
+
self[:W] ||= [1, 1, 1]
|
|
226
|
+
super
|
|
227
|
+
end
|
|
228
|
+
|
|
222
229
|
end
|
|
223
230
|
|
|
224
231
|
end
|
data/lib/hexapdf/version.rb
CHANGED
data/lib/hexapdf/writer.rb
CHANGED
|
@@ -207,7 +207,7 @@ module HexaPDF
|
|
|
207
207
|
end
|
|
208
208
|
|
|
209
209
|
if (!object_streams.empty? || @use_xref_streams) && xref_stream.nil?
|
|
210
|
-
xref_stream = @document.wrap({
|
|
210
|
+
xref_stream = @document.wrap({}, type: Type::XRefStream, oid: @document.revisions.next_oid)
|
|
211
211
|
rev.add(xref_stream)
|
|
212
212
|
end
|
|
213
213
|
|
|
@@ -26,12 +26,26 @@ describe HexaPDF::Filter::FlateDecode do
|
|
|
26
26
|
assert_equal(@decoded, collector(@obj.decoder(feeder(@encoded_predictor), @predictor_opts)))
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
describe "invalid input is handled as good as possible" do
|
|
30
|
+
def strict_mode
|
|
31
|
+
HexaPDF::GlobalConfiguration['filter.flate.on_error'] = proc { true }
|
|
32
|
+
yield
|
|
33
|
+
ensure
|
|
34
|
+
HexaPDF::GlobalConfiguration['filter.flate.on_error'] = proc { false }
|
|
32
35
|
end
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
|
|
37
|
+
it "handles completely invalid data" do
|
|
38
|
+
assert_equal('', collector(@obj.decoder(feeder("some data"))))
|
|
39
|
+
assert_raises(HexaPDF::FilterError) do
|
|
40
|
+
strict_mode { collector(@obj.decoder(feeder("some data"))) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "handles missing data" do
|
|
45
|
+
assert_equal('abcdefg', collector(@obj.decoder(feeder(@encoded[0..-2]))))
|
|
46
|
+
assert_raises(HexaPDF::FilterError) do
|
|
47
|
+
strict_mode { collector(@obj.decoder(feeder(@encoded[0..-2]))) }
|
|
48
|
+
end
|
|
35
49
|
end
|
|
36
50
|
end
|
|
37
51
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
require 'hexapdf/layout/page_style'
|
|
5
|
+
require 'hexapdf/document'
|
|
6
|
+
|
|
7
|
+
describe HexaPDF::Layout::PageStyle do
|
|
8
|
+
it "allows assigning the page size, orientation and template on initialization" do
|
|
9
|
+
block = lambda {}
|
|
10
|
+
style = HexaPDF::Layout::PageStyle.new(page_size: :A3, orientation: :landscape, &block)
|
|
11
|
+
assert_equal(:A3, style.page_size)
|
|
12
|
+
assert_equal(:landscape, style.orientation)
|
|
13
|
+
assert_same(block, style.template)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "uses defaults for all values" do
|
|
17
|
+
style = HexaPDF::Layout::PageStyle.new
|
|
18
|
+
assert_equal(:A4, style.page_size)
|
|
19
|
+
assert_equal(:portrait, style.orientation)
|
|
20
|
+
assert_nil(style.template)
|
|
21
|
+
assert_nil(style.frame)
|
|
22
|
+
assert_nil(style.next_style)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe "create_page" do
|
|
26
|
+
before do
|
|
27
|
+
@doc = HexaPDF::Document.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "creates a new page object" do
|
|
31
|
+
style = HexaPDF::Layout::PageStyle.new do |canvas, istyle|
|
|
32
|
+
canvas.rectangle(0, 0, 10, 10).stroke
|
|
33
|
+
istyle.frame = :frame
|
|
34
|
+
istyle.next_style = :other
|
|
35
|
+
end
|
|
36
|
+
page = style.create_page(@doc)
|
|
37
|
+
assert_equal([0, 0, 595, 842], page.box(:media))
|
|
38
|
+
assert_equal("0 0 10 10 re\nS\n", page.contents)
|
|
39
|
+
assert_equal(:frame, style.frame)
|
|
40
|
+
assert_equal(:other, style.next_style)
|
|
41
|
+
assert_equal(0, @doc.pages.count)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "works when no template is set" do
|
|
45
|
+
style = HexaPDF::Layout::PageStyle.new
|
|
46
|
+
page = style.create_page(@doc)
|
|
47
|
+
assert_equal("", page.contents)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "creates a default frame if none is set beforehand or during template execution" do
|
|
51
|
+
style = HexaPDF::Layout::PageStyle.new
|
|
52
|
+
style.create_page(@doc)
|
|
53
|
+
assert_kind_of(HexaPDF::Layout::Frame, style.frame)
|
|
54
|
+
assert_equal(36, style.frame.left)
|
|
55
|
+
assert_equal(36, style.frame.bottom)
|
|
56
|
+
assert_equal(523, style.frame.width)
|
|
57
|
+
assert_equal(770, style.frame.height)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "creates new frame objects given a page and a margin specification" do
|
|
62
|
+
doc = HexaPDF::Document.new
|
|
63
|
+
style = HexaPDF::Layout::PageStyle.new
|
|
64
|
+
frame = style.create_frame(style.create_page(doc), [15, 10])
|
|
65
|
+
assert_equal(10, frame.left)
|
|
66
|
+
assert_equal(15, frame.bottom)
|
|
67
|
+
assert_equal(575, frame.width)
|
|
68
|
+
assert_equal(812, frame.height)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -81,8 +81,10 @@ describe HexaPDF::Task::Optimize do
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
it "compacts and deletes xref streams" do
|
|
84
|
-
@doc.revisions.all[0].add(@doc.wrap({
|
|
85
|
-
|
|
84
|
+
@doc.revisions.all[0].add(@doc.wrap({}, type: HexaPDF::Type::XRefStream,
|
|
85
|
+
oid: @doc.revisions.next_oid))
|
|
86
|
+
@doc.revisions.all[1].add(@doc.wrap({}, type: HexaPDF::Type::XRefStream,
|
|
87
|
+
oid: @doc.revisions.next_oid))
|
|
86
88
|
@doc.task(:optimize, compact: true, xref_streams: :delete)
|
|
87
89
|
assert_no_xrefstms
|
|
88
90
|
assert_default_deleted
|
|
@@ -92,8 +94,8 @@ describe HexaPDF::Task::Optimize do
|
|
|
92
94
|
describe "object_streams" do
|
|
93
95
|
def reload_document_with_objstm_from_io
|
|
94
96
|
io = StringIO.new
|
|
95
|
-
objstm = @doc.add({
|
|
96
|
-
@doc.add({
|
|
97
|
+
objstm = @doc.add({}, type: HexaPDF::Type::ObjectStream)
|
|
98
|
+
@doc.add({}, type: HexaPDF::Type::XRefStream)
|
|
97
99
|
objstm.add_object(@doc.add({Type: :Test}))
|
|
98
100
|
@doc.write(io)
|
|
99
101
|
io.rewind
|
|
@@ -102,7 +104,7 @@ describe HexaPDF::Task::Optimize do
|
|
|
102
104
|
|
|
103
105
|
it "generates object streams" do
|
|
104
106
|
210.times { @doc.add(5) }
|
|
105
|
-
objstm = @doc.add({
|
|
107
|
+
objstm = @doc.add({}, type: HexaPDF::Type::ObjectStream)
|
|
106
108
|
reload_document_with_objstm_from_io
|
|
107
109
|
@doc.task(:optimize, object_streams: :generate)
|
|
108
110
|
assert_objstms_generated
|
|
@@ -122,8 +124,8 @@ describe HexaPDF::Task::Optimize do
|
|
|
122
124
|
end
|
|
123
125
|
|
|
124
126
|
it "deletes object and generates xref streams" do
|
|
125
|
-
@doc.add({
|
|
126
|
-
xref = @doc.add({
|
|
127
|
+
@doc.add({}, type: HexaPDF::Type::ObjectStream)
|
|
128
|
+
xref = @doc.add({}, type: HexaPDF::Type::XRefStream)
|
|
127
129
|
@doc.task(:optimize, object_streams: :delete, xref_streams: :generate)
|
|
128
130
|
assert_no_objstms
|
|
129
131
|
assert_xrefstms_generated
|
|
@@ -140,13 +142,13 @@ describe HexaPDF::Task::Optimize do
|
|
|
140
142
|
end
|
|
141
143
|
|
|
142
144
|
it "reuses an xref stream in generatation mode" do
|
|
143
|
-
@doc.add({
|
|
145
|
+
@doc.add({}, type: HexaPDF::Type::XRefStream)
|
|
144
146
|
@doc.task(:optimize, xref_streams: :generate)
|
|
145
147
|
assert_xrefstms_generated
|
|
146
148
|
end
|
|
147
149
|
|
|
148
150
|
it "deletes xref streams" do
|
|
149
|
-
@doc.add({
|
|
151
|
+
@doc.add({}, type: HexaPDF::Type::XRefStream)
|
|
150
152
|
@doc.task(:optimize, xref_streams: :delete)
|
|
151
153
|
assert_no_xrefstms
|
|
152
154
|
assert_default_deleted
|
|
@@ -39,6 +39,14 @@ describe HexaPDF::Composer do
|
|
|
39
39
|
assert_equal(682, composer.frame.height)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
it "allows skipping the initial page creation" do
|
|
43
|
+
composer = HexaPDF::Composer.new(skip_page_creation: true)
|
|
44
|
+
assert_nil(composer.page)
|
|
45
|
+
assert_nil(composer.canvas)
|
|
46
|
+
assert_nil(composer.frame)
|
|
47
|
+
assert_nil(composer.page_style(:default))
|
|
48
|
+
end
|
|
49
|
+
|
|
42
50
|
it "yields itself" do
|
|
43
51
|
yielded = nil
|
|
44
52
|
composer = HexaPDF::Composer.new {|c| yielded = c }
|
|
@@ -56,7 +64,7 @@ describe HexaPDF::Composer do
|
|
|
56
64
|
end
|
|
57
65
|
|
|
58
66
|
describe "new_page" do
|
|
59
|
-
it "creates a new page
|
|
67
|
+
it "creates a new page" do
|
|
60
68
|
c = HexaPDF::Composer.new(page_size: [0, 0, 50, 100], margin: 10)
|
|
61
69
|
c.new_page
|
|
62
70
|
assert_equal([0, 0, 50, 100], c.page.box.value)
|
|
@@ -64,16 +72,33 @@ describe HexaPDF::Composer do
|
|
|
64
72
|
assert_equal(10, c.frame.bottom)
|
|
65
73
|
end
|
|
66
74
|
|
|
67
|
-
it "uses the
|
|
68
|
-
@composer.
|
|
69
|
-
|
|
70
|
-
assert_equal(
|
|
71
|
-
|
|
75
|
+
it "uses the named page style for the new page" do
|
|
76
|
+
@composer.page_style(:other, page_size: [0, 0, 100, 100])
|
|
77
|
+
@composer.new_page(:other)
|
|
78
|
+
assert_equal([0, 0, 100, 100], @composer.page.box.value)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "sets the next page's style to the next_style value of the used page style" do
|
|
82
|
+
@composer.page_style(:one, page_size: [0, 0, 1, 1]).next_style = :two
|
|
83
|
+
@composer.page_style(:two, page_size: [0, 0, 2, 2]).next_style = :one
|
|
84
|
+
@composer.new_page(:one)
|
|
85
|
+
assert_equal([0, 0, 1, 1], @composer.page.box.value)
|
|
86
|
+
@composer.new_page
|
|
87
|
+
assert_equal([0, 0, 2, 2], @composer.page.box.value)
|
|
88
|
+
@composer.new_page
|
|
89
|
+
assert_equal([0, 0, 1, 1], @composer.page.box.value)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "uses the current page style for new pages if no next_style value is set" do
|
|
93
|
+
@composer.page_style(:one, page_size: [0, 0, 1, 1])
|
|
94
|
+
@composer.new_page(:one)
|
|
95
|
+
assert_equal([0, 0, 1, 1], @composer.page.box.value)
|
|
72
96
|
@composer.new_page
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
assert_equal([0, 0, 1, 1], @composer.page.box.value)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "fails if the specified page style has not been defined" do
|
|
101
|
+
assert_raises(ArgumentError) { @composer.new_page(:unknown) }
|
|
77
102
|
end
|
|
78
103
|
end
|
|
79
104
|
|
data/test/hexapdf/test_writer.rb
CHANGED
|
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
|
|
|
40
40
|
219
|
|
41
41
|
%%EOF
|
|
42
42
|
3 0 obj
|
|
43
|
-
<</Producer(HexaPDF version 0.
|
|
43
|
+
<</Producer(HexaPDF version 0.31.0)>>
|
|
44
44
|
endobj
|
|
45
45
|
xref
|
|
46
46
|
3 1
|
|
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
|
|
|
72
72
|
141
|
|
73
73
|
%%EOF
|
|
74
74
|
6 0 obj
|
|
75
|
-
<</Producer(HexaPDF version 0.
|
|
75
|
+
<</Producer(HexaPDF version 0.31.0)>>
|
|
76
76
|
endobj
|
|
77
77
|
2 0 obj
|
|
78
78
|
<</Length 10>>stream
|
|
@@ -171,7 +171,7 @@ describe HexaPDF::Writer do
|
|
|
171
171
|
|
|
172
172
|
it "creates an xref stream if no xref stream is in a revision but object streams are" do
|
|
173
173
|
document = HexaPDF::Document.new
|
|
174
|
-
document.add({
|
|
174
|
+
document.add({}, type: HexaPDF::Type::ObjectStream)
|
|
175
175
|
HexaPDF::Writer.new(document, StringIO.new).write
|
|
176
176
|
assert_equal(:XRef, document.object(4).type)
|
|
177
177
|
end
|
|
@@ -184,7 +184,7 @@ describe HexaPDF::Writer do
|
|
|
184
184
|
|
|
185
185
|
document = HexaPDF::Document.new(io: io)
|
|
186
186
|
document.pages.add
|
|
187
|
-
document.add({
|
|
187
|
+
document.add({}, type: HexaPDF::Type::ObjectStream)
|
|
188
188
|
io2 = StringIO.new
|
|
189
189
|
HexaPDF::Writer.new(document, io2).write_incremental
|
|
190
190
|
|
|
@@ -214,7 +214,7 @@ describe HexaPDF::Writer do
|
|
|
214
214
|
<</Type/Page/MediaBox[0 0 595 842]/Parent 2 0 R/Resources<<>>>>
|
|
215
215
|
endobj
|
|
216
216
|
5 0 obj
|
|
217
|
-
<</Producer(HexaPDF version 0.
|
|
217
|
+
<</Producer(HexaPDF version 0.31.0)>>
|
|
218
218
|
endobj
|
|
219
219
|
4 0 obj
|
|
220
220
|
<</Root 1 0 R/Info 5 0 R/Size 6/Type/XRef/W[1 1 2]/Index[0 6]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 33>>stream
|
|
@@ -123,12 +123,21 @@ describe HexaPDF::Type::ObjectStream do
|
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
describe "perform_validation" do
|
|
127
|
+
it "fails validation if gen != 0" do
|
|
128
|
+
assert(@obj.validate(auto_correct: false))
|
|
129
|
+
@obj.gen = 1
|
|
130
|
+
refute(@obj.validate(auto_correct: false) do |msg, correctable|
|
|
131
|
+
assert_match(/invalid generation/, msg)
|
|
132
|
+
refute(correctable)
|
|
133
|
+
end)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "sets the /N and /First entries to dummy values so that validation works" do
|
|
137
|
+
@obj = HexaPDF::Type::ObjectStream.new({}, oid: 1, document: @doc)
|
|
138
|
+
assert(@obj.validate(auto_correct: false))
|
|
139
|
+
assert_equal(0, @obj[:N])
|
|
140
|
+
assert_equal(0, @obj[:First])
|
|
141
|
+
end
|
|
133
142
|
end
|
|
134
143
|
end
|
|
@@ -30,11 +30,12 @@ describe HexaPDF::Type::Outline do
|
|
|
30
30
|
|
|
31
31
|
describe "perform_validation" do
|
|
32
32
|
before do
|
|
33
|
-
5.times { @outline.add_item("Test1") }
|
|
33
|
+
@outline_items = 5.times.map { @outline.add_item("Test1") }
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
it "fixes a missing /First entry" do
|
|
37
37
|
@outline.delete(:First)
|
|
38
|
+
@outline_items[0][:Prev] = HexaPDF::Reference.new(100)
|
|
38
39
|
called = false
|
|
39
40
|
@outline.validate do |msg, correctable, _|
|
|
40
41
|
called = true
|
|
@@ -46,6 +47,7 @@ describe HexaPDF::Type::Outline do
|
|
|
46
47
|
|
|
47
48
|
it "fixes a missing /Last entry" do
|
|
48
49
|
@outline.delete(:Last)
|
|
50
|
+
@outline_items[4][:Next] = HexaPDF::Reference.new(100)
|
|
49
51
|
called = false
|
|
50
52
|
@outline.validate do |msg, correctable, _|
|
|
51
53
|
called = true
|
|
@@ -276,12 +276,13 @@ describe HexaPDF::Type::OutlineItem do
|
|
|
276
276
|
|
|
277
277
|
describe "perform_validation" do
|
|
278
278
|
before do
|
|
279
|
-
5.times { @item.add_item("Test1") }
|
|
279
|
+
@outline_items = 5.times.map { @item.add_item("Test1") }
|
|
280
280
|
@item[:Parent] = @doc.add({})
|
|
281
281
|
end
|
|
282
282
|
|
|
283
283
|
it "fixes a missing /First entry" do
|
|
284
284
|
@item.delete(:First)
|
|
285
|
+
@outline_items[0][:Prev] = HexaPDF::Reference.new(100)
|
|
285
286
|
called = false
|
|
286
287
|
@item.validate do |msg, correctable, _|
|
|
287
288
|
called = true
|
|
@@ -293,6 +294,7 @@ describe HexaPDF::Type::OutlineItem do
|
|
|
293
294
|
|
|
294
295
|
it "fixes a missing /Last entry" do
|
|
295
296
|
@item.delete(:Last)
|
|
297
|
+
@outline_items[4][:Next] = HexaPDF::Reference.new(100)
|
|
296
298
|
called = false
|
|
297
299
|
@item.validate do |msg, correctable, _|
|
|
298
300
|
called = true
|
|
@@ -19,9 +19,21 @@ describe HexaPDF::Type::Page do
|
|
|
19
19
|
assert_equal([0, 0, 842, 595], HexaPDF::Type::Page.media_box(:A4, orientation: :landscape))
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
it "works with a paper size array" do
|
|
23
|
+
assert_equal([0, 0, 842, 595], HexaPDF::Type::Page.media_box([0, 0, 842, 595]))
|
|
24
|
+
end
|
|
25
|
+
|
|
22
26
|
it "fails if the paper size is unknown" do
|
|
23
27
|
assert_raises(HexaPDF::Error) { HexaPDF::Type::Page.media_box(:Unknown) }
|
|
24
28
|
end
|
|
29
|
+
|
|
30
|
+
it "fails if the array doesn't contain four numbers" do
|
|
31
|
+
assert_raises(HexaPDF::Error) { HexaPDF::Type::Page.media_box([0, 1, 2]) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "fails if the array doesn't contain only numbers" do
|
|
35
|
+
assert_raises(HexaPDF::Error) { HexaPDF::Type::Page.media_box([0, 1, 2, 'a']) }
|
|
36
|
+
end
|
|
25
37
|
end
|
|
26
38
|
|
|
27
39
|
# Asserts that the page's contents contains the operators.
|
|
@@ -70,22 +82,41 @@ describe HexaPDF::Type::Page do
|
|
|
70
82
|
end
|
|
71
83
|
end
|
|
72
84
|
|
|
73
|
-
describe "
|
|
85
|
+
describe "perform_validation" do
|
|
74
86
|
it "only does validation if the page is in the document's page tree" do
|
|
75
87
|
page = @doc.add({Type: :Page})
|
|
88
|
+
assert(page.validate(auto_correct: false))
|
|
89
|
+
page[:Parent] = @doc.add({Type: :Pages, Kids: [page], Count: 1})
|
|
90
|
+
assert(page.validate(auto_correct: false))
|
|
91
|
+
@doc.pages.add(page)
|
|
92
|
+
refute(page.validate(auto_correct: false))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "validates that the required inheritable field /Resources is set" do
|
|
96
|
+
page = @doc.pages.add
|
|
97
|
+
page.delete(:Resources)
|
|
98
|
+
refute(page.validate(auto_correct: false))
|
|
76
99
|
assert(page.validate)
|
|
77
|
-
|
|
78
|
-
assert(page.validate)
|
|
79
|
-
page[:Parent] = @doc.catalog.pages
|
|
80
|
-
refute(page.validate)
|
|
100
|
+
assert_kind_of(HexaPDF::Dictionary, page[:Resources])
|
|
81
101
|
end
|
|
82
102
|
|
|
83
|
-
it "
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
it "validates that the required inheritable field /MediaBox is set" do
|
|
104
|
+
page1 = @doc.pages.add(:Letter)
|
|
105
|
+
page2 = @doc.pages.add(:Letter)
|
|
106
|
+
page3 = @doc.pages.add(:Letter)
|
|
107
|
+
|
|
108
|
+
[page1, page2, page3].each do |page|
|
|
109
|
+
page.delete(:MediaBox)
|
|
110
|
+
refute(page.validate(auto_correct: false))
|
|
111
|
+
assert(page.validate)
|
|
112
|
+
assert_equal([0, 0, 612, 792], page[:MediaBox])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
page2.delete(:MediaBox)
|
|
116
|
+
page1[:MediaBox] = [0, 0, 1, 1]
|
|
117
|
+
refute(page2.validate(auto_correct: false))
|
|
118
|
+
assert(page2.validate)
|
|
119
|
+
assert_equal([0, 0, 595, 842], page2[:MediaBox])
|
|
89
120
|
end
|
|
90
121
|
end
|
|
91
122
|
|
|
@@ -6,9 +6,10 @@ require 'hexapdf/type/xref_stream'
|
|
|
6
6
|
describe HexaPDF::Type::XRefStream do
|
|
7
7
|
before do
|
|
8
8
|
@doc = Object.new
|
|
9
|
+
@doc.instance_variable_set(:@version, '1.5')
|
|
9
10
|
def (@doc).deref(obj); obj; end
|
|
10
11
|
def (@doc).wrap(obj, **); obj; end
|
|
11
|
-
@obj = HexaPDF::Type::XRefStream.new({}, oid: 1, document: @doc)
|
|
12
|
+
@obj = HexaPDF::Type::XRefStream.new({}, oid: 1, document: @doc, stream: '')
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
describe "xref_section" do
|
|
@@ -141,4 +142,8 @@ describe HexaPDF::Type::XRefStream do
|
|
|
141
142
|
assert_raises(HexaPDF::Error) { @obj.update_with_xref_section_and_trailer(@section, {}) }
|
|
142
143
|
end
|
|
143
144
|
end
|
|
145
|
+
|
|
146
|
+
it "sets /Size and /W to dummy values to make validation work" do
|
|
147
|
+
assert(@obj.validate(auto_correct: false))
|
|
148
|
+
end
|
|
144
149
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hexapdf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.31.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Thomas Leitner
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-02-
|
|
11
|
+
date: 2023-02-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: cmdparse
|
|
@@ -428,6 +428,7 @@ files:
|
|
|
428
428
|
- lib/hexapdf/layout/line.rb
|
|
429
429
|
- lib/hexapdf/layout/list_box.rb
|
|
430
430
|
- lib/hexapdf/layout/numeric_refinements.rb
|
|
431
|
+
- lib/hexapdf/layout/page_style.rb
|
|
431
432
|
- lib/hexapdf/layout/style.rb
|
|
432
433
|
- lib/hexapdf/layout/text_box.rb
|
|
433
434
|
- lib/hexapdf/layout/text_fragment.rb
|
|
@@ -679,6 +680,7 @@ files:
|
|
|
679
680
|
- test/hexapdf/layout/test_inline_box.rb
|
|
680
681
|
- test/hexapdf/layout/test_line.rb
|
|
681
682
|
- test/hexapdf/layout/test_list_box.rb
|
|
683
|
+
- test/hexapdf/layout/test_page_style.rb
|
|
682
684
|
- test/hexapdf/layout/test_style.rb
|
|
683
685
|
- test/hexapdf/layout/test_text_box.rb
|
|
684
686
|
- test/hexapdf/layout/test_text_fragment.rb
|