freedoom/lumps/colormap/colormap.py
Mike Swanson 8835afef5f Copyright date bump and apply SPDX tags.
The tags are shorthand for the license of each file and avoid
copying the full license text into each one (and avoids having
to manually update the dates in each one...).
2017-02-15 16:41:53 -08:00

229 lines
6.4 KiB
Python
Executable file

#!/usr/bin/env python
# Copyright (C) 2001 Colin Phipps <cphipps@doomworld.com>
# Copyright (C) 2008, 2013 Simon Howard
# Parts copyright (C) 1999 by id Software (http://www.idsoftware.com/)
#
# SPDX-License-Identifier: GPL-2.0+
#
# Takes PLAYPAL as input (filename is the only parameter)
# Produces a light graduated COLORMAP on stdout
# O(n^2)
#
# This was originally a Perl script by Colin Phipps; it was converted
# to Python and now is a more generic tool for generating all kinds of
# COLORMAP effects.
#
import os
import sys
import struct
# Parameters affecting colormap generation:
# "Darkness" is this color, which is usually black, but can be
# overridden (RGB 0-255):
dark_color = (0, 0, 0)
# Color to tint the colormap (RGB 0.0-1.0):
tint_color = (255, 255, 255)
# Fractional balance between tint and normal color. 0 is no tint applied,
# 1.0 is full tint.
tint_frac = 0
# Fudge factor to adjust brightness when calculating 'tinted' version
# of colors. Larger values are brighter but may cause color clipping.
# A value of 0.33 is a straight-average of the RGB channels. Maximum
# sensible value is 1.0, though it can be overdriven for fancy
# brightness effects.
tint_bright = 0.5
def read_palette(filename):
"""Read palette from file and return a list of tuples containing
RGB values."""
f = open(filename, "rb")
colors = []
for i in range(256):
data = f.read(3)
color = struct.unpack("BBB", data)
colors.append(color)
return colors
# Return closest palette entry to the given RGB triple
def search_palette(palette, target):
"""Search the given palette and find the nearest matching
color to the given color, returning an index into the
palette of the color that best matches."""
best_diff = None
best_index = None
def square(x):
return x * x
for i in range(len(palette)):
color = palette[i]
diff = square(target[0] - color[0]) \
+ square(target[1] - color[1]) \
+ square(target[2] - color[2])
if best_index is None or diff < best_diff:
best_diff = diff
best_index = i
return best_index
def generate_colormap(colors, palette):
"""Given a list of colors, translate these into indexes into
the given palette, finding the nearest color where an exact
match cannot be found."""
result = []
for color in colors:
index = search_palette(palette, color)
result.append(index)
return result
def tint_colors(colors, tint, bright=0.5):
"""Given a list of colors, tint them a particular color."""
result = []
for c in colors:
# I've experimented with different methods of calculating
# intensity, but this seems to work the best. This is basically
# doing an average of the full channels, but a straight
# average causes the picture to get darker - eg. (0,0,255)
# maps to (87,87,87). So we have a controllable brightness
# factor that allows the brightness to be adjusted.
intensity = min((c[0] + c[1] + c[2]) * bright, 255) / 255.0
result.append((
tint[0] * intensity,
tint[1] * intensity,
tint[2] * intensity,
))
return result
def blend_colors(colors1, colors2, factor=0.5):
"""Blend the two given lists of colors, with 'factor' controlling
the mix between the two. factor=0 is exactly colors1, while
factor=1 is exactly colors2. Returns a list of blended colors."""
result = []
for index, c1 in enumerate(colors1):
c2 = colors2[index]
result.append((
c2[0] * factor + c1[0] * (1 - factor),
c2[1] * factor + c1[1] * (1 - factor),
c2[2] * factor + c1[2] * (1 - factor),
))
return result
def invert_colors(colors):
"""Given a list of colors, translate them to inverted monochrome."""
result = []
for color in colors:
average = (color[0] + color[1] + color[2]) // 3
inverse = 255 - average
result.append((inverse, inverse, inverse))
return result
def solid_color_list(color):
"""Generate a 256-entry palette where all entries are the
same color."""
return [color] * 256
def output_colormap(colormap):
"""Output the given palette to stdout."""
for c in colormap:
x = struct.pack("B", c)
os.write(sys.stdout.fileno(), x)
def print_palette(colors):
for y in range(16):
for x in range(16):
color = colors[y * 16 + x]
print("#%02x%02x%02x" % color)
print()
def parse_color_code(s):
"""Parse a color code in HTML color code format, into an RGB
3-tuple value."""
if not s.startswith('#') or len(s) != 7:
raise Exception('Not in HTML color code form: %s' % s)
return (int(s[1:3], 16), int(s[3:5], 16), int(s[5:7], 16))
def set_parameter(name, value):
"""Set configuration value, from command line parameters."""
global dark_color, tint_color, tint_frac, tint_bright
if name == 'dark_color':
dark_color = parse_color_code(value)
elif name == 'tint_color':
tint_color = parse_color_code(value)
elif name == 'tint_pct':
tint_frac = int(value) / 100.0
elif name == 'tint_bright':
tint_bright = float(value)
else:
raise Exception("Unknown parameter: '%s'" % name)
# Parse command line.
playpal_filename = None
for arg in sys.argv[1:]:
if arg.startswith('--') and '=' in arg:
key, val = arg[2:].split('=', 2)
set_parameter(key, val)
else:
playpal_filename = arg
if playpal_filename is None:
print("Usage: %s playpal.lmp > output-file.lmp" % sys.argv[0])
sys.exit(1)
palette = read_palette(playpal_filename)
colors = palette
# Apply tint, if enabled.
# The tint is intentionally applied *before* the darkening effect is
# applied. This allows us to darken to a different color than the tint
# color, if so desired.
if tint_frac > 0:
colors = blend_colors(palette,
tint_colors(colors, tint_color, tint_bright),
tint_frac)
# Generate colormaps for different darkness levels, by blending between
# the default colors and a palette where every entry is the "dark" color.
dark = solid_color_list(dark_color)
for i in range(32):
darken_factor = (32 - i) / 32.0
darkened_colors = blend_colors(dark, colors, darken_factor)
output_colormap(generate_colormap(darkened_colors, palette))
# Inverse color map for invulnerability effect.
inverse_colors = invert_colors(palette)
output_colormap(generate_colormap(inverse_colors, palette))
# Last colormap is all black, and is actually unused in Vanilla Doom
# (it was mistakenly included by the dcolors.c utility). It's
# strictly unneeded, though some utilities (SLADE) do not detect a
# lump as a COLORMAP unless it is the right length.
output_colormap(generate_colormap(dark, palette))