Created
November 12, 2025 18:58
-
-
Save PlugFox/af42cf56d201edbb9af06850814642f3 to your computer and use it in GitHub Desktop.
Simple PDF Example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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