/*
* Copyright (c) 2005, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.imageio.plugins.tiff;
import java.awt.Point;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.PixelInterleavedSampleModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Iterator;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import org.w3c.dom.Node;
/**
* Base class for all possible forms of JPEG compression in TIFF.
*/
public abstract class TIFFBaseJPEGCompressor extends TIFFCompressor {
// Stream metadata format.
protected static final String STREAM_METADATA_NAME =
"javax_imageio_jpeg_stream_1.0";
// Image metadata format.
protected static final String IMAGE_METADATA_NAME =
"javax_imageio_jpeg_image_1.0";
// ImageWriteParam passed in.
private ImageWriteParam param = null;
/**
* ImageWriteParam for JPEG writer.
* May be initialized by {@link #initJPEGWriter}.
*/
protected JPEGImageWriteParam JPEGParam = null;
/**
* The JPEG writer.
* May be initialized by {@link #initJPEGWriter}.
*/
protected ImageWriter JPEGWriter = null;
/**
* Whether to write abbreviated JPEG streams (default == false).
* A subclass which sets this to {@code true} should also
* initialized {@link #JPEGStreamMetadata}.
*/
protected boolean writeAbbreviatedStream = false;
/**
* Stream metadata equivalent to a tables-only stream such as in
* the {@code JPEGTables}. Default value is {@code null}.
* This should be set by any subclass which sets
* {@link #writeAbbreviatedStream} to {@code true}.
*/
protected IIOMetadata JPEGStreamMetadata = null;
// A pruned image metadata object containing only essential nodes.
private IIOMetadata JPEGImageMetadata = null;
// Array-based output stream.
private IIOByteArrayOutputStream baos;
/**
* Removes nonessential nodes from a JPEG native image metadata tree.
* All nodes derived from JPEG marker segments other than DHT, DQT,
* SOF, SOS segments are removed unless {@code pruneTables} is
* {@code true} in which case the nodes derived from the DHT and
* DQT marker segments are also removed.
*
* @param tree A javax_imageio_jpeg_image_1.0 tree.
* @param pruneTables Whether to prune Huffman and quantization tables.
* @throws NullPointerException if {@code tree} is
* {@code null}.
* @throws IllegalArgumentException if {@code tree} is not the root
* of a JPEG native image metadata tree.
*/
private static void pruneNodes(Node tree, boolean pruneTables) {
if(tree == null) {
throw new NullPointerException("tree == null!");
}
if(!tree.getNodeName().equals(IMAGE_METADATA_NAME)) {
throw new IllegalArgumentException
("root node name is not "+IMAGE_METADATA_NAME+"!");
}
// Create list of required nodes.
List wantedNodes = new ArrayList();
wantedNodes.addAll(Arrays.asList(new String[] {
"JPEGvariety", "markerSequence",
"sof", "componentSpec",
"sos", "scanComponentSpec"
}));
// Add Huffman and quantization table nodes if not pruning tables.
if(!pruneTables) {
wantedNodes.add("dht");
wantedNodes.add("dhtable");
wantedNodes.add("dqt");
wantedNodes.add("dqtable");
}
IIOMetadataNode iioTree = (IIOMetadataNode)tree;
List nodes = getAllNodes(iioTree, null);
int numNodes = nodes.size();
for(int i = 0; i < numNodes; i++) {
Node node = nodes.get(i);
if(!wantedNodes.contains(node.getNodeName())) {
node.getParentNode().removeChild(node);
}
}
}
private static List getAllNodes(IIOMetadataNode root, List nodes) {
if(nodes == null) nodes = new ArrayList();
if(root.hasChildNodes()) {
Node sibling = root.getFirstChild();
while(sibling != null) {
nodes.add(sibling);
nodes = getAllNodes((IIOMetadataNode)sibling, nodes);
sibling = sibling.getNextSibling();
}
}
return nodes;
}
public TIFFBaseJPEGCompressor(String compressionType,
int compressionTagValue,
boolean isCompressionLossless,
ImageWriteParam param) {
super(compressionType, compressionTagValue, isCompressionLossless);
this.param = param;
}
/**
* A {@code ByteArrayOutputStream} which allows writing to an
* {@code ImageOutputStream}.
*/
private static class IIOByteArrayOutputStream extends ByteArrayOutputStream {
IIOByteArrayOutputStream() {
super();
}
IIOByteArrayOutputStream(int size) {
super(size);
}
public synchronized void writeTo(ImageOutputStream ios)
throws IOException {
ios.write(buf, 0, count);
}
}
/**
* Initializes the JPEGWriter and JPEGParam instance variables.
* This method must be called before encode() is invoked.
*
* @param supportsStreamMetadata Whether the JPEG writer must
* support JPEG native stream metadata, i.e., be capable of writing
* abbreviated streams.
* @param supportsImageMetadata Whether the JPEG writer must
* support JPEG native image metadata.
*/
protected void initJPEGWriter(boolean supportsStreamMetadata,
boolean supportsImageMetadata) {
// Reset the writer to null if it does not match preferences.
if(this.JPEGWriter != null &&
(supportsStreamMetadata || supportsImageMetadata)) {
ImageWriterSpi spi = this.JPEGWriter.getOriginatingProvider();
if(supportsStreamMetadata) {
String smName = spi.getNativeStreamMetadataFormatName();
if(smName == null || !smName.equals(STREAM_METADATA_NAME)) {
this.JPEGWriter = null;
}
}
if(this.JPEGWriter != null && supportsImageMetadata) {
String imName = spi.getNativeImageMetadataFormatName();
if(imName == null || !imName.equals(IMAGE_METADATA_NAME)) {
this.JPEGWriter = null;
}
}
}
// Set the writer.
if(this.JPEGWriter == null) {
Iterator iter = ImageIO.getImageWritersByFormatName("jpeg");
while(iter.hasNext()) {
// Get a writer.
ImageWriter writer = iter.next();
// Verify its metadata support level.
if(supportsStreamMetadata || supportsImageMetadata) {
ImageWriterSpi spi = writer.getOriginatingProvider();
if(supportsStreamMetadata) {
String smName =
spi.getNativeStreamMetadataFormatName();
if(smName == null ||
!smName.equals(STREAM_METADATA_NAME)) {
// Try the next one.
continue;
}
}
if(supportsImageMetadata) {
String imName =
spi.getNativeImageMetadataFormatName();
if(imName == null ||
!imName.equals(IMAGE_METADATA_NAME)) {
// Try the next one.
continue;
}
}
}
// Set the writer.
this.JPEGWriter = writer;
break;
}
if(this.JPEGWriter == null) {
throw new NullPointerException
("No appropriate JPEG writers found!");
}
}
// Initialize the ImageWriteParam.
if(this.JPEGParam == null) {
if(param != null && param instanceof JPEGImageWriteParam) {
JPEGParam = (JPEGImageWriteParam)param;
} else {
JPEGParam =
new JPEGImageWriteParam(writer != null ?
writer.getLocale() : null);
if (param != null && param.getCompressionMode()
== ImageWriteParam.MODE_EXPLICIT) {
JPEGParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
JPEGParam.setCompressionQuality(param.getCompressionQuality());
}
}
}
}
/**
* Retrieves image metadata with non-core nodes removed.
*/
private IIOMetadata getImageMetadata(boolean pruneTables)
throws IIOException {
if(JPEGImageMetadata == null &&
IMAGE_METADATA_NAME.equals(JPEGWriter.getOriginatingProvider().getNativeImageMetadataFormatName())) {
TIFFImageWriter tiffWriter = (TIFFImageWriter)this.writer;
// Get default image metadata.
JPEGImageMetadata =
JPEGWriter.getDefaultImageMetadata(tiffWriter.getImageType(),
JPEGParam);
// Get the DOM tree.
Node tree = JPEGImageMetadata.getAsTree(IMAGE_METADATA_NAME);
// Remove unwanted marker segments.
try {
pruneNodes(tree, pruneTables);
} catch(IllegalArgumentException e) {
throw new IIOException("Error pruning unwanted nodes", e);
}
// Set the DOM back into the metadata.
try {
JPEGImageMetadata.setFromTree(IMAGE_METADATA_NAME, tree);
} catch(IIOInvalidTreeException e) {
throw new IIOException
("Cannot set pruned image metadata!", e);
}
}
return JPEGImageMetadata;
}
public final int encode(byte[] b, int off,
int width, int height,
int[] bitsPerSample,
int scanlineStride) throws IOException {
if (this.JPEGWriter == null) {
throw new IIOException("JPEG writer has not been initialized!");
}
if (!((bitsPerSample.length == 3
&& bitsPerSample[0] == 8
&& bitsPerSample[1] == 8
&& bitsPerSample[2] == 8)
|| (bitsPerSample.length == 1
&& bitsPerSample[0] == 8))) {
throw new IIOException("Can only JPEG compress 8- and 24-bit images!");
}
// Set the stream.
// The stream has to be wrapped as the Java Image I/O JPEG
// ImageWriter flushes the stream at the end of each write()
// and this causes problems for the TIFF writer.
if (baos == null) {
baos = new IIOByteArrayOutputStream();
} else {
baos.reset();
}
ImageOutputStream ios = new MemoryCacheImageOutputStream(baos);
JPEGWriter.setOutput(ios);
// Create a DataBuffer.
DataBufferByte dbb;
if (off == 0) {
dbb = new DataBufferByte(b, b.length);
} else {
//
// Workaround for bug in core Java Image I/O JPEG
// ImageWriter which cannot handle non-zero offsets.
//
int bytesPerSegment = scanlineStride * height;
byte[] btmp = new byte[bytesPerSegment];
System.arraycopy(b, off, btmp, 0, bytesPerSegment);
dbb = new DataBufferByte(btmp, bytesPerSegment);
off = 0;
}
// Set up the ColorSpace.
int[] offsets;
ColorSpace cs;
if (bitsPerSample.length == 3) {
offsets = new int[]{off, off + 1, off + 2};
cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
} else {
offsets = new int[]{off};
cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
}
// Create the ColorModel.
ColorModel cm = new ComponentColorModel(cs,
false,
false,
Transparency.OPAQUE,
DataBuffer.TYPE_BYTE);
// Create the SampleModel.
SampleModel sm
= new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE,
width, height,
bitsPerSample.length,
scanlineStride,
offsets);
// Create the WritableRaster.
WritableRaster wras
= Raster.createWritableRaster(sm, dbb, new Point(0, 0));
// Create the BufferedImage.
BufferedImage bi = new BufferedImage(cm, wras, false, null);
// Get the pruned JPEG image metadata (may be null).
IIOMetadata imageMetadata = getImageMetadata(writeAbbreviatedStream);
// Compress the image into the output stream.
int compDataLength;
if (writeAbbreviatedStream) {
// Write abbreviated JPEG stream
// First write the tables-only data.
JPEGWriter.prepareWriteSequence(JPEGStreamMetadata);
ios.flush();
// Rewind to the beginning of the byte array.
baos.reset();
// Write the abbreviated image data.
IIOImage image = new IIOImage(bi, null, imageMetadata);
JPEGWriter.writeToSequence(image, JPEGParam);
JPEGWriter.endWriteSequence();
} else {
// Write complete JPEG stream
JPEGWriter.write(null,
new IIOImage(bi, null, imageMetadata),
JPEGParam);
}
compDataLength = baos.size();
baos.writeTo(stream);
baos.reset();
return compDataLength;
}
protected void finalize() throws Throwable {
super.finalize();
if(JPEGWriter != null) {
JPEGWriter.dispose();
}
}
}