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 65d1d016d105ae4b0144db6fc2462cd13ca44b3a Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Dec 18 14:53:03 2024 +0100 In case of error while writing a GeoTIFF file, restore the file to its previous state. --- .../apache/sis/storage/geotiff/GeoTiffStore.java | 1 - .../org/apache/sis/storage/geotiff/Writer.java | 108 ++++++++++----------- .../sis/storage/geotiff/writer/TileMatrix.java | 11 +-- .../apache/sis/io/stream/ChannelDataOutput.java | 28 ++++++ 4 files changed, 87 insertions(+), 61 deletions(-) diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java index 5dabda1abe..34925a609e 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java @@ -699,7 +699,6 @@ public class GeoTiffStore extends DataStore implements Aggregate { final long offsetIFD; try { offsetIFD = writer.append(image, grid, metadata); - writer.flush(); } finally { writer.synchronize(reader, true); } 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 43f7aa3d52..1ce0d8140c 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 @@ -146,13 +146,14 @@ final class Writer extends IOBase implements Flushable { int imageIndex; /** - * Offset where to write the next image, or {@code null} if writing a mandatory image (the first one). + * Offset where to write the offset of current image, or {@code null} when writing the first image of the file. * If null, the IFD offset is assumed already written and the {@linkplain #output} already at that position. - * Otherwise the value at the specified offset should be zero and will be updated if a new image is appended. + * Otherwise, the value at the specified offset is initially zero and will be updated after it become known. * - * @see #seekToNextImage() + * <p>After the image has been successfully written, this pointer is replaced by the pointer where the next + * image (if any) can be appended.</p> */ - private UpdatableWrite<?> nextIFD; + private UpdatableWrite<?> currentIFD; /** * All values that couldn't be written immediately. @@ -228,13 +229,13 @@ final class Writer extends IOBase implements Flushable { } /** - * Prepares the writer to write after the last images. + * Prepares the writer to write after the last image. * * @param reader the reader of images. */ final void moveAfterExisting(final Reader reader) throws IOException, DataStoreException { Class<? extends Number> type = isBigTIFF ? Long.class : Integer.class; - nextIFD = UpdatableWrite.ofZeroAt(reader.offsetOfWritableIFD(), type); + currentIFD = UpdatableWrite.ofZeroAt(reader.offsetOfWritableIFD(), type); imageIndex = reader.getImageCacheSize(); } @@ -295,37 +296,46 @@ final class Writer extends IOBase implements Flushable { throws IOException, DataStoreException { final var exportable = new ReformattedImage(image, this::processor, anyTileSize); - final TileMatrix tiles; + /* + * Offset where the image IFD will start. The current version appends the new image at the end of file. + * A future version could perform a more extensive search for free space in the middle of the file. + * It could be useful when images have been deleted. + */ + final long offsetIFD = output.length(); + if (currentIFD != null) { + currentIFD.setAsLong(offsetIFD); + writeOrQueue(currentIFD); + output.seek(offsetIFD); + } + /* + * Write the Image File Directory (IFD) followed by the raster data. + */ try { - tiles = writeImageFileDirectory(exportable, grid, metadata, false); - } finally { - largeTagData.clear(); // For making sure that there is no memory retention. - } - tiles.writeRasters(output); - wordAlign(output); - tiles.writeOffsetsAndLengths(output); - return tiles.offsetIFD; - } - - /** - * Sets the {@linkplain #output} position to where to write the next image. - * - * @return offset where the image IFD will start. This is the {@link #output} position. - * - * @todo Current version append the new image at the end of file. A future version could perform a more extensive - * search for free space in the middle of the file. It could be useful when images have been deleted. - */ - private long seekToNextImage() throws IOException { - if (nextIFD == null) { - // `output` is already at the right position. - return output.getStreamPosition(); - } - final long position = output.length(); - nextIFD.setAsLong(position); - writeOrQueue(nextIFD); - output.seek(position); - nextIFD = null; - return position; + final TileMatrix tiles; + try { + tiles = writeImageFileDirectory(exportable, grid, metadata, false); + } finally { + largeTagData.clear(); // For making sure that there is no memory retention. + } + tiles.writeRasters(output); + wordAlign(output); + tiles.writeOffsetsAndLengths(output); + flush(); + currentIFD = tiles.nextIFD; // Set only after the operation succeeded. + } catch (Throwable e) { + try { + deferredWrites.clear(); + output.truncate(offsetIFD); + if (currentIFD != null) { + currentIFD.setAsLong(0); + currentIFD.update(output); + } + } catch (Throwable more) { + e.addSuppressed(more); + } + throw e; + } + return offsetIFD; } /** @@ -410,14 +420,13 @@ final class Writer extends IOBase implements Flushable { * If the image has any unsupported feature, the exception should have been thrown before this point. * Now start writing the entries. The entries in an IFD must be sorted in ascending order by tag code. */ - output.flush(); // Makes room in the buffer for increasing our ability to modify past values. + output.flush(); // Make room in the buffer for increasing our ability to modify previous values. largeTagData.clear(); - final long offsetIFD = seekToNextImage(); final UpdatableWrite<?> tagCountWriter = isBigTIFF ? UpdatableWrite.of(output, (long) numberOfTags) : UpdatableWrite.of(output, (short) numberOfTags); - final var tiling = new TileMatrix(image.exportable, numPlanes, bitsPerSample, offsetIFD, + final var tiling = new TileMatrix(image.exportable, numPlanes, bitsPerSample, compression.method, compression.level, compression.predictor); /* * Reminder: TIFF tags should be written in increasing numerical order. @@ -472,7 +481,7 @@ final class Writer extends IOBase implements Flushable { */ tagCountWriter.setAsLong(numberOfTags); writeOrQueue(tagCountWriter); - nextIFD = writeOffset(0); + tiling.nextIFD = writeOffset(0); for (final TagValue tag : largeTagData) { UpdatableWrite<?> offset = tag.writeHere(output); if (offset != null) deferredWrites.add(offset); @@ -863,18 +872,6 @@ final class Writer extends IOBase implements Flushable { } } - /** - * Writes deferred values immediately to the output stream. - * - * @throws IOException if an error occurred while writing deferred data. - */ - private void flushDeferredWrites() throws IOException { - UpdatableWrite<?> change; - while ((change = deferredWrites.pollFirst()) != null) { - change.update(output); - } - } - /** * Sends to the writable channel any information that are still in buffers. * This method does not flush the writable channel itself. @@ -883,8 +880,11 @@ final class Writer extends IOBase implements Flushable { */ @Override public void flush() throws IOException { - flushDeferredWrites(); - output.flush(); + UpdatableWrite<?> change; + while ((change = deferredWrites.pollFirst()) != null) { + change.update(output); + } + output.flush(); // Flush buffer → channel, but not channel → disk. } /** 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 52947d685b..0292e39dbd 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 @@ -33,6 +33,7 @@ import java.awt.image.DataBufferDouble; import java.awt.image.RasterFormatException; import java.awt.image.SampleModel; import org.apache.sis.image.DataType; +import org.apache.sis.io.stream.UpdatableWrite; import org.apache.sis.io.stream.ChannelDataOutput; import org.apache.sis.io.stream.HyperRectangleWriter; import org.apache.sis.storage.DataStoreException; @@ -51,9 +52,10 @@ import org.apache.sis.pending.jdk.JDK18; */ public final class TileMatrix { /** - * Offset in {@link ChannelDataOutput} where the IFD starts. + * Where the next image will be written. + * This information is saved for the caller but not used by this class. */ - public final long offsetIFD; + public UpdatableWrite<?> nextIFD; /** * The images to write. @@ -121,17 +123,14 @@ public final class TileMatrix { * @param image the image to write. * @param numPlanes the number of banks (plane in TIFF terminology). * @param bitsPerSample number of bits per sample to write. - * @param offsetIFD offset in {@link ChannelDataOutput} where the IFD starts. * @param compression the compression method to apply. * @param compressionLevel compression level (0-9), or -1 for the default. * @param predictor the predictor to apply before to compress data. */ public TileMatrix(final RenderedImage image, final int numPlanes, final int[] bitsPerSample, - final long offsetIFD, final Compression compression, final int compressionLevel, - final Predictor predictor) + final Compression compression, final int compressionLevel, final Predictor predictor) { final int pixelSize, numArrays; - this.offsetIFD = offsetIFD; this.numPlanes = numPlanes; this.image = image; this.compression = compression; diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java index dcaef76e99..557a1eec7b 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java @@ -787,6 +787,34 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha } } + /** + * Truncates the stream to the given position. If the given size is greater than the current stream size, + * then this method does nothing. Otherwise this method set the stream size to the given size and, if the + * current position is greater than the new size, also set the stream to that position. + * + * @param size the position where to truncate. + * @throws IOException if the stream cannot be truncated to the given position. + */ + public final void truncate(final long size) throws IOException { + if (channel instanceof SeekableByteChannel) { + // Unconditional truncate because more data may exist after current position. + ((SeekableByteChannel) channel).truncate(toSeekableByteChannelPosition(size)); + if (size <= bufferOffset) { + bufferOffset = size; + bitPosition = 0; + buffer.limit(0); + return; + } + } + final long p = Math.subtractExact(size, bufferOffset); + if (p >= 0 && p <= buffer.limit()) { + buffer.limit((int) p); + bitPosition = 0; + } else { + throw new IOException(Resources.format(Resources.Keys.StreamIsForwardOnly_1, filename)); + } + } + /** * Moves to the given position in this stream. * If the given position is greater than the stream length, then the values of all bytes between