import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:archive/archive.dart'; import 'package:path/path.dart' as p; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; enum ExportFormat { pdf, pptx } extension ExportFormatExtension on ExportFormat { String get label { switch (this) { case ExportFormat.pdf: return 'PDF'; case ExportFormat.pptx: return 'PowerPoint (PPTX)'; } } String get extension { switch (this) { case ExportFormat.pdf: return '.pdf'; case ExportFormat.pptx: return '.pptx'; } } } class ExportResult { final bool success; final String? outputPath; final String? error; const ExportResult._({required this.success, this.outputPath, this.error}); factory ExportResult.ok(String path) => ExportResult._(success: true, outputPath: path); factory ExportResult.fail(String error) => ExportResult._(success: false, error: error); } /// Builds PDF and PPTX files from pre-rendered slide images (WYSIWYG export). /// Slides are expected to be 16:9 PNG bytes (see [SlideRasterizer]). class ExportService { // 16:9 widescreen slide size in EMU (English Metric Units): 13.333" x 7.5". static const int _slideWidthEmu = 12192000; static const int _slideHeightEmu = 6858000; /// Write [images] to a file derived from [deckPath] (same folder/base name) /// in the requested [format]. Future export( String deckPath, ExportFormat format, List images, ) async { if (images.isEmpty) { return ExportResult.fail('Geen slides om te exporteren.'); } final outputPath = '${p.withoutExtension(deckPath)}${format.extension}'; try { final Uint8List bytes; switch (format) { case ExportFormat.pdf: bytes = await _buildPdf(images); case ExportFormat.pptx: bytes = _buildPptx(images); } await File(outputPath).writeAsBytes(bytes, flush: true); return ExportResult.ok(outputPath); } catch (e) { return ExportResult.fail('Export fout: $e'); } } // ── PDF ─────────────────────────────────────────────────────────────────── Future _buildPdf(List images) async { final doc = pw.Document(); // Page size in points; only the ratio matters for a full-bleed image. const format = PdfPageFormat(1280, 720, marginAll: 0); for (final png in images) { final image = pw.MemoryImage(png); doc.addPage( pw.Page( pageFormat: format, build: (_) => pw.Image(image, fit: pw.BoxFit.fill), ), ); } return doc.save(); } // ── PPTX (Office Open XML) ───────────────────────────────────────────────── Uint8List _buildPptx(List images) { final archive = Archive(); void addText(String name, String content) { final data = utf8Bytes(content); archive.add(ArchiveFile(name, data.length, data)); } final slideCount = images.length; addText('[Content_Types].xml', _contentTypes(slideCount)); addText('_rels/.rels', _rootRels()); addText('ppt/presentation.xml', _presentationXml(slideCount)); addText('ppt/_rels/presentation.xml.rels', _presentationRels(slideCount)); addText('ppt/presProps.xml', _presProps()); addText('ppt/theme/theme1.xml', _theme1()); addText('ppt/slideMasters/slideMaster1.xml', _slideMaster()); addText('ppt/slideMasters/_rels/slideMaster1.xml.rels', _slideMasterRels()); addText('ppt/slideLayouts/slideLayout1.xml', _slideLayout()); addText('ppt/slideLayouts/_rels/slideLayout1.xml.rels', _slideLayoutRels()); for (var i = 0; i < slideCount; i++) { final n = i + 1; addText('ppt/slides/slide$n.xml', _slideXml()); addText('ppt/slides/_rels/slide$n.xml.rels', _slideRels(n)); final png = images[i]; archive.add(ArchiveFile('ppt/media/image$n.png', png.length, png)); } return ZipEncoder().encodeBytes(archive); } static List utf8Bytes(String s) => utf8.encode(s); String _contentTypes(int count) { final overrides = StringBuffer(); for (var i = 1; i <= count; i++) { overrides.write( '', ); } return '' '' '' '' '' '' '' '' '' '' '$overrides' ''; } String _rootRels() { return '' '' '' ''; } String _presentationXml(int count) { final sldIds = StringBuffer(); for (var i = 0; i < count; i++) { // Slide relationship ids start at rId2 (rId1 = master). sldIds.write(''); } return '' '' '' '$sldIds' '' '' ''; } String _presentationRels(int count) { final rels = StringBuffer(); rels.write( '', ); for (var i = 0; i < count; i++) { final n = i + 1; rels.write( '', ); } final presPropsId = 'rId${count + 2}'; final themeId = 'rId${count + 3}'; rels.write( '', ); rels.write( '', ); return '' '' '$rels' ''; } String _presProps() { return '' ''; } String _slideMaster() { return '' '' '' '' '${_emptySpTree()}' '' '' '' '' ''; } String _slideMasterRels() { return '' '' '' '' ''; } String _slideLayout() { return '' '' '${_emptySpTree()}' '' ''; } String _slideLayoutRels() { return '' '' '' ''; } String _slideXml() { return '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' ''; } String _slideRels(int n) { return '' '' '' '' ''; } String _emptySpTree() { return '' '' '' '' ''; } String _theme1() { return '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' ''; } }