android: MDT temperature decode + analyzer probe UI
This commit is contained in:
@@ -33,6 +33,21 @@ object TempMath {
|
||||
return temp.toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a raw measurement frame (19200 x u16 LE = 38400 B, as stored in
|
||||
* the MDT BLOCK_FRAME) into a millidegree-C map, one entry per pixel.
|
||||
*/
|
||||
fun tempMapFromPixels(pixels: ByteArray, w: Int = 160, h: Int = 120): IntArray {
|
||||
val n = minOf(w * h, pixels.size / 2)
|
||||
val out = IntArray(n)
|
||||
for (i in 0 until n) {
|
||||
val lo = pixels[i * 2].toInt() and 0xFF
|
||||
val hi = pixels[i * 2 + 1].toInt() and 0xFF
|
||||
out[i] = countsToTempMc(lo or (hi shl 8))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* T2E piecewise-linear evaluation with Q13 band selection
|
||||
* (vendor ReviseTemperature/CorrectTemperature core, 274-entry curve).
|
||||
|
||||
@@ -94,7 +94,7 @@ object Mdt {
|
||||
|
||||
/** Parse an MDT file produced by [compose] (or any file whose last 152
|
||||
* bytes carry a valid tail). Returns the sections or null. */
|
||||
fun parse(bytes: ByteArray): Parsed? {
|
||||
fun parse(bytes: ByteArray): MdtFile? {
|
||||
if (bytes.size < 152) return null
|
||||
val tail = bytes.copyOfRange(bytes.size - 152, bytes.size)
|
||||
if (u32(tail, 0) != SECTION_TAIL) return null
|
||||
@@ -103,7 +103,7 @@ object Mdt {
|
||||
if (u32(bytes, ddtOffset) != SECTION_DDT) return null
|
||||
val bodySize = u32(bytes, ddtOffset + 4)
|
||||
val bodyStart = ddtOffset + 0x88
|
||||
if (bodyStart + bodySize > bytes.size - 152) return null
|
||||
if (bodySize < 0 || bodyStart + bodySize > bytes.size - 152) return null
|
||||
val blocks = HashMap<Int, ByteArray>()
|
||||
var p = bodyStart
|
||||
val end = bodyStart + bodySize
|
||||
@@ -114,20 +114,39 @@ object Mdt {
|
||||
blocks[magic] = bytes.copyOfRange(p + 8, p + 8 + len)
|
||||
p += 8 + len
|
||||
}
|
||||
return Parsed(
|
||||
jpg = bytes.copyOfRange(0, ddtOffset),
|
||||
return MdtFile(
|
||||
jpg = extractJpg(bytes, ddtOffset),
|
||||
info0 = blocks[BLOCK_INFO0],
|
||||
info1 = blocks[BLOCK_INFO1],
|
||||
frame = blocks[BLOCK_FRAME],
|
||||
text = blocks[BLOCK_TXT],
|
||||
framePixels = blocks[BLOCK_FRAME],
|
||||
// block payloads are padded to 4; strip the NUL padding
|
||||
text = blocks[BLOCK_TXT]?.let { String(it, Charsets.UTF_8).trimEnd('\u0000') },
|
||||
)
|
||||
}
|
||||
|
||||
class Parsed(
|
||||
/**
|
||||
* The JPEG section is padded to 4 bytes before the DDT header, so cut it
|
||||
* back to the last EOI marker (FF D9) — the padding bytes are zeros and
|
||||
* cannot contain one.
|
||||
*/
|
||||
private fun extractJpg(bytes: ByteArray, ddtOffset: Int): ByteArray {
|
||||
var i = ddtOffset - 2
|
||||
while (i >= 0) {
|
||||
if ((bytes[i].toInt() and 0xFF) == 0xFF && (bytes[i + 1].toInt() and 0xFF) == 0xD9) {
|
||||
return bytes.copyOfRange(0, i + 2)
|
||||
}
|
||||
i--
|
||||
}
|
||||
return bytes.copyOfRange(0, ddtOffset)
|
||||
}
|
||||
|
||||
/** Decoded MDT container (mirror of [compose]). */
|
||||
class MdtFile(
|
||||
val jpg: ByteArray,
|
||||
val info0: ByteArray?,
|
||||
val info1: ByteArray?,
|
||||
val frame: ByteArray?,
|
||||
val text: ByteArray?,
|
||||
/** Raw 38400 B measurement frame (19200 x u16 LE), null when absent. */
|
||||
val framePixels: ByteArray?,
|
||||
val text: String?,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ class AnalyzeViewModel(
|
||||
private val containerBytes: ByteArray,
|
||||
private val fileUri: android.net.Uri,
|
||||
) : AndroidViewModel(app) {
|
||||
val parsed: Mdt.Parsed? = Mdt.parse(containerBytes)
|
||||
val parsed: Mdt.MdtFile? = Mdt.parse(containerBytes)
|
||||
|
||||
/** Raw measurement frame (19200 uint16) if present. */
|
||||
val rawFrame: IntArray? by lazy {
|
||||
parsed?.frame?.let { raw ->
|
||||
parsed?.framePixels?.let { raw ->
|
||||
val out = IntArray(19200)
|
||||
for (i in out.indices) {
|
||||
out[i] = (raw[i * 2].toInt() and 0xFF) or ((raw[i * 2 + 1].toInt() and 0xFF) shl 8)
|
||||
@@ -33,6 +33,39 @@ class AnalyzeViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/** Millidegree-C map of the measurement frame (19200 entries), if present. */
|
||||
private val _tempMap = androidx.compose.runtime.mutableStateOf<IntArray?>(null)
|
||||
val tempMap: IntArray? get() = _tempMap.value
|
||||
|
||||
/**
|
||||
* Selected probe in sensor coordinates (x 0..159, y 0..119).
|
||||
* Backed by snapshot state so the viewer's draw phase invalidates on change.
|
||||
*/
|
||||
private val _probe = androidx.compose.runtime.mutableStateOf<Pair<Int, Int>?>(null)
|
||||
var probe: Pair<Int, Int>?
|
||||
get() = _probe.value
|
||||
set(value) {
|
||||
_probe.value = value
|
||||
}
|
||||
|
||||
init {
|
||||
val frame = parsed?.framePixels
|
||||
if (frame != null) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val map = com.mag160c.thermal.core.TempMath.tempMapFromPixels(frame)
|
||||
withContext(Dispatchers.Main) { _tempMap.value = map }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Temperature (millidegree C) at a sensor pixel from the decoded map. */
|
||||
fun probeTempMc(x: Int, y: Int): Int? {
|
||||
val map = _tempMap.value ?: return null
|
||||
if (x < 0 || y < 0 || x >= 160 || y >= 120) return null
|
||||
val idx = y * 160 + x
|
||||
return if (idx < map.size) map[idx] else null
|
||||
}
|
||||
|
||||
private val _render = MutableStateFlow<Bitmap?>(null)
|
||||
val render: StateFlow<Bitmap?> = _render
|
||||
|
||||
@@ -65,10 +98,7 @@ class AnalyzeViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeNote(): String? {
|
||||
val t = parsed?.text ?: return null
|
||||
return String(t, Charsets.UTF_8)
|
||||
}
|
||||
fun decodeNote(): String? = parsed?.text
|
||||
|
||||
/** Probe temperature approximation at a raw pixel (millidegrees C). */
|
||||
fun probeTemp(x: Int, y: Int): Int? {
|
||||
@@ -90,7 +120,7 @@ class AnalyzeViewModel(
|
||||
jpg = jpg,
|
||||
info0 = parsed?.info0,
|
||||
info1 = parsed?.info1,
|
||||
framePixels = parsed?.frame,
|
||||
framePixels = parsed?.framePixels,
|
||||
text = note.toByteArray(Charsets.UTF_8),
|
||||
)
|
||||
val ok = runCatching {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -33,10 +34,15 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mag160c.thermal.core.Palettes
|
||||
import com.mag160c.thermal.ui.gallery.GalleryViewModel
|
||||
|
||||
@@ -83,6 +89,9 @@ fun AnalyzeViewer(item: GalleryViewModel.Item, galleryVm: GalleryViewModel) {
|
||||
pan = Offset.Zero
|
||||
}
|
||||
}
|
||||
}
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures { pos -> vm.probe = tapProbe(vm.probe, pos, zoom, pan, size) }
|
||||
},
|
||||
) {
|
||||
val img = render?.asImageBitmap()
|
||||
@@ -97,6 +106,11 @@ fun AnalyzeViewer(item: GalleryViewModel.Item, galleryVm: GalleryViewModel) {
|
||||
dstSize = IntSize(w.toInt(), h.toInt()),
|
||||
)
|
||||
}
|
||||
val tempMap = vm.tempMap
|
||||
if (tempMap != null) {
|
||||
drawTemperatureOsd(tempMap)
|
||||
vm.probe?.let { drawProbeMarker(it, tempMap, zoom, pan) }
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -166,6 +180,123 @@ fun AnalyzeViewer(item: GalleryViewModel.Item, galleryVm: GalleryViewModel) {
|
||||
|
||||
private fun IntOffsetCompat(x: Float, y: Float): Offset = Offset(x, y)
|
||||
|
||||
private const val TEMP_W = 160
|
||||
private const val TEMP_H = 120
|
||||
|
||||
/**
|
||||
* Image rect actually occupied by the bitmap for the current zoom/pan.
|
||||
* NOTE: unlike the live screen (which always draws the frame rotated 90 deg
|
||||
* CW), this viewer draws it in native orientation, so the tap mapping below is
|
||||
* the direct one — copying the live 90-deg inverse map here would return the
|
||||
* temperature of a wrongly transposed pixel.
|
||||
*/
|
||||
private fun imageRect(size: androidx.compose.ui.geometry.Size, zoom: Float, pan: Offset) =
|
||||
androidx.compose.ui.geometry.Rect(
|
||||
left = (size.width - size.width * zoom) / 2 + pan.x,
|
||||
top = (size.height - size.height * zoom) / 2 + pan.y,
|
||||
right = (size.width - size.width * zoom) / 2 + pan.x + size.width * zoom,
|
||||
bottom = (size.height - size.height * zoom) / 2 + pan.y + size.height * zoom,
|
||||
)
|
||||
|
||||
/** Tap -> sensor pixel; tapping the same spot again clears the probe. */
|
||||
private fun PointerInputScope.tapProbe(
|
||||
current: Pair<Int, Int>?,
|
||||
pos: Offset,
|
||||
zoom: Float,
|
||||
pan: Offset,
|
||||
size: IntSize,
|
||||
): Pair<Int, Int>? {
|
||||
val rect = imageRect(
|
||||
androidx.compose.ui.geometry.Size(size.width.toFloat(), size.height.toFloat()),
|
||||
zoom, pan,
|
||||
)
|
||||
if (!rect.contains(pos)) return current
|
||||
val x = ((pos.x - rect.left) / rect.width * TEMP_W).toInt().coerceIn(0, TEMP_W - 1)
|
||||
val y = ((pos.y - rect.top) / rect.height * TEMP_H).toInt().coerceIn(0, TEMP_H - 1)
|
||||
val hit = current?.let {
|
||||
val dx = (it.first - x) * rect.width / TEMP_W
|
||||
val dy = (it.second - y) * rect.height / TEMP_H
|
||||
dx * dx + dy * dy <= (6.dp.toPx() * 6.dp.toPx())
|
||||
} ?: false
|
||||
return if (hit) null else x to y
|
||||
}
|
||||
|
||||
/** Temperature bar glued to the top of the displayed image (fit-rect based). */
|
||||
private fun DrawScope.drawTemperatureOsd(map: IntArray) {
|
||||
if (map.size < TEMP_W * TEMP_H) return
|
||||
val rect = imageRect(size, 1f, Offset.Zero)
|
||||
var mn = Int.MAX_VALUE
|
||||
var mx = Int.MIN_VALUE
|
||||
for (v in map) {
|
||||
if (v < mn) mn = v
|
||||
if (v > mx) mx = v
|
||||
}
|
||||
val center = map[(TEMP_H / 2) * TEMP_W + TEMP_W / 2]
|
||||
val text = "中心 %.1f℃ 最低 %.1f℃ 最高 %.1f℃".format(
|
||||
Locale.US, center / 1000f, mn / 1000f, mx / 1000f,
|
||||
)
|
||||
val barH = 28.dp.toPx()
|
||||
val radius = 4.dp.toPx()
|
||||
drawRoundRect(
|
||||
color = Color(0x99000000),
|
||||
topLeft = Offset(rect.left, rect.top),
|
||||
size = androidx.compose.ui.geometry.Size(rect.width, barH),
|
||||
cornerRadius = androidx.compose.ui.geometry.CornerRadius(radius, radius),
|
||||
)
|
||||
val paint = android.graphics.Paint().apply {
|
||||
color = android.graphics.Color.WHITE
|
||||
textSize = 12.sp.toPx()
|
||||
isAntiAlias = true
|
||||
}
|
||||
val baseline = rect.top + barH / 2f - (paint.ascent() + paint.descent()) / 2f
|
||||
drawIntoCanvas { canvas ->
|
||||
canvas.nativeCanvas.drawText(text, rect.left + 12.dp.toPx(), baseline, paint)
|
||||
}
|
||||
}
|
||||
|
||||
/** White dot + ring marker at the probed pixel, with a temperature label. */
|
||||
private fun DrawScope.drawProbeMarker(
|
||||
probe: Pair<Int, Int>,
|
||||
map: IntArray,
|
||||
zoom: Float,
|
||||
pan: Offset,
|
||||
) {
|
||||
val idx = probe.second * TEMP_W + probe.first
|
||||
if (idx < 0 || idx >= map.size) return
|
||||
val rect = imageRect(size, zoom, pan)
|
||||
val cx = rect.left + (probe.first + 0.5f) / TEMP_W * rect.width
|
||||
val cy = rect.top + (probe.second + 0.5f) / TEMP_H * rect.height
|
||||
val dotR = 4.dp.toPx()
|
||||
val ringR = 9.dp.toPx()
|
||||
drawCircle(Color.White, dotR, Offset(cx, cy))
|
||||
drawCircle(Color.White, ringR, Offset(cx, cy), style = androidx.compose.ui.graphics.drawscope.Stroke(2.dp.toPx()))
|
||||
|
||||
val label = "%.1f℃".format(Locale.US, map[idx] / 1000f)
|
||||
val paint = android.graphics.Paint().apply {
|
||||
color = android.graphics.Color.BLACK
|
||||
textSize = 12.sp.toPx()
|
||||
isAntiAlias = true
|
||||
}
|
||||
val padH = 6.dp.toPx()
|
||||
val padV = 4.dp.toPx()
|
||||
val tw = paint.measureText(label)
|
||||
val boxW = tw + padH * 2
|
||||
val boxH = paint.textSize + padV * 2
|
||||
var boxLeft = cx + 8.dp.toPx()
|
||||
if (boxLeft + boxW > rect.right) boxLeft = cx - 8.dp.toPx() - boxW
|
||||
var boxTop = cy - 8.dp.toPx() - boxH
|
||||
if (boxTop < rect.top) boxTop = cy + 8.dp.toPx()
|
||||
val radius = 4.dp.toPx()
|
||||
drawRoundRect(
|
||||
color = Color(0xF0FFFFFF),
|
||||
topLeft = Offset(boxLeft, boxTop),
|
||||
size = androidx.compose.ui.geometry.Size(boxW, boxH),
|
||||
cornerRadius = androidx.compose.ui.geometry.CornerRadius(radius, radius),
|
||||
)
|
||||
val baseline = boxTop + boxH / 2f - (paint.ascent() + paint.descent()) / 2f
|
||||
drawIntoCanvas { canvas -> canvas.nativeCanvas.drawText(label, boxLeft + padH, baseline, paint) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoteEditor(initial: String, onSave: (String) -> Unit, onDismiss: () -> Unit) {
|
||||
var text by remember { mutableStateOf(initial) }
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.mag160c.thermal.core
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/** Phase B: MDT raw-frame -> millidegree temperature map. */
|
||||
class TempMathTest {
|
||||
private fun frameLe(vararg counts: Int): ByteArray {
|
||||
val out = ByteArray(counts.size * 2)
|
||||
counts.forEachIndexed { i, c ->
|
||||
out[i * 2] = (c and 0xFF).toByte()
|
||||
out[i * 2 + 1] = ((c shr 8) and 0xFF).toByte()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mapHas19200EntriesForAFullFrame() {
|
||||
val frame = ByteArray(38400) { ((it * 7 + 33) and 0xFF).toByte() }
|
||||
val map = TempMath.tempMapFromPixels(frame)
|
||||
assertEquals(19200, map.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mapIsMonotonicInCounts() {
|
||||
// increasing counts must give increasing temperature (spot samples)
|
||||
val counts = intArrayOf(6000, 6500, 7000, 7500, 8000)
|
||||
val map = TempMath.tempMapFromPixels(frameLe(*counts), w = counts.size, h = 1)
|
||||
for (i in 1 until map.size) {
|
||||
assertTrue(
|
||||
"monotonic: ${counts[i - 1]}->${counts[i]} gave ${map[i - 1]}->${map[i]}",
|
||||
map[i] > map[i - 1],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mapMatchesScalarConversionPerPixel() {
|
||||
val counts = intArrayOf(5000, 7000, 9000, 11000)
|
||||
val map = TempMath.tempMapFromPixels(frameLe(*counts), w = counts.size, h = 1)
|
||||
counts.forEachIndexed { i, c ->
|
||||
assertEquals("pixel $i", TempMath.countsToTempMc(c), map[i])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ambientRegionIsPlausibleCelsius() {
|
||||
// 7000 counts is the ambient-ish region used by the live pipeline test
|
||||
val map = TempMath.tempMapFromPixels(frameLe(7000), w = 1, h = 1)
|
||||
assertTrue("plausible mC: ${map[0]}", map[0] in 0..60_000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shortInputIsHandledWithoutThrowing() {
|
||||
// defensively stop at the available bytes instead of over-reading
|
||||
val map = TempMath.tempMapFromPixels(frameLe(7000, 7001, 7002))
|
||||
assertEquals(3, map.size)
|
||||
assertEquals(TempMath.countsToTempMc(7000), map[0])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.mag160c.thermal.media
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* MDT container round trip: compose -> parse must reproduce every section,
|
||||
* and a corrupted tail must be rejected (offline analysis, phase B).
|
||||
*/
|
||||
class MdtTest {
|
||||
/** Minimal fake JPEG: SOI + payload + EOI, length deliberately not
|
||||
* aligned to 4 so the padding cut-back is exercised. */
|
||||
private fun fakeJpg(): ByteArray = byteArrayOf(
|
||||
0xFF.toByte(), 0xD8.toByte(), 0x11, 0x22, 0x33, 0x44, 0x55,
|
||||
0xFF.toByte(), 0xD9.toByte(),
|
||||
)
|
||||
|
||||
private fun pixels(seed: Int = 1234): ByteArray {
|
||||
val out = ByteArray(38400)
|
||||
var s = seed
|
||||
for (i in out.indices) {
|
||||
s = s * 1103515245 + 12345
|
||||
out[i] = ((s shr 16) and 0xFF).toByte()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@Test
|
||||
fun composeParseRoundTripPreservesAllSections() {
|
||||
val jpg = fakeJpg()
|
||||
val info0 = ByteArray(0x38) { (it * 3).toByte() }
|
||||
val info1 = ByteArray(0x38) { (it * 5 + 1).toByte() }
|
||||
val frame = pixels()
|
||||
val note = "现场巡检 A 区 3 号柜"
|
||||
|
||||
val mdt = Mdt.compose(
|
||||
jpg = jpg,
|
||||
info0 = info0,
|
||||
info1 = info1,
|
||||
framePixels = frame,
|
||||
text = note.toByteArray(Charsets.UTF_8),
|
||||
)
|
||||
val parsed = Mdt.parse(mdt)
|
||||
assertNotNull("parse must succeed", parsed)
|
||||
parsed!!
|
||||
assertArrayEquals("jpg (padding trimmed)", jpg, parsed.jpg)
|
||||
assertArrayEquals("info0", info0, parsed.info0)
|
||||
assertArrayEquals("info1", info1, parsed.info1)
|
||||
assertArrayEquals("framePixels", frame, parsed.framePixels)
|
||||
assertEquals("text", note, parsed.text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun composeWithoutOptionalBlocksStillParses() {
|
||||
val jpg = fakeJpg()
|
||||
val mdt = Mdt.compose(jpg = jpg, info0 = null, info1 = null, framePixels = null)
|
||||
val parsed = Mdt.parse(mdt)
|
||||
assertNotNull(parsed)
|
||||
parsed!!
|
||||
assertArrayEquals(jpg, parsed.jpg)
|
||||
assertNull(parsed.info0)
|
||||
assertNull(parsed.info1)
|
||||
assertNull(parsed.framePixels)
|
||||
assertNull(parsed.text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun corruptedTailReturnsNull() {
|
||||
val mdt = Mdt.compose(fakeJpg(), ByteArray(0x38) { 1 }, null, pixels())
|
||||
// the tail carries the section magic + ddt offset; the remaining 144
|
||||
// reserved bytes are opaque to us (no checksum in the format), so the
|
||||
// detectable single-byte corruptions are the two meaningful fields
|
||||
val tail = mdt.size - 152
|
||||
for (flipAt in intArrayOf(tail, tail + 4, tail + 6)) {
|
||||
val bad = mdt.copyOf()
|
||||
bad[flipAt] = (bad[flipAt].toInt() xor 0x5A).toByte()
|
||||
assertNull("flip at $flipAt must be rejected", Mdt.parse(bad))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun corruptedDdtHeaderReturnsNull() {
|
||||
val mdt = Mdt.compose(fakeJpg(), null, null, pixels())
|
||||
val ddtOffset = Mdt.u32(mdt, mdt.size - 152 + 4)
|
||||
val bad = mdt.copyOf()
|
||||
bad[ddtOffset] = (bad[ddtOffset].toInt() xor 0xFF).toByte()
|
||||
assertNull("bad ddt section magic must be rejected", Mdt.parse(bad))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun truncatedFileReturnsNull() {
|
||||
val mdt = Mdt.compose(fakeJpg(), null, null, pixels())
|
||||
assertNull(Mdt.parse(mdt.copyOfRange(0, 100)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun framePixelsAre19200LittleEndianShort() {
|
||||
val mdt = Mdt.compose(fakeJpg(), null, null, pixels())
|
||||
val parsed = Mdt.parse(mdt)!!
|
||||
val frame = parsed.framePixels!!
|
||||
assertEquals("38400 bytes = 19200 u16 LE", 19200, frame.size / 2)
|
||||
assertTrue("some non-zero payload", frame.any { it.toInt() != 0 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user