PDF 到了审阅环节就变成一份要来回传的文件:这一页要标出重点,那一处要写明修改意见,定稿还得盖上「已核准」。这些留痕如果靠人工在阅读器里逐个点,文档一多就顾不过来,而且下一版又要重来一遍。把留痕做进生成流程,注释就能跟着文档一起产出。
Spire.PDF for JavaScript 基于 WebAssembly 在浏览器端直接加载、修改与保存 PDF 文档,注释全部在本地生成,通过虚拟文件系统(VFS)读写文件,无需后端配合。本文用 PdfPage.Annotations 承载八类注释:文本标记、自由文本、弹出式、印章、线条与多边形,以及网页链接、文件链接和文档内跳转链接。
本文介绍八个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
添加文本标记注释
PdfTextMarkupAnnotation 在已有文字上叠加标记,不改动原文字本身。把 TextMarkupAnnotationType 设为 Highlight、再用 TextMarkupColor 指定颜色,就得到最常见的高亮,与正文颜色互不影响;标记范围由四个角点(QuadPoints)围成,用的是绝对页面坐标,所以要先清掉页面边距,画布与角点才对得上。title 与 text 会作为批注的作者和备注写进注释对象,页面不会显示,需要在阅读器的批注面板中查看。
function App() {
const addTextMarkupAnnotation = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 页面上的中文说明文字要用到字体,先把它载入 VFS
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const doc = new pdfModule.PdfDocument();
// 页面边距清零,画布坐标与注释的角点坐标才能对齐
const margins = new pdfModule.PdfMargins();
margins.Top = 0;
margins.Bottom = 0;
margins.Left = 0;
margins.Right = 0;
doc.PageSettings.Margins = margins;
const page = doc.Pages.Add();
const font = new pdfModule.PdfTrueTypeFont({
fontFamily: 'Arial Unicode MS',
size: 14,
style: pdfModule.PdfFontStyle.Regular,
unicode: true,
});
const brush = pdfModule.PdfBrushes.get_Black();
const text = '高亮标记';
// 先把目标文字画到页面上
page.Canvas.DrawString({ s: text, font, brush, x: 80, y: 120 });
// 取文字的宽高,让标记正好盖住文字
const size = font.MeasureString({ text });
const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: size.Width, height: size.Height });
const annotation = new pdfModule.PdfTextMarkupAnnotation({ title: '校对', text: '需要复核', rect, font });
// 标记范围:文字矩形的四个角点
annotation.QuadPoints = [
new pdfModule.PointF(rect.Left, rect.Top),
new pdfModule.PointF(rect.Right, rect.Top),
new pdfModule.PointF(rect.Left, rect.Bottom),
new pdfModule.PointF(rect.Right, rect.Bottom),
];
annotation.TextMarkupAnnotationType = pdfModule.PdfTextMarkupAnnotationType.Highlight;
annotation.TextMarkupColor = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_LightSkyBlue() });
page.Annotations.Add(annotation);
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={addTextMarkupAnnotation}>添加注释</button>
</div>
);
}
export default App;
页面上的一段文字被高亮标记盖住:

添加自由文本注释
PdfFreeTextAnnotation 把一段文字以文本框的形式直接画在页面上,常见于批注、说明或填空。文本框的位置与大小由构造时的矩形决定,字体、边框、底色和不透明度都可以再设。
function App() {
const addFreeTextAnnotation = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 文本框内的中文要用到字体,先把它载入 VFS
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: 220, height: 60 });
// 矩形作为构造参数传入,注释的位置与大小就由它决定
const annotation = new pdfModule.PdfFreeTextAnnotation(rect);
annotation.Text = '这是一条自由文本注释';
annotation.Font = new pdfModule.PdfTrueTypeFont({
fontFamily: 'Arial Unicode MS',
size: 14,
style: pdfModule.PdfFontStyle.Regular,
unicode: true,
});
annotation.Border = new pdfModule.PdfAnnotationBorder({ borderWidth: 1 });
annotation.BorderColor = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Gray() });
annotation.Color = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_LightGoldenrodYellow() });
annotation.Opacity = 0.8;
page.Annotations.Add(annotation);
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={addFreeTextAnnotation}>添加注释</button>
</div>
);
}
export default App;
页面上带边框与底色的一段自由文本注释:

添加弹出式注释
PdfPopupAnnotation 放置一个可展开的弹出窗口,收起时只留一个图标,适合放较长的说明而不占版面。图标样式由 Icon 指定,取值有 Note、Comment、Help、Insert、Key、NewParagraph、Paragraph。
function App() {
const addPopupAnnotation = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
// 弹出注释收起时只有一个图标,用零尺寸矩形标记所在位置
const rect = new pdfModule.RectangleF({ x: 120, y: 160, width: 0, height: 0 });
const annotation = new pdfModule.PdfPopupAnnotation({ rectangle: rect, text: '这里是弹出注释的内容' });
annotation.Icon = pdfModule.PdfPopupIcon.Note;
annotation.Open = true;
annotation.Color = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Yellow() });
page.Annotations.Add(annotation);
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={addPopupAnnotation}>添加注释</button>
</div>
);
}
export default App;
页面上的弹出注释,图标旁展开注释内容:

添加印章注释
PdfRubberStampAnnotation 用于盖图章。它内置了一组现成图标,指定 Icon 就能得到一个带文字的印章框,不必自己画外观。可选的图标包括 Approved、Confidential、Draft、Final、Expired、Sold、TopSecret、NotForPublicRelease 等十几种。
function App() {
const addStampAnnotations = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
// 用内置图标盖三种常见的印章
const stamps = [
{ icon: pdfModule.PdfRubberStampAnnotationIcon.Approved, y: 200, color: pdfModule.Color.get_LightGreen() },
{ icon: pdfModule.PdfRubberStampAnnotationIcon.Confidential, y: 300, color: pdfModule.Color.get_LightPink() },
{ icon: pdfModule.PdfRubberStampAnnotationIcon.Draft, y: 400, color: pdfModule.Color.get_LightBlue() },
];
for (const stamp of stamps) {
const rect = new pdfModule.RectangleF({ x: 100, y: stamp.y, width: 150, height: 60 });
const annotation = new pdfModule.PdfRubberStampAnnotation({ rectangle: rect, text: '印章注释' });
annotation.Icon = stamp.icon;
annotation.Color = new pdfModule.PdfRGBColor({ color: stamp.color });
page.Annotations.Add(annotation);
}
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={addStampAnnotations}>添加印章</button>
</div>
);
}
export default App;
添加三种内置图标印章:

