/******************************************************************************* * Copyright (c) 2016 Weasis Team and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Nicolas Roduit - initial API and implementation *******************************************************************************/ package org.weasis.dicom.codec; import java.awt.Dimension; import java.awt.Transparency; import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.ComponentColorModel; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferUShort; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.awt.image.renderable.ParameterBlock; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutput; import java.io.ObjectOutputStream; import java.lang.ref.Reference; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteOrder; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.ImageTypeSpecifier; import javax.imageio.metadata.IIOMetadata; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStreamImpl; import javax.media.jai.JAI; import javax.media.jai.PlanarImage; import javax.media.jai.operator.AndConstDescriptor; import javax.media.jai.operator.NullDescriptor; import org.dcm4che3.data.Attributes; import org.dcm4che3.data.BulkData; import org.dcm4che3.data.Fragments; import org.dcm4che3.data.Tag; import org.dcm4che3.data.UID; import org.dcm4che3.data.VR; import org.dcm4che3.image.Overlays; import org.dcm4che3.image.PhotometricInterpretation; import org.dcm4che3.imageio.codec.ImageReaderFactory; import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLS; import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLSImageInputStream; import org.dcm4che3.imageio.plugins.dcm.DicomImageReadParam; import org.dcm4che3.imageio.plugins.dcm.DicomImageReaderSpi; import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; import org.dcm4che3.imageio.stream.ImageInputStreamAdapter; import org.dcm4che3.imageio.stream.SegmentedInputImageStream; import org.dcm4che3.io.DicomInputStream; import org.dcm4che3.io.DicomInputStream.IncludeBulkData; import org.dcm4che3.io.DicomOutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.weasis.core.api.explorer.model.DataExplorerModel; import org.weasis.core.api.gui.util.AppProperties; import org.weasis.core.api.image.op.RectifySignedShortDataDescriptor; import org.weasis.core.api.image.op.RectifyUShortToShortDataDescriptor; import org.weasis.core.api.image.util.ImageFiler; import org.weasis.core.api.image.util.LayoutUtil; import org.weasis.core.api.media.data.Codec; import org.weasis.core.api.media.data.FileCache; import org.weasis.core.api.media.data.MediaElement; import org.weasis.core.api.media.data.MediaSeries; import org.weasis.core.api.media.data.MediaSeriesGroup; import org.weasis.core.api.media.data.Series; import org.weasis.core.api.media.data.SimpleTagable; import org.weasis.core.api.media.data.SoftHashMap; import org.weasis.core.api.media.data.TagView; import org.weasis.core.api.media.data.TagW; import org.weasis.core.api.service.BundleTools; import org.weasis.core.api.util.FileUtil; import org.weasis.core.api.util.StringUtil; import org.weasis.dicom.codec.TagD.Level; import org.weasis.dicom.codec.display.CornerDisplay; import org.weasis.dicom.codec.display.Modality; import org.weasis.dicom.codec.display.ModalityInfoData; import org.weasis.dicom.codec.display.ModalityView; import org.weasis.dicom.codec.geometry.ImageOrientation; import org.weasis.dicom.codec.utils.DicomImageUtils; import org.weasis.dicom.codec.utils.DicomMediaUtils; import org.weasis.dicom.codec.utils.OverlayUtils; import com.sun.media.imageio.stream.RawImageInputStream; import com.sun.media.imageioimpl.common.SignedDataImageParam; import com.sun.media.jai.util.ImageUtil; public class DicomMediaIO extends ImageReader implements DcmMediaReader { private static final Logger LOGGER = LoggerFactory.getLogger(DicomMediaIO.class); public static final File DICOM_EXPORT_DIR = AppProperties.buildAccessibleTempDirectory("dicom"); //$NON-NLS-1$ public static final String MIMETYPE = "application/dicom"; //$NON-NLS-1$ public static final String IMAGE_MIMETYPE = "image/dicom"; //$NON-NLS-1$ public static final String SERIES_VIDEO_MIMETYPE = "video/dicom"; //$NON-NLS-1$ public static final String SERIES_MIMETYPE = "series/dicom"; //$NON-NLS-1$ public static final String SERIES_PR_MIMETYPE = "pr/dicom"; //$NON-NLS-1$ public static final String SERIES_KO_MIMETYPE = "ko/dicom"; //$NON-NLS-1$ public static final String SERIES_ENCAP_DOC_MIMETYPE = "encap/dicom"; //$NON-NLS-1$ public static final String UNREADABLE = "unreadable/dicom"; //$NON-NLS-1$ public static final String SERIES_XDSI = "xds-i/dicom"; //$NON-NLS-1$ private static final AtomicInteger instanceID = new AtomicInteger(1); public static final TagManager tagManager = new TagManager(); static { // PatientPseudoUID is the unique identifying tag for this patient group // -------- Mandatory Tags -------- tagManager.addTag(Tag.PatientID, Level.PATIENT); tagManager.addTag(Tag.PatientName, Level.PATIENT); // -------- End of Mandatory Tags -------- tagManager.addTag(Tag.PatientBirthDate, Level.PATIENT); tagManager.addTag(Tag.PatientBirthTime, Level.PATIENT); tagManager.addTag(Tag.PatientAge, Level.PATIENT); tagManager.addTag(Tag.PatientSex, Level.PATIENT); tagManager.addTag(Tag.IssuerOfPatientID, Level.PATIENT); tagManager.addTag(Tag.PatientWeight, Level.PATIENT); tagManager.addTag(Tag.PatientComments, Level.PATIENT); // StudyInstanceUID is the unique identifying tag for this study group tagManager.addTag(Tag.StudyID, Level.STUDY); tagManager.addTag(Tag.StudyDate, Level.STUDY); tagManager.addTag(Tag.StudyTime, Level.STUDY); tagManager.addTag(Tag.StudyDescription, Level.STUDY); tagManager.addTag(Tag.StudyComments, Level.STUDY); tagManager.addTag(Tag.AccessionNumber, Level.STUDY); tagManager.addTag(Tag.ModalitiesInStudy, Level.STUDY); // not required tagManager.addTag(Tag.NumberOfStudyRelatedInstances, Level.STUDY); // not required tagManager.addTag(Tag.NumberOfStudyRelatedSeries, Level.STUDY); // not required // SubseriesInstanceUID is the unique identifying tag for this series group // -------- Mandatory Tags -------- tagManager.addTag(Tag.SeriesInstanceUID, Level.SERIES); tagManager.addTag(Tag.Modality, Level.SERIES); // -------- End of Mandatory Tags -------- tagManager.addTag(Tag.SeriesDescription, Level.SERIES); tagManager.addTag(Tag.RetrieveAETitle, Level.SERIES); // not required tagManager.addTag(Tag.ReferringPhysicianName, Level.SERIES); tagManager.addTag(Tag.InstitutionName, Level.SERIES); tagManager.addTag(Tag.InstitutionalDepartmentName, Level.SERIES); tagManager.addTag(Tag.StationName, Level.SERIES); tagManager.addTag(Tag.Manufacturer, Level.SERIES); tagManager.addTag(Tag.ManufacturerModelName, Level.SERIES); tagManager.addTag(Tag.SeriesNumber, Level.SERIES); tagManager.addTag(Tag.NumberOfFrames, Level.SERIES); tagManager.addTag(Tag.SeriesDate, Level.SERIES); tagManager.addTag(Tag.SeriesTime, Level.SERIES); tagManager.addTag(Tag.PerformedProcedureStepStartDate, Level.SERIES); // not // required tagManager.addTag(Tag.PerformedProcedureStepStartTime, Level.SERIES); // not // required // Should be in image // C.7.6.5 Cine Module // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.5.html tagManager.addTag(Tag.PreferredPlaybackSequencing, Level.SERIES); tagManager.addTag(Tag.CineRate, Level.SERIES); tagManager.addTag(Tag.RecommendedDisplayFrameRate, Level.SERIES); tagManager.addTag(Tag.KVP, Level.SERIES); tagManager.addTag(Tag.BodyPartExamined, Level.SERIES); tagManager.addTag(Tag.FrameOfReferenceUID, Level.SERIES); tagManager.addTag(Tag.NumberOfSeriesRelatedInstances, Level.SERIES); tagManager.addTag(Tag.Laterality, Level.SERIES); // SOPInstanceUID is the unique identifying tag of a DICOM object // -------- Mandatory Tags -------- // Tags for identifying group (Patient, Study, Series) tagManager.addTag(Tag.PatientID, Level.INSTANCE); tagManager.addTag(Tag.PatientName, Level.INSTANCE); tagManager.addTag(Tag.PatientBirthDate, Level.INSTANCE); tagManager.addTag(Tag.IssuerOfPatientID, Level.INSTANCE); tagManager.addTag(Tag.StudyInstanceUID, Level.INSTANCE); tagManager.addTag(Tag.SeriesInstanceUID, Level.INSTANCE); tagManager.addTag(Tag.Modality, Level.INSTANCE); // -------- End of Mandatory Tags -------- tagManager.addTag(Tag.GantryDetectorTilt, Level.INSTANCE); tagManager.addTag(Tag.PatientOrientation, Level.INSTANCE); tagManager.addTag(Tag.SliceLocation, Level.INSTANCE); tagManager.addTag(Tag.SliceThickness, Level.INSTANCE); tagManager.addTag(Tag.AcquisitionDate, Level.INSTANCE); tagManager.addTag(Tag.AcquisitionTime, Level.INSTANCE); tagManager.addTag(Tag.ContentDate, Level.INSTANCE); tagManager.addTag(Tag.ContentTime, Level.INSTANCE); tagManager.addTag(Tag.DiffusionBValue, Level.INSTANCE); tagManager.addTag(Tag.MIMETypeOfEncapsulatedDocument, Level.INSTANCE); tagManager.addTag(Tag.PixelDataProviderURL, Level.INSTANCE); for (Entry<Modality, ModalityInfoData> entry : ModalityView.getModalityViewEntries()) { readTagsInModalityView(entry.getValue().getCornerInfo(CornerDisplay.TOP_LEFT).getInfos()); readTagsInModalityView(entry.getValue().getCornerInfo(CornerDisplay.TOP_RIGHT).getInfos()); readTagsInModalityView(entry.getValue().getCornerInfo(CornerDisplay.BOTTOM_RIGHT).getInfos()); } // TODO init with a profile DicomMediaUtils.enableAnonymizationProfile(true); } public static final Map<String, DicomSpecialElementFactory> DCM_ELEMENT_FACTORIES = new HashMap<>(); static { /* * DICOM PR and KO are not displayed with a special viewer but are transversally managed objects. So they are * not registered from a viewer. */ DCM_ELEMENT_FACTORIES.put("PR", new DicomSpecialElementFactory() { //$NON-NLS-1$ @Override public String getSeriesMimeType() { return SERIES_PR_MIMETYPE; } @Override public String[] getModalities() { return new String[] { "PR" }; //$NON-NLS-1$ } @Override public DicomSpecialElement buildDicomSpecialElement(DicomMediaIO mediaIO) { return new PRSpecialElement(mediaIO); } }); DCM_ELEMENT_FACTORIES.put("KO", new DicomSpecialElementFactory() { //$NON-NLS-1$ @Override public String getSeriesMimeType() { return SERIES_KO_MIMETYPE; } @Override public String[] getModalities() { return new String[] { "KO" }; //$NON-NLS-1$ } @Override public DicomSpecialElement buildDicomSpecialElement(DicomMediaIO mediaIO) { if (RejectedKOSpecialElement.isRejectionKOS(mediaIO)) { return new RejectedKOSpecialElement(mediaIO); } return new KOSpecialElement(mediaIO); } }); } static final DicomImageReaderSpi dicomImageReaderSpi = new DicomImageReaderSpi(); private static final SoftHashMap<DicomMediaIO, DicomMetaData> HEADER_CACHE = new SoftHashMap<DicomMediaIO, DicomMetaData>() { @Override public void removeElement(Reference<? extends DicomMetaData> soft) { DicomMediaIO key = reverseLookup.remove(soft); if (key != null) { hash.remove(key); key.reset(); } } }; // The above softReference HEADER_CACHE shall be used instead of the following dcmMetadata variable to get access to // the current DicomObject unless it's virtual and then URI doesn't exit. This case appends when the dcmMetadata is // created within the application and is given to the ImageReader constructor private DicomMetaData dcmMetadata = null; private BulkData pixeldata; private final VR.Holder pixeldataVR = new VR.Holder(); private Fragments pixeldataFragments; private ImageReader decompressor; private PatchJPEGLS patchJpegLS; private int frameLength; private PhotometricInterpretation pmi; private URI uri; private int numberOfFrame; private final Map<TagW, Object> tags; private volatile MediaElement[] image = null; private volatile String mimeType; private final ArrayList<Integer> fragmentsPositions = new ArrayList<>(); private volatile ImageInputStream iis; private DicomInputStream dis; private int dataType = 0; private boolean hasPixel = false; private boolean banded = false; private int bitsStored; private int bitsAllocated; private int highBit; /** * Store the transfer syntax locally in case it gets modified to re-write the image */ private String tsuid; /** Used to indicate whether or not to skip large private dicom elements. */ private boolean skipLargePrivate = true; private volatile boolean readingHeader = false; private volatile boolean readingImage = false; private final FileCache fileCache; public DicomMediaIO(URI uri) { super(dicomImageReaderSpi); this.uri = Objects.requireNonNull(uri); this.numberOfFrame = 0; this.tags = new HashMap<>(); this.mimeType = MIMETYPE; this.fileCache = new FileCache(this); } public DicomMediaIO(File source) { this(Objects.requireNonNull(source).toURI()); } public DicomMediaIO(Path path) throws URISyntaxException { this(Objects.requireNonNull(path).toUri()); } public DicomMediaIO(Attributes dcmItems) throws URISyntaxException { this(new URI("data:" + Objects.requireNonNull(dcmItems).getString(Tag.SOPInstanceUID))); //$NON-NLS-1$ this.dcmMetadata = new DicomMetaData(null, Objects.requireNonNull(dcmItems)); } private static void readTagsInModalityView(TagView[] views) { for (TagView tagView : views) { if (tagView != null) { for (TagW tag : tagView.getTag()) { if (tag != null) { if (!DicomMediaIO.tagManager.contains(tag, Level.PATIENT) && !DicomMediaIO.tagManager.contains(tag, Level.STUDY) && !DicomMediaIO.tagManager.contains(tag, Level.SERIES)) { DicomMediaIO.tagManager.addTag(tag, Level.INSTANCE); } } } } } } @Override public synchronized void replaceURI(URI uri) { if (!Objects.equals(this.uri, Objects.requireNonNull(uri))) { this.uri = uri; reset(); } } /** * * @return true when the DICOM Object has no source file (only in memory) */ public boolean isEditableDicom() { return dcmMetadata != null && "data".equals(uri.getScheme()); //$NON-NLS-1$ } public boolean isReadableDicom() { if (UNREADABLE.equals(mimeType)) { // Return true only to display the error message in the view return true; } if ("data".equals(uri.getScheme()) && dcmMetadata == null) { //$NON-NLS-1$ return false; } if (tags.size() == 0) { try { DicomMetaData md = readMetaData(false); Attributes fmi = md.getFileMetaInformation(); Attributes header = md.getAttributes(); // Exclude DICOMDIR String mediaStorageSOPClassUID = fmi == null ? null : fmi.getString(Tag.MediaStorageSOPClassUID); if ("1.2.840.10008.1.3.10".equals(mediaStorageSOPClassUID)) { //$NON-NLS-1$ mimeType = UNREADABLE; close(); return false; } if (hasPixel) { String ts = fmi == null ? null : fmi.getString(Tag.TransferSyntaxUID); if (ts != null && ts.startsWith("1.2.840.10008.1.2.4.10")) { //$NON-NLS-1$ $NON-NLS-2$ // MPEG2 MP@ML 1.2.840.10008.1.2.4.100 // MEPG2 MP@HL 1.2.840.10008.1.2.4.101 // MPEG4 AVC/H.264 1.2.840.10008.1.2.4.102 // MPEG4 AVC/H.264 BD 1.2.840.10008.1.2.4.103 mimeType = SERIES_VIDEO_MIMETYPE; } else { mimeType = IMAGE_MIMETYPE; } } else { boolean special = setDicomSpecialType(header); if (!special) { // Not supported DICOM file mimeType = UNREADABLE; close(); return false; } } writeInstanceTags(fmi, header); } catch (Exception | OutOfMemoryError e) { mimeType = UNREADABLE; LOGGER.error("Cannot read DICOM:", e); //$NON-NLS-1$ close(); return false; } } return true; } private ImageReader initRawImageReader() { long[] frameOffsets = new long[numberOfFrame]; frameOffsets[0] = pixeldata.offset(); for (int i = 1; i < frameOffsets.length; i++) { frameOffsets[i] = frameOffsets[i - 1] + frameLength; } Dimension[] imageDimensions = new Dimension[numberOfFrame]; int width = TagD.getTagValue(this, Tag.Columns, Integer.class); int height = TagD.getTagValue(this, Tag.Rows, Integer.class); Arrays.fill(imageDimensions, new Dimension(width, height)); ColorModel cmodel = createColorModel(bitsStored, dataType); SampleModel smodel; if (pmi.isSubSambled()) { // Cannot handle tiles with subsampled model smodel = createSampleModel(dataType, banded); } else { if (width >= 1024 || height >= 1024) { width = Math.min(width, ImageFiler.TILESIZE); height = Math.min(height, ImageFiler.TILESIZE); } smodel = pmi.createSampleModel(dataType, width, height, TagD.getTagValue(this, Tag.SamplesPerPixel, Integer.class), banded); } ImageReader reader = ImageIO.getImageReadersByFormatName("RAW").next(); //$NON-NLS-1$ if (reader == null) { throw new IllegalStateException("Cannot get RAW image reader"); //$NON-NLS-1$ } RawImageInputStream riis = new RawImageInputStream(iis, new ImageTypeSpecifier(cmodel, smodel), frameOffsets, imageDimensions); // endianess is already in iis? // riis.setByteOrder(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); reader.setInput(riis); return reader; } private boolean setDicomSpecialType(Attributes header) { String modality = header.getString(Tag.Modality); if (modality != null) { String encap = header.getString(Tag.MIMETypeOfEncapsulatedDocument); DicomSpecialElementFactory factory = DCM_ELEMENT_FACTORIES.get(modality); if (factory != null) { mimeType = factory.getSeriesMimeType(); // Can be not null for instance by ECG with encapsulated pdf if (encap == null) { return true; } } if (encap != null) { mimeType = SERIES_ENCAP_DOC_MIMETYPE; return true; } } return false; } public String getMimeType() { return mimeType; } @Override public Object getTagValue(TagW tag) { return tag == null ? null : tags.get(tag); } @Override public void setTag(TagW tag, Object value) { DicomMediaUtils.setTag(tags, tag, value); } @Override public void setTagNoNull(TagW tag, Object value) { if (value != null) { setTag(tag, value); } } @Override public boolean containTagKey(TagW tag) { return tags.containsKey(tag); } @Override public Iterator<Entry<TagW, Object>> getTagEntrySetIterator() { return tags.entrySet().iterator(); } @Override public void writeMetaData(MediaSeriesGroup group) { if (group == null) { return; } // Get the dicom header Attributes header = getDicomObject(); DicomMediaUtils.writeMetaData(group, header); // Series Group if (TagW.SubseriesInstanceUID.equals(group.getTagID())) { // Information for series ToolTips group.setTagNoNull(TagD.get(Tag.PatientName), getTagValue(TagD.get(Tag.PatientName))); group.setTagNoNull(TagD.get(Tag.StudyDescription), header.getString(Tag.StudyDescription)); } } private void writeInstanceTags(Attributes fmi, Attributes header) { if (tags.size() > 0 || header == null) { return; } tagManager.readTags(Level.INSTANCE, header, this); // -------- Mandatory Tags -------- // Tags for identifying group (Patient, Study, Series) // Global Identifier for the patient. setTag(TagW.PatientPseudoUID, DicomMediaUtils.buildPatientPseudoUID(this)); Integer instNb = DicomMediaUtils.getIntegerFromDicomElement(header, Tag.InstanceNumber, instanceID.incrementAndGet()); setTag(TagD.get(Tag.InstanceNumber), instNb); setTag(TagD.get(Tag.SOPInstanceUID), header.getString(Tag.SOPInstanceUID, String.valueOf(instNb))); if (fmi != null) { setTagNoNull(TagD.get(Tag.TransferSyntaxUID), fmi.getString(Tag.TransferSyntaxUID)); } // -------- End of Mandatory Tags -------- writeImageValues(header); writeSharedFunctionalGroupsSequence(header); DicomMediaUtils.writePerFrameFunctionalGroupsSequence(this, header, 0); boolean pr = SERIES_PR_MIMETYPE.equals(mimeType); boolean ko = SERIES_KO_MIMETYPE.equals(mimeType); if (pr) { // Set the series list for applying the PR DicomMediaUtils.buildSeriesReferences(this, header); DicomMediaUtils.readPRLUTsModule(header, this); setTagNoNull(TagW.HasOverlay, DicomMediaUtils.hasOverlay(header)); } if (pr || ko) { // Set other required fields TagW[] tagIDs = TagD.getTagFromIDs(Tag.SeriesDescription, Tag.SeriesDate, Tag.SeriesTime, Tag.SeriesNumber); for (TagW tag : tagIDs) { tag.readValue(header, this); } } DicomMediaUtils.computeSlicePositionVector(this); DicomMediaUtils.setShutter(this, header); DicomMediaUtils.computeSUVFactor(header, this, 0); } private void writeSharedFunctionalGroupsSequence(Attributes header) { if (header != null) { DicomMediaUtils.writeFunctionalGroupsSequence(this, header.getNestedDataset(Tag.SharedFunctionalGroupsSequence)); } } private void writeImageValues(Attributes header) { if (header != null && hasPixel) { TagD.get(Tag.ImagePositionPatient).readValue(header, this); TagD.get(Tag.ImageOrientationPatient).readValue(header, this); setTagNoNull(TagW.ImageOrientationPlane, ImageOrientation.makeImageOrientationLabelFromImageOrientationPatient( TagD.getTagValue(this, Tag.ImageOrientationPatient, double[].class))); bitsAllocated = DicomMediaUtils.getIntegerFromDicomElement(header, Tag.BitsAllocated, 8); bitsStored = DicomMediaUtils.getIntegerFromDicomElement(header, Tag.BitsStored, bitsAllocated); highBit = DicomMediaUtils.getIntegerFromDicomElement(header, Tag.HighBit, bitsStored - 1); if (highBit >= bitsAllocated) { highBit = bitsStored - 1; } int pixelRepresentation = DicomMediaUtils.getIntegerFromDicomElement(header, Tag.PixelRepresentation, 0); setTagNoNull(TagD.get(Tag.BitsAllocated), bitsAllocated); setTagNoNull(TagD.get(Tag.BitsStored), bitsStored); setTagNoNull(TagD.get(Tag.PixelRepresentation), pixelRepresentation); TagD.get(Tag.PixelSpacing).readValue(header, this); TagD.get(Tag.PixelAspectRatio).readValue(header, this); TagD.get(Tag.PixelSpacingCalibrationDescription).readValue(header, this); TagD.get(Tag.ImagerPixelSpacing).readValue(header, this); TagD.get(Tag.NominalScannedPixelSpacing).readValue(header, this); DicomMediaUtils.applyModalityLutModule(header, this, null); TagD.get(Tag.PixelIntensityRelationship).readValue(header, this); DicomMediaUtils.applyVoiLutModule(header, header, this, null); TagD.get(Tag.Units).readValue(header, this); TagD.get(Tag.NumberOfFrames).readValue(header, this); setTagNoNull(TagW.HasOverlay, DicomMediaUtils.hasOverlay(header)); int samplesPerPixel = DicomMediaUtils.getIntegerFromDicomElement(header, Tag.SamplesPerPixel, 1); setTag(TagD.get(Tag.SamplesPerPixel), samplesPerPixel); banded = samplesPerPixel > 1 && DicomMediaUtils.getIntegerFromDicomElement(header, Tag.PlanarConfiguration, 0) != 0; dataType = bitsAllocated <= 8 ? DataBuffer.TYPE_BYTE : pixelRepresentation != 0 ? DataBuffer.TYPE_SHORT : DataBuffer.TYPE_USHORT; if (bitsAllocated == 32 && samplesPerPixel == 1) { dataType = header.getValue(Tag.FloatPixelData, pixeldataVR) == null ? DataBuffer.TYPE_INT : DataBuffer.TYPE_FLOAT; } else if (bitsAllocated == 64 && samplesPerPixel == 1) { dataType = DataBuffer.TYPE_DOUBLE; } String photometricInterpretation = header.getString(Tag.PhotometricInterpretation, "MONOCHROME2"); //$NON-NLS-1$ pmi = PhotometricInterpretation.fromString(photometricInterpretation); TagD.get(Tag.PresentationLUTShape).readValue(header, this); setTag(TagD.get(Tag.PhotometricInterpretation), photometricInterpretation); setTag(TagW.MonoChrome, samplesPerPixel == 1 && !"PALETTE COLOR".equalsIgnoreCase(photometricInterpretation)); //$NON-NLS-1$ setTag(TagD.get(Tag.Rows), DicomMediaUtils.getIntegerFromDicomElement(header, Tag.Rows, 0)); setTag(TagD.get(Tag.Columns), DicomMediaUtils.getIntegerFromDicomElement(header, Tag.Columns, 0)); setTagNoNull(TagD.get(Tag.PixelPaddingValue), DicomMediaUtils.getIntPixelValue(header, Tag.PixelPaddingValue, pixelRepresentation != 0, bitsStored)); setTagNoNull(TagD.get(Tag.PixelPaddingRangeLimit), DicomMediaUtils.getIntPixelValue(header, Tag.PixelPaddingRangeLimit, pixelRepresentation != 0, bitsStored)); /* * * @see <a href= * "http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.html#sect_C.7.6.1.1.5" >C * .7.6.1.1.5 Lossy Image Compression</a> */ setTagNoNull(TagD.get(Tag.LossyImageCompression), header.getString(Tag.LossyImageCompression, header.getString(Tag.LossyImageCompressionRetired))); TagD.get(Tag.LossyImageCompressionRatio).readValue(header, this); TagD.get(Tag.LossyImageCompressionMethod).readValue(header, this); TagD.get(Tag.DerivationDescription).readValue(header, this); /* * * For overlays encoded in Overlay Data Element (60xx,3000), Overlay Bits Allocated (60xx,0100) is always 1 * and Overlay Bit Position (60xx,0102) is always 0. * * @see - Dicom Standard 2011 - PS 3.5 ยง 8.1.2 Overlay data encoding of related data elements */ if (bitsStored < bitsAllocated && dataType >= DataBuffer.TYPE_BYTE && dataType < DataBuffer.TYPE_INT && Overlays.getEmbeddedOverlayGroupOffsets(header).length > 0) { int high = highBit + 1; int val = (1 << high) - 1; if (high > bitsStored) { val -= (1 << (high - bitsStored)) - 1; } /* * Set to 0 all bits upper than highBit and if lower than high-bitsStored (=> all bits outside * bitStored) */ setTagNoNull(TagW.OverlayBitMask, val); if (high > bitsStored) { // Combine to the slope value Double slopeVal = TagD.getTagValue(this, Tag.RescaleSlope, Double.class); if (slopeVal == null) { slopeVal = 1.0; // Set valid modality LUT values Double ri = TagD.getTagValue(this, Tag.RescaleIntercept, Double.class); String rt = TagD.getTagValue(this, Tag.RescaleType, String.class); setTag(TagD.get(Tag.RescaleIntercept), ri == null ? 0.0 : ri); setTag(TagD.get(Tag.RescaleType), rt == null ? "US" : rt); //$NON-NLS-1$ } // Divide pixel value by (2 ^ rightBit) => remove right bits slopeVal /= 1 << (high - bitsStored); setTag(TagD.get(Tag.RescaleSlope), slopeVal); } } } } public boolean containTag(int id) { for (Iterator<TagW> it = tags.keySet().iterator(); it.hasNext();) { if (it.next().getId() == id) { return true; } } return false; } @Override public URI getUri() { return uri; } @Override public FileCache getFileCache() { return fileCache; } @Override public boolean buildFile(File output) { // When object is in memory, write it if (isEditableDicom()) { Attributes dcm = getDicomObject(); if (dcm != null) { try (DicomOutputStream out = new DicomOutputStream(output)) { out.writeDataset(dcm.createFileMetaInformation(UID.ImplicitVRLittleEndian), dcm); return true; } catch (IOException e) { LOGGER.error("Cannot write dicom file", e); //$NON-NLS-1$ } } } return false; } @Override public PlanarImage getImageFragment(MediaElement media) throws Exception { if (media != null && media.getKey() instanceof Integer && isReadableDicom()) { int frame = (Integer) media.getKey(); if (frame >= 0 && frame < numberOfFrame && hasPixel) { // read as tiled rendered image LOGGER.debug("Start reading dicom image frame: {} sopUID: {}", //$NON-NLS-1$ frame, TagD.getTagValue(this, Tag.SOPInstanceUID)); return getValidImage(readAsRenderedImage(frame, null), media); } } return null; } private PlanarImage getValidImage(RenderedImage buffer, MediaElement media) { PlanarImage img = null; if (buffer != null) { // Bug fix: CLibImageReader and J2KImageReaderCodecLib (imageio libs) do not handle negative values // for short data. They convert signed short to unsigned short. if (dataType == DataBuffer.TYPE_SHORT && buffer.getSampleModel().getDataType() == DataBuffer.TYPE_USHORT) { img = RectifyUShortToShortDataDescriptor.create(buffer, LayoutUtil.createTiledLayoutHints(buffer)); } else if (ImageUtil.isBinary(buffer.getSampleModel())) { ParameterBlock pb = new ParameterBlock(); pb.addSource(buffer); // Tile size are set in this operation img = JAI.create("formatbinary", pb, null); //$NON-NLS-1$ } else if (buffer.getTileWidth() != ImageFiler.TILESIZE || buffer.getTileHeight() != ImageFiler.TILESIZE) { img = ImageFiler.tileImage(buffer); } else { img = NullDescriptor.create(buffer, LayoutUtil.createTiledLayoutHints(buffer)); } /* * Handle overlay in pixel data: extract the overlay, serialize it in a file and set all values to O in the * pixel data. */ Integer overlayBitMask = (Integer) getTagValue(TagW.OverlayBitMask); if (overlayBitMask != null) { if (media.getTagValue(TagW.OverlayBurninDataPath) == null) { // Serialize overlay (from pixel data) Attributes ds = getDicomObject(); int[] embeddedOverlayGroupOffsets = Overlays.getEmbeddedOverlayGroupOffsets(ds); if (embeddedOverlayGroupOffsets.length > 0) { FileOutputStream fileOut = null; ObjectOutput objOut = null; try { byte[][] overlayData = new byte[embeddedOverlayGroupOffsets.length][]; Raster raster = buffer.getData(); for (int i = 0; i < embeddedOverlayGroupOffsets.length; i++) { overlayData[i] = OverlayUtils.extractOverlay(embeddedOverlayGroupOffsets[i], raster, ds); } File file = File.createTempFile("ovly_", "", AppProperties.FILE_CACHE_DIR); //$NON-NLS-1$ //$NON-NLS-2$ fileOut = new FileOutputStream(file); objOut = new ObjectOutputStream(fileOut); objOut.writeObject(overlayData); media.setTag(TagW.OverlayBurninDataPath, file.getPath()); } catch (Exception e) { LOGGER.error("Cannot serialize overlay", e); //$NON-NLS-1$ } finally { FileUtil.safeClose(objOut); FileUtil.safeClose(fileOut); } } } // Set to 0 all bits outside bitStored img = AndConstDescriptor.create(img, new int[] { overlayBitMask }, null); } img = DicomImageUtils.getRGBImageFromPaletteColorModel(img, getDicomObject()); } return img; } private MediaElement getSingleImage() { MediaElement[] elements = getMediaElement(); if (elements != null && elements.length > 0) { return elements[0]; } return null; } @Override public MediaElement getPreview() { return getSingleImage(); } @Override public boolean delegate(DataExplorerModel explorerModel) { return false; } @Override public MediaElement[] getMediaElement() { if (image == null && isReadableDicom()) { if (SERIES_VIDEO_MIMETYPE.equals(mimeType)) { image = new MediaElement[] { new DicomVideoElement(this, null) }; } else if (SERIES_ENCAP_DOC_MIMETYPE.equals(mimeType)) { image = new MediaElement[] { new DicomEncapDocElement(this, null) }; } else { if (numberOfFrame > 0) { image = new MediaElement[numberOfFrame]; for (int i = 0; i < image.length; i++) { image[i] = new DicomImageElement(this, i); } if (numberOfFrame > 1) { // IF enhanced DICOM, instance number can be overridden later // IF simple Multiframe instance number is necessary for (int i = 0; i < image.length; i++) { image[i].setTag(TagD.get(Tag.InstanceNumber), i + 1); } } } else { String modality = TagD.getTagValue(this, Tag.Modality, String.class); if (modality != null) { DicomSpecialElementFactory factory = DCM_ELEMENT_FACTORIES.get(modality); if (factory != null) { image = new MediaElement[1]; image[0] = factory.buildDicomSpecialElement(this); } } if (image == null) { // Corrupted image => should have one frame image = new MediaElement[0]; } } } } return image; } @Override public MediaSeries<MediaElement> getMediaSeries() { Series<MediaElement> series = null; if (isReadableDicom()) { String seriesUID = TagD.getTagValue(this, Tag.SeriesInstanceUID, String.class); series = buildSeries(seriesUID); writeMetaData(series); // no need to apply splitting rules // also no model MediaElement[] elements = getMediaElement(); if (elements != null) { for (MediaElement media : elements) { series.addMedia(media); } } } return series; } @Override public int getMediaElementNumber() { return numberOfFrame; } @Override public String getMediaFragmentMimeType() { return mimeType; } @Override public Map<TagW, Object> getMediaFragmentTags(Object key) { if (key instanceof Integer) { if ((Integer) key > 0) { // Clone the shared tag Map<TagW, Object> tagList = new HashMap<>(tags); SimpleTagable tagable = new SimpleTagable(tagList); if (DicomMediaUtils.writePerFrameFunctionalGroupsSequence(tagable, getDicomObject(), (Integer) key)) { DicomMediaUtils.computeSlicePositionVector(tagable); } return tagList; } } return tags; } @Override public void close() { dispose(); } @Override public Codec getCodec() { return BundleTools.getCodec(DicomMediaIO.MIMETYPE, DicomCodec.NAME); } @Override public String[] getReaderDescription() { String[] desc = new String[3]; desc[0] = "DICOM Codec: " + DicomCodec.NAME; //$NON-NLS-1$ if (decompressor != null) { desc[1] = "Image Reader Class: " + decompressor.getClass().getName(); //$NON-NLS-1$ try { desc[2] = "Image Format: " + decompressor.getFormatName(); //$NON-NLS-1$ } catch (IOException e) { desc[2] = "Image Format: unknown"; //$NON-NLS-1$ } } if (desc[1] == null) { String ts = tsuid; if (ts == null) { ts = "unknown"; //$NON-NLS-1$ } desc[1] = Messages.getString("DicomMediaIO.msg_no_reader") + StringUtil.COLON_AND_SPACE + ts; //$NON-NLS-1$ } return desc; } public Series<MediaElement> buildSeries(String seriesUID) { Series<? extends MediaElement> series; if (IMAGE_MIMETYPE.equals(mimeType)) { series = new DicomSeries(seriesUID); } else if (SERIES_VIDEO_MIMETYPE.equals(mimeType)) { series = new DicomVideoSeries(seriesUID); } else if (SERIES_ENCAP_DOC_MIMETYPE.equals(mimeType)) { series = new DicomEncapDocSeries(seriesUID); } else { series = new DicomSeries(seriesUID); } return (Series<MediaElement>) series; } @Override public int getHeight(int frameIndex) throws IOException { checkIndex(frameIndex); return TagD.getTagValue(this, Tag.Rows, Integer.class); } @Override public int getWidth(int frameIndex) throws IOException { checkIndex(frameIndex); return TagD.getTagValue(this, Tag.Columns, Integer.class); } @Override public ImageTypeSpecifier getRawImageType(int frameIndex) throws IOException { readMetaData(false); checkIndex(frameIndex); if (decompressor == null) { createImageType(bitsStored, dataType, banded); } if (isRLELossless()) { createImageType(bitsStored, dataType, true); } decompressor.setInput(iisOfFrame(0)); return decompressor.getRawImageType(0); } @Override public Iterator<ImageTypeSpecifier> getImageTypes(int frameIndex) throws IOException { readMetaData(true); checkIndex(frameIndex); ImageTypeSpecifier imageType; if (pmi.isMonochrome()) { imageType = createImageType(8, DataBuffer.TYPE_BYTE, false); } else if (decompressor == null) { imageType = createImageType(bitsStored, dataType, banded); } else if (isRLELossless()) { imageType = createImageType(bitsStored, dataType, true); } else { decompressor.setInput(iisOfFrame(0)); return decompressor.getImageTypes(0); } return Collections.singletonList(imageType).iterator(); } private boolean isRLELossless() { return dis == null ? false : dis.getTransferSyntax().equals(UID.RLELossless); } private ImageInputStreamImpl iisOfFrame(int frameIndex) throws IOException { // Extract compressed file // if (!fileCache.isElementInMemory()) { // String extension = "." + Optional.ofNullable(decompressor).map(d -> // d.getOriginatingProvider().getFileSuffixes()[0]).orElse("raw"); // FileUtil.writeFile(buildSegmentedImageInputStream(frameIndex), // new FileOutputStream(new File(AppProperties.FILE_CACHE_DIR, // fileCache.getFinalFile().getName() + "-" + frameIndex + extension))); // } org.dcm4che3.imageio.stream.SegmentedInputImageStream siis = buildSegmentedImageInputStream(frameIndex); return patchJpegLS != null ? new PatchJPEGLSImageInputStream(siis, patchJpegLS) : siis; } private SegmentedInputImageStream buildSegmentedImageInputStream(int frameIndex) throws IOException { int nbFragments = pixeldataFragments.size(); long[] offsets; int[] length; if (numberOfFrame >= nbFragments - 1) { // nbFrames > nbFragments should never happen offsets = new long[1]; length = new int[offsets.length]; int index = frameIndex < nbFragments - 1 ? frameIndex + 1 : nbFragments - 1; BulkData bulkData = (BulkData) pixeldataFragments.get(index); offsets[0] = bulkData.offset(); length[0] = bulkData.length(); } else { if (numberOfFrame == 1) { offsets = new long[nbFragments - 1]; length = new int[offsets.length]; for (int i = 0; i < length.length; i++) { BulkData bulkData = (BulkData) pixeldataFragments.get(i + frameIndex + 1); offsets[i] = bulkData.offset(); length[i] = bulkData.length(); } } else { // Multi-frames where each frames can have multiple fragments. if (fragmentsPositions.isEmpty()) { if (decompressor == null) { throw new IOException("no decompressor!"); //$NON-NLS-1$ } for (int i = 1; i < nbFragments; i++) { BulkData bulkData = (BulkData) pixeldataFragments.get(i); ImageReaderSpi provider = decompressor.getOriginatingProvider(); if (provider.canDecodeInput(new org.dcm4che3.imageio.stream.SegmentedInputImageStream(iis, new long[] { bulkData.offset() }, new int[] { bulkData.length() }))) { fragmentsPositions.add(i); } } } if (fragmentsPositions.size() == numberOfFrame) { int start = fragmentsPositions.get(frameIndex); int end = (frameIndex + 1) >= fragmentsPositions.size() ? nbFragments : fragmentsPositions.get(frameIndex + 1); offsets = new long[end - start]; length = new int[offsets.length]; for (int i = 0; i < offsets.length; i++) { BulkData bulkData = (BulkData) pixeldataFragments.get(start + i); offsets[i] = bulkData.offset(); length[i] = bulkData.length(); } } else { throw new IOException("Cannot match all the fragments to all the frames!"); //$NON-NLS-1$ } } } return new org.dcm4che3.imageio.stream.SegmentedInputImageStream(iis, offsets, length); } @Override public boolean canReadRaster() { return true; } @Override public Raster readRaster(int frameIndex, ImageReadParam param) throws IOException { readingImage = true; try { readMetaData(true); checkIndex(frameIndex); if (decompressor != null) { decompressor.setInput(iisOfFrame(frameIndex)); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Start decompressing frame #" + (frameIndex + 1)); //$NON-NLS-1$ } Raster wr = pmi.decompress() == pmi && decompressor.canReadRaster() ? decompressor.readRaster(0, decompressParam(param)) : decompressor.read(0, decompressParam(param)).getRaster(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Finished decompressing frame #" + (frameIndex + 1)); //$NON-NLS-1$ } return wr; } iis.seek(pixeldata.offset() + frameIndex * frameLength); WritableRaster wr = Raster.createWritableRaster(createSampleModel(dataType, banded), null); DataBuffer buf = wr.getDataBuffer(); if (buf instanceof DataBufferByte) { byte[][] data = ((DataBufferByte) buf).getBankData(); for (byte[] bs : data) { iis.readFully(bs); } } else { short[] data = ((DataBufferUShort) buf).getData(); iis.readFully(data, 0, data.length); } return wr; } finally { readingImage = false; } } private ImageReadParam decompressParam(ImageReadParam param) { ImageReadParam decompressParam = decompressor.getDefaultReadParam(); ImageTypeSpecifier imageType = param.getDestinationType(); BufferedImage dest = param.getDestination(); if (isRLELossless() && imageType == null && dest == null) { imageType = createImageType(bitsStored, dataType, true); } decompressParam.setDestinationType(imageType); decompressParam.setDestination(dest); if (decompressParam instanceof SignedDataImageParam) { ((SignedDataImageParam) decompressParam).setSignedData(dataType == DataBuffer.TYPE_SHORT); } return decompressParam; } @Override public BufferedImage read(int frameIndex, ImageReadParam param) throws IOException { readingImage = true; try { checkIndex(frameIndex); if (param == null) { param = getDefaultReadParam(); } WritableRaster raster; if (decompressor != null) { decompressor.setInput(iisOfFrame(frameIndex)); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Start decompressing frame #" + (frameIndex + 1)); //$NON-NLS-1$ } BufferedImage bi = decompressor.read(0, decompressParam(param)); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Finished decompressing frame #" + (frameIndex + 1)); //$NON-NLS-1$ } return bi; } else { raster = (WritableRaster) readRaster(frameIndex, param); } ColorModel cm = createColorModel(bitsStored, dataType); return new BufferedImage(cm, raster, false, null); } finally { readingImage = false; } } @Override public RenderedImage readAsRenderedImage(int frameIndex, ImageReadParam param) throws IOException { readingImage = true; try { readMetaData(true); checkIndex(frameIndex); if (param == null) { param = getDefaultReadParam(); } RenderedImage bi; if (decompressor != null) { decompressor.setInput(iisOfFrame(frameIndex)); if (isRLELossless() && (pmi.isSubSambled() || pmi.name().startsWith("YBR"))) { //$NON-NLS-1$ bi = convertSubSambledAndYBR(frameIndex, param); } else { bi = decompressor.readAsRenderedImage(0, decompressParam(param)); } } else { // Rewrite image with subsampled model (otherwise cannot not be displayed as RenderedImage) // Convert YBR_FULL into RBG as the ybr model is not well supported. if (pmi.isSubSambled() || pmi.name().startsWith("YBR")) { //$NON-NLS-1$ bi = convertSubSambledAndYBR(frameIndex, param); } else { ImageReader reader = initRawImageReader(); bi = reader.readAsRenderedImage(frameIndex, param); } } return validateSignedShortDataBuffer(bi); } finally { /* * "readingImage = false" will close the stream of the tiled image. The problem is that * readAsRenderedImage() do not read data immediately: RenderedImage delays the image reading */ } } private BufferedImage convertSubSambledAndYBR(int frameIndex, ImageReadParam param) throws IOException { // TODO improve this WritableRaster raster = (WritableRaster) readRaster(frameIndex, param); ColorModel cm = createColorModel(bitsStored, dataType); ColorModel cmodel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] { 8, 8, 8 }, false, // has // alpha false, // alpha premultipled Transparency.OPAQUE, DataBuffer.TYPE_BYTE); int width = raster.getWidth(); int height = raster.getHeight(); SampleModel sampleModel = cmodel.createCompatibleSampleModel(width, height); DataBuffer dataBuffer = sampleModel.createDataBuffer(); WritableRaster rasterDst = Raster.createWritableRaster(sampleModel, dataBuffer, null); ColorSpace cs = cm.getColorSpace(); for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { byte[] ba = (byte[]) raster.getDataElements(j, i, null); float[] fba = new float[] { (ba[0] & 0xFF) / 255f, (ba[1] & 0xFF) / 255f, (ba[2] & 0xFF) / 255f }; float[] rgb = cs.toRGB(fba); ba[0] = (byte) (rgb[0] * 255); ba[1] = (byte) (rgb[1] * 255); ba[2] = (byte) (rgb[2] * 255); rasterDst.setDataElements(j, i, ba); } } BufferedImage bi = new BufferedImage(cmodel, rasterDst, false, null); readingImage = true; return bi; } public RenderedImage validateSignedShortDataBuffer(RenderedImage source) { /* * Issue in ComponentColorModel when signed short DataBuffer, only 16 bits is supported see * http://java.sun.com/javase/6/docs/api/java/awt/image/ ComponentColorModel.html Instances of * ComponentColorModel created with transfer types DataBuffer.TYPE_SHORT, DataBuffer.TYPE_FLOAT, and * DataBuffer.TYPE_DOUBLE use all the bits of all sample values. Thus all color/alpha components have 16 bits * when using DataBuffer.TYPE_SHORT, 32 bits when using DataBuffer.TYPE_FLOAT, and 64 bits when using * DataBuffer.TYPE_DOUBLE. When the ComponentColorModel(ColorSpace, int[], boolean, boolean, int, int) form of * constructor is used with one of these transfer types, the bits array argument is ignored. */ // TODO test with all decoders (works with raw decoder) if (source != null && dataType == DataBuffer.TYPE_SHORT && source.getSampleModel().getDataType() == DataBuffer.TYPE_SHORT && (highBit + 1) < bitsAllocated) { return RectifySignedShortDataDescriptor.create(source, new int[] { highBit + 1 }, null); } return source; } public boolean isSkipLargePrivate() { return skipLargePrivate; } public void setSkipLargePrivate(boolean skipLargePrivate) { this.skipLargePrivate = skipLargePrivate; } @Override public Attributes getDicomObject() { try { DicomMetaData md = readMetaData(false); return md.getAttributes(); } catch (Exception e) { if (LOGGER.isDebugEnabled()) { LOGGER.error("Cannot read DICOM:", e); //$NON-NLS-1$ } else { LOGGER.error(e.getMessage()); } } return null; } @Override public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { super.setInput(input, seekForwardOnly, ignoreMetadata); resetInternalState(); if (input != null) { if (!(input instanceof ImageInputStream)) { throw new IllegalArgumentException("Input not an ImageInputStream!"); //$NON-NLS-1$ } this.iis = (ImageInputStream) input; } } @Override public void dispose() { HEADER_CACHE.remove(this); readingHeader = false; readingImage = false; reset(); super.dispose(); } @Override public void reset() { /* * readingHeader: prevent error when reading images from a large multiframe and the header is removed from the * cache at the same time. * * readingImage: prevent closing stream when reading an image or for the RenderedImage which delays the image * reading). */ if (!readingHeader && !readingImage) { super.reset(); resetInternalState(); } } private void resetInternalState() { FileUtil.safeClose(iis); iis = null; dis = null; tsuid = null; pixeldata = null; pixeldataFragments = null; if (decompressor != null) { decompressor.dispose(); decompressor = null; } patchJpegLS = null; } private void checkIndex(int frameIndex) { if (frameIndex < 0 || frameIndex >= numberOfFrame) { throw new IndexOutOfBoundsException("imageIndex: " + frameIndex); //$NON-NLS-1$ } } @Override public ImageReadParam getDefaultReadParam() { return new DicomImageReadParam(); } /** * Return a DicomStreamMetaData object that includes the DICOM header. <b>WARNING:</b> If this class is used to read * directly from a cache or other location that contains uncorrected data, the DICOM header will have the * uncorrected data as well. That is, assume the DB has some fixes to patient demographics. These will not usually * be applied to the DICOM files directly, so you can get the wrong information from the header. This is not an * issue if you know the DICOM is up to date, or if you use the DB information as authoritative. */ @Override public IIOMetadata getStreamMetadata() throws IOException { return readMetaData(false); } /** * Gets any image specific meta data. This should return the image specific blocks for enhanced multi-frame, but * currently it merely returns null. */ @Override public IIOMetadata getImageMetadata(int imageIndex) throws IOException { return null; } /** * Returns the number of regular images in the study. This excludes overlays. */ @Override public int getNumImages(boolean allowSearch) throws IOException { return numberOfFrame; } /** * Reads the DICOM header meta-data, up to, but not including pixel data. * * @throws Exception */ private synchronized DicomMetaData readMetaData(boolean readImageAfter) throws IOException { DicomMetaData header = HEADER_CACHE.get(this); if (header != null) { if (!readImageAfter) { return header; } } else if (dcmMetadata != null) { return dcmMetadata; } try { readingHeader = true; if (iis == null) { Optional<File> file = fileCache.getOriginalFile(); if (file.isPresent()) { setInput(ImageIO.createImageInputStream(new File(uri)), false, false); } } if (iis == null) { throw new IllegalStateException("Input not set!"); //$NON-NLS-1$ } /* * When readImageAfter is true, do not read again the header if it is in cache and the variables has been * initialized */ if (header != null && tsuid != null) { return header; } iis.seek(0L); dis = new DicomInputStream(new ImageInputStreamAdapter(iis)); dis.setIncludeBulkData(IncludeBulkData.URI); dis.setBulkDataDescriptor(DicomCodec.BULKDATA_DESCRIPTOR); // avoid a copy of pixeldata into temporary file dis.setURI(uri.toString()); Attributes fmi = dis.readFileMetaInformation(); Attributes ds = dis.readDataset(-1, -1); if (fmi == null) { fmi = ds.createFileMetaInformation(dis.getTransferSyntax()); } DicomMetaData metadata = new DicomMetaData(fmi, ds); Object pixdata = ds.getValue(Tag.PixelData, pixeldataVR); if (pixdata == null) { pixdata = ds.getValue(Tag.FloatPixelData, pixeldataVR); } if (pixdata == null) { pixdata = ds.getValue(Tag.DoubleFloatPixelData, pixeldataVR); } if (pixdata != null) { tsuid = dis.getTransferSyntax(); numberOfFrame = ds.getInt(Tag.NumberOfFrames, 1); hasPixel = ds.getInt(Tag.BitsStored, ds.getInt(Tag.BitsAllocated, 0)) > 0; if (readImageAfter && !tsuid.startsWith("1.2.840.10008.1.2.4.10") && hasPixel) { //$NON-NLS-1$ if (pixdata instanceof BulkData) { int width = TagD.getTagValue(this, Tag.Columns, Integer.class); int height = TagD.getTagValue(this, Tag.Rows, Integer.class); int samples = TagD.getTagValue(this, Tag.SamplesPerPixel, Integer.class); iis.setByteOrder(ds.bigEndian() ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); this.frameLength = pmi.frameLength(width, height, samples, bitsAllocated); this.pixeldata = (BulkData) pixdata; // Handle JPIP } else if (ds.getString(Tag.PixelDataProviderURL) != null) { if (numberOfFrame == 0) { numberOfFrame = 1; // compressed = true; } } else if (pixdata instanceof Fragments) { ImageReaderFactory.ImageReaderItem readerItem = ImageReaderFactory.getImageReader(tsuid); if (readerItem == null) { throw new IOException("Unsupported Transfer Syntax: " + tsuid); //$NON-NLS-1$ } this.decompressor = readerItem.getImageReader(); this.pixeldataFragments = (Fragments) pixdata; } } } HEADER_CACHE.put(this, metadata); return metadata; } finally { readingHeader = false; if (!readImageAfter) { // Reset must be called only after reading the header, because closing imageStream does not let through // getTile(x,y) read image data. // unlock file to be deleted on exit // System.err.println("Close stream: reading header"); FileUtil.safeClose(iis); iis = null; } } } private SampleModel createSampleModel(int dataType, boolean banded) { return pmi.createSampleModel(dataType, TagD.getTagValue(this, Tag.Columns, Integer.class), TagD.getTagValue(this, Tag.Rows, Integer.class), TagD.getTagValue(this, Tag.SamplesPerPixel, Integer.class), banded); } private ImageTypeSpecifier createImageType(int bits, int dataType, boolean banded) { return new ImageTypeSpecifier(createColorModel(bits, dataType), createSampleModel(dataType, banded)); } private ColorModel createColorModel(int bits, int dataType) { return pmi.createColorModel(bits, dataType, getDicomObject()); } }