From 4ec2b418fd001bf0c0d95534cd38988b6a44cd1d Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sat, 22 Aug 2026 09:29:56 +0500 Subject: [PATCH] feat: core types and io --- web/src/lib/core/io.ts | 71 +++++++++++++++++++++++++++++++++++++++ web/src/lib/core/types.ts | 13 +++++++ 2 files changed, 84 insertions(+) create mode 100644 web/src/lib/core/io.ts create mode 100644 web/src/lib/core/types.ts diff --git a/web/src/lib/core/io.ts b/web/src/lib/core/io.ts new file mode 100644 index 0000000..d3e31b9 --- /dev/null +++ b/web/src/lib/core/io.ts @@ -0,0 +1,71 @@ +import { type PixelImage } from './types'; + +export type OutputMime = 'image/png' | 'image/jpeg' | 'image/webp'; + +export const ACCEPTED_IMAGE_TYPES = + 'image/png,image/jpeg,image/webp,image/gif,image/bmp,image/x-icon'; + +export async function decodeFile(file: File): Promise { + const bitmap = await createImageBitmap(file); + try { + const canvas = document.createElement('canvas'); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + const ctx = canvas.getContext('2d', { willReadFrequently: true }); + if (!ctx) { + throw new Error('Canvas 2D context недоступен в этом браузере'); + } + ctx.drawImage(bitmap, 0, 0); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + return { width: imageData.width, height: imageData.height, data: imageData.data }; + } finally { + bitmap.close(); + } +} + +export async function encode( + img: PixelImage, + mime: OutputMime = 'image/png', + quality?: number +): Promise { + if (mime === 'image/jpeg' || mime === 'image/webp') { + if (quality !== undefined && (quality < 0 || quality > 1)) { + throw new RangeError('quality должен быть в диапазоне 0..1'); + } + } + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Canvas 2D context недоступен в этом браузере'); + } + ctx.putImageData(new ImageData(img.data, img.width, img.height), 0, 0); + return await canvasToBlob(canvas, mime, quality); +} + +function canvasToBlob(canvas: HTMLCanvasElement, mime: OutputMime, quality?: number): Promise { + return new Promise((resolve, reject) => { + canvas.toBlob( + (blob) => (blob ? resolve(blob) : reject(new Error(`Браузер не поддерживает кодирование в ${mime}`))), + mime, + quality + ); + }); +} + +export function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.append(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); +} + +export function replaceExtension(filename: string, ext: string): string { + const base = filename.replace(/\.[^./\\]+$/, ''); + return `${base}.${ext}`; +} diff --git a/web/src/lib/core/types.ts b/web/src/lib/core/types.ts new file mode 100644 index 0000000..1a2fae0 --- /dev/null +++ b/web/src/lib/core/types.ts @@ -0,0 +1,13 @@ +export type PixelImage = { + width: number; + height: number; + data: Uint8ClampedArray; +}; + +export function createPixelImage(width: number, height: number): PixelImage { + return { + width, + height, + data: new Uint8ClampedArray(width * height * 4) + }; +}