Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prevent replacement annotation from changing position and text size?

Tags:

c#

pdf

itext

I'm using iTextSharp to replace Typewriter annotations with Textboxes that have the same content and position, but some of the resulting Textboxes end up in different positions with different text sizes, despite seemingly having exactly the same data in their hashMap.

The problem is that when I create these new annotations on this sample PDF, and then view the PDF in Adobe Acrobat XI, the new Textboxes have the following problems:

  • They have moved from their original positions (adjacent to the arrows) to lower on the page

  • The text has changed in size

  • There is no Properties available when the new Textbox is right-clicked

I suspect that all 3 problems are due to a single underlying issue with how I'm creating the new Textbox.

When I check the /Rect key in the hashMap for annot, it has the same rectangle coordinates in the same order as the original freeTextAnnot, so I don't understand why some of the annotations end up displaced.

Here's my code for creating the new Textboxes with the existing Typewriter annotation data. Note that you'll need to set inputPath and outputPath to the actual location of the PDF and its destination path:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using iTextSharp;
using iTextSharp.text.pdf;
using System.IO;

namespace PDFConverterTester
{
    public class PdfModifierTester
    {
        public void testWithPaths()
        {
            //set to location of test PDF
            string inputPath = @"C:\InputPath\Before Conversion Dummy.pdf";
            //set to destination of new PDF
            string outputPath = @"C:\OutputPath\Before Conversion Dummy.pdf";
            test(inputPath, outputPath);
        }

        public void test(string inputPath, string outputPath)
        {
            PdfReader pdfReader = new PdfReader(inputPath);
            PdfStamper pdfStamper = new PdfStamper(pdfReader, new FileStream(outputPath, FileMode.Create));
            //get the PdfDictionary of the 1st page
            PdfDictionary pageDict = pdfReader.GetPageN(1);
            //get annotation array
            PdfArray annotArray = pageDict.GetAsArray(PdfName.ANNOTS);
            //iterate through annotation array
            int size = annotArray.Size;
            for (int i = size - 1; i >= 0; i--)
            {
                PdfDictionary dict = annotArray.GetAsDict(i);
                PdfName ITName = dict.GetAsName(new PdfName("IT"));
                if (ITName != null)
                {
                    if (ITName.Equals(new PdfName("FreeTextTypewriter")))
                    {
                        PdfAnnotation annot = copyToNewAnnotation_SSCCE(dict,pdfStamper);
                        pdfStamper.AddAnnotation(annot, 1);
                        annotArray.Remove(i);
                    }
                }
            }
            pdfStamper.Close();
            pdfReader.Close();
        }

        private PdfAnnotation copyToNewAnnotation_SSCCE(PdfDictionary freeTextAnnot,PdfStamper pdfStamper)
        {

            //need Rectangle for CreateFreeText()
            iTextSharp.text.Rectangle rect;
            PdfArray rectArray = freeTextAnnot.GetAsArray(PdfName.RECT);
            if (rectArray == null)
            {
                rect = null;
            }
            else
            {
                rect = new iTextSharp.text.Rectangle(getFloat(rectArray, 0),
                                                                          getFloat(rectArray, 1),
                                                                          getFloat(rectArray, 2),
                                                                          getFloat(rectArray, 3));
            }
            //create new annotation
            PdfContentByte pcb = new PdfContentByte(pdfStamper.Writer);
            PdfAnnotation annot = PdfAnnotation.CreateFreeText(pdfStamper.Writer, rect, "", pcb);
            //make array of all possible PdfName keys in dictionary for freeTextAnnot
            string pdfNames = "AP,BS,C,CA,CL,CONTENTS,CREATIONDATE,DA,DS,F,IT,LE,M,NM,P,POPUP,Q,RC,RD,ROTATE,SUBJ,SUBTYPE,T,TYPE";
            string[] pdfNameArray = pdfNames.Split(',');
            //iterate through key array copying key-value pairs to new annotation
            foreach (string pdfName in pdfNameArray)
            {
                //get value for this PdfName
                PdfName key = new PdfName(pdfName);
                PdfObject obj = freeTextAnnot.Get(key);
                //if returned value is null, maybe key is case-sensitive
                if (obj == null)
                {
                    //try with first letter only capitalized, e.g. "Contents" instead of "CONTENTS"
                    string firstCappdfName = char.ToUpper(pdfName[0]) + pdfName.Substring(1);
                    key = new PdfName(firstCappdfName);
                    obj = freeTextAnnot.Get(key);
                }
                //set key-value pair in new annotation
                annot.Put(key, obj);
            }
            //change annotation type to Textbox
            annot.Put(PdfName.SUBTYPE, PdfName.FREETEXT);
            annot.Put(new PdfName("Subj"), new PdfString("Textbox"));

            //set a default blank border
            annot.Put(PdfName.BORDER, new PdfBorderArray(0, 0, 0));

            return annot;


        }

        private float getFloat(PdfArray arr, int index)
        {
            return float.Parse(arr[index].ToString());
        }
    }
}

EDIT: It seems that part of the problem may be in the call to pdfStamper.AddAnnotation(annot,1), because annot's values for the /Rect key change after this call is made. E.g.:

before AddAnnotation() call:

{[2401, 408.56, 2445.64, 693]}

