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      */
pruneNodes(Node tree, boolean pruneTables)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 
getAllNodes(IIOMetadataNode root, List<Node> nodes)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 
TIFFBaseJPEGCompressor(String compressionType, int compressionTagValue, boolean isCompressionLossless, ImageWriteParam param)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 {
IIOByteArrayOutputStream()189         IIOByteArrayOutputStream() {
190             super();
191         }
192 
IIOByteArrayOutputStream(int size)193         IIOByteArrayOutputStream(int size) {
194             super(size);
195         }
196 
writeTo(ImageOutputStream ios)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      */
initJPEGWriter(boolean supportsStreamMetadata, boolean supportsImageMetadata)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      */
getImageMetadata(boolean pruneTables)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 
encode(byte[] b, int off, int width, int height, int[] bitsPerSample, int scanlineStride)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     @SuppressWarnings("deprecation")
finalize()439     protected void finalize() throws Throwable {
440         super.finalize();
441         if(JPEGWriter != null) {
442             JPEGWriter.dispose();
443         }
444     }
445 }
446