Replace image palette upon import.

I have several pngs with indexed color and I’m looking for a sane way to automatically replace image palette upon import - BEFORE unity converts it to rgb data.

My first thought was asset preprocessor, BUT it looks like it would require System.Graphics (which are, as far as I can tell, unsupported in unity), and it doesn’t look like there’s a way to overwrite image data before it is imported:

Restrictions:

  1. No non-free asset store packages.
  2. No trying to detect palette index based on rgb data. (because palette may have duplicate entries)

Any ideas?

So, any ideas?

Basically, I need a way to access some raw image manipulation routines from unity. System.Drawing.Graphics are not available, and libpng functions are not exposed to C# script.

Alright, I “Solved” the issue in the most horrible way possible:

Basically, I wrote a native plugin that can directly read png data, and then abused asset preprocessor class to overwrite texture data using that plugin.

Code:

Asset preprocessor:

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();
    }
}

Plugin code:

#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:

LIBRARY

EXPORTS
getImageInfo
getImageData

However, this is a really ugly way to go about it. Would be nice to know if there is a better way to go about it.

Perhaps, someone from unity can chime in? @Andy-Touch , @superpig or someone else?