From 439cb1ce1ab9160632704648f50ae49cd2b0f988 Mon Sep 17 00:00:00 2001 From: Derek Dohler Date: Mon, 30 Jan 2023 10:56:51 -0500 Subject: [PATCH] Add RasterIO example to thumbnail demo --- Makefile | 3 + README.md | 9 + examples/thumbnail/index.html | 16 +- examples/thumbnail/index.js | 29 ++- examples/thumbnail/worker.js | 345 ++++++++++++++++++++++------------ 5 files changed, 271 insertions(+), 131 deletions(-) diff --git a/Makefile b/Makefile index 2607b25ee49c..14d608529488 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,9 @@ EXPORTED_FUNCTIONS = "[\ '_GDALGetRasterMinimum',\ '_GDALGetRasterMaximum',\ '_GDALGetRasterNoDataValue',\ + '_GDALGetDataTypeSizeBytes',\ + '_GDALGetDataTypeByName',\ + '_GDALGetDataTypeName',\ '_GDALRasterIO',\ '_GDALRasterIOEx',\ '_GDALReadBlock',\ diff --git a/README.md b/README.md index c12c9fbfd387..493df5e2799e 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,15 @@ This library exports the following GDAL functions: - GDALGetRasterMinimum - GDALGetRasterMaximum - GDALGetRasterNoDataValue +- GDALGetDataTypeSizeBytes +- GDALGetDataTypeByName +- GDALGetDataTypeName +- GDALRasterIO +- GDALRasterIOEx +- GDALReadBlock +- GDALWriteBlock +- GDALGetBlockSize +- GDALGetActualBlockSize - GDALGetProjectionRef - GDALSetProjection - GDALGetGeoTransform diff --git a/examples/thumbnail/index.html b/examples/thumbnail/index.html index d732274f0979..02e220ad87e6 100644 --- a/examples/thumbnail/index.html +++ b/examples/thumbnail/index.html @@ -1,15 +1,19 @@ + Test GDAL.js + - - -

Select a GeoTIFF using the Browse... button. -Click on the "Thumbnail" button and a thumbnail of the image will be -displayed below.

+ + +

Select a GeoTIFF using the Browse... button. + Click on the "Thumbnail" button and a thumbnail of the image will be + displayed below.

+

Once you've displayed a thumbnail, you can click it to see pixel values displayed in the Developer console.

