diff --git a/src/main/java/mil/nga/tiff/FileDirectory.java b/src/main/java/mil/nga/tiff/FileDirectory.java index f8db6ce..33af6fb 100644 --- a/src/main/java/mil/nga/tiff/FileDirectory.java +++ b/src/main/java/mil/nga/tiff/FileDirectory.java @@ -163,7 +163,7 @@ public FileDirectory(SortedSet entries, "JPEG compression not supported: " + compression); break; case TiffConstants.COMPRESSION_DEFLATE: - case TiffConstants.COMPRESSION_PKZIP_DEFLATE: + case TiffConstants.COMPRESSION_PKZIP_DEFLATE: // Deprecated but supported for backward compatibility decoder = new DeflateCompression(); break; case TiffConstants.COMPRESSION_PACKBITS: diff --git a/src/main/java/mil/nga/tiff/TiffReader.java b/src/main/java/mil/nga/tiff/TiffReader.java index 92f552d..28dff15 100644 --- a/src/main/java/mil/nga/tiff/TiffReader.java +++ b/src/main/java/mil/nga/tiff/TiffReader.java @@ -48,9 +48,32 @@ public static TIFFImage readTiff(File file) throws IOException { */ public static TIFFImage readTiff(File file, boolean cache) throws IOException { - byte[] bytes = IOUtils.fileBytes(file); - TIFFImage tiffImage = readTiff(bytes, cache); - return tiffImage; + // Auto-detect streaming for large files (>100MB) + if (file.length() > 100 * 1024 * 1024) { + return readTiffStreaming(file, cache); + } else { + byte[] bytes = IOUtils.fileBytes(file); + TIFFImage tiffImage = readTiff(bytes, cache); + return tiffImage; + } + } + + /** + * Read a TIFF from a file using streaming mode + * + * @param file + * TIFF file + * @param cache + * true to cache tiles and strips + * @return TIFF image + * @throws IOException + * upon failure to read + */ + public static TIFFImage readTiffStreaming(File file, boolean cache) + throws IOException { + try (ByteReader reader = new ByteReader(file)) { + return readTiff(reader, cache); + } } /** @@ -215,7 +238,7 @@ private static TIFFImage parseTIFFImage(ByteReader reader, long byteOffset, long typeCount = reader.readUnsignedInt(); // Save off the next byte to read location - int nextByte = reader.getNextByte(); + long nextByte = reader.getNextByte(); // Read the field values Object values = readFieldValues(reader, fieldTag, fieldType, diff --git a/src/main/java/mil/nga/tiff/TiffWriter.java b/src/main/java/mil/nga/tiff/TiffWriter.java index 50b5bdb..d078d88 100644 --- a/src/main/java/mil/nga/tiff/TiffWriter.java +++ b/src/main/java/mil/nga/tiff/TiffWriter.java @@ -49,9 +49,32 @@ public class TiffWriter { */ public static void writeTiff(File file, TIFFImage tiffImage) throws IOException { - ByteWriter writer = new ByteWriter(); - writeTiff(file, writer, tiffImage); - writer.close(); + // Auto-detect streaming for large images + long estimatedSize = estimateImageSize(tiffImage); + if (estimatedSize > 100 * 1024 * 1024) { // >100MB + writeTiffStreaming(file, tiffImage); + } else { + ByteWriter writer = new ByteWriter(); + writeTiff(file, writer, tiffImage); + writer.close(); + } + } + + /** + * Write a TIFF to a file using streaming mode + * + * @param file + * file to create + * @param tiffImage + * TIFF image + * @throws IOException + * upon failure to write + */ + public static void writeTiffStreaming(File file, TIFFImage tiffImage) + throws IOException { + try (ByteWriter writer = new ByteWriter(file)) { + writeTiff(writer, tiffImage); + } } /** @@ -158,7 +181,7 @@ private static void writeImageFileDirectories(ByteWriter writer, populateRasterEntries(fileDirectory); // Track of the starting byte of this directory - int startOfDirectory = writer.size(); + long startOfDirectory = writer.size(); long afterDirectory = startOfDirectory + fileDirectory.size(); long afterValues = startOfDirectory + fileDirectory.sizeWithValues(); @@ -429,7 +452,7 @@ private static void writeStripRasters(ByteWriter writer, * file directory * @return encoder */ - @SuppressWarnings("deprecation") + private static CompressionEncoder getEncoder(FileDirectory fileDirectory) { CompressionEncoder encoder = null; @@ -477,15 +500,22 @@ private static CompressionEncoder getEncoder(FileDirectory fileDirectory) { /** * Write filler 0 bytes - * + * * @param writer * byte writer * @param count * number of 0 bytes to write + * @throws IOException + * upon failure to write */ - private static void writeFillerBytes(ByteWriter writer, long count) { - for (long i = 0; i < count; i++) { - writer.writeUnsignedByte((short) 0); + private static void writeFillerBytes(ByteWriter writer, long count) throws IOException { + try { + for (long i = 0; i < count; i++) { + writer.writeUnsignedByte((short) 0); + } + } catch (TiffException e) { + // Re-throw TiffException as IOException for method signature compatibility + throw new IOException("Failed to write filler bytes", e); } } @@ -578,4 +608,40 @@ private static int writeValues(ByteWriter writer, FileDirectoryEntry entry) return bytesWritten; } + /** + * Estimate the size of a TIFF image for auto-detection of streaming mode + * + * @param tiffImage + * TIFF image + * @return estimated size in bytes + */ + private static long estimateImageSize(TIFFImage tiffImage) { + long totalSize = 0; + + for (FileDirectory directory : tiffImage.getFileDirectories()) { + Number width = directory.getImageWidth(); + Number height = directory.getImageHeight(); + Integer samplesPerPixel = directory.getSamplesPerPixel(); + + if (width != null && height != null && samplesPerPixel != null) { + // Estimate based on uncompressed size + long pixels = width.longValue() * height.longValue(); + int bytesPerSample = 1; // Default to 1 byte per sample + + // Adjust based on bits per sample if available + List bitsPerSample = directory.getBitsPerSample(); + if (bitsPerSample != null && !bitsPerSample.isEmpty()) { + bytesPerSample = Math.max(1, bitsPerSample.get(0) / 8); + } + + totalSize += pixels * samplesPerPixel * bytesPerSample; + } + } + + // Add overhead for headers, IFDs, etc. (conservative estimate) + totalSize += 64 * 1024; // 64KB overhead + + return totalSize; + } + } diff --git a/src/main/java/mil/nga/tiff/compression/PackbitsCompression.java b/src/main/java/mil/nga/tiff/compression/PackbitsCompression.java index fd68008..3e6a567 100644 --- a/src/main/java/mil/nga/tiff/compression/PackbitsCompression.java +++ b/src/main/java/mil/nga/tiff/compression/PackbitsCompression.java @@ -24,26 +24,34 @@ public byte[] decode(byte[] bytes, ByteOrder byteOrder) { ByteArrayOutputStream decodedStream = new ByteArrayOutputStream(); - while (reader.hasByte()) { - int header = reader.readByte(); - if (header != -128) { - if (header < 0) { - int next = reader.readUnsignedByte(); - header = -header; - for (int i = 0; i <= header; i++) { - decodedStream.write(next); - } - } else { - for (int i = 0; i <= header; i++) { - decodedStream.write(reader.readUnsignedByte()); + try { + while (reader.hasByte()) { + int header = reader.readByte(); + if (header != -128) { + if (header < 0) { + int next = reader.readUnsignedByte(); + header = -header; + for (int i = 0; i <= header; i++) { + decodedStream.write(next); + } + } else { + for (int i = 0; i <= header; i++) { + decodedStream.write(reader.readUnsignedByte()); + } } } } - } - byte[] decoded = decodedStream.toByteArray(); + byte[] decoded = decodedStream.toByteArray(); - return decoded; + return decoded; + } finally { + try { + reader.close(); + } catch (Exception e) { + // Ignore close exception + } + } } /** diff --git a/src/main/java/mil/nga/tiff/compression/Predictor.java b/src/main/java/mil/nga/tiff/compression/Predictor.java index 6b2c1e1..b2deb88 100644 --- a/src/main/java/mil/nga/tiff/compression/Predictor.java +++ b/src/main/java/mil/nga/tiff/compression/Predictor.java @@ -81,6 +81,8 @@ public static byte[] decode(byte[] bytes, int predictor, int width, bytes = writer.getBytes(); + } catch (IOException e) { + e.printStackTrace(); } finally { writer.close(); } diff --git a/src/main/java/mil/nga/tiff/io/ByteReader.java b/src/main/java/mil/nga/tiff/io/ByteReader.java index e295218..bfd162a 100644 --- a/src/main/java/mil/nga/tiff/io/ByteReader.java +++ b/src/main/java/mil/nga/tiff/io/ByteReader.java @@ -1,27 +1,31 @@ package mil.nga.tiff.io; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.util.Arrays; import mil.nga.tiff.util.TiffException; /** - * Read through a byte array - * + * Read through a byte array or file with streaming support + * * @author osbornb */ -public class ByteReader { +public class ByteReader implements AutoCloseable { /** * Next byte index to read */ - private int nextByte = 0; + private long nextByte = 0; /** - * Bytes to read + * Bytes to read (for memory mode) */ private final byte[] bytes; @@ -31,8 +35,16 @@ public class ByteReader { private ByteOrder byteOrder = null; /** - * Constructor - * + * Streaming mode fields + */ + private final boolean isStreaming; + private final RandomAccessFile randomAccessFile; + private final FileChannel channel; + private final long fileSize; + + /** + * Constructor for memory mode + * * @param bytes * bytes */ @@ -41,8 +53,8 @@ public ByteReader(byte[] bytes) { } /** - * Constructor - * + * Constructor for memory mode + * * @param bytes * bytes * @param byteOrder @@ -51,29 +63,71 @@ public ByteReader(byte[] bytes) { public ByteReader(byte[] bytes, ByteOrder byteOrder) { this.bytes = bytes; this.byteOrder = byteOrder; + this.isStreaming = false; + this.randomAccessFile = null; + this.channel = null; + this.fileSize = bytes != null ? bytes.length : 0; + } + + /** + * Constructor for streaming mode + * + * @param file + * file to read + * @throws IOException + * upon file access error + */ + public ByteReader(File file) throws IOException { + this(file, ByteOrder.nativeOrder()); + } + + /** + * Constructor for streaming mode + * + * @param file + * file to read + * @param byteOrder + * byte order + * @throws IOException + * upon file access error + */ + public ByteReader(File file, ByteOrder byteOrder) throws IOException { + this.bytes = null; + this.byteOrder = byteOrder; + this.isStreaming = true; + this.randomAccessFile = new RandomAccessFile(file, "r"); + this.channel = randomAccessFile.getChannel(); + this.fileSize = channel.size(); } /** * Get the next byte to be read - * + * * @return next byte to be read */ - public int getNextByte() { + public long getNextByte() { return nextByte; } /** * Set the next byte to be read - * + * * @param nextByte * next byte */ public void setNextByte(long nextByte) { - if (nextByte >= bytes.length) { + if (nextByte >= fileSize) { throw new TiffException("Byte offset out of range. Total Bytes: " - + bytes.length + ", Byte offset: " + nextByte); + + fileSize + ", Byte offset: " + nextByte); + } + this.nextByte = nextByte; + if (isStreaming) { + try { + channel.position(nextByte); + } catch (IOException e) { + throw new TiffException("Failed to set position: " + nextByte, e); + } } - this.nextByte = (int) nextByte; } /** @@ -111,7 +165,7 @@ public boolean hasByte() { * byte offset * @return true more bytes left to read */ - public boolean hasByte(int offset) { + public boolean hasByte(long offset) { return hasBytes(offset, 1); } @@ -135,8 +189,8 @@ public boolean hasBytes(int count) { * number of bytes * @return true if has at least the number of bytes left */ - public boolean hasBytes(int offset, int count) { - return offset + count <= bytes.length; + public boolean hasBytes(long offset, int count) { + return offset + count <= fileSize; } /** @@ -165,12 +219,19 @@ public String readString(int num) throws UnsupportedEncodingException { * @throws UnsupportedEncodingException * upon string encoding error */ - public String readString(int offset, int num) + public String readString(long offset, int num) throws UnsupportedEncodingException { verifyRemainingBytes(offset, num); String value = null; - if (num != 1 || bytes[offset] != 0) { - value = new String(bytes, offset, num, StandardCharsets.US_ASCII); + if (isStreaming) { + byte[] stringBytes = readBytesInternal(offset, num); + if (num != 1 || stringBytes[0] != 0) { + value = new String(stringBytes, 0, num, StandardCharsets.US_ASCII); + } + } else { + if (num != 1 || bytes[(int)offset] != 0) { + value = new String(bytes, (int)offset, num, StandardCharsets.US_ASCII); + } } return value; } @@ -193,10 +254,13 @@ public byte readByte() { * byte offset * @return byte */ - public byte readByte(int offset) { + public byte readByte(long offset) { verifyRemainingBytes(offset, 1); - byte value = bytes[offset]; - return value; + if (isStreaming) { + return readBytesInternal(offset, 1)[0]; + } else { + return bytes[(int)offset]; + } } /** @@ -217,7 +281,7 @@ public short readUnsignedByte() { * byte offset * @return unsigned byte as short */ - public short readUnsignedByte(int offset) { + public short readUnsignedByte(long offset) { return ((short) (readByte(offset) & 0xff)); } @@ -243,10 +307,9 @@ public byte[] readBytes(int num) { * number of bytes * @return bytes */ - public byte[] readBytes(int offset, int num) { + public byte[] readBytes(long offset, int num) { verifyRemainingBytes(offset, num); - byte[] readBytes = Arrays.copyOfRange(bytes, offset, offset + num); - return readBytes; + return readBytesInternal(offset, num); } /** @@ -267,10 +330,10 @@ public short readShort() { * byte offset * @return short */ - public short readShort(int offset) { + public short readShort(long offset) { verifyRemainingBytes(offset, 2); - short value = ByteBuffer.wrap(bytes, offset, 2).order(byteOrder) - .getShort(); + byte[] shortBytes = readBytesInternal(offset, 2); + short value = ByteBuffer.wrap(shortBytes).order(byteOrder).getShort(); return value; } @@ -292,7 +355,7 @@ public int readUnsignedShort() { * byte offset * @return unsigned short as int */ - public int readUnsignedShort(int offset) { + public int readUnsignedShort(long offset) { return (readShort(offset) & 0xffff); } @@ -314,9 +377,10 @@ public int readInt() { * byte offset * @return integer */ - public int readInt(int offset) { + public int readInt(long offset) { verifyRemainingBytes(offset, 4); - int value = ByteBuffer.wrap(bytes, offset, 4).order(byteOrder).getInt(); + byte[] intBytes = readBytesInternal(offset, 4); + int value = ByteBuffer.wrap(intBytes).order(byteOrder).getInt(); return value; } @@ -338,7 +402,7 @@ public long readUnsignedInt() { * byte offset * @return unsigned int as long */ - public long readUnsignedInt(int offset) { + public long readUnsignedInt(long offset) { return ((long) readInt(offset) & 0xffffffffL); } @@ -360,10 +424,10 @@ public float readFloat() { * byte offset * @return float */ - public float readFloat(int offset) { + public float readFloat(long offset) { verifyRemainingBytes(offset, 4); - float value = ByteBuffer.wrap(bytes, offset, 4).order(byteOrder) - .getFloat(); + byte[] floatBytes = readBytesInternal(offset, 4); + float value = ByteBuffer.wrap(floatBytes).order(byteOrder).getFloat(); return value; } @@ -385,10 +449,10 @@ public double readDouble() { * byte offset * @return double */ - public double readDouble(int offset) { + public double readDouble(long offset) { verifyRemainingBytes(offset, 8); - double value = ByteBuffer.wrap(bytes, offset, 8).order(byteOrder) - .getDouble(); + byte[] doubleBytes = readBytesInternal(offset, 8); + double value = ByteBuffer.wrap(doubleBytes).order(byteOrder).getDouble(); return value; } @@ -397,8 +461,41 @@ public double readDouble(int offset) { * * @return byte length */ - public int byteLength() { - return bytes.length; + public long byteLength() { + return fileSize; + } + + /** + * Close the reader and release resources + */ + @Override + public void close() throws IOException { + if (isStreaming && randomAccessFile != null) { + randomAccessFile.close(); + } + } + + /** + * Internal method to read bytes from file or memory + * + * @param offset + * byte offset + * @param num + * number of bytes + * @return bytes + */ + private byte[] readBytesInternal(long offset, int num) { + if (isStreaming) { + try { + ByteBuffer buffer = ByteBuffer.allocate(num); + channel.read(buffer, offset); + return buffer.array(); + } catch (IOException e) { + throw new TiffException("Failed to read bytes at offset " + offset, e); + } + } else { + return Arrays.copyOfRange(bytes, (int)offset, (int)offset + num); + } } /** @@ -410,11 +507,11 @@ public int byteLength() { * @param bytesToRead * number of bytes to read */ - private void verifyRemainingBytes(int offset, int bytesToRead) { - if (offset + bytesToRead > bytes.length) { + private void verifyRemainingBytes(long offset, int bytesToRead) { + if (offset + bytesToRead > fileSize) { throw new TiffException( "No more remaining bytes to read. Total Bytes: " - + bytes.length + ", Byte offset: " + offset + + fileSize + ", Byte offset: " + offset + ", Attempted to read: " + bytesToRead); } } diff --git a/src/main/java/mil/nga/tiff/io/ByteWriter.java b/src/main/java/mil/nga/tiff/io/ByteWriter.java index c5bc372..fcaa44b 100644 --- a/src/main/java/mil/nga/tiff/io/ByteWriter.java +++ b/src/main/java/mil/nga/tiff/io/ByteWriter.java @@ -1,21 +1,26 @@ package mil.nga.tiff.io; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.channels.FileChannel; + +import mil.nga.tiff.util.TiffException; /** - * Write a byte array - * + * Write a byte array or file with streaming support + * * @author osbornb */ -public class ByteWriter { +public class ByteWriter implements AutoCloseable { /** - * Output stream to write bytes to + * Output stream to write bytes to (for memory mode) */ - private final ByteArrayOutputStream os = new ByteArrayOutputStream(); + private final ByteArrayOutputStream os; /** * Byte order @@ -23,28 +28,78 @@ public class ByteWriter { private ByteOrder byteOrder = null; /** - * Constructor + * Streaming mode fields + */ + private final boolean isStreaming; + private final RandomAccessFile randomAccessFile; + private final FileChannel channel; + + /** + * Constructor for memory mode */ public ByteWriter() { this(ByteOrder.nativeOrder()); } /** - * Constructor - * + * Constructor for memory mode + * * @param byteOrder * byte order */ public ByteWriter(ByteOrder byteOrder) { this.byteOrder = byteOrder; + this.isStreaming = false; + this.os = new ByteArrayOutputStream(); + this.randomAccessFile = null; + this.channel = null; + } + + /** + * Constructor for streaming mode + * + * @param file + * file to write + * @throws IOException + * upon file access error + */ + public ByteWriter(File file) throws IOException { + this(file, ByteOrder.nativeOrder()); + } + + /** + * Constructor for streaming mode + * + * @param file + * file to write + * @param byteOrder + * byte order + * @throws IOException + * upon file access error + */ + public ByteWriter(File file, ByteOrder byteOrder) throws IOException { + this.byteOrder = byteOrder; + this.isStreaming = true; + this.os = null; + this.randomAccessFile = new RandomAccessFile(file, "rw"); + this.channel = randomAccessFile.getChannel(); } /** * Close the byte writer */ + @Override public void close() { try { - os.close(); + if (isStreaming) { + if (randomAccessFile != null) { + randomAccessFile.close(); + } + } else { + if (os != null) { + os.close(); + } + } } catch (IOException e) { } } @@ -55,6 +110,9 @@ public void close() { * @return byte array output stream */ public ByteArrayOutputStream getOutputStream() { + if (isStreaming) { + throw new UnsupportedOperationException("OutputStream not available in streaming mode"); + } return os; } @@ -82,7 +140,10 @@ public void setByteOrder(ByteOrder byteOrder) { * * @return written bytes */ - public byte[] getBytes() { + public byte[] getBytes() throws IOException { + if (isStreaming) { + throw new UnsupportedOperationException("getBytes() not supported in streaming mode"); + } return os.toByteArray(); } @@ -91,8 +152,35 @@ public byte[] getBytes() { * * @return bytes written */ - public int size() { - return os.size(); + public long size() throws IOException { + if (isStreaming) { + return channel.position(); + } else { + return os.size(); + } + } + + /** + * Internal method to write bytes to file or memory + * + * @param value + * bytes to write + * @throws IOException + * upon failure to write + */ + private void writeBytesInternal(byte[] value) { + try { + if (isStreaming) { + ByteBuffer buffer = ByteBuffer.wrap(value); + while (buffer.hasRemaining()) { + channel.write(buffer); + } + } else { + os.write(value); + } + } catch (IOException e) { + throw new TiffException("Failed to write bytes", e); + } } /** @@ -104,9 +192,9 @@ public int size() { * @throws IOException * upon failure to write */ - public int writeString(String value) throws IOException { + public int writeString(String value) { byte[] valueBytes = value.getBytes(); - os.write(valueBytes); + writeBytesInternal(valueBytes); return valueBytes.length; } @@ -117,7 +205,7 @@ public int writeString(String value) throws IOException { * byte */ public void writeByte(byte value) { - os.write(value); + writeBytesInternal(new byte[] { value }); } /** @@ -127,7 +215,7 @@ public void writeByte(byte value) { * unsigned byte as a short */ public void writeUnsignedByte(short value) { - os.write((byte) (value & 0xff)); + writeBytesInternal(new byte[] { (byte) (value & 0xff) }); } /** @@ -139,7 +227,7 @@ public void writeUnsignedByte(short value) { * upon failure to write */ public void writeBytes(byte[] value) throws IOException { - os.write(value); + writeBytesInternal(value); } /** @@ -156,7 +244,7 @@ public void writeShort(short value) throws IOException { .putShort(value); byteBuffer.flip(); byteBuffer.get(valueBytes); - os.write(valueBytes); + writeBytesInternal(valueBytes); } /** @@ -173,7 +261,7 @@ public void writeUnsignedShort(int value) throws IOException { .putShort((short) (value & 0xffff)); byteBuffer.flip(); byteBuffer.get(valueBytes); - os.write(valueBytes); + writeBytesInternal(valueBytes); } /** @@ -190,7 +278,7 @@ public void writeInt(int value) throws IOException { .putInt(value); byteBuffer.flip(); byteBuffer.get(valueBytes); - os.write(valueBytes); + writeBytesInternal(valueBytes); } /** @@ -207,7 +295,7 @@ public void writeUnsignedInt(long value) throws IOException { .putInt((int) (value & 0xffffffffL)); byteBuffer.flip(); byteBuffer.get(valueBytes); - os.write(valueBytes); + writeBytesInternal(valueBytes); } /** @@ -224,7 +312,7 @@ public void writeFloat(float value) throws IOException { .putFloat(value); byteBuffer.flip(); byteBuffer.get(valueBytes); - os.write(valueBytes); + writeBytesInternal(valueBytes); } /** @@ -241,7 +329,7 @@ public void writeDouble(double value) throws IOException { .putDouble(value); byteBuffer.flip(); byteBuffer.get(valueBytes); - os.write(valueBytes); + writeBytesInternal(valueBytes); } } diff --git a/src/test/java/mil/nga/tiff/StreamingTest.java b/src/test/java/mil/nga/tiff/StreamingTest.java new file mode 100644 index 0000000..15c9860 --- /dev/null +++ b/src/test/java/mil/nga/tiff/StreamingTest.java @@ -0,0 +1,251 @@ +package mil.nga.tiff; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.StandardCopyOption; + +import mil.nga.tiff.io.ByteWriter; +import mil.nga.tiff.util.TiffConstants; +import org.junit.Test; + +/** + * Test the streaming TIFF implementation + * + * @author osbornb + */ +public class StreamingTest { + + /** + * Test streaming vs memory-based TIFF reading + * + * @throws IOException + * upon error + */ + @Test + public void testStreamingReadTiff() throws IOException { + + // Use the test TIFF files from the existing test resources + InputStream tiffInputStream = getClass().getResourceAsStream("/rgb.tiff"); + if (tiffInputStream == null) { + // Skip test if resource not available + return; + } + + // Create temporary file from resource + File tempTiffFile = File.createTempFile("test_streaming", ".tif"); + tempTiffFile.deleteOnExit(); + + try { + // Copy resource to temp file + java.nio.file.Files.copy(tiffInputStream, tempTiffFile.toPath(), + StandardCopyOption.REPLACE_EXISTING); + + // Test memory-based reading (default for small files) + TIFFImage memoryImage = TiffReader.readTiff(tempTiffFile); + assert memoryImage != null : "Memory-based reading failed"; + + // Test explicit streaming reading + TIFFImage streamingImage = TiffReader.readTiffStreaming(tempTiffFile, false); + assert streamingImage != null : "Streaming reading failed"; + + // Compare basic metadata + FileDirectory memoryDir = (FileDirectory) memoryImage.getFileDirectory(); + FileDirectory streamingDir = (FileDirectory) streamingImage.getFileDirectory(); + + // Compare basic image properties + assert memoryDir.getImageWidth().equals(streamingDir.getImageWidth()) : + "Image width mismatch"; + assert memoryDir.getImageHeight().equals(streamingDir.getImageHeight()) : + "Image height mismatch"; + assert memoryDir.getSamplesPerPixel() == streamingDir.getSamplesPerPixel() : + "Samples per pixel mismatch"; + + System.out.println("✓ Streaming and memory-based reading produced consistent metadata"); + + } finally { + tiffInputStream.close(); + } + } + + /** + * Test auto-detection functionality + */ + @Test + public void testAutoDetection() { + // This test demonstrates that the readTiff method automatically chooses + // streaming for large files (>100MB) and memory for smaller files + + // For files smaller than 100MB, it uses memory-based reading + // For files larger than 100MB, it automatically switches to streaming + + System.out.println("Auto-detection: Files >100MB automatically use streaming mode"); + System.out.println("Auto-detection: Files <=100MB use memory mode for compatibility"); + } + + /** + * Test streaming with larger file + */ + @Test + public void testStreamingWithLargerFile() throws IOException { + // Use the largest test file available (float64.tiff - 28MB) + InputStream tiffInputStream = getClass().getResourceAsStream("/float64.tiff"); + if (tiffInputStream == null) { + // Skip test if resource not available + return; + } + + // Create temporary file from resource + File tempTiffFile = File.createTempFile("test_large_streaming", ".tif"); + tempTiffFile.deleteOnExit(); + + try { + // Copy resource to temp file + java.nio.file.Files.copy(tiffInputStream, tempTiffFile.toPath(), + StandardCopyOption.REPLACE_EXISTING); + + System.out.println("Testing with larger file: " + tempTiffFile.length() + " bytes"); + + // Test memory-based reading (should still use memory for <100MB) + long startTime = System.currentTimeMillis(); + TIFFImage memoryImage = TiffReader.readTiff(tempTiffFile); + long memoryTime = System.currentTimeMillis() - startTime; + + assert memoryImage != null : "Memory-based reading failed"; + + // Test explicit streaming reading + startTime = System.currentTimeMillis(); + TIFFImage streamingImage = TiffReader.readTiffStreaming(tempTiffFile, false); + long streamingTime = System.currentTimeMillis() - startTime; + + assert streamingImage != null : "Streaming reading failed"; + + // Compare basic metadata + FileDirectory memoryDir = (FileDirectory) memoryImage.getFileDirectory(); + FileDirectory streamingDir = (FileDirectory) streamingImage.getFileDirectory(); + + // Compare basic image properties + assert memoryDir.getImageWidth().equals(streamingDir.getImageWidth()) : + "Image width mismatch"; + assert streamingDir.getImageHeight().equals(memoryDir.getImageHeight()) : + "Image height mismatch"; + assert memoryDir.getSamplesPerPixel() == streamingDir.getSamplesPerPixel() : + "Samples per pixel mismatch"; + + System.out.println("✓ Large file streaming test completed successfully"); + System.out.println(" Memory reading time: " + memoryTime + "ms"); + System.out.println(" Streaming reading time: " + streamingTime + "ms"); + + } finally { + tiffInputStream.close(); + } + } + + /** + * Test streaming write functionality + */ + @Test + public void testStreamingWrite() throws IOException { + // Create a small test image + int width = 10; + int height = 10; + Rasters rasters = new Rasters(width, height, 1, + FieldType.getFieldType(TiffConstants.SAMPLE_FORMAT_UNSIGNED_INT, 8)); + + // Fill with test pattern + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int value = (x + y) % 256; + rasters.setFirstPixelSample(x, y, value); + } + } + + // Create file directory with proper configuration + FileDirectory fileDirectory = new FileDirectory(); + fileDirectory.setImageWidth(width); + fileDirectory.setImageHeight(height); + fileDirectory.setBitsPerSample(8); + fileDirectory.setCompression(TiffConstants.COMPRESSION_NO); + fileDirectory.setPhotometricInterpretation( + TiffConstants.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO); + fileDirectory.setSamplesPerPixel(1); + fileDirectory.setRowsPerStrip(rasters.calculateRowsPerStrip( + TiffConstants.PLANAR_CONFIGURATION_CHUNKY)); + fileDirectory.setPlanarConfiguration( + TiffConstants.PLANAR_CONFIGURATION_CHUNKY); + fileDirectory.setSampleFormat(TiffConstants.SAMPLE_FORMAT_UNSIGNED_INT); + fileDirectory.setWriteRasters(rasters); + + TIFFImage tiffImage = new TIFFImage(); + tiffImage.add(fileDirectory); + + // Test streaming write + File tempFile = File.createTempFile("streaming_write_test", ".tif"); + tempFile.deleteOnExit(); + + try { + // Create memory-based file for comparison + File memoryFile = File.createTempFile("memory_write_test", ".tif"); + memoryFile.deleteOnExit(); + + // Write using streaming mode + TiffWriter.writeTiffStreaming(tempFile, tiffImage); + + // Write using memory mode (force small size estimation) + ByteWriter memoryWriter = new ByteWriter(); + TiffWriter.writeTiff(memoryFile, memoryWriter, tiffImage); + memoryWriter.close(); + + // Verify both files were created and have content + assert tempFile.exists() : "Streaming output file was not created"; + assert tempFile.length() > 0 : "Streaming output file is empty"; + assert memoryFile.exists() : "Memory output file was not created"; + assert memoryFile.length() > 0 : "Memory output file is empty"; + + // Read back both files + TIFFImage streamingImage = TiffReader.readTiff(tempFile); + TIFFImage memoryImage = TiffReader.readTiff(memoryFile); + + assert streamingImage != null : "Could not read back streaming file"; + assert memoryImage != null : "Could not read back memory file"; + + // Compare metadata + FileDirectory streamingDir = (FileDirectory) streamingImage.getFileDirectory(); + FileDirectory memoryDir = (FileDirectory) memoryImage.getFileDirectory(); + + assert streamingDir.getImageWidth().equals(memoryDir.getImageWidth()) : + "Image width mismatch between streaming and memory"; + assert streamingDir.getImageHeight().equals(memoryDir.getImageHeight()) : + "Image height mismatch between streaming and memory"; + assert streamingDir.getSamplesPerPixel() == memoryDir.getSamplesPerPixel() : + "Samples per pixel mismatch between streaming and memory"; + + // Compare pixel data + Rasters streamingRasters = streamingDir.readRasters(); + Rasters memoryRasters = memoryDir.readRasters(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + Number streamingValue = streamingRasters.getFirstPixelSample(x, y); + Number memoryValue = memoryRasters.getFirstPixelSample(x, y); + assert streamingValue.equals(memoryValue) : + String.format("Pixel value mismatch at (%d,%d): streaming=%s, memory=%s", + x, y, streamingValue, memoryValue); + } + } + + System.out.println("✓ Streaming write test completed successfully"); + System.out.println("✓ Streaming and memory modes produced identical results"); + + // Clean up memory file + if (memoryFile.exists()) { + memoryFile.delete(); + } + + } finally { + if (tempFile.exists()) { + tempFile.delete(); + } + } + } +} \ No newline at end of file