From 8e8654bec1d93f14b7e9290fc3b85d60a5781b23 Mon Sep 17 00:00:00 2001 From: Evren Ozkan Date: Tue, 21 Jun 2016 11:56:14 -0400 Subject: [PATCH] - Update cornerstoneTools from "v0.7.8 - 2016-03-17" to "v0.7.8 - 2016-05-17" - Update cornerstoneWADOImageLoader from "v0.9.1 - 2016-02-09" to "v0.13.3 - 2016-06-02" - Update dicomParser from "v1.2.1 - 2016-02-07" to "v1.7.1 - 2016-06-09" --- .../cornerstone/client/cornerstoneTools.js | 481 +++++--- .../client/cornerstoneWADOImageLoader.js | 1031 ++++++++++++----- Packages/cornerstone/client/dicomParser.js | 704 ++++++++--- 3 files changed, 1609 insertions(+), 607 deletions(-) diff --git a/Packages/cornerstone/client/cornerstoneTools.js b/Packages/cornerstone/client/cornerstoneTools.js index b398ebe0e..62dfc9fa1 100644 --- a/Packages/cornerstone/client/cornerstoneTools.js +++ b/Packages/cornerstone/client/cornerstoneTools.js @@ -1,9 +1,13 @@ -/*! cornerstoneTools - v0.7.8 - 2016-03-17 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneTools */ +/*! cornerstoneTools - v0.7.8 - 2016-05-17 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneTools */ // Begin Source: src/header.js if (typeof cornerstone === 'undefined') { cornerstone = {}; } +if (typeof dicomParser === 'undefined') { + dicomParser = {}; +} + if (typeof cornerstoneTools === 'undefined') { cornerstoneTools = { referenceLines: {}, @@ -18,39 +22,64 @@ if (typeof cornerstoneTools === 'undefined') { 'use strict'; + var scrollTimeout; + var scrollTimeoutDelay = 1; + function mouseWheel(e) { - // !!!HACK/NOTE/WARNING!!! - // for some reason I am getting mousewheel and DOMMouseScroll events on my - // mac os x mavericks system when middle mouse button dragging. - // I couldn't find any info about this so this might break other systems - // webkit hack - if (e.originalEvent.type === 'mousewheel' && e.originalEvent.wheelDeltaY === 0) { - return; - } - // firefox hack - if (e.originalEvent.type === 'DOMMouseScroll' && e.originalEvent.axis === 1) { - return; - } + clearTimeout(scrollTimeout); - var element = e.currentTarget; - var startingCoords = cornerstone.pageToPixel(element, e.pageX || e.originalEvent.pageX, e.pageY || e.originalEvent.pageY); + scrollTimeout = setTimeout(function() { + // !!!HACK/NOTE/WARNING!!! + // for some reason I am getting mousewheel and DOMMouseScroll events on my + // mac os x mavericks system when middle mouse button dragging. + // I couldn't find any info about this so this might break other systems + // webkit hack + if (e.originalEvent.type === 'mousewheel' && e.originalEvent.wheelDeltaY === 0) { + return; + } + // firefox hack + if (e.originalEvent.type === 'DOMMouseScroll' && e.originalEvent.axis === 1) { + return; + } - e = window.event || e; // old IE support - var wheelDelta = e.wheelDelta || -e.detail || -e.originalEvent.detail; - var direction = Math.max(-1, Math.min(1, (wheelDelta))); + var element = e.currentTarget; - var mouseWheelData = { - element: element, - viewport: cornerstone.getViewport(element), - image: cornerstone.getEnabledElement(element).image, - direction: direction, - pageX: e.pageX || e.originalEvent.pageX, - pageY: e.pageY || e.originalEvent.pageY, - imageX: startingCoords.x, - imageY: startingCoords.y - }; + var x; + var y; - $(element).trigger('CornerstoneToolsMouseWheel', mouseWheelData); + if (e.pageX !== undefined && e.pageY !== undefined) { + x = e.pageX; + y = e.pageY; + } else if (e.originalEvent && + e.originalEvent.pageX !== undefined && + e.originalEvent.pageY !== undefined) { + x = e.originalEvent.pageX; + y = e.originalEvent.pageY; + } else { + // IE9 & IE10 + x = e.x; + y = e.y; + } + + var startingCoords = cornerstone.pageToPixel(element, x, y); + + e = window.event || e; // old IE support + var wheelDelta = e.wheelDelta || -e.detail || -e.originalEvent.detail || -e.originalEvent.deltaY; + var direction = Math.max(-1, Math.min(1, (wheelDelta))); + + var mouseWheelData = { + element: element, + viewport: cornerstone.getViewport(element), + image: cornerstone.getEnabledElement(element).image, + direction: direction, + pageX: x, + pageY: y, + imageX: startingCoords.x, + imageY: startingCoords.y + }; + + $(element).trigger('CornerstoneToolsMouseWheel', mouseWheelData); + }, scrollTimeoutDelay); } var mouseWheelEvents = 'mousewheel DOMMouseScroll'; @@ -2109,6 +2138,7 @@ if (typeof cornerstoneTools === 'undefined') { $(mouseEventData.element).on('CornerstoneToolsMouseMove', eventData, cornerstoneTools.arrowAnnotate.mouseMoveCallback); $(mouseEventData.element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.arrowAnnotate.mouseDownCallback); $(mouseEventData.element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.arrowAnnotate.mouseDownActivateCallback); + $(mouseEventData.element).on('CornerstoneToolsMouseDoubleClick', eventData, cornerstoneTools.arrowAnnotate.mouseDoubleClickCallback); } // associate this data with this imageId so we can render it and manipulate it @@ -2119,6 +2149,7 @@ if (typeof cornerstoneTools === 'undefined') { $(mouseEventData.element).off('CornerstoneToolsMouseMove', cornerstoneTools.arrowAnnotate.mouseMoveCallback); $(mouseEventData.element).off('CornerstoneToolsMouseDown', cornerstoneTools.arrowAnnotate.mouseDownCallback); $(mouseEventData.element).off('CornerstoneToolsMouseDownActivate', cornerstoneTools.arrowAnnotate.mouseDownActivateCallback); + $(mouseEventData.element).off('CornerstoneToolsMouseDoubleClick', cornerstoneTools.arrowAnnotate.mouseDoubleClickCallback); cornerstone.updateImage(mouseEventData.element); cornerstoneTools.moveNewHandle(mouseEventData, toolType, measurementData, measurementData.handles.end, function() { @@ -2886,11 +2917,12 @@ if (typeof cornerstoneTools === 'undefined') { toolCoords = cornerstone.pageToPixel(element, eventData.currentPoints.page.x, eventData.currentPoints.page.y - cornerstoneTools.textStyle.getFontSize() * 4); } else { - toolCoords = eventData.currentPoints.image; + toolCoords = cornerstone.pageToPixel(element, eventData.currentPoints.page.x, + eventData.currentPoints.page.y - cornerstoneTools.textStyle.getFontSize() / 2); } var storedPixels; - var text; + var text = ''; if (toolCoords.x < 0 || toolCoords.y < 0 || toolCoords.x >= image.columns || toolCoords.y >= image.rows) { @@ -2898,12 +2930,29 @@ if (typeof cornerstoneTools === 'undefined') { } if (image.color) { - storedPixels = cornerstone.getStoredPixels(element, toolCoords.x, toolCoords.y, 3, 1); + storedPixels = cornerstoneTools.getRGBPixels(element, toolCoords.x, toolCoords.y, 1, 1); text = 'R: ' + storedPixels[0] + ' G: ' + storedPixels[1] + ' B: ' + storedPixels[2]; } else { storedPixels = cornerstone.getStoredPixels(element, toolCoords.x, toolCoords.y, 1, 1); - var huValue = storedPixels[0] * image.slope + image.intercept; - text = parseFloat(huValue.toFixed(3)); + var sp = storedPixels[0]; + var mo = sp * eventData.image.slope + eventData.image.intercept; + var suv = cornerstoneTools.calculateSUV(eventData.image, sp); + + var modalityTag = 'x00080060'; + var modality; + if (eventData.image.data) { + modality = eventData.image.data.string(modalityTag); + } + + if (modality === 'CT') { + text += 'HU: '; + } + + // Draw text + text += parseFloat(mo.toFixed(2)); + if (suv) { + text += ' SUV: ' + parseFloat(suv.toFixed(2)); + } } // Prepare text @@ -2913,26 +2962,26 @@ if (typeof cornerstoneTools === 'undefined') { // Translate the x/y away from the cursor var translation; + var handleRadius = 6; + var width = context.measureText(text).width; + if (eventData.isTouchEvent === true) { - var handleRadius = 6; - var width = context.measureText(text).width; - translation = { x: -width / 2 - 5, y: -cornerstoneTools.textStyle.getFontSize() - 10 - 2 * handleRadius }; - - context.beginPath(); - context.strokeStyle = color; - context.arc(textCoords.x, textCoords.y, handleRadius, 0, 2 * Math.PI); - context.stroke(); } else { translation = { - x: 4, - y: -4 + x: 12, + y: -(cornerstoneTools.textStyle.getFontSize() + 10) / 2 }; } + context.beginPath(); + context.strokeStyle = color; + context.arc(textCoords.x, textCoords.y, handleRadius, 0, 2 * Math.PI); + context.stroke(); + cornerstoneTools.drawTextBox(context, text, textCoords.x + translation.x, textCoords.y + translation.y, color); context.restore(); } @@ -3133,118 +3182,208 @@ if (typeof cornerstoneTools === 'undefined') { } function onImageRendered(e, eventData) { - - // if we have no toolData for this element, return immediately as there is nothing to do + // If we have no toolData for this element, return immediately as there is nothing to do var toolData = cornerstoneTools.getToolState(e.currentTarget, toolType); if (!toolData) { return; } - // we have tool data for this element - iterate over each one and draw it + var image = eventData.image; + var element = eventData.element; + var lineWidth = cornerstoneTools.toolStyle.getToolWidth(); + var config = cornerstoneTools.ellipticalRoi.getConfiguration(); var context = eventData.canvasContext.canvas.getContext('2d'); context.setTransform(1, 0, 0, 1, 0, 0); - //activation color - var lineWidth = cornerstoneTools.toolStyle.getToolWidth(); - var config = cornerstoneTools.ellipticalRoi.getConfiguration(); + // Retrieve the image modality from its metadata, if available + var modalityTag = 'x00080060'; + var modality; + if (image.data) { + modality = image.data.string(modalityTag); + } + // If we have tool data for this element - iterate over each set and draw it for (var i = 0; i < toolData.data.length; i++) { context.save(); + var data = toolData.data[i]; + + // Apply any shadow settings defined in the tool configuration if (config && config.shadow) { context.shadowColor = config.shadowColor || '#000000'; context.shadowOffsetX = config.shadowOffsetX || 1; context.shadowOffsetY = config.shadowOffsetY || 1; } - var data = toolData.data[i]; - - //differentiate the color of activation tool + // Check which color the rendered tool should be var color = cornerstoneTools.toolColors.getColorIfActive(data.active); - // draw the ellipse - var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start); - var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end); + // Convert Image coordinates to Canvas coordinates given the element + var handleStartCanvas = cornerstone.pixelToCanvas(element, data.handles.start); + var handleEndCanvas = cornerstone.pixelToCanvas(element, data.handles.end); - var widthCanvas = Math.abs(handleStartCanvas.x - handleEndCanvas.x); - var heightCanvas = Math.abs(handleStartCanvas.y - handleEndCanvas.y); + // Retrieve the bounds of the ellipse (left, top, width, and height) + // in Canvas coordinates var leftCanvas = Math.min(handleStartCanvas.x, handleEndCanvas.x); var topCanvas = Math.min(handleStartCanvas.y, handleEndCanvas.y); + var widthCanvas = Math.abs(handleStartCanvas.x - handleEndCanvas.x); + var heightCanvas = Math.abs(handleStartCanvas.y - handleEndCanvas.y); + // Draw the ellipse on the canvas context.beginPath(); context.strokeStyle = color; context.lineWidth = lineWidth; cornerstoneTools.drawEllipse(context, leftCanvas, topCanvas, widthCanvas, heightCanvas); context.closePath(); - // draw the handles - var handleOptions = { - drawHandlesIfActive: (config && config.drawHandlesOnHover) - }; - - cornerstoneTools.drawHandles(context, eventData, data.handles, color, handleOptions); + // If the tool configuration specifies to only draw the handles on hover / active, + // follow this logic + if (config && config.drawHandlesOnHover) { + // Draw the handles if the tool is active + if (data.active === true) { + cornerstoneTools.drawHandles(context, eventData, data.handles, color); + } else { + // If the tool is inactive, draw the handles only if each specific handle is being + // hovered over + var handleOptions = { + drawHandlesIfActive: true + }; + cornerstoneTools.drawHandles(context, eventData, data.handles, color, handleOptions); + } + } else { + // If the tool has no configuration settings, always draw the handles + cornerstoneTools.drawHandles(context, eventData, data.handles, color); + } + // Define variables for the area and mean/standard deviation var area, - meanStdDev; + meanStdDev, + meanStdDevSUV; + // Perform a check to see if the tool has been invalidated. This is to prevent + // unnecessary re-calculation of the area, mean, and standard deviation if the + // image is re-rendered but the tool has not moved (e.g. during a zoom) if (!data.invalidated) { + // If the data is not invalidated, retrieve it from the toolData meanStdDev = data.meanStdDev; + meanStdDevSUV = data.meanStdDevSUV; area = data.area; } else { - // TODO: calculate this in web worker for large pixel counts... - var width = Math.abs(data.handles.start.x - data.handles.end.x); - var height = Math.abs(data.handles.start.y - data.handles.end.y); - var left = Math.min(data.handles.start.x, data.handles.end.x); - var top = Math.min(data.handles.start.y, data.handles.end.y); - - var pixels = cornerstone.getPixels(eventData.element, left, top, width, height); + // If the data has been invalidated, we need to calculate it again + // Retrieve the bounds of the ellipse in image coordinates var ellipse = { - left: left, - top: top, - width: width, - height: height + left: Math.min(data.handles.start.x, data.handles.end.x), + top: Math.min(data.handles.start.y, data.handles.end.y), + width: Math.abs(data.handles.start.x - data.handles.end.x), + height: Math.abs(data.handles.start.y - data.handles.end.y) }; - // Calculate the mean, stddev, and area - meanStdDev = calculateMeanStdDev(pixels, ellipse); - area = Math.PI * (width * eventData.image.columnPixelSpacing / 2) * (height * eventData.image.rowPixelSpacing / 2); + // First, make sure this is not a color image, since no mean / standard + // deviation will be calculated for color images. + if (!image.color) { + // Retrieve the array of pixels that the ellipse bounds cover + var pixels = cornerstone.getPixels(element, ellipse.left, ellipse.top, ellipse.width, ellipse.height); - data.invalidated = false; + // Calculate the mean & standard deviation from the pixels and the ellipse details + meanStdDev = calculateMeanStdDev(pixels, ellipse); + + if (modality === 'PT') { + // If the image is from a PET scan, use the DICOM tags to + // calculate the SUV from the mean and standard deviation. + + // Note that because we are using modality pixel values from getPixels, and + // the calculateSUV routine also rescales to modality pixel values, we are first + // returning the values to storedPixel values before calcuating SUV with them. + // TODO: Clean this up? Should we add an option to not scale in calculateSUV? + meanStdDevSUV = { + mean: cornerstoneTools.calculateSUV(image, (meanStdDev.mean - image.intercept) / image.slope), + stdDev: cornerstoneTools.calculateSUV(image, (meanStdDev.stdDev - image.intercept) / image.slope) + }; + } + + // If the mean and standard deviation values are sane, store them for later retrieval + if (meanStdDev && !isNaN(meanStdDev.mean)) { + data.meanStdDev = meanStdDev; + data.meanStdDevSUV = meanStdDevSUV; + } + } + + // Retrieve the pixel spacing values, and if they are not + // real non-zero values, set them to 1 + var columnPixelSpacing = image.columnPixelSpacing || 1; + var rowPixelSpacing = image.rowPixelSpacing || 1; + + // Calculate the image area from the ellipse dimensions and pixel spacing + area = Math.PI * (ellipse.width * columnPixelSpacing / 2) * (ellipse.height * rowPixelSpacing / 2); + + // If the area value is sane, store it for later retrieval if (!isNaN(area)) { data.area = area; } - if (!isNaN(meanStdDev.mean) && !isNaN(meanStdDev.stdDev)) { - data.meanStdDev = meanStdDev; - data.mean = meanStdDev.mean; - data.stdev = meanStdDev.stdev; - } + // Set the invalidated flag to false so that this data won't automatically be recalculated + data.invalidated = false; } + // Define an array to store the rows of text for the textbox var textLines = []; - if (meanStdDev) { - var meanText = 'Mean: ' + numberWithCommas(meanStdDev.mean.toFixed(2)); - textLines.push(meanText); - var stdDevText = 'StdDev: ' + numberWithCommas(meanStdDev.stdDev.toFixed(2)); + // If the mean and standard deviation values are present, display them + if (meanStdDev && meanStdDev.mean) { + // If the modality is CT, add HU to denote Hounsfield Units + var moSuffix = ''; + if (modality === 'CT') { + moSuffix = ' HU'; + } + + // Create a line of text to display the mean and any units that were specified (i.e. HU) + var meanText = 'Mean: ' + numberWithCommas(meanStdDev.mean.toFixed(2)) + moSuffix; + // Create a line of text to display the standard deviation and any units that were specified (i.e. HU) + var stdDevText = 'StdDev: ' + numberWithCommas(meanStdDev.stdDev.toFixed(2)) + moSuffix; + + // If this image has SUV values to display, concatenate them to the text line + if (meanStdDevSUV && meanStdDevSUV.mean !== undefined) { + var SUVtext = ' SUV: '; + meanText += SUVtext + numberWithCommas(meanStdDevSUV.mean.toFixed(2)); + stdDevText += SUVtext + numberWithCommas(meanStdDevSUV.stdDev.toFixed(2)); + } + + // Add these text lines to the array to be displayed in the textbox + textLines.push(meanText); textLines.push(stdDevText); } - if (area !== undefined && !isNaN(area)) { - // Char code 178 is a superscript 2 for mm^2 - var areaText = 'Area: ' + numberWithCommas(area.toFixed(2)) + ' mm' + String.fromCharCode(178); + // If the area is a sane value, display it + if (area) { + // Determine the area suffix based on the pixel spacing in the image. + // If pixel spacing is present, use millimeters. Otherwise, use pixels. + // This uses Char code 178 for a superscript 2 + var suffix = ' mm' + String.fromCharCode(178); + if (!image.rowPixelSpacing || !image.columnPixelSpacing) { + suffix = ' pixels' + String.fromCharCode(178); + } + + // Create a line of text to display the area and its units + var areaText = 'Area: ' + numberWithCommas(area.toFixed(2)) + suffix; + + // Add this text line to the array to be displayed in the textbox textLines.push(areaText); } + // If the textbox has not been moved by the user, it should be displayed on the right-most + // side of the tool. if (!data.handles.textBox.hasMoved) { + // Find the rightmost side of the ellipse at its vertical center, and place the textbox here + // Note that this calculates it in image coordinates data.handles.textBox.x = Math.max(data.handles.start.x, data.handles.end.x); data.handles.textBox.y = (data.handles.start.y + data.handles.end.y) / 2; } - var textCoords = cornerstone.pixelToCanvas(eventData.element, data.handles.textBox); + // Convert the textbox Image coordinates into Canvas coordinates + var textCoords = cornerstone.pixelToCanvas(element, data.handles.textBox); - // Draw text + // Set options for the textbox drawing function var options = { centering: { x: false, @@ -3252,18 +3391,30 @@ if (typeof cornerstoneTools === 'undefined') { } }; + // Draw the textbox and retrieves it's bounding box for mouse-dragging and highlighting var boundingBox = cornerstoneTools.drawTextBox(context, textLines, textCoords.x, textCoords.y, color, options); + // Store the bounding box data in the handle for mouse-dragging and highlighting data.handles.textBox.boundingBox = boundingBox; + // If the textbox has moved, we would like to draw a line linking it with the tool + // This section decides where to draw this line to on the Ellipse based on the location + // of the textbox relative to the ellipse. if (data.handles.textBox.hasMoved) { // Draw dashed link line between tool and text + + // The initial link position is at the center of the + // textbox. var link = { start: {}, - end: {} + end: { + x: textCoords.x, + y: textCoords.y + } }; + // First we calculate the ellipse points (top, left, right, and bottom) var ellipsePoints = [ { // Top middle point of ellipse x: leftCanvas + widthCanvas / 2, @@ -3280,14 +3431,13 @@ if (typeof cornerstoneTools === 'undefined') { // Right middle point of ellipse x: leftCanvas + widthCanvas, y: topCanvas + heightCanvas / 2 - }, - ]; - - link.end.x = textCoords.x; - link.end.y = textCoords.y; + } ]; + // We obtain the link starting point by finding the closest point on the ellipse to the + // center of the textbox link.start = cornerstoneMath.point.findClosestPoint(ellipsePoints, link.end); + // Next we calculate the corners of the textbox bounding box var boundingBoxPoints = [ { // Top middle point of bounding box x: boundingBox.left + boundingBox.width / 2, @@ -3304,11 +3454,13 @@ if (typeof cornerstoneTools === 'undefined') { // Right middle point of bounding box x: boundingBox.left + boundingBox.width, y: boundingBox.top + boundingBox.height / 2 - }, - ]; + }, ]; + // Now we recalculate the link endpoint by identifying which corner of the bounding box + // is closest to the start point we just calculated. link.end = cornerstoneMath.point.findClosestPoint(boundingBoxPoints, link.start); + // Finally we draw the dashed linking line context.beginPath(); context.strokeStyle = color; context.lineWidth = lineWidth; @@ -5484,11 +5636,26 @@ if (typeof cornerstoneTools === 'undefined') { x: mouseEventData.currentPoints.image.x, y: mouseEventData.currentPoints.image.y, highlight: true, - active: true + active: true, + hasBoundingBox: true } } }; + // Create a rectangle representing the image + var imageRect = { + left: 0, + top: 0, + width: mouseEventData.image.width, + height: mouseEventData.image.height + }; + + // Check if the current handle is outside the image, + // If it is, prevent the handle creation + if (!cornerstoneMath.point.insideRect(measurementData.handles.end, imageRect)) { + return; + } + // Update the current marker for the next marker var currentIndex = config.markers.indexOf(config.current); if (config.ascending) { @@ -5519,12 +5686,13 @@ if (typeof cornerstoneTools === 'undefined') { ///////// BEGIN IMAGE RENDERING /////// function pointNearTool(element, data, coords) { - if (!data.textBoundingBox) { + if (!data.handles.end.boundingBox) { return; } - var distanceToPoint = cornerstoneMath.rect.distanceToPoint(data.textBoundingBox, coords); - return (distanceToPoint < 10); + var distanceToPoint = cornerstoneMath.rect.distanceToPoint(data.handles.end.boundingBox, coords); + var insideBoundingBox = cornerstoneTools.pointInsideBoundingBox(data.handles.end, coords); + return (distanceToPoint < 10) || insideBoundingBox; } function onImageRendered(e, eventData) { @@ -5563,8 +5731,15 @@ if (typeof cornerstoneTools === 'undefined') { var textCoords = cornerstone.pixelToCanvas(eventData.element, data.handles.end); - var boundingBox = cornerstoneTools.drawTextBox(context, data.text, textCoords.x - data.textWidth / 2, textCoords.y, color); - data.textBoundingBox = boundingBox; + var options = { + centering: { + x: true, + y: true + } + }; + + var boundingBox = cornerstoneTools.drawTextBox(context, data.text, textCoords.x, textCoords.y - 10, color, options); + data.handles.end.boundingBox = boundingBox; context.restore(); } @@ -5583,10 +5758,15 @@ if (typeof cornerstoneTools === 'undefined') { data.active = false; cornerstone.updateImage(element); - $(element).on('CornerstoneToolsMouseMove', eventData, cornerstoneTools.textMarker.mouseMoveCallback); - $(element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.textMarker.mouseDownCallback); - $(element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.textMarker.mouseDownActivateCallback); - $(element).on('CornerstoneToolsMouseDoubleClick', eventData, cornerstoneTools.textMarker.mouseDoubleClickCallback); + + var mouseButtonData = { + mouseButtonMask: e.data.mouseButtonMask + }; + + $(element).on('CornerstoneToolsMouseMove', mouseButtonData, cornerstoneTools.textMarker.mouseMoveCallback); + $(element).on('CornerstoneToolsMouseDown', mouseButtonData, cornerstoneTools.textMarker.mouseDownCallback); + $(element).on('CornerstoneToolsMouseDownActivate', mouseButtonData, cornerstoneTools.textMarker.mouseDownActivateCallback); + $(element).on('CornerstoneToolsMouseDoubleClick', mouseButtonData, cornerstoneTools.textMarker.mouseDoubleClickCallback); } if (e.data && e.data.mouseButtonMask && !cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) { @@ -5645,10 +5825,6 @@ if (typeof cornerstoneTools === 'undefined') { $(element).on('CornerstoneToolsTouchPress', cornerstoneTools.textMarkerTouch.pressCallback); } - if (e.data && e.data.mouseButtonMask && !cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) { - return false; - } - var config = cornerstoneTools.textMarker.getConfiguration(); var coords = eventData.currentPoints.canvas; @@ -6322,6 +6498,13 @@ if (typeof cornerstoneTools === 'undefined') { function mouseWheelCallback(e, eventData) { var ticks = -eventData.direction / 4; + + // Allow inversion of the mouse wheel scroll via a configuration option + var config = cornerstoneTools.zoom.getConfiguration(); + if (config && config.invert) { + ticks *= -1; + } + var viewport = changeViewportScale(eventData.viewport, ticks); cornerstone.setViewport(eventData.element, viewport); } @@ -7729,6 +7912,7 @@ if (typeof cornerstoneTools === 'undefined') { numRequests[type]--; // console.log(numRequests); failCallback(error); + startAgain(); }); return; } @@ -7750,6 +7934,7 @@ if (typeof cornerstoneTools === 'undefined') { numRequests[type]--; // console.log(numRequests); failCallback(error); + startAgain(); }); } @@ -7853,7 +8038,7 @@ if (typeof cornerstoneTools === 'undefined') { if (!playClipToolData || !playClipToolData.data || !playClipToolData.data.length) { playClipData = { intervalId: undefined, - framesPerSecond: framesPerSecond || 30, + framesPerSecond: 30, lastFrameTimeStamp: undefined, frameRate: 0, loop: true @@ -7863,6 +8048,11 @@ if (typeof cornerstoneTools === 'undefined') { playClipData = playClipToolData.data[0]; } + // If a framerate is specified, update the playClipData now + if (framesPerSecond) { + playClipData.framesPerSecond = framesPerSecond; + } + // if already playing, do not set a new interval if (playClipData.intervalId !== undefined) { return; @@ -8143,16 +8333,6 @@ Display scroll progress bar across bottom of image. return; } - function doneCallback(image) { - //console.log('prefetch done: ' + image.imageId); - var imageIdIndex = stack.imageIds.indexOf(image.imageId); - removeFromList(imageIdIndex); - } - - function failCallback(error) { - console.log('prefetch errored: ' + error); - } - // Clear the requestPool of prefetch requests var requestPoolManager = cornerstoneTools.requestPoolManager; requestPoolManager.clearRequestStack(requestType); @@ -8164,6 +8344,22 @@ Display scroll progress bar across bottom of image. nextImageIdIndex, preventCache = false; + function doneCallback(image) { + //console.log('prefetch done: ' + image.imageId); + var imageIdIndex = stack.imageIds.indexOf(image.imageId); + removeFromList(imageIdIndex); + } + + // Retrieve the errorLoadingHandler if one exists + var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler(); + + function failCallback(error) { + console.log('prefetch errored: ' + error); + if (errorLoadingHandler) { + errorLoadingHandler(element, imageId, error, 'stackPrefetch'); + } + } + // Prefetch images around the current image (before and after) var lowerIndex = nearest.low; var higherIndex = nearest.high; @@ -10288,41 +10484,50 @@ Display scroll progress bar across bottom of image. 'use strict'; + // Returns a decimal value given a fractional value + function fracToDec(fractionalValue) { + return parseFloat('.' + fractionalValue); + } + function calculateSUV(image, storedPixelValue) { - // if no dicom data set, return undefined + if (!dicomParser) { + return; + } + + // if no dicom data set, return if (image.data === undefined) { - return undefined; + return; } // image must be PET if (image.data.string('x00080060') !== 'PT') { - return undefined; + return; } var modalityPixelValue = storedPixelValue * image.slope + image.intercept; var patientWeight = image.data.floatString('x00101030'); // in kg if (patientWeight === undefined) { - return undefined; + return; } var petSequence = image.data.elements.x00540016; if (petSequence === undefined) { - return undefined; + return; } petSequence = petSequence.items[0].dataSet; - var startTime = petSequence.time('x00181072'); + var startTime = dicomParser.parseTM(petSequence.string('x00181072')); var totalDose = petSequence.floatString('x00181074'); var halfLife = petSequence.floatString('x00181075'); - var acquisitionTime = image.data.time('x00080032'); + var seriesAcquisitionTime = dicomParser.parseTM(image.data.string('x00080031')); - if (!startTime || !totalDose || !halfLife || !acquisitionTime) { - return undefined; + if (!startTime || !totalDose || !halfLife || !seriesAcquisitionTime) { + return; } - var acquisitionTimeInSeconds = acquisitionTime.fractionalSeconds + acquisitionTime.seconds + acquisitionTime.minutes * 60 + acquisitionTime.hours * 60 * 60; - var injectionStartTimeInSeconds = startTime.fractionalSeconds + startTime.seconds + startTime.minutes * 60 + startTime.hours * 60 * 60; + var acquisitionTimeInSeconds = fracToDec(seriesAcquisitionTime.fractionalSeconds) + seriesAcquisitionTime.seconds + seriesAcquisitionTime.minutes * 60 + seriesAcquisitionTime.hours * 60 * 60; + var injectionStartTimeInSeconds = fracToDec(startTime.fractionalSeconds) + startTime.seconds + startTime.minutes * 60 + startTime.hours * 60 * 60; var durationInSeconds = acquisitionTimeInSeconds - injectionStartTimeInSeconds; var correctedDose = totalDose * Math.exp(-durationInSeconds * Math.log(2) / halfLife); var suv = modalityPixelValue * patientWeight / correctedDose * 1000; diff --git a/Packages/cornerstone/client/cornerstoneWADOImageLoader.js b/Packages/cornerstone/client/cornerstoneWADOImageLoader.js index 9cb4c493a..e607c5bcf 100644 --- a/Packages/cornerstone/client/cornerstoneWADOImageLoader.js +++ b/Packages/cornerstone/client/cornerstoneWADOImageLoader.js @@ -1,4 +1,4 @@ -/*! cornerstone-wado-image-loader - v0.9.1 - 2016-02-09 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneWADOImageLoader */ +/*! cornerstone-wado-image-loader - v0.13.3 - 2016-06-02 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneWADOImageLoader */ // // This is a cornerstone image loader for WADO-URI requests. It has limited support for compressed // transfer syntaxes, check here to see what is currently supported: @@ -19,14 +19,91 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ options : { // callback allowing customization of the xhr (e.g. adding custom auth headers, cors, etc) beforeSend: function (xhr) { + }, + // callback allowing modification of newly created image objects + imageCreated : function(image) { } - }, - multiFrameCacheHack : {} + } } }; } + +(function ($, cornerstone, cornerstoneWADOImageLoader) { + + "use strict"; + + // add a decache callback function to clear out our dataSetCacheManager + function addDecache(image) { + image.decache = function() { + //console.log('decache'); + var parsedImageId = cornerstoneWADOImageLoader.parseImageId(image.imageId); + cornerstoneWADOImageLoader.dataSetCacheManager.unload(parsedImageId.url); + }; + } + + function loadDataSetFromPromise(xhrRequestPromise, imageId, frame, sharedCacheKey) { + var deferred = $.Deferred(); + xhrRequestPromise.then(function(dataSet) { + var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, frame, sharedCacheKey); + imagePromise.then(function(image) { + addDecache(image); + deferred.resolve(image); + }, function(error) { + deferred.reject(error); + }); + }, function(error) { + deferred.reject(error); + }); + return deferred; + } + + function getLoaderForScheme(scheme) { + if(scheme === 'dicomweb' || scheme === 'wadouri') { + return cornerstoneWADOImageLoader.internal.xhrRequest; + } + else if(scheme === 'dicomfile') { + return cornerstoneWADOImageLoader.internal.loadFileRequest; + } + } + + function loadImage(imageId) { + var parsedImageId = cornerstoneWADOImageLoader.parseImageId(imageId); + + var loader = getLoaderForScheme(parsedImageId.scheme); + + // if the dataset for this url is already loaded, use it + if(cornerstoneWADOImageLoader.dataSetCacheManager.isLoaded(parsedImageId.url)) { + return loadDataSetFromPromise(cornerstoneWADOImageLoader.dataSetCacheManager.load(parsedImageId.url, loader), imageId, parsedImageId.frame, parsedImageId.url); + } + + // if multiframe, load the dataSet via the dataSetCacheManager to keep it in memory + if(parsedImageId.frame !== undefined) { + return loadDataSetFromPromise(cornerstoneWADOImageLoader.dataSetCacheManager.load(parsedImageId.url, loader), imageId, parsedImageId.frame, parsedImageId.url); + } + + // not multiframe, load it directly and let cornerstone cache manager its lifetime + var deferred = $.Deferred(); + var xhrRequestPromise = loader(parsedImageId.url, imageId); + xhrRequestPromise.then(function(dataSet) { + var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, parsedImageId.frame); + imagePromise.then(function(image) { + addDecache(image); + deferred.resolve(image); + }, function(error) { + deferred.reject(error); + }); + }, function(error) { + deferred.reject(error); + }); + return deferred; + } + + // register dicomweb and wadouri image loader prefixes + cornerstoneWADOImageLoader.internal.loadImage = loadImage; + +}($, cornerstone, cornerstoneWADOImageLoader)); (function (cornerstoneWADOImageLoader) { "use strict"; @@ -40,6 +117,15 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ } } + function convertYBRFull(dataSet, decodedImageFrame, rgbaBuffer) { + var planarConfiguration = dataSet.uint16('x00280006'); + if(planarConfiguration === 0) { + cornerstoneWADOImageLoader.convertYBRFullByPixel(decodedImageFrame, rgbaBuffer); + } else { + cornerstoneWADOImageLoader.convertYBRFullByPlane(decodedImageFrame, rgbaBuffer); + } + } + function convertColorSpace(canvas, dataSet, imageFrame) { // extract the fields we need var height = dataSet.uint16('x00280010'); @@ -52,7 +138,6 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ var context = canvas.getContext('2d'); var imageData = context.createImageData(width, height); - // convert based on the photometric interpretation var deferred = $.Deferred(); try { @@ -74,11 +159,11 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ } else if( photometricInterpretation === "YBR_FULL_422" ) { - cornerstoneWADOImageLoader.convertYBRFull(imageFrame, imageData.data); + convertYBRFull(dataSet, imageFrame, imageData.data); } else if(photometricInterpretation === "YBR_FULL" ) { - cornerstoneWADOImageLoader.convertYBRFull(imageFrame, imageData.data); + convertYBRFull(dataSet, imageFrame, imageData.data); } else { @@ -102,6 +187,17 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ function convertPALETTECOLOR( imageFrame, rgbaBuffer, dataSet ) { var len=dataSet.int16('x00281101',0); + + // Account for zero-values for the lookup table length + // + // "The first Palette Color Lookup Table Descriptor value is the number of entries in the lookup table. + // When the number of table entries is equal to 2^16 then this value shall be 0." + // + // See: http://dicom.nema.org/MEDICAL/Dicom/2015c/output/chtml/part03/sect_C.7.6.3.html#sect_C.7.6.3.1.5 + if (!len) { + len = 65536; + } + var start=dataSet.int16('x00281101',1); var bits=dataSet.int16('x00281101',2); var shift = (bits===8 ? 0 : 8 ); @@ -180,16 +276,14 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ var numPixels = imageFrame.length / 3; var rgbaIndex = 0; + var rIndex = 0; + var gIndex = numPixels; + var bIndex = numPixels*2; for(var i= 0; i < numPixels; i++) { - var rIndex = 0; - var gIndex = numPixels; - var bIndex = numPixels*2; - for(var i= 0; i < numPixels; i++) { - rgbaBuffer[rgbaIndex++] = imageFrame[rIndex++]; // red - rgbaBuffer[rgbaIndex++] = imageFrame[gIndex++]; // green - rgbaBuffer[rgbaIndex++] = imageFrame[bIndex++]; // blue - rgbaBuffer[rgbaIndex++] = 255; //alpha - } + rgbaBuffer[rgbaIndex++] = imageFrame[rIndex++]; // red + rgbaBuffer[rgbaIndex++] = imageFrame[gIndex++]; // green + rgbaBuffer[rgbaIndex++] = imageFrame[bIndex++]; // blue + rgbaBuffer[rgbaIndex++] = 255; //alpha } } @@ -202,7 +296,7 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ "use strict"; - function convertYBRFull(imageFrame, rgbaBuffer) { + function convertYBRFullByPixel(imageFrame, rgbaBuffer) { if(imageFrame === undefined) { throw "decodeRGB: ybrBuffer must not be undefined"; } @@ -225,7 +319,40 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ } // module exports - cornerstoneWADOImageLoader.convertYBRFull = convertYBRFull; + cornerstoneWADOImageLoader.convertYBRFullByPixel = convertYBRFullByPixel; +}(cornerstoneWADOImageLoader)); +/** + */ +(function (cornerstoneWADOImageLoader) { + + "use strict"; + + function convertYBRFullByPlane(imageFrame, rgbaBuffer) { + if (imageFrame === undefined) { + throw "decodeRGB: ybrBuffer must not be undefined"; + } + if (imageFrame.length % 3 !== 0) { + throw "decodeRGB: ybrBuffer length must be divisble by 3"; + } + + + var numPixels = imageFrame.length / 3; + var rgbaIndex = 0; + var yIndex = 0; + var cbIndex = numPixels; + var crIndex = numPixels * 2; + for (var i = 0; i < numPixels; i++) { + var y = imageFrame[yIndex++]; + var cb = imageFrame[cbIndex++]; + var cr = imageFrame[crIndex++]; + rgbaBuffer[rgbaIndex++] = y + 1.40200 * (cr - 128);// red + rgbaBuffer[rgbaIndex++] = y - 0.34414 * (cb - 128) - 0.71414 * (cr - 128); // green + rgbaBuffer[rgbaIndex++] = y + 1.77200 * (cb - 128); // blue + rgbaBuffer[rgbaIndex++] = 255; //alpha + } + } + // module exports + cornerstoneWADOImageLoader.convertYBRFullByPlane = convertYBRFullByPlane; }(cornerstoneWADOImageLoader)); (function (cornerstoneWADOImageLoader) { @@ -261,17 +388,104 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ // module exports cornerstoneWADOImageLoader.createImageObject = createImageObject; +}(cornerstoneWADOImageLoader)); +/** + * This object supports loading of DICOM P10 dataset from a uri and caching it so it can be accessed + * by the caller. This allows a caller to access the datasets without having to go through cornerstone's + * image loader mechanism. One reason a caller may need to do this is to determine the number of frames + * in a multiframe sop instance so it can create the imageId's correctly. + */ +(function (cornerstoneWADOImageLoader) { + + "use strict"; + + var loadedDataSets = {}; + var promises = {}; + + // returns true if the wadouri for the specified index has been loaded + function isLoaded(uri) { + return loadedDataSets[uri] !== undefined; + } + + // loads the dicom dataset from the wadouri sp + function load(uri, loadRequest) { + + // if already loaded return it right away + if(loadedDataSets[uri]) { + //console.log('using loaded dataset ' + uri); + var alreadyLoadedpromise = $.Deferred(); + loadedDataSets[uri].cacheCount++; + alreadyLoadedpromise.resolve(loadedDataSets[uri].dataSet); + return alreadyLoadedpromise; + } + + // if we are currently loading this uri, return its promise + if(promises[uri]) { + //console.log('returning existing load promise for ' + uri); + return promises[uri]; + } + + //console.log('loading ' + uri); + + // This uri is not loaded or being loaded, load it via an xhrRequest + var promise = loadRequest(uri); + promises[uri] = promise; + + // handle success and failure of the XHR request load + promise.then(function(dataSet) { + loadedDataSets[uri] = { + dataSet: dataSet, + cacheCount: 1 + }; + // done loading, remove the promise + delete promises[uri]; + }, function () { + }).always(function() { + // error thrown, remove the promise + delete promises[uri]; + }); + return promise; + } + + // remove the cached/loaded dicom dataset for the specified wadouri to free up memory + function unload(uri) { + //console.log('unload for ' + uri); + if(loadedDataSets[uri]) { + loadedDataSets[uri].cacheCount--; + if(loadedDataSets[uri].cacheCount === 0) { + //console.log('removing loaded dataset for ' + uri); + delete loadedDataSets[uri]; + } + } + } + + // removes all cached datasets from memory + function purge() { + loadedDataSets = {}; + promises = {}; + } + + // module exports + cornerstoneWADOImageLoader.dataSetCacheManager = { + isLoaded: isLoaded, + load: load, + unload: unload, + purge: purge + }; + }(cornerstoneWADOImageLoader)); (function ($, cornerstone, cornerstoneWADOImageLoader) { "use strict"; - function decodeJPEG2000(dataSet, frame) - { + + function decodeJpx(dataSet, frame) { var height = dataSet.uint16('x00280010'); var width = dataSet.uint16('x00280011'); - var compressedPixelData = dicomParser.readEncapsulatedPixelData(dataSet, dataSet.elements.x7fe00010, frame); + + var encodedImageFrame = cornerstoneWADOImageLoader.getEncodedImageFrame(dataSet, frame); + var jpxImage = new JpxImage(); - jpxImage.parse(compressedPixelData); + jpxImage.parse(encodedImageFrame); var j2kWidth = jpxImage.width; var j2kHeight = jpxImage.height; @@ -287,23 +501,156 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ } var tileComponents = jpxImage.tiles[0]; var pixelData = tileComponents.items; + return pixelData; } + var openJPEG; + + function decodeOpenJPEG(data, bytesPerPixel, signed) { + var dataPtr = openJPEG._malloc(data.length); + openJPEG.writeArrayToMemory(data, dataPtr); + + // create param outpout + var imagePtrPtr=openJPEG._malloc(4); + var imageSizePtr=openJPEG._malloc(4); + var imageSizeXPtr=openJPEG._malloc(4); + var imageSizeYPtr=openJPEG._malloc(4); + var imageSizeCompPtr=openJPEG._malloc(4); + + var t0 = Date.now(); + var ret = openJPEG.ccall('jp2_decode','number', ['number', 'number', 'number', 'number', 'number', 'number', 'number'], + [dataPtr, data.length, imagePtrPtr, imageSizePtr, imageSizeXPtr, imageSizeYPtr, imageSizeCompPtr]); + // add num vomp..etc + if(ret !== 0){ + console.log('[opj_decode] decoding failed!') + openJPEG._free(dataPtr); + openJPEG._free(openJPEG.getValue(imagePtrPtr, '*')); + openJPEG._free(imageSizeXPtr); + openJPEG._free(imageSizeYPtr); + openJPEG._free(imageSizePtr); + openJPEG._free(imageSizeCompPtr); + return undefined; + } + + var imagePtr = openJPEG.getValue(imagePtrPtr, '*') + + var image = { + length : openJPEG.getValue(imageSizePtr,'i32'), + sx : openJPEG.getValue(imageSizeXPtr,'i32'), + sy : openJPEG.getValue(imageSizeYPtr,'i32'), + nbChannels : openJPEG.getValue(imageSizeCompPtr,'i32'), // hard coded for now + perf_timetodecode : undefined, + pixelData : undefined + }; + + // Copy the data from the EMSCRIPTEN heap into the correct type array + var length = image.sx*image.sy*image.nbChannels; + var src32 = new Uint32Array(openJPEG.HEAP32.buffer, imagePtr, length); + if(bytesPerPixel === 1) { + if(Uint8Array.from) { + image.pixelData = Uint8Array.from(src32); + } else { + image.pixelData = new Uint8Array(length); + for(var i=0; i < length; i++) { + image.pixelData[i] = src32[i]; + } + } + } else { + if (signed) { + if(Int16Array.from) { + image.pixelData = Int16Array.from(src32); + } else { + image.pixelData = new Int16Array(length); + for(var i=0; i < length; i++) { + image.pixelData[i] = src32[i]; + } + } + } else { + if(Uint16Array.from) { + image.pixelData = Uint16Array.from(src32); + } else { + image.pixelData = new Uint16Array(length); + for(var i=0; i < length; i++) { + image.pixelData[i] = src32[i]; + } + } + } + } + + var t1 = Date.now(); + image.perf_timetodecode = t1-t0; + + // free + openJPEG._free(dataPtr); + openJPEG._free(imagePtrPtr); + openJPEG._free(imagePtr); + openJPEG._free(imageSizePtr); + openJPEG._free(imageSizeXPtr); + openJPEG._free(imageSizeYPtr); + openJPEG._free(imageSizeCompPtr); + + return image; + } + + function decodeOpenJpeg2000(dataSet, frame) { + var height = dataSet.uint16('x00280010'); + var width = dataSet.uint16('x00280011'); + + var encodedImageFrame = cornerstoneWADOImageLoader.getEncodedImageFrame(dataSet, frame); + + var bytesPerPixel = dataSet.uint16('x00280100') <= 8 ? 1 : 2; + var signed = dataSet.uint16('x00280103') ? true : false; + + var image = decodeOpenJPEG(encodedImageFrame, bytesPerPixel, signed); + var j2kWidth = image.sx; + var j2kHeight = image.sy; + + if(j2kWidth !== width) { + throw 'JPEG2000 decoder returned width of ' + j2kWidth + ', when ' + width + ' is expected'; + } + if(j2kHeight !== height) { + throw 'JPEG2000 decoder returned width of ' + j2kHeight + ', when ' + height + ' is expected'; + } + return image.pixelData; + } + + function decodeJPEG2000(dataSet, frame) + { + // Try to initialize OpenJPEG + if(typeof OpenJPEG !== 'undefined' && !openJPEG) { + openJPEG = OpenJPEG(); + if(!openJPEG || !openJPEG._jp2_decode) { + throw 'OpenJPEG failed to initialize'; + } + } + + // OpenJPEG2000 https://github.com/jpambrun/openjpeg + if(openJPEG && openJPEG._jp2_decode) { + return decodeOpenJpeg2000(dataSet, frame); + } + + // OHIF image-JPEG2000 https://github.com/OHIF/image-JPEG2000 + if(typeof JpxImage !== 'undefined') { + return decodeJpx(dataSet, frame); + } + throw 'No JPEG2000 decoder loaded'; + } + cornerstoneWADOImageLoader.decodeJPEG2000 = decodeJPEG2000; }($, cornerstone, cornerstoneWADOImageLoader)); (function ($, cornerstone, cornerstoneWADOImageLoader) { "use strict"; + function decodeJPEGBaseline(dataSet, frame) { - var pixelDataElement = dataSet.elements.x7fe00010; var height = dataSet.uint16('x00280010'); var width = dataSet.uint16('x00280011'); var bitsAllocated = dataSet.uint16('x00280100'); - var frameData = dicomParser.readEncapsulatedPixelData(dataSet, pixelDataElement, frame); + var encodedImageFrame = cornerstoneWADOImageLoader.getEncodedImageFrame(dataSet, frame); var jpeg = new JpegImage(); - jpeg.parse( frameData ); + jpeg.parse( encodedImageFrame ); if(bitsAllocated === 8) { return jpeg.getData(width, height); } @@ -349,9 +696,9 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ canvas.height = height; canvas.width = width; - var encodedPixelData = dicomParser.readEncapsulatedPixelData(dataSet, dataSet.elements.x7fe00010, frame); + var encodedImageFrame = cornerstoneWADOImageLoader.getEncodedImageFrame(dataSet, frame); - var imgBlob = new Blob([encodedPixelData], {type: "image/jpeg"}); + var imgBlob = new Blob([encodedImageFrame], {type: "image/jpeg"}); var r = new FileReader(); if(r.readAsBinaryString === undefined) { @@ -399,19 +746,133 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ cornerstoneWADOImageLoader.decodeJPEGBaseline8Bit = decodeJPEGBaseline8Bit; cornerstoneWADOImageLoader.isJPEGBaseline8Bit = isJPEGBaseline8Bit; +}(cornerstoneWADOImageLoader)); +"use strict"; +(function (cornerstoneWADOImageLoader) { + + + var charLS; + + function jpegLSDecode(data) { + + // prepare input parameters + var dataPtr = charLS._malloc(data.length); + charLS.writeArrayToMemory(data, dataPtr); + + // prepare output parameters + var imagePtrPtr=charLS._malloc(4); + var imageSizePtr=charLS._malloc(4); + var widthPtr=charLS._malloc(4); + var heightPtr=charLS._malloc(4); + var bitsPerSamplePtr=charLS._malloc(4); + var stridePtr=charLS._malloc(4); + var allowedLossyErrorPtr =charLS._malloc(4); + var componentsPtr=charLS._malloc(4); + + var interleaveModePtr=charLS._malloc(4); + + // Decode the image + var result = charLS.ccall( + 'jpegls_decode', + 'number', + ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'], + [dataPtr, data.length, imagePtrPtr, imageSizePtr, widthPtr, heightPtr, bitsPerSamplePtr, stridePtr, componentsPtr, allowedLossyErrorPtr, interleaveModePtr] + ); + + // Extract result values into object + var image = { + result : result, + width : charLS.getValue(widthPtr,'i32'), + height : charLS.getValue(heightPtr,'i32'), + bitsPerSample : charLS.getValue(bitsPerSamplePtr,'i32'), + stride : charLS.getValue(stridePtr,'i32'), + components : charLS.getValue(componentsPtr, 'i32'), + allowedLossyError : charLS.getValue(allowedLossyErrorPtr, 'i32'), + interleaveMode: charLS.getValue(interleaveModePtr, 'i32'), + pixelData: undefined + }; + + // Copy image from emscripten heap into appropriate array buffer type + var imagePtr = charLS.getValue(imagePtrPtr, '*'); + if(image.bitsPerSample <= 8) { + image.pixelData = new Uint8Array(image.width * image.height * image.components); + var src8 = new Uint8Array(charLS.HEAP8.buffer, imagePtr, image.pixelData.length); + image.pixelData.set(src8); + } else { + // I have seen 16 bit signed images, but I don't know if 16 bit unsigned is valid, hoping to get + // answer here: + // https://github.com/team-charls/charls/issues/14 + image.pixelData = new Int16Array(image.width * image.height * image.components); + var src16 = new Int16Array(charLS.HEAP16.buffer, imagePtr, image.pixelData.length); + image.pixelData.set(src16); + } + + // free memory and return image object + charLS._free(dataPtr); + charLS._free(imagePtr); + charLS._free(imagePtrPtr); + charLS._free(imageSizePtr); + charLS._free(widthPtr); + charLS._free(heightPtr); + charLS._free(bitsPerSamplePtr); + charLS._free(stridePtr); + charLS._free(componentsPtr); + charLS._free(interleaveModePtr); + + return image; + } + + function decodeJPEGLS(dataSet, frame) + { + // Try to initialize CharLS + if(CharLS && !charLS) { + charLS = CharLS(); + } + + // CharLS https://github.com/chafey/charls + if(!charLS || !charLS._jpegls_decode) { + throw 'No JPEG-LS decoder loaded'; + } + + var height = dataSet.uint16('x00280010'); + var width = dataSet.uint16('x00280011'); + + var encodedImageFrame = cornerstoneWADOImageLoader.getEncodedImageFrame(dataSet, frame); + + var image = jpegLSDecode(encodedImageFrame); + //console.log(image); + + // throw error if not success or too much data + if(image.result !== 0 && image.result !== 6) { + throw 'JPEG-LS decoder failed to decode frame (error code ' + image.result + ')'; + } + + // Sanity check the size + if(image.width !== width) { + throw 'JPEG-LS decoder returned width of ' + image.width + ', when ' + width + ' is expected'; + } + if(image.height !== height) { + throw 'JPEG-LS decoder returned width of ' + image.height + ', when ' + height + ' is expected'; + } + + return image.pixelData; + } + + // module exports + cornerstoneWADOImageLoader.decodeJPEGLS = decodeJPEGLS; + }(cornerstoneWADOImageLoader)); "use strict"; (function (cornerstoneWADOImageLoader) { function decodeJPEGLossless(dataSet, frame) { - var pixelDataElement = dataSet.elements.x7fe00010; var bitsAllocated = dataSet.uint16('x00280100'); var pixelRepresentation = dataSet.uint16('x00280103'); - var frameData = dicomParser.readEncapsulatedPixelData(dataSet, pixelDataElement, frame); + var encodedImageFrame = cornerstoneWADOImageLoader.getEncodedImageFrame(dataSet, frame); var byteOutput = bitsAllocated <= 8 ? 1 : 2; //console.time('jpeglossless'); var decoder = new jpeg.lossless.Decoder(); - var decompressedData = decoder.decode(frameData.buffer, frameData.byteOffset, frameData.length, byteOutput); + var decompressedData = decoder.decode(encodedImageFrame.buffer, encodedImageFrame.byteOffset, encodedImageFrame.length, byteOutput); //console.timeEnd('jpeglossless'); if (pixelRepresentation === 0) { if (byteOutput === 2) { @@ -438,7 +899,7 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ var samplesPerPixel = dataSet.uint16('x00280002'); var pixelDataElement = dataSet.elements.x7fe00010; - var frameData = dicomParser.readEncapsulatedPixelData(dataSet, pixelDataElement, frame); + var frameData = dicomParser.readEncapsulatedPixelDataFromFragments(dataSet, pixelDataElement, frame); var pixelFormat = cornerstoneWADOImageLoader.getPixelFormat(dataSet); @@ -555,28 +1016,6 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ { return cornerstoneWADOImageLoader.extractUncompressedPixels(dataSet, frame, true); } - // JPEG 2000 Lossless - else if(transferSyntax === "1.2.840.10008.1.2.4.90") - { - return cornerstoneWADOImageLoader.decodeJPEG2000(dataSet, frame); - } - // JPEG 2000 Lossy - else if(transferSyntax === "1.2.840.10008.1.2.4.91") - { - return cornerstoneWADOImageLoader.decodeJPEG2000(dataSet, frame); - } - /* Don't know if these work... - // JPEG 2000 Part 2 Multicomponent Image Compression (Lossless Only) - else if(transferSyntax === "1.2.840.10008.1.2.4.92") - { - return cornerstoneWADOImageLoader.decodeJPEG2000(dataSet, frame); - } - // JPEG 2000 Part 2 Multicomponent Image Compression - else if(transferSyntax === "1.2.840.10008.1.2.4.93") - { - return cornerstoneWADOImageLoader.decodeJPEG2000(dataSet, frame); - } - */ // RLE Lossless else if ( transferSyntax === "1.2.840.10008.1.2.5" ) { @@ -602,6 +1041,38 @@ if(typeof cornerstoneWADOImageLoader === 'undefined'){ { return cornerstoneWADOImageLoader.decodeJPEGLossless(dataSet, frame); } + // JPEG-LS Lossless Image Compression + else if ( transferSyntax === "1.2.840.10008.1.2.4.80" ) + { + return cornerstoneWADOImageLoader.decodeJPEGLS(dataSet, frame); + } + // JPEG-LS Lossy (Near-Lossless) Image Compression + else if ( transferSyntax === "1.2.840.10008.1.2.4.81" ) + { + return cornerstoneWADOImageLoader.decodeJPEGLS(dataSet, frame); + } + // JPEG 2000 Lossless + else if(transferSyntax === "1.2.840.10008.1.2.4.90") + { + return cornerstoneWADOImageLoader.decodeJPEG2000(dataSet, frame); + } + // JPEG 2000 Lossy + else if(transferSyntax === "1.2.840.10008.1.2.4.91") + { + return cornerstoneWADOImageLoader.decodeJPEG2000(dataSet, frame); + } + /* Don't know if these work... + // JPEG 2000 Part 2 Multicomponent Image Compression (Lossless Only) + else if(transferSyntax === "1.2.840.10008.1.2.4.92") + { + return cornerstoneWADOImageLoader.decodeJPEG2000(dataSet, frame); + } + // JPEG 2000 Part 2 Multicomponent Image Compression + else if(transferSyntax === "1.2.840.10008.1.2.4.93") + { + return cornerstoneWADOImageLoader.decodeJPEG2000(dataSet, frame); + } + */ else { if(console && console.log) { @@ -1828,6 +2299,8 @@ var JpegImage = (function jpegImage() { this.yLoc = 0; this.numBytes = 0; this.outputData = null; + this.restarting = false; + this.mask = 0; if (typeof numBytes !== "undefined") { this.numBytes = numBytes; @@ -1843,7 +2316,8 @@ var JpegImage = (function jpegImage() { 10, 19, 23, 32, 39, 45, 52, 54, 20, 22, 33, 38, 46, 51, 55, 60, 21, 34, 37, 47, 50, 56, 59, 61, 35, 36, 48, 49, 57, 58, 62, 63]; jpeg.lossless.Decoder.MAX_HUFFMAN_SUBTREE = 50; jpeg.lossless.Decoder.MSB = 0x80000000; - + jpeg.lossless.Decoder.RESTART_MARKER_BEGIN = 0xFFD0; + jpeg.lossless.Decoder.RESTART_MARKER_END = 0xFFD7; /*** Prototype Methods ***/ @@ -1977,7 +2451,13 @@ var JpegImage = (function jpegImage() { this.components = this.frame.components; if (!this.numBytes) { - this.numBytes = parseInt(this.precision / 8); + this.numBytes = parseInt(Math.ceil(this.precision / 8)); + } + + if (this.numBytes == 1) { + this.mask = 0xFF; + } else { + this.mask = 0xFFFF; } this.scan.read(this.stream); @@ -2061,6 +2541,7 @@ var JpegImage = (function jpegImage() { } for (mcuNum = 0; mcuNum < this.restartInterval; mcuNum+=1) { + this.restarting = (mcuNum == 0); current = this.decodeUnit(pred, temp, index); this.output(pred); @@ -2078,7 +2559,8 @@ var JpegImage = (function jpegImage() { } } - if (!((current >= 0xFFD0) && (current <= 0xFFD7))) { + if (!((current >= jpeg.lossless.Decoder.RESTART_MARKER_BEGIN) && + (current <= jpeg.lossless.Decoder.RESTART_MARKER_END))) { break; //current=MARKER } } @@ -2203,9 +2685,14 @@ var JpegImage = (function jpegImage() { jpeg.lossless.Decoder.prototype.decodeSingle = function (prev, temp, index) { /*jslint bitwise: true */ - var value, i; + var value, i, n, nRestart; - prev[0] = this.selector(); + if (this.restarting) { + this.restarting = false; + prev[0] = (1 << (this.frame.precision - 1)); + } else { + prev[0] = this.selector(); + } for (i = 0; i < this.nBlock[0]; i+=1) { value = this.getHuffmanValue(this.dcTab[0], temp, index); @@ -2213,7 +2700,14 @@ var JpegImage = (function jpegImage() { return value; } - prev[0] += this.getn(prev, value, temp, index); + n = this.getn(prev, value, temp, index); + nRestart = (n >> 8); + + if ((nRestart >= jpeg.lossless.Decoder.RESTART_MARKER_BEGIN) && (nRestart <= jpeg.lossless.Decoder.RESTART_MARKER_END)) { + return nRestart; + } + + prev[0] += n; } return 0; @@ -2445,7 +2939,7 @@ var JpegImage = (function jpegImage() { jpeg.lossless.Decoder.prototype.outputSingle = function (PRED) { if ((this.xLoc < this.xDim) && (this.yLoc < this.yDim)) { - this.setter((((this.yLoc * this.xDim) + this.xLoc)), PRED[0]); + this.setter((((this.yLoc * this.xDim) + this.xLoc)), this.mask & PRED[0]); this.xLoc+=1; @@ -2484,7 +2978,7 @@ var JpegImage = (function jpegImage() { jpeg.lossless.Decoder.prototype.getValue16 = function (index) { - return this.outputData.getInt16(index * 2, true); + return this.outputData.getInt16(index * 2, true) & this.mask; }; @@ -2496,7 +2990,7 @@ var JpegImage = (function jpegImage() { jpeg.lossless.Decoder.prototype.getValue8 = function (index) { - return this.outputData.getInt8(index); + return this.outputData.getInt8(index) & this.mask; }; @@ -2871,7 +3365,7 @@ var JpegImage = (function jpegImage() { "use strict"; - /*** Imports ***/ + /*** Imports ****/ var jpeg = jpeg || {}; jpeg.lossless = jpeg.lossless || {}; jpeg.lossless.ComponentSpec = jpeg.lossless.ComponentSpec || ((typeof require !== 'undefined') ? require('./component-spec.js') : null); @@ -3284,6 +3778,32 @@ var JpegImage = (function jpegImage() { }; +// http://stackoverflow.com/questions/18638900/javascript-crc32 + jpeg.lossless.Utils.makeCRCTable = function(){ + var c; + var crcTable = []; + for(var n =0; n < 256; n++){ + c = n; + for(var k =0; k < 8; k++){ + c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)); + } + crcTable[n] = c; + } + return crcTable; + }; + + jpeg.lossless.Utils.crc32 = function(dataView) { + var crcTable = jpeg.lossless.Utils.crcTable || (jpeg.lossless.Utils.crcTable = jpeg.lossless.Utils.makeCRCTable()); + var crc = 0 ^ (-1); + + for (var i = 0; i < dataView.byteLength; i++ ) { + crc = (crc >>> 8) ^ crcTable[(crc ^ dataView.getUint8(i)) & 0xFF]; + } + + return (crc ^ (-1)) >>> 0; + }; + + /*** Exports ***/ var moduleType = typeof module; @@ -3330,15 +3850,23 @@ var JpegImage = (function jpegImage() { var frameOffset = 0; if(pixelFormat === 1) { frameOffset = pixelDataOffset + frame * numPixels; + if(frameOffset >= dataSet.byteArray.length) { + throw 'frame exceeds size of pixelData'; + } return new Uint8Array(dataSet.byteArray.buffer, frameOffset, numPixels); } else if(pixelFormat === 2) { frameOffset = pixelDataOffset + frame * numPixels * 2; + if(frameOffset >= dataSet.byteArray.length) { + throw 'frame exceeds size of pixelData'; + } return new Uint16Array(dataSet.byteArray.buffer, frameOffset, numPixels); - return imageFrame; } else if(pixelFormat === 3) { frameOffset = pixelDataOffset + frame * numPixels * 2; + if(frameOffset >= dataSet.byteArray.length) { + throw 'frame exceeds size of pixelData'; + } return new Int16Array(dataSet.byteArray.buffer, frameOffset, numPixels); } throw "Unknown pixel format"; @@ -3346,6 +3874,57 @@ var JpegImage = (function jpegImage() { cornerstoneWADOImageLoader.extractUncompressedPixels = extractUncompressedPixels; }($, cornerstone, cornerstoneWADOImageLoader)); +/** + * Function to deal with extracting an image frame from an encapsulated data set. + */ +(function ($, cornerstone, cornerstoneWADOImageLoader) { + + "use strict"; + + function isMultiFrame(dataSet) { + var numberOfFrames = dataSet.intString('x00280008'); + return numberOfFrames > 1; + } + + function isFragmented(dataSet) { + var numberOfFrames = dataSet.intString('x00280008'); + var pixelDataElement = dataSet.elements.x7fe00010; + if(numberOfFrames != pixelDataElement.fragments.length) { + return true; + } + } + + function getEncodedImageFrameEmptyBasicOffsetTable(dataSet, frame) { + var pixelDataElement = dataSet.elements.x7fe00010; + + if(isMultiFrame(dataSet)) { + if(isFragmented(dataSet)) { + // decoding multi-frame with an empty basic offset table requires parsing the fragments + // to find frame boundaries. + throw 'multi-frame sop instance with no basic offset table is not currently supported'; + } + + // not fragmented, a frame maps to the fragment + return dicomParser.readEncapsulatedPixelDataFromFragments(dataSet, pixelDataElement, frame); + } + + // Single frame - all fragments are for the one image frame + var startFragment = 0; + var numFragments = pixelDataElement.fragments.length; + return dicomParser.readEncapsulatedPixelDataFromFragments(dataSet, pixelDataElement, startFragment, numFragments); + } + + function getEncodedImageFrame(dataSet, frame) { + // Empty basic offset table + if(!dataSet.elements.x7fe00010.basicOffsetTable.length) { + return getEncodedImageFrameEmptyBasicOffsetTable(dataSet, frame); + } + + // Basic Offset Table is not empty + return dicomParser.readEncapsulatedImageFrame(dataSet, dataSet.elements.x7fe00010, frame); + } + cornerstoneWADOImageLoader.getEncodedImageFrame = getEncodedImageFrame; +}($, cornerstone, cornerstoneWADOImageLoader)); (function (cornerstoneWADOImageLoader) { "use strict"; @@ -3486,77 +4065,8 @@ var JpegImage = (function jpegImage() { "use strict"; - function loadImage(imageId) { - // create a deferred object - var deferred = $.Deferred(); - - // build a url by parsing out the url scheme and frame index from the imageId - var firstColonIndex = imageId.indexOf(':'); - var url = imageId.substring(firstColonIndex + 1); - var frameIndex = url.indexOf('frame='); - var frame; - if(frameIndex !== -1) { - var frameStr = url.substr(frameIndex + 6); - frame = parseInt(frameStr); - url = url.substr(0, frameIndex-1); - } - - // if multiframe and cached, use the cached data set to extract the frame - if(frame !== undefined && - cornerstoneWADOImageLoader.internal.multiFrameCacheHack.hasOwnProperty(url)) - { - var dataSet = cornerstoneWADOImageLoader.internal.multiFrameCacheHack[url]; - var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, frame); - imagePromise.then(function(image) { - deferred.resolve(image); - }, function(error) { - deferred.reject(error); - }); - return deferred.promise(); - } - - var fileIndex = parseInt(url); - var file = cornerstoneWADOImageLoader.fileManager.get(fileIndex); - if(file === undefined) { - deferred.reject('unknown file index ' + url); - return deferred.promise(); - } - - - var fileReader = new FileReader(); - fileReader.onload = function(e) { - // Parse the DICOM File - var dicomPart10AsArrayBuffer = e.target.result; - var byteArray = new Uint8Array(dicomPart10AsArrayBuffer); - var dataSet = dicomParser.parseDicom(byteArray); - - // if multiframe, cache the parsed data set to speed up subsequent - // requests for the other frames - if(frame !== undefined) { - var dataSet = cornerstoneWADOImageLoader.internal.multiFrameCacheHack[url]; - var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, frame); - imagePromise.then(function(image) { - deferred.resolve(image); - }, function(error) { - deferred.reject(error); - }); - return deferred.promise(); - } - - var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, frame); - imagePromise.then(function(image) { - deferred.resolve(image); - }, function(error) { - deferred.reject(error); - }); - }; - fileReader.readAsArrayBuffer(file); - - return deferred.promise(); - } - - // registery dicomweb and wadouri image loader prefixes - cornerstone.registerImageLoader('dicomfile', loadImage); + // register dicomfile image loader prefixes + cornerstone.registerImageLoader('dicomfile', cornerstoneWADOImageLoader.internal.loadImage); }($, cornerstone, cornerstoneWADOImageLoader)); /** @@ -3593,6 +4103,35 @@ var JpegImage = (function jpegImage() { }; }(cornerstoneWADOImageLoader)); +(function ($, cornerstone, cornerstoneWADOImageLoader) { + + "use strict"; + + function loadFileRequest(uri) { + + var parsedImageId = cornerstoneWADOImageLoader.parseImageId(uri); + var fileIndex = parseInt(parsedImageId.url); + var file = cornerstoneWADOImageLoader.fileManager.get(fileIndex); + + // create a deferred object + var deferred = $.Deferred(); + + var fileReader = new FileReader(); + fileReader.onload = function (e) { + // Parse the DICOM File + var dicomPart10AsArrayBuffer = e.target.result; + var byteArray = new Uint8Array(dicomPart10AsArrayBuffer); + var dataSet = dicomParser.parseDicom(byteArray); + + deferred.resolve(dataSet); + }; + fileReader.readAsArrayBuffer(file); + + return deferred.promise(); + } + cornerstoneWADOImageLoader.internal.loadFileRequest = loadFileRequest; +}($, cornerstone, cornerstoneWADOImageLoader)); + (function (cornerstoneWADOImageLoader) { function checkToken(token, data, dataOffset) { @@ -3820,180 +4359,14 @@ var JpegImage = (function jpegImage() { cornerstone.registerImageLoader('wadors', loadImage); }($, cornerstone, cornerstoneWADOImageLoader)); -/** - * This object supports loading of DICOM P10 dataset from a uri and caching it so it can be accessed - * by the caller. This allows a caller to access the datasets without having to go through cornerstone's - * image loader mechanism. One reason a caller may need to do this is to determine the number of frames - * in a multiframe sop instance so it can create the imageId's correctly. - */ -(function (cornerstoneWADOImageLoader) { - - "use strict"; - - var loadedDataSets = {}; - var promises = {}; - - // returns true if the wadouri for the specified index has been loaded - function isLoaded(uri) { - return loadedDataSets[uri] !== undefined; - } - - // loads the dicom dataset from the wadouri sp - function load(uri) { - - // if already loaded return it right away - if(loadedDataSets[uri]) { - //console.log('using loaded dataset ' + uri); - var alreadyLoadedpromise = $.Deferred(); - loadedDataSets[uri].cacheCount++; - alreadyLoadedpromise.resolve(loadedDataSets[uri].dataSet); - return alreadyLoadedpromise; - } - - // if we are currently loading this uri, return its promise - if(promises[uri]) { - //console.log('returning existing load promise for ' + uri); - return promises[uri]; - } - - //console.log('loading ' + uri); - - // This uri is not loaded or being loaded, load it via an xhrRequest - var promise = cornerstoneWADOImageLoader.internal.xhrRequest(uri); - promises[uri] = promise; - - // handle success and failure of the XHR request load - promise.then(function(dataSet) { - loadedDataSets[uri] = { - dataSet: dataSet, - cacheCount: 1 - }; - // done loading, remove the promise - delete promises[uri]; - }, function () { - }).always(function() { - // error thrown, remove the promise - delete promises[uri]; - }); - return promise; - } - - // remove the cached/loaded dicom dataset for the specified wadouri to free up memory - function unload(uri) { - //console.log('unload for ' + uri); - if(loadedDataSets[uri]) { - loadedDataSets[uri].cacheCount--; - if(loadedDataSets[uri].cacheCount === 0) { - //console.log('removing loaded dataset for ' + uri); - delete loadedDataSets[uri]; - } - } - } - - // removes all cached datasets from memory - function purge() { - loadedDataSets = {}; - promises = {}; - } - - // module exports - cornerstoneWADOImageLoader.dataSetCacheManager = { - isLoaded: isLoaded, - load: load, - unload: unload, - purge: purge - }; - -}(cornerstoneWADOImageLoader)); (function ($, cornerstone, cornerstoneWADOImageLoader) { "use strict"; - function parseImageId(imageId) { - // build a url by parsing out the url scheme and frame index from the imageId - var firstColonIndex = imageId.indexOf(':'); - var url = imageId.substring(firstColonIndex + 1); - var frameIndex = url.indexOf('frame='); - var frame; - if(frameIndex !== -1) { - var frameStr = url.substr(frameIndex + 6); - frame = parseInt(frameStr); - url = url.substr(0, frameIndex-1); - } - return { - url : url, - frame: frame - }; - } - - // add a decache callback function to clear out our dataSetCacheManager - function addDecache(image) { - image.decache = function() { - //console.log('decache'); - var parsedImageId = parseImageId(image.imageId); - cornerstoneWADOImageLoader.dataSetCacheManager.unload(parsedImageId.url); - }; - } - - function loadDataSetFromPromise(xhrRequestPromise, imageId, frame, sharedCacheKey) { - var deferred = $.Deferred(); - xhrRequestPromise.then(function(dataSet) { - var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, frame, sharedCacheKey); - imagePromise.then(function(image) { - addDecache(image); - deferred.resolve(image); - }, function(error) { - deferred.reject(error); - }); - }, function(error) { - deferred.reject(error); - }); - return deferred; - } - - // Loads an image given an imageId - // wado url example: - // http://localhost:3333/wado?requestType=WADO&studyUID=1.3.6.1.4.1.25403.166563008443.5076.20120418075541.1&seriesUID=1.3.6.1.4.1.25403.166563008443.5076.20120418075541.2&objectUID=1.3.6.1.4.1.25403.166563008443.5076.20120418075557.1&contentType=application%2Fdicom&transferSyntax=1.2.840.10008.1.2.1 - // NOTE: supposedly the instance will be returned in Explicit Little Endian transfer syntax if you don't - // specify a transferSyntax but Osirix doesn't do this and seems to return it with the transfer syntax it is - // stored as. - function loadImage(imageId) { - // create a deferred object - - // build a url by parsing out the url scheme and frame index from the imageId - var parsedImageId = parseImageId(imageId); - - // if the dataset for this url is already loaded, use it - if(cornerstoneWADOImageLoader.dataSetCacheManager.isLoaded(parsedImageId.url)) { - return loadDataSetFromPromise(cornerstoneWADOImageLoader.dataSetCacheManager.load(parsedImageId.url), imageId, parsedImageId.frame, parsedImageId.url); - } - - // if multiframe, load the dataSet via the dataSetCacheManager to keep it in memory - if(parsedImageId.frame !== undefined) { - return loadDataSetFromPromise(cornerstoneWADOImageLoader.dataSetCacheManager.load(parsedImageId.url), imageId, parsedImageId.frame, parsedImageId.url); - } - - // not multiframe, load it directly and let cornerstone cache manager its lifetime - var deferred = $.Deferred(); - var xhrRequestPromise = cornerstoneWADOImageLoader.internal.xhrRequest(parsedImageId.url, imageId); - xhrRequestPromise.then(function(dataSet) { - var imagePromise = cornerstoneWADOImageLoader.createImageObject(dataSet, imageId, parsedImageId.frame); - imagePromise.then(function(image) { - addDecache(image); - deferred.resolve(image); - }, function(error) { - deferred.reject(error); - }); - }, function(error) { - deferred.reject(error); - }); - return deferred; - } - - // registery dicomweb and wadouri image loader prefixes - cornerstone.registerImageLoader('dicomweb', loadImage); - cornerstone.registerImageLoader('wadouri', loadImage); + // register dicomweb and wadouri image loader prefixes + cornerstone.registerImageLoader('dicomweb', cornerstoneWADOImageLoader.internal.loadImage); + cornerstone.registerImageLoader('wadouri', cornerstoneWADOImageLoader.internal.loadImage); }($, cornerstone, cornerstoneWADOImageLoader)); (function (cornerstoneWADOImageLoader) { @@ -4132,6 +4505,13 @@ var JpegImage = (function jpegImage() { image.windowWidth = 255; image.windowCenter = 128; } + + // invoke the callback to allow external code to modify the newly created image object if needed - e.g. + // apply vendor specific workarounds and such + if(cornerstoneWADOImageLoader.internal.options.imageCreated) { + cornerstoneWADOImageLoader.internal.options.imageCreated(image); + } + deferred.resolve(image); }, function(error) { deferred.reject(error); @@ -4190,6 +4570,14 @@ var JpegImage = (function jpegImage() { return lut; } + function isModalityLUTForDisplay(dataSet) { + // special case for XA and XRF + // https://groups.google.com/forum/#!searchin/comp.protocols.dicom/Modality$20LUT$20XA/comp.protocols.dicom/UBxhOZ2anJ0/D0R_QP8V2wIJ + var sopClassUid = dataSet.string('x00080016'); + return sopClassUid !== '1.2.840.10008.5.1.4.1.1.12.1' && // XA + sopClassUid !== '1.2.840.10008.5.1.4.1.1.12.2.1 '; // XRF + } + function makeGrayscaleImage(imageId, dataSet, frame, sharedCacheKey) { var deferred = $.Deferred(); @@ -4257,7 +4645,7 @@ var JpegImage = (function jpegImage() { // modality LUT var pixelRepresentation = dataSet.uint16('x00280103'); - if(dataSet.elements.x00283000) { + if(dataSet.elements.x00283000 && isModalityLUTForDisplay(dataSet)) { image.modalityLUT = getLUT(image, pixelRepresentation, dataSet.elements.x00283000.items[0].dataSet); } @@ -4282,6 +4670,12 @@ var JpegImage = (function jpegImage() { image.windowCenter = (maxVoi + minVoi) / 2; } + // invoke the callback to allow external code to modify the newly created image object if needed - e.g. + // apply vendor specific workarounds and such + if(cornerstoneWADOImageLoader.internal.options.imageCreated) { + cornerstoneWADOImageLoader.internal.options.imageCreated(image); + } + deferred.resolve(image); return deferred.promise(); } @@ -4292,9 +4686,34 @@ var JpegImage = (function jpegImage() { (function (cornerstoneWADOImageLoader) { "use strict"; + function parseImageId(imageId) { + // build a url by parsing out the url scheme and frame index from the imageId + var firstColonIndex = imageId.indexOf(':'); + var url = imageId.substring(firstColonIndex + 1); + var frameIndex = url.indexOf('frame='); + var frame; + if(frameIndex !== -1) { + var frameStr = url.substr(frameIndex + 6); + frame = parseInt(frameStr); + url = url.substr(0, frameIndex-1); + } + return { + scheme: imageId.substr(0, firstColonIndex), + url : url, + frame: frame + }; + } // module exports - cornerstoneWADOImageLoader.version = '0.9.1'; + cornerstoneWADOImageLoader.parseImageId = parseImageId; + +}(cornerstoneWADOImageLoader)); +(function (cornerstoneWADOImageLoader) { + + "use strict"; + + // module exports + cornerstoneWADOImageLoader.version = '0.13.3'; }(cornerstoneWADOImageLoader)); (function ($, cornerstone, cornerstoneWADOImageLoader) { @@ -4325,7 +4744,7 @@ var JpegImage = (function jpegImage() { } else { // request failed, reject the deferred - deferred.reject(xhr.response); + deferred.reject(xhr); } } }; diff --git a/Packages/cornerstone/client/dicomParser.js b/Packages/cornerstone/client/dicomParser.js index 1d56f7543..0ce1bca75 100644 --- a/Packages/cornerstone/client/dicomParser.js +++ b/Packages/cornerstone/client/dicomParser.js @@ -1,4 +1,4 @@ -/*! dicom-parser - v1.2.1 - 2016-02-07 | (c) 2014 Chris Hafey | https://github.com/chafey/dicomParser */ +/*! dicom-parser - v1.7.1 - 2016-06-09 | (c) 2014 Chris Hafey | https://github.com/chafey/dicomParser */ (function (root, factory) { // node.js @@ -22,17 +22,17 @@ } }(this, function () { - /** - * Parses a DICOM P10 byte array and returns a DataSet object with the parsed elements. If the options - * argument is supplied and it contains the untilTag property, parsing will stop once that - * tag is encoutered. This can be used to parse partial byte streams. - * - * @param byteArray the byte array - * @param options object to control parsing behavior (optional) - * @returns {DataSet} - * @throws error if an error occurs while parsing. The exception object will contain a property dataSet with the - * elements successfully parsed before the error. - */ +/** + * Parses a DICOM P10 byte array and returns a DataSet object with the parsed elements. If the options + * argument is supplied and it contains the untilTag property, parsing will stop once that + * tag is encoutered. This can be used to parse partial byte streams. + * + * @param byteArray the byte array + * @param options object to control parsing behavior (optional) + * @returns {DataSet} + * @throws error if an error occurs while parsing. The exception object will contain a property dataSet with the + * elements successfully parsed before the error. + */ var dicomParser = (function(dicomParser) { if(dicomParser === undefined) { @@ -66,18 +66,42 @@ var dicomParser = (function(dicomParser) { function getDataSetByteStream(transferSyntax, position) { if(transferSyntax === '1.2.840.10008.1.2.1.99') { - // https://github.com/nodeca/pako - if(typeof(pako) === "undefined") { - throw 'dicomParser.parseDicom: deflated transfer syntax encountered but pako not loaded'; - } - var deflated = byteArray.slice(position); - var inflated = pako.inflateRaw(deflated); + // if an infalter callback is registered, use it + if (options && options.inflater) { + var fullByteArrayCallback = options.inflater(byteArray, position); + return new dicomParser.ByteStream(dicomParser.littleEndianByteArrayParser, fullByteArrayCallback, 0); + } + // if running on node, use the zlib library to inflate + // http://stackoverflow.com/questions/4224606/how-to-check-whether-a-script-is-running-under-node-js + else if (typeof module !== 'undefined' && this.module !== module) { + // inflate it + var zlib = require('zlib'); + var deflatedBuffer = dicomParser.sharedCopy(byteArray, position, byteArray.length - position); + var inflatedBuffer = zlib.inflateRawSync(deflatedBuffer); - // create a single byte array with the full header bytes and the inflated bytes - var fullByteArray = new Uint8Array(inflated.length + position); - fullByteArray.set(byteArray.slice(0, position), 0); - fullByteArray.set(inflated, position); - return new dicomParser.ByteStream(dicomParser.littleEndianByteArrayParser, fullByteArray, 0); + // create a single byte array with the full header bytes and the inflated bytes + var fullByteArrayBuffer = dicomParser.alloc(byteArray, inflatedBuffer.length + position); + byteArray.copy(fullByteArrayBuffer, 0, 0, position); + inflatedBuffer.copy(fullByteArrayBuffer, position); + return new dicomParser.ByteStream(dicomParser.littleEndianByteArrayParser, fullByteArrayBuffer, 0); + } + // if pako is defined - use it. This is the web browser path + // https://github.com/nodeca/pako + else if(typeof pako !== "undefined") { + // inflate it + var deflated = byteArray.slice(position); + var inflated = pako.inflateRaw(deflated); + + // create a single byte array with the full header bytes and the inflated bytes + var fullByteArray = dicomParser.alloc(byteArray, inflated.length + position); + fullByteArray.set(byteArray.slice(0, position), 0); + fullByteArray.set(inflated, position); + return new dicomParser.ByteStream(dicomParser.littleEndianByteArrayParser, fullByteArray, 0); + } + // throw exception since no inflater is available + else { + throw 'dicomParser.parseDicom: no inflater available to handle deflate transfer syntax'; + } } if(transferSyntax === '1.2.840.10008.1.2.2') // explicit big endian { @@ -151,6 +175,124 @@ var dicomParser = (function(dicomParser) { return dicomParser; })(dicomParser); +/** + * Utility function for creating a basic offset table for JPEG transfer syntaxes + */ + +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + // Each JPEG image has an end of image marker 0xFFD9 + function isEndOfImageMarker(dataSet, position) { + return (dataSet.byteArray[position] === 0xFF && + dataSet.byteArray[position + 1] === 0xD9); + } + + // Each JPEG image has a start of image marker + function isStartOfImageMarker(dataSet, position) { + return (dataSet.byteArray[position] === 0xFF && + dataSet.byteArray[position + 1] === 0xD8); + } + + function isFragmentStartOfImage(dataSet, pixelDataElement, fragmentIndex) { + var fragment = pixelDataElement.fragments[fragmentIndex]; + if(isStartOfImageMarker(dataSet, fragment.position)) { + return true; + } + return false; + } + + function isFragmentEndOfImage(dataSet, pixelDataElement, fragmentIndex) { + var fragment = pixelDataElement.fragments[fragmentIndex]; + // Need to check the last two bytes and the last three bytes for marker since odd length + // fragments are zero padded + if(isEndOfImageMarker(dataSet, fragment.position + fragment.length - 2) || + isEndOfImageMarker(dataSet, fragment.position + fragment.length - 3)) { + return true; + } + return false; + } + + function findLastImageFrameFragmentIndex(dataSet, pixelDataElement, startFragment) { + for(var fragmentIndex=startFragment; fragmentIndex < pixelDataElement.fragments.length; fragmentIndex++) { + if(isFragmentEndOfImage(dataSet, pixelDataElement, fragmentIndex)) { + // if not last fragment, peek ahead to make sure the next fragment has a start of image marker just to + // be safe + if(fragmentIndex === pixelDataElement.fragments.length - 1 || + isFragmentStartOfImage(dataSet, pixelDataElement, fragmentIndex+1)) { + return fragmentIndex; + } + } + } + } + + /** + * Creates a basic offset table by scanning fragments for JPEG start of image and end Of Image markers + * @param {object} dataSet - the parsed dicom dataset + * @param {object} pixelDataElement - the pixel data element + * @param [fragments] - optional array of objects describing each fragment (offset, position, length) + * @returns {Array} basic offset table (array of offsets to beginning of each frame) + */ + dicomParser.createJPEGBasicOffsetTable = function(dataSet, pixelDataElement, fragments) { + // Validate parameters + if(dataSet === undefined) { + throw 'dicomParser.createJPEGBasicOffsetTable: missing required parameter dataSet'; + } + if(pixelDataElement === undefined) { + throw 'dicomParser.createJPEGBasicOffsetTable: missing required parameter pixelDataElement'; + } + if(pixelDataElement.tag !== 'x7fe00010') { + throw "dicomParser.createJPEGBasicOffsetTable: parameter 'pixelDataElement' refers to non pixel data tag (expected tag = x7fe00010'"; + } + if(pixelDataElement.encapsulatedPixelData !== true) { + throw "dicomParser.createJPEGBasicOffsetTable: parameter 'pixelDataElement' refers to pixel data element that does not have encapsulated pixel data"; + } + if(pixelDataElement.hadUndefinedLength !== true) { + throw "dicomParser.createJPEGBasicOffsetTable: parameter 'pixelDataElement' refers to pixel data element that does not have encapsulated pixel data"; + } + if(pixelDataElement.basicOffsetTable === undefined) { + throw "dicomParser.createJPEGBasicOffsetTable: parameter 'pixelDataElement' refers to pixel data element that does not have encapsulated pixel data"; + } + if(pixelDataElement.fragments === undefined) { + throw "dicomParser.createJPEGBasicOffsetTable: parameter 'pixelDataElement' refers to pixel data element that does not have encapsulated pixel data"; + } + if(pixelDataElement.fragments.length <= 0) { + throw "dicomParser.createJPEGBasicOffsetTable: parameter 'pixelDataElement' refers to pixel data element that does not have encapsulated pixel data"; + } + if(fragments && fragments.length <=0) { + throw "dicomParser.createJPEGBasicOffsetTable: parameter 'fragments' must not be zero length"; + } + + // Default values + fragments = fragments || pixelDataElement.fragments; + + var basicOffsetTable = []; + + var startFragmentIndex = 0; + // sanity check first fragment has a JPEG start of image marker + if(!isFragmentStartOfImage(dataSet, pixelDataElement, startFragmentIndex)) { + throw 'first fragment does not have JPEG start of image marker'; + } + + while(true) { + // Add the offset for the start fragment + basicOffsetTable.push(pixelDataElement.fragments[startFragmentIndex].offset); + var endFragmentIndex = findLastImageFrameFragmentIndex(dataSet, pixelDataElement, startFragmentIndex); + if(endFragmentIndex === undefined || endFragmentIndex === pixelDataElement.fragments.length -1) { + return basicOffsetTable; + } + startFragmentIndex = endFragmentIndex + 1; + } + }; + + return dicomParser; +}(dicomParser)); var dicomParser = (function (dicomParser) { "use strict"; @@ -533,94 +675,31 @@ var dicomParser = (function (dicomParser) dicomParser = {}; } - function getPixelDataFromFragments(byteStream, fragments, bufferSize) - { - // if there is only one fragment, return a view on this array to avoid copying - if(fragments.length === 1) { - return new Uint8Array(byteStream.byteArray.buffer, fragments[0].dataOffset, fragments[0].length); - } - - // more than one fragment, combine all of the fragments into one buffer - var pixelData = new Uint8Array(bufferSize); - var pixelDataIndex = 0; - for(var i=0; i < fragments.length; i++) { - var fragmentOffset = fragments[i].dataOffset; - for(var j=0; j < fragments[i].length; j++) { - pixelData[pixelDataIndex++] = byteStream.byteArray[fragmentOffset++]; - } - } - - return pixelData; - } - - function readFragmentsUntil(byteStream, endOfFrame) { - // Read fragments until we reach endOfFrame - var fragments = []; - var bufferSize = 0; - while(byteStream.position < endOfFrame && byteStream.position < byteStream.byteArray.length) { - var fragment = dicomParser.readSequenceItem(byteStream); - // NOTE: we only encounter this for the sequence delimiter tag when extracting the last frame - if(fragment.tag === 'xfffee0dd') { - break; - } - fragments.push(fragment); - byteStream.seek(fragment.length); - bufferSize += fragment.length; - } - - // Convert the fragments into a single pixelData buffer - var pixelData = getPixelDataFromFragments(byteStream, fragments, bufferSize); - return pixelData; - } - - function readEncapsulatedPixelDataWithBasicOffsetTable(pixelDataElement, byteStream, frame) { - // validate that we have an offset for this frame - var numFrames = pixelDataElement.basicOffsetTable.length; - if(frame > numFrames) { - throw "dicomParser.readEncapsulatedPixelData: parameter frame exceeds number of frames in basic offset table"; - } - - // move to the start of this frame - var frameOffset = pixelDataElement.basicOffsetTable[frame]; - var firstFragment = byteStream.position; - byteStream.seek(frameOffset); - - // Find the end of this frame - var endOfFrameOffset = pixelDataElement.basicOffsetTable[frame + 1]; - if(endOfFrameOffset === undefined) { // special case for last frame - endOfFrameOffset = byteStream.position + pixelDataElement.length; - } else { - endOfFrameOffset += firstFragment; - } - - // read this frame - var pixelData = readFragmentsUntil(byteStream, endOfFrameOffset); - return pixelData; - } - - function readEncapsulatedDataNoBasicOffsetTable(pixelDataElement, byteStream, frame) { - // if the basic offset table is empty, this is a single frame so make sure the requested - // frame is 0 - if(frame !== 0) { - throw 'dicomParser.readEncapsulatedPixelData: non zero frame specified for single frame encapsulated pixel data'; - } - - // read this frame - var endOfFrame = byteStream.position + pixelDataElement.length; - var pixelData = readFragmentsUntil(byteStream, endOfFrame); - return pixelData; - } + var deprecatedNoticeLogged = false; /** - * Returns the pixel data for the specified frame in an encapsulated pixel data element + * Returns the pixel data for the specified frame in an encapsulated pixel data element. If no basic offset + * table is present, it assumes that all fragments are for one frame. Note that this assumption/logic is not + * valid for multi-frame instances so this function has been deprecated and will eventually be removed. Code + * should be updated to use readEncapsulatedPixelDataFromFragments() or readEncapsulatedImageFrame() * + * @deprecated since version 1.6 - use readEncapsulatedPixelDataFromFragments() or readEncapsulatedImageFrame() * @param dataSet - the dataSet containing the encapsulated pixel data * @param pixelDataElement - the pixel data element (x7fe00010) to extract the frame from * @param frame - the zero based frame index - * @returns Uint8Array with the encapsulated pixel data + * @returns {object} with the encapsulated pixel data */ + + dicomParser.readEncapsulatedPixelData = function(dataSet, pixelDataElement, frame) { + if(!deprecatedNoticeLogged) { + deprecatedNoticeLogged = true; + if(console && console.log) { + console.log("WARNING: dicomParser.readEncapsulatedPixelData() has been deprecated"); + } + } + if(dataSet === undefined) { throw "dicomParser.readEncapsulatedPixelData: missing required parameter 'dataSet'"; } @@ -649,30 +728,54 @@ var dicomParser = (function (dicomParser) throw "dicomParser.readEncapsulatedPixelData: parameter 'frame' must be >= 0"; } - // seek past the basic offset table (no need to parse it again since we already have) - var byteStream = new dicomParser.ByteStream(dataSet.byteArrayParser, dataSet.byteArray, pixelDataElement.dataOffset); - var basicOffsetTable = dicomParser.readSequenceItem(byteStream); - if(basicOffsetTable.tag !== 'xfffee000') - { - throw "dicomParser.readEncapsulatedPixelData: missing basic offset table xfffee000"; - } - byteStream.seek(basicOffsetTable.length); - - // If the basic offset table is empty (no entries), it is a single frame. If it is not empty, - // it has at least one frame so use the basic offset table to find the bytes + // If the basic offset table is not empty, we can extract the frame if(pixelDataElement.basicOffsetTable.length !== 0) { - return readEncapsulatedPixelDataWithBasicOffsetTable(pixelDataElement, byteStream, frame); + return dicomParser.readEncapsulatedImageFrame(dataSet, pixelDataElement, frame); } else { - return readEncapsulatedDataNoBasicOffsetTable(pixelDataElement, byteStream, frame); + // No basic offset table, assume all fragments are for one frame - NOTE that this is NOT a valid + // assumption but is the original behavior so we are keeping it for now + return dicomParser.readEncapsulatedPixelDataFromFragments(dataSet, pixelDataElement, 0, pixelDataElement.fragments.length); } }; return dicomParser; }(dicomParser)); +/** + * + * Internal helper function to allocate new byteArray buffers + */ +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + /** + * Creates a new byteArray of the same type (Uint8Array or Buffer) of the specified length. + * @param byteArray the underlying byteArray (either Uint8Array or Buffer) + * @param length number of bytes of the Byte Array + * @returns {object} Uint8Array or Buffer depending on the type of byteArray + */ + dicomParser.alloc = function(byteArray, length) { + if (typeof Buffer !== 'undefined' && byteArray instanceof Buffer) { + return Buffer.alloc(length); + } + else if(byteArray instanceof Uint8Array) { + return new Uint8Array(length); + } else { + throw 'dicomParser.alloc: unknown type for byteArray'; + } + }; + + return dicomParser; +}(dicomParser)); /** * Internal helper functions for parsing different types from a big-endian byte array */ @@ -876,7 +979,7 @@ var dicomParser = (function (dicomParser) { if(length < 0) { - throw 'readFixedString - length cannot be less than 0'; + throw 'dicomParser.readFixedString - length cannot be less than 0'; } if(position + length > byteArray.length) { @@ -884,9 +987,10 @@ var dicomParser = (function (dicomParser) } var result = ""; + var byte; for(var i=0; i < length; i++) { - var byte = byteArray[position + i]; + byte = byteArray[position + i]; if(byte === 0) { position += length; return result; @@ -966,7 +1070,7 @@ var dicomParser = (function (dicomParser) { if(this.position + offset < 0) { - throw "cannot seek to position < 0"; + throw "dicomParser.ByteStream.prototype.seek: cannot seek to position < 0"; } this.position += offset; }; @@ -980,9 +1084,9 @@ var dicomParser = (function (dicomParser) dicomParser.ByteStream.prototype.readByteStream = function(numBytes) { if(this.position + numBytes > this.byteArray.length) { - throw 'readByteStream - buffer overread'; + throw 'dicomParser.ByteStream.prototype.readByteStream: readByteStream - buffer overread'; } - var byteArrayView = new Uint8Array(this.byteArray.buffer, this.position, numBytes); + var byteArrayView = dicomParser.sharedCopy(this.byteArray, this.position, numBytes); this.position += numBytes; return new dicomParser.ByteStream(this.byteArrayParser, byteArrayView); }; @@ -1704,7 +1808,7 @@ var dicomParser = (function (dicomParser) while(byteStream.position < maxPosition) { - var element = dicomParser.readDicomElementImplicit(byteStream, options.untilTag); + var element = dicomParser.readDicomElementImplicit(byteStream, options.untilTag, options.vrCallback); elements[element.tag] = element; if(element.tag === options.untilTag) { return; @@ -1817,7 +1921,25 @@ var dicomParser = (function (dicomParser) dicomParser = {}; } - dicomParser.readDicomElementImplicit = function(byteStream, untilTag) + function isSequence(element, byteStream, vrCallback) { + // if a data dictionary callback was provided, use that to verify that the element is a sequence. + if (typeof vrCallback !== 'undefined') { + return (vrCallback(element.tag) === 'SQ'); + } + if ((byteStream.position + 4) <= byteStream.byteArray.length) { + var nextTag = dicomParser.readTag(byteStream); + byteStream.seek(-4); + // Item start tag (fffe,e000) or sequence delimiter (i.e. end of sequence) tag (0fffe,e0dd) + // These are the tags that could potentially be found directly after a sequence start tag (the delimiter + // is found in the case of an empty sequence). This is not 100% safe because a non-sequence item + // could have data that has these bytes, but this is how to do it without a data dictionary. + return (nextTag === 'xfffee000') || (nextTag === 'xfffee0dd'); + } + byteStream.warnings.push('eof encountered before finding sequence item tag or sequence delimiter tag in peeking to determine VR'); + return false; + } + + dicomParser.readDicomElementImplicit = function(byteStream, untilTag, vrCallback) { if(byteStream === undefined) { @@ -1830,8 +1952,7 @@ var dicomParser = (function (dicomParser) dataOffset : byteStream.position }; - if(element.length === 4294967295) - { + if(element.length === 4294967295) { element.hadUndefinedLength = true; } @@ -1839,31 +1960,15 @@ var dicomParser = (function (dicomParser) return element; } - // peek ahead at the next tag to see if it looks like a sequence. This is not 100% - // safe because a non sequence item could have data that has these bytes, but this - // is how to do it without a data dictionary. - if ((byteStream.position + 4) <= byteStream.byteArray.length) { - var nextTag = dicomParser.readTag(byteStream); - byteStream.seek(-4); - - // zero length sequence - if (element.hadUndefinedLength && nextTag === 'xfffee0dd') { - element.length = 0; - byteStream.seek(8); - return element; - } - - if (nextTag === 'xfffee000') { - // parse the sequence - dicomParser.readSequenceItemsImplicit(byteStream, element); - //element.length = byteStream.byteArray.length - element.dataOffset; - return element; - } + if (isSequence(element, byteStream, vrCallback)) { + // parse the sequence + dicomParser.readSequenceItemsImplicit(byteStream, element); + return element; } // if element is not a sequence and has undefined length, we have to // scan the data for a magic number to figure out when it ends. - if(element.length === 4294967295) + if(element.hadUndefinedLength) { dicomParser.findItemDelimitationItemAndSetElementLength(byteStream, element); return element; @@ -1877,6 +1982,236 @@ var dicomParser = (function (dicomParser) return dicomParser; }(dicomParser)); +/** + * Functionality for extracting encapsulated pixel data + */ + +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + function findFragmentIndexWithOffset(fragments, offset) { + for(var i=0; i < fragments.length; i++) { + if(fragments[i].offset === offset) { + return i; + } + } + } + + function calculateNumberOfFragmentsForFrame(frameIndex, basicOffsetTable, fragments, startFragmentIndex) { + // special case for last frame + if(frameIndex === basicOffsetTable.length -1) { + return fragments.length - startFragmentIndex; + } + + // iterate through each fragment looking for the one matching the offset for the next frame + var nextFrameOffset = basicOffsetTable[frameIndex + 1]; + for(var i=startFragmentIndex + 1; i < fragments.length; i++) { + if(fragments[i].offset === nextFrameOffset) { + return i - startFragmentIndex; + } + } + + throw "dicomParser.calculateNumberOfFragmentsForFrame: could not find fragment with offset matching basic offset table"; + } + + /** + * Returns the pixel data for the specified frame in an encapsulated pixel data element that has a non + * empty basic offset table. Note that this function will fail if the basic offset table is empty - in that + * case you need to determine which fragments map to which frames and read them using + * readEncapsulatedPixelDataFromFragments(). Also see the function createJEPGBasicOffsetTable() to see + * how a basic offset table can be created for JPEG images + * + * @param dataSet - the dataSet containing the encapsulated pixel data + * @param pixelDataElement - the pixel data element (x7fe00010) to extract the frame from + * @param frameIndex - the zero based frame index + * @param [basicOffsetTable] - optional array of starting offsets for frames + * @param [fragments] - optional array of objects describing each fragment (offset, position, length) + * @returns {object} with the encapsulated pixel data + */ + dicomParser.readEncapsulatedImageFrame = function(dataSet, pixelDataElement, frameIndex, basicOffsetTable, fragments) + { + // default parameters + basicOffsetTable = basicOffsetTable || pixelDataElement.basicOffsetTable; + fragments = fragments || pixelDataElement.fragments; + + // Validate parameters + if(dataSet === undefined) { + throw "dicomParser.readEncapsulatedImageFrame: missing required parameter 'dataSet'"; + } + if(pixelDataElement === undefined) { + throw "dicomParser.readEncapsulatedImageFrame: missing required parameter 'pixelDataElement'"; + } + if(frameIndex === undefined) { + throw "dicomParser.readEncapsulatedImageFrame: missing required parameter 'frameIndex'"; + } + if(basicOffsetTable === undefined) { + throw "dicomParser.readEncapsulatedImageFrame: parameter 'pixelDataElement' does not have basicOffsetTable"; + } + if(pixelDataElement.tag !== 'x7fe00010') { + throw "dicomParser.readEncapsulatedImageFrame: parameter 'pixelDataElement' refers to non pixel data tag (expected tag = x7fe00010'"; + } + if(pixelDataElement.encapsulatedPixelData !== true) { + throw "dicomParser.readEncapsulatedImageFrame: parameter 'pixelDataElement' refers to pixel data element that does not have encapsulated pixel data"; + } + if(pixelDataElement.hadUndefinedLength !== true) { + throw "dicomParser.readEncapsulatedImageFrame: parameter 'pixelDataElement' refers to pixel data element that does not have undefined length"; + } + if(pixelDataElement.fragments === undefined) { + throw "dicomParser.readEncapsulatedImageFrame: parameter 'pixelDataElement' refers to pixel data element that does not have fragments"; + } + if(basicOffsetTable.length === 0) { + throw "dicomParser.readEncapsulatedImageFrame: basicOffsetTable has zero entries"; + } + if(frameIndex < 0) { + throw "dicomParser.readEncapsulatedImageFrame: parameter 'frameIndex' must be >= 0"; + } + if(frameIndex >= basicOffsetTable.length) { + throw "dicomParser.readEncapsulatedImageFrame: parameter 'frameIndex' must be < basicOffsetTable.length"; + } + + // find starting fragment based on the offset for the frame in the basic offset table + var offset = basicOffsetTable[frameIndex]; + var startFragmentIndex = findFragmentIndexWithOffset(fragments, offset); + if(startFragmentIndex === undefined) { + throw "dicomParser.readEncapsulatedImageFrame: unable to find fragment that matches basic offset table entry"; + } + + // calculate the number of fragments for this frame + var numFragments = calculateNumberOfFragmentsForFrame(frameIndex, basicOffsetTable, fragments, startFragmentIndex); + + // now extract the frame from the fragments + return dicomParser.readEncapsulatedPixelDataFromFragments(dataSet, pixelDataElement, startFragmentIndex, numFragments, fragments); + }; + + return dicomParser; +}(dicomParser)); + +/** + * Functionality for extracting encapsulated pixel data + */ + +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + function calculateBufferSize(fragments, startFragment, numFragments) { + var bufferSize = 0; + for(var i=startFragment; i < startFragment + numFragments; i++) { + bufferSize += fragments[i].length; + } + return bufferSize; + } + + /** + * Returns the encapsulated pixel data from the specified fragments. Use this function when you know + * the fragments you want to extract data from. See + * + * @param dataSet - the dataSet containing the encapsulated pixel data + * @param pixelDataElement - the pixel data element (x7fe00010) to extract the fragment data from + * @param startFragmentIndex - zero based index of the first fragment to extract from + * @param [numFragments] - the number of fragments to extract from, default is 1 + * @param [fragments] - optional array of objects describing each fragment (offset, position, length) + * @returns {object} byte array with the encapsulated pixel data + */ + dicomParser.readEncapsulatedPixelDataFromFragments = function(dataSet, pixelDataElement, startFragmentIndex, numFragments, fragments) + { + // default values + numFragments = numFragments || 1; + fragments = fragments || pixelDataElement.fragments; + + // check parameters + if(dataSet === undefined) { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: missing required parameter 'dataSet'"; + } + if(pixelDataElement === undefined) { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: missing required parameter 'pixelDataElement'"; + } + if(startFragmentIndex === undefined) { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: missing required parameter 'startFragmentIndex'"; + } + if(numFragments === undefined) { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: missing required parameter 'numFragments'"; + } + if(pixelDataElement.tag !== 'x7fe00010') { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: parameter 'pixelDataElement' refers to non pixel data tag (expected tag = x7fe00010'"; + } + if(pixelDataElement.encapsulatedPixelData !== true) { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: parameter 'pixelDataElement' refers to pixel data element that does not have encapsulated pixel data"; + } + if(pixelDataElement.hadUndefinedLength !== true) { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: parameter 'pixelDataElement' refers to pixel data element that does not have encapsulated pixel data"; + } + if(pixelDataElement.basicOffsetTable === undefined) { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: parameter 'pixelDataElement' refers to pixel data element that does not have encapsulated pixel data"; + } + if(pixelDataElement.fragments === undefined) { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: parameter 'pixelDataElement' refers to pixel data element that does not have encapsulated pixel data"; + } + if(pixelDataElement.fragments.length <= 0) { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: parameter 'pixelDataElement' refers to pixel data element that does not have encapsulated pixel data"; + } + if(startFragmentIndex < 0) { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: parameter 'startFragmentIndex' must be >= 0"; + } + if(startFragmentIndex >= pixelDataElement.fragments.length) { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: parameter 'startFragmentIndex' must be < number of fragments"; + } + if(numFragments < 1) { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: parameter 'numFragments' must be > 0"; + } + if(startFragmentIndex + numFragments > pixelDataElement.fragments.length) { + throw "dicomParser.readEncapsulatedPixelDataFromFragments: parameter 'startFragment' + 'numFragments' < number of fragments"; + } + + // create byte stream on the data for this pixel data element + var byteStream = new dicomParser.ByteStream(dataSet.byteArrayParser, dataSet.byteArray, pixelDataElement.dataOffset); + + // seek past the basic offset table (no need to parse it again since we already have) + var basicOffsetTable = dicomParser.readSequenceItem(byteStream); + if(basicOffsetTable.tag !== 'xfffee000') + { + throw "dicomParser.readEncapsulatedPixelData: missing basic offset table xfffee000"; + } + byteStream.seek(basicOffsetTable.length); + + var fragmentZeroPosition = byteStream.position; + var fragmentHeaderSize = 8; // tag + length + + // if there is only one fragment, return a view on this array to avoid copying + if(numFragments === 1) { + return dicomParser.sharedCopy(byteStream.byteArray, fragmentZeroPosition + fragments[startFragmentIndex].offset + fragmentHeaderSize, fragments[startFragmentIndex].length); + } + + // more than one fragment, combine all of the fragments into one buffer + var bufferSize = calculateBufferSize(fragments, startFragmentIndex, numFragments); + + var pixelData = dicomParser.alloc(byteStream.byteArray, bufferSize); + + var pixelDataIndex = 0; + for(var i=startFragmentIndex; i < startFragmentIndex + numFragments; i++) { + var fragmentOffset = fragmentZeroPosition + fragments[i].offset + fragmentHeaderSize; + for(var j=0; j < fragments[i].length; j++) { + pixelData[pixelDataIndex++] = byteStream.byteArray[fragmentOffset++]; + } + } + + return pixelData; + }; + + return dicomParser; +}(dicomParser)); + /** * Parses a DICOM P10 byte array and returns a DataSet object with the parsed elements. If the options * argument is supplied and it contains the untilTag property, parsing will stop once that @@ -1909,7 +2244,7 @@ var dicomParser = (function(dicomParser) { var prefix = littleEndianByteStream.readFixedString(4); if(prefix !== "DICM") { - throw "dicomParser.readPart10Header: DICM prefix not found at location 132"; + throw "dicomParser.readPart10Header: DICM prefix not found at location 132 - this is not a valid DICOM P10 file."; } } @@ -1978,7 +2313,7 @@ var dicomParser = (function (dicomParser) } // eof encountered - log a warning and return what we have for the element - byteStream.warnings.push('eof encountered before finding sequence delimitation item while reading sequence item of undefined length'); + warnings.push('eof encountered before finding item delimiter tag while reading sequence item of undefined length'); return new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, elements); } @@ -2002,7 +2337,7 @@ var dicomParser = (function (dicomParser) function readSQElementUndefinedLengthExplicit(byteStream, element, warnings) { - while(byteStream.position < byteStream.byteArray.length) + while((byteStream.position + 4) <= byteStream.byteArray.length) { // end reading this sequence if the next tag is the sequence delimitation item var nextTag = dicomParser.readTag(byteStream); @@ -2017,6 +2352,7 @@ var dicomParser = (function (dicomParser) var item = readSequenceItemExplicit(byteStream, warnings); element.items.push(item); } + warnings.push('eof encountered before finding sequence delimitation tag while reading sequence of undefined length'); element.length = byteStream.position - element.dataOffset; } @@ -2045,7 +2381,7 @@ var dicomParser = (function (dicomParser) if(element.length === 4294967295) { - readSQElementUndefinedLengthExplicit(byteStream, element); + readSQElementUndefinedLengthExplicit(byteStream, element, warnings); } else { @@ -2069,13 +2405,13 @@ var dicomParser = (function (dicomParser) dicomParser = {}; } - function readDicomDataSetImplicitUndefinedLength(byteStream) + function readDicomDataSetImplicitUndefinedLength(byteStream, vrCallback) { var elements = {}; while(byteStream.position < byteStream.byteArray.length) { - var element = dicomParser.readDicomElementImplicit(byteStream); + var element = dicomParser.readDicomElementImplicit(byteStream, undefined, vrCallback); elements[element.tag] = element; // we hit an item delimiter tag, return the current offset to mark @@ -2090,27 +2426,27 @@ var dicomParser = (function (dicomParser) return new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, elements); } - function readSequenceItemImplicit(byteStream) + function readSequenceItemImplicit(byteStream, vrCallback) { var item = dicomParser.readSequenceItem(byteStream); if(item.length === 4294967295) { item.hadUndefinedLength = true; - item.dataSet = readDicomDataSetImplicitUndefinedLength(byteStream); + item.dataSet = readDicomDataSetImplicitUndefinedLength(byteStream, vrCallback); item.length = byteStream.position - item.dataOffset; } else { item.dataSet = new dicomParser.DataSet(byteStream.byteArrayParser, byteStream.byteArray, {}); - dicomParser.parseDicomDataSetImplicit(item.dataSet, byteStream, byteStream.position + item.length); + dicomParser.parseDicomDataSetImplicit(item.dataSet, byteStream, byteStream.position + item.length, {vrCallback: vrCallback}); } return item; } - function readSQElementUndefinedLengthImplicit(byteStream, element) + function readSQElementUndefinedLengthImplicit(byteStream, element, vrCallback) { - while(byteStream.position < byteStream.byteArray.length) + while((byteStream.position + 4) <= byteStream.byteArray.length) { // end reading this sequence if the next tag is the sequence delimitation item var nextTag = dicomParser.readTag(byteStream); @@ -2122,18 +2458,19 @@ var dicomParser = (function (dicomParser) return element; } - var item = readSequenceItemImplicit(byteStream); + var item = readSequenceItemImplicit(byteStream, vrCallback); element.items.push(item); } + byteStream.warnings.push('eof encountered before finding sequence delimiter in sequence of undefined length'); element.length = byteStream.byteArray.length - element.dataOffset; } - function readSQElementKnownLengthImplicit(byteStream, element) + function readSQElementKnownLengthImplicit(byteStream, element, vrCallback) { var maxPosition = element.dataOffset + element.length; while(byteStream.position < maxPosition) { - var item = readSequenceItemImplicit(byteStream); + var item = readSequenceItemImplicit(byteStream, vrCallback); element.items.push(item); } } @@ -2142,8 +2479,9 @@ var dicomParser = (function (dicomParser) * Reads sequence items for an element in an implicit little endian byte stream * @param byteStream the implicit little endian byte stream * @param element the element to read the sequence items for + * @param vrCallback an optional method that returns a VR string given a tag */ - dicomParser.readSequenceItemsImplicit = function(byteStream, element) + dicomParser.readSequenceItemsImplicit = function(byteStream, element, vrCallback) { if(byteStream === undefined) { @@ -2158,11 +2496,11 @@ var dicomParser = (function (dicomParser) if(element.length === 4294967295) { - readSQElementUndefinedLengthImplicit(byteStream, element); + readSQElementUndefinedLengthImplicit(byteStream, element, vrCallback); } else { - readSQElementKnownLengthImplicit(byteStream, element); + readSQElementKnownLengthImplicit(byteStream, element, vrCallback); } }; @@ -2202,6 +2540,11 @@ var dicomParser = (function (dicomParser) dataOffset : byteStream.position }; + if (element.tag !== 'xfffee000') { + var startPosition = byteStream.position; + throw "dicomParser.readSequenceItem: item tag (FFFE,E000) not found at offset " + startPosition; + } + return element; }; @@ -2242,6 +2585,41 @@ var dicomParser = (function (dicomParser) return dicomParser; }(dicomParser)); +/** + * + * Internal helper function to create a shared copy of a byteArray + * + */ +var dicomParser = (function (dicomParser) +{ + "use strict"; + + if(dicomParser === undefined) + { + dicomParser = {}; + } + + /** + * Creates a view of the underlying byteArray. The view is of the same type as the byteArray (e.g. + * Uint8Array or Buffer) and shares the same underlying memory (changing one changes the other) + * @param byteArray the underlying byteArray (either Uint8Array or Buffer) + * @param byteOffset offset into the underlying byteArray to create the view of + * @param length number of bytes in the view + * @returns {object} Uint8Array or Buffer depending on the type of byteArray + */ + dicomParser.sharedCopy = function(byteArray, byteOffset, length) { + if (typeof Buffer !== 'undefined' && byteArray instanceof Buffer) { + return byteArray.slice(byteOffset, byteOffset + length); + } + else if(byteArray instanceof Uint8Array) { + return new Uint8Array(byteArray.buffer, byteArray.byteOffset + byteOffset, length); + } else { + throw 'dicomParser.from: unknown type for byteArray'; + } + }; + + return dicomParser; +}(dicomParser)); /** * Version */ @@ -2255,7 +2633,7 @@ var dicomParser = (function (dicomParser) dicomParser = {}; } - dicomParser.version = "1.2.1"; + dicomParser.version = "1.7.1"; return dicomParser; }(dicomParser));