- Rust 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| src | ||
| test_files | ||
| .gitattributes | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
packit — Sprite Atlas Packer
Упаковщик спрайтов в атласы с агрегацией в .images@<ppu>x_<fmt>.new.zip в формате референсного пайплайна ConvertAndRepack.
Формат атласов и atlas_data.jsonl совпадает с описанием из test_files/README.md.
Возможности
- Упаковка webp-изображений (по
layout.jsonl) в атласы - Форматы вывода: PNG, WebP (
webp), ETC1, ETC2 (атлас —png/webp/pvr; для ETC1 дополнительноatlas_N_alpha.pvrпри наличии прозрачности) - Рендер и кодирование изображений атласов: lossy webp (libwebp, VP8X+ALPH+VP8), ETC1/ETC2 в контейнер PVR v3 (заголовок и метадата побайтово как в референсе)
- Детерминированный zip: фиксированные временные штампы (1980-01-01), без extra-полей — повторная упаковка тех же входов даёт бит-в-бит идентичный файл
- Структура zip как в референсе: порядок записей (
webp:.sprites→.webp;etc1:.pvr→.sprites→atlas_N_alpha.pvr;etc2:.pvr→.sprites),.webp/.pngбез сжатия (stored),.sprites/.pvrdeflate, CRLF в.sprites - Результат:
.images@1x_<fmt>.new.zip+.images@2x_<fmt>.new.zip(при--input-2x),.layouts@1x|2x.new.list, маркерыrestype_uisprite.txt/unity_import_flag— как в референсе - Дедупликация по хэшу/размеру,
tiled-спрайты → singleton wrap-атласы - Сравнение с референсными данными (
test_files/atlas_data.jsonl)
Установка
cargo build --release
Использование
Упаковка папки с изображениями
cargo run --release -- pack --input test_files/input/rf_common_1x --input-2x test_files/input/rf_common_2x --format webp --out out --padding 2
Входная папка должна содержать спрайты (.webp) и layout.jsonl
({"name","x","y","width","height","hash"[,"tiled","scale"]} — источник истины).
| Флаг | Описание | По умолчанию |
|---|---|---|
--input |
Папка со спрайтами 1x + layout.jsonl |
— |
--input-2x |
Набор 2x, пакуется отдельно в .images@2x_… |
— |
--format |
Формат: png, webp, etc1, etc2 |
— |
--quality |
Качество lossy webp (0–100) | 90 |
--out |
Папка для вывода | out |
--padding |
Отступ между спрайтами в пикселях | 2 |
--max-size |
Максимальный размер атласа | 4096 |
Сравнение с референсом
cargo run --release -- compare-jsonl --limit 1954
| Флаг | Описание | По умолчанию |
|---|---|---|
--jsonl |
Путь к jsonl | test_files/atlas_data.jsonl |
--filter |
Подстрока для фильтрации ключа | — |
--limit |
Макс. строк для проверки | 2000 |
Тест rf_common
Сравнение против референсных записей с "rf" и "common" в ключе:
cargo run --release -- test-rf-common --limit 20
| Флаг | Описание | По умолчанию |
|---|---|---|
--jsonl |
Путь к jsonl | test_files/atlas_data.jsonl |
--limit |
Макс. строк для проверки | 2000 |
Формат вывода
out/
├── .images@1x_webp.new.zip # atlas_N.webp + atlas_N.sprites
├── .images@2x_webp.new.zip # при --input-2x
├── .layouts@1x.new.list # name {Position=x y; Size=w h} <sha256>
├── .layouts@2x.new.list
├── restype_uisprite.txt
└── unity_import_flag
Состав повторяет референс; никаких дополнительных jsonl-файлов на выходе нет.
Для etc1/etc2 атласы — atlas_N.pvr (etc1: + atlas_N_alpha.pvr, только если в атласе есть прозрачность), для png — atlas_N.png.
PVR v3 (etc1/etc2)
52-байтовый little-endian заголовок (version 0x03525650, flags=0, глубина/поверхности/грани/mipmaps=1) + 15-байтовая метадата из референса, затем блоки ETC:
| Формат | pixelFormat | Блок | Размер данных |
|---|---|---|---|
| etc1 (цвет и альфа) | 6 (ETC1_RGB8) | 8 байт / 4×4 | w·h/2 |
| etc2 без прозрачности | 22 (ETC2_RGB8) | 8 байт / 4×4 | w·h/2 |
| etc2 с прозрачностью | 23 (ETC2_RGBA8) | 16 байт / 4×4 | w·h |
Альфа-текстура etc1 — альфа-канал, реплицированный в RGB и закодированный как ETC1. Размерности атласов дополняются до кратности 4 (размер ETC-блока).
Детерминизм
Повторная упаковка тех же входов даёт бит-в-бит идентичный zip: у всех записей фиксированное время 1980-01-01 00:00, extra-поля не пишутся, сжатие и порядок записей зависят только от содержимого.
test_files/atlas_data.jsonl
Референсный файл с метаданными упаковки (используется командами compare-jsonl / test-rf-common, на выходе pack не создаётся):
{"key":"/abs/path/rf_common/webp","atlases":[[1024,1024]],"sprites":[[768,513]],"positions":[[0,0,0,0]]}
| Поле | Тип | Описание |
|---|---|---|
key |
string | Путь к папке (без .images_@2x.zip) |
atlases |
[[w,h],...] |
Размеры атласов |
sprites |
[[w,h],...] |
Размеры спрайтов |
positions |
[[atlas_idx, sprite_idx, x, y],...] |
Позиции спрайтов в атласах |
Алгоритм упаковки
- Дедупликация: одинаковые по размеру спрайты делят одну область атласа (как в референсе)
- MaxRects с портфолио из 4 правил размещения (Best Short/Long Side Fit, Best Area Fit, Bottom-Left)
- 13 порядков сортировки (по площади, сторонам, периметру, сторонам; в обе стороны)
- 3 стратегии score: минимальный абсолютный waste, минимальный относительный waste, максимум размещённых спрайтов
- Обрезка атласов по содержимому: точный bounding box (webp/etc2), округление до POT (etc1)
- Мульти-высотные кандидаты: для каждой ширины несколько высот (минимальная, POT, максимум) + «естественные» размеры спрайтов
- Принудительный первый атлас: перебор всех кандидатов в качестве первого (и двух первых для сложных наборов) с жадным хвостом
- Exact-поиск (backtracking) с бюджетом узлов: замыкает жадные тупики, которые MaxRects не видит (например, 8 decor-спрайтов в один 1024×4096)
- Subset DP для наборов ≤ 12 unique-спрайтов: оптимальное разбиение на атласы (3^n перебор разбиений поверх exact/greedy-упаковок подмножеств)
- Singleton-опция: по одному атласу на спрайт, если мульти-упаковка теряет площадь (как в референсе)
- Выбор минимальной суммарной площади атласов по всем комбинациям портфолио
Качество упаковки
Сравнение с test_files/atlas_data.jsonl (1954 наборов):
| Формат | Лучше | Равно | Хуже |
|---|---|---|---|
| etc1 | 226 | 257 | 0 |
| etc2 | 474 | 9 | 0 |
| webp | 98 | 396 | 0 |
| webp2x | 98 | 396 | 0 |
- 896 лучше / 1058 равно / 0 регрессий
- Отношение площадей: 0.9385 (наши атласы = 93.9% от площади референса)
- Все equal-случаи webp/webp2x достигают теоретического минимума (сумма площадей unique-спрайтов); etc1 equal — POT-минимумы (группировка не даёт выигрыша)
- rf_common тест: 10 лучше / 6 равно / 0 хуже, ratio=0.6115
Производительность
Упаковка test_files/input (204 спрайта 1x + 204 2x, release): ~6 с для png/webp/etc2, ~3 с для etc1 — практически всё время уходит на декодирование входных webp и кодирование атласов; сама укладка — десятки миллисекунд.
Зависимости
image— загрузка webp, кодирование pngwebp(libwebp) — lossy-кодирование атласов webp с альфа-каналомctt-etcpak— компрессия ETC1/ETC2clap— CLIzip— создание zip-архивовserde/serde_json— JSONLthiserror/anyhow— обработка ошибок
Сборка webp и ctt-etcpak требует C/C++-компилятора.
Структура проекта
src/
├── main.rs # CLI, Error/Result, точка входа
├── packer.rs # Алгоритм упаковки (MaxRects + exact + subset DP)
├── codec/mod.rs # Форматы (Format, RgbaImage, load_dir)
├── codec/encode.rs # Рендер атласов и кодирование (png/webp/etc1/etc2)
├── codec/pvr.rs # Контейнер PVR v3 + метадата референса
├── layout.rs # Парсинг входного layout.jsonl
├── compare.rs # Сравнение с atlas_data.jsonl
├── bundle.rs # Сборка .images@<ppu>x_<fmt>.new.zip + layouts-списков
└── sprites_file.rs # Парсинг/запись .sprites метаданных