소스 검색

!97 新增 dcm 等医疗数位影像预览
Merge pull request !97 from 高雄/dcm

kailing 1 년 전
부모
커밋
f520001863

+ 1 - 0
README.cn.md

@@ -21,6 +21,7 @@
 19. 支持 svg 矢量图像格式文件
 20. 支持 mp3,wav,mp4,flv 等音视频格式文件
 21. 支持 avi,mov,rm,webm,ts,rm,mkv,mpeg,ogg,mpg,rmvb,wmv,3gp,ts,swf 等视频格式转码预览
+22. 支持 dcm 等医疗数位影像预览
 
 > 基于当前良好的架构模式,支持的文件类型在进一步丰富中
 ### 项目特性

+ 1 - 0
README.md

@@ -25,6 +25,7 @@ Document online preview project solution, built using the popular Spring Boot fr
 19. Supports vector image format files such as `svg`.
 20. Supports `mp3`,`wav`,`mp4`,`flv` .
 21. Supports many audio and video format files such as `avi`, `mov`, `wmv`, `mkv`, `3gp`, and `rm`.
+22. Supports for `dcm` .
 
 ### Features
 - Build with the popular frame spring boot

+ 6 - 1
server/src/main/java/cn/keking/model/FileType.java

@@ -30,7 +30,8 @@ public enum FileType {
     XMIND("xmindFilePreviewImpl"),
     SVG("svgFilePreviewImpl"),
     Epub("epubFilePreviewImpl"),
-    BPMN("bpmnFilePreviewImpl");
+    BPMN("bpmnFilePreviewImpl"),
+    DCM("dcmFilePreviewImpl");
 
     private static final String[] OFFICE_TYPES = {"docx", "wps", "doc", "docm", "xls", "xlsx", "csv" ,"xlsm", "ppt", "pptx", "vsd", "rtf", "odt", "wmf", "emf", "dps", "et", "ods", "ots", "tsv", "odp", "otp", "sxi", "ott", "vsdx", "fodt", "fods", "xltx","tga","psd","dotm","ett","xlt","xltm","wpt","dot","xlam","dotx","xla"};
     private static final String[] PICTURE_TYPES = {"jpg", "jpeg", "png", "gif", "bmp", "ico", "jfif", "webp"};
@@ -39,6 +40,7 @@ public enum FileType {
     private static final String[] EML_TYPES = {"eml"};
     private static final String[] XMIND_TYPES = {"xmind"};
     private static final String[] Epub_TYPES = {"epub"};
+    private static final String[] DCM_TYPES = {"dcm"};
     private static final String[] TIFF_TYPES = {"tif", "tiff"};
     private static final String[] OFD_TYPES = {"ofd"};
     private static final String[] SVG_TYPES = {"svg"};
@@ -95,6 +97,9 @@ public enum FileType {
         for (String online3D : Online3D_TYPES) {
             FILE_TYPE_MAPPER.put(online3D, FileType.Online3D);
         }
+        for (String dcm : DCM_TYPES) {
+            FILE_TYPE_MAPPER.put(dcm, FileType.DCM);
+        }
         FILE_TYPE_MAPPER.put("md", FileType.MARKDOWN);
         FILE_TYPE_MAPPER.put("xml", FileType.XML);
         FILE_TYPE_MAPPER.put("pdf", FileType.PDF);

+ 1 - 0
server/src/main/java/cn/keking/service/FilePreview.java

@@ -29,6 +29,7 @@ public interface FilePreview {
     String XML_FILE_PREVIEW_PAGE = "xml";
     String MARKDOWN_FILE_PREVIEW_PAGE = "markdown";
     String BPMN_FILE_PREVIEW_PAGE = "bpmn";
+    String DCM_FILE_PREVIEW_PAGE = "dcm";
     String NOT_SUPPORTED_FILE_PAGE = "fileNotSupported";
 
     String filePreviewHandle(String url, Model model, FileAttribute fileAttribute);

+ 25 - 0
server/src/main/java/cn/keking/service/impl/DcmFilePreviewImpl.java

@@ -0,0 +1,25 @@
+package cn.keking.service.impl;
+
+import cn.keking.model.FileAttribute;
+import cn.keking.service.FilePreview;
+import org.springframework.stereotype.Service;
+import org.springframework.ui.Model;
+
+/**
+ * Dcm 文件处理
+ */
+@Service
+public class DcmFilePreviewImpl implements FilePreview {
+
+    private final CommonPreviewImpl commonPreview;
+
+    public DcmFilePreviewImpl(CommonPreviewImpl commonPreview) {
+        this.commonPreview = commonPreview;
+    }
+
+    @Override
+    public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
+        commonPreview.filePreviewHandle(url,model,fileAttribute);
+        return DCM_FILE_PREVIEW_PAGE;
+    }
+}

+ 159 - 0
server/src/main/resources/static/dcm/DICOMZero.js

@@ -0,0 +1,159 @@
+class DICOMZero {
+  constructor(options={}) {
+    this.status = options.status || function() {};
+    this.reset();
+  }
+
+  reset() {
+    this.mappingLog = [];
+    this.dataTransfer = undefined;
+    this.datasets = [];
+    this.readers = [];
+    this.arrayBuffers = [];
+    this.files = [];
+    this.fileIndex = 0;
+    this.context = {patients: []};
+  }
+
+  static datasetFromArrayBuffer(arrayBuffer) {
+    let dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
+    let dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomData.dict);
+    dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta);
+    return(dataset);
+  }
+
+  // return a function to use as the 'onload' callback for the file reader.
+  // The function takes a progress event argument and it knows (from this class instance)
+  // when all files have been read so it can invoke the doneCallback when all
+  // have been read.
+  getReadDICOMFunction(doneCallback, statusCallback) {
+    statusCallback = statusCallback || console.log;
+    return progressEvent => {
+      let reader = progressEvent.target;
+      let arrayBuffer = reader.result;
+      this.arrayBuffers.push(arrayBuffer);
+
+      let dicomData;
+      try {
+        dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
+        let dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomData.dict);
+        dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta);
+        this.datasets.push(dataset);
+      } catch (error) {
+        console.error(error);
+        statusCallback("skipping non-dicom file");
+      }
+
+      let readerIndex = this.readers.indexOf(reader);
+      if (readerIndex < 0) {
+        reject("Logic error: Unexpected reader!");
+      } else {
+        this.readers.splice(readerIndex, 1); // remove the reader
+      }
+
+      if (this.fileIndex === this.dataTransfer.files.length) {
+        statusCallback(`Normalizing...`);
+        try {
+          this.multiframe = dcmjs.normalizers.Normalizer.normalizeToDataset(this.datasets);
+        } catch (e) {
+          console.error('Could not convert to multiframe');
+          console.error(e);
+        }
+		
+		if (this.multiframe.SOPClassUID == dcmjs.data.DicomMetaDictionary.sopClassUIDsByName['Segmentation']){
+			statusCallback(`Creating segmentation...`);
+			try {
+			  this.seg = new dcmjs.derivations.Segmentation([this.multiframe]);
+			  statusCallback(`Created ${this.multiframe.NumberOfFrames} frame multiframe object and segmentation.`);
+			} catch (e) {
+			  console.error('Could not create segmentation');
+			  console.error(e);
+			}
+	    } else if (this.multiframe.SOPClassUID == dcmjs.data.DicomMetaDictionary.sopClassUIDsByName['ParametricMapStorage']){
+			statusCallback(`Creating parametric map...`);
+			try {
+			  this.pm = new dcmjs.derivations.ParametricMap([this.multiframe]);
+			  statusCallback(`Created ${this.multiframe.NumberOfFrames} frame multiframe object and parametric map.`);
+			} catch (e) {
+			  console.error('Could not create parametric map');
+			  console.error(e);
+			}
+		}
+        doneCallback();
+      } else {
+        statusCallback(`Reading... (${this.fileIndex+1}).`);
+        this.readOneFile(doneCallback, statusCallback);
+      }
+    };
+  }
+
+  // Used for file selection button or drop of file list
+  readOneFile(doneCallback, statusCallback) {
+    let file = this.dataTransfer.files[this.fileIndex];
+    this.fileIndex++;
+
+    let reader = new FileReader();
+    reader.onload = this.getReadDICOMFunction(doneCallback, statusCallback);
+    reader.readAsArrayBuffer(file);
+
+    this.files.push(file);
+    this.readers.push(reader);
+  }
+
+  handleDataTransferFileAsDataset(file, options={}) {
+    options.doneCallback = options.doneCallback || function(){};
+
+    let reader = new FileReader();
+    reader.onload = (progressEvent) => {
+      let dataset = DICOMZero.datasetFromArrayBuffer(reader.result);
+      options.doneCallback(dataset);
+    }
+    reader.readAsArrayBuffer(file);
+  }
+  
+  extractDatasetFromZipArrayBuffer(arrayBuffer) {
+    this.status(`Extracting ${this.datasets.length} of ${this.expectedDICOMFileCount}...`);
+    this.datasets.push(DICOMZero.datasetFromArrayBuffer(arrayBuffer));
+    if (this.datasets.length == this.expectedDICOMFileCount) {
+      this.status(`Finished extracting`);
+      this.zipFinishCallback();
+    }
+  };
+
+  handleZip(zip) {
+    this.zip = zip;
+    this.expectedDICOMFileCount = 0;
+    Object.keys(zip.files).forEach(fileKey => {
+      this.status(`Considering ${fileKey}...`);
+      if (fileKey.endsWith('.dcm')) {
+        this.expectedDICOMFileCount += 1;
+        zip.files[fileKey].async('arraybuffer').then(this.extractDatasetFromZipArrayBuffer.bind(this));
+      }
+    });
+  }
+
+  extractFromZipArrayBuffer(arrayBuffer, finishCallback=function(){}) {
+    this.zipFinishCallback = finishCallback;
+    this.status("Extracting from zip...");
+    JSZip.loadAsync(arrayBuffer)
+    .then(this.handleZip.bind(this));
+  }
+
+  organizeDatasets() {
+    this.datasets.forEach(dataset => {
+      let patientName = dataset.PatientName;
+      let studyTag = dataset.StudyDate + ": " + dataset.StudyDescription;
+      let seriesTag = dataset.SeriesNumber + ": " + dataset.SeriesDescription;
+      let patientNames = this.context.patients.map(patient => patient.name);
+      let patientIndex = patientNames.indexOf(dataset.PatientName);
+      if (patientIndex == -1) {
+        this.context.patients.push({
+          name: dataset.PatientName,
+          id: this.context.patients.length,
+          studies: {}
+        });
+      }
+      let studyNames; // TODO - finish organizing
+    });
+  }
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 8547 - 0
server/src/main/resources/static/dcm/cornerstone.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 2084 - 0
server/src/main/resources/static/dcm/cornerstoneMath.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 36430 - 0
server/src/main/resources/static/dcm/cornerstoneTools.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 2 - 0
server/src/main/resources/static/dcm/cornerstoneWADOImageLoader.bundle.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 3955 - 0
server/src/main/resources/static/dcm/dicomParser.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 7 - 0
server/src/main/resources/static/dcm/hammer.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 7119 - 0
server/src/main/resources/static/dcm/index.umd.js


