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

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>
相关推荐
雨季66617 分钟前
Flutter 三端应用实战:OpenHarmony 简易“动态内边距调节器”交互模式深度解析
javascript·flutter·ui·交互·dart
天人合一peng32 分钟前
Unity中button 和toggle监听事件函数有无参数
前端·unity·游戏引擎
会飞的战斗鸡1 小时前
JS中的链表(含leetcode例题)
javascript·leetcode·链表
方也_arkling1 小时前
别名路径联想提示。@/统一文件路径的配置
前端·javascript
毕设源码-朱学姐1 小时前
【开题答辩全过程】以 基于web教师继续教育系统的设计与实现为例,包含答辩的问题和答案
前端
qq_177767372 小时前
React Native鸿蒙跨平台剧集管理应用实现,包含主应用组件、剧集列表、分类筛选、搜索排序等功能模块
javascript·react native·react.js·交互·harmonyos
qq_177767372 小时前
React Native鸿蒙跨平台自定义复选框组件,通过样式数组实现选中/未选中状态的样式切换,使用链式调用替代样式数组,实现状态驱动的样式变化
javascript·react native·react.js·架构·ecmascript·harmonyos·媒体
web打印社区2 小时前
web-print-pdf:突破浏览器限制,实现专业级Web静默打印
前端·javascript·vue.js·electron·html
RFCEO2 小时前
前端编程 课程十三、:CSS核心基础1:CSS选择器
前端·css·css基础选择器详细教程·css类选择器使用方法·css类选择器命名规范·css后代选择器·精准选中嵌套元素
烬头88212 小时前
React Native鸿蒙跨平台采用了函数式组件的形式,通过 props 接收分类数据,使用 TouchableOpacity实现了点击交互效果
javascript·react native·react.js·ecmascript·交互·harmonyos