Metadata-Version: 2.4
Name: gif2vtf
Version: 1.0.0
Summary: Convert animated GIFs into animated Valve Texture Format (.vtf) files.
Author-email: Zisomerism <29962815+Zisomerism@users.noreply.github.com>
License-Expression: GPL-3.0-or-later
Project-URL: Homepage, https://github.com/Zisomerism/gif2vtf
Project-URL: Repository, https://github.com/Zisomerism/gif2vtf
Project-URL: Issues, https://github.com/Zisomerism/gif2vtf/issues
Keywords: vtf,gif,source engine,valve,spray,texture
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Games/Entertainment
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: srctools>=2.4
Requires-Dist: Pillow>=9.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Dynamic: license-file

# gif2vtf

Convert animated GIFs into animated [Valve Texture Format](https://developer.valvesoftware.com/wiki/Valve_Texture_Format) (`.vtf`) files from the command line.

This automates the manual workflow described in the [Steam "animated sprays" guide](https://steamcommunity.com/sharedfiles/filedetails/?id=1288408087): split a GIF into frames, normalise their size, and pack them into a single multi-frame VTF. Instead of GIMP plus VTFEdit, you run one command.

## How it works

```
GIF -> decode + coalesce frames -> resize/clamp -> detect alpha
    -> pick image format -> write multi-frame VTF (srctools)
```

Frames are decoded with Pillow and fully composited (this undoes GIF "optimization", equivalent to the guide's GIMP *Unoptimize* step). They are resized to a power-of-two size, then written as a single animated VTF using [srctools](https://github.com/TeamSpen210/srctools).

## Installation

```bash
pip install gif2vtf
```

This installs the `gif2vtf` command. Dependencies are `srctools` and `Pillow`.

To install from a source checkout instead:

```bash
pip install .
```

To run the tests as well:

```bash
pip install .[dev]
pytest
```

> **Compressed formats need the compiled srctools build.** DXT1/DXT3/DXT5 and ATI2N compression is implemented in srctools' optional Cython extension (libsquish). The standard `pip install srctools` wheel includes it. If your build lacks it, those formats raise an error on save; use an uncompressed format such as `RGBA8888` or `BGRA8888` instead.

## Usage

```bash
gif2vtf input.gif                 # writes input.vtf with the "general" preset
gif2vtf input.gif -o out.vtf
gif2vtf input.gif --preset spray --width 128 --height 128
```

### Presets

A preset only supplies defaults; any explicit flag overrides it.

| Preset | Defaults |
| --- | --- |
| `general` (default) | mipmaps on, resize to nearest power of two, clamp 4096, `DXT1` / `DXT5` for alpha |
| `spray` | mipmaps off, clamp 512, point sample + clamp S/T + no LOD flags, warns if over 512 KB |

### Options

```
--format FORMAT          Format for frames without alpha (default DXT1)
--alpha-format FORMAT    Format for frames with alpha (default DXT5)
--alpha-threshold N      One-bit-alpha cutoff (0-255)

--resize / --no-resize   Resize frames to a valid VTF size
--resize-method METHOD   nearest-power2 | biggest-power2 | smallest-power2 | set
--width W --height H     Explicit target size (implies the 'set' method)
--clamp / --no-clamp     Cap dimensions to the clamp size
--clamp-width W          Maximum width when clamping
--clamp-height H         Maximum height when clamping
--resize-filter FILTER   nearest | bilinear | bicubic | lanczos

--mipmaps / --no-mipmaps Generate a mipmap chain
--mip-filter FILTER      nearest | bilinear

--flag NAME              Add a VTF flag, e.g. CLAMP_S (repeatable)
--no-flag NAME           Remove a preset flag (repeatable)

--max-frames N           Keep at most N frames
--skip N                 Skip the first N frames
--decimate N             Drop every Nth frame (1-based); N must be >= 2
--optimize-frames        EXPERIMENTAL: remove duplicate and zero-delay frames
--optimize-fuzz N        Max per-channel RGBA difference for duplicate detection
--version 7.5            VTF version (7.2-7.5)
--strict-size            Fail instead of warning when over the spray limit
-v / --verbose           Print conversion details
```

### Reducing frame count

Sprays have a tight size budget, so trimming frames is often necessary.

- `--decimate N` drops every Nth frame using 1-based counting. `--decimate 2` on a 10-frame GIF removes frames 2, 4, 6, 8, 10 and keeps 5; `--decimate 4` removes every 4th frame and keeps roughly three quarters.
- `--optimize-frames` is experimental. It removes whole frames that add nothing to the visible animation: zero-delay intermediate frames and runs of consecutive identical frames (the spirit of ImageMagick's [`RemoveZero` and `RemoveDups`](https://usage.imagemagick.org/anim_opt/#frame_opt) layer methods). Duplicate detection is exact by default; raise the tolerance with `--optimize-fuzz N`, where `N` is the maximum allowed per-channel difference. This does not perform GIF sub-frame or disposal optimization, which is irrelevant to VTF because every VTF frame stores a full image.

Optimization runs before decimation, and both run after `--skip` / `--max-frames`.

Supported format names: `DXT1`, `DXT3`, `DXT5`, `DXT1_ONEBITALPHA`, `BGR888`, `RGB888`, `BGRA8888`, `RGBA8888`, `BGR565`, `RGB565`, `ATI2N`.

## Notes and limitations

- **Dimensions must be powers of two.** srctools only writes power-of-two VTFs, so all resize methods converge on power-of-two sizes. With `--no-resize`, every frame must already be a valid power-of-two size.
- **Frame rate is not stored in the VTF.** Source controls animation timing through the material (`.vmt`) and engine, not the texture. In-game sprays play at roughly 5 FPS regardless of the source GIF.
- **Disabling mipmaps** writes only the base image (and sets the `NO_MIP` flag), which keeps sprays under the 512 KB limit as the guide recommends.
- **Mipmap filters** are limited to `nearest` and `bilinear` (all srctools supports).
- This tool outputs `.vtf` only; it does not generate a companion `.vmt` material file.

## Releasing (maintainers)

### One-time PyPI trusted publishing setup

GitHub Actions publishes to PyPI using [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC). No API token is stored in GitHub secrets.

1. **PyPI** — open [Account settings → Publishing](https://pypi.org/manage/account/publishing/) (or the project page after the first upload) and add a trusted publisher:
   - PyPI project name: `gif2vtf`
   - Owner: `Zisomerism`
   - Repository name: `gif2vtf`
   - Workflow name: `publish.yml`
   - Environment name: `pypi`
2. **GitHub** — in this repository go to **Settings → Environments**, create an environment named `pypi` (optionally restrict it to the `main` branch and require approval for production releases).

### Publish a release

1. Bump the version in [`pyproject.toml`](pyproject.toml), [`src/gif2vtf/__init__.py`](src/gif2vtf/__init__.py), and add a [`CHANGELOG.md`](CHANGELOG.md) entry.
2. Commit, push, and tag (e.g. `v1.0.0`).
3. Create a [GitHub Release](https://github.com/Zisomerism/gif2vtf/releases/new) from that tag. Publishing the release triggers [`.github/workflows/publish.yml`](.github/workflows/publish.yml), which builds the sdist/wheel and uploads them to PyPI.

### Manual upload (fallback)

```bash
pip install build twine
python -m build
twine check dist/*
twine upload dist/* --username __token__ --password pypi-...
```

## License

This project is licensed under the GNU General Public License, version 3 or later (GPL-3.0-or-later). See [LICENSE](LICENSE) for the full text.

Copyright (C) 2026 Zisomerism.
