hexapdf 0.29.0 → 0.31.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/lib/hexapdf/cli/command.rb +16 -1
  4. data/lib/hexapdf/cli/info.rb +9 -1
  5. data/lib/hexapdf/cli/inspect.rb +2 -2
  6. data/lib/hexapdf/composer.rb +76 -28
  7. data/lib/hexapdf/configuration.rb +17 -4
  8. data/lib/hexapdf/dictionary_fields.rb +7 -2
  9. data/lib/hexapdf/document/pages.rb +31 -18
  10. data/lib/hexapdf/document.rb +8 -1
  11. data/lib/hexapdf/encryption/standard_security_handler.rb +2 -2
  12. data/lib/hexapdf/filter/flate_decode.rb +20 -8
  13. data/lib/hexapdf/layout/page_style.rb +144 -0
  14. data/lib/hexapdf/layout.rb +1 -0
  15. data/lib/hexapdf/task/optimize.rb +8 -6
  16. data/lib/hexapdf/type/font_simple.rb +14 -2
  17. data/lib/hexapdf/type/object_stream.rb +7 -2
  18. data/lib/hexapdf/type/outline.rb +1 -1
  19. data/lib/hexapdf/type/outline_item.rb +1 -1
  20. data/lib/hexapdf/type/page.rb +29 -8
  21. data/lib/hexapdf/type/xref_stream.rb +11 -4
  22. data/lib/hexapdf/version.rb +1 -1
  23. data/lib/hexapdf/writer.rb +1 -1
  24. data/test/hexapdf/document/test_pages.rb +25 -0
  25. data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -2
  26. data/test/hexapdf/filter/test_flate_decode.rb +19 -5
  27. data/test/hexapdf/layout/test_page_style.rb +70 -0
  28. data/test/hexapdf/task/test_optimize.rb +11 -9
  29. data/test/hexapdf/test_composer.rb +35 -10
  30. data/test/hexapdf/test_dictionary_fields.rb +9 -4
  31. data/test/hexapdf/test_writer.rb +8 -8
  32. data/test/hexapdf/type/test_font_simple.rb +18 -6
  33. data/test/hexapdf/type/test_object_stream.rb +16 -7
  34. data/test/hexapdf/type/test_outline.rb +3 -1
  35. data/test/hexapdf/type/test_outline_item.rb +3 -1
  36. data/test/hexapdf/type/test_page.rb +42 -11
  37. data/test/hexapdf/type/test_xref_stream.rb +6 -1
  38. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1fdace78c8d34d39c345e2ccd04edda4755e2fc8076cc7320793a4ef16f48520
4
- data.tar.gz: a0ec03dc2d579eb8663512ec0c84cadbc877b9ec43cabb8ea0d6ab58df337585
3
+ metadata.gz: 7a32d8f7558ea14ae5dc40849c690b79f5ed4a797cffb85253e7fd6d00c54736
4
+ data.tar.gz: 92713168ee64efc59f86ff1d4f8989054ab02653ccaf84795aed3fea02a4811d
5
5
  SHA512:
