Search code examples
javapdfbox

How to invert underlaying colors in a PDF Renderer


I'm currently working on implementing a PDF renderer in Java using Apache PDFBox 3.0.0, and I would like to add the ability to draw PDF elements like strings, rectangles, and lines using a XOR (Exclusive OR) mode to invert the underlying elements' colors. This effect is commonly used for highlighting or inverting graphics.

Example:

enter image description here

PDFRenderer:

public class PDFRenderer {
    private PDDocument pdDocument;
    private PDPageContentStream contentStream;

    private float originY;
    private float originX;

    public PDFRenderer() {
        this.pdDocument = new PDDocument();

        PDPage pdPage = new PDPage();
        this.pdDocument.addPage(pdPage);

        try {
            this.contentStream = new PDPageContentStream(pdDocument, pdPage, AppendMode.APPEND, false, false);
        } catch (IOException e) {
            e.printStackTrace();
        }

        originY = pdPage.getMediaBox().getHeight();
        originX = 0;
    }

    /**
     * Draws a string on the canvas.
     *
     * @param text The text to draw.
     * @param x The x-coordinate of the top-left corner of the text.
     * @param y The y-coordinate of the top-left corner of the text.
     * @param width The width of the text box.
     * @param height The height of the text box.
     * @param color The color of the text.
     * @param font The font to use.
     */
    public void drawString(String text, int x, int y, int width, int height, CustomColor color, CustomFont font) {
        try {
            setStrokingColor(color);

            int fontHeight = Math.round((font.getFont().getFontDescriptor().getCapHeight()) / 1000 * height);
            float textWidth = font.getFont().getStringWidth(text) / 1000 * height;

            contentStream.setFont(font.getFont(), height);
            contentStream.setHorizontalScaling(100 + width);
            contentStream.beginText();
            contentStream.newLineAtOffset(originX + x, originY - fontHeight - y);
            contentStream.showText(text);
            contentStream.endText();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Draws a rectangle on the canvas.
     *
     * @param x The x-coordinate of the top-left corner of the rectangle.
     * @param y The y-coordinate of the top-left corner of the rectangle.
     * @param width The width of the rectangle.
     * @param height The height of the rectangle.
     * @param borderThickness The thickness of the rectangle's border.
     * @param color The color of the rectangle.
     * @param degreeOfCornerRounding The degree of corner rounding for the rectangle accepted are values from 1 to 8.
     * @param colorInverted Indicates if the rectangle color is inverted.
     */
    public void drawRectangle(int x, int y, int width, int height, int borderThickness, CustomColor color, int degreeOfCornerRounding, boolean colorInverted) {
        try {
            setStrokingColor(color);

            if (borderThickness > width / 2) {
                borderThickness = width / 2;
            } else if (borderThickness > height / 2) {
                borderThickness = height / 2;
            }

            float borderOffset = borderThickness / 2.0f;
            float adjustedX = originX + x + borderOffset;
            float adjustedY = originY - y - height + borderOffset;
            float adjustedWidth = width - borderThickness;
            float adjustedHeight = height - borderThickness;

            contentStream.setLineWidth(borderThickness);

            contentStream.addRect(adjustedX, adjustedY, adjustedWidth, adjustedHeight);

            contentStream.stroke();
        } catch (NumberFormatException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Draws a line on the canvas.
     *
     * @param x The x-coordinate of the starting point of the line.
     * @param y The y-coordinate of the starting point of the line.
     * @param width The horizontal length of the line.
     * @param height The vertical length of the line.
     * @param thickness The thickness of the line.
     * @param color The color of the line.
     */
    public void drawLine(int x, int y, int width, int height, int thickness, CustomColor color) {
        try {
            setStrokingColor(color);

            float adjustedX = originX + x;
            float adjustedY = originY - y;

            contentStream.setLineWidth(thickness);
            contentStream.moveTo(adjustedX, adjustedY);
            contentStream.lineTo(adjustedX + width, adjustedY - height);
            contentStream.stroke();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void setStrokingColor(CustomColor color) throws NumberFormatException, IOException {
        contentStream.setStrokingColor(
                Integer.parseInt(color.getHexadecimal().substring(1, 3), 16) / 255f,
                Integer.parseInt(color.getHexadecimal().substring(3, 5), 16) / 255f,
                Integer.parseInt(color.getHexadecimal().substring(5, 7), 16) / 255f
        );
    }
}

Usage:

PDFRenderer pdfRenderer = new PDFRenderer();

pdfRenderer.drawRectangle(50, 50, 100, 100, 100, CustomColortim.BLACK, 2);
pdfRenderer.drawLine(50, 500, 700, 3, 3, CustomColor.BLACK);
pdfRenderer.drawString("Hello, World!", 220, 195, 100, 30, CustomColor.BLACK, CustomFont.A);

Specific questions

  1. How can I modify the PDFRenderer class to implement XOR color inversion when drawing PDF elements like rectangles, lines, and text using Apache PDFBox 3.0.0 in Java?

  2. Are there any built-in functions or methods in PDFBox that support XOR mode for color manipulation?

  3. Can you provide code examples or suggestions for implementing XOR color inversion in this context?


Solution

  • A general problem with altering colours in a PDF is you cannot be certain what colour mode other viewers may render. So often I am asked why some viewers show a pattern as both cyan and green and yet others show single colour red and both can be as different shapes.

    Much of this is down to the complexity of continuous modified "Graphics States" and then an "Extended" setting added to that ! Here is one file, different PDF viewers, and NO, Acrobat is not the correct one. The correct one is top left. Top right is the simple inversion so all RGBW colours are shown as CMYK by pressing the invert button. Thus all "Pink Faces" turn "Smurf Blue" and a complaint that only some colours need changing, but then which simple way, can image pixels be filtered?.

    Acrobat at lower left can filter some limited types of object, so text has been specified as "Green", or the full range can be distorted for accessibility by setting the stop end colours. "Black is Red" "White is Green" as per lower right.

    However to do that, item by item, in a complex file will often result in missing objects, as some are naturally RGB and others CMYK. So every single object and running state needs analysis, so as not to be negated into oblivion.

    enter image description here