Browse Source

fix: preserve image MIME types in workflow-images API to prevent duplicates

Fixes duplicate image files in generations folder when Gemini returns JPEG.

Changes:
- POST handler now extracts MIME type from data URL and uses correct extension
- GET handler searches multiple extensions (png, jpg, jpeg, gif, webp)
- GET handler returns correct MIME type in data URL based on file extension

Root cause: workflow-images/route.ts hardcoded .png extension regardless of
actual image format, while save-generation/route.ts correctly preserved formats.
This caused Gemini's JPEG images to be saved twice (once as .jpg, once as .png).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
handoff-20260429-1057
Shrimbly 5 months ago
parent
commit
c41a4ae839
  1. 60
      src/app/api/workflow-images/route.ts

60
src/app/api/workflow-images/route.ts

@ -8,6 +8,24 @@ export const maxDuration = 300; // 5 minute timeout for large image operations
const IMAGES_FOLDER = "inputs"; const IMAGES_FOLDER = "inputs";
const LEGACY_IMAGES_FOLDER = ".images"; // For backward compatibility const LEGACY_IMAGES_FOLDER = ".images"; // For backward compatibility
// Helper to extract MIME type and extension from data URL
function getMimeAndExtension(dataUrl: string): { mime: string; extension: string } {
const match = dataUrl.match(/^data:(image\/\w+);base64,/);
if (match) {
const mime = match[1];
const mimeToExt: Record<string, string> = {
"image/png": "png",
"image/jpeg": "jpg",
"image/jpg": "jpg",
"image/gif": "gif",
"image/webp": "webp",
};
return { mime, extension: mimeToExt[mime] || "png" };
}
// Default to PNG if no MIME type found
return { mime: "image/png", extension: "png" };
}
// POST: Save an image to the workflow's inputs or generations folder // POST: Save an image to the workflow's inputs or generations folder
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
let workflowPath: string | undefined; let workflowPath: string | undefined;
@ -80,8 +98,9 @@ export async function POST(request: NextRequest) {
); );
} }
// Construct file path // Extract MIME type and determine file extension
const filename = `${imageId}.png`; const { extension } = getMimeAndExtension(imageData);
const filename = `${imageId}.${extension}`;
const filePath = path.join(targetFolder, filename); const filePath = path.join(targetFolder, filename);
// Extract base64 data and convert to buffer // Extract base64 data and convert to buffer
@ -157,8 +176,8 @@ export async function GET(request: NextRequest) {
); );
} }
// Construct file path - check folders in order based on hint // Construct file path - check folders and extensions in order
const filename = `${imageId}.png`; const possibleExtensions = ["png", "jpg", "jpeg", "gif", "webp"];
const inputsFolder = path.join(workflowPath, IMAGES_FOLDER); const inputsFolder = path.join(workflowPath, IMAGES_FOLDER);
const generationsFolder = path.join(workflowPath, "generations"); const generationsFolder = path.join(workflowPath, "generations");
const legacyFolder = path.join(workflowPath, LEGACY_IMAGES_FOLDER); const legacyFolder = path.join(workflowPath, LEGACY_IMAGES_FOLDER);
@ -169,20 +188,26 @@ export async function GET(request: NextRequest) {
: [inputsFolder, generationsFolder, legacyFolder]; : [inputsFolder, generationsFolder, legacyFolder];
let filePath: string | null = null; let filePath: string | null = null;
let foundExtension = "png"; // Track which extension was found
// Check each folder in order // Check each folder and extension combination in order
for (const searchFolder of searchOrder) { for (const searchFolder of searchOrder) {
const candidatePath = path.join(searchFolder, filename); for (const ext of possibleExtensions) {
try { const filename = `${imageId}.${ext}`;
await fs.access(candidatePath); const candidatePath = path.join(searchFolder, filename);
filePath = candidatePath; try {
if (searchFolder === legacyFolder) { await fs.access(candidatePath);
logger.info('file.load', 'Found image in legacy .images folder', { filePath }); filePath = candidatePath;
foundExtension = ext;
if (searchFolder === legacyFolder) {
logger.info('file.load', 'Found image in legacy .images folder', { filePath });
}
break;
} catch {
// File not found with this extension, try next
} }
break;
} catch {
// File not found in this folder, try next
} }
if (filePath) break; // Stop searching if file was found
} }
if (!filePath) { if (!filePath) {
@ -202,9 +227,12 @@ export async function GET(request: NextRequest) {
// Read the image file // Read the image file
const buffer = await fs.readFile(filePath); const buffer = await fs.readFile(filePath);
// Convert to base64 data URL // Convert to base64 data URL with correct MIME type
const base64 = buffer.toString("base64"); const base64 = buffer.toString("base64");
const dataUrl = `data:image/png;base64,${base64}`; const mimeType = foundExtension === "jpg" || foundExtension === "jpeg"
? "image/jpeg"
: `image/${foundExtension}`;
const dataUrl = `data:${mimeType};base64,${base64}`;
logger.info('file.load', 'Workflow image loaded successfully', { logger.info('file.load', 'Workflow image loaded successfully', {
filePath, filePath,

Loading…
Cancel
Save