image_atlas/
lib.rs

1//! # image-atlas
2//!
3//! This library provides a general-purpose texture atlas generator with a focus on ease of use and simplicity.
4//!
5//! There are multiple generation methods and mip map options.
6//!
7//! - No padding between elements
8//! - With padding between elements
9//! - With smart padding between elements for mip map generation.
10//!
11//! This library uses `image` crate for image processing and `rectangle-pack` crate for computing element layout.
12//!
13//! # Examples
14//!
15//! ```rust
16//! use image_atlas::*;
17//!
18//! let atlas = create_atlas(&AtlasDescriptor {
19//!     max_page_count: 8,
20//!     size: 2048,
21//!     mip: AtlasMipOption::MipWithBlock(AtlasMipFilter::Lanczos3, 32),
22//!     entries: &[AtlasEntry {
23//!         texture: image::RgbImage::new(512, 512),
24//!         mip: AtlasEntryMipOption::Clamp,
25//!     }],
26//! })
27//! .unwrap();
28//!
29//! let texcoord = &atlas.texcoords[0];
30//! let texture = &atlas.textures[texcoord.page as usize].mip_maps[0];
31//! ```
32
33use std::{collections::BTreeMap, error, fmt};
34
35/// A filter type using by mip map geration.
36///
37/// - `Nearest`: Nearest neighbor filter.
38/// - `Linear`: Bilinear filter.
39/// - `Cubic`: Bicubic filter (Catmull-Rom).
40/// - `Gaussian`: Gaussian filter.
41/// - `Lanczos3`: Lanczos with window 3 filter.
42///
43/// See the [FilterType](image::imageops::FilterType) for details.
44#[repr(C)]
45#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Debug)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47pub enum AtlasMipFilter {
48    #[default]
49    Nearest,
50    Linear,
51    Cubic,
52    Gaussian,
53    Lanczos3,
54}
55
56impl From<AtlasMipFilter> for image::imageops::FilterType {
57    #[inline]
58    fn from(value: AtlasMipFilter) -> Self {
59        match value {
60            AtlasMipFilter::Nearest => Self::Nearest,
61            AtlasMipFilter::Linear => Self::Triangle,
62            AtlasMipFilter::Cubic => Self::CatmullRom,
63            AtlasMipFilter::Gaussian => Self::Gaussian,
64            AtlasMipFilter::Lanczos3 => Self::Lanczos3,
65        }
66    }
67}
68
69/// A mip map method using by texture atlas generation.
70///
71/// - `NoMip`: No mip map generation.
72/// - `NoMipWithPadding(padding size)`: No mip map generation with padding.
73/// - `Mip(filter, padding size)`: Mip map generation.
74/// - `MipWithPadding(filter, padding size)`: Mip map generation with padding.
75/// - `MipWithBlock(filter, block size)`: Mip map generation with block. block size must be power of two.
76#[repr(C)]
77#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Debug)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79pub enum AtlasMipOption {
80    #[default]
81    NoMip,
82    NoMipWithPadding(u32),
83    Mip(AtlasMipFilter),
84    MipWithPadding(AtlasMipFilter, u32),
85    MipWithBlock(AtlasMipFilter, u32),
86}
87
88/// A tiling method using by texture atlas generation.
89///
90/// - `Clamp`: No tiling.
91/// - `Repeat`: Repeat tiling.
92/// - `Mirror`: Mirror tiling.
93#[repr(C)]
94#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Debug)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96pub enum AtlasEntryMipOption {
97    #[default]
98    Clamp,
99    Repeat,
100    Mirror,
101}
102
103/// A texture atlas generation entry description.
104///
105/// - `texture`: A input texture.
106/// - `mip`: A mip map tiling option.
107#[derive(Clone, PartialEq, Eq, Default, Debug)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109pub struct AtlasEntry<I: image::GenericImageView> {
110    pub texture: I,
111    pub mip: AtlasEntryMipOption,
112}
113
114/// A texture atlas generation description.
115///
116/// - `max_page_count`: A maximum output texture count.
117/// - `size`: A texture width and height (same width and height).
118/// - `mip`: A mip map method option.
119/// - `entries`: A input texture entries.
120#[derive(Clone, PartialEq, Eq, Default, Debug)]
121pub struct AtlasDescriptor<'a, I: image::GenericImageView> {
122    pub max_page_count: u32,
123    pub size: u32,
124    pub mip: AtlasMipOption,
125    pub entries: &'a [AtlasEntry<I>],
126}
127
128/// Creates a new texture atlas.
129///
130/// # Errors
131///
132/// Returns an error if:
133/// - `max_page_count` is zero.
134/// - `size` is not power of two.
135/// - `block_size` is not power of two.
136/// - `entries` is empty.
137/// - Packing error occurred.
138///
139/// See the [AtlasError](AtlasError) for details.
140///
141/// # Examples
142///
143/// ```rust
144/// use image_atlas::*;
145///
146/// let atlas = create_atlas(&AtlasDescriptor {
147///     max_page_count: 8,
148///     size: 2048,
149///     mip: AtlasMipOption::MipWithBlock(AtlasMipFilter::Lanczos3, 32),
150///     entries: &[AtlasEntry {
151///         texture: image::RgbImage::new(512, 512),
152///         mip: AtlasEntryMipOption::Clamp,
153///     }],
154/// })
155/// .unwrap();
156/// ```
157#[rustfmt::skip]
158pub fn create_atlas<I>(desc: &AtlasDescriptor<'_, I>) -> Result<Atlas<I::Pixel>, AtlasError>
159where
160    I: image::GenericImage,
161    I::Pixel: 'static,
162{
163    match desc.mip {
164        AtlasMipOption::NoMip => {
165            create_atlas_with_padding(desc.max_page_count, desc.size, 0, desc.entries)
166        }
167        AtlasMipOption::NoMipWithPadding(padding) => {
168            create_atlas_with_padding(desc.max_page_count, desc.size, padding, desc.entries)
169        }
170        AtlasMipOption::Mip(filter) => {
171            create_atlas_mip_with_padding(desc.max_page_count, desc.size, filter, 0, desc.entries)
172        }
173        AtlasMipOption::MipWithPadding(filter, padding) => {
174            create_atlas_mip_with_padding(desc.max_page_count, desc.size, filter, padding, desc.entries)
175        }
176        AtlasMipOption::MipWithBlock(filter, block_size) => {
177            create_atlas_mip_with_block(desc.max_page_count, desc.size, filter, block_size, desc.entries)
178        }
179    }
180}
181
182#[inline]
183fn create_atlas_with_padding<I>(
184    max_page_count: u32,
185    size: u32,
186    padding: u32,
187    entries: &[AtlasEntry<I>],
188) -> Result<Atlas<I::Pixel>, AtlasError>
189where
190    I: image::GenericImage,
191    I::Pixel: 'static,
192{
193    if max_page_count == 0 {
194        return Err(AtlasError::ZeroMaxPageCount);
195    }
196
197    if entries.is_empty() {
198        return Err(AtlasError::ZeroEntry);
199    }
200
201    let mut rects = rectangle_pack::GroupedRectsToPlace::<_, ()>::new();
202    for (i, entry) in entries.iter().enumerate() {
203        let rect = rectangle_pack::RectToInsert::new(
204            entry.texture.width() + padding * 2,
205            entry.texture.height() + padding * 2,
206            1,
207        );
208        rects.push_rect(i, None, rect);
209    }
210
211    let mut target_bins = BTreeMap::new();
212    for i in 0..max_page_count {
213        target_bins.insert(i, rectangle_pack::TargetBin::new(size, size, 1));
214    }
215
216    let locations = rectangle_pack::pack_rects(
217        &rects,
218        &mut target_bins,
219        &rectangle_pack::volume_heuristic,
220        &rectangle_pack::contains_smallest_box,
221    )?;
222
223    let mut page_count = 0;
224    let mut texcoords = vec![Texcoord::default(); entries.len()];
225    for (&i, &(page, location)) in locations.packed_locations() {
226        page_count = u32::max(page_count, page + 1);
227
228        let texcoord = Texcoord {
229            page,
230            min_x: location.x() + padding,
231            min_y: location.y() + padding,
232            max_x: location.x() + location.width() - padding,
233            max_y: location.y() + location.height() - padding,
234            size,
235        };
236        texcoords[i] = texcoord;
237    }
238
239    let mip_level_count = 1;
240    let mut textures = vec![Texture::new(size, mip_level_count); page_count as usize];
241    for (&i, &(page, location)) in locations.packed_locations() {
242        let entry = &entries[i];
243
244        let src = resample(
245            &entry.texture,
246            entry.mip,
247            padding,
248            padding,
249            location.width(),
250            location.height(),
251        );
252
253        let target = &mut textures[page as usize].mip_maps[0];
254        image::imageops::replace(target, &src, location.x() as i64, location.y() as i64);
255    }
256
257    Ok(Atlas {
258        page_count,
259        size,
260        mip_level_count,
261        textures,
262        texcoords,
263    })
264}
265
266#[inline]
267fn create_atlas_mip_with_padding<I>(
268    max_page_count: u32,
269    size: u32,
270    filter: AtlasMipFilter,
271    padding: u32,
272    entries: &[AtlasEntry<I>],
273) -> Result<Atlas<I::Pixel>, AtlasError>
274where
275    I: image::GenericImage,
276    I::Pixel: 'static,
277{
278    if max_page_count == 0 {
279        return Err(AtlasError::ZeroMaxPageCount);
280    }
281
282    if !size.is_power_of_two() {
283        return Err(AtlasError::InvalidSize(size));
284    }
285
286    if entries.is_empty() {
287        return Err(AtlasError::ZeroEntry);
288    }
289
290    let mut rects = rectangle_pack::GroupedRectsToPlace::<_, ()>::new();
291    for (i, entry) in entries.iter().enumerate() {
292        let rect = rectangle_pack::RectToInsert::new(
293            entry.texture.width() + padding * 2,
294            entry.texture.height() + padding * 2,
295            1,
296        );
297        rects.push_rect(i, None, rect);
298    }
299
300    let mut target_bins = BTreeMap::new();
301    for i in 0..max_page_count {
302        target_bins.insert(i, rectangle_pack::TargetBin::new(size, size, 1));
303    }
304
305    let locations = rectangle_pack::pack_rects(
306        &rects,
307        &mut target_bins,
308        &rectangle_pack::volume_heuristic,
309        &rectangle_pack::contains_smallest_box,
310    )?;
311
312    let mut page_count = 0;
313    let mut texcoords = vec![Texcoord::default(); entries.len()];
314    for (&i, &(page, location)) in locations.packed_locations() {
315        page_count = u32::max(page_count, page + 1);
316
317        let texcoord = Texcoord {
318            page,
319            min_x: location.x() + padding,
320            min_y: location.y() + padding,
321            max_x: location.x() + location.width() - padding,
322            max_y: location.y() + location.height() - padding,
323            size,
324        };
325        texcoords[i] = texcoord;
326    }
327
328    let mip_level_count = size.ilog2() + 1;
329    let mut textures = vec![Texture::new(size, mip_level_count); page_count as usize];
330    for (&i, &(page, location)) in locations.packed_locations() {
331        let entry = &entries[i];
332
333        let src = resample(
334            &entry.texture,
335            entry.mip,
336            padding,
337            padding,
338            location.width(),
339            location.height(),
340        );
341
342        let target = &mut textures[page as usize].mip_maps[0];
343        image::imageops::replace(target, &src, location.x() as i64, location.y() as i64);
344    }
345
346    for mip_level in 1..mip_level_count {
347        let size = size >> mip_level;
348
349        for page in 0..page_count {
350            let src = &textures[page as usize].mip_maps[0];
351
352            let mip_map = image::imageops::resize(src, size, size, filter.into());
353
354            let target = &mut textures[page as usize].mip_maps[mip_level as usize];
355            image::imageops::replace(target, &mip_map, 0, 0);
356        }
357    }
358
359    Ok(Atlas {
360        page_count,
361        size,
362        mip_level_count,
363        textures,
364        texcoords,
365    })
366}
367
368#[inline]
369fn create_atlas_mip_with_block<I>(
370    max_page_count: u32,
371    size: u32,
372    filter: AtlasMipFilter,
373    block_size: u32,
374    entries: &[AtlasEntry<I>],
375) -> Result<Atlas<I::Pixel>, AtlasError>
376where
377    I: image::GenericImage,
378    I::Pixel: 'static,
379{
380    if max_page_count == 0 {
381        return Err(AtlasError::ZeroMaxPageCount);
382    }
383
384    if !size.is_power_of_two() {
385        return Err(AtlasError::InvalidSize(size));
386    }
387
388    if !block_size.is_power_of_two() {
389        return Err(AtlasError::InvalidBlockSize(block_size));
390    }
391
392    if entries.is_empty() {
393        return Err(AtlasError::ZeroEntry);
394    }
395
396    let padding = block_size >> 1;
397
398    let mut rects = rectangle_pack::GroupedRectsToPlace::<_, ()>::new();
399    for (i, entry) in entries.iter().enumerate() {
400        let rect = rectangle_pack::RectToInsert::new(
401            ((entry.texture.width() + block_size) as f32 / block_size as f32).ceil() as u32,
402            ((entry.texture.height() + block_size) as f32 / block_size as f32).ceil() as u32,
403            1,
404        );
405        rects.push_rect(i, None, rect);
406    }
407
408    let bin_size = size / block_size;
409    let mut target_bins = BTreeMap::new();
410    for i in 0..max_page_count {
411        target_bins.insert(i, rectangle_pack::TargetBin::new(bin_size, bin_size, 1));
412    }
413
414    let locations = rectangle_pack::pack_rects(
415        &rects,
416        &mut target_bins,
417        &rectangle_pack::volume_heuristic,
418        &rectangle_pack::contains_smallest_box,
419    )?;
420
421    let mut page_count = 0;
422    let mut texcoords = vec![Texcoord::default(); entries.len()];
423    for (&i, &(page, location)) in locations.packed_locations() {
424        page_count = u32::max(page_count, page + 1);
425
426        let texcoord = Texcoord {
427            page,
428            min_x: location.x() * block_size + padding,
429            min_y: location.y() * block_size + padding,
430            max_x: location.x() * block_size + padding + entries[i].texture.width(),
431            max_y: location.y() * block_size + padding + entries[i].texture.height(),
432            size,
433        };
434        texcoords[i] = texcoord;
435    }
436
437    let mip_level_count = block_size.ilog2() + 1;
438    let mut textures = vec![Texture::new(size, mip_level_count); page_count as usize];
439    for (&i, &(page, location)) in locations.packed_locations() {
440        let entry = &entries[i];
441
442        let src = resample(
443            &entry.texture,
444            entry.mip,
445            padding,
446            padding,
447            location.width() * block_size,
448            location.height() * block_size,
449        );
450
451        for mip_level in 0..mip_level_count {
452            let width = src.width() >> mip_level;
453            let height = src.height() >> mip_level;
454            let mip_map = image::imageops::resize(&src, width, height, filter.into());
455
456            let target = &mut textures[page as usize].mip_maps[mip_level as usize];
457            let x = location.x() as i64 * (block_size >> mip_level) as i64;
458            let y = location.y() as i64 * (block_size >> mip_level) as i64;
459            image::imageops::replace(target, &mip_map, x, y);
460        }
461    }
462
463    Ok(Atlas {
464        page_count,
465        size,
466        mip_level_count,
467        textures,
468        texcoords,
469    })
470}
471
472#[inline]
473#[rustfmt::skip]
474fn resample<I>(
475    src: &I,
476    mip: AtlasEntryMipOption,
477    shift_x: u32,
478    shift_y: u32,
479    width: u32,
480    height: u32,
481) -> image::ImageBuffer<I::Pixel, Vec<<I::Pixel as image::Pixel>::Subpixel>>
482where
483    I: image::GenericImage,
484{
485    let mut target = image::ImageBuffer::new(width, height);
486    match mip {
487        AtlasEntryMipOption::Clamp => {
488            for x in 0..width {
489                for y in 0..height {
490                    let sx = (x as i32 - shift_x as i32).max(0).min(src.width() as i32 - 1);
491                    let sy = (y as i32 - shift_y as i32).max(0).min(src.height() as i32 - 1);
492                    *target.get_pixel_mut(x, y) = src.get_pixel(sx as u32, sy as u32);
493                }
494            }
495        }
496        AtlasEntryMipOption::Repeat => {
497            for x in 0..width {
498                for y in 0..height {
499                    let sx = (x as i32 - shift_x as i32).rem_euclid(src.width() as i32);
500                    let sy = (y as i32 - shift_y as i32).rem_euclid(src.height() as i32);
501                    *target.get_pixel_mut(x, y) = src.get_pixel(sx as u32, sy as u32);
502                }
503            }
504        }
505        AtlasEntryMipOption::Mirror => {
506            for x in 0..width {
507                for y in 0..height {
508                    let xx = (x as i32 - shift_x as i32).div_euclid(src.width() as i32);
509                    let yy = (y as i32 - shift_y as i32).div_euclid(src.height() as i32);
510                    let mut sx = (x as i32 - shift_x as i32).rem_euclid(src.width() as i32);
511                    let mut sy = (y as i32 - shift_y as i32).rem_euclid(src.height() as i32);
512                    if xx & 1 == 0 { sx = src.width() as i32 - 1 - sx; }
513                    if yy & 1 == 0 { sy = src.height() as i32 - 1 - sy; }
514                    *target.get_pixel_mut(x, y) = src.get_pixel(sx as u32, sy as u32);
515                }
516            }
517        }
518    }
519    target
520}
521
522/// A result of texture atlas generation.
523///
524/// - `page_count`: A output texture count.
525/// - `size`: A output texture width and height (same width and height).
526/// - `mip_level_count`: A mip map count of output texture (1 is no mip map).
527/// - `textures`: A vec of output texture.
528/// - `texcoord`: A vec of texcoord in output texture (same order as `entries`).
529#[derive(Clone, Default)]
530pub struct Atlas<P: image::Pixel> {
531    pub page_count: u32,
532    pub size: u32,
533    pub mip_level_count: u32,
534    pub textures: Vec<Texture<P>>,
535    pub texcoords: Vec<Texcoord>,
536}
537
538impl<P> fmt::Debug for Atlas<P>
539where
540    P: image::Pixel + fmt::Debug,
541    P::Subpixel: fmt::Debug,
542{
543    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
544        f.debug_struct("Atlas")
545            .field("page_count", &self.page_count)
546            .field("size", &self.size)
547            .field("mip_level_count", &self.mip_level_count)
548            .field("textures", &self.textures)
549            .field("texcoords", &self.texcoords)
550            .finish()
551    }
552}
553
554/// A output texture entry of texture atlas.
555///
556/// - `size`: A output texture width and height (same width and height).
557/// - `mip_level_count`: A mip map count of output texture (1 is no mip map).
558/// - `mip_maps`: A vec of mip map.
559#[derive(Clone, Default)]
560pub struct Texture<P: image::Pixel> {
561    pub size: u32,
562    pub mip_level_count: u32,
563    pub mip_maps: Vec<image::ImageBuffer<P, Vec<P::Subpixel>>>,
564}
565
566impl<P: image::Pixel> Texture<P> {
567    #[inline]
568    pub fn new(size: u32, mip_level_count: u32) -> Self {
569        let mip_maps = (0..mip_level_count)
570            .map(|mip_level| size >> mip_level)
571            .map(|size| image::ImageBuffer::new(size, size))
572            .collect::<Vec<_>>();
573        Self {
574            size,
575            mip_level_count,
576            mip_maps,
577        }
578    }
579}
580
581impl<P> fmt::Debug for Texture<P>
582where
583    P: image::Pixel + fmt::Debug,
584    P::Subpixel: fmt::Debug,
585{
586    #[inline]
587    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588        f.debug_struct("Texture")
589            .field("size", &self.size)
590            .field("mip_level_count", &self.mip_level_count)
591            .field("mip_maps", &self.mip_maps)
592            .finish()
593    }
594}
595
596/// An element coordinate representing `u32` position.
597///
598/// - `page`: A page index of texture.
599/// - `min_x`: A minimum x position.
600/// - `min_y`: A minimum y position.
601/// - `max_x`: A maximum x position.
602/// - `max_y`: A maximum y position.
603///
604/// `to_f32` and `to_f64` methods are provided for normalized texcoord.
605#[repr(C)]
606#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
607#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
608pub struct Texcoord {
609    pub page: u32,
610    pub min_x: u32,
611    pub min_y: u32,
612    pub max_x: u32,
613    pub max_y: u32,
614    pub size: u32,
615}
616
617impl Texcoord {
618    /// Returns a normalized texcoord using f32.
619    #[inline]
620    pub fn to_f32(self) -> Texcoord32 {
621        Texcoord32 {
622            page: self.page,
623            min_x: self.min_x as f32 / self.size as f32,
624            min_y: self.min_y as f32 / self.size as f32,
625            max_x: self.max_x as f32 / self.size as f32,
626            max_y: self.max_y as f32 / self.size as f32,
627        }
628    }
629
630    /// Returns a normalized texcoord using f64.
631    #[inline]
632    pub fn to_f64(self) -> Texcoord64 {
633        Texcoord64 {
634            page: self.page,
635            min_x: self.min_x as f64 / self.size as f64,
636            min_y: self.min_y as f64 / self.size as f64,
637            max_x: self.max_x as f64 / self.size as f64,
638            max_y: self.max_y as f64 / self.size as f64,
639        }
640    }
641}
642
643/// An element coordinate representing `f32` position.
644///
645/// - `page`: A page index of texture.
646/// - `min_x`: A minimum x position (normalized).
647/// - `min_y`: A minimum y position (normalized).
648/// - `max_x`: A maximum x position (normalized).
649/// - `max_y`: A maximum y position (normalized).
650#[repr(C)]
651#[derive(Clone, Copy, PartialEq, Default, Debug)]
652#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
653pub struct Texcoord32 {
654    pub page: u32,
655    pub min_x: f32,
656    pub min_y: f32,
657    pub max_x: f32,
658    pub max_y: f32,
659}
660
661impl From<Texcoord> for Texcoord32 {
662    #[inline]
663    fn from(value: Texcoord) -> Self {
664        value.to_f32()
665    }
666}
667
668/// An element coordinate representing `f64` position.
669///
670/// - `page`: A page index of texture.
671/// - `min_x`: A minimum x position (normalized).
672/// - `min_y`: A minimum y position (normalized).
673/// - `max_x`: A maximum x position (normalized).
674/// - `max_y`: A maximum y position (normalized).
675#[repr(C)]
676#[derive(Clone, Copy, PartialEq, Default, Debug)]
677#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
678pub struct Texcoord64 {
679    pub page: u32,
680    pub min_x: f64,
681    pub min_y: f64,
682    pub max_x: f64,
683    pub max_y: f64,
684}
685
686impl From<Texcoord> for Texcoord64 {
687    #[inline]
688    fn from(value: Texcoord) -> Self {
689        value.to_f64()
690    }
691}
692
693/// An error type for texture atlas generation.
694///
695/// - `ZeroMaxPageCount`: `max_page_count` is zero.
696/// - `InvalidSize(size)`: `size` is not power of two.
697/// - `InvalidBlockSize(block_size)`: `block_size` is not power of two.
698/// - `ZeroEntry`: `entries` is empty.
699/// - `Packing(err)`: Packing error occurred.
700///
701/// See the [RectanglePackError](rectangle_pack::RectanglePackError) for details.
702#[derive(Debug)]
703pub enum AtlasError {
704    ZeroMaxPageCount,
705    InvalidSize(u32),
706    InvalidBlockSize(u32),
707    ZeroEntry,
708    Packing(rectangle_pack::RectanglePackError),
709}
710
711impl fmt::Display for AtlasError {
712    #[rustfmt::skip]
713    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
714        match self {
715            AtlasError::ZeroMaxPageCount => write!(f, "max page count is zero."),
716            AtlasError::InvalidSize(size) => write!(f, "size is not power of two: {}.", size),
717            AtlasError::InvalidBlockSize(block_size) => write!(f, "block size is not power of two: {}.", block_size),
718            AtlasError::ZeroEntry => write!(f, "entry is empty."),
719            AtlasError::Packing(err) => err.fmt(f),
720        }
721    }
722}
723
724impl error::Error for AtlasError {}
725
726impl From<rectangle_pack::RectanglePackError> for AtlasError {
727    fn from(value: rectangle_pack::RectanglePackError) -> Self {
728        AtlasError::Packing(value)
729    }
730}