hexapdf 0.42.0 → 0.44.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/Rakefile +1 -1
  4. data/examples/030-pdfa.rb +1 -0
  5. data/lib/hexapdf/composer.rb +1 -0
  6. data/lib/hexapdf/dictionary.rb +3 -3
  7. data/lib/hexapdf/document/files.rb +7 -2
  8. data/lib/hexapdf/document/metadata.rb +12 -1
  9. data/lib/hexapdf/document.rb +14 -1
  10. data/lib/hexapdf/encryption.rb +17 -0
  11. data/lib/hexapdf/layout/box.rb +161 -61
  12. data/lib/hexapdf/layout/box_fitter.rb +4 -3
  13. data/lib/hexapdf/layout/column_box.rb +23 -25
  14. data/lib/hexapdf/layout/container_box.rb +3 -3
  15. data/lib/hexapdf/layout/frame.rb +13 -95
  16. data/lib/hexapdf/layout/image_box.rb +4 -4
  17. data/lib/hexapdf/layout/line.rb +4 -0
  18. data/lib/hexapdf/layout/list_box.rb +12 -20
  19. data/lib/hexapdf/layout/style.rb +5 -1
  20. data/lib/hexapdf/layout/table_box.rb +48 -55
  21. data/lib/hexapdf/layout/text_box.rb +38 -39
  22. data/lib/hexapdf/parser.rb +23 -17
  23. data/lib/hexapdf/type/acro_form/form.rb +78 -27
  24. data/lib/hexapdf/type/file_specification.rb +9 -5
  25. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  26. data/lib/hexapdf/version.rb +1 -1
  27. data/test/hexapdf/document/test_files.rb +5 -0
  28. data/test/hexapdf/document/test_metadata.rb +21 -0
  29. data/test/hexapdf/layout/test_box.rb +82 -37
  30. data/test/hexapdf/layout/test_box_fitter.rb +10 -3
  31. data/test/hexapdf/layout/test_column_box.rb +7 -13
  32. data/test/hexapdf/layout/test_container_box.rb +1 -1
  33. data/test/hexapdf/layout/test_frame.rb +0 -48
  34. data/test/hexapdf/layout/test_image_box.rb +14 -6
  35. data/test/hexapdf/layout/test_list_box.rb +25 -26
  36. data/test/hexapdf/layout/test_table_box.rb +39 -53
  37. data/test/hexapdf/layout/test_text_box.rb +65 -67
  38. data/test/hexapdf/test_composer.rb +6 -0
  39. data/test/hexapdf/test_dictionary.rb +6 -4
  40. data/test/hexapdf/test_parser.rb +20 -0
  41. data/test/hexapdf/type/acro_form/test_form.rb +63 -2
  42. data/test/hexapdf/type/test_file_specification.rb +2 -1
  43. metadata +2 -2
@@ -43,6 +43,11 @@ module HexaPDF
43
43
  # objects of a Frame.
44
44
  #
45
45
  # This class uses TextLayouter behind the scenes to do the hard work.
46
+ #
47
+ # == Used Box Properties
48
+ #
49
+ # The spacing after the last line can be controlled via the style property +last_line_gap+. Also
50
+ # see TextLayouter#style for other style properties taken into account.
46
51
  class TextBox < Box
47
52
 
48
53
  # Creates a new TextBox object with the given inline items (e.g. TextFragment and InlineBox
@@ -52,6 +57,7 @@ module HexaPDF
52
57
  @tl = TextLayouter.new(style)
53
58
  @items = items
54
59
  @result = nil
60
+ @x_offset = 0
55
61
  end
56
62
 
57
63
  # Returns the text that will be drawn.
@@ -66,21 +72,27 @@ module HexaPDF
66
72
  true
67
73
  end
68
74
 
75
+ # :nodoc:
76
+ def draw(canvas, x, y)
77
+ super(canvas, x + @x_offset, y)
78
+ end
79
+
80
+ # :nodoc:
81
+ def empty?
82
+ super && (!@result || @result.lines.empty?)
83
+ end
84
+
85
+ private
86
+
69
87
  # Fits the text box into the Frame.
70
88
  #
71
89
  # Depending on the 'position' style property, the text is either fit into the current region
72
90
  # of the frame using +available_width+ and +available_height+, or fit to the shape of the
73
91
  # frame starting from the top (when 'position' is set to :flow).
