You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
titleText = "BY ROYAL DECREE",
mainText = "Hear ye, hear ye!\n\nBy order of the Crown, it is hereby decreed that all subjects shall partake in joyous festivities and general merriment. Let there be cake for all! Further, any and all grievances must be submitted in triplicate via carrier pigeon no later than the next full moon. Failure to comply will result in a stern frowning.\n\nSo let it be written, so let it be done!",
closingText = "Given under my hand and seal,",
signatureText = "His Majesty, The King",
dateText = "", // Auto-generated if empty
fontName = "MedievalSharp", // A thematic font like "MedievalSharp" or "Uncial Antiqua"
textColor = "#3a2414", // Dark brown, like old ink
backgroundColor = "#f5e8c8", // Parchment paper color
borderColor = "#654321", // Darker brown for border
sealColor = "#a02c2c", // Dark red for wax seal
sealSymbolColor = "#ffd700", // Gold for symbol on seal
canvasWidth = 600,
canvasHeight = 800
) {
const fontToLoadForCSS = fontName.replace(/ /g, '+');
const fontToLoadForCanvas = fontName.includes(' ') ? `"${fontName}"` : fontName;
try {
if (!document.fonts.check(`12px ${fontToLoadForCanvas}`)) {
const fontLink = document.createElement('link');
fontLink.href = `https://fonts.googleapis.com/css2?family=${fontToLoadForCSS}:wght@400&display=swap`;
fontLink.rel = 'stylesheet';
const fontLoadPromise = new Promise((resolve, reject) => {
fontLink.onload = resolve;
fontLink.onerror = (err) => reject(new Error(`Failed to load stylesheet for font ${fontName}: ${err}`));
});
document.head.appendChild(fontLink);
await fontLoadPromise;
await document.fonts.load(`12px ${fontToLoadForCanvas}`);
console.log(`Font "${fontName}" loaded successfully.`);
} else {
console.log(`Font "${fontName}" is already available.`);
}
} catch (error) {
console.warn(`Failed to load font "${fontName}". Using system default. Error:`, error);
// The canvas will use the fontName, and browser will fallback if loading failed
}
const canvas = document.createElement('canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext('2d');
// Helper function for text wrapping
function renderWrappedText(context, text, x, y, maxWidth, lineHeight, currentFont, color, textAlign = "left") {
context.font = currentFont;
context.fillStyle = color;
context.textAlign = textAlign;
const words = text.split(' ');
let line = '';
let currentDrawY = y;
let actualDrawX;
if (textAlign === 'left') actualDrawX = x;
else if (textAlign === 'center') actualDrawX = x + maxWidth / 2;
else if (textAlign === 'right') actualDrawX = x + maxWidth;
else { // Default to left
actualDrawX = x;
context.textAlign = 'left';
}
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = context.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
context.fillText(line.trim(), actualDrawX, currentDrawY);
line = words[n] + ' ';
currentDrawY += lineHeight;
} else {
line = testLine;
}
}
context.fillText(line.trim(), actualDrawX, currentDrawY);
return currentDrawY + lineHeight;
}
// 1. Background
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Subtle texture (optional)
const textureStrength = 10;
if (textureStrength > 0) {
const id = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = id.data;
for (let i = 0; i < pixels.length; i += 4) {
const noise = (Math.random() - 0.5) * textureStrength;
pixels[i] = Math.max(0, Math.min(255, pixels[i] + noise));
pixels[i + 1] = Math.max(0, Math.min(255, pixels[i + 1] + noise));
pixels[i + 2] = Math.max(0, Math.min(255, pixels[i + 2] + noise));
}
ctx.putImageData(id, 0, 0);
}
// 2. Border
const pageMargin = 30;
const borderThickness = 8;
const innerLineMargin = 5;
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderThickness;
ctx.strokeRect(pageMargin, pageMargin, canvas.width - 2 * pageMargin, canvas.height - 2 * pageMargin);
ctx.lineWidth = 1;
const innerLineOffset = pageMargin + borderThickness / 2 + innerLineMargin; // Adjust for center of thick border
ctx.strokeRect(innerLineOffset, innerLineOffset,
canvas.width - 2 * innerLineOffset, canvas.height - 2 * innerLineOffset);
// Content layout variables
const contentPadding = 15;
const contentX = innerLineOffset + contentPadding;
const contentYstart = innerLineOffset + contentPadding;
const contentWidth = canvas.width - 2 * contentX;
let currentY = contentYstart;
// 3. Title
currentY += 30; // Initial offset for title baseline
ctx.font = `bold 36px ${fontToLoadForCanvas}`;
ctx.fillStyle = textColor;
ctx.textAlign = 'center';
ctx.fillText(titleText, canvas.width / 2, currentY);
currentY += 50; // Space after title
// 4. Original Image (if provided)
if (originalImg && originalImg.complete && originalImg.naturalWidth > 0) {
const imgMaxHeight = 150;
const imgMaxWidth = contentWidth - 40;
let imgHeight = originalImg.height;
let imgWidth = originalImg.width;
const ratio = Math.min(imgMaxWidth / imgWidth, imgMaxHeight / imgHeight);
imgWidth *= ratio;
imgHeight *= ratio;
const imgX = (canvas.width - imgWidth) / 2;
ctx.drawImage(originalImg, imgX, currentY, imgWidth, imgHeight);
currentY += imgHeight + 30; // Space after image
}
// 5. Main Text
const paragraphs = mainText.split('\n');
const paraFontSize = 18;
const paraLineHeight = paraFontSize * 1.5;
const mainTextActualX = contentX; // For left-aligned text block
for (const para of paragraphs) {
if (para.trim() === "") {
currentY += paraLineHeight * 0.7; // Smaller space for empty line (paragraph break)
continue;
}
currentY = renderWrappedText(ctx, para, mainTextActualX, currentY, contentWidth, paraLineHeight, `${paraFontSize}px ${fontToLoadForCanvas}`, textColor, 'left');
currentY += paraLineHeight * 0.3; // Inter-paragraph spacing (already includes one lineheight from renderWrapped)
}
currentY += 20; // Space before closing/signature section
// 6. Closing Text
if (closingText) {
ctx.font = `italic ${paraFontSize * 1.1}px ${fontToLoadForCanvas}`;
ctx.fillStyle = textColor;
ctx.textAlign = 'center';
ctx.fillText(closingText, canvas.width / 2, currentY);
currentY += paraLineHeight * 1.2;
}
currentY += paraLineHeight * 0.5; // Space before signature block
// 7. Footer: Seal, Signature, Date
const footerStartY = currentY;
const sealRadius = 30;
// Signature and Date
let sigDateCurrentY = footerStartY + 10;
const sigDateContentX = canvas.width - contentX; // Right align to content margin
if (dateText === "") {
const today = new Date();
const day = today.getDate();
const nth = (d) => {
if (d > 3 && d < 21) return 'th';
switch (d % 10) {
case 1: return "st";
case 2: return "nd";
case 3: return "rd";
default: return "th";
}
};
dateText = `This ${day}${nth(day)} day of ${today.toLocaleString('default', { month: 'long' })}, Anno Domini ${today.getFullYear()}`;
}
// Draw Date first at the bottom of the pair
const dateFont = `${paraFontSize * 0.9}px ${fontToLoadForCanvas}`;
ctx.font = dateFont;
ctx.fillStyle = textColor;
ctx.textAlign = 'right';
const dateMetrics = ctx.measureText(dateText);
// Place date ensuring it is above the bottom margin
const dateBaselineY = Math.min(footerStartY + paraLineHeight * 1.2 + paraLineHeight, canvasHeight - contentYstart - 5);
ctx.fillText(dateText, sigDateContentX, dateBaselineY);
// Draw Signature above Date
const sigFont = `bold ${paraFontSize * 1.2}px ${fontToLoadForCanvas}`;
ctx.font = sigFont;
ctx.fillStyle = textColor;
ctx.textAlign = 'right';
const sigBaselineY = dateBaselineY - paraLineHeight;
ctx.fillText(signatureText, sigDateContentX, sigBaselineY);
// Seal on the left
const sealActualX = contentX + sealRadius + 10; // Position from left content edge
const sealActualY = Math.min(sigBaselineY - sealRadius + paraLineHeight/2, dateBaselineY - sealRadius); // Align with signature/date block vertically
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 3;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.fillStyle = sealColor;
ctx.beginPath();
const numPoints = 20;
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 2;
const radiusOffset = (Math.random() - 0.5) * (sealRadius * 0.25);
const r = sealRadius + radiusOffset;
const px = sealActualX + r * Math.cos(angle);
const py = sealActualY + r * Math.sin(angle);
if (i === 0) ctx.moveTo(px, py);
else {
const cAngle1 = angle - (Math.PI / numPoints) * 0.5 + (Math.random() - 0.5) * 0.3;
const cAngle2 = angle - (Math.PI / numPoints) * 0.1 + (Math.random() - 0.5) * 0.3;
const cRadius = sealRadius * 1.1 + (Math.random() - 0.5) * (sealRadius * 0.1);
const cp1x = sealActualX + cRadius * Math.cos(cAngle1);
const cp1y = sealActualY + cRadius * Math.sin(cAngle1);
ctx.quadraticCurveTo(cp1x, cp1y, px, py); // Wavier edges
}
}
ctx.closePath();
ctx.fill();
ctx.restore();
// Symbol on seal
ctx.fillStyle = sealSymbolColor;
ctx.font = `bold ${sealRadius * 0.9}px Arial`; // Use a common font for symbol
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText("★", sealActualX, sealActualY); // Star symbol
ctx.textBaseline = 'alphabetic'; // Reset
// Check for overflow
const finalContentBottomY = Math.max(sealActualY + sealRadius, dateBaselineY);
if (finalContentBottomY > canvas.height - pageMargin - borderThickness/2) {
console.warn("Content may have overflowed the canvas height. Consider reducing text or increasing canvas height.");
}
return canvas;
}
Apply Changes