overhauled format handler functions to be more generic / easily

expandable. all coversions functions have been verified. file input
sanitation now more robust.
This commit is contained in:
2025-01-20 13:35:03 -05:00
parent 22838a24da
commit 0d0f27b944
+233 -220
View File
@@ -1,30 +1,24 @@
import math import math
import argparse 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'] TYPES = ['hex', 'rgb', 'cmy', 'cmyk', 'hsl', 'hsv']
# look into LAB # look into LAB
HEX_LETTERS = ['a', 'b', 'c', 'd', 'e', 'f'] HEX_LETTERS = ['a', 'b', 'c', 'd', 'e', 'f']
OUTPUT = 'stdout' OUTPUT = 'stdout'
APPEND = False
VERBOSE = False VERBOSE = False
def main(): def main():
# Set up arguments ''' ESTABLISH ARGS '''
parser = argparse.ArgumentParser(prog='color-converter', parser = argparse.ArgumentParser(prog='color-converter', description='Color code converting utility written in Python.', epilog='Hope this helps :)')
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('-hex', action='store_true', help='convert/output to Hex')
parser.add_argument('-rgb', action='store_true', help='convert/output to RGB') 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('-cmy', action='store_true', help='convert/output to CMY')
parser.add_argument('-cmyk', action='store_true', help='convert/output to CMYK') parser.add_argument('-cmyk', action='store_true', help='convert/output to CMYK (CMYK conversions are uncalibrated)')
parser.add_argument('-hsl', action='store_true', help='convert/output to HSL') 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('-hsv', action='store_true', help='convert/output to HSV')
@@ -36,19 +30,15 @@ def main():
parser.add_argument('-isHsv', 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('--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('--output', '-o', help='the name of a 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('--append', '-a', action='store_true', help='append rather than overwrite when outputting to file rather than stdout')
parser.add_argument('--verbose', '-v', action='store_true', help='print when performing conversions') 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)') parser.add_argument('color', nargs='*', help='a color in Hex, RGB, CMY, CMYK, HSL or HSV format')
args = parser.parse_args() args = parser.parse_args()
# debug print ''' PROCESS ARGS '''
# print(vars(args))
''' ARGS PROCESSING '''
# determine which conversions to perform # determine which conversions to perform
outputFormats = [] outputFormats = []
flagsActive = 0 flagsActive = 0
@@ -62,13 +52,14 @@ def main():
for colorFormat in TYPES : for colorFormat in TYPES :
outputFormats.append(colorFormat) outputFormats.append(colorFormat)
# if output should be written to file, set global vars for reference later # determine if output should be (over?)written to file
if args.output : if args.output :
global OUTPUT global OUTPUT
OUTPUT = args.output OUTPUT = args.output
if args.append : if not args.append :
global APPEND output_file = open(OUTPUT, 'w')
APPEND = True output_file.write('')
output_file.close()
# print conversion updates? # print conversion updates?
if args.verbose : if args.verbose :
@@ -76,20 +67,20 @@ def main():
VERBOSE = True VERBOSE = True
''' PARSE INPUTTED COLORS ''' ''' PARSE INPUTTED COLORS '''
colorCodes = [] colorCodes = []
# if provided a file of values # if provided a file of values
if args.input : if args.input :
with open(args.input, 'r', encoding='utf-8') as file : with open(args.input, 'r', encoding='utf-8') as file :
for line in file : for line in file :
colorCodes.append(line.strip()) line = line.strip()
if line != '' :
colorCodes.append(line)
# else grab from stdin # else grab from stdin
else : else :
colorCodes = args.color colorCodes = args.color
''' PROCESS COLORS ''' ''' PROCESS COLORS '''
for color in colorCodes : for color in colorCodes :
# Try to automatically handle value # Try to automatically handle value
@@ -112,180 +103,169 @@ def main():
else : else :
print('ERROR: Could not detect inputted color format and no fallback flag was specified. see --help for more information on usage.') print('ERROR: Could not detect inputted color format and no fallback flag was specified. see --help for more information on usage.')
return return
return return
'''
##
# FORMAT HANDLERS # FORMAT HANDLERS
## '''
# ARGS # ARGS
# color: string containing hex code # color: string containing hex code
# outputFormats: list indicating which conversions to perform # outputFormats: list indicating which conversions to perform
def handleHex(color, outputFormats) : def handleHex(color, outputFormats) :
hexCode = validateHex(color) # validate/sanitize input
if hexCode is None : hexcode = validateHex(color)
if hexcode is None :
return return
# collect conversion results
results = {}
if VERBOSE : if VERBOSE :
print('CONVERTING HEX: ', hexCode) results['verbose-msg'] = 'CONVERTING HEX: ' + hexcode
outputs = {} rgbValues = HEXtoRGB(hexcode)
rgbValues = HEXtoRGB(hexCode) for colorFormat in TYPES :
if 'hex' in outputFormats : if colorFormat in outputFormats and colorFormat == 'hex' :
outputs['hex'] = hexCode results['hex'] = hexcode
if 'rgb' in outputFormats : elif colorFormat in outputFormats and colorFormat == 'rgb' :
outputs['rgb'] = rgbValues results['rgb'] = rgbValues
if 'cmy' in outputFormats : elif colorFormat in outputFormats :
outputs['cmy'] = RGBtoCMY(rgbValues) results[colorFormat] = rgbTo(colorFormat, 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) printConversions(results)
# ARGS # ARGS
# color: string containing RGB code # color: string containing RGB code
# outputFormats: list indicating which conversions to perform # outputFormats: list indicating which conversions to perform
def handleRGB(color, outputFormats) : def handleRGB(color, outputFormats) :
# validate/sanitize input
rgbValues = validateRGB(color) rgbValues = validateRGB(color)
if rgbValues is None : if rgbValues is None :
return return
# collect conversion results
results = {}
if VERBOSE : if VERBOSE :
print('CONVERTING RGB: ', rgbValues) results['verbose-msg'] = 'CONVERTING RGB: ' + str(rgbValues)
outputs = {} for colorFormat in TYPES :
if 'hex' in outputFormats : if colorFormat in outputFormats and colorFormat == 'rgb' :
outputs['hex'] = RGBtoHEX(rgbValues) results['rgb'] = rgbValues
if 'rgb' in outputFormats : elif colorFormat in outputFormats :
outputs['rgb'] = rgbValues results[colorFormat] = rgbTo(colorFormat, 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) printConversions(results)
# ARGS # ARGS
# color: string containing CMY code # color: string containing CMY code
# outputFormats: list indicating which conversions to perform # outputFormats: list indicating which conversions to perform
def handleCMY(color, outputFormats) : def handleCMY(color, outputFormats) :
# validate/sanitize input
cmyValues = validateCMYorCMYK(color, False) cmyValues = validateCMYorCMYK(color, False)
if cmyValues is None : if cmyValues is None :
return return
# collect conversion results
results = {}
if VERBOSE : if VERBOSE :
print('CONVERTING CMY: ', cmyValues) results['verbose-msg'] = 'CONVERTING CMY: ' + str(cmyValues)
outputs = {}
rgbValues = CMYtoRGB(cmyValues) rgbValues = CMYtoRGB(cmyValues)
if 'hex' in outputFormats : for colorFormat in TYPES :
outputs['hex'] = RGBtoHEX(rgbValues) if colorFormat in outputFormats and colorFormat == 'rgb' :
if 'rgb' in outputFormats : results['rgb'] = rgbValues
outputs['rgb'] = rgbValues elif colorFormat in outputFormats and colorFormat == 'cmy' :
if 'cmy' in outputFormats : results['cmy'] = cmyValues
outputs['cmy'] = cmyValues elif colorFormat in outputFormats :
if 'cmyk' in outputFormats : results[colorFormat] = rgbTo(colorFormat, rgbValues)
outputs['cmyk'] = RGBtoCMYK(rgbValues)
if 'hsl' in outputFormats :
outputs['hsl'] = RGBtoHSVorHSL(rgbValues, 'hsl')
if 'hsv' in outputFormats :
outputs['hsv'] = RGBtoHSVorHSL(rgbValues, 'hsv')
printConversions(outputs) printConversions(results)
# ARGS # ARGS
# color: string containing CMYK code # color: string containing CMYK code
# outputFormats: list indicating which conversions to perform # outputFormats: list indicating which conversions to perform
def handleCMYK(color, outputFormats) : def handleCMYK(color, outputFormats) :
# validate/sanitize input
cmykValues = validateCMYorCMYK(color, True) cmykValues = validateCMYorCMYK(color, True)
if cmykValues is None : if cmykValues is None :
return return
# collect conversion results
results = {}
if VERBOSE : if VERBOSE :
print('CONVERTING CMYK: ', cmykValues) results['verbose-msg'] = 'CONVERTING CMYK: ' + str(cmykValues)
outputs = {}
rgbValues = CMYKtoRGB(cmykValues) rgbValues = CMYKtoRGB(cmykValues)
if 'hex' in outputFormats : for colorFormat in TYPES :
outputs['hex'] = RGBtoHEX(rgbValues) if (colorFormat in outputFormats) and (colorFormat == 'rgb') :
if 'rgb' in outputFormats : results['rgb'] = rgbValues
outputs['rgb'] = rgbValues elif (colorFormat in outputFormats) and (colorFormat == 'cmyk') :
if 'cmy' in outputFormats : results['cmyk'] = cmykValues
outputs['cmy'] = RGBtoCMY(rgbValues) elif colorFormat in outputFormats :
if 'cmyk' in outputFormats : results[colorFormat] = rgbTo(colorFormat, rgbValues)
outputs['cmyk'] = cmykValues
if 'hsl' in outputFormats :
outputs['hsl'] = RGBtoHSVorHSL(rgbValues, 'hsl')
if 'hsv' in outputFormats :
outputs['hsv'] = RGBtoHSVorHSL(rgbValues, 'hsv')
printConversions(outputs) printConversions(results)
# ARGS # ARGS
# color: string containing CMY code # color: string containing CMY code
# outputFormats: list indicating which conversions to perform # outputFormats: list indicating which conversions to perform
# handle: string ('hsl' or 'hsv'), indicating which format to handle # handle: string ('hsl' or 'hsv'), indicating which format to handle
def handleHSVorHSL(color, handle, outputFormats) : def handleHSVorHSL(color, handle, outputFormats) :
# collect conversion results
validated = validateHSLorHSV(color) validated = validateHSLorHSV(color)
if validated is None : if validated is None :
return return
# collect conversion results
results = {}
if VERBOSE : if VERBOSE :
if handle == 'hsl' : if handle == 'hsl' :
print('CONVERTING HSL: ', color) results['verbose-msg'] = 'CONVERTING HSL: ' + str(validated)
else : else :
print('CONVERTING HSV: ', color) results['verbose-msg'] = 'CONVERTING HSV: ' + str(validated)
outputs = {}
rgbValues = HSLorHSVToRGB(validated, handle) rgbValues = HSLorHSVToRGB(validated, handle)
if 'hex' in outputFormats : for colorFormat in TYPES :
outputs['hex'] = RGBtoHEX(rgbValues) if (colorFormat in outputFormats) and (colorFormat == 'rgb') :
if 'rgb' in outputFormats : results['rgb'] = rgbValues
outputs['rgb'] = rgbValues elif (colorFormat in outputFormats) and (colorFormat == 'hsl') and (handle == 'hsl') :
if 'cmy' in outputFormats : results['hsl'] = validated
outputs['cmy'] = RGBtoCMY(rgbValues) elif (colorFormat in outputFormats) and (colorFormat == 'hsv') and (handle == 'hsv'):
if 'cmyk' in outputFormats : results['hsv'] = validated
outputs['cmyk'] = RGBtoCMYK(rgbValues) elif colorFormat in outputFormats :
if 'hsl' in outputFormats : results[colorFormat] = rgbTo(colorFormat, rgbValues)
outputs['hsl'] = RGBtoHSVorHSL(rgbValues, 'hsl')
if 'hsv' in outputFormats :
outputs['hsv'] = RGBtoHSVorHSL(rgbValues, 'hsv')
printConversions(outputs) printConversions(results)
## '''
# CONVERSION FUNCTIONS # CONVERSION FUNCTIONS
'''
## ##
# RGB -> OTHER FORMATS SECTION
###
# Takes in a string, returns a list of 3 integers # rgbTo() acts as an interface between format handlers and RGB conversions (for easier addition of formats later)
def HEXtoRGB(hexCode) : # ARGS
rgbValues = [] # colorFormat: a string containing a color format to convert to
# rgbValues: the rgbValues to convert
tempSum = 0 # RETURNS
i = 0 # the converted color values in the specified format
while i < 6 : def rgbTo(colorFormat, rgbValues) :
tempSum += int(hexCode[i], 16) * 16 if colorFormat == 'hex' :
tempSum += int(hexCode[i+1], 16) return RGBtoHEX(rgbValues)
rgbValues.append(tempSum) if colorFormat == 'cmy' :
i = i + 2 return RGBtoCMY(rgbValues)
tempSum = 0 if colorFormat == 'cmyk' :
return RGBtoCMYK(rgbValues)
return rgbValues if colorFormat == 'hsl' :
return RGBtoHSVorHSL(rgbValues, 'hsl')
if colorFormat == 'hsv' :
return RGBtoHSVorHSL(rgbValues, 'hsv')
# Takes in a list of 3 integers, returns a string # Takes in a list of 3 integers, returns a string
def RGBtoHEX(rgbValues) : def RGBtoHEX(rgbValues) :
@@ -371,6 +351,24 @@ def RGBtoHSVorHSL(rgbValues, convertTo) :
return [hue, saturation * 100, value * 100] return [hue, saturation * 100, value * 100]
##
# OTHER FORMATS -> RGB SECTION
##
# 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 floats, returns a list of 3 integers # Takes in a list of 3 floats, returns a list of 3 integers
def CMYtoRGB(cmyValues) : def CMYtoRGB(cmyValues) :
red = (1 - (cmyValues[0] / 100)) * 255 red = (1 - (cmyValues[0] / 100)) * 255
@@ -434,15 +432,100 @@ def HSLorHSVToRGB(values, convertFrom) :
R = chrome + m R = chrome + m
G = 0 + m G = 0 + m
B = x + m B = x + m
return [smartRound(R * 255), smartRound(G * 255), smartRound(B * 255)] return [int(R * 255), int(G * 255), int(B * 255)]
else : else :
print('RGB to HSV/HSL conversion failed') print('RGB to HSV/HSL conversion failed')
return return
##
# INPUT VALIDATION
##
'''
# INPUT VALIDATION
'''
# 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
'''
# ARGS
# color: a string containing a color value in an unknown format
# outputFormats: a list containing information necessary for handling conversions, used if format is detected
# RETURNS
# truth if success, false if failure to detect a format
def detectColorFormat(color, outputFormats) : def detectColorFormat(color, outputFormats) :
if '#' in color : if '#' in color :
handleHex(color, outputFormats) handleHex(color, outputFormats)
@@ -466,13 +549,13 @@ def detectColorFormat(color, outputFormats) :
return False return False
# Extracts numerical values from a string. # Extracts numerical (hex or decimal) values from a string.
# Parameters: # ARGS
# color: string containing value(s) # color: string containing value(s)
# numValues: determines how many values to extract (e.g. 3 for RGB, 4 for CMYK) # 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 # isHex: determines if we're looking for hex rather than decimal
# RETURNS
# returns: list of extracted color/number values in string form # a list of extracted color/number values in string form (or false if faulure to extract)
def extractValues(color, numValues, isHex = False) : def extractValues(color, numValues, isHex = False) :
i = 0 i = 0
tempValue = '' tempValue = ''
@@ -519,90 +602,16 @@ def extractValues(color, numValues, isHex = False) :
return extractedValues return extractedValues
# formats and delivers color values to STDOUT or a specified output file
# Takes in a string. # ARGS
# If string contains a valid Hex color code, it gets returned as a string. # convertedValues: a map of the results from the color translations that were performed on a single color value,
def validateHex(value) : # where the key indicates the format and the value is a list of raw numeric values
# 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) : def printConversions(convertedValues) :
# format converted values into better output strings
output = [] output = []
if VERBOSE :
output.append(convertedValues['verbose-msg'])
# format converted values into better output strings
for colorFormat in TYPES : for colorFormat in TYPES :
if convertedValues.get(colorFormat, None) is None : if convertedValues.get(colorFormat, None) is None :
continue continue
@@ -623,18 +632,22 @@ def printConversions(convertedValues) :
colorCode += str(value) + ', ' colorCode += str(value) + ', '
output.append(colorCode) output.append(colorCode)
# determine how to deliver output # deliver formatted output
if OUTPUT != 'stdout' : if OUTPUT != 'stdout' :
if APPEND :
'''
TODO FOR EFFICIENCY:
right now, unless i'm mistaken and there's some hidden optimization going on, this is
going to open and close the file to write the conversions of each inputted color code.
if there's a huge file being used as input, this may get slow, so perhaps the best move is
to actually open the file back in main() at the point when we evaluate args, and then close
it after everything in main() has ran... food for thought.
'''
with open(OUTPUT, 'a', encoding='utf-8') as file : with open(OUTPUT, 'a', encoding='utf-8') as file :
for color in output : for color in output :
file.write(color + '\n') file.write(color + '\n')
file.write('\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 : else :
for color in output : for color in output :
print(color) print(color)