乐于分享
好东西不私藏

一个无广告无收费的图片PDF加水印工具(只要有浏览器就能用),找AI制作了一个

一个无广告无收费的图片PDF加水印工具(只要有浏览器就能用),找AI制作了一个

点击关注上方“消遣杂谈”,关注后了解更多内容。

前言:根据丈母娘的需求,我借鉴了相声大哥的思路,我寻思能不能用html制作一个图片、PDF加水印的工具能,说干就干,然后人工智能就给我搞好了。特写此文,我会把代码贴出来,从此不求人。文中带小广告。排版进行了重新调整。        

使用非常简单,无论是win系统还是银河麒麟系统重,自己将代码(代码在后面)复制进入txt文本,然后改后缀名为html即可

上图是我在WIN系统的edge的实际图
使用非常简单,视频都不用做了,哈哈,到时候录制视频只是为了放在B站上
问询了人工智能,直接给我
做了这个以后,很感叹,只要我会提需求

那么人工智能就能帮我完成我的诉求

我不是一个专业的编程人员,我也没有那么多专业的知识,但是我是个材料,我知道自己如何将自己的诉求表达清楚,我掌握了很基础的一些知识,能初步看懂这些底层代码。因为自己预览,感觉影响观感,所以代码直接放后面,本来想着要不要删除算了,觉得这样比较快,就放着吧。