6
- metadata.gz: a8b18782359af03f0eda710658d0839554f0e95cd8068683dcaea56e8493c3b8a0b4cc30c9e39a3fdbe743df4d5e34b84ce4ab27125c8c14300b861ecbff16e9
7
- data.tar.gz: 8d5c60a368d85fa9c2b118d955933943f5d0cf79e91d744348cf1e3f1c9435d44880033c844b58ab098c8ab8d1e2e7cd4c5e04710133b25543f31457277d0ba2
6
+ metadata.gz: 43b05643d9a59e9656a464eda221fa28e04bb4fe8ba1f97d0e31e434fbce64ffb9ed551689ce68957c8b4a7b82fbc85ecd91c1eac6d177b0109d4b9b5d275a6c
7
+ data.tar.gz: 780e7051327b0463e800ca4ea3b9f1ac1c3723d960154a2ac27c9e9bed867d0a257c48c20762cd0947f8638dc1f72b598a33ff76e1f0d1d2b0f1ff843497eb29
data/CHANGELOG.md CHANGED
@@ -1,3 +1,48 @@
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
29
+
30
+ ### Added
31
+
32
+ * [HexaPDF::Document::Pages#create] for creating a page object without adding it
33
+ to the page tree
34
+
35
+ ### Changed
36
+
37
+ * `HexaPDF::Type::FontSimple#perform_validation` to correct /Widths fields in
38
+ case it has an invalid number of entries
39
+
40
+ ### Fixed
41
+
42
+ * [HexaPDF::DictionaryFields::DateConverter] to handle invalid months, day,
43
+ hour, minute and second values
44
+
45
+
1
46
  ## 0.29.0 - 2023-01-30
2
47
 
3
48
  ### Added
@@ -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',
@@ -305,8 +305,13 @@ module HexaPDF
305
305
  else
306
306
  (m[7] == '-' ? -1 : 1) * (m[8].to_i * 3600 + m[9].to_i * 60).clamp(0, 86399)
307
307
  end
308
- Time.new(m[1].to_i, (m[2] ? m[2].to_i : 1), (m[3] ? m[3].to_i : 1),
309
- m[4].to_i, m[5].to_i, m[6].to_i, utc_offset)
308
+ begin
309
+ Time.new(m[1].to_i, (m[2] ? m[2].to_i : 1), (m[3] ? m[3].to_i : 1),
310
+ m[4].to_i, m[5].to_i, m[6].to_i, utc_offset)
311
+ rescue ArgumentError
312
+ Time.new(m[1].to_i, m[2].to_i.clamp(1, 12), m[3].to_i.clamp(1, 31),
313
+ m[4].to_i.clamp(0, 23), m[5].to_i.clamp(0, 59), m[6].to_i.clamp(0, 59), utc_offset)
314
+ end
310
315
  end
311
316
 
312
317
  end
@@ -75,28 +75,41 @@ module HexaPDF
75
75
  @document.catalog.pages
76
76
  end
77
77
 
78
- # :call-seq:
79
- # pages.add -> new_page
80
- # pages.add(media_box, orientation: :portrait) -> new_page
81
- # pages.add(page) -> page
78
+ # Creates a page object and returns it *without* adding it to the page tree.
79
+ #
80
+ # +media_box+::
81
+ # If this argument is +nil+/not specified, the value is taken from the configuration
82
+ # option 'page.default_media_box'.
83
+ #
84
+ # If the resulting value is an array with four numbers (specifying the media box), the new
85
+ # page will have these exact dimensions.
82
86
  #
83
- # Adds the page or a new empty page at the end and returns it.
87
+ # If the value is a symbol, it is taken as a reference to a pre-defined media box in
88
+ # HexaPDF::Type::Page::PAPER_SIZE. The +orientation+ can then be used to specify the page
89
+ # orientation.
84
90
  #
85
- # If no argument is given, a new page with the default dimensions (see configuration option
86
- # 'page.default_media_box') is used.
91
+ # +orientation+::
92
+ # If this argument is not specified, it is taken from 'page.default_media_orientation'. It
93
+ # is only used if +media_box+ is a symbol and not an array.
94
+ def create(media_box: nil, orientation: nil)
95
+ media_box ||= @document.config['page.default_media_box']
96
+ orientation ||= @document.config['page.default_media_orientation']
97
+ box = Type::Page.media_box(media_box, orientation: orientation)
98
+ @document.add({Type: :Page, MediaBox: box})
99
+ end
100
+
101
+ # :call-seq:
102
+ # pages.add -> new_page
103
+ # pages.add(page) -> page
104
+ # pages.add(media_box, orientation: nil) -> new_page
87
105
  #
88
- # If the single argument is an array with four numbers (specifying the media box), the new
89
- # page will have these dimensions.
106
+ # Adds the given page or a new empty page at the end and returns it.
90
107
  #
91
- # If the single argument is a symbol, it is taken as referencing a pre-defined media box in
92
- # HexaPDF::Type::Page::PAPER_SIZE for the new page. The optional argument +orientation+ can be
93
- # used to change the orientation to :landscape if needed.
94
- def add(page = nil, orientation: :portrait)
95
- if page.kind_of?(Array)
96
- page = @document.add({Type: :Page, MediaBox: page})
97
- elsif page.kind_of?(Symbol)
98
- box = Type::Page.media_box(page, orientation: orientation)
99
- page = @document.add({Type: :Page, MediaBox: box})
108
+ # If called with a page object as argument, that page object is used. Otherwise #create is
109
+ # called with the arguments +media_box+ and +orientation+ to create a new page.
110
+ def add(page = nil, orientation: nil)
111
+ unless page.kind_of?(HexaPDF::Type::Page)
112
+ page = create(media_box: page, orientation: orientation)
100
113
  end
101
114
  @document.catalog.pages.add_page(page)
102
115
  end
@@ -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)
@@ -324,10 +324,10 @@ module HexaPDF
324
324
  def prepare_decryption(password: '', check_permissions: true)
325
325
  if dict[:Filter] != :Standard
326
326
  raise(HexaPDF::UnsupportedEncryptionError,
327
- "Invalid /Filter value for standard security handler")
327
+ "Invalid /Filter value #{dict[:Filter]} for standard security handler")
328
328
  elsif ![2, 3, 4, 6].include?(dict[:R])
329
329
  raise(HexaPDF::UnsupportedEncryptionError,
330
- "Invalid /R value for standard security handler")
330
+ "Invalid /R value #{dict[:R]} for standard security handler")
331
331
  elsif dict[:R] <= 4 && !document.trailer[:ID].kind_of?(PDFArray)
332
332
  document.trailer[:ID] = ['', '']
333
333
  end
@@ -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