This repository has been archived by the owner on Jun 23, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat thumb creation. Options to use recompress data on thumb creation * fix codecs package update * refactor code review: comments and lint fix * refactor add jest config for cs-lite and mock it until there are tests for it or using it * refactor code review: fix values ww/wc when creating image (cs-lite)
- Loading branch information
1 parent
8112628
commit 2fc350a
Showing
56 changed files
with
1,671 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# `@ohif/static-cs-lite` | ||
|
||
Lite and simplified version of cornerstone package which allows simulating server rendering. | ||
It uses JSDOM to support cornerstone to use server rendering approach. | ||
The sandbox configuration MUST be revisited for each new cornerstone integration. | ||
|
||
## Pre-requisites | ||
View root pre-requisites section [pre-requisites](../../README.md#pre-requisites) | ||
|
||
## Development | ||
View root development section [development](../../README.md#development) | ||
|
||
## Usage | ||
|
||
``` | ||
const staticCsLite = require('@ohif/static-cs-lite'); | ||
... | ||
const doneCallback = (imageBuffer) => { | ||
// write on disk imageBuffer (thumbnail) | ||
} | ||
// simulate image rendering on server side | ||
staticCsLite.simulateRendering(transferSyntaxUid, | ||
decodedPixelData, | ||
rawDataset, | ||
doneCallback) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
const baseConfig = require("../../.config/jest/jest.config"); | ||
|
||
module.exports = { | ||
...baseConfig | ||
} |
21 changes: 21 additions & 0 deletions
21
packages/static-cs-lite/lib/adapters/canvasImageToBuffer.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/** | ||
* Convert image from the given canvas to buffer object. | ||
* | ||
* @param {*} canvas that holds image to be converted. | ||
* @param {*} imageType target imageType. | ||
* @returns Buffer object | ||
*/ | ||
function canvasImageToBuffer(canvas, imageType = "image/jpeg") { | ||
let result; | ||
if (imageType === "image/jpeg") { | ||
const dataUrl = canvas.toDataURL(imageType, 1); | ||
const base64Data = dataUrl.replace(/^data:image\/(jpeg|png);base64,/, ""); | ||
result = Buffer.from(base64Data, "base64"); | ||
} | ||
|
||
console.log(`Can't convert canvas to image type of ${imageType}`); | ||
|
||
return result; | ||
} | ||
|
||
module.exports = canvasImageToBuffer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
const createImage = require("../image/createImage"); | ||
const setUpEnv = require("../sandbox"); | ||
const canvasImageToBuffer = require("../adapters/canvasImageToBuffer"); | ||
|
||
/** | ||
* It gets through callback call the rendered image into canvas. | ||
* It simulates rendering of decodedPixel data into server side (fake) canvas. | ||
* Once that is completed doneCallback is called (in case of failure/success) | ||
* | ||
* @param {*} transferSyntaxUid | ||
* @param {*} decodedPixelData data to be rendered on canvas | ||
* @param {*} metadata | ||
* @param {*} doneCallback Callback method that is invoked once image is rendered | ||
*/ | ||
function getRenderedBuffer(transferSyntaxUid, decodedPixelData, metadata, doneCallback) { | ||
const { csCore, canvas } = setUpEnv(); | ||
|
||
function doneRendering(customEvent = {}) { | ||
const { detail = {} } = customEvent; | ||
const { enabledElement } = detail; | ||
|
||
if (!enabledElement || !enabledElement.canvas) { | ||
doneCallback(); | ||
} | ||
|
||
const buffer = canvasImageToBuffer(enabledElement.canvas); | ||
doneCallback(buffer); | ||
} | ||
|
||
function failureRendering() { | ||
doneCallback(); | ||
} | ||
|
||
try { | ||
const imageObj = createImage(transferSyntaxUid, decodedPixelData, metadata, canvas); | ||
|
||
canvas.addEventListener(csCore.EVENTS.IMAGE_RENDERED, doneRendering); | ||
csCore.renderToCanvas(canvas, imageObj); | ||
} catch (e) { | ||
console.log("Failed to render", e); | ||
failureRendering(); | ||
} | ||
} | ||
|
||
module.exports = getRenderedBuffer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const getRenderedBuffer = require("./getRenderedBuffer"); | ||
|
||
exports.getRenderedBuffer = getRenderedBuffer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
/* eslint-disable no-param-reassign */ | ||
const dcmjs = require("dcmjs"); | ||
const { imageFrameUtils } = require("../util"); | ||
|
||
/** | ||
* Creates CornerstoneCore image object for already decodedPixel data. | ||
* It returns an complex object to be used by CS api methods layer. | ||
* | ||
* @param {*} transferSyntax | ||
* @param {*} decodedPixelData | ||
* @param {*} metadata | ||
* @param {*} canvas browser canvas (this param is mutate) | ||
* @param {*} options | ||
* @returns | ||
*/ | ||
function createImage(transferSyntax, decodedPixelData, metadata, canvas, options = {}) { | ||
const dataSet = dcmjs.data.DicomMetaDictionary.naturalizeDataset(metadata); | ||
const imageFrame = imageFrameUtils.get.fromDataset(dataSet, decodedPixelData); | ||
|
||
const { convertFloatPixelDataToInt, targetBuffer } = options; | ||
|
||
// If we have a target buffer that was written to in the | ||
// Decode task, point the image to it here. | ||
// We can't have done it within the thread incase it was a SharedArrayBuffer. | ||
const alreadyTyped = imageFrameUtils.convert.pixelDataToTargetBuffer(imageFrame, targetBuffer); | ||
const originalDataConstructor = imageFrame.pixelData.constructor; | ||
|
||
// setup the canvas context | ||
canvas.height = imageFrame.rows; | ||
canvas.width = imageFrame.columns; | ||
|
||
const { | ||
ModalityLUTSequence: modalityLUTSequence, | ||
PixelSpacing: pixelSpacing, | ||
RescaleIntercept: intercept = 0, | ||
RescaleSlope: slope = 1, | ||
VOILUTSequence: voiLUTSequence, | ||
WindowCenter: windowCenter, | ||
WindowWidth: windowWidth, | ||
SOPClassUID: sopClassUID, | ||
} = dataSet; | ||
|
||
const [rowPixelSpacing, columnPixelSpacing] = pixelSpacing || []; | ||
const isColorImage = imageFrameUtils.is.colorImage(imageFrame); | ||
|
||
// JPEGBaseline (8 bits) is already returning the pixel data in the right format (rgba) | ||
// because it's using a canvas to load and decode images. | ||
if (!imageFrameUtils.is.jpegBaseline8BitColor(imageFrame, transferSyntax)) { | ||
if (!alreadyTyped) { | ||
imageFrameUtils.convert.pixelDataType(imageFrame); | ||
} | ||
|
||
// convert color space | ||
if (isColorImage) { | ||
const context = canvas.getContext("2d"); | ||
const imageData = context.createImageData(imageFrame.columns, imageFrame.rows); | ||
|
||
// imageData.data is being changed by reference. | ||
imageFrameUtils.convert.colorSpace(imageFrame, imageData.data); | ||
if (!imageData.data) { | ||
throw new Error("Missing image data after converting color space"); | ||
} | ||
imageFrame.imageData = imageData; | ||
imageFrame.pixelData = imageData.data; | ||
} | ||
} | ||
|
||
if ((!imageFrame.smallestPixelValue || !imageFrame.largestPixelValue || imageFrame.pixelData.constructor, originalDataConstructor)) { | ||
// calculate smallest and largest PixelValue of the converted pixelData | ||
const { min, max } = imageFrameUtils.get.pixelDataMinMax(imageFrame.pixelData); | ||
|
||
imageFrame.smallestPixelValue = min; | ||
imageFrame.largestPixelValue = max; | ||
} | ||
|
||
const image = { | ||
color: isColorImage, | ||
columnPixelSpacing, | ||
columns: imageFrame.columns, | ||
height: imageFrame.rows, | ||
preScale: imageFrame.preScale, | ||
intercept, | ||
slope, | ||
invert: imageFrame.photometricInterpretation === "MONOCHROME1", | ||
minPixelValue: imageFrame.smallestPixelValue, | ||
maxPixelValue: imageFrame.largestPixelValue, | ||
rowPixelSpacing, | ||
rows: imageFrame.rows, | ||
sizeInBytes: imageFrame.pixelData.byteLength, | ||
width: imageFrame.columns, | ||
windowCenter, | ||
windowWidth, | ||
decodeTimeInMS: imageFrame.decodeTimeInMS, | ||
floatPixelData: undefined, | ||
imageFrame, | ||
}; | ||
|
||
// If pixel data is intrinsically floating 32 array, we convert it to int for | ||
// display in cornerstone. For other cases when pixel data is typed as | ||
// Float32Array for scaling; this conversion is not needed. | ||
if (imageFrame.pixelData instanceof Float32Array && convertFloatPixelDataToInt) { | ||
const floatPixelData = imageFrame.pixelData; | ||
const results = imageFrameUtils.get.pixelDataIntType(floatPixelData); | ||
|
||
image.minPixelValue = results.min; | ||
image.maxPixelValue = results.max; | ||
image.slope = results.slope; | ||
image.intercept = results.intercept; | ||
image.floatPixelData = floatPixelData; | ||
image.getPixelData = () => results.intPixelData; | ||
} else { | ||
image.getPixelData = () => imageFrame.pixelData; | ||
} | ||
|
||
if (image.color) { | ||
// let lastImageIdDrawn; | ||
image.getCanvas = () => { | ||
canvas.height = image.rows; | ||
canvas.width = image.columns; | ||
const context = canvas.getContext("2d"); | ||
|
||
context.putImageData(imageFrame.imageData, 0, 0); | ||
|
||
return canvas; | ||
}; | ||
} | ||
|
||
// Modality LUT | ||
if (modalityLUTSequence && modalityLUTSequence.length > 0 && imageFrameUtils.is.modalityLUT(sopClassUID)) { | ||
image.modalityLUT = modalityLUTSequence[0]; | ||
} | ||
|
||
// VOI LUT | ||
if (voiLUTSequence && voiLUTSequence.length > 0) { | ||
image.voiLUT = voiLUTSequence[0]; | ||
} | ||
|
||
if (image.color) { | ||
image.windowWidth = 256; | ||
image.windowCenter = 128; | ||
} | ||
|
||
// set the ww/wc to cover the dynamic range of the image if no values are supplied | ||
if (image.windowCenter === undefined || image.windowWidth === undefined) { | ||
const maxVoi = image.maxPixelValue * image.slope + image.intercept; | ||
const minVoi = image.minPixelValue * image.slope + image.intercept; | ||
|
||
image.windowWidth = maxVoi - minVoi + 1; | ||
image.windowCenter = (maxVoi + minVoi + 1) / 2; | ||
} | ||
|
||
return image; | ||
} | ||
|
||
module.exports = createImage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const api = require("./api"); | ||
|
||
exports.getRenderedBuffer = api.getRenderedBuffer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/** | ||
* Mock api getRenderedBuffer to by pass inner layers | ||
*/ | ||
function mockGetRenderedBuffer(_1, _2, _3, doneCallback) { | ||
doneCallback(); | ||
} | ||
|
||
exports.getRenderedBuffer = mockGetRenderedBuffer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
const createBrowser = require("jsdom-context-require"); | ||
|
||
/** | ||
* CS lite sandbox | ||
* @typedef {Object} CsLiteSandbox | ||
* @property {Object} csCore cornerstone core module loaded. | ||
* @property {Object} context JSDOM context. | ||
* @property {Object} canvas canvas object for given context. | ||
*/ | ||
/** | ||
* Configures a fake node sandbox to run cornerstone in it. | ||
* It might mock some of web standards in other to allow cornerstone to run on server side for existing exposed apis. | ||
* It creates a sandbox with canvas tag, set needed cs globals then load cs module. | ||
* | ||
* @returns {CsLiteSandbox} | ||
*/ | ||
function setUpEnvSandbox() { | ||
const context = createBrowser({ | ||
dir: __dirname, | ||
html: "<!DOCTYPE html><div><canvas></canvas></div>", | ||
}); | ||
|
||
const csCore = context.require("cornerstone-core"); | ||
|
||
return { csCore, context, canvas: context.window.document.querySelector("canvas") }; | ||
} | ||
|
||
module.exports = setUpEnvSandbox; |
21 changes: 21 additions & 0 deletions
21
packages/static-cs-lite/lib/util/assertArrayDivisibility.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
const assert = require("assert").strict; | ||
|
||
function assertArrayDivisibility(array, divisor, errorMessages = []) { | ||
const [errorArrayMessage, errorDivisorMessage] = errorMessages; | ||
|
||
try { | ||
assert.ok(!!array, errorArrayMessage); | ||
const result = array.length % divisor !== 0; | ||
assert.ok(result, errorDivisorMessage); | ||
} catch (e) { | ||
if (errorArrayMessage || errorDivisorMessage) { | ||
throw e; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
module.exports = assertArrayDivisibility; |
37 changes: 37 additions & 0 deletions
37
packages/static-cs-lite/lib/util/imageFrame/convert/color/colorSpace.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
const convertRGB = require("./convertRGB"); | ||
const convertPALETTECOLOR = require("./convertPALETTECOLOR"); | ||
const convertYBRFull422ByPixel = require("./convertYBRFull422ByPixel"); | ||
const convertYBRFull = require("./convertYBRFull"); | ||
|
||
/** | ||
* Convert pixel data with different Photometric Interpretation types to RGBA | ||
* | ||
* @param {ImageFrame} imageFrame | ||
* @param {Uint8ClampedArray} rgbaBuffer buffer result (this param is mutate) | ||
* @returns {void} | ||
*/ | ||
function colorSpace(imageFrame, rgbaBuffer) { | ||
// convert based on the photometric interpretation | ||
const { photometricInterpretation } = imageFrame; | ||
|
||
switch (photometricInterpretation) { | ||
case "RGB": | ||
case "YBR_RCT": | ||
case "YBR_ICT": | ||
convertRGB(imageFrame, rgbaBuffer); | ||
break; | ||
case "PALETTE COLOR": | ||
convertPALETTECOLOR(imageFrame, rgbaBuffer); | ||
break; | ||
case "YBR_FULL_422": | ||
convertYBRFull422ByPixel(imageFrame, rgbaBuffer); | ||
break; | ||
case "YBR_FULL": | ||
convertYBRFull(imageFrame, rgbaBuffer); | ||
break; | ||
default: | ||
throw new Error(`No color space conversion for photometric interpretation ${photometricInterpretation}`); | ||
} | ||
} | ||
|
||
module.exports = colorSpace; |
Oops, something went wrong.