Last active
September 26, 2024 14:51
-
-
Save maradondt/b44c7e194aa5bc3a2ea7945f751841e0 to your computer and use it in GitHub Desktop.
Helper for work with images
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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