From d886ba7f2fe9b268764af9290cf77dc06c65e3c8 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Mon, 1 Apr 2024 19:25:40 -0300 Subject: [PATCH] [api]: Implement XISF read/write --- .../nebulosa/api/mounts/MountService.kt | 2 - .../main/kotlin/nebulosa/fits/FitsFormat.kt | 9 +- nebulosa-fits/src/test/kotlin/FitsReadTest.kt | 4 +- .../src/test/kotlin/FitsWriteTest.kt | 4 +- .../test/kotlin/ComputationAlgorithmTest.kt | 4 +- .../test/kotlin/FitsTransformAlgorithmTest.kt | 12 +- nebulosa-image/src/test/kotlin/HFDTest.kt | 7 +- .../test/kotlin/XisfTransformAlgorithmTest.kt | 12 +- nebulosa-nova/src/test/kotlin/ICRFTest.kt | 5 +- .../nebulosa/test/AbstractFitsAndXisfTest.kt | 233 ++++++++++++++++++ .../kotlin/nebulosa/test/FitsStringSpec.kt | 162 ------------ .../nebulosa/test/Hips2FitsStringSpec.kt | 43 ---- .../src/test/kotlin/TAIMinusUTCTest.kt | 2 +- ...DBMinusTTByFairheadAndBretagnon1990Test.kt | 2 +- .../src/test/kotlin/WatnetPlateSolverTest.kt | 7 +- .../src/test/kotlin/WatneyStarDetectorTest.kt | 6 +- .../nebulosa/xisf/XisfHeaderInputStream.kt | 36 ++- .../nebulosa/xisf/XisfMonolithicFileHeader.kt | 2 + .../xisf/XisfMonolithicFileHeaderImageData.kt | 17 +- .../xisf/XisfMonolithicFileHeaderImageHdu.kt | 1 - .../src/test/kotlin/XisfFormatTest.kt | 71 ++---- .../test/kotlin/XisfHeaderInputStreamTest.kt | 4 +- 22 files changed, 336 insertions(+), 309 deletions(-) create mode 100644 nebulosa-test/src/main/kotlin/nebulosa/test/AbstractFitsAndXisfTest.kt delete mode 100644 nebulosa-test/src/main/kotlin/nebulosa/test/FitsStringSpec.kt delete mode 100644 nebulosa-test/src/main/kotlin/nebulosa/test/Hips2FitsStringSpec.kt diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt index c97723dbe..9843904c0 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountService.kt @@ -16,8 +16,6 @@ import nebulosa.nova.position.GeographicPosition import nebulosa.nova.position.Geoid import nebulosa.nova.position.ICRF import nebulosa.time.CurrentTime -import nebulosa.time.TimeJD -import nebulosa.time.UTC import nebulosa.wcs.WCS import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode diff --git a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsFormat.kt b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsFormat.kt index 0b9cf274c..95e648f7a 100644 --- a/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsFormat.kt +++ b/nebulosa-fits/src/main/kotlin/nebulosa/fits/FitsFormat.kt @@ -123,7 +123,7 @@ data object FitsFormat : ImageFormat { Buffer().use { buffer -> for (channel in 0 until data.numberOfChannels) { for (i in 0 until data.numberOfPixels) { - buffer.writePixel(channels[0][i], bitpix) + buffer.writePixel(channels[channel][i], bitpix) if (buffer.size >= 1024L) { byteCount += buffer.readAll(sink) @@ -131,6 +131,7 @@ data object FitsFormat : ImageFormat { } } + byteCount += buffer.readAll(sink) val remainingBytes = computeRemainingBytesToSkip(byteCount) if (remainingBytes > 0) { @@ -165,9 +166,9 @@ data object FitsFormat : ImageFormat { internal fun Buffer.writePixel(pixel: Float, bitpix: Bitpix) { when (bitpix) { Bitpix.BYTE -> writeByte((pixel * 255f).toInt()) - Bitpix.SHORT -> writeShort((pixel * 65535f).toInt()) - Bitpix.INTEGER -> writeInt((pixel * 4294967295.0).toInt()) - Bitpix.LONG -> TODO("Unsupported UInt64 sample format") + Bitpix.SHORT -> writeShort((pixel * 65535f).toInt() - 32768) + Bitpix.INTEGER -> writeInt(((pixel * 4294967295.0).toLong() - 2147483648L).toInt()) + Bitpix.LONG -> TODO("Unsupported 64-bit format") Bitpix.FLOAT -> writeFloat(pixel) Bitpix.DOUBLE -> writeDouble(pixel.toDouble()) } diff --git a/nebulosa-fits/src/test/kotlin/FitsReadTest.kt b/nebulosa-fits/src/test/kotlin/FitsReadTest.kt index a1685a3f3..ce38e8d58 100644 --- a/nebulosa-fits/src/test/kotlin/FitsReadTest.kt +++ b/nebulosa-fits/src/test/kotlin/FitsReadTest.kt @@ -4,9 +4,9 @@ import nebulosa.fits.Bitpix import nebulosa.fits.bitpix import nebulosa.fits.fits import nebulosa.image.format.ImageHdu -import nebulosa.test.FitsStringSpec +import nebulosa.test.AbstractFitsAndXisfTest -class FitsReadTest : FitsStringSpec() { +class FitsReadTest : AbstractFitsAndXisfTest() { init { "mono:8-bit" { diff --git a/nebulosa-fits/src/test/kotlin/FitsWriteTest.kt b/nebulosa-fits/src/test/kotlin/FitsWriteTest.kt index 0e2f60cd2..f455b85c5 100644 --- a/nebulosa-fits/src/test/kotlin/FitsWriteTest.kt +++ b/nebulosa-fits/src/test/kotlin/FitsWriteTest.kt @@ -4,10 +4,10 @@ import nebulosa.fits.fits import nebulosa.image.format.ImageHdu import nebulosa.io.sink import nebulosa.io.source -import nebulosa.test.FitsStringSpec +import nebulosa.test.AbstractFitsAndXisfTest import okio.ByteString.Companion.toByteString -class FitsWriteTest : FitsStringSpec() { +class FitsWriteTest : AbstractFitsAndXisfTest() { init { "mono" { diff --git a/nebulosa-image/src/test/kotlin/ComputationAlgorithmTest.kt b/nebulosa-image/src/test/kotlin/ComputationAlgorithmTest.kt index 76566dc9e..1ade07c95 100644 --- a/nebulosa-image/src/test/kotlin/ComputationAlgorithmTest.kt +++ b/nebulosa-image/src/test/kotlin/ComputationAlgorithmTest.kt @@ -7,9 +7,9 @@ import nebulosa.image.Image import nebulosa.image.algorithms.computation.MedianAbsoluteDeviation import nebulosa.image.algorithms.computation.Statistics import nebulosa.image.format.ImageChannel -import nebulosa.test.FitsStringSpec +import nebulosa.test.AbstractFitsAndXisfTest -class ComputationAlgorithmTest : FitsStringSpec() { +class ComputationAlgorithmTest : AbstractFitsAndXisfTest() { init { "mono:median absolute deviation" { diff --git a/nebulosa-image/src/test/kotlin/FitsTransformAlgorithmTest.kt b/nebulosa-image/src/test/kotlin/FitsTransformAlgorithmTest.kt index 504a9cf05..855fc06ea 100644 --- a/nebulosa-image/src/test/kotlin/FitsTransformAlgorithmTest.kt +++ b/nebulosa-image/src/test/kotlin/FitsTransformAlgorithmTest.kt @@ -9,9 +9,9 @@ import nebulosa.image.Image import nebulosa.image.algorithms.transformation.* import nebulosa.image.algorithms.transformation.convolution.* import nebulosa.image.format.ImageChannel -import nebulosa.test.FitsStringSpec +import nebulosa.test.AbstractFitsAndXisfTest -class FitsTransformAlgorithmTest : FitsStringSpec() { +class FitsTransformAlgorithmTest : AbstractFitsAndXisfTest() { init { "mono:raw" { @@ -278,23 +278,23 @@ class FitsTransformAlgorithmTest : FitsStringSpec() { nImage.save("fits-color-grayscale-y").second shouldBe "24dd4a7e0fa9e4be34c53c924a78a940" } "color:debayer" { - val mImage = Image.open(DEBAYER_FITS_PATH.fits()) + val mImage = Image.open(DEBAYER_FITS.fits()) val nImage = mImage.transform(AutoScreenTransformFunction) nImage.save("fits-color-debayer").second shouldBe "86b5bdd67dfd6bbf5495afae4bf2bc04" } "color:no-debayer" { - val mImage = Image.open(DEBAYER_FITS_PATH.fits(), false) + val mImage = Image.open(DEBAYER_FITS.fits(), false) val nImage = mImage.transform(AutoScreenTransformFunction) nImage.save("fits-color-no-debayer").second shouldBe "958ccea020deec1f0c075042a9ba37c3" } "color:reload" { val mImage0 = Image.open(NGC3344_COLOR_32_FITS.fits()) - var mImage1 = Image.open(DEBAYER_FITS_PATH.fits()) + var mImage1 = Image.open(DEBAYER_FITS.fits()) mImage1.load(mImage0.hdu).shouldNotBeNull() mImage1.save("fits-color-reload").second shouldBe "18fb83e240bc7a4cbafbc1aba2741db6" - mImage1 = Image.open(DEBAYER_FITS_PATH.fits(), false) + mImage1 = Image.open(DEBAYER_FITS.fits(), false) mImage1.load(mImage0.hdu).shouldBeNull() mImage0.load(mImage1.hdu).shouldBeNull() diff --git a/nebulosa-image/src/test/kotlin/HFDTest.kt b/nebulosa-image/src/test/kotlin/HFDTest.kt index 8cffda51d..c5f4305a8 100644 --- a/nebulosa-image/src/test/kotlin/HFDTest.kt +++ b/nebulosa-image/src/test/kotlin/HFDTest.kt @@ -1,10 +1,11 @@ import io.kotest.matchers.floats.plusOrMinus import io.kotest.matchers.shouldBe +import nebulosa.fits.fits import nebulosa.image.Image import nebulosa.image.algorithms.computation.hfd.HFD -import nebulosa.test.FitsStringSpec +import nebulosa.test.AbstractFitsAndXisfTest -class HFDTest : FitsStringSpec() { +class HFDTest : AbstractFitsAndXisfTest() { init { "focus" { @@ -29,7 +30,7 @@ class HFDTest : FitsStringSpec() { ) for ((first, second) in starFocus) { - val focusImage = Image.open(first) + val focusImage = Image.open(closeAfterEach(first.fits())) val star = focusImage.compute(HFD(focusImage.width / 2, focusImage.height / 2, 50)) star.hfd shouldBe (second[0] plusOrMinus 0.1f) star.snr shouldBe (second[1] plusOrMinus 0.1f) diff --git a/nebulosa-image/src/test/kotlin/XisfTransformAlgorithmTest.kt b/nebulosa-image/src/test/kotlin/XisfTransformAlgorithmTest.kt index a3ef8d1c7..a50872f8a 100644 --- a/nebulosa-image/src/test/kotlin/XisfTransformAlgorithmTest.kt +++ b/nebulosa-image/src/test/kotlin/XisfTransformAlgorithmTest.kt @@ -8,10 +8,10 @@ import nebulosa.image.Image import nebulosa.image.algorithms.transformation.* import nebulosa.image.algorithms.transformation.convolution.* import nebulosa.image.format.ImageChannel -import nebulosa.test.FitsStringSpec +import nebulosa.test.AbstractFitsAndXisfTest import nebulosa.xisf.xisf -class XisfTransformAlgorithmTest : FitsStringSpec() { +class XisfTransformAlgorithmTest : AbstractFitsAndXisfTest() { init { "mono:raw" { @@ -278,23 +278,23 @@ class XisfTransformAlgorithmTest : FitsStringSpec() { nImage.save("xisf-color-grayscale-y").second shouldBe "7a2bef966d460742533a1c8c3a74f1c5" } "!color:debayer" { - val mImage = Image.open(DEBAYER_FITS_PATH.xisf()) + val mImage = Image.open(DEBAYER_FITS.xisf()) val nImage = mImage.transform(AutoScreenTransformFunction) nImage.save("xisf-color-debayer").second shouldBe "86b5bdd67dfd6bbf5495afae4bf2bc04" } "!color:no-debayer" { - val mImage = Image.open(DEBAYER_FITS_PATH.xisf(), false) + val mImage = Image.open(DEBAYER_FITS.xisf(), false) val nImage = mImage.transform(AutoScreenTransformFunction) nImage.save("xisf-color-no-debayer").second shouldBe "958ccea020deec1f0c075042a9ba37c3" } "!color:reload" { val mImage0 = Image.open(M82_COLOR_32_XISF.xisf()) - var mImage1 = Image.open(DEBAYER_FITS_PATH.xisf()) + var mImage1 = Image.open(DEBAYER_FITS.xisf()) mImage1.load(mImage0.hdu).shouldNotBeNull() mImage1.save("xisf-color-reload").second shouldBe "18fb83e240bc7a4cbafbc1aba2741db6" - mImage1 = Image.open(DEBAYER_FITS_PATH.xisf(), false) + mImage1 = Image.open(DEBAYER_FITS.xisf(), false) mImage1.load(mImage0.hdu).shouldBeNull() mImage0.load(mImage1.hdu).shouldBeNull() diff --git a/nebulosa-nova/src/test/kotlin/ICRFTest.kt b/nebulosa-nova/src/test/kotlin/ICRFTest.kt index ba3229db0..8725481cc 100644 --- a/nebulosa-nova/src/test/kotlin/ICRFTest.kt +++ b/nebulosa-nova/src/test/kotlin/ICRFTest.kt @@ -4,7 +4,10 @@ import io.kotest.matchers.shouldBe import nebulosa.math.* import nebulosa.nova.position.Geoid import nebulosa.nova.position.ICRF -import nebulosa.time.* +import nebulosa.time.IERS +import nebulosa.time.IERSA +import nebulosa.time.TT +import nebulosa.time.TimeYMDHMS import java.nio.file.Path import kotlin.io.path.inputStream diff --git a/nebulosa-test/src/main/kotlin/nebulosa/test/AbstractFitsAndXisfTest.kt b/nebulosa-test/src/main/kotlin/nebulosa/test/AbstractFitsAndXisfTest.kt new file mode 100644 index 000000000..7fda567c9 --- /dev/null +++ b/nebulosa-test/src/main/kotlin/nebulosa/test/AbstractFitsAndXisfTest.kt @@ -0,0 +1,233 @@ +package nebulosa.test + +import io.kotest.core.listeners.TestListener +import io.kotest.core.spec.style.StringSpec +import io.kotest.core.test.TestCase +import io.kotest.core.test.TestResult +import io.kotest.core.test.TestScope +import nebulosa.hips2fits.Hips2FitsService +import nebulosa.hips2fits.HipsSurvey +import nebulosa.image.format.ImageHdu +import nebulosa.io.transferAndCloseOutput +import nebulosa.math.Angle +import nebulosa.math.deg +import nebulosa.math.hours +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.logging.HttpLoggingInterceptor +import okio.ByteString.Companion.toByteString +import java.awt.image.BufferedImage +import java.awt.image.DataBufferByte +import java.awt.image.DataBufferInt +import java.io.Closeable +import java.nio.file.Path +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedDeque +import java.util.concurrent.TimeUnit +import javax.imageio.ImageIO +import kotlin.io.path.* + +@Suppress("PropertyName") +abstract class AbstractFitsAndXisfTest : StringSpec() { + + protected val M82_MONO_8_LZ4_XISF by lazy { download("M82.Mono.8.LZ4.xisf", GITHUB_XISF_URL) } + protected val M82_MONO_8_LZ4_HC_XISF by lazy { download("M82.Mono.8.LZ4-HC.xisf", GITHUB_XISF_URL) } + protected val M82_MONO_8_XISF by lazy { download("M82.Mono.8.xisf", GITHUB_XISF_URL) } + protected val M82_MONO_8_ZLIB_XISF by lazy { download("M82.Mono.8.ZLib.xisf", GITHUB_XISF_URL) } + protected val M82_MONO_8_ZSTANDARD_XISF by lazy { download("M82.Mono.8.ZStandard.xisf", GITHUB_XISF_URL) } + protected val M82_MONO_16_XISF by lazy { download("M82.Mono.16.xisf", GITHUB_XISF_URL) } + protected val M82_MONO_32_XISF by lazy { download("M82.Mono.32.xisf", GITHUB_XISF_URL) } + protected val M82_MONO_F32_XISF by lazy { download("M82.Mono.F32.xisf", GITHUB_XISF_URL) } + protected val M82_MONO_F64_XISF by lazy { download("M82.Mono.F64.xisf", GITHUB_XISF_URL) } + + protected val M82_COLOR_8_LZ4_XISF by lazy { download("M82.Color.8.LZ4.xisf", GITHUB_XISF_URL) } + protected val M82_COLOR_8_LZ4_HC_XISF by lazy { download("M82.Color.8.LZ4-HC.xisf", GITHUB_XISF_URL) } + protected val M82_COLOR_8_XISF by lazy { download("M82.Color.8.xisf", GITHUB_XISF_URL) } + protected val M82_COLOR_8_ZLIB_XISF by lazy { download("M82.Color.8.ZLib.xisf", GITHUB_XISF_URL) } + protected val M82_COLOR_8_ZSTANDARD_XISF by lazy { download("M82.Color.8.ZStandard.xisf", GITHUB_XISF_URL) } + protected val M82_COLOR_16_XISF by lazy { download("M82.Color.16.xisf", GITHUB_XISF_URL) } + protected val M82_COLOR_32_XISF by lazy { download("M82.Color.32.xisf", GITHUB_XISF_URL) } + protected val M82_COLOR_F32_XISF by lazy { download("M82.Color.F32.xisf", GITHUB_XISF_URL) } + protected val M82_COLOR_F64_XISF by lazy { download("M82.Color.F64.xisf", GITHUB_XISF_URL) } + protected val DEBAYER_XISF_PATH by lazy { download("Debayer.xisf", GITHUB_XISF_URL) } + + protected val NGC3344_MONO_8_FITS by lazy { download("NGC3344.Mono.8.fits", GITHUB_FITS_URL) } + protected val NGC3344_MONO_16_FITS by lazy { download("NGC3344.Mono.16.fits", GITHUB_FITS_URL) } + protected val NGC3344_MONO_32_FITS by lazy { download("NGC3344.Mono.32.fits", GITHUB_FITS_URL) } + protected val NGC3344_MONO_F32_FITS by lazy { download("NGC3344.Mono.F32.fits", GITHUB_FITS_URL) } + protected val NGC3344_MONO_F64_FITS by lazy { download("NGC3344.Mono.F64.fits", GITHUB_FITS_URL) } + + protected val NGC3344_COLOR_8_FITS by lazy { download("NGC3344.Color.8.fits", GITHUB_FITS_URL) } + protected val NGC3344_COLOR_16_FITS by lazy { download("NGC3344.Color.16.fits", GITHUB_FITS_URL) } + protected val NGC3344_COLOR_32_FITS by lazy { download("NGC3344.Color.32.fits", GITHUB_FITS_URL) } + protected val NGC3344_COLOR_F32_FITS by lazy { download("NGC3344.Color.F32.fits", GITHUB_FITS_URL) } + protected val NGC3344_COLOR_F64_FITS by lazy { download("NGC3344.Color.F64.fits", GITHUB_FITS_URL) } + + protected val PALETTE_MONO_8_FITS by lazy { download("PALETTE.Mono.8.fits", GITHUB_FITS_URL) } + protected val PALETTE_MONO_16_FITS by lazy { download("PALETTE.Mono.16.fits", GITHUB_FITS_URL) } + protected val PALETTE_MONO_32_FITS by lazy { download("PALETTE.Mono.32.fits", GITHUB_FITS_URL) } + protected val PALETTE_MONO_F32_FITS by lazy { download("PALETTE.Mono.F32.fits", GITHUB_FITS_URL) } + protected val PALETTE_MONO_F64_FITS by lazy { download("PALETTE.Mono.F64.fits", GITHUB_FITS_URL) } + + protected val PALETTE_COLOR_8_FITS by lazy { download("PALETTE.Color.8.fits", GITHUB_FITS_URL) } + protected val PALETTE_COLOR_16_FITS by lazy { download("PALETTE.Color.16.fits", GITHUB_FITS_URL) } + protected val PALETTE_COLOR_32_FITS by lazy { download("PALETTE.Color.32.fits", GITHUB_FITS_URL) } + protected val PALETTE_COLOR_F32_FITS by lazy { download("PALETTE.Color.F32.fits", GITHUB_FITS_URL) } + protected val PALETTE_COLOR_F64_FITS by lazy { download("PALETTE.Color.F64.fits", GITHUB_FITS_URL) } + + protected val PALETTE_MONO_8_XISF by lazy { download("PALETTE.Mono.8.xisf", GITHUB_XISF_URL) } + protected val PALETTE_MONO_16_XISF by lazy { download("PALETTE.Mono.16.xisf", GITHUB_XISF_URL) } + protected val PALETTE_MONO_32_XISF by lazy { download("PALETTE.Mono.32.xisf", GITHUB_XISF_URL) } + protected val PALETTE_MONO_F32_XISF by lazy { download("PALETTE.Mono.F32.xisf", GITHUB_XISF_URL) } + protected val PALETTE_MONO_F64_XISF by lazy { download("PALETTE.Mono.F64.xisf", GITHUB_XISF_URL) } + + protected val PALETTE_COLOR_8_XISF by lazy { download("PALETTE.Color.8.xisf", GITHUB_XISF_URL) } + protected val PALETTE_COLOR_16_XISF by lazy { download("PALETTE.Color.16.xisf", GITHUB_XISF_URL) } + protected val PALETTE_COLOR_32_XISF by lazy { download("PALETTE.Color.32.xisf", GITHUB_XISF_URL) } + protected val PALETTE_COLOR_F32_XISF by lazy { download("PALETTE.Color.F32.xisf", GITHUB_XISF_URL) } + protected val PALETTE_COLOR_F64_XISF by lazy { download("PALETTE.Color.F64.xisf", GITHUB_XISF_URL) } + + protected val DEBAYER_FITS by lazy { download("Debayer.fits", GITHUB_FITS_URL) } + protected val M6707HH by lazy { download("M6707HH.fits", ASTROPY_PHOTOMETRY_URL) } + protected val M31_FITS by lazy { download("00 42 44.3".hours, "41 16 9".deg, 3.deg) } + + protected val STAR_FOCUS_1 by lazy { download("STAR.FOCUS.1.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_2 by lazy { download("STAR.FOCUS.2.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_3 by lazy { download("STAR.FOCUS.3.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_4 by lazy { download("STAR.FOCUS.4.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_5 by lazy { download("STAR.FOCUS.5.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_6 by lazy { download("STAR.FOCUS.6.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_7 by lazy { download("STAR.FOCUS.7.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_8 by lazy { download("STAR.FOCUS.8.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_9 by lazy { download("STAR.FOCUS.9.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_10 by lazy { download("STAR.FOCUS.10.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_11 by lazy { download("STAR.FOCUS.11.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_12 by lazy { download("STAR.FOCUS.12.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_13 by lazy { download("STAR.FOCUS.13.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_14 by lazy { download("STAR.FOCUS.14.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_15 by lazy { download("STAR.FOCUS.15.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_16 by lazy { download("STAR.FOCUS.16.fits", GITHUB_FITS_URL) } + protected val STAR_FOCUS_17 by lazy { download("STAR.FOCUS.17.fits", GITHUB_FITS_URL) } + + private val afterEach = AfterEach() + + init { + prependExtension(afterEach) + } + + protected fun TestScope.closeAfterEach(closeable: T) = closeable.apply { + afterEach.add(testCase to this) + } + + protected fun BufferedImage.save(name: String): Pair { + val path = Path.of("..", "data", "test", "$name.png").createParentDirectories() + ImageIO.write(this, "PNG", path.toFile()) + val md5 = path.md5() + println("$name: $md5") + return path to md5 + } + + protected fun ByteArray.md5(): String { + return toByteString().md5().hex() + } + + protected fun Path.md5(): String { + return readBytes().md5() + } + + protected fun download(name: String, baseUrl: String): Path { + return synchronize(name) { + val path = Path.of(System.getProperty("java.io.tmpdir"), name) + + if (!path.exists() || path.fileSize() <= 0L) { + val request = Request.Builder() + .get() + .url("$baseUrl/$name") + .build() + + val call = HTTP_CLIENT.newCall(request) + + call.execute().use { + it.body?.byteStream()?.transferAndCloseOutput(path.outputStream()) + } + } + + path + } + } + + protected fun download(centerRA: Angle, centerDEC: Angle, fov: Angle): Path { + val name = "$centerRA@$centerDEC@$fov".toByteArray().md5() + + return synchronize(name) { + val path = Path.of(System.getProperty("java.io.tmpdir"), name) + + if (!path.exists() || path.fileSize() <= 0L) { + HIPS_SERVICE + .query(CDS_P_DSS2_NIR.id, centerRA, centerDEC, 1280, 720, 0.0, fov) + .execute() + .body()!! + .use { it.byteStream().transferAndCloseOutput(path.outputStream()) } + } + + path + } + } + + protected fun ImageHdu.makeImage(): BufferedImage { + val type = if (numberOfChannels == 1) BufferedImage.TYPE_BYTE_GRAY else BufferedImage.TYPE_INT_RGB + val image = BufferedImage(width, height, type) + val numberOfPixels = data.numberOfPixels + + if (numberOfChannels == 1) { + val buffer = (image.raster.dataBuffer as DataBufferByte).data + + repeat(numberOfPixels) { + buffer[it] = (data.red[it] * 255f).toInt().toByte() + } + } else { + val buffer = (image.raster.dataBuffer as DataBufferInt).data + + repeat(numberOfPixels) { + val red = (data.red[it] * 255f).toInt() and 0xFF + val green = (data.green[it] * 255f).toInt() and 0xFF + val blue = (data.blue[it] * 255f).toInt() and 0xFF + buffer[it] = blue or (green shl 8) or (red shl 16) + } + } + + return image + } + + @Suppress("BlockingMethodInNonBlockingContext") + private class AfterEach : ConcurrentLinkedDeque>(), TestListener { + + override suspend fun afterEach(testCase: TestCase, result: TestResult) { + find { it.first === testCase }?.second?.close() ?: return + } + } + + companion object { + + const val ASTROPY_PHOTOMETRY_URL = "https://www.astropy.org/astropy-data/photometry" + const val GITHUB_FITS_URL = "https://github.com/tiagohm/nebulosa.data/raw/main/test/fits" + const val GITHUB_XISF_URL = "https://github.com/tiagohm/nebulosa.data/raw/main/test/xisf" + + @JvmStatic val HTTP_CLIENT = OkHttpClient.Builder() + .readTimeout(60L, TimeUnit.SECONDS) + .writeTimeout(60L, TimeUnit.SECONDS) + .connectTimeout(60L, TimeUnit.SECONDS) + .callTimeout(60L, TimeUnit.SECONDS) + .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) + .build() + + @JvmStatic val HIPS_SERVICE = Hips2FitsService(httpClient = HTTP_CLIENT) + @JvmStatic val CDS_P_DSS2_NIR = HipsSurvey("CDS/P/DSS2/NIR") + @JvmStatic @PublishedApi internal val SYNC_KEYS = ConcurrentHashMap() + + inline fun synchronize(key: String, block: () -> R): R { + val lock = synchronized(SYNC_KEYS) { SYNC_KEYS.getOrPut(key) { Any() } } + return synchronized(lock, block) + } + } +} diff --git a/nebulosa-test/src/main/kotlin/nebulosa/test/FitsStringSpec.kt b/nebulosa-test/src/main/kotlin/nebulosa/test/FitsStringSpec.kt deleted file mode 100644 index 11e5464a3..000000000 --- a/nebulosa-test/src/main/kotlin/nebulosa/test/FitsStringSpec.kt +++ /dev/null @@ -1,162 +0,0 @@ -package nebulosa.test - -import io.kotest.core.listeners.TestListener -import io.kotest.core.spec.style.StringSpec -import io.kotest.core.test.TestCase -import io.kotest.core.test.TestResult -import io.kotest.core.test.TestScope -import nebulosa.fits.FitsPath -import nebulosa.fits.fits -import nebulosa.io.transferAndCloseOutput -import nebulosa.xisf.XisfPath -import nebulosa.xisf.xisf -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.logging.HttpLoggingInterceptor -import okio.ByteString.Companion.toByteString -import java.awt.image.BufferedImage -import java.io.Closeable -import java.nio.file.Path -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.TimeUnit -import javax.imageio.ImageIO -import kotlin.io.path.* - -@Suppress("PropertyName") -abstract class FitsStringSpec : StringSpec() { - - protected val M82_MONO_8_LZ4_XISF by lazy { download("M82.Mono.8.LZ4.xisf", GITHUB_XISF_URL) } - protected val M82_MONO_8_LZ4_HC_XISF by lazy { download("M82.Mono.8.LZ4-HC.xisf", GITHUB_XISF_URL) } - protected val M82_MONO_8_XISF by lazy { download("M82.Mono.8.xisf", GITHUB_XISF_URL) } - protected val M82_MONO_8_ZLIB_XISF by lazy { download("M82.Mono.8.ZLib.xisf", GITHUB_XISF_URL) } - protected val M82_MONO_8_ZSTANDARD_XISF by lazy { download("M82.Mono.8.ZStandard.xisf", GITHUB_XISF_URL) } - protected val M82_MONO_16_XISF by lazy { download("M82.Mono.16.xisf", GITHUB_XISF_URL) } - protected val M82_MONO_32_XISF by lazy { download("M82.Mono.32.xisf", GITHUB_XISF_URL) } - protected val M82_MONO_F32_XISF by lazy { download("M82.Mono.F32.xisf", GITHUB_XISF_URL) } - protected val M82_MONO_F64_XISF by lazy { download("M82.Mono.F64.xisf", GITHUB_XISF_URL) } - - protected val M82_COLOR_8_LZ4_XISF by lazy { download("M82.Color.8.LZ4.xisf", GITHUB_XISF_URL) } - protected val M82_COLOR_8_LZ4_HC_XISF by lazy { download("M82.Color.8.LZ4-HC.xisf", GITHUB_XISF_URL) } - protected val M82_COLOR_8_XISF by lazy { download("M82.Color.8.xisf", GITHUB_XISF_URL) } - protected val M82_COLOR_8_ZLIB_XISF by lazy { download("M82.Color.8.ZLib.xisf", GITHUB_XISF_URL) } - protected val M82_COLOR_8_ZSTANDARD_XISF by lazy { download("M82.Color.8.ZStandard.xisf", GITHUB_XISF_URL) } - protected val M82_COLOR_16_XISF by lazy { download("M82.Color.16.xisf", GITHUB_XISF_URL) } - protected val M82_COLOR_32_XISF by lazy { download("M82.Color.32.xisf", GITHUB_XISF_URL) } - protected val M82_COLOR_F32_XISF by lazy { download("M82.Color.F32.xisf", GITHUB_XISF_URL) } - protected val M82_COLOR_F64_XISF by lazy { download("M82.Color.F64.xisf", GITHUB_XISF_URL) } - protected val DEBAYER_XISF_PATH by lazy { download("Debayer.xisf", GITHUB_XISF_URL) } - - protected val NGC3344_MONO_8_FITS by lazy { download("NGC3344.Mono.8.fits", GITHUB_FITS_URL) } - protected val NGC3344_MONO_16_FITS by lazy { download("NGC3344.Mono.16.fits", GITHUB_FITS_URL) } - protected val NGC3344_MONO_32_FITS by lazy { download("NGC3344.Mono.32.fits", GITHUB_FITS_URL) } - protected val NGC3344_MONO_F32_FITS by lazy { download("NGC3344.Mono.F32.fits", GITHUB_FITS_URL) } - protected val NGC3344_MONO_F64_FITS by lazy { download("NGC3344.Mono.F64.fits", GITHUB_FITS_URL) } - - protected val NGC3344_COLOR_8_FITS by lazy { download("NGC3344.Color.8.fits", GITHUB_FITS_URL) } - protected val NGC3344_COLOR_16_FITS by lazy { download("NGC3344.Color.16.fits", GITHUB_FITS_URL) } - protected val NGC3344_COLOR_32_FITS by lazy { download("NGC3344.Color.32.fits", GITHUB_FITS_URL) } - protected val NGC3344_COLOR_F32_FITS by lazy { download("NGC3344.Color.F32.fits", GITHUB_FITS_URL) } - protected val NGC3344_COLOR_F64_FITS by lazy { download("NGC3344.Color.F64.fits", GITHUB_FITS_URL) } - - protected val DEBAYER_FITS_PATH by lazy { download("Debayer.fits", GITHUB_FITS_URL) } - protected val M6707HH by lazyFITS("M6707HH.fits", ASTROPY_PHOTOMETRY_URL) - - protected val STAR_FOCUS_1 by lazyFITS("STAR.FOCUS.1.fits") - protected val STAR_FOCUS_2 by lazyFITS("STAR.FOCUS.2.fits") - protected val STAR_FOCUS_3 by lazyFITS("STAR.FOCUS.3.fits") - protected val STAR_FOCUS_4 by lazyFITS("STAR.FOCUS.4.fits") - protected val STAR_FOCUS_5 by lazyFITS("STAR.FOCUS.5.fits") - protected val STAR_FOCUS_6 by lazyFITS("STAR.FOCUS.6.fits") - protected val STAR_FOCUS_7 by lazyFITS("STAR.FOCUS.7.fits") - protected val STAR_FOCUS_8 by lazyFITS("STAR.FOCUS.8.fits") - protected val STAR_FOCUS_9 by lazyFITS("STAR.FOCUS.9.fits") - protected val STAR_FOCUS_10 by lazyFITS("STAR.FOCUS.10.fits") - protected val STAR_FOCUS_11 by lazyFITS("STAR.FOCUS.11.fits") - protected val STAR_FOCUS_12 by lazyFITS("STAR.FOCUS.12.fits") - protected val STAR_FOCUS_13 by lazyFITS("STAR.FOCUS.13.fits") - protected val STAR_FOCUS_14 by lazyFITS("STAR.FOCUS.14.fits") - protected val STAR_FOCUS_15 by lazyFITS("STAR.FOCUS.15.fits") - protected val STAR_FOCUS_16 by lazyFITS("STAR.FOCUS.16.fits") - protected val STAR_FOCUS_17 by lazyFITS("STAR.FOCUS.17.fits") - - private val afterEach = AfterEach() - - init { - prependExtension(afterEach) - } - - protected fun TestScope.closeAfterEach(closeable: T) = closeable.apply { - afterEach[testCase] = this - } - - protected fun BufferedImage.save(name: String): Pair { - val path = Path.of("..", "data", "test", "$name.png").createParentDirectories() - ImageIO.write(this, "PNG", path.toFile()) - val md5 = path.md5() - println("$name: $md5") - return path to md5 - } - - protected fun Path.md5(): String { - return readBytes().toByteString().md5().hex() - } - - protected fun lazyFITS(name: String, baseUrl: String = GITHUB_FITS_URL): Lazy { - return lazy { download(name, baseUrl).fits() } - } - - protected fun lazyXISF(name: String, baseUrl: String = GITHUB_XISF_URL): Lazy { - return lazy { download(name, baseUrl).xisf() } - } - - protected fun download(name: String, baseUrl: String): Path { - return synchronize(name) { - val path = Path.of(System.getProperty("java.io.tmpdir"), name) - - if (!path.exists() || path.fileSize() <= 0L) { - val request = Request.Builder() - .get() - .url("$baseUrl/$name") - .build() - - val call = HTTP_CLIENT.newCall(request) - - call.execute().use { - it.body?.byteStream()?.transferAndCloseOutput(path.outputStream()) - } - } - - path - } - } - - @Suppress("BlockingMethodInNonBlockingContext") - private class AfterEach : ConcurrentHashMap(), TestListener { - - override suspend fun afterEach(testCase: TestCase, result: TestResult) { - remove(testCase)?.close() - } - } - - companion object { - - const val ASTROPY_PHOTOMETRY_URL = "https://www.astropy.org/astropy-data/photometry" - const val GITHUB_FITS_URL = "https://github.com/tiagohm/nebulosa.data/raw/main/test/fits" - const val GITHUB_XISF_URL = "https://github.com/tiagohm/nebulosa.data/raw/main/test/xisf" - - @JvmStatic val HTTP_CLIENT = OkHttpClient.Builder() - .readTimeout(60L, TimeUnit.SECONDS) - .writeTimeout(60L, TimeUnit.SECONDS) - .connectTimeout(60L, TimeUnit.SECONDS) - .callTimeout(60L, TimeUnit.SECONDS) - .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)) - .build() - - @JvmStatic @PublishedApi internal val SYNC_KEYS = ConcurrentHashMap() - - inline fun synchronize(key: String, block: () -> R): R { - val lock = synchronized(SYNC_KEYS) { SYNC_KEYS.getOrPut(key) { Any() } } - return synchronized(lock, block) - } - } -} diff --git a/nebulosa-test/src/main/kotlin/nebulosa/test/Hips2FitsStringSpec.kt b/nebulosa-test/src/main/kotlin/nebulosa/test/Hips2FitsStringSpec.kt deleted file mode 100644 index 89f5715fa..000000000 --- a/nebulosa-test/src/main/kotlin/nebulosa/test/Hips2FitsStringSpec.kt +++ /dev/null @@ -1,43 +0,0 @@ -package nebulosa.test - -import nebulosa.fits.fits -import nebulosa.hips2fits.Hips2FitsService -import nebulosa.hips2fits.HipsSurvey -import nebulosa.io.transferAndCloseOutput -import nebulosa.math.Angle -import nebulosa.math.deg -import nebulosa.math.hours -import okio.ByteString.Companion.toByteString -import java.nio.file.Path -import kotlin.io.path.exists -import kotlin.io.path.fileSize -import kotlin.io.path.outputStream - -@Suppress("PropertyName") -abstract class Hips2FitsStringSpec : FitsStringSpec() { - - protected val M31 by lazy { download("00 42 44.3".hours, "41 16 9".deg, 3.deg).fits() } - - protected fun download(centerRA: Angle, centerDEC: Angle, fov: Angle): Path { - val name = "$centerRA@$centerDEC@$fov".toByteArray().toByteString().md5().hex() - val path = Path.of(System.getProperty("java.io.tmpdir"), name) - - if (path.exists() && path.fileSize() > 0L) { - return path - } - - HIPS_SERVICE - .query(CDS_P_DSS2_NIR.id, centerRA, centerDEC, 1280, 720, 0.0, fov) - .execute() - .body()!! - .use { it.byteStream().transferAndCloseOutput(path.outputStream()) } - - return path - } - - companion object { - - @JvmStatic val HIPS_SERVICE = Hips2FitsService(httpClient = HTTP_CLIENT) - @JvmStatic val CDS_P_DSS2_NIR = HipsSurvey("CDS/P/DSS2/NIR") - } -} diff --git a/nebulosa-time/src/test/kotlin/TAIMinusUTCTest.kt b/nebulosa-time/src/test/kotlin/TAIMinusUTCTest.kt index 2ed368ac6..28cbe7cc3 100644 --- a/nebulosa-time/src/test/kotlin/TAIMinusUTCTest.kt +++ b/nebulosa-time/src/test/kotlin/TAIMinusUTCTest.kt @@ -1,7 +1,7 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.doubles.shouldBeExactly -import nebulosa.time.TimeDelta import nebulosa.time.TAIMinusUTC +import nebulosa.time.TimeDelta import nebulosa.time.TimeYMDHMS class TAIMinusUTCTest : StringSpec(), TimeDelta by TAIMinusUTC { diff --git a/nebulosa-time/src/test/kotlin/TDBMinusTTByFairheadAndBretagnon1990Test.kt b/nebulosa-time/src/test/kotlin/TDBMinusTTByFairheadAndBretagnon1990Test.kt index 1d88388e3..213306b9f 100644 --- a/nebulosa-time/src/test/kotlin/TDBMinusTTByFairheadAndBretagnon1990Test.kt +++ b/nebulosa-time/src/test/kotlin/TDBMinusTTByFairheadAndBretagnon1990Test.kt @@ -1,9 +1,9 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.doubles.plusOrMinus import io.kotest.matchers.shouldBe -import nebulosa.time.TimeDelta import nebulosa.time.TDB import nebulosa.time.TDBMinusTTByFairheadAndBretagnon1990 +import nebulosa.time.TimeDelta class TDBMinusTTByFairheadAndBretagnon1990Test : StringSpec(), TimeDelta by TDBMinusTTByFairheadAndBretagnon1990 { diff --git a/nebulosa-watney/src/test/kotlin/WatnetPlateSolverTest.kt b/nebulosa-watney/src/test/kotlin/WatnetPlateSolverTest.kt index c669eaa33..552d408a6 100644 --- a/nebulosa-watney/src/test/kotlin/WatnetPlateSolverTest.kt +++ b/nebulosa-watney/src/test/kotlin/WatnetPlateSolverTest.kt @@ -3,9 +3,10 @@ import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.doubles.plusOrMinus import io.kotest.matchers.ints.shouldBeExactly import io.kotest.matchers.shouldBe +import nebulosa.fits.fits import nebulosa.image.Image import nebulosa.math.deg -import nebulosa.test.Hips2FitsStringSpec +import nebulosa.test.AbstractFitsAndXisfTest import nebulosa.test.NonGitHubOnlyCondition import nebulosa.watney.plate.solving.WatneyPlateSolver import nebulosa.watney.plate.solving.quad.CompactQuadDatabase @@ -13,7 +14,7 @@ import nebulosa.watney.star.detection.Star import java.nio.file.Path @EnabledIf(NonGitHubOnlyCondition::class) -class WatnetPlateSolverTest : Hips2FitsStringSpec() { +class WatnetPlateSolverTest : AbstractFitsAndXisfTest() { init { val quadDir = Path.of("/home/tiagohm/Downloads/watneyqdb") @@ -21,7 +22,7 @@ class WatnetPlateSolverTest : Hips2FitsStringSpec() { val solver = WatneyPlateSolver(quadDatabase) "blind" { - val image = Image.open(M31) + val image = Image.open(closeAfterEach(M31_FITS.fits())) println(solver.solve(null, image, 0.0, 0.0, 0.deg)) } "form image star quads" { diff --git a/nebulosa-watney/src/test/kotlin/WatneyStarDetectorTest.kt b/nebulosa-watney/src/test/kotlin/WatneyStarDetectorTest.kt index e038cb96f..1499471a9 100644 --- a/nebulosa-watney/src/test/kotlin/WatneyStarDetectorTest.kt +++ b/nebulosa-watney/src/test/kotlin/WatneyStarDetectorTest.kt @@ -5,13 +5,13 @@ import nebulosa.image.Image import nebulosa.image.algorithms.transformation.Draw import nebulosa.image.algorithms.transformation.convolution.Mean import nebulosa.star.detection.ImageStar -import nebulosa.test.FitsStringSpec +import nebulosa.test.AbstractFitsAndXisfTest import nebulosa.watney.star.detection.WatneyStarDetector import java.awt.Color import java.awt.Graphics2D import kotlin.math.roundToInt -class WatneyStarDetectorTest : FitsStringSpec() { +class WatneyStarDetectorTest : AbstractFitsAndXisfTest() { init { val detector = WatneyStarDetector(computeHFD = true) @@ -23,7 +23,7 @@ class WatneyStarDetectorTest : FitsStringSpec() { image.transform(ImageStarsDraw(stars)).save("color-detected-stars-1") .second shouldBe "bb237ce03f7cc9e44e69a5354b7a6fd1" - image = Image.open(M6707HH) + image = Image.open(M6707HH.fits()) stars = detector.detect(image.transform(Mean)) stars shouldHaveSize 870 image.transform(ImageStarsDraw(stars)).save("color-detected-stars-870") diff --git a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfHeaderInputStream.kt b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfHeaderInputStream.kt index 4e9a15b6b..fceda6a25 100644 --- a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfHeaderInputStream.kt +++ b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfHeaderInputStream.kt @@ -8,6 +8,7 @@ import nebulosa.image.format.HeaderCard import nebulosa.io.ByteOrder import nebulosa.xisf.XisfMonolithicFileHeader.* import nebulosa.xml.attribute +import okio.ByteString.Companion.decodeBase64 import java.io.Closeable import java.io.InputStream import javax.xml.stream.XMLStreamConstants @@ -16,6 +17,13 @@ class XisfHeaderInputStream(source: InputStream) : Closeable { private val reader = XML_INPUT_FACTORY.createXMLStreamReader(source) + @Suppress("ArrayInDataClass") + private data class ParsedAttributes( + @JvmField val cards: Collection, + @JvmField val image: Image?, + @JvmField val embedded: ByteArray, + ) + fun read(): XisfMonolithicFileHeader? { while (reader.hasNext()) { when (reader.next()) { @@ -39,9 +47,6 @@ class XisfHeaderInputStream(source: InputStream) : Closeable { val (width, height, numberOfChannels) = reader.attribute("geometry")!!.split(":") val location = reader.attribute("location")!! - check(location.startsWith("attachment")) - val (_, position, size) = location.split(":") - val sampleFormat = SampleFormat.valueOf(reader.attribute("sampleFormat")!!.uppercase()) val colorSpace = reader.attribute("colorSpace")?.uppercase()?.let(ColorSpace::valueOf) val pixelStorage = reader.attribute("pixelStorage")?.uppercase()?.let(PixelStorageModel::valueOf) @@ -49,9 +54,18 @@ class XisfHeaderInputStream(source: InputStream) : Closeable { val imageType = reader.attribute("imageType")?.let { ImageType.parse(it) } val compression = reader.attribute("compression")?.let { CompressionFormat.parse(it) } val bounds = reader.attribute("bounds")?.split(":")?.let { it[0].toFloat()..it[1].toFloat() } - val (keywords, thumbnail) = parseKeywords() + val (keywords, thumbnail, embedded) = parseAttributes() val header = FitsHeader() val isMono = numberOfChannels == "1" + var position = 0L + var size = 0L + + if (location.startsWith("attachment")) { + with(location.split(":")) { + position = this[1].toLong() + size = this[2].toLong() + } + } header.add(FitsHeaderCard.SIMPLE) header.add(sampleFormat.bitpix) @@ -64,21 +78,22 @@ class XisfHeaderInputStream(source: InputStream) : Closeable { return Image( width.toInt(), height.toInt(), numberOfChannels.toInt(), - position.toLong(), size.toLong(), + position, size, sampleFormat, colorSpace ?: ColorSpace.GRAY, pixelStorage ?: PixelStorageModel.PLANAR, byteOrder ?: ByteOrder.LITTLE, compression, imageType ?: ImageType.LIGHT, bounds ?: XisfMonolithicFileHeader.DEFAULT_BOUNDS, - header, thumbnail, + header, thumbnail, embedded, ) } - private fun parseKeywords(): Pair, Image?> { + private fun parseAttributes(): ParsedAttributes { val name = reader.localName val cards = ArrayList() var thumbnail: Image? = null + var embeddedData = ByteArray(0) fun addHeaderCard(card: HeaderCard) { if (cards.find { it.key == card.key } == null) { @@ -96,11 +111,12 @@ class XisfHeaderInputStream(source: InputStream) : Closeable { "FITSKeyword" -> addHeaderCard(parseFITSKeyword()) "Thumbnail" -> thumbnail = parseImage() "Property" -> addHeaderCard(parseProperty() ?: continue) + "Data" -> embeddedData = parseEmbeddedData() } } } - return cards to thumbnail + return ParsedAttributes(cards, thumbnail, embeddedData) } private fun parseFITSKeyword(): HeaderCard { @@ -119,6 +135,10 @@ class XisfHeaderInputStream(source: InputStream) : Closeable { return XisfHeaderCard(key.key, value, key.comment, propertyType) } + private fun parseEmbeddedData(): ByteArray { + return reader.elementText.trim().decodeBase64()!!.toByteArray() + } + override fun close() { reader.close() } diff --git a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfMonolithicFileHeader.kt b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfMonolithicFileHeader.kt index 512375336..26180c557 100644 --- a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfMonolithicFileHeader.kt +++ b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfMonolithicFileHeader.kt @@ -108,6 +108,7 @@ sealed interface XisfMonolithicFileHeader { } } + @Suppress("ArrayInDataClass") data class Image( @JvmField val width: Int, @JvmField val height: Int, @JvmField val numberOfChannels: Int, @JvmField val position: Long, @JvmField val size: Long, @@ -118,6 +119,7 @@ sealed interface XisfMonolithicFileHeader { @JvmField val bounds: ClosedFloatingPointRange = DEFAULT_BOUNDS, @JvmField val keywords: FitsHeader = FitsHeader.EMPTY, @JvmField val thumbnail: Image? = null, + @JvmField val embedded: ByteArray = ByteArray(0), ) : XisfMonolithicFileHeader companion object { diff --git a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfMonolithicFileHeaderImageData.kt b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfMonolithicFileHeaderImageData.kt index e5312f29b..0f17b1a67 100644 --- a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfMonolithicFileHeaderImageData.kt +++ b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfMonolithicFileHeaderImageData.kt @@ -3,6 +3,7 @@ package nebulosa.xisf import nebulosa.image.format.ImageChannel import nebulosa.image.format.ImageData import nebulosa.io.SeekableSource +import nebulosa.io.source import nebulosa.xisf.XisfFormat.readPixel import nebulosa.xisf.XisfMonolithicFileHeader.* import okio.Buffer @@ -20,10 +21,14 @@ internal data class XisfMonolithicFileHeaderImageData( override val height = image.height override val numberOfChannels = image.numberOfChannels + private val isEmbedded = image.embedded.isNotEmpty() + + private val realSource by lazy { if (isEmbedded) image.embedded.source() else source } + init { val uncompressedSize = image.compressionFormat?.uncompressedSize ?: image.size val expectedSize = numberOfChannels * numberOfPixels * image.sampleFormat.byteLength - check(uncompressedSize == expectedSize) { "invalid size. $uncompressedSize != $expectedSize" } + check(isEmbedded || uncompressedSize == expectedSize) { "invalid size. $uncompressedSize != $expectedSize" } } override val red by lazy { readImage(ImageChannel.RED) } @@ -79,7 +84,7 @@ internal data class XisfMonolithicFileHeaderImageData( private fun readPlanar(channel: ImageChannel, data: FloatArray): FloatArray { val startIndex = numberOfPixels * image.sampleFormat.byteLength * channel.index - source.seek(image.position + startIndex) + realSource.seek(image.position + startIndex) var remainingPixels = data.size var pos = 0 @@ -87,11 +92,11 @@ internal data class XisfMonolithicFileHeaderImageData( var closeable: (() -> Unit)? = null val compressedSource = when (image.compressionFormat?.type) { - CompressionType.ZLIB -> InflaterSource(source, Inflater(false).also { closeable = it::end }) + CompressionType.ZLIB -> InflaterSource(realSource, Inflater(false).also { closeable = it::end }) CompressionType.LZ4 -> TODO("Not implemented yet") CompressionType.LZ4_HC -> TODO("Not implemented yet") CompressionType.ZSTD -> TODO("Not implemented yet") - null -> source + null -> realSource } try { @@ -126,7 +131,7 @@ internal data class XisfMonolithicFileHeaderImageData( * a contiguous sequence (all pixel samples are stored in a single block). */ private fun readNormal(channel: ImageChannel, data: FloatArray): FloatArray { - source.seek(image.position) + realSource.seek(image.position) val blockSizeInBytes = numberOfChannels * image.sampleFormat.byteLength val bytesToSkipBefore = channel.index * image.sampleFormat.byteLength @@ -139,7 +144,7 @@ internal data class XisfMonolithicFileHeaderImageData( val n = min(PIXEL_COUNT, remainingPixels) val byteCount = n * blockSizeInBytes - check(source.read(buffer, byteCount) == byteCount) + check(realSource.read(buffer, byteCount) == byteCount) repeat(n) { if (bytesToSkipBefore > 0) buffer.skip(bytesToSkipBefore) diff --git a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfMonolithicFileHeaderImageHdu.kt b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfMonolithicFileHeaderImageHdu.kt index 2611919b0..49ae60e59 100644 --- a/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfMonolithicFileHeaderImageHdu.kt +++ b/nebulosa-xisf/src/main/kotlin/nebulosa/xisf/XisfMonolithicFileHeaderImageHdu.kt @@ -1,6 +1,5 @@ package nebulosa.xisf -import nebulosa.fits.FitsHeader import nebulosa.image.format.ImageHdu import nebulosa.io.SeekableSource diff --git a/nebulosa-xisf/src/test/kotlin/XisfFormatTest.kt b/nebulosa-xisf/src/test/kotlin/XisfFormatTest.kt index f1086772b..289992b3c 100644 --- a/nebulosa-xisf/src/test/kotlin/XisfFormatTest.kt +++ b/nebulosa-xisf/src/test/kotlin/XisfFormatTest.kt @@ -10,13 +10,10 @@ import nebulosa.fits.bitpix import nebulosa.image.format.ImageHdu import nebulosa.io.seekableSink import nebulosa.io.seekableSource -import nebulosa.test.FitsStringSpec +import nebulosa.test.AbstractFitsAndXisfTest import nebulosa.xisf.XisfFormat -import java.awt.image.BufferedImage -import java.awt.image.DataBufferByte -import java.awt.image.DataBufferInt -class XisfFormatTest : FitsStringSpec() { +class XisfFormatTest : AbstractFitsAndXisfTest() { init { "mono:planar:8" { @@ -130,7 +127,7 @@ class XisfFormatTest : FitsStringSpec() { data.blue shouldNotBeSameInstanceAs data.green val image = makeImage() - image.save("xisf-color-planar-8").second shouldBe "89beed384ee9e97ce033ba447a377937" + image.save("xisf-color-planar-8").second shouldBe "764e326cc5260d81f3761112ad6a1969" } } "color:planar:16" { @@ -149,7 +146,7 @@ class XisfFormatTest : FitsStringSpec() { data.blue shouldNotBeSameInstanceAs data.green val image = makeImage() - image.save("xisf-color-planar-16").second shouldBe "89beed384ee9e97ce033ba447a377937" + image.save("xisf-color-planar-16").second shouldBe "764e326cc5260d81f3761112ad6a1969" } } "color:planar:32" { @@ -168,7 +165,7 @@ class XisfFormatTest : FitsStringSpec() { data.blue shouldNotBeSameInstanceAs data.green val image = makeImage() - image.save("xisf-color-planar-32").second shouldBe "89beed384ee9e97ce033ba447a377937" + image.save("xisf-color-planar-32").second shouldBe "764e326cc5260d81f3761112ad6a1969" } } "color:planar:F32" { @@ -187,7 +184,7 @@ class XisfFormatTest : FitsStringSpec() { data.blue shouldNotBeSameInstanceAs data.green val image = makeImage() - image.save("xisf-color-planar-F32").second shouldBe "89beed384ee9e97ce033ba447a377937" + image.save("xisf-color-planar-F32").second shouldBe "764e326cc5260d81f3761112ad6a1969" } } "color:planar:F64" { @@ -206,7 +203,7 @@ class XisfFormatTest : FitsStringSpec() { data.blue shouldNotBeSameInstanceAs data.green val image = makeImage() - image.save("xisf-color-planar-F64").second shouldBe "89beed384ee9e97ce033ba447a377937" + image.save("xisf-color-planar-F64").second shouldBe "764e326cc5260d81f3761112ad6a1969" } } "mono:planar:8:zlib" { @@ -229,7 +226,7 @@ class XisfFormatTest : FitsStringSpec() { } } "mono:write" { - val formats = arrayOf(M82_MONO_8_XISF, M82_MONO_16_XISF, M82_MONO_32_XISF, M82_MONO_F32_XISF, M82_MONO_F64_XISF) + val formats = arrayOf(PALETTE_MONO_8_XISF, PALETTE_MONO_16_XISF, PALETTE_MONO_32_XISF, PALETTE_MONO_F32_XISF, PALETTE_MONO_F64_XISF) for (format in formats) { val source0 = closeAfterEach(format.seekableSource()) @@ -251,11 +248,11 @@ class XisfFormatTest : FitsStringSpec() { val bitpix = hdus1[0].header.bitpix val image = hdus1[0].makeImage() - image.save("xisf-mono-write-$bitpix").second shouldBe "0dca7efedef5b3525f8037f401518b0b" + image.save("xisf-mono-write-$bitpix").second shouldBe "07762064ff54ccc7771ba5b34fca86cf" } } "color:write" { - val formats = arrayOf(M82_COLOR_8_XISF, M82_COLOR_16_XISF, M82_COLOR_32_XISF, M82_COLOR_F32_XISF, M82_COLOR_F64_XISF) + val formats = arrayOf(PALETTE_COLOR_8_XISF, PALETTE_COLOR_16_XISF, PALETTE_COLOR_32_XISF, PALETTE_COLOR_F32_XISF, PALETTE_COLOR_F64_XISF) for (format in formats) { val source0 = closeAfterEach(format.seekableSource()) @@ -281,11 +278,11 @@ class XisfFormatTest : FitsStringSpec() { val bitpix = hdus1[0].header.bitpix val image = hdus1[0].makeImage() - image.save("xisf-color-write-$bitpix").second shouldBe "89beed384ee9e97ce033ba447a377937" + image.save("xisf-color-write-$bitpix").second shouldBe "7233886f62065800b43419f3b1b6c833" } } "fits-to-xisf:mono" { - val formats = arrayOf(NGC3344_MONO_8_FITS, NGC3344_MONO_16_FITS, NGC3344_MONO_32_FITS, NGC3344_MONO_F32_FITS, NGC3344_MONO_F64_FITS) + val formats = arrayOf(PALETTE_MONO_8_FITS, PALETTE_MONO_16_FITS, PALETTE_MONO_32_FITS, PALETTE_MONO_F32_FITS, PALETTE_MONO_F64_FITS) for (format in formats) { val source0 = closeAfterEach(format.seekableSource()) @@ -309,11 +306,11 @@ class XisfFormatTest : FitsStringSpec() { val bitpix = hdus1[0].header.bitpix val image = hdus1[0].makeImage() - image.save("fits-to-xisf-mono-$bitpix").second shouldBe "e17cfc29c3b343409cd8617b6913330e" + image.save("fits-to-xisf-mono-$bitpix").second shouldBe "07762064ff54ccc7771ba5b34fca86cf" } } "fits-to-xisf:color" { - val formats = arrayOf(NGC3344_COLOR_8_FITS, NGC3344_COLOR_16_FITS, NGC3344_COLOR_32_FITS, NGC3344_COLOR_F32_FITS, NGC3344_COLOR_F64_FITS) + val formats = arrayOf(PALETTE_COLOR_8_FITS, PALETTE_COLOR_16_FITS, PALETTE_COLOR_32_FITS, PALETTE_COLOR_F32_FITS, PALETTE_COLOR_F64_FITS) for (format in formats) { val source0 = closeAfterEach(format.seekableSource()) @@ -329,7 +326,7 @@ class XisfFormatTest : FitsStringSpec() { val hdus1 = XisfFormat.read(source1) hdus1 shouldHaveSize 1 - hdus1[0].data.numberOfChannels shouldBeExactly 4 + 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 @@ -341,11 +338,11 @@ class XisfFormatTest : FitsStringSpec() { val bitpix = hdus1[0].header.bitpix val image = hdus1[0].makeImage() - image.save("fits-to-xisf-color-$bitpix").second shouldBe "18fb83e240bc7a4cbafbc1aba2741db6" + image.save("fits-to-xisf-color-$bitpix").second shouldBe "7233886f62065800b43419f3b1b6c833" } } "xisf-to-fits:mono" { - val formats = arrayOf(M82_MONO_8_XISF, M82_MONO_16_XISF, M82_MONO_32_XISF, M82_MONO_F32_XISF, M82_MONO_F64_XISF) + val formats = arrayOf(PALETTE_MONO_8_XISF, PALETTE_MONO_16_XISF, PALETTE_MONO_32_XISF, PALETTE_MONO_F32_XISF, PALETTE_MONO_F64_XISF) for (format in formats) { val source0 = closeAfterEach(format.seekableSource()) @@ -369,11 +366,11 @@ class XisfFormatTest : FitsStringSpec() { val bitpix = hdus1[0].header.bitpix val image = hdus1[0].makeImage() - image.save("xisf-to-fits-mono-$bitpix").second shouldBe "0dca7efedef5b3525f8037f401518b0b" + image.save("xisf-to-fits-mono-$bitpix").second shouldBe "07762064ff54ccc7771ba5b34fca86cf" } } "xisf-to-fits:color" { - val formats = arrayOf(M82_COLOR_8_XISF, M82_COLOR_16_XISF, M82_COLOR_32_XISF, M82_COLOR_F32_XISF, M82_COLOR_F64_XISF) + val formats = arrayOf(PALETTE_COLOR_8_XISF, PALETTE_COLOR_16_XISF, PALETTE_COLOR_32_XISF, PALETTE_COLOR_F32_XISF, PALETTE_COLOR_F64_XISF) for (format in formats) { val source0 = closeAfterEach(format.seekableSource()) @@ -401,36 +398,8 @@ class XisfFormatTest : FitsStringSpec() { val bitpix = hdus1[0].header.bitpix val image = hdus1[0].makeImage() - image.save("xisf-to-fits-color-$bitpix").second shouldBe "89beed384ee9e97ce033ba447a377937" - } - } - } - - companion object { - - @JvmStatic - private fun ImageHdu.makeImage(): BufferedImage { - val type = if (numberOfChannels == 1) BufferedImage.TYPE_BYTE_GRAY else BufferedImage.TYPE_INT_RGB - val image = BufferedImage(width, height, type) - - if (numberOfChannels == 1) { - val buffer = (image.raster.dataBuffer as DataBufferByte) - - repeat(width * height) { - buffer.data[it] = (data.red[it] * 255f).toInt().toByte() - } - } else { - val buffer = (image.raster.dataBuffer as DataBufferInt) - - repeat(width * height) { - val red = (data.red[it] * 255f).toInt() and 0xFF - val green = (data.green[it] * 255f).toInt() and 0xFF - val blue = (data.blue[it] * 255f).toInt() and 0xFF - buffer.data[it] = red or (green shl 8) or (blue shl 16) - } + image.save("xisf-to-fits-color-$bitpix").second shouldBe "7233886f62065800b43419f3b1b6c833" } - - return image } } } diff --git a/nebulosa-xisf/src/test/kotlin/XisfHeaderInputStreamTest.kt b/nebulosa-xisf/src/test/kotlin/XisfHeaderInputStreamTest.kt index a5943e1a2..457aa4c2f 100644 --- a/nebulosa-xisf/src/test/kotlin/XisfHeaderInputStreamTest.kt +++ b/nebulosa-xisf/src/test/kotlin/XisfHeaderInputStreamTest.kt @@ -4,12 +4,12 @@ import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf import nebulosa.io.ByteOrder -import nebulosa.test.FitsStringSpec +import nebulosa.test.AbstractFitsAndXisfTest import nebulosa.xisf.XisfHeaderInputStream import nebulosa.xisf.XisfMonolithicFileHeader import kotlin.io.path.inputStream -class XisfHeaderInputStreamTest : FitsStringSpec() { +class XisfHeaderInputStreamTest : AbstractFitsAndXisfTest() { init { "read:8:gray" {