1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2025-01-14 14:43:17 +08:00

Adding animated gif support #779

This commit is contained in:
Patrik J. Braun 2023-11-26 22:47:57 +01:00
parent 4be86d6ebc
commit 1458faca70
6 changed files with 103 additions and 38 deletions

View File

@ -47,6 +47,7 @@ interface RendererInput {
outPath: string;
quality: number;
useLanczos3: boolean;
animate: boolean; // animates the output. Used for Gifs
cut?: {
left: number;
top: number;
@ -93,21 +94,21 @@ export class VideoRendererFactory {
const folder = path.dirname(input.outPath);
let executedCmd = '';
command
.on('start', (cmd): void => {
executedCmd = cmd;
})
.on('end', (): void => {
resolve();
})
.on('error', (e): void => {
reject('[FFmpeg] ' + e.toString() + ' executed: ' + executedCmd);
})
.outputOptions(['-qscale:v 4']);
.on('start', (cmd): void => {
executedCmd = cmd;
})
.on('end', (): void => {
resolve();
})
.on('error', (e): void => {
reject('[FFmpeg] ' + e.toString() + ' executed: ' + executedCmd);
})
.outputOptions(['-qscale:v 4']);
if (input.makeSquare === false) {
const newSize =
width < height
? Math.min(input.size, width) + 'x?'
: '?x' + Math.min(input.size, height);
width < height
? Math.min(input.size, width) + 'x?'
: '?x' + Math.min(input.size, height);
command.takeScreenshots({
timemarks: ['10%'],
size: newSize,
@ -130,18 +131,18 @@ export class VideoRendererFactory {
export class ImageRendererFactory {
@ExtensionDecorator(e=>e.gallery.ImageRenderer.render)
@ExtensionDecorator(e => e.gallery.ImageRenderer.render)
public static async render(input: MediaRendererInput | SvgRendererInput): Promise<void> {
let image: Sharp;
if ((input as MediaRendererInput).mediaPath) {
Logger.silly(
'[SharpRenderer] rendering photo:' +
(input as MediaRendererInput).mediaPath +
', size:' +
input.size
'[SharpRenderer] rendering photo:' +
(input as MediaRendererInput).mediaPath +
', size:' +
input.size
);
image = sharp((input as MediaRendererInput).mediaPath, {failOnError: false});
image = sharp((input as MediaRendererInput).mediaPath, {failOnError: false, animated: input.animate});
} else {
const svg_buffer = Buffer.from((input as SvgRendererInput).svgString);
image = sharp(svg_buffer, {density: 450});
@ -149,9 +150,9 @@ export class ImageRendererFactory {
image.rotate();
const metadata: Metadata = await image.metadata();
const kernel =
input.useLanczos3 === true
? sharp.kernel.lanczos3
: sharp.kernel.nearest;
input.useLanczos3 === true
? sharp.kernel.lanczos3
: sharp.kernel.nearest;
if (input.cut) {
image.extract(input.cut);

View File

@ -110,10 +110,14 @@ export class PhotoProcessing {
public static generateConvertedPath(mediaPath: string, size: number): string {
const file = path.basename(mediaPath);
const animated = Config.Media.Thumbnail.animateGif && path.extname(mediaPath).toLowerCase() == '.gif';
return path.join(
ProjectPath.TranscodedFolder,
ProjectPath.getRelativePathToImages(path.dirname(mediaPath)),
file + '_' + size + 'q' + Config.Media.Thumbnail.quality + (Config.Media.Thumbnail.smartSubsample ? 'cs' : '') + PhotoProcessing.CONVERTED_EXTENSION
file + '_' + size + 'q' + Config.Media.Thumbnail.quality +
(animated ? 'anim' : '') +
(Config.Media.Thumbnail.smartSubsample ? 'cs' : '') +
PhotoProcessing.CONVERTED_EXTENSION
);
}
@ -161,11 +165,13 @@ export class PhotoProcessing {
if (path.extname(convertedPath) !== PhotoProcessing.CONVERTED_EXTENSION) {
return false;
}
let nextIndex = convertedPath.lastIndexOf('_') + 1;
const sizeStr = convertedPath.substring(
convertedPath.lastIndexOf('_') + 1,
nextIndex,
convertedPath.lastIndexOf('q')
);
nextIndex = convertedPath.lastIndexOf('q') + 1;
const size = parseInt(sizeStr, 10);
@ -177,19 +183,8 @@ export class PhotoProcessing {
return false;
}
let qualityStr = convertedPath.substring(
convertedPath.lastIndexOf('q') + 1,
convertedPath.length - path.extname(convertedPath).length
);
if (Config.Media.Thumbnail.smartSubsample) {
if (!qualityStr.endsWith('cs')) { // remove chromatic subsampling flag if exists
return false;
}
qualityStr = qualityStr.slice(0, -2);
}
const qualityStr =convertedPath.substring(nextIndex,
nextIndex+convertedPath.substring(nextIndex).search(/[A-Za-z]/)); // end of quality string
const quality = parseInt(qualityStr, 10);
@ -198,6 +193,40 @@ export class PhotoProcessing {
return false;
}
nextIndex += qualityStr.length;
const lowerExt = path.extname(origFilePath).toLowerCase();
const shouldBeAnimated = Config.Media.Thumbnail.animateGif && lowerExt == '.gif';
if (shouldBeAnimated) {
if (convertedPath.substring(
nextIndex,
nextIndex + 'anim'.length
) != 'anim') {
return false;
}
nextIndex += 'anim'.length;
}
if (Config.Media.Thumbnail.smartSubsample) {
if (convertedPath.substring(
nextIndex,
nextIndex + 2
) != 'cs') {
return false;
}
nextIndex+=2;
}
if(convertedPath.substring(
nextIndex
).toLowerCase() !== path.extname(convertedPath)){
return false;
}
try {
await fsp.access(origFilePath, fsConstants.R_OK);
} catch (e) {
@ -298,6 +327,7 @@ viewBox="${svgIcon.viewBox || '0 0 512 512'}">d="${svgIcon.items}</svg>`,
size: size,
outPath,
makeSquare: false,
animate: false,
useLanczos3: Config.Media.Thumbnail.useLanczos3,
quality: Config.Media.Thumbnail.quality,
smartSubsample: Config.Media.Thumbnail.smartSubsample,

View File

@ -329,6 +329,7 @@ export class ServerThumbnailConfig extends ClientThumbnailConfig {
description: $localize`Use high quality chroma subsampling in webp. See: https://sharp.pixelplumbing.com/api-output#webp.`
})
smartSubsample = true;
@ConfigProperty({
type: 'float',
tags:
@ -339,6 +340,16 @@ export class ServerThumbnailConfig extends ClientThumbnailConfig {
description: $localize`This ratio of the face bounding box will be added to the face as a margin. Higher number add more margin.`
})
personFaceMargin: number = 0.7; // in ratio [0-1]
@ConfigProperty({
type: 'boolean',
tags:
{
name: $localize`Keep Gif animation`,
priority: ConfigPriority.underTheHood
},
description: $localize`Converts Gif to animated webp.`
})
animateGif = true;
}
@SubConfigClass({softReadonly: true})

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

View File

@ -12,6 +12,7 @@ describe('PhotoProcessing', () => {
await Config.load();
Config.Media.Thumbnail.thumbnailSizes = [];
Config.Media.Thumbnail.animateGif = true;
ProjectPath.ImageFolder = path.join(__dirname, './../../../assets');
const photoPath = path.join(ProjectPath.ImageFolder, 'test_png.png');
@ -33,6 +34,28 @@ describe('PhotoProcessing', () => {
}
});
it('should generate converted gif file path', async () => {
await Config.load();
Config.Media.Thumbnail.thumbnailSizes = [];
ProjectPath.ImageFolder = path.join(__dirname, './../../../assets');
const gifPath = path.join(ProjectPath.ImageFolder, 'earth.gif');
Config.Media.Thumbnail.animateGif = true;
expect(await PhotoProcessing
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(gifPath,
Config.Media.Photo.Converting.resolution)))
.to.be.true;
Config.Media.Thumbnail.animateGif = false;
expect(await PhotoProcessing
.isValidConvertedPath(PhotoProcessing.generateConvertedPath(gifPath,
Config.Media.Photo.Converting.resolution)))
.to.be.true;
});
/* eslint-disable no-unused-expressions,@typescript-eslint/no-unused-expressions */
it('should generate converted thumbnail path', async () => {

View File

@ -24,7 +24,7 @@ describe('DiskMangerWorker', () => {
ProjectPath.ImageFolder = path.join(__dirname, '/../../../assets');
const dir = await DiskManager.scanDirectory('/');
// should match the number of media (photo/video) files in the assets folder
expect(dir.media.length).to.be.equals(10);
expect(dir.media.length).to.be.equals(11);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const expected = require(path.join(__dirname, '/../../../assets/test image öüóőúéáű-.,.json'));
const i = dir.media.findIndex(m => m.name === 'test image öüóőúéáű-.,.jpg');