使用pdfjs-dist转换PDF表单时丢失复选框勾选标记的问题
问题背景
基于Stack Overflow相关内容改编了PDF转文本的Node.js代码,使用pdfjs-dist 3.1.81版本,实现代码如下:
import pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js'; import { TextItem, DocumentInitParameters, } from 'pdfjs-dist/types/src/display/api'; const getPageText = async (pdf: pdfjsLib.PDFDocumentProxy, pageNo: number) => { const page = await pdf.getPage(pageNo); const tokenizedText = await page.getTextContent(); var textItems = tokenizedText.items; var finalString = ''; var line = 0; // Concatenate the string of the item to the final string for (var i = 0; i < textItems.length; i++) { if (line != (textItems[i] as TextItem).transform[5]) { if (line != 0) { finalString += '\r\n'; } line = (textItems[i] as TextItem).transform[5]; } var item = textItems[i]; finalString += (item as TextItem).str; } return finalString; }; export const getPDFText = async ( data: string, password: string | undefined = undefined ) => { const initParams: DocumentInitParameters = { data: Buffer.from(data, 'base64'), //useSystemFonts: true, //disableFontFace: false, standardFontDataUrl: 'standard_fonts/' }; if (password !== undefined) { initParams.password = password; } const pdf = await pdfjsLib.getDocument(initParams).promise; const maxPages = pdf.numPages; const pageTextPromises = []; for (let pageNo = 1; pageNo <= maxPages; pageNo += 1) { pageTextPromises.push(getPageText(pdf, pageNo)); } const pageTexts = await Promise.all(pageTextPromises); const joined = pageTexts.join(' '); return joined; };
执行代码:
import { join } from 'path'; import { readFileSync } from 'fs'; const rawContents = readFileSync(join('directory', 'file.pdf'), 'base64'); const pdfText = await getPDFText(rawContents as string);
遇到的问题
转换效果整体尚可,但存在两个问题:
- 表单中的复选框勾选标记丢失
- 文本框的值出现在每页末尾而非对应问题的位置
对比同类型工具输出,其能保留复选框勾选标记,且文本框值位置正确。
问题解答
1. 复选框勾选标记丢失的原因
PDF里的复选框勾选标记通常不是以文本内容存储的,而是作为表单域的状态属性存在(比如/V属性为/Yes表示已勾选)。当前代码仅提取页面绘制的文本元素(getTextContent()的返回结果),完全未处理表单域的状态信息,因此无法获取勾选标记。
专业PDF转文本工具会额外解析PDF的表单域结构,读取复选框状态后,将对应标记(如[X]或✓)插入到文本对应位置。
2. 文本框值位置异常的原因
PDF中表单域(含文本框)的填写内容,很多时候独立于页面静态文本存储。getTextContent()仅提取页面静态文本,而表单的动态填写值可能存储在页面资源层,或仅在渲染时定位,导致提取时被归到页面末尾。要解决此问题,需主动解析PDF表单域,获取文本框的位置信息,再将值插入到对应位置的文本中。
3. 关于标准字体的问题
即使未收到警告,也建议将pdfjs-dist提供的全部标准字体复制到standard_fonts目录。部分特殊表单可能使用少见的标准字体,缺失字体可能导致文本渲染(提取)异常,比如某些符号(包括复选框默认标记)无法被正确识别为文本。
代码调整方向
要解决这两个问题,需在现有代码基础上增加表单域解析逻辑:
- 使用
pdf.getFieldObjects()获取所有表单域 - 遍历表单域,区分复选框和文本框:
- 复选框:判断
fieldType为'checkbox',若value为'Yes',则在对应位置插入勾选标记(如[X]) - 文本框:获取
value和位置信息(rect属性),将值插入到文本对应坐标位置
- 复选框:判断
调整后的核心逻辑示例:
// 在getPDFText函数中,获取PDF后解析表单域 const getPDFText = async (data: string, password?: string) => { // 原有的initParams和getDocument逻辑 const initParams: DocumentInitParameters = { data: Buffer.from(data, 'base64'), standardFontDataUrl: 'standard_fonts/' }; if (password) initParams.password = password; const pdf = await pdfjsLib.getDocument(initParams).promise; // 新增:获取表单域 const fieldObjects = await pdf.getFieldObjects(); // 原有的页面文本提取逻辑 const maxPages = pdf.numPages; const pageTextPromises = []; for (let pageNo = 1; pageNo <= maxPages; pageNo += 1) { pageTextPromises.push(getPageText(pdf, pageNo)); } const pageTexts = await Promise.all(pageTextPromises); // 处理表单域,将内容插入到对应位置的文本中 for (const fieldName in fieldObjects) { const field = fieldObjects[fieldName]; const pageNo = field.pageIndex + 1; // pageIndex从0开始,转换为页码 const pageText = pageTexts[pageNo - 1]; if (field.fieldType === 'checkbox' && field.value === 'Yes') { // 根据field.rect坐标,找到页面文本中对应位置插入[X] // 需实现坐标到文本位置的映射逻辑 pageTexts[pageNo - 1] = insertCheckboxMark(pageText, field.rect); } if (field.fieldType === 'text' && field.value) { // 根据field.rect坐标,将文本框值插入对应位置 pageTexts[pageNo - 1] = insertTextboxValue(pageText, field.rect, field.value); } } const joined = pageTexts.join('\n\n'); // 用换行分隔页面更合理 return joined; }; // 示例:坐标到文本位置的映射函数(需根据实际文本元素位置实现) function insertCheckboxMark(pageText: string, rect: number[]) { // 这里需要结合getPageText中每个文本元素的transform坐标,找到rect附近的文本位置 // 简化示例:直接在页面开头插入,实际需做精准匹配 return `[X] ${pageText}`; } function insertTextboxValue(pageText: string, rect: number[], value: string) { // 同理,需根据rect坐标定位插入位置 return pageText.replace(/(问题文本)/, `$1 ${value}`); }
注意:坐标到文本位置的映射需结合getPageText中每个文本元素的transform属性(包含位置信息)实现,对比表单域rect与文本元素位置,找到最接近的位置插入内容。
内容的提问来源于stack exchange,提问作者chrismead

