Search code examples
c#pdfrotationitext7stamp

iTextsharp 7: Rotating a Stamp or Annotation


A lot of questions I found regarding rotations dealt with Page rotation - and at best, image rotation. How exactly are objects such as Rectangles and stamp annotations rotated via iText?

I initially expected there will be a high level method such as Rectangle.SetRotation(), but that did not exist. Attempting to only use Image.SetRotation (float) has no effect on the orientation of the stamp - which leads me to believe it's all about rotating the FormXObject rectangle.

The stamping code for the sake of context:

ImageData img = ImageDataFactory.Create(imgsrc);

float iWidth = img.GetWidth();
float iHeight = img.GetHeight();
if (crop.GetWidth() > crop.GetHeight())
{
    w = crop.GetWidth();
    h = crop.GetHeight();
}
else
{
    w = crop.GetHeight();
    h = crop.GetWidth();
}


//Adjust to Page in Future Code
Rectangle location = new Rectangle(crop.GetLeft(),crop.GetBottom(),iWidth/4,iHeight/4);


PdfStampAnnotation stamp = new PdfStampAnnotation(location).SetStampName(new PdfName("#Logo"));

PdfFormXObject xObj = new PdfFormXObject(new Rectangle(iWidth, iHeight));
PdfCanvas canvas = new PdfCanvas(xObj, pdfDoc);
canvas.AddImage(img, 0, 0,iWidth, false);
stamp.SetNormalAppearance(xObj.GetPdfObject());



stamp.SetFlags(PdfAnnotation.PRINT);
stamp.SetFlags(PdfAnnotation.INVISIBLE);

pdfDoc.GetFirstPage().AddAnnotation(stamp);
pdfDoc.Close();

Thanks in advance...


Solution

  • I created the test methods using Java and iText 7 for Java; they should be trivial to port to C# and iText 7 for .Net, though, mainly starting methods with uppercase letters instead of lowercase ones.

    There essentially are two ways to create a stamp annotation whose content appears rotated:

    • Set the current transformation matrix inside the appearance stream to rotate everything you add.
    • Add a rotation Matrix entry to the appearance XObject of the annotation.

    Using the CTM

    As your annotation content only consists of a bitmap image, one can use an PdfCanvas.addImage overload which allows setting the CTM for inserting the image:

    ImageData imageData = ImageDataFactory.create(ByteStreams.toByteArray(imageStream));
    float iWidth = imageData.getWidth();
    float iHeight = imageData.getHeight();
    
    Rectangle crop = pdfDocument.getFirstPage().getCropBox();
    // The content image of the annotation shall be rotated, so switch width and height
    Rectangle location = new Rectangle(crop.getLeft(), crop.getBottom(), iHeight/4, iWidth/4);
    
    PdfStampAnnotation stamp = new PdfStampAnnotation(location).setStampName(new PdfName("#Logo"));
    
    // The content image in the appearance shall be rotated, so switch width and height
    PdfFormXObject xObj = new PdfFormXObject(new Rectangle(iHeight, iWidth));
    PdfCanvas canvas = new PdfCanvas(xObj, pdfDocument);
    // Insert image using rotation transformation matrix
    canvas.addImage(imageData, 0, iWidth, -iHeight, 0, iHeight, 0);
    stamp.setNormalAppearance(xObj.getPdfObject());
    
    stamp.put(PdfName.Type, PdfName.Annot);
    stamp.setFlags(PdfAnnotation.PRINT);
    
    pdfDocument.getFirstPage().addAnnotation(stamp);
    

    (AddRotatedAnnotation test testRotateImage)

    Using the appearance Matrix

    This is even more simple than the above:

    ImageData imageData = ImageDataFactory.create(ByteStreams.toByteArray(imageStream));
    float iWidth = imageData.getWidth();
    float iHeight = imageData.getHeight();
    
    Rectangle crop = pdfDocument.getFirstPage().getCropBox();
    // The appearance (with the upright image) of the annotation shall be rotated, so switch width and height
    Rectangle location = new Rectangle(crop.getLeft(), crop.getBottom(), iHeight/4, iWidth/4);
    
    PdfStampAnnotation stamp = new PdfStampAnnotation(location).setStampName(new PdfName("#Logo"));
    
    // The content image in the appearance shall be upright, so don't switch width and height
    PdfFormXObject xObj = new PdfFormXObject(new Rectangle(iWidth, iHeight));
    // The appearance shall be rotated
    xObj.put(PdfName.Matrix, new PdfArray(new int[]{0, 1, -1, 0, 0, 0}));
    PdfCanvas canvas = new PdfCanvas(xObj, pdfDocument);
    // Insert upright image
    canvas.addImage(imageData, 0, 0, iWidth, false);
    stamp.setNormalAppearance(xObj.getPdfObject());
    
    stamp.put(PdfName.Type, PdfName.Annot);
    stamp.setFlags(PdfAnnotation.PRINT);
    
    pdfDocument.getFirstPage().addAnnotation(stamp);
    

    (AddRotatedAnnotation test testRotateMatrix)

    Background: the appearance Matrix

    In a comment you stated

    it works so beautifully with the annotation matrix. If you don't mind, would you care to explain the significance of the Put method?

    That put call really only adds an entry with key Matrix and the given matrix as value to the annotation appearance dictionary.

    But this suffices as the PDF specification requires

    The algorithm outlined in this sub-clause shall be used to map from the coordinate system of the appearance XObject (as defined by its Matrix entry; see Table 97) to the annotation’s rectangle in default user space:

    Algorithm: Appearance streams

    a) The appearance’s bounding box (specified by its BBox entry) shall be transformed, using Matrix, to produce a quadrilateral with arbitrary orientation. The transformed appearance box is the smallest upright rectangle that encompasses this quadrilateral.

    b) A matrix A shall be computed that scales and translates the transformed appearance box to align with the edges of the annotation’s rectangle (specified by the Rect entry). A maps the lower-left corner (the corner with the smallest x and y coordinates) and the upper-right corner (the corner with the greatest x and y coordinates) of the transformed appearance box to the corresponding corners of the annotation’s rectangle.

    c) Matrix shall be concatenated with A to form a matrix AA that maps from the appearance’s coordinate system to the annotation’s rectangle in default user space:

    AA = Matrix * A

    (ISO 32000-1, section 12.5.5 "Appearance Streams")

    Thus, whenever an annotation with appearance stream is rendered, the appearance is transformed by Matrix and the result thereof is squeezed and/or stretched to fit the annotation rectangle.