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 c40700c0c774e7dbe299a2420ffa35e034fd3111 Author: Martin Desruisseaux <[email protected]> AuthorDate: Thu Oct 8 16:12:17 2026 +0200 Fix: invalid TIFF image written when the header contains short texts (2 characters in classical TIFF or 6 characters in BigTIFF). --- .../org/apache/sis/storage/geotiff/Writer.java | 33 ++++++++++++---------- .../apache/sis/storage/geotiff/reader/Type.java | 18 +++++++++++- .../org/apache/sis/storage/geotiff/WriterTest.java | 24 ++++++++++++---- 3 files changed, 53 insertions(+), 22 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 14c54d4c84..c31d21cb7c 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 @@ -76,7 +76,7 @@ import org.opengis.coverage.CannotEvaluateException; * because they are not useful for geospatial applications. This restriction does not reduce the set * of Java2D images that this writer can encode.</p> * - * <p>The TIFF format specification version 6.0 (June 3, 1992) is available + * <p>The <abbr>TIFF</abbr> format specification version 6.0 (June 3, 1992) is available * <a href="https://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf">here</a>.</p> * * @author Erwan Roussel (Geomatys) @@ -98,7 +98,7 @@ final class Writer extends IOBase implements OverviewIterator, Flushable { static final short TIFF_ULONG = 16; /** - * Sizes of a few TIFF tags used in this writer. + * Sizes of a few <abbr>TIFF</abbr> tags used in this writer, in number of bytes. * * @see #writeTag(short, short, int[]) * @see #writeTag(short, short, double[]) @@ -190,7 +190,7 @@ final class Writer extends IOBase implements OverviewIterator, Flushable { private final Queue<TagValue> largeTagData = new ArrayDeque<>(); /** - * Number of TIFF tag entries in the image being written. + * Number of <abbr>TIFF</abbr> tag entries in the image being written. * This is a temporary information used during the writing of an Image File Directory (IFD). */ private int numberOfTags; @@ -367,7 +367,7 @@ final class Writer extends IOBase implements OverviewIterator, Flushable { largeTagData.clear(); // For making sure that there is no memory retention. } tiles.writeRasters(output); - wordAlign(output); + wordAlign(); tiles.writeOffsetsAndLengths(output); flush(); currentIFD = tiles.nextIFD; // Set only after the operation succeeded. @@ -388,11 +388,11 @@ final class Writer extends IOBase implements OverviewIterator, Flushable { } /** - * Writes the Image File Directory (IFD) of the given image at the current {@link #output} position. + * Writes the Image File Directory (<abbr>IFD</abbr>) of the given image at the current {@link #output} position. * This method does not write the pixel values. Those values must be written by the caller. * This separation makes possible to write directories in any order compared to pixel data. * - * @param image the image for which to write the IFD. + * @param image the image for which to write the <abbr>IFD</abbr>. * @param grid mapping from pixel coordinates to "real world" coordinates, or {@code null} if none. * @param metadata title, author and other information, or {@code null} if none. * @param oveverview whether the image is an overview of another image. @@ -530,7 +530,10 @@ final class Writer extends IOBase implements OverviewIterator, Flushable { tiling.nextIFD = writeOffset(0); for (final TagValue tag : largeTagData) { UpdatableWrite<?> offset = tag.writeHere(output); - if (offset != null) deferredWrites.add(offset); + wordAlign(); + if (offset != null) { + deferredWrites.add(offset); + } } return tiling; } @@ -558,7 +561,7 @@ final class Writer extends IOBase implements OverviewIterator, Flushable { } /** - * Writes a 32-bits or 64-bits offset, depending on whether the format is classic TIFF or BigTIFF. + * Writes a 32-bits or 64-bits offset, depending on whether the format is classic <abbr>TIFF</abbr> or BigTIFF. * * @param offset an initial guess of the offset value. * @return a handler for updating later the offset with its actual value. @@ -572,12 +575,12 @@ final class Writer extends IOBase implements OverviewIterator, Flushable { /** * Forces 16-bits word alignment. - * The TIFF specification requires that tag values are aligned. + * The <abbr>TIFF</abbr> specification requires that tag values are aligned. * * @param channel the channel on which to apply 16-bits word alignment. * @throws IOException if an error occurred while writing to the output stream. */ - private static void wordAlign(final ChannelDataOutput output) throws IOException { + private void wordAlign() throws IOException { if ((output.getStreamPosition() & 1) != 0) { output.writeByte(0); } @@ -646,7 +649,7 @@ final class Writer extends IOBase implements OverviewIterator, Flushable { } /** - * Writes a tag value which is potentially too large for fitting in the IFD entry. + * Writes a tag value which is potentially too large for fitting in the <abbr>IFD</abbr> entry. * * @param tag the code of the tag to write, usually a constant defined by the TIFF specification. * @param type one of the {@link TIFFTag} constants such as {@code TIFF_SHORT} or {@code TIFF_LONG}. @@ -670,7 +673,7 @@ final class Writer extends IOBase implements OverviewIterator, Flushable { * Writes the color map tag. * * @param cm color model from which to read color values. - * @param count number of colors to write, <strong>not</strong> multiplied by 3 for the RGB bands. + * @param count number of colors to write, <strong>not</strong> multiplied by 3 for the <abbr>RGB</abbr> bands. * @throws IOException if an error occurred while writing to the output. */ private void writeColorPalette(final IndexColorModel cm, final long count) throws IOException { @@ -696,8 +699,8 @@ final class Writer extends IOBase implements OverviewIterator, Flushable { } /** - * Writes a tag with string values stored as ASCII characters. - * The list of valid tag code is defined by TIFF specification. + * Writes a tag with string values stored as <abbr>ASCII</abbr> characters. + * The list of valid tag codes is defined by <abbr>TIFF</abbr> specification. * * @param tag the code of the tag to write, usually a constant defined by the TIFF specification. * @param values the values to write, or {@code null} if none. @@ -732,9 +735,9 @@ final class Writer extends IOBase implements OverviewIterator, Flushable { if (c != null) { output.write(c); output.writeByte(0); - wordAlign(output); } } + // Do not invoke `wordAlign()` because the number of bytes written must be exactly `count`. } }); } diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Type.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Type.java index 9f45f09b1e..db214e7a58 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Type.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Type.java @@ -333,15 +333,19 @@ public enum Type { * 8-bits byte that contains a 7-bit ASCII code. In a string of ASCII characters, the last byte must be NUL * (binary zero). The string length (including the NUL byte) is the {@code count} field before the string. * NUL bytes may also appear in the middle of the string for separating its content into multi-strings. + * NUL byte shall appear only once, not counting the padding byte after all strings. + * * <ul> * <li>TIFF name: {@code ASCII}</li> * <li>TIFF code: 2</li> * </ul> + * + * Non-standard extension: if NUL is missing, all remaining characters are taken anyway. */ ASCII(TIFFTag.TIFF_ASCII, Byte.BYTES, false) { @Override public String[] readAsStrings(final ChannelDataInput input, final long length, final Charset charset) throws IOException { final byte[] chars = input.readBytes(Math.toIntExact(length)); - String[] lines = new String[1]; // We will usually have exactly one string. + String[] lines = new String[1]; // We will usually have exactly one string. int count = 0, lower = 0; for (int i=0; i<chars.length; i++) { if (chars[i] == 0) { @@ -349,6 +353,18 @@ public enum Type { lines = Arrays.copyOf(lines, 2*count); } lines[count++] = new String(chars, lower, i-lower, charset); + lower = i + 1; + } + } + // Non-standard extension: take remaining characters if any. + final int n = chars.length - lower; + if (n > 0) { + final String remaining = new String(chars, lower, n, charset).trim(); + if (!remaining.isBlank()) { + if (count >= lines.length) { + return ArraysExt.append(lines, remaining); + } + lines[count++] = remaining; } } return ArraysExt.resize(lines, count); 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 c5779a49e5..f92e6243c3 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 @@ -43,6 +43,9 @@ import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.coverage.grid.GridOrientation; import org.apache.sis.image.DataType; import org.apache.sis.image.internal.shared.ColorModelBuilder; +import org.apache.sis.metadata.iso.DefaultMetadata; +import org.apache.sis.metadata.iso.citation.DefaultCitation; +import org.apache.sis.metadata.iso.identification.DefaultDataIdentification; import org.apache.sis.geometry.Envelope2D; // Test dependencies @@ -176,10 +179,19 @@ public final class WriterTest extends TestCase { * @throws DataStoreException if the image is incompatible with writer capability. */ private void writeImage() throws IOException, DataStoreException { - store.append(image, gridGeometry, null); + final var metadata = new DefaultMetadata(); + final var id = new DefaultDataIdentification(); + id.setCitation(new DefaultCitation("abcd")); // Short enough for being stored directly in BigTIFF slot. + assertTrue(metadata.getIdentificationInfo().add(id)); + store.append(image, gridGeometry, metadata); data.clear().limit(Math.toIntExact(output.size())); } + /** + * Common number of tags which will be written, including the metadata added by {@link #writeImage()}. + */ + private static final int COMMON_NUMBER_OF_TAGS = Writer.COMMON_NUMBER_OF_TAGS + 1; + /** * Tests the writing a gray scale image made of a single tile with pixels on 8 bits. * This is the simplest type of image. @@ -193,7 +205,7 @@ public final class WriterTest extends TestCase { FormatModifier.ANY_TILE_SIZE); writeImage(); verifyHeader(false, IOBase.BIG_ENDIAN); - verifyImageFileDirectory(Writer.COMMON_NUMBER_OF_TAGS - 1, // One less tag because stripped layout. + verifyImageFileDirectory(COMMON_NUMBER_OF_TAGS - 1, // One less tag because stripped layout. PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO, new short[] {Byte.SIZE}, false); verifySampleValues(1); @@ -212,7 +224,7 @@ public final class WriterTest extends TestCase { FormatModifier.ANY_TILE_SIZE, FormatModifier.BIG_TIFF); writeImage(); verifyHeader(true, IOBase.LITTLE_ENDIAN); - verifyImageFileDirectory(Writer.COMMON_NUMBER_OF_TAGS - 1, // One less tag because stripped layout. + verifyImageFileDirectory(COMMON_NUMBER_OF_TAGS - 1, // One less tag because stripped layout. PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO, new short[] {Byte.SIZE}, false); verifySampleValues(1); @@ -231,7 +243,7 @@ public final class WriterTest extends TestCase { initialize(DataType.BYTE, ByteOrder.LITTLE_ENDIAN, false, 1, 3, 4, FormatModifier.ANY_TILE_SIZE); writeImage(); verifyHeader(false, IOBase.LITTLE_ENDIAN); - verifyImageFileDirectory(Writer.COMMON_NUMBER_OF_TAGS, + verifyImageFileDirectory(COMMON_NUMBER_OF_TAGS, PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO, new short[] {Byte.SIZE}, true); verifySampleValues(1); @@ -250,7 +262,7 @@ public final class WriterTest extends TestCase { image.setColorModel(new ColorModelBuilder().createRGB(image.getSampleModel())); writeImage(); verifyHeader(false, IOBase.LITTLE_ENDIAN); - verifyImageFileDirectory(Writer.COMMON_NUMBER_OF_TAGS - 1, // One less tag because stripped layout. + verifyImageFileDirectory(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); @@ -287,7 +299,7 @@ public final class WriterTest extends TestCase { * So the test cannot expects an exact number of tags. */ int tagCount = data.getShort(data.position()); - assertTrue(tagCount >= Writer.COMMON_NUMBER_OF_TAGS + 3 - 1); // 3 more for RGB, 1 less for strips. + assertTrue(tagCount >= COMMON_NUMBER_OF_TAGS + 3 - 1); // 3 more for RGB, 1 less for strips. verifyImageFileDirectory(tagCount, PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO, new short[] {Byte.SIZE}, false); verifySampleValues(1); store.close();
