From 121b65577c3f28802974ee1aa3eca16c26ef90c5 Mon Sep 17 00:00:00 2001 From: Anubhav-Mondal Date: Sat, 9 May 2026 14:10:56 +0530 Subject: [PATCH] feat(watermark): add image watermark support --- src/pages/Watermark/Watermark.jsx | 112 +++++++++++++++++++++++++----- src/services/pdf.service.js | 82 +++++++++++++++++++++- 2 files changed, 175 insertions(+), 19 deletions(-) diff --git a/src/pages/Watermark/Watermark.jsx b/src/pages/Watermark/Watermark.jsx index 7f82491..22574e5 100644 --- a/src/pages/Watermark/Watermark.jsx +++ b/src/pages/Watermark/Watermark.jsx @@ -27,6 +27,10 @@ export function Watermark() { offsetY: 0 }); + const [watermarkMode, setWatermarkMode] = useState("text"); + const [watermarkImage, setWatermarkImage] = useState(null); + const [watermarkImagePreview, setWatermarkImagePreview] = useState(null); + const { isPremium, hasReachedGlobalLimit, @@ -51,15 +55,14 @@ export function Watermark() { }, [file]); useEffect(() => { - if (!file || !watermarkText.trim()) { - const t = setTimeout(() => setPreviewUrl(null), 0); - return () => clearTimeout(t); - } - const timer = setTimeout(async () => { + if (!file) { setPreviewUrl(null); return; } + if (watermarkMode === "text" && !watermarkText.trim()) { setPreviewUrl(null); return; } + if (watermarkMode === "image" && !watermarkImage) { setPreviewUrl(null); return; } + try { setIsProcessing(true); - const blob = await addWatermark(file, watermarkText, options); + const blob = await addWatermark(file, watermarkText, options, watermarkMode === "image" ? watermarkImage : null); const url = URL.createObjectURL(blob); if (lastUrlRef.current) URL.revokeObjectURL(lastUrlRef.current); @@ -73,7 +76,7 @@ export function Watermark() { }, 500); return () => clearTimeout(timer); - }, [file, watermarkText, options]); + }, [file, watermarkText, options, watermarkMode, watermarkImage]); // 3. Handle cleanup on unmount useEffect(() => { @@ -111,6 +114,8 @@ export function Watermark() { setPreviewUrl(null); setOriginalPreviewUrl(null); setPageCount(0); + setWatermarkImage(null); + setWatermarkImagePreview(null); }; const updateOption = (key, value) => { @@ -159,13 +164,73 @@ export function Watermark() {
- - setWatermarkText(e.target.value)} - className="w-full h-11 px-4 bg-black border border-white/10 text-white rounded-lg uppercase outline-none focus:ring-1 focus:ring-white/20" - /> + +
+ {["text", "image"].map((mode) => ( + + ))} +
+ {watermarkMode === "text" && ( +
+ + setWatermarkText(e.target.value)} + className="w-full h-11 px-4 bg-black border border-white/10 text-white rounded-lg uppercase outline-none focus:ring-1 focus:ring-white/20" + /> +
+ )} + {watermarkMode === "image" && ( +
+ + {!watermarkImage ? ( + { + const f = files[0]; + if (!f) return; + setWatermarkImage(f); + setWatermarkImagePreview(URL.createObjectURL(f)); + }} + multiple={false} + text="Click or drop a PNG / JPG" + accept="image/png, image/jpeg, image/jpg" + hintText="PNG, JPG only" + /> + ) : ( +
+ Watermark preview +
+

{watermarkImage.name}

+

{(watermarkImage.size / 1024).toFixed(1)} KB

+
+ +
+ )} +
+ )}
@@ -190,10 +255,21 @@ export function Watermark() {
-
- - updateOption("fontSize", parseInt(e.target.value))} className="w-full h-1.5 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-white" /> -
+ {watermarkMode === "text" && ( +
+ + updateOption("fontSize", parseInt(e.target.value))} className="w-full h-1.5 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-white" /> +
+ )} + {watermarkMode === "image" && ( +
+ + updateOption("imageScale", parseFloat(e.target.value))} + className="w-full h-1.5 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-white" /> +
+ )}
updateOption("opacity", parseFloat(e.target.value))} className="w-full h-1.5 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-white" /> diff --git a/src/services/pdf.service.js b/src/services/pdf.service.js index d328e2b..2114f30 100644 --- a/src/services/pdf.service.js +++ b/src/services/pdf.service.js @@ -62,7 +62,31 @@ export const getPdfPageCount = async (file) => { return pdf.getPageCount(); }; -export const addWatermark = async (file, watermarkText = "CONFIDENTIAL", options = {}) => { +// Rotates an Image bytes before embedding +async function rotateImageBytes(imageFile, angleDeg) { + const bitmap = await createImageBitmap(imageFile); + const rad = (angleDeg * Math.PI) / 180; + const sin = Math.abs(Math.sin(rad)); + const cos = Math.abs(Math.cos(rad)); + + const canvasW = Math.round(bitmap.width * cos + bitmap.height * sin); + const canvasH = Math.round(bitmap.width * sin + bitmap.height * cos); + + const canvas = document.createElement("canvas"); + canvas.width = canvasW; + canvas.height = canvasH; + + const ctx = canvas.getContext("2d"); + ctx.translate(canvasW / 2, canvasH / 2); + ctx.rotate(rad); + ctx.drawImage(bitmap, -bitmap.width / 2, -bitmap.height / 2); + + const blob = await new Promise((res) => canvas.toBlob(res, "image/png")); + const bytes = await blob.arrayBuffer(); + return { bytes, width: canvasW, height: canvasH }; +} + +export const addWatermark = async (file, watermarkText = "CONFIDENTIAL", options = {}, watermarkImage = null) => { if (!file) throw new Error("Please provide a PDF file."); const { @@ -72,6 +96,7 @@ export const addWatermark = async (file, watermarkText = "CONFIDENTIAL", options rotation = 45, offsetX = 0, offsetY = 0, + imageScale = 0.4, } = options; const arrayBuffer = await file.arrayBuffer(); @@ -79,6 +104,60 @@ export const addWatermark = async (file, watermarkText = "CONFIDENTIAL", options const helveticaFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold); const pages = pdfDoc.getPages(); + if (watermarkImage) { + let embeddedImage, imgW, imgH; + + if (rotation !== 0) { + const { bytes, width, height } = await rotateImageBytes(watermarkImage, rotation); + embeddedImage = await pdfDoc.embedPng(bytes); + imgW = width; + imgH = height; + } else { + const imgBytes = await watermarkImage.arrayBuffer(); + embeddedImage = watermarkImage.type === "image/png" + ? await pdfDoc.embedPng(imgBytes) + : await pdfDoc.embedJpg(imgBytes); + const size = embeddedImage.size(); + imgW = size.width; + imgH = size.height; + } + + pages.forEach((page) => { + const { width: pageW, height: pageH } = page.getSize(); + const targetW = pageW * imageScale; + const scale = targetW / imgW; + const targetH = imgH * scale; + + const margin = 40; + let x, y; + + switch (position) { + case "top-left": + x = margin; y = pageH - margin - targetH; break; + case "top-right": + x = pageW - margin - targetW; y = pageH - margin - targetH; break; + case "bottom-left": + x = margin; y = margin; break; + case "bottom-right": + x = pageW - margin - targetW; y = margin; break; + case "center": + default: + x = (pageW - targetW) / 2; + y = (pageH - targetH) / 2; + break; + } + + page.drawImage(embeddedImage, { + x: x + Number(offsetX), + y: y + Number(offsetY), + width: targetW, + height: targetH, + opacity: parseFloat(opacity), + }) + }) + + } else { + pages.forEach((page) => { const { width, height } = page.getSize(); const textWidth = helveticaFont.widthOfTextAtSize(watermarkText, fontSize); @@ -124,6 +203,7 @@ export const addWatermark = async (file, watermarkText = "CONFIDENTIAL", options rotate: degrees(rotation), }); }); + } const pdfBytes = await pdfDoc.save(); return new Blob([pdfBytes], { type: "application/pdf" });