UNPKG

@bashbers/astro-image-dithering

Version:

Astro image dithering plugin - converts png's in project to dithered.pngs and uses them inside markdown files.

110 lines (98 loc) 3.78 kB
import { AstroIntegration } from "astro"; import { cwd } from "node:process"; import { PNG } from 'pngjs'; import { createReadStream, createWriteStream} from "node:fs"; import { BayerDithering, recursiveFindByExtension } from "./lib.js"; import rehypeDitheredImageContainerHtml from "./rehypeDitheredImageContainerHtml.js"; // @ts-ignore const OptiPng = await import('optipng'); interface ditherImagesOptions { directoryToTraverse?: string; injectCss?: boolean; injectClientsideJs?: boolean; } export function ditherImagesIntegration(options: ditherImagesOptions = {}): AstroIntegration { const { directoryToTraverse = cwd(), injectCss = true, injectClientsideJs = true } = options; return { name: 'dither-images-integration', hooks: { 'astro:build:start': async ({logger}) => { const foundPngFiles = recursiveFindByExtension(directoryToTraverse, 'png', null, []); logger.debug(foundPngFiles.toString()); const promises = foundPngFiles.map((foundPng) => { return new Promise<void>((resolve, reject) => { // We skip dithered.png files, files inside dist folders to prevent double dithering and files from which an existing dithered.png exists if(foundPng.includes('-dithered.png') || foundPng.includes('dist\\') || foundPngFiles.includes(foundPng + "-dithered.png")) { return resolve(); } createReadStream(foundPng) .pipe(new PNG()) .on('error', reject) .on('parsed', function () { // `this` is a PNG instance const pixels = Array.from(this.data); const palette = [[25,25,25], [75,75,75],[125,125,125],[175,175,175],[225,225,225],[250,250,250]] const dithered = BayerDithering(pixels, this.width, this.height, palette); // The final image const png = new PNG({ width: this.width, height: this.height, }); // Intermittent rgba array with every 4th item set to 255 for opacity. const rgba = new Uint8Array(this.width * this.height * 4); // Dithered is actually a index, not concrete values yet. for (let i = 0; i < dithered.length; i++) { const color = palette[dithered[i]]; // [R, G, B] const rgbaOffset = i * 4; rgba[rgbaOffset] = color[0]; // R rgba[rgbaOffset + 1] = color[1]; // G rgba[rgbaOffset + 2] = color[2]; // B rgba[rgbaOffset + 3] = 255; // A } // Fill png.data with the RGBA values for (let i = 0; i < rgba.length; i++) { png.data[i] = rgba[i] || 0; } //Optimize range is between 1 and 7. Its lossless so we use 7. const optimizer = new OptiPng.default(['-o7']); const ditheredFilename = foundPng.split('.')[0] + "-dithered.png"; png.pack() .pipe(optimizer) .pipe(createWriteStream(ditheredFilename)) .on('finish', () => { logger.info(`Processed ${ditheredFilename}!`); resolve(); }); }); }) }); await Promise.all(promises); logger.info('🎉 All images dithered and saved before build continues!'); }, 'astro:config:setup': ({ updateConfig, injectScript }) => { if(injectClientsideJs) { injectScript('page', ` import '@bashbers/astro-image-dithering/dist/dithered-image-container.js'; `); } if(injectCss) { injectScript('page', ` import '@bashbers/astro-image-dithering/dist/dither-image-toggle.css'; `); } updateConfig({ markdown: { rehypePlugins: [ rehypeDitheredImageContainerHtml ] }, vite: { ssr: { noExternal: ['@bashbers/astro-image-dithering'] } } }); } } } }