Please bookmark this page to avoid losing your image tool!

Image Wizard’s Magical Tome 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,
    coverColor = "#4B0082",      // Dark purple/indigo for a magical feel
    borderColor = "#FFD700",      // Gold for richness
    spineTitleText = "Tome of Secrets",
    spineTitleColor = "#FFFFE0", // Light Yellow / Ivory for text visibility
    pageColor = "#F5F5DC",        // Beige for aged paper
    pageLineColor = "rgba(0,0,0,0.12)", // Subtle lines for pages
    coverAccentColor = "#DAA520", // Goldenrod (darker gold for corners/accents)
    coverTitleText = "",           // Optional title on the front cover, empty by default
    coverTitleFont = "30px 'MedievalSharp', Times, serif", // Font for cover title
    coverTitleColor = "#FFFFE0",   // Color for cover title
    spineTitleFont = "20px 'MedievalSharp', Times, serif"  // Font for spine title
) {

    // Helper function to load custom font dynamically using FontFace API
    async function loadCustomFont(fontFamily, fontUrl, fontWeight = 'normal', fontStyle = 'normal') {
        const fontId = `${fontFamily}-${fontWeight}-${fontStyle}`;
        if (!window.loadedFonts) {
            window.loadedFonts = {}; // Cache to track loaded fonts globally
        }

        // If already attempted and succeeded/failed, or font is already available system-wide
        if (window.loadedFonts[fontId] === 'loaded' || document.fonts.check(`12px "${fontFamily}"`, { weight: fontWeight, style: fontStyle })) {
            if (!document.fonts.check(`12px "${fontFamily}"`, { weight: fontWeight, style: fontStyle })) {
                 // It was marked loaded but check fails. Maybe cleared from document.fonts. Try reloading.
            } else {
                return true;
            }
        }
        if (window.loadedFonts[fontId] === 'failed') return false;

        try {
            const fontFace = new FontFace(fontFamily, `url(${fontUrl})`, {
                weight: fontWeight,
                style: fontStyle,
                display: 'swap', // Use swap for better perceived performance
            });
            await fontFace.load();
            document.fonts.add(fontFace);
            window.loadedFonts[fontId] = 'loaded';
            return true;
        } catch (e) {
            console.warn(`Failed to load font '${fontFamily}' (${fontWeight}, ${fontStyle}) from ${fontUrl}:`, e);
            window.loadedFonts[fontId] = 'failed';
            return false;
        }
    }

    // Load the 'MedievalSharp' font. It's a Google Font suitable for a magical tome.
    // Using the direct .woff2 URL for 'MedievalSharp Regular 400'.
    const medievalSharpFontUrl = 'https://fonts.gstatic.com/s/medievalsharp/v21/EvNMgfuHkjOK_S9N42Xbf-bFquHXAIlMzw.woff2';
    await loadCustomFont('MedievalSharp', medievalSharpFontUrl, '400', 'normal');

    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    const canvasWidth = 600;
    const canvasHeight = 800;
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;

    // Book dimensions and positioning
    const bookTotalHeight = 700;
    const bookTotalWidth = 500;
    const bookX = (canvasWidth - bookTotalWidth) / 2;
    const bookY = (canvasHeight - bookTotalHeight) / 2;

    const spineWidth = 50;
    const pageEdgeWidth = 25;
    const coverOverhangVertical = 10; // How much cover extends beyond pages vertically

    const frontCoverPanelWidth = bookTotalWidth - spineWidth - pageEdgeWidth;

    const pageBlockVisualY = bookY + coverOverhangVertical;
    const pageBlockVisualHeight = bookTotalHeight - 2 * coverOverhangVertical;

    // 1. Draw Page Edges (Right Side)
    const pageEdgeX = bookX + bookTotalWidth - pageEdgeWidth;
    ctx.fillStyle = pageColor;
    ctx.fillRect(pageEdgeX, pageBlockVisualY, pageEdgeWidth, pageBlockVisualHeight);

    // Draw lines to simulate pages
    ctx.strokeStyle = pageLineColor;
    ctx.lineWidth = 0.5;
    for (let y_ = pageBlockVisualY; y_ < pageBlockVisualY + pageBlockVisualHeight; y_ += 3) {
        ctx.beginPath();
        ctx.moveTo(pageEdgeX, y_);
        ctx.lineTo(pageEdgeX + pageEdgeWidth, y_);
        ctx.stroke();
    }
    // Add a subtle shadow to make pages look inset from the cover edge
    ctx.fillStyle = 'rgba(0,0,0,0.05)';
    ctx.fillRect(pageEdgeX, pageBlockVisualY, pageEdgeWidth, pageBlockVisualHeight);


    // 2. Draw Spine (Left Side)
    ctx.fillStyle = coverColor;
    ctx.fillRect(bookX, bookY, spineWidth, bookTotalHeight); // Base spine color

    // Spine shading for a rounded effect (using translucent overlays)
    ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; // Darker shade
    ctx.fillRect(bookX, bookY, spineWidth * 0.4, bookTotalHeight);
    ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; // Lighter highlight
    ctx.fillRect(bookX + spineWidth * 0.6, bookY, spineWidth * 0.4, bookTotalHeight);


    // 3. Draw Front Cover Panel
    const fcPanelX = bookX + spineWidth;
    ctx.fillStyle = coverColor;
    ctx.fillRect(fcPanelX, bookY, frontCoverPanelWidth, bookTotalHeight);


    // 4. Front Cover Decorations
    // Border
    const borderMargin = 20;
    const borderWidth = 6;
    ctx.strokeStyle = borderColor;
    ctx.lineWidth = borderWidth;
    const innerBorderX = fcPanelX + borderMargin;
    const innerBorderY = bookY + borderMargin;
    const innerBorderW = frontCoverPanelWidth - 2 * borderMargin;
    const innerBorderH = bookTotalHeight - 2 * borderMargin;
    if (innerBorderW > 0 && innerBorderH > 0) {
        ctx.strokeRect(innerBorderX + borderWidth/2, innerBorderY + borderWidth/2, innerBorderW - borderWidth, innerBorderH - borderWidth);
    }


    // Corner Decorations
    const cornerRadius = 25;
    ctx.fillStyle = coverAccentColor;
    if (innerBorderW > cornerRadius * 2 && innerBorderH > cornerRadius * 2) {
        // Top-left
        ctx.beginPath(); ctx.moveTo(innerBorderX, innerBorderY + cornerRadius);
        ctx.arcTo(innerBorderX, innerBorderY, innerBorderX + cornerRadius, innerBorderY, cornerRadius);
        ctx.lineTo(innerBorderX, innerBorderY); ctx.closePath(); ctx.fill();
        // Top-right
        ctx.beginPath(); ctx.moveTo(innerBorderX + innerBorderW - cornerRadius, innerBorderY);
        ctx.arcTo(innerBorderX + innerBorderW, innerBorderY, innerBorderX + innerBorderW, innerBorderY + cornerRadius, cornerRadius);
        ctx.lineTo(innerBorderX + innerBorderW, innerBorderY); ctx.closePath(); ctx.fill();
        // Bottom-left
        ctx.beginPath(); ctx.moveTo(innerBorderX, innerBorderY + innerBorderH - cornerRadius);
        ctx.arcTo(innerBorderX, innerBorderY + innerBorderH, innerBorderX + cornerRadius, innerBorderY + innerBorderH, cornerRadius);
        ctx.lineTo(innerBorderX, innerBorderY + innerBorderH); ctx.closePath(); ctx.fill();
        // Bottom-right
        ctx.beginPath(); ctx.moveTo(innerBorderX + innerBorderW - cornerRadius, innerBorderY + innerBorderH);
        ctx.arcTo(innerBorderX + innerBorderW, innerBorderY + innerBorderH, innerBorderX + innerBorderW, innerBorderY + innerBorderH - cornerRadius, cornerRadius);
        ctx.lineTo(innerBorderX + innerBorderW, innerBorderY + innerBorderH); ctx.closePath(); ctx.fill();
    }


    // 5. Draw originalImg on Front Cover
    const imgPaddingFromBorder = Math.max(5, cornerRadius / 2 + 5) ; // Ensure some padding even without corners
    const imgAvailableX = innerBorderX + imgPaddingFromBorder;
    const imgAvailableY = innerBorderY + imgPaddingFromBorder;
    const imgAvailableW = innerBorderW - 2 * imgPaddingFromBorder;
    const imgAvailableH = innerBorderH - 2 * imgPaddingFromBorder;
    let imgDrawn = false; // Flag to track if image was drawn

    if (imgAvailableW > 0 && imgAvailableH > 0 && originalImg && originalImg.width > 0 && originalImg.height > 0) {
        const imgAspect = originalImg.width / originalImg.height;
        let drawW = imgAvailableW;
        let drawH = drawW / imgAspect;

        if (drawH > imgAvailableH) {
            drawH = imgAvailableH;
            drawW = drawH * imgAspect;
        }
        // Center the image
        const imgDrawX = imgAvailableX + (imgAvailableW - drawW) / 2;
        const imgDrawY = imgAvailableY + (imgAvailableH - drawH) / 2;
        
        try {
            ctx.drawImage(originalImg, imgDrawX, imgDrawY, drawW, drawH);
            imgDrawn = true; // Set flag
        } catch (e) {
            console.warn("Error drawing image, it might not be fully loaded or is invalid:", e);
        }
    }


    // 6. Draw Spine Title
    if (spineTitleText && spineTitleText.trim() !== "") {
        ctx.font = spineTitleFont;
        ctx.fillStyle = spineTitleColor;
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.save();
        ctx.translate(bookX + spineWidth / 2, bookY + bookTotalHeight / 2);
        ctx.rotate(-Math.PI / 2); // Rotate text for spine
        ctx.fillText(spineTitleText, 0, 0);
        ctx.restore();
    }

    // 7. Draw Cover Title (if provided)
    if (coverTitleText && coverTitleText.trim() !== "") {
        ctx.font = coverTitleFont;
        ctx.fillStyle = coverTitleColor;
        ctx.textAlign = "center";
        
        let titleY;
        const metrics = ctx.measureText("M"); // A character to estimate height
        const titleHeightEstimate = (metrics.actualBoundingBoxAscent || parseInt(ctx.font, 10) * 0.75) + (metrics.actualBoundingBoxDescent || parseInt(ctx.font, 10) * 0.25) ;
        
        if (imgDrawn) {
            const imgBottomY = (imgAvailableY + (imgAvailableH - (innerBorderW / (originalImg.width / originalImg.height) > imgAvailableH ? imgAvailableH : innerBorderW / (originalImg.width / originalImg.height)))/2 ) + (innerBorderW / (originalImg.width / originalImg.height) > imgAvailableH ? imgAvailableH : innerBorderW / (originalImg.width / originalImg.height));
            const spaceBelowImage = (innerBorderY + innerBorderH) - imgBottomY;

            if (spaceBelowImage > titleHeightEstimate + 10) { // Prefer below image
                titleY = imgBottomY + 10 ;
                ctx.textBaseline = "top";
            } else { // Try above image
                 const imgTopY = imgAvailableY + (imgAvailableH - (innerBorderW / (originalImg.width / originalImg.height) > imgAvailableH ? imgAvailableH : innerBorderW / (originalImg.width / originalImg.height)))/2 ;
                if (imgTopY - innerBorderY > titleHeightEstimate + 10) {
                    titleY = imgTopY - 10;
                    ctx.textBaseline = "bottom";
                } else { // Fallback: center of image area
                    titleY = imgAvailableY + imgAvailableH / 2;
                    ctx.textBaseline = "middle";
                }
            }
        } else { // No image, center title in the available decorated area
            titleY = innerBorderY + innerBorderH / 2;
            ctx.textBaseline = "middle";
        }
        if (innerBorderW > 0) { // Only draw if there's space
             ctx.fillText(coverTitleText, innerBorderX + innerBorderW / 2, titleY);
        }
    }

    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

Image Wizard’s Magical Tome Creator is a versatile online tool that enables users to create beautifully designed book covers with a mystical aesthetic. Users can upload an image to be featured on the front cover, specify various design elements such as cover and spine colors, choose text for the spine, and add decorative accents. This tool is ideal for authors, artists, and creators looking to produce visually appealing book covers, customizable for genres like fantasy, fiction, or any thematic projects. Perfect for enhancing presentations, self-publishing projects, or personal use, it allows for unique, personalized designs that stand out.

Leave a Reply

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