From df0e075c9e950668b3c47842e9e851a54385759d Mon Sep 17 00:00:00 2001 From: hahmann Date: Wed, 14 Oct 2026 16:05:09 +0200 Subject: [PATCH 1/9] Replace PyramidContents name and placeholder flag fields with accessors Both were public fields. They are now private behind name() and hasPlaceholderCalibration(). Neither was mutable, but the remaining fields will get accessors for issue #114, and the API should be uniform. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../main/java/ome/zarr/fiji/PyramidalBdv.java | 2 +- .../java/ome/zarr/fiji/PyramidalDataset.java | 4 +- .../main/java/ome/zarr/fiji/read/OmeZarr.java | 6 +-- .../zarr/fiji/util/OmeZarrImagesChooser.java | 2 +- .../java/ome/zarr/fiji/read/OmeZarrTest.java | 2 +- .../ome/zarr/imglib2/PyramidContents.java | 37 ++++++++++++------- .../ome/zarr/n5/N5PyramidBackendTest.java | 2 +- .../zarrjava/ZarrJavaPyramidBackendTest.java | 6 +-- .../zarr/imglib2/PyramidBackendTestBase.java | 12 +++--- 9 files changed, 42 insertions(+), 31 deletions(-) diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java index 524728744..1506a2568 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java @@ -94,7 +94,7 @@ public List< SourceAndConverter< T > > asSources() public String getName() { - return contents.name; + return contents.name(); } @SuppressWarnings( "unchecked" ) diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalDataset.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalDataset.java index 9c4409f43..a46635a52 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalDataset.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalDataset.java @@ -79,7 +79,7 @@ public < T extends NativeType< T > & RealType< T > > PyramidalDataset( final Con this.resolutionLevel = resolutionLevel; setRGBMerged( false ); if ( contents.numResolutionLevels() > 1 ) - setName( multiResolutionName( contents.name, resolutionLevel, contents.numResolutionLevels() ) ); + setName( multiResolutionName( contents.name(), resolutionLevel, contents.numResolutionLevels() ) ); } public int resolutionLevel() @@ -137,7 +137,7 @@ private static < T extends NativeType< T > & RealType< T > > ImgPlus< T > create } final AxisCalibration[] selectedAxes = contents.axesPerLevel[ resolutionLevel ]; - final ImgPlus< T > imgPlus = new ImgPlus<>( contents.cachedCellImgs[ resolutionLevel ], contents.name ); + final ImgPlus< T > imgPlus = new ImgPlus<>( contents.cachedCellImgs[ resolutionLevel ], contents.name() ); for ( int i = 0; i < selectedAxes.length; i++ ) { final AxisType axisType = AXIS_TYPE_MAP.getOrDefault( selectedAxes[ i ].name, Axes.unknown() ); diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/read/OmeZarr.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/read/OmeZarr.java index c850281ce..dbc0aacb3 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/read/OmeZarr.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/read/OmeZarr.java @@ -76,7 +76,7 @@ * BigDataViewer displays all levels and streams them lazily. *

