npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

unity-asset-reader-texture

v1.0.1

Published

Texture2D and Sprite decoding to RGBA for unity-asset-reader.

Readme

unity-asset-reader-texture

Decodes Unity Texture2D and Sprite objects read by unity-asset-reader to RGBA8 pixels. It runs in browsers, Web Workers and Node.js.

  • Plain formats (RGBA32, RGB565, RHalf, ...) are converted in TypeScript.
  • Block-compressed and Crunch formats (BC1–BC7, ETC, EAC, PVRTC, ASTC, ATC) are decoded by texture2ddecoder-wasm, a small single-threaded WASM module (about 150 KB). It needs no special headers (COOP/COEP).
  • Output is always RGBA, top row first: { data, width, height }, ready for ImageData or an image encoder. Unity stores rows bottom first and the WASM decoders produce BGRA; both are undone for you.

Install

npm install unity-asset-reader unity-asset-reader-texture

unity-asset-reader is a peer dependency, so your app has exactly one copy of the parser. texture2ddecoder-wasm is installed with this package.

Usage

Images are free functions over the assets of env.assets():

import { load } from "unity-asset-reader";
import { isImage, imageInfo, decodeImage, images } from "unity-asset-reader-texture";

const env = load([{ name: "ui.bundle", data: bundleBytes }]);

for (const asset of env.assets()) {
  if (isImage(asset)) {                     // a Texture2D or Sprite asset
    const info = imageInfo(asset);          // sync metadata, no WASM
    const image = await decodeImage(asset); // info + { rgba, width, height }
  }
}
for await (const image of images(env)) {    // every Texture2D and Sprite, decoded
  console.log(image.kind, image.name, image.width, image.height, image.formatName);
}
  • isImage(asset) is a type guard: true for a Texture2D or Sprite asset.
  • imageInfo(asset) describes the image without decoding it: kind, name, path, pathId, file, width, height, format (the TextureFormat value), formatName ("DXT5"), compression ("none", "bc", "etc", "etc2", "eac", "pvrtc", "atc", "astc", "crunch", or "unknown" for a format number TextureFormat does not name), mipCount, readable, colorSpace ("srgb" or "linear"), filterMode, wrapMode ({ u, v, w }), platform (the BuildTarget), encodedSize and streamed (the data is in a .resS). It is synchronous, needs no WASM, and reads no image data, so a .resS that is not loaded does not stop it. Each field's JSDoc names the Unity field it comes from.
  • For a Sprite, width and height are the size of the cut-out image, the encoding fields are those of the texture it is cut from, and info.sprite adds rect, textureRect, pivot, border, pixelsPerUnit, packed, packingMode ("tight" or "rectangle"), rotation (a SpritePackingRotation), atlas (the SpriteAtlas' name, when it is packed into one that is loaded) and texture (the texture's own imageInfo). info.kind === "Sprite" narrows to it.
  • decodeImage(asset, options?) returns imageInfo(asset) plus rgba: RGBA8 pixels, top row first, width * height * 4 bytes. A Sprite is cut out of its texture or atlas, as decodeSprite does.
  • images(env, { onError }) decodes every Texture2D and Sprite, in env.assets() order. An image that fails to decode throws (onError: "throw", the default) or is left out (onError: "skip"). It gives the event loop a turn between images, so a loop on a page's main thread keeps the page responsive.

decodeImage and images load the WASM decoder on first use. In Node.js that needs nothing. A browser has to say where the WASM files are: pass { wasmPath } to the first call, or call initTexture({ wasmPath }) once before (see "Where the WASM files come from").

In a browser, put the pixels on a canvas:

const { rgba, width, height } = await decodeImage(asset, { wasmPath });
const imageData = new ImageData(new Uint8ClampedArray(rgba.buffer, rgba.byteOffset, rgba.length), width, height);
canvas.getContext("2d").putImageData(imageData, 0, 0);

In Node.js, hand the raw RGBA to the image library of your choice. For example, with sharp: sharp(rgba, { raw: { width, height, channels: 4 } }).png().toFile("out.png"). This package does not encode images itself.

The low-level API

decodeTexture2D, decodeSprite and initTexture work on the objects of env.objects directly. They are what decodeImage uses, and they do not load the WASM themselves: call initTexture() once, and wait for it, before the first decode.

import { load, ClassID } from "unity-asset-reader";
import { initTexture, decodeTexture2D, decodeSprite } from "unity-asset-reader-texture";

await initTexture(); // Node.js: no options. Browsers: see "Where the WASM files come from".

const env = load([{ name: "ui.bundle", data: bundleBytes }]);
for (const obj of env.objects) {
  if (obj.type === ClassID.Texture2D) {
    const { data, width, height } = await decodeTexture2D(obj.read()); // RGBA, top row first
  }
  if (obj.type === ClassID.Sprite) {
    const { data, width, height } = await decodeSprite(obj, env); // its rectangle, cut out
  }
}

Where the WASM files come from

initTexture() loads texture2ddecoder.js and texture2ddecoder.wasm. decodeImage and images call it on first use with the wasmPath they were given; decodeTexture2D and decodeSprite need it called, and waited for, before the first decode. Every format needs it, the plain ones too.

  • Node.js: nothing to do, or await initTexture(). The files are found inside the installed package.

  • Browser, self-hosted: copy the files into your static folder, then pass their URL:

    npx texture2ddecoder-copy-wasm public/wasm
    await initTexture({ wasmPath: "/wasm" });

    A root-relative path is resolved against the page's or the Worker's location.

  • Browser, CDN: await initTexture({ wasmPath: "https://cdn.jsdelivr.net/npm/texture2ddecoder-wasm@1/wasm" }).

In a Web Worker, texture2ddecoder-wasm 1.2.3 or later is needed. 1.2.2 refuses to initialize in a Worker (#149). This package depends on ^1.2.3; a CDN wasmPath should point at 1.2.3 or later too.

The Bundler Guide has the setup for Vite, Next.js and CDN pages. Next.js, and a root-relative wasmPath in the Vite dev server, need texture2ddecoder-wasm 1.2.4 or later (#171).

Sprites

A sprite's pixels are in another object. decodeImage finds it through the Sprite asset's env; decodeSprite(obj, env, options?) takes the Sprite's ObjectReader and the env that loaded it. Both find the texture through the Sprite's pointers, through its SpriteAtlas when that atlas is loaded. It cuts the sprite's rectangle out, and it undoes the packer's flip or rotation. So pass the bundles holding the texture and the atlas to the same load().

With decodeSprite's { tightMesh: true }, pixels outside a tight-packed sprite's mesh become transparent, as AssetStudio does. The default, and decodeImage, return the whole rectangle.

Texture formats

decodeTexture2D decodes the first mip level. Every format below is checked against pixels from UnityPy (the test oracle) or, where UnityPy cannot decode it, against AssetStudio's decoder. "Editor-built" means the test texture was made by the Unity editor. "Synthetic" means it was generated block data; no current editor writes those formats.

| Group | Formats | Decoded by | Tested on | |---|---|---|---| | Plain | Alpha8, ARGB4444, RGB24, RGBA32, ARGB32, RGB565, R16, RGBA4444, BGRA32, RHalf, RGHalf, RGBAHalf, RFloat, RGFloat, RGBAFloat, RGB9e5Float, YUY2 | TypeScript | editor-built, all 17 | | BCn | DXT1, DXT5, BC4, BC5, BC6H, BC7 | WASM | editor-built | | ETC / EAC | ETC_RGB4, ETC2_RGB, ETC2_RGBA1, ETC2_RGBA8, EAC_R, EAC_RG | WASM | editor-built | | | EAC_R_SIGNED, EAC_RG_SIGNED | WASM | synthetic | | | ETC_RGB4_3DS, ETC_RGBA8_3DS | WASM (same decoder as ETC_RGB4 / ETC2_RGBA8) | not separately | | PVRTC | PVRTC_RGB2, PVRTC_RGBA2, PVRTC_RGB4, PVRTC_RGBA4 | WASM | editor-built (2019.4) | | ASTC | ASTC_RGB_4x4 to ASTC_RGB_12x12 (Unity's ASTC_4x4 ...), ASTC_HDR_4x4, ASTC_HDR_12x12 | WASM | editor-built | | | ASTC_RGBA_4x4 to ASTC_RGBA_12x12, ASTC_HDR_5x5 to ASTC_HDR_10x10 | WASM (same decoder) | not separately | | ATC | ATC_RGB4, ATC_RGBA8 | WASM | synthetic | | Crunch | DXT1Crunched, DXT5Crunched, ETC_RGB4Crunched, ETC2_RGBA8Crunched | WASM | editor-built (Unity's crunch, 2017.3+) |

Details worth knowing:

  • Output is 8 bits per channel. Half and float channels are scaled by 255 and clamped, so HDR values saturate.
  • Channels a format lacks are 0 (color) or 255 (alpha); Alpha8 is white with that alpha.
  • DXT1Crunched / DXT5Crunched from before Unity 2017.3 use the original crunch format. It is unpacked too, and checked against AssetStudio's decoder only.
  • DXT5 color is decoded in 4-color mode, as the S3TC spec says. On blocks with c0 <= c1 that differs from AssetStudio (#137).

Platforms

  • Switch: swizzled textures are deswizzled first (as UnityPy does; AssetStudio has no Switch support). Formats with no known Switch layout, such as Crunch, ETC, PVRTC and ASTC HDR, are refused.
  • Xbox 360: the byte order of ARGB4444, RGB565, DXT1 and DXT5 is swapped back.
  • PS4, PS5: refused. Their textures can be tiled, and no reference implementation detiles them yet.

Not supported

Each of these throws UnsupportedError, whose kind and found say what was refused:

  • Formats: DXT3, ARGBFloat, RGBFloat, BGR24, R8, RG16, RG32, RGB48, RGBA64. YUY2 of odd width. (A swizzled Switch texture stores BGR24 as BGRA32; that one decodes.)
  • Textures built for PS4 or PS5.
  • Sprites with an alpha texture (ETC1 split alpha), and sprites of a variant atlas (a downscaleMultiplier other than 1).

Not provided at all: mip levels other than the first; the Cubemap, Texture2DArray and Texture3D classes; image encoding (PNG, JPEG).

Rotate90-packed sprites are turned the way AssetStudio turns them, which no test bundle has confirmed against Unity's packer yet (#160).

Requirements

  • Browsers: WebAssembly and ES2020. No COOP/COEP headers, no SharedArrayBuffer.
  • Node.js: 20.19+ or 22.12+ (engines: ^20.19.0 || >=22.12.0), for both import and require; CI tests on 20.19.0 and 22.12.0. initTexture() loads texture2ddecoder-wasm's ES-module glue code, which older Node.js versions refuse (#172). Node.js 22 prints a MODULE_TYPELESS_PACKAGE_JSON warning while loading it. The warning is harmless.

API reference

Every export. Each one has full JSDoc (parameters, return values, what it throws) in the bundled index.d.ts.

| Export | What | |---|---| | isImage(asset) | Type guard: whether an asset is a Texture2D or Sprite (an ImageAsset) | | imageInfo(asset) | An image asset's ImageInfo, sync, no WASM, no image data read | | decodeImage(asset, options?) | An image asset decoded: its ImageInfo plus rgba, top row first. Loads the WASM on first use | | images(env, options?) | Async generator: every Texture2D and Sprite of env, decoded | | ImageAsset | Asset<"Texture2D" \| "Sprite"> | | ImageInfo, TextureImageInfo, SpriteImageInfo, SpriteInfo | What imageInfo returns; kind tells the two apart | | ImageCompression | compression's values | | DecodedImage | ImageInfo & { rgba } | | DecodeImageOptions, ImagesOptions | { wasmPath? }, and { wasmPath?, onError? } | | initTexture(options?) | Load the WASM decoder. Optional before decodeImage and images; call it once, and await it, before decodeTexture2D and decodeSprite | | InitTextureOptions | { wasmPath?, locateFile? }, passed to texture2ddecoder-wasm's initialize | | decodeTexture2D(texture) | A Texture2D, as obj.read() returns it, to RGBA, top row first | | decodeSprite(obj, env, options?) | A Sprite to RGBA, top row first, cut out of its texture or atlas | | DecodeSpriteOptions | { tightMesh? } | | convertPlain(data, width, height, format) | One plain-format image to RGBA, rows as stored (bottom row first). No console layouts undone. decodeTexture2D is usually what you want | | RgbaImage | { data, width, height }: 4 bytes per pixel, R G B A |

License

MIT AND Apache-2.0. The package is MIT, except the sprite tight-mesh fill in decodeSprite. That fill is derived from ImageSharp.Drawing and is under the Apache License 2.0. See NOTICE and LICENSE-APACHE. The texture conversion is ported from AssetStudio and UnityPy (MIT).