hexapdf 0.37.2 → 0.39.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7633d38801df5768a809a8491de34a33ff90069c1ea55eed29a75d0ccd4e5c67
4
- data.tar.gz: f74d51319f639d5e218e27cb264231e370943fa365b4b4c1ef9b0c47df123dfb
3
+ metadata.gz: d0a43d232528df9bfc4a2c9baddd9cd1ddbd33fe7cce9d146b4e25760f25429b
4
+ data.tar.gz: 37cb10752d22bbc89fd18a779152ef7b5e02e6237318006a4b0e0a420d2890e2
5
5
  SHA512:
6
- metadata.gz: cfc63a537dacfece92e4df2116362b8a270b9f291e1350604368820a8e2e02ebc2e47b462391dcd74cf9a16ab498d9b3b72ab1a8ff063eab816ed65636405379
7
- data.tar.gz: e90d2be37bc9150c9e040b06678fe98ea601c20dab39b75e689559418f043b63c0967784f0e5f0b0d11f1cce5ee10557d65a4e996099301bd27fb4962dcbe961
6
+ metadata.gz: 79a27b101c502261e1bca7a25fa26fa434d38cec09a2ccb28a698a4364d8ed337ee63d82ce8cd4dd3097334ed54f0877565c25e20c2494c0ae7029156dc188ef
7
+ data.tar.gz: 69befc3a7066eb90a58ad4a65243939930f26ecc461e0f6f7c9bf0894ddfa58bf1a02567563caa453b0c6e745421de75e7dd433926d2a56b96aa4d71557059f5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,44 @@
1
+ ## 0.39.0 - 2024-03-18
2
+
3
+ ### Added
4
+
5
+ * Hierarchical box information to the document layout engine
6
+ * Style property 'text_overflow' for controlling how overflowing text should be
7
+ handled
8
+
9
+ ### Changed
10
+
11
+ * [HexaPDF::Layout::Frame::FitResult#draw] to provide better optional content
12
+ group names
13
+
14
+ ### Fixed
15
+
16
+ * [HexaPDF::Layout::TextBox] to correctly respect a set height
17
+
18
+
19
+ ## 0.38.0 - 2024-03-10
20
+
21
+ ### Added
22
+
23
+ * [HexaPDF::Task::PDFA] for creating PDF/A conforming PDF files
24
+ * [HexaPDF::Type::OutputIntent] for defining output intents
25
+ * [HexaPDF::Document::Metadata#delete] for deleting metadata properties
26
+ * PDF/A metadata properties definitions
27
+ * Added a /Name entry to the default optional content configuration dictionary
28
+ (needed by PDF/A)
29
+
30
+ ### Changed
31
+
32
+ * Default language for XMP metadata from English to 'x-default'
33
+ * [HexaPDF::Layout::ListBox] to use the style's font for drawing markers and to
34
+ fall back to Times and ZapfDingbats if necessary
35
+ * [HexaPDF::Document::Layout#table_box] to merge the `:cell` keys that define
36
+ the cell style instead of using the last one
37
+ * [HexaPDF::Document::Layout] style retrieval to fall back to using the font of
38
+ the `:base` style and only if that doesn't exist to 'Times'
39
+ * XMP metadata stream contents to satisfy more PDF/A validators
40
+
41
+
1
42
  ## 0.37.2 - 2024-02-27
2
43
 
3
44
  ### Fixed
data/README.md CHANGED
@@ -47,19 +47,19 @@ section](#License) for details.
47
47
  * [`hexapdf` binary][hp] for most common PDF manipulation tasks
48
48
 
49
49
 
50
- [canvas API]: https://hexapdf.gettalong.org/documentation/reference/api/HexaPDF/Content/Canvas.html
51
- [document composition engine]: https://hexapdf.gettalong.org/documentation/key-topics/document-layout.html
50
+ [canvas API]: https://hexapdf.gettalong.org/documentation/api/HexaPDF/Content/Canvas.html
51
+ [document composition engine]: https://hexapdf.gettalong.org/documentation/document-creation/document-layout.html
52
52
  [flowing text]: https://hexapdf.gettalong.org/examples/frame_text_flow.html
53
- [styles]: https://hexapdf.gettalong.org/documentation/reference/api/HexaPDF/Layout/Style/index.html
54
- [(un)ordered lists]: https://hexapdf.gettalong.org/documentation/reference/api/HexaPDF/Layout/ListBox.html
55
- [multi-column layout]: https://hexapdf.gettalong.org/documentation/reference/api/HexaPDF/Layout/ColumnBox.html
56
- [PDF forms]: https://hexapdf.gettalong.org/documentation/key-topics/forms.html
57
- [Document outline]: https://hexapdf.gettalong.org/documentation/reference/api/HexaPDF/Type/Outline.html
58
- [attaching files]: https://hexapdf.gettalong.org/documentation/reference/api/HexaPDF/Document/Files.html
59
- [Encryption]: https://hexapdf.gettalong.org/documentation/key-topics/encryption.html
60
- [Digital Signatures]: https://hexapdf.gettalong.org/documentation/key-topics/digital-signatures.html
53
+ [styles]: https://hexapdf.gettalong.org/documentation/api/HexaPDF/Layout/Style/index.html
54
+ [(un)ordered lists]: https://hexapdf.gettalong.org/documentation/api/HexaPDF/Layout/ListBox.html
55
+ [multi-column layout]: https://hexapdf.gettalong.org/documentation/api/HexaPDF/Layout/ColumnBox.html
56
+ [PDF forms]: https://hexapdf.gettalong.org/documentation/interactive-forms/index.html
57
+ [Document outline]: https://hexapdf.gettalong.org/documentation/outline/index.html
58
+ [attaching files]: https://hexapdf.gettalong.org/documentation/api/HexaPDF/Document/Files.html
59
+ [Encryption]: https://hexapdf.gettalong.org/documentation/encryption/index.html
60
+ [Digital Signatures]: https://hexapdf.gettalong.org/documentation/digital-signatures/index.html
61
61
  [File size optimization]: https://hexapdf.gettalong.org/documentation/benchmarks/optimization.html
62
- [hp]: https://hexapdf.gettalong.org/documentation/reference/hexapdf.1.html
62
+ [hp]: https://hexapdf.gettalong.org/documentation/hexapdf.1.html
63
63
 
64
64
 
65
65
  ## Usage
@@ -126,7 +126,7 @@ featureful API when it comes to creating content, for individual pages as well a
126
126
  If you want to migrate from Prawn to HexaPDF, there is the [migration guide] with detailed
127
127
  information and examples, comparing the Prawn API to HexaPDF's equivalents.
128
128
 
129
- [migration guide]: https://hexapdf.gettalong.org/documentation/howtos/migrating-from-prawn.html
129
+ [migration guide]: https://hexapdf.gettalong.org/documentation/document-creation/migrating-from-prawn.html
130
130
 
131
131
  Why use HexaPDF?
132
132
 
@@ -145,9 +145,10 @@ Why use HexaPDF?
145
145
  manipulating PDFs. This tool is intended to be a replacement for tools like `pdftk` and the
146
146
  various Poppler-based tools like `pdfinfo`, `pdfimages`, ...
147
147
 
148
- [Prawn]: http://prawnpdf.org
148
+ [Prawn]: https://prawnpdf.org
149
149
  [page canvas API]: https://hexapdf.gettalong.org/api/HexaPDF/Content/Canvas.html
150
150
 
151
+
151
152
  ## Development
152
153
 
153
154
  Clone the repository and then run `rake dev:setup`. This will install the needed Rubygem
@@ -178,6 +179,9 @@ Some included files have a different license:
178
179
  * The AES test vector files in `test/data/aes-test-vectors` have been created using the test vector
179
180
  file available from <http://csrc.nist.gov/groups/STM/cavp/block-ciphers.html#test-vectors>.
180
181
 
182
+ * The license of the file `data/hexapdf/sRGB2014.icc` is available in the
183
+ `data/hexapdf/sRGB2014.icc.LICENSE` file.
184
+
181
185
 
182
186
  ## Contributing
183
187
 
Binary file
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2015 International Color Consortium
2
+
3
+ This profile is made available by the International Color Consortium,
4
+ and may be copied, distributed, embedded, made, used, and sold without
5
+ restriction. Altered versions of this profile shall have the original
6
+ identification and copyright information removed and shall not be
7
+ misrepresented as the original profile.
@@ -0,0 +1,89 @@
1
+ # # PDF/A Conformance
2
+ #
3
+ # This example shows how to create a PDF file that is PDF/A compliant.
4
+ #
5
+ # In this case we are creating a simple invoice, with multiple line
6
+ # items that break across the page boundary.
7
+ #
8
+ # Usage:
9
+ # : `ruby pdfa.rb`
10
+ #
11
+ require 'hexapdf'
12
+
13
+ HexaPDF::Composer.create('pdfa.pdf') do |composer|
14
+ composer.document.task(:pdfa)
15
+ composer.document.config['font.map'] = {
16
+ 'Lato' => {
17
+ none: '/usr/share/fonts/truetype/lato/Lato-Regular.ttf',
18
+ bold: '/usr/share/fonts/truetype/lato/Lato-Bold.ttf',
19
+ italic: '/usr/share/fonts/truetype/lato/Lato-Italic.ttf',
20
+ bold_italic: '/usr/share/fonts/truetype/lato/Lato-BoldItalic.ttf',
21
+ },
22
+ }
23
+
24
+ company = {
25
+ name: 'Sample Corp Limited',
26
+ address: ["Example Avenue 1", "12345 Runway"],
27
+ }
28
+
29
+ # Define all styles
30
+ composer.style(:base, font: 'Lato', font_size: 10, line_spacing: 1.3)
31
+ composer.style(:top, font_size: 8)
32
+ composer.style(:top_box, padding: [100, 0, 0], margin: [0, 0, 10], border: {width: [0, 0, 1]})
33
+ composer.style(:header, font: ['Lato', variant: :bold], font_size: 20, margin: [50, 0, 20])
34
+ composer.style(:line_items, border: {width: 1, color: "eee"}, margin: [20, 0])
35
+ composer.style(:line_item_cell, font_size: 8)
36
+ composer.style(:footer, border: {width: [1, 0, 0], color: "darkgrey"},
37
+ padding: [5, 0, 0], valign: :bottom)
38
+ composer.style(:footer_heading, font: ['Lato', variant: :bold],
39
+ font_size: 8, padding: [0, 0, 8])
40
+ composer.style(:footer_text, font_size: 8, fill_color: "darkgrey")
41
+
42
+ # Top part
43
+ composer.box(:container, style: :top_box) do |container|
44
+ container.formatted_text([{text: company[:name], font: ['Lato', variant: :bold]},
45
+ " - " + company[:address].join(' - ')], style: :top)
46
+ end
47
+ composer.text("Mega Client\nSmall Lane 5\n67890 Noonestown", mask_mode: :box)
48
+ cells = [["Invoice number:", "2024/01"],
49
+ ["Invoice date", "2024-03-10"],
50
+ ["Service date:", "2024-02-01"]]
51
+ composer.table(cells, column_widths: [150, 80], style: {align: :right}) do |args|
52
+ args[] = {cell: {border: {width: 0}, padding: 2}, text_align: :right}
53
+ args[0..-1, 0] = {font: ['Lato', variant: :bold]}
54
+ end
55
+
56
+ # Middle part
57
+ composer.text("Invoice - 2024/01", style: :header)
58
+ composer.text("Thank you for your order. Following are the items you purchased:")
59
+
60
+ cells = [["Description", "Price", "Amount", "Total"]]
61
+ max = 40
62
+ 1.upto(max) do |index|
63
+ cells << ["Sample Item E.g. #{index}", "€ 250,00", index, "€ #{250 * index},00"]
64
+ end
65
+ cells << [nil, nil, nil, "€ #{250 * max * (max + 1) / 2},00"]
66
+ composer.table(cells, column_widths: [250, 80], style: :line_items) do |args|
67
+ args[] = {cell: {border: {width: 0}, padding: 8}, style: :line_item_cell}
68
+ args[0] = {cell: {background_color: "eee"}, font: ["Lato", variant: :bold]}
69
+ args[-1] = {cell: {background_color: "eee", border: {width: [2, 0, 0]}},
70
+ font: ["Lato", variant: :bold]}
71
+ args[0..-1, 1..-1] = {text_align: :right}
72
+ end
73
+
74
+ composer.text("Please transfer the total amount via SEPA transfer to the bank " \
75
+ "account below immediately after receiving the invoice - thank you.")
76
+
77
+ # Bottom part
78
+ l = composer.document.layout
79
+ cells = [
80
+ [l.text(company[:name], style: :footer_heading),
81
+ l.text(company[:address].join("\n"), style: :footer_text)],
82
+ [l.text('Contact', style: :footer_heading),
83
+ l.text("owner@samplecorp.com\nOwner: Me, Myself, And I", style: :footer_text)],
84
+ [l.text('Bank Account', style: :footer_heading),
85
+ l.text("Sample Corp Bank\nIBAN: SC01 2345 6789 0123 4567\nBIC: SACOZZB123",
86
+ style: :footer_text)],
87
+ ]
88
+ composer.table([cells], cell_style: {border: {width: 0}}, style: :footer)
89
+ end
@@ -568,6 +568,7 @@ module HexaPDF
568
568
  'task.map' => {
569
569
  optimize: 'HexaPDF::Task::Optimize',
570
570
  dereference: 'HexaPDF::Task::Dereference',
571
+ pdfa: 'HexaPDF::Task::PDFA',
571
572
  })
572
573
 
573
574
  # The global configuration object, providing the following options:
@@ -688,6 +689,8 @@ module HexaPDF
688
689
  XXCIDSystemInfo: 'HexaPDF::Type::CIDFont::CIDSystemInfo',
689
690
  Group: 'HexaPDF::Type::Form::Group',
690
691
  Metadata: 'HexaPDF::Type::Metadata',
692
+ OutputIntent: 'HexaPDF::Type::OutputIntent',
693
+ XXDestOutputProfileRef: 'HexaPDF::Type::OutputIntent::DestOutputProfileRef',
691
694
  },
692
695
  'object.subtype_map' => {
693
696
  nil => {
@@ -707,6 +710,9 @@ module HexaPDF
707
710
  Link: 'HexaPDF::Type::Annotations::Link',
708
711
  Widget: 'HexaPDF::Type::Annotations::Widget',
709
712
  XML: 'HexaPDF::Type::Metadata',
713
+ GTS_PDFX: 'HexaPDF::Type::OutputIntent',
714
+ GTS_PDFA1: 'HexaPDF::Type::OutputIntent',
715
+ ISO_PDFE1: 'HexaPDF::Type::OutputIntent',
710
716
  },
711
717
  XObject: {
712
718
  Image: 'HexaPDF::Type::Image',
@@ -738,6 +744,11 @@ module HexaPDF
738
744
  Ch: 'HexaPDF::Type::AcroForm::ChoiceField',
739
745
  Sig: 'HexaPDF::Type::AcroForm::SignatureField',
740
746
  },
747
+ OutputIntent: {
748
+ GTS_PDFX: 'HexaPDF::Type::OutputIntent',
749
+ GTS_PDFA1: 'HexaPDF::Type::OutputIntent',
750
+ ISO_PDFE1: 'HexaPDF::Type::OutputIntent',
751
+ },
741
752
  })
