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