mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
refactor: move fit to size function
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<Blob>;
|
||||
|
||||
/** 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<Blob> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<Blob> {
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user