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