Управляйте контурами, цветом и т. Д. В iText

Мне нужно проанализировать данные пути к файлам PDF и управлять содержимым с помощью iText 7. Манипуляции включают удаление / замену и раскрашивание.

Я могу хорошо проанализировать графику с помощью примерно следующего кода:

public class ContentParsing {
    public static void main(String[] args) throws IOException {
        new ContentParsing().inspectPdf("testdata/test.pdf");
    }

    public void inspectPdf(String path) throws IOException {
        File file = new File(path);
        PdfDocument pdf = new PdfDocument(new PdfReader(file.getAbsolutePath()));
        PdfDocumentContentParser parser = new PdfDocumentContentParser(pdf);
        for (int i=1; i<=pdf.getNumberOfPages(); i++) {
            parser.processContent(i, new PathEventListener());
        }
        pdf.close();
    }
}


public class PathEventListener implements IEventListener {
    public void eventOccurred(IEventData eventData, EventType eventType) {
        PathRenderInfo pathRenderInfo = (PathRenderInfo) eventData;
        for ( Subpath subpath : pathRenderInfo.getPath().getSubpaths() ) {
            for ( IShape segment : subpath.getSegments() ) {
                // Here goes some path analysis code
                System.out.println(segment.getBasePoints());
            }
        }
    }

    public Set<EventType> getSupportedEvents() {
        Set<EventType> supportedEvents = new HashSet<EventType>();
        supportedEvents.add(EventType.RENDER_PATH);
        return supportedEvents;
    }
}

Теперь, как можно манипулировать вещами и записывать их обратно в PDF? Нужно ли мне создавать совершенно новый PDF-документ и копировать все (в измененной форме), или я могу каким-то образом напрямую манипулировать прочитанными данными PDF?


person Thomas W    schedule 03.12.2016    source источник
comment
Создание нового PDF-файла и добавление измененного содержимого, вероятно, лучший способ получить полный контроль. Изменение существующего PDF-файла технически возможно, а некоторые задачи, такие как добавление контента поверх / под существующий контент или выделение другим цветом, довольно легко выполнить с помощью iText. Другие, особенно такие как замена текста или поиск, содержат большое количество подводных камней и технически сложны. Я бы рекомендовал взглянуть на developers.itextpdf.com и просмотреть несколько примеров и руководств, чтобы увидеть что возможно.   -  person Samuel Huylebroeck    schedule 05.12.2016


Ответы (1)


Теперь, как можно манипулировать вещами и записывать их обратно в PDF? Нужно ли мне создавать совершенно новый PDF-документ и копировать все (в измененной форме), или я могу каким-то образом напрямую манипулировать прочитанными данными PDF?

По сути, вы ищете класс, который не просто анализирует поток содержимого PDF и сигнализирует о содержащихся в нем инструкциях, таких как PdfCanvasProcessor (PdfDocumentContentParser, который вы используете, просто очень тонкая оболочка для PdfCanvasProcessor), но который также создает поток содержимого заново с инструкции, которые вы перешли обратно к нему.

Общий класс редактора потока контента

Для iText 5.5.x доказательство концепции для такого класса редактора потока контента можно найти в этом ответе (Java версия находится немного ниже в тексте ответа).

Это перенос проверенной концепции на iText 7:

public class PdfCanvasEditor extends PdfCanvasProcessor
{
    /**
     * This method edits the immediate contents of a page, i.e. its content stream.
     * It explicitly does not descent into form xobjects, patterns, or annotations.
     */
    public void editPage(PdfDocument pdfDocument, int pageNumber) throws IOException
    {
        if ((pdfDocument.getReader() == null) || (pdfDocument.getWriter() == null))
        {
            throw new PdfException("PdfDocument must be opened in stamping mode.");
        }

        PdfPage page = pdfDocument.getPage(pageNumber);
        PdfResources pdfResources = page.getResources();
        PdfCanvas pdfCanvas = new PdfCanvas(new PdfStream(), pdfResources, pdfDocument);
        editContent(page.getContentBytes(), pdfResources, pdfCanvas);
        page.put(PdfName.Contents, pdfCanvas.getContentStream());
    }

    /**
     * This method processes the content bytes and outputs to the given canvas.
     * It explicitly does not descent into form xobjects, patterns, or annotations.
     */
    public void editContent(byte[] contentBytes, PdfResources resources, PdfCanvas canvas)
    {
        this.canvas = canvas;
        processContent(contentBytes, resources);
        this.canvas = null;
    }