742
753
 
743
754
  end
@@ -486,10 +486,14 @@ module HexaPDF
486
486
 
487
487
  # Retrieves the merged keyword arguments for the cell in +row+ and +col+.
488
488
  #
489
- # Earlier defined arguments are overridden by later ones.
489
+ # Earlier defined arguments are overridden by later ones, except for the +:cell+ key which
490
+ # is merged.
490
491
  def retrieve_arguments_for(row, col)
491
492
  @argument_infos.each_with_object({}) do |arg_info, result|
492
493
  next unless arg_info.rows.cover?(row) && arg_info.cols.cover?(col)
494
+ if arg_info.args[:cell]
495
+ arg_info.args[:cell] = (result[:cell] || {}).merge(arg_info.args[:cell])
496
+ end
493
497
  result.update(arg_info.args)
494
498
  end
495
499
  end
@@ -635,15 +639,15 @@ module HexaPDF
635
639
  # If the +properties+ hash is not empty, the retrieved style is duplicated and the properties
636
640
  # hash is applied to it.
637
641
  #
638
- # Finally, a default font is set if necessary to ensure that the style object works in all
639
- # cases.
642
+ # Finally, a default font (the one from the :base style or otherwise 'Times') is set if
643
+ # necessary to ensure that the style object works in all cases.
640
644
  def retrieve_style(style, properties = nil)
