5

As the title says, I'm trying to use custom quantization tables to compress an image in JPEG format. My problem is the resulting file can't be opened and the error is:

Quantization table 0x00 was not defined

This is how my code looks like:

        JPEGImageWriteParam params = new JPEGImageWriteParam(null);
        if (mQMatrix != null) {
            JPEGHuffmanTable[] huffmanDcTables = {JPEGHuffmanTable.StdDCLuminance, JPEGHuffmanTable.StdDCChrominance};
            JPEGHuffmanTable[] huffmanAcTables = {JPEGHuffmanTable.StdACLuminance, JPEGHuffmanTable.StdACChrominance};
            dumpMatrices(mQMatrix);
            params.setEncodeTables(mQMatrix, huffmanDcTables, huffmanAcTables);
        }

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        Iterator writers = ImageIO.getImageWritersByFormatName("JPEG");
        ImageWriter imageWriter = (ImageWriter) writers.next();

        ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(outputStream);
        imageWriter.setOutput(imageOutputStream);
        imageWriter.write(null, new IIOImage(mSourceImage, null, null), params);

        mCompressedImageSize = outputStream.size();

        try (FileOutputStream fileOutputStream = new FileOutputStream(mOutFileName)) {
            fileOutputStream.write(outputStream.toByteArray());

        }
        mCompressedImage = ImageIO.read(new ByteArrayInputStream(outputStream.toByteArray()));

My guess is that it has something to do with the metadata, but I had no luck finding a solution.

Thanks, R.

UPDATE: Using a hex viewer I determined that the quantization table (DQT - 0xFF, 0xDB section) isn't getting written to the output file. I'm assuming I have to force it to be written somehow.

UPDATE 2: So after actually debugging execution, what I found is that if the tables are set in the parameters object, then metadata isn't generated for neither the quantization not the Huffman tables. If the metadata is missing, then the tables aren't being written in the file. The thing is I see no way to customize the contents of the metadata.

rhobincu
  • 906
  • 1
  • 7
  • 22

1 Answers1

2

Very interesting question, and unfortunately non-trivial... Here's what I've found:

First of all, using JPEGImageWriteParam.setEncodeTables(...) won't do. From the JavaDoc:

Sets the quantization and Huffman tables to use in encoding abbreviated streams.

And further from JPEG Metadata Format Specification and Usage Notes:

This ordering implements the design intention that tables should be included in JPEGImageWriteParams only as a means of specifying tables when no other source is available, and this can occur only when writing to an abbreviated stream without tables using known non-standard tables for compression.

I.e., the param option can only be used for writing "abbreviated streams" (customs JPEGs without tables, assuming the tables will be provided when reading back). Conclusion: The only way we can specify tables to be encoded with the JPEG, is to pass it in the meta data.

From the same document mentioned above, the tables in the metadata will be ignored and replaced unless compression mode is MODE_COPY_FROM_METADATA, so we need to specify that.

See the Image Metadata DTD for documentation on the metadata structure. The important parts are the dqt and dht nodes with sub-nodes, and their "User object"s (not to be confused with normal DOM "userData"). We need to update these nodes, with the new tables we want to use.

Here's the code I came up with:

// Obtain qtables
mQMatrix = ...;

// Read source image
ImageInputStream imageInputStream = ImageIO.createImageInputStream(...);
ImageReader reader = ImageIO.getImageReaders(imageInputStream).next();
reader.setInput(imageInputStream);

mSourceImage = reader.read(0);
IIOMetadata metadata = null;

// We need the imageWriter to create the default JPEG metadata
ImageWriter imageWriter = ImageIO.getImageWritersByFormatName("JPEG").next();

if (mQMatrix != null) {
    dumpMatrices(mQMatrix);

    // Obtain default image metadata data, in native JPEG format
    metadata = imageWriter.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(m‌​SourceImage), null);
    IIOMetadataNode nativeMeta = (IIOMetadataNode) metadata.getAsTree("javax_imageio_jpeg_image_1.0");

    // Update dqt to values from mQMatrix
    NodeList dqtables = nativeMeta.getElementsByTagName("dqtable");
    for (int i = 0; i < dqtables.getLength(); i++) {
        IIOMetadataNode dqt = (IIOMetadataNode) dqtables.item(i);
        int dqtId = Integer.parseInt(dqt.getAttribute("qtableId"));
        dqt.setUserObject(mQMatrix[dqtId]);
    }

    // For some reason, we need dht explicitly defined, when using MODE_COPY_FROM_METADATA...
    NodeList dhtables = nativeMeta.getElementsByTagName("dhtable");

    // Just use defaults for dht
    JPEGHuffmanTable[] huffmanDcTables = {JPEGHuffmanTable.StdDCLuminance, JPEGHuffmanTable.StdDCChrominance};
    JPEGHuffmanTable[] huffmanAcTables = {JPEGHuffmanTable.StdACLuminance, JPEGHuffmanTable.StdACChrominance};

    // Update dht
    for (int i = 0; i < dhtables.getLength(); i++) {
        IIOMetadataNode dht = (IIOMetadataNode) dhtables.item(i);
        int dhtClass = Integer.parseInt(dht.getAttribute("class")); // 0: DC, 1: AC
        int dhtId = Integer.parseInt(dht.getAttribute("htableId"));

        dht.setUserObject(dhtClass == 0 ? huffmanDcTables[dhtId] : huffmanAcTables[dhtId]);
    }

    // Merge updated tree back (important!)
    metadata.mergeTree("javax_imageio_jpeg_image_1.0", nativeMeta);
}

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(outputStream);
imageWriter.setOutput(imageOutputStream);

// See http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#tables
JPEGImageWriteParam params = new JPEGImageWriteParam(null);
params.setCompressionMode(metadata == null ? MODE_DEFAULT : MODE_COPY_FROM_METADATA); // Unless MODE_COPY_FROM_METADATA, tables will be created!

imageWriter.write(null, new IIOImage(mSourceImage, null, metadata), params);
imageOutputStream.close();

mCompressedImageSize = outputStream.size();

try (FileOutputStream fileOutputStream = new FileOutputStream(mOutFileName)) {
    fileOutputStream.write(outputStream.toByteArray());
}

mCompressedImage = ImageIO.read(new ByteArrayInputStream(outputStream.toByteArray()));
Harald K
  • 26,314
  • 7
  • 65
  • 111
  • I though that it had something to do with the metadata but I wasn't that familiar with the JPEG format. Thanks! Rep well earned! :) – rhobincu Aug 05 '14 at 14:09
  • Personally, I think the ImageIO metadata API non-intuitive and hard to work with. But it usually gets the job done. Anyway, thanks! :-) – Harald K Aug 05 '14 at 14:21
  • 1
    So I got around to implementing this, I have a small correction to make: you can't get the native jpeg metadata from the reader if you want to open any kind of file (I'm mostly trying with raw bmp/ tiff/ png files). So I had to load the default jpeg metadata and merge the new nodes with that: "IIOMetadata metadata = imageWriter.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(mSourceImage), new JPEGImageWriteParam(null));" – rhobincu Aug 07 '14 at 13:48
  • @rhobincu I fixed the code, similar to your suggested edit (it was somehow rejected...). – Harald K Aug 09 '14 at 12:30