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 2020/08/21 18:05:18 UTC

[sis] branch geoapi-4.0 updated (5339509 -> 396816f)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git.


    from 5339509  Do not overwrite puxels in destination image that are not covered by a pixel in source image.
     new c5ae48b  Replace the `PreferredSize` hack by a public API.
     new 563cba7  Rename `ImageProcessor.getStatistics(…)` as `valueOfStatistics(…)`. The intent is to make a clearer separation with property getter methods. It also reduces the impression that this method returns a pre-existing value (the computation can be expansive).
     new 396816f  Hide the splash screen after application startup.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../main/java/org/apache/sis/gui/DataViewer.java   | 10 +++-
 .../org/apache/sis/gui/coverage/RenderingData.java |  8 +--
 .../apache/sis/internal/gui/ImageConverter.java    |  2 +-
 .../java/org/apache/sis/image/ImageCombiner.java   |  2 +-
 .../java/org/apache/sis/image/ImageProcessor.java  | 64 +++++++++++++++++++--
 .../java/org/apache/sis/image/RecoloredImage.java  |  2 +-
 .../java/org/apache/sis/image/Visualization.java   |  2 +-
 .../sis/internal/coverage/j2d/ImageLayout.java     | 64 ++++++++++++++++-----
 .../sis/internal/coverage/j2d/PreferredSize.java   | 67 ----------------------
 .../apache/sis/image/StatisticsCalculatorTest.java |  6 +-
 10 files changed, 131 insertions(+), 96 deletions(-)
 delete mode 100644 core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PreferredSize.java


[sis] 03/03: Hide the splash screen after application startup.

Posted by de...@apache.org.
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

commit 396816fd527b757baf60ae75769bbb76bc3be28d
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Aug 21 19:43:47 2020 +0200

    Hide the splash screen after application startup.
---
 .../src/main/java/org/apache/sis/gui/DataViewer.java           | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java
index c8479d1..a9ad25b 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/DataViewer.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
+import java.awt.SplashScreen;
 import javafx.application.Application;
 import javafx.application.Platform;
 import javafx.geometry.Rectangle2D;
@@ -74,7 +75,7 @@ public class DataViewer extends Application {
          * (tested on Java 14 and JavaFX 14).
          */
         java.awt.im.InputContext.getInstance();
-        launch(args);
+        launch(DataViewer.class, args);
     }
 
     /**
@@ -181,6 +182,13 @@ public class DataViewer extends Application {
         window.setWidth (0.75 * bounds.getWidth());
         window.setHeight(0.75 * bounds.getHeight());
         window.show();
+        /*
+         * Hide splash screen after the main window became visible.
+         */
+        final SplashScreen sp = SplashScreen.getSplashScreen();
+        if (sp != null) {
+            sp.close();
+        }
     }
 
     /**


[sis] 02/03: Rename `ImageProcessor.getStatistics(…)` as `valueOfStatistics(…)`. The intent is to make a clearer separation with property getter methods. It also reduces the impression that this method returns a pre-existing value (the computation can be expansive).

Posted by de...@apache.org.
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

commit 563cba79ab1135ff936a849f1bd50d9fef6bbc34
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Aug 21 16:15:34 2020 +0200

    Rename `ImageProcessor.getStatistics(…)` as `valueOfStatistics(…)`.
    The intent is to make a clearer separation with property getter methods.
    It also reduces the impression that this method returns a pre-existing value
    (the computation can be expansive).
---
 .../src/main/java/org/apache/sis/gui/coverage/RenderingData.java  | 2 +-
 .../src/main/java/org/apache/sis/internal/gui/ImageConverter.java | 2 +-
 .../src/main/java/org/apache/sis/image/ImageProcessor.java        | 8 ++++----
 .../src/main/java/org/apache/sis/image/RecoloredImage.java        | 2 +-
 .../src/main/java/org/apache/sis/image/Visualization.java         | 2 +-
 .../test/java/org/apache/sis/image/StatisticsCalculatorTest.java  | 6 +++---
 6 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
index b7b2f2a..111a22d 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
@@ -247,7 +247,7 @@ final class RenderingData implements Cloneable {
         if (selectedDerivative != Stretching.NONE) {
             final Map<String,Object> modifiers = new HashMap<>(4);
             if (statistics == null) {
-                statistics = processor.getStatistics(image, null);
+                statistics = processor.valueOfStatistics(image, null);
             }
             modifiers.put("statistics", statistics);
             if (selectedDerivative == Stretching.AUTOMATIC) {
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
index 24ce727..86a58d6 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImageConverter.java
@@ -126,7 +126,7 @@ final class ImageConverter extends Task<Statistics[]> {
         toCanvas.translate(-bounds.x, -bounds.y);
 
         final ImageProcessor processor  = new ImageProcessor();
-        final Statistics[]   statistics = processor.getStatistics(source, bounds);
+        final Statistics[]   statistics = processor.valueOfStatistics(source, bounds);
         final RenderedImage  image      = processor.stretchColorRamp(source, JDK9.mapOf("multStdDev", 3, "statistics", statistics));
         final RenderedImage  mask       = getMask(processor);
         final BufferedImage  buffer     = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
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 df2d310..ac69507 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
@@ -597,7 +597,7 @@ public class ImageProcessor implements Cloneable {
      * @see #statistics(RenderedImage, Shape)
      * @see StatisticsCalculator#STATISTICS_KEY
      */
