So you'd end up with a BI that is of TYPE_CUSTOM ?
That might make decoding a bit faster but rendering would be very slow.
I'm not sure I'd want that trade-off.
-phil.
On 1/6/26 8:59 AM, Jeremy Wood wrote:
I’m exploring the performance of loading BufferedImages. I recently
noticed that ImageIO.read() can be made ~15% faster by changing the
BufferedImage type (so we'd avoid a RGB -> BGR conversion).
IMO this wouldn’t be too hard to program, but it might be considered
too invasive to accept. Is there any interest/support in exploring
this idea if I submit a PR for it?
Specifically this is the performance I’m observing on my MacBook:
Benchmark Mode Cnt Score Error
Units
JPEG_Default_vs_RGB.measureDefaultImageType avgt 15 42.589 ± 0.137
ms/op
JPEG_Default_vs_RGB.measureRGBImageType avgt 15 35.624 ± 0.589
ms/op
The first “default” approach uses ImageIO.read(inputStream).
The second “RGB” approach creates a BufferedImage target with a custom
ColorModel that is similar to TYPE_3BYTE_BGR, except it reverse the
colors so they are ordered RGB.
This derives from the observation that in JPEGImageReader we create a
one-line raster field that uses this 3-byte RGB model. Later in
acceptPixels() we call target.setRect(x, y, raster) . Here target is
the WritableRaster of the final BufferedImage. By default it will be a
3-byte BGR. So we’re spending 15+% of our time converting RGB-encoded
data (from raster) to BGR-encoded data (for target).
So the “pros” of my proposal should include a faster loading time for
many JPEG images. I’d argue ImageIO should always default to the
fastest (reasonable) implementation possible.
IMO the major “con” is: target.getType() would change from
BufferedImage.TYPE_3BYTE_BGR to BufferedImage.TYPE_CUSTOM . This
doesn’t technically violate any documentation that I know of, but it
seems (IMO) like something some clients will have made assumptions
about, and therefore some downstream code may break. (And maybe other
devs here can identify other problems I’m not anticipating.)
Any thoughts / feedback?
Regards,
- Jeremy
Below is the JMH code used to generate the output above:
package org.sun.awt.image;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 20)
@Fork(3)
@State(Scope.Thread)
public class JPEG_Default_vs_RGB {
byte[] jpgImageData;
@Setup
public void setup() throws Exception {
jpgImageData = createImageData(2_500);
}
@Benchmark
public void measureDefaultImageType(Blackhole bh) throws Exception {
BufferedImage bi = readJPG(false);
bi.flush();
bh.consume(bi);
}
@Benchmark
public void measureRGBImageType(Blackhole bh) throws Exception {
BufferedImage bi = readJPG(true);
bi.flush();
bh.consume(bi);
}
private BufferedImage readJPG(boolean useRGBTarget) throws Exception {
Iterator<ImageReader> readers;
try (ByteArrayInputStream byteIn = new
ByteArrayInputStream(jpgImageData)) {
if (!useRGBTarget)
return ImageIO.read(byteIn);
readers =
ImageIO.getImageReaders(ImageIO.createImageInputStream(byteIn));
if (!readers.hasNext()) {
throw new IOException("No reader found for the given
file.”);
}
}
ImageReader reader = readers.next();
try (ByteArrayInputStream byteIn = new
ByteArrayInputStream(jpgImageData)) {
reader.setInput(ImageIO.createImageInputStream(byteIn));
int width = reader.getWidth(0);
int height = reader.getHeight(0);
// this is copied from how BufferedImage sets up a
TYPE_3BYTE_BGR image,
// except we use {0, 1, 2} to make it an RGB image:
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
int[] nBits = {8, 8, 8};
int[] bOffs = {0, 1, 2};
ColorModel colorModel = new ComponentColorModel(cs, nBits,
false, false,
Transparency.OPAQUE,
DataBuffer.TYPE_BYTE);
WritableRaster raster =
Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE,
width, height, width * 3, 3, bOffs, null);
BufferedImage rgbImage = new BufferedImage(colorModel,
raster, false, null);
ImageReadParam param = reader.getDefaultReadParam();
param.setDestination(rgbImage);
reader.read(0, param);
return rgbImage;
} finally {
reader.dispose();
}
}
/**
* Create a large sample image stored as a JPG
*
* @return the byte representation of the JPG image.
*/
private static byte[] createImageData(int squareSize) throws
Exception {
BufferedImage bi = new BufferedImage(squareSize, squareSize,
BufferedImage.TYPE_INT_RGB);
Random r = new Random(0);
Graphics2D g = bi.createGraphics();
for (int a = 0; a < 20000; a++) {
g.setColor(new Color(r.nextInt(0xffffff)));
int radius = 10 + r.nextInt(90);
g.fillOval(r.nextInt(bi.getWidth()),
r.nextInt(bi.getHeight()),
radius, radius);
}
g.dispose();
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
ImageIO.write(bi, "jpg", out);
return out.toByteArray();
}
}
}