First literate programming version

This commit is contained in:
Denis Lehmann 2020-09-12 13:27:57 +00:00
parent c652a156ef
commit e6a90fd6d5

298
huepaper.org Executable file
View File

@ -0,0 +1,298 @@
* huepaper
:PROPERTIES:
:header-args: :tangle huepaper_tangled.py :shebang "#!/usr/bin/env python"
:END:
This is the literate programming file for =huepaper.py=.
To export it, open it in *Emacs* and *tangle* it.
** Imports
huepaper is mainly based on two libraries.
[[https://github.com/vaab/colour][Colour]] and [[https://python-pillow.org/][Pillow]].
#+BEGIN_SRC python
import argparse
import os.path
import random
import sys
from colour import Color
from PIL import Image, ImageDraw, ImageOps
#+END_SRC
** Base color
Every huepaper has a base color.
It is used to calculate the final colors.
If a base color is given by the user, it can have every form, Colour supports.
If no color is given, a random one is chosen.
#+BEGIN_SRC python
def get_base_color(color_string = None):
# If no color string is given, create a random color
if not color_string:
hue = random.uniform(0, 1)
sat = random.uniform(sat_min, sat_max)
lum = random.uniform(lum_min, lum_max)
base_color = Color(hue = hue, saturation = sat, luminance = lum)
print('Selected random base color: {}'.format(base_color.hex))
# Else try to parse string
else:
try:
base_color = Color(color_string)
except:
print('Not a valid color expression: {}'.format(color_string))
sys.exit(1)
return base_color
#+END_SRC
** Colors
The final color limits for each edge of the huepaper are calculated with respect to the base color.
They rely also on the given input parameters such as saturation and luminance..
Those decide how much the colors differ from the base color.
#+BEGIN_SRC python
def create_colors(base_color):
colors = []
max_sat_diff = 0.1
max_lum_diff = 0.1
# Create four random colors similar to the given base_color
for i in range(0, 4):
tmp_hue = base_color.hue + random.uniform(-max_hue / 2.0, max_hue / 2.0)
if tmp_hue > 1.0:
tmp_hue -= 1
tmp_sat = base_color.saturation + random.uniform(-max_sat_diff, max_sat_diff)
tmp_sat = min(sat_max, max(sat_min, tmp_sat))
tmp_lum = base_color.luminance + random.uniform(-max_lum_diff, max_lum_diff)
tmp_lum = min(lum_max, max(lum_min, tmp_lum))
color = Color(hue = tmp_hue, saturation = tmp_sat, luminance = tmp_lum)
colors.append(color.rgb)
return tuple(colors)
#+END_SRC
** Rest
#+BEGIN_SRC python
# Create base image from four colors, width and height
# c1 - top left
# c2 - top right
# c3 - bottom right
# c4 - bottom left
def create_base_image(c1, c2, c3, c4):
# Lambda for adding four colors
add = lambda c1, c2, c3, c4 : (c1[0] + c2[0] + c3[0] + c4[0], c1[1] + c2[1] + c3[1] + c4[1], c1[2] + c2[2] + c3[2] + c4[2])
# Lambda for multiplying a color with a factor
mul = lambda c, x : (c[0] * x, c[1] * x, c[2] * x)
# Lambda for scaling a color from [0 , 1] to [0, 255]
cor = lambda c : (int(c[0] * 255), int(c[1] * 255), int(c[2] * 255))
# Lambda for calculating a color at x and y in range [0, 1]
# Color limits are set at creation
col = lambda x, y, c1 = c1, c2 = c2, c3 = c3, c4 = c4 : cor(add(mul(c1, (1.0 - x) * (1.0 - y)), mul(c2, x * (1.0 - y)), mul(c3, x * y), mul(c4, (1.0 - x) * y)))
# Create image
image = Image.new('RGBA', (width, height))
pixels = image.load()
for x in range(0, width):
for y in range(0, height):
pixels[x, y] = col(x / (width - 1), y / (height - 1))
return image
# Add lines to an image
def add_lines(image, color):
line_image = Image.new('RGBA', (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(line_image)
# Set color
color = tuple(map(lambda x : int(x * 255), color))
# Generate lines
number_of_lines = random.randint(1, 3)
scale = width / 100.0
base_width = random.randint(int(2 * scale), int(5 * scale))
rand_width = lambda base_width = base_width : base_width + random.randint(-base_width // 2, base_width // 2)
space = rand_width() // 2
offset = random.randint(0, space)
for i in range(0, number_of_lines):
line_width = rand_width()
x = offset + space + (line_width // 2)
draw.line((x, 0, x, height), fill = color, width = line_width)
offset += space + line_width
# Mirror line image eventually
orientation = random.randrange(2)
if orientation == 1:
line_image = ImageOps.mirror(line_image)
# Add line image to input image
image.alpha_composite(line_image, (0, 0))
return image
# Add pixelation to image
def add_pixelation(image, x, y):
image = image.resize((x, y))
image = image.resize((width, height), Image.BOX)
return image
# Add emblem to an image from a filepath
def add_emblem(image, filepath):
# Load image
try:
emblem_image = Image.open(filepath)
except Exception as e:
print('Failed to load emblem: {}'.format(e))
sys.exit(1)
# Exit if emblem is too big
if emblem_image.size[0] > width or emblem_image.size[1] > height:
print('Emblem can\'t be bigger than the wallpaper')
sys.exit(1)
# Insert emblem in the center
offset = ((image.size[0] - emblem_image.size[0]) // 2, (image.size[1] - emblem_image.size[1]) // 2)
image.alpha_composite(emblem_image, offset)
return image
# Save image to filepath
def save_image(filepath, image):
save = True
# Check whether file exists
if os.path.isfile(filepath):
overwrite = input('The file {} already exists. Do you want to overwrite it? [y/N] '.format(filepath))
if overwrite != 'y' and overwrite != 'Y':
save = False
if save:
stop = False
while not stop:
try:
image.save(filepath)
stop = True
except Exception as e:
print('Failed to save wallpaper: {}'.format(e))
again = input('Do you want to try again? [Y/n] ')
if again == 'n' or again == 'N':
stop = True
else:
filepath = input('Please enter new path where the wallpaper shall be saved: ')
'''
Main
'''
def main():
global width, height, max_hue, sat_min, sat_max, lum_min, lum_max
# Initialize parser
parser = argparse.ArgumentParser(description = 'Create wallpapers based on color hues.')
parser.add_argument('-W', '--width', default = 1920, type = int, help = 'width of wallpaper (defaul: 1920)')
parser.add_argument('-H', '--height', default = 1080, type = int, help = 'height of wallpaper (default: 1080)')
parser.add_argument('-c', '--color', help = 'color, the wallpaper is generated from (uses a random color if not given)')
parser.add_argument('-p', '--preview', action = 'store_true', help = 'preview wallpaper')
parser.add_argument('-o', '--output', help = 'file where to save the wallpaper to (default: None)')
parser.add_argument('-l', '--lines', nargs = '?', const = 0.1, type = float, help = 'include one to three random lines in base color with given opacity in range [0, 1] (default: 0.1)')
parser.add_argument('-lb', '--lines_bright', nargs = '?', const = 0.1, type = float, help = 'include one to three bright random lines with given opacity in range [0, 1] (default: 0.1)')
parser.add_argument('-ld', '--lines_dark', nargs = '?', const = 0.1, type = float, help = 'include one to three dark random lines with given opacity in range [0, 1] (default: 0.1)')
parser.add_argument('-P', '--pixelate', help = "pixelate image (e.g. 42x42)")
parser.add_argument('-e', '--emblem', help = 'emblem to add in the center of the wallpaper')
parser.add_argument('-hue', default = 0.1, type = float, help = 'maximum hue to differ from given color in range [0, 1] (default: 0.1)')
parser.add_argument('-smin', default = 0.2, type = float, help = 'minimum satisfaction for colors in range [0, 1] (default: 0.2)')
parser.add_argument('-smax', default = 1.0, type = float, help = 'maximum satisfaction for colors in range [0, 1] (default: 1.0)')
parser.add_argument('-lmin', default = 0.2, type = float, help = 'minimum luminance for colors in range [0, 1] (default: 0.2)')
parser.add_argument('-lmax', default = 0.9, type = float, help = 'maximum luminance for colors in range [0, 1] (default: 0.9)')
# Get args
args = parser.parse_args()
width = args.width
height = args.height
color = args.color
preview = args.preview
output = args.output
lines = args.lines
lines_bright = args.lines_bright
lines_dark = args.lines_dark
emblem = args.emblem
pixelate = args.pixelate
max_hue = args.hue
sat_min = args.smin
sat_max = args.smax
lum_min = args.lmin
lum_max = args.lmax
# Check preconditions
if not preview and not output:
parser.error('You must either set -p (--preview) or -o (--output)')
if pixelate:
try:
values = pixelate.split('x')
px = int(values[0])
py = int(values[1])
except:
parser.error('Pixelation value must be set in form: 42x42')
# Main routine
base_color = get_base_color(color)
c1, c2, c3, c4 = create_colors(base_color)
image = create_base_image(c1, c2, c3, c4)
if lines:
image = add_lines(image, base_color.rgb + (lines,))
if lines_bright:
image = add_lines(image, (1.0, 1.0, 1.0, lines_bright))
if lines_dark:
image = add_lines(image, (0.0, 0.0, 0.0, lines_dark))
if pixelate:
image = add_pixelation(image, px, py)
if emblem:
image = add_emblem(image, emblem)
if preview:
image.show()
if not output:
save = input('Do you want to save the image? [y/N] ')
if save == 'y' or save == 'Y':
path = input('Enter the path where the wallpaper shall be saved: ')
save_image(path, image)
if output:
save_image(output, image)
if __name__ == '__main__':
main()
#+END_SRC