vue3预览pdf可旋转并拖拽盖章任意位置生成新文件

对pdf旋转、拖拽公章或其他图片到pdf任意位置,生成一个新的pdf进行下载

网上搜了一下代码量都挺大的,这里自己来实现一个吧!

涉及到的技术栈:pdfjs-dist、pdf-lib、vueuse

<script setup> import { ref, onMounted, computed } from 'vue';
import * as pdfjsLib from 'pdfjs-dist/build/pdf';
import { useDraggable } from '@vueuse/core';
import { PDFDocument,degrees  } from 'pdf-lib';
import img2 from '@/assets/222.jpg'; // 这里的文件从node_modules/pdfjs-dist/build/目录下复制到public目录下 pdfjsLib.GlobalWorkerOptions.workerSrc = `./pdf.worker.mjs`;  const pdfCanvases = ref([]);
const pageCount = ref(0);
const pdfCanvasContainer = ref(null);
const rotation = ref(0); // 添加旋转角度变量  const imgRef = ref(null);
const pdfWidth = ref(0);
const pdfHeight = ref(0);
const pdfTotalHeight = ref(0);

const myImage = ref(null);
const imageWidth = ref(0);
const imageHeight = ref(0); // 存储初始位置 const initialX = ref(0);
const initialY = ref(0); // 最后停留的位置 const finalX = ref(0);
const finalY = ref(0);

const getImageSize = () => { if (myImage.value) {
    imageWidth.value = myImage.value.naturalWidth;
    imageHeight.value = myImage.value.naturalHeight;
  }
};

const { x, y, style } = useDraggable(imgRef); // 计算考虑滚动偏移后的样式 const draggableStyle = computed(() => {
  let newX = x.value + window.scrollX;
  let newY = y.value + window.scrollY; // 边界检查,直接限制在 PDF 边界内  newX = Math.max(0, Math.min(newX, pdfWidth.value));
  newY = Math.max(0, Math.min(newY, pdfTotalHeight.value)); if (newX + imageWidth.value >= pdfWidth.value) {
    newX = pdfWidth.value - imageWidth.value;
  } if (newY + imageHeight.value >= pdfTotalHeight.value) {
    newY = pdfTotalHeight.value - imageHeight.value;
  }
  finalX.value = newX;
  finalY.value = newY; return {
    transform: `translate(${newX}px, ${newY}px)`,
  };
});

onMounted(() => {
  loadPdf('download.pdf'); // 获取初始位置  initialX.value = x.value;
  initialY.value = y.value;
});

const loadPdf = async (url) => { try { // 清除旧的 canvas if (pdfCanvasContainer.value) { while (pdfCanvasContainer.value.firstChild) {
        pdfCanvasContainer.value.removeChild(pdfCanvasContainer.value.firstChild);
      }
    }


    const loadingTask = pdfjsLib.getDocument(url);
    const pdf = await loadingTask.promise;
    pageCount.value = pdf.numPages; // 清空画布数组  pdfCanvases.value = []; for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++) {
      const page = await pdf.getPage(pageNumber);
      const scale = 1;
      let viewport = page.getViewport({ scale }); // 应用旋转  viewport = page.getViewport({ scale, rotation: rotation.value });

      const canvas = document.createElement('canvas'); // 直接创建 canvas 元素  canvas.height = viewport.height;
      canvas.width = viewport.width;
      pdfCanvases.value.push(canvas); // 将创建的 canvas 添加到数组中  const context = canvas.getContext('2d');

      const renderContext = {
        canvasContext: context,
        viewport,
      };
      await page.render(renderContext).promise; // 获取第一页的尺寸作为 PDF 预览区域的尺寸 if (pageNumber === 1) {
        pdfWidth.value = viewport.width;
        pdfHeight.value = viewport.height;
        pdfTotalHeight.value = viewport.height * pdf.numPages;
      }
    } // 将动态创建的 canvas 添加到模板中 if (pdfCanvasContainer.value) {
      pdfCanvases.value.forEach((canvas) => {
        pdfCanvasContainer.value.appendChild(canvas);
      });
    }
  } catch (error) {
    console.error('Error loading PDF:', error);
  }
}; // 恢复初始位置 const resetPosition = () => {
  x.value = initialX.value;
  y.value = initialY.value;
};

const generatePdf = async () => { try {
    const pdfBytes = await fetch('download.pdf').then((res) => res.arrayBuffer());
    const pdfDoc = await PDFDocument.load(pdfBytes);
    const imageBytes = await fetch(img2).then((res) => res.arrayBuffer()); // 检测 MIME 类型  const mimeType = await detectMimeType(imageBytes);
    let image; if (mimeType === 'image/jpeg') {
      image = await pdfDoc.embedJpg(imageBytes);
    } else if (mimeType === 'image/png') {
      image = await pdfDoc.embedPng(imageBytes);
    } else {
      console.error('Unsupported image format.'); return;
    }

    const pages = pdfDoc.getPages();
    const firstPage = pages[Math.floor(finalY.value / pdfHeight.value)]; // 将图片添加到第一页,你可以根据需要修改  pages.forEach(page=>{
      page.setRotation(degrees(rotation.value));
    }) // 计算图片相对于当前页面的 y 坐标  const pageY = finalY.value % pdfHeight.value;

    firstPage.drawImage(image, {
      x: rotation.value === 90?(firstPage.getHeight() - pageY - imageHeight.value):finalX.value,
      y: rotation.value === 90?finalX.value:firstPage.getHeight() - pageY - imageHeight.value, // 注意:pdf-lib 的坐标系原点在左下角  width: imageWidth.value,
      height: imageHeight.value,
    });

    const modifiedPdfBytes = await pdfDoc.save();
    const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
    const link = document.createElement('a');
    link.href = window.URL.createObjectURL(blob);
    link.download = 'merged.pdf';
    link.click();
  } catch (error) {
    console.error('Error generating PDF:', error);
  }
}; // 检测 MIME 类型的辅助函数 const detectMimeType = (arrayBuffer) => { return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const uint8Array = new Uint8Array(reader.result);
      let mimeType = ''; if (uint8Array[0] === 0xff && uint8Array[1] === 0xd8 && uint8Array[2] === 0xff) {
        mimeType = 'image/jpeg';
      } else if (
        uint8Array[0] === 0x89 && uint8Array[1] === 0x50 && uint8Array[2] === 0x4e && uint8Array[3] === 0x47 ) {
        mimeType = 'image/png';
      } else {
        mimeType = 'unknown';
      }

      resolve(mimeType);
    };
    reader.onerror = reject; // 将 ArrayBuffer 转换为 Blob  const blob = new Blob([arrayBuffer.slice(0, 4)]);
    reader.readAsArrayBuffer(blob);
  });
};

const rota = ()=>{
  loadPdf('download.pdf');
} </script> <template> <div> <div style="position: absolute;left: 50px"> <div> {{ style }} </div> <div> <button @click="resetPosition">恢复初始位置</button> <button @click="generatePdf">生成 PDF</button> </div> <div> <label>旋转角度:</label> <input type="number" v-model.number="rotation" step="90" /> <button @click="rota">转换</button> </div> </div> <div style="position: absolute" ref="imgRef" :style="draggableStyle"> <img ref="myImage" src="@/assets/222.jpg" alt="静态图片" @load="getImageSize" /> </div> <div ref="pdfCanvasContainer"></div> </div> </template> <style scoped></style>