java - 如何缩放PNG图像直到达到目标文件大小
问题描述
正如标题所说,我正在尝试调整 PNG 图像的大小以达到目标文件大小(以兆字节为单位)。
我在 SO 和网络上进行了很多搜索,发现了很多代码,但它们都没有考虑到最终文件的大小。
我已经安排了一些代码,但试图优化性能。
例子:
- 源图像尺寸 = 30 MB
- 目标文件输出大小 = 5 MB
电流:
- 1 - 将PNG图像加载为
BufferedImage
- 2 - 递归使用
Scalr.resize(...)
以调整图像大小- 2.1 - 对于每个步骤,用于
ImageIO.write
将压缩的 PNG 存储在临时文件中 - 2.2 - 检查大小
File.length
,如果磁盘大小 > 5 MB 返回步骤 2
- 2.1 - 对于每个步骤,用于
- 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
对象开始计算/猜测最终文件大小?
请告诉我我是否已经明确表示或者我可以提供更多信息。
如果您认为该问题的解释性不够,还建议为该问题设置一个更合适的标题。
谢谢。
解决方案
沿图像宽度/高度的任何单调函数都可用于执行二分搜索。
这种方法适用于许多可能需要的更改(从 PNG 更改为 JPG、添加压缩、更改优化目标)与直接预测 PNG 的大小(例如取决于您的生产服务器或应用程序使用的客户端上安装的库)。
存储的字节预计是单调的(无论如何我的实现在没有单调函数的情况下是安全的 [但不是最佳的])。
该函数使用任何函数对低域执行二分搜索(例如,不放大图像):
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;
}
}
}
如果我们对最终的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
替换函数而不保留图像。
注意:我们的搜索空间是size = 1, 2, ...
,但您可以使用更多参数(如压缩级别、像素颜色空间、... .org/wiki/Gradient_descent或类似文件)。
推荐阅读
- php - 一类得到“PHP 致命错误:未捕获的错误:类...”,但其他类没有
- awk - AWK:将时间戳转换为纪元;第一条记录总是返回 -1
- python - 在opencv中制作用户定义的阈值
- echarts - 获取堆叠系列中点的像素坐标
- javascript - 知道为什么这个“if else”不能正常工作吗?
- time-complexity - 如何从 Big-O 表示法中找到运行时?
- javascript - 需要复制我创建的模态,但我不确定需要更改哪些属性才能这样做?需要复制3次
- python - python humanclicker 将缓慢移动并且不会在几分之一秒内移动
- microsoft-graph-api - MS Teams - 从外部应用程序向用户发送消息
- grpc-python - 如何对 gRPC 服务器的异步方法进行单元测试?