Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface PixelForgeOptions {
pwa?: boolean;
web?: boolean;
seo?: boolean;
transparent?: boolean;

// Note: Individual platform options removed - only essential social generation available

Expand All @@ -41,6 +42,7 @@ export interface PixelForgeResult {
social?: string[];
web?: string[];
seo?: string[];
transparent?: string[];
};

// Meta tags (always generated)
Expand Down Expand Up @@ -125,6 +127,7 @@ export async function generateAssets(imagePath: string, options: PixelForgeOptio
pwa: options.pwa,
web: options.web,
seo: options.seo,
transparent: options.transparent,
format: options.format as any,
verbose: options.verbose
};
Expand Down Expand Up @@ -176,6 +179,10 @@ export async function generateAssets(imagePath: string, options: PixelForgeOptio
);
}

if (options.transparent) {
files.transparent = imageFiles.filter(file => file.includes('transparent'));
}

const result: PixelForgeResult = {
images: imageFiles,
files,
Expand Down Expand Up @@ -243,5 +250,14 @@ export async function generateWeb(imagePath: string, outputDir: string = './asse
};
}

export async function generateTransparent(imagePath: string, outputDir: string = './assets') {
const result = await generateAssets(imagePath, { transparent: true, outputDir });
return {
files: result.files.transparent || [],
metaTags: result.metaTags,
summary: result.summary
};
}

// Export the main function as default
export default generateAssets;
21 changes: 17 additions & 4 deletions src/cli/commands/generate-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export async function generateAll(
file.endsWith('.svg') ||
file.endsWith('.ico') ||
file.endsWith('.json') ||
file.endsWith('.xml')
file.endsWith('.xml') ||
file.endsWith('.html')
);

