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 */
045package com.github.jaiimageio.impl.plugins.tiff;
046
047import com.github.jaiimageio.plugins.tiff.TIFFCompressor;
048
049import java.awt.Point;
050import java.awt.Transparency;
051import java.awt.color.ColorSpace;
052import java.awt.image.BufferedImage;
053import java.awt.image.ColorModel;
054import java.awt.image.ComponentColorModel;
055import java.awt.image.DataBuffer;
056import java.awt.image.DataBufferByte;
057import java.awt.image.PixelInterleavedSampleModel;
058import java.awt.image.Raster;
059import java.awt.image.SampleModel;
060import java.awt.image.WritableRaster;
061import java.io.IOException;
062import java.io.ByteArrayOutputStream;
063import java.util.ArrayList;
064import java.util.Arrays;
065import java.util.List;
066import java.util.Iterator;
067
068import javax.imageio.IIOException;
069import javax.imageio.IIOImage;
070import javax.imageio.ImageIO;
071import javax.imageio.ImageReader;
072import javax.imageio.ImageWriteParam;
073import javax.imageio.ImageWriter;
074import javax.imageio.metadata.IIOInvalidTreeException;
075import javax.imageio.metadata.IIOMetadata;
076import javax.imageio.metadata.IIOMetadataNode;
077import javax.imageio.spi.ImageWriterSpi;
078import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
079import javax.imageio.stream.ImageOutputStream;
080import javax.imageio.stream.MemoryCacheImageOutputStream;
081
082import org.w3c.dom.Node;
083
084/**
085 * Base class for all possible forms of JPEG compression in TIFF.
086 */
087public abstract class TIFFBaseJPEGCompressor extends TIFFCompressor {
088
089    private static final boolean DEBUG = false; // XXX false for release.
090
091    // Stream metadata format.
092    protected static final String STREAM_METADATA_NAME =
093        "javax_imageio_jpeg_stream_1.0";
094
095    // Image metadata format.
096    protected static final String IMAGE_METADATA_NAME =
097        "javax_imageio_jpeg_image_1.0";
098
099    // ImageWriteParam passed in.
100    private ImageWriteParam param = null;
101
102    /**
103     * ImageWriteParam for JPEG writer.
104     * May be initialized by {@link #initJPEGWriter()}.
105     */
106    protected JPEGImageWriteParam JPEGParam = null;
107
108    /**
109     * The JPEG writer.
110     * May be initialized by {@link #initJPEGWriter()}.
111     */
112    protected ImageWriter JPEGWriter = null;
113
114    /**
115     * Whether to write abbreviated JPEG streams (default == false).
116     * A subclass which sets this to <code>true</code> should also
117     * initialized {@link #JPEGStreamMetadata}.
118     */
119    protected boolean writeAbbreviatedStream = false;
120
121    /**
122     * Stream metadata equivalent to a tables-only stream such as in
123     * the <code>JPEGTables</code>. Default value is <code>null</code>.
124     * This should be set by any subclass which sets
125     * {@link writeAbbreviatedStream} to <code>true</code>.
126     */
127    protected IIOMetadata JPEGStreamMetadata = null;
128
129    // A pruned image metadata object containing only essential nodes.
130    private IIOMetadata JPEGImageMetadata = null;
131
132    // Whether the codecLib native JPEG writer is being used.
133    private boolean usingCodecLib;
134
135    // Array-based output stream.
136    private IIOByteArrayOutputStream baos;
137
138    /**
139     * Removes nonessential nodes from a JPEG native image metadata tree.
140     * All nodes derived from JPEG marker segments other than DHT, DQT,
141     * SOF, SOS segments are removed unless <code>pruneTables</code> is
142     * <code>true</code> in which case the nodes derived from the DHT and
143     * DQT marker segments are also removed.
144     *
145     * @param tree A <tt>javax_imageio_jpeg_image_1.0</tt> tree.
146     * @param pruneTables Whether to prune Huffman and quantization tables.
147     * @throws IllegalArgumentException if <code>tree</code> is
148     * <code>null</code> or is not the root of a JPEG native image
149     * metadata tree.
150     */
151    private static void pruneNodes(Node tree, boolean pruneTables) {
152        if(tree == null) {
153            throw new IllegalArgumentException("tree == null!");
154        }
155        if(!tree.getNodeName().equals(IMAGE_METADATA_NAME)) {
156            throw new IllegalArgumentException
157                ("root node name is not "+IMAGE_METADATA_NAME+"!");
158        }
159        if(DEBUG) {
160            System.out.println("pruneNodes("+tree+","+pruneTables+")");
161        }
162
163        // Create list of required nodes.
164        List wantedNodes = new ArrayList();
165        wantedNodes.addAll(Arrays.asList(new String[] {
166            "JPEGvariety", "markerSequence",
167            "sof", "componentSpec",
168            "sos", "scanComponentSpec"
169        }));
170
171        // Add Huffman and quantization table nodes if not pruning tables.
172        if(!pruneTables) {
173            wantedNodes.add("dht");
174            wantedNodes.add("dhtable");
175            wantedNodes.add("dqt");
176            wantedNodes.add("dqtable");
177        }
178
179        IIOMetadataNode iioTree = (IIOMetadataNode)tree;
180
181        List nodes = getAllNodes(iioTree, null);
182        int numNodes = nodes.size();
183
184        for(int i = 0; i < numNodes; i++) {
185            Node node = (Node)nodes.get(i);
186            if(!wantedNodes.contains(node.getNodeName())) {
187                if(DEBUG) {
188                    System.out.println("Removing "+node.getNodeName());
189                }
190                node.getParentNode().removeChild(node);
191            }
192        }
193    }
194
195    private static List getAllNodes(IIOMetadataNode root, List nodes) {
196        if(nodes == null) nodes = new ArrayList();
197
198        if(root.hasChildNodes()) {
199            Node sibling = root.getFirstChild();
200            while(sibling != null) {
201                nodes.add(sibling);
202                nodes = getAllNodes((IIOMetadataNode)sibling, nodes);
203                sibling = sibling.getNextSibling();
204            }
205        }
206
207        return nodes;
208    }
209
210    public TIFFBaseJPEGCompressor(String compressionType,
211                                  int compressionTagValue,
212                                  boolean isCompressionLossless,
213                                  ImageWriteParam param) {
214        super(compressionType, compressionTagValue, isCompressionLossless);
215
216        this.param = param;
217    }
218
219    /**
220     * A <code>ByteArrayOutputStream</code> which allows writing to an
221     * <code>ImageOutputStream</code>.
222     */
223    private static class IIOByteArrayOutputStream extends ByteArrayOutputStream {
224        IIOByteArrayOutputStream() {
225            super();
226        }
227
228        IIOByteArrayOutputStream(int size) {
229            super(size);
230        }
231
232        public synchronized void writeTo(ImageOutputStream ios)
233            throws IOException {
234            ios.write(buf, 0, count);
235        }
236    }
237
238    /**
239     * Initializes the JPEGWriter and JPEGParam instance variables.
240     * This method must be called before encode() is invoked.
241     *
242     * @param supportsStreamMetadata Whether the JPEG writer must
243     * support JPEG native stream metadata, i.e., be capable of writing
244     * abbreviated streams.
245     * @param supportsImageMetadata Whether the JPEG writer must
246     * support JPEG native image metadata.
247     */
248    protected void initJPEGWriter(boolean supportsStreamMetadata,
249                                  boolean supportsImageMetadata) {
250        // Reset the writer to null if it does not match preferences.
251        if(this.JPEGWriter != null &&
252           (supportsStreamMetadata || supportsImageMetadata)) {
253            ImageWriterSpi spi = this.JPEGWriter.getOriginatingProvider();
254            if(supportsStreamMetadata) {
255                String smName = spi.getNativeStreamMetadataFormatName();
256                if(smName == null || !smName.equals(STREAM_METADATA_NAME)) {
257                    this.JPEGWriter = null;
258                }
259            }
260            if(this.JPEGWriter != null && supportsImageMetadata) {
261                String imName = spi.getNativeImageMetadataFormatName();
262                if(imName == null || !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.getOriginatingProvider();
279                    if(supportsStreamMetadata) {
280                        String smName =
281                            spi.getNativeStreamMetadataFormatName();
282                        if(smName == null ||
283                           !smName.equals(STREAM_METADATA_NAME)) {
284                            // Try the next one.
285                            continue;
286                        }
287                    }
288                    if(supportsImageMetadata) {
289                        String imName =
290                            spi.getNativeImageMetadataFormatName();
291                        if(imName == null ||
292                           !imName.equals(IMAGE_METADATA_NAME)) {
293                            // Try the next one.
294                            continue;
295                        }
296                    }
297                }
298
299                // Set the writer.
300                this.JPEGWriter = writer;
301                break;
302            }
303
304            if(this.JPEGWriter == null) {
305                // XXX The exception thrown should really be an IIOException.
306                throw new IllegalStateException
307                    ("No appropriate JPEG writers found!");
308            }
309        }
310
311        this.usingCodecLib =
312            JPEGWriter.getClass().getName().startsWith("com.sun.media");
313        if(DEBUG) System.out.println("usingCodecLib = "+usingCodecLib);
314
315        // Initialize the ImageWriteParam.
316        if(this.JPEGParam == null) {
317            if(param != null && param instanceof JPEGImageWriteParam) {
318                JPEGParam = (JPEGImageWriteParam)param;
319            } else {
320                JPEGParam =
321                    new JPEGImageWriteParam(writer != null ?
322                                            writer.getLocale() : null);
323                if(param.getCompressionMode() ==
324                   ImageWriteParam.MODE_EXPLICIT) {
325                    JPEGParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
326                    JPEGParam.setCompressionQuality(param.getCompressionQuality());
327                }
328            }
329        }
330    }
331
332    /**
333     * Retrieves image metadata with non-core nodes removed.
334     */
335    private IIOMetadata getImageMetadata(boolean pruneTables)
336        throws IIOException {
337        if(DEBUG) {
338            System.out.println("getImageMetadata("+pruneTables+")");
339        }
340        if(JPEGImageMetadata == null &&
341           IMAGE_METADATA_NAME.equals(JPEGWriter.getOriginatingProvider().getNativeImageMetadataFormatName())) {
342            TIFFImageWriter tiffWriter = (TIFFImageWriter)this.writer;
343
344            // Get default image metadata.
345            JPEGImageMetadata =
346                JPEGWriter.getDefaultImageMetadata(tiffWriter.imageType,
347                                                   JPEGParam);
348
349            // Get the DOM tree.
350            Node tree = JPEGImageMetadata.getAsTree(IMAGE_METADATA_NAME);
351
352            // Remove unwanted marker segments.
353            try {
354                pruneNodes(tree, pruneTables);
355            } catch(IllegalArgumentException e) {
356                throw new IIOException("Error pruning unwanted nodes", e);
357            }
358
359            // Set the DOM back into the metadata.
360            try {
361                JPEGImageMetadata.setFromTree(IMAGE_METADATA_NAME, tree);
362            } catch(IIOInvalidTreeException e) {
363                // XXX This should really be a warning that image data
364                // segments will be written with tables despite the
365                // present of JPEGTables field.
366                throw new IIOException
367                    ("Cannot set pruned image metadata!", e);
368            }
369        }
370
371        return JPEGImageMetadata;
372    }
373
374    public final int encode(byte[] b, int off,
375                            int width, int height,
376                            int[] bitsPerSample,
377                            int scanlineStride) throws IOException {
378        if (this.JPEGWriter == null) {
379            throw new IIOException
380                ("JPEG writer has not been initialized!");
381        }
382        if (!((bitsPerSample.length == 3 &&
383               bitsPerSample[0] == 8 &&
384               bitsPerSample[1] == 8 &&
385               bitsPerSample[2] == 8) ||
386              (bitsPerSample.length == 1 &&
387               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,
443                                                false,
444                                                false,
445                                                Transparency.OPAQUE,
446                                                DataBuffer.TYPE_BYTE);
447
448        // Create the SampleModel.
449        SampleModel sm =
450            new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE,
451                                            width, height,
452                                            bitsPerSample.length,
453                                            scanlineStride,
454                                            offsets);
455
456        // Create the WritableRaster.
457        WritableRaster wras =
458            Raster.createWritableRaster(sm, dbb, new Point(0, 0));
459
460        // Create the BufferedImage.
461        BufferedImage bi = new BufferedImage(cm, wras, false, null);
462
463        // Get the pruned JPEG image metadata (may be null).
464        IIOMetadata imageMetadata = getImageMetadata(writeAbbreviatedStream);
465
466        // Compress the image into the output stream.
467        int compDataLength;
468        if(usingCodecLib && !writeAbbreviatedStream) {
469            // Write complete JPEG stream
470            JPEGWriter.write(null, new IIOImage(bi, null, imageMetadata),
471                             JPEGParam);
472
473            compDataLength =
474                (int)(stream.getStreamPosition() - initialStreamPosition);
475        } else {
476            if(writeAbbreviatedStream) {
477                // Write abbreviated JPEG stream
478
479                // First write the tables-only data.
480                JPEGWriter.prepareWriteSequence(JPEGStreamMetadata);
481                ios.flush();
482
483                // Rewind to the beginning of the byte array.
484                baos.reset();
485
486                // Write the abbreviated image data.
487                IIOImage image = new IIOImage(bi, null, imageMetadata);
488                JPEGWriter.writeToSequence(image, JPEGParam);
489                JPEGWriter.endWriteSequence();
490            } else {
491                // Write complete JPEG stream
492                JPEGWriter.write(null,
493                                 new IIOImage(bi, null, imageMetadata),
494                                 JPEGParam);
495            }
496
497            compDataLength = baos.size();
498            baos.writeTo(stream);
499            baos.reset();
500        }
501
502        return compDataLength;
503    }
504
505    protected void finalize() throws Throwable {
506        super.finalize();
507        if(JPEGWriter != null) {
508            JPEGWriter.dispose();
509        }
510    }
511}