把报表、单据、说明文字在浏览器里直接排版导出成 PDF,往往是从一张空白页开始的。页眉页脚通常要在建文档的阶段一并安排:等到正文排完再往回补,位置和分页都会很被动。手工逐页敲一遍同样繁琐,交给服务端处理又意味着数据要离开用户的设备。
本文用 Spire.PDF for JavaScript 从零新建 PDF 并添加页眉与页脚。页面自身边距设为 0,四周留白改由文档模板(PdfDocumentTemplate)的 Left、Top、Right、Bottom 四块 PdfPageTemplateElement 占位,页眉页脚就画在上下两块模板里,新建的每一页在渲染时自动带上。页脚还能用 PdfCompositeField 动态填出页码。它基于 WebAssembly 在浏览器端新建、绘制与保存文档,通过虚拟文件系统(VFS)读写文件,无需后端配合。
本文介绍两个核心功能点:
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
创建新 PDF 时添加页眉
Spire.PDF for JavaScript 提供 PdfPageTemplateElement,用于为页面预留一块可独立绘制、并随页面自动带上的内容区。把它赋给 PdfDocumentTemplate.Top,新建的每一页都会在上页边距里带上这块页眉模板。模板元素的高度取上页边距,内部画线、写字、放图片的坐标以页面左上角为原点,x 向右、y 向下延伸。
function App() {
const addHeaderToNewPdf = 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/`);
const imageFileName = 'E-iceblue.png';
await window.spire.FetchFileToVFS(imageFileName, "", `${process.env.PUBLIC_URL}/data/`);
// 新建 PDF 文档,页面尺寸 A4
const doc = new pdfModule.PdfDocument();
const pageSize = pdfModule.PdfPageSize.A4();
doc.PageSettings.Size = pageSize;
// 页面自身边距设为 0,四周留白改由文档模板的各边模板占位(各 50 磅)
doc.PageSettings.Margins = new pdfModule.PdfMargins({ margin: 0 });
const margins = new pdfModule.PdfMargins({ margin: 50 });
// 左右下三边先用空白模板占位,正文区因此内缩
doc.Template.Left = new pdfModule.PdfPageTemplateElement({ width: margins.Left, height: pageSize.Height - margins.Top - margins.Bottom });
doc.Template.Right = new pdfModule.PdfPageTemplateElement({ width: margins.Right, height: pageSize.Height - margins.Top - margins.Bottom });
doc.Template.Bottom = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Bottom });
// 页眉模板:高度取上页边距,Foreground = true 让它画在正文之上
const headerSpace = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Top });
headerSpace.Foreground = true;
doc.Template.Top = headerSpace;
// 页眉用的字体、画刷、画笔与对齐方式
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 10, style: pdfModule.PdfFontStyle.Italic });
const brush = pdfModule.PdfBrushes.get_Gray();
const pen = new pdfModule.PdfPen({ brush: pdfModule.PdfBrushes.get_Gray(), width: 1.0 });
const leftAlign = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Left });
// 读入页眉图片,并把像素尺寸换算成磅
const headerImage = pdfModule.PdfImage.FromFile(imageFileName);
const unitCvtr = new pdfModule.PdfUnitConvertor();
const pointWidth = unitCvtr.ConvertUnits(headerImage.Width, pdfModule.PdfGraphicsUnit.Pixel, pdfModule.PdfGraphicsUnit.Point);
const pointHeight = unitCvtr.ConvertUnits(headerImage.Height, pdfModule.PdfGraphicsUnit.Pixel, pdfModule.PdfGraphicsUnit.Point);
// 页眉右侧放标识图片,左侧放两行文字,底部画一条分隔线
headerSpace.Graphics.DrawImage({
image: headerImage,
x: headerSpace.Width - margins.Left - pointWidth,
y: headerSpace.Height - pointHeight
});
headerSpace.Graphics.DrawString({
s: '成都冰蓝科技有限公司\nwww.e-iceblue.cn',
font: font,
brush: brush,
x: margins.Left,
y: headerSpace.Height - font.Height * 2,
format: leftAlign
});
headerSpace.Graphics.DrawLine({
pen: pen,
x1: margins.Left,
y1: margins.Top,
x2: pageSize.Width - margins.Left,
y2: margins.Top
});
// 新建页面并写入正文,页眉由模板在渲染时自动带上
const bodyFont = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 14 });
const bodyBrush = pdfModule.PdfBrushes.get_Blue();
const texts = ['Spire.PDF 添加页眉示例', '第二页正文内容'];
for (let i = 0; i < texts.length; i++) {
const page = doc.Pages.Add();
page.Canvas.DrawString({ s: texts[i], font: bodyFont, brush: bodyBrush, x: margins.Left, y: margins.Top + 30 });
}
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>创建新 PDF 时添加页眉</h1>
<button onClick={addHeaderToNewPdf}>
开始生成
</button>
</div>
);
}
export default App;
新建的每一页都在顶部带上页眉:

创建新 PDF 时添加页脚
页脚挂在 PdfDocumentTemplate.Bottom 上,做法与页眉一致,只是模板高度换成了下页边距。页脚里常带页码:PdfPageNumberField 与 PdfPageCountField 合成 PdfCompositeField,翻到哪一页,数字就替成哪一页。
function App() {
const addFooterToNewPdf = 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 文档,页面尺寸 A4
const doc = new pdfModule.PdfDocument();
const pageSize = pdfModule.PdfPageSize.A4();
doc.PageSettings.Size = pageSize;
// 页面自身边距设为 0,四周留白改由文档模板的各边模板占位(各 50 磅)
doc.PageSettings.Margins = new pdfModule.PdfMargins({ margin: 0 });
const margins = new pdfModule.PdfMargins({ margin: 50 });
// 左右上三边先用空白模板占位,正文区因此内缩
doc.Template.Left = new pdfModule.PdfPageTemplateElement({ width: margins.Left, height: pageSize.Height - margins.Top - margins.Bottom });
doc.Template.Right = new pdfModule.PdfPageTemplateElement({ width: margins.Right, height: pageSize.Height - margins.Top - margins.Bottom });
doc.Template.Top = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Top });
// 页脚模板:高度取下页边距,Foreground = true 让它画在正文之上
const footerSpace = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Bottom });
footerSpace.Foreground = true;
doc.Template.Bottom = footerSpace;
// 页脚用的字体、画刷、画笔与对齐方式
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 12, style: pdfModule.PdfFontStyle.Italic });
const brush = pdfModule.PdfBrushes.get_Gray();
const pen = new pdfModule.PdfPen({ brush: pdfModule.PdfBrushes.get_Gray(), width: 1.0 });
const leftAlign = new pdfModule.PdfStringFormat({ alignment: pdfModule.PdfTextAlignment.Left });
// 页脚顶部画分隔线,线下左对齐放两行联系方式
footerSpace.Graphics.DrawLine({ pen: pen, x1: margins.Left, y1: 0, x2: pageSize.Width - margins.Left, y2: 0 });
footerSpace.Graphics.DrawString({
s: '邮箱:该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。\n电话:028-81705109',
font: font,
brush: brush,
x: margins.Left,
y: 2,
format: leftAlign
});
// 当前页码与总页数两个动态字段
const number = new pdfModule.PdfPageNumberField();
const count = new pdfModule.PdfPageCountField();
// 按占位符顺序拼成“第{0}页共{1}页”
const compositeField = new pdfModule.PdfCompositeField({
font: font,
brush: brush,
text: '第{0}页共{1}页',
list: [number, count]
});
compositeField.StringFormat = new pdfModule.PdfStringFormat({
alignment: pdfModule.PdfTextAlignment.Right,
lineAlignment: pdfModule.PdfVerticalAlignment.Top
});
// 量出页码文字尺寸,右对齐落在页脚同一行的右端
const size = font.MeasureString({ text: compositeField.Text });
compositeField.Bounds = new pdfModule.RectangleF({
x: pageSize.Width - margins.Left - size.Width,
y: 2,
width: size.Width,
height: size.Height
});
compositeField.Draw({ graphics: footerSpace.Graphics });
// 新建页面并写入正文,页脚由模板在渲染时自动带上
const bodyFont = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/SIMSUN.TTF', size: 14 });
const bodyBrush = pdfModule.PdfBrushes.get_Blue();
const texts = ['Spire.PDF 添加页脚示例', '第二页正文内容'];
for (let i = 0; i < texts.length; i++) {
const page = doc.Pages.Add();
page.Canvas.DrawString({ s: texts[i], font: bodyFont, brush: bodyBrush, x: margins.Left, y: margins.Top + 30 });
}
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>创建新 PDF 时添加页脚</h1>
<button onClick={addFooterToNewPdf}>
开始生成
</button>
</div>
);
}
export default App;
新建的每一页都在底部带上页脚:

常见问题
正文贴到页面边缘,和页眉页脚叠在一起
原因:页面自身的边距被设为 0,正文区的留白完全由 doc.Template 的 Left、Top、Right、Bottom 四块模板占位。只给页眉或页脚那个方向挂了模板,其余方向没挂,正文就会顶到那一侧的页面边缘。
解决:四个方向都挂上模板元素,不需要放内容的用空白模板占位:
doc.Template.Left = new pdfModule.PdfPageTemplateElement({ width: margins.Left, height: pageSize.Height - margins.Top - margins.Bottom });
doc.Template.Right = new pdfModule.PdfPageTemplateElement({ width: margins.Right, height: pageSize.Height - margins.Top - margins.Bottom });
doc.Template.Top = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Top });
doc.Template.Bottom = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Bottom });
页眉页脚里的中文显示成方框
原因:模板文字用了不含中文字形的西文字体(如 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 });
页眉页脚被正文盖住
原因:PdfPageTemplateElement.Foreground 默认为 false,模板画在正文下层。正文若铺到同一位置,就会盖住页眉页脚。
解决:把模板元素的 Foreground 设为 true,让它画在正文之上:
const headerSpace = new pdfModule.PdfPageTemplateElement({ width: pageSize.Width, height: margins.Top });
headerSpace.Foreground = true;
doc.Template.Top = headerSpace;
获取免费许可证
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。