641
645
  if style.kind_of?(Symbol) && !@styles.key?(style)
642
646
  raise HexaPDF::Error, "Style #{style} not defined"
643
647
  end
644
648
  style = HexaPDF::Layout::Style.create(@styles[style] || style || @styles[:base])
645
649
  style = style.dup.update(**properties) unless properties.nil? || properties.empty?
646
- style.font('Times') unless style.font?
650
+ style.font(@styles[:base].font? && @styles[:base].font || 'Times') unless style.font?
647
651
  unless style.font.respond_to?(:pdf_object)
648
652
  name, options = *style.font
649
653
  style.font(@document.fonts.add(name, **(options || {})))
@@ -80,6 +80,10 @@ module HexaPDF
80
80
  # String::
81
81
  # Maps to the XMP simple string value. Values need to be of type String.
82
82
  #
83
+ # Integer::
84
+ # Maps to the XMP integer core value type and gets formatted as string. Values need to be of
85
+ # type Integer.
86
+ #
83
87
  # Date::
84
88
  # Maps to the XMP simple string value, correctly formatted. Values need to be of type Time,
85
89
  # Date, or DateTime
@@ -123,6 +127,7 @@ module HexaPDF
123
127
  "pdf" => "http://ns.adobe.com/pdf/1.3/",
