pdf.js-canvas出现文字显示缺失问题处理

文章描述了一个关于业务系统在Chrome浏览器中打开PDF文件时出现文字缺失的难题。作者排查了操作系统、字体和pdfjscMapUrl路径等因素,问题仍未解决。最终,发现禁用硬件加速后问题得到解决,暗示可能与Dell电脑的Intel网卡驱动有关,可能是驱动未优化导致的浏览器渲染异常。系统使用了vue.js、pdf.js、pdf-lib和fontkit等技术栈。

最近在修复一个业务系统困扰了很久的问题,现象是在某些用户电脑上使用业务系统浏览pdf文件时(Chrome浏览器),出现文字缺失的现象。如下图所示:

这个文字缺失的现象会随机出现在故障机器的不同位置,先前以为是操作系统原因、字体原因、pdfjs cMapUrl路径原因,反复测试,问题还是一样。关键这个问题在我自己的电脑不能重现,不能重现就意味着不能快速修复。

最后,突然想起昨天同事安装一台Dell电脑的网卡(Intel芯片)出现的怪现象,打电话到Intel,厂家给的驱动竟然无法安装,那边说可能是dell改了驱动,做了限制。难道这个是显卡驱动导致的问题?

继续查,baidu,google,stackoverflow,又到pdf.js官方翻issue,无果!!!

如果是显卡驱动问题,那么浏览器有什么特殊的配置刚好用到它?最后定位到 硬件加速,禁用硬件加速,问题解决,打开问题出现。这难道是dell厂商改驱动没有做好优化吗?

系统使用了以下开发环境包:

vue.js

pdf.js

pdf-lib

fontkit

and so on...

