001/* ***** BEGIN LICENSE BLOCK ***** 002 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 003 * 004 * The contents of this file are subject to the Mozilla Public License Version 005 * 1.1 (the "License"); you may not use this file except in compliance with 006 * the License. You may obtain a copy of the License at 007 * http://www.mozilla.org/MPL/ 008 * 009 * Software distributed under the License is distributed on an "AS IS" basis, 010 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 011 * for the specific language governing rights and limitations under the 012 * License. 013 * 014 * The Original Code is part of dcm4che, an implementation of DICOM(TM) in 015 * Java(TM), hosted at https://github.com/gunterze/dcm4che. 016 * 017 * The Initial Developer of the Original Code is 018 * Agfa Healthcare. 019 * Portions created by the Initial Developer are Copyright (C) 2011 020 * the Initial Developer. All Rights Reserved. 021 * 022 * Contributor(s): 023 * See @authors listed below 024 * 025 * Alternatively, the contents of this file may be used under the terms of 026 * either the GNU General Public License Version 2 or later (the "GPL"), or 027 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 028 * in which case the provisions of the GPL or the LGPL are applicable instead 029 * of those above. If you wish to allow use of your version of this file only 030 * under the terms of either the GPL or the LGPL, and not to allow others to 031 * use your version of this file under the terms of the MPL, indicate your 032 * decision by deleting the provisions above and replace them with the notice 033 * and other provisions required by the GPL or the LGPL. If you do not delete 034 * the provisions above, a recipient may use your version of this file under 035 * the terms of any one of the MPL, the GPL or the LGPL. 036 * 037 * ***** END LICENSE BLOCK ***** */ 038package org.dcm4che3.imageio.codec; 039 040import org.dcm4che3.data.Attributes; 041import org.dcm4che3.data.BulkData; 042import org.dcm4che3.data.Fragments; 043import org.dcm4che3.data.Tag; 044import org.dcm4che3.data.VR; 045import org.dcm4che3.data.Value; 046import org.dcm4che3.image.Overlays; 047import org.dcm4che3.image.PhotometricInterpretation; 048import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLS; 049import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLSImageOutputStream; 050import org.dcm4che3.io.DicomEncodingOptions; 051import org.dcm4che3.io.DicomOutputStream; 052import org.dcm4che3.util.ByteUtils; 053import org.dcm4che3.util.Property; 054import org.slf4j.Logger; 055import org.slf4j.LoggerFactory; 056 057import javax.imageio.IIOImage; 058import javax.imageio.ImageReadParam; 059import javax.imageio.ImageReader; 060import javax.imageio.ImageWriteParam; 061import javax.imageio.ImageWriter; 062import javax.imageio.stream.FileImageInputStream; 063import javax.imageio.stream.ImageInputStream; 064import javax.imageio.stream.MemoryCacheImageInputStream; 065import javax.imageio.stream.MemoryCacheImageOutputStream; 066import java.awt.image.BufferedImage; 067import java.awt.image.DataBuffer; 068import java.awt.image.DataBufferByte; 069import java.awt.image.DataBufferShort; 070import java.awt.image.DataBufferUShort; 071import java.io.ByteArrayInputStream; 072import java.io.ByteArrayOutputStream; 073import java.io.Closeable; 074import java.io.FilterOutputStream; 075import java.io.IOException; 076import java.io.OutputStream; 077import java.nio.ByteOrder; 078 079/** 080 * Compresses the pixel data of DICOM images to a lossless or lossy encapsulated transfer syntax format. 081 * <p> 082 * If the source image is already compressed it will be transcoded (i.e. first decompressed then compressed again). 083 * 084 * @author Gunter Zeilinger <gunterze@gmail.com> 085 * @author Hermann Czedik-Eysenberg <hermann-agfa@czedik.net> 086 */ 087public class Compressor implements Closeable { 088 089 private static final Logger LOG = LoggerFactory.getLogger(Compressor.class); 090 091 private final Attributes dataset; 092 private final String compressTsuid; 093 private Object pixels; 094 private VR.Holder pixeldataVR = new VR.Holder(); 095 private final TransferSyntaxType tsType; 096 private ImageWriter compressor; 097 private ImageReader verifier; 098 private PatchJPEGLS compressPatchJPEGLS; 099 private ImageWriteParam compressParam; 100 private ImageInputStream iis; 101 private IOException ex; 102 private int[] embeddedOverlays; 103 private int maxPixelValueError = -1; 104 private int avgPixelValueBlockSize = 1; 105 private BufferedImage decompressedImageForVerification; 106 private ImageParams imageParams; 107 private Decompressor decompressor = null; 108 private BufferedImage uncompressedImage; 109 110 private ImageReadParam verifyParam; 111 112 public Compressor(Attributes dataset, String tsuid, String compressTsuid, Property... compressParams) { 113 if (compressTsuid == null) 114 throw new NullPointerException("compressTsuid"); 115 116 this.dataset = dataset; 117 this.imageParams = new ImageParams(dataset); 118 this.tsType = TransferSyntaxType.forUID(tsuid); 119 this.compressTsuid = compressTsuid; 120 121 pixels = dataset.getValue(Tag.PixelData, pixeldataVR); 122 if (pixels == null) 123 return; 124 125 if (pixels instanceof BulkData) { 126 PhotometricInterpretation pmi = imageParams.getPhotometricInterpretation(); 127 if (pmi.isSubSambled()) 128 throw new UnsupportedOperationException( 129 "Unsupported Photometric Interpretation: " + pmi); 130 if (((BulkData) pixels).length() < imageParams.getLength()) 131 throw new IllegalArgumentException( 132 "Pixel data too short: " + ((BulkData) pixels).length() 133 + " instead " + imageParams.getLength() + " bytes"); 134 } 135 136 if (pixels instanceof Fragments) 137 this.decompressor = new Decompressor(dataset, tsuid); 138 139 embeddedOverlays = Overlays.getEmbeddedOverlayGroupOffsets(dataset); 140 141 ImageWriterFactory.ImageWriterParam param = 142 ImageWriterFactory.getImageWriterParam(compressTsuid); 143 if (param == null) 144 throw new UnsupportedOperationException( 145 "Unsupported Transfer Syntax: " + compressTsuid); 146 147 this.compressor = ImageWriterFactory.getImageWriter(param); 148 LOG.debug("Compressor: {}", compressor.getClass().getName()); 149 this.compressPatchJPEGLS = param.patchJPEGLS; 150 151 this.compressParam = compressor.getDefaultWriteParam(); 152 int count = 0; 153 for (Property property : cat(param.getImageWriteParams(), compressParams)) { 154 String name = property.getName(); 155 if (name.equals("maxPixelValueError")) { 156 maxPixelValueError = ((Number) property.getValue()).intValue(); 157 } else if (name.equals("avgPixelValueBlockSize")) { 158 avgPixelValueBlockSize = ((Number) property.getValue()).intValue(); 159 } else if(name.equals("compressionType")) { 160 compressParam.setCompressionType((String)property.getValue()); 161 } else { 162 if (count++ == 0) { 163 compressParam.setCompressionMode( 164 ImageWriteParam.MODE_EXPLICIT); 165 } 166 property.setAt(compressParam); 167 } 168 } 169 170 if (maxPixelValueError >= 0) { 171 ImageReaderFactory.ImageReaderParam readerParam = 172 ImageReaderFactory.getImageReaderParam(compressTsuid); 173 if (readerParam == null) 174 throw new UnsupportedOperationException( 175 "Unsupported Transfer Syntax: " + compressTsuid); 176 177 this.verifier = ImageReaderFactory.getImageReader(readerParam); 178 this.verifyParam = verifier.getDefaultReadParam(); 179 LOG.debug("Verifier: {}", verifier.getClass().getName()); 180 } 181 } 182 183 public boolean compress() throws IOException { 184 185 if (pixels == null) 186 return false; 187 188 TransferSyntaxType compressTsType = TransferSyntaxType.forUID(compressTsuid); 189 if (decompressor == null || tsType == TransferSyntaxType.RLE) 190 uncompressedImage = BufferedImageUtils.createBufferedImage(imageParams, compressTsType); 191 imageParams.compress(dataset, compressTsType); 192 int frames = imageParams.getFrames(); 193 Fragments compressedPixeldata = 194 dataset.newFragments(Tag.PixelData, VR.OB, frames + 1); 195 compressedPixeldata.add(Value.NULL); 196 for (int i = 0; i < frames; i++) { 197 CompressedFrame frame = new CompressedFrame(i); 198 if (needToExtractEmbeddedOverlays()) 199 frame.compress(); 200 compressedPixeldata.add(frame); 201 } 202 for (int gg0000 : embeddedOverlays) { 203 dataset.setInt(Tag.OverlayBitsAllocated | gg0000, VR.US, 1); 204 dataset.setInt(Tag.OverlayBitPosition | gg0000, VR.US, 0); 205 } 206 return true; 207 } 208 209 private boolean needToExtractEmbeddedOverlays() { 210 return embeddedOverlays.length != 0; 211 } 212 213 private Property[] cat(Property[] a, Property[] b) { 214 if (a.length == 0) 215 return b; 216 if (b.length == 0) 217 return a; 218 Property[] c = new Property[a.length + b.length]; 219 System.arraycopy(a, 0, c, 0, a.length); 220 System.arraycopy(b, 0, c, a.length, b.length); 221 return c; 222 } 223 224 public void close() { 225 if (iis != null) 226 try { iis.close(); } catch (IOException ignore) {} 227 dispose(); 228 } 229 230 public void dispose() { 231 if (compressor != null) 232 compressor.dispose(); 233 234 if (decompressor != null) 235 decompressor.dispose(); 236 237 if (verifier != null) 238 verifier.dispose(); 239 240 compressor = null; 241 verifier = null; 242 } 243 244 private class CompressedFrame implements Value { 245 246 private int frameIndex; 247 private int streamLength; 248 private CacheOutputStream cacheout = new CacheOutputStream(); 249 private MemoryCacheImageOutputStream cache; 250 251 public CompressedFrame(int frameIndex) throws IOException { 252 this.frameIndex = frameIndex; 253 } 254 255 @Override 256 public boolean isEmpty() { 257 return false; 258 } 259 260 @Override 261 public byte[] toBytes(VR vr, boolean bigEndian) throws IOException { 262 ByteArrayOutputStream out = new ByteArrayOutputStream(); 263 writeTo(out); 264 return out.toByteArray(); 265 } 266 267 @Override 268 public void writeTo(DicomOutputStream out, VR vr) throws IOException { 269 writeTo(out); 270 } 271 272 @Override 273 public int calcLength(DicomEncodingOptions encOpts, boolean explicitVR, VR vr) { 274 return getEncodedLength(encOpts, explicitVR, vr); 275 } 276 277 @Override 278 public int getEncodedLength(DicomEncodingOptions encOpts, boolean explicitVR, VR vr) { 279 try { 280 compress(); 281 } catch (IOException e) { 282 return -1; 283 } 284 return (streamLength + 1) & ~1; 285 } 286 287 private void writeTo(OutputStream out) throws IOException { 288 compress(); 289 cacheout.set(out); 290 long start = System.currentTimeMillis(); 291 cache.close(); 292 if ((streamLength & 1) != 0) 293 out.write(0); 294 long end = System.currentTimeMillis(); 295 LOG.debug("Flushed frame #{} from memory in {} ms", frameIndex + 1, end - start); 296 } 297 298 private void compress() throws IOException { 299 if (cache != null) 300 return; 301 302 if (ex != null) 303 throw ex; 304 305 try { 306 BufferedImage imageToCompress = Compressor.this.readFrame(frameIndex); 307 Compressor.this.extractEmbeddedOverlays(frameIndex, imageToCompress); 308 if (imageParams.getBitsStored() < imageParams.getBitsAllocated()) 309 BufferedImageUtils.nullifyUnusedBits(imageParams.getBitsStored(), 310 imageToCompress.getRaster().getDataBuffer()); 311 cache = new MemoryCacheImageOutputStream(cacheout) { 312 313 @Override 314 public void flush() throws IOException { 315 // defer flush to writeTo() 316 LOG.debug("Ignore invoke of MemoryCacheImageOutputStream.flush()"); 317 } 318 }; 319 compressor.setOutput(compressPatchJPEGLS != null 320 ? new PatchJPEGLSImageOutputStream(cache, compressPatchJPEGLS) 321 : cache); 322 long start = System.currentTimeMillis(); 323 compressor.write(null, new IIOImage(imageToCompress, null, null), compressParam); 324 long end = System.currentTimeMillis(); 325 streamLength = (int) cache.getStreamPosition(); 326 if (LOG.isDebugEnabled()) 327 LOG.debug("Compressed frame #{} {}:1 in {} ms", 328 frameIndex + 1, 329 (float) BufferedImageUtils.sizeOf(imageToCompress) / streamLength, 330 end - start); 331 Compressor.this.verify(cache, frameIndex); 332 } catch (IOException ex) { 333 cache = null; 334 Compressor.this.ex = ex; 335 throw ex; 336 } 337 } 338 339 } 340 341 private static class CacheOutputStream extends FilterOutputStream { 342 343 public CacheOutputStream() { 344 super(null); 345 } 346 347 public void set(OutputStream out) { 348 this.out = out; 349 } 350 } 351 352 public BufferedImage readFrame(int frameIndex) throws IOException { 353 if (iis == null) 354 iis = createImageInputStream(frameIndex); 355 356 if (decompressor != null) 357 return decompressor.decompressFrame(iis, frameIndex); 358 359 if (pixels instanceof BulkData) { 360 iis.setByteOrder(((BulkData)pixels).bigEndian 361 ? ByteOrder.BIG_ENDIAN 362 : ByteOrder.LITTLE_ENDIAN); 363 iis.seek(((BulkData)pixels).offset() + imageParams.getFrameLength() * frameIndex); 364 } else { 365 iis.setByteOrder(dataset.bigEndian() 366 ? ByteOrder.BIG_ENDIAN 367 : ByteOrder.LITTLE_ENDIAN); 368 iis.seek(imageParams.getFrameLength() * frameIndex); 369 } 370 DataBuffer db = uncompressedImage.getRaster().getDataBuffer(); 371 switch (db.getDataType()) { 372 case DataBuffer.TYPE_BYTE: 373 byte[][] data = ((DataBufferByte) db).getBankData(); 374 for (byte[] bs : data) 375 iis.readFully(bs); 376 if (pixels instanceof BulkData) { 377 if (((BulkData)pixels).bigEndian && pixeldataVR.vr == VR.OW) 378 ByteUtils.swapShorts(data); 379 } else { 380 if (dataset.bigEndian() && pixeldataVR.vr == VR.OW) 381 ByteUtils.swapShorts(data); 382 } 383 break; 384 case DataBuffer.TYPE_USHORT: 385 readFully(((DataBufferUShort) db).getData()); 386 break; 387 case DataBuffer.TYPE_SHORT: 388 readFully(((DataBufferShort) db).getData()); 389 break; 390 default: 391 throw new UnsupportedOperationException( 392 "Unsupported Datatype: " + db.getDataType()); 393 } 394 return uncompressedImage; 395 } 396 397 private void verify(ImageInputStream iis, int index) 398 throws IOException { 399 if (verifier == null) 400 return; 401 402 iis.seek(0); 403 verifier.setInput(iis); 404 verifyParam.setDestination(decompressedImageForVerification); 405 long start = System.currentTimeMillis(); 406 decompressedImageForVerification = verifier.read(0, verifyParam); 407 int maxDiff = BufferedImageUtils.maxDiff(uncompressedImage.getRaster(), decompressedImageForVerification.getRaster(), avgPixelValueBlockSize); 408 long end = System.currentTimeMillis(); 409 if (LOG.isDebugEnabled()) 410 LOG.debug("Verified compressed frame #{} in {} ms - max pixel value error: {}", 411 index + 1, end - start, maxDiff); 412 if (maxDiff > maxPixelValueError) 413 throw new CompressionVerificationException(maxDiff); 414 415 } 416 417 private void extractEmbeddedOverlays(int frameIndex, BufferedImage bi) { 418 for (int gg0000 : embeddedOverlays) { 419 int ovlyRow = dataset.getInt(Tag.OverlayRows | gg0000, 0); 420 int ovlyColumns = dataset.getInt(Tag.OverlayColumns | gg0000, 0); 421 int ovlyBitPosition = dataset.getInt(Tag.OverlayBitPosition | gg0000, 0); 422 int mask = 1 << ovlyBitPosition; 423 int ovlyLength = ovlyRow * ovlyColumns; 424 byte[] ovlyData = dataset.getSafeBytes(Tag.OverlayData | gg0000); 425 if (ovlyData == null) { 426 ovlyData = new byte[(((ovlyLength*imageParams.getFrames()+7)>>>3)+1)&(~1)]; 427 dataset.setBytes(Tag.OverlayData | gg0000, VR.OB, ovlyData); 428 } 429 Overlays.extractFromPixeldata(bi.getRaster(), mask, ovlyData, 430 ovlyLength * frameIndex, ovlyLength); 431 LOG.debug("Extracted embedded overlay #{} from bit #{} of frame #{}", 432 (gg0000 >>> 17) + 1, ovlyBitPosition, frameIndex + 1); 433 } 434 } 435 436 private void readFully(short[] data) throws IOException { 437 iis.readFully(data, 0, data.length); 438 } 439 440 public ImageInputStream createImageInputStream(int frameIndex) throws IOException { 441 442 if (pixels instanceof BulkData) { 443 return new FileImageInputStream(((BulkData) pixels).getFile()); 444 } 445 446 if (pixels instanceof byte[]) { 447 return new MemoryCacheImageInputStream(new ByteArrayInputStream((byte[])pixels)); 448 } 449 450 return null; 451 } 452 453 /** 454 * @return (Pessimistic) estimation of the maximum heap memory (in bytes) that will be needed at any moment in time 455 * during compression. 456 */ 457 public long getEstimatedNeededMemory() { 458 if (pixels == null) 459 return 0; 460 461 long memoryNeededDuringDecompression = 0; 462 463 long uncompressedFrameLength = imageParams.getFrameLength(); 464 465 if (decompressor != null) { 466 // memory for decompression, if decompression is needed 467 memoryNeededDuringDecompression += decompressor.getEstimatedNeededMemory(); 468 } 469 470 long memoryNeededDuringCompression = 0; 471 472 // Memory for the uncompressed buffered image (only one frame, as frames are always processed sequentially) 473 memoryNeededDuringCompression += uncompressedFrameLength; 474 475 if (verifier != null) { 476 // verification step done, an additional uncompressed buffered image needs to be allocated 477 memoryNeededDuringCompression += uncompressedFrameLength; 478 } 479 480 // Memory for one compressed frame 481 // (For now: pessimistic assumption that same memory as for the uncompressed frame is needed. This very much 482 // depends on the compression algorithm, properties and the compressor implementation.) 483 long compressedFrameLength = uncompressedFrameLength; 484 485 if (!needToExtractEmbeddedOverlays()) { 486 // As the compression happens lazily on demand (when writing to the OutputStream), we just need to keep one 487 // frame in memory at one moment in time. 488 memoryNeededDuringCompression += compressedFrameLength; 489 } else { 490 // if embedded overlays need to be extracted, compression for each frame is done eagerly, so all compressed 491 // frames have to be kept in memory 492 memoryNeededDuringCompression += compressedFrameLength * imageParams.getFrames(); 493 } 494 495 // as decompression and compression are executed sequentially (in between GC can run), 496 // therefore we have to consider the maximum of the two 497 return Math.max(memoryNeededDuringDecompression, memoryNeededDuringCompression); 498 } 499}