Skip to content

Commit

Permalink
PDF/A 3b (#1750)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

---------

Co-authored-by: David PHAM-VAN <[email protected]>
Co-authored-by: Colin Ihlenfeldt <[email protected]>
  • Loading branch information
3 people authored Nov 28, 2024
1 parent 32c1e5e commit 41d3237
Show file tree
Hide file tree
Showing 11 changed files with 439 additions and 7 deletions.
4 changes: 4 additions & 0 deletions pdf/lib/pdf.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export 'src/pdf/obj/outline.dart';
export 'src/pdf/obj/page.dart';
export 'src/pdf/obj/page_label.dart';
export 'src/pdf/obj/pattern.dart';
export 'src/pdf/obj/pdfa/pdfa_attached_files.dart';
export 'src/pdf/obj/pdfa/pdfa_color_profile.dart';
export 'src/pdf/obj/pdfa/pdfa_facturx_rdf.dart';
export 'src/pdf/obj/pdfa/pdfa_rdf.dart';
export 'src/pdf/obj/shading.dart';
export 'src/pdf/obj/signature.dart';
export 'src/pdf/obj/smask.dart';
Expand Down
16 changes: 10 additions & 6 deletions pdf/lib/src/pdf/obj/annotation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,16 @@ abstract class PdfAnnotBase {
this.border,
this.content,
this.name,
this.flags,
Set<PdfAnnotFlags>? flags,
this.date,
this.color,
this.subject,
this.author,
});
}) {
this.flags = flags ?? {
PdfAnnotFlags.print,
};
}

