Deploy / validate-dispatch (push) Successful in 43s
Deploy / scheduled-rebuild-dev (push) Skipped
Deploy / scheduled-rebuild-master (push) Skipped
Deploy / rollback-dev (push) Skipped
Deploy / rollback-master (push) Skipped
Deploy / changes (push) Successful in 13s
Deploy / secret-scan (push) Successful in 9s
Deploy / app-quality (push) Successful in 11m21s
Deploy / security-deps (push) Successful in 14s
Deploy / context (push) Failing after 1s
Deploy / app-image (push) Skipped
Deploy / deploy-dev (push) Skipped
Deploy / deploy-master (push) Skipped
84 lines
2.4 KiB
JavaScript
84 lines
2.4 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import sharp from "sharp";
|
|
|
|
const PUBLIC_IMAGES_DIR = path.resolve(import.meta.dirname, "../public/images");
|
|
const SUPPORTED_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".webp", ".avif"]);
|
|
const METADATA_FIELDS = ["exif", "icc", "iptc", "xmp"];
|
|
const CHECK_ONLY = process.argv.includes("--check");
|
|
|
|
function collectImages(directory) {
|
|
if (!fs.existsSync(directory)) return [];
|
|
|
|
const files = [];
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
const fullPath = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(...collectImages(fullPath));
|
|
continue;
|
|
}
|
|
|
|
if (entry.isFile() && SUPPORTED_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
|
|
files.push(fullPath);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function getEncoder(pipeline, extension) {
|
|
if (extension === ".png") {
|
|
return pipeline.png({ compressionLevel: 9, adaptiveFiltering: true });
|
|
}
|
|
if (extension === ".webp") {
|
|
return pipeline.webp({ quality: 80 });
|
|
}
|
|
if (extension === ".avif") {
|
|
return pipeline.avif({ quality: 50 });
|
|
}
|
|
return pipeline.jpeg({ quality: 85, progressive: true, mozjpeg: true });
|
|
}
|
|
|
|
async function hasMetadata(filePath) {
|
|
const metadata = await sharp(filePath).metadata();
|
|
return METADATA_FIELDS.some((field) => Boolean(metadata[field]));
|
|
}
|
|
|
|
async function stripMetadata(filePath) {
|
|
const extension = path.extname(filePath).toLowerCase();
|
|
const tempPath = `${filePath}.tmp`;
|
|
if (fs.existsSync(tempPath)) {
|
|
fs.rmSync(tempPath);
|
|
}
|
|
|
|
const pipeline = sharp(filePath).rotate();
|
|
await getEncoder(pipeline, extension).toFile(tempPath);
|
|
fs.renameSync(tempPath, filePath);
|
|
}
|
|
|
|
const files = collectImages(PUBLIC_IMAGES_DIR);
|
|
const filesWithMetadata = [];
|
|
|
|
for (const file of files) {
|
|
if (await hasMetadata(file)) {
|
|
filesWithMetadata.push(file);
|
|
}
|
|
}
|
|
|
|
if (CHECK_ONLY) {
|
|
for (const file of filesWithMetadata) {
|
|
console.error(`[images:check-metadata] Metadata found: ${file}`);
|
|
}
|
|
if (filesWithMetadata.length > 0) {
|
|
process.exitCode = 1;
|
|
} else {
|
|
console.log(`[images:check-metadata] ${files.length} images checked; no metadata blocks found.`);
|
|
}
|
|
} else {
|
|
for (const file of filesWithMetadata) {
|
|
await stripMetadata(file);
|
|
}
|
|
console.log(
|
|
`[images:strip-metadata] ${filesWithMetadata.length} of ${files.length} images rewritten.`,
|
|
);
|
|
}
|