+ 43 - 0
server/src/main/resources/static/dcm/initCornerstone.js

@@ -0,0 +1,43 @@
+cornerstoneTools.external.cornerstone = cornerstone;
+cornerstoneTools.external.Hammer = Hammer;
+cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
+
+cornerstoneTools.init();
+
+cornerstoneTools.addTool(cornerstoneTools.BidirectionalTool);
+cornerstoneTools.addTool(cornerstoneTools.ArrowAnnotateTool);
+cornerstoneTools.addTool(cornerstoneTools.EllipticalRoiTool);
+
+function getBlobUrl(url) {
+    const baseUrl = window.URL || window.webkitURL;
+    const blob = new Blob([`importScripts('${url}')`], {
+        type: "application/javascript"
+    });
+
+    return baseUrl.createObjectURL(blob);
+}
+
+const config = {
+    maxWebWorkers: navigator.hardwareConcurrency || 1,
+    startWebWorkersOnDemand: true,
+    webWorkerPath: getBlobUrl(
+        "https://unpkg.com/cornerstone-wado-image-loader/dist/cornerstoneWADOImageLoaderWebWorker.min.js"
+    ),
+    webWorkerTaskPaths: [],
+    taskConfiguration: {
+        decodeTask: {
+            loadCodecsOnStartup: true,
+            initializeCodecsOnStartup: false,
+            codecsPath: getBlobUrl(
+                "https://unpkg.com/cornerstone-wado-image-loader/dist/cornerstoneWADOImageLoaderCodecs.min.js"
+            ),
+            usePDFJS: false,
+            strict: false
+        }
+    }
+};
+
+cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
+
+cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
+cornerstoneWADOImageLoader.external.dicomParser = dicomParser;

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 25147 - 0
server/src/main/resources/static/dcm/react-dom.development.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 3318 - 0
server/src/main/resources/static/dcm/react.development.js


+ 103 - 0
server/src/main/resources/web/dcm.ftl

@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+
+<html lang="en">
+<head>
+    <meta charset="utf-8"/>
+    <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0">
+    <title>DCM预览</title>
+    <#include "*/commonHeader.ftl">
+</head>
+	<style>
+     .container{
+		width: 100%; 
+		height: 600px;
+		max-width: 900px;  
+		margin: auto;
+         position: absolute;
+        top: 0;
+       left: 0;
+       bottom: 0;
+       right: 0;
+     background-color: green;
+			}
+		</style>
+<body>
+<#if currentUrl?contains("http://") || currentUrl?contains("https://")>
+    <#assign finalUrl="${currentUrl}">
+<#else>
+    <#assign finalUrl="${baseUrl}${currentUrl}">
+</#if>
+    <div class="container" id="cornerstoneViewport">
+
+        </div>
+        <script src="dcm/cornerstone.js"></script>
+        <script src="dcm/cornerstoneMath.js"></script>
+        <script src="dcm/cornerstoneTools.js"></script>
+        <script src="dcm/dicomParser.js"></script>
+        <script src="dcm/cornerstoneWADOImageLoader.bundle.min.js"></script>
+        <script src="dcm/hammer.min.js"></script>
+        <script src="dcm/initCornerstone.js"></script>
+        <script  src="dcm/react.development.js" ></script>
+        <script src="dcm/react-dom.development.js"></script>
+        <script>
+            "use strict";
+
+            var process = {
+                env: {
+                    NODE_ENV: "production"
+                }
+            };
+
+            window.process = process;
+        </script>
+        <script src="dcm/index.umd.js"></script>
+
+        <script>
+          var url = '${finalUrl}';
+    var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
+    if (!url.startsWith(baseUrl)) {
+        url = baseUrl + 'getCorsFile?urlPath=' + encodeURIComponent(Base64.encode(url));
+    }
+            "use strict";
+         
+            var imageNames = [];
+            for (var i = 1; i < 546; i++) {
+                imageNames.push(url);
+            }
+          //  console.log(url);
+            var imageIds = imageNames.map(name => {
+                 return 'dicomweb:'+url+'';
+            });
+            var imagePromises = imageIds.map(imageId => {
+                return cornerstone.loadAndCacheImage(imageId);
+            });
+         
+            var exampleData = {
+                stack: {
+                    currentImageIdIndex: 0,
+                    imageIds: imageIds
+                }
+            };
+
+            var CornerstoneViewport = window["react-cornerstone-viewport"];
+            var props = {
+                viewportData: exampleData,
+                cornerstone,
+                cornerstoneTools,
+                activeTool: "Brush"
+            };
+
+            var app = React.createElement(CornerstoneViewport, props, null);
+
+            ReactDOM.render(
+                app,
+                document.getElementById("cornerstoneViewport")
+            );
+             /*初始化水印*/
+    window.onload = function () {
+        initWaterMark();
+    }
+        </script>
+</body>
+
+</html>

+ 1 - 0
server/src/main/resources/web/main/index.ftl

@@ -79,6 +79,7 @@
             <li>支持 svg 矢量图像格式文件</li>
             <li>支持 mp3,wav,mp4,flv 等音视频格式文件</li>
             <li>支持 avi,mov,rm,webm,ts,rm,mkv,mpeg,ogg,mpg,rmvb,wmv,3gp,ts,swf 等视频格式转码预览</li>
+            <li>支持 dcm 等医疗数位影像预览</li>
         </ol>
     </div>
     <#--  输入下载地址预览文件  -->

+ 10 - 0
server/src/main/resources/web/main/record.ftl

@@ -46,6 +46,16 @@
     <div class="page-header">
         <h1>版本发布记录</h1>
     </div>
+    <div class="panel panel-success">
+        <div class="panel-heading">
+            <h3 class="panel-title">2023年04月20日,v4.3.0-SNAPSHOT版本</h3>
+
+        </div>
+        <div class="panel-body">
+            <div>
+                1.新增 dcm 等医疗数位影像预览<br>
+            </div>
+        </div>
     <div class="panel panel-success">
         <div class="panel-heading">
             <h3 class="panel-title">2023年04月18日,v4.2.1 版本</h3>