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 b925009bef9fedeac029dcee5a53ab022d150935 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Feb 26 15:29:00 2025 +0100 Make color model and sample model builders more flexible in preparation for GeoHEIF reader. --- .../main/org/apache/sis/image/DataType.java | 5 +- .../main/org/apache/sis/image/MaskImage.java | 2 +- .../main/org/apache/sis/image/RecoloredImage.java | 3 +- .../apache/sis/image/privy/ColorModelBuilder.java | 309 ++++++++++++++------- .../apache/sis/image/privy/ColorModelFactory.java | 35 ++- .../apache/sis/image/privy/SampleModelBuilder.java | 35 ++- .../org/apache/sis/image/BandSelectImageTest.java | 2 +- .../sis/image/privy/SampleModelBuilderTest.java | 22 +- .../sis/storage/geotiff/ImageFileDirectory.java | 9 +- .../org/apache/sis/storage/AbstractResource.java | 10 +- .../apache/sis/storage/base/TiledGridCoverage.java | 6 +- .../org/apache/sis/storage/esri/RasterStore.java | 2 +- .../org/apache/sis/storage/gdal/TiledResource.java | 2 +- 13 files changed, 302 insertions(+), 140 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/DataType.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/DataType.java index 9b4fa8d71a..4ea76572c5 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/DataType.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/DataType.java @@ -338,14 +338,13 @@ public enum DataType { /** * Returns the size in bytes of this data type. - * If the {@linkplain #size() number of bits} is smaller than {@value Byte#SIZE}, then this method returns 1. * - * @return size in bytes of this data type, not smaller than 1. + * @return size in bytes of this data type, from 1 to 4 inclusive. * * @since 1.3 */ public final int bytes() { - return Math.max(size() >>> 3, 1); + return size() >>> 3; // `size()` is never smaller than 8. } /** diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/MaskImage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/MaskImage.java index 18506461dd..ba6ea1262b 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/MaskImage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/MaskImage.java @@ -48,7 +48,7 @@ final class MaskImage extends SourceAlignedImage { * Creates a new instance for the given image. */ MaskImage(final ResampledImage image) { - super(image, ColorModelFactory.createIndexColorModel( + super(image, ColorModelFactory.createIndexColorModel(null, 0, 1, Math.max(0, ImageUtilities.getVisibleBand(image)), new int[] {0, -1}, true, 0)); MathTransform converter = null; diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/RecoloredImage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/RecoloredImage.java index ca0297e3c4..7d5b8742b2 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/RecoloredImage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/RecoloredImage.java @@ -342,7 +342,8 @@ final class RecoloredImage extends ImageAdapter { ARGB[i] = icm.getRGB(Math.round((i - start) * scale) + validMin); } final SampleModel sm = source.getSampleModel(); - cm = ColorModelFactory.createIndexColorModel(sm.getNumBands(), visibleBand, ARGB, icm.hasAlpha(), icm.getTransparentPixel()); + cm = ColorModelFactory.createIndexColorModel(null, 0, sm.getNumBands(), + visibleBand, ARGB, icm.hasAlpha(), icm.getTransparentPixel()); } else { /* * Wraps the given image with its colors ramp scaled between the given bounds. If the given image is diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelBuilder.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelBuilder.java index 89988e5d4d..830b8a9bee 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelBuilder.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelBuilder.java @@ -16,12 +16,10 @@ */ package org.apache.sis.image.privy; -import java.util.Arrays; import java.awt.Transparency; import java.awt.color.ColorSpace; import java.awt.image.ColorModel; import java.awt.image.DirectColorModel; -import java.awt.image.BandedSampleModel; import java.awt.image.ComponentColorModel; import java.awt.image.SinglePixelPackedSampleModel; import java.awt.image.DataBuffer; @@ -29,6 +27,7 @@ import java.awt.image.SampleModel; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.ArraysExt; import org.apache.sis.image.DataType; +import org.apache.sis.measure.NumberRange; /** @@ -48,21 +47,69 @@ import org.apache.sis.image.DataType; */ public final class ColorModelBuilder { /** - * The usual position of the alpha channel. - * This channel is usually immediately after the <abbr>RGB</abbr> components. + * Sentinel value for saying that {@link #alphaBand} has not been set. + * This is different than setting {@code alphaBand} to -1, which means "no alpha". */ - private static final int STANDARD_ALPHA_BAND = 3; + private static final int UNSPECIFIED_ALPHA = -2; + + /** + * The color space of the color model to build. + * The default value is a <abbr>RGB</abbr> color space. + * + * <h4>Limitation</h4> + * Note that {@link #createPackedRGB()} is limited to color spaces of the <abbr>RGB</abbr> family. + * This is a restriction of {@link DirectColorModel}. + */ + private final ColorSpace colorSpace; + + /** + * The data type, or {@code null} for automatic. + * + * @see #dataType(DataType) + * @see #dataType(int) + */ + private DataType dataType; /** * Number of bits per sample, or {@code null} for the default values in all bands. * The array length is the number of bands: 3 for <abbr>RGB</abbr> or 4 for <abbr>ARGB</abbr>. * Each array element may be 0, which means to use the default for that specific band. * If no number of bits is specified, the default is {@value Byte#SIZE}. + * + * @see #bitsPerSample(int[]) + * @see #getBitsPerSample(int, int) */ private int[] bitsPerSample; /** - * Index of the alpha channel (usually the last band), or -1 if none. + * Alternative to {@link #bitsPerSample} when all samples use the same number of bits. + * Stored separately because the number of bands may not be known. + * The default value is {@value Byte#SIZE}. + * + * @see #bitsPerSample(int) + * @see #getBitsPerSample(int, int) + */ + private int defaultBitsPerSample; + + /** + * The red, green, blue and alpha masks, in that order. This is used only for {@link DirectColorModel}, + * where all sample values are packed in a single integer value. + * + * @see #componentMasks(int...) + */ + private int[] componentMasks; + + /** + * The band to show in the case of gray scale or indexed color model. + * The default value is {@value ColorModelFactory#DEFAULT_VISIBLE_BAND}. + * + * @see #visibleBand(int) + * @see ColorModelFactory#DEFAULT_VISIBLE_BAND + */ + private int visibleBand; + + /** + * Index of the alpha channel (usually the last band), or negative if none. */ private int alphaBand; @@ -72,10 +119,39 @@ public final class ColorModelBuilder { private boolean isAlphaPremultiplied; /** - * Creates a new builder initialized with default values. + * Range of the sample values in the {@link #visibleBand}, or {@code null} if unknown. + * This is used for the gray scale fallback if the <abbr>ARGN</abbr> color model cannot be created. + */ + private NumberRange<?> sampleValuesRange; + + /** + * Creates a new builder initialized with default values for an <abbr>RGB</abbr> color space. */ public ColorModelBuilder() { - alphaBand = -1; + this(true); + } + + /** + * Creates a new builder initialized with default values. + * + * @param isRGB {@code true} for an <abbr>RGB</abbr> color space, or {@code false} for gray scale. + */ + public ColorModelBuilder(final boolean isRGB) { + colorSpace = ColorSpace.getInstance(isRGB ? ColorSpace.CS_sRGB : ColorSpace.CS_GRAY); + defaultBitsPerSample = Byte.SIZE; + alphaBand = UNSPECIFIED_ALPHA; + } + + /** + * Sets the type of data. If this method is not invoked, + * the default is to determine the type automatically from the number of bits. + * + * @param type the data type to use, or {@code null} for automatic. + * @return {@code this} for method calls chaining. + */ + public ColorModelBuilder dataType(final DataType type) { + dataType = type; + return this; } /** @@ -88,9 +164,15 @@ public final class ColorModelBuilder { * @return {@code this} for method calls chaining. */ public ColorModelBuilder bitsPerSample(final int[] numBits) { - bitsPerSample = numBits; - if (hasDefaultBitsPerSample()) { - bitsPerSample = null; + defaultBitsPerSample = Byte.SIZE; + bitsPerSample = null; + if (numBits != null) { + for (int n : numBits) { + if (n != Byte.SIZE) { + bitsPerSample = numBits; + break; + } + } } return this; } @@ -105,29 +187,11 @@ public final class ColorModelBuilder { */ public ColorModelBuilder bitsPerSample(final int numBits) { ArgumentChecks.ensureBetween("bitsPerSample", 1, Integer.SIZE, numBits); - if (numBits != Byte.SIZE) { - bitsPerSample = new int[STANDARD_ALPHA_BAND + 1]; - Arrays.fill(bitsPerSample, numBits); - } else { - bitsPerSample = null; - } + defaultBitsPerSample = numBits; + bitsPerSample = null; return this; } - /** - * Returns whether this builder currently uses the default number of bits per sample. - */ - private boolean hasDefaultBitsPerSample() { - if (bitsPerSample != null) { - for (int numBits : bitsPerSample) { - if (numBits != Byte.SIZE) { - return false; - } - } - } - return true; - } - /** * Returns the number of bits per sample in the given band. * If no number of bits per sample was specified, the default value is {@link Byte#SIZE}. @@ -145,7 +209,48 @@ public final class ColorModelBuilder { return numBits; } } - return Byte.SIZE; + return defaultBitsPerSample; + } + + /** + * Sets the red, green, blue and alpha masks, in that order. Those masks are used only for + * {@link DirectColorModel}, where all sample values are packed in a single integer value. + * Trailing zeros are trimmed, as a convenience for setting the mask of the alpha band to + * zero when there is no alpha band. + * + * <p>The given array may be stored without copy on the assumption that is will not be modified + * (this is okay for internal API). If no mask is specified, the default masks will be computed + * from the {@code bitsPerSample} values.</p> + * + * @param masks the red, green, blue and alpha masks (in that order), or {@code null} for defaults. + * @return {@code this} for method calls chaining. + */ + public ColorModelBuilder componentMasks(int... masks) { + if (masks != null) { + for (int i = masks.length; --i >= 0;) { + if (masks[i] != 0) { + masks = ArraysExt.resize(masks, i+1); + break; + } + } + } + componentMasks = masks; + return this; + } + + /** + * Sets the band to show in the case of gray scale or indexed color model. + * The default value is {@value ColorModelFactory#DEFAULT_VISIBLE_BAND}. + * + * @param index of the alpha channel (usually the first band). + * @param range range of the sample values, or {@code null} if unknown. + * @return {@code this} for method calls chaining. + */ + public ColorModelBuilder visibleBand(final int index, final NumberRange<?> range) { + ArgumentChecks.ensurePositive("visibleBand", index); + visibleBand = index; + sampleValuesRange = range; + return this; } /** @@ -156,10 +261,7 @@ public final class ColorModelBuilder { * @return {@code this} for method calls chaining. */ public ColorModelBuilder alphaBand(final int index) { - if (index >= 0) { - ArgumentChecks.ensureBetween("alphaBand", 0, STANDARD_ALPHA_BAND, index); - } - alphaBand = index; + alphaBand = Math.max(index, UNSPECIFIED_ALPHA + 1); return this; } @@ -169,28 +271,44 @@ public final class ColorModelBuilder { * @param p whether the alpha value (if present) is premultiplied. * @return {@code this} for method calls chaining. */ - public ColorModelBuilder isAlphaPremultiplied(final boolean p) { + public ColorModelBuilder alphaPremultiplied(final boolean p) { isAlphaPremultiplied = p; return this; } /** - * Returns the color space of the color model to build. - * The current implementation fixes the color space to <abbr>RGB</abbr>, - * but it may change in a future implementation if needed. + * Returns the given array, or an empty array if the given array is null. + */ + private static int[] orEmpty(final int[] numBits) { + return (numBits != null) ? numBits : ArraysExt.EMPTY_INT; + } + + /** + * Returns the number of bands. If the user provided no indication, + * a default value suitable for <abbr>RGB(A)</abbr> colors is returned. * - * <h4>Limitation</h4> - * Note that {@link #createPackedRGB()} is limited to color spaces of the <abbr>RGB</abbr> family. - * This is a restriction of {@link DirectColorModel}. + * @return the number of bands. */ - private static ColorSpace colorSpace() { - return ColorSpace.getInstance(ColorSpace.CS_sRGB); + private int numBands() { + int numBands = Math.max(orEmpty(bitsPerSample).length, orEmpty(componentMasks).length); + if (numBands == 0) { + numBands = colorSpace.getNumComponents(); + if (alphaBand >= 0) { + ArgumentChecks.ensureBetween("alphaBand", 0, numBands, alphaBand); + numBands++; + } + } + return numBands; } /** * Returns the data type to use for the given number of bits. + * The number of bits is ignored if the user explicitly specified a data type. */ - private static int dataType(final int numBits) { + private int dataType(final int numBits) { + if (dataType != null) { + return dataType.toDataBufferType(); + } return (numBits <= Byte.SIZE) ? DataBuffer.TYPE_BYTE : (numBits <= Short.SIZE) ? DataBuffer.TYPE_USHORT : DataBuffer.TYPE_INT; } @@ -207,98 +325,103 @@ public final class ColorModelBuilder { * @throws IllegalArgumentException if any argument specified to the builder is invalid. */ public ColorModel createPackedRGB() { - if (!isAlphaPremultiplied && alphaBand == STANDARD_ALPHA_BAND && hasDefaultBitsPerSample()) { - return ColorModel.getRGBdefault(); - } - // Red, Green, Blue, Alpha masks in that order. - final int[] masks = new int[STANDARD_ALPHA_BAND + 1]; int numBits = 0; - for (int i=STANDARD_ALPHA_BAND - 1; i >= -1; i--) { - int band = i; - if (band < 0) { - band = alphaBand; - if (band < 0) break; + int[] masks = componentMasks; // Red, Green, Blue, Alpha masks in that order. + if (masks == null) { + final int numBands = numBands(); + if (numBands == 4 && alphaBand == 3 && !isAlphaPremultiplied) { + return ColorModel.getRGBdefault(); // Shared instance. + } + masks = new int[numBands]; + for (int i=numBands; --i >= -1;) { + if (i != alphaBand) { + int band = i; + if (band < 0) { + band = alphaBand; + if (band < 0) break; + } + int n = getBitsPerSample(band, Math.min(Byte.SIZE, Integer.SIZE - numBits)); + masks[i] = ((1 << n) - 1) << numBits; + numBits += n; + } } - int n = getBitsPerSample(band, Math.min(Byte.SIZE, Integer.SIZE - numBits)); - masks[i] = ((1 << n) - 1) << numBits; - numBits += n; + } else { + for (int i = masks.length; --i >= 0;) numBits |= masks[i]; + numBits = Integer.SIZE - Integer.numberOfLeadingZeros(numBits); } - return ColorModelFactory.unique(new DirectColorModel(colorSpace(), - numBits, masks[0], masks[1], masks[2], masks[3], isAlphaPremultiplied, dataType(numBits))); + return ColorModelFactory.unique(new DirectColorModel(colorSpace, numBits, + masks[0], masks[1], masks[2], masks[3], isAlphaPremultiplied, dataType(numBits))); } /** - * Creates a RGB color model for use with {@link BandedSampleModel}. + * Creates a RGB color model for use with {@link ComponentSampleModel}. * Each color component (sample value) is stored in a separated data element. + * Note that "banded" is taken in a loose sense here, as the data do not need + * to be stored in separated arrays as long as the components are distinct elements. * * <h4>Limitations</h4> * The current version requires the alpha channel (if present) to be the last band. * If this condition is not met, this method returns {@code null}. * - * @return color model for use with {@link java.awt.image.BandedSampleModel}. + * @return color model for use with {@link ComponentSampleModel}. * @throws IllegalArgumentException if any argument specified to the builder is invalid. */ public ColorModel createBandedRGB() { - final int numBands; - final int transparency; + final int numBands = numBands(); final boolean hasAlpha = (alphaBand >= 0); - if (hasAlpha) { - if (alphaBand != STANDARD_ALPHA_BAND) { - throw new IllegalArgumentException("Alpha channel must be after the color components."); - } - numBands = 4; - transparency = Transparency.TRANSLUCENT; - } else { - numBands = 3; - transparency = Transparency.OPAQUE; + if (hasAlpha && alphaBand != numBands - 1) { + throw new IllegalArgumentException("Alpha channel must be after the color components."); } - int[] numBits = bitsPerSample; - numBits = ArraysExt.resize(numBits != null ? numBits : ArraysExt.EMPTY_INT, numBands); + int[] numBits = ArraysExt.resize(orEmpty(bitsPerSample), numBands); int maxSize = 0; for (int i=0; i<numBands; i++) { if (numBits[i] == 0) { if (numBits == bitsPerSample) { numBits = numBits.clone(); } - numBits[i] = Byte.SIZE; - } else { - maxSize = Math.max(maxSize, numBits[i]); + numBits[i] = defaultBitsPerSample; } + maxSize = Math.max(maxSize, numBits[i]); } - return ColorModelFactory.unique(new ComponentColorModel(colorSpace(), - numBits, hasAlpha, isAlphaPremultiplied, transparency, dataType(maxSize))); + return ColorModelFactory.unique(new ComponentColorModel(colorSpace, + numBits, hasAlpha, isAlphaPremultiplied, + hasAlpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE, + dataType(maxSize))); } /** - * Creates a <abbr>RGB</abbr> color model for the given sample model. - * The sample model shall use integer type and have 3 or 4 bands. - * If no <abbr>RGB</abbr> or <abbr>ARGB</abbr> color model can be created, - * this method default on a gray scale color model. + * Creates a <abbr>RGB</abbr> or gray scale color model for the given sample model. + * If this method does not know how to create the requested color model, + * it defaults on a gray scale color model. * * @param targetModel the sample model for which to create a color model. * @return the <abbr>RGB</abbr> color model, or a gray scale color model as a fallback. * @throws IllegalArgumentException if any argument specified to the builder is invalid. */ public ColorModel createRGB(final SampleModel targetModel) { -check: if (DataType.isInteger(targetModel)) { +check: if (DataType.isInteger(targetModel) && colorSpace.getType() == ColorSpace.TYPE_RGB) { final int numBands = targetModel.getNumBands(); - switch (numBands) { - case 3: alphaBand = -1; break; - case 4: alphaBand = STANDARD_ALPHA_BAND; break; - default: break check; + if (alphaBand == UNSPECIFIED_ALPHA) { + switch (numBands) { + case 3: alphaBand = -1; break; + case 4: alphaBand = 3; break; + default: break check; + } + } + if (bitsPerSample == null) { + bitsPerSample(targetModel.getSampleSize()); } - bitsPerSample = targetModel.getSampleSize(); if (targetModel.getNumDataElements() != 1) { return createBandedRGB(); } else { for (int i=0; i<numBands; i++) { - if (bitsPerSample[i] > Byte.SIZE) { + if (getBitsPerSample(i, Integer.SIZE) > Byte.SIZE) { break check; } } return createPackedRGB(); } } - return ColorModelFactory.createGrayScale(targetModel, ColorModelFactory.DEFAULT_VISIBLE_BAND, null); + return ColorModelFactory.createGrayScale(targetModel, visibleBand, sampleValuesRange); } } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelFactory.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelFactory.java index 0e4322ef10..d5945ed4fc 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelFactory.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/ColorModelFactory.java @@ -361,10 +361,10 @@ public final class ColorModelFactory { final int categoryCount = pieceStarts.length - 1; if (numBands == 1 && categoryCount <= 0) { final ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY); - final int[] nBits = { + final int[] numBits = { DataBuffer.getDataTypeSize(dataType) }; - return unique(new ComponentColorModel(cs, nBits, false, true, Transparency.OPAQUE, dataType)); + return unique(new ComponentColorModel(cs, numBits, false, true, Transparency.OPAQUE, dataType)); } /* * Interpolates the colors in the color palette. Colors that do not fall @@ -386,7 +386,7 @@ public final class ColorModelFactory { expand(colors, colorMap, lower, upper); } } - return createIndexColorModel(numBands, visibleBand, colorMap, true, transparent); + return createIndexColorModel(null, 0, numBands, visibleBand, colorMap, true, transparent); } /** @@ -421,28 +421,33 @@ public final class ColorModelFactory { * <p>This methods caches previously created instances using weak references, * because index color model may be big (up to 256 kb).</p> * - * @param numBands the number of bands. - * @param visibleBand the band to display. - * @param ARGB an array of ARGB values. - * @param hasAlpha indicates whether alpha values are contained in the {@code ARGB} array. - * @param transparent the transparent pixel, or -1 for auto-detection. + * @param transferType the transfer type, or {@code null} for automatic. + * @param numBits the number of bits in sample values, or 0 for automatic. + * @param numBands the number of bands (usually 1). + * @param visibleBand the band to display (usually {@value #DEFAULT_VISIBLE_BAND}). + * @param ARGB an array of ARGB values. + * @param hasAlpha indicates whether alpha values are contained in the {@code ARGB} array. + * @param transparent the transparent pixel, or -1 for auto-detection. * @return an index color model for the specified array of ARGB values. */ - public static IndexColorModel createIndexColorModel(final int numBands, final int visibleBand, final int[] ARGB, + public static IndexColorModel createIndexColorModel(final DataType transferType, int numBits, + final int numBands, final int visibleBand, final int[] ARGB, final boolean hasAlpha, final int transparent) { /* * No need to scan the ARGB values in search of a transparent pixel; * the IndexColorModel constructor does that for us. */ - final int length = ARGB.length; - final int bits = getBitCount(length); - final int dataType = getTransferType(length); + final int length = ARGB.length; + if (numBits == 0) { + numBits = getBitCount(length); + } + final int dataType = (transferType != null) ? transferType.toDataBufferType() : getTransferType(length); final IndexColorModel cm; if (numBands == 1) { - cm = new IndexColorModel(bits, length, ARGB, 0, hasAlpha, transparent, dataType); + cm = new IndexColorModel(numBits, length, ARGB, 0, hasAlpha, transparent, dataType); } else { - cm = new MultiBandsIndexColorModel(bits, length, ARGB, 0, hasAlpha, transparent, + cm = new MultiBandsIndexColorModel(numBits, length, ARGB, 0, hasAlpha, transparent, dataType, numBands, visibleBand); } return CACHE.unique(cm); @@ -450,7 +455,7 @@ public final class ColorModelFactory { /** * Returns a unique instance of the given color model. - * This method is a shortcut used when the return type does not need to be a specialized type. + * The color models are kept by weak references. * * @param cm the color model. * @return a unique instance of the given color model. diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/SampleModelBuilder.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/SampleModelBuilder.java index cb619181b9..46f395cdd9 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/SampleModelBuilder.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/privy/SampleModelBuilder.java @@ -108,36 +108,49 @@ public final class SampleModelBuilder { * * @param type type of sample values. * @param size tile width and height in pixels. - * @param numBands number of bands. - * @param bitsPerSample number of bits per sample values. + * @param bitsPerSample number of bits per sample values. The array length is the number of bands. * @param isBanded {@code true} if each band is stored in a separated bank. * @throws RasterFormatException if the arguments imply a sample model of unsupported type. */ public SampleModelBuilder(final DataType type, final Dimension size, - final int numBands, final int bitsPerSample, final boolean isBanded) + final int[] bitsPerSample, final boolean isBanded) { this.dataType = type.toDataBufferType(); this.width = size.width; this.height = size.height; - this.numBands = numBands; + this.numBands = bitsPerSample.length; scanlineStride = width; pixelStride = 1; - if (bitsPerSample != type.size()) { + boolean packed = true; + final int elementSize = type.size(); + for (int n : bitsPerSample) { + if (n >= elementSize) { + packed = false; + break; + } + } + if (packed) { if (numBands == 1) { // MultiPixelPackedSampleModel pixelStride = 0; - numberOfBits = bitsPerSample; - scanlineStride = JDK18.ceilDiv(Math.multiplyExact(width, numberOfBits), type.size()); + numberOfBits = bitsPerSample[0]; + scanlineStride = JDK18.ceilDiv(Math.multiplyExact(width, numberOfBits), elementSize); } else if (!isBanded) { // SinglePixelPackedSampleModel + int shift = 0; bitMasks = new int[numBands]; - bitMasks[0] = (1 << bitsPerSample) - 1; - for (int i=1; i < bitMasks.length; i++) { - bitMasks[i] = bitMasks[i-1] << bitsPerSample; + for (int i=0; i<numBands; i++) { + final int n = bitsPerSample[i]; + ArgumentChecks.ensureBetween("bitsPerSample", 1, elementSize, n); + bitMasks[i] = ((1 << n) - 1) << shift; + shift += n; + } + if (shift > elementSize) { + throw new RasterFormatException(Errors.format(Errors.Keys.IntegerOverflow_1, elementSize)); } } else { // TODO: we can support that with a little bit more work. - throw new RasterFormatException(Errors.format(Errors.Keys.UnsupportedType_1, "bitsPerSample=" + type.size())); + throw new RasterFormatException(Errors.format(Errors.Keys.UnsupportedType_1, "bitsPerSample=" + bitsPerSample[0])); } } else if (isBanded) { // BandedSampleModel diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandSelectImageTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandSelectImageTest.java index 498d3ba9bb..ab7fa2fa9d 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandSelectImageTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandSelectImageTest.java @@ -92,7 +92,7 @@ public final class BandSelectImageTest extends TestCase { if (icm) { final int[] ARGB = new int[256]; ColorModelFactory.expand(new int[] {0xFF000000, 0xFFFFFFFF}, ARGB, 0, ARGB.length); - cm = ColorModelFactory.createIndexColorModel(numBands, checkedBand, ARGB, true, -1); + cm = ColorModelFactory.createIndexColorModel(null, 0, numBands, checkedBand, ARGB, true, -1); } else { cm = ColorModelFactory.createGrayScale(DataBuffer.TYPE_BYTE, numBands, checkedBand, Byte.MIN_VALUE, Byte.MAX_VALUE); } diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/privy/SampleModelBuilderTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/privy/SampleModelBuilderTest.java index 671f47f0c1..dd719b0f18 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/privy/SampleModelBuilderTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/privy/SampleModelBuilderTest.java @@ -16,6 +16,7 @@ */ package org.apache.sis.image.privy; +import java.util.Arrays; import java.awt.Dimension; import java.awt.image.DataBuffer; import java.awt.image.SampleModel; @@ -56,13 +57,26 @@ public final class SampleModelBuilderTest extends TestCase { return new Dimension(WIDTH, HEIGHT); } + /** + * Returns an array of bits per sample. + * + * @param numBands number of bands. + * @param numBits number of bits per sample in each band. + * @return the array to give to {@link SampleModelBuilder} constructor. + */ + private static int[] bitsPerSample(final int numBands, final int numBits) { + final var bitsPerSample = new int[numBands]; + Arrays.fill(bitsPerSample, numBits); + return bitsPerSample; + } + /** * Tests the creation and modification of a {@link BandedSampleModel}. */ @Test public void testBanded() { final BandedSampleModel model = test(BandedSampleModel.class, - new SampleModelBuilder(DataType.FLOAT, size(), NUM_BANDS, Float.SIZE, true)); + new SampleModelBuilder(DataType.FLOAT, size(), bitsPerSample(NUM_BANDS, Float.SIZE), true)); assertArrayEquals(new int[] {1, 0, 2}, model.getBankIndices()); assertArrayEquals(new int[] {0, 0, 0}, model.getBandOffsets()); @@ -77,7 +91,7 @@ public final class SampleModelBuilderTest extends TestCase { @Test public void testPixelInterleaved() { final PixelInterleavedSampleModel model = test(PixelInterleavedSampleModel.class, - new SampleModelBuilder(DataType.BYTE, size(), NUM_BANDS, Byte.SIZE, false)); + new SampleModelBuilder(DataType.BYTE, size(), bitsPerSample(NUM_BANDS, Byte.SIZE), false)); assertArrayEquals(new int[] {0, 0, 0}, model.getBankIndices()); assertArrayEquals(new int[] {1, 0, 2}, model.getBandOffsets()); @@ -94,7 +108,7 @@ public final class SampleModelBuilderTest extends TestCase { @Test public void testSinglePixelPacked() { final SinglePixelPackedSampleModel model = test(SinglePixelPackedSampleModel.class, - new SampleModelBuilder(DataType.INT, size(), NUM_BANDS, 5, false)); + new SampleModelBuilder(DataType.INT, size(), bitsPerSample(NUM_BANDS, 5), false)); final int[] expected = { 0b1111100000, // Band 2 specified, 1 after compression. @@ -114,7 +128,7 @@ public final class SampleModelBuilderTest extends TestCase { @Test public void testPixelMultiPixelPacked() { final int bitsPerSample = 4; - var builder = new SampleModelBuilder(DataType.INT, size(), 1, bitsPerSample, false); + var builder = new SampleModelBuilder(DataType.INT, size(), bitsPerSample(1, bitsPerSample), false); final var model = (MultiPixelPackedSampleModel) builder.build(); assertEquals(bitsPerSample, model.getPixelBitStride()); diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java index c6cc20ebec..78404db9c5 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java @@ -1610,7 +1610,9 @@ final class ImageFileDirectory extends DataCube { final DataType type = getDataType(); if (type != null) try { var size = new Dimension(tileWidth, tileHeight); - sampleModel = new SampleModelBuilder(type, size, samplesPerPixel, bitsPerSample, isPlanar).build(); + var numBits = new int[samplesPerPixel]; + Arrays.fill(numBits, bitsPerSample); + sampleModel = new SampleModelBuilder(type, size, numBits, isPlanar).build(); } catch (IllegalArgumentException | RasterFormatException e) { error = e; } @@ -1750,7 +1752,7 @@ final class ImageFileDirectory extends DataCube { case PHOTOMETRIC_INTERPRETATION_RGB: { if (alphaBand >= 0) alphaBand += 3; // Must add the number of color bands. final var builder = new ColorModelBuilder().bitsPerSample(bitsPerSample) - .alphaBand(alphaBand).isAlphaPremultiplied(isAlphaPremultiplied); + .alphaBand(alphaBand).alphaPremultiplied(isAlphaPremultiplied); if (getSampleModel(null) instanceof SinglePixelPackedSampleModel) { colorModel = builder.createPackedRGB(); } else { @@ -1773,7 +1775,8 @@ final class ImageFileDirectory extends DataCube { | ((colorMap.intValue(bi++) & 0xFF00) >>> Byte.SIZE); } int transparent = Double.isFinite(noData) ? (int) Math.round(noData) : -1; - colorModel = ColorModelFactory.createIndexColorModel(samplesPerPixel, VISIBLE_BAND, ARGB, true, transparent); + colorModel = ColorModelFactory.createIndexColorModel(null, 0, + samplesPerPixel, VISIBLE_BAND, ARGB, true, transparent); break; } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractResource.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractResource.java index 63a5fb41cb..49c9497819 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractResource.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractResource.java @@ -66,10 +66,11 @@ public abstract class AbstractResource implements Resource { /** * A description of this resource as an unmodifiable metadata, or {@code null} if not yet computed. - * If non-null, this metadata should contain at least the resource {@linkplain #getIdentifier() identifier}. - * Those metadata are created by {@link #createMetadata()} when first needed. + * This metadata should contain at least the resource {@linkplain #getIdentifier() identifier}. + * This field is initialized by {@link #getMetadata()} when first needed. * * @see #getMetadata() + * @see #createMetadata() */ private volatile Metadata metadata; @@ -152,7 +153,8 @@ public abstract class AbstractResource implements Resource { } /** - * Returns a description of this resource. This method invokes {@link #createMetadata()} + * Returns a description of this resource. + * The implementation invokes {@link #createMetadata()} * in a synchronized block when first needed, then caches the result. * * @return information about this resource (never {@code null}). @@ -189,7 +191,7 @@ public abstract class AbstractResource implements Resource { * @throws DataStoreException if an error occurred while reading metadata from this resource. */ protected Metadata createMetadata() throws DataStoreException { - final MetadataBuilder builder = new MetadataBuilder(); + final var builder = new MetadataBuilder(); builder.addDefaultMetadata(this, listeners); return builder.build(); } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java index f298f1a0e9..2a1ec30e29 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java @@ -68,7 +68,7 @@ import org.opengis.coordinate.MismatchedDimensionException; * When there is a subsampling, cell coordinates in this coverage are divided by the subsampling factors. * Conversions are done by {@link #coverageToResourceCoordinate(long, int)}. * - * <p><b>DEsign note:</b> {@code TiledGridCoverage} use the same cell coordinates as the originating + * <p><b>Design note:</b> {@code TiledGridCoverage} uses the same cell coordinates as the originating * {@link TiledGridResource} (when no subsampling) because those two classes use {@code long} integers. * There is no integer overflow to avoid.</p> * @@ -240,7 +240,9 @@ public abstract class TiledGridCoverage extends GridCoverage { private final boolean deferredTileReading; /** - * Creates a new tiled grid coverage. + * Creates a new tiled grid coverage. This constructor does not load any tile. + * Callers should invoke {@link TiledGridResource#preload(GridCoverage)} after + * construction for loading tiles when immediate loading was requested by user. * * @param subset description of the {@link TiledGridResource} subset to cover. * @throws ArithmeticException if the number of tiles overflows 32 bits integer arithmetic. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java index f3f1234c5c..6d7792ea5d 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java @@ -255,7 +255,7 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource break; } } - return ColorModelFactory.createIndexColorModel(numBands, VISIBLE_BAND, ARGB, true, -1); + return ColorModelFactory.createIndexColorModel(null, 0, numBands, VISIBLE_BAND, ARGB, true, -1); } /** diff --git a/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java b/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java index 3f72b176de..e88786fb79 100644 --- a/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java +++ b/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java @@ -486,7 +486,7 @@ final class TiledResource extends TiledGridResource { colorModel = new ColorModelBuilder().bitsPerSample(dataType.numBits).alphaBand(alpha).createBandedRGB(); // TODO: needs custom color model if too many bands, or if order is not (A)RGB. } else if (palette != null) { - colorModel = ColorModelFactory.createIndexColorModel(selectedBands.length, paletteIndex, palette, true, -1); + colorModel = ColorModelFactory.createIndexColorModel(null, 0, selectedBands.length, paletteIndex, palette, true, -1); } else { gray = Math.max(gray, 0); final Band band = selectedBands[gray];