You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
recipeTitle = "Delicious Dish Recipe",
ingredientsText = "• 200g All-purpose flour\n• 100g Sugar\n• 2 Large eggs\n• 100ml Milk\n• 1 tsp Vanilla extract",
instructionsText = "1. Preheat oven to 180°C (350°F).\n2. In a large bowl, mix together the flour and sugar.\n3. In another bowl, whisk the eggs, then add the milk and vanilla extract.\n4. Pour the wet ingredients into the dry ingredients and mix until just combined.\n5. Pour the batter into a greased baking pan and bake for 25-30 minutes.",
fontFamily = "Lora",
textColor = "#333333",
backgroundColor = "#f7f5f2",
baseFontSize = 16
) {
const titleFontSize = baseFontSize * 2.2;
const headingFontSize = baseFontSize * 1.4;
const bodyFontSize = baseFontSize;
/**
* Dynamically loads a font from Google Fonts and waits for it to be available.
* @param {string} fontFamily - The name of the font family.
* @returns {Promise<void>} - A promise that resolves when the font is loaded.
*/
const loadAndInjectFont = async (fontFamily) => {
const sanitizedFontFamily = fontFamily.replace(/ /g, '+');
const fontUrl = `https://fonts.googleapis.com/css2?family=${sanitizedFontFamily}:wght@400;700&display=swap`;
const fontId = `google-font-${sanitizedFontFamily}`;
if (document.getElementById(fontId)) {
try {
await document.fonts.load(`1em "${fontFamily}"`);
return;
} catch (e) {
console.error(`Font "${fontFamily}" was requested but failed to load.`, e);
}
}
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.id = fontId;
link.rel = 'stylesheet';
link.href = fontUrl;
link.onload = () => {
document.fonts.load(`1em "${fontFamily}"`)
.then(() => resolve())
.catch(e => {
console.error(`Failed to load font face for ${fontFamily}`, e);
// Resolve anyway so the app doesn't break, will use fallback font
resolve();
});
};
link.onerror = (e) => {
console.error(`Failed to load stylesheet for font: ${fontFamily}`, e);
// Resolve anyway so the app doesn't break, will use fallback font
resolve();
}
document.head.appendChild(link);
});
};
/**
* Measures the required height for a given text block with line wrapping.
* @param {CanvasRenderingContext2D} context - The canvas rendering context.
* @param {string} text - The text content to measure.
* @param {number} maxWidth - The maximum width for a line.
* @param {number} lineHeight - The height of each line.
* @returns {number} - The total calculated height.
*/
const measureWrappedTextHeight = (context, text, maxWidth, lineHeight) => {
if (!text || text.trim() === '') return 0;
let totalHeight = 0;
const paragraphs = text.split('\n');
paragraphs.forEach(paragraph => {
if (paragraph.trim() === '') {
totalHeight += lineHeight * 0.5; // Smaller gap for empty lines
return;
}
let currentLine = '';
const words = paragraph.split(' ');
for (let i = 0; i < words.length; i++) {
const testLine = currentLine + words[i] + ' ';
if (context.measureText(testLine).width > maxWidth && i > 0) {
totalHeight += lineHeight;
currentLine = words[i] + ' ';
} else {
currentLine = testLine;
}
}
totalHeight += lineHeight; // Add height for the last line of the paragraph
});
return totalHeight;
};
/**
* Draws wrapped text onto the canvas.
* @param {CanvasRenderingContext2D} context - The canvas rendering context.
* @param {string} text - The text content to draw.
* @param {number} x - The starting X coordinate.
* @param {number} y - The starting Y coordinate.
* @param {number} maxWidth - The maximum width for a line.
* @param {number} lineHeight - The height of each line.
* @returns {number} The new Y position after drawing the text.
*/
const drawWrappedText = (context, text, x, y, maxWidth, lineHeight) => {
if (!text || text.trim() === '') return y;
let currentY = y;
const paragraphs = text.split('\n');
paragraphs.forEach(paragraph => {
if (paragraph.trim() === '') {
currentY += lineHeight * 0.5;
return;
}
let currentLine = '';
const words = paragraph.split(' ');
for (let i = 0; i < words.length; i++) {
const testLine = currentLine + words[i] + ' ';
if (context.measureText(testLine).width > maxWidth && i > 0) {
context.fillText(currentLine.trim(), x, currentY);
currentY += lineHeight;
currentLine = words[i] + ' ';
} else {
currentLine = testLine;
}
}
context.fillText(currentLine.trim(), x, currentY);
currentY += lineHeight;
});
return currentY;
};
// --- Main Function Logic ---
await loadAndInjectFont(fontFamily);
const IMAGE_PANEL_WIDTH = 800;
const TEXT_PANEL_WIDTH = 500;
const PADDING = 40;
const GAP = 25;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const imageAspectRatio = originalImg.width / originalImg.height;
const scaledImageWidth = IMAGE_PANEL_WIDTH;
const scaledImageHeight = scaledImageWidth / imageAspectRatio;
const textContentWidth = TEXT_PANEL_WIDTH - 2 * PADDING;
let requiredTextHeight = PADDING;
// Measure Title
ctx.font = `700 ${titleFontSize}px "${fontFamily}", serif`;
requiredTextHeight += measureWrappedTextHeight(ctx, recipeTitle, textContentWidth, titleFontSize * 1.2);
requiredTextHeight += GAP;
// Measure Ingredients text block
ctx.font = `700 ${headingFontSize}px "${fontFamily}", serif`;
requiredTextHeight += measureWrappedTextHeight(ctx, "Ingredients", textContentWidth, headingFontSize * 1.2);
requiredTextHeight += GAP * 0.5;
ctx.font = `400 ${bodyFontSize}px "${fontFamily}", serif`;
requiredTextHeight += measureWrappedTextHeight(ctx, ingredientsText, textContentWidth, bodyFontSize * 1.5);
requiredTextHeight += GAP;
// Measure Instructions text block
ctx.font = `700 ${headingFontSize}px "${fontFamily}", serif`;
requiredTextHeight += measureWrappedTextHeight(ctx, "Instructions", textContentWidth, headingFontSize * 1.2);
requiredTextHeight += GAP * 0.5;
ctx.font = `400 ${bodyFontSize}px "${fontFamily}", serif`;
requiredTextHeight += measureWrappedTextHeight(ctx, instructionsText, textContentWidth, bodyFontSize * 1.5);
requiredTextHeight += PADDING;
canvas.width = IMAGE_PANEL_WIDTH + TEXT_PANEL_WIDTH;
canvas.height = Math.ceil(Math.max(scaledImageHeight, requiredTextHeight));
// --- Drawing Phase ---
ctx.fillStyle = backgroundColor;
ctx.fillRect(IMAGE_PANEL_WIDTH, 0, TEXT_PANEL_WIDTH, canvas.height);
const imageY = (canvas.height - scaledImageHeight) / 2;
ctx.drawImage(originalImg, 0, Math.max(0, imageY), scaledImageWidth, scaledImageHeight);
ctx.fillStyle = textColor;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
const textX = IMAGE_PANEL_WIDTH + PADDING;
let currentY = PADDING;
// Draw Title
ctx.font = `700 ${titleFontSize}px "${fontFamily}", serif`;
currentY = drawWrappedText(ctx, recipeTitle, textX, currentY, textContentWidth, titleFontSize * 1.2);
currentY += GAP;
// Draw Ingredients
ctx.font = `700 ${headingFontSize}px "${fontFamily}", serif`;
currentY = drawWrappedText(ctx, "Ingredients", textX, currentY, textContentWidth, headingFontSize * 1.2);
currentY += GAP * 0.5;
ctx.font = `400 ${bodyFontSize}px "${fontFamily}", serif`;
currentY = drawWrappedText(ctx, ingredientsText, textX, currentY, textContentWidth, bodyFontSize * 1.5);
currentY += GAP;
// Draw Instructions
ctx.font = `700 ${headingFontSize}px "${fontFamily}", serif`;
currentY = drawWrappedText(ctx, "Instructions", textX, currentY, textContentWidth, headingFontSize * 1.2);
currentY += GAP * 0.5;
ctx.font = `400 ${bodyFontSize}px "${fontFamily}", serif`;
currentY = drawWrappedText(ctx, instructionsText, textX, currentY, textContentWidth, bodyFontSize * 1.5);
return canvas;
}
Apply Changes