commit
f520001863
|
@ -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 等医疗数位影像预览
|
||||
|
||||
> 基于当前良好的架构模式,支持的文件类型在进一步丰富中
|
||||
### 项目特性
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2
server/src/main/resources/static/dcm/cornerstoneWADOImageLoader.bundle.min.js
vendored
Normal file
2
server/src/main/resources/static/dcm/cornerstoneWADOImageLoader.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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>
|
|
@ -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>
|
||||
<#-- 输入下载地址预览文件 -->
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue