Angular5中基于ng2-pdf-viewer获取矩形坐标实现电子签名定位
我之前做过类似的PDF标注需求,用ng2-pdf-viewer实现矩形绘制并保存坐标其实核心是处理好渲染坐标和PDF原始坐标的映射,下面一步步给你拆解实现方案:
核心思路
我们需要在PDF视图上方叠加一个透明的绘图层(用Canvas最合适),监听鼠标事件完成矩形绘制,然后把画布上的坐标转换为PDF的原始坐标(不受缩放、页面尺寸影响)保存起来;下次打开PDF时,再把保存的原始坐标转换回当前渲染的画布坐标,重新绘制矩形。
具体实现步骤
1. 搭建HTML结构
给ng2-pdf-viewer套一个相对定位的容器,里面加一个绝对定位的Canvas作为绘图层,确保Canvas完全覆盖PDF视图:
<div class="pdf-container" style="position: relative; max-width: 100%; overflow: auto;"> <!-- ng2-pdf-viewer组件 --> <ng2-pdf-viewer [src]="pdfFilePath" [zoom]="currentZoom" (afterLoad)="onPdfLoaded($event)" (pageChange)="onCurrentPageChange($event)" style="display: block;" ></ng2-pdf-viewer> <!-- 绘图Canvas --> <canvas #drawingCanvas style="position: absolute; top: 0; left: 0; pointer-events: none; border: none; background: transparent;" ></canvas> </div> <!-- 控制按钮 --> <div style="margin: 1rem 0;"> <button (click)="toggleDrawingMode()" [disabled]="isDrawing"> {{ isDrawing ? '结束绘制' : '开始绘制矩形' }} </button> <button (click)="saveAnnotations()" style="margin-left: 1rem;">保存标注</button> </div>
2. 组件逻辑实现
在Angular组件中处理PDF加载、鼠标绘图、坐标转换、保存/恢复标注等逻辑:
import { Component, ViewChild, ElementRef, OnInit } from '@angular/core'; @Component({ selector: 'app-pdf-annotator', templateUrl: './pdf-annotator.component.html', styleUrls: ['./pdf-annotator.component.css'] }) export class PdfAnnotatorComponent implements OnInit { pdfFilePath = '/assets/sample.pdf'; // 替换成你的PDF路径 currentZoom = 1.0; currentPage = 1; isDrawing = false; @ViewChild('drawingCanvas') drawingCanvas!: ElementRef<HTMLCanvasElement>; private ctx: CanvasRenderingContext2D | null = null; private pdfDoc: any; // 存储加载后的PDF文档对象 private startPoint = { x: 0, y: 0 }; // 存储所有标注的原始PDF坐标:页面号 + 矩形坐标 private annotations: Array<{ page: number, x1: number, y1: number, x2: number, y2: number }> = []; ngOnInit() { // 初始化时加载已保存的标注 this.loadSavedAnnotations(); } ngAfterViewInit() { this.ctx = this.drawingCanvas.nativeElement.getContext('2d'); } // PDF加载完成后初始化画布尺寸 onPdfLoaded(pdf: any) { this.pdfDoc = pdf; this.resizeCanvas(); this.redrawCurrentPageAnnotations(); } // 页面切换时重新绘制当前页的标注 onCurrentPageChange(page: number) { this.currentPage = page; this.redrawCurrentPageAnnotations(); } // 切换绘图模式 toggleDrawingMode() { this.isDrawing = !this.isDrawing; const canvas = this.drawingCanvas.nativeElement; if (this.isDrawing) { // 开启画布的鼠标事件监听 canvas.style.pointerEvents = 'auto'; canvas.addEventListener('mousedown', this.handleMouseDown.bind(this)); canvas.addEventListener('mousemove', this.handleMouseMove.bind(this)); canvas.addEventListener('mouseup', this.handleMouseUp.bind(this)); } else { // 关闭鼠标事件 canvas.style.pointerEvents = 'none'; canvas.removeEventListener('mousedown', this.handleMouseDown.bind(this)); canvas.removeEventListener('mousemove', this.handleMouseMove.bind(this)); canvas.removeEventListener('mouseup', this.handleMouseUp.bind(this)); } } // 鼠标按下:记录起始点 private handleMouseDown(e: MouseEvent) { const canvasRect = this.drawingCanvas.nativeElement.getBoundingClientRect(); this.startPoint = { x: e.clientX - canvasRect.left, y: e.clientY - canvasRect.top }; } // 鼠标移动:实时绘制临时矩形 private handleMouseMove(e: MouseEvent) { if (!this.isDrawing) return; const canvasRect = this.drawingCanvas.nativeElement.getBoundingClientRect(); const endPoint = { x: e.clientX - canvasRect.left, y: e.clientY - canvasRect.top }; // 清除画布,先重绘已保存的标注,再画临时矩形 this.clearCanvas(); this.redrawCurrentPageAnnotations(); if (this.ctx) { this.ctx.strokeStyle = '#ff4444'; this.ctx.lineWidth = 2; this.ctx.strokeRect( this.startPoint.x, this.startPoint.y, endPoint.x - this.startPoint.x, endPoint.y - this.startPoint.y ); } } // 鼠标松开:保存矩形的原始PDF坐标 private handleMouseUp(e: MouseEvent) { if (!this.isDrawing) return; const canvasRect = this.drawingCanvas.nativeElement.getBoundingClientRect(); const endPoint = { x: e.clientX - canvasRect.left, y: e.clientY - canvasRect.top }; // 转换画布坐标为PDF原始坐标 const pdfCoords = this.convertCanvasToPdf( this.startPoint.x, this.startPoint.y, endPoint.x, endPoint.y ); // 保存标注 this.annotations.push({ page: this.currentPage, x1: pdfCoords.x1, y1: pdfCoords.y1, x2: pdfCoords.x2, y2: pdfCoords.y2 }); // 重绘所有标注,去掉临时矩形 this.clearCanvas(); this.redrawCurrentPageAnnotations(); } // 画布坐标 → PDF原始坐标(关键!) private convertCanvasToPdf(canvasX1: number, canvasY1: number, canvasX2: number, canvasY2: number) { const page = this.pdfDoc.getPage(this.currentPage); const pdfOriginalWidth = page.view[2]; // PDF页面原始宽度(比如A4是595) const pdfOriginalHeight = page.view[3]; // PDF页面原始高度(比如A4是842) const canvasWidth = this.drawingCanvas.nativeElement.width; // 计算缩放比例(假设PDF自适应容器宽度) const scale = canvasWidth / pdfOriginalWidth; // 注意PDF的Y轴是从下往上的,需要反转Y坐标 const x1 = canvasX1 / scale; const x2 = canvasX2 / scale; const y1 = pdfOriginalHeight - (canvasY1 / scale); const y2 = pdfOriginalHeight - (canvasY2 / scale); // 确保坐标是左上到右下的顺序 return { x1: Math.min(x1, x2), y1: Math.min(y1, y2), x2: Math.max(x1, x2), y2: Math.max(y1, y2) }; } // PDF原始坐标 → 画布坐标 private convertPdfToCanvas(pdfX1: number, pdfY1: number, pdfX2: number, pdfY2: number) { const page = this.pdfDoc.getPage(this.currentPage); const pdfOriginalWidth = page.view[2]; const pdfOriginalHeight = page.view[3]; const canvasWidth = this.drawingCanvas.nativeElement.width; const scale = canvasWidth / pdfOriginalWidth; // 反转Y坐标 const y1 = pdfOriginalHeight - pdfY1; const y2 = pdfOriginalHeight - pdfY2; return { x1: pdfX1 * scale, y1: y1 * scale, x2: pdfX2 * scale, y2: y2 * scale }; } // 调整画布尺寸和PDF容器一致 private resizeCanvas() { const container = document.querySelector('.pdf-container'); if (container && this.drawingCanvas.nativeElement) { this.drawingCanvas.nativeElement.width = container.clientWidth; this.drawingCanvas.nativeElement.height = container.clientHeight; } } // 清除画布 private clearCanvas() { if (!this.ctx) return; this.ctx.clearRect(0, 0, this.drawingCanvas.nativeElement.width, this.drawingCanvas.nativeElement.height); } // 重绘当前页的所有标注 private redrawCurrentPageAnnotations() { this.clearCanvas(); const currentPageAnnotations = this.annotations.filter(ann => ann.page === this.currentPage); currentPageAnnotations.forEach(ann => { const canvasCoords = this.convertPdfToCanvas(ann.x1, ann.y1, ann.x2, ann.y2); if (this.ctx) { this.ctx.strokeStyle = '#ff4444'; this.ctx.lineWidth = 2; this.ctx.strokeRect( canvasCoords.x1, canvasCoords.y1, canvasCoords.x2 - canvasCoords.x1, canvasCoords.y2 - canvasCoords.y1 ); } }); } // 保存标注到localStorage(实际项目可以存后端) saveAnnotations() { localStorage.setItem('pdfAnnotations', JSON.stringify(this.annotations)); alert('标注已保存!'); } // 加载已保存的标注 loadSavedAnnotations() { const saved = localStorage.getItem('pdfAnnotations'); if (saved) { this.annotations = JSON.parse(saved); } } }
关键细节处理
- 坐标映射的准确性:一定要注意PDF的Y轴方向和Canvas相反(PDF从下往上,Canvas从上往下),转换时必须反转Y坐标,否则标注位置会颠倒。
- 缩放适配:如果用户调整了zoom级别,需要重新计算坐标并重绘标注,可以监听ng2-pdf-viewer的
(zoomChange)事件,调用redrawCurrentPageAnnotations()。 - 页面切换:切换页面时只渲染当前页的标注,避免画布上出现其他页面的矩形。
- 画布尺寸调整:如果容器大小变化(比如窗口 resize),需要重新调整Canvas尺寸,确保完全覆盖PDF视图。
内容的提问来源于stack exchange,提问作者Usman Hussain
相关产品推荐
相关产品推荐

