6
\$\begingroup\$

I'm creating some images for an existing game that allows for custom content. I have a large collection of PNG images with indexed colors (all using the same palette). I would like to be able to go through these images and selectively paint over parts of them in such a way that it replaces the old color with a new color, but only with a certain range of the palette. The last 12 orange colors in the palette are special indices, and I want to use them in certain parts of the graphics.

palette

For example, I would brush over this image to replace the colors with the closest match only from the 12 orange colors at the end of the palette (this tree was originally all green, but I want to paint its leaves the orange color, leaving the trunk alone, just as an example):

trees getting painted

However I'm having a really difficult time finding tooling that will do this exactly, while supporting indexed mode and ensuring that the resulting area I paint only uses the colors from certain indices.

I have a lot of images to adjust, so I'd prefer a tool that lets me just open an image, brush it, save, and get out.

Some things I've tried so far:

  • Photoshop: Doesn't like doing color replace with indexed images; you can convert to RGB mode and back, but there is no guarantee that the orange bits of the resulting image will be the correct ones, since there are other oranges in the palette that are pretty close.
  • Gimp: Actually does color replacement in indexed mode with a brush, but also doesn't guarantee that the colors I get back are the right ones for the same reason.
  • Aseprite: Has some promising scripting options, but doesn't seem to support this kind of custom brushing, and would only do the entire image, which is undesirable.
  • Paint.NET: Doesn't even know what indexed palettes are, as far as I know.

I'm running out of options that I can think of aside from literally writing my own software to do this, which I'd like to avoid... are there other tools out there that can do what I need? Or functionality in the tools I've tried that I've missed?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

If you know which colors you want to map to which, wouldn't a script work better than a brush? It would seem it's not a spatial targeting issue (what a brush would solve) but rather a color targeting issue.

If you're up for compiling and running a Java program, then this will let you map same-length index ranges from one to another. I chose Java because in C# (my first choice) there were issues with both System.Drawing.Bitmap and Eto.Drawing.Bitmap in console apps, and because the Aseprite trial version doesn't allow me to try the scripting feature before buying.

If you still need to use a brush, you can import both images as layers in your preferred software, and just use the eraser tool on one.

import javax.imageio.ImageIO;
import java.awt.image.*;
import java.io.File;
import java.io.IOException;
import java.util.Scanner;

public class MapIndexedColoredImage {

    public static void main(String[] args) {

        Scanner input = new Scanner(System.in);

        // Read in user-specified file.
        BufferedImage image;
        while (true) {
            System.out.print("Enter image filename: ");
            String filename = input.nextLine();
            File file = new File(filename);

            if (!file.exists()) {
                System.out.println("Could not find file!");
                continue;
            }

            try {
                image = ImageIO.read(new File(filename));
            } catch (IOException e) {
                System.out.println("Could not open file for some reason!");
                continue;
            }

            // Make sure it's indexed-colored.
            if (image.getType() != BufferedImage.TYPE_BYTE_INDEXED) {
                System.out.println("Image does not seem to be indexed-colored!");
                continue;
            }

            break;
        }

        // Get a handle on it (and make a copy of its data the way that seemed to work).
        DataBuffer imageDataBuffer = image.getRaster().getDataBuffer();
        int[] imageDataBufferElementsCopy = new int[imageDataBuffer.getSize()];
        for (int i = 0; i < imageDataBuffer.getSize(); i++) {
            imageDataBufferElementsCopy[i] = imageDataBuffer.getElem(i);
        }

        while (true) {

            // Get the translation range start or the save file name.
            int mappingSourceStart;
            while (true) {
                System.out.print("First color index in span to translate from, range [0,255], or filename to save image as: ");
                String token = input.nextLine();
                try {
                    mappingSourceStart = Integer.parseInt(token);
                } catch (NumberFormatException e) {

                    // Not a number => treat it as a filename. Save and return from program main function.
                    try {
                        ImageIO.write(image, "png", new File(token));
                    } catch (IOException e2) {
                        System.out.println("Could not write file for some reason!");
                        continue;
                    }
                    return;

                }
                if (mappingSourceStart < 0 || mappingSourceStart > 0xFF) {
                    System.out.println("Please enter an integer range 0-255!");
                    continue;
                }
                break;
            }

            int mappingSourceEnd = acceptInteger255OrBlank(input,
                    "Last color index in span to translate from, range [0,255]; blank for single color: ");
            if (mappingSourceEnd < 0) {
                mappingSourceEnd = mappingSourceStart;
            }

            int mappingDestinationStart = acceptInteger255OrBlank(input,
                    "First color index in span to translate to, range [0,255]; blank to correct above: ");
            if (mappingDestinationStart < 0) {
                continue;
            }

            System.out.println("Mapping colors [" + mappingSourceStart + ", " + mappingSourceEnd + "] to [" +
                    mappingDestinationStart + ", " + ((mappingSourceEnd - mappingSourceStart + mappingDestinationStart) & 0xFF) + "]...");
            if (mappingSourceEnd < mappingSourceStart) {
                mappingSourceEnd += 0x100; // handles wrap-around ranges (part 1)
            }
            for (int i = 0; i < imageDataBuffer.getSize(); i++) {
                int colorIndex = imageDataBufferElementsCopy[i];
                if ((colorIndex >= mappingSourceStart && colorIndex <= mappingSourceEnd) ||
                        colorIndex <= mappingSourceEnd - 0x100) { // handles wrap-around ranges (part 2)
                    colorIndex = (mappingDestinationStart - mappingSourceStart + colorIndex) & 0xFF;
                    imageDataBuffer.setElem(i, colorIndex);
                }
            }
        }
    }

    private static int acceptInteger255OrBlank(Scanner input, String text) {
        int value;
        while (true) {
            System.out.print(text);
            String token = input.nextLine();
            if ("".equals(token)) {
                return -1;
            }
            try {
                value = Integer.parseInt(token);
            } catch (NumberFormatException e) {
                System.out.println("Please enter an integer or blank!");
                continue;
            }
            if (value < 0 || value > 0xFF) {
                System.out.println("Please enter an integer range 0-255!");
                continue;
            }
            break;
        }
        return value;
    }
}
Enter image filename: test.png
First color index in span to translate from, range [0,255], or filename to save image as: 96
Last color index in span to translate from, range [0,255]; blank for single color: 107
First color index in span to translate to, range [0,255]; blank to correct above: 243
Mapping colors [96, 107] to [243, 254]...
First color index in span to translate from, range [0,255], or filename to save image as: test2.png
\$\endgroup\$
1
  • \$\begingroup\$ I appreciate the code. This might work directly for some cases I have. However I wanted a "brush" solution because each image I have is different, and for each image, there's a different color range I want to remap. It's a lot easier and quicker to look at it, instead of finding colors and typing their indexes into a script. \$\endgroup\$
    – Andrew
    Mar 16, 2023 at 0:33

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .