【问题标题】:PDFBox: Remove a single field from PDFPDFBox:从 ​​PDF 中删除单个字段
【发布时间】:2016-12-10 01:08:44
【问题描述】:

我可以描述该问题的最简单方法是,我们使用 PDFbox 从 HelloSign 发送给我们的 PDF 中仅删除一个字段。 (例如信用卡号)

  1. 相关数据将始终位于最后一页,并且始终位于页面中的相同坐标处。
  2. 需要从 PDF 中完全删除数据。我们不能简单地将字体更改为白色或在顶部画一个框,因为它仍然可以选择,因此可以复制。
  3. 只能删除一个字段。我们仍然需要其他字段和签名。
  4. 我创建了一个示例文档并将其上传到 Dropbox。 input.pdf
  5. 为了这个问题,我们假设要删除的字段是我上传的文件中的街道地址。不是城市、州、邮编、签名或日期。 (在现实生活中,这将是一个敏感数据字段,例如信用卡号或 SSN。)

我在下面的第一条评论中对问题以及到目前为止我尝试过的内容进行了冗长的解释。

【问题讨论】:

  • 我们有生成的 PDF 文件并通过 HelloSign 发送给客户以供签名和填写数据(地址、电话等)。在每个人都签名后,我们将本地副本存储在我们的服务器上。我们想开始在末尾附上信用卡授权表以加快付款速度。但是,出于合规性原因,我们不希望信用卡号以纯文本形式存储在我们的服务器上,因此我们希望使用 PDFBox 从 PDF 中删除 3 个字段。数据/字段将始终位于最后一页,并且始终位于该页面中的相同坐标。
  • 首先,我尝试在该位置上绘制图像或框。只有这样的作品。没关系,您在屏幕上或打印时看不到它,但您仍然可以选择并复制它。接下来,我尝试搜索和替换文本,我知道这不是微不足道的,但我在任何地方都找不到。然后我从 svn 的示例目录中尝试了RemoveAllText.java,这让我意识到数据不是以文本形式存储的。
  • 我继续修改 RemoveAllText.java 以显示文件中的所有标记并开始一一删除它们,直到找到可以删除它的标记。它是一个“Do”令牌,但删除它会删除所有数据以及所有签名。没有布埃诺。多一点调查告诉我,我正在使用的对象是 org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject。但是,我似乎无法找到一种方法来仅删除此对象中的 3 个字段而不删除所有这些字段。
  • 在我发现它是一个 PDFormXObject 之后,我想我会尝试PrintFields.java 示例,但该文档似乎没有 AcroForm。
  • “我在下面的第一条评论中对问题和迄今为止我尝试过的问题进行了冗长的解释。”请不要。所有相关信息都应该在您的问题中(注意“相关”,这与建议的“冗长”相反)。

标签: java pdf pdfbox


【解决方案1】:

这个答案中的代码可能看起来有点通用,因为它首先确定文档中的字段映射,然后允许删除文本字段的任何组合。但请注意,它仅使用此问题中的单个示例 PDF 进行开发。因此,我无法确定我是否正确理解了为/由 HelloSign 标记字段的方式,尤其是 HelloSign 填充这些字段的方式。

这个答案提供了两个类,一个分析 HelloSign 表单,另一个通过清除选定字段来操作它;后者依赖于前者收集的信息。这两个类都建立在 PDFBox PDFTextStripper 实用程序类之上。

该代码已针对当前 PDFBox 开发版本 2.1.0-SNAPSHOT 开发。很可能它也适用于所有 2.0.x 版本。

HelloSignAnalyzer

这个类分析给定的PDDocument 寻找序列

  • [$varname ] 似乎定义了用于放置表单字段内容的占位符,并且
  • [def:$varname|type|req|signer|display|label] 似乎定义了占位符的属性。

它创建了一个HelloSignField 实例的集合,每个实例都描述了这样一个占位符。如果可以在占位符上方找到文本,它们还包含相应字段的值。

此外,它还存储在页面上绘制的最后一个 xobject 的名称,在示例文档的情况下,它是 HelloSign 绘制其字段内容的位置。

public class HelloSignAnalyzer extends PDFTextStripper
{
    public class HelloSignField
    {
        public String getName()
        { return name; }
        public String getValue()
        { return value; }
        public float getX()
        { return x; }
        public float getY()
        { return y; }
        public float getWidth()
        { return width; }
        public String getType()
        { return type; }
        public boolean isOptional()
        { return optional; }
        public String getSigner()
        { return signer; }
        public String getDisplay()
        { return display; }
        public String getLabel()
        { return label; }
        public float getLastX()
        { return lastX; }

