跳到主要内容前端纯 JS 实现 PDF 图片提取工具 | 极客日志JavaScript大前端
前端纯 JS 实现 PDF 图片提取工具
利用 pdf.js 库在前端解析 PDF 并提取嵌入图片,无需后端支持。通过遍历页面操作符识别图像对象,结合 Canvas 转换格式为 PNG 供下载。代码包含拖拽上传、进度展示及图片预览功能,重点解决了不同图像数据类型的兼容处理问题,适合文档处理场景。
技术博主1 浏览 前言
在处理文档类应用时,我们经常遇到需要从 PDF 文件中提取嵌入图片的需求。传统方案往往依赖后端服务进行解析,但这会增加服务器负载并涉及隐私传输问题。利用浏览器端的 pdf.js 库,我们可以完全在前端完成这一过程。
下面是一个单文件解决方案,集成了拖拽上传、进度展示、图片预览及下载功能。代码逻辑清晰,适合直接集成到现有项目中。
完整代码实现
将以下代码保存为 .html 文件即可直接在浏览器运行。注意确保网络能访问 CDN 上的 pdf.js 资源。
<!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, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: ; : ; }
{ : ; : auto; }
{ : center; : white; : ; }
{ : white; : ; : ; : (, , , ); : ; }
{ : dashed ; : ; : ; : center; : pointer; : all ease; : ; }
{ : ; : ; : (-); }
{ : none; : white; : ; : ; : (, , , ); }
{ : grid; : (auto-fill, (, fr)); : ; }
{ : solid ; : ; : hidden; : all ease; : white; }
{ : ; : ; : contain; }
{ : (, , ); : white; : none; : ; : ; : pointer; }
{ : none; : fixed; : ; : ; : ; : ; : (, , , ); : ; : center; : center; }
{ : flex; }
{ : ; : ; }
📄 PDF 图片提取工具上传 PDF 文件,自动提取其中的所有图片
📁
点击或拖拽 PDF 文件到此处
支持单个 PDF 文件上传
处理中...
提取的图片
0 张图片
×
100vh
padding
20px
.container
max-width
1200px
margin
0
.header
text-align
color
margin-bottom
30px
.upload-card
background
border-radius
16px
padding
40px
box-shadow
0
10px
30px
rgba
0
0
0
0.2
margin-bottom
30px
.upload-area
border
3px
#667eea
border-radius
12px
padding
60px
20px
text-align
cursor
transition
0.3s
background
#f8f9ff
.upload-area
:hover
border-color
#764ba2
background
#f0f2ff
transform
translateY
2px
.images-container
display
background
border-radius
16px
padding
40px
box-shadow
0
10px
30px
rgba
0
0
0
0.2
.images-grid
display
grid-template-columns
repeat
minmax
250px
1
gap
20px
.image-card
border
2px
#e0e0e0
border-radius
12px
overflow
transition
0.3s
background
.image-wrapper
img
max-width
100%
max-height
200px
object-fit
.btn
background
linear-gradient
135deg
#667eea
0%
#764ba2
100%
color
border
padding
12px
30px
border-radius
8px
cursor
.modal
display
position
top
0
left
0
width
100%
height
100%
background
rgba
0
0
0
0.9
z-index
1000
align-items
justify-content
.modal
.active
display
.modal-image
max-width
90%
max-height
90vh
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>
</h1>
<p>
</p>
</div>
<div class="upload-card">
<div id="uploadArea" class="upload-area">
<div style="font-size: 48px;">
</div>
<div style="font-size: 18px; font-weight: 600;">
</div>
<div style="color: #666;">
</div>
<input type="file" accept=".pdf,application/pdf" id="fileInput" style="display: none;">
</div>
<div id="progressContainer" style="display: none; margin-top: 20px;">
<div style="width: 100%; height: 8px; background: #e0e0e0; border-radius: 4px; overflow: hidden;">
<div id="progressFill" style="height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); width: 0%; transition: width 0.3s ease;">
</div>
</div>
<div id="progressText" style="text-align: center; margin-top: 10px; color: #666; font-size: 14px;">
</div>
</div>
</div>
<div id="imagesContainer" class="images-container">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #f0f0f0;">
<h2 style="font-size: 24px;">
</h2>
<span id="imagesCount" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 8px 20px; border-radius: 20px; font-size: 14px;">
</span>
</div>
<div id="imagesGrid" class="images-grid">
</div>
</div>
</div>
<div id="modal" class="modal">
<div style="position: relative;">
<button onclick="closeModal()" style="position: absolute; top: -40px; right: 0; background: white; border: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 20px;">
</button>
<img id="modalImage" class="modal-image" alt="预览">
</div>
</div>
<script>
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);
});
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';
}
}
async function extractImagesFromPDF(arrayBuffer, fileName) {
const pdfDocument = await pdfjsLib.getDocument({ data: arrayBuffer, useSystemFonts: true, verbosity: 0 }).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) => {
if (!img) { resolve(); return; }
try {
const canvas = document.createElement('canvas');
canvas.width = img.width || 0;
canvas.height = img.height || 0;
const ctx = canvas.getContext('2d');
if (img.bitmap instanceof ImageBitmap) {
ctx.drawImage(img.bitmap, 0, 0);
} else if (img.data) {
const imageData = ctx.createImageData(img.width, img.height);
imageData.data.set(img.data);
ctx.putImageData(imageData, 0, 0);
} else if (img.src) {
const image = new Image();
image.onload = () => ctx.drawImage(image, 0, 0);
image.onerror = () => {};
image.src = img.src;
} else {
ctx.drawImage(img, 0, 0);
}
canvas.toBlob((blob) => {
if (blob) {
const url = URL.createObjectURL(blob);
const name = `${fileName.replace('.pdf', '')}_page${pageNum}_img${imageIndex}.png`;
extractedImages.push({ url, name, size: blob.size, width: canvas.width, height: canvas.height, blob });
}
resolve();
}, 'image/png');
} catch (err) { resolve(); }
});
});
imageIndex++;
}
}
}
}
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 style="text-align:center;padding:60px;color:#999;">未在 PDF 中找到图片</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.innerHTML = `
<div class="image-wrapper"><img src="${image.url}" alt="${image.name}"></div>
<div style="padding:15px;background:white;">
<div title="${image.name}" style="font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${image.name}</div>
<div style="display:flex;justify-content:space-between;font-size:12px;color:#999;margin-bottom:12px;">
<span>${image.width} × ${image.height}</span>
<span>${formatBytes(image.size)}</span>
</div>
<div style="display:flex;gap:8px;">
<button onclick="previewImage('${image.url}')" style="flex:1;padding:8px;border:none;border-radius:6px;background:#f0f0f0;cursor:pointer;">预览</button>
<button onclick="downloadImage(${index})" style="flex:1;padding:8px;border:none;border-radius:6px;background:#667eea;color:white;cursor:pointer;">下载</button>
</div>
</div>`;
imagesGrid.appendChild(card);
});
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024, 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>
核心逻辑说明
1. Worker 配置
pdf.js 需要独立的 Worker 线程来处理解析任务,避免阻塞主线程。务必正确设置 GlobalWorkerOptions.workerSrc,指向与主库版本一致的 worker 文件。
2. 图像对象识别
通过遍历页面的 operatorList,查找 paintImageXObject 和 paintInlineImageXObject 操作符。这些操作符对应 PDF 中的图像内容。获取对应的图像对象后,需兼容多种数据格式(如 bitmap、data 数组或 src 链接)。
3. 渲染与转换
使用 Canvas API 将图像数据绘制到画布上,再通过 toBlob 方法转换为 PNG 格式的 Blob 对象。这样既保留了图片质量,又方便前端直接生成下载链接。
注意事项
- 性能优化:如果 PDF 页数较多或图片分辨率极高,建议在循环中加入防抖或分片处理,防止页面卡顿。
- 兼容性:部分老旧 PDF 可能包含非标准图像编码,代码中已做基础容错处理,但极端情况下可能需要额外适配。
- 内存管理:生成的 Blob URL 在页面关闭后会自动释放,若长时间运行建议手动调用
URL.revokeObjectURL 清理引用。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online