吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 585|回复: 30
上一主题 下一主题
收起左侧

[其他原创] 【HTML+CSS+JS】海东青便利小工具之A3卷子转A4分割+打印

  [复制链接]
跳转到指定楼层
楼主
海东青1013 发表于 2026-5-6 09:34 回帖奖励
本帖最后由 海东青1013 于 2026-5-6 11:13 编辑

创作原由:孩子上小学,老师发送A3卷子图片需要转换为A4图片,孩子不会,遂创作这个三件套。
下载链接在最下面,解压时点击右键→解压到桌面即可使用,代码已全部上传,感谢版主指导,谢谢。。
解压密码:52pojie
第一段:
这里主要是主页面的设定,为了方便孩子选择卷子和编辑卷子。
[JavaScript] 纯文本查看 复制代码
    <div class="row">
        <button class="btn-select" id="selFolderBtn">&#128193; 选择保存A4卷子的位置(请点击左侧桌面文字→点击上传→上传即可)</button>
        <label><input type="checkbox" id="saveDefault"> 设为默认文件夹</label>
        <div class="path-text" id="showPath">未选择文件夹</div>
    </div>
    <!-- 全部按钮排成一行,一页显示 -->
    <div class="row">
        <button class="btn-rotate-left" id="rotateLeft">向左&#8635;转图</button>
        <button class="btn-rotate-right" id="rotateRight">向右&#8634;转图</button>
        <button class="btn-rotate-180" id="rotate180">180°旋转&#8597;图</button>
        <button class="btn-export" id="packBtn">打包+下载A4卷子</button>
        <button class="btn-print" id="printBtn">批量打印A4卷子(打印机自己选择)</button>
    </div>
    <div id="preview"></div>
</div>
<input type="file" id="folderInput" style="display:none" webkitdirectory directory>
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js"></script>


