You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
    }
  }
}
关键细节处理
  1. 坐标映射的准确性:一定要注意PDF的Y轴方向和Canvas相反(PDF从下往上,Canvas从上往下),转换时必须反转Y坐标,否则标注位置会颠倒。
  2. 缩放适配:如果用户调整了zoom级别,需要重新计算坐标并重绘标注,可以监听ng2-pdf-viewer的(zoomChange)事件,调用redrawCurrentPageAnnotations()。
  3. 页面切换:切换页面时只渲染当前页的标注,避免画布上出现其他页面的矩形。
  4. 画布尺寸调整:如果容器大小变化(比如窗口 resize),需要重新调整Canvas尺寸,确保完全覆盖PDF视图。

内容的提问来源于stack exchange,提问作者Usman Hussain

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:54:07