Download web/src/lib/image-size-preview.ts from zlcy/study: direct link, hf CLI and curl.
- Browser
- Download file 9.8 kB
-
https://huggingface.co/spaces/zlcy/study/resolve/main/web/src/lib/image-size-preview.ts
- Command line
-
hf download hf://spaces/zlcy/study/web/src/lib/image-size-preview.ts
-
curl -L -o image-size-preview.ts https://huggingface.co/spaces/zlcy/study/resolve/main/web/src/lib/image-size-preview.ts
9.8 kB
| 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<string>(); | |
| 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}`, | |
| }; | |
| } | |