124
128
  "dc" => "http://purl.org/dc/elements/1.1/",
125
129
  "x" => "adobe:ns:meta/",
130
+ "pdfaid" => "http://www.aiim.org/pdfa/ns/id/",
126
131
  }.freeze
127
132
 
128
133
  # Contains a mapping of predefined XMP properties to their types, i.e. from namespace to
@@ -143,6 +148,10 @@ module HexaPDF
143
148
  'description' => 'LanguageArray',
144
149
  'title' => 'LanguageArray',
145
150
  }.freeze,
151
+ "http://www.aiim.org/pdfa/ns/id/" => {
152
+ 'part' => 'Integer',
153
+ 'conformance' => 'String',
154
+ }.freeze,
146
155
  }.freeze
147
156
 
148
157
  # Creates a new Metadata object for the given PDF document.
@@ -150,7 +159,7 @@ module HexaPDF
150
159
  @document = document
151
160
  @namespaces = PREDEFINED_NAMESPACES.dup
152
161
  @properties = PREDEFINED_PROPERTIES.transform_values(&:dup)
153
- @default_language = document.catalog[:Lang] || 'en'
162
+ @default_language = document.catalog[:Lang] || 'x-default'
154
163
  @metadata = Hash.new {|h, k| h[k] = {} }
155
164
  write_info_dict(true)
