001: /*
002: * $RCSfile: TIFFBaseJPEGCompressor.java,v $
003: *
004: *
005: * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved.
006: *
007: * Redistribution and use in source and binary forms, with or without
008: * modification, are permitted provided that the following conditions
009: * are met:
010: *
011: * - Redistribution of source code must retain the above copyright
012: * notice, this list of conditions and the following disclaimer.
013: *
014: * - Redistribution in binary form must reproduce the above copyright
015: * notice, this list of conditions and the following disclaimer in
016: * the documentation and/or other materials provided with the
017: * distribution.
018: *
019: * Neither the name of Sun Microsystems, Inc. or the names of
020: * contributors may be used to endorse or promote products derived
021: * from this software without specific prior written permission.
022: *
023: * This software is provided "AS IS," without a warranty of any
024: * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND
025: * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY,
026: * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
027: * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL
028: * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF
029: * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
030: * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR
031: * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
032: * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
033: * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
034: * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
035: * POSSIBILITY OF SUCH DAMAGES.
036: *
037: * You acknowledge that this software is not designed or intended for
038: * use in the design, construction, operation or maintenance of any
039: * nuclear facility.
040: *
041: * $Revision: 1.5 $
042: * $Date: 2007/09/01 00:27:20 $
043: * $State: Exp $
044: */
045: package com.sun.media.imageioimpl.plugins.tiff;
046:
047: import com.sun.media.imageio.plugins.tiff.TIFFCompressor;
048: import java.awt.Point;
049: import java.awt.Transparency;
050: import java.awt.color.ColorSpace;
051: import java.awt.image.BufferedImage;
052: import java.awt.image.ColorModel;
053: import java.awt.image.ComponentColorModel;
054: import java.awt.image.DataBuffer;
055: import java.awt.image.DataBufferByte;
056: import java.awt.image.PixelInterleavedSampleModel;
057: import java.awt.image.Raster;
058: import java.awt.image.SampleModel;
059: import java.awt.image.WritableRaster;
060: import java.io.IOException;
061: import java.io.ByteArrayOutputStream;
062: import java.util.ArrayList;
063: import java.util.Arrays;
064: import java.util.List;
065: import java.util.Iterator;
066: import javax.imageio.IIOException;
067: import javax.imageio.IIOImage;
068: import javax.imageio.ImageIO;
069: import javax.imageio.ImageReader;
070: import javax.imageio.ImageWriteParam;
071: import javax.imageio.ImageWriter;
072: import javax.imageio.metadata.IIOInvalidTreeException;
073: import javax.imageio.metadata.IIOMetadata;
074: import javax.imageio.metadata.IIOMetadataNode;
075: import javax.imageio.spi.ImageWriterSpi;
076: import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
077: import javax.imageio.stream.ImageOutputStream;
078: import javax.imageio.stream.MemoryCacheImageOutputStream;
079: import org.w3c.dom.Node;
080:
081: /**
082: * Base class for all possible forms of JPEG compression in TIFF.
083: */
084: public abstract class TIFFBaseJPEGCompressor extends TIFFCompressor {
085:
086: private static final boolean DEBUG = false; // XXX false for release.
087:
088: // Stream metadata format.
089: protected static final String STREAM_METADATA_NAME = "javax_imageio_jpeg_stream_1.0";
090:
091: // Image metadata format.
092: protected static final String IMAGE_METADATA_NAME = "javax_imageio_jpeg_image_1.0";
093:
094: // ImageWriteParam passed in.
095: private ImageWriteParam param = null;
096:
097: /**
098: * ImageWriteParam for JPEG writer.
099: * May be initialized by {@link #initJPEGWriter()}.
100: */
101: protected JPEGImageWriteParam JPEGParam = null;
102:
103: /**
104: * The JPEG writer.
105: * May be initialized by {@link #initJPEGWriter()}.
106: */
107: protected ImageWriter JPEGWriter = null;
108:
109: /**
110: * Whether to write abbreviated JPEG streams (default == false).
111: * A subclass which sets this to <code>true</code> should also
112: * initialized {@link #JPEGStreamMetadata}.
113: */
114: protected boolean writeAbbreviatedStream = false;
115:
116: /**
117: * Stream metadata equivalent to a tables-only stream such as in
118: * the <code>JPEGTables</code>. Default value is <code>null</code>.
119: * This should be set by any subclass which sets
120: * {@link writeAbbreviatedStream} to <code>true</code>.
121: */
122: protected IIOMetadata JPEGStreamMetadata = null;
123:
124: // A pruned image metadata object containing only essential nodes.
125: private IIOMetadata JPEGImageMetadata = null;
126:
127: // Whether the codecLib native JPEG writer is being used.
128: private boolean usingCodecLib;
129:
130: // Array-based output stream.
131: private IIOByteArrayOutputStream baos;
132:
133: /**
134: * Removes nonessential nodes from a JPEG native image metadata tree.
135: * All nodes derived from JPEG marker segments other than DHT, DQT,
136: * SOF, SOS segments are removed unless <code>pruneTables</code> is
137: * <code>true</code> in which case the nodes derived from the DHT and
138: * DQT marker segments are also removed.
139: *
140: * @param tree A <tt>javax_imageio_jpeg_image_1.0</tt> tree.
141: * @param pruneTables Whether to prune Huffman and quantization tables.
142: * @throws IllegalArgumentException if <code>tree</code> is
143: * <code>null</code> or is not the root of a JPEG native image
144: * metadata tree.
145: */
146: private static void pruneNodes(Node tree, boolean pruneTables) {
147: if (tree == null) {
148: throw new IllegalArgumentException("tree == null!");
149: }
150: if (!tree.getNodeName().equals(IMAGE_METADATA_NAME)) {
151: throw new IllegalArgumentException("root node name is not "
152: + IMAGE_METADATA_NAME + "!");
153: }
154: if (DEBUG) {
155: System.out.println("pruneNodes(" + tree + "," + pruneTables
156: + ")");
157: }
158:
159: // Create list of required nodes.
160: List wantedNodes = new ArrayList();
161: wantedNodes.addAll(Arrays.asList(new String[] { "JPEGvariety",
162: "markerSequence", "sof", "componentSpec", "sos",
163: "scanComponentSpec" }));
164:
165: // Add Huffman and quantization table nodes if not pruning tables.
166: if (!pruneTables) {
167: wantedNodes.add("dht");
168: wantedNodes.add("dhtable");
169: wantedNodes.add("dqt");
170: wantedNodes.add("dqtable");
171: }
172:
173: IIOMetadataNode iioTree = (IIOMetadataNode) tree;
174:
175: List nodes = getAllNodes(iioTree, null);
176: int numNodes = nodes.size();
177:
178: for (int i = 0; i < numNodes; i++) {
179: Node node = (Node) nodes.get(i);
180: if (!wantedNodes.contains(node.getNodeName())) {
181: if (DEBUG) {
182: System.out
183: .println("Removing " + node.getNodeName());
184: }
185: node.getParentNode().removeChild(node);
186: }
187: }
188: }
189:
190: private static List getAllNodes(IIOMetadataNode root, List nodes) {
191: if (nodes == null)
192: nodes = new ArrayList();
193:
194: if (root.hasChildNodes()) {
195: Node sibling = root.getFirstChild();
196: while (sibling != null) {
197: nodes.add(sibling);
198: nodes = getAllNodes((IIOMetadataNode) sibling, nodes);
199: sibling = sibling.getNextSibling();
200: }
201: }
202:
203: return nodes;
204: }
205:
206: public TIFFBaseJPEGCompressor(String compressionType,
207: int compressionTagValue, boolean isCompressionLossless,
208: ImageWriteParam param) {
209: super (compressionType, compressionTagValue,
210: isCompressionLossless);
211:
212: this .param = param;
213: }
214:
215: /**
216: * A <code>ByteArrayOutputStream</code> which allows writing to an
217: * <code>ImageOutputStream</code>.
218: */
219: private static class IIOByteArrayOutputStream extends
220: ByteArrayOutputStream {
221: IIOByteArrayOutputStream() {
222: super ();
223: }
224:
225: IIOByteArrayOutputStream(int size) {
226: super (size);
227: }
228:
229: public synchronized void writeTo(ImageOutputStream ios)
230: throws IOException {
231: ios.write(buf, 0, count);
232: }
233: }
234:
235: /**
236: * Initializes the JPEGWriter and JPEGParam instance variables.
237: * This method must be called before encode() is invoked.
238: *
239: * @param supportsStreamMetadata Whether the JPEG writer must
240: * support JPEG native stream metadata, i.e., be capable of writing
241: * abbreviated streams.
242: * @param supportsImageMetadata Whether the JPEG writer must
243: * support JPEG native image metadata.
244: */
245: protected void initJPEGWriter(boolean supportsStreamMetadata,
246: boolean supportsImageMetadata) {
247: // Reset the writer to null if it does not match preferences.
248: if (this .JPEGWriter != null
249: && (supportsStreamMetadata || supportsImageMetadata)) {
250: ImageWriterSpi spi = this .JPEGWriter
251: .getOriginatingProvider();
252: if (supportsStreamMetadata) {
253: String smName = spi.getNativeStreamMetadataFormatName();
254: if (smName == null
255: || !smName.equals(STREAM_METADATA_NAME)) {
256: this .JPEGWriter = null;
257: }
258: }
259: if (this .JPEGWriter != null && supportsImageMetadata) {
260: String imName = spi.getNativeImageMetadataFormatName();
261: if (imName == null
262: || !imName.equals(IMAGE_METADATA_NAME)) {
263: this .JPEGWriter = null;
264: }
265: }
266: }
267:
268: // Set the writer.
269: if (this .JPEGWriter == null) {
270: Iterator iter = ImageIO.getImageWritersByFormatName("jpeg");
271:
272: while (iter.hasNext()) {
273: // Get a writer.
274: ImageWriter writer = (ImageWriter) iter.next();
275:
276: // Verify its metadata support level.
277: if (supportsStreamMetadata || supportsImageMetadata) {
278: ImageWriterSpi spi = writer
279: .getOriginatingProvider();
280: if (supportsStreamMetadata) {
281: String smName = spi
282: .getNativeStreamMetadataFormatName();
283: if (smName == null
284: || !smName.equals(STREAM_METADATA_NAME)) {
285: // Try the next one.
286: continue;
287: }
288: }
289: if (supportsImageMetadata) {
290: String imName = spi
291: .getNativeImageMetadataFormatName();
292: if (imName == null
293: || !imName.equals(IMAGE_METADATA_NAME)) {
294: // Try the next one.
295: continue;
296: }
297: }
298: }
299:
300: // Set the writer.
301: this .JPEGWriter = writer;
302: break;
303: }
304:
305: if (this .JPEGWriter == null) {
306: // XXX The exception thrown should really be an IIOException.
307: throw new IllegalStateException(
308: "No appropriate JPEG writers found!");
309: }
310: }
311:
312: this .usingCodecLib = JPEGWriter.getClass().getName()
313: .startsWith("com.sun.media");
314: if (DEBUG)
315: System.out.println("usingCodecLib = " + usingCodecLib);
316:
317: // Initialize the ImageWriteParam.
318: if (this .JPEGParam == null) {
319: if (param != null && param instanceof JPEGImageWriteParam) {
320: JPEGParam = (JPEGImageWriteParam) param;
321: } else {
322: JPEGParam = new JPEGImageWriteParam(
323: writer != null ? writer.getLocale() : null);
324: if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) {
325: JPEGParam
326: .setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
327: JPEGParam.setCompressionQuality(param
328: .getCompressionQuality());
329: }
330: }
331: }
332: }
333:
334: /**
335: * Retrieves image metadata with non-core nodes removed.
336: */
337: private IIOMetadata getImageMetadata(boolean pruneTables)
338: throws IIOException {
339: if (DEBUG) {
340: System.out.println("getImageMetadata(" + pruneTables + ")");
341: }
342: if (JPEGImageMetadata == null
343: && IMAGE_METADATA_NAME.equals(JPEGWriter
344: .getOriginatingProvider()
345: .getNativeImageMetadataFormatName())) {
346: TIFFImageWriter tiffWriter = (TIFFImageWriter) this .writer;
347:
348: // Get default image metadata.
349: JPEGImageMetadata = JPEGWriter.getDefaultImageMetadata(
350: tiffWriter.imageType, JPEGParam);
351:
352: // Get the DOM tree.
353: Node tree = JPEGImageMetadata
354: .getAsTree(IMAGE_METADATA_NAME);
355:
356: // Remove unwanted marker segments.
357: try {
358: pruneNodes(tree, pruneTables);
359: } catch (IllegalArgumentException e) {
360: throw new IIOException("Error pruning unwanted nodes",
361: e);
362: }
363:
364: // Set the DOM back into the metadata.
365: try {
366: JPEGImageMetadata
367: .setFromTree(IMAGE_METADATA_NAME, tree);
368: } catch (IIOInvalidTreeException e) {
369: // XXX This should really be a warning that image data
370: // segments will be written with tables despite the
371: // present of JPEGTables field.
372: throw new IIOException(
373: "Cannot set pruned image metadata!", e);
374: }
375: }
376:
377: return JPEGImageMetadata;
378: }
379:
380: public final int encode(byte[] b, int off, int width, int height,
381: int[] bitsPerSample, int scanlineStride) throws IOException {
382: if (this .JPEGWriter == null) {
383: throw new IIOException(
384: "JPEG writer has not been initialized!");
385: }
386: if (!((bitsPerSample.length == 3 && bitsPerSample[0] == 8
387: && bitsPerSample[1] == 8 && bitsPerSample[2] == 8) || (bitsPerSample.length == 1 && bitsPerSample[0] == 8))) {
388: throw new IIOException(
389: "Can only JPEG compress 8- and 24-bit images!");
390: }
391:
392: // Set the stream.
393: ImageOutputStream ios;
394: long initialStreamPosition; // usingCodecLib && !writeAbbreviatedStream
395: if (usingCodecLib && !writeAbbreviatedStream) {
396: ios = stream;
397: initialStreamPosition = stream.getStreamPosition();
398: } else {
399: // If not using codecLib then the stream has to be wrapped as
400: // 1) the core Java Image I/O JPEG ImageWriter flushes the
401: // stream at the end of each write() and this causes problems
402: // for the TIFF writer, or 2) the codecLib JPEG ImageWriter
403: // is using a stream on the native side which cannot be reset.
404: if (baos == null) {
405: baos = new IIOByteArrayOutputStream();
406: } else {
407: baos.reset();
408: }
409: ios = new MemoryCacheImageOutputStream(baos);
410: initialStreamPosition = 0L;
411: }
412: JPEGWriter.setOutput(ios);
413:
414: // Create a DataBuffer.
415: DataBufferByte dbb;
416: if (off == 0 || usingCodecLib) {
417: dbb = new DataBufferByte(b, b.length);
418: } else {
419: //
420: // Workaround for bug in core Java Image I/O JPEG
421: // ImageWriter which cannot handle non-zero offsets.
422: //
423: int bytesPerSegment = scanlineStride * height;
424: byte[] btmp = new byte[bytesPerSegment];
425: System.arraycopy(b, off, btmp, 0, bytesPerSegment);
426: dbb = new DataBufferByte(btmp, bytesPerSegment);
427: off = 0;
428: }
429:
430: // Set up the ColorSpace.
431: int[] offsets;
432: ColorSpace cs;
433: if (bitsPerSample.length == 3) {
434: offsets = new int[] { off, off + 1, off + 2 };
435: cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
436: } else {
437: offsets = new int[] { off };
438: cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
439: }
440:
441: // Create the ColorModel.
442: ColorModel cm = new ComponentColorModel(cs, false, false,
443: Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
444:
445: // Create the SampleModel.
446: SampleModel sm = new PixelInterleavedSampleModel(
447: DataBuffer.TYPE_BYTE, width, height,
448: bitsPerSample.length, scanlineStride, offsets);
449:
450: // Create the WritableRaster.
451: WritableRaster wras = Raster.createWritableRaster(sm, dbb,
452: new Point(0, 0));
453:
454: // Create the BufferedImage.
455: BufferedImage bi = new BufferedImage(cm, wras, false, null);
456:
457: // Get the pruned JPEG image metadata (may be null).
458: IIOMetadata imageMetadata = getImageMetadata(writeAbbreviatedStream);
459:
460: // Compress the image into the output stream.
461: int compDataLength;
462: if (usingCodecLib && !writeAbbreviatedStream) {
463: // Write complete JPEG stream
464: JPEGWriter.write(null,
465: new IIOImage(bi, null, imageMetadata), JPEGParam);
466:
467: compDataLength = (int) (stream.getStreamPosition() - initialStreamPosition);
468: } else {
469: if (writeAbbreviatedStream) {
470: // Write abbreviated JPEG stream
471:
472: // First write the tables-only data.
473: JPEGWriter.prepareWriteSequence(JPEGStreamMetadata);
474: ios.flush();
475:
476: // Rewind to the beginning of the byte array.
477: baos.reset();
478:
479: // Write the abbreviated image data.
480: IIOImage image = new IIOImage(bi, null, imageMetadata);
481: JPEGWriter.writeToSequence(image, JPEGParam);
482: JPEGWriter.endWriteSequence();
483: } else {
484: // Write complete JPEG stream
485: JPEGWriter.write(null, new IIOImage(bi, null,
486: imageMetadata), JPEGParam);
487: }
488:
489: compDataLength = baos.size();
490: baos.writeTo(stream);
491: baos.reset();
492: }
493:
494: return compDataLength;
495: }
496:
497: protected void finalize() throws Throwable {
498: super.finalize();
499: if (JPEGWriter != null) {
500: JPEGWriter.dispose();
501: }
502: }
503: }
|