如何使用Java的ImageIO编写嵌入大缩略图的JPEG?

问题描述 投票:0回答:1

我正在使用

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));
        }
    }
}
java jpeg javax.imageio
1个回答
0
投票

我想通了。下面的

writeJPEG
方法能够写入大于255x255的JPEG缩略图。

限制包括:

  1. JPEG 压缩停留在 75%。 (截至撰写本文时,我看不到修改它的方法。)
  2. 缩略图作为其自己的内部 JPEG 嵌入。并且 JPEG 必须低于 ~65KB,否则 JPEGImageWriter 将失败并出现异常。在此测试用例中,这意味着我可以制作最大 1500x1500 像素的缩略图,但最大尺寸将根据图像的压缩方式而变化。不过,我绝对应该能够写出 300x300 或 400x400 的缩略图。
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)

© www.soinside.com 2019 - 2024. All rights reserved.