156
165
  write_metadata_stream(true)
@@ -166,7 +175,7 @@ module HexaPDF
166
175
  # is given. Otherwise sets the default language to the given language.
167
176
  #
168
177
  # The initial default lanuage is taken from the document catalog's /Lang entry. If that is not
169
- # set, the default language is assumed to be English ('en').
178
+ # set, the default language is assumed to be default language ('x-default').
170
179
  def default_language(value = :UNSET)
171
180
  if value == :UNSET
172
181
  @default_language
@@ -213,8 +222,8 @@ module HexaPDF
213
222
 
214
223
  # Registers the +property+ for the namespace specified via +prefix+ as the given +type+.
215
224
  #
216
- # The argument +type+ has to be one of the following: 'String', 'Date', 'URI', 'Boolean',
217
- # 'OrderedArray', 'UnorderedArray', or 'LanguageArray'.
225
+ # The argument +type+ has to be one of the following: 'String', 'Integer', 'Date', 'URI',
226
+ # 'Boolean', 'OrderedArray', 'UnorderedArray', or 'LanguageArray'.
218
227
  def register_property_type(prefix, property, type)
219
228
  (@properties[namespace(prefix)] ||= {})[property] = type
220
229
  end
@@ -240,13 +249,31 @@ module HexaPDF
240
249
  end
