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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 139f1864e4decd05c57468dc3b326a9cc4e749f1d35d4b5dadb3e0549215afd6
4
- data.tar.gz: e7a0d34979abe6738a084dec2334f449ebc1db3eb479dd5f2374a81eeabd83d7
3
+ metadata.gz: 7a32d8f7558ea14ae5dc40849c690b79f5ed4a797cffb85253e7fd6d00c54736
4
+ data.tar.gz: 92713168ee64efc59f86ff1d4f8989054ab02653ccaf84795aed3fea02a4811d
5
5
  SHA512:
6
- metadata.gz: 339c38737585eafdf447f7516a52ba7ce40b7e2e476336a2404e97a97645d5043b2956835d874296841b8b68d5935c53d97244ac90a2fd621d0471ce85ae8809
7
- data.tar.gz: 4a4c843a32859c639fdf724ca1c6492fa3d387a1c8a92335a2341e82de42a5b9bc30663a3226c1cc0f2ca116dd638071e24b595b323e64fa1e0342d8681199bd
6
+ metadata.gz: 43b05643d9a59e9656a464eda221fa28e04bb4fe8ba1f97d0e31e434fbce64ffb9ed551689ce68957c8b4a7b82fbc85ecd91c1eac6d177b0109d4b9b5d275a6c
7
+ data.tar.gz: 780e7051327b0463e800ca4ea3b9f1ac1c3723d960154a2ac27c9e9bed867d0a257c48c20762cd0947f8638dc1f72b598a33ff76e1f0d1d2b0f1ff843497eb29
data/CHANGELOG.md CHANGED
@@ -1,4 +1,31 @@
1
- ## 0.30.0 - 2023-01-13
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
 
@@ -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
@@ -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'] = false
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}"
@@ -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: Invalid page range argument")
169
+ rescue StandardError => e
170
+ $stderr.puts("Error: #{e}")
171
171
  next
172
172
  end
173
173
  page_list = @doc.pages.to_a
@@ -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
- def initialize(page_size: :A4, page_orientation: :portrait, margin: 36) #:yields: composer
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
- @page_size = page_size
129
- @page_orientation = page_orientation
130
- @margin = Layout::Style::Quad.new(margin)
131
-
132
- new_page
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
- # If any of +page_size+, +page_orientation+ or +margin+ are set, they will be used instead of
139
- # the default values and will become the default values.
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.new_page # uses the default values
144
- # composer.new_page(page_size: :A5, margin: [72, 36])
145
- def new_page(page_size: nil, page_orientation: nil, margin: nil)
146
- @page_size = page_size if page_size
147
- @page_orientation = page_orientation if page_orientation
148
- @margin = Layout::Style::Quad.new(margin) if margin
149
-
150
- @page = @document.pages.add(@page_size, orientation: @page_orientation)
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
- create_frame
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.flate_compression::
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.flate_memory::
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.flate_compression' => 9,
547
- 'filter.flate_memory' => 6,
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 = if media_box.kind_of?(Array)
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
 
@@ -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
- klass = GlobalConfiguration.constantize('object.type_map', type) { nil } if type
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
- data = inflater.inflate(data)
62
- rescue StandardError => e
63
- raise FilterError, "Problem while decoding Flate encoded stream: #{e}"
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 StandardError => e
72
- raise FilterError, "Problem while decoding Flate encoded stream: #{e}"
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.flate_compression'],
102
+ deflater = Zlib::Deflate.new(HexaPDF::GlobalConfiguration['filter.flate.compression'],
91
103
  Zlib::MAX_WBITS,
92
- HexaPDF::GlobalConfiguration['filter.flate_memory'])
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
@@ -56,6 +56,7 @@ module HexaPDF
56
56
  autoload(:ImageBox, 'hexapdf/layout/image_box')
57
57
  autoload(:ColumnBox, 'hexapdf/layout/column_box')
58
58
  autoload(:ListBox, 'hexapdf/layout/list_box')
59
+ autoload(:PageStyle, 'hexapdf/layout/page_style')
59
60
 
60
61
  end
61
62
 
@@ -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({Type: :XRef})
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({Type: :XRef}, oid: doc.revisions.next_oid))
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({Type: :ObjStm})]
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({Type: :ObjStm})
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({Type: :XRef}, oid: doc.revisions.next_oid)) unless xref_stream
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({Type: :XRef}, oid: doc.revisions.next_oid)) unless xref_stream
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 # not required, will be auto-filled on #write_objects
105
- define_field :First, type: Integer # not required, will be auto-filled on #write_objects
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
@@ -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.key?(dir)
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.key?(dir)
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)
@@ -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. See PAPER_SIZE for the defined paper sizes.
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
- REQUIRED_INHERITABLE_FIELDS.each do |name|
613
- next if self[name]
614
- yield("Inheritable page field #{name} not set", name == :Resources)
615
- resources.validate(&block) if name == :Ressources
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
- # Size is not required because it will be auto-filled before the object is written
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
- # W is not required because it will be auto-filled on #update_with_xref_section_and_trailer
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
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.30.0'
40
+ VERSION = '0.31.0'
41
41
 
42
42
  end
@@ -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({Type: :XRef}, oid: @document.revisions.next_oid)
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
- it "fails on invalid input" do
30
- assert_raises(HexaPDF::FilterError) do
31
- collector(@obj.decoder(feeder(@encoded[0..-2], @encoded.length - 3)))
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
- assert_raises(HexaPDF::FilterError) do
34
- collector(@obj.decoder(feeder("some data")))
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({Type: :XRef}, oid: @doc.revisions.next_oid))
85
- @doc.revisions.all[1].add(@doc.wrap({Type: :XRef}, oid: @doc.revisions.next_oid))
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({Type: :ObjStm})
96
- @doc.add({Type: :XRef})
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({Type: :ObjStm})
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({Type: :ObjStm})
126
- xref = @doc.add({Type: :XRef})
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({Type: :XRef})
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({Type: :XRef})
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 with the stored information" do
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 provided information for the new and all following pages" do
68
- @composer.new_page(page_size: [0, 0, 50, 100], margin: 10)
69
- assert_equal([0, 0, 50, 100], @composer.page.box.value)
70
- assert_equal(10, @composer.frame.left)
71
- assert_equal(10, @composer.frame.bottom)
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
- assert_same(@composer.document.pages[2], @composer.page)
74
- assert_equal([0, 0, 50, 100], @composer.page.box.value)
75
- assert_equal(10, @composer.frame.left)
76
- assert_equal(10, @composer.frame.bottom)
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
 
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.30.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.30.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({Type: :ObjStm})
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({Type: :ObjStm})
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.30.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
- it "fails validation if gen != 0" do
127
- assert(@obj.validate(auto_correct: false))
128
- @obj.gen = 1
129
- refute(@obj.validate(auto_correct: false) do |msg, correctable|
130
- assert_match(/invalid generation/, msg)
131
- refute(correctable)
132
- end)
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 "validation" do
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
- page[:Parent] = @doc.add({Type: :Pages, Kids: [page]})
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 "fails if a required inheritable field is not set" do
84
- root = @doc.catalog[:Pages] = @doc.add({Type: :Pages})
85
- page = @doc.add({Type: :Page, Parent: root})
86
- message = ''
87
- refute(page.validate {|m, _| message = m })
88
- assert_match(/inheritable.*MediaBox/i, message)
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.30.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-13 00:00:00.000000000 Z
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