release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { mkdirSync, statSync, writeFileSync } from "node:fs";
|
||||
import { basename, extname, join, resolve } from "node:path";
|
||||
import { normalizeImageWidths, type ImageFormat } from "./index.ts";
|
||||
|
||||
export interface ImageProcessorResult {
|
||||
data: Uint8Array;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface ImageProcessor {
|
||||
transform(
|
||||
input: string,
|
||||
options: { width: number; format: Exclude<ImageFormat, "original">; quality: number },
|
||||
): Promise<ImageProcessorResult>;
|
||||
}
|
||||
|
||||
export interface OptimizeImageOptions {
|
||||
outputDir: string;
|
||||
widths: number[];
|
||||
formats?: Array<Exclude<ImageFormat, "original">>;
|
||||
quality?: number;
|
||||
maxVariants?: number;
|
||||
processor?: ImageProcessor;
|
||||
}
|
||||
|
||||
export interface OptimizedImageVariant {
|
||||
path: string;
|
||||
width: number;
|
||||
height: number;
|
||||
format: Exclude<ImageFormat, "original">;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
export interface OptimizedImageManifest {
|
||||
source: string;
|
||||
variants: OptimizedImageVariant[];
|
||||
}
|
||||
|
||||
async function sharpProcessor(): Promise<ImageProcessor> {
|
||||
const packageName = "sharp";
|
||||
let sharp: any;
|
||||
try {
|
||||
const module = (await import(packageName)) as { default?: any };
|
||||
sharp = module.default ?? module;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
"WRN-IMAGE-SHARP-MISSING: install the optional `sharp` peer or provide an ImageProcessor",
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
return {
|
||||
async transform(input, options) {
|
||||
const pipeline = sharp(input)
|
||||
.rotate()
|
||||
.resize({ width: options.width, withoutEnlargement: true });
|
||||
const { data, info } = await pipeline
|
||||
.toFormat(options.format, { quality: options.quality })
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
return { data: new Uint8Array(data), width: info.width, height: info.height };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function optimizeImage(
|
||||
input: string,
|
||||
options: OptimizeImageOptions,
|
||||
): Promise<OptimizedImageManifest> {
|
||||
const widths = normalizeImageWidths(options.widths);
|
||||
const formats = [...new Set(options.formats ?? ["avif", "webp"])] as Array<
|
||||
Exclude<ImageFormat, "original">
|
||||
>;
|
||||
if (!widths.length) throw new RangeError("Image optimization requires at least one valid width");
|
||||
if (
|
||||
!formats.length ||
|
||||
formats.some((format) => !["avif", "webp", "jpeg", "png"].includes(format))
|
||||
)
|
||||
throw new RangeError("Image optimization has an unsupported output format");
|
||||
const maxVariants = options.maxVariants ?? 32;
|
||||
if (!Number.isInteger(maxVariants) || maxVariants < 1)
|
||||
throw new RangeError("Image maxVariants must be positive");
|
||||
if (widths.length * formats.length > maxVariants)
|
||||
throw new RangeError(`Image optimization exceeds ${maxVariants} variants`);
|
||||
const quality = Math.max(1, Math.min(100, Math.round(options.quality ?? 80)));
|
||||
const processor = options.processor ?? (await sharpProcessor());
|
||||
const outputDir = resolve(options.outputDir);
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
const stem = basename(input, extname(input)).replace(/[^A-Za-z0-9._-]+/g, "-") || "image";
|
||||
const variants: OptimizedImageVariant[] = [];
|
||||
for (const format of formats) {
|
||||
for (const width of widths) {
|
||||
const result = await processor.transform(resolve(input), { width, format, quality });
|
||||
if (
|
||||
!Number.isInteger(result.width) ||
|
||||
result.width < 1 ||
|
||||
!Number.isInteger(result.height) ||
|
||||
result.height < 1
|
||||
)
|
||||
throw new Error("Image processor returned invalid dimensions");
|
||||
const path = join(outputDir, `${stem}-${result.width}.${format === "jpeg" ? "jpg" : format}`);
|
||||
writeFileSync(path, result.data);
|
||||
variants.push({
|
||||
path,
|
||||
width: result.width,
|
||||
height: result.height,
|
||||
format,
|
||||
bytes: statSync(path).size,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { source: resolve(input), variants };
|
||||
}
|
||||
Reference in New Issue
Block a user