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

Bai*_*ong 6 java jpeg

我想将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);
    }
}
Run Code Online (Sandbox Code Playgroud)

并发生错误:

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
Run Code Online (Sandbox Code Playgroud)

那么如何禁用JPEG压缩呢?还是有任何方法可以通过任何压缩将任何图像压缩到固定的文件大小?

gur*_*oso 2

至于第一个问题,如何创建非压缩的 jpeg:出于根本原因,人们不能。虽然我最初认为可以编写一个非压缩 jpg 编码器,通过操作涉及的霍夫曼树来生成可以使用任何现有解码器进行解码的输出,但我不得不放弃它。霍夫曼编码只是一系列转换的最后一步,不能跳过。自定义霍夫曼树也可能会破坏不太复杂的解码器。

对于考虑到评论中所做的要求更改的答案(以您喜欢的任何方式调整大小和压缩,只需给我所需的文件大小),可以这样推理:

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));
}
Run Code Online (Sandbox Code Playgroud)