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.*; 041import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLS; 042import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLSImageInputStream; 043import org.dcm4che3.imageio.stream.SegmentedImageInputStream; 044import org.dcm4che3.io.DicomEncodingOptions; 045import org.dcm4che3.io.DicomOutputStream; 046import org.slf4j.Logger; 047import org.slf4j.LoggerFactory; 048 049import javax.imageio.ImageReadParam; 050import javax.imageio.ImageReader; 051import javax.imageio.stream.FileImageInputStream; 052import javax.imageio.stream.ImageInputStream; 053import javax.imageio.stream.MemoryCacheImageInputStream; 054import java.awt.image.BufferedImage; 055import java.io.ByteArrayInputStream; 056import java.io.ByteArrayOutputStream; 057import java.io.IOException; 058import java.io.OutputStream; 059 060/** 061 * Decompresses the pixel data of compressed DICOM images to the native (uncompressed) format. 062 * 063 * @author Gunter Zeilinger <gunterze@gmail.com> 064 * @author Hermann Czedik-Eysenberg <hermann-agfa@czedik.net> 065 */ 066public class Decompressor { 067 068 private static final Logger LOG = LoggerFactory.getLogger(Decompressor.class); 069 070 private final Attributes dataset; 071 private Object pixels; 072 private final String tsuid; 073 private final TransferSyntaxType tsType; 074 private ImageParams imageParams; 075 private BufferedImage decompressedImage; 076 private ImageReader imageReader; 077 private ImageReadParam readParam; 078 private PatchJPEGLS patchJPEGLS; 079 080 public Decompressor(Attributes dataset, String tsuid) { 081 if (tsuid == null) 082 throw new NullPointerException("tsuid"); 083 084 this.dataset = dataset; 085 this.tsuid = tsuid; 086 this.tsType = TransferSyntaxType.forUID(tsuid); 087 this.pixels = dataset.getValue(Tag.PixelData); 088 if (this.pixels == null) 089 return; 090 091 if (tsType == null) 092 throw new IllegalArgumentException("Unknown Transfer Syntax: " + tsuid); 093 094 this.imageParams = new ImageParams(dataset); 095 int frames = imageParams.getFrames(); 096 097 if (this.pixels instanceof Fragments) { 098 if (!tsType.isPixeldataEncapsulated()) 099 throw new IllegalArgumentException("Encapusulated Pixel Data" 100 + "with Transfer Syntax: " + tsuid); 101 102 int numFragments = ((Fragments)this.pixels).size(); 103 if (frames == 1 ? (numFragments < 2) 104 : (numFragments != frames + 1)) 105 throw new IllegalArgumentException( 106 "Number of Pixel Data Fragments: " 107 + numFragments + " does not match " + frames); 108 109 ImageReaderFactory.ImageReaderParam param = 110 ImageReaderFactory.getImageReaderParam(tsuid); 111 if (param == null) 112 throw new UnsupportedOperationException( 113 "Unsupported Transfer Syntax: " + tsuid); 114 115 this.imageReader = ImageReaderFactory.getImageReader(param); 116 LOG.debug("Decompressor: {}", imageReader.getClass().getName()); 117 this.readParam = imageReader.getDefaultReadParam(); 118 this.patchJPEGLS = param.patchJPEGLS; 119 } 120 } 121 122 public void dispose() { 123 if (imageReader != null) 124 imageReader.dispose(); 125 126 imageReader = null; 127 } 128 129 public boolean decompress() { 130 if (imageReader == null) 131 return false; 132 133 imageParams.decompress(dataset, tsType); 134 135 dataset.setValue(Tag.PixelData, VR.OW, new Value() { 136 137 @Override 138 public boolean isEmpty() { 139 return false; 140 } 141 142 @Override 143 public byte[] toBytes(VR vr, boolean bigEndian) throws IOException { 144 ByteArrayOutputStream out = new ByteArrayOutputStream(); 145 Decompressor.this.writeTo(out); 146 return out.toByteArray(); 147 } 148 149 @Override 150 public void writeTo(DicomOutputStream out, VR vr) throws IOException { 151 Decompressor.this.writeTo(out); 152 } 153 154 @Override 155 public int calcLength(DicomEncodingOptions encOpts, boolean explicitVR, VR vr) { 156 return imageParams.getEncodedLength(); 157 } 158 159 @Override 160 public int getEncodedLength(DicomEncodingOptions encOpts, boolean explicitVR, VR vr) { 161 return imageParams.getEncodedLength(); 162 } 163 }); 164 return true; 165 } 166 167 public static boolean decompress(Attributes dataset, String tsuid) { 168 return new Decompressor(dataset, tsuid).decompress(); 169 } 170 171 public void writeTo(OutputStream out) throws IOException { 172 int frames = imageParams.getFrames(); 173 try { 174 for (int i = 0; i < frames; ++i) { 175 ImageInputStream iis = createImageInputStream(i); 176 writeFrameTo(iis, i, out); 177 close(iis); 178 } 179 if (imageParams.paddingNull()) 180 out.write(0); 181 } finally { 182 183 imageReader.dispose(); 184 } 185 } 186 187 private void close (ImageInputStream iis) { 188 try { iis.close(); } catch (IOException ignore) {} 189 } 190 191 public void writeFrameTo(ImageInputStream iis, int frameIndex, 192 OutputStream out) throws IOException { 193 BufferedImageUtils.writeTo(decompressFrame(iis, frameIndex), out); 194 } 195 196 @SuppressWarnings("resource") 197 protected BufferedImage decompressFrame(ImageInputStream iis, int index) 198 throws IOException { 199 200 if (pixels instanceof Fragments && ((Fragments) pixels).get(index+1) instanceof BulkData) 201 iis = SegmentedImageInputStream.ofFrame(iis, (Fragments) pixels, index, imageParams.getFrames()); 202 203 if (decompressedImage == null && tsType == TransferSyntaxType.RLE) 204 decompressedImage = BufferedImageUtils.createBufferedImage(imageParams, tsType); 205 206 imageReader.setInput(patchJPEGLS != null 207 ? new PatchJPEGLSImageInputStream(iis, patchJPEGLS) 208 : iis); 209 readParam.setDestination(decompressedImage); 210 long start = System.currentTimeMillis(); 211 decompressedImage = imageReader.read(0, readParam); 212 long end = System.currentTimeMillis(); 213 if (LOG.isDebugEnabled()) 214 LOG.debug("Decompressed frame #{} 1:{} in {} ms", 215 new Object[] {index + 1, 216 (float) BufferedImageUtils.sizeOf(decompressedImage) / iis.getStreamPosition(), 217 end - start }); 218 return decompressedImage; 219 } 220 221 public ImageInputStream createImageInputStream() throws IOException { 222 return createImageInputStream(0); 223 } 224 225 public ImageInputStream createImageInputStream(int frameIndex) throws IOException { 226 227 if (pixels instanceof Fragments) { 228 Fragments pixelFragments = (Fragments) pixels; 229 if (pixelFragments.get(frameIndex + 1) instanceof BulkData) 230 return new FileImageInputStream(((BulkData) pixelFragments.get(frameIndex + 1)).getFile()); 231 else if (pixelFragments.get(frameIndex + 1) instanceof byte[]) 232 return new MemoryCacheImageInputStream(new ByteArrayInputStream((byte[])pixelFragments.get(frameIndex + 1))); 233 else 234 return null; 235 } 236 237 if (pixels instanceof byte[]) { 238 return new MemoryCacheImageInputStream(new ByteArrayInputStream((byte[])pixels)); 239 } 240 241 return null; 242 } 243 244 /** 245 * @return (Pessimistic) estimation of the maximum heap memory (in bytes) that will be needed at any moment in time 246 * during decompression. 247 */ 248 public long getEstimatedNeededMemory() { 249 if (pixels == null) 250 return 0; 251 252 long uncompressedFrameLength = imageParams.getFrameLength(); 253 254 // Memory needed for reading one compressed frame 255 // (For now: pessimistic assumption that same memory as for the uncompressed frame is needed. This very much 256 // depends on the compression algorithm and properties.) 257 // Actually it might be much less, if the decompressor supports streaming in the compressed data. 258 long compressedFrameLength = uncompressedFrameLength; 259 260 // As decompression happens lazily on demand (when writing to the OutputStream) the needed memory at one moment 261 // in time will just be one compressed frame plus one decompressed frame. 262 return compressedFrameLength + uncompressedFrameLength; 263 } 264}