Mark Stephens Mark founded the company and has worked with Java and PDF since 1997. The original creator of the core code, he is also a NetBeans enthusiast who enjoys speaking at conferences and reading. He holds an Athletics Blue and an MA in Mediaeval History from St. Andrews University.

Java Image Processing: Libraries, Code & Format Support

7 min read

image formats java

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:

FormatReadWriteNotes
AVIFnono
BMPyesyes
DICOMnono
GIFyesyesAnimation read since Java 6; writing single-frame only
HEIC / HEIFnonoiPhone default
JPEGyesyesBaseline only. No JPEG 2000, no JPEG XL, no CMYK
JPEG 2000nono
JPEG XLnono
PNGyesyesFull 8/16-bit, indexed, alpha
TIFFyes (Java 9+)yes (Java 9+)Baseline plus common extensions; compression coverage is thinner than commercial libs
WebPnono

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:

CapabilityTwelveMonkeysApache Commons ImagingJDeli
LicenceBSD-3Apache 2.0Commercial
Standard web formats (JPEG, PNG, GIF, TIFF)yesyes (slower encoders)yes (optimised encoders)
Modern formats (HEIC, WebP, AVIF, JPEG XL)WebP read only; HEIC experimentalnoyes (all)
Specialised formats (JPEG 2000, DICOM)nonoyes (all)
EXIF preservation through conversionnomanualautomatic
Multi-threaded encodingnonoyes
Commercial support and SLAnonoyes
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?

Mark Stephens Mark founded the company and has worked with Java and PDF since 1997. The original creator of the core code, he is also a NetBeans enthusiast who enjoys speaking at conferences and reading. He holds an Athletics Blue and an MA in Mediaeval History from St. Andrews University.