Get/Set color properties easily in Python

Shared Libraries
Forum rules
For sharing working examples of macros / scripts. These can be in any script language supported by OpenOffice.org [Basic, Python, Netbean] or as source code files in Java or C# even - but requires the actual source code listing. This section is not for asking questions about writing your own macros.
Post Reply
Induane
Posts: 28
Joined: Fri Mar 28, 2014 4:49 am

Get/Set color properties easily in Python

Post by Induane »

Have you ever noticed that the way OpenOffice stores colors is basically inscrutible? I'm sure it all makes sense in some way, but it just isn't very intuitive. Even if you've managed to do some conversions one way or another from somethign familiar into that weird numerical storage value that's used internally, you still end up having to do something each time you set it. Worse still, what happens when you want to get a color value and then display it in a way that makes sense to you just reading a log file or something? I've found a few utilities that do one way conversions, etc.. but none were ever to my liking.

In any case I finally got tired of the annoyance and wrote some utilities to help me out. Everyone here on the forums has tried to be really helpful to me, so I suppose it's time I humbly give back something.

Code: Select all

# Standard
import re
import logging

LOG = logging.getLogger(__name__)


class ColorConvert(object):
    """
    Conversion utilities for OpenOffice/LibreOffice color values
    """

    @staticmethod
    def int_to_rgb(oo_int):
        """
        Convert OpenOffice/LibreOffice color integer representation back to
        rgb format.

        :type oo_int: int
        :param oo_int: openoffice integer representation of a color value
        :retuns: red, green, and blue values
        :rtype: tuple
        """
        red = oo_int >> 16
        green = (oo_int >> 8) - (red << 8)
        blue = oo_int - (green << 8) - (red << 16)
        return (red, green, blue)

    @staticmethod
    def rgb_to_int(red, green, blue):
        """
        Return an integer which repsents an OpenOffice/LibreOffice color from a
        set or rgb values.

        :type red: int
        :param red: amount of included red
        :type green: int
        :param green: amount of included green
        :type blue: int
        :param blue: amount of included blue
        :returns: special int representation of color value
        :rtype: int
        """
        assert isinstance(red, int), '%s is not an integer'
        assert isinstance(green, int), '%s is not an integer'
        assert isinstance(blue, int), '%s is not an integer'
        red_int = (red & 255) << 16
        green_int = (green & 255) << 8
        blue_int = (blue & 255)
        return (red_int | green_int | blue_int)

    @staticmethod
    def rgb_to_html(red, green, blue):
        """
        Given a set of integers representing red, green, and blue values,
        return an html type format code.

        :type red: int
        :param red: amount of included red
        :type green: int
        :param green: amount of included green
        :type blue: int
        :param blue: amount of included blue
        :returns: html code representation of color value
        :rtype: str
        """
        assert isinstance(red, int), '%s is not an integer'
        assert isinstance(green, int), '%s is not an integer'
        assert isinstance(blue, int), '%s is not an integer'
        red_int = (red & 255)
        green_int = (green & 255)
        blue_int = (blue & 255)
        red_str = hex(red_int)[2:].upper()
        red_str = '00' if red_str == '0' else red_str
        green_str = hex(green_int)[2:].upper()
        green_str = '00' if green_str == '0' else green_str
        blue_str = hex(blue_int)[2:].upper()
        blue_str = '00' if blue_str == '0' else blue_str
        return "#%s%s%s" % (red_str, green_str, blue_str)

    @staticmethod
    def html_to_rgb(html_str):
        """
        Convert an html string into rgb integer values

        :type html_str: str
        :param html_str: html color notation value
        :returns: rgb integer values
        :rtype: tuple
        """
        html_str = html_str.lstrip('#')
        if len(html_str) == 3:
            red = int("%s%s" % (html_str[0], html_str[0]), 16)
            green = int("%s%s" % (html_str[1], html_str[1]), 16)
            blue = int("%s%s" % (html_str[2], html_str[2]), 16)
        elif len(html_str) == 6:
            red = int(html_str[:2], 16)
            green = int(html_str[2:4], 16)
            blue = int(html_str[4:6], 16)
        else:
            raise ValueError('"%s" is not valid html notation.' % html_str)
        return (red, green, blue)

    @staticmethod
    def html_to_int(html_str):
        """
        Convert a string of html color notation into an openoffice color
        integer.

        :type html_str: str
        :param html_str: html color notation value
        :returns: OpenOffice/LibreOffice color notation integer value
        :rtype: int
        """
        rgb = ColorConvert.html_to_rgb(html_str)
        return ColorConvert.rgb_to_int(*rgb)

    @staticmethod
    def int_to_html(oo_int):
        """
        Convert an OpenOffice/LibreOffice color integer into html notation.

        :type oo_int: int
        :param oo_int: openoffice integer representation of a color value
        :returns: html code representation of color value
        :rtype: str
        """
        rgb = ColorConvert.int_to_rgb(oo_int)
        return ColorConvert.rgb_to_html(*rgb)


