Question

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.

Was it helpful?

Solution

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()));
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top