241
250
 
242
251
  # :call-seq:
243
- # metadata.title -> title or nil
244
- # metadata.title(value -> value
252
+ # metadata.delete
253
+ # metadata.delete(ns_prefix)
254
+ # metadata.delete(ns_prefix, name)
255
+ #
256
+ # Deletes either all metadata properties, only the ones from a specific namespace, or a
257
+ # specific one.
258
+ def delete(ns = nil, property = nil)
259
+ if ns.nil? && property.nil?
260
+ @metadata.clear
261
+ elsif property.nil?
262
+ @metadata.delete(namespace(ns))
263
+ else
264
+ @metadata[namespace(ns)].delete(property)
265
+ end
266
+ end
267
+
268
+ # :call-seq:
269
+ # metadata.title -> title or nil
270
+ # metadata.title(value) -> value
245
271
  #
246
272
  # Returns the document's title if no argument is given. Otherwise sets the document's title to
247
273
  # the given value.
248
274
  #
249
- # The language for the title is specified via #default_language.
275
+ # If the +value+ is a LocalizedString, the language for the title is taken from it. Otherwise
276
+ # the language specified via #default_language is used.
250
277
  #
251
278
  # The value +nil+ is returned if the property is not set. And by using +nil+ as +value+ the
252
279
  # property is deleted from the metadata.
@@ -278,7 +305,8 @@ module HexaPDF
278
305
  # Returns the subject of the document if no argument is given. Otherwise sets the subject to
279
306
  # the given value.
280
307
  #
281
- # The language for the subject is specified via #default_language.
308
+ # If the +value+ is a LocalizedString, the language for the subject is taken from it.
309
+ # Otherwise the language specified via #default_language is used.
282
310
  #
283
311
  # The value +nil+ is returned if the property ist not set. And by using +nil+ as +value+ the
284
312
  # property is deleted from the metadata.
@@ -406,23 +434,30 @@ module HexaPDF
406
434
  ns_xmp = namespace('xmp')
407
435
  ns_pdf = namespace('pdf')
408
436
 
437
+ producer("HexaPDF version #{HexaPDF::VERSION}")
438
+
409
439
  if write_info_dict?
410
440
  info_dict = @document.trailer.info
411
441
  info_dict[:Title] = Array(@metadata[ns_dc]['title']).first
412
- info_dict[:Author] = Array(@metadata[ns_dc]['creator']).join(', ')
442
+ if @metadata[ns_dc].key?('creator')
443
+ info_dict[:Author] = Array(@metadata[ns_dc]['creator']).join(', ')
444
+ end
413
445
  info_dict[:Subject] = Array(@metadata[ns_dc]['description']).first
414
446
  info_dict[:Creator] = @metadata[ns_xmp]['CreatorTool']
415
447
  info_dict[:CreationDate] = @metadata[ns_xmp]['CreateDate']
416
448
  info_dict[:ModDate] = @metadata[ns_xmp]['ModifyDate']
417
449
  info_dict[:Keywords] = @metadata[ns_pdf]['Keywords']
418
450
  info_dict[:Producer] = @metadata[ns_pdf]['Producer']
419
- info_dict[:Trapped] = @metadata[ns_pdf]['Trapped'] ? :True : :False
451
+ if @metadata[ns_pdf].key?('Trapped')
452
+ info_dict[:Trapped] = @metadata[ns_pdf]['Trapped'] ? :True : :False
453
+ end
420
454
  end
421
455
 
422
456
  if write_metadata_stream?
423
457
  descriptions = @metadata.map do |namespace, values|
458
+ next if values.empty?
424
459
  xmp_description(@namespaces.key(namespace), values)
425
- end.join("\n")
460
+ end.compact.join("\n")
426
461
  obj = @document.catalog[:Metadata] ||= @document.add({Type: :Metadata, Subtype: :XML})
427
462
  obj.stream = xmp_packet(descriptions)
428
463
  end
@@ -432,9 +467,11 @@ module HexaPDF
432
467
  def xmp_packet(data)
