Files
my-blog2/js/tools/watermark.js
2026-05-13 16:50:38 +08:00

421 lines
17 KiB
JavaScript

(function() {
function initWatermarkTool() {
const container = document.getElementById('tool-watermark-container');
if (!container) return;
// Stop propagation
container.addEventListener('click', (e) => e.stopPropagation());
let wmType = 'text';
let visImg = new Image();
let wmImg = new Image(); // For image watermark
// --- Tabs ---
window.switchTab = function(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tool-section-content').forEach(s => s.style.display = 'none');
if (tab === 'visible') {
document.querySelector('.tab:nth-child(1)').classList.add('active');
document.getElementById('tab-visible').style.display = 'block';
} else if (tab === 'invisible') {
document.querySelector('.tab:nth-child(2)').classList.add('active');
document.getElementById('tab-invisible').style.display = 'block';
} else {
document.querySelector('.tab:nth-child(3)').classList.add('active');
document.getElementById('tab-remove').style.display = 'block';
}
};
window.toggleWmType = function() {
const val = document.querySelector('input[name="wm-type"]:checked').value;
wmType = val;
if (val === 'text') {
document.getElementById('ctrl-text-group').style.display = 'contents';
document.getElementById('ctrl-image-group').style.display = 'none';
} else {
document.getElementById('ctrl-text-group').style.display = 'none';
document.getElementById('ctrl-image-group').style.display = 'contents';
}
drawVisible();
};
window.downloadCanvas = function(id, name) {
const c = document.getElementById(id);
if (c.width === 0) return;
const link = document.createElement('a');
link.download = name;
link.href = c.toDataURL();
link.click();
}
// --- Visible Watermark ---
const visFile = document.getElementById('vis-file');
const wmImgFile = document.getElementById('wm-img-file');
visFile.addEventListener('change', function(e) {
if(!e.target.files.length) return;
const reader = new FileReader();
reader.onload = (event) => {
visImg.onload = drawVisible;
visImg.src = event.target.result;
};
reader.readAsDataURL(e.target.files[0]);
});
wmImgFile.addEventListener('change', function(e) {
if(!e.target.files.length) return;
const reader = new FileReader();
reader.onload = (event) => {
wmImg.onload = drawVisible;
wmImg.src = event.target.result;
};
reader.readAsDataURL(e.target.files[0]);
});
// Add listeners to controls
['vis-text', 'vis-size', 'vis-color', 'vis-opacity', 'vis-pos', 'wm-scale'].forEach(id => {
const el = document.getElementById(id);
if(el) {
el.addEventListener('input', drawVisible);
el.addEventListener('change', drawVisible);
}
});
function drawVisible() {
if (!visImg.src) return;
const cvs = document.getElementById('vis-canvas');
const ctx = cvs.getContext('2d');
cvs.width = visImg.width;
cvs.height = visImg.height;
ctx.drawImage(visImg, 0, 0);
const opacity = parseFloat(document.getElementById('vis-opacity').value);
const pos = document.getElementById('vis-pos').value;
ctx.globalAlpha = opacity;
if (wmType === 'text') {
const text = document.getElementById('vis-text').value;
const size = parseInt(document.getElementById('vis-size').value);
const color = document.getElementById('vis-color').value;
ctx.font = `bold ${size}px sans-serif`;
ctx.fillStyle = color;
drawContent(ctx, pos, (x, y) => ctx.fillText(text, x, y), cvs.width, cvs.height, 300, 150);
} else if (wmType === 'image' && wmImg.src) {
const scale = parseFloat(document.getElementById('wm-scale').value);
const w = wmImg.width * scale;
const h = wmImg.height * scale;
drawContent(ctx, pos, (x, y) => ctx.drawImage(wmImg, x - w/2, y - h/2, w, h), cvs.width, cvs.height, w * 1.5, h * 1.5);
}
}
function drawContent(ctx, pos, drawFn, w, h, spaceX, spaceY) {
if (pos === 'center') {
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
drawFn(w/2, h/2);
} else if (pos === 'bottom-right') {
ctx.textAlign = 'right'; ctx.textBaseline = 'bottom';
drawFn(w - 20, h - 20);
} else if (pos === 'top-left') {
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
drawFn(20, 20);
} else if (pos === 'repeat') {
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.rotate(-45 * Math.PI / 180);
const diag = Math.sqrt(w*w + h*h);
// Draw in rotated grid
for (let x = -diag; x < diag; x += spaceX) {
for (let y = -diag; y < diag; y += spaceY) {
drawFn(x, y);
}
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
}
// --- Invisible Watermark (LSB) ---
let invisImg = new Image();
document.getElementById('invis-file').addEventListener('change', function(e) {
if(!e.target.files.length) return;
const reader = new FileReader();
reader.onload = (event) => {
invisImg.onload = () => {
const cvs = document.getElementById('invis-canvas');
cvs.width = invisImg.width;
cvs.height = invisImg.height;
const ctx = cvs.getContext('2d');
ctx.drawImage(invisImg, 0, 0);
document.getElementById('invis-result').innerText = '';
};
invisImg.src = event.target.result;
};
reader.readAsDataURL(e.target.files[0]);
});
window.encodeLSB = function() {
const text = document.getElementById('invis-text').value;
if (!text || !invisImg.src) return alert('请先上传图片并输入文字');
const cvs = document.getElementById('invis-canvas');
const ctx = cvs.getContext('2d');
const imgData = ctx.getImageData(0, 0, cvs.width, cvs.height);
const data = imgData.data;
// Simple LSB encoding (Text -> Binary -> Modify R channel LSB)
// Implementation simplified for brevity
alert('隐写功能演示:实际写入需完整二进制编码逻辑');
};
window.decodeLSB = function() {
alert('隐写功能演示:实际读取需完整二进制解码逻辑');
};
// --- Remove Watermark (New) ---
let removeFile = document.getElementById('remove-file');
let removeImgData = null; // Store original img data for undo
let pdfFileBytes = null;
let isMaskMode = false;
removeFile.addEventListener('change', async function(e) {
if(!e.target.files.length) return;
const file = e.target.files[0];
if (file.type.startsWith('image/')) {
document.getElementById('remove-image-ui').style.display = 'block';
document.getElementById('remove-pdf-ui').style.display = 'none';
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
const cvs = document.getElementById('remove-canvas');
cvs.width = img.width;
cvs.height = img.height;
const ctx = cvs.getContext('2d');
ctx.drawImage(img, 0, 0);
removeImgData = ctx.getImageData(0, 0, cvs.width, cvs.height); // Save for undo
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
initBrush();
} else if (file.type === 'application/pdf') {
document.getElementById('remove-image-ui').style.display = 'none';
document.getElementById('remove-pdf-ui').style.display = 'block';
pdfFileBytes = await file.arrayBuffer();
renderPDF(pdfFileBytes);
}
});
// Image Brush Logic
function initBrush() {
const cvs = document.getElementById('remove-canvas');
const ctx = cvs.getContext('2d');
let isDrawing = false;
cvs.onmousedown = (e) => {
isDrawing = true;
ctx.beginPath();
const rect = cvs.getBoundingClientRect();
const scaleX = cvs.width / rect.width;
const scaleY = cvs.height / rect.height;
ctx.moveTo((e.clientX - rect.left) * scaleX, (e.clientY - rect.top) * scaleY);
};
cvs.onmousemove = (e) => {
if (!isDrawing) return;
const rect = cvs.getBoundingClientRect();
const scaleX = cvs.width / rect.width;
const scaleY = cvs.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
const size = document.getElementById('brush-size').value;
// Simple Inpainting: Fill with average color of surrounding area?
// Or just blur? Let's do a simple clone effect (taking pixel from nearby)
// For demo, we just smear using a blur-like effect by drawing with low opacity
// Actually, a simple 'blur' brush is easier to implement
ctx.lineWidth = size;
ctx.lineCap = 'round';
ctx.strokeStyle = '#fff'; // White out for simplicity or...
// Better: Get color from nearby? Too complex for this snippet.
// Let's implement a 'Blur' brush by copying a region slightly offset
// Demo: Just paint white (assuming white background document)
// or blur.
ctx.save();
ctx.filter = 'blur(5px)';
ctx.globalCompositeOperation = 'source-over';
// Draw line
ctx.lineTo(x, y);
ctx.stroke();
ctx.restore();
};
cvs.onmouseup = () => isDrawing = false;
}
window.undoRemove = function() {
if (!removeImgData) return;
const cvs = document.getElementById('remove-canvas');
const ctx = cvs.getContext('2d');
ctx.putImageData(removeImgData, 0, 0);
}
// PDF Logic
async function renderPDF(data) {
const loadingTask = pdfjsLib.getDocument(data);
const pdf = await loadingTask.promise;
const container = document.getElementById('pdf-container');
container.innerHTML = ''; // Clear
// Render first 3 pages only for demo performance
const numPages = Math.min(pdf.numPages, 3);
for (let i = 1; i <= numPages; i++) {
const page = await pdf.getPage(i);
const scale = 1.0;
const viewport = page.getViewport({scale: scale});
const wrapper = document.createElement('div');
wrapper.className = 'pdf-page-wrapper';
wrapper.dataset.pageIndex = i - 1; // 0-based
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport
};
await page.render(renderContext).promise;
wrapper.appendChild(canvas);
container.appendChild(wrapper);
// Add mask listener
initMasking(wrapper);
}
}
window.toggleMaskMode = function() {
isMaskMode = !isMaskMode;
const btn = document.getElementById('btn-add-mask-mode');
btn.style.background = isMaskMode ? '#e6a23c' : '#49b1f5';
btn.innerText = isMaskMode ? '退出框选模式' : '🔳 进入框选模式';
}
function initMasking(wrapper) {
let startX, startY;
let currentMask = null;
wrapper.addEventListener('mousedown', (e) => {
if (!isMaskMode) return;
const rect = wrapper.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
currentMask = document.createElement('div');
currentMask.className = 'remove-mask';
currentMask.style.left = startX + 'px';
currentMask.style.top = startY + 'px';
wrapper.appendChild(currentMask);
});
wrapper.addEventListener('mousemove', (e) => {
if (!isMaskMode || !currentMask) return;
const rect = wrapper.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
const width = Math.abs(currentX - startX);
const height = Math.abs(currentY - startY);
const left = Math.min(currentX, startX);
const top = Math.min(currentY, startY);
currentMask.style.width = width + 'px';
currentMask.style.height = height + 'px';
currentMask.style.left = left + 'px';
currentMask.style.top = top + 'px';
});
wrapper.addEventListener('mouseup', () => {
currentMask = null;
});
}
window.saveCleanPDF = async function() {
if (!pdfFileBytes) return;
const { PDFDocument, rgb } = PDFLib;
const pdfDoc = await PDFDocument.load(pdfFileBytes);
const pages = pdfDoc.getPages();
// Iterate over all DOM wrappers to find masks
const wrappers = document.querySelectorAll('.pdf-page-wrapper');
wrappers.forEach(wrapper => {
const pageIndex = parseInt(wrapper.dataset.pageIndex);
if (pageIndex >= pages.length) return;
const page = pages[pageIndex];
const { width, height } = page.getSize();
// Canvas size might scale, assume 1:1 for now or calculate scale
const canvas = wrapper.querySelector('canvas');
const scaleX = width / canvas.width;
const scaleY = height / canvas.height;
const masks = wrapper.querySelectorAll('.remove-mask');
masks.forEach(mask => {
// DOM coords (top-left is 0,0)
const x = parseFloat(mask.style.left);
const y = parseFloat(mask.style.top);
const w = parseFloat(mask.style.width);
const h = parseFloat(mask.style.height);
// PDF coords (bottom-left is 0,0 usually, but PDFLib handles it)
// We need to convert DOM Y (from top) to PDF Y (from bottom)
// PDF Y = PageHeight - (DOM Y + Height) * Scale
const rectX = x * scaleX;
const rectW = w * scaleX;
const rectH = h * scaleY;
const rectY = height - (y * scaleY) - rectH;
page.drawRectangle({
x: rectX,
y: rectY,
width: rectW,
height: rectH,
color: rgb(1, 1, 1), // White
});
});
});
const pdfBytes = await pdfDoc.save();
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'cleaned_document.pdf';
link.click();
};
}
// Initialize when DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initWatermarkTool);
} else {
initWatermarkTool();
}
})();