hexapdf 0.42.0 → 0.44.0

Sign up to get free protection for your applications and to get access to all the features.
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'