/******************************************************************************* * 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.explorer; import java.awt.Point; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.dcm4che3.data.Attributes; import org.dcm4che3.data.Tag; import org.dcm4che3.image.PhotometricInterpretation; import org.dcm4che3.media.DicomDirReader; import org.dcm4che3.media.DicomDirWriter; import org.dcm4che3.media.RecordFactory; import org.dcm4che3.media.RecordType; import org.dcm4che3.util.UIDUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.weasis.core.api.explorer.model.DataExplorerModel; import org.weasis.core.api.gui.util.GuiExecutor; import org.weasis.core.api.image.util.ImageFiler; import org.weasis.core.api.media.data.MediaSeriesGroup; import org.weasis.core.api.media.data.MediaSeriesGroupNode; import org.weasis.core.api.media.data.Series; import org.weasis.core.api.media.data.TagW; import org.weasis.core.api.media.data.Thumbnail; import org.weasis.core.ui.docking.UIManager; import org.weasis.core.ui.editor.image.ViewerPlugin; import org.weasis.dicom.codec.DicomInstance; import org.weasis.dicom.codec.DicomSeries; import org.weasis.dicom.codec.TagD; import org.weasis.dicom.codec.TagD.Level; import org.weasis.dicom.codec.utils.DicomImageUtils; import org.weasis.dicom.codec.utils.DicomMediaUtils; import org.weasis.dicom.codec.wado.WadoParameters; import org.weasis.dicom.explorer.wado.DownloadPriority; import org.weasis.dicom.explorer.wado.LoadSeries; public class DicomDirLoader { private static final Logger LOGGER = LoggerFactory.getLogger(DicomDirLoader.class); public static final RecordFactory RecordFactory = new RecordFactory(); private final DicomModel dicomModel; private final ArrayList<LoadSeries> seriesList; private final WadoParameters wadoParameters; private final boolean writeInCache; private final File dcmDirFile; public DicomDirLoader(File dcmDirFile, DataExplorerModel explorerModel, boolean writeInCache) { if (dcmDirFile == null || !dcmDirFile.canRead() || !(explorerModel instanceof DicomModel)) { throw new IllegalArgumentException("invalid parameters"); //$NON-NLS-1$ } this.dicomModel = (DicomModel) explorerModel; this.writeInCache = writeInCache; this.dcmDirFile = dcmDirFile; wadoParameters = new WadoParameters("", true, "", null, null); //$NON-NLS-1$ //$NON-NLS-2$ seriesList = new ArrayList<>(); } public List<LoadSeries> readDicomDir() { Attributes dcmPatient = null; MediaSeriesGroup patient = null; int pat = 0; try (DicomDirReader reader = new DicomDirReader(dcmDirFile)) { dcmPatient = findFirstRootDirectoryRecordInUse(reader); while (dcmPatient != null) { if (RecordType.PATIENT.name().equals(dcmPatient.getString(Tag.DirectoryRecordType))) { parsePatient(dcmPatient, reader); } dcmPatient = findNextSiblingRecord(dcmPatient, reader); } } catch (IOException e) { LOGGER.error("Cannot read DICOMDIR !", e); //$NON-NLS-1$ } if (pat == 1) { // In case of the patient already exists, select it final MediaSeriesGroup uniquePatient = patient; GuiExecutor.instance().execute(new Runnable() { @Override public void run() { synchronized (UIManager.VIEWER_PLUGINS) { for (final ViewerPlugin p : UIManager.VIEWER_PLUGINS) { if (uniquePatient.equals(p.getGroupID())) { p.setSelectedAndGetFocus(); break; } } } } }); } for (LoadSeries loadSeries : seriesList) { String modality = TagD.getTagValue(loadSeries.getDicomSeries(), Tag.Modality, String.class); boolean ps = modality != null && ("PR".equals(modality) || "KO".equals(modality)); //$NON-NLS-1$ //$NON-NLS-2$ if (!ps) { loadSeries.startDownloadImageReference(wadoParameters); } } return seriesList; } private boolean parsePatient(Attributes dcmPatient, DicomDirReader reader) { boolean newPatient = false; try { String patientPseudoUID = DicomMediaUtils.buildPatientPseudoUID(dcmPatient.getString(Tag.PatientID, TagW.NO_VALUE), dcmPatient.getString(Tag.IssuerOfPatientID), dcmPatient.getString(Tag.PatientName, TagW.NO_VALUE)); MediaSeriesGroup patient = dicomModel.getHierarchyNode(MediaSeriesGroupNode.rootNode, patientPseudoUID); if (patient == null) { patient = new MediaSeriesGroupNode(TagD.getUID(Level.PATIENT), patientPseudoUID, DicomModel.patient.getTagView()); DicomMediaUtils.writeMetaData(patient, dcmPatient); dicomModel.addHierarchyNode(MediaSeriesGroupNode.rootNode, patient); newPatient = true; } parseStudy(patient, dcmPatient, reader); } catch (Exception e) { LOGGER.error("Cannot read DICOMDIR !", e); //$NON-NLS-1$ } return newPatient; } private void parseStudy(MediaSeriesGroup patient, Attributes dcmPatient, DicomDirReader reader) { Attributes dcmStudy = findFirstChildRecord(dcmPatient, reader); while (dcmStudy != null) { if (RecordType.STUDY.name().equals(dcmStudy.getString(Tag.DirectoryRecordType))) { String studyUID = (String) TagD.getUID(Level.STUDY).getValue(dcmStudy); MediaSeriesGroup study = dicomModel.getHierarchyNode(patient, studyUID); if (study == null) { study = new MediaSeriesGroupNode(TagD.getUID(Level.STUDY), studyUID, DicomModel.study.getTagView()); DicomMediaUtils.writeMetaData(study, dcmStudy); dicomModel.addHierarchyNode(patient, study); } parseSeries(patient, study, dcmStudy, reader); } dcmStudy = findNextSiblingRecord(dcmStudy, reader); } } private void parseSeries(MediaSeriesGroup patient, MediaSeriesGroup study, Attributes dcmStudy, DicomDirReader reader) { Attributes series = findFirstChildRecord(dcmStudy, reader); while (series != null) { if (RecordType.SERIES.name().equals(series.getString(Tag.DirectoryRecordType))) { String seriesUID = series.getString(Tag.SeriesInstanceUID, TagW.NO_VALUE); Series dicomSeries = (Series) dicomModel.getHierarchyNode(study, seriesUID); if (dicomSeries == null) { dicomSeries = new DicomSeries(seriesUID); dicomSeries.setTag(TagW.ExplorerModel, dicomModel); dicomSeries.setTag(TagW.WadoParameters, wadoParameters); dicomSeries.setTag(TagW.WadoInstanceReferenceList, new ArrayList<DicomInstance>()); DicomMediaUtils.writeMetaData(dicomSeries, series); dicomModel.addHierarchyNode(study, dicomSeries); } else { WadoParameters wado = (WadoParameters) dicomSeries.getTagValue(TagW.WadoParameters); if (wado == null) { // Should never happen dicomSeries.setTag(TagW.WadoParameters, wadoParameters); } } List<DicomInstance> dicomInstances = (List<DicomInstance>) dicomSeries.getTagValue(TagW.WadoInstanceReferenceList); if (dicomInstances == null) { dicomInstances = new ArrayList<>(); dicomSeries.setTag(TagW.WadoInstanceReferenceList, dicomInstances); } // Icon Image Sequence (0088,0200).This Icon Image is representative of the Series. It may or may not // correspond to one of the images of the Series. Attributes iconInstance = series.getNestedDataset(Tag.IconImageSequence); Attributes instance = findFirstChildRecord(series, reader); while (instance != null) { // Try to read all the file types of the Series. String sopInstanceUID = instance.getString(Tag.ReferencedSOPInstanceUIDInFile); if (sopInstanceUID != null) { DicomInstance dcmInstance = new DicomInstance(sopInstanceUID); if (dicomInstances.contains(dcmInstance)) { LOGGER.warn("DICOM instance {} already exists, abort downloading.", sopInstanceUID); //$NON-NLS-1$ } else { File file = toFileName(instance, reader); if (file != null) { if (file.exists()) { dcmInstance.setInstanceNumber( DicomMediaUtils.getIntegerFromDicomElement(instance, Tag.InstanceNumber, -1)); dcmInstance.setDirectDownloadFile(file.toURI().toString()); dicomInstances.add(dcmInstance); if (iconInstance == null) { // Icon Image Sequence (0088,0200). This Icon Image is representative of the // Image. Only a single Item is permitted in this Sequence. iconInstance = instance.getNestedDataset(Tag.IconImageSequence); } } else { LOGGER.error("Missing DICOMDIR entry: {}", file.getPath()); //$NON-NLS-1$ } } } } instance = findNextSiblingRecord(instance, reader); } if (!dicomInstances.isEmpty()) { dicomSeries.setTag(TagW.DirectDownloadThumbnail, readDicomDirIcon(iconInstance)); dicomSeries.setTag(TagW.ReadFromDicomdir, true); final LoadSeries loadSeries = new LoadSeries(dicomSeries, dicomModel, 1, writeInCache); loadSeries.setPriority(new DownloadPriority(patient, study, dicomSeries, false)); seriesList.add(loadSeries); } } series = findNextSiblingRecord(series, reader); } } /** * Reads DICOMDIR icon. Only monochrome and palette color images shall be used. Samples per Pixel (0028,0002) shall * have a Value of 1, Photometric Interpretation (0028,0004) shall have a Value of either MONOCHROME 1, MONOCHROME 2 * or PALETTE COLOR, Planar Configuration (0028,0006) shall not be present. * * @see <a href="http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_F.7.html">F.7 Icon Image Key * Definition</a> * * @param iconInstance * Attributes * @return the thumbnail path */ private String readDicomDirIcon(Attributes iconInstance) { if (iconInstance != null) { byte[] pixelData = null; try { pixelData = iconInstance.getBytes(Tag.PixelData); if (pixelData != null) { File thumbnailPath = File.createTempFile("tumb_", ".jpg", Thumbnail.THUMBNAIL_CACHE_DIR); //$NON-NLS-1$ //$NON-NLS-2$ if (thumbnailPath != null) { int width = iconInstance.getInt(Tag.Columns, 0); int height = iconInstance.getInt(Tag.Rows, 0); if (width != 0 && height != 0) { WritableRaster raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height, 1, new Point(0, 0)); raster.setDataElements(0, 0, width, height, pixelData); PhotometricInterpretation pmi = PhotometricInterpretation .fromString(iconInstance.getString(Tag.PhotometricInterpretation, "MONOCHROME2")); //$NON-NLS-1$ BufferedImage thumbnail = new BufferedImage( pmi.createColorModel(8, DataBuffer.TYPE_BYTE, iconInstance), raster, false, null); if (ImageFiler.writeJPG(thumbnailPath, DicomImageUtils.getRGBImageFromPaletteColorModel(thumbnail, iconInstance), 0.75f)) { return thumbnailPath.getPath(); } } } } } catch (Exception e) { LOGGER.error("Cannot read Icon in DICOMDIR!", e); //$NON-NLS-1$ } } return null; } private Attributes findFirstChildRecord(Attributes dcmObject, DicomDirReader reader) { try { return reader.findLowerDirectoryRecordInUse(dcmObject, true); } catch (IOException e) { LOGGER.error("Cannot read first DICOMDIR entry!", e); //$NON-NLS-1$ } return null; } private Attributes findNextSiblingRecord(Attributes dcmObject, DicomDirReader reader) { try { return reader.findNextDirectoryRecordInUse(dcmObject, true); } catch (IOException e) { LOGGER.error("Cannot read next DICOMDIR entry!", e); //$NON-NLS-1$ } return null; } private Attributes findFirstRootDirectoryRecordInUse(DicomDirReader reader) { try { return reader.findFirstRootDirectoryRecordInUse(true); } catch (IOException e) { LOGGER.error("Cannot find Patient in DICOMDIR !", e); //$NON-NLS-1$ } return null; } private File toFileName(Attributes dcmObject, DicomDirReader reader) { String[] fileID = dcmObject.getStrings(Tag.ReferencedFileID); if (fileID == null || fileID.length == 0) { return null; } StringBuilder sb = new StringBuilder(fileID[0]); for (int i = 1; i < fileID.length; i++) { sb.append(File.separatorChar).append(fileID[i]); } File file = new File(reader.getFile().getParent(), sb.toString()); if (!file.exists()) { // Try to find lower case relative path, it happens sometimes when mounting cdrom on Linux File fileLowerCase = new File(reader.getFile().getParent(), sb.toString().toLowerCase()); if (fileLowerCase.exists()) { file = fileLowerCase; } } return file; } public static DicomDirWriter open(File file) throws IOException { if (file.createNewFile()) { DicomDirWriter.createEmptyDirectory(file, UIDUtils.createUID(), null, null, null); } return DicomDirWriter.open(file); } }