ai-lab-comic / src /lib /writeIntoBubble.ts
jbilcke-hf's picture
jbilcke-hf HF staff
adding some counter measures to reduce the pressure on the server
cb3fdda
raw
history blame
3.92 kB
/*
I have a PNG image which contains a colored shape (roughly in the shape of a speech bubble), surrounded by white
Please write a TypeScript function (it should work in the browser) to:
1. replace all the white pixels with a transparent PNG pixel
2. replace all the colored pixels with a white pixel
3. write some input text into the colored shape
4. Make sure line returns are handled
5. It should have some padding (eg. 20px)
6. use Comic Sans MS
You can use the canvas for your operation. The signature should be something like:
- Please adjust the font size, based on the available number of pixels inside the bubble, taking some margin into account.
- The text should not be below 8px
- If there is not enough room to display it without going outside the shape, then crop the text.
- in other words, NEVER write outside the shape!
The function should be something like:
writeIntoBubble(image: string, text: string): Promise<string>
*/
export async function writeIntoBubble(image: string, text: string): Promise<string> {
const padding = 20; // Pixels
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const physicalWidth = img.width;
const physicalHeight = img.height;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
reject('Unable to get canvas context');
return;
}
canvas.width = physicalWidth;
canvas.height = physicalHeight;
ctx.drawImage(img, 0, 0, physicalWidth, physicalHeight);
const imageData = ctx.getImageData(0, 0, physicalWidth, physicalHeight);
const data = imageData.data;
let minX = physicalWidth, minY = physicalHeight, maxX = 0, maxY = 0;
for (let y = 0; y < physicalHeight; y++) {
for (let x = 0; x < physicalWidth; x++) {
const i = (y * physicalWidth + x) * 4;
if (data[i] !== 255 || data[i + 1] !== 255 || data[i + 2] !== 255) {
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
data[i] = data[i + 1] = data[i + 2] = 255;
data[i + 3] = 255;
} else {
data[i + 3] = 0;
}
}
}
ctx.putImageData(imageData, 0, 0);
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transforms to handle padding correctly
const textX = minX + padding;
const textY = minY + padding;
const textWidth = (maxX - minX) - 2 * padding;
const textHeight = (maxY - minY) - 2 * padding;
ctx.restore();
ctx.rect(textX, textY, textWidth, textHeight);
ctx.clip(); // Clip outside of the region
let fontSize = 20; // Start with a large size
let lines = [];
do {
ctx.font = `${fontSize}px Comic Sans MS`;
lines = wrapText(ctx, text, textWidth);
fontSize -= 2; // Reduce size and try again if text doesn't fit
} while(lines.length > textHeight / fontSize && fontSize > 8);
ctx.font = `${fontSize}px Comic Sans MS`;
lines.forEach((line, i) => ctx.fillText(line, textX, textY + padding + i * fontSize));
resolve(canvas.toDataURL());
};
img.onerror = reject;
img.src = image;
});
}
// Function to wrap text into lines that fit inside a specified width
function wrapText(context: CanvasRenderingContext2D, text: string, maxWidth: number): string[] {
const words = text.split(' ');
const lines = [];
let line = '';
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = context.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
lines.push(line);
line = words[n] + ' ';
} else {
line = testLine;
}
}
lines.push(line);
return lines;
}