找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 4|回复: 0

[Windows] 图片加文字水印软件工具

[复制链接]

139

主题

16

回帖

1万

积分

VIP会员

积分
10158

VIP会员资源整合达人专属

威望
78
阁豆
9818
最后登录
2026-7-28
在线时间
19 小时
发表于 6 天前 | 显示全部楼层 |阅读模式
图片加文字水印软件工具
给图片加水印,日常多用于证件照加文字水印,一个简单的网页即可

软件成品下载地址:
游客,如果您要查看本帖隐藏内容请回复


源代码:
  1. [hide]<!DOCTYPE html>
  2. <html lang="zh-cn">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
  6.   <title>批量文字水印工具</title>
  7.   <style>
  8.     * { box-sizing: border-box; margin: 0; padding: 0; }
  9.     body { font-family: 'Microsoft YaHei', SimSun, sans-serif; background: #f7f8fa; }
  10.     .navbar {
  11.       width: 100%; background: #2E65BD; color: #fff; padding: 0;
  12.       box-shadow: 0 2px 8px #e0e6f6; display: flex; align-items: center;
  13.       justify-content: space-between; height: 54px; position: sticky; top: 0; z-index: 100;
  14.     }
  15.     .navbar .nav-left { font-size: 20px; font-weight: bold; letter-spacing: 2px; margin-left: 140px; }
  16.     .navbar .back-link { position: absolute; left: 40px; text-decoration: none; color: #fff; font-size: 15px; }
  17.     .navbar .back-link:hover { text-decoration: underline; }
  18.     .container { max-width: 1400px; margin: 20px auto; background: #fff; border-radius: 8px; box-shadow: 0 2px 8px #eee; padding: 24px 28px; }
  19.     h2 { font-size: 22px; margin-bottom: 6px; }
  20.     .desc { color: #666; font-size: 14px; margin-bottom: 16px; line-height: 1.6; }


  21.     .toolbar { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; align-items: center; }
  22.     .toolbar .btn { white-space: nowrap; }


  23.     .toggle-wrap { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #555; cursor: pointer; user-select: none; }
  24.     .switch { position: relative; display: inline-block; width: 36px; height: 20px; }
  25.     .switch input { opacity: 0; width: 0; height: 0; }
  26.     .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #ccc; transition: .2s; border-radius: 20px; }
  27.     .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background: #fff; transition: .2s; border-radius: 50%; }
  28.     .switch input:checked + .slider { background: #409eff; }
  29.     .switch input:checked + .slider:before { transform: translateX(16px); }
  30.     .switch input:disabled + .slider { opacity: 0.5; cursor: not-allowed; }


  31.     .main-layout { display: flex; gap: 16px; align-items: stretch; }
  32.     .left-panel { flex: 0 0 140px; min-width: 0; display: flex; flex-direction: column; }
  33.     .right-panel { flex: 1; min-width: 0; display: flex; flex-direction: column; }


  34.     .settings-panel {
  35.       background: #f7f7fb; border-radius: 8px; padding: 16px 20px;
  36.       margin-bottom: 14px; box-shadow: 0 1px 3px #eee;
  37.     }
  38.     .settings-panel h3 { font-size: 15px; margin-bottom: 10px; color: #333; }
  39.     .form-row { display: flex; flex-wrap: nowrap; gap: 10px; align-items: flex-end; margin-bottom: 0; overflow-x: auto; }
  40.     .form-group { display: flex; flex-direction: column; flex-shrink: 0; }
  41.     .form-group label { font-size: 12px; color: #555; margin-bottom: 3px; }
  42.     .form-group input, .form-group select {
  43.       height: 34px; padding: 4px 10px; border: 1px solid #ccc; border-radius: 6px;
  44.       font-size: 13px; min-width: 90px;
  45.     }
  46.     .form-group input[type="number"] { width: 80px; }
  47.     .form-group input[type="range"] { min-width: 120px; height: auto; }
  48.     .color-wrap { display: flex; align-items: center; gap: 6px; }
  49.     .color-wrap input[type="color"] { height: 34px; width: 36px; border: 1px solid #ccc; border-radius: 6px; padding: 2px; cursor: pointer; }
  50.     .color-wrap .color-hex { font-size: 12px; color: #666; }


  51.     .btn { background: #409eff; color: #fff; border: none; border-radius: 6px; padding: 8px 16px; cursor: pointer; font-size: 13px; transition: background 0.2s; }
  52.     .btn:hover { background: #3a8ee6; }
  53.     .btn:active { background: #2d7cd6; }
  54.     .btn-success { background: #67c23a; }
  55.     .btn-success:hover { background: #5daf34; }
  56.     .btn-warn { background: #e6a23c; }
  57.     .btn-warn:hover { background: #cf9236; }
  58.     .btn-danger { background: #f56c6c; }
  59.     .btn-danger:hover { background: #e25c5c; }
  60.     .btn-sm { padding: 6px 12px; font-size: 12px; }


  61.     .thumb-section { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
  62.     .thumb-header {
  63.       display: flex; justify-content: space-between; align-items: center;
  64.       margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #eee;
  65.     }
  66.     .thumb-header h3 { font-size: 15px; color: #333; }
  67.     .thumb-header .count { font-size: 13px; color: #888; }
  68.     .thumb-grid {
  69.       display: flex; flex-direction: column;
  70.       gap: 8px; overflow-y: auto; flex: 1; padding: 2px;
  71.       max-height: calc(100vh - 260px);
  72.     }
  73.     .thumb-card {
  74.       border: 2px solid #e8e8e8; border-radius: 6px; overflow: hidden;
  75.       cursor: pointer; transition: border-color 0.2s, box-shadow 0.2s;
  76.       background: #fafafa; position: relative; flex-shrink: 0;
  77.     }
  78.     .thumb-card:hover { border-color: #a0c4ff; }
  79.     .thumb-card.active { border-color: #409eff; box-shadow: 0 0 0 2px rgba(64,158,255,0.3); }
  80.     .thumb-card img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; }
  81.     .thumb-card .thumb-name {
  82.       font-size: 10px; color: #555; padding: 3px 4px;
  83.       white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  84.       text-align: center; background: #f5f5f5;
  85.     }
  86.     .thumb-card .thumb-remove {
  87.       position: absolute; top: 2px; right: 2px; width: 18px; height: 18px;
  88.       border-radius: 50%; background: rgba(245,108,108,0.85); color: #fff;
  89.       border: none; font-size: 11px; cursor: pointer; display: none;
  90.       align-items: center; justify-content: center; line-height: 1;
  91.     }
  92.     .thumb-card:hover .thumb-remove { display: flex; }
  93.     .thumb-empty {
  94.       text-align: center; padding: 40px 10px;
  95.       color: #bbb; font-size: 13px;
  96.     }


  97.     .preview-section {
  98.       background: #f7f7fb; border-radius: 8px; padding: 16px;
  99.       flex: 1; display: flex; flex-direction: column;
  100.     }
  101.     .preview-header {
  102.       display: flex; justify-content: space-between; align-items: center;
  103.       margin-bottom: 10px;
  104.     }
  105.     .preview-header h3 { font-size: 15px; color: #333; }
  106.     .preview-header .preview-name { font-size: 12px; color: #888; max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  107.     .preview-canvas-wrap {
  108.       flex: 1; display: flex; align-items: center; justify-content: center;
  109.       background: repeating-conic-gradient(#e8e8e8 0% 25%, #fff 0% 50%) 0 0 / 16px 16px;
  110.       border-radius: 6px; overflow: hidden; min-height: 400px;
  111.     }
  112.     .preview-canvas-wrap canvas {
  113.       max-width: 100%; max-height: calc(100vh - 320px); display: block;
  114.       box-shadow: 0 2px 12px rgba(0,0,0,0.1);
  115.     }
  116.     .preview-empty {
  117.       text-align: center; color: #bbb; font-size: 15px; padding: 80px 20px;
  118.     }


  119.     .exporting-overlay {
  120.       position: fixed; left: 50%; top: 30%; transform: translate(-50%, -50%);
  121.       background: rgba(34,34,34,0.95); color: #fff; font-size: 18px;
  122.       padding: 20px 36px; border-radius: 10px; z-index: 9999;
  123.       box-shadow: 0 2px 16px rgba(0,0,0,0.3); display: none;
  124.     }


  125.     .opacity-val { font-size: 12px; color: #888; min-width: 30px; text-align: center; }

  126.     [url=home.php?mod=space&uid=945662]@media[/url] (max-width: 900px) {
  127.       .main-layout { flex-direction: column; }
  128.       .left-panel { flex: none; }
  129.       .right-panel { flex: none; }
  130.       .thumb-grid { max-height: 200px; flex-direction: row; flex-wrap: wrap; }
  131.       .thumb-card { width: 80px; flex-shrink: 0; }
  132.     }
  133.     @media (max-width: 700px) {
  134.       .container { margin: 0; border-radius: 0; padding: 10px 3vw 60px 3vw; }
  135.       .navbar { height: 44px; }
  136.       .navbar .nav-left { margin-left: 120px; font-size: 16px; }
  137.       .navbar .back-link { left: 10px; font-size: 13px; }
  138.     }
  139.   </style>
  140. </head>
  141. <body>

  142.   <div class="container">
  143.     <h2>批量文字水印工具</h2>

  144.     <div class="toolbar">
  145.       <button class="btn" onclick="document.getElementById('singleInput').click()">&#128444;&#65039; 选择图片</button>
  146.       <input type="file" id="singleInput" accept="image/*" multiple style="display:none;" />
  147.       <button class="btn" onclick="document.getElementById('folderInput').click()">&#128193; 选择文件夹</button>
  148.       <input type="file" id="folderInput" webkitdirectory multiple style="display:none;" />
  149.       <label class="toggle-wrap">
  150.         <span class="switch"><input type="checkbox" id="moveWatermarkSwitch" onchange="toggleMove()" /><span class="slider"></span></span>
  151.         移动水印
  152.       </label>
  153.       <label class="toggle-wrap">
  154.         <span class="switch"><input type="checkbox" id="individualSwitch" onchange="toggleIndividual()" /><span class="slider"></span></span>
  155.         单独调整
  156.       </label>
  157.       <span style="flex:1;"></span>
  158.       <button class="btn btn-warn btn-sm" onclick="exportZip()">压缩包下载</button>
  159.       <button class="btn btn-success btn-sm" onclick="exportAll()">导出全部</button>
  160.       <button class="btn btn-danger btn-sm" onclick="clearAll()">清空</button>
  161.     </div>


  162.     <div class="main-layout">

  163.       <div class="left-panel">
  164.         <div class="thumb-section">
  165.           <div class="thumb-header">
  166.             <h3>图片列表</h3>
  167.             <span class="count" id="thumbCount">共 0 张</span>
  168.           </div>
  169.           <div class="thumb-grid" id="thumbGrid">
  170.             <div class="thumb-empty">暂无图片,请上传</div>
  171.           </div>
  172.         </div>
  173.       </div>


  174.       <div class="right-panel">
  175.         <!-- 水印设置 -->
  176.         <div class="settings-panel">
  177.           <h3 style="margin-bottom:8px;">文字水印参数</h3>
  178.           <div class="form-row">
  179.             <div class="form-group">
  180.               <label>水印文字</label>
  181.               <input type="text" id="wmText" value="仅限办理***使用" placeholder="水印文字" style="width:180px;" />
  182.             </div>
  183.             <div class="form-group">
  184.               <label>字体</label>
  185.               <select id="wmFont">
  186.                 <option value="Microsoft YaHei" selected>微软雅黑</option>
  187.                 <option value="SimHei">黑体</option>
  188.                 <option value="SimSun">宋体</option>
  189.                 <option value="KaiTi">楷体</option>
  190.                 <option value="FangSong">仿宋</option>
  191.                 <option value="DengXian">等线</option>
  192.                 <option value="YouYuan">幼圆</option>
  193.                 <option value="LiSu">隶书</option>
  194.                 <option value="FZXiaoBiaoSong-B05S">方正小标宋</option>
  195.                 <option value="Arial">Arial</option>
  196.                 <option value="Times New Roman">Times New Roman</option>
  197.               </select>
  198.             </div>
  199.             <div class="form-group">
  200.               <label>字号</label>
  201.               <input type="number" id="wmSize" value="60" min="8" max="300" />
  202.             </div>
  203.             <div class="form-group">
  204.               <label>颜色</label>
  205.               <div class="color-wrap">
  206.                 <input type="color" id="wmColor" value="#000000" />
  207.               </div>
  208.             </div>
  209.             <div class="form-group">
  210.               <label>透明度</label>
  211.               <div style="display:flex;align-items:center;gap:4px;">
  212.                 <input type="range" id="wmOpacity" min="5" max="100" value="50" style="min-width:80px;" />
  213.                 <span class="opacity-val" id="wmOpacityVal">40%</span>
  214.               </div>
  215.             </div>
  216.             <div class="form-group">
  217.               <label>旋转</label>
  218.               <div style="display:flex;align-items:center;gap:4px;">
  219.                 <input type="range" id="wmRotate" min="-180" max="180" value="-30" style="min-width:80px;" />
  220.                 <span class="opacity-val" id="wmRotateVal">-30°</span>
  221.               </div>
  222.             </div>
  223.             <div class="form-group">
  224.               <label>行</label>
  225.               <input type="number" id="wmRows" value="3" min="1" max="20" style="width:48px;" />
  226.             </div>
  227.             <div class="form-group">
  228.               <label>列</label>
  229.               <input type="number" id="wmCols" value="3" min="1" max="20" style="width:48px;" />
  230.             </div>
  231.           </div>
  232.         </div>



  233.         <div class="preview-section">
  234.           <div class="preview-header">
  235.             <h3>水印预览</h3>
  236.             <span class="preview-name" id="previewName"></span>
  237.           </div>
  238.           <div class="preview-canvas-wrap" id="previewWrap">
  239.             <div class="preview-empty" id="previewEmpty">请上传图片后查看预览</div>
  240.             <canvas id="previewCanvas" style="display:none;"></canvas>
  241.           </div>
  242.         </div>
  243.       </div>
  244.     </div>
  245.   </div>


  246.   <div class="exporting-overlay" id="exportingOverlay">正在导出中</div>

  247.   <script src="./js/jszip.min.js"></script>
  248.   <script>

  249.     let imageFiles = [];
  250.     let selectedIdx = -1;
  251.     let previewTimer = null;
  252.     let moveWatermark = false;
  253.     let individualAdjust = false;
  254.     let isGlobalMode = false;
  255.     let isDragging = false;
  256.     let dragStartX = 0;
  257.     let dragStartY = 0;
  258.     let dragInitialOffsetX = 0;
  259.     let dragInitialOffsetY = 0;


  260.     const singleInput = document.getElementById('singleInput');
  261.     const folderInput = document.getElementById('folderInput');

  262.     singleInput.addEventListener('change', function(e) {
  263.       addFiles(Array.from(e.target.files));
  264.       singleInput.value = '';
  265.     });
  266.     folderInput.addEventListener('change', function(e) {
  267.       addFiles(Array.from(e.target.files));
  268.       folderInput.value = '';
  269.     });

  270.     function addFiles(files) {
  271.       const imgFiles = files.filter(f => f.type.startsWith('image/'));
  272.       if (imgFiles.length === 0) { alert('未检测到图片文件'); return; }
  273.       let loaded = 0;
  274.       imgFiles.forEach(file => {
  275.         const reader = new FileReader();
  276.         reader.onload = function(e) {
  277.           const imgEl = new Image();
  278.           imgEl.onload = function() {
  279.             imageFiles.push({ file, name: file.name, dataUrl: e.target.result, imgEl, offsetX: 0, offsetY: 0 });
  280.             loaded++;
  281.             if (loaded === imgFiles.length) {
  282.               renderThumbnails();

  283.               if (selectedIdx < 0 || selectedIdx >= imageFiles.length) {
  284.                 selectImage(imageFiles.length - imgFiles.length);
  285.               } else {
  286.                 updatePreview();
  287.               }
  288.             }
  289.           };
  290.           imgEl.src = e.target.result;
  291.         };
  292.         reader.readAsDataURL(file);
  293.       });
  294.     }


  295.     function renderThumbnails() {
  296.       const grid = document.getElementById('thumbGrid');
  297.       const countEl = document.getElementById('thumbCount');
  298.       countEl.textContent = '共 ' + imageFiles.length + ' 张';

  299.       if (imageFiles.length === 0) {
  300.         grid.innerHTML = '<div class="thumb-empty">暂无图片,请上传</div>';
  301.         return;
  302.       }

  303.       grid.innerHTML = '';
  304.       imageFiles.forEach((item, idx) => {
  305.         const card = document.createElement('div');
  306.         card.className = 'thumb-card' + (idx === selectedIdx ? ' active' : '');
  307.         card.onclick = function() { selectImage(idx); };

  308.         const img = document.createElement('img');
  309.         img.src = item.dataUrl;
  310.         img.alt = item.name;

  311.         const nameDiv = document.createElement('div');
  312.         nameDiv.className = 'thumb-name';
  313.         nameDiv.title = item.name;
  314.         nameDiv.textContent = item.name;

  315.         const removeBtn = document.createElement('button');
  316.         removeBtn.className = 'thumb-remove';
  317.         removeBtn.textContent = '&#10005;';
  318.         removeBtn.onclick = function(e) { e.stopPropagation(); removeImage(idx); };

  319.         card.appendChild(img);
  320.         card.appendChild(nameDiv);
  321.         card.appendChild(removeBtn);
  322.         grid.appendChild(card);
  323.       });
  324.     }

  325.     function selectImage(idx) {
  326.       if (idx < 0 || idx >= imageFiles.length) return;
  327.       selectedIdx = idx;

  328.       const cards = document.querySelectorAll('.thumb-card');
  329.       cards.forEach((c, i) => c.classList.toggle('active', i === idx));

  330.       updatePreview();
  331.     }

  332.     function removeImage(idx) {
  333.       imageFiles.splice(idx, 1);
  334.       if (imageFiles.length === 0) {
  335.         selectedIdx = -1;
  336.         document.getElementById('previewCanvas').style.display = 'none';
  337.         document.getElementById('previewEmpty').style.display = '';
  338.         document.getElementById('previewName').textContent = '';
  339.       } else if (selectedIdx >= imageFiles.length) {
  340.         selectedIdx = imageFiles.length - 1;
  341.       } else if (selectedIdx === idx) {
  342.         selectedIdx = Math.min(idx, imageFiles.length - 1);
  343.       } else if (selectedIdx > idx) {
  344.         selectedIdx--;
  345.       }
  346.       renderThumbnails();
  347.       if (imageFiles.length > 0) updatePreview();
  348.     }

  349.     function clearAll() {
  350.       imageFiles = [];
  351.       selectedIdx = -1;
  352.       renderThumbnails();
  353.       document.getElementById('previewCanvas').style.display = 'none';
  354.       document.getElementById('previewEmpty').style.display = '';
  355.       document.getElementById('previewName').textContent = '';
  356.     }


  357.     function toggleMove() {
  358.     moveWatermark = document.getElementById('moveWatermarkSwitch').checked;

  359.     isGlobalMode = moveWatermark;

  360.     if (moveWatermark && individualAdjust) {
  361.         individualAdjust = false;
  362.         document.getElementById('individualSwitch').checked = false;
  363.     }
  364.     updateCanvasCursor();
  365.     }
  366.     function toggleIndividual() {
  367.     individualAdjust = document.getElementById('individualSwitch').checked;

  368.     if (individualAdjust && moveWatermark) {
  369.         moveWatermark = false;
  370.         document.getElementById('moveWatermarkSwitch').checked = false;
  371.         isGlobalMode = false; // 切换到单独调整模式
  372.     } else {

  373.         isGlobalMode = moveWatermark;
  374.     }
  375.     updateCanvasCursor();
  376.     updatePreview();
  377.     }
  378.     function updateCanvasCursor() {
  379.       const canvas = document.getElementById('previewCanvas');
  380.       canvas.style.cursor = (moveWatermark || individualAdjust) ? 'move' : '';
  381.     }
  382.     function getImageOffset(item) {

  383.     return { x: item.offsetX || 0, y: item.offsetY || 0 };
  384.     }


  385.     function initCanvasDrag() {
  386.       const canvas = document.getElementById('previewCanvas');
  387.       

  388.       function startDrag(e) {
  389.         if (!moveWatermark && !individualAdjust) return;
  390.         isDragging = true;
  391.         const rect = canvas.getBoundingClientRect();
  392.         const clientX = e.touches ? e.touches[0].clientX : e.clientX;
  393.         const clientY = e.touches ? e.touches[0].clientY : e.clientY;
  394.         dragStartX = clientX - rect.left;
  395.         dragStartY = clientY - rect.top;
  396.         if (individualAdjust) {
  397.           const item = imageFiles[selectedIdx];
  398.           dragInitialOffsetX = item.offsetX || 0;
  399.           dragInitialOffsetY = item.offsetY || 0;
  400.         } else {
  401.           dragInitialOffsetX = globalOffsetX;
  402.           dragInitialOffsetY = globalOffsetY;
  403.         }
  404.         if (e.touches) e.preventDefault();
  405.       }
  406.       

  407.       function doDrag(e) {
  408.     if (!isDragging) return;
  409.     const rect = canvas.getBoundingClientRect();
  410.     const scaleX = canvas.width / rect.width;
  411.     const scaleY = canvas.height / rect.height;
  412.     const clientX = e.touches ? e.touches[0].clientX : e.clientX;
  413.     const clientY = e.touches ? e.touches[0].clientY : e.clientY;
  414.     const currentX = clientX - rect.left;
  415.     const currentY = clientY - rect.top;
  416.     const dx = (currentX - dragStartX) * scaleX;
  417.     const dy = (currentY - dragStartY) * scaleY;


  418.     const newOffsetX = dragInitialOffsetX + dx;
  419.     const newOffsetY = dragInitialOffsetY + dy;


  420.     imageFiles[selectedIdx].offsetX = newOffsetX;
  421.     imageFiles[selectedIdx].offsetY = newOffsetY;


  422.     if (isGlobalMode) {
  423.         imageFiles.forEach((item, idx) => {
  424.             if (idx !== selectedIdx) {
  425.                 item.offsetX = newOffsetX;
  426.                 item.offsetY = newOffsetY;
  427.             }
  428.         });
  429.     }

  430.     updatePreview();
  431.     if (e.touches) e.preventDefault();
  432.     }
  433.       

  434.       function endDrag() {
  435.         isDragging = false;
  436.       }
  437.       

  438.       canvas.addEventListener('mousedown', startDrag);
  439.       canvas.addEventListener('mousemove', doDrag);
  440.       canvas.addEventListener('mouseup', endDrag);
  441.       canvas.addEventListener('mouseleave', endDrag);
  442.       

  443.       canvas.addEventListener('touchstart', startDrag, { passive: false });
  444.       canvas.addEventListener('touchmove', doDrag, { passive: false });
  445.       canvas.addEventListener('touchend', endDrag);
  446.       canvas.addEventListener('touchcancel', endDrag);
  447.     }


  448.     function getWatermarkParams() {
  449.       return {
  450.         text: document.getElementById('wmText').value || '水印',
  451.         font: document.getElementById('wmFont').value,
  452.         size: parseInt(document.getElementById('wmSize').value) || 40,
  453.         color: document.getElementById('wmColor').value,
  454.         opacity: parseInt(document.getElementById('wmOpacity').value) / 100,
  455.         rotate: parseInt(document.getElementById('wmRotate').value) || 0,
  456.         rows: Math.max(1, parseInt(document.getElementById('wmRows').value) || 3),
  457.         cols: Math.max(1, parseInt(document.getElementById('wmCols').value) || 3),
  458.       };
  459.     }

  460.     function drawWatermark(ctx, img, width, height, params, offsetX, offsetY) {
  461.       ctx.clearRect(0, 0, width, height);
  462.       ctx.drawImage(img, 0, 0, width, height);

  463.       const { text, font, size, color, opacity, rotate, rows, cols } = params;
  464.       const scale = Math.min(width, height) / 800;
  465.       const fontSize = Math.round(size * scale);
  466.       const ox = offsetX || 0;
  467.       const oy = offsetY || 0;

  468.       ctx.save();
  469.       ctx.globalAlpha = opacity;
  470.       ctx.fillStyle = color;
  471.       ctx.font = fontSize + 'px "' + font + '"';
  472.       ctx.textAlign = 'center';
  473.       ctx.textBaseline = 'middle';

  474.       const cellW = width / cols;
  475.       const cellH = height / rows;
  476.       for (let r = 0; r < rows; r++) {
  477.         for (let c = 0; c < cols; c++) {
  478.           const cx = cellW * c + cellW / 2 + ox;
  479.           const cy = cellH * r + cellH / 2 + oy;
  480.           ctx.save();
  481.           ctx.translate(cx, cy);
  482.           ctx.rotate(rotate * Math.PI / 180);
  483.           ctx.fillText(text, 0, 0);
  484.           ctx.restore();
  485.         }
  486.       }
  487.       ctx.restore();
  488.     }


  489.     function updatePreview() {
  490.       if (selectedIdx < 0 || selectedIdx >= imageFiles.length) return;
  491.       const item = imageFiles[selectedIdx];
  492.       const canvas = document.getElementById('previewCanvas');
  493.       const empty = document.getElementById('previewEmpty');
  494.       const nameEl = document.getElementById('previewName');

  495.       empty.style.display = 'none';
  496.       canvas.style.display = '';
  497.       nameEl.textContent = item.name;
  498.       nameEl.title = item.name;

  499.       canvas.width = item.imgEl.naturalWidth;
  500.       canvas.height = item.imgEl.naturalHeight;
  501.       const ctx = canvas.getContext('2d');
  502.       const offset = getImageOffset(item);
  503.       drawWatermark(ctx, item.imgEl, canvas.width, canvas.height, getWatermarkParams(), offset.x, offset.y);
  504.     }


  505.     function schedulePreview() {
  506.       clearTimeout(previewTimer);
  507.       previewTimer = setTimeout(updatePreview, 80);
  508.     }


  509.     ['wmText', 'wmFont', 'wmSize', 'wmColor', 'wmOpacity', 'wmRotate', 'wmRows', 'wmCols'].forEach(function(id) {
  510.       const el = document.getElementById(id);
  511.       el.addEventListener('input', function() {
  512.         if (id === 'wmOpacity') document.getElementById('wmOpacityVal').textContent = el.value + '%';
  513.         if (id === 'wmRotate') document.getElementById('wmRotateVal').textContent = el.value + '°';
  514.         schedulePreview();
  515.       });
  516.       el.addEventListener('change', function() {
  517.         schedulePreview();
  518.       });
  519.     });


  520.     function getExportFileName(originalName) {
  521.       const nameNoExt = originalName.replace(/\.[^.]+$/, '');
  522.       const now = new Date();
  523.       const pad = n => n < 10 ? '0' + n : n;
  524.       const ts = '' + now.getFullYear() + pad(now.getMonth() + 1) + pad(now.getDate()) +
  525.         '_' + pad(now.getHours()) + pad(now.getMinutes());
  526.       return nameNoExt + '_水印_' + ts + '.png';
  527.     }

  528.     function renderWatermarkedImage(item, callback) {
  529.       const canvas = document.createElement('canvas');
  530.       canvas.width = item.imgEl.naturalWidth;
  531.       canvas.height = item.imgEl.naturalHeight;
  532.       const ctx = canvas.getContext('2d');
  533.       const offset = getImageOffset(item);
  534.       drawWatermark(ctx, item.imgEl, canvas.width, canvas.height, getWatermarkParams(), offset.x, offset.y);
  535.       canvas.toBlob(function(blob) { callback(blob); }, 'image/png');
  536.     }

  537.     function exportAll() {
  538.       if (imageFiles.length === 0) { alert('请先上传图片'); return; }
  539.       const overlay = document.getElementById('exportingOverlay');
  540.       overlay.textContent = '正在导出中';
  541.       overlay.style.display = '';
  542.       let idx = 0;
  543.       function processNext() {
  544.         if (idx >= imageFiles.length) { overlay.style.display = 'none'; return; }
  545.         renderWatermarkedImage(imageFiles[idx], function(blob) {
  546.           const url = URL.createObjectURL(blob);
  547.           const a = document.createElement('a');
  548.           a.href = url;
  549.           a.download = getExportFileName(imageFiles[idx].name);
  550.           document.body.appendChild(a);
  551.           a.click();
  552.           document.body.removeChild(a);
  553.           setTimeout(function() { URL.revokeObjectURL(url); }, 100);
  554.           idx++;
  555.           setTimeout(processNext, 200);
  556.         });
  557.       }
  558.       processNext();
  559.     }

  560.     function exportZip() {
  561.       if (imageFiles.length === 0) { alert('请先上传图片'); return; }
  562.       const overlay = document.getElementById('exportingOverlay');
  563.       overlay.textContent = '正在压缩中';
  564.       overlay.style.display = '';
  565.       const zip = new JSZip();
  566.       let pending = imageFiles.length;
  567.       const results = [];
  568.       imageFiles.forEach(function(item, idx) {
  569.         renderWatermarkedImage(item, function(blob) {
  570.           results[idx] = { name: getExportFileName(item.name), blob: blob };
  571.           pending--;
  572.           if (pending === 0) {
  573.             results.forEach(function(r) { zip.file(r.name, r.blob); });
  574.             const now = new Date();
  575.             const pad = n => n < 10 ? '0' + n : n;
  576.             const ts = '' + now.getFullYear() + pad(now.getMonth() + 1) + pad(now.getDate()) +
  577.               '_' + pad(now.getHours()) + pad(now.getMinutes());
  578.             zip.generateAsync({ type: 'blob' }).then(function(zipBlob) {
  579.               const url = URL.createObjectURL(zipBlob);
  580.               const a = document.createElement('a');
  581.               a.href = url;
  582.               a.download = '水印图片_' + ts + '.zip';
  583.               document.body.appendChild(a);
  584.               a.click();
  585.               document.body.removeChild(a);
  586.               setTimeout(function() { URL.revokeObjectURL(url); }, 100);
  587.               overlay.style.display = 'none';
  588.             });
  589.           }
  590.         });
  591.       });
  592.     }


  593.     document.addEventListener('DOMContentLoaded', function() {
  594.       initCanvasDrag();
  595.     });
  596.   </script>
  597. </body>
  598. </html>[/hide]
复制代码




ScreenShot_2026-07-23_122152_322.png
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ:413752269(客服)| Email:liranqing2008@Gmail.com|手机版|小黑屋|版权声明|侵权投诉|免责声明|捐助支持|免费流量卡|理想阁 ( 晋ICP备2026000046号-1 )

GMT+8, 2026-7-29 06:33 , Processed in 0.051440 second(s), 24 queries .

Powered by 理想阁 社区

© 2001-2026 理想阁论坛社区.

快速回复 返回顶部 返回列表