export type ImageSizeBucket = "1K" | "2K" | "4K"; export type ImageDimensionConstraints = { maxEdge?: number; maxPixels?: number; minEdge?: number; multiple?: number; }; export type ImageDimensions = { width: number; height: number; }; export function normalizeImageSizeBucket(value?: string, fallback: ImageSizeBucket = "2K"): ImageSizeBucket { const normalized = String(value || "").trim().toUpperCase(); if (normalized === "1K" || normalized === "2K" || normalized === "4K") return normalized; return fallback; } export function parseAspectRatioPair(value?: string): ImageDimensions | null { const normalized = String(value || "").trim().toLowerCase(); if (!normalized || normalized === "auto") return null; const match = normalized.match(/^(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)$/); if (!match) return null; const width = Number(match[1]); const height = Number(match[2]); if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { return null; } return { width, height }; } export function roundToMultiple(value: number, multiple = 1) { return Math.max(multiple, Math.round(value / multiple) * multiple); } export function floorToMultiple(value: number, multiple = 1) { return Math.max(multiple, Math.floor(value / multiple) * multiple); } export function constrainImageDimensions( dimensions: ImageDimensions, constraints: ImageDimensionConstraints = {}, ): ImageDimensions { const multiple = Math.max(1, Math.floor(constraints.multiple || 1)); const minEdge = Math.max(multiple, Math.floor(constraints.minEdge || multiple)); const maxEdge = Number.isFinite(constraints.maxEdge) ? Math.max(minEdge, Math.floor(Number(constraints.maxEdge))) : Number.POSITIVE_INFINITY; const maxPixels = Number.isFinite(constraints.maxPixels) ? Math.max(minEdge * minEdge, Math.floor(Number(constraints.maxPixels))) : Number.POSITIVE_INFINITY; let width = Math.max(minEdge, floorToMultiple(Math.max(1, dimensions.width), multiple)); let height = Math.max(minEdge, floorToMultiple(Math.max(1, dimensions.height), multiple)); while (width > maxEdge || height > maxEdge || width * height > maxPixels) { const scale = Math.min(maxEdge / width, maxEdge / height, Math.sqrt(maxPixels / (width * height)), 1); const nextWidth = Math.max(minEdge, floorToMultiple(width * scale, multiple)); const nextHeight = Math.max(minEdge, floorToMultiple(height * scale, multiple)); if (nextWidth === width && nextHeight === height) { if (width >= height && width > minEdge) width -= multiple; else if (height > minEdge) height -= multiple; else break; } else { width = nextWidth; height = nextHeight; } } return { width, height }; } export function resolveLongestEdgeDimensions(params: { aspectRatio?: string; longestEdge: number; multiple?: number; constraints?: ImageDimensionConstraints; }): ImageDimensions { const ratioPair = parseAspectRatioPair(params.aspectRatio) || { width: 1, height: 1 }; const scale = params.longestEdge / Math.max(ratioPair.width, ratioPair.height); return constrainImageDimensions( { width: ratioPair.width * scale, height: ratioPair.height * scale, }, { multiple: params.multiple || params.constraints?.multiple, ...params.constraints, }, ); }