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

传入临时文件参数后无法获取PdfSignatureAppearance的rangeStream

问题:使用临时文件处理大PDF签名时,appearance.getRangeStream()抛出流已关闭异常

我在使用iText进行PDF签名开发时,遇到以下问题:

  • 当调用PdfStamper.createSignature时指定了临时文件参数(第四个参数,用于处理大文件签名),执行MakeSignature.signExternalContainer后调用appearance.getRangeStream()会抛出IOException,提示流已关闭。
  • 如果将临时文件参数设为null,就能正常获取该流,但临时文件是处理大PDF签名必需的,不能移除。

以下是我的代码片段:

os = new FileOutputStream(rutaDestinoArchivo);
PdfStamper stamper;
if (claveEncriptar != null && !claveEncriptar.isEmpty()) {
    stamper = PdfStamper.createSignature(reader, os, '\0', new File(rutaDestinoArchivo + ".tmp"), false);
    stamper.setEncryption(claveEncriptar.getBytes(),
            claveEncriptar.getBytes(),
            PdfWriter.ALLOW_PRINTING, PdfWriter.ENCRYPTION_AES_128 | PdfWriter.DO_NOT_ENCRYPT_METADATA);
} else {
    stamper = PdfStamper.createSignature(reader, os, '\0', new File(rutaDestinoArchivo + ".tmp"), true);
}
PdfSignatureAppearance appearance = stamper.getSignatureAppearance();

if (!seguirFirmando) {
    appearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
}

motivo = " " + motivo + " ";
if (motivo.length() > 0) {
    String[] parts = motivo.split("@motloc@");
    String part11 = parts[0].trim();
    String part21 = parts[1].trim();
    motivo = "";
    localidad = "";
    if (part11.length() > 0) {
        motivo = part11;
    }
    if (part21.length() > 0) {
        localidad = part21;
    }
} else {
    motivo = "";
    localidad = "";
}
appearance.setReason(motivo);
appearance.setLocation(localidad);

String ttitulo = tituloFirma;
if (ttitulo.length() == 0) {
    ttitulo = "FIRMA DIGITAL";
}
Integer ttamletra = tamanoLetra;
if (ttamletra > 0) {
    appearance.setLayer2Font(new Font(Font.FontFamily.UNDEFINED, ttamletra));
}
if (descripcion != null && !descripcion.trim().isEmpty()) {
    appearance.setLayer2Text(descripcion);
}
appearance.setLayer4Text(ttitulo);
appearance.setAcro6Layers(true);
appearance.setImageScale(0.5f);
appearance.setRunDirection(PdfWriter.RUN_DIRECTION_LTR);
if (firmaVisible) {
    DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
    Date date = new Date();
    this.nombreFirma = ((nombreFirma != null && !nombreFirma.trim().isEmpty()) ? nombreFirma : "sig" + dateFormat.format(date));
    if (rutaArchivoImagen == null || rutaArchivoImagen.isEmpty()) {
        appearance.setRenderingMode(RenderingMode.DESCRIPTION);
    } else {
        appearance.setRenderingMode(RenderingMode.GRAPHIC_AND_DESCRIPTION);
        Image watermark = Image.getInstance(rutaArchivoImagen);
        appearance.setSignatureGraphic(watermark);
    }
    appearance.setVisibleSignature(new Rectangle(x, y, x + ancho, y + alto), pagina, this.nombreFirma);
}
ExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE,
        PdfName.ADBE_PKCS7_DETACHED);
MakeSignature.signExternalContainer(appearance, external, tamanoFirma);

InputStream inp = appearance.getRangeStream(); // 此处抛出IOException:流已关闭

解决方案

问题根源

当使用临时文件参数时,MakeSignature.signExternalContainer执行完成后会自动关闭所有相关流(包括rangeStream),导致后续无法从appearance中获取该流。而不指定临时文件时,流会保留在内存中,因此可以正常获取。

解决步骤

核心思路是在签名容器的sign方法执行过程中获取并缓存rangeStream,而不是在方法执行完毕后从appearance中获取。

  1. 自定义ExternalSignatureContainer子类,用于缓存rangeStream
class CachedRangeSignatureContainer implements ExternalSignatureContainer {
    private final ExternalSignatureContainer delegate;
    private InputStream rangeStream;

    public CachedRangeSignatureContainer(ExternalSignatureContainer delegate) {
        this.delegate = delegate;
    }

    @Override
    public byte[] sign(InputStream data) throws GeneralSecurityException {
        // 缓存要签名的数据流(即rangeStream)
        this.rangeStream = data;
        // 委托给原容器处理签名逻辑
        return delegate.sign(data);
    }

    // 提供获取缓存流的方法
    public InputStream getRangeStream() {
        return rangeStream;
    }
}
  1. 修改原代码中的签名容器调用逻辑
    将原来的ExternalBlankSignatureContainer替换为自定义的缓存容器,并从自定义容器中获取rangeStream:
// 替换原签名容器创建逻辑
CachedRangeSignatureContainer cachedContainer = new CachedRangeSignatureContainer(
        new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED)
);
MakeSignature.signExternalContainer(appearance, cachedContainer, tamanoFirma);

// 从自定义容器中获取rangeStream,而非appearance
InputStream inp = cachedContainer.getRangeStream();
  1. 优化建议
    如果你的需求是计算PDF的哈希值,可以直接在sign方法中对data流进行哈希计算,无需缓存流,这样能避免额外的资源占用:
class HashCalculatingSignatureContainer implements ExternalSignatureContainer {
    private final ExternalSignatureContainer delegate;
    private byte[] documentHash;

    public HashCalculatingSignatureContainer(ExternalSignatureContainer delegate) {
        this.delegate = delegate;
    }

    @Override
    public byte[] sign(InputStream data) throws GeneralSecurityException {
        try {
            // 计算SHA-256哈希(可根据需求替换算法)
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] buffer = new byte[8192];
            int read;
            while ((read = data.read(buffer)) != -1) {
                digest.update(buffer, 0, read);
            }
            documentHash = digest.digest();
            // 重置流,确保原签名容器能正常读取
            data.reset();
        } catch (IOException e) {
            throw new GeneralSecurityException("Failed to calculate document hash", e);
        }
        return delegate.sign(data);
    }

    public byte[] getDocumentHash() {
        return documentHash;
    }
}

使用时直接获取哈希值即可:

HashCalculatingSignatureContainer hashContainer = new HashCalculatingSignatureContainer(
        new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED)
);
MakeSignature.signExternalContainer(appearance, hashContainer, tamanoFirma);

byte[] pdfHash = hashContainer.getDocumentHash();

内容的提问来源于stack exchange,提问作者Cesar Palomino Salas

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 07:35:20