Add binary exporters and template manager
Template tests / tests (push) Failing after 52s

- Native PDF exporter (cover, TOC, bookmarks, images, code, tables,
  text blocks); validated under Ghostscript
- Animated GIF exporter (title card, title overlay, progress bar)
- Image bundle exporter with watermark compositing
- DOCX and PPTX emitters (hand-built OOXML over our zip writer) with
  structural + relationship + XML well-formedness validation in tests
- Per-format template manager with .sfglt share archives
- Unified export dispatcher covering all nine formats
- 10 more workflow tests (52 total)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Iisyourdad
2026-06-10 16:59:19 -05:00
parent ca73db68e3
commit a5bbdde480
9 changed files with 1072 additions and 1 deletions
+54
View File
@@ -0,0 +1,54 @@
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const { guideSlug, writeStepImages } = require('./common');
const raster = require('../core/raster');
const { decodePng, encodePng } = require('../core/png');
/**
* Image bundle exporter: one annotated PNG per image step plus a
* metadata.json describing the guide, with an optional watermark overlay.
*/
const DEFAULT_TEMPLATE = {
watermarkPath: '', // PNG overlaid bottom-right when set
watermarkOpacity: 0.6,
};
function exportImageBundle(ast, outDir, template = {}) {
const tpl = { ...DEFAULT_TEMPLATE, ...template };
fs.mkdirSync(outDir, { recursive: true });
const images = writeStepImages(ast, outDir);
if (tpl.watermarkPath && fs.existsSync(tpl.watermarkPath)) {
const mark = decodePng(fs.readFileSync(tpl.watermarkPath));
const alpha = Math.round(255 * Math.max(0, Math.min(1, tpl.watermarkOpacity)));
for (const { relPath } of images.values()) {
const file = path.join(outDir, relPath);
const img = decodePng(fs.readFileSync(file));
const faded = raster.cloneImage(mark);
for (let i = 3; i < faded.data.length; i += 4) {
faded.data[i] = Math.round((faded.data[i] * alpha) / 255);
}
raster.drawImage(img, faded, Math.max(0, img.width - mark.width - 12), Math.max(0, img.height - mark.height - 12));
fs.writeFileSync(file, encodePng(img));
}
}
const meta = {
format: 'stepforge-image-bundle',
version: 1,
guide: { title: ast.guide.title, generatedAt: ast.generatedAt },
steps: ast.steps.map((step) => ({
number: step.number,
title: step.title,
image: images.get(step.stepId)?.relPath || null,
})),
};
const metaFile = path.join(outDir, `${guideSlug(ast)}-bundle.json`);
fs.writeFileSync(metaFile, JSON.stringify(meta, null, 2) + '\n');
return { file: metaFile, imageCount: images.size };
}
module.exports = { exportImageBundle, DEFAULT_TEMPLATE };