line-and-surface/scripts/generate-blurhashes.js
2025-07-28 17:57:42 +02:00

146 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Script to preprocess image directories and generate thumbnail data URIs
* to be stored in files.lst files.
*
* Usage:
* node generate-blurhashes.js <directory>
*
* Example:
* node generate-blurhashes.js ../content/motion_source/monstera1
*/
const fs = require("fs");
const path = require("path");
const { createCanvas, loadImage } = require("canvas");
// Configuration
const MAX_THUMBNAIL_SIZE = 32;
/**
* Create a thumbnail data URI for an image
* @param {string} imagePath - Path to the image
* @returns {Promise<string>} The data URI
*/
async function createThumbnailDataURI(imagePath) {
try {
const image = await loadImage(imagePath);
// Create a small canvas for thumbnail
const aspectRatio = image.width / image.height;
let thumbnailWidth = MAX_THUMBNAIL_SIZE;
let thumbnailHeight = MAX_THUMBNAIL_SIZE;
if (aspectRatio > 1) {
thumbnailHeight = Math.round(thumbnailWidth / aspectRatio);
} else {
thumbnailWidth = Math.round(thumbnailHeight * aspectRatio);
}
const canvas = createCanvas(thumbnailWidth, thumbnailHeight);
const ctx = canvas.getContext("2d");
// Draw image to thumbnail size
ctx.drawImage(image, 0, 0, thumbnailWidth, thumbnailHeight);
// Convert to data URI
const dataURI = canvas.toDataURL("image/png");
return dataURI;
} catch (err) {
console.error(`Error processing ${imagePath}:`, err);
return null;
}
}
/**
* Process a directory and update its files.lst with thumbnail data URIs
* @param {string} dirPath - Path to the directory containing images
*/
async function processDirectory(dirPath) {
console.log(`Processing directory: ${dirPath}`);
// Read directory for image files
const files = fs
.readdirSync(dirPath)
.filter((file) =>
[".jpg", ".jpeg", ".png", ".webp", ".gif"].includes(
path.extname(file).toLowerCase()
)
)
.sort((a, b) => {
// Natural sort for numbered files (e.g., img001.jpg, img002.jpg)
const numA = parseInt(a.match(/\d+/) || "0");
const numB = parseInt(b.match(/\d+/) || "0");
return numA - numB;
});
if (files.length === 0) {
console.warn(`No image files found in ${dirPath}`);
return;
}
// Process each file and collect blurhashes
const fileData = [];
let count = 0;
const total = files.length;
for (const file of files) {
count++;
const filePath = path.join(dirPath, file);
process.stdout.write(`\rProcessing image ${count}/${total}: ${file}`);
const dataURI = await createThumbnailDataURI(filePath);
if (dataURI) {
fileData.push({ file, dataURI });
} else {
// Include file without data URI if creation fails
fileData.push({ file, dataURI: null });
}
}
console.log("\nWriting files.lst...");
// Write to files.lst
const outputPath = path.join(dirPath, "files.lst");
const fileLines = fileData.map((entry) => {
if (entry.dataURI) {
return `${entry.file}\t${entry.dataURI}`;
}
return entry.file;
});
fs.writeFileSync(outputPath, fileLines.join("\n"));
console.log(
`Created files.lst with ${fileData.length} entries (${
fileData.filter((e) => e.dataURI).length
} with thumbnail data URIs)`
);
}
// Main execution
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Please provide a directory path");
process.exit(1);
}
const dirPath = args[0];
if (!fs.existsSync(dirPath)) {
console.error(`Directory does not exist: ${dirPath}`);
process.exit(1);
}
if (!fs.statSync(dirPath).isDirectory()) {
console.error(`Not a directory: ${dirPath}`);
process.exit(1);
}
await processDirectory(dirPath);
}
main().catch((err) => {
console.error("Error:", err);
process.exit(1);
});