Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I save a BufferedImage to be below a particular size

(Using java 8) Given a image user needs to be able to specify min/max image size in pixels and also maximum size of saved image in kbs, image is saved as jpg.

So I have the first bit working, by resizing buffered image:

public static BufferedImage resizeUsingImageIO(Image srcImage, int size)
    {
        int w = srcImage.getWidth(null);
        int h = srcImage.getHeight(null);

        // Determine the scaling required to get desired result.
        float scaleW = (float) size / (float) w;
        float scaleH = (float) size / (float) h;

        MainWindow.logger.finest("Image Resizing to size:" + size + " w:" + w + ":h:" + h + ":scaleW:" + scaleW + ":scaleH" + scaleH);

        //Create an image buffer in which to paint on, create as an opaque Rgb type image, it doesn't matter what type
        //the original image is we want to convert to the best type for displaying on screen regardless
        BufferedImage bi = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);

        // Set the scale.
        AffineTransform tx = new AffineTransform();
        tx.scale(scaleW, scaleH);

        // Paint image.
        Graphics2D g2d = bi.createGraphics();
        g2d.setColor(Color.WHITE);
        g2d.fillRect(0, 0, size, size);
        g2d.setComposite(AlphaComposite.SrcOver);
        g2d.drawImage(srcImage, tx, null);
        g2d.dispose();
        return bi;
    }

The image is eventually output as a jpg as follows

public static byte[] convertToByteArray(BufferedImage bi) throws Exception
{
    final ByteArrayOutputStream output = new ByteArrayOutputStream();
    //Convert JPEG and then a byte array
    if (ImageIO.write(bi, FILE_SUFFIX_JPG, new DataOutputStream(output)))
    {
        final byte[] imageData = output.toByteArray();
        return imageData;
    }
}

but is there a way I specify a max image size , and make it perform more compression as required to get underneath that size.

And should I set limits to width and height in first stage based on total size required, i.e if the total size is too small it would be impossible to get a good image if compression to a size that is too small

like image 447
Paul Taylor Avatar asked Feb 25 '14 13:02

Paul Taylor


1 Answers

I don't know an "easy" or "elegant" way to do this.

However, 1.5 years ago, I wrote this code snippet: It is a small utility that allows selecting the image resolution, compression quality and resulting JPG file size, and shows a preview of the resulting image.

The slider for the image quality and the spinner for the JPG file size are "linked": When you change the quality, the resulting file size will be updated. When you change the file size, the quality will be adjusted so that the resulting image is not larger than the given file size (if possible).

The quality adjustment is done using some sort of a "binary search" (see the computeQuality method), because the predicting the file size depending on the compression is hard (or even impossible). Of course, this implies some computational cost, but there are not so many alternatives, I guess. (You could define another stopping criterion. At the moment, it tries hard to find the "perfect" quality for the given file size limit). Maybe one or another method from this utility may be helpful for you, anyhow.

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JSplitPane;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class ImageLimiterTest
{
    public static void main(String[] args) throws IOException
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                try
                {
                    createAndShowGUI();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        });
    }

    private static void createAndShowGUI() throws IOException
    {
        JFrame f = new JFrame("ImageLimiter");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ImageLimiterPanel imageLimiterPanel = new ImageLimiterPanel(new ImageLimiter());
        BufferedImage inputImage = ImageIO.read(new File("lena512color.png"));
        imageLimiterPanel.setInputImage(inputImage);
        f.getContentPane().add(imageLimiterPanel);
        f.setSize(800,600);
        f.setVisible(true);
    }
}

class ImageLimiter
{
    private BufferedImage inputImage;
    private BufferedImage scaledImage;
    private BufferedImage outputImage;

    private int maxResolution;
    private float quality;

    private int fileSizeBytes;

    public void setInputImage(BufferedImage inputImage)
    {
        this.inputImage = inputImage;
        this.maxResolution = Math.max(inputImage.getWidth(), inputImage.getHeight());
        this.quality = 1.0f;
        this.scaledImage = computeScaledImage(inputImage, maxResolution);
        updateOutputImage();
    }

    public BufferedImage getOutputImage()
    {
        return outputImage;
    }

    public int getFileSizeBytes()
    {
        return fileSizeBytes;
    }

    public void setMaxResolution(int maxResolution)
    {
        this.maxResolution = maxResolution;
        this.scaledImage = computeScaledImage(inputImage, maxResolution);
        updateOutputImage();
    }

    public void setQuality(float quality)
    {
        this.quality = quality;
        updateOutputImage();
    }

    public float getQuality()
    {
        return quality;
    }

    public void setMaxFileSize(int maxFileSizeBytes)
    {
        this.quality = computeQuality(scaledImage, maxFileSizeBytes);
        updateOutputImage();
    }

