hexapdf 0.37.2 → 0.39.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -227,12 +227,12 @@ module HexaPDF
227
227
  remove_indent_from_frame_shape(shape) unless shape.polygons.empty?
228
228
  end
229
229
 
230
- item_frame = Frame.new(item_frame_left, top - height, item_frame_width, height,
231
- shape: shape, context: frame.context)
230
+ item_frame = frame.child_frame(item_frame_left, top - height, item_frame_width, height,
231
+ shape: shape, box: self)
232
232
 
233
233
  if index != 0 || !split_box? || @split_box == :show_first_marker
234
234
  box = item_marker_box(frame.document, index)
235
- marker_frame = Frame.new(0, 0, content_indentation, height, context: frame.context)
235
+ marker_frame = frame.child_frame(0, 0, content_indentation, height, box: self)
236
236
  break unless box.fit(content_indentation, height, marker_frame)
237
237
  item_result.marker = box
238
238
  item_result.marker_pos_x = item_frame.x - content_indentation
@@ -325,27 +325,30 @@ module HexaPDF
325
325
  return @marker_type.call(document, self, index) if @marker_type.kind_of?(Proc)
326
326
  return @item_marker_box if defined?(@item_marker_box)
327
327
 
328
+ marker_style = {
329
+ font: style.font? ? style.font : document.fonts.add("Times"),
330
+ font_size: style.font_size || 10, fill_color: style.fill_color
331
+ }
328
332
  fragment = case @marker_type
329
333
  when :disc
330
- TextFragment.create("•", font: document.fonts.add("Times"),
331
- font_size: style.font_size, fill_color: style.fill_color)
334
+ TextFragment.create("•", marker_style)
332
335
  when :circle
333
- TextFragment.create("❍", font: document.fonts.add("ZapfDingbats"),
336
+ unless marker_style[:font].decode_codepoint("❍".ord).valid?
337
+ marker_style[:font] = document.fonts.add("ZapfDingbats")
338
+ end
339
+ TextFragment.create("❍", **marker_style,
334
340
  font_size: style.font_size / 2.0,
335
- fill_color: style.fill_color,
336
341
  text_rise: -style.font_size / 1.8)
337
342
  when :square
338
- TextFragment.create("■", font: document.fonts.add("ZapfDingbats"),
343
+ unless marker_style[:font].decode_codepoint("■".ord).valid?
344
+ marker_style[:font] = document.fonts.add("ZapfDingbats")
345
+ end
346
+ TextFragment.create("■", **marker_style,
339
347
  font_size: style.font_size / 2.0,
340
- fill_color: style.fill_color,
341
348
  text_rise: -style.font_size / 1.8)
342
349
  when :decimal
343
350
  text = (@start_number + index).to_s << "."
344
- decimal_style = {
345
- font: (style.font? ? style.font : document.fonts.add("Times")),
346
- font_size: style.font_size || 10, fill_color: style.fill_color
347
- }
348
- TextFragment.create(text, decimal_style)
351
+ TextFragment.create(text, marker_style)
349
352
  else
350
353
  raise HexaPDF::Error, "Unknown list marker type #{@marker_type.inspect}"
351
354
  end
@@ -1084,6 +1084,25 @@ module HexaPDF
1084
1084
  # 'Centered',
1085
1085
  # {text: "\u{00a0}", fill_horizontal: 1, overlays: overlays}])
1086
1086
 
1087
+ ##
1088
+ # :method: text_overflow
1089
+ # :call-seq:
1090
+ # text_overflow(mode = nil)
1091
+ #
1092
+ # Specifies how text overflowing a box with a given initial height should be handled:
1093
+ #
1094
+ # Possible values:
1095
+ #
1096
+ # :error:: An error is raised (default).
1097
+ # :truncate:: Truncates the overflowing text.
1098
+ #
1099
+ # Examples:
1100
+ #
1101
+ # #>pdf-composer100
1102
+ # composer.text("This is some longer text that does appear in two lines.")
1103
+ # composer.text("This is some longer text that does not appear in two lines.",
1104
+ # height: 15, text_overflow: :truncate)
1105
+
1087
1106
  ##
1088
1107
  # :method: background_color
1089
1108
  # :call-seq:
@@ -1435,6 +1454,7 @@ module HexaPDF
1435
1454
  extra_args: ", extra_arg = nil"}],
1436
1455
  [:last_line_gap, false, {valid_values: [true, false]}],
1437
1456
  [:fill_horizontal, nil],
1457
+ [:text_overflow, :error],
1438
1458
  [:background_color, nil],
1439
1459
  [:background_alpha, 1],
1440
1460
  [:padding, "Quad.new(0)", {setter: "Quad.new(value)"}],
@@ -218,7 +218,7 @@ module HexaPDF
218
218
  height = available_height - reserved_height
219
219
  return false if width <= 0 || height <= 0
220
220
 
221
- frame = Frame.new(0, 0, width, height, context: frame.context)
221
+ frame = frame.child_frame(0, 0, width, height, box: self)
222
222
  case children
223
223
  when Box
224
224
  fit_result = frame.fit(children)
@@ -607,6 +607,7 @@ module HexaPDF
607
607
  columns = calculate_column_widths(width)
608
608
  return false if columns.empty?
609
609
 
610
+ frame = frame.child_frame(box: self)
610
611
  @special_cells_fit_not_successful = false
611
612
  [@header_cells, @footer_cells].each do |special_cells|
612
613
  next unless special_cells
@@ -79,6 +79,7 @@ module HexaPDF
79
79
  return false if (@initial_width > 0 && @initial_width > available_width) ||
80
80
  (@initial_height > 0 && @initial_height > available_height)
81
81
 
82
+ frame = frame.child_frame(box: self)
82
83
  @width = @height = 0
83
84
  @result = if style.position == :flow
