Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there a replacement for onEndPage(...) to add a Watermark for every page without an additional loop through the pages #472

Closed
cba-manitz opened this issue Apr 30, 2020 · 11 comments

Comments

@cba-manitz
Copy link

Hello,

We are currently in the process of rebuilding our PDF generator with Open HTML to PDF.
Formerly we were using the Flying Saucer library. The underlying IText has the functionality
to set a page event.
We used that to print a string as a watermark on every page during creation of
the pdf.

old version:

renderer.getWriter().setPageEvent( new PdfPageEventHelper() {

    Font FONT = new Font(Font.HELVETICA, 72.0f, Font.BOLD, new GrayColor(0.85f));

    @Override
    public void onEndPage(PdfWriter writer, com.lowagie.text.Document document) {
        PdfCreator.log.info("onEndPage called");

        PdfContentByte content = writer.getDirectContent();
        content.saveState();

        PdfGState gs1 = new PdfGState();
        gs1.setFillOpacity(0.5f);
        content.setGState(gs1);
        
        Rectangle rectSize = content.getPdfDocument().getPageSize();
        
        int watermarkRotation = 60;
        if (rectSize.getWidth() > rectSize.getHeight()) { // Landscape
            watermarkRotation = 30;
        } 
        float watermarkXPosition = rectSize.getWidth() / 2;
        float watermarkYPosition = rectSize.getHeight() / 2;

        ColumnText.showTextAligned( content,
                                    Element.ALIGN_CENTER,
                                    new Phrase("Watermark", FONT),
                                    watermarkXPosition,
                                    watermarkYPosition,
                                    watermarkRotation );

        content.restoreState();
    }
});

Unfortunately we weren't able to find such functionality with Open HTML to PDF and PDFBox.
So we came up with our current solution that takes the final document (after the conversion process)
and iterates through all pages once again. There it adds the watermark.

current method:

private void printTextWatermark(PDDocument document) throws IOException {
            
    PDFont font = PDType1Font.HELVETICA_BOLD;
    float fontSize = 80.0f;
    Color color = new Color(216, 216, 216);
    PDExtendedGraphicsState r0 = new PDExtendedGraphicsState();
    float textWidth = font.getStringWidth(watermarkText) / 1000 * fontSize;
    AppendMode appendmode;
    
    if(this.watermarkTextForeground) {
        appendmode = AppendMode.APPEND;
        r0.setNonStrokingAlphaConstant(0.5f);
    } else {
        appendmode = AppendMode.PREPEND;
        r0.setNonStrokingAlphaConstant(1f);
    }
    
    for(PDPage page : document.getPages()) {
        
        float height = page.getMediaBox().getHeight();
        float width = page.getMediaBox().getWidth();

        double beta = Math.atan(height / width);
        double alpha = Math.toRadians(180.0) - beta - Math.toRadians(90);
        double a = textWidth * Math.sin(alpha);
        double b = textWidth * Math.cos(alpha);

        Matrix textMatrix = Matrix.getRotateInstance(beta, (float) ((width - a)/2), (float) ((height-b)/2) - fontSize/2);
        
        PDPageContentStream contentStream = new PDPageContentStream(document, page, appendmode, true);
        contentStream.setGraphicsStateParameters(r0);
        contentStream.setNonStrokingColor(color);
        contentStream.beginText();
        contentStream.setFont(font, fontSize);
        contentStream.setTextMatrix(textMatrix);
        contentStream.showText(watermarkText);
        contentStream.endText();
        contentStream.close();
    }
}

Despite this working kind of well our problem with this solution is that it needs to loop all pages again.
This might become very time and memory consuming when it comes to converting a document with a vast number of pages.
We would prefere a solution that adds the watermark during the conversion where the PDF pages are touched the first time.

What would you suggest is the preferred way to achieve such a behaviour?

Thanks

danfickle added a commit that referenced this issue May 9, 2020
…iple pages.

