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: 2 additions & 7 deletions templates/next-image/src/app/api/edit-image/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,8 @@ const providers = {
gemini: handleGoogleEdit,
};

export const config = {
api: {
bodyParser: {
sizeLimit: '4mb',
},
},
};
// App Router route segment config
export const maxDuration = 60;

export async function POST(req: Request) {
try {
Expand Down
9 changes: 2 additions & 7 deletions templates/next-image/src/app/api/generate-image/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,8 @@ const providers = {
gemini: handleGoogleGenerate,
};

export const config = {
api: {
bodyParser: {
sizeLimit: '4mb',
},
},
};
// App Router route segment config
export const maxDuration = 60;

export async function POST(req: Request) {
try {
Expand Down
34 changes: 18 additions & 16 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, compressImageDataUrl } from '@/lib/image-utils';
import type {
EditImageRequest,
GeneratedImage,
Expand Down Expand Up @@ -92,6 +92,20 @@ async function editImage(request: EditImageRequest): Promise<ImageResponse> {
return response.json();
}

/**
* Helper: convert a PromptInput file attachment to a compressed data URL
*/
async function attachmentToCompressedDataUrl(
fileRef: { url: string; mediaType?: string; filename?: string }
): Promise<string> {
const response = await fetch(fileRef.url);
const blob = await response.blob();
const raw = await fileToDataUrl(
new File([blob], fileRef.filename || 'image', { type: fileRef.mediaType })
);
return compressImageDataUrl(raw);
}

/**
* Main ImageGenerator component
*
Expand Down Expand Up @@ -170,13 +184,7 @@ export default function ImageGenerator() {
.filter(f => f.mediaType?.startsWith('image/'))
.map(async f => {
try {
const response = await fetch(f.url);
const blob = await response.blob();
return await fileToDataUrl(
new File([blob], f.filename || 'image', {
type: f.mediaType,
})
);
return await attachmentToCompressedDataUrl(f);
} catch (error) {
console.error(
'Failed to convert attachment to data URL:',
Expand Down Expand Up @@ -217,15 +225,9 @@ export default function ImageGenerator() {
}

try {
// Compress images before sending to avoid 413 errors
const imageUrls = await Promise.all(
imageFiles.map(async imageFile => {
// Convert blob URL to data URL for API
const response = await fetch(imageFile.url);
const blob = await response.blob();
return await fileToDataUrl(
new File([blob], 'image', { type: imageFile.mediaType })
);
})
imageFiles.map(f => attachmentToCompressedDataUrl(f))
);

const result = await editImage({
Expand Down
73 changes: 71 additions & 2 deletions templates/next-image/src/lib/image-utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
/**
* Minimal Image Utilities
* Image Utilities
*
* Simple, clean API with just data URLs. No complex conversions.
* Data URL helpers and client-side image compression to avoid
* "Request Entity Too Large" (HTTP 413) when sending base64 payloads.
*/

/** Max dimension (width or height) for images sent to the API */
const MAX_IMAGE_DIMENSION = 1024;

/** JPEG quality used when compressing (0-1) */
const COMPRESS_QUALITY = 0.85;

/** Target max size in bytes for a single compressed image (~3MB) */
const MAX_IMAGE_BYTES = 3 * 1024 * 1024;

/**
* Converts a File to a data URL
*/
Expand All @@ -16,6 +26,65 @@ export async function fileToDataUrl(file: File): Promise<string> {
});
}

/**
* Compresses an image data URL by resizing and converting to JPEG.
* Falls back to the original if compression fails (e.g. in non-browser envs).
*/
export async function compressImageDataUrl(
dataUrl: string,
maxDimension: number = MAX_IMAGE_DIMENSION,
quality: number = COMPRESS_QUALITY
): Promise<string> {
if (typeof document === 'undefined') return dataUrl;

return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
let { width, height } = img;

// Scale down if either dimension exceeds the limit
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);

// Try JPEG first for smaller size, iteratively reduce quality if needed
let compressed = canvas.toDataURL('image/jpeg', quality);
let currentQuality = quality;

while (estimateBase64Bytes(compressed) > MAX_IMAGE_BYTES && currentQuality > 0.3) {
currentQuality -= 0.1;
compressed = canvas.toDataURL('image/jpeg', currentQuality);
}

resolve(compressed);
};
img.onerror = () => resolve(dataUrl);
img.src = dataUrl;
});
}

/**
* Rough estimate of the decoded byte size of a base64 data URL
*/
function estimateBase64Bytes(dataUrl: string): number {
const base64 = dataUrl.split(',')[1] || '';
return Math.floor(base64.length * 0.75);
}

/**
* Converts a data URL to a File object
*/
Expand Down