如何使用Java ImageIO写入包含大尺寸嵌入缩略图的JPEG?
问题
我正在使用javax.imageio.ImageIO写入JPEG文件,默认情况下如果尝试写入带缩略图的图片,缩略图会被裁剪至255x255尺寸。
能否仅使用ImageIO类嵌入300x300px的缩略图?
我自己分析的原因是ImageIO的实现采用了旧式APP0标记(使用JFIF而非更现代的EXIF),宽高必须用单字节存储,所以超过255的数值会被截断。
不过查看Sun的代码后,我认为如果能触发使用JFIFThumbJPEG实现的逻辑,应该可以嵌入更大的缩略图。默认情况下缩略图是以原始RGB字节表的形式写入,但JFIFThumbJPEG会在文件中嵌入一个独立的迷你JPEG,其宽高不受单字节限制。
以下是复现该问题的单元测试:
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; /** * 这个测试类演示ImageIO无法写入超过255x255尺寸的JPEG缩略图。 * <p> * 但从实现和规范来看,ImageIO其实是支持不同类型缩略图的。我觉得如果能找到方法 * 通过IIOMetaData触发JFIFThumbJPEG的使用,或许就能写入任意尺寸的缩略图。 * </p> */ public class TestThumbnails extends TestCase { @Test public void testImageIOThumbnailSizes() throws IOException { BufferedImage largeImage = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_RGB); // 尺寸小于255的缩略图可以正常工作: BufferedImage goodThumbnail = new BufferedImage(200, 200, BufferedImage.TYPE_INT_RGB); // 尺寸大于255的缩略图会被裁剪到255: BufferedImage badThumbnail = new BufferedImage(300, 300, BufferedImage.TYPE_INT_RGB); testThumbnail(largeImage, goodThumbnail); System.out.println("第一张缩略图测试通过"); testThumbnail(largeImage, badThumbnail); System.out.println("第二张缩略图测试通过"); } 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)); // 大于255x255的缩略图会被截断,我希望读取到的缩略图和传入的尺寸一致: assertEquals(expectedThumbnailWidth, reader.getThumbnailWidth(0, 0)); assertEquals(expectedThumbnailHeight, reader.getThumbnailHeight(0, 0)); } } }
解决方案
可以通过操作IIOMetadata触发JFIFThumbJPEG的使用,从而嵌入任意尺寸的缩略图。核心思路是将缩略图预先编码为JPEG字节数组,再通过元数据告诉ImageIO以独立JPEG的形式嵌入,而非原始RGB数据。
修改后的testThumbnail方法如下:
import javax.imageio.metadata.IIOMetadataNode; import java.util.Base64; // ... 保留原有类结构,替换testThumbnail方法 private void testThumbnail(BufferedImage largeImage, BufferedImage thumbnail) throws IOException { Iterator<ImageWriter> iter = ImageIO.getImageWritersBySuffix("jpg"); ImageWriter writer = iter.next(); // 1. 将缩略图编码为JPEG字节数组 ByteArrayOutputStream thumbOut = new ByteArrayOutputStream(); ImageIO.write(thumbnail, "jpg", thumbOut); byte[] thumbJpegData = thumbOut.toByteArray(); // 2. 获取默认元数据并修改缩略图配置 ImageWriteParam writeParam = writer.getDefaultWriteParam(); IIOMetadata metadata = writer.getDefaultImageMetadata( ImageTypeSpecifier.createFromRenderedImage(largeImage), writeParam); String formatName = metadata.getNativeMetadataFormatName(); IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(formatName); // 定位或创建JFIF和缩略图节点 IIOMetadataNode jfifNode = (IIOMetadataNode) root.getElementsByTagName("app0JFIF").item(0); IIOMetadataNode thumbNode = (IIOMetadataNode) jfifNode.getElementsByTagName("thumbnail").item(0); if (thumbNode == null) { thumbNode = new IIOMetadataNode("thumbnail"); jfifNode.appendChild(thumbNode); } // 设置缩略图类型为JPEG,传入编码后的字节数据 thumbNode.setAttribute("type", "JPEG"); thumbNode.setAttribute("data", Base64.getEncoder().encodeToString(thumbJpegData)); // 将修改后的元数据写回 metadata.setFromTree(formatName, root); // 3. 写入图片,使用修改后的元数据 byte[] jpegData; try (ByteArrayOutputStream byteOut = new ByteArrayOutputStream()) { IIOImage iioImage = new IIOImage(largeImage, null, metadata); ImageOutputStream stream = ImageIO.createImageOutputStream(byteOut); writer.setOutput(stream); writer.write(metadata, iioImage, writeParam); jpegData = byteOut.toByteArray(); } testReadingThumbnail(jpegData, thumbnail.getWidth(), thumbnail.getHeight()); }
注意事项
- 需额外导入
javax.imageio.metadata.IIOMetadataNode和java.util.Base64; - 写入时不再需要将缩略图传入
IIOImage构造函数,而是通过元数据传递JPEG格式的缩略图数据; - 这种方式利用JFIF的APP0标记嵌入独立JPEG缩略图,不受单字节尺寸限制,300x300的缩略图可正常写入和读取。
修改后运行测试类,两张缩略图的测试都会通过,300x300的缩略图不会被裁剪。
内容的提问来源于stack exchange,提问作者mickleness
相关产品推荐
相关产品推荐