    /**
     * <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 the {@link ContentOperatorWrapper} instances
     * forward to it.
     * </p>
     * <p>
     * Override this method to achieve some fancy editing effect.
     * </p> 
     */
    protected void write(PdfCanvasProcessor processor, PdfLiteral operator, List<PdfObject> operands)
    {
        PdfOutputStream pdfOutputStream = canvas.getContentStream().getOutputStream();
        int index = 0;

        for (PdfObject object : operands)
        {
            pdfOutputStream.write(object);
            if (operands.size() > ++index)
                pdfOutputStream.writeSpace();
            else
                pdfOutputStream.writeNewLine();
        }
    }

    //
    // constructor giving the parent a dummy listener to talk to 
    //
    public PdfCanvasEditor()
    {
        super(new DummyEventListener());
    }

    //
    // Overrides of PdfContentStreamProcessor methods
    //
    @Override
    public IContentOperator registerContentOperator(String operatorString, IContentOperator operator)
    {
        ContentOperatorWrapper wrapper = new ContentOperatorWrapper();
        wrapper.setOriginalOperator(operator);
        IContentOperator formerOperator = super.registerContentOperator(operatorString, wrapper);
        return formerOperator instanceof ContentOperatorWrapper ? ((ContentOperatorWrapper)formerOperator).getOriginalOperator() : formerOperator;
    }

    //
    // members holding the output canvas and the resources
    //
    protected PdfCanvas canvas = null;

    //
    // A content operator class to wrap all content operators to forward the invocation to the editor
    //
    class ContentOperatorWrapper implements IContentOperator
    {
        public IContentOperator getOriginalOperator()
        {
            return originalOperator;
        }

        public void setOriginalOperator(IContentOperator originalOperator)
        {
            this.originalOperator = originalOperator;
        }

        @Override
        public void invoke(PdfCanvasProcessor processor, PdfLiteral operator, List<PdfObject> operands)
        {
            if (originalOperator != null && !"Do".equals(operator.toString()))
            {
                originalOperator.invoke(processor, operator, operands);
            }
            write(processor, operator, operands);
        }

        private IContentOperator originalOperator = null;
    }

    //
    // A dummy event listener to give to the underlying canvas processor to feed events to
    //
    static class DummyEventListener implements IEventListener
    {
        @Override
        public void eventOccurred(IEventData data, EventType type)
        { }

        @Override
        public Set<EventType> getSupportedEvents()
        {
            return null;
        }
    }
}

(PdfCanvasEditor.java)

Объяснения из ответа iText 5 все еще применимы, структура синтаксического анализа не сильно изменилась с iText 5.5.x до iText 7.0. Икс.

Примеры использования

К сожалению, вы очень расплывчато написали о том, как именно вы хотите изменить содержимое. Таким образом, я просто портировал несколько примеров iText 5, в которых использовался исходный класс редактора потока контента iText 5:

Удаление водяных знаков

Это порты вариантов использования в этом ответе.

testRemoveBoldMTTextDocument

В этом примере удаляется весь текст, написанный шрифтом, имя которого заканчивается на BoldMT:

try (   InputStream resource = getClass().getResourceAsStream("document.pdf");
        PdfReader pdfReader = new PdfReader(resource);
        OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "document-noBoldMTText.pdf"));
        PdfWriter pdfWriter = new PdfWriter(result);
        PdfDocument pdfDocument = new PdfDocument(pdfReader, pdfWriter) )
{
    PdfCanvasEditor editor = new PdfCanvasEditor()
    {

        @Override
        protected void write(PdfCanvasProcessor processor, PdfLiteral operator, List<PdfObject> operands)
        {
            String operatorString = operator.toString();

            if (TEXT_SHOWING_OPERATORS.contains(operatorString))
            {
                if (getGraphicsState().getFont().getFontProgram().getFontNames().getFontName().endsWith("BoldMT"))
                    return;
            }
            
            super.write(processor, operator, operands);
        }

        final List<String> TEXT_SHOWING_OPERATORS = Arrays.asList("Tj", "'", "\"", "TJ");
    };
    for (int i = 1; i <= pdfDocument.getNumberOfPages(); i++)
    {
        editor.editPage(pdfDocument, i);
    }
}

(EditPageContent.java метод тестирования testRemoveBoldMTTextDocument)

testRemoveBigTextDocument

В этом примере удаляется весь текст, написанный крупным шрифтом:

try (   InputStream resource = getClass().getResourceAsStream("document.pdf");
        PdfReader pdfReader = new PdfReader(resource);
        OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "document-noBigText.pdf"));
        PdfWriter pdfWriter = new PdfWriter(result);
        PdfDocument pdfDocument = new PdfDocument(pdfReader, pdfWriter) )
{
    PdfCanvasEditor editor = new PdfCanvasEditor()
    {

        @Override
        protected void write(PdfCanvasProcessor processor, PdfLiteral operator, List<PdfObject> operands)
        {
            String operatorString = operator.toString();

            if (TEXT_SHOWING_OPERATORS.contains(operatorString))
            {
                if (getGraphicsState().getFontSize() > 100)
                    return;
            }
            
            super.write(processor, operator, operands);
        }

        final List<String> TEXT_SHOWING_OPERATORS = Arrays.asList("Tj", "'", "\"", "TJ");
    };
    for (int i = 1; i <= pdfDocument.getNumberOfPages(); i++)
    {
        editor.editPage(pdfDocument, i);
    }
}

