ilaurillard
Committed by GitHub

PDF/A 3b (#1750)

* PDF/A 3b

* PDF/A 3b

* PDF/A, reorganized into multiple files

* PDF/A, make all annotations printable as a default (for pdf/a)

* PDF/A, merging /Names when attaching files

* PDF/A, extended facturx-rdf

* Update pdf/lib/src/pdf/obj/pdfa/README.md

Co-authored-by: Colin Ihlenfeldt <clnmaier@gmail.com>

---------

Co-authored-by: David PHAM-VAN <1387855+DavBfr@users.noreply.github.com>
Co-authored-by: Colin Ihlenfeldt <clnmaier@gmail.com>
@@ -37,6 +37,10 @@ export 'src/pdf/obj/outline.dart'; @@ -37,6 +37,10 @@ export 'src/pdf/obj/outline.dart';
37 export 'src/pdf/obj/page.dart'; 37 export 'src/pdf/obj/page.dart';
38 export 'src/pdf/obj/page_label.dart'; 38 export 'src/pdf/obj/page_label.dart';
39 export 'src/pdf/obj/pattern.dart'; 39 export 'src/pdf/obj/pattern.dart';
  40 +export 'src/pdf/obj/pdfa/pdfa_attached_files.dart';
  41 +export 'src/pdf/obj/pdfa/pdfa_color_profile.dart';
  42 +export 'src/pdf/obj/pdfa/pdfa_facturx_rdf.dart';
  43 +export 'src/pdf/obj/pdfa/pdfa_rdf.dart';
40 export 'src/pdf/obj/shading.dart'; 44 export 'src/pdf/obj/shading.dart';
41 export 'src/pdf/obj/signature.dart'; 45 export 'src/pdf/obj/signature.dart';
42 export 'src/pdf/obj/smask.dart'; 46 export 'src/pdf/obj/smask.dart';
@@ -173,12 +173,16 @@ abstract class PdfAnnotBase { @@ -173,12 +173,16 @@ abstract class PdfAnnotBase {
173 this.border, 173 this.border,
174 this.content, 174 this.content,
175 this.name, 175 this.name,
176 - this.flags, 176 + Set<PdfAnnotFlags>? flags,
177 this.date, 177 this.date,
178 this.color, 178 this.color,
179 this.subject, 179 this.subject,
180 this.author, 180 this.author,
181 - }); 181 + }) {
  182 + this.flags = flags ?? {
  183 + PdfAnnotFlags.print,
  184 + };
  185 + }
