hexapdf 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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