7

What is the best practice for reducing the size of JPEG images in a PDF file, newly created using iText? (My objective is a trade-off between image quality and file size.)

The images are created as follows:

Image image = new Image(ImageDataFactory.create(imagePath))

I would like to provide a scale factor, for instance 0.5, which halves the number of pixels in a row.

Say I generate a PDF with a single 3 MB image. I tried image.scale(0.5f, 0.5f), but the resulting PDF file is still roughly 3 MB. I expected it to become much smaller.

Thus I guess the source image, embedded in the PDF file, is not touched. But that is what I need: The total number of pixels in the entire PDF file stored on disk should be reduced.

What is the easiest/recommended way to achieve this?

ideaboxer
  • 3,863
  • 8
  • 43
  • 62
  • `image.scale` etc do **not** change the bitmap data as such, they only change the dimensions the image will have in the PDF. – mkl Mar 20 '18 at 10:07
  • Whenever you offer a bounty, you should more clearly indicate what you expect. In particular, in which way is @Ben's answer not credible? – mkl Mar 22 '18 at 22:57
  • It is not easy (compared to iText usage). The image quality of the resulting image could be better (for instance, I get better results with the same number of pixels using GIMP). The result is not a JPEG and thus counters reaching my objective of smaller file size. The resulting format is not determined by the input format. I am seeking a simple, foolproof and straight-forward solution for a usual problem. – ideaboxer Mar 22 '18 at 23:07
  • Ok. I would propose, though, that you revise your question a bit: iText itself does not include functionality to downsize bitmap image data, it sensibly expects you to use software specialized on bitmap image processing for that. Thus, you should re-formulate the question to ask for [tag:image-compression] [tag:image-processing] options in [tag:java] to in particular downsize [tag:jpeg] [tag:bitmap] images and use these proposed tags. The question should not too much sound like a request for software recommendations as those nowadays are considered off-topic on stack overflow. – mkl Mar 23 '18 at 08:13

2 Answers2

5

Scale the image first, then open the scaled image with iText.

There is a create method in ImageDataFactory that accepts an AWT image. Scale the image using AWT tools first, then open it like this:

String imagePath = "C:\\path\\to\\image.jpg";
java.awt.Image awtImage = ImageIO.read(new File(imagePath));

// scale image here
int scaledWidth = awtImage.getWidth(null) / 2;
int scaledHeight = awtImage.getHeight(null) / 2;
BufferedImage scaledAwtImage = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = scaledAwtImage.createGraphics();
g.drawImage(awtImage, 0, 0, scaledWidth, scaledHeight, null); 
g.dispose();

/* 
Optionally pick a color to replace with transparency.
Any pixels that match this color will be replaced by tansparency.
*/
Color bgColor = Color.WHITE;

Image itextImage = new Image(ImageDataFactory.create(scaledAwtImage, bgColor));

For better tips on how to scale an image, see How can I resize an image using Java?

If you still need the original size when adding to PDF, just scale it back up again.

itextImage.scale(2f, 2f);

Note: This code is untested.


EDIT in response to comments on bounty

You got me thinking and looking. It appears iText treats importing an AWT image as a raw image. I presume it treats it the same as a BMP, which simply writes the pixel data using /FlateDecode, which is probably significantly less than optimal. The only way I can think of to achieve your requirement would be to use ImageIO to write the scaled image to the file system or a ByteArrayOutputStream as a jpeg, then use the resultant file/bytes to open with iText.

Here's an updated example using byte arrays. If you want to get any more fancy with compression levels and such, refer here.

String imagePath = "C:\\path\\to\\image.jpg";
java.awt.Image awtImage = ImageIO.read(new File(imagePath));

// scale image here
int scaledWidth = awtImage.getWidth(null) / 2;
int scaledHeight = awtImage.getHeight(null) / 2;
BufferedImage scaledAwtImage = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = scaledAwtImage.createGraphics();
g.drawImage(awtImage, 0, 0, scaledWidth, scaledHeight, null); 
g.dispose();

ByteArrayOutputStream bout = new ByteArrayOutputStream()
ImageIO.write(scaledAwtImage, "jpeg", bout);
byte[] imageBytes = bout.toByteArray();