/// The subtype of the outline, ie text, note, etc
final String subtype;
Expand All @@ -201,7 +205,7 @@ abstract class PdfAnnotBase {
final String? subject;

/// Flags specifying various characteristics of the annotation
final Set<PdfAnnotFlags>? flags;
late final Set<PdfAnnotFlags> flags;

/// Last modification date
final DateTime? date;
Expand All @@ -214,11 +218,11 @@ abstract class PdfAnnotBase {
PdfName? _as;

int get flagValue {
if (flags == null || flags!.isEmpty) {
if (flags.isEmpty) {
return 0;
}

return flags!
return flags
.map<int>((PdfAnnotFlags e) => 1 << e.index)
.reduce((int a, int b) => a | b);
}
Expand Down Expand Up @@ -296,7 +300,7 @@ abstract class PdfAnnotBase {
params['/NM'] = PdfString.fromString(name!);
}

if (flags != null && flags!.isNotEmpty) {
if (flags.isNotEmpty) {
params['/F'] = PdfNum(flagValue);
}

Expand Down
19 changes: 19 additions & 0 deletions pdf/lib/src/pdf/obj/catalog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import 'object.dart';
import 'outline.dart';
import 'page_label.dart';
import 'page_list.dart';
import 'pdfa/pdfa_attached_files.dart';
import 'pdfa/pdfa_color_profile.dart';

/// Pdf Catalog object
class PdfCatalog extends PdfObject<PdfDict> {
Expand Down Expand Up @@ -54,6 +56,12 @@ class PdfCatalog extends PdfObject<PdfDict> {
/// The document metadata
PdfMetadata? metadata;

/// Colorprofile output intent (Pdf/A)
PdfaColorProfile? colorProfile;

/// Attached files (Pdf/A 3b)
PdfaAttachedFiles? attached;

/// The initial page mode
final PdfPageMode? pageMode;

Expand Down Expand Up @@ -89,6 +97,13 @@ class PdfCatalog extends PdfObject<PdfDict> {
params['/Metadata'] = metadata!.ref();
}

if (attached != null && attached!.isNotEmpty) {
//params['/Names'] = attached!.catalogNames();
names ??= PdfNames(pdfDocument);
names!.params.merge(attached!.catalogNames());
params['/AF'] = attached!.catalogAF();
}

// the Names object
if (names != null) {
params['/Names'] = names!.ref();
Expand Down Expand Up @@ -158,5 +173,9 @@ class PdfCatalog extends PdfObject<PdfDict> {
{'/Font': fontRefs});
}
}

if (colorProfile != null) {
params['/OutputIntents'] = colorProfile!.outputIntents();
}
}
}
3 changes: 2 additions & 1 deletion pdf/lib/src/pdf/obj/metadata.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ class PdfMetadata extends PdfObject<PdfDictStream> {
@override
void prepare() {
super.prepare();
params['/SubType'] = const PdfName('/XML');
params['/Type'] = const PdfName('/Metadata');
params['/Subtype'] = const PdfName('/XML');
params.data = Uint8List.fromList(utf8.encode(metadata.toString()));
}
}
40 changes: 40 additions & 0 deletions pdf/lib/src/pdf/obj/pdfa/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
Here are some classes to help you creating PDF/A compliant PDFs
plus embedding Facturx invoices.

### Rules

1. Your PDF must only use embedded Fonts,
2. For now you cannot use any Annotations in your PDF
3. You must include a special Meta-XML, use below "PdfaRdf" and put the reuslting XML document into your documents metadata
4. You must include a Colorprofile, use the below "PdfaColorProfile" and embed the contents of "sRGB2014.icc"
5. Optionally attach an InvoiceXML using "PdfaFacturxRdf" and "PdfaAttachedFiles"

### Example

```
pw.Document pdf = pw.Document(
...
metadata: PdfaRdf(
...
invoiceRdf: PdfaFacturxRdf().create()
).create(),
);
PdfaColorProfile(
pdf.document,
File('sRGB2014.icc').readAsBytesSync(),
);
PdfaAttachedFiles(
pdf.document,
{
'factur-x.xml': myInvoiceXmlDocument,
},
);
```

### Validating

https://demo.verapdf.org
https://avepdf.com/pdfa-validation
https://www.mustangproject.org
163 changes: 163 additions & 0 deletions pdf/lib/src/pdf/obj/pdfa/pdfa_attached_files.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import 'dart:convert';
import 'dart:typed_data';

import '../../document.dart';
import '../../format/array.dart';
import '../../format/base.dart';
import '../../format/dict.dart';
import '../../format/dict_stream.dart';
import '../../format/indirect.dart';
import '../../format/name.dart';
import '../../format/num.dart';
import '../../format/object_base.dart';
import '../../format/stream.dart';
import '../../format/string.dart';
import '../object.dart';
import 'pdfa_date_format.dart';

class PdfaAttachedFiles {
PdfaAttachedFiles(
PdfDocument pdfDocument,
Map<String, String> files,
) {
for (var entry in files.entries) {
_files.add(
_AttachedFileSpec(
pdfDocument,
_AttachedFile(
pdfDocument,
entry.key,
entry.value,
),
),
);
}
_names = _AttachedFileNames(
pdfDocument,
_files,
);
pdfDocument.catalog.attached = this;
}

final List<_AttachedFileSpec> _files = [];

late final _AttachedFileNames _names;

bool get isNotEmpty => _files.isNotEmpty;

PdfDict catalogNames() {
return PdfDict({
'/EmbeddedFiles': _names.ref(),
});
}

PdfArray catalogAF() {
final tmp = <PdfIndirect>[];
for (var spec in _files) {
tmp.add(spec.ref());
}
return PdfArray(tmp);
}
}

class _AttachedFileNames extends PdfObject<PdfDict> {
_AttachedFileNames(
PdfDocument pdfDocument,
this._files,
) : super(
pdfDocument,
params: PdfDict(),
);
final List<_AttachedFileSpec> _files;

@override
void prepare() {
super.prepare();
params['/Names'] = PdfArray(
[
_PdfRaw(0, _files.first),
],
);
}
}

class _AttachedFileSpec extends PdfObject<PdfDict> {
_AttachedFileSpec(
PdfDocument pdfDocument,
this._file,
) : super(
pdfDocument,
params: PdfDict(),
);
final _AttachedFile _file;

@override
void prepare() {
super.prepare();

params['/Type'] = const PdfName('/Filespec');
params['/F'] = PdfString(
Uint8List.fromList(_file.fileName.codeUnits),
);
params['/UF'] = PdfString(
Uint8List.fromList(_file.fileName.codeUnits),
);
params['/EF'] = PdfDict({
'/F': _file.ref(),
});
params['/AFRelationship'] = const PdfName('/Unspecified');
}
}

class _AttachedFile extends PdfObject<PdfDictStream> {
_AttachedFile(
PdfDocument pdfDocument,
this.fileName,
this.content,
) : super(
pdfDocument,
params: PdfDictStream(
compress: false,
encrypt: false,
),
);

final String fileName;
final String content;

@override
void prepare() {
super.prepare();

final modDate = PdfaDateFormat().format(dt: DateTime.now());
params['/Type'] = const PdfName('/EmbeddedFile');
params['/Subtype'] = const PdfName('/application/octet-stream');
params['/Params'] = PdfDict({
'/Size': PdfNum(content.codeUnits.length),
'/ModDate': PdfString(
Uint8List.fromList('D:$modDate+00\'00\''.codeUnits),
),
});

params.data = Uint8List.fromList(utf8.encode(content));
}
}

class _PdfRaw extends PdfDataType {
const _PdfRaw(
this.nr,
this.spec,
);

final int nr;
final _AttachedFileSpec spec;

@override
void output(
PdfObjectBase o,
PdfStream s, [
int? indent,
]) {
s.putString('(${nr.toString().padLeft(3, '0')}) ${spec.ref()}');
}
}
49 changes: 49 additions & 0 deletions pdf/lib/src/pdf/obj/pdfa/pdfa_color_profile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'dart:typed_data';

import '../../document.dart';
import '../../format/array.dart';
import '../../format/dict.dart';
import '../../format/dict_stream.dart';
import '../../format/name.dart';
import '../../format/num.dart';
import '../../format/string.dart';
import '../object.dart';

class PdfaColorProfile extends PdfObject<PdfDictStream> {
PdfaColorProfile(
PdfDocument pdfDocument,
this.icc,
) : super(
pdfDocument,
params: PdfDictStream(
compress: false,
encrypt: false,
),
) {
pdfDocument.catalog.colorProfile = this;
}

final Uint8List icc;

@override
void prepare() {
super.prepare();
params['/N'] = const PdfNum(3);
params.data = icc;
}

PdfArray outputIntents() {
return PdfArray<PdfDict>([
PdfDict({
'/Type': const PdfName('/OutputIntent'),
'/S': const PdfName('/GTS_PDFA1'),
'/OutputConditionIdentifier':
PdfString(Uint8List.fromList('sRGB2014.icc'.codeUnits)),
'/Info': PdfString(Uint8List.fromList('sRGB2014.icc'.codeUnits)),
'/RegistryName':
PdfString(Uint8List.fromList('http://www.color.org'.codeUnits)),
'/DestOutputProfile': ref(),
}),
]);
}
}
20 changes: 20 additions & 0 deletions pdf/lib/src/pdf/obj/pdfa/pdfa_date_format.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class PdfaDateFormat {
String format({
required DateTime dt,
bool asIso = false,
}) {
final year = dt.year.toString().padLeft(4, '0');
final month = dt.month.toString().padLeft(2, '0');
final day = dt.day.toString().padLeft(2, '0');
final hour = dt.hour.toString().padLeft(2, '0');
final minute = dt.minute.toString().padLeft(2, '0');
final second = dt.second.toString().padLeft(2, '0');

if (asIso) {
// "yyyy-MM-dd'T'HH:mm:ss"
return '$year-$month-${day}T$hour:$minute:$second';
}
// "yyyyMMddHHmmss"
return '$year$month$day$hour$minute$second';
}
}
Loading

0 comments on commit 41d3237

Please sign in to comment.