diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/__main__.py | 20 | ||||
-rw-r--r-- | src/blackbody.py | 4 | ||||
-rw-r--r-- | src/icc.py | 175 | ||||
-rw-r--r-- | src/monitor.py | 28 |
4 files changed, 165 insertions, 62 deletions
diff --git a/src/__main__.py b/src/__main__.py index 287a142..40777dc 100755 --- a/src/__main__.py +++ b/src/__main__.py @@ -26,7 +26,15 @@ from argparser import * PROGRAM_NAME = 'blueshift' +''' +:str The name of the program +''' + PROGRAM_VERSION = '1.15' +''' +:str The version of the program +''' + ## Set process title @@ -38,13 +46,16 @@ def setproctitle(title): ''' import ctypes try: + # Create strng buffer with title title = title.encode(sys.getdefaultencoding(), 'replace') title = ctypes.create_string_buffer(title) if 'linux' in sys.platform: - libc = ctypes.cdll.LoadLibrary("libc.so.6") + # Set process title on Linux + libc = ctypes.cdll.LoadLibrary('libc.so.6') libc.prctl(15, ctypes.byref(title), 0, 0, 0) elif 'bsd' in sys.platform: - libc = ctypes.cdll.LoadLibrary("libc.so.7") + # Set process title on at least FreeBSD + libc = ctypes.cdll.LoadLibrary('libc.so.7') libc.setproctitle(ctypes.create_string_buffer(b'-%s'), title) except: pass @@ -53,10 +64,10 @@ setproctitle(sys.argv[0]) ## Set global variables -global DATADIR, i_size, o_size, r_curve, g_curve, b_curve, clip_result, reset, panicgate +global i_size, o_size, r_curve, g_curve, b_curve, clip_result, reset, panicgate global periodically, wait_period, fadein_time, fadeout_time, fadein_steps, fadeout_steps global monitor_controller, running, continuous_run, panic, _globals_, conf_storage, parser -global signal_SIGTERM, signal_SIGUSR1, signal_SIGUSR2 +global signal_SIGTERM, signal_SIGUSR1, signal_SIGUSR2, DATADIR, LIBDIR, LIBEXECDIR from aux import * @@ -70,6 +81,7 @@ from backlight import * from blackbody import * + config_file = None ''' :str The configuration file to load diff --git a/src/blackbody.py b/src/blackbody.py index 02972da..83b2b9d 100644 --- a/src/blackbody.py +++ b/src/blackbody.py @@ -25,8 +25,10 @@ from colour import * -# /usr/share/blueshift DATADIR = 'res' +''' +:str The path to program resources, '/usr/share/blueshift' is standard +''' @@ -15,12 +15,19 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +# This module implements support for ICC profiles + +import os from subprocess import Popen, PIPE from curve import * -# /usr/libexec + + LIBEXECDIR = 'bin' +''' +:str Path to executable libraries, '/usr/libexec' is standard +''' @@ -31,10 +38,8 @@ def load_icc(pathname): @param pathname:str The ICC profile file @return :()→void Function to invoke, parameterless, to apply the ICC profile to the colour curves ''' - content = None with open(pathname, 'rb') as file: - content = file.read() - return parse_icc(content) + return parse_icc(file.read()) def get_current_icc(): @@ -52,18 +57,22 @@ def get_current_icc_raw(): @return list<(screen:int, monitor:int, profile:bytes())> List of used profiles ''' - process = Popen([LIBEXECDIR + "/blueshift_iccprofile"], stdout = PIPE) + # Spawn the libexec blueshift_iccprofile + process = Popen([LIBEXECDIR + os.sep + 'blueshift_iccprofile'], stdout = PIPE) + # Wait for the child process to exit and gather its output to stdout lines = process.communicate()[0].decode('utf-8', 'error').split('\n') + # Ensure the tha process has exited while process.returncode is None: process.wait() + # Throw exception if the child process failed if process.returncode != 0: raise Exception('blueshift_iccprofile exited with value %i' % process.returncode) rc = [] - for line in lines: - if len(line) == 0: - continue - (s, m, p) = line.split(': ') + # Get the screen, output and profile for each monitor with an _ICC_PROFILE(_n) atom set + for s, m, p in [line.split(': ') for line in lines if not line == '']: + # Convert the program from hexadecminal encoding to raw octet encoding p = bytes([int(p[i : i + 2], 16) for i in range(0, len(p), 2)]) + # List the profile rc.append((int(s), int(m), p)) return rc @@ -75,76 +84,122 @@ def parse_icc(content): @param content:bytes The ICC profile data @return :()→void Function to invoke, parameterless, to apply the ICC profile to the colour curves ''' + # Magic number for dual-byte precision lookup table based profiles MLUT_TAG = 0x6d4c5554 + # Magic number for gamma–brightness–contrast based profiles + # and for variable precision lookup table profiles VCGT_TAG = 0x76636774 def fcurve(R_curve, G_curve, B_curve): + ''' + Apply an ICC profile mapping + + @param R_curve:list<float> Lookup table for the red channel + @param G_curve:list<float> Lookup table for the green channel + @param B_curve:list<float> Lookup table for the blue channel + ''' for curve, icc in curves(R_curve, G_curve, B_curve): for i in range(i_size): + # Nearest neighbour y = int(curve[i] * (len(icc) - 1) + 0.5) + # Trunction to actual neighbour y = min(max(0, y), len(icc) - 1) + # Apply mapping curve[i] = icc[y] - int_ = lambda bs : sum([bs[len(bs) - 1 - i] << (8 * i) for i in range(len(bs))]) + # Integers in ICC profiles are encoded with the most significant byte first + int_ = lambda bs : sum([v << (i * 8) for i, v in enumerate(reversed(bs))]) + def read(n): + ''' + Read a set of bytes for the encoded ICC profile + + @param n:int The number of bytes to read + @return :list<int> The next `n` bytes of the profile + ''' if len(content) < n: - raise Except("Premature end of file: %s" % pathname) + raise Exception('Premature end of file: %s' % pathname) rc, content[:] = content[:n], content[n:] return rc + # Convert profile encoding format for bytes to integer list content = list(content) + # Skip the first 128 bytes read(128) - - R_curve, G_curve, B_curve = [], [], [] + # Get the number of bytes n_tags, ptr = int_(read(4)), 128 + 4 + # Create array for the lookup tables to creat + R_curve, G_curve, B_curve = [], [], [] + for i_tag in range(n_tags): - tag_name = int_(read(4)) - tag_offset = int_(read(4)) - tag_size = int_(read(4)) - ptr += 3 * 4 + # Get profile encoding type, offset to the profile and the encoding size of its data + (tag_name, tag_offset, tag_size), ptr = [int_(read(4)) for _ in range(3)], ptr + 3 * 4 + # XXX should I not jump to the data now instead of inside the if statements' bodies? if tag_name == MLUT_TAG: + ## The profile is encododed as an dual-byte precision lookup table + # Jump to the profile data read(tag_offset - ptr) - for i in range(256): R_curve.append(int_(read(2)) / 65535) - for i in range(256): G_curve.append(int_(read(2)) / 65535) - for i in range(256): B_curve.append(int_(read(2)) / 65535) + # Get the lookup table for the red channel, + for _ in range(256): R_curve.append(int_(read(2)) / 65535) + # for the green channel + for _ in range(256): G_curve.append(int_(read(2)) / 65535) + # and for the blue channel. + for _ in range(256): B_curve.append(int_(read(2)) / 65535) return lambda : fcurve(R_curve, G_curve, B_curve) elif tag_name == VCGT_TAG: + ## The profile is encoded as with gamma, brightness and contrast values + # or as a variable precision lookup table profile + # Jump to the profile data read(tag_offset - ptr) + # VCGT profiles starts where their magic number tag_name = int_(read(4)) if not tag_name == VCGT_TAG: break + # Skip four bytes read(4) + # and get the actual encoding type gamma_type = int_(read(4)) if gamma_type == 0: - n_channels = int_(read(2)) - n_entries = int_(read(2)) - entry_size = int_(read(2)) + ## The profile is encoded as a variable precision lookup table + (n_channels, n_entries, entry_size) = [int_(read(2)) for _ in range(3)] if tag_size == 1584: - n_channels, n_entries, entry_size = 3, 256, 2 - if not n_channels == 3: # assuming sRGB + (n_channels, n_entries, entry_size) = 3, 256, 2 + if not n_channels == 3: + # Assuming sRGB, can only be an correct assumption if there are exactly three channels break - int__ = lambda m : int_(read(m)) / ((256 ** m) - 1) - for i in range(n_entries): R_curve.append(int__(entry_size)) - for i in range(n_entries): G_curve.append(int__(entry_size)) - for i in range(n_entries): B_curve.append(int__(entry_size)) + # Calculate the divisor for mapping to [0, 1] + divisor = (256 ** entry_size) - 1 + # Values are encoded in integer form with `entry_size` bytes + int__ = lambda : int_(read(entry_size)) / divisor + # Get the lookup table for the red channel, + for _ in range(n_entries): R_curve.append(int__()) + # for the green channel + for _ in range(n_entries): G_curve.append(int__()) + # and for the blue channel. + for _ in range(n_entries): B_curve.append(int__()) return lambda : fcurve(R_curve, G_curve, B_curve) elif gamma_type == 1: - r_gamma = int_(read(4)) / 65535 - r_min = int_(read(4)) / 65535 - r_max = int_(read(4)) / 65535 - g_gamma = int_(read(4)) / 65535 - g_min = int_(read(4)) / 65535 - g_max = int_(read(4)) / 65535 - b_gamma = int_(read(4)) / 65535 - b_min = int_(read(4)) / 65535 - b_max = int_(read(4)) / 65535 + ## The profile is encoded with gamma, brightness and contrast values + # Get the gamma, brightness and contrast for the red channel, + (r_gamma, r_min, r_max) = [int_(read(4)) / 65535 for _ in range(3)] + # green channel + (g_gamma, g_min, g_max) = [int_(read(4)) / 65535 for _ in range(3)] + # and blue channel. + (b_gamma, b_min, b_max) = [int_(read(4)) / 65535 for _ in range(3)] def f(): + ''' + Apply the gamma, brightness and contrast + ''' + # Apply gamma gamma(r_gamma, g_gamma, b_gamma) + # before brightness and contrast rgb_limits(r_min, r_max, g_min, g_max, b_min, b_max) return f break + # XXX should I not jump to (tag_offset + tag_size - ptr) here + # and not break the loops when unkown? - raise Exception("Unsupported ICC profile file") + raise Exception('Unsupported ICC profile file') def make_icc_interpolation(profiles): @@ -164,35 +219,53 @@ def make_icc_interpolation(profiles): to which the profile should be applied. ''' def f(t, a): - pro0 = profiles[(int(t) + 0) % len(profiles)] - pro1 = profiles[(int(t) + 1) % len(profiles)] - t %= 1 + # Get floor and ceiling profiles and weight + (pro0, pro1), t = [profiles[(int(t) + 0) % len(profiles)] for i in range(2)], t % 1 if (pro0 is pro1) and (a == 1): + # If the floor and ceiling are the same profile, + # and the alpha is 1, than we can just simple apply + # one without any interpolation pro0() return + # But otherwise, we will need to save the current curves r_, g_, b_ = r_curve[:], g_curve[:], b_curve[:] + # reset the curves start_over() + # and apply on of the profiles pro0() + # so that we can get the mapping of one of the profiles. r0, g0, b0 = r_curve[:], g_curve[:], b_curve[:] + # After which we can get the last encoding value n = len(r0) - 1 - r, g, b = None, None, None + rgb = None if pro0 is pro1: - r = [v * a + i * (1 - a) / n for i, v in enumerate(r0)] - g = [v * a + i * (1 - a) / n for i, v in enumerate(g0)] - b = [v * a + i * (1 - a) / n for i, v in enumerate(b0)] + # Now, if the floor and ceiling profiles are than same profile, + # then we can use just one of then interpolat between it and a clean adjustment. + rgb = [[v * a + i * (1 - a) / n for i, v in enumerate(c0)] for c0 in (r0, g0, b0)] else: + # Otherwise we need to clear the curves from the floor profile's adjustments start_over() + # and apply the ceiling profile pro1() - r1, g1, b1 = r_curve[:], g_curve[:], b_curve[:] + # so that we can that profile's adjustments. + # Than we pair the floor and ceilings profile for each channel. + r01, g01, b01 = (r0, r_curve[:]), (g0, g_curve[:]), (b0, b_curve[:]) + # Now that we have two profiles, when we interpolate between them and a clean + # state, we first interpolate the profiles and the interpolate between that + # interpolation and a clean adjustment. interpol = lambda i, v0, v1 : (v0 * (1 - t) + v1 * t) * a + i * (1 - a) / n - r = [interpol(i, v0, v1) for i, (v0, v1) in enumerate(zip(r0, r1))] - g = [interpol(i, v0, v1) for i, (v0, v1) in enumerate(zip(g0, g1))] - b = [interpol(i, v0, v1) for i, (v0, v1) in enumerate(zip(b0, b1))] + rgb = [[interpol(i, v0, v1) for i, (v0, v1) in enumerate(zip(*c01))] for c01 in (r01, g01, b01)] + # Now that we have read the profiles, it is time to restore the curves to the + # state they were in, r_curve[:], g_curve[:], b_curve[:] = r_, g_, b_ - for curve, icc in curves(r, g, b): + # and apply the interpolated profile adjustments on top of it. + for curve, icc in curves(*rgb): for i in range(i_size): + # Nearest neighbour y = int(curve[i] * (len(icc) - 1) + 0.5) + # Trunction to actual neighbour y = min(max(0, y), len(icc) - 1) + # Apply mapping curve[i] = icc[y] return f diff --git a/src/monitor.py b/src/monitor.py index 470335c..f00b4bc 100644 --- a/src/monitor.py +++ b/src/monitor.py @@ -22,23 +22,39 @@ from aux import * from curve import * -# /usr/lib LIBDIR = 'bin' -sys.path.append(LIBDIR) +''' +:str Path to libraries, '/usr/lib' is standard +''' -# /usr/libexec LIBEXECDIR = 'bin' +''' +:str Path to executable libraries, '/usr/libexec' is standard +''' -randr_opened = None -vidmode_opened = None + +## Add the path to libraries to the list of paths to Python modules +sys.path.append(LIBDIR) +## Load DRM module try: from blueshift_drm import * except: - pass ## Not compiled with DRM support + # Not compiled with DRM support + pass +randr_opened = None +''' +:int? The index of the, with RandR, opened X screen, if any +''' + +vidmode_opened = None +''' +:int? The index of the, with vidmode, opened X screen, if any +''' + def close_c_bindings(): ''' |