Apologies for any ignorance, but I have never worked with jpeg images (let alone any types of images) in Java before.
Supposing I want to send a jpeg image from a web service to a client. Is there any way that I can reduce the jpeg file size by manipulating the colour profile of the image in some way?
I have already been able to reduce the image size by scaling it using a neat tool for BufferedImages called imgscalr. See here.
I would also like a jpeg that has less colours than a high quality jpeg image. For example, I would like to be able to use 8bit colour in my jpeg instead of say 16bit colour.
What exactly would I need to change if I have a BufferedImage from Java's 2D package?
Another way to reduce image size is to change compression level. You can do that using ImageWriter.
ImageWriter writer = null;
Iterator<ImageWriter> iwi = ImageIO.getImageWritersByFormatName("jpg");
if (!iwi.hasNext())
return;
writer = (ImageWriter) iwi.next();
ImageWriteParam iwp = writer.getDefaultWriteParam();
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT) ;
iwp.setCompressionQuality(compressionQuality);
writer.setOutput(...);
writer.write(null, image, iwp);
The easiest way to do this is to decompress the byte stream into a Java Image, optionally resize it (which makes it smaller) and then regenerate a JPEG image from this with the desired quality setting.
This new image is then what is sent to the client.
Have a look at the ImageIO class. As for reducing file size: since the image would already be a JPEG the only things you could do is reduce the quality or the image size.
Another thing to keep in mind: if the image is a CMYK jpeg it might be bigger. Unfortunately ImageIO can't handle those, but you can try JAI ImageIO to convert from CMYK to RGB (which should be much smaller).
Two of the possible solutions are downscaling the image, here's how you'd do it:
BufferedImage original = //your image here
scaled = original.getScaledInstance(finalWidth, finalHeight, Image.SCALE_SMOOTH); // scale the image to a smaller one
BufferedImage result = new BufferedImage(finalWidth, finalHeight, original.getType());
Graphics2D g = result.createGraphics();
g.drawImage(scaled, 0, 0, null); //draw the smaller image
g.dispose();
Obviously, you have to calculate the scaled width and height so the image stays by the same aspect ratio.
Once you have drawn it smaller, you can now turn this image into a JPEG file:
BufferedImage image = // this is the final scaled down image
JPEGImageEncoder jpegEncoder = JPEGCodec.createJPEGEncoder(output);
JPEGEncodeParam jpegEncodeParam = jpegEncoder.getDefaultJPEGEncodeParam(image);
jpegEncodeParam.setDensityUnit(JPEGEncodeParam.DENSITY_UNIT_DOTS_INCH);
jpegEncodeParam.setXDensity(92);
jpegEncodeParam.setYDensity(92);
jpegEncodeParam.setQuality( 0.8F , false);
jpegEncoder.encode(image, jpegEncodeParam);
These classes are from the JAI package (more exactly com.sun.image.codec.jpeg) and the JVM might complain that they should not be used directly, but you can ignore that.
You can possibly download JAI from here, if it does not work I have github mirrors setup for the two libraries, JAI core and JAI ImageIO.
Related
I need to read and process a large number of PNG files that are grayscale. By that I mean that if they are opened in either Photoshop or GIMP, the image mode is Grayscale - not an RGB image with grayscale values.
ImageIO does not seem to achieve this. It appears to treat all image files as sRGB. This mangles grayscale values. I need to read and process these PNG files where (in my code) each pixel has exactly the same value as if I had opened the grayscale file in Photoshop or GIMP. Does anybody know of some open source software that can achieve this, please? Or better how to achieve this using ImageIO.
Additional Information:
I am using getRGB() on a BufferedImage. The underlying pixel in the image file is 0x86. I understand that this does not necessarily correspond to an ARGB pixel containing 0xFF868686, as this depends upon luminance/gamma. However, in the absence of a getter with a gamma type argument, I would have expected the default mapping to be to ARGB=0xFF868686. If I use GIMP or Photoshop to convert a grayscale image containing a pixel with the value of 0x86 to RGB then the pixel becomes 0xFF868686. This is the obvious default.
However, ImageIO seems to use a weird gamma (whether you like it or not) with grayscale image files that makes the grayscale pixels very, very light after mapping to ARGB. In this case, 0x86 maps to 0xFFC0C0C0. This is not only very light, it can also result in considerable data loss as many grayscale values can be mapped to fewer ARGB values. The only time that this distortion will not result in data loss is for very dark grayscale images. An appropriate Gamma is context dependent, different physical media will distort luminance differently. However, in the absence of a context, the mapping: 0x86 --> 0xFF868686 makes most sense - witness the choices made for GIMP and Photoshop.
Leaving the getRGB() issue to one side, having loaded the grayscale image (using ImageIO.read( imageFile )), the getType() method of BufferedImage returns Type=0 (Custom) and not Type=10 (TYPE_BYTE_GRAY) as I would have expected.
In short, ImageIO does not seem to provide a nice and simple high level way of reading and manipulating existing grayscale images. I had hoped not to have to mess around under the covers with Rasters, ICC, sampling etc. Nor do I want to have to physically convert all the grayscale image files to RGB. All I wanted was an API load() method for BufferedImage that works just like open file does in GIMP or Photoshop. I have not been able to achieve this. I am hoping that this is my ignorance and not a limitation of Java ImageIO.
Possible Solution:
After digging around I have the following to offer as a possible technique for accessing the underlying grayscale values:
final File imageFile = new File( "test.png" );
final BufferedImage image = ImageIO.read( imageFile );
// --- Confirm that image has ColorSpace Type is GRAY and PixelSize==16
final Raster raster = image.getData();
// --- Confirm that: raster.getTransferType() == DataBuffer.TYPE_BYTE
for( int x=0, xLimit=image.getWidth(); x < xLimit; x++ ) {
for( int y=0, yLimit=image.getHeight(); y < yLimit; y++ ) {
final Object dataObject = raster.getDataElements( x, y, null );
// --- Confirm that dataObject is instance of byte[]
final byte[] pixelData = (byte[]) dataObject;
// --- Confirm that: pixelData.length == 2
final int grayscalePixelValue = pixelData[0] & 0xFF;
final int grayscalePixelAlpha = pixelData[1] & 0xFF;
// --- Do something with the grayscale pixel data
}
}
The javadoc is not great, so I cannot guarantee that this is correct, but it seems to work for me.
In case you want to try a third party (mine) lib: https://github.com/leonbloy/pngj/
If you are certain that the image is plain grayscale (8 bits, no alpha, no palette, no profile), it's quite simple:
PngReaderByte pngr = new PngReaderByte(new File(filename)); //
if (pngr.imgInfo.channels!=1 || pngr.imgInfo.bitDepth != 8 || pngr.imgInfo.indexed)
throw new RuntimeException("This method is for gray images");
for (int row = 0; row < pngr.imgInfo.rows; row++) {
ImageLineByte line = pngr.readRowByte();
byte [] buf = line.getScanlineByte();
// do what you want
}
pngr.end();
Java's ImageIO is known to be broken on images with a grayscale palette.
Java ImageIO Grayscale PNG Issue
javax.imageio.ImageIO reading incorrect RGB values on grayscale images
My batch jpg resizer works with color images, but grayscale ones become washed out
Wrong brightness converting image to grayscale in Java
Oracle: JDK-5051418 : Grayscale TYPE_CUSTOM BufferedImages are rendered lighter than TYPE_BYTE_GRAY
Oracle: JDK-6467250 : BufferedImage getRGB(x,y) problem
I'm a bit of a Java noob, and I have read some basics about sockets and I can successfully send images over socket using ImageIO, but I want to reduce the amount of data that is sent. Ultimately I want the image (screen capture) to be send as fast as possible with the smallest possible file size.
Right now, I have imageIO set up as such;
DataInputStream in=new DataInputStream(client.getInputStream());
DataOutputStream out = new DataOutputStream(client.getOutputStream());
ImageIO.write(captureImg(),"JPG",client.getOutputStream());
And the receiver:
BufferedImage img=ImageIO.read(ImageIO.createImageInputStream(server.getInputStream()));
File outputfile = new File("Screen"+(date.toString())+".jpg");
ImageIO.write(img, "jpg", outputfile);
In case you're wondering, this is my method that is used to take the image.
Rectangle screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
BufferedImage capture = new Robot().createScreenCapture(screenRect);
I have heard about Byte arrays, where you can send the bytes then draw the image at the other end. However I'm not sure if this is more efficient.
Any help would be greatly appreciated, please ask if you would like me to add any extra info or code for the byte array!
Thanks.
EDIT: Patrick:
ByteArrayOutputStream bScrn = new ByteArrayOutputStream();
ImageIO.write(captureImg(), "JPG", bScrn);
byte imgBytes[] = bScrn.toByteArray();
out.write((Integer.toString(imgBytes.length)).getBytes());
out.write(imgBytes,0,imgBytes.length);
There already has been an extensive discussion in the comments, but to summarize a few points that I find important:
You have a trade-off between several criteria:
Minimize network traffic
Minimize CPU load
Maximize image quality
You can reduce the network traffic with a high image compression. But this will increase the CPU load and might reduce the image quality.
Whether it reduces the image quality depends on the compression type: For JPG, you can make the image arbitrarily small, but the quality of the image will then be ... well, arbitrarily bad. For PNG, the image quality will stay the same (since it is a lossless compression), but the CPU load and the resulting image size may be greater.
The option of ZIPping the image data was also mentioned. It is true that ZIPping the JPG or PNG data of an image will hardly reduce the amount of data (because the data already is compressed). But compressing the raw image data can be a feasible option, as an alternative to JPG or PNG.
Which compression technique (JPG, PNG or ZIP) is appropriate also depends on the image content: JPG is more suitable for "natural" images, like photos or rendered images. These can withstand a high compression without causing artefacts. For artifical images (like line drawings), it will quickly cause undesirable artefacts, particularly at sharp edges or when the image contains texts. In contrast to that: When the image contains large areas with a single color, then a compression like PNG (or ZIP) can reduce the image size due to the "run length compression" nature of these compression methods.
I already made some experiments for such an image transfer quite a while ago, and implemented it in a way that easily allowed tweaking and tuning these parameters and switching between the different methods, and comparing the speed for different application cases. But from the tip of my head, I can not give a profound summary of the results.
BTW: Depending on what you actually want to transfer, you could consider obtaining the image data with a different technique than Robot#createScreenCapture(Rectangle). This method is well-known to be distressingly slow. For example, when you want to transfer a Swing application, you could let your application directly paint into an image. Roughly with a pattern like
BufferedImage image = new BufferedImage(w,h,type);
Graphics g = image.getGraphics();
myMainFrame.paint(g);
g.dispose();
(This is only a sketch, to show the basic idea!)
Additionally, you could consider further options for increasing the "percieved speed" of such an image transfer. For example, you could divide your image into tiles, and transfer these tiles one after another. The receiver will possibly appreciate it if the image would at least be partially visible as quickly as possible. This idea could be extended further. For example, by detecting which tiles have really changed between two frames, and only transfer these changed tiles. (This approach could be extended and implemented in a rather sophisticated way, by detecting the "minimum regions" that have to be transferred)
However, for the case that you first want to play around with the most obvious tuning parameter: Here is a method that allows writing a JPG image with a quality value between 0.0 and 1.0 into an output stream:
public static void writeJPG(
BufferedImage bufferedImage,
OutputStream outputStream,
float quality) throws IOException
{
Iterator<ImageWriter> iterator =
ImageIO.getImageWritersByFormatName("jpg");
ImageWriter imageWriter = iterator.next();
ImageWriteParam imageWriteParam = imageWriter.getDefaultWriteParam();
imageWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
imageWriteParam.setCompressionQuality(quality);
ImageOutputStream imageOutputStream =
new MemoryCacheImageOutputStream(outputStream);
imageWriter.setOutput(imageOutputStream);
IIOImage iioimage = new IIOImage(bufferedImage, null, null);
imageWriter.write(null, iioimage, imageWriteParam);
imageOutputStream.flush();
}
I have tried the below code to generate the high quality thumbnail image but got the thumb nail blurred and not that much clarity.
BufferedImage thumbImage = new BufferedImage(thumbWidth, thumbHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics2D = thumbImage.createGraphics();
graphics2D.setBackground(Color.WHITE);
graphics2D.setPaint(Color.WHITE);
graphics2D.fillRect(0, 0, thumbWidth, thumbHeight);
graphics2D.setComposite(AlphaComposite.Src);
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.drawImage(image, 0, 0, thumbWidth, thumbHeight, null);
graphics2D.dispose();
File file = new File(thumbnailFile);
if (javax.imageio.ImageIO.write(thumbImage, "JPG", file))
return file;
}
The original image is a high quality image. Why am I getting the thumbnail image distorted and low in quality?
There are two possibilities from looking at the code:
The original image is very large, and the thumbnail is very small, resulting in image quality being degraded by the simple bilinear interpolation.
The compression artifacts resulting from the JPEG encoding is degrading the quality of the thumbnail.
Using a simple bilinear interpolation can be adequate if the original image and the thumbnail doesn't differ in size by a whole lot, for example, going from a 200x200 to 100x100.
However, when it comes to resizing images that are large (such as 1600x1200) to a thumbnail size image, bilinear interpolation (and bicubic interpolation, for that matter), so alternative techniques such as multi-step resizing should be used.
The article The Perils of Image.getScaledInstance() by Chris Campbell goes into more details about how and why downscaling large images can lead to degradation of image quality.
The book Filthy Rich Clients by Chet Haase and Romain Guy also goes into some details about creating high-quality thumbnails.
I maintain a thumbnail generation library called Thumbnailator, which uses techniques such as multi-step resizing to create high-quality thumbnails with an easy-to-use API.
For example, your example code could be written using Thumbnailator like this:
Thumbnails.of(image)
.size(thumbWidth, thumbHeight)
.outputFormat("JPG")
.toFile(file);
It's also possible to specify the compression quality settings if compression artifacts are causing image quality degradation:
Thumbnails.of(image)
.size(thumbWidth, thumbHeight)
.outputFormat("JPG")
.outoutQuality(0.9)
.toFile(file);
Thumbnails4j (I'm a maintainer, but it's owned by Elastic) is a java library that can be used to create thumbnails from image files, as well as from other file types.
File input = new File("/path/to/my_file.jpeg");
Thumbnailer thumbnailer = new ImageThumbnailer("png"); // or "jpg", whichever output format you want
List<Dimensions> outputDimensions = Collections.singletonList(new Dimensions(100, 100));
BufferedImage output = thumbnailer.getThumbnails(input, outputDimensions).get(0);
One of the approaches it takes is to iteratively resize the image, instead of jumping straight to the desired size, as this helps to preserve image quality. But this library allows you to avoid having to worry about the details of the image processing algorithm.
I have a large swing component to write to TIFF. The component is too large to load the TIFF in memory, so I either need to make a big BufferedImage which is backed by a disk-based WritableRaster (as mentioned here) or use JAI.
JAI seems like the better answer, aside from the utter confusion of the project.
Given that, can someone outline steps for writing my swing component to a tiled TIFF without running out of Memory?
Image size will be maybe 10000x700
Ideally I would create some sort of disk-based image, and write parts of the component to it, each write being flushed to disk.
EDIT
I think I could do this with an ImageWriter, however I'm getting a NoSuchElementException when I call:
ImageWriter imageWriter = ImageIO.getImageWritersByFormatName("tif").next();
I have the jai_code.jar and jai_core.jar jars on my classpath, is there something else I need to do?
EDIT
I can create a very large TIFF using JAI, but JAI doesn't support TIFF compression, so the file is 92 MB.
If I install JAI-ImageIO, I can create a compressed TIFF Using an ImageWriter, but only from a Raster or BufferedImage, which I don't have enough memory for.
Is there some way to do a two-step approach, use JAI to create the large TIFF, then compress the large TIFF without loading the whole thing into memory?
I had to load and store a large tiff (59392x40192px) with JAI. My solution is: TiledImages.
I have used a TiledImage because I need tiles and subimages.
To use the TiledImage efficient you should construct it with your prefered tile size. JAI uses a TileCache so not the whole Image will be in memory, when it's not needed.
To write the TiledImage in a File use the option "writeTiled" (avoid OutOfMemory because it writes tile by tile):
public void storeImage(TiledImage img, String filepath) {
TIFFEncodeParam tep = new TIFFEncodeParam();
//important to avoid OutOfMemory
tep.setTileSize(256, 256);
tep.setWriteTiled(true);
//fast compression
tep.setCompression(TIFFEncodeParam.COMPRESSION_PACKBITS);
//write file
JAI.create("filestore", img, filepath, "TIFF", tep);
}
It works fine with images up to 690mb (compressed), for larger images i haven't tested yet.
But if you are working on WinXP 32-bit you may not able to have more as 1280m HeapSpace size, this is still a limit of Java VM.
My TiledImage is build with a IndexedColorModel from my image-source data:
//here you create a ColorModel for your Image
ColorModel cm = source.createColorModel();
//then create a compatible SampleModel, with the tilesize
SampleModel sm = cm.createCompatibleSampleModel(tileWidth,tileHeight);
TiledImage image = new TiledImage(0, 0, imageWidth, imageHeight, 0, 0, sm, cm);
I had the same situation and I used these steps:
Load as BufferedImage with JAI
Resize BufferedImage size to preferable size (600x600px) maintaining aspect-ratio using Image#getScaledInstance(int w, int h, Image.SCALE_SMOOTH)
Draw image using Graphics2d.drawImage(..) method in JComponent#paintComponent(java.awt.Graphics) method
That helped me with showing and manipulating TIFF images ~50MB (5000x5000px).
I am using a common format all over application for images as png.Any jpg image uploaded still gets saved as png using code as below.
java.awt.image.BufferedImage bufferedImage = ImageIO.read(jpgImagePAth);
if(!IsExtensionPng(jpgImagePath)){
ImageIO.write(bufferedImage, "png", new File(pptFolder, justNamePng));
}
But this preserves alpha even though it was not there in the jpg so makes a 2MB Image 7MB and 6MB to 16MB . Is there anyway to save png without maintaining the alpha ?
The reason I need to conver to PNG is that later on when I add text on image it looses the actual resolution. I already tried loseless JPEG which didnt fix it.
It's not the alpha channel that is causing the file size to grow, it's the file type. JPG uses lossy compression; PNG is lossless compression. In other words, JPG is throwing out some data to reduce the size of the file. That's why you get to choose a "quality" level when saving to JPG - that determines how much is thrown out.
How do you know you're getting the alpha channel anyway? If you still want PNG and want to be sure you're dropping the alpha channel, set the image type to BufferedImage.TYPE_RGB, e.g.
BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_RGB);
You'll have to get the graphics object associated with your new BufferedImage and copy the jpg onto it, then write it out. This question isn't quite the same as yours but has sample code you may find useful.
Paul
I don't know exactly in what situation you are. But I should keep JPEG JPEG. The only advantage of converting JPEG to PNG is wasting hdd space.