You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/04/05 18:25:29 UTC
[sis] branch geoapi-4.0 updated: 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.
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 <ma...@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);
}
}
}