Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to disable compression when writing JPEG in java?

Tags:

java

jpeg

I want to compress JPEG to fixed file size (20480 bytes). Here is my code:

package io.github.baijifeilong.jpeg;

import lombok.SneakyThrows;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.stream.FileImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;

/**
 * Created by [email protected] at 2019/10/9 上午11:26
 */
public class JpegApp {

    @SneakyThrows
    public static void main(String[] args) {
        BufferedImage inImage = ImageIO.read(new File("demo.jpg"));
        BufferedImage outImage = new BufferedImage(143, 143, BufferedImage.TYPE_INT_RGB);
        outImage.getGraphics().drawImage(inImage.getScaledInstance(143, 143, Image.SCALE_SMOOTH), 0, 0, null);

        JPEGImageWriteParam jpegParams = new JPEGImageWriteParam(null);
        jpegParams.setCompressionMode(ImageWriteParam.MODE_DISABLED);
        ImageWriter imageWriter = ImageIO.getImageWritersByFormatName("jpg").next();
        imageWriter.setOutput(new FileImageOutputStream(new File("demo-xxx.jpg")));
        imageWriter.write(null, new IIOImage(outImage, null, null), jpegParams);
    }
}

And the error occured:

Exception in thread "main" javax.imageio.IIOException: JPEG compression cannot be disabled
    at com.sun.imageio.plugins.jpeg.JPEGImageWriter.writeOnThread(JPEGImageWriter.java:580)
    at com.sun.imageio.plugins.jpeg.JPEGImageWriter.write(JPEGImageWriter.java:363)
    at io.github.baijifeilong.jpeg.JpegApp.main(JpegApp.java:30)

Process finished with exit code 1

So how to disable JPEG compression? Or there be any method that can compress any image to a fixed file size with any compression?

like image 908
BaiJiFeiLong Avatar asked Oct 09 '19 05:10

BaiJiFeiLong


1 Answers

As for the initial question, how to create non-compressed jpegs: one can't, for fundamental reasons. While I initially assumed that it is possible to write a non-compressing jpg encoder producing output that can be decoded with any existing decoder by manipulating the Huffman tree involved, I had to dismiss it. The Huffman encoding is just the last step of quite a pipeline of transformations, that can not be skipped. Custom Huffman trees may also break less sophisticated decoders.

For an answer that takes into consideration the requirement change made in comments (resize and compress any way you like, just give me the desired file size) one could reason this way:

The jpeg file specification defines an End of Image marker. So chances are, that patching zeros (or just anything perhaps) afterwards make no difference. An experiment patching some images up to a specific size showed that gimp, chrome, firefox and your JpegApp swallowed such an inflated file without complaint.

It would be rather complicated to create a compression that for any image compresses precisely to your size requirement (kind of: for image A you need a compression ratio of 0.7143, for Image B 0.9356633, for C 12.445 ...). There are attempts to predict image compression ratios based on raw image data, though.

So I'd propose just to resize/compress to any size < 20480 and then patch it:

  1. calculate the scaling ratio based on the original jpg size and the desired size, including a safety margin to account for the inherently vague nature of the issue

  2. resize the image with that ratio

  3. patch missing bytes to match exactly the desired size

As outlined here

    private static void scaleAndcompress(String fileNameIn, String fileNameOut, Long desiredSize) {
    try {
        long size = getSize(fileNameIn);

        // calculate desired ratio for conversion to stay within size limit, including a safte maring (of 10%)
        // to account for the vague nature of the procedure. note, that this will also scale up if needed
        double desiredRatio = (desiredSize.floatValue() / size) * (1 - SAFETY_MARGIN);

        BufferedImage inImg = ImageIO.read(new File(fileNameIn));
        int widthOut = round(inImg.getWidth() * desiredRatio);
        int heigthOut = round(inImg.getHeight() * desiredRatio);

        BufferedImage outImg = new BufferedImage(widthOut, heigthOut, BufferedImage.TYPE_INT_RGB);
        outImg.getGraphics().drawImage(inImg.getScaledInstance(widthOut, heigthOut, Image.SCALE_SMOOTH), 0, 0, null);

        JPEGImageWriter imageWriter = (JPEGImageWriter) ImageIO.getImageWritersByFormatName("jpg").next();

        ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
        imageWriter.setOutput(new MemoryCacheImageOutputStream(outBytes));
        imageWriter.write(null, new IIOImage(outImg, null, null), new JPEGImageWriteParam(null));

        if (outBytes.size() > desiredSize) {
            throw new IllegalStateException(String.format("Excess output data size %d for image %s", outBytes.size(), fileNameIn));
        }
        System.out.println(String.format("patching %d bytes to %s", desiredSize - outBytes.size(), fileNameOut));
        patch(desiredSize, outBytes);
        try (FileOutputStream outFileStream = new FileOutputStream(new File(fileNameOut))) {
            outBytes.writeTo(outFileStream);
        }

    } catch (IOException e) {
        e.printStackTrace();
    }
}

private static void patch(Long desiredSize, ByteArrayOutputStream bytesOut) {
    long patchSize = desiredSize - bytesOut.size();
    for (long i = 0; i < patchSize; i++) {
        bytesOut.write(0);
    }
}

private static long getSize(String fileName) {
    return (new File(fileName)).length();
}

private static int round(double f) {
    return Math.toIntExact(Math.round(f));
}
like image 122
Curiosa Globunznik Avatar answered Sep 30 '22 11:09

Curiosa Globunznik