Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions templates/next-image/src/app/api/edit-image/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* This route demonstrates Echo SDK integration with AI image editing:
* - Uses both Google Gemini and OpenAI for image editing
* - Supports both data URLs (base64) and regular URLs
* - Images should be compressed client-side to avoid 413 errors
* - Validates input images and prompts
* - Returns edited images in appropriate format
*/
Expand All @@ -17,14 +18,6 @@ const providers = {
gemini: handleGoogleEdit,
};

export const config = {
api: {
bodyParser: {
sizeLimit: '4mb',
},
},
};

export async function POST(req: Request) {
try {
const body = await req.json();
Expand Down
9 changes: 1 addition & 8 deletions templates/next-image/src/app/api/generate-image/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* - Supports both OpenAI and Gemini models
* - Handles text-to-image generation
* - Returns base64 encoded images for consistent handling
* - Request payloads are kept small (generation only sends text prompts)
*/

import {
Expand All @@ -19,14 +20,6 @@ const providers = {
gemini: handleGoogleGenerate,
};

export const config = {
api: {
bodyParser: {
sizeLimit: '4mb',
},
},
};

export async function POST(req: Request) {
try {
const body = await req.json();
Expand Down
7 changes: 4 additions & 3 deletions templates/next-image/src/components/image-generator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { Button } from '@/components/ui/button';
import { X } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';

import { fileToDataUrl } from '@/lib/image-utils';
import { fileToDataUrl, compressImage } from '@/lib/image-utils';
import type {
EditImageRequest,
GeneratedImage,
Expand Down Expand Up @@ -219,12 +219,13 @@ export default function ImageGenerator() {
try {
const imageUrls = await Promise.all(
imageFiles.map(async imageFile => {
// Convert blob URL to data URL for API
// Convert blob URL to data URL, then compress to avoid 413 errors
const response = await fetch(imageFile.url);
const blob = await response.blob();
return await fileToDataUrl(
const dataUrl = await fileToDataUrl(
new File([blob], 'image', { type: imageFile.mediaType })
);
return await compressImage(dataUrl);
})
);

Expand Down
48 changes: 48 additions & 0 deletions templates/next-image/src/lib/image-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,51 @@ export function getMediaTypeFromDataUrl(dataUrl: string): string {
if (!dataUrl.startsWith('data:')) return 'image/jpeg';
return dataUrl.match(/^data:([^;]+);base64,/)?.[1] || 'image/jpeg';
}

/**
* Compresses an image to reduce payload size before sending to the API.
* Prevents HTTP 413 (Request Entity Too Large) when images are sent as
* base64 data URLs in JSON request bodies.
*
* @param dataUrl - The source image as a data URL
* @param maxDimension - Maximum width or height in pixels (default: 2048)
* @param quality - JPEG compression quality 0-1 (default: 0.85)
* @returns Compressed image as a data URL
*/
export async function compressImage(
dataUrl: string,
maxDimension = 2048,
quality = 0.85
): Promise<string> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
let { width, height } = img;

// Scale down if exceeding max dimension, preserving aspect ratio
if (width > maxDimension || height > maxDimension) {
const scale = maxDimension / Math.max(width, height);
width = Math.round(width * scale);
height = Math.round(height * scale);
}

const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
resolve(dataUrl);
return;
}
ctx.drawImage(img, 0, 0, width, height);

// Keep PNG for images that may have transparency, use JPEG otherwise
const mediaType = getMediaTypeFromDataUrl(dataUrl);
const outputType = mediaType === 'image/png' ? 'image/png' : 'image/jpeg';
resolve(canvas.toDataURL(outputType, quality));
};
img.onerror = () =>
reject(new Error('Failed to load image for compression'));
img.src = dataUrl;
});
}