mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-12 14:55:39 +01:00
move image converting to child_process
This commit is contained in:
@@ -1,15 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BMPencode } from 'bmp-img';
|
||||
import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto';
|
||||
import {
|
||||
FullMime,
|
||||
ImageMime,
|
||||
SupportedMimeCategory
|
||||
FullMime, SupportedMimeCategory
|
||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { AsyncFailable, Fail } from 'picsur-shared/dist/types';
|
||||
import { QOIencode } from 'qoi-img';
|
||||
import { Sharp } from 'sharp';
|
||||
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { SharpWrapper } from '../../workers/sharp.wrapper';
|
||||
import { ImageResult } from './imageresult';
|
||||
import { UniversalSharp } from './universal-sharp';
|
||||
|
||||
@Injectable()
|
||||
export class ImageConverterService {
|
||||
@@ -17,6 +13,7 @@ export class ImageConverterService {
|
||||
image: Buffer,
|
||||
sourcemime: FullMime,
|
||||
targetmime: FullMime,
|
||||
options: ImageRequestParams,
|
||||
): AsyncFailable<ImageResult> {
|
||||
if (sourcemime.type !== targetmime.type) {
|
||||
return Fail("Can't convert from animated to still or vice versa");
|
||||
@@ -30,9 +27,9 @@ export class ImageConverterService {
|
||||
}
|
||||
|
||||
if (targetmime.type === SupportedMimeCategory.Image) {
|
||||
return this.convertStill(image, sourcemime, targetmime);
|
||||
return this.convertStill(image, sourcemime, targetmime, options);
|
||||
} else if (targetmime.type === SupportedMimeCategory.Animation) {
|
||||
return this.convertAnimation(image, targetmime);
|
||||
return this.convertAnimation(image, targetmime, options);
|
||||
} else {
|
||||
return Fail('Unsupported mime type');
|
||||
}
|
||||
@@ -42,43 +39,58 @@ export class ImageConverterService {
|
||||
image: Buffer,
|
||||
sourcemime: FullMime,
|
||||
targetmime: FullMime,
|
||||
options: ImageRequestParams,
|
||||
): AsyncFailable<ImageResult> {
|
||||
const sharpImage = UniversalSharp(image, sourcemime);
|
||||
const sharpWrapper = new SharpWrapper();
|
||||
|
||||
const hasStarted = await sharpWrapper.start(image, sourcemime);
|
||||
if (HasFailed(hasStarted)) return hasStarted;
|
||||
|
||||
// Do modifications
|
||||
|
||||
// Export
|
||||
let result: Buffer;
|
||||
|
||||
try {
|
||||
switch (targetmime.mime) {
|
||||
case ImageMime.PNG:
|
||||
result = await sharpImage.png().toBuffer();
|
||||
break;
|
||||
case ImageMime.JPEG:
|
||||
result = await sharpImage.jpeg().toBuffer();
|
||||
break;
|
||||
case ImageMime.TIFF:
|
||||
result = await sharpImage.tiff().toBuffer();
|
||||
break;
|
||||
case ImageMime.WEBP:
|
||||
result = await sharpImage.webp().toBuffer();
|
||||
break;
|
||||
case ImageMime.BMP:
|
||||
result = await this.sharpToBMP(sharpImage);
|
||||
break;
|
||||
case ImageMime.QOI:
|
||||
result = await this.sharpToQOI(sharpImage);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported mime type');
|
||||
if (options.height || options.width) {
|
||||
if (options.height && options.width) {
|
||||
sharpWrapper.operation('resize', {
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
fit: 'fill',
|
||||
kernel: 'cubic',
|
||||
});
|
||||
} else {
|
||||
sharpWrapper.operation('resize', {
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
fit: 'contain',
|
||||
kernel: 'cubic',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
return Fail(e);
|
||||
}
|
||||
if (options.rotate) {
|
||||
sharpWrapper.operation('rotate', options.rotate, {
|
||||
background: 'transparent',
|
||||
});
|
||||
}
|
||||
if (options.flipx) {
|
||||
sharpWrapper.operation('flop');
|
||||
}
|
||||
if (options.flipy) {
|
||||
sharpWrapper.operation('flip');
|
||||
}
|
||||
if (options.noalpha) {
|
||||
sharpWrapper.operation('removeAlpha');
|
||||
}
|
||||
if (options.negative) {
|
||||
sharpWrapper.operation('negate');
|
||||
}
|
||||
if (options.greyscale) {
|
||||
sharpWrapper.operation('greyscale');
|
||||
}
|
||||
|
||||
// Export
|
||||
const result = await sharpWrapper.finish(targetmime, options);
|
||||
if (HasFailed(result)) return result;
|
||||
|
||||
return {
|
||||
image: result,
|
||||
image: result.data,
|
||||
mime: targetmime.mime,
|
||||
};
|
||||
}
|
||||
@@ -86,6 +98,7 @@ export class ImageConverterService {
|
||||
private async convertAnimation(
|
||||
image: Buffer,
|
||||
targetmime: FullMime,
|
||||
options: ImageRequestParams,
|
||||
): AsyncFailable<ImageResult> {
|
||||
// Apng and gif are stored as is for now
|
||||
return {
|
||||
@@ -93,38 +106,4 @@ export class ImageConverterService {
|
||||
mime: targetmime.mime,
|
||||
};
|
||||
}
|
||||
|
||||
private async sharpToBMP(sharpImage: Sharp): Promise<Buffer> {
|
||||
const dimensions = await sharpImage.metadata();
|
||||
if (!dimensions.width || !dimensions.height || !dimensions.channels) {
|
||||
throw new Error('Invalid image');
|
||||
}
|
||||
|
||||
const raw = await sharpImage.raw().toBuffer();
|
||||
|
||||
const encoded = BMPencode(raw, {
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
channels: dimensions.channels,
|
||||
});
|
||||
|
||||
return encoded;
|
||||
}
|
||||
|
||||
private async sharpToQOI(sharpImage: Sharp): Promise<Buffer> {
|
||||
const dimensions = await sharpImage.metadata();
|
||||
if (!dimensions.width || !dimensions.height || !dimensions.channels) {
|
||||
throw new Error('Invalid image');
|
||||
}
|
||||
|
||||
const raw = await sharpImage.raw().toBuffer();
|
||||
|
||||
const encoded = QOIencode(raw, {
|
||||
height: dimensions.height,
|
||||
width: dimensions.width,
|
||||
channels: dimensions.channels,
|
||||
});
|
||||
|
||||
return encoded;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,26 +35,23 @@ export class ImageProcessorService {
|
||||
|
||||
sharpImage = sharpImage.toColorspace('srgb');
|
||||
|
||||
const metadata = await sharpImage.metadata();
|
||||
const pixels = await sharpImage.raw().toBuffer();
|
||||
const processedImage = await sharpImage.raw().toBuffer({
|
||||
resolveWithObject: true,
|
||||
});
|
||||
|
||||
if (
|
||||
metadata.hasAlpha === undefined ||
|
||||
metadata.width === undefined ||
|
||||
metadata.height === undefined
|
||||
)
|
||||
return Fail('Invalid image');
|
||||
|
||||
if (metadata.width >= 32768 || metadata.height >= 32768) {
|
||||
processedImage.info.width >= 32768 ||
|
||||
processedImage.info.height >= 32768
|
||||
) {
|
||||
return Fail('Image too large');
|
||||
}
|
||||
|
||||
// Png can be more efficient than QOI, but its just sooooooo slow
|
||||
const qoiImage = QOIencode(pixels, {
|
||||
channels: metadata.hasAlpha ? 4 : 3,
|
||||
const qoiImage = QOIencode(processedImage.data, {
|
||||
channels: processedImage.info.channels,
|
||||
colorspace: QOIColorSpace.SRGB,
|
||||
height: metadata.height,
|
||||
width: metadata.width,
|
||||
height: processedImage.info.height,
|
||||
width: processedImage.info.width,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import Crypto from 'crypto';
|
||||
import { fileTypeFromBuffer, FileTypeResult } from 'file-type';
|
||||
import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto';
|
||||
import { ImageFileType } from 'picsur-shared/dist/dto/image-file-types.dto';
|
||||
import { FullMime } from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.dto';
|
||||
import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.dto';
|
||||
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { ParseMime } from 'picsur-shared/dist/util/parse-mime';
|
||||
import { IsQOI } from 'qoi-img';
|
||||
import { ImageDBService } from '../../collections/image-db/image-db.service';
|
||||
import { ImageFileDBService } from '../../collections/image-db/image-file-db.service';
|
||||
import { SysPreferenceService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { UsrPreferenceService } from '../../collections/preference-db/usr-preference-db.service';
|
||||
import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity';
|
||||
import { EImageFileBackend } from '../../models/entities/image-file.entity';
|
||||
@@ -27,6 +30,7 @@ export class ImageManagerService {
|
||||
private readonly processService: ImageProcessorService,
|
||||
private readonly convertService: ImageConverterService,
|
||||
private readonly userPref: UsrPreferenceService,
|
||||
private readonly sysPref: SysPreferenceService,
|
||||
) {}
|
||||
|
||||
public async retrieveInfo(id: string): AsyncFailable<EImageBackend> {
|
||||
@@ -84,18 +88,28 @@ export class ImageManagerService {
|
||||
|
||||
public async getConverted(
|
||||
imageId: string,
|
||||
options: {
|
||||
mime: string;
|
||||
},
|
||||
mime: string,
|
||||
options: ImageRequestParams,
|
||||
): AsyncFailable<EImageDerivativeBackend> {
|
||||
const targetMime = ParseMime(options.mime);
|
||||
const targetMime = ParseMime(mime);
|
||||
if (HasFailed(targetMime)) return targetMime;
|
||||
|
||||
const converted_key = this.getConvertHash(options);
|
||||
const converted_key = this.getConvertHash({ mime, ...options });
|
||||
|
||||
const [save_derivatives, allow_editing] = await Promise.all([
|
||||
this.sysPref.getBooleanPreference(SysPreference.SaveDerivatives),
|
||||
this.sysPref.getBooleanPreference(SysPreference.AllowEditing),
|
||||
]);
|
||||
if (HasFailed(save_derivatives)) return save_derivatives;
|
||||
if (HasFailed(allow_editing)) return allow_editing;
|
||||
|
||||
return MutexFallBack(
|
||||
converted_key,
|
||||
() => this.imageFilesService.getDerivative(imageId, converted_key),
|
||||
() => {
|
||||
if (save_derivatives)
|
||||
return this.imageFilesService.getDerivative(imageId, converted_key);
|
||||
else return Promise.resolve(null);
|
||||
},
|
||||
async () => {
|
||||
const masterImage = await this.getMaster(imageId);
|
||||
if (HasFailed(masterImage)) return masterImage;
|
||||
@@ -108,6 +122,7 @@ export class ImageManagerService {
|
||||
masterImage.data,
|
||||
sourceMime,
|
||||
targetMime,
|
||||
allow_editing ? options : {},
|
||||
);
|
||||
if (HasFailed(convertResult)) return convertResult;
|
||||
|
||||
@@ -117,12 +132,21 @@ export class ImageManagerService {
|
||||
} in ${Date.now() - startTime}ms`,
|
||||
);
|
||||
|
||||
return await this.imageFilesService.addDerivative(
|
||||
imageId,
|
||||
converted_key,
|
||||
convertResult.mime,
|
||||
convertResult.image,
|
||||
);
|
||||
if (save_derivatives) {
|
||||
return await this.imageFilesService.addDerivative(
|
||||
imageId,
|
||||
converted_key,
|
||||
convertResult.mime,
|
||||
convertResult.image,
|
||||
);
|
||||
} else {
|
||||
const derivative = new EImageDerivativeBackend();
|
||||
derivative.mime = convertResult.mime;
|
||||
derivative.data = convertResult.image;
|
||||
derivative.image_id = imageId;
|
||||
derivative.key = converted_key;
|
||||
return derivative;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user