74
- #
75
- # The spacing after the last line can be controlled via the style property +last_line_gap+.
76
- #
77
- # Also see TextLayouter#style for other style properties taken into account.
78
- def fit(available_width, available_height, frame)
79
- return false if (@initial_width > 0 && @initial_width > available_width) ||
80
- (@initial_height > 0 && @initial_height > available_height)
81
-
92
+ def fit_content(available_width, available_height, frame)
82
93
  frame = frame.child_frame(box: self)
83
- @width = @height = 0
94
+ @width = @x_offset = @height = 0
95
+
84
96
  @result = if style.position == :flow
85
97
  @tl.fit(@items, frame.width_specification, frame.shape.bbox.height,
86
98
  apply_first_text_indent: !split_box?, frame: frame)
@@ -91,8 +103,17 @@ module HexaPDF
91
103
  height = (@initial_height > 0 ? @initial_height : available_height) - @height
92
104
  @tl.fit(@items, width, height, apply_first_text_indent: !split_box?, frame: frame)
93
105
  end
106
+
94
107
  @width += if @initial_width > 0 || style.text_align == :center || style.text_align == :right
95
108
  width
109
+ elsif style.position == :flow
110
+ min_x = +Float::INFINITY
111
+ max_x = -Float::INFINITY
112
+ @result.lines.each do |line|
113
+ min_x = [min_x, line.x_offset].min
114
+ max_x = [max_x, line.x_offset + line.width].max
115
+ end
116
+ min_x.finite? ? (@x_offset = min_x; max_x - min_x) : 0
96
117
  else
97
118
  @result.lines.max_by(&:width)&.width || 0
98
119
  end
@@ -105,44 +126,22 @@ module HexaPDF
105
126
  @height += style.line_spacing.gap(@result.lines.last, @result.lines.last)
106
127
  end
107
128
 
108
- @result.status == :success ||
109
- (@result.status == :height && @initial_height > 0 && style.overflow == :truncate)
110
- end
111
-
112
- # Splits the text box into two boxes if necessary and possible.
113
- def split(available_width, available_height, frame)
114
- fit(available_width, available_height, frame) unless @result
115
-
116
- if style.position != :flow && (float_compare(@width, available_width) > 0 ||
117
- float_compare(@height, available_height) > 0)
118
- [nil, self]
119
- elsif @result.remaining_items.empty?
120
- [self]
121
- elsif @result.lines.empty?
122
- [nil, self]
123
- else
124
- [self, create_box_for_remaining_items]
129
+ if @result.status == :success
130
+ fit_result.success!
131
+ elsif @result.status == :height && !@result.lines.empty?
132
+ fit_result.overflow!
125
133
  end
126
134
  end
127
135
 
128
- # :nodoc:
129
- def empty?
130
- super && (!@result || @result.lines.empty?)
136
+ # Splits the text box into two.
137
+ def split_content
138
+ [self, create_box_for_remaining_items]
131
139
  end
132
140
 
133
- private
134
-
135
141
  # Draws the text into the box.
136
142
  def draw_content(canvas, x, y)
137
- return unless @result
138
-
139
- if @result.status == :height && @initial_height > 0 && style.overflow == :error
140
- raise HexaPDF::Error, "Text doesn't fit into box with limited height and " \
141
- "style property overflow is set to :error"
142
- end
143
-
144
143
  return if @result.lines.empty?
145
- @result.draw(canvas, x, y + content_height)
144
+ @result.draw(canvas, x - @x_offset, y + content_height)
146
145
  end
147
146
 
148
147
  # Creates a new TextBox instance for the items remaining after fitting the box.
@@ -362,29 +362,35 @@ module HexaPDF
362
362
  pos = @io.pos
363
363
  lines = @io.read(step_size + 40).split(/[\r\n]+/)
364
364
 
365
- eof_index = lines.rindex {|l| l.strip == '%%EOF' }
366
- if !eof_index
367
- eof_not_found = true
368
- elsif lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
369
- startxref_offset = $1.to_i
370
- startxref_mangled = true
371
- break # we found it even if it the syntax is not entirely correct
372
- elsif eof_index < 2 || lines[eof_index - 2].strip != "startxref"
373
- startxref_missing = true
374
- else
375
- startxref_offset = lines[eof_index - 1].to_i
376
- break # we found it
365
+ # Need to iterate through the whole lines array in case there are multiple %%EOF to try
366
+ eof_index = 0
367
+ while (eof_index = lines[0..(eof_index - 1)].rindex {|l| l.strip == '%%EOF' })
368
+ if eof_index > 0 && lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
369
+ startxref_offset = $1.to_i
370
+ startxref_mangled = true
371
+ break # we found it even if it the syntax is not entirely correct
372
+ elsif eof_index < 2
373
+ startxref_missing = true
374
+ break
375
+ elsif lines[eof_index - 2].strip != "startxref"
376
+ startxref_missing = true
377
+ else
378
+ startxref_offset = lines[eof_index - 1].to_i
379
+ break # we found it
380
+ end
377
381
  end
