Отфильтровать весь текст выше определенного размера шрифта из PDF
Как сказано в заголовке, я хочу отфильтровать весь текст из PDF-файла, размер шрифта которого превышает определенный. В настоящее время я использую библиотеку PDFBox, но я открыт для использования любой другой бесплатной библиотеки для Java.
Мой подход заключался в использовании PDFStreamParser для перебора токенов. Когда я передаю оператор Tf, размер которого превышает мой порог, не добавляйте следующее видимое значение Tj/TJ. Однако мне стало ясно, что этот относительно простой подход не будет работать, потому что текст может быть масштабирован текущей матрицей преобразования.
Есть ли лучший подход, который я мог бы использовать, или способ заставить мой подход работать, не усложняя его?
1 ответ
Ваш подход
Когда я передаю оператор Tf, размер которого превышает мой порог, не добавляйте следующее видимое значение Tj/TJ.
слишком просто.
С одной стороны, как вы сами замечаете,
текст можно масштабировать с помощью текущей матрицы преобразования.
(На самом деле не только по матрице преобразования, но и по текстовой матрице!)
Таким образом, вы должны отслеживать эти матрицы.
С другой стороны, Tf не только устанавливает базовый размер шрифта для следующей видимой инструкции рисования текста, но и устанавливает его до тех пор, пока размер не будет явно изменен какой-либо другой инструкцией.
Кроме того, размер шрифта текста и текущая матрица преобразования являются частью состояния графики; таким образом, они подлежат инструкциям сохранения и восстановления состояния.
Следовательно, чтобы редактировать поток контента в соответствии с текущим состоянием, вам необходимо отслеживать большой объем информации. К счастью, PDFBox содержит классы, которые выполняют здесь тяжелую работу, иерархия классов основана наPDFStreamEngine
, позволяя вам сосредоточиться на своей задаче. Чтобы как можно больше информации было доступно для редактирования,PDFGraphicsStreamEngine
class кажется хорошим выбором для дальнейшего развития.
Общий класс редактора потока контента
Итак, выведем PdfContentStreamEditor
от PDFGraphicsStreamEngine
и добавить код для создания потока замещающего контента.
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)
Этот код отменяет 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 testtestIdentityInput
)
следовательно, будет создан результирующий PDF-файл с эквивалентными потоками контента.
Настройка редактора потока контента для вашего варианта использования
Вы хотите
отфильтровать весь текст из PDF-файла, размер шрифта которого превышает определенный.
Таким образом, мы должны проверить 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 testtestRemoveBigTextDocument
)
Строго говоря, полного отказа от рассматриваемой инструкции может быть недостаточно; вместо этого нужно было бы заменить его инструкцией по изменению текстовой матрицы, как это сделали бы инструкции рисования отброшенного текста. В противном случае следующий неотброшенный текст может быть перемещен. Однако часто это работает как есть, потому что текстовая матрица заново устанавливается для следующего другого текста. Так что давайте будем проще.
Ограничения и замечания
Эта PdfContentStreamEditor
только редактирует поток содержимого страницы. Оттуда можно использовать XObjects и Patterns, которые в настоящее время не редактируются редактором. Тем не менее, должно быть легко после редактирования потока содержимого страницы рекурсивно перебирать объекты XObjects и Patterns и редактировать их аналогичным образом.
Эта PdfContentStreamEditor
по сути, это порт PdfContentStreamEditor
для iText 5 (.Net/Java) из этого ответа иPdfCanvasEditor
для iText 7 из этого ответа. Примеры использования этих классов редактора могут дать некоторые подсказки о том, как использовать этоPdfContentStreamEditor
для PDFBox.
Аналогичный (но менее общий) подход использовался ранее в классе HelloSignManipulator в этом ответе.
Исправление ошибки
В контексте этого вопроса ошибка вPdfContentStreamEditor
было обнаружено, что некоторые текстовые строки в примере PDF в фокусе были перемещены туда.
Предпосылки: Некоторые инструкции PDF определяются через другие, например, tx ty TD указано, чтобы иметь тот же эффект, что и -ty TL tx ty Td. Соответствующий PDFBoxOperatorProcessor
реализации для простоты работают, передавая эквивалентные инструкции обратно в движок потока.
В 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;