IText читает PDF как pdftotext -layout?
Я ищу самый простой способ реализовать решение Java, которое тихо похоже на вывод
pdftotext -layout FILE
на машинах Linux. (И, конечно, это должно быть дешево)
Я только что попробовал некоторые фрагменты кода IText, PDFBox и PDFTextStream. На сегодняшний день наиболее точным решением является PDFTextStream, который использует VisualOutputTarget, чтобы получить отличное представление моего файла.
Таким образом, мое расположение столбцов признано правильным, и я могу работать с ним. Но должно быть и решение для IText, или?
Каждый простой фрагмент кода, который я нашел, производит простые упорядоченные строки, которые являются беспорядком (запутывают строку / столбец / строки). Есть ли какое-то решение, которое может быть проще и не может включать собственную стратегию? Или есть стратегия с открытым исходным кодом, которую я могу использовать?
// Я следовал инструкциям mkl и написал собственный объект стратегии следующим образом:
package com.test.pdfextractiontest.itext;
import ...
public class MyLocationTextExtractionStrategy implements TextExtractionStrategy {
/** set to true for debugging */
static boolean DUMP_STATE = false;
/** a summary of all found text */
private final List<TextChunk> locationalResult = new ArrayList<TextChunk>();
public MyLocationTextExtractionStrategy() {
}
@Override
public void beginTextBlock() {
}
@Override
public void endTextBlock() {
}
private boolean startsWithSpace(final String str) {
if (str.length() == 0) {
return false;
}
return str.charAt(0) == ' ';
}
private boolean endsWithSpace(final String str) {
if (str.length() == 0) {
return false;
}
return str.charAt(str.length() - 1) == ' ';
}
private List<TextChunk> filterTextChunks(final List<TextChunk> textChunks, final TextChunkFilter filter) {
if (filter == null) {
return textChunks;
}
final List<TextChunk> filtered = new ArrayList<TextChunk>();
for (final TextChunk textChunk : textChunks) {
if (filter.accept(textChunk)) {
filtered.add(textChunk);
}
}
return filtered;
}
protected boolean isChunkAtWordBoundary(final TextChunk chunk, final TextChunk previousChunk) {
final float dist = chunk.distanceFromEndOf(previousChunk);
if (dist < -chunk.getCharSpaceWidth() || dist > chunk.getCharSpaceWidth() / 2.0f) {
return true;
}
return false;
}
public String getResultantText(final TextChunkFilter chunkFilter) {
if (DUMP_STATE) {
dumpState();
}
final List<TextChunk> filteredTextChunks = filterTextChunks(this.locationalResult, chunkFilter);
Collections.sort(filteredTextChunks);
final StringBuffer sb = new StringBuffer();
TextChunk lastChunk = null;
for (final TextChunk chunk : filteredTextChunks) {
if (lastChunk == null) {
sb.append(chunk.text);
} else {
if (chunk.sameLine(lastChunk)) {
if (isChunkAtWordBoundary(chunk, lastChunk) && !startsWithSpace(chunk.text)
&& !endsWithSpace(lastChunk.text)) {
sb.append(' ');
}
final Float dist = chunk.distanceFromEndOf(lastChunk)/3;
for(int i = 0; i<Math.round(dist); i++) {
sb.append(' ');
}
sb.append(chunk.text);
} else {
sb.append('\n');
sb.append(chunk.text);
}
}
lastChunk = chunk;
}
return sb.toString();
}
eturn Строка с полученным текстом. */ @Override public String getResultantText() {
return getResultantText(null);
}
private void dumpState() {
for (final TextChunk location : this.locationalResult) {
location.printDiagnostics();
System.out.println();
}
}
@Override
public void renderText(final TextRenderInfo renderInfo) {
LineSegment segment = renderInfo.getBaseline();
if (renderInfo.getRise() != 0) {
final Matrix riseOffsetTransform = new Matrix(0, -renderInfo.getRise());
segment = segment.transformBy(riseOffsetTransform);
}
final TextChunk location =
new TextChunk(renderInfo.getText(), segment.getStartPoint(), segment.getEndPoint(),
renderInfo.getSingleSpaceWidth(),renderInfo);
this.locationalResult.add(location);
}
public static class TextChunk implements Comparable<TextChunk> {
/** the text of the chunk */
private final String text;
/** the starting location of the chunk */
private final Vector startLocation;
/** the ending location of the chunk */
private final Vector endLocation;
/** unit vector in the orientation of the chunk */
private final Vector orientationVector;
/** the orientation as a scalar for quick sorting */
private final int orientationMagnitude;
private final TextRenderInfo info;
private final int distPerpendicular;
private final float distParallelStart;
private final float distParallelEnd;
/** the width of a single space character in the font of the chunk */
private final float charSpaceWidth;
public TextChunk(final String string, final Vector startLocation, final Vector endLocation,
final float charSpaceWidth,final TextRenderInfo ri) {
this.text = string;
this.startLocation = startLocation;
this.endLocation = endLocation;
this.charSpaceWidth = charSpaceWidth;
this.info = ri;
Vector oVector = endLocation.subtract(startLocation);
if (oVector.length() == 0) {
oVector = new Vector(1, 0, 0);
}
this.orientationVector = oVector.normalize();
this.orientationMagnitude =
(int) (Math.atan2(this.orientationVector.get(Vector.I2), this.orientationVector.get(Vector.I1)) * 1000);
final Vector origin = new Vector(0, 0, 1);
this.distPerpendicular = (int) startLocation.subtract(origin).cross(this.orientationVector).get(Vector.I3);
this.distParallelStart = this.orientationVector.dot(startLocation);
this.distParallelEnd = this.orientationVector.dot(endLocation);
}
public Vector getStartLocation() {
return this.startLocation;
}
public Vector getEndLocation() {
return this.endLocation;
}
public String getText() {
return this.text;
}
public float getCharSpaceWidth() {
return this.charSpaceWidth;
}
private void printDiagnostics() {
System.out.println("Text (@" + this.startLocation + " -> " + this.endLocation + "): " + this.text);
System.out.println("orientationMagnitude: " + this.orientationMagnitude);
System.out.println("distPerpendicular: " + this.distPerpendicular);
System.out.println("distParallel: " + this.distParallelStart);
}
public boolean sameLine(final TextChunk as) {
if (this.orientationMagnitude != as.orientationMagnitude) {
return false;
}
if (this.distPerpendicular != as.distPerpendicular) {
return false;
}
return true;
}
public float distanceFromEndOf(final TextChunk other) {
final float distance = this.distParallelStart - other.distParallelEnd;
return distance;
}
public float myDistanceFromEndOf(final TextChunk other) {
final float distance = this.distParallelStart - other.distParallelEnd;
return distance;
}
@Override
public int compareTo(final TextChunk rhs) {
if (this == rhs) {
return 0; // not really needed, but just in case
}
int rslt;
rslt = compareInts(this.orientationMagnitude, rhs.orientationMagnitude);
if (rslt != 0) {
return rslt;
}
rslt = compareInts(this.distPerpendicular, rhs.distPerpendicular);
if (rslt != 0) {
return rslt;
}
return Float.compare(this.distParallelStart, rhs.distParallelStart);
}
private static int compareInts(final int int1, final int int2) {
return int1 == int2 ? 0 : int1 < int2 ? -1 : 1;
}
public TextRenderInfo getInfo() {
return this.info;
}
}
@Override
public void renderImage(final ImageRenderInfo renderInfo) {
// do nothing
}
public static interface TextChunkFilter {
public boolean accept(TextChunk textChunk);
}
}
Как вы можете видеть, большинство из них совпадают с исходным классом. я только добавил это:
final Float dist = chunk.distanceFromEndOf(lastChunk)/3;
for(int i = 0; i<Math.round(dist); i++) {
sb.append(' ');
}
в метод getResultantText, чтобы расширить пробелы с пробелами. Но вот проблема:
расстояние кажется неточным или неточным. результат выглядит
этот:
У кого-нибудь есть идеи, как рассчитать лучшее или значение для расстояния? Я думаю, это потому, что исходный тип шрифта - ArialMT, а мой редактор работает курьером, но для работы с этим листом рекомендуется разделить таблицу в нужном месте, чтобы получить мои данные. это трудно из-за плавающего начала и конца значения usw.
: - /
1 ответ
Проблема с вашим подходом вставки пробелов, как это
final Float dist = chunk.distanceFromEndOf(lastChunk)/3;
for(int i = 0; i<Math.round(dist); i++) {
sb.append(' ');
}
является то, что предполагается, что текущая позиция в StringBuffer
точно соответствует концу lastChunk
предполагая ширину символа в 3 единицы пространства пользователя. Это не должно иметь место, как правило, каждое добавление символов разрушает такую прежнюю корреспонденцию. Например, эти две строки имеют разную ширину при использовании пропорционального шрифта:
ililili
MWMWMWM
в то время как в StringBuffer
они занимают одинаковую длину.
Таким образом, вы должны посмотреть, где chunk
начинается относительно левой границы страницы и соответственно добавляет пробелы в буфер.
Кроме того, ваш код полностью игнорирует свободное место в начале строк.
Ваши результаты должны улучшиться, если вы замените оригинальный метод getResultantText(TextChunkFilter
по этому коду вместо этого:
public String getResultantText(TextChunkFilter chunkFilter){
if (DUMP_STATE) dumpState();
List<TextChunk> filteredTextChunks = filterTextChunks(locationalResult, chunkFilter);
Collections.sort(filteredTextChunks);
int startOfLinePosition = 0;
StringBuffer sb = new StringBuffer();
TextChunk lastChunk = null;
for (TextChunk chunk : filteredTextChunks) {
if (lastChunk == null){
insertSpaces(sb, startOfLinePosition, chunk.distParallelStart, false);
sb.append(chunk.text);
} else {
if (chunk.sameLine(lastChunk))
{
if (isChunkAtWordBoundary(chunk, lastChunk))
{
insertSpaces(sb, startOfLinePosition, chunk.distParallelStart, !startsWithSpace(chunk.text) && !endsWithSpace(lastChunk.text));
}
sb.append(chunk.text);
} else {
sb.append('\n');
startOfLinePosition = sb.length();
insertSpaces(sb, startOfLinePosition, chunk.distParallelStart, false);
sb.append(chunk.text);
}
}
lastChunk = chunk;
}
return sb.toString();
}
void insertSpaces(StringBuffer sb, int startOfLinePosition, float chunkStart, boolean spaceRequired)
{
int indexNow = sb.length() - startOfLinePosition;
int indexToBe = (int)((chunkStart - pageLeft) / fixedCharWidth);
int spacesToInsert = indexToBe - indexNow;
if (spacesToInsert < 1 && spaceRequired)
spacesToInsert = 1;
for (; spacesToInsert > 0; spacesToInsert--)
{
sb.append(' ');
}
}
public float pageLeft = 0;
public float fixedCharWidth = 6;
pageLeft
координата левой границы страницы. Стратегия не знает этого и, следовательно, должна быть рассказана явно; во многих случаях, однако, 0 является правильным значением.
В качестве альтернативы можно использовать минимум distParallelStart
стоимость всех кусков. Это обрезало бы левое поле, но не требовало бы, чтобы вы вводили точное значение левой границы страницы.
fixedCharWidth
предполагаемая ширина символа. В зависимости от записи в рассматриваемом PDF-документе может быть более подходящим другое значение. В вашем случае значение 3 кажется лучше, чем мое 6.
В этом коде еще много возможностей для улучшения. Например
Предполагается, что нет текстовых блоков, охватывающих несколько столбцов таблицы. Это предположение очень часто верно, но я видел странные PDF-файлы, в которых нормальный интервал между словами был реализован с использованием отдельных фрагментов текста с некоторым смещением, но интервал между столбцами был представлен одним пробелом в одном фрагменте (spanning конец одного столбца и начало следующего)! Ширина этого пробела была изменена настройкой межсловного пространства в графическом состоянии PDF.
Он игнорирует различное количество вертикального пространства.