Add frontend qoi rendering
This commit is contained in:
parent
b778b86eae
commit
cadb909c40
|
@ -31,17 +31,18 @@
|
|||
"@vingle/bmp-js": "^0.2.5",
|
||||
"bcrypt": "^5.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"decode-ico": "^0.4.0",
|
||||
"fastify-helmet": "^7.0.1",
|
||||
"fastify-multipart": "^5.3.1",
|
||||
"fastify-static": "^4.6.1",
|
||||
"file-type": "^17.1.1",
|
||||
"ico-to-png": "^0.2.1",
|
||||
"passport": "^0.5.2",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-strategy": "^1.0.0",
|
||||
"pg": "^8.7.3",
|
||||
"picsur-shared": "*",
|
||||
"qoi-img": "^1.0.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.5.5",
|
||||
|
|
|
@ -2,11 +2,10 @@ import { Module } from '@nestjs/common';
|
|||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { EImageBackend } from '../../models/entities/image.entity';
|
||||
import { ImageDBService } from './imagedb.service';
|
||||
import { MimesService } from './mimes.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([EImageBackend])],
|
||||
providers: [ImageDBService, MimesService],
|
||||
exports: [ImageDBService, MimesService],
|
||||
providers: [ImageDBService],
|
||||
exports: [ImageDBService],
|
||||
})
|
||||
export class ImageDBModule {}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Fail, Failable } from 'picsur-shared/dist/types';
|
||||
import {
|
||||
FullMime,
|
||||
SupportedAnimMimes,
|
||||
SupportedImageMimes,
|
||||
SupportedMimeCategory
|
||||
} from '../../models/dto/mimes.dto';
|
||||
|
||||
@Injectable()
|
||||
export class MimesService {
|
||||
public getFullMime(mime: string): Failable<FullMime> {
|
||||
if (SupportedImageMimes.includes(mime)) {
|
||||
return { mime, type: SupportedMimeCategory.Image };
|
||||
}
|
||||
if (SupportedAnimMimes.includes(mime)) {
|
||||
return { mime, type: SupportedMimeCategory.Animation };
|
||||
}
|
||||
return Fail('Unsupported mime type');
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { fileTypeFromBuffer, FileTypeResult } from 'file-type';
|
||||
import { FullMime } from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { ParseMime } from 'picsur-shared/dist/util/parse-mime';
|
||||
import { ImageDBService } from '../../collections/imagedb/imagedb.service';
|
||||
import { MimesService } from '../../collections/imagedb/mimes.service';
|
||||
import { FullMime } from '../../models/dto/mimes.dto';
|
||||
import { EImageBackend } from '../../models/entities/image.entity';
|
||||
import { ImageProcessorService } from './imageprocessor.service';
|
||||
|
||||
|
@ -15,7 +15,6 @@ import { ImageProcessorService } from './imageprocessor.service';
|
|||
export class ImageManagerService {
|
||||
constructor(
|
||||
private readonly imagesService: ImageDBService,
|
||||
private readonly mimesService: MimesService,
|
||||
private readonly processService: ImageProcessorService,
|
||||
) {}
|
||||
|
||||
|
@ -35,9 +34,16 @@ export class ImageManagerService {
|
|||
image: Buffer,
|
||||
userid: string,
|
||||
): AsyncFailable<EImageBackend> {
|
||||
let startTime = Date.now();
|
||||
|
||||
console.log('Uploading image');
|
||||
|
||||
const fullMime = await this.getFullMimeFromBuffer(image);
|
||||
if (HasFailed(fullMime)) return fullMime;
|
||||
|
||||
console.log('Got full mime after ' + (Date.now() - startTime) + 'ms');
|
||||
startTime = Date.now();
|
||||
|
||||
const processedImage = await this.processService.process(
|
||||
image,
|
||||
fullMime,
|
||||
|
@ -45,20 +51,26 @@ export class ImageManagerService {
|
|||
);
|
||||
if (HasFailed(processedImage)) return processedImage;
|
||||
|
||||
console.log('Processed image after ' + (Date.now() - startTime) + 'ms');
|
||||
startTime = Date.now();
|
||||
|
||||
const imageEntity = await this.imagesService.create(
|
||||
processedImage,
|
||||
fullMime.mime,
|
||||
);
|
||||
if (HasFailed(imageEntity)) return imageEntity;
|
||||
|
||||
console.log('Created image after ' + (Date.now() - startTime) + 'ms');
|
||||
|
||||
return imageEntity;
|
||||
}
|
||||
|
||||
private async getFullMimeFromBuffer(image: Buffer): AsyncFailable<FullMime> {
|
||||
const mime: FileTypeResult | undefined = await fileTypeFromBuffer(image);
|
||||
const fullMime = await this.mimesService.getFullMime(
|
||||
mime?.mime ?? 'extra/discard',
|
||||
);
|
||||
|
||||
console.log(mime);
|
||||
|
||||
const fullMime = ParseMime(mime?.mime ?? 'extra/discard');
|
||||
return fullMime;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import * as bmp from '@vingle/bmp-js';
|
||||
import icoToPng from 'ico-to-png';
|
||||
import { AsyncFailable, Fail } from 'picsur-shared/dist/types';
|
||||
import sharp from 'sharp';
|
||||
import { UsrPreferenceService } from '../../collections/preferencesdb/usrpreferencedb.service';
|
||||
import decodeico from 'decode-ico';
|
||||
import {
|
||||
FullMime,
|
||||
ImageMime,
|
||||
SupportedMimeCategory
|
||||
} from '../../models/dto/mimes.dto';
|
||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { AsyncFailable, Fail } from 'picsur-shared/dist/types';
|
||||
import { QOIColorSpace, QOIencode } from 'qoi-img';
|
||||
import sharp from 'sharp';
|
||||
import { UsrPreferenceService } from '../../collections/preferencesdb/usrpreferencedb.service';
|
||||
|
||||
@Injectable()
|
||||
export class ImageProcessorService {
|
||||
private readonly PngOptions = {
|
||||
compressionLevel: 9,
|
||||
effort: 10,
|
||||
};
|
||||
|
||||
constructor(private readonly userPref: UsrPreferenceService) {}
|
||||
|
||||
public async process(
|
||||
|
@ -48,22 +44,38 @@ export class ImageProcessorService {
|
|||
mime: FullMime,
|
||||
options: {},
|
||||
): AsyncFailable<Buffer> {
|
||||
let processedImage = image;
|
||||
let sharpImage: sharp.Sharp;
|
||||
|
||||
if (mime.mime === ImageMime.ICO) {
|
||||
processedImage = await icoToPng(processedImage, 512);
|
||||
sharpImage = this.icoSharp(image);
|
||||
} else if (mime.mime === ImageMime.BMP) {
|
||||
processedImage = await this.bmpSharp(processedImage)
|
||||
.png(this.PngOptions)
|
||||
.toBuffer();
|
||||
sharpImage = this.bmpSharp(image);
|
||||
} else {
|
||||
processedImage = await sharp(processedImage)
|
||||
.png(this.PngOptions)
|
||||
.toBuffer();
|
||||
sharpImage = sharp(image);
|
||||
}
|
||||
mime.mime = ImageMime.PNG;
|
||||
|
||||
return processedImage;
|
||||
sharpImage = sharpImage.toColorspace('srgb');
|
||||
|
||||
const metadata = await sharpImage.metadata();
|
||||
const pixels = await sharpImage.raw().toBuffer();
|
||||
|
||||
if (
|
||||
metadata.hasAlpha === undefined ||
|
||||
metadata.width === undefined ||
|
||||
metadata.height === undefined
|
||||
)
|
||||
return Fail('Invalid image');
|
||||
|
||||
// Png can be more efficient than QOI, but its just sooooooo slow
|
||||
const qoiImage = QOIencode(pixels, {
|
||||
channels: metadata.hasAlpha ? 4 : 3,
|
||||
colorSpace: QOIColorSpace.SRGB,
|
||||
height: metadata.height,
|
||||
width: metadata.width,
|
||||
});
|
||||
|
||||
return qoiImage;
|
||||
}
|
||||
|
||||
private async processAnimation(
|
||||
|
@ -85,4 +97,18 @@ export class ImageProcessorService {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
private icoSharp(image: Buffer) {
|
||||
const result = decodeico(image);
|
||||
// Get biggest image
|
||||
const best = result.sort((a, b) => b.width - a.width)[0];
|
||||
|
||||
return sharp(best.data, {
|
||||
raw: {
|
||||
width: best.width,
|
||||
height: best.height,
|
||||
channels: 4,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Head,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
|
@ -46,6 +47,20 @@ export class ImageController {
|
|||
return image.data;
|
||||
}
|
||||
|
||||
@Head(':hash')
|
||||
async headImage(
|
||||
@Res({ passthrough: true }) res: FastifyReply,
|
||||
@Param('hash', ImageIdValidator) hash: string,
|
||||
) {
|
||||
const image = await this.imagesService.retrieveInfo(hash);
|
||||
if (HasFailed(image)) {
|
||||
this.logger.warn(image.getReason());
|
||||
throw new NotFoundException('Could not find image');
|
||||
}
|
||||
|
||||
res.type(image.mime);
|
||||
}
|
||||
|
||||
@Get('meta/:hash')
|
||||
@Returns(ImageMetaResponse)
|
||||
async getImageMeta(
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.json",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/assets"],
|
||||
"styles": ["src/styles.scss"],
|
||||
|
@ -37,7 +37,8 @@
|
|||
"buffer/",
|
||||
"sha.js"
|
||||
],
|
||||
"optimization": true
|
||||
"optimization": true,
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<canvas
|
||||
*ngIf="state === 'canvas' || state === 'loading'"
|
||||
[style.display]="state === 'loading' ? 'none' : 'unset'"
|
||||
height="0"
|
||||
width="0"
|
||||
#targetcanvas
|
||||
></canvas>
|
||||
<img *ngIf="state === 'image'" [src]="imageURL" />
|
||||
<mat-spinner *ngIf="state === 'loading'" color="accent"></mat-spinner>
|
|
@ -0,0 +1,19 @@
|
|||
:host {
|
||||
display: block;
|
||||
// Clip contents
|
||||
overflow: hidden;
|
||||
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
canvas, img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { SupportedMimeCategory } from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { 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 { Logger } from 'src/app/services/logger/logger.service';
|
||||
import { QoiWorkerService } from 'src/app/workers/qoi-worker.service';
|
||||
|
||||
enum PicsurImgState {
|
||||
Loading = 'loading',
|
||||
Canvas = 'canvas',
|
||||
Image = 'image',
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'picsur-img',
|
||||
templateUrl: './picsur-img.component.html',
|
||||
styleUrls: ['./picsur-img.component.scss'],
|
||||
})
|
||||
export class PicsurImgComponent implements OnChanges {
|
||||
private readonly logger = new Logger('ZodImgComponent');
|
||||
|
||||
@ViewChild('targetcanvas') canvas: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
@Input('src') imageURL: string | undefined;
|
||||
|
||||
public state: PicsurImgState = PicsurImgState.Loading;
|
||||
|
||||
constructor(private qoiWorker: QoiWorkerService) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
let url = this.imageURL ?? '';
|
||||
if (URLRegex.test(url)) {
|
||||
this.update(url).catch((err) => this.logger.error(err));
|
||||
} else {
|
||||
this.state = PicsurImgState.Loading;
|
||||
}
|
||||
}
|
||||
|
||||
private async update(url: string) {
|
||||
const mime = await this.getMime(url);
|
||||
if (HasFailed(mime)) {
|
||||
this.logger.error(mime.getReason());
|
||||
return;
|
||||
}
|
||||
|
||||
if (mime.type === SupportedMimeCategory.Image) {
|
||||
const result = await this.qoiWorker.decode(url);
|
||||
|
||||
const canvas = this.canvas.nativeElement;
|
||||
canvas.height = result.height;
|
||||
canvas.width = result.width;
|
||||
canvas.getContext('2d')?.putImageData(result.data, 0, 0);
|
||||
|
||||
this.state = PicsurImgState.Canvas;
|
||||
} else if (mime.type === SupportedMimeCategory.Animation) {
|
||||
this.state = PicsurImgState.Image;
|
||||
} else {
|
||||
this.logger.error(`Unsupported mime type: ${mime.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async getMime(url: string) {
|
||||
const response = await fetch(url, {
|
||||
method: 'HEAD',
|
||||
});
|
||||
const mimeHeader = response.headers.get('content-type') ?? '';
|
||||
const mime = mimeHeader.split(';')[0];
|
||||
|
||||
const fullMime = ParseMime(mime);
|
||||
return fullMime;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { PicsurImgComponent } from './picsur-img.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [PicsurImgComponent],
|
||||
imports: [CommonModule, MatProgressSpinnerModule],
|
||||
exports: [PicsurImgComponent],
|
||||
})
|
||||
export class PicsurImgModule {}
|
|
@ -6,11 +6,14 @@
|
|||
</div>
|
||||
|
||||
<div class="col-12 py-3">
|
||||
<img class="uploadedimage" src="{{ imageUrl }}" als="Uploaded" />
|
||||
<picsur-img
|
||||
class="uploadedimage"
|
||||
[src]="imageLinks.source"
|
||||
></picsur-img>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<copyfield label="Image URL" [value]="imageUrl"></copyfield>
|
||||
<copyfield label="Image URL" [value]="imageLinks.source"></copyfield>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<copyfield label="Markdown" [value]="imageLinks.markdown"></copyfield>
|
||||
|
@ -25,7 +28,21 @@
|
|||
<copyfield label="Rst" [value]="imageLinks.rst"></copyfield>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button mat-raised-button color="accent" (click)="goBackHome()">
|
||||
<button
|
||||
mat-raised-button
|
||||
class="mx-1"
|
||||
color="accent"
|
||||
(click)="downloadImage()"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-raised-button
|
||||
class="mx-1"
|
||||
color="primary"
|
||||
(click)="goBackHome()"
|
||||
>
|
||||
Upload Another
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -6,5 +6,3 @@
|
|||
.content-border {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ export class ViewComponent implements OnInit {
|
|||
private utilService: UtilService
|
||||
) {}
|
||||
|
||||
public imageUrl: string = '';
|
||||
public imageLinks = new ImageLinks();
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -28,13 +27,16 @@ export class ViewComponent implements OnInit {
|
|||
return this.utilService.quitError('Invalid image link');
|
||||
}
|
||||
|
||||
const imageMeta = await this.imageService.GetImageMeta(hash);
|
||||
if (HasFailed(imageMeta)) {
|
||||
return this.utilService.quitError(imageMeta.getReason());
|
||||
const metadata = await this.imageService.GetImageMeta(hash);
|
||||
if (HasFailed(metadata)) {
|
||||
return this.utilService.quitError(metadata.getReason());
|
||||
}
|
||||
|
||||
this.imageUrl = this.imageService.GetImageURL(hash);
|
||||
this.imageLinks = this.imageService.CreateImageLinks(this.imageUrl);
|
||||
this.imageLinks = this.imageService.CreateImageLinksFromID(hash);
|
||||
}
|
||||
|
||||
downloadImage() {
|
||||
this.utilService.downloadFile(this.imageLinks.source);
|
||||
}
|
||||
|
||||
goBackHome() {
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import Fuse from 'fuse.js';
|
||||
import { CopyFieldModule } from 'src/app/components/copyfield/copyfield.module';
|
||||
import { PicsurImgModule } from 'src/app/components/picsur-img/picsur-img.module';
|
||||
import { ViewComponent } from './view.component';
|
||||
import { ViewRoutingModule } from './view.routing.module';
|
||||
const a = Fuse;
|
||||
@NgModule({
|
||||
declarations: [ViewComponent],
|
||||
imports: [CommonModule, CopyFieldModule, ViewRoutingModule, MatButtonModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
CopyFieldModule,
|
||||
ViewRoutingModule,
|
||||
MatButtonModule,
|
||||
PicsurImgModule,
|
||||
],
|
||||
})
|
||||
export class ViewRouteModule {}
|
||||
|
|
|
@ -41,4 +41,8 @@ export class ImageService {
|
|||
bbcode: `[img]${imageURL}[/img]`,
|
||||
};
|
||||
}
|
||||
|
||||
public CreateImageLinksFromID(imageID: string): ImageLinks {
|
||||
return this.CreateImageLinks(this.GetImageURL(imageID));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
/**
|
||||
* Decode a QOI file given as an ArrayBuffer.
|
||||
*
|
||||
* @param {ArrayBuffer} arrayBuffer ArrayBuffer containing the QOI file.
|
||||
* @param {int|null} [byteOffset] Offset to the start of the QOI file in arrayBuffer
|
||||
* @param {int|null} [byteLength] Length of the QOI file in bytes
|
||||
* @param {int|null} [outputChannels] Number of channels to include in the decoded array
|
||||
*
|
||||
* @returns {{channels: number, data: Uint8Array, colorspace: number, width: number, error: boolean, height: number}}
|
||||
*/
|
||||
export function QOIdecodeJS(
|
||||
arrayBuffer: ArrayBuffer,
|
||||
byteOffset: number | null,
|
||||
byteLength: number | null,
|
||||
outputChannels: number | null
|
||||
): {
|
||||
channels: number;
|
||||
data: Uint8Array;
|
||||
colorspace: number;
|
||||
width: number;
|
||||
height: number;
|
||||
} {
|
||||
if (typeof byteOffset === 'undefined' || byteOffset === null) {
|
||||
byteOffset = 0;
|
||||
}
|
||||
|
||||
if (typeof byteLength === 'undefined' || byteLength === null) {
|
||||
byteLength = arrayBuffer.byteLength - byteOffset;
|
||||
}
|
||||
|
||||
const uint8 = new Uint8Array(arrayBuffer, byteOffset, byteLength);
|
||||
|
||||
const magic1 = uint8[0];
|
||||
const magic2 = uint8[1];
|
||||
const magic3 = uint8[2];
|
||||
const magic4 = uint8[3];
|
||||
|
||||
const width =
|
||||
((uint8[4] << 24) | (uint8[5] << 16) | (uint8[6] << 8) | uint8[7]) >>> 0;
|
||||
const height =
|
||||
((uint8[8] << 24) | (uint8[9] << 16) | (uint8[10] << 8) | uint8[11]) >>> 0;
|
||||
|
||||
const channels = uint8[12];
|
||||
const colorspace = uint8[13];
|
||||
|
||||
if (typeof outputChannels === 'undefined' || outputChannels === null) {
|
||||
outputChannels = channels;
|
||||
}
|
||||
|
||||
if (
|
||||
magic1 !== 0x71 ||
|
||||
magic2 !== 0x6f ||
|
||||
magic3 !== 0x69 ||
|
||||
magic4 !== 0x66
|
||||
) {
|
||||
throw new Error('QOI.decode: The signature of the QOI file is invalid');
|
||||
}
|
||||
|
||||
if (channels < 3 || channels > 4) {
|
||||
throw new Error(
|
||||
'QOI.decode: The number of channels declared in the file is invalid'
|
||||
);
|
||||
}
|
||||
|
||||
if (colorspace > 1) {
|
||||
throw new Error(
|
||||
'QOI.decode: The colorspace declared in the file is invalid'
|
||||
);
|
||||
}
|
||||
|
||||
if (outputChannels < 3 || outputChannels > 4) {
|
||||
throw new Error(
|
||||
'QOI.decode: The number of channels for the output is invalid'
|
||||
);
|
||||
}
|
||||
|
||||
const pixelLength = width * height * outputChannels;
|
||||
const result = new Uint8Array(pixelLength);
|
||||
|
||||
let arrayPosition = 14;
|
||||
|
||||
const index = new Uint8Array(64 * 4);
|
||||
let indexPosition = 0;
|
||||
|
||||
let red = 0;
|
||||
let green = 0;
|
||||
let blue = 0;
|
||||
let alpha = 255;
|
||||
|
||||
const chunksLength = byteLength - 8;
|
||||
|
||||
let run = 0;
|
||||
let pixelPosition = 0;
|
||||
|
||||
for (
|
||||
;
|
||||
pixelPosition < pixelLength && arrayPosition < byteLength - 4;
|
||||
pixelPosition += outputChannels
|
||||
) {
|
||||
if (run > 0) {
|
||||
run--;
|
||||
} else if (arrayPosition < chunksLength) {
|
||||
const byte1 = uint8[arrayPosition++];
|
||||
|
||||
if (byte1 === 0b11111110) {
|
||||
// QOI_OP_RGB
|
||||
red = uint8[arrayPosition++];
|
||||
green = uint8[arrayPosition++];
|
||||
blue = uint8[arrayPosition++];
|
||||
} else if (byte1 === 0b11111111) {
|
||||
// QOI_OP_RGBA
|
||||
red = uint8[arrayPosition++];
|
||||
green = uint8[arrayPosition++];
|
||||
blue = uint8[arrayPosition++];
|
||||
alpha = uint8[arrayPosition++];
|
||||
} else if ((byte1 & 0b11000000) === 0b00000000) {
|
||||
// QOI_OP_INDEX
|
||||
red = index[byte1 * 4];
|
||||
green = index[byte1 * 4 + 1];
|
||||
blue = index[byte1 * 4 + 2];
|
||||
alpha = index[byte1 * 4 + 3];
|
||||
} else if ((byte1 & 0b11000000) === 0b01000000) {
|
||||
// QOI_OP_DIFF
|
||||
red += ((byte1 >> 4) & 0b00000011) - 2;
|
||||
green += ((byte1 >> 2) & 0b00000011) - 2;
|
||||
blue += (byte1 & 0b00000011) - 2;
|
||||
|
||||
// handle wraparound
|
||||
red = (red + 256) % 256;
|
||||
green = (green + 256) % 256;
|
||||
blue = (blue + 256) % 256;
|
||||
} else if ((byte1 & 0b11000000) === 0b10000000) {
|
||||
// QOI_OP_LUMA
|
||||
const byte2 = uint8[arrayPosition++];
|
||||
const greenDiff = (byte1 & 0b00111111) - 32;
|
||||
const redDiff = greenDiff + ((byte2 >> 4) & 0b00001111) - 8;
|
||||
const blueDiff = greenDiff + (byte2 & 0b00001111) - 8;
|
||||
|
||||
// handle wraparound
|
||||
red = (red + redDiff + 256) % 256;
|
||||
green = (green + greenDiff + 256) % 256;
|
||||
blue = (blue + blueDiff + 256) % 256;
|
||||
} else if ((byte1 & 0b11000000) === 0b11000000) {
|
||||
// QOI_OP_RUN
|
||||
run = byte1 & 0b00111111;
|
||||
}
|
||||
|
||||
indexPosition = ((red * 3 + green * 5 + blue * 7 + alpha * 11) % 64) * 4;
|
||||
index[indexPosition] = red;
|
||||
index[indexPosition + 1] = green;
|
||||
index[indexPosition + 2] = blue;
|
||||
index[indexPosition + 3] = alpha;
|
||||
}
|
||||
|
||||
if (outputChannels === 4) {
|
||||
// RGBA
|
||||
result[pixelPosition] = red;
|
||||
result[pixelPosition + 1] = green;
|
||||
result[pixelPosition + 2] = blue;
|
||||
result[pixelPosition + 3] = alpha;
|
||||
} else {
|
||||
// RGB
|
||||
result[pixelPosition] = red;
|
||||
result[pixelPosition + 1] = green;
|
||||
result[pixelPosition + 2] = blue;
|
||||
}
|
||||
}
|
||||
|
||||
if (pixelPosition < pixelLength) {
|
||||
throw new Error('QOI.decode: Incomplete image');
|
||||
}
|
||||
|
||||
// checking the 00000001 padding is not required, as per specs
|
||||
|
||||
return {
|
||||
width: width,
|
||||
height: height,
|
||||
colorspace: colorspace,
|
||||
channels: outputChannels,
|
||||
data: result,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Encode a QOI file.
|
||||
*
|
||||
* @param {Uint8Array|Uint8ClampedArray} colorData Array containing the color information for each pixel of the image (left to right, top to bottom)
|
||||
* @param {object} description
|
||||
* @param {int} description.width Width of the image
|
||||
* @param {int} description.height Height of the image
|
||||
* @param {int} description.channels Number of channels in the image (3: RGB, 4: RGBA)
|
||||
* @param {int} description.colorspace Colorspace used in the image (0: sRGB with linear alpha, 1: linear)
|
||||
*
|
||||
* @returns {ArrayBuffer} ArrayBuffer containing the QOI file content
|
||||
*/
|
||||
export function QOIencodeJS(
|
||||
colorData: Uint8Array | Uint8ClampedArray,
|
||||
description: {
|
||||
width: number;
|
||||
height: number;
|
||||
channels: number;
|
||||
colorspace: number;
|
||||
}
|
||||
) {
|
||||
const width = description.width;
|
||||
const height = description.height;
|
||||
const channels = description.channels;
|
||||
const colorspace = description.colorspace;
|
||||
|
||||
let red = 0;
|
||||
let green = 0;
|
||||
let blue = 0;
|
||||
let alpha = 255;
|
||||
let prevRed = red;
|
||||
let prevGreen = green;
|
||||
let prevBlue = blue;
|
||||
let prevAlpha = alpha;
|
||||
|
||||
let run = 0;
|
||||
let p = 0;
|
||||
const pixelLength = width * height * channels;
|
||||
const pixelEnd = pixelLength - channels;
|
||||
|
||||
if (width < 0 || width >= 4294967296) {
|
||||
throw new Error('QOI.encode: Invalid description.width');
|
||||
}
|
||||
|
||||
if (height < 0 || height >= 4294967296) {
|
||||
throw new Error('QOI.encode: Invalid description.height');
|
||||
}
|
||||
|
||||
if (
|
||||
colorData.constructor.name !== 'Uint8Array' &&
|
||||
colorData.constructor.name !== 'Uint8ClampedArray'
|
||||
) {
|
||||
throw new Error(
|
||||
'QOI.encode: The provided colorData must be instance of Uint8Array or Uint8ClampedArray'
|
||||
);
|
||||
}
|
||||
|
||||
if (colorData.length !== pixelLength) {
|
||||
throw new Error('QOI.encode: The length of colorData is incorrect');
|
||||
}
|
||||
|
||||
if (channels !== 3 && channels !== 4) {
|
||||
throw new Error('QOI.encode: Invalid description.channels, must be 3 or 4');
|
||||
}
|
||||
|
||||
if (colorspace !== 0 && colorspace !== 1) {
|
||||
throw new Error(
|
||||
'QOI.encode: Invalid description.colorspace, must be 0 or 1'
|
||||
);
|
||||
}
|
||||
|
||||
const maxSize = width * height * (channels + 1) + 14 + 8;
|
||||
const result = new Uint8Array(maxSize);
|
||||
const index = new Uint8Array(64 * 4);
|
||||
|
||||
// 0->3 : magic "qoif"
|
||||
result[p++] = 0x71;
|
||||
result[p++] = 0x6f;
|
||||
result[p++] = 0x69;
|
||||
result[p++] = 0x66;
|
||||
|
||||
// 4->7 : width
|
||||
result[p++] = (width >> 24) & 0xff;
|
||||
result[p++] = (width >> 16) & 0xff;
|
||||
result[p++] = (width >> 8) & 0xff;
|
||||
result[p++] = width & 0xff;
|
||||
|
||||
// 8->11 : height
|
||||
result[p++] = (height >> 24) & 0xff;
|
||||
result[p++] = (height >> 16) & 0xff;
|
||||
result[p++] = (height >> 8) & 0xff;
|
||||
result[p++] = height & 0xff;
|
||||
|
||||
// 12 : channels, 13 : colorspace
|
||||
result[p++] = channels;
|
||||
result[p++] = colorspace;
|
||||
|
||||
for (let pixelPos = 0; pixelPos < pixelLength; pixelPos += channels) {
|
||||
if (channels === 4) {
|
||||
red = colorData[pixelPos];
|
||||
green = colorData[pixelPos + 1];
|
||||
blue = colorData[pixelPos + 2];
|
||||
alpha = colorData[pixelPos + 3];
|
||||
} else {
|
||||
red = colorData[pixelPos];
|
||||
green = colorData[pixelPos + 1];
|
||||
blue = colorData[pixelPos + 2];
|
||||
}
|
||||
|
||||
if (
|
||||
prevRed === red &&
|
||||
prevGreen === green &&
|
||||
prevBlue === blue &&
|
||||
prevAlpha === alpha
|
||||
) {
|
||||
run++;
|
||||
|
||||
// reached the maximum run length, or reached the end of colorData
|
||||
if (run === 62 || pixelPos === pixelEnd) {
|
||||
// QOI_OP_RUN
|
||||
result[p++] = 0b11000000 | (run - 1);
|
||||
run = 0;
|
||||
}
|
||||
} else {
|
||||
if (run > 0) {
|
||||
// QOI_OP_RUN
|
||||
result[p++] = 0b11000000 | (run - 1);
|
||||
run = 0;
|
||||
}
|
||||
|
||||
const indexPosition =
|
||||
((red * 3 + green * 5 + blue * 7 + alpha * 11) % 64) * 4;
|
||||
|
||||
if (
|
||||
index[indexPosition] === red &&
|
||||
index[indexPosition + 1] === green &&
|
||||
index[indexPosition + 2] === blue &&
|
||||
index[indexPosition + 3] === alpha
|
||||
) {
|
||||
result[p++] = indexPosition / 4;
|
||||
} else {
|
||||
index[indexPosition] = red;
|
||||
index[indexPosition + 1] = green;
|
||||
index[indexPosition + 2] = blue;
|
||||
index[indexPosition + 3] = alpha;
|
||||
|
||||
if (alpha === prevAlpha) {
|
||||
// ternary with bitmask handles the wraparound
|
||||
let vr = red - prevRed;
|
||||
vr = vr & 0b10000000 ? (vr - 256) % 256 : (vr + 256) % 256;
|
||||
let vg = green - prevGreen;
|
||||
vg = vg & 0b10000000 ? (vg - 256) % 256 : (vg + 256) % 256;
|
||||
let vb = blue - prevBlue;
|
||||
vb = vb & 0b10000000 ? (vb - 256) % 256 : (vb + 256) % 256;
|
||||
|
||||
const vg_r = vr - vg;
|
||||
const vg_b = vb - vg;
|
||||
|
||||
if (vr > -3 && vr < 2 && vg > -3 && vg < 2 && vb > -3 && vb < 2) {
|
||||
// QOI_OP_DIFF
|
||||
result[p++] =
|
||||
0b01000000 | ((vr + 2) << 4) | ((vg + 2) << 2) | (vb + 2);
|
||||
} else if (
|
||||
vg_r > -9 &&
|
||||
vg_r < 8 &&
|
||||
vg > -33 &&
|
||||
vg < 32 &&
|
||||
vg_b > -9 &&
|
||||
vg_b < 8
|
||||
) {
|
||||
// QOI_OP_LUMA
|
||||
result[p++] = 0b10000000 | (vg + 32);
|
||||
result[p++] = ((vg_r + 8) << 4) | (vg_b + 8);
|
||||
} else {
|
||||
// QOI_OP_RGB
|
||||
result[p++] = 0b11111110;
|
||||
result[p++] = red;
|
||||
result[p++] = green;
|
||||
result[p++] = blue;
|
||||
}
|
||||
} else {
|
||||
// QOI_OP_RGBA
|
||||
result[p++] = 0b11111111;
|
||||
result[p++] = red;
|
||||
result[p++] = green;
|
||||
result[p++] = blue;
|
||||
result[p++] = alpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevRed = red;
|
||||
prevGreen = green;
|
||||
prevBlue = blue;
|
||||
prevAlpha = alpha;
|
||||
}
|
||||
|
||||
// 00000001 end marker/padding
|
||||
result[p++] = 0;
|
||||
result[p++] = 0;
|
||||
result[p++] = 0;
|
||||
result[p++] = 0;
|
||||
result[p++] = 0;
|
||||
result[p++] = 0;
|
||||
result[p++] = 0;
|
||||
result[p++] = 1;
|
||||
|
||||
// return an ArrayBuffer trimmed to the correct length
|
||||
return result.buffer.slice(0, p);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
# QOI.js
|
||||
|
||||
I included it directly because it needed to be typescript compatible
|
||||
|
||||
<https://github.com/kchapelier/qoijs>
|
|
@ -47,6 +47,15 @@ export class UtilService {
|
|||
});
|
||||
}
|
||||
|
||||
public downloadFile(url: string) {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = url.split('/').pop() ?? '';
|
||||
link.click();
|
||||
|
||||
link.remove();
|
||||
}
|
||||
|
||||
public async sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class QoiWorkerService {
|
||||
private worker: Worker | null = null;
|
||||
private job: Promise<Function> | null = null;
|
||||
|
||||
constructor() {
|
||||
if (typeof Worker !== 'undefined') {
|
||||
this.worker = new Worker(new URL('./qoi.worker', import.meta.url));
|
||||
} else {
|
||||
this.job = import('./qoi.job').then((m) => m.default);
|
||||
}
|
||||
}
|
||||
|
||||
public async decode(url: string): Promise<{
|
||||
data: ImageData;
|
||||
width: number;
|
||||
height: number;
|
||||
}> {
|
||||
if (this.worker && !this.job) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = Date.now();
|
||||
const listener = ({ data }: { data: any }) => {
|
||||
if (data.id !== id) return;
|
||||
this.worker!.removeEventListener('message', listener);
|
||||
|
||||
resolve({
|
||||
data: data.data,
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
});
|
||||
};
|
||||
this.worker!.addEventListener('message', listener);
|
||||
this.worker!.postMessage({ id, url });
|
||||
});
|
||||
} else if (!this.worker && this.job) {
|
||||
const job = await this.job;
|
||||
return job(url);
|
||||
} else {
|
||||
throw new Error('No worker available');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { QOIdecodeJS } from '../util/qoi/qoi-decode';
|
||||
|
||||
export default async function qoiDecodeJob(url: string): Promise<{
|
||||
data: ImageData;
|
||||
width: number;
|
||||
height: number;
|
||||
}> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image: ${url}`);
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
const image = QOIdecodeJS(buffer, null, null, 4);
|
||||
|
||||
const imageData = new ImageData(
|
||||
new Uint8ClampedArray(image.data.buffer),
|
||||
image.width,
|
||||
image.height
|
||||
);
|
||||
|
||||
return {
|
||||
data: imageData,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/// <reference lib="webworker" />
|
||||
|
||||
import qoiDecodeJob from './qoi.job';
|
||||
|
||||
addEventListener('message', async (msg) => {
|
||||
const { id, url } = msg.data;
|
||||
if (!id || !url) {
|
||||
throw new Error('Invalid message');
|
||||
}
|
||||
|
||||
const result = await qoiDecodeJob(url);
|
||||
|
||||
postMessage({
|
||||
id,
|
||||
...result,
|
||||
});
|
||||
});
|
|
@ -47,7 +47,6 @@
|
|||
*/
|
||||
import 'reflect-metadata';
|
||||
import 'zone.js'; // Included with Angular CLI.
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/",
|
||||
|
||||
"target": "es2017",
|
||||
"module": "es2020",
|
||||
|
||||
"lib": ["es2020", "dom"],
|
||||
},
|
||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
||||
"include": ["src/**/*.d.ts", "src/app/**/*.ts"],
|
||||
"exclude": ["src/**/*.worker.ts"]
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
|
||||
"target": "es2017",
|
||||
"module": "es2020",
|
||||
|
||||
"experimentalDecorators": true,
|
||||
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"importHelpers": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
|
@ -1,30 +1,11 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./out-tsc/",
|
||||
|
||||
"target": "es2017",
|
||||
"module": "es2020",
|
||||
|
||||
"lib": ["es2020", "dom"],
|
||||
|
||||
"experimentalDecorators": true,
|
||||
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"importHelpers": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
||||
"include": ["src/**/*.d.ts", "src/app/**/*.ts"],
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.worker.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/worker",
|
||||
"lib": ["es2020", "webworker"],
|
||||
|
||||
"isolatedModules": false
|
||||
},
|
||||
"include": ["src/app/workers/*.worker.ts"]
|
||||
}
|
|
@ -30,3 +30,4 @@ export interface FullMime {
|
|||
mime: string;
|
||||
type: SupportedMimeCategory;
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
export const AlphaNumeric = /^[a-zA-Z0-9]+$/;
|
||||
export const SHA256 = /^[a-f0-9A-F]{64}$/;
|
||||
export const SemVer = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
||||
export const URLRegex =
|
||||
/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/;
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import { FullMime, SupportedAnimMimes, SupportedImageMimes, SupportedMimeCategory } from '../dto/mimes.dto';
|
||||
import { Fail, Failable } from '../types';
|
||||
|
||||
export function ParseMime(mime: string): Failable<FullMime> {
|
||||
if (SupportedImageMimes.includes(mime)) {
|
||||
return { mime, type: SupportedMimeCategory.Image };
|
||||
}
|
||||
if (SupportedAnimMimes.includes(mime)) {
|
||||
return { mime, type: SupportedMimeCategory.Animation };
|
||||
}
|
||||
return Fail('Unsupported mime type');
|
||||
}
|
54
yarn.lock
54
yarn.lock
|
@ -2482,6 +2482,13 @@ binary-extensions@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||
|
||||
bindings@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
|
||||
integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
|
||||
dependencies:
|
||||
file-uri-to-path "1.0.0"
|
||||
|
||||
bl@^4.0.3, bl@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
|
||||
|
@ -3286,9 +3293,9 @@ ee-first@1.1.1:
|
|||
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
|
||||
|
||||
electron-to-chromium@^1.4.84:
|
||||
version "1.4.109"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.109.tgz#74df3109e37f1feed6124f255f52c96aa62feb7f"
|
||||
integrity sha512-LCF+Oqs2Oqwf8M3oc8T59Wi9C0xpL1qVyqIR6bPTCl8uPvln7G184L39tO4SE4Dyg/Kp1RjAz//BKMvi0uvw4w==
|
||||
version "1.4.111"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.111.tgz#897613f6504f3f17c9381c7499a635b413e4df4e"
|
||||
integrity sha512-/s3+fwhKf1YK4k7btOImOzCQLpUjS6MaPf0ODTNuT4eTM1Bg4itBpLkydhOzJmpmH6Z9eXFyuuK5czsmzRzwtw==
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
|
@ -3933,6 +3940,11 @@ file-type@^17.1.1:
|
|||
strtok3 "^7.0.0-alpha.7"
|
||||
token-types "^5.0.0-alpha.2"
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
|
||||
|
||||
fill-range@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||
|
@ -4435,15 +4447,6 @@ humanize-ms@^1.2.1:
|
|||
dependencies:
|
||||
ms "^2.0.0"
|
||||
|
||||
ico-to-png@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ico-to-png/-/ico-to-png-0.2.1.tgz#d159356bbd9cc042670cf6eae80f45fcc729fd75"
|
||||
integrity sha512-wP2Jmsj9ZMxi5fIv3VrcQ9w7vmUu4r6ocfMgeDwoHkzG50sY/LYsZcXEZypaD4FkMdjGQU9klNVzxQMMF6rYBw==
|
||||
dependencies:
|
||||
decode-ico "^0.4.0"
|
||||
lodepng "^2.0.0"
|
||||
resize-image-data "^0.3.0"
|
||||
|
||||
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
|
@ -5038,13 +5041,6 @@ lodash@4.17.21, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21:
|
|||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
lodepng@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/lodepng/-/lodepng-2.1.0.tgz#64e9f09a3566658ea4a2abf7615df77a48b805b2"
|
||||
integrity sha512-7qzjpeyCG7HOFuwXfQjvGP7/NbZTUaDVoExPSoQgm+15J8gUADQ/8f9BD0C0u7G82QIxc+YGaTBf0Zz9RheyKA==
|
||||
dependencies:
|
||||
"@canvas/image-data" "^1.0.0"
|
||||
|
||||
log-symbols@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
|
||||
|
@ -6486,6 +6482,13 @@ punycode@^2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
qoi-img@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/qoi-img/-/qoi-img-1.0.1.tgz#fc82aceb1b08362016b48f6a267345670d1841fd"
|
||||
integrity sha512-30MqNNNnaS2bm1xLAF4ZM2Utz1DEC12OT9qxh1+C57qliGhFUY7MxHtvGXN+Ft9CUwVfLch6OyU6oqzaGL2Ajg==
|
||||
dependencies:
|
||||
bindings "^1.5.0"
|
||||
|
||||
qs@6.9.7:
|
||||
version "6.9.7"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe"
|
||||
|
@ -6679,13 +6682,6 @@ requires-port@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||
|
||||
resize-image-data@^0.3.0:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/resize-image-data/-/resize-image-data-0.3.1.tgz#2ad715721b4927c18d07fb657c01844ae7c75a0c"
|
||||
integrity sha512-6hVRn2S6W1cdycreA6Vth5XRN2NnGs7/RnVpxNw/1OCK8aCoevRFH2WprmQRZDnnH3e6awLv2tTIPuv7/7xeGg==
|
||||
dependencies:
|
||||
"@canvas/image-data" "^1.0.0"
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
|
@ -7745,9 +7741,9 @@ uuid@8.3.2, uuid@^8.3.2:
|
|||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
v8-compile-cache-lib@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8"
|
||||
integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
|
||||
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
|
||||
|
||||
v8-compile-cache@^2.0.3:
|
||||
version "2.3.0"
|
||||
|
|
Loading…
Reference in New Issue