class StrConst(str):
    """Subclasses string and allows other arguments to pass through to init"""

    def __init__(self, *args, **kwargs):
        super(StrConst, self).__init__()

    def __new__(cls, value, *args, **kwargs):
        return str.__new__(cls, value)


class PropertyName(StrConst):
    """Constants value for names of openoffice property values"""
    def __init__(self, *args, **kwargs):
        super(PropertyName, self).__init__(*args, **kwargs)

    def __call__(self, x_model):
        """
        Extract the value from the x_model by calling through to the
        extract_value method.
        """
        return self.get_value(x_model)

    def apply_value(self, x_model, value, overwrite=True):
        """
        Apply the given value to the specified x_model using this properties
        name.

        :type x_model: object
        :param x_model: object to apply this property and value to
        :type value: str | int | object
        :param value: new value to set for the property value on the x_model
        :type overwrite: bool
        :param overwrite: Switches overwriting of existing values on and off
        :raises: ValueError
        """
        if not overwrite:
            if x_model.getPropertySetInfo().hasPropertyByName(self.__str__()):
                raise ValueError('%s already exists on xmodel and'
                                 ' overwrite set to "False"')
        LOG.debug("Setting Property %s to %s" % (self.__str__(), value))
        try:
            x_model.setPropertyValue(self.__str__(), value)
        except Exception as ex:
            LOG.error('Set property "%s" failed. %s' % (self, ex))
            raise ValueError('Invalid property name %s' % str(self))

    def get_value(self, x_model, default=None):
        """
        Extract this property from the given x_model. If the property does not
        exist then return the default value instead.
        """
        if x_model.getPropertySetInfo().hasPropertyByName(self.__str__()):
            return x_model.getPropertyValue(self.__str__())
        else:
            LOG.debug("No property named %s. Using default" % self.__str__())
            return default


class ColorMixins(object):
    """Mixin class for Color values """

    @property
    def rgb_exp(self):
        return (
            r'^(?:[R,r][G,g][B,b])'  # Match rgb case insensitive
            r'[(]'                   # Match start of rgb parenthesis
            r'(?:\d{1,3}, ?){2}'     # Match up to 3 digits with trailing comma
            r'(?:\d{1,3} ?){1}'      # Match 3 digits with no trailing comma
            r'[)]$'                  # End rgb parenthesis
        )

    @property
    def html_exp(self):
        """Regex for matching string if html color notation"""
        return (
            r'^#?(?:'            # Start regex, optional # at html code start
            r'[A-F,a-f,0-9]{6}'  # Allow 6 numbers or characters a-f
            r'|'                 # OR
            r'[A-F,a-f,0-9]{3}'  # Allow 3 numbers or characters af
            r')$'                # End regex
        )

    def _parse_rgb(self, rgb_val):
        """
        Parse a string into a tuple of three integers. The string should
        resemble::

            rgb(255, 201, 90)

        :type rgb_val: str
        :param rgb_val: value to parse into tuple of three integers
        :returns: integers contained in rgb string
        :rtype: tuple
        """
        rgb_val = rgb_val.lower.replace('rgb', '').replace(' ', '')
        rgb_val = rgb_val.replace('(', '')
        rgb_val = rgb_val.replace(')', '')
        val_list = rgb_val.split(',')
        return (int(val_list[0]), int(val_list[1]), int(val_list[1]))

    def _value_to_int(self, value):
        """
        Convert whatever value is provided to the correct int notation. If the
        value is already in integer notation, just return it.

        Example allowed values::

            FOO.apply_value(16777215)               # API Integer Notation
            FOO.apply_value('#FEFD4A')              # HTML Notation
            FOO.apply_value('rgb(255, 255, 255)')   # RGB String
            FOO.apply_value((255, 255, 255))        # RGB Tuple
        :type value: str | int | object
        :param value: new value to set for the property value on the x_model
        :returns: value converted to openoffice color integer value
        :rtype: int
        """
        # If we're given a html code or rgb value, convert it to an integer
        if isinstance(value, basestring):
            is_html = re.match(self.html_exp, value)
            if is_html:
                value = ColorConvert.html_to_int(value)
            else:
                is_rgb = re.match(self.rgb_exp, value)
                if is_rgb:
                    rgb = self._parse_rgb(value)
                    value = ColorConvert.rgb_to_int(*rgb)
        elif isinstance(value, tuple):
            assert len(value) == 3, 'RGB requires 3 values in Tuple'
        else:
            assert isinstance(value, int), 'Invalid color value'
            if value > 16777215:
                raise ValueError('Value larger than 16777215 - invalid color.')
        return value


