hexapdf 0.43.0 → 0.45.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/examples/027-composer_optional_content.rb +6 -4
  4. data/examples/030-pdfa.rb +13 -11
  5. data/lib/hexapdf/composer.rb +23 -0
  6. data/lib/hexapdf/content/canvas.rb +3 -3
  7. data/lib/hexapdf/content/canvas_composer.rb +1 -0
  8. data/lib/hexapdf/document/files.rb +7 -2
  9. data/lib/hexapdf/document/layout.rb +15 -3
  10. data/lib/hexapdf/document/metadata.rb +12 -1
  11. data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
  12. data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
  13. data/lib/hexapdf/layout/box.rb +180 -66
  14. data/lib/hexapdf/layout/box_fitter.rb +1 -0
  15. data/lib/hexapdf/layout/column_box.rb +18 -28
  16. data/lib/hexapdf/layout/container_box.rb +6 -6
  17. data/lib/hexapdf/layout/frame.rb +13 -94
  18. data/lib/hexapdf/layout/image_box.rb +4 -4
  19. data/lib/hexapdf/layout/list_box.rb +13 -31
  20. data/lib/hexapdf/layout/style.rb +8 -4
  21. data/lib/hexapdf/layout/table_box.rb +55 -58
  22. data/lib/hexapdf/layout/text_box.rb +84 -71
  23. data/lib/hexapdf/layout/text_fragment.rb +1 -1
  24. data/lib/hexapdf/layout/text_layouter.rb +7 -8
  25. data/lib/hexapdf/parser.rb +5 -2
  26. data/lib/hexapdf/rectangle.rb +4 -4
  27. data/lib/hexapdf/type/file_specification.rb +9 -5
  28. data/lib/hexapdf/type/form.rb +2 -2
  29. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  30. data/lib/hexapdf/version.rb +1 -1
  31. data/test/hexapdf/content/test_canvas_composer.rb +13 -8
  32. data/test/hexapdf/document/test_files.rb +5 -0
  33. data/test/hexapdf/document/test_layout.rb +16 -0
  34. data/test/hexapdf/document/test_metadata.rb +21 -0
  35. data/test/hexapdf/layout/test_box.rb +93 -37
  36. data/test/hexapdf/layout/test_box_fitter.rb +7 -0
  37. data/test/hexapdf/layout/test_column_box.rb +7 -13
  38. data/test/hexapdf/layout/test_container_box.rb +1 -1
  39. data/test/hexapdf/layout/test_frame.rb +7 -46
  40. data/test/hexapdf/layout/test_image_box.rb +14 -6
  41. data/test/hexapdf/layout/test_list_box.rb +26 -27
  42. data/test/hexapdf/layout/test_table_box.rb +47 -54
  43. data/test/hexapdf/layout/test_text_box.rb +83 -83
  44. data/test/hexapdf/test_composer.rb +20 -5
  45. data/test/hexapdf/test_parser.rb +8 -0
  46. data/test/hexapdf/test_serializer.rb +1 -0
  47. data/test/hexapdf/type/test_file_specification.rb +2 -1
  48. metadata +2 -2
@@ -42,7 +42,46 @@ module HexaPDF
42
42
  # A TextBox is used for drawing text, either inside a rectangular box or by flowing it around
43
43
  # objects of a Frame.
44
44
  #
45
+ # The standard usage is through the helper methods Document::Layout#text and
46
+ # Document::Layout#formatted_text.
47
+ #
45
48
  # This class uses TextLayouter behind the scenes to do the hard work.
