/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is part of dcm4che, an implementation of DICOM(TM) in
* Java(TM), hosted at https://github.com/gunterze/dcm4che.
*
* The Initial Developer of the Original Code is
* Agfa Healthcare.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* See @authors listed below
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.dcm4che3.imageio.codec;
import org.dcm4che3.data.*;
import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLS;
import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLSImageInputStream;
import org.dcm4che3.imageio.stream.SegmentedImageInputStream;
import org.dcm4che3.io.DicomEncodingOptions;
import org.dcm4che3.io.DicomOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* Decompresses the pixel data of compressed DICOM images to the native (uncompressed) format.
*
* @author Gunter Zeilinger <gunterze@gmail.com>
* @author Hermann Czedik-Eysenberg <hermann-agfa@czedik.net>
*/
public class Decompressor {
private static final Logger LOG = LoggerFactory.getLogger(Decompressor.class);
private final Attributes dataset;
private Object pixels;
private final String tsuid;
private final TransferSyntaxType tsType;
private ImageParams imageParams;
private BufferedImage decompressedImage;
private ImageReader imageReader;
private ImageReadParam readParam;
private PatchJPEGLS patchJPEGLS;
public Decompressor(Attributes dataset, String tsuid) {
if (tsuid == null)
throw new NullPointerException("tsuid");
this.dataset = dataset;
this.tsuid = tsuid;
this.tsType = TransferSyntaxType.forUID(tsuid);
this.pixels = dataset.getValue(Tag.PixelData);
if (this.pixels == null)
return;
if (tsType == null)
throw new IllegalArgumentException("Unknown Transfer Syntax: " + tsuid);
this.imageParams = new ImageParams(dataset);
int frames = imageParams.getFrames();
if (this.pixels instanceof Fragments) {
if (!tsType.isPixeldataEncapsulated())
throw new IllegalArgumentException("Encapusulated Pixel Data"
+ "with Transfer Syntax: " + tsuid);
int numFragments = ((Fragments)this.pixels).size();
if (frames == 1 ? (numFragments < 2)
: (numFragments != frames + 1))
throw new IllegalArgumentException(
"Number of Pixel Data Fragments: "
+ numFragments + " does not match " + frames);
ImageReaderFactory.ImageReaderParam param =
ImageReaderFactory.getImageReaderParam(tsuid);
if (param == null)
throw new UnsupportedOperationException(
"Unsupported Transfer Syntax: " + tsuid);
this.imageReader = ImageReaderFactory.getImageReader(param);
LOG.debug("Decompressor: {}", imageReader.getClass().getName());
this.readParam = imageReader.getDefaultReadParam();
this.patchJPEGLS = param.patchJPEGLS;
}
}
public void dispose() {
if (imageReader != null)
imageReader.dispose();
imageReader = null;
}
public boolean decompress() {
if (imageReader == null)
return false;
imageParams.decompress(dataset, tsType);
dataset.setValue(Tag.PixelData, VR.OW, new Value() {
@Override
public boolean isEmpty() {
return false;
}
@Override
public byte[] toBytes(VR vr, boolean bigEndian) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Decompressor.this.writeTo(out);
return out.toByteArray();
}
@Override
public void writeTo(DicomOutputStream out, VR vr) throws IOException {
Decompressor.this.writeTo(out);
}
@Override
public int calcLength(DicomEncodingOptions encOpts, boolean explicitVR, VR vr) {
return imageParams.getEncodedLength();
}
@Override
public int getEncodedLength(DicomEncodingOptions encOpts, boolean explicitVR, VR vr) {
return imageParams.getEncodedLength();
}
});
return true;
}
public static boolean decompress(Attributes dataset, String tsuid) {
return new Decompressor(dataset, tsuid).decompress();
}
public void writeTo(OutputStream out) throws IOException {
int frames = imageParams.getFrames();
try {
for (int i = 0; i < frames; ++i) {
ImageInputStream iis = createImageInputStream(i);
writeFrameTo(iis, i, out);
close(iis);
}
if (imageParams.paddingNull())
out.write(0);
} finally {
imageReader.dispose();
}
}
private void close (ImageInputStream iis) {
try { iis.close(); } catch (IOException ignore) {}
}
public void writeFrameTo(ImageInputStream iis, int frameIndex,
OutputStream out) throws IOException {
BufferedImageUtils.writeTo(decompressFrame(iis, frameIndex), out);
}
@SuppressWarnings("resource")
protected BufferedImage decompressFrame(ImageInputStream iis, int index)
throws IOException {
if (pixels instanceof Fragments && ((Fragments) pixels).get(index+1) instanceof BulkData)
iis = SegmentedImageInputStream.ofFrame(iis, (Fragments) pixels, index, imageParams.getFrames());
if (decompressedImage == null && tsType == TransferSyntaxType.RLE)
decompressedImage = BufferedImageUtils.createBufferedImage(imageParams, tsType);
imageReader.setInput(patchJPEGLS != null
? new PatchJPEGLSImageInputStream(iis, patchJPEGLS)
: iis);
readParam.setDestination(decompressedImage);
long start = System.currentTimeMillis();
decompressedImage = imageReader.read(0, readParam);
long end = System.currentTimeMillis();
if (LOG.isDebugEnabled())
LOG.debug("Decompressed frame #{} 1:{} in {} ms",
new Object[] {index + 1,
(float) BufferedImageUtils.sizeOf(decompressedImage) / iis.getStreamPosition(),
end - start });
return decompressedImage;
}
public ImageInputStream createImageInputStream() throws IOException {
return createImageInputStream(0);
}
public ImageInputStream createImageInputStream(int frameIndex) throws IOException {
if (pixels instanceof Fragments) {
Fragments pixelFragments = (Fragments) pixels;
if (pixelFragments.get(frameIndex + 1) instanceof BulkData)
return new FileImageInputStream(((BulkData) pixelFragments.get(frameIndex + 1)).getFile());
else if (pixelFragments.get(frameIndex + 1) instanceof byte[])
return new MemoryCacheImageInputStream(new ByteArrayInputStream((byte[])pixelFragments.get(frameIndex + 1)));
else
return null;
}
if (pixels instanceof byte[]) {
return new MemoryCacheImageInputStream(new ByteArrayInputStream((byte[])pixels));
}
return null;
}
/**
* @return (Pessimistic) estimation of the maximum heap memory (in bytes) that will be needed at any moment in time
* during decompression.
*/
public long getEstimatedNeededMemory() {
if (pixels == null)
return 0;
long uncompressedFrameLength = imageParams.getFrameLength();
// Memory needed for reading one compressed frame
// (For now: pessimistic assumption that same memory as for the uncompressed frame is needed. This very much
// depends on the compression algorithm and properties.)
// Actually it might be much less, if the decompressor supports streaming in the compressed data.
long compressedFrameLength = uncompressedFrameLength;
// As decompression happens lazily on demand (when writing to the OutputStream) the needed memory at one moment
// in time will just be one compressed frame plus one decompressed frame.
return compressedFrameLength + uncompressedFrameLength;
}
}