ohif-viewer/extensions/cornerstone/src/utils/DicomFileUploader.ts

205 lines
5.5 KiB
TypeScript

import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
import { PubSubService } from '@ohif/core';
export const EVENTS = {
PROGRESS: 'event:DicomFileUploader:progress',
};
export interface DicomFileUploaderEvent {
fileId: number;
}
export interface DicomFileUploaderProgressEvent extends DicomFileUploaderEvent {
percentComplete: number;
}
export enum UploadStatus {
NotStarted,
InProgress,
Success,
Failed,
Cancelled,
}
type CancelOrFailed = UploadStatus.Cancelled | UploadStatus.Failed;
export class UploadRejection {
message: string;
status: CancelOrFailed;
constructor(status: CancelOrFailed, message: string) {
this.message = message;
this.status = status;
}
}
export default class DicomFileUploader extends PubSubService {
private _file;
private _fileId;
private _dataSource;
private _loadPromise;
private _abortController = new AbortController();
private _status: UploadStatus = UploadStatus.NotStarted;
private _percentComplete = 0;
constructor(file, dataSource) {
super(EVENTS);
this._file = file;
this._fileId = dicomImageLoader.wadouri.fileManager.add(file);
this._dataSource = dataSource;
}
getFileId(): string {
return this._fileId;
}
getFileName(): string {
return this._file.name;
}
getFileSize(): number {
return this._file.size;
}
cancel(): void {
this._abortController.abort();
}
getStatus(): UploadStatus {
return this._status;
}
getPercentComplete(): number {
return this._percentComplete;
}
async load(): Promise<void> {
if (this._loadPromise) {
// Already started loading, return the load promise.
return this._loadPromise;
}
this._loadPromise = new Promise<void>((resolve, reject) => {
// The upload listeners: fire progress events and/or settle the promise.
const uploadCallbacks = {
progress: evt => {
if (!evt.lengthComputable) {
// Progress computation is not possible.
return;
}
this._status = UploadStatus.InProgress;
this._percentComplete = Math.round((100 * evt.loaded) / evt.total);
this._broadcastEvent(EVENTS.PROGRESS, {
fileId: this._fileId,
percentComplete: this._percentComplete,
});
},
timeout: () => {
this._reject(reject, new UploadRejection(UploadStatus.Failed, 'The request timed out.'));
},
abort: () => {
this._reject(reject, new UploadRejection(UploadStatus.Cancelled, 'Cancelled'));
},
error: () => {
this._reject(reject, new UploadRejection(UploadStatus.Failed, 'The request failed.'));
},
};
// First try to load the file.
dicomImageLoader.wadouri
.loadFileRequest(this._fileId)
.then(dicomFile => {
if (this._abortController.signal.aborted) {
this._reject(reject, new UploadRejection(UploadStatus.Cancelled, 'Cancelled'));
return;
}
if (!this._checkDicomFile(dicomFile)) {
// The file is not DICOM
this._reject(
reject,
new UploadRejection(UploadStatus.Failed, 'Not a valid DICOM file.')
);
return;
}
const request = new XMLHttpRequest();
this._addRequestCallbacks(request, uploadCallbacks);
// Do the actual upload by supplying the DICOM file and upload callbacks/listeners.
return this._dataSource.store
.dicom(dicomFile, request)
.then(() => {
this._status = UploadStatus.Success;
resolve();
})
.catch(reason => {
this._reject(reject, reason);
});
})
.catch(reason => {
this._reject(reject, reason);
});
});
return this._loadPromise;
}
private _isRejected(): boolean {
return this._status === UploadStatus.Failed || this._status === UploadStatus.Cancelled;
}
private _reject(reject: (reason?: any) => void, reason: any) {
if (this._isRejected()) {
return;
}
if (reason instanceof UploadRejection) {
this._status = reason.status;
reject(reason);
return;
}
this._status = UploadStatus.Failed;
if (reason.message) {
reject(new UploadRejection(UploadStatus.Failed, reason.message));
return;
}
reject(new UploadRejection(UploadStatus.Failed, reason));
}
private _addRequestCallbacks(request: XMLHttpRequest, uploadCallbacks) {
const abortCallback = () => request.abort();
this._abortController.signal.addEventListener('abort', abortCallback);
for (const [eventName, callback] of Object.entries(uploadCallbacks)) {
request.upload.addEventListener(eventName, callback);
}
const cleanUpCallback = () => {
this._abortController.signal.removeEventListener('abort', abortCallback);
for (const [eventName, callback] of Object.entries(uploadCallbacks)) {
request.upload.removeEventListener(eventName, callback);
}
request.removeEventListener('loadend', cleanUpCallback);
};
request.addEventListener('loadend', cleanUpCallback);
}
private _checkDicomFile(arrayBuffer: ArrayBuffer) {
if (arrayBuffer.length <= 132) {
return false;
}
const arr = new Uint8Array(arrayBuffer.slice(128, 132));
// bytes from 128 to 132 must be "DICM"
return Array.from('DICM').every((char, i) => char.charCodeAt(0) === arr[i]);
}
}