49
+ #
50
+ # == Used Box Properties
51
+ #
52
+ # The spacing after the last line can be controlled via the style property +last_line_gap+. Also
53
+ # see TextLayouter#style for other style properties taken into account.
54
+ #
55
+ # == Limitations
56
+ #
57
+ # When setting the style property 'position' to +:flow+, padding and border to the left and
58
+ # right as well as a predefined fixed width are not respected and the result will look wrong.
59
+ #
60
+ # == Examples
61
+ #
62
+ # Showing some text:
63
+ #
64
+ # #>pdf-composer
65
+ # composer.box(:text, items: layout.text_fragments("This is some text."))
66
+ # # Or easier with the provided convenience method
67
+ # composer.text("This is also some text")
68
+ #
69
+ # It is possible to flow the text around other objects by using the style property
70
+ # 'position' with the value +:flow+:
71
+ #
72
+ # #>pdf-composer
73
+ # composer.box(:base, width: 30, height: 30,
74
+ # style: {margin: 5, position: :float, background_color: "hp-blue-light"})
75
+ # composer.text("This is some text. " * 20, position: :flow)
76
+ #
77
+ # While top and bottom padding and border can be used with flow positioning, left and right
78
+ # padding and border are not supported and the result will look wrong:
79
+ #
80
+ # #>pdf-composer
81
+ # composer.box(:base, width: 30, height: 30,
82
+ # style: {margin: 5, position: :float, background_color: "hp-blue-light"})
83
+ # composer.text("This is some text. " * 20, padding: 10, position: :flow,
84
+ # text_align: :justify)
46
85
  class TextBox < Box
47
86
 
48
87
  # Creates a new TextBox object with the given inline items (e.g. TextFragment and InlineBox
@@ -67,94 +106,68 @@ module HexaPDF
67
106
  true
68
107
  end
69
108
 
109
+ # :nodoc:
110
+ def draw(canvas, x, y)
111
+ super(canvas, x + @x_offset, y)
112
+ end
113
+
114
+ # :nodoc:
115
+ def empty?
116
+ super && (!@result || @result.lines.empty?)
117
+ end
118
+
119
+ private
120
+
70
121
  # Fits the text box into the Frame.
71
122
  #
72
123
  # Depending on the 'position' style property, the text is either fit into the current region
73
124
  # of the frame using +available_width+ and +available_height+, or fit to the shape of the
74
125
  # frame starting from the top (when 'position' is set to :flow).
75
- #
76
- # The spacing after the last line can be controlled via the style property +last_line_gap+.
77
- #
78
- # Also see TextLayouter#style for other style properties taken into account.
79
- def fit(available_width, available_height, frame)
80
- return false if (@initial_width > 0 && @initial_width > available_width) ||
81
- (@initial_height > 0 && @initial_height > available_height)
82
-
126
+ def fit_content(_available_width, _available_height, frame)
83
127
  frame = frame.child_frame(box: self)
