Image processing in Java means reading raster image data into memory, transforming the pixels, and writing the result back to a file or stream. The JDK ships with java.awt.image.BufferedImage and the javax.imageio.ImageIO API, which together cover BMP, GIF, JPEG, PNG, and (from Java 9 onward) baseline TIFF.
Anything beyond those formats, HEIC from iPhones, JPEG 2000 from medical and archival systems, JPEG XL, AVIF, requires a third-party library. This guide walks through the JDK primitives, the two open-source libraries Java teams most often reach for, and where JDeli can replace both for production workloads.
What the JDK Gives You Out of the Box
Built-in format support
ImageIO is plugin-based. The default plugins shipped with java.desktop give you the formats below:
| Format | Read | Write | Notes |
|---|---|---|---|
| AVIF | no | no | |
| BMP | yes | yes | |
| DICOM | no | no | |
| GIF | yes | yes | Animation read since Java 6; writing single-frame only |
| HEIC / HEIF | no | no | iPhone default |
| JPEG | yes | yes | Baseline only. No JPEG 2000, no JPEG XL, no CMYK |
| JPEG 2000 | no | no | |
| JPEG XL | no | no | |
| PNG | yes | yes | Full 8/16-bit, indexed, alpha |
| TIFF | yes (Java 9+) | yes (Java 9+) | Baseline plus common extensions; compression coverage is thinner than commercial libs |
| WebP | no | no |
If your input is browser-friendly content you control, ImageIO is fine. If your input is whatever a user uploads from a phone, or whatever an enterprise customer sends you, the JDK alone will fail on a meaningful percentage of files.
Reading and writing with BufferedImage
Java adds a really nice abstraction providing a general BufferedImage object which code can manipulate. ImageIO provides the means to create this from supported Image formats and to save out at the end.
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class ReadWriteExample {
public static void main(String[] args) throws IOException {
BufferedImage image = ImageIO.read(new File("input.jpg"));
if (image == null) {
throw new IOException("No registered ImageIO reader could decode the file");
}
System.out.println(image.getWidth() + "x" + image.getHeight());
boolean written = ImageIO.write(image, "png", new File("output.png"));
if (!written) {
throw new IOException("No writer found for format 'png'");
}
}
}
There are two non-obvious things here. ImageIO.read returns null rather than throwing when no reader matches. ImageIO.write returns false in the same situation. A lot of production bugs are silent because nobody checked these return values.
Common Image Operations in Java
These eight cover roughly 90% of real-world tasks. Each uses only the JDK and works with any library that produces a BufferedImage.
Resize with high-quality interpolation
public static BufferedImage resize(BufferedImage src, int width, int height) {
BufferedImage dst = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = dst.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
g.drawImage(src, 0, 0, width, height, null);
g.dispose();
return dst;
}
Crop Images
public static BufferedImage crop(BufferedImage src, int x, int y, int w, int h) {
return src.getSubimage(x, y, w, h);
}
getSubimage shares the pixel buffer with the source. Wrap it in a new BufferedImage if you need an independent copy.Rotate by arbitrary degrees
public static BufferedImage rotate(BufferedImage src, double degrees) {
double radians = Math.toRadians(degrees);
double sin = Math.abs(Math.sin(radians));
double cos = Math.abs(Math.cos(radians));
int w = src.getWidth();
int h = src.getHeight();
int newW = (int) Math.floor(w * cos + h * sin);
int newH = (int) Math.floor(h * cos + w * sin);
BufferedImage dst = new BufferedImage(newW, newH, src.getType());
Graphics2D g = dst.createGraphics();
g.translate((newW – w) / 2, (newH – h) / 2);
g.rotate(radians, w / 2.0, h / 2.0);
g.drawRenderedImage(src, null);
g.dispose();
return dst;
}
Convert to grayscale
public static BufferedImage toGrayscale(BufferedImage src) {
BufferedImage gray = new BufferedImage(src.getWidth(), src.getHeight(),
BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g = gray.createGraphics();
g.drawImage(src, 0, 0, null);
g.dispose();
return gray;
}
Blur with a convolution kernel
public static BufferedImage blur(BufferedImage src) {
float[] kernel = {
1/9f, 1/9f, 1/9f,
1/9f, 1/9f, 1/9f,
1/9f, 1/9f, 1/9f
};
BufferedImageOp op = new ConvolveOp(new Kernel(3, 3, kernel),
ConvolveOp.EDGE_NO_OP, null);
return op.filter(src, null);
}
Adjust brightness
public static BufferedImage adjustBrightness(BufferedImage src, float factor) {
return new RescaleOp(factor, 0, null).filter(src, null);
}
Flip horizontally
public static BufferedImage flipHorizontal(BufferedImage src) {
AffineTransform tx = AffineTransform.getScaleInstance(-1, 1);
tx.translate(-src.getWidth(), 0);
return new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR).filter(src, null);
}
Convert between formats
BufferedImage image = ImageIO.read(new File("photo.jpg"));
ImageIO.write(image, "png", new File("photo.png"));
Converting a JPEG to PNG with alpha will not magically add transparency; ImageIO re-encodes the existing samples. If the source was opaque, the output stays opaque.
When the JDK Isn’t Enough
The JDK is fine until one of these happens:
- A user uploads a .heic file from an iPhone and ImageIO.read returns null
- A customer ships you a CMYK JPEG and the decoder throws
- You need to write JPEG 2000 for a medical or archival pipeline
- You need to preserve EXIF, IPTC, or ICC metadata through a transform
- You need throughput the single-threaded ImageIO encoders cannot deliver
We will look at two open-source solutions; TwelveMonkeys ImageIO and Apache Commons Imaging, in addition to JDeli, to handle the realistic expectation of what teams can expect.
TwelveMonkeys ImageIO Plugin
TwelveMonkeys is a set of ImageIO plugins that drop in via the classpath and silently extend ImageIO.read and ImageIO.write with no API changes. If you only add one open-source library to your project, this is the one.
Maven Dependencies
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-jpeg</artifactId>
<version>3.13.1</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-tiff</artifactId>
<version>3.13.1</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-webp</artifactId>
<version>3.13.1</version>
</dependency>
Usage
The API is identical to the JDK:
BufferedImage image = ImageIO.read(new File("photo.webp"));
ImageIO.write(image, "tiff", new File("output.tiff"));
What it fixes
CMYK JPEG decoding, better handling of corrupted JPEGs than stock ImageIO, broader TIFF compression coverage, and a WebP decoder. PSD and PCX read support if you add those plugins.
What it doesn’t cover
This solution is unsuitable for large scale use because it lacks support for modern formats like AVIF or JPEG XL and omits DICOM compatibility. With an experimental and stagnant HEIC plugin, slow development, no multi-threaded encoding, and no commercial support, it fails to meet demanding production standards.
Apache Commons Imaging Alternative Library
Apache Commons Imaging (formerly Sanselan) is pure Java with a different focus: metadata reading, format detection without decoding, and some obscure format coverage. Treat it as a complement to ImageIO, not a replacement.
Maven Dependencies
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-imaging</artifactId>
<version>1.0.0-alpha6</version>
</dependency>
The version is still alpha after years of development. The metadata API is stable enough for production use; expect occasional incompatibilities between alpha releases.
Reading EXIF metadata
This is the primary reason to use it. ImageIO does not give you EXIF without significant ceremony; Apache Commons Imaging does.
import org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.common.ImageMetadata;
import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
ImageMetadata metadata = Imaging.getMetadata(new File("photo.jpg"));
if (metadata instanceof JpegImageMetadata jpegMeta) {
// Camera model
String model = jpegMeta.findExifValueWithExactMatch(
ExifTagConstants.EXIF_TAG_LENS_MODEL).getStringValue();
// GPS coordinates if present
TiffImageMetadata exif = jpegMeta.getExif();
if (exif != null && exif.getGpsInfo() != null) {
double lat = exif.getGpsInfo().getLatitudeAsDegreesNorth();
double lon = exif.getGpsInfo().getLongitudeAsDegreesEast();
System.out.printf("Captured by %s at %.6f, %.6f%n", model, lat, lon);
}
}
Detecting format without decoding
Useful when validating uploaded files before allocating a decoder for them.
import org.apache.commons.imaging.ImageFormat;
import org.apache.commons.imaging.ImageFormats;
import org.apache.commons.imaging.Imaging;
ImageFormat format = Imaging.guessFormat(new File("upload.bin"));
if (format == ImageFormats.UNKNOWN) {
throw new IllegalArgumentException("Not a recognised image format");
}
What it fixes
EXIF, IPTC, and ICC profile reading without writing your own parsers. Format sniffing without decoding. PCX, TGA, XBM, XPM and a handful of other niche formats.
What it doesn’t cover
This library is in alpha status with no commercial support and lacks essential features, including multi-threaded encoding, advanced format support, and encoder tuning. Its decoding performance significantly lags behind alternatives like ImageIO and TwelveMonkeys, making it less reliable for commercial use.
JDeli ImageIO Plugin and Alternative Image Library
JDeli is IDR Solutions’ Java image SDK. Full disclosure: it is our product. The case for using it is narrow and specific, and best made against the open-source alternatives directly.
Drop-in API
The API is deliberately near drop-in for ImageIO:
import com.idrsolutions.image.JDeli;
import com.idrsolutions.image.encoder.OutputFormat;
import java.awt.image.BufferedImage;
import java.io.File;
// Read any supported format into a BufferedImage
BufferedImage image = JDeli.read(new File("photo.heic"));
// Write to any supported format
JDeli.write(image, OutputFormat.JPEG, new File("photo.jpg"));
// Or convert directly without round-tripping through BufferedImage
JDeli.convert(new File("scan.jp2"), new File("scan.png"));
The convert path is faster than read-then-write because it skips the BufferedImage materialisation when the operation doesn’t need pixel access.
Per-format encoder options
Quality, progressive encoding, chroma subsampling, and similar settings are exposed as typed encoder option classes:
import com.idrsolutions.image.encoder.options.JpegEncoderOptions;
import com.idrsolutions.image.encoder.options.PngEncoderOptions;
JpegEncoderOptions jpegOptions = new JpegEncoderOptions();
jpegOptions.setQuality(0.92f);
JDeli.write(image, jpegOptions, new File("photo.jpg"));
PngEncoderOptions pngOptions = new PngEncoderOptions();
pngOptions.setCompressionFormat(PngCompressionFormat.QUANTISED8BIT);
JDeli.write(image, pngOptions, new File("photo.png"));
Reading from streams
Real applications rarely have a File. JDeli accepts InputStream directly:
try (InputStream in = new FileInputStream("photo.heic")) {
BufferedImage image = JDeli.read(in);
// process
}
Multi-page TIFF
A common production requirement that the open-source options handle poorly or not at all:
import com.idrsolutions.image.tiff.TiffEncoder;
//if you already have a multi-page file like dicom you can just use jdeli convert
JDeli.convert(new File("input.dcm", new TiffEncoderOptions(), new File("output.tiff")));
//or you can grab your own separate images
List pages = scanPagesFromBatch();
try (TiffEncoder encoder = new TiffEncoder() {
for (BufferedImage page : pages) {
encoder.append(page, "outputFile.tiff");
}
}
EXIF and metadata preservation
A standard ImageIO round trip discards EXIF. JDeli preserves it by default on format conversions where the target supports it:
// EXIF orientation, GPS, camera data preserved automatically
JDeli.convert(new File("photo.heic"), new File("photo.jpg"));
Multi-threaded encoding
JDeli’s JPEG and PNG encoders parallelise across cores for large images. Open-source ImageIO encoders are single-threaded per image, which becomes the bottleneck for any pipeline that processes images above ~10 megapixels at scale.
Comparison: JDeli vs TwelveMonkeys vs Apache Commons Imaging
The table below summarises the differences between the 3 solutions we have mentioned:
| Capability | TwelveMonkeys | Apache Commons Imaging | JDeli |
|---|---|---|---|
| Licence | BSD-3 | Apache 2.0 | Commercial |
| Standard web formats (JPEG, PNG, GIF, TIFF) | yes | yes (slower encoders) | yes (optimised encoders) |
| Modern formats (HEIC, WebP, AVIF, JPEG XL) | WebP read only; HEIC experimental | no | yes (all) |
| Specialised formats (JPEG 2000, DICOM) | no | no | yes (all) |
| EXIF preservation through conversion | no | manual | automatic |
| Multi-threaded encoding | no | no | yes |
| Commercial support and SLA | no | no | yes |
| Active maintenance (2026) | yes (slow releases) | yes (alpha status) | yes |
As experienced Java developers, we help you work with images in Java and bring over a decade of hands-on experience with many image file formats.
Are you a Java Developer working with Image files?
// Read an image
BufferedImage bufferedImage = JDeli.read(avifImageFile);
// Write an image
JDeli.write(bufferedImage, "avif", outputStreamOrFile);// Read an image
BufferedImage bufferedImage = JDeli.read(dicomImageFile);// Read an image
BufferedImage bufferedImage = JDeli.read(heicImageFile);
// Write an image
JDeli.write(bufferedImage, "heic", outputStreamOrFile);// Read an image
BufferedImage bufferedImage = JDeli.read(jpegImageFile);
// Write an image
JDeli.write(bufferedImage, "jpeg", outputStreamOrFile);
// Read an image
BufferedImage bufferedImage = JDeli.read(jpeg2000ImageFile);
// Write an image
JDeli.write(bufferedImage, "jpx", outputStreamOrFile);
// Write an image
JDeli.write(bufferedImage, "pdf", outputStreamOrFile);
// Read an image
BufferedImage bufferedImage = JDeli.read(pngImageFile);
// Write an image
JDeli.write(bufferedImage, "png", outputStreamOrFile);
// Read an image
BufferedImage bufferedImage = JDeli.read(tiffImageFile);
// Write an image
JDeli.write(bufferedImage, "tiff", outputStreamOrFile);
// Read an image
BufferedImage bufferedImage = JDeli.read(webpImageFile);
// Write an image
JDeli.write(bufferedImage, "webp", outputStreamOrFile);
What is JDeli?
JDeli is a commercial Java Image library that is used to read, write, convert, manipulate and process many different image formats.
Why use JDeli?
To handle many well known formats such as JPEG, PNG, TIFF as well as newer formats like AVIF, HEIC and JPEG XL in java with no calls to any external system or third party library.
What licenses are available?
We have 3 licenses available:
Server for on premises and cloud servers, Distribution for use in a named end user applications, and Custom for more demanding requirements.
How does JDeli compare?
We work hard to make sure JDeli performance is better than or similar to other java image libraries. Check out our benchmarks to see just how well JDeli performs.