打印本页

在 React 中使用 JavaScript 为 PDF 文档添加图章

合同、审批单定稿之后常要盖一枚「已核准」的图章,或在页角留一行时间戳,标记这份文件由谁在什么时候确认过。桌面软件做这件事要一枚枚手动盖,文件一多就很吃力;把文档传到服务端处理,又等于让文件离开了本地。

Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端加载、修改与保存 PDF 文档,图章在本地生成,通过虚拟文件系统(VFS)读写,无需后端配合。它把图章当作一类注释处理:PdfRubberStampAnnotation 决定图章在页面上的位置与大小,外观则由 PdfAppearance 挂上的 PdfTemplate 决定——在模板上画图片就是图像章,画文字就是文字章。

本文介绍三个核心功能点:

有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。


添加图像图章

Spire.PDF for JavaScript 提供 PdfRubberStampAnnotation 类,用于在页面的指定位置放置一枚图章。它只框定图章的位置与大小,外观由 PdfAppearance 决定。

function App() {
  const addImageStamp = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将待处理的 PDF 与印章图片载入 VFS
    const inputFileName = '示例文档.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
    const stampImageName = '印章.png';
    await window.spire.FetchFileToVFS(stampImageName, "", `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载文档
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 获取第一页
    let page = doc.Pages.get_Item(0);

    // 载入印章图片
    let image = pdfModule.PdfImage.FromFile(stampImageName);

    // 把图片缩到 120 磅
    const maxSize = 120;
    const scale = Math.min(maxSize / image.Width, maxSize / image.Height, 1);
    const stampWidth = image.Width * scale;
    const stampHeight = image.Height * scale;

    // 创建与图章等大的模板,并把图片铺满模板
    let template = new pdfModule.PdfTemplate({ width: stampWidth, height: stampHeight });
    template.Graphics.DrawImage({ image: image, x: 0, y: 0, width: stampWidth, height: stampHeight });

    // 在页面右下角框出图章的位置,矩形与模板等大
    let rect = new pdfModule.RectangleF({
      location: new pdfModule.PointF(page.ActualSize.Width - stampWidth - 40, page.ActualSize.Height - stampHeight - 40),
      size: new pdfModule.SizeF({ width: stampWidth, height: stampHeight })
    });

    // 创建图章注释,并把模板设成它的外观
    let stamp = new pdfModule.PdfRubberStampAnnotation({ rectangle: rect });
    let appearance = new pdfModule.PdfAppearance(stamp);
    appearance.Normal = template;
    stamp.Appearance = appearance;

    // 将图章注释添加到页面的注释集合中
    page.AnnotationsWidget.Add(stamp);

    // 保存文档
    const outputFileName = '图像图章.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>添加图像图章</h1>
      <button onClick={addImageStamp}>
        开始添加
      </button>
    </div>
  );
}

export default App;

在页面右下角盖上图像图章后的 PDF 文档:

在页面右下角盖上图像图章后的 PDF 文档


添加文字图章

Spire.PDF for JavaScript 还提供 PdfPath、PdfPen 与 PdfTrueTypeFont,用于把文字画成图章。PdfPath 用四段圆弧拼出圆角矩形,DrawPath 把它描边成外框,再用 DrawString 在框内写上文字,得到的是一枚带边框的文字图章。

function App() {
  const addTextStamp = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将含中文字形的字体载入 VFS 的字体目录
    await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 将待处理的 PDF 载入 VFS
    const inputFileName = '示例文档.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载文档
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 获取第一页
    let page = doc.Pages.get_Item(0);

    // 创建承载图章的模板
    let template = new pdfModule.PdfTemplate({ width: 125, height: 55 });

    // 用 VFS 中的字体文件构造字体,并准备画笔
    let font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 12, style: pdfModule.PdfFontStyle.Bold });
    let brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_DarkRed() }) });
    let pen = new pdfModule.PdfPen({ brush: brush });

    // 用四段圆弧加一条直线拼出圆角矩形路径
    const radius = 20;
    let path = new pdfModule.PdfPath();
    path.AddArc({ x: 0, y: 0, width: radius, height: radius, startAngle: 180, sweepAngle: 90 });
    path.AddArc({ x: template.Width - radius, y: 0, width: radius, height: radius, startAngle: 270, sweepAngle: 90 });
    path.AddArc({ x: template.Width - radius, y: template.Height - radius, width: radius, height: radius, startAngle: 0, sweepAngle: 90 });
    path.AddArc({ x: 0, y: template.Height - radius, width: radius, height: radius, startAngle: 90, sweepAngle: 90 });
    path.AddLine({ x1: 0, y1: template.Height - radius, x2: 0, y2: radius / 2 });

    // 把路径描边成图章的外框
    template.Graphics.DrawPath({ pen: pen, path: path });

    // 在外框内写上两行文字
    template.Graphics.DrawString({ s: '已核准', font: font, brush: brush, point: new pdfModule.PointF(40, 10) });
    template.Graphics.DrawString({ s: '市场营销部', font: font, brush: brush, point: new pdfModule.PointF(30, 32) });

    // 在页面右下角框出图章的位置,矩形与模板等大
    let rect = new pdfModule.RectangleF({
      location: new pdfModule.PointF(page.ActualSize.Width - template.Width - 40, page.ActualSize.Height - template.Height - 40),
      size: template.Size
    });

    // 创建图章注释,并把模板设成它的外观
    let stamp = new pdfModule.PdfRubberStampAnnotation({ rectangle: rect });
    let appearance = new pdfModule.PdfAppearance(stamp);
    appearance.Normal = template;
    stamp.Appearance = appearance;

    // 将图章注释添加到页面的注释集合中
    page.AnnotationsWidget.Add(stamp);

    // 保存文档
    const outputFileName = '文字图章.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>添加文字图章</h1>
      <button onClick={addTextStamp}>
        开始添加
      </button>
    </div>
  );
}

export default App;

带圆角边框与「已核准」文字的文字图章:

带圆角边框与「已核准」文字的文字图章


添加日期时间图章

Spire.PDF for JavaScript 还提供 PdfTemplate,用于把任意内容渲染成图章外观。下面的代码演示了在模板上画一行当前时间:字符串用浏览器原生的 Date 生成,DrawString 接受的就是一段普通字符串,图章内容便会随生成时机变化。

function App() {
  const addDateTimeStamp = async () => {
    // 获取 Spire.PDF WASM 模块
    const pdfModule = window.wasmModule?.spirepdf;

    // 检查模块是否就绪
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // 将含中文字形的字体载入 VFS 的字体目录
    await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

    // 将待处理的 PDF 载入 VFS
    const inputFileName = '示例文档.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // 创建 PdfDocument 对象并加载文档
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // 获取第一页
    let page = doc.Pages.get_Item(0);

    // 获取当前日期时间
    const now = new Date();
    const pad = (value) => String(value).padStart(2, '0');
    let timeString = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;

    // 创建模板,宽高按文字长度预留
    let template = new pdfModule.PdfTemplate({ width: 150, height: 16 });

    // 准备字体与画笔
    let font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 12 });
    let brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() }) });

    // 把时间文字画到模板上
    template.Graphics.DrawString({ s: timeString, font: font, brush: brush, point: new pdfModule.PointF(0, 0) });

    // 在页面右下角框出图章的位置,矩形与模板等大
    let rect = new pdfModule.RectangleF({
      location: new pdfModule.PointF(page.ActualSize.Width - template.Width - 20, page.ActualSize.Height - template.Height - 20),
      size: template.Size
    });

    // 创建图章注释,并把模板设成它的外观
    let stamp = new pdfModule.PdfRubberStampAnnotation({ rectangle: rect });
    let appearance = new pdfModule.PdfAppearance(stamp);
    appearance.Normal = template;
    stamp.Appearance = appearance;

    // 将图章注释添加到页面的注释集合中
    page.AnnotationsWidget.Add(stamp);

    // 保存文档
    const outputFileName = '日期时间图章.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // 从 VFS 读取生成的文件,触发下载
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>添加日期时间图章</h1>
      <button onClick={addDateTimeStamp}>
        开始添加
      </button>
    </div>
  );
}

export default App;

页角记录了当前日期时间的图章:

页角记录了当前日期时间的图章


常见问题

图章盖得太大,位置也对不上

原因:图章在页面上的位置和大小由 PdfRubberStampAnnotation 的矩形决定,而外观又铺满这个矩形。模板尺寸与矩形尺寸不一致时,外观会被拉伸;矩形坐标若直接套用图片的像素宽高而不缩放,图章还会盖过正文甚至越出页面。

解决:先算好目标显示尺寸,让模板与矩形共用同一组宽高,再按页面尺寸定位:

// 模板与矩形用同一组宽高
let template = new pdfModule.PdfTemplate({ width: stampWidth, height: stampHeight });
let rect = new pdfModule.RectangleF({
  location: new pdfModule.PointF(page.ActualSize.Width - stampWidth - 40, page.ActualSize.Height - stampHeight - 40),
  size: template.Size
});

文字图章里的中文显示成空心方框

原因:PdfTrueTypeFont 通过 fontFile 取字体文件,若该文件不含中文字形(如 ARIAL.TTF 这类西文字体),中文取不到字形,就渲染成一串方框。

解决:换用含中文字形的字体,并先把文件载入虚拟文件系统的 /Library/Fonts/:

await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 12 });

日期时间图章的时间格式怎么自定义

原因:DrawString 只画传入的字符串,格式完全由生成字符串的代码决定;toLocaleString 之类的写法又依赖浏览器的区域设置,换一个环境输出可能就不一样。

解决:需要固定格式时,用 Date 的取值方法自己拼:

const now = new Date();
const pad = (value) => String(value).padStart(2, '0');
const timeString = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;

获取免费许可证

如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。

阅读 10 次数