382
+ eof_not_found ||= !eof_index
383
+ break if startxref_offset
378
384
  end
379
385
 
380
- if eof_not_found
381
- maybe_raise("PDF file trailer with end-of-file marker not found", pos: pos,
382
- force: !eof_index)
383
- elsif startxref_mangled
386
+ if startxref_mangled
384
387
  maybe_raise("PDF file trailer keyword startxref on same line as value", pos: pos)
385
388
  elsif startxref_missing
386
389
  maybe_raise("PDF file trailer is missing startxref keyword", pos: pos,
387
- force: eof_index < 2 || lines[eof_index - 2].strip != "startxref")
390
+ force: !startxref_offset)
391
+ elsif eof_not_found
392
+ maybe_raise("PDF file trailer with end-of-file marker not found", pos: pos,
393
+ force: !startxref_offset)
388
394
  end
389
395
 
390
396
  @startxref_offset = startxref_offset
@@ -163,10 +163,21 @@ module HexaPDF
163
163
  field
164
164
  end
165
165
 
166
+ # Creates an untyped namespace field for creating hierarchies.
167
+ #
168
+ # Example:
169
+ #
170
+ # form.create_namespace_field('text')
171
+ # form.create_text_field('text.a1')
172
+ def create_namespace_field(name)
173
+ create_field(name)
174
+ end
175
+
166
176
  # Creates a new text field with the given name and adds it to the form.
167
177
  #
168
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
169
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
178
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
179
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
180
+ # the +name+ doesn't contain dots, a top-level field is created.
170
181
  #
171
182
  # The optional keyword arguments allow setting often used properties of the field:
172
183
  #
@@ -202,8 +213,9 @@ module HexaPDF
202
213
 
203
214
  # Creates a new multiline text field with the given name and adds it to the form.
204
215
  #
205
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
206
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
216
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
217
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
218
+ # the +name+ doesn't contain dots, a top-level field is created.
207
219
  #
208
220
  # The optional keyword arguments allow setting often used properties of the field, see
209
221
  # #create_text_field for details.
@@ -221,8 +233,9 @@ module HexaPDF
221
233
  # The +max_chars+ argument defines the maximum number of characters the comb text field can
222
234
  # accommodate.
223
235
  #
224
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
225
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
236
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
237
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
238
+ # the +name+ doesn't contain dots, a top-level field is created.
226
239
  #
227
240
  # The optional keyword arguments allow setting often used properties of the field, see
228
241
  # #create_text_field for details.
@@ -238,8 +251,9 @@ module HexaPDF
238
251
 
239
252
  # Creates a new file select field with the given name and adds it to the form.
240
253
  #
241
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
242
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
254
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
255
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
256
+ # the +name+ doesn't contain dots, a top-level field is created.
243
257
  #
244
258
  # The optional keyword arguments allow setting often used properties of the field, see
245
259
  # #create_text_field for details.
@@ -254,8 +268,9 @@ module HexaPDF
254
268
 
255
269
  # Creates a new password field with the given name and adds it to the form.
256
270
  #
257
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
258
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
271
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
272
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
273
+ # the +name+ doesn't contain dots, a top-level field is created.
259
274
  #
260
275
  # The optional keyword arguments allow setting often used properties of the field, see
261
276
  # #create_text_field for details.
@@ -270,8 +285,9 @@ module HexaPDF
270
285
 
271
286
  # Creates a new check box with the given name and adds it to the form.
272
287
  #
273
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
274
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
288
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
289
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
290
+ # the +name+ doesn't contain dots, a top-level field is created.
275
291
  #
276
292
  # Before a field value other than +false+ can be assigned to the check box, a widget needs
277
293
  # to be created.
@@ -281,8 +297,9 @@ module HexaPDF
281
297
 
282
298
  # Creates a radio button with the given name and adds it to the form.
283
299
  #
284
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
285
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
300
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
301
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
302
+ # the +name+ doesn't contain dots, a top-level field is created.
286
303
  #
