写在前面
这是一个直观易用的图片切割工具,用户上传长图后自动分割成多个不超过5MB的片段,保持原始宽度不变,自己用的,顺带发表一下,不喜勿喷!
主要有两个小工具,一个针对长图智能切割,一个直接上传pdf之后导出切割后的图片;
长图智能切割并导出
设计思路
-
创建简洁的UI,包含上传区域和结果展示区
-
实现智能分割算法,保持原始宽度,动态计算高度
-
确保每个片段不超过5MB
-
使用Canvas进行图片处理和压缩
-
添加下载功能,支持下载所有切割后的图片
长图切割效果预览
图片切割最终实现代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能图片切割工具</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
color: #fff;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
padding: 30px 0;
margin-bottom: 30px;
background: rgba(0, 0, 0, 0.3);
border-radius: 15px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
h1 {
font-size: 2.5rem;
margin-bottom: 15px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
max-width: 700px;
margin: 0 auto;
line-height: 1.6;
}
.card {
background: rgba(255, 255, 255, 0.15);
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
.upload-area {
border: 3px dashed rgba(255, 255, 255, 0.4);
border-radius: 10px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.upload-area:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.7);
}
.upload-area i {
font-size: 60px;
margin-bottom: 20px;
display: block;
color: rgba(255, 255, 255, 0.8);
}
.upload-area h2 {
margin-bottom: 15px;
}
.upload-area p {
margin-bottom: 20px;
opacity: 0.8;
}
.btn {
background: linear-gradient(to right, #ff416c, #ff4b2b);
color: white;
border: none;
padding: 12px 30px;
font-size: 1.1rem;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
display: inline-block;
box-shadow: 0 4px 15px rgba(255, 75, 43, 0.4);
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(255, 75, 43, 0.6);
}
.btn:active {
transform: translateY(1px);
}
.btn-secondary {
background: linear-gradient(to right, #2193b0, #6dd5ed);
box-shadow: 0 4px 15px rgba(33, 147, 176, 0.4);
}
.btn-secondary:hover {
box-shadow: 0 6px 20px rgba(33, 147, 176, 0.6);
}
.btn:disabled {
background: #999;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
input[type="file"] {
display: none;
}
.preview-container {
display: none;
}
.image-info {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
background: rgba(0, 0, 0, 0.2);
padding: 15px;
border-radius: 10px;
}
.image-info div {
text-align: center;
flex: 1;
}
.image-info h3 {
font-size: 1.1rem;
margin-bottom: 8px;
opacity: 0.8;
}
.image-info p {
font-size: 1.3rem;
font-weight: bold;
}
.results-container {
display: none;
margin-top: 30px;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.slices-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.slice-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease;
}
.slice-card:hover {
transform: translateY(-5px);
}
.slice-img {
width: 100%;
display: block;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.slice-info {
padding: 15px;
text-align: center;
}
.slice-name {
font-weight: bold;
margin-bottom: 8px;
font-size: 1.1rem;
}
.slice-size {
font-size: 0.9rem;
opacity: 0.8;
}
.progress-container {
margin: 20px 0;
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(to right, #00b09b, #96c93d);
width: 0%;
transition: width 0.5s ease;
}
.status-text {
text-align: center;
margin: 10px 0;
font-style: italic;
opacity: 0.8;
}
.loading {
text-align: center;
padding: 30px;
display: none;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
margin: 0 auto 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.action-buttons {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 20px;
flex-wrap: wrap;
}
footer {
text-align: center;
margin-top: 40px;
padding: 20px;
font-size: 0.9rem;
opacity: 0.7;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.card {
padding: 20px;
}
.image-info {
flex-direction: column;
gap: 15px;
}
.action-buttons {
flex-direction: column;
align-items: center;
}
.btn {
width: 100%;
max-width: 300px;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>智能图片切割工具</h1>
<p class="subtitle">上传长图,自动切割为多个不超过5MB的片段,保持原始宽度不变,尽可能减少切割数量</p>
</header>
<main>
<div class="card">
<div id="upload-section">
<div class="upload-area" id="drop-area">
<i>📁</i>
<h2>上传您的图片</h2>
<p>点击或拖放图片到此处<br>支持JPG、PNG等格式,图片大小无限制</p>
<button class="btn" id="select-btn">选择图片</button>
<input type="file" id="file-input" accept="image/*">
</div>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p id="status-text">处理中,请稍候...</p>
<div class="progress-container">
<div class="progress-bar" id="progress-bar"></div>
</div>
</div>
<div class="preview-container" id="preview-section">
<div class="image-info">
<div>
<h3>原始图片尺寸</h3>
<p id="original-dimensions">-</p>
</div>
<div>
<h3>原始文件大小</h3>
<p id="original-size">-</p>
</div>
<div>
<h3>切割片段数量</h3>
<p id="slice-count">-</p>
</div>
</div>
<div class="action-buttons">
<button class="btn" id="process-btn">开始切割图片</button>
<button class="btn btn-secondary" id="reset-btn">重新选择图片</button>
</div>
</div>
</div>
<div class="card results-container" id="results-section">
<div class="results-header">
<h2>切割结果</h2>
<button class="btn" id="download-all">下载全部图片</button>
</div>
<div class="slices-container" id="slices-container">
<!-- 切割后的图片将在这里展示 -->
</div>
</div>
</main>
<footer>
<p>© 2025 智能图片切割工具 | 使用Canvas技术实现 | 所有图片处理均在浏览器完成</p>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 获取DOM元素
const fileInput = document.getElementById('file-input');
const selectBtn = document.getElementById('select-btn');
const dropArea = document.getElementById('drop-area');
const processBtn = document.getElementById('process-btn');
const resetBtn = document.getElementById('reset-btn');
const downloadAllBtn = document.getElementById('download-all');
const previewSection = document.getElementById('preview-section');
const resultsSection = document.getElementById('results-section');
const loadingSection = document.getElementById('loading');
const statusText = document.getElementById('status-text');
const progressBar = document.getElementById('progress-bar');
const slicesContainer = document.getElementById('slices-container');
// 存储原始图片和切割结果
let originalImage = null;
let imageSlices = [];
// 事件绑定
selectBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileSelect);
dropArea.addEventListener('dragover', handleDragOver);
dropArea.addEventListener('drop', handleDrop);
processBtn.addEventListener('click', processImage);
resetBtn.addEventListener('click', resetApp);
downloadAllBtn.addEventListener('click', downloadAllSlices);
// 处理文件选择
function handleFileSelect(e) {
const file = e.target.files[0];
if (file && file.type.match('image.*')) {
loadImage(file);
}
}
// 处理拖放
function handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
dropArea.style.borderColor = 'rgba(255, 255, 255, 0.7)';
dropArea.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
}
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
dropArea.style.borderColor = 'rgba(255, 255, 255, 0.4)';
dropArea.style.backgroundColor = '';
const file = e.dataTransfer.files[0];
if (file && file.type.match('image.*')) {
loadImage(file);
}
}
// 加载图片
function loadImage(file) {
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
originalImage = {
element: img,
file: file,
width: img.width,
height: img.height,
size: file.size
};
// 显示图片信息
document.getElementById('original-dimensions').textContent =
`${img.width} × ${img.height} 像素`;
document.getElementById('original-size').textContent =
formatFileSize(file.size);
document.getElementById('slice-count').textContent =
calculateSliceCount(img.height, file.size);
// 显示预览区域
previewSection.style.display = 'block';
resultsSection.style.display = 'none';
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// 计算切割数量
function calculateSliceCount(height, fileSize) {
// 简单估算:假设文件大小与高度成正比
const maxSize = 5 * 1024 * 1024; // 5MB
const minSlices = Math.ceil(fileSize / maxSize);
// 高度切割的最小单位(至少保留100像素高度)
const minHeight = 100;
const maxSliceHeight = Math.max(minHeight, Math.floor(height / minSlices));
// 实际切片数
const sliceCount = Math.ceil(height / maxSliceHeight);
return sliceCount;
}
// 处理图片切割
async function processImage() {
if (!originalImage) return;
// 显示加载状态
loadingSection.style.display = 'block';
previewSection.style.display = 'none';
resultsSection.style.display = 'none';
// 清除之前的切割结果
imageSlices = [];
slicesContainer.innerHTML = '';
try {
// 计算切割参数
const maxSize = 4.8 * 1024 * 1024; // 4.8MB (预留空间)
const originalWidth = originalImage.width;
const originalHeight = originalImage.height;
// 初始切片高度(基于原始大小比例)
let sliceHeight = Math.floor(originalHeight * (maxSize / originalImage.size));
// 确保最小高度
sliceHeight = Math.max(100, sliceHeight);
// 计算切片数量
const sliceCount = Math.ceil(originalHeight / sliceHeight);
// 更新状态
statusText.textContent = `准备切割图片为 ${sliceCount} 个片段...`;
progressBar.style.width = '0%';
// 创建临时canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = originalWidth;
// 处理每个切片
for (let i = 0; i < sliceCount; i++) {
// 更新进度
const progress = Math.floor((i / sliceCount) * 100);
progressBar.style.width = `${progress}%`;
statusText.textContent = `处理片段 ${i+1}/${sliceCount}...`;
// 计算当前切片的y坐标和高度
const y = i * sliceHeight;
const h = Math.min(sliceHeight, originalHeight - y);
// 设置canvas高度
canvas.height = h;
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制切片
ctx.drawImage(
originalImage.element,
0, y, originalWidth, h,
0, 0, originalWidth, h
);
// 获取Blob并调整质量
let quality = 0.9;
let blob = null;
do {
// 将canvas转为Blob
blob = await new Promise(resolve =>
canvas.toBlob(resolve, 'image/jpeg', quality)
);
// 如果文件太大,降低质量
if (blob.size > maxSize && quality > 0.5) {
quality -= 0.1;
} else {
break;
}
} while (quality > 0.5);
// 创建切片对象
const slice = {
index: i,
width: originalWidth,
height: h,
blob: blob,
url: URL.createObjectURL(blob),
size: blob.size
};
imageSlices.push(slice);
}
// 显示结果
showResults();
} catch (error) {
console.error('图片处理失败:', error);
statusText.textContent = '处理失败,请重试';
} finally {
// 隐藏加载状态
loadingSection.style.display = 'none';
}
}
// 显示切割结果
function showResults() {
// 更新切片数量显示
document.getElementById('slice-count').textContent = imageSlices.length;
// 显示结果区域
resultsSection.style.display = 'block';
previewSection.style.display = 'block';
// 清空容器
slicesContainer.innerHTML = '';
// 添加每个切片
imageSlices.forEach((slice, index) => {
const sliceElement = document.createElement('div');
sliceElement.className = 'slice-card';
sliceElement.innerHTML = `
<img src="${slice.url}" alt="图片片段 ${index+1}" class="slice-img">
<div class="slice-info">
<div class="slice-name">片段 #${index+1}</div>
<div class="slice-size">${formatFileSize(slice.size)} | ${slice.width}×${slice.height}</div>
</div>
`;
// 添加点击下载事件
sliceElement.addEventListener('click', () => {
downloadSlice(slice, index);
});
slicesContainer.appendChild(sliceElement);
});
}
// 下载单个切片
function downloadSlice(slice, index) {
const a = document.createElement('a');
a.href = slice.url;
a.download = `image-slice-${index+1}.jpg`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// 下载所有切片
function downloadAllSlices() {
imageSlices.forEach((slice, index) => {
setTimeout(() => {
downloadSlice(slice, index);
}, index * 300); // 避免同时下载导致浏览器阻塞
});
}
// 重置应用
function resetApp() {
// 清除所有状态
originalImage = null;
imageSlices = [];
// 释放URL对象
imageSlices.forEach(slice => URL.revokeObjectURL(slice.url));
// 重置UI
fileInput.value = '';
previewSection.style.display = 'none';
resultsSection.style.display = 'none';
slicesContainer.innerHTML = '';
}
// 辅助函数:格式化文件大小
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
else return (bytes / 1048576).toFixed(1) + ' MB';
}
});
</script>
</body>
</html>
PDF智能转换并切割图片
自己不想办理wps会员,所以不想使用wps转换成长图再使用以上的代码分割,所以补充了一下,直接上传pdf,可以直接下载,也可以打包下载!
前端库直接使用cdn链接,当然下载下来放在本地也可以!
功能说明
-
PDF文件上传:支持点击上传或拖放PDF文件
-
智能图片分割:
-
将PDF转换为高质量图片
-
自动分割图片确保每张图片不超过5MB
-
保持原始宽度不变,只调整高度
-
尽量使图片数量最小化
-
-
结果展示:
-
显示生成的所有图片预览
-
展示每张图片的尺寸和大小信息
-
-
下载功能:
-
可下载单张图片
-
可打包下载所有图片(ZIP格式)
-
-
状态显示:
-
显示文件处理状态
-
显示生成图片数量和总大小
-
使用说明
-
点击"选择PDF文件"按钮或拖放PDF文件到上传区域
-
等待处理完成(处理时间取决于PDF大小和页数)
-
查看生成的图片预览
-
点击单张图片下方的"下载"按钮下载该图片
-
点击"下载全部图片"按钮打包下载所有图片
-
点击"重置工具"按钮重新开始
技术实现
-
使用pdf.js库处理PDF文件
-
使用Canvas渲染PDF页面
-
智能分割算法确保图片不超过5MB
-
使用JSZip库打包下载所有图片
-
响应式设计,适配各种设备
这个工具完全在浏览器端运行,无需服务器支持,确保您的文件隐私安全。
PDF智能转换并切割图片效果预览
PDF智能转换并切割图片全代码
<!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/pdf.js/3.4.120/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
min-height: 100vh;
padding: 20px;
color: #333;
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 100%;
max-width: 1200px;
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.4);
overflow: hidden;
}
header {
background: linear-gradient(to right, #2c3e50, #4a6491);
color: white;
padding: 25px 40px;
text-align: center;
position: relative;
overflow: hidden;
}
header::before {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 70%);
transform: rotate(30deg);
}
h1 {
font-size: 2.8rem;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
position: relative;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.header-subtitle {
font-size: 1.2rem;
opacity: 0.9;
max-width: 800px;
margin: 0 auto;
line-height: 1.6;
position: relative;
}
.main-content {
padding: 30px;
display: flex;
flex-direction: column;
gap: 30px;
}
.upload-section {
background: linear-gradient(135deg, #f8f9ff, #eef2f9);
border-radius: 12px;
padding: 40px 30px;
text-align: center;
border: 3px dashed #4a6491;
transition: all 0.4s ease;
position: relative;
overflow: hidden;
}
.upload-section:hover {
background: linear-gradient(135deg, #edf2ff, #e0e9ff);
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(74, 100, 145, 0.2);
}
.upload-icon {
font-size: 5rem;
color: #4a6491;
margin-bottom: 25px;
text-shadow: 0 3px 6px rgba(0,0,0,0.1);
position: relative;
}
.upload-text {
font-size: 1.6rem;
margin-bottom: 25px;
color: #2c3e50;
font-weight: 600;
}
.upload-subtext {
font-size: 1.1rem;
color: #5a6b8c;
max-width: 700px;
margin: 0 auto 30px;
line-height: 1.7;
}
.file-input {
display: none;
}
.upload-btn {
background: linear-gradient(to right, #4a6491, #3a5680);
color: white;
border: none;
padding: 18px 45px;
font-size: 1.3rem;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 15px;
box-shadow: 0 6px 20px rgba(74, 100, 145, 0.4);
position: relative;
overflow: hidden;
z-index: 1;
}
.upload-btn::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to right, #3a5680, #2c3e50);
z-index: -1;
opacity: 0;
transition: opacity 0.3s ease;
}
.upload-btn:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(74, 100, 145, 0.6);
}
.upload-btn:hover::before {
opacity: 1;
}
.stats-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 30px;
}
.stat-card {
background: linear-gradient(135deg, #eef2f7, #f8f9ff);
border-radius: 12px;
padding: 25px;
text-align: center;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
transition: all 0.3s ease;
border: 1px solid #e0e6f0;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
}
.stat-title {
font-size: 1.1rem;
color: #5a6b8c;
margin-bottom: 15px;
font-weight: 600;
}
.stat-value {
font-size: 2.2rem;
font-weight: 700;
color: #4a6491;
margin-bottom: 5px;
}
.stat-subtext {
font-size: 1rem;
color: #7a8aa3;
}
.results-section {
display: none;
flex-direction: column;
gap: 25px;
margin-top: 20px;
}
.section-title {
font-size: 1.8rem;
color: #2c3e50;
padding-bottom: 15px;
border-bottom: 2px solid #e0e6f0;
margin-bottom: 25px;
display: flex;
align-items: center;
gap: 15px;
}
.images-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 30px;
}
.image-card {
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
transition: all 0.4s ease;
position: relative;
border: 1px solid #e0e6f0;
}
.image-card:hover {
transform: translateY(-8px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
}
.image-preview {
width: 100%;
height: 280px;
object-fit: contain;
background: #f8f9ff;
border-bottom: 1px solid #e0e6f0;
padding: 15px;
}
.image-info {
padding: 20px;
background: #f8f9ff;
}
.image-title {
font-weight: 700;
margin-bottom: 10px;
color: #2c3e50;
font-size: 1.3rem;
}
.image-details {
display: flex;
justify-content: space-between;
font-size: 1rem;
color: #5a6b8c;
margin-bottom: 15px;
}
.image-pages {
background: #eef2f7;
padding: 8px 15px;
border-radius: 20px;
font-size: 0.95rem;
color: #4a6491;
font-weight: 600;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 25px;
margin-top: 30px;
flex-wrap: wrap;
}
.action-btn {
background: linear-gradient(to right, #4a6491, #3a5680);
color: white;
border: none;
padding: 18px 40px;
font-size: 1.2rem;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 12px;
min-width: 250px;
justify-content: center;
box-shadow: 0 6px 20px rgba(74, 100, 145, 0.4);
position: relative;
overflow: hidden;
z-index: 1;
}
.action-btn::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to right, #3a5680, #2c3e50);
z-index: -1;
opacity: 0;
transition: opacity 0.3s ease;
}
.action-btn:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(74, 100, 145, 0.6);
}
.action-btn:hover::before {
opacity: 1;
}
.download-all {
background: linear-gradient(to right, #00b09b, #38ef7d);
}
.download-all::before {
background: linear-gradient(to right, #009e8a, #28c76f);
}
.reset-btn {
background: linear-gradient(to right, #ff416c, #ff4b2b);
}
.reset-btn::before {
background: linear-gradient(to right, #e03a60, #e54224);
}
.loading {
display: none;
text-align: center;
padding: 50px 30px;
background: rgba(248, 249, 255, 0.8);
border-radius: 12px;
margin: 20px 0;
}
.spinner {
width: 70px;
height: 70px;
border: 6px solid rgba(74, 100, 145, 0.2);
border-top: 6px solid #4a6491;
border-radius: 50%;
animation: spin 1.2s linear infinite;
margin: 0 auto 30px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 1.4rem;
color: #4a6491;
font-weight: 600;
margin-bottom: 15px;
}
.loading-subtext {
font-size: 1.1rem;
color: #7a8aa3;
max-width: 600px;
margin: 0 auto;
}
.progress-container {
width: 100%;
max-width: 600px;
height: 12px;
background: #e0e6f0;
border-radius: 10px;
overflow: hidden;
margin: 25px auto;
}
.progress-bar {
height: 100%;
background: linear-gradient(to right, #4a6491, #38ef7d);
border-radius: 10px;
width: 0%;
transition: width 0.4s ease;
}
footer {
text-align: center;
padding: 25px;
background: #f0f4fa;
color: #5a6b8c;
font-size: 1rem;
border-top: 1px solid #e0e6f0;
}
.note {
background: #edf7ff;
border-left: 4px solid #4a90e2;
padding: 20px;
border-radius: 0 10px 10px 0;
margin-top: 25px;
font-size: 1.05rem;
line-height: 1.7;
}
.highlight {
background: linear-gradient(120deg, #f6d365 0%, #fda085 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 700;
}
@media (max-width: 768px) {
.images-container {
grid-template-columns: 1fr;
}
h1 {
font-size: 2.2rem;
}
.upload-text {
font-size: 1.4rem;
}
.action-buttons {
flex-direction: column;
gap: 15px;
}
.action-btn {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-file-pdf"></i> PDF转图片智能分割工具</h1>
<p class="header-subtitle">上传PDF文件,自动转换为图片并智能分割成大小不超过5MB的片段。保持原始宽度不变,图片数量最小化</p>
</header>
<div class="main-content">
<div class="upload-section" id="uploadArea">
<div class="upload-icon">
<i class="fas fa-cloud-upload-alt"></i>
</div>
<p class="upload-text">拖放PDF文件到此处或点击上传</p>
<p class="upload-subtext">智能算法将自动合并页面,生成接近5MB的图片片段,减少图片数量</p>
<button class="upload-btn" id="uploadBtn">
<i class="fas fa-file-upload"></i> 选择PDF文件
</button>
<input type="file" id="pdfFile" class="file-input" accept=".pdf">
<div class="stats-container">
<div class="stat-card">
<div class="stat-title">目标图片大小</div>
<div class="stat-value">< 5 MB</div>
<div class="stat-subtext">每张图片最大不超过5MB</div>
</div>
<div class="stat-card">
<div class="stat-title">智能合并</div>
<div class="stat-value">自动优化</div>
<div class="stat-subtext">多页合并为单张图片</div>
</div>
<div class="stat-card">
<div class="stat-title">保持宽度</div>
<div class="stat-value">原始比例</div>
<div class="stat-subtext">宽度不变,高度自适应</div>
</div>
</div>
</div>
<div class="note">
<p><i class="fas fa-lightbulb"></i> <strong>智能分割说明:</strong> 本工具会智能合并PDF页面,每张图片包含尽可能多的页面(接近5MB)。例如,100页PDF(每页0.5MB)会被合并为10张图片(每张约5MB),而不是生成100张图片。</p>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p class="loading-text">正在处理PDF文件</p>
<p class="loading-subtext">智能合并页面中,请稍候... 处理时间取决于文件大小和页数</p>
<div class="progress-container">
<div class="progress-bar" id="progressBar"></div>
</div>
</div>
<div class="results-section" id="resultsSection">
<h2 class="section-title"><i class="fas fa-images"></i> 智能分割结果</h2>
<div class="images-container" id="imagesContainer">
<!-- 生成的图片卡片将在这里显示 -->
</div>
<div class="action-buttons">
<button class="action-btn download-all" id="downloadAll">
<i class="fas fa-download"></i> 下载全部图片 (ZIP)
</button>
<button class="action-btn reset-btn" id="resetBtn">
<i class="fas fa-redo"></i> 处理新文件
</button>
</div>
</div>
</div>
<footer>
<p>© 2025 PDF转图片智能分割工具 | 基于pdf.js构建 | 智能合并算法 | 本地处理无需上传服务器</p>
</footer>
</div>
<script>
// 设置pdf.js worker路径
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js';
// DOM元素引用
const pdfFileInput = document.getElementById('pdfFile');
const uploadBtn = document.getElementById('uploadBtn');
const uploadArea = document.getElementById('uploadArea');
const resultsSection = document.getElementById('resultsSection');
const imagesContainer = document.getElementById('imagesContainer');
const downloadAllBtn = document.getElementById('downloadAll');
const resetBtn = document.getElementById('resetBtn');
const loading = document.getElementById('loading');
const progressBar = document.getElementById('progressBar');
// 存储生成的图片数据
let generatedImages = [];
// 上传按钮点击事件
uploadBtn.addEventListener('click', () => {
pdfFileInput.click();
});
// 拖放功能
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.style.borderColor = '#38ef7d';
uploadArea.style.background = 'linear-gradient(135deg, #e6f7f5, #ddf3ec)';
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.style.borderColor = '#4a6491';
uploadArea.style.background = 'linear-gradient(135deg, #f8f9ff, #eef2f9)';
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.style.borderColor = '#4a6491';
uploadArea.style.background = 'linear-gradient(135deg, #f8f9ff, #eef2f9)';
if (e.dataTransfer.files.length) {
const file = e.dataTransfer.files[0];
if (file.type === 'application/pdf') {
processPDF(file);
} else {
alert('请上传PDF文件');
}
}
});
// 文件选择事件
pdfFileInput.addEventListener('change', (e) => {
if (e.target.files.length) {
const file = e.target.files[0];
if (file.type === 'application/pdf') {
processPDF(file);
} else {
alert('请上传PDF文件');
pdfFileInput.value = '';
}
}
});
// 处理PDF文件 - 智能合并页面
async function processPDF(file) {
// 重置状态
generatedImages = [];
imagesContainer.innerHTML = '';
resultsSection.style.display = 'none';
loading.style.display = 'block';
progressBar.style.width = '0%';
try {
// 读取PDF文件
const arrayBuffer = await file.arrayBuffer();
const pdfData = new Uint8Array(arrayBuffer);
// 加载PDF文档
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
const numPages = pdfDoc.numPages;
// 更新进度
progressBar.style.width = '10%';
// 智能分组算法
const targetSizeMB = 4.8; // 目标大小4.8MB(留0.2MB缓冲)
const maxSizeBytes = targetSizeMB * 1024 * 1024;
let currentGroup = [];
let currentGroupSize = 0;
const groups = [];
// 第一轮:估算每页大小并分组
for (let i = 1; i <= numPages; i++) {
// 更新进度
progressBar.style.width = `${10 + (i / numPages * 60)}%`;
const page = await pdfDoc.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
// 创建临时canvas
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
// 渲染页面
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
// 估算页面大小(使用JPEG质量0.85)
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
const pageSize = Math.round((dataUrl.length * 3) / 4); // 近似字节数
// 智能分组逻辑
if (currentGroupSize + pageSize > maxSizeBytes && currentGroup.length > 0) {
// 当前组已接近5MB,保存并开始新组
groups.push([...currentGroup]);
currentGroup = [];
currentGroupSize = 0;
}
// 将页面添加到当前组
currentGroup.push({
pageNum: i,
viewport: viewport,
canvas: canvas,
size: pageSize
});
currentGroupSize += pageSize;
// 释放资源
page.cleanup();
}
// 添加最后一组
if (currentGroup.length > 0) {
groups.push([...currentGroup]);
}
// 第二轮:生成合并后的图片
progressBar.style.width = '70%';
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
const group = groups[groupIndex];
// 更新进度
progressBar.style.width = `${70 + (groupIndex / groups.length * 25)}%`;
// 计算组的总高度和最大宽度
let totalHeight = 0;
let maxWidth = 0;
for (const pageData of group) {
totalHeight += pageData.viewport.height;
if (pageData.viewport.width > maxWidth) maxWidth = pageData.viewport.width;
}
// 创建大canvas
const groupCanvas = document.createElement('canvas');
const groupCtx = groupCanvas.getContext('2d');
groupCanvas.width = maxWidth;
groupCanvas.height = totalHeight;
// 绘制所有页面到canvas
let currentY = 0;
for (const pageData of group) {
groupCtx.drawImage(pageData.canvas, 0, currentY);
currentY += pageData.viewport.height;
}
// 转换为Blob
const blob = await new Promise(resolve => {
groupCanvas.toBlob(blob => {
resolve(blob);
}, 'image/jpeg', 0.85);
});
// 存储合并后的图片
generatedImages.push({
blob: blob,
name: `${file.name}-${groupIndex + 1}.jpg`,
width: groupCanvas.width,
height: groupCanvas.height,
size: blob.size,
pages: group.map(p => p.pageNum)
});
}
// 更新进度
progressBar.style.width = '98%';
// 显示结果
setTimeout(() => {
displayResults();
loading.style.display = 'none';
resultsSection.style.display = 'flex';
progressBar.style.width = '100%';
}, 500);
} catch (error) {
console.error('PDF处理失败:', error);
loading.style.display = 'none';
alert('处理PDF时发生错误: ' + error.message);
resetBtn.click();
}
}
// 显示生成的结果
function displayResults() {
imagesContainer.innerHTML = '';
// 计算总大小
let totalSize = 0;
generatedImages.forEach((img, index) => {
const url = URL.createObjectURL(img.blob);
const sizeMB = (img.size / (1024 * 1024)).toFixed(2);
totalSize += img.size;
const pagesText = img.pages.length > 1 ?
`第 ${Math.min(...img.pages)}-${Math.max(...img.pages)} 页` :
`第 ${img.pages[0]} 页`;
const imageCard = document.createElement('div');
imageCard.className = 'image-card';
imageCard.innerHTML = `
<img src="${url}" alt="PDF片段 ${index+1}" class="image-preview">
<div class="image-info">
<div class="image-title">片段 #${index+1}</div>
<div class="image-details">
<span>尺寸: ${img.width} × ${img.height} px</span>
<span>大小: ${sizeMB} MB</span>
</div>
<div class="image-details">
<span>包含: ${img.pages.length} 页</span>
<span class="image-pages">${pagesText}</span>
</div>
<button class="action-btn" style="padding: 12px 20px; margin-top: 15px; width: 100%; font-size: 1rem;" data-index="${index}">
<i class="fas fa-download"></i> 下载此片段
</button>
</div>
`;
imagesContainer.appendChild(imageCard);
});
// 添加下载按钮事件
document.querySelectorAll('.action-btn[data-index]').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = e.target.closest('button').getAttribute('data-index');
downloadImage(generatedImages[index].blob, generatedImages[index].name);
});
});
// 更新总大小显示
const totalSizeMB = (totalSize / (1024 * 1024)).toFixed(2);
document.querySelector('.stat-card:nth-child(2) .stat-value').textContent = generatedImages.length;
document.querySelector('.stat-card:nth-child(3) .stat-value').textContent = `${totalSizeMB} MB`;
}
// 下载单张图片
function downloadImage(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 下载所有图片
downloadAllBtn.addEventListener('click', () => {
if (generatedImages.length === 0) {
alert('没有可下载的图片');
return;
}
// 不打包文件
generatedImages.forEach((item, index) => {
setTimeout(() => {
downloadImage(item.blob, item.name);
}, 200)
})
// todo 如果需要打包文件,注释掉以上代码,用下面的就可以
// 显示加载状态
// const originalText = downloadAllBtn.innerHTML;
// downloadAllBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 正在创建压缩包...';
// downloadAllBtn.disabled = true;
//
// // 使用JSZip打包所有图片
// const zip = new JSZip();
// const folder = zip.folder("pdf-images");
//
// generatedImages.forEach((img, index) => {
// folder.file(img.name, img.blob);
// });
//
// zip.generateAsync({type:"blob"})
// .then(function(content) {
// // 下载ZIP文件
// const url = URL.createObjectURL(content);
// const a = document.createElement('a');
// a.href = url;
// a.download = 'pdf-images.zip';
// document.body.appendChild(a);
// a.click();
// document.body.removeChild(a);
// URL.revokeObjectURL(url);
//
// // 恢复按钮状态
// downloadAllBtn.innerHTML = originalText;
// downloadAllBtn.disabled = false;
// });
});
// 重置按钮
resetBtn.addEventListener('click', () => {
pdfFileInput.value = '';
generatedImages = [];
imagesContainer.innerHTML = '';
resultsSection.style.display = 'none';
loading.style.display = 'none';
progressBar.style.width = '0%';
// 重置统计信息
document.querySelector('.stat-card:nth-child(2) .stat-value').textContent = '0';
document.querySelector('.stat-card:nth-child(3) .stat-value').textContent = '0 MB';
});
</script>
</body>
</html>