        String name = null;
        String value = "";
        float x = 0, y = 0, width = 0;
        String type = null;
        boolean optional = false;
        String signer = null;
        String display = null;
        String label = null;

        float lastX = 0;

        @Override
        public String toString()
        {
            return String.format("[Name: '%s'; Value: `%s` Position: %s, %s; Width: %s; Type: '%s'; Optional: %s; Signer: '%s'; Display: '%s', Label: '%s']",
                    name, value, x, y, width, type, optional, signer, display, label);
        }

        void checkForValue(List<TextPosition> textPositions)
        {
            for (TextPosition textPosition : textPositions)
            {
                if (inField(textPosition))
                {
                    float textX = textPosition.getTextMatrix().getTranslateX();
                    if (textX > lastX + textPosition.getWidthOfSpace() / 2 && value.length() > 0)
                        value += " ";
                    value += textPosition.getUnicode();
                    lastX = textX + textPosition.getWidth();
                }
            }
        }

        boolean inField(TextPosition textPosition)
        {
            float yPos = textPosition.getTextMatrix().getTranslateY();
            float xPos = textPosition.getTextMatrix().getTranslateX();

            return inField(xPos, yPos);
        }

        boolean inField(float xPos, float yPos)
        {
            if (yPos < y - 3 || yPos > y + 3)
                return false;

            if (xPos < x - 1 || xPos > x + width + 1)
                return false;

            return true;
        }
    }

    public HelloSignAnalyzer(PDDocument pdDocument) throws IOException
    {
        super();
        this.pdDocument = pdDocument;
    }

    public Map<String, HelloSignField> analyze() throws IOException
    {
        if (!analyzed)
        {
            fields = new HashMap<>();

            setStartPage(pdDocument.getNumberOfPages());
            getText(pdDocument);

            analyzed = true;
        }
        return Collections.unmodifiableMap(fields);
    }

    public String getLastFormName()
    {
        return lastFormName;
    }

    //
    // PDFTextStripper overrides
    //
    @Override
    protected void writeString(String text, List<TextPosition> textPositions) throws IOException
    {
        {
            for (HelloSignField field : fields.values())
            {
                field.checkForValue(textPositions);
            }
        }

        int position = -1;
        while ((position = text.indexOf('[', position + 1)) >= 0)
        {
            int endPosition = text.indexOf(']', position);
            if (endPosition < 0)
                continue;
            if (endPosition > position + 1 && text.charAt(position + 1) == '$')
            {
                String fieldName = text.substring(position + 2, endPosition);
                int spacePosition = fieldName.indexOf(' ');
                if (spacePosition >= 0)
                    fieldName = fieldName.substring(0, spacePosition);
                HelloSignField field = getOrCreateField(fieldName);

                TextPosition start = textPositions.get(position);
                field.x = start.getTextMatrix().getTranslateX();
                field.y = start.getTextMatrix().getTranslateY();
                TextPosition end = textPositions.get(endPosition);
                field.width = end.getTextMatrix().getTranslateX() + end.getWidth() - field.x;
            }
            else if (endPosition > position + 5 && "def:$".equals(text.substring(position + 1, position + 6)))
            {
                String definition = text.substring(position + 6, endPosition);
                String[] pieces = definition.split("\\|");
                if (pieces.length == 0)
                    continue;
                HelloSignField field = getOrCreateField(pieces[0]);

                if (pieces.length > 1)
                    field.type = pieces[1];
                if (pieces.length > 2)
                    field.optional = !"req".equals(pieces[2]);
                if (pieces.length > 3)
                    field.signer = pieces[3];
                if (pieces.length > 4)
                    field.display = pieces[4];
                if (pieces.length > 5)
                    field.label = pieces[5];
            }
        }

        super.writeString(text, textPositions);
    }

    @Override
    protected void processOperator(Operator operator, List<COSBase> operands) throws IOException
    {
        String currentFormName = formName; 
        if (operator != null && "Do".equals(operator.getName()) && operands != null && operands.size() > 0)
        {
            COSBase base0 = operands.get(0);
            if (base0 instanceof COSName)
            {
                formName = ((COSName)base0).getName();
                if (currentFormName == null)
                    lastFormName = formName;
            }
        }
        try
        {
            super.processOperator(operator, operands);
        }
        finally
        {
            formName = currentFormName;
        }
    }

    //
    // helper methods
    //
    HelloSignField getOrCreateField(String name)
    {
        HelloSignField field = fields.get(name);
        if (field == null)
        {
            field = new HelloSignField();
            field.name = name;
            fields.put(name, field);
        }
        return field;
    }

