Please bookmark this page to avoid losing your image tool!

Image Vintage Apothecary Label 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 = "Mystic Elixir",
    subtitleText = "Pure & Potent",
    bodyText = "A wondrous concoction of distilled moonbeams, phoenix feathers, and whispering willow bark. Brewed under celestial alignments, this elixir is guaranteed to invigorate the spirit and illuminate the mind. Take one spoonful when the stars align for maximum potency.",
    footerText = "Arcane Apothecary Guild - Est. MCDLXII",
    fontName = "IM Fell English SC",
    fallbackFont = "Times New Roman, serif",
    backgroundColor = "#f2e8d9", // Aged paper cream
    textColor = "#4a3b31", // Dark sepia/brown
    borderColor = "#6f5f51", // Muted brown for border
    borderStyle = "double", // "single", "double", "none"
    borderWidth = 2, // px, for each line of the border
    imagePlacement = "center", // "center", "background", "none"
    centerImageScale = 0.7, // Relative scale for centered image (0-1 of its allocated space)
    backgroundImageOpacity = 0.1, // Opacity for background image (0-1)
    vignetteOpacity = 0.35, // Opacity for vignette (0-1)
    textureIntensity = 0.05 // Intensity of noise texture (0-1), 0 for none
) {

    // --- Helper Functions ---

    async function loadFontIfNeeded(fontToLoad, baseFont) {
        try {
            // Check if font is already loaded or is generic
            if (document.fonts.check(`12px "${fontToLoad}"`) || fontToLoad.toLowerCase() === baseFont.toLowerCase()) {
                return `"${fontToLoad}", ${baseFont}`;
            }
            // Attempt to load from Google Fonts (common for 'IM Fell English SC')
            // More robust would be specific URLs for other fonts.
            // For "IM Fell English SC":
            if (fontToLoad === "IM Fell English SC") {
                 const font = new FontFace(fontToLoad, `url(https://fonts.gstatic.com/s/imfellenglishsc/v17/a8IENpD3CDX-4zrWfr1VY879qYWc05R9d_k.woff2)`);
                 await font.load();
                 document.fonts.add(font);
                 return `"${fontToLoad}", ${baseFont}`;
            }
            console.warn(`Font "${fontToLoad}" not preloaded and no specific loader defined. Using fallback "${baseFont}".`);
            return `"${baseFont}"`; // Fallback if not a known special font
        } catch (e) {
            console.warn(`Could not load font "${fontToLoad}", using fallback "${baseFont}". Error:`, e);
            return `"${baseFont}"`;
        }
    }

    function getWrappedLines(context, text, maxWidth, fontStyle) {
        context.font = fontStyle;
        const words = text.split(' ');
        const lines = [];
        if (!words[0]) return []; // Handle empty text
        
        let currentLine = words[0];

        for (let i = 1; i < words.length; i++) {
            const word = words[i];
            const testLine = currentLine + " " + word;
            if (context.measureText(testLine).width < maxWidth || currentLine === "") { // Allow single word to exceed maxWidth if it's the first on line
                currentLine = testLine;
            } else {
                lines.push(currentLine);
                currentLine = word;
            }
        }
        lines.push(currentLine);
        return lines;
    }

    function createNoisePattern(width, height, R = 0, G = 0, B = 0, A = 20, density = 0.5) {
        const noiseCanvas = document.createElement('canvas');
        noiseCanvas.width = width;
        noiseCanvas.height = height;
        const noiseCtx = noiseCanvas.getContext('2d');
        if (!noiseCtx) return null; // Canvas context might not be available in some environments
        const id = noiseCtx.createImageData(width, height);
        const data = id.data;
        const len = data.length;

        for (let i = 0; i < len; i += 4) {
            if (Math.random() < density) {
                data[i] = R;
                data[i + 1] = G;
                data[i + 2] = B;
                data[i + 3] = A;
            } else {
                data[i + 3] = 0; // Transparent
            }
        }
        noiseCtx.putImageData(id, 0, 0);
        return noiseCanvas;
    }
    
    async function ensureImageLoaded(img) {
        if (img && img.src && !img.complete) {
            try {
                await new Promise((resolve, reject) => {
                    img.onload = resolve;
                    img.onerror = () => reject(new Error("Image failed to load."));
                    if (img.complete) resolve(); // Already loaded
                });
            } catch (error) {
                console.error(error.message);
                return false;
            }
        }
        return img && img.naturalWidth > 0 && img.naturalHeight > 0;
    }


    // --- Main Logic ---

    const imageIsValidAndLoaded = await ensureImageLoaded(originalImg);
    const effectiveFont = await loadFontIfNeeded(fontName, fallbackFont);

    const CANVAS_W = 400;
    const CANVAS_H = 600;
    const PADDING = 20;

    const canvas = document.createElement('canvas');
    canvas.width = CANVAS_W;
    canvas.height = CANVAS_H;
    const ctx = canvas.getContext('2d');
    if (!ctx) return document.createElement('div'); // Fallback if context fails

    // 1. Background Color
    ctx.fillStyle = backgroundColor;
    ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);

    // 2. Background Image (if specified)
    if (imagePlacement === 'background' && imageIsValidAndLoaded) {
        ctx.save();
        ctx.globalAlpha = backgroundImageOpacity;
        const imgAspect = originalImg.naturalWidth / originalImg.naturalHeight;
        const canvasAspect = CANVAS_W / CANVAS_H;
        let sx = 0, sy = 0, sWidth = originalImg.naturalWidth, sHeight = originalImg.naturalHeight;

        if (imgAspect > canvasAspect) { // Image wider than canvas aspect (crop sides)
            sWidth = originalImg.naturalHeight * canvasAspect;
            sx = (originalImg.naturalWidth - sWidth) / 2;
        } else { // Image taller than canvas aspect (crop top/bottom)
            sHeight = originalImg.naturalWidth / canvasAspect;
            sy = (originalImg.naturalHeight - sHeight) / 2;
        }
        ctx.drawImage(originalImg, sx, sy, sWidth, sHeight, 0, 0, CANVAS_W, CANVAS_H);
        ctx.restore();
    }

    // 3. Texture Overlay
    if (textureIntensity > 0) {
        const noiseAlpha = Math.floor(Math.max(0, Math.min(1, textureIntensity)) * 50); // Map 0-1 intensity to 0-50 alpha
        const noiseTile = createNoisePattern(100, 100, 0, 0, 0, noiseAlpha, 0.5);
        if (noiseTile) {
            const pattern = ctx.createPattern(noiseTile, 'repeat');
            if (pattern) {
                ctx.fillStyle = pattern;
                ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
            }
        }
    }
    
    // --- Content Area and Borders Setup ---
    const contentX = PADDING;
    const contentY = PADDING;
    const contentW = CANVAS_W - 2 * PADDING;
    const contentH = CANVAS_H - 2 * PADDING;

    // Draw Borders (within the contentX/Y/W/H area)
    if (borderStyle !== "none") {
        ctx.strokeStyle = borderColor;
        ctx.lineWidth = borderWidth;
        const bw_half = borderWidth / 2;

        // Outer border line
        ctx.strokeRect(contentX + bw_half, contentY + bw_half, contentW - borderWidth, contentH - borderWidth);

        if (borderStyle === "double") {
            const gap = 5; // Space between double border lines
            const innerBorderOffset = borderWidth + gap;
            ctx.strokeRect(
                contentX + innerBorderOffset + bw_half,
                contentY + innerBorderOffset + bw_half,
                contentW - 2 * innerBorderOffset - borderWidth,
                contentH - 2 * innerBorderOffset - borderWidth
            );
        }
    }
    
    // Calculate available text block area (inside borders and with internal margins)
    let textBlockBorderOffset = 0;
    if (borderStyle !== "none") {
        textBlockBorderOffset = borderWidth;
        if (borderStyle === "double") {
            textBlockBorderOffset += 5 + borderWidth; // gap + inner border width
        }
    }
    const textMargin = 15; // Inner margin for text from borders
    
    const availableTextX = contentX + textBlockBorderOffset + textMargin;
    const availableTextY = contentY + textBlockBorderOffset + textMargin;
    const availableTextW = contentW - 2 * (textBlockBorderOffset + textMargin);
    const availableTextH = contentH - 2 * (textBlockBorderOffset + textMargin);
    
    const textCenterX = availableTextX + availableTextW / 2;
    let currentY = availableTextY;

    ctx.fillStyle = textColor;
    ctx.textAlign = 'center';

    // --- Text Elements and Centered Image ---

    // Title
    if (titleText) {
        const titleFontSize = 36;
        const titleLineHeight = titleFontSize * 1.1;
        ctx.font = `bold ${titleFontSize}px ${effectiveFont}`;
        const titleLines = getWrappedLines(ctx, titleText, availableTextW, `bold ${titleFontSize}px ${effectiveFont}`);
        for (const line of titleLines) {
            ctx.fillText(line, textCenterX, currentY + titleFontSize * 0.8); // Adjust for baseline
            currentY += titleLineHeight;
        }
        currentY += titleLineHeight * 0.3; // Extra space after title
    }

    // Subtitle
    if (subtitleText) {
        const subtitleFontSize = 20;
        const subtitleLineHeight = subtitleFontSize * 1.2;
        ctx.font = `${subtitleFontSize}px ${effectiveFont}`;
        const subtitleLines = getWrappedLines(ctx, subtitleText, availableTextW * 0.9, `${subtitleFontSize}px ${effectiveFont}`); // slightly less width for subtitle
        for (const line of subtitleLines) {
            ctx.fillText(line, textCenterX, currentY + subtitleFontSize * 0.8);
            currentY += subtitleLineHeight;
        }
        currentY += subtitleLineHeight * 0.5; // Extra space after subtitle
    }
    
    // Optional Separator (simple line)
    if (titleText || subtitleText) { // Add separator if there's any text above
        currentY += 5;
        ctx.beginPath();
        ctx.moveTo(availableTextX + availableTextW * 0.15, currentY);
        ctx.lineTo(availableTextX + availableTextW * 0.85, currentY);
        ctx.lineWidth = Math.max(1, borderWidth / 2); // Thinner line relative to border
        ctx.strokeStyle = textColor; // Use text color for separator consistency
        ctx.stroke();
        currentY += 15;
    }


    // Centered Image
    const centeredImgMaxH = 120; // Max height for the centered image
    let centeredImgActualH = 0;
    if (imagePlacement === 'center' && imageIsValidAndLoaded) {
        let imgDrawW = originalImg.naturalWidth;
        let imgDrawH = originalImg.naturalHeight;
        const imgMaxW = availableTextW * 0.8; // Max width for image, leave some padding

        // Scale to fit, preserving aspect ratio
        if (imgDrawW > imgMaxW) {
            const ratio = imgMaxW / imgDrawW;
            imgDrawW = imgMaxW;
            imgDrawH *= ratio;
        }
        if (imgDrawH > centeredImgMaxH) {
            const ratio = centeredImgMaxH / imgDrawH;
            imgDrawH = centeredImgMaxH;
            imgDrawW *= ratio;
        }
        
        imgDrawW *= centerImageScale; // Apply user scale
        imgDrawH *= centerImageScale;

        if (imgDrawW > 0 && imgDrawH > 0 && (currentY + imgDrawH) < (availableTextY + availableTextH - 50) ) { // Check if fits
            const imgX = textCenterX - imgDrawW / 2;
            ctx.drawImage(originalImg, imgX, currentY, imgDrawW, imgDrawH);
            currentY += imgDrawH;
            centeredImgActualH = imgDrawH;
            currentY += 20; // Space after image
        }
    }
    
    // Footer setup
    const footerFontSize = 13;
    const footerReserveHeight = footerFontSize * 2.5; // Includes lines and spacing
    const footerBaselineY = availableTextY + availableTextH - footerFontSize * 0.5; // Baseline for last line of footer

    // Body Text
    if (bodyText) {
        const bodyFontSize = 15;
        const bodyLineHeight = bodyFontSize * 1.3;
        ctx.font = `${bodyFontSize}px ${effectiveFont}`;
        ctx.textAlign = 'center';
        
        const maxBodyY = footerBaselineY - footerReserveHeight; // Where body text must end
        const bodyLines = getWrappedLines(ctx, bodyText, availableTextW, `${bodyFontSize}px ${effectiveFont}`);
        
        for (const line of bodyLines) {
            if (currentY + bodyLineHeight < maxBodyY) {
                ctx.fillText(line, textCenterX, currentY + bodyFontSize * 0.8);
                currentY += bodyLineHeight;
            } else {
                // Optional: Add "..." or just stop printing
                if (currentY < maxBodyY) { // Try to fit one last truncated line
                    let partialLine = line;
                    while (ctx.measureText(partialLine + "...").width > availableTextW && partialLine.length > 0) {
                        partialLine = partialLine.slice(0, -1);
                    }
                    ctx.fillText(partialLine + "...", textCenterX, currentY + bodyFontSize * 0.8);
                }
                break;
            }
        }
    }

    // Footer Text (drawn from bottom up for simple multi-line)
    if (footerText) {
        ctx.font = `italic ${footerFontSize}px ${effectiveFont}`;
        const footerLines = getWrappedLines(ctx, footerText, availableTextW, `italic ${footerFontSize}px ${effectiveFont}`).reverse();
        let footerCurrentY = footerBaselineY;
        for (const line of footerLines) {
            ctx.fillText(line, textCenterX, footerCurrentY);
            footerCurrentY -= footerFontSize * 1.2; // Move upwards for next line
        }
    }

    // 5. Vignette
    if (vignetteOpacity > 0) {
        ctx.save();
        const vignetteOuterRadius = Math.max(CANVAS_W, CANVAS_H) * 0.7;
        const vignetteInnerRadius = Math.min(CANVAS_W, CANVAS_H) * 0.3;
        const gradient = ctx.createRadialGradient(
            CANVAS_W / 2, CANVAS_H / 2, vignetteInnerRadius,
            CANVAS_W / 2, CANVAS_H / 2, vignetteOuterRadius
        );
        gradient.addColorStop(0, 'rgba(0,0,0,0)');
        gradient.addColorStop(1, `rgba(0,0,0,${vignetteOpacity})`);
        ctx.fillStyle = gradient;
        ctx.globalCompositeOperation = 'source-over'; // Ensure it draws normally on top
        ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
        ctx.restore();
    }
    
    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 Vintage Apothecary Label Creator is an online tool designed to generate vintage-style labels for apothecary products. Users can upload an image and customize various text elements, including a title, subtitle, body text, and footer, with a selection of font options. The tool allows you to adjust visual elements such as background colors, text colors, and border styles. Ideal for creating personalized labels for homemade remedies, craft products, or themed events, this tool offers a creative way to present your items with an antique aesthetic.

Leave a Reply

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