# -*- coding: utf-8 -*-
"""
Miscellaneous utilities.
"""
from __future__ import unicode_literals, absolute_import
from functools import wraps
import struct
import sys
from . import enums
from typing import Any, BinaryIO, Callable, List, Type, TYPE_CHECKING # NOQA
if TYPE_CHECKING:
import numpy as np # NOQA
DEBUG = False
[docs]def read_value(fd, fmt, endian='>'):
# type: (BinaryIO, unicode, unicode) -> Any
"""
Read a values from a file-like object.
Parameters
----------
fd : file-like object
Must be opened for reading, in binary mode.
fmt : str
A `struct` module `format character
<https://docs.python.org/2/library/struct.html#format-characters>`__
string.
endian : str
The endianness. Must be ``>`` or ``<``. Default: ``>``.
Returns
-------
value : any
The value(s) read from the file.
If a single value, it is returned alone. If multiple values,
a tuple is returned.
"""
fmt = endian + fmt
size = struct.calcsize(fmt) # type: ignore
result = struct.unpack(fmt, fd.read(size)) # type: ignore
if len(result) == 1:
return result[0]
else:
return result
[docs]def write_value(fd, fmt, *value, **kwargs):
"""
Write a single binary value to a file-like object.
Parameters
----------
fd : file-like object
Must be opened for writing, in binary mode.
fmt : str
A `struct` module `format character
<https://docs.python.org/2/library/struct.html#format-characters>`__
string.
value : any
The value to encode and write to the file.
endian : str
The endianness. Must be ``>`` or ``<``. Default: ``>``.
"""
endian = kwargs.get('endian', '>')
fmt = endian + fmt
fd.write(struct.pack(fmt, *value))
[docs]def pad(number, divisor):
# type: (int, int) -> int
"""
Pads an integer up to the given divisor.
"""
if number % divisor:
number = (number // divisor + 1) * divisor
return number
[docs]def read_pascal_string(fd, padding=1):
# type: (BinaryIO, int) -> unicode
"""
Read a UTF-8-encoded Pascal string from a file.
Parameters
----------
fd : file-like object
Must be opened for reading, seekable and in binary mode.
padding : int, optional
If provided, additional pad bytes will be read until
the total amount read is a multiple of padding.
Returns
-------
value : str
The unicode value of the string.
"""
length = read_value(fd, 'B')
if length == 0:
fd.seek(padding - 1, 1)
return ''
result = fd.read(length)
padded_length = pad(length + 1, padding) - 1
fd.seek(padded_length - length, 1)
return result.decode('utf8', 'replace')
[docs]def write_pascal_string(fd, value, padding=1):
# type: (BinaryIO, unicode, int) -> None
"""
Write a UTF-8-encoded Pascal string to a file.
Parameters
----------
fd : file-like object
Must be opened for writing and in binary mode.
value : str
A unicode string value.
padding : int, optional
If provided, additional pad bytes will be written until
the total amount written is a multiple of padding.
"""
value = value.encode('utf8')
length = len(value)
if length > 255:
value = value[:255]
length = 255
write_value(fd, 'B', len(value))
if len(value) == 0:
fd.write(b'\0' * (padding - 1))
return
fd.write(value)
padding = pad(length + 1, padding) - 1 - length
if padding != 0:
fd.write(b'\0' * padding)
[docs]def pascal_string_length(value, padding=1):
# type: (unicode, int) -> int
"""
Calculates the total length of writing a UTF-8-encoded Pascal
string to disk.
Parameters
----------
value : str
A unicode string value.
Returns
-------
length : int
The length, in bytes.
"""
value = value.encode('utf8')
if len(value) == 0:
return padding
length = len(value)
padding = pad(length + 1, padding) - 1 - length
return length + padding + 1
[docs]def decode_unicode_string(data):
# type: (bytes) -> unicode
"""
Decode Photoshop's definition of a `Unicode String
<https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#UnicodeStringDefine>`__.
"""
return data[4:].rstrip(b'\0').decode('utf_16_be')
[docs]def encode_unicode_string(s):
# type: (unicode) -> bytes
"""
Encode Photoshop's definition of a `Unicode String
<https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#UnicodeStringDefine>`__.
"""
return (struct.pack('>L', len(s) + 1) # type: ignore
+ s.encode('utf_16_be') + b'\0\0')
[docs]def read_unicode_string(fd):
# type: (BinaryIO) -> unicode
"""
Read a UTF-16-BE-encoded Unicode string (with length) from a file.
Parameters
----------
fd : file-like object
Must be opened for reading, seekable and in binary mode.
Returns
-------
value : str
The unicode value of the string.
"""
length = read_value(fd, 'L')
data = fd.read(length * 2)
return data.rstrip(b'\0').decode('utf_16_be')
[docs]def write_unicode_string(fd, value):
# type: (BinaryIO, unicode) -> None
"""
Write a UTF-16-BE-encoded Unicode string (with length) to a file.
Parameters
----------
fd : file-like object
Must be opened for writing and in binary mode.
value : str
A unicode string value.
"""
fd.write(encode_unicode_string(value))
[docs]def unicode_string_length(value):
# type: (unicode) -> int
"""
Calculates the total length of writing a UTF-16-BE-encoded Unicode
string (with length) to a file.
Parameters
----------
value : str
A unicode string value.
Returns
-------
length : int
The length, in bytes.
"""
return len(encode_unicode_string(value))
_indent = [0]
[docs]def trace_read(func): # pragma: no cover
"""
Prints debugging information from a read or write method.
For internal use only.
"""
@wraps(func)
def wrapper(self, fd, *args):
if isinstance(self, type):
name = self.__name__
else:
name = self.__class__.__name__
log('>>> {} @ {}', name, fd.tell())
_indent[0] += 1
result = func(self, fd, *args)
_indent[0] -= 1
log('<<< {} @ {}', name, fd.tell())
return result
if DEBUG:
return wrapper
else:
return func
trace_write = trace_read
[docs]def log(msg, *args): # pragma: no cover
"""
Print a logging message if debugging is turned on.
"""
if DEBUG:
print(" " * _indent[0], msg.format(*args))
[docs]def do_byteswap(arr):
# type: (np.ndarray) -> np.ndarray
"""
Return a copy of an array, byteswapped.
"""
return arr.byteswap().view(arr.dtype.newbyteorder('>'))
[docs]def ensure_bigendian(arr):
# type: (np.ndarray) -> np.ndarray
"""
Ensure that a Numpy array is in big-endian order.
Returns a copy if the endianness needed to be changed.
"""
if needs_byteswap(arr):
return do_byteswap(arr)
return arr
if sys.byteorder == 'little':
def needs_byteswap(arr):
# type: (np.ndarray) -> bool
"""
Returns True if the array needs to be byteswapped.
"""
order = arr.dtype.byteorder
return order in ('<', '=')
else:
[docs] def needs_byteswap(arr):
# type: (np.ndarray) -> bool
"""
Returns True if the array needs to be byteswapped.
"""
order = arr.dtype.byteorder
return order == '<'
[docs]def ensure_native_endian(arr):
# type: (np.ndarray) -> np.ndarray
"""
Ensure that a Numpy array is in native-endian order.
Returns a copy if the endianness needed to be changed.
"""
order = arr.dtype.byteorder
if order != '=':
return arr.byteswap().view(arr.dtype.newbyteorder('='))
return arr
[docs]def unpack_bitflags(value, nbits):
# type: (int, int) -> List[bool]
"""
Unpack a bitfield into its constituent parts.
"""
return [bool(value & (1 << i)) for i in range(nbits)]
[docs]def pack_bitflags(*values):
# type: (*bool) -> int
"""
Pack separate booleans back into a bit field.
"""
result = 0
for i, val in enumerate(values):
if val:
result |= (1 << i)
return result
[docs]def assert_is_list_of(value, cls, min=None, max=None):
# type: (Any, Type, int, int) -> None
"""
If value is not a list of cls instances, raises TypeError.
"""
if not isinstance(value, list):
raise TypeError("Must be list of {}".format(cls.__name__))
for item in value:
if not isinstance(item, cls):
raise TypeError("Must be list of {}".format(cls.__name__))
if ((min is not None and item < min) or
(max is not None and item > max)):
raise ValueError(
"All values must be in range {} to {}".format(min, max)
)
def _get_channel_id(color, color_mode):
if color not in enums.ColorChannelMapping:
raise ValueError("Unknown color '{}'".format(color))
exp_color_mode, channel_id = enums.ColorChannelMapping[color]
if exp_color_mode is not None and exp_color_mode != color_mode:
raise ValueError(
"Color '{!s}' is not valid for color mode '{!s}', "
"expected '{!s}'".format(
color, color_mode, exp_color_mode)
)
return channel_id
[docs]def get_channel(color, color_mode, channels):
channel_id = _get_channel_id(color, color_mode)
return channels[channel_id]
[docs]def set_channel(color, channel, color_mode, channels):
channel_id = _get_channel_id(color, color_mode)
channels[channel_id] = channel