Please bookmark this page to avoid losing your image tool!

Image Witch’s Spell Book Page 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,
    spellTitle = "Ancient Incantation",
    spellText = "Hoc est exemplar textūs pro cantibus et incantationibus. Verba hic scripta potentiam arcanam evocant, et symbola in pagina dispersa energias mysticas amplificant. Cave ne incaute legas, nam antiqui spiritus observant.",
    fontFamilyName = "MedievalSharp",
    textColor = "#3a2d1d", // Dark Brown
    paperColor = "#f5f5dc", // Beige / Old Paper
    stainColor = "rgba(160, 82, 45, 0.15)", // Sienna with alpha for stains
    imageBorderColor = "#5d4037", // Darker Brown
    imageBorderWidth = 3,
    titleFontSize = 40,
    textFontSize = 18,
    pagePadding = 40,
    canvasWidth = 700,
    canvasHeight = 1000
) {

    const fontMap = {
        "MedievalSharp": "https://fonts.gstatic.com/s/medievalsharp/v26/EvOJzAlL3oU5AQl2mP5KdgptvezUpanEwBM.woff2",
        "Uncial Antiqua": "https://fonts.gstatic.com/s/uncialantiqua/v22/N0bM2S5WOex4OUbESzoESK-i-MfX4zk.woff2",
        "Cinzel Decorative": "https://fonts.gstatic.com/s/cinzeldecorative/v19/daaHSScvJGqLYhGmdBAKDGNocIWomJwMAKSK.woff2"
    };

    async function loadWebFont(fontFamily, url) {
        if (!url) return false;

        for (const font of document.fonts) {
            if (font.family === fontFamily && font.status === 'loaded') {
                return true;
            }
        }
        
        if (document.fonts.check(`12px "${fontFamily}"`)) {
             try {
                await document.fonts.load(`12px "${fontFamily}"`);
                return true;
             } catch (e) { /* Will try FontFace loading next */ }
        }

        const fontFace = new FontFace(fontFamily, `url(${url}) format('woff2')`);
        try {
            await fontFace.load();
            document.fonts.add(fontFace);
            return true;
        } catch (e) {
            console.error(`Failed to load font ${fontFamily} from ${url}:`, e);
            return false;
        }
    }

    let actualFontFamily = "serif";
    const fontUrl = fontMap[fontFamilyName];

    if (fontUrl) {
        const fontLoaded = await loadWebFont(fontFamilyName, fontUrl);
        if (fontLoaded) {
            actualFontFamily = `"${fontFamilyName}"`;
        } else {
            console.warn(`Failed to load web font ${fontFamilyName}. Using fallback '${actualFontFamily}'.`);
        }
    } else {
        const generics = ["serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui"];
        if (generics.includes(fontFamilyName.toLowerCase())) {
            actualFontFamily = fontFamilyName;
        } else {
            actualFontFamily = `"${fontFamilyName}"`;
            if (!document.fonts.check(`12px ${actualFontFamily}`)) {
                 console.warn(`Font ${actualFontFamily} might not be available. Browser will attempt to use it or fall back.`);
            }
        }
    }

    const canvas = document.createElement('canvas');
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
    const ctx = canvas.getContext('2d');

    ctx.fillStyle = paperColor;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    function drawRandomStains(context, count, color, maxRadius, pageW, pageH) {
        context.fillStyle = color;
        for (let i = 0; i < count; i++) {
            const x = Math.random() * pageW;
            const y = Math.random() * pageH;
            const radiusX = Math.random() * maxRadius + 10; 
            const radiusY = Math.random() * maxRadius + 10;
            const rotation = Math.random() * Math.PI * 2;
            context.beginPath();
            context.ellipse(x, y, radiusX, radiusY, rotation, 0, Math.PI * 2);
            context.fill();
        }
    }
    drawRandomStains(ctx, 20, stainColor, canvasWidth / 10, canvas.width, canvas.height);

    let currentY = pagePadding;
    const contentAreaWidth = canvas.width - 2 * pagePadding;
    const contentAreaX = pagePadding;
    const pageBottomMargin = canvas.height - pagePadding;

    function getWrappedTextLines(context, text, maxWidth, fontStyleForCalc, baseFontSize) {
        const words = String(text).split(' ');
        const lines = [];
        let currentLine = "";
        context.font = fontStyleForCalc; // Set font for measurements

        let M_height_approx = baseFontSize;
        const tm = context.measureText("M"); // Metrics for a capital M
        if (tm.actualBoundingBoxAscent && tm.actualBoundingBoxDescent) {
            M_height_approx = tm.actualBoundingBoxAscent + tm.actualBoundingBoxDescent;
        } else if (tm.fontBoundingBoxAscent && tm.fontBoundingBoxDescent) {
             M_height_approx = tm.fontBoundingBoxAscent + tm.fontBoundingBoxDescent;
        }
        M_height_approx = Math.max(M_height_approx, baseFontSize * 0.8); // Ensure some sensible minimum

        for (let i = 0; i < words.length; i++) {
            const word = words[i];
            const testLine = currentLine === "" ? word : currentLine + " " + word;
            const metrics = context.measureText(testLine);
            if (metrics.width > maxWidth && currentLine !== "") {
                lines.push({ text: currentLine, height: M_height_approx });
                currentLine = word;
            } else {
                currentLine = testLine;
            }
        }
        if (currentLine !== "") {
            lines.push({ text: currentLine, height: M_height_approx });
        }
        return lines;
    }

    // --- Spell Title ---
    ctx.fillStyle = textColor;
    ctx.textAlign = 'center';
    const titleFont = `bold ${titleFontSize}px ${actualFontFamily}`;
    currentY += titleFontSize; // Initial baseline for the first line

    const titleLines = getWrappedTextLines(ctx, spellTitle, contentAreaWidth, titleFont, titleFontSize);
    ctx.font = titleFont; // Set font for drawing title
    titleLines.forEach(line => {
        if (currentY < pageBottomMargin){
            ctx.fillText(line.text, canvas.width / 2, currentY);
            currentY += line.height * 1.2; 
        }
    });
    currentY += titleFontSize * 0.3; // Spacing after title block

    // --- Image ---
    const spaceForImageAndText = pageBottomMargin - currentY;
    const imgMaxW = contentAreaWidth;
    // Allocate roughly 60% of remaining vertical space for image, ensure some min height
    const imgMaxH = Math.max(50, spaceForImageAndText * 0.55); 

    const aspectRatio = originalImg.naturalWidth > 0 && originalImg.naturalHeight > 0 ? 
                        originalImg.naturalWidth / originalImg.naturalHeight : 1;
    let dispW = imgMaxW;
    let dispH = dispW / aspectRatio;

    if (dispH > imgMaxH) {
        dispH = imgMaxH;
        dispW = dispH * aspectRatio;
    }
    if (dispW > imgMaxW) {
        dispW = imgMaxW;
        dispH = dispW / aspectRatio;
    }    
    dispW = Math.max(10, dispW); // Ensure min dimensions
    dispH = Math.max(10, dispH);

    const imgX = (canvas.width - dispW) / 2;
    const imgY = currentY; // Image top aligns with currentY

    // Check if image fits before drawing
    if (imgY + dispH < pageBottomMargin - (textFontSize * 2)) { // Reserve space for at least 2 lines of text
        if (originalImg.complete && originalImg.naturalWidth > 0) { // Ensure image is loaded
             if (imageBorderWidth > 0) {
                ctx.strokeStyle = imageBorderColor;
                ctx.lineWidth = imageBorderWidth;
                ctx.strokeRect(
                    imgX - imageBorderWidth / 2,
                    imgY - imageBorderWidth / 2,
                    dispW + imageBorderWidth,
                    dispH + imageBorderWidth
                );
            }
            ctx.drawImage(originalImg, imgX, imgY, dispW, dispH);
        }
        currentY += dispH; // Advance Y to bottom of image
    }    
    currentY += textFontSize; // currentY becomes baseline for the first line of spell text

    // --- Spell Text ---
    ctx.fillStyle = textColor;
    ctx.textAlign = 'left';
    const textFont = `${textFontSize}px ${actualFontFamily}`;
    const textStartX = contentAreaX + 10;

    const spellTextLines = getWrappedTextLines(ctx, spellText, contentAreaWidth - 20, textFont, textFontSize);
    ctx.font = textFont; // Set font for drawing text
    spellTextLines.forEach(line => {
        if (currentY < pageBottomMargin) {
            ctx.fillText(line.text, textStartX, currentY);
            currentY += line.height * 1.4; 
        }
    });
    
    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 Witch’s Spell Book Page Creator is an online tool designed to transform images into beautifully styled pages resembling those found in a mystical spell book. Users can upload an image, and the tool will overlay a customizable spell title and text, using decorative fonts and artistic layouts. This tool is ideal for creating unique fantasy-themed pages for games, storytelling, or personalized projects. It allows for adjustments in styling, such as text color, font choice, and background effects to enhance the pages with an ancient and magical aesthetic.

Leave a Reply

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