Newer
Older
SmTIAS-Capture / android / app / src / main / kotlin / com / example / mini_tias / PngEncoder.kt
package com.example.mini_tias

import android.graphics.Bitmap
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.util.zip.CRC32
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream

/**
 * Bitmap を非圧縮 PNG(Deflater.NO_COMPRESSION)としてエンコードするユーティリティ.
 *
 * 圧縮率より速度を優先する研究用途向け.撮影フローのボトルネック解消のため
 * 標準の `Bitmap.compress` を使わず自前で IDAT を非圧縮 zlib でラップする.
 */
internal object PngEncoder {

    /** [bitmap] を非圧縮 PNG のバイト列に変換して返す. */
    fun encodeUncompressed(bitmap: Bitmap): ByteArray {
        val w = bitmap.width
        val h = bitmap.height
        val out = ByteArrayOutputStream(w * h * 3 + 1024)

        // PNG シグネチャ
        out.write(byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A))

        // IHDR チャンク
        val ihdrData = ByteBuffer.allocate(13)
            .putInt(w)
            .putInt(h)
            .put(8)  // bit depth
            .put(2)  // color type: RGB
            .put(0)  // compression method
            .put(0)  // filter method
            .put(0)  // interlace method
            .array()
        writeChunk(out, "IHDR", ihdrData)

        // IDAT チャンク: 非圧縮 zlib でラップした RGB データ
        val idatStream = ByteArrayOutputStream(w * h * 3 + h + 64)
        val deflater = Deflater(Deflater.NO_COMPRESSION)
        val deflaterOut = DeflaterOutputStream(idatStream, deflater, 65536)

        val pixels = IntArray(w)
        val rowBytes = ByteArray(1 + w * 3)  // フィルタバイト(0) + RGB
        rowBytes[0] = 0  // フィルタ: None

        for (y in 0 until h) {
            bitmap.getPixels(pixels, 0, w, 0, y, w, 1)
            for (x in 0 until w) {
                val pixel = pixels[x]
                rowBytes[1 + x * 3] = ((pixel shr 16) and 0xFF).toByte()  // R
                rowBytes[1 + x * 3 + 1] = ((pixel shr 8) and 0xFF).toByte()  // G
                rowBytes[1 + x * 3 + 2] = (pixel and 0xFF).toByte()  // B
            }
            deflaterOut.write(rowBytes)
        }
        deflaterOut.finish()
        deflaterOut.close()
        deflater.end()

        writeChunk(out, "IDAT", idatStream.toByteArray())

        // IEND チャンク
        writeChunk(out, "IEND", byteArrayOf())

        return out.toByteArray()
    }

    /** PNG チャンクを書き込む(length + type + data + CRC). */
    private fun writeChunk(out: ByteArrayOutputStream, type: String, data: ByteArray) {
        val typeBytes = type.toByteArray(Charsets.US_ASCII)
        // length (4 bytes, big-endian)
        out.write(ByteBuffer.allocate(4).putInt(data.size).array())
        // type (4 bytes)
        out.write(typeBytes)
        // data
        out.write(data)
        // CRC (type + data)
        val crc = CRC32()
        crc.update(typeBytes)
        crc.update(data)
        out.write(ByteBuffer.allocate(4).putInt(crc.value.toInt()).array())
    }
}