-
+
+ diff --git a/examples/thumbnail/index.js b/examples/thumbnail/index.js index 0cc21ddeef3c..0d794043ab5c 100644 --- a/examples/thumbnail/index.js +++ b/examples/thumbnail/index.js @@ -1,16 +1,33 @@ var thumbnailer = new Worker('worker.js'); function makeThumbnail() { - var files = document.querySelector('#geotiff-select').files; - thumbnailer.postMessage(files); + var files = document.querySelector('#geotiff-select').files; + thumbnailer.postMessage({ type: 'thumbnail', payload: files }); } thumbnailer.onmessage = function(evt) { - displayImage(evt.data); + if (evt.data.type === 'thumbnail') { + displayImage(evt.data.payload); + } + if (evt.data.type === 'click') { + displayPixelValues(evt.data.payload); + } }; +function postClickCoordinates(evt) { + var files = document.querySelector('#geotiff-select').files; + thumbnailer.postMessage({ type: 'click', payload: { files: files, x: evt.offsetX, y: evt.offsetY } }); +} + function displayImage(imageBytes) { - var outputBlob = new Blob([imageBytes], { type: 'image/png' }); - var imgDisplay = document.querySelector('#thumbnail'); - imgDisplay.src = window.URL.createObjectURL(outputBlob); + var outputBlob = new Blob([imageBytes], { type: 'image/png' }); + var imgDisplay = document.querySelector('#thumbnail'); + imgDisplay.removeEventListener('click', postClickCoordinates); + imgDisplay.addEventListener('click', postClickCoordinates); + imgDisplay.src = window.URL.createObjectURL(outputBlob); + +} + +function displayPixelValues(bandValues) { + console.log(bandValues); } diff --git a/examples/thumbnail/worker.js b/examples/thumbnail/worker.js index e95d491a8dfd..0cd038b66002 100644 --- a/examples/thumbnail/worker.js +++ b/examples/thumbnail/worker.js @@ -3,55 +3,90 @@ */ var TIFFPATH = '/tiffs'; var PNGPATH = '/pngs'; +const THUMB_X_SIZE = 512; var initialized = false; var GDALOpen, - GDALClose, - GDALGetRasterCount, - GDALTranslate, - GDALTranslateOptionsNew, - GDALTranslateOptionsFree, - CSLCount; - + GDALClose, + GDALGetRasterCount, + GDALGetRasterXSize, + GDALGetRasterYSize, + GDALGetRasterBand, + GDALGetRasterDataType, + GDALGetDataTypeName, + GDALGetDataTypeSizeBytes, + GDALGetDataTypeByName, + GDALDatasetRasterIO, + GDALTranslate, + GDALTranslateOptionsNew, + GDALTranslateOptionsFree, + CSLCount; + // Set up Module object for gdal.js to populate. Emscripten sets up its compiled // code to look for a Module object in the global scope. If found, it reads runtime // configuration from the existing object, and then further populates that object // with other helpful functionality (e.g. ccall() and cwrap(), which are used in // the onRuntimeInitialized callback, below). var Module = { - 'print': function(text) { console.log('stdout: ' + text); }, - 'printErr': function(text) { console.log('stderr: ' + text); }, - // Optimized builds contain a .js.mem file which is loaded asynchronously; - // this waits until that has finished before performing further setup. - 'onRuntimeInitialized': function() { - // Initialize GDAL - Module.ccall('GDALAllRegister', null, [], []); - - // Set up JS proxy functions - // Note that JS Number types are used to represent pointers, which means that - // any time we want to pass a pointer to an object, such as in GDALOpen, which in - // C returns a pointer to a GDALDataset, we need to use 'number'. - GDALOpen = Module.cwrap('GDALOpen', 'number', ['string']); - GDALClose = Module.cwrap('GDALClose', 'number', ['number']); - GDALGetRasterCount = Module.cwrap('GDALGetRasterCount', 'number', ['number']); - // Params: - // 1. Output path - // 2. Pointer to a GDALDataset - // 3. Pointer to a GDALTranslateOptions - // 4. Int to use for error reporting - // Returns a pointer to a new GDAL Dataset - GDALTranslate = Module.cwrap('GDALTranslate', 'number', ['string', 'number', 'number', 'number']); - // Params: array of option strings as to gdal_translate; pointer to a struct that should be null. - GDALTranslateOptionsNew = Module.cwrap('GDALTranslateOptionsNew', 'number', ['number', 'number']); - GDALTranslateOptionsFree = Module.cwrap('GDALTranslateOptionsFree', 'number', ['number']); - - CSLCount = Module.cwrap('CSLCount', 'number', ['number']); - // Create a "directory" where user-selected files will be placed - FS.mkdir(TIFFPATH); - FS.mkdir(PNGPATH); - initialized = true; - } + 'print': function(text) { console.log('stdout: ' + text); }, + 'printErr': function(text) { console.log('stderr: ' + text); }, + // Optimized builds contain a .js.mem file which is loaded asynchronously; + // this waits until that has finished before performing further setup. + 'onRuntimeInitialized': function() { + // Initialize GDAL + Module.ccall('GDALAllRegister', null, [], []); + + // Set up JS proxy functions + // Note that JS Number types are used to represent pointers, which means that + // any time we want to pass a pointer to an object, such as in GDALOpen, which in + // C returns a pointer to a GDALDataset, we need to use 'number'. + GDALOpen = Module.cwrap('GDALOpen', 'number', ['string', 'number']); + GDALClose = Module.cwrap('GDALClose', 'number', ['number']); + GDALGetRasterBand = Module.cwrap('GDALGetRasterBand', 'number', ['number', 'number']); + GDALGetDataTypeSizeBytes = Module.cwrap('GDALGetDataTypeSizeBytes', 'number', ['number']); + GDALGetDataTypeName = Module.cwrap('GDALGetDataTypeName', 'string', ['number']); + GDALGetRasterDataType = Module.cwrap('GDALGetRasterDataType', 'number', ['number']); + GDALGetRasterCount = Module.cwrap('GDALGetRasterCount', 'number', ['number']); + GDALGetRasterXSize = Module.cwrap('GDALGetRasterXSize', 'number', ['number']); + GDALGetRasterYSize = Module.cwrap('GDALGetRasterYSize', 'number', ['number']); + // Params: + // 1. Output path + // 2. Pointer to a GDALDataset + // 3. Pointer to a GDALTranslateOptions + // 4. Int to use for error reporting + // Returns a pointer to a new GDAL Dataset + GDALTranslate = Module.cwrap('GDALTranslate', 'number', ['string', 'number', 'number', 'number']); + // Params: array of option strings as to gdal_translate; pointer to a struct that should be null. + GDALTranslateOptionsNew = Module.cwrap('GDALTranslateOptionsNew', 'number', ['number', 'number']); + GDALTranslateOptionsFree = Module.cwrap('GDALTranslateOptionsFree', 'number', ['number']); + + GDALGetDataTypeByName = Module.cwrap('GDALGetDataTypeByName', 'number', ['string']); + GDALDatasetRasterIO = Module.cwrap('GDALDatasetRasterIO', 'number', [ + 'number', // GDALDatasetH + 'number', // GF_READ or GF_WRITE + 'number', // nXOff + 'number', // nYOff + 'number', // nXSize + 'number', // nYSize + 'number', // pData + 'number', // nBufXSize + 'number', // nBufYSize + 'number', // eBufType + 'number', // nBandCount + 'number', // panBandMap + 'number', // nPixelSpace + 'number', // nLineSpace + 'number', // nBandSpace + 'number', // psExtraArg + ]); + + CSLCount = Module.cwrap('CSLCount', 'number', ['number']); + // Create a "directory" where user-selected files will be placed + FS.mkdir(TIFFPATH); + FS.mkdir(PNGPATH); + initialized = true; + } }; // Load gdal.js. This will populate the Module object, and then call @@ -64,88 +99,160 @@ importScripts('gdal.js'); // Use GDAL functions to translate file into PNG format // @param files a FileList object as returned by a file input's .files field function translateTiff(files) { - // Make GeoTiffs available to GDAL in the virtual filesystem that it lives inside - FS.mount(WORKERFS, { - files: files - }, TIFFPATH); - - // Create a GDAL Dataset - // TODO: Dynamically adjust Module['TOTAL_MEMORY'] based on incoming file size - var dataset = GDALOpen(TIFFPATH + '/' + files[0].name); - var bandCount = GDALGetRasterCount(dataset); - console.log('Band count', bandCount); - // TODO: Dynamically adjust the band output based on the band count - // Things get a bit ugly passing string arrays to C++ functions. Bear with me. - var translateOptions = [ - '-ot', 'Byte', - '-of', 'PNG', - '-outsize', '512', '0', - '-r', 'nearest', - '-scale' - ]; - // Dynamically adjust band output based on availability - for (var i = 1; i <= 3 && i <= bandCount; i++) { - translateOptions.push('-b'); - translateOptions.push(i.toString()); - } - // So first, we need to allocate Emscripten heap space sufficient to store each string - // as a null-terminated C string. - var ptrsArray = translateOptions.map(function(str) { - return Module._malloc(Module.lengthBytesUTF8(str) + 1); // +1 for the null terminator byte - }); - - // In addition to each individual argument being null-terminated, the GDAL docs specify that - // GDALTranslateOptionsNew take its options passed in as a null-terminated array of pointers, - // so we have to add on a null (0) byte at the end. - ptrsArray.push(0); - // Because the C function signature is char **, we'll eventually need to get a pointer to the list of - // pointers, so we're going to prepare by storing the pointers as a typed array so that we can - // more easily copy it into heap space later. - var strPtrs = Uint32Array.from(ptrsArray); - - // Next, we need to write each string from the JS string array into the Emscripten heap space - // we've allocated for it. - translateOptions.forEach(function(str, i) { - Module.stringToUTF8(str, strPtrs[i], Module.lengthBytesUTF8(str) + 1); - }); - - // Now, as mentioned above, we also need to copy the pointer array itself into heap space. - var ptrOffset = Module._malloc(strPtrs.length * strPtrs.BYTES_PER_ELEMENT); - Module.HEAPU32.set(strPtrs, ptrOffset/strPtrs.BYTES_PER_ELEMENT); - // Whew, all finished. ptrOffset is now the address of the start of the list of pointers in - // Emscripten heap space. Each pointer identifies the address of the start of a parameter - // string, also stored in heap space. This is the direct equivalent of a char **, which is what - // GDALTranslateOptionsNew requires. - var translateOptionsPtr = GDALTranslateOptionsNew(ptrOffset, null); - // Now that we have our translate options, we need to make a file location to hold the output. - var pngFilePath = PNGPATH + '/thumb.png'; - // And then we can kick off the actual translation process. - var pngDataset = GDALTranslate(pngFilePath, dataset, translateOptionsPtr, null); - - // Close out the output dataset before reading from it. - GDALClose(pngDataset); - // Read the output dataset (which is a PNG image) and send it back to the caller. - postMessage(FS.readFile(pngFilePath, { encoding: 'binary' })); - - // Now cleanup - GDALClose(dataset); - FS.unmount(TIFFPATH); - FS.unlink(pngFilePath); - // TODO this results in "Invalid argument" - //FS.unmount(PNGPATH); - ptrsArray.pop(); // Remove 0 terminator from the end; we don't want to free() this. - strPtrs.forEach(function(ptr) { Module._free(ptr); }); - Module._free(ptrOffset); - - // Deallocate TranslateOptions - GDALTranslateOptionsFree(translateOptionsPtr); + // Make GeoTiffs available to GDAL in the virtual filesystem that it lives inside + FS.mount(WORKERFS, { + files: files + }, TIFFPATH); + + // Create a GDAL Dataset + // TODO: Dynamically adjust Module['TOTAL_MEMORY'] based on incoming file size + var dataset = GDALOpen(TIFFPATH + '/' + files[0].name); + var bandCount = GDALGetRasterCount(dataset); + // TODO: Dynamically adjust the band output based on the band count + // Things get a bit ugly passing string arrays to C++ functions. Bear with me. + var translateOptions = [ + '-ot', 'Byte', + '-of', 'PNG', + '-outsize', THUMB_X_SIZE.toString(), '0', + '-r', 'nearest', + '-scale' + ]; + // Dynamically adjust band output based on availability + for (var i = 1; i <= 3 && i <= bandCount; i++) { + translateOptions.push('-b'); + translateOptions.push(i.toString()); + } + // So first, we need to allocate Emscripten heap space sufficient to store each string + // as a null-terminated C string. + var ptrsArray = translateOptions.map(function(str) { + return Module._malloc(Module.lengthBytesUTF8(str) + 1); // +1 for the null terminator byte + }); + + // In addition to each individual argument being null-terminated, the GDAL docs specify that + // GDALTranslateOptionsNew take its options passed in as a null-terminated array of pointers, + // so we have to add on a null (0) byte at the end. + ptrsArray.push(0); + // Because the C function signature is char **, we'll eventually need to get a pointer to the list of + // pointers, so we're going to prepare by storing the pointers as a typed array so that we can + // more easily copy it into heap space later. + var strPtrs = Uint32Array.from(ptrsArray); + + // Next, we need to write each string from the JS string array into the Emscripten heap space + // we've allocated for it. + translateOptions.forEach(function(str, i) { + Module.stringToUTF8(str, strPtrs[i], Module.lengthBytesUTF8(str) + 1); + }); + + // Now, as mentioned above, we also need to copy the pointer array itself into heap space. + var ptrOffset = Module._malloc(strPtrs.length * strPtrs.BYTES_PER_ELEMENT); + Module.HEAPU32.set(strPtrs, ptrOffset / strPtrs.BYTES_PER_ELEMENT); + // Whew, all finished. ptrOffset is now the address of the start of the list of pointers in + // Emscripten heap space. Each pointer identifies the address of the start of a parameter + // string, also stored in heap space. This is the direct equivalent of a char **, which is what + // GDALTranslateOptionsNew requires. + var translateOptionsPtr = GDALTranslateOptionsNew(ptrOffset, null); + // Now that we have our translate options, we need to make a file location to hold the output. + var pngFilePath = PNGPATH + '/thumb.png'; + // And then we can kick off the actual translation process. + var pngDataset = GDALTranslate(pngFilePath, dataset, translateOptionsPtr, null); + + // Close out the output dataset before reading from it. + GDALClose(pngDataset); + // Read the output dataset (which is a PNG image) and send it back to the caller. + postMessage({ type: 'thumbnail', payload: FS.readFile(pngFilePath, { encoding: 'binary' }) }); + + // Now cleanup + GDALClose(dataset); + FS.unmount(TIFFPATH); + FS.unlink(pngFilePath); + // TODO this results in "Invalid argument" + //FS.unmount(PNGPATH); + ptrsArray.pop(); // Remove 0 terminator from the end; we don't want to free() this. + strPtrs.forEach(function(ptr) { Module._free(ptr); }); + Module._free(ptrOffset); + + // Deallocate TranslateOptions + GDALTranslateOptionsFree(translateOptionsPtr); +} + +function getPixelValues(files, x, y) { + // Make GeoTiffs available to GDAL in the virtual filesystem that it lives inside + FS.mount(WORKERFS, { + files: files + }, TIFFPATH); + + // Create a GDAL Dataset + const dataset = GDALOpen(TIFFPATH + '/' + files[0].name, 0); + const bandCount = GDALGetRasterCount(dataset); + + // Create an array of the band data type to receive pixel values. + const band1 = GDALGetRasterBand(dataset, 1); + const band1Dt = GDALGetRasterDataType(band1); + const band1DtBytes = GDALGetDataTypeSizeBytes(band1Dt); + + const pixDataDest = Module._malloc(bandCount * band1DtBytes); + + // Transform thumbnail coordinates to image coordinates. + // The thumbnail is always 512 x Y. So we need to figure out what Y is and then + // transform both coordinates. + const dsXSize = GDALGetRasterXSize(dataset); + const dsYSize = GDALGetRasterYSize(dataset); + const thumbYSize = (THUMB_X_SIZE / dsXSize) * dsYSize; + + const dsXPix = Math.floor((dsXSize / THUMB_X_SIZE) * x); + const dsYPix = Math.floor((dsYSize / thumbYSize) * y); + // Read from the raster + // https://gdal.org/api/gdaldataset_cpp.html#_CPPv4N11GDALDataset8RasterIOE10GDALRWFlagiiiiPvii12GDALDataTypeiPi8GSpacing8GSpacing8GSpacingP20GDALRasterIOExtraArg + GDALDatasetRasterIO( + dataset, + 0, // GF_READ + dsXPix, // nXOff + dsYPix, // nYOff + 1, // nXSize + 1, // nYSize + pixDataDest, // pData + 1, // nBufXSize + 1, // nBufYSize + band1Dt, // eBufType + bandCount, // nBandCount + 0, // panBandMap + 0, // nPixelSpace + 0, // nLineSpace + 0, // nBandSpace + 0, // psExtraArg + ); + + const band1DtName = GDALGetDataTypeName(band1Dt); + console.log('band1 Datatype', band1DtName); + const memView = + band1DtName === 'Byte' ? Module.HEAPU8 : + band1DtName === 'UInt16' ? Module.HEAPU16 : + band1DtName === 'Int16' ? Module.HEAP16 : + band1DtName === 'UInt32' ? Module.HEAPU32 : + band1DtName === 'Int32' ? Module.HEAP32 : + band1DtName === 'Float32' ? Module.HEAPF32 : + band1DtName === 'Float64' ? Module.HEAPF64 : Module.HEAPF64; + const pixValues = Array.from(memView.subarray(pixDataDest / band1DtBytes, pixDataDest / band1DtBytes + bandCount)); + postMessage({ type: 'click', payload: pixValues }); + + + Module._free(pixDataDest); + GDALClose(dataset); + FS.unmount(TIFFPATH); + } // Assume that all incoming messages are FileLists of GeoTiffs and inspect them. onmessage = function(msg) { - if (!initialized) { - console.log('Runtime not initialized yet, try again'); - return; - } - translateTiff(msg.data); + if (!initialized) { + console.log('Runtime not initialized yet, try again'); + return; + } + if (msg.data.type === 'thumbnail') { + translateTiff(msg.data.payload); + } + if (msg.data.type === 'click') { + const payload = msg.data.payload; + getPixelValues(payload.files, payload.x, payload.y); + } };