Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monterey 12.3 removed python 2 #1

Open
saschaeggi opened this issue Mar 19, 2022 · 7 comments
Open

Monterey 12.3 removed python 2 #1

saschaeggi opened this issue Mar 19, 2022 · 7 comments
Assignees

Comments

@saschaeggi
Copy link

Hey,

First of all thank you so much for this helpful tool!

As Apple has removed phyton 2.7 from Monterey 12.3 (see https://scriptingosx.com/2022/03/macos-monterey-12-3-removes-python-2-link-collection/) this project needs to be updated.

@supercurio
Copy link
Owner

supercurio commented Mar 22, 2022

Hi @saschaeggi !
Yep, it's gone now.

Thanks for reporting the issue. It's good to know that this utility actually has users. Without telemetry I wasn't really sure.
I've been working on a Python 3 version for some time and I guess it's time to start to prepare a release!

@supercurio supercurio self-assigned this Mar 22, 2022
@saschaeggi
Copy link
Author

@supercurio super looking forward to it!

@GHubbler
Copy link

Would be glad to see the project maintained and updated to python3. :)

@saschaeggi
Copy link
Author

Same here but I have no clue about python...

@saschaeggi
Copy link
Author

Any update on this? 🙂

@GHubbler
Copy link

GHubbler commented Jun 3, 2023

I'm not a Python expert (yet), but I managed to modify the code to work with Python3 on macOS Monterey 12.6.6.
What Do you think?

#!/usr/bin/env python3
# coding=utf-8

# Author: François Simond (supercurio)
# project: https://github.com/supercurio/xdr-tuner
# license: Apache 2 (see LICENSE)
# 2023-06-03: Updated to Python 3 and macOS Monterey and above?)

import signal
import sys
import time
from CoreFoundation import kCFURLPOSIXPathStyle
from Foundation import *
import Quartz
import objc
import struct
import json
from optparse import OptionParser
import os

version = "0.3"

color_sync_framework = '/System/Library/Frameworks/ApplicationServices.framework/' \
                       'Versions/A/Frameworks/ColorSync.framework'

color_sync_bridge_string = """<?xml version='1.0'?>
    <signatures version='1.0'>
      <constant name='kColorSyncDeviceDefaultProfileID' type='^{__CFString=}'/>
      <constant name='kColorSyncDisplayDeviceClass' type='^{__CFString=}'/>
      <constant name='kColorSyncProfileUserScope' type='^{__CFString=}'/>
      <function name='CGDisplayCreateUUIDFromDisplayID'>
        <arg type='I'/>
        <retval already_retained='true' type='^{__CFUUID=}'/>
      </function>
      <function name='ColorSyncDeviceCopyDeviceInfo'>
        <arg type='^{__CFString=}'/>
        <arg type='^{__CFUUID=}'/>
        <retval already_retained='true' type='^{__CFDictionary=}'/>
      </function>
      <function name='ColorSyncDeviceSetCustomProfiles'>
        <arg type='^{__CFString=}'/>
        <arg type='^{__CFUUID=}'/>
        <arg type='^{__CFDictionary=}'/>
        <retval type='B'/>
      </function>
    </signatures>"""

objc.parseBridgeSupport(color_sync_bridge_string, globals(),
                        color_sync_framework)


def get_device_info():
    online_display_list_result = Quartz.CGGetOnlineDisplayList(32, None, None)
    error = online_display_list_result[0]
    if error != Quartz.kCGErrorSuccess:
        raise Exception('Failed to get online displays from Quartz')
    display_id = online_display_list_result[1][0]
    device_info = ColorSyncDeviceCopyDeviceInfo(kColorSyncDisplayDeviceClass,
                                                CGDisplayCreateUUIDFromDisplayID(display_id))
    if not device_info:
        raise Exception('KVM connection on bot is broken, please file a bug')
    return device_info


def get_device_id():
    return get_device_info()['DeviceID']


def get_factory_profile_path():
    device_info = get_device_info()
    factory_profile_url = device_info['FactoryProfiles']['1']['DeviceProfileURL']
    return Foundation.CFURLCopyFileSystemPath(factory_profile_url, kCFURLPOSIXPathStyle)


def get_custom_profile_path():
    device_info = get_device_info()
    custom_profiles = device_info.get('CustomProfiles')
    if custom_profiles:
        factory_profile_url = custom_profiles['1']
        return Foundation.CFURLCopyFileSystemPath(factory_profile_url, kCFURLPOSIXPathStyle)
    else:
        return None


def set_display_custom_profile(profile_path):
    if profile_path is None:
        profile_url = Foundation.kCFNull
    else:
        profile_url = Foundation.CFURLCreateFromFileSystemRepresentation(None, profile_path.encode('utf-8'),
                                                                         len(profile_path), False)
    profile_info = {
        kColorSyncDeviceDefaultProfileID: profile_url,
        kColorSyncProfileUserScope: Foundation.kCFPreferencesCurrentUser
    }
    device_id = get_device_id()
    result = ColorSyncDeviceSetCustomProfiles(kColorSyncDisplayDeviceClass, device_id, profile_info)
    if not result:
        raise Exception('Failed to set display custom profile')


