Archived
1
0

feat: 新增PDF转图片工具的前端实现

添加了完整的PDF转图片工具的前端实现,包括HTML、CSS、JavaScript代码。该工具允许用户在浏览器中安全地将PDF文件转换为图片,支持单页导出或合并为单张长图,并提供了实时预览功能。所有处理均在本地完成,无需上传文件到服务器,确保用户数据安全。
This commit is contained in:
Snowz 2025-04-13 02:26:28 +08:00
commit 85142d373e
5 changed files with 757 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 PDF转图片工具
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

55
README.md Normal file
View File

@ -0,0 +1,55 @@
# PDF转图片工具
一个纯前端的PDF转图片工具所有处理均在浏览器中完成无需上传文件到服务器保证用户数据安全。
## 功能特点
- 🔒 **安全可靠**:所有处理均在本地浏览器中完成,无需上传文件到服务器
- 🚀 **高效转换**快速将PDF文件转换为高质量图片
- 📱 **响应式设计**:适配各种设备屏幕
- 🖼️ **多种导出选项**:支持导出单页图片或合并为单张长图
- 🔍 **实时预览**转换前可预览PDF内容
- 📦 **批量处理**支持多页PDF一次性处理
## 使用方法
1. 打开网页应用
2. 拖放PDF文件到指定区域或点击"选择文件"按钮
3. 等待PDF加载和预览生成
4. 选择导出选项(单页图片或合并为单张图片)
5. 点击"导出图片"按钮
6. 下载生成的图片文件
## 本地部署
1. 克隆本仓库
2. 确保`cssjs/js`目录下包含所有必要的JS库文件
3. 使用Web服务器如Nginx、Apache等提供静态文件服务
4. 访问index.html即可使用
## 所需JS库文件
本项目依赖以下JS库请下载并放置在`cssjs/js`目录下:
1. [pdf.js](https://cdn.jsdelivr.net/npm/pdf.js@3.4.120/build/pdf.min.js) - PDF渲染核心库
2. [pdf.worker.js](https://cdn.jsdelivr.net/npm/pdf.js@3.4.120/build/pdf.worker.min.js) - PDF.js工作线程
3. [jszip.min.js](https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js) - 用于创建ZIP文件
4. [FileSaver.min.js](https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js) - 用于保存文件
## 技术栈
- HTML5 / CSS3
- JavaScript (ES6+)
- [PDF.js](https://mozilla.github.io/pdf.js/) - Mozilla的PDF渲染库
- [JSZip](https://stuk.github.io/jszip/) - 用于创建ZIP文件的JavaScript库
- [FileSaver.js](https://github.com/eligrey/FileSaver.js/) - 客户端保存文件的解决方案
- [Bootstrap 5](https://getbootstrap.com/) - 用于UI组件和响应式设计
- [Bootstrap Icons](https://icons.getbootstrap.com/) - 图标库
## 致谢
本项目基于原始的PDF转图片工具进行了重构和改进感谢原项目的开发者提供的基础功能和灵感。
## 许可证
本项目采用MIT许可证详情请查看[LICENSE](LICENSE)文件。

211
cssjs/css/style.css Normal file
View File

@ -0,0 +1,211 @@
:root {
--primary-color: #4361ee;
--secondary-color: #3f37c9;
--accent-color: #4895ef;
--light-color: #f8f9fa;
--dark-color: #212529;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
color: var(--dark-color);
line-height: 1.6;
}
.app-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
.header {
text-align: center;
margin-bottom: 2rem;
}
.header h1 {
font-weight: 700;
color: var(--primary-color);
margin-bottom: 0.5rem;
}
.header p {
color: #6c757d;
font-size: 1.1rem;
}
.card {
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
border: none;
overflow: hidden;
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
}
.card-header {
background-color: var(--primary-color);
color: white;
font-weight: 600;
padding: 1rem;
}
.upload-area {
border: 2px dashed #dee2e6;
border-radius: 8px;
padding: 3rem 2rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background-color: #f8f9fa;
}
.upload-area:hover, .upload-area.dragover {
border-color: var(--primary-color);
background-color: rgba(67, 97, 238, 0.05);
}
.upload-icon {
font-size: 3rem;
color: var(--primary-color);
margin-bottom: 1rem;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover {
background-color: var(--secondary-color);
border-color: var(--secondary-color);
}
.btn-outline-primary {
color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-outline-primary:hover {
background-color: var(--primary-color);
color: white;
}
.progress {
height: 10px;
border-radius: 5px;
}
.pdf-info {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
}
.pdf-info-item {
text-align: center;
padding: 0.5rem;
background-color: #f8f9fa;
border-radius: 8px;
flex: 1;
margin: 0 0.5rem;
}
.pdf-info-item .value {
font-size: 1.2rem;
font-weight: 600;
color: var(--primary-color);
}
.pdf-info-item .label {
font-size: 0.9rem;
color: #6c757d;
}
.preview-container {
margin-top: 2rem;
}
.preview-item {
margin-bottom: 2rem;
position: relative;
}
.preview-item canvas {
width: 100%;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.preview-item .page-number {
position: absolute;
top: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
}
.export-options {
margin-top: 1rem;
display: flex;
align-items: center;
}
.export-options .form-check {
margin-right: 1.5rem;
}
footer {
text-align: center;
margin-top: 3rem;
padding-top: 1rem;
border-top: 1px solid #dee2e6;
color: #6c757d;
}
@media (max-width: 768px) {
.pdf-info {
flex-direction: column;
}
.pdf-info-item {
margin: 0.5rem 0;
}
.export-options {
flex-direction: column;
align-items: flex-start;
}
.export-options .form-check {
margin-bottom: 0.5rem;
}
}
/* 加载动画 */
.spinner {
width: 40px;
height: 40px;
margin: 100px auto;
background-color: var(--primary-color);
border-radius: 100%;
-webkit-animation: sk-scaleout 1.0s infinite ease-in-out;
animation: sk-scaleout 1.0s infinite ease-in-out;
}
@-webkit-keyframes sk-scaleout {
0% { -webkit-transform: scale(0) }
100% { -webkit-transform: scale(1.0); opacity: 0; }
}
@keyframes sk-scaleout {
0% { transform: scale(0); -webkit-transform: scale(0); }
100% { transform: scale(1.0); -webkit-transform: scale(1.0); opacity: 0; }
}

376
cssjs/js/main.js Normal file
View File

@ -0,0 +1,376 @@
// 设置PDF.js worker路径
pdfjsLib.GlobalWorkerOptions.workerSrc = 'cssjs/js/pdf.worker.js';
// 全局变量
let pdfDocument = null;
let pdfFile = null;
let renderedPages = [];
// DOM元素
const uploadArea = document.getElementById('upload-area');
const pdfFileInput = document.getElementById('pdf-file-input');
const selectFileBtn = document.getElementById('select-file-btn');
const loadingContainer = document.getElementById('loading-container');
const pdfInfoContainer = document.getElementById('pdf-info-container');
const previewContainer = document.getElementById('preview-container');
const previewItems = document.getElementById('preview-items');
const exportBtn = document.getElementById('export-btn');
const combinePagesSwitch = document.getElementById('combine-pages-switch');
const highQualitySwitch = document.getElementById('high-quality-switch');
// PDF信息显示元素
const pdfNameValue = document.getElementById('pdf-name-value');
const pdfSizeValue = document.getElementById('pdf-size-value');
const pdfPagesValue = document.getElementById('pdf-pages-value');
// 事件监听器
document.addEventListener('DOMContentLoaded', function() {
// 检查PDF.js是否正确加载
if (typeof pdfjsLib === 'undefined') {
alert('PDF.js库加载失败请确保pdf.js文件已正确放置');
return;
}
// 初始化事件监听
initEventListeners();
});
// 初始化事件监听器
function initEventListeners() {
selectFileBtn.addEventListener('click', () => pdfFileInput.click());
pdfFileInput.addEventListener('change', handleFileSelect);
exportBtn.addEventListener('click', exportImages);
// 拖放功能
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
const file = e.dataTransfer.files[0];
if (file.type === 'application/pdf') {
pdfFileInput.files = e.dataTransfer.files;
handleFileSelect(e);
} else {
showError('请选择PDF文件');
}
}
});
}
// 处理文件选择
function handleFileSelect(e) {
const file = pdfFileInput.files[0];
if (!file) return;
if (file.type !== 'application/pdf') {
showError('请选择PDF文件');
return;
}
if (file.size > 20 * 1024 * 1024) { // 20MB
showError('文件大小不能超过20MB');
return;
}
pdfFile = file;
// 显示加载状态
uploadArea.style.display = 'none';
loadingContainer.style.display = 'block';
previewContainer.style.display = 'none';
pdfInfoContainer.style.display = 'none';
previewItems.innerHTML = '';
renderedPages = [];
// 读取PDF文件
const reader = new FileReader();
reader.onload = function(event) {
const typedArray = new Uint8Array(event.target.result);
loadPdfFromData(typedArray);
};
reader.readAsArrayBuffer(file);
}
// 从ArrayBuffer加载PDF
function loadPdfFromData(data) {
pdfjsLib.getDocument({ data }).promise
.then(pdf => {
pdfDocument = pdf;
// 更新PDF信息
pdfNameValue.textContent = pdfFile.name;
pdfSizeValue.textContent = formatFileSize(pdfFile.size);
pdfPagesValue.textContent = pdf.numPages;
// 渲染预览
renderPdfPreview(pdf);
})
.catch(error => {
console.error('PDF加载错误:', error);
showError('无法加载PDF文件请确保文件未损坏');
resetUI();
});
}
// 渲染PDF预览
function renderPdfPreview(pdf) {
const totalPages = pdf.numPages;
let renderedCount = 0;
// 更新加载文本
document.getElementById('loading-text').textContent = `正在渲染预览 (0/${totalPages})`;
// 为每一页创建预览
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber++) {
pdf.getPage(pageNumber).then(page => {
const scale = 0.5; // 预览缩放比例
const viewport = page.getViewport({ scale });
// 创建canvas元素
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
// 渲染PDF页面到canvas
const renderContext = {
canvasContext: context,
viewport: viewport
};
page.render(renderContext).promise.then(() => {
renderedCount++;
document.getElementById('loading-text').textContent = `正在渲染预览 (${renderedCount}/${totalPages})`;
// 存储渲染的页面
renderedPages[pageNumber - 1] = {
pageNumber: pageNumber,
canvas: canvas,
width: viewport.width,
height: viewport.height
};
// 如果所有页面都已渲染,显示预览
if (renderedCount === totalPages) {
displayPreviews();
}
});
});
}
}
// 显示预览
function displayPreviews() {
// 按页码排序
renderedPages.sort((a, b) => a.pageNumber - b.pageNumber);
// 清空预览容器
previewItems.innerHTML = '';
// 添加每一页的预览
renderedPages.forEach(page => {
const colDiv = document.createElement('div');
colDiv.className = 'col-md-6 col-lg-4 preview-item';
const pageNumberDiv = document.createElement('div');
pageNumberDiv.className = 'page-number';
pageNumberDiv.textContent = `${page.pageNumber}`;
// 克隆canvas以避免原始canvas被修改
const displayCanvas = document.createElement('canvas');
displayCanvas.width = page.canvas.width;
displayCanvas.height = page.canvas.height;
const displayContext = displayCanvas.getContext('2d');
displayContext.drawImage(page.canvas, 0, 0);
colDiv.appendChild(displayCanvas);
colDiv.appendChild(pageNumberDiv);
previewItems.appendChild(colDiv);
});
// 显示预览和控制面板
loadingContainer.style.display = 'none';
pdfInfoContainer.style.display = 'block';
previewContainer.style.display = 'block';
}
// 导出图片
function exportImages() {
if (!pdfDocument || renderedPages.length === 0) {
showError('没有可导出的内容');
return;
}
const combinePages = combinePagesSwitch.checked;
const highQuality = highQualitySwitch.checked;
const scale = highQuality ? 2.0 : 1.0;
// 显示加载状态
loadingContainer.style.display = 'block';
document.getElementById('loading-text').textContent = '正在准备导出...';
if (combinePages) {
// 合并为单张图片
exportCombinedImage(scale);
} else {
// 导出为多张图片
exportMultipleImages(scale);
}
}
// 导出合并的单张图片
function exportCombinedImage(scale) {
// 计算合并后的图片尺寸
let totalHeight = 0;
let maxWidth = 0;
renderedPages.forEach(page => {
totalHeight += page.height * (scale / 0.5);
maxWidth = Math.max(maxWidth, page.width * (scale / 0.5));
});
// 创建合并的canvas
const combinedCanvas = document.createElement('canvas');
combinedCanvas.width = maxWidth;
combinedCanvas.height = totalHeight;
const combinedContext = combinedCanvas.getContext('2d');
// 填充白色背景
combinedContext.fillStyle = '#FFFFFF';
combinedContext.fillRect(0, 0, combinedCanvas.width, combinedCanvas.height);
// 重新渲染每一页到合并的canvas
let currentY = 0;
let renderedCount = 0;
const renderNextPage = (index) => {
if (index >= renderedPages.length) {
// 所有页面都已渲染,导出图片
combinedCanvas.toBlob(blob => {
saveAs(blob, `${pdfFile.name.replace('.pdf', '')}_combined.png`);
loadingContainer.style.display = 'none';
}, 'image/png');
return;
}
const page = renderedPages[index];
// 更新加载文本
document.getElementById('loading-text').textContent = `正在合并页面 (${index + 1}/${renderedPages.length})`;
// 获取原始页面
pdfDocument.getPage(page.pageNumber).then(pdfPage => {
const viewport = pdfPage.getViewport({ scale });
// 创建临时canvas
const tempCanvas = document.createElement('canvas');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
const tempContext = tempCanvas.getContext('2d');
// 渲染到临时canvas
const renderContext = {
canvasContext: tempContext,
viewport: viewport
};
pdfPage.render(renderContext).promise.then(() => {
// 将临时canvas的内容绘制到合并的canvas
const x = (maxWidth - viewport.width) / 2; // 居中
combinedContext.drawImage(tempCanvas, x, currentY);
// 更新Y坐标
currentY += viewport.height;
// 渲染下一页
renderNextPage(index + 1);
});
});
};
// 开始渲染第一页
renderNextPage(0);
}
// 导出多张图片
function exportMultipleImages(scale) {
const zip = new JSZip();
const folder = zip.folder("images");
let processedCount = 0;
// 更新加载文本
document.getElementById('loading-text').textContent = `正在导出图片 (0/${renderedPages.length})`;
// 处理每一页
renderedPages.forEach((page, index) => {
// 获取原始页面
pdfDocument.getPage(page.pageNumber).then(pdfPage => {
const viewport = pdfPage.getViewport({ scale });
// 创建canvas
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d');
// 填充白色背景
context.fillStyle = '#FFFFFF';
context.fillRect(0, 0, canvas.width, canvas.height);
// 渲染PDF页面到canvas
const renderContext = {
canvasContext: context,
viewport: viewport
};
pdfPage.render(renderContext).promise.then(() => {
// 将canvas转换为blob
canvas.toBlob(blob => {
// 添加到zip
folder.file(`page_${page.pageNumber}.png`, blob);
processedCount++;
document.getElementById('loading-text').textContent = `正在导出图片 (${processedCount}/${renderedPages.length})`;
// 如果所有页面都已处理生成并下载zip
if (processedCount === renderedPages.length) {
zip.generateAsync({ type: 'blob' }).then(content => {
saveAs(content, `${pdfFile.name.replace('.pdf', '')}_images.zip`);
loadingContainer.style.display = 'none';
});
}
}, 'image/png');
});
});
});
}
// 辅助函数
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + ' KB';
else return (bytes / 1048576).toFixed(2) + ' MB';
}
function showError(message) {
alert(message);
}
function resetUI() {
uploadArea.style.display = 'block';
loadingContainer.style.display = 'none';
pdfInfoContainer.style.display = 'none';
previewContainer.style.display = 'none';
pdfFileInput.value = '';
}

94
index.html Normal file
View File

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="description" content="PDF转图片工具 - 在浏览器中安全地将PDF转换为图片" />
<title>PDF转图片工具</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="cssjs/css/style.css" />
</head>
<body>
<div class="app-container">
<div class="header">
<h1>PDF转图片工具</h1>
<p>安全、高效地将PDF文件转换为高质量图片所有处理均在浏览器中完成</p>
</div>
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-file-earmark-pdf"></i> 选择PDF文件
</div>
<div class="card-body">
<div id="upload-area" class="upload-area">
<i class="bi bi-cloud-arrow-up upload-icon"></i>
<h4>拖放PDF文件到这里</h4>
<p>或者</p>
<button class="btn btn-primary" id="select-file-btn">
<i class="bi bi-file-earmark-plus"></i> 选择文件
</button>
<input type="file" id="pdf-file-input" accept="application/pdf" style="display: none;">
<p class="mt-3 text-muted small">最大文件大小: 20MB</p>
</div>
<div id="loading-container" style="display: none; text-align: center;">
<div class="spinner"></div>
<p id="loading-text">正在处理PDF文件...</p>
</div>
<div id="pdf-info-container" style="display: none;" class="mt-4">
<div class="pdf-info">
<div class="pdf-info-item">
<div class="value" id="pdf-name-value">-</div>
<div class="label">文件名称</div>
</div>
<div class="pdf-info-item">
<div class="value" id="pdf-size-value">-</div>
<div class="label">文件大小</div>
</div>
<div class="pdf-info-item">
<div class="value" id="pdf-pages-value">-</div>
<div class="label">总页数</div>
</div>
</div>
<div class="export-options">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="combine-pages-switch">
<label class="form-check-label" for="combine-pages-switch">合并为单张图片</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="high-quality-switch" checked>
<label class="form-check-label" for="high-quality-switch">高质量输出</label>
</div>
<div class="ms-auto">
<button class="btn btn-primary" id="export-btn">
<i class="bi bi-download"></i> 导出图片
</button>
</div>
</div>
</div>
</div>
</div>
<div id="preview-container" class="preview-container" style="display: none;">
<h3 class="mb-3">预览</h3>
<div id="preview-items" class="row"></div>
</div>
<footer>
<p>© 2025 PDF转图片工具 | <a href="https://github.com/xxlllq/pdf2img" target="_blank">GitHub</a></p>
</footer>
</div>
<!-- 使用本地JS文件 -->
<script src="cssjs/js/pdf.js"></script>
<script src="cssjs/js/jszip.min.js"></script>
<script src="cssjs/js/FileSaver.min.js"></script>
<script src="cssjs/js/main.js"></script>
</body>
</html>