(EditPageContent.java метод тестирования testRemoveBigTextDocument)

Изменение цвета текста

Это порт варианта использования в этом ответе.

testChangeBlackTextToGreenDocument

В этом примере цвет черного текста меняется на зеленый.

try (   InputStream resource = getClass().getResourceAsStream("document.pdf");
        PdfReader pdfReader = new PdfReader(resource);
        OutputStream result = new FileOutputStream(new File(RESULT_FOLDER, "document-blackTextToGreen.pdf"));
        PdfWriter pdfWriter = new PdfWriter(result);
        PdfDocument pdfDocument = new PdfDocument(pdfReader, pdfWriter) )
{
    PdfCanvasEditor editor = new PdfCanvasEditor()
    {

        @Override
        protected void write(PdfCanvasProcessor processor, PdfLiteral operator, List<PdfObject> operands)
        {
            String operatorString = operator.toString();

            if (TEXT_SHOWING_OPERATORS.contains(operatorString))
            {
                if (currentlyReplacedBlack == null)
                {
                    Color currentFillColor = getGraphicsState().getFillColor();
                    if (Color.BLACK.equals(currentFillColor))
                    {
                        currentlyReplacedBlack = currentFillColor;
                        super.write(processor, new PdfLiteral("rg"), Arrays.asList(new PdfNumber(0), new PdfNumber(1), new PdfNumber(0), new PdfLiteral("rg")));
                    }
                }
            }
            else if (currentlyReplacedBlack != null)
            {
                if (currentlyReplacedBlack instanceof DeviceCmyk)
                {
                    super.write(processor, new PdfLiteral("k"), Arrays.asList(new PdfNumber(0), new PdfNumber(0), new PdfNumber(0), new PdfNumber(1), new PdfLiteral("k")));
                }
                else if (currentlyReplacedBlack instanceof DeviceGray)
                {
                    super.write(processor, new PdfLiteral("g"), Arrays.asList(new PdfNumber(0), new PdfLiteral("g")));
                }
                else
                {
                    super.write(processor, new PdfLiteral("rg"), Arrays.asList(new PdfNumber(0), new PdfNumber(0), new PdfNumber(0), new PdfLiteral("rg")));
                }
                currentlyReplacedBlack = null;
            }

            super.write(processor, operator, operands);
        }

        Color currentlyReplacedBlack = null;

        final List<String> TEXT_SHOWING_OPERATORS = Arrays.asList("Tj", "'", "\"", "TJ");
    };
    for (int i = 1; i <= pdfDocument.getNumberOfPages(); i++)
    {
        editor.editPage(pdfDocument, i);
    }
}

(EditPageContent.java метод тестирования testChangeBlackTextToGreenDocument)

person mkl    schedule 06.12.2016
comment
Вау, очень обстоятельный ответ! Мне нужно время, чтобы пройти через это. Большое спасибо! - person Thomas W; 06.12.2016
comment
Просто чтобы закончить примеры использования - кажется, в трех примерах использования должно быть pdfDocument.close() после for циклов, верно? По крайней мере, я получу пустой файл, только если я его не добавлю. Или это проблема только с Java 1.8? - person Thomas W; 19.12.2016
comment
@Thomas экземпляры PdfDocument pdfDocument в примерах автоматически закрываются, поскольку они определены соответственно try ( HERE ) {...}. - person mkl; 19.12.2016
comment
Ах я вижу. Eclipse пожаловался на try-with-resources, поэтому я удалил его, чтобы получить базовую версию, с которой можно было бы повозиться. Раньше почти не работал с Java - я понял, что в Eclipse мне нужно щелкнуть проект правой кнопкой мыши, выбрать «Свойства / Java Compiler» и установить для параметров соответствия компилятора значение 1.7. - person Thomas W; 19.12.2016
comment
Как лицензируется ваш класс PdfCanvasProcessor? Я не вижу лицензии на репозиторий GitHub, где вы его опубликовали. - person Thomas W; 27.11.2020
comment
По сути, я опубликовал его здесь, в stackoverflow, на github - это просто законченная копия, включая импорт и т. Д. Таким образом, применяется лицензирование, производное от stackoverflow. - person mkl; 27.11.2020