mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-08 13:05:39 +01:00
change the way file types are handled
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { ImageFileType } from 'picsur-shared/dist/dto/image-file-types.enum';
|
import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum';
|
||||||
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
|
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
|
||||||
import { LessThan, Repository } from 'typeorm';
|
import { LessThan, Repository } from 'typeorm';
|
||||||
import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity';
|
import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity';
|
||||||
@@ -20,19 +20,19 @@ export class ImageFileDBService {
|
|||||||
|
|
||||||
public async setFile(
|
public async setFile(
|
||||||
imageId: string,
|
imageId: string,
|
||||||
type: ImageFileType,
|
variant: ImageEntryVariant,
|
||||||
file: Buffer,
|
file: Buffer,
|
||||||
mime: string,
|
filetype: string,
|
||||||
): AsyncFailable<true> {
|
): AsyncFailable<true> {
|
||||||
const imageFile = new EImageFileBackend();
|
const imageFile = new EImageFileBackend();
|
||||||
imageFile.image_id = imageId;
|
imageFile.image_id = imageId;
|
||||||
imageFile.type = type;
|
imageFile.variant = variant;
|
||||||
imageFile.mime = mime;
|
imageFile.filetype = filetype;
|
||||||
imageFile.data = file;
|
imageFile.data = file;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.imageFileRepo.upsert(imageFile, {
|
await this.imageFileRepo.upsert(imageFile, {
|
||||||
conflictPaths: ['image_id', 'type'],
|
conflictPaths: ['image_id', 'variant'],
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Fail(FT.Database, e);
|
return Fail(FT.Database, e);
|
||||||
@@ -43,11 +43,11 @@ export class ImageFileDBService {
|
|||||||
|
|
||||||
public async getFile(
|
public async getFile(
|
||||||
imageId: string,
|
imageId: string,
|
||||||
type: ImageFileType,
|
variant: ImageEntryVariant,
|
||||||
): AsyncFailable<EImageFileBackend> {
|
): AsyncFailable<EImageFileBackend> {
|
||||||
try {
|
try {
|
||||||
const found = await this.imageFileRepo.findOne({
|
const found = await this.imageFileRepo.findOne({
|
||||||
where: { image_id: imageId ?? '', type: type ?? '' },
|
where: { image_id: imageId ?? '', variant: variant ?? '' },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!found) return Fail(FT.NotFound, 'Image not found');
|
if (!found) return Fail(FT.NotFound, 'Image not found');
|
||||||
@@ -58,20 +58,20 @@ export class ImageFileDBService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This is useful because you dont have to pull the whole image file
|
// This is useful because you dont have to pull the whole image file
|
||||||
public async getFileMimes(
|
public async getFileTypes(
|
||||||
imageId: string,
|
imageId: string,
|
||||||
): AsyncFailable<{ [key in ImageFileType]?: string }> {
|
): AsyncFailable<{ [key in ImageEntryVariant]?: string }> {
|
||||||
try {
|
try {
|
||||||
const found = await this.imageFileRepo.find({
|
const found = await this.imageFileRepo.find({
|
||||||
where: { image_id: imageId },
|
where: { image_id: imageId },
|
||||||
select: ['type', 'mime'],
|
select: ['variant', 'filetype'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!found) return Fail(FT.NotFound, 'Image not found');
|
if (!found) return Fail(FT.NotFound, 'Image not found');
|
||||||
|
|
||||||
const result: { [key in ImageFileType]?: string } = {};
|
const result: { [key in ImageEntryVariant]?: string } = {};
|
||||||
for (const file of found) {
|
for (const file of found) {
|
||||||
result[file.type] = file.mime;
|
result[file.variant] = file.filetype;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -83,13 +83,13 @@ export class ImageFileDBService {
|
|||||||
public async addDerivative(
|
public async addDerivative(
|
||||||
imageId: string,
|
imageId: string,
|
||||||
key: string,
|
key: string,
|
||||||
mime: string,
|
filetype: string,
|
||||||
file: Buffer,
|
file: Buffer,
|
||||||
): AsyncFailable<EImageDerivativeBackend> {
|
): AsyncFailable<EImageDerivativeBackend> {
|
||||||
const imageDerivative = new EImageDerivativeBackend();
|
const imageDerivative = new EImageDerivativeBackend();
|
||||||
imageDerivative.image_id = imageId;
|
imageDerivative.image_id = imageId;
|
||||||
imageDerivative.key = key;
|
imageDerivative.key = key;
|
||||||
imageDerivative.mime = mime;
|
imageDerivative.filetype = filetype;
|
||||||
imageDerivative.data = file;
|
imageDerivative.data = file;
|
||||||
imageDerivative.last_read = new Date();
|
imageDerivative.last_read = new Date();
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import {
|
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
|
||||||
ArgumentMetadata, Injectable,
|
import { Ext2FileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
PipeTransform
|
import { Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||||
} from '@nestjs/common';
|
|
||||||
import { Ext2Mime } from 'picsur-shared/dist/dto/mimes.dto';
|
|
||||||
import { Fail, FT } from 'picsur-shared/dist/types';
|
|
||||||
import { UUIDRegex } from 'picsur-shared/dist/util/common-regex';
|
import { UUIDRegex } from 'picsur-shared/dist/util/common-regex';
|
||||||
import { ImageFullId } from '../../models/constants/image-full-id.const';
|
import { ImageFullId } from '../../models/constants/image-full-id.const';
|
||||||
|
|
||||||
@@ -16,19 +13,19 @@ export class ImageFullIdPipe implements PipeTransform<string, ImageFullId> {
|
|||||||
if (!UUIDRegex.test(id))
|
if (!UUIDRegex.test(id))
|
||||||
throw Fail(FT.UsrValidation, 'Invalid image identifier');
|
throw Fail(FT.UsrValidation, 'Invalid image identifier');
|
||||||
|
|
||||||
const mime = Ext2Mime(ext);
|
const filetype = Ext2FileType(ext);
|
||||||
|
|
||||||
if (mime === undefined)
|
if (HasFailed(filetype))
|
||||||
throw Fail(FT.UsrValidation, 'Invalid image identifier');
|
throw Fail(FT.UsrValidation, 'Invalid image identifier');
|
||||||
|
|
||||||
return { type: 'normal', id, ext, mime };
|
return { variant: 'normal', id, ext, filetype };
|
||||||
} else if (split.length === 1) {
|
} else if (split.length === 1) {
|
||||||
const [id] = split;
|
const [id] = split;
|
||||||
|
|
||||||
if (!UUIDRegex.test(id))
|
if (!UUIDRegex.test(id))
|
||||||
throw Fail(FT.UsrValidation, 'Invalid image identifier');
|
throw Fail(FT.UsrValidation, 'Invalid image identifier');
|
||||||
|
|
||||||
return { type: 'original', id, ext: null, mime: null };
|
return { variant: 'original', id, ext: null, filetype: null };
|
||||||
} else {
|
} else {
|
||||||
throw Fail(FT.UsrValidation, 'Invalid image identifier');
|
throw Fail(FT.UsrValidation, 'Invalid image identifier');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ async function bootstrap() {
|
|||||||
AppModule,
|
AppModule,
|
||||||
fastifyAdapter,
|
fastifyAdapter,
|
||||||
{
|
{
|
||||||
bufferLogs: true,
|
bufferLogs: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto';
|
import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto';
|
||||||
import {
|
import {
|
||||||
FullMime,
|
FileType,
|
||||||
SupportedMimeCategory
|
SupportedFileTypeCategory
|
||||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||||
@@ -17,28 +17,29 @@ export class ImageConverterService {
|
|||||||
|
|
||||||
public async convert(
|
public async convert(
|
||||||
image: Buffer,
|
image: Buffer,
|
||||||
sourcemime: FullMime,
|
sourceFiletype: FileType,
|
||||||
targetmime: FullMime,
|
targetFiletype: FileType,
|
||||||
options: ImageRequestParams,
|
options: ImageRequestParams,
|
||||||
): AsyncFailable<ImageResult> {
|
): AsyncFailable<ImageResult> {
|
||||||
if (sourcemime.type !== targetmime.type) {
|
if (sourceFiletype.category !== sourceFiletype.category) {
|
||||||
return Fail(
|
return Fail(
|
||||||
FT.Impossible,
|
FT.Impossible,
|
||||||
"Can't convert from animated to still or vice versa",
|
"Can't convert from animated to still or vice versa",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourcemime.mime === targetmime.mime) {
|
if (sourceFiletype.identifier === targetFiletype.identifier) {
|
||||||
return {
|
return {
|
||||||
mime: targetmime.mime,
|
filetype: targetFiletype.identifier,
|
||||||
image,
|
image,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetmime.type === SupportedMimeCategory.Image) {
|
if (targetFiletype.category === SupportedFileTypeCategory.Image) {
|
||||||
return this.convertStill(image, sourcemime, targetmime, options);
|
return this.convertStill(image, sourceFiletype, targetFiletype, options);
|
||||||
} else if (targetmime.type === SupportedMimeCategory.Animation) {
|
} else if (targetFiletype.category === SupportedFileTypeCategory.Animation) {
|
||||||
return this.convertAnimation(image, targetmime, options);
|
return this.convertStill(image, sourceFiletype, targetFiletype, options);
|
||||||
|
//return this.convertAnimation(image, targetmime, options);
|
||||||
} else {
|
} else {
|
||||||
return Fail(FT.SysValidation, 'Unsupported mime type');
|
return Fail(FT.SysValidation, 'Unsupported mime type');
|
||||||
}
|
}
|
||||||
@@ -46,8 +47,8 @@ export class ImageConverterService {
|
|||||||
|
|
||||||
private async convertStill(
|
private async convertStill(
|
||||||
image: Buffer,
|
image: Buffer,
|
||||||
sourcemime: FullMime,
|
sourceFiletype: FileType,
|
||||||
targetmime: FullMime,
|
targetFiletype: FileType,
|
||||||
options: ImageRequestParams,
|
options: ImageRequestParams,
|
||||||
): AsyncFailable<ImageResult> {
|
): AsyncFailable<ImageResult> {
|
||||||
const [memLimit, timeLimit] = await Promise.all([
|
const [memLimit, timeLimit] = await Promise.all([
|
||||||
@@ -60,7 +61,7 @@ export class ImageConverterService {
|
|||||||
const timeLimitMS = ms(timeLimit);
|
const timeLimitMS = ms(timeLimit);
|
||||||
|
|
||||||
const sharpWrapper = new SharpWrapper(timeLimitMS, memLimit);
|
const sharpWrapper = new SharpWrapper(timeLimitMS, memLimit);
|
||||||
const hasStarted = await sharpWrapper.start(image, sourcemime);
|
const hasStarted = await sharpWrapper.start(image, sourceFiletype);
|
||||||
if (HasFailed(hasStarted)) return hasStarted;
|
if (HasFailed(hasStarted)) return hasStarted;
|
||||||
|
|
||||||
// Do modifications
|
// Do modifications
|
||||||
@@ -103,24 +104,24 @@ export class ImageConverterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Export
|
// Export
|
||||||
const result = await sharpWrapper.finish(targetmime, options);
|
const result = await sharpWrapper.finish(targetFiletype, options);
|
||||||
if (HasFailed(result)) return result;
|
if (HasFailed(result)) return result;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
image: result.data,
|
image: result.data,
|
||||||
mime: targetmime.mime,
|
filetype: targetFiletype.identifier,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async convertAnimation(
|
private async convertAnimation(
|
||||||
image: Buffer,
|
image: Buffer,
|
||||||
targetmime: FullMime,
|
targetFiletype: FileType,
|
||||||
options: ImageRequestParams,
|
options: ImageRequestParams,
|
||||||
): AsyncFailable<ImageResult> {
|
): AsyncFailable<ImageResult> {
|
||||||
// Apng and gif are stored as is for now
|
// Apng and gif are stored as is for now
|
||||||
return {
|
return {
|
||||||
image: image,
|
image: image,
|
||||||
mime: targetmime.mime,
|
filetype: targetFiletype.identifier,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
FullMime,
|
FileType,
|
||||||
ImageMime,
|
ImageFileType,
|
||||||
SupportedMimeCategory
|
SupportedFileTypeCategory
|
||||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
|
|
||||||
import { QOIColorSpace, QOIencode } from 'qoi-img';
|
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||||
|
import { ParseFileType } from 'picsur-shared/dist/util/parse-mime';
|
||||||
|
import { ImageConverterService } from './image-converter.service';
|
||||||
import { ImageResult } from './imageresult';
|
import { ImageResult } from './imageresult';
|
||||||
import { UniversalSharp } from './universal-sharp';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImageProcessorService {
|
export class ImageProcessorService {
|
||||||
|
constructor(private readonly imageConverter: ImageConverterService) {}
|
||||||
|
|
||||||
public async process(
|
public async process(
|
||||||
image: Buffer,
|
image: Buffer,
|
||||||
mime: FullMime,
|
filetype: FileType,
|
||||||
): AsyncFailable<ImageResult> {
|
): AsyncFailable<ImageResult> {
|
||||||
if (mime.type === SupportedMimeCategory.Image) {
|
if (filetype.category === SupportedFileTypeCategory.Image) {
|
||||||
return await this.processStill(image, mime);
|
return await this.processStill(image, filetype);
|
||||||
} else if (mime.type === SupportedMimeCategory.Animation) {
|
} else if (filetype.category === SupportedFileTypeCategory.Animation) {
|
||||||
return await this.processAnimation(image, mime);
|
return await this.processAnimation(image, filetype);
|
||||||
} else {
|
} else {
|
||||||
return Fail(FT.SysValidation, 'Unsupported mime type');
|
return Fail(FT.SysValidation, 'Unsupported mime type');
|
||||||
}
|
}
|
||||||
@@ -26,48 +29,22 @@ export class ImageProcessorService {
|
|||||||
|
|
||||||
private async processStill(
|
private async processStill(
|
||||||
image: Buffer,
|
image: Buffer,
|
||||||
mime: FullMime,
|
filetype: FileType,
|
||||||
): AsyncFailable<ImageResult> {
|
): AsyncFailable<ImageResult> {
|
||||||
let processedMime = mime.mime;
|
const outputFileType = ParseFileType(ImageFileType.QOI);
|
||||||
|
if (HasFailed(outputFileType)) return outputFileType;
|
||||||
|
|
||||||
let sharpImage = UniversalSharp(image, mime);
|
return this.imageConverter.convert(image, filetype, outputFileType, {});
|
||||||
processedMime = ImageMime.QOI;
|
|
||||||
|
|
||||||
sharpImage = sharpImage.toColorspace('srgb');
|
|
||||||
|
|
||||||
const processedImage = await sharpImage.raw().toBuffer({
|
|
||||||
resolveWithObject: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
processedImage.info.width >= 32768 ||
|
|
||||||
processedImage.info.height >= 32768
|
|
||||||
) {
|
|
||||||
return Fail(FT.UsrValidation, 'Image too large');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Png can be more efficient than QOI, but its just sooooooo slow
|
|
||||||
const qoiImage = QOIencode(processedImage.data, {
|
|
||||||
channels: processedImage.info.channels,
|
|
||||||
colorspace: QOIColorSpace.SRGB,
|
|
||||||
height: processedImage.info.height,
|
|
||||||
width: processedImage.info.width,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
image: qoiImage,
|
|
||||||
mime: processedMime,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processAnimation(
|
private async processAnimation(
|
||||||
image: Buffer,
|
image: Buffer,
|
||||||
mime: FullMime,
|
filetype: FileType,
|
||||||
): AsyncFailable<ImageResult> {
|
): AsyncFailable<ImageResult> {
|
||||||
// Apng and gif are stored as is for now
|
// Apng and gif are stored as is for now
|
||||||
return {
|
return {
|
||||||
image: image,
|
image: image,
|
||||||
mime: mime.mime,
|
filetype: filetype.identifier,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
import Crypto from 'crypto';
|
import Crypto from 'crypto';
|
||||||
import { fileTypeFromBuffer, FileTypeResult } from 'file-type';
|
import { fileTypeFromBuffer, FileTypeResult } from 'file-type';
|
||||||
import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto';
|
import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto';
|
||||||
import { ImageFileType } from 'picsur-shared/dist/dto/image-file-types.enum';
|
import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum';
|
||||||
import { FullMime } from 'picsur-shared/dist/dto/mimes.dto';
|
import { FileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||||
import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.enum';
|
import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.enum';
|
||||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||||
import { FindResult } from 'picsur-shared/dist/types/find-result';
|
import { FindResult } from 'picsur-shared/dist/types/find-result';
|
||||||
import { ParseMime } from 'picsur-shared/dist/util/parse-mime';
|
import {
|
||||||
|
ParseFileType,
|
||||||
|
ParseMime2FileType
|
||||||
|
} from 'picsur-shared/dist/util/parse-mime';
|
||||||
import { IsQOI } from 'qoi-img';
|
import { IsQOI } from 'qoi-img';
|
||||||
import { ImageDBService } from '../../collections/image-db/image-db.service';
|
import { ImageDBService } from '../../collections/image-db/image-db.service';
|
||||||
import { ImageFileDBService } from '../../collections/image-db/image-file-db.service';
|
import { ImageFileDBService } from '../../collections/image-db/image-file-db.service';
|
||||||
@@ -57,8 +60,8 @@ export class ImageManagerService {
|
|||||||
image: Buffer,
|
image: Buffer,
|
||||||
userid: string,
|
userid: string,
|
||||||
): AsyncFailable<EImageBackend> {
|
): AsyncFailable<EImageBackend> {
|
||||||
const fullMime = await this.getFullMimeFromBuffer(image);
|
const fileType = await this.getFileTypeFromBuffer(image);
|
||||||
if (HasFailed(fullMime)) return fullMime;
|
if (HasFailed(fileType)) return fileType;
|
||||||
|
|
||||||
// Check if need to save orignal
|
// Check if need to save orignal
|
||||||
const keepOriginal = await this.userPref.getBooleanPreference(
|
const keepOriginal = await this.userPref.getBooleanPreference(
|
||||||
@@ -68,7 +71,7 @@ export class ImageManagerService {
|
|||||||
if (HasFailed(keepOriginal)) return keepOriginal;
|
if (HasFailed(keepOriginal)) return keepOriginal;
|
||||||
|
|
||||||
// Process
|
// Process
|
||||||
const processResult = await this.processService.process(image, fullMime);
|
const processResult = await this.processService.process(image, fileType);
|
||||||
if (HasFailed(processResult)) return processResult;
|
if (HasFailed(processResult)) return processResult;
|
||||||
|
|
||||||
// Save processed to db
|
// Save processed to db
|
||||||
@@ -77,18 +80,18 @@ export class ImageManagerService {
|
|||||||
|
|
||||||
const imageFileEntity = await this.imageFilesService.setFile(
|
const imageFileEntity = await this.imageFilesService.setFile(
|
||||||
imageEntity.id,
|
imageEntity.id,
|
||||||
ImageFileType.MASTER,
|
ImageEntryVariant.MASTER,
|
||||||
processResult.image,
|
processResult.image,
|
||||||
processResult.mime,
|
processResult.filetype,
|
||||||
);
|
);
|
||||||
if (HasFailed(imageFileEntity)) return imageFileEntity;
|
if (HasFailed(imageFileEntity)) return imageFileEntity;
|
||||||
|
|
||||||
if (keepOriginal) {
|
if (keepOriginal) {
|
||||||
const originalFileEntity = await this.imageFilesService.setFile(
|
const originalFileEntity = await this.imageFilesService.setFile(
|
||||||
imageEntity.id,
|
imageEntity.id,
|
||||||
ImageFileType.ORIGINAL,
|
ImageEntryVariant.ORIGINAL,
|
||||||
image,
|
image,
|
||||||
fullMime.mime,
|
fileType.identifier,
|
||||||
);
|
);
|
||||||
if (HasFailed(originalFileEntity)) return originalFileEntity;
|
if (HasFailed(originalFileEntity)) return originalFileEntity;
|
||||||
}
|
}
|
||||||
@@ -98,13 +101,13 @@ export class ImageManagerService {
|
|||||||
|
|
||||||
public async getConverted(
|
public async getConverted(
|
||||||
imageId: string,
|
imageId: string,
|
||||||
mime: string,
|
fileType: string,
|
||||||
options: ImageRequestParams,
|
options: ImageRequestParams,
|
||||||
): AsyncFailable<EImageDerivativeBackend> {
|
): AsyncFailable<EImageDerivativeBackend> {
|
||||||
const targetMime = ParseMime(mime);
|
const targetFileType = ParseFileType(fileType);
|
||||||
if (HasFailed(targetMime)) return targetMime;
|
if (HasFailed(targetFileType)) return targetFileType;
|
||||||
|
|
||||||
const converted_key = this.getConvertHash({ mime, ...options });
|
const converted_key = this.getConvertHash({ mime: fileType, ...options });
|
||||||
|
|
||||||
const [save_derivatives, allow_editing] = await Promise.all([
|
const [save_derivatives, allow_editing] = await Promise.all([
|
||||||
this.sysPref.getBooleanPreference(SysPreference.SaveDerivatives),
|
this.sysPref.getBooleanPreference(SysPreference.SaveDerivatives),
|
||||||
@@ -124,21 +127,21 @@ export class ImageManagerService {
|
|||||||
const masterImage = await this.getMaster(imageId);
|
const masterImage = await this.getMaster(imageId);
|
||||||
if (HasFailed(masterImage)) return masterImage;
|
if (HasFailed(masterImage)) return masterImage;
|
||||||
|
|
||||||
const sourceMime = ParseMime(masterImage.mime);
|
const sourceFileType = ParseFileType(masterImage.filetype);
|
||||||
if (HasFailed(sourceMime)) return sourceMime;
|
if (HasFailed(sourceFileType)) return sourceFileType;
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const convertResult = await this.convertService.convert(
|
const convertResult = await this.convertService.convert(
|
||||||
masterImage.data,
|
masterImage.data,
|
||||||
sourceMime,
|
sourceFileType,
|
||||||
targetMime,
|
targetFileType,
|
||||||
allow_editing ? options : {},
|
allow_editing ? options : {},
|
||||||
);
|
);
|
||||||
if (HasFailed(convertResult)) return convertResult;
|
if (HasFailed(convertResult)) return convertResult;
|
||||||
|
|
||||||
this.logger.verbose(
|
this.logger.verbose(
|
||||||
`Converted ${imageId} from ${sourceMime.mime} to ${
|
`Converted ${imageId} from ${sourceFileType.identifier} to ${
|
||||||
targetMime.mime
|
targetFileType.identifier
|
||||||
} in ${Date.now() - startTime}ms`,
|
} in ${Date.now() - startTime}ms`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -146,12 +149,12 @@ export class ImageManagerService {
|
|||||||
return await this.imageFilesService.addDerivative(
|
return await this.imageFilesService.addDerivative(
|
||||||
imageId,
|
imageId,
|
||||||
converted_key,
|
converted_key,
|
||||||
convertResult.mime,
|
convertResult.filetype,
|
||||||
convertResult.image,
|
convertResult.image,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const derivative = new EImageDerivativeBackend();
|
const derivative = new EImageDerivativeBackend();
|
||||||
derivative.mime = convertResult.mime;
|
derivative.filetype = convertResult.filetype;
|
||||||
derivative.data = convertResult.image;
|
derivative.data = convertResult.image;
|
||||||
derivative.image_id = imageId;
|
derivative.image_id = imageId;
|
||||||
derivative.key = converted_key;
|
derivative.key = converted_key;
|
||||||
@@ -164,52 +167,52 @@ export class ImageManagerService {
|
|||||||
// File getters ==============================================================
|
// File getters ==============================================================
|
||||||
|
|
||||||
public async getMaster(imageId: string): AsyncFailable<EImageFileBackend> {
|
public async getMaster(imageId: string): AsyncFailable<EImageFileBackend> {
|
||||||
return this.imageFilesService.getFile(imageId, ImageFileType.MASTER);
|
return this.imageFilesService.getFile(imageId, ImageEntryVariant.MASTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMasterMime(imageId: string): AsyncFailable<FullMime> {
|
public async getMasterFileType(imageId: string): AsyncFailable<FileType> {
|
||||||
const mime = await this.imageFilesService.getFileMimes(imageId);
|
const mime = await this.imageFilesService.getFileTypes(imageId);
|
||||||
if (HasFailed(mime)) return mime;
|
if (HasFailed(mime)) return mime;
|
||||||
|
|
||||||
if (mime.master === undefined) return Fail(FT.NotFound, 'No master file');
|
if (mime['master'] === undefined) return Fail(FT.NotFound, 'No master file');
|
||||||
|
|
||||||
return ParseMime(mime.master);
|
return ParseFileType(mime['master']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getOriginal(imageId: string): AsyncFailable<EImageFileBackend> {
|
public async getOriginal(imageId: string): AsyncFailable<EImageFileBackend> {
|
||||||
return this.imageFilesService.getFile(imageId, ImageFileType.ORIGINAL);
|
return this.imageFilesService.getFile(imageId, ImageEntryVariant.ORIGINAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getOriginalMime(imageId: string): AsyncFailable<FullMime> {
|
public async getOriginalFileType(imageId: string): AsyncFailable<FileType> {
|
||||||
const mime = await this.imageFilesService.getFileMimes(imageId);
|
const filetypes = await this.imageFilesService.getFileTypes(imageId);
|
||||||
if (HasFailed(mime)) return mime;
|
if (HasFailed(filetypes)) return filetypes;
|
||||||
|
|
||||||
if (mime.original === undefined)
|
if (filetypes['original'] === undefined)
|
||||||
return Fail(FT.NotFound, 'No original file');
|
return Fail(FT.NotFound, 'No original file');
|
||||||
|
|
||||||
return ParseMime(mime.original);
|
return ParseFileType(filetypes['original']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFileMimes(imageId: string): AsyncFailable<{
|
public async getFileMimes(imageId: string): AsyncFailable<{
|
||||||
[ImageFileType.MASTER]: string;
|
[ImageEntryVariant.MASTER]: string;
|
||||||
[ImageFileType.ORIGINAL]: string | undefined;
|
[ImageEntryVariant.ORIGINAL]: string | undefined;
|
||||||
}> {
|
}> {
|
||||||
const result = await this.imageFilesService.getFileMimes(imageId);
|
const result = await this.imageFilesService.getFileTypes(imageId);
|
||||||
if (HasFailed(result)) return result;
|
if (HasFailed(result)) return result;
|
||||||
|
|
||||||
if (result[ImageFileType.MASTER] === undefined) {
|
if (result[ImageEntryVariant.MASTER] === undefined) {
|
||||||
return Fail(FT.NotFound, 'No master file found');
|
return Fail(FT.NotFound, 'No master file found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[ImageFileType.MASTER]: result[ImageFileType.MASTER]!,
|
[ImageEntryVariant.MASTER]: result[ImageEntryVariant.MASTER]!,
|
||||||
[ImageFileType.ORIGINAL]: result[ImageFileType.ORIGINAL],
|
[ImageEntryVariant.ORIGINAL]: result[ImageEntryVariant.ORIGINAL],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Util stuff ==================================================================
|
// Util stuff ==================================================================
|
||||||
|
|
||||||
private async getFullMimeFromBuffer(image: Buffer): AsyncFailable<FullMime> {
|
private async getFileTypeFromBuffer(image: Buffer): AsyncFailable<FileType> {
|
||||||
const filetypeResult: FileTypeResult | undefined = await fileTypeFromBuffer(
|
const filetypeResult: FileTypeResult | undefined = await fileTypeFromBuffer(
|
||||||
image,
|
image,
|
||||||
);
|
);
|
||||||
@@ -221,8 +224,7 @@ export class ImageManagerService {
|
|||||||
mime = filetypeResult.mime;
|
mime = filetypeResult.mime;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullMime = ParseMime(mime ?? 'other/unknown');
|
return ParseMime2FileType(mime ?? 'other/unknown');
|
||||||
return fullMime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getConvertHash(options: object) {
|
private getConvertHash(options: object) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface ImageResult {
|
export interface ImageResult {
|
||||||
image: Buffer;
|
image: Buffer;
|
||||||
mime: string;
|
filetype: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import { BMPdecode } from 'bmp-img';
|
|
||||||
import { FullMime, ImageMime } from 'picsur-shared/dist/dto/mimes.dto';
|
|
||||||
import { QOIdecode } from 'qoi-img';
|
|
||||||
import sharp, { Sharp, SharpOptions } from 'sharp';
|
|
||||||
|
|
||||||
export function UniversalSharp(
|
|
||||||
image: Buffer,
|
|
||||||
mime: FullMime,
|
|
||||||
options?: SharpOptions,
|
|
||||||
): Sharp {
|
|
||||||
// if (mime.mime === ImageMime.ICO) {
|
|
||||||
// return icoSharp(image, options);
|
|
||||||
// } else
|
|
||||||
if (mime.mime === ImageMime.BMP) {
|
|
||||||
return bmpSharp(image, options);
|
|
||||||
} else if (mime.mime === ImageMime.QOI) {
|
|
||||||
return qoiSharp(image, options);
|
|
||||||
} else {
|
|
||||||
return sharp(image, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bmpSharp(image: Buffer, options?: SharpOptions) {
|
|
||||||
const bitmap = BMPdecode(image);
|
|
||||||
return sharp(bitmap.pixels, {
|
|
||||||
...options,
|
|
||||||
raw: {
|
|
||||||
width: bitmap.width,
|
|
||||||
height: bitmap.height,
|
|
||||||
channels: bitmap.channels,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// function icoSharp(image: Buffer, options?: SharpOptions) {
|
|
||||||
// const result = decodeico(image);
|
|
||||||
// // Get biggest image
|
|
||||||
// const best = result.sort((a, b) => b.width - a.width)[0];
|
|
||||||
|
|
||||||
// return sharp(best.data, {
|
|
||||||
// ...options,
|
|
||||||
// raw: {
|
|
||||||
// width: best.width,
|
|
||||||
// height: best.height,
|
|
||||||
// channels: 4,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
function qoiSharp(image: Buffer, options?: SharpOptions) {
|
|
||||||
const result = QOIdecode(image);
|
|
||||||
|
|
||||||
return sharp(result.pixels, {
|
|
||||||
...options,
|
|
||||||
raw: {
|
|
||||||
width: result.width,
|
|
||||||
height: result.height,
|
|
||||||
channels: result.channels,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
interface NormalImage {
|
interface NormalImage {
|
||||||
type: 'normal';
|
variant: 'normal';
|
||||||
id: string;
|
id: string;
|
||||||
ext: string;
|
ext: string;
|
||||||
mime: string;
|
filetype: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OriginalImage {
|
interface OriginalImage {
|
||||||
type: 'original';
|
variant: 'original';
|
||||||
id: string;
|
id: string;
|
||||||
ext: null;
|
ext: null;
|
||||||
mime: null;
|
filetype: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ImageFullId = NormalImage | OriginalImage;
|
export type ImageFullId = NormalImage | OriginalImage;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class EImageDerivativeBackend {
|
|||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
mime: string;
|
filetype: string;
|
||||||
|
|
||||||
@Column({ name: 'last_read', nullable: false })
|
@Column({ name: 'last_read', nullable: false })
|
||||||
last_read: Date;
|
last_read: Date;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ImageFileType } from 'picsur-shared/dist/dto/image-file-types.enum';
|
import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum';
|
||||||
import { Column, Entity, Index, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
import { Column, Entity, Index, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Unique(['image_id', 'type'])
|
@Unique(['image_id', 'variant'])
|
||||||
export class EImageFileBackend {
|
export class EImageFileBackend {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
private _id?: string;
|
private _id?: string;
|
||||||
@@ -12,11 +12,11 @@ export class EImageFileBackend {
|
|||||||
image_id: string;
|
image_id: string;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column({ nullable: false, enum: ImageFileType })
|
@Column({ nullable: false, enum: ImageEntryVariant })
|
||||||
type: ImageFileType;
|
variant: ImageEntryVariant;
|
||||||
|
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
mime: string;
|
filetype: string;
|
||||||
|
|
||||||
// Binary data
|
// Binary data
|
||||||
@Column({ type: 'bytea', nullable: false })
|
@Column({ type: 'bytea', nullable: false })
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import {
|
|||||||
AllPermissionsResponse,
|
AllPermissionsResponse,
|
||||||
InfoResponse
|
InfoResponse
|
||||||
} from 'picsur-shared/dist/dto/api/info.dto';
|
} from 'picsur-shared/dist/dto/api/info.dto';
|
||||||
import {
|
import { FileType2Ext, FileType2Mime, SupportedAnimFileTypes, SupportedImageFileTypes } from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
AnimMime2ExtMap,
|
|
||||||
ImageMime2ExtMap
|
|
||||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
|
||||||
import { HostConfigService } from '../../../config/early/host.config.service';
|
import { HostConfigService } from '../../../config/early/host.config.service';
|
||||||
import { NoPermissions } from '../../../decorators/permissions.decorator';
|
import { NoPermissions } from '../../../decorators/permissions.decorator';
|
||||||
import { Returns } from '../../../decorators/returns.decorator';
|
import { Returns } from '../../../decorators/returns.decorator';
|
||||||
@@ -42,8 +39,18 @@ export class InfoController {
|
|||||||
@Returns(AllFormatsResponse)
|
@Returns(AllFormatsResponse)
|
||||||
async getFormats(): Promise<AllFormatsResponse> {
|
async getFormats(): Promise<AllFormatsResponse> {
|
||||||
return {
|
return {
|
||||||
image: ImageMime2ExtMap,
|
image: Object.fromEntries(
|
||||||
anim: AnimMime2ExtMap,
|
SupportedImageFileTypes.map((filetype) => [
|
||||||
|
FileType2Mime(filetype),
|
||||||
|
FileType2Ext(filetype),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
anim: Object.fromEntries(
|
||||||
|
SupportedAnimFileTypes.map((filetype) => [
|
||||||
|
FileType2Mime(filetype),
|
||||||
|
FileType2Ext(filetype),
|
||||||
|
]),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import {
|
import { Controller, Get, Head, Logger, Query, Res } from '@nestjs/common';
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
Head, Logger, Query,
|
|
||||||
Res
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import type { FastifyReply } from 'fastify';
|
import type { FastifyReply } from 'fastify';
|
||||||
import {
|
import {
|
||||||
ImageMetaResponse,
|
ImageMetaResponse,
|
||||||
ImageRequestParams
|
ImageRequestParams
|
||||||
} from 'picsur-shared/dist/dto/api/image.dto';
|
} from 'picsur-shared/dist/dto/api/image.dto';
|
||||||
|
import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum';
|
||||||
|
import { FileType2Mime } from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
import { ThrowIfFailed } from 'picsur-shared/dist/types';
|
import { ThrowIfFailed } from 'picsur-shared/dist/types';
|
||||||
import { UsersService } from '../../collections/user-db/user-db.service';
|
import { UsersService } from '../../collections/user-db/user-db.service';
|
||||||
import { ImageFullIdParam } from '../../decorators/image-id/image-full-id.decorator';
|
import { ImageFullIdParam } from '../../decorators/image-id/image-full-id.decorator';
|
||||||
@@ -36,16 +33,16 @@ export class ImageController {
|
|||||||
@Res({ passthrough: true }) res: FastifyReply,
|
@Res({ passthrough: true }) res: FastifyReply,
|
||||||
@ImageFullIdParam() fullid: ImageFullId,
|
@ImageFullIdParam() fullid: ImageFullId,
|
||||||
) {
|
) {
|
||||||
if (fullid.type === 'original') {
|
if (fullid.variant === ImageEntryVariant.ORIGINAL) {
|
||||||
const fullmime = ThrowIfFailed(
|
const filetype = ThrowIfFailed(
|
||||||
await this.imagesService.getOriginalMime(fullid.id),
|
await this.imagesService.getOriginalFileType(fullid.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
res.type(fullmime.mime);
|
res.type(ThrowIfFailed(FileType2Mime(filetype.identifier)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.type(fullid.mime);
|
res.type(ThrowIfFailed(FileType2Mime(fullid.filetype)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@@ -56,20 +53,20 @@ export class ImageController {
|
|||||||
@ImageFullIdParam() fullid: ImageFullId,
|
@ImageFullIdParam() fullid: ImageFullId,
|
||||||
@Query() params: ImageRequestParams,
|
@Query() params: ImageRequestParams,
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
if (fullid.type === 'original') {
|
if (fullid.variant === ImageEntryVariant.ORIGINAL) {
|
||||||
const image = ThrowIfFailed(
|
const image = ThrowIfFailed(
|
||||||
await this.imagesService.getOriginal(fullid.id),
|
await this.imagesService.getOriginal(fullid.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
res.type(image.mime);
|
res.type(ThrowIfFailed(FileType2Mime(image.filetype)));
|
||||||
return image.data;
|
return image.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = ThrowIfFailed(
|
const image = ThrowIfFailed(
|
||||||
await this.imagesService.getConverted(fullid.id, fullid.mime, params),
|
await this.imagesService.getConverted(fullid.id, fullid.filetype, params),
|
||||||
);
|
);
|
||||||
|
|
||||||
res.type(image.mime);
|
res.type(ThrowIfFailed(FileType2Mime(image.filetype)));
|
||||||
return image.data;
|
return image.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,11 +78,11 @@ export class ImageController {
|
|||||||
const [fileMimesRes, imageUserRes] = await Promise.all([
|
const [fileMimesRes, imageUserRes] = await Promise.all([
|
||||||
this.imagesService.getFileMimes(id),
|
this.imagesService.getFileMimes(id),
|
||||||
this.userService.findOne(image.user_id),
|
this.userService.findOne(image.user_id),
|
||||||
])
|
]);
|
||||||
|
|
||||||
const fileMimes = ThrowIfFailed(fileMimesRes);
|
const fileTypes = ThrowIfFailed(fileMimesRes);
|
||||||
const imageUser = ThrowIfFailed(imageUserRes);
|
const imageUser = ThrowIfFailed(imageUserRes);
|
||||||
|
|
||||||
return { image, user: EUserBackend2EUser(imageUser), fileMimes };
|
return { image, user: EUserBackend2EUser(imageUser), fileTypes };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Logger } from '@nestjs/common';
|
|||||||
import { ChildProcess, fork } from 'child_process';
|
import { ChildProcess, fork } from 'child_process';
|
||||||
import pTimeout from 'p-timeout';
|
import pTimeout from 'p-timeout';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { FullMime } from 'picsur-shared/dist/dto/mimes.dto';
|
import { FileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
import {
|
import {
|
||||||
AsyncFailable,
|
AsyncFailable,
|
||||||
Fail,
|
Fail,
|
||||||
@@ -41,7 +41,7 @@ export class SharpWrapper {
|
|||||||
private readonly memory_limit: number,
|
private readonly memory_limit: number,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async start(image: Buffer, mime: FullMime): AsyncFailable<true> {
|
public async start(image: Buffer, filetype: FileType): AsyncFailable<true> {
|
||||||
this.worker = fork(SharpWrapper.WORKER_PATH, {
|
this.worker = fork(SharpWrapper.WORKER_PATH, {
|
||||||
serialization: 'advanced',
|
serialization: 'advanced',
|
||||||
timeout: this.instance_timeout,
|
timeout: this.instance_timeout,
|
||||||
@@ -79,7 +79,7 @@ export class SharpWrapper {
|
|||||||
const hasSent = this.sendToWorker({
|
const hasSent = this.sendToWorker({
|
||||||
type: 'init',
|
type: 'init',
|
||||||
image,
|
image,
|
||||||
mime,
|
filetype,
|
||||||
});
|
});
|
||||||
if (HasFailed(hasSent)) {
|
if (HasFailed(hasSent)) {
|
||||||
this.purge();
|
this.purge();
|
||||||
@@ -117,7 +117,7 @@ export class SharpWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async finish(
|
public async finish(
|
||||||
targetMime: FullMime,
|
targetFiletype: FileType,
|
||||||
options?: SharpWorkerFinishOptions,
|
options?: SharpWorkerFinishOptions,
|
||||||
): AsyncFailable<SharpResult> {
|
): AsyncFailable<SharpResult> {
|
||||||
if (!this.worker) {
|
if (!this.worker) {
|
||||||
@@ -126,7 +126,7 @@ export class SharpWrapper {
|
|||||||
|
|
||||||
const hasSent = this.sendToWorker({
|
const hasSent = this.sendToWorker({
|
||||||
type: 'finish',
|
type: 'finish',
|
||||||
mime: targetMime,
|
filetype: targetFiletype,
|
||||||
options: options ?? {},
|
options: options ?? {},
|
||||||
});
|
});
|
||||||
if (HasFailed(hasSent)) {
|
if (HasFailed(hasSent)) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FullMime } from 'picsur-shared/dist/dto/mimes.dto';
|
import { FileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
import { Sharp } from 'sharp';
|
import { Sharp } from 'sharp';
|
||||||
import { SharpResult } from './universal-sharp';
|
import { SharpResult } from './universal-sharp';
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export interface SharpWorkerFinishOptions {
|
|||||||
export interface SharpWorkerInitMessage {
|
export interface SharpWorkerInitMessage {
|
||||||
type: 'init';
|
type: 'init';
|
||||||
image: Buffer;
|
image: Buffer;
|
||||||
mime: FullMime;
|
filetype: FileType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharpWorkerOperationMessage {
|
export interface SharpWorkerOperationMessage {
|
||||||
@@ -43,7 +43,7 @@ export interface SharpWorkerOperationMessage {
|
|||||||
|
|
||||||
export interface SharpWorkerFinishMessage {
|
export interface SharpWorkerFinishMessage {
|
||||||
type: 'finish';
|
type: 'finish';
|
||||||
mime: FullMime;
|
filetype: FileType;
|
||||||
options: SharpWorkerFinishOptions;
|
options: SharpWorkerFinishOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FullMime } from 'picsur-shared/dist/dto/mimes.dto';
|
import { FileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
import posix from 'posix.js';
|
import posix from 'posix.js';
|
||||||
import { Sharp } from 'sharp';
|
import { Sharp } from 'sharp';
|
||||||
import {
|
import {
|
||||||
@@ -47,7 +47,7 @@ export class SharpWorker {
|
|||||||
} else if (message.type === 'operation') {
|
} else if (message.type === 'operation') {
|
||||||
this.operation(message);
|
this.operation(message);
|
||||||
} else if (message.type === 'finish') {
|
} else if (message.type === 'finish') {
|
||||||
this.finish(message.mime, message.options);
|
this.finish(message.filetype, message.options);
|
||||||
} else {
|
} else {
|
||||||
return this.purge('Unknown message type');
|
return this.purge('Unknown message type');
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ export class SharpWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.startTime = Date.now();
|
this.startTime = Date.now();
|
||||||
this.sharpi = UniversalSharpIn(message.image, message.mime);
|
this.sharpi = UniversalSharpIn(message.image, message.filetype);
|
||||||
}
|
}
|
||||||
|
|
||||||
private operation(message: SharpWorkerOperationMessage): void {
|
private operation(message: SharpWorkerOperationMessage): void {
|
||||||
@@ -74,7 +74,7 @@ export class SharpWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async finish(
|
private async finish(
|
||||||
mime: FullMime,
|
filetype: FileType,
|
||||||
options: SharpWorkerFinishOptions,
|
options: SharpWorkerFinishOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (this.sharpi === null) {
|
if (this.sharpi === null) {
|
||||||
@@ -85,7 +85,7 @@ export class SharpWorker {
|
|||||||
this.sharpi = null;
|
this.sharpi = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await UniversalSharpOut(sharpi, mime, options);
|
const result = await UniversalSharpOut(sharpi, filetype, options);
|
||||||
const processingTime = Date.now() - this.startTime;
|
const processingTime = Date.now() - this.startTime;
|
||||||
|
|
||||||
this.sendMessage({
|
this.sendMessage({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BMPdecode, BMPencode } from 'bmp-img';
|
import { BMPdecode, BMPencode } from 'bmp-img';
|
||||||
import { FullMime, ImageMime } from 'picsur-shared/dist/dto/mimes.dto';
|
import { FileType, ImageFileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
import { QOIdecode, QOIencode } from 'qoi-img';
|
import { QOIdecode, QOIencode } from 'qoi-img';
|
||||||
import sharp, { Sharp, SharpOptions } from 'sharp';
|
import sharp, { Sharp, SharpOptions } from 'sharp';
|
||||||
|
|
||||||
@@ -10,16 +10,21 @@ export interface SharpResult {
|
|||||||
|
|
||||||
export function UniversalSharpIn(
|
export function UniversalSharpIn(
|
||||||
image: Buffer,
|
image: Buffer,
|
||||||
mime: FullMime,
|
filetype: FileType,
|
||||||
options?: SharpOptions,
|
options?: SharpOptions,
|
||||||
): Sharp {
|
): Sharp {
|
||||||
// if (mime.mime === ImageMime.ICO) {
|
// if (mime.mime === ImageFileType.ICO) {
|
||||||
// return icoSharpIn(image, options);
|
// return icoSharpIn(image, options);
|
||||||
// } else
|
// } else
|
||||||
if (mime.mime === ImageMime.BMP) {
|
if (filetype.identifier === ImageFileType.BMP) {
|
||||||
return bmpSharpIn(image, options);
|
return bmpSharpIn(image, options);
|
||||||
} else if (mime.mime === ImageMime.QOI) {
|
} else if (filetype.identifier === ImageFileType.QOI) {
|
||||||
return qoiSharpIn(image, options);
|
return qoiSharpIn(image, options);
|
||||||
|
// } else if (filetype.identifier === AnimFileType.GIF) {
|
||||||
|
// return sharp(image, {
|
||||||
|
// ...options,
|
||||||
|
// animated: true,
|
||||||
|
// });
|
||||||
} else {
|
} else {
|
||||||
return sharp(image, options);
|
return sharp(image, options);
|
||||||
}
|
}
|
||||||
@@ -67,40 +72,43 @@ function qoiSharpIn(image: Buffer, options?: SharpOptions) {
|
|||||||
|
|
||||||
export async function UniversalSharpOut(
|
export async function UniversalSharpOut(
|
||||||
image: Sharp,
|
image: Sharp,
|
||||||
mime: FullMime,
|
filetype: FileType,
|
||||||
options?: {
|
options?: {
|
||||||
quality?: number;
|
quality?: number;
|
||||||
},
|
},
|
||||||
): Promise<SharpResult> {
|
): Promise<SharpResult> {
|
||||||
let result: SharpResult | undefined;
|
let result: SharpResult | undefined;
|
||||||
|
|
||||||
switch (mime.mime) {
|
switch (filetype.identifier) {
|
||||||
case ImageMime.PNG:
|
case ImageFileType.PNG:
|
||||||
result = await image
|
result = await image
|
||||||
.png({ quality: options?.quality })
|
.png({ quality: options?.quality })
|
||||||
.toBuffer({ resolveWithObject: true });
|
.toBuffer({ resolveWithObject: true });
|
||||||
break;
|
break;
|
||||||
case ImageMime.JPEG:
|
case ImageFileType.JPEG:
|
||||||
result = await image
|
result = await image
|
||||||
.jpeg({ quality: options?.quality })
|
.jpeg({ quality: options?.quality })
|
||||||
.toBuffer({ resolveWithObject: true });
|
.toBuffer({ resolveWithObject: true });
|
||||||
break;
|
break;
|
||||||
case ImageMime.TIFF:
|
case ImageFileType.TIFF:
|
||||||
result = await image
|
result = await image
|
||||||
.tiff({ quality: options?.quality })
|
.tiff({ quality: options?.quality })
|
||||||
.toBuffer({ resolveWithObject: true });
|
.toBuffer({ resolveWithObject: true });
|
||||||
break;
|
break;
|
||||||
case ImageMime.WEBP:
|
case ImageFileType.WEBP:
|
||||||
result = await image
|
result = await image
|
||||||
.webp({ quality: options?.quality })
|
.webp({ quality: options?.quality })
|
||||||
.toBuffer({ resolveWithObject: true });
|
.toBuffer({ resolveWithObject: true });
|
||||||
break;
|
break;
|
||||||
case ImageMime.BMP:
|
case ImageFileType.BMP:
|
||||||
result = await bmpSharpOut(image);
|
result = await bmpSharpOut(image);
|
||||||
break;
|
break;
|
||||||
case ImageMime.QOI:
|
case ImageFileType.QOI:
|
||||||
result = await qoiSharpOut(image);
|
result = await qoiSharpOut(image);
|
||||||
break;
|
break;
|
||||||
|
// case AnimFileType.GIF:
|
||||||
|
// result = await image.gif().toBuffer({ resolveWithObject: true });
|
||||||
|
// break;
|
||||||
default:
|
default:
|
||||||
throw new Error('Unsupported mime type');
|
throw new Error('Unsupported mime type');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import {
|
|||||||
SimpleChanges,
|
SimpleChanges,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FullMime, ImageMime } from 'picsur-shared/dist/dto/mimes.dto';
|
import { FileType, ImageFileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||||
import { URLRegex } from 'picsur-shared/dist/util/common-regex';
|
import { URLRegex } from 'picsur-shared/dist/util/common-regex';
|
||||||
import { ParseMime } from 'picsur-shared/dist/util/parse-mime';
|
import { ParseMime2FileType } from 'picsur-shared/dist/util/parse-mime';
|
||||||
import { ApiService } from 'src/app/services/api/api.service';
|
import { ApiService } from 'src/app/services/api/api.service';
|
||||||
import { Logger } from 'src/app/services/logger/logger.service';
|
import { Logger } from 'src/app/services/logger/logger.service';
|
||||||
import { QoiWorkerService } from 'src/app/workers/qoi-worker.service';
|
import { QoiWorkerService } from 'src/app/workers/qoi-worker.service';
|
||||||
@@ -69,10 +69,10 @@ export class PicsurImgComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async update(url: string): AsyncFailable<void> {
|
private async update(url: string): AsyncFailable<void> {
|
||||||
const mime = await this.getMime(url);
|
const filetype = await this.getFileType(url);
|
||||||
if (HasFailed(mime)) return mime;
|
if (HasFailed(filetype)) return filetype;
|
||||||
|
|
||||||
if (mime.mime === ImageMime.QOI) {
|
if (filetype.identifier === ImageFileType.QOI) {
|
||||||
const result = await this.qoiWorker.decode(url);
|
const result = await this.qoiWorker.decode(url);
|
||||||
if (HasFailed(result)) return result;
|
if (HasFailed(result)) return result;
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ export class PicsurImgComponent implements OnChanges {
|
|||||||
this.changeDetector.markForCheck();
|
this.changeDetector.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getMime(url: string): AsyncFailable<FullMime> {
|
private async getFileType(url: string): AsyncFailable<FileType> {
|
||||||
const response = await this.apiService.head(url);
|
const response = await this.apiService.head(url);
|
||||||
if (HasFailed(response)) {
|
if (HasFailed(response)) {
|
||||||
return response;
|
return response;
|
||||||
@@ -97,8 +97,7 @@ export class PicsurImgComponent implements OnChanges {
|
|||||||
const mimeHeader = response.get('content-type') ?? '';
|
const mimeHeader = response.get('content-type') ?? '';
|
||||||
const mime = mimeHeader.split(';')[0];
|
const mime = mimeHeader.split(';')[0];
|
||||||
|
|
||||||
const fullMime = ParseMime(mime);
|
return ParseMime2FileType(mime);
|
||||||
return fullMime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onInview(e: any) {
|
onInview(e: any) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
|
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
|
||||||
import { ImageMime } from 'picsur-shared/dist/dto/mimes.dto';
|
import { ImageFileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
||||||
import { HasFailed } from 'picsur-shared/dist/types/failable';
|
import { HasFailed } from 'picsur-shared/dist/types/failable';
|
||||||
import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto';
|
import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto';
|
||||||
@@ -71,7 +71,7 @@ export class ImagesComponent implements OnInit {
|
|||||||
|
|
||||||
getThumbnailUrl(image: EImage) {
|
getThumbnailUrl(image: EImage) {
|
||||||
return (
|
return (
|
||||||
this.imageService.GetImageURL(image.id, ImageMime.QOI) + '?height=480'
|
this.imageService.GetImageURL(image.id, ImageFileType.QOI) + '?height=480'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,20 @@ import { Component, OnInit } from '@angular/core';
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ImageLinks } from 'picsur-shared/dist/dto/image-links.class';
|
import { ImageLinks } from 'picsur-shared/dist/dto/image-links.class';
|
||||||
import {
|
import {
|
||||||
AnimMime,
|
AnimFileType,
|
||||||
FullMime,
|
FileType,
|
||||||
ImageMime,
|
FileType2Ext,
|
||||||
Mime2Ext,
|
ImageFileType,
|
||||||
SupportedAnimMimes,
|
SupportedAnimFileTypes,
|
||||||
SupportedImageMimes,
|
SupportedFileTypeCategory,
|
||||||
SupportedMimeCategory
|
SupportedImageFileTypes
|
||||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
|
|
||||||
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
||||||
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
||||||
import { HasFailed, HasSuccess } from 'picsur-shared/dist/types';
|
import { HasFailed, HasSuccess } from 'picsur-shared/dist/types';
|
||||||
import { UUIDRegex } from 'picsur-shared/dist/util/common-regex';
|
import { UUIDRegex } from 'picsur-shared/dist/util/common-regex';
|
||||||
import { ParseMime } from 'picsur-shared/dist/util/parse-mime';
|
import { ParseFileType } from 'picsur-shared/dist/util/parse-mime';
|
||||||
import { ImageService } from 'src/app/services/api/image.service';
|
import { ImageService } from 'src/app/services/api/image.service';
|
||||||
import { UtilService } from 'src/app/util/util-module/util.service';
|
import { UtilService } from 'src/app/util/util-module/util.service';
|
||||||
import {
|
import {
|
||||||
@@ -36,18 +37,18 @@ export class ViewComponent implements OnInit {
|
|||||||
|
|
||||||
private id: string;
|
private id: string;
|
||||||
private hasOriginal: boolean = false;
|
private hasOriginal: boolean = false;
|
||||||
private masterMime: FullMime = {
|
private masterFileType: FileType = {
|
||||||
mime: ImageMime.JPEG,
|
identifier: ImageFileType.JPEG,
|
||||||
type: SupportedMimeCategory.Image,
|
category: SupportedFileTypeCategory.Image,
|
||||||
};
|
};
|
||||||
private currentSelectedFormat: string = ImageMime.JPEG;
|
private currentSelectedFormat: string = ImageFileType.JPEG;
|
||||||
|
|
||||||
public formatOptions: {
|
public formatOptions: {
|
||||||
value: string;
|
value: string;
|
||||||
key: string;
|
key: string;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
public setSelectedFormat: string = ImageMime.JPEG;
|
public setSelectedFormat: string = ImageFileType.JPEG;
|
||||||
|
|
||||||
public previewLink = '';
|
public previewLink = '';
|
||||||
public imageLinks = new ImageLinks();
|
public imageLinks = new ImageLinks();
|
||||||
@@ -69,25 +70,27 @@ export class ViewComponent implements OnInit {
|
|||||||
|
|
||||||
this.previewLink = this.imageService.GetImageURL(
|
this.previewLink = this.imageService.GetImageURL(
|
||||||
this.id,
|
this.id,
|
||||||
metadata.fileMimes.master,
|
metadata.fileTypes.master,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.hasOriginal = metadata.fileMimes.original !== undefined;
|
this.hasOriginal = metadata.fileTypes.original !== undefined;
|
||||||
|
|
||||||
this.imageUser = metadata.user;
|
this.imageUser = metadata.user;
|
||||||
this.image = metadata.image;
|
this.image = metadata.image;
|
||||||
|
|
||||||
const masterMime = ParseMime(metadata.fileMimes.master);
|
const masterFiletype = ParseFileType(metadata.fileTypes.master);
|
||||||
if (HasSuccess(masterMime)) {
|
if (HasSuccess(masterFiletype)) {
|
||||||
this.masterMime = masterMime;
|
this.masterFileType = masterFiletype;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.masterMime.type === SupportedMimeCategory.Image) {
|
if (this.masterFileType.category === SupportedFileTypeCategory.Image) {
|
||||||
this.setSelectedFormat = ImageMime.JPEG;
|
this.setSelectedFormat = ImageFileType.JPEG;
|
||||||
} else if (this.masterMime.type === SupportedMimeCategory.Animation) {
|
} else if (
|
||||||
this.setSelectedFormat = AnimMime.GIF;
|
this.masterFileType.category === SupportedFileTypeCategory.Animation
|
||||||
|
) {
|
||||||
|
this.setSelectedFormat = AnimFileType.GIF;
|
||||||
} else {
|
} else {
|
||||||
this.setSelectedFormat = metadata.fileMimes.master;
|
this.setSelectedFormat = metadata.fileTypes.master;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedFormat(this.setSelectedFormat);
|
this.selectedFormat(this.setSelectedFormat);
|
||||||
@@ -122,7 +125,7 @@ export class ViewComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (options.selectedFormat === 'original') {
|
if (options.selectedFormat === 'original') {
|
||||||
options.selectedFormat = this.masterMime.mime;
|
options.selectedFormat = this.masterFileType.identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.utilService.showCustomDialog(CustomizeDialogComponent, options, {
|
await this.utilService.showCustomDialog(CustomizeDialogComponent, options, {
|
||||||
@@ -157,19 +160,29 @@ export class ViewComponent implements OnInit {
|
|||||||
key: string;
|
key: string;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
if (this.masterMime.type === SupportedMimeCategory.Image) {
|
if (this.masterFileType.category === SupportedFileTypeCategory.Image) {
|
||||||
newOptions.push(
|
newOptions.push(
|
||||||
...SupportedImageMimes.map((mime) => ({
|
...SupportedImageFileTypes.map((mime) => {
|
||||||
value: Mime2Ext(mime)?.toUpperCase() ?? 'Error',
|
let ext = FileType2Ext(mime);
|
||||||
|
if (HasFailed(ext)) ext = 'Error';
|
||||||
|
return {
|
||||||
|
value: ext.toUpperCase(),
|
||||||
key: mime,
|
key: mime,
|
||||||
})),
|
};
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
} else if (this.masterMime.type === SupportedMimeCategory.Animation) {
|
} else if (
|
||||||
|
this.masterFileType.category === SupportedFileTypeCategory.Animation
|
||||||
|
) {
|
||||||
newOptions.push(
|
newOptions.push(
|
||||||
...SupportedAnimMimes.map((mime) => ({
|
...SupportedAnimFileTypes.map((mime) => {
|
||||||
value: Mime2Ext(mime)?.toUpperCase() ?? 'Error',
|
let ext = FileType2Ext(mime);
|
||||||
|
if (HasFailed(ext)) ext = 'Error';
|
||||||
|
return {
|
||||||
|
value: ext.toUpperCase(),
|
||||||
key: mime,
|
key: mime,
|
||||||
})),
|
};
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { WINDOW } from '@ng-web-apis/common';
|
import { WINDOW } from '@ng-web-apis/common';
|
||||||
import { ApiResponseSchema } from 'picsur-shared/dist/dto/api/api.dto';
|
import { ApiResponseSchema } from 'picsur-shared/dist/dto/api/api.dto';
|
||||||
import { Mime2Ext } from 'picsur-shared/dist/dto/mimes.dto';
|
import { FileType2Ext } from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
import {
|
||||||
|
AsyncFailable,
|
||||||
|
Fail,
|
||||||
|
FT,
|
||||||
|
HasFailed,
|
||||||
|
HasSuccess
|
||||||
|
} from 'picsur-shared/dist/types';
|
||||||
import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
|
import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
|
||||||
|
import { ParseMime2FileType } from 'picsur-shared/dist/util/parse-mime';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { ApiBuffer } from 'src/app/models/dto/api-buffer.dto';
|
import { ApiBuffer } from 'src/app/models/dto/api-buffer.dto';
|
||||||
import { ApiError } from 'src/app/models/dto/api-error.dto';
|
import { ApiError } from 'src/app/models/dto/api-error.dto';
|
||||||
@@ -142,9 +149,14 @@ export class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mimeTypeExt = Mime2Ext(mimeType);
|
const filetype = ParseMime2FileType(mimeType);
|
||||||
if (mimeTypeExt !== undefined && !name.endsWith(mimeTypeExt)) {
|
if (HasSuccess(filetype)) {
|
||||||
name += '.' + mimeTypeExt;
|
const ext = FileType2Ext(filetype.identifier);
|
||||||
|
if (HasSuccess(ext)) {
|
||||||
|
if (name.endsWith(ext)) {
|
||||||
|
name += '.' + ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import {
|
|||||||
ImageRequestParams
|
ImageRequestParams
|
||||||
} from 'picsur-shared/dist/dto/api/image.dto';
|
} from 'picsur-shared/dist/dto/api/image.dto';
|
||||||
import { ImageLinks } from 'picsur-shared/dist/dto/image-links.class';
|
import { ImageLinks } from 'picsur-shared/dist/dto/image-links.class';
|
||||||
import { Mime2Ext } from 'picsur-shared/dist/dto/mimes.dto';
|
import { FileType2Ext } from 'picsur-shared/dist/dto/mimes.dto';
|
||||||
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
||||||
import { AsyncFailable } from 'picsur-shared/dist/types';
|
import { AsyncFailable } from 'picsur-shared/dist/types';
|
||||||
import { Fail, FT, HasFailed, Open } from 'picsur-shared/dist/types/failable';
|
import { Fail, FT, HasFailed, HasSuccess, Open } from 'picsur-shared/dist/types/failable';
|
||||||
import { ImageUploadRequest } from '../../models/dto/image-upload-request.dto';
|
import { ImageUploadRequest } from '../../models/dto/image-upload-request.dto';
|
||||||
import { ApiService } from './api.service';
|
import { ApiService } from './api.service';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
@@ -103,19 +103,19 @@ export class ImageService {
|
|||||||
|
|
||||||
// Non api calls
|
// Non api calls
|
||||||
|
|
||||||
public GetImageURL(image: string, mime: string | null): string {
|
public GetImageURL(image: string, filetype: string | null): string {
|
||||||
const baseURL = this.location.protocol + '//' + this.location.host;
|
const baseURL = this.location.protocol + '//' + this.location.host;
|
||||||
const extension = mime !== null ? Mime2Ext(mime) : null;
|
const extension = FileType2Ext(filetype ?? '');
|
||||||
|
|
||||||
return `${baseURL}/i/${image}${extension !== null ? '.' + extension : ''}`;
|
return `${baseURL}/i/${image}${HasSuccess(extension) ? '.' + extension : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetImageURLCustomized(
|
public GetImageURLCustomized(
|
||||||
image: string,
|
image: string,
|
||||||
mime: string | null,
|
filetype: string | null,
|
||||||
options: ImageRequestParams,
|
options: ImageRequestParams,
|
||||||
): string {
|
): string {
|
||||||
const baseURL = this.GetImageURL(image, mime);
|
const baseURL = this.GetImageURL(image, filetype);
|
||||||
const betterOptions = ImageRequestParams.zodSchema.safeParse(options);
|
const betterOptions = ImageRequestParams.zodSchema.safeParse(options);
|
||||||
|
|
||||||
if (!betterOptions.success) return baseURL;
|
if (!betterOptions.success) return baseURL;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|||||||
import { EImageSchema } from '../../entities/image.entity';
|
import { EImageSchema } from '../../entities/image.entity';
|
||||||
import { EUserSchema } from '../../entities/user.entity';
|
import { EUserSchema } from '../../entities/user.entity';
|
||||||
import { createZodDto } from '../../util/create-zod-dto';
|
import { createZodDto } from '../../util/create-zod-dto';
|
||||||
import { ImageFileType } from '../image-file-types.enum';
|
import { ImageEntryVariant } from '../image-entry-variant.enum';
|
||||||
|
|
||||||
const parseBool = (value: unknown): boolean | null => {
|
const parseBool = (value: unknown): boolean | null => {
|
||||||
if (value === true || value === 'true' || value === '1' || value === 'yes')
|
if (value === true || value === 'true' || value === '1' || value === 'yes')
|
||||||
@@ -36,9 +36,9 @@ export class ImageRequestParams extends createZodDto(
|
|||||||
export const ImageMetaResponseSchema = z.object({
|
export const ImageMetaResponseSchema = z.object({
|
||||||
image: EImageSchema,
|
image: EImageSchema,
|
||||||
user: EUserSchema,
|
user: EUserSchema,
|
||||||
fileMimes: z.object({
|
fileTypes: z.object({
|
||||||
[ImageFileType.MASTER]: z.string(),
|
[ImageEntryVariant.MASTER]: z.string(),
|
||||||
[ImageFileType.ORIGINAL]: z.union([z.string(), z.undefined()]),
|
[ImageEntryVariant.ORIGINAL]: z.union([z.string(), z.undefined()]),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
export class ImageMetaResponse extends createZodDto(ImageMetaResponseSchema) {}
|
export class ImageMetaResponse extends createZodDto(ImageMetaResponseSchema) {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export enum ImageFileType {
|
export enum ImageEntryVariant {
|
||||||
ORIGINAL = 'original',
|
ORIGINAL = 'original',
|
||||||
MASTER = 'master',
|
MASTER = 'master',
|
||||||
}
|
}
|
||||||
@@ -1,69 +1,110 @@
|
|||||||
|
import { Fail, Failable, FT } from '../types';
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
export enum ImageMime {
|
export enum ImageFileType {
|
||||||
QOI = 'image/x-qoi',
|
QOI = 'image:qoi',
|
||||||
JPEG = 'image/jpeg',
|
JPEG = 'image:jpeg',
|
||||||
PNG = 'image/png',
|
PNG = 'image:png',
|
||||||
WEBP = 'image/webp',
|
WEBP = 'image:webp',
|
||||||
TIFF = 'image/tiff',
|
TIFF = 'image:tiff',
|
||||||
BMP = 'image/bmp',
|
BMP = 'image:bmp',
|
||||||
// ICO = 'image/x-icon',
|
// ICO = 'image:ico',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AnimMime {
|
export enum AnimFileType {
|
||||||
APNG = 'image/apng',
|
GIF = 'anim:gif',
|
||||||
GIF = 'image/gif',
|
WEBP = 'anim:webp',
|
||||||
|
//APNG = 'anim:apng',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derivatives
|
// Derivatives
|
||||||
|
|
||||||
export const SupportedImageMimes: string[] = Object.values(ImageMime);
|
export const SupportedImageFileTypes: string[] = Object.values(ImageFileType);
|
||||||
export const SupportedAnimMimes: string[] = Object.values(AnimMime);
|
export const SupportedAnimFileTypes: string[] = Object.values(AnimFileType);
|
||||||
export const SupportedMimes: string[] = Object.values({ ...ImageMime, ...AnimMime });
|
export const SupportedFileTypes: string[] = Object.values({
|
||||||
|
...ImageFileType,
|
||||||
|
...AnimFileType,
|
||||||
|
});
|
||||||
|
|
||||||
export enum SupportedMimeCategory {
|
export enum SupportedFileTypeCategory {
|
||||||
Image = 'image',
|
Image = 'image',
|
||||||
Animation = 'anim',
|
Animation = 'anim',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FullMime {
|
export interface FileType {
|
||||||
mime: string;
|
identifier: string;
|
||||||
type: SupportedMimeCategory;
|
category: SupportedFileTypeCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImageMime2ExtMap: {
|
// Converters
|
||||||
[key in ImageMime]: string;
|
|
||||||
|
// -- Ext
|
||||||
|
|
||||||
|
const FileType2ExtMap: {
|
||||||
|
[key in ImageFileType | AnimFileType]: string;
|
||||||
} = {
|
} = {
|
||||||
[ImageMime.QOI]: 'qoi',
|
[AnimFileType.GIF]: 'gif',
|
||||||
[ImageMime.JPEG]: 'jpg',
|
[AnimFileType.WEBP]: 'webp',
|
||||||
[ImageMime.PNG]: 'png',
|
// [AnimFileType.APNG]: 'apng',
|
||||||
[ImageMime.WEBP]: 'webp',
|
[ImageFileType.QOI]: 'qoi',
|
||||||
[ImageMime.TIFF]: 'tiff',
|
[ImageFileType.JPEG]: 'jpg',
|
||||||
[ImageMime.BMP]: 'bmp',
|
[ImageFileType.PNG]: 'png',
|
||||||
// [ImageMime.ICO]: 'ico',
|
[ImageFileType.WEBP]: 'webp',
|
||||||
|
[ImageFileType.TIFF]: 'tiff',
|
||||||
|
[ImageFileType.BMP]: 'bmp',
|
||||||
|
// [ImageFileType.ICO]: 'ico',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnimMime2ExtMap: {
|
const Ext2FileTypeMap: {
|
||||||
[key in AnimMime]: string;
|
|
||||||
} = {
|
|
||||||
[AnimMime.GIF]: 'gif',
|
|
||||||
[AnimMime.APNG]: 'apng',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Mime2ExtMap: {
|
|
||||||
[key in ImageMime | AnimMime]: string;
|
|
||||||
} = {
|
|
||||||
...ImageMime2ExtMap,
|
|
||||||
...AnimMime2ExtMap,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Ext2MimeMap: {
|
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
} = Object.fromEntries(Object.entries(Mime2ExtMap).map(([k, v]) => [v, k]));
|
} = Object.fromEntries(Object.entries(FileType2ExtMap).map(([k, v]) => [v, k]));
|
||||||
|
|
||||||
export const Mime2Ext = (mime: string): string | undefined => {
|
export const FileType2Ext = (mime: string): Failable<string> => {
|
||||||
return Mime2ExtMap[mime as ImageMime | AnimMime];
|
const result = FileType2ExtMap[mime as ImageFileType | AnimFileType];
|
||||||
|
if (result === undefined)
|
||||||
|
return Fail(FT.Internal, undefined, `Unsupported mime type: ${mime}`);
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Ext2Mime = (ext: string): string | undefined => {
|
export const Ext2FileType = (ext: string): Failable<string> => {
|
||||||
return Ext2MimeMap[ext];
|
const result = Ext2FileTypeMap[ext];
|
||||||
|
if (result === undefined)
|
||||||
|
return Fail(FT.Internal, undefined, `Unsupported ext: ${ext}`);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// -- Mime
|
||||||
|
|
||||||
|
const FileType2MimeMap: {
|
||||||
|
[key in ImageFileType | AnimFileType]: string;
|
||||||
|
} = {
|
||||||
|
[AnimFileType.GIF]: 'image/gif',
|
||||||
|
[AnimFileType.WEBP]: 'image/webp',
|
||||||
|
// [AnimFileType.APNG]: 'image/apng',
|
||||||
|
[ImageFileType.QOI]: 'image/x-qoi',
|
||||||
|
[ImageFileType.JPEG]: 'image/jpeg',
|
||||||
|
[ImageFileType.PNG]: 'image/png',
|
||||||
|
[ImageFileType.WEBP]: 'image/webp',
|
||||||
|
[ImageFileType.TIFF]: 'image/tiff',
|
||||||
|
[ImageFileType.BMP]: 'image/bmp',
|
||||||
|
// [ImageFileType.ICO]: 'image/x-icon',
|
||||||
|
};
|
||||||
|
|
||||||
|
const Mime2FileTypeMap: {
|
||||||
|
[key: string]: string;
|
||||||
|
} = Object.fromEntries(
|
||||||
|
Object.entries(FileType2MimeMap).map(([k, v]) => [v, k]),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Mime2FileType = (mime: string): Failable<string> => {
|
||||||
|
const result = Mime2FileTypeMap[mime as ImageFileType | AnimFileType];
|
||||||
|
if (result === undefined)
|
||||||
|
return Fail(FT.Internal, undefined, `Unsupported mime type: ${mime}`);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
export const FileType2Mime = (filetype: string): Failable<string> => {
|
||||||
|
const result = FileType2MimeMap[filetype as ImageFileType | AnimFileType];
|
||||||
|
if (result === undefined)
|
||||||
|
return Fail(FT.Internal, undefined, `Unsupported filetype: ${filetype}`);
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -141,10 +141,20 @@ export function Fail(type: FT, reason?: any, dbgReason?: any): Failure {
|
|||||||
if (dbgReason === undefined || dbgReason === null) {
|
if (dbgReason === undefined || dbgReason === null) {
|
||||||
if (reason === undefined || reason === null) {
|
if (reason === undefined || reason === null) {
|
||||||
// If both are null, just return a default error message
|
// If both are null, just return a default error message
|
||||||
return new Failure(type, FTProps[type].message, undefined, undefined);
|
return new Failure(
|
||||||
|
type,
|
||||||
|
FTProps[type].message,
|
||||||
|
new Error(String(FTProps[type].message)).stack,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
} else if (typeof reason === 'string') {
|
} else if (typeof reason === 'string') {
|
||||||
// If it is a string, this was intentionally specified, so pass it through
|
// If it is a string, this was intentionally specified, so pass it through
|
||||||
return new Failure(type, reason, undefined, undefined);
|
return new Failure(
|
||||||
|
type,
|
||||||
|
reason,
|
||||||
|
new Error(String(reason)).stack,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
} else if (reason instanceof Error) {
|
} else if (reason instanceof Error) {
|
||||||
// In case of an error, we want to keep that hidden, so return the default message
|
// In case of an error, we want to keep that hidden, so return the default message
|
||||||
// Only send the specifics to debug
|
// Only send the specifics to debug
|
||||||
@@ -159,7 +169,7 @@ export function Fail(type: FT, reason?: any, dbgReason?: any): Failure {
|
|||||||
return new Failure(
|
return new Failure(
|
||||||
type,
|
type,
|
||||||
FTProps[type].message,
|
FTProps[type].message,
|
||||||
undefined,
|
new Error(String(reason)).stack,
|
||||||
String(reason),
|
String(reason),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -168,11 +178,21 @@ export function Fail(type: FT, reason?: any, dbgReason?: any): Failure {
|
|||||||
const strReason = reason?.toString() ?? FTProps[type].message;
|
const strReason = reason?.toString() ?? FTProps[type].message;
|
||||||
|
|
||||||
if (typeof dbgReason === 'string') {
|
if (typeof dbgReason === 'string') {
|
||||||
return new Failure(type, strReason, undefined, dbgReason);
|
return new Failure(
|
||||||
|
type,
|
||||||
|
strReason,
|
||||||
|
new Error(String(dbgReason)).stack,
|
||||||
|
dbgReason,
|
||||||
|
);
|
||||||
} else if (dbgReason instanceof Error) {
|
} else if (dbgReason instanceof Error) {
|
||||||
return new Failure(type, strReason, dbgReason.stack, dbgReason.message);
|
return new Failure(type, strReason, dbgReason.stack, dbgReason.message);
|
||||||
} else {
|
} else {
|
||||||
return new Failure(type, strReason, undefined, String(dbgReason));
|
return new Failure(
|
||||||
|
type,
|
||||||
|
strReason,
|
||||||
|
new Error(String(dbgReason)).stack,
|
||||||
|
String(dbgReason),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,34 @@
|
|||||||
import {
|
import {
|
||||||
FullMime,
|
Ext2FileType,
|
||||||
SupportedAnimMimes,
|
FileType,
|
||||||
SupportedImageMimes,
|
Mime2FileType,
|
||||||
SupportedMimeCategory
|
SupportedAnimFileTypes,
|
||||||
|
SupportedFileTypeCategory,
|
||||||
|
SupportedImageFileTypes
|
||||||
} from '../dto/mimes.dto';
|
} from '../dto/mimes.dto';
|
||||||
import { Fail, Failable, FT } from '../types';
|
import { Fail, Failable, FT, HasFailed } from '../types';
|
||||||
|
|
||||||
export function ParseMime(mime: string): Failable<FullMime> {
|
export function ParseFileType(filetype: string): Failable<FileType> {
|
||||||
if (SupportedImageMimes.includes(mime))
|
if (SupportedImageFileTypes.includes(filetype))
|
||||||
return { mime, type: SupportedMimeCategory.Image };
|
return { identifier: filetype, category: SupportedFileTypeCategory.Image };
|
||||||
|
|
||||||
if (SupportedAnimMimes.includes(mime))
|
if (SupportedAnimFileTypes.includes(filetype))
|
||||||
return { mime, type: SupportedMimeCategory.Animation };
|
return {
|
||||||
|
identifier: filetype,
|
||||||
|
category: SupportedFileTypeCategory.Animation,
|
||||||
|
};
|
||||||
|
|
||||||
return Fail(FT.UsrValidation, 'Unsupported mime type');
|
return Fail(FT.UsrValidation, 'Unsupported file type');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ParseExt2FileType(ext: string): Failable<FileType> {
|
||||||
|
const result = Ext2FileType(ext);
|
||||||
|
if (HasFailed(result)) return result;
|
||||||
|
return ParseFileType(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ParseMime2FileType(mime: string): Failable<FileType> {
|
||||||
|
const result = Mime2FileType(mime);
|
||||||
|
if (HasFailed(result)) return result;
|
||||||
|
return ParseFileType(result);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user