diff --git a/Documentation/content/docs/gallery/TIFFReaderWithIcon.jpg b/Documentation/content/docs/gallery/TIFFReaderWithIcon.jpg
new file mode 100644
index 00000000000..30a56b4040c
Binary files /dev/null and b/Documentation/content/docs/gallery/TIFFReaderWithIcon.jpg differ
diff --git a/Documentation/content/examples/index.md b/Documentation/content/examples/index.md
index 7166c0a0b02..2e4c6ef956e 100644
--- a/Documentation/content/examples/index.md
+++ b/Documentation/content/examples/index.md
@@ -188,7 +188,8 @@ This will allow you to see the some live code running in your browser. Just pick
[![OfflineLocalView Example][OfflineLocalViewWithIcon]](./OfflineLocalView.html "Load a serialized scene (VTKSZ)")
[![G-Code Example][GCodeReaderWithIcon]](./GCodeReader.html "G-Code reader(gcode)")
[![HDRReader Example][HDRReaderWithIcon]](./HDRReader.html "Load an HDR image")
-[![TGAReader Example][TGAReaderWithIcon]](./TGAReader.html "Load an TGA image")
+[![TGAReader Example][TGAReaderWithIcon]](./TGAReader.html "Load a TGA image")
+[![TIFFReader Example][TIFFReaderWithIcon]](./TGAReader.html "Load a TIFF image")
@@ -213,6 +214,7 @@ This will allow you to see the some live code running in your browser. Just pick
[GCodeReaderWithIcon]: ../docs/gallery/GCodeReaderWithIcon.jpg
[HDRReaderWithIcon]: ../docs/gallery/HDRReaderWithIcon.jpg
[TGAReaderWithIcon]: ../docs/gallery/TGAReaderWithIcon.jpg
+[TIFFReaderWithIcon]: ../docs/gallery/TIFFReaderWithIcon.jpg
# Actors
diff --git a/Sources/IO/Image/TIFFReader/example/index.js b/Sources/IO/Image/TIFFReader/example/index.js
new file mode 100644
index 00000000000..dccfc85ec78
--- /dev/null
+++ b/Sources/IO/Image/TIFFReader/example/index.js
@@ -0,0 +1,122 @@
+import '@kitware/vtk.js/favicon';
+
+// Load the rendering pieces we want to use (for both WebGL and WebGPU)
+import '@kitware/vtk.js/Rendering/Profiles/Geometry';
+
+import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow';
+import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';
+import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper';
+import vtkPlaneSource from '@kitware/vtk.js/Filters/Sources/PlaneSource';
+import vtkTIFFReader from '@kitware/vtk.js/IO/Image/TIFFReader';
+import vtkTexture from '@kitware/vtk.js/Rendering/Core/Texture';
+import vtkURLExtract from '@kitware/vtk.js/Common/Core/URLExtract';
+
+// ----------------------------------------------------------------------------
+// Example code
+// ----------------------------------------------------------------------------
+const userParams = vtkURLExtract.extractURLParameters();
+
+const reader = vtkTIFFReader.newInstance();
+const texture = vtkTexture.newInstance();
+const planeSource = vtkPlaneSource.newInstance();
+const mapper = vtkMapper.newInstance();
+const actor = vtkActor.newInstance();
+mapper.setInputConnection(planeSource.getOutputPort());
+actor.setMapper(mapper);
+
+// ----------------------------------------------------------------------------
+// Use a file reader to load a local file
+// ----------------------------------------------------------------------------
+
+const myContainer = document.querySelector('body');
+const fileContainer = document.createElement('div');
+fileContainer.innerHTML =
+ '
Select a tiff file.
';
+myContainer.appendChild(fileContainer);
+
+const fileInput = fileContainer.querySelector('input');
+
+function zoomCameraToFitPlane(camera, planeWidth, planeHeight) {
+ const fov = 60; // Field of view in degrees
+
+ // Calculate the distance needed to fit the plane in view
+ const distance =
+ Math.max(planeWidth, planeHeight) /
+ (2 * Math.tan((fov * Math.PI) / 180 / 2));
+
+ // Set camera position
+ camera.setPosition(planeWidth / 2, planeHeight / 2, distance);
+ camera.setFocalPoint(planeWidth / 2, planeHeight / 2, 0);
+ camera.setViewUp(0, 1, 0);
+
+ // Set parallel scale for orthographic projection
+ camera.setParallelScale(planeHeight / 2);
+}
+
+function update() {
+ // Get the vtkImageData from the reader
+ const imageData = reader.getOutputData(0);
+
+ // Set the vtkImageData as the texture input
+ texture.setInputData(imageData);
+
+ // // Get the image's extent and spacing
+ const [xMin, xMax, yMin, yMax] = imageData.getExtent();
+ const [spacingX, spacingY] = imageData.getSpacing();
+
+ // // Calculate the plane's width and height based on the image's dimensions
+ const planeWidth = (xMax - xMin + 1) * spacingX;
+ const planeHeight = (yMax - yMin + 1) * spacingY;
+
+ // Set the plane's origin and corners based on calculated width and height
+ planeSource.setOrigin(0, 0, 0);
+ planeSource.setPoint1(planeWidth, 0, 0); // Horizontal edge
+ planeSource.setPoint2(0, planeHeight, 0); // Vertical edge
+
+ actor.addTexture(texture);
+
+ const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance();
+ const renderer = fullScreenRenderer.getRenderer();
+ const renderWindow = fullScreenRenderer.getRenderWindow();
+ const camera = renderer.getActiveCamera();
+ const interactor = renderWindow.getInteractor();
+
+ // Disable default interactor style
+ interactor.setInteractorStyle(null);
+
+ renderer.addActor(actor);
+
+ // Adjust the camera to fit the plane in the view
+ zoomCameraToFitPlane(camera, planeWidth, planeHeight);
+ renderer.resetCameraClippingRange();
+
+ renderWindow.render();
+}
+
+function handleFile(event) {
+ event.preventDefault();
+ const dataTransfer = event.dataTransfer;
+ const files = event.target.files || dataTransfer.files;
+ if (files.length === 1) {
+ const file = files[0];
+ const fileReader = new FileReader();
+ fileReader.onload = () => {
+ reader.parse(fileReader.result);
+ update();
+ };
+ fileReader.readAsArrayBuffer(file);
+ }
+}
+
+fileInput.addEventListener('change', handleFile);
+
+// ----------------------------------------------------------------------------
+// Use the reader to download a file
+// ----------------------------------------------------------------------------
+if (userParams.fileURL) {
+ reader.setUrl(userParams.fileURL).then(() => {
+ reader.loadData().then(() => {
+ update();
+ });
+ });
+}
diff --git a/Sources/IO/Image/TIFFReader/index.d.ts b/Sources/IO/Image/TIFFReader/index.d.ts
new file mode 100644
index 00000000000..ad931a93d94
--- /dev/null
+++ b/Sources/IO/Image/TIFFReader/index.d.ts
@@ -0,0 +1,133 @@
+import { vtkAlgorithm, vtkObject } from '../../../interfaces';
+import HtmlDataAccessHelper from '../../Core/DataAccessHelper/HtmlDataAccessHelper';
+import HttpDataAccessHelper from '../../Core/DataAccessHelper/HttpDataAccessHelper';
+import JSZipDataAccessHelper from '../../Core/DataAccessHelper/JSZipDataAccessHelper';
+import LiteHttpDataAccessHelper from '../../Core/DataAccessHelper/LiteHttpDataAccessHelper';
+
+interface ITIFFReaderOptions {
+ compression?: string;
+ progressCallback?: any;
+ flipY?: boolean;
+}
+
+/**
+ *
+ */
+export interface ITIFFReaderInitialValues {}
+
+type vtkTIFFReaderBase = vtkObject &
+ Omit<
+ vtkAlgorithm,
+ | 'getInputData'
+ | 'setInputData'
+ | 'setInputConnection'
+ | 'getInputConnection'
+ | 'addInputConnection'
+ | 'addInputData'
+ >;
+
+export interface vtkTIFFReader extends vtkTIFFReaderBase {
+ /**
+ * Get the base url.
+ */
+ getBaseURL(): string;
+
+ /**
+ * Get if the image is flipped vertically.
+ */
+ getFlipY(): boolean;
+
+ /**
+ * Get the dataAccess helper.
+ */
+ getDataAccessHelper():
+ | HtmlDataAccessHelper
+ | HttpDataAccessHelper
+ | JSZipDataAccessHelper
+ | LiteHttpDataAccessHelper;
+
+ /**
+ * Get the url of the object to load.
+ */
+ getUrl(): string;
+
+ /**
+ * Load the object data.
+ * @param {ITIFFReaderOptions} [options]
+ */
+ loadData(options?: ITIFFReaderOptions): Promise;
+
+ /**
+ * Parse data.
+ * @param {ArrayBuffer} content The content to parse.
+ */
+ parse(content: ArrayBuffer): void;
+
+ /**
+ * Parse data as ArrayBuffer.
+ * @param {ArrayBuffer} content The content to parse.
+ */
+ parseAsArrayBuffer(content: ArrayBuffer): void;
+
+ /**
+ *
+ * @param inData
+ * @param outData
+ */
+ requestData(inData: any, outData: any): void;
+
+ /**
+ * Flip the image vertically.
+ * @param {String} flipY If true, flip the image vertically.
+ */
+ setFlipY(flipY: boolean): boolean;
+
+ /**
+ *
+ * @param dataAccessHelper
+ */
+ setDataAccessHelper(
+ dataAccessHelper:
+ | HtmlDataAccessHelper
+ | HttpDataAccessHelper
+ | JSZipDataAccessHelper
+ | LiteHttpDataAccessHelper
+ ): boolean;
+
+ /**
+ * Set the url of the object to load.
+ * @param {String} url the url of the object to load.
+ * @param {ITIFFReaderOptions} [option] The PLY reader options.
+ */
+ setUrl(url: string, option?: ITIFFReaderOptions): Promise;
+}
+
+/**
+ * Method used to decorate a given object (publicAPI+model) with vtkTIFFReader characteristics.
+ *
+ * @param publicAPI object on which methods will be bounds (public)
+ * @param model object on which data structure will be bounds (protected)
+ * @param {ITIFFReaderInitialValues} [initialValues] (default: {})
+ */
+export function extend(
+ publicAPI: object,
+ model: object,
+ initialValues?: ITIFFReaderInitialValues
+): void;
+
+/**
+ * Method used to create a new instance of vtkTIFFReader
+ * @param {ITIFFReaderInitialValues} [initialValues] for pre-setting some of its content
+ */
+export function newInstance(
+ initialValues?: ITIFFReaderInitialValues
+): vtkTIFFReader;
+
+/**
+ * vtkTIFFReader is a source object that reads TIFF files.
+ */
+export declare const vtkTIFFReader: {
+ newInstance: typeof newInstance;
+ extend: typeof extend;
+};
+export default vtkTIFFReader;
diff --git a/Sources/IO/Image/TIFFReader/index.js b/Sources/IO/Image/TIFFReader/index.js
new file mode 100644
index 00000000000..fcd1537f778
--- /dev/null
+++ b/Sources/IO/Image/TIFFReader/index.js
@@ -0,0 +1,149 @@
+import macro from 'vtk.js/Sources/macros';
+
+// Enable data soure for DataAccessHelper
+import 'vtk.js/Sources/IO/Core/DataAccessHelper/LiteHttpDataAccessHelper'; // Just need HTTP
+// import 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; // HTTP + zip
+// import 'vtk.js/Sources/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; // html + base64 + zip
+// import 'vtk.js/Sources/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; // zip
+
+import DataAccessHelper from 'vtk.js/Sources/IO/Core/DataAccessHelper';
+import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
+import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
+import UTIF from 'utif';
+
+// ----------------------------------------------------------------------------
+// vtkTIFFReader methods
+// ----------------------------------------------------------------------------
+
+function vtkTIFFReader(publicAPI, model) {
+ // Set our className
+ model.classHierarchy.push('vtkTIFFReader');
+
+ // Create default dataAccessHelper if not available
+ if (!model.dataAccessHelper) {
+ model.dataAccessHelper = DataAccessHelper.get('http');
+ }
+
+ // Internal method to fetch Array
+ function fetchData(url, option = {}) {
+ const { compression, progressCallback } = model;
+ return model.dataAccessHelper.fetchBinary(url, {
+ compression,
+ progressCallback,
+ });
+ }
+
+ // Set DataSet url
+ publicAPI.setUrl = (url, option = { binary: true }) => {
+ model.url = url;
+
+ // Remove the file in the URL
+ const path = url.split('/');
+ path.pop();
+ model.baseURL = path.join('/');
+
+ model.compression = option.compression;
+
+ // Fetch metadata
+ return publicAPI.loadData({
+ progressCallback: option.progressCallback,
+ });
+ };
+
+ // Fetch the actual data arrays
+ publicAPI.loadData = (option = {}) => {
+ const promise = fetchData(model.url, option);
+ promise.then(publicAPI.parse);
+ return promise;
+ };
+
+ publicAPI.parse = (content) => {
+ publicAPI.parseAsArrayBuffer(content);
+ };
+
+ publicAPI.parseAsArrayBuffer = (content) => {
+ if (!content) {
+ return;
+ }
+
+ // Read Header
+ const ifds = UTIF.decode(content);
+ UTIF.decodeImage(content, ifds[0]);
+ const data = UTIF.toRGBA8(ifds[0]);
+
+ const width = ifds[0].width;
+ const height = ifds[0].height;
+ const output = new Uint8Array(data.length);
+
+ if (model.flipY) {
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ const srcIndex = (y * width + x) * 4;
+ const destIndex = ((height - y - 1) * width + x) * 4;
+
+ output[destIndex] = data[srcIndex]; // R
+ output[destIndex + 1] = data[srcIndex + 1]; // G
+ output[destIndex + 2] = data[srcIndex + 2]; // B
+ output[destIndex + 3] = data[srcIndex + 3]; // A
+ }
+ }
+ }
+
+ const dataExtent = [0, width - 1, 0, height - 1];
+ const dataSpacing = [1, 1, 1];
+
+ const imageData = vtkImageData.newInstance();
+ imageData.setDimensions(width, height, 1);
+ imageData.setExtent(dataExtent);
+ imageData.setSpacing(dataSpacing);
+
+ const dataArray = vtkDataArray.newInstance({
+ name: 'TIFFImage',
+ numberOfComponents: 4,
+ values: output,
+ });
+
+ imageData.getPointData().setScalars(dataArray);
+ model.output[0] = imageData;
+ };
+
+ publicAPI.requestData = (inData, outData) => {
+ publicAPI.parse(model.parseData);
+ };
+}
+
+// ----------------------------------------------------------------------------
+// Object factory
+// ----------------------------------------------------------------------------
+
+const DEFAULT_VALUES = {
+ flipY: true,
+ compression: null,
+ progressCallback: null,
+};
+
+// ----------------------------------------------------------------------------
+
+export function extend(publicAPI, model, initialValues = {}) {
+ Object.assign(model, DEFAULT_VALUES, initialValues);
+
+ // Make this a VTK object
+ macro.obj(publicAPI, model);
+
+ // Also make it an algorithm with one input and one output
+ macro.algo(publicAPI, model, 0, 1);
+
+ macro.get(publicAPI, model, ['url', 'baseURL']);
+ macro.setGet(publicAPI, model, ['dataAccessHelper', 'flipY']);
+
+ // Object specific methods
+ vtkTIFFReader(publicAPI, model);
+}
+
+// ----------------------------------------------------------------------------
+
+export const newInstance = macro.newInstance(extend, 'vtkTIFFReader');
+
+// ----------------------------------------------------------------------------
+
+export default { newInstance, extend };
diff --git a/Sources/IO/Image/index.js b/Sources/IO/Image/index.js
index 3c54f2b35c2..88fd5b4b58d 100644
--- a/Sources/IO/Image/index.js
+++ b/Sources/IO/Image/index.js
@@ -1,7 +1,9 @@
import vtkHDRReader from './HDRReader';
import vtkTGAReader from './TGAReader';
+import vtkTIFFReader from './TIFFReader';
export default {
vtkHDRReader,
vtkTGAReader,
+ vtkTIFFReader,
};
diff --git a/package-lock.json b/package-lock.json
index 167780d7b26..46d2538453d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"shelljs": "0.8.5",
"spark-md5": "3.0.2",
"stream-browserify": "3.0.0",
+ "utif": "3.1.0",
"webworker-promise": "0.5.0",
"worker-loader": "3.0.8",
"xmlbuilder2": "3.0.2"
@@ -16519,6 +16520,11 @@
"node": ">=6"
}
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ },
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -21253,6 +21259,14 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
+ "node_modules/utif": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/utif/-/utif-3.1.0.tgz",
+ "integrity": "sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==",
+ "dependencies": {
+ "pako": "^1.0.5"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -34207,6 +34221,11 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
+ "pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ },
"param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -37645,6 +37664,14 @@
"integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==",
"dev": true
},
+ "utif": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/utif/-/utif-3.1.0.tgz",
+ "integrity": "sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==",
+ "requires": {
+ "pako": "^1.0.5"
+ }
+ },
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index a402e60d822..76190f1fb5e 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"shelljs": "0.8.5",
"spark-md5": "3.0.2",
"stream-browserify": "3.0.0",
+ "utif": "3.1.0",
"webworker-promise": "0.5.0",
"worker-loader": "3.0.8",
"xmlbuilder2": "3.0.2"