class ColorPropertyName(PropertyName, ColorMixins):
    """
    Constants value for names of OpenOffice/LibreOffice property values which
    reference properties using the special color integer type. To make use of
    color properties easier, values are returned by default as an HTML color
    notation string (i.e. #FE25A1). Alternatively they can be returned as a
    simple tuple of rgb integers (i.e. (223, 10, 149)). Values may also be set
    using color notation, a tuple of rgb values, or a string indicating rgb
    values. The default OpenOffice/LibreOffice integer notation may also be
    used to set the value.
    """

    def apply_value(self, x_model, value, overwrite=True):
        """
        Apply the given color value to the specified x_model using this
        properties name. If the value passed is an rgb value or html color
        notation, convert the value to a color integer first.

        Example allowed values::

            FOO.apply_value(16777215)               # API Integer Notation
            FOO.apply_value('#FEFD4A')              # HTML Notation
            FOO.apply_value('rgb(255, 255, 255)')   # RGB String
            FOO.apply_value((255, 255, 255))        # RGB Tuple

        :type x_model: object
        :param x_model: object to apply this property and value to
        :type value: str | int | object
        :param value: new value to set for the property value on the x_model
        :type overwrite: bool
        :param overwrite: Switches overwriting of existing values on and off
        :raises: ValueError
        """
        if not overwrite:
            if x_model.getPropertySetInfo().hasPropertyByName(self.__str__()):
                raise ValueError('%s already exists on xmodel and'
                                 ' overwrite set to "False"')

        # If we're given a html code or rgb value, convert it to an integer
        value = self._value_to_int(value)

        LOG.debug("Setting Color Property %s to %s" % (self.__str__(), value))
        x_model.setPropertyValue(self.__str__(), value)

    def get_value(self, x_model, default=None, rgb=False):
        """
        Extract this property from the given x_model. If the property does not
        exist then return the default value instead. By default this method
        returns html notation for the color value. It can alternatively return
        a tuple of red, green, and blue values.

        :type default: Anything
        :param default: value to return if the property not set
        :type rgb: bool
        :param rgb: if True, return an rgb tuple instead of html notation
        :returns: html notation string or tuple
        :rtype: str | tuple
        """
        if x_model.getPropertySetInfo().hasPropertyByName(self.__str__()):
            value = x_model.getPropertyValue(self.__str__())
            if rgb:
                return ColorConvert.int_to_rgb(value)
            return ColorConvert.int_to_html(value)
        else:
            LOG.debug("No property named %s. Using default" % self.__str__())
            return default
Now for the part that's more fun - actually using this utility. First off I'm just going to assume you've imported it. I keep around a constants file with values like:

Code: Select all

TEXT_COLOR = ColorPropertyName('TextColor')
BORDER_COLOR = ColorPropertyName('BorderColor')
TEXT_LINE_COLOR = ColorPropertyName('TextLineColor')
BACKGROUND_COLOR = ColorPropertyName('BackgroundColor')
Then if I ever need to say - set the text color value on a character span or a widget label, I simply do:

Code: Select all

TEXT_COLOR.apply_value(char_span_model, '#FE569F')
Or if I want to GET the value of that property:

Code: Select all

span_color = TEXT_COLOR.get_value(char_span_model)
To me this is much nicer and simpler than doing weird conversions all the time. In fact I often setup classes that abstract the properties of something like say a dialog label widget so that I can use it all the time - something akin to:

Code: Select all

class Label(object):
    def __init__(self, x_model, control):
        self._control = control
        self._x_model = x_model

    @property
    def control(self):
        return self._control

    @property
    def x_model(self):
        return self._x_model

    def _get_text_color(self):
        return TEXT_COLOR.get_value(self.x_model)

    def _set_text_color(self, value):
        TEXT_COLOR.set_value(self.x_model, value)

    text_color = property(_get_text_color, _set_text_color)

# Then on any instance of a Label I can just set its color like this:
somelabel.text_color = '#55667F'
Of course I stick more than that on the class, but hopefully that gives some idea of how powerful it is to make simple abstractions with such tools. It's worth noting that there is a generic PropertyName for properties that aren't color values. I'm sure one could conjure up more special types. All of the values are subclasses of string objects too, so you can use them in multiproperty setters, dictionary keys, anywhere you'd normally use a string (overstating the obvious is apparently my middle name!). This makes it a really flexible way to maintain such values, while adding a layer of power and control to them.

Anyway, I hope someone finds this useful and saves them from ripping out some of their hair!

Cheers all!
OpenOffice 3.1 on Windows 7 / LibreOffice 3.6 on Ubuntu 13.10 / LibreOffice 4.1 on Ubuntu 13.10
Post Reply