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:
parent
4be86d6ebc
commit
1458faca70
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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})
|
||||
|
BIN
test/backend/assets/earth.gif
Normal file
BIN
test/backend/assets/earth.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 978 KiB |
@ -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 () => {
|
||||
|
||||
|
@ -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');
|
||||
|
Loading…
x
Reference in New Issue
Block a user