@nolebase/vitepress-plugin-thumbnail-hash
Version:
A VitePress plugin that scan and generate data with blurhash, thumbhash hashing algorithm for images, as well as a standalone component to render images with blurhash and thumbhash.
168 lines (164 loc) • 6.34 kB
JavaScript
import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
import { relative, join } from 'node:path';
import CanvasKitInit from 'canvaskit-wasm';
import { cyan, gray } from 'colorette';
import ora from 'ora';
import { rgbaToThumbHash, thumbHashToDataURL } from 'thumbhash';
import { glob } from 'tinyglobby';
import { normalizePath } from 'vite';
import { subtle } from 'uncrypto';
function binaryToBase64(binary) {
return btoa(String.fromCharCode(...binary));
}
async function digestUint8ArrayDataSha256(data) {
const hashBuffer = await subtle.digest("SHA-256", data);
return Array.from(new Uint8Array(hashBuffer));
}
async function hash(data, length = 10) {
const hashResult = await digestUint8ArrayDataSha256(data);
const hashBase64 = binaryToBase64(new Uint8Array(hashResult));
if (length > 0)
return hashBase64.substring(0, length);
return hashBase64;
}
function normalizeBase64(base64) {
return base64.replace("/", "_").replace("+", "-").replace("=", "-");
}
async function calculateThumbHashForFile(imageData) {
const canvasKit = await CanvasKitInit();
const image = canvasKit.MakeImageFromEncoded(imageData);
if (!image)
throw new Error("Failed to make image from encoded data.");
const width = image.width();
const height = image.height();
const scale = 100 / Math.max(width, height);
const resizedWidth = Math.round(width * scale);
const resizedHeight = Math.round(height * scale);
const canvas = canvasKit.MakeCanvas(resizedWidth, resizedHeight);
const context = canvas.getContext("2d");
context.drawImage(image, 0, 0, resizedWidth, resizedHeight);
const pixels = context.getImageData(0, 0, resizedWidth, resizedHeight);
const thumbHashBinary = rgbaToThumbHash(pixels.width, pixels.height, pixels.data);
const thumbHashBase64 = binaryToBase64(thumbHashBinary);
const thumbHashDataURL = await thumbHashToDataURL(thumbHashBinary);
return {
dataBase64: thumbHashBase64,
dataUrl: thumbHashDataURL,
width: resizedWidth,
height: resizedHeight,
originalWidth: width,
originalHeight: height
};
}
function createThumbHashDataFromThumbHash(rootDir, assetDir, baseUrl, imageFileName, imageFullFileName, imageFullHash, imageFileHash, thumbHash) {
const fileName = relative(rootDir, imageFileName);
const thumbhashData = {
...thumbHash,
assetFileName: normalizePath(relative(rootDir, imageFullFileName)),
assetFullFileName: normalizePath(imageFullFileName),
assetFullHash: imageFullHash,
assetFileHash: imageFileHash,
assetUrl: normalizePath(join(assetDir, fileName)),
assetUrlWithBase: normalizePath(join(baseUrl, assetDir, fileName))
};
if (!thumbhashData.assetUrlWithBase.startsWith("/"))
thumbhashData.assetUrlWithBase = `/${thumbhashData.assetUrlWithBase}`;
return thumbhashData;
}
function getCacheDir(vitePressCacheDir) {
return join(vitePressCacheDir, "@nolebase", "vitepress-plugin-thumbnail-hash", "thumbhashes");
}
function getMapFilename(cacheDir) {
return join(cacheDir, "map.json");
}
async function exists(path) {
try {
await stat(path);
return true;
} catch (error) {
if (!(error instanceof Error))
throw error;
if (!("code" in error))
throw error;
if (error.code !== "ENOENT")
throw error;
return false;
}
}
async function mkdirIfNotExist(dir) {
const targetDirExists = await exists(dir);
if (targetDirExists)
return;
await mkdir(dir, { recursive: true });
}
async function readCachedMapFile(path) {
const targetPathExists = await exists(path);
if (!targetPathExists)
return {};
const content = await readFile(path);
return JSON.parse(content.toString("utf-8"));
}
function ThumbnailHashImages() {
return {
name: "@nolebase/vitepress-plugin-thumbnail-hash/images",
enforce: "pre",
config() {
return {
optimizeDeps: {
exclude: [
"@nolebase/vitepress-plugin-thumbnail-hash/client"
]
},
ssr: {
noExternal: [
"@nolebase/vitepress-plugin-thumbnail-hash"
]
}
};
},
async configResolved(config) {
const root = config.root;
const vitepressConfig = config.vitepress;
const startsAt = Date.now();
const moduleNamePrefix = cyan("@nolebase/vitepress-plugin-thumbnail-hash/images");
const grayPrefix = gray(":");
const spinnerPrefix = `${moduleNamePrefix}${grayPrefix}`;
const spinner = ora({ discardStdin: false, isEnabled: config.command === "serve" });
spinner.start(`${spinnerPrefix} Prepare to generate hashes for images...`);
const cacheDir = getCacheDir(vitepressConfig.cacheDir);
await mkdirIfNotExist(cacheDir);
const thumbhashMap = await readCachedMapFile(getMapFilename(cacheDir));
spinner.text = `${spinnerPrefix} Searching for images...`;
const files = await glob(`${root}/**/*.+(jpg|jpeg|png)`, { onlyFiles: true });
spinner.text = `${spinnerPrefix} Calculating thumbhashes for images...`;
const thumbhashes = await Promise.all(files.map(async (file) => {
const cacheHit = thumbhashMap[normalizePath(relative(root, file))];
if (cacheHit)
return cacheHit;
const readImageRawData = await readFile(file);
const imageFullHash = await hash(readImageRawData, -1);
const imageFileHash = normalizeBase64(imageFullHash.substring(0, 10));
const calculatedThumbhashData = await calculateThumbHashForFile(readImageRawData);
return createThumbHashDataFromThumbHash(
root,
vitepressConfig.assetsDir,
vitepressConfig.site.base,
file,
file,
imageFullHash,
imageFileHash,
calculatedThumbhashData
);
}));
spinner.text = `${spinnerPrefix} Aggregating calculated thumbhash data...`;
for (const thumbhash of thumbhashes)
thumbhashMap[thumbhash.assetFileName] = thumbhash;
spinner.text = `${spinnerPrefix} Writing thumbhash data to cache...`;
await writeFile(getMapFilename(cacheDir), JSON.stringify(thumbhashMap, null, 2));
const elapsed = Date.now() - startsAt;
spinner.succeed(`${spinnerPrefix} Done. ${gray(`(${elapsed}ms)`)}`);
}
};
}
export { ThumbnailHashImages };
//# sourceMappingURL=index.mjs.map