summaryrefslogtreecommitdiffstats
path: root/src/icc.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/icc.py175
1 files changed, 124 insertions, 51 deletions
diff --git a/src/icc.py b/src/icc.py
index e08dfa7..507c347 100644
--- a/src/icc.py
+++ b/src/icc.py
@@ -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