-    public Statistics[] getStatistics(final RenderedImage source, final Shape areaOfInterest) {
+    public Statistics[] valueOfStatistics(final RenderedImage source, final Shape areaOfInterest) {
         ArgumentChecks.ensureNonNull("source", source);
         if (areaOfInterest == null) {
             final Object property = source.getProperty(StatisticsCalculator.STATISTICS_KEY);
@@ -619,7 +619,7 @@ public class ImageProcessor implements Cloneable {
          */
         final AnnotatedImage calculator = new StatisticsCalculator(source, areaOfInterest, parallel, failOnException);
         final Object property = calculator.getProperty(StatisticsCalculator.STATISTICS_KEY);
-        calculator.logAndClearError(ImageProcessor.class, "getStatistics", errorListener);
+        calculator.logAndClearError(ImageProcessor.class, "valueOfStatistics", errorListener);
         return (Statistics[]) property;
     }
 
@@ -649,7 +649,7 @@ public class ImageProcessor implements Cloneable {
      * @return an image with an {@value StatisticsCalculator#STATISTICS_KEY} property.
      *         May be {@code image} if the given argument already has a statistics property.
      *
-     * @see #getStatistics(RenderedImage, Shape)
+     * @see #valueOfStatistics(RenderedImage, Shape)
      * @see StatisticsCalculator#STATISTICS_KEY
      */
     public RenderedImage statistics(final RenderedImage source, final Shape areaOfInterest) {
@@ -674,7 +674,7 @@ public class ImageProcessor implements Cloneable {
      * mapped to their colors.
      *
      * <p>The minimum and maximum value can be either specified explicitly,
-     * or determined from {@linkplain #getStatistics(RenderedImage, Shape) statistics} on the image.
+     * or determined from {@link #valueOfStatistics(RenderedImage, Shape) statistics} on the image.
      * In the later case a range of value is determined first from the {@linkplain Statistics#minimum() minimum}
      * and {@linkplain Statistics#maximum() maximum} values found in the image, optionally narrowed to an interval
      * of some {@linkplain Statistics#standardDeviation(boolean) standard deviations} around the mean value.</p>
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
index bb5aafd..67eb19a 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
@@ -161,7 +161,7 @@ final class RecoloredImage extends ImageAdapter {
             if (statistics == null) {
                 if (statsAllBands == null) {
                     final Object areaOfInterest = modifiers.get("areaOfInterest");
-                    statsAllBands = processor.getStatistics(statsSource,
+                    statsAllBands = processor.valueOfStatistics(statsSource,
                             (areaOfInterest instanceof Shape) ? (Shape) areaOfInterest : null);
                 }
                 if (statsAllBands != null && visibleBand < statsAllBands.length) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
index 62ba955..807f3ba 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
@@ -290,7 +290,7 @@ final class Visualization extends ResampledImage {
              * If none of above Colorizer configurations worked, use statistics in last resort. We do that
              * after we reduced the image to a single band, in order to reduce the amount of calculations.
              */
-            final Statistics statistics = processor.getStatistics(source, null)[0];
+            final Statistics statistics = processor.valueOfStatistics(source, null)[0];
             colorizer.initialize(statistics.minimum(), statistics.maximum());
         }
         /*
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
index ea9f89b..a0110e4 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
@@ -95,7 +95,7 @@ public final strictfp class StatisticsCalculatorTest extends TestCase {
         operations.setExecutionMode(ImageProcessor.Mode.PARALLEL);
         final TiledImageMock image = createImage();
         final Statistics[] expected = computeSequentially(image);
-        final Statistics[] actual = operations.getStatistics(image, null);
+        final Statistics[] actual = operations.valueOfStatistics(image, null);
         for (int i=0; i<expected.length; i++) {
             final Statistics e = expected[i];
             final Statistics a = actual  [i];
@@ -116,7 +116,7 @@ public final strictfp class StatisticsCalculatorTest extends TestCase {
         final TiledImageMock image = createImage();
         image.failRandomly(new Random(-8739538736973900203L));
         try {
-            operations.getStatistics(image, null);
+            operations.valueOfStatistics(image, null);
             fail("Expected ImagingOpException.");
         } catch (ImagingOpException e) {
             final String message = e.getMessage();
@@ -136,7 +136,7 @@ public final strictfp class StatisticsCalculatorTest extends TestCase {
         operations.setErrorAction(ImageProcessor.ErrorAction.LOG);
         final TiledImageMock image = createImage();
         image.failRandomly(new Random(8004277484984714811L));
-        final Statistics[] stats = operations.getStatistics(image, null);
+        final Statistics[] stats = operations.valueOfStatistics(image, null);
         for (final Statistics a : stats) {
             assertTrue(a.count() > 0);
         }


[sis] 01/03: Replace the `PreferredSize` hack by a public API.

Posted by de...@apache.org.
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

commit c5ae48b39368dffc9f148f4cb632db0727c1cafc
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Aug 21 16:12:12 2020 +0200

    Replace the `PreferredSize` hack by a public API.
---
 .../org/apache/sis/gui/coverage/RenderingData.java |  6 +-
 .../java/org/apache/sis/image/ImageCombiner.java   |  2 +-
 .../java/org/apache/sis/image/ImageProcessor.java  | 56 ++++++++++++++++++
 .../sis/internal/coverage/j2d/ImageLayout.java     | 64 ++++++++++++++++-----
 .../sis/internal/coverage/j2d/PreferredSize.java   | 67 ----------------------
 5 files changed, 111 insertions(+), 84 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
index e5ca996..b7b2f2a 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
@@ -40,7 +40,6 @@ import org.apache.sis.geometry.Shapes2D;
 import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.internal.coverage.j2d.ColorModelType;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
-import org.apache.sis.internal.coverage.j2d.PreferredSize;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.math.Statistics;
 import org.apache.sis.measure.Quantities;
@@ -188,6 +187,7 @@ final class RenderingData implements Cloneable {
         selectedDerivative = Stretching.NONE;
         processor = new ImageProcessor();
         processor.setErrorAction(ImageProcessor.ErrorAction.LOG);
+        processor.setImageResizingPolicy(ImageProcessor.Resizing.EXPAND);
     }
 
     /**
@@ -314,9 +314,9 @@ final class RenderingData implements Cloneable {
         displayToObjective = AffineTransforms2D.castOrCopy(inverse);
         final MathTransform cornerToDisplay = MathTransforms.concatenate(cornerToObjective, objectiveToDisplay);
         final MathTransform displayToCenter = MathTransforms.concatenate(inverse, centerToObjective.inverse());
-        final PreferredSize bounds = (PreferredSize) Shapes2D.transform(
+        final Rectangle bounds = (Rectangle) Shapes2D.transform(
                 MathTransforms.bidimensional(cornerToDisplay),
-                ImageUtilities.getBounds(recoloredImage), new PreferredSize());
+                ImageUtilities.getBounds(recoloredImage), new Rectangle());
         /*
          * Apply a map projection on the image, then convert the floating point results to integer values
          * that we can use with IndexColorModel.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java
index c503e0e..3b0d49c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java
@@ -94,7 +94,7 @@ public class ImageCombiner implements Consumer<RenderedImage> {
 
         /** Creates a new layout which will request the specified sample model. */
         Layout(final SampleModel sampleModel) {
-            super(null);
+            super(null, false);
             ArgumentChecks.ensureNonNull("sampleModel", sampleModel);
             this.sampleModel = sampleModel;
             minTile = new Point();
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 eb95181..df2d310 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
@@ -66,6 +66,9 @@ import org.apache.sis.measure.Units;
  *     {@linkplain #setCategoryColors(Function) Category colors} for mapping sample values
  *     (identified by their range, name or unit of measurement) to colors.
  *   </li><li>
+ *     {@linkplain #setImageResizingPolicy(Resizing) Image resizing policy} to apply
+ *     if a requested image size prevent the image to be tiled.
+ *   </li><li>
  *     {@linkplain #setPositionalAccuracyHints(Quantity...) Positional accuracy hints}
  *     for enabling the use of faster algorithm when a lower accuracy is acceptable.
  *   </li><li>
@@ -154,6 +157,33 @@ public class ImageProcessor implements Cloneable {
     private ImageLayout layout;
 
     /**
+     * Whether {@code ImageProcessor} can produce an image of different size compared to requested size.
+     * An image may be resized if the requested size can not be subdivided into tiles of reasonable size.
+     * For example if the image width is a prime number, there is no way to divide the image horizontally with
+     * an integer number of tiles. The only way to get an integer number of tiles is to change the image size.
+     *
+     * <p>The image resizing policy may be used by any operation that involve a {@linkplain #resample resampling}.
+     * If a resizing is applied, the new size will be written in the {@code bounds} argument (a {@link Rectangle}).</p>
+     *
+     * @see #getImageResizingPolicy()
+     * @see #setImageResizingPolicy(Resizing)
+     */
+    public enum Resizing {
+        /**
+         * Image size is unmodified; the requested value is used unconditionally.
+         * It may result in big tiles (potentially a single tile for the whole image)
+         * if the image size is not divisible by a tile size.
+         */
+        NONE,
+
+        /**
+         * Image size can be increased. {@code ImageProcessor} will try to increase
+         * by the smallest amount of pixels allowing the image to be subdivided in tiles.
+         */
+        EXPAND
+    }
+
+    /**
      * Interpolation to use during resample operations.
      *
      * @see #getInterpolation()
@@ -382,6 +412,26 @@ public class ImageProcessor implements Cloneable {
     }
 
     /**
+     * Returns whether {@code ImageProcessor} can produce an image of different size compared to requested size.
+     * If this processor can use a different size, the enumeration value specifies what kind of changes may be applied.
+     *
+     * @return the image resizing policy.
+     */
+    public synchronized Resizing getImageResizingPolicy() {
+        return layout.isBoundsAdjustmentAllowed ? Resizing.EXPAND : Resizing.NONE;
+    }
+
+    /**
+     * Sets whether {@code ImageProcessor} can produce an image of different size compared to requested size.
+     *
+     * @param  policy   the new image resizing policy.
+     */
+    public synchronized void setImageResizingPolicy(final Resizing policy) {
+        ArgumentChecks.ensureNonNull("policy", policy);
+        layout = (policy == Resizing.EXPAND) ? ImageLayout.SIZE_ADJUST : ImageLayout.DEFAULT;
+    }
+
+    /**
      * Returns hints about the desired positional accuracy, in "real world" units or in pixel units.
      * This is an empty array by default, which means that {@code ImageProcessor} aims for the best
      * accuracy it can produce. If the returned array is non-empty and contains accuracies large enough,
@@ -787,12 +837,15 @@ public class ImageProcessor implements Cloneable {
      * <ul>
      *   <li>{@linkplain #getInterpolation() Interpolation method} (nearest neighbor, bilinear, <i>etc</i>).</li>
      *   <li>{@linkplain #getFillValues() Fill values} for pixels outside source image.</li>
+     *   <li>{@linkplain #getImageResizingPolicy() Image resizing policy} to apply
+     *       if {@code bounds} size is not divisible by a tile size.</li>
      *   <li>{@linkplain #getPositionalAccuracyHints() Positional accuracy hints}
      *       for enabling faster resampling at the cost of lower precision.</li>
      * </ul>
      *
      * @param  source    the image to be resampled.
      * @param  bounds    domain of pixel coordinates of resampled image to create.
+     *                   Updated by this method if {@link Resizing#EXPAND} policy is applied.
      * @param  toSource  conversion of pixel coordinates from resampled image to {@code source} image.
      * @return resampled image (may be {@code source}).
      */
@@ -983,6 +1036,8 @@ public class ImageProcessor implements Cloneable {
      * <ul>
      *   <li>{@linkplain #getInterpolation() Interpolation method} (nearest neighbor, bilinear, <i>etc</i>).</li>
      *   <li>{@linkplain #getFillValues() Fill values} for pixels outside source image.</li>
+     *   <li>{@linkplain #getImageResizingPolicy() Image resizing policy} to apply
+     *       if {@code bounds} size is not divisible by a tile size.</li>
      *   <li>{@linkplain #getPositionalAccuracyHints() Positional accuracy hints}
      *       for enabling faster resampling at the cost of lower precision.</li>
      *   <li>{@linkplain #getCategoryColors() Category colors}.</li>
@@ -990,6 +1045,7 @@ public class ImageProcessor implements Cloneable {
      *
      * @param  source    the image to be resampled and recolored.
      * @param  bounds    domain of pixel coordinates of resampled image to create.
+     *                   Updated by this method if {@link Resizing#EXPAND} policy is applied.
      * @param  toSource  conversion of pixel coordinates from resampled image to {@code source} image.
      * @param  ranges    description of {@code source} bands, or {@code null} if none. This is typically
      *                   obtained by {@link org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()}.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
index 449b1de..dd1656a 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
@@ -55,7 +55,12 @@ public class ImageLayout {
      * The default instance which will target {@value ImageUtilities#DEFAULT_TILE_SIZE} pixels as tile
      * width and height.
      */
-    public static final ImageLayout DEFAULT = new ImageLayout(null);
+    public static final ImageLayout DEFAULT = new ImageLayout(null, false);
+
+    /**
+     * Same as {@link #DEFAULT}, but makes image size an integer amount of tiles.
+     */
+    public static final ImageLayout SIZE_ADJUST = new ImageLayout(null, true);
 
     /**
      * Preferred size for tiles.
@@ -65,11 +70,23 @@ public class ImageLayout {
     private final int preferredTileWidth, preferredTileHeight;
 
     /**
+     * Whether image size can be modified if needed. Changes are applied only if an image can not be tiled
+     * because {@link #suggestTileSize(int, int, boolean)} can not find a tile size close to the desired size.
+     * For example if the image width is a prime number, there is no way to divide the image horizontally with
+     * an integer number of tiles. The only way to get an integer number of tiles is to change the image size.
+     *
+     * <p>If this flag is {@code true}, then the {@code bounds} argument given to the
+     * {@link #suggestTileSize(RenderedImage, Rectangle, boolean)} will be modified in-place.</p>
+     */
+    public final boolean isBoundsAdjustmentAllowed;
+
+    /**
      * Creates a new image layout.
      *
-     * @param  preferredTileSize  the preferred tile size, or {@code null} for the default size.
+     * @param  preferredTileSize          the preferred tile size, or {@code null} for the default size.
+     * @param  isBoundsAdjustmentAllowed  whether image size can be modified if needed.
      */
-    public ImageLayout(final Dimension preferredTileSize) {
+    public ImageLayout(final Dimension preferredTileSize, final boolean isBoundsAdjustmentAllowed) {
         if (preferredTileSize != null) {
             preferredTileWidth  = preferredTileSize.width;
             preferredTileHeight = preferredTileSize.height;
@@ -77,6 +94,7 @@ public class ImageLayout {
             preferredTileWidth  = ImageUtilities.DEFAULT_TILE_SIZE;
             preferredTileHeight = ImageUtilities.DEFAULT_TILE_SIZE;
         }
+        this.isBoundsAdjustmentAllowed = isBoundsAdjustmentAllowed;
     }
 
     /**
@@ -189,11 +207,12 @@ public class ImageLayout {
      *
      * @param  image   the image for which to derive a tile size, or {@code null}.
      * @param  bounds  the bounds of the image to create, or {@code null} if same as {@code image}.
+     * @param  allowPartialTiles  whether to allow tiles that are only partially filled.
+     *         This argument is ignored (reset to {@code false}) if the given image is opaque.
      * @return suggested tile size for the given image.
      */
-    public Dimension suggestTileSize(final RenderedImage image, final Rectangle bounds) {
-        boolean allowPartialTiles = (bounds instanceof PreferredSize);
-        if (allowPartialTiles && image != null) {
+    public Dimension suggestTileSize(final RenderedImage image, final Rectangle bounds, boolean allowPartialTiles) {
+        if (allowPartialTiles && image != null && !isBoundsAdjustmentAllowed) {
             final ColorModel cm = image.getColorModel();
             allowPartialTiles = (cm != null);
             if (allowPartialTiles) {
@@ -228,15 +247,33 @@ public class ImageLayout {
         final Dimension tileSize = new Dimension(
                 toTileSize(width,  preferredTileWidth,  allowPartialTiles & singleXTile),
                 toTileSize(height, preferredTileHeight, allowPartialTiles & singleYTile));
-        if (allowPartialTiles) {
-            ((PreferredSize) bounds).makeDivisible(tileSize);
+        /*
+         * Optionally adjust the image bounds for making it divisible by the tile size.
+         */
+        if (isBoundsAdjustmentAllowed && bounds != null && !bounds.isEmpty()) {
+            final int sx = sizeToAdd(bounds.width,  tileSize.width);
+            final int sy = sizeToAdd(bounds.height, tileSize.height);
+            if ((bounds.width  += sx) < 0) bounds.width  -= tileSize.width;     // if (overflow) reduce to valid range.
+            if ((bounds.height += sy) < 0) bounds.height -= tileSize.height;
+            bounds.translate(-sx/2, -sy/2);
         }
         return tileSize;
     }
 
     /**
-     * Creates a banded sample model of the given type with a {@linkplain #suggestTileSize(RenderedImage, Rectangle)
-     * the suggested size} for the given image.
+     * Computes the size to add to the width or height for making it divisible by the given tile size.
+     */
+    private static int sizeToAdd(int size, final int tileSize) {
+        size %= tileSize;
+        if (size != 0) {
+            size = tileSize - size;
+        }
+        return size;
+    }
+
+    /**
+     * Creates a banded sample model of the given type with
+     * {@linkplain #suggestTileSize(RenderedImage, Rectangle, boolean) the suggested tile size} for the given image.
      *
      * @param  type      desired data type as a {@link java.awt.image.DataBuffer} constant.
      * @param  numBands  desired number of bands.
@@ -247,7 +284,7 @@ public class ImageLayout {
     public BandedSampleModel createBandedSampleModel(final int type, final int numBands,
             final RenderedImage image, final Rectangle bounds)
     {
-        final Dimension tile = suggestTileSize(image, bounds);
+        final Dimension tile = suggestTileSize(image, bounds, isBoundsAdjustmentAllowed);
         return RasterFactory.unique(new BandedSampleModel(type, tile.width, tile.height, numBands));
     }
 
@@ -265,7 +302,7 @@ public class ImageLayout {
      */
     public SampleModel createCompatibleSampleModel(final RenderedImage image, final Rectangle bounds) {
         ArgumentChecks.ensureNonNull("image", image);
-        final Dimension tile = suggestTileSize(image, bounds);
+        final Dimension tile = suggestTileSize(image, bounds, isBoundsAdjustmentAllowed);
         SampleModel sm = image.getSampleModel();
         if (sm.getWidth() != tile.width || sm.getHeight() != tile.height) {
             sm = sm.createCompatibleSampleModel(tile.width, tile.height);
@@ -292,6 +329,7 @@ public class ImageLayout {
     @Override
     public String toString() {
         return Strings.toString(getClass(),
-                "preferredTileSize", new StringBuilder().append(preferredTileWidth).append('×').append(preferredTileHeight));
+                "preferredTileSize", new StringBuilder().append(preferredTileWidth).append('×').append(preferredTileHeight),
+                "isBoundsAdjustmentAllowed", isBoundsAdjustmentAllowed);
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PreferredSize.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PreferredSize.java
deleted file mode 100644
index 06f564d..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PreferredSize.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.internal.coverage.j2d;
-
-import java.awt.Dimension;
-import java.awt.Rectangle;
-
-
-/**
- * Specifies an image size which can be modified by {@link ImageLayout} if needed. Changes are applied only if
- * an image can not be tiled because {@link ImageLayout} can not find a tile size close to the desired size.
- * For example if the image width is a prime number, there is no way to divide the image horizontally with
- * an integer number of tiles. The only way to get an integer number of tiles is to change the image size.
- * The use of this class is understood by {@link ImageLayout} as a permission to do so.
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
- * @since   1.1
- * @module
- */
-@SuppressWarnings("serial")     // Not intended to be serialized.
-public final class PreferredSize extends Rectangle {
-    /**
-     * Creates a new rectangle.
-     */
-    public PreferredSize() {
-    }
-
-    /**
-     * Adjusts the bounds for making it divisible by the given tile size.
-     */
-    final void makeDivisible(final Dimension tileSize) {
-        if (!isEmpty()) {
-            final int sx = sizeToAdd(width,   tileSize.width);
-            final int sy = sizeToAdd(height,  tileSize.height);
-            if ((width  += sx) < 0) width  -= tileSize.width;       // if (overflow) reduce to valid range.
-            if ((height += sy) < 0) height -= tileSize.height;
-            if (x < (x -= sx/2)) x = Integer.MIN_VALUE;             // if (overflow) set to minimal value.
-            if (y < (y -= sy/2)) y = Integer.MIN_VALUE;
-        }
-    }
-
-    /**
-     * Computes the size to add to the width or height for making it divisible by the given tile size.
-     */
-    private static int sizeToAdd(int size, final int tileSize) {
-        size %= tileSize;
-        if (size != 0) {
-            size = tileSize - size;
-        }
-        return size;
-    }
-}