mirror of
https://github.com/OPSnet/Gazelle.git
synced 2026-01-16 18:04:34 -05:00
304 lines
9.6 KiB
Python
Executable File
304 lines
9.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""
|
|
# This is a development tool to see the colors in a stylesheet, via HTML in the browser.
|
|
# Works best on SCSS files or any textfile that defines colors.
|
|
# Typical Usage:
|
|
# python view-palette _variables.scss
|
|
# Copy and paste:
|
|
# python view-palette ../Gazelle-repo/sass/apollostage/_variables.scss
|
|
# python view-palette ../Gazelle-repo/sass/apollostage_paper/style.scss
|
|
# python view-palette ../Gazelle-repo/sass/apollostage_coffee/style.scss
|
|
# python view-palette ../Gazelle-repo/sass/apollostage_sunset/style.scss
|
|
|
|
Public domain via unlicense; see http://unlicense.org
|
|
|
|
Requirements fulfilled by this code:
|
|
1. The program takes a text file (like a CSS stylesheet) as a parameter from the command line, or if none is provided, prompts for a filename.
|
|
2. The file is expected to be a text file containing color codes and optional color names. Color codes may be separated by spaces, commas, or carriage returns.
|
|
3. When reading the input file, all paragraphs without any hex color codes (like #003399) are considered labels.
|
|
4. If a paragraph includes text before a color code, that text is treated as the name of the color. Optional color names should be displayed in the same table cell as the color swatch, directly above the swatch.
|
|
5. Any text after the last valid color code in a line is dropped from the input file.
|
|
6. Swatches should be displayed horizontally within a table layout, with captions (RGB, HSL, Hex code) directly below each swatch.
|
|
7. Each swatch includes its RGB and HSL values in the format suitable for CSS.
|
|
8. If the output file already exists, the program increments the file number in the filename, e.g., output2.html.
|
|
9. The output HTML file should include a button to toggle the background color between white, 50% gray, and black using JavaScript.
|
|
"""
|
|
|
|
import sys
|
|
import re
|
|
import os
|
|
import colorsys
|
|
|
|
|
|
def read_file(filename):
|
|
with open(filename, "r") as file:
|
|
return file.readlines()
|
|
|
|
|
|
def hex_to_rgb(hex_code):
|
|
hex_code = hex_code.lstrip("#")
|
|
|
|
# Expand short color codes to full form if necessary
|
|
if len(hex_code) == 3:
|
|
hex_code = "".join([char * 2 for char in hex_code])
|
|
|
|
if len(hex_code) == 6 and all(c in "0123456789ABCDEFabcdef" for c in hex_code):
|
|
# Check if hex_code is a valid 6-character hexadecimal string
|
|
return tuple(int(hex_code[i : i + 2], 16) for i in (0, 2, 4))
|
|
else:
|
|
print(f"Invalid hexadecimal color code: {hex_code}")
|
|
return None # Return None for invalid color codes
|
|
|
|
|
|
def rgb_to_hsl(rgb):
|
|
r, g, b = rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0
|
|
max_val = max(r, g, b)
|
|
min_val = min(r, g, b)
|
|
l = (max_val + min_val) / 2.0
|
|
|
|
if max_val == min_val:
|
|
h = s = 0.0 # achromatic
|
|
else:
|
|
delta = max_val - min_val
|
|
if l > 0.5:
|
|
s = delta / (2.0 - max_val - min_val)
|
|
else:
|
|
s = delta / (max_val + min_val)
|
|
|
|
if max_val == r:
|
|
h = (g - b) / delta + (6 if g < b else 0)
|
|
elif max_val == g:
|
|
h = (b - r) / delta + 2
|
|
elif max_val == b:
|
|
h = (r - g) / delta + 4
|
|
|
|
h /= 6
|
|
|
|
h = h * 360 # Convert to degrees
|
|
s = s * 100 # Convert to percentage
|
|
l = l * 100 # Convert to percentage
|
|
|
|
return h, s, l
|
|
|
|
|
|
def format_hsl(h_degrees, s_percent, l_percent):
|
|
h_degrees = round(h_degrees)
|
|
s_percent = round(s_percent)
|
|
l_percent = round(l_percent)
|
|
return f"hsl({h_degrees}, {s_percent}%, {l_percent}%)"
|
|
|
|
|
|
def format_rgb_255(rgb):
|
|
r, g, b = rgb
|
|
return f"rgb({r}, {g}, {b})"
|
|
|
|
|
|
def rgb_to_hsv(rgb):
|
|
r, g, b = rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0
|
|
|
|
max_val = max(r, g, b)
|
|
min_val = min(r, g, b)
|
|
v = max_val
|
|
|
|
delta = max_val - min_val
|
|
if max_val == 0:
|
|
s = 0
|
|
else:
|
|
s = delta / max_val
|
|
|
|
if delta == 0:
|
|
h = 0
|
|
else:
|
|
if max_val == r:
|
|
h = (g - b) / delta + (6 if g < b else 0)
|
|
elif max_val == g:
|
|
h = (b - r) / delta + 2
|
|
elif max_val == b:
|
|
h = (r - g) / delta + 4
|
|
h /= 6
|
|
|
|
h = h * 360
|
|
s = s * 100
|
|
v = v * 100
|
|
return h, s, v
|
|
|
|
|
|
def format_hsv(h_degrees, s_percent, v_percent):
|
|
h_degrees = round(h_degrees)
|
|
s_percent = round(s_percent)
|
|
v_percent = round(v_percent)
|
|
return f"hsv({h_degrees}, {s_percent}%, {v_percent}%)"
|
|
|
|
|
|
def split_around_first_hex_color_code(text):
|
|
# Returns before, hex_code, after or text, None, None if no hex code is found
|
|
|
|
# Define the regex pattern for hex color codes
|
|
hex_color_pattern = r"#(?:[0-9a-fA-F]{3}){1,2}\b"
|
|
|
|
# Use re.search() to find the first match in the string
|
|
match = re.search(hex_color_pattern, text)
|
|
|
|
if match:
|
|
# Extract the start and end indices of the match
|
|
start, end = match.span()
|
|
# Split the text into three parts
|
|
before = text[:start]
|
|
hex_code = match.group(0)
|
|
after = text[end:]
|
|
return before, hex_code, after
|
|
else:
|
|
# If no match is found, return the original text as the 'before' part, and None for the other parts
|
|
return text, None, None
|
|
|
|
|
|
def parse_content(lines):
|
|
parsed_data = []
|
|
current_label = None
|
|
current_swatches = []
|
|
|
|
for line in lines:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
|
|
before, hex_code, after = split_around_first_hex_color_code(line)
|
|
|
|
if not hex_code: # Line has no hex color code
|
|
if current_label is not None and current_swatches:
|
|
parsed_data.append((current_label, current_swatches))
|
|
current_label = line
|
|
current_swatches = []
|
|
else: # Line has one or more hex color codes
|
|
while hex_code:
|
|
current_color_name = before.strip() if before.strip() else None
|
|
current_swatches.append((current_color_name, hex_code))
|
|
|
|
line = after.strip()
|
|
before, hex_code, after = split_around_first_hex_color_code(line)
|
|
|
|
if current_label is not None and current_swatches:
|
|
parsed_data.append((current_label, current_swatches))
|
|
|
|
return parsed_data
|
|
|
|
|
|
def generate_html(parsed_data, output_filename, input_filename):
|
|
html_content = """
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Color Swatches</title>
|
|
<style>
|
|
body {
|
|
transition: background-color 0.05s ease;
|
|
}
|
|
table {
|
|
border-collapse: collapse;
|
|
}
|
|
td {
|
|
border: thin solid gray;
|
|
padding: 5px;
|
|
}
|
|
.swatch {
|
|
width: 50px;
|
|
height: 50px;
|
|
}
|
|
.info {
|
|
font-size: 8px;
|
|
}
|
|
.name {
|
|
font-size: 8px;
|
|
font-weight: bold;
|
|
}
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, Inter, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, sans-serif;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Toggle button to change background color -->
|
|
<button onclick="toggleBackground()">Toggle Background</button>
|
|
|
|
<script>
|
|
function toggleBackground() {
|
|
var body = document.body;
|
|
var currentColor = body.style.backgroundColor;
|
|
if (currentColor === "rgb(255, 255, 255)") {
|
|
body.style.backgroundColor = "rgb(128, 128, 128)"; // 50% gray
|
|
} else if (currentColor === "rgb(128, 128, 128)") {
|
|
body.style.backgroundColor = "rgb(40,40,40)"; // near-Black
|
|
} else {
|
|
body.style.backgroundColor = "rgb(255, 255, 255)"; // White
|
|
}
|
|
}
|
|
</script>
|
|
|
|
|
|
"""
|
|
|
|
html_content += f"<h1>Input Filename: {input_filename}</h1>\n"
|
|
for label, swatches in parsed_data:
|
|
html_content += f"<h2>{label}</h2>\n"
|
|
html_content += "<table>\n<tr valign=top>\n"
|
|
for color_name, swatch in swatches:
|
|
rgb = hex_to_rgb(swatch)
|
|
h, s, l = rgb_to_hsl(rgb)
|
|
hsl = format_hsl(h, s, l)
|
|
h, s, v = rgb_to_hsv(rgb)
|
|
hsv = format_hsv(h, s, v)
|
|
rgb_255 = format_rgb_255(rgb)
|
|
html_content += f"<td>\n"
|
|
html_content += (
|
|
f'<div class="swatch" style="background-color: {swatch};"></div>\n'
|
|
)
|
|
html_content += f'<div class="info">{rgb_255}</div>\n'
|
|
html_content += f'<div class="info">{hsl}</div>\n'
|
|
html_content += f'<div class="info">{hsv}</div>\n'
|
|
html_content += f'<div class="info">{swatch}</div>\n'
|
|
if color_name:
|
|
html_content += f'<div class="name">{color_name}</div>\n'
|
|
html_content += "</td>\n"
|
|
html_content += "</tr>\n</table>\n"
|
|
|
|
html_content += """
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
with open(output_filename, "w") as file:
|
|
file.write(html_content)
|
|
|
|
print(f"HTML file generated: {output_filename}")
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) > 1:
|
|
input_filename = sys.argv[1]
|
|
else:
|
|
input_filename = input("Enter the input filename: ")
|
|
|
|
while not os.path.isfile(input_filename):
|
|
print("File not found.")
|
|
input_filename = input("Enter the input filename: ")
|
|
|
|
lines = read_file(input_filename)
|
|
parsed_data = parse_content(lines)
|
|
|
|
output_filename = "output.html"
|
|
if os.path.isfile(output_filename):
|
|
filename, extension = os.path.splitext(output_filename)
|
|
count = 2
|
|
while os.path.isfile(f"{filename}{count}{extension}"):
|
|
count += 1
|
|
output_filename = f"{filename}{count}{extension}"
|
|
|
|
generate_html(parsed_data, output_filename, input_filename)
|
|
generate_html(parsed_data, "latest.html", input_filename)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|