| /* |
| * 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.geotiff; |
| |
| import java.io.Flushable; |
| import java.io.IOException; |
| import java.nio.ByteOrder; |
| import java.nio.ByteBuffer; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Date; |
| import java.util.ArrayDeque; |
| import java.util.List; |
| import java.util.Deque; |
| import java.util.Queue; |
| import java.util.Set; |
| import java.awt.image.RenderedImage; |
| import java.awt.image.SampleModel; |
| import java.awt.image.BandedSampleModel; |
| import java.awt.image.IndexColorModel; |
| import javax.imageio.plugins.tiff.TIFFTag; |
| import static javax.imageio.plugins.tiff.BaselineTIFFTagSet.*; |
| import static javax.imageio.plugins.tiff.GeoTIFFTagSet.*; |
| import javax.measure.IncommensurableException; |
| import org.opengis.util.FactoryException; |
| import org.opengis.metadata.Metadata; |
| import org.apache.sis.image.ImageProcessor; |
| import org.apache.sis.coverage.grid.GridGeometry; |
| import org.apache.sis.coverage.privy.ImageUtilities; |
| import org.apache.sis.storage.DataStoreException; |
| import org.apache.sis.storage.DataStoreReferencingException; |
| import org.apache.sis.storage.ReadOnlyStorageException; |
| import org.apache.sis.storage.base.MetadataFetcher; |
| import org.apache.sis.io.stream.ChannelDataOutput; |
| import org.apache.sis.io.stream.UpdatableWrite; |
| import org.apache.sis.util.CharSequences; |
| import org.apache.sis.util.ArraysExt; |
| import org.apache.sis.util.privy.Numerics; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.math.Fraction; |
| import org.apache.sis.storage.geotiff.writer.TagValue; |
| import org.apache.sis.storage.geotiff.writer.TileMatrix; |
| import org.apache.sis.storage.geotiff.writer.GeoEncoder; |
| import org.apache.sis.storage.geotiff.writer.ReformattedImage; |
| |
| |
| /** |
| * An image writer for GeoTIFF files. This writer duplicates the implementations performed by other libraries, |
| * but we nevertheless provide our own writer in Apache SIS for better control on the internal file structure, |
| * such as keeping metadata close to each other (for Cloud Optimized GeoTIFF) and tiles order. |
| * This image writer can also handle <i>Big TIFF</i> images. |
| * |
| * <p>This writer supports only the tile layout. It does not support the writing of stripped images, |
| * 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 |
| * <a href="https://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf">here</a>.</p> |
| * |
| * @author Erwan Roussel (Geomatys) |
| * @author Martin Desruisseaux (Geomatys) |
| */ |
| final class Writer extends IOBase implements Flushable { |
| /** |
| * BigTIFF code for unsigned 64-bits integer type. |
| * |
| * @see Type#ULONG |
| */ |
| static final short TIFF_ULONG = 16; |
| |
| /** |
| * Sizes of a few TIFF tags used in this writer. |
| * |
| * @see #writeTag(short, short, int[]) |
| * @see #writeTag(short, short, double[]) |
| */ |
| private static final byte[] TYPE_SIZES = new byte[TIFF_ULONG + 1]; |
| static { |
| TYPE_SIZES[TIFFTag.TIFF_ASCII] = // TIFF uses US-ASCII encoding as bytes. |
| TYPE_SIZES[TIFFTag.TIFF_BYTE] = |
| TYPE_SIZES[TIFFTag.TIFF_SBYTE] = Byte.BYTES; |
| TYPE_SIZES[TIFFTag.TIFF_SHORT] = |
| TYPE_SIZES[TIFFTag.TIFF_SSHORT] = Short.BYTES; |
| TYPE_SIZES[TIFFTag.TIFF_LONG] = |
| TYPE_SIZES[TIFFTag.TIFF_SLONG] = Integer.BYTES; // What TIFF calls "long" is Java integer. |
| TYPE_SIZES[TIFFTag.TIFF_RATIONAL] = |
| TYPE_SIZES[TIFFTag.TIFF_SRATIONAL] = Integer.BYTES * 2; |
| TYPE_SIZES[TIFFTag.TIFF_FLOAT] = Float.BYTES; |
| TYPE_SIZES[TIFFTag.TIFF_DOUBLE] = Double.BYTES; |
| TYPE_SIZES[TIFFTag.TIFF_IFD_POINTER] = Integer.BYTES; // Assuming standard TIFF (not BigTIFF). |
| TYPE_SIZES[ TIFF_ULONG] = Long.BYTES; |
| } |
| |
| /** |
| * 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 are 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 COMMON_NUMBER_OF_TAGS = 16; |
| |
| /** |
| * The processor to use for transforming the image before to write it. |
| * Created only if needed. |
| * |
| * @see #processor() |
| */ |
| private ImageProcessor processor; |
| |
| /** |
| * The stream where to write the data. |
| */ |
| private final ChannelDataOutput output; |
| |
| /** |
| * Whether the lengths and offsets shall be written as 64-bits integers instead of 32-bits integers. |
| * |
| * @see #getFormat() |
| */ |
| private final boolean isBigTIFF; |
| |
| /** |
| * Index of the image to write. This information is not needed by the writer, but is |
| * needed by {@link WritableStore} for determining the "effectively added resource". |
| */ |
| int imageIndex; |
| |
| /** |
| * Offset where to write the next image, or {@code null} if writing a mandatory image (the first one). |
| * 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. |
| * |
| * @see #seekToNextImage() |
| */ |
| private UpdatableWrite<?> nextIFD; |
| |
| /** |
| * All values that couldn't be written immediately. |
| * Values shall be sorted in increasing order of stream position. |
| */ |
| private final Deque<UpdatableWrite<?>> deferredWrites = new ArrayDeque<>(); |
| |
| /** |
| * Write operations for tag having data too large for fitting inside a IFD tag entry. |
| * The writing of those data need to be delayed somewhere after the sequence of entries. |
| */ |
| private final Queue<TagValue> largeTagData = new ArrayDeque<>(); |
| |
| /** |
| * Number of TIFF 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; |
| |
| /** |
| * Creates a new GeoTIFF writer which will write data in the given output. |
| * |
| * @param store the store writing data. |
| * @param output where to write the bytes. |
| * @param options the format modifiers (BigTIFF, COG…), or {@code null} if none. |
| * @throws IOException if an error occurred while writing the first bytes to the stream. |
| */ |
| Writer(final GeoTiffStore store, final ChannelDataOutput output, final FormatModifier[] options) |
| throws IOException, DataStoreException |
| { |
| super(store); |
| this.output = output; |
| isBigTIFF = ArraysExt.contains(options, FormatModifier.BIG_TIFF); |
| /* |
| * Write the TIFF file header before first IFD. Stream position matter and must start at zero. |
| * Note that it does not necessarily mean that the stream has no bytes before current position. |
| */ |
| output.relocateOrigin(); |
| output.writeShort((output.buffer.order() == ByteOrder.LITTLE_ENDIAN) ? LITTLE_ENDIAN : BIG_ENDIAN); |
| output.writeShort(isBigTIFF ? BIG_TIFF : CLASSIC); |
| if (isBigTIFF) { |
| output.writeShort((short) Long.BYTES); // Byte size of offsets. |
| output.writeShort((short) 0); // Constant. |
| output.writeLong(Long.BYTES + 4*Short.BYTES); // Position of the first IFD. |
| } else { |
| output.writeInt(Integer.BYTES + 2*Short.BYTES); |
| } |
| } |
| |
| /** |
| * Creates a new writer which will append images a the end of an existing file. |
| * It is caller's responsibility to keep reader and writer positions consistent. |
| * This is done by invoking {@link #synchronize(Reader, boolean)} before and after |
| * write operations. |
| * |
| * @param reader the reader of the existing GeoTIFF file. |
| * @throws ReadOnlyStorageException if the channel is read-only. |
| * @throws DataStoreException if the writer cannot be created. |
| * @throws IOException if an I/O error occurred. |
| */ |
| Writer(final Reader reader) throws IOException, DataStoreException { |
| super(reader.store); |
| isBigTIFF = (reader.intSizeExpansion != 0); |
| try { |
| output = new ChannelDataOutput(reader.input); |
| } catch (ClassCastException e) { |
| throw new ReadOnlyStorageException(store.readOrWriteOnly(0), e); |
| } |
| moveAfterExisting(reader); |
| } |
| |
| /** |
| * Prepares the writer to write after the last images. |
| * |
| * @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); |
| imageIndex = reader.getImageCacheSize(); |
| } |
| |
| /** |
| * Ensures that the reader and writer positions are consistent. It is caller's responsibility to invoke |
| * {@link #flush()} before to invoke {@code synchronize(reader, true)}, unless the write operation failed. |
| * In the latter case, the caller should cancel the write operation if possible. |
| * |
| * @param reader the reader, or {@code null} if none. |
| * @param finish {@code false} if invoked before write operations, or {@code true} if invoked after. |
| */ |
| final void synchronize(final Reader reader, final boolean finish) throws IOException { |
| if (reader != null) { |
| if (finish) { |
| output.yield(reader.input); |
| } else { |
| reader.input.yield(output); |
| } |
| } |
| } |
| |
| /** |
| * {@return the modifiers (BigTIFF, COG…) used by this writer}. |
| */ |
| @Override |
| public final Set<FormatModifier> getModifiers() { |
| return isBigTIFF ? Set.of(FormatModifier.BIG_TIFF) : Set.of(); |
| } |
| |
| /** |
| * {@return the processor to use for reformatting the image before to write it}. |
| * The processor is created only when this method is first invoked. |
| */ |
| private ImageProcessor processor() { |
| if (processor == null) { |
| processor = new ImageProcessor(); |
| } |
| return processor; |
| } |
| |
| /** |
| * Encodes the given image to the output stream given at construction time. |
| * The image is appended after any previous images written before the given one. |
| * This method does not handle pyramids such as Cloud Optimized GeoTIFF (COG). |
| * It is caller responsibility to append image overviews if a pyramid is wanted. |
| * |
| * @param image the image to encode. |
| * @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. |
| * @return offset if {@link #output} where the Image File Directory (IFD) starts. |
| * @throws IOException if an error occurred while writing to the output. |
| * @throws DataStoreException if the given {@code image} has a property |
| * which is not supported by TIFF specification or by this writer. |
| */ |
| public final long append(final RenderedImage image, final GridGeometry grid, final Metadata metadata) |
| throws IOException, DataStoreException |
| { |
| final TileMatrix tiles; |
| try { |
| tiles = writeImageFileDirectory(new ReformattedImage(image, this::processor), 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; |
| } |
| |
| /** |
| * Writes the Image File Directory (IFD) 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 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. |
| * @return handler for writing offsets and lengths of the tiles to write. |
| * @throws IOException if an error occurred while writing to the output. |
| * @throws DataStoreException if the given {@code image} has a property |
| * which is not supported by TIFF specification or by this writer. |
| */ |
| private TileMatrix writeImageFileDirectory(final ReformattedImage image, final GridGeometry grid, final Metadata metadata, |
| final boolean overview) throws IOException, DataStoreException |
| { |
| final SampleModel sm = image.visibleBands.getSampleModel(); |
| Compression compression = store.getCompression().orElse(Compression.DEFLATE); |
| if (!ImageUtilities.isIntegerType(sm)) { |
| compression = compression.withPredictor(PREDICTOR_NONE); |
| } |
| /* |
| * Extract all image properties and metadata that we will need to encode in the Image File Directory. |
| * It allows us to know if we will be able to encode the image before we start writing in the stream, |
| * so that the TIFF file is not corrupted if we cannot write that image. It is also more convenient |
| * 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 = 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) { |
| numberOfTags++; |
| } |
| final int sampleFormat = image.getSampleFormat(); |
| final int[] bitsPerSample = sm.getSampleSize(); |
| final int numBands = sm.getNumBands(); |
| final int numPlanes, planarConfiguration; |
| if (sm instanceof BandedSampleModel) { |
| planarConfiguration = PLANAR_CONFIGURATION_PLANAR; |
| numPlanes = numBands; |
| } else { |
| planarConfiguration = PLANAR_CONFIGURATION_CHUNKY; |
| numPlanes = 1; |
| } |
| /* |
| * Metadata (optional) and GeoTIFF. They are managed by separated classes. |
| */ |
| final double[][] statistics = image.statistics(numBands); |
| final short[][] shortStats = toShorts(statistics, sampleFormat); |
| final MetadataFetcher<String> mf = new MetadataFetcher<>(store.dataLocale) { |
| @Override protected String convertDate(final Date date) { |
| return store.getDateFormat().format(date); |
| } |
| }; |
| mf.accept(metadata); |
| GeoEncoder geoKeys = null; |
| if (grid != null && grid.isDefined(GridGeometry.GRID_TO_CRS)) try { |
| geoKeys = new GeoEncoder(store.listeners()); |
| geoKeys.write(grid, mf); |
| } catch (FactoryException | IncommensurableException | RuntimeException e) { |
| throw new DataStoreReferencingException(e.getMessage(), e); |
| } |
| /* |
| * Conversion factor from physical size to pixel size. "Physical size" here should be understood as |
| * paper size, as suggested by the units of measurement which are restricted to inch or centimeters. |
| * This is not very useful for geospatial applications, except as aspect ratio. |
| */ |
| final Fraction xres = new Fraction(1, 1); // TODO |
| final Fraction yres = xres; |
| /* |
| * 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. |
| 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.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()); |
| writeTag((short) TAG_IMAGE_LENGTH, (short) TIFFTag.TIFF_LONG, image.visibleBands.getHeight()); |
| writeTag((short) TAG_BITS_PER_SAMPLE, (short) TIFFTag.TIFF_SHORT, bitsPerSample); |
| writeTag((short) TAG_COMPRESSION, (short) TIFFTag.TIFF_SHORT, compression.method.code); |
| writeTag((short) TAG_PHOTOMETRIC_INTERPRETATION, (short) TIFFTag.TIFF_SHORT, colorInterpretation); |
| 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); |
| writeTag((short) TAG_Y_RESOLUTION, /* TIFF_RATIONAL */ yres); |
| writeTag((short) TAG_PLANAR_CONFIGURATION, (short) TIFFTag.TIFF_SHORT, planarConfiguration); |
| writeTag((short) TAG_RESOLUTION_UNIT, (short) TIFFTag.TIFF_SHORT, RESOLUTION_UNIT_NONE); |
| writeTag((short) TAG_SOFTWARE, /* TIFF_ASCII */ mf.software); |
| writeTag((short) TAG_DATE_TIME, /* TIFF_ASCII */ mf.creationDate); |
| writeTag((short) TAG_ARTIST, /* TIFF_ASCII */ mf.party); |
| writeTag((short) TAG_HOST_COMPUTER, /* TIFF_ASCII */ mf.procedure); |
| if (compression.usePredictor()) { |
| writeTag((short) TAG_PREDICTOR, (short) TIFFTag.TIFF_SHORT, compression.predictor.code); |
| } |
| if (colorInterpretation == PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR) { |
| writeColorPalette((IndexColorModel) image.visibleBands.getColorModel(), 1L << bitsPerSample[0]); |
| } |
| 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()); |
| writeTag((short) TAG_GEO_DOUBLE_PARAMS, (short) TIFFTag.TIFF_DOUBLE, geoKeys.doubleParams()); |
| writeTag((short) TAG_GEO_ASCII_PARAMS, /* TIFF_ASCII */ geoKeys.asciiParams()); |
| } |
| /* |
| * At this point, all tags have been written. Update the number of tags, |
| * then write all values that couldn't be written directly in the tags. |
| */ |
| tagCountWriter.setAsLong(numberOfTags); |
| writeOrQueue(tagCountWriter); |
| nextIFD = writeOffset(0); |
| for (final TagValue tag : largeTagData) { |
| UpdatableWrite<?> offset = tag.writeHere(output); |
| if (offset != null) deferredWrites.add(offset); |
| } |
| 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. |
| * |
| * @param offset an initial guess of the offset value. |
| * @return a handler for updating later the offset with its actual value. |
| * @throws IOException if an error occurred while writing to the output. |
| */ |
| private UpdatableWrite<?> writeOffset(final long offset) throws IOException { |
| return isBigTIFF ? UpdatableWrite.of(output, offset) |
| : UpdatableWrite.of(output, (int) offset); |
| // No need to check the validity of above cast because the value is only a guess. |
| } |
| |
| /** |
| * Forces 16-bits word alignment. |
| * The TIFF 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 { |
| if ((output.getStreamPosition() & 1) != 0) { |
| output.writeByte(0); |
| } |
| } |
| |
| /** |
| * If the sample format is integer, cast statistics to integer type and clears the given array. |
| * Otherwise do nothing. This is used for choosing only one of {@code TAG_MIN_SAMPLE_VALUE} and |
| * {@code TAG_S_MIN_SAMPLE_VALUE} tags (same for maximum). |
| * |
| * @param statistics the statistic to potentially cast and clear. |
| * @param sampleFormat the sample format. |
| * @return statistics for the tags restricted to integer types. |
| */ |
| private static short[][] toShorts(final double[][] statistics, final int sampleFormat) { |
| final short[][] c = new short[statistics.length][]; |
| final long min, max; |
| switch (sampleFormat) { |
| case SAMPLE_FORMAT_UNSIGNED_INTEGER: min = 0; max = 0xFFFF; break; |
| case SAMPLE_FORMAT_SIGNED_INTEGER: min = Short.MIN_VALUE; max = Short.MAX_VALUE; break; |
| default: return c; |
| } |
| for (int j=0; j < c.length; j++) { |
| final double[] source = statistics[j]; |
| if (source != null) { |
| final short[] target = new short[source.length]; |
| for (int i=0; i < source.length; i++) { |
| target[i] = (short) Math.max(min, Math.min(max, Math.round(source[i]))); |
| // Unsigned values may look signed after the cast, but this is okay. |
| } |
| c[j] = target; |
| } |
| } |
| return c; |
| } |
| |
| /** |
| * Writes a new tag except for the value. This method ensures that the buffer has enough room for a full tag entry, |
| * so the caller can append an {@code int} (classical TIFF) or a {@code long} (big TIFF) directly in the buffer. |
| * |
| * @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}. |
| * @param count number of values. |
| * @return number of bytes available for the IFD entry value. |
| * @throws IOException if an error occurred while writing to the output. |
| * @throws ArithmeticException if the count is too large for the TIFF format in use. |
| */ |
| private int writeTagHeader(final short tag, final short type, final long count) throws IOException { |
| numberOfTags++; |
| output.ensureBufferAccepts(isBigTIFF |
| ? (2*Short.BYTES + 2*Long.BYTES) |
| : (2*Short.BYTES + 2*Integer.BYTES)); |
| final ByteBuffer buffer = output.buffer; |
| buffer.putShort(tag); |
| buffer.putShort(type); |
| if (isBigTIFF) { |
| buffer.putLong(count); |
| return Long.BYTES; |
| } else if ((count & Numerics.HIGH_BITS_MASK) == 0) { |
| // Note: unsigned integer may look negative after cast, this is okay. |
| buffer.putInt((int) count); |
| return Integer.BYTES; |
| } else { |
| throw new ArithmeticException(errors().getString(Errors.Keys.IntegerOverflow_1, Integer.SIZE)); |
| } |
| } |
| |
| /** |
| * Writes a tag value which is potentially too large for fitting in the IFD 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}. |
| * @param count number of values. |
| * @throws IOException if an error occurred while writing to the output. |
| * @throws ArithmeticException if the count is too large for the TIFF format in use. |
| */ |
| private TagValue writeLargeTag(final short tag, final short type, final long count, final TagValue deferred) throws IOException { |
| final long r = writeTagHeader(tag, type, count) - TYPE_SIZES[type] * count; |
| if (r >= 0) { |
| deferred.markAndWrite(output); |
| output.repeat(r, (byte) 0); |
| } else { |
| deferred.mark(writeOffset(0)); |
| largeTagData.add(deferred); |
| } |
| return deferred; |
| } |
| |
| /** |
| * 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. |
| * @throws IOException if an error occurred while writing to the output. |
| */ |
| private void writeColorPalette(final IndexColorModel cm, final long count) throws IOException { |
| final int numBands = 3; |
| writeLargeTag((short) TAG_COLOR_MAP, (short) TIFFTag.TIFF_SHORT, count * numBands, new TagValue() { |
| @Override protected void write(final ChannelDataOutput output) throws IOException { |
| final int n = (int) Math.min(cm.getMapSize(), count); |
| for (int band=0; band < numBands; band++) { |
| for (int i=0; i<n; i++) { |
| final int c; |
| switch (band) { |
| case 0: c = cm.getRed (i); break; |
| case 1: c = cm.getGreen(i); break; |
| case 2: c = cm.getBlue (i); break; |
| default: throw new AssertionError(band); |
| } |
| output.writeShort(c | (c << Byte.SIZE)); |
| } |
| output.repeat((count - n) * Short.BYTES, (byte) 0); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Writes a tag with string values stored as ASCII characters. |
| * The list of valid tag code is defined by TIFF 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. |
| * @throws IOException if an error occurred while writing to the output. |
| * @throws ArithmeticException if the combined string length is too large. |
| */ |
| private void writeTag(final short tag, final List<String> values) throws IOException { |
| if (values == null) { |
| return; |
| } |
| long count = 0; |
| final var chars = new byte[values.size()][]; |
| for (int i=0; i<chars.length; i++) { |
| String value = values.get(i).trim(); |
| if (StandardCharsets.US_ASCII.equals(store.encoding)) { |
| value = CharSequences.toASCII(value).toString(); |
| } |
| final byte[] ascii = value.getBytes(store.encoding); |
| int length = 0; |
| for (final byte c : ascii) { |
| if (c != 0) ascii[length++] = c; // Remove any NUL character that may appear in the string. |
| } |
| if (length != 0) { |
| count += length + 1L; // Count shall include the trailing NUL character. |
| chars[i] = ArraysExt.resize(ascii, length); |
| } |
| } |
| if (count != 0) { |
| writeLargeTag(tag, (short) TIFFTag.TIFF_ASCII, count, new TagValue() { |
| @Override protected void write(final ChannelDataOutput output) throws IOException { |
| for (final byte[] c : chars) { |
| if (c != null) { |
| output.write(c); |
| output.writeByte(0); |
| wordAlign(output); |
| } |
| } |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Writes a tag as a rational number. Rational numbers are made of two integers: the numerator and denominator, |
| * in that order. In BigTIFF format, those two numbers fit in the entry and this method returns {@code null}. |
| * In classical format, those two numbers do not fit and must be stored in an array after the directory entries. |
| * In such case, this method saves a handle for performing that deferred write operation later. |
| * |
| * @param tag the code of the tag to write, usually a constant defined by the TIFF specification. |
| * @param value numerator and denominator of the rational number to store, or {@code null} if none. |
| * @throws IOException if an error occurred while writing to the output. |
| */ |
| private void writeTag(final short tag, final Fraction value) throws IOException { |
| if (value == null) { |
| return; |
| } |
| writeLargeTag(tag, (short) TIFFTag.TIFF_RATIONAL, 1, new TagValue() { |
| @Override protected void write(final ChannelDataOutput output) throws IOException { |
| output.writeInt(value.numerator); |
| output.writeInt(value.denominator); |
| } |
| }); |
| } |
| |
| /** |
| * Writes a tag with values stored as 32 or 64 bits floating point numbers. |
| * The list of valid tag codes is defined by TIFF specification. |
| * |
| * @param tag the code of the tag to write, usually a constant defined by the TIFF specification. |
| * @param type {@code TIFF_FLOAT} or {@code TIFF_DOUBLE}. |
| * @param values the values to write as floating point values. |
| * @return a handler for rewriting the data if the array content changes. |
| * @throws IOException if an error occurred while writing to the output. |
| */ |
| private TagValue writeTag(final short tag, final short type, final double[] values) throws IOException { |
| if (values == null || values.length == 0) { |
| return null; |
| } |
| return writeLargeTag(tag, type, values.length, new TagValue() { |
| @Override protected void write(final ChannelDataOutput output) throws IOException { |
| switch (type) { |
| default: throw new AssertionError(type); |
| case TIFFTag.TIFF_DOUBLE: output.writeDoubles(values); break; |
| case TIFFTag.TIFF_FLOAT: { |
| for (final double value : values) { |
| output.writeFloat((float) value); |
| } |
| break; |
| } |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Writes a tag with an arbitrary number of values stored as 64 or 32 bits unsigned integers. |
| * The number of bits depends on whether this writer is writing BigTIFF or classic TIFF. |
| * |
| * @param tag the code of the tag to write, usually a constant defined by the TIFF specification. |
| * @param values the values to write as unsigned 64 or 32 bits integers. |
| * @return a handler for rewriting the data if the array content changes. |
| * @throws IOException if an error occurred while writing to the output. |
| */ |
| private TagValue writeTag(final short tag, final long[] values) throws IOException { |
| if (values == null || values.length == 0) { |
| return null; |
| } |
| final short type = isBigTIFF ? TIFF_ULONG : TIFFTag.TIFF_LONG; |
| return writeLargeTag(tag, type, values.length, new TagValue() { |
| @Override protected void write(final ChannelDataOutput output) throws IOException { |
| switch (type) { |
| default: throw new AssertionError(type); |
| case TIFF_ULONG: output.writeLongs(values); break; |
| case TIFFTag.TIFF_LONG: { |
| for (final long value : values) { |
| output.writeInt(Math.toIntExact(value)); |
| } |
| break; |
| } |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Writes a tag with an arbitrary number of values stored as 16 bits integers. |
| * |
| * @param tag the code of the tag to write, usually a constant defined by the TIFF specification. |
| * @param values the values to write as 16 bits integers. |
| * @return a handler for rewriting the data if the array content changes. |
| * @throws IOException if an error occurred while writing to the output. |
| */ |
| private TagValue writeTag(final short tag, final short[] values) throws IOException { |
| if (values == null || values.length == 0) { |
| return null; |
| } |
| return writeLargeTag(tag, (short) TIFFTag.TIFF_SHORT, values.length, new TagValue() { |
| @Override protected void write(final ChannelDataOutput output) throws IOException { |
| output.writeShorts(values); |
| } |
| }); |
| } |
| |
| /** |
| * Writes a tag with an arbitrary number of values stored as 16 or 32 bits unsigned integers. |
| * The list of valid tag codes is defined by TIFF specification. |
| * |
| * @param tag the code of the tag to write, usually a constant defined by the TIFF specification. |
| * @param type {@code TIFF_SHORT} or {@code TIFF_LONG}. |
| * @param values the values to write as unsigned integers. |
| * @return a handler for rewriting the data if the array content changes. |
| * @throws IOException if an error occurred while writing to the output. |
| */ |
| private TagValue writeTag(final short tag, final short type, final int[] values) throws IOException { |
| if (values == null || values.length == 0) { |
| return null; |
| } |
| return writeLargeTag(tag, type, values.length, new TagValue() { |
| @Override protected void write(final ChannelDataOutput output) throws IOException { |
| switch (type) { |
| default: throw new AssertionError(type); |
| case TIFFTag.TIFF_LONG: output.writeInts(values); break; |
| case TIFFTag.TIFF_SHORT: { |
| for (final int value : values) { |
| output.writeShort(value); |
| } |
| break; |
| } |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Writes a tag with a single value stored as a 16 or 32 bits unsigned integer. |
| * The list of valid tag codes is defined by TIFF specification. |
| * |
| * <p>The {@code TIFF_LONG} type is preferred when TIFF specification leaves the choice between 16 or 32 bits, |
| * because the TIFF structure is such as encoding those numbers on 16 bits does not provide any performance or |
| * space benefit. It was maybe a performance advantage when 16 bits processors were common.</p> |
| * |
| * @param tag the code of the tag to write, usually a constant defined by the TIFF specification. |
| * @param type {@code TIFF_SHORT} or {@code TIFF_LONG}. |
| * @param value the value to write as an unsigned integer. |
| * @throws IOException if an error occurred while writing to the output. |
| */ |
| private void writeTag(final short tag, final short type, final int value) throws IOException { |
| writeTagHeader(tag, type, 1); |
| final ByteBuffer buffer = output.buffer; |
| switch (type) { |
| case TIFFTag.TIFF_LONG: { // TIFF "long" is Java `int` but unsigned. |
| buffer.putInt(value); |
| break; |
| } |
| case TIFFTag.TIFF_SHORT: { |
| assert value >= 0 && value <= 0xFFFF : value; |
| buffer.putShort((short) value); // Value shall be left-aligned. |
| buffer.putShort((short) 0); // This space is lost. |
| break; |
| } |
| default: throw new AssertionError(type); |
| } |
| if (isBigTIFF) { |
| buffer.putInt(0); // Make the slot 64 bits large, left-aligned value. |
| } |
| } |
| |
| /** |
| * Executes the given deferred write operation immediately if doing so is cheap, |
| * or queue the operation for later execution otherwise. |
| * |
| * @param value the deferred value to write immediately or later. |
| * @throws IOException if an error occurred while writing the value. |
| */ |
| private void writeOrQueue(final UpdatableWrite<?> value) throws IOException { |
| if (!value.tryUpdateBuffer(output)) { |
| deferredWrites.add(value); |
| } |
| } |
| |
| /** |
| * 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. |
| * |
| * @throws IOException if an error occurred while closing this writer. |
| */ |
| @Override |
| public void flush() throws IOException { |
| flushDeferredWrites(); |
| output.flush(); |
| } |
| |
| /** |
| * Closes this writer and the associated writable channel. |
| * |
| * @throws IOException if an error occurred while closing this writer. |
| */ |
| @Override |
| public void close() throws IOException { |
| try (output.channel) { |
| flush(); |
| } |
| } |
| } |