This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 297e7a67fe Refactor `WritableRenderedImage`support in `BandedSampleConverter` for sharing more code with other writable images. Refactor `BandAggregateImage` by moving its inner helper class outside, and add `WritableRenderedImage`support. `BandAggregateImage` is no longer an "all or nothing" implementation: can have a mix of shared and copied arrays. 297e7a67fe is described below commit 297e7a67feb23ed45d356270e455139ff15d9f17 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Apr 5 09:53:38 2023 +0200 Refactor `WritableRenderedImage`support in `BandedSampleConverter` for sharing more code with other writable images. Refactor `BandAggregateImage` by moving its inner helper class outside, and add `WritableRenderedImage`support. `BandAggregateImage` is no longer an "all or nothing" implementation: can have a mix of shared and copied arrays. --- .../org/apache/sis/image/BandAggregateImage.java | 354 ++++++------------- .../org/apache/sis/image/BandSharedRaster.java | 181 ++++++++++ .../java/org/apache/sis/image/BandSharing.java | 382 +++++++++++++++++++++ .../apache/sis/image/BandedSampleConverter.java | 83 +---- .../org/apache/sis/image/CombinedImageLayout.java | 49 +-- .../java/org/apache/sis/image/ComputedImage.java | 35 +- .../java/org/apache/sis/image/ImageProcessor.java | 5 + .../apache/sis/image/WritableComputedImage.java | 177 ++++++++++ .../apache/sis/image/BandAggregateImageTest.java | 222 ++++++++---- 9 files changed, 1048 insertions(+), 440 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java index e1e437b86f..46c0a540cf 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java @@ -20,18 +20,11 @@ import java.util.Arrays; import java.util.Objects; import java.awt.Rectangle; import java.awt.image.ColorModel; -import java.awt.image.SampleModel; -import java.awt.image.ComponentSampleModel; -import java.awt.image.DataBuffer; -import java.awt.image.DataBufferByte; -import java.awt.image.DataBufferShort; -import java.awt.image.DataBufferUShort; -import java.awt.image.DataBufferInt; -import java.awt.image.DataBufferFloat; -import java.awt.image.DataBufferDouble; +import java.awt.image.BandedSampleModel; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.WritableRaster; +import java.awt.image.WritableRenderedImage; import org.apache.sis.util.ArraysExt; import org.apache.sis.internal.coverage.j2d.ImageUtilities; @@ -50,12 +43,12 @@ import org.apache.sis.internal.coverage.j2d.ImageUtilities; * * @since 1.4 */ -final class BandAggregateImage extends ComputedImage { +class BandAggregateImage extends WritableComputedImage { /** * The source images with only the bands to aggregate, in order. * Those images are views; the band sample values are not copied. */ - private final RenderedImage[] filteredSources; + protected final RenderedImage[] filteredSources; /** * Color model of the aggregated image. @@ -79,8 +72,11 @@ final class BandAggregateImage extends ComputedImage { private final int minTileX, minTileY; /** - * Whether all sources have tiles at the same locations and use the same scanline stride. - * In such case, it is possible to share references to data arrays without copying them. + * Whether the sharing of data arrays is allowed. + * When a source tile has the same bounds and scanline stride than the target tile, + * it is possible to share references to data arrays without copying the pixels. + * This sharing is decided automatically on a source-by-source basis. + * This flag allows to disable completely the sharing for all sources. */ private final boolean allowSharing; @@ -99,7 +95,12 @@ final class BandAggregateImage extends ComputedImage { final Colorizer colorizer, final boolean allowSharing) { final var layout = CombinedImageLayout.create(sources, bandsPerSource, allowSharing); - final var image = new BandAggregateImage(layout, colorizer); + final BandAggregateImage image; + if (layout.isWritable()) { + image = new Writable(layout, colorizer, allowSharing); + } else { + image = new BandAggregateImage(layout, colorizer, allowSharing); + } if (image.filteredSources.length == 1) { final RenderedImage c = image.filteredSources[0]; if (image.colorModel == null) { @@ -119,8 +120,9 @@ final class BandAggregateImage extends ComputedImage { * @param layout pixel and tile coordinate spaces of this image, together with sample model. * @param colorizer provider of color model to use for this image, or {@code null} for automatic. */ - private BandAggregateImage(final CombinedImageLayout layout, final Colorizer colorizer) { + BandAggregateImage(final CombinedImageLayout layout, final Colorizer colorizer, final boolean allowSharing) { super(layout.sampleModel, layout.sources); + this.allowSharing = allowSharing; final Rectangle r = layout.domain; minX = r.x; minY = r.y; @@ -128,8 +130,7 @@ final class BandAggregateImage extends ComputedImage { height = r.height; minTileX = layout.minTileX; minTileY = layout.minTileY; - allowSharing = layout.allowSharing; - filteredSources = layout.getFilteredSources(); + filteredSources = layout.filteredSources; colorModel = layout.createColorModel(colorizer); ensureCompatible(colorModel); } @@ -152,19 +153,21 @@ final class BandAggregateImage extends ComputedImage { */ @Override protected Raster computeTile(final int tileX, final int tileY, WritableRaster tile) { + if (tile instanceof BandSharedRaster) { + tile = null; // Do not take the risk of writing in source images. + } /* * If we are allowed to share the data arrays, try that first. + * The cast to `BandedSampleModel` is safe because this is the + * type given by `CombinedImageLayout` in the constructor. */ + BandSharedRaster shared = null; if (allowSharing) { - final Sharing sharing = Sharing.create(sampleModel.getDataType(), sampleModel.getNumBands()); + final BandSharing sharing = BandSharing.create((BandedSampleModel) sampleModel); if (sharing != null) { - final DataBuffer buffer = sharing.createDataBuffer( - Math.multiplyFull(tileX - minTileX, getTileWidth()) + minX, - Math.multiplyFull(tileY - minTileY, getTileHeight()) + minY, - filteredSources); - if (buffer != null) { - return Raster.createRaster(sampleModel, buffer, computeTileLocation(tileX, tileY)); - } + final long x = Math.multiplyFull(tileX - minTileX, getTileWidth()) + minX; + final long y = Math.multiplyFull(tileY - minTileY, getTileHeight()) + minY; + tile = shared = sharing.createRaster(x, y, filteredSources); } } /* @@ -175,263 +178,116 @@ final class BandAggregateImage extends ComputedImage { tile = createTile(tileX, tileY); } int band = 0; - for (final RenderedImage source : filteredSources) { - final Rectangle aoi = tile.getBounds(); - ImageUtilities.clipBounds(source, aoi); + for (int i=0; i < filteredSources.length; i++) { + final RenderedImage source = filteredSources[i]; final int numBands = ImageUtilities.getNumBands(source); - final int[] bands = ArraysExt.range(band, band + numBands); - var target = tile.createWritableChild(aoi.x, aoi.y, aoi.width, aoi.height, - aoi.x, aoi.y, bands); + if (shared == null || shared.needCopy(i)) { + final Rectangle aoi = tile.getBounds(); + ImageUtilities.clipBounds(source, aoi); + if (!aoi.isEmpty()) { + final int[] bands = ArraysExt.range(band, band + numBands); + var target = tile.createWritableChild(aoi.x, aoi.y, aoi.width, aoi.height, + aoi.x, aoi.y, bands); + copyData(aoi, source, target); + } + } band += numBands; - copyData(aoi, source, target); } return tile; } /** - * A builder of data buffers sharing arrays of source images. - * There is a subclass for each supported data type. + * A {@code BandAggregateImage} where all sources are writable rendered images. */ - private abstract static class Sharing { + private static final class Writable extends BandAggregateImage implements WritableRenderedImage { /** - * The offsets of the first valid element into each bank array. - * Will be computed with the assumption that all offsets are zero - * in the target {@link java.awt.image.BandedSampleModel}. + * Creates a new writable rendered image. + * + * @param layout pixel and tile coordinate spaces of this image, together with sample model. + * @param colorizer provider of color model to use for this image, or {@code null} for automatic. */ - protected final int[] offsets; + Writable(final CombinedImageLayout layout, final Colorizer colorizer, final boolean allowSharing) { + super(layout, colorizer, allowSharing); + } /** - * For subclass constructors. + * Checks out a tile for writing. */ - protected Sharing(final int numBands) { - offsets = new int[numBands]; + @Override + public WritableRaster getWritableTile(final int tileX, final int tileY) { + final WritableRaster tile = (WritableRaster) getTile(tileX, tileY); + if (tile instanceof BandSharedRaster) { + ((BandSharedRaster) tile).acquireWritableTiles(filteredSources); + } + try { + markTileWritable(tileX, tileY, true); + } catch (RuntimeException e) { + if (tile instanceof BandSharedRaster) { + ((BandSharedRaster) tile).releaseWritableTiles(e); + } + throw e; + } + return tile; } /** - * Creates a new builder. - * - * @param dataType the data type as one of {@link DataBuffer} constants. - * @param numBands number of banks of the data buffer to create. - * @return the data buffer, or {@code null} if the dat type is not recognized. + * Relinquishes the right to write to a tile. */ - static Sharing create(final int dataType, final int numBands) { - switch (dataType) { - case DataBuffer.TYPE_BYTE: return new Bytes (numBands); - case DataBuffer.TYPE_SHORT: return new Shorts (numBands); - case DataBuffer.TYPE_USHORT: return new UShorts (numBands); - case DataBuffer.TYPE_INT: return new Integers(numBands); - case DataBuffer.TYPE_FLOAT: return new Floats (numBands); - case DataBuffer.TYPE_DOUBLE: return new Doubles (numBands); + @Override + public void releaseWritableTile(final int tileX, final int tileY) { + if (markTileWritable(tileX, tileY, false)) { + final Raster tile = getTile(tileX, tileY); + if (tile instanceof BandSharedRaster) { + ((BandSharedRaster) tile).releaseWritableTiles(null); + } + setData(tile); } - return null; } /** - * Creates a data buffer sharing the arrays of all given sources, in order. - * This method assumes a target {@link java.awt.image.BandedSampleModel} where - * all band offsets are zero and where bank indices define an identity mapping. + * Sets a region of the image to the contents of the given raster. + * The raster is assumed to be in the same coordinate space as this image. + * The operation is clipped to the bounds of this image. * - * @param x <var>x</var> pixel coordinate of the tile. - * @param y <var>y</var> pixel coordinate of the tile. - * @param sources the sources for which to aggregate all bands. - * @return a data buffer containing the aggregation of all bands, or {@code null} if it can not be created. + * @param tile the values to write in this image. */ - final DataBuffer createDataBuffer(final long x, final long y, final RenderedImage[] sources) { + @Override + public void setData(final Raster tile) { + final BandSharedRaster shared = (tile instanceof BandSharedRaster) ? (BandSharedRaster) tile : null; int band = 0; - int size = Integer.MAX_VALUE; - for (final RenderedImage source : sources) { - final int tileWidth = source.getTileWidth(); - final int tileHeight = source.getTileHeight(); - long tileX = x - source.getTileGridXOffset(); - long tileY = y - source.getTileGridYOffset(); - if (((tileX % tileWidth) | (tileY % tileHeight)) != 0) { - return null; // Source tile not aligned on target tile. - } - tileX /= tileWidth; - tileY /= tileHeight; - final Raster raster = source.getTile(Math.toIntExact(tileX), Math.toIntExact(tileY)); - final SampleModel c = raster.getSampleModel(); - if (!(c instanceof ComponentSampleModel)) { - return null; // Should never happen if `BandAggregateImage.allowSharing` is true. + for (int i=0; i < filteredSources.length; i++) { + final var target = (WritableRenderedImage) filteredSources[i]; + final int numBands = ImageUtilities.getNumBands(target); + if (shared == null || shared.needCopy(i)) { + final Rectangle aoi = tile.getBounds(); + ImageUtilities.clipBounds(target, aoi); + if (!aoi.isEmpty()) { + final int[] bands = ArraysExt.range(band, band + numBands); + var source = tile.createChild(aoi.x, aoi.y, aoi.width, aoi.height, + aoi.x, aoi.y, bands); + target.setData(source); + } } - final var sm = (ComponentSampleModel) c; - final var buffer = raster.getDataBuffer(); - final int[] offsets1 = buffer.getOffsets(); - final int[] offsets2 = sm.getBandOffsets(); - final int[] indices = sm.getBankIndices(); - for (int i=0; i<indices.length; i++) { - final int b = indices[i]; - takeReference(buffer, b, band); - offsets[band] = offsets1[b] + offsets2[i]; // Assume zero offset in target `BandedSampleModel`. - band++; - } - size = Math.min(size, buffer.getSize()); + band += numBands; } - final DataBuffer buffer = build(size); - assert buffer.getNumBanks() == band; - return buffer; } /** - * Takes a reference to an array in the given data buffer. - * - * @param source the data buffer from which to take a reference to an array. - * @param src bank index of the reference to take. - * @param dst band index where to store the reference. + * Restores the identity behavior for writable image, + * because it may have listeners attached to this specific instance. */ - abstract void takeReference(DataBuffer source, int src, int dst); + @Override + public int hashCode() { + return System.identityHashCode(this); + } /** - * Builds the data buffer after all references have been taken. - * The data buffer shall specify {@link #offsets} to the buffer constructor. - * - * @param size number of elements in the data buffer. - * @return the new data buffer. + * Restores the identity behavior for writable image, + * because it may have listeners attached to this specific instance. */ - abstract DataBuffer build(int size); - } - - /** - * A builder of data buffer of {@link DataBuffer#TYPE_BYTE}. - */ - private static final class Bytes extends Sharing { - /** The shared arrays. */ - private final byte[][] data; - - /** Creates a new builder. */ - Bytes(final int numBands) { - super(numBands); - data = new byte[numBands][]; - } - - /** Takes a reference to an array in the given data buffer. */ - @Override void takeReference(DataBuffer buffer, int src, int dst) { - data[dst] = ((DataBufferByte) buffer).getData(src); - } - - /** Builds the data buffer after all references have been taken. */ - @Override DataBuffer build(int size) { - return new DataBufferByte(data, size, offsets); - } - } - - /** - * A builder of data buffer of {@link DataBuffer#TYPE_SHORT}. - */ - private static final class Shorts extends Sharing { - /** The shared arrays. */ - private final short[][] data; - - /** Creates a new builder. */ - Shorts(final int numBands) { - super(numBands); - data = new short[numBands][]; - } - - /** Takes a reference to an array in the given data buffer. */ - @Override void takeReference(DataBuffer buffer, int src, int dst) { - data[dst] = ((DataBufferShort) buffer).getData(src); - } - - /** Builds the data buffer after all references have been taken. */ - @Override DataBuffer build(int size) { - return new DataBufferShort(data, size, offsets); - } - } - - /** - * A builder of data buffer of {@link DataBuffer#TYPE_USHORT}. - */ - private static final class UShorts extends Sharing { - /** The shared arrays. */ - private final short[][] data; - - /** Creates a new builder. */ - UShorts(final int numBands) { - super(numBands); - data = new short[numBands][]; - } - - /** Takes a reference to an array in the given data buffer. */ - @Override void takeReference(DataBuffer buffer, int src, int dst) { - data[dst] = ((DataBufferUShort) buffer).getData(src); - } - - /** Builds the data buffer after all references have been taken. */ - @Override DataBuffer build(int size) { - return new DataBufferUShort(data, size, offsets); - } - } - - /** - * A builder of data buffer of {@link DataBuffer#TYPE_INT}. - */ - private static final class Integers extends Sharing { - /** The shared arrays. */ - private final int[][] data; - - /** Creates a new builder. */ - Integers(final int numBands) { - super(numBands); - data = new int[numBands][]; - } - - /** Takes a reference to an array in the given data buffer. */ - @Override void takeReference(DataBuffer buffer, int src, int dst) { - data[dst] = ((DataBufferInt) buffer).getData(src); - } - - /** Builds the data buffer after all references have been taken. */ - @Override DataBuffer build(int size) { - return new DataBufferInt(data, size, offsets); - } - } - - /** - * A builder of data buffer of {@link DataBuffer#TYPE_FLOAT}. - */ - private static final class Floats extends Sharing { - /** The shared arrays. */ - private final float[][] data; - - /** Creates a new builder. */ - Floats(final int numBands) { - super(numBands); - data = new float[numBands][]; - } - - /** Takes a reference to an array in the given data buffer. */ - @Override void takeReference(DataBuffer buffer, int src, int dst) { - data[dst] = ((DataBufferFloat) buffer).getData(src); - } - - /** Builds the data buffer after all references have been taken. */ - @Override DataBuffer build(int size) { - return new DataBufferFloat(data, size, offsets); - } - } - - /** - * A builder of data buffer of {@link DataBuffer#TYPE_DOUBLE}. - */ - private static final class Doubles extends Sharing { - /** The shared arrays. */ - private final double[][] data; - - /** Creates a new builder. */ - Doubles(final int numBands) { - super(numBands); - data = new double[numBands][]; - } - - /** Takes a reference to an array in the given data buffer. */ - @Override void takeReference(DataBuffer buffer, int src, int dst) { - data[dst] = ((DataBufferDouble) buffer).getData(src); - } - - /** Builds the data buffer after all references have been taken. */ - @Override DataBuffer build(int size) { - return new DataBufferDouble(data, size, offsets); + @Override + public boolean equals(final Object object) { + return object == this; } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandSharedRaster.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandSharedRaster.java new file mode 100644 index 0000000000..cb477b2084 --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandSharedRaster.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.image; + +import java.awt.Point; +import java.awt.image.Raster; +import java.awt.image.DataBuffer; +import java.awt.image.SampleModel; +import java.awt.image.BandedSampleModel; +import java.awt.image.RenderedImage; +import java.awt.image.WritableRaster; +import java.awt.image.WritableRenderedImage; +import java.awt.image.RasterFormatException; + + +/** + * A raster where some or all bands are shared with other rasters. + * This implementation is restricted to {@link BandedSampleModel}. + * + * <h2>Performance note</h2> + * The standard Java library has many specialized implementations for different sample models. + * By using our own implementation, we block ourselves from using those specialized subclasses. + * However as of OpenJDK 19, all those specialized subclasses are for sample models other than + * {@link BandedSampleModel}. Consequently we do not expect a big difference in this case. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * @since 1.4 + */ +final class BandSharedRaster extends WritableRaster { + /** + * The sources of this raster for which bands are shared. + * A null element means that the pixels of the corresponding source needs to be copied. + * The non-null elements are not directly used but kept for avoiding garbage collection. + * Because this {@code BandSharedRaster} keep references to {@code sources} data arrays, + * garbage collection of source {@link Raster} instances will not free a lot of memory. + * Quite the opposite, it would consume more memory if a source raster needs to be recomputed. + */ + private final Raster[] parents; + + /** + * Sources for which a writable raster has been acquired. + * The length is always the total number of sources, but elements in this array may be null. + * Non-null elements exist only if this tile has been acquired for write operations by a call + * to {@link WritableRenderedImage#getWritableTile(int, int)}. + */ + private final WritableRenderedImage[] writableSources; + + /** + * Indices of tiles in source images. + * Values at even indices are <var>x</var> tile coordinates and + * values at odd indices are <var>y</var> tile coordinates. + * Values may be invalid when the corresponding {@code parents} element is null. + */ + private final int[] sourceTileIndices; + + /** + * Creates a new raster. + * + * @param srcCount total number of source images. + * @param parents the sources of this raster for which bands are shared. + * @param model the sample model that specifies the layout. + * @param buffer the buffer that contains the image data. + * @param location the coordinate of upper-left corner. + */ + BandSharedRaster(final int[] sourceTileIndices, final Raster[] parents, + final SampleModel model, final DataBuffer buffer, final Point location) + { + super(model, buffer, location); + writableSources = new WritableRenderedImage[sourceTileIndices.length >>> 1]; + this.sourceTileIndices = sourceTileIndices; + this.parents = parents; + int numBands = 0; + for (final Raster source : parents) { + if (source != null) { + final int n = source.getNumBands(); + if (n > numBands) { + numBands = n; + parent = source; + } + } + } + } + + /** + * Returns {@code true} if pixel values for the given source index needs to be copied. + * It may happen because {@code BandSharedRaster} does not necessarily share the data arrays + * of all sources. We may have a mix of shared sources and sources that need to be copied. + * + * @param i index of a source image. + * @return whether pixel values for the specified source needs to be copied. + */ + final boolean needCopy(final int i) { + return parents[i] == null; + } + + /** + * Notifies all shared sources that the tile is about to be written. + * + * @param sources all sources of the band aggregate image. + */ + final synchronized void acquireWritableTiles(final RenderedImage[] sources) { + final var pending = new WritableRenderedImage[sources.length]; + try { + for (int i=0; i < sources.length; i++) { + final Raster parent = parents[i]; + if (parent != null && writableSources[i] == null) { + final int n = i << 1; + final WritableRenderedImage target = (WritableRenderedImage) sources[i]; + final WritableRaster tile = target.getWritableTile(sourceTileIndices[n], sourceTileIndices[n+1]); + pending[i] = target; + if (parent != tile) { // Quick test for the most common case. + if (parent.getDataBuffer() != tile.getDataBuffer() || + !parent.getSampleModel().equals(tile.getSampleModel())) + { + throw new RasterFormatException("DataBuffer replacement not yet supported."); + } + } + } + } + } catch (RuntimeException error) { + releaseWritableTiles(pending, error); // Rollback the tile acquisitions. + } + /* + * Save the writable status only after we know that the operation is successful. + * We want a "all or nothing" behavior: after we acquired all tiles and the method + * returns successfully, or we acquired none of them and the method throws an exception. + */ + for (int i=0; i < pending.length; i++) { + final WritableRenderedImage target = pending[i]; + if (target != null) writableSources[i] = target; + } + } + + /** + * Release all tiles which were acquired for write operations. + * + * @param error the exception to throw after this method completed, or {@code null} if none. + */ + final synchronized void releaseWritableTiles(RuntimeException error) { + releaseWritableTiles(writableSources, error); + } + + /** + * Release all non-null tiles in the specified array. + * Released tiles are set to null. + * + * @param sources the band aggregate image sources for which to release writable tiles. + * @param error the exception to throw after this method completed, or {@code null} if none. + */ + private void releaseWritableTiles(final WritableRenderedImage[] sources, RuntimeException error) { + for (int i=0; i < sources.length; i++) { + final WritableRenderedImage source = sources[i]; + if (source != null) try { + sources[i] = null; + final int n = i << 1; + source.releaseWritableTile(sourceTileIndices[n], sourceTileIndices[n+1]); + } catch (RuntimeException e) { + if (error == null) error = e; + else error.addSuppressed(e); + } + } + if (error != null) { + throw error; + } + } +} diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandSharing.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandSharing.java new file mode 100644 index 0000000000..29ffb0fba5 --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandSharing.java @@ -0,0 +1,382 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.image; + +import java.awt.Point; +import java.awt.image.SampleModel; +import java.awt.image.BandedSampleModel; +import java.awt.image.ComponentSampleModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferShort; +import java.awt.image.DataBufferUShort; +import java.awt.image.DataBufferInt; +import java.awt.image.DataBufferFloat; +import java.awt.image.DataBufferDouble; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import org.apache.sis.internal.coverage.j2d.ImageUtilities; + + +/** + * A builder of data buffers sharing arrays of source images. + * There is a subclass for each supported data type. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * @since 1.4 + */ +abstract class BandSharing { + /** + * The offsets of the first valid element into each bank array. + * Will be computed with the assumption that all offsets are zeros + * in the target {@link BandedSampleModel}. + */ + protected final int[] offsets; + + /** + * The sample model of the raster to create. + * All band offsets shall be zeros and bank indices shall define an identity mapping. + */ + private final BandedSampleModel target; + + /** + * The sources of the tile for which bands can be shared. + * The length of this array is the number of source images. + * Some elements may be {@code null} if we cannot share data arrays + * of the corresponding source and instead need to copy pixel values. + * + * @see BandSharedRaster#parents + */ + private Raster[] parents; + + /** + * Indices of tiles in source images. + * Values at even indices are <var>x</var> tile coordinates and + * values at odd indices are <var>y</var> tile coordinates. + * Values may be invalid when the corresponding {@code parents} element is null. + */ + private int[] sourceTileIndices; + + /** + * For subclass constructors. + */ + protected BandSharing(final BandedSampleModel target) { + this.target = target; + offsets = new int[target.getNumBands()]; + } + + /** + * Creates a new builder. + * + * @param target the sample model of the tile to create. + * @return the data buffer, or {@code null} if the data type is not recognized. + */ + static BandSharing create(final BandedSampleModel target) { + switch (target.getDataType()) { + case DataBuffer.TYPE_BYTE: return new Bytes (target); + case DataBuffer.TYPE_SHORT: return new Shorts (target); + case DataBuffer.TYPE_USHORT: return new UShorts (target); + case DataBuffer.TYPE_INT: return new Integers(target); + case DataBuffer.TYPE_FLOAT: return new Floats (target); + case DataBuffer.TYPE_DOUBLE: return new Doubles (target); + } + return null; + } + + /** + * Prepares sharing the arrays of the given sources when possible. + * This method does not allocate new {@link DataBuffer} banks. + * + * @param x <var>x</var> pixel coordinate of the tile. + * @param y <var>y</var> pixel coordinate of the tile. + * @param sources the sources for which to aggregate all bands. + * @return data buffer size, or 0 if there is nothing to share. + */ + private int prepare(final long x, final long y, final RenderedImage[] sources) { + final int tileWidth = target.getWidth(); + final int tileHeight = target.getHeight(); + final int scanlineStride = target.getScanlineStride(); + int size = scanlineStride * tileHeight; // Size of the data buffer to create. + int band = 0; // Band of the target image. + boolean sharing = false; + parents = new Raster[sources.length]; + sourceTileIndices = new int[sources.length * 2]; + for (int si=0; si < sources.length; si++) { + final RenderedImage source = sources[si]; + if (source.getTileWidth() == tileWidth && + source.getTileHeight() == tileHeight) + { + long tileX = x - source.getTileGridXOffset(); + long tileY = y - source.getTileGridYOffset(); + if (((tileX % tileWidth) | (tileY % tileHeight)) == 0) { + tileX /= tileWidth; + tileY /= tileHeight; + final int tx = Math.toIntExact(tileX); + final int ty = Math.toIntExact(tileY); + final int n = si << 1; + sourceTileIndices[n ] = tx; + sourceTileIndices[n+1] = ty; + final Raster raster = source.getTile(tx, ty); + final SampleModel c = raster.getSampleModel(); + if (c instanceof ComponentSampleModel) { + final var sm = (ComponentSampleModel) c; + if (sm.getPixelStride() == 1 && + sm.getScanlineStride() == scanlineStride) + { + final var buffer = raster.getDataBuffer(); + final int[] offsets1 = buffer.getOffsets(); + final int[] offsets2 = sm.getBandOffsets(); + final int[] indices = sm.getBankIndices(); + for (int i=0; i < indices.length; i++) { + final int b = indices[i]; + takeReference(buffer, b, band); + offsets[band] = offsets1[b] + offsets2[i]; // Assume zero offset in target `BandedSampleModel`. + band++; + } + size = Math.max(size, buffer.getSize()); + parents[si] = raster; + sharing = true; + continue; + } + } + } + } + /* + * If we reach this point, it was not possible to share the data arrays of a source. + * We will need to copy the pixels. New arrays will be allocated for holding the copy. + */ + band += ImageUtilities.getNumBands(source); + } + if (band != offsets.length) { // No `assert` keyword because it is okay to let this check be unconditional. + throw new AssertionError(); + } + return sharing ? size : 0; + } + + /** + * Creates a raster sharing the arrays of given sources when possible. + * This method assumes a target {@link BandedSampleModel} where all band offsets. + * + * @param x <var>x</var> pixel coordinate of the tile. + * @param y <var>y</var> pixel coordinate of the tile. + * @param sources the sources for which to aggregate all bands. + * @return a raster containing the aggregation of all bands, or {@code null} if the is nothing to share. + */ + final BandSharedRaster createRaster(final long x, final long y, final RenderedImage[] sources) { + final int size = prepare(x, y, sources); + if (size == 0) { + return null; + } + final DataBuffer buffer = allocate(size); + final var location = new Point(Math.toIntExact(x), Math.toIntExact(y)); + return new BandSharedRaster(sourceTileIndices, parents, target, buffer, location); + } + + /** + * Takes a reference to an array in the given data buffer. + * + * @param source the data buffer from which to take a reference to an array. + * @param src bank index of the reference to take. + * @param dst band index where to store the reference. + */ + abstract void takeReference(DataBuffer source, int src, int dst); + + /** + * Allocates banks for all bands that are not shared, then builds the data buffer. + * Subclasses shall specify the {@link #offsets} array to the buffer constructor. + * + * @param size number of elements in the data buffer. + * @return the new data buffer. + */ + abstract DataBuffer allocate(int size); + + + /** + * A builder of data buffer of {@link DataBuffer#TYPE_BYTE}. + */ + private static final class Bytes extends BandSharing { + /** The shared arrays. */ + private final byte[][] data; + + /** Creates a new builder. */ + Bytes(final BandedSampleModel target) { + super(target); + data = new byte[offsets.length][]; + } + + /** Takes a reference to an array in the given data buffer. */ + @Override void takeReference(DataBuffer buffer, int src, int dst) { + data[dst] = ((DataBufferByte) buffer).getData(src); + } + + /** Builds the data buffer after all references have been taken. */ + @Override DataBuffer allocate(int size) { + for (int i=0; i<data.length; i++) { + if (data[i] == null) { + data[i] = new byte[size]; + } + } + return new DataBufferByte(data, size, offsets); + } + } + + /** + * A builder of data buffer of {@link DataBuffer#TYPE_SHORT}. + */ + private static final class Shorts extends BandSharing { + /** The shared arrays. */ + private final short[][] data; + + /** Creates a new builder. */ + Shorts(final BandedSampleModel target) { + super(target); + data = new short[offsets.length][]; + } + + /** Takes a reference to an array in the given data buffer. */ + @Override void takeReference(DataBuffer buffer, int src, int dst) { + data[dst] = ((DataBufferShort) buffer).getData(src); + } + + /** Builds the data buffer after all references have been taken. */ + @Override DataBuffer allocate(int size) { + for (int i=0; i<data.length; i++) { + if (data[i] == null) { + data[i] = new short[size]; + } + } + return new DataBufferShort(data, size, offsets); + } + } + + /** + * A builder of data buffer of {@link DataBuffer#TYPE_USHORT}. + */ + private static final class UShorts extends BandSharing { + /** The shared arrays. */ + private final short[][] data; + + /** Creates a new builder. */ + UShorts(final BandedSampleModel target) { + super(target); + data = new short[offsets.length][]; + } + + /** Takes a reference to an array in the given data buffer. */ + @Override void takeReference(DataBuffer buffer, int src, int dst) { + data[dst] = ((DataBufferUShort) buffer).getData(src); + } + + /** Builds the data buffer after all references have been taken. */ + @Override DataBuffer allocate(int size) { + for (int i=0; i<data.length; i++) { + if (data[i] == null) { + data[i] = new short[size]; + } + } + return new DataBufferUShort(data, size, offsets); + } + } + + /** + * A builder of data buffer of {@link DataBuffer#TYPE_INT}. + */ + private static final class Integers extends BandSharing { + /** The shared arrays. */ + private final int[][] data; + + /** Creates a new builder. */ + Integers(final BandedSampleModel target) { + super(target); + data = new int[offsets.length][]; + } + + /** Takes a reference to an array in the given data buffer. */ + @Override void takeReference(DataBuffer buffer, int src, int dst) { + data[dst] = ((DataBufferInt) buffer).getData(src); + } + + /** Builds the data buffer after all references have been taken. */ + @Override DataBuffer allocate(int size) { + for (int i=0; i<data.length; i++) { + if (data[i] == null) { + data[i] = new int[size]; + } + } + return new DataBufferInt(data, size, offsets); + } + } + + /** + * A builder of data buffer of {@link DataBuffer#TYPE_FLOAT}. + */ + private static final class Floats extends BandSharing { + /** The shared arrays. */ + private final float[][] data; + + /** Creates a new builder. */ + Floats(final BandedSampleModel target) { + super(target); + data = new float[offsets.length][]; + } + + /** Takes a reference to an array in the given data buffer. */ + @Override void takeReference(DataBuffer buffer, int src, int dst) { + data[dst] = ((DataBufferFloat) buffer).getData(src); + } + + /** Builds the data buffer after all references have been taken. */ + @Override DataBuffer allocate(int size) { + for (int i=0; i<data.length; i++) { + if (data[i] == null) { + data[i] = new float[size]; + } + } + return new DataBufferFloat(data, size, offsets); + } + } + + /** + * A builder of data buffer of {@link DataBuffer#TYPE_DOUBLE}. + */ + private static final class Doubles extends BandSharing { + /** The shared arrays. */ + private final double[][] data; + + /** Creates a new builder. */ + Doubles(final BandedSampleModel target) { + super(target); + data = new double[offsets.length][]; + } + + /** Takes a reference to an array in the given data buffer. */ + @Override void takeReference(DataBuffer buffer, int src, int dst) { + data[dst] = ((DataBufferDouble) buffer).getData(src); + } + + /** Builds the data buffer after all references have been taken. */ + @Override DataBuffer allocate(int size) { + for (int i=0; i<data.length; i++) { + if (data[i] == null) { + data[i] = new double[size]; + } + } + return new DataBufferDouble(data, size, offsets); + } + } +} diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java index 2076fe5329..e1075cac30 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java @@ -27,7 +27,6 @@ import java.awt.image.BandedSampleModel; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.SampleModel; -import java.awt.image.TileObserver; import java.lang.reflect.Array; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.operation.TransformException; @@ -36,7 +35,6 @@ import org.apache.sis.internal.coverage.j2d.ColorModelBuilder; import org.apache.sis.internal.coverage.j2d.ImageLayout; import org.apache.sis.internal.coverage.j2d.ImageUtilities; import org.apache.sis.internal.coverage.j2d.TileOpExecutor; -import org.apache.sis.internal.coverage.j2d.WriteSupport; import org.apache.sis.internal.coverage.SampleDimensions; import org.apache.sis.internal.util.UnmodifiableArrayList; import org.apache.sis.util.Numbers; @@ -72,7 +70,7 @@ import static org.apache.sis.internal.coverage.j2d.ImageUtilities.LOGGER; * @version 1.4 * @since 1.1 */ -class BandedSampleConverter extends ComputedImage { +class BandedSampleConverter extends WritableComputedImage { /* * Do not extend `SourceAlignedImage` because we want to inherit the `getNumTiles()` * and `getTileGridOffset()` methods defined by `PlanarImage`. @@ -429,17 +427,6 @@ class BandedSampleConverter extends ComputedImage { */ private final MathTransform1D[] inverses; - /** - * The observers, or {@code null} if none. This is a copy-on-write array: - * values are never modified after construction (new arrays are created). - * - * This field is declared volatile because it is read without synchronization by - * {@link #markTileWritable(int, int, boolean)}. Since this is a copy-on-write array, - * it is okay to omit synchronization for that method but we still need the memory effect. - */ - @SuppressWarnings("VolatileArrayField") - private volatile TileObserver[] observers; - /** * Creates a new writable image which will compute values using the given converters. */ @@ -452,74 +439,6 @@ class BandedSampleConverter extends ComputedImage { this.inverses = inverses; } - /** - * Adds an observer to be notified when a tile is checked out for writing. - * If the observer is already present, it will receive multiple notifications. - * - * @param observer the observer to notify. - */ - @Override - public synchronized void addTileObserver(final TileObserver observer) { - observers = WriteSupport.addTileObserver(observers, observer); - } - - /** - * Removes an observer from the list of observers notified when a tile is checked out for writing. - * If the observer was not registered, nothing happens. If the observer was registered for multiple - * notifications, it will now be registered for one fewer. - * - * @param observer the observer to stop notifying. - */ - @Override - public synchronized void removeTileObserver(final TileObserver observer) { - observers = WriteSupport.removeTileObserver(observers, observer); - } - - /** - * Sets or clears whether a tile is checked out for writing and notifies the listener if needed. - * - * @param tileX the <var>x</var> index of the tile to acquire or release. - * @param tileY the <var>y</var> index of the tile to acquire or release. - * @param writing {@code true} for acquiring the tile, or {@code false} for releasing it. - */ - @Override - protected boolean markTileWritable(final int tileX, final int tileY, final boolean writing) { - final boolean notify = super.markTileWritable(tileX, tileY, writing); - if (notify) { - WriteSupport.fireTileUpdate(observers, this, tileX, tileY, writing); - } - return notify; - } - - /** - * Checks out a tile for writing. - * - * @param tileX the <var>x</var> index of the tile. - * @param tileY the <var>y</var> index of the tile. - * @return the specified tile as a writable tile. - */ - @Override - public WritableRaster getWritableTile(final int tileX, final int tileY) { - final WritableRaster tile = (WritableRaster) getTile(tileX, tileY); - markTileWritable(tileX, tileY, true); - return tile; - } - - /** - * Relinquishes the right to write to a tile. If the tile goes from having one writer to - * having no writers, the values are inverse converted and written in the original image. - * If the caller continues to write to the tile, the results are undefined. - * - * @param tileX the <var>x</var> index of the tile. - * @param tileY the <var>y</var> index of the tile. - */ - @Override - public void releaseWritableTile(final int tileX, final int tileY) { - if (markTileWritable(tileX, tileY, false)) { - setData(getTile(tileX, tileY)); - } - } - /** * Sets a region of the image to the contents of the given raster. * The raster is assumed to be in the same coordinate space as this image. diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java b/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java index 0edd294a15..7f283984fe 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java @@ -26,6 +26,7 @@ import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.awt.image.BandedSampleModel; import java.awt.image.ComponentSampleModel; +import java.awt.image.WritableRenderedImage; import org.apache.sis.util.Workaround; import org.apache.sis.util.collection.FrequencySortedSet; import org.apache.sis.internal.feature.Resources; @@ -60,6 +61,12 @@ final class CombinedImageLayout extends ImageLayout { */ final RenderedImage[] sources; + /** + * The source images with only the user-specified bands. + * Those images are views, the pixels are not copied. + */ + final RenderedImage[] filteredSources; + /** * Ordered (not necessarily sorted) indices of bands to select in each source image. * The length of this array is always equal to the length of the {@link #sources} array. @@ -70,7 +77,7 @@ final class CombinedImageLayout extends ImageLayout { /** * The sample model of the combined image. - * All {@linkplain BandedSampleModel#getBandOffsets() band offsets} are zero and + * All {@linkplain BandedSampleModel#getBandOffsets() band offsets} are zeros and * all {@linkplain BandedSampleModel#getBankIndices() bank indices} are identity mapping. * This simplicity is needed by current implementation of {@link BandAggregateImage}. */ @@ -101,12 +108,6 @@ final class CombinedImageLayout extends ImageLayout { */ private final boolean exactTileSize; - /** - * Whether all sources have tiles at the same locations and use the same scanline stride. - * In such case, it is possible to share references to data arrays without copying them. - */ - final boolean allowSharing; - /** * Computes the layout of an image combining all the specified source images. * The optional {@code bandsPerSource} argument specifies the bands to select in each source images. @@ -230,9 +231,20 @@ final class CombinedImageLayout extends ImageLayout { this.domain = domain; this.minTileX = minTileX; this.minTileY = minTileY; - this.allowSharing = (scanlineStride > 0); this.sampleModel = createBandedSampleModel(commonDataType, numBands, null, domain, scanlineStride); - // Sample model must be last (all other fields must be initialized before). + /* + * Note: above call to `createBandedSampleModel(…)` must be last, + * except for `filteredSources` which is not needed by that method. + */ + filteredSources = new RenderedImage[sources.length]; + for (int i=0; i<filteredSources.length; i++) { + RenderedImage source = sources[i]; + final int[] bands = bandsPerSource[i]; + if (bands != null) { + source = BandSelectImage.create(source, bands); + } + filteredSources[i] = source; + } } /** @@ -327,22 +339,17 @@ final class CombinedImageLayout extends ImageLayout { } /** - * Returns the source images with only the user-specified bands. - * The returned images are views; the bands are not copied. + * Returns {@code true} if all filtered sources are writable. * - * @return the source images with only user-supplied bands. + * @return whether a destination using all filtered sources could be writable. */ - final RenderedImage[] getFilteredSources() { - final RenderedImage[] images = new RenderedImage[sources.length]; - for (int i=0; i<images.length; i++) { - RenderedImage source = sources[i]; - final int[] bands = bandsPerSource[i]; - if (bands != null) { - source = BandSelectImage.create(source, bands); + final boolean isWritable() { + for (final RenderedImage source : filteredSources) { + if (!(source instanceof WritableRenderedImage)) { + return false; } - images[i] = source; } - return images; + return true; } /** diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java index a9033a25de..126fe58caf 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java @@ -183,14 +183,14 @@ public abstract class ComputedImage extends PlanarImage implements Disposable { * If this field is set to a non-null value, then this assignment should be done * soon after construction time before any tile computation started. * - * <div class="note"><b>Note on interaction with tile cache</b><br> + * <h4>Note on interaction with tile cache</h4> * The use of a destination image may produce unexpected result if {@link #computeTile(int, int, WritableRaster)} * is invoked two times or more for the same destination tile. It may look like a problem because computed tiles * can be discarded and recomputed at any time. However, this problem should not happen because tiles computed by * this {@code ComputedImage} will not be discarded as long as {@code destination} has a reference to that tile. * If a {@code ComputedImage} tile has been discarded, then it implies that the corresponding {@code destination} * tile has been discarded as well, in which case the tile computation will restart from scratch; it will not be - * a recomputation of only this {@code ComputedImage} on top of an old {@code destination} tile.</div> + * a recomputation of only this {@code ComputedImage} on top of an old {@code destination} tile. * * @see #setDestination(WritableRenderedImage) */ @@ -203,10 +203,12 @@ public abstract class ComputedImage extends PlanarImage implements Disposable { * and the {@linkplain SampleModel#getHeight() sample model height} * determines this {@linkplain #getTileHeight() image tile height}. * - * <div class="note"><b>Design note:</b> + * <h4>Design note:</h4> * {@code ComputedImage} requires the sample model to have exactly the desired tile size * otherwise tiles created by {@link #createTile(int, int)} will consume more memory - * than needed.</div> + * than needed. + * + * @see #getSampleModel() */ protected final SampleModel sampleModel; @@ -406,10 +408,10 @@ public abstract class ComputedImage extends PlanarImage implements Disposable { /** * Returns the width of tiles in this image. The default implementation returns {@link SampleModel#getWidth()}. * - * <div class="note"><b>Note:</b> - * a raster can have a smaller width than its sample model, for example when a raster is a view over a subregion + * <h4>Note</h4> + * A raster can have a smaller width than its sample model, for example when a raster is a view over a subregion * of another raster. But this is not recommended in the particular case of this {@code ComputedImage} class, - * because it would cause {@link #createTile(int, int)} to consume more memory than necessary.</div> + * because it would cause {@link #createTile(int, int)} to consume more memory than necessary. * * @return the width of this image in pixels. */ @@ -421,10 +423,10 @@ public abstract class ComputedImage extends PlanarImage implements Disposable { /** * Returns the height of tiles in this image. The default implementation returns {@link SampleModel#getHeight()}. * - * <div class="note"><b>Note:</b> - * a raster can have a smaller height than its sample model, for example when a raster is a view over a subregion + * <h4>Note</h4> + * A raster can have a smaller height than its sample model, for example when a raster is a view over a subregion * of another raster. But this is not recommended in the particular case of this {@code ComputedImage} class, - * because it would cause {@link #createTile(int, int)} to consume more memory than necessary.</div> + * because it would cause {@link #createTile(int, int)} to consume more memory than necessary. * * @return the height of this image in pixels. */ @@ -586,21 +588,10 @@ public abstract class ComputedImage extends PlanarImage implements Disposable { * @return initially empty tile for the given indices (cannot be null). */ protected WritableRaster createTile(final int tileX, final int tileY) { - return WritableRaster.createWritableRaster(getSampleModel(), computeTileLocation(tileX, tileY)); - } - - /** - * Returns the location of the tile to create for the given tile indices. - * - * @param tileX the column index of the tile to create. - * @param tileY the row index of the tile to create. - * @return location of the tile to create. - */ - final Point computeTileLocation(final int tileX, final int tileY) { // A temporary `int` overflow may occur before the final addition. final int x = Math.toIntExact((((long) tileX) - getMinTileX()) * getTileWidth() + getMinX()); final int y = Math.toIntExact((((long) tileY) - getMinTileY()) * getTileHeight() + getMinY()); - return new Point(x,y); + return WritableRaster.createWritableRaster(sampleModel, new Point(x,y)); } /** diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java index 656b267a52..fab9c30fe6 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java @@ -922,6 +922,11 @@ public class ImageProcessor implements Cloneable { * contain values from the pixels at the same coordinates in all source images. * The result image will be bounded by the intersection of all source images. * + * <p>If all source images are {@link WritableRenderedImage} instances, + * then the returned image will also be a {@link WritableRenderedImage}. + * In such case values written in the returned image will be copied back + * to the source images.</p> + * * <h4>Restrictions</h4> * All images shall use the same {@linkplain SampleModel#getDataType() data type}, * and all source images shall intersect each other with a non-empty intersection area. diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/WritableComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/WritableComputedImage.java new file mode 100644 index 0000000000..adcfd1b6fc --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/image/WritableComputedImage.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.image; + +import java.awt.image.SampleModel; +import java.awt.image.TileObserver; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.awt.image.WritableRaster; +import java.awt.image.WritableRenderedImage; +import org.apache.sis.internal.coverage.j2d.WriteSupport; + + +/** + * Parent classes for computed images that are <em>potentially</em> writable. + * This class implements some {@link WritableRenderedImage} methods such as + * the methods for adding or removing tile listeners. However this class does + * <em>not</em> implement the {@link WritableRenderedImage} interface itself. + * It is up to subclasses to implement that interface explicitly + * when they have determined that the image is effectively writable. + * + * <h2>Usage pattern</h2> + * Create a package-private read-only image by extending this class as if + * {@link ComputedImage} was extended directly. Ignore all public methods + * defined in this class. Do not make the class public for preventing users + * users to access those public methods. + * + * <p>Create a package-private writable image as a subclass of above read-only image. + * Override {@link #setData(Raster)}, {@link #equals(Object)} and {@link #hashCode()}. + * The latter two methods need to be overridden for restoring the identity behavior + * for writable image, because it may have listeners attached to this specific instance. + * Example:</p> + * + * {@snippet lang="java" : + * class MyOperation extends WritableComputedImage { + * + * // Constructors omitted for brevity. + * + * static final class Writable extends MyOperation implements WritableRenderedImage { + * @Override + * public void setData(Raster data) { + * // Write data back to original images here. + * } + * + * @Override + * public boolean equals(final Object object) { + * return object == this; + * } + * + * @Override + * public int hashCode() { + * return System.identityHashCode(this); + * } + * } + * } + * } + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * @since 1.4 + */ +abstract class WritableComputedImage extends ComputedImage { + /** + * The observers, or {@code null} if none. This is a copy-on-write array: + * values are never modified after construction (new arrays are created). + * + * This field is declared volatile because it is read without synchronization by + * {@link #markTileWritable(int, int, boolean)}. Since this is a copy-on-write array, + * it is okay to omit synchronization for that method but we still need the memory effect. + */ + @SuppressWarnings("VolatileArrayField") + private volatile TileObserver[] observers; + + /** + * Creates an initially empty image with the given sample model. + * The source images are not necessarily {@link WritableRenderedImage} + * because this {@code WritableComputedImage} instance may be effectively read-only. + * However if this {@code WritableComputedImage} instance is effectively writable, + * then the given sources should be writable too. + * + * @param sampleModel the sample model shared by all tiles in this image. + * @param sources sources of this image (may be an empty array), or a null array if unknown. + */ + protected WritableComputedImage(SampleModel sampleModel, RenderedImage... sources) { + super(sampleModel, sources); + } + + /** + * Adds an observer to be notified when a tile is checked out for writing. + * If the observer is already present, it will receive multiple notifications. + * + * @param observer the observer to notify. + */ + public synchronized void addTileObserver(final TileObserver observer) { + observers = WriteSupport.addTileObserver(observers, observer); + } + + /** + * Removes an observer from the list of observers notified when a tile is checked out for writing. + * If the observer was not registered, nothing happens. If the observer was registered for multiple + * notifications, it will now be registered for one fewer. + * + * @param observer the observer to stop notifying. + */ + public synchronized void removeTileObserver(final TileObserver observer) { + observers = WriteSupport.removeTileObserver(observers, observer); + } + + /** + * Sets or clears whether a tile is checked out for writing and notifies the listener if needed. + * + * @param tileX the <var>x</var> index of the tile to acquire or release. + * @param tileY the <var>y</var> index of the tile to acquire or release. + * @param writing {@code true} for acquiring the tile, or {@code false} for releasing it. + */ + @Override + protected boolean markTileWritable(final int tileX, final int tileY, final boolean writing) { + final boolean notify = super.markTileWritable(tileX, tileY, writing); + if (notify && this instanceof WritableRenderedImage) { + WriteSupport.fireTileUpdate(observers, (WritableRenderedImage) this, tileX, tileY, writing); + } + return notify; + } + + /** + * Checks out a tile for writing. + * + * @param tileX the <var>x</var> index of the tile. + * @param tileY the <var>y</var> index of the tile. + * @return the specified tile as a writable tile. + */ + public WritableRaster getWritableTile(final int tileX, final int tileY) { + final WritableRaster tile = (WritableRaster) getTile(tileX, tileY); + markTileWritable(tileX, tileY, true); + return tile; + } + + /** + * Relinquishes the right to write to a tile. + * If the tile goes from having one writer to having no writers, + * then the values are written to the original images by a call to {@link #setData(Raster)}. + * If the caller continues to write to the tile, the results are undefined. + * + * @param tileX the <var>x</var> index of the tile. + * @param tileY the <var>y</var> index of the tile. + */ + public void releaseWritableTile(final int tileX, final int tileY) { + if (markTileWritable(tileX, tileY, false)) { + setData(getTile(tileX, tileY)); + } + } + + /** + * Sets a region of the image to the contents of the given raster. + * The raster is assumed to be in the same coordinate space as this image. + * The operation is clipped to the bounds of this image. + * + * @param data the values to write in this image. + */ + protected void setData(final Raster data) { + throw new UnsupportedOperationException(); + } +} diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java index 0cb29939fa..d3ec94a516 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java @@ -16,15 +16,19 @@ */ package org.apache.sis.image; +import java.util.Arrays; import java.util.HashSet; import java.util.stream.IntStream; -import java.util.function.Consumer; +import java.util.function.ObjIntConsumer; import java.awt.Rectangle; import java.awt.image.BandedSampleModel; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.Raster; import java.awt.image.RenderedImage; +import java.awt.image.WritableRaster; +import java.awt.image.WritableRenderedImage; +import org.apache.sis.internal.coverage.j2d.ImageUtilities; import org.apache.sis.internal.coverage.j2d.RasterFactory; import org.apache.sis.util.ArraysExt; import org.apache.sis.test.TestCase; @@ -43,6 +47,16 @@ import static org.junit.Assert.*; * @since 1.4 */ public final class BandAggregateImageTest extends TestCase { + /** + * Whether to test write operations. + */ + private static final boolean WRITABLE = true; + + /** + * Source images used for building the band aggregate image. + */ + private RenderedImage[] sourceImages; + /** * Whether to allow the sharing of data arrays. * If {@code false}, tests will force copies. @@ -61,9 +75,9 @@ public final class BandAggregateImageTest extends TestCase { * This is the simplest case in this test class. */ @Test - public void copyUntiledImages() { + public void testForcedCopy() { allowSharing = false; - aggregateUntiledImages(); + testUntiledImages(); } /** @@ -71,16 +85,17 @@ public final class BandAggregateImageTest extends TestCase { * Sample values should not be copied unless forced to. */ @Test - @DependsOnMethod("copyUntiledImages") - public void aggregateUntiledImages() { + @DependsOnMethod("testForcedCopy") + public void testUntiledImages() { final int width = 3; final int height = 4; final BufferedImage im1 = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); final BufferedImage im2 = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); im1.getRaster().setSamples(0, 0, width, height, 0, IntStream.range(0, width*height).map(s -> s + 1).toArray()); im2.getRaster().setSamples(0, 0, width, height, 0, IntStream.range(0, width*height).map(s -> s * 2).toArray()); + sourceImages = new RenderedImage[] {im1, im2}; - final RenderedImage result = BandAggregateImage.create(new RenderedImage[] {im1, im2}, null, null, allowSharing); + final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing); assertNotNull(result); assertEquals(0, result.getMinTileX()); assertEquals(0, result.getMinTileY()); @@ -99,33 +114,71 @@ public final class BandAggregateImageTest extends TestCase { }, tile.getPixels(0, 0, width, height, (int[]) null) ); - verifySharing(result, allowSharing); + verifySharing(result, allowSharing, allowSharing); + /* + * Try writing two values, then check again. + */ + if (WRITABLE) { + final int tileX = 0; + final int tileY = 0; + final WritableRenderedImage writable = (WritableRenderedImage) result; + final WritableRaster target = writable.getWritableTile(tileX, tileY); + assertSame(tile, target); + target.setPixel(2, 1, new int[] {100, 80}); + target.setPixel(1, 3, new int[] { 60, 40}); + writable.releaseWritableTile(tileX, tileY); + assertSame(target, result.getTile(tileX, tileY)); + assertArrayEquals( + new int[] { + 1, 0, 2, 2, 3, 4, + 4, 6, 5, 8, 100, 80, + 7, 12, 8, 14, 9, 16, + 10, 18, 60, 40, 12, 22 + }, + tile.getPixels(0, 0, width, height, (int[]) null) + ); + assertEquals(100, im1.getRaster().getSample(2, 1, 0)); + assertEquals( 80, im2.getRaster().getSample(2, 1, 0)); + assertEquals( 60, im1.getRaster().getSample(1, 3, 0)); + assertEquals( 40, im2.getRaster().getSample(1, 3, 0)); + } } /** * Tests the aggregation of two tiled images having the same tile matrix. * The same test is executed many times with different but equivalent classes of sample models. - * Bands may be copied or references, depending on the sample models. + * Bands may be copied or referenced, depending on the sample models. */ @Test - @DependsOnMethod("aggregateUntiledImages") - public void aggregateSimilarlyTiledImages() { + @DependsOnMethod("testUntiledImages") + public void testSimilarlyTiledImages() { do { - aggregateSimilarlyTiledImages(true, true); - aggregateSimilarlyTiledImages(false, false); - aggregateSimilarlyTiledImages(true, false); - aggregateSimilarlyTiledImages(false, true); + testSimilarlyTiledImages(true, true, false); + testSimilarlyTiledImages(false, false, false); + testSimilarlyTiledImages(true, false, false); + testSimilarlyTiledImages(false, true, false); } while ((allowSharing = !allowSharing) == false); // Loop executed exactly twice. } + /** + * Tests write operations in the aggregation of two tiled images having the same tile matrix. + */ + @Test + @DependsOnMethod("testSimilarlyTiledImages") + public void testWriteOperation() { + testSimilarlyTiledImages(true, true, WRITABLE); + // Other modes are not supported by `TiledImageMock`. + } + /** * Implementation of {@link #aggregateSimilarlyTiledImages()} with sample model classes * specified by the boolean arguments. * * @param firstBanded whether to use {@code BandedSampleModel} for the first image. * @param secondBanded whether to use {@code BandedSampleModel} for the second image. + * @param testWrite whether to test write operation. */ - private void aggregateSimilarlyTiledImages(final boolean firstBanded, final boolean secondBanded) { + private void testSimilarlyTiledImages(final boolean firstBanded, final boolean secondBanded, final boolean testWrite) { final int minX = 7; final int minY = -5; final int width = 6; @@ -133,8 +186,9 @@ public final class BandAggregateImageTest extends TestCase { final TiledImageMock im1 = new TiledImageMock(DataBuffer.TYPE_USHORT, 2, minX, minY, width, height, 3, 3, 1, 2, firstBanded); final TiledImageMock im2 = new TiledImageMock(DataBuffer.TYPE_USHORT, 2, minX, minY, width, height, 3, 3, 3, 4, secondBanded); initializeAllTiles(im1, im2); + sourceImages = new RenderedImage[] {im1, im2}; - RenderedImage result = BandAggregateImage.create(new RenderedImage[] {im1, im2}, null, null, allowSharing); + RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing); assertNotNull(result); assertEquals(minX, result.getMinX()); assertEquals(minY, result.getMinY()); @@ -147,33 +201,57 @@ public final class BandAggregateImageTest extends TestCase { assertEquals(2, result.getNumXTiles()); assertEquals(3, result.getNumYTiles()); assertEquals(4, result.getSampleModel().getNumBands()); - + final int[] expected = { + // Tile 1 Tile 2 + 1100, 2100, 3100, 4100, 1101, 2101, 3101, 4101, 1102, 2102, 3102, 4102, 1200, 2200, 3200, 4200, 1201, 2201, 3201, 4201, 1202, 2202, 3202, 4202, + 1110, 2110, 3110, 4110, 1111, 2111, 3111, 4111, 1112, 2112, 3112, 4112, 1210, 2210, 3210, 4210, 1211, 2211, 3211, 4211, 1212, 2212, 3212, 4212, + 1120, 2120, 3120, 4120, 1121, 2121, 3121, 4121, 1122, 2122, 3122, 4122, 1220, 2220, 3220, 4220, 1221, 2221, 3221, 4221, 1222, 2222, 3222, 4222, + // Tile 3 Tile 4 + 1300, 2300, 3300, 4300, 1301, 2301, 3301, 4301, 1302, 2302, 3302, 4302, 1400, 2400, 3400, 4400, 1401, 2401, 3401, 4401, 1402, 2402, 3402, 4402, + 1310, 2310, 3310, 4310, 1311, 2311, 3311, 4311, 1312, 2312, 3312, 4312, 1410, 2410, 3410, 4410, 1411, 2411, 3411, 4411, 1412, 2412, 3412, 4412, + 1320, 2320, 3320, 4320, 1321, 2321, 3321, 4321, 1322, 2322, 3322, 4322, 1420, 2420, 3420, 4420, 1421, 2421, 3421, 4421, 1422, 2422, 3422, 4422, + // Tile 5 Tile 6 + 1500, 2500, 3500, 4500, 1501, 2501, 3501, 4501, 1502, 2502, 3502, 4502, 1600, 2600, 3600, 4600, 1601, 2601, 3601, 4601, 1602, 2602, 3602, 4602, + 1510, 2510, 3510, 4510, 1511, 2511, 3511, 4511, 1512, 2512, 3512, 4512, 1610, 2610, 3610, 4610, 1611, 2611, 3611, 4611, 1612, 2612, 3612, 4612, + 1520, 2520, 3520, 4520, 1521, 2521, 3521, 4521, 1522, 2522, 3522, 4522, 1620, 2620, 3620, 4620, 1621, 2621, 3621, 4621, 1622, 2622, 3622, 4622 + }; Raster raster = result.getData(); assertEquals(4, raster.getNumBands()); assertEquals(new Rectangle(minX, minY, width, height), raster.getBounds()); - assertArrayEquals( - new int[] { - // Tile 1 Tile 2 - 1100, 2100, 3100, 4100, 1101, 2101, 3101, 4101, 1102, 2102, 3102, 4102, 1200, 2200, 3200, 4200, 1201, 2201, 3201, 4201, 1202, 2202, 3202, 4202, - 1110, 2110, 3110, 4110, 1111, 2111, 3111, 4111, 1112, 2112, 3112, 4112, 1210, 2210, 3210, 4210, 1211, 2211, 3211, 4211, 1212, 2212, 3212, 4212, - 1120, 2120, 3120, 4120, 1121, 2121, 3121, 4121, 1122, 2122, 3122, 4122, 1220, 2220, 3220, 4220, 1221, 2221, 3221, 4221, 1222, 2222, 3222, 4222, - // Tile 3 Tile 4 - 1300, 2300, 3300, 4300, 1301, 2301, 3301, 4301, 1302, 2302, 3302, 4302, 1400, 2400, 3400, 4400, 1401, 2401, 3401, 4401, 1402, 2402, 3402, 4402, - 1310, 2310, 3310, 4310, 1311, 2311, 3311, 4311, 1312, 2312, 3312, 4312, 1410, 2410, 3410, 4410, 1411, 2411, 3411, 4411, 1412, 2412, 3412, 4412, - 1320, 2320, 3320, 4320, 1321, 2321, 3321, 4321, 1322, 2322, 3322, 4322, 1420, 2420, 3420, 4420, 1421, 2421, 3421, 4421, 1422, 2422, 3422, 4422, - // Tile 5 Tile 6 - 1500, 2500, 3500, 4500, 1501, 2501, 3501, 4501, 1502, 2502, 3502, 4502, 1600, 2600, 3600, 4600, 1601, 2601, 3601, 4601, 1602, 2602, 3602, 4602, - 1510, 2510, 3510, 4510, 1511, 2511, 3511, 4511, 1512, 2512, 3512, 4512, 1610, 2610, 3610, 4610, 1611, 2611, 3611, 4611, 1612, 2612, 3612, 4612, - 1520, 2520, 3520, 4520, 1521, 2521, 3521, 4521, 1522, 2522, 3522, 4522, 1620, 2620, 3620, 4620, 1621, 2621, 3621, 4621, 1622, 2622, 3622, 4622 - }, - raster.getPixels(minX, minY, width, height, (int[]) null) - ); - verifySharing(result, allowSharing && allowSharing(im1, im2)); + assertArrayEquals(expected, raster.getPixels(minX, minY, width, height, (int[]) null)); + verifySharing(result, allowSharing(4, im1, im2)); + /* + * Try writing two values, then check again. + * The modified tile is labeled "Tile 4" above. + */ + if (testWrite) { + final int tileX = 2; // minTileX = 1 + final int tileY = 3; // minTileY = 2 + final WritableRenderedImage writable = (WritableRenderedImage) result; + final WritableRaster target = writable.getWritableTile(tileX, tileY); + target.setPixel(10, -2, new int[] {100, 80, 20, 30}); // Upper left corner of tile 4 + target.setPixel(12, -1, new int[] {200, 240, 260, 250}); + writable.releaseWritableTile(tileX, tileY); + assertEquals(1400, expected[ 84]); // For verifying that we are at the correct location. + assertEquals(1412, expected[116]); + expected[ 84] = 100; + expected[ 85] = 80; + expected[ 86] = 20; + expected[ 87] = 30; + expected[116] = 200; + expected[117] = 240; + expected[118] = 260; + expected[119] = 250; + assertSame(target, result.getTile(tileX, tileY)); + assertArrayEquals(expected, result.getData().getPixels(minX, minY, width, height, (int[]) null)); + return; // Can not continue the tests because the source images have been modified. + } /* * Repeat the test with a custom band selection. * One of the source images is used twice, but with a different selection of bands. */ - result = BandAggregateImage.create(new RenderedImage[] {im1, im2, im1}, new int[][] { + sourceImages = new RenderedImage[] {im1, im2, im1}; + result = BandAggregateImage.create(sourceImages, new int[][] { new int[] {1}, // Take second band of image 1. null, // Take all bands of image 2. new int[] {0} // Take first band of image 1. @@ -221,8 +299,8 @@ public final class BandAggregateImageTest extends TestCase { * A copy of sample values can not be avoided in this case. */ @Test - @DependsOnMethod("aggregateSimilarlyTiledImages") - public void aggregateImagesUsingSameExtentButDifferentTileSizes() { + @DependsOnMethod("testSimilarlyTiledImages") + public void testImagesUsingSameExtentButDifferentTileSizes() { final int minX = 3; final int minY = 1; final int width = 8; @@ -236,10 +314,9 @@ public final class BandAggregateImageTest extends TestCase { final TiledImageMock tiled4x1 = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, minX, minY, width, height, 4, 1, 3, 4, true); final TiledImageMock oneTile = new TiledImageMock(DataBuffer.TYPE_FLOAT, 1, minX, minY, width, height, 8, 4, 5, 6, true); initializeAllTiles(tiled2x2, tiled4x1, oneTile); + sourceImages = new RenderedImage[] {tiled2x2, tiled4x1, oneTile}; - final RenderedImage result = BandAggregateImage.create( - new RenderedImage[] {tiled2x2, tiled4x1, oneTile}, null, null, allowSharing); - + final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing); assertNotNull(result); assertEquals(minX, result.getMinX()); assertEquals(minY, result.getMinY()); @@ -264,21 +341,21 @@ public final class BandAggregateImageTest extends TestCase { }, raster.getPixels(minX, minY, width, height, (int[]) null) ); - verifySharing(result, false); + verifySharing(result, false, false, false); } /** * Tests the aggregation of three tiled images having different extents and different tile matrices. - * A copy of sample values can not be avoided in this case. + * A copy of sample values can not be avoided in this case, except on the second image. */ @Test - @DependsOnMethod("aggregateImagesUsingSameExtentButDifferentTileSizes") - public void aggregateImagesUsingDifferentExtentsAndDifferentSquaredTiling() { + @DependsOnMethod("testImagesUsingSameExtentButDifferentTileSizes") + public void testImagesUsingDifferentExtentsAndDifferentSquaredTiling() { /* * Tip: band number match image tile width. i.e: * * untiled → band 1 - * tiled 2x2 → bands 2 and 3 + * tiled 2x2 → bands 2 and 3 — reference to data arrays can be shared. * tiled 4x4 → bands 4 and 5 * tiled 6x6 → band 6 */ @@ -287,10 +364,9 @@ public final class BandAggregateImageTest extends TestCase { final TiledImageMock tiled4x4 = new TiledImageMock(DataBuffer.TYPE_SHORT, 2, 4, 2, 8, 8, 4, 4, 0, 0, true); final TiledImageMock tiled6x6 = new TiledImageMock(DataBuffer.TYPE_SHORT, 1, 2, 0, 12, 6, 6, 6, 0, 0, true); initializeAllTiles(untiled, tiled2x2, tiled4x4, tiled6x6); + sourceImages = new RenderedImage[] {untiled, tiled2x2, tiled4x4, tiled6x6}; - final RenderedImage result = BandAggregateImage.create( - new RenderedImage[] {untiled, tiled2x2, tiled4x4, tiled6x6}, null, null, allowSharing); - + final RenderedImage result = BandAggregateImage.create(sourceImages, null, null, allowSharing); assertNotNull(result); assertEquals(4, result.getMinX()); assertEquals(2, result.getMinY()); @@ -316,7 +392,7 @@ public final class BandAggregateImageTest extends TestCase { }, raster.getPixels(4, 2, 8, 4, (int[]) null) ); - verifySharing(result, false); + verifySharing(result, false, allowSharing, true, false, false, false); } /** @@ -344,31 +420,45 @@ public final class BandAggregateImageTest extends TestCase { * the internal data arrays. This method should be invoked for {@link TiledImageMock} having * more than 1 band, because their sample model is selected randomly. */ - private static boolean allowSharing(final RenderedImage... sources) { - for (final RenderedImage source : sources) { - if (!(source.getSampleModel() instanceof BandedSampleModel)) { - return false; + private boolean[] allowSharing(final int numBands, final RenderedImage... sources) { + final boolean[] sharingPerBand = new boolean[numBands]; + if (allowSharing) { + int lower = 0; + for (final RenderedImage source : sources) { + final int upper = lower + ImageUtilities.getNumBands(source); + if (source.getSampleModel() instanceof BandedSampleModel) { + Arrays.fill(sharingPerBand, lower, upper, true); + } + lower = upper; } + assertEquals(numBands, lower); } - return true; + return sharingPerBand; } /** * Verifies if the given image reuses the data arrays of all its source. * * @param result the result of band aggregation. - * @param sharing whether the caller expects the result to share data arrays. + * @param sharingPerBand whether the caller expects the result to share data arrays. One value per band. */ - private static void verifySharing(final RenderedImage result, final boolean sharing) { + private static void verifySharing(final RenderedImage result, final boolean... sharingPerBand) { + assertEquals(ImageUtilities.getNumBands(result), sharingPerBand.length); final var arrays = new HashSet<Object>(); for (final RenderedImage source : result.getSources()) { - forAllDataArrays(source, (data) -> assertTrue("Found two references to the same array.", arrays.add(data))); + forAllDataArrays(source, (data, band) -> assertTrue("Found two references to the same array.", arrays.add(data))); + } + forAllDataArrays(result, (data, band) -> { + final boolean sharing = sharingPerBand[band]; + assertEquals(sharing ? "Expected the target image to reference an existing array." + : "Expected only copies, no references to existing arrays.", + sharing, arrays.remove(data)); + }); + boolean sharing = true; + for (int i=0; i < sharingPerBand.length; i++) { + sharing &= sharingPerBand[i]; } - final String message = sharing - ? "Expected the target image to reference an existing array." - : "Expected only copies, no references to existing arrays."; - forAllDataArrays(result, (data) -> assertEquals(message, sharing, arrays.remove(data))); - assertEquals("Expected sharing of either all arrays or none of them.", sharing, arrays.isEmpty()); + assertEquals(sharing, arrays.isEmpty()); } /** @@ -377,14 +467,14 @@ public final class BandAggregateImageTest extends TestCase { * @param source the image for which to get data arrays. * @param action the action to execute for each data arrays. */ - private static void forAllDataArrays(final RenderedImage source, final Consumer<Object> action) { + private static void forAllDataArrays(final RenderedImage source, final ObjIntConsumer<Object> action) { for (int x = source.getNumXTiles(); --x >= 0;) { final int tileX = source.getMinTileX() + x; for (int y = source.getNumYTiles(); --y >= 0;) { final int tileY = source.getMinTileY() + y; final DataBuffer buffer = source.getTile(tileX, tileY).getDataBuffer(); - for (int b = buffer.getNumBanks(); --b >= 0;) { - action.accept(RasterFactory.createBuffer(buffer, b).array()); + for (int band = buffer.getNumBanks(); --band >= 0;) { + action.accept(RasterFactory.createBuffer(buffer, band).array(), band); } } }