0

I have a pdf that is rendered outside of iText in SSRS. The report comes as separate PDFs and I use iText to apply some further functionality e.g. merging the pdf's into one, adding password encrypton etc.

I am trying to add a watermark onto each page of the pdf like so:

public string MergeReports(ConcurrentDictionary<int, string> base64Reports, string password = null, string watermark = null)
{
    try
    {
        var passwordBytes = password != null ? Encoding.ASCII.GetBytes(password) : null;

        // Creating new PdfDocument with smart mode on which maintains formatting and styling.
        using (var ms = new MemoryStream())
        using (var mergedPdf = new PdfDocument(new PdfWriter(ms, 
            new WriterProperties().SetStandardEncryption(passwordBytes, passwordBytes, EncryptionConstants.ALLOW_PRINTING,
                EncryptionConstants.ENCRYPTION_AES_128 | EncryptionConstants.DO_NOT_ENCRYPT_METADATA)).SetSmartMode(true)))
        {
            foreach (var report in base64Reports.OrderBy(x => x.Key))
            {
                var (_, base64String) = report;
                var bytes = Convert.FromBase64String(base64String);

                using (var memoryStream = new MemoryStream(bytes))
                {
                    using (PdfReader reader = new PdfReader(memoryStream))
                    {
                        using (var pdfDoc = new PdfDocument(reader))
                        {
                            if (watermark != null)
                                AddWatermark(watermark, pdfDoc);

                            pdfDoc.CopyPagesTo(1, pdfDoc.GetNumberOfPages(), mergedPdf);
                            pdfDoc.Close();
                        }
                    }
                }
            }
            
            mergedPdf.Close();

            return Convert.ToBase64String(ms.ToArray());
        }
    }
    catch (Exception ex)
    {
        throw new PdfException($"Unable to merge PDF reports: {ex.Message}");
    }
}
private void AddWatermark(string watermark, PdfDocument pdfDoc)
{
    float watermarkTrimmingRectangleWidth = 300;
    float watermarkTrimmingRectangleHeight = 300;

    float formWidth = 300;
    float formHeight = 300;
    float formXOffset = 0;
    float formYOffset = 0;

    float xTranslation = 50;
    float yTranslation = 25;

    double rotationInRads = Math.PI / 3;
    var fontProgram = FontProgramFactory.CreateFont(_configuration.GetSection("FontFiles")["Calibri"]);
    PdfFont font = PdfFontFactory.CreateFont(fontProgram); 
    
    float fontSize = 150;

    var numberOfPages = pdfDoc.GetNumberOfPages();
    PdfPage page = null;

    for (var i = 1; i <= numberOfPages; i++)
    {
        page = pdfDoc.GetPage(i);
        Rectangle ps = page.GetPageSize();

        //Center the annotation
        float bottomLeftX = ps.GetWidth() / 2 - watermarkTrimmingRectangleWidth / 2;
        float bottomLeftY = ps.GetHeight() / 2 - watermarkTrimmingRectangleHeight / 2;
        Rectangle watermarkTrimmingRectangle = new Rectangle(bottomLeftX, bottomLeftY, watermarkTrimmingRectangleWidth, watermarkTrimmingRectangleHeight);

        PdfWatermarkAnnotation pdfWatermark = new PdfWatermarkAnnotation(watermarkTrimmingRectangle);

        //Apply linear algebra rotation math
        //Create identity matrix
        AffineTransform transform = new AffineTransform();//No-args constructor creates the identity transform
                                                          //Apply translation
        transform.Translate(xTranslation, yTranslation);
        //Apply rotation
        transform.Rotate(rotationInRads);

        PdfFixedPrint fixedPrint = new PdfFixedPrint();
        pdfWatermark.SetFixedPrint(fixedPrint);
        //Create appearance
        Rectangle formRectangle = new Rectangle(formXOffset, formYOffset, formWidth, formHeight);

        //Observation: font XObject will be resized to fit inside the watermark rectangle
        PdfFormXObject form = new PdfFormXObject(formRectangle);
        PdfExtGState gs1 = new PdfExtGState().SetFillOpacity(0.6f);
        PdfCanvas canvas = new PdfCanvas(form, pdfDoc);

        float[] transformValues = new float[6];
        transform.GetMatrix(transformValues);
        canvas.SaveState()
            .BeginText().SetColor(ColorConstants.GRAY, true).SetExtGState(gs1)
            .SetTextMatrix(transformValues[0], transformValues[1], transformValues[2], transformValues[3], transformValues[4], transformValues[5])
            .SetFontAndSize(font, fontSize)
            .ShowText(watermark)
            .EndText()
            .RestoreState();

        canvas.Release();

        pdfWatermark.SetAppearance(PdfName.N, new PdfAnnotationAppearance(form.GetPdfObject()));
        pdfWatermark.SetFlags(PdfAnnotation.PRINT);

        page.AddAnnotation(pdfWatermark);

    }
    page?.Flush();
}

I found the watermarking code from this stackoverflow answer and had to modify it a little to match my implementation.

When debugging, stepping through it seems to throw an exception on canvas.SaveState(..)

The exception I get is

((iText.Kernel.PdfException)ex).Message: Document has no pages.

Stacktrace

at iText.Kernel.Pdf.PdfPagesTree.GenerateTree() at iText.Kernel.Pdf.PdfDocument.Close() at iText.Kernel.Pdf.PdfDocument.System.IDisposable.Dispose() at RenderService.Services.PDFMerger.MergeReports(ConcurrentDictionary`2 base64Reports, String password, String watermark) in D:_src\render-service\Solutions\Render Service\Source\RenderService\Services\PDFMerger.cs:line 66

Adding this because it might be relevant but I was having an issue with the iText7 library when it was trying to create a font. The error

System,NullReferenceException Object reference not set to an instance of an object exception

at this line:

PdfFont font = PdfFontFactory.CreateFont(fontProgram);

I'm still having this error but ignored this exception when thrown from the itext.io.dll as mentioned in a comment on the question thread.

link to above issue

Usman Khan
  • 676
  • 1
  • 7
  • 20

1 Answers1

1

There is more than one problem in your question but to the resolve the issue with adding watermark you have to create another PdfWriter because you are editing pdf:

using (var destMemoryStream = new MemoryStream())
{
    using (var memoryStream = new MemoryStream(bytes))
    {
        using (PdfReader reader = new PdfReader(memoryStream))
        {
            using (PdfWriter pdfWriter = new PdfWriter(destMemoryStream))
            {
                using (var pdfDoc = new PdfDocument(reader, pdfWriter))
                {
                    if (watermark != null)
                        AddWatermark(watermark, pdfDoc);

                    pdfDoc.CopyPagesTo(1, pdfDoc.GetNumberOfPages(), mergedPdf);
                    pdfDoc.Close();
                }
            }
        }
    }
}

I'm not sure if is necessary to call page?.Flush() in your AddWatermark and if merging will work. You are trying to copy pdf pages to empty pdf document.

user2250152
  • 14,658
  • 4
  • 33
  • 57
  • Is it necessary to have a different dest memory stream? Can I not just use `memoryStream` in the `PdfWriter(memoryStream);` – Usman Khan Jan 21 '21 at 15:03
  • @UsmanKhan It makes sense for me to use just one PdfWriter and destination memory stream but I didn't test it. – user2250152 Jan 21 '21 at 15:15
  • 1
    Figured it out, thanks for the help. I used the `PdfWriter` I declared for the `mergedPdf` and moved my call to `AddWatermark` outside of the foreach. – Usman Khan Jan 21 '21 at 15:28