use crate::config::PackerConfig;
use crate::config::{AlgorithmFamily, AutoMode};
use crate::error::{Result, TexPackerError};
use crate::geometry::PackingContext;
use crate::model::{Atlas, Frame, Meta, Page, Rect};
use crate::packer::{
Packer, guillotine::GuillotinePacker, maxrects::MaxRectsPacker, skyline::SkylinePacker,
};
use crate::preparation::{PreparedItem, prepare_images, prepare_layout, prepare_layout_items};
use image::{DynamicImage, RgbaImage};
use std::collections::HashSet;
use std::time::Instant;
use tracing::instrument;
pub use crate::preparation::compute_trim_rect;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
pub struct InputImage {
pub key: String,
pub image: DynamicImage,
}
pub struct OutputPage {
pub page: Page,
pub rgba: RgbaImage,
}
pub struct PackOutput {
pub atlas: Atlas,
pub pages: Vec<OutputPage>,
}
impl PackOutput {
pub fn stats(&self) -> crate::model::PackStats {
self.atlas.stats()
}
}
#[instrument(skip_all)]
pub fn pack_images(inputs: Vec<InputImage>, cfg: PackerConfig) -> Result<PackOutput> {
cfg.validate()?;
if inputs.is_empty() {
return Err(TexPackerError::Empty);
}
let prepared = prepare_images(&inputs, &cfg);
pack_prepared(&prepared, &cfg)
}
#[derive(Clone)]
struct PackedFrame {
item_index: usize,
frame: Frame,
}
#[derive(Clone)]
struct PackedPage {
id: usize,
width: u32,
height: u32,
frames: Vec<PackedFrame>,
}
impl PackedPage {
fn public_frames(&self) -> Vec<Frame> {
self.frames.iter().map(|f| f.frame.clone()).collect()
}
fn to_page(&self) -> Page {
Page {
id: self.id,
width: self.width,
height: self.height,
frames: self.public_frames(),
}
}
}
struct OfflinePipeline<'a> {
cfg: &'a PackerConfig,
}
impl<'a> OfflinePipeline<'a> {
fn new(cfg: &'a PackerConfig) -> Self {
Self { cfg }
}
fn pack_images(&self, prepared: &[PreparedItem<RgbaImage>]) -> Result<PackOutput> {
let packed_pages = self.pack_pages(prepared)?;
Ok(self.build_output(prepared, &packed_pages))
}
fn pack_layout<T: Sync>(&self, prepared: &[PreparedItem<T>]) -> Result<Atlas> {
let packed_pages = self.pack_pages(prepared)?;
Ok(self.build_atlas(&packed_pages))
}
fn pack_pages<T: Sync>(&self, prepared: &[PreparedItem<T>]) -> Result<Vec<PackedPage>> {
if matches!(self.cfg.family, AlgorithmFamily::Auto) {
return pack_auto_pages(prepared, self.cfg.clone());
}
self.pack_pages_for_family(prepared)
}
fn pack_pages_for_family<T>(&self, prepared: &[PreparedItem<T>]) -> Result<Vec<PackedPage>> {
pack_pages_for_family(prepared, self.cfg)
}
fn build_output(
&self,
prepared: &[PreparedItem<RgbaImage>],
packed_pages: &[PackedPage],
) -> PackOutput {
let pages = packed_pages
.iter()
.map(|packed_page| render_output_page(prepared, packed_page, self.cfg))
.collect();
let atlas = self.build_atlas(packed_pages);
PackOutput { atlas, pages }
}
fn build_atlas(&self, packed_pages: &[PackedPage]) -> Atlas {
build_atlas(packed_pages, self.cfg)
}
}
fn pack_prepared(prepared: &[PreparedItem<RgbaImage>], cfg: &PackerConfig) -> Result<PackOutput> {
OfflinePipeline::new(cfg).pack_images(prepared)
}
fn pack_pages_for_family<T>(
prepared: &[PreparedItem<T>],
cfg: &PackerConfig,
) -> Result<Vec<PackedPage>> {
let mut pages: Vec<PackedPage> = Vec::new();
let mut remaining: Vec<usize> = (0..prepared.len()).collect();
let mut page_id = 0usize;
while !remaining.is_empty() {
let mut packer = create_packer(cfg);
let mut frames: Vec<PackedFrame> = Vec::new();
loop {
let mut placed_any = false;
let mut remove_set: HashSet<usize> = HashSet::new();
for &idx in &remaining {
let p = &prepared[idx];
if !packer.can_pack(&p.rect) {
continue;
}
if let Some(mut f) = packer.pack(p.key.clone(), &p.rect) {
f.trimmed = p.trimmed;
f.source = p.source;
f.source_size = p.orig_size;
frames.push(PackedFrame {
item_index: idx,
frame: f,
});
remove_set.insert(idx);
placed_any = true;
}
}
if !placed_any {
break;
}
if !remove_set.is_empty() {
remaining.retain(|i| !remove_set.contains(i));
}
}
if frames.is_empty() {
let placed = prepared.len() - remaining.len();
return Err(TexPackerError::OutOfSpaceGeneric {
placed,
total: prepared.len(),
});
}
let public_frames: Vec<Frame> = frames.iter().map(|f| f.frame.clone()).collect();
let (page_w, page_h) = compute_page_size(&public_frames, cfg);
pages.push(PackedPage {
id: page_id,
width: page_w,
height: page_h,
frames,
});
page_id += 1;
}
Ok(pages)
}
fn create_packer(cfg: &PackerConfig) -> Box<dyn Packer<String>> {
match cfg.family {
AlgorithmFamily::Skyline => Box::new(SkylinePacker::new(cfg.clone())),
AlgorithmFamily::MaxRects => {
Box::new(MaxRectsPacker::new(cfg.clone(), cfg.mr_heuristic.clone()))
}
AlgorithmFamily::Guillotine => Box::new(GuillotinePacker::new(
cfg.clone(),
cfg.g_choice.clone(),
cfg.g_split.clone(),
)),
AlgorithmFamily::Auto => unreachable!(),
}
}
fn render_output_page(
prepared: &[PreparedItem<RgbaImage>],
packed_page: &PackedPage,
cfg: &PackerConfig,
) -> OutputPage {
let mut canvas = RgbaImage::new(packed_page.width, packed_page.height);
for packed_frame in &packed_page.frames {
let prep = &prepared[packed_frame.item_index];
let f = &packed_frame.frame;
let dst = crate::compositing::BlitRect::new(f.frame.x, f.frame.y, f.frame.w, f.frame.h);
let src = crate::compositing::BlitRect::new(
prep.source.x,
prep.source.y,
prep.source.w,
prep.source.h,
);
let options = crate::compositing::BlitOptions {
rotated: f.rotated,
extrude: cfg.texture_extrusion,
outlines: cfg.texture_outlines,
};
crate::compositing::blit_rgba(&prep.payload, &mut canvas, dst, src, options);
}
OutputPage {
page: packed_page.to_page(),
rgba: canvas,
}
}
fn build_atlas(packed_pages: &[PackedPage], cfg: &PackerConfig) -> Atlas {
let atlas_pages = packed_pages.iter().map(PackedPage::to_page).collect();
let meta = Meta {
schema_version: "1".into(),
app: "tex-packer".into(),
version: env!("CARGO_PKG_VERSION").into(),
format: "RGBA8888".into(),
scale: 1.0,
power_of_two: cfg.power_of_two,
square: cfg.square,
max_dim: (cfg.max_width, cfg.max_height),
padding: (cfg.border_padding, cfg.texture_padding),
extrude: cfg.texture_extrusion,
allow_rotation: cfg.allow_rotation,
trim_mode: if cfg.trim { "trim" } else { "none" }.into(),
background_color: None,
};
Atlas {
pages: atlas_pages,
meta,
}
}
fn total_packed_area(pages: &[PackedPage]) -> u64 {
pages
.iter()
.map(|p| (p.width as u64) * (p.height as u64))
.sum()
}
fn pack_auto_pages<T: Sync>(
prepared: &[PreparedItem<T>],
base: PackerConfig,
) -> Result<Vec<PackedPage>> {
let mut candidates: Vec<PackerConfig> = Vec::new();
let n_inputs = prepared.len();
let budget_ms = base.time_budget_ms.unwrap_or(0);
let thr_time = base.auto_mr_ref_time_ms_threshold.unwrap_or(200);
let thr_inputs = base.auto_mr_ref_input_threshold.unwrap_or(800);
let enable_mr_ref = matches!(base.auto_mode, AutoMode::Quality)
&& (budget_ms >= thr_time || n_inputs >= thr_inputs);
match base.auto_mode {
AutoMode::Fast => {
let mut s_bl = base.clone();
s_bl.family = AlgorithmFamily::Skyline;
s_bl.skyline_heuristic = crate::config::SkylineHeuristic::BottomLeft;
candidates.push(s_bl);
let mut mr_baf = base.clone();
mr_baf.family = AlgorithmFamily::MaxRects;
mr_baf.mr_heuristic = crate::config::MaxRectsHeuristic::BestAreaFit;
mr_baf.mr_reference = false;
candidates.push(mr_baf);
}
AutoMode::Quality => {
let mut s_mw = base.clone();
s_mw.family = AlgorithmFamily::Skyline;
s_mw.skyline_heuristic = crate::config::SkylineHeuristic::MinWaste;
candidates.push(s_mw);
let mut mr_baf = base.clone();
mr_baf.family = AlgorithmFamily::MaxRects;
mr_baf.mr_heuristic = crate::config::MaxRectsHeuristic::BestAreaFit;
mr_baf.mr_reference = enable_mr_ref;
candidates.push(mr_baf);
let mut mr_bl = base.clone();
mr_bl.family = AlgorithmFamily::MaxRects;
mr_bl.mr_heuristic = crate::config::MaxRectsHeuristic::BottomLeft;
mr_bl.mr_reference = enable_mr_ref;
candidates.push(mr_bl);
let mut mr_cp = base.clone();
mr_cp.family = AlgorithmFamily::MaxRects;
mr_cp.mr_heuristic = crate::config::MaxRectsHeuristic::ContactPoint;
mr_cp.mr_reference = enable_mr_ref;
candidates.push(mr_cp);
let mut g = base.clone();
g.family = AlgorithmFamily::Guillotine;
g.g_choice = crate::config::GuillotineChoice::BestAreaFit;
g.g_split = crate::config::GuillotineSplit::SplitShorterLeftoverAxis;
candidates.push(g);
}
}
let start = Instant::now();
#[cfg(feature = "parallel")]
{
if base.parallel {
let results: Vec<(Vec<PackedPage>, u64, u32)> = candidates
.par_iter()
.filter_map(|cand| pack_pages_for_family(prepared, cand).ok())
.map(|pages| {
let page_count = pages.len() as u32;
let total_area = total_packed_area(&pages);
(pages, total_area, page_count)
})
.collect();
let best = results.into_iter().min_by(|a, b| match a.2.cmp(&b.2) {
std::cmp::Ordering::Equal => a.1.cmp(&b.1),
other => other,
});
return best.map(|x| x.0).ok_or(TexPackerError::OutOfSpaceGeneric {
placed: 0,
total: prepared.len(),
});
}
}
let mut best: Option<(Vec<PackedPage>, u64, u32)> = None; for cand in candidates.into_iter() {
if budget_ms > 0 && start.elapsed().as_millis() as u64 > budget_ms {
break;
}
if let Ok(packed_pages) = pack_pages_for_family(prepared, &cand) {
let pages = packed_pages.len() as u32;
let total_area = total_packed_area(&packed_pages);
match &mut best {
None => best = Some((packed_pages, total_area, pages)),
Some((bo, barea, bpages)) => {
if pages < *bpages || (pages == *bpages && total_area < *barea) {
*bo = packed_pages;
*barea = total_area;
*bpages = pages;
}
}
}
}
}
best.map(|x| x.0).ok_or(TexPackerError::OutOfSpaceGeneric {
placed: 0,
total: prepared.len(),
})
}
pub fn pack_layout<K: Into<String>>(
inputs: Vec<(K, u32, u32)>,
cfg: PackerConfig,
) -> Result<Atlas<String>> {
cfg.validate()?;
if inputs.is_empty() {
return Err(TexPackerError::Empty);
}
let prepared = prepare_layout(inputs, &cfg);
OfflinePipeline::new(&cfg).pack_layout(&prepared)
}
#[derive(Debug, Clone)]
pub struct LayoutItem<K = String> {
pub key: K,
pub w: u32,
pub h: u32,
pub source: Option<Rect>,
pub source_size: Option<(u32, u32)>,
pub trimmed: bool,
}
pub fn pack_layout_items<K: Into<String>>(
items: Vec<LayoutItem<K>>,
cfg: PackerConfig,
) -> Result<Atlas<String>> {
cfg.validate()?;
if items.is_empty() {
return Err(TexPackerError::Empty);
}
let prepared = prepare_layout_items(items, &cfg);
OfflinePipeline::new(&cfg).pack_layout(&prepared)
}
fn compute_page_size(frames: &[Frame], cfg: &PackerConfig) -> (u32, u32) {
PackingContext::new(cfg).compute_page_size(frames)
}