84
- @width = @x_offset = @height = 0
85
- @result = if style.position == :flow
86
- @tl.fit(@items, frame.width_specification, frame.shape.bbox.height,
128
+ @x_offset = 0
129
+
130
+ if style.position == :flow
131
+ height = (@initial_height > 0 ? @initial_height : frame.shape.bbox.height) - reserved_height
132
+ @result = @tl.fit(@items, frame.width_specification(reserved_height_top), height,
87
133
  apply_first_text_indent: !split_box?, frame: frame)
88
- else
89
- @width = reserved_width
90
- @height = reserved_height
91
- width = (@initial_width > 0 ? @initial_width : available_width) - @width
92
- height = (@initial_height > 0 ? @initial_height : available_height) - @height
93
- @tl.fit(@items, width, height, apply_first_text_indent: !split_box?, frame: frame)
94
- end
95
- @width += if @initial_width > 0 || style.text_align == :center || style.text_align == :right
96
- width
97
- elsif style.position == :flow
98
- min_x = +Float::INFINITY
99
- max_x = -Float::INFINITY
100
- @result.lines.each do |line|
101
- min_x = [min_x, line.x_offset].min
102
- max_x = [max_x, line.x_offset + line.width].max
103
- end
104
- min_x.finite? ? (@x_offset = min_x; max_x - min_x) : 0
105
- else
106
- @result.lines.max_by(&:width)&.width || 0
107
- end
108
- @height += if @initial_height > 0 || style.text_valign == :center || style.text_valign == :bottom
109
- height
110
- else
111
- @result.height
112
- end
113
- if style.last_line_gap && @result.lines.last
114
- @height += style.line_spacing.gap(@result.lines.last, @result.lines.last)
134
+ min_x = +Float::INFINITY
135
+ max_x = -Float::INFINITY
136
+ @result.lines.each do |line|
137
+ min_x = [min_x, line.x_offset].min
138
+ max_x = [max_x, line.x_offset + line.width].max
139
+ end
140
+ @width = (min_x.finite? ? (@x_offset = min_x; max_x - min_x) : 0) + reserved_width
141
+ @height = @initial_height > 0 ? @initial_height : @result.height + reserved_height
142
+ else
143
+ @result = @tl.fit(@items, @width - reserved_width, @height - reserved_height,
144
+ apply_first_text_indent: !split_box?, frame: frame)
145
+ if style.text_align == :left && @initial_width == 0
146
+ @width = (@result.lines.max_by(&:width)&.width || 0) + reserved_width
147
+ end
148
+ if style.text_valign == :top && @initial_height == 0
149
+ @height = @result.height + reserved_height
150
+ end
115
151
  end
116
152
 
117
- @result.status == :success ||
118
- (@result.status == :height && @initial_height > 0 && style.overflow == :truncate)
119
- end
120
-
121
- # Splits the text box into two boxes if necessary and possible.
122
- def split(available_width, available_height, frame)
123
- fit(available_width, available_height, frame) unless @result
124
-
125
- if style.position != :flow && (float_compare(@width, available_width) > 0 ||
126
- float_compare(@height, available_height) > 0)
127
- [nil, self]
128
- elsif @result.remaining_items.empty?
129
- [self]
130
- elsif @result.lines.empty?
131
- [nil, self]
132
- else
133
- [self, create_box_for_remaining_items]
153
+ if style.last_line_gap && @result.lines.last && @initial_height == 0
154
+ @height += style.line_spacing.gap(@result.lines.last, @result.lines.last)
134
155
  end
135
- end
136
156
 
137
- # :nodoc:
138
- def draw(canvas, x, y)
139
- super(canvas, x + @x_offset, y)
157
+ if @result.status == :success
158
+ fit_result.success!
159
+ elsif @result.status == :height && !@result.lines.empty?
160
+ fit_result.overflow!
161
+ end
140
162
  end
141
163
 
142
- # :nodoc:
143
- def empty?
144
- super && (!@result || @result.lines.empty?)
164
+ # Splits the text box into two.
165
+ def split_content
166
+ [self, create_box_for_remaining_items]
145
167
  end
146
168
 
147
- private
148
-
149
169
  # Draws the text into the box.
150
170
  def draw_content(canvas, x, y)
151
- return unless @result
152
-
153
- if @result.status == :height && @initial_height > 0 && style.overflow == :error
154
- raise HexaPDF::Error, "Text doesn't fit into box with limited height and " \
155
- "style property overflow is set to :error"
156
- end
157
-
158
171
  return if @result.lines.empty?
159
172
  @result.draw(canvas, x - @x_offset, y + content_height)
160
173
  end
@@ -52,7 +52,7 @@ module HexaPDF
52
52
  # The items of a text fragment may be frozen to indicate that the fragment is potentially used
53
53
  # multiple times.
54
54
  #
55
- # The rectangle with the bottom left corner (#x_min, #y_min) and the top right corner (#x_max,
55
+ # The rectangle with the bottom-left corner (#x_min, #y_min) and the top-right corner (#x_max,
56
56
  # #y_max) describes the minimum bounding box of the whole text fragment and is usually *not*
57
57
  # equal to the box (0, 0)-(#width, #height).
58
58
  class TextFragment
@@ -218,7 +218,10 @@ module HexaPDF
218
218
 
219
219
  # Breaks are detected at: space, tab, zero-width-space, non-breaking space, hyphen,
220
220
  # soft-hypen and any valid Unicode newline separator
221
- BREAK_RE = /[ \u{A}-\u{D}\u{85}\u{2028}\u{2029}\t\u{200B}\u{00AD}\u{00A0}-]/
221
+ BREAK_CHARS = {}
222
+ " \u{A}\u{B}\u{C}\u{D}\u{85}\u{2028}\u{2029}\t\u{200B}\u{00AD}\u{00A0}-".each_char do |c|
223
+ BREAK_CHARS[c] = true
224
+ end
222
225
 
223
226
  # Breaks the items (an array of InlineBox and TextFragment objects) into atomic pieces
224
227
  # wrapped by Box, Glue or Penalty items, and returns those as an array.
@@ -235,7 +238,7 @@ module HexaPDF
235
238
  # Collect characters and kerning values until break character is encountered
236
239
  box_items = []
237
240
  while (glyph = item.items[i]) &&
238
- (glyph.kind_of?(Numeric) || !BREAK_RE.match?(glyph.str))
241
+ (glyph.kind_of?(Numeric) || !BREAK_CHARS.key?(glyph.str))
239
242
  box_items << glyph
240
243
  i += 1
241
244
  end
@@ -428,9 +431,7 @@ module HexaPDF
428
431
  end
429
432
 
430
433
  line = create_unjustified_line
431
- last_line_used = true
432
- last_line_used = yield(line, nil) if item.nil? && !line.items.empty?
433
-
434
+ last_line_used = (item.nil? && !line.items.empty? ? yield(line, nil) : true)
434
435
  item.nil? && last_line_used ? [] : @items[@beginning_of_line_index..-1]
435
436
  end
436
437
 
@@ -500,9 +501,7 @@ module HexaPDF
500
501
  end
501
502
 
502
503
  line = create_unjustified_line
503
- last_line_used = true
504
- last_line_used = yield(line, nil) if item.nil? && !line.items.empty?
505
-
504
+ last_line_used = (item.nil? && !line.items.empty? ? yield(line, nil) : true)
506
505
  item.nil? && last_line_used ? [] : @items[@beginning_of_line_index..-1]
507
506
  end
508
507
 
@@ -365,11 +365,14 @@ module HexaPDF
365
365
  # Need to iterate through the whole lines array in case there are multiple %%EOF to try
366
366
  eof_index = 0
367
367
  while (eof_index = lines[0..(eof_index - 1)].rindex {|l| l.strip == '%%EOF' })
368
- if lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
368
+ if eof_index > 0 && lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
369
369
  startxref_offset = $1.to_i
370
370
  startxref_mangled = true
371
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"
372
+ elsif eof_index < 2
373
+ startxref_missing = true
374
+ break
375
+ elsif lines[eof_index - 2].strip != "startxref"
373
376
  startxref_missing = true
374
377
  else
375
378
  startxref_offset = lines[eof_index - 1].to_i
@@ -48,8 +48,8 @@ module HexaPDF
48
48
  #
49
49
  # [left, bottom, right, top]
50
50
  #
51
- # where +left+ is the bottom left x-coordinate, +bottom+ is the bottom left y-coordinate, +right+
52
- # is the top right x-coordinate and +top+ is the top right y-coordinate.
51
+ # where +left+ is the bottom-left x-coordinate, +bottom+ is the bottom-left y-coordinate, +right+
52
+ # is the top-right x-coordinate and +top+ is the top-right y-coordinate.
53
53
  #
54
54
  # See: PDF2.0 s7.9.5
55
55
  class Rectangle < HexaPDF::PDFArray
@@ -119,8 +119,8 @@ module HexaPDF
119
119
  #:nodoc:
120
120
  RECTANGLE_ERROR_MSG = "A PDF rectangle structure must contain an array of four numbers"
121
121
 
122
- # Ensures that the value is an array containing four numbers that specify the bottom left and
123
- # top right corner.
122
+ # Ensures that the value is an array containing four numbers that specify the bottom-left and
123
+ # top-right corners.
124
124
  def after_data_change
125
125
  super
126
126
  unless value.size == 4 && all? {|v| v.kind_of?(Numeric) }
@@ -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)
@@ -159,8 +159,8 @@ module HexaPDF
159
159
  # retained without the need for parsing its contents.
