"""The module defines :class:`.ImageCache`, a utility class used to store image and color data and
all other utility methods needed to create, load and store thumbnail images.
Use the high-level :func:`get_thumbnail` to get an item's thumbnail. This relies on the
:class:`ImageCache` to retrieve data and will return an existing thumbnail image or a
suitable placeholder image. See :func:`get_placeholder_path`.
Note:
The thumbnail files are stored in the bookmark item cache folder (see
``common.bookmark_cache_dir``).
Under the hood, :func:`get_thumbnail` uses :meth:`ImageCache.get_pixmap` and
:meth:`ImageCache.get_image`.
We're using OpenImageIO to generate thumbnails.
To load an image using OpenImageIO as a QtGui.QImage see :func:`oiio_get_qimage`.
To load gui resources, use :meth:`rsc_pixmap`.
"""
import functools
import os
import time
import OpenImageIO
import pyimageutil
from PySide2 import QtWidgets, QtGui, QtCore
from . import common
from . import log
#: The list of image formats QT is configured to read.
QT_IMAGE_FORMATS = {f.data().decode('utf8')
for f in QtGui.QImageReader.supportedImageFormats()}
BufferType = QtCore.Qt.UserRole
PixmapType = BufferType + 1
ImageType = PixmapType + 1
IconType = ImageType + 1
ResourcePixmapType = IconType + 1
ColorType = ResourcePixmapType + 1
accepted_codecs = ('h.264', 'h264', 'mpeg-4', 'mpeg4')
def get_cache_size():
v = common.get_py_obj_size(common.oiio_cache)
v += common.get_py_obj_size(common.image_resource_data)
v += common.get_py_obj_size(common.image_cache)
return v
[docs]def init_image_cache():
"""Initialises the OpenImageIO and our own internal image cache.
"""
common.image_resource_list = {
common.GuiResource: [],
common.ThumbnailResource: [],
common.FormatResource: [],
}
common.image_resource_data = {}
common.image_cache = {
BufferType: {},
PixmapType: {},
ImageType: {},
IconType: {},
ResourcePixmapType: {},
ColorType: {},
}
[docs]def init_resources():
"""Initialises the GUI image resources.
"""
for _source, k in ((common.rsc(f), f) for f in
(common.GuiResource, common.ThumbnailResource,
common.FormatResource)):
for _entry in os.scandir(_source):
common.image_resource_list[k].append(_entry.name.split('.', maxsplit=1)[0])
[docs]def init_pixel_ratio():
"""Initialises the pixel ratio value.
"""
app = QtWidgets.QApplication.instance()
if not app:
log.error(
'`init_pixel_ratio()` was called before a QApplication was created.')
if app and common.pixel_ratio is None:
common.pixel_ratio = app.primaryScreen().devicePixelRatio()
else:
common.pixel_ratio = 1.0
[docs]def get_thumbnail(server, job, root, source, size=common.thumbnail_size,
fallback_thumb='placeholder',
get_path=False):
"""Get the thumbnail of a list item.
When an item is missing a bespoke cached thumbnail file, we will try to load
a fallback image instead. For files, this will be an image associated with
the file-format, or for asset and bookmark items, we will look in
bookmark's, then the job's root folder to see if we can find a
`thumbnail.png` file. When all lookup fails we'll return the provided
`fallback_thumb`.
See also :func:`get_cached_thumbnail_path()` for a lower level method used
to find a cached image file.
Args:
server (str): `server` path segment.
job (str): `job` path segment.
root (str): `root` path segment.
source (str): Full file path of source item.
size (int): The size of the thumbnail image in pixels.
fallback_thumb(str): A fallback thumbnail image.
get_path (bool): Returns a path instead of a QPixmap if set to `True`.
Returns:
tuple: `(QPixmap, QColor)`, or `(None, None)`.
str: Path to the thumbnail file if `get_path=True`.
"""
if not all((server, job, root, source)):
if get_path:
return None
return (None, None)
def _get(server, job, root, source, proxy):
path = get_cached_thumbnail_path(
server, job, root, source, proxy=proxy)
pixmap = ImageCache.get_pixmap(path, size)
if not pixmap or pixmap.isNull():
return (path, None, None)
color = ImageCache.get_color(path)
if not color:
return (path, pixmap, None)
return (path, pixmap, color)
size = int(round(size * common.pixel_ratio))
args = (server, job, root)
# In the simplest of all cases, the source has a bespoke thumbnail saved we
# can return outright.
thumbnail_path, pixmap, color = _get(server, job, root, source, False)
if pixmap and not pixmap.isNull() and get_path:
return thumbnail_path
if pixmap and not pixmap.isNull():
return (pixmap, color)
# If this item is an un-collapsed sequence item, the sequence
# might have a thumbnail instead.
thumbnail_path, pixmap, color = _get(server, job, root, source, True)
if pixmap and not pixmap.isNull() and get_path:
return thumbnail_path
if pixmap and not pixmap.isNull():
return (pixmap, color)
# If the item refers to a folder, e.g. an asset or a bookmark item, we'll
# check for a 'thumbnail.{ext}' file in the folder's root and if this fails,
# we will check the job folder. If both fails will we proceed to load a
# placeholder thumbnail.
if common.is_dir(source):
_hash = common.get_hash(source)
n = 3
while n >= 1:
thumb_path = f'{"/".join(args[0:n])}/thumbnail.{common.thumbnail_format}'
pixmap = ImageCache.get_pixmap(
thumb_path,
size,
hash=_hash
)
if pixmap and get_path:
return thumb_path
if pixmap:
color = ImageCache.get_color(
thumb_path,
hash=_hash,
)
return pixmap, color
n -= 1
# Let's load a placeholder if there's no generated thumbnail or
# thumbnail file present in the source's root.
thumb_path = get_placeholder_path(source, fallback_thumb)
pixmap = ImageCache.get_pixmap(thumb_path, size)
if pixmap and not pixmap.isNull() and get_path:
return thumb_path
if pixmap and not pixmap.isNull():
return pixmap, None
# In theory, we will never get here as get_placeholder_path should always
# return a valid pixmap
if get_path:
return None
return (None, None)
[docs]def wait_for_lock(source, timeout=1.0):
"""Waits for a maximum amount of time when a lock file is present.
Args:
source (str): A file path.
timeout (float): The maximum amount of time to wait in seconds.
"""
t = 0.0
while t <= timeout:
if not os.path.isfile(f'{source}.lock'):
break
time.sleep(0.1)
t += 0.1
[docs]@functools.lru_cache(maxsize=4194304)
def get_oiio_extensions():
"""Returns a list of extensions accepted by OpenImageIO.
"""
v = OpenImageIO.get_string_attribute('extension_list')
extensions = []
for e in [f.split(':')[1] for f in v.split(';')]:
extensions += e.split(',')
return set(sorted(extensions))
[docs]@functools.lru_cache(maxsize=4194304)
def get_oiio_namefilters():
"""Gets all accepted formats from the oiio build as a namefilter list.
Use the return value on the QFileDialog.setNameFilters() method.
"""
extension_list = OpenImageIO.get_string_attribute('extension_list')
namefilters = []
arr = []
for exts in extension_list.split(';'):
exts = exts.split(':')
_exts = exts[1].split(',')
e = ['*.{}'.format(f) for f in _exts]
namefilter = '{} files ({})'.format(exts[0].upper(), ' '.join(e))
namefilters.append(namefilter)
for _e in _exts:
arr.append(_e)
allfiles = ['*.{}'.format(f) for f in arr]
allfiles = ' '.join(allfiles)
allfiles = 'All files ({})'.format(allfiles)
namefilters.insert(0, allfiles)
return ';;'.join(namefilters)
[docs]@common.error
@common.debug
def create_thumbnail_from_image(server, job, root, source, image, proxy=False):
"""Creates a thumbnail from a given image file and saves it as the source
file's thumbnail image.
The ``server``, ``job``, ``root``, ``source`` arguments refer to a file we
want to create a new thumbnail for. The ``image`` argument should be a path to
an image file that will be converted using `pyimageutil.make_thumbnail()` to a
thumbnail image and saved to our image cache and disk to represent ``source``.
Args:
server (str): `server` path segment.
job (str): `job` path segment.
root (str): `root` path segment.
source (str): The full file path.
image (str): Path to an image file to use as a thumbnail for ``source``.
proxy (bool, optional): Specify if the source is an image sequence and if
the proxy path should be used to save the thumbnail instead.
"""
thumbnail_path = get_cached_thumbnail_path(
server, job, root, source, proxy=proxy)
if QtCore.QFileInfo(thumbnail_path).exists():
if not QtCore.QFile(thumbnail_path).remove():
s = 'Failed to remove existing thumbnail file.'
raise RuntimeError(s)
res = pyimageutil.convert_image(
image,
thumbnail_path,
max_size=int(common.thumbnail_size)
)
if not res:
raise RuntimeError('Failed to make thumbnail.')
ImageCache.flush(thumbnail_path)
[docs]@functools.lru_cache(maxsize=4194304)
def get_cached_thumbnail_path(server, job, root, source, proxy=False):
"""Returns the path to a cached thumbnail file.
When `proxy` is set to `True` or the source file is a sequence, we will use
the sequence's first item as our thumbnail source.
Args:
server (str): `server` path segment.
job (str): `job` path segment.
root (str): `root` path segment.
source (str): The full file path.
Returns:
str: The resolved thumbnail path.
"""
for arg in (server, job, root, source):
common.check_type(arg, str)
if proxy or common.is_collapsed(source):
source = common.proxy_path(source)
name = common.get_hash(source) + '.' + common.thumbnail_format
return f'{server}/{job}/{root}/{common.bookmark_cache_dir}/thumbnails/{name}'
[docs]@functools.lru_cache(maxsize=4194304)
def get_placeholder_path(file_path, fallback):
"""Returns an image path used to represent an item.
In absence of a custom user, or generated thumbnail, we'll try and find one based on
the file's format extension.
Args:
file_path (str): Path to a file or folder.
fallback (str): An image to use if no suitable placeholder is found.
Returns:
str: Path to the placeholder image.
"""
common.check_type(file_path, str)
def _path(r, n):
return common.rsc(f'{r}/{n}.{common.thumbnail_format}')
file_info = QtCore.QFileInfo(file_path)
suffix = file_info.suffix().lower()
if suffix in common.image_resource_list[common.FormatResource]:
path = _path(common.FormatResource, suffix)
return os.path.normpath(path)
if fallback in common.image_resource_list[common.FormatResource]:
path = _path(common.FormatResource, fallback)
elif fallback in common.image_resource_list[common.ThumbnailResource]:
path = _path(common.ThumbnailResource, fallback)
elif fallback in common.image_resource_list[common.GuiResource]:
path = _path(common.GuiResource, fallback)
else:
path = _path(common.GuiResource, 'placeholder')
return os.path.normpath(path)
[docs]def oiio_get_buf(source, hash=None, force=False, subimage=0):
"""Checks and loads a source image with OpenImageIO's format reader.
Args:
source (str): Path to an OpenImageIO compatible image file.
hash (str): Specify the hash manually, otherwise will be generated.
force (bool): When `true`, forces the buffer to be re-cached.
Returns:
ImageBuf: An `ImageBuf` instance or `None` if the image cannot be read.
"""
common.check_type(source, str)
if hash is None:
hash = common.get_hash(source)
if not force and ImageCache.contains(hash, BufferType):
return ImageCache.value(hash, BufferType)
# We use the extension to initiate an ImageInput with a format
# which in turn is used to check the source's validity
if '.' not in source:
return None
ext = source.split('.')[-1].lower()
if ext not in get_oiio_extensions():
return None
i = OpenImageIO.ImageInput.create(ext)
if not i or not i.valid_file(source):
i.close()
return OpenImageIO.ImageBuf()
# If all went well, we can initiate an ImageBuf
config = OpenImageIO.ImageSpec()
config.format = OpenImageIO.TypeDesc(OpenImageIO.FLOAT)
buf = OpenImageIO.ImageBuf()
buf.reset(source, subimage, 0, config=config)
if buf.has_error:
return OpenImageIO.ImageBuf()
ImageCache.setValue(hash, buf, BufferType)
return buf
[docs]def oiio_get_qimage(source, buf=None, force=True):
"""Load the pixel data using OpenImageIO and return it as a
`RGBA8888` / `RGB888` QImage.
Args:
source (str): Path to an OpenImageIO readable image.
buf (OpenImageIO.ImageBuf): When buf is valid ImageBuf instance it will be used
as the source instead of `source`. Defaults to `None`.
Returns:
QImage: An QImage instance or `None` if the image/source is invalid.
"""
if buf is None:
buf = oiio_get_buf(source, force=force)
if buf is None:
return None
spec = buf.spec()
if not int(spec.nchannels):
return None
if int(spec.nchannels) < 3:
b = OpenImageIO.ImageBufAlgo.channels(
buf,
(spec.channelnames[0], spec.channelnames[0], spec.channelnames[0]),
('R', 'G', 'B')
)
elif int(spec.nchannels) > 4:
if spec.channelindex('A') > -1:
b = OpenImageIO.ImageBufAlgo.channels(
buf, ('R', 'G', 'B', 'A'), ('R', 'G', 'B', 'A'))
else:
b = OpenImageIO.ImageBufAlgo.channels(
buf, ('R', 'G', 'B'), ('R', 'G', 'B'))
np_arr = buf.get_pixels(OpenImageIO.UINT8)
if np_arr.shape[2] == 1:
_format = QtGui.QImage.Format_Grayscale8
if np_arr.shape[2] == 2:
_format = QtGui.QImage.Format_Invalid
elif np_arr.shape[2] == 3:
_format = QtGui.QImage.Format_RGB888
elif np_arr.shape[2] == 4:
_format = QtGui.QImage.Format_RGBA8888
elif np_arr.shape[2] > 4:
_format = QtGui.QImage.Format_Invalid
image = QtGui.QImage(
np_arr,
spec.width,
spec.height,
spec.width * spec.nchannels, # scanlines
_format
)
image.setDevicePixelRatio(common.pixel_ratio)
# As soon as the numpy array is garbage collected, the data for QImage becomes
# unusable and Qt5 crashes. This could possibly be a bug, I would expect,
# the data to be copied automatically, but by making a copy
# the numpy array can safely be GC'd
return image.copy()
[docs]def make_color(source, hash=None):
"""Calculate the average color of a source image.
Args:
source (str): Path to an image file.
hash (str, optional): Has value to use instead of source image's hash.
Returns:
QtGui.QImage: The average color of the source image.
"""
buf = oiio_get_buf(source)
if not buf:
return None
if hash is None:
hash = common.get_hash(source)
stats = OpenImageIO.ImageBufAlgo.computePixelStats(buf)
if not stats:
return None
if stats.avg and len(stats.avg) > 3:
color = QtGui.QColor(
int(stats.avg[0] * 255),
int(stats.avg[1] * 255),
int(stats.avg[2] * 255),
a=240
# a=int(stats.avg[3] * 255)
)
elif stats.avg and len(stats.avg) == 3:
color = QtGui.QColor(
int(stats.avg[0] * 255),
int(stats.avg[1] * 255),
int(stats.avg[2] * 255),
)
elif stats.avg and len(stats.avg) < 3:
color = QtGui.QColor(
int(stats.avg[0] * 255),
int(stats.avg[0] * 255),
int(stats.avg[0] * 255),
)
else:
return None
ImageCache.setValue(hash, color, ColorType)
return color
[docs]def resize_image(image, size):
"""Returns a scaled copy of the image that fits in size.
Args:
image (QImage): The image to rescale.
size (int): The size of the square to fit.
Returns:
QImage: The resized copy of the original image.
"""
common.check_type(size, (int, float))
common.check_type(image, QtGui.QImage)
w = image.width()
h = image.height()
factor = float(size) / max(w, h)
w *= factor
h *= factor
return image.smoothScaled(round(w), round(h))
[docs]def rsc_pixmap(name, color, size, opacity=1.0, resource=common.GuiResource,
get_path=False, oiio=False):
"""Loads an image resource and returns it as a resized (and recolored) QPixmap.
Args:
name (str): Name of the resource without the extension.
color (QColor or None): The color of the icon.
size (int or None): The size of pixmap.
opacity (float): Sets the opacity of the returned pixmap.
resource (str): Optional resource type. Default: common.GuiResource.
get_path (bool): Returns a path when True.
Returns:
A QPixmap of the requested resource, or a str path if ``get_path`` is True.
None if the resource could not be found.
"""
common.check_type(name, str)
common.check_type(color, (None, QtGui.QColor))
common.check_type(size, (None, float, int))
common.check_type(opacity, float)
source = common.rsc(f'{resource}/{name}.{common.thumbnail_format}')
if get_path:
file_info = QtCore.QFileInfo(source)
return file_info.absoluteFilePath()
size = size if isinstance(size, (float, int)) else -1
_color = color.name() if isinstance(color, QtGui.QColor) else 'null'
k = 'rsc:' + name + ':' + str(int(size)) + ':' + _color
if k in common.image_resource_data:
return common.image_resource_data[k]
image = ImageCache.get_image(source, size, oiio=oiio)
if image.isNull():
return QtGui.QPixmap()
# Do a re-color pass on the source image
if color is not None:
painter = QtGui.QPainter()
painter.begin(image)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, True)
painter.setCompositionMode(QtGui.QPainter.CompositionMode_SourceIn)
painter.setBrush(QtGui.QBrush(color))
painter.drawRect(image.rect())
painter.end()
# Setting transparency
if opacity < 1.0:
_image = QtGui.QImage(image)
_image.setDevicePixelRatio(common.pixel_ratio)
_image.fill(QtCore.Qt.transparent)
painter = QtGui.QPainter()
painter.begin(_image)
painter.setOpacity(opacity)
painter.drawImage(0, 0, image)
painter.end()
image = _image
# Finally, we'll convert the image to a pixmap
pixmap = QtGui.QPixmap()
pixmap.setDevicePixelRatio(common.pixel_ratio)
pixmap.convertFromImage(image, flags=QtCore.Qt.ColorOnly)
common.image_resource_data[k] = pixmap
return common.image_resource_data[k]
[docs]class ImageCache(QtCore.QObject):
"""Utility class for storing, and accessing image data.
You shouldn't have to use the :meth:`value` and :meth:`setValue` methods. Instead,
use the :meth:`get_image` and :meth:`get_pixmap` - they're high-level functions, that
will automatically cache and convert resources.
The stored data is associated with a `type` and `hash` (see
:func:`~bookmarks.common.core.get_hash`) and `size`. This means a single source image can have
multiple QPixmap and QImage cache entries as various sizes are requested and cached.
.. code-block:: python
:linenos:
common.image_cache[cache_type][hash][size] = pixmap
To remove a resource from the cache use the :meth:`flush` method.
"""
lock = QtCore.QMutex()
[docs] @classmethod
def flush(cls, source):
"""Flushes all values associated with a given source from the image cache.
Args:
source (str): A file path.
"""
hash = common.get_hash(source)
for k in common.image_cache:
if hash in common.image_cache[k]:
del common.image_cache[k][hash]
[docs] @classmethod
def get_pixmap(cls, source, size, hash=None, force=False, oiio=False):
"""Loads, resizes `source` as a QPixmap and stores it for later use.
When size is '-1' the full image will be loaded without resizing.
The resource will be stored as a QPixmap instance and saved to
:attr:`common.image_cache[PixmapType][hash]`. The hash value is generated using
``source`` but this can be overwritten by explicitly setting ``hash``.
Note:
It is not possible to call this method outside the main gui thread.
Use :meth:`get_image` instead. This method is backed by :meth:`get_image`
anyway.
Args:
source (str): Path to an image file.
size (int): The size of the requested image.
hash (str): Use this hash key instead of a source's hash value to store the data.
force (bool): Force reloads the pixmap.
oiio (bool): Use OpenImageIO to load the image, instead of Qt.
Returns:
QPixmap: The loaded and resized QPixmap, or `None`.
"""
if not QtGui.QGuiApplication.instance():
raise RuntimeError(
'Cannot create QPixmaps without a gui application.')
common.check_type(source, str)
app = QtWidgets.QApplication.instance()
if app and app.thread() != QtCore.QThread.currentThread():
s = 'Pixmaps can only be initiated in the main gui thread.'
raise RuntimeError(s)
if isinstance(size, float):
size = int(round(size))
if size == -1:
buf = oiio_get_buf(source)
if not buf:
return None
spec = buf.spec()
size = max((spec.width, spec.height))
# Check the cache and return the previously stored value if exists
if hash is None:
hash = common.get_hash(source)
contains = cls.contains(hash, PixmapType)
if not force and contains:
data = cls.value(hash, PixmapType, size=size)
if data:
return data
# We'll load a cache a QImage to use as the basis for the QPixmap. This
# is because of how the thread affinity of QPixmaps don't permit use
# outside the main gui thread
image = cls.get_image(source, size, hash=hash, force=force, oiio=oiio)
if not image:
return None
pixmap = QtGui.QPixmap()
pixmap.setDevicePixelRatio(common.pixel_ratio)
pixmap.convertFromImage(image, flags=QtCore.Qt.ColorOnly)
if pixmap.isNull():
return None
cls.setValue(hash, pixmap, PixmapType, size=size)
return pixmap
[docs] @classmethod
def get_image(cls, source, size, hash=None, force=False, oiio=False):
"""Loads, resizes `source` as a QImage and stores it for later use.
When size is '-1' the full image will be loaded without resizing.
The resource will be stored as QImage instance at
`common.image_cache[ImageType][hash]`. The hash value is generated by default
using `source`'s value but this can be overwritten by explicitly
setting `hash`.
Args:
source (str): Path to an OpenImageIO compliant image file.
size (int): The size of the requested image.
hash (str): Use this hash key instead source to store the data.
force (bool): Force reloads the image from source.
oiio (bool): Use OpenImageIO to load the image data.
Returns:
QImage: The loaded and resized QImage, or `None` if loading fails.
"""
common.check_type(source, str)
if isinstance(size, float):
size = int(round(size))
if size == -1:
buf = oiio_get_buf(source)
spec = buf.spec()
size = max((spec.width, spec.height))
if hash is None:
hash = common.get_hash(source)
# Check the cache and return the previously stored value
if not force and cls.contains(hash, ImageType):
data = cls.value(hash, ImageType, size=size)
if data:
return data
# If not yet stored, load and save the data
if size != -1:
buf = oiio_get_buf(source, hash=hash, force=force)
if not buf:
return None
if oiio:
image = oiio_get_qimage(source)
else:
image = QtGui.QImage(source)
image.setDevicePixelRatio(common.pixel_ratio)
if image.isNull():
return None
if size != -1:
image = resize_image(image, size)
if image.isNull():
return None
# ...and store
cls.setValue(hash, image, ImageType, size=size)
return image
[docs] @classmethod
def get_color(cls, source, hash=None, force=False):
"""Gets a cached QColor associated with the given source.
Args:
source (str): A file path.
force (bool): Force value recache.
hash (str): Hash value override.
"""
common.check_type(source, str)
# Check the cache and return the previously stored value if exists
if hash is None:
hash = common.get_hash(source)
if not cls.contains(hash, ColorType):
return make_color(source, hash=hash)
elif cls.contains(hash, ColorType) and not force:
return cls.value(hash, ColorType)
elif cls.contains(hash, ColorType) and force:
return make_color(source, hash=hash)
return None
[docs] @classmethod
def contains(cls, _hash, cache_type):
"""Checks if the given hash exists in the database."""
return _hash in common.image_cache[cache_type]
[docs] @classmethod
def value(cls, hash, cache_type, size=None):
"""Get a value from the ImageCache.
Args:
hash (str): A hash value generated by `common.get_hash`
"""
cls.lock.lock()
try:
if not cls.contains(hash, cache_type):
return None
if size is not None:
if size not in common.image_cache[cache_type][hash]:
return None
return common.image_cache[cache_type][hash][size]
return common.image_cache[cache_type][hash]
finally:
cls.lock.unlock()
[docs] @classmethod
def setValue(cls, hash, value, cache_type, size=None):
"""Sets a value in the ImageCache using `hash` and the `cache_type`.
If force is `True`, we will flush the sizes stored in the cache before
setting the new value. This only applies to Image- and PixmapTypes.
"""
cls.lock.lock()
try:
if not cls.contains(hash, cache_type):
common.image_cache[cache_type][hash] = {}
if cache_type == BufferType:
common.check_type(value, OpenImageIO.ImageBuf)
common.image_cache[BufferType][hash] = value
return common.image_cache[BufferType][hash]
elif cache_type == ImageType:
common.check_type(value, QtGui.QImage)
if size is None:
raise ValueError('Invalid size value.')
if not isinstance(size, int):
size = int(size)
common.image_cache[cache_type][hash][size] = value
return common.image_cache[cache_type][hash][size]
elif cache_type in (PixmapType, ResourcePixmapType):
common.check_type(value, QtGui.QPixmap)
if not isinstance(size, int):
size = int(size)
common.image_cache[cache_type][hash][size] = value
return common.image_cache[cache_type][hash][size]
elif cache_type == ColorType:
common.check_type(value, QtGui.QColor)
common.image_cache[ColorType][hash] = value
return common.image_cache[ColorType][hash]
raise TypeError('`cache_type` is invalid.')
finally:
cls.lock.unlock()