This commit is contained in:
vladislav 2026-09-03 23:01:53 -04:00
parent 8e648752ee
commit b263e27491
Signed by: Vladislav
GPG Key ID: D97C9E5E4860EE45

View File

@ -6,6 +6,7 @@ async function imageWithTextBlob(imageUrl, text, options = {}) {
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
const imageBlob = await response.blob(); const imageBlob = await response.blob();
const imageBitmap = await createImageBitmap(imageBlob); const imageBitmap = await createImageBitmap(imageBlob);
// OffscreenCanvas (works in modern browsers; fallback to normal canvas if needed) // OffscreenCanvas (works in modern browsers; fallback to normal canvas if needed)
const canvas = typeof OffscreenCanvas !== 'undefined' const canvas = typeof OffscreenCanvas !== 'undefined'
? new OffscreenCanvas( ? new OffscreenCanvas(
@ -13,16 +14,21 @@ async function imageWithTextBlob(imageUrl, text, options = {}) {
options.height ?? imageBitmap.height options.height ?? imageBitmap.height
) )
: document.createElement('canvas'); : document.createElement('canvas');
const width = canvas.width ?? (options.width ?? imageBitmap.width); const width = canvas.width ?? (options.width ?? imageBitmap.width);
const height = canvas.height ?? (options.height ?? imageBitmap.height); const height = canvas.height ?? (options.height ?? imageBitmap.height);
if (!(canvas instanceof OffscreenCanvas)) { if (!(canvas instanceof OffscreenCanvas)) {
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
} }
const ctx = canvas.getContext('2d', { alpha: true }); const ctx = canvas.getContext('2d', { alpha: true });
// Draw image // Draw image
ctx.drawImage(imageBitmap, 0, 0, width, height); ctx.drawImage(imageBitmap, 0, 0, width, height);
// No text: return original bitmap as blob (rendered)
// No text: return original bitmap as blob
if (!text?.trim()) { if (!text?.trim()) {
if (canvas.convertToBlob) { if (canvas.convertToBlob) {
return canvas.convertToBlob({ return canvas.convertToBlob({
@ -34,17 +40,20 @@ async function imageWithTextBlob(imageUrl, text, options = {}) {
canvas.toBlob((b) => resolve(b), options.type ?? 'image/png', options.quality ?? 0.92) canvas.toBlob((b) => resolve(b), options.type ?? 'image/png', options.quality ?? 0.92)
); );
} }
// === Text rendering === // === Text rendering ===
const basePadding = 6; const basePadding = options.basePadding ?? 8; // internal left/right/top padding
const fontSize = options.fontSize ?? Math.round(width * 0.05); const fontSize = options.fontSize ?? Math.round(width * 0.05);
ctx.font = `${fontSize}px ${options.fontFamily ?? 'sans-serif'}`; ctx.font = `${fontSize}px ${options.fontFamily ?? 'sans-serif'}`;
ctx.textBaseline = 'bottom'; ctx.textBaseline = 'bottom';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
const textColor = options.textColor ?? '#5F1798'; const textColor = options.textColor ?? '#5F1798';
const bgColor = options.bgColor ?? '#fff'; const bgColor = options.bgColor ?? '#fff';
const strokeColor = options.strokeColor ?? 'rgba(0,0,0,0.35)'; const strokeColor = options.strokeColor ?? 'rgba(0,0,0,0.35)';
const shadowBlur = options.shadowBlur ?? 6; const shadowBlur = options.shadowBlur ?? 6;
const maxWidth = width - basePadding * 2 - (options.extraPadding ?? 0); const maxWidth = width - basePadding * 2 - (options.extraPadding ?? 0);
// Wrap text into lines // Wrap text into lines
const words = text.split(' '); const words = text.split(' ');
let line = ''; let line = '';
@ -59,14 +68,23 @@ async function imageWithTextBlob(imageUrl, text, options = {}) {
} }
} }
if (line) lines.push(line); if (line) lines.push(line);
const lineHeight = fontSize + (options.lineGap ?? 6); const lineHeight = fontSize + (options.lineGap ?? 6);
const boxHeight = lines.length * lineHeight + basePadding * 2;
// Extra internal bottom padding inside the rounded box
const internalBottomPadding = options.internalBottomPadding ?? Math.round(fontSize * 0.35);
const boxHeight = lines.length * lineHeight + basePadding + internalBottomPadding;
const maxLineWidth = Math.max(...lines.map((l) => ctx.measureText(l).width)); const maxLineWidth = Math.max(...lines.map((l) => ctx.measureText(l).width));
const boxWidth = maxLineWidth + basePadding * 2; const boxWidth = maxLineWidth + basePadding * 2;
const bottomPadding = options.bottomPadding ?? Math.round(fontSize * 0.6);
// Distance from the very bottom of the image to the bottom of the text box
const bottomPadding = options.bottomPadding ?? Math.round(fontSize * 0.9);
const boxX = (width - boxWidth) / 2; const boxX = (width - boxWidth) / 2;
const boxY = height - bottomPadding - boxHeight; const boxY = height - bottomPadding - boxHeight;
const radius = options.radius ?? 8; const radius = options.radius ?? 8;
// Background rounded rect // Background rounded rect
ctx.save(); ctx.save();
ctx.fillStyle = bgColor; ctx.fillStyle = bgColor;
@ -74,9 +92,11 @@ async function imageWithTextBlob(imageUrl, text, options = {}) {
ctx.shadowBlur = shadowBlur; ctx.shadowBlur = shadowBlur;
roundRect(ctx, boxX, boxY, boxWidth, boxHeight, radius, true, false); roundRect(ctx, boxX, boxY, boxWidth, boxHeight, radius, true, false);
ctx.restore(); ctx.restore();
// Text // Text
ctx.fillStyle = textColor; ctx.fillStyle = textColor;
ctx.shadowColor = 'transparent'; ctx.shadowColor = 'transparent';
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const lineY = boxY + basePadding + (i + 1) * lineHeight - (lineHeight - fontSize) / 2; const lineY = boxY + basePadding + (i + 1) * lineHeight - (lineHeight - fontSize) / 2;
const lineX = width / 2; const lineX = width / 2;
@ -88,6 +108,7 @@ async function imageWithTextBlob(imageUrl, text, options = {}) {
} }
ctx.fillText(lines[i], lineX, lineY); ctx.fillText(lines[i], lineX, lineY);
} }
// Convert to blob // Convert to blob
if (canvas.convertToBlob) { if (canvas.convertToBlob) {
return await canvas.convertToBlob({ return await canvas.convertToBlob({