2
votes

I am writing a desktop WPF app to allow people to open up scanned PDF files, rotate them if need be, and then redact them if need be. The page is rendered to the screen using PDFium so the user can see what needs to be done. If it needs to be rotated, they click the rotate button to rotate it. If it needs to be redacted, they click on the appropriate button and then use the mouse to draw a System.Windows.Shapes.Rectangle on a Canvas. Then they click the save button to save the redaction (or redactions) to the pdf file. The actual changes to the PDF are made using PDFSharp v1.50.4000-beta3b downloaded through NuGet in Visual Studio 2013.

If the page is right side up, IE the rotate value is 0, then everything works fine. I can draw boxes all over the place with no problems. The issue arises when the rotate value is anything other than 0. If I rotate the page 90 degrees in either direction (rotate = 90 or -90), then when I try to draw the box on the page it messes things up. It seems to be swapping the height and width (turning it from landscape to portrait or vice versa) of the page without changing the content of the page. Then it draws the rectangle at the point it would be at if the page was rotated another 90 degrees.

To hopefully better demonstrate what I mean, here's an example: I've got a pdf page that is the standard size (A4, Letter, doesn't matter). It has a big smiley face on the top third of the file and text on the remainder and the rotate setting is 0 and the orientation is Portrait. I open it up in my program and rotate it 90 degrees. Now it is landscape and the smiley face is sideways on the right third of the page. I try and draw a box in the upper right corner of the page. When I click the save button, it changes the file and now it displays in portrait orientation however the content didn't change so now the Smiley face is invisible off of the right edge of the page. The box I had tried to place in the upper right corner acts as if it has been rotated and it is now in the lower right corner. If I make it a nice oblong rectangle, I can see that it really does look as though it's been rotated with the whole page but without the content. If I do it again, with another box in the upper right corner and then click save, it will swap the height and width again and rotate my box into a position 90 degrees off from where I placed it. Now I can see the smiley face again but the box still isn't where I want it.

Also, if the page rotation is 180, then when it saves the box, it some how rotates the position it's supposed to be in 180 degrees. So if my smiley face is upside down on the bottom of the page and I draw a box over his eyes (at the bottom of the page), it saves the box at the top of the page.

The weirdest part is that it was working perfectly a few weeks ago and now it isn't. From my testing, it appears as though the change is somehow being made in the PdfDocument.Save() method because before that point, the coordinates of the rectangle are what they should be for the current orientation/position of the page.

Anyways, now that I've explained the problem, here's my code.

First we have the code that handles the rotation. It's in a helper class and it stores the path to the file and a total page count. It takes in a list of page numbers to rotate. Also, I have to set the orientation to portrait (whether it should be or not) because PDFSharp is automatically setting that elsewhere but if I set it manually here, it rotates the pages properly and if I don't set it here, the page content will rotate without changing the size/orientation of the page itself.

public bool RotatePages(List<int> pageNums)
    {
        if (pageNums.Count > 0)
        {
            PdfDocument currDoc = PdfReader.Open(fullPath, PdfDocumentOpenMode.Modify);

            for (int i = 0; i < totalPageCount; i++)
            {
                PdfPage newPage = currDoc.Pages[i]; //newDoc.AddPage();

                if (pageNums.Contains(i))
                {
                    newPage.Orientation = PdfSharp.PageOrientation.Portrait;

                    newPage.Rotate = (newPage.Rotate + 90) % 360;
                }
            }

            currDoc.Save(fullPath);

            return true;
        }
        else
            return false;
    }

Next is the code to draw the redaction boxes. It takes in a list of System.Windows.Rect objects, a list of colors, a page number to mark, and a matrix. The matrix is because the pdf is rendered to an image but the rectangles are drawn by a user to a Canvas. The image can be zoomed in on or panned around and the matrix stores those transformations so that I can match the position of the rectangle on the Canvas to the appropriate point on the image/pdf. It works perfectly if the page rotation is 0.

public bool Redact(List<Rect> redactions, List<System.Windows.Media.Color> redactionColors, System.Windows.Media.Matrix matrix, int pageNum)
    {
        if (pageNum >= 0 && pageNum < totalPageCount && redactions.Count > 0 && redactions.Count == redactionColors.Count)
        {
            PdfDocument currDoc = PdfReader.Open(fullPath, PdfDocumentOpenMode.Modify);
            PdfPage newPage = currDoc.Pages[pageNum];
            XGraphics gfx = XGraphics.FromPdfPage(newPage);
            XBrush brush = null;

            for (int i = 0; i < redactions.Count; i++)
            {
                Rect redaction = redactions[i];
                System.Windows.Media.Color redactionColor = redactionColors[i];

                redaction.X = redaction.X / (matrix.OffsetX / newPage.Width);
                redaction.Y = redaction.Y / (matrix.OffsetY / newPage.Height);
                redaction.Width = redaction.Width / (matrix.OffsetX / newPage.Width);
                redaction.Height = redaction.Height / (matrix.OffsetY / newPage.Height);

                redaction.Width = redaction.Width / matrix.M11;
                redaction.Height = redaction.Height / matrix.M12;

                brush = new XSolidBrush(XColor.FromArgb(redactionColor.A, redactionColor));

                gfx.DrawRectangle(brush, redaction);
            }

            gfx.Save();
            currDoc.Save(fullPath);

            return true;
        }
        else
            return false;
    }

In the matrix (and no I'm not using it for matrix math, just using it to pass data around rather than using 6 ints/doubles, yes I am aware that it's lousy coding practice but fixing it is a rather low priority):

M11 = the x scale transform
M12 = the y scale transform
M21 = the x translate transform
M22 = the y translate transform
OffsetX = the actual width of the image control
OffsetY = the actual height of the image control

As near as I can tell by walking through step by step, my math and everything looks and works exactly as it should until currDoc.Save(fullPath); and then it magically gets the wrong values. If I break program execution at any time before that line, the actual file doesn't get a box but the moment it passes that line it messes up.

I have no idea what's going on here or how to fix it. It was previously working and I don't remember what I did to change it so it stopped working. I've been searching for a solution all day with no luck so far. Any help would be greatly appreciated.

2

2 Answers

3
votes

So I finally figured it out. Apparently PDFSharp has some issues with how it handles page rotation. To fix it, I first had to tweak the source code for PDFSharp.

I had to comment out the code that was swapping the height/width values when the page was set to landscape. Apparently PDFSharp uses a "orientation" variable to store the orientation despite the fact that PDFs don't have such a setting. By commenting out those lines, I finally started getting the right height and width for rotated pages. This is a change to PdfPage.cs.

public XUnit Height
    {
        get
        {
            PdfRectangle rect = MediaBox;
            //return _orientation == PageOrientation.Portrait ? rect.Height : rect.Width;
            return rect.Height;
        }
        set
        {
            PdfRectangle rect = MediaBox;
            //if (_orientation == PageOrientation.Portrait)
                MediaBox = new PdfRectangle(rect.X1, 0, rect.X2, value);
            //else
            //    MediaBox = new PdfRectangle(0, rect.Y1, value, rect.Y2);
            _pageSize = PageSize.Undefined;
        }
    }


public XUnit Width
    {
        get
        {
            PdfRectangle rect = MediaBox;
            //return _orientation == PageOrientation.Portrait ? rect.Width : rect.Height;
            return rect.Width;
        }
        set
        {
            PdfRectangle rect = MediaBox;
            //if (_orientation == PageOrientation.Portrait)
                MediaBox = new PdfRectangle(0, rect.Y1, value, rect.Y2);
            //else
            //    MediaBox = new PdfRectangle(rect.X1, 0, rect.X2, value);
            _pageSize = PageSize.Undefined;
        }
    }

Then I had to comment out a few lines in the WriteObject method that were flipping the height and width values of the mediabox. These are the lines I commented out. This stopped PDFSharp from flipping my rotated pages size every time I saved it.

        //// HACK: temporarily flip media box if Landscape
        //PdfRectangle mediaBox = MediaBox;
        //// TODO: Take /Rotate into account
        //if (_orientation == PageOrientation.Landscape)
        //    MediaBox = new PdfRectangle(mediaBox.X1, mediaBox.Y1, mediaBox.Y2, mediaBox.X2);

...

//if (_orientation == PageOrientation.Landscape)
        //    MediaBox

Lastly, in my own code I had to change most of the redaction code to put the boxes in the right spot at the right size. Getting the math right took forever and the code is messy but it works. Any suggestions on how to clean it up would be appreciated.

public bool Redact(List<Rect> redactions, List<System.Windows.Media.Color> redactionColors, System.Windows.Media.Matrix matrix, int pageNum)
    {
        if (pageNum >= 0 && pageNum < totalPageCount && redactions.Count > 0 && redactions.Count == redactionColors.Count)
        {
            PdfDocument currDoc = PdfReader.Open(fullPath, PdfDocumentOpenMode.Modify);
            int angle = currDoc.Pages[pageNum].Rotate;
            PdfPage oldPage = currDoc.Pages[pageNum];
            XBrush brush = null;
            XGraphics gfx = XGraphics.FromPdfPage(oldPage);
            XPoint pagePoint = new XPoint(0, 0);

            if (angle == 180)
            {
                pagePoint.X = oldPage.Width / 2;
                pagePoint.Y = oldPage.Height / 2;
                gfx.RotateAtTransform(180, pagePoint);
            }

            for (int i = 0; i < redactions.Count; i++)
            {
                Rect redaction = redactions[i];
                System.Windows.Media.Color redactionColor = redactionColors[i];
                double scaleValue = oldPage.Height / matrix.OffsetX;

                if (angle == 180 || angle == 0)
                {
                    redaction.X = redaction.X / (matrix.OffsetX / oldPage.Width);
                    redaction.Y = redaction.Y / (matrix.OffsetY / oldPage.Height);
                    redaction.Width = redaction.Width / (matrix.OffsetX / oldPage.Width);
                    redaction.Height = redaction.Height / (matrix.OffsetY / oldPage.Height);

                    redaction.Width = redaction.Width / matrix.M11;
                    redaction.Height = redaction.Height / matrix.M12;
                }
                else if (angle == 90 || angle == 270)
                {
                    Rect tempRect = redaction;

                    tempRect.X = redaction.X * scaleValue;
                    tempRect.Y = redaction.Y * scaleValue;

                    tempRect.Height = redaction.Height * scaleValue;
                    tempRect.Width = redaction.Width * scaleValue;

                    redaction.Width = tempRect.Height;
                    redaction.Height = tempRect.Width;

                    tempRect.Width = tempRect.Width / matrix.M11;
                    tempRect.Height = tempRect.Height / matrix.M12;

                    redaction.X = oldPage.Width - tempRect.Y - tempRect.Height;
                    redaction.Y = tempRect.X;

                    if (angle == 90)
                        gfx.RotateAtTransform(180, new XPoint(oldPage.Width / 2, oldPage.Height / 2));

                    redaction.Width = tempRect.Height;
                    redaction.Height = tempRect.Width;
                }

                brush = new XSolidBrush(XColor.FromArgb(redactionColor.A, redactionColor));
                gfx.DrawRectangle(brush, redaction);
            }

            gfx.Save();
            currDoc.Save(fullPath);

            return true;
        }
        else
            return false;
    }

So that's the solution I found that works for me.

-1
votes

This problem also persists when rotating page using Adobe Libraries, when you rotate a page and add annotations the annotations are all off. The easiest solution I have found is to rotate the page, extract the page, resave the individual page as a PDF/A and then then reinsert back into the pdf. By re-saving as a pdf/A it fixes all the underlying text coordinates on that page to the new rotation so that when you add annotations they are all where the should be. Unfortunately I have not found a way of saving an existing pdf to pdf/A using pdfshap.