From 0d0f27b944d701553ec0e6fdc7885bb4a3297b82 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 20 Jan 2025 13:35:03 -0500 Subject: overhauled format handler functions to be more generic / easily expandable. all coversions functions have been verified. file input sanitation now more robust. --- color-converter.py | 465 +++++++++++++++++++++++++++-------------------------- 1 file changed, 239 insertions(+), 226 deletions(-) diff --git a/color-converter.py b/color-converter.py index 9f5620d..a0123e0 100644 --- a/color-converter.py +++ b/color-converter.py @@ -1,30 +1,24 @@ 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 :)') - + ''' ESTABLISH ARGS ''' + 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('-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('-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('--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('--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 when outputting to file rather than stdout') 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() - # debug print - # print(vars(args)) - - - ''' ARGS PROCESSING ''' + ''' PROCESS ARGS ''' # determine which conversions to perform outputFormats = [] flagsActive = 0 @@ -62,13 +52,14 @@ def main(): for colorFormat in TYPES : 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 : global OUTPUT OUTPUT = args.output - if args.append : - global APPEND - APPEND = True + if not args.append : + output_file = open(OUTPUT, 'w') + output_file.write('') + output_file.close() # print conversion updates? if args.verbose : @@ -76,20 +67,20 @@ def main(): 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()) + line = line.strip() + if line != '' : + colorCodes.append(line) # else grab from stdin else : colorCodes = args.color - ''' PROCESS COLORS ''' for color in colorCodes : # Try to automatically handle value @@ -112,180 +103,169 @@ def main(): 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 +# outputFormats: list indicating which conversions to perform def handleHex(color, outputFormats) : - hexCode = validateHex(color) - if hexCode is None : + # validate/sanitize input + hexcode = validateHex(color) + if hexcode is None : return + # collect conversion results + results = {} 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) + results['verbose-msg'] = 'CONVERTING HEX: ' + hexcode + + rgbValues = HEXtoRGB(hexcode) + for colorFormat in TYPES : + if colorFormat in outputFormats and colorFormat == 'hex' : + results['hex'] = hexcode + elif colorFormat in outputFormats and colorFormat == 'rgb' : + results['rgb'] = rgbValues + elif colorFormat in outputFormats : + results[colorFormat] = rgbTo(colorFormat, rgbValues) + + printConversions(results) # ARGS # color: string containing RGB code # outputFormats: list indicating which conversions to perform def handleRGB(color, outputFormats) : + # validate/sanitize input rgbValues = validateRGB(color) if rgbValues is None : return + # collect conversion results + results = {} if VERBOSE : - print('CONVERTING RGB: ', rgbValues) + results['verbose-msg'] = 'CONVERTING RGB: ' + str(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') + for colorFormat in TYPES : + if colorFormat in outputFormats and colorFormat == 'rgb' : + results['rgb'] = rgbValues + elif colorFormat in outputFormats : + results[colorFormat] = rgbTo(colorFormat, rgbValues) - printConversions(outputs) + printConversions(results) # ARGS # color: string containing CMY code # outputFormats: list indicating which conversions to perform def handleCMY(color, outputFormats) : + # validate/sanitize input cmyValues = validateCMYorCMYK(color, False) if cmyValues is None : return + # collect conversion results + results = {} if VERBOSE : - print('CONVERTING CMY: ', cmyValues) + results['verbose-msg'] = 'CONVERTING CMY: ' + str(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) + for colorFormat in TYPES : + if colorFormat in outputFormats and colorFormat == 'rgb' : + results['rgb'] = rgbValues + elif colorFormat in outputFormats and colorFormat == 'cmy' : + results['cmy'] = cmyValues + elif colorFormat in outputFormats : + results[colorFormat] = rgbTo(colorFormat, rgbValues) + + printConversions(results) # ARGS # color: string containing CMYK code # outputFormats: list indicating which conversions to perform def handleCMYK(color, outputFormats) : + # validate/sanitize input cmykValues = validateCMYorCMYK(color, True) if cmykValues is None : return + # collect conversion results + results = {} if VERBOSE : - print('CONVERTING CMYK: ', cmykValues) + results['verbose-msg'] = 'CONVERTING CMYK: ' + str(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) + for colorFormat in TYPES : + if (colorFormat in outputFormats) and (colorFormat == 'rgb') : + results['rgb'] = rgbValues + elif (colorFormat in outputFormats) and (colorFormat == 'cmyk') : + results['cmyk'] = cmykValues + elif colorFormat in outputFormats : + results[colorFormat] = rgbTo(colorFormat, rgbValues) + + printConversions(results) # 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) : + # collect conversion results validated = validateHSLorHSV(color) if validated is None : return + # collect conversion results + results = {} if VERBOSE : if handle == 'hsl' : - print('CONVERTING HSL: ', color) + results['verbose-msg'] = 'CONVERTING HSL: ' + str(validated) else : - print('CONVERTING HSV: ', color) + results['verbose-msg'] = 'CONVERTING HSV: ' + str(validated) - 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') + for colorFormat in TYPES : + if (colorFormat in outputFormats) and (colorFormat == 'rgb') : + results['rgb'] = rgbValues + elif (colorFormat in outputFormats) and (colorFormat == 'hsl') and (handle == 'hsl') : + results['hsl'] = validated + elif (colorFormat in outputFormats) and (colorFormat == 'hsv') and (handle == 'hsv'): + results['hsv'] = validated + elif colorFormat in outputFormats : + results[colorFormat] = rgbTo(colorFormat, rgbValues) - printConversions(outputs) + printConversions(results) -## -# CONVERSION FUNCTIONS -## +''' +# CONVERSION FUNCTIONS +''' -# Takes in a string, returns a list of 3 integers -def HEXtoRGB(hexCode) : - rgbValues = [] +## +# RGB -> OTHER FORMATS SECTION +### - 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 +# rgbTo() acts as an interface between format handlers and RGB conversions (for easier addition of formats later) +# ARGS +# colorFormat: a string containing a color format to convert to +# rgbValues: the rgbValues to convert +# RETURNS +# the converted color values in the specified format +def rgbTo(colorFormat, rgbValues) : + if colorFormat == 'hex' : + return RGBtoHEX(rgbValues) + if colorFormat == 'cmy' : + return RGBtoCMY(rgbValues) + if colorFormat == 'cmyk' : + return RGBtoCMYK(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 def RGBtoHEX(rgbValues) : @@ -371,6 +351,24 @@ def RGBtoHSVorHSL(rgbValues, convertTo) : 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 def CMYtoRGB(cmyValues) : red = (1 - (cmyValues[0] / 100)) * 255 @@ -434,91 +432,15 @@ def HSLorHSVToRGB(values, convertFrom) : R = chrome + m G = 0 + 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 : 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 and tempValue != '.' : - extractedValues.append(tempValue) - tempValue = '' - else : - 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 - +''' +# INPUT VALIDATION +''' # Takes in a string. # If string contains a valid Hex color code, it gets returned as a string. @@ -564,7 +486,7 @@ def validateCMYorCMYK(color, include_K) : 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(%)') + print('ERROR: Each CMY(K) value must be between 0,0-100.0') return floatValues.append(value) @@ -595,14 +517,101 @@ def validateHSLorHSV(color) : 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 +# 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) : + 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 (hex or decimal) values from a string. +# ARGS +# 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 +# a list of extracted color/number values in string form (or false if faulure to extract) +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 and tempValue != '.' : + extractedValues.append(tempValue) + tempValue = '' + else : + 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 + + +# formats and delivers color values to STDOUT or a specified output file +# ARGS +# convertedValues: a map of the results from the color translations that were performed on a single color value, +# where the key indicates the format and the value is a list of raw numeric values +def printConversions(convertedValues) : output = [] + if VERBOSE : + output.append(convertedValues['verbose-msg']) + + # format converted values into better output strings for colorFormat in TYPES : if convertedValues.get(colorFormat, None) is None : continue @@ -623,18 +632,22 @@ def printConversions(convertedValues) : colorCode += str(value) + ', ' output.append(colorCode) - # determine how to deliver output + # deliver formatted 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') + + ''' + 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 : + for color in output : + file.write(color + '\n') + file.write('\n') else : for color in output : print(color) -- cgit v1.3.1-4-g156e