hexapdf 0.37.2 → 0.38.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 +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +17 -13
- data/data/hexapdf/sRGB2014.icc +0 -0
- data/data/hexapdf/sRGB2014.icc.LICENSE +7 -0
- data/examples/030-pdfa.rb +89 -0
- data/lib/hexapdf/configuration.rb +11 -0
- data/lib/hexapdf/document/layout.rb +8 -4
- data/lib/hexapdf/document/metadata.rb +50 -13
- data/lib/hexapdf/layout/list_box.rb +14 -11
- data/lib/hexapdf/task/pdfa.rb +87 -0
- data/lib/hexapdf/task.rb +1 -0
- data/lib/hexapdf/type/optional_content_properties.rb +2 -2
- data/lib/hexapdf/type/output_intent.rb +85 -0
- data/lib/hexapdf/type.rb +1 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/document/test_layout.rb +13 -0
- data/test/hexapdf/document/test_metadata.rb +60 -5
- data/test/hexapdf/layout/test_list_box.rb +20 -0
- data/test/hexapdf/task/test_pdfa.rb +41 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/test_optional_content_properties.rb +2 -2
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b6df8bc8bdf189f60287c3dc0a0eccb1e1bca2a8b4310c1d5c2bb5d6661bae11
|
|
4
|
+
data.tar.gz: a28fc9d3319f596774bda74476fbc77fe40ba8e29a2dcd1e588c18f0928e8538
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 11a382eceaa702705fe15484314e8d8b21c26ce2bf50d082be5e4ae27749251562474bfe1f4d410e53008120b37b6f43b42b1f9ecaca0ce123a672a6868a0515
|
|
7
|
+
data.tar.gz: ef98043f0d6460bbfd4819ba34fd2fab4d9e7426b04d0005f1f65ae88d813ff92db6f9cb89cb3e88997c8b68dea7e7631ceb873f5091a6c1b8bd6aa72075ab32
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## 0.38.0 - 2024-03-10
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
* [HexaPDF::Task::PDFA] for creating PDF/A conforming PDF files
|
|
6
|
+
* [HexaPDF::Type::OutputIntent] for defining output intents
|
|
7
|
+
* [HexaPDF::Document::Metadata#delete] for deleting metadata properties
|
|
8
|
+
* PDF/A metadata properties definitions
|
|
9
|
+
* Added a /Name entry to the default optional content configuration dictionary
|
|
10
|
+
(needed by PDF/A)
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
* Default language for XMP metadata from English to 'x-default'
|
|
15
|
+
* [HexaPDF::Layout::ListBox] to use the style's font for drawing markers and to
|
|
16
|
+
fall back to Times and ZapfDingbats if necessary
|
|
17
|
+
* [HexaPDF::Document::Layout#table_box] to merge the `:cell` keys that define
|
|
18
|
+
the cell style instead of using the last one
|
|
19
|
+
* [HexaPDF::Document::Layout] style retrieval to fall back to using the font of
|
|
20
|
+
the `:base` style and only if that doesn't exist to 'Times'
|
|
21
|
+
* XMP metadata stream contents to satisfy more PDF/A validators
|
|
22
|
+
|
|
23
|
+
|
|
1
24
|
## 0.37.2 - 2024-02-27
|
|
2
25
|
|
|
3
26
|
### 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/
|
|
51
|
-
[document composition engine]: https://hexapdf.gettalong.org/documentation/
|
|
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/
|
|
54
|
-
[(un)ordered lists]: https://hexapdf.gettalong.org/documentation/
|
|
55
|
-
[multi-column layout]: https://hexapdf.gettalong.org/documentation/
|
|
56
|
-
[PDF forms]: https://hexapdf.gettalong.org/documentation/
|
|
57
|
-
[Document outline]: https://hexapdf.gettalong.org/documentation/
|
|
58
|
-
[attaching files]: https://hexapdf.gettalong.org/documentation/
|
|
59
|
-
[Encryption]: https://hexapdf.gettalong.org/documentation/
|
|
60
|
-
[Digital Signatures]: https://hexapdf.gettalong.org/documentation/
|
|
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/
|
|
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/
|
|
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]:
|
|
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 Compliance
|
|
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
|
|
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] || '
|
|
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
|
|
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', '
|
|
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.
|
|
244
|
-
# metadata.
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
@@ -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("•",
|
|
331
|
-
font_size: style.font_size, fill_color: style.fill_color)
|
|
334
|
+
TextFragment.create("•", marker_style)
|
|
332
335
|
when :circle
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
@@ -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
|
|
data/lib/hexapdf/version.rb
CHANGED
|
@@ -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
|
|
31
|
-
assert_equal('
|
|
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="
|
|
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="
|
|
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>
|
|
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+/, ''))
|
|
@@ -143,6 +143,7 @@ describe HexaPDF::Layout::ListBox do
|
|
|
143
143
|
@canvas = @page.canvas
|
|
144
144
|
draw_block = lambda {|canvas, box| }
|
|
145
145
|
@fixed_size_boxes = 5.times.map { HexaPDF::Layout::Box.new(width: 20, height: 10, &draw_block) }
|
|
146
|
+
@helvetica = @doc.fonts.add('Helvetica')
|
|
146
147
|
end
|
|
147
148
|
|
|
148
149
|
it "draws the result" do
|
|
@@ -295,5 +296,24 @@ describe HexaPDF::Layout::ListBox do
|
|
|
295
296
|
]
|
|
296
297
|
assert_operators(@canvas.contents, operators)
|
|
297
298
|
end
|
|
299
|
+
|
|
300
|
+
it "uses the font set on the list box for the marker" do
|
|
301
|
+
box = create_box(children: @fixed_size_boxes[0, 1],
|
|
302
|
+
style: {font: @helvetica, font_size: 12})
|
|
303
|
+
box.fit(100, 100, @frame)
|
|
304
|
+
box.draw(@canvas, 0, 100 - box.height)
|
|
305
|
+
assert_operators(@canvas.contents, [:set_font_and_size, [:F1, 12]], range: 1)
|
|
306
|
+
assert_equal(:Helvetica, @canvas.resources.font(:F1)[:BaseFont])
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
it "falls back to ZapfDingbats if the set font doesn't contain the necessary symbol" do
|
|
310
|
+
box = create_box(children: @fixed_size_boxes[0, 1], marker_type: :circle,
|
|
311
|
+
style: {font: @helvetica})
|
|
312
|
+
box.fit(100, 100, @frame)
|
|
313
|
+
box.draw(@canvas, 0, 100 - box.height)
|
|
314
|
+
assert_operators(@canvas.contents, [:set_font_and_size, [:F1, 5]], range: 1)
|
|
315
|
+
assert_equal(:ZapfDingbats, @canvas.resources.font(:F1)[:BaseFont])
|
|
316
|
+
end
|
|
317
|
+
|
|
298
318
|
end
|
|
299
319
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
require 'test_helper'
|
|
5
|
+
require 'hexapdf/document'
|
|
6
|
+
|
|
7
|
+
describe HexaPDF::Task::PDFA do
|
|
8
|
+
before do
|
|
9
|
+
@doc = HexaPDF::Document.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "fails if the given PDF/A level is invalid" do
|
|
13
|
+
assert_raises(ArgumentError) { @doc.task(:pdfa, level: '1a') }
|
|
14
|
+
assert_raises(ArgumentError) { @doc.task(:pdfa, level: '2a') }
|
|
15
|
+
assert_raises(ArgumentError) { @doc.task(:pdfa, level: '3a') }
|
|
16
|
+
assert_raises(ArgumentError) { @doc.task(:pdfa, level: '4e') }
|
|
17
|
+
assert_raises(ArgumentError) { @doc.task(:pdfa, level: 'something') }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "removes the standard 14 PDF font loader" do
|
|
21
|
+
@doc.task(:pdfa)
|
|
22
|
+
assert_raises(HexaPDF::Error) { @doc.fonts.add('Helvetia') }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "adds the necessary XMP metadata entries before the document is written" do
|
|
26
|
+
@doc.task(:pdfa, level: '3b')
|
|
27
|
+
@doc.write(StringIO.new)
|
|
28
|
+
assert_equal('3', @doc.metadata.property('pdfaid', 'part'))
|
|
29
|
+
assert_equal('B', @doc.metadata.property('pdfaid', 'conformance'))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "adds an RGB output intent before the document is written" do
|
|
33
|
+
@doc.task(:pdfa)
|
|
34
|
+
@doc.write(StringIO.new)
|
|
35
|
+
oi = @doc.catalog[:OutputIntents].first
|
|
36
|
+
assert_equal(:GTS_PDFA1, oi[:S])
|
|
37
|
+
assert_equal('sRGB2014.icc', oi[:OutputConditionIdentifier])
|
|
38
|
+
assert_equal('sRGB2014.icc', oi[:Info])
|
|
39
|
+
assert_kind_of(HexaPDF::Stream, oi[:DestOutputProfile])
|
|
40
|
+
end
|
|
41
|
+
end
|
data/test/hexapdf/test_writer.rb
CHANGED
|
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
|
|
|
40
40
|
219
|
|
41
41
|
%%EOF
|
|
42
42
|
3 0 obj
|
|
43
|
-
<</Producer(HexaPDF version
|
|
43
|
+
<</Producer(HexaPDF version #{HexaPDF::VERSION})>>
|
|
44
44
|
endobj
|
|
45
45
|
xref
|
|
46
46
|
3 1
|
|
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
|
|
|
72
72
|
141
|
|
73
73
|
%%EOF
|
|
74
74
|
6 0 obj
|
|
75
|
-
<</Producer(HexaPDF version
|
|
75
|
+
<</Producer(HexaPDF version #{HexaPDF::VERSION})>>
|
|
76
76
|
endobj
|
|
77
77
|
2 0 obj
|
|
78
78
|
<</Length 10>>stream
|
|
@@ -85,7 +85,7 @@ describe HexaPDF::Type::OptionalContentProperties do
|
|
|
85
85
|
|
|
86
86
|
it "sets and returns a default configuration dictionary if none is set" do
|
|
87
87
|
@oc.delete(:D)
|
|
88
|
-
assert_equal({Creator: 'HexaPDF'}, @oc.default_configuration.value)
|
|
88
|
+
assert_equal({Name: 'Default', Creator: 'HexaPDF'}, @oc.default_configuration.value)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
it "sets the default configuration dictionary to the given value" do
|
|
@@ -103,7 +103,7 @@ describe HexaPDF::Type::OptionalContentProperties do
|
|
|
103
103
|
refute(@oc.validate(auto_correct: false))
|
|
104
104
|
refute(@oc.key?(:D))
|
|
105
105
|
assert(@oc.validate(auto_correct: true))
|
|
106
|
-
assert_equal({Creator: 'HexaPDF'}, @oc[:D].value)
|
|
106
|
+
assert_equal({Name: 'Default', Creator: 'HexaPDF'}, @oc[:D].value)
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
109
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hexapdf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.38.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Thomas Leitner
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2024-
|
|
11
|
+
date: 2024-03-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: cmdparse
|
|
@@ -274,6 +274,8 @@ files:
|
|
|
274
274
|
- data/hexapdf/cmap/V
|
|
275
275
|
- data/hexapdf/encoding/glyphlist.txt
|
|
276
276
|
- data/hexapdf/encoding/zapfdingbats.txt
|
|
277
|
+
- data/hexapdf/sRGB2014.icc
|
|
278
|
+
- data/hexapdf/sRGB2014.icc.LICENSE
|
|
277
279
|
- examples/001-hello_world.rb
|
|
278
280
|
- examples/002-graphics.rb
|
|
279
281
|
- examples/003-arcs.rb
|
|
@@ -303,6 +305,7 @@ files:
|
|
|
303
305
|
- examples/027-composer_optional_content.rb
|
|
304
306
|
- examples/028-frame_mask_mode.rb
|
|
305
307
|
- examples/029-composer_fallback_fonts.rb
|
|
308
|
+
- examples/030-pdfa.rb
|
|
306
309
|
- examples/emoji-smile.png
|
|
307
310
|
- examples/emoji-wink.png
|
|
308
311
|
- examples/machupicchu.jpg
|
|
@@ -464,6 +467,7 @@ files:
|
|
|
464
467
|
- lib/hexapdf/task.rb
|
|
465
468
|
- lib/hexapdf/task/dereference.rb
|
|
466
469
|
- lib/hexapdf/task/optimize.rb
|
|
470
|
+
- lib/hexapdf/task/pdfa.rb
|
|
467
471
|
- lib/hexapdf/test_utils.rb
|
|
468
472
|
- lib/hexapdf/tokenizer.rb
|
|
469
473
|
- lib/hexapdf/type.rb
|
|
@@ -515,6 +519,7 @@ files:
|
|
|
515
519
|
- lib/hexapdf/type/optional_content_properties.rb
|
|
516
520
|
- lib/hexapdf/type/outline.rb
|
|
517
521
|
- lib/hexapdf/type/outline_item.rb
|
|
522
|
+
- lib/hexapdf/type/output_intent.rb
|
|
518
523
|
- lib/hexapdf/type/page.rb
|
|
519
524
|
- lib/hexapdf/type/page_label.rb
|
|
520
525
|
- lib/hexapdf/type/page_tree_node.rb
|
|
@@ -715,6 +720,7 @@ files:
|
|
|
715
720
|
- test/hexapdf/layout/test_width_from_polygon.rb
|
|
716
721
|
- test/hexapdf/task/test_dereference.rb
|
|
717
722
|
- test/hexapdf/task/test_optimize.rb
|
|
723
|
+
- test/hexapdf/task/test_pdfa.rb
|
|
718
724
|
- test/hexapdf/test_composer.rb
|
|
719
725
|
- test/hexapdf/test_configuration.rb
|
|
720
726
|
- test/hexapdf/test_data_dir.rb
|