    private void updateOutputImage()
    {
        try
        {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            writeJPG(scaledImage, baos, quality);
            baos.close();
            byte data[] = baos.toByteArray();
            fileSizeBytes = data.length;
            ByteArrayInputStream bais = new ByteArrayInputStream(data);
            outputImage = ImageIO.read(bais);
            bais.close();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }


    static float computeQuality(BufferedImage image, int sizeLimitBytes)
    {
        int minSizeBytes = computeSizeBytes(image, 0.0f);
        if (sizeLimitBytes < minSizeBytes)
        {
            return 0.0f;
        }
        int maxSizeBytes = computeSizeBytes(image, 1.0f);
        if (sizeLimitBytes > maxSizeBytes)
        {
            return 1.0f;
        }
        float intervalSize = 0.5f;
        float quality = 0.5f;
        float lastSmaller = 0;
        while (true)
        {
            int sizeBytes = computeSizeBytes(image, quality);
            if (sizeBytes >= sizeLimitBytes)
            {
                //System.out.println("For "+quality+" have size "+sizeBytes+", decrease quality by "+intervalSize);
                quality -= intervalSize;
                intervalSize /= 2;
            }
            else if (sizeBytes < sizeLimitBytes)
            {
                //System.out.println("For "+quality+" have size "+sizeBytes+", increase quality by "+intervalSize);
                lastSmaller = quality;
                quality += intervalSize;
                intervalSize /= 2;
            }
            if (intervalSize < 0.01f)
            {
                break;
            }
        }
        return lastSmaller;
    }

    private static int computeSizeBytes(BufferedImage image, float quality)
    {
        quality = Math.min(1, Math.max(0, quality));
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try
        {
            writeJPG(image, baos, quality);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            try
            {
                baos.close();
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
        }
        byte data[] = baos.toByteArray();
        return data.length;
    }

    private static BufferedImage computeScaledImage(BufferedImage input, int limit)
    {
        int w = input.getWidth();
        int h  = input.getHeight();
        float aspect = (float)w / h;
        if (aspect > 1)
        {
            w = limit;
            h = (int)(w / aspect);
        }
        else
        {
            h = limit;
            w = (int)(h * aspect);
        }
        BufferedImage output = new BufferedImage(
            w, h, BufferedImage.TYPE_INT_ARGB);    

        Graphics2D g = output.createGraphics();
        g.setRenderingHint(
            RenderingHints.KEY_INTERPOLATION,
            RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.drawImage(input, 0, 0, w, h, null);
        g.dispose();

        return output;
    }

    /**
     * Write the given RenderedImage as a JPEG to the given outputStream,
     * using the given quality. The quality must be a value between
     * 0 (lowest quality, maximum compression) and 1 (highest
     * quality, minimum compression). The caller is responsible for
     * closing the given stream.
     *  
     * @param renderedImage The image to write
     * @param outputStream The stream to write to
     * @param quality The quality, between 0 and 1
     * @throws IOException If an IO error occurs
     */
    public static void writeJPG(RenderedImage renderedImage,
        OutputStream outputStream, float quality) throws IOException
    {
        Iterator<ImageWriter> imageWriters =
            ImageIO.getImageWritersByFormatName("jpeg");
        ImageWriter imageWriter = imageWriters.next();
        ImageOutputStream imageOutputStream =
            ImageIO.createImageOutputStream(outputStream);
        imageWriter.setOutput(imageOutputStream);
        ImageWriteParam param = imageWriter.getDefaultWriteParam();
        param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        param.setCompressionQuality(quality);
        IIOImage iioImage = new IIOImage(renderedImage, null, null);
        imageWriter.write(null, iioImage, param);
    }   
}


class ImageLimiterPanel extends JPanel
{
    private ImageLimiter imageLimiter;

    private ImageIcon inputImageIcon;
    private ImageIcon outputImageIcon;

    private JScrollPane inputScrollPane;
    private JScrollPane outputScrollPane;

    private JSlider qualitySlider;
    private JLabel qualityLabel;

    private JSlider resolutionLimitSlider;
    private JLabel resolutionLimitLabel;

    private JSpinner sizeLimitSpinner;
    private JLabel sizeLimitLabel;

    private boolean updating = false;

    public ImageLimiterPanel(ImageLimiter imageLimiter)
    {
        this.imageLimiter = imageLimiter;

        setLayout(new BorderLayout());

        final JSplitPane splitPane = new JSplitPane();

        inputImageIcon = new ImageIcon();
        JLabel inputImageLabel = new JLabel(inputImageIcon);
        inputScrollPane = new JScrollPane(inputImageLabel);
        inputScrollPane.setBorder(BorderFactory.createTitledBorder("Input"));
        splitPane.setLeftComponent(inputScrollPane);

        outputImageIcon = new ImageIcon();
        JLabel outputImageLabel = new JLabel(outputImageIcon);
        outputScrollPane = new JScrollPane(outputImageLabel);
        outputScrollPane.setBorder(BorderFactory.createTitledBorder("Output"));
        splitPane.setRightComponent(outputScrollPane);
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                splitPane.setDividerLocation(0.5);
            }
        });

        add(splitPane, BorderLayout.CENTER);


        JPanel controlPanel = new JPanel(new GridLayout(0,1));

        JPanel resolutionLimitPanel = createResolutionLimitPanel();
        controlPanel.add(resolutionLimitPanel);

        JPanel qualityPanel = createQualityPanel();
        controlPanel.add(qualityPanel);

        JPanel sizePanel = createSizeLimitPanel();
        controlPanel.add(sizePanel);

        add(controlPanel, BorderLayout.NORTH);

    }

