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

基于Node.js实现PDF文本替换的Web表单开发问题及代码求助

PDF文本替换Web工具实现

本文实现了一个简易Web工具,支持上传包含图片等元素的PDF文件,将指定文本批量替换为新内容,处理完成后可下载修改后的PDF。该方案解决了其他PDF库仅能新增文本无法替换原有内容的问题,核心采用Hummus库实现PDF内容修改。


前端HTML页面代码

<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PDF文本替换工具</title>
</head>
<body>
    <h1>PDF上传与文本替换</h1>
    <form id="uploadForm" enctype="multipart/form-data">
        <input type="file" name="pdfFile" accept=".pdf" required>
        <button type="submit">上传并处理PDF</button>
    </form>

    <script>
        document.getElementById('uploadForm').addEventListener('submit', async (event) => {
            event.preventDefault();

            const formData = new FormData(event.target);

            try {
                const response = await fetch('/upload-pdf', {
                    method: 'POST',
                    body: formData
                });

                if (!response.ok) {
                    throw new Error('请求失败:' + response.statusText);
                }

                const result = await response.json();
                alert(result.message);
                // 自动跳转至下载页面
                window.location.href = '/download-output';
            } catch (error) {
                console.error('错误信息:', error);
                alert('文件处理失败:' + error.message);
            }
        });
    </script>
</body>
</html>

后端Node.js代码

const express = require('express');
const app = express();
const port = 3000;
const path = require('path'); // 引入path模块
const hummus = require('hummus'); // 引入HummusJS库
const fs = require('fs');
const multer = require('multer');

// 根路由返回前端页面
app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

// 配置Multer处理文件上传
const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        // 自动创建uploads目录(不存在时)
        if (!fs.existsSync('uploads/')) {
            fs.mkdirSync('uploads/');
        }
        cb(null, 'uploads/');
    },
    filename: function (req, file, cb) {
        cb(null, file.originalname); // 保留原文件名存储
    }
});

const upload = multer({ storage: storage });

/**
 * 修改PDF中的指定文本
 * @param {string} sourceFilePath 源PDF路径
 * @param {string} findText 需替换的目标文本
 * @param {string} replaceText 替换后的新文本
 */
function modifyExistingPDF(sourceFilePath, findText, replaceText) {
    // 检查源文件是否存在
    if (!fs.existsSync(sourceFilePath)) {
        throw new Error(`源文件 ${sourceFilePath} 不存在。`);
    }

    // 创建PDF修改器,生成临时输出文件
    const modPdfWriter = hummus.createWriterToModify(sourceFilePath, { modifiedFilePath: './output.pdf', compress: false });

    // 获取PDF解析上下文
    const sourceParser = modPdfWriter.createPDFCopyingContextForModifiedFile().getSourceDocumentParser();

    // 遍历处理所有页面
    const pageCount = sourceParser.getPagesCount();
    for (let pageNum = 0; pageNum < pageCount; pageNum++) {
        const pageObject = sourceParser.parsePage(pageNum);
        
        // 获取页面内容流
        const contentsStream = sourceParser.queryDictionaryObject(pageObject.getDictionary(), 'Contents');
        if (!contentsStream) {
            console.warn(`第${pageNum+1}页未找到内容流,跳过处理`);
            continue;
        }

        // 读取内容流数据
        const data = [];
        const readStream = sourceParser.startReadingFromStream(contentsStream);
        while (readStream.notEnded()) {
            const readData = readStream.read(10000);
            data.push(...readData);
        }

        // 转换为字符串并替换文本
        let contentStr = Buffer.from(data).toString('utf-8');
        if (contentStr.includes(findText)) {
            contentStr = contentStr.replace(new RegExp(findText, 'g'), replaceText);
        } else {
            console.warn(`第${pageNum+1}页未找到文本 "${findText}"`);
        }

        // 写入修改后的内容流
        const objectsContext = modPdfWriter.getObjectsContext();
        const textObjectID = pageObject.getDictionary().toJSObject().Contents.getObjectID();
        if (typeof textObjectID !== 'number') {
            throw new Error(`第${pageNum+1}页文本对象ID无效`);
        }

        objectsContext.startModifiedIndirectObject(textObjectID);
        const stream = objectsContext.startUnfilteredPDFStream();
        stream.getWriteStream().write(Buffer.from(contentStr, 'utf-8'));
        objectsContext.endPDFStream(stream);
        objectsContext.endIndirectObject();
    }

    // 完成PDF写入
    modPdfWriter.end();

    // 清理无效对象,生成最终可下载文件
    hummus.recrypt('./output.pdf', './outputClean.pdf');
    // 删除临时文件
    fs.unlinkSync('./output.pdf');
}

// 文件上传与处理接口
app.post('/upload-pdf', upload.single('pdfFile'), (req, res) => {
    console.log('文件正在上传至服务器...');

    if (!req.file) {
        return res.status(400).json({ error: '未上传文件' });
    }

    const sourceFilePath = req.file.path;
    const findText = 'Word'; // 需替换的目标文本,可改为前端传入参数
    const replaceText = 'Letter'; // 替换后的新文本,可改为前端传入参数

    try {
        modifyExistingPDF(sourceFilePath, findText, replaceText);
        // 删除上传的源文件
        fs.unlinkSync(sourceFilePath);
        res.json({ message: 'PDF处理完成,即将开始下载' });
    } catch (error) {
        console.error('PDF处理错误:', error);
        res.status(500).json({ error: 'PDF处理失败:' + error.message });
    }
});

// 下载处理后PDF的接口
app.get('/download-output', (req, res) => {
    const outputPath = './outputClean.pdf';
    if (fs.existsSync(outputPath)) {
        res.download(outputPath, '修改后的PDF.pdf', (err) => {
            if (err) {
                res.status(500).json({ error: '文件下载失败' });
            }
            // 下载完成后删除文件
            fs.unlinkSync(outputPath);
        });
    } else {
        res.status(404).json({ error: '未找到处理后的文件' });
    }
});

// 启动服务器
app.listen(port, () => {
    console.log(`服务器运行在 http://localhost:${port}`);
});

说明:

  1. 扩展了原代码的单页处理逻辑,支持遍历处理PDF所有页面
  2. 新增自动清理临时文件、源文件的逻辑,避免服务器存储冗余
  3. 可将findText和replaceText改为前端传入参数,提升工具灵活性

内容的提问来源于stack exchange,提问作者git abbuser

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 12:11:02