This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 83e5974f24c5d5de365994526ca97fc126d34cea Author: Martin Desruisseaux <[email protected]> AuthorDate: Fri Jun 12 17:58:51 2026 +0200 Refactor the GeoHEIF reader with more separation of concern. The code that creates color and sample models moves to a new class. The code for uncompressing tiles with ZLIB uses the code shared with GeoTIFF. --- .../sis/storage/geoheif/CoverageBuilder.java | 309 ++++----------------- .../apache/sis/storage/geoheif/FromImageIO.java | 66 +++-- .../main/org/apache/sis/storage/geoheif/Image.java | 22 +- .../org/apache/sis/storage/geoheif/ImageModel.java | 289 +++++++++++++++++++ .../apache/sis/storage/geoheif/ImageResource.java | 25 +- .../sis/storage/geoheif/ResourceBuilder.java | 41 ++- .../sis/storage/geoheif/UncompressedImage.java | 24 +- .../apache/sis/storage/isobmff/mpeg/Component.java | 16 +- 8 files changed, 445 insertions(+), 347 deletions(-) diff --git a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/CoverageBuilder.java b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/CoverageBuilder.java index c16745d0e4..4f2c8033e6 100644 --- a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/CoverageBuilder.java +++ b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/CoverageBuilder.java @@ -28,19 +28,10 @@ import java.util.logging.Level; import java.util.logging.LogRecord; import java.io.IOException; import java.nio.ByteOrder; -import java.awt.Dimension; -import java.awt.image.ColorModel; import java.awt.image.SampleModel; -import java.awt.image.RasterFormatException; -import javax.imageio.ImageTypeSpecifier; import org.opengis.util.GenericName; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; -import org.apache.sis.image.DataType; -import org.apache.sis.image.internal.shared.ColorModelBuilder; -import org.apache.sis.image.internal.shared.ColorModelFactory; -import org.apache.sis.image.internal.shared.SampleModelBuilder; -import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.coverage.grid.PixelInCell; @@ -54,7 +45,6 @@ import org.apache.sis.storage.isobmff.base.ItemInfoEntry; import org.apache.sis.storage.isobmff.base.ItemProperties; import org.apache.sis.storage.isobmff.geo.ModelTransformation; import org.apache.sis.storage.isobmff.geo.ModelCRS; -import org.apache.sis.storage.isobmff.mpeg.Component; import org.apache.sis.storage.isobmff.mpeg.ComponentType; import org.apache.sis.storage.isobmff.mpeg.ComponentPalette; import org.apache.sis.storage.isobmff.mpeg.ComponentDefinition; @@ -62,13 +52,10 @@ import org.apache.sis.storage.isobmff.mpeg.CompressedUnitsItemInfo; import org.apache.sis.storage.isobmff.mpeg.CompressionConfiguration; import org.apache.sis.storage.isobmff.mpeg.UncompressedFrameConfig; import org.apache.sis.storage.isobmff.mpeg.UnitType; -import org.apache.sis.storage.isobmff.mpeg.InterleavingMode; import org.apache.sis.storage.isobmff.image.ImageSpatialExtents; import org.apache.sis.storage.isobmff.image.PixelInformation; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.Emptiable; -import org.apache.sis.util.collection.Containers; -import org.apache.sis.util.internal.shared.Numerics; import org.apache.sis.util.resources.Errors; import org.apache.sis.pending.jdk.JDK18; @@ -90,7 +77,7 @@ final class CoverageBuilder implements Emptiable { * An index of the image to build, for information purpose only. * This is given to {@link CoverageModifier.Source} constructor. */ - private final int imageIndex; + final int imageIndex; /** * Name or identifier of the resource. This is taken from {@link ItemInfoEntry#itemName} @@ -105,7 +92,7 @@ final class CoverageBuilder implements Emptiable { private String name; /** - * The size of the reconstructed image in pixels. + * The size (in pixels) of the reconstructed image. */ private int width, height; @@ -169,35 +156,12 @@ final class CoverageBuilder implements Emptiable { private final Map<Object, Boolean> unknownBoxes; /** - * The type of sample values stored in the raster. - * Determined as a side effect of {@link #sampleDimensions()}. + * The color model, sample model and sample dimensions. + * This is created when first needed. The same instance may be shared by many tiles. * - * @see #dataType() + * @see #imageModel() */ - private DataType dataType; - - /** - * The color model, created as a side effect of {@link #sampleDimensions()}. - * May stay null if this builder does not have enough information. - * - * @see #colorModel() - */ - private ColorModel colorModel; - - /** - * The sample model, created as a side effect of {@link #sampleDimensions()}. - * May stay null if this builder does not have enough information. - * - * @see #sampleModel() - */ - private SampleModel sampleModel; - - /** - * The sample dimensions, created when first requested. - * - * @see #sampleDimensions() - */ - private SampleDimension[] sampleDimensions; + private ImageModel imageModel; /** * The builder of metadata, created when first needed. @@ -208,6 +172,8 @@ final class CoverageBuilder implements Emptiable { /** * The builder used for building tiles, or {@code null} if none. + * + * @see #setTileBuilder(CoverageBuilder) */ private CoverageBuilder tileBuilder; @@ -219,7 +185,11 @@ final class CoverageBuilder implements Emptiable { * @param properties source of coverage properties for this coverage item. * @param duplicatedBoxes names of boxes that were duplicated. Used for logging a warning only once per type of box. */ - CoverageBuilder(final GeoHeifStore store, final int imageIndex, final ItemProperties.ForID properties, final Set<String> duplicatedBoxes) { + CoverageBuilder(final GeoHeifStore store, + final int imageIndex, + final ItemProperties.ForID properties, + final Set<String> duplicatedBoxes) + { this.store = store; this.imageIndex = imageIndex; unknownBoxes = new LinkedHashMap<>(); @@ -415,10 +385,10 @@ final class CoverageBuilder implements Emptiable { * @param name name of the resource. * @param image the single tile of the image. * @return the resource. - * @throws RasterFormatException if the sample dimensions or sample model cannot be created. - * @throws DataStoreException if the "grid to <abbr>CRS</abbr>" transform cannot be created. + * @throws DataStoreContentException if the "grid to <abbr>CRS</abbr>" transform or the sample dimensions cannot be created. + * @throws DataStoreException if the construction failed for another reason. */ - final ImageResource build(final String name, final Image.Supplier image) throws DataStoreException { + final ImageResource build(final String name, final Image image) throws DataStoreException { this.name = name; return new ImageResource(this, null, image); } @@ -430,8 +400,8 @@ final class CoverageBuilder implements Emptiable { * @param name name of the resource. * @param tiles all tiles of the image. * @return the resource. - * @throws RasterFormatException if the sample dimensions or sample model cannot be created. - * @throws DataStoreException if the "grid to <abbr>CRS</abbr>" transform cannot be created. + * @throws DataStoreContentException if the "grid to <abbr>CRS</abbr>" transform or the sample dimensions cannot be created. + * @throws DataStoreException if the construction failed for another reason. */ final ImageResource build(final String name, final List<Image> tiles) throws DataStoreException { this.name = name; @@ -473,17 +443,22 @@ final class CoverageBuilder implements Emptiable { * * @param dimension the dimension: 0 or 1. * @return number of tiles in the requested dimension. + * @throws DataStoreContentException if the sample model cannot be created. + * @throws DataStoreException if the reading failed for another reason. */ - public final int numTiles(final int dimension) { + public final int numTiles(final int dimension) throws DataStoreException { if (model != null) { switch (dimension) { case 0: return model.numTileCols; case 1: return model.numTileRows; } - } else if ((width | height) != 0 && sampleModel != null) { - switch (dimension) { - case 0: return JDK18.ceilDiv(width, sampleModel.getWidth()); - case 1: return JDK18.ceilDiv(height, sampleModel.getHeight()); + } else if (!isEmpty()) { + final SampleModel sampleModel = imageModel().sampleModel; + if (sampleModel != null) { + switch (dimension) { + case 0: return JDK18.ceilDiv(width, sampleModel.getWidth()); + case 1: return JDK18.ceilDiv(height, sampleModel.getHeight()); + } } } return 1; @@ -498,8 +473,9 @@ final class CoverageBuilder implements Emptiable { /** * Fetches the color model and sample model from a sample tile (usually the first). - * This case happens when each tile is a JPEG image. In such case, the sample model - * is not declared in {@code UncompressedFrameConfig} and we have to get it from a tile. + * This case happens when each tile is a <abbr>JPEG</abbr> image. + * In such case, the sample model is not declared in {@code UncompressedFrameConfig} + * and we have to get it from a tile. * * <p>If this sample model has already been initialized, then this method does nothing. * This is desirable because this method is invoked in a loop for every tiles, while @@ -510,222 +486,45 @@ final class CoverageBuilder implements Emptiable { * @throws IOException if an I/O error occurred. */ final void setImageLayout(final Image image) throws DataStoreException, IOException { - if (sampleModel == null) { - ImageTypeSpecifier type = image.getImageType(store); - colorModel = type.getColorModel(); - sampleModel = type.getSampleModel(); - dataType = DataType.forDataBufferType(sampleModel.getDataType()); + if (imageModel == null) { + imageModel = new ImageModel(image.getImageType(store)); } } /** - * Returns the type of sample values stored in the raster. - * One of {@link #sampleModel()} or {@link #sampleDimensions()} - * methods must have been invoked before this method. - */ - public final DataType dataType() { - return dataType; - } - - /** - * Returns the color model, or {@code null} if unknown. - * The {@link #sampleDimensions()} method must have been invoked before this method. - */ - public final ColorModel colorModel() { - return colorModel; - } - - /** - * Returns the sample model with a size equals to the tile size. + * Returns color model, sample model and sample dimensions computed from <abbr>HEIF</abbr> boxes. * - * @return the sample model (never {@code null}). - * @throws RasterFormatException if the sample dimensions or sample model cannot be created. - * @throws DataStoreException if an error occurred in {@link CoverageModifier}. - */ - public final SampleModel sampleModel() throws DataStoreException { - if (sampleModel == null) { - sampleDimensions(false); - if (sampleModel == null) { - throw new RasterFormatException("Unspecified sample model."); + * @throws DataStoreContentException if the sample model or sample dimensions cannot be created. + * @throws DataStoreException if the reading failed for another reason. + */ + final ImageModel imageModel() throws DataStoreException { + if (imageModel == null) { + imageModel = new ImageModel(width, height, model, componentTypes, bitsPerChannel, palette, this); + if (imageModel.sampleModel == null && tileBuilder != null) { + imageModel = tileBuilder.imageModel(); } } - return sampleModel; + return imageModel; } /** - * Implementation of {@link #sampleDimensions()} and {@link #sampleModel()}. - * - * @todo Need to add information from the {@code CellPropertyTypeProperty} box. - * These information include sample dimension name and unit of measurement. - * - * @param full {@code true} for creating all objects, or {@code false} for creating only the sample model. - * @throws RasterFormatException if the sample dimensions or sample model cannot be created. - * @throws DataStoreException if an error occurred in {@link CoverageModifier}. - */ - private void sampleDimensions(final boolean full) throws DataStoreException { - final int nc = (model == null) ? 0 : model.components.length; - final int nt = (componentTypes == null) ? 0 : componentTypes.length; - final int ns = (bitsPerChannel == null) ? 0 : bitsPerChannel.length; - final int numBands = Math.max(Math.max(nc, nt), ns); - if (numBands == 0) { - if (tileBuilder != null) { - if (tileBuilder.sampleDimensions == null) { - tileBuilder.sampleDimensions(full); - } - sampleDimensions = tileBuilder.sampleDimensions; - sampleModel = tileBuilder.sampleModel; - colorModel = tileBuilder.colorModel; - dataType = tileBuilder.dataType; - } - return; - } - final var bitsPerSample = new int[numBands]; - final var sd = full ? new SampleDimension[numBands] : null; - final var sb = full ? new SampleDimension.Builder() : null; - int numBits=0, redMask=0, greenMask=0, blueMask=0, alphaMask=0; - int grayBand = -1, indexBand = -1, alphaBand = -1; // Negative value means none. - for (int band=0; band<numBands; band++) { - /* - * `colorType` can be an enumeration value such as `RED`, `GREEN`, `BLUE`, - * or an URI, or an Integer value. The same value can appear in two boxes. - * `bitDepth` can also appear in two boxes. The ISO 23001-17:2024 standard - * said that when the component type information appears in the two boxes, - * `ComponentDefinitionBox` shall precede `UncompressedFrameConfigBox`. - */ - var colorType = (band < nt) ? componentTypes[band] : null; // From `ComponentDefinition`. - int bitDepth = (band < ns) ? Byte.toUnsignedInt(bitsPerChannel[band]) : 0; - if (band < nc) { - final Component c = model.components[band]; - if (colorType == null) colorType = c.type; // From `UncompressedFrameConfig` - if (dataType == null) { - dataType = c.getDataType(); - } else if (dataType != c.getDataType()) { - throw new RasterFormatException("All bands shall be of the same data type."); - } - bitDepth = Short.toUnsignedInt(c.bitDepth); - /* - * Example: 10-bit unaligned RGB components followed by a 1-byte aligned 7-bit alpha component. - * Stored as 30 consecutive bits containing R, G and B, followed by 2 pre-alignment padding bits - * for byte alignment, followed by one alignment padding bit then followed by the 7-bit alpha value. - */ - final int alignSize = Byte.toUnsignedInt(c.alignSize) * Byte.SIZE; - if (alignSize != 0) { - numBits = Numerics.snapToCeil(numBits, alignSize) - + Numerics.snapToCeil(bitDepth, alignSize) - bitDepth; - } - } - /* - * Create the sample dimension and derive metadata from it. - * TODO: parse CellPropertyTypeProperty and CellPropertyCategoriesProperty boxes. - */ - if (full) { - if (colorType != null) { - sb.setName(colorType.toString()); - } else { - sb.setName(band); - } - var source = new CoverageModifier.BandSource(store, imageIndex, band, numBands, dataType); - metadata().addNewBand(sd[band] = store.customizer.customize(source, sb)); - sb.clear(); - } - if (bitDepth == 0) { - bitDepth = Component.DEFAULT_BIT_DEPTH; - } else if (full) { - metadata().setBitPerSample(bitDepth); - } - /* - * Identify the bands that we can map to RGBA. - * Will be used for building the color model. - */ - bitsPerSample[band] = bitDepth; - numBits += bitDepth; - if (full && colorType instanceof ComponentType ct) { - int mask = 0; - if (numBits < Integer.SIZE) { - mask = ((1 << bitDepth) - 1) << (Integer.SIZE - numBits); - } - switch (ct) { - case RED: redMask |= mask; break; - case GREEN: greenMask |= mask; break; - case BLUE: blueMask |= mask; break; - case ALPHA: alphaMask |= mask; alphaBand = band; break; - case MONOCHROME: grayBand = band; break; - case PALETTE: indexBand = band; break; - } - } - } - final boolean isRGB = (redMask | greenMask | blueMask) != 0; - if (isRGB && numBits < Integer.SIZE) { - final int n = Integer.SIZE - numBits; // Number of unused bits. - redMask >>>= n; - greenMask >>>= n; - blueMask >>>= n; - alphaMask >>>= n; - } - /* - * Build a sample model. The `InterleavingMode.COMPONENT` default value is arbitrary, - * as the `UncompressedFrameConfig` box is mandatory according ISO/IEC 23001-17:2024. - */ - if (sampleModel == null && dataType != null) { - InterleavingMode interleaveType = InterleavingMode.COMPONENT; - final var tileSize = new Dimension(width, height); - if (model != null) { - tileSize.width /= model.numTileCols; - tileSize.height /= model.numTileRows; - interleaveType = model.interleaveType; - } - final boolean isBanded; - switch (interleaveType) { - case COMPONENT: isBanded = true; break; // Java2D: BandedSampleModel - case PIXEL: isBanded = false; break; // Java2D: PixelInterleavedSampleModel - default: throw new RasterFormatException("Unsupported interleave type: " + interleaveType); - } - sampleModel = new SampleModelBuilder(dataType, tileSize, bitsPerSample, isBanded).build(); - } - /* - * Build a RGB(A) or indexed color model compatible with the sample model. - * The gray scale is used as a fallback for all unrecognized color models. - */ - if (full) { - if (indexBand >= 0 && palette != null) { - colorModel = palette.toARGB(dataType, bitsPerSample[indexBand], numBands, indexBand); // May be null. - } - if (colorModel == null && sampleModel != null) { - if (grayBand < 0 && (grayBand = indexBand) < 0) { - grayBand = ColorModelFactory.DEFAULT_VISIBLE_BAND; - } - final var cb = new ColorModelBuilder(isRGB) - .dataType(dataType) - .bitsPerSample(bitsPerSample) - .alphaBand(alphaBand) - .visibleBand(grayBand, sd[grayBand].getSampleRange().orElse(null)); - if (isRGB) { - cb.componentMasks(redMask, greenMask, blueMask, alphaMask); - // TODO: use another color space if not RGB. - } - colorModel = cb.createRGB(sampleModel); - } - } - sampleDimensions = sd; - } - - /** - * Returns the sample dimensions for the bands and prepares metadata related to them. + * Returns the sample model with a size equals to the tile size. * - * @return the sample dimensions for all bands. - * @throws RasterFormatException if the sample dimensions or sample model cannot be created. - * @throws DataStoreException if an error occurred in {@link CoverageModifier}. + * @return the sample model (never {@code null}). + * @throws DataStoreContentException if the sample model or sample dimensions cannot be created. + * @throws DataStoreException if the reading failed for another reason. */ - public final List<SampleDimension> sampleDimensions() throws DataStoreException { - if (sampleDimensions == null) { - sampleDimensions(true); + public final SampleModel sampleModel() throws DataStoreException { + final SampleModel sampleModel = imageModel().sampleModel; + if (sampleModel != null) { + return sampleModel; } - return Containers.viewAsUnmodifiableList(sampleDimensions); + throw new DataStoreContentException("Unspecified sample model."); } /** * Creates the grid geometry and opportunistically prepares metadata related to it. - * This method should be invoked exactly once, preferably after {@link #sampleDimensions()}. + * This method should be invoked at most once. * It may be invoked not at all when this object is used for building a tile instead of an image. * * @todo Need to add information from the {@code ExtraDimensionProperty} box. @@ -749,7 +548,7 @@ final class CoverageBuilder implements Emptiable { if (gridGeometry.isDefined(GridGeometry.ENVELOPE)) { metadata.addExtent(gridGeometry.getEnvelope(), store.listeners()); } - var source = new CoverageModifier.Source(store, imageIndex, dataType); + var source = new CoverageModifier.Source(store, imageIndex, imageModel().dataType); return store.customizer.customize(source, gridGeometry); } } diff --git a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/FromImageIO.java b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/FromImageIO.java index b708a0a402..3b59410b62 100644 --- a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/FromImageIO.java +++ b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/FromImageIO.java @@ -16,12 +16,13 @@ */ package org.apache.sis.storage.geoheif; +import java.util.Iterator; import java.io.IOException; import java.awt.image.SampleModel; import java.awt.image.BufferedImage; -import java.awt.image.RasterFormatException; import javax.imageio.ImageReader; import javax.imageio.ImageTypeSpecifier; +import javax.imageio.event.IIOReadWarningListener; import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import org.apache.sis.io.stream.ChannelDataInput; @@ -40,7 +41,7 @@ import org.apache.sis.util.ArraysExt; * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) */ -final class FromImageIO extends Image { +final class FromImageIO extends Image implements IIOReadWarningListener { /** * Index of the image to read. */ @@ -58,31 +59,34 @@ final class FromImageIO extends Image { * @param locator the provider of bytes to read from the <abbr>ISOBMFF</abbr> box. * @param provider provider of image readers for decoding the payload. * @param name a name that identifies this image, for debugging purpose. - * @throws RasterFormatException if the sample model cannot be created. + * @throws DataStoreException if an error occurred while decoding <abbr>HEIF</abbr> boxes. */ - FromImageIO(final CoverageBuilder builder, final ByteRanges.Reader locator, final ImageReaderSpi provider, final String name) { + FromImageIO(CoverageBuilder builder, ByteRanges.Reader locator, final ImageReaderSpi provider, String name) + throws DataStoreException + { super(builder, locator, name); this.provider = provider; } /** - * First an image reader provider by format name. + * Finds an image reader provider by format name. + * The search is case-insensitive. * * @param format name of the desired format. * @return image provider for the given format. * @throws UnsupportedEncodingException if no provider was found for the specified format. + * + * @see javax.imageio.ImageIO#getImageReadersByFormatName(String) */ static ImageReaderSpi byFormatName(final String format) throws UnsupportedEncodingException { - var rg = IIORegistry.getDefaultInstance(); - var it = rg.getServiceProviders( - ImageReaderSpi.class, - (spi) -> ArraysExt.containsIgnoreCase(((ImageReaderSpi) spi).getFormatNames(), format), - true); - if (it.hasNext()) { - return it.next(); - } else { - throw new UnsupportedEncodingException("Could not find a JPEG reader."); + final Iterator<ImageReaderSpi> it = IIORegistry.getDefaultInstance().getServiceProviders(ImageReaderSpi.class, true); + while (it.hasNext()) { + final ImageReaderSpi provider = it.next(); + if (ArraysExt.containsIgnoreCase(provider.getFormatNames(), format)) { + return provider; + } } + throw new UnsupportedEncodingException("Could not find a " + format + " reader."); } /** @@ -110,6 +114,8 @@ final class FromImageIO extends Image { * Returns the sample model and color model of this image. * The size of the sample model is the tile size. * + * @todo Try to fetch the image reader from {@code ImageResource.Coverage.ReadContext.getReader(provider))}. + * * @param store the store that opened the <abbr>HEIF</abbr> file. * @throws DataStoreContentException if this image does not include information about the sample/color models. * @throws DataStoreException if the input cannot be set because of its type. @@ -118,6 +124,7 @@ final class FromImageIO extends Image { @Override protected ImageTypeSpecifier getImageType(final GeoHeifStore store) throws DataStoreException, IOException { final ImageReader reader = provider.createReaderInstance(); + reader.addIIOReadWarningListener(this); final var ranges = new ByteRanges(); locator.resolve(0, -1, ranges); setReaderInput(reader, ranges.viewAsConsecutiveBytes(store.ensureOpen()), ranges); @@ -125,15 +132,15 @@ final class FromImageIO extends Image { ImageTypeSpecifier specifier; if (it.hasNext()) { specifier = it.next(); + final int width = reader.getTileWidth (IMAGE_INDEX); + final int height = reader.getTileHeight(IMAGE_INDEX); + SampleModel sampleModel = specifier.getSampleModel(); + if (sampleModel.getWidth() != width || sampleModel.getHeight() != height) { + sampleModel = specifier.getSampleModel(width, height); + specifier = new ImageTypeSpecifier(specifier.getColorModel(), sampleModel); + } } else { - return super.getImageType(store); - } - final int width = reader.getTileWidth (IMAGE_INDEX); - final int height = reader.getTileHeight(IMAGE_INDEX); - SampleModel sampleModel = specifier.getSampleModel(); - if (sampleModel.getWidth() != width || sampleModel.getHeight() != height) { - sampleModel = specifier.getSampleModel(width, height); - specifier = new ImageTypeSpecifier(specifier.getColorModel(), sampleModel); + specifier = super.getImageType(store); } reader.dispose(); return specifier; @@ -152,17 +159,30 @@ final class FromImageIO extends Image { final int tx = Math.toIntExact(context.subTileX); final int ty = Math.toIntExact(context.subTileY); final ImageReader reader = context.getReader(provider); - setReaderInput(reader, input, context); final BufferedImage image; try { + reader.addIIOReadWarningListener(this); + setReaderInput(reader, input, context); if (reader.canReadRaster()) { return reader.readTileRaster(IMAGE_INDEX, tx, ty); } image = reader.readTile(IMAGE_INDEX, tx, ty); } finally { reader.setInput(null); + reader.removeIIOReadWarningListener(this); } return image.getRaster(); }; } + + /** + * Reports a non-fatal error during decoding. + * + * @param source the reader calling this method. + * @param message the warning. + */ + @Override + public void warningOccurred(final ImageReader source, final String message) { + listeners.warning(message); + } } diff --git a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Image.java b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Image.java index 39813392f9..f662062f3c 100644 --- a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Image.java +++ b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Image.java @@ -19,11 +19,11 @@ package org.apache.sis.storage.geoheif; import java.nio.ByteOrder; import java.io.IOException; import java.awt.image.Raster; -import java.awt.image.RasterFormatException; import javax.imageio.ImageTypeSpecifier; import org.apache.sis.io.stream.ChannelDataInput; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.DataStoreContentException; +import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.storage.isobmff.ByteRanges; @@ -58,31 +58,31 @@ abstract class Image { */ protected final ByteOrder byteOrder; + /** + * Where to send the warning. + */ + protected final StoreListeners listeners; + /** * Creates a new tile. * * @param builder helper class for building the grid geometry and sample dimensions. * @param locator the provider of bytes to read from the <abbr>ISOBMFF</abbr> box. * @param name a name that identifies this image, for debugging purpose. - * @throws RasterFormatException if the sample model cannot be created. + * @throws DataStoreException if an error occurred while decoding <abbr>HEIF</abbr> boxes. */ - protected Image(final CoverageBuilder builder, final ByteRanges.Reader locator, final String name) { + protected Image(final CoverageBuilder builder, final ByteRanges.Reader locator, final String name) + throws DataStoreException + { this.locator = locator; this.name = name; byteOrder = builder.byteOrder(); numXTiles = builder.numTiles(0); numYTiles = builder.numTiles(1); + listeners = builder.store.listeners(); // Do NOT invoke `builder.sampleModel()`, because that information is not available for all types. } - /** - * A supplier of image, used for deferred computation. - */ - @FunctionalInterface - interface Supplier { - Image get() throws DataStoreException; - } - /** * Returns the sample model and color model of this image. * For uncompressed image, this information is not available and this method always throws an exception. diff --git a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageModel.java b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageModel.java new file mode 100644 index 0000000000..c7fa5c9c51 --- /dev/null +++ b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageModel.java @@ -0,0 +1,289 @@ +/* + * 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.storage.geoheif; + +import java.net.URI; +import java.util.List; +import java.awt.Dimension; +import java.awt.image.ColorModel; +import java.awt.image.SampleModel; +import javax.imageio.ImageTypeSpecifier; +import org.opengis.util.GenericName; +import org.apache.sis.image.DataType; +import org.apache.sis.image.internal.shared.ColorModelBuilder; +import org.apache.sis.image.internal.shared.ColorModelFactory; +import org.apache.sis.coverage.SampleDimension; +import org.apache.sis.image.internal.shared.SampleModelBuilder; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.DataStoreContentException; +import org.apache.sis.storage.UnsupportedEncodingException; +import org.apache.sis.storage.modifier.CoverageModifier; +import org.apache.sis.storage.isobmff.mpeg.Component; +import org.apache.sis.storage.isobmff.mpeg.ComponentType; +import org.apache.sis.storage.isobmff.mpeg.ComponentPalette; +import org.apache.sis.storage.isobmff.mpeg.InterleavingMode; +import org.apache.sis.storage.isobmff.mpeg.UncompressedFrameConfig; +import org.apache.sis.util.internal.shared.Numerics; + + +/** + * Color model, sample model and sample dimensions computed from <abbr>HEIF</abbr> boxes. + * + * @todo The same {@code ImageModel} may be duplicated in many tiles of the same image. + * This class is intended to be used for avoiding to recompute the same information for every tiles. + * This sharing has not yet been implemented. + * + * @author Martin Desruisseaux (Geomatys) + */ +final class ImageModel { + /** + * The type of sample values stored in the raster. + */ + final DataType dataType; + + /** + * The color model, or {@code null} if this builder did not have enough information. + */ + final ColorModel colorModel; + + /** + * The sample model, {@code null} if this builder did not have enough information. + */ + final SampleModel sampleModel; + + /** + * Default name of each band. + */ + private final GenericName[] defaultBandNames; + + /** + * The sample dimensions for each band. + */ + private final SampleDimension[] sampleDimensions; + + /** + * Creates a new image model from the given Image I/O specifier. + * + * @param type the image specifier from the Image I/O <abbr>API</abbr>. + */ + ImageModel(final ImageTypeSpecifier type) { + colorModel = type.getColorModel(); + sampleModel = type.getSampleModel(); + dataType = DataType.forDataBufferType(sampleModel.getDataType()); + sampleDimensions = null; + defaultBandNames = null; + } + + /** + * Computes date type, color model, sample model, and sample dimensions. + * + * @todo Need to add information from the {@code CellPropertyTypeProperty} box. + * These information include sample dimension name and unit of measurement. + * + * @param width the width (in pixels) of the reconstructed image. + * @param height the height (in pixels) of the reconstructed image. + * @param model information about the sample model (data type, <i>etc.</i>), or {@code null}. + * @param componentTypes pixel data displaying as {@link ComponentType}, {@link URI} or {@link Integer}. + * @param bitsPerChannel number of bits per channel in the reconstructed image, or {@code null}. + * @param palette the color palette of an indexed color model, or {@code null}. + * @param builder the builder which is creating a grid coverage. + * @throws DataStoreContentException if the sample model or sample dimensions cannot be created. + * @throws DataStoreException if the reading failed for another reason. + */ + ImageModel(final int width, + final int height, + final UncompressedFrameConfig model, + final Object[] componentTypes, + final byte[] bitsPerChannel, + final ComponentPalette palette, + final CoverageBuilder builder) + throws DataStoreException + { + final int nc = (model == null) ? 0 : model.components.length; + final int nt = (componentTypes == null) ? 0 : componentTypes.length; + final int ns = (bitsPerChannel == null) ? 0 : bitsPerChannel.length; + final int numBands = Math.max(Math.max(nc, nt), ns); + final var bitsPerSample = new int[numBands]; + final var sb = new SampleDimension.Builder(); + sampleDimensions = new SampleDimension[numBands]; + defaultBandNames = new GenericName[numBands]; + + @SuppressWarnings("LocalVariableHidesMemberVariable") + DataType dataType = null; + int numBits=0, redMask=0, greenMask=0, blueMask=0, alphaMask=0; + int grayBand = -1, indexBand = -1, alphaBand = -1; // Negative value means none. + for (int band = 0; band < numBands; band++) { + /* + * `colorType` can be an enumeration value such as `RED`, `GREEN`, `BLUE`, + * or an URI, or an Integer value. The same value can appear in two boxes. + * `bitDepth` can also appear in two boxes. The ISO 23001-17:2024 standard + * said that when the component type information appears in the two boxes, + * `ComponentDefinitionBox` shall precede `UncompressedFrameConfigBox`. + */ + var colorType = (band < nt) ? componentTypes[band] : null; // From `ComponentDefinition`. + int bitDepth = (band < ns) ? Byte.toUnsignedInt(bitsPerChannel[band]) : 0; + if (band < nc) { + final Component c = model.components[band]; + if (colorType == null) { + colorType = c.type; // From `UncompressedFrameConfig` + } + if (dataType == null) { + dataType = c.getDataType(); + } else if (dataType != c.getDataType()) { + throw new DataStoreContentException("All bands shall be of the same data type."); + } + bitDepth = Short.toUnsignedInt(c.bitDepth); + /* + * Example: 10-bit unaligned RGB components followed by a 1-byte aligned 7-bit alpha component. + * Stored as 30 consecutive bits containing R, G and B, followed by 2 pre-alignment padding bits + * for byte alignment, followed by one alignment padding bit then followed by the 7-bit alpha value. + */ + final int alignSize = Byte.toUnsignedInt(c.alignSize) * Byte.SIZE; + if (alignSize != 0) { + numBits = Numerics.snapToCeil(numBits, alignSize) + + Numerics.snapToCeil(bitDepth, alignSize) - bitDepth; + } + } + /* + * Create the sample dimension and derive metadata from it. + * TODO: parse CellPropertyTypeProperty and CellPropertyCategoriesProperty boxes. + */ + if (colorType != null) { + sb.setName(colorType.toString()); + } else { + sb.setName(band); + } + defaultBandNames[band] = sb.getName(); + var source = new CoverageModifier.BandSource(builder.store, builder.imageIndex, band, numBands, dataType); + builder.metadata().addNewBand(sampleDimensions[band] = builder.store.customizer.customize(source, sb)); + sb.clear(); + if (bitDepth == 0) { + bitDepth = Component.DEFAULT_BIT_DEPTH; + } else { + builder.metadata().setBitPerSample(bitDepth); + } + /* + * Identify the bands that we can map to RGBA. + * Will be used for building the color model. + */ + bitsPerSample[band] = bitDepth; + numBits += bitDepth; + if (colorType instanceof ComponentType ct) { + int mask = 0; + if (numBits < Integer.SIZE) { + mask = ((1 << bitDepth) - 1) << (Integer.SIZE - numBits); + } + switch (ct) { + case RED: redMask |= mask; break; + case GREEN: greenMask |= mask; break; + case BLUE: blueMask |= mask; break; + case ALPHA: alphaMask |= mask; alphaBand = band; break; + case MONOCHROME: grayBand = band; break; + case PALETTE: indexBand = band; break; + } + } + } + final boolean isRGB = (redMask | greenMask | blueMask) != 0; + if (isRGB && numBits < Integer.SIZE) { + final int n = Integer.SIZE - numBits; // Number of unused bits. + redMask >>>= n; + greenMask >>>= n; + blueMask >>>= n; + alphaMask >>>= n; + } + /* + * Build a sample model. The `InterleavingMode.COMPONENT` default value is arbitrary, + * as the `UncompressedFrameConfig` box is mandatory according ISO/IEC 23001-17:2024. + */ + this.dataType = dataType; + if (dataType != null) { + InterleavingMode interleaveType = InterleavingMode.COMPONENT; + final var tileSize = new Dimension(width, height); + if (model != null) { + tileSize.width /= model.numTileCols; + tileSize.height /= model.numTileRows; + interleaveType = model.interleaveType; + } + final boolean isBanded; + switch (interleaveType) { + case COMPONENT: isBanded = true; break; // Java2D: BandedSampleModel + case PIXEL: isBanded = false; break; // Java2D: PixelInterleavedSampleModel + default: throw new UnsupportedEncodingException("Unsupported interleave type: " + interleaveType); + } + sampleModel = new SampleModelBuilder(dataType, tileSize, bitsPerSample, isBanded).build(); + } else { + sampleModel = null; + } + /* + * Build a RGB(A) or indexed color model compatible with the sample model. + * The gray scale is used as a fallback for all unrecognized color models. + */ + @SuppressWarnings("LocalVariableHidesMemberVariable") + ColorModel colorModel = null; + if (indexBand >= 0 && palette != null) { + colorModel = palette.toARGB(dataType, bitsPerSample[indexBand], numBands, indexBand); // May be null. + } + if (colorModel == null && sampleModel != null) { + if (grayBand < 0 && (grayBand = indexBand) < 0) { + grayBand = ColorModelFactory.DEFAULT_VISIBLE_BAND; + } + final var cb = new ColorModelBuilder(isRGB) + .dataType(dataType) + .bitsPerSample(bitsPerSample) + .alphaBand(alphaBand) + .visibleBand(grayBand, sampleDimensions[grayBand].getSampleRange().orElse(null)); + if (isRGB) { + cb.componentMasks(redMask, greenMask, blueMask, alphaMask); + // TODO: use another color space if not RGB. + } + colorModel = cb.createRGB(sampleModel); + } + this.colorModel = colorModel; + } + + /** + * Returns the sample dimensions. + * If the {@code coverage} argument is null, it is assumed the same as at construction time. + * + * @param builder builder of the coverage for which to create sample dimensions, or {@code null}. + * @return the sample dimensions. + * @throws DataStoreContentException if the sample dimensions cannot be created. + */ + final List<SampleDimension> sampleDimensions(final CoverageBuilder builder) throws DataStoreException { + SampleDimension[] bands = sampleDimensions; + boolean share = true; + if (builder != null) { + bands = bands.clone(); + final int numBands = bands.length; + final var sb = new SampleDimension.Builder(); + for (int band = 0; band < numBands; band++) { + sb.setName(defaultBandNames[band]); + var source = new CoverageModifier.BandSource(builder.store, builder.imageIndex, band, numBands, dataType); + final SampleDimension sd = builder.store.customizer.customize(source, sb); + if (!sd.equals(bands[band])) { + bands[band] = sd; + share = false; + } + sb.clear(); + } + if (share) { + bands = sampleDimensions; + } + } + return List.of(bands); + } +} diff --git a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java index d624101ad3..c767e2a646 100644 --- a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java +++ b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java @@ -27,7 +27,6 @@ import static java.lang.Math.addExact; import static java.lang.Math.multiplyExact; import java.awt.image.ColorModel; import java.awt.image.Raster; -import java.awt.image.RasterFormatException; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import javax.imageio.ImageReader; @@ -125,24 +124,21 @@ final class ImageResource extends TiledGridCoverageResource implements StoreReso * @param builder helper class for building the grid geometry and sample dimensions. * @param tiles the images that constitute the tiles, or {@code null} if {@code reader} is provided. * @param image the single tile for the whole image, or {@code null} if {@code tiles} is provided. - * @throws RasterFormatException if the sample dimensions or sample model cannot be created. - * @throws DataStoreException if the "grid to <abbr>CRS</abbr>" transform cannot be created. + * @throws DataStoreException if the "grid to <abbr>CRS</abbr>" transform or the sample dimensions cannot be created. */ - ImageResource(final CoverageBuilder builder, Image[] tiles, final Image.Supplier image) throws DataStoreException { + ImageResource(final CoverageBuilder builder, Image[] tiles, final Image image) throws DataStoreException { super(builder.store); this.store = builder.store; identifier = builder.name(); - sampleDimensions = builder.sampleDimensions(); - gridGeometry = builder.gridGeometry(); // Should be after `sampleDimensions()`. + sampleDimensions = builder.imageModel().sampleDimensions(null); + gridGeometry = builder.gridGeometry(); if (tiles == null) { // Shall be after the call to `sampleDimensions()`. - tiles = new Image[] { - image.get() // Actual I/O operations are deferred (not in this call). - }; + tiles = new Image[] {image}; } this.tiles = tiles; sampleModel = builder.sampleModel(); - colorModel = builder.colorModel(); + colorModel = builder.imageModel().colorModel; metadata = builder.metadata().build(); // Not `buildAndFreeze()` as the metadata may still be modified. tileMatrixRowStride = builder.numTiles(0); } @@ -379,6 +375,7 @@ final class ImageResource extends TiledGridCoverageResource implements StoreReso * @param tileX 0-based column index of the tile to read, starting from image left. * @param tileY 0-based row index of the tile to read, starting from image top. */ + @SuppressWarnings("LeakingThisInConstructor") private ReadContext(final ReadContext parent, final ImageResource owner) throws DataStoreException { this.iterator = new Snapshot(parent.iterator); this.readers = parent.readers; @@ -420,15 +417,15 @@ final class ImageResource extends TiledGridCoverageResource implements StoreReso * This method caches the first reader created by this method, * then returns the cached value in subsequent calls. * - * @param spi the provider of image reader. + * @param provider the provider of image reader. * @return an image reader instance created by the given provider. * @throws IOException if an error occurred while creating the image reader. */ - public ImageReader getReader(final ImageReaderSpi spi) throws IOException { + public ImageReader getReader(final ImageReaderSpi provider) throws IOException { try { - return readers.computeIfAbsent(spi, (factory) -> { + return readers.computeIfAbsent(provider, (spi) -> { try { - return factory.createReaderInstance(); + return spi.createReaderInstance(); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ResourceBuilder.java b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ResourceBuilder.java index 85b1e8c759..53cc82e09a 100644 --- a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ResourceBuilder.java +++ b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ResourceBuilder.java @@ -25,7 +25,6 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.logging.Level; import java.util.logging.LogRecord; -import java.awt.image.RasterFormatException; import java.io.IOException; import javax.imageio.spi.ImageReaderSpi; import org.opengis.util.GenericName; @@ -130,7 +129,8 @@ final class ResourceBuilder { private MediaData data; /** - * Provider of Image I/O readers for the JPEG format, or {@code null} if not yet fetched. + * Provider of Image I/O readers for the <abbr>JPEG</abbr> format, + * or {@code null} if not yet fetched. * * @see #readerJPEG() */ @@ -222,7 +222,7 @@ final class ResourceBuilder { } /** - * Returns the provider of JPEG readers. + * Returns the provider of <abbr>JPEG</abbr> readers. */ private ImageReaderSpi readerJPEG() throws UnsupportedEncodingException { if (readerJPEG == null) { @@ -307,7 +307,7 @@ final class ResourceBuilder { * @param info information about the item, normally as a singleton but other size are nevertheless accepted. * @param addTo a list where to add the image, or {@code null} for adding to {@link #resources} instead. * @return the builder used for building the image. If many images were created, returns the first builder. - * @throws RasterFormatException if the sample dimensions or sample model cannot be created. + * @throws DataStoreContentException if the "grid to <abbr>CRS</abbr>" transform or the sample dimensions cannot be created. * @throws DataStoreException if another error occurred while building the image or resource. */ private CoverageBuilder createImage(final Integer itemID, final List<ItemInfoEntry> info, final List<Image> addTo) @@ -335,8 +335,7 @@ final class ResourceBuilder { if (firstBuilder == null) { firstBuilder = coverage; } - final ByteRanges.Reader locator; - final Image.Supplier image; + Image image = null; switch (entry.itemType) { default: { warning("Unsupported type " + Box.formatFourCC(entry.itemType) + " for the \"{0}\" resource.", name); @@ -373,11 +372,12 @@ final class ResourceBuilder { * the constructor will not ask for the sample model. */ case ItemInfoEntry.JPEG: { - final ImageReaderSpi reader = readerJPEG(); - locator = getLocationByIdentifier(itemID); - final var prepared = new FromImageIO(coverage, locator, reader, name); - if (locator != null) coverage.setImageLayout(prepared); - image = () -> prepared; + final ImageReaderSpi provider = readerJPEG(); + final ByteRanges.Reader locator = getLocationByIdentifier(itemID); + if (locator != null) { + image = new FromImageIO(coverage, locator, provider, name); + coverage.setImageLayout(image); + } break; } /* @@ -386,16 +386,18 @@ final class ResourceBuilder { * actual reading is deferred. */ case ItemInfoEntry.UNCI: { - locator = getLocationByIdentifier(itemID); - image = () -> new UncompressedImage(coverage, locator, name); + final ByteRanges.Reader locator = getLocationByIdentifier(itemID); + if (locator != null) { + image = new UncompressedImage(coverage, locator, name); + } break; } } - if (locator == null) { + if (image == null) { warning("No data found for the \"{0}\" resource.", name); } else { if (addTo != null) { - addTo.add(image.get()); + addTo.add(image); } else { builders.remove(itemProperties); // Builder cannot be reused after resource creation. resources.add(coverage.build(name, image)); @@ -419,19 +421,14 @@ final class ResourceBuilder { info = itemInfos.remove(0); // `itemInfoEntry.itemID` = 0 means the primary item. if (info == null) continue; } - try { - createImage(primary.itemID, info, null); - } catch (RasterFormatException e) { - // Considered fatal because it was the primary resource. - throw new DataStoreContentException("Unsupported image layout.", e); - } + createImage(primary.itemID, info, null); } // Iterate over a snapshot because elements may be removed by `createImage(…)`. for (final Integer itemID : itemInfos.keySet().toArray(Integer[]::new)) { final List<ItemInfoEntry> info = itemInfos.remove(itemID); if (info != null) try { createImage(itemID, info, null); - } catch (RasterFormatException e) { + } catch (UnsupportedEncodingException e) { store.listeners().warning("A resource uses an unsupported sample model.", e); } } diff --git a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/UncompressedImage.java b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/UncompressedImage.java index df4d47afc4..a9c77ede8c 100644 --- a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/UncompressedImage.java +++ b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/UncompressedImage.java @@ -17,24 +17,22 @@ package org.apache.sis.storage.geoheif; import java.util.Arrays; -import java.util.zip.InflaterInputStream; -import java.io.ByteArrayInputStream; import static java.lang.Math.addExact; import static java.lang.Math.multiplyExact; import java.awt.image.DataBuffer; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; -import java.awt.image.RasterFormatException; import org.apache.sis.image.DataType; import org.apache.sis.image.internal.shared.ImageUtilities; import org.apache.sis.image.internal.shared.RasterFactory; import org.apache.sis.io.stream.ChannelDataInput; import org.apache.sis.io.stream.HyperRectangleReader; import org.apache.sis.io.stream.Region; +import org.apache.sis.io.stream.inflater.Deflate; import org.apache.sis.storage.DataStoreException; -import org.apache.sis.storage.StorageConnector; import org.apache.sis.storage.isobmff.ByteRanges; import org.apache.sis.storage.isobmff.mpeg.CompressedUnitsItemInfo; +import org.apache.sis.util.internal.shared.Numerics; /** @@ -77,14 +75,14 @@ final class UncompressedImage extends Image { * @param builder helper class for building the grid geometry and sample dimensions. * @param locator the provider of bytes to read from the <abbr>ISOBMFF</abbr> box. * @param name a name that identifies this image, for debugging purpose. - * @throws RasterFormatException if the sample model cannot be created. + * @throws DataStoreContentException if the "grid to <abbr>CRS</abbr>" transform or the sample dimensions cannot be created. */ UncompressedImage(final CoverageBuilder builder, final ByteRanges.Reader locator, final String name) throws DataStoreException { super(builder, locator, name); sampleModel = builder.sampleModel(); - dataType = builder.dataType(); // Shall be after `sampleModel()`. + dataType = builder.imageModel().dataType; compressedImageUnit = builder.getCompressedImageUnit(); } @@ -132,18 +130,16 @@ final class UncompressedImage extends Image { final var region = region(sourceSize, sourceSize); final int dataSize = dataType.bytes(); final long tileSize = multiplyExact(region.length, dataSize); - final long skipBytes = region.getStartByteOffset(dataSize); final long tileIndex = addExact(multiplyExact(context.subTileY, numXTiles), context.subTileX); - final long offset = addExact(multiplyExact(tileIndex, tileSize), skipBytes); - locator.resolve(offset, tileSize - skipBytes, context); + final long offset = multiplyExact(tileIndex, tileSize); + locator.resolve(offset, tileSize, context); return (ChannelDataInput input) -> { - long origin = context.offset() - skipBytes; + long origin = context.offset(); final CompressedUnitsItemInfo.Unit unit = compressedImageUnit; if (unit != null) { - input.skipNBytes(unit.offset); - final byte[] compressedChunk = input.readBytes((int) unit.size); - InflaterInputStream iis = new InflaterInputStream(new ByteArrayInputStream(compressedChunk)); - input = new StorageConnector(iis).getStorageAs(ChannelDataInput.class); + final var inflater = new Deflate(input, listeners); + inflater.setInputRegion(addExact(origin, unit.offset), unit.size); + input = inflater.createDataInput(inflater, Numerics.clamp(sourceSize[0]), true); origin = 0; } /* diff --git a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/mpeg/Component.java b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/mpeg/Component.java index 70146b2c57..312dddb9d0 100644 --- a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/mpeg/Component.java +++ b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/mpeg/Component.java @@ -18,9 +18,9 @@ package org.apache.sis.storage.isobmff.mpeg; import java.net.URI; import java.io.IOException; -import java.awt.image.RasterFormatException; import org.apache.sis.image.DataType; import org.apache.sis.io.stream.ChannelDataInput; +import org.apache.sis.storage.UnsupportedEncodingException; import org.apache.sis.storage.isobmff.TreeNode; import org.apache.sis.util.resources.Errors; @@ -126,16 +126,16 @@ public final class Component extends TreeNode { /** * Returns the Java2D data type for this component. * - * @throws RasterFormatException if the {@link #format} value is unsupported. + * @throws UnsupportedEncodingException if the {@link #format} value is unsupported. */ - public DataType getDataType() { + public DataType getDataType() throws UnsupportedEncodingException { + boolean real = false; boolean signed = false; - boolean real = false; switch (format) { - case 0: break; - case 1: real = true; break; - case 3: signed = true; break; - default: throw new RasterFormatException(Errors.format(Errors.Keys.UnsupportedFormat_1, format)); + case 0: break; + case 1: real = true; break; + case 3: signed = true; break; + default: throw new UnsupportedEncodingException(Errors.format(Errors.Keys.UnsupportedFormat_1, format)); } return DataType.forNumberOfBits(Short.toUnsignedInt(bitDepth), real, signed); }
