1 /* 2 * Copyright (c) 2005, 2016, 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 }