gifenc: improve no-compression mode by keeping code lengths the same
[nihav.git] / nihav-commonfmt / src / codecs / gifenc.rs
CommitLineData
fc39649d
KS
1use nihav_core::codecs::*;
2use nihav_core::io::byteio::*;
3use nihav_core::io::bitwriter::*;
4
5#[derive(Clone,Copy,Default,PartialEq)]
6enum CompressionLevel {
7 None,
8 Fast,
9 #[default]
10 Best
11}
12
13impl std::string::ToString for CompressionLevel {
14 fn to_string(&self) -> String {
15 match *self {
16 CompressionLevel::None => "none".to_string(),
17 CompressionLevel::Fast => "fast".to_string(),
18 CompressionLevel::Best => "best".to_string(),
19 }
20 }
21}
22
23const NO_CODE: u16 = 0;
24
25struct LZWDictionary {
26 cur_size: usize,
27 bit_len: u8,
28 clear_code: u16,
29 end_code: u16,
30 orig_len: u8,
31 trie: Vec<[u16; 257]>,
32}
33
34impl LZWDictionary {
35 fn new() -> Self {
36 Self {
37 trie: Vec::with_capacity(4096),
38 cur_size: 0,
39 bit_len: 0,
40 clear_code: 0,
41 end_code: 0,
42 orig_len: 0,
43 }
44 }
45 fn init(&mut self, bits: u8) {
46 self.cur_size = (1 << bits) + 2;
47 self.bit_len = bits + 1;
48 self.clear_code = 1 << bits;
49 self.end_code = self.clear_code + 1;
50 self.orig_len = self.bit_len;
51
52 self.trie.clear();
53 for _ in 0..self.cur_size {
54 self.trie.push([NO_CODE; 257]);
55 }
56 for (idx, nodes) in self.trie.iter_mut().enumerate() {
57 nodes[256] = idx as u16;
58 }
59 }
60 fn find(&self, src: &[u8]) -> (u16, usize, usize) {
61 let mut idx = usize::from(src[0]);
62 let mut last_len = 0;
63 for (pos, &next) in src.iter().enumerate().skip(1) {
64 let next = usize::from(next);
65 if self.trie[idx][next] != NO_CODE {
66 idx = usize::from(self.trie[idx][next]);
67 } else {
68 return (self.trie[idx][256], pos, idx);
69 }
70 last_len = pos;
71 }
72 (self.trie[idx][256], last_len + 1, idx)
73 }
74 fn add(&mut self, lastidx: usize, next: u8) {
75 if self.cur_size >= (1 << 12) {
76 return;
77 }
78 let next = usize::from(next);
79 if self.trie[lastidx][next] == NO_CODE {
80 let newnode = self.trie.len();
81 self.trie.push([NO_CODE; 257]);
82 self.trie[newnode][256] = self.cur_size as u16;
83 self.trie[lastidx][next] = newnode as u16;
84 }
85 if (self.cur_size & (self.cur_size - 1)) == 0 && self.bit_len < 12 {
86 self.bit_len += 1;
87 }
88 self.cur_size += 1;
89 }
90 fn reset(&mut self) {
91 self.bit_len = self.orig_len;
92 self.cur_size = usize::from(self.end_code) + 1;
93 self.trie.truncate(self.cur_size);
94 for nodes in self.trie.iter_mut() {
95 for el in nodes[..256].iter_mut() {
96 *el = NO_CODE;
97 }
98 }
99 }
100}
101
102struct LZWEncoder {
103 dict: LZWDictionary,
104 level: CompressionLevel,
105 tmp: Vec<u8>,
106}
107
108impl LZWEncoder {
109 fn new() -> Self {
110 Self {
111 dict: LZWDictionary::new(),
112 level: CompressionLevel::default(),
113 tmp: Vec::new(),
114 }
115 }
116 fn compress(&mut self, writer: &mut ByteWriter, src: &[u8]) -> EncoderResult<()> {
117 let clr_bits: u8 = if self.level != CompressionLevel::None {
118 let maxclr = u16::from(src.iter().fold(0u8, |acc, &a| acc.max(a))) + 1;
119 let mut bits = 2;
120 while (1 << bits) < maxclr {
121 bits += 1;
122 }
123 bits
124 } else { 8 };
125
126 self.dict.init(clr_bits);
127
128 self.tmp.clear();
129 let mut tbuf = Vec::new();
130 std::mem::swap(&mut tbuf, &mut self.tmp);
131 let mut bw = BitWriter::new(tbuf, BitWriterMode::LE);
132
133 bw.write(u32::from(self.dict.clear_code), self.dict.bit_len);
134
135 match self.level {
136 CompressionLevel::None => {
8fd97a64 137 let sym_limit = 1 << (clr_bits + 1);
fc39649d 138 for &b in src.iter() {
8fd97a64
KS
139 if self.dict.cur_size >= sym_limit {
140 bw.write(u32::from(self.dict.clear_code), self.dict.bit_len);
141 self.dict.reset();
142 }
fc39649d
KS
143 bw.write(u32::from(b), self.dict.bit_len);
144 self.dict.add(usize::from(b), 0);
145 }
146 },
147 CompressionLevel::Fast => {
148 let mut pos = 0;
149 while pos < src.len() {
150 let (idx, len, trieidx) = self.dict.find(&src[pos..]);
151 bw.write(u32::from(idx), self.dict.bit_len);
152 pos += len;
153 if pos < src.len() {
154 self.dict.add(trieidx, src[pos]);
155 }
156 if self.dict.cur_size == 4096 {
157 bw.write(u32::from(self.dict.clear_code), self.dict.bit_len);
158 self.dict.reset();
159 }
160 }
161 },
162 CompressionLevel::Best => {
163 let mut pos = 0;
164 let mut hist = [0; 16];
165 let mut avg = 0;
166 let mut avg1 = 0;
167 let mut hpos = 0;
168 while pos < src.len() {
169 let (idx, len, trieidx) = self.dict.find(&src[pos..]);
170 bw.write(u32::from(idx), self.dict.bit_len);
171 pos += len;
172 if pos >= src.len() {
173 break;
174 }
175 self.dict.add(trieidx, src[pos]);
176
177 avg1 -= hist[(hpos + 1) & 0xF];
178 avg1 += len;
179 if self.dict.cur_size == 4096 && (avg1 < avg - avg / 8) {
180 bw.write(u32::from(self.dict.clear_code), self.dict.bit_len);
181 self.dict.reset();
182 }
183 avg = avg1;
184 hpos = (hpos + 1) & 0xF;
185 hist[hpos] = len;
186 }
187 },
188 };
189
190 bw.write(u32::from(self.dict.end_code), self.dict.bit_len);
191 tbuf = bw.end();
192 std::mem::swap(&mut tbuf, &mut self.tmp);
193
194 writer.write_byte(clr_bits)?;
195 for chunk in self.tmp.chunks(255) {
196 writer.write_byte(chunk.len() as u8)?;
197 writer.write_buf(chunk)?;
198 }
199 writer.write_byte(0x00)?; // data end marker
200 Ok(())
201 }
202}
203
204struct GIFEncoder {
205 stream: Option<NAStreamRef>,
206 cur_frm: Vec<u8>,
207 prev_frm: Vec<u8>,
208 tmp_buf: Vec<u8>,
209 pal: [u8; 768],
210 pkt: Option<NAPacket>,
211 first: bool,
212 width: usize,
213 height: usize,
214 lzw: LZWEncoder,
215 p_trans: bool,
216 tr_idx: Option<u8>,
217}
218
219impl GIFEncoder {
220 fn new() -> Self {
221 Self {
222 stream: None,
223 pkt: None,
224 cur_frm: Vec::new(),
225 prev_frm: Vec::new(),
226 pal: [0; 768],
227 tmp_buf: Vec::new(),
228 first: true,
229 width: 0,
230 height: 0,
231 lzw: LZWEncoder::new(),
232 p_trans: false,
233 tr_idx: None,
234 }
235 }
236 fn write_dummy_frame(&mut self, bw: &mut ByteWriter) -> EncoderResult<()> {
237 let mut pix = [self.cur_frm[0]];
238 if let (true, Some(tr_idx)) = (self.p_trans, self.tr_idx) {
239 if tr_idx < pix[0] {
240 pix[0] = tr_idx;
241 }
242 }
243
244 // 1x1 image descriptor
245 bw.write_buf(&[0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00])?;
246 self.lzw.compress(bw, &pix)?;
247 Ok(())
248 }
249}
250
251impl NAEncoder for GIFEncoder {
252 fn negotiate_format(&self, encinfo: &EncodeParameters) -> EncoderResult<EncodeParameters> {
253 match encinfo.format {
254 NACodecTypeInfo::None => {
255 Ok(EncodeParameters {
256 format: NACodecTypeInfo::Video(NAVideoInfo::new(0, 0, true, YUV420_FORMAT)),
257 ..Default::default()
258 })
259 },
260 NACodecTypeInfo::Audio(_) => Err(EncoderError::FormatError),
261 NACodecTypeInfo::Video(vinfo) => {
262 let outinfo = NAVideoInfo::new(vinfo.width, vinfo.height, false, PAL8_FORMAT);
263 let mut ofmt = *encinfo;
264 ofmt.format = NACodecTypeInfo::Video(outinfo);
265 Ok(ofmt)
266 }
267 }
268 }
269 fn get_capabilities(&self) -> u64 { ENC_CAPS_SKIPFRAME }
270 fn init(&mut self, stream_id: u32, encinfo: EncodeParameters) -> EncoderResult<NAStreamRef> {
271 match encinfo.format {
272 NACodecTypeInfo::None => Err(EncoderError::FormatError),
273 NACodecTypeInfo::Audio(_) => Err(EncoderError::FormatError),
274 NACodecTypeInfo::Video(vinfo) => {
275 if vinfo.width > 65535 || vinfo.height > 65535 {
276 return Err(EncoderError::FormatError);
277 }
278 self.width = vinfo.width;
279 self.height = vinfo.height;
280
281 let edata = self.tr_idx.map(|val| vec![val]);
282
283 let out_info = NAVideoInfo::new(vinfo.width, vinfo.height, false, PAL8_FORMAT);
284 let info = NACodecInfo::new("gif", NACodecTypeInfo::Video(out_info), edata);
285 let mut stream = NAStream::new(StreamType::Video, stream_id, info, encinfo.tb_num, encinfo.tb_den, 0);
286 stream.set_num(stream_id as usize);
287 let stream = stream.into_ref();
288
289 self.stream = Some(stream.clone());
290
291 self.cur_frm = vec![0; vinfo.width * vinfo.height];
292 self.prev_frm = vec![0; vinfo.width * vinfo.height];
293 self.tmp_buf.clear();
294 self.tmp_buf.reserve(vinfo.width * vinfo.height);
295
296 self.first = true;
297
298 Ok(stream)
299 },
300 }
301 }
302 fn encode(&mut self, frm: &NAFrame) -> EncoderResult<()> {
303 let mut dbuf = Vec::with_capacity(4);
304 let mut gw = GrowableMemoryWriter::new_write(&mut dbuf);
305 let mut bw = ByteWriter::new(&mut gw);
306
307 self.tmp_buf.clear();
308
309 match frm.get_buffer() {
310 NABufferType::Video(ref buf) => {
311 let src = buf.get_data();
312 let stride = buf.get_stride(0);
313 let src = &src[buf.get_offset(0)..];
314
315 for (dline, sline) in self.cur_frm.chunks_exact_mut(self.width)
316 .zip(src.chunks_exact(stride)) {
317 dline.copy_from_slice(&sline[..self.width]);
318 }
319
320 let cur_pal = &src[buf.get_offset(1)..][..768];
321 if self.first {
322 self.pal.copy_from_slice(cur_pal);
323 }
324
325 let mut pal_changed = false;
326 if !self.first {
327 let mut used = [false; 256];
328 for &b in self.cur_frm.iter() {
329 used[usize::from(b)] = true;
330 }
331 for (&used, (pal1, pal2)) in used.iter()
332 .zip(self.pal.chunks_exact(3).zip(cur_pal.chunks_exact(3))) {
333 if used && (pal1 != pal2) {
334 pal_changed = true;
335 break;
336 }
337 }
338 }
339
340 if self.first {
341 bw.write_byte(0x2C)?; // image descriptor
342 bw.write_u16le(0)?; // left
343 bw.write_u16le(0)?; // top
344 bw.write_u16le(self.width as u16)?;
345 bw.write_u16le(self.height as u16)?;
346 bw.write_byte(0)?; // flags
347 self.lzw.compress(&mut bw, &self.cur_frm)?;
348 } else {
349 let mut top = 0;
350 for (y, (line1, line2)) in self.cur_frm.chunks_exact(self.width)
351 .zip(self.prev_frm.chunks_exact(self.width)).enumerate() {
352 if line1 == line2 {
353 top = y;
354 } else {
355 break;
356 }
357 }
358 if top != self.height - 1 {
359 let mut bot = self.height;
360 for (y, (line1, line2)) in self.cur_frm.chunks_exact(self.width)
361 .zip(self.prev_frm.chunks_exact(self.width)).enumerate().rev() {
362 if line1 == line2 {
363 bot = y + 1;
364 } else {
365 break;
366 }
367 }
368 let mut left = self.width - 1;
369 let mut right = 0;
370 for (line1, line2) in self.cur_frm.chunks_exact(self.width)
371 .zip(self.prev_frm.chunks_exact(self.width))
372 .skip(top).take(bot - top) {
373 if left > 0 {
374 let mut cur_l = 0;
375 for (x, (&p1, &p2)) in line1.iter().zip(line2.iter()).enumerate() {
376 if p1 == p2 {
377 cur_l = x + 1;
378 } else {
379 break;
380 }
381 }
382 left = left.min(cur_l);
383 }
384 if right < self.width {
385 let mut cur_r = self.width;
386 for (x, (&p1, &p2)) in line1.iter().zip(line2.iter())
387 .enumerate().rev() {
388 if p1 == p2 {
389 cur_r = x + 1;
390 } else {
391 break;
392 }
393 }
394 right = right.max(cur_r);
395 }
396 }
397 self.tmp_buf.clear();
398 let use_transparency = self.p_trans && self.tr_idx.is_some();
399 let full_frame = right == 0 && top == 0 && left == self.width && bot == self.height;
400
401 let pic = match (use_transparency, full_frame) {
402 (true, _) => {
403 let tr_idx = self.tr_idx.unwrap_or(0);
404 for (cline, pline) in self.cur_frm.chunks_exact(self.width)
405 .zip(self.prev_frm.chunks_exact(self.width))
406 .skip(top).take(bot - top) {
407 for (&cpix, &ppix) in cline[left..right].iter()
408 .zip(pline[left..right].iter()) {
409 self.tmp_buf.push(if cpix == ppix { tr_idx } else { cpix });
410 }
411 }
412 &self.tmp_buf
413 },
414 (false, true) => {
415 &self.cur_frm
416 },
417 (false, false) => {
418 for line in self.cur_frm.chunks_exact(self.width)
419 .skip(top).take(bot - top) {
420 self.tmp_buf.extend_from_slice(&line[left..right]);
421 }
422 &self.tmp_buf
423 },
424 };
425
426 bw.write_byte(0x2C)?; // image descriptor
427 bw.write_u16le(left as u16)?;
428 bw.write_u16le(top as u16)?;
429 bw.write_u16le((right - left) as u16)?;
430 bw.write_u16le((bot - top) as u16)?;
431 if !pal_changed {
432 bw.write_byte(0)?; // flags
433 } else {
434 let maxclr = pic.iter().fold(0u8, |acc, &a| acc.max(a));
435 let clr_bits = if maxclr > 128 {
436 8
437 } else {
438 let mut bits = 1;
439 while (1 << bits) < maxclr {
440 bits += 1;
441 }
442 bits
443 };
444 bw.write_byte(0x80 | (clr_bits - 1))?;
445 bw.write_buf(&cur_pal[..(3 << clr_bits)])?;
446 }
447 self.lzw.compress(&mut bw, pic)?;
448 } else {
449 self.write_dummy_frame(&mut bw)?;
450 }
451 }
452 },
453 NABufferType::None if !self.first => {
454 self.write_dummy_frame(&mut bw)?;
455 },
456 _ => return Err(EncoderError::InvalidParameters),
457 };
458
459 self.pkt = Some(NAPacket::new(self.stream.clone().unwrap(), frm.ts, self.first, dbuf));
460 self.first = false;
461
462 if let NABufferType::Video(ref buf) = frm.get_buffer() {
463 let paloff = buf.get_offset(1);
464 let data = buf.get_data();
465 let mut pal = [0; 1024];
466 let srcpal = &data[paloff..][..768];
467 for (dclr, sclr) in pal.chunks_exact_mut(4).zip(srcpal.chunks_exact(3)) {
468 dclr[..3].copy_from_slice(sclr);
469 }
470 if let Some(ref mut pkt) = &mut self.pkt {
471 pkt.side_data.push(NASideData::Palette(true, Arc::new(pal)));
472 }
473 }
474
475 std::mem::swap(&mut self.cur_frm, &mut self.prev_frm);
476 Ok(())
477 }
478 fn get_packet(&mut self) -> EncoderResult<Option<NAPacket>> {
479 let mut npkt = None;
480 std::mem::swap(&mut self.pkt, &mut npkt);
481 Ok(npkt)
482 }
483 fn flush(&mut self) -> EncoderResult<()> {
484 Ok(())
485 }
486}
487
488const ENCODER_OPTS: &[NAOptionDefinition] = &[
489 NAOptionDefinition {
490 name: "compr", description: "Compression level",
491 opt_type: NAOptionDefinitionType::String(Some(&["none", "fast", "best"])) },
492 NAOptionDefinition {
493 name: "inter_transparent", description: "Code changed regions with transparency",
494 opt_type: NAOptionDefinitionType::Bool },
495 NAOptionDefinition {
496 name: "transparent_idx", description: "Palette index to use for transparency (on inter frames too if requested)",
497 opt_type: NAOptionDefinitionType::Int(Some(-1), Some(255)) },
498];
499
500impl NAOptionHandler for GIFEncoder {
501 fn get_supported_options(&self) -> &[NAOptionDefinition] { ENCODER_OPTS }
502 fn set_options(&mut self, options: &[NAOption]) {
503 for option in options.iter() {
504 for opt_def in ENCODER_OPTS.iter() {
505 if opt_def.check(option).is_ok() {
506 match option.name {
507 "compr" => {
508 if let NAValue::String(ref strval) = option.value {
509 match strval.as_str() {
510 "none" => self.lzw.level = CompressionLevel::None,
511 "fast" => self.lzw.level = CompressionLevel::Fast,
512 "best" => self.lzw.level = CompressionLevel::Best,
513 _ => {},
514 };
515 }
516 },
517 "inter_transparent" => {
518 if let NAValue::Bool(bval) = option.value {
519 self.p_trans = bval;
520 }
521 },
522 "transparent_idx" => {
523 if let NAValue::Int(ival) = option.value {
524 self.tr_idx = if ival >= 0 { Some(ival as u8) } else { None };
525 }
526 },
527 _ => {},
528 };
529 }
530 }
531 }
532 }
533 fn query_option_value(&self, name: &str) -> Option<NAValue> {
534 match name {
535 "compr" => Some(NAValue::String(self.lzw.level.to_string())),
536 "inter_transparent" => Some(NAValue::Bool(self.p_trans)),
537 "transparent_idx" => Some(NAValue::Int(self.tr_idx.map_or(-1i64, i64::from))),
538 _ => None,
539 }
540 }
541}
542
543pub fn get_encoder() -> Box<dyn NAEncoder + Send> {
544 Box::new(GIFEncoder::new())
545}
546
547#[cfg(test)]
548mod test {
549 use nihav_core::codecs::*;
550 use nihav_core::demuxers::*;
551 use nihav_core::muxers::*;
552 use crate::*;
553 use nihav_codec_support::test::enc_video::*;
554
555 // sample: https://samples.mplayerhq.hu/V-codecs/Uncompressed/8bpp.avi
556 fn test_gif_encoder_single(out_name: &'static str, enc_options: &[NAOption], hash: &[u32; 4]) {
557 let mut dmx_reg = RegisteredDemuxers::new();
558 generic_register_all_demuxers(&mut dmx_reg);
559 let mut dec_reg = RegisteredDecoders::new();
560 generic_register_all_decoders(&mut dec_reg);
561 let mut mux_reg = RegisteredMuxers::new();
562 generic_register_all_muxers(&mut mux_reg);
563 let mut enc_reg = RegisteredEncoders::new();
564 generic_register_all_encoders(&mut enc_reg);
565
566 let dec_config = DecoderTestParams {
567 demuxer: "avi",
568 in_name: "assets/Misc/8bpp.avi",
569 stream_type: StreamType::Video,
570 limit: Some(0),
571 dmx_reg, dec_reg,
572 };
573 let enc_config = EncoderTestParams {
574 muxer: "gif",
575 enc_name: "gif",
576 out_name,
577 mux_reg, enc_reg,
578 };
579 let dst_vinfo = NAVideoInfo {
580 width: 0,
581 height: 0,
582 format: PAL8_FORMAT,
583 flipped: false,
584 bits: 8,
585 };
586 let enc_params = EncodeParameters {
587 format: NACodecTypeInfo::Video(dst_vinfo),
588 quality: 0,
589 bitrate: 0,
590 tb_num: 0,
591 tb_den: 0,
592 flags: 0,
593 };
594 //test_encoding_to_file(&dec_config, &enc_config, enc_params, enc_options);
595 test_encoding_md5(&dec_config, &enc_config, enc_params, enc_options, hash);
596 }
597 // sample: https://samples.mplayerhq.hu/image-samples/GIF/3D.gif
598 fn test_gif_anim(out_name: &'static str, enc_options: &[NAOption], hash: &[u32; 4]) {
599 let mut dmx_reg = RegisteredDemuxers::new();
600 generic_register_all_demuxers(&mut dmx_reg);
601 let mut dec_reg = RegisteredDecoders::new();
602 generic_register_all_decoders(&mut dec_reg);
603 let mut mux_reg = RegisteredMuxers::new();
604 generic_register_all_muxers(&mut mux_reg);
605 let mut enc_reg = RegisteredEncoders::new();
606 generic_register_all_encoders(&mut enc_reg);
607
608 let dec_config = DecoderTestParams {
609 demuxer: "gif",
610 in_name: "assets/Misc/3D.gif",
611 stream_type: StreamType::Video,
612 limit: None,
613 dmx_reg, dec_reg,
614 };
615 let enc_config = EncoderTestParams {
616 muxer: "gif",
617 enc_name: "gif",
618 out_name,
619 mux_reg, enc_reg,
620 };
621 let dst_vinfo = NAVideoInfo {
622 width: 0,
623 height: 0,
624 format: PAL8_FORMAT,
625 flipped: false,
626 bits: 8,
627 };
628 let enc_params = EncodeParameters {
629 format: NACodecTypeInfo::Video(dst_vinfo),
630 quality: 0,
631 bitrate: 0,
632 tb_num: 0,
633 tb_den: 0,
634 flags: 0,
635 };
636 //test_encoding_to_file(&dec_config, &enc_config, enc_params, enc_options);
637 test_encoding_md5(&dec_config, &enc_config, enc_params, enc_options, hash);
638 }
639 #[test]
640 fn test_gif_single_none() {
641 let enc_options = &[
642 NAOption { name: "compr", value: NAValue::String("none".to_string()) },
643 ];
8fd97a64 644 test_gif_encoder_single("none.gif", enc_options, &[0x32900cff, 0xef979bb0, 0x2d0355e8, 0x424bddee]);
fc39649d
KS
645 }
646 #[test]
647 fn test_gif_single_fast() {
648 let enc_options = &[
649 NAOption { name: "compr", value: NAValue::String("fast".to_string()) },
650 ];
651 test_gif_encoder_single("fast.gif", enc_options, &[0x9644f682, 0x497593cd, 0xdabb483d, 0x8fce63f4]);
652 }
653 #[test]
654 fn test_gif_single_best() {
655 let enc_options = &[
656 NAOption { name: "compr", value: NAValue::String("best".to_string()) },
657 ];
658 test_gif_encoder_single("best.gif", enc_options, &[0x9644f682, 0x497593cd, 0xdabb483d, 0x8fce63f4]);
659 }
660 #[test]
661 fn test_gif_anim_opaque() {
662 let enc_options = &[
663 NAOption { name: "compr", value: NAValue::String("fast".to_string()) },
664 ];
665 test_gif_anim("anim-opaque.gif", enc_options, &[0x58489e31, 0x1721d75e, 0xaebf93f2, 0x3fea9c6e]);
666 }
667 #[test]
668 fn test_gif_anim_transparent() {
669 let enc_options = &[
670 NAOption { name: "compr", value: NAValue::String("fast".to_string()) },
671 NAOption { name: "inter_transparent", value: NAValue::Bool(true) },
672 NAOption { name: "transparent_idx", value: NAValue::Int(0x7F) },
673 ];
674 test_gif_anim("anim-transp.gif", enc_options, &[0x62df6232, 0x0c334457, 0x73738404, 0xa8829dcc]);
675 }
676}