Compare commits
No commits in common. "test" and "master" have entirely different histories.
16
README.md
16
README.md
@ -1,25 +1,24 @@
|
|||||||
# PDF转换工具
|
# PDF转图片工具
|
||||||
|
|
||||||
一个纯前端的PDF转换工具,支持将PDF转换为图片或Word文档,所有处理均在浏览器中完成,无需上传文件到服务器,保证用户数据安全。
|
一个纯前端的PDF转图片工具,所有处理均在浏览器中完成,无需上传文件到服务器,保证用户数据安全。
|
||||||
|
|
||||||
## 功能特点
|
## 功能特点
|
||||||
|
|
||||||
- 🔒 **安全可靠**:所有处理均在本地浏览器中完成,无需上传文件到服务器
|
- 🔒 **安全可靠**:所有处理均在本地浏览器中完成,无需上传文件到服务器
|
||||||
- 🚀 **高效转换**:快速将PDF文件转换为高质量图片或Word文档
|
- 🚀 **高效转换**:快速将PDF文件转换为高质量图片
|
||||||
- 📱 **响应式设计**:适配各种设备屏幕
|
- 📱 **响应式设计**:适配各种设备屏幕
|
||||||
- 🖼️ **多种导出选项**:支持导出单页图片、合并为单张长图或转换为Word文档
|
- 🖼️ **多种导出选项**:支持导出单页图片或合并为单张长图
|
||||||
- 🔍 **实时预览**:转换前可预览PDF内容
|
- 🔍 **实时预览**:转换前可预览PDF内容
|
||||||
- 📦 **批量处理**:支持多页PDF一次性处理
|
- 📦 **批量处理**:支持多页PDF一次性处理
|
||||||
- 📄 **Word转换**:支持将PDF转换为可编辑的Word文档
|
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
1. 打开网页应用
|
1. 打开网页应用
|
||||||
2. 拖放PDF文件到指定区域或点击"选择文件"按钮
|
2. 拖放PDF文件到指定区域或点击"选择文件"按钮
|
||||||
3. 等待PDF加载和预览生成
|
3. 等待PDF加载和预览生成
|
||||||
4. 选择导出类型(图片或Word)和相关选项
|
4. 选择导出选项(单页图片或合并为单张图片)
|
||||||
5. 点击"导出文件"按钮
|
5. 点击"导出图片"按钮
|
||||||
6. 下载生成的图片或Word文档
|
6. 下载生成的图片文件
|
||||||
|
|
||||||
## 本地部署
|
## 本地部署
|
||||||
|
|
||||||
@ -70,7 +69,6 @@ types {
|
|||||||
- [PDF.js](https://mozilla.github.io/pdf.js/) - Mozilla的PDF渲染库
|
- [PDF.js](https://mozilla.github.io/pdf.js/) - Mozilla的PDF渲染库
|
||||||
- [JSZip](https://stuk.github.io/jszip/) - 用于创建ZIP文件的JavaScript库
|
- [JSZip](https://stuk.github.io/jszip/) - 用于创建ZIP文件的JavaScript库
|
||||||
- [FileSaver.js](https://github.com/eligrey/FileSaver.js/) - 客户端保存文件的解决方案
|
- [FileSaver.js](https://github.com/eligrey/FileSaver.js/) - 客户端保存文件的解决方案
|
||||||
- [docx](https://github.com/dolanmiu/docx) - 用于生成Word文档的JavaScript库
|
|
||||||
- [Bootstrap 5](https://getbootstrap.com/) - 用于UI组件和响应式设计
|
- [Bootstrap 5](https://getbootstrap.com/) - 用于UI组件和响应式设计
|
||||||
- [Bootstrap Icons](https://icons.getbootstrap.com/) - 图标库
|
- [Bootstrap Icons](https://icons.getbootstrap.com/) - 图标库
|
||||||
|
|
||||||
|
@ -154,26 +154,12 @@ body {
|
|||||||
|
|
||||||
.export-options {
|
.export-options {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.export-options .form-check {
|
.export-options .form-check {
|
||||||
margin-right: 1.5rem;
|
margin-right: 1.5rem;
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.export-type-selector {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline-primary {
|
|
||||||
color: var(--primary-color);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline-primary:hover, .btn-check:checked + .btn-outline-primary {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
// 等待 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
21620
cssjs/js/docx.min.js
vendored
File diff suppressed because it is too large
Load Diff
188
cssjs/js/main.js
188
cssjs/js/main.js
@ -1,7 +1,6 @@
|
|||||||
// 导入依赖库
|
// 导入依赖库
|
||||||
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路径
|
// 设置PDF.js worker路径
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'cssjs/js/pdf.worker.mjs';
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'cssjs/js/pdf.worker.mjs';
|
||||||
@ -22,11 +21,6 @@ const previewItems = document.getElementById('preview-items');
|
|||||||
const exportBtn = document.getElementById('export-btn');
|
const exportBtn = document.getElementById('export-btn');
|
||||||
const combinePagesSwitch = document.getElementById('combine-pages-switch');
|
const combinePagesSwitch = document.getElementById('combine-pages-switch');
|
||||||
const highQualitySwitch = document.getElementById('high-quality-switch');
|
const highQualitySwitch = document.getElementById('high-quality-switch');
|
||||||
const preserveLayoutSwitch = document.getElementById('preserve-layout-switch');
|
|
||||||
const exportImageRadio = document.getElementById('export-image');
|
|
||||||
const exportWordRadio = document.getElementById('export-word');
|
|
||||||
const imageOptions = document.getElementById('image-options');
|
|
||||||
const wordOptions = document.getElementById('word-options');
|
|
||||||
|
|
||||||
// PDF信息显示元素
|
// PDF信息显示元素
|
||||||
const pdfNameValue = document.getElementById('pdf-name-value');
|
const pdfNameValue = document.getElementById('pdf-name-value');
|
||||||
@ -43,11 +37,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
function initEventListeners() {
|
function initEventListeners() {
|
||||||
selectFileBtn.addEventListener('click', () => pdfFileInput.click());
|
selectFileBtn.addEventListener('click', () => pdfFileInput.click());
|
||||||
pdfFileInput.addEventListener('change', handleFileSelect);
|
pdfFileInput.addEventListener('change', handleFileSelect);
|
||||||
exportBtn.addEventListener('click', handleExport);
|
exportBtn.addEventListener('click', exportImages);
|
||||||
|
|
||||||
// 导出类型切换
|
|
||||||
exportImageRadio.addEventListener('change', updateExportOptions);
|
|
||||||
exportWordRadio.addEventListener('change', updateExportOptions);
|
|
||||||
|
|
||||||
// 拖放功能
|
// 拖放功能
|
||||||
uploadArea.addEventListener('dragover', (e) => {
|
uploadArea.addEventListener('dragover', (e) => {
|
||||||
@ -75,30 +65,8 @@ function initEventListeners() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新导出选项显示
|
// 其余函数保持不变
|
||||||
function updateExportOptions() {
|
// ...
|
||||||
if (exportImageRadio.checked) {
|
|
||||||
imageOptions.style.display = 'block';
|
|
||||||
wordOptions.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
imageOptions.style.display = 'none';
|
|
||||||
wordOptions.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理导出按钮点击
|
|
||||||
function handleExport() {
|
|
||||||
if (!pdfDocument || renderedPages.length === 0) {
|
|
||||||
showError('没有可导出的内容');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exportImageRadio.checked) {
|
|
||||||
exportImages();
|
|
||||||
} else {
|
|
||||||
exportWord();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理文件选择
|
// 处理文件选择
|
||||||
function handleFileSelect(e) {
|
function handleFileSelect(e) {
|
||||||
@ -240,6 +208,10 @@ function displayPreviews() {
|
|||||||
|
|
||||||
// 导出图片
|
// 导出图片
|
||||||
function exportImages() {
|
function exportImages() {
|
||||||
|
if (!pdfDocument || renderedPages.length === 0) {
|
||||||
|
showError('没有可导出的内容');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const combinePages = combinePagesSwitch.checked;
|
const combinePages = combinePagesSwitch.checked;
|
||||||
const highQuality = highQualitySwitch.checked;
|
const highQuality = highQualitySwitch.checked;
|
||||||
@ -404,149 +376,3 @@ function resetUI() {
|
|||||||
previewContainer.style.display = 'none';
|
previewContainer.style.display = 'none';
|
||||||
pdfFileInput.value = '';
|
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,38 +3,21 @@
|
|||||||
<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转换为图片或Word文档" />
|
<meta name="description" content="PDF转图片工具 - 在浏览器中安全地将PDF转换为图片" />
|
||||||
<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文件转换为高质量图片或Word文档,所有处理均在浏览器中完成</p>
|
<p>安全、高效地将PDF文件转换为高质量图片,所有处理均在浏览器中完成</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
@ -75,38 +58,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="export-options">
|
<div class="export-options">
|
||||||
<div class="export-type-selector mb-3">
|
<div class="form-check form-switch">
|
||||||
<div class="btn-group" role="group" aria-label="导出类型选择">
|
<input class="form-check-input" type="checkbox" id="combine-pages-switch">
|
||||||
<input type="radio" class="btn-check" name="export-type" id="export-image" autocomplete="off" checked>
|
<label class="form-check-label" for="combine-pages-switch">合并为单张图片</label>
|
||||||
<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 id="image-options">
|
<div class="form-check form-switch">
|
||||||
<div class="form-check form-switch">
|
<input class="form-check-input" type="checkbox" id="high-quality-switch" checked>
|
||||||
<input class="form-check-input" type="checkbox" id="combine-pages-switch">
|
<label class="form-check-label" for="high-quality-switch">高质量输出</label>
|
||||||
<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 id="word-options" style="display: none;">
|
<div class="ms-auto">
|
||||||
<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>
|
||||||
@ -120,7 +84,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