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"