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
16pub struct InputImage {
18 pub key: String,
19 pub image: DynamicImage,
20}
21
22pub struct OutputPage {
24 pub page: Page,
25 pub rgba: RgbaImage,
26}
27
28pub struct PackOutput {
30 pub atlas: Atlas,
31 pub pages: Vec<OutputPage>,
32}
33
34impl PackOutput {
35 pub fn stats(&self) -> crate::model::PackStats {
38 self.atlas.stats()
39 }
40}
41
42#[instrument(skip_all)]
43pub fn pack_images(inputs: Vec<InputImage>, cfg: PackerConfig) -> Result<PackOutput> {
50 cfg.validate()?;
52
53 if inputs.is_empty() {
54 return Err(TexPackerError::Empty);
55 }
56
57 let prepared = prepare_inputs(&inputs, &cfg);
59
60 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 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 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 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 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)]
156struct 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 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 let prep_map: HashMap<String, &Prep> = prepared.iter().map(|p| (p.key.clone(), p)).collect();
245
246 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 if !remove_set.is_empty() {
287 remaining.retain(|i| !remove_set.contains(i));
288 }
289 }
290
291 if frames.is_empty() {
292 let placed = prepared.len() - remaining.len();
294 return Err(TexPackerError::OutOfSpaceGeneric {
295 placed,
296 total: prepared.len(),
297 });
298 }
299
300 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 #[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 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 let mut best: Option<(PackOutput, u64, u32)> = None; 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
468pub fn pack_layout<K: Into<String>>(
473 inputs: Vec<(K, u32, u32)>,
474 cfg: PackerConfig,
475) -> Result<Atlas<String>> {
476 cfg.validate()?;
478
479 if inputs.is_empty() {
480 return Err(TexPackerError::Empty);
481 }
482 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 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 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#[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
623pub fn pack_layout_items<K: Into<String>>(
625 items: Vec<LayoutItem<K>>,
626 cfg: PackerConfig,
627) -> Result<Atlas<String>> {
628 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
762fn compute_page_size(frames: &[Frame], cfg: &PackerConfig) -> (u32, u32) {
764 if cfg.force_max_dimensions {
765 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}