#!/usr/bin/env python3 # Copyright © 2014 Mattias Andrée (maandree@member.fsf.org) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # 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/>. from subprocess import Popen, PIPE from curve import * # /usr/libexec LIBEXECDIR = 'bin' def load_icc(pathname): ''' Load ICC profile from a file @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) def get_current_icc(): ''' Get all currently applied ICC profiles as profile applying functions @return list<(screen:int, monitor:int, profile:()→void)> List of used profiles ''' return [(screen, monitor, parse_icc(profile)) for screen, monitor, profile in get_current_icc_raw()] def get_current_icc_raw(): ''' Get all currently applied ICC profiles as raw profile data @return list<(screen:int, monitor:int, profile:bytes())> List of used profiles ''' process = Popen([LIBEXECDIR + "/blueshift_iccprofile"], stdout = PIPE) lines = process.communicate()[0].decode('utf-8', 'error').split('\n') while process.returncode is None: process.wait() 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(': ') p = bytes([int(p[i : i + 2], 16) for i in range(0, len(p), 2)]) rc.append((int(s), int(m), p)) return rc def parse_icc(content): ''' Parse ICC profile from raw data @param content:bytes The ICC profile data @return :()→void Function to invoke, parameterless, to apply the ICC profile to the colour curves ''' MLUT_TAG = 0x6d4c5554 VCGT_TAG = 0x76636774 def fcurve(R_curve, G_curve, B_curve): for curve, icc in curves(R_curve, G_curve, B_curve): for i in range(i_size): y = int(curve[i] * (len(icc) - 1) + 0.5) y = min(max(0, y), len(icc) - 1) curve[i] = icc[y] int_ = lambda bs : sum([bs[len(bs) - 1 - i] << (8 * i) for i in range(len(bs))]) def read(n): if len(content) < n: raise Except("Premature end of file: %s" % pathname) rc, content[:] = content[:n], content[n:] return rc content = list(content) read(128) R_curve, G_curve, B_curve = [], [], [] n_tags, ptr = int_(read(4)), 128 + 4 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 if tag_name == MLUT_TAG: 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) return lambda : fcurve(R_curve, G_curve, B_curve) elif tag_name == VCGT_TAG: read(tag_offset - ptr) tag_name = int_(read(4)) if not tag_name == VCGT_TAG: break read(4) gamma_type = int_(read(4)) if gamma_type == 0: n_channels = int_(read(2)) n_entries = int_(read(2)) entry_size = int_(read(2)) if tag_size == 1584: n_channels, n_entries, entry_size = 3, 256, 2 if not n_channels == 3: # assuming sRGB 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)) 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 def f(): gamma(r_gamma, g_gamma, b_gamma) rgb_limits(r_min, r_max, g_min, g_max, b_min, b_max) return f break raise Exception("Unsupported ICC profile file") def make_icc_interpolation(profiles): ''' An interpolation function for ICC profiles @param profiles:list<()→void> Profile applying functions @return :(timepoint:float, alpha:float)→void() A function that applies an interpolation of the profiles, it takes to arguments: the timepoint and the filter alpha. The timepoint is normally a [0, 1] floating point of the dayness level, this means that you only have two ICC profiles, but you have multiple profiles, in such case profile #⌊timepoint⌋ and profile #(⌊timepoint⌋ + 1) (modulus the number of profiles) are interpolated with (timepoint - ⌊timepoint⌋) weight to the second profile. The filter alpha is a [0, 1] floating point of the degree 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 if (pro0 is pro1) and (a == 1): pro0() return r_, g_, b_ = r_curve[:], g_curve[:], b_curve[:] start_over() pro0() r0, g0, b0 = r_curve[:], g_curve[:], b_curve[:] n = len(r0) - 1 r, g, b = None, None, 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)] else: start_over() pro1() r1, g1, b1 = r_curve[:], g_curve[:], b_curve[:] 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))] r_curve[:], g_curve[:], b_curve[:] = r_, g_, b_ for curve, icc in curves(r, g, b): for i in range(i_size): y = int(curve[i] * (len(icc) - 1) + 0.5) y = min(max(0, y), len(icc) - 1) curve[i] = icc[y] return f