182 186
183 /// The subtype of the outline, ie text, note, etc 187 /// The subtype of the outline, ie text, note, etc
184 final String subtype; 188 final String subtype;
@@ -201,7 +205,7 @@ abstract class PdfAnnotBase { @@ -201,7 +205,7 @@ abstract class PdfAnnotBase {
201 final String? subject; 205 final String? subject;
202 206
203 /// Flags specifying various characteristics of the annotation 207 /// Flags specifying various characteristics of the annotation
204 - final Set<PdfAnnotFlags>? flags; 208 + late final Set<PdfAnnotFlags> flags;
205 209
206 /// Last modification date 210 /// Last modification date
207 final DateTime? date; 211 final DateTime? date;
@@ -214,11 +218,11 @@ abstract class PdfAnnotBase { @@ -214,11 +218,11 @@ abstract class PdfAnnotBase {
214 PdfName? _as; 218 PdfName? _as;
215 219
216 int get flagValue { 220 int get flagValue {
217 - if (flags == null || flags!.isEmpty) { 221 + if (flags.isEmpty) {
218 return 0; 222 return 0;
219 } 223 }
220 224
221 - return flags! 225 + return flags
222 .map<int>((PdfAnnotFlags e) => 1 << e.index) 226 .map<int>((PdfAnnotFlags e) => 1 << e.index)
223 .reduce((int a, int b) => a | b); 227 .reduce((int a, int b) => a | b);
224 } 228 }
@@ -296,7 +300,7 @@ abstract class PdfAnnotBase { @@ -296,7 +300,7 @@ abstract class PdfAnnotBase {
296 params['/NM'] = PdfString.fromString(name!); 300 params['/NM'] = PdfString.fromString(name!);
297 } 301 }
298 302
299 - if (flags != null && flags!.isNotEmpty) { 303 + if (flags.isNotEmpty) {
300 params['/F'] = PdfNum(flagValue); 304 params['/F'] = PdfNum(flagValue);
301 } 305 }
302 306
@@ -26,6 +26,8 @@ import 'object.dart'; @@ -26,6 +26,8 @@ import 'object.dart';
26 import 'outline.dart'; 26 import 'outline.dart';
27 import 'page_label.dart'; 27 import 'page_label.dart';
28 import 'page_list.dart'; 28 import 'page_list.dart';
  29 +import 'pdfa/pdfa_attached_files.dart';
  30 +import 'pdfa/pdfa_color_profile.dart';
29 31
30 /// Pdf Catalog object 32 /// Pdf Catalog object
31 class PdfCatalog extends PdfObject<PdfDict> { 33 class PdfCatalog extends PdfObject<PdfDict> {
@@ -54,6 +56,12 @@ class PdfCatalog extends PdfObject<PdfDict> { @@ -54,6 +56,12 @@ class PdfCatalog extends PdfObject<PdfDict> {
54 /// The document metadata 56 /// The document metadata
55 PdfMetadata? metadata; 57 PdfMetadata? metadata;
56 58
  59 + /// Colorprofile output intent (Pdf/A)
  60 + PdfaColorProfile? colorProfile;
  61 +
  62 + /// Attached files (Pdf/A 3b)
  63 + PdfaAttachedFiles? attached;
  64 +
57 /// The initial page mode 65 /// The initial page mode
58 final PdfPageMode? pageMode; 66 final PdfPageMode? pageMode;
59 67
@@ -89,6 +97,13 @@ class PdfCatalog extends PdfObject<PdfDict> { @@ -89,6 +97,13 @@ class PdfCatalog extends PdfObject<PdfDict> {
89 params['/Metadata'] = metadata!.ref(); 97 params['/Metadata'] = metadata!.ref();
90 } 98 }
91 99
  100 + if (attached != null && attached!.isNotEmpty) {
  101 + //params['/Names'] = attached!.catalogNames();
  102 + names ??= PdfNames(pdfDocument);
  103 + names!.params.merge(attached!.catalogNames());
  104 + params['/AF'] = attached!.catalogAF();
  105 + }
  106 +
92 // the Names object 107 // the Names object
93 if (names != null) { 108 if (names != null) {
94 params['/Names'] = names!.ref(); 109 params['/Names'] = names!.ref();
@@ -158,5 +173,9 @@ class PdfCatalog extends PdfObject<PdfDict> { @@ -158,5 +173,9 @@ class PdfCatalog extends PdfObject<PdfDict> {
158 {'/Font': fontRefs}); 173 {'/Font': fontRefs});
159 } 174 }
160 } 175 }
  176 +
  177 + if (colorProfile != null) {
  178 + params['/OutputIntents'] = colorProfile!.outputIntents();
  179 + }
161 } 180 }
162 } 181 }
@@ -45,7 +45,8 @@ class PdfMetadata extends PdfObject<PdfDictStream> { @@ -45,7 +45,8 @@ class PdfMetadata extends PdfObject<PdfDictStream> {
45 @override 45 @override
46 void prepare() { 46 void prepare() {
47 super.prepare(); 47 super.prepare();
48 - params['/SubType'] = const PdfName('/XML'); 48 + params['/Type'] = const PdfName('/Metadata');
  49 + params['/Subtype'] = const PdfName('/XML');
49 params.data = Uint8List.fromList(utf8.encode(metadata.toString())); 50 params.data = Uint8List.fromList(utf8.encode(metadata.toString()));
50 } 51 }
51 } 52 }
  1 +Here are some classes to help you creating PDF/A compliant PDFs
  2 +plus embedding Facturx invoices.
  3 +
  4 +### Rules
  5 +
  6 +1. Your PDF must only use embedded Fonts,
  7 +2. For now you cannot use any Annotations in your PDF
  8 +3. You must include a special Meta-XML, use below "PdfaRdf" and put the reuslting XML document into your documents metadata
  9 +4. You must include a Colorprofile, use the below "PdfaColorProfile" and embed the contents of "sRGB2014.icc"
  10 +5. Optionally attach an InvoiceXML using "PdfaFacturxRdf" and "PdfaAttachedFiles"
  11 +
  12 +### Example
  13 +
  14 +```
  15 +pw.Document pdf = pw.Document(
  16 + ...
  17 + metadata: PdfaRdf(
  18 + ...
  19 + invoiceRdf: PdfaFacturxRdf().create()
  20 + ).create(),
  21 +);
  22 +
  23 +PdfaColorProfile(
  24 + pdf.document,
  25 + File('sRGB2014.icc').readAsBytesSync(),
  26 +);
  27 +
  28 +PdfaAttachedFiles(
  29 + pdf.document,
  30 + {
  31 + 'factur-x.xml': myInvoiceXmlDocument,
  32 + },
  33 +);
  34 +```
  35 +
  36 +### Validating
  37 +
  38 +https://demo.verapdf.org
  39 +https://avepdf.com/pdfa-validation
  40 +https://www.mustangproject.org
  1 +import 'dart:convert';
  2 +import 'dart:typed_data';
  3 +
  4 +import '../../document.dart';
  5 +import '../../format/array.dart';
  6 +import '../../format/base.dart';
  7 +import '../../format/dict.dart';
  8 +import '../../format/dict_stream.dart';
  9 +import '../../format/indirect.dart';
  10 +import '../../format/name.dart';
  11 +import '../../format/num.dart';
  12 +import '../../format/object_base.dart';
  13 +import '../../format/stream.dart';
  14 +import '../../format/string.dart';
  15 +import '../object.dart';
  16 +import 'pdfa_date_format.dart';
  17 +
  18 +class PdfaAttachedFiles {
  19 + PdfaAttachedFiles(
  20 + PdfDocument pdfDocument,
  21 + Map<String, String> files,
  22 + ) {
  23 + for (var entry in files.entries) {
  24 + _files.add(
  25 + _AttachedFileSpec(
  26 + pdfDocument,
  27 + _AttachedFile(
  28 + pdfDocument,
  29 + entry.key,
  30 + entry.value,
  31 + ),
  32 + ),
  33 + );
  34 + }
  35 + _names = _AttachedFileNames(
  36 + pdfDocument,
  37 + _files,
  38 + );
  39 + pdfDocument.catalog.attached = this;
  40 + }
  41 +
  42 + final List<_AttachedFileSpec> _files = [];
  43 +
  44 + late final _AttachedFileNames _names;
  45 +
  46 + bool get isNotEmpty => _files.isNotEmpty;
  47 +
  48 + PdfDict catalogNames() {
  49 + return PdfDict({
  50 + '/EmbeddedFiles': _names.ref(),
  51 + });
  52 + }
  53 +
  54 + PdfArray catalogAF() {
  55 + final tmp = <PdfIndirect>[];
  56 + for (var spec in _files) {
  57 + tmp.add(spec.ref());
  58 + }
  59 + return PdfArray(tmp);
  60 + }
  61 +}
  62 +
  63 +class _AttachedFileNames extends PdfObject<PdfDict> {
  64 + _AttachedFileNames(
  65 + PdfDocument pdfDocument,
  66 + this._files,
  67 + ) : super(
  68 + pdfDocument,
  69 + params: PdfDict(),
  70 + );
  71 + final List<_AttachedFileSpec> _files;
  72 +
  73 + @override
  74 + void prepare() {
  75 + super.prepare();
  76 + params['/Names'] = PdfArray(
  77 + [
  78 + _PdfRaw(0, _files.first),
  79 + ],
  80 + );
  81 + }
  82 +}
  83 +
  84 +class _AttachedFileSpec extends PdfObject<PdfDict> {
  85 + _AttachedFileSpec(
  86 + PdfDocument pdfDocument,
  87 + this._file,
  88 + ) : super(
  89 + pdfDocument,
  90 + params: PdfDict(),
  91 + );
  92 + final _AttachedFile _file;
  93 +
  94 + @override
  95 + void prepare() {
  96 + super.prepare();
  97 +
  98 + params['/Type'] = const PdfName('/Filespec');
  99 + params['/F'] = PdfString(
  100 + Uint8List.fromList(_file.fileName.codeUnits),
  101 + );
  102 + params['/UF'] = PdfString(
  103 + Uint8List.fromList(_file.fileName.codeUnits),
  104 + );
  105 + params['/EF'] = PdfDict({
  106 + '/F': _file.ref(),
  107 + });
  108 + params['/AFRelationship'] = const PdfName('/Unspecified');
  109 + }
  110 +}
  111 +
  112 +class _AttachedFile extends PdfObject<PdfDictStream> {
  113 + _AttachedFile(
  114 + PdfDocument pdfDocument,
  115 + this.fileName,
  116 + this.content,
  117 + ) : super(
  118 + pdfDocument,
  119 + params: PdfDictStream(
  120 + compress: false,
  121 + encrypt: false,
  122 + ),
  123 + );
  124 +
  125 + final String fileName;
  126 + final String content;
  127 +
  128 + @override
  129 + void prepare() {
  130 + super.prepare();
  131 +
  132 + final modDate = PdfaDateFormat().format(dt: DateTime.now());
  133 + params['/Type'] = const PdfName('/EmbeddedFile');
  134 + params['/Subtype'] = const PdfName('/application/octet-stream');
  135 + params['/Params'] = PdfDict({
  136 + '/Size': PdfNum(content.codeUnits.length),
  137 + '/ModDate': PdfString(
  138 + Uint8List.fromList('D:$modDate+00\'00\''.codeUnits),
  139 + ),
  140 + });
  141 +
  142 + params.data = Uint8List.fromList(utf8.encode(content));
  143 + }
  144 +}
  145 +
  146 +class _PdfRaw extends PdfDataType {
  147 + const _PdfRaw(
  148 + this.nr,
  149 + this.spec,
  150 + );
  151 +
  152 + final int nr;
  153 + final _AttachedFileSpec spec;
  154 +
  155 + @override
  156 + void output(
  157 + PdfObjectBase o,
  158 + PdfStream s, [
  159 + int? indent,
  160 + ]) {
  161 + s.putString('(${nr.toString().padLeft(3, '0')}) ${spec.ref()}');
  162 + }
  163 +}
  1 +import 'dart:typed_data';
  2 +
  3 +import '../../document.dart';
  4 +import '../../format/array.dart';
  5 +import '../../format/dict.dart';
  6 +import '../../format/dict_stream.dart';
  7 +import '../../format/name.dart';
  8 +import '../../format/num.dart';
  9 +import '../../format/string.dart';
  10 +import '../object.dart';
  11 +
  12 +class PdfaColorProfile extends PdfObject<PdfDictStream> {
  13 + PdfaColorProfile(
  14 + PdfDocument pdfDocument,
  15 + this.icc,
  16 + ) : super(
  17 + pdfDocument,
  18 + params: PdfDictStream(
  19 + compress: false,
  20 + encrypt: false,
  21 + ),
  22 + ) {
  23 + pdfDocument.catalog.colorProfile = this;
  24 + }
  25 +
  26 + final Uint8List icc;
  27 +
  28 + @override
  29 + void prepare() {
  30 + super.prepare();
  31 + params['/N'] = const PdfNum(3);
  32 + params.data = icc;
  33 + }
  34 +
  35 + PdfArray outputIntents() {
  36 + return PdfArray<PdfDict>([
  37 + PdfDict({
  38 + '/Type': const PdfName('/OutputIntent'),
  39 + '/S': const PdfName('/GTS_PDFA1'),
  40 + '/OutputConditionIdentifier':
  41 + PdfString(Uint8List.fromList('sRGB2014.icc'.codeUnits)),
  42 + '/Info': PdfString(Uint8List.fromList('sRGB2014.icc'.codeUnits)),
  43 + '/RegistryName':
  44 + PdfString(Uint8List.fromList('http://www.color.org'.codeUnits)),
  45 + '/DestOutputProfile': ref(),
  46 + }),
  47 + ]);
  48 + }
  49 +}
  1 +class PdfaDateFormat {
  2 + String format({
  3 + required DateTime dt,
  4 + bool asIso = false,
  5 + }) {
  6 + final year = dt.year.toString().padLeft(4, '0');
  7 + final month = dt.month.toString().padLeft(2, '0');
  8 + final day = dt.day.toString().padLeft(2, '0');
  9 + final hour = dt.hour.toString().padLeft(2, '0');
  10 + final minute = dt.minute.toString().padLeft(2, '0');
  11 + final second = dt.second.toString().padLeft(2, '0');
  12 +
  13 + if (asIso) {
  14 + // "yyyy-MM-dd'T'HH:mm:ss"
  15 + return '$year-$month-${day}T$hour:$minute:$second';
  16 + }
  17 + // "yyyyMMddHHmmss"
  18 + return '$year$month$day$hour$minute$second';
  19 + }
  20 +}
  1 +class PdfaFacturxRdf {
  2 + String create({
  3 + String filename = 'factur-x.xml',
  4 + String namespace = 'urn:cen.eu:invoice:1p0:schema#',
  5 + String conformanceLevel = 'BASIC',
  6 + String version = '1.0',
  7 + }) {
  8 + return '''
  9 +
  10 +<rdf:Description xmlns:fx="$namespace" rdf:about="">
  11 + <fx:DocumentType>INVOICE</fx:DocumentType>
  12 + <fx:DocumentFileName>$filename</fx:DocumentFileName>
  13 + <fx:Version>$version</fx:Version>
  14 + <fx:ConformanceLevel>$conformanceLevel</fx:ConformanceLevel>
  15 +</rdf:Description>
  16 +
  17 +<rdf:Description xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/"
  18 + xmlns:pdfaField="http://www.aiim.org/pdfa/ns/field#"
  19 + xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#"
  20 + xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#"
  21 + xmlns:pdfaType="http://www.aiim.org/pdfa/ns/type#"
  22 + rdf:about=""
  23 +>
  24 + <pdfaExtension:schemas>
  25 + <rdf:Bag>
  26 + <rdf:li rdf:parseType="Resource">
  27 + <pdfaSchema:schema>Invoice PDFA Extension Schema</pdfaSchema:schema>
  28 + <pdfaSchema:namespaceURI>$namespace</pdfaSchema:namespaceURI>
  29 + <pdfaSchema:prefix>fx</pdfaSchema:prefix>
  30 + <pdfaSchema:property>
  31 + <rdf:Seq>
  32 + <rdf:li rdf:parseType="Resource">
  33 + <pdfaProperty:name>DocumentFileName</pdfaProperty:name>
  34 + <pdfaProperty:valueType>Text</pdfaProperty:valueType>
  35 + <pdfaProperty:category>external</pdfaProperty:category>
  36 + <pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
  37 + </rdf:li>
  38 + <rdf:li rdf:parseType="Resource">
  39 + <pdfaProperty:name>DocumentType</pdfaProperty:name>
  40 + <pdfaProperty:valueType>Text</pdfaProperty:valueType>
  41 + <pdfaProperty:category>external</pdfaProperty:category>
  42 + <pdfaProperty:description>INVOICE</pdfaProperty:description>
  43 + </rdf:li>
  44 + <rdf:li rdf:parseType="Resource">
  45 + <pdfaProperty:name>Version</pdfaProperty:name>
  46 + <pdfaProperty:valueType>Text</pdfaProperty:valueType>
  47 + <pdfaProperty:category>external</pdfaProperty:category>
  48 + <pdfaProperty:description>The actual version of the ZUGFeRD data</pdfaProperty:description>
  49 + </rdf:li>
  50 + <rdf:li rdf:parseType="Resource">
  51 + <pdfaProperty:name>ConformanceLevel</pdfaProperty:name>
  52 + <pdfaProperty:valueType>Text</pdfaProperty:valueType>
  53 + <pdfaProperty:category>external</pdfaProperty:category>
  54 + <pdfaProperty:description>The conformance level of the ZUGFeRD data</pdfaProperty:description>
  55 + </rdf:li>
  56 + </rdf:Seq>
  57 + </pdfaSchema:property>
  58 + </rdf:li>
  59 + </rdf:Bag>
  60 + </pdfaExtension:schemas>
  61 +</rdf:Description>
  62 +''';
  63 + }
  64 +}
  1 +import 'package:xml/xml.dart';
  2 +
  3 +import 'pdfa_date_format.dart';
  4 +
  5 +class PdfaRdf {
  6 + PdfaRdf({
  7 + this.title,
  8 + this.author,
  9 + this.creator,
  10 + this.subject,
  11 + this.keywords,
  12 + this.producer,
  13 + DateTime? creationDate,
  14 + this.invoiceRdf = '',
  15 + }) {
  16 + this.creationDate = creationDate ?? DateTime.now();
  17 + }
  18 +
  19 + final String? title;
  20 + final String? author;
  21 + final String? creator;
  22 + final String? subject;
  23 + final String? keywords;
  24 + final String? producer;
  25 + late final DateTime creationDate;
  26 + final String invoiceRdf;
  27 +
  28 + XmlDocument? create() {
  29 + var createDate = PdfaDateFormat().format(dt: creationDate, asIso: true);
  30 + final offset = creationDate.timeZoneOffset;
  31 + final hours =
  32 + offset.inHours > 0 ? offset.inHours : 1; // For fixing divide by 0
  33 + if (!offset.isNegative) {
  34 + createDate =
  35 + "$createDate+${offset.inHours.toString().padLeft(2, '0')}:${(offset.inMinutes % (hours * 60)).toString().padLeft(2, '0')}";
  36 + } else {
  37 + createDate =
  38 + "$createDate-${(-offset.inHours).toString().padLeft(2, '0')}:${(offset.inMinutes % (hours * 60)).toString().padLeft(2, '0')}";
  39 + }
  40 +
  41 + return XmlDocument.parse('''
  42 +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
  43 +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
  44 + <rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
  45 + <pdf:Producer>$producer</pdf:Producer>
  46 + <pdf:Keywords>$keywords</pdf:Keywords>
  47 + </rdf:Description>
  48 + <rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/">
  49 + <xmp:CreateDate>$createDate</xmp:CreateDate>
  50 + <xmp:CreatorTool>$creator</xmp:CreatorTool>
  51 + </rdf:Description>
  52 + <rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">
  53 + <dc:creator><rdf:Seq><rdf:li>$author</rdf:li></rdf:Seq></dc:creator>
  54 + <dc:title><rdf:Alt><rdf:li xml:lang="x-default">$title</rdf:li></rdf:Alt></dc:title>
  55 + <dc:description><rdf:Alt><rdf:li xml:lang="x-default">$subject</rdf:li></rdf:Alt></dc:description>
  56 + </rdf:Description>
  57 + <rdf:Description rdf:about="" xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">
  58 + <pdfaid:part>3</pdfaid:part>
  59 + <pdfaid:conformance>B</pdfaid:conformance>
  60 + </rdf:Description>
  61 +
  62 + $invoiceRdf
  63 +
  64 +</rdf:RDF>
  65 +<?xpacket end="r"?>
  66 +''');
  67 + }
  68 +}
No preview for this file type