    //
    // inner member variables
    //
    final PDDocument pdDocument;
    boolean analyzed = false;
    Map<String, HelloSignField> fields = null;
    String formName = null;
    String lastFormName = null;
}

(HelloSignAnalyzer.java)

用法

可以将HelloSignAnalyzer 应用于文档,如下所示:

PDDocument pdDocument = PDDocument.load(...);

HelloSignAnalyzer helloSignAnalyzer = new HelloSignAnalyzer(pdDocument);

Map<String, HelloSignField> fields = helloSignAnalyzer.analyze();

System.out.printf("Found %s fields:\n\n", fields.size());

for (Map.Entry<String, HelloSignField> entry : fields.entrySet())
{
    System.out.printf("%s -> %s\n", entry.getKey(), entry.getValue());
}

System.out.printf("\nLast form name: %s\n", helloSignAnalyzer.getLastFormName());

(PlayWithHelloSign.java测试方法testAnalyzeInput)

如果是 OP 的示例文档,则输出为

Found 8 fields:

var1001 -> [Name: 'var1001'; Value: `123 Main St.` Position: 90.0, 580.0; Width: 165.53601; Type: 'text'; Optional: false; Signer: 'signer1'; Display: 'Address', Label: 'address1']
var1004 -> [Name: 'var1004'; Value: `12345` Position: 210.0, 564.0; Width: 45.53601; Type: 'text'; Optional: false; Signer: 'signer1'; Display: 'Postal Code', Label: 'zip']
var1002 -> [Name: 'var1002'; Value: `TestCity` Position: 90.0, 564.0; Width: 65.53601; Type: 'text'; Optional: false; Signer: 'signer1'; Display: 'City', Label: 'city']
var1003 -> [Name: 'var1003'; Value: `AA` Position: 161.0, 564.0; Width: 45.53601; Type: 'text'; Optional: false; Signer: 'signer1'; Display: 'State', Label: 'state']
date2 -> [Name: 'date2'; Value: `2016/12/09` Position: 397.0, 407.0; Width: 124.63202; Type: 'date'; Optional: false; Signer: 'signer2'; Display: 'null', Label: 'null']
signature1 -> [Name: 'signature1'; Value: `` Position: 88.0, 489.0; Width: 236.624; Type: 'sig'; Optional: false; Signer: 'signer1'; Display: 'null', Label: 'null']
date1 -> [Name: 'date1'; Value: `2016/12/09` Position: 397.0, 489.0; Width: 124.63202; Type: 'date'; Optional: false; Signer: 'signer1'; Display: 'null', Label: 'null']
signature2 -> [Name: 'signature2'; Value: `` Position: 88.0, 407.0; Width: 236.624; Type: 'sig'; Optional: false; Signer: 'signer2'; Display: 'null', Label: 'null']

Last form name: Xi0

HelloSignManipulator

此类利用HelloSignAnalyzer 收集的信息来清除由其名称给出的文本字段的内容。

public class HelloSignManipulator extends PDFTextStripper
{
    public HelloSignManipulator(HelloSignAnalyzer helloSignAnalyzer) throws IOException
    {
        super();
        this.helloSignAnalyzer = helloSignAnalyzer;
        addOperator(new SelectiveDrawObject());
    }

    public void clearFields(Iterable<String> fieldNames) throws IOException
    {
        try
        {
            Map<String, HelloSignField> fieldMap = helloSignAnalyzer.analyze();
            List<HelloSignField> selectedFields = new ArrayList<>();
            for (String fieldName : fieldNames)
            {
                selectedFields.add(fieldMap.get(fieldName));
            }
            fields = selectedFields;

            PDDocument pdDocument = helloSignAnalyzer.pdDocument;
            setStartPage(pdDocument.getNumberOfPages());
            getText(pdDocument);
        }
        finally
        {
            fields = null;
        }
    }

    class SelectiveDrawObject extends OperatorProcessor
    {
        @Override
        public void process(Operator operator, List<COSBase> arguments) throws IOException
        {
            if (arguments.size() < 1)
            {
                throw new MissingOperandException(operator, arguments);
            }
            COSBase base0 = arguments.get(0);
            if (!(base0 instanceof COSName))
            {
                return;
            }
            COSName name = (COSName) base0;

            if (replacement != null || !helloSignAnalyzer.getLastFormName().equals(name.getName()))
            {
                return;
            }

            if (context.getResources().isImageXObject(name))
            {
                throw new IllegalArgumentException("The form xobject to edit turned out to be an image.");
            }

            PDXObject xobject = context.getResources().getXObject(name);

            if (xobject instanceof PDTransparencyGroup)
            {
                throw new IllegalArgumentException("The form xobject to edit turned out to be a transparency group.");
            }
            else if (xobject instanceof PDFormXObject)
            {
                PDFormXObject form = (PDFormXObject) xobject;
                PDFormXObject formReplacement = new PDFormXObject(helloSignAnalyzer.pdDocument);
                formReplacement.setBBox(form.getBBox());
                formReplacement.setFormType(form.getFormType());
                formReplacement.setMatrix(form.getMatrix().createAffineTransform());
                formReplacement.setResources(form.getResources());
                OutputStream outputStream = formReplacement.getContentStream().createOutputStream(COSName.FLATE_DECODE);
                replacement = new ContentStreamWriter(outputStream);

                context.showForm(form);

                outputStream.close();
                getResources().put(name, formReplacement);
                replacement = null;
            }
        }

        @Override
        public String getName()
        {
            return "Do";
        }
    }

    //
    // PDFTextStripper overrides
    //
    @Override
    protected void processOperator(Operator operator, List<COSBase> operands) throws IOException
    {
        if (replacement != null)
        {
            boolean copy = true;
            if (TjTJ.contains(operator.getName()))
            {
                Matrix transformation = getTextMatrix().multiply(getGraphicsState().getCurrentTransformationMatrix());
                float xPos = transformation.getTranslateX();
                float yPos = transformation.getTranslateY();
                for (HelloSignField field : fields)
                {
                    if (field.inField(xPos, yPos))
                    {
                        copy = false;
                    }
                }
            }

            if (copy)
            {
                replacement.writeTokens(operands);
                replacement.writeToken(operator);
            }
        }
        super.processOperator(operator, operands);
    }

    //
    // helper methods
    //
    final HelloSignAnalyzer helloSignAnalyzer;
    final Collection<String> TjTJ = Arrays.asList("Tj", "TJ");
    Iterable<HelloSignField> fields;
    ContentStreamWriter replacement = null;
}

(HelloSignManipulator.java)

用法:清除单个字段

可以将HelloSignManipulator 应用于文档,如下所示清除单个字段:

PDDocument pdDocument = PDDocument.load(...);

HelloSignAnalyzer helloSignAnalyzer = new HelloSignAnalyzer(pdDocument);

HelloSignManipulator helloSignManipulator = new HelloSignManipulator(helloSignAnalyzer);

helloSignManipulator.clearFields(Collections.singleton("var1001"));

pdDocument.save(...);

(PlayWithHelloSign.java测试方法testClearAddress1Input)

用法:一次清除多个字段

您可以将HelloSignManipulator 应用到文档中,以一次清除多个字段:

PDDocument pdDocument = PDDocument.load(...);

HelloSignAnalyzer helloSignAnalyzer = new HelloSignAnalyzer(pdDocument);

HelloSignManipulator helloSignManipulator = new HelloSignManipulator(helloSignAnalyzer);

helloSignManipulator.clearFields(Arrays.asList("var1004", "var1003", "date2"));

pdDocument.save(...);

(PlayWithHelloSign.java测试方法testClearZipStateDate2Input)

用法:依次清除多个字段

可以将HelloSignManipulator应用到一个文档中,依次清除多个字段:

PDDocument pdDocument = PDDocument.load(...);

HelloSignAnalyzer helloSignAnalyzer = new HelloSignAnalyzer(pdDocument);

HelloSignManipulator helloSignManipulator = new HelloSignManipulator(helloSignAnalyzer);

helloSignManipulator.clearFields(Collections.singleton("var1004"));
helloSignManipulator.clearFields(Collections.singleton("var1003"));
helloSignManipulator.clearFields(Collections.singleton("date2"));

pdDocument.save(...);

(PlayWithHelloSign.java测试方法testClearZipStateDate2SuccessivelyInput)

警告

这些类只是概念验证。一方面,它们是基于单个 HelloSign 示例文件构建的,因此很可能会错过重要的细节。另一方面,有一些内置的假设,例如在HelloSignField 方法inField 中。

此外,一般而言,操纵签名的 HelloSign 文件可能不是一个好主意。如果我正确理解了他们的概念,他们会存储每个签名文档的哈希值以允许验证内容,如果文档如上所示被操纵,哈希值将不再匹配。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-19
    • 1970-01-01
    • 1970-01-01
    • 2021-02-21
    • 1970-01-01
    • 2020-07-26
    相关资源
    最近更新 更多