after call:

{[1899, 2445.64, 2183.44, 2401]}

So maybe the following code from PdfStamper.AddAnnotation() (link to source), lines 1463-1493, is responsible, I'm currently investigating this possibility:

if (!annot.IsUsed()) {
                        PdfRectangle rect = (PdfRectangle)annot.Get(PdfName.RECT);
                        if (rect != null && (rect.Left != 0 || rect.Right != 0 || rect.Top != 0 || rect.Bottom != 0)) {
                            int rotation = reader.GetPageRotation(pageN);
                            Rectangle pageSize = reader.GetPageSizeWithRotation(pageN);
                            switch (rotation) {
                                case 90:
                                    annot.Put(PdfName.RECT, new PdfRectangle(
                                        pageSize.Top - rect.Top,
                                        rect.Right,
                                        pageSize.Top - rect.Bottom,
                                        rect.Left));
                                    break;
                                case 180:
                                    annot.Put(PdfName.RECT, new PdfRectangle(
                                        pageSize.Right - rect.Left,
                                        pageSize.Top - rect.Bottom,
                                        pageSize.Right - rect.Right,
                                        pageSize.Top - rect.Top));
                                    break;
                                case 270:
                                    annot.Put(PdfName.RECT, new PdfRectangle(
                                        rect.Bottom,
                                        pageSize.Right - rect.Left,
                                        rect.Top,
                                        pageSize.Right - rect.Right));
                                    break;
                            }
                        }
                    }
                }
like image 879
sigil Avatar asked Feb 05 '23 14:02

sigil


1 Answers

The cause

You have yourself found the cause for the change of position and dimension of the annotation:

It seems that part of the problem may be in the call to pdfStamper.AddAnnotation(annot,1), because annot's values for the /Rect key change after this call is made.

... code from PdfStamper.AddAnnotation() (link to source), lines 1463-1493, is responsible

Indeed, that code changes the annotation rectangle if the page is rotated.

The rationale behind this is that for rotated pages iText attempts to lift the burden of adding the rotation and translation to page content required to draw upright text and have the coordinate system origin in the lower left of the page of the users' shoulders, so that the users don't have to deal with page rotation at all. Consequently, it also does so for annotations.

For page content there is a PdfStamper property RotateContents defaulting to true which allows to switch this off if one explicitly does not want this rotation and translation. Unfortunately there isn't a similar property for annotations, their positions and sizes always get "corrected", i.e. rotated and translated.

A work-around

As the page rotation is the trigger for iText to rotate and translate the rectangle, one can simply remove the page rotation before manipulating the annotations and add it again later:

PdfDictionary pageDict = pdfReader.GetPageN(1);
// hide the page rotation
PdfNumber rotation = pageDict.GetAsNumber(PdfName.ROTATE);
pageDict.Remove(PdfName.ROTATE);
//get annotation array
PdfArray annotArray = pageDict.GetAsArray(PdfName.ANNOTS);
//iterate through annotation array
int size = annotArray.Size;
for (int i = size - 1; i >= 0; i--)
{
    ...
}
// add page rotation again if required
if (rotation != null)
    pageDict.Put(PdfName.ROTATE, rotation);
pdfStamper.Close();

With this addition the annotations stay in place.

The missing Properties

You also observed:

There is no Properties available when the new Textbox is right-clicked

This is because you have not changed the intent (IT) entry, so they still contained FreeTextTypewriter, so Adobe Reader is not sure what kind of object that is and, therefore, offers no Properties dialog. If you also change the intent:

//change annotation type to Textbox
annot.Put(PdfName.SUBTYPE, PdfName.FREETEXT);
annot.Put(new PdfName("IT"), PdfName.FREETEXT); // <======
annot.Put(new PdfName("Subj"), new PdfString("Textbox"));

you'll get the Properties dialog.

As an aside

Your method getFloat first caused weirdest changes in coordinate systems for me because my locale does not use dots as decimal separator.

I changed it to this to make it locale-independent:

private float getFloat(PdfArray arr, int index)
{
    return arr.GetAsNumber(index).FloatValue;
}

An alternative approach

Is there a specific reason why you replace the original annotation instead of simply editing it? E.g.:

public void AlternativeReplaceFreetextByTextbox(string InputPath, string OutputPath)
{
    PdfName IT = new PdfName("IT");
    PdfName FREETEXTTYPEWRITER = new PdfName("FreeTextTypewriter");
    using (PdfReader Reader = new PdfReader(InputPath))
    {
        PdfDictionary Page = Reader.GetPageN(1);
        PdfArray Annotations = Page.GetAsArray(PdfName.ANNOTS);
        foreach (PdfObject Object in Annotations)
        {
            PdfDictionary Annotation = (PdfDictionary)PdfReader.GetPdfObject(Object);
            PdfName Intent = Annotation.GetAsName(IT);
            if (FREETEXTTYPEWRITER.Equals(Intent))
            {
                // change annotation type to Textbox
                Annotation.Put(IT, PdfName.FREETEXT);
            }
        }

        using (PdfStamper Stamper = new PdfStamper(Reader, new FileStream(OutputPath, FileMode.Create)))
        { }
    }

}
like image 57
mkl Avatar answered Feb 08 '23 15:02

mkl