Search code examples
pdfrotationitextpdfboxitext7

How to rotate a specific text in a pdf which is already present in the pdf using itext or pdfbox?


I know we can insert text into pdf with rotation using itext. But I want to rotate the text which is already present in the pdf.


Solution

  • First of all, in your question you only talk about how to rotate a specific text but in your example you additionally rotate a red rectangle. This answer focuses on rotating text. The process of guessing which graphics might be related to the text and, therefore, probably should be rotated along, is a topic in its own right.

    You also mention you are looking for a solution using itext or pdfbox and used the tags , , and . For this answer I chose iText 7.

    You did not explain what kind of text pieces you want to rotate but offered a representative example PDF. In that example I saw that the text to rotate was drawn using a single text showing instruction which is the only such instruction in the encompassing text object in the page content stream. To keep the code in the answer simple, therefore, I can assume the text to rotate is drawn in a consecutive sequence of text showing instructions in a text object in the page content stream framed by instructions that are not text showing ones. This is a generalization of your case.

    Furthermore, you did not mention the center of rotation. Based on your example files I assume it to be approximately the start of the base line of the text to rotate.

    A Simple Implementation

    When editing PDF content streams it is helpful to know the current graphics state at each instruction, e.g. to properly recognize the text drawn by a text showing operation one needs to know the current font to map the character codes to Unicode characters. The text extraction framework in iText already contains code to follow the graphics state. Thus, in this answer a base PdfCanvasEditor class has been developed on top of the text extraction framework.

    We can base a solution for the task at hand on that class after a small extension; that class originally sets the text extraction event listener to a dummy implementation but here we'll need a custom one. So we need to add an additional constructor that accepts such a custom event listener as parameter:

    public PdfCanvasEditor(IEventListener listener)
    {
        super(listener);
    }
    

    (Additional PdfCanvasEditor constructor)

    Based on this extended PdfCanvasEditor we can implement the task by inspecting the existing page content stream instruction by instruction. For a sequence of consecutive text showing instructions we retrieve the text matrix before and after the sequence, and if the text drawn by the sequence turns out to be the text to rotate, we insert an instruction before that sequence setting the initial text matrix to a rotated version of itself and another one after that sequence setting the text matrix back to what it was there originally.

    Our implementation LimitedTextRotater accepts a Matrix representing the desired rotation and a Predicate matching the string to rotate.

    public class LimitedTextRotater extends PdfCanvasEditor {
        public LimitedTextRotater(Matrix rotation, Predicate<String> textMatcher) {
            super(new TextRetrievingListener());
            ((TextRetrievingListener)getEventListener()).limitedTextRotater = this;
            this.rotation = rotation;
            this.textMatcher = textMatcher;
        }
    
        @Override
        protected void write(PdfCanvasProcessor processor, PdfLiteral operator, List<PdfObject> operands) {
            String operatorString = operator.toString();
    
            if (TEXT_SHOWING_OPERATORS.contains(operatorString)) {
                recentTextOperations.add(new ArrayList<>(operands));
            } else {
                if (!recentTextOperations.isEmpty()) {
                    boolean rotate = textMatcher.test(text.toString());
                    if (rotate)
                        writeSetTextMatrix(processor, rotation.multiply(initialTextMatrix));
                    for (List<PdfObject> recentOperation : recentTextOperations) {
                        super.write(processor, (PdfLiteral) recentOperation.get(recentOperation.size() - 1), recentOperation);
                    }
                    if (rotate)
                        writeSetTextMatrix(processor, finalTextMatrix);
                    recentTextOperations.clear();
                    text.setLength(0);
                    initialTextMatrix = null;
                }
                super.write(processor, operator, operands);
            }
        }
    
        void writeSetTextMatrix(PdfCanvasProcessor processor, Matrix textMatrix) {
            PdfLiteral operator = new PdfLiteral("Tm\n");
            List<PdfObject> operands = new ArrayList<>();
            operands.add(new PdfNumber(textMatrix.get(Matrix.I11)));
            operands.add(new PdfNumber(textMatrix.get(Matrix.I12)));
            operands.add(new PdfNumber(textMatrix.get(Matrix.I21)));
            operands.add(new PdfNumber(textMatrix.get(Matrix.I22)));
            operands.add(new PdfNumber(textMatrix.get(Matrix.I31)));
            operands.add(new PdfNumber(textMatrix.get(Matrix.I32)));
            operands.add(operator);
            super.write(processor, operator, operands);
        }
    
        void eventOccurred(TextRenderInfo textRenderInfo) {
            Matrix textMatrix = textRenderInfo.getTextMatrix();
            if (initialTextMatrix == null)
                initialTextMatrix = textMatrix;
            finalTextMatrix = new Matrix(textRenderInfo.getUnscaledWidth(), 0).multiply(textMatrix);
    
            text.append(textRenderInfo.getText());
        }
    
        static class TextRetrievingListener implements IEventListener {
            @Override
            public void eventOccurred(IEventData data, EventType type) {
                if (data instanceof TextRenderInfo) {
                    limitedTextRotater.eventOccurred((TextRenderInfo) data);
                }
            }
    
            @Override
            public Set<EventType> getSupportedEvents() {
                return null;
            }
    
            LimitedTextRotater limitedTextRotater;
        }
    
        final static List<String> TEXT_SHOWING_OPERATORS = Arrays.asList("Tj", "'", "\"", "TJ");
    
        final Matrix rotation;
        final Predicate<String> textMatcher;
    
        final List<List<PdfObject>> recentTextOperations = new ArrayList<>();
        final StringBuilder text = new StringBuilder();
        Matrix initialTextMatrix = null;
        Matrix finalTextMatrix = null;
    }
    

    (LimitedTextRotater)

    You can apply it to a document like this:

    try (   PdfReader pdfReader = new PdfReader(...);
            PdfWriter pdfWriter = new PdfWriter(...);
            PdfDocument pdfDocument = new PdfDocument(pdfReader, pdfWriter) )
    {
        PdfCanvasEditor editor = new LimitedTextRotater(new Matrix(0, -1, 1, 0, 0, 0), text -> true);
        for (int i = 1; i <= pdfDocument.getNumberOfPages(); i++){
            editor.editPage(pdfDocument, i);
        }
    }
    

    (RotateText test testBeforeAkhilNagaSai)

    The Predicate used here is text -> true which matches any text. In case of your example PDF that is ok as the text to rotate is the only text. In general you might want a more specific check, e.g. text -> text.equals("The text to be rotated"). In general try not to be too specific, though, as the extracted text might slightly deviate from expectations, e.g. by extra spaces.

    The result:

    screen shot

    As you can see the text is rotated. In contrast to your After.pdf, though, the red rectangle is not rotated. The reason is - as already mentioned at the start - that that rectangle in no way is part of the text.

    Some Ideas

    First of all, there are ports of the PdfCanvasEditor to iText 5 (the PdfContentStreamEditor in this answer) and PDFBox (the PdfContentStreamEditor in this answer). Thus, if you eventually prefer to switch to either of these PDF libraries, you can create equivalent implementations.

    Then, if the assumption that the text to rotate is drawn in a consecutive sequence of text showing instructions in a text object in the page content stream framed by instructions that are not text showing ones does not hold for you, you can generalize the implementation here somewhat. Have a look at the SimpleTextRemover in this answer for inspiration which is based on the PdfContentStreamEditor for iText 5. Here also texts that start somewhere in one text showing instruction and end somewhere in another one are processed which requires some more detailed data keeping and splitting of existing text drawing instructions.

    Also, if you want to rotate graphics along with the text that a human viewer might consider associated with it (like the red rectangle in your example file), you can try and extend the example accordingly, e.g. by also extracting the coordinates of the rotated text and in a second run trying to guess which graphics around those coordinates are related and rotating the graphics along. This is not trivial, though.

    Finally note that the Matrix provided in the constructor is not limited to rotations, it can represent an arbitrary affine transformation. So instead of rotating text you can also move it or scale it or skew it, ...