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