代码如下
<!DOCTYPE html><htmllang="zh-CN"><head>    <metacharset="UTF-8">    <metaname="viewport"content="width=device-width, initial-scale=1.0">    <title>消遣杂谈- 图片与PDF水印工具</title>    <linkrel="preconnect"href="https://fonts.googleapis.com">    <linkrel="preconnect"href="https://fonts.gstatic.com"crossorigin>    <linkhref="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap"rel="stylesheet">    <scriptsrc="https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>    <style>        * { margin0padding0box-sizing: border-box; }        :root {            --primary#4f46e5;            --primary-light#6366f1;            --primary-dark#4338ca;            --bg#f0f2f5;            --card-bg#ffffff;            --text#1e293b;            --text-light#64748b;            --border#e2e8f0;            --success#10b981;            --danger#ef4444;            --radius12px;            --shadow0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);            --shadow-lg0 10px 30px rgba(0,0,0,0.1);        }        body {            font-family'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;            backgroundvar(--bg);            colorvar(--text);            min-height100vh;            line-height1.6;        }        /* ===== Header ===== */        header {            backgroundlinear-gradient(135degvar(--primary) 0%var(--primary-light) 100%);            color: white;            padding28px 24px;            text-align: center;            box-shadow0 4px 20px rgba(79702290.25);        }        header h1 {            font-size1.8rem;            font-weight700;            margin-bottom6px;        }        header p {            font-size0.95rem;            opacity0.9;        }        /* ===== Layout ===== */        .container {            max-width1100px;            margin0 auto;            padding24px 20px 60px;        }        .main-grid {            display: grid;            grid-template-columns1fr 1fr;            gap20px;        }        @media (max-width820px) {            .main-grid { grid-template-columns1fr; }        }        .card {            backgroundvar(--card-bg);            border-radiusvar(--radius);            padding24px;            box-shadowvar(--shadow);        }        .card h2 {            font-size1.15rem;            font-weight700;            margin-bottom18px;            display: flex;            align-items: center;            gap8px;        }        .card h2 .icon {            width28px;            height28px;            border-radius8px;            backgroundlinear-gradient(135degvar(--primary), var(--primary-light));            display: inline-flex;            align-items: center;            justify-content: center;            color: white;            font-size0.8rem;            flex-shrink0;        }        /* ===== Upload Zone ===== */        .upload-zone {            border2px dashed var(--border);            border-radiusvar(--radius);            padding36px 20px;            text-align: center;            cursor: pointer;            transition: all 0.25s ease;            background#fafbfc;        }        .upload-zone:hover {            border-colorvar(--primary-light);            background#f5f3ff;        }        .upload-zone.dragover {            border-colorvar(--primary);            background#eef2ff;            transformscale(1.01);        }        .upload-zone .upload-icon {            font-size2.5rem;            margin-bottom8px;        }        .upload-zone p {            colorvar(--text-light);            font-size0.9rem;        }        .upload-zone .browse-link {            colorvar(--primary);            font-weight500;            text-decoration: underline;        }        .upload-zone .formats {            font-size0.8rem;            colorvar(--text-light);            margin-top6px;        }        /* ===== File List ===== */        .file-list {            margin-top16px;            display: flex;            flex-direction: column;            gap8px;        }        .file-item {            display: flex;            align-items: center;            gap10px;            padding10px 14px;            background#f8fafc;            border1px solid var(--border);            border-radius8px;            font-size0.875rem;            transition: all 0.2s;        }        .file-item:hover { border-colorvar(--primary-light); }        .file-item .file-type-badge {            padding2px 8px;            border-radius6px;            font-size0.7rem;            font-weight600;            text-transform: uppercase;            color: white;            flex-shrink0;        }        .file-item .file-type-badge.img { background#3b82f6; }        .file-item .file-type-badge.pdf { background#ef4444; }        .file-item .file-name {            flex1;            overflow: hidden;            text-overflow: ellipsis;            white-space: nowrap;        }        .file-item .file-size {            colorvar(--text-light);            font-size0.78rem;            flex-shrink0;        }        .file-item .remove-btn {            background: none;            border: none;            colorvar(--text-light);            cursor: pointer;            font-size1.1rem;            padding2px 6px;            border-radius4px;            transition: all 0.2s;        }        .file-item .remove-btn:hover {            colorvar(--danger);            background#fef2f2;        }        /* ===== Settings ===== */        .settings-group {            display: flex;            flex-direction: column;            gap18px;        }        .setting-row {            display: flex;            flex-direction: column;            gap6px;        }        .setting-row label {            font-size0.85rem;            font-weight500;            colorvar(--text);            display: flex;            justify-content: space-between;            align-items: center;        }        .setting-row label .value-tag {            backgroundvar(--primary);            color: white;            padding1px 8px;            border-radius6px;            font-size0.75rem;            font-weight600;            min-width36px;            text-align: center;        }        .setting-row input[type="text"] {            padding10px 14px;            border1px solid var(--border);            border-radius8px;            font-size0.9rem;            font-family: inherit;            transition: border-color 0.2s;        }        .setting-row input[type="text"]:focus {            outline: none;            border-colorvar(--primary);            box-shadow0 0 0 3px rgba(79702290.1);        }        .setting-row input[type="range"] {            -webkit-appearance: none;            appearance: none;            width100%;            height6px;            backgroundvar(--border);            border-radius3px;            outline: none;        }        .setting-row input[type="range"]::-webkit-slider-thumb {            -webkit-appearance: none;            appearance: none;            width18px;            height18px;            backgroundvar(--primary);            border-radius50%;            cursor: pointer;            border2px solid white;            box-shadow0 2px 4px rgba(0,0,0,0.15);            transition: transform 0.15s;        }        .setting-row input[type="range"]::-webkit-slider-thumb:hover {            transformscale(1.2);        }        .setting-row input[type="range"]::-moz-range-thumb {            width18px;            height18px;            backgroundvar(--primary);            border-radius50%;            cursor: pointer;            border2px solid white;            box-shadow0 2px 4px rgba(0,0,0,0.15);        }        .setting-row input[type="color"] {            width100%;            height40px;            border1px solid var(--border);            border-radius8px;            cursor: pointer;            background: white;            padding4px;        }        .setting-row select {            padding10px 14px;            border1px solid var(--border);            border-radius8px;            font-size0.9rem;            font-family: inherit;            background: white;            cursor: pointer;        }        .setting-row select:focus {            outline: none;            border-colorvar(--primary);        }        .setting-inline {            display: flex;            gap12px;        }        .setting-inline > .setting-row { flex1; }        /* ===== Preview ===== */        .preview-wrapper {            margin-top16px;            border-radiusvar(--radius);            overflow: hidden;            background#f1f5f9;            border1px solid var(--border);            display: flex;            justify-content: center;            align-items: center;            min-height200px;            position: relative;        }        .preview-wrapper canvas {            max-width100%;            height: auto;            display: block;        }        .preview-placeholder {            colorvar(--text-light);            font-size0.9rem;            padding40px;        }        /* ===== Buttons ===== */        .btn {            padding12px 28px;            border: none;            border-radius8px;            font-size0.95rem;            font-weight600;            font-family: inherit;            cursor: pointer;            transition: all 0.2s ease;            display: inline-flex;            align-items: center;            justify-content: center;            gap8px;        }        .btn-primary {            backgroundlinear-gradient(135degvar(--primary), var(--primary-light));            color: white;            box-shadow0 4px 14px rgba(79702290.3);        }        .btn-primary:hover {            transformtranslateY(-1px);            box-shadow0 6px 20px rgba(79702290.4);        }        .btn-primary:active { transformtranslateY(0); }        .btn-primary:disabled {            opacity0.5;            cursor: not-allowed;            transform: none;            box-shadow: none;        }        .btn-secondary {            background: white;            colorvar(--text);            border1px solid var(--border);        }        .btn-secondary:hover {            border-colorvar(--primary);            colorvar(--primary);        }        .btn-sm {            padding7px 16px;            font-size0.82rem;        }        .btn-success {            backgroundvar(--success);            color: white;        }        .btn-success:hover { background#059669; }        .process-bar {            margin-top20px;            display: flex;            gap12px;            align-items: center;        }        /* ===== Results ===== */        .results-section {            margin-top28px;        }        .results-header {            display: flex;            justify-content: space-between;            align-items: center;            margin-bottom16px;        }        .results-header h2 {            font-size1.2rem;            font-weight700;        }        .results-grid {            display: grid;            grid-template-columnsrepeat(auto-fill, minmax(240px1fr));            gap16px;        }        .result-card {            backgroundvar(--card-bg);            border-radiusvar(--radius);            overflow: hidden;            box-shadowvar(--shadow);            transition: all 0.2s;        }        .result-card:hover {            box-shadowvar(--shadow-lg);            transformtranslateY(-2px);        }        .result-thumb {            width100%;            height180px;            background#f1f5f9;            display: flex;            justify-content: center;            align-items: center;            overflow: hidden;            position: relative;        }        .result-thumb img {            max-width100%;            max-height100%;            object-fit: contain;        }        .result-thumb .pdf-placeholder {            font-size3rem;        }        .result-info {            padding12px 14px;        }        .result-info .name {            font-size0.82rem;            font-weight500;            overflow: hidden;            text-overflow: ellipsis;            white-space: nowrap;            margin-bottom8px;        }        .result-info .actions {            display: flex;            gap8px;        }        /* ===== Progress ===== */        .progress-overlay {            position: fixed;            top0left0right0bottom0;            backgroundrgba(0,0,0,0.5);            display: none;            justify-content: center;            align-items: center;            z-index1000;        }        .progress-overlay.active { display: flex; }        .progress-box {            background: white;            padding32px 40px;            border-radiusvar(--radius);            text-align: center;            box-shadowvar(--shadow-lg);        }        .spinner {            width48px;            height48px;            border4px solid var(--border);            border-top-colorvar(--primary);            border-radius50%;            animation: spin 0.8s linear infinite;            margin0 auto 16px;        }        @keyframes spin { to { transformrotate(360deg); } }        .progress-text {            font-size0.95rem;            colorvar(--text);        }        .progress-detail {            font-size0.82rem;            colorvar(--text-light);            margin-top4px;        }        /* ===== Empty State ===== */        .empty-state {            text-align: center;            padding40px 20px;            colorvar(--text-light);        }        .empty-state .emoji {            font-size2.5rem;            margin-bottom8px;        }        /* ===== Toast ===== */        .toast {            position: fixed;            bottom24px;            left50%;            transformtranslateX(-50%translateY(100px);            backgroundvar(--text);            color: white;            padding12px 24px;            border-radius8px;            font-size0.88rem;            z-index2000;            opacity0;            transition: all 0.3s ease;            box-shadowvar(--shadow-lg);        }        .toast.show {            opacity1;            transformtranslateX(-50%translateY(0);        }        .toast.error { backgroundvar(--danger); }        .toast.success { backgroundvar(--success); }    </style></head><body><header>    <h1>消遣杂谈制作的水印工具@HBIKO</h1>    <p>上传图片或PDF,自定义水印文字,一键导出水印文件</p></header><divclass="container">    <divclass="main-grid">        <!-- Left: Upload + Files -->        <divclass="card">            <h2><spanclass="icon">1</span>上传文件</h2>            <divclass="upload-zone"id="uploadZone">                <divclass="upload-icon">📁</div>                <p>将文件拖拽到此处,或 <spanclass="browse-link">点击浏览</span></p>                <divclass="formats">支持 PNG / JPG / JPEG / GIF / WEBP / PDF</div>            </div>            <inputtype="file"id="fileInput"multipleaccept="image/png,image/jpeg,image/gif,image/webp,application/pdf"hidden>            <divclass="file-list"id="fileList"></div>        </div>        <!-- Right: Settings + Preview -->        <divclass="card">            <h2><spanclass="icon">2</span>水印设置</h2>            <divclass="settings-group">                <divclass="setting-row">                    <label>水印文字</label>                    <inputtype="text"id="wmText"value="仅供预览 严禁商用"placeholder="请输入水印文字...">                </div>                <divclass="setting-inline">                    <divclass="setting-row">                        <label>字体大小 <spanclass="value-tag"id="fontSizeTag">28</span></label>                        <inputtype="range"id="fontSize"min="10"max="80"value="28">                    </div>                    <divclass="setting-row">                        <label>透明度 <spanclass="value-tag"id="opacityTag">30%</span></label>                        <inputtype="range"id="opacity"min="5"max="100"value="30">                    </div>                </div>                <divclass="setting-inline">                    <divclass="setting-row">                        <label>旋转角度 <spanclass="value-tag"id="rotationTag">-30°</span></label>                        <inputtype="range"id="rotation"min="-90"max="90"value="-30">                    </div>                    <divclass="setting-row">                        <label>文字颜色</label>                        <inputtype="color"id="color"value="#808080">                    </div>                </div>                <divclass="setting-inline">                    <divclass="setting-row">                        <label>布局模式</label>                        <selectid="layoutMode">                            <optionvalue="tile">平铺(满屏重复)</option>                            <optionvalue="center">居中(单个水印)</option>                        </select>                    </div>                    <divclass="setting-row"id="densityRow">                        <label>平铺密度 <spanclass="value-tag"id="densityTag"></span></label>                        <inputtype="range"id="density"min="1"max="5"value="3">                    </div>                </div>                <divclass="setting-row">                    <label>图片导出格式</label>                    <selectid="imgFormat">                        <optionvalue="image/png">PNG(无损,支持透明)</option>                        <optionvalue="image/jpeg">JPEG(体积小)</option>                    </select>                </div>            </div>            <h2style="margin-top: 24px;"><spanclass="icon">3</span>实时预览</h2>            <divclass="preview-wrapper"id="previewWrapper">                <divclass="preview-placeholder">输入水印文字后此处显示预览效果</div>            </div>        </div>    </div>    <!-- Process Button -->    <divclass="process-bar">        <buttonclass="btn btn-primary"id="processBtn"disabled>            <span>✨ 添加水印并导出</span>        </button>        <buttonclass="btn btn-secondary"id="clearBtn">清空文件</button>        <spanid="fileCount"style="font-size: 0.85rem; color: var(--text-light);"></span>    </div>    <!-- Results -->    <divclass="results-section"id="resultsSection"style="display: none;">        <divclass="results-header">            <h2>处理结果</h2>            <buttonclass="btn btn-success btn-sm"id="downloadAllBtn">⬇ 全部下载</button>        </div>        <divclass="results-grid"id="resultsGrid"></div>    </div></div><!-- Progress Overlay --><divclass="progress-overlay"id="progressOverlay">    <divclass="progress-box">        <divclass="spinner"></div>        <divclass="progress-text"id="progressText">正在处理文件...</div>        <divclass="progress-detail"id="progressDetail"></div>    </div></div><!-- Toast --><divclass="toast"id="toast"></div><script>(function() {    'use strict';    // ===== State =====    let files = [];    let results = [];    let fontReady = false;    // ===== DOM =====    const uploadZone = document.getElementById('uploadZone');    const fileInput = document.getElementById('fileInput');    const fileList = document.getElementById('fileList');    const fileCount = document.getElementById('fileCount');    const processBtn = document.getElementById('processBtn');    const clearBtn = document.getElementById('clearBtn');    const resultsSection = document.getElementById('resultsSection');    const resultsGrid = document.getElementById('resultsGrid');    const downloadAllBtn = document.getElementById('downloadAllBtn');    const progressOverlay = document.getElementById('progressOverlay');    const progressText = document.getElementById('progressText');    const progressDetail = document.getElementById('progressDetail');    const toast = document.getElementById('toast');    // Settings    const wmText = document.getElementById('wmText');    const fontSize = document.getElementById('fontSize');    const fontSizeTag = document.getElementById('fontSizeTag');    const opacity = document.getElementById('opacity');    const opacityTag = document.getElementById('opacityTag');    const rotation = document.getElementById('rotation');    const rotationTag = document.getElementById('rotationTag');    const color = document.getElementById('color');    const layoutMode = document.getElementById('layoutMode');    const density = document.getElementById('density');    const densityTag = document.getElementById('densityTag');    const densityRow = document.getElementById('densityRow');    const imgFormat = document.getElementById('imgFormat');    const previewWrapper = document.getElementById('previewWrapper');    const densityLabels = { 1'疏'2'较疏'3'中'4'较密'5'密' };    // ===== Init Font =====    async function initFont() {        try {            await document.fonts.load('700 28px "Noto Sans SC"');            await document.fonts.load('400 28px "Noto Sans SC"');            fontReady = true;        } catch(e) {            console.warn('Font load failed, using fallback:', e);            fontReady = true;        }        updatePreview();    }    // ===== Toast =====    function showToast(msg, type) {        toast.textContent = msg;        toast.className = 'toast show' + (type ? ' ' + type : '');        setTimeout(() => { toast.className = 'toast'; }, 3000);    }    // ===== Settings Object =====    function getSettings() {        return {            text: wmText.value || '水印',            fontSizeparseInt(fontSize.value),            opacityparseInt(opacity.value) / 100,            rotationparseInt(rotation.value),            color: color.value,            layoutMode: layoutMode.value,            densityparseInt(density.value),            imgFormat: imgFormat.value        };    }    // ===== Watermark Canvas Drawing =====    function drawWatermarkOnCanvas(ctx, width, height, settings) {        const s = settings;        ctx.save();        ctx.fillStyle = s.color;        ctx.globalAlpha = s.opacity;        ctx.font = '700 ' + s.fontSize + 'px "Noto Sans SC", sans-serif';        ctx.textAlign = 'center';        ctx.textBaseline = 'middle';        const rad = s.rotation * Math.PI / 180;        const textWidth = ctx.measureText(s.text).width;        const stepX = textWidth + s.fontSize * (7 - s.density);        const stepY = s.fontSize * (4 + (5 - s.density) * 1.5);        if (s.layoutMode === 'center') {            ctx.translate(width / 2, height / 2);            ctx.rotate(rad);            ctx.fillText(s.text00);        } else {            // Tile mode: cover entire canvas, including edges after rotation            const diag = Math.sqrt(width * width + height * height);            const cols = Math.ceil(diag / stepX) + 2;            const rows = Math.ceil(diag / stepY) + 2;            const startX = (width / 2) - (cols * stepX) / 2;            const startY = (height / 2) - (rows * stepY) / 2;            for (let r = 0; r < rows; r++) {                // Offset every other row for a nicer pattern                const offsetX = (r % 2) * (stepX / 2);                for (let c = 0; c < cols; c++) {                    const x = startX + c * stepX + offsetX;                    const y = startY + r * stepY;                    ctx.save();                    ctx.translate(x, y);                    ctx.rotate(rad);                    ctx.fillText(s.text00);                    ctx.restore();                }            }        }        ctx.restore();    }    // Creates a transparent watermark overlay canvas    function createWatermarkCanvas(width, height, settings, scale) {        scale = scale || 1;        const canvas = document.createElement('canvas');        canvas.width = Math.round(width * scale);        canvas.height = Math.round(height * scale);        const ctx = canvas.getContext('2d');        if (scale !== 1) {            ctx.scale(scale, scale);        }        drawWatermarkOnCanvas(ctx, width, height, settings);        return canvas;    }    // ===== Preview =====    function updatePreview() {        const s = getSettings();        if (!s.text.trim()) {            previewWrapper.innerHTML = '<div class="preview-placeholder">请输入水印文字</div>';            return;        }        const pw = 480, ph = 270;        const canvas = document.createElement('canvas');        canvas.width = pw;        canvas.height = ph;        const ctx = canvas.getContext('2d');        // Draw a sample background        const grad = ctx.createLinearGradient(00, pw, ph);        grad.addColorStop(0'#e0e7ff');        grad.addColorStop(1'#f0f9ff');        ctx.fillStyle = grad;        ctx.fillRect(00, pw, ph);        // Draw sample content lines        ctx.strokeStyle = 'rgba(100,116,139,0.15)';        ctx.lineWidth = 1;        for (let i = 1; i <= 6; i++) {            const y = (ph / 7) * i;            ctx.beginPath();            ctx.moveTo(40, y);            ctx.lineTo(pw - 40, y);            ctx.stroke();        }        // Draw watermark        drawWatermarkOnCanvas(ctx, pw, ph, s);        previewWrapper.innerHTML = '';        previewWrapper.appendChild(canvas);    }    // ===== File Handling =====    function handleFiles(fileListObj) {        const newFiles = Array.from(fileListObj);        let added = 0;        for (const f of newFiles) {            if (files.some(existing => existing.name === f.name && existing.size === f.size)) {                continue;            }            if (f.type.startsWith('image/') || f.type === 'application/pdf') {                files.push(f);                added++;            }        }        if (added > 0) {            showToast('已添加 ' + added + ' 个文件''success');        }        renderFileList();        updateButtons();    }    function renderFileList() {        fileList.innerHTML = '';        files.forEach((f, i) => {            const item = document.createElement('div');            item.className = 'file-item';            const isPDF = f.type === 'application/pdf';            const badgeClass = isPDF ? 'pdf' : 'img';            const badgeText = isPDF ? 'PDF' : 'IMG';            item.innerHTML =                '<span class="file-type-badge ' + badgeClass + '">' + badgeText + '</span>' +                '<span class="file-name">' + escapeHtml(f.name) + '</span>' +                '<span class="file-size">' + formatSize(f.size) + '</span>' +                '<button class="remove-btn" data-index="' + i + '" title="移除">✕</button>';            fileList.appendChild(item);        });        fileList.querySelectorAll('.remove-btn').forEach(btn => {            btn.addEventListener('click'function() {                const idx = parseInt(this.dataset.index);                files.splice(idx, 1);                renderFileList();                updateButtons();            });        });    }    function updateButtons() {        processBtn.disabled = files.length === 0;        fileCount.textContent = files.length > 0 ? '共 ' + files.length + ' 个文件' : '';    }    // ===== Process Files =====    async function processAll() {        if (files.length === 0return;        results = [];        resultsGrid.innerHTML = '';        const settings = getSettings();        if (!settings.text.trim()) {            showToast('请输入水印文字''error');            return;        }        progressOverlay.classList.add('active');        for (let i = 0; i < files.length; i++) {            const f = files[i];            progressText.textContent = '正在处理文件 (' + (i + 1) + '/' + files.length + ')';            progressDetail.textContent = f.name;            try {                if (f.type === 'application/pdf') {                    await processPDF(f, settings);                } else {                    await processImage(f, settings);                }            } catch(err) {                console.error('Error processing', f.name, err);                showToast('处理失败: ' + f.name'error');            }        }        progressOverlay.classList.remove('active');        renderResults();        if (results.length > 0) {            resultsSection.style.display = 'block';            showToast('完成!共处理 ' + results.length + ' 个文件''success');            resultsSection.scrollIntoView({ behavior'smooth' });        }    }    // ===== Process Image =====    function processImage(file, settings) {        return new Promise((resolve, reject) => {            const img = new Image();            const url = URL.createObjectURL(file);            img.onload = function() {                try {                    const canvas = document.createElement('canvas');                    canvas.width = img.naturalWidth;                    canvas.height = img.naturalHeight;                    const ctx = canvas.getContext('2d');                    ctx.drawImage(img, 00);                    // Draw watermark                    drawWatermarkOnCanvas(ctx, canvas.width, canvas.height, settings);                    // Export                    const format = settings.imgFormat;                    const ext = format === 'image/png' ? 'png' : 'jpg';                    const baseName = file.name.replace(/\.[^.]+$/'');                    const outName = baseName + '_watermarked.' + ext;                    canvas.toBlob(function(blob) {                        URL.revokeObjectURL(url);                        if (blob) {                            // Create thumbnail                            const thumbUrl = URL.createObjectURL(blob);                            results.push({                                name: outName,                                blob: blob,                                thumbUrl: thumbUrl,                                isImagetrue                            });                        }                        resolve();                    }, format, 0.92);                } catch(err) {                    URL.revokeObjectURL(url);                    reject(err);                }            };            img.onerror = function() {                URL.revokeObjectURL(url);                reject(new Error('图片加载失败'));            };            img.src = url;        });    }    // ===== Process PDF =====    async function processPDF(file, settings) {        const { PDFDocument } = PDFLib;        const arrayBuffer = await file.arrayBuffer();        const pdfDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryptiontrue });        const pages = pdfDoc.getPages();        for (const page of pages) {            const { width, height } = page.getSize();            // Create watermark overlay canvas at 2x scale for sharpness            const scale = 2;            const wmCanvas = createWatermarkCanvas(width, height, settings, scale);            // Convert canvas to PNG bytes            const pngBlob = await new Promise(function(resolve) {                wmCanvas.toBlob(resolve, 'image/png');            });            const pngBytes = await pngBlob.arrayBuffer();            // Embed PNG into PDF            const pngImage = await pdfDoc.embedPng(pngBytes);            // Draw watermark image over the page (on top of existing content)            page.drawImage(pngImage, {                x0,                y0,                width: width,                height: height            });        }        // Save        const pdfBytes = await pdfDoc.save();        const blob = new Blob([pdfBytes], { type'application/pdf' });        const baseName = file.name.replace(/\.[^.]+$/'');        const outName = baseName + '_watermarked.pdf';        results.push({            name: outName,            blob: blob,            thumbUrlnull,            isImagefalse        });    }    // ===== Render Results =====    function renderResults() {        resultsGrid.innerHTML = '';        results.forEach((r, i) => {            const card = document.createElement('div');            card.className = 'result-card';            let thumbHtml;            if (r.isImage) {                thumbHtml = '<img src="' + r.thumbUrl + '" alt="预览">';            } else {                thumbHtml = '<div class="pdf-placeholder">📄</div>';            }            card.innerHTML =                '<div class="result-thumb">' + thumbHtml + '</div>' +                '<div class="result-info">' +                    '<div class="name">' + escapeHtml(r.name) + '</div>' +                    '<div class="actions">' +                        '<button class="btn btn-primary btn-sm" data-index="' + i + '">⬇ 下载</button>' +                    '</div>' +                '</div>';            resultsGrid.appendChild(card);        });        resultsGrid.querySelectorAll('button[data-index]').forEach(btn => {            btn.addEventListener('click'function() {                downloadResult(parseInt(this.dataset.index));            });        });    }    function downloadResult(index) {        const r = results[index];        if (!r) return;        const url = URL.createObjectURL(r.blob);        const a = document.createElement('a');        a.href = url;        a.download = r.name;        document.body.appendChild(a);        a.click();        document.body.removeChild(a);        setTimeout(() => URL.revokeObjectURL(url), 1000);    }    function downloadAll() {        if (results.length === 0return;        results.forEach((r, i) => {            setTimeout(() => downloadResult(i), i * 300);        });        showToast('开始下载全部文件...''success');    }    // ===== Utility =====    function formatSize(bytes) {        if (bytes < 1024return bytes + ' B';        if (bytes < 1048576return (bytes / 1024).toFixed(1) + ' KB';        return (bytes / 1048576).toFixed(1) + ' MB';    }    function escapeHtml(str) {        const div = document.createElement('div');        div.textContent = str;        return div.innerHTML;    }    // ===== Event Listeners =====    uploadZone.addEventListener('click'() => fileInput.click());    fileInput.addEventListener('change'function() {        if (this.files.length > 0handleFiles(this.files);        this.value = '';    });    uploadZone.addEventListener('dragover'function(e) {        e.preventDefault();        this.classList.add('dragover');    });    uploadZone.addEventListener('dragleave'function() {        this.classList.remove('dragover');    });    uploadZone.addEventListener('drop'function(e) {        e.preventDefault();        this.classList.remove('dragover');        if (e.dataTransfer.files.length > 0handleFiles(e.dataTransfer.files);    });    // Settings listeners    fontSize.addEventListener('input'function() {        fontSizeTag.textContent = this.value;        updatePreview();    });    opacity.addEventListener('input'function() {        opacityTag.textContent = this.value + '%';        updatePreview();    });    rotation.addEventListener('input'function() {        rotationTag.textContent = this.value + '°';        updatePreview();    });    color.addEventListener('input', updatePreview);    wmText.addEventListener('input', updatePreview);    layoutMode.addEventListener('change'function() {        densityRow.style.display = this.value === 'tile' ? 'flex' : 'none';        updatePreview();    });    density.addEventListener('input'function() {        densityTag.textContent = densityLabels[this.value];        updatePreview();    });    imgFormat.addEventListener('change', updatePreview);    processBtn.addEventListener('click', processAll);    clearBtn.addEventListener('click'function() {        if (files.length === 0return;        files = [];        results = [];        renderFileList();        updateButtons();        resultsSection.style.display = 'none';        showToast('已清空文件列表');    });    downloadAllBtn.addEventListener('click', downloadAll);    // Init    densityRow.style.display = 'flex';    initFont();})();</script></body></html>
最近筹划写一篇:AI时代,普通人如何学习的文章。

以前写的少,现在写的多,很多时候我还有很多文章,工作很忙,实在没空写,写了个题目,好多想写的,需要时间梳理。

最后希望大家通过这个文章能有所收获,让我们一起变得更强

如果觉得不错,给我点个赞,分享给身边的人,我最近把我写的文章都导入到了ima知识库,强烈推荐看我置顶的文章,基本上我自己的ima知识库,对我来说非常好用。

觉得内容好看

点个↓↓