Please bookmark this page to avoid losing your image tool!

Mythical Kingdom Image Document 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 = "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;
}

Free Image Tool Creator

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

Description

The Mythical Kingdom Image Document Creator is a versatile online tool that allows users to create visually appealing image-based documents with a medieval theme. Users can upload an image to be incorporated into a document layout that features decorative borders, styled fonts, and customizable text elements. This tool is ideal for crafting royal decrees, certificates, invitations, or any other ceremonial documents that require an elegant presentation. It allows for personalization through various options like title and body text, border colors, and seal designs, making it perfect for event planning, gaming campaigns, or any creative project that seeks to evoke a sense of fantasy and nobility.

Leave a Reply

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