tex_packer_core/
pipeline.rs

1use crate::config::PackerConfig;
2use crate::config::{AlgorithmFamily, AutoMode, SortOrder};
3use crate::error::{Result, TexPackerError};
4use crate::model::{Atlas, Frame, Meta, Page, Rect};
5use crate::packer::{
6    Packer, guillotine::GuillotinePacker, maxrects::MaxRectsPacker, skyline::SkylinePacker,
7};
8use image::{DynamicImage, RgbaImage};
9use std::collections::{HashMap, HashSet};
10use std::time::Instant;
11use tracing::instrument;
12
13#[cfg(feature = "parallel")]
14use rayon::prelude::*;
15
16/// In-memory image to pack (key + decoded image).
17pub struct InputImage {
18    pub key: String,
19    pub image: DynamicImage,
20}
21
22/// Output RGBA page and its logical page record.
23pub struct OutputPage {
24    pub page: Page,
25    pub rgba: RgbaImage,
26}
27
28/// Output of a packing run: atlas metadata and RGBA pages.
29pub struct PackOutput {
30    pub atlas: Atlas,
31    pub pages: Vec<OutputPage>,
32}
33
34impl PackOutput {
35    /// Computes packing statistics for this output.
36    /// This is a convenience method that delegates to `atlas.stats()`.
37    pub fn stats(&self) -> crate::model::PackStats {
38        self.atlas.stats()
39    }
40}
41
42#[instrument(skip_all)]
43/// Packs `inputs` into atlas pages using configuration `cfg` and returns metadata and RGBA pages.
44///
45/// Notes:
46/// - Sorting is stable for deterministic results.
47/// - When `family` is `Auto`, a small portfolio is tried and the best result is chosen (pages first, then total area).
48/// - `time_budget_ms` can limit Auto evaluation time; `parallel` may evaluate in parallel when enabled.
49pub fn pack_images(inputs: Vec<InputImage>, cfg: PackerConfig) -> Result<PackOutput> {
50    // Validate configuration first
51    cfg.validate()?;
52
53    if inputs.is_empty() {
54        return Err(TexPackerError::Empty);
55    }
56
57    // Preprocess once
58    let prepared = prepare_inputs(&inputs, &cfg);
59
60    // Auto portfolio
61    if matches!(cfg.family, AlgorithmFamily::Auto) {
62        return pack_auto(&prepared, cfg);
63    }
64
65    pack_prepared(&prepared, &cfg)
66}
67
68pub fn compute_trim_rect(rgba: &RgbaImage, threshold: u8) -> (Option<Rect>, Rect) {
69    let (w, h) = rgba.dimensions();
70    let mut x1 = 0;
71    let mut y1 = 0;
72    let mut x2 = w.saturating_sub(1);
73    let mut y2 = h.saturating_sub(1);
74    // left
75    while x1 < w {
76        let mut all_transparent = true;
77        for y in 0..h {
78            if rgba.get_pixel(x1, y)[3] > threshold {
79                all_transparent = false;
80                break;
81            }
82        }
83        if all_transparent {
84            x1 += 1;
85        } else {
86            break;
87        }
88    }
89    if x1 >= w {
90        return (None, Rect::new(0, 0, w, h));
91    }
92    // right
93    while x2 > x1 {
94        let mut all_transparent = true;
95        for y in 0..h {
96            if rgba.get_pixel(x2, y)[3] > threshold {
97                all_transparent = false;
98                break;
99            }
100        }
101        if all_transparent {
102            x2 -= 1;
103        } else {
104            break;
105        }
106    }
107    // top
108    while y1 < h {
109        let mut all_transparent = true;
110        for x in x1..=x2 {
111            if rgba.get_pixel(x, y1)[3] > threshold {
112                all_transparent = false;
113                break;
114            }
115        }
116        if all_transparent {
117            y1 += 1;
118        } else {
119            break;
120        }
121    }
122    // bottom
123    while y2 > y1 {
124        let mut all_transparent = true;
125        for x in x1..=x2 {
126            if rgba.get_pixel(x, y2)[3] > threshold {
127                all_transparent = false;
128                break;
129            }
130        }
131        if all_transparent {
132            y2 -= 1;
133        } else {
134            break;
135        }
136    }
137    let tw = x2 - x1 + 1;
138    let th = y2 - y1 + 1;
139    (Some(Rect::new(0, 0, tw, th)), Rect::new(x1, y1, tw, th))
140}
141
142fn next_pow2(mut v: u32) -> u32 {
143    if v <= 1 {
144        return 1;
145    }
146    v -= 1;
147    v |= v >> 1;
148    v |= v >> 2;
149    v |= v >> 4;
150    v |= v >> 8;
151    v |= v >> 16;
152    v + 1
153}
154
155#[allow(clippy::too_many_arguments)]
156// moved to compositing::blit_rgba for reuse in runtime
157
158// ---------- helpers for multi-run (auto) ----------
159
160struct Prep {
161    key: String,
162    rgba: RgbaImage,
163    rect: Rect,
164    trimmed: bool,
165    source: Rect,
166    orig_size: (u32, u32),
167}
168
169fn prepare_inputs(inputs: &[InputImage], cfg: &PackerConfig) -> Vec<Prep> {
170    let mut out = Vec::with_capacity(inputs.len());
171    for inp in inputs.iter() {
172        let rgba = inp.image.to_rgba8();
173        let (iw, ih) = rgba.dimensions();
174        let mut push_entry = true;
175        let (rect, trimmed, source) = if cfg.trim {
176            let (trim_rect_opt, src_rect) = compute_trim_rect(&rgba, cfg.trim_threshold);
177            match trim_rect_opt {
178                Some(r) => (Rect::new(0, 0, r.w, r.h), true, src_rect),
179                None => match cfg.transparent_policy {
180                    crate::config::TransparentPolicy::Keep => {
181                        (Rect::new(0, 0, iw, ih), false, Rect::new(0, 0, iw, ih))
182                    }
183                    crate::config::TransparentPolicy::OneByOne => {
184                        (Rect::new(0, 0, 1, 1), true, Rect::new(0, 0, 1, 1))
185                    }
186                    crate::config::TransparentPolicy::Skip => {
187                        push_entry = false;
188                        (Rect::new(0, 0, 0, 0), false, Rect::new(0, 0, 0, 0))
189                    }
190                },
191            }
192        } else {
193            (Rect::new(0, 0, iw, ih), false, Rect::new(0, 0, iw, ih))
194        };
195        if !push_entry {
196            continue;
197        }
198        out.push(Prep {
199            key: inp.key.clone(),
200            rgba,
201            rect,
202            trimmed,
203            source,
204            orig_size: (iw, ih),
205        });
206    }
207    // stable sort per config
208    match cfg.sort_order {
209        SortOrder::None => {}
210        SortOrder::NameAsc => {
211            out.sort_by(|a, b| a.key.cmp(&b.key));
212        }
213        SortOrder::AreaDesc => {
214            out.sort_by(|a, b| {
215                (b.rect.w * b.rect.h)
216                    .cmp(&(a.rect.w * a.rect.h))
217                    .then_with(|| a.key.cmp(&b.key))
218            });
219        }
220        SortOrder::MaxSideDesc => {
221            out.sort_by(|a, b| {
222                b.rect
223                    .w
224                    .max(b.rect.h)
225                    .cmp(&a.rect.w.max(a.rect.h))
226                    .then_with(|| a.key.cmp(&b.key))
227            });
228        }
229        SortOrder::HeightDesc => {
230            out.sort_by(|a, b| b.rect.h.cmp(&a.rect.h).then_with(|| a.key.cmp(&b.key)));
231        }
232        SortOrder::WidthDesc => {
233            out.sort_by(|a, b| b.rect.w.cmp(&a.rect.w).then_with(|| a.key.cmp(&b.key)));
234        }
235    }
236    out
237}
238
239fn pack_prepared(prepared: &[Prep], cfg: &PackerConfig) -> Result<PackOutput> {
240    let mut pages: Vec<OutputPage> = Vec::new();
241    let mut atlas_pages: Vec<Page> = Vec::new();
242
243    // Map for quick lookup during compositing
244    let prep_map: HashMap<String, &Prep> = prepared.iter().map(|p| (p.key.clone(), p)).collect();
245
246    // Remaining indices to place (in sorted order)
247    let mut remaining: Vec<usize> = (0..prepared.len()).collect();
248    let mut page_id = 0usize;
249
250    while !remaining.is_empty() {
251        let mut packer: Box<dyn Packer<String>> = match cfg.family {
252            AlgorithmFamily::Skyline => Box::new(SkylinePacker::new(cfg.clone())),
253            AlgorithmFamily::MaxRects => {
254                Box::new(MaxRectsPacker::new(cfg.clone(), cfg.mr_heuristic.clone()))
255            }
256            AlgorithmFamily::Guillotine => Box::new(GuillotinePacker::new(
257                cfg.clone(),
258                cfg.g_choice.clone(),
259                cfg.g_split.clone(),
260            )),
261            AlgorithmFamily::Auto => unreachable!(),
262        };
263        let mut frames: Vec<Frame> = Vec::new();
264
265        loop {
266            let mut placed_any = false;
267            let mut remove_set: HashSet<usize> = HashSet::new();
268            for &idx in &remaining {
269                let p = &prepared[idx];
270                if !packer.can_pack(&p.rect) {
271                    continue;
272                }
273                if let Some(mut f) = packer.pack(p.key.clone(), &p.rect) {
274                    f.trimmed = p.trimmed;
275                    f.source = p.source;
276                    f.source_size = p.orig_size;
277                    frames.push(f);
278                    remove_set.insert(idx);
279                    placed_any = true;
280                }
281            }
282            if !placed_any {
283                break;
284            }
285            // Retain only indices not placed
286            if !remove_set.is_empty() {
287                remaining.retain(|i| !remove_set.contains(i));
288            }
289        }
290
291        if frames.is_empty() {
292            // No textures could be placed on this page - likely first texture is too large
293            let placed = prepared.len() - remaining.len();
294            return Err(TexPackerError::OutOfSpaceGeneric {
295                placed,
296                total: prepared.len(),
297            });
298        }
299
300        // Compute final page size via helper to keep logic consistent across APIs
301        let (page_w, page_h) = compute_page_size(&frames, cfg);
302
303        let mut canvas = RgbaImage::new(page_w, page_h);
304        for f in &frames {
305            if let Some(prep) = prep_map.get(&f.key) {
306                crate::compositing::blit_rgba(
307                    &prep.rgba,
308                    &mut canvas,
309                    f.frame.x,
310                    f.frame.y,
311                    prep.source.x,
312                    prep.source.y,
313                    prep.source.w,
314                    prep.source.h,
315                    f.rotated,
316                    cfg.texture_extrusion,
317                    cfg.texture_outlines,
318                );
319            }
320        }
321        let page = Page {
322            id: page_id,
323            width: page_w,
324            height: page_h,
325            frames: frames.clone(),
326        };
327        pages.push(OutputPage {
328            page: page.clone(),
329            rgba: canvas,
330        });
331        atlas_pages.push(page);
332        page_id += 1;
333    }
334
335    let meta = Meta {
336        schema_version: "1".into(),
337        app: "tex-packer".into(),
338        version: env!("CARGO_PKG_VERSION").into(),
339        format: "RGBA8888".into(),
340        scale: 1.0,
341        power_of_two: cfg.power_of_two,
342        square: cfg.square,
343        max_dim: (cfg.max_width, cfg.max_height),
344        padding: (cfg.border_padding, cfg.texture_padding),
345        extrude: cfg.texture_extrusion,
346        allow_rotation: cfg.allow_rotation,
347        trim_mode: if cfg.trim { "trim" } else { "none" }.into(),
348        background_color: None,
349    };
350    let atlas = Atlas {
351        pages: atlas_pages,
352        meta,
353    };
354    Ok(PackOutput { atlas, pages })
355}
356
357fn pack_auto(prepared: &[Prep], base: PackerConfig) -> Result<PackOutput> {
358    let mut candidates: Vec<PackerConfig> = Vec::new();
359    let n_inputs = prepared.len();
360    let budget_ms = base.time_budget_ms.unwrap_or(0);
361    let thr_time = base.auto_mr_ref_time_ms_threshold.unwrap_or(200);
362    let thr_inputs = base.auto_mr_ref_input_threshold.unwrap_or(800);
363    let enable_mr_ref = matches!(base.auto_mode, AutoMode::Quality)
364        && (budget_ms >= thr_time || n_inputs >= thr_inputs);
365    match base.auto_mode {
366        AutoMode::Fast => {
367            let mut s_bl = base.clone();
368            s_bl.family = AlgorithmFamily::Skyline;
369            s_bl.skyline_heuristic = crate::config::SkylineHeuristic::BottomLeft;
370            candidates.push(s_bl);
371            let mut mr_baf = base.clone();
372            mr_baf.family = AlgorithmFamily::MaxRects;
373            mr_baf.mr_heuristic = crate::config::MaxRectsHeuristic::BestAreaFit;
374            mr_baf.mr_reference = false;
375            candidates.push(mr_baf);
376        }
377        AutoMode::Quality => {
378            let mut s_mw = base.clone();
379            s_mw.family = AlgorithmFamily::Skyline;
380            s_mw.skyline_heuristic = crate::config::SkylineHeuristic::MinWaste;
381            candidates.push(s_mw);
382            let mut mr_baf = base.clone();
383            mr_baf.family = AlgorithmFamily::MaxRects;
384            mr_baf.mr_heuristic = crate::config::MaxRectsHeuristic::BestAreaFit;
385            mr_baf.mr_reference = enable_mr_ref;
386            candidates.push(mr_baf);
387            let mut mr_bl = base.clone();
388            mr_bl.family = AlgorithmFamily::MaxRects;
389            mr_bl.mr_heuristic = crate::config::MaxRectsHeuristic::BottomLeft;
390            mr_bl.mr_reference = enable_mr_ref;
391            candidates.push(mr_bl);
392            let mut mr_cp = base.clone();
393            mr_cp.family = AlgorithmFamily::MaxRects;
394            mr_cp.mr_heuristic = crate::config::MaxRectsHeuristic::ContactPoint;
395            mr_cp.mr_reference = enable_mr_ref;
396            candidates.push(mr_cp);
397            let mut g = base.clone();
398            g.family = AlgorithmFamily::Guillotine;
399            g.g_choice = crate::config::GuillotineChoice::BestAreaFit;
400            g.g_split = crate::config::GuillotineSplit::SplitShorterLeftoverAxis;
401            candidates.push(g);
402        }
403    }
404    let start = Instant::now();
405
406    // Parallel path (optional)
407    #[cfg(feature = "parallel")]
408    {
409        if base.parallel {
410            let results: Vec<(PackOutput, u64, u32)> = candidates
411                .par_iter()
412                .filter_map(|cand| pack_prepared(prepared, cand).ok())
413                .map(|out| {
414                    let pages = out.atlas.pages.len() as u32;
415                    let total_area: u64 = out
416                        .atlas
417                        .pages
418                        .iter()
419                        .map(|p| (p.width as u64) * (p.height as u64))
420                        .sum();
421                    (out, total_area, pages)
422                })
423                .collect();
424            let best = results.into_iter().min_by(|a, b| match a.2.cmp(&b.2) {
425                // pages asc
426                std::cmp::Ordering::Equal => a.1.cmp(&b.1),
427                other => other,
428            });
429            return best.map(|x| x.0).ok_or(TexPackerError::OutOfSpaceGeneric {
430                placed: 0,
431                total: prepared.len(),
432            });
433        }
434    }
435
436    // Sequential path with optional time budget
437    let mut best: Option<(PackOutput, u64, u32)> = None; // (output, total_area, pages)
438    for cand in candidates.into_iter() {
439        if budget_ms > 0 && start.elapsed().as_millis() as u64 > budget_ms {
440            break;
441        }
442        if let Ok(out) = pack_prepared(prepared, &cand) {
443            let pages = out.atlas.pages.len() as u32;
444            let total_area: u64 = out
445                .atlas
446                .pages
447                .iter()
448                .map(|p| (p.width as u64) * (p.height as u64))
449                .sum();
450            match &mut best {
451                None => best = Some((out, total_area, pages)),
452                Some((bo, barea, bpages)) => {
453                    if pages < *bpages || (pages == *bpages && total_area < *barea) {
454                        *bo = out;
455                        *barea = total_area;
456                        *bpages = pages;
457                    }
458                }
459            }
460        }
461    }
462    best.map(|x| x.0).ok_or(TexPackerError::OutOfSpaceGeneric {
463        placed: 0,
464        total: prepared.len(),
465    })
466}
467
468// ---------------- Layout-only API ----------------
469
470/// Packs sizes into pages without compositing pixel data.
471/// Inputs are (key, width, height). Returns an Atlas with pages and frames; no RGBA pages.
472pub fn pack_layout<K: Into<String>>(
473    inputs: Vec<(K, u32, u32)>,
474    cfg: PackerConfig,
475) -> Result<Atlas<String>> {
476    // Validate configuration first
477    cfg.validate()?;
478
479    if inputs.is_empty() {
480        return Err(TexPackerError::Empty);
481    }
482    // Build lightweight preps
483    struct PrepL {
484        key: String,
485        rect: Rect,
486        trimmed: bool,
487        source: Rect,
488        orig_size: (u32, u32),
489    }
490    let mut prepared: Vec<PrepL> = inputs
491        .into_iter()
492        .map(|(k, w, h)| {
493            let key = k.into();
494            let rect = Rect::new(0, 0, w, h);
495            let source = Rect::new(0, 0, w, h);
496            PrepL {
497                key,
498                rect,
499                trimmed: false,
500                source,
501                orig_size: (w, h),
502            }
503        })
504        .collect();
505    // Sort like pack_images
506    match cfg.sort_order {
507        SortOrder::None => {}
508        SortOrder::NameAsc => prepared.sort_by(|a, b| a.key.cmp(&b.key)),
509        SortOrder::AreaDesc => prepared.sort_by(|a, b| {
510            (b.rect.w * b.rect.h)
511                .cmp(&(a.rect.w * a.rect.h))
512                .then_with(|| a.key.cmp(&b.key))
513        }),
514        SortOrder::MaxSideDesc => prepared.sort_by(|a, b| {
515            b.rect
516                .w
517                .max(b.rect.h)
518                .cmp(&a.rect.w.max(a.rect.h))
519                .then_with(|| a.key.cmp(&b.key))
520        }),
521        SortOrder::HeightDesc => {
522            prepared.sort_by(|a, b| b.rect.h.cmp(&a.rect.h).then_with(|| a.key.cmp(&b.key)))
523        }
524        SortOrder::WidthDesc => {
525            prepared.sort_by(|a, b| b.rect.w.cmp(&a.rect.w).then_with(|| a.key.cmp(&b.key)))
526        }
527    }
528
529    let mut remaining: Vec<usize> = (0..prepared.len()).collect();
530    let mut atlas_pages: Vec<Page> = Vec::new();
531    let mut page_id = 0usize;
532    while !remaining.is_empty() {
533        let mut packer: Box<dyn Packer<String>> = match cfg.family {
534            AlgorithmFamily::Skyline => Box::new(SkylinePacker::new(cfg.clone())),
535            AlgorithmFamily::MaxRects => {
536                Box::new(MaxRectsPacker::new(cfg.clone(), cfg.mr_heuristic.clone()))
537            }
538            AlgorithmFamily::Guillotine => Box::new(GuillotinePacker::new(
539                cfg.clone(),
540                cfg.g_choice.clone(),
541                cfg.g_split.clone(),
542            )),
543            AlgorithmFamily::Auto => unreachable!(),
544        };
545        let mut frames: Vec<Frame> = Vec::new();
546        loop {
547            let mut placed_any = false;
548            let mut remove_set: HashSet<usize> = HashSet::new();
549            for &idx in &remaining {
550                let p = &prepared[idx];
551                if !packer.can_pack(&p.rect) {
552                    continue;
553                }
554                if let Some(mut f) = packer.pack(p.key.clone(), &p.rect) {
555                    f.trimmed = p.trimmed;
556                    f.source = p.source;
557                    f.source_size = p.orig_size;
558                    frames.push(f);
559                    remove_set.insert(idx);
560                    placed_any = true;
561                }
562            }
563            if !placed_any {
564                break;
565            }
566            if !remove_set.is_empty() {
567                remaining.retain(|i| !remove_set.contains(i));
568            }
569        }
570        if frames.is_empty() {
571            let placed = prepared.len() - remaining.len();
572            return Err(TexPackerError::OutOfSpaceGeneric {
573                placed,
574                total: prepared.len(),
575            });
576        }
577
578        // Compute page size same as pack_prepared
579        let (page_w, page_h) = compute_page_size(&frames, &cfg);
580
581        let page = Page {
582            id: page_id,
583            width: page_w,
584            height: page_h,
585            frames: frames.clone(),
586        };
587        atlas_pages.push(page);
588        page_id += 1;
589    }
590
591    let meta = Meta {
592        schema_version: "1".into(),
593        app: "tex-packer".into(),
594        version: env!("CARGO_PKG_VERSION").into(),
595        format: "RGBA8888".into(),
596        scale: 1.0,
597        power_of_two: cfg.power_of_two,
598        square: cfg.square,
599        max_dim: (cfg.max_width, cfg.max_height),
600        padding: (cfg.border_padding, cfg.texture_padding),
601        extrude: cfg.texture_extrusion,
602        allow_rotation: cfg.allow_rotation,
603        trim_mode: if cfg.trim { "trim" } else { "none" }.into(),
604        background_color: None,
605    };
606    Ok(Atlas {
607        pages: atlas_pages,
608        meta,
609    })
610}
611
612/// Layout-only item with optional source/source_size to propagate trimming metadata.
613#[derive(Debug, Clone)]
614pub struct LayoutItem<K = String> {
615    pub key: K,
616    pub w: u32,
617    pub h: u32,
618    pub source: Option<Rect>,
619    pub source_size: Option<(u32, u32)>,
620    pub trimmed: bool,
621}
622
623/// Packs layout-only items (with optional source/source_size metadata) into pages.
624pub fn pack_layout_items<K: Into<String>>(
625    items: Vec<LayoutItem<K>>,
626    cfg: PackerConfig,
627) -> Result<Atlas<String>> {
628    // Validate configuration first
629    cfg.validate()?;
630
631    if items.is_empty() {
632        return Err(TexPackerError::Empty);
633    }
634    struct PrepL {
635        key: String,
636        rect: Rect,
637        trimmed: bool,
638        source: Rect,
639        orig_size: (u32, u32),
640    }
641    let mut prepared: Vec<PrepL> = items
642        .into_iter()
643        .map(|it| {
644            let key = it.key.into();
645            let rect = Rect::new(0, 0, it.w, it.h);
646            let source = it.source.unwrap_or(Rect::new(0, 0, it.w, it.h));
647            let orig = it.source_size.unwrap_or((it.w, it.h));
648            PrepL {
649                key,
650                rect,
651                trimmed: it.trimmed,
652                source,
653                orig_size: orig,
654            }
655        })
656        .collect();
657    match cfg.sort_order {
658        SortOrder::None => {}
659        SortOrder::NameAsc => prepared.sort_by(|a, b| a.key.cmp(&b.key)),
660        SortOrder::AreaDesc => prepared.sort_by(|a, b| {
661            (b.rect.w * b.rect.h)
662                .cmp(&(a.rect.w * a.rect.h))
663                .then_with(|| a.key.cmp(&b.key))
664        }),
665        SortOrder::MaxSideDesc => prepared.sort_by(|a, b| {
666            b.rect
667                .w
668                .max(b.rect.h)
669                .cmp(&a.rect.w.max(a.rect.h))
670                .then_with(|| a.key.cmp(&b.key))
671        }),
672        SortOrder::HeightDesc => {
673            prepared.sort_by(|a, b| b.rect.h.cmp(&a.rect.h).then_with(|| a.key.cmp(&b.key)))
674        }
675        SortOrder::WidthDesc => {
676            prepared.sort_by(|a, b| b.rect.w.cmp(&a.rect.w).then_with(|| a.key.cmp(&b.key)))
677        }
678    }
679
680    let mut remaining: Vec<usize> = (0..prepared.len()).collect();
681    let mut atlas_pages: Vec<Page> = Vec::new();
682    let mut page_id = 0usize;
683    while !remaining.is_empty() {
684        let mut packer: Box<dyn Packer<String>> = match cfg.family {
685            AlgorithmFamily::Skyline => Box::new(SkylinePacker::new(cfg.clone())),
686            AlgorithmFamily::MaxRects => {
687                Box::new(MaxRectsPacker::new(cfg.clone(), cfg.mr_heuristic.clone()))
688            }
689            AlgorithmFamily::Guillotine => Box::new(GuillotinePacker::new(
690                cfg.clone(),
691                cfg.g_choice.clone(),
692                cfg.g_split.clone(),
693            )),
694            AlgorithmFamily::Auto => unreachable!(),
695        };
696        let mut frames: Vec<Frame> = Vec::new();
697        loop {
698            let mut placed_any = false;
699            let mut remove_set: HashSet<usize> = HashSet::new();
700            for &idx in &remaining {
701                let p = &prepared[idx];
702                if !packer.can_pack(&p.rect) {
703                    continue;
704                }
705                if let Some(mut f) = packer.pack(p.key.clone(), &p.rect) {
706                    f.trimmed = p.trimmed;
707                    f.source = p.source;
708                    f.source_size = p.orig_size;
709                    frames.push(f);
710                    remove_set.insert(idx);
711                    placed_any = true;
712                }
713            }
714            if !placed_any {
715                break;
716            }
717            if !remove_set.is_empty() {
718                remaining.retain(|i| !remove_set.contains(i));
719            }
720        }
721        if frames.is_empty() {
722            let placed = prepared.len() - remaining.len();
723            return Err(TexPackerError::OutOfSpaceGeneric {
724                placed,
725                total: prepared.len(),
726            });
727        }
728
729        let (page_w, page_h) = compute_page_size(&frames, &cfg);
730
731        let page = Page {
732            id: page_id,
733            width: page_w,
734            height: page_h,
735            frames: frames.clone(),
736        };
737        atlas_pages.push(page);
738        page_id += 1;
739    }
740
741    let meta = Meta {
742        schema_version: "1".into(),
743        app: "tex-packer".into(),
744        version: env!("CARGO_PKG_VERSION").into(),
745        format: "RGBA8888".into(),
746        scale: 1.0,
747        power_of_two: cfg.power_of_two,
748        square: cfg.square,
749        max_dim: (cfg.max_width, cfg.max_height),
750        padding: (cfg.border_padding, cfg.texture_padding),
751        extrude: cfg.texture_extrusion,
752        allow_rotation: cfg.allow_rotation,
753        trim_mode: if cfg.trim { "trim" } else { "none" }.into(),
754        background_color: None,
755    };
756    Ok(Atlas {
757        pages: atlas_pages,
758        meta,
759    })
760}
761
762/// Compute final page dimensions given placed frames and config.
763fn compute_page_size(frames: &[Frame], cfg: &PackerConfig) -> (u32, u32) {
764    if cfg.force_max_dimensions {
765        // When forced, return exactly the configured dimensions, ignoring pow2/square adjustments.
766        return (cfg.max_width, cfg.max_height);
767    }
768    let pad_half = cfg.texture_padding / 2;
769    let pad_rem = cfg.texture_padding - pad_half;
770    let right_extra = cfg.texture_extrusion + pad_rem;
771    let bottom_extra = cfg.texture_extrusion + pad_rem;
772    let mut page_w = 0u32;
773    let mut page_h = 0u32;
774    for f in frames {
775        page_w = page_w.max(f.frame.right() + 1 + right_extra + cfg.border_padding);
776        page_h = page_h.max(f.frame.bottom() + 1 + bottom_extra + cfg.border_padding);
777    }
778    if cfg.power_of_two {
779        page_w = next_pow2(page_w.max(1));
780        page_h = next_pow2(page_h.max(1));
781    }
782    if cfg.square {
783        let m = page_w.max(page_h);
784        page_w = m;
785        page_h = m;
786    }
787    (page_w, page_h)
788}