Skip to main content

tex_packer_core/
pipeline.rs

1use crate::config::PackerConfig;
2use crate::config::{AlgorithmFamily, AutoMode};
3use crate::error::{Result, TexPackerError};
4use crate::geometry::PackingContext;
5use crate::model::{Atlas, Frame, Meta, Page, Rect};
6use crate::packer::{
7    Packer, guillotine::GuillotinePacker, maxrects::MaxRectsPacker, skyline::SkylinePacker,
8};
9use crate::preparation::{PreparedItem, prepare_images, prepare_layout, prepare_layout_items};
10use image::{DynamicImage, RgbaImage};
11use std::collections::HashSet;
12use std::time::Instant;
13use tracing::instrument;
14
15pub use crate::preparation::compute_trim_rect;
16
17#[cfg(feature = "parallel")]
18use rayon::prelude::*;
19
20/// In-memory image to pack (key + decoded image).
21pub struct InputImage {
22    pub key: String,
23    pub image: DynamicImage,
24}
25
26/// Output RGBA page and its logical page record.
27pub struct OutputPage {
28    pub page: Page,
29    pub rgba: RgbaImage,
30}
31
32/// Output of a packing run: atlas metadata and RGBA pages.
33pub struct PackOutput {
34    pub atlas: Atlas,
35    pub pages: Vec<OutputPage>,
36}
37
38impl PackOutput {
39    /// Computes packing statistics for this output.
40    /// This is a convenience method that delegates to `atlas.stats()`.
41    pub fn stats(&self) -> crate::model::PackStats {
42        self.atlas.stats()
43    }
44}
45
46#[instrument(skip_all)]
47/// Packs `inputs` into atlas pages using configuration `cfg` and returns metadata and RGBA pages.
48///
49/// Notes:
50/// - Sorting is stable for deterministic results.
51/// - When `family` is `Auto`, a small portfolio is tried and the best result is chosen (pages first, then total area).
52/// - `time_budget_ms` can limit Auto evaluation time; `parallel` may evaluate in parallel when enabled.
53pub fn pack_images(inputs: Vec<InputImage>, cfg: PackerConfig) -> Result<PackOutput> {
54    // Validate configuration first
55    cfg.validate()?;
56
57    if inputs.is_empty() {
58        return Err(TexPackerError::Empty);
59    }
60
61    let prepared = prepare_images(&inputs, &cfg);
62
63    pack_prepared(&prepared, &cfg)
64}
65
66#[derive(Clone)]
67struct PackedFrame {
68    item_index: usize,
69    frame: Frame,
70}
71
72#[derive(Clone)]
73struct PackedPage {
74    id: usize,
75    width: u32,
76    height: u32,
77    frames: Vec<PackedFrame>,
78}
79
80impl PackedPage {
81    fn public_frames(&self) -> Vec<Frame> {
82        self.frames.iter().map(|f| f.frame.clone()).collect()
83    }
84
85    fn to_page(&self) -> Page {
86        Page {
87            id: self.id,
88            width: self.width,
89            height: self.height,
90            frames: self.public_frames(),
91        }
92    }
93}
94
95struct OfflinePipeline<'a> {
96    cfg: &'a PackerConfig,
97}
98
99impl<'a> OfflinePipeline<'a> {
100    fn new(cfg: &'a PackerConfig) -> Self {
101        Self { cfg }
102    }
103
104    fn pack_images(&self, prepared: &[PreparedItem<RgbaImage>]) -> Result<PackOutput> {
105        let packed_pages = self.pack_pages(prepared)?;
106        Ok(self.build_output(prepared, &packed_pages))
107    }
108
109    fn pack_layout<T: Sync>(&self, prepared: &[PreparedItem<T>]) -> Result<Atlas> {
110        let packed_pages = self.pack_pages(prepared)?;
111        Ok(self.build_atlas(&packed_pages))
112    }
113
114    fn pack_pages<T: Sync>(&self, prepared: &[PreparedItem<T>]) -> Result<Vec<PackedPage>> {
115        if matches!(self.cfg.family, AlgorithmFamily::Auto) {
116            return pack_auto_pages(prepared, self.cfg.clone());
117        }
118
119        self.pack_pages_for_family(prepared)
120    }
121
122    fn pack_pages_for_family<T>(&self, prepared: &[PreparedItem<T>]) -> Result<Vec<PackedPage>> {
123        pack_pages_for_family(prepared, self.cfg)
124    }
125
126    fn build_output(
127        &self,
128        prepared: &[PreparedItem<RgbaImage>],
129        packed_pages: &[PackedPage],
130    ) -> PackOutput {
131        let pages = packed_pages
132            .iter()
133            .map(|packed_page| render_output_page(prepared, packed_page, self.cfg))
134            .collect();
135        let atlas = self.build_atlas(packed_pages);
136
137        PackOutput { atlas, pages }
138    }
139
140    fn build_atlas(&self, packed_pages: &[PackedPage]) -> Atlas {
141        build_atlas(packed_pages, self.cfg)
142    }
143}
144
145fn pack_prepared(prepared: &[PreparedItem<RgbaImage>], cfg: &PackerConfig) -> Result<PackOutput> {
146    OfflinePipeline::new(cfg).pack_images(prepared)
147}
148
149fn pack_pages_for_family<T>(
150    prepared: &[PreparedItem<T>],
151    cfg: &PackerConfig,
152) -> Result<Vec<PackedPage>> {
153    let mut pages: Vec<PackedPage> = Vec::new();
154    // Remaining indices to place (in sorted order)
155    let mut remaining: Vec<usize> = (0..prepared.len()).collect();
156    let mut page_id = 0usize;
157
158    while !remaining.is_empty() {
159        let mut packer = create_packer(cfg);
160        let mut frames: Vec<PackedFrame> = Vec::new();
161
162        loop {
163            let mut placed_any = false;
164            let mut remove_set: HashSet<usize> = HashSet::new();
165            for &idx in &remaining {
166                let p = &prepared[idx];
167                if !packer.can_pack(&p.rect) {
168                    continue;
169                }
170                if let Some(mut f) = packer.pack(p.key.clone(), &p.rect) {
171                    f.trimmed = p.trimmed;
172                    f.source = p.source;
173                    f.source_size = p.orig_size;
174                    frames.push(PackedFrame {
175                        item_index: idx,
176                        frame: f,
177                    });
178                    remove_set.insert(idx);
179                    placed_any = true;
180                }
181            }
182            if !placed_any {
183                break;
184            }
185            // Retain only indices not placed
186            if !remove_set.is_empty() {
187                remaining.retain(|i| !remove_set.contains(i));
188            }
189        }
190
191        if frames.is_empty() {
192            // No textures could be placed on this page - likely first texture is too large
193            let placed = prepared.len() - remaining.len();
194            return Err(TexPackerError::OutOfSpaceGeneric {
195                placed,
196                total: prepared.len(),
197            });
198        }
199
200        // Compute final page size via helper to keep logic consistent across APIs
201        let public_frames: Vec<Frame> = frames.iter().map(|f| f.frame.clone()).collect();
202        let (page_w, page_h) = compute_page_size(&public_frames, cfg);
203
204        pages.push(PackedPage {
205            id: page_id,
206            width: page_w,
207            height: page_h,
208            frames,
209        });
210        page_id += 1;
211    }
212
213    Ok(pages)
214}
215
216fn create_packer(cfg: &PackerConfig) -> Box<dyn Packer<String>> {
217    match cfg.family {
218        AlgorithmFamily::Skyline => Box::new(SkylinePacker::new(cfg.clone())),
219        AlgorithmFamily::MaxRects => {
220            Box::new(MaxRectsPacker::new(cfg.clone(), cfg.mr_heuristic.clone()))
221        }
222        AlgorithmFamily::Guillotine => Box::new(GuillotinePacker::new(
223            cfg.clone(),
224            cfg.g_choice.clone(),
225            cfg.g_split.clone(),
226        )),
227        AlgorithmFamily::Auto => unreachable!(),
228    }
229}
230
231fn render_output_page(
232    prepared: &[PreparedItem<RgbaImage>],
233    packed_page: &PackedPage,
234    cfg: &PackerConfig,
235) -> OutputPage {
236    let mut canvas = RgbaImage::new(packed_page.width, packed_page.height);
237    for packed_frame in &packed_page.frames {
238        let prep = &prepared[packed_frame.item_index];
239        let f = &packed_frame.frame;
240        let dst = crate::compositing::BlitRect::new(f.frame.x, f.frame.y, f.frame.w, f.frame.h);
241        let src = crate::compositing::BlitRect::new(
242            prep.source.x,
243            prep.source.y,
244            prep.source.w,
245            prep.source.h,
246        );
247        let options = crate::compositing::BlitOptions {
248            rotated: f.rotated,
249            extrude: cfg.texture_extrusion,
250            outlines: cfg.texture_outlines,
251        };
252        crate::compositing::blit_rgba(&prep.payload, &mut canvas, dst, src, options);
253    }
254
255    OutputPage {
256        page: packed_page.to_page(),
257        rgba: canvas,
258    }
259}
260
261fn build_atlas(packed_pages: &[PackedPage], cfg: &PackerConfig) -> Atlas {
262    let atlas_pages = packed_pages.iter().map(PackedPage::to_page).collect();
263    let meta = Meta {
264        schema_version: "1".into(),
265        app: "tex-packer".into(),
266        version: env!("CARGO_PKG_VERSION").into(),
267        format: "RGBA8888".into(),
268        scale: 1.0,
269        power_of_two: cfg.power_of_two,
270        square: cfg.square,
271        max_dim: (cfg.max_width, cfg.max_height),
272        padding: (cfg.border_padding, cfg.texture_padding),
273        extrude: cfg.texture_extrusion,
274        allow_rotation: cfg.allow_rotation,
275        trim_mode: if cfg.trim { "trim" } else { "none" }.into(),
276        background_color: None,
277    };
278
279    Atlas {
280        pages: atlas_pages,
281        meta,
282    }
283}
284
285fn total_packed_area(pages: &[PackedPage]) -> u64 {
286    pages
287        .iter()
288        .map(|p| (p.width as u64) * (p.height as u64))
289        .sum()
290}
291
292fn pack_auto_pages<T: Sync>(
293    prepared: &[PreparedItem<T>],
294    base: PackerConfig,
295) -> Result<Vec<PackedPage>> {
296    let mut candidates: Vec<PackerConfig> = Vec::new();
297    let n_inputs = prepared.len();
298    let budget_ms = base.time_budget_ms.unwrap_or(0);
299    let thr_time = base.auto_mr_ref_time_ms_threshold.unwrap_or(200);
300    let thr_inputs = base.auto_mr_ref_input_threshold.unwrap_or(800);
301    let enable_mr_ref = matches!(base.auto_mode, AutoMode::Quality)
302        && (budget_ms >= thr_time || n_inputs >= thr_inputs);
303    match base.auto_mode {
304        AutoMode::Fast => {
305            let mut s_bl = base.clone();
306            s_bl.family = AlgorithmFamily::Skyline;
307            s_bl.skyline_heuristic = crate::config::SkylineHeuristic::BottomLeft;
308            candidates.push(s_bl);
309            let mut mr_baf = base.clone();
310            mr_baf.family = AlgorithmFamily::MaxRects;
311            mr_baf.mr_heuristic = crate::config::MaxRectsHeuristic::BestAreaFit;
312            mr_baf.mr_reference = false;
313            candidates.push(mr_baf);
314        }
315        AutoMode::Quality => {
316            let mut s_mw = base.clone();
317            s_mw.family = AlgorithmFamily::Skyline;
318            s_mw.skyline_heuristic = crate::config::SkylineHeuristic::MinWaste;
319            candidates.push(s_mw);
320            let mut mr_baf = base.clone();
321            mr_baf.family = AlgorithmFamily::MaxRects;
322            mr_baf.mr_heuristic = crate::config::MaxRectsHeuristic::BestAreaFit;
323            mr_baf.mr_reference = enable_mr_ref;
324            candidates.push(mr_baf);
325            let mut mr_bl = base.clone();
326            mr_bl.family = AlgorithmFamily::MaxRects;
327            mr_bl.mr_heuristic = crate::config::MaxRectsHeuristic::BottomLeft;
328            mr_bl.mr_reference = enable_mr_ref;
329            candidates.push(mr_bl);
330            let mut mr_cp = base.clone();
331            mr_cp.family = AlgorithmFamily::MaxRects;
332            mr_cp.mr_heuristic = crate::config::MaxRectsHeuristic::ContactPoint;
333            mr_cp.mr_reference = enable_mr_ref;
334            candidates.push(mr_cp);
335            let mut g = base.clone();
336            g.family = AlgorithmFamily::Guillotine;
337            g.g_choice = crate::config::GuillotineChoice::BestAreaFit;
338            g.g_split = crate::config::GuillotineSplit::SplitShorterLeftoverAxis;
339            candidates.push(g);
340        }
341    }
342    let start = Instant::now();
343
344    // Parallel path (optional)
345    #[cfg(feature = "parallel")]
346    {
347        if base.parallel {
348            let results: Vec<(Vec<PackedPage>, u64, u32)> = candidates
349                .par_iter()
350                .filter_map(|cand| pack_pages_for_family(prepared, cand).ok())
351                .map(|pages| {
352                    let page_count = pages.len() as u32;
353                    let total_area = total_packed_area(&pages);
354                    (pages, total_area, page_count)
355                })
356                .collect();
357            let best = results.into_iter().min_by(|a, b| match a.2.cmp(&b.2) {
358                // pages asc
359                std::cmp::Ordering::Equal => a.1.cmp(&b.1),
360                other => other,
361            });
362            return best.map(|x| x.0).ok_or(TexPackerError::OutOfSpaceGeneric {
363                placed: 0,
364                total: prepared.len(),
365            });
366        }
367    }
368
369    // Sequential path with optional time budget
370    let mut best: Option<(Vec<PackedPage>, u64, u32)> = None; // (pages, total_area, page count)
371    for cand in candidates.into_iter() {
372        if budget_ms > 0 && start.elapsed().as_millis() as u64 > budget_ms {
373            break;
374        }
375        if let Ok(packed_pages) = pack_pages_for_family(prepared, &cand) {
376            let pages = packed_pages.len() as u32;
377            let total_area = total_packed_area(&packed_pages);
378            match &mut best {
379                None => best = Some((packed_pages, total_area, pages)),
380                Some((bo, barea, bpages)) => {
381                    if pages < *bpages || (pages == *bpages && total_area < *barea) {
382                        *bo = packed_pages;
383                        *barea = total_area;
384                        *bpages = pages;
385                    }
386                }
387            }
388        }
389    }
390    best.map(|x| x.0).ok_or(TexPackerError::OutOfSpaceGeneric {
391        placed: 0,
392        total: prepared.len(),
393    })
394}
395
396// ---------------- Layout-only API ----------------
397
398/// Packs sizes into pages without compositing pixel data.
399/// Inputs are (key, width, height). Returns an Atlas with pages and frames; no RGBA pages.
400pub fn pack_layout<K: Into<String>>(
401    inputs: Vec<(K, u32, u32)>,
402    cfg: PackerConfig,
403) -> Result<Atlas<String>> {
404    // Validate configuration first
405    cfg.validate()?;
406
407    if inputs.is_empty() {
408        return Err(TexPackerError::Empty);
409    }
410    let prepared = prepare_layout(inputs, &cfg);
411
412    OfflinePipeline::new(&cfg).pack_layout(&prepared)
413}
414
415/// Layout-only item with optional source/source_size to propagate trimming metadata.
416#[derive(Debug, Clone)]
417pub struct LayoutItem<K = String> {
418    pub key: K,
419    pub w: u32,
420    pub h: u32,
421    pub source: Option<Rect>,
422    pub source_size: Option<(u32, u32)>,
423    pub trimmed: bool,
424}
425
426/// Packs layout-only items (with optional source/source_size metadata) into pages.
427pub fn pack_layout_items<K: Into<String>>(
428    items: Vec<LayoutItem<K>>,
429    cfg: PackerConfig,
430) -> Result<Atlas<String>> {
431    // Validate configuration first
432    cfg.validate()?;
433
434    if items.is_empty() {
435        return Err(TexPackerError::Empty);
436    }
437    let prepared = prepare_layout_items(items, &cfg);
438
439    OfflinePipeline::new(&cfg).pack_layout(&prepared)
440}
441
442/// Compute final page dimensions given placed frames and config.
443fn compute_page_size(frames: &[Frame], cfg: &PackerConfig) -> (u32, u32) {
444    PackingContext::new(cfg).compute_page_size(frames)
445}