84
85
  @tl.fit(@items, frame.width_specification, frame.shape.bbox.height,
@@ -104,7 +105,8 @@ module HexaPDF
104
105
  @height += style.line_spacing.gap(@result.lines.last, @result.lines.last)
105
106
  end
106
107
 
107
- @result.status == :success
108
+ @result.status == :success ||
109
+ (@result.status == :height && @initial_height > 0 && style.text_overflow == :truncate)
108
110
  end
109
111
 
110
112
  # Splits the text box into two boxes if necessary and possible.
@@ -132,7 +134,14 @@ module HexaPDF
132
134
 
133
135
  # Draws the text into the box.
134
136
  def draw_content(canvas, x, y)
135
- return unless @result && !@result.lines.empty?
137
+ return unless @result
138
+
139
+ if @result.status == :height && @initial_height > 0 && style.text_overflow == :error
140
+ raise HexaPDF::Error, "Text doesn't fit into box with limited height and " \
141
+ "style property text_overflow is set to :error"
142
+ end
143
+
144
+ return if @result.lines.empty?
136
145
  @result.draw(canvas, x, y + content_height)
137
146
  end
138
147
 
@@ -307,10 +307,15 @@ module HexaPDF
307
307
  class SimpleLineWrapping
308
308
 
309
309
  # :call-seq:
310
- # SimpleLineWrapping.call(items, width_block) {|line, item| block } -> rest
310
+ # SimpleLineWrapping.call(items, width_block, frame = nil) {|line, item| block } -> rest
311
311
  #
312
312
  # Arranges the items into lines.
313
313
  #
314
+ # The optional +frame+ argument needs to be a Frame object that is used when fitting inline
315
+ # boxes. If not provided, a custom Frame object is used. However, if the items contain
316
+ # inline boxes that need to access a frame's context object, it is mandatory to provide an
317
+ # appropriate Frame object.
318
+ #
314
319
  # The +width_block+ argument has to be a callable object that returns the width of the line:
315
320
  #
316
321
  # * If the line width doesn't depend on the height or the vertical position of the line
@@ -340,7 +345,7 @@ module HexaPDF
340
345
  # current start of the line index should be stored for later use.
341
346
  #
342
347
  # After the algorithm is finished, it returns the unused items.
343
- def self.call(items, width_block, frame, &block)
348
+ def self.call(items, width_block, frame = nil, &block)
344
349
  obj = new(items, width_block, frame)
345
350
  if width_block.arity == 1
346
351
  obj.variable_width_wrapping(&block)
@@ -507,7 +512,7 @@ module HexaPDF
507
512
  #
508
513
  # Returns +true+ if the item could be added and +false+ otherwise.
509
514
  def add_box_item(item)
510
- item.fit_wrapped_box(@frame&.context) if item.kind_of?(InlineBox)
515
+ item.fit_wrapped_box(@frame) if item.kind_of?(InlineBox)
511
516
  return false unless @width + item.width <= @available_width
512
517
  @line_items.concat(@glue_items).push(item)
513
518
  @width += item.width
@@ -718,6 +723,10 @@ module HexaPDF
718
723
  # Specifies whether style.text_indent should be applied to the first line. This should be
719
724
  # set to +false+ if the items start with a continuation of a paragraph instead of starting
720
725
  # a new paragraph (e.g. after a page break).
726
+ #
727
+ # +frame+::
728
+ # If used with the document layout functionality, this should be the frame into which the
729
+ # text is laid out.
721
730
  def fit(items, width, height, apply_first_text_indent: true, frame: nil)
722
731
  unless items.empty? || items[0].respond_to?(:type)
723
732
  items = style.text_segmentation_algorithm.call(items)
@@ -0,0 +1,87 @@
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-2024 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 'set'
38
+ require 'hexapdf/serializer'
39
+ require 'hexapdf/content/parser'
40
+ require 'hexapdf/content/operator'
41
+ require 'hexapdf/type/xref_stream'
42
+ require 'hexapdf/type/object_stream'
43
+
44
+ module HexaPDF
45
+ module Task
46
+
47
+ # Task for creating a PDF/A compliant document.
48
+ #
49
+ # It automatically
50
+ #
51
+ # * prevents the Standard 14 PDF fonts to be used.
52
+ # * adds an appropriate output intent if none is set.
53
+ # * adds the necessary PDF/A metadata properties.
54
+ module PDFA
55
+
56
+ # Performs the necessary tasks to make the document PDF/A compatible.
57
+ #
58
+ # +level+::
59
+ # Specifies the PDF/A conformance level that should be used. Can be one of the following
60
+ # strings: 2b, 2u, 3b, 3u.
61
+ def self.call(doc, level: '3u')
62
+ unless level.match?(/\A[23][bu]\z/)
63
+ raise ArgumentError, "The given PDF/A conformance level '#{level}' is not supported"
64
+ end
65
+ doc.config['font_loader'].delete('HexaPDF::FontLoader::Standard14')
66
+ doc.register_listener(:complete_objects) do
67
+ part, conformance = level.chars
68
+ doc.metadata.property('pdfaid', 'part', part)
69
+ doc.metadata.property('pdfaid', 'conformance', conformance.upcase)
70
+ add_srgb_icc_output_intent(doc) unless doc.catalog.key?(:OutputIntents)
71
+ end
72
+ end
73
+
74
+ SRGB_ICC = 'sRGB2014.icc' # :nodoc:
75
+
76
+ def self.add_srgb_icc_output_intent(doc) # :nodoc:
77
+ icc = doc.add({N: 3}, stream: File.binread(File.join(HexaPDF.data_dir, SRGB_ICC)))
78
+ doc.catalog[:OutputIntents] = [
79
+ doc.add({S: :GTS_PDFA1, OutputConditionIdentifier: SRGB_ICC, Info: SRGB_ICC,
80
+ RegistryName: 'https://www.color.org', DestOutputProfile: icc}),
81
+ ]
82
+ end
83
+
84
+ end
85
+
86
+ end
87
+ end
data/lib/hexapdf/task.rb CHANGED
@@ -64,6 +64,7 @@ module HexaPDF
64
64
 
65
65
  autoload(:Optimize, 'hexapdf/task/optimize')
66
66
  autoload(:Dereference, 'hexapdf/task/dereference')
67
+ autoload(:PDFA, 'hexapdf/task/pdfa')
67
68
 
68
69
  end
69
70
 
@@ -136,7 +136,7 @@ module HexaPDF
136
136
  if hash
137
137
  self[:D] = hash
138
138
  else
139
- self[:D] ||= {Creator: 'HexaPDF'}
139
+ self[:D] ||= {Name: 'Default', Creator: 'HexaPDF'}
140
140
  end
141
141
  self[:D]
142
142
  end
@@ -146,7 +146,7 @@ module HexaPDF
146
146
  def perform_validation(&block) # :nodoc:
147
147
  unless key?(:D)
148
148
  yield('The OptionalContentProperties dictionary needs a default configuration', true)
149
- self[:D] = {Creator: 'HexaPDF'}
149
+ self[:D] = {Name: 'Default', Creator: 'HexaPDF'}
150
150
  end
151
151
  super
152
152
  end
@@ -0,0 +1,85 @@
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-2024 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/dictionary'
38
+
39
+ module HexaPDF
40
+ module Type
41
+
42
+ # Represents an output intent dictionary.
43
+ #
44
+ # Such a dictionary may be referenced from the catalog's /OutputIntents entry or from the
45
+ # /OutputIntents entry of a page object.
46
+ #
47
+ # See: PDF2.0 s14.11.5, Catalog
48
+ class OutputIntent < Dictionary
49
+
50
+ # Represents a destination output profile reference dictionary.
51
+ #
52
+ # Such a dictionary is referenced from the /DestOutputProfileRef entry of an OutputIntent
53
+ # dictionary.
54
+ #
55
+ # See: PDF2.0 s14.11.5
56
+ class DestOutputProfileRef < Dictionary
57
+
58
+ define_type :XXDestOutputProfileRef
59
+
60
+ define_field :CheckSum, type: String, version: "2.0"
61
+ define_field :ColorantTable, type: PDFArray, version: "2.0"
62
+ define_field :ICCVersion, type: String, version: "2.0"
63
+ define_field :ProfileCS, type: String, version: "2.0"
64
+ define_field :ProfileName, type: String, version: "2.0"
65
+ define_field :URLs, type: PDFArray, version: "2.0"
66
+
67
+ end
68
+
69
+ define_type :OutputIntent
70
+
71
+ define_field :Type, type: Symbol, required: false, default: type
72
+ define_field :S, type: Symbol, required: true
73
+ define_field :OutputCondition, type: String
74
+ define_field :OutputConditionIdentifier, type: String, required: true
75
+ define_field :RegistryName, type: String
76
+ define_field :Info, type: String
77
+ define_field :DestOutputProfile, type: Stream
78
+ define_field :DestOutputProfileRef, type: :XXDestOutputProfileRef, version: "2.0"
79
+ define_field :MixingHints, type: Dictionary, version: "2.0"
80
+ define_field :SpectralData, type: Dictionary, version: "2.0"
81
+
82
+ end
83
+
84
+ end
85
+ end
data/lib/hexapdf/type.rb CHANGED
@@ -81,6 +81,7 @@ module HexaPDF
81
81
  autoload(:OptionalContentProperties, 'hexapdf/type/optional_content_properties')
82
82
  autoload(:OptionalContentConfiguration, 'hexapdf/type/optional_content_configuration')
83
83
  autoload(:Metadata, 'hexapdf/type/metadata')
84
+ autoload(:OutputIntent, 'hexapdf/type/output_intent')
84
85
 
85
86
  end
86
87
 
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.37.2'
40
+ VERSION = '0.39.0'
41
41
 
42
42
  end
@@ -104,6 +104,13 @@ describe HexaPDF::Document::Layout::CellArgumentCollector do
104
104
  @args[5, 6] = {e: :f}
105
105
  assert_equal({key: :value, a: :c, e: :f}, @args.retrieve_arguments_for(5, 6))
106
106
  end
107
+
108
+ it "deep merges the :cell keys" do
109
+ @args[] = {cell: {a: :b, c: :d}}
110
+ @args[3..7] = {cell: {a: :y, e: :f}}
111
+ @args[5, 6] = {cell: {a: :z}}
112
+ assert_equal({cell: {a: :z, c: :d, e: :f}}, @args.retrieve_arguments_for(5, 6))
113
+ end
107
114
  end
108
115
  end
109
116
 
@@ -229,6 +236,12 @@ describe HexaPDF::Document::Layout do
229
236
  assert_equal(20, box.style.font_size)
230
237
 
231
238
  box = @layout.text_box("Test", style: {font_size: 20})
239
+ assert_same(@doc.fonts.add("Times"), box.style.font)
240
+ assert_equal(20, box.style.font_size)
241
+
242
+ @layout.style(:base, font: ['Times', {variant: :bold}])
243
+ box = @layout.text_box("Test", style: {font_size: 20})
244
+ assert_same(@doc.fonts.add("Times", variant: :bold), box.style.font)
232
245
  assert_equal(20, box.style.font_size)
233
246
 
234
247
  @layout.style(:named, font_size: 20)
@@ -27,8 +27,8 @@ describe HexaPDF::Document::Metadata do
27
27
  assert_equal("de", HexaPDF::Document::Metadata.new(@doc).default_language)
28
28
  end
29
29
 
30
- it "falls back to English if the document doesn't have a default language set" do
31
- assert_equal('en', @metadata.default_language)
30
+ it "falls back to the default language if the document doesn't have a default language set" do
31
+ assert_equal('x-default', @metadata.default_language)
32
32
  end
33
33
 
34
34
  it "allows changing the default language" do
@@ -80,6 +80,25 @@ describe HexaPDF::Document::Metadata do
80
80
  refute(@metadata.instance_variable_get(:@metadata)[@metadata.namespace('dc')].key?('title'))
81
81
  end
82
82
 
83
+ describe "delete" do
84
+ it "deletes all properties" do
85
+ @metadata.delete
86
+ assert(@metadata.instance_variable_get(:@metadata).empty?)
87
+ end
88
+
89
+ it "deletes all properties of a single namespace" do
90
+ @metadata.creator('Test')
91
+ @metadata.delete('dc')
92
+ assert_equal('Test', @metadata.creator)
93
+ refute(@metadata.instance_variable_get(:@metadata).key?(@metadata.namespace('dc')))
94
+ end
95
+
96
+ it "deletes a specific property" do
97
+ @metadata.delete('dc', 'title')
98
+ assert_nil(@metadata.title)
99
+ end
100
+ end
101
+
83
102
  it "allows reading and setting all info dictionary properties" do
84
103
  [['title', 'dc', 'title'], ['author', 'dc', 'creator'], ['subject', 'dc', 'description'],
85
104
  ['keywords', 'pdf', 'Keywords'], ['creator', 'xmp', 'CreatorTool'],
@@ -120,6 +139,17 @@ describe HexaPDF::Document::Metadata do
120
139
  assert_equal(:True, info[:Trapped])
121
140
  end
122
141
 
142
+ it "omits values in the info dictionary that are not set" do
143
+ @metadata.delete('pdf', 'Trapped')
144
+ @metadata.delete('dc', 'title')
145
+ @metadata.delete('dc', 'creator')
146
+ @doc.write(StringIO.new, update_fields: false)
147
+ info = @doc.trailer.info
148
+ refute(info.key?(:Title))
149
+ refute(info.key?(:Author))
150
+ refute(info.key?(:Trapped))
151
+ end
152
+
123
153
  it "uses a correctly updated modification date if set so by Document#write" do
124
154
  info = @doc.trailer.info
125
155
  sleep(0.1)
@@ -140,6 +170,23 @@ describe HexaPDF::Document::Metadata do
140
170
  assert_equal('Subject', info[:Subject])
141
171
  end
142
172
 
173
+ it "omits rdf:Description elements without values" do
174
+ @metadata.delete
175
+ @doc.write(StringIO.new, update_fields: false)
176
+ metadata = <<~XMP
177
+ <?xpacket begin="" id=""?>
178
+ <x:xmpmeta xmlns:x="adobe:ns:meta/">
179
+ <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
180
+ <rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
181
+ <pdf:Producer>HexaPDF version #{HexaPDF::VERSION}</pdf:Producer>
182
+ </rdf:Description>
183
+ </rdf:RDF>
184
+ </x:xmpmeta>
185
+ <?xpacket end="r"?>
186
+ XMP
187
+ assert_equal(metadata, @doc.catalog[:Metadata].stream.sub(/(?<=id=")\w+/, ''))
188
+ end
189
+
143
190
  it "writes the XMP metadata" do
144
191
  title = HexaPDF::Document::Metadata::LocalizedString.new('Der Titel')
145
192
  title.language = 'de'
@@ -147,13 +194,16 @@ describe HexaPDF::Document::Metadata do
147
194
  @metadata.author(['Author 1', 'Author 2'])
148
195
  @metadata.register_property_type('dc', 'other', 'URI')
149
196
  @metadata.property('dc', 'other', 'https://test.org/example')
197
+ @metadata.property('pdfaid', 'part', 3)
198
+ @metadata.property('pdfaid', 'conformance', 'b')
150
199
  @doc.write(StringIO.new, update_fields: false)
151
200
  metadata = <<~XMP
152
201
  <?xpacket begin="" id=""?>
202
+ <x:xmpmeta xmlns:x="adobe:ns:meta/">
153
203
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
154
204
  <rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">
155
205
  <dc:title><rdf:Alt>
156
- <rdf:li xml:lang="en">Title</rdf:li>
206
+ <rdf:li xml:lang="x-default">Title</rdf:li>
157
207
  <rdf:li xml:lang="de">Der Titel</rdf:li>
158
208
  </rdf:Alt></dc:title>
159
209
  <dc:creator><rdf:Seq>
@@ -161,13 +211,13 @@ describe HexaPDF::Document::Metadata do
161
211
  <rdf:li>Author 2</rdf:li>
162
212
  </rdf:Seq></dc:creator>
163
213
  <dc:description><rdf:Alt>
164
- <rdf:li xml:lang="en">Subject</rdf:li>
214
+ <rdf:li xml:lang="x-default">Subject</rdf:li>
165
215
  </rdf:Alt></dc:description>
166
216
  <dc:other rdf:resource="https://test.org/example" />
167
217
  </rdf:Description>
168
218
  <rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
169
219
  <pdf:Keywords>Keywords</pdf:Keywords>
170
- <pdf:Producer>Producer</pdf:Producer>
220
+ <pdf:Producer>HexaPDF version #{HexaPDF::VERSION}</pdf:Producer>
171
221
  <pdf:Trapped>True</pdf:Trapped>
172
222
  </rdf:Description>
173
223
  <rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/">
@@ -175,7 +225,12 @@ describe HexaPDF::Document::Metadata do
175
225
  <xmp:CreateDate>#{@metadata.send(:xmp_date, @time)}</xmp:CreateDate>
176
226
  <xmp:ModifyDate>#{@metadata.send(:xmp_date, @time)}</xmp:ModifyDate>
177
227
  </rdf:Description>
228
+ <rdf:Description rdf:about="" xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">
229
+ <pdfaid:part>3</pdfaid:part>
230
+ <pdfaid:conformance>b</pdfaid:conformance>
231
+ </rdf:Description>
178
232
  </rdf:RDF>
233
+ </x:xmpmeta>
179
234
  <?xpacket end="r"?>
180
235
  XMP
181
236
  assert_equal(metadata, @doc.catalog[:Metadata].stream.sub(/(?<=id=")\w+/, ''))
@@ -10,7 +10,8 @@ describe HexaPDF::Layout::Frame::FitResult do
10
10
  doc = HexaPDF::Document.new(config: {'debug' => true})
11
11
  canvas = doc.pages.add.canvas
12
12
  box = HexaPDF::Layout::Box.create(width: 20, height: 20) {}
13
- result = HexaPDF::Layout::Frame::FitResult.new(box)
13
+ frame = HexaPDF::Layout::Frame.new(5, 10, 100, 150)
14
+ result = HexaPDF::Layout::Frame::FitResult.new(frame, box)
14
15
  result.mask = Geom2D::Rectangle(0, 0, 20, 20)
15
16
  result.x = result.y = 0
16
17
  result.draw(canvas, dx: 10, dy: 15)
@@ -30,7 +31,8 @@ describe HexaPDF::Layout::Frame::FitResult do
30
31
  Q
31
32
  CONTENTS
32
33
  ocg = doc.optional_content.ocgs.first
33
- assert_equal([['Debug', ocg]], doc.optional_content.default_configuration[:Order])
34
+ assert_equal([['Debug', ['Page 1', ocg]]], doc.optional_content.default_configuration[:Order])
35
+ assert_match(/10,15-20x20/, ocg.name)
34
36
  end
35
37
  end
36
38
 
@@ -61,6 +63,11 @@ describe HexaPDF::Layout::Frame do
61
63
  assert_equal(150, @frame.available_height)
62
64
  end
63
65
 
66
+ it "allows access to the frame's parent boxes" do
67
+ frame = HexaPDF::Layout::Frame.new(5, 10, 100, 150, parent_boxes: [:a])
68
+ assert_equal([:a], frame.parent_boxes)
69
+ end
70
+
64
71
  it "allows setting the shape of the frame on initialization" do
65
72
  shape = Geom2D::Polygon([50, 10], [55, 100], [105, 100], [105, 10])
66
73
  frame = HexaPDF::Layout::Frame.new(5, 10, 100, 150, shape: shape)
@@ -71,6 +78,27 @@ describe HexaPDF::Layout::Frame do
71
78
  assert_equal(90, frame.available_height)
72
79
  end
73
80
 
81
+ describe "child_frame" do
82
+ before do
83
+ @frame = HexaPDF::Layout::Frame.new(10, 10, 100, 100, parent_boxes: [:a])
84
+ end
85
+
86
+ it "duplicates the frame setting the parent boxes appropriately" do
87
+ assert_same(@frame.parent_boxes, @frame.child_frame.parent_boxes)
88
+ frame = @frame.child_frame(box: :b)
89
+ assert_equal([:a, :b], frame.parent_boxes)
90
+ end
91
+
92
+ it "creates a new frame, optionally adding a parent box" do
93
+ shape = Geom2D::Rectangle(0, 0, 20, 20)
94
+ frame = @frame.child_frame(0, 0, 20, 20, shape: shape)
95
+ assert_same(@frame.parent_boxes, frame.parent_boxes)
96
+ assert_equal(shape, frame.shape)
97
+ frame = @frame.child_frame(0, 0, 20, 20, box: :b)
98
+ assert_equal([:a, :b], frame.parent_boxes)
99
+ end
100
+ end
101
+
74
102
  it "returns an appropriate width specification object" do
75
103
  ws = @frame.width_specification(10)
76
104
  assert_kind_of(HexaPDF::Layout::WidthFromPolygon, ws)
@@ -97,6 +125,7 @@ describe HexaPDF::Layout::Frame do
97
125
  @canvas.expect(:translate, nil, pos)
98
126
  fit_result = @frame.fit(@box)
99
127
  refute_nil(fit_result)
128
+ assert_same(@frame, fit_result.frame)
100
129
  @frame.draw(@canvas, fit_result)
101
130
  assert_equal(mask, fit_result.mask.bbox.to_a)
102
131
  if @frame.shape.respond_to?(:polygons)
@@ -31,7 +31,14 @@ describe HexaPDF::Layout::InlineBox do
31
31
  end
32
32
 
33
33
  describe "fit_wrapped_box" do
34
- it "automatically fits the provided box into a frame" do
34
+ it "automatically fits the provided box into the given frame" do
35
+ ibox = inline_box(HexaPDF::Document.new.layout.text("test is going good", width: 20))
36
+ ibox.fit_wrapped_box(HexaPDF::Layout::Frame.new(0, 0, 50, 50))
37
+ assert_equal(20, ibox.width)
38
+ assert_equal(45, ibox.height)
39
+ end
40
+
41
+ it "automatically fits the provided box into a custom frame" do
35
42
  ibox = inline_box(HexaPDF::Document.new.layout.text("test is going good", width: 20))
36
43
  ibox.fit_wrapped_box(nil)
37
44
  assert_equal(20, ibox.width)