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}