从 PDF 中过滤掉所有超过特定字体大小的文本
Filter out all text above a certain font size from PDF
正如标题所说,我想从 PDF 中过滤掉超过特定字体大小的所有文本。目前,我正在使用 PDFBox 库,但我愿意为 Java.
使用任何其他免费库
我的方法是使用 PDFStreamParser 遍历标记。当我传递一个大小大于我的阈值的 Tf 运算符时,不要添加看到的下一个 Tj/TJ。然而,我已经清楚这种相对简单的方法是行不通的,因为文本可能会被当前的变换矩阵缩放。
有没有我可以采用的更好的方法,或者有什么方法可以使我的方法行得通而又不会变得太复杂?
你的方法
When I pass a Tf operator that has a size greater than my threshold, don't add the next Tj/TJ that is seen.
太简单了。
一方面,正如您所说,
the text may be scaled by the current transformation matrix.
(其实不仅是变换矩阵,还有文本矩阵!)
因此,您必须跟踪这些矩阵。
另一方面,Tf 不仅为 看到的下一个文本绘制指令 设置了基本字体大小,它还设置了它直到大小被其他指令明确更改。
此外,文本字体大小和当前变换矩阵是图形状态的一部分;因此,它们受保存状态和恢复状态指令的约束。
因此,要根据当前状态编辑内容流,您必须跟踪大量信息。幸运的是,PDFBox 包含 classes 来完成这里的繁重工作,class 层次结构基于 PDFStreamEngine
,让您可以专注于您的任务。要获得尽可能多的可用于编辑的信息,PDFGraphicsStreamEngine
class 似乎是一个不错的选择。
通用内容流编辑器class
因此,让我们从 PDFGraphicsStreamEngine
派生 PdfContentStreamEditor
并添加一些代码来生成替换内容流。
public class PdfContentStreamEditor extends PDFGraphicsStreamEngine {
public PdfContentStreamEditor(PDDocument document, PDPage page) {
super(page);
this.document = document;
}
/**
* <p>
* This method retrieves the next operation before its registered
* listener is called. The default does nothing.
* </p>
* <p>
* Override this method to retrieve state information from before the
* operation execution.
* </p>
*/
protected void nextOperation(Operator operator, List<COSBase> operands) {
}
/**
* <p>
* This method writes content stream operations to the target canvas. The default
* implementation writes them as they come, so it essentially generates identical
* copies of the original instructions {@link #processOperator(Operator, List)}
* forwards to it.
* </p>
* <p>
* Override this method to achieve some fancy editing effect.
* </p>
*/
protected void write(ContentStreamWriter contentStreamWriter, Operator operator, List<COSBase> operands) throws IOException {
contentStreamWriter.writeTokens(operands);
contentStreamWriter.writeToken(operator);
}
// stub implementation of PDFGraphicsStreamEngine abstract methods
@Override
public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException { }
@Override
public void drawImage(PDImage pdImage) throws IOException { }
@Override
public void clip(int windingRule) throws IOException { }
@Override
public void moveTo(float x, float y) throws IOException { }
@Override
public void lineTo(float x, float y) throws IOException { }
@Override
public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) throws IOException { }
@Override
public Point2D getCurrentPoint() throws IOException { return null; }
@Override
public void closePath() throws IOException { }
@Override
public void endPath() throws IOException { }
@Override
public void strokePath() throws IOException { }
@Override
public void fillPath(int windingRule) throws IOException { }
@Override
public void fillAndStrokePath(int windingRule) throws IOException { }
@Override
public void shadingFill(COSName shadingName) throws IOException { }
// PDFStreamEngine overrides to allow editing
@Override
public void processPage(PDPage page) throws IOException {
PDStream stream = new PDStream(document);
replacement = new ContentStreamWriter(replacementStream = stream.createOutputStream(COSName.FLATE_DECODE));
super.processPage(page);
replacementStream.close();
page.setContents(stream);
replacement = null;
replacementStream = null;
}
@Override
public void showForm(PDFormXObject form) throws IOException {
// DON'T descend into XObjects
// super.showForm(form);
}
@Override
protected void processOperator(Operator operator, List<COSBase> operands) throws IOException {
nextOperation(operator, operands);
super.processOperator(operator, operands);
write(replacement, operator, operands);
}
final PDDocument document;
OutputStream replacementStream = null;
ContentStreamWriter replacement = null;
}
(PdfContentStreamEditor class)
此代码覆盖 processPage
以创建新的页面内容流并最终用它替换旧的。它覆盖 processOperator
以提供处理后的编辑指令。
要进行编辑,只需在此处覆盖 write
。现有的实现只是简单地编写指令,而您可以更改指令以编写。覆盖 nextOperation
允许您在 应用当前指令之前 查看图形状态。
按原样应用编辑器,
PDDocument document = PDDocument.load(SOURCE);
for (PDPage page : document.getDocumentCatalog().getPages()) {
PdfContentStreamEditor identity = new PdfContentStreamEditor(document, page);
identity.processPage(page);
}
document.save(RESULT);
(EditPageContent 测试 testIdentityInput
)
因此,将创建具有等效内容流的结果 PDF。
为您的用例自定义内容流编辑器
你想要
filter out all text from a PDF that is above a certain font size.
因此,我们要在write
中检查当前指令是否为文本绘制指令,如果是,则需要检查当前有效字号,即由文本矩阵和当前变换矩阵。如果有效字体太大,我们不得不删除指令。
这可以按如下方式完成:
PDDocument document = PDDocument.load(SOURCE);
for (PDPage page : document.getDocumentCatalog().getPages()) {
PdfContentStreamEditor identity = new PdfContentStreamEditor(document, page) {
@Override
protected void write(ContentStreamWriter contentStreamWriter, Operator operator, List<COSBase> operands) throws IOException {
String operatorString = operator.getName();
if (TEXT_SHOWING_OPERATORS.contains(operatorString))
{
float fs = getGraphicsState().getTextState().getFontSize();
Matrix matrix = getTextMatrix().multiply(getGraphicsState().getCurrentTransformationMatrix());
Point2D.Float transformedFsVector = matrix.transformPoint(0, fs);
Point2D.Float transformedOrigin = matrix.transformPoint(0, 0);
double transformedFs = transformedFsVector.distance(transformedOrigin);
if (transformedFs > 100)
return;
}
super.write(contentStreamWriter, operator, operands);
}
final List<String> TEXT_SHOWING_OPERATORS = Arrays.asList("Tj", "'", "\"", "TJ");
};
identity.processPage(page);
}
document.save(RESULT);
(EditPageContent 测试 testRemoveBigTextDocument
)
严格来说,完全删除有问题的指令可能还不够;取而代之的是,必须将其替换为更改文本矩阵的指令,就像丢弃的文本绘图指令所做的那样。否则,可能会移动以下未删除的文本。但是,这通常会按原样工作,因为文本矩阵是为以下不同文本新设置的。所以让我们在这里保持简单。
约束和备注
此 PdfContentStreamEditor
仅编辑页面内容流。从那里可以使用当前未被编辑器编辑的 XObjects 和模式。不过,在编辑页面内容流之后,递归迭代 XObject 和模式并以类似的方式编辑它们应该很容易。
这个 PdfContentStreamEditor
本质上是 PdfContentStreamEditor
for iText 5 (.Net/Java) 来自 this answer and the PdfCanvasEditor
for iText 7 from 的移植。使用这些编辑器 classes 的示例可能会提供一些有关如何将此 PdfContentStreamEditor
用于 PDFBox 的提示。
以前在 HelloSignManipulator class in this answer.
中使用过类似(但不太通用)的方法
修复错误
在 this question 的上下文中发现了 PdfContentStreamEditor
中的一个错误,该错误导致焦点所在的示例 PDF 中的一些文本行被移动。
背景:一些 PDF 指令是通过其他指令定义的,例如tx ty TD 被指定为与 -ty TL [=105 具有相同的效果=]tx ty Td.为简单起见,相应的 PDFBox OperatorProcessor
实现通过将等效指令反馈回流引擎来工作。
上面实现的PdfContentStreamEditor
在这种情况下会检索替换指令和原始指令的信号,并将它们全部写回到结果流中。因此,这些指令的效果加倍。例如。在 TD 指令的情况下,文本插入点转发两行而不是一行...
因此,我们不得不忽略替换指令。为此,将上面的方法 processOperator
替换为
@Override
protected void processOperator(Operator operator, List<COSBase> operands) throws IOException {
if (inOperator) {
super.processOperator(operator, operands);
} else {
inOperator = true;
nextOperation(operator, operands);
super.processOperator(operator, operands);
write(replacement, operator, operands);
inOperator = false;
}
}
boolean inOperator = false;
正如标题所说,我想从 PDF 中过滤掉超过特定字体大小的所有文本。目前,我正在使用 PDFBox 库,但我愿意为 Java.
使用任何其他免费库我的方法是使用 PDFStreamParser 遍历标记。当我传递一个大小大于我的阈值的 Tf 运算符时,不要添加看到的下一个 Tj/TJ。然而,我已经清楚这种相对简单的方法是行不通的,因为文本可能会被当前的变换矩阵缩放。
有没有我可以采用的更好的方法,或者有什么方法可以使我的方法行得通而又不会变得太复杂?
你的方法
When I pass a Tf operator that has a size greater than my threshold, don't add the next Tj/TJ that is seen.
太简单了。
一方面,正如您所说,
the text may be scaled by the current transformation matrix.
(其实不仅是变换矩阵,还有文本矩阵!)
因此,您必须跟踪这些矩阵。
另一方面,Tf 不仅为 看到的下一个文本绘制指令 设置了基本字体大小,它还设置了它直到大小被其他指令明确更改。
此外,文本字体大小和当前变换矩阵是图形状态的一部分;因此,它们受保存状态和恢复状态指令的约束。
因此,要根据当前状态编辑内容流,您必须跟踪大量信息。幸运的是,PDFBox 包含 classes 来完成这里的繁重工作,class 层次结构基于 PDFStreamEngine
,让您可以专注于您的任务。要获得尽可能多的可用于编辑的信息,PDFGraphicsStreamEngine
class 似乎是一个不错的选择。
通用内容流编辑器class
因此,让我们从 PDFGraphicsStreamEngine
派生 PdfContentStreamEditor
并添加一些代码来生成替换内容流。
public class PdfContentStreamEditor extends PDFGraphicsStreamEngine {
public PdfContentStreamEditor(PDDocument document, PDPage page) {
super(page);
this.document = document;
}
/**
* <p>
* This method retrieves the next operation before its registered
* listener is called. The default does nothing.
* </p>
* <p>
* Override this method to retrieve state information from before the
* operation execution.
* </p>
*/
protected void nextOperation(Operator operator, List<COSBase> operands) {
}
/**
* <p>
* This method writes content stream operations to the target canvas. The default
* implementation writes them as they come, so it essentially generates identical
* copies of the original instructions {@link #processOperator(Operator, List)}
* forwards to it.
* </p>
* <p>
* Override this method to achieve some fancy editing effect.
* </p>
*/
protected void write(ContentStreamWriter contentStreamWriter, Operator operator, List<COSBase> operands) throws IOException {
contentStreamWriter.writeTokens(operands);
contentStreamWriter.writeToken(operator);
}
// stub implementation of PDFGraphicsStreamEngine abstract methods
@Override
public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException { }
@Override
public void drawImage(PDImage pdImage) throws IOException { }
@Override
public void clip(int windingRule) throws IOException { }
@Override
public void moveTo(float x, float y) throws IOException { }
@Override
public void lineTo(float x, float y) throws IOException { }
@Override
public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) throws IOException { }
@Override
public Point2D getCurrentPoint() throws IOException { return null; }
@Override
public void closePath() throws IOException { }
@Override
public void endPath() throws IOException { }
@Override
public void strokePath() throws IOException { }
@Override
public void fillPath(int windingRule) throws IOException { }
@Override
public void fillAndStrokePath(int windingRule) throws IOException { }
@Override
public void shadingFill(COSName shadingName) throws IOException { }
// PDFStreamEngine overrides to allow editing
@Override
public void processPage(PDPage page) throws IOException {
PDStream stream = new PDStream(document);
replacement = new ContentStreamWriter(replacementStream = stream.createOutputStream(COSName.FLATE_DECODE));
super.processPage(page);
replacementStream.close();
page.setContents(stream);
replacement = null;
replacementStream = null;
}
@Override
public void showForm(PDFormXObject form) throws IOException {
// DON'T descend into XObjects
// super.showForm(form);
}
@Override
protected void processOperator(Operator operator, List<COSBase> operands) throws IOException {
nextOperation(operator, operands);
super.processOperator(operator, operands);
write(replacement, operator, operands);
}
final PDDocument document;
OutputStream replacementStream = null;
ContentStreamWriter replacement = null;
}
(PdfContentStreamEditor class)
此代码覆盖 processPage
以创建新的页面内容流并最终用它替换旧的。它覆盖 processOperator
以提供处理后的编辑指令。
要进行编辑,只需在此处覆盖 write
。现有的实现只是简单地编写指令,而您可以更改指令以编写。覆盖 nextOperation
允许您在 应用当前指令之前 查看图形状态。
按原样应用编辑器,
PDDocument document = PDDocument.load(SOURCE);
for (PDPage page : document.getDocumentCatalog().getPages()) {
PdfContentStreamEditor identity = new PdfContentStreamEditor(document, page);
identity.processPage(page);
}
document.save(RESULT);
(EditPageContent 测试 testIdentityInput
)
因此,将创建具有等效内容流的结果 PDF。
为您的用例自定义内容流编辑器
你想要
filter out all text from a PDF that is above a certain font size.
因此,我们要在write
中检查当前指令是否为文本绘制指令,如果是,则需要检查当前有效字号,即由文本矩阵和当前变换矩阵。如果有效字体太大,我们不得不删除指令。
这可以按如下方式完成:
PDDocument document = PDDocument.load(SOURCE);
for (PDPage page : document.getDocumentCatalog().getPages()) {
PdfContentStreamEditor identity = new PdfContentStreamEditor(document, page) {
@Override
protected void write(ContentStreamWriter contentStreamWriter, Operator operator, List<COSBase> operands) throws IOException {
String operatorString = operator.getName();
if (TEXT_SHOWING_OPERATORS.contains(operatorString))
{
float fs = getGraphicsState().getTextState().getFontSize();
Matrix matrix = getTextMatrix().multiply(getGraphicsState().getCurrentTransformationMatrix());
Point2D.Float transformedFsVector = matrix.transformPoint(0, fs);
Point2D.Float transformedOrigin = matrix.transformPoint(0, 0);
double transformedFs = transformedFsVector.distance(transformedOrigin);
if (transformedFs > 100)
return;
}
super.write(contentStreamWriter, operator, operands);
}
final List<String> TEXT_SHOWING_OPERATORS = Arrays.asList("Tj", "'", "\"", "TJ");
};
identity.processPage(page);
}
document.save(RESULT);
(EditPageContent 测试 testRemoveBigTextDocument
)
严格来说,完全删除有问题的指令可能还不够;取而代之的是,必须将其替换为更改文本矩阵的指令,就像丢弃的文本绘图指令所做的那样。否则,可能会移动以下未删除的文本。但是,这通常会按原样工作,因为文本矩阵是为以下不同文本新设置的。所以让我们在这里保持简单。
约束和备注
此 PdfContentStreamEditor
仅编辑页面内容流。从那里可以使用当前未被编辑器编辑的 XObjects 和模式。不过,在编辑页面内容流之后,递归迭代 XObject 和模式并以类似的方式编辑它们应该很容易。
这个 PdfContentStreamEditor
本质上是 PdfContentStreamEditor
for iText 5 (.Net/Java) 来自 this answer and the PdfCanvasEditor
for iText 7 from PdfContentStreamEditor
用于 PDFBox 的提示。
以前在 HelloSignManipulator class in this answer.
中使用过类似(但不太通用)的方法修复错误
在 this question 的上下文中发现了 PdfContentStreamEditor
中的一个错误,该错误导致焦点所在的示例 PDF 中的一些文本行被移动。
背景:一些 PDF 指令是通过其他指令定义的,例如tx ty TD 被指定为与 -ty TL [=105 具有相同的效果=]tx ty Td.为简单起见,相应的 PDFBox OperatorProcessor
实现通过将等效指令反馈回流引擎来工作。
上面实现的PdfContentStreamEditor
在这种情况下会检索替换指令和原始指令的信号,并将它们全部写回到结果流中。因此,这些指令的效果加倍。例如。在 TD 指令的情况下,文本插入点转发两行而不是一行...
因此,我们不得不忽略替换指令。为此,将上面的方法 processOperator
替换为
@Override
protected void processOperator(Operator operator, List<COSBase> operands) throws IOException {
if (inOperator) {
super.processOperator(operator, operands);
} else {
inOperator = true;
nextOperation(operator, operands);
super.processOperator(operator, operands);
write(replacement, operator, operands);
inOperator = false;
}
}
boolean inOperator = false;