This test is to make sure we don't break this functionality while trying to fix custom object drawer in fixed position.
danfickle added a commit that referenced this issue May 9, 2020
…tiple page documents.

This confirms that the issue for fixed position elements being in the wrong place in the output is confined to custom object drawers.
danfickle added a commit that referenced this issue May 9, 2020
Otherwise, an environment font may be embedded, breaking the test on different platforms.
danfickle added a commit that referenced this issue May 9, 2020
… right.

Forgot to commit the actual test proof!
@rototor
Copy link
Contributor

rototor commented May 14, 2020

You can do such stuff using the ObjectDrawer (which are still underdocumentated...). I added some explanation in #475.

I did a ObjectDrawer in the objects package that just puts on every page a background PDF. After registering the ObjectDrawer Factory from the objects package you can just place this object in the page footer or header, so that it gets drawn every page:

	<object type="pdf/background" pdfsrc="background.pdf" style="width:1px;height:1px"></object>

If you would like to generate the contents dynamic instead of using a predefined PDF you can just take MergeBackgroundPdfDrawer.java as a base. For registering custom drawers see #475.

The Map/SoftReference stuff I do there is just to try to reuse the background PDF, which is imported as XForm, on every page. So that it gets not duplicated all the time.

@sillen102
Copy link

sillen102 commented Oct 14, 2020

You can do such stuff using the ObjectDrawer (which are still underdocumentated...). I added some explanation in #475.

I did a ObjectDrawer in the objects package that just puts on every page a background PDF. After registering the ObjectDrawer Factory from the objects package you can just place this object in the page footer or header, so that it gets drawn every page:

	<object type="pdf/background" pdfsrc="background.pdf" style="width:1px;height:1px"></object>

If you would like to generate the contents dynamic instead of using a predefined PDF you can just take MergeBackgroundPdfDrawer.java as a base. For registering custom drawers see #475.

The Map/SoftReference stuff I do there is just to try to reuse the background PDF, which is imported as XForm, on every page. So that it gets not duplicated all the time.

How do you get it to render on every page? I can only get it to render on the first page. The problem is that I don't know in advance how long the PDF will be since I'm using a template and populate the predefined tags with data. For that I use jsoup to insert data into a jsoup Document and then feed it to the renderer with this code:

PdfRendererBuilder builder = new PdfRendererBuilder();
builder.useFastMode();

MergeBackgroundPdfDrawer pdfDrawer = new MergeBackgroundPdfDrawer();
StandardObjectDrawerFactory objectDrawerFactory = new StandardObjectDrawerFactory();
objectDrawerFactory.registerDrawer("pdf/background", pdfDrawer);
builder.useObjectDrawerFactory(objectDrawerFactory);

builder.withHtmlContent(doc.html(), uri);
builder.toStream(os);
builder.run();

This is how my HTML starts:

<div id="invoice">
    <div class="invoice" style="height: 750px;-fs-page-break-min-height:400px">
        <object type="pdf/background"
                pdfsrc="background.pdf"
                style="width:1px;height:1px;-fs-page-break-min-height:400px">
        </object>

Also is there a way to bring it to the front? I would like to add a watermark and for that I would like it to be on the front transparent.

I have also tried to add an image as watermark and adding it to the first page is fine. But how do I make it be added to every page?

@sillen102
Copy link

sillen102 commented Oct 14, 2020

current method:

private void printTextWatermark(PDDocument document) throws IOException {
            
    PDFont font = PDType1Font.HELVETICA_BOLD;
    float fontSize = 80.0f;
    Color color = new Color(216, 216, 216);
    PDExtendedGraphicsState r0 = new PDExtendedGraphicsState();
    float textWidth = font.getStringWidth(watermarkText) / 1000 * fontSize;
    AppendMode appendmode;
    
    if(this.watermarkTextForeground) {
        appendmode = AppendMode.APPEND;
        r0.setNonStrokingAlphaConstant(0.5f);
    } else {
        appendmode = AppendMode.PREPEND;
        r0.setNonStrokingAlphaConstant(1f);
    }
    
    for(PDPage page : document.getPages()) {
        
        float height = page.getMediaBox().getHeight();
        float width = page.getMediaBox().getWidth();

        double beta = Math.atan(height / width);
        double alpha = Math.toRadians(180.0) - beta - Math.toRadians(90);
        double a = textWidth * Math.sin(alpha);
        double b = textWidth * Math.cos(alpha);

        Matrix textMatrix = Matrix.getRotateInstance(beta, (float) ((width - a)/2), (float) ((height-b)/2) - fontSize/2);
        
        PDPageContentStream contentStream = new PDPageContentStream(document, page, appendmode, true);
        contentStream.setGraphicsStateParameters(r0);
        contentStream.setNonStrokingColor(color);
        contentStream.beginText();
        contentStream.setFont(font, fontSize);
        contentStream.setTextMatrix(textMatrix);
        contentStream.showText(watermarkText);
        contentStream.endText();
        contentStream.close();
    }
}

Where does the PDDocument come from? And how do you insert it into the builder?

I've tried this but nothing happens. Nothing gets added to the PDF.

try {
    PdfRendererBuilder builder = new PdfRendererBuilder();
    builder.useFastMode();
    builder.withHtmlContent(doc.html(), uri);
    
    PdfBoxRenderer renderer = builder.buildPdfRenderer();
    PDDocument document = renderer.getPdfDocument();
    printTextWatermark(document);
    builder.usePDDocument(document);

    builder.toStream(os);
    builder.run();
} finally {
    os.close();
}

@danfickle
Copy link
Owner

From memory, I think you can use the object as a running element:

<html>
<head>
<style>
@page {
  @top-left {
    content: element(myrunner);
  }
}
</style>
</head>
<body>
<object style="position: running(myrunner); ..."

This will pull it out of normal flow and place it on every page.

@sillen102
Copy link

From memory, I think you can use the object as a running element:

<html>
<head>
<style>
@page {
  @top-left {
    content: element(myrunner);
  }
}
</style>
</head>
<body>
<object style="position: running(myrunner); ..."

This will pull it out of normal flow and place it on every page.

Ok that seems to be working, sortof. But can I place it on top? Right now if I place something it gets on top of it. z-index is not working.

rototor added a commit to rototor/openhtmltopdf that referenced this issue Oct 16, 2020
…a baseclass, and add ForegroundPdfDrawer which

always puts the PDF in front of the page.
rototor added a commit to rototor/openhtmltopdf that referenced this issue Oct 16, 2020
… a base class, and add ForegroundPdfDrawer which

always puts the PDF in front of the page.
@rototor
Copy link
Contributor

rototor commented Oct 16, 2020

I've made a PullRequest #577 with an object drawer which puts the PDF into the "foreground". There is no such a thing as a z-index in PDF. You only have a "ContentStream" which consists of all different kind of draw commands. What is drawn first is in the background, the stuff drawn last is in the foreground... So to have the watermark in the foreground it needs the be drawn last.

In the PullRequest is a sample, which shows that. Or you can look here on page 3
featuredocumentation.pdf - that the result of the (smoke) test driver, showing that feature and trying to document it at the same time.

@sillen102
Copy link

I've made a PullRequest #577 with an object drawer which puts the PDF into the "foreground". There is no such a thing as a z-index in PDF. You only have a "ContentStream" which consists of all different kind of draw commands. What is drawn first is in the background, the stuff drawn last is in the foreground... So to have the watermark in the foreground it needs the be drawn last.

In the PullRequest is a sample, which shows that. Or you can look here on page 3
featuredocumentation.pdf - that the result of the (smoke) test driver, showing that feature and trying to document it at the same time.

That looks amazing! Is it possible to get the full HTML/CSS and maybe even Java code for that PDF file?

@rototor
Copy link
Contributor

rototor commented Oct 16, 2020