def modify_profile(factory_profile, config, out_file):
    f = open(factory_profile, 'rb')
    profile_data = f.read()
    f.close()

    # find the offset
    tag = b'vcgt' # Convert tag to bytes
    tag_offset = profile_data.find(tag, profile_data.find(tag) + 4)

    # parse the table
    vcgt_data_fmt = '>9i'
    vcgt_data_offset = tag_offset + 12
    vcgt_struct = struct.Struct(vcgt_data_fmt)
    (red_gamma, red_min, red_max,
     green_gamma, green_min, green_max,
     blue_gamma, blue_min, blue_max) = vcgt_struct.unpack_from(profile_data, vcgt_data_offset)

    maximum = config['maximum']
    red_max = round(red_max * maximum['red'])
    green_max = round(green_max * maximum['green'])
    blue_max = round(blue_max * maximum['blue'])

    gamma = config['gamma']
    red_gamma = round(red_gamma * gamma['red'])
    green_gamma = round(green_gamma * gamma['green'])
    blue_gamma = round(blue_gamma * gamma['blue'])

    buff = bytearray(profile_data)

    if config['reorder_channels']:
        vcgt_struct.pack_into(buff, vcgt_data_offset,
                              green_gamma, green_min, green_max,
                              blue_gamma, blue_min, blue_max,
                              red_gamma, red_min, red_max)
    else:
        vcgt_struct.pack_into(buff, vcgt_data_offset,
                              red_gamma, red_min, red_max,
                              green_gamma, green_min, green_max,
                              blue_gamma, blue_min, blue_max)

    out = open(out_file, 'wb')
    out.write(buff)


def read_config(config_file):
    return json.load(open(config_file, 'r'))


def set_auto_apply(status):
    plist_file = os.path.expanduser('~') + "/Library/LaunchAgents/xdr-tuner-auto-apply.plist"
    if status:
        os.system("plutil -create xml1 " + plist_file)
        os.system("plutil -insert \"Label\" -string \"XDR Tuner\" " + plist_file)
        os.system("plutil -insert \"ProgramArguments\" -array " + plist_file)
        os.system("plutil -insert \"ProgramArguments.0\" -string \"{}\" ".format(os.path.realpath(__file__))
                  + plist_file)
        os.system("plutil -insert \"ProgramArguments.1\" -string \"-r\" " + plist_file)
        os.system("plutil -insert \"RunAtLoad\" -bool YES " + plist_file)
    else:
        try:
            os.remove(plist_file)
        except OSError:
            print("No auto-apply to remove")


def signal_handler(sig, frame):
    print('Stopped the tuning loop.')
    sys.exit(0)


def main():
    print("Liquid Retina XDR display tuner v{}\n".format(version),
          "  by François Simond (supercurio)\n",
          "  https://github.com/supercurio/xdr-tuner\n")

    signal.signal(signal.SIGINT, signal_handler)

    script_path = os.path.dirname(os.path.realpath(__file__))

    parser = OptionParser()
    parser.add_option("-o", "--out", dest="out_file", default=script_path + "/profiles/tuned.icc",
                      help="output ICC file")
    parser.add_option("-c", "--config", dest="config_file", default=script_path + "/configs/default.json",
                      help="read config from a custom JSON file")
    parser.add_option("-l", "--loop", dest="loop", action="store_true", default=False,
                      help="apply the config in a loop until interrupted")
    parser.add_option("-f", "--factory", dest="factory", action="store_true", default=False,
                      help="reset to factory profile")
    parser.add_option("-a", "--apply", dest="apply_icc", default="", help="apply ICC profile")
    parser.add_option("-r", "--re-apply", dest="re_apply", action="store_true", default=False,
                      help="re-apply last custom profile set")
    parser.add_option("-t", "--auto-apply", dest="auto_apply", action="store_true", default=False,
                      help="enable auto load of custom profile at start")
    parser.add_option("-u", "--remove-auto-apply", dest="remove_auto_apply", action="store_true", default=False,
                      help="disable auto load of custom profile at start")
    (options, _) = parser.parse_args()

    if options.apply_icc:
        print("Apply existing profile: " + options.apply_icc)
        set_display_custom_profile(options.apply_icc)
        return

    if options.factory:
        print("Reset to factory profile")
        set_display_custom_profile(None)
        return

    if options.re_apply:
        current_custom_profile = get_custom_profile_path()
        if current_custom_profile:
            print("Reapply custom profile: " + current_custom_profile)
            set_display_custom_profile(current_custom_profile)
        else:
            print("No custom profile set to re-apply")
        return

    if options.auto_apply:
        print("Enable loading of custom profile at start")
        set_auto_apply(True)
        return

    if options.remove_auto_apply:
        print("Disable loading of custom profile at start")
        set_auto_apply(False)
        return

    out_file = options.out_file
    factory_profile = get_factory_profile_path()
    print("Factory ICC profile:\n  " + factory_profile)
    print("Output ICC profile:\n  " + out_file)

    if options.loop:
        print("\nReloading " + options.config_file + " in a loop:")

    while True:
        config = read_config(options.config_file)
        modify_profile(factory_profile, config, out_file)
        set_display_custom_profile(out_file)
        if not options.loop:
            return
        print('.', end='')
        sys.stdout.flush()
        time.sleep(1 / 4.0)


if __name__ == '__main__':
    main()

@saschaeggi
Copy link
Author

@GHubbler that works like a charm, thank you! 👏 cc @supercurio

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants