Unity3d - Importing indexed textures using native plugin.

Posted on 2017-05-30 in unity3d

So I wanted to import an indexed color texture into unity as a grayscale gradient, where gray level would correspond to original color index.

One would think there's an easy way to do it, but nope. System.Drawing.Graphics is not accessible in unity, and no indexed color support is exposed anywhere.

So... I got a bit angry and solved the whole thing via a native plugin.

Code dump below. Assume that code is available under ZLib license.

Native plugin (C++, requires zlib and libpng):

#include <png.h>
#include <vector>
#include <memory>
#include <functional>

struct ImageInfo{
    int width = 0;
    int height = 0;
    int colorType = 0;
    int bitDepth = 0;
    int interlace = 0;
    int compression = 0;
    int filter = 0;
    int paletteColors = 0;
};

typedef std::shared_ptr<png_struct> PngPtr;
typedef std::shared_ptr<png_info> PngInfoPtr;

bool processPng(wchar_t* path, std::function<bool(png_structp, png_infop)>callback){
    auto f = std::shared_ptr<FILE>(_wfopen(path, L"rb"),
        [](FILE* p){
            if (p)
                fclose(p);
        }
    );

    uint8_t sig[8];
    fread(sig, 1, 8, f.get());
    if (!png_check_sig(sig, 8))
        return false;

    auto pngPtr = PngPtr(png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0),
        [&](png_structp p){
            if (p)
                png_destroy_read_struct(&p, 0, 0);
        }
    );
    if (!pngPtr)
        return false;

    auto infoPtr = PngInfoPtr(
        png_create_info_struct(pngPtr.get()),
        [&](png_infop p){
            if(p)
                png_destroy_info_struct(pngPtr.get(), &p);
        }
    );
    if (!infoPtr)
        return false;

    png_init_io(pngPtr.get(), f.get());
    png_set_sig_bytes(pngPtr.get(), 8);
    png_read_info(pngPtr.get(), infoPtr.get());

    return callback(pngPtr.get(), infoPtr.get());
}

extern "C" int getImageInfo(wchar_t* path, ImageInfo* imageInfo){
    int numColors = -1;
    auto result = processPng(path, [&](png_structp png, png_infop info){
        uint32_t width = 0, height = 0;
        png_get_IHDR(png, info, 
            &width, &height, 
            &imageInfo->bitDepth, &imageInfo->colorType, 
            &imageInfo->interlace, &imageInfo->compression, 
            &imageInfo->filter);

        imageInfo->width = (int)width;
        imageInfo->height = (int)height;
        imageInfo->paletteColors = -1;
        if (imageInfo->colorType != PNG_COLOR_TYPE_PALETTE)
            return true;

        png_set_strip_16(png);
        png_set_packing(png);

        png_colorp pal = 0;
        int numPalette = 0;
        png_get_PLTE(png, info, &pal, &numPalette);
        imageInfo->paletteColors = numPalette;
        return true;
    });

    return (int)result;
}

extern "C" int getImageData(wchar_t* path, uint8_t* outPalette, uint8_t* outImageBytes){
    auto result = processPng(path, [&](png_structp png, png_infop info){
        uint32_t width = 0, height = 0;
        int bitDepth = 0, colorType = 0, interlace = 0, compression = 0, filter = 0;
        png_get_IHDR(png, info, &width, &height, &bitDepth, &colorType, &interlace, &compression, &filter);

        if (colorType != PNG_COLOR_TYPE_PALETTE)
            return false;

        png_set_strip_16(png);
        png_set_packing(png);
        auto numColors = png_get_palette_max(png, info);

        png_colorp pal = 0;
        int numPalette = 0;
        png_get_PLTE(png, info, &pal, &numPalette);
        for(int i = 0; (i < numPalette); i++){
            auto cur = pal[i];
            outPalette[i*3 + 0] = cur.red;
            outPalette[i*3 + 1] = cur.green;
            outPalette[i*3 + 2] = cur.blue;
        }

        png_read_update_info(png, info);

        auto rowBytes = png_get_rowbytes(png, info);
        auto rowPointers = std::vector<png_bytep>(height);
        for(int i = 0; i < height; i++){
            rowPointers[i] = outImageBytes + width*i;
        }
        png_read_image(png, rowPointers.data());

        return true;
    });
    return (int)result; 
}

*.def file for said plugin (by the way, it is named "PalettePlugin":

LIBRARY

EXPORTS
getImageInfo
getImageData

*.cs glue script for asset importer:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Runtime.InteropServices;

public class PaletteTexturePostProcessor: AssetPostprocessor{
    [System.Serializable]
    [StructLayout(LayoutKind.Sequential)]
    struct ImageInfo{
        public int width;
        public int height;
        public int colorType;
        public int bitDepth;
        public int interlace;
        public int compression;
        public int filter;
        public int paletteColors;
    };

    [DllImport("PalettePlugin", CharSet = CharSet.Unicode)]
    static extern int getImageData([MarshalAs (UnmanagedType.LPWStr)]string filepath, byte[] outPalette, byte[] outImageBytes);

    [DllImport("PalettePlugin", CharSet = CharSet.Unicode)]
    static extern int getImageInfo([MarshalAs (UnmanagedType.LPWStr)]string filepath, [In, Out] ref ImageInfo imageInfo);

    bool isIndexedTextureAsset(){
        var lowercasePath = assetPath.ToLower();
        return lowercasePath.Contains("/indexed/");
    }

    void OnPreprocessTexture(){
        if (!isIndexedTextureAsset())
            return;
        var importer = (TextureImporter)assetImporter;
        if (!importer)
            return;
        importer.filterMode = FilterMode.Point;
        importer.mipmapEnabled = false;
        importer.textureType = TextureImporterType.Default;
        importer.compressionQuality = 100;
        var settings = importer.GetDefaultPlatformTextureSettings();
        //settings.format = TextureImporterFormat.
        settings.textureCompression = TextureImporterCompression.Uncompressed;
        importer.SetPlatformTextureSettings(settings);
    }

    void OnPostprocessTexture(Texture2D tex){
        if (!isIndexedTextureAsset())
            return;

        var filePath = assetPath;
        Debug.LogFormat("filePath: {0}", filePath);
        int numChannels = 3;
        var info = new ImageInfo();
        if (getImageInfo(filePath, ref info) == 0){
            Debug.LogFormat("Get image info failed");
            return;
        }
        Debug.LogFormat("Image info: {0}x{1} {2}bpp", info.width, info.height, info.bitDepth);

        var palette = new byte[info.paletteColors * numChannels];
        var bytes = new byte[info.width * info.height];

        if (getImageData(filePath, palette, bytes) == 0){
            Debug.LogFormat("Get image data failed");
            return;
        }

        Debug.LogFormat("palette");
        for(int i = 0; i < info.paletteColors; i++){
            Debug.LogFormat("Entry: {0}, R:{1}, G:{2}, B:{3}", i, palette[i*3], palette[i*3+1], palette[i*3+2]);
        }

        var colors = tex.GetPixels32();

        for(int y = 0; y < info.height; y++){
            var dstRowStart = y * info.width;
            var srcRowStart = (info.height - 1 - y) * info.width;
            for(int x = 0; x < info.width; x++){
                var srcOffset = srcRowStart + x;
                var dstOffset = dstRowStart + x;

                var idx = bytes[srcOffset];

                colors[dstOffset].a = idx;
                colors[dstOffset].r = idx;
                colors[dstOffset].g = idx;
                colors[dstOffset].b = idx;
            }
        }

        tex.SetPixels32(colors);
        tex.Apply();
    }
}

This should go into one of the "Editor" script folders.

Basically, anything in any "/Indexed/" subfolder will be processed.

Keep in mind that this particular importer sets up some default values for texture import and those might not be what you want.

Also, it wastes time first improting the texture and then discarding imported color.

Aaand of course it'll only work on png images.

P.S. On related note, it turns out that "Source Code Pro" font had missing ampersand in the bold version. The font is currently disabled.