A powerful web tool to watermark, rename, resize, and convert images in bulk with drag-and-drop. Features include watermark customization, multiple resize modes, preview, progress bar, and ZIP download with a confirmation modal.
Watermarking images protects your intellectual property, promotes your brand, and adds a professional touch to your visuals. This tool streamlines bulk processing for efficiency.
Drag and drop your images here or click to select files.
Built with HTML5 Canvas, JavaScript, and JSZip, this tool processes images client-side. It includes advanced resize algorithms, file size optimization, and a user-friendly download confirmation modal.
// Helper functions
function hexToRgb(hex) {
const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 255, g: 255, b: 255 };
}
function getWatermarkPosition(position, width, height, padding) {
let x, y, align, baseline;
switch (position) {
case 'top-left': align = 'left'; baseline = 'top'; x = padding; y = padding; break;
case 'top-center': align = 'center'; baseline = 'top'; x = width / 2; y = padding; break;
case 'top-right': align = 'right'; baseline = 'top'; x = width - padding; y = padding; break;
case 'middle-left': align = 'left'; baseline = 'middle'; x = padding; y = height / 2; break;
case 'middle-center': align = 'center'; baseline = 'middle'; x = width / 2; y = height / 2; break;
case 'middle-right': align = 'right'; baseline = 'middle'; x = width - padding; y = height / 2; break;
case 'bottom-left': align = 'left'; baseline = 'bottom'; x = padding; y = height - padding; break;
case 'bottom-center': align = 'center'; baseline = 'bottom'; x = width / 2; y = height - padding; break;
case 'bottom-right': align = 'right'; baseline = 'bottom'; x = width - padding; y = height - padding; break;
}
return { x, y, align, baseline };
}
async function adjustToFileSize(img, targetSizeKB, format, watermarkText, watermarkParams, maxIterations = 10) {
let quality = 0.9;
let step = 0.1;
let iterations = 0;
let blob;
const { position, opacity, rgb, fontSize } = watermarkParams;
while (iterations < maxIterations) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
if (watermarkText) {
ctx.font = \`${fontSize}px Arial\`;
ctx.fillStyle = \`rgba(\${rgb.r}, \${rgb.g}, \${rgb.b}, \${opacity})\`;
const pos = getWatermarkPosition(position, canvas.width, canvas.height, 20);
ctx.textAlign = pos.align;
ctx.textBaseline = pos.baseline;
ctx.fillText(watermarkText, pos.x, pos.y);
}
blob = await new Promise(resolve => canvas.toBlob(resolve, format, quality));
if (!blob) throw new Error('Failed to create blob');
const sizeKB = blob.size / 1024;
if (Math.abs(sizeKB - targetSizeKB) < 10 || iterations === maxIterations - 1) break;
if (sizeKB > targetSizeKB) quality -= step;
else quality += step;
step /= 2;
iterations++;
}
return blob;
}
// Core logic in updatePreview and processImages (see script below)