上传文件,在前端用 pdf.js 提取 上传的pdf文件中的图片

上传文件,在前端用 pdf.js 提取 上传的pdf文件中的图片

在线访问:

https://chat.xutongbao.top/nextjs/light/pdf

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>PDF 图片提取工具</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; } .container { max-width: 1200px; margin: 0 auto; } .header { text-align: center; color: white; margin-bottom: 30px; } .header h1 { font-size: 36px; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); } .header p { font-size: 16px; opacity: 0.9; } .upload-card { background: white; border-radius: 16px; padding: 40px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); margin-bottom: 30px; } .upload-area { border: 3px dashed #667eea; border-radius: 12px; padding: 60px 20px; text-align: center; cursor: pointer; transition: all 0.3s ease; background: #f8f9ff; } .upload-area:hover { border-color: #764ba2; background: #f0f2ff; transform: translateY(-2px); } .upload-area.dragover { border-color: #764ba2; background: #e8ebff; transform: scale(1.02); } .upload-icon { font-size: 48px; margin-bottom: 20px; } .upload-text { font-size: 18px; color: #333; margin-bottom: 10px; font-weight: 600; } .upload-hint { font-size: 14px; color: #666; } #fileInput { display: none; } .btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 12px 30px; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); } .btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6); } .btn:active { transform: translateY(0); } .btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; } .progress-container { display: none; margin-top: 20px; } .progress-bar { width: 100%; height: 8px; background: #e0e0e0; border-radius: 4px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); border-radius: 4px; transition: width 0.3s ease; width: 0%; } .progress-text { text-align: center; margin-top: 10px; color: #666; font-size: 14px; } .images-container { display: none; background: white; border-radius: 16px; padding: 40px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); } .images-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #f0f0f0; } .images-title { font-size: 24px; font-weight: 700; color: #333; } .images-count { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 8px 20px; border-radius: 20px; font-size: 14px; font-weight: 600; } .images-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; } .image-card { border: 2px solid #e0e0e0; border-radius: 12px; overflow: hidden; transition: all 0.3s ease; background: white; } .image-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); border-color: #667eea; } .image-wrapper { width: 100%; height: 200px; display: flex; align-items: center; justify-content: center; background: #f8f9fa; overflow: hidden; } .image-wrapper img { max-width: 100%; max-height: 100%; object-fit: contain; } .image-info { padding: 15px; background: white; } .image-name { font-size: 14px; color: #333; margin-bottom: 8px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .image-meta { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #999; margin-bottom: 12px; } .image-actions { display: flex; gap: 8px; } .btn-small { flex: 1; padding: 8px 16px; font-size: 13px; border-radius: 6px; border: none; cursor: pointer; transition: all 0.2s ease; font-weight: 600; } .btn-download { background: #667eea; color: white; } .btn-download:hover { background: #5568d3; transform: translateY(-1px); } .btn-preview { background: #f0f0f0; color: #333; } .btn-preview:hover { background: #e0e0e0; } .empty-state { text-align: center; padding: 60px 20px; color: #999; } .empty-icon { font-size: 64px; margin-bottom: 20px; opacity: 0.5; } .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.9); z-index: 1000; align-items: center; justify-content: center; } .modal.active { display: flex; } .modal-content { max-width: 90%; max-height: 90%; position: relative; } .modal-image { max-width: 100%; max-height: 90vh; object-fit: contain; } .modal-close { position: absolute; top: -40px; right: 0; background: white; color: #333; border: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 20px; font-weight: bold; transition: all 0.2s ease; } .modal-close:hover { background: #f0f0f0; transform: rotate(90deg); } @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .image-card { animation: fadeIn 0.3s ease; } </style> </head> <body> <div> <div> <h1>📄 PDF 图片提取工具</h1> <p>上传 PDF 文件,自动提取其中的所有图片</p> </div> <div> <div> <div>📁</div> <div>点击或拖拽 PDF 文件到此处</div> <div>支持单个 PDF 文件上传</div> </div> <input type="file" accept=".pdf,application/pdf"> <div> <div> <div></div> </div> <div>处理中...</div> </div> </div> <div> <div> <div>提取的图片</div> <div>0 张图片</div> </div> <div></div> </div> </div> <div> <div> <button onclick="closeModal()">×</button> <img alt="预览"> </div> </div> <script> // 配置 PDF.js worker pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; const uploadArea = document.getElementById('uploadArea'); const fileInput = document.getElementById('fileInput'); const progressContainer = document.getElementById('progressContainer'); const progressFill = document.getElementById('progressFill'); const progressText = document.getElementById('progressText'); const imagesContainer = document.getElementById('imagesContainer'); const imagesGrid = document.getElementById('imagesGrid'); const imagesCount = document.getElementById('imagesCount'); let extractedImages = []; // 上传区域点击事件 uploadArea.addEventListener('click', () => { fileInput.click(); }); // 文件选择事件 fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file && file.type === 'application/pdf') { handleFile(file); } }); // 拖拽事件 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'); const file = e.dataTransfer.files[0]; if (file && file.type === 'application/pdf') { handleFile(file); } }); // 处理 PDF 文件 async function handleFile(file) { extractedImages = []; imagesGrid.innerHTML = ''; imagesContainer.style.display = 'none'; progressContainer.style.display = 'block'; try { const arrayBuffer = await file.arrayBuffer(); await extractImagesFromPDF(arrayBuffer, file.name); progressContainer.style.display = 'none'; displayImages(); } catch (error) { console.error('处理 PDF 失败:', error); progressText.textContent = '处理失败: ' + error.message; progressText.style.color = '#e74c3c'; } } // 提取 PDF 中的图片 async function extractImagesFromPDF(arrayBuffer, fileName) { const pdfDocument = await pdfjsLib.getDocument({ data: arrayBuffer, useSystemFonts: true, disableFontFace: false, verbosity: 0, isEvalSupported: false, maxImageSize: 1024 * 1024 * 10 }).promise; const totalPages = pdfDocument.numPages; let imageIndex = 0; for (let pageNum = 1; pageNum <= totalPages; pageNum++) { updateProgress(pageNum, totalPages); const page = await pdfDocument.getPage(pageNum); const operatorList = await page.getOperatorList(); for (let i = 0; i < operatorList.fnArray.length; i++) { const fn = operatorList.fnArray[i]; if (fn === pdfjsLib.OPS.paintImageXObject || fn === pdfjsLib.OPS.paintInlineImageXObject) { const imageName = operatorList.argsArray[i][0]; await new Promise((resolve) => { page.objs.get(imageName, async (img) => { console.log('Image object:', img); // 调试输出 if (!img) { resolve(); return; } try { // 检查是否有 bitmap 属性(ImageBitmap) if (img.bitmap && img.bitmap instanceof ImageBitmap) { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); // 使用 ImageBitmap 绘制 ctx.drawImage(img.bitmap, 0, 0); await finishImageProcessing(canvas, img, fileName, pageNum, imageIndex); resolve(); return; } // 如果 img 本身是 ImageBitmap if (window.ImageBitmap && img instanceof ImageBitmap) { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); await finishImageProcessing(canvas, img, fileName, pageNum, imageIndex); resolve(); return; } // 如果有 data 属性(像素数据) if (img.data && img.width && img.height) { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(img.width, img.height); imageData.data.set(img.data); ctx.putImageData(imageData, 0, 0); await finishImageProcessing(canvas, img, fileName, pageNum, imageIndex); resolve(); return; } // 如果是 HTMLImageElement 或 HTMLCanvasElement if (img instanceof HTMLImageElement || img instanceof HTMLCanvasElement) { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); await finishImageProcessing(canvas, img, fileName, pageNum, imageIndex); resolve(); return; } // 如果有 src 属性 if (img.src) { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); const image = new Image(); image.onload = async () => { ctx.drawImage(image, 0, 0); await finishImageProcessing(canvas, img, fileName, pageNum, imageIndex); resolve(); }; image.onerror = () => { console.error('加载图片失败'); resolve(); }; image.src = img.src; return; } // 无法处理的情况 console.warn('无法处理的图片对象:', { hasData: !!img.data, hasBitmap: !!img.bitmap, width: img.width, height: img.height, keys: Object.keys(img) }); resolve(); } catch (error) { console.error('处理图片失败:', error, img); resolve(); } }); }); imageIndex++; } } } } // 完成图片处理 function finishImageProcessing(canvas, img, fileName, pageNum, imageIndex) { return new Promise((resolve) => { canvas.toBlob((blob) => { if (blob) { const url = URL.createObjectURL(blob); const name = `${fileName.replace('.pdf', '')}_page${pageNum}_img${imageIndex}.png`; extractedImages.push({ url: url, name: name, size: blob.size, width: canvas.width, height: canvas.height, blob: blob }); } resolve(); }, 'image/png'); }); } // 更新进度 function updateProgress(current, total) { const percent = (current / total) * 100; progressFill.style.width = percent + '%'; progressText.textContent = `正在处理第 ${current}/${total} 页...`; } // 显示图片 function displayImages() { if (extractedImages.length === 0) { imagesContainer.style.display = 'block'; imagesGrid.innerHTML = ` <div> <div>🖼️</div> <div>未在 PDF 中找到图片</div> </div> `; imagesCount.textContent = '0 张图片'; return; } imagesContainer.style.display = 'block'; imagesCount.textContent = `${extractedImages.length} 张图片`; extractedImages.forEach((image, index) => { const card = document.createElement('div'); card.className = 'image-card'; card.style.animationDelay = `${index * 0.05}s`; card.innerHTML = ` <div> <img src="${image.url}" alt="${image.name}"> </div> <div> <div title="${image.name}">${image.name}</div> <div> <span>${image.width} × ${image.height}</span> <span>${formatBytes(image.size)}</span> </div> <div> <button onclick="previewImage('${image.url}')">预览</button> <button onclick="downloadImage(${index})">下载</button> </div> </div> `; imagesGrid.appendChild(card); }); } // 格式化文件大小 function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } // 预览图片 function previewImage(url) { document.getElementById('modalImage').src = url; document.getElementById('modal').classList.add('active'); } // 关闭模态框 function closeModal() { document.getElementById('modal').classList.remove('active'); } // 下载图片 function downloadImage(index) { const image = extractedImages[index]; const link = document.createElement('a'); link.href = image.url; link.download = image.name; link.click(); } // 点击模态框背景关闭 document.getElementById('modal').addEventListener('click', (e) => { if (e.target.id === 'modal') { closeModal(); } }); </script> </body> </html> 

Read more

AI绘画新体验:圣光艺苑一键生成鎏金画框艺术品(含提示词秘籍)

AI绘画新体验:圣光艺苑一键生成鎏金画框艺术品(含提示词秘籍) 1. 为什么说“圣光艺苑”不是又一个AI绘图工具? 你试过在深夜调了27次参数,只为让AI画出一张不歪脖子、不三只手、背景不糊成浆糊的肖像吗? 你是否也曾在一堆冷冰冰的滑块、下拉菜单和英文报错中,忘了自己最初想画的是什么——不是技术,而是一幅能挂在墙上的画。 圣光艺苑不一样。 它不叫“WebUI”,不标“v2.3.5-beta”,没有“CFG Scale”“Denoising Strength”这类让人皱眉的术语。它的界面是亚麻布纹理的,主色调是梵高《星空》里的深蓝与《向日葵》中的金黄;你输入的不是“prompt”,而是“绘意”;你排除的不是“negative prompt”,而是“避讳”;你启动的不是“Generate”,而是“🏺 挥毫泼墨”。 这不是把SDXL塞进一个漂亮外壳——它是把4090显卡的算力,

AI时代下的低代码复兴:开发民主化的新机遇

AI时代下的低代码复兴:开发民主化的新机遇

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕人工智能这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获! 文章目录 * AI时代下的低代码复兴:开发民主化的新机遇 🚀 * 引言:打破编程的“围城” * 第一部分:回望过去——低代码的兴衰与转型 * 1.1 传统低代码的痛点 * 1.2 枯木逢春:AI带来的“认知革命” * 第二部分:架构解析——AI是如何驱动低代码的? * 第三部分:实战演练——代码改变低代码 * 3.1 场景:构建一个“客户反馈管理系统” * 3.2 进阶:AI自动修复与优化 * 第四部分:机遇与挑战——谁乘风破浪? * 4.1 谁是受益者? * 4.

虚幻版Pico大空间VR入门教程 04 —— PicoOpenXR和PicoXR插件对于PDC串流、SteamVR串流、OpenXR串流对比 和 手势追踪对比

虚幻版Pico大空间VR入门教程 04 —— PicoOpenXR和PicoXR插件对于PDC串流、SteamVR串流、OpenXR串流对比 和 手势追踪对比

省流 串流方式最重要,笔者使用【Pico4UE 企业版】一体机,使用【PicoOpenXR插件+OpenXR插件】【企业串流v2.0的apk+exe应用】和【OpenXR串流方式】进行有线串流, 串流调试时可以正常手势追踪,打包apk和exe的VR手势追踪正常。 文章包含整理的百度云资源、SteamVR串流、不同UE版本的手势追踪对比记录,曾经的踩坑笔记(略长)。 插件文档 PicoXR和PicoOpenXR 插件文档 https://developer-cn.picoxr.com/document/ PicoXR 开发文档 https://developer-cn.picoxr.com/document/unreal/ PicoOpenXR 开发文档 https://developer-cn.picoxr.com/document/unreal-openxr/ 插件下载 PicoXR和PicoOpenXR Pico SDK

GPEN与Stable Diffusion结合:人像增强插件开发教程

GPEN与Stable Diffusion结合:人像增强插件开发教程 你是不是也遇到过这样的问题:用Stable Diffusion生成的人像图,细节不够锐利、皮肤质感偏塑料、五官轮廓略显模糊?或者修复老照片时,AI总在发际线、睫毛、耳垂这些精细部位“自由发挥”?别急——今天我们就来把GPEN这个专精人像修复的“细节控”模型,变成Stable Diffusion的得力助手。不是简单调用API,而是手把手带你开发一个真正可用、可集成、可复用的本地化人像增强插件。 这篇教程不讲空泛理论,不堆参数配置,只聚焦一件事:让你明天就能在WebUI里点一下,就把SD生成的图“唤醒”——让眼睛有神、皮肤有质、发丝有形、轮廓有骨。全程基于ZEEKLOG星图提供的GPEN人像修复增强模型镜像,开箱即用,零环境踩坑。 1. 为什么是GPEN?它和Stable Diffusion不是“同类选手” 先说清楚一个常见误解:GPEN不是另一个文生图模型,它不理解“赛博朋克风”或“水墨晕染”,也不生成新构图。它的核心能力非常纯粹—