6

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 BaiJiFeiLong@gmail.com 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?

BaiJiFeiLong
  • 3,716
  • 1
  • 30
  • 28
  • Given that a TYPE_INT_RGB pixel takes 3 bytes to represent if you don't compress it, and 358 by 441 pixels hence take 358 * 411 * 3 = 473634 bytes, I don't see how you could store it in 20480 bytes without compression. I'm also not seeing any attempt in your code to limit the file size to 20480 bytes. – Erwin Bolwidt Oct 09 '19 at 05:43
  • @ErwinBolwidt YES. It should be 143 * 143. – BaiJiFeiLong Oct 09 '19 at 05:56
  • 1
    The JPEGImageWriter code is clear about it, you can't have jpeg compression without compression. – Curiosa Globunznik Oct 09 '19 at 05:56
  • JPEG is a compressed format. If you dont want compression you should be going with PNG/TIFF may be? – Kris Oct 09 '19 at 06:11
  • @Kris Can JPEG be zero-compression (3 bytes per pixel)? – BaiJiFeiLong Oct 09 '19 at 06:14
  • If you are looking for lossless JPEG standard the answer is here https://stackoverflow.com/questions/5702142/100-java-library-for-jpeg-lossless-decoding – Kris Oct 09 '19 at 06:40
  • I proposed a solution below that would fulfill your requirements, but do you actually really care if the file size is precisely x? And not x+1 bytes or x-2 bytes? – Curiosa Globunznik Oct 09 '19 at 14:17
  • The goal of at least **limiting** the size of a resulting JPEG can be achieved, to some extent: In https://stackoverflow.com/a/22016608/3182664 I showed a (somewhat quirky) approach for limiting the image quality so that a given file size is not exceeded, using some sort of "binary search" for the quality parameter... – Marco13 Oct 09 '19 at 17:10

2 Answers2

2

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));
}
Curiosa Globunznik
  • 3,129
  • 1
  • 16
  • 24
0

A solution using Magick (sudo apt install imagemagick on Debian), maybe not work for some images. Thanks to @curiosa-g.

package io.github.baijifeilong.jpeg;

import lombok.SneakyThrows;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Scanner;

/**
 * Created by BaiJiFeiLong@gmail.com at 2019/10/9 上午11:26
 */
public class JpegApp {

    @SneakyThrows
    private static InputStream compressJpeg(InputStream inputStream, int fileSize) {
        File tmpFile = new File(String.format("tmp-%d.jpg", Thread.currentThread().getId()));
        FileUtils.copyInputStreamToFile(inputStream, tmpFile);
        Process process = Runtime.getRuntime().exec(String.format("mogrify -strip -resize 512 -define jpeg:extent=%d %s", fileSize, tmpFile.getName()));
        try (Scanner scanner = new Scanner(process.getErrorStream()).useDelimiter("\\A")) {
            if (process.waitFor() > 0) throw new RuntimeException(String.format("Mogrify Error \n### %s###", scanner.hasNext() ? scanner.next() : "Unknown"));
        }
        try (FileInputStream fileInputStream = new FileInputStream(tmpFile)) {
            byte[] bytes = IOUtils.toByteArray(fileInputStream);
            assert bytes.length <= fileSize;
            byte[] newBytes = new byte[fileSize];
            System.arraycopy(bytes, 0, newBytes, 0, bytes.length);
            Files.delete(Paths.get(tmpFile.getPath()));
            return new ByteArrayInputStream(newBytes);
        }
    }

    @SneakyThrows
    public static void main(String[] args) {
        InputStream inputStream = compressJpeg(new FileInputStream("big.jpg"), 40 * 1024);
        IOUtils.copy(inputStream, new FileOutputStream("40KB.jpg"));
        System.out.println(40 * 1024);
        System.out.println(new File("40KB.jpg").length());
    }
}

And the output:

40960
40960
BaiJiFeiLong
  • 3,716
  • 1
  • 30
  • 28