160
160
  #
161
161
  # If the bounding box of the form XObject doesn't have its origin at (0, 0), the canvas origin
162
- # is translated into the bottom left corner so that this detail doesn't matter when using the
163
- # canvas. This means that the canvas' origin is always at the bottom left corner of the
162
+ # is translated into the bottom-left corner so that this detail doesn't matter when using the
163
+ # canvas. This means that the canvas' origin is always at the bottom-left corner of the
164
164
  # bounding box.
165
165
  #
166
166
  # *Note* that a canvas can only be retrieved for initially empty form XObjects!
@@ -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.43.0'
40
+ VERSION = '0.45.0'
41
41
 
42
42
  end
@@ -47,21 +47,20 @@ describe HexaPDF::Content::CanvasComposer do
47
47
  end
48
48
 
49
49
  it "splits the box if possible" do
50
- @composer.draw_box(create_box(width: 400, style: {position: :float}))
51
- box = create_box(width: 400, height: 100)
52
- box.define_singleton_method(:split) do |*|
53
- [box, HexaPDF::Layout::Box.new(height: 100) {}]
54
- end
50
+ @composer.draw_box(create_box(width: 300, height: 300, style: {position: :float}))
51
+ box = create_box(style: {mask_mode: :box})
52
+ box.define_singleton_method(:fit_content) {|*| fit_result.overflow! }
53
+ box.define_singleton_method(:split_content) { [box, HexaPDF::Layout::Box.new(height: 100) {}] }
55
54
  @composer.draw_box(box)
