2022-04-15 12:52:53 +02:00
|
|
|
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';
|
2022-04-15 12:52:53 +02:00
|
|
|
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';
|
2022-04-15 12:52:53 +02:00
|
|
|
|
2022-04-21 16:53:40 +02:00
|
|
|
interface ProcessResult {
|
|
|
|
|
image: Buffer;
|
|
|
|
|
mime: string;
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-15 12:52:53 +02:00
|
|
|
@Injectable()
|
|
|
|
|
export class ImageProcessorService {
|
|
|
|
|
public async process(
|
|
|
|
|
image: Buffer,
|
|
|
|
|
mime: FullMime,
|
|
|
|
|
userid: string,
|
2022-04-21 16:53:40 +02:00
|
|
|
): AsyncFailable<ProcessResult> {
|
2022-04-15 12:52:53 +02:00
|
|
|
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: {},
|
2022-04-21 16:53:40 +02:00
|
|
|
): AsyncFailable<ProcessResult> {
|
|
|
|
|
let processedMime = mime.mime;
|
2022-04-16 16:35:28 +02:00
|
|
|
let sharpImage: sharp.Sharp;
|
2022-04-15 12:52:53 +02:00
|
|
|
|
2022-04-21 16:53:40 +02:00
|
|
|
// 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);
|
2022-04-15 12:52:53 +02:00
|
|
|
}
|
2022-04-21 16:53:40 +02:00
|
|
|
processedMime = ImageMime.QOI;
|
2022-04-15 12:52:53 +02:00
|
|
|
|
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');
|
|
|
|
|
|
2022-04-21 16:53:40 +02:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
|
2022-04-21 16:53:40 +02:00
|
|
|
return {
|
|
|
|
|
image: qoiImage,
|
|
|
|
|
mime: processedMime,
|
|
|
|
|
};
|
2022-04-15 12:52:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async processAnimation(
|
|
|
|
|
image: Buffer,
|
|
|
|
|
mime: FullMime,
|
|
|
|
|
options: {},
|
2022-04-21 16:53:40 +02:00
|
|
|
): AsyncFailable<ProcessResult> {
|
2022-04-15 12:52:53 +02:00
|
|
|
// Apng and gif are stored as is for now
|
2022-04-21 16:53:40 +02:00
|
|
|
return {
|
|
|
|
|
image: image,
|
|
|
|
|
mime: mime.mime,
|
|
|
|
|
};
|
2022-04-15 12:52:53 +02:00
|
|
|
}
|
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,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
2022-04-15 12:52:53 +02:00
|
|
|
}
|