433
468
  <<~XMP
434
469
  <?xpacket begin="\u{FEFF}" id="#{SecureRandom.uuid.tr('-', '')}"?>
470
+ <x:xmpmeta xmlns:x="adobe:ns:meta/">
435
471
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
436
472
  #{data}
437
473
  </rdf:RDF>
474
+ </x:xmpmeta>
438
475
  <?xpacket end="r"?>
439
476
  XMP
440
477
  end
@@ -444,8 +481,8 @@ module HexaPDF
444
481
  values = values.map do |name, value|
445
482
  str = +"<#{ns_prefix}:#{name}"
446
483
  case (property_type = @properties[namespace(ns_prefix)][name])
447
- when 'String'
448
- str << ">#{xmp_escape(value)}</#{ns_prefix}:#{name}>"
484
+ when 'String', 'Integer'
485
+ str << ">#{xmp_escape(value.to_s)}</#{ns_prefix}:#{name}>"
449
486
  when 'Date'
450
487
  str << ">#{xmp_date(value)}</#{ns_prefix}:#{name}>"
451
488
  when 'URI'
@@ -179,8 +179,8 @@ module HexaPDF
179
179
  [column_left, column_bottom + height])
180
180
  shape = Geom2D::Algorithms::PolygonOperation.run(frame.shape, rect, :intersection)
181
181
  end
182
- column_frame = Frame.new(column_left, column_bottom, column_width, height,
183
- shape: shape, context: frame.context)
182
+ column_frame = frame.child_frame(column_left, column_bottom, column_width, height,
183
+ shape: shape, box: self)
184
184
  @box_fitter << column_frame
185
185
  end
186
186
 
@@ -131,8 +131,9 @@ module HexaPDF
131
131
 
132
132
  # Fits the children into the container.
133
133
  def fit_content(_available_width, _available_height, frame)
134
- my_frame = Frame.new(frame.x + reserved_width_left, frame.y - @height + reserved_height_bottom,
135
- content_width, content_height, context: frame.context)
134
+ my_frame = frame.child_frame(frame.x + reserved_width_left,
135
+ frame.y - @height + reserved_height_bottom,
136
+ content_width, content_height, box: self)
136
137
  @box_fitter = BoxFitter.new([my_frame])
137
138
  children.each {|box| @box_fitter.fit(box) }
138
139
 
@@ -91,6 +91,9 @@ module HexaPDF
91
91
  # Stores the result of fitting a box in a Frame.
92
92
  class FitResult
93
93
 
94
+ # The frame into which the box was fitted.
95
+ attr_accessor :frame
96
+
94
97
  # The box that was fitted into the frame.
95
98
  attr_accessor :box
96
99
 
@@ -110,8 +113,9 @@ module HexaPDF
110
113
  # drawing the box.
111
114
  attr_accessor :mask
112
115
 
113
- # Initialize the result object for the given box.
114
- def initialize(box)
116
+ # Initialize the result object for the given frame and box.
117
+ def initialize(frame, box)
118
+ @frame = frame
115
119
  @box = box
116
120
  @available_width = 0
117
121
  @available_height = 0
@@ -138,7 +142,10 @@ module HexaPDF
138
142
  def draw(canvas, dx: 0, dy: 0)
139
143
  doc = canvas.context.document
140
144
  if doc.config['debug']
141
- name = "#{box.class} (#{x.to_i},#{y.to_i}-#{box.width.to_i}x#{box.height.to_i})"
145
+ name = (frame.parent_boxes + [box]).map do |box|
146
+ box.class.to_s.sub(/.*::/, '')
147
+ end.join('-') << "##{box.object_id}"
148
+ name = "#{name} (#{(x + dx).to_i},#{(y + dy).to_i}-#{mask.width.to_i}x#{mask.height.to_i})"
142
149
  ocg = doc.optional_content.ocg(name)
143
150
  canvas.optional_content(ocg) do
144
151
  canvas.translate(dx, dy) do
@@ -147,7 +154,8 @@ module HexaPDF
147
154
  draw(:geom2d, object: mask, path_only: true).fill_stroke
148
155
  end
149
156
  end
