1 #!/usr/bin/env python 2 3 """ 4 Convert and optimise images for display in an Acorn Electron MODE 1 variant 5 with four colours per line but eight colours available for selection on each 6 line. 7 8 Copyright (C) 2015 Paul Boddie <paul@boddie.org.uk> 9 10 This program is free software; you can redistribute it and/or modify it under 11 the terms of the GNU General Public License as published by the Free Software 12 Foundation; either version 3 of the License, or (at your option) any later 13 version. 14 15 This program is distributed in the hope that it will be useful, but WITHOUT ANY 16 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 17 PARTICULAR PURPOSE. See the GNU General Public License for more details. 18 19 You should have received a copy of the GNU General Public License along 20 with this program. If not, see <http://www.gnu.org/licenses/>. 21 """ 22 23 from random import random, randrange 24 25 corners = [ 26 (0, 0, 0), (255, 0, 0), (0, 255, 0), (255, 255, 0), 27 (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255) 28 ] 29 30 # Basic colour operations. 31 32 def within(v, lower, upper): 33 return min(max(v, lower), upper) 34 35 def clip(v): 36 return int(within(v, 0, 255)) 37 38 def restore(srgb): 39 r, g, b = srgb 40 return int(r * 255.0), int(g * 255.0), int(b * 255.0) 41 42 def scale(rgb): 43 r, g, b = rgb 44 return r / 255.0, g / 255.0, b / 255.0 45 46 def invert(srgb): 47 r, g, b = srgb 48 return 1.0 - r, 1.0 - g, 1.0 - b 49 50 scaled_corners = map(scale, corners) 51 zipped_corners = zip(corners, scaled_corners) 52 53 # Colour distribution functions. 54 55 def combination(rgb): 56 57 "Return the colour distribution for 'rgb'." 58 59 # Get the colour with components scaled from 0 to 1, plus the inverted 60 # component values. 61 62 srgb = scale(rgb) 63 rgbi = invert(srgb) 64 pairs = zip(rgbi, srgb) 65 66 # For each corner of the colour cube (primary and secondary colours plus 67 # black and white), calculate the corner value's contribution to the 68 # input colour. 69 70 d = [] 71 for corner, scaled in zipped_corners: 72 rs, gs, bs = scaled 73 74 # Obtain inverted channel values where corner channels are low; 75 # obtain original channel values where corner channels are high. 76 77 d.append((pairs[0][int(rs)] * pairs[1][int(gs)] * pairs[2][int(bs)], corner)) 78 79 # Balance the corner contributions. 80 81 return balance(d) 82 83 def complements(rgb): 84 85 "Return 'rgb' and its complement." 86 87 r, g, b = rgb 88 return rgb, restore(invert(scale(rgb))) 89 90 bases = [(0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255)] 91 base_complements = map(complements, bases) 92 93 def balance(d): 94 95 """ 96 Balance distribution 'd', cancelling opposing values and their complements 97 and replacing their common contributions with black and white contributions. 98 """ 99 100 dd = dict([(value, f) for f, value in d]) 101 for primary, secondary in base_complements: 102 common = min(dd[primary], dd[secondary]) 103 dd[primary] -= common 104 dd[secondary] -= common 105 return [(f, value) for value, f in dd.items()] 106 107 def combine(d): 108 109 "Combine distribution 'd' to get a colour value." 110 111 out = [0, 0, 0] 112 for v, rgb in d: 113 out[0] += v * rgb[0] 114 out[1] += v * rgb[1] 115 out[2] += v * rgb[2] 116 return tuple(map(int, out)) 117 118 def pattern(rgb, chosen=None): 119 120 """ 121 Obtain a sorted colour distribution for 'rgb', optionally limited to any 122 specified 'chosen' colours. 123 """ 124 125 l = [(f, value) for f, value in combination(rgb) if not chosen or value in chosen] 126 l.sort(reverse=True) 127 return l 128 129 def get_value(rgb, chosen=None, fail=False): 130 131 """ 132 Get an output colour for 'rgb', optionally limited to any specified 'chosen' 133 colours. If 'fail' is set to a true value, return None if the colour cannot 134 be expressed using any of the chosen colours. 135 """ 136 137 l = pattern(rgb, chosen) 138 limit = sum([f for f, c in l]) 139 if not limit: 140 if fail: 141 return None 142 else: 143 return l[randrange(0, len(l))][1] 144 145 choose = random() * limit 146 threshold = 0 147 for f, c in l: 148 threshold += f 149 if choose < threshold: 150 return c 151 return c 152 153 # Colour processing operations. 154 155 def sign(x): 156 if x >= 0: 157 return 1 158 else: 159 return -1 160 161 def saturate_rgb(rgb, exp): 162 r, g, b = rgb 163 return saturate_value(r, exp), saturate_value(g, exp), saturate_value(b, exp) 164 165 def saturate_value(x, exp): 166 return int(127.5 + sign(x - 127.5) * 127.5 * pow(abs(x - 127.5) / 127.5, exp)) 167 168 def amplify_rgb(rgb, exp): 169 r, g, b = rgb 170 return amplify_value(r, exp), amplify_value(g, exp), amplify_value(b, exp) 171 172 def amplify_value(x, exp): 173 return int(pow(x / 255.0, exp) * 255.0) 174 175 # Exercise functions for Shedskin. 176 177 if __name__ == "__main__": 178 rgb = (200, 100, 50) 179 saturate_rgb(rgb, 1.0) 180 amplify_rgb(rgb, 1.0) 181 get_value(rgb) 182 get_value(rgb, [(255, 255, 255), (255, 0, 0), (255, 255, 0), (0, 0, 0)]) 183 combine([(1.0, (255, 0, 0)), (0.0, (0, 0, 0))]) 184 clip(200.0) 185 186 # vim: tabstop=4 expandtab shiftwidth=4