适用产品:福昕 PDF SDK for Web(UIExtension / PDFViewCtrl)| 接口起始版本:7.4.0,本文接口在 11.1 验证
PDF 页面里的图形对象(GraphicsObject)分为文本、路径、图像、渐变着色、Form XObject 五类。在做内容审核、图纸解析、渲染耗时预估、按页分片处理这类需求时,通常需要先知道每一页到底有多少个图形对象。本文给出可直接使用的接口调用方式、按类型细分的两条实现路线,以及在不同文档规模下该怎么选。
一、接口一览
统计相关的方法都在 PDFPage 上,GraphicsObject 提供类型信息。调用前请先确认版本,getGraphicsObjectsCount 自 7.4.0 起提供。
| 接口 |
参数 |
返回值 |
起始版本 |
用途 |
PDFDoc.getPageCount() |
无 |
number(同步) |
— |
取总页数 |
PDFDoc.getPageByIndex(index) |
index: number |
Promise<PDFPage> |
— |
取指定页 |
PDFPage.getGraphicsObjectsCount() |
无 |
Promise<number> |
7.4.0 |
取本页图形对象总数 |
PDFPage.getGraphicsObjectByIndex(index) |
index: number |
Promise<GraphicsObject> |
7.3.0 |
取第 index 个对象 |
GraphicsObject.getType() |
无 |
number(Graphics_ObjectType) |
7.4.0 |
取对象类型 |
PDFPage.getGraphicsObjectsByRect(options) |
options 见下 |
Promise<GraphicsObject[]> |
8.5.0 |
按矩形与类型筛选 |
Graphics_ObjectType 枚举定义在 PDFViewCtrl.PDF.constant.Graphics_ObjectType:
| 成员 |
值 |
含义 |
按矩形查询支持 |
All |
0 |
全部类型,仅作过滤用 |
是 |
Text |
1 |
文本对象 |
是 |
Path |
2 |
路径/矢量图形对象 |
是 |
Image |
3 |
图像对象 |
是 |
Shading |
4 |
渐变着色对象 |
是 |
FormXObject |
5 |
Form XObject 对象 |
不支持 |
有一个容易踩的点:getPageCount() 是同步返回值,不需要也不应该 await;getPageByIndex() 才是 Promise。混用容易得到 undefined。
二、统计每页对象总数
只需要数量时,一次 getGraphicsObjectsCount() 就能拿到本页结果,不要通过循环 getGraphicsObjectByIndex() 来计数——后者的调用次数等于该页对象数,在图纸类文档上会慢几个数量级。
最简形式如下,可直接在已初始化 pdfui 的页面控制台执行:
const viewer = await pdfui.getPDFViewer();
const doc = viewer.getCurrentPDFDoc(); // PDFViewer 上同步返回 PDFDoc | null
if (!doc) throw new Error('未打开文档');
const counts = [];
for (let i = 0; i < doc.getPageCount(); i++) {
const page = await doc.getPageByIndex(i);
counts.push(await page.getGraphicsObjectsCount());
}
console.log(counts); // 例如 [12, 5, 0, 31, ...]
要同时拿到页码和数量、便于输出报表时,用下面这段:
async function countPerPage(pdfui) {
const viewer = await pdfui.getPDFViewer();
const doc = viewer.getCurrentPDFDoc();
if (!doc) throw new Error('未打开文档');
const list = [];
for (let i = 0; i < doc.getPageCount(); i++) {
const page = await doc.getPageByIndex(i);
list.push({ pageNo: i + 1, count: await page.getGraphicsObjectsCount() });
}
console.table(list);
return list;
}
const rows = await countPerPage(pdfui);
list 的结构是 [{ pageNo, count }, ...],页码从 1 开始。这个循环的调用次数是页数的两倍,200 页文档通常在数百毫秒内完成。
关于 pdfui 与 pdfViewer 的取值差异需要注意:PDFUI 类只继承了 PDFViewer 的方法声明,实际把调用委托给 PDFViewer 实例,所以在 pdfui 上调用 getCurrentPDFDoc() 返回的是 Promise,在 pdfViewer 实例上调用则是同步返回 PDFDoc。为了行为一致,建议统一走 await pdfui.getPDFViewer() 拿到实例后再调用。统计本身不依赖页面渲染,文档加载完成即可执行,服务端环境同样支持。
三、按类型细分:两条路线
需要区分文本、路径、图像各有多少时,有两条路线。它们的差异不在代码风格,而在复杂度量级——一条随对象数线性增长,一条是常数。
路线 A:矩形查询(推荐)
思路是把”统计某类型有几个”变成”查询该类型在整页有哪些”,用返回数组的长度作为数量。整页矩形从 getAllBoxes() 取 cropBox(缺失时退回 mediaBox),然后把 isInRect 设为 false,让查询覆盖与整页相交的所有对象,而不是仅完全位于页内的对象。
async function typeBreakdownFast(page) {
const c = PDFViewCtrl.PDF.constant.Graphics_ObjectType;
// 取整页矩形;PDFRect 结构为 { left, bottom, right, top },单位 pt,原点在左下角
const boxes = await page.getAllBoxes();
const rect = boxes.cropBox || boxes.mediaBox;
const result = {};
const types = ['Text', 'Path', 'Image', 'Shading'];
for (const name of types) {
const objs = await page.getGraphicsObjectsByRect({
rect: rect,
tolerance: 0,
type: c[name],
isInRect: false
});
result[name] = objs ? objs.length : 0;
}
// 该接口不支持 FormXObject,用总数减其余四类推算
const total = await page.getGraphicsObjectsCount();
const known = types.reduce((s, k) => s + result[k], 0);
result.FormXObject = total - known;
return result;
}
每页固定 5 次调用(4 次类型查询 + 1 次总数),与页面对象数无关。单页 3000 个对象的图纸,这条路线通常在几十毫秒内返回。
getGraphicsObjectsByRect 的 options 字段:
| 字段 |
类型 |
说明 |
rect |
PDFRect |
查询矩形,PDF 坐标({left, bottom, right, top}) |
tolerance |
number |
矩形容差,用整页矩形时传 0 即可 |
type |
Graphics_ObjectType |
目标类型,不支持 FormXObject |
isInRect |
boolean |
true 只返回完全在矩形内的对象;false 返回与矩形相交的对象。默认 true |
isInRect 的默认值是 true,用整页矩形查询时如果保持默认,跨越页面边界(例如因裁切框与媒体框不一致)的对象会被漏掉,导致数量偏小。这是路线 A 最常见的错误来源。
路线 B:逐对象遍历(精确,但慢)
逐个取出对象、读 getType() 再累加。这条路线的结果最精确,FormXObject 也能直接得到真实值而非推算。
async function typeBreakdownSlow(page, total) {
const c = PDFViewCtrl.PDF.constant.Graphics_ObjectType;
const nameOf = {};
for (const k in c) {
if (typeof c[k] === 'number') nameOf[c[k]] = k;
}
const result = { Text: 0, Path: 0, Image: 0, Shading: 0, FormXObject: 0 };
for (let i = 0; i < total; i++) {
const obj = await page.getGraphicsObjectByIndex(i);
if (!obj) continue;
const name = nameOf[obj.getType()] || ('Unknown(' + obj.getType() + ')');
result[name] = (result[name] || 0) + 1;
}
return result;
}
单个对象的取值是一次跨线程的异步调用,total 为多少就要等多少次。单页 3000 个对象时,这条路线的耗时通常在分钟级。它适合页数不多、或只对少数几页要求精确值的场景。
两条路线的取舍
| 对比项 |
路线 A(矩形查询) |
路线 B(逐对象) |
| 每页调用次数 |
固定 5 次 |
等于该页对象数 |
| 单页 3000 对象的量级 |
数十毫秒 |
分钟级 |
| Shading |
支持 |
支持 |
| FormXObject |
不支持,只能按差值推算 |
支持,结果为真实值 |
| 结果精度 |
四类精确,FormXObject 为推算值 |
全部精确 |
| 适用场景 |
大文档全量统计、报表、抽样 |
少量页面、需精确 FormXObject |
一个实用做法是先用路线 A 跑全文档,同时挑对象最少的一页用路线 B 交叉校验,确认两者的四类结果一致后再采信路线 A 的全量数据。如果某页的 FormXObject 推算值为负,说明路线 A 的其余四类查询有遗漏,通常是 isInRect 用了默认值或 rect 取错了框。
四、性能与正确性注意事项
串行遍历,不要一次性并发。 按页串行访问符合 SDK 文档模型的使用方式。把几十页的 getPageByIndex 全部塞进 Promise.all 并不会更快,内部仍要排队,反而更容易触发资源竞争。需要并发时建议控制在 3 到 5 页以内,并实测确认。
统计不需要先渲染页面。 getPageByIndex 配合 getGraphicsObjectsCount 走的是文档模型,与视图无关。不必等页面渲染完成,也不必跳转到目标页。
统计范围要分清。 这里统计的是 PDF 内容流中的图形对象,包含上述五类。它不等于 PDF 编辑器的”内容”面板所展示的条目数——后者按内容流操作符层级组织,两者计算范围不同,不能直接对比。表单域(Widget)属于注释体系,不包含在图形对象里,需要另外用 getAnnotCountAsync() 统计;从 11.1.0 起注释采用懒加载,取总数请用异步版本而非 getAnnotCount()。
长任务要提供进度反馈。 用路线 B 处理多页文档时,务必把进度刷新放在最内层循环内部,并配一个独立于业务逻辑的定时器更新已用时间。只在”整页处理完”后刷新是不够的:某一页对象特别多时,内层长时间不返回,界面会停在上一句提示上,看起来与卡死无异。同时建议在控制台打印每页耗时,便于定位是哪一页拖慢了整体。
getBitmap 不要在统计循环里调用。 它真正触发渲染,开销远高于取类型。
五、常见问题
1. 统计结果和 PDF 编辑器”内容”面板的条目数不一致
两者计算范围不同。本文统计的是内容流中的图形对象,按 Text、Path、Image、Shading、FormXObject 五类划分;编辑器的内容面板展示的是内容流操作符层级。Form XObject 内部的对象是否展开为独立对象,也取决于该 Form 是否被作为独立对象返回。做数量比对时请用同一套接口,不要跨工具比较。
2. 按矩形查询得到的数量小于全页总数
先检查 isInRect 是否用了默认值。默认 true 只返回完全位于矩形内的对象,跨越边界的对象会被排除。整页统计应显式传 isInRect: false。其次确认 rect 取自 cropBox 而非其他框——getAllBoxes() 返回 mediaBox、cropBox、trimBox、artBox、bleedBox、calcBox 六个矩形及最小宽高,页面可见区域对应的是 cropBox。
3. FormXObject 的推算值是负数
说明其余四类的查询结果之和超过了总数,即四类中有遗漏或重复。常见原因是 isInRect 用了默认值,或 rect 传入了错误的框(例如误用 trimBox 而文档实际以 cropBox 显示)。修正 rect 与 isInRect 后重算;如仍有异常,对该页改用路线 B 取真实值。
4. 逐对象统计在长文档上运行很久,控制台也没有输出
这通常是路线选择问题,而非代码缺陷。逐对象统计的调用次数等于总对象数,图纸类文档单页可达数千个对象。判断某一页的预期耗时,可以只取该页前 50 个对象计时后按比例推算:
async function probeSlowPage(page, total, sample) {
const n = Math.min(sample, total);
const t0 = performance.now();
for (let i = 0; i < n; i++) {
await page.getGraphicsObjectByIndex(i);
}
const dt = performance.now() - t0;
return { objects: total, sample: n, sampleMs: dt, estimateSec: (dt / n * total) / 1000 };
}
推算结果显示该页需要数十秒以上时,改用路线 A。
5. 在控制台执行统计脚本后没有任何反应
按以下顺序排查。
| 现象 |
原因 |
处理 |
只回显函数定义或 undefined,无表格输出 |
脚本只定义了函数,尚未调用 |
显式调用该函数并 await |
| 无输出且页面无变化 |
使用了逐对象统计,长文档耗时过长 |
先只统计总数,或改用路线 A |
报 xxx is not a function 或读到 null |
在 pdfui 上直接调了 getCurrentPDFDoc(),拿到的是 Promise |
先 await pdfui.getPDFViewer() 再取文档 |
报 Identifier 'xxx' has already been declared |
同一段含 const/let 的代码粘贴了第二次 |
刷新页面,或用立即执行函数包裹 |
| 无表格但无报错 |
控制台过滤级别设为 Errors 或 Warnings,console.table 属 Info 级 |
切回全部级别 |
6. 只想统计某个区域内的对象数量
用 getGraphicsObjectsByRect 指定 rect 为该区域,取返回数组长度。若使用 7.4.0 至 8.4.0 之间提供的 getGraphicsObjectsInRect(rect, tolerance, type),注意该接口同时不支持 Shading 与 FormXObject,覆盖面比 getGraphicsObjectsByRect 更窄。
六、参考
- 接口签名与返回值:Foxit PDF SDK for Web API Reference 中的
PDFPage、GraphicsObject、Constants 类目,见 https://devdocs.fuxinsoft.cn/resources/pdf-sdk-web/api_reference/html/index.html
- 矩形结构
PDFRect 的字段定义与坐标系约定,见上述 API Reference 中 PDFRect 的说明