You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
titleText = "Royal Decree",
bodyText = "Hark, let it be known throughout the realms and beyond the farthest borders that this edict holds true and steadfast. By order of the High Sovereign, and with the full assent of the Elder Council, the bearer of this document is hereby recognized and honored for their valiant deeds, unwavering loyalty to the Crown, and contributions to the enduring peace and prosperity of the Kingdom of Eldoria.",
borderColor = "#8B4513", // SaddleBrown
titleFontFamily = "MedievalSharp, fantasy",
bodyFontFamily = "IM Fell DW Pica, serif",
paperColor = "#F5E8C8", // Parchment
imageStyle = "portrait", // 'portrait' or 'illustration'
sealText = "Official Seal",
sealColor = "#800000" // Maroon
) {
// 1. Font Loading Helper
const loadWebFont = async (fontFamilyName, fontUrl) => {
// Normalize font family name (remove quotes for checking)
const normalizedFontName = fontFamilyName.replace(/['"]/g, '');
if (document.fonts.check(`12px ${normalizedFontName}`)) {
return true;
}
const fontFace = new FontFace(normalizedFontName, `url(${fontUrl})`);
try {
await fontFace.load();
document.fonts.add(fontFace);
return true;
} catch (e) {
console.error(`Font ${normalizedFontName} (${fontUrl}) failed to load:`, e);
return false;
}
};
const knownFonts = {
"MedievalSharp": "https://fonts.gstatic.com/s/medievalsharp/v25/EvOJzAlL3oU5AQl2mP5KdgptAqZM5Q.woff2",
"IM Fell DW Pica": "https://fonts.gstatic.com/s/imfelldwpica/v17/2sDGZGRQotv9_Bqa2onMHsV973LhM7E_Kg.woff2",
};
const fontPromises = [];
const primaryTitleFont = titleFontFamily.split(',')[0].trim().replace(/['"]/g, '');
const primaryBodyFont = bodyFontFamily.split(',')[0].trim().replace(/['"]/g, '');
if (knownFonts[primaryTitleFont]) {
fontPromises.push(loadWebFont(primaryTitleFont, knownFonts[primaryTitleFont]));
}
if (knownFonts[primaryBodyFont]) {
fontPromises.push(loadWebFont(primaryBodyFont, knownFonts[primaryBodyFont]));
}
if (fontPromises.length > 0) {
await Promise.all(fontPromises);
}
// 2. Canvas Setup
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const docWidth = 800;
const docHeight = 1100;
canvas.width = docWidth;
canvas.height = docHeight;
// 3. Background
ctx.fillStyle = paperColor;
ctx.fillRect(0, 0, docWidth, docHeight);
// 4. Decorative Borders
const mainBorderWidth = 15;
const innerBorderPaddingFromEdge = mainBorderWidth + 10;
ctx.strokeStyle = borderColor;
ctx.lineWidth = mainBorderWidth;
ctx.strokeRect(mainBorderWidth / 2, mainBorderWidth / 2, docWidth - mainBorderWidth, docHeight - mainBorderWidth);
ctx.strokeStyle = "rgba(218,165,32,0.7)"; // Goldenrod with transparency
ctx.lineWidth = 3;
ctx.strokeRect(innerBorderPaddingFromEdge, innerBorderPaddingFromEdge, docWidth - 2 * innerBorderPaddingFromEdge, docHeight - 2 * innerBorderPaddingFromEdge);
// 5. Content Area Definition
const contentPadding = innerBorderPaddingFromEdge + 25;
const contentWidth = docWidth - 2 * contentPadding;
const contentBottomY = docHeight - contentPadding;
let currentY = contentPadding;
// 6. Title
ctx.fillStyle = "#4A2A00";
ctx.font = `bold 48px ${titleFontFamily}`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
currentY += 20; // Top margin for title
ctx.fillText(titleText, docWidth / 2, currentY);
const titleHeight = ctx.measureText("M").actualBoundingBoxAscent || 48; //Approximate height
currentY += titleHeight + 30; // Space after title
// 7. Image
if (originalImg && originalImg.width > 0 && originalImg.height > 0) {
const imageMaxContainerWidth = contentWidth * 0.65;
const imageMaxContainerHeight = docHeight * 0.30;
let imgDrawWidth = originalImg.width;
let imgDrawHeight = originalImg.height;
const imageAspectRatio = originalImg.width / originalImg.height;
if (imgDrawWidth > imageMaxContainerWidth) {
imgDrawWidth = imageMaxContainerWidth;
imgDrawHeight = imgDrawWidth / imageAspectRatio;
}
if (imgDrawHeight > imageMaxContainerHeight) {
imgDrawHeight = imageMaxContainerHeight;
imgDrawWidth = imgDrawHeight * imageAspectRatio;
}
const imgX = (docWidth - imgDrawWidth) / 2;
const imgY = currentY;
if (imgDrawWidth > 0 && imgDrawHeight > 0) {
if (imageStyle === "portrait") {
ctx.save();
const frameOuterPadding = 8;
const frameInnerPadding = 4;
ctx.strokeStyle = borderColor;
ctx.lineWidth = 6;
ctx.strokeRect(imgX - frameOuterPadding, imgY - frameOuterPadding, imgDrawWidth + 2 * frameOuterPadding, imgDrawHeight + 2 * frameOuterPadding);
ctx.strokeStyle = "goldenrod";
ctx.lineWidth = 2;
ctx.strokeRect(imgX - frameOuterPadding - frameInnerPadding, imgY - frameOuterPadding - frameInnerPadding,
imgDrawWidth + 2 * (frameOuterPadding + frameInnerPadding), imgDrawHeight + 2 * (frameOuterPadding + frameInnerPadding));
ctx.restore();
currentY += imgDrawHeight + 2 * (frameOuterPadding + frameInnerPadding);
} else {
currentY += imgDrawHeight;
}
ctx.drawImage(originalImg, imgX, imgY, imgDrawWidth, imgDrawHeight);
}
}
currentY += 30;
// --- Elements from bottom-up for better space management ---
let bottomCursorY = contentBottomY - 10;
// 9. Seal
const sealRadius = 35;
const sealX = docWidth - contentPadding - sealRadius - 20;
const sealY = bottomCursorY - sealRadius - 10;
ctx.fillStyle = sealColor;
ctx.beginPath();
const sealPoints = 20;
for (let i = 0; i < sealPoints * 2; i++) {
const angle = (i * Math.PI) / sealPoints;
const rFactor = (i % 2 === 0 ? 1 : 0.80);
const r = sealRadius * rFactor;
const px = sealX + r * Math.cos(angle);
const py = sealY + r * Math.sin(angle);
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
ctx.fillStyle = "rgba(255, 255, 220, 0.9)";
const sealFontName = bodyFontFamily.split(',')[0].trim().replace(/['"]/g, '');
ctx.font = `bold 10px ${sealFontName}, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const sealWords = sealText.toUpperCase().split(' ');
if (sealWords.length === 1) {
ctx.fillText(sealWords[0], sealX, sealY);
} else if (sealWords.length > 1) {
ctx.fillText(sealWords[0], sealX, sealY - 5);
ctx.fillText(sealWords.slice(1).join(' '), sealX, sealY + 7);
}
bottomCursorY = sealY - sealRadius - 20;
// 10. Optional: "Handwritten" Signature line
const sigLineHeight = 50;
if (bottomCursorY - sigLineHeight > currentY + 20) {
const sigLineYPos = bottomCursorY - 15;
const sigLineStartX = docWidth - contentPadding - 250;
const sigLineEndX = docWidth - contentPadding - 30;
ctx.beginPath();
ctx.moveTo(sigLineStartX, sigLineYPos);
ctx.quadraticCurveTo(sigLineStartX + (sigLineEndX - sigLineStartX) / 2, sigLineYPos - Math.random()*6 - 2 , sigLineEndX, sigLineYPos + Math.random()*4 -1 );
ctx.strokeStyle = "#5D4037";
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = "#5D4037";
ctx.font = `italic 16px ${bodyFontFamily}`;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillText("By Royal Assent", sigLineStartX + (sigLineEndX - sigLineStartX) / 2, sigLineYPos - 8);
bottomCursorY = sigLineYPos - 35;
}
// 8. Body Text
ctx.fillStyle = "#4A2A00";
ctx.font = `20px ${bodyFontFamily}`;
ctx.textAlign = "left";
ctx.textBaseline = "top";
const lineHeigh_t = 26;
const textX = contentPadding + 10;
const textMaxWidth = contentWidth - 20;
const availableTextHeight = Math.max(0, bottomCursorY - currentY);
function wrapText(context, text, x, startY, maxWidth, lineHeight, maxHeight) {
const words = text.split(' ');
let line = '';
let currentTextY = startY;
const endY = startY + maxHeight;
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = context.measureText(testLine);
if ((metrics.width > maxWidth && n > 0) || line.length > 200) { // Also break very long lines without spaces
if (currentTextY + lineHeight > endY) { // Not enough space for the next full line
let lastChunk = line.trim();
while (context.measureText(lastChunk + "...").width > maxWidth && lastChunk.length > 3) {
lastChunk = lastChunk.slice(0, -1);
}
if (lastChunk.length > 0) context.fillText(lastChunk + "...", x, currentTextY);
return currentTextY + lineHeight;
}
context.fillText(line.trim(), x, currentTextY);
line = words[n] + ' ';
currentTextY += lineHeight;
} else {
line = testLine;
}
}
// Draw the last remaining line if space allows
if (currentTextY + lineHeight <= endY + 1 || (currentTextY <= endY + 1 && context.measureText(line.trim()).width <= maxWidth)) {
context.fillText(line.trim(), x, currentTextY);
currentTextY += lineHeight;
} else if (currentTextY <= endY +1) { // Try to fit truncated last line
let lastChunk = line.trim();
while (context.measureText(lastChunk + "...").width > maxWidth && lastChunk.length > 3) {
lastChunk = lastChunk.slice(0, -1);
}
if (lastChunk.length > 0) context.fillText(lastChunk + "...", x, currentTextY);
currentTextY += lineHeight;
}
return currentTextY;
}
if (availableTextHeight > lineHeigh_t) { // Only draw text if there's meaningful space
wrapText(ctx, bodyText, textX, currentY, textMaxWidth, lineHeigh_t, availableTextHeight);
}
return canvas;
}
Apply Changes