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