Skip to content

Instantly share code, notes, and snippets.

@maradondt
Last active September 26, 2024 14:51
Show Gist options
  • Save maradondt/b44c7e194aa5bc3a2ea7945f751841e0 to your computer and use it in GitHub Desktop.
Save maradondt/b44c7e194aa5bc3a2ea7945f751841e0 to your computer and use it in GitHub Desktop.
Helper for work with images
import dayjs from 'dayjs';
import { instanceOf } from '../typeguard';
export type Coordinates = { x1: number; y1: number; x2: number; y2: number };
const ResultType = {
blob: 'blob',
file: 'file',
url: 'url',
image: 'image',
} as const;
type ResultType = typeof ResultType[keyof typeof ResultType];
type ResultMap = {
blob: Blob;
file: File;
url: string;
image: HTMLImageElement;
};
type ResultDispatcher<T extends ResultType> = ResultMap[T];
type ImageSource = string | CanvasImageSource;
export class ImageProcessor {
private static fileType = 'image/png';
private static fileExt = 'png';
/**
* Метод для создания спрайта из массива изображений
* @param imagesUrls urls of images to create a sprite
* @returns
*/
public static async createSprite<T extends ResultType = 'blob'>(
imagesUrls: string[],
type = 'blob' as T
) {
let totalWidth = 0;
let maxHeight = 0;
const imagesPromises = imagesUrls.map((url) => {
return this.createImage(url).then((img) => {
totalWidth += img.width;
maxHeight = Math.max(maxHeight, img.height);
return img;
});
});
const images = await Promise.all(imagesPromises);
const { canvas, context } = this.createCanvas(totalWidth, maxHeight);
let currentX = 0;
const coordinates: Coordinates[] = [];
images.forEach((img) => {
context.drawImage(img, currentX, 0);
coordinates.push({
x1: currentX,
y1: 0,
x2: currentX + img.width,
y2: img.height,
});
currentX += img.width;
});
const sprite = await this.converter(canvas, type);
return { sprite, coordinates };
}
public static async pickImage<T extends ResultType = 'blob'>(
sources: ImageSource | ImageSource[],
coordinates: Coordinates,
type = 'blob' as T
) {
const { x1, y1, x2, y2 } = coordinates;
const width = x2 - x1;
const height = y2 - y1;
const { canvas: clipCanvas, context: clipContext } = this.createCanvas(width, height);
const arrayOfSources = Array.isArray(sources) ? sources : [sources];
for (const _source of arrayOfSources) {
const source = await this.handleSource(_source);
clipContext.drawImage(source, x1, y1, width, height, 0, 0, width, height);
}
return this.converter(clipCanvas, type);
}
public static async resize<T extends ResultType = 'blob'>(
_source: ImageSource,
width: number,
height: number,
type = 'blob' as T
) {
const { canvas: clipCanvas, context: clipContext } = this.createCanvas(width, height);
const source = await this.handleSource(_source);
clipContext.drawImage(source, 0, 0, width, height);
return this.converter(clipCanvas, type);
}
public static async converter<T extends ResultType>(
source: Blob | HTMLImageElement | string | HTMLCanvasElement,
type: T
): Promise<ResultDispatcher<T>> {
let blob: Blob | null = null;
if (instanceOf(Blob)(source)) {
blob = source;
}
if (typeof source === 'string') {
blob = await fetch(source).then((r) => r.blob());
}
if (instanceOf(HTMLImageElement)(source)) {
blob = await fetch(source.src).then((r) => r.blob());
}
if (instanceOf(HTMLCanvasElement)(source)) {
blob = await this.getImgBlobFromCanvas(source);
}
if (!blob) throw new Error('Blob create Error');
switch (type) {
case 'image':
return this.createImage(this.blobToUrl(blob)) as Promise<ResultDispatcher<T>>;
case 'file':
return Promise.resolve(this.blobToFile(blob)) as Promise<ResultDispatcher<T>>;
case 'url':
return Promise.resolve(this.blobToUrl(blob)) as Promise<ResultDispatcher<T>>;
case 'blob':
default:
return Promise.resolve(blob) as Promise<ResultDispatcher<T>>;
}
}
public static async save(source: Blob | HTMLImageElement | string | HTMLCanvasElement) {
const url = await this.converter(source, 'url');
window.open(url, '__blank');
}
/**
* Increase file size
*/
public static async addSizeToFile(
source: Blob | HTMLImageElement | string | HTMLCanvasElement,
minSize = 1024
): Promise<File> {
const file = await this.converter(source, 'file');
if (file.size >= minSize) return file;
const fileIncreasedSize = await new Promise<File>((resolve, reject) => {
const reader = new FileReader();
reader.onload = function (event) {
const arrayBuffer = event.target?.result;
if (!instanceOf(ArrayBuffer)(arrayBuffer)) {
return reject(new Error('File could not be read'));
}
const currentSize = arrayBuffer?.byteLength;
if (currentSize >= minSize) {
return resolve(file);
}
const paddingSize = minSize - currentSize;
const resultArray = new Uint8Array(minSize);
resultArray.set(new Uint8Array(arrayBuffer), 0);
resultArray.set(new Uint8Array(paddingSize).fill(0, paddingSize), arrayBuffer.byteLength);
const newBlob = new Blob([resultArray], { type: file.type });
const newFile = new File([newBlob], file.name, { type: file.type });
resolve(newFile);
};
reader.onerror = function () {
reject(new Error('File could not be read'));
};
reader.readAsArrayBuffer(file);
});
return this.converter(fileIncreasedSize, 'file');
}
private static getImgBlobFromCanvas<T extends ResultType = 'blob'>(
canvas: HTMLCanvasElement,
type = 'blob' as T
) {
return new Promise<Blob>((res, rej) => {
canvas.toBlob(
(blob) => {
if (!blob) {
return rej(new Error('Sprite creating blob error'));
}
res(blob);
},
this.fileType,
1
);
}).then((blob) => this.converter(blob, type));
}
private static createImage(url: string) {
return new Promise<HTMLImageElement>((res, rej) => {
const img = new Image();
img.src = url;
img.onload = () => {
res(img);
};
img.onerror = () => {
rej(new Error('Unable to render image'));
};
});
}
private static blobToFile(blob: Blob) {
return new File([blob], this.getFileName(), {
type: this.fileType,
});
}
private static blobToUrl(blob: Blob) {
return URL.createObjectURL(blob);
}
private static getFileName() {
return `sprite_${dayjs().format('L LT')}.${this.fileExt}`;
}
private static createCanvas(width: number, height: number) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Unable to get canvas context');
}
return { canvas, context };
}
private static handleSource(source: ImageSource) {
if (typeof source === 'string') {
return this.createImage(source);
}
return Promise.resolve(source);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment