我正在使用
javax.imageio.ImageIO
来写入 JPEG。默认情况下:如果我尝试编写带有缩略图的图像,该缩略图将被剪切为 255x255。
是否可以仅使用 ImageIO 类来嵌入 300x300px 的缩略图?
我(想我)明白这是因为 ImageIO 的实现使用了老式的 APP0 标记(使用 JFIF,而不是更现代的 EXIF),并且宽度和高度必须以字节表示。
但是: 我正在查看 Sun 的代码,我认为(?)如果我可以触发使用
JFIFThumbJPEG
实现的代码,应该可以嵌入更大的缩略图。默认缩略图被写入 RGB 字节的原始表,但 JFIFThumbJPEG
将(我认为)在文件中嵌入一个单独的迷你 JPEG。其宽度和高度应该不限于 1 个字节。
这是一个演示失败的单元测试:
import junit.framework.TestCase;
import org.junit.Test;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
/**
* This demonstrates that ImageIO fails to write a JPEG thumbnail that is larger than 255x255.
* <p>
* However, when I did through the implementation/specification, it looks like ImageIO *is*
* capable of supporting different types of thumbnails. I think if I can figure out
* how to write IIOMetaData to use a JFIFThumbJPEG then I may (?) be able to write thumbnails
* of arbitrary size.
* </p>
*/
public class TestThumbnails extends TestCase {
@Test
public void testImageIOThumbnailSizes() throws IOException {
BufferedImage largeImage = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_RGB);
// a value smaller than 255 will work:
BufferedImage goodThumbnail = new BufferedImage(200, 200, BufferedImage.TYPE_INT_RGB);
// a value larger than 255 will be cropped to 255
BufferedImage badThumbnail = new BufferedImage(300, 300, BufferedImage.TYPE_INT_RGB);
testThumbnail(largeImage, goodThumbnail);
System.out.println("first thumbnail passed");
testThumbnail(largeImage, badThumbnail);
System.out.println("second thumbnail passed");
}
private void testThumbnail(BufferedImage largeImage, BufferedImage thumbnail) throws IOException {
Iterator<ImageWriter> iter = ImageIO.getImageWritersBySuffix("jpg");
ImageWriter w = iter.next();
byte[] jpegData;
try (ByteArrayOutputStream byteOut = new ByteArrayOutputStream()) {
IIOImage iioImage = new IIOImage(largeImage, Arrays.asList(thumbnail), null);
ImageOutputStream stream = ImageIO.createImageOutputStream(byteOut);
w.setOutput(stream);
w.write(iioImage);
jpegData = byteOut.toByteArray();
}
testReadingThumbnail(jpegData, thumbnail.getWidth(), thumbnail.getHeight());
}
private void testReadingThumbnail(byte[] jpegData, int expectedThumbnailWidth, int expectedThumbnailHeight) throws IOException {
Iterator<ImageReader> iter = ImageIO.getImageReadersBySuffix("jpg");
ImageReader reader = iter.next();
try (ByteArrayInputStream byteIn = new ByteArrayInputStream(jpegData)) {
reader.setInput(ImageIO.createImageInputStream(byteIn));
// Thumbnails larger than 255x255 are trimmed. I want to read back
// a thumbnail that uses the dimensions I passed in:
assertEquals(expectedThumbnailWidth, reader.getThumbnailWidth(0, 0));
assertEquals(expectedThumbnailHeight, reader.getThumbnailHeight(0, 0));
}
}
}
我想通了。下面的
writeJPEG
方法能够写入大于255x255的JPEG缩略图。
限制包括:
import junit.framework.TestCase;
import org.junit.Test;
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Iterator;
public class TestThumbnails extends TestCase {
/**
* Write an image to an output stream.
* <p>
* Normally ImageIO writes JPEG thumbnails using a JFIF APP0 marker segment, which guarantees
* a lossless thumbnail but it constrains the thumbnail to 255x255. This method will switch to
* a JFIF *extension* APP0 marker segmen for larger thumbnails. This lets us embed a thumbnail
* of arbitrary size as another JPEG. This offers better compression, but it uses the default
* JPEG compression level (see com.sun.imageio.plugins.jpeg.JPEG.DEFAULT_QUALITY).
* </p>
*
* @param out the stream to write the JPEG image to.
* @param bufferedImage the large image to write.
* @param thumbnail the thumbnail to encode with the image.
* @param jpegQuality the JPEG quality from 0-1. Where 1 is lossless.
*/
public static void writeJPEG(OutputStream out, BufferedImage bufferedImage, BufferedImage thumbnail, float jpegQuality) throws IOException {
Iterator<ImageWriter> iter = ImageIO.getImageWritersBySuffix("jpg");
ImageWriter jpegWriter = iter.next();
IIOImage iioImage = new IIOImage(bufferedImage, Arrays.asList(thumbnail), null);
ImageWriteParam param = jpegWriter.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(jpegQuality);
if (thumbnail.getWidth() > 255 || thumbnail.getHeight() > 255) {
// this complex / cryptic code lets us activate the JFIFthumbJPEG logic that lets us
// embed larger thumbnails:
ImageTypeSpecifier imageType = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB);
IIOMetadata metadata = jpegWriter.getDefaultImageMetadata(imageType, param);
// String formatName = com.sun.imageio.plugins.jpeg.JPEG.nativeStreamMetadataFormatName
String formatName = "javax_imageio_jpeg_image_1.0";
IIOMetadataNode rootNode = new IIOMetadataNode(formatName);
IIOMetadataNode JPEGvariety = new IIOMetadataNode();
IIOMetadataNode markerSequenceNode = new IIOMetadataNode();
rootNode.appendChild(JPEGvariety);
rootNode.appendChild(markerSequenceNode);
IIOMetadataNode jfifMarkerSegment = new IIOMetadataNode();
JPEGvariety.appendChild(jfifMarkerSegment);
IIOMetadataNode jfxx = new IIOMetadataNode("JFXX");
jfifMarkerSegment.appendChild(jfxx);
IIOMetadataNode app0JFXX = new IIOMetadataNode();
// see https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format#JFIF_extension_APP0_marker_segment
app0JFXX.setAttribute("extensionCode", "16");
jfxx.appendChild(app0JFXX);
IIOMetadataNode JFIFthumbJPEG = new IIOMetadataNode("JFIFthumbJPEG");
app0JFXX.appendChild(JFIFthumbJPEG);
metadata.setFromTree(formatName, rootNode);
iioImage.setMetadata(metadata);
}
ImageOutputStream stream = ImageIO.createImageOutputStream(out);
jpegWriter.setOutput(stream);
jpegWriter.write(iioImage);
}
@Test
public void testImageIOThumbnailSizes() throws IOException {
BufferedImage largeImage = createImage(1000);
// a value smaller than 255 works without any special intervention:
BufferedImage simpleThumbnail = createImage(200);
// the smaller thumbnail should be encoded losslessly, so we can use an RGB delta of 0
testThumbnail(largeImage, simpleThumbnail, 0);
System.out.println("PASSED: thumbnail size " + simpleThumbnail.getWidth() + "x" + simpleThumbnail.getHeight());
for (int size = 300; size <= 1500; size += 100) {
BufferedImage biggerThumbnail = createImage(size);
// the larger thumbnail will have compression artifacts, so allow some RGB delta
testThumbnail(largeImage, biggerThumbnail, 10);
System.out.println("PASSED: thumbnail size " + biggerThumbnail.getWidth() + "x" + biggerThumbnail.getHeight());
}
// at size = 1600 we get a IllegalThumbException because the encoded thumbnail exceeds ~65536 . That limit
// is based on the allocation of bytes, so it's possible to predict exactly what *dimension* will trigger
// that condition. (Since the byte size of a JPEG will vary depending on how well the image data compresses.)
}
/**
* This creates a HSB gradient image. This will be easy to scan later in
* {@link #assertSimilar(BufferedImage, BufferedImage, int)}, and because it's a gradient it
* should compress well without clunky compression artifacts.
*/
private BufferedImage createImage(int size) {
BufferedImage bi = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
for (int y = 0; y < bi.getHeight(); y++) {
for (int x = 0; x < bi.getWidth(); x++) {
int rgb = Color.HSBtoRGB( (float)x / ((float) size), 1, (float)y / ((float) size) );
bi.setRGB(x, y, rgb);
}
}
return bi;
}
private void testThumbnail(BufferedImage largeImage, BufferedImage thumbnail, int allowedRGBDelta) throws IOException {
byte[] jpegData;
try (ByteArrayOutputStream byteOut = new ByteArrayOutputStream()) {
writeJPEG(byteOut, largeImage, thumbnail, .8f);
jpegData = byteOut.toByteArray();
}
testReadingThumbnail(jpegData, thumbnail, allowedRGBDelta);
}
/**
* This reads back a JPEG and asserts that the embedded thumbnail matches what we expect.
*/
private void testReadingThumbnail(byte[] jpegData, BufferedImage expectedThumbnail, int allowedRGBDelta) throws IOException {
Iterator<ImageReader> iter = ImageIO.getImageReadersBySuffix("jpg");
ImageReader reader = iter.next();
try (ByteArrayInputStream byteIn = new ByteArrayInputStream(jpegData)) {
reader.setInput(ImageIO.createImageInputStream(byteIn));
assertEquals(expectedThumbnail.getWidth(), reader.getThumbnailWidth(0, 0));
assertEquals(expectedThumbnail.getHeight(), reader.getThumbnailHeight(0, 0));
BufferedImage actualThumbnail = reader.readThumbnail(0, 0);
assertSimilar(expectedThumbnail, actualThumbnail, allowedRGBDelta);
}
}
/**
* Make sure two images are nearly the same. This examines the RGB of each pixel and
* asserts that each channel of both images is at least `allowedRGBDelta` units similar.
*/
private void assertSimilar(BufferedImage expectedImg, BufferedImage actualImg, int allowedRGBDelta) {
assertEquals(expectedImg.getWidth(), actualImg.getWidth());
assertEquals(expectedImg.getHeight(), actualImg.getHeight());
for (int y = 0; y <expectedImg.getHeight(); y++) {
for (int x = 0; x < expectedImg.getWidth(); x++) {
int argb1 = expectedImg.getRGB(x,y);
int argb2 = actualImg.getRGB(x,y);
int r1 = (argb1 >> 16) & 0xff;
int g1 = (argb1 >> 8) & 0xff;
int b1 = (argb1 >> 0) & 0xff;
int r2 = (argb2 >> 16) & 0xff;
int g2 = (argb2 >> 8) & 0xff;
int b2 = (argb2 >> 0) & 0xff;
assertTrue("(" + x + "," + y+") r1 = " + r1 + ", r2 = " + r2, Math.abs(r1 - r2) <= allowedRGBDelta);
assertTrue("(" + x + "," + y+") g1 = " + g1 + ", g2 = " + g2, Math.abs(g1 - g2) <= allowedRGBDelta);
assertTrue("(" + x + "," + y+") b1 = " + b1 + ", b2 = " + b2, Math.abs(b1 - b2) <= allowedRGBDelta);
}
}
}
}
作为参考,正在激活的非公开 Sun 代码位于
com.sun.imageio.plugins.jpeg.JFIFMarkerSegment
中。搜索此短语以了解更多信息:JFIFThumbJPEG(BufferedImage thumb)