Ocideck/lib/services/export_service.dart

356 lines
15 KiB
Dart
Raw Normal View History

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<ExportResult> export(
String deckPath,
ExportFormat format,
List<Uint8List> 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<Uint8List> _buildPdf(List<Uint8List> 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<Uint8List> 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<int> utf8Bytes(String s) => utf8.encode(s);
String _contentTypes(int count) {
final overrides = StringBuffer();
for (var i = 1; i <= count; i++) {
overrides.write(
'<Override PartName="/ppt/slides/slide$i.xml" '
'ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>',
);
}
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
'<Default Extension="xml" ContentType="application/xml"/>'
'<Default Extension="png" ContentType="image/png"/>'
'<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>'
'<Override PartName="/ppt/presProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"/>'
'<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>'
'<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>'
'<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>'
'$overrides'
'</Types>';
}
String _rootRels() {
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>'
'</Relationships>';
}
String _presentationXml(int count) {
final sldIds = StringBuffer();
for (var i = 0; i < count; i++) {
// Slide relationship ids start at rId2 (rId1 = master).
sldIds.write('<p:sldId id="${256 + i}" r:id="rId${i + 2}"/>');
}
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">'
'<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>'
'<p:sldIdLst>$sldIds</p:sldIdLst>'
'<p:sldSz cx="$_slideWidthEmu" cy="$_slideHeightEmu" type="screen16x9"/>'
'<p:notesSz cx="6858000" cy="9144000"/>'
'</p:presentation>';
}
String _presentationRels(int count) {
final rels = StringBuffer();
rels.write(
'<Relationship Id="rId1" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" '
'Target="slideMasters/slideMaster1.xml"/>',
);
for (var i = 0; i < count; i++) {
final n = i + 1;
rels.write(
'<Relationship Id="rId${i + 2}" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" '
'Target="slides/slide$n.xml"/>',
);
}
final presPropsId = 'rId${count + 2}';
final themeId = 'rId${count + 3}';
rels.write(
'<Relationship Id="$presPropsId" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps" '
'Target="presProps.xml"/>',
);
rels.write(
'<Relationship Id="$themeId" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" '
'Target="theme/theme1.xml"/>',
);
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'$rels'
'</Relationships>';
}
String _presProps() {
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<p:presentationPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"/>';
}
String _slideMaster() {
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">'
'<p:cSld>'
'<p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef></p:bg>'
'${_emptySpTree()}'
'</p:cSld>'
'<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" '
'accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" '
'accent6="accent6" hlink="hlink" folHlink="folHlink"/>'
'<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>'
'<p:txStyles><p:titleStyle/><p:bodyStyle/><p:otherStyle/></p:txStyles>'
'</p:sldMaster>';
}
String _slideMasterRels() {
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'<Relationship Id="rId1" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" '
'Target="../slideLayouts/slideLayout1.xml"/>'
'<Relationship Id="rId2" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" '
'Target="../theme/theme1.xml"/>'
'</Relationships>';
}
String _slideLayout() {
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" '
'type="blank" preserve="1">'
'<p:cSld name="Leeg">${_emptySpTree()}</p:cSld>'
'<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>'
'</p:sldLayout>';
}
String _slideLayoutRels() {
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'<Relationship Id="rId1" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" '
'Target="../slideMasters/slideMaster1.xml"/>'
'</Relationships>';
}
String _slideXml() {
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">'
'<p:cSld><p:spTree>'
'<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>'
'<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/>'
'<a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>'
'<p:pic>'
'<p:nvPicPr><p:cNvPr id="2" name="Slide"/>'
'<p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr>'
'<p:blipFill><a:blip r:embed="rId2"/><a:stretch><a:fillRect/></a:stretch></p:blipFill>'
'<p:spPr><a:xfrm><a:off x="0" y="0"/>'
'<a:ext cx="$_slideWidthEmu" cy="$_slideHeightEmu"/></a:xfrm>'
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>'
'</p:pic>'
'</p:spTree></p:cSld>'
'<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>'
'</p:sld>';
}
String _slideRels(int n) {
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'<Relationship Id="rId1" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" '
'Target="../slideLayouts/slideLayout1.xml"/>'
'<Relationship Id="rId2" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" '
'Target="../media/image$n.png"/>'
'</Relationships>';
}
String _emptySpTree() {
return '<p:spTree>'
'<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>'
'<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/>'
'<a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>'
'</p:spTree>';
}
String _theme1() {
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office">'
'<a:themeElements>'
'<a:clrScheme name="Office">'
'<a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>'
'<a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>'
'<a:dk2><a:srgbClr val="44546A"/></a:dk2>'
'<a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>'
'<a:accent1><a:srgbClr val="4472C4"/></a:accent1>'
'<a:accent2><a:srgbClr val="ED7D31"/></a:accent2>'
'<a:accent3><a:srgbClr val="A5A5A5"/></a:accent3>'
'<a:accent4><a:srgbClr val="FFC000"/></a:accent4>'
'<a:accent5><a:srgbClr val="5B9BD5"/></a:accent5>'
'<a:accent6><a:srgbClr val="70AD47"/></a:accent6>'
'<a:hlink><a:srgbClr val="0563C1"/></a:hlink>'
'<a:folHlink><a:srgbClr val="954F72"/></a:folHlink>'
'</a:clrScheme>'
'<a:fontScheme name="Office">'
'<a:majorFont><a:latin typeface="Calibri Light"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>'
'<a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>'
'</a:fontScheme>'
'<a:fmtScheme name="Office">'
'<a:fillStyleLst>'
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>'
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>'
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>'
'</a:fillStyleLst>'
'<a:lnStyleLst>'
'<a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln>'
'<a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln>'
'<a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln>'
'</a:lnStyleLst>'
'<a:effectStyleLst>'
'<a:effectStyle><a:effectLst/></a:effectStyle>'
'<a:effectStyle><a:effectLst/></a:effectStyle>'
'<a:effectStyle><a:effectLst/></a:effectStyle>'
'</a:effectStyleLst>'
'<a:bgFillStyleLst>'
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>'
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>'
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>'
'</a:bgFillStyleLst>'
'</a:fmtScheme>'
'</a:themeElements>'
'</a:theme>';
}
}