You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
reportTitle = "FIELD REPORT: UNIDENTIFIED SUBJECT",
sightingDate = new Date().toISOString().split('T')[0],
sightingTime = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }),
sightingLocation = "SECTOR 7 // COORDINATES REDACTED",
caseFileNumber = `CFN-${Math.random().toString(36).substring(2, 8).toUpperCase()}`,
agentName = "AGENT [REDACTED]",
reportNotes = "Subject observed exhibiting anomalous characteristics. Photographic evidence attached. Conditions: Low light, significant distance. Quality of media is suboptimal but warrants further analysis.",
classificationStamp = "CONFIDENTIAL // EYES ONLY",
imageEffect = "grainy" // "none", "grainy", "blurry"
) {
// --- Helper: Load Web Font ---
async function loadWebFont(fontFamily, fontUrl) {
try {
// Check if font is already loaded or available using the CSS Font Loading API
if (document.fonts.check(`12px "${fontFamily}"`)) {
return true;
}
const fontFace = new FontFace(fontFamily, `url(${fontUrl}) format('woff2')`);
await fontFace.load();
document.fonts.add(fontFace);
return true;
} catch (e) {
console.error(`Font ${fontFamily} failed to load from ${fontUrl}:`, e);
return false;
}
}
// --- Helper: Wrap Text for Height Calculation ---
function getWrappedTextHeight(context, text, maxWidth, lineHeight, font) {
context.font = font;
const words = text.split(' ');
if (text.trim() === "") return 0;
let line = '';
let linesCount = 1;
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = context.measureText(testLine);
if (metrics.width > maxWidth && n > 0) {
linesCount++;
line = words[n] + ' ';
} else {
line = testLine;
}
}
return linesCount * lineHeight;
}
// --- Helper: Wrap Text and Draw ---
function wrapTextActualDraw(context, text, x, y, maxWidth, lineHeight, font, color) {
context.font = font;
context.fillStyle = color;
context.textBaseline = 'top';
const words = text.split(' ');
if (text.trim() === "") return y;
let line = '';
let currentY = y;
for (let n = 0; n < words.length; n++) {
const word = words[n];
const testLineTry = line + word + ' ';
const metrics = context.measureText(testLineTry);
if (metrics.width > maxWidth && line.length > 0) { // line.length > 0 to ensure not breaking an empty line
context.fillText(line.trim(), x, currentY);
line = word + ' ';
currentY += lineHeight;
} else {
line = testLineTry;
}
}
context.fillText(line.trim(), x, currentY); // Draw the last line
return currentY + lineHeight;
}
// --- Constants and Configuration ---
const CANVAS_WIDTH = 800;
const PADDING = 40;
const CONTENT_WIDTH = CANVAS_WIDTH - 2 * PADDING;
const BG_COLOR = "#F0EAD6"; // Creamy beige
const TEXT_COLOR = "#3D3D3D"; // Dark desaturated gray
const TEXT_COLOR_MUTED = "#666666";
const STAMP_TEXT_COLOR = "rgba(170, 0, 0, 0.75)"; // Semi-transparent red
const STAMP_BORDER_COLOR = "rgba(100, 0, 0, 0.8)"; // Darker red for outline
const FONT_URL_SPECIAL_ELITE = 'https://fonts.gstatic.com/s/specialelite/v13/XLYgIZbkc4JPVQnNI7zQMYCSTlNPVSNONA.woff2';
const fontLoaded = await loadWebFont('Special Elite', FONT_URL_SPECIAL_ELITE);
const fontFamilyCSS = fontLoaded ? "'Special Elite', monospace" : "monospace";
const FONT_TITLE_SIZE = 26;
const FONT_TITLE = `bold ${FONT_TITLE_SIZE}px ${fontFamilyCSS}`;
const FONT_HEADER_LABEL_SIZE = 13;
const FONT_HEADER_LABEL = `bold ${FONT_HEADER_LABEL_SIZE}px ${fontFamilyCSS}`;
const FONT_HEADER_VALUE_SIZE = 13;
const FONT_HEADER_VALUE = `${FONT_HEADER_VALUE_SIZE}px ${fontFamilyCSS}`;
const FONT_NOTES_TITLE_SIZE = 14;
const FONT_NOTES_TITLE = `bold ${FONT_NOTES_TITLE_SIZE}px ${fontFamilyCSS}`;
const FONT_NOTES_CONTENT_SIZE = 12;
const FONT_NOTES_CONTENT = `${FONT_NOTES_CONTENT_SIZE}px ${fontFamilyCSS}`;
const FONT_STAMP_SIZE = 28;
const FONT_STAMP = `bold ${FONT_STAMP_SIZE}px 'Arial Black', Gadget, sans-serif`;
const TITLE_SPACING = 30;
const HEADER_BLOCK_SPACING = 15;
const DIVIDER_SPACING_AFTER = 20;
const IMAGE_SPACING_AFTER = 25;
const NOTES_SPACING_AFTER = 20; // Space after notes content, before stamp consideration
const STAMP_AREA_RESERVE_HEIGHT = FONT_STAMP_SIZE * 1.5;
const LINE_HEIGHT_HEADER = FONT_HEADER_LABEL_SIZE * 1.5;
const LINE_HEIGHT_NOTES_TITLE_ACTUAL = FONT_NOTES_TITLE_SIZE * 1.2; // Actual drawing height for title
const LINE_HEIGHT_NOTES_CONTENT = FONT_NOTES_CONTENT_SIZE * 1.4;
// --- 1. Calculate Content Heights and Total Canvas Height ---
let layoutY = PADDING;
layoutY += FONT_TITLE_SIZE;
layoutY += TITLE_SPACING;
layoutY += LINE_HEIGHT_HEADER * 4; // 4 lines in header block
layoutY += HEADER_BLOCK_SPACING;
layoutY += 1; // Divider line height
layoutY += DIVIDER_SPACING_AFTER;
const MAX_IMG_DISPLAY_HEIGHT = 350;
const MAX_IMG_DISPLAY_WIDTH = CONTENT_WIDTH;
let imgDisplayWidth = originalImg.naturalWidth || originalImg.width;
let imgDisplayHeight = originalImg.naturalHeight || originalImg.height;
const aspectRatio = imgDisplayWidth / imgDisplayHeight;
if (imgDisplayWidth > MAX_IMG_DISPLAY_WIDTH) {
imgDisplayWidth = MAX_IMG_DISPLAY_WIDTH;
imgDisplayHeight = imgDisplayWidth / aspectRatio;
}
if (imgDisplayHeight > MAX_IMG_DISPLAY_HEIGHT) {
imgDisplayHeight = MAX_IMG_DISPLAY_HEIGHT;
imgDisplayWidth = imgDisplayHeight * aspectRatio;
}
layoutY += imgDisplayHeight;
layoutY += IMAGE_SPACING_AFTER;
layoutY += LINE_HEIGHT_NOTES_TITLE_ACTUAL;
const tempCtxMeasure = document.createElement('canvas').getContext('2d');
const notesActualHeight = getWrappedTextHeight(tempCtxMeasure, reportNotes, CONTENT_WIDTH, LINE_HEIGHT_NOTES_CONTENT, FONT_NOTES_CONTENT);
layoutY += notesActualHeight;
layoutY += NOTES_SPACING_AFTER;
layoutY += STAMP_AREA_RESERVE_HEIGHT;
const totalCalculatedHeight = layoutY + PADDING;
// --- 2. Create Main Canvas and Draw ---
const canvas = document.createElement('canvas');
canvas.width = CANVAS_WIDTH;
canvas.height = totalCalculatedHeight;
const ctx = canvas.getContext('2d');
ctx.fillStyle = BG_COLOR;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.textBaseline = 'top';
let currentY = PADDING; // Reset Y for drawing
ctx.font = FONT_TITLE;
ctx.fillStyle = TEXT_COLOR;
ctx.textAlign = 'center';
ctx.fillText(reportTitle, CANVAS_WIDTH / 2, currentY);
currentY += FONT_TITLE_SIZE + TITLE_SPACING;
ctx.textAlign = 'left';
const headerValueXOffset = PADDING + 95;
ctx.font = FONT_HEADER_LABEL;
ctx.fillStyle = TEXT_COLOR;
ctx.fillText("CASE FILE:", PADDING, currentY);
ctx.font = FONT_HEADER_VALUE;
ctx.fillText(caseFileNumber, headerValueXOffset, currentY);
currentY += LINE_HEIGHT_HEADER;
ctx.font = FONT_HEADER_LABEL;
ctx.fillText("DATE:", PADDING, currentY);
ctx.font = FONT_HEADER_VALUE;
ctx.fillText(sightingDate, headerValueXOffset, currentY);
const timeLabelText = "TIME:";
ctx.font = FONT_HEADER_LABEL;
const timeLabelWidth = ctx.measureText(timeLabelText).width;
ctx.font = FONT_HEADER_VALUE; // For measuring value
const timeValueWidth = ctx.measureText(sightingTime).width;
const timeValueX = CANVAS_WIDTH - PADDING - timeValueWidth;
const timeLabelX = timeValueX - timeLabelWidth - 5;
ctx.font = FONT_HEADER_LABEL;
ctx.fillText(timeLabelText, timeLabelX, currentY);
ctx.font = FONT_HEADER_VALUE;
ctx.fillText(sightingTime, timeValueX, currentY);
currentY += LINE_HEIGHT_HEADER;
ctx.font = FONT_HEADER_LABEL;
ctx.fillText("LOCATION:", PADDING, currentY);
ctx.font = FONT_HEADER_VALUE;
ctx.fillText(sightingLocation, headerValueXOffset, currentY, CONTENT_WIDTH - (headerValueXOffset - PADDING));
currentY += LINE_HEIGHT_HEADER;
ctx.font = FONT_HEADER_LABEL;
ctx.fillText("AGENT:", PADDING, currentY);
ctx.font = FONT_HEADER_VALUE;
ctx.fillText(agentName, headerValueXOffset, currentY);
currentY += LINE_HEIGHT_HEADER + HEADER_BLOCK_SPACING;
ctx.beginPath();
ctx.moveTo(PADDING, currentY);
ctx.lineTo(CANVAS_WIDTH - PADDING, currentY);
ctx.strokeStyle = TEXT_COLOR_MUTED;
ctx.lineWidth = 0.75;
ctx.stroke();
currentY += 1 + DIVIDER_SPACING_AFTER;
const processedImageCanvas = document.createElement('canvas');
processedImageCanvas.width = originalImg.naturalWidth || originalImg.width;
processedImageCanvas.height = originalImg.naturalHeight || originalImg.height;
const pCtx = processedImageCanvas.getContext('2d');
pCtx.drawImage(originalImg, 0, 0, processedImageCanvas.width, processedImageCanvas.height);
if (imageEffect === "grainy") {
const imageData = pCtx.getImageData(0, 0, processedImageCanvas.width, processedImageCanvas.height);
const data = imageData.data;
const grainAmount = 30;
for (let i = 0; i < data.length; i += 4) {
if (data[i+3] === 0) continue;
const noise = (Math.random() - 0.5) * grainAmount;
data[i] = Math.max(0, Math.min(255, data[i] + noise));
data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + noise));
data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + noise));
}
pCtx.putImageData(imageData, 0, 0);
} else if (imageEffect === "blurry") {
pCtx.filter = 'blur(1.2px)';
pCtx.drawImage(processedImageCanvas, 0, 0);
pCtx.filter = 'none';
}
const imgX = (CANVAS_WIDTH - imgDisplayWidth) / 2;
ctx.drawImage(processedImageCanvas, imgX, currentY, imgDisplayWidth, imgDisplayHeight);
ctx.strokeStyle = "#999999";
ctx.lineWidth = 1;
ctx.strokeRect(imgX -1, currentY -1, imgDisplayWidth + 2, imgDisplayHeight + 2);
currentY += imgDisplayHeight + IMAGE_SPACING_AFTER;
ctx.textAlign = 'left';
ctx.font = FONT_NOTES_TITLE;
ctx.fillStyle = TEXT_COLOR;
ctx.fillText("OBSERVATION NOTES:", PADDING, currentY);
currentY += LINE_HEIGHT_NOTES_TITLE_ACTUAL;
currentY = wrapTextActualDraw(ctx, reportNotes, PADDING, currentY, CONTENT_WIDTH, LINE_HEIGHT_NOTES_CONTENT, FONT_NOTES_CONTENT, TEXT_COLOR);
currentY += NOTES_SPACING_AFTER;
const stampAngle = -Math.PI / 12; // Approx 15 degrees
ctx.font = FONT_STAMP; // Set font for measuring stamp text
const stampTextMetrics = ctx.measureText(classificationStamp);
const stampTextWidth = stampTextMetrics.width;
// Position stamp near bottom right
const stampBoxWidth = stampTextWidth * Math.cos(stampAngle) + FONT_STAMP_SIZE * Math.sin(Math.abs(stampAngle)); // Approximated bounding box width
const stampX = CANVAS_WIDTH - PADDING - (stampBoxWidth / 2) - 10; // Adjust positioning
const stampY = totalCalculatedHeight - PADDING - (FONT_STAMP_SIZE / 2) - 10;
ctx.save();
ctx.textAlign = 'center'; // Center the text for rotation
ctx.translate(stampX, stampY);
ctx.rotate(stampAngle);
ctx.fillStyle = STAMP_TEXT_COLOR;
ctx.fillText(classificationStamp, 0, 0);
ctx.strokeStyle = STAMP_BORDER_COLOR;
ctx.lineWidth = 0.7;
ctx.strokeText(classificationStamp, 0, 0);
ctx.restore();
return canvas;
}
Apply Changes