1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// PNG 元数据读取(纯 MoonBit):text chunks (tEXt/iTXt)

///|

///|
/// PNG text chunk(关键字-文本对)
pub(all) struct PngTextChunk {
  keyword : String
  text : String
} derive(Eq, @debug.Debug)

///|
fn read_png_bytes_as_string(data : Bytes, start : Int, end : Int) -> String {
  let chars : Array[Byte] = []
  for i in start..<end {
    chars.push(data[i])
  }
  @encoding/utf8.decode(Bytes::from_array(chars)[:]) catch {
    _ => ""
  }
}

///|
/// 从 PNG 字节流读取 text chunks(tEXt 和 iTXt)。
/// 返回关键字-文本对数组。不含 EXIF 等其他元数据。
pub fn read_png_text_chunks(data : Bytes) -> Array[PngTextChunk] {
  let result : Array[PngTextChunk] = []
  let len = data.length()
  // PNG signature: 8 bytes
  if len < 8 ||
    data[0].to_int() != 0x89 ||
    data[1].to_int() != 0x50 ||
    data[2].to_int() != 0x4E ||
    data[3].to_int() != 0x47 {
    return result
  }
  let mut pos = 8
  while pos + 8 <= len {
    // chunk length (4 bytes BE) + type (4 bytes)
    let chunk_len = data[pos].to_int() * 16777216 +
      data[pos + 1].to_int() * 65536 +
      data[pos + 2].to_int() * 256 +
      data[pos + 3].to_int()
    let type0 = data[pos + 4].to_int()
    let type1 = data[pos + 5].to_int()
    let type2 = data[pos + 6].to_int()
    let type3 = data[pos + 7].to_int()
    let data_start = pos + 8
    if data_start + chunk_len > len {
      break
    }
    // tEXt: type = 0x74 0x45 0x58 0x74
    if type0 == 0x74 && type1 == 0x45 && type2 == 0x58 && type3 == 0x74 {
      // tEXt: keyword + 0x00 + text
      let mut sep = -1
      for i in 0..<chunk_len {
        if data[data_start + i] == b'\x00' {
          sep = i
          break
        }
      }
      if sep >= 0 {
        let keyword = read_png_bytes_as_string(
          data,
          data_start,
          data_start + sep,
        )
        let text = read_png_bytes_as_string(
          data,
          data_start + sep + 1,
          data_start + chunk_len,
        )
        result.push(PngTextChunk::{ keyword, text })
      }
    }
    // iTXt: type = 0x69 0x54 0x58 0x74
    if type0 == 0x69 && type1 == 0x54 && type2 == 0x58 && type3 == 0x74 {
      // iTXt: keyword + 0x00 + compression_flag + compression_method + language_tag + 0x00 + translated_keyword + 0x00 + text
      let mut sep = -1
      for i in 0..<chunk_len {
        if data[data_start + i] == b'\x00' {
          sep = i
          break
        }
      }
      if sep >= 0 && sep + 2 < chunk_len {
        let keyword = read_png_bytes_as_string(
          data,
          data_start,
          data_start + sep,
        )
        let comp_flag = data[data_start + sep + 1].to_int()
        // 跳过 compression_method (1 byte) + language_tag + 0x00 + translated_keyword + 0x00
        let mut text_start = sep + 3 // skip comp_flag + comp_method
        // 跳过 language tag (until 0x00)
        while text_start < chunk_len && data[data_start + text_start] != b'\x00' {
          text_start += 1
        }
        text_start += 1 // skip 0x00
        // 跳过 translated keyword (until 0x00)
        while text_start < chunk_len && data[data_start + text_start] != b'\x00' {
          text_start += 1
        }
        text_start += 1 // skip 0x00
        if text_start <= chunk_len {
          let text = if comp_flag == 0 {
            read_png_bytes_as_string(
              data,
              data_start + text_start,
              data_start + chunk_len,
            )
          } else {
            // 压缩文本不支持(需要 zlib 解压)
            "[compressed]"
          }
          result.push(PngTextChunk::{ keyword, text })
        }
      }
    }
    // IEND 标志着 PNG 结束
    if type0 == 0x49 && type1 == 0x45 && type2 == 0x4E && type3 == 0x44 {
      break
    }
    // 前进到下一个 chunk: length(4) + type(4) + data(chunk_len) + CRC(4)
    pos = data_start + chunk_len + 4
  }
  result
}