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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
// 纯 MoonBit APNG 编解码(全目标可用:native/wasm/js)
// APNG 规范: https://wiki.mozilla.org/APNG_Specification
// APNG 是 PNG 的扩展,新增 acTL/fcTL/fdAT chunk
// 仅支持全尺寸帧(x_offset=0, y_offset=0, frame_w=width, frame_h=height)
///|
/// 从 4 字节构造 chunk type 字符串
fn apng_chunk_type(data : Bytes, offset : Int) -> String {
let buf : Array[Byte] = [
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]
@encoding/utf8.decode(Bytes::from_array(buf)) catch {
_ => "????"
}
}
///|
/// PNG chunk 信息
priv struct PngChunk {
chunk_type : String
data_offset : Int
data_size : Int
}
///|
/// 解析所有 PNG chunk
fn apng_parse_chunks(data : Bytes) -> Array[PngChunk] raise @types.LoadError {
let len = data.length()
if len < 8 {
raise @types.LoadError::DecodeFailed("PNG/APNG 数据过短")
}
// 验证 PNG signature
if !is_png_signature(data, 0) {
raise @types.LoadError::DecodeFailed("PNG signature 错误")
}
let chunks : Array[PngChunk] = []
let mut pos = 8
while pos + 8 <= len {
let chunk_len = read_u32_be(data, pos)
let chunk_type = apng_chunk_type(data, pos + 4)
pos += 8
if pos + chunk_len + 4 > len {
raise @types.LoadError::DecodeFailed("PNG chunk 数据越界")
}
chunks.push(PngChunk::{ chunk_type, data_offset: pos, data_size: chunk_len })
pos += chunk_len + 4 // 跳过数据 + CRC
}
chunks
}
///|
/// 从 chunk 数据中提取子字节流
fn apng_extract_chunk_data(data : Bytes, chunk : PngChunk) -> Bytes {
let out : Array[Byte] = []
for i in 0..<chunk.data_size {
out.push(data[chunk.data_offset + i])
}
Bytes::from_array(out)
}
///|
/// 构建独立 PNG 文件(signature + IHDR + IDAT + IEND)
fn build_standalone_png(ihdr_data : Bytes, idat_data : Bytes) -> Bytes {
let out : Array[Byte] = []
// PNG signature
out.push(b'\x89')
out.push(b'\x50')
out.push(b'\x4E')
out.push(b'\x47')
out.push(b'\x0D')
out.push(b'\x0A')
out.push(b'\x1A')
out.push(b'\x0A')
// IHDR
write_u32_be(out, ihdr_data.length())
out.push(b'\x49')
out.push(b'\x48')
out.push(b'\x44')
out.push(b'\x52')
for i in 0..<ihdr_data.length() {
out.push(ihdr_data[i])
}
// IHDR CRC (simplified: compute over type+data)
let crc_data : Array[Byte] = [b'\x49', b'\x48', b'\x44', b'\x52']
for i in 0..<ihdr_data.length() {
crc_data.push(ihdr_data[i])
}
let ihdr_crc = @util.crc32(Bytes::from_array(crc_data))
write_u32_be(out, ihdr_crc)
// IDAT
write_u32_be(out, idat_data.length())
out.push(b'\x49')
out.push(b'\x44')
out.push(b'\x41')
out.push(b'\x54')
for i in 0..<idat_data.length() {
out.push(idat_data[i])
}
let idat_crc_data : Array[Byte] = [b'\x49', b'\x44', b'\x41', b'\x54']
for i in 0..<idat_data.length() {
idat_crc_data.push(idat_data[i])
}
let idat_crc = @util.crc32(Bytes::from_array(idat_crc_data))
write_u32_be(out, idat_crc)
// IEND
write_u32_be(out, 0)
out.push(b'\x49')
out.push(b'\x45')
out.push(b'\x4E')
out.push(b'\x44')
let iend_crc = @util.crc32(b"\x49\x45\x4E\x44")
write_u32_be(out, iend_crc)
Bytes::from_array(out)
}
///|
/// 解码 APNG 字节流为 PngAnimation
/// 如果不是 APNG(无 acTL chunk),则当作单帧 PNG 返回
pub fn decode_apng_pure(
data : Bytes,
) -> @types.PngAnimation raise @types.LoadError {
let chunks = apng_parse_chunks(data)
// 查找 IHDR
let mut ihdr_data : Bytes = b""
let mut found_ihdr = false
for i in 0..<chunks.length() {
if chunks[i].chunk_type == "IHDR" {
ihdr_data = apng_extract_chunk_data(data, chunks[i])
found_ihdr = true
break
}
}
if !found_ihdr {
raise @types.LoadError::DecodeFailed("PNG/APNG 缺少 IHDR")
}
// 查找 acTL
let mut has_actl = false
for i in 0..<chunks.length() {
if chunks[i].chunk_type == "acTL" {
has_actl = true
break
}
}
if !has_actl {
// 普通 PNG,当作单帧动画
let img = decode_png_pure(data)
return @types.PngAnimation::{ frames: [img], delays: [0] }
}
// APNG: 收集所有帧
let frames : Array[@types.Image] = []
let delays : Array[Int] = []
// 收集 IDAT 数据(第一帧可能用 IDAT)
let mut idat_data : Bytes = b""
let mut found_idat = false
for i in 0..<chunks.length() {
if chunks[i].chunk_type == "IDAT" {
let chunk_data = apng_extract_chunk_data(data, chunks[i])
if !found_idat {
idat_data = chunk_data
found_idat = true
} else {
// 合并多个 IDAT
let merged : Array[Byte] = []
for j in 0..<idat_data.length() {
merged.push(idat_data[j])
}
for j in 0..<chunk_data.length() {
merged.push(chunk_data[j])
}
idat_data = Bytes::from_array(merged)
}
}
}
// 遍历 chunk,处理 fcTL + IDAT/fdAT
let mut i = 0
let mut first_frame_done = false
while i < chunks.length() {
if chunks[i].chunk_type == "fcTL" {
// 解析 fcTL
let fctl_data = apng_extract_chunk_data(data, chunks[i])
// delay_num (2 bytes at offset 20), delay_den (2 bytes at offset 22)
let delay_num = read_u16_be(fctl_data, 20)
let delay_den = read_u16_be(fctl_data, 22)
let delay_ms = if delay_den == 0 {
0
} else {
delay_num * 1000 / delay_den
}
// 查找对应的帧数据(IDAT 或 fdAT)
let mut frame_idat : Bytes = b""
let mut found_frame_data = false
let mut j = i + 1
while j < chunks.length() {
if chunks[j].chunk_type == "IDAT" && !first_frame_done {
frame_idat = idat_data
found_frame_data = true
first_frame_done = true
break
} else if chunks[j].chunk_type == "fdAT" {
// fdAT: 前 4 字节是 seq_num,后面是 IDAT 数据
let fdat_data = apng_extract_chunk_data(data, chunks[j])
let fdat_idat : Array[Byte] = []
for k in 4..<fdat_data.length() {
fdat_idat.push(fdat_data[k])
}
frame_idat = Bytes::from_array(fdat_idat)
found_frame_data = true
break
} else if chunks[j].chunk_type == "fcTL" {
// 遇到下一个 fcTL,停止
break
}
j += 1
}
if found_frame_data {
let png = build_standalone_png(ihdr_data, frame_idat)
let img = decode_png_pure(png)
frames.push(img)
delays.push(delay_ms)
}
}
i += 1
}
if frames.length() == 0 {
raise @types.LoadError::DecodeFailed("APNG 无有效帧")
}
@types.PngAnimation::{ frames, delays }
}
///|
/// 从 PNG 字节流中提取 IDAT 数据
fn extract_idat_from_png(png_data : Bytes) -> Bytes raise @types.LoadError {
let chunks = apng_parse_chunks(png_data)
let idat : Array[Byte] = []
for i in 0..<chunks.length() {
if chunks[i].chunk_type == "IDAT" {
for j in 0..<chunks[i].data_size {
idat.push(png_data[chunks[i].data_offset + j])
}
}
}
Bytes::from_array(idat)
}
///|
/// 从 PNG 字节流中提取 IHDR 数据
fn extract_ihdr_from_png(png_data : Bytes) -> Bytes raise @types.LoadError {
let chunks = apng_parse_chunks(png_data)
for i in 0..<chunks.length() {
if chunks[i].chunk_type == "IHDR" {
return apng_extract_chunk_data(png_data, chunks[i])
}
}
raise @types.LoadError::DecodeFailed("PNG 缺少 IHDR")
}
///|
/// 写入 PNG chunk(type + data + CRC)
fn apng_write_chunk(
out : Array[Byte],
chunk_type : String,
chunk_data : Bytes,
) -> Unit {
write_u32_be(out, chunk_data.length())
let type_bytes = @encoding/utf8.encode(chunk_type)
for i in 0..<4 {
out.push(type_bytes[i])
}
for i in 0..<chunk_data.length() {
out.push(chunk_data[i])
}
// CRC
let crc_data : Array[Byte] = []
for i in 0..<4 {
crc_data.push(type_bytes[i])
}
for i in 0..<chunk_data.length() {
crc_data.push(chunk_data[i])
}
let crc = @util.crc32(Bytes::from_array(crc_data))
write_u32_be(out, crc)
}
///|
/// 将 PngAnimation 编码为 APNG 字节流
/// 所有帧必须尺寸相同
pub fn encode_apng_pure(
anim : @types.PngAnimation,
) -> Bytes raise @types.LoadError {
let num_frames = anim.frames.length()
if num_frames == 0 {
raise @types.LoadError::EncodeFailed("APNG 至少需要一帧")
}
let first = anim.frames[0]
let width = first.width
let height = first.height
let out : Array[Byte] = []
// PNG signature
out.push(b'\x89')
out.push(b'\x50')
out.push(b'\x4E')
out.push(b'\x47')
out.push(b'\x0D')
out.push(b'\x0A')
out.push(b'\x1A')
out.push(b'\x0A')
// 编码第一帧为 PNG,提取 IHDR 和 IDAT
let first_png = encode_png_pure(first)
let ihdr_data = extract_ihdr_from_png(first_png)
// IHDR chunk
apng_write_chunk(out, "IHDR", ihdr_data)
// acTL chunk: num_frames + num_plays(0=无限)
let actl : Array[Byte] = []
write_u32_be(actl, num_frames)
write_u32_be(actl, 0)
apng_write_chunk(out, "acTL", Bytes::from_array(actl))
// 序列号计数器
let mut seq = 0
// 第一帧 fcTL
let fctl1 : Array[Byte] = []
write_u32_be(fctl1, seq) // sequence_number
seq += 1
write_u32_be(fctl1, width) // width
write_u32_be(fctl1, height) // height
write_u32_be(fctl1, 0) // x_offset
write_u32_be(fctl1, 0) // y_offset
let delay1 = if anim.delays.length() > 0 { anim.delays[0] } else { 0 }
write_u16_be(fctl1, delay1) // delay_num (ms)
write_u16_be(fctl1, 1000) // delay_den
fctl1.push(b'\x01') // dispose_op = APNG_DISPOSE_OP_BACKGROUND
fctl1.push(b'\x00') // blend_op = APNG_BLEND_OP_SOURCE
apng_write_chunk(out, "fcTL", Bytes::from_array(fctl1))
// 第一帧 IDAT
let first_idat = extract_idat_from_png(first_png)
apng_write_chunk(out, "IDAT", first_idat)
// 后续帧
for i in 1..<num_frames {
let frame = anim.frames[i]
let frame_png = encode_png_pure(frame)
let frame_idat = extract_idat_from_png(frame_png)
// fcTL
let fctl : Array[Byte] = []
write_u32_be(fctl, seq) // sequence_number
seq += 1
write_u32_be(fctl, frame.width)
write_u32_be(fctl, frame.height)
write_u32_be(fctl, 0) // x_offset
write_u32_be(fctl, 0) // y_offset
let delay = if i < anim.delays.length() { anim.delays[i] } else { 0 }
write_u16_be(fctl, delay) // delay_num (ms)
write_u16_be(fctl, 1000) // delay_den
fctl.push(b'\x01') // dispose_op
fctl.push(b'\x00') // blend_op
apng_write_chunk(out, "fcTL", Bytes::from_array(fctl))
// fdAT: seq_num + IDAT data
let fdat : Array[Byte] = []
write_u32_be(fdat, seq) // sequence_number
seq += 1
for j in 0..<frame_idat.length() {
fdat.push(frame_idat[j])
}
apng_write_chunk(out, "fdAT", Bytes::from_array(fdat))
}
// IEND
apng_write_chunk(out, "IEND", Bytes::from_array([]))
Bytes::from_array(out)
}