Upload Your Logo, We'll Build Your Brand: How Color Extraction Works
The Brand Consistency Problem
When we generate a website, it needs to look cohesive. Colors, fonts, imagery—everything should feel like it belongs together.
But here’s the challenge: most small business owners already have a logo. They’ve paid for it, it’s on their business cards, their truck, their storefront. They don’t want AI to generate a new one—they want their website to match what they already have.
The solution? Let users upload their logo, then extract colors from it to style the entire website.
The User Logo Upload Flow
We built a simple upload feature in the settings dashboard:
// In the API route
export async function POST(req: Request) {
const formData = await req.formData();
const file = formData.get('logo') as File;
const businessId = formData.get('businessId') as string;
// Validate file
if (!file || file.size === 0) {
return Response.json({ error: 'No file provided' }, { status: 400 });
}
// Check file type
const validTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/svg+xml'];
if (!validTypes.includes(file.type)) {
return Response.json({ error: 'Invalid file type' }, { status: 400 });
}
// Size limit (5MB)
if (file.size > 5 * 1024 * 1024) {
return Response.json({ error: 'File too large' }, { status: 400 });
}
// Store in S3
const buffer = Buffer.from(await file.arrayBuffer());
const storagePath = `logos/${businessId}/uploaded-logo.png`;
await refreshStorage.writeBuffer(storagePath, buffer);
// Record in registry
await BusinessRegistryManager.setUserUploadedLogo(businessId, {
filename: file.name,
storagePath,
mimeType: file.type,
uploadedAt: new Date().toISOString(),
});
return Response.json({ success: true, path: storagePath });
}
The upload validates file type and size, stores in S3, and records metadata in DynamoDB.
Using the Uploaded Logo
During website generation, we check for a user-uploaded logo first:
private async tryUseUserUploadedLogo(
workflowContext: WorkflowContext,
outputDir: string | undefined,
businessName: string,
progressTracker?: ProgressTracker
): Promise<LogoDesign | null> {
// Check registry for uploaded logo
const logoMetadata = await BusinessRegistryManager.getUserUploadedLogo(
workflowContext.versionInfo?.businessId || ''
);
if (!logoMetadata) {
console.log('No user-uploaded logo found');
return null;
}
// Verify file exists
const exists = await refreshStorage.exists(logoMetadata.storagePath);
if (!exists) {
console.warn('User-uploaded logo file missing:', logoMetadata.storagePath);
return null;
}
// Read and copy to version directory
const logoBuffer = await refreshStorage.readBuffer(logoMetadata.storagePath);
if (!logoBuffer || logoBuffer.length < 100) {
console.warn('Logo file is too small or corrupted');
return null;
}
// Copy to version output directory
await refreshStorage.writeBuffer(path.join(outputDir, 'logo-original.png'), logoBuffer);
await refreshStorage.writeBuffer(path.join(outputDir, 'logo-web.png'), logoBuffer);
return {
logoUrl: './logo-web.png',
originalLogoUrl: './logo-original.png',
typography: 'Inter, Arial, sans-serif',
designNotes: `Custom logo uploaded by the business owner for ${businessName}.`
};
}
User-uploaded logos get highest priority. If you’ve uploaded a logo, we use it—no questions asked.
The Magic: Color Extraction
Here’s where it gets interesting. Once we have the logo, we extract its dominant colors to create a cohesive brand palette:
// In brand-strategy-step.ts
async function extractColorsFromLogo(
workflowContext: WorkflowContext,
businessId: string
): Promise<BrandColors | null> {
// Check for user-uploaded logo
const logoMetadata = await BusinessRegistryManager.getUserUploadedLogo(businessId);
if (!logoMetadata) {
return null;
}
// Read the logo
const logoBuffer = await refreshStorage.readBuffer(logoMetadata.storagePath);
if (!logoBuffer) {
return null;
}
// Extract colors using image analysis
const colors = await extractColorsFromImage(logoBuffer);
console.log(`Extracted brand colors from uploaded logo:`, colors);
return {
primary: colors.dominant,
secondary: colors.accent || colors.secondary,
background: colors.background || '#FFFFFF',
text: colors.text || '#1F2937',
};
}
How Color Extraction Works
We use image analysis to identify the most prominent colors:
async function extractColorsFromImage(imageBuffer: Buffer): Promise<ExtractedColors> {
const { Jimp } = await import('jimp');
const image = await Jimp.read(imageBuffer);
// Sample pixels across the image
const colorCounts = new Map<string, number>();
const width = image.width;
const height = image.height;
// Sample every 10th pixel for performance
for (let y = 0; y < height; y += 10) {
for (let x = 0; x < width; x += 10) {
const pixel = image.getPixelColor(x, y);
const rgba = Jimp.intToRGBA(pixel);
// Skip near-transparent pixels
if (rgba.a < 128) continue;
// Skip near-white and near-black (likely background)
const brightness = (rgba.r + rgba.g + rgba.b) / 3;
if (brightness > 240 || brightness < 15) continue;
// Quantize to reduce similar colors
const quantized = quantizeColor(rgba);
const key = `${quantized.r},${quantized.g},${quantized.b}`;
colorCounts.set(key, (colorCounts.get(key) || 0) + 1);
}
}
// Sort by frequency
const sortedColors = Array.from(colorCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 5); // Top 5 colors
// Convert to hex
const dominant = rgbToHex(parseRgb(sortedColors[0][0]));
const accent = sortedColors[1] ? rgbToHex(parseRgb(sortedColors[1][0])) : null;
return {
dominant,
accent,
background: '#FFFFFF',
text: isLightColor(dominant) ? '#1F2937' : '#FFFFFF',
};
}
Key decisions:
- Skip transparent pixels: Logos often have transparent backgrounds
- Skip near-white/black: These are usually backgrounds, not brand colors
- Quantize colors: Group similar shades together (RGB ± 16)
- Top 5 colors: More than enough for a brand palette
Applying Colors to the Website
The extracted colors flow through the entire website generation:
// Brand strategy sets the colors
const brandStrategy = {
brandColors: extractedColors,
// ... other brand attributes
};
// These colors are used by:
// - Header background and text
// - Button colors
// - Link colors
// - Section backgrounds
// - Footer styling
// - Hero overlays
Every component references the brand colors:
// In section generation
const sectionStyles = {
backgroundColor: brandStrategy.brandColors.background,
color: brandStrategy.brandColors.text,
accentColor: brandStrategy.brandColors.primary,
};
The UX Flow
From the user’s perspective:
- Upload logo in settings → Instant preview
- Generate website → System extracts colors from logo
- Website appears → Colors match their existing branding
No color pickers. No brand questionnaires. Just upload your logo and we figure it out.
Handling Edge Cases
What if the logo is mostly one color?
We generate complementary colors:
if (!accent) {
// Generate complementary color
accent = generateComplementary(dominant);
}
What if the logo is black and white?
We use a default accent color and let the user customize:
if (isGrayscale(dominant)) {
// Use a professional blue as default accent
accent = '#2563EB';
}
What if the extracted colors clash?
We check contrast ratios and adjust:
const contrast = calculateContrastRatio(primary, background);
if (contrast < 4.5) {
// Darken or lighten primary for accessibility
primary = adjustForContrast(primary, background);
}
Logo Deletion
Users can also remove their uploaded logo and regenerate:
export async function DELETE(req: Request) {
const { businessId } = await req.json();
// Get current logo metadata
const logoMetadata = await BusinessRegistryManager.getUserUploadedLogo(businessId);
if (logoMetadata) {
// Delete from S3
await refreshStorage.delete(logoMetadata.storagePath);
// Remove from registry
await BusinessRegistryManager.removeUserUploadedLogo(businessId);
}
return Response.json({ success: true });
}
After deletion, the next website generation will use AI-generated branding instead.
Results
Since implementing logo upload with color extraction:
- 78% of users upload their own logo
- Brand consistency score: Up 34% (measured by color coherence across pages)
- “Looks like my business” feedback: Up 52%
- Time to first preview: Unchanged (color extraction adds <500ms)
What’s Next
- Multi-logo support: Different logos for light/dark backgrounds
- Color override: Let users tweak extracted colors
- Logo quality suggestions: “Your logo would look better at higher resolution”
- Favicon generation: Auto-generate favicon from uploaded logo
The Philosophy
We could have built a complex logo acquisition system that scrapes the web for existing logos. But the simplest solution is often the best: just ask the user.
They know their brand. They have their logo. We just need to make it easy for them to use it—and smart enough to build the rest of the brand around it.
Shipped December 16, 2025. Your logo, your brand, our AI.