Files
Picsur/backend/src/managers/image/image-processor.service.ts

133 lines
3.2 KiB
TypeScript
Raw Normal View History

import { Injectable } from '@nestjs/common';
2022-04-15 13:28:25 +02:00
import * as bmp from '@vingle/bmp-js';
2022-04-16 16:35:28 +02:00
import decodeico from 'decode-ico';
import {
FullMime,
ImageMime,
SupportedMimeCategory
2022-04-16 16:35:28 +02:00
} from 'picsur-shared/dist/dto/mimes.dto';
import { AsyncFailable, Fail } from 'picsur-shared/dist/types';
2022-04-16 16:54:13 +02:00
import { QOIColorSpace, QOIdecode, QOIencode } from 'qoi-img';
2022-04-16 16:35:28 +02:00
import sharp from 'sharp';
interface ProcessResult {
image: Buffer;
mime: string;
}
@Injectable()
export class ImageProcessorService {
public async process(
image: Buffer,
mime: FullMime,
userid: string,
): AsyncFailable<ProcessResult> {
if (mime.type === SupportedMimeCategory.Image) {
return await this.processStill(image, mime, {});
} else if (mime.type === SupportedMimeCategory.Animation) {
return await this.processAnimation(image, mime, {});
} else {
return Fail('Unsupported mime type');
}
}
private async processStill(
image: Buffer,
mime: FullMime,
options: {},
): AsyncFailable<ProcessResult> {
let processedMime = mime.mime;
2022-04-16 16:35:28 +02:00
let sharpImage: sharp.Sharp;
// TODO: ensure mime and sharp are in agreement
2022-04-15 13:28:25 +02:00
if (mime.mime === ImageMime.ICO) {
2022-04-16 16:35:28 +02:00
sharpImage = this.icoSharp(image);
2022-04-15 13:28:25 +02:00
} else if (mime.mime === ImageMime.BMP) {
2022-04-16 16:35:28 +02:00
sharpImage = this.bmpSharp(image);
2022-04-16 16:54:13 +02:00
} else if (mime.mime === ImageMime.QOI) {
sharpImage = this.qoiSharp(image);
2022-04-15 13:28:25 +02:00
} else {
2022-04-16 16:35:28 +02:00
sharpImage = sharp(image);
}
processedMime = ImageMime.QOI;
2022-04-16 16:35:28 +02:00
sharpImage = sharpImage.toColorspace('srgb');
const metadata = await sharpImage.metadata();
const pixels = await sharpImage.raw().toBuffer();
if (
metadata.hasAlpha === undefined ||
metadata.width === undefined ||
metadata.height === undefined
)
return Fail('Invalid image');
if (metadata.width >= 32768 || metadata.height >= 32768) {
return Fail('Image too large');
}
2022-04-16 16:35:28 +02:00
// Png can be more efficient than QOI, but its just sooooooo slow
const qoiImage = QOIencode(pixels, {
channels: metadata.hasAlpha ? 4 : 3,
colorSpace: QOIColorSpace.SRGB,
height: metadata.height,
width: metadata.width,
});
return {
image: qoiImage,
mime: processedMime,
};
}
private async processAnimation(
image: Buffer,
mime: FullMime,
options: {},
): AsyncFailable<ProcessResult> {
// Apng and gif are stored as is for now
return {
image: image,
mime: mime.mime,
};
}
2022-04-15 13:28:25 +02:00
private bmpSharp(image: Buffer) {
const bitmap = bmp.decode(image, true);
return sharp(bitmap.data, {
raw: {
width: bitmap.width,
height: bitmap.height,
channels: 4,
},
});
}
2022-04-16 16:35:28 +02:00
private icoSharp(image: Buffer) {
const result = decodeico(image);
// Get biggest image
const best = result.sort((a, b) => b.width - a.width)[0];
return sharp(best.data, {
raw: {
width: best.width,
height: best.height,
channels: 4,
},
});
}
2022-04-16 16:54:13 +02:00
private qoiSharp(image: Buffer) {
const result = QOIdecode(image);
return sharp(result.pixels, {
raw: {
width: result.width,
height: result.height,
channels: result.channels,
},
});
}
}