Files
tools/image_to_pdf.html
2025-12-09 15:14:11 +08:00

1349 lines
38 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>图片转PDF</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
margin: 20px;
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.main-content {
display: flex;
gap: 20px;
align-items: flex-start;
}
.left-panel {
flex: 1;
min-width: 0;
max-width: 600px;
}
.right-panel {
flex: 1;
min-width: 0;
max-width: 600px;
position: sticky;
top: 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
font-weight: 600;
color: #333;
font-size: 2rem;
}
.upload-area {
border: 2px dashed #3498db;
border-radius: 8px;
padding: 40px 20px;
text-align: center;
margin-bottom: 30px;
background: #f8f9fa;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-area:hover {
border-color: #2980b9;
background: #e3f2fd;
}
.upload-area.drag-over {
border-color: #27ae60;
background: #d4edda;
}
.upload-icon {
font-size: 48px;
color: #3498db;
margin-bottom: 15px;
}
.upload-text {
font-size: 16px;
color: #555;
margin-bottom: 10px;
}
.upload-hint {
font-size: 14px;
color: #888;
}
input[type="file"] {
display: none;
}
.file-select-btn {
background-color: #3498db;
color: white;
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: background-color 0.3s ease;
margin-top: 15px;
}
.file-select-btn:hover {
background-color: #2980b9;
}
.image-preview-container {
margin-top: 30px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.image-count {
font-size: 14px;
color: #666;
font-weight: normal;
}
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.image-item {
position: relative;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
background: #f8f9fa;
cursor: move;
transition: all 0.3s ease;
}
.image-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.image-item.dragging {
opacity: 0.5;
transform: rotate(2deg);
}
.image-preview {
width: 100%;
height: 120px;
object-fit: cover;
border-bottom: 1px solid #ddd;
}
.image-info {
padding: 8px;
font-size: 12px;
color: #666;
background: white;
}
.image-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
}
.image-size {
color: #888;
}
.image-actions {
position: absolute;
top: 5px;
right: 5px;
display: flex;
gap: 5px;
}
.image-action-btn {
width: 24px;
height: 24px;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
transition: all 0.3s ease;
}
.delete-btn {
background: rgba(231, 76, 60, 0.9);
color: white;
}
.delete-btn:hover {
background: #e74c3c;
}
.move-btn {
background: rgba(52, 152, 219, 0.9);
color: white;
}
.move-btn:hover {
background: #3498db;
}
.action-buttons {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
flex-wrap: wrap;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s ease;
min-width: 120px;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-primary:hover {
background-color: #2980b9;
}
.btn-secondary {
background-color: #95a5a6;
color: white;
}
.btn-secondary:hover {
background-color: #7f8c8d;
}
.btn-danger {
background-color: #e74c3c;
color: white;
}
.btn-danger:hover {
background-color: #c0392b;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.pdf-options {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.option-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.form-group input,
.form-group select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #3498db;
}
.progress-container {
display: none;
margin-top: 20px;
}
.progress-bar {
width: 100%;
height: 8px;
background: #ecf0f1;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3498db, #2980b9);
width: 0%;
transition: width 0.3s ease;
}
.progress-text {
text-align: center;
margin-top: 10px;
font-size: 14px;
color: #666;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #888;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 20px;
opacity: 0.5;
}
.empty-state-text {
font-size: 16px;
margin-bottom: 10px;
}
.empty-state-hint {
font-size: 14px;
color: #aaa;
}
/* PDF预览区域样式 */
.preview-container {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
border: 1px solid #e9ecef;
}
.preview-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
}
.preview-title {
font-size: 18px;
font-weight: 600;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
.preview-status {
font-size: 14px;
color: #666;
padding: 4px 8px;
background: #e9ecef;
border-radius: 4px;
}
.preview-status.ready {
background: #d4edda;
color: #155724;
}
.preview-status.generating {
background: #fff3cd;
color: #856404;
}
.preview-canvas-container {
background: white;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
position: relative;
margin-bottom: 15px;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
.preview-canvas {
max-width: 100%;
max-height: 500px;
display: block;
margin: 0 auto;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.preview-placeholder {
text-align: center;
color: #888;
padding: 40px 20px;
}
.preview-placeholder-icon {
font-size: 48px;
margin-bottom: 10px;
opacity: 0.5;
}
.preview-placeholder-text {
font-size: 14px;
line-height: 1.4;
}
.preview-controls {
display: flex;
gap: 10px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.preview-page-nav {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.preview-page-btn {
width: 28px;
height: 28px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.preview-page-btn:hover:not(:disabled) {
background: #f8f9fa;
border-color: #3498db;
}
.preview-page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.preview-zoom {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.preview-zoom-btn {
width: 28px;
height: 28px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.preview-zoom-btn:hover {
background: #f8f9fa;
border-color: #3498db;
}
.preview-zoom-slider {
width: 100px;
}
.preview-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #666;
margin-top: 10px;
padding: 8px;
background: white;
border-radius: 4px;
border: 1px solid #e9ecef;
}
@media (max-width: 1200px) {
.main-content {
flex-direction: column;
}
.left-panel,
.right-panel {
max-width: none;
position: static;
}
}
@media (max-width: 768px) {
.container {
margin: 10px;
padding: 15px;
}
.image-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.option-group {
grid-template-columns: 1fr;
}
.action-buttons {
flex-direction: column;
}
.btn {
width: 100%;
}
.preview-canvas-container {
min-height: 300px;
}
.preview-controls {
flex-direction: column;
gap: 15px;
}
.preview-info {
flex-direction: column;
gap: 5px;
text-align: center;
}
}
@media (max-width: 480px) {
.image-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
</head>
<body>
<div class="container">
<h1>图片转PDF纯前端后台不保存数据</h1>
<div class="main-content">
<!-- 左侧面板 -->
<div class="left-panel">
<!-- 上传区域 -->
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📷</div>
<div class="upload-text">点击或拖拽图片到这里</div>
<div class="upload-hint">支持 JPG、PNG、GIF、BMP、WEBP 格式</div>
<button class="file-select-btn">选择图片</button>
<input type="file" id="fileInput" multiple accept="image/*" />
</div>
<!-- PDF选项 -->
<div class="pdf-options">
<div class="option-group">
<div class="form-group">
<label for="pageSize">页面尺寸:</label>
<select id="pageSize">
<option value="a4" selected>A4 (210×297mm)</option>
<option value="a3">A3 (297×420mm)</option>
<option value="letter">Letter (216×279mm)</option>
<option value="legal">Legal (216×356mm)</option>
<option value="auto">自动适应图片</option>
</select>
</div>
<div class="form-group">
<label for="orientation">页面方向:</label>
<select id="orientation">
<option value="auto" selected>自动适应</option>
<option value="portrait">纵向</option>
<option value="landscape">横向</option>
</select>
</div>
<div class="form-group">
<label for="imageQuality">图片质量:</label>
<select id="imageQuality">
<option value="high">高质量</option>
<option value="medium" selected>中等质量</option>
<option value="low">低质量</option>
</select>
</div>
<div class="form-group">
<label for="margin">边距 (mm):</label>
<input type="number" id="margin" value="0" min="0" max="50" />
</div>
</div>
</div>
<!-- 图片预览容器 -->
<div
class="image-preview-container"
id="imagePreviewContainer"
style="display: none"
>
<div class="section-title">
<span>图片列表</span>
<span class="image-count" id="imageCount">0 张图片</span>
</div>
<div class="image-grid" id="imageGrid"></div>
</div>
<!-- 空状态 -->
<div class="empty-state" id="emptyState">
<div class="empty-state-icon">🖼️</div>
<div class="empty-state-text">还没有选择图片</div>
<div class="empty-state-hint">点击上方区域选择要转换的图片</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons" id="actionButtons" style="display: none">
<button class="btn btn-secondary" id="clearAllBtn">清空图片</button>
<button class="btn btn-primary" id="generatePdfBtn">生成PDF</button>
</div>
<!-- 进度条 -->
<div class="progress-container" id="progressContainer">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text" id="progressText">处理中...</div>
</div>
</div>
<!-- 右侧面板 - PDF预览 -->
<div class="right-panel">
<div class="preview-container">
<div class="preview-header">
<div class="preview-title">📄 PDF预览</div>
<div class="preview-status" id="previewStatus">等待图片</div>
</div>
<div class="preview-canvas-container" id="previewCanvasContainer">
<div class="preview-placeholder" id="previewPlaceholder">
<div class="preview-placeholder-icon">📄</div>
<div class="preview-placeholder-text">
添加图片后将显示PDF预览<br />
可以实时查看生成效果
</div>
</div>
<canvas
class="preview-canvas"
id="previewCanvas"
style="display: none"
></canvas>
</div>
<div
class="preview-controls"
id="previewControls"
style="display: none"
>
<div class="preview-page-nav">
<button
class="preview-page-btn"
id="prevPageBtn"
title="上一页"
>
</button>
<span id="pageInfo">1 / 1</span>
<button
class="preview-page-btn"
id="nextPageBtn"
title="下一页"
>
</button>
</div>
<div class="preview-zoom">
<button class="preview-zoom-btn" id="zoomOutBtn" title="缩小">
</button>
<input
type="range"
class="preview-zoom-slider"
id="zoomSlider"
min="50"
max="200"
value="100"
step="10"
/>
<button class="preview-zoom-btn" id="zoomInBtn" title="放大">
</button>
<span id="zoomLevel">100%</span>
</div>
</div>
<div class="preview-info" id="previewInfo" style="display: none">
<span id="pageSizeInfo">页面: A4</span>
<span id="imageCountInfo">0 张图片</span>
<span id="fileSizeInfo">约 0 KB</span>
</div>
</div>
</div>
</div>
</div>
<script>
// 全局变量
let uploadedImages = [];
let isDragging = false;
let currentPage = 0;
let currentZoom = 1;
let previewPages = [];
let previewUpdateTimeout = null;
// DOM元素
const uploadArea = document.getElementById("uploadArea");
const fileInput = document.getElementById("fileInput");
const imagePreviewContainer = document.getElementById(
"imagePreviewContainer",
);
const imageGrid = document.getElementById("imageGrid");
const imageCount = document.getElementById("imageCount");
const emptyState = document.getElementById("emptyState");
const actionButtons = document.getElementById("actionButtons");
const generatePdfBtn = document.getElementById("generatePdfBtn");
const clearAllBtn = document.getElementById("clearAllBtn");
const progressContainer = document.getElementById("progressContainer");
const progressFill = document.getElementById("progressFill");
const progressText = document.getElementById("progressText");
// 预览相关DOM元素
const previewStatus = document.getElementById("previewStatus");
const previewPlaceholder = document.getElementById("previewPlaceholder");
const previewCanvas = document.getElementById("previewCanvas");
const previewControls = document.getElementById("previewControls");
const previewInfo = document.getElementById("previewInfo");
const prevPageBtn = document.getElementById("prevPageBtn");
const nextPageBtn = document.getElementById("nextPageBtn");
const pageInfo = document.getElementById("pageInfo");
const zoomOutBtn = document.getElementById("zoomOutBtn");
const zoomInBtn = document.getElementById("zoomInBtn");
const zoomSlider = document.getElementById("zoomSlider");
const zoomLevel = document.getElementById("zoomLevel");
const pageSizeInfo = document.getElementById("pageSizeInfo");
const imageCountInfo = document.getElementById("imageCountInfo");
const fileSizeInfo = document.getElementById("fileSizeInfo");
// 事件监听器
uploadArea.addEventListener("click", () => fileInput.click());
fileInput.addEventListener("change", handleFileSelect);
generatePdfBtn.addEventListener("click", generatePdf);
clearAllBtn.addEventListener("click", clearAllImages);
// 预览控制事件监听器
prevPageBtn.addEventListener("click", () => navigatePage(-1));
nextPageBtn.addEventListener("click", () => navigatePage(1));
zoomOutBtn.addEventListener("click", () => changeZoom(-0.1));
zoomInBtn.addEventListener("click", () => changeZoom(0.1));
zoomSlider.addEventListener("input", (e) => {
currentZoom = e.target.value / 100;
zoomLevel.textContent = e.target.value + "%";
renderPreview();
});
// 监听PDF选项变化
document
.getElementById("pageSize")
.addEventListener("change", schedulePreviewUpdate);
document
.getElementById("orientation")
.addEventListener("change", schedulePreviewUpdate);
document
.getElementById("margin")
.addEventListener("input", schedulePreviewUpdate);
// 拖拽功能
uploadArea.addEventListener("dragover", handleDragOver);
uploadArea.addEventListener("dragleave", handleDragLeave);
uploadArea.addEventListener("drop", handleDrop);
function handleDragOver(e) {
e.preventDefault();
uploadArea.classList.add("drag-over");
}
function handleDragLeave(e) {
e.preventDefault();
uploadArea.classList.remove("drag-over");
}
function handleDrop(e) {
e.preventDefault();
uploadArea.classList.remove("drag-over");
const files = Array.from(e.dataTransfer.files).filter((file) =>
file.type.startsWith("image/"),
);
if (files.length > 0) {
processFiles(files);
}
}
function handleFileSelect(e) {
const files = Array.from(e.target.files);
processFiles(files);
// 清空input允许重复选择同一文件
e.target.value = "";
}
function processFiles(files) {
files.forEach((file) => {
if (!file.type.startsWith("image/")) {
alert(`文件 ${file.name} 不是有效的图片格式`);
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const imageObj = {
id: Date.now() + Math.random(),
name: file.name,
size: formatFileSize(file.size),
dataUrl: e.target.result,
file: file,
};
uploadedImages.push(imageObj);
updateUI();
};
reader.readAsDataURL(file);
});
}
function formatFileSize(bytes) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
function updateUI() {
const hasImages = uploadedImages.length > 0;
// 显示/隐藏相关元素
emptyState.style.display = hasImages ? "none" : "block";
imagePreviewContainer.style.display = hasImages ? "block" : "none";
actionButtons.style.display = hasImages ? "flex" : "none";
// 更新图片计数
imageCount.textContent = `${uploadedImages.length} 张图片`;
// 渲染图片网格
renderImageGrid();
// 更新预览
if (hasImages) {
schedulePreviewUpdate();
} else {
resetPreview();
}
}
function renderImageGrid() {
imageGrid.innerHTML = "";
uploadedImages.forEach((image, index) => {
const imageItem = document.createElement("div");
imageItem.className = "image-item";
imageItem.draggable = true;
imageItem.dataset.index = index;
imageItem.innerHTML = `
<img src="${image.dataUrl}" alt="${image.name}" class="image-preview">
<div class="image-info">
<div class="image-name">${image.name}</div>
<div class="image-size">${image.size}</div>
</div>
<div class="image-actions">
<button class="image-action-btn move-btn" onclick="moveImage(${index}, -1)" title="上移">↑</button>
<button class="image-action-btn move-btn" onclick="moveImage(${index}, 1)" title="下移">↓</button>
<button class="image-action-btn delete-btn" onclick="deleteImage(${index})" title="删除">×</button>
</div>
`;
// 拖拽排序功能
imageItem.addEventListener("dragstart", handleDragStart);
imageItem.addEventListener("dragend", handleDragEnd);
imageItem.addEventListener("dragover", handleItemDragOver);
imageItem.addEventListener("drop", handleItemDrop);
imageGrid.appendChild(imageItem);
});
}
let draggedElement = null;
let draggedIndex = null;
function handleDragStart(e) {
draggedElement = this;
draggedIndex = parseInt(this.dataset.index);
this.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
}
function handleDragEnd(e) {
this.classList.remove("dragging");
draggedElement = null;
draggedIndex = null;
}
function handleItemDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = "move";
return false;
}
function handleItemDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
if (draggedElement !== this) {
const dropIndex = parseInt(this.dataset.index);
const draggedImage = uploadedImages[draggedIndex];
// 重新排序数组
uploadedImages.splice(draggedIndex, 1);
uploadedImages.splice(dropIndex, 0, draggedImage);
updateUI();
}
return false;
}
function moveImage(index, direction) {
const newIndex = index + direction;
if (newIndex < 0 || newIndex >= uploadedImages.length) {
return;
}
// 交换位置
[uploadedImages[index], uploadedImages[newIndex]] = [
uploadedImages[newIndex],
uploadedImages[index],
];
updateUI();
}
function deleteImage(index) {
uploadedImages.splice(index, 1);
updateUI();
}
function clearAllImages() {
if (confirm("确定要清空所有图片吗?")) {
uploadedImages = [];
fileInput.value = "";
updateUI();
}
}
// --- PDF预览功能 ---
function schedulePreviewUpdate() {
if (previewUpdateTimeout) {
clearTimeout(previewUpdateTimeout);
}
previewUpdateTimeout = setTimeout(() => {
updatePreview();
}, 500);
}
function resetPreview() {
currentPage = 0;
previewPages = [];
previewStatus.textContent = "等待图片";
previewStatus.className = "preview-status";
previewPlaceholder.style.display = "block";
previewCanvas.style.display = "none";
previewControls.style.display = "none";
previewInfo.style.display = "none";
}
async function updatePreview() {
if (uploadedImages.length === 0) {
resetPreview();
return;
}
try {
previewStatus.textContent = "生成预览中...";
previewStatus.className = "preview-status generating";
previewPages = [];
// 获取用户设置
const pageSizeSelect = document.getElementById("pageSize").value;
const orientationSelect =
document.getElementById("orientation").value;
const margin = parseInt(document.getElementById("margin").value) || 0;
// 生成每页预览
for (let i = 0; i < uploadedImages.length; i++) {
const imageObj = uploadedImages[i];
const img = await loadImagePromise(imageObj.dataUrl);
// 确定页面方向
let pageOrientation = img.width > img.height ? "l" : "p";
if (orientationSelect !== "auto") {
pageOrientation = orientationSelect === "landscape" ? "l" : "p";
}
// 确定页面尺寸
let pageWidth, pageHeight;
if (pageSizeSelect === "auto") {
pageWidth = 210;
pageHeight = 297;
// 如果是横向,交换宽高
if (pageOrientation === "l") {
[pageWidth, pageHeight] = [pageHeight, pageWidth];
}
} else {
const pageSizes = {
a4: { width: 210, height: 297 },
a3: { width: 297, height: 420 },
letter: { width: 216, height: 279 },
legal: { width: 216, height: 356 },
};
const size = pageSizes[pageSizeSelect] || pageSizes["a4"];
pageWidth = pageOrientation === "l" ? size.height : size.width;
pageHeight = pageOrientation === "l" ? size.width : size.height;
}
previewPages.push({
image: img,
pageWidth: pageWidth,
pageHeight: pageHeight,
margin: margin,
});
}
// 显示预览控件
previewPlaceholder.style.display = "none";
previewCanvas.style.display = "block";
previewControls.style.display = "flex";
previewInfo.style.display = "flex";
// 更新状态
previewStatus.textContent = "预览就绪";
previewStatus.className = "preview-status ready";
// 更新信息
updatePreviewInfo();
// 确保当前页在有效范围内
if (currentPage >= previewPages.length) {
currentPage = previewPages.length - 1;
}
// 渲染当前页
renderPreview();
} catch (error) {
console.error("预览生成失败:", error);
previewStatus.textContent = "预览失败";
previewStatus.className = "preview-status";
}
}
function updatePreviewInfo() {
const pageSize = document.getElementById("pageSize").value;
const pageSizeText =
pageSize === "auto" ? "自动" : pageSize.toUpperCase();
pageSizeInfo.textContent = `页面: ${pageSizeText}`;
imageCountInfo.textContent = `${previewPages.length} 张图片`;
// 估算文件大小
const estimatedSize = previewPages.length * 500; // 每页约500KB
if (estimatedSize < 1024) {
fileSizeInfo.textContent = `${estimatedSize} KB`;
} else {
fileSizeInfo.textContent = `${(estimatedSize / 1024).toFixed(1)} MB`;
}
// 更新页面导航
pageInfo.textContent = `${currentPage + 1} / ${previewPages.length}`;
prevPageBtn.disabled = currentPage === 0;
nextPageBtn.disabled = currentPage === previewPages.length - 1;
}
function renderPreview() {
if (previewPages.length === 0) return;
const pageData = previewPages[currentPage];
const canvas = previewCanvas;
const ctx = canvas.getContext("2d");
// 设置canvas尺寸考虑缩放
const scale = Math.min(2, Math.max(0.5, currentZoom));
canvas.width = pageData.pageWidth * scale * 3.78; // mm to pixels at 96dpi
canvas.height = pageData.pageHeight * scale * 3.78;
// 清空画布
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 计算内容区域
const contentWidth =
(pageData.pageWidth - pageData.margin * 2) * scale * 3.78;
const contentHeight =
(pageData.pageHeight - pageData.margin * 2) * scale * 3.78;
const offsetX = pageData.margin * scale * 3.78;
const offsetY = pageData.margin * scale * 3.78;
// 计算图片缩放和位置
const imgRatio = pageData.image.width / pageData.image.height;
const contentRatio = contentWidth / contentHeight;
let drawWidth, drawHeight;
if (imgRatio > contentRatio) {
drawWidth = contentWidth;
drawHeight = contentWidth / imgRatio;
} else {
drawHeight = contentHeight;
drawWidth = contentHeight * imgRatio;
}
const drawX = offsetX + (contentWidth - drawWidth) / 2;
const drawY = offsetY + (contentHeight - drawHeight) / 2;
// 绘制图片
ctx.drawImage(pageData.image, drawX, drawY, drawWidth, drawHeight);
// 绘制边框(可选)
ctx.strokeStyle = "#e0e0e0";
ctx.lineWidth = 1;
ctx.strokeRect(0, 0, canvas.width, canvas.height);
}
function navigatePage(direction) {
const newPage = currentPage + direction;
if (newPage >= 0 && newPage < previewPages.length) {
currentPage = newPage;
renderPreview();
updatePreviewInfo();
}
}
function changeZoom(delta) {
const newZoom = Math.max(0.5, Math.min(2, currentZoom + delta));
currentZoom = newZoom;
const percentage = Math.round(newZoom * 100);
zoomLevel.textContent = percentage + "%";
zoomSlider.value = percentage;
renderPreview();
}
// --- 核心逻辑修改部分 ---
// 辅助函数:将图片加载封装为 Promise以便提前获取尺寸
function loadImagePromise(dataUrl) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = dataUrl;
});
}
async function generatePdf() {
if (uploadedImages.length === 0) {
alert("请先选择图片");
return;
}
try {
showProgress(true);
updateProgress(0, "准备生成PDF...");
// 获取用户设置
const pageSizeSelect = document.getElementById("pageSize").value;
const orientationSelect =
document.getElementById("orientation").value;
const imageQuality = document.getElementById("imageQuality").value;
const margin = parseInt(document.getElementById("margin").value) || 0;
const { jsPDF } = window.jspdf;
let pdf = null;
// 遍历处理每一张图片
for (let i = 0; i < uploadedImages.length; i++) {
updateProgress(
(i / uploadedImages.length) * 90,
`正在处理第 ${i + 1}/${uploadedImages.length} 张图片...`,
);
const imageObj = uploadedImages[i];
const img = await loadImagePromise(imageObj.dataUrl);
// 1. 确定当前页面的方向 (Portrait 'p' 或 Landscape 'l')
let pageOrientation = "p"; // 默认纵向
if (orientationSelect === "auto") {
// 自适应:如果图片宽 > 高,则使用横向
pageOrientation = img.width > img.height ? "l" : "p";
} else {
// 用户强制指定
pageOrientation = orientationSelect === "landscape" ? "l" : "p";
}
// 2. 确定页面尺寸格式 (如 'a4', 'a3' 或自定义尺寸)
let format = pageSizeSelect;
if (pageSizeSelect === "auto") {
// 如果页面完全适应图片,这里我们给一个动态的尺寸
// 为了方便打印通常我们还是会映射到标准纸张或者直接用图片像素转mm
// 这里实现将图片大小按比例映射到A4的宽度或高度基准或者直接自定义大小
// 简化逻辑:如果是 auto我们用 A4 作为基准,但是完全贴合比例
format = "a4";
}
// 3. 添加页面或初始化 PDF
if (i === 0) {
// 第一张图:初始化 PDF 实例
// 注意jsPDF 初始化时就需要指定方向,这样第一页才正确
pdf = new jsPDF({
orientation: pageOrientation,
unit: "mm",
format: format,
});
} else {
// 后续图片:添加新页面,指定该页面的方向
pdf.addPage(format, pageOrientation);
}
// 4. 计算图片在页面中的绘制尺寸和位置
// 获取当前页面的物理宽高 (jsPDF 会根据 orientation 自动调整 internal.pageSize 的宽高)
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
// 可用绘图区域 (减去边距)
const contentWidth = pageWidth - margin * 2;
const contentHeight = pageHeight - margin * 2;
// 计算缩放比例:保持长宽比,填满可用区域
// Math.min 确保图片完全包含在页面内 (contain)
const widthRatio = contentWidth / img.width;
const heightRatio = contentHeight / img.height;
const scaleFactor = Math.min(widthRatio, heightRatio);
const finalWidth = img.width * scaleFactor;
const finalHeight = img.height * scaleFactor;
// 居中计算
const x = (pageWidth - finalWidth) / 2;
const y = (pageHeight - finalHeight) / 2;
// 5. 绘制图片
let jpegQuality = 0.8;
if (imageQuality === "high") jpegQuality = 0.95;
if (imageQuality === "low") jpegQuality = 0.6;
pdf.addImage(
imageObj.dataUrl,
"JPEG",
x,
y,
finalWidth,
finalHeight,
undefined,
"FAST", // 压缩速度
0, // 旋转角度
);
}
updateProgress(95, "正在保存PDF...");
const fileName = `images_${new Date().getTime()}.pdf`;
pdf.save(fileName);
updateProgress(100, "PDF生成完成");
setTimeout(() => {
showProgress(false);
}, 2000);
} catch (error) {
console.error("生成PDF时出错:", error);
alert("生成PDF时出错: " + error.message);
showProgress(false);
}
}
function showProgress(show) {
progressContainer.style.display = show ? "block" : "none";
generatePdfBtn.disabled = show;
clearAllBtn.disabled = show;
}
function updateProgress(percent, text) {
progressFill.style.width = percent + "%";
progressText.textContent = text;
}
// 初始化
updateUI();
</script>
</body>
</html>