html
复制代码
<!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 class="container">
<div class="header">
<h1>📄 PDF 图片提取工具</h1>
<p>上传 PDF 文件,自动提取其中的所有图片</p>
</div>
<div class="upload-card">
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📁</div>
<div class="upload-text">点击或拖拽 PDF 文件到此处</div>
<div class="upload-hint">支持单个 PDF 文件上传</div>
</div>
<input type="file" id="fileInput" accept=".pdf,application/pdf">
<div class="progress-container" id="progressContainer">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text" id="progressText">处理中...</div>
</div>
</div>
<div class="images-container" id="imagesContainer">
<div class="images-header">
<div class="images-title">提取的图片</div>
<div class="images-count" id="imagesCount">0 张图片</div>
</div>
<div class="images-grid" id="imagesGrid"></div>
</div>
</div>
<div class="modal" id="modal">
<div class="modal-content">
<button class="modal-close" onclick="closeModal()">×</button>
<img class="modal-image" id="modalImage" src="" 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 class="empty-state" style="grid-column: 1 / -1;">
<div class="empty-icon">🖼️</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 class="image-wrapper">
<img src="${image.url}" alt="${image.name}">
</div>
<div class="image-info">
<div class="image-name" title="${image.name}">${image.name}</div>
<div class="image-meta">
<span>${image.width} × ${image.height}</span>
<span>${formatBytes(image.size)}</span>
</div>
<div class="image-actions">
<button class="btn-small btn-preview" onclick="previewImage('${image.url}')">预览</button>
<button class="btn-small btn-download" 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>