feat: 更新PDF转换工具,支持导出为Word文档
修改了index.html以更新标题和描述,增强了用户界面,添加了导出为Word文档的功能。更新了README.md以反映新功能,增加了使用方法和功能特点的描述。同时,调整了CSS样式以支持新的导出选项,确保用户体验流畅。修复了LICENSE文件的格式问题。
This commit is contained in:
parent
31a1bccb6f
commit
2565754d83
40
LICENSE
40
LICENSE
@ -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
154
README.md
@ -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)文件。
|
@ -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
19
cssjs/js/docx.esm.js
Normal 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
21620
cssjs/js/docx.min.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
928
cssjs/js/main.js
928
cssjs/js/main.js
@ -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';
|
||||||
|
}
|
||||||
}
|
}
|
64
index.html
64
index.html
@ -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>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user