正式外发的合同、报告,正文之外通常还要带上页眉和页脚:页眉标明这份文件属于谁,页脚放页码与“内部资料”之类的字样。逐份手工敲进去费时又容易漏,交给服务端批量处理,则意味着文档要先离开用户的设备。
本文介绍用 Spire.PDF for JavaScript 在现成 PDF 中添加文字页眉页脚、图片页眉页脚,以及自动页码。它基于 WebAssembly 在浏览器端直接加载、修改与保存 PDF 文档,页眉页脚画在页面已有的留白区内,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。
本文介绍三个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
坐标体系
在现成的 PDF 页面中,坐标体系设定如下:坐标原点位于页面的左上角,X 轴向右延伸,Y 轴向下延伸(如下图所示)。数值的单位是磅,下面三个功能点里的绘制坐标都按这套口径计算。

添加文字页眉页脚
给每页加上固定的页眉页脚,用 PdfPageBase.Canvas 上的 DrawString 把文字画到指定坐标即可。
function App() {
const addTextHeaderFooter = 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 对象并加载 PDF 文档
const doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 页面边距,以及页眉页脚共用的画笔、字体与对齐方式
const margin = doc.PageSettings.Margins;
const pen = new pdfModule.PdfPen({ brush: pdfModule.PdfBrushes.get_Black(), width: 0.75 });
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 10 });
const brush = pdfModule.PdfBrushes.get_Black();
const rightAlign = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Right });
rightAlign.MeasureTrailingSpaces = true;
// 分隔线与文字之间留出的间距
const space = font.Height * 0.75;
// 逐页绘制页眉与页脚
for (let i = 0; i < doc.Pages.Count; i++) {
const canvas = doc.Pages.get_Item(i).Canvas;
const x = margin.Left;
const width = canvas.ClientSize.Width - margin.Left - margin.Right;
// 页眉:内容区上方画分隔线,线上方右对齐排标题
let y = margin.Top - space;
canvas.DrawLine({ pen: pen, x1: x, y1: y, x2: x + width, y2: y });
canvas.DrawString({
s: '冰蓝科技',
font: font,
brush: brush,
x: x + width,
y: y - font.Height,
format: rightAlign
});
// 页脚:内容区下方画分隔线,线下方右对齐排说明
y = canvas.ClientSize.Height - margin.Bottom + space;
canvas.DrawLine({ pen: pen, x1: x, y1: y, x2: x + width, y2: y });
canvas.DrawString({
s: '内部资料,请勿外传',
font: font,
brush: brush,
x: x + width,
y: y + 1,
format: rightAlign
});
}
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={addTextHeaderFooter}>
开始添加
</button>
</div>
);
}
export default App;
每一页都带上右对齐的页眉与页脚,与正文之间各有一条分隔线:

在页眉页脚中添加图片
页眉页脚里也常放公司标识,PdfImage.FromFile 从虚拟文件系统读入图片后,用 Canvas.DrawImage 按左上角坐标把它画到页面上。图片尺寸以像素计,先用 PdfUnitConvertor 换算成磅,才好和页面坐标对齐。
function App() {
const addImageHeaderFooter = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将待处理的 PDF 与标识图片载入 VFS
const inputFileName = '多页文档.pdf';
const imageFileName = 'E-iceblue.png';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS(imageFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 创建 PdfDocument 对象并加载 PDF 文档
const doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 读入标识图片,并把像素尺寸换算成磅
const image = pdfModule.PdfImage.FromFile(imageFileName);
const unitCvtr = new pdfModule.PdfUnitConvertor();
const pointWidth = unitCvtr.ConvertUnits(image.Width, pdfModule.PdfGraphicsUnit.Pixel, pdfModule.PdfGraphicsUnit.Point);
const pointHeight = unitCvtr.ConvertUnits(image.Height, pdfModule.PdfGraphicsUnit.Pixel, pdfModule.PdfGraphicsUnit.Point);
// 逐页把标识画进页眉右上角与页脚左下角
for (let i = 0; i < doc.Pages.Count; i++) {
const page = doc.Pages.get_Item(i);
const canvas = page.Canvas;
// 页眉右上角
canvas.DrawImage({
image: image,
x: page.ActualSize.Width - pointWidth - 55,
y: 20
});
// 页脚左下角
canvas.DrawImage({
image: image,
x: 55,
y: page.ActualSize.Height - pointHeight - 20
});
}
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={addImageHeaderFooter}>
开始添加
</button>
</div>
);
}
export default App;
公司标识落在页眉右上角与页脚左下角:

添加自动页码与总页数
产品提供了 PdfPageNumberField 和 PdfPageCountField 类,用以获取当前页面的页码和 PDF 文档的总页数。它们可以合并成一个 PdfCompositeField,将输出格式化为 "Page X of Y",其中 X 表示当前页码,Y 表示总页数。
function App() {
const addPageNumbers = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 把中文字体载入 VFS,供“第 X 页 / 共 Y 页”使用
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 对象并加载 PDF 文档
const doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// 页面边距,以及页码文字的画笔、字体与对齐方式
const margin = doc.PageSettings.Margins;
const pen = new pdfModule.PdfPen({ brush: pdfModule.PdfBrushes.get_Black(), width: 0.75 });
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 10 });
const brush = pdfModule.PdfBrushes.get_Black();
const rightAlign = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Right });
rightAlign.MeasureTrailingSpaces = true;
const space = font.Height * 0.75;
// 当前页码与文档总页数两个动态字段
const number = new pdfModule.PdfPageNumberField();
const count = new pdfModule.PdfPageCountField();
// 按占位符顺序拼成“第 X 页 / 共 Y 页”,list 的顺序要和 {0}、{1} 一一对应
const compositeField = new pdfModule.PdfCompositeField({
font: font,
brush: brush,
text: '第 {0} 页 / 共 {1} 页',
list: [number, count]
});
compositeField.StringFormat = rightAlign;
// 逐页画分隔线并右对齐放置页码
for (let i = 0; i < doc.Pages.Count; i++) {
const canvas = doc.Pages.get_Item(i).Canvas;
const x = margin.Left;
const width = canvas.ClientSize.Width - margin.Left - margin.Right;
// 内容区下方画一条分隔线
const y = canvas.ClientSize.Height - margin.Bottom + space;
canvas.DrawLine({ pen: pen, x1: x, y1: y, x2: x + width, y2: y });
// 量出文字尺寸,把页码落在分隔线下、贴齐内容区右边缘
const size = font.MeasureString({ text: '第 1 页 / 共 1 页' });
compositeField.Bounds = new pdfModule.RectangleF({
x: x + width - size.Width,
y: y + 1,
width: size.Width,
height: size.Height
});
// 动态字段直接绘制到画布
compositeField.Draw({ graphics: canvas });
}
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={addPageNumbers}>
开始添加
</button>
</div>
);
}
export default App;
页脚在分隔线下右对齐显示“第 X 页 / 共 Y 页”:

常见问题
页眉页脚直接画上去,压住了正文
原因:页眉页脚是叠在页面上的,位置由 PageSettings.Margins 推出的坐标决定。载入的 PDF 若上下留白很小,这行文字就会和正文重叠。
解决:先把原页面存成模板,画完页眉页脚后再把原内容叠回原位,正文与页眉页脚各占一层;不需要分层时,换一份上下留白足够的源文件:
// 先把当前页内容存成模板
const template = page.CreateTemplate();
// 画完页眉页脚后,把原内容重新铺满整页
const graphicsWidget = new pdfModule.PdfGraphicsWidget(template.H);
graphicsWidget.Draw({ graphics: canvas, location: new pdfModule.PointF(0, 0) });
页眉页脚里的中文显示成方框
原因:字体不含中文字形。用 ARIAL.TTF 这类西文字体渲染中文,取不到对应字形,页面上就是一个个空心方框。
解决:改用带中文的字体文件,并先把它载入虚拟文件系统的 /Library/Fonts/:
// 先载入中文字体,再交给 PdfTrueTypeFont 使用
await window.spire.FetchFileToVFS('SIMSUN.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 10 });
页码里的数字对不上
原因:PdfCompositeField 的 text 用 {0}、{1} 当占位符,它们按 list 给出的顺序依次填入。把 PdfPageCountField 排在 PdfPageNumberField 前面,或者 text 里两个占位符写反,都会让页码和总页数互换。
解决:占位符顺序与 list 保持一致,页码在前、总页数在后:
const compositeField = new pdfModule.PdfCompositeField({
font: font,
brush: brush,
text: '第 {0} 页 / 共 {1} 页',
list: [number, count]
});
获取免费许可证
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。







