Technical reference for contributors building or extending TextureAtlas Toolbox. This document covers architecture, real code patterns, and copy-pasteable examples for parsers, exporters, packers, and animation formats.
Note: This doc is kept in sync with the source. If something looks outdated, open an issue or PR.
- Prerequisites
- Project Layout
- Architecture Overview
- Core Contracts
- Extension Guides
- Debug and Testing
- Contribution Checklist
| Requirement | Notes |
|---|---|
| Python 3.14+ | Required; earlier versions may work but are unsupported. |
| Git | For version control. |
| Pillow | Core image handling; installed via requirements. |
| Wand + ImageMagick | GIF export with advanced features (quantization, duplicate removal). |
| PySide6 | Qt 6 bindings for GUI; runtime included. |
| etcpak | GPU texture compression (BC1/BC3/ETC); installed via requirements. |
| qt-material | Material Design theme support; installed via requirements. |
| qtawesome | Icon font integration (fallback icons); installed via requirements. |
git clone https://github.com/MeguminBOT/TextureAtlas-to-GIF-and-Frames.git
cd TextureAtlas-to-GIF-and-Frames
python -m venv .venv
# Windows (bash): source .venv/Scripts/activate
# macOS/Linux: source .venv/bin/activate
pip install -r setup/requirements.txt
# Optional bleeding-edge stack:
# pip install -r setup/requirements-experimental.txtRun the test suite:
pytest tests/We don't enforce linting globally yet, but prefer black and ruff locally.
src/
├── Main.py # Application entry point and GUI wiring
├── version.py # APP_VERSION constant
├── core/
│ ├── extractor/ # Extraction pipeline classes
│ │ ├── extractor.py # Multi-threaded batch orchestrator
│ │ ├── atlas_processor.py # Atlas image loading
│ │ ├── sprite_processor.py # Sprite grouping
│ │ ├── animation_processor.py # Animation dispatch
│ │ ├── animation_exporter.py # GIF/WebP/APNG export
│ │ ├── frame_pipeline.py # Frame normalization & selection
│ │ ├── frame_selector.py # Duplicate detection
│ │ ├── frame_exporter.py # Static frame export
│ │ ├── preview_generator.py # Animation preview helper
│ │ └── image_utils.py # Low-level NumPy/Pillow helpers
│ ├── generator/
│ │ └── atlas_generator.py # Full generation pipeline
│ ├── optimizer/ # Image optimization and GPU compression
│ │ ├── constants.py # Enums, labels, OptimizeOptions
│ │ ├── optimizer.py # PNG optimization pipeline
│ │ └── texture_compress.py # GPU texture compression engine
│ └── editor/ # Visual editor components
├── parsers/
│ ├── base_parser.py # Abstract base parser
│ ├── parser_registry.py # Auto-detection registry
│ ├── parser_types.py # SpriteData, ParseResult, ParserError
│ └── *_parser.py # Concrete format parsers
├── exporters/
│ ├── base_exporter.py # Abstract base exporter
│ ├── exporter_registry.py# Format registry
│ ├── exporter_types.py # ExportOptions, ExportResult, errors
│ └── *_exporter.py # Concrete format exporters
├── packers/
│ ├── base_packer.py # Abstract base packer
│ ├── packer_registry.py # Algorithm registry
│ ├── packer_types.py # FrameInput, PackedFrame, PackerOptions
│ ├── maxrects_packer.py # MaxRects bin-packing
│ ├── guillotine_packer.py# Guillotine packing
│ ├── shelf_packer.py # Shelf packing
│ └── skyline_packer.py # Skyline packing
├── gui/ # PySide6 widgets and windows
├── utils/
│ ├── app_config.py # Persistent JSON config
│ ├── settings_manager.py # Hierarchical settings
│ ├── utilities.py # General helpers
│ ├── resampling.py # Resize filter utilities
│ └── FNF/ # Friday Night Funkin' engine support
└── translations/ # Localization files
┌────────────────────────────────────────────────────────────────────────────┐
│ GUI Layer (PySide6) │
│ Main.py · extract_tab_widget · generate_tab_widget · editor_tab │
│ optimize_tab_widget │
└───────────────────────────────┬────────────────────────────────────────────┘
│
┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ Extraction │ │ Generation │ │ Editor │
│ Pipeline │ │ Pipeline │ │ (optional) │
└───────┬────────┘ └─────────┬──────────┘ └────────────────────┘
│ │
▼ ▼
┌────────────────┐ ┌────────────────────┐
│ Parsers │ │ Packers │
│ (read metadata)│ │ (layout algorithm) │
└───────┬────────┘ └─────────┬──────────┘
│ │
│ ▼
│ ┌────────────────────┐
│ │ Exporters │
│ │ (write metadata) │
│ └─────────┬──────────┘
│ │
│ ▼
│ ┌────────────────────┐
│ │ TextureCompressor │
│ │ (GPU compression) │
│ └────────────────────┘
▼
┌────────────────┐
│AnimationExporter│
│ (GIF/WebP/APNG) │
└────────────────┘
Extraction reads an existing atlas image + metadata file, groups frames into animations, and writes GIF/WebP/APNG (and optionally static frames).
Generation takes individual images, packs them using a bin-packing algorithm, composites an atlas image, and writes metadata in any supported format.
Keep these pipelines separate: parsers are input-only; exporters are output-only.
Location: src/parsers/base_parser.py
All extraction-side parsers inherit from BaseParser. The base class handles
file loading boilerplate and delegates format-specific logic to subclasses.
Contract:
| Member | Description |
|---|---|
FILE_EXTENSIONS |
Tuple of supported extensions, e.g. (".json",). |
extract_names() |
Return Set[str] of animation/sprite names for UI population. |
parse_file(cls, path) |
Class method returning ParseResult with sprites, warnings, errors. |
can_parse(cls, path) |
Optional override for content-based detection beyond extension. |
SpriteData dict (canonical format):
{
"name": str, # Required: sprite/frame identifier
"x": int, # Required: X position in atlas
"y": int, # Required: Y position in atlas
"width": int, # Required: sprite width
"height": int, # Required: sprite height
"frameX": int, # Optional: offset for trimmed sprites (default 0)
"frameY": int, # Optional: offset for trimmed sprites (default 0)
"frameWidth": int, # Optional: original width before trim (default = width)
"frameHeight": int, # Optional: original height before trim (default = height)
"rotated": bool, # Optional: 90° clockwise rotation (default False)
}Minimal parser example:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Parser for a custom JSON spritesheet format."""
from __future__ import annotations
import json
from typing import Any, Dict, List, Set
from parsers.base_parser import BaseParser
from parsers.parser_registry import ParserRegistry
from parsers.parser_types import ParseResult
from utils.utilities import Utilities
@ParserRegistry.register
class MyFormatParser(BaseParser):
"""Parse my custom JSON spritesheet format."""
FILE_EXTENSIONS = (".myjson",)
def extract_names(self) -> Set[str]:
"""Extract unique animation base names (for UI lists)."""
with open(self.file_path, "r", encoding="utf-8") as f:
data = json.load(f)
return {
Utilities.strip_trailing_digits(frame.get("name", ""))
for frame in data.get("frames", [])
if frame.get("name")
}
@classmethod
def parse_file(cls, file_path: str) -> ParseResult:
"""Parse file and return sprites with error handling."""
raw_sprites = cls.parse_json_data(file_path)
return cls.validate_sprites(raw_sprites, file_path)
@staticmethod
def parse_json_data(file_path: str) -> List[Dict[str, Any]]:
"""Load JSON and convert to canonical sprite dicts."""
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
sprites = []
for frame in data.get("frames", []):
sprites.append({
"name": frame["name"],
"x": int(frame["x"]),
"y": int(frame["y"]),
"width": int(frame["w"]),
"height": int(frame["h"]),
"frameX": int(frame.get("fx", 0)),
"frameY": int(frame.get("fy", 0)),
"frameWidth": int(frame.get("fw", frame["w"])),
"frameHeight": int(frame.get("fh", frame["h"])),
"rotated": bool(frame.get("rotated", False)),
})
return spritesThe @ParserRegistry.register decorator automatically adds the parser to the
registry. Auto-detection in ParserRegistry.detect_parser() uses extension
matching and content sniffing for ambiguous formats like .json and .xml.
Location: src/parsers/parser_registry.py
Central hub for parser discovery and auto-detection.
| Method | Description |
|---|---|
@register |
Decorator to register a parser class. |
detect_parser(file_path) |
Return the best parser for a file (extension + content). |
parse_file(file_path) |
Detect parser and parse in one call; returns ParseResult. |
get_parsers_for_extension(ext) |
List all parsers handling a given extension. |
Auto-detection for .json files:
The registry inspects JSON structure to distinguish Aseprite, JSON-Hash, JSON-Array, Phaser3, Egret2D, Godot Atlas, and Adobe Animate spritemap formats.
Location: src/exporters/base_exporter.py
All generator-side exporters inherit from BaseExporter. The base class
provides packing (basic shelf algorithm), atlas compositing, and file writing.
Subclasses only need to implement metadata serialization.
Contract:
| Member | Description |
|---|---|
FILE_EXTENSION |
Output extension, e.g. ".json". |
FORMAT_NAME |
Display name, e.g. "json-hash". |
build_metadata(...) |
Return metadata as str or bytes. |
export_file(sprites, images, path) |
Main entry point; returns ExportResult. |
Minimal exporter example:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Exporter for a custom atlas metadata format."""
from __future__ import annotations
import json
from typing import Any, Dict, List, Optional, Union
from exporters.base_exporter import BaseExporter
from exporters.exporter_registry import ExporterRegistry
from exporters.exporter_types import ExportOptions, GeneratorMetadata, PackedSprite
@ExporterRegistry.register
class MyFormatExporter(BaseExporter):
"""Export sprites to my custom metadata format."""
FILE_EXTENSION = ".myfmt"
FORMAT_NAME = "my-format"
def build_metadata(
self,
packed_sprites: List[PackedSprite],
atlas_width: int,
atlas_height: int,
image_name: str,
generator_metadata: Optional[GeneratorMetadata] = None,
) -> Union[str, bytes]:
frames: Dict[str, Any] = {}
for packed in packed_sprites:
frames[packed.name] = {
"x": packed.atlas_x,
"y": packed.atlas_y,
"w": packed.sprite["width"],
"h": packed.sprite["height"],
"rotated": packed.rotated,
}
output = {
"image": image_name,
"size": {"w": atlas_width, "h": atlas_height},
"frames": frames,
}
indent = 4 if self.options.pretty_print else None
return json.dumps(output, indent=indent, ensure_ascii=False)After creating a new exporter, import it in ExporterRegistry.initialize() so
registration runs at startup.
Location: src/exporters/exporter_registry.py
| Method | Description |
|---|---|
@register |
Decorator to register an exporter class. |
get_exporter(format_name) |
Lookup by FORMAT_NAME or extension. |
export_file(..., format_name) |
One-call export using the matched exporter. |
get_all_formats() |
List registered format names. |
initialize() |
Import all exporter modules; call once at startup. |
Location: src/packers/base_packer.py
All packing algorithms inherit from BasePacker. The base class handles
preprocessing (sorting, validation), atlas sizing, and result building.
Subclasses implement the core layout logic.
Contract:
| Member | Description |
|---|---|
ALGORITHM_NAME |
Unique key, e.g. "maxrects". |
DISPLAY_NAME |
Human-readable name for UI. |
SUPPORTED_HEURISTICS |
List of (key, display_name) tuples. |
_pack_internal(frames, w, h) |
Core algorithm; return List[PackedFrame]. |
pack(frames) |
Entry point; handles expansion and returns PackerResult. |
Available packers:
| Algorithm | Heuristics |
|---|---|
maxrects |
BSSF, BLSF, BAF, BL, CP |
guillotine |
BSSF/BLSF/BAF/WAF × split strategies |
shelf |
NEXT_FIT, FIRST_FIT, BEST_WIDTH/HEIGHT_FIT |
skyline |
BOTTOM_LEFT, MIN_WASTE, BEST_FIT |
PackerOptions controls padding, max size, power-of-two, rotation, etc.
Location: src/packers/packer_registry.py
| Method | Description |
|---|---|
@register |
Decorator to register a packer class. |
get_packer(algorithm) |
Return instantiated packer. |
pack(algorithm, frames) |
Convenience wrapper. |
get_all_algorithms() |
List registered algorithm names. |
Location: src/core/extractor/animation_exporter.py
Handles GIF, WebP, and APNG export from extracted frame sequences.
Key methods:
| Method | Description |
|---|---|
save_animations() |
Dispatcher based on settings["animation_format"]. |
save_gif() |
Uses Wand/ImageMagick for quantization and duplicate removal. |
save_webp() |
Pillow lossless animated WebP. |
save_apng() |
Pillow APNG with metadata. |
Helper utilities in frame_pipeline.py:
prepare_scaled_sequence()– scale and crop frames.build_frame_durations()– compute per-frame timing from fps/delay/period.compute_shared_bbox()– union bounding box for cropping.
Adding a new animation format:
- Add a branch in
save_animations()and implementsave_<format>(). - Update UI combo boxes in
extract_tab_widget.pyandanimation_format_mapinMain.py. - Map the format to a file extension in
preview_generator.py::_preview_extension_for_format.
Example skeleton:
def save_myformat(self, images, filename, fps, delay, period, scale, settings):
final_images = prepare_scaled_sequence(
images, self.scale_image, scale, settings.get("crop_option")
)
if not final_images:
return
durations = build_frame_durations(
len(final_images), fps, delay, period, settings.get("var_delay", False)
)
if not durations:
return
out_path = os.path.join(self.output_dir, f"{filename}.myfmt")
# Encode final_images with durations using your library
print(f"Saved MyFormat animation: {out_path}")Location: src/utils/settings_manager.py
Three-tier hierarchy: global → spritesheet → animation. When retrieving settings, values merge with later tiers taking precedence.
| Method | Description |
|---|---|
set_global_settings(**kwargs) |
Update global defaults. |
set_spritesheet_settings(name, **kw) |
Override for a specific spritesheet. |
set_animation_settings(name, **kw) |
Override for a specific animation. |
get_settings(filename, animation_name) |
Return merged dict with all overrides. |
Location: src/utils/app_config.py
Persistent JSON-backed configuration. Settings are validated against
TYPE_MAP on load/save.
| Attribute | Description |
|---|---|
DEFAULTS |
Nested dict of default values. |
TYPE_MAP |
Flat dict mapping setting keys to Python types. |
To add a new persistent setting:
- Add default value to
DEFAULTS(usually underextraction_defaults). - Add type to
TYPE_MAP, e.g."my_flag": bool. - Access via
app_config.get("extraction_defaults")["my_flag"].
- Create a file in
src/parsers/, e.g.my_format_parser.py. - Subclass
BaseParser, defineFILE_EXTENSIONS, implementextract_names(). - Implement
parse_file()(or a legacyparse_<type>_data()static method). - Decorate with
@ParserRegistry.register. - If the extension is ambiguous (e.g.
.json), add content-detection logic inParserRegistry._detect_json_parser().
- Create a file in
src/exporters/, e.g.my_format_exporter.py. - Subclass
BaseExporter, defineFILE_EXTENSIONandFORMAT_NAME. - Implement
build_metadata(). - Decorate with
@ExporterRegistry.register. - Import your module in
ExporterRegistry.initialize(). - If the UI lists formats explicitly (e.g. combo boxes), add the display name.
- Create a file in
src/packers/, e.g.my_packer.py. - Subclass
BasePacker, defineALGORITHM_NAME,DISPLAY_NAME, optionallySUPPORTED_HEURISTICS. - Implement
_pack_internal(frames, width, height)returningList[PackedFrame]. - Decorate with
@PackerRegistry.register(viaregister_packerconvenience). - Import in
packers/__init__.pyif needed.
- Add a branch in
AnimationExporter.save_animations(). - Implement
save_<format>()following the existing patterns. - Update
animation_format_combobox.addItemsinextract_tab_widget.py. - Extend
animation_format_mapinMain.py. - Map the format extension in
preview_generator.py.
- Extend
utils/FNF/engine_detector.pywith_is_<engine>(). - Add a branch in
CharacterData._process_character_file()to parse the format and call_update_animation_settings(). - Handle any offset or index quirks in
utils/FNF/alignment.py.
- Add the default in
AppConfig.DEFAULTS(typically underextraction_defaultsor a new section). - Add the type to
AppConfig.TYPE_MAP, e.g."my_flag": bool. - Access via
app_config.get_extraction_defaults()["my_flag"].
- Follow naming conventions:
snake_casefunctions,PascalCaseclasses,UPPER_SNAKE_CASEconstants. - Add docstrings to public classes and methods.
- Update docs when adding formats or settings.
- Don't touch
latestVersion.txt(triggers premature update prompts).
For usage instructions, see the User Manual.
For installation help, see the Installation Guide.
For an AI-generated overview, see DeepWiki.
Last updated: December 6, 2025 — TextureAtlas Toolbox v2.0.0