287
304
  # Before a field value other than +nil+ can be assigned to the radio button, at least one
288
305
  # widget needs to be created.
@@ -292,8 +309,9 @@ module HexaPDF
292
309
 
293
310
  # Creates a combo box with the given name and adds it to the form.
294
311
  #
295
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
296
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
312
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
313
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
314
+ # the +name+ doesn't contain dots, a top-level field is created.
297
315
  #
298
316
  # The optional keyword arguments allow setting often used properties of the field:
299
317
  #
@@ -319,8 +337,9 @@ module HexaPDF
319
337
 
320
338
  # Creates a list box with the given name and adds it to the form.
321
339
  #
322
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
323
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
340
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
341
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
342
+ # the +name+ doesn't contain dots, a top-level field is created.
324
343
  #
325
344
  # The optional keyword arguments allow setting often used properties of the field:
326
345
  #
@@ -345,10 +364,38 @@ module HexaPDF
345
364
 
346
365
  # Creates a signature field with the given name and adds it to the form.
347
366
  #
348
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
349
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
367
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
368
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
369
+ # the +name+ doesn't contain dots, a top-level field is created.
350
370
  def create_signature_field(name)
351
- create_field(name, :Sig) {}
371
+ create_field(name, :Sig)
372
+ end
373
+
374
+ # :call-seq:
375
+ # form.delete_field(name)
376
+ # form.delete_field(field)
377
+ #
378
+ # Deletes the field specified by the given name or via the given field object.
379
+ #
380
+ # If the field is a signature field, the associated signature dictionary is also deleted.
381
+ def delete_field(name_or_field)
382
+ field = (name_or_field.kind_of?(String) ? field_by_name(name_or_field) : name_or_field)
383
+ document.delete(field[:V]) if field.field_type == :Sig
384
+
385
+ to_delete = field.each_widget(direct_only: false).to_a
386
+ document.pages.each do |page|
387
+ next unless page.key?(:Annots)
388
+ page_annots = page[:Annots].to_a - to_delete
389
+ page[:Annots].value.replace(page_annots)
390
+ end
391
+ to_delete.each {|widget| document.delete(widget) }
392
+
393
+ if field[:Parent]
394
+ field[:Parent][:Kids].delete(field)
395
+ else
396
+ self[:Fields].delete(field)
397
+ end
398
+ document.delete(field)
352
399
  end
353
400
 
354
401
  # Fills form fields with the values from the given +data+ hash.
@@ -485,23 +532,27 @@ module HexaPDF
485
532
 
486
533
  private
487
534
 
488
- # Creates a new field with the full name +name+ and the field type +type+.
489
- def create_field(name, type)
535
+ # Creates a new field with the full name +name+ and the optional field type +type+.
536
+ def create_field(name, type = nil)
490
537
  parent_name, _, name = name.rpartition('.')
491
538
  parent_field = parent_name.empty? ? nil : field_by_name(parent_name)
492
539
  if !parent_name.empty? && !parent_field
493
- raise HexaPDF::Error, "Parent field '#{parent_name}' not found"
540
+ parent_field = create_namespace_field(parent_name)
494
541
  end
495
542
 
496
- field = document.add({FT: type, T: name, Parent: parent_field},
497
- type: :XXAcroFormField, subtype: type)
543
+ field = if type
544
+ document.add({FT: type, T: name, Parent: parent_field},
545
+ type: :XXAcroFormField, subtype: type)
546
+ else
547
+ document.add({T: name, Parent: parent_field}, type: :XXAcroFormField)
548
+ end
498
549
  if parent_field
499
550
  (parent_field[:Kids] ||= []) << field
500
551
  else
501
552
  (self[:Fields] ||= []) << field
502
553
  end
503
554
 
504
- yield(field)
555
+ yield(field) if block_given?
505
556
 
506
557
  field
507
558
  end
@@ -158,11 +158,11 @@ module HexaPDF
158
158
  end
159
159
 
160
160
  # :call-seq:
161
- # file_spec.embed(filename, name: File.basename(filename), register: true) -> ef_stream
162
- # file_spec.embed(io, name:, register: true) -> ef_stream
161
+ # file_spec.embed(filename, name: File.basename(filename), mime_type: nil, register: true) -> ef_stream
162
+ # file_spec.embed(io, name:, mime_type: nil, register: true) -> ef_stream
163
163
  #
164
- # Embeds the given file or IO stream into the PDF file, sets the path accordingly and returns
165
- # the created stream object.
164
+ # Embeds the given file or IO stream into the PDF file, sets the path and MIME type
165
+ # accordingly and returns the created stream object.
166
166
  #
167
167
  # If a file is given, the +name+ option defaults to the basename of the file. However, if an
168
168
  # IO object is given, the +name+ argument is mandatory.
@@ -177,13 +177,16 @@ module HexaPDF
177
177
  # name::
178
178
  # The name that should be used as path value and when registering.
179
179
  #
180
+ # mime_type::
181
+ # Optionally specifies the MIME type of the file.
182
+ #
180
183
  # register::
181
184
  # Specifies whether the embedded file will be added to the EmbeddedFiles name tree under
182
185
  # the +name+. If the name is already taken, it's value is overwritten.
183
186
  #
184
187
  # The file has to be available until the PDF document gets written because reading and
185
188
  # writing is done lazily.
186
- def embed(file_or_io, name: nil, register: true)
189
+ def embed(file_or_io, name: nil, mime_type: nil, register: true)
187
190
  name ||= File.basename(file_or_io) if file_or_io.kind_of?(String)
188
191
  if name.nil?
189
192
  raise ArgumentError, "The name argument is mandatory when given an IO object"
@@ -194,6 +197,7 @@ module HexaPDF
194
197
 
195
198
  self[:EF] ||= {}
196
199
  ef_stream = self[:EF][:UF] = self[:EF][:F] = document.add({Type: :EmbeddedFile})
200
+ ef_stream[:Subtype] = mime_type.to_sym if mime_type
197
201
  stat = if file_or_io.kind_of?(String)
198
202
  File.stat(file_or_io)
199
203
  elsif file_or_io.respond_to?(:stat)
@@ -51,7 +51,7 @@ module HexaPDF
51
51
 
52
52
  define_type :ExtGState
53
53
 
54
- define_field :Type, type: Symbol, required: true, default: type
54
+ define_field :Type, type: Symbol, default: type
55
55
  define_field :LW, type: Numeric, version: "1.3"
56
56
  define_field :LC, type: Integer, version: "1.3"
57
57
  define_field :LJ, type: Integer, version: "1.3"
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.42.0'
40
+ VERSION = '0.44.0'
41
41
 
42
42
  end
@@ -43,6 +43,11 @@ describe HexaPDF::Document::Files do
43
43
  assert_equal('Some file', spec[:Desc])
44
44
  end
45
45
 
46
+ it "optionally sets the MIME type of an embedded file" do
47
+ spec = @doc.files.add(@file.path, mime_type: 'application/pdf')
48
+ assert_equal(:'application/pdf', spec.embedded_file_stream[:Subtype])
49
+ end
50
+
46
51
  it "requires the name argument when given an IO object" do
47
52
  assert_raises(ArgumentError) { @doc.files.add(StringIO.new) }
48
53
  end
@@ -187,6 +187,27 @@ describe HexaPDF::Document::Metadata do
187
187
  assert_equal(metadata, @doc.catalog[:Metadata].stream.sub(/(?<=id=")\w+/, ''))
188
188
  end
189
189
 
190
+ it "writes the custom metadata" do
191
+ @metadata.delete
192
+ @metadata.custom_metadata("<rdf:Description>Test</rdf:Description>")
193
+ @metadata.custom_metadata("<rdf:Description>Test2</rdf:Description>")
194
+ @doc.write(StringIO.new, update_fields: false)
195
+ metadata = <<~XMP
196
+ <?xpacket begin="" id=""?>
197
+ <x:xmpmeta xmlns:x="adobe:ns:meta/">
198
+ <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
199
+ <rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
200
+ <pdf:Producer>HexaPDF version #{HexaPDF::VERSION}</pdf:Producer>
201
+ </rdf:Description>
202
+ <rdf:Description>Test</rdf:Description>
203
+ <rdf:Description>Test2</rdf:Description>
204
+ </rdf:RDF>
205
+ </x:xmpmeta>
206
+ <?xpacket end="r"?>
207
+ XMP
208
+ assert_equal(metadata, @doc.catalog[:Metadata].stream.sub(/(?<=id=")\w+/, ''))
209
+ end
210
+
190
211
  it "writes the XMP metadata" do
191
212
  title = HexaPDF::Document::Metadata::LocalizedString.new('Der Titel')
192
213
  title.language = 'de'