335 lines
6.9 KiB
JavaScript
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,
|
|
};
|