Files
color-converter/color-converter.py
T
2025-01-20 00:44:13 -05:00

665 lines
21 KiB
Python

import math
import argparse
# Order of types must match order of arguments defined
# (or else arg validation will no longer work properly)
TYPES = ['hex', 'rgb', 'cmy', 'cmyk', 'hsl', 'hsv']
# look into LAB
HEX_LETTERS = ['a', 'b', 'c', 'd', 'e', 'f']
OUTPUT = 'stdout'
APPEND = False
VERBOSE = False
def main():
# Set up arguments
parser = argparse.ArgumentParser(prog='color-converter',
description='Color code converting utility written in Python.',
epilog='Hope this helps :)')
parser.add_argument('-hex', action='store_true', help='convert/output to Hex')
parser.add_argument('-rgb', action='store_true', help='convert/output to RGB')
parser.add_argument('-cmy', action='store_true', help='convert/output to CMY')
parser.add_argument('-cmyk', action='store_true', help='convert/output to CMYK')
parser.add_argument('-hsl', action='store_true', help='convert/output to HSL')
parser.add_argument('-hsv', action='store_true', help='convert/output to HSV')
parser.add_argument('-isHex', action='store_true', help='indicate that inputted value(s) will be hex')
parser.add_argument('-isRgb', action='store_true', help='indicate that inputted value(s) will be sets of RGB codes')
parser.add_argument('-isCmy', action='store_true', help='indicate that inputted value(s) will be sets of CMY codes')
parser.add_argument('-isCmyk', action='store_true', help='indicate that inputted value(s) will be sets of CMYK codes')
parser.add_argument('-isHsl', action='store_true', help='indicate that inputted value(s) will be sets of HSL codes')
parser.add_argument('-isHsv', action='store_true', help='indicate that inputted value(s) will be sets of HSL codes')
parser.add_argument('--input', '-i', help='name of the input file containing color codes to process')
parser.add_argument('--output', '-o', help='the name of the file to store output in (will create file if doesn\'t exist, will OVERWRITE existing file\'s contents)')
parser.add_argument('--append', '-a', action='store_true', help='append rather than overwrite file output')
parser.add_argument('--verbose', '-v', action='store_true', help='print when performing conversions')
parser.add_argument('color', nargs='*', help='accepts a color in Hex, RGB, CMY, CMYK, or HSL and performs format conversions (does not support CMYK profiles, conversions are uncalibrated)')
args = parser.parse_args()
# debug print
# print(vars(args))
''' ARGS PROCESSING '''
# determine which conversions to perform
outputFormats = []
flagsActive = 0
for flag in vars(args).keys() :
# if the flag for an output type is present, keep trac
if (vars(args).get(flag, False)) and (flag in TYPES) :
flagsActive += 1
outputFormats.append(flag)
# if no conversion flags specified, perform every format conversion
if flagsActive == 0 :
for colorFormat in TYPES :
outputFormats.append(colorFormat)
# if output should be written to file, set global vars for reference later
if args.output :
global OUTPUT
OUTPUT = args.output
if args.append :
global APPEND
APPEND = True
# print conversion updates?
if args.verbose :
global VERBOSE
VERBOSE = True
''' PARSE INPUTTED COLORS '''
colorCodes = []
# if provided a file of values
if args.input :
with open(args.input, 'r', encoding='utf-8') as file :
for line in file :
colorCodes.append(line.strip())
# else grab from stdin
else :
colorCodes = args.color
''' PROCESS COLORS '''
for color in colorCodes :
# Try to automatically handle value
if detectColorFormat(color, outputFormats) :
continue
# if we can't auto-detect the format, check args for guidance
elif args.isHex :
handleHex(color, outputFormats)
elif args.isRgb :
handleRGB(color, outputFormats)
elif args.isCmy :
handleCMY(color, outputFormats)
elif args.isCmyk :
handleCMYK(color, outputFormats)
elif args.isHsl :
handleHSVorHSL(color, 'hsl', outputFormats)
elif args.isHsv :
handleHSVorHSL(color, 'hsv', outputFormats)
else :
print('ERROR: Could not detect inputted color format and no fallback flag was specified. see --help for more information on usage.')
return
return
##
# FORMAT HANDLERS
##
# ARGS
# color: string containing hex code
# outputFormats: list indicating which conversions to perform
def handleHex(color, outputFormats) :
hexCode = validateHex(color)
if hexCode is None :
return
if VERBOSE :
print('CONVERTING HEX: ', hexCode)
outputs = {}
rgbValues = HEXtoRGB(hexCode)
if 'hex' in outputFormats :
outputs['hex'] = hexCode
if 'rgb' in outputFormats :
outputs['rgb'] = rgbValues
if 'cmy' in outputFormats :
outputs['cmy'] = RGBtoCMY(rgbValues)
if 'cmyk' in outputFormats :
outputs['cmyk'] = RGBtoCMYK(rgbValues)
if 'hsl' in outputFormats :
outputs['hsl'] = RGBtoHSVorHSL(rgbValues, 'hsl')
if 'hsv' in outputFormats :
outputs['hsv'] = RGBtoHSVorHSL(rgbValues, 'hsv')
printConversions(outputs)
# ARGS
# color: string containing RGB code
# outputFormats: list indicating which conversions to perform
def handleRGB(color, outputFormats) :
rgbValues = validateRGB(color)
if rgbValues is None :
return
if VERBOSE :
print('CONVERTING RGB: ', rgbValues)
outputs = {}
if 'hex' in outputFormats :
outputs['hex'] = RGBtoHEX(rgbValues)
if 'rgb' in outputFormats :
outputs['rgb'] = rgbValues
if 'cmy' in outputFormats :
outputs['cmy'] = RGBtoCMY(rgbValues)
if 'cmyk' in outputFormats :
outputs['cmyk'] = RGBtoCMYK(rgbValues)
if 'hsl' in outputFormats :
outputs['hsl'] = RGBtoHSVorHSL(rgbValues, 'hsl')
if 'hsv' in outputFormats :
outputs['hsv'] = RGBtoHSVorHSL(rgbValues, 'hsv')
printConversions(outputs)
# ARGS
# color: string containing CMY code
# outputFormats: list indicating which conversions to perform
def handleCMY(color, outputFormats) :
cmyValues = validateCMYorCMYK(color, False)
if cmyValues is None :
return
if VERBOSE :
print('CONVERTING CMY: ', cmyValues)
outputs = {}
rgbValues = CMYtoRGB(cmyValues)
if 'hex' in outputFormats :
outputs['hex'] = RGBtoHEX(rgbValues)
if 'rgb' in outputFormats :
outputs['rgb'] = rgbValues
if 'cmy' in outputFormats :
outputs['cmy'] = cmyValues
if 'cmyk' in outputFormats :
outputs['cmyk'] = RGBtoCMYK(rgbValues)
if 'hsl' in outputFormats :
outputs['hsl'] = RGBtoHSVorHSL(rgbValues, 'hsl')
if 'hsv' in outputFormats :
outputs['hsv'] = RGBtoHSVorHSL(rgbValues, 'hsv')
printConversions(outputs)
# ARGS
# color: string containing CMYK code
# outputFormats: list indicating which conversions to perform
def handleCMYK(color, outputFormats) :
cmykValues = validateCMYorCMYK(color, True)
if cmykValues is None :
return
if VERBOSE :
print('CONVERTING CMYK: ', cmykValues)
outputs = {}
rgbValues = CMYKtoRGB(cmykValues)
if 'hex' in outputFormats :
outputs['hex'] = RGBtoHEX(rgbValues)
if 'rgb' in outputFormats :
outputs['rgb'] = rgbValues
if 'cmy' in outputFormats :
outputs['cmy'] = RGBtoCMY(rgbValues)
if 'cmyk' in outputFormats :
outputs['cmyk'] = cmykValues
if 'hsl' in outputFormats :
outputs['hsl'] = RGBtoHSVorHSL(rgbValues, 'hsl')
if 'hsv' in outputFormats :
outputs['hsv'] = RGBtoHSVorHSL(rgbValues, 'hsv')
printConversions(outputs)
# ARGS
# color: string containing CMY code
# outputFormats: list indicating which conversions to perform
# handle: string ('hsl' or 'hsv'), indicating which format to handle
def handleHSVorHSL(color, handle, outputFormats) :
validated = validateHSLorHSV(color)
if validated is None :
return
if VERBOSE :
if handle == 'hsl' :
print('CONVERTING HSL: ', color)
else :
print('CONVERTING HSV: ', color)
outputs = {}
rgbValues = HSLorHSVToRGB(validated, handle)
if 'hex' in outputFormats :
outputs['hex'] = RGBtoHEX(rgbValues)
if 'rgb' in outputFormats :
outputs['rgb'] = rgbValues
if 'cmy' in outputFormats :
outputs['cmy'] = RGBtoCMY(rgbValues)
if 'cmyk' in outputFormats :
outputs['cmyk'] = RGBtoCMYK(rgbValues)
if 'hsl' in outputFormats :
outputs['hsl'] = RGBtoHSVorHSL(rgbValues, 'hsl')
if 'hsv' in outputFormats :
outputs['hsv'] = RGBtoHSVorHSL(rgbValues, 'hsv')
printConversions(outputs)
##
# CONVERSION FUNCTIONS
##
# Takes in a string, returns a list of 3 integers
def HEXtoRGB(hexCode) :
rgbValues = []
tempSum = 0
i = 0
while i < 6 :
tempSum += int(hexCode[i], 16) * 16
tempSum += int(hexCode[i+1], 16)
rgbValues.append(tempSum)
i = i + 2
tempSum = 0
return rgbValues
# Takes in a list of 3 integers, returns a string
def RGBtoHEX(rgbValues) :
hexValue = ''
for rgbValue in rgbValues :
hexValue += hex(rgbValue / 16)
hexValue += hex(rgbValue % 16)
return hexValue
# Takes in a list of 3 integers, returns a list of 3 floats (percentages)
def RGBtoCMY(rgbValues) :
# Normalize RGB values
normalRed = rgbValues[0] / 255
normalGreen = rgbValues[1] / 255
normalBlue = rgbValues[2] / 255
# Convert
cyan = 1 - normalRed
magenta = 1 - normalGreen
yellow = 1 - normalBlue
return [cyan * 100, magenta * 100, yellow * 100]
# Takes in a list of 3 integers, returns a list of 4 floats (percentages)
def RGBtoCMYK(rgbValues) :
# Normalize RGB values
normalRed = rgbValues[0] / 255
normalGreen = rgbValues[1] / 255
normalBlue = rgbValues[2] / 255
black = 1 - max(normalRed, normalGreen, normalBlue)
x = 1 - black
# Convert
if x == 0 :
return [0, 0, 0, round(black * 100, 2)]
else :
cyan = (1 - normalRed - black) / x
magenta = (1 - normalGreen - black) / x
yellow = (1 - normalBlue - black) / x
return [cyan * 100, magenta * 100, yellow * 100, black * 100]
# Takes in a list of 3 integers, returns a list of 3 floats (percentages)
def RGBtoHSVorHSL(rgbValues, convertTo) :
hue = 0
saturation = 0.0
value = 0.0
lightness = 0.0
# Normalize RGB values
normalRed = rgbValues[0] / 255
normalGreen = rgbValues[1] / 255
normalBlue = rgbValues[2] / 255
# Establish variables for formula
xMax = max(normalRed, normalGreen, normalBlue)
xMin = min(normalRed, normalGreen, normalBlue)
chroma = xMax - xMin
# Convert by following formula
value = xMax
lightness = (xMax + xMin) / 2
if chroma == 0 :
hue = 0
elif value == normalRed :
hue = 60 * (((normalGreen - normalBlue) / chroma) % 6 )
elif value == normalGreen :
hue = 60 * (((normalBlue - normalRed) / chroma) + 2)
elif value == normalBlue :
hue = 60 * (((normalRed - normalGreen) / chroma) + 4)
if convertTo == 'hsl' :
if (lightness != 0) and (lightness != 1) :
saturation = (value - lightness) / min(lightness, 1 - lightness)
return [hue, saturation * 100, lightness * 100]
else : # convert to HSV
if value != 0 :
saturation = chroma / value
return [hue, saturation * 100, value * 100]
# Takes in a list of 3 floats, returns a list of 3 integers
def CMYtoRGB(cmyValues) :
red = (1 - (cmyValues[0] / 100)) * 255
green = (1 - (cmyValues[1] / 100)) * 255
blue = (1 - (cmyValues[2] / 100)) * 255
return [smartRound(red), smartRound(green), smartRound(blue)]
# Takes in a list of 4 floats, returns a list of 3 integers
def CMYKtoRGB(cmykValues) :
x = 1 - (cmykValues[3] / 100)
red = 255 * (1 - (cmykValues[0] / 100)) * x
green = 255 * (1 - (cmykValues[1] / 100)) * x
blue = 255 * (1 - (cmykValues[2] / 100)) * x
return [smartRound(red), smartRound(green), smartRound(blue)]
# Takes in a list with 1 integer and 2 floats (in that order), and returns 3 integers
def HSLorHSVToRGB(values, convertFrom) :
normalSaturation = values[1] / 100
normalLorV= values[2] / 100
# Perform first part of conversion
if convertFrom == 'hsl' :
chroma = (1 - math.fabs((2 * normalLorV) - 1)) * normalSaturation
m = normalLorV - (chroma / 2)
else : # is HSV
chroma = normalLorV * normalSaturation
m = normalLorV - chroma
hPrime = values[0] / 60
temp = (hPrime % 2) - 1
x = chroma * (1 - math.fabs(temp))
if (hPrime >= 0) and (hPrime < 1) :
R = chroma + m
G = x + m
B = 0 + m
return [smartRound(R * 255), smartRound(G * 255), smartRound(B * 255)]
elif (hPrime >= 1) and (hPrime < 2) :
R = x + m
G = chroma + m
B = 0 + m
return [smartRound(R * 255), smartRound(G * 255), smartRound(B * 255)]
elif (hPrime >= 2) and (hPrime < 3) :
R = 0 + m
G = chroma + m
B = x + m
return [smartRound(R * 255), smartRound(G * 255), smartRound(B * 255)]
elif (hPrime >= 3) and (hPrime < 4) :
R = 0 + m
G = x + m
B = chroma + m
return [smartRound(R * 255), smartRound(G * 255), smartRound(B * 255)]
elif (hPrime >= 4) and (hPrime < 5) :
R = x + m
G = 0 + m
B = chroma + m
return [smartRound(R * 255), smartRound(G * 255), smartRound(B * 255)]
elif (hPrime >= 5) and (hPrime <= 6) :
R = chrome + m
G = 0 + m
B = x + m
return [smartRound(R * 255), smartRound(G * 255), smartRound(B * 255)]
else :
print('RGB to HSV/HSL conversion failed')
return
##
# INPUT VALIDATION
##
def detectColorFormat(color, outputFormats) :
if '#' in color :
handleHex(color, outputFormats)
return True
elif 'rgb' in color :
handleRGB(color, outputFormats)
return True
elif 'cmyk' in color :
handleCMYK(color, outputFormats)
return True
elif 'cmy' in color :
handleCMY(color, outputFormats)
return True
elif color.strip().startswith('hsl') :
handleHSVorHSL(color, 'hsl', outputFormats)
return True
elif color.strip().startswith('hsv') :
handleHSVorHSL(color, 'hsv', outputFormats)
return True
else :
return False
# Extracts numerical values from a string.
# Parameters:
# color: string containing value(s)
# numValues: determines how many values to extract (e.g. 3 for RGB, 4 for CMYK)
# isHex: determines if we're looking for hex rather than decimal
# returns: list of extracted color/number values in string form
def extractValues(color, numValues, isHex = False) :
i = 0
tempValue = ''
extractedValues = []
if isHex :
# search for hex values
while i < len(color) :
if (color[i].isnumeric()) or (color[i] in HEX_LETTERS) :
tempValue += color[i]
else :
tempValue = ''
if len(tempValue) == 6 :
extractedValues.append(tempValue)
tempValue = ''
if len(extractedValues) == numValues :
break
i = i + 1
if (len(extractedValues) != numValues) and (len(tempValue) == 6) :
extractedValues.append(tempValue)
else :
# search for decimal values
while i < len(color) :
if color[i].isnumeric() or color[i] == '.' :
tempValue += color[i]
elif len(tempValue) > 0 :
extractedValues.append(tempValue)
tempValue = ''
if len(extractedValues) == numValues :
break
i = i + 1
if (len(extractedValues) != numValues) and (len(tempValue) > 0) :
extractedValues.append(tempValue)
if len(extractedValues) != numValues :
print(f'Could not extract the correct number of values from input: {color}. numValues requested: {numValues}, isHex: {isHex}, Values successfully extracted: {len(extractedValues)}, {extractedValues}')
return False
return extractedValues
# Takes in a string.
# If string contains a valid Hex color code, it gets returned as a string.
def validateHex(value) :
# attempt to extract hex code
hexcode = extractValues(value, 1, True)[0]
if not hexcode :
print('ERROR: Improper format for hex code (see --help)')
return
return hexcode
# Takes in a string. Returns same list as integers if valid RGB values.
def validateRGB(color) :
# extract 3 numbers from the provided values
rgbValues = extractValues(color, 3)
if not rgbValues :
return
intValues = []
for value in rgbValues :
# TODO see if i should smartround here instead of int cast
value = smartRound(value)
if (value < 0) or (value > 255) :
print('ERROR: Each RBG value must be between 0-255')
return
intValues.append(value)
return intValues
# Takes in a list of 3 or 4 strings. Returns same list as integers if valid CMYK values.
def validateCMYorCMYK(color, include_K) :
if include_K :
values = extractValues(color, 4)
else :
values = extractValues(color, 3)
if not values :
return
floatValues = []
for value in values :
value = float(value)
if (value < 0) or (value > 100) :
print('ERROR: Each CMY(K) value must be between 0,0-100.0(%)')
return
floatValues.append(value)
return floatValues
# Takes in a list of 3 strings. Returns same list as 1 integer and 2 floats
def validateHSLorHSV(color) :
color = extractValues(color, 3)
if color is None :
return
for i in range(3) :
if i == 0 :
color[i] = smartRound(color[i])
else :
color[i] = float(color[i])
if (color[0] < 0) or (color[0] > 255) :
print('ERROR: Invalid hue value (should be 0-255)')
return
if (color[1] < 0) or (color[1] > 100) :
print('ERROR: Invalid saturation value (should be 0.0-100.0)')
return
if (color[2] < 0) or (color[2] > 100) :
print('ERROR: Invalid lightness/value value (should be 0.0-100.0)')
return
return [color[0], color[1], color[2]]
##
# GENERAL UTILITIES
##
# takes in a map of color values and either prints their values to STDOUT or into a specified file
def printConversions(convertedValues) :
# format converted values into better output strings
output = []
for colorFormat in TYPES :
if convertedValues.get(colorFormat, None) is None :
continue
elif colorFormat == 'hex' :
hexCode = '#' + convertedValues['hex']
output.append(hexCode)
else :
# format strings for xyz(a,b,c) type formats
colorCode = colorFormat + '('
numValues = len(convertedValues[colorFormat])
for valueIndex in range(numValues) :
value = convertedValues[colorFormat][valueIndex]
if type(value) == float :
value = f"{value:.2f}"
if valueIndex == numValues - 1 :
colorCode += str(value) + ')'
else :
colorCode += str(value) + ', '
output.append(colorCode)
# determine how to deliver output
if OUTPUT != 'stdout' :
if APPEND :
with open(OUTPUT, 'a', encoding='utf-8') as file :
for color in output :
file.write(color + '\n')
file.write('\n')
else :
with open(OUTPUT, 'w', encoding='utf-8') as file :
for color in output :
file.write(color + '\n')
file.write('\n')
else :
for color in output :
print(color)
print()
# Takes in a float, returns the nearest integer
def smartRound(value) :
value = float(value)
if (value % 1) > .50 :
return math.ceil(value)
else :
return math.floor(value)
# Takes in a decimal number and converts it to hexadecimal
def hex(number) :
number = int(number)
if number > 16 :
print("ERROR: Decimal to Hexidecimal conversion failed")
return "ERROR: Decimal to Hexidecimal conversion failed"
if number < 10 :
return str(number)
return HEX_LETTERS[number % 10]
''' init '''
if __name__ == '__main__' :
main()