#!/usr/bin/env python
#
# Copyright 2009 VMware, Inc.
# Copyright 2014 Intel Corporation
# All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sub license, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice (including the
# next paragraph) shall be included in all copies or substantial portions
# of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
# IN NO EVENT SHALL VMWARE AND/OR ITS SUPPLIERS BE LIABLE FOR
# ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import sys
VOID = 'x'
UNSIGNED = 'u'
SIGNED = 's'
FLOAT = 'f'
ARRAY = 'array'
PACKED = 'packed'
OTHER = 'other'
RGB = 'rgb'
SRGB = 'srgb'
YUV = 'yuv'
ZS = 'zs'
VERY_LARGE = 99999999999999999999999
class Channel:
"""Describes a color channel."""
def __init__(self, type, norm, size):
self.type = type
self.norm = norm
self.size = size
self.sign = type in (SIGNED, FLOAT)
self.name = None # Set when the channels are added to the format
self.shift = -1 # Set when the channels are added to the format
self.index = -1 # Set when the channels are added to the format
def __str__(self):
s = str(self.type)
if self.norm:
s += 'n'
s += str(self.size)
return s
def __eq__(self, other):
return self.type == other.type and self.norm == other.norm and self.size == other.size
def max(self):
"""Returns the maximum representable number."""
if self.type == FLOAT:
return VERY_LARGE
if self.norm:
return 1
if self.type == UNSIGNED:
return (1 << self.size) - 1
if self.type == SIGNED:
return (1 << (self.size - 1)) - 1
assert False
def min(self):
"""Returns the minimum representable number."""
if self.type == FLOAT:
return -VERY_LARGE
if self.type == UNSIGNED:
return 0
if self.norm:
return -1
if self.type == SIGNED:
return -(1 << (self.size - 1))
assert False
def one(self):
"""Returns the value that represents 1.0f."""
if self.type == UNSIGNED:
return (1 << self.size) - 1
if self.type == SIGNED:
return (1 << (self.size - 1)) - 1
else:
return 1
def datatype(self):
"""Returns the datatype corresponding to a channel type and size"""
return _get_datatype(self.type, self.size)
class Swizzle:
"""Describes a swizzle operation.
A Swizzle is a mapping from one set of channels in one format to the
channels in another. Each channel in the destination format is
associated with one of the following constants:
* SWIZZLE_X: The first channel in the source format
* SWIZZLE_Y: The second channel in the source format
* SWIZZLE_Z: The third channel in the source format
* SWIZZLE_W: The fourth channel in the source format
* SWIZZLE_ZERO: The numeric constant 0
* SWIZZLE_ONE: THe numeric constant 1
* SWIZZLE_NONE: No data available for this channel
Sometimes a Swizzle is represented by a 4-character string. In this
case, the source channels are represented by the characters "x", "y",
"z", and "w"; the numeric constants are represented as "0" and "1"; and
no mapping is represented by "_". For instance, the map from
luminance-alpha to rgba is given by "xxxy" because each of the three rgb
channels maps to the first luminance-alpha channel and the alpha channel
maps to second luminance-alpha channel. The mapping from bgr to rgba is
given by "zyx1" because the first three colors are reversed and alpha is
always 1.
"""
__identity_str = 'xyzw01_'
SWIZZLE_X = 0
SWIZZLE_Y = 1
SWIZZLE_Z = 2
SWIZZLE_W = 3
SWIZZLE_ZERO = 4
SWIZZLE_ONE = 5
SWIZZLE_NONE = 6
def __init__(self, swizzle):
"""Creates a Swizzle object from a string or array."""
if isinstance(swizzle, str):
swizzle = [Swizzle.__identity_str.index(c) for c in swizzle]
else:
swizzle = list(swizzle)
for s in swizzle:
assert isinstance(s, int) and 0 <= s and s <= Swizzle.SWIZZLE_NONE
assert len(swizzle) <= 4
self.__list = swizzle + [Swizzle.SWIZZLE_NONE] * (4 - len(swizzle))
assert len(self.__list) == 4
def __iter__(self):
"""Returns an iterator that iterates over this Swizzle.
The values that the iterator produces are described by the SWIZZLE_*
constants.
"""
return self.__list.__iter__()
def __str__(self):
"""Returns a string representation of this Swizzle."""
return ''.join(Swizzle.__identity_str[i] for i in self.__list)
def __getitem__(self, idx):
"""Returns the SWIZZLE_* constant for the given destination channel.
Valid values for the destination channel include any of the SWIZZLE_*
constants or any of the following single-character strings: "x", "y",
"z", "w", "r", "g", "b", "a", "z" "s".
"""
if isinstance(idx, int):
assert idx >= Swizzle.SWIZZLE_X and idx <= Swizzle.SWIZZLE_NONE
if idx <= Swizzle.SWIZZLE_W:
return self.__list.__getitem__(idx)
else:
return idx
elif isinstance(idx, str):
if idx in 'xyzw':
idx = 'xyzw'.find(idx)
elif idx in 'rgba':
idx = 'rgba'.find(idx)
elif idx in 'zs':
idx = 'zs'.find(idx)
else:
assert False
return self.__list.__getitem__(idx)
else:
assert False
def __mul__(self, other):
"""Returns the composition of this Swizzle with another Swizzle.
The resulting swizzle is such that, for any valid input to
__getitem__, (a * b)[i] = a[b[i]].
"""
assert isinstance(other, Swizzle)
return Swizzle(self[x] for x in other)
def inverse(self):
"""Returns a pseudo-inverse of this swizzle.
Since swizzling isn't necisaraly a bijection, a Swizzle can never
be truely inverted. However, the swizzle returned is *almost* the
inverse of this swizzle in the sense that, for each i in range(3),
a[a.inverse()[i]] is either i or SWIZZLE_NONE. If swizzle is just
a permutation with no channels added or removed, then this
function returns the actual inverse.
This "pseudo-inverse" idea can be demonstrated by mapping from
luminance-alpha to rgba that is given by "xxxy". To get from rgba
to lumanence-alpha, we use Swizzle("xxxy").inverse() or "xw__".
This maps the first component in the lumanence-alpha texture is
the red component of the rgba image and the second to the alpha
component, exactly as you would expect.
"""
rev = [Swizzle.SWIZZLE_NONE] * 4
for i in xrange(4):
for j in xrange(4):
if self.__list[j] == i and rev[i] == Swizzle.SWIZZLE_NONE:
rev[i] = j
return Swizzle(rev)
class Format:
"""Describes a pixel format."""
def __init__(self, name, layout, block_width, block_height, block_depth, channels, swizzle, colorspace):
"""Constructs a Format from some metadata and a list of channels.
The channel objects must be unique to this Format and should not be
re-used to construct another Format. This is because certain channel
information such as shift, offset, and the channel name are set when
the Format is created and are calculated based on the entire list of
channels.
Arguments:
name -- Name of the format such as 'MESA_FORMAT_A8R8G8B8'
layout -- One of 'array', 'packed' 'other', or a compressed layout
block_width -- The block width if the format is compressed, 1 otherwise
block_height -- The block height if the format is compressed, 1 otherwise
block_depth -- The block depth if the format is compressed, 1 otherwise
channels -- A list of Channel objects
swizzle -- A Swizzle from this format to rgba
colorspace -- one of 'rgb', 'srgb', 'yuv', or 'zs'
"""
self.name = name
self.layout = layout
self.block_width = block_width
self.block_height = block_height
self.block_depth = block_depth
self.channels = channels
assert isinstance(swizzle, Swizzle)
self.swizzle = swizzle
self.name = name
assert colorspace in (RGB, SRGB, YUV, ZS)
self.colorspace = colorspace
# Name the channels
chan_names = ['']*4
if self.colorspace in (RGB, SRGB):
for (i, s) in enumerate(swizzle):
if s < 4:
chan_names[s] += 'rgba'[i]
elif colorspace == ZS:
for (i, s) in enumerate(swizzle):
if s < 4:
chan_names[s] += 'zs'[i]
else:
chan_names = ['x', 'y', 'z', 'w']
for c, name in zip(self.channels, chan_names):
assert c.name is None
if name == 'rgb':
c.name = 'l'
elif name == 'rgba':
c.name = 'i'
elif name == '':
c.name = 'x'
else:
c.name = name
# Set indices and offsets
if self.layout == PACKED:
shift = 0
for channel in self.channels:
assert channel.shift == -1
channel.shift = shift
shift += channel.size
for idx, channel in enumerate(self.channels):
assert channel.index == -1
channel.index = idx
else:
pass # Shift means nothing here
def __str__(self):
return self.name
def short_name(self):
"""Returns a short name for a format.
The short name should be suitable to be used as suffix in function
names.
"""
name = self.name
if name.startswith('MESA_FORMAT_'):
name = name[len('MESA_FORMAT_'):]
name = name.lower()
return name
def block_size(self):
"""Returns the block size (in bits) of the format."""
size = 0
for channel in self.channels:
size += channel.size
return size
def num_channels(self):
"""Returns the number of channels in the format."""
nr_channels = 0
for channel in self.channels:
if channel.size:
nr_channels += 1
return nr_channels
def array_element(self):
"""Returns a non-void channel if this format is an array, otherwise None.
If the returned channel is not None, then this format can be
considered to be an array of num_channels() channels identical to the
returned channel.
"""
if self.layout == ARRAY:
return self.channels[0]
elif self.layout == PACKED:
ref_channel = self.channels[0]
if ref_channel.type == VOID:
ref_channel = self.channels[1]
for channel in self.channels:
if channel.size == 0 or channel.type == VOID:
continue
if channel.size != ref_channel.size or channel.size % 8 != 0:
return None
if channel.type != ref_channel.type:
return None
if channel.norm != ref_channel.norm:
return None
return ref_channel
else:
return None
def is_array(self):
"""Returns true if this format can be considered an array format.
This function will return true if self.layout == 'array'. However,
some formats, such as MESA_FORMAT_A8G8B8R8, can be considered as
array formats even though they are technically packed.
"""
return self.array_element() != None
def is_compressed(self):
"""Returns true if this is a compressed format."""
return self.block_width != 1 or self.block_height != 1 or self.block_depth != 1
def is_int(self):
"""Returns true if this format is an integer format.
See also: is_norm()
"""
if self.layout not in (ARRAY, PACKED):
return False
for channel in self.channels:
if channel.type not in (VOID, UNSIGNED, SIGNED):
return False
return True
def is_float(self):
"""Returns true if this format is an floating-point format."""
if self.layout not in (ARRAY, PACKED):
return False
for channel in self.channels:
if channel.type not in (VOID, FLOAT):
return False
return True
def channel_type(self):
"""Returns the type of the channels in this format."""
_type = VOID
for c in self.channels:
if c.type == VOID:
continue
if _type == VOID:
_type = c.type
assert c.type == _type
return _type
def channel_size(self):
"""Returns the size (in bits) of the channels in this format.
This function should only be called if all of the channels have the
same size. This is always the case if is_array() returns true.
"""
size = None
for c in self.channels:
if c.type == VOID:
continue
if size is None:
size = c.size
assert c.size == size
return size
def max_channel_size(self):
"""Returns the size of the largest channel."""
size = 0
for c in self.channels:
if c.type == VOID:
continue
size = max(size, c.size)
return size
def is_normalized(self):
"""Returns true if this format is normalized.
While only integer formats can be normalized, not all integer formats
are normalized. Normalized integer formats are those where the
integer value is re-interpreted as a fixed point value in the range
[0, 1].
"""
norm = None
for c in self.channels:
if c.type == VOID:
continue
if norm is None:
norm = c.norm
assert c.norm == norm
return norm
def has_channel(self, name):
"""Returns true if this format has the given channel."""
if self.is_compressed():
# Compressed formats are a bit tricky because the list of channels
# contains a single channel of type void. Since we don't have any
# channel information there, we pull it from the swizzle.
if str(self.swizzle) == 'xxxx':
return name == 'i'
elif str(self.swizzle)[0:3] in ('xxx', 'yyy'):
if name == 'l':
return True
elif name == 'a':
return self.swizzle['a'] <= Swizzle.SWIZZLE_W
else:
return False
elif name in 'rgba':
return self.swizzle[name] <= Swizzle.SWIZZLE_W
else:
return False
else:
for channel in self.channels:
if channel.name == name:
return True
return False
def get_channel(self, name):
"""Returns the channel with the given name if it exists."""
for channel in self.channels:
if channel.name == name:
return channel
return None
def datatype(self):
"""Returns the datatype corresponding to a format's channel type and size"""
if self.layout == PACKED:
if self.block_size() == 8:
return 'uint8_t'
if self.block_size() == 16:
return 'uint16_t'
if self.block_size() == 32:
return 'uint32_t'
else:
assert False
else:
return _get_datatype(self.channel_type(), self.channel_size())
def _get_datatype(type, size):
if type == FLOAT:
if size == 32:
return 'float'
elif size == 16:
return 'uint16_t'
else:
assert False
elif type == UNSIGNED:
if size <= 8:
return 'uint8_t'
elif size <= 16:
return 'uint16_t'
elif size <= 32:
return 'uint32_t'
else:
assert False
elif type == SIGNED:
if size <= 8:
return 'int8_t'
elif size <= 16:
return 'int16_t'
elif size <= 32:
return 'int32_t'
else:
assert False
else:
assert False
def _parse_channels(fields, layout, colorspace, swizzle):
channels = []
for field in fields:
if not field:
continue
type = field[0] if field[0] else 'x'
if field[1] == 'n':
norm = True
size = int(field[2:])
else:
norm = False
size = int(field[1:])
channel = Channel(type, norm, size)
channels.append(channel)
return channels
def parse(filename):
"""Parse a format description in CSV format.
This function parses the given CSV file and returns an iterable of
channels."""
with open(filename) as stream:
for line in stream:
try:
comment = line.index('#')
except ValueError:
pass
else:
line = line[:comment]
line = line.strip()
if not line:
continue
fields = [field.strip() for field in line.split(',')]
name = fields[0]
layout = fields[1]
block_width = int(fields[2])
block_height = int(fields[3])
block_depth = int(fields[4])
colorspace = fields[10]
try:
swizzle = Swizzle(fields[9])
except:
sys.exit("error parsing swizzle for format " + name)
channels = _parse_channels(fields[5:9], layout, colorspace, swizzle)
yield Format(name, layout, block_width, block_height, block_depth, channels, swizzle, colorspace)