/** * Real brand marks as reference assets. Video/image models drawing a logo * "from memory" produce near-logos with mangled letterforms — the fix is to * fetch the actual mark once and attach it as a reference image. * * Keyless public sources, tried in order: * 1. Clearbit logo CDN — color PNG by company domain * 2. SimpleIcons CDN — monochrome brand-mark SVG by slug * 3. Google favicon API — small PNG, last resort * * Trademark clearance stays the operator's call — surface that once per * project, then proceed. */ export type LogoSource = 'clearbit' | 'simpleicons' | 'favicon'; export interface FetchedLogo { bytes: Uint8Array; /** File extension without the dot (png | svg). */ ext: string; source: LogoSource; url: string; } export interface FetchLogoOptions { /** Explicit company domain (else guessed as .com). */ domain?: string; /** Restrict to one source instead of the ladder. */ source?: LogoSource; /** SimpleIcons tint, e.g. "white" or "0A0A0A". */ color?: string; /** Clearbit size in px (default 512). */ size?: number; /** Injectable fetch for tests. */ fetchImpl?: typeof fetch; } export function guessDomain(brand: string): string { const slug = brand.trim().toLowerCase().replace(/[^a-z0-9]/g, ''); return `${slug}.com`; } function simpleIconsSlug(brand: string): string { return brand.trim().toLowerCase().replace(/[^a-z0-9]/g, ''); } async function tryUrl( url: string, expectExt: string, source: LogoSource, fetchImpl: typeof fetch, ): Promise { try { const res = await fetchImpl(url); if (!res.ok) return null; const bytes = new Uint8Array(await res.arrayBuffer()); // Sources return tiny placeholder bodies for unknown brands — treat those as misses. if (bytes.byteLength < 128) return null; return { bytes, ext: expectExt, source, url }; } catch { return null; } } /** * Fetch a brand mark, walking the source ladder. Returns null when every * source misses — the caller should then ask the operator for a file/URL * rather than letting a model improvise the mark. */ export async function fetchBrandLogo(brand: string, opts: FetchLogoOptions = {}): Promise { const fetchImpl = opts.fetchImpl ?? fetch; const size = opts.size ?? 512; const domain = opts.domain ?? guessDomain(brand); const ladder: Array<() => Promise> = []; const want = (s: LogoSource) => !opts.source || opts.source === s; if (want('clearbit')) { ladder.push(() => tryUrl(`https://logo.clearbit.com/${domain}?size=${size}`, 'png', 'clearbit', fetchImpl)); } if (want('simpleicons')) { const slug = simpleIconsSlug(brand); const tint = opts.color ? `/${encodeURIComponent(opts.color)}` : ''; ladder.push(() => tryUrl(`https://cdn.simpleicons.org/${slug}${tint}`, 'svg', 'simpleicons', fetchImpl)); } if (want('favicon')) { ladder.push(() => tryUrl(`https://www.google.com/s2/favicons?domain=${domain}&sz=${Math.min(size, 256)}`, 'png', 'favicon', fetchImpl), ); } for (const attempt of ladder) { const hit = await attempt(); if (hit) return hit; } return null; }