| 85 | * @returns A Promise that resolves with the data URL of the watermarked image. |
| 86 | */ |
| 87 | export const embedWatermark = (imageUrl: string, text: string): Promise<string> => { |
| 88 | return new Promise((resolve, reject) => { |
| 89 | const canvas = document.createElement('canvas'); |
| 90 | const ctx = canvas.getContext('2d'); |
| 91 | if (!ctx) { |
| 92 | return reject(new Error("Could not get canvas context.")); |
| 93 | } |
| 94 | |
| 95 | const img = new Image(); |
| 96 | img.crossOrigin = "anonymous"; |
| 97 | img.onload = () => { |
| 98 | canvas.width = img.width; |
| 99 | canvas.height = img.height; |
| 100 | ctx.drawImage(img, 0, 0); |
| 101 | |
| 102 | const watermarkText = text + "::END"; // Add a delimiter |
| 103 | const binaryMessage = textToBinary(watermarkText); |
| 104 | const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
| 105 | const data = imageData.data; |
| 106 | |
| 107 | if (binaryMessage.length > data.length / 4 * 3) { |
| 108 | console.warn("Watermark is too long for the image. Skipping."); |
| 109 | return resolve(imageUrl); // Return original if too long |
| 110 | } |
| 111 | |
| 112 | let messageIndex = 0; |
| 113 | for (let i = 0; i < data.length && messageIndex < binaryMessage.length; i += 4) { |
| 114 | // Embed in R, G, B channels |
| 115 | for (let j = 0; j < 3 && messageIndex < binaryMessage.length; j++) { |
| 116 | const bit = parseInt(binaryMessage[messageIndex], 2); |
| 117 | // Clear the LSB and then set it |
| 118 | data[i + j] = (data[i + j] & 0xFE) | bit; |
| 119 | messageIndex++; |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | ctx.putImageData(imageData, 0, 0); |
| 124 | resolve(canvas.toDataURL('image/png')); |
| 125 | }; |
| 126 | img.onerror = () => reject(new Error("Failed to load image for watermarking.")); |
| 127 | img.src = imageUrl; |
| 128 | }); |
| 129 | }; |
| 130 | |
| 131 | /** |
| 132 | * Programmatically triggers a file download for a given data URL. |