You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
specimenId = "SPECIMEN ID",
genus = "Genus",
species = "species",
locality = "Locality",
formation = "Formation/Horizon",
collector = "Collector",
dateCollected = "Date Collected",
institution = "MUSEUM OF PALEONTOLOGY",
fontFamily = "Times New Roman, serif",
baseFontSize = 12, // px
textColor = "black",
backgroundColor = "white",
borderColor = "black",
borderWidth = 2, // px
labelWidth = 600, // px
imagePortion = 0.4 // Ratio of (content width after padding) for image block
) {
// Helper function for text wrapping. Returns array of lines.
function _addTextLinesToFit(ctx, text, maxWidth, fontFamilyName, fSize, fStyle, fItalic) {
ctx.font = `${fStyle} ${fItalic ? "italic " : ""}${fSize}px ${fontFamilyName}`;
const words = String(text).split(' '); // Ensure text is a string
const lines = [];
if (String(text).trim().length === 0) return [""]; // Empty text, one empty line.
let currentLine = words[0] || "";
for (let i = 1; i < words.length; i++) {
const word = words[i];
const testLine = currentLine + " " + word;
if (ctx.measureText(testLine).width <= maxWidth && currentLine) { // Also check currentLine is not empty
currentLine = testLine;
} else {
lines.push(currentLine);
currentLine = word;
}
}
lines.push(currentLine); // Add the last line
return lines;
}
// --- 1. Initial canvas and context for measurements ---
const tempCanvas = document.createElement('canvas');
const ctx = tempCanvas.getContext('2d');
// --- 2. Define parameters, fonts, layout constants ---
const padding = baseFontSize; // General padding unit
const institutionFontSize = baseFontSize * 1.5;
const mainTextFontSize = baseFontSize;
const lineHeight = baseFontSize * 1.4; // Line height for main text
const institutionLineHeight = institutionFontSize * 1.2; // Line height for institution text
// --- 3. Calculate Institution Text Height & Lines ---
let calculatedCurrentY = padding;
ctx.font = `bold ${institutionFontSize}px ${fontFamily}`; // Font for measurement
const institutionLines = _addTextLinesToFit(ctx, institution.toUpperCase(), labelWidth - 2 * padding, fontFamily, institutionFontSize, "bold", false);
institutionLines.forEach(() => calculatedCurrentY += institutionLineHeight);
calculatedCurrentY += padding; // Padding after institution block
const imageAndTextStartY = calculatedCurrentY;
// --- 4. Calculate Image Dimensions (scaled to fit its allocated portion) ---
// Image block width calculation
const totalContentWidth = labelWidth - 2 * padding; // Width available between outer paddings
const imageBlockMaxWidth = (totalContentWidth - padding) * imagePortion; // Max width for image, with padding between image and text
let displayImgWidth = imageBlockMaxWidth;
let displayImgHeight = 0;
if (originalImg && originalImg.naturalWidth > 0 && originalImg.naturalHeight > 0) {
const imgAspectRatio = originalImg.naturalWidth / originalImg.naturalHeight;
displayImgHeight = displayImgWidth / imgAspectRatio;
// Optional: cap max image height (e.g., to prevent extremely tall images from dominating)
const maxPracticalImgHeight = labelWidth * 0.6; // مثلاً %60 از عرض لیبل
if (displayImgHeight > maxPracticalImgHeight) {
displayImgHeight = maxPracticalImgHeight;
displayImgWidth = displayImgHeight * imgAspectRatio;
}
// Ensure it still fits width-wise if height capping changed width beyond max
if (displayImgWidth > imageBlockMaxWidth) {
displayImgWidth = imageBlockMaxWidth;
displayImgHeight = displayImgWidth / imgAspectRatio;
}
} else { // Handle cases where image is not valid or not loaded
displayImgWidth = imageBlockMaxWidth; // Reserve space
displayImgHeight = imageBlockMaxWidth; // Default to a square-ish placeholder area
}
// --- 5. Calculate Text Block final X position and Width ---
const textBlockX = padding + displayImgWidth + padding; // X-coord for text block start
const textBlockWidth = labelWidth - textBlockX - padding; // Actual width for text block content
// --- 6. Calculate Text Stack Height & Prepare Drawable Text Items ---
let textStackHeight = 0;
const processedTextItems = []; // Stores objects describing each text line/block for drawing
const itemsToDrawConfig = [
{ type: "field", label: "Genus: ", value: genus, isValueItalic: true },
{ type: "field", label: "Species: ", value: species, isValueItalic: true },
{ type: "field", label: "Specimen ID: ", value: specimenId, isValueItalic: false },
{ type: "separator", height: lineHeight * 0.5 },
{ type: "full_wrap_field", label: "Locality: ", value: locality, isItalic: false },
{ type: "full_wrap_field", label: "Formation: ", value: formation, isItalic: false },
{ type: "field", label: "Collector: ", value: collector, isValueItalic: false },
{ type: "field", label: "Date Collected: ", value: dateCollected, isValueItalic: false },
];
for (const item of itemsToDrawConfig) {
if (item.type === "separator") {
processedTextItems.push(item);
textStackHeight += item.height;
} else if (item.type === "full_wrap_field") {
const fullText = item.label + item.value;
const wrappedLines = _addTextLinesToFit(ctx, fullText, textBlockWidth, fontFamily, mainTextFontSize, "normal", item.isItalic);
wrappedLines.forEach(line => {
processedTextItems.push({ type: "text_line", text: line, fontStyle: "normal", isItalic: item.isItalic });
textStackHeight += lineHeight;
});
} else if (item.type === "field") { // mixed_style_line logic
ctx.font = `normal ${mainTextFontSize}px ${fontFamily}`; // Font for label part measurement
const labelMetrics = ctx.measureText(item.label);
const valueAvailableWidth = textBlockWidth - labelMetrics.width;
const valueLines = _addTextLinesToFit(ctx, item.value, valueAvailableWidth, fontFamily, mainTextFontSize, "normal", item.isValueItalic);
processedTextItems.push({
type: "mixed_style_line",
label: item.label,
valueLines: valueLines,
isValueItalic: item.isValueItalic
});
textStackHeight += lineHeight * Math.max(1, valueLines.length); // Each wrapped value line takes height
}
}
// --- 7. Determine Main Content Height and Final Canvas Height ---
const mainContentBlockHeight = Math.max(displayImgHeight, textStackHeight);
const canvasHeight = imageAndTextStartY + mainContentBlockHeight + padding;
// --- 8. Create final canvas and get its context ---
const canvas = document.createElement('canvas');
canvas.width = labelWidth;
canvas.height = canvasHeight;
const drawCtx = canvas.getContext('2d');
// --- 9. Fill background and draw border ---
drawCtx.fillStyle = backgroundColor;
drawCtx.fillRect(0, 0, canvas.width, canvas.height);
if (borderWidth > 0) {
drawCtx.strokeStyle = borderColor;
drawCtx.lineWidth = borderWidth;
drawCtx.strokeRect(borderWidth / 2, borderWidth / 2, canvas.width - borderWidth, canvas.height - borderWidth);
}
// --- 10. Draw elements ---
drawCtx.fillStyle = textColor;
drawCtx.textBaseline = "top";
// 10.1. Draw Institution Text
let currentDrawY = padding;
drawCtx.font = `bold ${institutionFontSize}px ${fontFamily}`;
drawCtx.textAlign = "center";
institutionLines.forEach(line => {
drawCtx.fillText(line, labelWidth / 2, currentDrawY);
currentDrawY += institutionLineHeight;
});
currentDrawY += padding; // This should now be == imageAndTextStartY
// 10.2. Draw Image (Vertically centered in its part of mainContentBlockHeight)
let imgDrawY = imageAndTextStartY;
if (mainContentBlockHeight > displayImgHeight) {
imgDrawY += (mainContentBlockHeight - displayImgHeight) / 2;
}
if (originalImg && originalImg.naturalWidth > 0) { // Only draw if valid image
drawCtx.drawImage(originalImg, padding, imgDrawY, displayImgWidth, displayImgHeight);
} else { // Optional: draw a placeholder box for image if not available
drawCtx.strokeStyle = '#ccc';
drawCtx.lineWidth = 1;
drawCtx.strokeRect(padding, imgDrawY, displayImgWidth, displayImgHeight);
drawCtx.textAlign = "center";
drawCtx.font = `${mainTextFontSize}px ${fontFamily}`;
drawCtx.fillText("Image", padding + displayImgWidth/2, imgDrawY + displayImgHeight/2 - mainTextFontSize/2);
}
// 10.3. Draw Text Block (Vertically centered in its part of mainContentBlockHeight)
let textDrawCurrentY = imageAndTextStartY;
if (mainContentBlockHeight > textStackHeight) {
textDrawCurrentY += (mainContentBlockHeight - textStackHeight) / 2;
}
drawCtx.textAlign = "left";
for (const pItem of processedTextItems) {
if (pItem.type === "separator") {
drawCtx.beginPath();
drawCtx.moveTo(textBlockX, textDrawCurrentY + pItem.height / 2);
drawCtx.lineTo(textBlockX + textBlockWidth, textDrawCurrentY + pItem.height / 2);
drawCtx.strokeStyle = textColor;
drawCtx.lineWidth = 1;
drawCtx.stroke();
textDrawCurrentY += pItem.height;
} else if (pItem.type === "text_line") {
drawCtx.font = `${pItem.fontStyle} ${pItem.isItalic ? "italic " : ""}${mainTextFontSize}px ${fontFamily}`;
drawCtx.fillText(pItem.text, textBlockX, textDrawCurrentY);
textDrawCurrentY += lineHeight;
} else if (pItem.type === "mixed_style_line") {
drawCtx.font = `normal ${mainTextFontSize}px ${fontFamily}`; // Label part is normal
const labelPartMetrics = drawCtx.measureText(pItem.label); // Measure for positioning value
drawCtx.fillText(pItem.label, textBlockX, textDrawCurrentY);
drawCtx.font = `normal ${pItem.isValueItalic ? "italic " : ""}${mainTextFontSize}px ${fontFamily}`; // Value part font
let valueLineY = textDrawCurrentY;
pItem.valueLines.forEach((vLine, index) => {
if (index === 0) {
drawCtx.fillText(vLine, textBlockX + labelPartMetrics.width, valueLineY);
} else {
// Subsequent wrapped lines of value align with start of value text
drawCtx.fillText(vLine, textBlockX + labelPartMetrics.width, valueLineY);
}
if (index < pItem.valueLines.length - 1) { // If not the last value line
valueLineY += lineHeight;
}
});
textDrawCurrentY += lineHeight * Math.max(1, pItem.valueLines.length);
}
}
return canvas;
}
Apply Changes