Search code examples
javapdfpdfbox

Problems determining image positioning using PDFBox


Context:

I have two images that I rotated 90 degrees (counterclockwise) and converted to PDF. I used two different tools to convert the images:

  • Document A: Mac Preview
  • Document B: ImageMagick

Visually, the documents appear exactly the same using a PDF client.

Problem:

I need to find the placement information of the images relative to the PDF page. My code correctly identifies the positioning for one document but is wrong for the other:

  • Document A: Rectangle[x=792.0,y=0.0,w=614.4000244140625,h=792.0]
  • Document B: Rectangle[x=0.0,y=0.0,w=792.0,h=614.4000244140625]

Visualizing the rectangle information, you can see that Document A (red) is not rotated and too far over on the x axis:

Rectangle Positions

Details:

Using the PDFBox debugger, I can see that Document A's image is rotated via CM operations:

q
Q
q
  /Perceptual ri
  q
    0 614.4 -792 0 792 0 cm
    /Im1 Do
  Q
Q

It appears that I'm not properly accounting for the rotation.

Code:

Matrix matrix = getGraphicsState().getCurrentTransformationMatrix();
Rectangle2D.Double rectangle = new Rectangle2D.Double(
        matrix.getTranslateX(),
        matrix.getTranslateY(),
        matrix.getScalingFactorX(),
        matrix.getScalingFactorY()
);

Attempted Solution

Now I can get the rotation degrees from the matrix:

    protected int getRotation() {
        Matrix matrix = getGraphicsState().getCurrentTransformationMatrix();
        double rotationRadians = -Math.atan2(matrix.getShearY(), matrix.getScaleY());
        return (int) Math.toDegrees(rotationRadians);
    }

And then rotate the rectangle:

    public Rectangle2D.Double rotate(Rectangle2D rectangle, int rotationDegrees) {
        double x = rectangle.getX() + rectangle.getWidth();
        double y = rectangle.getY();
        
        AffineTransform rotationTransform = new AffineTransform();
        rotationTransform.rotate(Math.toRadians(rotationDegrees), x, y);
        Shape rotatedRectangle = rotationTransform.createTransformedShape(rectangle);
        Rectangle2D bounds = rotatedRectangle.getBounds2D();
        return (Rectangle2D.Double) bounds;
    }

Now it's in the right orientation (green rectangle) but it needs to slide over:

Rotated Rectangle Positions

I could adjust the new rectangle's x positioning by subtracting the sum of the original and rotated widths. It feels like maybe I might start getting into brittle territory here. I also feel like I'm probably starting to re-invent a wheel.

Is there a better way to solve this problem?

Update:

After further testing, this solution needs adjustments for other angles. I've handled 90, 180, 270. Thankfully we don't usually run across odd angles but I'm a bit concerned when we do.


Solution

  • In a comment you mentioned that the coordinates of the 4 corners would be all you need.

    You can extract the default user space coordinates of the bitmap images used in a PDF by walking the page content stream instructions and, when finding a bitmap image drawing instruction, applying the then current transformation matrix to the corners of the unit square, (0, 0), (0, 1), (1, 0), and (1, 1).

    You can implement that using a specialized PDFStreamEngine like this:

    public class UserSpaceCoordinatesPrinter extends PrintImageLocations {
        public UserSpaceCoordinatesPrinter() throws IOException {
            super();
        }
    
        @Override
        protected void processOperator(Operator operator, List<COSBase> operands) throws IOException {
            String operation = operator.getName();
            if (OperatorName.DRAW_OBJECT.equals(operation)) {
                COSName objectName = (COSName) operands.get(0);
                PDXObject xobject = getResources().getXObject(objectName);
                if (xobject instanceof PDImageXObject) {
                    System.out.println("\nFound image [" + objectName.getName() + "]");
                    Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
                    System.out.print("Corner coordinates in user space:");
                    for (Vector corner : UNIT_SQUARE_CORNERS) {
                        System.out.print(" " + ctm.transform(corner));
                    }
                    System.out.println();
                } else if(xobject instanceof PDFormXObject) {
                    PDFormXObject form = (PDFormXObject)xobject;
                    showForm(form);
                }
            } else {
                super.processOperator(operator, operands);
            }
        }
    
        public static void print(PDDocument document) throws IOException {
            UserSpaceCoordinatesPrinter printer = new UserSpaceCoordinatesPrinter();
            int pageNum = 0;
            for (PDPage page : document.getPages()) {
                pageNum++;
                System.out.println("\n\nProcessing page: " + pageNum);
                System.out.println("Media box: " + page.getMediaBox());
                System.out.println("Page rotation: " + page.getRotation());
                printer.processPage(page);
            }
        }
    
        final static List<Vector> UNIT_SQUARE_CORNERS = List.of(
                new Vector(0, 0), new Vector(0, 1), new Vector(1, 0), new Vector(1, 1));
    }
    

    (PrintImageCornerCoordinates utility class)

    You can use it like this:

    try (PDDocument document = Loader.loadPDF(PDF_SOURCE)) {
        UserSpaceCoordinatesPrinter.print(document);
    }
    

    (PrintImageCornerCoordinates test testUserCoordinatesInput)