@sillen102 The background and the foreground are PDFs. I made the foreground with Inkscape. You can set the opacity of the layer to get a transparency effect.

If you look at the pull request you will also see the sources of the featuredocumentation.pdf. See featuredocumentation.ftl and FreeMarkerGenerator.java.

If your plan is to generate the watermark dynamically, you can use outputDevice.drawWithGraphics() in your custom object and set a AlphaComposite on the Graphics2D. All you draw on the Graphics2D after setting the AlphaComposite will have the opacity set in the AlphaComposite. As an example how to use drawWithGraphics() you can look here at the class SampleObjectDrawerBinaryTree. The problem here is, that the XForm generated by drawWithGraphics() will be placed inside the stream. If you don't call your object drawer in the footer it might get under all the content of the page. It might even not work when called in the footer - didn't test.

If you want to generate the content but have it for sure drawn over everything on the page, you should look at #577 in the class ForegroundPdfDrawer. But instead of importing an existing PDF page as XForm you would generate a XForm using PdfBoxGraphics2D, see the example in the readme here.

@danfickle
Copy link
Owner

@sillen102, you could also try position: fixed which in theory will put an object on every page (in the page area, not the page margins) and will respect the z-index property in terms of painting order.

@danfickle
Copy link
Owner

@sillen102, you could also try position: fixed which in theory will put an object on every page (in the page area, not the page margins) and will respect the z-index property in terms of painting order.

Scratch this for now. The whole promblem is that fixed position custom object drawers are not positioning correctly on subsequent pages. I'll have another go at debugging now.

danfickle added a commit that referenced this issue Oct 18, 2020
…t (watermark).

It turns out it only needed the left and top properties defined in the html.
@danfickle
Copy link
Owner

It tuns out it only needed the left and top properties defined in the sample! So here is the sample watermark creator:

<html>
<head>
<style>
@page {
  size: 300px 300px;
  margin: 10px;
}
object[type="watermark"] {
  position: fixed;
  display: block;
  width: 100%;
  height: 100%;
  transform: rotate(45deg);
  z-index: 1000;
  left: 0;
  top: 0;
}
</style>
</head>
<body>
<object type="watermark"></object>


Other multiple page content here...
</body>
</html>
private static class WatermarkDrawer implements FSObjectDrawer {
        @Override
        public Map<Shape, String> drawObject(Element e, double x, double y, double width, double height,
                OutputDevice outputDevice, RenderingContext ctx, int dotsPerPixel) {
            outputDevice.drawWithGraphics((float) x, (float) y, (float) width / dotsPerPixel,
                (float) height / dotsPerPixel, (Graphics2D g2d) -> {

                double realWidth = width / dotsPerPixel;
                double realHeight = height / dotsPerPixel;

                Font font;
                try {
                    Font parent = Font.createFont(Font.TRUETYPE_FONT, new File("target/test/visual-tests/Karla-Bold.ttf"));
                    font = parent.deriveFont(20f);
                } catch (FontFormatException | IOException e1) {
                    e1.printStackTrace();
                    throw new RuntimeException(e1);
                }

                Rectangle2D bounds = font.getStringBounds("OpenHTMLToPDF", g2d.getFontRenderContext());

                g2d.setFont(font);
                g2d.setPaint(Color.RED);
                g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f));

                g2d.drawString("OpenHTMLToPDF",
                   (float) ((realWidth - bounds.getWidth()) / 2),
                   (float) ((realHeight - bounds.getHeight()) / 2));

            });

            return null;
        }
    }

    private static class WatermarkDrawerFactory implements FSObjectDrawerFactory {
        @Override
        public FSObjectDrawer createDrawer(Element e) {
           if (isReplacedObject(e)) {
              return new WatermarkDrawer();
           }
           return null;
        }

        @Override
        public boolean isReplacedObject(Element e) {
           return e.getAttribute("type").equals("watermark");
        }
    }

issue-472-add-semi-transparent-watermark.pdf

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants