#                                                                          RGBcube.py
# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
# Purpose
# ‾‾‾‾‾‾‾
# This collection of functions is intended to provide a way to
# use standard matrix operations on the RGB values of the pixels in an image.

# Designed May 2023
# ‾‾‾‾‾‾‾‾ Tim (https://hingstonbrook.com/48cube/)

# Modified  August 2023
# ‾‾‾‾‾‾‾‾ Tim
# Slightly improved documentation

# Notes
# ‾‾‾‾‾
# The cube in the title is unconventional: its coordinates have integer values
# in the range -255 to 255 inclusive. This allows standard RGB values to be mapped
# onto a cube with mid grey at its centre,
# and red, green, bue, cyan, magenta, yellow, black and white at its vertices.
# The -255 to 255 range allows integer arithmetic to avoid rounding errors.
#
# Matrices here operate on 3×1 ndarrays, which act like column vectors
# for consistency with the usual  f(x)  function notation.
#
# NB These functions do not check the validity of input or output:
# that is left to any program that calls them.
# _____________________________________________________________________________________
from matrices import *
from PIL import Image

# Constants                                                                   Constants
# ‾‾‾‾‾‾‾‾‾                                                                   ‾‾‾‾‾‾‾‾‾
# np.identity(3) has real entries but we want integer arithmetic in these functions.
I = np.array([[1, 0, 0],
              [0, 1, 0],
              [0, 0, 1]])
R = np.array([[1, 0,  0],
              [0, 0, -1],
              [0, 1,  0]])
B = np.array([[ 0, 1,  0],
              [-1, 0, 0],
              [ 0, 0, 1]])
G = R @ R @ R @ B @ R
C, M, Y, K = R @ G, G @ R, R @ B, B @ G

ROTATION = [I, R, R@R, R@R@R, G, G@G, G@G@G, B, B@B, B@B@B,
            C, C@C, M, M@M, Y, Y@Y, K, K@K,
            Y@B, M@G, R@C, C@G, R@Y, M@R]
ROTATIONS = len(ROTATION)
# We cound say  REFLECTION = -ROTATION  but it's not necessary.

# Functions                                                                   Functions
# ‾‾‾‾‾‾‾‾‾                                                                   ‾‾‾‾‾‾‾‾‾

# Map rgb values triple to a 3×1 ndaarray representing its point in the cube
def intoCube(rgb): return (2 * np.atleast_2d(rgb) - 255).T

# Map a point in the cube (a 3×1 ndaarray) into a standard rgb triple
# This is a strange formula. I'll use it until I find out why Python has a problem with +255
def fromCube(pt): return tuple((pt.flatten() + 253) // 2 + 1)

# Manipulate the colours in a Pillow Image, pic,
# by applying a matrix, M, to the points in the RBG cube
def transform(M, pic):
    picl = pic.load()
    COLS, ROWS = pic.width, pic.height
    for x in range(COLS):
        for y in range(ROWS):
            pic.putpixel((x, y), fromCube(M @ intoCube(picl[x, y])))
    return pic

# Read an image file, transform it and save it under a suffixed name in the same folder
# M is the transform matrix; path includes the file name without the extension
def transfile(M, path, ext, suffix):
    pic = transform(M, Image.open(path + '.' + ext, 'r'))
    pic.save(path + suffix + '.' + ext)
    return pic

# Read an image file and save every RGBcube-rotated version of it.
def rotationsFile(path, ext):
    print('Generating recoloured versions of ' + path + '.' + ext + '...')
    for i in range(ROTATIONS):
        transfile(ROTATION[i], path, ext, '-v'+str(i))
        print('Saved ' + path + '-v' + str(i) + '.' + ext + '.')
    return

# Read an image file and save every RGBcube-reflected or -rotation-reflected version of it.
def reflectionsFile(path, ext):
    print('Generating inverted versions of ' + path + '.' + ext + '...')
    for i in range(ROTATIONS):
        transfile(-ROTATION[i], path, ext, '-v-'+str(i))
        print('Saved ' + path + '-v-' + str(i) + '.' + ext + '.')
    return

# _____________________________________________________________________________________
