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"
+ />
+ ) : (
+
+

+
+
{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" });