This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 1bcda2bd6b Use TIFF strips instead of tiles when the rows are not divided in tiles. The intend is to avoid the restriction about tile size multiple of 16 bytes. 1bcda2bd6b is described below commit 1bcda2bd6b2aa10c43315781204df8a5287c0e1c Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Tue Oct 31 23:25:03 2023 +0100 Use TIFF strips instead of tiles when the rows are not divided in tiles. The intend is to avoid the restriction about tile size multiple of 16 bytes. --- .../org/apache/sis/storage/geotiff/Writer.java | 56 ++++++++++++++++------ .../sis/storage/geotiff/writer/TileMatrix.java | 9 ++++ .../org/apache/sis/storage/geotiff/WriterTest.java | 34 ++++++++----- 3 files changed, 71 insertions(+), 28 deletions(-) diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java index b0ec5408cc..95d3d27a08 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java @@ -106,12 +106,12 @@ final class Writer extends IOBase implements Flushable { } /** - * Minimal number of tags which will be written. This amount is for grayscale images with no metadata - * and no statistics. For RGB images, there is one more tag. For color maps, there is two more tags. - * This number is only a hint for avoiding the need to update this information if the number appears - * to be right. + * Common number of tags which will be written. This amount is for tiled grayscale images with no metadata + * and no statistics. For stripped images, there is one less tag. For RGB images, there is one more tag. + * For color maps, there is two more tags. This number is only a hint for avoiding the need to update + * this information if the number appears to be right. */ - static final int MINIMAL_NUMBER_OF_TAGS = 16; + static final int COMMON_NUMBER_OF_TAGS = 16; /** * The processor to use for transforming the image before to write it. @@ -329,7 +329,7 @@ final class Writer extends IOBase implements Flushable { * because the tags need to be written in increasing code order, which causes ColorModel-related tags * (for example) to be interleaved with other aspects. */ - numberOfTags = MINIMAL_NUMBER_OF_TAGS; // Only a guess at this stage. Real number computed later. + numberOfTags = COMMON_NUMBER_OF_TAGS; // Only a guess at this stage. Real number computed later. if (compression.usePredictor()) numberOfTags++; final int colorInterpretation = image.getColorInterpretation(); if (colorInterpretation == PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR) { @@ -381,6 +381,9 @@ final class Writer extends IOBase implements Flushable { final UpdatableWrite<?> tagCountWriter = isBigTIFF ? UpdatableWrite.of(output, (long) numberOfTags) : UpdatableWrite.of(output, (short) numberOfTags); + + final var tiling = new TileMatrix(image.visibleBands, numPlanes, bitsPerSample, offsetIFD, + compression.method, compression.level, compression.predictor); numberOfTags = 0; writeTag((short) TAG_NEW_SUBFILE_TYPE, (short) TIFFTag.TIFF_LONG, overview ? 1 : 0); writeTag((short) TAG_IMAGE_WIDTH, (short) TIFFTag.TIFF_LONG, image.visibleBands.getWidth()); @@ -391,7 +394,10 @@ final class Writer extends IOBase implements Flushable { writeTag((short) TAG_DOCUMENT_NAME, /* TIFF_ASCII */ mf.series); writeTag((short) TAG_IMAGE_DESCRIPTION, /* TIFF_ASCII */ mf.title); writeTag((short) TAG_MODEL, /* TIFF_ASCII */ mf.instrument); + writeTag((short) TAG_STRIP_OFFSETS, /* TIFF_LONG */ tiling, true); writeTag((short) TAG_SAMPLES_PER_PIXEL, (short) TIFFTag.TIFF_SHORT, numBands); + writeTag((short) TAG_ROWS_PER_STRIP, /* TIFF_LONG */ tiling, true); + writeTag((short) TAG_STRIP_BYTE_COUNTS, /* TIFF_LONG */ tiling, true); writeTag((short) TAG_MIN_SAMPLE_VALUE, /* TIFF_SHORT */ shortStats[0]); writeTag((short) TAG_MAX_SAMPLE_VALUE, /* TIFF_SHORT */ shortStats[1]); writeTag((short) TAG_X_RESOLUTION, /* TIFF_RATIONAL */ xres); @@ -408,15 +414,13 @@ final class Writer extends IOBase implements Flushable { if (colorInterpretation == PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR) { writeColorPalette((IndexColorModel) image.visibleBands.getColorModel(), 1L << bitsPerSample[0]); } - final var tiling = new TileMatrix(image.visibleBands, numPlanes, bitsPerSample, offsetIFD, - compression.method, compression.level, compression.predictor); - writeTag((short) TAG_TILE_WIDTH, (short) TIFFTag.TIFF_LONG, tiling.tileWidth); - writeTag((short) TAG_TILE_LENGTH, (short) TIFFTag.TIFF_LONG, tiling.tileHeight); - tiling.offsetsTag = writeTag((short) TAG_TILE_OFFSETS, tiling.offsets); - tiling.lengthsTag = writeTag((short) TAG_TILE_BYTE_COUNTS, (short) TIFFTag.TIFF_LONG, tiling.lengths); - writeTag((short) TAG_SAMPLE_FORMAT, (short) TIFFTag.TIFF_SHORT, sampleFormat); - writeTag((short) TAG_S_MIN_SAMPLE_VALUE, (short) TIFFTag.TIFF_FLOAT, statistics[0]); - writeTag((short) TAG_S_MAX_SAMPLE_VALUE, (short) TIFFTag.TIFF_FLOAT, statistics[1]); + writeTag((short) TAG_TILE_WIDTH, /* TIFF_LONG */ tiling, false); + writeTag((short) TAG_TILE_LENGTH, /* TIFF_LONG */ tiling, false); + writeTag((short) TAG_TILE_OFFSETS, /* TIFF_LONG */ tiling, false); + writeTag((short) TAG_TILE_BYTE_COUNTS, /* TIFF_LONG */ tiling, false); + writeTag((short) TAG_SAMPLE_FORMAT, (short) TIFFTag.TIFF_SHORT, sampleFormat); + writeTag((short) TAG_S_MIN_SAMPLE_VALUE, (short) TIFFTag.TIFF_FLOAT, statistics[0]); + writeTag((short) TAG_S_MAX_SAMPLE_VALUE, (short) TIFFTag.TIFF_FLOAT, statistics[1]); if (geoKeys != null) { writeTag((short) TAG_MODEL_TRANSFORMATION, (short) TIFFTag.TIFF_DOUBLE, geoKeys.modelTransformation()); writeTag((short) TAG_GEO_KEY_DIRECTORY, /* TIFF_SHORT */ geoKeys.keyDirectory()); @@ -437,6 +441,28 @@ final class Writer extends IOBase implements Flushable { return tiling; } + /** + * Writes a tag related to the location of the data. We use a separated method instead of + * inlining this code inside the {@code writeImageFileDirectory(…)} method for readability. + * It allows us to keep {@code writeImageFileDirectory(…)} formatted more like a table. + */ + private void writeTag(final short tag, final TileMatrix tiling, final boolean useStrips) throws IOException { + if (tiling.useStrips() == useStrips) { + final int value; + switch (tag) { + case TAG_TILE_WIDTH: value = tiling.tileWidth; break; + case TAG_TILE_LENGTH: + case TAG_ROWS_PER_STRIP: value = tiling.tileHeight; break; + case TAG_TILE_OFFSETS: + case TAG_STRIP_OFFSETS: tiling.offsetsTag = writeTag(tag, tiling.offsets); return; + case TAG_TILE_BYTE_COUNTS: + case TAG_STRIP_BYTE_COUNTS: tiling.lengthsTag = writeTag(tag, (short) TIFFTag.TIFF_LONG, tiling.lengths); return; + default: throw new AssertionError(tag); + } + writeTag(tag, (short) TIFFTag.TIFF_LONG, value); + } + } + /** * Writes a 32-bits or 64-bits offset, depending on whether the format is classic TIFF or BigTIFF. * diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java index 1ddf9f6dee..1bf8b24f65 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java @@ -149,6 +149,15 @@ public final class TileMatrix { Arrays.fill(lengths, tileSize); } + /** + * {@return whether to use strips instead of tiles}. + * This is {@code true} if image rows are not separated in tiles. + * The purpose of using strips is to avoid the restriction that tile size must be multiple of 16 bytes. + */ + public boolean useStrips() { + return numXTiles == 1; + } + /** * Rewrites the offsets and lengths arrays in the IFD. * This method shall be invoked after all tiles have been written. diff --git a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java index 663557865c..9acb11ed76 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java @@ -185,8 +185,9 @@ public final class WriterTest extends TestCase { initialize(DataType.BYTE, ByteOrder.BIG_ENDIAN, false, 1, 1, 1); writeImage(); verifyHeader(false, IOBase.BIG_ENDIAN); - verifyImageFileDirectory(Writer.MINIMAL_NUMBER_OF_TAGS, PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO, - new short[] {Byte.SIZE}); + verifyImageFileDirectory(Writer.COMMON_NUMBER_OF_TAGS - 1, // One less tag because stripped layout. + PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO, + new short[] {Byte.SIZE}, false); verifySampleValues(1); store.close(); } @@ -202,8 +203,9 @@ public final class WriterTest extends TestCase { initialize(DataType.BYTE, ByteOrder.LITTLE_ENDIAN, false, 1, 1, 1, FormatModifier.BIG_TIFF); writeImage(); verifyHeader(true, IOBase.LITTLE_ENDIAN); - verifyImageFileDirectory(Writer.MINIMAL_NUMBER_OF_TAGS, PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO, - new short[] {Byte.SIZE}); + verifyImageFileDirectory(Writer.COMMON_NUMBER_OF_TAGS - 1, // One less tag because stripped layout. + PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO, + new short[] {Byte.SIZE}, false); verifySampleValues(1); store.close(); } @@ -220,8 +222,9 @@ public final class WriterTest extends TestCase { initialize(DataType.BYTE, ByteOrder.LITTLE_ENDIAN, false, 1, 3, 4); writeImage(); verifyHeader(false, IOBase.LITTLE_ENDIAN); - verifyImageFileDirectory(Writer.MINIMAL_NUMBER_OF_TAGS, PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO, - new short[] {Byte.SIZE}); + verifyImageFileDirectory(Writer.COMMON_NUMBER_OF_TAGS, + PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO, + new short[] {Byte.SIZE}, true); verifySampleValues(1); store.close(); } @@ -238,8 +241,9 @@ public final class WriterTest extends TestCase { image.setColorModel(ColorModelFactory.createRGB(image.getSampleModel())); writeImage(); verifyHeader(false, IOBase.LITTLE_ENDIAN); - verifyImageFileDirectory(Writer.MINIMAL_NUMBER_OF_TAGS, PHOTOMETRIC_INTERPRETATION_RGB, - new short[] {Byte.SIZE, Byte.SIZE, Byte.SIZE}); + verifyImageFileDirectory(Writer.COMMON_NUMBER_OF_TAGS - 1, // One less tag because stripped layout. + PHOTOMETRIC_INTERPRETATION_RGB, + new short[] {Byte.SIZE, Byte.SIZE, Byte.SIZE}, false); verifySampleValues(3); store.close(); } @@ -256,8 +260,8 @@ public final class WriterTest extends TestCase { createGridGeometry(); writeImage(); verifyHeader(false, IOBase.LITTLE_ENDIAN); - verifyImageFileDirectory(Writer.MINIMAL_NUMBER_OF_TAGS + 3, // GeoTIFF adds 3 tags. - PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO, new short[] {Byte.SIZE}); + verifyImageFileDirectory(Writer.COMMON_NUMBER_OF_TAGS + 3 - 1, // 3 more for RGB, 1 less for strips. + PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO, new short[] {Byte.SIZE}, false); verifySampleValues(1); store.close(); } @@ -303,11 +307,11 @@ public final class WriterTest extends TestCase { * @param tagCount expected number of tags. * @param interpretation one of {@code PHOTOMETRIC_INTERPRETATION_} constants. * @param bitsPerSample expected number of bits per sample. The array length is the number of bands. + * @param isTiled whether the image uses tiles instead of strips. */ - private void verifyImageFileDirectory(int tagCount, final int interpretation, final short[] bitsPerSample) { + private void verifyImageFileDirectory(int tagCount, final int interpretation, final short[] bitsPerSample, final boolean isTiled) { @SuppressWarnings("LocalVariableHidesMemberVariable") final ByteBuffer data = this.data; - final boolean isTiled = true; final boolean isBigTIFF = store.getModifiers().contains(FormatModifier.BIG_TIFF); final boolean isBigEndian = ByteOrder.BIG_ENDIAN.equals(data.order()); assertEquals(tagCount, isBigTIFF ? data.getLong() : data.getShort()); @@ -349,9 +353,13 @@ public final class WriterTest extends TestCase { case TAG_SAMPLES_PER_PIXEL: expected = (short) bitsPerSample.length; break; case TAG_RESOLUTION_UNIT: expected = (short) RESOLUTION_UNIT_NONE; break; case TAG_TILE_WIDTH: expected = TILE_WIDTH; break; - case TAG_TILE_LENGTH: expected = TILE_HEIGHT; break; + case TAG_TILE_LENGTH: + case TAG_ROWS_PER_STRIP: expected = TILE_HEIGHT; break; + case TAG_STRIP_BYTE_COUNTS: case TAG_TILE_BYTE_COUNTS: expected = expectedTileByteCounts(); break; + case TAG_STRIP_OFFSETS: case TAG_TILE_OFFSETS: { + assertNull(tileOffsets); tileOffsets = getIntegers(value, count, isBigTIFF ? Writer.TIFF_ULONG : TIFFTag.TIFF_LONG); continue; }