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) 2013
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
039package org.dcm4che3.imageio.plugins.dcm;
040
041import java.awt.image.BufferedImage;
042import java.awt.image.ColorModel;
043import java.awt.image.DataBuffer;
044import java.awt.image.DataBufferByte;
045import java.awt.image.DataBufferUShort;
046import java.awt.image.Raster;
047import java.awt.image.SampleModel;
048import java.awt.image.WritableRaster;
049import java.io.ByteArrayInputStream;
050import java.io.File;
051import java.io.FileNotFoundException;
052import java.io.IOException;
053import java.nio.ByteOrder;
054import java.util.Collections;
055import java.util.Iterator;
056
057import javax.imageio.ImageReadParam;
058import javax.imageio.ImageReader;
059import javax.imageio.ImageTypeSpecifier;
060import javax.imageio.metadata.IIOMetadata;
061import javax.imageio.spi.ImageReaderSpi;
062import javax.imageio.stream.FileImageInputStream;
063import javax.imageio.stream.ImageInputStream;
064import javax.imageio.stream.ImageInputStreamImpl;
065import javax.imageio.stream.MemoryCacheImageInputStream;
066
067import org.dcm4che3.data.Tag;
068import org.dcm4che3.data.UID;
069import org.dcm4che3.data.Attributes;
070import org.dcm4che3.data.BulkData;
071import org.dcm4che3.data.Fragments;
072import org.dcm4che3.data.Sequence;
073import org.dcm4che3.data.VR;
074import org.dcm4che3.image.LookupTable;
075import org.dcm4che3.image.LookupTableFactory;
076import org.dcm4che3.image.Overlays;
077import org.dcm4che3.image.PhotometricInterpretation;
078import org.dcm4che3.image.StoredValue;
079import org.dcm4che3.imageio.codec.ImageReaderFactory;
080import org.dcm4che3.imageio.codec.ImageReaderFactory.ImageReaderParam;
081import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLS;
082import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLSImageInputStream;
083import org.dcm4che3.imageio.stream.ImageInputStreamAdapter;
084import org.dcm4che3.imageio.stream.SegmentedImageInputStream;
085import org.dcm4che3.io.BulkDataDescriptor;
086import org.dcm4che3.io.DicomInputStream;
087import org.dcm4che3.io.DicomInputStream.IncludeBulkData;
088import org.dcm4che3.util.ByteUtils;
089import org.slf4j.Logger;
090import org.slf4j.LoggerFactory;
091
092/**
093 * @author Gunter Zeilinger <gunterze@gmail.com>
094 * @since Feb 2013
095 *
096 */
097public class DicomImageReader extends ImageReader {
098
099    private static final Logger LOG = LoggerFactory.getLogger(DicomImageReader.class);
100
101    private ImageInputStream iis;
102
103    private Attributes ds;
104
105    private DicomMetaData metadata;
106
107    private int frames;
108
109    private int width;
110
111    private int height;
112
113    private BulkData pixelBulkData;
114
115    private final VR.Holder pixeldataVR = new VR.Holder();
116
117    private Fragments pixeldataFragments;
118
119    private File pixeldataFile;
120
121    private byte[] pixeldataBytes;
122
123    private ImageReader decompressor;
124
125    private boolean rle;
126
127    private PatchJPEGLS patchJpegLS;
128
129    private int samples;
130
131    private boolean banded;
132
133    private int bitsStored;
134
135    private int bitsAllocated;
136
137    private int dataType;
138
139    private int frameLength;
140
141    private PhotometricInterpretation pmi;
142
143    public DicomImageReader(ImageReaderSpi originatingProvider) {
144        super(originatingProvider);
145    }
146
147    @Override
148    public void setInput(Object input, boolean seekForwardOnly,
149            boolean ignoreMetadata) {
150        super.setInput(input, seekForwardOnly, ignoreMetadata);
151        resetInternalState();
152        if (input instanceof DicomMetaData) {
153            setMetadata((DicomMetaData) input);
154            if (pixelBulkData != null) {
155                pixeldataFile = pixelBulkData.getFile();
156            } else if (pixeldataFragments != null && pixeldataFragments.size() > 1) {
157                Object fragment = pixeldataFragments.get(1);
158                if (fragment instanceof BulkData) {
159                    pixeldataFile = ((BulkData) fragment).getFile();
160                } else if (!(fragment instanceof byte[])) {
161                    throw new IllegalArgumentException("Fragments is neither BulkData nor byte[]; instead it is " + fragment.getClass());
162                }
163            }
164        } else {
165            iis = (ImageInputStream) input;
166        }
167    }
168
169    @Override
170    public int getNumImages(boolean allowSearch) throws IOException {
171        readMetadata();
172        return frames;
173    }
174
175    @Override
176    public int getWidth(int frameIndex) throws IOException {
177       readMetadata();
178       checkIndex(frameIndex);
179       return width;
180    }
181
182    @Override
183    public int getHeight(int frameIndex) throws IOException {
184        readMetadata();
185        checkIndex(frameIndex);
186        return height;
187    }
188
189
190    @Override
191    public ImageTypeSpecifier getRawImageType(int frameIndex)
192            throws IOException {
193        readMetadata();
194        checkIndex(frameIndex);
195
196        if (decompressor == null)
197            return createImageType(bitsStored, dataType, banded);
198
199        if (rle)
200            return createImageType(bitsStored, dataType, true);
201
202        openiis();
203        try {
204            decompressor.setInput(iisOfFrame(0));
205            return decompressor.getRawImageType(0);
206        } finally {
207            closeiis();
208        }
209    }
210
211    @Override
212    public Iterator<ImageTypeSpecifier> getImageTypes(int frameIndex)
213            throws IOException {
214        readMetadata();
215        checkIndex(frameIndex);
216
217        ImageTypeSpecifier imageType;
218        if (pmi.isMonochrome())
219            imageType = createImageType(8, DataBuffer.TYPE_BYTE, false);
220        else if (decompressor == null)
221            imageType = createImageType(bitsStored, dataType, banded);
222        else if (rle)
223            imageType = createImageType(bitsStored, dataType, true);
224        else {
225            openiis();
226            try {
227                decompressor.setInput(iisOfFrame(0));
228                return decompressor.getImageTypes(0);
229            } finally {
230                closeiis();
231            }
232        }
233
234        return Collections.singletonList(imageType).iterator();
235    }
236
237    private void openiis() throws FileNotFoundException, IOException {
238        if (iis == null) {
239            if (pixeldataFile != null) {
240                iis = new FileImageInputStream(pixeldataFile);
241            } else if (pixeldataBytes != null) {
242                iis = new MemoryCacheImageInputStream(new ByteArrayInputStream(pixeldataBytes));
243            } else if (pixeldataFragments != null) {
244                iis = new MemoryCacheImageInputStream(
245                     new ByteArrayInputStream((byte[]) pixeldataFragments.get(1)));
246            }
247        }
248    }
249
250    private void closeiis() throws IOException {
251        if (pixeldataFile != null && iis != null) {
252            iis.close();
253            iis = null;
254        }
255    }
256
257    @Override
258    public ImageReadParam getDefaultReadParam() {
259        return new DicomImageReadParam();
260    }
261
262    @Override
263    public IIOMetadata getStreamMetadata() throws IOException {
264        readMetadata();
265        return metadata;
266    }
267
268    @Override
269    public IIOMetadata getImageMetadata(int frameIndex) throws IOException {
270        return null;
271    }
272
273    @Override
274    public boolean canReadRaster() {
275        return true;
276    }
277
278    @Override
279    public Raster readRaster(int frameIndex, ImageReadParam param)
280            throws IOException {
281        readMetadata();
282        checkIndex(frameIndex);
283
284        openiis();
285        try {
286            if (decompressor != null) {
287                decompressor.setInput(iisOfFrame(frameIndex));
288
289                if (LOG.isDebugEnabled())
290                    LOG.debug("Start decompressing frame #" + (frameIndex + 1));
291                Raster wr = pmi.decompress() == pmi && decompressor.canReadRaster()
292                        ? decompressor.readRaster(0, decompressParam(param))
293                        : decompressor.read(0, decompressParam(param)).getRaster();
294                if (LOG.isDebugEnabled())
295                    LOG.debug("Finished decompressing frame #" + (frameIndex + 1));
296                return wr;
297            }
298            iis.setByteOrder(ds.bigEndian()
299                    ? ByteOrder.BIG_ENDIAN
300                    : ByteOrder.LITTLE_ENDIAN);
301            if (pixelBulkData != null) {
302                iis.seek(pixelBulkData.offset() + frameIndex * frameLength);
303            } else if (pixeldataBytes != null) {
304                iis.seek(frameIndex * frameLength);
305            } else if (pixeldataFragments != null && pixeldataFile == null) {
306              throw new RuntimeException("fix this?");
307            }
308
309            WritableRaster wr = Raster.createWritableRaster(
310                    createSampleModel(dataType, banded), null);
311            DataBuffer buf = wr.getDataBuffer();
312            if (buf instanceof DataBufferByte) {
313                byte[][] data = ((DataBufferByte) buf).getBankData();
314                for (byte[] bs : data)
315                    iis.readFully(bs);
316                if (pixelBulkData.bigEndian && pixeldataVR.vr == VR.OW)
317                    ByteUtils.swapShorts(data);
318            } else {
319                short[] data = ((DataBufferUShort) buf).getData();
320                iis.readFully(data, 0, data.length);
321            }
322            return wr;
323        } finally {
324            closeiis();
325        }
326    }
327
328    private ImageReadParam decompressParam(ImageReadParam param) {
329        ImageReadParam decompressParam = decompressor.getDefaultReadParam();
330        ImageTypeSpecifier imageType = null;
331        BufferedImage dest = null;
332        if (param != null) {
333            imageType = param.getDestinationType();
334            dest = param.getDestination();
335        }
336        if (rle && imageType == null && dest == null)
337            imageType = createImageType(bitsStored, dataType, true);
338        decompressParam.setDestinationType(imageType);
339        decompressParam.setDestination(dest);
340        return decompressParam;
341    }
342
343    @Override
344    public BufferedImage read(int frameIndex, ImageReadParam param)
345            throws IOException {
346        readMetadata();
347        checkIndex(frameIndex);
348
349        WritableRaster raster;
350        if (decompressor != null) {
351            openiis();
352            try {
353                decompressor.setInput(iisOfFrame(frameIndex));
354                if (LOG.isDebugEnabled())
355                    LOG.debug("Start decompressing frame #" + (frameIndex + 1));
356                BufferedImage bi = decompressor.read(0, decompressParam(param));
357                if (LOG.isDebugEnabled())
358                    LOG.debug("Finished decompressing frame #" + (frameIndex + 1));
359                if (samples > 1)
360                    return bi;
361
362                raster = bi.getRaster();
363            } finally {
364                closeiis();
365            }
366        } else
367            raster = (WritableRaster) readRaster(frameIndex, param);
368
369        ColorModel cm;
370        if (pmi.isMonochrome()) {
371            int[] overlayGroupOffsets = getActiveOverlayGroupOffsets(param);
372            byte[][] overlayData = new byte[overlayGroupOffsets.length][];
373            for (int i = 0; i < overlayGroupOffsets.length; i++) {
374                overlayData[i] = extractOverlay(overlayGroupOffsets[i], raster);
375            }
376            cm = createColorModel(8, DataBuffer.TYPE_BYTE);
377            SampleModel sm = createSampleModel(DataBuffer.TYPE_BYTE, false);
378            raster = applyLUTs(raster, frameIndex, param, sm, 8);
379            for (int i = 0; i < overlayGroupOffsets.length; i++) {
380                applyOverlay(overlayGroupOffsets[i],
381                        raster, frameIndex, param, 8, overlayData[i]);
382            }
383        } else {
384            cm = createColorModel(bitsStored, dataType);
385        }
386        return new BufferedImage(cm, raster , false, null);
387    }
388
389    private byte[] extractOverlay(int gg0000, WritableRaster raster) {
390        Attributes attrs = metadata.getAttributes();
391
392        if (attrs.getInt(Tag.OverlayBitsAllocated | gg0000, 1) == 1)
393            return null;
394
395        int ovlyRows = attrs.getInt(Tag.OverlayRows | gg0000, 0);
396        int ovlyColumns = attrs.getInt(Tag.OverlayColumns | gg0000, 0);
397        int bitPosition = attrs.getInt(Tag.OverlayBitPosition | gg0000, 0);
398
399        int mask = 1<<bitPosition;
400        int length = ovlyRows * ovlyColumns;
401
402        byte[] ovlyData = new byte[(((length+7)>>>3)+1)&(~1)] ;
403        Overlays.extractFromPixeldata(raster, mask, ovlyData, 0, length);
404        return ovlyData;
405    }
406
407    @SuppressWarnings("resource")
408    private ImageInputStreamImpl iisOfFrame(int frameIndex)
409            throws IOException {
410        SegmentedImageInputStream siis = SegmentedImageInputStream.ofFrame(
411                iis, pixeldataFragments, frameIndex, frames);
412        return patchJpegLS != null
413                ? new PatchJPEGLSImageInputStream(siis, patchJpegLS)
414                : siis;
415    }
416
417    private void applyOverlay(int gg0000, WritableRaster raster,
418            int frameIndex, ImageReadParam param, int outBits, byte[] ovlyData) {
419        Attributes ovlyAttrs = metadata.getAttributes();
420        int grayscaleValue = 0xffff;
421        if (param instanceof DicomImageReadParam) {
422            DicomImageReadParam dParam = (DicomImageReadParam) param;
423            Attributes psAttrs = dParam.getPresentationState();
424            if (psAttrs != null) {
425                if (psAttrs.containsValue(Tag.OverlayData | gg0000))
426                    ovlyAttrs = psAttrs;
427                grayscaleValue = Overlays.getRecommendedDisplayGrayscaleValue(
428                        psAttrs, gg0000);
429            } else
430                grayscaleValue = dParam.getOverlayGrayscaleValue();
431        }
432        Overlays.applyOverlay(ovlyData != null ? 0 : frameIndex, raster,
433                ovlyAttrs, gg0000, grayscaleValue >>> (16-outBits), ovlyData);
434    }
435
436    private int[] getActiveOverlayGroupOffsets(ImageReadParam param) {
437        if (param instanceof DicomImageReadParam) {
438            DicomImageReadParam dParam = (DicomImageReadParam) param;
439            Attributes psAttrs = dParam.getPresentationState();
440            if (psAttrs != null)
441                return Overlays.getActiveOverlayGroupOffsets(psAttrs);
442            else
443                return Overlays.getActiveOverlayGroupOffsets(
444                        metadata.getAttributes(),
445                        dParam.getOverlayActivationMask());
446        }
447        return Overlays.getActiveOverlayGroupOffsets(
448                metadata.getAttributes(),
449                0xffff);
450    }
451
452    private WritableRaster applyLUTs(WritableRaster raster,
453            int frameIndex, ImageReadParam param, SampleModel sm, int outBits) {
454         WritableRaster destRaster =
455                sm.getDataType() == raster.getSampleModel().getDataType()
456                        ? raster
457                        : Raster.createWritableRaster(sm, null);
458        Attributes imgAttrs = metadata.getAttributes();
459        StoredValue sv = StoredValue.valueOf(imgAttrs);
460        LookupTableFactory lutParam = new LookupTableFactory(sv);
461        DicomImageReadParam dParam = param instanceof DicomImageReadParam
462                ? (DicomImageReadParam) param
463                : new DicomImageReadParam();
464        Attributes psAttrs = dParam.getPresentationState();
465        if (psAttrs != null) {
466            lutParam.setModalityLUT(psAttrs);
467            lutParam.setVOI(
468                    selectVOILUT(psAttrs,
469                            imgAttrs.getString(Tag.SOPInstanceUID),
470                            frameIndex+1),
471                    0, 0, false);
472            lutParam.setPresentationLUT(psAttrs);
473        } else {
474            Attributes sharedFctGroups = imgAttrs.getNestedDataset(
475                    Tag.SharedFunctionalGroupsSequence);
476            Attributes frameFctGroups = imgAttrs.getNestedDataset(
477                    Tag.PerFrameFunctionalGroupsSequence, frameIndex);
478            lutParam.setModalityLUT(
479                    selectFctGroup(imgAttrs, sharedFctGroups, frameFctGroups,
480                            Tag.PixelValueTransformationSequence));
481            if (dParam.getWindowWidth() != 0) {
482                lutParam.setWindowCenter(dParam.getWindowCenter());
483                lutParam.setWindowWidth(dParam.getWindowWidth());
484            } else
485                lutParam.setVOI(
486                    selectFctGroup(imgAttrs, sharedFctGroups, frameFctGroups,
487                            Tag.FrameVOILUTSequence),
488                    dParam.getWindowIndex(),
489                    dParam.getVOILUTIndex(),
490                    dParam.isPreferWindow());
491            if (dParam.isAutoWindowing())
492                lutParam.autoWindowing(imgAttrs, raster);
493            lutParam.setPresentationLUT(imgAttrs);
494        }
495        LookupTable lut = lutParam.createLUT(outBits);
496        lut.lookup(raster, destRaster);
497        return destRaster;
498    }
499
500    private Attributes selectFctGroup(Attributes imgAttrs,
501            Attributes sharedFctGroups,
502            Attributes frameFctGroups,
503            int tag) {
504        if (frameFctGroups == null) {
505            return imgAttrs;
506        }
507        Attributes group = frameFctGroups.getNestedDataset(tag);
508        if (group == null && sharedFctGroups != null) {
509            group = sharedFctGroups.getNestedDataset(tag);
510        }
511        return group != null ? group : imgAttrs;
512    }
513
514    private Attributes selectVOILUT(Attributes psAttrs, String iuid, int frame) {
515        Sequence voiLUTs = psAttrs.getSequence(Tag.SoftcopyVOILUTSequence);
516        if (voiLUTs != null)
517            for (Attributes voiLUT : voiLUTs) {
518                Sequence refImgs = voiLUT.getSequence(Tag.ReferencedImageSequence);
519                if (refImgs == null || refImgs.isEmpty())
520                    return voiLUT;
521                for (Attributes refImg : refImgs) {
522                    if (iuid.equals(refImg.getString(Tag.ReferencedSOPInstanceUID))) {
523                        int[] refFrames = refImg.getInts(Tag.ReferencedFrameNumber);
524                        if (refFrames == null)
525                            return voiLUT;
526
527                        for (int refFrame : refFrames)
528                            if (refFrame == frame)
529                                return voiLUT;
530                    }
531                }
532            }
533        return null;
534    }
535
536    private void readMetadata() throws IOException {
537        if (metadata != null)
538            return;
539
540        if (iis == null)
541            throw new IllegalStateException("Input not set");
542
543        @SuppressWarnings("resource")
544        DicomInputStream dis = new DicomInputStream(new ImageInputStreamAdapter(iis));
545        dis.setIncludeBulkData(IncludeBulkData.URI);
546        dis.setBulkDataDescriptor(BulkDataDescriptor.PIXELDATA);
547        dis.setURI("java:iis"); // avoid copy of pixeldata to temporary file
548        Attributes fmi = dis.readFileMetaInformation();
549        Attributes ds = dis.readDataset(-1, -1);
550        setMetadata(new DicomMetaData(fmi, ds));
551    }
552
553    private void setMetadata(DicomMetaData metadata) {
554        this.metadata = metadata;
555        this.ds = metadata.getAttributes();
556        Object pixeldata = ds.getValue(Tag.PixelData, pixeldataVR );
557        if (pixeldata != null) {
558            frames = ds.getInt(Tag.NumberOfFrames, 1);
559            width = ds.getInt(Tag.Columns, 0);
560            height = ds.getInt(Tag.Rows, 0);
561            samples = ds.getInt(Tag.SamplesPerPixel, 1);
562            banded = samples > 1 && ds.getInt(Tag.PlanarConfiguration, 0) != 0;
563            bitsAllocated = ds.getInt(Tag.BitsAllocated, 8);
564            bitsStored = ds.getInt(Tag.BitsStored, bitsAllocated);
565            dataType = bitsAllocated <= 8 ? DataBuffer.TYPE_BYTE
566                                          : DataBuffer.TYPE_USHORT;
567            pmi = PhotometricInterpretation.fromString(
568                    ds.getString(Tag.PhotometricInterpretation, "MONOCHROME2"));
569            if (pixeldata instanceof BulkData) {
570                this.frameLength = pmi.frameLength(width, height, samples, bitsAllocated);
571                this.pixelBulkData = (BulkData) pixeldata;
572            } else if (pixeldata instanceof byte[]) {
573                this.frameLength = pmi.frameLength(width, height, samples, bitsAllocated);
574                this.pixeldataBytes = (byte[]) pixeldata;
575            } else { // Fragments
576                Attributes fmi = metadata.getFileMetaInformation();
577                if (fmi == null)
578                    throw new IllegalArgumentException("Missing File Meta Information for Data Set with compressed Pixel Data");
579
580                String tsuid = fmi.getString(Tag.TransferSyntaxUID);
581                ImageReaderParam param =
582                        ImageReaderFactory.getImageReaderParam(tsuid);
583                if (param == null)
584                    throw new UnsupportedOperationException("Unsupported Transfer Syntax: " + tsuid);
585                this.rle = tsuid.equals(UID.RLELossless);
586                this.decompressor = ImageReaderFactory.getImageReader(param);
587                this.patchJpegLS = param.patchJPEGLS;
588                this.pixeldataFragments = (Fragments) pixeldata;
589            }
590        }
591    }
592
593    private SampleModel createSampleModel(int dataType, boolean banded) {
594        return pmi.createSampleModel(dataType, width, height, samples, banded);
595    }
596
597    private ImageTypeSpecifier createImageType(int bits, int dataType, boolean banded) {
598        return new ImageTypeSpecifier(
599                createColorModel(bits, dataType),
600                createSampleModel(dataType, banded));
601    }
602
603    private ColorModel createColorModel(int bits, int dataType) {
604        return pmi.createColorModel(bits, dataType, metadata.getAttributes());
605    }
606
607    private void resetInternalState() {
608        metadata = null;
609        ds = null;
610        pixeldataFile = null;
611        frames = 0;
612        width = 0;
613        height = 0;
614        pixelBulkData = null;
615        frameLength = 0;
616        pixeldataFragments = null;
617        pixeldataBytes = null;
618        if (decompressor != null) {
619            decompressor.dispose();
620            decompressor = null;
621        }
622        patchJpegLS = null;
623        pmi = null;
624    }
625
626    private void checkIndex(int frameIndex) {
627        if (frames == 0)
628            throw new IllegalStateException("Missing Pixel Data");
629
630        if (frameIndex < 0 || frameIndex >= frames)
631            throw new IndexOutOfBoundsException("imageIndex: " + frameIndex);
632    }
633
634    @Override
635    public void dispose() {
636        resetInternalState();
637    }
638
639}