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 0a53a466e34dcb3ab390f4f1313aff3408215985
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Feb 28 12:17:02 2025 +0100

    Add a safety against singular matrices read from GeoTIFF files.
---
 .../sis/referencing/operation/matrix/Matrices.java | 62 ++++++++++++++++++----
 .../referencing/operation/matrix/MatricesTest.java | 16 ++++++
 .../geotiff/reader/GridGeometryBuilder.java        | 13 ++++-
 .../sis/storage/geotiff/writer/GeoEncoder.java     |  6 ++-
 4 files changed, 84 insertions(+), 13 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
index f058efe7bf..85960cec2e 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
@@ -75,7 +75,7 @@ import org.opengis.coordinate.MismatchedDimensionException;
  * </ul>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @see org.apache.sis.parameter.TensorParameters
  *
@@ -891,6 +891,46 @@ public final class Matrices extends Static {
         return changed;
     }
 
+    /**
+     * Forces the matrix to have at least one non-zero coefficient in every 
row, assuming an affine transform.
+     * The last column (the translation terms) and the last row (the [0 0 … 1] 
terms) are ignored.
+     * If a row contains only zero values (ignoring the translation term),
+     * then this method sets one element of that row to the given {@code 
defaultValue}.
+     * That modification occurs in the first free column, i.e. a column having 
no non-zero value.
+     * If no such free column is found, then this method stops the operation 
and returns {@code false}.
+     *
+     * @param  matrix        the matrix in which to force non-zero scale 
factors. Will be modified in-place.
+     * @param  defaultValue  the scale factor to assign to rows that do not 
have a non-zero scale factor.
+     * @return {@code true} on success (including when this method does 
nothing), or
+     *         {@code false} if this method cannot complete the operation.
+     *
+     * @since 1.5
+     */
+    public static boolean forceNonZeroScales(final Matrix matrix, final double 
defaultValue) {
+        final int numCol = matrix.getNumCol() - 1;
+        final int numRow = matrix.getNumRow() - 1;
+        int freeColumn = 0;
+okay:   for (int j=0; j<numRow; j++) {
+            for (int i=0; i<numCol; i++) {
+                if (matrix.getElement(j, i) != 0) {
+                    continue okay;
+                }
+            }
+search:     while (freeColumn < numCol) {
+                for (int i=0; i<numRow; i++) {
+                    if (matrix.getElement(i, freeColumn) != 0) {
+                        freeColumn++;
+                        continue search;
+                    }
+                }
+                matrix.setElement(j, freeColumn++, defaultValue);
+                continue okay;
+            }
+            return false;
+        }
+        return true;
+    }
+
     /**
      * Returns {@code true} if the given matrix is likely to use extended 
precision.
      * A value of {@code true} is not a guarantee that the matrix uses 
extended precision,
@@ -1000,7 +1040,7 @@ public final class Matrices extends Static {
         if (numRow == numCol) {
             return Solver.inverse(matrix);
         }
-        final NonSquareMatrix result = new NonSquareMatrix(numRow, numCol, 
false);
+        final var result = new NonSquareMatrix(numRow, numCol, false);
         result.setMatrix(matrix);
         return result.inverse();
     }
@@ -1221,13 +1261,13 @@ public final class Matrices extends Static {
     public static String toString(final Matrix matrix) {
         final int numRow = matrix.getNumRow();
         final int numCol = matrix.getNumCol();
-        final String[]  elements            = new String [numCol * numRow];    
 // String representation of matrix values.
-        final boolean[] noFractionDigits    = new boolean[numCol * numRow];    
 // Whether to remove the trailing ".0" for a given number.
-        final boolean[] hasDecimalSeparator = new boolean[numCol];             
 // Whether the column has at least one number where fraction digits are shown.
-        final byte[] maximumFractionDigits  = new byte   [numCol];             
 // The greatest number of fraction digits found in a column.
-        final byte[] maximumPaddingZeros    = new byte   [numCol * numRow];    
 // Maximal number of zeros that we can append before to exceed the IEEE 754 
accuracy.
-        final byte[] widthBeforeFraction    = new byte   [numCol];             
 // Number of characters before the fraction digits: spacing + ('-') + 
integerDigits + '.'
-        final byte[] columnWidth            = new byte   [numCol];             
 // Total column width.
+        final var elements              = new String [numCol * numRow];     // 
String representation of matrix values.
+        final var noFractionDigits      = new boolean[numCol * numRow];     // 
Whether to remove the trailing ".0" for a given number.
+        final var hasDecimalSeparator   = new boolean[numCol];              // 
Whether the column has at least one number where fraction digits are shown.
+        final var maximumFractionDigits = new byte   [numCol];              // 
The greatest number of fraction digits found in a column.
+        final var maximumPaddingZeros   = new byte   [numCol * numRow];     // 
Maximal number of zeros that we can append before to exceed the IEEE 754 
accuracy.
+        final var widthBeforeFraction   = new byte   [numCol];              // 
Number of characters before the fraction digits: spacing + ('-') + 
integerDigits + '.'
+        final var columnWidth           = new byte   [numCol];              // 
Total column width.
         int totalWidth = 1;
         /*
          * Create now the string representation of all matrix elements and 
measure the width
@@ -1290,9 +1330,9 @@ public final class Matrices extends Static {
          * Now append the formatted elements with the appropriate number of 
spaces before each value,
          * and trailling zeros after each value except ±0, ±1, NaN and 
infinities.
          */
