如何在Java中写入JPEG时禁用压缩?

6
我希望能够将JPEG压缩至固定的文件大小(20480字节)。以下是我的代码:
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);
    }
}

错误发生了:

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

如何禁用JPEG压缩?或是否有一种方法可以将任何图像压缩到固定的文件大小,并采用任何压缩方式?

考虑到如果不进行压缩,一个 TYPE_INT_RGB 像素需要 3 个字节来表示,而 358 × 441 个像素则需要 358 × 411 × 3 = 473634 字节,我不认为你可以在未压缩的情况下将其存储在 20480 字节中。我也没有看到你的代码中有任何尝试将文件大小限制在 20480 字节以内的方法。 - Erwin Bolwidt
@ErwinBolwidt 是的,应该是 143 * 143。 - BaiJiFeiLong
1
JPEGImageWriter 的代码表明,压缩必须存在,否则不能进行 JPEG 压缩。 - Curiosa Globunznik
JPEG是一种压缩格式。如果您不想进行压缩,可以选择PNG/TIFF。 - Kris
@Kris JPEG可以是零压缩的吗(每像素3个字节)? - BaiJiFeiLong
显示剩余3条评论
2个回答

2
关于最初的问题,如何创建无压缩的JPEG图像:基本上是不可能的。尽管我最初认为可以通过操纵所涉及的Huffman树来编写一个非压缩的JPEG编码器,从而生成可以用任何现有解码器解码的输出,但我不得不放弃这个想法。Huffman编码只是一系列转换的最后一步,不能跳过。自定义的Huffman树也可能破坏不那么复杂的解码器。
针对评论中提出的要求更改(调整大小并以任何方式压缩,只需给出所需的文件大小)的答案可以这样理解:
JPEG文件规范定义了一个图像结束标记。因此,补零(或其他任何东西)可能没有任何影响。对一些图像进行实验,将它们补充到特定大小,结果GIMP、Chrome、Firefox和JpegApp都可以接受这样的膨胀文件而没有任何投诉。
创建一个可以精确压缩到您的大小要求的压缩可能会非常复杂(例如:对于图像A,您需要0.7143的压缩比,对于图像B,您需要0.9356633,对于C,您需要12.445...)。虽然有一些尝试基于原始图像数据预测图像压缩比,但仍然存在困难。
因此,我建议只需将图像调整/压缩到小于20480的任何大小,然后进行修补:
  1. 根据原始jpg大小和所需大小计算缩放比率,包括安全边距以解决问题的内在模糊性
  2. 使用该比率调整图像大小
  3. 补充缺失的字节以完全匹配所需大小
如在此处概述。
    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));
}

0

使用 Magick(在 Debian 上使用 sudo apt install imagemagick)的解决方案,对于某些图像可能无效。感谢 @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());
    }
}

以及输出:

40960
40960

啊,你连缩放比例都不在意吗?只是以任何方式进行缩放压缩以达到20480字节(或20480 * 2)? - Curiosa Globunznik
我需要批量处理许多JPEG文件。文件大小是唯一的要求。 - BaiJiFeiLong
我认为这段代码对于一些奇怪的图片可能无法正常工作。 - BaiJiFeiLong
不适用于在Cent OS 7上构建的ImageMagick 6.7.8-9。 - BaiJiFeiLong
这里不是cn,我不必在我的话之前删除 :) - BaiJiFeiLong
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接