<%@ page language=“java” import=“java.util.*” pageEncoding=“gbk”%> <%@ taglib uri=“http://java.sun.com/jsp/jstl/functions” prefix=“fn” %> <%@ taglib uri=“http://java.sun.com/jsp/jstl/core” prefix=“c”%> <% String path = request.getContextPath(); String basePath = request.getScheme()+“😕/”+request.getServerName()+“:”+request.getServerPort()+path+“/”; %> <!DOCTYPE html> <html> <head> <meta charset="gbk"> <meta name="renderer" content="webkit"> <title>PDF在线盖章系统</title> <meta http-equiv="X-UA-Compatible" content="IE=edge,Chrome=1" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <link rel="stylesheet" href="layui/css/layui.css" media="all"> <link rel="stylesheet" type="text/css" href="layuis/css/common_main.css"/> <script type="text/javascript" src="jquery/jquery-1.7.2.min.js"></script> <script src="layui/layui.all.js" charset="gbk"></script> <script type="text/javascript" src="js/pdf/pdf.min.js" charset="gbk"></script> <script type="text/javascript" src="js/pdf/jspdf.umd.min.js" charset="gbk"></script> <script type="text/javascript" src="js/pdf/pdf.worker.min.js" charset="gbk"></script> <style type="text/css"> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1200px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1); overflow: hidden; } header { background: linear-gradient(135deg, #2c3e50, #4a6491); color: white; padding: 20px; text-align: center; } h1 { font-size: 28px; margin-bottom: 10px; } .description { font-size: 16px; opacity: 0.9; max-width: 600px; margin: 0 auto; } .main-content { display: flex; padding: 20px; gap: 20px; } .left-panel { flex: 0 0 250px; display: flex; flex-direction: column; gap: 20px; } .control-panel { background: #f8f9fa; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); } .button { padding: 12px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; transition: all 0.3s; width: 100%; margin-bottom: 10px; } .secondary-btn { background: #4a6491; color: white; } .secondary-btn:hover { background: #3a547e; } .secondary-btn2 { background: #27ae60; color: white; } .secondary-btn2:hover { background: #219653; } .secondary-btn3 { background: #e74c3c; color: white; } .secondary-btn3:hover { background: #c0392b; } .stamp-panel { background: #f8f9fa; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); } .stamp-panel h3 { margin-bottom: 15px; color: #2c3e50; border-bottom: 1px solid #ddd; padding-bottom: 10px; } .stamp-item { padding: 12px; background: white; border: 2px dashed #4a6491; border-radius: 6px; text-align: center; cursor: grab; margin-bottom: 10px; transition: all 0.3s; } .stamp-item:hover { background: #e8f0fe; transform: translateY(-2px); } .pdf-viewer { flex: 1; background: #f8f9fa; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); } .viewer-header { padding: 15px; background: #4a6491; color: white; display: flex; justify-content: space-between; align-items: center; } .canvas-container { position: relative; overflow: auto; height: 600px; background: #525659; display: flex; justify-content: center; padding: 20px; } #pdfCanvas { box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); } .loading { text-align: center; padding: 40px; color: #777; font-size: 18px; } .page-controls { display: flex; justify-content: center; padding: 15px; background: #e8e8e8; gap: 15px; } .page-btn { padding: 8px 16px; background: #4a6491; color: white; border: none; border-radius: 4px; cursor: pointer; } .page-info { padding: 8px 16px; background: white; border-radius: 4px; min-width: 100px; text-align: center; } .stamp-indicator { position: absolute; width: 100px; height: 100px; border: 2px dashed #ff0000; border-radius: 50%; background: rgba(255, 0, 0, 0.1); display: flex; align-items: center; justify-content: center; font-weight: bold; color: #ff0000; pointer-events: none; display: none; } footer { text-align: center; padding: 20px; color: #777; font-size: 14px; border-top: 1px solid #eee; } @media (max-width: 768px) { .main-content { flex-direction: column; } .left-panel { flex: 1; width: 100%; } .canvas-container { height: 400px; } } /* 印章样式 */ .signature-stamp { color: #1a73e8; border: 2px solid #1a73e8; } .approval-stamp { color: #0b8043; border: 2px solid #0b8043; } .review-stamp { color: #e67c73; border: 2px solid #e67c73; } .date-stamp { color: #8e24aa; border: 2px solid #8e24aa; } .stamp-on-canvas { position: absolute; width: 100px; height: 100px; border: 2px solid; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; cursor: pointer; z-index: 10; } .zoom-controls { display: flex; justify-content: center; padding: 10px; background: #e8e8e8; gap: 10px; } .zoom-btn { padding: 6px 12px; background: #4a6491; color: white; border: none; border-radius: 4px; cursor: pointer; } </style> </head> <body> <div class="container"> <div class="main-content"> <div class="left-panel"> <div class="control-panel"> <h3>文档操作</h3> <button id="save-btn" class="button secondary-btn">保存签章</button> <button id="export-btn" class="button secondary-btn2">导出文档</button> <button id="reset-btn" class="button secondary-btn3">重置签章</button> </div> <div class="stamp-panel"> <h3>选择印章</h3> <div class="stamp-item signature-stamp" data-type="signature" data-text="张三"> <div>个人签名</div> </div> <div class="stamp-item approval-stamp" data-type="approval" data-text="已批准"> <div>批准印章</div> </div> <div class="stamp-item review-stamp" data-type="review" data-text="已审核"> <div>审核印章</div> </div> <div class="stamp-item date-stamp" data-type="date" data-text="2023-11-08"> <div>日期印章</div> </div> </div> </div> <div class="pdf-viewer"> <div class="viewer-header"> <h3>文档预览</h3> <div id="doc-info">加载中...</div> </div> <div class="canvas-container"> <div class="loading" id="loading-message">正在加载PDF文档...</div> <canvas id="pdfCanvas"></canvas> <div id="stamp-preview" class="stamp-indicator">印章预览</div> </div> <div class="page-controls"> <button id="prev-page" class="page-btn">上一页</button> <div class="page-info">页码: <span id="page-num">0</span> / <span id="page-count">0</span></div> <button id="next-page" class="page-btn">下一页</button> </div> <div class="zoom-controls"> <button id="zoom-out" class="zoom-btn">缩小</button> <button id="zoom-reset" class="zoom-btn">重置缩放</button> <button id="zoom-in" class="zoom-btn">放大</button> </div> </div> </div> </div> <script> // 设置PDF.js worker pdfjsLib.GlobalWorkerOptions.workerSrc = 'js/pdf/pdf.worker.min.js'; // 局变量 let pdfDoc = null; let pageNum = 1; let pageRendering = false; let pageNumPending = null; let pdfScale = 1.2; let canvas = document.getElementById('pdfCanvas'); let ctx = canvas.getContext('2d'); let currentStamp = null; let stamps = []; // 预设PDF URL - 可以根据需要修改 const DEFAULT_PDF_URL = decodeURI('${pdfurl}'); // 元素引用 const loadingMessage = document.getElementById('loading-message'); const pageNumElement = document.getElementById('page-num'); const pageCountElement = document.getElementById('page-count'); const docInfoElement = document.getElementById('doc-info'); const prevPageButton = document.getElementById('prev-page'); const nextPageButton = document.getElementById('next-page'); const saveButton = document.getElementById('save-btn'); const exportButton = document.getElementById('export-btn'); const resetButton = document.getElementById('reset-btn'); const stampItems = document.querySelectorAll('.stamp-item'); const stampPreview = document.getElementById('stamp-preview'); const zoomInBtn = document.getElementById('zoom-in'); const zoomOutBtn = document.getElementById('zoom-out'); const zoomResetBtn = document.getElementById('zoom-reset'); // 从URL加载PDF function loadPdfFromUrl(url) { loadingMessage.style.display = 'block'; loadingMessage.textContent = '正在加载PDF文档...'; canvas.style.display = 'none'; pdfjsLib.getDocument(url).promise.then(function(pdf) { pdfDoc = pdf; pageCountElement.textContent = pdf.numPages; docInfoElement.textContent = `已加载 - ${pdf.numPages} 页`; loadingMessage.style.display = 'none'; canvas.style.display = 'block'; // 渲染第一页 pageNum = 1; renderPage(pageNum); }).catch(function(error) { loadingMessage.textContent = '加载PDF失败: ' + error.message; console.error('PDF加载错误:', error); // 尝试备用PDF URL setTimeout(() => { loadingMessage.textContent = '尝试备用PDF文档...'; loadPdfFromUrl(DEFAULT_PDF_URL); }, 2000); }); } // 渲染PDF页面 function renderPage(num) { pageRendering = true; pdfDoc.getPage(num).then(function(page) { const viewport = page.getViewport({ scale: pdfScale }); canvas.height = viewport.height; canvas.width = viewport.width; const renderContext = { canvasContext: ctx, viewport: viewport }; const renderTask = page.render(renderContext); renderTask.promise.then(function() { pageRendering = false; pageNumElement.textContent = num; // 如果存在待处理的页面渲染,立即渲染 if (pageNumPending !== null) { renderPage(pageNumPending); pageNumPending = null; } // 绘制当前页的印章 drawStampsForPage(num); }); }); } // 排队渲染页面 function queueRenderPage(num) { if (pageRendering) { pageNumPending = num; } else { renderPage(num); } } // 上一页 function onPrevPage() { if (pageNum <= 1) return; pageNum--; queueRenderPage(pageNum); } // 下一页 function onNextPage() { if (pageNum >= pdfDoc.numPages) return; pageNum++; queueRenderPage(pageNum); } // 绘制当前页的印章 function drawStampsForPage(pageNumber) { // 清除画布并重新渲染PDF页面 ctx.clearRect(0, 0, canvas.width, canvas.height); // 重新渲染PDF页面 pdfDoc.getPage(pageNumber).then(function(page) { const viewport = page.getViewport({ scale: pdfScale }); const renderContext = { canvasContext: ctx, viewport: viewport }; page.render(renderContext).promise.then(function() { // 绘制印章 stamps.filter(stamp => stamp.page === pageNumber).forEach(stamp => { ctx.font = 'bold 20px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; if (stamp.type === 'signature') { ctx.fillStyle = '#1a73e8'; ctx.strokeStyle = '#1a73e8'; } else if (stamp.type === 'approval') { ctx.fillStyle = '#0b8043'; ctx.strokeStyle = '#0b8043'; } else if (stamp.type === 'review') { ctx.fillStyle = '#e67c73'; ctx.strokeStyle = '#e67c73'; } else if (stamp.type === 'date') { ctx.fillStyle = '#8e24aa'; ctx.strokeStyle = '#8e24aa'; } // 绘制圆形印章 ctx.beginPath(); ctx.arc(stamp.x, stamp.y, 50, 0, Math.PI * 2); ctx.stroke(); // 绘制印章文字 ctx.fillText(stamp.text, stamp.x, stamp.y); // 如果是批准或审核印章,添加额外文字 if (stamp.type === 'approval' || stamp.type === 'review') { ctx.font = 'bold 14px Arial'; ctx.fillText('印章', stamp.x, stamp.y + 20); } }); }); }); } // 缩放功能 function zoomIn() { pdfScale += 0.2; queueRenderPage(pageNum); } function zoomOut() { if (pdfScale > 0.5) { pdfScale -= 0.2; queueRenderPage(pageNum); } } function zoomReset() { pdfScale = 1.2; queueRenderPage(pageNum); } // 初始化事件监听 function initEvents() { // 上一页/下一页按钮 prevPageButton.addEventListener('click', onPrevPage); nextPageButton.addEventListener('click', onNextPage); // 保存、导出、重置按钮 saveButton.addEventListener('click', function() { if (stamps.length === 0) { alert('请先添加印章'); return; } alert('保存成功!共添加了 ' + stamps.length + ' 个印章'); }); exportButton.addEventListener('click', function() { if (stamps.length === 0) { alert('请先添加印章'); return; } alert('导出功能已触发(实际应用中会生成带印章的PDF)'); }); resetButton.addEventListener('click', function() { if (stamps.length === 0) { alert('当前没有印章可重置'); return; } if (confirm('确定要重置所有印章吗?')) { stamps = []; renderPage(pageNum); alert('已重置所有印章'); } }); // 缩放按钮 zoomInBtn.addEventListener('click', zoomIn); zoomOutBtn.addEventListener('click', zoomOut); zoomResetBtn.addEventListener('click', zoomReset); // 印章拖放功能 stampItems.forEach(item => { item.addEventListener('dragstart', function(e) { currentStamp = { type: this.dataset.type, text: this.dataset.text || '' }; // 设置拖放预览图像 stampPreview.textContent = this.dataset.text; stampPreview.style.display = 'block'; // 根据印章类型设置样式 if (currentStamp.type === 'signature') { stampPreview.style.borderColor = '#1a73e8'; stampPreview.style.color = '#1a73e8'; stampPreview.style.background = 'rgba(26, 115, 232, 0.1)'; } else if (currentStamp.type === 'approval') { stampPreview.style.borderColor = '#0b8043'; stampPreview.style.color = '#0b8043'; stampPreview.style.background = 'rgba(11, 128, 67, 0.1)'; } else if (currentStamp.type === 'review') { stampPreview.style.borderColor = '#e67c73'; stampPreview.style.color = '#e67c73'; stampPreview.style.background = 'rgba(230, 124, 115, 0.1)'; } else if (currentStamp.type === 'date') { stampPreview.style.borderColor = '#8e24aa'; stampPreview.style.color = '#8e24aa'; stampPreview.style.background = 'rgba(142, 36, 170, 0.1)'; } e.dataTransfer.setDragImage(stampPreview, 50, 50); }); }); // 画布拖放事件 canvas.addEventListener('dragover', function(e) { e.preventDefault(); if (currentStamp) { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; stampPreview.style.display = 'block'; stampPreview.style.left = (x - 50) + 'px'; stampPreview.style.top = (y - 50) + 'px'; } }); canvas.addEventListener('dragleave', function() { stampPreview.style.display = 'none'; }); canvas.addEventListener('drop', function(e) { e.preventDefault(); stampPreview.style.display = 'none'; if (currentStamp) { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const stamp = { type: currentStamp.type, text: currentStamp.text, x: x, y: y, page: pageNum }; stamps.push(stamp); drawStampsForPage(pageNum); } }); } // 初始化 window.onload = function() { initEvents(); // 页面加载后自动加载默认PDF loadPdfFromUrl(DEFAULT_PDF_URL); }; </script> </body> </html>这个代码加载到的pdf乱码,页码正常
最新发布
09-09
<template> <Teleport to="body"> <div v-if="visible" class="pdf-mask" @mousedown="startDrag"> <div ref="modalRef" class="pdf-modal" :style="modalStyle"> <div class="pdf-header"> <span class="title">报关资料</span> <div class="tools"> <el-button size="small" @click="zoomIn">放大</el-button> <el-button size="small" @click="zoomOut">缩小</el-button> <el-button size="small" @click="close">关闭</el-button> </div> </div> <!-- 滚动容器 --> <el-tabs v-model="activeTab" class="pdf-tabs"> <el-tab-pane v-for="tab in tabs" :key="tab.id" :label="tab.name" :name="tab.id"> <!-- 单个 PDF 容器 --> <div class="pdf-body" ref="scrollRef" @wheel="onWheel" @mousedown="onPdfMouseDown"> <div :ref="(el) => setPdfContainer(tab.id, el)" /> </div> </el-tab-pane> </el-tabs> </div> </div> </Teleport> </template> <script setup lang="ts"> import { ref, reactive, watch, nextTick } from 'vue' import * as pdfjsLib from 'pdfjs-dist' import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url' pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker const emit = defineEmits<{ (e: 'update:visible', v: boolean): void (e: 'tabschange', id: string | number): void // 新增 }>() const pdfRealSize = ref({ width: 0, height: 0 }) /* tabs 数据 */ const tabs = ref<Array<{ id: string | number, name: string, url: string }>>([]) const activeTab = ref<string | number>('') async function openWithTabs( list: Array<{ code: string; name: string; url: string }> ) { tabs.value = list .filter((i) => i.name !== 'all_files') .map((i) => ({ id: i.code, name: i.name, url: i.url })) const firstId = tabs.value[0]?.id activeTab.value = firstId ?? '' // 手动让弹窗显示 emit('update:visible', true) // 等 DOM 更新完,立即渲染第一页 await nextTick() if (firstId) { const container = pdfContainerMap.get(firstId) const tab = tabs.value.find((t) => t.id === firstId) if (container && tab) { await renderSinglePDF(tab.url, container) } } // 弹框居中 const cw = window.innerWidth, ch = window.innerHeight modalStyle.left = (cw - 800) / 2 + 'px' modalStyle.top = (ch - 600) / 2 + 'px' } const pdfContainerMap = new Map<string | number, HTMLDivElement>() function setPdfContainer(id: string | number, el: any) { if (el) pdfContainerMap.set(id, el as HTMLDivElement) } watch(activeTab, (id) => { const tab = tabs.value.find(t => t.id === id) if (!tab) return renderSinglePDF(tab.url, pdfContainerMap.get(id)!) }) /* ---------------- props & emits ---------------- */ const props = defineProps<{ visible: boolean url?: string highlight?: { x0: number; y0: number; x1: number; y1: number } | null }>() /* ---------------- 弹窗位置 ---------------- */ const modalRef = ref<HTMLDivElement>() const pdfContainer = ref<HTMLDivElement>() const scrollRef = ref<HTMLDivElement>() // 新增:滚动容器 const modalStyle = reactive({ left: '0px', top: '0px', width: '800px', height: '600px', cursor: 'default' }) let dragStart: { x: number; y: number } | null = null /* ---------------- 拖拽 ---------------- */ function startDrag(e: MouseEvent) { const target = e.target as HTMLElement if (!target.classList.contains('pdf-mask') && !target.closest('.pdf-header')) return if (!modalRef.value) return dragStart = { x: e.clientX, y: e.clientY } modalStyle.cursor = 'grabbing' document.addEventListener('mousemove', onDrag) document.addEventListener('mouseup', stopDrag) e.preventDefault() } function onDrag(e: MouseEvent) { if (!dragStart) return const dx = e.clientX - dragStart.x const dy = e.clientY - dragStart.y modalStyle.left = parseFloat(modalStyle.left) + dx + 'px' modalStyle.top = parseFloat(modalStyle.top) + dy + 'px' dragStart = { x: e.clientX, y: e.clientY } } function stopDrag() { dragStart = null modalStyle.cursor = 'default' document.removeEventListener('mousemove', onDrag) document.removeEventListener('mouseup', stopDrag) if (modalRef.value) { modalStyle.width = modalRef.value.offsetWidth + 'px' modalStyle.height = modalRef.value.offsetHeight + 'px' } } /* ---------------- 缩放 ---------------- */ let scale = 1 // 1️⃣ 换掉局 scale → 每个 tab 独立 const tabScale = new Map<string | number, number>() const getScale = (id: string | number) => tabScale.get(id) ?? 1 const setScale = (id: string | number, v: number) => tabScale.set(id, Math.min(Math.max(v, 0.5), 3)) // 2️⃣ 放大 / 缩小 const zoomIn = () => { setScale(activeTab.value, getScale(activeTab.value) + 0.2); rerenderActiveTab() } const zoomOut = () => { setScale(activeTab.value, getScale(activeTab.value) - 0.2); rerenderActiveTab() } // 3️⃣ 重新渲染当前 tab async function rerenderActiveTab() { const id = activeTab.value const tab = tabs.value.find(t => t.id === id) const container = pdfContainerMap.get(id) if (tab && container) await renderSinglePDF(tab.url, container) } // 4️⃣ renderSinglePDF 改用当前 tab 的 scale let baseViewport: pdfjsLib.PageViewport | null = null const tabBaseViewport = new Map<string | number, pdfjsLib.PageViewport>() let abortController: AbortController | null = null async function renderSinglePDF(url: string, container: HTMLDivElement) { if (!container) return if (abortController) abortController.abort() abortController = new AbortController() container.innerHTML = '' const pdf = await pdfjsLib.getDocument(url).promise const page = await pdf.getPage(1) baseViewport = page.getViewport({ scale: 1 }) // ← 保存原始 viewport tabBaseViewport.set(activeTab.value, baseViewport) const scale = getScale(activeTab.value) * (800 / baseViewport.width) const vp = page.getViewport({ scale }) const canvas = document.createElement('canvas') canvas.width = vp.width canvas.height = vp.height const ctx = canvas.getContext('2d')! container.appendChild(canvas) try { await page.render({ canvasContext: ctx, viewport: vp, signal: abortController.signal } as any).promise } catch (err) { if (err !== 'AbortError') throw err } // await page.render({ // canvasContext: ctx, // viewport: vp, // canvas // }).promise; // canvas.dataset.scale = String(scale) // ← 保存当前缩放比例 // 计算当前缩放后的真实尺寸 // 新增:更新滚动容器尺寸 canvas.dataset.scale = String(getScale(activeTab.value)) pdfRealSize.value = { width: vp.width, height: vp.height } const scrollBox = container.parentElement as HTMLDivElement if (scrollBox) { scrollBox.style.width = vp.width + 'px' scrollBox.style.height = vp.height + 'px' } } /* ---------------- PDF 逻辑宽高 ---------------- */ const pdfLogicWidth = ref(0) const pdfLogicHeight = ref(0) /* ---------------- PDF 渲染 ---------------- */ async function renderPDF() { if (!pdfContainer.value || !props.url) return pdfContainer.value.innerHTML = '' try { const pdf = await pdfjsLib.getDocument(props.url).promise const page = await pdf.getPage(1) const baseViewport = page.getViewport({ scale: 1 }) pdfLogicWidth.value = baseViewport.width pdfLogicHeight.value = baseViewport.height const viewport = page.getViewport({ scale: (800 / pdfLogicWidth.value) * scale }) const canvas = document.createElement('canvas') canvas.width = viewport.width canvas.height = viewport.height const ctx = canvas.getContext('2d')! pdfContainer.value.appendChild(canvas) await page.render({ canvasContext: ctx, viewport, canvas }).promise if (props.highlight) drawHighlight(props.highlight) } catch (err) { console.error('Failed to load PDF:', err) } } /* ---------------- 高亮 + 自动滚动 ---------------- */ function drawHighlight(rect: { x0: number; y0: number; x1: number; y1: number }) { const id = activeTab.value const container = pdfContainerMap.get(id) const scrollContainer = container?.parentElement as HTMLDivElement if (!container || !scrollContainer) return // 清除旧高亮 container.querySelectorAll('.pdf-highlight').forEach(el => el.remove()) const canvas = container.querySelector('canvas') if (!canvas) return const canvasScale = parseFloat(canvas.dataset.scale || '1') const baseViewport = tabBaseViewport.get(id) if (!baseViewport) return // 将 PDF 原始坐标 → canvas 渲染坐标 const sx = canvas.width / baseViewport.width const sy = canvas.height / baseViewport.height const hlLeft = rect.x0 * sx const hlTop = rect.y0 * sy const hlWidth = (rect.x1 - rect.x0) * sx const hlHeight = (rect.y1 - rect.y0) * sy const div = document.createElement('div') div.className = 'pdf-highlight' Object.assign(div.style, { position: 'absolute', left: hlLeft + 'px', top: hlTop + 'px', width: hlWidth + 'px', height: hlHeight + 'px', background: 'rgba(0,255,0,0.4)', border: '2px dashed #ff4d4f', pointerEvents: 'none', zIndex: 9999 }) container.style.position = 'relative' container.appendChild(div) /* 自动滚动 */ nextTick(() => { const contW = scrollContainer.clientWidth const contH = scrollContainer.clientHeight scrollContainer.scrollTo({ left: Math.max(0, hlLeft + hlWidth / 2 - contW / 2), top: Math.max(0, hlTop + hlHeight / 2 - contH / 2), behavior: 'smooth' }) }) } /* ---------------- 拖拽 PDF 内容 ---------------- */ let pdfDragging = false let pdfDragStart = { x: 0, y: 0 } let scrollStart = { left: 0, top: 0 } function onPdfMouseDown(e: MouseEvent) { const container = scrollRef.value if (!container) return // 只有在放大时才允许拖动 const scale = getScale(activeTab.value) if (scale <= 1) return pdfDragging = true pdfDragStart = { x: e.clientX, y: e.clientY } scrollStart = { left: container.scrollLeft, top: container.scrollTop } document.addEventListener('mousemove', onPdfMouseMove) document.addEventListener('mouseup', onPdfMouseUp) e.preventDefault() } function onPdfMouseMove(e: MouseEvent) { if (!pdfDragging) return const dx = e.clientX - pdfDragStart.x const dy = e.clientY - pdfDragStart.y const container = scrollRef.value if (container) { container.scrollTo({ left: scrollStart.left - dx, top: scrollStart.top - dy }) } } function onPdfMouseUp() { pdfDragging = false document.removeEventListener('mousemove', onPdfMouseMove) document.removeEventListener('mouseup', onPdfMouseUp) } /* ---------------- 阻止滚轮冒泡 ---------------- */ function onWheel(e: WheelEvent) { e.stopPropagation() } /* ---------------- 监听 visible ---------------- */ watch( () => props.visible, async (v, ov) => { if (!v) { scale = 1; return } await nextTick() if (ov === false) { const cw = window.innerWidth const ch = window.innerHeight const mw = 800 const mh = 600 modalStyle.left = (cw - mw) / 2 + 'px' modalStyle.top = (ch - mh) / 2 + 'px' } renderPDF() }, { immediate: true } ) /* ---------------- 关闭 ---------------- */ function close() { pdfRealSize.value = { width: 0, height: 0 } emit('update:visible', false) } /* ---------------- 监听 highlight ---------------- */ watch( () => props.highlight, (newVal) => { if (newVal) drawHighlight(newVal) }, { immediate: false } ) watch(activeTab, (id) => { const tab = tabs.value.find(t => t.id === id) if (!tab) return emit('tabschange', id) renderSinglePDF(tab.url, pdfContainerMap.get(id)!) }) function drawHighlightByCoords(rect: { x0: number; y0: number; x1: number; y1: number }) { drawHighlight(rect) } onMounted(async () => { await nextTick(); const scrollContainer = scrollRef.value; if (scrollContainer) { scrollContainer.addEventListener('mousedown', onPdfMouseDown); } }); defineExpose({ openWithTabs, drawHighlight: drawHighlightByCoords }) </script> <style scoped> .pdf-mask { position: fixed; inset: 0; z-index: 9999; pointer-events: none; } .pdf-modal { position: absolute; background: #fff; border: 1px solid #ccc; border-radius: 4px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); display: flex; flex-direction: column; pointer-events: auto; min-width: 400px; min-height: 300px; resize: both; overflow: hidden; } .pdf-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #f5f5f5; cursor: grab; border-bottom: 1px solid #e4e4e4; user-select: none; } .pdf-header:active { cursor: grabbing; } /* .pdf-body { flex: 1; overflow: auto; } */ .pdf-body { flex: none; /* 拉伸 */ overflow: auto; /* 出现滚动条 */ /* 宽高由 JS 动态设置,这里再写死 */ } .pdf-body canvas { pointer-events: none; } .pdf-modal { height: 700px; } </style>帮我处理一下,当前打开的pdf显示两个相同的数据,并且我需要图片放大时就是特别大时,只会显示左上角的一小块界面,我需要可以用鼠标拖拽看到其他放大的区域界面,并面优化相关代码
08-27
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值