likes
comments
collection
share

如何将pdf的签章变成黑色脱密

作者站长头像
站长
· 阅读数 10

前言

事情是这样的,前段时间同事接到一个需求,需要将项目系统的签章正文脱密下载。不经意间听到同事嘀咕找不到头绪,网上的相关资料也很少,于是帮忙研究研究。

实现的思路:

首先,我们必须要明白一个PDF中存在哪些东西?PDF可以存储各种类型的内容,包括文本、图片、图形、表格、注释、标记和多媒体元素。那么印章在我们的PDF中其实就是存储的一个图片,然后这个图片附加的有印章信息,可用于文件的有效性验证,说白了其实就是一种【特殊的图片】,那么我们需要做的就是如何找到这个图片并如何将这个图片变成黑色最后插入到pdf的原始位置。下面我们就分析一下其处理的过程。

准备工作

我们使用apache 提供的 pdfbox用来处理和操作。

<dependency>
  <groupId>org.apache.pdfbox</groupId>
  <artifactId>pdfbox</artifactId>
  <version>2.0.24</version>
</dependency>

过程分析

查找印章定义

印章定义通常存储在 PDF 的资源文件中,例如字体、图像等。因此,我们需要找到印章定义所对应的 PDAnnotation(签名列表)。不同厂商对 签名信息 的标识可能不同,因此我们需要查找 PDF 文件中的 PDAnnotation。在这一步中,我们需要使用一些调试技巧和定向猜测,通过debug的模式我们去找或者猜测一下厂商的印章签名是什么,比如金格的就是:GoldGrid:AddSeal 。这个签名就带了金格的厂商名。

  • 首先是加载文档:PDDocument document = PDDocument.load(new File("test.pdf"));
  • 其次是遍历文档,查找每一个页中是否含有印章签名信息
List<PDAnnotation> annotations = page.getAnnotations();
for (PDAnnotation annotation : annotations) {
    if (KG_SIGN.equals(annotation.getSubtype()) || NTKO_SIGN.equals(annotation.getSubtype())) {
        // todo 
    }
}

上诉步骤我们就完成了查询信息的全过程,接下来我们需要获取印章图片信息。

获取印章流

一旦我们找到了印章定义所对应的 PDAnnotation,我们就可以获取到印章图片信息中相关的附加信息,比如印章的位置信息,字体,文字等等信息。

PDRectangle rectangle = annotation.getRectangle();
float width = rectangle.getWidth();
float height = rectangle.getHeight();

上诉代码我们获取了印章图片的大小信息,用于后续我们填充印章时的文件信息。PDRectangle 对象定义了矩形区域的左下角坐标、宽度和高度等属性。

PDAppearanceDictionary appearanceDictionary = annotation.getAppearance();
PDAppearanceEntry normalAppearance = appearanceDictionary.getNormalAppearance();
PDAppearanceStream appearanceStream = normalAppearance.getAppearanceStream();
PDResources resources = appearanceStream.getResources();
PDImageXObject xObject = (PDImageXObject)resources.getXObject(xObjectName);

那么上面代码就是我们获取到的原始图片对象信息。通过对PDImageXObject进行操作以完成我们的目的。

PDResources 资源对象包含了注释所需的所有资源,例如字体、图像等。可以使用资源对象进行进一步的操作,例如替换资源、添加新资源等。

在PDF文件中,图像通常被保存为一个XObject对象,该对象包含了图像的信息,例如像素数据、颜色空间、压缩方式等。对于一个PDF文档中的图像对象,通常需要从资源(Resources)对象中获取。

处理原始图片

一旦我们找到了印章图片对象,我们需要将其变成黑色。印章通常是红色的,因此我们可以遍历图像的像素,并将红色像素点变成黑色像素点。在这一步中,我们需要使用一些图像处理技术,例如使用 Java 的 BufferedImage 类来访问和修改图像的像素。

public static void replaceRed2Black(BufferedImage image) {
        int width = image.getWidth();
        int height = image.getHeight();
        // 获取图片的像素信息
        int[] pixels = image.getRGB(0, 0, width, height, null, 0, width);
        // 循环遍历每一个像素点
        for (int i = 0; i < pixels.length; i++) {
            // 获取当前像素点的颜色
            Color color = new Color(pixels[i]);
            // 如果当前像素点的颜色为白色 rgb(255, 255, 255),颜色不变
            if (color.getRed() == 255 && color.getGreen() == 255 && color.getBlue() == 255) {
                pixels[i] &= 0x00FFFFFF;
            }else{
                // 其他颜色设置为黑色 :rgb(0, 0, 0)
                pixels[i] &= 0xFF000000;
            }
        }
        image.setRGB(0, 0, width, height, pixels, 0, width);
    }

代码逻辑:首先获取图片的宽高信息,然后获取图片的像素信息,循环每一个像素,然后判断像素的颜色是什么色,如果不是白色那么就将颜色替换为黑色。

tips:这里其实有个小插曲,当时做的时候判断条件是如果为红色则将其变换为黑色,但是这里有个问题就是在红色边缘的时候,其颜色的rgb数字是一个区间,这样去替换的话,图片里面就会存在模糊和替换不全。所以后来灵光一现,改成现在这样。

插入处理后的图片

最后,我们需要将新的印章图像插入到 PDF 文件中原始印章的位置上,代码如下:

PDAppearanceStream newAppearanceStream = new PDAppearanceStream(appearanceStream.getCOSObject());
PDAppearanceContentStream newContentStream = new PDAppearanceContentStream(newAppearanceStream);
newContentStream.addRect(0, 0, width, height);
File file = new File("image.png");
PDImageXObject image = PDImageXObject.createFromFileByContent(file, document);
// 在内容流中绘制图片
newContentStream.drawImage(image, 0, 0, width, height);
// 关闭外观流对象和内容流对象
newContentStream.close();

这段代码是在Java语言中使用PDFBox库操作PDF文件时,创建一个新的外观流(Appearance Stream)对象,并在该流中绘制一张图片。

首先,通过调用PDAppearanceStream类的构造方法,创建一个新的外观流对象,并将其初始化为与原有外观流对象相同的COS对象。这里使用appearanceStream.getCOSObject()方法获取原有外观流对象的COS对象。然后,创建一个新的内容流(AppearanceContent Stream)对象,将其与新的外观流对象关联起来。

接下来,使用addRect()方法向内容流中添加一个矩形,其左下角坐标为(0,0),宽度为width,高度为height。该操作用于确定图片在外观流中的位置和大小。

然后,通过PDImageXObject类中的createFromFileByContent()方法创建一个PDImageXObject对象,该对象表示从文件中读取的图片。这里使用一个File对象和PDF文档对象document作为参数创建PDImageXObject对象。

接下来,使用drawImage()方法将读取的图片绘制到内容流中。该方法以PDImageXObject对象、x坐标、y坐标、宽度、高度作为参数,用于将指定的图片绘制到内容流中的指定位置。

最后,通过调用close()方法关闭内容流对象,从而生成一个完整的外观流对象。

到此我们就完成了印章的脱密下载的全过程,这个任务的难点在于怎么查找不同厂商对印章的签名定义以及对pdf的理解和工具API的理解。