Skip to main content

zenavif_serialize/
grid.rs

1//! Grid (tiled) AVIF image serialization.
2//!
3//! Writes an AVIF file where the primary item is an ImageGrid descriptor
4//! with `dimg` references to individual AV1 tile items.
5
6use crate::boxes::*;
7use arrayvec::ArrayVec;
8use std::io;
9
10/// Builder for grid (tiled) AVIF container serialization.
11///
12/// Holds codec configuration and optional metadata. Call [`serialize`](GridImage::serialize)
13/// with per-encode data (layout, dimensions, tile data) to produce the AVIF file.
14pub struct GridImage {
15    color_config: Av1CBox,
16    alpha_config: Option<Av1CBox>,
17    depth_bits: u8,
18    colr: Option<ColrBox>,
19    premultiplied_alpha: bool,
20}
21
22impl Default for GridImage {
23    fn default() -> Self { Self::new() }
24}
25
26impl GridImage {
27    /// Create with sensible defaults (8-bit 4:2:0, no alpha, no colr).
28    pub fn new() -> Self {
29        Self {
30            color_config: Av1CBox::default(),
31            alpha_config: None,
32            depth_bits: 8,
33            colr: None,
34            premultiplied_alpha: false,
35        }
36    }
37
38    /// AV1 codec configuration for color tiles.
39    pub fn set_color_config(&mut self, config: Av1CBox) -> &mut Self { self.color_config = config; self }
40    /// AV1 codec configuration for alpha tiles.
41    pub fn set_alpha_config(&mut self, config: Av1CBox) -> &mut Self { self.alpha_config = Some(config); self }
42    /// Bit depth (8, 10, or 12). Default: 8.
43    pub fn set_depth_bits(&mut self, depth: u8) -> &mut Self { self.depth_bits = depth; self }
44    /// CICP color info (nclx).
45    pub fn set_colr(&mut self, colr: ColrBox) -> &mut Self { self.colr = Some(colr); self }
46    /// Whether alpha is premultiplied. Default: false.
47    pub fn set_premultiplied_alpha(&mut self, premultiplied: bool) -> &mut Self { self.premultiplied_alpha = premultiplied; self }
48
49    /// Serialize a grid AVIF image.
50    ///
51    /// - `rows`, `columns`: tile grid layout (1-256 each)
52    /// - `output_width`, `output_height`: final image dimensions
53    /// - `tile_width`, `tile_height`: dimensions of each tile
54    /// - `tile_data`: AV1-encoded data for each tile in row-major order (length must equal `rows * columns`)
55    /// - `alpha_data`: optional alpha tile data (same order and count as `tile_data`)
56    #[allow(clippy::too_many_arguments)]
57    pub fn serialize(&self, rows: u8, columns: u8,
58                     output_width: u32, output_height: u32,
59                     tile_width: u32, tile_height: u32,
60                     tile_data: &[&[u8]], alpha_data: Option<&[&[u8]]>) -> io::Result<Vec<u8>> {
61    let image = self;
62    let tile_count = rows as usize * columns as usize;
63    if tile_data.len() != tile_count {
64        return Err(io::Error::new(io::ErrorKind::InvalidInput,
65            format!("tile_data.len() ({}) != rows*columns ({})", tile_data.len(), tile_count)));
66    }
67    if let Some(alpha) = alpha_data
68        && alpha.len() != tile_count {
69            return Err(io::Error::new(io::ErrorKind::InvalidInput,
70                format!("alpha_data.len() ({}) != rows*columns ({})", alpha.len(), tile_count)));
71        }
72
73    let has_alpha = alpha_data.is_some() && image.alpha_config.is_some();
74
75    // Item IDs:
76    // 1 = color grid item
77    // 2 = alpha grid item (if has_alpha)
78    // 3..3+N = color tile items
79    // 3+N..3+2N = alpha tile items (if has_alpha)
80    let color_grid_id: u16 = 1;
81    let alpha_grid_id: u16 = 2;
82    let color_tile_base: u16 = if has_alpha { 3 } else { 2 };
83    let alpha_tile_base: u16 = color_tile_base + tile_count as u16;
84
85    // Build the ImageGrid descriptor (item data for the grid item)
86    let grid_descriptor = make_grid_descriptor(
87        rows, columns,
88        output_width, output_height,
89    );
90
91    let alpha_grid_descriptor = if has_alpha {
92        Some(make_grid_descriptor(
93            rows, columns,
94            output_width, output_height,
95        ))
96    } else {
97        None
98    };
99
100    // Build box structures
101    let mut image_items: Vec<InfeBox> = Vec::new();
102    let mut ipma_entries: Vec<IpmaEntry> = Vec::new();
103    let mut irefs: Vec<IrefEntryBox> = Vec::new();
104    let mut ipco = IpcoBox::new();
105    const ESSENTIAL_BIT: u8 = 0x80;
106
107    // Shared properties
108    let ispe_output = ipco.push(IpcoProp::Ispe(IspeBox {
109        width: output_width,
110        height: output_height,
111    })).ok_or(io::ErrorKind::InvalidInput)?;
112
113    let ispe_tile = ipco.push(IpcoProp::Ispe(IspeBox {
114        width: tile_width,
115        height: tile_height,
116    })).ok_or(io::ErrorKind::InvalidInput)?;
117
118    let av1c_color = ipco.push(IpcoProp::Av1C(image.color_config)).ok_or(io::ErrorKind::InvalidInput)?;
119
120    let pixi_color = ipco.push(IpcoProp::Pixi(PixiBox {
121        channels: if image.color_config.monochrome { 1 } else { 3 },
122        depth: image.depth_bits,
123    })).ok_or(io::ErrorKind::InvalidInput)?;
124
125    // Optional colr
126    let colr_prop = if let Some(ref colr) = image.colr {
127        if *colr != ColrBox::default() {
128            Some(ipco.push(IpcoProp::Colr(*colr)).ok_or(io::ErrorKind::InvalidInput)?)
129        } else {
130            None
131        }
132    } else {
133        None
134    };
135
136    // Alpha AV1C + pixi + auxC (if applicable)
137    let (av1c_alpha, pixi_alpha, auxc_alpha) = if has_alpha {
138        let ac = ipco.push(IpcoProp::Av1C(*image.alpha_config.as_ref().unwrap())).ok_or(io::ErrorKind::InvalidInput)?;
139        let pa = ipco.push(IpcoProp::Pixi(PixiBox {
140            channels: 1,
141            depth: image.depth_bits,
142        })).ok_or(io::ErrorKind::InvalidInput)?;
143        let auxc = ipco.push(IpcoProp::AuxC(AuxCBox {
144            urn: "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha",
145        })).ok_or(io::ErrorKind::InvalidInput)?;
146        (Some(ac), Some(pa), Some(auxc))
147    } else {
148        (None, None, None)
149    };
150
151    // --- Color grid item ---
152    image_items.push(InfeBox {
153        id: color_grid_id,
154        typ: FourCC(*b"grid"),
155        name: "",
156        content_type: "",
157    });
158
159    // Grid item's ipma: ispe (output), pixi, and optionally colr
160    let mut grid_ipma = IpmaEntry {
161        item_id: color_grid_id,
162        prop_ids: {
163            let mut v = ArrayVec::new();
164            v.push(ispe_output);
165            v.push(pixi_color);
166            if let Some(colr_p) = colr_prop {
167                v.push(colr_p);
168            }
169            v
170        },
171    };
172    let _ = &mut grid_ipma; // suppress clippy
173    ipma_entries.push(grid_ipma);
174
175    // --- Alpha grid item ---
176    if has_alpha {
177        image_items.push(InfeBox {
178            id: alpha_grid_id,
179            typ: FourCC(*b"grid"),
180            name: "",
181            content_type: "",
182        });
183
184        irefs.push(IrefEntryBox {
185            from_id: alpha_grid_id,
186            to_id: color_grid_id,
187            typ: FourCC(*b"auxl"),
188        });
189
190        if image.premultiplied_alpha {
191            irefs.push(IrefEntryBox {
192                from_id: color_grid_id,
193                to_id: alpha_grid_id,
194                typ: FourCC(*b"prem"),
195            });
196        }
197
198        ipma_entries.push(IpmaEntry {
199            item_id: alpha_grid_id,
200            prop_ids: {
201                let mut v = ArrayVec::new();
202                v.push(ispe_output);
203                v.push(pixi_alpha.unwrap());
204                v.push(auxc_alpha.unwrap());
205                v
206            },
207        });
208    }
209
210    // --- Color tile items ---
211    for i in 0..tile_count {
212        let tile_id = color_tile_base + i as u16;
213
214        image_items.push(InfeBox {
215            id: tile_id,
216            typ: FourCC(*b"av01"),
217            name: "",
218            content_type: "",
219        });
220
221        irefs.push(IrefEntryBox {
222            from_id: color_grid_id,
223            to_id: tile_id,
224            typ: FourCC(*b"dimg"),
225        });
226
227        ipma_entries.push(IpmaEntry {
228            item_id: tile_id,
229            prop_ids: {
230                let mut v = ArrayVec::new();
231                v.push(ispe_tile);
232                v.push(av1c_color | ESSENTIAL_BIT);
233                v
234            },
235        });
236    }
237
238    // --- Alpha tile items ---
239    if has_alpha {
240        for i in 0..tile_count {
241            let tile_id = alpha_tile_base + i as u16;
242
243            image_items.push(InfeBox {
244                id: tile_id,
245                typ: FourCC(*b"av01"),
246                name: "",
247                content_type: "",
248            });
249
250            irefs.push(IrefEntryBox {
251                from_id: alpha_grid_id,
252                to_id: tile_id,
253                typ: FourCC(*b"dimg"),
254            });
255
256            ipma_entries.push(IpmaEntry {
257                item_id: tile_id,
258                prop_ids: {
259                    let mut v = ArrayVec::new();
260                    v.push(ispe_tile);
261                    v.push(av1c_alpha.unwrap() | ESSENTIAL_BIT);
262                    v
263                },
264            });
265        }
266    }
267
268    // Now we need to use the animated.rs approach (begin_box/end_box) because the
269    // still-image AvifFile struct doesn't support variable item counts. We use the
270    // same pattern: write boxes with size placeholders, then patch offsets.
271
272    let mut out = Vec::new();
273
274    // ftyp
275    write_ftyp(&mut out);
276
277    // meta
278    write_meta_grid(
279        &mut out,
280        &image_items,
281        &ipma_entries,
282        &ipco,
283        &irefs,
284        color_grid_id,
285        &grid_descriptor,
286        alpha_grid_descriptor.as_deref(),
287        alpha_grid_id,
288        tile_data,
289        alpha_data,
290        color_tile_base,
291        alpha_tile_base,
292        tile_count,
293        has_alpha,
294    );
295
296    // mdat
297    let mdat_pos = begin_box(&mut out, b"mdat");
298    let mdat_data_start = out.len() as u32;
299
300    // First: grid descriptors, then tiles (color grid, alpha grid, color tiles, alpha tiles)
301    // Actually, the iloc offset tracking is done in write_meta_grid with placeholders.
302    // We need to write the actual data in iloc order.
303
304    // Grid descriptor data
305    out.extend_from_slice(&grid_descriptor);
306    if let Some(ref agd) = alpha_grid_descriptor {
307        out.extend_from_slice(agd);
308    }
309
310    // Color tile data
311    for tile in tile_data {
312        out.extend_from_slice(tile);
313    }
314
315    // Alpha tile data
316    if let Some(alpha) = alpha_data {
317        for tile in alpha {
318            out.extend_from_slice(tile);
319        }
320    }
321
322    end_box(&mut out, mdat_pos);
323
324    // Patch iloc offsets
325    patch_iloc_offsets(&mut out, mdat_data_start);
326
327    Ok(out)
328    }
329}
330
331// ─── Helpers ─────────────────────────────────────────────────────────
332
333const ILOC_PLACEHOLDER: u32 = 0xBAAD_F00D;
334
335fn make_grid_descriptor(rows: u8, columns: u8, width: u32, height: u32) -> Vec<u8> {
336    let mut desc = Vec::new();
337    desc.push(0); // version
338    if width > u16::MAX as u32 || height > u16::MAX as u32 {
339        desc.push(1); // flags: 32-bit fields
340    } else {
341        desc.push(0); // flags: 16-bit fields
342    }
343    desc.push(rows.saturating_sub(1)); // rows_minus_one
344    desc.push(columns.saturating_sub(1)); // columns_minus_one
345    if width > u16::MAX as u32 || height > u16::MAX as u32 {
346        desc.extend_from_slice(&width.to_be_bytes());
347        desc.extend_from_slice(&height.to_be_bytes());
348    } else {
349        desc.extend_from_slice(&(width as u16).to_be_bytes());
350        desc.extend_from_slice(&(height as u16).to_be_bytes());
351    }
352    desc
353}
354
355fn write_u16(out: &mut Vec<u8>, v: u16) {
356    out.extend_from_slice(&v.to_be_bytes());
357}
358
359fn write_u32(out: &mut Vec<u8>, v: u32) {
360    out.extend_from_slice(&v.to_be_bytes());
361}
362
363fn begin_box(out: &mut Vec<u8>, box_type: &[u8; 4]) -> usize {
364    let pos = out.len();
365    write_u32(out, 0);
366    out.extend_from_slice(box_type);
367    pos
368}
369
370fn end_box(out: &mut [u8], pos: usize) {
371    let size = (out.len() - pos) as u32;
372    out[pos..pos + 4].copy_from_slice(&size.to_be_bytes());
373}
374
375fn write_fullbox(out: &mut Vec<u8>, version: u8, flags: u32) {
376    out.push(version);
377    out.push((flags >> 16) as u8);
378    out.push((flags >> 8) as u8);
379    out.push(flags as u8);
380}
381
382fn write_ftyp(out: &mut Vec<u8>) {
383    let pos = begin_box(out, b"ftyp");
384    out.extend_from_slice(b"avif");
385    write_u32(out, 0);
386    out.extend_from_slice(b"avif");
387    out.extend_from_slice(b"mif1");
388    out.extend_from_slice(b"miaf");
389    end_box(out, pos);
390}
391
392#[allow(clippy::too_many_arguments)]
393fn write_meta_grid(
394    out: &mut Vec<u8>,
395    image_items: &[InfeBox],
396    ipma_entries: &[IpmaEntry],
397    ipco: &IpcoBox,
398    irefs: &[IrefEntryBox],
399    primary_id: u16,
400    grid_descriptor: &[u8],
401    alpha_grid_descriptor: Option<&[u8]>,
402    alpha_grid_id: u16,
403    tile_data: &[&[u8]],
404    alpha_data: Option<&[&[u8]]>,
405    color_tile_base: u16,
406    alpha_tile_base: u16,
407    tile_count: usize,
408    has_alpha: bool,
409) {
410    let meta_pos = begin_box(out, b"meta");
411    write_fullbox(out, 0, 0);
412
413    // hdlr
414    {
415        let pos = begin_box(out, b"hdlr");
416        write_fullbox(out, 0, 0);
417        write_u32(out, 0);
418        out.extend_from_slice(b"pict");
419        out.extend_from_slice(&[0u8; 12]);
420        out.push(0);
421        end_box(out, pos);
422    }
423
424    // pitm
425    {
426        let pos = begin_box(out, b"pitm");
427        write_fullbox(out, 0, 0);
428        write_u16(out, primary_id);
429        end_box(out, pos);
430    }
431
432    // iloc — uses placeholder offsets, patched after mdat
433    {
434        let pos = begin_box(out, b"iloc");
435        write_fullbox(out, 0, 0);
436        out.push(0x44); // offset_size=4, length_size=4
437        out.push(0x00);
438
439        // Count items: grid descriptors + tiles
440        let mut item_count: u16 = 1 + tile_count as u16; // color grid + color tiles
441        if has_alpha {
442            item_count += 1 + tile_count as u16; // alpha grid + alpha tiles
443        }
444        write_u16(out, item_count);
445
446        // Color grid item
447        write_u16(out, primary_id);
448        write_u16(out, 0); // data_reference_index
449        write_u16(out, 1); // extent_count
450        write_u32(out, ILOC_PLACEHOLDER);
451        write_u32(out, grid_descriptor.len() as u32);
452
453        // Alpha grid item
454        if has_alpha {
455            write_u16(out, alpha_grid_id);
456            write_u16(out, 0);
457            write_u16(out, 1);
458            write_u32(out, ILOC_PLACEHOLDER);
459            write_u32(out, alpha_grid_descriptor.map_or(0, |d| d.len() as u32));
460        }
461
462        // Color tile items
463        for (i, tile) in tile_data.iter().enumerate() {
464            write_u16(out, color_tile_base + i as u16);
465            write_u16(out, 0);
466            write_u16(out, 1);
467            write_u32(out, ILOC_PLACEHOLDER);
468            write_u32(out, tile.len() as u32);
469        }
470
471        // Alpha tile items
472        if let Some(alpha) = alpha_data {
473            for (i, tile) in alpha.iter().enumerate() {
474                write_u16(out, alpha_tile_base + i as u16);
475                write_u16(out, 0);
476                write_u16(out, 1);
477                write_u32(out, ILOC_PLACEHOLDER);
478                write_u32(out, tile.len() as u32);
479            }
480        }
481
482        end_box(out, pos);
483    }
484
485    // iinf
486    {
487        let iinf_pos = begin_box(out, b"iinf");
488        write_fullbox(out, 0, 0);
489        write_u16(out, image_items.len() as u16);
490
491        for item in image_items {
492            let infe_pos = begin_box(out, b"infe");
493            write_fullbox(out, 2, 0);
494            write_u16(out, item.id);
495            write_u16(out, 0); // protection_index
496            out.extend_from_slice(&item.typ.0);
497            out.push(0); // name (null-terminated)
498            if !item.content_type.is_empty() {
499                out.extend_from_slice(item.content_type.as_bytes());
500                out.push(0);
501            }
502            end_box(out, infe_pos);
503        }
504
505        end_box(out, iinf_pos);
506    }
507
508    // iref
509    if !irefs.is_empty() {
510        let iref_pos = begin_box(out, b"iref");
511        write_fullbox(out, 0, 0);
512        for entry in irefs {
513            let entry_pos = begin_box(out, &entry.typ.0);
514            write_u16(out, entry.from_id);
515            write_u16(out, 1); // reference_count
516            write_u16(out, entry.to_id);
517            end_box(out, entry_pos);
518        }
519        end_box(out, iref_pos);
520    }
521
522    // iprp (ipco + ipma)
523    {
524        let iprp_pos = begin_box(out, b"iprp");
525
526        // ipco — serialize using the MpegBox trait
527        {
528            let mut tmp = Vec::new();
529            let mut w = crate::writer::Writer::new(&mut tmp);
530            let _ = ipco.write(&mut w);
531            drop(w);
532            out.extend_from_slice(&tmp);
533        }
534
535        // ipma
536        {
537            let pos = begin_box(out, b"ipma");
538            write_fullbox(out, 0, 0);
539            write_u32(out, ipma_entries.len() as u32);
540            for entry in ipma_entries {
541                write_u16(out, entry.item_id);
542                out.push(entry.prop_ids.len() as u8);
543                for &p in &entry.prop_ids {
544                    out.push(p);
545                }
546            }
547            end_box(out, pos);
548        }
549
550        end_box(out, iprp_pos);
551    }
552
553    end_box(out, meta_pos);
554}
555
556/// Patch iloc placeholder offsets with actual mdat offsets.
557/// Items are laid out in iloc order: grid desc(s), then tiles.
558fn patch_iloc_offsets(out: &mut [u8], mdat_data_start: u32) {
559    let placeholder = ILOC_PLACEHOLDER.to_be_bytes();
560    let mut current_offset = mdat_data_start;
561    let mut i = 0;
562
563    while i + 4 <= out.len() {
564        if out[i..i + 4] == placeholder {
565            // Read the length that follows this offset (4 bytes after)
566            let len = if i + 8 <= out.len() {
567                u32::from_be_bytes([out[i + 4], out[i + 5], out[i + 6], out[i + 7]])
568            } else {
569                0
570            };
571
572            out[i..i + 4].copy_from_slice(&current_offset.to_be_bytes());
573            current_offset += len;
574            i += 8; // skip past offset + length
575        } else {
576            i += 1;
577        }
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584
585    fn basic_av1c() -> Av1CBox {
586        Av1CBox {
587            seq_profile: 0,
588            seq_level_idx_0: 4,
589            seq_tier_0: false,
590            high_bitdepth: false,
591            twelve_bit: false,
592            monochrome: false,
593            chroma_subsampling_x: true,
594            chroma_subsampling_y: true,
595            chroma_sample_position: 0,
596        }
597    }
598
599    fn mono_av1c() -> Av1CBox {
600        Av1CBox {
601            seq_profile: 0,
602            seq_level_idx_0: 4,
603            seq_tier_0: false,
604            high_bitdepth: false,
605            twelve_bit: false,
606            monochrome: true,
607            chroma_subsampling_x: true,
608            chroma_subsampling_y: true,
609            chroma_sample_position: 0,
610        }
611    }
612
613    #[test]
614    fn grid_2x2_roundtrip() {
615        let tiles: Vec<Vec<u8>> = (0..4).map(|i| vec![i as u8; 100]).collect();
616        let tile_refs: Vec<&[u8]> = tiles.iter().map(|t| t.as_slice()).collect();
617
618        let mut image = GridImage::new();
619        image.set_color_config(basic_av1c());
620
621        let avif = image.serialize(2, 2, 200, 200, 100, 100, &tile_refs, None).unwrap();
622
623        // Verify ftyp
624        assert_eq!(&avif[4..8], b"ftyp");
625        assert_eq!(&avif[8..12], b"avif");
626
627        // Verify tile data is present in mdat
628        for tile in &tiles {
629            assert!(avif.windows(tile.len()).any(|w| w == tile.as_slice()),
630                "tile data should be in output");
631        }
632
633        // Parse with zenavif-parse
634        let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
635        let grid = parser.grid_config().expect("should have grid config");
636        assert_eq!(grid.rows, 2);
637        assert_eq!(grid.columns, 2);
638        assert_eq!(grid.output_width, 200);
639        assert_eq!(grid.output_height, 200);
640        assert_eq!(parser.grid_tile_count(), 4);
641    }
642
643    #[test]
644    fn grid_1x3_roundtrip() {
645        let tiles: Vec<Vec<u8>> = (0..3).map(|i| vec![(i + 10) as u8; 50]).collect();
646        let tile_refs: Vec<&[u8]> = tiles.iter().map(|t| t.as_slice()).collect();
647
648        let mut image = GridImage::new();
649        image.set_color_config(basic_av1c());
650
651        let avif = image.serialize(1, 3, 300, 100, 100, 100, &tile_refs, None).unwrap();
652        let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
653        let grid = parser.grid_config().expect("grid config");
654        assert_eq!(grid.rows, 1);
655        assert_eq!(grid.columns, 3);
656        assert_eq!(parser.grid_tile_count(), 3);
657    }
658
659    #[test]
660    fn grid_with_alpha() {
661        let color_tiles: Vec<Vec<u8>> = (0..4).map(|i| vec![i as u8; 80]).collect();
662        let alpha_tiles: Vec<Vec<u8>> = (0..4).map(|i| vec![(i + 100) as u8; 40]).collect();
663        let color_refs: Vec<&[u8]> = color_tiles.iter().map(|t| t.as_slice()).collect();
664        let alpha_refs: Vec<&[u8]> = alpha_tiles.iter().map(|t| t.as_slice()).collect();
665
666        let mut image = GridImage::new();
667        image.set_color_config(basic_av1c());
668        image.set_alpha_config(mono_av1c());
669
670        let avif = image.serialize(2, 2, 128, 128, 64, 64, &color_refs, Some(&alpha_refs)).unwrap();
671
672        // Should contain all color and alpha tile data
673        for tile in &color_tiles {
674            assert!(avif.windows(tile.len()).any(|w| w == tile.as_slice()));
675        }
676        for tile in &alpha_tiles {
677            assert!(avif.windows(tile.len()).any(|w| w == tile.as_slice()));
678        }
679
680        let parser = zenavif_parse::AvifParser::from_bytes(&avif).unwrap();
681        let grid = parser.grid_config().expect("grid config");
682        assert_eq!(grid.rows, 2);
683        assert_eq!(grid.columns, 2);
684    }
685
686    #[test]
687    fn grid_wrong_tile_count_errors() {
688        let tiles = [vec![0u8; 10]];
689        let tile_refs: Vec<&[u8]> = tiles.iter().map(|t| t.as_slice()).collect();
690
691        let image = GridImage::new();
692        // only 1 tile, need 4
693        assert!(image.serialize(2, 2, 200, 200, 100, 100, &tile_refs, None).is_err());
694    }
695}