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}