hexapdf 0.30.0 → 0.31.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +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
|