summaryrefslogtreecommitdiffstats
path: root/src/output.py
diff options
context:
space:
mode:
authorMattias Andrée <maandree@kth.se>2017-02-25 19:37:57 +0100
committerMattias Andrée <maandree@kth.se>2017-02-25 19:37:57 +0100
commitbb1758ef0beee1ddb900b62a4ad4814e0eb0aa60 (patch)
tree69b8d70a1dfbd3fb829a36abb6f3d659c3306449 /src/output.py
parentUpdate readme (diff)
downloadblueshift-bb1758ef0beee1ddb900b62a4ad4814e0eb0aa60.tar.gz
blueshift-bb1758ef0beee1ddb900b62a4ad4814e0eb0aa60.tar.bz2
blueshift-bb1758ef0beee1ddb900b62a4ad4814e0eb0aa60.tar.xz
Start on output.py
Signed-off-by: Mattias Andrée <maandree@kth.se>
Diffstat (limited to '')
-rw-r--r--src/output.py1016
1 files changed, 1016 insertions, 0 deletions
diff --git a/src/output.py b/src/output.py
new file mode 100644
index 0000000..d0de14a
--- /dev/null
+++ b/src/output.py
@@ -0,0 +1,1016 @@
+#!/usr/bin/env python3
+
+# Copyright © 2014, 2015, 2016, 2017 Mattias Andrée (maandree@kth.se)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# This module is responsible for access to the monitors.
+
+class Tristate:
+ '''
+ Ternary values
+
+ @constant NO:int
+ @constant MAYBE:int
+ @constant YES:int
+ '''
+ NO = 0
+ MAYBE = 1
+ YES = 2
+
+class Lifespan:
+ '''
+ The lifespan of a gamma adjustment, for cooperative gamma
+
+ @constant UNTIL_DEATH:int Remove adjustment when connection to server closes or
+ when explicitly removed
+ @constant UNTIL_REMOVAL:int Only remove adjustment once it is requested explicitly
+ @constant REMOVE:int Request that the adjustment be removed now
+ '''
+ UNTIL_DEATH = 0
+ '''
+ :int Remove adjustment when connection to server closes or when explicitly removed
+ '''
+
+ UNTIL_REMOVAL = 1
+ '''
+ :int Only remove adjustment once it is requested explicitly
+ '''
+
+ REMOVE = 2
+ '''
+ :int Request that the adjustment be removed now
+ '''
+
+class EDID:
+ '''
+ Parsed EDID data
+
+ @variable manufacturer_id:str? The manufacturer's ID
+ @variable manufacturer_product_code:int? Manufacturer specific product code
+ @variable serial_number:int? Serial number
+ @variable manufacture_week:int? Week of manifacture
+ @variable manufacture_year:int? Year of manifacture
+ @variable model_year:int? Year of model
+ @variable edid_version:(major:int, minor:int)? EDID version
+ @variable digital_input:bool? Whether the monitor takes digital input
+ @variable vesa_dfp_1x_tmds_crgb_compatible:bool? Whether the monitor is VESA DFP 1.x TMDS CRGB, 1 pixel
+ per clock, up to 8 bits per color, MSB aligned compatible
+ @variable relative_white_level:float? Voltage level for white relative to blank
+ @variable relative_sync_level:float? Voltage level for separate relative to blank
+ @variable blank_to_black:bool? Whether blank to black setup is expected
+ @variable separate_sync_supported:bool? Whether separate synchronisation is supported
+ @variable composite_sync_supported:bool? Whether composite synchronisation is supported
+ @variable sync_on_green_supported:bool? Whether synchronisation on green is supported
+ @variable vsync_pulse_serrated:bool? Whether vertical synchronisation pulse is serrated
+ @variable width_mm:int? The width of the monitor's viewport in millimetres
+ @variable height_mm:int? The heiht of the monitor's viewport in millimetres
+ @variable display_gamma:float? The monitor's gamma
+ @variable dpms_standby_supported:bool? Whether DPMS standby is supported
+ @variable dpms_suspend_supported:bool? Whether DPMS suspend is supported
+ @variable dpms_active_off_supported:bool? Whether DPMS active-off is supported
+ @variable digital_rgb_444_supported:bool? Whether digital RGB 4:4:4 input is supported
+ @variable digital_ycrcb_444_supported:bool? Whether digital YCrCb 4:4:4 input is supported
+ @variable digital_ycrcb_422_supported:bool? Whether digital YCrCb 4:2:2 input is supported
+ @variable analogue_grey_mono_display:bool? Whether the display is greyscale or monochrome and
+ takes analogue input
+ @variable analogue_rgb_display:bool? Whether the display is coloured and uses an RGB
+ colour space and takes analogue input
+ @variable analogue_non_rgb_display:bool? Whether the display is coloured and uses a non-RGB
+ colour model and takes analogue input
+ @variable srgb:bool? Whether the monitor uses sRGB
+ @variable preferred_timing_mode:bool? For EDID 1.2-: Whether the preferred timing mode is
+ specified in descriptor block 1.
+ For EDID 1.3+: Whether the preferred timing mode
+ (specified in descriptor block 1) includes native
+ pixel format and refresh rate.
+ @variable gtf_supported:bool? Whether Generalized Timing Formula is supported with
+ default parameter values
+ @variable red_chroma:(x:float, y:float)? The CIE xyY x and y values of the red primary colour
+ @variable green_chroma:(x:float, y:float)? The CIE xyY x and y values of the green primary colour
+ @variable blue_chroma:(x:float, y:float)? The CIE xyY x and y values of the blue primary colour
+ @variable white_chroma:(x:float, y:float)? The CIE xyY x and y values of the white point
+ '''
+ def __init__(self, edid):
+ '''
+ Constructor
+
+ @edid edid:str The EDID in upper case hexadecimal representation
+ '''
+ self.manufacturer_id = None
+ self.manufacturer_product_code = None
+ self.serial_number = None
+ self.manufacture_week = None
+ self.manufacture_year = None
+ self.model_year = None
+ self.edid_version = None
+ self.digital_input = None
+ self.vesa_dfp_1x_tmds_crgb_compatible = None
+ self.relative_white_level = None
+ self.relative_sync_level = None
+ self.blank_to_black = None
+ self.separate_sync_supported = None
+ self.composite_sync_supported = None
+ self.sync_on_green_supported = None
+ self.vsync_pulse_serrated = None
+ self.width_mm = None
+ self.height_mm = None
+ self.display_gamma = None
+ self.dpms_standby_supported = None
+ self.dpms_suspend_supported = None
+ self.dpms_active_off_supported = None
+ self.digital_rgb_444_supported = None
+ self.digital_ycrcb_444_supported = None
+ self.digital_ycrcb_422_supported = None
+ self.analogue_grey_mono_display = None
+ self.analogue_rgb_display = None
+ self.analogue_non_rgb_display = None
+ self.srgb = None
+ self.preferred_timing_mode = None
+ self.gtf_supported = None
+ self.red_chroma = None
+ self.green_chroma = None
+ self.blue_chroma = None
+ self.white_chroma = None
+
+ if edid[:len('00FFFFFFFFFFFF00')] == '00FFFFFFFFFFFF00' or len(edid) % 2 == 1:
+ return
+ edid = [int(edid[i * 2 : i * 2 + 2], 16) for i in range(len(edid) // 2)]
+ if sum(edid) % 256 != 0 or len(edid[:128]) < 128:
+ return
+
+ self.manufacturer_id = [(edid[8] >> 2) & 0x0F, (edid[9] >> 4) | (edid[8] & 1) << 4, edid[9] & 0x0F]
+ self.manufacturer_id = ''.join(chr(ord('@') + c) for c in self.manufacturer_id)
+ self.manufacturer_product_code = edid[10] | (edid[11] << 8)
+ self.serial_number = edid[12] | (edid[13] << 8) | (edid[14] << 16) | (edid[15] << 24)
+ self.manufacture_week = edid[16] # inconsistent between manufacturers
+ self.manufacture_year = 1990 + edid[17]
+ if self.manufacture_week == 255:
+ self.model_year = self.manufacture_year
+ self.manufacture_week = None
+ self.manufacture_year = None
+ self.edid_version = (edid[18], edid[19])
+ self.digital_input = (edid[20] & 0x80) == 0x80
+ if self.digital_input:
+ self.vesa_dfp_1x_tmds_crgb_compatible = (edid[20] & 1) == 1
+ else:
+ self.relative_white_level = (0.7, 0.714, 1, 0.7)[(edid >> 5) & 3]
+ self.relative_sync_level = (-0.3, -0.286, -0.4, 0)[(edid >> 5) & 3]
+ self.blank_to_black = (edid[20] & 16) == 16
+ self.separate_sync_supported = (edid[20] & 8) == 8
+ self.composite_sync_supported = (edid[20] & 4) == 4
+ self.sync_on_green_supported = (edid[20] & 2) == 2
+ self.vsync_pulse_serrated = (edid[20] & 1) == 1
+ self.width_mm = edid[21] * 10
+ self.height_mm = edid[22] * 10
+ if edid[21] == 0 or edid[22] == 0:
+ self.width_mm = self.height_mm = 0
+ self.display_gamma = edid[23] / 100 + 1 if edid[23] else None
+ self.dpms_standby_supported = (edid[24] & 128) == 128
+ self.dpms_suspend_supported = (edid[24] & 64) == 64
+ self.dpms_active_off_supported = (edid[24] & 32) == 32
+ if self.digital_input:
+ self.digital_rgb_444_supported = True
+ self.digital_ycrcb_444_supported = (edid[24] & 8) == 8
+ self.digital_ycrcb_422_supported = (edid[24] & 16) == 16
+ self.analogue_grey_mono_display = False
+ self.analogue_rgb_display = False
+ self.analogue_non_rgb_display = False
+ else:
+ self.digital_rgb_444_supported = False
+ self.digital_ycrcb_444_supported = False
+ self.digital_ycrcb_422_supported = False
+ self.analogue_grey_mono_display = ((edid[24] >> 3) & 3) == 0
+ self.analogue_rgb_display = ((edid[24] >> 3) & 3) == 1
+ self.analogue_non_rgb_display = ((edid[24] >> 3) & 3) == 2
+ self.srgb = (edid[24] & 4) == 4
+ self.preferred_timing_mode = (edid[24] & 2) == 2
+ self.gtf_supported = (edid[24] & 1) == 1
+ rx = (edid[27] << 2) | ((edid[25] >> 6) & 3)
+ ry = (edid[28] << 2) | ((edid[25] >> 4) & 3)
+ gx = (edid[29] << 2) | ((edid[25] >> 2) & 3)
+ gy = (edid[30] << 2) | ((edid[25] >> 0) & 3)
+ bx = (edid[31] << 2) | ((edid[26] >> 6) & 3)
+ by = (edid[32] << 2) | ((edid[26] >> 4) & 3)
+ wx = (edid[33] << 2) | ((edid[26] >> 2) & 3)
+ wy = (edid[34] << 2) | ((edid[26] >> 0) & 3)
+ self.red_chroma = (rx / 1024, ry / 1024)
+ self.green_chroma = (gx / 1024, gy / 1024)
+ self.blue_chroma = (bx / 1024, by / 1024)
+ self.white_chroma = (wx / 1024, wy / 1024)
+ # There are also mode lines and maybe extensions, but yeah...
+
+class MultiCRTC:
+ '''
+ A group of CRTC:s organised for efficient gamma ramp adjustments
+ '''
+ def __init__(self, crtcs, interpolation = None):
+ '''
+ Constructor
+
+ @param crtc:iter<CTRC> The CRTC:s
+ @param interpolation:(r:list<float>, g:list<float>, b:list<float>)→
+ (r:list<float>, g:list<float>, b:list<float>)?
+ Function used to interpolate gamma ramps to new dimentions,
+ `None` for the default interpolator, which is intentionally
+ unspecified
+ '''
+ self.interpolation = interpolation
+ self.layers = []
+ for crtc in crtcs:
+ self.add(crtc)
+
+ def add(self, crtc):
+ '''
+ Add a CRTC
+
+ @param crtc:CRTC The CRTC to add
+ '''
+ found = None
+ for layer in self.layers:
+ ref = layer[0][0][0]
+ if crtc.red_gamma_size == ref.red_gamma_size:
+ if crtc.green_gamma_size == ref.green_gamma_size:
+ if crtc.blue_gamma_size == ref.blue_gamma_size:
+ found = layer
+ break
+ if not found:
+ found = []
+ self.layers.append(found)
+
+ subfound = None
+ for sublayer in found:
+ ref = sublayer[0][0]
+ if crtc.gamma_depth == ref.gamma_depth:
+ subfound = sublayer
+ break
+ if not subfound:
+ subfound = []
+ found.append(subfound)
+
+ subsubfound = None
+ for subsublayer in subfound:
+ ref = subsublayer[0]
+ if crtc.backend == ref.backend:
+ subsubfound = subsublayer
+ break
+ if not subsubfound:
+ subsubfound = []
+ subfound.append(subsubfound)
+
+ subsubfound.append(crtc)
+
+ def make_ramps(self, depth = -2):
+ '''
+ Create a gamma-ramp trio where each ramp is as large as the
+ largest ramp, of the samp colour, of the CRTCs in the group
+
+ @param depth:int The gamma depth, 8 for unsigned 8-bit integers,
+ 16 for unsigned 16-bit integers, 32 for unsigned
+ 32-bit integers, 64 for unsigned 64-bit integers,
+ -1 for single-precision floating-point values, and
+ -2 for double-precision floating-point values
+ @return :Ramps A new gamma-ramp trio suited for the group
+ '''
+ size = [1, 1, 1]
+ for layer in self.layers:
+ crtc = layer[0][0][0]
+ size[0] = max(size[0], crtc.red_gamma_size)
+ size[1] = max(size[1], crtc.green_gamma_size)
+ size[2] = max(size[2], crtc.blue_gamma_size)
+ return Ramps(None, depth = depth, size = size)
+
+ def set_gamma(self, ramps, priority = None, rule = None, lifespan = 1):
+ '''
+ Set the gamma ramps on all CRTC:s in the group
+
+ @param ramps:Ramps The gamma ramps
+ @param priority:int? The priority of the adjustment, `None` for the default.
+ Must be `None` (default) if cooperative gamma is not supported.
+ @param rule:str? The rule of the adjustment, `None` for the default.
+ The rule is the last part of the adjustment's identifier,
+ if this is unique within the program, it should be universally
+ unique unless another program is intentionally make it not so.
+ Must be `None` (default) if cooperative gamma is not supported.
+ @param lifespan:int The lifespan of the algorithm: `Lifespan.UNTIL_DEATH`,
+ `Lifespan.UNTIL_REMOVAL` (default), or `Lifespan.REMOVE`
+ '''
+ if lifespan == Lifespan.REMOVE:
+ for layer in self.layers:
+ for sublayer in layer:
+ for subsublayer in sublayer:
+ for crtc in subsublayer:
+ crtc.set_gamma(None, priority, rule, lifespan)
+ return
+
+ for layer in self.layers:
+ ref = layer[0][0][0]
+ refsize = (ref.red_gamma_size, ref.green_gamma_size, ref.blue_gamma_size)
+ if refsize == (len(ramps.red), len(ramps.green), len(ramps.blue)):
+ ramps_size = ramps
+ else
+ ramps_size = Ramps.copy(ramps, refcrtc.depth, refsize)
+ for sublayer in layer:
+ ref = sublayer[0][0]
+ refdepth = ref.gamma_depth
+ if refdepth == ramps_size.depth:
+ ramps_depth = ramps_size
+ else:
+ ramps_depth = Ramps.copy(ramps, refdepth, refsize)
+ for subsublayer in sublayer:
+ ramps_backend = ramps_depth
+ for crtc in subsublayer:
+ ramps_backend = crtc.set_gamma(ramps_backend, priority, rule, lifespan)
+
+class CRTC:
+ '''
+ A CRTC
+
+ @function restore:(self)?→void Restore the CLUT:s to the (configured) system
+ defaults, `None` if not supported
+
+ @variable edid:str? The EDID in upper case hexadecimal representation
+ @variable red_gamma_size:int? The number of stops in the red gamma ramp
+ @variable green_gamma_size:int? The number of stops in the green gamma ramp
+ @variable blue_gamma_size:int? The number of stops in the blue gamma ramp
+ @variable gamma_depth:int? The gamma depth, 8 for unsigned 8-bit integers,
+ 16 for unsigned 16-bit integers, 32 for unsigned
+ 32-bit integers, 64 for unsigned 64-bit integers,
+ -1 for single-precision floating-point values, and
+ -2 for double-precision floating-point values
+ @variable gamma_support:int? 0 (`Tristate.NO`) if gamma adjustments are not supported,
+ 1 (`Tristate.MAYBE`) if gamma adjustments support is unknown,
+ and 2 (`Tristate.YES`) if gamma adjustments are supported,
+ @variable subpixel_order:str? The subpixel order:
+ "RGB" for red at left, green in centre, and blue at right;
+ "BGR" for red at right, green in centre, and blue at left;
+ "vRGB" for red at top, green in middle, and blue at bottom;
+ "vBGR" for red at bottom, green in middle, and blue at top;
+ and "None" for no subpixel order (e.g. on most old to
+ semi-old CRT:s)
+ @variable active:bool? Whether the monitor is active
+ @variable connector_name:str? The connector name
+ @variable connector_type:str? The connector type
+ @variable ramps Gamma ramps, you should not use it directly (INTERNAL)
+ @variable cooperative:bool Whether cooperative gamma is supported
+ @variable default_rule:str The default cooperative gamma rule (part of the class (filter identifier))
+ @variable default_priority:int The default cooperative gamma priority (filter order)
+
+ You will also find the following variables in `.edid_data`, however, here they are as
+ specified by the display server rather than as specified in the EDID. This means that
+ they can be been corrected by the user or the display server. On the other hand, they
+ can also be incorrect. For exampel, under X.org `.width_mm` and `.height_mm` are
+ calculated from assumed properties and can be completely, and horribly, wrong.
+
+ @variable manufacturer_id:str? The manufacturer's ID
+ @variable manufacturer_product_code:int? Manufacturer specific product code
+ @variable serial_number:int? Serial number
+ @variable manufacture_week:int? Week of manifacture
+ @variable manufacture_year:int? Year of manifacture
+ @variable model_year:int? Year of model
+ @variable edid_version:(major:int, minor:int)? EDID version
+ @variable digital_input:bool? Whether the monitor takes digital input
+ @variable vesa_dfp_1x_tmds_crgb_compatible:bool? Whether the monitor is VESA DFP 1.x TMDS CRGB, 1 pixel
+ per clock, up to 8 bits per color, MSB aligned compatible
+ @variable relative_white_level:float? Voltage level for white relative to blank
+ @variable relative_sync_level:float? Voltage level for separate relative to blank
+ @variable blank_to_black:bool? Whether blank to black setup is expected
+ @variable separate_sync_supported:bool? Whether separate synchronisation is supported
+ @variable composite_sync_supported:bool? Whether composite synchronisation is supported
+ @variable sync_on_green_supported:bool? Whether synchronisation on green is supported
+ @variable vsync_pulse_serrated:bool? Whether vertical synchronisation pulse is serrated
+ @variable width_mm:int? The width of the monitor's viewport in millimetres
+ @variable height_mm:int? The heiht of the monitor's viewport in millimetres
+ @variable display_gamma:float? The monitor's gamma
+ @variable dpms_standby_supported:bool? Whether DPMS standby is supported
+ @variable dpms_suspend_supported:bool? Whether DPMS suspend is supported
+ @variable dpms_active_off_supported:bool? Whether DPMS active-off is supported
+ @variable digital_rgb_444_supported:bool? Whether digital RGB 4:4:4 input is supported
+ @variable digital_ycrcb_444_supported:bool? Whether digital YCrCb 4:4:4 input is supported
+ @variable digital_ycrcb_422_supported:bool? Whether digital YCrCb 4:2:2 input is supported
+ @variable analogue_grey_mono_display:bool? Whether the display is greyscale or monochrome and
+ takes analogue input
+ @variable analogue_rgb_display:bool? Whether the display is coloured and uses an RGB
+ colour space and takes analogue input
+ @variable analogue_non_rgb_display:bool? Whether the display is coloured and uses a non-RGB
+ colour model and takes analogue input
+ @variable srgb:bool? Whether the monitor uses sRGB
+ @variable preferred_timing_mode:bool? For EDID 1.2-: Whether the preferred timing mode is
+ specified in descriptor block 1.
+ For EDID 1.3+: Whether the preferred timing mode
+ (specified in descriptor block 1) includes native
+ pixel format and refresh rate.
+ @variable gtf_supported:bool? Whether Generalized Timing Formula is supported with
+ default parameter values
+ @variable red_chroma:(x:float, y:float)? The CIE xyY x and y values of the red primary colour
+ @variable green_chroma:(x:float, y:float)? The CIE xyY x and y values of the green primary colour
+ @variable blue_chroma:(x:float, y:float)? The CIE xyY x and y values of the blue primary colour
+ @variable white_chroma:(x:float, y:float)? The CIE xyY x and y values of the white point
+ '''
+ def __init__(self):
+ '''
+ Constructor
+ '''
+ self.__edid_data = ...
+ self.edid = None
+ self.red_gamma_size = None
+ self.green_gamma_size = None
+ self.blue_gamma_size = None
+ self.gamma_depth = None
+ self.gamma_support = None
+ self.subpixel_order = None
+ self.active = None
+ self.connector_name = None
+ self.connector_type = None
+ self.ramps = None
+ self.cooperative = False
+ self.default_rule = 'standard'
+ self.default_priority = 1 << 59
+
+ # Everything that is in the EDID class, in
+ # case it is specified by the display server
+ # and potentially configured by the user.
+ self.manufacturer_id = None
+ self.manufacturer_product_code = None
+ self.serial_number = None
+ self.manufacture_week = None
+ self.manufacture_year = None
+ self.model_year = None
+ self.edid_version = None
+ self.digital_input = None
+ self.vesa_dfp_1x_tmds_crgb_compatible = None
+ self.relative_white_level = None
+ self.relative_sync_level = None
+ self.blank_to_black = None
+ self.separate_sync_supported = None
+ self.composite_sync_supported = None
+ self.sync_on_green_supported = None
+ self.vsync_pulse_serrated = None
+ self.width_mm = None
+ self.height_mm = None
+ self.display_gamma = None
+ self.dpms_standby_supported = None
+ self.dpms_suspend_supported = None
+ self.dpms_active_off_supported = None
+ self.digital_rgb_444_supported = None
+ self.digital_ycrcb_444_supported = None
+ self.digital_ycrcb_422_supported = None
+ self.analogue_grey_mono_display = None
+ self.analogue_rgb_display = None
+ self.analogue_non_rgb_display = None
+ self.srgb = None
+ self.preferred_timing_mode = None
+ self.gtf_supported = None
+ self.red_chroma = None
+ self.green_chroma = None
+ self.blue_chroma = None
+ self.white_chroma = None
+
+ def make_ramps(self, depth = None):
+ '''
+ Create gamma ramps with the same size as the CRTC expects
+
+ @param depth:int? The gamma depth, 8 for unsigned 8-bit integers,
+ 16 for unsigned 16-bit integers, 32 for unsigned
+ 32-bit integers, 64 for unsigned 64-bit integers,
+ -1 for single-precision floating-point values,
+ -2 for double-precision floating-point values, and
+ `None` for the gamma depth the CRTC expects
+ @return :Ramps The created gamma ramps
+ '''
+ return Ramps(self, depth = depth)
+
+ @property
+ def edid_data(self):
+ '''
+ Get parsed EDID information for the CRTC
+
+ @return :EDID Parsed EDID information
+ '''
+ if self.__edid_data is ...:
+ self.__edid_data = None if self.edid is None else EDID(self.edid)
+ return self.__edid_data
+
+class Screen:
+ '''
+ A screen or graphics card
+
+ @function restore:(self)?→void Restore the CLUT:s to the (configured) system
+ defaults, `None` if not supported
+ '''
+ def __len__(self):
+ '''
+ Get the number of CRTC:s in the screen
+
+ @return :int The number of CRTC:s in the screen
+ '''
+ return len(self.crtcs)
+
+ def __getitem__(self, indices):
+ '''
+ Get CRTC:s in the screen
+
+ @param indices:int|slice The index or index range of CRTC:s to return
+ @return :CRTC|list<CRTC> The CRTC or CRTC:s with the specified indices
+ '''
+ return self.crtcs[indices]
+
+ def __iter__(self):
+ '''
+ Iterator of the screen's CRTC:s
+
+ @yield :CRTC CRTC in the screen
+ '''
+ for value in self[:]:
+ yield value
+
+class Display:
+ '''
+ A display
+
+ @function restore:(self)?→void Restore the CLUT:s to the (configured) system
+ defaults, `None` if not supported
+ '''
+ def __len__(self):
+ '''
+ Get the number of screens in the display
+
+ @return :int The number of screens in the display
+ '''
+ return len(self.crtcs)
+
+ def __getitem__(self, indices):
+ '''
+ Get screens in the display
+
+ @param indices:int|slice The index or index range of screens to return
+ @return :CRTC|list<CRTC> The screen or screens with the specified indices
+ '''
+ return self.crtcs[indices]
+
+ def __iter__(self):
+ '''
+ Iterator of the display's screens
+
+ @yield :Screen Screen in the display
+ '''
+ for value in self[:]:
+ yield value
+
+class Ramps: ## TODO adjustments
+ '''
+ Gamma ramps
+
+ @variable red:list<float> The gamma ramp of the red channel
+ @variable green:list<float> The gamma ramp of the green channel
+ @variable blue:list<float> The gamma ramp of the blue channel
+ @variable depth:int The gamma depth, 8 for unsigned 8-bit integers,
+ 16 for unsigned 16-bit integers, 32 for unsigned
+ 32-bit integers, 64 for unsigned 64-bit integers,
+ -1 for single-precision floating-point values, and
+ -2 for double-precision floating-point values
+ '''
+ def __init__(self, crtc, depth = None, size = None):
+ '''
+ Constructor
+
+ @param crtc:CRTC? The CRTC the ramps should match, may
+ only be `None` if neither `depth` nor
+ `size` is `None`
+ @param depth:int? The gamma depth, 8 for unsigned 8-bit integers,
+ 16 for unsigned 16-bit integers, 32 for unsigned
+ 32-bit integers, 64 for unsigned 64-bit integers,
+ -1 for single-precision floating-point values,
+ -2 for double-precision floating-point values, and
+ `None` for the gamma depth the CRTC expects
+ @param size:int|(red:int, green:int, blue:int)?
+ The size of the ramps, either an integer of the size that
+ is applied to all three channels, three integers with
+ the size of each channel, or `None` for the sizes the
+ CRTC expects
+ '''
+ if depth is None:
+ depth = crtc.depth
+ if size is not None and isinstance(size, int):
+ size = (size, size, size)
+ self.depth = depth
+ def make_ramp(depth, size):
+ if depth > 0:
+ m = 1 << (depth - 1)
+ return [int(x * m / (size - 1) + 0.5) for x in range(size)]
+ else:
+ return [x / (size - 1) for x in range(size)]
+ self.red = make_ramp(self.depth, crtc.red_gamma_size if size is None else size[0])
+ self.green = make_ramp(self.depth, crtc.green_gamma_size if size is None else size[1])
+ self.blue = make_ramp(self.depth, crtc.blue_gamma_size if size is None else size[2])
+
+ def copy(self, depth = None, size = None, interpolation = None):
+ '''
+ Create a copy, optionally with a new depth or size
+
+ @param depth:int? The gamma depth in the copy, 8 for unsigned 8-bit integers,
+ 16 for unsigned 16-bit integers, 32 for unsigned
+ 32-bit integers, 64 for unsigned 64-bit integers,
+ -1 for single-precision floating-point values,
+ -2 for double-precision floating-point values, and
+ `None` for the gamma depth of the original (`self`)
+ @param size:int|(red:int, green:int, blue:int)?
+ The size of the copy, either an integer of the size that
+ is applied to all three channels, three integers with
+ the size of each channel, or `None` for the sizes of
+ the original
+ @param interpolation:(red:list<float>, green:list<float>, blue:list<float>)?→
+ (red:list<float>, green:list<float>, blue:list<float>)
+ Function used for interpolation used for resizing the
+ ramps. `None` for the default, which is intentionally
+ unspecified.
+ @return :Ramps The copy
+ '''
+ if size is None:
+ size = (len(self.red), len(self.green), len(self.blue))
+ r = Ramps(None, self.depth if depth is None else depth, size)
+ ramps = (self.red, self.green, self.blue)
+ if len(self.red) == len(r.red) and len(self.green) == len(r.green) and len(self.blue) == len(r.blue):
+ pass
+ elif interpolation is None:
+ import interpolation as interpol
+ ramps = interpol.linearly_interpolate_ramp(*ramps)
+ else:
+ ramps = interpolation(*ramps)
+ r.red[:] = ramps[0]
+ r.green[:] = ramps[1]
+ r.blue[:] = ramps[2]
+ old_max = 1 << (self.depth - 1) if self.depth > 0 else 1
+ new_max = 1 << (depth - 1) if depth > 0 else 1
+ if new_max != old_max:
+ for ramp in (r.red, r.green, r.blue):
+ for i in range(len(ramp)):
+ ramp[i] = ramp[i] * new_max / old_max
+ return r
+
+class LibgammaCRTC(CRTC):
+ '''
+ A CRTC using the libgamma backend
+ '''
+ def __init__(self, screen, crtc):
+ '''
+ Constructor
+
+ The user should not use this, but use `get_outputs` instead
+
+ @param screen:LibgammaScreen The screen of the CRTC, using the libgamma backend
+ @param crtc:int The index of the CRTC
+ '''
+ import libgamma
+ CRTC.__init__(self)
+ self.crtc = libgamma.CRTC(screen, crtc)
+ if screen.display.caps.crtc_restore:
+ self.restore = self.crtc.restore
+ else:
+ self.restore = None
+ info = libgamma.information(~0)[0]
+ connector_types = {
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_9PinDIN = '9PinDIN',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_Component = 'Component',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_Composite = 'Composite',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_DSI = 'DSI',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_DVI = 'DVI',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_DVIA = 'DVIA',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_DVID = 'DVID',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_DVII = 'DVII',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_DisplayPort = 'DisplayPort',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_HDMI = 'HDMI',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_HDMIA = 'HDMIA',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_HDMIB = 'HDMIB',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_LFP = 'LFP',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_LVDS = 'LVDS',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_SVIDEO = 'SVIDEO',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_TV = 'TV',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_VGA = 'VGA',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_VIRTUAL = 'VIRTUAL',
+ libgamma.LIBGAMMA_CONNECTOR_TYPE_eDP = 'eDP'
+ }
+ subpixel_orders = {
+ libgamma.LIBGAMMA_SUBPIXEL_ORDER_HORIZONTAL_BGR = 'BGR',
+ libgamma.LIBGAMMA_SUBPIXEL_ORDER_HORIZONTAL_RGB = 'RGB',
+ libgamma.LIBGAMMA_SUBPIXEL_ORDER_NONE = 'None',
+ libgamma.LIBGAMMA_SUBPIXEL_ORDER_VERTICAL_BGR = 'vBGR',
+ libgamma.LIBGAMMA_SUBPIXEL_ORDER_VERTICAL_RGB = 'vRGB'
+ }
+ self.edid = None if info.edid_error else libgamma.behex_edid_uppercase(info.edid)
+ self.width_mm = None if info.width_mm_error else info.width_mm
+ self.height_mm = None if info.height_mm_error else info.height_mm
+ self.red_gamma_size = None if info.gamma_size_error else info.red_gamma_size
+ self.green_gamma_size = None if info.gamma_size_error else info.green_gamma_size
+ self.blue_gamma_size = None if info.gamma_size_error else info.blue_gamma_size
+ self.gamma_depth = None if info.gamma_depth_error else info.gamma_depth
+ self.gamma_support = None if info.gamma_support_error else info.gamma_support
+ self.subpixel_order = None if info.subpixel_order_error else info.subpixel_order
+ if self.subpixel_order in subpixel_orders:
+ self.subpixel_order = subpixel_orders[self.subpixel_order]
+ self.active = None if info.active_error else info.active
+ self.connector_name = None if info.connector_name_error else info.connector_name
+ self.connector_type = None if info.connector_type_error else info.connector_type
+ if self.connector_type in connector_types:
+ self.connector_type = connector_types[self.connector_type]
+ if not info.gamma_size_error and not info.gamma_depth_error:
+ self.ramps = libgamma.GammaRamps(self.red_gamma_size, self.green_gamma_size,
+ self.blue_gamma_size, depth = self.gamma_depth)
+
+ @property
+ def backend(self):
+ '''
+ The backend which is used to access the CLUT:s, is either the
+ name of a library or the name of a display server or protocol
+
+ @return :str The backend which is used to access the CLUT:s
+ '''
+ return 'libgamma'
+
+ def get_gamma(self, low_priority = None, high_priority = None, coalesce = True):
+ '''
+ Get the gamma ramps on the CRTC or the table of applied adjustments
+
+ @param low_priority:int? Do not return adjustments with lower priority than
+ this value, `None` means that there is not lower bound.
+ Must be `None` if cooperative gamma is not supported.
+ @param high_priority:int? Do not return adjustments with higher priority than
+ this value, `None` means that there is not upper bound.
+ Must be `None` if cooperative gamma is not supported.
+ @param coalesce:bool If `False` return the adjustment table, if `True`
+ return the resulting ramps of all adjustments with a
+ priority within [`low_priority`, `high_priority`].
+ Must be `True` if cooperative gamma is not supported.
+ @return :Ramps|list<(class:str, priority:int, ramps:Ramps)>
+ The resulting ramps (if `coalesce` is `True`, or the
+ ramps if cooperative gamma is not supported) or
+ a list, sorted by priority, of the adjustments (if
+ `coalesce` is `False`), where each element is a tuple
+ with the adjustment's identifier, priority, and ramps.
+ '''
+ if low_priority is not None or high_priority is not None or not coalesce:
+ raise Exception('Cooperative gamma is not supported')
+ self.crtc.get_gamma(self.ramps):
+ return Ramps.copy(self.ramps)
+
+ def set_gamma(self, ramps, priority = None, rule = None, lifespan = 1):
+ '''
+ Set the gamma ramps on the CRTC
+
+ @param ramps:Ramps The gamma ramps
+ @param priority:int? The priority of the adjustment, `None` for the default.
+ Must be `None` (default) if cooperative gamma is not supported.
+ @param rule:str? The rule of the adjustment, `None` for the default.
+ The rule is the last part of the adjustment's identifier,
+ if this is unique within the program, it should be universally
+ unique unless another program is intentionally make it not so.
+ Must be `None` (default) if cooperative gamma is not supported.
+ @param lifespan:int The lifespan of the algorithm: `Lifespan.UNTIL_DEATH`,
+ `Lifespan.UNTIL_REMOVAL` (default), or `Lifespan.REMOVE`
+ @return The ramps which the adjustments are written to, this will
+ either be `ramps` or `self.ramps`
+ '''
+ import libgamma
+ if priority is not None or rule is not None or lifespan != 1:
+ raise Exception('Cooperative gamma is not supported')
+ if ramps is self.ramps:
+ self.crtc.set_gamma(ramps):
+ return ramps
+ match = ramps.gamma == self.gamma_depth:
+ match = match and len(ramps.red) == self.red_gamma_size:
+ match = match and len(ramps.green) == self.green_gamma_size:
+ match = match and len(ramps.blue) == self.blue_gamma_size:
+ if not match:
+ ramps = Ramps.copy(ramp, self.gamma_depth,
+ (self.red_gamma_size, self.green_gamma_size, self.blue_gamma_size))
+ if isinstance(ramps, libgamma.GammaRamps):
+ self.crtc.set_gamma(ramps):
+ return
+ for i in range(len(ramps.red)):
+ self.ramps.red[i] = ramps.red[i]
+ for i in range(len(ramps.green)):
+ self.ramps.green[i] = ramps.green[i]
+ for i in range(len(ramps.blue)):
+ self.ramps.blue[i] = ramps.blue[i]
+ self.crtc.set_gamma(self.ramps)
+ return self.ramps
+
+class LibgammaScreen(Screen):
+ '''
+ A screen (or graphics card) using the libgamma backend
+
+ @variable crtcs:list<LibgammaCRTC> The CRTC:s in the screen
+ '''
+ def __init__(self, display, screen, crtcs = None):
+ '''
+ Constructor
+
+ The user should not use this, but use `get_outputs` instead
+
+ @param display:LibgammaDisplay The display of the screen, using the libgamma backend
+ @param screen:int The index of the screen
+ @param crtcs:set<int|str>? List of CRTC:s to include, `None` for all
+ '''
+ import libgamma
+ self.screen = libgamma.Partition(display, screen)
+ if display.caps.partition_restore:
+ self.restore = self.screen.restore
+ elif display.caps.crtc_restore:
+ self.restore = self.__restore_all_crtcs
+ else:
+ self.restore = None
+ self.crtcs = []
+ if crtcs is not None:
+ crtcs = list(crtcs)
+ for i in range(self.display.crtcs_available):
+ crtc = LibgammaCRTC(self.screen, i)
+ if (crtcs is None) or (i in crtcs) or (crtc.connector_name in crtcs):
+ self.crtcs.append(crtc)
+ elif isinstance(crtc.edid, str) and (crtc.edid.upper() in crtcs):
+ self.crtcs.append(crtc)
+ else:
+ del crtc
+
+ @property
+ def backend(self):
+ '''
+ The backend which is used to access the CLUT:s, is either the
+ name of a library or the name of a display server or protocol
+
+ @return :str The backend which is used to access the CLUT:s
+ '''
+ return 'libgamma'
+
+ def __restore_all_crtcs(self):
+ '''
+ Restore the CLUT:s to the (configured) system defaults, for each CRTC
+ '''
+ for crtc for self.crtcs:
+ crtc.restore()
+
+class LibgammaDisplay(Display):
+ '''
+ A display using the libgamma backend
+
+ @variable screens:list<LibgammaScreen> The screens in the display
+ @variable crtcs:list<LibgammaCRTC> The CRTC:s in the display
+ @variable cooperative:bool Whether the adjustment method supports cooperative gamma
+ '''
+ def __init__(self, method = None, display = None, screens = None, crtcs = None):
+ '''
+ Constructor
+
+ The user should not use this, but use `get_outputs` instead
+
+ @param method:str? The adjustment method, `None` for the best available
+ @param display:str? The display, `None` to read the environment, or use
+ the only display if the adjustment method only supports
+ one display (e.g. like on Windows)
+ @param screens:set<int>? Lists of screens to include, `None` for all
+ @param crtcs:set<int|str>|dict<int,set<int|str>>?
+ List of CRTC:s to include, `None` for all, elements can
+ either be indices, connector name, or EDID:s; or a
+ dictionary mapping for screen indices to such lists
+ '''
+ import libgamma
+ self.cooperative = False
+ if method is None:
+ method = get_adjustment_methods()[0]
+ self.display = libgamma.Site(method, display)
+ self.caps = libgamma.method_capabilities(method)
+ if self.caps.site_restore:
+ self.restore = self.display.restore
+ elif self.caps.partition_restore or self.caps.crtc_restore:
+ self.restore = self.__restore_all_partitions
+ else:
+ self.restore = None
+ if screens is None:
+ screens = range(self.display.partitions_available)
+ self.screens = []
+ self.crtcs = []
+ for screen in screens:
+ cs = crtcs
+ if isinstance(cs, dict):
+ cs = cs[screen] if screen in cs else []
+ screen = LibgammaScreen(self.site, screen, cs)
+ self.screens.append(screen)
+ self.crtcs.extend(screen.crtcs)
+
+ @property
+ def backend(self):
+ '''
+ The backend which is used to access the CLUT:s, is either the
+ name of a library or the name of a display server or protocol
+
+ @return :str The backend which is used to access the CLUT:s
+ '''
+ return 'libgamma'
+
+ @property
+ def lowest_priority(self):
+ '''
+ Return the lowest filter priority accepted by the display server,
+ or other backend implementing cooperative gamma, that is, the
+ priority that guarantees that no other filter, that is not also
+ using this priority, is applied after a filter
+
+ @return :int? The lowest accepted filter priority (applied last),
+ `None` if cooperative gamma is not supported
+ '''
+ return None
+
+ @property
+ def highest_priority(self):
+ '''
+ Return the highest filter priority accepted by the display server,
+ or other backend implementing cooperative gamma, that is, the
+ priority that guarantees that no other filter, that is not also
+ using this priority, is applied before a filter
+
+ @return :int? The highest accepted filter priority (applied first),
+ `None` if cooperative gamma is not supported
+ '''
+ return None
+
+ def __restore_all_partitions(self):
+ '''
+ Restore the CLUT:s to the (configured) system defaults, for each screen
+ '''
+ for screen for self.screens:
+ screen.restore()
+
+def get_adjustment_methods(libgamma_level = 0):
+ '''
+ Returns a list of available adjustment methods
+
+ @param libgamma_level:int Which libgamma adjustment methods to include:
+ -1: None
+ 0: Methods that the environment suggests will work, excluding fake.
+ 1: Methods that the environment suggests will work, including fake.
+ 2: All real non-fake methods.
+ 3: All real methods.
+ 4: All methods.
+ @return :list<str> Adjustment method in order of preference
+ '''
+ ret = []
+ if libgamma_level >= 0:
+ try:
+ import libgamma
+ lgamma_meths = libgamma.list_methods(libgamma_level)
+ lgamma_map = {
+ libgamma.LIBGAMMA_METHOD_DUMMY = 'dummy',
+ libgamma.LIBGAMMA_METHOD_X_RANDR = 'randr',
+ libgamma.LIBGAMMA_METHOD_X_VIDMODE = 'vidmode'
+ libgamma.LIBGAMMA_METHOD_LINUX_DRM = 'drm',
+ libgamma.LIBGAMMA_METHOD_W32_GDI = 'w32gdi',
+ libgamma.LIBGAMMA_METHOD_QUARTZ_CORE_GRAPHICS = 'quartz'
+ }
+ ret += [lgamma_map[m] if m in lgamma_map else m for m in lgamma_meths]
+ except:
+ pass
+ return ret
+
+def get_outputs(method = None, display = None, screens = None, crtcs = None):
+ '''
+ Get access to CRTC for editing the their gamma ramps
+
+ @param method:str? The adjustment method, `None` for the best available.
+ "dummy" for libgamma with dummy method,
+ "randr" for libgamma with X's RAndR protocol,
+ "vidmode" for libgamma with X's VidMode protocol,
+ "drm" for libgamma with Direct Rendering Manager,
+ "w32gdi" for libgamma with Window's GDI,
+ "quartz" for libgamma with Quartz's (MacOS's) Core Graphics
+ @param display:str? The display, `None` to read the environment, or use
+ the only display if the adjustment method only supports
+ one display (e.g. like on Windows)
+ @param screens:set<int>? Lists of screens to include, `None` for all
+ @param crtcs:set<int|str>|dict<int,set<int|str>>?
+ List of CRTC:s to include, `None` for all, elements can
+ either be indices, connector name, or EDID:s; or a
+ dictionary mapping for screen indices to such lists
+ @return :Display A display
+ '''
+ if isinstance(method, str):
+ lgamma_meths = {
+ 'dummy' = libgamma.LIBGAMMA_METHOD_DUMMY,
+ 'randr' = libgamma.LIBGAMMA_METHOD_X_RANDR,
+ 'vidmode' = libgamma.LIBGAMMA_METHOD_X_VIDMODE,
+ 'drm' = libgamma.LIBGAMMA_METHOD_LINUX_DRM,
+ 'w32gdi' = libgamma.LIBGAMMA_METHOD_W32_GDI,
+ 'quartz' = libgamma.LIBGAMMA_METHOD_QUARTZ_CORE_GRAPHICS
+ }
+ return LibgammaDisplay(lgamma_meths[method], display, screen, crtc)
+ else:
+ return LibgammaDisplay(method, display, screen, crtc)