421 lines
17 KiB
JavaScript
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();
|
|
}
|
|
})();
|