    public void setInputImage(BufferedImage inputImage)
    {
        imageLimiter.setInputImage(inputImage);
        inputImageIcon.setImage(inputImage);
        int max = Math.max(inputImage.getWidth(), inputImage.getHeight());
        resolutionLimitSlider.setMaximum(max);        
        resolutionLimitSlider.setValue(max);        
    }

    private JPanel createResolutionLimitPanel()
    {
        JPanel resolutionLimitPanel = new JPanel(new BorderLayout());
        resolutionLimitLabel = new JLabel("Resolution: ");
        resolutionLimitLabel.setPreferredSize(new Dimension(300, 10));
        resolutionLimitPanel.add(resolutionLimitLabel, BorderLayout.WEST);
        resolutionLimitSlider = new JSlider(0,100,80);
        resolutionLimitSlider.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent arg0)
            {
                if (!updating)
                {
                    updating = true;

                    int maxResolution = resolutionLimitSlider.getValue();
                    imageLimiter.setMaxResolution(maxResolution);
                    updateOutputImage(imageLimiter.getOutputImage());

                    sizeLimitLabel.setText("Size: "+imageLimiter.getFileSizeBytes());
                    sizeLimitSpinner.setValue(imageLimiter.getFileSizeBytes());

                    updating = false;
                }
            }
        });
        resolutionLimitPanel.add(resolutionLimitSlider, BorderLayout.CENTER);
        return resolutionLimitPanel;
    }

    private JPanel createQualityPanel()
    {
        JPanel qualityPanel = new JPanel(new BorderLayout());
        qualityLabel = new JLabel("Quality: ");
        qualityLabel.setPreferredSize(new Dimension(300, 10));
        qualityPanel.add(qualityLabel, BorderLayout.WEST);
        qualitySlider = new JSlider(0,100,80);
        qualitySlider.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent arg0)
            {
                if (!updating)
                {
                    updating = true;

                    float quality = qualitySlider.getValue()/100.0f;
                    imageLimiter.setQuality(quality);
                    updateOutputImage(imageLimiter.getOutputImage());

                    qualityLabel.setText("Quality: "+String.format("%.2f", quality));
                    sizeLimitLabel.setText("Size: "+imageLimiter.getFileSizeBytes());
                    sizeLimitSpinner.setValue(imageLimiter.getFileSizeBytes());

                    updating = false;
                }
            }
        });
        qualityPanel.add(qualitySlider, BorderLayout.CENTER);
        return qualityPanel;
    }

    private JPanel createSizeLimitPanel()
    {
        JPanel sizeLimitPanel = new JPanel(new BorderLayout());
        sizeLimitLabel = new JLabel("Size: ");
        sizeLimitLabel.setPreferredSize(new Dimension(300, 10));
        sizeLimitPanel.add(sizeLimitLabel, BorderLayout.WEST);
        sizeLimitSpinner = new JSpinner(new SpinnerNumberModel(10000, 0, 1000000000, 1000));
        sizeLimitSpinner.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent arg0)
            {
                if (!updating)
                {
                    updating = true;

                    int sizeLimit = (Integer)sizeLimitSpinner.getValue();
                    imageLimiter.setMaxFileSize(sizeLimit);
                    updateOutputImage(imageLimiter.getOutputImage());

                    qualityLabel.setText("Quality: "+String.format("%.2f", imageLimiter.getQuality()));
                    qualitySlider.setValue((int)(imageLimiter.getQuality()*100));

                    sizeLimitLabel.setText("Size: "+imageLimiter.getFileSizeBytes());
                    sizeLimitSpinner.setValue(imageLimiter.getFileSizeBytes());

                    updating = false;
                }

            }
        });
        sizeLimitPanel.add(sizeLimitSpinner, BorderLayout.CENTER);
        return sizeLimitPanel;
    }

    private void updateOutputImage(BufferedImage outputImage)
    {
        outputImageIcon.setImage(outputImage);
        outputScrollPane.invalidate();
        revalidate();
        outputScrollPane.repaint();
    }

}
like image 191
Marco13 Avatar answered Sep 22 '22 14:09

Marco13