From 1ac86fdf6950909e6b2dec9b3dc011290dae2d71 Mon Sep 17 00:00:00 2001 From: Nico Stuurman Date: Wed, 17 Jun 2026 15:11:27 -0700 Subject: [PATCH 1/3] Prevent fileChannel NPE when overwriting pixels. This NPE is unavoidable when writing large data sets. Instead of letting it happen, re-open the file channel and write the desired data and close it again to avoid leaving files open. --- .../ndtiffstorage/NDTiffWriter.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/java/src/main/java/org/micromanager/ndtiffstorage/NDTiffWriter.java b/java/src/main/java/org/micromanager/ndtiffstorage/NDTiffWriter.java index 4427c58..33c8c4b 100644 --- a/java/src/main/java/org/micromanager/ndtiffstorage/NDTiffWriter.java +++ b/java/src/main/java/org/micromanager/ndtiffstorage/NDTiffWriter.java @@ -292,9 +292,41 @@ private long unsignInt(int i) { public void overwritePixels(String indexKey, Object pixels, boolean rgb) throws IOException { long pixelOffset = indexMap_.get(indexKey).pixOffset_; Buffer pixBuff = getPixelBuffer(pixels, rgb); + if (fileChannel_ == null) { + // This writer has already been finishedWriting() (its file was rotated/closed as + // the file set grew). Pixels still need to be overwritten in place -- e.g. when + // building low-res pyramid levels, a downsampled tile receives contributions from + // several full-res tiles, and the tile's first write may live in an earlier, + // now-closed file. Reopen the file read-write just for this in-place write, then + // close it again so we don't leak file descriptors for every rotated file. + overwritePixelsReopening(pixBuff, pixelOffset); + return; + } fileChannelWrite(pixBuff, pixelOffset); } + /** + * Overwrite pixels in place in a file whose channel has been closed (after + * finishedWriting()). Opens the existing file read-write at {@code position}, writes the + * buffer, and closes -- the writer stays in its finished state. The index entry's + * pixOffset_ is an absolute file offset, so no header re-parsing is needed. + */ + private void overwritePixelsReopening(final Buffer buffer, final long position) + throws IOException { + buffer.rewind(); + RandomAccessFile raf = null; + try { + raf = new RandomAccessFile(filename_, "rw"); + FileChannel ch = raf.getChannel(); + ch.write((ByteBuffer) buffer, position); + } finally { + if (raf != null) { + raf.close(); + } + } + masterMPTiffStorage_.tryRecycleLargeBuffer((ByteBuffer) buffer); + } + private IndexEntryData writeIFD(String indexKey, Object pixels, byte[] metadata, boolean rgb, int imageHeight, int imageWidth, int bitDepth ) throws IOException { From 9f852808ac4d3be124972f589dda4dcfe591d039 Mon Sep 17 00:00:00 2001 From: Nico Stuurman Date: Wed, 17 Jun 2026 15:12:19 -0700 Subject: [PATCH 2/3] Bump version to 2.19.3 --- java/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/pom.xml b/java/pom.xml index 5ca09ae..026ba39 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.micro-manager.ndtiffstorage NDTiffStorage - 2.19.2 + 2.19.3 jar NDTiff Storage file format Java-based writer and reader used for NDTiffStorage format and NDRAM storage From 9039a3640997c5728565b0caeba358633d7f4db5 Mon Sep 17 00:00:00 2001 From: Nico Stuurman Date: Wed, 17 Jun 2026 15:24:57 -0700 Subject: [PATCH 3/3] =?UTF-8?q?=20=201.=20Buffer=20recycle=20moved=20into?= =?UTF-8?q?=20finally=20=E2=80=94=20tryRecycleLargeBuffer(byteBuffer)=20no?= =?UTF-8?q?w=20=20=20=20runs=20whether=20or=20not=20ch.write=20throws,=20s?= =?UTF-8?q?o=20a=20write/IO=20failure=20can't=20leak=20a=20large=20=20=20d?= =?UTF-8?q?irect=20buffer.=20(Recycle=20runs=20before=20raf.close(),=20bot?= =?UTF-8?q?h=20in=20finally=20=E2=80=94=20order=20is=20=20=20fine=20since?= =?UTF-8?q?=20recycling=20just=20returns=20the=20buffer=20to=20the=20pool.?= =?UTF-8?q?)=20=20=202.=20Partial-write=20loop=20=E2=80=94=20while=20(byte?= =?UTF-8?q?Buffer.hasRemaining())=20pos=20+=3D=20=20=20ch.write(byteBuffer?= =?UTF-8?q?,=20pos).=20Positional=20writes=20can=20be=20short,=20so=20it?= =?UTF-8?q?=20loops=20until=20=20=20the=20buffer=20is=20fully=20written,?= =?UTF-8?q?=20advancing=20the=20file=20position=20by=20the=20bytes=20actua?= =?UTF-8?q?lly=20=20=20written=20each=20iteration.=20The=20relative=20posi?= =?UTF-8?q?tion=20of=20the=20buffer=20auto-advances=20=20=20(positional=20?= =?UTF-8?q?write=20consumes=20from=20position()),=20and=20pos=20tracks=20t?= =?UTF-8?q?he=20file=20offset=20=20=20independently=20=E2=80=94=20correct.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/micromanager/ndtiffstorage/NDTiffWriter.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/java/src/main/java/org/micromanager/ndtiffstorage/NDTiffWriter.java b/java/src/main/java/org/micromanager/ndtiffstorage/NDTiffWriter.java index 33c8c4b..5996891 100644 --- a/java/src/main/java/org/micromanager/ndtiffstorage/NDTiffWriter.java +++ b/java/src/main/java/org/micromanager/ndtiffstorage/NDTiffWriter.java @@ -314,17 +314,24 @@ public void overwritePixels(String indexKey, Object pixels, boolean rgb) throws private void overwritePixelsReopening(final Buffer buffer, final long position) throws IOException { buffer.rewind(); + ByteBuffer byteBuffer = (ByteBuffer) buffer; RandomAccessFile raf = null; try { raf = new RandomAccessFile(filename_, "rw"); FileChannel ch = raf.getChannel(); - ch.write((ByteBuffer) buffer, position); + // A single positional write is not guaranteed to write the whole buffer; loop + // until it is fully written, advancing the file position by the bytes written. + long pos = position; + while (byteBuffer.hasRemaining()) { + pos += ch.write(byteBuffer, pos); + } } finally { + // Recycle in finally so a write/IO failure cannot leak a large direct buffer. + masterMPTiffStorage_.tryRecycleLargeBuffer(byteBuffer); if (raf != null) { raf.close(); } } - masterMPTiffStorage_.tryRecycleLargeBuffer((ByteBuffer) buffer); } private IndexEntryData writeIFD(String indexKey, Object pixels, byte[] metadata,