56
55
  assert_operators(@composer.canvas.contents,
57
56
  [[:save_graphics_state],
58
- [:concatenate_matrix, [1, 0, 0, 1, 0, 0]],
57
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 541.889764]],
59
58
  [:restore_graphics_state],
60
59
  [:save_graphics_state],
61
- [:concatenate_matrix, [1, 0, 0, 1, 400, 741.889764]],
60
+ [:concatenate_matrix, [1, 0, 0, 1, 300, 0]],
62
61
  [:restore_graphics_state],
63
62
  [:save_graphics_state],
64
- [:concatenate_matrix, [1, 0, 0, 1, 400, 641.889764]],
63
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 441.889764]],
65
64
  [:restore_graphics_state]])
66
65
  end
67
66
 
@@ -77,6 +76,12 @@ describe HexaPDF::Content::CanvasComposer do
77
76
  [:restore_graphics_state]])
78
77
  end
79
78
 
79
+ it "handles truncated boxes correctly" do
80
+ box = create_box(height: 400, style: {overflow: :truncate})
81
+ box.define_singleton_method(:fit_content) {|*| fit_result.overflow! }
82
+ assert_same(box, @composer.draw_box(box))
83
+ end
84
+
80
85
  it "returns the last drawn box" do
81
86
  box = create_box(height: 400)
82
87
  assert_same(box, @composer.draw_box(box))
@@ -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
@@ -141,6 +141,22 @@ describe HexaPDF::Document::Layout do
141
141
  end
142
142
  end
143
143
 
144
+ describe "styles" do
145
+ it "returns the existing styles" do
146
+ @layout.style(:test, font_size: 20)
147
+ assert_equal([:base, :test], @layout.styles.keys)
148
+ end
149
+
150
+ it "sets multiple styles at once" do
151
+ styles = @layout.styles(
152
+ test: {font_size: 20},
153
+ test2: {font_size: 30},
154
+ )
155
+ assert_same(styles, @layout.styles)
156
+ assert_equal([:base, :test, :test2], @layout.styles.keys)
157
+ end
158
+ end
159
+
144
160
  describe "inline_box" do
145
161
  it "takes a box as argument" do
146
162
  box = HexaPDF::Layout::Box.create(width: 10, height: 10)
@@ -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'