Files
ttstd-infinite-canvas/web/src/lib/image-size-presets.ts

98 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { buildImageResolutionOptions, type ImageResolutionOption, type ImageResolutionTier } from "./image-resolution-tiers";
import type { ImageCapabilityConfig } from "./model-capabilities";
export const IMAGE_RESOLUTIONS: ImageResolutionTier[] = ["1k", "2k", "4k"];
export const IMAGE_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9"];
const standardSizes: Record<string, string[]> = {
"1:1": ["1024x1024", "2048x2048", "2880x2880"],
"16:9": ["1824x1024", "2752x1536", "3840x2160"],
"9:16": ["1024x1824", "1536x2752", "2160x3840"],
"4:3": ["1360x1024", "2304x1728", "3264x2448"],
"3:4": ["1024x1360", "1728x2304", "2448x3264"],
"3:2": ["1536x1024", "2496x1664", "3504x2336"],
"2:3": ["1024x1536", "1664x2496", "2336x3504"],
"4:5": ["1024x1280", "1792x2240", "2560x3200"],
"5:4": ["1280x1024", "2240x1792", "3200x2560"],
"21:9": ["2048x878", "3136x1344", "3808x1632"],
};
export function imagePresetForRatio(tier: ImageResolutionTier, input: string): ImageResolutionOption {
const match = input.trim().match(/^(\d{1,5})\s*[::]\s*(\d{1,5})$/);
if (!match) throw new Error("请输入宽高比,例如 16:9");
let w = Number(match[1]),
h = Number(match[2]);
if (!w || !h || Math.max(w, h) / Math.min(w, h) > 3) throw new Error("宽高比必须为正数,且不超过 3:1");
let a = w,
b = h;
while (b) [a, b] = [b, a % b];
w /= a;
h /= a;
const ratio =
Object.keys(standardSizes).find((value) => {
const [width, height] = value.split(":").map(Number);
return width * h === height * w;
}) || `${w}:${h}`;
const index = IMAGE_RESOLUTIONS.indexOf(tier);
if (index < 0) throw new Error("分辨率仅支持 1K、2K、4K");
const standard = standardSizes[ratio]?.[index];
const pixels = [1_048_576, 4_194_304, 8_294_400][index];
let width = Math.round(Math.sqrt((pixels * w) / h) / 16) * 16;
let height = Math.round((width * h) / w / 16) * 16;
while (width * height > pixels || Math.max(width, height) > 3840) {
if (width >= height) {
width -= 16;
height = Math.round((width * h) / w / 16) * 16;
} else {
height -= 16;
width = Math.round((height * w) / h / 16) * 16;
}
}
if (standard) [width, height] = standard.split("x").map(Number);
return { tier, ratio, width, height, size: `${width}x${height}` };
}
export function imageQualityForTier(profile: ImageCapabilityConfig, tier: ImageResolutionTier) {
const aliases = { "1k": ["1k", "low"], "2k": ["2k", "medium"], "4k": ["4k", "high"] }[tier];
return profile.quality.supported ? profile.quality.values.find((value) => aliases.includes(value.toLowerCase())) : undefined;
}
export function imageResolutionUsesQuality(profile: ImageCapabilityConfig) {
return profile.size.parameter === "aspect_ratio" && IMAGE_RESOLUTIONS.some((tier) => imageQualityForTier(profile, tier));
}
export function imageTierAvailable(profile: ImageCapabilityConfig, tier: ImageResolutionTier) {
// 比例协议必须能把档位传给上游;允许自定义比例不等于支持任意分辨率。
if (profile.size.parameter !== "aspect_ratio") return profile.size.parameter !== "none";
return imageResolutionUsesQuality(profile) ? Boolean(imageQualityForTier(profile, tier)) : tier === "1k";
}
export function imageSizePresets(profile: ImageCapabilityConfig): ImageResolutionOption[] {
if (profile.size.parameter === "none") return [];
if (profile.size.presets) return profile.size.presets;
const pixels = buildImageResolutionOptions(profile.size.values);
const tiers = profile.size.parameter === "aspect_ratio" ? IMAGE_RESOLUTIONS.filter((tier) => imageQualityForTier(profile, tier)) : ["1k" as const];
const ratios = (tiers.length ? tiers : ["1k" as const]).flatMap((tier) =>
profile.size.values.flatMap((ratio) => {
try {
const preset = imagePresetForRatio(tier, ratio);
return pixels.some((pixel) => pixel.ratio === preset.ratio) ? [] : [preset];
} catch {
return [];
}
}),
);
return [...pixels, ...ratios];
}
export function imagePresetValue(profile: ImageCapabilityConfig, preset: ImageResolutionOption) {
if (!profile.size.presets && !profile.size.allowCustom && preset.tier === "1k" && profile.size.values.includes(preset.ratio) && !profile.size.values.includes(preset.size)) return preset.ratio;
return profile.size.parameter === "aspect_ratio" ? preset.ratio : preset.size;
}
export function imageSizeConfigWithPresets(profile: ImageCapabilityConfig, presets: ImageResolutionOption[]): ImageCapabilityConfig["size"] {
const values = [...new Set(presets.map((preset) => (profile.size.parameter === "aspect_ratio" ? preset.ratio : preset.size)))];
if (profile.size.values.includes("auto")) values.unshift("auto");
if (!values.length) values.push("auto");
return { ...profile.size, presets, values, default: values.includes(profile.size.default) ? profile.size.default : values[0] || "auto" };
}