添加形状注释
形状注释把几何图形作为独立对象挂在页面上,和直接往画布绘制不同。它们带着自己的属性和备注,能被阅读器识别、单独选中。三类分别对应线、多边形和折线:
| 类 | 说明 |
|---|---|
PdfLineAnnotation |
直线,可设端点样式与箭头 |
PdfPolygonAnnotation |
由多个顶点围成的闭合多边形 |
PdfPolyLineAnnotation |
由多个顶点连成的开放折线 |
多边形在加入页面前会重新生成外观,需要先设好 Author 与 Subject,否则 Add 会因空引用失败。
function App() {
const addShapeAnnotations = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
// 线条:给定起点与终点,两端加箭头
const line = new pdfModule.PdfLineAnnotation({ linePoints: [80, 120, 300, 200], text: '这是一条线注释' });
line.lineBorder.BorderStyle = pdfModule.PdfLineBorderStyle.Solid;
line.lineBorder.BorderWidth = 2;
line.BeginLineStyle = pdfModule.PdfLineEndingStyle.Circle;
line.EndLineStyle = pdfModule.PdfLineEndingStyle.ClosedArrow;
line.BackColor = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Blue() });
page.Annotations.Add(line);
// 多边形:顶点首尾相接围成闭合区域
const polygonPoints = [
new pdfModule.PointF(80, 300),
new pdfModule.PointF(160, 260),
new pdfModule.PointF(240, 300),
new pdfModule.PointF(200, 380),
new pdfModule.PointF(120, 380),
new pdfModule.PointF(80, 300),
];
const polygon = new pdfModule.PdfPolygonAnnotation(page, polygonPoints);
polygon.Color = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_PaleVioletRed() });
polygon.Text = '这是一个多边形注释';
polygon.Author = 'E-ICEBLUE';
polygon.Subject = 'polygon annotation demo';
polygon.BorderEffect = pdfModule.PdfBorderEffect.BigCloud;
polygon.ModifiedDate = new Date();
page.Annotations.Add(polygon);
// 折线:顶点依次相连,不闭合
const polylinePoints = [
new pdfModule.PointF(320, 300),
new pdfModule.PointF(400, 260),
new pdfModule.PointF(480, 340),
new pdfModule.PointF(540, 300),
];
const polyline = new pdfModule.PdfPolyLineAnnotation(page, polylinePoints);
polyline.Color = new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Green() });
polyline.Text = '这是一条折线注释';
polyline.Border = new pdfModule.PdfAnnotationBorder({ borderWidth: 2 });
page.Annotations.Add(polyline);
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={addShapeAnnotations}>添加形状</button>
</div>
);
}
export default App;
同一页上的线、多边形与折线三种形状注释:

添加网页链接注释
PdfUriAnnotation 把一块矩形区域变成指向网址的链接,点击后由阅读器交给浏览器打开。
function App() {
const addWebLinkAnnotation = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 页面上要用中文字体,先把它载入 VFS
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
const font = new pdfModule.PdfTrueTypeFont({ fontFamily: 'Arial Unicode MS', size: 14, style: pdfModule.PdfFontStyle.Regular, unicode: true });
const text = '点击访问 Spire.PDF 产品页';
// 链接区域不可见,先画一段文字作为可点击的入口
page.Canvas.DrawString({ s: text, font, brush: pdfModule.PdfBrushes.get_DodgerBlue(), x: 80, y: 120 });
const size = font.MeasureString({ text });
const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: size.Width, height: size.Height });
const link = new pdfModule.PdfUriAnnotation({ rectangle: rect });
link.Uri = 'https://www.e-iceblue.com/Introduce/Spire-PDF-Javascript.html';
page.Annotations.Add(link);
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={addWebLinkAnnotation}>添加链接</button>
</div>
);
}
export default App;
指向产品页的网页链接注释,链接区域覆盖这段文字:

添加文件链接注释
PdfFileLinkAnnotation 把区域变成文件链接,点击后由阅读器按系统关联打开指定文件。
function App() {
const addFileLinkAnnotation = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 页面上要用中文字体,先把它载入 VFS
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
const font = new pdfModule.PdfTrueTypeFont({ fontFamily: 'Arial Unicode MS', size: 14, style: pdfModule.PdfFontStyle.Regular, unicode: true });
const text = '点击打开记事本';
// 先画一段文字,作为文件链接的点击入口
page.Canvas.DrawString({ s: text, font, brush: pdfModule.PdfBrushes.get_DodgerBlue(), x: 80, y: 120 });
const size = font.MeasureString({ text });
const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: size.Width, height: size.Height });
const link = new pdfModule.PdfFileLinkAnnotation(rect, 'C:\\Windows\\System32\\notepad.exe');
page.Annotations.Add(link);
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={addFileLinkAnnotation}>添加链接</button>
</div>
);
}
export default App;
点击后按系统关联打开指定文件的文件链接注释:

