From 0b929eced2980f7509263873f2ceb79cd87dbf52 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sat, 10 Oct 2026 06:50:07 +0500 Subject: [PATCH] refactor: move fit to size function --- docs/architecture-audit-2026-10.md | 12 ++++--- .../schema/schema-tool-runner.svelte.ts | 10 ++---- web/src/lib/core/compress.ts | 34 +++++++++++++++++++ web/src/lib/core/io.ts | 28 --------------- 4 files changed, 44 insertions(+), 40 deletions(-) diff --git a/docs/architecture-audit-2026-10.md b/docs/architecture-audit-2026-10.md index 58bebb5..fd4c254 100644 --- a/docs/architecture-audit-2026-10.md +++ b/docs/architecture-audit-2026-10.md @@ -91,12 +91,14 @@ aligned, ontogglealign). mime-контракт `OutputMime`; сам `io.ts` таблицу не использовал. Потребители (4 файла) переведены на новый путь. -### M6. Доменная политика сжатия в IO-модуле +### M6. Доменная политика сжатия в IO-модуле — СДЕЛАНО (2026-10) -`web/src/lib/core/io.ts:191-210`: `fitWithinBytes` — стратегия «бинарный поиск -по quality / по числу цветов квантизации» поверх `quantizeImage`, -`findMaxColorsWithin`, `findQualityWithin`. Это доменная логика, не ввод-вывод -(у файла 19 экспортов). +`fitWithinBytes` перенесён из `core/io.ts` в `core/compress.ts` (дом стратегий +сжатия). Сигнатура с инъекцией: +`fitWithinBytes(img, mime, targetBytes, quality, encodeFn)` — вызывающий +(`schema-tool-runner`) передаёт `io.encode`, поэтому `compress.ts` остаётся +DOM-free и node-тестируемым (попутно `fitWithinBytes` теперь можно +юнит-тестировать с фейковым encode). `io.ts` вернулся к чистому IO. ### M7. `clamp` продублирован 5 раз — СДЕЛАНО (2026-10) diff --git a/web/src/lib/components/schema/schema-tool-runner.svelte.ts b/web/src/lib/components/schema/schema-tool-runner.svelte.ts index 9aaa633..d16067c 100644 --- a/web/src/lib/components/schema/schema-tool-runner.svelte.ts +++ b/web/src/lib/components/schema/schema-tool-runner.svelte.ts @@ -1,10 +1,6 @@ +import { fitWithinBytes } from "$lib/core/compress"; import { ToolError, type ErrorVars } from "$lib/core/errors"; -import { - downloadBlob, - encode, - fitWithinBytes, - type OutputMime, -} from "$lib/core/io"; +import { downloadBlob, encode, type OutputMime } from "$lib/core/io"; import { outputFormatByMime } from "$lib/output-formats"; import type { PixelImage } from "$lib/core/types"; import { execute } from "$lib/executor"; @@ -155,7 +151,7 @@ async function download(s: RunnerState, ctx: SchemaToolRunnerCtx) { const limit = ctx.limitKb(); const quality = q !== undefined ? q / 100 : undefined; const blob = limit - ? await fitWithinBytes(s.result, out.mime, limit * 1024, quality) + ? await fitWithinBytes(s.result, out.mime, limit * 1024, quality, encode) : await encode(s.result, out.mime, quality); downloadBlob(blob, `${ctx.pageSlug()}.${out.ext}`); } catch (e) { diff --git a/web/src/lib/core/compress.ts b/web/src/lib/core/compress.ts index fc5b8c0..e7665e9 100644 --- a/web/src/lib/core/compress.ts +++ b/web/src/lib/core/compress.ts @@ -1,3 +1,7 @@ +import type { OutputMime } from "./io"; +import { quantizeImage } from "./quantize"; +import type { PixelImage } from "./types"; + /** Compression presets: level -> number of quantization colors. */ export const COMPRESSION_LEVELS = { light: 192, @@ -80,3 +84,33 @@ export async function findQualityWithin( } return ok; } + +export type EncodeFn = ( + img: PixelImage, + mime: OutputMime, + quality?: number, +) => Promise; + +/** Encodes img to fit targetBytes (quality search for lossy, color count for + * PNG; BMP as-is). `encodeFn` injected (core/io.encode): policy stays DOM-free. */ +export async function fitWithinBytes( + img: PixelImage, + mime: OutputMime, + targetBytes: number, + quality: number | undefined, + encodeFn: EncodeFn, +): Promise { + if (mime === "image/bmp") return encodeFn(img, mime); + const initial = await encodeFn(img, mime, quality); + if (initial.size <= targetBytes) return initial; + if (mime === "image/png") { + const encodeSize = async (k: number) => + (await encodeFn(quantizeImage(img, k).image, mime)).size; + const k = await findMaxColorsWithin(targetBytes, 256, encodeSize); + return encodeFn(quantizeImage(img, k).image, mime); + } + const encodeSize = async (q: number) => + (await encodeFn(img, mime, q / 100)).size; + const q = await findQualityWithin(targetBytes, encodeSize); + return encodeFn(img, mime, q / 100); +} diff --git a/web/src/lib/core/io.ts b/web/src/lib/core/io.ts index 6a6c0bc..f53a7bf 100644 --- a/web/src/lib/core/io.ts +++ b/web/src/lib/core/io.ts @@ -1,7 +1,5 @@ import { encodeBmpBytes } from "./bmp"; -import { findMaxColorsWithin, findQualityWithin } from "./compress"; import { ToolError } from "./errors"; -import { quantizeImage } from "./quantize"; import { type PixelImage } from "./types"; export type OutputMime = @@ -144,32 +142,6 @@ function canvasToBlob( }); } -/** - * Encodes img to fit targetBytes: binary search over quality for lossy - * formats, over quantization colors for PNG. BMP has no compression knobs -- - * returned as-is (the caller warns the user the limit does not apply). - */ -export async function fitWithinBytes( - img: PixelImage, - mime: OutputMime, - targetBytes: number, - quality?: number, -): Promise { - if (mime === "image/bmp") return encode(img, mime); - const initial = await encode(img, mime, quality); - if (initial.size <= targetBytes) return initial; - if (mime === "image/png") { - const encodeSize = async (k: number) => - (await encode(quantizeImage(img, k).image, mime)).size; - const k = await findMaxColorsWithin(targetBytes, 256, encodeSize); - return encode(quantizeImage(img, k).image, mime); - } - const encodeSize = async (q: number) => - (await encode(img, mime, q / 100)).size; - const q = await findQualityWithin(targetBytes, encodeSize); - return encode(img, mime, q / 100); -} - export function downloadBlob(blob: Blob, filename: string): void { const url = URL.createObjectURL(blob); const a = document.createElement("a");