diff --git a/deploy/csp/index.js b/deploy/csp/index.js
index b31a3f9f1a68f85ef3643b1aeb1da53aa9c3889a..d745b8d58f0a70422b9d48cb481cf533f01401eb 100644
--- a/deploy/csp/index.js
+++ b/deploy/csp/index.js
@@ -104,7 +104,7 @@ module.exports = {
           'unpkg.com/kg-dataset-previewer@1.2.0/', // preview component
           'cdnjs.cloudflare.com/ajax/libs/mathjax/', // math jax
           'https://unpkg.com/three-surfer@0.0.10/dist/bundle.js', // for threeSurfer (freesurfer support in browser)
-          'https://unpkg.com/ng-layer-tune@0.0.1/dist/ng-layer-tune/ng-layer-tune.esm.js', // needed for ng layer control
+          'https://unpkg.com/ng-layer-tune@0.0.2/dist/ng-layer-tune/ng-layer-tune.esm.js', // needed for ng layer control
           (req, res) => res.locals.nonce ? `'nonce-${res.locals.nonce}'` : null,
           ...SCRIPT_SRC,
           ...WHITE_LIST_SRC,
diff --git a/src/atlasViewer/atlasViewer.workerService.service.ts b/src/atlasViewer/atlasViewer.workerService.service.ts
index 227f5f2aa1f9a7ee255ccb9dba0e0d2b57642338..62395463088ed3939c419ec61329c18e68865993 100644
--- a/src/atlasViewer/atlasViewer.workerService.service.ts
+++ b/src/atlasViewer/atlasViewer.workerService.service.ts
@@ -7,6 +7,7 @@ import { getUuid } from "src/util/fn";
 
 import '!!file-loader?name=worker.js!worker/worker.js'
 import '!!file-loader?name=worker-plotly.js!worker/worker-plotly.js'
+import '!!file-loader?name=worker-nifti.js!worker/worker-nifti.js'
 
 /**
  * export the worker, so that services that does not require dependency injection can import the worker
@@ -16,6 +17,7 @@ export const worker = new Worker('worker.js')
 interface IWorkerMessage {
   method: string
   param: any
+  transfers?: any[]
 }
 
 @Injectable({
@@ -25,20 +27,24 @@ interface IWorkerMessage {
 export class AtlasWorkerService {
   public worker = worker
 
-  async sendMessage(data: IWorkerMessage){
+  async sendMessage(_data: IWorkerMessage){
 
+    const { transfers = [], ...data } = _data
     const newUuid = getUuid()
     this.worker.postMessage({
       id: newUuid,
       ...data
-    })
+    }, transfers)
     const message = await fromEvent(this.worker, 'message').pipe(
       filter((message: MessageEvent) => message.data.id && message.data.id === newUuid),
       take(1)
     ).toPromise()
     
     const { data: returnData } = message as MessageEvent
-    const { id, ...rest } = returnData
+    const { id, error, ...rest } = returnData
+    if (error) {
+      throw new Error(error.message || error)
+    }
     return rest
   }
 }
diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts
index 5bf60d5906fe324de2ece7d04c17db1f15e559a2..caa678cc44df957b127679abf2ac3421f88f1f60 100644
--- a/src/environments/environment.common.ts
+++ b/src/environments/environment.common.ts
@@ -4,7 +4,7 @@ export const environment = {
   PRODUCTION: true,
   BACKEND_URL: null,
   DATASET_PREVIEW_URL: 'https://hbp-kg-dataset-previewer.apps.hbp.eu/v2',
-  BS_REST_URL: 'https://siibra-api-latest.apps-dev.hbp.eu/v1_0',
+  BS_REST_URL: 'https://siibra-api-edge.apps-dev.hbp.eu/v1_0',
   SPATIAL_TRANSFORM_BACKEND: 'https://hbp-spatial-backend.apps.hbp.eu',
   MATOMO_URL: null,
   MATOMO_ID: null,
diff --git a/src/index.html b/src/index.html
index c313b64c0f931b7c6bca5d3ca051ed2f220dfb72..5701618243131618751f45ef96644368a9baf333 100644
--- a/src/index.html
+++ b/src/index.html
@@ -15,7 +15,7 @@
   <script src="https://unpkg.com/kg-dataset-previewer@1.2.0/dist/kg-dataset-previewer/kg-dataset-previewer.js" defer>
   </script>
   <script src="https://unpkg.com/three-surfer@0.0.10/dist/bundle.js" defer></script>
-  <script type="module" src="https://unpkg.com/ng-layer-tune@0.0.1/dist/ng-layer-tune/ng-layer-tune.esm.js"></script>
+  <script type="module" src="https://unpkg.com/ng-layer-tune@0.0.2/dist/ng-layer-tune/ng-layer-tune.esm.js"></script>
   
   <title>Interactive Atlas Viewer</title>
 </head>
diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
index 30e0f9c0df3886f6c13c63acf11a5f80e84c7a29..6256f6b7e8f02b90e4043e35ee6d0c714a510126 100644
--- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
+++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
@@ -23,13 +23,14 @@ import { MouseHoverDirective } from "src/mouseoverModule";
 import { NehubaMeshService } from "../mesh.service";
 import { IQuickTourData } from "src/ui/quickTour/constrants";
 import { NehubaLayerControlService, IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service";
-import { getUuid, switchMapWaitFor } from "src/util/fn";
+import { getExportNehuba, getUuid, switchMapWaitFor } from "src/util/fn";
 import { INavObj } from "../navigation.service";
 import { NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY } from "../layerCtrl.service/layerCtrl.util";
 import { MatSnackBar } from "@angular/material/snack-bar";
 import { getShader } from "src/util/constants";
 import { EnumColorMapName } from "src/util/colorMaps";
 import { MatDialog } from "@angular/material/dialog";
+import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service";
 
 export const INVALID_FILE_INPUT = `Exactly one (1) nifti file is required!`
 
@@ -311,6 +312,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A
     private log: LoggingService,
     private snackbar: MatSnackBar,
     private dialog: MatDialog,
+    private worker: AtlasWorkerService,
     @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor,
     @Optional() @Inject(API_SERVICE_SET_VIEWER_HANDLE_TOKEN) setViewerHandle: TSetViewerHandle,
     @Optional() private layerCtrlService: NehubaLayerControlService,
@@ -718,7 +720,7 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A
       URL.revokeObjectURL(resourceUrl)
     }
   }
-  public handleFileDrop(files: File[]){
+  public async handleFileDrop(files: File[]){
     if (files.length !== 1) {
       this.snackbar.open(INVALID_FILE_INPUT, 'Dismiss', {
         duration: 5000
@@ -731,45 +733,77 @@ export class NehubaGlueCmp implements IViewer<'nehuba'>, OnChanges, OnDestroy, A
     /**
      * TODO check extension?
      */
-    
+     
     this.dismissAllAddedLayers()
     
-    const url = URL.createObjectURL(file)
-    this.droppedLayerNames.push({
-      layerName: randomUuid,
-      resourceUrl: url
-    })
-    this.layerCtrlService.addNgLayer([{
-      name: randomUuid,
-      mixability: 'mixable',
-      source: `nifti://${url}`,
-      shader: getShader({
-        colormap: EnumColorMapName.MAGMA
-      })
-    }])
+    // Get file, try to inflate, if files, use original array buffer
+    const buf = await file.arrayBuffer()
+    let outbuf
+    try {
+      outbuf = getExportNehuba().pako.inflate(buf).buffer
+    } catch (e) {
+      console.log('unpack error', e)
+      outbuf = buf
+    }
 
-    this.dialog.open(
-      this.layerCtrlTmpl,
-      {
-        data: {
-          layerName: randomUuid,
-          filename: file.name,
-          moreInfoFlag: false
-        },
-        hasBackdrop: false,
-        disableClose: true,
-        position: {
-          top: '0em'
+    try {
+      const { result, ...other } = await this.worker.sendMessage({
+        method: 'PROCESS_NIFTI',
+        param: {
+          nifti: outbuf
         },
-        autoFocus: false,
-        panelClass: [
-          'no-padding-dialog',
-          'w-100'
-        ]
-      }
-    ).afterClosed().subscribe(
-      () => this.dismissAllAddedLayers()
-    )
+        transfers: [ outbuf ]
+      })
+      
+      const { meta, buffer } = result
+
+      const url = URL.createObjectURL(new Blob([ buffer ]))
+      this.droppedLayerNames.push({
+        layerName: randomUuid,
+        resourceUrl: url
+      })
+      this.layerCtrlService.addNgLayer([{
+        name: randomUuid,
+        mixability: 'mixable',
+        source: `nifti://${url}`,
+        shader: getShader({
+          colormap: EnumColorMapName.MAGMA,
+          lowThreshold: meta.min || 0,
+          highThreshold: meta.max || 1
+        })
+      }])
+
+      this.dialog.open(
+        this.layerCtrlTmpl,
+        {
+          data: {
+            layerName: randomUuid,
+            filename: file.name,
+            moreInfoFlag: false,
+            min: meta.min || 0,
+            max: meta.max || 1,
+            warning: meta.warning || []
+          },
+          hasBackdrop: false,
+          disableClose: true,
+          position: {
+            top: '0em'
+          },
+          autoFocus: false,
+          panelClass: [
+            'no-padding-dialog',
+            'w-100'
+          ]
+        }
+      ).afterClosed().subscribe(
+        () => this.dismissAllAddedLayers()
+      )
+    } catch (e) {
+      console.error(e)
+      this.snackbar.open(`Error loading nifti: ${e.toString()}`, 'Dismiss', {
+        duration: 5000
+      })
+    }
   }
 
 
diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html
index 3cda701b7cebafcae2dc9569a3a38c0ed0ad58d7..ba41f41c2cc6d2fc711a0ccb60cee8c8f512ae3f 100644
--- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html
+++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.template.html
@@ -217,8 +217,14 @@
 
     <div *ngIf="data.moreInfoFlag"
       class="iv-custom-comp darker-bg overflow-hidden grid-wide-3">
-      <ng-layer-tune [ngLayerName]="data.layerName">
+      <ng-layer-tune
+        [ngLayerName]="data.layerName"
+        [thresholdMin]="data.min"
+        [thresholdMax]="data.max">
       </ng-layer-tune>
+      <ul>
+        <li *ngFor="let warn of data.warning">{{ warn }}</li>
+      </ul>
     </div>
 
   </div>
diff --git a/worker/worker-nifti.js b/worker/worker-nifti.js
new file mode 100644
index 0000000000000000000000000000000000000000..56f0aabaef84833182d543db3b0f08a56f147f42
--- /dev/null
+++ b/worker/worker-nifti.js
@@ -0,0 +1,267 @@
+(function(exports){
+  const DTYPE = {
+    SHORT: 0,
+    BYTE: 1,
+    FLOAT: 2,
+    DOUBLE: 3,
+    INT: 4,
+    LONG: 5,
+  }
+
+  const NIFTI_CONST = {
+    TYPE_FLOAT64: 64,
+    SPATIAL_UNITS_MASK: 0x07,
+    TEMPORAL_UNITS_MASK: 0x38,
+    NIFTI1_HDR_SIZE: 348,
+    NIFTI2_HDR_SIZE: 540
+  }
+
+  const nifti1 = {
+    datatype: {
+      offset: 70,
+      type: DTYPE.SHORT
+    },
+    dim0: {
+      offset: 40,
+      type: DTYPE.SHORT
+    },
+    dim1: {
+      offset: 42,
+      type: DTYPE.SHORT
+    },
+    dim2: {
+      offset: 44,
+      type: DTYPE.SHORT
+    },
+    dim3: {
+      offset: 46,
+      type: DTYPE.SHORT
+    },
+    dim4: {
+      offset: 48,
+      type: DTYPE.SHORT
+    },
+    dim5: {
+      offset: 50,
+      type: DTYPE.SHORT
+    },
+    xyztUnits: {
+      offset: 123,
+      type: DTYPE.BYTE
+    },
+    voxOffset: {
+      offset: 108,
+      type: DTYPE.FLOAT
+    },
+    numBitsPerVoxel: {
+      offset: 72,
+      type: DTYPE.SHORT
+    }
+  }
+  
+  const nifti2 = {
+    datatype: {
+      offset: 12,
+      type: DTYPE.SHORT
+    },
+    dim0: {
+      offset: 16,
+      type: DTYPE.LONG
+    },
+    dim1: {
+      offset: 24,
+      type: DTYPE.LONG
+    },
+    dim2: {
+      offset: 32,
+      type: DTYPE.LONG
+    },
+    dim3: {
+      offset: 40,
+      type: DTYPE.LONG
+    },
+    dim4: {
+      offset: 48,
+      type: DTYPE.LONG
+    },
+    dim5: {
+      offset: 56,
+      type: DTYPE.LONG
+    },
+    xyztUnits: {
+      offset: 500,
+      type: DTYPE.INT
+    },
+    voxOffset: {
+      offset: 168,
+      type: DTYPE.LONG
+    },
+    numBitsPerVoxel: {
+      offset: 14,
+      type: DTYPE.SHORT
+    }
+  }
+
+  const isNifti1 = data => {
+    if (data.byteLength < 348) return false
+    const buf = new DataView(data)
+    return buf.getUint8(344) === 0x6E
+    && buf.getUint8(345) === 0x2B
+    && buf.getUint8(346) === 0x31
+  }
+  
+  const isNifti2 = data => {
+    if (data.byteLength < 348) return false
+    const buf = new DataView(data)
+    return buf.getUint8(4) === 0x6E
+    && buf.getUint8(5) === 0x69
+    && buf.getUint8(6) === 0x31
+  }
+
+  const readData = (buf, spec, le = false) => {
+    const { offset, type } = spec
+    if (type === DTYPE.SHORT) return buf.getInt16(offset, le)
+    if (type === DTYPE.INT) return buf.getInt32(offset, le)
+    if (type === DTYPE.FLOAT) return buf.getFloat32(offset, le)
+    if (type === DTYPE.DOUBLE) return buf.getFloat64(offset, le)
+    if (type === DTYPE.BYTE) return buf.getInt8(offset, le)
+    if (type === DTYPE.LONG) {
+      const ints = []
+      let final = 0
+      for (const i = 0; i < 8; i++) {
+        ints.push(
+          readData(buf, {
+            offset: offset + i,
+            type: DTYPE.INT
+          }, le)
+        )
+      }
+      for (const i = 0; i < 8; i++) {
+        const counter = le ? i : (7 - i)
+        final += ints[counter] * 256
+      }
+      return final
+    }
+    throw new Error(`Unknown type ${type}`)
+  }
+
+  const setData = (buf, spec, value, le = false) => {
+    const { offset, type } = spec
+    if (type === DTYPE.SHORT) return buf.setInt16(offset, value, le)
+    if (type === DTYPE.INT) return buf.setInt32(offset, value, le)
+    if (type === DTYPE.FLOAT) return buf.setFloat32(offset, value, le)
+    if (type === DTYPE.DOUBLE) return buf.setFloat64(offset, value, le)
+    if (type === DTYPE.BYTE) return buf.setInt8(offset, value, le)
+    if (type === DTYPE.LONG) {
+      throw new Error(`Writing to LONG not currently supported`)
+    }
+    throw new Error(`Unknown type ${type}`)
+  }
+
+  exports.nifti = {
+    convert: buf => {
+      
+      const is1 = isNifti1(buf)
+      const is2 = isNifti2(buf)
+      if (!is1 && !is2) {
+        throw new Error(`The file is not a valid nifti file`)
+      }
+      const warning = []
+
+      const dict = is1
+        ? nifti1
+        : nifti2
+      const dataView = new DataView(buf)
+
+      let le = false
+
+      // determine the endianness
+      const expectedHdrSize = is1 ? NIFTI_CONST.NIFTI1_HDR_SIZE : NIFTI_CONST.NIFTI2_HDR_SIZE
+      const hdrSize = readData(dataView, {
+        type: DTYPE.INT,
+        offset: 0
+      }, le)
+      if (hdrSize !== expectedHdrSize) le = !le
+
+      // check datatypes
+      const datatype = readData(dataView, dict.datatype, le)
+      if (datatype == NIFTI_CONST.TYPE_FLOAT64) {
+        throw new Error(`Float64 not currently supported.`)
+      }
+
+      // check... other headers
+
+      const dim0 = readData(dataView, dict.dim0, le)
+      const dim1 = readData(dataView, dict.dim1, le)
+      const dim2 = readData(dataView, dict.dim2, le)
+      const dim3 = readData(dataView, dict.dim3, le)
+      const dim4 = readData(dataView, dict.dim4, le)
+      const dim5 = readData(dataView, dict.dim5, le)
+      if (dim4 === 0) {
+        warning.push(`dim[4] was 0, set to 1 instead`)
+        setData(dataView, dict.dim4, 1, le)
+      }
+      if (dim4 > 1) {
+        throw new Error(`Cannot parse time series`)
+      }
+      if (dim5 === 0) {
+        warning.push(`dim[5] was 0, set to 1 instead`)
+        setData(dataView, dict.dim5, 1, le)
+      }
+      if (dim5 > 1) {
+        throw new Error(`cannot show nifti with dim[5] > 1`)
+      }
+      const xyztUnits = readData(dataView, dict.xyztUnits, le)
+      const xyzUnit = xyztUnits & NIFTI_CONST.SPATIAL_UNITS_MASK
+      if (xyzUnit === 0) {
+        warning.push(`xyzt spatial unit not defined. Forcing to be mm.`)
+        setData(dataView, dict.xyztUnits, xyztUnits + 2, le)
+      }
+      const voxOffset = readData(dataView, dict.voxOffset, le)
+      const numBitsPerVoxel = readData(dataView, dict.numBitsPerVoxel, le)
+      
+      let type, increment = 0, min = null, max = null
+      // INT8
+      if (datatype === 256) type = DTYPE.BYTE, increment = 1
+      // INT16
+      if (datatype === 4) type = DTYPE.SHORT, increment = 2
+      // INT32
+      if (datatype === 8) type = DTYPE.INT, increment = 4
+      // FLOAT32
+      if (datatype === 16) type = DTYPE.FLOAT, increment = 4
+      if (type) {
+
+        const pointer = {
+          offset: voxOffset,
+          type
+        }
+        
+        while (true) {
+          try {
+            const val = readData(dataView, pointer, le)
+            if (min === null) min = val
+            if (max === null) max = val
+            if (val < min) min = val
+            if (val > max) max = val
+          } catch (e) {
+            console.error(`error in while true block`)
+            break
+          }
+          pointer.offset += increment
+        }
+      }
+
+      return {
+        meta: {
+          min, max,
+          warning
+        },
+        buffer: dataView.buffer
+      }
+    }
+  }
+})(
+  typeof exports === 'undefined'
+  ? self
+  : exports
+)
\ No newline at end of file
diff --git a/worker/worker.js b/worker/worker.js
index ef24f040c13260bc6fd9183a8376e9b326f52539..51156b1b95fe6ac2dd8d064156c0bf36ca6d56e9 100644
--- a/worker/worker.js
+++ b/worker/worker.js
@@ -8,6 +8,7 @@ globalThis.constants = {
 }
 
 if (typeof self.importScripts === 'function')  self.importScripts('./worker-plotly.js')
+if (typeof self.importScripts === 'function')  self.importScripts('./worker-nifti.js')
 
 /**
  * TODO migrate processing functionalities to other scripts
@@ -21,11 +22,13 @@ const validTypes = [
 ]
 
 const VALID_METHOD = {
-  PROCESS_PLOTLY: `PROCESS_PLOTLY`
+  PROCESS_PLOTLY: `PROCESS_PLOTLY`,
+  PROCESS_NIFTI: 'PROCESS_NIFTI',
 }
 
 const VALID_METHODS = [
-  VALID_METHOD.PROCESS_PLOTLY
+  VALID_METHOD.PROCESS_PLOTLY,
+  VALID_METHOD.PROCESS_NIFTI,
 ]
 
 const validOutType = [
@@ -262,6 +265,32 @@ onmessage = (message) => {
         })
       }
     }
+
+    if (message.data.method === VALID_METHOD.PROCESS_NIFTI) {
+      try {
+        const { nifti } = message.data.param
+        const {
+          meta,
+          buffer
+        } = self.nifti.convert(nifti)
+
+        postMessage({
+          id,
+          result: {
+            meta,
+            buffer
+          }
+        }, [ buffer ])
+      } catch (e) {
+        postMessage({
+          id,
+          error: {
+            code: 401,
+            message: `nifti error: ${e.toString()}`
+          }
+        })
+      }
+    }
     postMessage({
       id,
       error: {