You can edit the below JavaScript code to customize the image tool.
Apply Changes
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;
}
Apply Changes