Skip to content

Commit

Permalink
[api]: Implement XISF write
Browse files Browse the repository at this point in the history
  • Loading branch information
tiagohm committed Apr 1, 2024
1 parent 3fa65d8 commit fa94428
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ abstract class AbstractSeekableSink : SeekableSink {

var remaining = byteCount

while (remaining > 0L) {
timeout.throwIfReached()
source.readUnsafe(cursor).use {
while (remaining > 0L) {
timeout.throwIfReached()

source.readUnsafe(cursor).use {
it.seek(0L)

val length = computeTransferedSize(it, remaining)
Expand All @@ -62,5 +62,5 @@ abstract class AbstractSeekableSink : SeekableSink {

override fun flush() = Unit

override fun close() = cursor.close()
override fun close() = Unit
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@ abstract class AbstractSeekableSource : SeekableSource {

override fun timeout() = timeout

override fun close() = cursor.close()
override fun close() = Unit
}
8 changes: 5 additions & 3 deletions nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import nebulosa.image.format.ImageHdu
import nebulosa.io.*
import nebulosa.xisf.XisfMonolithicFileHeader.ColorSpace
import nebulosa.xisf.XisfMonolithicFileHeader.ImageType
import nebulosa.xml.escapeXml
import okio.Buffer
import okio.Sink
import okio.Timeout
Expand Down Expand Up @@ -75,7 +76,7 @@ data object XisfFormat : ImageFormat {
if (key != null) {
if (key.valueType == ValueType.STRING || key.valueType == ValueType.ANY) {
val value = header.getStringOrNull(key) ?: continue
buffer.writeUtf8(STRING_PROPERTY_TAG.format(name, value))
buffer.writeUtf8(STRING_PROPERTY_TAG.format(name, value.escapeXml()))
} else if (key.valueType == ValueType.LOGICAL) {
val value = header.getBooleanOrNull(key) ?: continue
buffer.writeUtf8(NON_STRING_PROPERTY_TAG.format(name, XisfPropertyType.BOOLEAN.typeName, if (value) 1 else 0))
Expand All @@ -90,15 +91,16 @@ data object XisfFormat : ImageFormat {
}

for (keyword in header) {
buffer.writeUtf8(FITS_KEYWORD_TAG.format(keyword.key, keyword.value, keyword.comment))
val value = if (keyword.isStringType) "'${keyword.value.escapeXml()}'" else keyword.value
buffer.writeUtf8(FITS_KEYWORD_TAG.format(keyword.key, value, keyword.comment.escapeXml()))
}
}
}

buffer.writeUtf8(IMAGE_END_TAG)
buffer.writeUtf8(XISF_END_TAG)

val headerSize = buffer.size - 8
val headerSize = buffer.size - 16

buffer.readAndWriteUnsafe().use {
it.seek(8L)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,27 +60,33 @@ class XisfHeaderInputStream(source: InputStream) : Closeable {
)
}

private fun parseKeywords(): Pair<List<HeaderCard>, Image?> {
private fun parseKeywords(): Pair<Collection<HeaderCard>, Image?> {
val name = reader.localName

val keywords = ArrayList<HeaderCard>()
val cards = ArrayList<HeaderCard>()
var thumbnail: Image? = null

fun addHeaderCard(card: HeaderCard) {
if (cards.find { it.key == card.key } == null) {
cards.add(card)
}
}

while (reader.hasNext()) {
val type = reader.next()

if (type == XMLStreamConstants.END_ELEMENT && reader.localName == name) {
break
} else if (type == XMLStreamConstants.START_ELEMENT) {
when (reader.localName) {
"FITSKeyword" -> keywords.add(parseFITSKeyword())
"FITSKeyword" -> addHeaderCard(parseFITSKeyword())
"Thumbnail" -> thumbnail = parseImage()
"Property" -> keywords.add(parseProperty() ?: continue)
"Property" -> addHeaderCard(parseProperty() ?: continue)
}
}
}

return keywords to thumbnail
return cards to thumbnail
}

private fun parseFITSKeyword(): HeaderCard {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ sealed interface XisfMonolithicFileHeader {
@JvmField val compressionFormat: CompressionFormat? = null,
@JvmField val imageType: ImageType = ImageType.LIGHT,
@JvmField val bounds: ClosedFloatingPointRange<Float> = DEFAULT_BOUNDS,
@JvmField val keywords: List<HeaderCard> = emptyList(),
@JvmField val keywords: Collection<HeaderCard> = emptyList(),
@JvmField val thumbnail: Image? = null,
) : XisfMonolithicFileHeader

Expand Down
56 changes: 45 additions & 11 deletions nebulosa-xisf/src/test/kotlin/XisfFormatTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io.kotest.engine.spec.tempfile
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.floats.shouldBeExactly
import io.kotest.matchers.ints.shouldBeExactly
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeSameInstanceAs
Expand All @@ -26,7 +27,7 @@ class XisfFormatTest : FitsStringSpec() {
width shouldBeExactly 512
height shouldBeExactly 512
numberOfChannels shouldBeExactly 1
header shouldHaveSize 29
header shouldHaveSize 17
data.red.size shouldBeExactly 512 * 512
data.green shouldBeSameInstanceAs data.red
data.blue shouldBeSameInstanceAs data.green
Expand All @@ -45,7 +46,7 @@ class XisfFormatTest : FitsStringSpec() {
width shouldBeExactly 512
height shouldBeExactly 512
numberOfChannels shouldBeExactly 1
header shouldHaveSize 29
header shouldHaveSize 17
data.red.size shouldBeExactly 512 * 512
data.green shouldBeSameInstanceAs data.red
data.blue shouldBeSameInstanceAs data.green
Expand All @@ -64,7 +65,7 @@ class XisfFormatTest : FitsStringSpec() {
width shouldBeExactly 512
height shouldBeExactly 512
numberOfChannels shouldBeExactly 1
header shouldHaveSize 29
header shouldHaveSize 17
data.red.size shouldBeExactly 512 * 512
data.green shouldBeSameInstanceAs data.red
data.blue shouldBeSameInstanceAs data.green
Expand All @@ -83,7 +84,7 @@ class XisfFormatTest : FitsStringSpec() {
width shouldBeExactly 512
height shouldBeExactly 512
numberOfChannels shouldBeExactly 1
header shouldHaveSize 29
header shouldHaveSize 17
data.red.size shouldBeExactly 512 * 512
data.green shouldBeSameInstanceAs data.red
data.blue shouldBeSameInstanceAs data.green
Expand All @@ -102,7 +103,7 @@ class XisfFormatTest : FitsStringSpec() {
width shouldBeExactly 512
height shouldBeExactly 512
numberOfChannels shouldBeExactly 1
header shouldHaveSize 29
header shouldHaveSize 17
data.red.size shouldBeExactly 512 * 512
data.green shouldBeSameInstanceAs data.red
data.blue shouldBeSameInstanceAs data.green
Expand All @@ -121,7 +122,7 @@ class XisfFormatTest : FitsStringSpec() {
width shouldBeExactly 512
height shouldBeExactly 512
numberOfChannels shouldBeExactly 3
header shouldHaveSize 29
header shouldHaveSize 17
data.red.size shouldBeExactly 512 * 512
data.green shouldNotBeSameInstanceAs data.red
data.blue shouldNotBeSameInstanceAs data.green
Expand All @@ -140,7 +141,7 @@ class XisfFormatTest : FitsStringSpec() {
width shouldBeExactly 512
height shouldBeExactly 512
numberOfChannels shouldBeExactly 3
header shouldHaveSize 29
header shouldHaveSize 17
data.red.size shouldBeExactly 512 * 512
data.green shouldNotBeSameInstanceAs data.red
data.blue shouldNotBeSameInstanceAs data.green
Expand All @@ -159,7 +160,7 @@ class XisfFormatTest : FitsStringSpec() {
width shouldBeExactly 512
height shouldBeExactly 512
numberOfChannels shouldBeExactly 3
header shouldHaveSize 29
header shouldHaveSize 17
data.red.size shouldBeExactly 512 * 512
data.green shouldNotBeSameInstanceAs data.red
data.blue shouldNotBeSameInstanceAs data.green
Expand All @@ -178,7 +179,7 @@ class XisfFormatTest : FitsStringSpec() {
width shouldBeExactly 512
height shouldBeExactly 512
numberOfChannels shouldBeExactly 3
header shouldHaveSize 29
header shouldHaveSize 17
data.red.size shouldBeExactly 512 * 512
data.green shouldNotBeSameInstanceAs data.red
data.blue shouldNotBeSameInstanceAs data.green
Expand All @@ -197,7 +198,7 @@ class XisfFormatTest : FitsStringSpec() {
width shouldBeExactly 512
height shouldBeExactly 512
numberOfChannels shouldBeExactly 3
header shouldHaveSize 29
header shouldHaveSize 17
data.red.size shouldBeExactly 512 * 512
data.green shouldNotBeSameInstanceAs data.red
data.blue shouldNotBeSameInstanceAs data.green
Expand All @@ -216,7 +217,7 @@ class XisfFormatTest : FitsStringSpec() {
width shouldBeExactly 512
height shouldBeExactly 512
numberOfChannels shouldBeExactly 1
header shouldHaveSize 29
header shouldHaveSize 17
data.red.size shouldBeExactly 512 * 512
data.green shouldBeSameInstanceAs data.red
data.blue shouldBeSameInstanceAs data.green
Expand All @@ -238,7 +239,40 @@ class XisfFormatTest : FitsStringSpec() {
val hdus1 = XisfFormat.read(source1)

hdus1 shouldHaveSize 1
hdus1[0].data.numberOfChannels shouldBeExactly 1
hdus1[0].header.size shouldBeExactly hdus0[0].header.size
hdus1[0].data.red.size shouldBeExactly hdus0[0].data.red.size
hdus1[0].data.red shouldNotBeSameInstanceAs hdus0[0].data.red
hdus1[0].data.red.forEachIndexed { i, value -> value shouldBeExactly hdus0[0].data.red[i] }

val image = hdus1[0].makeImage()
image.save("xisf-mono-write").second shouldBe "0dca7efedef5b3525f8037f401518b0b"
}
"color:write" {
val source0 = closeAfterEach(M82_COLOR_32_XISF.seekableSource())
val hdus0 = XisfFormat.read(source0)

Check warning on line 253 in nebulosa-xisf/src/test/kotlin/XisfFormatTest.kt

View check run for this annotation

codefactor.io / CodeFactor

nebulosa-xisf/src/test/kotlin/XisfFormatTest.kt#L253

Parentheses in (image.raster.dataBuffer as DataBufferByte) are unnecessary and can be replaced with: image.raster.dataBuffer as DataBufferByte (detekt.UnnecessaryParentheses)

val outputPath = tempfile()
val sink = closeAfterEach(outputPath.seekableSink())

XisfFormat.write(sink, hdus0)

Check warning on line 259 in nebulosa-xisf/src/test/kotlin/XisfFormatTest.kt

View check run for this annotation

codefactor.io / CodeFactor

nebulosa-xisf/src/test/kotlin/XisfFormatTest.kt#L259

Parentheses in (image.raster.dataBuffer as DataBufferInt) are unnecessary and can be replaced with: image.raster.dataBuffer as DataBufferInt (detekt.UnnecessaryParentheses)
val source1 = closeAfterEach(outputPath.seekableSource())
val hdus1 = XisfFormat.read(source1)

hdus1 shouldHaveSize 1
hdus1[0].data.numberOfChannels shouldBeExactly 3
hdus1[0].header.size shouldBeExactly hdus0[0].header.size
hdus1[0].data.red.size shouldBeExactly hdus0[0].data.red.size
hdus1[0].data.red shouldNotBeSameInstanceAs hdus0[0].data.red
hdus1[0].data.red.forEachIndexed { i, value -> value shouldBeExactly hdus0[0].data.red[i] }
hdus1[0].data.green shouldNotBeSameInstanceAs hdus0[0].data.green
hdus1[0].data.green.forEachIndexed { i, value -> value shouldBeExactly hdus0[0].data.green[i] }
hdus1[0].data.blue shouldNotBeSameInstanceAs hdus0[0].data.blue
hdus1[0].data.blue.forEachIndexed { i, value -> value shouldBeExactly hdus0[0].data.blue[i] }

val image = hdus1[0].makeImage()
image.save("xisf-color-write").second shouldBe "89beed384ee9e97ce033ba447a377937"
}
}

Expand Down
28 changes: 14 additions & 14 deletions nebulosa-xisf/src/test/kotlin/XisfHeaderInputStreamTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
sampleFormat shouldBe XisfMonolithicFileHeader.SampleFormat.UINT8
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.GRAY
byteOrder shouldBe ByteOrder.LITTLE
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
"read:8:gray:zlib" {
Expand All @@ -40,7 +40,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.GRAY
byteOrder shouldBe ByteOrder.LITTLE
compressionFormat.shouldNotBeNull().type shouldBe XisfMonolithicFileHeader.CompressionType.ZLIB
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
"read:8:gray:lz4" {
Expand All @@ -56,7 +56,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.GRAY
byteOrder shouldBe ByteOrder.LITTLE
compressionFormat.shouldNotBeNull().type shouldBe XisfMonolithicFileHeader.CompressionType.LZ4
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
"read:8:gray:lz4-hc" {
Expand All @@ -72,7 +72,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.GRAY
byteOrder shouldBe ByteOrder.LITTLE
compressionFormat.shouldNotBeNull().type shouldBe XisfMonolithicFileHeader.CompressionType.LZ4_HC
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
"read:8:gray:zstd" {
Expand All @@ -88,7 +88,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.GRAY
byteOrder shouldBe ByteOrder.LITTLE
compressionFormat.shouldNotBeNull().type shouldBe XisfMonolithicFileHeader.CompressionType.ZSTD
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
"read:16:gray" {
Expand All @@ -103,7 +103,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
sampleFormat shouldBe XisfMonolithicFileHeader.SampleFormat.UINT16
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.GRAY
byteOrder shouldBe ByteOrder.LITTLE
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
"read:32:gray" {
Expand All @@ -118,7 +118,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
sampleFormat shouldBe XisfMonolithicFileHeader.SampleFormat.UINT32
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.GRAY
byteOrder shouldBe ByteOrder.LITTLE
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
"read:F32:gray" {
Expand All @@ -133,7 +133,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
sampleFormat shouldBe XisfMonolithicFileHeader.SampleFormat.FLOAT32
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.GRAY
byteOrder shouldBe ByteOrder.LITTLE
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
"read:F64:gray" {
Expand All @@ -148,7 +148,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
sampleFormat shouldBe XisfMonolithicFileHeader.SampleFormat.FLOAT64
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.GRAY
byteOrder shouldBe ByteOrder.LITTLE
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
"read:8:rgb" {
Expand All @@ -163,7 +163,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
sampleFormat shouldBe XisfMonolithicFileHeader.SampleFormat.UINT8
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.RGB
byteOrder shouldBe ByteOrder.LITTLE
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
"read:16:rgb" {
Expand All @@ -178,7 +178,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
sampleFormat shouldBe XisfMonolithicFileHeader.SampleFormat.UINT16
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.RGB
byteOrder shouldBe ByteOrder.LITTLE
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
"read:32:rgb" {
Expand All @@ -193,7 +193,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
sampleFormat shouldBe XisfMonolithicFileHeader.SampleFormat.UINT32
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.RGB
byteOrder shouldBe ByteOrder.LITTLE
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
"read:F32:rgb" {
Expand All @@ -208,7 +208,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
sampleFormat shouldBe XisfMonolithicFileHeader.SampleFormat.FLOAT32
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.RGB
byteOrder shouldBe ByteOrder.LITTLE
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
"read:F64:rgb" {
Expand All @@ -223,7 +223,7 @@ class XisfHeaderInputStreamTest : FitsStringSpec() {
sampleFormat shouldBe XisfMonolithicFileHeader.SampleFormat.FLOAT64
colorSpace shouldBe XisfMonolithicFileHeader.ColorSpace.RGB
byteOrder shouldBe ByteOrder.LITTLE
keywords shouldHaveSize 29
keywords shouldHaveSize 17
}
}
}
Expand Down
Loading

0 comments on commit fa94428

Please sign in to comment.