Skip to content
This repository has been archived by the owner on Jun 23, 2023. It is now read-only.

Commit

Permalink
Feature/thumb process (#14)
Browse files Browse the repository at this point in the history
* 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
ladeirarodolfo authored Apr 4, 2022
1 parent 8112628 commit 2fc350a
Show file tree
Hide file tree
Showing 56 changed files with 1,671 additions and 46 deletions.
2 changes: 2 additions & 0 deletions .config/jest/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ module.exports = {
moduleNameMapper: {
"(.*)-(charls|openjpeg)\/wasmjs": "$1-$2/dist/$2wasm.js",
"(.*)-(libjpeg)-(turbo)-(8bit)\/wasmjs": "$1-$2-$3-$4/dist/$2$3wasm.js",
// This should be removed once there is a setup for jest-jsdom or there are e2e tests from/to it.
"@ohif/static-cs-lite": "@ohif/static-cs-lite/lib/index.mock.js"
},
moduleDirectories: [
"<rootDir>/node_modules",
Expand Down
27 changes: 27 additions & 0 deletions packages/static-cs-lite/README.md
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)
```
5 changes: 5 additions & 0 deletions packages/static-cs-lite/jest.config.js
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 packages/static-cs-lite/lib/adapters/canvasImageToBuffer.js
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;
45 changes: 45 additions & 0 deletions packages/static-cs-lite/lib/api/getRenderedBuffer.js
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;
3 changes: 3 additions & 0 deletions packages/static-cs-lite/lib/api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const getRenderedBuffer = require("./getRenderedBuffer");

exports.getRenderedBuffer = getRenderedBuffer;
155 changes: 155 additions & 0 deletions packages/static-cs-lite/lib/image/createImage.js
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;
3 changes: 3 additions & 0 deletions packages/static-cs-lite/lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const api = require("./api");

exports.getRenderedBuffer = api.getRenderedBuffer;
8 changes: 8 additions & 0 deletions packages/static-cs-lite/lib/index.mock.js
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;
28 changes: 28 additions & 0 deletions packages/static-cs-lite/lib/sandbox/index.js
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 packages/static-cs-lite/lib/util/assertArrayDivisibility.js
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;
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;
Loading

0 comments on commit 2fc350a

Please sign in to comment.