* Independently of the width, every display path refuses to show an image whose - * calibration is a placeholder ({@link PyramidContents#hasPlaceholderCalibration}) + * calibration is a placeholder ({@link PyramidContents#hasPlaceholderCalibration()}) * unless the user confirms. */ public class OmeZarr @@ -267,7 +267,7 @@ public PyramidalDataset showInImageJ() /** * Whether the image must not be shown: {@code true} only when its calibration is - * a placeholder (see {@link PyramidContents#hasPlaceholderCalibration}) and the + * a placeholder (see {@link PyramidContents#hasPlaceholderCalibration()}) and the * user declined to open it anyway. *

* Every display path asks this, because the invented scale and unit belong to @@ -276,7 +276,7 @@ public PyramidalDataset showInImageJ() */ private boolean uncalibratedAndDeclined( final PyramidContents< ? > contents ) { - if ( !contents.hasPlaceholderCalibration ) + if ( !contents.hasPlaceholderCalibration() ) return false; final boolean openAnyway = openAnywayConfirmation.test( uncalibratedMessage() ); logger.info( "{} has no scale or unit of its own. Opening it anyway: {}.", inputUri, openAnyway ); diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/OmeZarrImagesChooser.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/OmeZarrImagesChooser.java index e575b2813..b5e79a76b 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/OmeZarrImagesChooser.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/OmeZarrImagesChooser.java @@ -61,7 +61,7 @@ private OmeZarrImagesChooser() public static List< PyramidContents< ? > > choose( final List< PyramidContents< ? > > candidates, final PyramidContents< ? > preselected ) { final List< JCheckBox > checkboxes = candidates.stream() - .map( contents -> new JCheckBox( contents.name, contents == preselected ) ) + .map( contents -> new JCheckBox( contents.name(), contents == preselected ) ) .collect( Collectors.toList() ); final JOptionPane pane = new JOptionPane( checkboxes.toArray(), JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION ); final JDialog dialog = pane.createDialog( "OME-Zarr Images to open" ); diff --git a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/read/OmeZarrTest.java b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/read/OmeZarrTest.java index 283f6f579..55078eb7d 100644 --- a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/read/OmeZarrTest.java +++ b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/read/OmeZarrTest.java @@ -223,7 +223,7 @@ void showInImageJAsksBeforeOpeningUncalibratedImage( PyramidBackend backend ) th assertEquals( 1, datasetService.getDatasets().size(), "Accepting must open the image" ); PyramidalDataset opened = assertInstanceOf( PyramidalDataset.class, context.getService( PyramidalService.class ).getPyramidals().get( 0 ) ); - assertTrue( opened.getPyramidContents().hasPlaceholderCalibration, + assertTrue( opened.getPyramidContents().hasPlaceholderCalibration(), "The opened image still knows its calibration was made up" ); SwingUtilities.invokeAndWait( () -> {} ); // let Swing process the show diff --git a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java index 0b58e0635..1f5a5fd3b 100644 --- a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java +++ b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java @@ -73,7 +73,7 @@ public final class PyramidContents< T extends NativeType< T > & RealType< T > > */ public static final int NO_MATCHING_LEVEL = -1; - public final String name; + private final String name; public final T type; @@ -101,15 +101,7 @@ public final class PyramidContents< T extends NativeType< T > & RealType< T > > /** OMERO rendering metadata, or {@code null} if unavailable. */ public final Omero omero; - /** - * Whether the scale and unit in {@link #axesPerLevel} are placeholders that no - * part of the dataset states, rather than real calibration (see - * {@link AxisCalibration#createPlaceholderCalibration}). - *

- * {@code true} only for a single array read from its own axis names alone, - * with no parent multiscales group to supply a scale or unit. - */ - public final boolean hasPlaceholderCalibration; + private final boolean hasPlaceholderCalibration; private PyramidContents( final Builder< T > b ) { @@ -127,6 +119,25 @@ private PyramidContents( final Builder< T > b ) + ") does not match the number of image dimensions (" + numDimensions + ")." ); } + /** Dataset name. */ + public String name() + { + return name; + } + + /** + * Whether the scale and unit in {@link #axesPerLevel} are placeholders that no + * part of the dataset states, rather than real calibration (see + * {@link AxisCalibration#createPlaceholderCalibration}). + *

+ * {@code true} only for a single array read from its own axis names alone, + * with no parent multiscales group to supply a scale or unit. + */ + public boolean hasPlaceholderCalibration() + { + return hasPlaceholderCalibration; + } + /** * Number of resolution levels in the pyramid, i.e. the length of * {@link #cachedCellImgs}. @@ -230,7 +241,7 @@ public int numTimepoints() /** * One label per channel. * Derived from the OMERO channel metadata when available and consistent with - * {@link #numChannels()}, otherwise falling back to the dataset {@link #name}. + * {@link #numChannels()}, otherwise falling back to the dataset {@link #name()}. */ public String[] channelLabels() { @@ -466,7 +477,7 @@ public static < T extends NativeType< T > & RealType< T > > PyramidContents< T > * Factory for one case where a level's calibration has to be made up: a bare * array node read from its own axis names alone, with no parent multiscales * group to state a scale or unit. The result has - * {@link #hasPlaceholderCalibration} set. + * {@link #hasPlaceholderCalibration()} set. * * @param pixel type * @param name dataset name @@ -540,7 +551,7 @@ public Builder< T > omero( final Omero o ) return this; } - /** See {@link PyramidContents#hasPlaceholderCalibration}. Defaults to {@code false}. */ + /** See {@link PyramidContents#hasPlaceholderCalibration()}. Defaults to {@code false}. */ public Builder< T > hasPlaceholderCalibration( final boolean h ) { this.hasPlaceholderCalibration = h; diff --git a/ome-zarr-n5/src/test/java/ome/zarr/n5/N5PyramidBackendTest.java b/ome-zarr-n5/src/test/java/ome/zarr/n5/N5PyramidBackendTest.java index 68cf9c73b..82a19e240 100644 --- a/ome-zarr-n5/src/test/java/ome/zarr/n5/N5PyramidBackendTest.java +++ b/ome-zarr-n5/src/test/java/ome/zarr/n5/N5PyramidBackendTest.java @@ -62,7 +62,7 @@ void testStaticReadPyramid() throws URISyntaxException Path path = ZarrTestUtils.resourcePath( "ome/zarr/testdata/5d_testing/5d_dataset_v4.ome.zarr" ); PyramidContents< ? > contents = N5PyramidBackend.readPyramid( path.toUri() ); assertNotNull( contents ); - assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name ); + assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name() ); assertEquals( 5, contents.numDimensions() ); assertEquals( 2, contents.numResolutionLevels() ); } diff --git a/ome-zarr-zarrjava/src/test/java/ome/zarr/zarrjava/ZarrJavaPyramidBackendTest.java b/ome-zarr-zarrjava/src/test/java/ome/zarr/zarrjava/ZarrJavaPyramidBackendTest.java index 953545794..fd3107920 100644 --- a/ome-zarr-zarrjava/src/test/java/ome/zarr/zarrjava/ZarrJavaPyramidBackendTest.java +++ b/ome-zarr-zarrjava/src/test/java/ome/zarr/zarrjava/ZarrJavaPyramidBackendTest.java @@ -64,7 +64,7 @@ void testStaticReadPyramid() throws URISyntaxException Path path = ZarrTestUtils.resourcePath( "ome/zarr/testdata/5d_testing/5d_dataset_v4.ome.zarr" ); PyramidContents< ? > contents = ZarrJavaPyramidBackend.readPyramid( path.toUri() ); assertNotNull( contents ); - assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name ); + assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name() ); assertEquals( 5, contents.numDimensions() ); assertEquals( 2, contents.numResolutionLevels() ); } @@ -75,7 +75,7 @@ void testReadZippedArchive( @TempDir final Path tempDir ) throws Exception Path archive = ZarrTestUtils.zipDataset( "ome/zarr/testdata/5d_testing/5d_dataset_v4.ome.zarr", tempDir.resolve( "5d.ozx" ) ); PyramidContents< ? > contents = ZarrJavaPyramidBackend.readPyramid( archive.toUri() ); assertNotNull( contents ); - assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name ); + assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name() ); assertEquals( 5, contents.numDimensions() ); assertEquals( 2, contents.numResolutionLevels() ); } @@ -91,7 +91,7 @@ void testReadZippedArchiveOverHttp( @TempDir final Path tempDir ) throws Excepti URI uri = URI.create( "http://localhost:" + server.getAddress().getPort() + "/5d.ozx" ); PyramidContents< ? > contents = ZarrJavaPyramidBackend.readPyramid( uri ); assertNotNull( contents ); - assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name ); + assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name() ); assertEquals( 5, contents.numDimensions() ); assertEquals( 2, contents.numResolutionLevels() ); } diff --git a/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java b/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java index cc9f21b20..4367168fa 100644 --- a/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java +++ b/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java @@ -151,9 +151,9 @@ default void testSingleArrayWithoutParentMultiscale() throws URISyntaxException assertNotNull( contents ); assertEquals( 1, contents.numResolutionLevels() ); assertEquals( 2, contents.numDimensions() ); - assertEquals( "0", contents.name, "Without a multiscales name, the array node name is used" ); + assertEquals( "0", contents.name(), "Without a multiscales name, the array node name is used" ); assertNull( contents.omero ); - assertTrue( contents.hasPlaceholderCalibration, + assertTrue( contents.hasPlaceholderCalibration(), "An array read from its own axis names has no real scale or unit, and must say so" ); // dimension_names are y, x; the imglib2 image is in F-order, so x comes first. @@ -219,7 +219,7 @@ default void testSingleLevelFromParentMultiscale( String resource ) throws URISy assertNotNull( contents ); assertEquals( 1, contents.numResolutionLevels(), "A single level is a one-level pyramid" ); assertEquals( 5, contents.numDimensions() ); - assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name, "The name comes from the parent multiscales entry" ); + assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name(), "The name comes from the parent multiscales entry" ); // Level 1 of the 5d dataset: zarr [t, c, z, y, x] = [4, 3, 8, 32, 32], // reversed into imglib2 F-order. @@ -239,7 +239,7 @@ default void testSingleLevelFromParentMultiscale( String resource ) throws URISy new double[] { transform.get( 0, 0 ), transform.get( 1, 1 ), transform.get( 2, 2 ) }, "The transform is the one of the level that was read, not of level 0" ); - assertFalse( contents.hasPlaceholderCalibration, + assertFalse( contents.hasPlaceholderCalibration(), "The scale and unit come from the parent multiscales group, so they are real" ); Omero omero = contents.omero; @@ -269,7 +269,7 @@ default void testNestedMultiscaleDatasetPaths( String resource ) throws URISynta try (Context context = new Context()) { PyramidContents< ? > contents = this.read( resource, context ); - assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name ); + assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name() ); assertEquals( 2, contents.numResolutionLevels() ); assertEquals( 16, contents.asImg( 0 ).dimension( 0 ) ); assertEquals( 8, contents.asImg( 1 ).dimension( 0 ) ); @@ -426,7 +426,7 @@ default void testGetName( String resource ) throws URISyntaxException try (Context context = new Context()) { PyramidContents< ? > contents = this.read( resource, context ); - assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name ); + assertEquals( ZarrTestUtils.IMAGE_NAME, contents.name() ); } } From b98a4b4c516844213e919d1f78ab7f525dc6691a Mon Sep 17 00:00:00 2001 From: hahmann Date: Wed, 14 Oct 2026 16:27:22 +0200 Subject: [PATCH 2/9] Make OMERO metadata in PyramidContents immutable Its public fields let any opener change the cached contents (#114). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../zarr/examples/demo/OmeroMetadataDemo.java | 28 +-- .../java/ome/zarr/fiji/util/BdvUtils.java | 41 +++-- .../ome/zarr/fiji/PyramidalBdvTestBase.java | 14 +- .../java/ome/zarr/fiji/util/BdvUtilsTest.java | 6 +- .../ome/zarr/imglib2/PyramidContents.java | 13 +- .../java/ome/zarr/imglib2/metadata/Omero.java | 174 +++++++++++++++--- .../zarr/zarrjava/ZarrJavaPyramidBackend.java | 47 ++--- .../zarr/imglib2/PyramidBackendTestBase.java | 12 +- 8 files changed, 236 insertions(+), 99 deletions(-) diff --git a/ome-zarr-fiji-ui/src/test/java/ome/zarr/examples/demo/OmeroMetadataDemo.java b/ome-zarr-fiji-ui/src/test/java/ome/zarr/examples/demo/OmeroMetadataDemo.java index 0d243dd1f..454264d7d 100644 --- a/ome-zarr-fiji-ui/src/test/java/ome/zarr/examples/demo/OmeroMetadataDemo.java +++ b/ome-zarr-fiji-ui/src/test/java/ome/zarr/examples/demo/OmeroMetadataDemo.java @@ -64,7 +64,7 @@ public static void main( final String[] args ) throws Exception private static void printInfo( final PyramidContents< ? > contents ) { - final Omero omero = contents.omero; + final Omero omero = contents.omero(); if ( omero == null ) { System.out.println( "OMERO metadata : not available" ); @@ -72,27 +72,27 @@ private static void printInfo( final PyramidContents< ? > contents ) else { System.out.println( "--- OMERO metadata ---" ); - System.out.println( " id : " + omero.id ); - System.out.println( " name : " + omero.name ); + System.out.println( " id : " + omero.id() ); + System.out.println( " name : " + omero.name() ); - if ( omero.rdefs != null ) + if ( omero.rdefs() != null ) { - System.out.println( " rdefs.defaultT : " + omero.rdefs.defaultT ); - System.out.println( " rdefs.defaultZ : " + omero.rdefs.defaultZ ); - System.out.println( " rdefs.model : " + omero.rdefs.model ); + System.out.println( " rdefs.defaultT : " + omero.rdefs().defaultT() ); + System.out.println( " rdefs.defaultZ : " + omero.rdefs().defaultZ() ); + System.out.println( " rdefs.model : " + omero.rdefs().model() ); } - if ( omero.channels != null ) + if ( omero.channels() != null ) { - System.out.println( " channels (" + omero.channels.size() + "):" ); - for ( int i = 0; i < omero.channels.size(); i++ ) + System.out.println( " channels (" + omero.channels().size() + "):" ); + for ( int i = 0; i < omero.channels().size(); i++ ) { - final Omero.Channel ch = omero.channels.get( i ); + final Omero.Channel ch = omero.channels().get( i ); System.out.printf( " [%d] label=%-20s active=%-5b color=%s%n", - i, ch.label, ch.active, ch.color ); - if ( ch.window != null ) + i, ch.label(), ch.active(), ch.color() ); + if ( ch.window() != null ) System.out.printf( " window: start=%.1f end=%.1f min=%.1f max=%.1f%n", - ch.window.start, ch.window.end, ch.window.min, ch.window.max ); + ch.window().start(), ch.window().end(), ch.window().min(), ch.window().max() ); } } } diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/BdvUtils.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/BdvUtils.java index 2ce31bd4e..bcf69334d 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/BdvUtils.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/BdvUtils.java @@ -128,8 +128,8 @@ public static BdvHandle show( final PyramidalBdv< ? > pyramidalBdv, final BdvHan shownIn.getViewerPanel().putClientProperty( BDV_HANDLE_KEY, shownIn ); if ( bdvHandle == null ) - setTimepoint( pyramidContents.omero, shownIn.getViewerPanel().state() ); - final List< Omero.Channel > omeroChannels = omeroChannels( pyramidContents.omero, pyramidalBdv.asSources().size() ); + setTimepoint( pyramidContents.omero(), shownIn.getViewerPanel().state() ); + final List< Omero.Channel > omeroChannels = omeroChannels( pyramidContents.omero(), pyramidalBdv.asSources().size() ); setChannelProperties( omeroChannels, pyramidalBdv.asSources(), shownIn.getConverterSetups(), shownIn.getViewerPanel().state() ); Container topLevelContainer = shownIn.getViewerPanel().getRootPane().getParent(); @@ -213,15 +213,16 @@ private static boolean isCurrentSource( final PyramidalBdv< ? > pyramidalBdv, fi } /** - * Colors each of {@code sources} the way the dataset's OMERO metadata asks - * for, gives it that channel's display range, and shows or hides it. + * Colors each of {@code sources} the way encoded in the OMERO channel metadata. + * Sets each source's display range and bounds to what the metadata asks for. + * Shows or hides each source according to the metadata. *

- * @param omeroChannels the channels to apply. Nothing is applied if it is empty + * @param omeroChannels the list of OMERO channels to apply, one per source and in source order * @param sources the viewer's sources, one per channel and in channel order - * @param converterSetups the viewer's converter setups, to look {@code sources} up in + * @param converterSetups the viewer's converter setups to look {@code sources} up in * @param state the viewer state that decides which sources are visible */ - private static void setChannelProperties( final List< Omero.Channel > omeroChannels, + public static void setChannelProperties( final List< Omero.Channel > omeroChannels, final List< ? extends SourceAndConverter< ? > > sources, final ConverterSetups converterSetups, final ViewerState state ) { @@ -233,8 +234,8 @@ private static void setChannelProperties( final List< Omero.Channel > omeroChann final Omero.Channel omeroChannel = omeroChannels.get( channelNumber ); final ConverterSetup converterSetup = converterSetups.getConverterSetup( source ); converterSetup.setColor( channelColor( omeroChannel ) ); - if ( omeroChannel != null && omeroChannel.window != null ) - setDisplayRangeAndBounds( converterSetup, converterSetups.getBounds(), omeroChannel.window ); + if ( omeroChannel != null && omeroChannel.window() != null ) + setDisplayRangeAndBounds( converterSetup, converterSetups.getBounds(), omeroChannel.window() ); state.setSourceActive( source, isChannelActive( omeroChannel ) ); } } @@ -254,9 +255,9 @@ private static void setChannelProperties( final List< Omero.Channel > omeroChann private static void setDisplayRangeAndBounds( final ConverterSetup converterSetup, final ConverterSetupBounds converterSetupBounds, final Omero.Channel.Window window ) { - converterSetup.setDisplayRange( window.start, window.end ); - if ( window.max > window.min ) - converterSetupBounds.setBounds( converterSetup, new Bounds( window.min, window.max ) ); + converterSetup.setDisplayRange( window.start(), window.end() ); + if ( window.max() > window.min() ) + converterSetupBounds.setBounds( converterSetup, new Bounds( window.min(), window.max() ) ); } /** @@ -272,16 +273,16 @@ private static void setDisplayRangeAndBounds( final ConverterSetup converterSetu */ static List< Omero.Channel > omeroChannels( final Omero omero, final int numChannels ) { - if ( omero == null || omero.channels == null || omero.channels.isEmpty() ) + if ( omero == null || omero.channels() == null || omero.channels().isEmpty() ) return Collections.emptyList(); - if ( omero.channels.size() != numChannels ) + if ( omero.channels().size() != numChannels ) { logger.warn( "The number of channels in the Omero metadata ({}) does not match the number of sources in the dataset ({}). Channel properties will not be applied.", - omero.channels.size(), numChannels ); + omero.channels().size(), numChannels ); return Collections.emptyList(); } - return omero.channels; + return omero.channels(); } /** @@ -294,9 +295,9 @@ static List< Omero.Channel > omeroChannels( final Omero omero, final int numChan */ private static ARGBType channelColor( final Omero.Channel omeroChannel ) { - final Color color = omeroChannel == null || omeroChannel.color == null + final Color color = omeroChannel == null || omeroChannel.color() == null ? Color.white - : Color.decode( "#" + omeroChannel.color ); + : Color.decode( "#" + omeroChannel.color() ); final int opaque = 255; return new ARGBType( ARGBType.rgba( color.getRed(), color.getGreen(), color.getBlue(), opaque ) ); } @@ -311,7 +312,7 @@ private static ARGBType channelColor( final Omero.Channel omeroChannel ) */ private static boolean isChannelActive( final Omero.Channel omeroChannel ) { - return omeroChannel == null || omeroChannel.active; + return omeroChannel == null || omeroChannel.active(); } /** @@ -323,7 +324,7 @@ private static boolean isChannelActive( final Omero.Channel omeroChannel ) */ private static int defaultTimepoint( final Omero omero ) { - return omero == null || omero.rdefs == null ? 0 : omero.rdefs.defaultT; + return omero == null || omero.rdefs() == null ? 0 : omero.rdefs().defaultT(); } /** diff --git a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/PyramidalBdvTestBase.java b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/PyramidalBdvTestBase.java index 76eb3f8f1..491838ce9 100644 --- a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/PyramidalBdvTestBase.java +++ b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/PyramidalBdvTestBase.java @@ -45,6 +45,7 @@ import org.scijava.Context; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.List; import bdv.tools.brightness.ConverterSetup; @@ -56,6 +57,7 @@ import ome.zarr.ZarrTestUtils; import ome.zarr.fiji.util.BdvUtils; import ome.zarr.imglib2.PyramidContents; +import ome.zarr.imglib2.metadata.Omero; /** * Shared parameterized tests for the BigDataViewer Fiji wrapper @@ -265,13 +267,21 @@ default void testOmeroDisplaySettings( final String resource ) throws URISyntaxE try (Context context = new Context()) { PyramidContents< ? > contents = read( resource, context ); - // NB: neither example turns a channel off, so switch one off to cover both answers. - contents.omero.channels.get( 1 ).active = false; PyramidalBdv< ? > pyramidalBdv = new PyramidalBdv<>( context, contents ); BdvHandle bdvHandle = BdvUtils.show( pyramidalBdv, null ); ViewerState state = bdvHandle.getViewerPanel().state(); assertEquals( 1, state.getCurrentTimepoint(), "the timepoint the rendering defaults name" ); List< ? extends SourceAndConverter< ? > > sources = pyramidalBdv.asSources(); + assertNotNull( contents ); + Omero omero = contents.omero(); + assertNotNull( omero ); + assertNotNull( omero.channels() ); + // NB: neither example turns a channel off, so switch one off to cover both answers. + List< Omero.Channel > channels = new ArrayList<>( omero.channels() ); + Omero.Channel on = channels.get( 1 ); + channels.set( 1, new Omero.Channel( false, on.coefficient(), on.color(), on.family(), on.inverted(), + on.label(), on.window() ) ); + BdvUtils.setChannelProperties( channels, sources, bdvHandle.getConverterSetups(), state ); assertTrue( state.isSourceActive( sources.get( 0 ) ) ); assertFalse( state.isSourceActive( sources.get( 1 ) ) ); assertTrue( state.isSourceActive( sources.get( 2 ) ) ); diff --git a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/util/BdvUtilsTest.java b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/util/BdvUtilsTest.java index 62febe0d1..a95c33d17 100644 --- a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/util/BdvUtilsTest.java +++ b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/util/BdvUtilsTest.java @@ -90,9 +90,9 @@ void showInOneWindowOpensNoWindowForNothing() @Test void testOmeroChannelCountMismatch() { - final Omero omero = new Omero(); - omero.channels = Arrays.asList( new Omero.Channel(), new Omero.Channel(), new Omero.Channel() ); - final int numChannels = omero.channels.size(); + final Omero.Channel channel = new Omero.Channel( true, 1, null, null, false, null, null ); + final Omero omero = new Omero( 0, null, null, Arrays.asList( channel, channel, channel ) ); + final int numChannels = omero.channels().size(); assertEquals( numChannels, BdvUtils.omeroChannels( omero, numChannels ).size(), "as many channels as sources" ); assertTrue( BdvUtils.omeroChannels( omero, numChannels + 1 ).isEmpty(), "fewer channels than sources" ); assertTrue( BdvUtils.omeroChannels( omero, numChannels - 1 ).isEmpty(), "more channels than sources" ); diff --git a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java index 1f5a5fd3b..c2838e8d6 100644 --- a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java +++ b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java @@ -98,8 +98,7 @@ public final class PyramidContents< T extends NativeType< T > & RealType< T > > */ public final AxisCalibration[][] axesPerLevel; - /** OMERO rendering metadata, or {@code null} if unavailable. */ - public final Omero omero; + private final Omero omero; private final boolean hasPlaceholderCalibration; @@ -138,6 +137,12 @@ public boolean hasPlaceholderCalibration() return hasPlaceholderCalibration; } + /** OMERO rendering metadata, or {@code null} if unavailable. */ + public Omero omero() + { + return omero; + } + /** * Number of resolution levels in the pyramid, i.e. the length of * {@link #cachedCellImgs}. @@ -246,7 +251,7 @@ public int numTimepoints() public String[] channelLabels() { final int numChannels = numChannels(); - final boolean omeroValid = omero != null && omero.channels != null && omero.channels.size() == numChannels; + final boolean omeroValid = omero != null && omero.channels() != null && omero.channels().size() == numChannels; if ( omeroValid ) logger.trace( "Creating with OMERO metadata: {}", omero ); else @@ -254,7 +259,7 @@ public String[] channelLabels() final String[] labels = new String[ numChannels ]; for ( int i = 0; i < numChannels; i++ ) - labels[ i ] = omeroValid ? omero.channels.get( i ).label : name; + labels[ i ] = omeroValid ? omero.channels().get( i ).label() : name; return labels; } diff --git a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/metadata/Omero.java b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/metadata/Omero.java index a6c5fa153..d5638639b 100644 --- a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/metadata/Omero.java +++ b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/metadata/Omero.java @@ -28,58 +28,186 @@ */ package ome.zarr.imglib2.metadata; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import com.google.gson.Gson; /** - * The class is used to represent omero metadata. + * The class is used to represent omero metadata. Immutable. */ @SuppressWarnings( "all" ) -public class Omero +public final class Omero { - public int id; + private final int id; - public String name; + private final String name; - public Rdefs rdefs; + private final Rdefs rdefs; - public List< Channel > channels; + private final List< Channel > channels; - public static class Rdefs + public Omero( final int id, final String name, final Rdefs rdefs, final List< Channel > channels ) { - public int defaultT; + this.id = id; + this.name = name; + this.rdefs = rdefs; + this.channels = channels == null ? null : new ArrayList<>( channels ); + } + + public int id() + { + return id; + } + + public String name() + { + return name; + } + + public Rdefs rdefs() + { + return rdefs; + } + + /** Unmodifiable, or {@code null} if the metadata lists no channels. */ + public List< Channel > channels() + { + return channels == null ? null : Collections.unmodifiableList( channels ); + } + + public static final class Rdefs + { + private final int defaultT; + + private final int defaultZ; + + private final String model; + + public Rdefs( final int defaultT, final int defaultZ, final String model ) + { + this.defaultT = defaultT; + this.defaultZ = defaultZ; + this.model = model; + } + + public int defaultT() + { + return defaultT; + } - public int defaultZ; + public int defaultZ() + { + return defaultZ; + } - public String model; + public String model() + { + return model; + } } - public static class Channel + public static final class Channel { - public boolean active; + private final boolean active; + + private final double coefficient; - public double coefficient; + private final String color; - public String color; + private final String family; - public String family; + private final boolean inverted; - public boolean inverted; + private final String label; - public String label; + private final Window window; - public Window window; + public Channel( final boolean active, final double coefficient, final String color, final String family, + final boolean inverted, final String label, final Window window ) + { + this.active = active; + this.coefficient = coefficient; + this.color = color; + this.family = family; + this.inverted = inverted; + this.label = label; + this.window = window; + } + + public boolean active() + { + return active; + } + + public double coefficient() + { + return coefficient; + } + + public String color() + { + return color; + } + + public String family() + { + return family; + } - public static class Window + public boolean inverted() { - public double start; + return inverted; + } + + public String label() + { + return label; + } + + public Window window() + { + return window; + } + + public static final class Window + { + private final double start; + + private final double end; + + private final double min; + + private final double max; + + public Window( final double start, final double end, final double min, final double max ) + { + this.start = start; + this.end = end; + this.min = min; + this.max = max; + } + + public double start() + { + return start; + } - public double end; + public double end() + { + return end; + } - public double min; + public double min() + { + return min; + } - public double max; + public double max() + { + return max; + } } } diff --git a/ome-zarr-zarrjava/src/main/java/ome/zarr/zarrjava/ZarrJavaPyramidBackend.java b/ome-zarr-zarrjava/src/main/java/ome/zarr/zarrjava/ZarrJavaPyramidBackend.java index e0cb857e1..784eb4f6d 100644 --- a/ome-zarr-zarrjava/src/main/java/ome/zarr/zarrjava/ZarrJavaPyramidBackend.java +++ b/ome-zarr-zarrjava/src/main/java/ome/zarr/zarrjava/ZarrJavaPyramidBackend.java @@ -190,56 +190,49 @@ private static Omero convertOmero( final OmeroMetadata source ) { if ( source == null ) return null; - final Omero omero = new Omero(); - omero.id = source.id != null ? source.id : 0; - omero.name = source.name; - omero.rdefs = convertRdefs( source.rdefs ); + List< Omero.Channel > channels = null; if ( source.channels != null ) { - final List< Omero.Channel > channels = new ArrayList<>( source.channels.size() ); + channels = new ArrayList<>( source.channels.size() ); for ( final OmeroChannel channel : source.channels ) channels.add( convertChannel( channel ) ); - omero.channels = channels; } - return omero; + return new Omero( source.id != null ? source.id : 0, source.name, convertRdefs( source.rdefs ), channels ); } private static Omero.Rdefs convertRdefs( final OmeroRdefs source ) { if ( source == null ) return null; - final Omero.Rdefs rdefs = new Omero.Rdefs(); - rdefs.defaultT = source.defaultT != null ? source.defaultT : 0; - rdefs.defaultZ = source.defaultZ != null ? source.defaultZ : 0; - rdefs.model = source.model; - return rdefs; + return new Omero.Rdefs( + source.defaultT != null ? source.defaultT : 0, + source.defaultZ != null ? source.defaultZ : 0, + source.model ); } private static Omero.Channel convertChannel( final OmeroChannel source ) { if ( source == null ) return null; - final Omero.Channel channel = new Omero.Channel(); - channel.active = source.active != null && source.active; - channel.coefficient = source.coefficient != null ? source.coefficient : 0.0; - channel.color = source.color; - channel.family = source.family; - channel.inverted = source.inverted != null && source.inverted; - channel.label = source.label; - channel.window = convertWindow( source.window ); - return channel; + return new Omero.Channel( + source.active != null && source.active, + source.coefficient != null ? source.coefficient : 0.0, + source.color, + source.family, + source.inverted != null && source.inverted, + source.label, + convertWindow( source.window ) ); } private static Omero.Channel.Window convertWindow( final OmeroWindow source ) { if ( source == null ) return null; - final Omero.Channel.Window window = new Omero.Channel.Window(); - window.start = source.start != null ? source.start : 0.0; - window.end = source.end != null ? source.end : 0.0; - window.min = source.min != null ? source.min : 0.0; - window.max = source.max != null ? source.max : 0.0; - return window; + return new Omero.Channel.Window( + source.start != null ? source.start : 0.0, + source.end != null ? source.end : 0.0, + source.min != null ? source.min : 0.0, + source.max != null ? source.max : 0.0 ); } // --------------------------------------------------------------------- diff --git a/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java b/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java index 4367168fa..2a07dd6cb 100644 --- a/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java +++ b/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java @@ -152,7 +152,7 @@ default void testSingleArrayWithoutParentMultiscale() throws URISyntaxException assertEquals( 1, contents.numResolutionLevels() ); assertEquals( 2, contents.numDimensions() ); assertEquals( "0", contents.name(), "Without a multiscales name, the array node name is used" ); - assertNull( contents.omero ); + assertNull( contents.omero() ); assertTrue( contents.hasPlaceholderCalibration(), "An array read from its own axis names has no real scale or unit, and must say so" ); @@ -242,11 +242,11 @@ default void testSingleLevelFromParentMultiscale( String resource ) throws URISy assertFalse( contents.hasPlaceholderCalibration(), "The scale and unit come from the parent multiscales group, so they are real" ); - Omero omero = contents.omero; + Omero omero = contents.omero(); assertNotNull( omero, "OMERO metadata is inherited from the parent group" ); - assertEquals( 3, omero.channels.size() ); - assertEquals( "lynEGFP", omero.channels.get( 0 ).label ); - assertEquals( "00FF00", omero.channels.get( 0 ).color ); + assertEquals( 3, omero.channels().size() ); + assertEquals( "lynEGFP", omero.channels().get( 0 ).label() ); + assertEquals( "00FF00", omero.channels().get( 0 ).color() ); } } @@ -438,7 +438,7 @@ default void testOmeroRdefs( String resource ) throws URISyntaxException { PyramidContents< ? > contents = this.read( resource, context ); if ( resource.contains( "5d_testing" ) ) // dataset with omero properties - assertEquals( 1, contents.omero.rdefs.defaultT ); + assertEquals( 1, contents.omero().rdefs().defaultT() ); } } From 48336892c70baa79ffb3150b70b3396b2e170e93 Mon Sep 17 00:00:00 2001 From: hahmann Date: Thu, 22 Oct 2026 15:51:00 +0200 Subject: [PATCH 3/9] Hand out a fresh pixel type from PyramidContents (#114) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../command/tools/OpenResolutionLevelCommand.java | 2 +- .../src/main/java/ome/zarr/fiji/PyramidalBdv.java | 2 +- .../main/java/ome/zarr/imglib2/PyramidContents.java | 10 +++++++++- .../java/ome/zarr/imglib2/PyramidBackendTestBase.java | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/plugin/command/tools/OpenResolutionLevelCommand.java b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/plugin/command/tools/OpenResolutionLevelCommand.java index ce5c137e5..aea492522 100644 --- a/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/plugin/command/tools/OpenResolutionLevelCommand.java +++ b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/plugin/command/tools/OpenResolutionLevelCommand.java @@ -158,7 +158,7 @@ static < T extends NativeType< T > & RealType< T > > String describeLevel( final sb.append( ", c=" ).append( contents.sizeAlongAxis( AxisCalibration.C, level ) ); if ( contents.hasAxis( AxisCalibration.T ) ) sb.append( ", t=" ).append( contents.sizeAlongAxis( AxisCalibration.T, level ) ); - return sb.append( ", " ).append( dataTypeName( contents.type ) ) + return sb.append( ", " ).append( dataTypeName( contents.type() ) ) .append( ", slice " ).append( ImageSizes.formatPixels( contents.numXYSlicePixels( level ) ) ) .append( " ~ " ).append( ImageSizes.formatBytes( contents.uncompressedXYSliceBytes( level ) ) ) .append( ", total " ).append( ImageSizes.formatBytes( contents.uncompressedBytes( level ) ) ) diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java index 1506a2568..eb587a99b 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java @@ -103,7 +103,7 @@ List< SourceAndConverter< T > > initSourceAndConverters( final PyramidContents< { final int nLevels = contents.numResolutionLevels(); final int numChannels = contents.numChannels(); - final T type = contents.type; + final T type = contents.type(); final AffineTransform3D[] mipmapTransforms = contents.transforms; final VoxelDimensions voxelDimensions = voxelDimensions( contents ); diff --git a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java index c2838e8d6..1504def33 100644 --- a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java +++ b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java @@ -75,7 +75,7 @@ public final class PyramidContents< T extends NativeType< T > & RealType< T > > private final String name; - public final T type; + private final T type; /** * Per-resolution-level transform from that level's image coordinates to the @@ -124,6 +124,14 @@ public String name() return name; } + /** + * Pixel type, as a new instance on every call. + */ + public T type() + { + return type.createVariable(); + } + /** * Whether the scale and unit in {@link #axesPerLevel} are placeholders that no * part of the dataset states, rather than real calibration (see diff --git a/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java b/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java index 2a07dd6cb..f80635a71 100644 --- a/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java +++ b/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java @@ -389,7 +389,7 @@ default void testGetType( String resource ) throws URISyntaxException try (Context context = new Context()) { PyramidContents< ? > contents = this.read( resource, context ); - Object type = contents.type; + Object type = contents.type(); assertInstanceOf( UnsignedByteType.class, type ); } } From 9ef3c60d87a8a1cdbb854699a99abf0eb2f5924d Mon Sep 17 00:00:00 2001 From: hahmann Date: Thu, 22 Oct 2026 16:16:08 +0200 Subject: [PATCH 4/9] Hide PyramidContents axes behind axes( level ) (#114) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../main/java/ome/zarr/fiji/PyramidalBdv.java | 2 +- .../java/ome/zarr/fiji/PyramidalDataset.java | 9 ++--- .../ome/zarr/imglib2/PyramidContents.java | 34 +++++++++++++------ .../zarr/imglib2/PyramidBackendTestBase.java | 15 ++++---- 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java index eb587a99b..96a34bc0c 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java @@ -149,7 +149,7 @@ private static VoxelDimensions voxelDimensions( final PyramidContents< ? > conte double yScale = 1.0; double zScale = 1.0; String unit = ""; - for ( final AxisCalibration axis : contents.axesPerLevel[ 0 ] ) + for ( final AxisCalibration axis : contents.axes( 0 ) ) { switch ( axis.name ) { diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalDataset.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalDataset.java index a46635a52..348854cf1 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalDataset.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalDataset.java @@ -30,6 +30,7 @@ import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.scijava.Context; @@ -136,12 +137,12 @@ private static < T extends NativeType< T > & RealType< T > > ImgPlus< T > create " (numResolutionLevels = " + contents.numResolutionLevels() + ")" ); } - final AxisCalibration[] selectedAxes = contents.axesPerLevel[ resolutionLevel ]; + final List< AxisCalibration > selectedAxes = contents.axes( resolutionLevel ); final ImgPlus< T > imgPlus = new ImgPlus<>( contents.cachedCellImgs[ resolutionLevel ], contents.name() ); - for ( int i = 0; i < selectedAxes.length; i++ ) + for ( int i = 0; i < selectedAxes.size(); i++ ) { - final AxisType axisType = AXIS_TYPE_MAP.getOrDefault( selectedAxes[ i ].name, Axes.unknown() ); - imgPlus.setAxis( new DefaultLinearAxis( axisType, selectedAxes[ i ].unit, selectedAxes[ i ].scale ), i ); + final AxisType axisType = AXIS_TYPE_MAP.getOrDefault( selectedAxes.get( i ).name, Axes.unknown() ); + imgPlus.setAxis( new DefaultLinearAxis( axisType, selectedAxes.get( i ).unit, selectedAxes.get( i ).scale ), i ); } return imgPlus; } diff --git a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java index 1504def33..695d2e90a 100644 --- a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java +++ b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java @@ -29,6 +29,9 @@ package ome.zarr.imglib2; import java.lang.invoke.MethodHandles; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import net.imglib2.cache.img.CachedCellImg; import net.imglib2.img.Img; @@ -55,7 +58,7 @@ *

* The resolution-level count and the per-axis sizes are derived on demand from * the full-resolution image and its axis list ({@code cachedCellImgs[ 0 ]} and - * {@code axesPerLevel[ 0 ]}), rather than stored, so {@link #numDimensions()}, + * {@code axes( 0 )}), rather than stored, so {@link #numDimensions()}, * {@link #numChannels()}, {@link #numTimepoints()} and {@link #axisIndex} cannot * disagree with one another. The constructor enforces the one cross-axis * invariant they all rely on: the axis list and the image have the same number @@ -85,7 +88,7 @@ public final class PyramidContents< T extends NativeType< T > & RealType< T > > * Source coordinates are the discrete pixel/voxel coordinates of * resolution level i, in imglib2 F-order (x, y, z). Target coordinates * are a single physical world space — in the spatial axes' unit (see - * {@link #axesPerLevel}) — shared by all levels, so that every level overlays + * {@link #axes(int)}) — shared by all levels, so that every level overlays * correctly when displayed together. The transform combines the level's voxel * spacing (scale) with its origin offset (translation) in that world space. */ @@ -93,10 +96,7 @@ public final class PyramidContents< T extends NativeType< T > & RealType< T > > public final CachedCellImg< T, ? >[] cachedCellImgs; - /** - * Axes per resolution level: {@code [resolutionLevel][axisIndex]}. - */ - public final AxisCalibration[][] axesPerLevel; + private final AxisCalibration[][] axesPerLevel; private final Omero omero; @@ -108,7 +108,9 @@ private PyramidContents( final Builder< T > b ) this.type = b.type; this.transforms = b.transforms; this.cachedCellImgs = b.cachedCellImgs; - this.axesPerLevel = b.axesPerLevel; + this.axesPerLevel = new AxisCalibration[ b.axesPerLevel.length ][]; + for ( int level = 0; level < axesPerLevel.length; level++ ) + axesPerLevel[ level ] = b.axesPerLevel[ level ].clone(); this.omero = b.omero; this.hasPlaceholderCalibration = b.hasPlaceholderCalibration; @@ -133,7 +135,7 @@ public T type() } /** - * Whether the scale and unit in {@link #axesPerLevel} are placeholders that no + * Whether the scale and unit in {@link #axes(int)} are placeholders that no * part of the dataset states, rather than real calibration (see * {@link AxisCalibration#createPlaceholderCalibration}). *

@@ -145,6 +147,18 @@ public boolean hasPlaceholderCalibration() return hasPlaceholderCalibration; } + /** + * Axes of the image at the given resolution level, unmodifiable and indexed + * like the image's dimensions (imglib2 F-order). + * + * @throws IndexOutOfBoundsException if {@code resolutionLevel} is not in + * {@code [0, numResolutionLevels())} + */ + public List< AxisCalibration > axes( final int resolutionLevel ) + { + return Collections.unmodifiableList( Arrays.asList( axesPerLevel[ resolutionLevel ] ) ); + } + /** OMERO rendering metadata, or {@code null} if unavailable. */ public Omero omero() { @@ -174,7 +188,7 @@ public int smallestResolutionLevel() * {@link #asLargestImg()}. *

* The dimensions are in imglib2 F-order — a subset of (x, y, z, c, t) — so use - * {@link #axisIndex} / {@link #hasAxis} together with {@link #axesPerLevel} to + * {@link #axisIndex} / {@link #hasAxis} together with {@link #axes(int)} to * map dimension indices to logical axes. */ public Img< T > asImg() @@ -280,7 +294,7 @@ public String[] channelLabels() * The axis is located by name rather than by a fixed position: axes * may be absent, so an axis' index shifts with the set of axes actually * present (in F-order, {@code c} is at index 2 in {@code xyct} but at index 3 - * in {@code xyzc}). The lookup goes through {@link #axesPerLevel}, which is + * in {@code xyzc}). The lookup goes through {@link #axes(int)}, which is * aligned 1:1 with the dimensions of {@code cachedCellImgs[ 0 ]}. */ public int axisIndex( final String axisName ) diff --git a/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java b/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java index f80635a71..e6d0f2df8 100644 --- a/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java +++ b/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java @@ -50,6 +50,7 @@ import org.scijava.Context; import java.net.URISyntaxException; +import java.util.List; import java.util.stream.Stream; import ome.zarr.ZarrTestUtils; @@ -159,7 +160,7 @@ default void testSingleArrayWithoutParentMultiscale() throws URISyntaxException // dimension_names are y, x; the imglib2 image is in F-order, so x comes first. assertEquals( 0, contents.axisIndex( AxisCalibration.X ) ); assertEquals( 1, contents.axisIndex( AxisCalibration.Y ) ); - for ( final AxisCalibration axis : contents.axesPerLevel[ 0 ] ) + for ( final AxisCalibration axis : contents.axes( 0 ) ) { assertEquals( 1.0, axis.scale, "An array without a parent multiscale reads uncalibrated" ); assertEquals( "", axis.unit ); @@ -255,7 +256,7 @@ static void assertScaleAtAxis( final PyramidContents< ? > contents, final String { final int index = contents.axisIndex( axisName ); assertNotEquals( -1, index, "Axis " + axisName + " is missing" ); - assertEquals( expectedScale, contents.axesPerLevel[ 0 ][ index ].scale, axisName + " scale" ); + assertEquals( expectedScale, contents.axes( 0 ).get( index ).scale, axisName + " scale" ); } /** @@ -538,11 +539,11 @@ default void testPhysicalSizeConsistentAcrossResolutionLevels( String resource ) */ static double[] physicalExtents( final PyramidContents< ? > contents, final int level ) { - final AxisCalibration[] axes = contents.axesPerLevel[ level ]; + final List< AxisCalibration > axes = contents.axes( level ); final Img< ? > img = contents.asImg( level ); - final double[] extents = new double[ axes.length ]; - for ( int d = 0; d < axes.length; d++ ) - extents[ d ] = axes[ d ].scale * img.dimension( d ); + final double[] extents = new double[ axes.size() ]; + for ( int d = 0; d < axes.size(); d++ ) + extents[ d ] = axes.get( d ).scale * img.dimension( d ); return extents; } @@ -560,7 +561,7 @@ default void testVoxelCalibration( String resource ) throws URISyntaxException final int index = contents.axisIndex( axisName ); if ( index < 0 ) continue; // axis not present (e.g. no z) - final AxisCalibration axis = contents.axesPerLevel[ 0 ][ index ]; + final AxisCalibration axis = contents.axes( 0 ).get( index ); assertEquals( 1.0, axis.scale ); assertEquals( "", axis.unit ); } From 51b4d06d10e05afed1b3c62a8bcf7441941596ea Mon Sep 17 00:00:00 2001 From: hahmann Date: Thu, 22 Oct 2026 16:25:47 +0200 Subject: [PATCH 5/9] Hide PyramidContents transforms behind transform( level ) (#114) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../main/java/ome/zarr/fiji/PyramidalBdv.java | 3 +- .../ome/zarr/imglib2/PyramidContents.java | 37 ++++++++++++------- .../zarr/imglib2/PyramidBackendTestBase.java | 2 +- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java index 96a34bc0c..9b0de08d5 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java @@ -104,7 +104,8 @@ List< SourceAndConverter< T > > initSourceAndConverters( final PyramidContents< final int nLevels = contents.numResolutionLevels(); final int numChannels = contents.numChannels(); final T type = contents.type(); - final AffineTransform3D[] mipmapTransforms = contents.transforms; + final AffineTransform3D[] mipmapTransforms = new AffineTransform3D[ nLevels ]; + Arrays.setAll( mipmapTransforms, contents::transform ); final VoxelDimensions voxelDimensions = voxelDimensions( contents ); final RandomAccessibleInterval< V >[] volatileImgs = createVolatileImgs( contents ); diff --git a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java index 695d2e90a..54343518e 100644 --- a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java +++ b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java @@ -80,19 +80,7 @@ public final class PyramidContents< T extends NativeType< T > & RealType< T > > private final T type; - /** - * Per-resolution-level transform from that level's image coordinates to the - * shared physical world coordinate system, in resolution-level order (index 0 - * is the highest resolution; aligned with {@link #cachedCellImgs}). - *

- * Source coordinates are the discrete pixel/voxel coordinates of - * resolution level i, in imglib2 F-order (x, y, z). Target coordinates - * are a single physical world space — in the spatial axes' unit (see - * {@link #axes(int)}) — shared by all levels, so that every level overlays - * correctly when displayed together. The transform combines the level's voxel - * spacing (scale) with its origin offset (translation) in that world space. - */ - public final AffineTransform3D[] transforms; + private final AffineTransform3D[] transforms; public final CachedCellImg< T, ? >[] cachedCellImgs; @@ -106,7 +94,9 @@ private PyramidContents( final Builder< T > b ) { this.name = b.name; this.type = b.type; - this.transforms = b.transforms; + this.transforms = new AffineTransform3D[ b.transforms.length ]; + for ( int level = 0; level < transforms.length; level++ ) + transforms[ level ] = b.transforms[ level ].copy(); this.cachedCellImgs = b.cachedCellImgs; this.axesPerLevel = new AxisCalibration[ b.axesPerLevel.length ][]; for ( int level = 0; level < axesPerLevel.length; level++ ) @@ -147,6 +137,25 @@ public boolean hasPlaceholderCalibration() return hasPlaceholderCalibration; } + /** + * Transform from the image coordinates of the given resolution level to the + * shared physical world coordinate system, as a new copy on every call. + *

+ * Source coordinates are the discrete pixel/voxel coordinates of + * the resolution level, in imglib2 F-order (x, y, z). Target coordinates + * are a single physical world space — in the spatial axes' unit (see + * {@link #axes(int)}) — shared by all levels, so that every level overlays + * correctly when displayed together. The transform combines the level's voxel + * spacing (scale) with its origin offset (translation) in that world space. + * + * @throws IndexOutOfBoundsException if {@code resolutionLevel} is not in + * {@code [0, numResolutionLevels())} + */ + public AffineTransform3D transform( final int resolutionLevel ) + { + return transforms[ resolutionLevel ].copy(); + } + /** * Axes of the image at the given resolution level, unmodifiable and indexed * like the image's dimensions (imglib2 F-order). diff --git a/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java b/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java index e6d0f2df8..04650c68e 100644 --- a/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java +++ b/test-shared/java/ome/zarr/imglib2/PyramidBackendTestBase.java @@ -235,7 +235,7 @@ default void testSingleLevelFromParentMultiscale( String resource ) throws URISy assertScaleAtAxis( contents, AxisCalibration.C, 1.0 ); assertScaleAtAxis( contents, AxisCalibration.T, 1.0 ); - AffineTransform3D transform = contents.transforms[ 0 ]; + AffineTransform3D transform = contents.transform( 0 ); assertArrayEquals( new double[] { 2.0, 2.0, 2.0 }, new double[] { transform.get( 0, 0 ), transform.get( 1, 1 ), transform.get( 2, 2 ) }, "The transform is the one of the level that was read, not of level 0" ); From fd4f9268b1e1abda265843977e1a51c888c58b40 Mon Sep 17 00:00:00 2001 From: hahmann Date: Thu, 22 Oct 2026 16:36:08 +0200 Subject: [PATCH 6/9] Keep PyramidContents images behind asImg( level ) (#114) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../main/java/ome/zarr/fiji/PyramidalBdv.java | 4 +- .../java/ome/zarr/fiji/PyramidalDataset.java | 2 +- .../ome/zarr/imglib2/PyramidContents.java | 37 ++++++++++++++----- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java index 9b0de08d5..643f0d3e6 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalBdv.java @@ -112,7 +112,7 @@ List< SourceAndConverter< T > > initSourceAndConverters( final PyramidContents< final V volatileType = volatileImgs[ 0 ].getType(); final RandomAccessibleInterval< T >[][] levelToChannels = new RandomAccessibleInterval[ nLevels ][]; - Arrays.setAll( levelToChannels, level -> splitInputStackIntoSourceStacks( contents, contents.cachedCellImgs[ level ] ) ); + Arrays.setAll( levelToChannels, level -> splitInputStackIntoSourceStacks( contents, contents.asImg( level ) ) ); final RandomAccessibleInterval< V >[][] levelToVolatileChannels = new RandomAccessibleInterval[ nLevels ][]; Arrays.setAll( levelToVolatileChannels, level -> splitInputStackIntoSourceStacks( contents, volatileImgs[ level ] ) ); @@ -181,7 +181,7 @@ RandomAccessibleInterval< V >[] createVolatileImgs( final PyramidContents< T > c final SharedQueue sharedQueue = new SharedQueue( Math.max( 1, Runtime.getRuntime().availableProcessors() / 2 ) ); final RandomAccessibleInterval< V >[] volatileImgs = new RandomAccessibleInterval[ contents.numResolutionLevels() ]; for ( int level = 0; level < contents.numResolutionLevels(); level++ ) - volatileImgs[ level ] = VolatileViews.wrapAsVolatile( contents.cachedCellImgs[ level ], sharedQueue ); + volatileImgs[ level ] = VolatileViews.wrapAsVolatile( contents.asImg( level ), sharedQueue ); return volatileImgs; } diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalDataset.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalDataset.java index 348854cf1..fdd0b04b3 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalDataset.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/PyramidalDataset.java @@ -138,7 +138,7 @@ private static < T extends NativeType< T > & RealType< T > > ImgPlus< T > create } final List< AxisCalibration > selectedAxes = contents.axes( resolutionLevel ); - final ImgPlus< T > imgPlus = new ImgPlus<>( contents.cachedCellImgs[ resolutionLevel ], contents.name() ); + final ImgPlus< T > imgPlus = new ImgPlus<>( contents.asImg( resolutionLevel ), contents.name() ); for ( int i = 0; i < selectedAxes.size(); i++ ) { final AxisType axisType = AXIS_TYPE_MAP.getOrDefault( selectedAxes.get( i ).name, Axes.unknown() ); diff --git a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java index 54343518e..3c54b8bb6 100644 --- a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java +++ b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java @@ -49,15 +49,19 @@ /** * Immutable snapshot of everything a {@link PyramidBackend} produces when - * reading an OME-Zarr multi-resolution image. + * reading an OME-Zarr multi-resolution image, except for pixel data: the + * images are lazily loaded views of the store, so they are shared rather than + * copied, and every call of {@link #asImg(int)} returns the same instance. + * Pixel writes therefore show up for every user of these contents and live in + * memory only, so they may be lost when the cache frees memory. *

- * Indices in {@code cachedCellImgs} and {@code transforms} - * are in resolution-level order (index 0 is the highest resolution). + * Resolution levels are indexed from {@code 0} (the highest resolution) to + * {@link #smallestResolutionLevel()}. * The imglib2 axis indices follow F-order (x, y, z, c, t), the order produced * after the backend has reversed any zarr C-order shapes. *

* The resolution-level count and the per-axis sizes are derived on demand from - * the full-resolution image and its axis list ({@code cachedCellImgs[ 0 ]} and + * the full-resolution image and its axis list ({@code asImg( 0 )} and * {@code axes( 0 )}), rather than stored, so {@link #numDimensions()}, * {@link #numChannels()}, {@link #numTimepoints()} and {@link #axisIndex} cannot * disagree with one another. The constructor enforces the one cross-axis @@ -82,7 +86,7 @@ public final class PyramidContents< T extends NativeType< T > & RealType< T > > private final AffineTransform3D[] transforms; - public final CachedCellImg< T, ? >[] cachedCellImgs; + private final CachedCellImg< T, ? >[] cachedCellImgs; private final AxisCalibration[][] axesPerLevel; @@ -97,7 +101,7 @@ private PyramidContents( final Builder< T > b ) this.transforms = new AffineTransform3D[ b.transforms.length ]; for ( int level = 0; level < transforms.length; level++ ) transforms[ level ] = b.transforms[ level ].copy(); - this.cachedCellImgs = b.cachedCellImgs; + this.cachedCellImgs = b.cachedCellImgs.clone(); this.axesPerLevel = new AxisCalibration[ b.axesPerLevel.length ][]; for ( int level = 0; level < axesPerLevel.length; level++ ) axesPerLevel[ level ] = b.axesPerLevel[ level ].clone(); @@ -175,8 +179,7 @@ public Omero omero() } /** - * Number of resolution levels in the pyramid, i.e. the length of - * {@link #cachedCellImgs}. + * Number of resolution levels in the pyramid. */ public int numResolutionLevels() { @@ -199,6 +202,10 @@ public int smallestResolutionLevel() * The dimensions are in imglib2 F-order — a subset of (x, y, z, c, t) — so use * {@link #axisIndex} / {@link #hasAxis} together with {@link #axes(int)} to * map dimension indices to logical axes. + *

+ * The image is shared, not copied: pixel writes show up for every user of + * these contents and live in memory only, so they may be lost when the cache + * frees memory. */ public Img< T > asImg() { @@ -208,6 +215,10 @@ public Img< T > asImg() /** * Largest image of the pyramid, i.e., the full-resolution level {@code 0}. See * {@link #asImg()} for the axis ordering. + *

+ * The image is shared, not copied: pixel writes show up for every user of + * these contents and live in memory only, so they may be lost when the cache + * frees memory. */ public Img< T > asLargestImg() { @@ -219,6 +230,10 @@ public Img< T > asLargestImg() * {@link #smallestResolutionLevel()}. For a single-level pyramid this is the * same image as {@link #asLargestImg()}. See {@link #asImg()} for the axis * ordering. + *

+ * The image is shared, not copied: pixel writes show up for every user of + * these contents and live in memory only, so they may be lost when the cache + * frees memory. */ public Img< T > asSmallestImg() { @@ -229,6 +244,10 @@ public Img< T > asSmallestImg() * Image at the given resolution level ({@code 0} = highest resolution). See * {@link #asImg()} for the axis ordering; {@link #suggestResolutionLevel} can * pick a level by preferred width. + *

+ * The image is shared, not copied: pixel writes show up for every user of + * these contents and live in memory only, so they may be lost when the cache + * frees memory. * * @throws IndexOutOfBoundsException if {@code resolutionLevel} is not in * {@code [0, numResolutionLevels())} @@ -304,7 +323,7 @@ public String[] channelLabels() * may be absent, so an axis' index shifts with the set of axes actually * present (in F-order, {@code c} is at index 2 in {@code xyct} but at index 3 * in {@code xyzc}). The lookup goes through {@link #axes(int)}, which is - * aligned 1:1 with the dimensions of {@code cachedCellImgs[ 0 ]}. + * aligned 1:1 with the dimensions of {@code asImg( 0 )}. */ public int axisIndex( final String axisName ) { From 85e884381a3f50f977c1a77a40d9ec287b87ae3c Mon Sep 17 00:00:00 2001 From: hahmann Date: Thu, 22 Oct 2026 16:37:46 +0200 Subject: [PATCH 7/9] Let asImg() delegate to asLargestImg() Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/main/java/ome/zarr/imglib2/PyramidContents.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java index 3c54b8bb6..b5f8b1c69 100644 --- a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java +++ b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java @@ -209,7 +209,7 @@ public int smallestResolutionLevel() */ public Img< T > asImg() { - return asImg( 0 ); + return asLargestImg(); } /** From a0d327b4791b129301ac7077d767e0d69226a36f Mon Sep 17 00:00:00 2001 From: hahmann Date: Thu, 22 Oct 2026 16:43:21 +0200 Subject: [PATCH 8/9] Check that PyramidContents level counts match (#114) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/main/java/ome/zarr/imglib2/PyramidContents.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java index b5f8b1c69..4838c771e 100644 --- a/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java +++ b/ome-zarr-imglib2/src/main/java/ome/zarr/imglib2/PyramidContents.java @@ -66,7 +66,8 @@ * {@link #numChannels()}, {@link #numTimepoints()} and {@link #axisIndex} cannot * disagree with one another. The constructor enforces the one cross-axis * invariant they all rely on: the axis list and the image have the same number - * of dimensions. + * of dimensions. It also checks that images, transforms and axis lists cover the + * same number of resolution levels. * * @param pixel type */ @@ -108,6 +109,10 @@ private PyramidContents( final Builder< T > b ) this.omero = b.omero; this.hasPlaceholderCalibration = b.hasPlaceholderCalibration; + if ( transforms.length != cachedCellImgs.length || axesPerLevel.length != cachedCellImgs.length ) + throw new IllegalArgumentException( "Level counts differ: " + cachedCellImgs.length + " images, " + + transforms.length + " transforms, " + axesPerLevel.length + " axis lists." ); + final int numDimensions = cachedCellImgs[ 0 ].numDimensions(); if ( axesPerLevel[ 0 ].length != numDimensions ) throw new IllegalArgumentException( "Full-resolution axis count (" + axesPerLevel[ 0 ].length From 0caf669d7789ef6587fec62a7e6bb00703c8da72 Mon Sep 17 00:00:00 2001 From: hahmann Date: Thu, 22 Oct 2026 16:46:20 +0200 Subject: [PATCH 9/9] Document the PyramidContents accessors (#114) Co-Authored-By: Claude Opus 5.5 (1M context) --- CLAUDE.md | 9 +++++---- doc/DEVELOPERS.md | 8 +++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index aaf1bb47c..223c464bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -185,10 +185,11 @@ group first, then the array's own `dimension_names`) – and leaves three `prote reader-specific steps: `readMultiscale`, `tryReadLevelFromParent` and `tryReadArrayNodeOnly` (the two `try*` hooks return `null` for "not applicable, try the next"; the last one throws `NotAMultiscaleImageException` when there is no array at all, so a missing location is never reported as unknown axes). `read` and `readSingleArray` are `final`, so -the order is fixed for every backend. Both `PyramidContents` and the per-level `CachedCellImg`s, transforms, calibration -and optional OMERO metadata it holds are immutable. Each backend also exposes a static `readPyramid(URI)` convenience -entry point for outside API users — not named `read`, because Java forbids a static method hiding an inherited instance -method. +the order is fixed for every backend. `PyramidContents` is immutable except for pixel data (issue #114): accessors hand +out copies or unmodifiable views, but the lazily loaded `CachedCellImg`s are shared, since copying one would read the +whole dataset — this matters because `OmeZarr` caches one instance for every opener, third-party ones included. Each +backend also exposes a static `readPyramid(URI)` convenience entry point for outside API users — not named `read`, +because Java forbids a static method hiding an inherited instance method. **A too-old reader library is reported, not thrown at the console.** A Fiji-Stable installation whose N5 stack predates this plugin fails inside the backend with a `NoClassDefFoundError` (e.g. on `OmeNgffMetadataParser`), which is useless diff --git a/doc/DEVELOPERS.md b/doc/DEVELOPERS.md index 9c2943e2f..8feafd222 100644 --- a/doc/DEVELOPERS.md +++ b/doc/DEVELOPERS.md @@ -95,12 +95,14 @@ public interface PyramidBackend } ``` -`PyramidContents` is what you get back, and it is immutable — public final fields, no setters: +`PyramidContents` is what you get back, and it is immutable — accessors only, no setters. The one exception is +pixel data: every `asImg(…)` call returns the same lazily loaded image, so pixel writes show up for every user and +live in memory only — they may be lost when the cache frees memory. | | | |------------------------|----------------------------------------------------------------------------------------------------------------------------------------| -| **Pixel data** | `cachedCellImgs[]` (one per resolution level), `type`, `name` | -| **Visualisation data** | `transforms[]`, `axesPerLevel[][]`, `omero`, `hasPlaceholderCalibration` | +| **Pixel data** | `asImg(int)` (one per resolution level) · `type()` · `name()` | +| **Visualisation data** | `transform(int)` · `axes(int)` · `omero()` · `hasPlaceholderCalibration()` | | **Levels** | `asImg()` (= level 0) · `asLargestImg()` · `asSmallestImg()` · `asImg(int)` · `numResolutionLevels()` · `smallestResolutionLevel()` | | **Level selection** | `suggestResolutionLevel(Integer preferredMaxWidth)` | | **Shape by axis name** | `hasAxis("z")` · `axisIndex("c")` · `sizeAlongAxis("t")` · `numDimensions()` · `numChannels()` · `numTimepoints()` · `channelLabels()` |