export type ImageResolutionTier = "1k" | "2k" | "4k"; export type ImageSizePreviewSource = "account_pool" | "image_api"; export type ImageSizePreview = { width: number; height: number; value: string; label: string; }; const MAX_4K_EDGE = 3840; const MAX_4K_PIXELS = 8_294_400; const MAX_2K_EDGE = 2048; const MAX_STANDARD_EDGE = 1024; const ACCOUNT_POOL_TARGET_PIXELS = 1536 * 1024; const SIZE_STEP = 8; const FOUR_K_SIZE_STEP = 16; const FOUR_K_MIN_EDGE = 16; const FOUR_K_MIN_PIXELS = 655_360; export const MAX_CUSTOM_ASPECT_RATIO = 3; function floorToStep(value: number, step: number) { return Math.max(step, Math.floor(value / step) * step); } function parseExplicitSize(value: string): [number, number] | null { const match = String(value || "").trim().match(/^(\d{2,5})\s*x\s*(\d{2,5})$/i); if (!match) { return null; } return [Number(match[1]), Number(match[2])]; } export function normalizeAspectRatioInput(value: string) { return String(value || "") .trim() .replace(/[:]/g, ":") .replace(/[/]/g, "/") .replace(/[×✕]/g, "x") .replace(/\s+/g, "") .replace(/[xX/]/g, ":"); } export function parseAspectRatio(value: string): [number, number] | null { const match = normalizeAspectRatioInput(value).match(/^([1-9]\d{0,4}):([1-9]\d{0,4})$/); if (!match) { return null; } return [Number(match[1]), Number(match[2])]; } export function isSupportedAspectRatio(value: string) { const ratio = parseAspectRatio(value); if (!ratio) { return false; } const longSide = Math.max(...ratio); const shortSide = Math.min(...ratio); return longSide / shortSide <= MAX_CUSTOM_ASPECT_RATIO; } function fitDimensionsToEdge(width: number, height: number, maxEdge: number, forceStep = false) { const safeWidth = Math.max(1, Math.round(width || 1)); const safeHeight = Math.max(1, Math.round(height || 1)); const scale = Math.min(1, maxEdge / Math.max(safeWidth, safeHeight)); if (scale < 1 || forceStep) { return { width: floorToStep(safeWidth * scale, SIZE_STEP), height: floorToStep(safeHeight * scale, SIZE_STEP), }; } return { width: safeWidth, height: safeHeight }; } function fitRatioToEdge(widthRatio: number, heightRatio: number, maxEdge: number) { const scale = maxEdge / Math.max(widthRatio, heightRatio); return fitDimensionsToEdge( Math.round(widthRatio * scale), Math.round(heightRatio * scale), maxEdge, true, ); } function fitRatioToPixelArea(widthRatio: number, heightRatio: number, targetPixels: number) { const scale = Math.sqrt(targetPixels / (widthRatio * heightRatio)); return { width: Math.max(1, Math.round(widthRatio * scale)), height: Math.max(1, Math.round(heightRatio * scale)), }; } function accountPoolPresetDimensions(widthRatio: number, heightRatio: number) { if (widthRatio === heightRatio) { return { width: 1254, height: 1254 }; } if (widthRatio === heightRatio * 3) { return { width: 2138, height: 736, label: "约 1881-2138 × 736-836" }; } if (widthRatio * 3 === heightRatio) { return { width: 736, height: 2135 }; } if (widthRatio === heightRatio * 2) { return { width: 1774, height: 887 }; } if (widthRatio * 2 === heightRatio) { return { width: 887, height: 1774 }; } if (widthRatio * 2 === heightRatio * 3) { return { width: 1536, height: 1024 }; } if (widthRatio * 3 === heightRatio * 2) { return { width: 1024, height: 1536 }; } if (widthRatio * 4 === heightRatio * 5) { return { width: 1402, height: 1122 }; } if (widthRatio * 5 === heightRatio * 4) { return { width: 1122, height: 1402 }; } if (widthRatio * 9 === heightRatio * 16) { return { width: 1672, height: 941 }; } if (widthRatio * 3 === heightRatio * 4) { return { width: 1448, height: 1086 }; } if (widthRatio * 4 === heightRatio * 3) { return { width: 1086, height: 1448 }; } if (widthRatio * 16 === heightRatio * 9) { return { width: 941, height: 1672 }; } return null; } function accountPoolDimensions(widthRatio: number, heightRatio: number): ImageSizePreview { const preset = accountPoolPresetDimensions(widthRatio, heightRatio); const dimensions = preset || fitRatioToPixelArea(widthRatio, heightRatio, ACCOUNT_POOL_TARGET_PIXELS); let label = preset?.label || `${dimensions.width} × ${dimensions.height}`; // Arbitrary ratios are rounded by the upstream image tool and can differ by one // pixel on the long edge. The range reflects repeated real account-pool runs. if (!preset && dimensions.width > dimensions.height) { label = `${dimensions.width - 1}-${dimensions.width} × ${dimensions.height}`; } else if (!preset && dimensions.height > dimensions.width) { label = `${dimensions.width} × ${dimensions.height - 1}-${dimensions.height}`; } return { ...dimensions, value: `${dimensions.width}x${dimensions.height}`, label, }; } function isValid4kDimensions(width: number, height: number) { if ( width < FOUR_K_MIN_EDGE || height < FOUR_K_MIN_EDGE || width > MAX_4K_EDGE || height > MAX_4K_EDGE || width % FOUR_K_SIZE_STEP !== 0 || height % FOUR_K_SIZE_STEP !== 0 ) { return false; } const pixels = width * height; if (pixels < FOUR_K_MIN_PIXELS || pixels > MAX_4K_PIXELS) { return false; } return Math.max(width, height) / Math.max(1, Math.min(width, height)) <= MAX_CUSTOM_ASPECT_RATIO; } function fit4kDimensions(width: number, height: number) { let nextWidth = Math.max(1, Math.round(width || 1)); let nextHeight = Math.max(1, Math.round(height || 1)); const scale = Math.min( 1, MAX_4K_EDGE / Math.max(nextWidth, nextHeight), Math.sqrt(MAX_4K_PIXELS / (nextWidth * nextHeight)), ); if (scale < 1) { nextWidth = floorToStep(nextWidth * scale, FOUR_K_SIZE_STEP); nextHeight = floorToStep(nextHeight * scale, FOUR_K_SIZE_STEP); } while (Math.max(nextWidth, nextHeight) > MAX_4K_EDGE || nextWidth * nextHeight > MAX_4K_PIXELS) { if (nextWidth >= nextHeight && nextWidth > FOUR_K_SIZE_STEP) { nextWidth -= FOUR_K_SIZE_STEP; } else if (nextHeight > FOUR_K_SIZE_STEP) { nextHeight -= FOUR_K_SIZE_STEP; } else { break; } } return { width: nextWidth, height: nextHeight }; } function fit4kRatio(widthRatio: number, heightRatio: number) { const ratio = widthRatio / heightRatio; const landscape = widthRatio >= heightRatio; const theoreticalLongEdge = landscape ? Math.sqrt(MAX_4K_PIXELS * ratio) : Math.sqrt(MAX_4K_PIXELS / ratio); let longEdge = floorToStep(Math.min(MAX_4K_EDGE, theoreticalLongEdge), FOUR_K_SIZE_STEP); while (longEdge >= FOUR_K_MIN_EDGE) { const width = landscape ? longEdge : floorToStep((longEdge * widthRatio) / heightRatio, FOUR_K_SIZE_STEP); const height = landscape ? floorToStep((longEdge * heightRatio) / widthRatio, FOUR_K_SIZE_STEP) : longEdge; if (isValid4kDimensions(width, height)) { return { width, height }; } longEdge -= FOUR_K_SIZE_STEP; } return null; } function closestSupportedSize( canonical: { width: number; height: number }, supportedSizes: string[], resolution: ImageResolutionTier, ) { const seen = new Set(); const candidates = supportedSizes.flatMap((value) => { const parsed = parseExplicitSize(value); if (!parsed) { return []; } const dimensions = resolution === "4k" ? fit4kDimensions(parsed[0], parsed[1]) : { width: parsed[0], height: parsed[1] }; const key = `${dimensions.width}x${dimensions.height}`; if (seen.has(key)) { return []; } seen.add(key); return [dimensions]; }); if (candidates.length === 0) { return canonical; } const exact = candidates.find((item) => item.width === canonical.width && item.height === canonical.height); if (exact) { return exact; } const targetRatio = canonical.width / canonical.height; return [...candidates].sort((left, right) => { const leftDelta = Math.abs(left.width / left.height - targetRatio); const rightDelta = Math.abs(right.width / right.height - targetRatio); if (leftDelta !== rightDelta) { return leftDelta - rightDelta; } return right.width * right.height - left.width * left.height; })[0]; } export function getImageSizePreview( value: string, resolution: string, supportedSizes: string[] = [], source: ImageSizePreviewSource = "image_api", ): ImageSizePreview | null { const tier: ImageResolutionTier = resolution === "4k" ? "4k" : resolution === "2k" ? "2k" : "1k"; const explicit = parseExplicitSize(value); let canonical: { width: number; height: number } | null = null; if (explicit) { if (Math.max(...explicit) / Math.min(...explicit) > MAX_CUSTOM_ASPECT_RATIO) { return null; } if (source === "account_pool") { return accountPoolDimensions(explicit[0], explicit[1]); } canonical = tier === "4k" ? fit4kDimensions(explicit[0], explicit[1]) : fitDimensionsToEdge(explicit[0], explicit[1], tier === "2k" ? MAX_2K_EDGE : MAX_STANDARD_EDGE); } else { const ratio = parseAspectRatio(value); if (!ratio || !isSupportedAspectRatio(value)) { return null; } if (source === "account_pool") { return accountPoolDimensions(ratio[0], ratio[1]); } canonical = tier === "4k" ? fit4kRatio(ratio[0], ratio[1]) : fitRatioToEdge(ratio[0], ratio[1], tier === "2k" ? MAX_2K_EDGE : MAX_STANDARD_EDGE); } if (!canonical) { return null; } const dimensions = closestSupportedSize(canonical, supportedSizes, tier); return { ...dimensions, value: `${dimensions.width}x${dimensions.height}`, label: `${dimensions.width} × ${dimensions.height}`, }; }