David PHAM-VAN

Simplify PDF generation

... ... @@ -20,8 +20,11 @@ import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'document_parser.dart';
import 'format/array.dart';
import 'format/num.dart';
import 'format/object_base.dart';
import 'format/stream.dart';
import 'format/string.dart';
import 'format/xref.dart';
import 'graphic_state.dart';
import 'io/vm.dart' if (dart.library.js) 'io/js.dart';
... ... @@ -36,7 +39,6 @@ import 'obj/page.dart';
import 'obj/page_label.dart';
import 'obj/page_list.dart';
import 'obj/signature.dart';
import 'output.dart';
/// Display hint for the PDF viewer
enum PdfPageMode {
... ... @@ -200,24 +202,36 @@ class PdfDocument {
/// This writes the document to an OutputStream.
Future<void> _write(PdfStream os) async {
final pos = PdfOutput(os, version, verbose);
// Write each object to the [PdfStream]. We call via the output
// as that builds the xref table
objects.where((e) => e.inUse).forEach(pos.write);
var lastFree = 0;
for (final obj in objects.where((e) => !e.inUse)) {
pos.xref.add(PdfXref(
obj.objser,
lastFree,
generation: obj.objgen,
type: PdfCrossRefEntryType.free,
));
lastFree = obj.objser;
PdfSignature? signature;
final xref = PdfXrefTable();
for (final ob in objects.where((e) => e.inUse)) {
ob.prepare();
if (ob is PdfInfo) {
xref.params['/Info'] = ob.ref();
} else if (ob is PdfEncryption) {
xref.params['/Encrypt'] = ob.ref();
} else if (ob is PdfSignature) {
assert(signature == null, 'Only one document signature is allowed');
signature = ob;
}
xref.objects.add(ob);
}
// Finally close the output, which writes the xref table.
await pos.close();
final id =
PdfString(documentID, format: PdfStringFormat.binary, encrypted: false);
xref.params['/ID'] = PdfArray([id, id]);
if (prev != null) {
xref.params['/Prev'] = PdfNum(prev!.xrefOffset);
}
xref.output(catalog, os);
if (signature != null) {
await signature.writeSignature(os);
}
}
/// Generate the PDF document as a memory file
... ...
... ... @@ -30,7 +30,7 @@ abstract class PdfDataType {
PdfStream _toStream() {
final s = PdfStream();
output(const PdfObjectBase(objser: 0), s);
output(PdfObjectBase(objser: 0, params: this), s);
return s;
}
... ...
... ... @@ -2,7 +2,7 @@ import 'dart:math' as math;
import 'package:meta/meta.dart';
import '../format/stream.dart';
import 'stream.dart';
mixin PdfDiagnostic {
static const _maxSize = 300;
... ... @@ -15,6 +15,8 @@ mixin PdfDiagnostic {
int get elapsedStopwatch => _stopwatch?.elapsedMicroseconds ?? 0;
int size = 0;
@protected
@mustCallSuper
void debugFill(String value) {
... ... @@ -29,10 +31,11 @@ mixin PdfDiagnostic {
}());
}
void setInsertion(PdfStream os) {
void setInsertion(PdfStream os, [int size = _maxSize]) {
assert(() {
this.size = size;
_offset = os.offset;
os.putComment(' ' * _maxSize);
os.putComment(' ' * size);
return true;
}());
}
... ... @@ -45,7 +48,7 @@ mixin PdfDiagnostic {
final b = o.output();
os.setBytes(
_offset!,
b.sublist(0, math.min(_maxSize + 2, b.lengthInBytes - 1)),
b.sublist(0, math.min(size + 2, b.lengthInBytes - 1)),
);
}
return true;
... ...
... ... @@ -99,6 +99,6 @@ class PdfDictStream extends PdfDict<PdfDataType> {
}
s.putString('stream\n');
s.putBytes(_data);
s.putString('\nendstream\n');
s.putString('\nendstream');
}
}
... ...
... ... @@ -16,7 +16,10 @@
import 'dart:typed_data';
import 'base.dart';
import 'diagnostic.dart';
import 'indirect.dart';
import 'stream.dart';
/// Callback used to compress the data
typedef DeflateCallback = List<int> Function(List<int> data);
... ... @@ -34,10 +37,11 @@ enum PdfVersion {
pdf_1_5,
}
class PdfObjectBase {
const PdfObjectBase({
class PdfObjectBase<T extends PdfDataType> with PdfDiagnostic {
PdfObjectBase({
required this.objser,
this.objgen = 0,
required this.params,
});
/// This is the unique serial number for this object.
... ... @@ -46,6 +50,8 @@ class PdfObjectBase {
/// This is the generation number for this object.
final int objgen;
final T params;
/// Callback used to compress the data
DeflateCallback? get deflate => null;
... ... @@ -59,4 +65,15 @@ class PdfObjectBase {
/// Returns the unique serial number in Pdf format
PdfIndirect ref() => PdfIndirect(objser, objgen);
void output(PdfStream s) {
s.putString('$objser $objgen obj\n');
writeContent(s);
s.putString('endobj\n');
}
void writeContent(PdfStream s) {
params.output(this, s, verbose ? 0 : null);
s.putByte(0x0a);
}
}
... ...
... ... @@ -19,6 +19,7 @@ import 'dart:typed_data';
import 'array.dart';
import 'base.dart';
import 'diagnostic.dart';
import 'dict.dart';
import 'dict_stream.dart';
import 'indirect.dart';
... ... @@ -95,16 +96,17 @@ class PdfXref {
int get hashCode => offset;
}
class PdfXrefTable extends PdfDataType {
class PdfXrefTable extends PdfDataType with PdfDiagnostic {
PdfXrefTable();
/// Contains offsets of each object
final offsets = <PdfXref>[];
/// Document root point
final params = PdfDict();
/// Add a cross reference element to the set
void add(PdfXref xref) {
offsets.add(xref);
}
/// List of objects to write
final objects = <PdfObjectBase>{};
/// Contains the offset of each objects
final _offsets = <PdfXref>[];
/// Writes a block of references to the Pdf file
void _writeBlock(PdfStream s, int firstId, List<PdfXref> block) {
... ... @@ -117,26 +119,109 @@ class PdfXrefTable extends PdfDataType {
}
@override
void output(PdfObjectBase o, PdfStream s, [int? indent]) {}
void output(PdfObjectBase o, PdfStream s, [int? indent]) {
String v;
switch (o.version) {
case PdfVersion.pdf_1_4:
v = '1.4';
break;
case PdfVersion.pdf_1_5:
v = '1.5';
break;
}
s.putString('%PDF-$v\n');
s.putBytes(const <int>[0x25, 0xC2, 0xA5, 0xC2, 0xB1, 0xC3, 0xAB, 0x0A]);
assert(() {
if (o.verbose) {
setInsertion(s);
startStopwatch();
debugFill('Verbose dart_pdf');
debugFill('Producer https://github.com/DavBfr/dart_pdf');
debugFill('Creation date: ${DateTime.now()}');
}
return true;
}());
for (final ob in objects) {
assert(() {
if (ob.verbose) {
ob.setInsertion(s, 150);
ob.startStopwatch();
}
return true;
}());
_offsets.add(PdfXref(ob.objser, s.offset, generation: ob.objgen));
ob.output(s);
assert(() {
if (ob.verbose) {
ob.stopStopwatch();
ob.debugFill(
'Creation time: ${ob.elapsedStopwatch / Duration.microsecondsPerSecond} seconds');
ob.writeDebug(s);
}
return true;
}());
}
final int xrefOffset;
params['/Root'] = o.ref();
switch (o.version) {
case PdfVersion.pdf_1_4:
xrefOffset = outputLegacy(o, s);
break;
case PdfVersion.pdf_1_5:
xrefOffset = outputCompressed(o, s);
break;
}
assert(() {
if (o.verbose) {
s.putComment('');
s.putComment('-' * 78);
s.putComment('$runtimeType');
}
return true;
}());
// the reference to the xref object
s.putString('startxref\n$xrefOffset\n%%EOF\n');
assert(() {
if (o.verbose) {
stopStopwatch();
debugFill(
'Creation time: ${elapsedStopwatch / Duration.microsecondsPerSecond} seconds');
debugFill('File size: ${s.offset} bytes');
// debugFill('Pages: ${rootID!.pdfDocument.pdfPageList.pages.length}');
debugFill('Objects: ${objects.length}');
writeDebug(s);
}
return true;
}());
}
@override
String toString() {
final s = StringBuffer();
for (final x in offsets) {
for (final x in _offsets) {
s.writeln(' $x');
}
return s.toString();
}
int outputLegacy(PdfObjectBase object, PdfStream s, PdfDict params) {
int outputLegacy(PdfObjectBase o, PdfStream s) {
// Now scan through the offsets list. They should be in sequence.
offsets.sort((a, b) => a.id.compareTo(b.id));
_offsets.sort((a, b) => a.id.compareTo(b.id));
final size = _offsets.last.id + 1;
assert(() {
if (object.verbose) {
if (o.verbose) {
s.putComment('');
s.putComment('-' * 78);
s.putComment('$runtimeType ${object.version.name}\n$this');
s.putComment('$runtimeType ${o.version.name}\n$this');
}
return true;
}());
... ... @@ -156,7 +241,7 @@ class PdfXrefTable extends PdfDataType {
final objOffset = s.offset;
s.putString('xref\n');
for (final x in offsets) {
for (final x in _offsets) {
// check to see if block is in range
if (x.id != (lastId + 1)) {
// no, so write this block, and reset
... ... @@ -175,31 +260,33 @@ class PdfXrefTable extends PdfDataType {
// the trailer object
assert(() {
if (object.verbose) {
if (o.verbose) {
s.putComment('');
}
return true;
}());
s.putString('trailer\n');
params.output(object, s, object.verbose ? 0 : null);
params['/Size'] = PdfNum(size);
params.output(o, s, o.verbose ? 0 : null);
s.putByte(0x0a);
return objOffset;
}
/// Output a compressed cross-reference table
int outputCompressed(PdfObjectBase object, PdfStream s, PdfDict params) {
int outputCompressed(PdfObjectBase o, PdfStream s) {
final offset = s.offset;
// Sort all references
offsets.sort((a, b) => a.id.compareTo(b.id));
_offsets.sort((a, b) => a.id.compareTo(b.id));
// Write this object too
final id = offsets.last.id + 1;
offsets.add(PdfXref(id, offset));
final id = _offsets.last.id + 1;
final size = id + 1;
_offsets.add(PdfXref(id, offset));
params['/Type'] = const PdfName('/XRef');
params['/Size'] = PdfNum(id + 1);
params['/Size'] = PdfNum(size);
var firstId = 0; // First id in block
var lastId = 0; // The last id used
... ... @@ -208,7 +295,7 @@ class PdfXrefTable extends PdfDataType {
// We need block 0 to exist
blocks.add(firstId);
for (final x in offsets) {
for (final x in _offsets) {
// check to see if block is in range
if (x.id != (lastId + 1)) {
// no, so store this block, and reset
... ... @@ -220,7 +307,7 @@ class PdfXrefTable extends PdfDataType {
}
blocks.add(lastId - firstId + 1);
if (!(blocks.length == 2 && blocks[0] == 0 && blocks[1] == id + 1)) {
if (!(blocks.length == 2 && blocks[0] == 0 && blocks[1] == size)) {
params['/Index'] = PdfArray.fromNum(blocks);
}
... ... @@ -229,21 +316,21 @@ class PdfXrefTable extends PdfDataType {
params['/W'] = PdfArray.fromNum(w);
final wl = w.reduce((a, b) => a + b);
final o = ByteData((offsets.length + 1) * wl);
final binOffsets = ByteData((_offsets.length + 1) * wl);
var ofs = 0;
// Write offset zero, all zeros
ofs += wl;
for (final x in offsets) {
ofs = x._compressedRef(o, ofs, w);
for (final x in _offsets) {
ofs = x._compressedRef(binOffsets, ofs, w);
}
// Write the object
assert(() {
if (object.verbose) {
if (o.verbose) {
s.putComment('');
s.putComment('-' * 78);
s.putComment('$runtimeType ${object.version.name}\n$this');
s.putComment('$runtimeType ${o.version.name}\n$this');
}
return true;
}());
... ... @@ -253,13 +340,13 @@ class PdfXrefTable extends PdfDataType {
s.putString('$id 0 obj\n');
PdfDictStream(
data: o.buffer.asUint8List(),
data: binOffsets.buffer.asUint8List(),
isBinary: false,
encrypt: false,
values: params.values,
).output(object, s, object.verbose ? 0 : null);
).output(o, s, o.verbose ? 0 : null);
s.putString('endobj\n');
s.putString('\nendobj\n');
return objOffset;
}
}
... ...
... ... @@ -19,26 +19,24 @@ import 'package:meta/meta.dart';
import '../document.dart';
import '../format/base.dart';
import '../format/object_base.dart';
import '../format/stream.dart';
import 'diagnostic.dart';
/// Base Object used in the PDF file
abstract class PdfObject<T extends PdfDataType> extends PdfObjectBase
with PdfDiagnostic {
abstract class PdfObject<T extends PdfDataType> extends PdfObjectBase<T> {
/// This is usually called by extensors to this class, and sets the
/// Pdf Object Type
PdfObject(
this.pdfDocument, {
required this.params,
required T params,
int objgen = 0,
int? objser,
}) : super(objser: objser ?? pdfDocument.genSerial(), objgen: objgen) {
}) : super(
objser: objser ?? pdfDocument.genSerial(),
objgen: objgen,
params: params,
) {
pdfDocument.objects.add(this);
}
/// This is the object parameters.
final T params;
/// This allows any Pdf object to refer to the document being constructed.
final PdfDocument pdfDocument;
... ... @@ -56,35 +54,10 @@ abstract class PdfObject<T extends PdfDataType> extends PdfObjectBase
@override
PdfVersion get version => pdfDocument.version;
/// Writes the object to the output stream.
void write(PdfStream os) {
prepare();
_writeStart(os);
writeContent(os);
_writeEnd(os);
}
/// Prepare the object to be written to the stream
@mustCallSuper
void prepare() {}
/// The write method should call this before writing anything to the
/// OutputStream. This will send the standard header for each object.
void _writeStart(PdfStream os) {
os.putString('$objser $objgen obj\n');
}
void writeContent(PdfStream os) {
params.output(this, os, verbose ? 0 : null);
os.putByte(0x0a);
}
/// The write method should call this after writing anything to the
/// OutputStream. This will send the standard footer for each object.
void _writeEnd(PdfStream os) {
os.putString('endobj\n');
}
@override
String toString() => '$runtimeType $params';
}
... ...
... ... @@ -36,10 +36,10 @@ class PdfObjectDict extends PdfObject<PdfDict> {
}
@override
void writeContent(PdfStream os) {
void writeContent(PdfStream s) {
if (params.isNotEmpty) {
params.output(this, os, pdfDocument.verbose ? 0 : null);
os.putByte(0x0a);
params.output(this, s, pdfDocument.verbose ? 0 : null);
s.putByte(0x0a);
}
}
}
... ...
... ... @@ -35,11 +35,12 @@ class PdfObjectStream extends PdfObjectDict {
final bool isBinary;
@override
void writeContent(PdfStream os) {
void writeContent(PdfStream s) {
PdfDictStream.values(
isBinary: isBinary,
values: params.values,
data: buf.output(),
).output(this, os, pdfDocument.verbose ? 0 : null);
).output(this, s, pdfDocument.verbose ? 0 : null);
s.putByte(0x0a);
}
}
... ...
... ... @@ -82,12 +82,12 @@ class PdfSignature extends PdfObjectDict {
int? _offsetEnd;
@override
void write(PdfStream os) {
void output(PdfStream s) {
value.preSign(this, params);
_offsetStart = os.offset + '$objser $objgen obj\n'.length;
super.write(os);
_offsetEnd = os.offset;
_offsetStart = s.offset + '$objser $objgen obj\n'.length;
super.output(s);
_offsetEnd = s.offset;
}
Future<void> writeSignature(PdfStream os) async {
... ...
/*
* Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'format/array.dart';
import 'format/dict.dart';
import 'format/num.dart';
import 'format/object_base.dart';
import 'format/stream.dart';
import 'format/string.dart';
import 'format/xref.dart';
import 'obj/catalog.dart';
import 'obj/diagnostic.dart';
import 'obj/encryption.dart';
import 'obj/info.dart';
import 'obj/object.dart';
import 'obj/signature.dart';
/// PDF document writer
class PdfOutput with PdfDiagnostic {
/// This creates a Pdf [PdfStream]
PdfOutput(this.os, this.version, this.verbose) {
String v;
switch (version) {
case PdfVersion.pdf_1_4:
v = '1.4';
break;
case PdfVersion.pdf_1_5:
v = '1.5';
break;
}
os.putString('%PDF-$v\n');
os.putBytes(const <int>[0x25, 0xC2, 0xA5, 0xC2, 0xB1, 0xC3, 0xAB, 0x0A]);
assert(() {
if (verbose) {
setInsertion(os);
startStopwatch();
debugFill('Verbose dart_pdf');
debugFill('Producer https://github.com/DavBfr/dart_pdf');
debugFill('Creation date: ${DateTime.now()}');
}
return true;
}());
}
/// Pdf version to output
final PdfVersion version;
/// This is the actual [PdfStream] used to write to.
final PdfStream os;
/// Cross reference table
final xref = PdfXrefTable();
/// This is used to track the /Root object (catalog)
PdfCatalog? rootID;
/// This is used to track the /Info object (info)
PdfInfo? infoID;
/// This is used to track the /Encrypt object (encryption)
PdfEncryption? encryptID;
/// This is used to track the /Sign object (signature)
PdfSignature? signatureID;
/// Generate a compressed cross reference table
bool get isCompressed => version.index > PdfVersion.pdf_1_4.index;
/// Verbose output
final bool verbose;
/// This method writes a [PdfObject] to the stream.
void write(PdfObject ob) {
// Check the object to see if it's one that is needed later
if (ob is PdfCatalog) {
rootID = ob;
} else if (ob is PdfInfo) {
infoID = ob;
} else if (ob is PdfEncryption) {
encryptID = ob;
} else if (ob is PdfSignature) {
assert(signatureID == null, 'Only one document signature is allowed');
signatureID = ob;
}
assert(() {
if (verbose) {
ob.setInsertion(os);
ob.startStopwatch();
}
return true;
}());
xref.add(PdfXref(ob.objser, os.offset, generation: ob.objgen));
ob.write(os);
assert(() {
if (verbose) {
ob.stopStopwatch();
ob.debugFill(
'Creation time: ${ob.elapsedStopwatch / Duration.microsecondsPerSecond} seconds');
ob.writeDebug(os);
}
return true;
}());
}
/// This closes the Stream, writing the xref table
Future<void> close() async {
if (rootID == null) {
throw Exception('Root object is not present in document');
}
final params = PdfDict();
// the number of entries (REQUIRED)
params['/Size'] = PdfNum(rootID!.pdfDocument.objser);
// the /Root catalog indirect reference (REQUIRED)
params['/Root'] = rootID!.ref();
final id = PdfString(rootID!.pdfDocument.documentID,
format: PdfStringFormat.binary, encrypted: false);
params['/ID'] = PdfArray([id, id]);
// the /Info reference (OPTIONAL)
if (infoID != null) {
params['/Info'] = infoID!.ref();
}
// the /Encrypt reference (OPTIONAL)
if (encryptID != null) {
params['/Encrypt'] = encryptID!.ref();
}
if (rootID!.pdfDocument.prev != null) {
params['/Prev'] = PdfNum(rootID!.pdfDocument.prev!.xrefOffset);
}
final _xref = isCompressed
? xref.outputCompressed(rootID!, os, params)
: xref.outputLegacy(rootID!, os, params);
assert(() {
if (verbose) {
os.putComment('');
os.putComment('-' * 78);
os.putComment('$runtimeType');
}
return true;
}());
// the reference to the xref object
os.putString('startxref\n$_xref\n%%EOF\n');
assert(() {
if (verbose) {
stopStopwatch();
debugFill(
'Creation time: ${elapsedStopwatch / Duration.microsecondsPerSecond} seconds');
debugFill('File size: ${os.offset} bytes');
debugFill('Pages: ${rootID!.pdfDocument.pdfPageList.pages.length}');
debugFill('Objects: ${xref.offsets.length}');
writeDebug(os);
}
return true;
}());
if (signatureID != null) {
await signatureID!.writeSignature(os);
}
}
}
... ... @@ -18,6 +18,7 @@ export 'pdf/format/array.dart';
export 'pdf/format/ascii85.dart';
export 'pdf/format/base.dart';
export 'pdf/format/bool.dart';
export 'pdf/format/diagnostic.dart';
export 'pdf/format/dict.dart';
export 'pdf/format/dict_stream.dart';
export 'pdf/format/indirect.dart';
... ...
... ... @@ -17,78 +17,74 @@
import 'dart:convert';
import 'dart:io';
import 'package:pdf/pdf.dart';
import 'package:pdf/src/priv.dart';
import 'package:test/test.dart';
class BasicObject extends PdfObjectBase {
const BasicObject(int objser) : super(objser: objser);
class BasicObject<T extends PdfDataType> extends PdfObjectBase<T> {
BasicObject({required super.objser, required super.params});
@override
bool get verbose => true;
void write(PdfStream os, PdfDataType value) {
os.putString('$objser $objgen obj\n');
value.output(this, os, verbose ? 0 : null);
os.putByte(0x0a);
os.putString('endobj\n');
}
@override
PdfVersion get version => PdfVersion.pdf_1_4;
@override
DeflateCallback? get deflate => zlib.encode;
}
void main() {
test('Pdf Minimal', () async {
final pages = PdfDict({
'/Type': const PdfName('/Pages'),
'/Count': const PdfNum(1),
});
final page = PdfDict({
'/Type': const PdfName('/Page'),
'/Parent': const PdfIndirect(2, 0),
'/MediaBox': PdfArray.fromNum([0, 0, 595.27559, 841.88976]),
'/Resources': PdfDict({
'/ProcSet': PdfArray([
const PdfName('/PDF'),
]),
}),
'/Contents': const PdfIndirect(4, 0),
});
final content = PdfDictStream(
data: latin1.encode('30 811.88976 m 200 641.88976 l S'),
);
pages['/Kids'] = PdfArray([const PdfIndirect(3, 0)]);
final catalog = PdfDict({
'/Type': const PdfName('/Catalog'),
'/Pages': const PdfIndirect(2, 0),
});
var objser = 1;
final os = PdfStream();
final pages = BasicObject(
objser: objser++,
params: PdfDict({
'/Type': const PdfName('/Pages'),
'/Count': const PdfNum(1),
}));
final xref = PdfXrefTable();
final content = BasicObject(
objser: objser++,
params: PdfDictStream(
data: latin1.encode('30 811.88976 m 200 641.88976 l S'),
));
final page = BasicObject(
objser: objser++,
params: PdfDict({
'/Type': const PdfName('/Page'),
'/Parent': pages.ref(),
'/MediaBox': PdfArray.fromNum([0, 0, 595.27559, 841.88976]),
'/Resources': PdfDict({
'/ProcSet': PdfArray([
const PdfName('/PDF'),
]),
}),
'/Contents': content.ref(),
}));
pages.params['/Kids'] = PdfArray([page.ref()]);
os.putString('%PDF-1.4\n');
os.putBytes(const <int>[0x25, 0xC2, 0xA5, 0xC2, 0xB1, 0xC3, 0xAB, 0x0A]);
xref.add(PdfXref(1, os.offset));
final cat = const BasicObject(1)..write(os, catalog);
xref.add(PdfXref(2, os.offset));
const BasicObject(2).write(os, pages);
xref.add(PdfXref(3, os.offset));
const BasicObject(3).write(os, page);
xref.add(PdfXref(4, os.offset));
const BasicObject(4).write(os, content);
final xrefOffset = xref.outputLegacy(
cat,
os,
PdfDict({
'/Size': PdfNum(xref.offsets.length + 1),
'/Root': const PdfIndirect(1, 0),
final catalog = BasicObject(
objser: objser++,
params: PdfDict({
'/Type': const PdfName('/Catalog'),
'/Pages': pages.ref(),
}));
os.putString('startxref\n$xrefOffset\n%%EOF\n');
final os = PdfStream();
final xref = PdfXrefTable();
xref.objects.addAll([
catalog,
pages,
page,
content,
]);
xref.output(catalog, os);
final file = File('minimal.pdf');
await file.writeAsBytes(os.output());
... ...