import { fromBlob, fromUrl, Pool, getDecoder, globals } from "https://cdn.jsdelivr.net/npm/geotiff@2.1.2/+esm"
// import { fromBlob, fromUrl, Pool, getDecoder, globals } from "./geotiff.js"
/* Class representing an Imagebox3 instance of a whole slide image. */
class Imagebox3 {
/**
* Create an Imagebox3 instance.
* @constructor
* @param {File|string} imageSource - (Required) The local File object or the remote URL referencing the TIFF file.
* @param {number} [numWorkers] - The number of web workers to be used to to decode image tiles. Defaults to 0, meaning all decoding operations are performed on the main thread.
*/
constructor(imageSource, numWorkers) {
if (imageSource instanceof File || typeof (imageSource) === 'string') {
this.imageSource = typeof (imageSource) === 'string' ? decodeURIComponent(imageSource) : imageSource
} else {
throw new Error("Unsupported image type for ImageBox3")
}
this.tiff = undefined
this.numWorkers = Number.isInteger(numWorkers) ? numWorkers : 0
this.workerPool = undefined
this.supportedDecoders = [5, 7, 8, 32773, 32946, 34887, 50001]
}
/**
* Initialize the created Imagebox3 instance by retrieving all Image File Directory metadata from the TIFF file.
* This function needs to be called after instantiation. Any operations should be performed only after the returned Promise is fulfilled.
* @async
* @return {Promise}
*/
async init() {
this.tiff = await getImagePyramid(this.imageSource, true)
this.tiff.allImages = await getAllImagesInPyramid(this.tiff)
this.tiff.imageSets = await getImageSetsInPyramid(this.tiff)
this.tiff.slideImages = await getSlideImagesInPyramid(this.tiff)
const { width: maxWidth, height: maxHeight } = this.tiff.slideImages.reduce((largestImageDimensions, image) => {
if (largestImageDimensions.width < image.getWidth() && largestImageDimensions.height < image.getHeight()) {
largestImageDimensions.width = image.getWidth()
largestImageDimensions.height = image.getHeight()
}
return largestImageDimensions
}, { width: 0, height: 0 })
this.tiff.maxWidth = maxWidth
this.tiff.maxHeight = maxHeight
await this.getSupportedDecoders()
await this.createWorkerPool(this.numWorkers)
}
/**
* Retrieve the image source to the TIFF file that the current Imagebox3 instance is using.
* @returns {File|string} - The local File object or the remote URL referencing the TIFF file.
*/
getImageSource() {
return this.imageSource
}
/**
* Switch image sources without destroying the Imagebox3 instance. Recommended when a new image needs to be loaded instead of
* re-instantiating Imagebox3, so as to avoid spawning a new worker pool. Await the fulfilment of the returned Promise
* before running further operations.
* @async
* @param {File|string} newImageSource - The local File object or the remote URL referencing the new TIFF file.
* @returns {Promise}
*/
async changeImageSource(newImageSource) {
// USE INSTEAD OF RE-INSTANTIATING IMAGEBOX3 FOR EACH NEW IMAGE TO AVOID SPAWNING A NEW WORKER POOL EVERY TIME!!!
// IT IS NECESSARY TO HAVE THE WORKER POOL CREATION BE TIED TO THE INSTANTIATION BECAUSE COMPRESSION METHODS COULD BE DIFFERENT
// IN DIFFERENT IMAGES, MEANING THE SAME WORKER POOL MIGHT NOT BE REPURPOSEABLE.
this.imageSource = typeof (newImageSource) === 'string' ? decodeURIComponent(newImageSource) : newImageSource
await this.init()
}
/**
* Retrieve the TIFF metadata obejct containing information about the image pyramid.
* @returns {Object}
*/
getPyramid() {
return this.tiff
}
/**
* Create a new pool of web workers to be used for decoding image tiles based on the supported decoders. Highly recommended if retrieving
* multiple patches parallelly. Destroys all previously created Imagebox3 worker pools before creating a new one.
* @async
* @param {number} numWorkers The number of web workers in the decoder pool. Defaults to 0, meaning all operations will be performed on the main thread.
* @returns {Object}
*/
async createWorkerPool(numWorkers) {
// TODO: Load only the decoders necessary for the current image, instead of having them all active. Not a major resource drain, but still.
if (this.workerPool) {
destroyPool(this.workerPool)
}
this.workerPool = createPool(await this.tiff.getImage(0), numWorkers, this.supportedDecoders)
this.numWorkers = numWorkers
return this.workerPool
}
/**
* Destroy the current pool of web workers. Highly recommended if re-instantiating Imagebox3 to avoid zombie web workers
* from slowing down the client, since web workers are not destroyed even if their initiator is garbage-colleged.
*/
destroyWorkerPool() {
// HIGHLY RECOMMENDED WHEN LOADING A NEW IMAGE!!! OTHERWISE EACH NEW INSTANTATION WILL CREATE A NEW WORKER POOL!!!!!
destroyPool(this.workerPool)
}
/**
* Get the compression scheme identifiers (as specified in the TIFF format specification) that Imagebox3 can decode.
* JPEG/LZW/Deflate/WebP are automatically supported, along with JPEG-2000 which is added as an external decoder
* if the image needs it.
* @async
* @returns {string[]}
*/
async getSupportedDecoders() {
// TODO: Make it possible for users to provide their own decoders.
this.supportedDecoders = this.supportedDecoders || await setupDecoders()
return this.supportedDecoders
}
/**
* Retrieve basic information about the largest image in the TIFF. Currently returns the image width and height,
* and the pixels per micron corresponding to the slide.
* @async
* @returns {Object}
*/
async getInfo() {
return await getImageInfo(this.tiff)
}
/**
* Retrieve a thumbnail representation of the whole slide image from the TIFF.
* @async
* @param {number} thumbnailWidth - The width of the thumbnail image to be returned.
* @param {number} thumbnailHeight - The height of the thumbnail image to be returned.
* @returns {Blob}
*/
async getThumbnail(thumbnailWidth, thumbnailHeight) {
const tileParams = {
thumbnailWidthToRender: thumbnailWidth,
thumbnailHeightToRender: thumbnailHeight
}
return await getImageThumbnail(this.tiff, tileParams, this.workerPool)
}
/**
* Retrieve a single tile from the whole slide corresponding to the bounding box formed by the parameters and at the specified resolution. The bounding box
* should always be parameterized as per the largest image representation in the TIFF pyramid. By default, the optimal image from which to retrieve
* the tile is estimated heuristically, based on the size of the bounding box and the requested resolution.
* @async
* @param {number} topLeftX - The X coordinate of the top-left corner of the bounding box for the tile to be retrieved.
* @param {number} topLeftY - The Y coordinate of the top-left corner of the bounding box for the tile to be retrieved.
* @param {number} tileWidthInImage - The width of the bounding box for the tile.
* @param {number} tileHeightInImage - The height of the bounding box for the tile.
* @param {number} tileResolutionToRender - The resolution in which the tile should be returned.
* @param {number} [imageIndex] - The index of the image in the pyramid to be specifically used to retrieve the tile.
* @returns {Blob}
*/
async getTile(topLeftX, topLeftY, tileWidthInImage, tileHeightInImage, tileResolutionToRender, imageIndex = -1) {
const tileParams = {
tileX: topLeftX,
tileY: topLeftY,
tileWidth: tileWidthInImage,
tileHeight: tileHeightInImage,
tileResolution: tileResolutionToRender || Math.max(tileWidthInImage, tileHeightInImage),
}
return await getImageTile(this.tiff, tileParams, this.workerPool, imageIndex)
}
/**
* Retrieve the number of images in the TIFF image pyramid.
* @returns {number}
*/
getImageCount() {
return this.tiff?.ifdRequests?.length
}
}
const utils = {
parseTileParams: (tileParams) => {
// Parse tile params into tile coordinates and size
const parsedTileParams = Object.entries(tileParams).reduce((parsed, [key, val]) => {
if (!isNaN(val)) {
parsed[key] = parseInt(val)
}
return parsed
}, {})
return parsedTileParams
},
getImageByRatio: async (imagePyramid, tileWidth, tileWidthToRender) => {
// Return the index of the appropriate image in the pyramid for the requested tile
// by comparing the ratio of the width of the requested tile and the requested resolution,
// and comparing it against the ratios of the widths of all images in the pyramid to the largest image.
// This is a heuristic that is used to determine the best image to use for a given tile request.
// Could be optimized further?
const tileWidthRatio = Math.floor(tileWidth / tileWidthToRender)
let bestImageIndex = 0
let imageWidthRatios = []
// if (!tiffPyramid.imageWidthRatios) {
// tiffPyramid.imageWidthRatios = []
const slideImages = imagePyramid.slideImages || await getSlideImagesInPyramid(imagePyramid)
for (let imageIndex = 0; imageIndex < slideImages.length; imageIndex++) {
const imageWidth = slideImages[imageIndex].getWidth()
const maxImageWidth = slideImages[0].getWidth()
imageWidthRatios.push(maxImageWidth / imageWidth)
}
// }
//
const sortedRatios = [...imageWidthRatios].sort((a, b) => a - b).slice(0, -1) // Remove thumbnail from consideration
// If the requested resolution is less than 1/8th the requested tile width, the smallest image should suffice.
if (tileWidthRatio >= sortedRatios[sortedRatios.length - 1]) {
bestImageIndex = imageWidthRatios.indexOf(sortedRatios[sortedRatios.length - 1])
}
else if (tileWidthRatio <= sortedRatios[1]) {
// Return the largest image for high magnification tiles
bestImageIndex = imageWidthRatios.indexOf(sortedRatios[0])
}
// If the requested resolution is between the highest and lowest resolution images in the pyramid,
// return the smallest image with resolution ratio greater than the requested resolution.
else {
const otherRatios = sortedRatios.slice(1, sortedRatios.length - 1)
if (otherRatios.length === 1) {
bestImageIndex = imageWidthRatios.indexOf(otherRatios[0])
} else {
otherRatios.forEach((ratio, index) => {
if (tileWidthRatio >= ratio && tileWidthRatio <= sortedRatios[index + 2]) {
bestImageIndex = imageWidthRatios.indexOf(otherRatios[index])
}
})
}
}
return slideImages[bestImageIndex]
},
handleConversion: (data, imageFileDirectory) => {
// Converters copied from pearcetm/GeoTIFFTileSource
const Converters = {
RGBAfromYCbCr: (input) => {
const rgbaRaster = new Uint8ClampedArray(input.length * 4 / 3);
let i, j;
for (i = 0, j = 0; i < input.length; i += 3, j += 4) {
const y = input[i];
const cb = input[i + 1];
const cr = input[i + 2];
rgbaRaster[j] = (y + (1.40200 * (cr - 0x80)));
rgbaRaster[j + 1] = (y - (0.34414 * (cb - 0x80)) - (0.71414 * (cr - 0x80)));
rgbaRaster[j + 2] = (y + (1.77200 * (cb - 0x80)));
rgbaRaster[j + 3] = 255;
}
return rgbaRaster;
},
RGBAfromRGB: (input) => {
const rgbaRaster = new Uint8ClampedArray(input.length * 4 / 3);
let i, j;
for (i = 0, j = 0; i < input.length; i += 3, j += 4) {
rgbaRaster[j] = input[i];
rgbaRaster[j + 1] = input[i + 1];
rgbaRaster[j + 2] = input[i + 2];
rgbaRaster[j + 3] = 255;
}
return rgbaRaster;
},
RGBAfromWhiteIsZero: (input, max) => {
const rgbaRaster = new Uint8ClampedArray(input.length * 4);
let value;
for (let i = 0, j = 0; i < input.length; ++i, j += 3) {
value = 256 - (input[i] / max * 256);
rgbaRaster[j] = value;
rgbaRaster[j + 1] = value;
rgbaRaster[j + 2] = value;
rgbaRaster[j + 3] = 255;
}
return rgbaRaster;
},
RGBAfromBlackIsZero: (input, max) => {
const rgbaRaster = new Uint8ClampedArray(input.length * 4);
let value;
for (let i = 0, j = 0; i < input.length; ++i, j += 3) {
value = input[i] / max * 256;
rgbaRaster[j] = value;
rgbaRaster[j + 1] = value;
rgbaRaster[j + 2] = value;
rgbaRaster[j + 3] = 255;
}
return rgbaRaster;
},
RGBAfromPalette: (input, colorMap) => {
const rgbaRaster = new Uint8ClampedArray(input.length * 4);
const greenOffset = colorMap.length / 3;
const blueOffset = colorMap.length / 3 * 2;
for (let i = 0, j = 0; i < input.length; ++i, j += 3) {
const mapIndex = input[i];
rgbaRaster[j] = colorMap[mapIndex] / 65536 * 256;
rgbaRaster[j + 1] = colorMap[mapIndex + greenOffset] / 65536 * 256;
rgbaRaster[j + 2] = colorMap[mapIndex + blueOffset] / 65536 * 256;
rgbaRaster[j + 3] = 255;
}
return rgbaRaster;
},
RGBAfromCMYK: (input) => {
const rgbaRaster = new Uint8ClampedArray(input.length);
for (let i = 0, j = 0; i < input.length; i += 4, j += 4) {
const c = input[i];
const m = input[i + 1];
const y = input[i + 2];
const k = input[i + 3];
rgbaRaster[j] = 255 * ((255 - c) / 256) * ((255 - k) / 256);
rgbaRaster[j + 1] = 255 * ((255 - m) / 256) * ((255 - k) / 256);
rgbaRaster[j + 2] = 255 * ((255 - y) / 256) * ((255 - k) / 256);
rgbaRaster[j + 3] = 255;
}
return rgbaRaster;
},
RGBAfromCIELab: (input) => {
// from https://github.com/antimatter15/rgb-lab/blob/master/color.js
const Xn = 0.95047;
const Yn = 1.00000;
const Zn = 1.08883;
const rgbaRaster = new Uint8ClampedArray(input.length * 4 / 3);
for (let i = 0, j = 0; i < input.length; i += 3, j += 4) {
const L = input[i + 0];
const a_ = input[i + 1] << 24 >> 24; // conversion from uint8 to int8
const b_ = input[i + 2] << 24 >> 24; // same
let y = (L + 16) / 116;
let x = (a_ / 500) + y;
let z = y - (b_ / 200);
let r;
let g;
let b;
x = Xn * ((x * x * x > 0.008856) ? x * x * x : (x - (16 / 116)) / 7.787);
y = Yn * ((y * y * y > 0.008856) ? y * y * y : (y - (16 / 116)) / 7.787);
z = Zn * ((z * z * z > 0.008856) ? z * z * z : (z - (16 / 116)) / 7.787);
r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986);
g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415);
b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570);
r = (r > 0.0031308) ? ((1.055 * (r ** (1 / 2.4))) - 0.055) : 12.92 * r;
g = (g > 0.0031308) ? ((1.055 * (g ** (1 / 2.4))) - 0.055) : 12.92 * g;
b = (b > 0.0031308) ? ((1.055 * (b ** (1 / 2.4))) - 0.055) : 12.92 * b;
rgbaRaster[j] = Math.max(0, Math.min(1, r)) * 255;
rgbaRaster[j + 1] = Math.max(0, Math.min(1, g)) * 255;
rgbaRaster[j + 2] = Math.max(0, Math.min(1, b)) * 255;
rgbaRaster[j + 3] = 255;
}
return rgbaRaster;
}
}
const { PhotometricInterpretation } = imageFileDirectory;
let imageData;
switch (PhotometricInterpretation) {
case globals.photometricInterpretations.WhiteIsZero: // grayscale, white is zero
imageData = Converters.RGBAfromWhiteIsZero(data, 2 ** imageFileDirectory.BitsPerSample[0]);
break;
case globals.photometricInterpretations.BlackIsZero: // grayscale, white is zero
imageData = Converters.RGBAfromBlackIsZero(data, 2 ** imageFileDirectory.BitsPerSample[0]);
break;
case globals.photometricInterpretations.RGB: // RGB
imageData = Converters.RGBAfromRGB(data);
break;
case globals.photometricInterpretations.Palette: // colormap
imageData = Converters.RGBAfromPalette(data, 2 ** imageFileDirectory.colorMap);
break;
// case globals.photometricInterpretations.TransparencyMask: // Transparency Mask
// break;
case globals.photometricInterpretations.CMYK: // CMYK
imageData = Converters.RGBAfromCMYK(data);
break;
case globals.photometricInterpretations.YCbCr: // YCbCr
imageData = Converters.RGBAfromYCbCr(data);
break;
case globals.photometricInterpretations.CIELab: // CIELab
imageData = Converters.RGBAfromCIELab(data);
break;
}
return imageData
},
convertToImageBlob: async (data, width, height, imageFileDirectory) => {
// TODO: Write Node.js module to convert to image
const imageData = await utils.handleConversion(data, imageFileDirectory)
const cv = new OffscreenCanvas(width, height) // Use OffscreenCanvas so it works in workers as well.
const ctx = cv.getContext("2d")
ctx.putImageData(new ImageData(imageData, width, height), 0, 0)
const blob = await cv.convertToBlob({
type: "image/png",
quality: 1.0,
})
return blob
}
}
const setupDecoders = async () => {
const baseURL = import.meta.url.split("/").slice(0, -1).join("/");
const decodersJSON_URL = `${baseURL}/decoders/decoders.json`;
return await (await fetch(decodersJSON_URL)).json()
}
export const getImagePyramid = async (imageSource, cache = true) => {
let tiffPyramid
try {
const headers = cache === false ? { headers: { 'Cache-Control': "no-cache, no-store" } } : {}
tiffPyramid = tiffPyramid || (imageSource instanceof File ? await fromBlob(imageSource) : await fromUrl(imageSource, headers))
}
catch (e) {
console.error("Couldn't get images", e)
if (cache) { // Retry in case Cache-Control is not part of Access-Control-Allow-Headers in preflight response
return await getImagePyramid(imageSource, !cache)
}
}
return tiffPyramid
}
export const getAllImagesInPyramid = async (imagePyramid) => {
if (typeof (imagePyramid?.ifdRequests) !== 'object') {
throw new Error("Malformed image pyramid. Please retry pyramid creation using the `getImagePyramid()` method.")
}
const imageCount = await imagePyramid.getImageCount()
const imageRequests = [...Array(imageCount)].map((_, ind) => imagePyramid.getImage(ind))
const resolvedPromises = await Promise.allSettled(imageRequests)
const resolvedImages = resolvedPromises.filter((promise) => promise.status === 'fulfilled').map(promise => promise.value)
return resolvedImages
}
export const getImageSetsInPyramid = async (imagePyramid) => {
// Get all sets of images in the pyramid, based on aspect ratio differences. For instance, there could be a set of images at different
// resolutions corresponding to the whole slide image, another set corresponding to a meta-image and so on.
if (typeof (imagePyramid?.ifdRequests) !== 'object') {
throw new Error("Malformed image pyramid. Please retry pyramid creation using the `getImagePyramid()` method.")
}
const allImages = imagePyramid.allImages || await getAllImagesInPyramid(imagePyramid)
const ASPECT_RATIO_TOLERANCE = 0.01
const imageSets = allImages
.filter(image => image.fileDirectory.photometricInterpretation !== globals.photometricInterpretations.TransparencyMask)
.sort((image1, image2) => image2.getWidth() - image1.getWidth())
.reduce((sets, image) => {
const aspectRatio = image.getWidth() / image.getHeight()
const aspectRatioSetIndex = sets.findIndex(set => Math.abs(set[0].getWidth() / set[0].getHeight() - aspectRatio) < ASPECT_RATIO_TOLERANCE)
if (aspectRatioSetIndex !== -1) {
sets[aspectRatioSetIndex].push(image)
} else {
sets.push([image])
}
return sets
}, [])
return imageSets
}
export const getSlideImagesInPyramid = async (imagePyramid) => {
// Get all images in the pyramid corresponding to the whole slide image. Filter out any meta-images or those with transparent masks.
if (typeof (imagePyramid?.ifdRequests) !== 'object') {
throw new Error("Malformed image pyramid. Please retry pyramid creation using the `getImagePyramid()` method.")
}
const imageSets = imagePyramid.imageSets || await getImageSetsInPyramid(imagePyramid)
const bestSet = imageSets.reduce((largestImageSet, set) => {
if (largestImageSet.length === 0 || largestImageSet[0].getWidth() < set[0].getWidth() || (largestImageSet[0].getWidth() === set[0].getWidth() && largestImageSet.length < set.length)) {
largestImageSet = set
}
return largestImageSet
}, [])
return bestSet
}
export const getImageInfo = async (imagePyramid) => {
// Get basic information about the image (width, height, MPP for now)
if (typeof (imagePyramid?.ifdRequests) !== 'object') {
throw new Error("Malformed image pyramid. Please retry pyramid creation using the `getImagePyramid()` method.")
}
const slideImages = imagePyramid.slideImages || await getSlideImagesInPyramid(imagePyramid)
const largestImage = slideImages[0]
let pixelsPerMicron = undefined
if (largestImage?.fileDirectory?.ImageDescription && largestImage.fileDirectory.ImageDescription.includes("MPP")) {
const micronsPerPixel = largestImage.fileDirectory.ImageDescription.split("|").find(s => s.includes("MPP")).split("=")[1].trim()
pixelsPerMicron = 1 / parseFloat(micronsPerPixel)
}
const imageInfo = {
'width': largestImage.getWidth(),
'height': largestImage.getHeight(),
pixelsPerMicron
}
return imageInfo
}
export const getImageThumbnail = async (imagePyramid, tileParams, pool) => {
if (typeof (imagePyramid?.ifdRequests) !== 'object') {
throw new Error("Malformed image pyramid. Please retry pyramid creation using the `getImagePyramid()` method.")
}
const parsedTileParams = utils.parseTileParams(tileParams)
let { thumbnailWidthToRender, thumbnailHeightToRender } = parsedTileParams
if (!Number.isInteger(thumbnailWidthToRender) && !Number.isInteger(thumbnailHeightToRender)) {
throw new Error(`Thumbnail Request missing critical parameters: thumbnailWidthToRender:${thumbnailWidthToRender}, thumbnailHeightToRender:${thumbnailHeightToRender}`)
}
let imageWidth = imagePyramid.maxWidth
let imageHeight = imagePyramid.maxHeight
if (!imageWidth || !imageHeight) {
const imageInfo = await (await getImageInfo(imagePyramid)).json()
imageWidth = imageInfo.width
imageHeight = imageInfo.height
}
const tileSize = Number.isInteger(thumbnailWidthToRender) && Number.isInteger(thumbnailHeightToRender) ? Math.max(thumbnailWidthToRender, thumbnailHeightToRender) : (Number.isInteger(thumbnailWidthToRender) ? thumbnailWidthToRender : thumbnailHeightToRender)
const thumbnailParams = {
'tileX': 0,
'tileY': 0,
'tileWidth': imageWidth,
'tileHeight': imageHeight,
'tileSize': tileSize
}
const thumbnailImage = await getImageTile(imagePyramid, thumbnailParams, pool)
return thumbnailImage
}
export const getImageTile = async (imagePyramid, tileParams, pool, imageIndex = -1) => {
// Get individual tiles from the appropriate image in the pyramid.
if (typeof (imagePyramid?.ifdRequests) !== 'object') {
throw new Error("Malformed image pyramid. Please retry pyramid creation using the `getImagePyramid()` method.")
}
const parsedTileParams = utils.parseTileParams(tileParams)
let { tileX, tileY, tileWidth, tileHeight, tileSize, tileResolution } = parsedTileParams
if (!Number.isInteger(tileX) || !Number.isInteger(tileY) || !Number.isInteger(tileWidth) || !Number.isInteger(tileHeight)) {
console.error("Tile Request missing critical parameters!", tileX, tileY, tileWidth, tileHeight)
return
}
if (!Number.isInteger(tileSize) && !Number.isInteger(tileResolution)) {
tileResolution = Math.max(tileWidth, tileHeight)
}
tileResolution = tileResolution || tileSize // To ensure backward compatibility.
let optimalImageInTiff = undefined
if (imageIndex >= 0 && imageIndex < imagePyramid.slideImages.length) {
optimalImageInTiff = imagePyramid.slideImages[imageIndex]
} else {
optimalImageInTiff = await utils.getImageByRatio(imagePyramid, tileWidth, tileResolution)
}
if (Array.isArray(optimalImageInTiff.fileDirectory["SampleFormat"]) && optimalImageInTiff.fileDirectory["SampleFormat"].length !== optimalImageInTiff.fileDirectory["BitsPerSample"].length) {
optimalImageInTiff.fileDirectory["SampleFormat"] = Array(optimalImageInTiff.fileDirectory["BitsPerSample"].length).fill(optimalImageInTiff.fileDirectory["SampleFormat"][0])
}
const optimalImageWidth = optimalImageInTiff.getWidth()
const optimalImageHeight = optimalImageInTiff.getHeight()
let tileWidthToRender, tileHeightToRender
if (tileWidth > tileHeight) {
tileHeightToRender = Math.floor(tileHeight * tileResolution / tileWidth)
tileWidthToRender = tileResolution
} else {
tileWidthToRender = Math.floor(tileWidth * tileResolution / tileHeight)
tileHeightToRender = tileResolution
}
const { maxWidth, maxHeight } = imagePyramid
const tileInImageLeftCoord = Math.max(Math.floor(tileX * optimalImageWidth / maxWidth), 0)
const tileInImageTopCoord = Math.max(Math.floor(tileY * optimalImageHeight / maxHeight), 0)
const tileInImageRightCoord = Math.min(Math.floor((tileX + tileWidth) * optimalImageWidth / maxWidth), optimalImageWidth)
const tileInImageBottomCoord = Math.min(Math.floor((tileY + tileHeight) * optimalImageHeight / maxHeight), optimalImageHeight)
const geotiffParameters = {
width: tileWidthToRender,
height: tileHeightToRender,
window: [
tileInImageLeftCoord,
tileInImageTopCoord,
tileInImageRightCoord,
tileInImageBottomCoord,
],
interleave: true
}
if (pool) {
geotiffParameters['pool'] = pool
}
const data = await optimalImageInTiff.readRasters(geotiffParameters)
const imageBlob = await utils.convertToImageBlob(data, tileWidthToRender, tileHeightToRender, optimalImageInTiff.fileDirectory)
return imageBlob
}
export const createPool = async (tiffImage, numWorkers = 0, supportedDecoders) => {
let workerPool = undefined
if (typeof (Worker) === 'undefined') {
console.warn("Worker pool creation failed. The environment does not support web workers. All operations shall run on the main thread.")
return workerPool
}
if (numWorkers === 0) {
return workerPool
} else if (Number.isInteger(numWorkers) && numWorkers > 0) {
if (!supportedDecoders) {
supportedDecoders = await setupDecoders()
}
// Condition to check if this is a service worker-like environment. Service workers cannot create new workers,
// plus the GeoTIFF version has to be downgraded to avoid any dynamic imports.
// As a result, thread creation and non-standard image decoding does not work inside service workers. You would typically
// only use service workers to support OpenSeadragon anyway, in which case you'd be better off using something like
// https://github.com/episphere/GeoTIFFTileSource-JPEG2k .
const imageCompression = tiffImage?.fileDirectory.Compression
let createWorker
try {
await getDecoder(tiffImage.fileDirectory)
} catch (e) {
if (e.message.includes("Unknown compression method")) {
const decoderForCompression = supportedDecoders?.[imageCompression]
if (decoderForCompression) {
const baseURL = import.meta.url.split("/").slice(0, -1).join("/");
createWorker = () => new Worker(URL.createObjectURL(new Blob([`
importScripts("${baseURL}/decoders/${decoderForCompression}")
`])));
} else {
throw new Error(`Unsupported compression method: ${imageCompression}. Cannot process this image.`)
}
}
}
workerPool = new Pool(Math.min(Math.floor(navigator.hardwareConcurrency / 2), numWorkers), createWorker)
// workerPool.supportedCompression = imageCompression
await new Promise(res => setTimeout(res, 500)) // Setting up the worker pool is an asynchronous task, give it time to complete before moving on.
}
return workerPool
}
export const destroyPool = (workerPool) => {
workerPool?.destroy()
workerPool = undefined
return workerPool
};
export { Imagebox3 }
export default Imagebox3