Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Created November 12, 2025 18:58
Show Gist options
  • Select an option

  • Save PlugFox/af42cf56d201edbb9af06850814642f3 to your computer and use it in GitHub Desktop.

Select an option

Save PlugFox/af42cf56d201edbb9af06850814642f3 to your computer and use it in GitHub Desktop.
Simple PDF Example
// dart run bin/make_pdf.dart
import 'dart:io';
import 'dart:typed_data';
void main() async {
final pdf = _PdfBuilder();
// Register built-in Type1 fonts (Base-14): Helvetica & Helvetica-Bold
final fontRegular = pdf.addRawObject(
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>',
);
final fontBold = pdf.addRawObject(
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>',
);
// Page size: A4 (points)
const mediaBox = '[0 0 595 842]';
// Build content stream: simple formatted text
final content =
StringBuffer()
// Begin text object
..writeln('BT')
// Set text leading (line spacing)
..writeln('14 TL')
// Move to start position (x=72pt, y=770pt)
..writeln('72 770 Td')
// Regular text, 12pt
..writeln('/F1 12 Tf')
..writeln('(Hello, PDF from pure Dart) Tj')
// New line: move down by 24pt
..writeln('0 -24 Td')
// Bold text, larger size
..writeln('/F2 18 Tf')
..writeln('(Bold headline) Tj')
// New line
..writeln('0 -28 Td')
// Back to regular 12pt
..writeln('/F1 12 Tf')
..writeln('(Left-aligned line) Tj')
// New line + little indent to the right
..writeln('0 -18 Td 24 0 Td')
..writeln('(Indented line) Tj')
// Another paragraph: reset X to 72pt, move further down
..writeln('T* T*') // two line feeds using TL
..writeln('ET'); // End text object
final contentStreamObj = pdf.addStreamObject(
content.toString(),
// Resources: reference fonts
// We'll attach resources on the Page object; stream only needs /Length
);
// Page object with resources (font dictionary)
final pageObj = pdf.addRawObject('''
<< /Type /Page
/Parent 2 0 R
/MediaBox $mediaBox
/Resources << /Font << /F1 ${fontRegular.id} 0 R /F2 ${fontBold.id} 0 R >> >>
/Contents ${contentStreamObj.id} 0 R
>>
''');
// Pages tree
final pagesObj = pdf.addRawObject('''
<< /Type /Pages
/Kids [ ${pageObj.id} 0 R ]
/Count 1
>>
''');
// Catalog
final catalogObj = pdf.addRawObject('''
<< /Type /Catalog
/Pages ${pagesObj.id} 0 R
>>
''');
// Finalize and write to file
final bytes = pdf.build(catalogObjId: catalogObj.id);
final file = File('sample.pdf');
await file.writeAsBytes(bytes, flush: true);
stdout.writeln('Written: ${file.absolute.path}');
}
/// Very small PDF builder that can emit:
/// - Header, numbered indirect objects
/// - Stream objects with correct /Length
/// - xref table, trailer, startxref
class _PdfBuilder {
// 1-based IDs in output order
_PdfBuilder() {
// PDF header; the binary comment line helps some readers detect binary data
_write('%PDF-1.4\n');
_write('%\xFF\xFF\xFF\xFF\n');
}
final BytesBuilder _buf = BytesBuilder();
final List<_Obj> _objects = [];
_Obj addRawObject(String body) {
final id = _objects.length + 1;
final obj = _Obj(id: id, isStream: false, body: body);
_objects.add(obj);
return obj;
}
_Obj addStreamObject(String streamData) {
final id = _objects.length + 1;
final obj = _Obj(id: id, isStream: true, body: streamData);
_objects.add(obj);
return obj;
}
Uint8List build({required int catalogObjId}) {
// Write all objects, recording their byte offsets
final offsets = <int>[0]; // dummy at index 0 to keep 1-based indexing
for (final obj in _objects) {
offsets.add(_offset);
_write('${obj.id} 0 obj\n');
if (obj.isStream) {
final data = obj.body;
final length = data.codeUnits.length;
_write('<< /Length $length >>\n');
_write('stream\n');
_write(data);
if (!data.endsWith('\n'))
_write('\n'); // ensure newline before endstream
_write('endstream\n');
_write('endobj\n');
} else {
_write('${obj.body.trimRight()}\n');
_write('endobj\n');
}
}
// xref
final xrefStart = _offset;
_write('xref\n');
_write('0 ${_objects.length + 1}\n');
// Object 0 entry (free)
_write('0000000000 65535 f \n');
// Object entries
for (final off in offsets.skip(1)) {
_write(_fmt10(off));
_write(' 00000 n \n');
}
// trailer
_write('trailer\n');
_write('<< /Size ${_objects.length + 1} /Root $catalogObjId 0 R >>\n');
_write('startxref\n');
_write('$xrefStart\n');
_write('%%EOF\n');
return _buf.toBytes();
}
// Helpers
void _write(String s) => _buf.add(s.codeUnits);
int get _offset => _buf.length;
String _fmt10(int v) {
// 10-digit, zero-padded
final s = v.toString();
if (s.length >= 10) return s.substring(s.length - 10);
return '0' * (10 - s.length) + s;
}
}
class _Obj {
_Obj({required this.id, required this.isStream, required this.body});
final int id;
final bool isStream;
final String body;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment