Archived
1
0

feat: 更新PDF转换工具,支持导出为Word文档

修改了index.html以更新标题和描述,增强了用户界面,添加了导出为Word文档的功能。更新了README.md以反映新功能,增加了使用方法和功能特点的描述。同时,调整了CSS样式以支持新的导出选项,确保用户体验流畅。修复了LICENSE文件的格式问题。
This commit is contained in:
Snowz 2025-04-18 03:19:01 +08:00
parent 31a1bccb6f
commit 2565754d83
7 changed files with 22562 additions and 697 deletions

40
LICENSE
View File

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

154
README.md
View File

@ -1,77 +1,79 @@
# PDF转图片工具 # PDF转换工具
一个纯前端的PDF转图片工具所有处理均在浏览器中完成无需上传文件到服务器保证用户数据安全。 一个纯前端的PDF转换工具支持将PDF转换为图片或Word文档所有处理均在浏览器中完成无需上传文件到服务器保证用户数据安全。
## 功能特点 ## 功能特点
- 🔒 **安全可靠**:所有处理均在本地浏览器中完成,无需上传文件到服务器 - 🔒 **安全可靠**:所有处理均在本地浏览器中完成,无需上传文件到服务器
- 🚀 **高效转换**快速将PDF文件转换为高质量图片 - 🚀 **高效转换**快速将PDF文件转换为高质量图片或Word文档
- 📱 **响应式设计**:适配各种设备屏幕 - 📱 **响应式设计**:适配各种设备屏幕
- 🖼️ **多种导出选项**:支持导出单页图片或合并为单张长图 - 🖼️ **多种导出选项**支持导出单页图片、合并为单张长图或转换为Word文档
- 🔍 **实时预览**转换前可预览PDF内容 - 🔍 **实时预览**转换前可预览PDF内容
- 📦 **批量处理**支持多页PDF一次性处理 - 📦 **批量处理**支持多页PDF一次性处理
- 📄 **Word转换**支持将PDF转换为可编辑的Word文档
## 使用方法
## 使用方法
1. 打开网页应用
2. 拖放PDF文件到指定区域或点击"选择文件"按钮 1. 打开网页应用
3. 等待PDF加载和预览生成 2. 拖放PDF文件到指定区域或点击"选择文件"按钮
4. 选择导出选项(单页图片或合并为单张图片) 3. 等待PDF加载和预览生成
5. 点击"导出图片"按钮 4. 选择导出类型图片或Word和相关选项
6. 下载生成的图片文件 5. 点击"导出文件"按钮
6. 下载生成的图片或Word文档
## 本地部署
## 本地部署
1. 克隆本仓库
2. 确保`cssjs/js`目录下包含所有必要的JS库文件 1. 克隆本仓库
3. 使用Web服务器如Nginx、Apache等提供静态文件服务 2. 确保`cssjs/js`目录下包含所有必要的JS库文件
4. 访问index.html即可使用 3. 使用Web服务器如Nginx、Apache等提供静态文件服务
4. 访问index.html即可使用
### NGINX 配置示例
### NGINX 配置示例
如果使用 NGINX 服务器,可以添加以下配置以支持 `.mjs` 文件:
如果使用 NGINX 服务器,可以添加以下配置以支持 `.mjs` 文件:
```
nginx ```
types { nginx
# 其他 MIME 类型... types {
text/javascript mjs; # 其他 MIME 类型...
} text/javascript mjs;
``` }
## 浏览器兼容性 ```
## 浏览器兼容性
本工具支持所有现代浏览器,包括:
本工具支持所有现代浏览器,包括:
- Chrome 60+
- Firefox 60+ - Chrome 60+
- Safari 11+ - Firefox 60+
- Edge 79+ - Safari 11+
- Edge 79+
## 隐私说明
## 隐私说明
- 所有文件处理均在本地浏览器中完成
- 不会将您的 PDF 文件或生成的图片上传到任何服务器 - 所有文件处理均在本地浏览器中完成
- 不会收集任何个人信息或使用情况数据 - 不会将您的 PDF 文件或生成的图片上传到任何服务器
- 不会收集任何个人信息或使用情况数据
## 致谢
## 致谢
- PDF.js
- JSZip - PDF.js
- FileSaver.js - JSZip
- Bootstrap - FileSaver.js
- Bootstrap
本项目基于原始的PDF转图片工具进行了重构和改进感谢[原项目](https://github.com/xxlllq/pdf2img)开发者提供的基础功能和灵感。
本项目基于原始的PDF转图片工具进行了重构和改进感谢[原项目](https://github.com/xxlllq/pdf2img)开发者提供的基础功能和灵感。
## 技术栈
## 技术栈
- HTML5 / CSS3
- JavaScript (ES6+) - HTML5 / CSS3
- [PDF.js](https://mozilla.github.io/pdf.js/) - Mozilla的PDF渲染库 - JavaScript (ES6+)
- [JSZip](https://stuk.github.io/jszip/) - 用于创建ZIP文件的JavaScript库 - [PDF.js](https://mozilla.github.io/pdf.js/) - Mozilla的PDF渲染库
- [FileSaver.js](https://github.com/eligrey/FileSaver.js/) - 客户端保存文件的解决方案 - [JSZip](https://stuk.github.io/jszip/) - 用于创建ZIP文件的JavaScript库
- [Bootstrap 5](https://getbootstrap.com/) - 用于UI组件和响应式设计 - [FileSaver.js](https://github.com/eligrey/FileSaver.js/) - 客户端保存文件的解决方案
- [Bootstrap Icons](https://icons.getbootstrap.com/) - 图标库 - [docx](https://github.com/dolanmiu/docx) - 用于生成Word文档的JavaScript库
- [Bootstrap 5](https://getbootstrap.com/) - 用于UI组件和响应式设计
## 许可证 - [Bootstrap Icons](https://icons.getbootstrap.com/) - 图标库
## 许可证
本项目采用MIT许可证详情请查看[LICENSE](LICENSE)文件。 本项目采用MIT许可证详情请查看[LICENSE](LICENSE)文件。

View File

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

19
cssjs/js/docx.esm.js Normal file
View File

@ -0,0 +1,19 @@
// 等待 docx.min.js 加载完成
const waitForDocx = new Promise((resolve) => {
const checkDocx = () => {
if (window.docx && window.docx.Document) {
resolve();
} else {
setTimeout(checkDocx, 100);
}
};
checkDocx();
});
// 导出 docx 库的所有必要组件
export const Document = waitForDocx.then(() => window.docx.Document);
export const Paragraph = waitForDocx.then(() => window.docx.Paragraph);
export const ImageRun = waitForDocx.then(() => window.docx.ImageRun);
export const HeadingLevel = waitForDocx.then(() => window.docx.HeadingLevel);
export const AlignmentType = waitForDocx.then(() => window.docx.AlignmentType);
export const Packer = waitForDocx.then(() => window.docx.Packer);

21620
cssjs/js/docx.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,378 +1,552 @@
// 导入依赖库 // 导入依赖库
import * as pdfjsLib from './pdf.mjs'; import * as pdfjsLib from './pdf.mjs';
import JSZip from './jszip.min.js'; import JSZip from './jszip.min.js';
import * as docx from './docx.esm.js';
// 设置PDF.js worker路径
pdfjsLib.GlobalWorkerOptions.workerSrc = 'cssjs/js/pdf.worker.mjs'; // 设置PDF.js worker路径
pdfjsLib.GlobalWorkerOptions.workerSrc = 'cssjs/js/pdf.worker.mjs';
// 全局变量
let pdfDocument = null; // 全局变量
let pdfFile = null; let pdfDocument = null;
let renderedPages = []; let pdfFile = null;
let renderedPages = [];
// DOM元素
const uploadArea = document.getElementById('upload-area'); // DOM元素
const pdfFileInput = document.getElementById('pdf-file-input'); const uploadArea = document.getElementById('upload-area');
const selectFileBtn = document.getElementById('select-file-btn'); const pdfFileInput = document.getElementById('pdf-file-input');
const loadingContainer = document.getElementById('loading-container'); const selectFileBtn = document.getElementById('select-file-btn');
const pdfInfoContainer = document.getElementById('pdf-info-container'); const loadingContainer = document.getElementById('loading-container');
const previewContainer = document.getElementById('preview-container'); const pdfInfoContainer = document.getElementById('pdf-info-container');
const previewItems = document.getElementById('preview-items'); const previewContainer = document.getElementById('preview-container');
const exportBtn = document.getElementById('export-btn'); const previewItems = document.getElementById('preview-items');
const combinePagesSwitch = document.getElementById('combine-pages-switch'); const exportBtn = document.getElementById('export-btn');
const highQualitySwitch = document.getElementById('high-quality-switch'); const combinePagesSwitch = document.getElementById('combine-pages-switch');
const highQualitySwitch = document.getElementById('high-quality-switch');
// PDF信息显示元素 const preserveLayoutSwitch = document.getElementById('preserve-layout-switch');
const pdfNameValue = document.getElementById('pdf-name-value'); const exportImageRadio = document.getElementById('export-image');
const pdfSizeValue = document.getElementById('pdf-size-value'); const exportWordRadio = document.getElementById('export-word');
const pdfPagesValue = document.getElementById('pdf-pages-value'); const imageOptions = document.getElementById('image-options');
const wordOptions = document.getElementById('word-options');
// 事件监听器
document.addEventListener('DOMContentLoaded', function() { // PDF信息显示元素
// 初始化事件监听 const pdfNameValue = document.getElementById('pdf-name-value');
initEventListeners(); const pdfSizeValue = document.getElementById('pdf-size-value');
}); const pdfPagesValue = document.getElementById('pdf-pages-value');
// 初始化事件监听器 // 事件监听器
function initEventListeners() { document.addEventListener('DOMContentLoaded', function() {
selectFileBtn.addEventListener('click', () => pdfFileInput.click()); // 初始化事件监听
pdfFileInput.addEventListener('change', handleFileSelect); initEventListeners();
exportBtn.addEventListener('click', exportImages); });
// 拖放功能 // 初始化事件监听器
uploadArea.addEventListener('dragover', (e) => { function initEventListeners() {
e.preventDefault(); selectFileBtn.addEventListener('click', () => pdfFileInput.click());
uploadArea.classList.add('dragover'); pdfFileInput.addEventListener('change', handleFileSelect);
}); exportBtn.addEventListener('click', handleExport);
uploadArea.addEventListener('dragleave', () => { // 导出类型切换
uploadArea.classList.remove('dragover'); exportImageRadio.addEventListener('change', updateExportOptions);
}); exportWordRadio.addEventListener('change', updateExportOptions);
uploadArea.addEventListener('drop', (e) => { // 拖放功能
e.preventDefault(); uploadArea.addEventListener('dragover', (e) => {
uploadArea.classList.remove('dragover'); e.preventDefault();
uploadArea.classList.add('dragover');
if (e.dataTransfer.files.length > 0) { });
const file = e.dataTransfer.files[0];
if (file.type === 'application/pdf') { uploadArea.addEventListener('dragleave', () => {
pdfFileInput.files = e.dataTransfer.files; uploadArea.classList.remove('dragover');
handleFileSelect(e); });
} else {
showError('请选择PDF文件'); 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 {
function handleFileSelect(e) { showError('请选择PDF文件');
const file = pdfFileInput.files[0]; }
}
if (!file) return; });
}
if (file.type !== 'application/pdf') {
showError('请选择PDF文件'); // 更新导出选项显示
return; function updateExportOptions() {
} if (exportImageRadio.checked) {
imageOptions.style.display = 'block';
if (file.size > 20 * 1024 * 1024) { // 20MB wordOptions.style.display = 'none';
showError('文件大小不能超过20MB'); } else {
return; imageOptions.style.display = 'none';
} wordOptions.style.display = 'block';
}
pdfFile = file; }
// 显示加载状态 // 处理导出按钮点击
uploadArea.style.display = 'none'; function handleExport() {
loadingContainer.style.display = 'block'; if (!pdfDocument || renderedPages.length === 0) {
previewContainer.style.display = 'none'; showError('没有可导出的内容');
pdfInfoContainer.style.display = 'none'; return;
previewItems.innerHTML = ''; }
renderedPages = [];
if (exportImageRadio.checked) {
// 读取PDF文件 exportImages();
const reader = new FileReader(); } else {
reader.onload = function(event) { exportWord();
const typedArray = new Uint8Array(event.target.result); }
loadPdfFromData(typedArray); }
};
reader.readAsArrayBuffer(file); // 处理文件选择
} function handleFileSelect(e) {
const file = pdfFileInput.files[0];
// 从ArrayBuffer加载PDF
function loadPdfFromData(data) { if (!file) return;
pdfjsLib.getDocument({ data }).promise
.then(pdf => { if (file.type !== 'application/pdf') {
pdfDocument = pdf; showError('请选择PDF文件');
return;
// 更新PDF信息 }
pdfNameValue.textContent = pdfFile.name;
pdfSizeValue.textContent = formatFileSize(pdfFile.size); if (file.size > 20 * 1024 * 1024) { // 20MB
pdfPagesValue.textContent = pdf.numPages; showError('文件大小不能超过20MB');
return;
// 渲染预览 }
renderPdfPreview(pdf);
}) pdfFile = file;
.catch(error => {
console.error('PDF加载错误:', error); // 显示加载状态
showError('无法加载PDF文件请确保文件未损坏'); uploadArea.style.display = 'none';
resetUI(); loadingContainer.style.display = 'block';
}); previewContainer.style.display = 'none';
} pdfInfoContainer.style.display = 'none';
previewItems.innerHTML = '';
// 渲染PDF预览 renderedPages = [];
function renderPdfPreview(pdf) {
const totalPages = pdf.numPages; // 读取PDF文件
let renderedCount = 0; const reader = new FileReader();
reader.onload = function(event) {
// 更新加载文本 const typedArray = new Uint8Array(event.target.result);
document.getElementById('loading-text').textContent = `正在渲染预览 (0/${totalPages})`; loadPdfFromData(typedArray);
};
// 为每一页创建预览 reader.readAsArrayBuffer(file);
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber++) { }
pdf.getPage(pageNumber).then(page => {
const scale = 0.5; // 预览缩放比例 // 从ArrayBuffer加载PDF
const viewport = page.getViewport({ scale }); function loadPdfFromData(data) {
pdfjsLib.getDocument({ data }).promise
// 创建canvas元素 .then(pdf => {
const canvas = document.createElement('canvas'); pdfDocument = pdf;
const context = canvas.getContext('2d');
canvas.width = viewport.width; // 更新PDF信息
canvas.height = viewport.height; pdfNameValue.textContent = pdfFile.name;
pdfSizeValue.textContent = formatFileSize(pdfFile.size);
// 渲染PDF页面到canvas pdfPagesValue.textContent = pdf.numPages;
const renderContext = {
canvasContext: context, // 渲染预览
viewport: viewport renderPdfPreview(pdf);
}; })
.catch(error => {
page.render(renderContext).promise.then(() => { console.error('PDF加载错误:', error);
renderedCount++; showError('无法加载PDF文件请确保文件未损坏');
document.getElementById('loading-text').textContent = `正在渲染预览 (${renderedCount}/${totalPages})`; resetUI();
});
// 存储渲染的页面 }
renderedPages[pageNumber - 1] = {
pageNumber: pageNumber, // 渲染PDF预览
canvas: canvas, function renderPdfPreview(pdf) {
width: viewport.width, const totalPages = pdf.numPages;
height: viewport.height let renderedCount = 0;
};
// 更新加载文本
// 如果所有页面都已渲染,显示预览 document.getElementById('loading-text').textContent = `正在渲染预览 (0/${totalPages})`;
if (renderedCount === totalPages) {
displayPreviews(); // 为每一页创建预览
} 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');
function displayPreviews() { const context = canvas.getContext('2d');
// 按页码排序 canvas.width = viewport.width;
renderedPages.sort((a, b) => a.pageNumber - b.pageNumber); canvas.height = viewport.height;
// 清空预览容器 // 渲染PDF页面到canvas
previewItems.innerHTML = ''; const renderContext = {
canvasContext: context,
// 添加每一页的预览 viewport: viewport
renderedPages.forEach(page => { };
const colDiv = document.createElement('div');
colDiv.className = 'col-md-6 col-lg-4 preview-item'; page.render(renderContext).promise.then(() => {
renderedCount++;
const pageNumberDiv = document.createElement('div'); document.getElementById('loading-text').textContent = `正在渲染预览 (${renderedCount}/${totalPages})`;
pageNumberDiv.className = 'page-number';
pageNumberDiv.textContent = `${page.pageNumber}`; // 存储渲染的页面
renderedPages[pageNumber - 1] = {
// 克隆canvas以避免原始canvas被修改 pageNumber: pageNumber,
const displayCanvas = document.createElement('canvas'); canvas: canvas,
displayCanvas.width = page.canvas.width; width: viewport.width,
displayCanvas.height = page.canvas.height; height: viewport.height
const displayContext = displayCanvas.getContext('2d'); };
displayContext.drawImage(page.canvas, 0, 0);
// 如果所有页面都已渲染,显示预览
colDiv.appendChild(displayCanvas); if (renderedCount === totalPages) {
colDiv.appendChild(pageNumberDiv); displayPreviews();
previewItems.appendChild(colDiv); }
}); });
});
// 显示预览和控制面板 }
loadingContainer.style.display = 'none'; }
pdfInfoContainer.style.display = 'block';
previewContainer.style.display = 'block'; // 显示预览
} function displayPreviews() {
// 按页码排序
// 导出图片 renderedPages.sort((a, b) => a.pageNumber - b.pageNumber);
function exportImages() {
if (!pdfDocument || renderedPages.length === 0) { // 清空预览容器
showError('没有可导出的内容'); previewItems.innerHTML = '';
return;
} // 添加每一页的预览
renderedPages.forEach(page => {
const combinePages = combinePagesSwitch.checked; const colDiv = document.createElement('div');
const highQuality = highQualitySwitch.checked; colDiv.className = 'col-md-6 col-lg-4 preview-item';
const scale = highQuality ? 2.0 : 1.0;
const pageNumberDiv = document.createElement('div');
// 显示加载状态 pageNumberDiv.className = 'page-number';
loadingContainer.style.display = 'block'; pageNumberDiv.textContent = `${page.pageNumber}`;
document.getElementById('loading-text').textContent = '正在准备导出...';
// 克隆canvas以避免原始canvas被修改
if (combinePages) { const displayCanvas = document.createElement('canvas');
// 合并为单张图片 displayCanvas.width = page.canvas.width;
exportCombinedImage(scale); displayCanvas.height = page.canvas.height;
} else { const displayContext = displayCanvas.getContext('2d');
// 导出为多张图片 displayContext.drawImage(page.canvas, 0, 0);
exportMultipleImages(scale);
} colDiv.appendChild(displayCanvas);
} colDiv.appendChild(pageNumberDiv);
previewItems.appendChild(colDiv);
// 导出合并的单张图片 });
function exportCombinedImage(scale) {
// 计算合并后的图片尺寸 // 显示预览和控制面板
let totalHeight = 0; loadingContainer.style.display = 'none';
let maxWidth = 0; pdfInfoContainer.style.display = 'block';
previewContainer.style.display = 'block';
renderedPages.forEach(page => { }
totalHeight += page.height * (scale / 0.5);
maxWidth = Math.max(maxWidth, page.width * (scale / 0.5)); // 导出图片
}); function exportImages() {
// 创建合并的canvas const combinePages = combinePagesSwitch.checked;
const combinedCanvas = document.createElement('canvas'); const highQuality = highQualitySwitch.checked;
combinedCanvas.width = maxWidth; const scale = highQuality ? 2.0 : 1.0;
combinedCanvas.height = totalHeight;
const combinedContext = combinedCanvas.getContext('2d'); // 显示加载状态
loadingContainer.style.display = 'block';
// 填充白色背景 document.getElementById('loading-text').textContent = '正在准备导出...';
combinedContext.fillStyle = '#FFFFFF';
combinedContext.fillRect(0, 0, combinedCanvas.width, combinedCanvas.height); if (combinePages) {
// 合并为单张图片
// 重新渲染每一页到合并的canvas exportCombinedImage(scale);
let currentY = 0; } else {
let renderedCount = 0; // 导出为多张图片
exportMultipleImages(scale);
const renderNextPage = (index) => { }
if (index >= renderedPages.length) { }
// 所有页面都已渲染,导出图片
combinedCanvas.toBlob(blob => { // 导出合并的单张图片
saveAs(blob, `${pdfFile.name.replace('.pdf', '')}_combined.png`); function exportCombinedImage(scale) {
loadingContainer.style.display = 'none'; // 计算合并后的图片尺寸
}, 'image/png'); let totalHeight = 0;
return; let maxWidth = 0;
}
renderedPages.forEach(page => {
const page = renderedPages[index]; totalHeight += page.height * (scale / 0.5);
maxWidth = Math.max(maxWidth, page.width * (scale / 0.5));
// 更新加载文本 });
document.getElementById('loading-text').textContent = `正在合并页面 (${index + 1}/${renderedPages.length})`;
// 创建合并的canvas
// 获取原始页面 const combinedCanvas = document.createElement('canvas');
pdfDocument.getPage(page.pageNumber).then(pdfPage => { combinedCanvas.width = maxWidth;
const viewport = pdfPage.getViewport({ scale }); combinedCanvas.height = totalHeight;
const combinedContext = combinedCanvas.getContext('2d');
// 创建临时canvas
const tempCanvas = document.createElement('canvas'); // 填充白色背景
tempCanvas.width = viewport.width; combinedContext.fillStyle = '#FFFFFF';
tempCanvas.height = viewport.height; combinedContext.fillRect(0, 0, combinedCanvas.width, combinedCanvas.height);
const tempContext = tempCanvas.getContext('2d');
// 重新渲染每一页到合并的canvas
// 渲染到临时canvas let currentY = 0;
const renderContext = { let renderedCount = 0;
canvasContext: tempContext,
viewport: viewport const renderNextPage = (index) => {
}; if (index >= renderedPages.length) {
// 所有页面都已渲染,导出图片
pdfPage.render(renderContext).promise.then(() => { combinedCanvas.toBlob(blob => {
// 将临时canvas的内容绘制到合并的canvas saveAs(blob, `${pdfFile.name.replace('.pdf', '')}_combined.png`);
const x = (maxWidth - viewport.width) / 2; // 居中 loadingContainer.style.display = 'none';
combinedContext.drawImage(tempCanvas, x, currentY); }, 'image/png');
return;
// 更新Y坐标 }
currentY += viewport.height;
const page = renderedPages[index];
// 渲染下一页
renderNextPage(index + 1); // 更新加载文本
}); document.getElementById('loading-text').textContent = `正在合并页面 (${index + 1}/${renderedPages.length})`;
});
}; // 获取原始页面
pdfDocument.getPage(page.pageNumber).then(pdfPage => {
// 开始渲染第一页 const viewport = pdfPage.getViewport({ scale });
renderNextPage(0);
} // 创建临时canvas
const tempCanvas = document.createElement('canvas');
// 导出多张图片 tempCanvas.width = viewport.width;
function exportMultipleImages(scale) { tempCanvas.height = viewport.height;
const zip = new JSZip(); const tempContext = tempCanvas.getContext('2d');
const folder = zip.folder("images");
let processedCount = 0; // 渲染到临时canvas
const renderContext = {
// 更新加载文本 canvasContext: tempContext,
document.getElementById('loading-text').textContent = `正在导出图片 (0/${renderedPages.length})`; viewport: viewport
};
// 处理每一页
renderedPages.forEach((page, index) => { pdfPage.render(renderContext).promise.then(() => {
// 获取原始页面 // 将临时canvas的内容绘制到合并的canvas
pdfDocument.getPage(page.pageNumber).then(pdfPage => { const x = (maxWidth - viewport.width) / 2; // 居中
const viewport = pdfPage.getViewport({ scale }); combinedContext.drawImage(tempCanvas, x, currentY);
// 创建canvas // 更新Y坐标
const canvas = document.createElement('canvas'); currentY += viewport.height;
canvas.width = viewport.width;
canvas.height = viewport.height; // 渲染下一页
const context = canvas.getContext('2d'); renderNextPage(index + 1);
});
// 填充白色背景 });
context.fillStyle = '#FFFFFF'; };
context.fillRect(0, 0, canvas.width, canvas.height);
// 开始渲染第一页
// 渲染PDF页面到canvas renderNextPage(0);
const renderContext = { }
canvasContext: context,
viewport: viewport // 导出多张图片
}; function exportMultipleImages(scale) {
const zip = new JSZip();
pdfPage.render(renderContext).promise.then(() => { const folder = zip.folder("images");
// 将canvas转换为blob let processedCount = 0;
canvas.toBlob(blob => {
// 添加到zip // 更新加载文本
folder.file(`page_${page.pageNumber}.png`, blob); document.getElementById('loading-text').textContent = `正在导出图片 (0/${renderedPages.length})`;
processedCount++; // 处理每一页
document.getElementById('loading-text').textContent = `正在导出图片 (${processedCount}/${renderedPages.length})`; renderedPages.forEach((page, index) => {
// 获取原始页面
// 如果所有页面都已处理生成并下载zip pdfDocument.getPage(page.pageNumber).then(pdfPage => {
if (processedCount === renderedPages.length) { const viewport = pdfPage.getViewport({ scale });
// 生成并下载zip文件
zip.generateAsync({ type: 'blob' }).then(content => { // 创建canvas
saveAs(content, `${pdfFile.name.replace('.pdf', '')}_images.zip`); const canvas = document.createElement('canvas');
loadingContainer.style.display = 'none'; canvas.width = viewport.width;
}); canvas.height = viewport.height;
} const context = canvas.getContext('2d');
}, 'image/png');
}); // 填充白色背景
}); context.fillStyle = '#FFFFFF';
}); context.fillRect(0, 0, canvas.width, canvas.height);
}
// 渲染PDF页面到canvas
// 辅助函数 const renderContext = {
function formatFileSize(bytes) { canvasContext: context,
if (bytes < 1024) return bytes + ' B'; viewport: viewport
else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + ' KB'; };
else return (bytes / 1048576).toFixed(2) + ' MB';
} pdfPage.render(renderContext).promise.then(() => {
// 将canvas转换为blob
function showError(message) { canvas.toBlob(blob => {
alert(message); // 添加到zip
} folder.file(`page_${page.pageNumber}.png`, blob);
function resetUI() { processedCount++;
uploadArea.style.display = 'block'; document.getElementById('loading-text').textContent = `正在导出图片 (${processedCount}/${renderedPages.length})`;
loadingContainer.style.display = 'none';
pdfInfoContainer.style.display = 'none'; // 如果所有页面都已处理生成并下载zip
previewContainer.style.display = 'none'; if (processedCount === renderedPages.length) {
pdfFileInput.value = ''; // 生成并下载zip文件
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 = '';
}
// 导出为Word文档
async function exportWord() {
// 显示加载状态
loadingContainer.style.display = 'block';
document.getElementById('loading-text').textContent = '正在准备导出Word文档...';
try {
// 等待 docx 组件加载完成
const Document = await docx.Document;
const Paragraph = await docx.Paragraph;
const ImageRun = await docx.ImageRun;
const HeadingLevel = await docx.HeadingLevel;
const AlignmentType = await docx.AlignmentType;
const Packer = await docx.Packer;
// 创建一个新的Word文档
const doc = new Document({
sections: [{
properties: {},
children: []
}]
});
const preserveLayout = preserveLayoutSwitch.checked;
let processedCount = 0;
// 处理每一页
for (let i = 0; i < renderedPages.length; i++) {
const page = renderedPages[i];
document.getElementById('loading-text').textContent = `正在处理第 ${i + 1}/${renderedPages.length}`;
// 获取页面文本内容
const textContent = await pdfDocument.getPage(page.pageNumber).then(pdfPage => {
return pdfPage.getTextContent();
});
// 如果保留布局,添加页面图像
if (preserveLayout) {
// 获取页面图像
const pdfPage = await pdfDocument.getPage(page.pageNumber);
const viewport = pdfPage.getViewport({ scale: 1.5 });
// 创建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
};
await pdfPage.render(renderContext).promise;
// 将canvas转换为图像数据
const imageData = canvas.toDataURL('image/png');
const imageBase64 = imageData.split(',')[1];
// 将base64转换为Uint8Array
const binaryString = atob(imageBase64);
const bytes = new Uint8Array(binaryString.length);
for (let j = 0; j < binaryString.length; j++) {
bytes[j] = binaryString.charCodeAt(j);
}
// 确保数据是Buffer类型
const imageBuffer = Buffer.from(bytes);
// 添加图像到Word文档
doc.addSection({
properties: {},
children: [
new Paragraph({
children: [
new ImageRun({
data: imageBuffer,
transformation: {
width: 600,
height: 600 * (viewport.height / viewport.width)
}
})
]
}),
new Paragraph({
text: `${page.pageNumber}`,
alignment: AlignmentType.CENTER
})
]
});
} else {
// 仅提取文本内容
let pageText = '';
let lastY = -1;
// 处理文本项
textContent.items.forEach(item => {
// 如果Y坐标变化添加换行
if (lastY !== -1 && lastY !== item.transform[5]) {
pageText += '\n';
}
pageText += item.str;
lastY = item.transform[5];
});
// 添加文本到Word文档
doc.addSection({
properties: {},
children: [
new Paragraph({
text: `${page.pageNumber}`,
heading: HeadingLevel.HEADING_1,
alignment: AlignmentType.CENTER
}),
new Paragraph({
text: pageText
})
]
});
}
processedCount++;
}
// 生成Word文档
document.getElementById('loading-text').textContent = '正在生成Word文档...';
const buffer = await Packer.toBlob(doc);
// 保存文件
saveAs(buffer, `${pdfFile.name.replace('.pdf', '')}.docx`);
// 隐藏加载状态
loadingContainer.style.display = 'none';
} catch (error) {
console.error('Word导出错误:', error);
showError('导出Word文档时出错');
loadingContainer.style.display = 'none';
}
} }

View File

@ -3,21 +3,38 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="description" content="PDF转图片工具 - 在浏览器中安全地将PDF转换为图片" /> <meta name="description" content="PDF转工具 - 在浏览器中安全地将PDF转换为图片或Word文档" />
<title>PDF转图片工具</title> <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@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="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="cssjs/css/style.css" /> <link rel="stylesheet" href="cssjs/css/style.css" />
<link rel="shortcut icon" href="Pdf.svg"> <link rel="shortcut icon" href="Pdf.svg">
<!-- 先加载 FileSaver.js --> <!-- 先加载依赖库 -->
<script>
// 添加完整的 Buffer polyfill
if (typeof window.Buffer === 'undefined') {
window.Buffer = {
from: function(data, encoding) {
if (typeof data === 'string') {
return new Uint8Array(data.split('').map(c => c.charCodeAt(0)));
}
return new Uint8Array(data);
},
isBuffer: function(obj) {
return obj instanceof Uint8Array;
}
};
}
</script>
<script src="cssjs/js/docx.min.js"></script>
<script src="cssjs/js/FileSaver.min.js"></script> <script src="cssjs/js/FileSaver.min.js"></script>
</head> </head>
<body> <body>
<div class="app-container"> <div class="app-container">
<div class="header"> <div class="header">
<h1>PDF转图片工具</h1> <h1>PDF转工具</h1>
<p>安全、高效地将PDF文件转换为高质量图片所有处理均在浏览器中完成</p> <p>安全、高效地将PDF文件转换为高质量图片或Word文档,所有处理均在浏览器中完成</p>
</div> </div>
<div class="card mb-4"> <div class="card mb-4">
@ -58,19 +75,38 @@
</div> </div>
<div class="export-options"> <div class="export-options">
<div class="form-check form-switch"> <div class="export-type-selector mb-3">
<input class="form-check-input" type="checkbox" id="combine-pages-switch"> <div class="btn-group" role="group" aria-label="导出类型选择">
<label class="form-check-label" for="combine-pages-switch">合并为单张图片</label> <input type="radio" class="btn-check" name="export-type" id="export-image" autocomplete="off" checked>
<label class="btn btn-outline-primary" for="export-image"><i class="bi bi-file-earmark-image"></i> 导出为图片</label>
<input type="radio" class="btn-check" name="export-type" id="export-word" autocomplete="off">
<label class="btn btn-outline-primary" for="export-word"><i class="bi bi-file-earmark-word"></i> 导出为Word</label>
</div>
</div> </div>
<div class="form-check form-switch"> <div id="image-options">
<input class="form-check-input" type="checkbox" id="high-quality-switch" checked> <div class="form-check form-switch">
<label class="form-check-label" for="high-quality-switch">高质量输出</label> <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> </div>
<div class="ms-auto"> <div id="word-options" style="display: none;">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="preserve-layout-switch" checked>
<label class="form-check-label" for="preserve-layout-switch">尽量保留原始布局</label>
</div>
</div>
<div class="ms-auto mt-3">
<button class="btn btn-primary" id="export-btn"> <button class="btn btn-primary" id="export-btn">
<i class="bi bi-download"></i> 导出图片 <i class="bi bi-download"></i> 导出文件
</button> </button>
</div> </div>
</div> </div>
@ -84,7 +120,7 @@
</div> </div>
<footer> <footer>
<p>© 2025 PDF转图片工具 | <a href="https://ckk.photo8.site/Photo8/pdf2img" target="_blank">GitHub</a></p> <p>© 2025 PDF转工具 | <a href="https://ckk.photo8.site/Photo8/pdf2img" target="_blank">GitHub</a></p>
</footer> </footer>
</div> </div>