WebZum Logo
WebZum

From Zero to Website Hero

Sign InSign Up
Back to Blog
brandingimage-processingcolor-extractionuxstartup

Upload Your Logo, We'll Build Your Brand: How Color Extraction Works

WebZum Team•December 16, 2025•7 min read
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:

  1. Upload logo in settings → Instant preview
  2. Generate website → System extracts colors from logo
  3. 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.

Ready to Build Your Website?

Join hundreds of businesses using WebZum to create professional websites in minutes, not weeks.

Get Started Free
Live in 5 minutesNo credit card required
Home•Free Tools•Blog•Directory•About•Agencies•Partners
FAQ•Privacy•Terms•© 2026 WebZum