Please bookmark this page to avoid losing your image tool!

Image Royal Decree Proclamation Creator

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
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;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Royal Decree Proclamation Creator is an online tool that allows users to create visually appealing royal decree proclamations using customizable text and images. Users can specify a title, main text, closing statements, and signatures, all rendered in elegant medieval-style fonts. This tool is ideal for events such as themed parties, historical reenactments, or educational activities where users want to create playful or authentic-looking proclamations. The tool also features options for color adjustments and decorative elements, making it versatile for various creative projects.

Leave a Reply

Your email address will not be published. Required fields are marked *