// Always generate meta tags for all generated files
Expand All @@ -70,10 +71,22 @@ export async function generateAll(

await metadataGenerator.saveToFile(config.output.path);

// Complete progress tracking (include meta-tags.html in count)
await progressTracker.complete(assetFiles.length + 1);
// Complete progress tracking (event-driven already counted all)
await progressTracker.complete();
const { current: actualCreated } = progressTracker.getProgress();

console.log(`✅ Generated ${assetFiles.length + 1} files in ${config.output.path}`);
// Section summaries
const imageExts = ['.png', '.jpg', '.jpeg', '.webp', '.svg', '.ico'];
const imageCount = assetFiles.filter(f => imageExts.some(ext => f.toLowerCase().endsWith(ext))).length;
const nonImage = assetFiles.filter(f => !imageExts.some(ext => f.toLowerCase().endsWith(ext)));
console.log(`📂 All Assets: ${imageCount} files`);

if (nonImage.includes('manifest.json')) {
console.log('🧾 Generated manifest.json');
}
console.log('📝 Created meta-tags.html');

console.log(`\n✅ Generated ${actualCreated} files in ${config.output.path}`);

if (options.verbose) {
console.log('\nGenerated files:');
Expand Down
72 changes: 59 additions & 13 deletions src/cli/commands/generate-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { PixelForgeConfig } from '../../core/config-validator';
import { getProgressTracker, resetProgressTracker } from '../utils/progress-tracker';
import { SmartMetadataGenerator } from '../../core/smart-metadata-generator';
import { makeBackgroundTransparent } from '../../core/transparent-background';
import path from 'path';
import { promises as fs } from 'fs';

export interface GenerateOptions {
all?: boolean;
Expand Down Expand Up @@ -38,6 +40,19 @@ export async function generateAssets(
format: options.format,
verbose: options.verbose
});
// Post-process transparency for all generated raster images if requested
if (options.transparent) {
const imageExts = ['.png', '.jpg', '.jpeg', '.webp', '.avif'];
const files = await fs.readdir(config.output.path);
const targets = files
.filter(f => imageExts.some(ext => f.toLowerCase().endsWith(ext)))
.map(f => path.join(config.output.path, f));
for (const filePath of targets) {
const tmp = `${filePath}.tmp`;
await makeBackgroundTransparent(filePath, tmp);
await fs.rename(tmp, filePath);
}
}
return;
}

Expand Down Expand Up @@ -87,12 +102,27 @@ export async function generateAssets(
results.push(seoResult);
}

// Handle --transparent (single image output with transparent background)
// Handle transparency flag
if (options.transparent) {
const outName = 'transparent.png';
const outPath = `${config.output.path}/${outName}`;
await makeBackgroundTransparent(sourceImage, outPath);
results.push({ name: 'Transparent Background', files: [outName] });
if (results.length > 0) {
// Apply transparency to all generated raster images in-place
const imageExts = ['.png', '.jpg', '.jpeg', '.webp', '.avif'];
const filesToProcess = Array.from(new Set(results.flatMap(r => r.files)))
.filter(name => imageExts.some(ext => name.toLowerCase().endsWith(ext)))
.map(name => path.join(config.output.path, name));

for (const filePath of filesToProcess) {
const tmp = `${filePath}.tmp`;
await makeBackgroundTransparent(filePath, tmp);
await fs.rename(tmp, filePath);
}
} else {
// No other generators selected: create a single transparent image
const outName = 'transparent.png';
const outPath = path.join(config.output.path, outName);
await makeBackgroundTransparent(sourceImage, outPath);
results.push({ name: 'Transparent Background', files: [outName] });
}
}

// If no specific options provided, default to social
Expand All @@ -111,23 +141,39 @@ export async function generateAssets(

await metadataGenerator.saveToFile(config.output.path);

// Complete progress tracking (include meta-tags.html in count)
const totalFiles = results.reduce((sum, { files }) => sum + files.length, 0);
await progressTracker.complete(totalFiles + 1); // +1 for meta-tags.html
// Complete progress tracking (event-driven count already includes all files)
await progressTracker.complete();

// Display summary
console.log('✅ Generation complete!\n');
const imageExts = ['.png', '.jpg', '.jpeg', '.webp', '.svg', '.ico'];
results.forEach(({ name, files }) => {
console.log(`📂 ${name}: ${files.length} files`);
const imageFiles = files.filter(f => imageExts.some(ext => f.toLowerCase().endsWith(ext)));
const nonImageFiles = files.filter(f => !imageExts.some(ext => f.toLowerCase().endsWith(ext)));
console.log(`📂 ${name}: ${imageFiles.length} files`);
if (options.verbose) {
files.slice(0, 5).forEach(file => console.log(` 📄 ${file}`));
if (files.length > 5) {
console.log(` ... and ${files.length - 5} more`);
imageFiles.slice(0, 5).forEach(file => console.log(` 📄 ${file}`));
if (imageFiles.length > 5) {
console.log(` ... and ${imageFiles.length - 5} more`);
}
}
// Mention special non-image files in context of the section
if (nonImageFiles.includes('manifest.json')) {
console.log('🧾 Generated manifest.json');
}
if (nonImageFiles.includes('meta-tags.html')) {
console.log('📝 Created meta-tags.html');
}
});

console.log(`\n🎉 Total: ${totalFiles + 1} files generated in ${config.output.path}`);
// Mention special non-image files if present
// Also print special files if they weren't associated with a section
const manifestPath = path.join(config.output.path, 'manifest.json');
try { await fs.access(manifestPath); console.log('🧾 Generated manifest.json'); } catch {}
console.log('📝 Created meta-tags.html');

const { current: actualCreated } = progressTracker.getProgress();
console.log(`\n🎉 Total: ${actualCreated} files generated in ${config.output.path}`);
} catch (error) {
progressTracker.stop();
throw error;
Expand Down
79 changes: 56 additions & 23 deletions src/cli/utils/progress-tracker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as cliProgress from 'cli-progress';
import { promises as fs } from 'fs';
import path from 'path';
import { setProgressRecorder } from '../../core/progress-events';
import { GenerateOptions } from '../commands/generate-orchestrator';

export interface ProgressConfig {
Expand All @@ -19,11 +21,13 @@ export class ProgressTracker {
private outputDirectory = '';
private pollingInterval: NodeJS.Timeout | null = null;
private isPolling = false;
private usingEvents = false;
private createdFiles: Set<string> = new Set();

// File extensions we consider as generated assets
private readonly assetExtensions = [
'.png', '.jpg', '.jpeg', '.webp', '.svg', '.ico',
'.json', '.xml'
'.json', '.xml', '.html'
];

/**
Expand All @@ -32,28 +36,34 @@ export class ProgressTracker {
static estimateFileCount(options: GenerateOptions): number {
let totalFiles = 0;

// Favicon generation (essential files with multiple sizes)
if (options.favicon) {
totalFiles += 6; // favicon.ico, favicon-16x16.png, favicon-32x32.png, favicon-48x48.png, apple-touch-icon.png, safari-pinned-tab.svg
}
// If --web flag, it includes favicon + PWA + SEO package
if (options.web) {
totalFiles += 16; // complete web package (favicons + PWA + SEO images)
} else {
// Favicon generation (essential files with multiple sizes)
if (options.favicon) {
totalFiles += 6; // favicon.ico, favicon-16x16.png, favicon-32x32.png, favicon-48x48.png, apple-touch-icon.png, safari-pinned-tab.svg
}

// PWA generation (essential files only)
if (options.pwa) {
totalFiles += 7; // pwa icons (4) + splash screens (2) + manifest.json (1)
}
// PWA generation (essential files only)
if (options.pwa) {
totalFiles += 7; // pwa icons (4) + splash screens (2) + manifest.json (1)
}

// Social media generation (optimized essential files only)
if (options.social) {
totalFiles += 3; // social-media-general.png, instagram-square.png, social-vertical.png
}
// Social media generation (optimized essential files only)
if (options.social) {
totalFiles += 3; // social-media-general.png, instagram-square.png, social-vertical.png
}

// SEO/Web generation
if (options.seo || options.web) {
totalFiles += 3; // og-image.png, opengraph.png, twitter-image.png
// SEO generation
if (options.seo) {
totalFiles += 3; // og-image.png, opengraph.png, twitter-image.png
}
}

// Transparent background generation (single output)
if ((options as any).transparent) {
// Transparent background generation (single output when used alone)
const hasOtherGenerators = !!(options.all || options.web || options.favicon || options.pwa || options.seo || options.social);
if ((options as any).transparent && !hasOtherGenerators) {
totalFiles += 1; // one transparent image
}

Expand Down Expand Up @@ -132,9 +142,17 @@ export class ProgressTracker {
this.totalFiles = ProgressTracker.estimateFileCount(options);
this.outputDirectory = outputDirectory;
this.currentProgress = 0;
this.createdFiles.clear();

// Count existing files to use as baseline
this.initialFileCount = await this.countAssetFiles(outputDirectory);
// Snapshot existing assets per extension for accurate diffing
try {
const files = await fs.readdir(outputDirectory);
this.initialFileCount = files.filter(file =>
this.assetExtensions.some(ext => file.toLowerCase().endsWith(ext))
).length;
} catch (_err) {
this.initialFileCount = 0;
}

// Create progress bar with custom format
this.bar = new cliProgress.SingleBar({
Expand All @@ -148,9 +166,22 @@ export class ProgressTracker {
}, cliProgress.Presets.shades_classic);

this.bar.start(this.totalFiles, 0);
// Prefer event-driven updates; disable polling to avoid regressions
this.usingEvents = true;
setProgressRecorder((filePath: string) => {
if (!this.bar) return;
const lower = filePath.toLowerCase();
if (this.assetExtensions.some(ext => lower.endsWith(ext))) {
this.currentProgress = Math.min(this.currentProgress + 1, this.totalFiles);
this.bar.update(this.currentProgress);
this.createdFiles.add(path.basename(filePath));
}
});

// Start monitoring the directory for file changes
await this.startPolling();
// If events aren't available for some reason, fallback to polling
if (!this.usingEvents) {
await this.startPolling();
}
}

/**
Expand All @@ -159,13 +190,15 @@ export class ProgressTracker {
async complete(actualFileCount?: number): Promise<void> {
// Stop polling first
this.stopPolling();
setProgressRecorder(null);
this.usingEvents = false;

if (!this.bar) return;

// Get final file count from directory if not provided
if (!actualFileCount && this.outputDirectory) {
const finalCount = await this.countAssetFiles(this.outputDirectory);
actualFileCount = finalCount - this.initialFileCount;
actualFileCount = Math.max(finalCount - this.initialFileCount, 0);
}

// If actual count is provided and different from estimate, adjust
Expand Down
3 changes: 3 additions & 0 deletions src/core/image-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { execFile } from 'child_process';
import { promisify } from 'util';
import path from 'path';
import { promises as fs } from 'fs';
import { emitProgress } from './progress-events';
// Minimal Jimp typing to avoid unsafe any while supporting ESM dynamic import
type JimpImage = {
bitmap: { width: number; height: number; data: Buffer };
Expand Down Expand Up @@ -494,6 +495,7 @@ export class ImageProcessor {
}
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await img.writeAsync(outputPath);
emitProgress(outputPath);
return;
}

Expand Down Expand Up @@ -565,6 +567,7 @@ export class ImageProcessor {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`ImageMagick save failed: ${errorMessage}`);
}
emitProgress(outputPath);
}

/**
Expand Down
19 changes: 19 additions & 0 deletions src/core/progress-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Lightweight global progress event hooks to avoid coupling core to the CLI

type Recorder = (filePath: string) => void;

let recorder: Recorder | null = null;

export function setProgressRecorder(fn: Recorder | null): void {
recorder = fn;
}

export function emitProgress(filePath: string): void {
try {
recorder?.(filePath);
} catch {
// Do not let UI progress errors break core image writes
}
}


7 changes: 7 additions & 0 deletions src/core/smart-metadata-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,5 +209,12 @@ ${metaTags.map(tag => tag.startsWith('<!--') || tag === '' ? tag : ` ${tag}`).j

const filePath = path.join(outputDir, filename);
await fs.writeFile(filePath, htmlContent, 'utf8');
try {
// Dynamically import to avoid circular deps in core
const { emitProgress } = await import('./progress-events');
emitProgress(filePath);
} catch {
// Best-effort emit
}
}
}
Loading