<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>明哥的小工具</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awes…;
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js"></scr…;
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></…;
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
header {
background-color: #2c3e50;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.main-container {
display: flex;
flex: 1;
overflow: hidden;
}
.toolbar {
width: 300px;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
padding: 1.5rem;
overflow-y: auto;
}
.preview-container {
flex: 1;
overflow: auto;
background-color: #f1f3f5;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
position: relative;
}
.a4-container {
position: relative;
margin-bottom: 2rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
background-color: white;
max-width: 90%;
width: 100%;
}
#combinedCanvas {
width: 100%;
height: auto;
display: block;
}
#pdfCanvas, #stampCanvas {
display: none;
position: absolute;
top: 0;
left: 0;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
margin-bottom: 1rem;
width: 100%;
}
.btn:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.btn-danger {
background-color: #dc3545;
}
.btn-danger:hover {
background-color: #c82333;
}
.btn i {
margin-right: 0.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #495057;
}
input[type="text"],
input[type="number"],
input[type="file"],
select {
width: 100%;
padding: 0.5rem;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
}
.page-controls {
display: flex;
align-items: center;
justify-content: center;
margin: 1rem 0;
width: 100%;
max-width: 800px;
}
.page-controls button {
margin: 0 0.5rem;
}
.page-number {
margin: 0 1rem;
font-weight: 500;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 123, 255, 0.2);
border-radius: 50%;
border-top: 4px solid #007bff;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.stamp-controls {
position: absolute;
display: none;
flex-direction: column;
background-color: white;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
padding: 0.5rem;
z-index: 100;
}
.seam-controls {
position: absolute;
display: none;
flex-direction: column;
background-color: white;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
padding: 0.5rem;
z-index: 100;
}
.stamp-controls button, .seam-controls button {
background: none;
border: none;
padding: 0.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.stamp-controls button:hover, .seam-controls button:hover {
background-color: #f1f3f5;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1000;
}
.error-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1000;
color: #dc3545;
text-align: center;
padding: 2rem;
}
.loading-progress {
margin-top: 1rem;
width: 300px;
height: 8px;
background-color: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.loading-bar {
height: 100%;
background-color: #007bff;
width: 0%;
transition: width 0.3s ease;
}
.loading-status {
margin-top: 0.5rem;
color: #6c757d;
font-size: 14px;
}
/* 骑缝章相关样式 */
.seam-seal-options {
background-color: #f0f7ff;
padding: 1.5rem;
border-radius: 8px;
margin: 1.5rem 0;
border: 1px solid #bed6ee;
}
.seam-seal-options h4 {
margin-bottom: 1rem;
color: #196ecf;
display: flex;
align-items: center;
}
.seam-seal-options h4 i {
margin-right: 0.5rem;
}
.position-selector {
display: flex;
justify-content: space-between;
margin: 1rem 0;
}
.position-option {
display: flex;
flex-direction: column;
align-items: center;
width: 70px;
cursor: pointer;
padding: 0.5rem 0;
border-radius: 4px;
}
.position-option.selected {
background-color: #e7f0fe;
border: 1px solid #007bff;
}
.upload-area {
border: 2px dashed #adb5bd;
border-radius: 6px;
padding: 1.5rem;
text-align: center;
cursor: pointer;
margin-bottom: 1rem;
}
.upload-area i {
font-size: 24px;
color: #6c757d;
margin-bottom: 0.5rem;
}
.stamp-items {
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
margin-top: 1rem;
}
.stamp-item {
width: 60px;
height: 60px;
border-radius: 4px;
overflow: hidden;
border: 2px solid transparent;
cursor: pointer;
position: relative;
}
.stamp-item.selected {
border-color: #007bff;
}
.stamp-item img {
width: 100%;
height: 100%;
object-fit: contain;
background-color: white;
}
.delete-stamp-btn {
position: absolute;
top: -5px;
right: -5px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
cursor: pointer;
padding: 0;
}
.stamp-actions {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
@media (max-width: 768px) {
.main-container {
flex-direction: column;
}
.toolbar {
width: 100%;
height: auto;
max-height: 50%;
border-right: none;
border-bottom: 1px solid #dee2e6;
}
.preview-container {
padding: 1rem;
}
}
</style>
</head>
<body>
<header>
<h1><i class="fa fa-file-pdf-o mr-2"></i>明哥的小工具</h1>
<div>
<button id="downloadPdf" class="btn" disabled>
<i class="fa fa-download"></i>下载盖章后的PDF
</button>
</div>
</header>
<div class="main-container">
<div class="toolbar">
<div class="form-group">
<label for="pdfUpload">上传PDF文件</label>
<input type="file" id="pdfUpload" accept="application/pdf, application/x-pdf, application/acrobat, applications/vnd.pdf, text/pdf, text/x-pdf" class="form-control">
<small style="color: #6c757d; font-size: 12px;">支持PDF格式文件(最大10MB)</small>
</div>
<hr style="margin: 1.5rem 0;">
<h3 style="margin-bottom: 1rem;">添加图片印章</h3>
<div class="form-group">
<label>选择图片(仅支持PNG格式)</label>
<div class="upload-area" id="stampUploadArea">
<input type="file" id="imageStamp" accept="image/png">
<i class="fa fa-upload"></i>
<p>点击上传印章图片</p>
</div>
<small style="color: #6c757d; font-size: 12px;">仅支持PNG格式(推荐透明背景,最大2MB)</small>
<div class="stamp-items" id="stampItems">
<!-- 印章图片将在这里显示 -->
</div>
</div>
<div class="form-group">
<label for="imageSize">图片大小</label>
<input type="number" id="imageSize" min="20" max="300" value="100">
</div>
<div class="form-group">
<label for="imageOpacity">透明度</label>
<input type="number" id="imageOpacity" min="1" max="100" value="100">
</div>
<button id="addImageStamp" class="btn" disabled>
<i class="fa fa-image"></i>添加图片印章
</button>
<!-- 骑缝章设置区域 -->
<div class="seam-seal-options">
<h4><i class="fa fa-scissors"></i>骑缝章设置</h4>
<div class="form-group">
<label for="seamStartPage">起始页</label>
<input type="number" id="seamStartPage" min="1" value="1" disabled>
</div>
<div class="form-group">
<label for="seamEndPage">结束页</label>
<input type="number" id="seamEndPage" min="1" value="1" disabled>
</div>
<div class="form-group">
<label>骑缝章位置</label>
<div class="position-selector">
<div class="position-option selected" data-position="left">
<div>
<svg width="30" height="30" viewBox="0 0 30 30">
<rect x="2" y="2" width="26" height="26" fill="none" stroke="#ccc" stroke-width="1"/>
<rect x="2" y="2" width="5" height="26" fill="#007bff" opacity="0.3"/>
</svg>
</div>
<span style="font-size: 12px;">左侧</span>
</div>
<div class="position-option" data-position="right">
<div>
<svg width="30" height="30" viewBox="0 0 30 30">
<rect x="2" y="2" width="26" height="26" fill="none" stroke="#ccc" stroke-width="1"/>
<rect x="23" y="2" width="5" height="26" fill="#007bff" opacity="0.3"/>
</svg>
</div>
<span style="font-size: 12px;">右侧</span>
</div>
</div>
</div>
<div class="form-group">
<label>骑缝章大小</label>
<div style="padding: 0.5rem; background-color: #f8f9fa; border-radius: 4px; font-size: 14px;">
与图片印章大小一致:<span id="seamSizeDisplay">100</span>px
</div>
</div>
<button id="addSeamStamp" class="btn btn-secondary" disabled>
<i class="fa fa-scissors"></i>添加骑缝章
</button>
</div>
<button id="clearStamps" class="btn btn-danger">
<i class="fa fa-trash"></i>清除所有印章
</button>
<hr style="margin: 1.5rem 0;">
<div class="form-group">
<label for="pageSize">页面尺寸</label>
<select id="pageSize">
<option value="a4" selected>A4 (210×297mm)</option>
<option value="original">原始尺寸</option>
</select>
</div>
<div class="form-group">
<label for="qualitySetting">下载质量</label>
<select id="qualitySetting">
<option value="1.5">标准 (较快)</option>
<option value="2.0" selected>高清 (推荐)</option>
</select>
</div>
</div>
<div class="preview-container">
<div class="a4-container">
<!-- 使用单个组合画布显示PDF和印章的混合效果 -->
<canvas id="combinedCanvas"></canvas>
<!-- 隐藏的原始画布用于处理 -->
<canvas id="pdfCanvas"></canvas>
<canvas id="stampCanvas"></canvas>
<div id="pdfPlaceholder" style="display: block; padding: 2rem; text-align: center; color: #666;">
<i class="fa fa-file-pdf-o" style="font-size: 48px; margin-bottom: 1rem; color: #dc3545;"></i>
<p>请上传PDF文件开始操作</p>
</div>
</div>
<div class="page-controls">
<button id="prevPage" class="btn" disabled>
<i class="fa fa-chevron-left"></i>上一页
</button>
<span id="pageNum" class="page-number">0 / 0</span>
<button id="nextPage" class="btn" disabled>
下一页<i class="fa fa-chevron-right"></i>
</button>
</div>
<div id="stampControls" class="stamp-controls">
<button id="moveUp"><i class="fa fa-arrow-up"></i></button>
<button id="moveDown"><i class="fa fa-arrow-down"></i></button>
<button id="moveLeft"><i class="fa fa-arrow-left"></i></button>
<button id="moveRight"><i class="fa fa-arrow-right"></i></button>
<button id="rotateStamp"><i class="fa fa-rotate-right"></i></button>
<button id="scaleUp"><i class="fa fa-search-plus"></i></button>
<button id="scaleDown"><i class="fa fa-search-minus"></i></button>
<button id="deleteStamp"><i class="fa fa-trash"></i></button>
</div>
<div id="seamControls" class="seam-controls">
<button id="seamMoveUp"><i class="fa fa-arrow-up"></i></button>
<button id="seamMoveDown"><i class="fa fa-arrow-down"></i></button>
<button id="deleteSeamStamp"><i class="fa fa-trash"></i></button>
</div>
</div>
</div>
<div id="libraryLoading" class="loading-overlay">
<div class="spinner"></div>
<p style="margin-top: 1rem;">正在加载必要组件,请稍候...</p>
<div class="loading-progress">
<div id="loadingBar" class="loading-bar"></div>
</div>
<div id="loadingStatus" class="loading-status">准备加载组件...</div>
</div>
<script>
// 全局变量
let pdfDoc = null;
let currentPage = 1;
let totalPages = 0;
let pdfCanvas = document.getElementById('pdfCanvas');
let stampCanvas = document.getElementById('stampCanvas');
let combinedCanvas = document.getElementById('combinedCanvas');
let pdfCtx = pdfCanvas.getContext('2d');
let stampCtx = stampCanvas.getContext('2d');
let combinedCtx = combinedCanvas.getContext('2d');
let stamps = []; // 普通印章
let selectedStampIndex = -1;
let stampImages = []; // 存储印章图片
let selectedStampId = null; // 当前选中的印章
let seamStamps = []; // 存储骑缝章信息
let selectedSeamIndex = -1; // 当前选中的骑缝章
let pdfCache = new Map(); // 缓存已渲染的PDF页面
let isDragging = false; // 拖拽状态标志
let dragType = null; // 拖拽类型: "stamp" 或 "seam"
let lastRenderTime = 0; // 上次渲染时间,用于节流
let renderQueue = Promise.resolve(); // 渲染队列,确保渲染操作顺序执行
// DOM元素
const pdfUpload = document.getElementById('pdfUpload');
const imageStamp = document.getElementById('imageStamp');
const stampUploadArea = document.getElementById('stampUploadArea');
const stampItems = document.getElementById('stampItems');
const addImageStampBtn = document.getElementById('addImageStamp');
const addSeamStampBtn = document.getElementById('addSeamStamp');
const clearStampsBtn = document.getElementById('clearStamps');
const downloadPdfBtn = document.getElementById('downloadPdf');
const imageSizeInput = document.getElementById('imageSize');
const imageOpacityInput = document.getElementById('imageOpacity');
const seamSizeDisplay = document.getElementById('seamSizeDisplay');
const pageSizeSelect = document.getElementById('pageSize');
const qualitySettingSelect = document.getElementById('qualitySetting');
const seamStartPageInput = document.getElementById('seamStartPage');
const seamEndPageInput = document.getElementById('seamEndPage');
const positionOptions = document.querySelectorAll('.position-option');
const prevPageBtn = document.getElementById('prevPage');
const nextPageBtn = document.getElementById('nextPage');
const pageNumDisplay = document.getElementById('pageNum');
const pdfPlaceholder = document.getElementById('pdfPlaceholder');
const stampControls = document.getElementById('stampControls');
const seamControls = document.getElementById('seamControls');
const moveUpBtn = document.getElementById('moveUp');
const moveDownBtn = document.getElementById('moveDown');
const moveLeftBtn = document.getElementById('moveLeft');
const moveRightBtn = document.getElementById('moveRight');
const rotateStampBtn = document.getElementById('rotateStamp');
const scaleUpBtn = document.getElementById('scaleUp');
const scaleDownBtn = document.getElementById('scaleDown');
const deleteStampBtn = document.getElementById('deleteStamp');
const seamMoveUpBtn = document.getElementById('seamMoveUp');
const seamMoveDownBtn = document.getElementById('seamMoveDown');
const deleteSeamStampBtn = document.getElementById('deleteSeamStamp');
// 更新加载进度
function updateLoadingProgress(percent, status) {
const loadingBar = document.getElementById('loadingBar');
const loadingStatus = document.getElementById('loadingStatus');
if (loadingBar) {
loadingBar.style.width = `${percent}%`;
}
if (loadingStatus && status) {
loadingStatus.textContent = status;
}
}
// 显示加载提示
function showLoadingOverlay(message) {
let overlay = document.querySelector('.loading-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'loading-overlay';
document.body.appendChild(overlay);
}
overlay.innerHTML = `
<div class="spinner"></div>
<p style="margin-top: 1rem;">${message || '加载中...'}</p>
<div class="loading-progress">
<div id="loadingBar" class="loading-bar"></div>
</div>
<div id="loadingStatus" class="loading-status">准备加载组件...</div>
`;
overlay.style.display = 'flex';
}
// 隐藏加载提示
function hideLoadingOverlay() {
const overlay = document.querySelector('.loading-overlay');
if (overlay) {
overlay.style.display = 'none';
}
}
// 显示错误提示
function showErrorOverlay(message) {
let overlay = document.querySelector('.error-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'error-overlay';
document.body.appendChild(overlay);
}
overlay.innerHTML = `
<i class="fa fa-exclamation-triangle" style="font-size: 48px; margin-bottom: 1rem;"></i>
<p style="margin-top: 1rem; font-size: 18px;">${message}</p>
<button class="btn" onclick="tryReloadLibraries()" style="margin-top: 1rem;">
<i class="fa fa-refresh"></i> 重试加载
</button>
<button class="btn btn-secondary" onclick="this.parentElement.style.display='none'" style="margin-top: 0.5rem;">
关闭提示
</button>
`;
overlay.style.display = 'flex';
hideLoadingOverlay();
}
// 定义PDF.js的多个CDN源
const pdfJsSources = [
{
main: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js',
worker: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js…;
},
{
main: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.min.js',
worker: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.worker.min.js…;
},
{
main: 'https://unpkg.com/pdfjs-dist@3.4.120/build/pdf.min.js',
worker: 'https://unpkg.com/pdfjs-dist@3.4.120/build/pdf.worker.min.js'
}
];
// 定义jsPDF的多个CDN源
const jsPdfSources = [
'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js',
'https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js',
'https://unpkg.com/jspdf@2.5.1/dist/jspdf.umd.min.js'
];
// 页面尺寸定义
const pageSizes = {
a4: { width: 595, height: 842, orientation: 'portrait' }
};
// 带重试机制的脚本加载函数
function loadScript(src, timeout = 15000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`加载超时: ${src}`));
}, timeout);
const script = document.createElement('script');
script.src = src + '?v=' + new Date().getTime(); // 防止缓存
script.crossOrigin = 'anonymous';
script.onload = () => {
clearTimeout(timer);
resolve();
};
script.onerror = () => {
clearTimeout(timer);
reject(new Error(`加载失败: ${src}`));
};
document.head.appendChild(script);
});
}
// 带重试的资源加载函数
async function loadWithRetry(loader, maxRetries = 2, delay = 1000) {
let retries = 0;
while (true) {
try {
return await loader();
} catch (error) {
retries++;
if (retries > maxRetries) {
throw error;
}
console.log(`重试加载 (${retries}/${maxRetries})...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// 初始化PDF.js
async function initPdfJs() {
updateLoadingProgress(30, '加载PDF处理组件...');
for (const source of pdfJsSources) {
try {
await loadScript(source.main);
pdfjsLib.GlobalWorkerOptions.workerSrc = source.worker;
updateLoadingProgress(60, 'PDF组件加载成功');
return;
} catch (error) {
console.error(`加载PDF.js失败 (${source.main}):`, error);
}
}
throw new Error('无法加载PDF处理组件,请检查网络连接');
}
// 初始化jsPDF
async function initJsPdf() {
updateLoadingProgress(70, '加载PDF生成组件...');
for (const source of jsPdfSources) {
try {
await loadScript(source);
updateLoadingProgress(90, 'PDF生成组件加载成功');
return;
} catch (error) {
console.error(`加载jsPDF失败 (${source}):`, error);
}
}
throw new Error('无法加载PDF生成组件,请检查网络连接');
}
// 尝试重新加载库
async function tryReloadLibraries() {
showLoadingOverlay('重新加载组件...');
try {
await initPdfJs();
await initJsPdf();
updateLoadingProgress(100, '所有组件加载完成');
setTimeout(hideLoadingOverlay, 500);
} catch (error) {
showErrorOverlay(error.message);
}
}
// 初始化库
async function initLibraries() {
try {
await initPdfJs();
await initJsPdf();
updateLoadingProgress(100, '所有组件加载完成');
setTimeout(hideLoadingOverlay, 500);
} catch (error) {
showErrorOverlay(error.message);
}
}
// 上传PDF文件
pdfUpload.addEventListener('change', async function(e) {
const file = e.target.files[0];
if (!file) return;
if (file.size > 10 * 1024 * 1024) {
alert('PDF文件大小不能超过10MB');
return;
}
// 验证文件类型
const fileExtension = file.name.split('.').pop().toLowerCase();
const allowedExtensions = ['pdf'];
if (!allowedExtensions.includes(fileExtension)) {
alert('请上传PDF格式的文件');
return;
}
showLoadingOverlay('加载PDF文件...');
try {
const arrayBuffer = await readFileAsync(file);
const typedArray = new Uint8Array(arrayBuffer);
// 加载PDF
pdfDoc = await pdfjsLib.getDocument(typedArray).promise;
totalPages = pdfDoc.numPages;
currentPage = 1;
// 清空缓存
pdfCache.clear();
// 更新页码显示
updatePageDisplay();
updatePageButtons();
// 渲染第一页
await renderPage(currentPage);
// 显示PDF,隐藏占位符
pdfPlaceholder.style.display = 'none';
// 更新骑缝章页面限制
seamStartPageInput.max = totalPages;
seamEndPageInput.max = totalPages;
seamEndPageInput.value = totalPages;
seamStartPageInput.disabled = false;
seamEndPageInput.disabled = false;
// 启用按钮
if (stampImages.length > 0) {
addImageStampBtn.disabled = false;
addSeamStampBtn.disabled = false;
}
downloadPdfBtn.disabled = false;
hideLoadingOverlay();
} catch (error) {
console.error('加载PDF失败:', error);
hideLoadingOverlay();
alert('加载PDF失败: ' + error.message);
}
});
// 上传印章图片 - 使用标志防止重复触发
let isFileDialogOpen = false;
imageStamp.addEventListener('change', function(e) {
// 重置标志
isFileDialogOpen = false;
handleStampUpload(e);
});
// 支持拖拽上传印章
stampUploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
this.style.borderColor = '#007bff';
});
stampUploadArea.addEventListener('dragleave', function() {
this.style.borderColor = '#adb5bd';
});
stampUploadArea.addEventListener('drop', function(e) {
e.preventDefault();
this.style.borderColor = '#adb5bd';
if (e.dataTransfer.files.length) {
const file = e.dataTransfer.files[0];
// 验证是否为PNG文件
if (file.type === 'image/png' && file.size <= 2 * 1024 * 1024) {
handleStampFile(file);
} else {
alert('请上传不超过2MB的PNG格式图片');
}
}
});
// 点击上传区域触发文件选择 - 添加标志防止重复打开
stampUploadArea.addEventListener('click', function(e) {
// 防止点击子元素时触发
if (e.target === this || e.target.tagName === 'P' || e.target.tagName === 'I') {
if (!isFileDialogOpen) {
isFileDialogOpen = true;
imageStamp.click();
}
}
});
// 处理印章上传
function handleStampUpload(e) {
const file = e.target.files[0];
if (file) {
// 验证文件类型和大小
if (file.type !== 'image/png') {
alert('请上传PNG格式的图片');
return;
}
if (file.size > 2 * 1024 * 1024) {
alert('图片大小不能超过2MB');
return;
}
handleStampFile(file);
}
}
// 处理印章文件
function handleStampFile(file) {
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
// 图像加载错误处理
img.onerror = function() {
alert('图像加载失败,请尝试其他图片');
console.error('图像加载失败:', file.name);
};
img.onload = function() {
const stampId = 'stamp_' + Date.now();
// 存储图像的原始数据,确保不会被垃圾回收
stampImages.push({
id: stampId,
image: img,
src: e.target.result,
width: img.width,
height: img.height,
loaded: true // 标记为已加载
});
// 添加到印章列表
renderStampItems();
// 自动选中新上传的印章
selectStamp(stampId);
// 启用按钮
if (pdfDoc) {
addImageStampBtn.disabled = false;
addSeamStampBtn.disabled = false;
}
};
img.src = e.target.result;
};
reader.onerror = function() {
alert('文件读取失败,请重试');
console.error('文件读取错误:', reader.error);
};
reader.readAsDataURL(file);
}
// 渲染印章列表
function renderStampItems() {
if (stampImages.length === 0) {
stampItems.innerHTML = '<div style="width:100%;text-align:center;padding:1rem;color:#6c757d;">暂无印章</div>';
return;
}
stampItems.innerHTML = '';
stampImages.forEach(stamp => {
const div = document.createElement('div');
div.className = `stamp-item ${selectedStampId === stamp.id ? 'selected' : ''}`;
div.dataset.id = stamp.id;
div.innerHTML = `
<img src="${stamp.src}" alt="印章">
<button class="delete-stamp-btn" data-id="${stamp.id}">
<i class="fa fa-times"></i>
</button>
`;
div.addEventListener('click', function(e) {
// 防止点击删除按钮时触发选择事件
if (!e.target.closest('.delete-stamp-btn')) {
selectStamp(stamp.id);
}
});
// 删除单个印章
const deleteBtn = div.querySelector('.delete-stamp-btn');
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
const stampId = this.dataset.id;
removeStamp(stampId);
});
stampItems.appendChild(div);
});
}
// 删除单个印章
function removeStamp(stampId) {
if (confirm('确定要删除这个印章吗?')) {
// 从印章列表中移除
stampImages = stampImages.filter(stamp => stamp.id !== stampId);
// 如果删除的是当前选中的印章,取消选择
if (selectedStampId === stampId) {
selectedStampId = null;
}
// 重新渲染列表
renderStampItems();
// 如果没有印章了,禁用相关按钮
if (stampImages.length === 0) {
addImageStampBtn.disabled = true;
addSeamStampBtn.disabled = true;
}
}
}
// 选择印章
function selectStamp(stampId) {
selectedStampId = stampId;
renderStampItems();
}
// 位置选择
positionOptions.forEach(option => {
option.addEventListener('click', function() {
positionOptions.forEach(opt => opt.classList.remove('selected'));
this.classList.add('selected');
});
});
// 当图片大小改变时,同步更新骑缝章大小显示
imageSizeInput.addEventListener('input', function() {
seamSizeDisplay.textContent = this.value;
});
// 添加普通印章
addImageStampBtn.addEventListener('click', function() {
if (!pdfDoc || !selectedStampId) return;
const size = parseInt(imageSizeInput.value);
const opacity = parseInt(imageOpacityInput.value) / 100;
// 获取选中的印章
const stamp = stampImages.find(s => s.id === selectedStampId);
if (!stamp) return;
// 添加新印章(默认位置在页面中心)
stamps.push({
id: 'normal_' + Date.now(),
type: 'normal',
stamp: stamp,
page: currentPage,
x: pdfCanvas.width / 2 - size / 2,
y: pdfCanvas.height / 2 - size / 2,
size: size,
opacity: opacity,
rotation: 0
});
// 选中新添加的印章
selectedStampIndex = stamps.length - 1;
selectedSeamIndex = -1; // 取消选中骑缝章
// 更新控件显示
updateControlVisibility();
// 只重绘印章
queueRenderStamps();
});
// 添加骑缝章 - 使用与图片印章相同的大小
addSeamStampBtn.addEventListener('click', function() {
if (!pdfDoc || !selectedStampId) return;
const startPage = parseInt(seamStartPageInput.value);
const endPage = parseInt(seamEndPageInput.value);
const position = document.querySelector('.position-option.selected').dataset.position;
// 使用当前图片印章大小作为骑缝章大小
const size = parseInt(imageSizeInput.value);
if (startPage > endPage) {
alert('起始页不能大于结束页');
return;
}
if (startPage < 1 || endPage > totalPages) {
alert('页面范围超出PDF页数');
return;
}
// 获取选中的印章
const stamp = stampImages.find(s => s.id === selectedStampId);
if (!stamp) return;
// 创建骑缝章组ID
const seamId = 'seam_' + Date.now();
// 存储骑缝章信息,添加垂直偏移量
seamStamps.push({
id: seamId,
type: 'seam',
stamp: stamp,
startPage: startPage,
endPage: endPage,
position: position,
size: size,
totalPages: endPage - startPage + 1,
offsetY: 0 // 垂直偏移量,用于上下移动
});
// 选中新添加的骑缝章
selectedSeamIndex = seamStamps.length - 1;
selectedStampIndex = -1; // 取消选中普通印章
// 更新控件显示
updateControlVisibility();
// 重新渲染当前页
queueRenderStamps();
});
// 清除所有印章(PDF上的印章,不包括印章库)
clearStampsBtn.addEventListener('click', function() {
if (confirm('确定要清除PDF上的所有印章吗?')) {
stamps = [];
seamStamps = [];
selectedStampIndex = -1;
selectedSeamIndex = -1;
stampControls.style.display = 'none';
seamControls.style.display = 'none';
queueRenderStamps();
}
});
// 渲染页面 - 带缓存优化
async function renderPage(pageNum) {
showLoadingOverlay('渲染页面...');
try {
// 检查缓存
if (pdfCache.has(pageNum)) {
const cached = pdfCache.get(pageNum);
pdfCanvas.width = cached.width;
pdfCanvas.height = cached.height;
stampCanvas.width = cached.width;
stampCanvas.height = cached.height;
combinedCanvas.width = cached.width;
combinedCanvas.height = cached.height;
pdfCtx.putImageData(cached.imageData, 0, 0);
} else {
// 获取页面
const page = await pdfDoc.getPage(pageNum);
// 设置画布尺寸
const quality = parseFloat(qualitySettingSelect.value);
const viewport = page.getViewport({ scale: quality });
pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
stampCanvas.width = viewport.width;
stampCanvas.height = viewport.height;
combinedCanvas.width = viewport.width;
combinedCanvas.height = viewport.height;
// 渲染PDF页面
const renderContext = {
canvasContext: pdfCtx,
viewport: viewport
};
await page.render(renderContext).promise;
// 缓存页面
pdfCache.set(pageNum, {
width: pdfCanvas.width,
height: pdfCanvas.height,
imageData: pdfCtx.getImageData(0, 0, pdfCanvas.width, pdfCanvas.height)
});
}
// 调整A4容器尺寸
const a4Container = document.querySelector('.a4-container');
if (pageSizeSelect.value === 'a4') {
a4Container.style.width = '100%';
a4Container.style.maxWidth = '800px';
a4Container.style.aspectRatio = '210/297';
} else {
a4Container.style.width = 'auto';
a4Container.style.maxWidth = '90%';
a4Container.style.aspectRatio = '';
}
// 绘制印章
drawStamps(pageNum);
// 合并画布
combinedCtx.drawImage(pdfCanvas, 0, 0);
combinedCtx.drawImage(stampCanvas, 0, 0);
hideLoadingOverlay();
} catch (error) {
console.error('渲染页面失败:', error);
hideLoadingOverlay();
alert('渲染页面失败: ' + error.message);
}
}
// 只渲染印章,不重新渲染PDF - 解决闪烁问题
function renderStampsOnly() {
// 节流处理,限制渲染频率
const now = Date.now();
if (now - lastRenderTime < 30) { // 每30ms最多渲染一次
return;
}
lastRenderTime = now;
drawStamps(currentPage);
// 只重绘组合画布,保留PDF内容
combinedCtx.clearRect(0, 0, combinedCanvas.width, combinedCanvas.height);
combinedCtx.drawImage(pdfCanvas, 0, 0);
combinedCtx.drawImage(stampCanvas, 0, 0);
}
// 排队渲染,确保顺序执行
function queueRenderStamps() {
renderQueue = renderQueue.then(() => {
return new Promise(resolve => {
requestAnimationFrame(() => {
renderStampsOnly();
resolve();
});
});
});
}
// 绘制印章
function drawStamps(pageNum) {
// 清除之前的印章
stampCtx.clearRect(0, 0, stampCanvas.width, stampCanvas.height);
// 绘制骑缝章
seamStamps.forEach((seam, index) => {
// 只绘制当前页在骑缝范围内的印章
if (pageNum >= seam.startPage && pageNum <= seam.endPage) {
drawSeamStampPart(seam, pageNum, index === selectedSeamIndex);
}
});
// 绘制普通印章
stamps.forEach((stamp, index) => {
if (stamp.page === pageNum) {
drawNormalStamp(stamp, index === selectedStampIndex);
}
});
}
// 绘制普通印章
function drawNormalStamp(stamp, isSelected) {
// 检查印章是否有效
if (!stamp.stamp || !stamp.stamp.image || !stamp.stamp.loaded) {
console.error('无效的印章图像', stamp);
return;
}
const { image } = stamp.stamp;
const { x, y, size, rotation, opacity } = stamp;
stampCtx.save();
stampCtx.globalAlpha = opacity;
try {
// 平移到印章中心进行旋转
stampCtx.translate(x + size / 2, y + size / 2);
stampCtx.rotate(rotation * Math.PI / 180);
// 绘制印章
stampCtx.drawImage(image, -size / 2, -size / 2, size, size);
// 如果是选中状态,绘制边框
if (isSelected) {
stampCtx.strokeStyle = '#007bff';
stampCtx.lineWidth = 2;
stampCtx.strokeRect(-size / 2 - 5, -size / 2 - 5, size + 10, size + 10);
}
} catch (error) {
console.error('绘制普通印章失败:', error);
// 尝试重新加载图像
if (stamp.stamp.src) {
const newImg = new Image();
newImg.onload = function() {
stamp.stamp.image = newImg;
console.log('印章图像已重新加载');
};
newImg.src = stamp.stamp.src;
}
} finally {
stampCtx.restore();
}
}
// 绘制骑缝章部分
function drawSeamStampPart(seam, pageNum, isSelected) {
const { stamp, startPage, endPage, position, size, totalPages, offsetY } = seam;
// 检查印章是否有效且已加载
if (!stamp || !stamp.image || !stamp.loaded) {
console.error('无效的印章图像', stamp);
return;
}
const pageIndex = pageNum - startPage; // 当前是骑缝中的第几页
// 计算当前页应显示的印章部分比例
const segmentRatio = 1 / totalPages;
const canvasWidth = stampCanvas.width;
const canvasHeight = stampCanvas.height;
// 减少边距限制,增加可移动范围
const margin = 10; // 减小边距
const availableHeight = canvasHeight - margin * 2; // 可用高度减去上下边距
// 保存当前上下文状态
stampCtx.save();
// 根据位置计算印章绘制位置和裁剪区域
if (position === 'left' || position === 'right') {
// 左右方向的骑缝章
const x = position === 'left' ? 0 : canvasWidth - size * segmentRatio;
// 设置裁剪区域,添加边距和偏移量
stampCtx.beginPath();
stampCtx.rect(
x,
margin + offsetY, // 顶部添加边距和偏移量
size * segmentRatio,
availableHeight // 使用调整后的高度
);
stampCtx.clip();
try {
// 绘制印章(仅显示当前页应有的部分)
stampCtx.drawImage(
stamp.image,
x - pageIndex * size * segmentRatio,
margin + offsetY + (availableHeight - size) / 2, // 垂直居中并考虑边距和偏移量
size,
size
);
// 如果是选中状态,绘制边框
if (isSelected) {
stampCtx.strokeStyle = '#007bff';
stampCtx.lineWidth = 2;
stampCtx.strokeRect(
x - 5,
margin + offsetY - 5,
size * segmentRatio + 10,
availableHeight + 10
);
}
} catch (error) {
console.error('绘制骑缝章失败:', error);
// 尝试重新加载图像
if (stamp.src) {
const newImg = new Image();
newImg.onload = function() {
stamp.image = newImg;
console.log('印章图像已重新加载');
};
newImg.src = stamp.src;
}
}
}
stampCtx.restore();
}
// 页面导航
prevPageBtn.addEventListener('click', async function() {
if (currentPage > 1) {
currentPage--;
// 取消选中状态
selectedStampIndex = -1;
selectedSeamIndex = -1;
updateControlVisibility();
await renderPage(currentPage);
updatePageDisplay();
updatePageButtons();
}
});
nextPageBtn.addEventListener('click', async function() {
if (currentPage < totalPages) {
currentPage++;
// 取消选中状态
selectedStampIndex = -1;
selectedSeamIndex = -1;
updateControlVisibility();
await renderPage(currentPage);
updatePageDisplay();
updatePageButtons();
}
});
// 更新页码显示
function updatePageDisplay() {
pageNumDisplay.textContent = `${currentPage} / ${totalPages}`;
}
// 更新页面按钮状态
function updatePageButtons() {
prevPageBtn.disabled = currentPage <= 1;
nextPageBtn.disabled = currentPage >= totalPages;
}
// 更新控件显示状态
function updateControlVisibility() {
if (selectedStampIndex !== -1) {
stampControls.style.display = 'flex';
seamControls.style.display = 'none';
} else if (selectedSeamIndex !== -1) {
stampControls.style.display = 'none';
seamControls.style.display = 'flex';
} else {
stampControls.style.display = 'none';
seamControls.style.display = 'none';
}
}
// 鼠标按下事件 - 开始拖拽
let dragOffsetX = 0;
let dragOffsetY = 0;
combinedCanvas.addEventListener('mousedown', function(e) {
if (!pdfDoc) return;
const rect = combinedCanvas.getBoundingClientRect();
const scaleX = combinedCanvas.width / rect.width;
const scaleY = combinedCanvas.height / rect.height;
const clickX = (e.clientX - rect.left) * scaleX;
const clickY = (e.clientY - rect.top) * scaleY;
// 先检查是否点击了骑缝章
let clickedSeamIndex = -1;
for (let i = seamStamps.length - 1; i >= 0; i--) {
const seam = seamStamps[i];
if (currentPage >= seam.startPage && currentPage <= seam.endPage) {
const pageIndex = currentPage - seam.startPage;
const segmentRatio = 1 / seam.totalPages;
const margin = 10; // 减小边距以扩大可点击区域
// 计算骑缝章在当前页的位置和大小
let x, width;
if (seam.position === 'left') {
x = 0;
width = seam.size * segmentRatio;
} else {
x = combinedCanvas.width - seam.size * segmentRatio;
width = seam.size * segmentRatio;
}
// 检查点击是否在骑缝章区域内
if (clickX >= x && clickX <= x + width &&
clickY >= margin + seam.offsetY &&
clickY <= margin + seam.offsetY + (combinedCanvas.height - margin * 2)) {
clickedSeamIndex = i;
isDragging = true;
dragType = "seam";
// 记录初始Y坐标用于计算偏移
dragOffsetY = clickY - (margin + seam.offsetY);
break;
}
}
}
// 如果点击了骑缝章,处理骑缝章选择
if (clickedSeamIndex !== -1) {
selectedSeamIndex = clickedSeamIndex;
selectedStampIndex = -1;
// 更新控制面板位置
seamControls.style.left = `${e.clientX + 10}px`;
seamControls.style.top = `${e.clientY + 10}px`;
updateControlVisibility();
queueRenderStamps();
return;
}
// 检查是否点击了某个普通印章
let clickedIndex = -1;
for (let i = stamps.length - 1; i >= 0; i--) {
const stamp = stamps[i];
if (stamp.page === currentPage) {
const halfSize = stamp.size / 2;
const centerX = stamp.x + halfSize;
const centerY = stamp.y + halfSize;
// 考虑旋转的简化碰撞检测
const maxDist = halfSize * Math.sqrt(2);
const dist = Math.sqrt(Math.pow(clickX - centerX, 2) + Math.pow(clickY - centerY, 2));
if (dist <= maxDist) {
clickedIndex = i;
// 计算偏移量
dragOffsetX = clickX - stamp.x;
dragOffsetY = clickY - stamp.y;
isDragging = true;
dragType = "stamp";
break;
}
}
}
// 更新选中状态
if (clickedIndex !== -1) {
selectedStampIndex = clickedIndex;
selectedSeamIndex = -1;
// 更新控制面板位置
stampControls.style.left = `${e.clientX + 10}px`;
stampControls.style.top = `${e.clientY + 10}px`;
} else {
selectedStampIndex = -1;
selectedSeamIndex = -1;
}
updateControlVisibility();
queueRenderStamps();
});
// 鼠标移动事件 - 拖拽印章或骑缝章
combinedCanvas.addEventListener('mousemove', function(e) {
if (!isDragging) return;
const rect = combinedCanvas.getBoundingClientRect();
const scaleY = combinedCanvas.height / rect.height;
const mouseY = (e.clientY - rect.top) * scaleY;
// 拖拽普通印章
if (dragType === "stamp" && selectedStampIndex !== -1) {
const scaleX = combinedCanvas.width / rect.width;
const mouseX = (e.clientX - rect.left) * scaleX;
// 更新印章位置,考虑偏移量
stamps[selectedStampIndex].x = mouseX - dragOffsetX;
stamps[selectedStampIndex].y = mouseY - dragOffsetY;
queueRenderStamps();
}
// 拖拽骑缝章(只能上下移动)
else if (dragType === "seam" && selectedSeamIndex !== -1) {
const margin = 10; // 减小边距
const availableHeight = combinedCanvas.height - margin * 2;
const seam = seamStamps[selectedSeamIndex];
// 大幅扩大骑缝章可移动范围
const maxMoveRange = availableHeight * 0.4; // 允许移动到页面40%的高度范围
// 计算新的偏移量,设置更宽松的限制
let newOffsetY = mouseY - dragOffsetY - margin;
// 新的宽松限制,允许更大范围移动
newOffsetY = Math.max(newOffsetY, -maxMoveRange);
newOffsetY = Math.min(newOffsetY, maxMoveRange);
// 更新骑缝章位置
if (seam.offsetY !== newOffsetY) {
seam.offsetY = newOffsetY;
queueRenderStamps();
}
}
});
// 鼠标释放事件 - 结束拖拽
document.addEventListener('mouseup', function() {
isDragging = false;
dragType = null;
});
// 普通印章控制按钮事件
moveUpBtn.addEventListener('click', function() {
if (selectedStampIndex !== -1) {
stamps[selectedStampIndex].y -= 10;
queueRenderStamps();
}
});
moveDownBtn.addEventListener('click', function() {
if (selectedStampIndex !== -1) {
stamps[selectedStampIndex].y += 10;
queueRenderStamps();
}
});
moveLeftBtn.addEventListener('click', function() {
if (selectedStampIndex !== -1) {
stamps[selectedStampIndex].x -= 10;
queueRenderStamps();
}
});
moveRightBtn.addEventListener('click', function() {
if (selectedStampIndex !== -1) {
stamps[selectedStampIndex].x += 10;
queueRenderStamps();
}
});
rotateStampBtn.addEventListener('click', function() {
if (selectedStampIndex !== -1) {
stamps[selectedStampIndex].rotation = (stamps[selectedStampIndex].rotation + 15) % 360;
queueRenderStamps();
}
});
scaleUpBtn.addEventListener('click', function() {
if (selectedStampIndex !== -1) {
const newSize = stamps[selectedStampIndex].size + 10;
if (newSize <= 300) {
stamps[selectedStampIndex].size = newSize;
queueRenderStamps();
}
}
});
scaleDownBtn.addEventListener('click', function() {
if (selectedStampIndex !== -1) {
const newSize = stamps[selectedStampIndex].size - 10;
if (newSize >= 20) {
stamps[selectedStampIndex].size = newSize;
queueRenderStamps();
}
}
});
deleteStampBtn.addEventListener('click', function() {
if (selectedStampIndex !== -1) {
stamps.splice(selectedStampIndex, 1);
selectedStampIndex = -1;
updateControlVisibility();
queueRenderStamps();
}
});
// 骑缝章控制按钮事件 - 增加每次移动距离
seamMoveUpBtn.addEventListener('click', function() {
if (selectedSeamIndex !== -1) {
// 向上移动骑缝章,增加移动距离
const margin = 10;
const availableHeight = combinedCanvas.height - margin * 2;
const maxMoveRange = availableHeight * 0.4;
const seam = seamStamps[selectedSeamIndex];
// 每次点击移动更大距离
seam.offsetY = Math.max(seam.offsetY - 15, -maxMoveRange);
queueRenderStamps();
}
});
seamMoveDownBtn.addEventListener('click', function() {
if (selectedSeamIndex !== -1) {
// 向下移动骑缝章,增加移动距离
const margin = 10;
const availableHeight = combinedCanvas.height - margin * 2;
const maxMoveRange = availableHeight * 0.4;
const seam = seamStamps[selectedSeamIndex];
// 每次点击移动更大距离
seam.offsetY = Math.min(seam.offsetY + 15, maxMoveRange);
queueRenderStamps();
}
});
deleteSeamStampBtn.addEventListener('click', function() {
if (selectedSeamIndex !== -1) {
seamStamps.splice(selectedSeamIndex, 1);
selectedSeamIndex = -1;
updateControlVisibility();
queueRenderStamps();
}
});
// 下载PDF
downloadPdfBtn.addEventListener('click', async function() {
if (!pdfDoc) return;
showLoadingOverlay('正在生成PDF文件...');
try {
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'px',
format: 'a4'
});
const quality = parseFloat(qualitySettingSelect.value);
// 逐页处理
for (let i = 1; i <= totalPages; i++) {
if (i > 1) {
pdf.addPage();
}
// 渲染当前页和印章
await renderPage(i);
// 将画布内容添加到PDF
const imgData = combinedCanvas.toDataURL('image/jpeg', 0.95);
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
pdf.addImage(imgData, 'JPEG', 0, 0, pageWidth, pageHeight);
// 更新进度
updateLoadingProgress((i / totalPages) * 100, `正在处理第 ${i} 页 / 共 ${totalPages} 页`);
}
// 保存PDF
pdf.save('盖章后的文件.pdf');
hideLoadingOverlay();
} catch (error) {
console.error('生成PDF失败:', error);
hideLoadingOverlay();
alert('生成PDF失败: ' + error.message);
}
});
// 页面尺寸变更事件
pageSizeSelect.addEventListener('change', function() {
if (pdfDoc) {
// 清除缓存,因为尺寸变了
pdfCache.clear();
renderPage(currentPage);
}
});
// 工具函数:读取文件
function readFileAsync(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
// 初始化
window.addEventListener('load', function() {
renderStampItems();
initLibraries();
});
</script>
</body>
</html>