You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用pdfjs-dist转换PDF表单时丢失复选框勾选标记的问题

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 10:25:15