Как отключить сжатие при записи JPEG в Java? - PullRequest
6 голосов
/ 09 октября 2019

Я хочу сжать 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? Или существует какой-либо метод, который может сжать любое изображение до фиксированного размера файла с любым сжатием?

Ответы [ 2 ]

2 голосов
/ 09 октября 2019

Что касается первоначального вопроса, как создавать несжатые JPEG: никто не может, по фундаментальным причинам. Хотя я изначально предполагал, что можно написать несжатый кодировщик jpg, производящий выходные данные, которые можно декодировать любым существующим декодером, манипулируя задействованным деревом Хаффмана, мне пришлось отклонить его . Кодирование Хаффмана - это только последний шаг целого конвейера преобразований, который нельзя пропустить. Пользовательские деревья Хаффмана могут также сломать менее сложные декодеры.

Для ответа, который учитывает изменение требований, внесенное в комментарии (измените размер и сожмите любым способом, просто дайте мне нужный файлsize) можно рассуждать так:

Спецификация jpeg файла определяет маркер конца изображения. Таким образом, есть вероятность, что исправление нулей (или просто чего-нибудь) впоследствии не имеет значения. Эксперимент по исправлению некоторых изображений до определенного размера показал, что gimp, chrome, firefox и ваш JpegApp проглотили такой раздутый файл без жалоб.

Было бы довольно сложно создать сжатие, которое для любого изображения сжимается точно доВаше требование к размеру (тип: для изображения A вам необходим коэффициент сжатия 0,7143, для изображения B 0,9356633, для C 12,445 ...). Однако есть попытки предсказать коэффициенты сжатия изображения на основе необработанных данных изображения.

Поэтому я бы предложил просто изменить размер / сжать до любого размера <20480, а затем залатать его: </p>

  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 голосов
/ 09 октября 2019

Решение, использующее Magick (sudo apt install imagemagick в Debian), может не работать для некоторых изображений. Благодаря @ 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
...