commit 85142d373e90b4f915a92ebad31668116405c1f1 Author: Snowz <372492339@qq.com> Date: Sun Apr 13 02:26:28 2025 +0800 feat: 新增PDF转图片工具的前端实现 添加了完整的PDF转图片工具的前端实现,包括HTML、CSS、JavaScript代码。该工具允许用户在浏览器中安全地将PDF文件转换为图片,支持单页导出或合并为单张长图,并提供了实时预览功能。所有处理均在本地完成,无需上传文件到服务器,确保用户数据安全。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6cd5749 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 PDF转图片工具 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d0abbe --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# PDF转图片工具 + +一个纯前端的PDF转图片工具,所有处理均在浏览器中完成,无需上传文件到服务器,保证用户数据安全。 + +## 功能特点 + +- 🔒 **安全可靠**:所有处理均在本地浏览器中完成,无需上传文件到服务器 +- 🚀 **高效转换**:快速将PDF文件转换为高质量图片 +- 📱 **响应式设计**:适配各种设备屏幕 +- 🖼️ **多种导出选项**:支持导出单页图片或合并为单张长图 +- 🔍 **实时预览**:转换前可预览PDF内容 +- 📦 **批量处理**:支持多页PDF一次性处理 + +## 使用方法 + +1. 打开网页应用 +2. 拖放PDF文件到指定区域或点击"选择文件"按钮 +3. 等待PDF加载和预览生成 +4. 选择导出选项(单页图片或合并为单张图片) +5. 点击"导出图片"按钮 +6. 下载生成的图片文件 + +## 本地部署 + +1. 克隆本仓库 +2. 确保`cssjs/js`目录下包含所有必要的JS库文件 +3. 使用Web服务器(如Nginx、Apache等)提供静态文件服务 +4. 访问index.html即可使用 + +## 所需JS库文件 + +本项目依赖以下JS库,请下载并放置在`cssjs/js`目录下: + +1. [pdf.js](https://cdn.jsdelivr.net/npm/pdf.js@3.4.120/build/pdf.min.js) - PDF渲染核心库 +2. [pdf.worker.js](https://cdn.jsdelivr.net/npm/pdf.js@3.4.120/build/pdf.worker.min.js) - PDF.js工作线程 +3. [jszip.min.js](https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js) - 用于创建ZIP文件 +4. [FileSaver.min.js](https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js) - 用于保存文件 + +## 技术栈 + +- HTML5 / CSS3 +- JavaScript (ES6+) +- [PDF.js](https://mozilla.github.io/pdf.js/) - Mozilla的PDF渲染库 +- [JSZip](https://stuk.github.io/jszip/) - 用于创建ZIP文件的JavaScript库 +- [FileSaver.js](https://github.com/eligrey/FileSaver.js/) - 客户端保存文件的解决方案 +- [Bootstrap 5](https://getbootstrap.com/) - 用于UI组件和响应式设计 +- [Bootstrap Icons](https://icons.getbootstrap.com/) - 图标库 + +## 致谢 + +本项目基于原始的PDF转图片工具进行了重构和改进,感谢原项目的开发者提供的基础功能和灵感。 + +## 许可证 + +本项目采用MIT许可证,详情请查看[LICENSE](LICENSE)文件。 \ No newline at end of file diff --git a/cssjs/css/style.css b/cssjs/css/style.css new file mode 100644 index 0000000..a9f7989 --- /dev/null +++ b/cssjs/css/style.css @@ -0,0 +1,211 @@ +:root { + --primary-color: #4361ee; + --secondary-color: #3f37c9; + --accent-color: #4895ef; + --light-color: #f8f9fa; + --dark-color: #212529; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f5f7fa; + color: var(--dark-color); + line-height: 1.6; +} + +.app-container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1rem; +} + +.header { + text-align: center; + margin-bottom: 2rem; +} + +.header h1 { + font-weight: 700; + color: var(--primary-color); + margin-bottom: 0.5rem; +} + +.header p { + color: #6c757d; + font-size: 1.1rem; +} + +.card { + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + border: none; + overflow: hidden; + transition: transform 0.3s ease; +} + +.card:hover { + transform: translateY(-5px); +} + +.card-header { + background-color: var(--primary-color); + color: white; + font-weight: 600; + padding: 1rem; +} + +.upload-area { + border: 2px dashed #dee2e6; + border-radius: 8px; + padding: 3rem 2rem; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + background-color: #f8f9fa; +} + +.upload-area:hover, .upload-area.dragover { + border-color: var(--primary-color); + background-color: rgba(67, 97, 238, 0.05); +} + +.upload-icon { + font-size: 3rem; + color: var(--primary-color); + margin-bottom: 1rem; +} + +.btn-primary { + background-color: var(--primary-color); + border-color: var(--primary-color); +} + +.btn-primary:hover { + background-color: var(--secondary-color); + border-color: var(--secondary-color); +} + +.btn-outline-primary { + color: var(--primary-color); + border-color: var(--primary-color); +} + +.btn-outline-primary:hover { + background-color: var(--primary-color); + color: white; +} + +.progress { + height: 10px; + border-radius: 5px; +} + +.pdf-info { + display: flex; + justify-content: space-between; + margin-bottom: 1rem; +} + +.pdf-info-item { + text-align: center; + padding: 0.5rem; + background-color: #f8f9fa; + border-radius: 8px; + flex: 1; + margin: 0 0.5rem; +} + +.pdf-info-item .value { + font-size: 1.2rem; + font-weight: 600; + color: var(--primary-color); +} + +.pdf-info-item .label { + font-size: 0.9rem; + color: #6c757d; +} + +.preview-container { + margin-top: 2rem; +} + +.preview-item { + margin-bottom: 2rem; + position: relative; +} + +.preview-item canvas { + width: 100%; + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.preview-item .page-number { + position: absolute; + top: 10px; + right: 10px; + background-color: rgba(0, 0, 0, 0.7); + color: white; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; +} + +.export-options { + margin-top: 1rem; + display: flex; + align-items: center; +} + +.export-options .form-check { + margin-right: 1.5rem; +} + +footer { + text-align: center; + margin-top: 3rem; + padding-top: 1rem; + border-top: 1px solid #dee2e6; + color: #6c757d; +} + +@media (max-width: 768px) { + .pdf-info { + flex-direction: column; + } + + .pdf-info-item { + margin: 0.5rem 0; + } + + .export-options { + flex-direction: column; + align-items: flex-start; + } + + .export-options .form-check { + margin-bottom: 0.5rem; + } +} + +/* 加载动画 */ +.spinner { + width: 40px; + height: 40px; + margin: 100px auto; + background-color: var(--primary-color); + border-radius: 100%; + -webkit-animation: sk-scaleout 1.0s infinite ease-in-out; + animation: sk-scaleout 1.0s infinite ease-in-out; +} + +@-webkit-keyframes sk-scaleout { + 0% { -webkit-transform: scale(0) } + 100% { -webkit-transform: scale(1.0); opacity: 0; } +} + +@keyframes sk-scaleout { + 0% { transform: scale(0); -webkit-transform: scale(0); } + 100% { transform: scale(1.0); -webkit-transform: scale(1.0); opacity: 0; } +} \ No newline at end of file diff --git a/cssjs/js/main.js b/cssjs/js/main.js new file mode 100644 index 0000000..e26c862 --- /dev/null +++ b/cssjs/js/main.js @@ -0,0 +1,376 @@ +// 设置PDF.js worker路径 +pdfjsLib.GlobalWorkerOptions.workerSrc = 'cssjs/js/pdf.worker.js'; + +// 全局变量 +let pdfDocument = null; +let pdfFile = null; +let renderedPages = []; + +// DOM元素 +const uploadArea = document.getElementById('upload-area'); +const pdfFileInput = document.getElementById('pdf-file-input'); +const selectFileBtn = document.getElementById('select-file-btn'); +const loadingContainer = document.getElementById('loading-container'); +const pdfInfoContainer = document.getElementById('pdf-info-container'); +const previewContainer = document.getElementById('preview-container'); +const previewItems = document.getElementById('preview-items'); +const exportBtn = document.getElementById('export-btn'); +const combinePagesSwitch = document.getElementById('combine-pages-switch'); +const highQualitySwitch = document.getElementById('high-quality-switch'); + +// PDF信息显示元素 +const pdfNameValue = document.getElementById('pdf-name-value'); +const pdfSizeValue = document.getElementById('pdf-size-value'); +const pdfPagesValue = document.getElementById('pdf-pages-value'); + +// 事件监听器 +document.addEventListener('DOMContentLoaded', function() { + // 检查PDF.js是否正确加载 + if (typeof pdfjsLib === 'undefined') { + alert('PDF.js库加载失败,请确保pdf.js文件已正确放置'); + return; + } + + // 初始化事件监听 + initEventListeners(); +}); + +// 初始化事件监听器 +function initEventListeners() { + selectFileBtn.addEventListener('click', () => pdfFileInput.click()); + pdfFileInput.addEventListener('change', handleFileSelect); + exportBtn.addEventListener('click', exportImages); + + // 拖放功能 + uploadArea.addEventListener('dragover', (e) => { + e.preventDefault(); + uploadArea.classList.add('dragover'); + }); + + uploadArea.addEventListener('dragleave', () => { + uploadArea.classList.remove('dragover'); + }); + + uploadArea.addEventListener('drop', (e) => { + e.preventDefault(); + uploadArea.classList.remove('dragover'); + + if (e.dataTransfer.files.length > 0) { + const file = e.dataTransfer.files[0]; + if (file.type === 'application/pdf') { + pdfFileInput.files = e.dataTransfer.files; + handleFileSelect(e); + } else { + showError('请选择PDF文件'); + } + } + }); +} + +// 处理文件选择 +function handleFileSelect(e) { + const file = pdfFileInput.files[0]; + + if (!file) return; + + if (file.type !== 'application/pdf') { + showError('请选择PDF文件'); + return; + } + + if (file.size > 20 * 1024 * 1024) { // 20MB + showError('文件大小不能超过20MB'); + return; + } + + pdfFile = file; + + // 显示加载状态 + uploadArea.style.display = 'none'; + loadingContainer.style.display = 'block'; + previewContainer.style.display = 'none'; + pdfInfoContainer.style.display = 'none'; + previewItems.innerHTML = ''; + renderedPages = []; + + // 读取PDF文件 + const reader = new FileReader(); + reader.onload = function(event) { + const typedArray = new Uint8Array(event.target.result); + loadPdfFromData(typedArray); + }; + reader.readAsArrayBuffer(file); +} + +// 从ArrayBuffer加载PDF +function loadPdfFromData(data) { + pdfjsLib.getDocument({ data }).promise + .then(pdf => { + pdfDocument = pdf; + + // 更新PDF信息 + pdfNameValue.textContent = pdfFile.name; + pdfSizeValue.textContent = formatFileSize(pdfFile.size); + pdfPagesValue.textContent = pdf.numPages; + + // 渲染预览 + renderPdfPreview(pdf); + }) + .catch(error => { + console.error('PDF加载错误:', error); + showError('无法加载PDF文件,请确保文件未损坏'); + resetUI(); + }); +} + +// 渲染PDF预览 +function renderPdfPreview(pdf) { + const totalPages = pdf.numPages; + let renderedCount = 0; + + // 更新加载文本 + document.getElementById('loading-text').textContent = `正在渲染预览 (0/${totalPages})`; + + // 为每一页创建预览 + for (let pageNumber = 1; pageNumber <= totalPages; pageNumber++) { + pdf.getPage(pageNumber).then(page => { + const scale = 0.5; // 预览缩放比例 + const viewport = page.getViewport({ scale }); + + // 创建canvas元素 + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.width = viewport.width; + canvas.height = viewport.height; + + // 渲染PDF页面到canvas + const renderContext = { + canvasContext: context, + viewport: viewport + }; + + page.render(renderContext).promise.then(() => { + renderedCount++; + document.getElementById('loading-text').textContent = `正在渲染预览 (${renderedCount}/${totalPages})`; + + // 存储渲染的页面 + renderedPages[pageNumber - 1] = { + pageNumber: pageNumber, + canvas: canvas, + width: viewport.width, + height: viewport.height + }; + + // 如果所有页面都已渲染,显示预览 + if (renderedCount === totalPages) { + displayPreviews(); + } + }); + }); + } +} + +// 显示预览 +function displayPreviews() { + // 按页码排序 + renderedPages.sort((a, b) => a.pageNumber - b.pageNumber); + + // 清空预览容器 + previewItems.innerHTML = ''; + + // 添加每一页的预览 + renderedPages.forEach(page => { + const colDiv = document.createElement('div'); + colDiv.className = 'col-md-6 col-lg-4 preview-item'; + + const pageNumberDiv = document.createElement('div'); + pageNumberDiv.className = 'page-number'; + pageNumberDiv.textContent = `第 ${page.pageNumber} 页`; + + // 克隆canvas以避免原始canvas被修改 + const displayCanvas = document.createElement('canvas'); + displayCanvas.width = page.canvas.width; + displayCanvas.height = page.canvas.height; + const displayContext = displayCanvas.getContext('2d'); + displayContext.drawImage(page.canvas, 0, 0); + + colDiv.appendChild(displayCanvas); + colDiv.appendChild(pageNumberDiv); + previewItems.appendChild(colDiv); + }); + + // 显示预览和控制面板 + loadingContainer.style.display = 'none'; + pdfInfoContainer.style.display = 'block'; + previewContainer.style.display = 'block'; +} + +// 导出图片 +function exportImages() { + if (!pdfDocument || renderedPages.length === 0) { + showError('没有可导出的内容'); + return; + } + + const combinePages = combinePagesSwitch.checked; + const highQuality = highQualitySwitch.checked; + const scale = highQuality ? 2.0 : 1.0; + + // 显示加载状态 + loadingContainer.style.display = 'block'; + document.getElementById('loading-text').textContent = '正在准备导出...'; + + if (combinePages) { + // 合并为单张图片 + exportCombinedImage(scale); + } else { + // 导出为多张图片 + exportMultipleImages(scale); + } +} + +// 导出合并的单张图片 +function exportCombinedImage(scale) { + // 计算合并后的图片尺寸 + let totalHeight = 0; + let maxWidth = 0; + + renderedPages.forEach(page => { + totalHeight += page.height * (scale / 0.5); + maxWidth = Math.max(maxWidth, page.width * (scale / 0.5)); + }); + + // 创建合并的canvas + const combinedCanvas = document.createElement('canvas'); + combinedCanvas.width = maxWidth; + combinedCanvas.height = totalHeight; + const combinedContext = combinedCanvas.getContext('2d'); + + // 填充白色背景 + combinedContext.fillStyle = '#FFFFFF'; + combinedContext.fillRect(0, 0, combinedCanvas.width, combinedCanvas.height); + + // 重新渲染每一页到合并的canvas + let currentY = 0; + let renderedCount = 0; + + const renderNextPage = (index) => { + if (index >= renderedPages.length) { + // 所有页面都已渲染,导出图片 + combinedCanvas.toBlob(blob => { + saveAs(blob, `${pdfFile.name.replace('.pdf', '')}_combined.png`); + loadingContainer.style.display = 'none'; + }, 'image/png'); + return; + } + + const page = renderedPages[index]; + + // 更新加载文本 + document.getElementById('loading-text').textContent = `正在合并页面 (${index + 1}/${renderedPages.length})`; + + // 获取原始页面 + pdfDocument.getPage(page.pageNumber).then(pdfPage => { + const viewport = pdfPage.getViewport({ scale }); + + // 创建临时canvas + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = viewport.width; + tempCanvas.height = viewport.height; + const tempContext = tempCanvas.getContext('2d'); + + // 渲染到临时canvas + const renderContext = { + canvasContext: tempContext, + viewport: viewport + }; + + pdfPage.render(renderContext).promise.then(() => { + // 将临时canvas的内容绘制到合并的canvas + const x = (maxWidth - viewport.width) / 2; // 居中 + combinedContext.drawImage(tempCanvas, x, currentY); + + // 更新Y坐标 + currentY += viewport.height; + + // 渲染下一页 + renderNextPage(index + 1); + }); + }); + }; + + // 开始渲染第一页 + renderNextPage(0); +} + +// 导出多张图片 +function exportMultipleImages(scale) { + const zip = new JSZip(); + const folder = zip.folder("images"); + let processedCount = 0; + + // 更新加载文本 + document.getElementById('loading-text').textContent = `正在导出图片 (0/${renderedPages.length})`; + + // 处理每一页 + renderedPages.forEach((page, index) => { + // 获取原始页面 + pdfDocument.getPage(page.pageNumber).then(pdfPage => { + const viewport = pdfPage.getViewport({ scale }); + + // 创建canvas + const canvas = document.createElement('canvas'); + canvas.width = viewport.width; + canvas.height = viewport.height; + const context = canvas.getContext('2d'); + + // 填充白色背景 + context.fillStyle = '#FFFFFF'; + context.fillRect(0, 0, canvas.width, canvas.height); + + // 渲染PDF页面到canvas + const renderContext = { + canvasContext: context, + viewport: viewport + }; + + pdfPage.render(renderContext).promise.then(() => { + // 将canvas转换为blob + canvas.toBlob(blob => { + // 添加到zip + folder.file(`page_${page.pageNumber}.png`, blob); + + processedCount++; + document.getElementById('loading-text').textContent = `正在导出图片 (${processedCount}/${renderedPages.length})`; + + // 如果所有页面都已处理,生成并下载zip + if (processedCount === renderedPages.length) { + zip.generateAsync({ type: 'blob' }).then(content => { + saveAs(content, `${pdfFile.name.replace('.pdf', '')}_images.zip`); + loadingContainer.style.display = 'none'; + }); + } + }, 'image/png'); + }); + }); + }); +} + +// 辅助函数 +function formatFileSize(bytes) { + if (bytes < 1024) return bytes + ' B'; + else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + ' KB'; + else return (bytes / 1048576).toFixed(2) + ' MB'; +} + +function showError(message) { + alert(message); +} + +function resetUI() { + uploadArea.style.display = 'block'; + loadingContainer.style.display = 'none'; + pdfInfoContainer.style.display = 'none'; + previewContainer.style.display = 'none'; + pdfFileInput.value = ''; +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..f92a513 --- /dev/null +++ b/index.html @@ -0,0 +1,94 @@ + + +
+ + + +安全、高效地将PDF文件转换为高质量图片,所有处理均在浏览器中完成
+或者
+ + +最大文件大小: 20MB
+