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>
posted @ 2025-07-01 17:07  扛着扫把闯天下  阅读(241)  评论(0)    收藏  举报