move image converting to child_process

This commit is contained in:
rubikscraft
2022-05-01 23:29:56 +02:00
parent 342e52601e
commit 377e6d7709
27 changed files with 1201 additions and 416 deletions

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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;
}
},
);
}