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;
}
}
}
}
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)
, becauseannot
'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.
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.
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.
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;
}
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)))
{ }
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With