Please bookmark this page to avoid losing your image tool!

Image Paranormal Investigation File 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,
    caseNumber = "CASE-X0815",
    dateTime = "",
    location = "SITE-07, CONTAINMENT WING B",
    subject = "Anomalous Entity #3A (Apparition)",
    classification = "CLASSIFIED",
    notes = "Subject exhibits unusual electromagnetic fluctuations and EVP readings. Standard containment protocols appear ineffective. Requesting Level 4 consultation. All personnel maintain minimum 50m distance. Refer to Addendum 4.2 for full spectral analysis and witness testimonies. Object secured: Yes. Hostile Intent: Undetermined.",
    imageEffect = "grayscale", // "none", "grayscale", "sepia", "lowcontrast"
    mainFontFamily = "'Courier New', Courier, monospace",
    stampFontFamily = "'Impact', 'Arial Black', sans-serif",
    titleText = "PARANORMAL INCIDENT REPORT",
    stampColor = "rgba(200, 0, 0, 0.75)",
    textColor = "#1a1a1a", // Dark gray, not pure black
    borderColor = "#4a4a4a", // Slightly lighter gray for lines
    backgroundColor = "#fdfbf0" // Creamy paper
) {

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

    // Document dimensions (similar to A4/Letter ratio)
    canvas.width = 800;
    canvas.height = 1100;

    // Fill background
    ctx.fillStyle = backgroundColor;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    const margin = 40;
    const contentWidth = canvas.width - 2 * margin;
    let currentY = margin;

    // Helper function for text wrapping
    function wrapText(context, text, x, y, maxWidth, lineHeight, availableHeight) {
        const words = String(text).split(' ');
        let line = '';
        let currentTextY = y;
        const startY = y;
        let linesDrawn = 0;

        for (let n = 0; n < words.length; n++) {
            const testLine = line + words[n] + ' ';
            const metrics = context.measureText(testLine);
            const testWidth = metrics.width;

            // Check if adding the current line would exceed availableHeight
            const potentialNextY = currentTextY + (linesDrawn === 0 && line === '' ? 0 : lineHeight); // If it's the very first line, don't add lineHeight yet
            
            if (potentialNextY > startY + availableHeight - lineHeight / 2) { // Check if drawing *this line* will overflow remaining space
                let prevLineContent = line.trim();
                if (prevLineContent) {
                    while (context.measureText(prevLineContent + "...").width > maxWidth && prevLineContent.length > 0) {
                        prevLineContent = prevLineContent.slice(0, -1);
                    }
                    if (prevLineContent.length > 0) {
                         context.fillText(prevLineContent + "...", x, currentTextY);
                    } else if (context.measureText("...").width <= maxWidth) { // if line became empty
                         context.fillText("...", x, currentTextY);
                    }
                    currentTextY += lineHeight;
                }
                return currentTextY; // Stop further processing
            }

            if (testWidth > maxWidth && n > 0) {
                context.fillText(line.trim(), x, currentTextY);
                linesDrawn++;
                line = words[n] + ' ';
                currentTextY += lineHeight;
            } else {
                line = testLine;
            }
        }
        
        // Draw the last remaining line
        if (currentTextY <= startY + availableHeight - lineHeight / 2) {
             if (line.trim()){
                context.fillText(line.trim(), x, currentTextY);
                currentTextY += lineHeight;
             }
        } else if (line.trim()) { // Last line exists but might slightly overflow, try to truncate
            let lastLine = line.trim();
             while (context.measureText(lastLine + "...").width > maxWidth && lastLine.length > 0) {
                lastLine = lastLine.slice(0, -1);
            }
            if(lastLine.length > 0) {
                context.fillText(lastLine + "...", x, currentTextY);
                currentTextY += lineHeight;
            } else if (context.measureText("...").width <= maxWidth) {
                 context.fillText("...", x, currentTextY);
                 currentTextY += lineHeight;
            }
        }
        return currentTextY;
    }


    // --- CLASSIFICATION STAMP (Top Right) ---
    ctx.save();
    const stampText = classification.toUpperCase();
    ctx.font = `bold 32px ${stampFontFamily}`;
    ctx.fillStyle = stampColor;
    const stampMetrics = ctx.measureText(stampText);
    const stampTextWidth = stampMetrics.width;
    const stampTextHeight = 32; // Approx height from font size
    const stampPadding = 10;

    // Position for top-right based on text center for rotation
    const stampCenterX = canvas.width - margin - (stampTextWidth / 2) - stampPadding;
    const stampCenterY = margin + (stampTextHeight / 2) + stampPadding;

    ctx.translate(stampCenterX, stampCenterY);
    ctx.rotate(Math.PI / 15); // Rotate ~12 degrees

    // Draw border for the stamp
    ctx.strokeStyle = stampColor;
    ctx.lineWidth = 3;
    ctx.strokeRect(
        -stampTextWidth / 2 - stampPadding,
        -stampTextHeight / 2 - stampPadding,
        stampTextWidth + stampPadding * 2,
        stampTextHeight + stampPadding * 2
    );
    // Draw stamp text
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText(stampText, 0, 0);
    ctx.restore();
    
    // --- HEADER / TITLE ---
    currentY = margin + 20; // Start title below potential stamp height influence
    
    ctx.font = `bold 26px ${mainFontFamily}`;
    ctx.fillStyle = textColor;
    ctx.textAlign = "center";
    ctx.fillText(titleText.toUpperCase(), canvas.width / 2, currentY);
    currentY += 35;
    
    // Optional: Sub-Agency Name
    ctx.font = `14px ${mainFontFamily}`;
    ctx.fillStyle = textColor;
    ctx.textAlign = "center";
    ctx.fillText("Federal Bureau of Paranormal Research & Containment", canvas.width / 2, currentY);
    currentY += 25;

    // Separator Line
    ctx.beginPath();
    ctx.moveTo(margin, currentY);
    ctx.lineTo(canvas.width - margin, currentY);
    ctx.strokeStyle = borderColor;
    ctx.lineWidth = 1.5;
    ctx.stroke();
    currentY += 20;

    // --- METADATA SECTION ---
    const metadataFontSize = 14;
    const metadataLineHeight = metadataFontSize * 1.5;
    ctx.font = `${metadataFontSize}px ${mainFontFamily}`;
    ctx.fillStyle = textColor;
    ctx.textAlign = "left";

    const effectiveDateTime = dateTime || new Date().toISOString().replace('T', ' ').substring(0, 19) + " HRS";
    
    const fieldWidth = contentWidth / 2 - 10;
    ctx.fillText(`CASE FILE NO.: ${caseNumber}`, margin, currentY);
    ctx.fillText(`DATE/TIME: ${effectiveDateTime}`, margin + contentWidth / 2, currentY);
    currentY += metadataLineHeight;

    ctx.fillText(`LOCATION: ${location}`, margin, currentY, fieldWidth);
    // Example of wrapping a potentially long location, though usually short
    // For subject, it could be longer. But for now, simple fillText.
    // If location/subject needs wrapping, call wrapText with limited width/height for that field.
    ctx.fillText(`SUBJECT ID: ${subject}`, margin + contentWidth/2, currentY, fieldWidth);
    currentY += metadataLineHeight + 10;

    // Separator Line
    ctx.beginPath();
    ctx.moveTo(margin, currentY);
    ctx.lineTo(canvas.width - margin, currentY);
    ctx.strokeStyle = borderColor;
    ctx.lineWidth = 1;
    ctx.stroke();
    currentY += 20;

    // --- IMAGE SECTION ---
    const imageSectionStartY = currentY;
    const imageMaxHeight = 350; // Max height for the image display area
    const imageMaxWidth = contentWidth;

    let imgDisplayWidth = originalImg.width;
    let imgDisplayHeight = originalImg.height;
    const scale = Math.min(imageMaxWidth / imgDisplayWidth, imageMaxHeight / imgDisplayHeight, 1); // Don't scale up

    imgDisplayWidth *= scale;
    imgDisplayHeight *= scale;

    const imgX = margin + (contentWidth - imgDisplayWidth) / 2; // Center the image
    const imgY = currentY;

    ctx.save();
    if (imageEffect === "grayscale") {
        ctx.filter = "grayscale(100%) contrast(110%) brightness(95%)";
    } else if (imageEffect === "sepia") {
        ctx.filter = "sepia(80%) contrast(110%) brightness(90%)";
    } else if (imageEffect === "lowcontrast") {
        ctx.filter = "contrast(70%) brightness(120%)";
    }
    // No 'none' explicit filter needed, default has no filter.
    
    ctx.drawImage(originalImg, imgX, imgY, imgDisplayWidth, imgDisplayHeight);
    ctx.restore(); // Restore context to remove filter for subsequent drawings

    // Border around image
    ctx.strokeStyle = borderColor;
    ctx.lineWidth = 1.5;
    ctx.strokeRect(imgX - 3, imgY - 3, imgDisplayWidth + 6, imgDisplayHeight + 6);
    
    currentY += imgDisplayHeight + 20; // Space after image

    // Separator Line
    ctx.beginPath();
    ctx.moveTo(margin, currentY);
    ctx.lineTo(canvas.width - margin, currentY);
    ctx.strokeStyle = borderColor;
    ctx.lineWidth = 1;
    ctx.stroke();
    currentY += 20;

    // --- NOTES SECTION ---
    ctx.fillStyle = textColor;
    ctx.textAlign = "left";
    ctx.font = `bold ${metadataFontSize + 2}px ${mainFontFamily}`; // Slightly larger for heading
    ctx.fillText("OBSERVATIONS & NOTES:", margin, currentY);
    currentY += metadataLineHeight * 1.2;

    ctx.font = `${metadataFontSize}px ${mainFontFamily}`; // Regular font for notes text
    const notesLineHeight = metadataFontSize * 1.4;
    const notesAvailableHeight = canvas.height - currentY - margin - 20; // Reserve space for footer

    wrapText(ctx, notes, margin, currentY, contentWidth, notesLineHeight, notesAvailableHeight);

    // --- FOOTER ---
    const footerY = canvas.height - margin / 1.5;
    ctx.textAlign = "center";
    ctx.font = `italic 11px ${mainFontFamily}`;
    ctx.fillStyle = borderColor; // Subtler color for footer
    ctx.fillText("DOCUMENT LEVEL: ACIES NOCTURNA - DISTRIBUTION RESTRICTED", canvas.width / 2, footerY);
    
    // Optional: Page number or unique ID at bottom
    // ctx.fillText(`Page 1 of 1 - UID: ${Math.random().toString(36).substring(2,10).toUpperCase()}`, canvas.width / 2, footerY + 15);


    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 Paranormal Investigation File Creator is a tool designed to transform images into formatted reports ideal for documenting paranormal incidents. Users can upload an image and provide relevant data including case numbers, dates, locations, and detailed notes about the subject being investigated. The tool allows customization of report elements such as font styles, colors, and image effects (like grayscale or sepia) to suit specific reporting needs. This tool is useful for paranormal researchers, investigators, or enthusiasts looking to create professional-looking documentation for their cases, complete with visual evidence and comprehensive details.

Leave a Reply

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