对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>