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
20pub struct InputImage {
22 pub key: String,
23 pub image: DynamicImage,
24}
25
26pub struct OutputPage {
28 pub page: Page,
29 pub rgba: RgbaImage,
30}
31
32pub struct PackOutput {
34 pub atlas: Atlas,
35 pub pages: Vec<OutputPage>,
36}
37
38impl PackOutput {
39 pub fn stats(&self) -> crate::model::PackStats {
42 self.atlas.stats()
43 }
44}
45
46#[instrument(skip_all)]
47pub fn pack_images(inputs: Vec<InputImage>, cfg: PackerConfig) -> Result<PackOutput> {
54 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 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 if !remove_set.is_empty() {
187 remaining.retain(|i| !remove_set.contains(i));
188 }
189 }
190
191 if frames.is_empty() {
192 let placed = prepared.len() - remaining.len();
194 return Err(TexPackerError::OutOfSpaceGeneric {
195 placed,
196 total: prepared.len(),
197 });
198 }
199
200 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 #[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 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 let mut best: Option<(Vec<PackedPage>, u64, u32)> = None; 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
396pub fn pack_layout<K: Into<String>>(
401 inputs: Vec<(K, u32, u32)>,
402 cfg: PackerConfig,
403) -> Result<Atlas<String>> {
404 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#[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
426pub fn pack_layout_items<K: Into<String>>(
428 items: Vec<LayoutItem<K>>,
429 cfg: PackerConfig,
430) -> Result<Atlas<String>> {
431 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
442fn compute_page_size(frames: &[Frame], cfg: &PackerConfig) -> (u32, u32) {
444 PackingContext::new(cfg).compute_page_size(frames)
445}