添加文档内链接注释
PdfDocumentLinkAnnotation 负责文档内部跳转,点击后按 PdfDestination 指定的页与位置定位。
function App() {
const addDocumentLinkAnnotation = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 页面上要用中文字体,先把它载入 VFS
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
// 追加第 2 页作为跳转目标
const target = doc.Pages.Add();
const font = new pdfModule.PdfTrueTypeFont({ fontFamily: 'Arial Unicode MS', size: 14, style: pdfModule.PdfFontStyle.Regular, unicode: true });
target.Canvas.DrawString({ s: '这是跳转目标页', font, brush: pdfModule.PdfBrushes.get_Black(), x: 80, y: 120 });
const text = '点击跳到第 2 页';
page.Canvas.DrawString({ s: text, font, brush: pdfModule.PdfBrushes.get_DodgerBlue(), x: 80, y: 120 });
const size = font.MeasureString({ text });
// 目标:第 2 页顶部向下 80 磅处,缩放 100%
const destination = new pdfModule.PdfDestination({ page: target });
destination.Mode = pdfModule.PdfDestinationMode.Location;
destination.Location = new pdfModule.PointF(0, 80);
destination.Zoom = 1;
const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: size.Width, height: size.Height });
const link = new pdfModule.PdfDocumentLinkAnnotation({ rectangle: rect, destination });
page.Annotations.Add(link);
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={addDocumentLinkAnnotation}>添加链接</button>
</div>
);
}
export default App;
第 1 页上的链接指向第 2 页的指定位置:

常见问题
页面上的中文显示成方框或空白
原因:正文与注释里的中文都需要一份中文 TrueType 字体。没有把字体载入 VFS,或者用 PdfTrueTypeFont 时漏了 unicode: true,字形就取不到,落盘后打开只能看到方框或空白。
解决:先把字体文件载入 VFS 的 /Library/Fonts/,再指定同一个字体名并把 unicode 打开:
// 载入字体到 VFS
await window.spire.FetchFileToVFS('ARIALUNI.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 用同一个字体名,并开启 unicode
const font = new pdfModule.PdfTrueTypeFont({ fontFamily: 'Arial Unicode MS', size: 14, style: pdfModule.PdfFontStyle.Regular, unicode: true });
链接注释在页面上看不到任何痕迹
原因:链接类注释只定义一个可点击区域和动作,本身不绘制外观。多数阅读器都不会给链接画边框,所以保存后页面看起来和没加一样——这不代表注释没写进去。
解决:要让它可见,就在同一矩形位置画上文字或图形,把链接盖在上面:
// 先画可见文字,再用同样大小的矩形构造链接
page.Canvas.DrawString({ s: text, font, brush: pdfModule.PdfBrushes.get_DodgerBlue(), x: 80, y: 120 });
const size = font.MeasureString({ text });
const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: size.Width, height: size.Height });
const link = new pdfModule.PdfUriAnnotation({ rectangle: rect });
link.Uri = 'https://www.e-iceblue.com/';
文本标记注释没有盖在文字上
原因:标记范围由 QuadPoints 决定,它用的是绝对页面坐标,不会跟着文字自动对齐;而画布的原点会被页面边距挤开。两者错位,标记就偏到旁边或盖住空处。
解决:先把页面边距清零,让画布与角点用同一套坐标;再用 MeasureString 量出文字宽高,据此给出四个角点:
const margins = new pdfModule.PdfMargins();
margins.Top = 0; margins.Bottom = 0; margins.Left = 0; margins.Right = 0;
doc.PageSettings.Margins = margins;
const size = font.MeasureString({ text: '高亮标记' });
const rect = new pdfModule.RectangleF({ x: 80, y: 120, width: size.Width, height: size.Height });
annotation.QuadPoints = [
new pdfModule.PointF(rect.Left, rect.Top),
new pdfModule.PointF(rect.Right, rect.Top),
new pdfModule.PointF(rect.Left, rect.Bottom),
new pdfModule.PointF(rect.Right, rect.Bottom),
];
获取免费许可证
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。







