galleria_photo_face_rknn_op.../api_v1/scanner/thumbs.js
2026-10-03 12:16:58 +02:00

335 lines
6.9 KiB
JavaScript

// api_v1/scanner/thumbs.js
const fs = require('fs');
const sharp = require('sharp');
const { exec } = require('child_process');
const path = require('path');
/*
* ============================================================
* CONFIGURAZIONE IMMAGINE CAST
* ============================================================
*/
/*
* Se il file originale è JPEG e pesa meno di questa soglia,
* il file _cast.jpg sarà una copia identica dell'originale.
*
* 500 * 1024 = 512000 byte.
*/
const CAST_JPEG_COPY_LIMIT_BYTES = 500 * 1024;
/*
* Dimensioni massime dell'immagine Cast.
*
* L'immagine viene sempre contenuta dentro questo rettangolo,
* senza crop e senza ingrandimento.
*/
const CAST_MAX_WIDTH = 1920;
const CAST_MAX_HEIGHT = 1080;
/*
* Qualità JPEG usata quando è necessaria la conversione.
*/
const CAST_JPEG_QUALITY = 85;
function deriveCastPathFromAvg(thumbAvgPath) {
if (!thumbAvgPath) {
return null;
}
/*
* Caso standard:
*
* IMG_0241_avg.jpg
* diventa:
* IMG_0241_cast.jpg
*/
if (thumbAvgPath.toLowerCase().endsWith('_avg.jpg')) {
return thumbAvgPath.replace(
/_avg\.jpg$/i,
'_cast.jpg'
);
}
/*
* Fallback generico.
*/
const ext = path.extname(thumbAvgPath);
const base = thumbAvgPath.slice(
0,
-ext.length
);
return `${base}_cast.jpg`;
}
/*
* Video: crea soltanto AVG e MIN.
*/
function createVideoThumbnail(
videoPath,
thumbMinPath,
thumbAvgPath
) {
return new Promise(resolve => {
const cmd = `
ffmpeg -y -i "${videoPath}" -ss 00:00:01.000 -vframes 1 "${thumbAvgPath}" &&
ffmpeg -y -i "${thumbAvgPath}" -vf "scale=100:-1" "${thumbMinPath}"
`;
exec(cmd, error => {
if (error) {
console.error(
'[THUMB VIDEO] errore creazione thumbnail',
{
videoPath,
thumbMinPath,
thumbAvgPath,
error: error.message,
}
);
} else {
console.log(
'[THUMB VIDEO] thumbnails create',
{
videoPath,
thumbMinPath,
thumbAvgPath,
}
);
}
/*
* Mantiene il comportamento originale:
* la Promise viene risolta anche in caso di errore.
*/
resolve();
});
});
}
/*
* Crea la versione CAST dell'immagine.
*
* Regole:
*
* 1. Se l'originale è JPEG e pesa meno di 500 KB:
* copia byte per byte l'originale nel file _cast.jpg.
*
* 2. In tutti gli altri casi:
* converte l'immagine in JPEG qualità 85.
*
* 3. Se l'immagine supera 1920x1080:
* viene ridimensionata per stare dentro 1920x1080.
*
* 4. Se l'immagine è già più piccola:
* mantiene le sue dimensioni, senza ingrandirla.
*
* 5. Non viene mai effettuato crop.
*/
async function createCastImage(
filePath,
finalCastPath
) {
const sourceStat =
await fs.promises.stat(filePath);
const metadata =
await sharp(filePath).metadata();
/*
* metadata.format === "jpeg" riconosce il contenuto reale
* dell'immagine, indipendentemente dall'estensione .jpg o .jpeg.
*/
const sourceIsJpeg =
metadata.format === 'jpeg';
const sourceIsUnderCopyLimit =
sourceStat.size <
CAST_JPEG_COPY_LIMIT_BYTES;
/*
* JPEG sotto 500 KB:
* copia identica dell'originale.
*
* Non viene effettuata:
*
* - ricompressione;
* - rotazione;
* - modifica dei metadati;
* - modifica delle dimensioni.
*/
if (
sourceIsJpeg &&
sourceIsUnderCopyLimit
) {
await fs.promises.copyFile(
filePath,
finalCastPath
);
const castStat =
await fs.promises.stat(finalCastPath);
console.log(
'[THUMB CAST] originale JPEG copiato',
{
source: filePath,
destination: finalCastPath,
sourceBytes: sourceStat.size,
castBytes: castStat.size,
copyLimitBytes:
CAST_JPEG_COPY_LIMIT_BYTES,
sourceFormat: metadata.format,
sourceWidth: metadata.width || null,
sourceHeight: metadata.height || null,
}
);
return;
}
/*
* Tutti gli altri casi:
*
* - JPEG da almeno 500 KB;
* - PNG;
* - WebP;
* - HEIC/HEIF, se supportato da Sharp;
* - TIFF;
* - altri formati decodificabili da Sharp.
*
* L'immagine viene convertita in JPEG qualità 85.
*
* withoutEnlargement: true garantisce che un'immagine più
* piccola di 1920x1080 mantenga le proprie dimensioni.
*/
await sharp(filePath)
.rotate()
.resize({
width: CAST_MAX_WIDTH,
height: CAST_MAX_HEIGHT,
fit: 'inside',
withoutEnlargement: true,
fastShrinkOnLoad: true,
})
.jpeg({
quality: CAST_JPEG_QUALITY,
progressive: false,
optimizeCoding: true,
})
.toFile(finalCastPath);
const castStat =
await fs.promises.stat(finalCastPath);
const castMetadata =
await sharp(finalCastPath).metadata();
console.log(
'[THUMB CAST] immagine convertita',
{
source: filePath,
destination: finalCastPath,
sourceBytes: sourceStat.size,
castBytes: castStat.size,
sourceFormat:
metadata.format || null,
sourceWidth:
metadata.width || null,
sourceHeight:
metadata.height || null,
castFormat:
castMetadata.format || null,
castWidth:
castMetadata.width || null,
castHeight:
castMetadata.height || null,
quality: CAST_JPEG_QUALITY,
maxWidth: CAST_MAX_WIDTH,
maxHeight: CAST_MAX_HEIGHT,
reason: sourceIsJpeg
? 'JPEG sopra o uguale alla soglia'
: 'formato originale non JPEG',
}
);
}
/*
* Foto: crea MIN, AVG e CAST.
*/
async function createThumbnails(
filePath,
thumbMinPath,
thumbAvgPath,
thumbCastPath
) {
try {
const finalCastPath =
thumbCastPath ||
deriveCastPathFromAvg(thumbAvgPath);
/*
* 1. MIN: massimo 100x100.
*/
await sharp(filePath)
.rotate()
.resize({
width: 100,
height: 100,
fit: 'inside',
withoutEnlargement: true,
})
.jpeg({
quality: 75,
progressive: false,
optimizeCoding: true,
})
.toFile(thumbMinPath);
/*
* 2. AVG: larghezza massima 400px.
*/
await sharp(filePath)
.rotate()
.resize({
width: 400,
fit: 'inside',
withoutEnlargement: true,
})
.jpeg({
quality: 78,
progressive: false,
optimizeCoding: true,
})
.toFile(thumbAvgPath);
/*
* 3. CAST.
*/
if (finalCastPath) {
await createCastImage(
filePath,
finalCastPath
);
}
} catch (err) {
console.error(
'[THUMB] errore creazione thumbnails',
{
filePath,
thumbMinPath,
thumbAvgPath,
thumbCastPath,
error: err.message,
stack: err.stack,
}
);
}
}
module.exports = {
createVideoThumbnail,
createThumbnails,
};