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
|
mime-контракт `OutputMime`; сам `io.ts` таблицу не использовал. Потребители (4
|
||||||
файла) переведены на новый путь.
|
файла) переведены на новый путь.
|
||||||
|
|
||||||
### M6. Доменная политика сжатия в IO-модуле
|
### M6. Доменная политика сжатия в IO-модуле — СДЕЛАНО (2026-10)
|
||||||
|
|
||||||
`web/src/lib/core/io.ts:191-210`: `fitWithinBytes` — стратегия «бинарный поиск
|
`fitWithinBytes` перенесён из `core/io.ts` в `core/compress.ts` (дом стратегий
|
||||||
по quality / по числу цветов квантизации» поверх `quantizeImage`,
|
сжатия). Сигнатура с инъекцией:
|
||||||
`findMaxColorsWithin`, `findQualityWithin`. Это доменная логика, не ввод-вывод
|
`fitWithinBytes(img, mime, targetBytes, quality, encodeFn)` — вызывающий
|
||||||
(у файла 19 экспортов).
|
(`schema-tool-runner`) передаёт `io.encode`, поэтому `compress.ts` остаётся
|
||||||
|
DOM-free и node-тестируемым (попутно `fitWithinBytes` теперь можно
|
||||||
|
юнит-тестировать с фейковым encode). `io.ts` вернулся к чистому IO.
|
||||||
|
|
||||||
### M7. `clamp` продублирован 5 раз — СДЕЛАНО (2026-10)
|
### M7. `clamp` продублирован 5 раз — СДЕЛАНО (2026-10)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
|
import { fitWithinBytes } from "$lib/core/compress";
|
||||||
import { ToolError, type ErrorVars } from "$lib/core/errors";
|
import { ToolError, type ErrorVars } from "$lib/core/errors";
|
||||||
import {
|
import { downloadBlob, encode, type OutputMime } from "$lib/core/io";
|
||||||
downloadBlob,
|
|
||||||
encode,
|
|
||||||
fitWithinBytes,
|
|
||||||
type OutputMime,
|
|
||||||
} from "$lib/core/io";
|
|
||||||
import { outputFormatByMime } from "$lib/output-formats";
|
import { outputFormatByMime } from "$lib/output-formats";
|
||||||
import type { PixelImage } from "$lib/core/types";
|
import type { PixelImage } from "$lib/core/types";
|
||||||
import { execute } from "$lib/executor";
|
import { execute } from "$lib/executor";
|
||||||
@@ -155,7 +151,7 @@ async function download(s: RunnerState, ctx: SchemaToolRunnerCtx) {
|
|||||||
const limit = ctx.limitKb();
|
const limit = ctx.limitKb();
|
||||||
const quality = q !== undefined ? q / 100 : undefined;
|
const quality = q !== undefined ? q / 100 : undefined;
|
||||||
const blob = limit
|
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);
|
: await encode(s.result, out.mime, quality);
|
||||||
downloadBlob(blob, `${ctx.pageSlug()}.${out.ext}`);
|
downloadBlob(blob, `${ctx.pageSlug()}.${out.ext}`);
|
||||||
} catch (e) {
|
} 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. */
|
/** Compression presets: level -> number of quantization colors. */
|
||||||
export const COMPRESSION_LEVELS = {
|
export const COMPRESSION_LEVELS = {
|
||||||
light: 192,
|
light: 192,
|
||||||
@@ -80,3 +84,33 @@ export async function findQualityWithin(
|
|||||||
}
|
}
|
||||||
return ok;
|
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 { encodeBmpBytes } from "./bmp";
|
||||||
import { findMaxColorsWithin, findQualityWithin } from "./compress";
|
|
||||||
import { ToolError } from "./errors";
|
import { ToolError } from "./errors";
|
||||||
import { quantizeImage } from "./quantize";
|
|
||||||
import { type PixelImage } from "./types";
|
import { type PixelImage } from "./types";
|
||||||
|
|
||||||
export type OutputMime =
|
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 {
|
export function downloadBlob(blob: Blob, filename: string): void {
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
|
|||||||
Reference in New Issue
Block a user