001/*
002 * **** BEGIN LICENSE BLOCK *****
003 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
004 *
005 * The contents of this file are subject to the Mozilla Public License Version
006 * 1.1 (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 * http://www.mozilla.org/MPL/
009 *
010 * Software distributed under the License is distributed on an "AS IS" basis,
011 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
012 * for the specific language governing rights and limitations under the
013 * License.
014 *
015 * The Original Code is part of dcm4che, an implementation of DICOM(TM) in
016 * Java(TM), hosted at https://github.com/dcm4che.
017 *
018 * The Initial Developer of the Original Code is Agfa Healthcare.
019 * Portions created by the Initial Developer are Copyright (C) 2011-2015
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 *****
038 */
039
040package org.dcm4che3.imageio.codec;
041
042import org.dcm4che3.data.Attributes;
043import org.dcm4che3.data.Tag;
044import org.dcm4che3.data.VR;
045import org.dcm4che3.image.Overlays;
046import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLS;
047import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLSImageOutputStream;
048import org.dcm4che3.io.DicomInputStream;
049import org.dcm4che3.io.DicomOutputStream;
050import org.dcm4che3.util.ByteUtils;
051import org.dcm4che3.util.Property;
052import org.dcm4che3.util.StreamUtils;
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055
056import javax.imageio.*;
057import javax.imageio.stream.ImageInputStream;
058import javax.imageio.stream.MemoryCacheImageOutputStream;
059import java.awt.image.*;
060import java.io.IOException;
061
062/**
063 * @author Gunter Zeilinger <gunterze@gmail.com>
064 * @since Feb 2015.
065 *
066 * @deprecated This is prototype code. StreamCompressor will be replaced by a Transcoder that supports both stream
067 * compression and decompression. For now you can continue using {@link Compressor} for non-stream compression.
068 */
069@Deprecated
070public class StreamCompressor extends StreamDecompressor {
071
072    private static final Logger LOG = LoggerFactory.getLogger(StreamCompressor.class);
073
074    private TransferSyntaxType compressTsType;
075    private ImageWriter compressor;
076    private PatchJPEGLS compressPatchJPEGLS;
077    private ImageWriteParam compressParam;
078    private int maxPixelValueError = -1;
079    private int avgPixelValueBlockSize = 1;
080    private ImageReader verifier;
081    private ImageReadParam verifyParam;
082    private ImageParams imageParams;
083    private BufferedImage bi2;
084    private int frameIndex;
085
086    public StreamCompressor(DicomInputStream in, String inTransferSyntaxUID, DicomOutputStream out) {
087        super(in, inTransferSyntaxUID, out);
088    }
089
090    public boolean compress(String compressTsuid, Property... params) throws IOException {
091        if (compressTsuid == null)
092            throw new NullPointerException("compressTsuid");
093
094        this.compressTsType = TransferSyntaxType.forUID(compressTsuid);
095        if (compressTsType == null)
096            throw new IllegalArgumentException("Unknown Transfer Syntax: " + compressTsuid);
097
098        ImageWriterFactory.ImageWriterParam param =
099                ImageWriterFactory.getImageWriterParam(compressTsuid);
100        if (param == null)
101            throw new UnsupportedOperationException(
102                    "Unsupported Transfer Syntax: " + compressTsuid);
103
104        this.compressor = ImageWriterFactory.getImageWriter(param);
105        LOG.debug("Compressor: {}", compressor.getClass().getName());
106        this.compressPatchJPEGLS = param.patchJPEGLS;
107        this.compressParam = compressor.getDefaultWriteParam();
108        int count = 0;
109        for (Property property : cat(param.getImageWriteParams(), params)) {
110            String name = property.getName();
111            if (name.equals("maxPixelValueError"))
112                this.maxPixelValueError = ((Number) property.getValue()).intValue();
113            else if (name.equals("avgPixelValueBlockSize"))
114                this.avgPixelValueBlockSize = ((Number) property.getValue()).intValue();
115            else {
116                if (count++ == 0)
117                    compressParam.setCompressionMode(
118                            ImageWriteParam.MODE_EXPLICIT);
119                property.setAt(compressParam);
120            }
121        }
122
123        if (maxPixelValueError >= 0) {
124            ImageReaderFactory.ImageReaderParam readerParam =
125                    ImageReaderFactory.getImageReaderParam(compressTsuid);
126            if (readerParam == null)
127                throw new UnsupportedOperationException(
128                        "Unsupported Transfer Syntax: " + compressTsuid);
129
130            this.verifier = ImageReaderFactory.getImageReader(readerParam);
131            this.verifyParam = verifier.getDefaultReadParam();
132            LOG.debug("Verifier: {}", verifier.getClass().getName());
133        }
134        decompress();
135        return pixeldataProcessed;
136    }
137
138    @Override
139    public void dispose() {
140        super.dispose();
141        if (compressor != null)
142            compressor.dispose();
143        if (verifier != null)
144            verifier.dispose();
145    }
146
147    private Property[] cat(Property[] a, Property[] b) {
148        if (a.length == 0)
149            return b;
150        if (b.length == 0)
151            return a;
152        Property[] c = new Property[a.length + b.length];
153        System.arraycopy(a, 0, c, 0, a.length);
154        System.arraycopy(b, 0, c, a.length, b.length);
155        return c;
156    }
157
158    @Override
159    protected void onPixelData(DicomInputStream dis, Attributes attrs) throws IOException {
160        int tag = dis.tag();
161        VR vr = dis.vr();
162        int len = dis.length();
163        BufferedImage bi = null;
164        this.imageParams = new ImageParams(dataset);
165        if (decompressor != null)
166            imageParams.decompress(attrs, tsType);
167
168        if (decompressor == null || tsType == TransferSyntaxType.RLE)
169            bi = BufferedImageUtils.createBufferedImage(imageParams, compressTsType);
170
171        imageParams.compress(attrs, compressTsType);
172        coerceAttributes.coerce(attrs).writeTo(out);
173        attrs.clear();
174        out.writeHeader(Tag.PixelData, VR.OB, -1);
175        out.writeHeader(Tag.Item, null, 0);
176
177        if (len == -1) {
178            if (!tsType.isPixeldataEncapsulated()) {
179                throw new IOException("Unexpected encapsulated Pixel Data");
180            }
181            decompressFrames(dis, imageParams, bi);
182        } else {
183            if (tsType.isPixeldataEncapsulated())
184                throw new IOException("Pixel Data not encapsulated");
185            int padding = len - imageParams.getLength();
186            if (padding < 0)
187                throw new IllegalArgumentException(
188                        "Pixel data too short: " + len + " instead "
189                                + imageParams.getLength() + " bytes");
190
191            byte[] buf = null;
192            DataBuffer dataBuffer = bi.getRaster().getDataBuffer();
193            if (dataBuffer.getDataType() != DataBuffer.TYPE_BYTE)
194                buf = new byte[imageParams.getFrameLength()];
195
196            for (int i = 0; i < imageParams.getFrames(); i++) {
197                readFrame(dis, dataBuffer, buf);
198                writeFrame(bi);
199            }
200            dis.skipFully(padding);
201        }
202        out.writeHeader(Tag.SequenceDelimitationItem, null, 0);
203        pixeldataProcessed = true;
204    }
205
206   private void readFrame(DicomInputStream dis, DataBuffer db, byte[] buf) throws IOException {
207        switch (db.getDataType()) {
208        case DataBuffer.TYPE_BYTE:
209            byte[][] data = ((DataBufferByte) db).getBankData();
210            for (byte[] bs : data)
211                dis.readFully(bs);
212            if (dis.bigEndian() && dis.vr() == VR.OW)
213                ByteUtils.swapShorts(data);
214            break;
215        case DataBuffer.TYPE_USHORT:
216            dis.readFully(buf);
217            ByteUtils.bytesToShorts(buf, 0, ((DataBufferUShort) db).getData(), 0, buf.length >> 1, dis.bigEndian());
218            break;
219        case DataBuffer.TYPE_SHORT:
220            dis.readFully(buf);
221            ByteUtils.bytesToShorts(buf, 0, ((DataBufferShort) db).getData(), 0, buf.length >> 1, dis.bigEndian());
222            break;
223        default:
224            throw new UnsupportedOperationException("Unsupported Datatype: " + db.getDataType());
225        }
226   }
227
228    @Override
229    protected void writeFrame(BufferedImage bi) throws IOException {
230        if (imageParams.getBitsStored() < imageParams.getBitsAllocated())
231            BufferedImageUtils.nullifyUnusedBits(imageParams.getBitsStored(), bi.getRaster().getDataBuffer());
232
233        MemoryCacheImageOutputStream compressedFrame = new MemoryCacheImageOutputStream(out) {
234
235            @Override
236            public void flush() throws IOException {
237                // defer flush to close()
238                LOG.debug("Ignore invoke of MemoryCacheImageOutputStream.flush()");
239            }
240        };
241        compressor.setOutput(compressPatchJPEGLS != null
242                ? new PatchJPEGLSImageOutputStream(compressedFrame, compressPatchJPEGLS)
243                : compressedFrame);
244        long start = System.currentTimeMillis();
245        compressor.write(null, new IIOImage(bi, null, null), compressParam);
246        long end = System.currentTimeMillis();
247        int streamLength = (int) compressedFrame.getStreamPosition();
248        if (LOG.isDebugEnabled())
249            LOG.debug("Compressed frame #{} {}:1 in {} ms",
250                    frameIndex+1, (float) BufferedImageUtils.sizeOf(bi) / streamLength,  end - start);
251        verify(compressedFrame, bi);
252        out.writeHeader(Tag.Item, null, (streamLength + 1) & ~1);
253        start = System.currentTimeMillis();
254        compressedFrame.close();
255        if ((streamLength & 1) != 0)
256            out.write(0);
257        end = System.currentTimeMillis();
258        LOG.debug("Flushed frame #{} from memory in {} ms", frameIndex+1, start - end);
259        frameIndex++;
260    }
261
262    private void verify(ImageInputStream iis, BufferedImage bi) throws IOException {
263        if (verifier == null)
264            return;
265
266        iis.seek(0);
267        verifier.setInput(iis);
268        verifyParam.setDestination(bi2);
269        long start = System.currentTimeMillis();
270        bi2 = verifier.read(0, verifyParam);
271        int maxDiff =  BufferedImageUtils.maxDiff(bi.getRaster(), bi2.getRaster(), avgPixelValueBlockSize);
272        long end = System.currentTimeMillis();
273        LOG.debug("Verified compressed frame #{} in {} ms - max pixel value error: {}",
274                frameIndex+1, end - start, maxDiff);
275        if (maxDiff > maxPixelValueError)
276            throw new CompressionVerificationException(maxDiff);
277    }
278}