a5bbdde480
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>
55 lines
1.9 KiB
JavaScript
55 lines
1.9 KiB
JavaScript
'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 };
|