java - 如何缩放PNG图像直到达到目标文件大小

标签 java bufferedimage

正如标题所说,我正在尝试调整 PNG 图像的大小以达到目标文件大小(以兆字节为单位)。
我在 SO 和网络上搜索了很多,找到了很多代码,但所有代码都没有考虑最终文件的大小。
我已经安排了一些代码,但试图优化性能。

例子:

  • 源图像尺寸 = 30 MB
  • 目标文件输出大小 = 5 MB

  • 当前流量:
  • 1 - 将 PNG 图像加载为 BufferedImage
  • 2 - 递归使用 Scalr.resize(...)为了调整图像大小
  • 2.1 - 对于每一步使用 ImageIO.write将压缩的 PNG 存储在临时文件中
  • 2.2 - 使用 File.length 检查尺寸, 如果磁盘大小 > 5 MB 返回到步骤 2

  • 3 - 使用 ImageIO.write(...) 保存图像

  • 这个方法行得通,微调一些参数(比如比例因子)我就可以完成任务了。
    我试图了解是否可以通过计算/猜测最终文件大小而不将图像存储在临时文件中来改进所有内容。
    有一个byte[] obj 存储到 BufferedImage我可以使用的 obj BufferedImage.getData().getDataBuffer()表示图像的内容,但显然由于 PNG 压缩算法,该数组的大小比文件的最终大小大 2 倍或 3 倍。
    我尝试了一些公式来计算值,例如:w * h * bitDepth * 8 / 1024 / 1024但我确信我丢失了很多数据并且帐户没有加起来!

    目前我主要使用以下代码:
    static void resize(BufferedImage image, String outPath, int scalingFactor) throws Exception {
        image = Scalr.resize(image, image.getWidth() - scalingFactor);
    
        // image.getData().getDataBuffer() - the byteArray containing image
    
        File tempFile = File.createTempFile("" + System.currentTimeMillis(), ".png");
        ImageIO.write(image, "png", tempFile);
    
        System.out.println("Calculated size in bytes is: " + tempFile.length() + " - factor: " + scalingFactor);
    
        // MAX_SIZE defined in bytes
        if (tempFile.length() > MAX_SIZE) {
            // recursion starts here
            resize(image, outPath, chooseFactor(tempFile, 4));
        } else {
            // break the recursive cycle
            ImageIO.write(image, "png", new File(outPath));
        }
    }
    
    static int chooseFactor(File image, int scale) {
        // MEGABYTE is 1024*1024
        double mbSize = (double) image.length() / MEGABYTE;
        return (int) ((mbSize / scale) * 100);
    }
    

    有一种方法可以计算/猜测从 BufferedImage 开始的最终文件大小目的?
    请告诉我我是否已说明清楚,或者我是否可以提供更多信息。
    如果您认为问题的解释性不够,还可以为问题推荐一个更合适的标题。
    谢谢。

    最佳答案

    任何沿图像宽度/高度的单调函数都可用于执行二分搜索。

    This approach will work well for many changes that may be needed (changing from PNG to JPG, adding compression, changing optimization targets) versus an ad-hoc solution such as directly predicting the size of a PNG (which, for example, could simply change depending on what library is installed on your production servers or on the client that your application uses).


    预计存储的字节是单调的(无论如何,我的实现在没有单调函数的情况下是安全的 [但不是最佳的])。
    此函数执行二分查找 到下域 (例如不放大图像)使用任何函数:
    static BufferedImage downScaleSearch(BufferedImage source, Function<BufferedImage, Boolean> downScale) {
    
        int initialSize = Math.max(source.getWidth(), source.getHeight());
    
        int a = 1;
        int b = initialSize;
    
        BufferedImage image = source;
        while(true) {
            int c = (a + b) / 2 - 1;
    
            // fix point
            if(c <= a)
                return image;
    
            BufferedImage scaled = Scalr.resize(source, c);
            if(downScale.apply(scaled)) {
                b = c;
            } else {
                // the last candidate will be the not greater than limit
                image = scaled;
                a = c;
            }
        }
    }
    
    如果我们对 final 感兴趣 PNG 文件大小,搜索功能将为 PNG 尺寸:
    static final Path output = Paths.get("/tmp/downscaled.png");
    
    static long persistAndReturnSize(BufferedImage image) {
        if(ImageIO.write(image, "png", output.toFile()))
            return Files.size(output);
        throw new RuntimeException("Cannot write PNG file!");
    }
    
    (您可以坚持使用内存而不是文件系统)。
    现在,我们可以生成大小不超过任何固定值的图像
    public static void main(String... args) throws IOException {
    
        BufferedImage image = ImageIO.read(Paths.get("/home/josejuan/tmp/test.png").toFile());
    
        for(long sz: asList(10_000, 30_000, 80_000, 150_000)) {
            final long MAX_SIZE = sz;
            BufferedImage bestFit = downScaleSearch(image, i -> persistAndReturnSize(i) >= MAX_SIZE);
            ImageIO.write(bestFit, "png", output.toFile());
            System.out.println("Size: " + sz + " >= " + Files.size(output));
        }
    
    }
    
    带输出
    Size: 10000 >= 9794
    Size: 30000 >= 29518
    Size: 80000 >= 79050
    Size: 150000 >= 143277
    
    注意:如果你不使用压缩或者你承认一个近似值,你可能可以替换 persistAndReturnSize函数由 estimator没有坚持形象。
    注意:我们的搜索空间是 size = 1, 2, ...但是你可以使用更多的参数来执行类似的搜索,比如压缩级别、像素颜色空间等(尽管,你的域可能不是单调的,你应该使用 https://en.wikipedia.org/wiki/Gradient_descent 或类似的)。

    关于java - 如何缩放PNG图像直到达到目标文件大小,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69040662/

    相关文章:

    java - 如何查找字符串中的字符总数?

    具有通用版本 8 和 17 的 Java 实例类型

    java - 如何绘制带有色调的 BufferedImage

    java - 手动构造的 BufferedImage 不会绘制到 JFrame 上

    java - 更改方法 header 中的参数

    java - Android 通过滑动整个适配器 View 填充一个栏

    java - 增加 2gig+ XML 文件的 JAVA 堆空间

    bufferedimage - JCodec 图片转 BufferedImage

    java - 在Java中组合多个图像

    java - 操作图像而不删除其 EXIF 数据