-        final String   lineSeparator = System.lineSeparator();
+        final String lineSeparator = System.lineSeparator();
         final CharSequence whiteLine = CharSequences.spaces(totalWidth);
-        final StringBuilder   buffer = new StringBuilder((totalWidth + 2 + 
lineSeparator.length()) * (numRow + 2));
+        final var buffer = new StringBuilder((totalWidth + 2 + 
lineSeparator.length()) * (numRow + 2));
         buffer.append('┌').append(whiteLine).append('┐').append(lineSeparator);
         int flatIndex = 0;
         for (int j=0; j<numRow; j++) {
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java
index 29b28f520f..252ae8862c 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java
@@ -442,6 +442,22 @@ public final class MatricesTest extends TestCase {
         }), matrix);
     }
 
+    /**
+     * Tests {@link Matrices#forceNonZeroScales(Matrix, double)}.
+     */
+    public void testForceNonZeroScales() {
+        MatrixSIS matrix = Matrices.create(4, 4, new double[] {
+            2, 0, 0, 8,
+            0, 0, 4, 7,
+            0, 0, 0, 6,
+            0, 0, 0, 1
+        });
+        MatrixSIS expected = matrix.clone();
+        expected.setElement(2, 1, 3);
+        assertTrue(Matrices.forceNonZeroScales(matrix, 3));
+        assertEquals(expected, matrix);
+    }
+
     /**
      * Tests {@link Matrices#equals(Matrix, Matrix, double, boolean)}.
      */
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java
index bb64c6df01..7a7366e899 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java
@@ -25,6 +25,7 @@ import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.parameter.ParameterNotFoundException;
 import org.opengis.referencing.NoSuchAuthorityCodeException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.TransformException;
@@ -76,6 +77,14 @@ import org.apache.sis.math.Vector;
  * @author  Martin Desruisseaux (Geomatys)
  */
 public final class GridGeometryBuilder extends GeoKeysLoader {
+    /**
+     * Default scale factory to apply if a row in the model transformation 
contains only zero values.
+     * The matrix in a GeoTIFF file is always of size 4×4 even if the 
<abbr>CRS</abbr> is two-dimensional.
+     * In the latter case, the matrix row for the third dimension has only 
zero values. That row should be
+     * discarded when building the final {@link GridGeometry}, but there is 
sometime inconsistency between
+     * the number of <abbr>CRS</abbr> dimensions and which matrix rows have 
been assigned non-zero values.
+     */
+    private static final double DEFAULT_SCALE_FACTOR = 1;
 
     //  
╔════════════════════════════════════════════════════════════════════════════════╗
     //  ║                                                                      
          ║
@@ -300,7 +309,9 @@ public final class GridGeometryBuilder extends 
GeoKeysLoader {
         try {
             MathTransform gridToCRS = null;
             if (affine != null) {
-                gridToCRS = 
factory.createAffineTransform(Matrices.resizeAffine(affine, ++n, n));
+                final Matrix m = Matrices.resizeAffine(affine, ++n, n);
+                Matrices.forceNonZeroScales(m, DEFAULT_SCALE_FACTOR);
+                gridToCRS = factory.createAffineTransform(m);
             } else if (modelTiePoints != null) {
                 pixelIsPoint = true;
                 gridToCRS = Localization.nonLinear(modelTiePoints);
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
index fc61bfaef5..a0d4db0378 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
@@ -97,7 +97,11 @@ public final class GeoEncoder {
 
     /**
      * Size of the model transformation matrix, in number of rows and columns.
-     * This size is fixed by the GeoTIFF specification.
+     * This size is fixed by the GeoTIFF specification. If some rows need to
+     * be added for filling the space, they will be filled with zero values
+     * (i.e., the matrix will be singular until the reader removes those rows).
+     *
+     * @see 
org.apache.sis.storage.geotiff.reader.GridGeometryBuilder#DEFAULT_SCALE_FACTOR
      */
     private static final int MATRIX_SIZE = 4;
 

Reply via email to