Decoding a 20-Year-Old Game Texture Format

4 min read

World of Warcraft stores its textures in a proprietary format called BLP. Blizzard introduced it in Warcraft III around 2002, and it's been baked into every WoW patch ever shipped since. If you've ever modded WoW or extracted game assets, you've run into these files. I built @pinta365/blp to read, parse, and convert them in TypeScript — and blp-toolkit as a browser-based tool for anyone who just needs to convert a file.

Why Does a Custom Image Format Exist?

The short answer is GPU efficiency. Standard image formats like PNG or JPEG are optimized for storage and transmission — they compress well and decode fast on a CPU. But a GPU doesn't want PNG. It wants textures in a format it can load directly into VRAM and sample from without any further decompression.

Blizzard needed a format that could:

  • Store compressed texture data the GPU can use natively
  • Bundle multiple resolutions of the same image (mipmaps)
  • Handle both photographic textures and palette-based art
  • Be fast to load on 2002-era hardware

BLP is the result. It's a container format that wraps one of several internal compression schemes depending on what the texture contains.

The Compression Variants

BLP2 (the version used in WoW) supports three compression types:

DXT (also called S3TC) is the interesting one. It's a block-based compression scheme designed specifically for GPU hardware. The image is divided into 4×4 pixel blocks, and each block is encoded as a pair of endpoint colors plus a 2-bit index per pixel that interpolates between them. The result is a fixed compression ratio regardless of image content — 4:1 for DXT1, 2:1 for DXT3 and DXT5 — and crucially, GPUs can decompress it on the fly in hardware while sampling.

Three variants handle different alpha needs:

  • DXT1 — no alpha or 1-bit alpha (transparent/opaque only). Most space-efficient.
  • DXT3 — explicit 4-bit alpha per pixel. Good for sharp alpha edges.
  • DXT5 — interpolated alpha, same block scheme as the color data. Best for smooth gradients and soft transparency.

RAW1 (palettized) is the legacy format, used heavily in Warcraft III and early WoW content. It stores 256 colors in a palette and one byte per pixel pointing into it — the same approach as GIF. Compact for textures with limited color ranges, but not GPU-native.

RAW3 is uncompressed RGBA — no tricks, just raw pixel data. Largest files, fastest conversion.

import { decodeBlpData, encodeToBLP } from "@pinta365/blp";

// Read a BLP file and get raw RGBA pixel data
const blpBytes = await Deno.readFile("texture.blp");
const { width, height, rgba } = await decodeBlpData(blpBytes);

// Convert a PNG back to BLP (DXT5 with alpha)
const pngBytes = await Deno.readFile("texture.png");
const blpOutput = await encodeToBLP(pngBytes, { compression: "dxt5" });
await Deno.writeFile("texture_new.blp", blpOutput);

Mipmaps

Every BLP file bundles a mipmap chain — pre-computed half-resolution copies of the texture, all the way down to 1×1. So a 512×512 texture ships with versions at 256×256, 128×128, 64×64, and so on, all inside the same file.

The GPU uses these automatically. When a texture is rendered small (far away, small on screen), the engine samples from a smaller mipmap instead of downscaling the full texture at runtime. This reduces aliasing and is far cheaper to compute.

The library gives you access to all mipmap levels, which matters if you're doing texture processing pipelines or want to inspect what the engine is actually using.

The Power-of-2 Requirement

BLP (like most GPU texture formats) requires image dimensions to be powers of two: 64, 128, 256, 512, 1024, and so on. This constraint goes back to how graphics hardware addresses texture memory.

If you try to convert a 300×200 PNG to BLP, something has to give. The library handles this automatically with configurable padding modes that preserve the entire image — no cropping, no distortion. The padding approach matters especially for texture atlases, where the actual content might not fill a power-of-2 canvas but all of it needs to survive the round-trip.

Smart Format Detection

When encoding to BLP, picking the right compression variant isn't always obvious. The library analyzes the source image and recommends a format:

  • If the image has no alpha channel → DXT1
  • If it has sharp, binary transparency → DXT1 with punch-through alpha
  • If it has smooth alpha gradients → DXT5
  • If the image uses fewer than 256 colors → RAW1 (palette)

You can override this, but the auto-detection handles the common cases correctly without requiring knowledge of the compression internals.

blp-toolkit: Conversion in the Browser

All of the above is available at blp.pinta.land without installing anything. Upload a BLP file to get a PNG preview with full metadata inspection — compression type, dimensions, mipmap count, all of it. Or upload a PNG and convert it to BLP with control over the target compression format and padding mode.

Everything runs client-side. Your files never leave the browser.

It's built in TypeScript with @pinta365/blp as the engine, so the same parsing and conversion logic that runs in your Deno or Node.js pipeline works identically in the browser.

Using the Library

# Deno
deno add @pinta365/blp

# Node.js / Bun
npx jsr add @pinta365/blp

Source for both projects is on GitHub: blp and blp-toolkit.

Who Is This For?

Primarily WoW modders and addon developers who work with game assets — either extracting textures from the client to reference in UI work, or creating custom textures to ship with an addon. But it's also useful for anyone building tooling around Blizzard game data more broadly: map editors, model viewers, asset pipelines.

The BLP format isn't going anywhere. Two decades of game content means two decades of BLP files out there, and the modding community still actively creates and converts them. Having a modern, cross-runtime TypeScript library for it felt like a gap worth filling.