1 /*
   2  * Copyright (c) 2005, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package com.sun.imageio.plugins.tiff;
  26 
  27 import java.awt.Point;
  28 import java.awt.Transparency;
  29 import java.awt.color.ColorSpace;
  30 import java.awt.image.BufferedImage;
  31 import java.awt.image.ColorModel;
  32 import java.awt.image.ComponentColorModel;
  33 import java.awt.image.DataBuffer;
  34 import java.awt.image.DataBufferByte;
  35 import java.awt.image.PixelInterleavedSampleModel;
  36 import java.awt.image.Raster;
  37 import java.awt.image.SampleModel;
  38 import java.awt.image.WritableRaster;
  39 import java.io.IOException;
  40 import java.io.ByteArrayOutputStream;
  41 import java.util.ArrayList;
  42 import java.util.Arrays;
  43 import java.util.List;
  44 import java.util.Iterator;
  45 import javax.imageio.IIOException;
  46 import javax.imageio.IIOImage;
  47 import javax.imageio.ImageIO;
  48 import javax.imageio.ImageWriteParam;
  49 import javax.imageio.ImageWriter;
  50 import javax.imageio.metadata.IIOInvalidTreeException;
  51 import javax.imageio.metadata.IIOMetadata;
  52 import javax.imageio.metadata.IIOMetadataNode;
  53 import javax.imageio.spi.ImageWriterSpi;
  54 import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
  55 import javax.imageio.stream.ImageOutputStream;
  56 import javax.imageio.stream.MemoryCacheImageOutputStream;
  57 import org.w3c.dom.Node;
  58 
  59 /**
  60  * Base class for all possible forms of JPEG compression in TIFF.
  61  */
  62 public abstract class TIFFBaseJPEGCompressor extends TIFFCompressor {
  63 
  64     // Stream metadata format.
  65     protected static final String STREAM_METADATA_NAME =
  66         "javax_imageio_jpeg_stream_1.0";
  67 
  68     // Image metadata format.
  69     protected static final String IMAGE_METADATA_NAME =
  70         "javax_imageio_jpeg_image_1.0";
  71 
  72     // ImageWriteParam passed in.
  73     private ImageWriteParam param = null;
  74 
  75     /**
  76      * ImageWriteParam for JPEG writer.
  77      * May be initialized by {@link #initJPEGWriter}.
  78      */
  79     protected JPEGImageWriteParam JPEGParam = null;
  80 
  81     /**
  82      * The JPEG writer.
  83      * May be initialized by {@link #initJPEGWriter}.
  84      */
  85     protected ImageWriter JPEGWriter = null;
  86 
  87     /**
  88      * Whether to write abbreviated JPEG streams (default == false).
  89      * A subclass which sets this to {@code true} should also
  90      * initialized {@link #JPEGStreamMetadata}.
  91      */
  92     protected boolean writeAbbreviatedStream = false;
  93 
  94     /**
  95      * Stream metadata equivalent to a tables-only stream such as in
  96      * the {@code JPEGTables}. Default value is {@code null}.
  97      * This should be set by any subclass which sets
  98      * {@link #writeAbbreviatedStream} to {@code true}.
  99      */
 100     protected IIOMetadata JPEGStreamMetadata = null;
 101 
 102     // A pruned image metadata object containing only essential nodes.
 103     private IIOMetadata JPEGImageMetadata = null;
 104 
 105     // Array-based output stream.
 106     private IIOByteArrayOutputStream baos;
 107 
 108     /**
 109      * Removes nonessential nodes from a JPEG native image metadata tree.
 110      * All nodes derived from JPEG marker segments other than DHT, DQT,
 111      * SOF, SOS segments are removed unless {@code pruneTables} is
 112      * {@code true} in which case the nodes derived from the DHT and
 113      * DQT marker segments are also removed.
 114      *
 115      * @param tree A <tt>javax_imageio_jpeg_image_1.0</tt> tree.
 116      * @param pruneTables Whether to prune Huffman and quantization tables.
 117      * @throws NullPointerException if {@code tree} is
 118      * {@code null}.
 119      * @throws IllegalArgumentException if {@code tree} is not the root
 120      * of a JPEG native image metadata tree.
 121      */
 122     private static void pruneNodes(Node tree, boolean pruneTables) {
 123         if(tree == null) {
 124             throw new NullPointerException("tree == null!");
 125         }
 126         if(!tree.getNodeName().equals(IMAGE_METADATA_NAME)) {
 127             throw new IllegalArgumentException
 128                 ("root node name is not "+IMAGE_METADATA_NAME+"!");
 129         }
 130 
 131         // Create list of required nodes.
 132         List<String> wantedNodes = new ArrayList<String>();
 133         wantedNodes.addAll(Arrays.asList(new String[] {
 134             "JPEGvariety", "markerSequence",
 135             "sof", "componentSpec",
 136             "sos", "scanComponentSpec"
 137         }));
 138 
 139         // Add Huffman and quantization table nodes if not pruning tables.
 140         if(!pruneTables) {
 141             wantedNodes.add("dht");
 142             wantedNodes.add("dhtable");
 143             wantedNodes.add("dqt");
 144             wantedNodes.add("dqtable");
 145         }
 146 
 147         IIOMetadataNode iioTree = (IIOMetadataNode)tree;
 148 
 149         List<Node> nodes = getAllNodes(iioTree, null);
 150         int numNodes = nodes.size();
 151 
 152         for(int i = 0; i < numNodes; i++) {
 153             Node node = nodes.get(i);
 154             if(!wantedNodes.contains(node.getNodeName())) {
 155                 node.getParentNode().removeChild(node);
 156             }
 157         }
 158     }
 159 
 160     private static List<Node> getAllNodes(IIOMetadataNode root, List<Node> nodes) {
 161         if(nodes == null) nodes = new ArrayList<Node>();
 162 
 163         if(root.hasChildNodes()) {
 164             Node sibling = root.getFirstChild();
 165             while(sibling != null) {
 166                 nodes.add(sibling);
 167                 nodes = getAllNodes((IIOMetadataNode)sibling, nodes);
 168                 sibling = sibling.getNextSibling();
 169             }
 170         }
 171 
 172         return nodes;
 173     }
 174 
 175     public TIFFBaseJPEGCompressor(String compressionType,
 176                                   int compressionTagValue,
 177                                   boolean isCompressionLossless,
 178                                   ImageWriteParam param) {
 179         super(compressionType, compressionTagValue, isCompressionLossless);
 180 
 181         this.param = param;
 182     }
 183 
 184     /**
 185      * A {@code ByteArrayOutputStream} which allows writing to an
 186      * {@code ImageOutputStream}.
 187      */
 188     private static class IIOByteArrayOutputStream extends ByteArrayOutputStream {
 189         IIOByteArrayOutputStream() {
 190             super();
 191         }
 192 
 193         IIOByteArrayOutputStream(int size) {
 194             super(size);
 195         }
 196 
 197         public synchronized void writeTo(ImageOutputStream ios)
 198             throws IOException {
 199             ios.write(buf, 0, count);
 200         }
 201     }
 202 
 203     /**
 204      * Initializes the JPEGWriter and JPEGParam instance variables.
 205      * This method must be called before encode() is invoked.
 206      *
 207      * @param supportsStreamMetadata Whether the JPEG writer must
 208      * support JPEG native stream metadata, i.e., be capable of writing
 209      * abbreviated streams.
 210      * @param supportsImageMetadata Whether the JPEG writer must
 211      * support JPEG native image metadata.
 212      */
 213     protected void initJPEGWriter(boolean supportsStreamMetadata,
 214                                   boolean supportsImageMetadata) {
 215         // Reset the writer to null if it does not match preferences.
 216         if(this.JPEGWriter != null &&
 217            (supportsStreamMetadata || supportsImageMetadata)) {
 218             ImageWriterSpi spi = this.JPEGWriter.getOriginatingProvider();
 219             if(supportsStreamMetadata) {
 220                 String smName = spi.getNativeStreamMetadataFormatName();
 221                 if(smName == null || !smName.equals(STREAM_METADATA_NAME)) {
 222                     this.JPEGWriter = null;
 223                 }
 224             }
 225             if(this.JPEGWriter != null && supportsImageMetadata) {
 226                 String imName = spi.getNativeImageMetadataFormatName();
 227                 if(imName == null || !imName.equals(IMAGE_METADATA_NAME)) {
 228                     this.JPEGWriter = null;
 229                 }
 230             }
 231         }
 232 
 233         // Set the writer.
 234         if(this.JPEGWriter == null) {
 235             Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName("jpeg");
 236 
 237             while(iter.hasNext()) {
 238                 // Get a writer.
 239                 ImageWriter writer = iter.next();
 240 
 241                 // Verify its metadata support level.
 242                 if(supportsStreamMetadata || supportsImageMetadata) {
 243                     ImageWriterSpi spi = writer.getOriginatingProvider();
 244                     if(supportsStreamMetadata) {
 245                         String smName =
 246                             spi.getNativeStreamMetadataFormatName();
 247                         if(smName == null ||
 248                            !smName.equals(STREAM_METADATA_NAME)) {
 249                             // Try the next one.
 250                             continue;
 251                         }
 252                     }
 253                     if(supportsImageMetadata) {
 254                         String imName =
 255                             spi.getNativeImageMetadataFormatName();
 256                         if(imName == null ||
 257                            !imName.equals(IMAGE_METADATA_NAME)) {
 258                             // Try the next one.
 259                             continue;
 260                         }
 261                     }
 262                 }
 263 
 264                 // Set the writer.
 265                 this.JPEGWriter = writer;
 266                 break;
 267             }
 268 
 269             if(this.JPEGWriter == null) {
 270                 throw new NullPointerException
 271                     ("No appropriate JPEG writers found!");
 272             }
 273         }
 274 
 275         // Initialize the ImageWriteParam.
 276         if(this.JPEGParam == null) {
 277             if(param != null && param instanceof JPEGImageWriteParam) {
 278                 JPEGParam = (JPEGImageWriteParam)param;
 279             } else {
 280                 JPEGParam =
 281                     new JPEGImageWriteParam(writer != null ?
 282                                             writer.getLocale() : null);
 283                 if (param != null && param.getCompressionMode()
 284                     == ImageWriteParam.MODE_EXPLICIT) {
 285                     JPEGParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
 286                     JPEGParam.setCompressionQuality(param.getCompressionQuality());
 287                 }
 288             }
 289         }
 290     }
 291 
 292     /**
 293      * Retrieves image metadata with non-core nodes removed.
 294      */
 295     private IIOMetadata getImageMetadata(boolean pruneTables)
 296         throws IIOException {
 297         if(JPEGImageMetadata == null &&
 298            IMAGE_METADATA_NAME.equals(JPEGWriter.getOriginatingProvider().getNativeImageMetadataFormatName())) {
 299             TIFFImageWriter tiffWriter = (TIFFImageWriter)this.writer;
 300 
 301             // Get default image metadata.
 302             JPEGImageMetadata =
 303                 JPEGWriter.getDefaultImageMetadata(tiffWriter.getImageType(),
 304                                                    JPEGParam);
 305 
 306             // Get the DOM tree.
 307             Node tree = JPEGImageMetadata.getAsTree(IMAGE_METADATA_NAME);
 308 
 309             // Remove unwanted marker segments.
 310             try {
 311                 pruneNodes(tree, pruneTables);
 312             } catch(IllegalArgumentException e) {
 313                 throw new IIOException("Error pruning unwanted nodes", e);
 314             }
 315 
 316             // Set the DOM back into the metadata.
 317             try {
 318                 JPEGImageMetadata.setFromTree(IMAGE_METADATA_NAME, tree);
 319             } catch(IIOInvalidTreeException e) {
 320                 throw new IIOException
 321                     ("Cannot set pruned image metadata!", e);
 322             }
 323         }
 324 
 325         return JPEGImageMetadata;
 326     }
 327 
 328     public final int encode(byte[] b, int off,
 329             int width, int height,
 330             int[] bitsPerSample,
 331             int scanlineStride) throws IOException {
 332         if (this.JPEGWriter == null) {
 333             throw new IIOException("JPEG writer has not been initialized!");
 334         }
 335         if (!((bitsPerSample.length == 3
 336                 && bitsPerSample[0] == 8
 337                 && bitsPerSample[1] == 8
 338                 && bitsPerSample[2] == 8)
 339                 || (bitsPerSample.length == 1
 340                 && bitsPerSample[0] == 8))) {
 341             throw new IIOException("Can only JPEG compress 8- and 24-bit images!");
 342         }
 343 
 344         // Set the stream.
 345         // The stream has to be wrapped as the Java Image I/O JPEG
 346         // ImageWriter flushes the stream at the end of each write()
 347         // and this causes problems for the TIFF writer.
 348         if (baos == null) {
 349             baos = new IIOByteArrayOutputStream();
 350         } else {
 351             baos.reset();
 352         }
 353         ImageOutputStream ios = new MemoryCacheImageOutputStream(baos);
 354         JPEGWriter.setOutput(ios);
 355 
 356         // Create a DataBuffer.
 357         DataBufferByte dbb;
 358         if (off == 0) {
 359             dbb = new DataBufferByte(b, b.length);
 360         } else {
 361             //
 362             // Workaround for bug in core Java Image I/O JPEG
 363             // ImageWriter which cannot handle non-zero offsets.
 364             //
 365             int bytesPerSegment = scanlineStride * height;
 366             byte[] btmp = new byte[bytesPerSegment];
 367             System.arraycopy(b, off, btmp, 0, bytesPerSegment);
 368             dbb = new DataBufferByte(btmp, bytesPerSegment);
 369             off = 0;
 370         }
 371 
 372         // Set up the ColorSpace.
 373         int[] offsets;
 374         ColorSpace cs;
 375         if (bitsPerSample.length == 3) {
 376             offsets = new int[]{off, off + 1, off + 2};
 377             cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
 378         } else {
 379             offsets = new int[]{off};
 380             cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
 381         }
 382 
 383         // Create the ColorModel.
 384         ColorModel cm = new ComponentColorModel(cs,
 385                 false,
 386                 false,
 387                 Transparency.OPAQUE,
 388                 DataBuffer.TYPE_BYTE);
 389 
 390         // Create the SampleModel.
 391         SampleModel sm
 392                 = new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE,
 393                         width, height,
 394                         bitsPerSample.length,
 395                         scanlineStride,
 396                         offsets);
 397 
 398         // Create the WritableRaster.
 399         WritableRaster wras
 400                 = Raster.createWritableRaster(sm, dbb, new Point(0, 0));
 401 
 402         // Create the BufferedImage.
 403         BufferedImage bi = new BufferedImage(cm, wras, false, null);
 404 
 405         // Get the pruned JPEG image metadata (may be null).
 406         IIOMetadata imageMetadata = getImageMetadata(writeAbbreviatedStream);
 407 
 408         // Compress the image into the output stream.
 409         int compDataLength;
 410         if (writeAbbreviatedStream) {
 411             // Write abbreviated JPEG stream
 412 
 413             // First write the tables-only data.
 414             JPEGWriter.prepareWriteSequence(JPEGStreamMetadata);
 415             ios.flush();
 416 
 417             // Rewind to the beginning of the byte array.
 418             baos.reset();
 419 
 420             // Write the abbreviated image data.
 421             IIOImage image = new IIOImage(bi, null, imageMetadata);
 422             JPEGWriter.writeToSequence(image, JPEGParam);
 423             JPEGWriter.endWriteSequence();
 424         } else {
 425             // Write complete JPEG stream
 426             JPEGWriter.write(null,
 427                     new IIOImage(bi, null, imageMetadata),
 428                     JPEGParam);
 429         }
 430 
 431         compDataLength = baos.size();
 432         baos.writeTo(stream);
 433         baos.reset();
 434 
 435         return compDataLength;
 436     }
 437 
 438     protected void finalize() throws Throwable {
 439         super.finalize();
 440         if(JPEGWriter != null) {
 441             JPEGWriter.dispose();
 442         }
 443     }
 444 }