150
- doc.optional_content.default_configuration.add_ocg_to_ui(ocg, path: 'Debug')
157
+ page = "Page #{canvas.context.index + 1}" rescue "XObject"
158
+ doc.optional_content.default_configuration.add_ocg_to_ui(ocg, path: ['Debug', page])
151
159
  end
152
160
  box.draw(canvas, x + dx, y + dy)
153
161
  end
@@ -195,14 +203,21 @@ module HexaPDF
195
203
  # should be used.
196
204
  attr_reader :context
197
205
 
206
+ # An array of box objects representing the parent boxes.
207
+ #
208
+ # The immediate parent is the last array entry, the top most parent the first one. All boxes
209
+ # that are fitted into this frame have to be child boxes of the immediate parent box.
210
+ attr_reader :parent_boxes
211
+
198
212
  # Creates a new Frame object for the given rectangular area.
199
- def initialize(left, bottom, width, height, shape: nil, context: nil)
213
+ def initialize(left, bottom, width, height, shape: nil, context: nil, parent_boxes: [])
200
214
  @left = left
201
215
  @bottom = bottom
202
216
  @width = width
203
217
  @height = height
204
218
  @shape = shape || create_rectangle(left, bottom, left + width, bottom + height)
205
219
  @context = context
220
+ @parent_boxes = parent_boxes.freeze
206
221
 
207
222
  @x = left
208
223
  @y = bottom + height
@@ -213,6 +228,25 @@ module HexaPDF
213
228
  @region_selection = :max_height
214
229
  end
215
230
 
231
+ # Creates a new Frame object based on this one.
232
+ #
233
+ # If the +init_args+ arguments are provided, a new Frame is created using the constructor. The
234
+ # optional +shape+ argument is then also passed to the constructor.
235
+ #
236
+ # Otherwise, this frame is duplicated. This kind of invocation is only useful if the +box+
237
+ # argument is provided (because otherwise there would be no difference to this frame).
238
+ #
239
+ # The +box+ argument can be used to add the appropriate parent box to the list of
240
+ # #parent_boxes for the newly created frame.
241
+ def child_frame(*init_args, shape: nil, box: nil)
242
+ parent_boxes = (box ? @parent_boxes.dup << box : @parent_boxes)
243
+ if init_args.empty?
244
+ dup.tap {|result| result.instance_variable_set(:@parent_boxes, parent_boxes) }
245
+ else
246
+ self.class.new(*init_args, shape: shape, context: @context, parent_boxes: parent_boxes)
247
+ end
248
+ end
249
+
216
250
  # Returns the HexaPDF::Document instance (through #context) that is associated with this Frame
217
251
  # object or +nil+ if no context object has been set.
218
252
  def document
@@ -227,7 +261,7 @@ module HexaPDF
227
261
  #
228
262
  # Use the FitResult#success? method to determine whether fitting was successful.
229
263
  def fit(box)
230
- fit_result = FitResult.new(box)
264
+ fit_result = FitResult.new(self, box)
231
265
  return fit_result if full?
232
266
 
233
267
  margin = box.style.margin if box.style.margin?
@@ -126,10 +126,17 @@ module HexaPDF
126
126
  height
127
127
  end
128
128
 
129
- # Fits the wrapped box, using the given context (see Frame#context).
130
- def fit_wrapped_box(context)
131
- @fit_result = Frame.new(0, 0, box.width, box.height == 0 ? 100_000 : box.height,
132
- context: context).fit(box)
129
+ # Fits the wrapped box.
130
+ #
131
+ # If the +frame+ argument is +nil+, a custom frame is created. Otherwise the given +frame+ is
132
+ # used for the fitting operation.
133
+ def fit_wrapped_box(frame)
134
+ frame = if frame
135
+ frame.child_frame(0, 0, box.width, box.height == 0 ? 100_000 : box.height)
136
+ else
137
+ Frame.new(0, 0, box.width, box.height == 0 ? 100_000 : box.height)
138
+ end
139
+ @fit_result = frame.fit(box)
133
140
  if !@fit_result.success?
134
141
  raise HexaPDF::Error, "Box for inline use could not be fit"
135
142
  elsif box.height > 99_000