commonMain.ag.granular.tiff.FileDirectory.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of tiffany-jvm Show documentation
Show all versions of tiffany-jvm Show documentation
A library for parsing Tagged Image File Format (Tiff) files
The newest version!
package ag.granular.tiff
import ag.granular.io.ByteBuffer
import ag.granular.tiff.compression.CompressionDecoder
import ag.granular.tiff.compression.DeflateCompression
import ag.granular.tiff.compression.LZWCompression
import ag.granular.tiff.compression.PackbitsCompression
import ag.granular.tiff.compression.RawCompression
import ag.granular.tiff.io.ByteReader
import ag.granular.tiff.util.TiffConstants
import ag.granular.tiff.util.TiffConstants.PLANAR_CONFIGURATION_CHUNKY
import ag.granular.tiff.util.TiffConstants.PLANAR_CONFIGURATION_PLANAR
import ag.granular.tiff.util.TiffException
import kotlin.math.max
import kotlin.math.min
/**
* File Directory, represents all directory entries and can be used to read the
* image raster
*/
class FileDirectory(
/**
* File directory entries in sorted tag id order
*/
private val entries: LinkedHashSet,
private val reader: ByteReader,
cacheData: Boolean
) {
/**
* Mapping between tags and entries
*/
private val fieldTagTypeMapping = HashMap()
/**
* Is this a tiled image
*/
val isTiled: Boolean
/**
* Planar configuration
*/
private val planarConfiguration: Int
/**
* Get the compression decoder
*
* @return compression decoder
*/
var decoder: CompressionDecoder? = null
private set
private var cache: MutableMap? = null
/**
* Rasters to write to the TIFF file
*/
var writeRasters: Rasters? = null
/**
* Last block index, index of single block cache
*/
private var lastBlockIndex = -1
/**
* Last block, single block cache when caching is not enabled
*/
private var lastBlock: ByteArray? = null
/**
* Get the image width
*
* @return image width
*/
val imageWidth: Number
get() = getNumberEntryValue(FieldTagType.ImageWidth)!!
/**
* Get the image height
*
* @return image height
*/
val imageHeight: Number
get() = getNumberEntryValue(FieldTagType.ImageLength)!!
var bitsPerSample: List?
get() = getIntegerListEntryValue(FieldTagType.BitsPerSample)
set(bitsPerSample) = setUnsignedIntegerListEntryValue(
FieldTagType.BitsPerSample,
bitsPerSample!!
)
/**
* Get the max bits per sample
*/
val maxBitsPerSample: Int?
get() = getMaxIntegerEntryValue(FieldTagType.BitsPerSample)
/**
* Get the compression
*/
val compression: Int?
get() = getIntegerEntryValue(FieldTagType.Compression)
/**
* Get the photometric interpretation
*/
val photometricInterpretation: Int?
get() = getIntegerEntryValue(FieldTagType.PhotometricInterpretation)
/**
* Get the strip offsets
*/
val stripOffsets: List?
get() = getNumberListEntryValue(FieldTagType.StripOffsets)
// if SamplesPerPixel tag is missing, use default value defined by
// TIFF standard
var samplesPerPixel: Int
get() {
var samplesPerPixel = getIntegerEntryValue(FieldTagType.SamplesPerPixel)
if (samplesPerPixel == null) {
samplesPerPixel = 1
}
return samplesPerPixel
}
set(samplesPerPixel) = setUnsignedIntegerEntryValue(
FieldTagType.SamplesPerPixel,
samplesPerPixel
)
val rowsPerStrip: Number?
get() = try {
getNumberEntryValue(FieldTagType.RowsPerStrip)
} catch (err: TiffException) {
null
}
val stripByteCounts: List?
get() = getNumberListEntryValue(FieldTagType.StripByteCounts)
var xResolution: List?
get() = getLongListEntryValue(FieldTagType.XResolution)
set(xResolution) = setUnsignedLongListEntryValue(FieldTagType.XResolution, xResolution!!)
/**
* Get the y resolution
*
* @return y resolution
*/
/**
* Set the y resolution
*
* @param yResolution
* y resolution
*/
var yResolution: List?
get() = getLongListEntryValue(FieldTagType.YResolution)
set(yResolution) = setUnsignedLongListEntryValue(FieldTagType.YResolution, yResolution!!)
/**
* Get the resolution unit
*
* @return resolution unit
*/
val resolutionUnit: Int?
get() = getIntegerEntryValue(FieldTagType.ResolutionUnit)
/**
* Get the color map
*
* @return color map
*/
/**
* Set the color map
*
* @param colorMap
* color map
*/
var colorMap: List?
get() = getIntegerListEntryValue(FieldTagType.ColorMap)
set(colorMap) = setUnsignedIntegerListEntryValue(FieldTagType.ColorMap, colorMap!!)
/**
* Get the tile width
*
* @return tile width
*/
val tileWidth: Number?
get() = if (isTiled)
getNumberEntryValue(FieldTagType.TileWidth)
else
imageWidth
/**
* Get the tile height
*
* @return tile height
*/
val tileHeight: Number?
get() = if (isTiled)
getNumberEntryValue(FieldTagType.TileLength)
else
rowsPerStrip
/**
* Get the tile offsets
*
* @return tile offsets
*/
/**
* Set the tile offsets
*
* @param tileOffsets
* tile offsets
*/
var tileOffsets: List?
get() = getLongListEntryValue(FieldTagType.TileOffsets)
set(tileOffsets) = setUnsignedLongListEntryValue(FieldTagType.TileOffsets, tileOffsets!!)
/**
* Get the tile byte counts
*
* @return tile byte counts
*/
val tileByteCounts: List?
get() = getNumberListEntryValue(FieldTagType.TileByteCounts)
/**
* Get the sample format
*
* @return sample format
*/
/**
* Set the sample format
*
* @param sampleFormat
* sample format
*/
var sampleFormat: List?
get() = getIntegerListEntryValue(FieldTagType.SampleFormat)
set(sampleFormat) = setUnsignedIntegerListEntryValue(
FieldTagType.SampleFormat,
sampleFormat!!
)
/**
* Get the max sample format
*
* @return max sample format
*/
val maxSampleFormat: Int?
get() = getMaxIntegerEntryValue(FieldTagType.SampleFormat)
/**
* Calculates the number of bytes for each pixel across all samples. Only
* full bytes are supported, an exception is thrown when this is not the
* case.
*
* @return the bytes per pixel
*/
private val bytesPerPixel: Int
get() {
var bitsPerSample = 0
val bitsPerSamples = this.bitsPerSample!!
for (i in bitsPerSamples.indices) {
val bits = bitsPerSamples[i]
if (bits % 8 != 0) {
throw TiffException(
"Sample bit-width of " + bits +
" is not supported"
)
} else if (bits != bitsPerSamples[0]) {
throw TiffException(
"Differing size of samples in a pixel are not supported. sample 0 = " +
bitsPerSamples[0] + ", sample " + i +
" = " + bits
)
}
bitsPerSample += bits
}
return bitsPerSample / 8
}
init {
for (entry in entries) {
fieldTagTypeMapping[entry.fieldTag] = entry
}
// Set the cache
setCache(cacheData)
// Determine if tiled
isTiled = rowsPerStrip == null
// Determine and validate the planar configuration
val pc = getPlanarConfiguration()
planarConfiguration = pc ?: PLANAR_CONFIGURATION_CHUNKY
if (planarConfiguration != PLANAR_CONFIGURATION_CHUNKY && planarConfiguration != PLANAR_CONFIGURATION_PLANAR) {
throw TiffException("Invalid planar configuration: $planarConfiguration")
}
// Determine the decoder based upon the compression
var compression = compression
if (compression == null) {
compression = TiffConstants.COMPRESSION_NO
}
when (compression) {
TiffConstants.COMPRESSION_NO -> decoder = RawCompression()
TiffConstants.COMPRESSION_CCITT_HUFFMAN -> throw TiffException("CCITT Huffman compression not supported: $compression")
TiffConstants.COMPRESSION_T4 -> throw TiffException("T4-encoding compression not supported: $compression")
TiffConstants.COMPRESSION_T6 -> throw TiffException("T6-encoding compression not supported: $compression")
TiffConstants.COMPRESSION_LZW -> decoder = LZWCompression()
TiffConstants.COMPRESSION_JPEG_OLD, TiffConstants.COMPRESSION_JPEG_NEW -> throw TiffException(
"JPEG compression not supported: $compression"
)
TiffConstants.COMPRESSION_DEFLATE, TiffConstants.COMPRESSION_PKZIP_DEFLATE -> decoder =
DeflateCompression()
TiffConstants.COMPRESSION_PACKBITS -> decoder =
PackbitsCompression()
else -> throw TiffException("Unknown compression method identifier: $compression")
}
} // Set the entries and the field tag type mapping
// /**
// * Constructor, for writing TIFF files
// */
// public FileDirectory() {
// this(null);
// }
// /**
// * Constructor, for writing TIFF files
// *
// * @param rasters
// * image rasters to write
// */
// public FileDirectory(Rasters rasters) {
// this(new TreeSet(), rasters);
// }
// /**
// * Constructor, for writing TIFF files
// *
// * @param entries
// * file directory entries
// * @param rasters
// * image rasters to write
// */
// public FileDirectory(SortedSet entries, Rasters rasters) {
// this.entries = entries;
// for (FileDirectoryEntry entry : entries) {
// fieldTagTypeMapping.put(entry.getFieldTag(), entry);
// }
// this.writeRasters = rasters;
// }
/**
* Add an entry
*
* @param entry
* file directory entry
*/
fun addEntry(entry: FileDirectoryEntry) {
entries.remove(entry)
entries.add(entry)
fieldTagTypeMapping[entry.fieldTag] = entry
}
/**
* Set whether to cache tiles. Does nothing is already caching tiles, clears
* the existing cache if set to false.
*
* @param cacheData
* true to cache tiles and strips
*/
fun setCache(cacheData: Boolean) {
if (cacheData) {
if (cache == null) {
cache = HashMap()
}
} else {
cache = null
}
}
/**
* Get the number of entries
*
* @return entry count
*/
fun numEntries(): Int {
return entries.size
}
/**
* Get a file directory entry from the field tag type
*
* @param fieldTagType
* field tag type
* @return file directory entry
*/
operator fun get(fieldTagType: FieldTagType): FileDirectoryEntry {
return fieldTagTypeMapping[fieldTagType]!!
}
/**
* Get the file directory entries
*
* @return file directory entries
*/
fun getEntries(): Set {
return entries.toSet()
}
/**
* Get the field tag type to file directory entry mapping
*
* @return field tag type mapping
*/
fun getFieldTagTypeMapping(): Map {
return fieldTagTypeMapping.toMap()
}
/**
* Set the image width
*
* @param width
* image width
*/
fun setImageWidth(width: Int) {
setUnsignedIntegerEntryValue(FieldTagType.ImageWidth, width)
}
/**
* Set the image width
*
* @param width
* image width
*/
fun setImageWidthAsLong(width: Long) {
setUnsignedLongEntryValue(FieldTagType.ImageWidth, width)
}
/**
* Set the image height
*
* @param height
* image height
*/
fun setImageHeight(height: Int) {
setUnsignedIntegerEntryValue(FieldTagType.ImageLength, height)
}
/**
* Set the image height
*
* @param height
* image height
*/
fun setImageHeightAsLong(height: Long) {
setUnsignedLongEntryValue(FieldTagType.ImageLength, height)
}
/**
* Set a single value bits per sample
*
* @param bitsPerSample
* bits per sample
*/
fun setBitsPerSample(bitsPerSample: Int) {
this.bitsPerSample = createSingleIntegerList(bitsPerSample)
}
/**
* Set the compression
*
* @param compression
* compression
*/
fun setCompression(compression: Int) {
setUnsignedIntegerEntryValue(FieldTagType.Compression, compression)
}
/**
* Set the photometric interpretation
*
* @param photometricInterpretation
* photometric interpretation
*/
fun setPhotometricInterpretation(photometricInterpretation: Int) {
setUnsignedIntegerEntryValue(
FieldTagType.PhotometricInterpretation,
photometricInterpretation
)
}
/**
* Set the strip offsets
*
* @param stripOffsets
* strip offsets
*/
fun setStripOffsets(stripOffsets: List) {
setUnsignedIntegerListEntryValue(
FieldTagType.StripOffsets,
stripOffsets
)
}
/**
* Set the strip offsets
*
* @param stripOffsets
* strip offsets
*/
fun setStripOffsetsAsLongs(stripOffsets: List) {
setUnsignedLongListEntryValue(FieldTagType.StripOffsets, stripOffsets)
}
/**
* Set a single value strip offset
*
* @param stripOffset
* strip offset
*/
fun setStripOffsets(stripOffset: Int) {
setStripOffsets(createSingleIntegerList(stripOffset))
}
/**
* Set a single value strip offset
*
* @param stripOffset
* strip offset
*/
fun setStripOffsets(stripOffset: Long) {
setStripOffsetsAsLongs(createSingleLongList(stripOffset))
}
/**
* Set the rows per strip
*
* @param rowsPerStrip
* rows per strip
*/
fun setRowsPerStrip(rowsPerStrip: Int) {
setUnsignedIntegerEntryValue(FieldTagType.RowsPerStrip, rowsPerStrip)
}
/**
* Set the rows per strip
*
* @param rowsPerStrip
* rows per strip
*/
fun setRowsPerStripAsLong(rowsPerStrip: Long) {
setUnsignedLongEntryValue(FieldTagType.RowsPerStrip, rowsPerStrip)
}
/**
* Set the strip byte counts
*
* @param stripByteCounts
* strip byte counts
*/
fun setStripByteCounts(stripByteCounts: List) {
setUnsignedIntegerListEntryValue(
FieldTagType.StripByteCounts,
stripByteCounts
)
}
/**
* Set the strip byte counts
*
* @param stripByteCounts
* strip byte counts
*/
fun setStripByteCountsAsLongs(stripByteCounts: List) {
setUnsignedLongListEntryValue(
FieldTagType.StripByteCounts,
stripByteCounts
)
}
/**
* Set a single value strip byte count
*
* @param stripByteCount
* strip byte count
*/
fun setStripByteCounts(stripByteCount: Int) {
setStripByteCounts(createSingleIntegerList(stripByteCount))
}
/**
* Set a single value strip byte count
*
* @param stripByteCount
* strip byte count
*/
fun setStripByteCounts(stripByteCount: Long) {
setStripByteCountsAsLongs(createSingleLongList(stripByteCount))
}
/**
* Set a single value x resolution
*
* @param xResolution
* x resolution
*/
fun setXResolution(xResolution: Long) {
this.xResolution = createSingleLongList(xResolution)
}
/**
* Set a single value y resolution
*
* @param yResolution
* y resolution
*/
fun setYResolution(yResolution: Long) {
this.yResolution = createSingleLongList(yResolution)
}
/**
* Get the planar configuration
*
* @return planar configuration
*/
fun getPlanarConfiguration(): Int? {
return getIntegerEntryValue(FieldTagType.PlanarConfiguration)
}
/**
* Set the planar configuration
*
* @param planarConfiguration
* planar configuration
*/
fun setPlanarConfiguration(planarConfiguration: Int) {
setUnsignedIntegerEntryValue(
FieldTagType.PlanarConfiguration,
planarConfiguration
)
}
/**
* Set the resolution unit
*
* @param resolutionUnit
* resolution unit
*/
fun setResolutionUnit(resolutionUnit: Int) {
setUnsignedIntegerEntryValue(
FieldTagType.ResolutionUnit,
resolutionUnit
)
}
/**
* Set a single value color map
*
* @param colorMap
* color map
*/
fun setColorMap(colorMap: Int) {
this.colorMap = createSingleIntegerList(colorMap)
}
/**
* Set the tile width
*
* @param tileWidth
* tile width
*/
fun setTileWidth(tileWidth: Int) {
setUnsignedIntegerEntryValue(FieldTagType.TileWidth, tileWidth)
}
/**
* Set the tile width
*
* @param tileWidth
* tile width
*/
fun setTileWidthAsLong(tileWidth: Long) {
setUnsignedLongEntryValue(FieldTagType.TileWidth, tileWidth)
}
/**
* Set the tile height
*
* @param tileHeight
* tile height
*/
fun setTileHeight(tileHeight: Int) {
setUnsignedIntegerEntryValue(FieldTagType.TileLength, tileHeight)
}
/**
* Set the tile height
*
* @param tileHeight
* tile height
*/
fun setTileHeightAsLong(tileHeight: Long) {
setUnsignedLongEntryValue(FieldTagType.TileLength, tileHeight)
}
/**
* Set a single value tile offset
*
* @param tileOffset
* tile offset
*/
fun setTileOffsets(tileOffset: Long) {
tileOffsets = createSingleLongList(tileOffset)
}
/**
* Set the tile byte counts
*
* @param tileByteCounts
* tile byte counts
*/
fun setTileByteCounts(tileByteCounts: List) {
setUnsignedIntegerListEntryValue(
FieldTagType.TileByteCounts,
tileByteCounts
)
}
/**
* Set the tile byte counts
*
* @param tileByteCounts
* tile byte counts
*/
fun setTileByteCountsAsLongs(tileByteCounts: List) {
setUnsignedLongListEntryValue(
FieldTagType.TileByteCounts,
tileByteCounts
)
}
/**
* Set a single value tile byte count
*
* @param tileByteCount
* tile byte count
*/
fun setTileByteCounts(tileByteCount: Int) {
setTileByteCounts(createSingleIntegerList(tileByteCount))
}
/**
* Set a single value tile byte count
*
* @param tileByteCount
* tile byte count
*/
fun setTileByteCounts(tileByteCount: Long) {
setTileByteCountsAsLongs(createSingleLongList(tileByteCount))
}
/**
* Set a single value sample format
*
* @param sampleFormat
* sample format
*/
fun setSampleFormat(sampleFormat: Int) {
this.sampleFormat = createSingleIntegerList(sampleFormat)
}
/**
* Read the rasters
*
* @return rasters
*/
fun readRasters(): Rasters = readRasters(window)
/**
* Read the rasters as interleaved
*
* @return rasters
*/
fun readInterleavedRasters(): Rasters = readInterleavedRasters(window)
/**
* Read the rasters
*
* @param samples
* pixel samples to read
* @return rasters
*/
fun readRasters(samples: IntArray): Rasters = readRasters(window, samples)
/**
* Read the rasters as interleaved
*
* @param samples
* pixel samples to read
* @return rasters
*/
fun readInterleavedRasters(samples: IntArray): Rasters = readInterleavedRasters(window, samples)
/**
* Read the rasters as interleaved
*
* @param window
* image window
* @param samples
* pixel samples to read
* @return rasters
*/
fun readInterleavedRasters(window: ImageWindow, samples: IntArray? = null): Rasters {
return readRasters(window, samples, false, true)
}
/**
* Read the rasters
*
* @param sampleValues
* true to read results per sample
* @param interleaveValues
* true to read results as interleaved
* @return rasters
*/
fun readRasters(sampleValues: Boolean, interleaveValues: Boolean): Rasters =
readRasters(window, sampleValues, interleaveValues)
/**
* Read the rasters
*
* @param window
* image window
* @param sampleValues
* true to read results per sample
* @param interleaveValues
* true to read results as interleaved
* @return rasters
*/
fun readRasters(
window: ImageWindow,
sampleValues: Boolean,
interleaveValues: Boolean
): Rasters {
return readRasters(window, null, sampleValues, interleaveValues)
}
/**
* Read the rasters
*
* @param samples
* pixel samples to read
* @param sampleValues
* true to read results per sample
* @param interleaveValues
* true to read results as interleaved
* @return rasters
*/
fun readRasters(
samples: IntArray,
sampleValues: Boolean,
interleaveValues: Boolean
): Rasters = readRasters(window, samples, sampleValues, interleaveValues)
/**
* Read the rasters
*
* @param window
* image window
* @param samples
* pixel samples to read
* @param sampleValues
* true to read results per sample
* @param interleaveValues
* true to read results as interleaved
* @return rasters
*/
fun readRasters(
window: ImageWindow,
samples: IntArray? = null,
sampleValues: Boolean = true,
interleaveValues: Boolean = false
): Rasters {
var samples = samples
val width = imageWidth!!.toInt()
val height = imageHeight!!.toInt()
// Validate the image window
if (window.minX < 0 || window.minY < 0 ||
window.maxX > width || window.maxY > height
) {
throw TiffException(
"Window is out of the image bounds. Width: " + width +
", Height: " + height + ", Window: " + window
)
} else if (window.minX > window.maxX || window.minY > window.maxY) {
throw TiffException("Invalid window range: $window")
}
val windowWidth = window.maxX - window.minX
val windowHeight = window.maxY - window.minY
val numPixels = windowWidth * windowHeight
// Set or validate the samples
val samplesPerPixel = samplesPerPixel
if (samples == null) {
samples = IntArray(samplesPerPixel)
for (i in samples.indices) {
samples[i] = i
}
} else {
for (i in samples.indices) {
if (samples[i] >= samplesPerPixel) {
throw TiffException("Invalid sample index: " + samples[i])
}
}
}
// Create the interleaved result buffer
val bitsPerSample = bitsPerSample
var bytesPerPixel = 0
for (i in 0 until samplesPerPixel) {
bytesPerPixel += bitsPerSample!![i] / 8
}
var interleave: ByteBuffer? = null
if (interleaveValues) {
interleave = ByteBuffer.allocate(numPixels * bytesPerPixel).apply {
order(reader.byteReaderOrder)
}
}
// Create the sample indexed result buffer array
var sample: Array? = null
if (sampleValues) {
sample = Array(samplesPerPixel) { i ->
ByteBuffer.allocate(numPixels * bitsPerSample!![i] / 8).apply {
order(reader.byteReaderOrder)
}
}
}
val fieldTypes = Array(samples.size) { i ->
getFieldTypeForSample(samples[i])
}
// Create the rasters results
val rasters = Rasters(
windowWidth, windowHeight, fieldTypes,
sample, interleave
)
// Read the rasters
readRaster(window, samples, rasters)
return rasters
}
private fun readRaster(window: ImageWindow, samples: IntArray, rasters: Rasters) {
val tileWidth = tileWidth!!.toInt()
val tileHeight = tileHeight!!.toInt()
val minXTile = window.minX / tileWidth
val maxXTile = (window.maxX + tileWidth - 1) / tileWidth
val minYTile = window.minY / tileHeight
val maxYTile = (window.maxY + tileHeight - 1) / tileHeight
val windowWidth = window.maxX - window.minX
var bytesPerPixel = bytesPerPixel
val srcSampleOffsets = IntArray(samples.size) { i ->
var sampleOffset = 0
if (planarConfiguration == PLANAR_CONFIGURATION_CHUNKY) {
sampleOffset = sum(bitsPerSample, 0, samples[i]) / 8
}
sampleOffset
}
val sampleFieldTypes = Array(samples.size) { i ->
getFieldTypeForSample(samples[i])
}
for (yTile in minYTile until maxYTile) {
for (xTile in minXTile until maxXTile) {
val firstLine = yTile * tileHeight
val firstCol = xTile * tileWidth
val lastLine = (yTile + 1) * tileHeight
val lastCol = (xTile + 1) * tileWidth
for (sampleIndex in samples.indices) {
val sample = samples[sampleIndex]
if (planarConfiguration == PLANAR_CONFIGURATION_PLANAR) {
bytesPerPixel = getSampleByteSize(sample)
}
val block = getTileOrStrip(xTile, yTile, sample)
val blockReader = ByteReader(
block,
reader.byteReaderOrder
)
for (y in max(0, window.minY - firstLine) until min(
tileHeight,
tileHeight - (lastLine - window.maxY)
)) {
for (x in max(0, window.minX - firstCol) until min(
tileWidth,
tileWidth - (lastCol - window.maxX)
)) {
val pixelOffset = (y * tileWidth + x) * bytesPerPixel
val valueOffset = pixelOffset + srcSampleOffsets[sampleIndex]
blockReader.nextByte = valueOffset
// Read the value
val value = readValue(
blockReader,
sampleFieldTypes[sampleIndex]!!
)
if (rasters.hasInterleaveValues()) {
val windowCoordinate = (y + firstLine - window
.minY) * windowWidth + (x + firstCol - window.minX)
rasters.addToInterleave(
sampleIndex,
windowCoordinate, value
)
}
if (rasters.hasSampleValues()) {
val windowCoordinate = ((y + firstLine - window
.minY) * windowWidth +
x +
firstCol) - window.minX
rasters.addToSample(
sampleIndex,
windowCoordinate, value
)
}
}
}
}
}
}
}
fun readTypedRasters(): TypedRasters {
val window = this.window
val width = imageWidth.toInt()
val height = imageHeight.toInt()
// Validate the image window
if (window.minX < 0 || window.minY < 0 ||
window.maxX > width || window.maxY > height
) {
throw TiffException(
"Window is out of the image bounds. Width: " + width +
", Height: " + height + ", Window: " + window
)
} else if (window.minX > window.maxX || window.minY > window.maxY) {
throw TiffException("Invalid window range: $window")
}
val windowWidth = window.maxX - window.minX
val windowHeight = window.maxY - window.minY
val numPixels = windowWidth * windowHeight
// Set or validate the samples
val samplesPerPixel = samplesPerPixel
// Create the interleaved result buffer
val bitsPerSample = bitsPerSample
var bytesPerPixel = 0
for (i in 0 until samplesPerPixel) {
bytesPerPixel += bitsPerSample!![i] / 8
}
// Create the sample indexed result buffer array
var samples = List(samplesPerPixel) {
when (val fieldType = getFieldTypeForSample(it)) {
FieldType.BYTE -> TypedSample.ByteSample(fieldType, ByteArray(numPixels))
FieldType.SHORT -> TypedSample.ShortSample(fieldType, ShortArray(numPixels))
FieldType.LONG -> TypedSample.IntSample(fieldType, IntArray(numPixels))
FieldType.SBYTE -> TypedSample.ByteSample(fieldType, ByteArray(numPixels))
FieldType.SSHORT -> TypedSample.ShortSample(fieldType, ShortArray(numPixels))
FieldType.SLONG -> TypedSample.IntSample(fieldType, IntArray(numPixels))
FieldType.FLOAT -> TypedSample.FloatSample(fieldType, FloatArray(numPixels))
FieldType.DOUBLE -> TypedSample.DoubleSample(fieldType, DoubleArray(numPixels))
else -> throw TiffException("Unsupported raster field type: $fieldType")
}
}
val fieldTypes = Array(samplesPerPixel) { i ->
getFieldTypeForSample(i)
}
// Read the rasters
// readRaster(window, samples, rasters)
val tileWidth = tileWidth!!.toInt()
val tileHeight = tileHeight!!.toInt()
val minXTile = window.minX / tileWidth
val maxXTile = (window.maxX + tileWidth - 1) / tileWidth
val minYTile = window.minY / tileHeight
val maxYTile = (window.maxY + tileHeight - 1) / tileHeight
val srcSampleOffsets = IntArray(samplesPerPixel) { i ->
var sampleOffset = 0
if (planarConfiguration == PLANAR_CONFIGURATION_CHUNKY) {
sampleOffset = sum(bitsPerSample, 0, i) / 8
}
sampleOffset
}
val sampleFieldTypes = Array(samplesPerPixel) { i ->
getFieldTypeForSample(i)
}
for (yTile in minYTile until maxYTile) {
for (xTile in minXTile until maxXTile) {
val firstLine = yTile * tileHeight
val firstCol = xTile * tileWidth
val lastLine = (yTile + 1) * tileHeight
val lastCol = (xTile + 1) * tileWidth
for (sampleIndex in 0 until samplesPerPixel) {
if (planarConfiguration == PLANAR_CONFIGURATION_PLANAR) {
bytesPerPixel = getSampleByteSize(sampleIndex)
}
val block = getTileOrStrip(xTile, yTile, sampleIndex)
val blockReader = ByteReader(
block,
reader.byteReaderOrder
)
for (y in max(0, window.minY - firstLine) until min(
tileHeight,
tileHeight - (lastLine - window.maxY)
)) {
for (x in max(0, window.minX - firstCol) until min(
tileWidth,
tileWidth - (lastCol - window.maxX)
)) {
val pixelOffset = (y * tileWidth + x) * bytesPerPixel
val valueOffset = pixelOffset + srcSampleOffsets[sampleIndex]
blockReader.nextByte = valueOffset
// Read the value
val value = readValue(
blockReader,
sampleFieldTypes[sampleIndex]
)
val windowCoordinate = ((y + firstLine - window.minY) * windowWidth + x + firstCol) - window.minX
val _unused = when (val typedSample = samples[sampleIndex]) {
is TypedSample.ByteSample -> typedSample.data[windowCoordinate] = value.toByte()
is TypedSample.ShortSample -> typedSample.data[windowCoordinate] = value.toShort()
is TypedSample.IntSample -> typedSample.data[windowCoordinate] = value.toInt()
is TypedSample.FloatSample -> typedSample.data[windowCoordinate] = value.toFloat()
is TypedSample.DoubleSample -> typedSample.data[windowCoordinate] = value.toDouble()
}
// println("windowCoordinate: $windowCoordinate, value: $value")
// sample[sampleIndex][windowCoordinate] = value
// rasters.addToSample(
// sampleIndex,
// windowCoordinate, value
// )
}
}
}
}
}
return TypedRasters(
windowWidth, windowHeight,
samples
)
}
private fun readValue(reader: ByteReader, fieldType: FieldType): Number = when (fieldType) {
FieldType.BYTE -> reader.readUnsignedByte()
FieldType.SHORT -> reader.readUnsignedShort()
FieldType.LONG -> reader.readUnsignedInt()
FieldType.SBYTE -> reader.readByte()
FieldType.SSHORT -> reader.readShort()
FieldType.SLONG -> reader.readInt()
FieldType.FLOAT -> reader.readFloat()
FieldType.DOUBLE -> reader.readDouble()
else -> throw TiffException("Unsupported raster field type: $fieldType")
}
/**
* Get the field type for the sample
*
* @param sampleIndex
* sample index
* @return field type
*/
fun getFieldTypeForSample(sampleIndex: Int): FieldType {
val sampleFormatList = sampleFormat
val sampleFormat = if (sampleFormatList == null)
TiffConstants.SAMPLE_FORMAT_UNSIGNED_INT
else
sampleFormatList[if (sampleIndex < sampleFormatList.size)
sampleIndex
else
0]
val bitsPerSample = bitsPerSample!![sampleIndex]
return FieldType.getFieldType(
sampleFormat,
bitsPerSample
)
}
/**
* Get the tile or strip for the sample coordinate
*
* @param x
* x coordinate
* @param y
* y coordinate
* @param sample
* sample index
* @return bytes
*/
private fun getTileOrStrip(x: Int, y: Int, sample: Int): ByteArray {
var tileOrStrip: ByteArray? = null
val imageWidth = imageWidth!!.toInt()
val imageHeight = imageHeight!!.toInt()
val tileWidth = tileWidth!!.toInt()
val tileHeight = tileHeight!!.toInt()
val numTilesPerRow = (imageWidth + tileWidth - 1) / tileWidth
val numTilesPerCol = (imageHeight + tileHeight - 1) / tileHeight
var index = when (planarConfiguration) {
PLANAR_CONFIGURATION_CHUNKY -> y * numTilesPerRow + x
PLANAR_CONFIGURATION_PLANAR -> sample * numTilesPerRow * numTilesPerCol + y * numTilesPerRow + x
else -> 0
}
// Attempt to pull from the cache
if (cache != null && cache!!.containsKey(index)) {
tileOrStrip = cache!![index]
} else if (lastBlockIndex == index && lastBlock != null) {
tileOrStrip = lastBlock
} else {
// Read and decode the block
var offset = 0
var byteCount = 0
if (isTiled) {
offset = tileOffsets!![index].toInt()
byteCount = tileByteCounts!![index].toInt()
} else {
offset = stripOffsets!![index].toInt()
byteCount = stripByteCounts!![index].toInt()
}
reader.nextByte = offset
val bytes = reader.readBytes(byteCount)
tileOrStrip = decoder!!.decode(bytes, reader.byteReaderOrder)
// Cache the data
if (cache != null) {
cache!![index] = tileOrStrip
} else {
lastBlockIndex = index
lastBlock = tileOrStrip
}
}
return tileOrStrip!!
}
/**
* Get the sample byte size
*
* @param sampleIndex
* sample index
* @return byte size
*/
private fun getSampleByteSize(sampleIndex: Int): Int {
val bitsPerSample = bitsPerSample
if (sampleIndex >= bitsPerSample!!.size) {
throw TiffException(
"Sample index " + sampleIndex +
" is out of range"
)
}
val bits = bitsPerSample[sampleIndex]
if (bits % 8 != 0) {
throw TiffException(
"Sample bit-width of " + bits +
" is not supported"
)
}
return bits / 8
}
fun getIntegerEntryValue(fieldTagType: FieldTagType): Int = getEntryValue(fieldTagType)
/**
* Set an unsigned integer entry value for the field tag type
*
* @param fieldTagType
* field tag type
* @param value
* unsigned integer value (16 bit)
* @since 2.0.0
*/
fun setUnsignedIntegerEntryValue(
fieldTagType: FieldTagType,
value: Int
) {
setEntryValue(fieldTagType, FieldType.SHORT, 1, value)
}
/**
* Get an number entry value
*
* @param fieldTagType
* field tag type
* @return number value
* @since 2.0.0
*/
fun getNumberEntryValue(fieldTagType: FieldTagType): Number = getEntryValue(fieldTagType)
/**
* Set an unsigned long entry value for the field tag type
*
* @param fieldTagType
* field tag type
* @param value
* unsigned long value (32 bit)
* @since 2.0.0
*/
fun setUnsignedLongEntryValue(fieldTagType: FieldTagType, value: Long) {
setEntryValue(fieldTagType, FieldType.LONG, 1, value)
}
/**
* Get a string entry value for the field tag type
*
* @param fieldTagType
* field tag type
* @return string value
* @since 2.0.0
*/
fun getStringEntryValue(fieldTagType: FieldTagType): String? {
var value: String? = null
val values = getEntryValue>(fieldTagType)
if (values != null && !values.isEmpty()) {
value = values[0]
}
return value
}
/**
* Set string value for the field tag type
*
* @param fieldTagType
* field tag type
* @param value
* string value
* @since 2.0.0
*/
fun setStringEntryValue(fieldTagType: FieldTagType, value: String) {
val values = ArrayList()
values.add(value)
setEntryValue(fieldTagType, FieldType.ASCII, (value.length + 1).toLong(), values)
}
/**
* Get an integer list entry value
*
* @param fieldTagType
* field tag type
* @return integer list value
* @since 2.0.0
*/
fun getIntegerListEntryValue(fieldTagType: FieldTagType): List? {
return getEntryValue>(fieldTagType)
}
/**
* Set an unsigned integer list of values for the field tag type
*
* @param fieldTagType
* field tag type
* @param value
* integer list value
* @since 2.0.0
*/
fun setUnsignedIntegerListEntryValue(
fieldTagType: FieldTagType,
value: List
) {
setEntryValue(fieldTagType, FieldType.SHORT, value.size.toLong(), value)
}
/**
* Get the max integer from integer list entry values
*
* @param fieldTagType
* field tag type
* @return max integer value
* @since 2.0.0
*/
fun getMaxIntegerEntryValue(fieldTagType: FieldTagType): Int? {
var maxValue: Int? = null
val values = getIntegerListEntryValue(fieldTagType)
if (values != null) {
maxValue = values.maxOrNull()
}
return maxValue
}
/**
* Get a number list entry value
*
* @param fieldTagType
* field tag type
* @return long list value
* @since 2.0.0
*/
fun getNumberListEntryValue(fieldTagType: FieldTagType): List? {
return getEntryValue>(fieldTagType)
}
/**
* Get a long list entry value
*
* @param fieldTagType
* field tag type
* @return long list value
* @since 2.0.0
*/
fun getLongListEntryValue(fieldTagType: FieldTagType): List? {
return getEntryValue>(fieldTagType)
}
/**
* Set an unsigned long list of values for the field tag type
*
* @param fieldTagType
* field tag type
* @param value
* long list value
* @since 2.0.0
*/
fun setUnsignedLongListEntryValue(
fieldTagType: FieldTagType,
value: List
) {
setEntryValue(fieldTagType, FieldType.LONG, value.size.toLong(), value)
}
/**
* Get an entry value
*
* @param fieldTagType
* field tag type
* @return value
*/
private fun getEntryValue(fieldTagType: FieldTagType): T {
return (fieldTagTypeMapping[fieldTagType]?.values as T?)
?: throw TiffException("The entry value not found for $fieldTagType field tag")
}
/**
* Create and set the entry value
*
* @param fieldTagType
* field tag type
* @param fieldType
* field type
* @param typeCount
* type count
* @param values
* entry values
*/
private fun setEntryValue(
fieldTagType: FieldTagType,
fieldType: FieldType,
typeCount: Long,
values: Any
) {
val entry = FileDirectoryEntry(
fieldTagType,
fieldType, typeCount, values
)
addEntry(entry)
}
/**
* Sum the list integer values in the provided range
*
* @param values
* integer values
* @param start
* inclusive start index
* @param end
* exclusive end index
* @return sum
*/
private fun sum(values: List?, start: Int, end: Int): Int {
var sum = 0
for (i in start until end) {
sum += values!![i]
}
return sum
}
/**
* Create a single integer list with the value
*
* @param value
* int value
* @return single value list
*/
private fun createSingleIntegerList(value: Int): List {
val valueList = ArrayList()
valueList.add(value)
return valueList
}
/**
* Create a single long list with the value
*
* @param value
* long value
* @return single value list
*/
private fun createSingleLongList(value: Long): List {
val valueList = ArrayList()
valueList.add(value)
return valueList
}
/**
* Size in bytes of the Image File Directory (all contiguous)
*
* @return size in bytes
*/
fun size(): Long {
return (TiffConstants.IFD_HEADER_BYTES +
entries.size * TiffConstants.IFD_ENTRY_BYTES +
TiffConstants.IFD_OFFSET_BYTES).toLong()
}
/**
* Size in bytes of the image file directory including entry values (not
* contiguous bytes)
*
* @return size in bytes
*/
fun sizeWithValues(): Long {
var size = (TiffConstants.IFD_HEADER_BYTES + TiffConstants.IFD_OFFSET_BYTES).toLong()
for (entry in entries) {
size += entry.sizeWithValues()
}
return size
}
}
val FileDirectory.window: ImageWindow
get() = ImageWindow(
minX = 0,
minY = 0,
maxX = imageWidth.toInt(),
maxY = imageHeight.toInt()
)