Flying Saucer: set custom DPI for output PDF

7k views Asked by At

I'm using Flying Saucer for HTML to PDF conversion. I need to produce an output PDF with 600dpi in Letter size. How can I achieve this?

3

There are 3 answers

6
obourgain On BEST ANSWER

You can set the letter size with the CSS page size property in your HTML document:

   @page {
      size: letter;
    }

You can change the dpi of the document using the following ITextRenderer constructor :

public ITextRenderer(float dotsPerPoint, int dotsPerPixel)

I don't understand what those values really represent, but default values are dotsPerPoint = 20f * 4f / 3f and dotsPerPixel = 20, and it will output a 96dpi document.

To get 600dpi, you can use dotsPerPoint = 500f / 3f and dotsPerPixel = 20.

Looking at the code of ITextRenderer, the final dpi is given by the formula: dpi = dotsPerPoint * 72 / dotsPerPixel.

5
Scott Dudley On

There are four different factors at play, and they are all interrelated:

Target Page Metrics

You want the page metrics to be correct, so that when you ask Flying Saucer to produce a "letter" size page, the resulting PDF will show up in Acrobat as measuring 8.5" x 11". You configure the page size directly in FS by specifying the page-size property in your CSS, as obourgain noted in the other answer: @page { size: letter; }

Resolution

You want the final output to be suitable for printing on some printer at a certain XXX dpi. This is all well and good, but remember that PDF is (mostly) a vector format. I didn't check the spec, but as far as I know, the PDF file/page don't "have" resolution because they are vector-based. That having been said, things placed inside the page have an effective resolution, so we'll need your desired XXX dpi number to calculate the numbers below.

Dots per point

In the FlyingSaucer (and Java) world, a point is always a constant 1/72 of an inch. Period. So we can calculate the dots-per-point value by taking the desired resolution and dividing by the size of a point. For example, if you want 300 dpi output:

  • dots-per-point = 300 dpi / 72 ppi = 300 dots/inch / 72 points/inch = 300/72 dots/point = 4.1666 dots/point

Dots per pixel

This is not a magic number and this value is directly related to the dots-per-point number, as well as the expected resolution of the graphical images that you are trying to feed into FlyingSaucer. More specifically, given a graphic image that is X x Y pixels in dimensions, you need to decide how big you want that to render in your PDF. If you are using an image that was prepared for screen (web) use, you probably are starting out at the standard 96 pixels/inch (so a 96 x 96 px image would render as a one-inch square on the PDF output).

So we can easily calculate dots-per-pixel as follows, again assuming we wanted 300 dpi output:

  • dots-per-pixel = dots-per-inch (dots) / pixels-per-inch (pixels)
  • dots-per-pixel = 300 dots/inch / 96 pixels/inch = 3.125 dots/pixel

If you go this approach, your images will be sized correctly, but they won't be the 300 dpi print quality that you're looking for. That's because your images were not of high enough resolution to begin with. More on that in a minute.

Getting it all set up

If you just call SharedContext#setDPI directly as suggested in one of the other answers, you'll likely get the wrong results. This is because it doesn't make logical sense to change the dots-per-point without also changing the resolution (dots-per-inch). The ITextRenderer constructor makes a fixed call to setDPI(72*dotsPerPoint), and when it goes to create a new page, it also uses the dotsPerPoint value set by the constructor to calculate the correct page width in points. If you have changed the resolution under its nose by calling setDPI, you'll end up with the wrong page size.

The correct way is to create a new ITextRenderer object using the values we calculated above. If we wanted 300 dpi output and we had 96 ppi images to feed it, we would call:

ITextRenderer renderer = new ITextRenderer(4.1666f, 3);

Note that the dots-per-pixel parameter only accepts integers, so we're rounding 3.125 to the closest integer above. However...it's truly the ratio between the two numbers that seems to be important, so to make the last argument an integer, we can multiply both numbers by 8 (the lowest integer multiplier that yields an integer), which gives 33.3333 and exactly 25. This also is my guess for the origin of the magic "20" number in the Flying Saucer sources.

Check your output

By this point, your output PDF should look pretty much the same as it did when you started (assuming that you were previously using the default 96 ppi configuration of Flying Saucer). But now we know the parameters that we need to tune to make everything work.

To get high-quality output, you need high quality input

So we've verified that the above parameters work for our purposes, but our images are still a lowly 96 ppi. If we want to print this stuff at high resolution, all you need to do is swap out the images for 300 ppi versions, change the constructor parameters, and then you're done, right?

Maybe. Let's work through the numbers:

Your expected output resolution (300 dpi) does not change, so dots-per-point is still 4.1666. But your input images are now at 300 ppi, so your dots-per-pixel = 300 dots/inch / 300 pixels/inch = 1 dot/pixel. So you'll now call the constructor like this:

ITextRenderer renderer = new ITextRenderer(4.1666f, 1);

Once you do this, your new 300 px x 300 px image will end up as 1" square on the PDF, which is exactly the print quality you wanted.

But wait! All of my text got really small too!

Flying Saucer uses the dots-per-pixel measurement to convert a number of things, not just images. In particular, if you have specified anything in your stylesheets that uses pixels, the dots-per-pixel measurement will have an impact on their sizes too.

If you have stylesheet rules like font-size: 10px; then increasing the dots-per-pixel supplied to the constructor will make that text smaller, which is probably also not what you want. After all, you should be able to increase the resolution of the images in your PDF while leaving your text in the same size and place.

The answer is to convert everything in your stylesheet to use points. (Or inches. At least something other than pixels!) If you started out with the default Flying Saucer settings (meaning pixels are 96 ppi), you simply need to convert all of your "px" measurements into points. Since 72 points = 1 inch, you would change "px" to "pt" and multiply the value by 72/96.

For example, the font-size: 10px; above would become font-size: 7.5pt;. If you want true consistency with what you had before, everything in your CSS that mentions "px" (as well as any inline styles) must be changed into "pt" with the the same conversion too.

Once you have made this change, your text and other layout will be consistent, and if you decide that you need 600 dpi output later, you can just adjust your images and change the constructor argument, but the rest of the layout will still remain constant. Done!

3
Charles Goodwin On

A simpler answer for setting the DPI when using Flying Saucer:

renderer.getSharedContext().setDPI(600);

As with obourgain's answer, to be used in conjuction with the @page { size:letter; } CSS.

Source