I have written a method that should compress a JPEG image using the ImageIO lib from Java. However, when I attempt to compress some images, their size actually increases! (from approx 21 kb to 36 kb). Any idea why this is happening? My code is shown below:
public boolean compressJPG(String originPath, String destinationPath){
float compression = 0.1f;
File in = new File(originPath);
File out = new File(destinationPath);
try {
RenderedImage image = ImageIO.read(in);
ImageWriter writer = null;
Iterator iter = ImageIO.getImageWritersByFormatName("jpg");
if(iter.hasNext()){
writer = (ImageWriter) iter.next();
}
ImageOutputStream outStream = ImageIO.createImageOutputStream(out);
writer.setOutput(outStream);
MyImageWriteParam iwparam = new MyImageWriteParam();
iwparam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT) ;
iwparam.setCompressionQuality(compression);
writer.write(null, new IIOImage(image, null, null), iwparam);
outStream.flush();
writer.dispose();
outStream.close();
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
public float setJPGCompressionQuality(float quality) {
if (quality < 0.0F || quality > 1.0F) {
throw new IllegalArgumentException("Quality out-of-bounds!");
}
return 256 - (quality * 256);
}
It may happened due to mode ImageWriteParam.MODE_EXPLICIT.
If the intersection is non-empty, writing will commence with the first subsampled pixel and include additional pixels within the intersected bounds according to the horizontal and vertical subsampling factors specified by IIOParam.setSourceSubsampling.
Use MODE_DEFAULT instead of MODE_EXPLICIT.
Related
Using Java ImageIO, is it possible to export a jpeg image that has a bit-depth of 8? How would I do this? Even when exporting a BufferedImage of TYPE_BYTE_BINARY, which is a grayscale image, the result is a JPEG with bit-depth of 24.
This is what I have so far.
public void testJpegBitDepth() throws Exception{
Path pIn = Paths.get("testing/jpg/box1.jpg"), pOut;
BufferedImage bi;
//*******************************************
//Write 8 bit jpg
//Init ImageWriter
Iterator<ImageWriter> it = ImageIO.getImageWritersByFormatName("jpg");
ImageWriter writer = null;
while(it.hasNext()) {
try {
writer = it.next();
//Read input
bi = ImageIO.read(pIn.toFile());
if(bi == null)
throw new Exception("Failed to read input file: " + pIn);
//Convert to gray
bi = AWTImaging.convertToGray(bi);
log.debug("Num bands from the image raster: " + bi.getRaster().getNumBands());
pOut = test.outputDir.resolve("jpegBitDepth-8-"
+ pIn.getFileName().toString() + ".jpg");
//Init ImageTypeSpecifier
ImageTypeSpecifier imageType = ImageTypeSpecifier.createGrayscale(
8, //8 bits per pixel
DataBuffer.TYPE_BYTE, //stored in a byte
false); //unsigned
//Init WriteParam
ImageWriteParam param = writer.getDefaultWriteParam();
param.setDestinationType(imageType);
//Not sure if this is required or not, but the same Exception occurs either way
//param.setSourceBands(new int[] {0});
//Init meta
IIOMetadata meta = writer.getDefaultImageMetadata(imageType, param);
String metadataFormat = "javax_imageio_jpeg_image_1.0";
IIOMetadataNode root = new IIOMetadataNode(metadataFormat);
IIOMetadataNode jpegVariety = new IIOMetadataNode("JPEGvariety");
IIOMetadataNode markerSequence = new IIOMetadataNode("markerSequence");
//I think we want app0JFIF metadata here, as it can specify a grayscale image https://docs.oracle.com/javase/10/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html
IIOMetadataNode app0JFIF = new IIOMetadataNode("app0JFIF");
root.appendChild(jpegVariety);
root.appendChild(markerSequence);
jpegVariety.appendChild(app0JFIF);
meta.mergeTree(metadataFormat, root);
//Export jpg
Files.deleteIfExists(pOut);
ImageOutputStream ios = ImageIO.createImageOutputStream(pOut.toFile());
writer.setOutput(ios);
writer.write(meta, new IIOImage(bi, null, meta), param);
log.debug("Succeded writing jpeg with writer: " + writer.getClass().toString());
break;
}catch(Exception e) {
log.error("Failed writing jpeg with writer: " + (writer != null ? writer.getClass().toString():"null"));
log.error("Ex: " + e);
}
}
}
I'm getting an Exception thrown from JpegImageWriter, here is the relevant stack trace:
Ex: javax.imageio.IIOException: Metadata components != number of destination bands
File=null,Class=com.sun.imageio.plugins.jpeg.JPEGImageWriter,Method=checkSOFBands,Line=-1
File=null,Class=com.sun.imageio.plugins.jpeg.JPEGImageWriter,Method=writeOnThread,Line=-1
File=null,Class=com.sun.imageio.plugins.jpeg.JPEGImageWriter,Method=write,Line=-1
Also I know that the Buffered Image is a TYPE_BYTE_BINARY, and the raster has 1 band (I printed this in a debug message above). So the Exception message would make me think that I need to define in the app0JFIF metadata that we are exporting 1 band. I don't know how to define this though, does anyone have any experience with this? This metadata is difficult to work with, or is it just me?
Thanks in advance.
You are correct about needing one band. Here’s how I did it:
if (bi.getSampleModel().getNumBands() != 1) {
ColorModel colorModel = new ComponentColorModel(
ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false,
Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
BufferedImage oneBandedImage = new BufferedImage(colorModel,
colorModel.createCompatibleWritableRaster(
bi.getWidth(), bi.getHeight()),
false, new Properties());
Graphics g = oneBandedImage.createGraphics();
g.drawImage(bi, 0, 0, null);
g.dispose();
bi = oneBandedImage;
}
After doing that, I didn’t need to directly obtain an ImageWriter and I didn’t need to set any metadata; ImageIO.write(bi, "JPEG", file) was sufficient.
I ran /usr/bin/file on the result, and got this:
JPEG image data, JFIF standard 1.02, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 315x180, components 1
I assume the components 1 part means that it has only one channel.
I'm trying to compress an image file(jpg) using ImageIO. my goal is to compress image to 30 kb or less. Below is the code i used to compress image. The code is working fine. But it is not compressing to 30 kb, even if i tried again with the compressed image.
Then I try to scale the image. that time i got 30 kb file but quality is vet\y poor.
My file is an image of an application form. So I need the data to be readable.
Is ther any way i can do this with ImageIO or any other libraries.
public void jpegCompression(File imageFile,String path,Integer flag) {
try {
File compressedImageFile = new File(path+flag+"_"+imageFile.getName());
InputStream is = new FileInputStream(imageFile);
OutputStream os = new FileOutputStream(compressedImageFile);
float quality = 0.1f;
// create a BufferedImage as the result of decoding the supplied InputStream
BufferedImage image = ImageIO.read(is);
// get all image writers for JPG format
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
if (!writers.hasNext())
throw new IllegalStateException("No writers found");
ImageWriter writer = (ImageWriter) writers.next();
ImageOutputStream ios = ImageIO.createImageOutputStream(os);
writer.setOutput(ios);
ImageWriteParam param = writer.getDefaultWriteParam();
// compress to a given quality
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(quality);
// appends a complete image stream containing a single image and
//associated stream and image metadata and thumbnails to the output
writer.write(null, new IIOImage(image, null, null), param);
// close all streams
is.close();
os.close();
ios.close();
writer.dispose();
} catch (IOException e) {
e.printStackTrace();
}
}
I'm using javax.imageio.ImageIO to save a BufferedImage as a jpeg file.
In particular, I created the following Java function:
public static void getScreenShot(BufferedImage capture, Path folder, String filename) {
try {
ImageIO.write(capture, "jpeg", new File(folder.toString()+"/"+filename+".jpg"));
} catch (AWTException | IOException ex) {
Logger.getLogger(ScreenShotMaker.class.getName()).log(Level.SEVERE, null, ex);
}
}
Likewise any image manipulation software, I wish to change the compression level of the jpeg file. However, I'm searching for this option that seems to be missing in ImageIO.
Can I set the compression level and how?
A more succinct way is to get the ImageWriter directly from ImageIO:
ImageWriter jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next();
ImageWriteParam jpgWriteParam = jpgWriter.getDefaultWriteParam();
jpgWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
jpgWriteParam.setCompressionQuality(0.7f);
ImageOutputStream outputStream = createOutputStream(); // For example implementations see below
jpgWriter.setOutput(outputStream);
IIOImage outputImage = new IIOImage(image, null, null);
jpgWriter.write(null, outputImage, jpgWriteParam);
jpgWriter.dispose();
The call to ImageWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT) is needed in order to explicitly set the compression's level (quality).
In ImageWriteParam.setCompressionQuality() 1.0f is maximum quality, minimum compression, while 0.0f is minimum quality, maximum compression.
ImageWriter.setOutput should be passed an ImageOutputStream. While the method accepts Object, according to documentation it's usually not supported:
Use of a general Object other than an ImageOutputStream is intended for writers that interact directly with an output device or imaging protocol. The set of legal classes is advertised by the writer's service provider's getOutputTypes method; most writers will return a single-element array containing only ImageOutputStream.class to indicate that they accept only an ImageOutputStream.
Most cases should be handled by these two classes:
FileImageOutputStream - an implementation of ImageOutputStream that writes its output directly to a File or RandomAccessFile.
MemoryCacheImageOutputStream - an implementation of ImageOutputStream that writes its output to a regular OutputStream. Usually used with ByteArrayOutputStream (thanks for the tip, #lmiguelmh!).
You have to use JPEGImageWriteParam and then save the image with ImageWriter.write(). Before to write, set the output via ImageWriter.setOutput.
Set the compression level as follows:
JPEGImageWriteParam jpegParams = new JPEGImageWriteParam(null);
jpegParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
jpegParams.setCompressionQuality(1f);
Where 1f is a float number that stands for 100% quality. Default value is around 70% if I don't remember wrong.
EDIT
Then, you have to do as follows to get an instance of an ImageWriter. There are two ways, a short and a long one (I keep both, just in case).
The short way (suggested by lapo in one comment) is:
final ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next();
// specifies where the jpg image has to be written
writer.setOutput(new FileImageOutputStream(
new File(folder.toString() + "/" + filename + ".jpg")));
// writes the file with given compression level
// from your JPEGImageWriteParam instance
writer.write(null, new IIOImage(capture, null, null), jpegParams);
or longer way
// use IIORegistry to get the available services
IIORegistry registry = IIORegistry.getDefaultInstance();
// return an iterator for the available ImageWriterSpi for jpeg images
Iterator<ImageWriterSpi> services = registry.getServiceProviders(ImageWriterSpi.class,
new ServiceRegistry.Filter() {
#Override
public boolean filter(Object provider) {
if (!(provider instanceof ImageWriterSpi)) return false;
ImageWriterSpi writerSPI = (ImageWriterSpi) provider;
String[] formatNames = writerSPI.getFormatNames();
for (int i = 0; i < formatNames.length; i++) {
if (formatNames[i].equalsIgnoreCase("JPEG")) {
return true;
}
}
return false;
}
},
true);
//...assuming that servies.hasNext() == true, I get the first available service.
ImageWriterSpi writerSpi = services.next();
ImageWriter writer = writerSpi.createWriterInstance();
// specifies where the jpg image has to be written
writer.setOutput(new FileImageOutputStream(
new File(folder.toString() + "/" + filename + ".jpg")));
// writes the file with given compression level
// from your JPEGImageWriteParam instance
writer.write(null, new IIOImage(capture, null, null), jpegParams);
A more general method will be (from Igor's answer) :
static void saveImage(BufferedImage image,File jpegFiletoSave,float quality) throws IOException{
// save jpeg image with specific quality. "1f" corresponds to 100% , "0.7f" corresponds to 70%
ImageWriter jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next();
ImageWriteParam jpgWriteParam = jpgWriter.getDefaultWriteParam();
jpgWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
jpgWriteParam.setCompressionQuality(quality);
jpgWriter.setOutput(ImageIO.createImageOutputStream(jpegFiletoSave));
IIOImage outputImage = new IIOImage(image, null, null);
jpgWriter.write(null, outputImage, jpgWriteParam);
jpgWriter.dispose();
}
Found same method in my ancient library:
/**
* Work method.
* Reads the jpeg image in rendImage, compresses the image, and writes it back out to outfile.
* JPEGQuality ranges between 0.0F and 1.0F, 0-lowest, 1-highest. ios is closed internally
*
* #param rendImage [#link RenderedImage} instance with an Rendered Image
* #param ios {#link ImageOutputStream} instance,
* note that it is disposed in this method
* #param JPEGQuality float value for the JPEG compression quality (0..1(max))
* #return {#code true} if image was successfully compressed
* else {#code false} on any error, e.g. bad (null) parameters
*/
public static boolean compressJpegFile( RenderedImage rendImage, ImageOutputStream ios, float JPEGQuality )
{
if ( rendImage == null )
return false;
if ( ios == null )
return false;
if ( ( JPEGQuality <= 0.0F ) || ( JPEGQuality > 1.0F ) )
return false;
ImageWriter writer = null;
try
{
// Find a jpeg writer
Iterator iter = ImageIO.getImageWritersByFormatName( "jpg" );
if ( iter.hasNext() )
writer = (ImageWriter) iter.next();
if ( writer == null )
throw new IllegalArgumentException( "jpg writer not found by call to ImageIO.getImageWritersByFormatName( \"jpg\" )" );
writer.setOutput( ios );
// Set the compression quality
ImageWriteParam iwparam = new MyImageWriteParam();
iwparam.setCompressionMode( ImageWriteParam.MODE_EXPLICIT );
iwparam.setCompressionQuality( JPEGQuality );
// float res = iwparam.getCompressionQuality();
// Write the image
writer.write( null, new IIOImage( rendImage, null, null ), iwparam );
return true;
}
catch ( Exception e )
{
return false;
}
finally
{
if ( writer != null )
writer.dispose();
// Cleanup
try
{
ios.flush();
ios.close();
}
catch ( IOException e )
{
}
}
}
If u require a byte[] output see a modified version of #user_3pij answer below:
private static byte[] compressImageToByteArray(BufferedImage image, float quality)
throws IOException {
// save jpeg image with specific quality. "1f" corresponds to 100% , "0.7f" corresponds to
// 70%
ImageWriter jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next();
ImageWriteParam jpgWriteParam = jpgWriter.getDefaultWriteParam();
jpgWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
jpgWriteParam.setCompressionQuality(quality);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ImageOutputStream ios = ImageIO.createImageOutputStream(bos);
jpgWriter.setOutput(ios);
IIOImage outputImage = new IIOImage(image, null, null);
jpgWriter.write(null, outputImage, jpgWriteParam);
byte[] result = bos.toByteArray();
jpgWriter.dispose();
return result;
}
I have a BufferedImage:
BufferedImage bi = new BufferedImage(14400, 14400, BufferedImage.TYPE_INT_ARGB);
I have saved this image to a PNG file using the following code:
public static void saveGridImage(BufferedImage sourceImage, int DPI,
File output) throws IOException {
output.delete();
final String formatName = "png";
for (Iterator<ImageWriter> iw = ImageIO
.getImageWritersByFormatName(formatName); iw.hasNext();) {
ImageWriter writer = iw.next();
ImageWriteParam writeParam = writer.getDefaultWriteParam();
ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier
.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
IIOMetadata metadata = writer.getDefaultImageMetadata(
typeSpecifier, writeParam);
if (metadata.isReadOnly()
|| !metadata.isStandardMetadataFormatSupported()) {
continue;
}
setDPI(metadata, DPI);
final ImageOutputStream stream = ImageIO
.createImageOutputStream(output);
try {
writer.setOutput(stream);
writer.write(metadata,
new IIOImage(sourceImage, null, metadata), writeParam);
} finally {
stream.close();
}
break;
}
}
public static void setDPI(IIOMetadata metadata, int DPI)
throws IIOInvalidTreeException {
double INCH_2_CM = 2.54;
// for PNG, it's dots per millimeter
double dotsPerMilli = 1.0 * DPI / 10 / INCH_2_CM;
IIOMetadataNode horiz = new IIOMetadataNode("HorizontalPixelSize");
horiz.setAttribute("value", Double.toString(dotsPerMilli));
IIOMetadataNode vert = new IIOMetadataNode("VerticalPixelSize");
vert.setAttribute("value", Double.toString(dotsPerMilli));
IIOMetadataNode dim = new IIOMetadataNode("Dimension");
dim.appendChild(horiz);
dim.appendChild(vert);
IIOMetadataNode root = new IIOMetadataNode("javax_imageio_1.0");
root.appendChild(dim);
metadata.mergeTree("javax_imageio_1.0", root);
}
When the code executes it creates an PNG file with 400 DPI and Disk Size of 168 MB; this is too much.
Is there any way or parameters I can use to save a smaller PNG?
Before, I had a 1.20 GB TIFF file, and when I converted it to PNG using imagemagick at 400 DPI, the resulting file size was only 700 KB.
So, I think I might be able to save the above file smaller.
Can pngj help me? Because I now have a png file which I can read in pngj library.
A 14400x14400 ARGB8 image has a raw (uncompressed) size of 791MB. It will compress more or less according to its nature (has uniform or smooth zones) and according (less important) to the PNG compression parameters.
when i convert it using imagemagic to PNG using 400 DPI , the
resulting file size is only 700 KB.
(I don't understand why you speak of DPI, that has nothing to do, what matters is the size in pixels) Are you saying that you are getting a 14400x14400 ARGB of 700KB? That would represent a compression of 1/1000, hard to believe unless the image is practically flat.
You should first understand what is going on here.
Anyway, here's a sample code with PNGJ
/** writes a BufferedImage of type TYPE_INT_ARGB to PNG using PNGJ */
public static void writeARGB(BufferedImage bi, OutputStream os) {
if(bi.getType() != BufferedImage.TYPE_INT_ARGB)
throw new PngjException("This method expects BufferedImage.TYPE_INT_ARGB" );
ImageInfo imi = new ImageInfo(bi.getWidth(), bi.getHeight(), 8, true);
PngWriter pngw = new PngWriter(os, imi);
pngw.setCompLevel(9);// maximum compression, not critical usually
pngw.setFilterType(FilterType.FILTER_AGGRESSIVE); // see what you prefer here
DataBufferInt db =((DataBufferInt) bi.getRaster().getDataBuffer());
SinglePixelPackedSampleModel samplemodel = (SinglePixelPackedSampleModel) bi.getSampleModel();
if(db.getNumBanks()!=1)
throw new PngjException("This method expects one bank");
ImageLine line = new ImageLine(imi);
for (int row = 0; row < imi.rows; row++) {
int elem=samplemodel.getOffset(0,row);
for (int col = 0,j=0; col < imi.cols; col++) {
int sample = db.getElem(elem++);
line.scanline[j++] = (sample & 0xFF0000)>>16; // R
line.scanline[j++] = (sample & 0xFF00)>>8; // G
line.scanline[j++] = (sample & 0xFF); // B
line.scanline[j++] = (((sample & 0xFF000000)>>24)&0xFF); // A
}
pngw.writeRow(line, row);
}
pngw.end();
}
I would attempt to fiddle with the settings on the writeParam object you're creating. Currently you're calling getDefaultWriteParam(); which gives you a basic writeParam object. My guess is the default would be NO compression.
After doing that, you can probably set some of the compression modes to reduce the file size.
writeParam.setCompressionMode(int mode);
writeParam.setCompressionQuality(float quality);
writeParam.setCompressionType(String compressionType);
See http://docs.oracle.com/javase/6/docs/api/javax/imageio/ImageWriteParam.html
And specifically http://docs.oracle.com/javase/6/docs/api/javax/imageio/ImageWriteParam.html#setCompressionMode(int)
sample code for pngj that works for 2.x versions of the leonbloy's pngj library
/** writes a BufferedImage of type TYPE_INT_ARGB to PNG using PNGJ */
public static void writePNGJARGB(BufferedImage bi, /*OutputStream os, */File file) {
System.out.println(".....entering PNGj alternative image file save mode....." );
if(bi.getType() != BufferedImage.TYPE_INT_ARGB) throw new PngjException("This method expects BufferedImage.TYPE_INT_ARGB" );
ImageInfo imi = new ImageInfo(bi.getWidth(), bi.getHeight(), 8, true);
PngWriter pngw = new PngWriter(file, imi, false);
// PngWriter pngw = new PngWriter(file,imginfo,overwrite); //params
pngw.setCompLevel(7); // tuning compression, not critical usually
pngw.setFilterType(FilterType.FILTER_PAETH); // tuning, see what you prefer here
System.out.println("..... PNGj metadata = "+pngw.getMetadata() );
DataBufferInt db =((DataBufferInt) bi.getRaster().getDataBuffer());
if(db.getNumBanks()!=1) {
throw new PngjException("This method expects one bank");
}
SinglePixelPackedSampleModel samplemodel = (SinglePixelPackedSampleModel) bi.getSampleModel();
ImageLineInt line = new ImageLineInt(imi);
int[] dbbuf = db.getData();
for (int row = 0; row < imi.rows; row++) {
int elem=samplemodel.getOffset(0,row);
for (int col = 0,j=0; col < imi.cols; col++) {
int sample = dbbuf[elem++];
line.getScanline()[j++] = (sample & 0xFF0000)>>16; // R
line.getScanline()[j++] = (sample & 0xFF00)>>8; // G
line.getScanline()[j++] = (sample & 0xFF); // B
line.getScanline()[j++] = (((sample & 0xFF000000)>>24)&0xFF); // A
}
//pngw.writeRow(line, /*imi.rows*/);
pngw.writeRow(line);
}
pngw.end();
}
I need to create tiled pyramid TIFF file with JPEG-compressed tiles from large JPEG image using lossless jpeg operations i.e. crop in jpegtran. Is there any tool or java code around to perform such operation?
Currently I'm using my Java code to create tiled pyramid TIFF, but it doing it in lossy way:
public static void writeTiff(RenderedOp src, File dstFile, float jpegQuality, int tileSize, int pyramid, String interpolation) throws IOException {
dstFile.getParentFile().mkdirs();
TIFFImageWriterSpi imageWriterSpi = new TIFFImageWriterSpi();
TIFFImageWriter imageWriter = (TIFFImageWriter)imageWriterSpi.createWriterInstance();
ImageOutputStream out = new FileImageOutputStream(dstFile);
try {
imageWriter.setOutput(out);
imageWriter.prepareWriteSequence(null);
BufferedImage img = null;
for(int i=0; i<pyramid; i++) {
img = img==null ? src.getAsBufferedImage() : JAIUtils.scale(img, 0.5F, interpolation).getAsBufferedImage();
TIFFImageWriteParam imageWriteParam = (TIFFImageWriteParam)imageWriter.getDefaultWriteParam();
if (tileSize>0 && (img.getWidth()>tileSize || img.getHeight()>tileSize)) {
imageWriteParam.setTilingMode(ImageWriteParam.MODE_EXPLICIT);
imageWriteParam.setTiling(tileSize, tileSize, 0, 0);
}
if (jpegQuality > 0) {
imageWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT );
imageWriteParam.setCompressionType("JPEG");
imageWriteParam.setCompressionQuality(jpegQuality);
}
imageWriter.writeToSequence(new IIOImage(img, null, null), imageWriteParam );
}
imageWriter.endWriteSequence();
} finally {
out.close();
}
}
Ossim works well from the command line, but I see it has a JNI binding, too.