第二段:
把你上传的 A3 试卷图片,从中间切成左右两半 A4 大小,同时支持各方向的旋转,然后显示预览、保存、打印。
[JavaScript] 纯文本查看 复制代码
async function processImages(){    preview.innerHTML = "";
    cutList = [];
    for(let item of rawImages){
        let {img, name} = item;
        let cvs = document.createElement('canvas');
        let ctx = cvs.getContext('2d');
        cvs.width = img.width;
        cvs.height = img.height;
        ctx.drawImage(img, 0, 0);
        if(currentRotate !== 0){
            cvs = rotateCanvas(cvs, currentRotate);
        }
        let w = cvs.width;
        let h = cvs.height;
        let half = Math.floor(w / 2);
        let leftCv = document.createElement('canvas');
        leftCv.width = half;
        leftCv.height = h;
        leftCv.getContext('2d').drawImage(cvs,0,0,half,h,0,0,half,h);
        let rightCv = document.createElement('canvas');
        rightCv.width = w-half;
        rightCv.height = h;
        rightCv.getContext('2d').drawImage(cvs,half,0,w-half,h,0,0,w-half,h);
        cutList.push({
            name:name,
            left:leftCv.toDataURL("image/jpeg",0.95),
            right:rightCv.toDataURL("image/jpeg",0.95)
        });
        let div = document.createElement('div');
        div.className = "item";
        div.innerHTML = `
            <p style="font-size:13px;">${name}</p>
            <img src="${leftCv.toDataURL()}">
            <img src="${rightCv.toDataURL()}">
        `;
        preview.appendChild(div);


第三段:把切割好的左半、右半的卷子图片,打包成一个 ZIP 压缩包下载到电脑中。
[JavaScript] 纯文本查看 复制代码
 if(cutList.length===0) return alert("请选择图片~");
    if(!selectFolderName) return alert("请选择保存文件夹");
    let d = new Date();
    let dateStr = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
    let zip = new JSZip();
    let folder = zip.folder(`A3转A4分割-${dateStr}`);
    for(let item of cutList){
        let base = item.name.replace(/\.\w+$/,'');
        folder.file(`${base}_左半A4.jpg`, item.left.split(',')[1], {base64:true});
        folder.file(`${base}_右半A4.jpg`, item.right.split(',')[1], {base64:true});
    }
    saveAs(await zip.generateAsync({type:"blob"}), `A3转A4分割-${dateStr}.zip`);
    alert("下载完成~请解压到你选择的文件夹即可");

1.png (73.81 KB, 下载次数: 0)

整体页面

整体页面

2.png (341.65 KB, 下载次数: 0)

裁剪页面A3转A4

裁剪页面A3转A4

3.png (515.12 KB, 下载次数: 0)

打印预览页面

打印预览页面

海东青的便利小工具之A3卷子转A4分割+打印.zip

3.3 KB, 下载次数: 127, 下载积分: 吾爱币 -1 CB

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
tydx0259 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
bybyby11 发表于 2026-5-6 16:03
本帖最后由 bybyby11 于 2026-5-6 16:06 编辑

[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>A3 试卷拆分为 A4 工具</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background: linear-gradient(#ffffff 50%, rgba(255,255,255,0) 0) 0 0,
                  radial-gradient(circle closest-side, #FFFFFF 53%, rgba(255,255,255,0) 0) 0 0,
                  radial-gradient(circle closest-side, #FFFFFF 50%, rgba(255,255,255,0) 0) 55px 0 #48B;
      background-size: 110px 200px;
      background-repeat: repeat-x;
      min-height: 100vh;
      color: #333;
      line-height: 1.6;
    }

    .container {
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
    }

    .header {
      text-align: center;
      margin-bottom: 30px;
      color: #111;
    }

    .header h1 {
      font-size: 2.5rem;
      margin-bottom: 10px;
      text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
    }

    .header p {
      font-size: 1.1rem;
      opacity: 0.9;
    }

    .main-content {
      background: white;
      border-radius: 20px;
      box-shadow: 0 20px 40px rgba(0,0,0,0.1);
      overflow: hidden;
      margin-bottom: 20px;
    }

    .top-panel {
      display: flex;
      flex-wrap: wrap;
      gap: 20px;
      align-items: stretch;
      padding: 30px;
      background: linear-gradient(135deg, rgba(240,147,251,0.12) 0%, rgba(245,87,108,0.12) 100%);
      border-bottom: 1px solid rgba(0,0,0,0.05);
    }

    .upload-section {
      flex: 1 1 40%;
      min-width: 260px;
      max-width: 360px;
      padding: 20px;
      background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
      color: white;
      text-align: center;
      border-radius: 16px;
      box-shadow: 0 15px 25px rgba(245,87,108,0.25);
      display: flex;
      flex-direction: column;
      justify-content: center;
    }

    .upload-area {
      border: 3px dashed rgba(255,255,255,0.5);
      border-radius: 15px;
      padding: 28px 16px;
      margin: 10px 0 0;
      transition: all 0.3s ease;
      cursor: pointer;
      background: rgba(255,255,255,0.05);
    }

    .upload-area:hover {
      border-color: rgba(255,255,255,0.8);
      background: rgba(255,255,255,0.1);
    }

    .upload-area.dragover {
      border-color: #fff;
      background: rgba(255,255,255,0.2);
      transform: scale(1.02);
    }

    .upload-icon {
      font-size: 3rem;
      margin-bottom: 15px;
    }

    .file-input {
      display: none;
    }

    .upload-text {
      font-size: 1.2rem;
      margin-bottom: 10px;
    }

    .upload-hint {
      font-size: 0.9rem;
      opacity: 0.8;
    }

    .instructions {
      flex: 1 1 50%;
      min-width: 280px;
      padding: 20px 24px;
      background: rgba(255,255,255,0.9);
      border: 1px solid rgba(148, 163, 184, 0.3);
      border-radius: 16px;
      box-shadow: inset 0 1px 0 rgba(255,255,255,0.6);
    }

    .instructions h3 {
      color: #495057;
      margin-bottom: 15px;
      font-size: 1.3rem;
    }

    .instructions ol {
      padding-left: 20px;
    }

    .instructions li {
      margin-bottom: 8px;
      color: #6c757d;
    }

    .canvas-container {
      padding: 20px;
      max-height: 70vh;
      overflow-y: auto;
      background: #f8f9fa;
    }

    .canvas-page {
      position: relative;
      margin-bottom: 20px;
      border-radius: 10px;
      overflow: hidden;
      box-shadow: 0 10px 30px rgba(0,0,0,0.1);
      transition: all 0.3s ease;
      background: white;
    }

    .canvas-page:hover {
      transform: translateY(-2px);
      box-shadow: 0 15px 40px rgba(0,0,0,0.15);
    }

    .canvas-page.selected {
      outline: 4px solid #007bff;
      transform: translateY(-3px);
      box-shadow: 0 20px 50px rgba(0,123,255,0.3);
    }

    .page-header {
      background: #007bff;
      color: white;
      padding: 10px 15px;
      font-weight: 500;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .page-number {
      font-size: 0.9rem;
    }

    .cut-lines-count {
      background: rgba(255,255,255,0.2);
      padding: 2px 8px;
      border-radius: 12px;
      font-size: 0.8rem;
    }

    .page-metrics {
      display: flex;
      gap: 8px;
      align-items: center;
    }

    .mask-count {
      background: rgba(79, 70, 229, 0.16);
      padding: 2px 8px;
      border-radius: 12px;
      font-size: 0.8rem;
      color: #312e81;
    }

    canvas {
      width: 100%;
      height: auto;
      display: block;
      background: #fff;
    }

    .canvas-wrapper {
      position: relative;
    }

    .mask-layer {
      position: absolute;
      inset: 0;
      pointer-events: none;
    }

    .a4-mask {
      position: absolute;
      border: 2px solid rgba(79,70,229,0.7);
      background: rgba(79,70,229,0.12);
      box-shadow: 0 0 0 1px rgba(255,255,255,0.6), 0 12px 25px rgba(79,70,229,0.25);
      border-radius: 10px;
      cursor: move;
      pointer-events: auto;
      backdrop-filter: blur(1px);
      transition: box-shadow 0.2s ease, transform 0.2s ease;
      z-index: 15;
    }

    .a4-mask:hover {
      transform: translateY(-1px);
    }

    .a4-mask .mask-label {
      position: absolute;
      top: 8px;
      left: 10px;
      background: rgba(79,70,229,0.9);
      color: #fff;
      padding: 2px 8px;
      border-radius: 12px;
      font-size: 12px;
      font-weight: 600;
      pointer-events: none;
    }

    .a4-mask .mask-remove {
      position: absolute;
      top: 8px;
      right: 8px;
      width: 22px;
      height: 22px;
      border-radius: 50%;
      background: rgba(220,53,69,0.9);
      color: #fff;
      border: none;
      font-size: 14px;
      line-height: 1;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      box-shadow: 0 4px 12px rgba(220,53,69,0.35);
    }

    .a4-mask .mask-remove:hover {
      background: rgba(220,53,69,1);
    }

    .a4-mask .mask-resize-handle {
      position: absolute;
      width: 18px;
      height: 18px;
      border-radius: 50%;
      border: 3px solid #fff;
      background: #4f46e5;
      bottom: -10px;
      right: -10px;
      cursor: nwse-resize;
      box-shadow: 0 6px 16px rgba(0,0,0,0.25);
    }

    .cut-line {
      position: absolute;
      top: 0;
      width: 4px;
      height: 100%;
      background: linear-gradient(to bottom, #ff4757, #ff3742);
      cursor: ew-resize;
      z-index: 10;
      border-radius: 2px;
      box-shadow: 0 0 10px rgba(255,71,87,0.5);
      transition: all 0.2s ease;
    }

    .cut-line:hover {
      width: 6px;
      box-shadow: 0 0 15px rgba(255,71,87,0.7);
    }

    .cut-line::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 20px;
      height: 20px;
      background: #ff4757;
      border-radius: 50%;
      border: 3px solid white;
      box-shadow: 0 2px 8px rgba(0,0,0,0.3);
    }

    .controls {
      position: fixed;
      bottom: 30px;
      right: 30px;
      display: flex;
      flex-direction: column;
      gap: 15px;
      z-index: 1000;
    }

    .control-btn {
      background: linear-gradient(135deg, #007bff, #0056b3);
      color: white;
      border: none;
      padding: 15px 20px;
      border-radius: 50px;
      font-size: 14px;
      font-weight: 500;
      cursor: pointer;
      box-shadow: 0 8px 25px rgba(0,123,255,0.3);
      transition: all 0.3s ease;
      min-width: 200px;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
    }

    .control-btn:hover:not(:disabled) {
      transform: translateY(-2px);
      box-shadow: 0 12px 35px rgba(0,123,255,0.4);
      background: linear-gradient(135deg, #0056b3, #004085);
    }

    .control-btn:active {
      transform: translateY(0);
    }

    .control-btn:disabled {
      background: #6c757d;
      cursor: not-allowed;
      transform: none;
      box-shadow: 0 4px 15px rgba(108,117,125,0.3);
    }

    .control-btn.export {
      background: linear-gradient(135deg, #28a745, #1e7e34);
      box-shadow: 0 8px 25px rgba(40,167,69,0.3);
    }

    .control-btn.export:hover:not(:disabled) {
      box-shadow: 0 12px 35px rgba(40,167,69,0.4);
      background: linear-gradient(135deg, #1e7e34, #155724);
    }

    .loading-overlay {
      position: fixed;
      inset: 0;
      background: rgba(0,0,0,0.8);
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      font-size: 1.2em;
      color: white;
      z-index: 2000;
      visibility: hidden;
      opacity: 0;
      transition: all 0.3s ease;
    }

    .loading-overlay.visible {
      visibility: visible;
      opacity: 1;
    }

    .loading-spinner {
      width: 50px;
      height: 50px;
      border: 4px solid rgba(255,255,255,0.3);
      border-top: 4px solid white;
      border-radius: 50%;
      animation: spin 1s linear infinite;
      margin-bottom: 20px;
    }

    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }

    .progress-bar {
      width: 300px;
      height: 6px;
      background: rgba(255,255,255,0.3);
      border-radius: 3px;
      overflow: hidden;
      margin-top: 15px;
    }

    .progress-fill {
      height: 100%;
      background: linear-gradient(90deg, #007bff, #28a745);
      width: 0%;
      transition: width 0.3s ease;
    }

    .status-message {
      margin-top: 10px;
      font-size: 0.9rem;
      opacity: 0.9;
    }

    .empty-state {
      text-align: center;
      padding: 60px 20px;
      color: #6c757d;
    }

    .empty-state-icon {
      font-size: 4rem;
      margin-bottom: 20px;
      opacity: 0.5;
    }

    .error-message {
      background: #f8d7da;
      color: #721c24;
      padding: 15px;
      border-radius: 8px;
      margin: 20px 0;
      border-left: 4px solid #dc3545;
    }

    .success-message {
      background: #d4edda;
      color: #155724;
      padding: 15px;
      border-radius: 8px;
      margin: 20px 0;
      border-left: 4px solid #28a745;
    }

    @media (max-width: 768px) {
      .container {
        padding: 10px;
      }
      
      .top-panel {
        flex-direction: column;
        padding: 20px;
      }

      .upload-section,
      .instructions {
        max-width: 100%;
      }

      .header h1 {
        font-size: 2rem;
      }
      
      .controls {
        bottom: 20px;
        right: 20px;
        left: 20px;
      }
      
      .control-btn {
        min-width: auto;
        padding: 12px 16px;
        font-size: 13px;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <h1>&#128196; A3试卷拆分为A4工具</h1>
      <p>智能裁剪,轻松转换,专业打印</p>
    </div>

    <div class="main-content">
      <div class="top-panel">
        <div class="upload-section">
          <div class="upload-area" id="uploadArea">
            <div class="upload-icon">&#128228;</div>
            <div class="upload-text">拖拽或点击上传文件</div>
            <div class="upload-hint">支持 PDF、PNG、JPEG、BMP 格式</div>
            <input type="file" id="fileInput" class="file-input" accept=".pdf,image/png,image/jpeg,image/jpg,image/bmp" />
          </div>
        </div>
        <div class="instructions">
          <h3>&#128736; 操作指南</h3>
          <ol>
          <li><strong>上传文件:</strong>支持导入 PDF、PNG、JPEG、BMP 等格式的 A3 试卷或图片。</li>
          <li><strong>选择页面:</strong>点击页面预览或使用“全选并添加裁切线”快速定位需要处理的页面。</li>
          <li><strong>添加裁切线:</strong>在目标页面上添加并拖动竖向裁切线,划分需要拆分的区域。</li>
          <li><strong>添加遮罩:</strong>点击“添加 A4 遮罩”生成固定比例遮罩框,可拖拽或拖动圆点调整位置与尺寸,可以多次重叠添加。</li>
          <li><strong>导出分割:</strong>使用“导出裁切线PDF”输出按裁切线拆分的 A4 页面。</li>
          <li><strong>导出拼接:</strong>使用“导出遮罩PDF”按遮罩顺序导出多页 A4 拼接版面。</li>
        </ol>
        </div>
      </div>

      <div class="canvas-container" id="canvasContainer">
        <div class="empty-state">
          <div class="empty-state-icon">&#128196;</div>
          <h3>请上传文件开始使用</h3>
          <p>支持 PDF 与常见图片格式</p>
        </div>
      </div>
    </div>

  <div class="loading-overlay" id="loadingOverlay">
    <div class="loading-spinner"></div>
    <div id="loadingText">正在处理,请稍候...</div>
    <div class="progress-bar">
      <div class="progress-fill" id="progressFill"></div>
    </div>
    <div class="status-message" id="statusMessage"></div>
  </div>

  <div class="controls">
    <button class="control-btn" id="selectAllAddCutLines">
      &#127760; 全部选中并添加裁切线
    </button>
    <button class="control-btn" id="addCutLine">
      &#10133; 添加裁切线
    </button>
    <button class="control-btn" id="removeCutLines">
      &#128465;&#65039; 删除本页裁切线
    </button>
    <button class="control-btn" id="addA4Mask">
      &#128208; 添加A4遮罩
    </button>
    <button class="control-btn export" id="exportA4MaskPDF">
      &#129513; 导出遮罩PDF
    </button>
    <button class="control-btn export" id="exportPDF">
      &#128640; 导出裁切线PDF
    </button>
  </div>

  <script src="https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.min.js?t=YUJCCh"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js?t=YUJCCh"></script>
  
  <script>
    pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.worker.min.js';

    // DOM 元素
    const fileInput = document.getElementById('fileInput');
    const uploadArea = document.getElementById('uploadArea');
    const container = document.getElementById('canvasContainer');
    const selectAllBtn = document.getElementById('selectAllAddCutLines');
    const addCutLineBtn = document.getElementById('addCutLine');
    const removeCutLinesBtn = document.getElementById('removeCutLines');
    const addMaskBtn = document.getElementById('addA4Mask');
    const exportMaskBtn = document.getElementById('exportA4MaskPDF');
    const exportBtn = document.getElementById('exportPDF');
    const loadingOverlay = document.getElementById('loadingOverlay');
    const loadingText = document.getElementById('loadingText');
    const statusMessage = document.getElementById('statusMessage');
    const progressFill = document.getElementById('progressFill');

    const supportedImageTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/bmp'];
    const A4_RATIO = 297 / 210;
    const MIN_MASK_WIDTH_RATIO = 0.1;
    const DEFAULT_MASK_WIDTH_RATIO = 0.7;
    const MASK_COLORS = ['#28a745', '#1f7aec', '#ff9f1c', '#d6336c', '#17a2b8', '#6f42c1', '#fb5607', '#673ab7'];

    // 状态变量
    let pages = [];
    let currentPageIndex = null;
    let isDragging = false;
    let activeLine = null;
    let activeMaskAction = null;
    let maskColorIndex = 0;

    // 工具函数
    function showLoading(message = "正在处理,请稍候...", progress = 0) {
      loadingText.textContent = message;
      progressFill.style.width = `${progress}%`;
      loadingOverlay.classList.add('visible');
      setButtonsDisabled(true);
    }

    function hideLoading() {
      loadingOverlay.classList.remove('visible');
      setButtonsDisabled(false);
    }

    function setButtonsDisabled(disabled) {
      [addCutLineBtn, removeCutLinesBtn, addMaskBtn, exportMaskBtn, exportBtn, selectAllBtn, fileInput].forEach(btn => {
        if (btn) btn.disabled = disabled;
      });
    }

    function updateProgress(progress, message) {
      progressFill.style.width = `${progress}%`;
      if (message) statusMessage.textContent = message;
    }

    function showMessage(message, type = 'success') {
      const messageDiv = document.createElement('div');
      messageDiv.className = type === 'error' ? 'error-message' : 'success-message';
      messageDiv.textContent = message;
      container.appendChild(messageDiv);
      setTimeout(() => messageDiv.remove(), 5000);
    }

    // 页面创建函数
    function createCanvasPage(width, height, pageIndex) {
      const wrapper = document.createElement('div');
      wrapper.className = 'canvas-page';
      
      const header = document.createElement('div');
      header.className = 'page-header';
      header.innerHTML = `
        <span class="page-number">第 ${pageIndex + 1} 页</span>
        <div class="page-metrics">
          <span class="cut-lines-count">0 条裁切线</span>
          <span class="mask-count">0 个A4遮罩</span>
        </div>
      `;
      
      const canvasWrapper = document.createElement('div');
      canvasWrapper.className = 'canvas-wrapper';
      const canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
      const maskLayer = document.createElement('div');
      maskLayer.className = 'mask-layer';
      
      canvasWrapper.appendChild(canvas);
      canvasWrapper.appendChild(maskLayer);
      
      wrapper.appendChild(header);
      wrapper.appendChild(canvasWrapper);
      container.appendChild(wrapper);

      const pageData = {
        wrapper,
        canvasWrapper,
        canvas,
        ctx: canvas.getContext('2d'),
        originalImage: null,
        cutLines: [],
        masks: [],
        maskLayer,
        pageIndex
      };

      wrapper.addEventListener('click', (e) => {
        if (e.target.classList.contains('cut-line') || e.target.closest('.a4-mask')) return;
        selectPage(pageData);
      });

      updatePageMeta(pageData);

      return pageData;
    }

    function selectPage(pageData) {
      pages.forEach((p, idx) => {
        p.wrapper.classList.remove('selected');
        if (p === pageData) currentPageIndex = idx;
      });
      pageData.wrapper.classList.add('selected');
      updatePageMeta(pageData);
    }

    function updatePageMeta(page) {
      const cutCountElement = page.wrapper.querySelector('.cut-lines-count');
      if (cutCountElement) {
        const cutCount = Array.isArray(page.cutLines) ? page.cutLines.length : 0;
        cutCountElement.textContent = `${cutCount} 条裁切线`;
      }
      const maskCountElement = page.wrapper.querySelector('.mask-count');
      if (maskCountElement) {
        const maskCount = Array.isArray(page.masks) ? page.masks.length : 0;
        maskCountElement.textContent = `${maskCount} 个A4遮罩`;
      }
    }

    // 裁切线管理
    function addCutLineToPage(page, initialRatio = 0.5) {
      const line = document.createElement('div');
      line.className = 'cut-line';
      const cutData = { el: line, ratio: initialRatio };

      const updateLinePosition = () => {
        if (!line.parentNode) {
          window.removeEventListener('resize', updateLinePosition);
          return;
        }
        const wrapperRect = page.wrapper.getBoundingClientRect();
        if (cutData && typeof cutData.ratio === 'number' && wrapperRect.width > 0) {
          line.style.left = `${cutData.ratio * wrapperRect.width}px`;
        }
      };

      line.addEventListener('mousedown', (e) => {
        isDragging = true;
        activeLine = { line, page, cutData };
        e.stopPropagation();
      });

      page.wrapper.appendChild(line);
      page.cutLines.push(cutData);
      cutData.resizeListener = updateLinePosition;
      updatePageMeta(page);

      requestAnimationFrame(updateLinePosition);
      window.addEventListener('resize', updateLinePosition);
    }

    function removeCutLinesFromPage(page) {
      page.cutLines.forEach(cut => {
        cut.el.remove();
        if (cut.resizeListener) {
          window.removeEventListener('resize', cut.resizeListener);
        }
      });
      page.cutLines = [];
      updatePageMeta(page);
    }

    // 遮罩功能
    function getPageDisplaySize(page) {
      if (!page || !page.canvas) {
        return { width: 0, height: 0 };
      }
      const width = page.canvas.clientWidth || page.canvas.width || 0;
      const height = page.canvas.clientHeight || page.canvas.height || 0;
      return { width, height };
    }

    function getMaxMaskWidthRatio(page) {
      const { width, height } = getPageDisplaySize(page);
      if (!width || !height) return 1;
      const maxWidthPx = Math.min(width, height / A4_RATIO);
      return maxWidthPx > 0 ? maxWidthPx / width : 1;
    }

    function hexToRgba(hex, alpha = 1) {
      const normalized = hex.replace('#', '');
      const parseHex = normalized.length === 3
        ? normalized.split('').map(ch => ch + ch).join('')
        : normalized.padEnd(6, '0');
      const r = parseInt(parseHex.slice(0, 2), 16);
      const g = parseInt(parseHex.slice(2, 4), 16);
      const b = parseInt(parseHex.slice(4, 6), 16);
      return `rgba(${r}, ${g}, ${b}, ${alpha})`;
    }

    function clampMaskPosition(page, maskData) {
      if (!maskData) return;
      const maxX = Math.max(0, 1 - maskData.widthRatio);
      const maxY = Math.max(0, 1 - maskData.heightRatio);
      maskData.xRatio = Math.max(0, Math.min(maskData.xRatio, maxX));
      maskData.yRatio = Math.max(0, Math.min(maskData.yRatio, maxY));
    }

    function addA4MaskToPage(page) {
      if (!page || !page.maskLayer) return;

      selectPage(page);

      const { width, height } = getPageDisplaySize(page);
      if (!width || !height) {
        requestAnimationFrame(() => addA4MaskToPage(page));
        return;
      }

      const mask = document.createElement('div');
      mask.className = 'a4-mask';
      mask.innerHTML = `
        <div class="mask-label">A4</div>
        <button type="button" class="mask-remove" aria-label="移除遮罩">×</button>
        <div class="mask-resize-handle"></div>
      `;

      const color = MASK_COLORS[maskColorIndex % MASK_COLORS.length];
      maskColorIndex += 1;
      const baseShadow = `0 0 0 1px rgba(255,255,255,0.6), 0 12px 25px ${hexToRgba(color, 0.35)}`;
      const hoverShadow = `0 0 0 2px ${hexToRgba(color, 0.6)}, 0 16px 30px ${hexToRgba(color, 0.4)}`;

      mask.style.borderColor = color;
      mask.style.backgroundColor = hexToRgba(color, 0.18);
      mask.style.boxShadow = baseShadow;

      const labelEl = mask.querySelector('.mask-label');
      if (labelEl) labelEl.style.background = color;

      const resizeHandle = mask.querySelector('.mask-resize-handle');
      if (resizeHandle) {
        resizeHandle.style.background = color;
        resizeHandle.style.boxShadow = `0 6px 16px ${hexToRgba(color, 0.35)}`;
      }

      mask.addEventListener('mouseenter', () => {
        mask.style.boxShadow = hoverShadow;
      });

      mask.addEventListener('mouseleave', () => {
        mask.style.boxShadow = baseShadow;
      });

      const maxWidthRatio = getMaxMaskWidthRatio(page);
      let widthRatio = maxWidthRatio * DEFAULT_MASK_WIDTH_RATIO;
      if (widthRatio < MIN_MASK_WIDTH_RATIO) {
        widthRatio = Math.min(maxWidthRatio, MIN_MASK_WIDTH_RATIO);
      }
      if (!widthRatio || widthRatio <= 0) {
        widthRatio = Math.min(maxWidthRatio, 0.5);
      }
      let heightRatio = (widthRatio * width * A4_RATIO) / height;
      if (heightRatio > 1) {
        heightRatio = 1;
        widthRatio = Math.min(widthRatio, getMaxMaskWidthRatio(page));
      }

      const maskData = {
        el: mask,
        widthRatio,
        heightRatio,
        xRatio: Math.max(0, (1 - widthRatio) / 2),
        yRatio: Math.max(0, (1 - heightRatio) / 2),
        color,
        baseShadow,
        hoverShadow
      };

      maskData.updatePosition = () => {
        const size = getPageDisplaySize(page);
        if (!size.width || !size.height) return;
        mask.style.width = `${maskData.widthRatio * size.width}px`;
        mask.style.height = `${maskData.heightRatio * size.height}px`;
        mask.style.left = `${maskData.xRatio * size.width}px`;
        mask.style.top = `${maskData.yRatio * size.height}px`;
      };

      maskData.resizeListener = () => maskData.updatePosition();
      window.addEventListener('resize', maskData.resizeListener);

      const removeBtn = mask.querySelector('.mask-remove');
      if (removeBtn) {
        removeBtn.addEventListener('click', (event) => {
          event.preventDefault();
          event.stopPropagation();
          removeMaskFromPage(page, maskData);
        });
      }

      mask.addEventListener('mousedown', (event) => {
        if (event.target.closest('.mask-remove') || event.target.closest('.mask-resize-handle')) return;
        event.preventDefault();
        event.stopPropagation();
        selectPage(page);
        startMaskMove(event, page, maskData);
      });

      if (resizeHandle) {
        resizeHandle.addEventListener('mousedown', (event) => {
          event.preventDefault();
          event.stopPropagation();
          selectPage(page);
          startMaskResize(event, page, maskData);
        });
      }

      page.maskLayer.appendChild(mask);
      if (!Array.isArray(page.masks)) page.masks = [];
      page.masks.push(maskData);
      clampMaskPosition(page, maskData);
      requestAnimationFrame(() => maskData.updatePosition());
      updatePageMeta(page);
    }

    function removeMaskFromPage(page, maskData) {
      if (!page || !maskData) return;
      if (maskData.el && maskData.el.parentNode) {
        maskData.el.parentNode.removeChild(maskData.el);
      }
      if (maskData.resizeListener) {
        window.removeEventListener('resize', maskData.resizeListener);
      }
      if (Array.isArray(page.masks)) {
        page.masks = page.masks.filter(m => m !== maskData);
      }
      if (activeMaskAction && activeMaskAction.maskData === maskData) {
        activeMaskAction = null;
        document.body.style.userSelect = '';
      }
      updatePageMeta(page);
    }

    function startMaskMove(event, page, maskData) {
      const { width, height } = getPageDisplaySize(page);
      if (!width || !height) return;
      activeMaskAction = {
        type: 'move',
        page,
        maskData,
        startX: event.clientX,
        startY: event.clientY,
        initialXRatio: maskData.xRatio,
        initialYRatio: maskData.yRatio
      };
      if (maskData && maskData.hoverShadow && maskData.el) {
        maskData.el.style.boxShadow = maskData.hoverShadow;
      }
      document.body.style.userSelect = 'none';
    }

    function startMaskResize(event, page, maskData) {
      const { width, height } = getPageDisplaySize(page);
      if (!width || !height) return;
      activeMaskAction = {
        type: 'resize',
        page,
        maskData,
        startX: event.clientX,
        startY: event.clientY,
        initialWidthRatio: maskData.widthRatio,
        initialHeightRatio: maskData.heightRatio,
        initialXRatio: maskData.xRatio,
        initialYRatio: maskData.yRatio
      };
      if (maskData && maskData.hoverShadow && maskData.el) {
        maskData.el.style.boxShadow = maskData.hoverShadow;
      }
      document.body.style.userSelect = 'none';
    }

    // 鼠标事件处理
    window.addEventListener('mousemove', (e) => {
      if (activeMaskAction) {
        const { page, maskData, type } = activeMaskAction;
        const { width, height } = getPageDisplaySize(page);
        if (width > 0 && height > 0) {
          if (type === 'move') {
            e.preventDefault();
            const deltaXRatio = (e.clientX - activeMaskAction.startX) / width;
            const deltaYRatio = (e.clientY - activeMaskAction.startY) / height;
            maskData.xRatio = Math.max(0, Math.min(1 - maskData.widthRatio, activeMaskAction.initialXRatio + deltaXRatio));
            maskData.yRatio = Math.max(0, Math.min(1 - maskData.heightRatio, activeMaskAction.initialYRatio + deltaYRatio));
            maskData.updatePosition();
          } else if (type === 'resize') {
            e.preventDefault();
            const deltaX = e.clientX - activeMaskAction.startX;
            const deltaY = e.clientY - activeMaskAction.startY;
            const initialWidthPx = activeMaskAction.initialWidthRatio * width;
            const initialHeightPx = activeMaskAction.initialHeightRatio * height;
            let proposedWidthPx = initialWidthPx + deltaX;
            const widthFromHeight = (initialHeightPx + deltaY) / A4_RATIO;
            if (isFinite(widthFromHeight) && Math.abs(widthFromHeight - initialWidthPx) > Math.abs(proposedWidthPx - initialWidthPx)) {
              proposedWidthPx = widthFromHeight;
            }
            const maxWidthPx = Math.min(width, height / A4_RATIO);
            if (maxWidthPx > 0) {
              let minWidthPx = Math.min(maxWidthPx, Math.max(width * MIN_MASK_WIDTH_RATIO, 60));
              if (minWidthPx > maxWidthPx) {
                minWidthPx = maxWidthPx * 0.8;
              }
              let newWidthPx = Math.max(minWidthPx, Math.min(maxWidthPx, proposedWidthPx));
              if (!isFinite(newWidthPx) || newWidthPx <= 0) {
                newWidthPx = Math.max(minWidthPx, maxWidthPx * 0.5);
              }
              const newWidthRatio = newWidthPx / width;
              const newHeightPx = newWidthPx * A4_RATIO;
              const newHeightRatio = newHeightPx / height;
              let newXRatio = activeMaskAction.initialXRatio;
              let newYRatio = activeMaskAction.initialYRatio;
              if (newXRatio + newWidthRatio > 1) newXRatio = Math.max(0, 1 - newWidthRatio);
              if (newYRatio + newHeightRatio > 1) newYRatio = Math.max(0, 1 - newHeightRatio);
              maskData.widthRatio = newWidthRatio;
              maskData.heightRatio = newHeightRatio;
              maskData.xRatio = newXRatio;
              maskData.yRatio = newYRatio;
              maskData.updatePosition();
            }
          }
        }
      }

      if (!isDragging || !activeLine) return;
      const { line, page, cutData } = activeLine;
      const wrapperRect = page.wrapper.getBoundingClientRect();
      if (wrapperRect.width <= 0) return;

      let newX = e.clientX - wrapperRect.left;
      newX = Math.max(0, Math.min(wrapperRect.width, newX));
      const newRatio = newX / wrapperRect.width;

      cutData.ratio = newRatio;
      line.style.left = `${newX}px`;
    });

    window.addEventListener('mouseup', () => {
      if (isDragging) {
        isDragging = false;
        activeLine = null;
      }
      if (activeMaskAction) {
        const { maskData } = activeMaskAction;
        if (maskData && maskData.baseShadow && maskData.el) {
          maskData.el.style.boxShadow = maskData.baseShadow;
        }
        activeMaskAction = null;
        document.body.style.userSelect = '';
      }
    });

    // 文件上传处理
    function handleFileUpload(file) {
      container.innerHTML = '';
      pages = [];
      currentPageIndex = null;

      if (!file) return;

      showLoading('正在加载文件,请稍候...');

      if (file.type === 'application/pdf') {
        handlePDFFile(file);
      } else if (supportedImageTypes.includes(file.type)) {
        handleImageFile(file);
      } else {
        showMessage('不支持的文件类型。请上传 PDF 或支持的图片格式。', 'error');
        hideLoading();
      }
    }

    async function handlePDFFile(file) {
      try {
        const fileReader = new FileReader();
        const arrayBuffer = await new Promise((resolve, reject) => {
          fileReader.onload = () => resolve(fileReader.result);
          fileReader.onerror = reject;
          fileReader.readAsArrayBuffer(file);
        });

        const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
        const pdf = await loadingTask.promise;
        const scale = 1.5;

        for (let i = 0; i < pdf.numPages; i++) {
          updateProgress((i / pdf.numPages) * 100, `正在加载 PDF 第 ${i + 1} / ${pdf.numPages} 页...`);
          
          const page = await pdf.getPage(i + 1);
          const viewport = page.getViewport({ scale });
          const pageData = createCanvasPage(viewport.width, viewport.height, i);

          const renderContext = { canvasContext: pageData.ctx, viewport: viewport };
          await page.render(renderContext).promise;

          const highResScale = 2.0;
          const highResViewport = page.getViewport({ scale: highResScale });
          const offscreenCanvas = document.createElement('canvas');
          offscreenCanvas.width = highResViewport.width;
          offscreenCanvas.height = highResViewport.height;
          const offscreenCtx = offscreenCanvas.getContext('2d');
          const highResRenderContext = { canvasContext: offscreenCtx, viewport: highResViewport };
          await page.render(highResRenderContext).promise;
          pageData.originalImage = offscreenCanvas.toDataURL('image/jpeg', 0.95);

          pages.push(pageData);
        }
        
        showMessage(`成功加载 ${pdf.numPages} 页PDF文件`);
      } catch (error) {
        console.error('PDF处理错误:', error);
        showMessage(`PDF处理失败: ${error.message}`, 'error');
      } finally {
        hideLoading();
      }
    }

    async function handleImageFile(file) {
      try {
        const img = new Image();
        const imageUrl = URL.createObjectURL(file);
        
        await new Promise((resolve, reject) => {
          img.onload = () => {
            const maxDimension = 5000;
            let width = img.naturalWidth;
            let height = img.naturalHeight;

            if (width > maxDimension || height > maxDimension) {
              showMessage('图片尺寸过大,将自动缩放以提高性能。');
              const scale = Math.min(maxDimension / width, maxDimension / height);
              width = width * scale;
              height = height * scale;
            }

            const pageData = createCanvasPage(width, height, 0);
            pageData.ctx.drawImage(img, 0, 0, width, height);
            pageData.originalImage = pageData.canvas.toDataURL('image/jpeg', 0.95);
            pages.push(pageData);
            
            URL.revokeObjectURL(imageUrl);
            resolve();
          };
          
          img.onerror = () => {
            URL.revokeObjectURL(imageUrl);
            reject(new Error('无法加载图片文件'));
          };
          
          img.src = imageUrl;
        });
        
        showMessage('成功加载图片文件');
      } catch (error) {
        console.error('图片处理错误:', error);
        showMessage(`图片处理失败: ${error.message}`, 'error');
      } finally {
        hideLoading();
      }
    }

    // 事件监听器
    fileInput.addEventListener('change', (e) => {
      const file = e.target.files[0];
      handleFileUpload(file);
    });

    uploadArea.addEventListener('click', () => {
      fileInput.click();
    });

    uploadArea.addEventListener('dragover', (e) => {
      e.preventDefault();
      uploadArea.classList.add('dragover');
    });

    uploadArea.addEventListener('dragleave', () => {
      uploadArea.classList.remove('dragover');
    });

    uploadArea.addEventListener('drop', (e) => {
      e.preventDefault();
      uploadArea.classList.remove('dragover');
      const file = e.dataTransfer.files[0];
      if (file) {
        fileInput.files = e.dataTransfer.files;
        handleFileUpload(file);
      }
    });

    selectAllBtn.addEventListener('click', () => {
      if (pages.length === 0) {
        showMessage('请先上传文件。', 'error');
        return;
      }
      
      pages.forEach((page, index) => {
        removeCutLinesFromPage(page);
        selectPage(page);
        addCutLineToPage(page, 0.5);
      });
      
      showMessage('已为所有页面添加裁切线!');
    });

    addCutLineBtn.addEventListener('click', () => {
      if (currentPageIndex === null || !pages[currentPageIndex]) {
        showMessage('请点击选中一页后再添加裁切线', 'error');
        return;
      }
      const page = pages[currentPageIndex];
      addCutLineToPage(page, 0.5);
    });

    removeCutLinesBtn.addEventListener('click', () => {
      if (currentPageIndex === null || !pages[currentPageIndex]) {
        showMessage('请先选中页面', 'error');
        return;
      }
      removeCutLinesFromPage(pages[currentPageIndex]);
    });

    addMaskBtn.addEventListener('click', () => {
      if (currentPageIndex === null || !pages[currentPageIndex]) {
        showMessage('请选择一页后再添加遮罩', 'error');
        return;
      }
      const page = pages[currentPageIndex];
      addA4MaskToPage(page);
    });

    exportMaskBtn.addEventListener('click', async () => {
      if (pages.length === 0) {
        showMessage('请先上传文件', 'error');
        return;
      }

      const totalMasks = pages.reduce((sum, page) => {
        if (!Array.isArray(page.masks)) return sum;
        return sum + page.masks.length;
      }, 0);

      if (totalMasks === 0) {
        showMessage('请至少添加一个A4遮罩后再导出', 'error');
        return;
      }

      showLoading('&#128640; 正在导出遮罩PDF,请稍候...', 0);
      exportMaskBtn.disabled = true;

      try {
        const a4WidthPt = 8.27 * 72;
        const a4HeightPt = 11.69 * 72;
        const { jsPDF } = jspdf;
        const pdf = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' });
        let pdfPageCount = 0;
        let processedMasks = 0;

        for (const [pageIndex, page] of pages.entries()) {
          if (!page.originalImage || !Array.isArray(page.masks) || page.masks.length === 0) continue;
          const maskList = page.masks.slice();
          const pageMaskCount = maskList.length;

          await new Promise((resolve) => {
            const img = new Image();
            img.onload = () => {
              const sourceCanvas = document.createElement('canvas');
              sourceCanvas.width = img.naturalWidth;
              sourceCanvas.height = img.naturalHeight;
              const sourceCtx = sourceCanvas.getContext('2d');
              sourceCtx.drawImage(img, 0, 0);

              for (const maskData of maskList) {
                processedMasks += 1;
                updateProgress((processedMasks / totalMasks) * 100, `正在生成第 ${processedMasks} / ${totalMasks} 个A4遮罩...`);

                const cropX = Math.max(0, Math.round(maskData.xRatio * sourceCanvas.width));
                const cropY = Math.max(0, Math.round(maskData.yRatio * sourceCanvas.height));
                let cropWidth = Math.round(maskData.widthRatio * sourceCanvas.width);
                let cropHeight = Math.round(maskData.heightRatio * sourceCanvas.height);

                if (cropX + cropWidth > sourceCanvas.width) {
                  cropWidth = sourceCanvas.width - cropX;
                }
                if (cropY + cropHeight > sourceCanvas.height) {
                  cropHeight = sourceCanvas.height - cropY;
                }

                if (cropWidth <= 0 || cropHeight <= 0) {
                  continue;
                }

                const tempCanvas = document.createElement('canvas');
                tempCanvas.width = cropWidth;
                tempCanvas.height = cropHeight;
                const tempCtx = tempCanvas.getContext('2d');
                tempCtx.drawImage(sourceCanvas, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);

                if (pdfPageCount > 0) pdf.addPage('a4', 'p');
                pdfPageCount += 1;
                pdf.setPage(pdfPageCount);

                const scale = Math.min(a4WidthPt / cropWidth, a4HeightPt / cropHeight);
                const finalWidth = cropWidth * scale;
                const finalHeight = cropHeight * scale;
                const offsetX = (a4WidthPt - finalWidth) / 2;
                const offsetY = (a4HeightPt - finalHeight) / 2;

                pdf.addImage(tempCanvas.toDataURL('image/jpeg', 0.95), 'JPEG', offsetX, offsetY, finalWidth, finalHeight);
              }

              resolve();
            };
            img.onerror = () => {
              console.error(`加载第 ${pageIndex + 1} 页图像数据失败,跳过其遮罩。`);
              processedMasks += pageMaskCount;
              updateProgress((processedMasks / totalMasks) * 100, `跳过第 ${pageIndex + 1} 页遮罩`);
              resolve();
            };
            img.src = page.originalImage;
          });
        }

        if (pdfPageCount > 0) {
          pdf.save('A4遮罩拼接.pdf');
          showMessage('遮罩PDF导出成功!');
        } else {
          showMessage('未能生成遮罩PDF,请检查遮罩范围。', 'error');
        }
      } catch (error) {
        console.error('导出遮罩PDF时发生错误:', error);
        showMessage(`导出遮罩PDF失败: ${error.message}`, 'error');
      } finally {
        hideLoading();
        exportMaskBtn.disabled = false;
      }
    });

    exportBtn.addEventListener('click', async () => {
      if (pages.length === 0) {
        showMessage('请先上传文件。', 'error');
        return;
      }

      const pagesWithoutCuts = pages.filter(p => p.cutLines.length === 0);
      if (pagesWithoutCuts.length > 0) {
        const confirmProceed = confirm(`警告:有 ${pagesWithoutCuts.length} 页没有添加裁切线,这些页面将以整页方式导出。是否继续?`);
        if (!confirmProceed) return;
      }

      showLoading('&#128640; 正在导出裁切线PDF,请稍候...');
      exportBtn.disabled = true;

      try {
        const a4WidthPt = 8.27 * 72;
        const a4HeightPt = 11.69 * 72;
        const { jsPDF } = jspdf;
        const pdf = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' });
        let pdfPageCount = 0;

        for (const [index, page] of pages.entries()) {
          updateProgress((index / pages.length) * 100, `正在处理第 ${index + 1} / ${pages.length} 页...`);

          await new Promise((resolve) => {
            if (!page.originalImage) {
              console.warn(`Page ${index + 1} missing original image data. Skipping.`);
              resolve();
              return;
            }

            const img = new Image();
            img.onload = () => {
              const sourceCanvas = document.createElement('canvas');
              sourceCanvas.width = img.naturalWidth;
              sourceCanvas.height = img.naturalHeight;
              const sourceCtx = sourceCanvas.getContext('2d');
              sourceCtx.drawImage(img, 0, 0);

              const hasCutLines = page.cutLines.length > 0;

              if (!hasCutLines) {
                if (sourceCanvas.width > 0 && sourceCanvas.height > 0) {
                  if (pdfPageCount > 0) pdf.addPage('a4', 'p');
                  pdfPageCount++;
                  pdf.setPage(pdfPageCount);

                  const imgWidth = sourceCanvas.width;
                  const imgHeight = sourceCanvas.height;
                  const scale = Math.min(a4WidthPt / imgWidth, a4HeightPt / imgHeight);
                  const finalWidth = imgWidth * scale;
                  const finalHeight = imgHeight * scale;
                  const offsetX = (a4WidthPt - finalWidth) / 2;
                  const offsetY = (a4HeightPt - finalHeight) / 2;

                  pdf.addImage(sourceCanvas.toDataURL('image/jpeg', 0.95), 'JPEG', offsetX, offsetY, finalWidth, finalHeight);
                }
              } else {
                const sortedRatios = page.cutLines.map(line => line.ratio).sort((a, b) => a - b);
                const uniqueSortedRatios = [...new Set(sortedRatios)];
                const positions = [0, ...uniqueSortedRatios, 1];

                for (let i = 0; i < positions.length - 1; i++) {
                  const startRatio = positions[i];
                  const endRatio = positions[i + 1];

                  if (typeof startRatio !== 'number' || typeof endRatio !== 'number' || startRatio < 0 || startRatio >= endRatio || endRatio > 1) {
                    continue;
                  }

                  const startX = Math.round(startRatio * sourceCanvas.width);
                  const segmentWidth = Math.max(0, Math.round((endRatio - startRatio) * sourceCanvas.width));
                  const segmentHeight = sourceCanvas.height;

                  if (segmentWidth <= 0 || segmentHeight <= 0) {
                    continue;
                  }

                  const tempCanvas = document.createElement('canvas');
                  tempCanvas.width = segmentWidth;
                  tempCanvas.height = segmentHeight;
                  const tempCtx = tempCanvas.getContext('2d');
                  tempCtx.drawImage(sourceCanvas, startX, 0, segmentWidth, segmentHeight, 0, 0, segmentWidth, segmentHeight);

                  if (pdfPageCount > 0) pdf.addPage('a4', 'p');
                  pdfPageCount++;
                  pdf.setPage(pdfPageCount);

                  const imgWidth = segmentWidth;
                  const imgHeight = segmentHeight;

                  if (imgWidth > 0 && imgHeight > 0) {
                    const scaleWidth = a4WidthPt / imgWidth;
                    const scaleHeight = a4HeightPt / imgHeight;
                    const scale = Math.min(scaleWidth, scaleHeight);
                    const finalWidth = imgWidth * scale;
                    const finalHeight = imgHeight * scale;
                    const offsetX = (a4WidthPt - finalWidth) / 2;
                    const offsetY = (a4HeightPt - finalHeight) / 2;

                    pdf.addImage(tempCanvas.toDataURL('image/jpeg', 0.95), 'JPEG', offsetX, offsetY, finalWidth, finalHeight);
                }
              }
            }

              if (page.cutLines.length > 0 && Array.isArray(page.masks) && page.masks.length > 0) {
                const maskList = page.masks.slice();
                maskList.forEach((maskData, maskIndex) => {
                  updateProgress((index / pages.length) * 100, `正在导出第 ${index + 1} 页遮罩区域 (${maskIndex + 1}/${maskList.length})...`);

                  const cropX = Math.max(0, Math.round(maskData.xRatio * sourceCanvas.width));
                  const cropY = Math.max(0, Math.round(maskData.yRatio * sourceCanvas.height));
                  let cropWidth = Math.round(maskData.widthRatio * sourceCanvas.width);
                  let cropHeight = Math.round(maskData.heightRatio * sourceCanvas.height);

                  if (cropX + cropWidth > sourceCanvas.width) {
                    cropWidth = sourceCanvas.width - cropX;
                  }
                  if (cropY + cropHeight > sourceCanvas.height) {
                    cropHeight = sourceCanvas.height - cropY;
                  }

                  if (cropWidth <= 0 || cropHeight <= 0) {
                    return;
                  }

                  const tempCanvas = document.createElement('canvas');
                  tempCanvas.width = cropWidth;
                  tempCanvas.height = cropHeight;
                  const tempCtx = tempCanvas.getContext('2d');
                  tempCtx.drawImage(sourceCanvas, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);

                  if (pdfPageCount > 0) pdf.addPage('a4', 'p');
                  pdfPageCount++;
                  pdf.setPage(pdfPageCount);

                  const scale = Math.min(a4WidthPt / cropWidth, a4HeightPt / cropHeight);
                  const finalWidth = cropWidth * scale;
                  const finalHeight = cropHeight * scale;
                  const offsetX = (a4WidthPt - finalWidth) / 2;
                  const offsetY = (a4HeightPt - finalHeight) / 2;

                  pdf.addImage(tempCanvas.toDataURL('image/jpeg', 0.95), 'JPEG', offsetX, offsetY, finalWidth, finalHeight);
                });
              }
              resolve();
            };

            img.onerror = () => {
              console.error(`Error loading image data for page ${index + 1} for export.`);
              resolve();
            };

            img.src = page.originalImage;
          });
        }

        if (pdfPageCount > 0) {
          pdf.save('裁切线导出.pdf');
          showMessage('裁切线PDF导出成功!');
        } else {
          showMessage('未能生成裁切线PDF,请检查裁切线和遮罩设置。', 'error');
        }
      } catch (error) {
        console.error('导出裁切线PDF时发生错误:', error);
        showMessage(`导出裁切线PDF失败: ${error.message}`, 'error');
      } finally {
        hideLoading();
        exportBtn.disabled = false;
      }
    });
  </script>
</body>
</html> 



其中的链接https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.min.js?t=YUJCCh、https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js?t=YUJCCh、https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.worker.min.js 是上传控件,必须联网才能使用
你看看这个是不是跟你这个差不多
推荐
 楼主| 海东青1013 发表于 2026-5-6 16:21 |楼主
本帖最后由 海东青1013 于 2026-5-6 16:25 编辑
bybyby11 发表于 2026-5-6 16:03
[HTML] 纯文本查看 复制代码


[/quote]看了下,你发的这个的确可以,但不适合小学生的孩子使用,因为上面的代码里缺少了图片左右上下翻个功能。
其次,现在家里电脑都联网了(所有图片都是钉钉发过来的),所以将代码中改了。如果要离线,可以改下代码,将链接中的文件另存到这个代码的同一目录(最好创建文件夹),然后变更对应代码为:

[mw_shl_code=javascript,true]<script src="pdf.min.js"></script>
<script src="jspdf.umd.min.js"></script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdf.worker.min.js';

12.png (495.86 KB, 下载次数: 0)

12.png
沙发
苏紫方璇 发表于 2026-5-6 09:54
请在帖子中插入部分关键代码
本版块仅限分享编程技术和源码相关内容,发布帖子必须带上关键代码和具体功能介绍
代码插入方法可见板块置顶帖
【公告】发帖代码插入以及添加链接教程(有福利)
https://www.52pojie.cn/thread-713042-1-1.html
(出处: 吾爱破解论坛)
3#
不依baya 发表于 2026-5-6 10:48
这个很不错,A3卷子用A4打印太小了
4#
fuxingjun674 发表于 2026-5-6 10:50
本帖最后由 fuxingjun674 于 2026-5-6 10:51 编辑

这个不错啊。可以实现一键A3纸,打印到A4纸!
5#
 楼主| 海东青1013 发表于 2026-5-6 10:51 |楼主
fuxingjun674 发表于 2026-5-6 10:50
这个不错啊。可以实现一键A3纸,打印到A4纸!解压密码?

感谢您的评价。^_^
6#
jun269 发表于 2026-5-6 10:53
A3打印成一张不是更好吗?
7#
dork 发表于 2026-5-6 10:57
除此之外,可以用扫描**王一类的APP导入图片后分割成二半,也可以实现,而且顺便可以将图片清晰化。
8#
 楼主| 海东青1013 发表于 2026-5-6 10:57 |楼主
jun269 发表于 2026-5-6 10:53
A3打印成一张不是更好吗?

我问了很多老师和家长,告知很多家里没有A3打印机,有的也基本都是黑白色的A4打印机。
9#
 楼主| 海东青1013 发表于 2026-5-6 11:01 |楼主
dork 发表于 2026-5-6 10:57
除此之外,可以用扫描**王一类的APP导入图片后分割成二半,也可以实现,而且顺便可以将图片清晰化。

这个您说的对,但孩子还小,自己打印存在要操作多个软件,且还要手动去调整\裁剪,所以改成HTMl的,为的就是让孩子自己整去,家长可以偷懒下。
10#
fuxingjun674 发表于 2026-5-6 11:01
jun269 发表于 2026-5-6 10:53
A3打印成一张不是更好吗?

问题是没有A3打印机
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-5-7 03:02

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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