feat: add text and generation tools

This commit is contained in:
2026-08-26 16:08:27 +05:00
parent 56ebe5b53b
commit 2b67414e79
14 changed files with 734 additions and 22 deletions
+89
View File
@@ -0,0 +1,89 @@
import { ToolError } from './errors';
import type { PixelImage } from './types';
import { createPixelImage } from './types';
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
/** Строки вида «r g b a» по одной строке на ряд пикселей. */
export function imageToByteRows(img: PixelImage): string {
const rows: string[] = [];
for (let y = 0; y < img.height; y++) {
const parts: string[] = [];
for (let x = 0; x < img.width; x++) {
const i = (y * img.width + x) * 4;
parts.push(`${img.data[i]} ${img.data[i + 1]} ${img.data[i + 2]} ${img.data[i + 3]}`);
}
rows.push(parts.join(' '));
}
return rows.join('\n');
}
export function bytesToImage(text: string, width: number): PixelImage {
const nums = (text.match(/-?\d+/g) ?? []).map(Number);
if (nums.length % 4 !== 0) {
throw new ToolError('errors.bytesCount', { count: nums.length });
}
if (nums.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {
throw new ToolError('errors.byteRange');
}
const w = Math.trunc(width);
if (!Number.isInteger(w) || w < 1) throw new ToolError('errors.widthInt');
if (nums.length / 4 % w !== 0) {
throw new ToolError('errors.pixelCountMismatch', { count: nums.length / 4, width: w });
}
const out = createPixelImage(w, nums.length / 4 / w);
out.data.set(nums);
return out;
}
/** Строки вида «rgba(r, g, b, a)», по одной на пиксель, ряды через перевод строки. */
export function imageToRgbValues(img: PixelImage): string {
const rows: string[] = [];
for (let y = 0; y < img.height; y++) {
const parts: string[] = [];
for (let x = 0; x < img.width; x++) {
const i = (y * img.width + x) * 4;
parts.push(`rgba(${img.data[i]}, ${img.data[i + 1]}, ${img.data[i + 2]}, ${img.data[i + 3]})`);
}
rows.push(parts.join(' '));
}
return rows.join('\n');
}
export function rgbValuesToImage(text: string, width: number): PixelImage {
const nums = (text.match(/-?\d+(?:\.\d+)?/g) ?? []).map(Number);
if (nums.length % 4 !== 0) {
throw new ToolError('errors.bytesCount', { count: nums.length });
}
if (nums.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {
throw new ToolError('errors.byteRange');
}
const w = Math.trunc(width);
if (!Number.isInteger(w) || w < 1) throw new ToolError('errors.widthInt');
const pxCount = nums.length / 4;
if (pxCount % w !== 0) {
throw new ToolError('errors.pixelCountMismatch', { count: pxCount, width: w });
}
const out = createPixelImage(w, pxCount / w);
out.data.set(nums);
return out;
}
/** Снимает префикс data-uri (data:image/...;base64,) при наличии. */
export function stripDataUri(text: string): string {
const m = /^\s*data:[^,\s]*base64,/i.exec(text);
return m ? text.slice(m[0].length) : text.trim();
}
export function base64ToBytes(text: string): Uint8Array {
const clean = text.replace(/\s+/g, '');
const binary = atob(clean);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes;
}
export function looksLikePng(bytes: Uint8Array): boolean {
if (bytes.length < PNG_SIGNATURE.length) return false;
return PNG_SIGNATURE.every((v, i) => bytes[i] === v);
}