Image itextImage = new Image(ImageDataFactory.create(imageBytes));
Ben Ingle
  • 575
  • 2
  • 12
  • 1
    Thank you, it works. 2 corrections: call `getWidth`/`getHeight` like that `awtImage.getWidth(null)` (passing `null`); and I had to take `WHITE` from `java.awt.Color` instead of `ColorConstants`. – ideaboxer Mar 20 '18 at 17:33
  • Whoops, fixed. Thanks! – Ben Ingle Mar 21 '18 at 10:50
  • Edited to include an example of converting the raw image to jpeg to enable better compression. – Ben Ingle Mar 24 '18 at 04:55
1

There is a way that listed in this documentations, that its give you access to compressing the image and reducing the entire PDF file stored on disk. hope it helps.

Below for the example of the code:

/*
 * This example was written by Bruno Lowagie in answer to the following question:
 * http://stackoverflow.com/questions/30483622/compressing-images-in-existing-pdfs-makes-the-resulting-pdf-file-bigger-lowagie
 */
package sandbox.images;

import com.itextpdf.text.DocumentException;
import com.itextpdf.text.pdf.PRStream;
import com.itextpdf.text.pdf.PdfName;
import com.itextpdf.text.pdf.PdfNumber;
import com.itextpdf.text.pdf.PdfObject;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.parser.PdfImageObject;

import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.imageio.ImageIO;
import sandbox.WrapToTest;

/**
 * @author Bruno Lowagie (iText Software)
 */
@WrapToTest
public class ReduceSize {

    public static final String SRC = "resources/pdfs/single_image.pdf";
    public static final String DEST = "results/images/single_image_reduced.pdf";
    public static final float FACTOR = 0.5f;

    public static void main(String[] args) throws DocumentException, IOException {
        File file = new File(DEST);
        file.getParentFile().mkdirs();
        new ReduceSize().manipulatePdf(SRC, DEST);
    }
    public void manipulatePdf(String src, String dest) throws DocumentException, IOException {
        PdfReader reader = new PdfReader(src);
        int n = reader.getXrefSize();
        PdfObject object;
        PRStream stream;
        // Look for image and manipulate image stream
        for (int i = 0; i < n; i++) {
            object = reader.getPdfObject(i);
            if (object == null || !object.isStream())
                continue;
            stream = (PRStream)object;
            if (!PdfName.IMAGE.equals(stream.getAsName(PdfName.SUBTYPE)))
                continue;
            if (!PdfName.DCTDECODE.equals(stream.getAsName(PdfName.FILTER)))
                continue;
            PdfImageObject image = new PdfImageObject(stream);
            BufferedImage bi = image.getBufferedImage();
            if (bi == null)
                continue;
            int width = (int)(bi.getWidth() * FACTOR);
            int height = (int)(bi.getHeight() * FACTOR);
            if (width <= 0 || height <= 0)
                continue;
            BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            AffineTransform at = AffineTransform.getScaleInstance(FACTOR, FACTOR);
            Graphics2D g = img.createGraphics();
            g.drawRenderedImage(bi, at);
            ByteArrayOutputStream imgBytes = new ByteArrayOutputStream();
            ImageIO.write(img, "JPG", imgBytes);
            stream.clear();
            stream.setData(imgBytes.toByteArray(), false, PRStream.NO_COMPRESSION);
            stream.put(PdfName.TYPE, PdfName.XOBJECT);
            stream.put(PdfName.SUBTYPE, PdfName.IMAGE);
            stream.put(PdfName.FILTER, PdfName.DCTDECODE);
            stream.put(PdfName.WIDTH, new PdfNumber(width));
            stream.put(PdfName.HEIGHT, new PdfNumber(height));
            stream.put(PdfName.BITSPERCOMPONENT, new PdfNumber(8));
            stream.put(PdfName.COLORSPACE, PdfName.DEVICERGB);
        }
        reader.removeUnusedObjects();
        // Save altered PDF
        PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(dest));
        stamper.setFullCompression();
        stamper.close();
        reader.close();
    }
}
shafrianadhi
  • 642
  • 5
  • 8
  • Thank you. Unfortunately not, because one of my objectives is to increase file creation performance (reduce file creation time). Previewer read performance included (the previewer reads the file immediately after its creation). Thus I need to reduce image file size on the fly before the image get written to the PDF. – ideaboxer Mar 28 '18 at 17:05
  • Original answer: https://stackoverflow.com/a/55728764/4398114 – Allinone51 Oct 22 '19 at 08:48