diff --git a/.config/jest/jest.config.js b/.config/jest/jest.config.js index c01438e..6a63251 100644 --- a/.config/jest/jest.config.js +++ b/.config/jest/jest.config.js @@ -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: [ "/node_modules", diff --git a/packages/static-cs-lite/README.md b/packages/static-cs-lite/README.md new file mode 100644 index 0000000..08d8b6e --- /dev/null +++ b/packages/static-cs-lite/README.md @@ -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) +``` diff --git a/packages/static-cs-lite/jest.config.js b/packages/static-cs-lite/jest.config.js new file mode 100644 index 0000000..56c4487 --- /dev/null +++ b/packages/static-cs-lite/jest.config.js @@ -0,0 +1,5 @@ +const baseConfig = require("../../.config/jest/jest.config"); + +module.exports = { + ...baseConfig +} \ No newline at end of file diff --git a/packages/static-cs-lite/lib/adapters/canvasImageToBuffer.js b/packages/static-cs-lite/lib/adapters/canvasImageToBuffer.js new file mode 100644 index 0000000..fc85608 --- /dev/null +++ b/packages/static-cs-lite/lib/adapters/canvasImageToBuffer.js @@ -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; diff --git a/packages/static-cs-lite/lib/api/getRenderedBuffer.js b/packages/static-cs-lite/lib/api/getRenderedBuffer.js new file mode 100644 index 0000000..8e8c1ac --- /dev/null +++ b/packages/static-cs-lite/lib/api/getRenderedBuffer.js @@ -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; diff --git a/packages/static-cs-lite/lib/api/index.js b/packages/static-cs-lite/lib/api/index.js new file mode 100644 index 0000000..dad8a46 --- /dev/null +++ b/packages/static-cs-lite/lib/api/index.js @@ -0,0 +1,3 @@ +const getRenderedBuffer = require("./getRenderedBuffer"); + +exports.getRenderedBuffer = getRenderedBuffer; diff --git a/packages/static-cs-lite/lib/image/createImage.js b/packages/static-cs-lite/lib/image/createImage.js new file mode 100644 index 0000000..7d49a44 --- /dev/null +++ b/packages/static-cs-lite/lib/image/createImage.js @@ -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; diff --git a/packages/static-cs-lite/lib/index.js b/packages/static-cs-lite/lib/index.js new file mode 100644 index 0000000..c86c01e --- /dev/null +++ b/packages/static-cs-lite/lib/index.js @@ -0,0 +1,3 @@ +const api = require("./api"); + +exports.getRenderedBuffer = api.getRenderedBuffer; diff --git a/packages/static-cs-lite/lib/index.mock.js b/packages/static-cs-lite/lib/index.mock.js new file mode 100644 index 0000000..d06c77f --- /dev/null +++ b/packages/static-cs-lite/lib/index.mock.js @@ -0,0 +1,8 @@ +/** + * Mock api getRenderedBuffer to by pass inner layers + */ +function mockGetRenderedBuffer(_1, _2, _3, doneCallback) { + doneCallback(); +} + +exports.getRenderedBuffer = mockGetRenderedBuffer; diff --git a/packages/static-cs-lite/lib/sandbox/index.js b/packages/static-cs-lite/lib/sandbox/index.js new file mode 100644 index 0000000..48a72b0 --- /dev/null +++ b/packages/static-cs-lite/lib/sandbox/index.js @@ -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: "
", + }); + + const csCore = context.require("cornerstone-core"); + + return { csCore, context, canvas: context.window.document.querySelector("canvas") }; +} + +module.exports = setUpEnvSandbox; diff --git a/packages/static-cs-lite/lib/util/assertArrayDivisibility.js b/packages/static-cs-lite/lib/util/assertArrayDivisibility.js new file mode 100644 index 0000000..e323fda --- /dev/null +++ b/packages/static-cs-lite/lib/util/assertArrayDivisibility.js @@ -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; diff --git a/packages/static-cs-lite/lib/util/imageFrame/convert/color/colorSpace.js b/packages/static-cs-lite/lib/util/imageFrame/convert/color/colorSpace.js new file mode 100644 index 0000000..c89450f --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/convert/color/colorSpace.js @@ -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; diff --git a/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertPALETTECOLOR.js b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertPALETTECOLOR.js new file mode 100755 index 0000000..305409e --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertPALETTECOLOR.js @@ -0,0 +1,57 @@ +/* eslint-disable no-plusplus, no-param-reassign, no-bitwise */ +function convertLUTto8Bit(lut, shift) { + const numEntries = lut.length; + const cleanedLUT = new Uint8ClampedArray(numEntries); + + for (let i = 0; i < numEntries; ++i) { + cleanedLUT[i] = lut[i] >> shift; + } + + return cleanedLUT; +} + +/** + * Convert pixel data with PALETTE COLOR Photometric Interpretation to RGBA + * + * @param {ImageFrame} imageFrame + * @param {Uint8ClampedArray} rgbaBuffer buffer result (this param is mutate) + * @returns {void} + */ +function converter(imageFrame, rgbaBuffer) { + const numPixels = imageFrame.columns * imageFrame.rows; + const pixelData = imageFrame.pixelData; + const rData = imageFrame.redPaletteColorLookupTableData; + const gData = imageFrame.greenPaletteColorLookupTableData; + const bData = imageFrame.bluePaletteColorLookupTableData; + const len = imageFrame.redPaletteColorLookupTableData.length; + + let palIndex = 0; + + let rgbaIndex = 0; + + const start = imageFrame.redPaletteColorLookupTableDescriptor[1]; + const shift = imageFrame.redPaletteColorLookupTableDescriptor[2] === 8 ? 0 : 8; + + const rDataCleaned = convertLUTto8Bit(rData, shift); + const gDataCleaned = convertLUTto8Bit(gData, shift); + const bDataCleaned = convertLUTto8Bit(bData, shift); + + for (let i = 0; i < numPixels; ++i) { + let value = pixelData[palIndex++]; + + if (value < start) { + value = 0; + } else if (value > start + len - 1) { + value = len - 1; + } else { + value -= start; + } + + rgbaBuffer[rgbaIndex++] = rDataCleaned[value]; + rgbaBuffer[rgbaIndex++] = gDataCleaned[value]; + rgbaBuffer[rgbaIndex++] = bDataCleaned[value]; + rgbaBuffer[rgbaIndex++] = 255; + } +} + +module.exports = converter; diff --git a/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertRGB.js b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertRGB.js new file mode 100644 index 0000000..4267d9e --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertRGB.js @@ -0,0 +1,20 @@ +const convertRGBColorByPixel = require("./convertRGBColorByPixel"); +const convertRGBColorByPlane = require("./convertRGBColorByPlane"); + +/** + * Convert pixel data with RGB (both pixel and plane) Photometric Interpretation to RGBA + * + * @param {ImageFrame} imageFrame + * @param {Uint8ClampedArray} rgbaBuffer buffer result (this param is mutate) + * @returns {void} + */ +function convertRGB(imageFrame, rgbaBuffer) { + const { planarConfiguration, pixelData } = imageFrame; + if (planarConfiguration === 0) { + convertRGBColorByPixel(pixelData, rgbaBuffer); + } else { + convertRGBColorByPlane(pixelData, rgbaBuffer); + } +} + +module.exports = convertRGB; diff --git a/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertRGBColorByPixel.js b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertRGBColorByPixel.js new file mode 100755 index 0000000..822a962 --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertRGBColorByPixel.js @@ -0,0 +1,30 @@ +/* eslint-disable no-plusplus, no-param-reassign */ +const assertArrayDivisibility = require("../../../assertArrayDivisibility"); + +/** + * Convert pixel data with RGB (by pixel) Photometric Interpretation to RGBA + * + * @param {ImageFrame} imageFrame + * @param {Uint8ClampedArray} rgbaBuffer buffer result (this param is mutate) + * @returns {void} + */ +function converter(imageFrame, rgbaBuffer) { + if (!assertArrayDivisibility(imageFrame, 3, ["decodeRGB: rgbBuffer must not be undefined", "decodeRGB: rgbBuffer length must be divisble by 3"])) { + return; + } + + const numPixels = imageFrame.length / 3; + + let rgbIndex = 0; + + let rgbaIndex = 0; + + for (let i = 0; i < numPixels; i++) { + rgbaBuffer[rgbaIndex++] = imageFrame[rgbIndex++]; // red + rgbaBuffer[rgbaIndex++] = imageFrame[rgbIndex++]; // green + rgbaBuffer[rgbaIndex++] = imageFrame[rgbIndex++]; // blue + rgbaBuffer[rgbaIndex++] = 255; // alpha + } +} + +module.exports = converter; diff --git a/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertRGBColorByPlane.js b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertRGBColorByPlane.js new file mode 100755 index 0000000..4228c3c --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertRGBColorByPlane.js @@ -0,0 +1,34 @@ +/* eslint-disable no-plusplus, no-param-reassign */ +const assertArrayDivisibility = require("../../../assertArrayDivisibility"); + +/** + * Convert pixel data with RGB (by plane) Photometric Interpretation to RGBA + * + * @param {ImageFrame} imageFrame + * @param {Uint8ClampedArray} rgbaBuffer buffer result (this param is mutate) + * @returns {void} + */ +function converter(imageFrame, rgbaBuffer) { + if (!assertArrayDivisibility(imageFrame, 3, ["decodeRGB: rgbBuffer must not be undefined", "decodeRGB: rgbBuffer length must be divisble by 3"])) { + return; + } + + const numPixels = imageFrame.length / 3; + + let rgbaIndex = 0; + + let rIndex = 0; + + let gIndex = numPixels; + + let bIndex = numPixels * 2; + + for (let i = 0; i < numPixels; i++) { + rgbaBuffer[rgbaIndex++] = imageFrame[rIndex++]; // red + rgbaBuffer[rgbaIndex++] = imageFrame[gIndex++]; // green + rgbaBuffer[rgbaIndex++] = imageFrame[bIndex++]; // blue + rgbaBuffer[rgbaIndex++] = 255; // alpha + } +} + +module.exports = converter; diff --git a/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertYBRFull.js b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertYBRFull.js new file mode 100644 index 0000000..f1ec223 --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertYBRFull.js @@ -0,0 +1,21 @@ +const convertYBRFullByPixel = require("./convertYBRFullByPixel"); +const convertYBRFullByPlane = require("./convertYBRFullByPlane"); + +/** + * Convert pixel data with YBR Full (both pixel and plane) Photometric Interpretation to RGBA + * + * @param {ImageFrame} imageFrame + * @param {Uint8ClampedArray} rgbaBuffer buffer result (this param is mutate) + * @returns {void} + */ +function convertYBRFull(imageFrame, rgbaBuffer) { + const { planarConfiguration, pixelData } = imageFrame; + + if (planarConfiguration === 0) { + convertYBRFullByPixel(pixelData, rgbaBuffer); + } else { + convertYBRFullByPlane(pixelData, rgbaBuffer); + } +} + +module.exports = convertYBRFull; diff --git a/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertYBRFull422ByPixel.js b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertYBRFull422ByPixel.js new file mode 100755 index 0000000..7f62601 --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertYBRFull422ByPixel.js @@ -0,0 +1,40 @@ +/* eslint-disable no-plusplus, no-param-reassign */ +const assertArrayDivisibility = require("../../../assertArrayDivisibility"); + +/** + * Convert pixel data with YBR Full 422 (by pixel) Photometric Interpretation to RGBA + * + * @param {ImageFrame} imageFrame + * @param {Uint8ClampedArray} rgbaBuffer buffer result (this param is mutate) + * @returns {void} + */ +function converter(imageFrame, rgbaBuffer) { + if (!assertArrayDivisibility(imageFrame, 2, ["decodeRGB: ybrBuffer must not be undefined", "decodeRGB: ybrBuffer length must be divisble by 2"])) { + return; + } + + const numPixels = imageFrame.length / 2; + + let ybrIndex = 0; + + let rgbaIndex = 0; + + for (let i = 0; i < numPixels; i += 2) { + const y1 = imageFrame[ybrIndex++]; + const y2 = imageFrame[ybrIndex++]; + const cb = imageFrame[ybrIndex++]; + const cr = imageFrame[ybrIndex++]; + + rgbaBuffer[rgbaIndex++] = y1 + 1.402 * (cr - 128); // red + rgbaBuffer[rgbaIndex++] = y1 - 0.34414 * (cb - 128) - 0.71414 * (cr - 128); // green + rgbaBuffer[rgbaIndex++] = y1 + 1.772 * (cb - 128); // blue + rgbaBuffer[rgbaIndex++] = 255; // alpha + + rgbaBuffer[rgbaIndex++] = y2 + 1.402 * (cr - 128); // red + rgbaBuffer[rgbaIndex++] = y2 - 0.34414 * (cb - 128) - 0.71414 * (cr - 128); // green + rgbaBuffer[rgbaIndex++] = y2 + 1.772 * (cb - 128); // blue + rgbaBuffer[rgbaIndex++] = 255; // alpha + } +} + +module.exports = converter; diff --git a/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertYBRFullByPixel.js b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertYBRFullByPixel.js new file mode 100755 index 0000000..259be27 --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertYBRFullByPixel.js @@ -0,0 +1,34 @@ +/* eslint-disable no-plusplus, no-param-reassign */ +const assertArrayDivisibility = require("../../../assertArrayDivisibility"); + +/** + * Convert pixel data with YBR Full (by pixel) Photometric Interpretation to RGBA + * + * @param {ImageFrame} imageFrame + * @param {Uint8ClampedArray} rgbaBuffer buffer result (this param is mutate) + * @returns {void} + */ +function converter(imageFrame, rgbaBuffer) { + if (!assertArrayDivisibility(imageFrame, 3, ["decodeRGB: ybrBuffer must not be undefined", "decodeRGB: ybrBuffer length must be divisble by 3"])) { + return; + } + + const numPixels = imageFrame.length / 3; + + let ybrIndex = 0; + + let rgbaIndex = 0; + + for (let i = 0; i < numPixels; i++) { + const y = imageFrame[ybrIndex++]; + const cb = imageFrame[ybrIndex++]; + const cr = imageFrame[ybrIndex++]; + + rgbaBuffer[rgbaIndex++] = y + 1.402 * (cr - 128); // red + rgbaBuffer[rgbaIndex++] = y - 0.34414 * (cb - 128) - 0.71414 * (cr - 128); // green + rgbaBuffer[rgbaIndex++] = y + 1.772 * (cb - 128); // blue + rgbaBuffer[rgbaIndex++] = 255; // alpha + } +} + +module.exports = converter; diff --git a/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertYBRFullByPlane.js b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertYBRFullByPlane.js new file mode 100755 index 0000000..eff38d3 --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/convert/color/convertYBRFullByPlane.js @@ -0,0 +1,38 @@ +/* eslint-disable no-plusplus, no-param-reassign */ +const assertArrayDivisibility = require("../../../assertArrayDivisibility"); + +/** + * Convert pixel data with YBR Full (by plane) Photometric Interpretation to RGBA + * + * @param {ImageFrame} imageFrame + * @param {Uint8ClampedArray} rgbaBuffer buffer result (this param is mutate) + * @returns {void} + */ +function converter(imageFrame, rgbaBuffer) { + if (!assertArrayDivisibility(imageFrame, 3, ["decodeRGB: ybrBuffer must not be undefined", "decodeRGB: ybrBuffer length must be divisble by 3"])) { + return; + } + + const numPixels = imageFrame.length / 3; + + let rgbaIndex = 0; + + let yIndex = 0; + + let cbIndex = numPixels; + + let crIndex = numPixels * 2; + + for (let i = 0; i < numPixels; i++) { + const y = imageFrame[yIndex++]; + const cb = imageFrame[cbIndex++]; + const cr = imageFrame[crIndex++]; + + rgbaBuffer[rgbaIndex++] = y + 1.402 * (cr - 128); // red + rgbaBuffer[rgbaIndex++] = y - 0.34414 * (cb - 128) - 0.71414 * (cr - 128); // green + rgbaBuffer[rgbaIndex++] = y + 1.772 * (cb - 128); // blue + rgbaBuffer[rgbaIndex++] = 255; // alpha + } +} + +module.exports = converter; diff --git a/packages/static-cs-lite/lib/util/imageFrame/convert/color/index.js b/packages/static-cs-lite/lib/util/imageFrame/convert/color/index.js new file mode 100755 index 0000000..2480fc9 --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/convert/color/index.js @@ -0,0 +1,23 @@ +const colorSpace = require("./colorSpace"); +/* +const convertPALETTECOLOR = require("./convertPALETTECOLOR"); +const convertRGB = require("./convertRGB"); +const convertRGBColorByPixel = require("./convertRGBColorByPixel"); +const convertRGBColorByPlane = require("./convertRGBColorByPlane"); +const convertYBRFull = require("./convertYBRFull"); +const convertYBRFull422ByPixel = require("./convertYBRFull422ByPixel"); +const convertYBRFullByPixel = require("./convertYBRFullByPixel"); +const convertYBRFullByPlane = require("./convertYBRFullByPlane"); +*/ + +exports.colorSpace = colorSpace; +/* Not yet exporting such granularity +exports.convertPALETTECOLOR = convertPALETTECOLOR; +exports.convertRGB = convertRGB; +exports.convertRGBColorByPixel = convertRGBColorByPixel; +exports.convertRGBColorByPlane = convertRGBColorByPlane; +exports.convertYBRFull = convertYBRFull; +exports.convertYBRFull422ByPixel = convertYBRFull422ByPixel; +exports.convertYBRFullByPixel = convertYBRFullByPixel; +exports.convertYBRFullByPlane = convertYBRFullByPlane; +*/ diff --git a/packages/static-cs-lite/lib/util/imageFrame/convert/index.js b/packages/static-cs-lite/lib/util/imageFrame/convert/index.js new file mode 100644 index 0000000..df5c120 --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/convert/index.js @@ -0,0 +1,7 @@ +const { colorSpace } = require("./color"); +const pixelDataType = require("./pixelDataType"); +const pixelDataToTargetBuffer = require("./pixelDataToTargetBuffer"); + +exports.colorSpace = colorSpace; +exports.pixelDataType = pixelDataType; +exports.pixelDataToTargetBuffer = pixelDataToTargetBuffer; diff --git a/packages/static-cs-lite/lib/util/imageFrame/convert/pixelDataToTargetBuffer b/packages/static-cs-lite/lib/util/imageFrame/convert/pixelDataToTargetBuffer new file mode 100644 index 0000000..0138620 --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/convert/pixelDataToTargetBuffer @@ -0,0 +1,54 @@ +function toTargetBuffer(imageFrame, targetBuffer) { + if (targetBuffer) { + // If we have a target buffer, write to that instead. This helps reduce memory duplication. + let { offset = null, length = null } = targetBuffer; + const { arrayBuffer, type } = targetBuffer; + + let TypedArrayConstructor; + + if (length === null) { + length = imageFrame.pixelDataLength; + } + + if (offset === null) { + offset = 0; + } + + switch (type) { + case "Uint8Array": + TypedArrayConstructor = Uint8Array; + break; + case "Uint16Array": + TypedArrayConstructor = Uint16Array; + break; + case "Float32Array": + TypedArrayConstructor = Float32Array; + break; + default: + throw new Error("target array for image does not have a valid type."); + } + + if (length !== imageFrame.pixelDataLength) { + throw new Error("target array for image does not have the same length as the decoded image length."); + } + + // TypedArray.Set is api level and ~50x faster than copying elements even for + // Arrays of different types, which aren't simply memcpy ops. + let typedArray; + + if (arrayBuffer) { + typedArray = new TypedArrayConstructor(arrayBuffer, offset, length); + } else { + typedArray = new TypedArrayConstructor(imageFrame.pixelData); + } + + // If need to scale, need to scale correct array. + imageFrame.pixelData = typedArray; + + return true; + } + + return false; +} + +module.exports = toTargetBuffer; \ No newline at end of file diff --git a/packages/static-cs-lite/lib/util/imageFrame/convert/pixelDataType.js b/packages/static-cs-lite/lib/util/imageFrame/convert/pixelDataType.js new file mode 100644 index 0000000..d8df139 --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/convert/pixelDataType.js @@ -0,0 +1,20 @@ +/* eslint-disable no-param-reassign */ +/** + * It converts pixel data type based on imageFrame properties + * @param {*} imageFrame object containing frame properties and also pixelData (this param is mutate) + */ +function convertPixelDataType(imageFrame) { + if (imageFrame.bitsAllocated === 32) { + imageFrame.pixelData = new Float32Array(imageFrame.pixelData); + } else if (imageFrame.bitsAllocated === 16) { + if (imageFrame.pixelRepresentation === 0) { + imageFrame.pixelData = new Uint16Array(imageFrame.pixelData); + } else { + imageFrame.pixelData = new Int16Array(imageFrame.pixelData); + } + } else { + imageFrame.pixelData = new Uint8Array(imageFrame.pixelData); + } +} + +module.exports = convertPixelDataType; diff --git a/packages/static-cs-lite/lib/util/imageFrame/get/fromDataset.js b/packages/static-cs-lite/lib/util/imageFrame/get/fromDataset.js new file mode 100644 index 0000000..53bd007 --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/get/fromDataset.js @@ -0,0 +1,36 @@ +const paletteColor = require("./paletteColor"); + +/** + * Get image frame from dataset + * + * @param {*} dataSet naturalized (by dcmjs) data set + * @param {*} decodedPixelData + * @returns image frame + */ +function fromDataset(dataSet, decodedPixelData) { + const bluePaletteColorLookupTableData = paletteColor(dataSet.BluePaletteColorLookupTableData, dataSet.BluePaletteColorLookupTableDescriptor); + const greenPaletteColorLookupTableData = paletteColor(dataSet.GreenPaletteColorLookupTableData, dataSet.GreenPaletteColorLookupTableDescriptor); + const redPaletteColorLookupTableData = paletteColor(dataSet.RedPaletteColorLookupTableData, dataSet.RedPaletteColorLookupTableDescriptor); + + return { + samplesPerPixel: dataSet.SamplesPerPixel, + photometricInterpretation: dataSet.PhotometricInterpretation, + planarConfiguration: dataSet.PlanarConfiguration, + rows: dataSet.Rows, + columns: dataSet.Columns, + bitsAllocated: dataSet.BitsAllocated, + bitsStored: dataSet.BitsStored, + pixelRepresentation: dataSet.PixelPresentation, // 0 = unsigned, + smallestPixelValue: dataSet.SmallestImagePixelValue, + largestPixelValue: dataSet.LargestImagePixelValue, + bluePaletteColorLookupTableData, + bluePaletteColorLookupTableDescriptor: dataSet.BluePaletteColorLookupTableDescriptor, + greenPaletteColorLookupTableData, + greenPaletteColorLookupTableDescriptor: dataSet.GreenPaletteColorLookupTableDescriptor, + redPaletteColorLookupTableData, + redPaletteColorLookupTableDescriptor: dataSet.RedPaletteColorLookupTableDescriptor, + pixelData: decodedPixelData, + }; +} + +module.exports = fromDataset; diff --git a/packages/static-cs-lite/lib/util/imageFrame/get/index.js b/packages/static-cs-lite/lib/util/imageFrame/get/index.js new file mode 100644 index 0000000..a89d3ad --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/get/index.js @@ -0,0 +1,9 @@ +const paletteColor = require("./paletteColor"); +const pixelDataMinMax = require("./pixelDataMinMax"); +const pixelDataIntType = require("./pixelDataIntType"); +const fromDataset = require("./fromDataset"); + +exports.fromDataset = fromDataset; +exports.paletteColor = paletteColor; +exports.pixelDataMinMax = pixelDataMinMax; +exports.pixelDataIntType = pixelDataIntType; diff --git a/packages/static-cs-lite/lib/util/imageFrame/get/paletteColor.js b/packages/static-cs-lite/lib/util/imageFrame/get/paletteColor.js new file mode 100644 index 0000000..3b80178 --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/get/paletteColor.js @@ -0,0 +1,54 @@ +/* eslint-disable no-plusplus, no-bitwise */ + +/** + * Returns palette color lut array. + * In case colorLutData has InlineBinary value it decodes the binary using lutDescriptor. + * + * @param {Object} colorLutData color look up table data + * @param {Array} colorLutDescriptor color look up table descriptor + * @returns + */ +function paletteColor(colorLutData, colorLutDescriptor) { + let result; + if (!colorLutDescriptor || !colorLutData) { + return undefined; + } + const numLutEntries = colorLutDescriptor[0]; + const bits = colorLutDescriptor[2]; + + const typedArrayToPaletteColorLUT = (typedArray) => { + const lut = []; + + if (bits === 16) { + let j = 0; + for (let i = 0; i < numLutEntries; i++) { + lut[i] = (typedArray[j++] + typedArray[j++]) << 8; + } + } else { + for (let i = 0; i < numLutEntries; i++) { + lut[i] = typedArray[i]; + } + } + return lut; + }; + + if (colorLutData.palette) { + result = colorLutData.palette; + } else if (colorLutData.InlineBinary) { + try { + const paletteStr = colorLutData.InlineBinary; + + const paletteBinaryStr = Buffer.from(paletteStr, "base64").toString("binary"); + + const paletteTypedArray = Uint8Array.from(paletteBinaryStr, (c) => c.charCodeAt(0)); + + result = typedArrayToPaletteColorLUT(paletteTypedArray); + } catch (e) { + console.log("Couldn't decode", colorLutData.InlineBinary, e); + } + } + + return result; +} + +module.exports = paletteColor; diff --git a/packages/static-cs-lite/lib/util/imageFrame/get/pixelDataIntType.js b/packages/static-cs-lite/lib/util/imageFrame/get/pixelDataIntType.js new file mode 100644 index 0000000..2e60cde --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/get/pixelDataIntType.js @@ -0,0 +1,33 @@ +const getMinMax = require("./pixelDataMinMax"); + +function pixelDataIntType(floatPixelData) { + const floatMinMax = getMinMax(floatPixelData); + const floatRange = Math.abs(floatMinMax.max - floatMinMax.min); + const intRange = 65535; + const slope = floatRange / intRange; + const intercept = floatMinMax.min; + const numPixels = floatPixelData.length; + const intPixelData = new Uint16Array(numPixels); + + let min = 65535; + + let max = 0; + + for (let i = 0; i < numPixels; i++) { + const rescaledPixel = Math.floor((floatPixelData[i] - intercept) / slope); + + intPixelData[i] = rescaledPixel; + min = Math.min(min, rescaledPixel); + max = Math.max(max, rescaledPixel); + } + + return { + min, + max, + intPixelData, + slope, + intercept, + }; +} + +module.exports = pixelDataIntType; diff --git a/packages/static-cs-lite/lib/util/imageFrame/get/pixelDataMinMax.js b/packages/static-cs-lite/lib/util/imageFrame/get/pixelDataMinMax.js new file mode 100644 index 0000000..2929bdd --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/get/pixelDataMinMax.js @@ -0,0 +1,24 @@ +function getMinMax(storedPixelData) { + // we always calculate the min max values since they are not always + // present in DICOM and we don't want to trust them anyway as cornerstone + // depends on us providing reliable values for these + let min = storedPixelData[0]; + + let max = storedPixelData[0]; + + let storedPixel; + const numPixels = storedPixelData.length; + + for (let index = 0; index < numPixels; index++) { + storedPixel = storedPixelData[index]; + min = Math.min(min, storedPixel); + max = Math.max(max, storedPixel); + } + + return { + min, + max, + }; +} + +module.exports = getMinMax; diff --git a/packages/static-cs-lite/lib/util/imageFrame/index.js b/packages/static-cs-lite/lib/util/imageFrame/index.js new file mode 100644 index 0000000..08df1b7 --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/index.js @@ -0,0 +1,7 @@ +const convert = require("./convert"); +const get = require("./get"); +const is = require("./is"); + +exports.convert = convert; +exports.get = get; +exports.is = is; diff --git a/packages/static-cs-lite/lib/util/imageFrame/is/colorImage.js b/packages/static-cs-lite/lib/util/imageFrame/is/colorImage.js new file mode 100644 index 0000000..59cf706 --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/is/colorImage.js @@ -0,0 +1,16 @@ +function isColorImage(imageFrame) { + const { photometricInterpretation } = imageFrame; + + return ( + photometricInterpretation === "RGB" || + photometricInterpretation === "PALETTE COLOR" || + photometricInterpretation === "YBR_FULL" || + photometricInterpretation === "YBR_FULL_422" || + photometricInterpretation === "YBR_PARTIAL_422" || + photometricInterpretation === "YBR_PARTIAL_420" || + photometricInterpretation === "YBR_RCT" || + photometricInterpretation === "YBR_ICT" + ); +} + +module.exports = isColorImage; diff --git a/packages/static-cs-lite/lib/util/imageFrame/is/index.js b/packages/static-cs-lite/lib/util/imageFrame/is/index.js new file mode 100644 index 0000000..e6bab4d --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/is/index.js @@ -0,0 +1,7 @@ +const colorImage = require("./colorImage"); +const modalityLut = require("./modalityLut"); +const jpegBaseline8BitColor = require("./jpegBaseline8BitColor"); + +exports.colorImage = colorImage; +exports.jpegBaseline8BitColor = jpegBaseline8BitColor; +exports.modalityLut = modalityLut; diff --git a/packages/static-cs-lite/lib/util/imageFrame/is/jpegBaseline8BitColor.js b/packages/static-cs-lite/lib/util/imageFrame/is/jpegBaseline8BitColor.js new file mode 100644 index 0000000..ddaedfe --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/is/jpegBaseline8BitColor.js @@ -0,0 +1,13 @@ +function isJPEGBaseline8BitColor(imageFrame, _transferSyntax) { + const { bitsAllocated, samplesPerPixel, transferSyntax: transferSyntaxFromFrame } = imageFrame; + const transferSyntax = _transferSyntax || transferSyntaxFromFrame; + let response = false; + + if (bitsAllocated === 8 && transferSyntax === "1.2.840.10008.1.2.4.50" && (samplesPerPixel === 3 || samplesPerPixel === 4)) { + response = true; + } + + return response; +} + +module.exports = isJPEGBaseline8BitColor; diff --git a/packages/static-cs-lite/lib/util/imageFrame/is/modalityLut.js b/packages/static-cs-lite/lib/util/imageFrame/is/modalityLut.js new file mode 100644 index 0000000..6f1515d --- /dev/null +++ b/packages/static-cs-lite/lib/util/imageFrame/is/modalityLut.js @@ -0,0 +1,10 @@ +function modalityLut(sopClassUid) { + // special case for XA and XRF + // https://groups.google.com/forum/#!searchin/comp.protocols.dicom/Modality$20LUT$20XA/comp.protocols.dicom/UBxhOZ2anJ0/D0R_QP8V2wIJ + 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 +} + +module.exports = modalityLut; diff --git a/packages/static-cs-lite/lib/util/index.js b/packages/static-cs-lite/lib/util/index.js new file mode 100644 index 0000000..8301b84 --- /dev/null +++ b/packages/static-cs-lite/lib/util/index.js @@ -0,0 +1,5 @@ +const imageFrameUtils = require("./imageFrame"); +const assertArrayDivisibility = require("./assertArrayDivisibility"); + +exports.imageFrameUtils = imageFrameUtils; +exports.assertArrayDivisibility = assertArrayDivisibility; diff --git a/packages/static-cs-lite/package.json b/packages/static-cs-lite/package.json new file mode 100644 index 0000000..ca0d9d9 --- /dev/null +++ b/packages/static-cs-lite/package.json @@ -0,0 +1,50 @@ +{ + "name": "@ohif/static-cs-lite", + "version": "0.0.1", + "description": "Cornerstone core lite version that runs on server side", + "publishConfig": { + "access": "public" + }, + "keywords": [ + "Cornerstone server side", + "DICOM" + ], + "author": "Rodolfo Ladeira ", + "contributors": [ + "Rodolfo Costa " + ], + "engines": { + "node": ">=14.18.1", + "npm": ">=6.14.15", + "yarn": ">=1.22.4" + }, + "repository": { + "type": "git", + "url": "https://github.com/OHIF/static-wado", + "directory": "packages/static-cs-lite" + }, + "bugs": { + "url": "https://github.com/OHIF/static-wado/issues" + }, + "homepage": "https://github.com/OHIF/static-wado#readme", + "license": "ISC", + "main": "lib/index.js", + "directories": { + "lib": "lib" + }, + "files": [ + "lib" + ], + "scripts": { + "test": "jest --config ./jest.config.js", + "build": "echo \"No build yet\" && exit 0", + "link:exec": "npm link", + "lint:fix": "npx eslint --fix" + }, + "dependencies": { + "cornerstone-core": "^2.6.1", + "dcmjs": "^0.19.6", + "jsdom": "^19.0.0", + "jsdom-context-require": "^4.0.4" + } +} diff --git a/packages/static-cs-lite/tests/e2e/index.js b/packages/static-cs-lite/tests/e2e/index.js new file mode 100644 index 0000000..07a4eae --- /dev/null +++ b/packages/static-cs-lite/tests/e2e/index.js @@ -0,0 +1,3 @@ +xdescribe("@ohif/static-cs-lite", () => { + test.todo("needs tests"); +}); diff --git a/packages/static-cs-lite/tests/unit/index.js b/packages/static-cs-lite/tests/unit/index.js new file mode 100644 index 0000000..07a4eae --- /dev/null +++ b/packages/static-cs-lite/tests/unit/index.js @@ -0,0 +1,3 @@ +xdescribe("@ohif/static-cs-lite", () => { + test.todo("needs tests"); +}); diff --git a/packages/static-wado-creator/README.md b/packages/static-wado-creator/README.md index 1edec95..30653c5 100644 --- a/packages/static-wado-creator/README.md +++ b/packages/static-wado-creator/README.md @@ -81,6 +81,10 @@ The options are: --no-recompress : Force no recompression [see](#to-recompress) + + --no-recompress-thumb + : Force no recompression for thumbnail + [see](#to-recompress-thumb) -o, --dir : Set output directory (default: "~/dicomweb") @@ -94,6 +98,10 @@ The options are: -r, --recompress : List of types to recompress separated by comma (choices: "uncompressed", "jp2", "jpeglossless", "rle", default: "uncompressed jp2") [see](#to-recompress) + + --recompress-thumb + : List of types to recompress thumb separated by comma (choices: "uncompressed", "jp2", "jpeglossless", "rle", default: "uncompressed jp2") + [see](#to-recompress-thumb) -s, --study : Write study metadata - on provided instances only (TO FIX}, (default: true) @@ -215,6 +223,36 @@ The table below shows the support for transfer syntax recompression(i.e use "... | 1.2.840.10008.1.2.5 | RLE Lossless | rle | 1.2.840.10008.1.2.4.80 | +### To recompress thumb +It tells thumbnail creation to use recompressed data or not. +The list of types MUST be necessarily a subset of recompress list. + +Obs: --no-recompress-thumb forces to not use compression at all. Use it if you want to disable even the default behavior. + +By default the recompression occurs for incoming types: uncompressed, jp2. This will recompress the types mentioned and will generate the thumbnails with the result of data recompression. + +``` +mkdicomweb ./folderName +``` + +Force recompression thumb. This will recompress the types: uncompressed jp2 rle, thumbnails creation will use original data for all types except jp2 (which will use recompress data). +``` +mkdicomweb -r uncompressed jp2 rle --recompress-thumb jp2 ./folderName +``` + +Skipping not recompressed data. This will recompress the types: uncompressed jp2, thumbnails creation will use original data for all types (since recompress-thumb list does not intersect with recompress list). +``` +mkdicomweb -r uncompressed jp2 --recompress-thumb rle ./folderName +``` + +Force no recompression thumb. This will recompress the types: uncompressed jp2 rle, but for thumbnails creation will use original data. +``` +mkdicomweb -r uncompressed jp2 rle --no-recompress-thumb ./folderName +``` + + + + ## Development ## TODO (Looking for help here!!) diff --git a/packages/static-wado-creator/dicom/jpeg8bit.dcm b/packages/static-wado-creator/dicom/jpeg8bit.dcm new file mode 100644 index 0000000..76cea0c Binary files /dev/null and b/packages/static-wado-creator/dicom/jpeg8bit.dcm differ diff --git a/packages/static-wado-creator/docs/archive-format.md b/packages/static-wado-creator/docs/archive-format.md index e948f5d..fbd9935 100644 --- a/packages/static-wado-creator/docs/archive-format.md +++ b/packages/static-wado-creator/docs/archive-format.md @@ -12,6 +12,7 @@ It is not necessary to have the current version and deduplicated instances locat * studies.gz containing the studies query studies?StudyInstanceUID= * series.gz containing the series query studies//series * deduplicated.gz containing the current study level deduplicated data (for all instances) +* studies//thumbail containing the study thumbnail in JPEG * series//metadata.gz containing the metadata for the series * series//instances.gz containing the instances query for this series * series//thumbnail containing the series thumbnail in JPEG diff --git a/packages/static-wado-creator/lib/index.js b/packages/static-wado-creator/lib/index.js index 8a0448f..a254d57 100644 --- a/packages/static-wado-creator/lib/index.js +++ b/packages/static-wado-creator/lib/index.js @@ -16,6 +16,8 @@ const HashDataWriter = require("./writer/HashDataWriter"); const VideoWriter = require("./writer/VideoWriter"); const { transcodeImageFrame, transcodeId, transcodeMetadata } = require("./operation/adapter/transcodeImage"); const mkdicomwebConfig = require("./mkdicomwebConfig"); +const ThumbnailWriter = require("./writer/ThumbnailWriter"); +const ThumbnailService = require("./operation/ThumbnailService"); function setStudyData(studyData) { this.studyData = studyData; @@ -40,6 +42,7 @@ class StaticWado { bulkdata: HashDataWriter(this.options), imageFrame: ImageFrameWriter(this.options), videoWriter: VideoWriter(this.options), + thumbWriter: ThumbnailWriter(this.options), completeStudy: CompleteStudyWriter(this.options), metadata: InstanceDeduplicate(this.options), deduplicated: DeduplicateWriter(this.options), @@ -113,6 +116,8 @@ class StaticWado { let bulkDataIndex = 0; let imageFrameIndex = 0; + const thumbnailService = new ThumbnailService(); + const generator = { bulkdata: async (bulkData, options) => { const _bulkDataIndex = bulkDataIndex; @@ -120,12 +125,23 @@ class StaticWado { return this.callback.bulkdata(targetId, _bulkDataIndex, bulkData, options); }, imageFrame: async (originalImageFrame) => { - const { imageFrame, id: transcodedId } = await transcodeImageFrame(id, targetId, originalImageFrame, dataSet, this.options); + const { imageFrame: transcodedImageFrame, id: transcodedId } = await transcodeImageFrame(id, targetId, originalImageFrame, dataSet, this.options); const currentImageFrameIndex = imageFrameIndex; imageFrameIndex += 1; - return this.callback.imageFrame(transcodedId, currentImageFrameIndex, imageFrame); + thumbnailService.queueThumbnail( + { + imageFrame: originalImageFrame, + transcodedImageFrame, + transcodedId, + id, + frameIndex: currentImageFrameIndex, + }, + this.options + ); + + return this.callback.imageFrame(transcodedId, currentImageFrameIndex, transcodedImageFrame); }, videoWriter: async (_dataSet) => this.callback.videoWriter(id, _dataSet), }; @@ -134,6 +150,7 @@ class StaticWado { const result = await getDataSet(dataSet, generator, this.options); const transcodedMeta = transcodeMetadata(result.metadata, id, this.options); + thumbnailService.generateThumbnails(dataSet, transcodedMeta, this.callback); await this.callback.metadata(targetId, transcodedMeta); diff --git a/packages/static-wado-creator/lib/operation/ThumbnailService.js b/packages/static-wado-creator/lib/operation/ThumbnailService.js new file mode 100644 index 0000000..24daab3 --- /dev/null +++ b/packages/static-wado-creator/lib/operation/ThumbnailService.js @@ -0,0 +1,145 @@ +const path = require("path"); +const glob = require("glob"); +const dicomCodec = require("@cornerstonejs/dicom-codec"); +const staticCS = require("@ohif/static-cs-lite"); +const fs = require("fs"); +const decodeImage = require("./adapter/decodeImage"); +const { shouldThumbUseTranscoded } = require("./adapter/transcodeImage"); + +/** + * Return the middle index of given list + * @param {*} listThumbs + * @returns + */ +function getThumbIndex(listThumbs) { + return Math.trunc(listThumbs / 2); +} +function internalGenerateThumbnail(originalImageFrame, dataset, metadata, transferSyntaxUid, doneCallback) { + decodeImage(originalImageFrame, dataset, transferSyntaxUid) + .then((decodeResult = {}) => { + const { imageFrame, imageInfo } = decodeResult; + const pixelData = dicomCodec.getPixelData(imageFrame, imageInfo, transferSyntaxUid); + staticCS.getRenderedBuffer(transferSyntaxUid, pixelData, metadata, doneCallback); + }) + .catch((error) => { + console.log(`Error while generating thumbnail:: ${error}`); + }); +} + +/** + * ThumbObj wrapper containing both original and transcoded content. + * + * @typedef {Object} ThumbObjWrapper + * @property {Object} id object containing study properties such as seriesUid, instanceUid + * @property {*} transcodedId transcoded object containing study properties such as seriesUid, instanceUid (this might be the same as original id depending on options) + * @property {*} imageFrame original image frame + * @property {*} transcodedImageFrame transcoded image frame (this might be the same as original frame depending on options) + * @property {number} frameIndex + */ + +/** + * ThumbObj containing imageFrame, id to be used. + * + * @typedef {Object} ThumbObj + * @property {Object} id object containing study properties such as seriesUid, instanceUid + * @property {*} imageFrame + * @property {number} frameIndex + */ + +/** + * Service to handle thumbnail creation. + * @class + * @constructor + * @property {Array} framesThumbnailObj Auxiliary list that contains all thumb obj. + * @property {ThumbObj} favoriteThumbnailObj Favorite thumb obj to be used on thumbnail creation + * @property {String} thumbFileName thumb file (with extension) to be used when creating the file. + * @public + */ +class ThumbnailService { + constructor() { + this.framesThumbnailObj = []; + this.favoriteThumbnailObj = {}; + this.thumbFileName = "thumbnail.jpeg"; + } + + /** + * Add thumbObj to list of possible thumbs. + * It might use original content or transcoded depending on options flags + * @param {ThumbObjWrapper} thumbObjWrapper + * @param {Object} programOpts + */ + queueThumbnail(thumbObjWrapper, programOpts) { + const { id, imageFrame, transcodedId, transcodedImageFrame, frameIndex } = thumbObjWrapper; + const getThumbContent = (originalContent, trancodedContent) => (shouldThumbUseTranscoded(id, programOpts) ? trancodedContent : originalContent); + + const thumbObj = { + imageFrame: getThumbContent(imageFrame, transcodedImageFrame), + id: getThumbContent(id, transcodedId), + frameIndex, + }; + + this.framesThumbnailObj.push(thumbObj); + + this.setFavoriteThumbnailObj(); + } + + setFavoriteThumbnailObj() { + const favIndex = getThumbIndex(this.framesThumbnailObj.length); + + this.favoriteThumbnailObj = this.framesThumbnailObj[favIndex]; + } + + /** + * Generates thumbnails for the levels: instances, series, study + * + * @param {*} dataSet + * @param {*} metadata + * @param {*} callback + */ + generateThumbnails(dataSet, metadata, callback) { + const { imageFrame, id } = this.favoriteThumbnailObj; + + internalGenerateThumbnail(imageFrame, dataSet, metadata, id.transferSyntaxUid, async (thumbBuffer) => { + if (thumbBuffer) { + await callback.thumbWriter(id.sopInstanceRootPath, this.thumbFileName, thumbBuffer); + + this.copySyncThumbnail(id.sopInstanceRootPath, id.seriesRootPath); + this.copySyncThumbnail(id.seriesRootPath, id.studyPath); + } + }); + } + + /** + * Copy thumbnail from sourceFolder to targetFolderPath. It usually is used to copy from child to parent folder. + * If there is already some existing thumbnails under targetFolderPath it actually get thumb on the index defined by getThumbIndex. + * Copy is a syncrhonous process. + * + * @param {*} sourceFolderPath + * @param {*} targetFolderPath + * @returns + */ + async copySyncThumbnail(sourceFolderPath, targetFolderPath) { + const parentPathLevel = path.join(sourceFolderPath, "../"); + const thumbFilesPath = glob.sync(`${parentPathLevel}*/${this.thumbFileName}`); + + const thumbIndex = getThumbIndex(thumbFilesPath.length); + const thumbFilePath = thumbFilesPath[thumbIndex]; + + if (!fs.existsSync(thumbFilePath)) { + console.log("Thumbnail to copy does not exists"); + return; + } + + try { + if (!fs.lstatSync(targetFolderPath).isDirectory()) { + throw new Error(`Target path: ${targetFolderPath} is not a directory`); + } + + fs.copyFileSync(thumbFilePath, `${targetFolderPath}/${this.thumbFileName}`); + } catch (e) { + console.log("The file could not be copied", e); + } + } +} + +module.exports = ThumbnailService; diff --git a/packages/static-wado-creator/lib/operation/adapter/decodeImage.js b/packages/static-wado-creator/lib/operation/adapter/decodeImage.js new file mode 100644 index 0000000..34de54a --- /dev/null +++ b/packages/static-wado-creator/lib/operation/adapter/decodeImage.js @@ -0,0 +1,9 @@ +const dicomCodec = require("@cornerstonejs/dicom-codec"); +const getImageInfo = require("./getImageInfo"); + +async function decodeImage(imageFrame, dataset, transferSyntaxUid) { + const imageInfo = getImageInfo(dataset); + return dicomCodec.decode(imageFrame, imageInfo, transferSyntaxUid); +} + +module.exports = decodeImage; diff --git a/packages/static-wado-creator/lib/operation/adapter/getImageInfo.js b/packages/static-wado-creator/lib/operation/adapter/getImageInfo.js new file mode 100644 index 0000000..622c8b9 --- /dev/null +++ b/packages/static-wado-creator/lib/operation/adapter/getImageInfo.js @@ -0,0 +1,21 @@ +/** + * Minimum image info data to be used on transcode process by dicom-codec api. + */ +function getImageInfo(dataSet) { + const rows = dataSet.uint16("x00280010"); + const columns = dataSet.uint16("x00280011"); + const bitsAllocated = dataSet.uint16("x00280100"); + const samplesPerPixel = dataSet.uint16("x00280002"); + const pixelRepresentation = dataSet.uint16("x00280103") || 0; // not yet being used. + + return { + bitsAllocated, + samplesPerPixel, + rows, // Number with the image rows/height + columns, // Number with the image columns/width, + signed: pixelRepresentation === 1, + pixelRepresentation, + }; +} + +module.exports = getImageInfo; diff --git a/packages/static-wado-creator/lib/operation/adapter/transcodeImage.js b/packages/static-wado-creator/lib/operation/adapter/transcodeImage.js index 17022c0..12de4e1 100644 --- a/packages/static-wado-creator/lib/operation/adapter/transcodeImage.js +++ b/packages/static-wado-creator/lib/operation/adapter/transcodeImage.js @@ -1,5 +1,6 @@ const dicomCodec = require("@cornerstonejs/dicom-codec"); const Tags = require("../../dictionary/Tags"); +const getImageInfo = require("./getImageInfo"); const transcodeOp = { none: 0, @@ -70,7 +71,7 @@ function getTranscoder(transferSyntaxUid) { * @param {*} options runner options */ function shouldTranscodeImageFrame(id, options) { - if (!options.recompressType) { + if (!options.recompress) { return false; } @@ -78,7 +79,30 @@ function shouldTranscodeImageFrame(id, options) { const { transferSyntaxUid } = id; const transcoder = getTranscoder(transferSyntaxUid); - return transcoder && transcoder.transferSyntaxUid && options.recompressType.includes(transcoder.alias); + return transcoder && transcoder.transferSyntaxUid && options.recompress.includes(transcoder.alias); + } + + return isValidTranscoder(); +} + +/** + * It tells if we should use transcoded image to generate thumb or not. + * + * @param {*} id + * @param {*} options + * @returns True if transcoder.alias is present on intersection of recompressThumb and recompress options. + */ +function shouldThumbUseTranscoded(id, options) { + if (!options.recompressThumb) { + return false; + } + + function isValidTranscoder() { + const { transferSyntaxUid } = id; + const transcoder = getTranscoder(transferSyntaxUid); + const result = transcoder && transcoder.transferSyntaxUid && options.recompress.includes(transcoder.alias) && options.recompressThumb.includes(transcoder.alias); + + return result; } return isValidTranscoder(); @@ -90,26 +114,6 @@ function transcodeLog(options, msg, error = "") { } } -/** - * Minimum image info data to be used on transcode process by dicom-codec api. - */ -function getImageInfo(dataSet) { - const rows = dataSet.uint16("x00280010"); - const columns = dataSet.uint16("x00280011"); - const bitsAllocated = dataSet.uint16("x00280100"); - const samplesPerPixel = dataSet.uint16("x00280002"); - const pixelRepresentation = dataSet.uint16("x00280103"); // not yet being used. - - return { - bitsAllocated, - samplesPerPixel, - rows, // Number with the image rows/height - columns, // Number with the image columns/width, - signed: pixelRepresentation === 1, - pixelRepresentation, - }; -} - /** * Transcode imageFrame. It uses transcodeMap to define the target encoding based on source encoding. * @@ -247,6 +251,7 @@ function transcodeMetadata(metadata, id, options) { } exports.shouldTranscodeImageFrame = shouldTranscodeImageFrame; +exports.shouldThumbUseTranscoded = shouldThumbUseTranscoded; exports.transcodeId = transcodeId; exports.transcodeImageFrame = transcodeImageFrame; exports.transcodeMetadata = transcodeMetadata; diff --git a/packages/static-wado-creator/lib/program/index.js b/packages/static-wado-creator/lib/program/index.js index d391f49..2a9f7c8 100644 --- a/packages/static-wado-creator/lib/program/index.js +++ b/packages/static-wado-creator/lib/program/index.js @@ -94,11 +94,22 @@ async function configureProgram(defaults) { defaultValue: ["uncompressed", "jp2"], choices: ["uncompressed", "jp2", "jpeglossless", "rle"], }, + { + key: "--recompress-thumb ", + description: "List of types to recompress thumb separated by comma", + defaultValue: ["uncompressed", "jp2"], + choices: ["uncompressed", "jp2", "jpeglossless", "rle"], + }, { key: "--no-recompress", description: "Force no recompression", defaultValue: false, }, + { + key: "--no-recompress-thumb", + description: "Force no recompression thumbnail", + defaultValue: false, + }, { key: "--path-deduplicated ", description: "Set the deduplicate data directory path (relative to dir)", diff --git a/packages/static-wado-creator/lib/util/IdCreator.js b/packages/static-wado-creator/lib/util/IdCreator.js index 6eccb03..0eab72b 100644 --- a/packages/static-wado-creator/lib/util/IdCreator.js +++ b/packages/static-wado-creator/lib/util/IdCreator.js @@ -3,15 +3,18 @@ const path = require("path"); function IdCreator({ directoryName, deduplicatedRoot, deduplicatedInstancesRoot }) { return (uids, filename) => { const studyPath = path.join(directoryName, "studies", uids.studyInstanceUid); + const seriesRootPath = path.join(studyPath, "series", uids.seriesInstanceUid); const sopInstanceRootPath = path.join(studyPath, "series", uids.seriesInstanceUid, "instances", uids.sopInstanceUid); const deduplicatedPath = path.join(deduplicatedRoot, uids.studyInstanceUid); const deduplicatedInstancesPath = path.join(deduplicatedInstancesRoot, uids.studyInstanceUid); const imageFrameRootPath = path.join(sopInstanceRootPath, "frames"); + return { ...uids, studyPath, deduplicatedPath, deduplicatedInstancesPath, + seriesRootPath, sopInstanceRootPath, imageFrameRootPath, filename, diff --git a/packages/static-wado-creator/lib/util/adaptProgramOpts.js b/packages/static-wado-creator/lib/util/adaptProgramOpts.js index 5bc874a..be345ba 100644 --- a/packages/static-wado-creator/lib/util/adaptProgramOpts.js +++ b/packages/static-wado-creator/lib/util/adaptProgramOpts.js @@ -8,6 +8,7 @@ module.exports = function adaptProgramOpts(programOpts, defaults) { study: isStudyData, clean: isClean, recompress, + recompressThumb, contentType, colourContentType, dir: rootDir, @@ -26,6 +27,7 @@ module.exports = function adaptProgramOpts(programOpts, defaults) { isStudyData, isClean, recompress, + recompressThumb, contentType, colourContentType, rootDir, diff --git a/packages/static-wado-creator/lib/writer/ThumbnailWriter.js b/packages/static-wado-creator/lib/writer/ThumbnailWriter.js new file mode 100644 index 0000000..4c8933b --- /dev/null +++ b/packages/static-wado-creator/lib/writer/ThumbnailWriter.js @@ -0,0 +1,21 @@ +const WriteStream = require("./WriteStream"); + +const ThumbnailWriter = (options) => { + const { verbose } = options; + + return async (filePath, fileName, thumbBuffer) => { + const writeStream = WriteStream(filePath, fileName, { + mkdir: true, + }); + + await writeStream.write(thumbBuffer); + + await writeStream.close(); + + if (verbose) { + console.log("Wrote thumbnail frame", filePath, fileName); + } + }; +}; + +module.exports = ThumbnailWriter; diff --git a/packages/static-wado-creator/package.json b/packages/static-wado-creator/package.json index f13d3f6..923c320 100644 --- a/packages/static-wado-creator/package.json +++ b/packages/static-wado-creator/package.json @@ -59,10 +59,13 @@ "verbose": true }, "dependencies": { - "@cornerstonejs/dicom-codec": "^0.1.2", + "@cornerstonejs/dicom-codec": "^0.1.3", + "@ohif/static-cs-lite": "^0.0.1", "@ohif/static-wado-util": "^0.6.1", + "canvas": "^2.9.1", "config-point": "^0.4.3", "dicom-parser": "^1.8.13", + "glob": "^7.2.0", "node-object-hash": "^2.3.10" }, "devDependencies": { diff --git a/packages/static-wado-util/lib/staticWadoConfig.js b/packages/static-wado-util/lib/staticWadoConfig.js index 5ba5c7a..42bcb28 100644 --- a/packages/static-wado-util/lib/staticWadoConfig.js +++ b/packages/static-wado-util/lib/staticWadoConfig.js @@ -5,7 +5,8 @@ const { staticWadoConfig } = ConfigPoint.register({ rootDir: "~/dicomweb", pathDeduplicated: "deduplicated", configurationFile: ["./static-wado.json5", "~/static-wado.json5"], - recompressType: "", + recompress: [""], + recompressThumb: [""], verbose: false, studyQuery: "studiesQueryByIndex", staticWadoAe: "DICOMWEB", diff --git a/packages/static-wado-webserver/lib/adapters/requestAdapters.mjs b/packages/static-wado-webserver/lib/adapters/requestAdapters.mjs index 99a207b..f5dd637 100644 --- a/packages/static-wado-webserver/lib/adapters/requestAdapters.mjs +++ b/packages/static-wado-webserver/lib/adapters/requestAdapters.mjs @@ -16,6 +16,15 @@ export const otherJsonMap = (req, res, next) => { next(); }; +/** + * Handles returning thumbnail jpeg + */ +export const thumbnailMap = (req, res, next) => { + res.setHeader("content-type", "image/jpeg"); + req.url = `${req.path}.jpeg`; + next(); +}; + export const htmlMap = (req, res, next) => { req.url = "/index.html"; next(); diff --git a/packages/static-wado-webserver/lib/controllers/server/staticControllers.mjs b/packages/static-wado-webserver/lib/controllers/server/staticControllers.mjs index a48f29e..b37da98 100644 --- a/packages/static-wado-webserver/lib/controllers/server/staticControllers.mjs +++ b/packages/static-wado-webserver/lib/controllers/server/staticControllers.mjs @@ -2,7 +2,7 @@ import express from "express"; import { gzipHeaders } from "../../adapters/responseAdapters.mjs"; -export function defaultGetStaticController(staticFilesDir) { +export function indexingStaticController(staticFilesDir) { return express.static(staticFilesDir, { index: "index.json.gz", setHeaders: gzipHeaders, @@ -11,3 +11,11 @@ export function defaultGetStaticController(staticFilesDir) { fallthrough: true, }); } + +export function nonIndexingStaticController(staticFilesDir) { + return express.static(staticFilesDir, { + index: false, + redirect: false, + fallthrough: true, + }); +} diff --git a/packages/static-wado-webserver/lib/routes/server/studies.mjs b/packages/static-wado-webserver/lib/routes/server/studies.mjs index 29ba446..fb42cd1 100644 --- a/packages/static-wado-webserver/lib/routes/server/studies.mjs +++ b/packages/static-wado-webserver/lib/routes/server/studies.mjs @@ -1,9 +1,9 @@ import { assertions } from "@ohif/static-wado-util"; -import { qidoMap, otherJsonMap } from "../../adapters/requestAdapters.mjs"; +import { qidoMap, otherJsonMap, thumbnailMap } from "../../adapters/requestAdapters.mjs"; import { defaultPostController as postController } from "../../controllers/server/commonControllers.mjs"; import { defaultNotFoundController as notFoundController } from "../../controllers/server/notFoundControllers.mjs"; import { defaultGetProxyController } from "../../controllers/server/proxyControllers.mjs"; -import { defaultGetStaticController as staticController } from "../../controllers/server/staticControllers.mjs"; +import { indexingStaticController, nonIndexingStaticController } from "../../controllers/server/staticControllers.mjs"; /** * Set studies (/studies) routes. @@ -14,12 +14,18 @@ import { defaultGetStaticController as staticController } from "../../controller */ export default function setRoutes(routerExpress, params, dir) { // adapt requests + routerExpress.get( + ["/studies/:studyUID/thumbnail", "/studies/:studyUID/series/:seriesUID/thumbnail", "/studies/:studyUID/series/:seriesUID/instances/:instanceUID/thumbnail"], + thumbnailMap + ); routerExpress.get(["/studies", "/studies/:studyUID/series", "/studies/:studyUID/series/:seriesUID/instances"], qidoMap); routerExpress.get("/studies/:studyUID/series/metadata", otherJsonMap); routerExpress.post(["/studies", "/studies/:studyUID/series", "/studies/:studyUID/series/:seriesUID/instances"], postController(params)); // Handle the QIDO queries - routerExpress.use(staticController(dir)); + routerExpress.use(indexingStaticController(dir)); + // serve static file content (non indexing) (thumbnail mostly) + routerExpress.use(nonIndexingStaticController(dir)); // fallback route to external SCP if (assertions.assertAeDefinition(params, "proxyAe") && !!params.staticWadoAe) { diff --git a/yarn.lock b/yarn.lock index ca82115..c97d2e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -978,10 +978,10 @@ resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjpeg/-/codec-openjpeg-0.2.0.tgz#3a688e89e30379dafea86aafafaa84b527df0e9f" integrity sha512-EzlDXrqffY3uSj7nWeWzHL5Jwyg3L/8ZvwNxwlOILy0RfKaIFySEOorfJwQTc6UjTg3V9V7QEv7K+1esci6uZw== -"@cornerstonejs/dicom-codec@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-codec/-/dicom-codec-0.1.2.tgz#e9311f38afdd965e571d2fed58b846e2f48cc9c6" - integrity sha512-fzwELk2vskIDgisqWVBqMK1+yUrSbuU9fk0ZyXz3YHRjHPU/q0E3y+pZy1/ykapPDD+lsMnHbx2s2yqYs2B2zQ== +"@cornerstonejs/dicom-codec@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-codec/-/dicom-codec-0.1.3.tgz#b5035e7eec4df9bb7ab6d29a526ac87b9e4f6769" + integrity sha512-z+YemNCIcMTebVU3SeLHCjDXxvz+J8hexVVhZFu0fXLRwsZPU7v4La44MfGWERte5YJVTJrTKw2uBtm8FsutAw== dependencies: "@cornerstonejs/codec-big-endian" "^0.1.0" "@cornerstonejs/codec-charls" "^0.2.0" @@ -1937,6 +1937,21 @@ npmlog "^4.1.2" write-file-atomic "^3.0.3" +"@mapbox/node-pre-gyp@^1.0.0": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc" + integrity sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2148,6 +2163,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": version "7.1.18" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.18.tgz#1a29abcc411a9c05e2094c98f9a1b7da6cdf49f8" @@ -2412,7 +2432,7 @@ acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.2.4, acorn@^8.7.0: +acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.0: version "8.7.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== @@ -2530,11 +2550,19 @@ aproba@^1.0.3: resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -aproba@^2.0.0: +"aproba@^1.0.3 || ^2.0.0", aproba@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + are-we-there-yet@~1.1.2: version "1.1.7" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" @@ -2856,6 +2884,13 @@ builtins@^1.0.3: resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= +builtins@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-2.0.1.tgz#42a4d6fe38973a7c185b435970d13e5e70f70f3c" + integrity sha512-XkkVe5QAb6guWPXTzpSrYpSlN3nqEmrrE2TkAr/tp7idSF6+MONh9WvKrAuR3HiKLvoSgmbs8l1U9IPmMrIoLw== + dependencies: + semver "^6.0.0" + byline@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" @@ -2932,6 +2967,15 @@ caniuse-lite@^1.0.30001312: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz#e11eba4b87e24d22697dae05455d5aea28550d5f" integrity sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ== +canvas@^2.9.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.9.1.tgz#58ec841cba36cef0675bc7a74ebd1561f0b476b0" + integrity sha512-vSQti1uG/2gjv3x6QLOZw7TctfufaerTWbVe+NSduHxxLGB+qf3kFgQ6n66DSnuoINtVUjrLLIK2R+lxrBG07A== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.0" + nan "^2.15.0" + simple-get "^3.0.3" + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -3094,6 +3138,11 @@ color-string@^1.6.0: color-name "^1.0.0" simple-swizzle "^0.2.2" +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + color@^3.1.3: version "3.2.1" resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" @@ -3179,7 +3228,7 @@ confusing-browser-globals@^1.0.10: resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== -console-control-strings@^1.0.0, console-control-strings@~1.1.0: +console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= @@ -3196,6 +3245,13 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +context-require@^1.1.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/context-require/-/context-require-1.2.2.tgz#886a61173afdc4b6f26970d7cee652c249fdb27b" + integrity sha512-A8pcwe19vTQWrc44KAfbJjbLNZk1eDK7eXJqbEViaZ7eoEXAbVkwfbf8YDNeRcswP0f8wq+R0IOMw7vNvgvnMw== + dependencies: + builtins "^2.0.0" + conventional-changelog-angular@^5.0.12: version "5.0.13" resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz#896885d63b914a70d4934b59d2fe7bde1832b28c" @@ -3318,6 +3374,11 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cornerstone-core@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/cornerstone-core/-/cornerstone-core-2.6.1.tgz#f9a29a49057f286e8c969c7020e979d567449d42" + integrity sha512-LAXjXwNnK2W65ZxpE7phVLjbijkYUrAQIGv5xqKjK8+dmIGlBxqvPHnyGwEqVDyKOWVrpXP6ymUPwrGGRwzgMg== + cors@^2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" @@ -3351,6 +3412,11 @@ cssom@^0.4.4: resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== +cssom@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" + integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== + cssom@~0.3.6: version "0.3.8" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" @@ -3384,6 +3450,15 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +data-urls@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.1.tgz#597fc2ae30f8bc4dbcf731fcd1b1954353afc6f8" + integrity sha512-Ds554NeT5Gennfoo9KN50Vh6tpgtvYEwraYjejXnyTpu1C7oXKxdFk75REooENHE8ndTVOJuv+BEs4/J/xcozw== + dependencies: + abab "^2.0.3" + whatwg-mimetype "^3.0.0" + whatwg-url "^10.0.0" + dateformat@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" @@ -3451,7 +3526,7 @@ decamelize@^1.1.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decimal.js@^10.2.1: +decimal.js@^10.2.1, decimal.js@^10.3.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== @@ -3461,6 +3536,13 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -3530,6 +3612,11 @@ detect-indent@^6.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -3586,6 +3673,13 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +domexception@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" + integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== + dependencies: + webidl-conversions "^7.0.0" + dot-prop@^5.1.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -4271,6 +4365,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -4344,6 +4447,21 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -4485,7 +4603,7 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" -glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -4626,6 +4744,13 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -4656,6 +4781,15 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -4697,7 +4831,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: +iconv-lite@0.6.3, iconv-lite@^0.6.2: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -4826,6 +4960,14 @@ ipaddr.js@1.9.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +is-absolute@^0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-0.2.6.tgz#20de69f3db942ef2d87b9c2da36f172235b1b5eb" + integrity sha1-IN5p89uULvLYe5wto28XIjWxtes= + dependencies: + is-relative "^0.2.1" + is-windows "^0.2.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -4973,6 +5115,13 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-relative@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5" + integrity sha1-0n9MfVFtF1+2ENuEu+7yPDvJeqU= + dependencies: + is-unc-path "^0.1.1" + is-shared-array-buffer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" @@ -5016,6 +5165,13 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-unc-path@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-0.1.2.tgz#6ab053a72573c10250ff416a3814c35178af39b9" + integrity sha1-arBTpyVzwQJQ/0FqOBTDUXivObk= + dependencies: + unc-path-regex "^0.1.0" + is-weakref@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" @@ -5023,6 +5179,11 @@ is-weakref@^1.0.1: dependencies: call-bind "^1.0.2" +is-windows@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" + integrity sha1-3hqm1j6indJIc3tp8f+LgALSEIw= + isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -5520,6 +5681,14 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= +jsdom-context-require@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/jsdom-context-require/-/jsdom-context-require-4.0.4.tgz#3976edd261e11cf00baa980b635138d602bdeef6" + integrity sha512-DlkAZsR0G6FfUiHKYahKAyDOJjBKW5h6x5z6iCft9cmXEJw6RhvwZ1JVFyH41on63cPEQrC/8N26ESxDAFHizw== + dependencies: + context-require "^1.1.0" + lasso-resolve-from "^1.2.0" + jsdom@^16.6.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" @@ -5553,6 +5722,39 @@ jsdom@^16.6.0: ws "^7.4.6" xml-name-validator "^3.0.0" +jsdom@^19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-19.0.0.tgz#93e67c149fe26816d38a849ea30ac93677e16b6a" + integrity sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A== + dependencies: + abab "^2.0.5" + acorn "^8.5.0" + acorn-globals "^6.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.1" + decimal.js "^10.3.1" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.0" + parse5 "6.0.1" + saxes "^5.0.1" + symbol-tree "^3.2.4" + tough-cookie "^4.0.0" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^10.0.0" + ws "^8.2.3" + xml-name-validator "^4.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -5656,6 +5858,23 @@ kuler@^2.0.0: resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== +lasso-caching-fs@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/lasso-caching-fs/-/lasso-caching-fs-1.0.2.tgz#9be4eb1f06aac1260344caeaef42c2f0086eb10d" + integrity sha1-m+TrHwaqwSYDRMrq70LC8AhusQ0= + dependencies: + raptor-async "^1.1.2" + +lasso-resolve-from@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/lasso-resolve-from/-/lasso-resolve-from-1.2.0.tgz#bfb234467afb69b5309f568ba459cc8320621c6e" + integrity sha1-v7I0Rnr7abUwn1aLpFnMgyBiHG4= + dependencies: + is-absolute "^0.2.3" + lasso-caching-fs "^1.0.1" + raptor-util "^1.0.10" + resolve-from "^2.0.0" + lerna@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/lerna/-/lerna-4.0.0.tgz#b139d685d50ea0ca1be87713a7c2f44a5b678e9e" @@ -5875,7 +6094,7 @@ make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.0: +make-dir@^3.0.0, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -6014,6 +6233,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-response@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -6196,6 +6420,11 @@ mute-stream@0.0.8, mute-stream@~0.0.4: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +nan@^2.15.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" + integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -6424,6 +6653,16 @@ npmlog@^4.1.2: gauge "~2.7.3" set-blocking "~2.0.0" +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" @@ -6439,7 +6678,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4, object-assign@^4.1.0: +object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -6503,7 +6742,7 @@ on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@1.4.0, once@^1.3.0, once@^1.4.0: +once@1.4.0, once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -7002,6 +7241,16 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +raptor-async@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/raptor-async/-/raptor-async-1.1.3.tgz#b83c3c9b603dc985c2c3a9f78d2b4073e6f6024c" + integrity sha1-uDw8m2A9yYXCw6n3jStAc+b2Akw= + +raptor-util@^1.0.10: + version "1.1.2" + resolved "https://registry.yarnpkg.com/raptor-util/-/raptor-util-1.1.2.tgz#f2ee8076a9ae3eae2e65672e46a220074fa2dff3" + integrity sha1-8u6AdqmuPq4uZWcuRqIgB0+i3/M= + raw-body@2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c" @@ -7253,6 +7502,11 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-from@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" + integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -7413,7 +7667,7 @@ serve-static@1.14.2: parseurl "~1.3.3" send "0.17.2" -set-blocking@~2.0.0: +set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -7456,6 +7710,20 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55" + integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -7823,7 +8091,7 @@ tar@^4.4.12: safe-buffer "^5.2.1" yallist "^3.1.1" -tar@^6.0.2, tar@^6.1.0: +tar@^6.0.2, tar@^6.1.0, tar@^6.1.11: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== @@ -7961,6 +8229,13 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -8119,6 +8394,11 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" +unc-path-regex@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -8279,6 +8559,13 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" +w3c-xmlserializer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923" + integrity sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg== + dependencies: + xml-name-validator "^4.0.0" + walker@^1.0.7: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" @@ -8308,6 +8595,11 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" @@ -8315,11 +8607,31 @@ whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + +whatwg-url@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-10.0.0.tgz#37264f720b575b4a311bd4094ed8c760caaa05da" + integrity sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -8362,7 +8674,7 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" -wide-align@^1.1.0: +wide-align@^1.1.0, wide-align@^1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== @@ -8475,11 +8787,21 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== +ws@^8.2.3: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml-name-validator@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" + integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"