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.

  • Он игнорирует различное количество вертикального пространства.

Другие вопросы по тегам