import type { AgentPsdTextAlign } from "@/lib/agents/psd-template-config"; function clampChannel(value: number) { return Math.max(0, Math.min(255, Math.round(value))); } function toHexChannel(value: number) { return clampChannel(value).toString(16).padStart(2, "0"); } export function normalizeHexColor(value: string, fallback = "#111111") { const normalized = String(value || "").trim().toLowerCase(); if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/.test(normalized)) { if (normalized.length === 4) { return `#${normalized[1]}${normalized[1]}${normalized[2]}${normalized[2]}${normalized[3]}${normalized[3]}`; } return normalized; } return fallback; } export function estimateGlyphWidth(char: string, fontSize: number, letterSpacing: number) { if (char === " ") return fontSize * 0.36 + letterSpacing; if (/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(char)) { return fontSize + letterSpacing; } if (/[A-Z0-9]/.test(char)) return fontSize * 0.66 + letterSpacing; return fontSize * 0.58 + letterSpacing; } export function wrapTextByWidth(params: { text: string; maxWidth: number; fontSize: number; letterSpacing: number; maxLines: number; }) { const { text, maxWidth, fontSize, letterSpacing, maxLines } = params; const paragraphs = String(text || "") .replace(/\r\n/g, "\n") .replace(/\r/g, "\n") .split("\n"); const lines: string[] = []; function pushLine(line: string) { if (lines.length < maxLines) { lines.push(line); } } for (const paragraph of paragraphs) { if (lines.length >= maxLines) break; if (!paragraph) { pushLine(""); continue; } let currentLine = ""; let currentWidth = 0; for (const char of paragraph) { const charWidth = estimateGlyphWidth(char, fontSize, letterSpacing); if (currentLine && currentWidth + charWidth > maxWidth) { pushLine(currentLine); currentLine = char; currentWidth = charWidth; if (lines.length >= maxLines) break; } else { currentLine += char; currentWidth += charWidth; } } if (lines.length >= maxLines) break; if (currentLine || paragraph.length === 0) { pushLine(currentLine); } } return lines.slice(0, maxLines); } export function resolveTextLayout(params: { text: string; width: number; height: number; fontSize: number; lineHeight: number; letterSpacing: number; paddingX: number; paddingY: number; maxLines: number; }) { const { text, width, height, fontSize, lineHeight, letterSpacing, paddingX, paddingY, maxLines, } = params; const normalizedText = String(text || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n"); const availableWidth = Math.max(8, width - paddingX * 2); const availableHeight = Math.max(8, height - paddingY * 2); const resolvedLineHeight = Math.max(1, Number(lineHeight || 1)); const resolvedMaxLines = Math.max(1, Number(maxLines || 1)); let fittedFontSize = Math.max(10, Number(fontSize || 10)); let bestLines: string[] = []; let bestLineHeightPx = Math.max(fittedFontSize, fittedFontSize * resolvedLineHeight); for (let index = 0; index < 24; index += 1) { const lineHeightPx = Math.max(fittedFontSize, fittedFontSize * resolvedLineHeight); const heightLimitedMaxLines = Math.max( 1, Math.floor((availableHeight + fittedFontSize * 0.2) / lineHeightPx) ); const effectiveMaxLines = Math.max(1, Math.min(resolvedMaxLines, heightLimitedMaxLines)); const fullLines = wrapTextByWidth({ text: normalizedText, maxWidth: availableWidth, fontSize: fittedFontSize, letterSpacing, maxLines: Math.max(effectiveMaxLines, normalizedText.length + 4), }); const hasOverflow = fullLines.length > effectiveMaxLines; const lines = fullLines.slice(0, effectiveMaxLines); const totalHeight = Math.max(1, lines.length) * lineHeightPx; bestLines = lines; bestLineHeightPx = lineHeightPx; if (!hasOverflow && totalHeight <= availableHeight + 0.5) { break; } if (fittedFontSize <= 10) { break; } fittedFontSize = Math.max(10, Number((fittedFontSize * 0.94).toFixed(2))); } return { fontSize: Number(fittedFontSize.toFixed(2)), lineHeightPx: Number(bestLineHeightPx.toFixed(2)), lines: bestLines, }; } export function analyzeTextLayerPixels(params: { rgba: Uint8ClampedArray; width: number; height: number; }) { const { rgba, width, height } = params; const pixelCount = Math.max(0, Math.floor(rgba.length / 4)); let minX = width; let minY = height; let maxX = -1; let maxY = -1; let weightedR = 0; let weightedG = 0; let weightedB = 0; let totalWeight = 0; for (let index = 0; index < pixelCount; index += 1) { const alpha = rgba[index * 4 + 3] || 0; if (alpha < 20) { continue; } const x = index % width; const y = Math.floor(index / width); const weight = (alpha / 255) ** 2; minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); weightedR += (rgba[index * 4] || 0) * weight; weightedG += (rgba[index * 4 + 1] || 0) * weight; weightedB += (rgba[index * 4 + 2] || 0) * weight; totalWeight += weight; } if (maxX < 0 || maxY < 0 || totalWeight <= 0) { return null; } const dominantColor = normalizeHexColor( `#${toHexChannel(weightedR / totalWeight)}${toHexChannel(weightedG / totalWeight)}${toHexChannel(weightedB / totalWeight)}` ); const leftGap = minX; const rightGap = Math.max(0, width - maxX - 1); const tolerance = Math.max(4, width * 0.08); const visibleWidth = maxX - minX + 1; const inferredTextAlign: AgentPsdTextAlign = visibleWidth >= width * 0.92 ? "left" : Math.abs(leftGap - rightGap) <= tolerance ? "center" : leftGap > rightGap ? "right" : "left"; return { dominantColor, inferredTextAlign, visibleBounds: { minX, minY, maxX, maxY, }, }; }