Skip to content

Commit

Permalink
Support for JPEG2k in imagebox3
Browse files Browse the repository at this point in the history
  • Loading branch information
PrafulB committed Feb 16, 2024
1 parent 5fff275 commit 574717a
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 30 deletions.
30 changes: 30 additions & 0 deletions decoders/decoder_33005.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
importScripts("https://cdn.jsdelivr.net/npm/@cornerstonejs/codec-openjpeg@1.2.3/dist/openjpegwasm.js");
importScripts("https://cdn.jsdelivr.net/npm/geotiff@2.0.7");

let decoder = {}
OpenJPEGWASM({ 'locateFile': (path, scriptDirectory) => "https://cdn.jsdelivr.net/npm/@cornerstonejs/codec-openjpeg@1.2.3/dist/" + path }).then(openjpegWASM => {
decoder = new openjpegWASM.J2KDecoder();
})

GeoTIFF.addDecoder([33003, 33005], async () =>
class JPEG2000Decoder extends GeoTIFF.BaseDecoder {
constructor(fileDirectory) {
super();
}
decodeBlock(b) {
let encodedBuffer = decoder.getEncodedBuffer(b.byteLength);
encodedBuffer.set(new Uint8Array(b));
decoder.decode();
let decodedBuffer = decoder.getDecodedBuffer();
return decodedBuffer.buffer;
}
}
);

self.addEventListener('message', async (e) => {
const { id, fileDirectory, buffer } = e.data;
const decoder = await GeoTIFF.getDecoder(fileDirectory);
const decoded = await decoder.decode(fileDirectory, buffer);
// console.log(decoded)
self.postMessage({ decoded, id }, [decoded]);
});
4 changes: 4 additions & 0 deletions decoders/decoders.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"33003": "decoder_33005.js",
"33005": "decoder_33005.js"
}
2 changes: 1 addition & 1 deletion demo/serviceWorker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ imgBox.changeImage = () => {

imgBox.addServiceWorker = async () => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(`../../imagebox3.js?tileServerPathSuffix=${tileServerPathSuffix}`)
navigator.serviceWorker.register(`../../imagebox3.js?tileServerPathSuffix=${tileServerPathSuffix}`, {type: 'classic'})
.catch((error) => {
console.log('Service worker registration failed', error)
})
Expand Down
65 changes: 50 additions & 15 deletions imagebox3.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ var imagebox3 = (() => {
ENVIRONMENT_IS_SERVICE_WORKER = ENVIRONMENT_IS_WEB_WORKER && typeof ServiceWorkerGlobalScope === "function" && self instanceof ServiceWorkerGlobalScope

const GEOTIFF_LIB_URL = {
"mjs": "https://cdn.jsdelivr.net/npm/geotiff@2.0.7/+esm", // for ES6 module fetching (since service workers don't support dynamic imports yet)
"js": "https://cdn.jsdelivr.net/npm/geotiff@2.0.7", // for web workers
"mjs": "https://cdn.jsdelivr.net/npm/geotiff@2.1.2/+esm", // for ES6 module fetching (since service workers don't support dynamic imports yet)
"js": "https://cdn.jsdelivr.net/npm/geotiff@2.1.2", // for web workers
"jsSW": "https://cdn.jsdelivr.net/npm/geotiff@1.0.4/dist-browser/geotiff.js" // for service workers. 1.0.4 is the last GeoTIFF version that service workers can import.
}

if (ENVIRONMENT_IS_SERVICE_WORKER) {
importScripts(GEOTIFF_LIB_URL["jsSW"])
GeoTIFF = self.GeoTIFF
Expand Down Expand Up @@ -99,6 +100,13 @@ var imagebox3 = (() => {
let tiff = {} // Variable to cache GeoTIFF instance per image for reuse.
const imageInfoContext = "http://iiif.io/api/image/2/context.json"

const decodersJSON_URL = `http://localhost:8081/decoders/decoders.json`;

let supportedDecoders = {};
fetch(decodersJSON_URL).then(resp => resp.json()).then(decoders => {
supportedDecoders = decoders
})

const utils = {
parseTileParams: (tileParams) => {
// Parse tile params into tile coordinates and size
Expand Down Expand Up @@ -230,7 +238,7 @@ var imagebox3 = (() => {
try {
const headers = cache ? { headers: {'Cache-Control': "no-cache, no-store"}} : {}
tiff[imageKey].pyramid = tiff[imageKey].pyramid || ( imageID instanceof File ? await GeoTIFF.fromBlob(imageID) : await GeoTIFF.fromUrl(imageID, headers) )

const imageCount = await tiff[imageKey].pyramid.getImageCount()
if (tiff[imageKey].pyramid.loadedCount !== imageCount) {
tiff[imageKey].pyramid.loadedCount = 0
Expand Down Expand Up @@ -262,10 +270,7 @@ var imagebox3 = (() => {
}

const getImageThumbnail = async (imageID, tileParams, pool=false) => {
if (pool) {
await createPool()
}


const parsedTileParams = utils.parseTileParams(tileParams)
let { thumbnailWidthToRender, thumbnailHeightToRender } = parsedTileParams

Expand All @@ -281,6 +286,9 @@ var imagebox3 = (() => {
}

const thumbnailImage = await tiff[imageKey].pyramid.getImage(1)
if (pool) {
await createPool(thumbnailImage)
}

if (thumbnailWidthToRender && !thumbnailHeightToRender) {
thumbnailHeightToRender = Math.floor(thumbnailImage.getHeight() * thumbnailWidthToRender / thumbnailImage.getWidth())
Expand All @@ -302,9 +310,7 @@ var imagebox3 = (() => {

const getImageTile = async (imageID, tileParams, pool=false) => {
// Get individual tiles from the appropriate image in the pyramid.
if (pool) {
await createPool()
}


const parsedTileParams = utils.parseTileParams(tileParams)
const { tileX, tileY, tileWidth, tileHeight, tileSize } = parsedTileParams
Expand All @@ -324,7 +330,11 @@ var imagebox3 = (() => {
const optimalImageWidth = optimalImageInTiff.getWidth()
const optimalImageHeight = optimalImageInTiff.getHeight()
const tileHeightToRender = Math.floor(tileHeight * tileSize / tileWidth)


if (pool) {
await createPool(optimalImageInTiff)
}

const { maxWidth, maxHeight } = tiff[imageKey].pyramid

const tileInImageLeftCoord = Math.max(Math.floor(tileX * optimalImageWidth / maxWidth), 0)
Expand All @@ -348,10 +358,35 @@ var imagebox3 = (() => {
return imageResponse
}

const createPool = async () => {
if (!$.workerPool) {
$.workerPool = new GeoTIFF.Pool(Math.floor(navigator.hardwareConcurrency/2))
return new Promise(res => setTimeout(() => res($.workerPool), 500)) // Setting up the worker pool is an asynchronous task, give it time to complete before moving on.
const createPool = async (tiffImage) => {
if (typeof(Worker) !== 'undefined') {
// Condition to check if this is a service worker-like environment. Service workers cannot create 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
const geotiffSupportsCompression = typeof(GeoTIFF.getDecoder(tiffImage.fileDirectory)) === 'function'
const decoderForCompression = supportedDecoders?.[imageCompression]

let createWorker = undefined
if (decoderForCompression) {
createWorker = () => new Worker( URL.createObjectURL( new Blob([`
importScripts("${baseURL}/decoders/${decoderForCompression}")
`])));
}

if (!$.workerPool) {
$.workerPool = new Pool(Math.min(Math.floor(navigator.hardwareConcurrency/2), 1), createWorker)
$.workerPool.supportedCompression = imageCompression
} else if (!geotiffSupportsCompression && $.workerPool.supportedCompression !== imageCompression) {
destroyPool()
$.workerPool = new Pool(Math.min(Math.floor(navigator.hardwareConcurrency/2), 1), 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.
}
}

Expand Down
64 changes: 50 additions & 14 deletions imagebox3.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// DO NOT USE THIS FILE IN SERVICE WORKERS. USE imagebox3.js INSTEAD.

import { fromBlob, fromUrl, Pool } from "https://cdn.jsdelivr.net/npm/geotiff@2.0.7/+esm"
import { fromBlob, fromUrl, Pool, getDecoder } from "https://cdn.jsdelivr.net/npm/geotiff@2.1.2/+esm"

const imagebox3 = (() => {

Expand Down Expand Up @@ -34,7 +34,7 @@ const imagebox3 = (() => {
self.clients.claim()
}

self.tileServerBasePath = utils.loadTileServerURL()
self.tileServerBasePath = utils.defineTileServerURL()

self.addEventListener("fetch", (e) => {

Expand Down Expand Up @@ -78,6 +78,15 @@ const imagebox3 = (() => {
let tiff = {} // Variable to cache GeoTIFF instance per image for reuse.
const imageInfoContext = "http://iiif.io/api/image/2/context.json"

const baseURL = import.meta.url.split("/").slice(0,-1).join("/");
const decodersJSON_URL = `${baseURL}/decoders/decoders.json`;

let supportedDecoders = {};
fetch(decodersJSON_URL).then(resp => resp.json()).then(decoders => {
supportedDecoders = decoders
console.log(supportedDecoders)
})

const utils = {
parseTileParams: (tileParams) => {
// Parse tile params into tile coordinates and size
Expand Down Expand Up @@ -241,9 +250,7 @@ const imagebox3 = (() => {
}

const getImageThumbnail = async (imageID, tileParams, pool=false) => {
if (pool) {
await createPool()
}


const parsedTileParams = utils.parseTileParams(tileParams)
let { thumbnailWidthToRender, thumbnailHeightToRender } = parsedTileParams
Expand All @@ -260,7 +267,10 @@ const imagebox3 = (() => {
}

const thumbnailImage = await tiff[imageKey].pyramid.getImage(1)

if (pool) {
await createPool(thumbnailImage)
}

if (!thumbnailHeightToRender) {
thumbnailHeightToRender = Math.floor(thumbnailImage.getHeight() * thumbnailWidthToRender / thumbnailImage.getWidth())
}
Expand All @@ -281,14 +291,11 @@ const imagebox3 = (() => {

const getImageTile = async (imageID, tileParams, pool=false) => {
// Get individual tiles from the appropriate image in the pyramid.
if (pool) {
await createPool()
}

const parsedTileParams = utils.parseTileParams(tileParams)
const { tileX, tileY, tileWidth, tileHeight, tileSize } = parsedTileParams

if (!Number.isInteger(tileX) || !Number.isInteger(tileY) || !Number.isInteger(tileWidth) || !Number.isInteger(tileHeight) || !Number.isInteger(tileSize)) {
if (!Number.isInteger(tileX) || !Number.isIntegoer(tileY) || !Number.isInteger(tileWidth) || !Number.isInteger(tileHeight) || !Number.isInteger(tileSize)) {
console.error("Tile Request missing critical parameters!", tileX, tileY, tileWidth, tileHeight, tileSize)
return
}
Expand All @@ -305,6 +312,10 @@ const imagebox3 = (() => {
const optimalImageHeight = optimalImageInTiff.getHeight()
const tileHeightToRender = Math.floor( tileHeight * tileSize / tileWidth)

if (pool) {
await createPool(optimalImageInTiff)
}

const { maxWidth, maxHeight } = tiff[imageKey].pyramid

const tileInImageLeftCoord = Math.max(Math.floor(tileX * optimalImageWidth / maxWidth), 0)
Expand All @@ -328,14 +339,39 @@ const imagebox3 = (() => {
return imageResponse
}

const createPool = async () => {
if (!$.workerPool) {
$.workerPool = new Pool(Math.floor(navigator.hardwareConcurrency/2))
const createPool = async (tiffImage) => {
if (typeof(Worker) !== 'undefined') {
// Condition to check if this is a service worker-like environment. Service workers cannot create 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
const geotiffSupportsCompression = typeof(getDecoder(tiffImage.fileDirectory)) === 'function'
const decoderForCompression = supportedDecoders?.[imageCompression]

let createWorker = undefined
if (decoderForCompression) {
createWorker = () => new Worker( URL.createObjectURL( new Blob([`
importScripts("${baseURL}/decoders/${decoderForCompression}")
`])));
}

if (!$.workerPool) {
$.workerPool = new Pool(Math.min(Math.floor(navigator.hardwareConcurrency/2), 1), createWorker)
$.workerPool.supportedCompression = imageCompression
} else if (!geotiffSupportsCompression && $.workerPool.supportedCompression !== imageCompression) {
destroyPool()
$.workerPool = new Pool(Math.min(Math.floor(navigator.hardwareConcurrency/2), 1), 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.
}
}

const destroyPool = async () => {
const destroyPool = () => {
$.workerPool?.destroy()
$.workerPool = undefined
}
Expand Down

0 comments on commit 574717a

Please sign in to comment.