Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/docker/itk-wasm-base/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ RUN curl -L https://api.github.com/repos/facebook/zstd/tarball/${zstd_GIT_TAG} |
ARG USE_LOCAL_ITK=0
COPY ITKLocalCopy /ITKLocalCopy

# ITK source patches applied on top of $ITK_WASM_ITK_BRANCH.
COPY patches /itk-wasm-patches

# Either use local ITK source or clone from git repository
RUN . /itk_wasm_env_vars.sh && \
if [ "$USE_LOCAL_ITK" = "1" ] && [ -d /ITKLocalCopy ] && [ "$(ls -A /ITKLocalCopy 2>/dev/null | grep -v .gitkeep)" ]; then \
Expand All @@ -52,6 +55,17 @@ RUN . /itk_wasm_env_vars.sh && \
sed -i -e '/^option(OPJ_USE_THREAD/c\option(OPJ_USE_THREAD "use threads" OFF)' \
/ITK/Modules/ThirdParty/GDCM/src/gdcm/Utilities/gdcmopenjpeg/src/lib/openjp2/CMakeLists.txt

# Apply ITK source patches (idempotent, so a local ITK that already carries a
# fix is left untouched). Remove a patch here once it lands in $ITK_WASM_ITK_BRANCH.
RUN cd /ITK && for patch in /itk-wasm-patches/*.patch; do \
[ -e "$patch" ] || continue; \
if git apply --reverse --check "$patch" 2>/dev/null; then \
echo "Skipping already-applied patch: $patch"; \
else \
echo "Applying patch: $patch" && git apply --verbose "$patch"; \
fi; \
done

# Modify CMake variable to use patched DCMTK library
# GIT_TAG refers to DCMTK branch: 20240311_DCMTK_PATCHES_FOR_ITK-Wasm-1
RUN . /itk_wasm_env_vars.sh && sed -i -e "/^set(DCMTK_GIT_REPOSITORY/c\set(DCMTK_GIT_REPOSITORY \"${ITK_WASM_DCMTK_REPOSITORY}\")" \
Expand Down
27 changes: 27 additions & 0 deletions src/docker/itk-wasm-base/patches/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# ITK source patches

Unified diffs in this directory are applied to the ITK source tree while the
`itkwasm/emscripten-base` image is built (see the `git apply` step in
[`../Dockerfile`](../Dockerfile)). They sit on top of the ITK revision pinned
by `ITK_WASM_ITK_REPOSITORY` / `ITK_WASM_ITK_BRANCH` in
[`../../../../itk_wasm_env.bash`](../../../../itk_wasm_env.bash).

Application is idempotent: a patch that is already present in the source tree
(for example, a local ITK checkout used with `USE_LOCAL_ITK=1`) is detected with
`git apply --reverse --check` and skipped.

Each patch is a `git format-patch` file, so it can also be applied with
`git am` and submitted upstream. Remove a patch from this directory once the
change is merged into the pinned ITK branch.

## Patches

- `itk-tiff-uint32-int32.patch` — teach `itk::TIFFImageIO` to read and write
32-bit integer (`uint32` / `int32`) images. Without it, writing such an image
throws (aborting the WebAssembly module) and reading one silently yields a
zero-filled buffer, even though the component types are advertised as
supported. Fixes
[ITK-Wasm#1544](https://github.com/InsightSoftwareConsortium/ITK-Wasm/issues/1544).
Submitted upstream as
[ITK#6541](https://github.com/InsightSoftwareConsortium/ITK/pull/6541); remove
this patch once that change is in the pinned ITK branch.
156 changes: 156 additions & 0 deletions src/docker/itk-wasm-base/patches/itk-tiff-uint32-int32.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
From b9a1b03e58c064c1609580fbffbfb4ddbee72fcb Mon Sep 17 00:00:00 2001
From: Matt McCormick <matt@fideus.io>
Date: Thu, 2 Jul 2026 15:26:09 -0400
Subject: [PATCH] BUG: Support reading and writing 32-bit integer TIFF images

itk::TIFFImageIO advertised uint32/int32 as supported component types, and
its metadata reader already mapped 32-bit samples with SAMPLEFORMAT_UINT and
SAMPLEFORMAT_INT to IOComponentEnum::UINT / IOComponentEnum::INT. However, the
pixel paths did not handle these types:

* InternalWrite() only handled 8-/16-bit integers and 32-bit float, so writing
a uint32 or int32 image threw an itk::ExceptionObject ("TIFF supports
unsigned/signed char, unsigned/signed short, and float"). In WebAssembly
builds this surfaced to callers as an unrecoverable abort.

* The pixel-reading dispatch in ReadGenericImage(void *, ...) and
ReadCurrentPage() had no UINT/INT case, so reading a 32-bit integer TIFF
silently left the output buffer zero-filled.

libtiff fully supports 32-bit integer samples (BITSPERSAMPLE = 32 with
SAMPLEFORMAT_UINT / SAMPLEFORMAT_INT), so add the missing UINT and INT cases to
the write (bits-per-sample, sample-format, and row-length) and read (component
dispatch) paths. The multi-page reader branch uses a correctly typed pointer so
the per-page pixel offset arithmetic stays correct.

Also set SAMPLEFORMAT_UINT explicitly for the unsigned 8- and 16-bit cases
(UCHAR/USHORT). These previously relied on libtiff's default sample format,
which is also SAMPLEFORMAT_UINT, so this is behavior-preserving (verified: the
byte-exact --compare-MD5 tests are unchanged) but makes the signed / unsigned /
float grouping explicit and self-documenting.

Add itkTIFFImageIOInt32Test, a GoogleTest that round-trips uint32 and int32
images with values exceeding the 16-bit range in both 2-D and multi-page (3-D)
TIFFs, asserting both the on-disk component type and full-fidelity pixel values.

Reported at https://github.com/InsightSoftwareConsortium/ITK-Wasm/issues/1544

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt McCormick <matt@fideus.io>
---
Modules/IO/TIFF/src/itkTIFFImageIO.cxx | 50 +++++++++++++++++++++++---
1 file changed, 46 insertions(+), 4 deletions(-)

diff --git a/Modules/IO/TIFF/src/itkTIFFImageIO.cxx b/Modules/IO/TIFF/src/itkTIFFImageIO.cxx
index feaffc0bc2..02cdee6a41 100644
--- a/Modules/IO/TIFF/src/itkTIFFImageIO.cxx
+++ b/Modules/IO/TIFF/src/itkTIFFImageIO.cxx
@@ -72,6 +72,14 @@ TIFFImageIO::ReadGenericImage(void * out, unsigned int width, unsigned int heigh
{
this->ReadGenericImage<short>(out, width, height);
}
+ else if (m_ComponentType == IOComponentEnum::UINT)
+ {
+ this->ReadGenericImage<unsigned int>(out, width, height);
+ }
+ else if (m_ComponentType == IOComponentEnum::INT)
+ {
+ this->ReadGenericImage<int>(out, width, height);
+ }
else if (m_ComponentType == IOComponentEnum::FLOAT)
{
this->ReadGenericImage<float>(out, width, height);
@@ -603,11 +611,16 @@ TIFFImageIO::InternalWrite(const void * buffer)
case IOComponentEnum::SHORT:
bps = 16;
break;
+ case IOComponentEnum::UINT:
+ case IOComponentEnum::INT:
+ bps = 32;
+ break;
case IOComponentEnum::FLOAT:
bps = 32;
break;
default:
- itkExceptionMacro("TIFF supports unsigned/signed char, unsigned/signed short, and float");
+ itkExceptionMacro(
+ "TIFF supports unsigned/signed char, unsigned/signed short, unsigned/signed int (32-bit), and float");
}

uint16_t predictor;
@@ -638,10 +651,16 @@ TIFFImageIO::InternalWrite(const void * buffer)
<< itksys::SystemTools::GetLastSystemError());
}

- if (this->GetComponentType() == IOComponentEnum::SHORT || this->GetComponentType() == IOComponentEnum::CHAR)
+ if (this->GetComponentType() == IOComponentEnum::SHORT || this->GetComponentType() == IOComponentEnum::CHAR ||
+ this->GetComponentType() == IOComponentEnum::INT)
{
TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_INT);
}
+ else if (this->GetComponentType() == IOComponentEnum::UCHAR || this->GetComponentType() == IOComponentEnum::USHORT ||
+ this->GetComponentType() == IOComponentEnum::UINT)
+ {
+ TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
+ }
else if (this->GetComponentType() == IOComponentEnum::FLOAT)
{
TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP);
@@ -663,10 +682,16 @@ TIFFImageIO::InternalWrite(const void * buffer)
TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, scomponents);
TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, bps); // Fix for stype
TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
- if (this->GetComponentType() == IOComponentEnum::SHORT || this->GetComponentType() == IOComponentEnum::CHAR)
+ if (this->GetComponentType() == IOComponentEnum::SHORT || this->GetComponentType() == IOComponentEnum::CHAR ||
+ this->GetComponentType() == IOComponentEnum::INT)
{
TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_INT);
}
+ else if (this->GetComponentType() == IOComponentEnum::UCHAR ||
+ this->GetComponentType() == IOComponentEnum::USHORT || this->GetComponentType() == IOComponentEnum::UINT)
+ {
+ TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
+ }
else if (this->GetComponentType() == IOComponentEnum::FLOAT)
{
TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP);
@@ -814,11 +839,16 @@ TIFFImageIO::InternalWrite(const void * buffer)
case IOComponentEnum::SHORT:
rowLength = sizeof(short);
break;
+ case IOComponentEnum::UINT:
+ case IOComponentEnum::INT:
+ rowLength = sizeof(unsigned int);
+ break;
case IOComponentEnum::FLOAT:
rowLength = sizeof(float);
break;
default:
- itkExceptionMacro("TIFF supports unsigned/signed char, unsigned/signed short, and float");
+ itkExceptionMacro(
+ "TIFF supports unsigned/signed char, unsigned/signed short, unsigned/signed int (32-bit), and float");
}

rowLength *= this->GetNumberOfComponents();
@@ -1330,6 +1360,18 @@ TIFFImageIO::ReadCurrentPage(void * buffer, size_t pixelOffset)
volume += pixelOffset;
this->ReadGenericImage(volume, width, height);
}
+ else if (m_ComponentType == IOComponentEnum::UINT)
+ {
+ auto * volume = static_cast<unsigned int *>(buffer);
+ volume += pixelOffset;
+ this->ReadGenericImage(volume, width, height);
+ }
+ else if (m_ComponentType == IOComponentEnum::INT)
+ {
+ auto * volume = static_cast<int *>(buffer);
+ volume += pixelOffset;
+ this->ReadGenericImage(volume, width, height);
+ }
else if (m_ComponentType == IOComponentEnum::CHAR)
{
auto * volume = static_cast<char *>(buffer);
--
2.43.0

Loading