As the title says I'm trying to resize a PNG image in order to reach a target file size (in terms of MegaBytes).
I searched a lot on SO and over the web, found a lot of codes but all of them are not taking in consideration the final file size.
I've arranged some code but trying to optimize performance.
Example:
Current flow:
BufferedImageScalr.resize(...) in order to resize the image
ImageIO.write to store the compressed PNG on a temporary fileFile.length, if size on disk is > 5 MB return to step 2ImageIO.write(...)This method works, fine-tuning some parameters (such as scale factor) I'm able to accomplish the task.
I'm trying to understand if I can improve all by calculating/guessing the final file size without storing the image on a temporary file.
There is a byte[] obj stored into the BufferedImage obj that I can get using BufferedImage.getData().getDataBuffer() that represents the content of the image but obviously the size of this array is 2x o 3x bigger than the final size of the file because of the PNG compression algo.
I've tried some formula in order to calculate the value, something like:
w * h * bitDepth * 8 / 1024 / 1024 but I'm sure that I'm loosing a lot of data and the accounts do not add up!
At the moment I'm using mainly this code:
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);
}
There is a way to calculate/guess the final file size starting from BufferedImage object?
Please tell me if I have made myself clear or can I give additional information.
Also recommend a more appropriate title for the question if you think it is not explanatory enough.
Thanks.
Any monotone function along image width/height can be used to perform a binary search.
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).
The stored bytes is expected to be monotone (anyway my implementation is safe [but not optimal] under no monotonic functions).
This function perform a binary search to the lower domain (e.g. not upscale the image) using any function:
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;
}
}
}
if we are interested in the final PNG file size, the search function will be the PNG size:
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!");
}
(you could persist to memory instead filesystem).
now, we can generate images with size no more than any fixed value
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));
}
}
with output
Size: 10000 >= 9794
Size: 30000 >= 29518
Size: 80000 >= 79050
Size: 150000 >= 143277
NOTE: if you do not use compression or you admit an approximation, probably you can replace the persistAndReturnSize function by an estimator without persist the image.
NOTE: our search space is size = 1, 2, ... but you could perform a similar search using more parameters like compression level, pixel color space, ... (although, probably, your domain then will be not monotone and you should use https://en.wikipedia.org/wiki/Gradient_descent or similar).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With