from __future__ import annotations
__all__ = [
'BusinessCardDisplayDefinition',
'FieldInfo',
]
import logging
import struct
from typing import List, Optional, Tuple
from ._helpers import BytesReader
from .. import constants
from ..enums import (
BCImageAlignment, BCImageSource, BCLabelFormat, BCTemplateID,
BCTextFormat
)
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
class BusinessCardDisplayDefinition:
"""
Data structure for PidLidBusinessCardDisplayDefinition.
Contains information used to contruct a business card for a contact.
"""
def __init__(self, data: bytes):
reader = BytesReader(data)
unpacked = constants.st.ST_BC_HEAD.unpack(reader.read(13))
# Because doc says it must be ignored, we don't check the reserved here.
reader.read(4)
self.__majorVersion = unpacked[0]
if self.__majorVersion < 3:
raise ValueError('Major version was less than 3.')
self.__minorVersion = unpacked[1]
self.__templateID = BCTemplateID(unpacked[2])
countOfFields = unpacked[3]
if unpacked[4] != 16:
raise ValueError('Value of FieldInfoSize was not 16.')
extraInfoSize = unpacked[5]
self.__imageAlignment = BCImageAlignment(unpacked[6])
self.__imageSource = BCImageSource(min(unpacked[7], 1))
self.__backgroundColor = unpacked[8:11]
self.__imageArea = unpacked[11]
extraInfoField = data[17 + 16 * countOfFields:]
extraInfoField = data[:extraInfoSize]
self.__fields = [FieldInfo(reader.read(16), extraInfoField)
for _ in range(countOfFields)]
def __bytes__(self) -> bytes:
return self.toBytes()
def toBytes(self) -> bytes:
# Pregenerate the extraInfo field.
offsets = []
extraInfo = b''
for info in self.__fields:
if info.labelText:
offsets.append(len(extraInfo))
extraInfo += info.labelText.encode('utf-16-le') + b'\x00\x00'
else:
offsets.append(0xFFFE)
if len(extraInfo) > 255:
raise ValueError('Label data can only be 127 characters total, including null characters.')
# Now that we have what we need, pack the header.
ret = constants.st.ST_BC_HEAD.pack(
self.__majorVersion,
self.__minorVersion,
self.__templateID,
len(self.__fields),
16,
len(extraInfo),
self.__imageAlignment,
self.__imageSource,
*self.__backgroundColor,
self.__imageArea
)
# Add the reserved.
ret += b'\x00\x00\x00\x00'
# Add each FieldInfo structure.
for index, info in enumerate(self.__fields):
ret += info.toBytes(offsets[index])
return ret + extraInfo
@property
def backgroundColor(self) -> Tuple[int, int, int]:
"""
A tuple of the RGB value of the color of the background.
"""
return self.__backgroundColor
@backgroundColor.setter
def backgroundColor(self, value: Tuple[int, int, int]) -> None:
if not isinstance(value, tuple) or len(value) != 3:
raise TypeError(':property backgroundColor: MUST be a tuple of 3 ints.')
# Quickly try to pack the ints and raise a value error if that fails.
try:
constants.st.ST_RGB(*value)
except struct.error:
raise ValueError('Value for :property backgroundColor: not in range.')
self.__backgroundColor = value
@property
def fields(self) -> List[FieldInfo]:
"""
The field info structures.
"""
return self.__fields
@property
def imageAlignment(self) -> BCImageAlignment:
"""
The alignment of the image within the image area.
Ignored if card is text only.
"""
return self.__imageAlignment
@imageAlignment.setter
def imageAlignment(self, value: BCImageAlignment) -> None:
if not isinstance(value, BCImageAlignment):
raise TypeError(':property imageAlignment: MUST be an instance of BCImageAlignment.')
self.__imageAlignment = value
@property
def imageArea(self) -> int:
"""
An integer that specified the percent of space that the image will
occupy on the business card.
Should be between 4 and 50.
"""
return self.__imageArea
@imageArea.setter
def imageArea(self, value: int) -> None:
if not isinstance(value, int):
raise TypeError(':property imageArea: MUST be an int.')
if value < 0:
raise ValueError(':property imageArea: MUST be positive.')
if value > 0xFF:
raise ValueError(':property imageArea: MUST be less than 0x100.')
if value < 4 or value > 50:
logger.warning(f'Business card image area was set to a value outside of suggested range of [4, 50] (got {value}).')
self.__imageArea = value
@property
def imageSource(self) -> BCImageSource:
"""
The source of the image.
"""
return self.__imageSource
@imageSource.setter
def imageSource(self, value: BCImageSource) -> None:
if not isinstance(value, BCImageSource):
raise TypeError(':property imageSource: MUST be an instance of BCImageSource.')
self.__imageSource = value
@property
def majorVersion(self) -> int:
"""
An 8-bit value that specified the major version number.
Must be 3 or greater.
"""
return self.__majorVersion
@majorVersion.setter
def majorVersion(self, value: int) -> None:
if not isinstance(value, int):
raise TypeError(':property majorVersion: MUST be an int.')
if value < 3:
raise ValueError(':property majorVersion: MUST be 3 or greater.')
if value > 0xFF:
raise ValueError(':property majorVersion: MUST be less than 0x100.')
self.__majorVersion = value
@property
def minorVersion(self) -> int:
"""
An 8-bit value that specifies the minor version number.
SHOULD be set to 0.
"""
return self.__minorVersion
@minorVersion.setter
def minorVersion(self, value: int) -> None:
if not isinstance(value, int):
raise TypeError(':property minorVersion: MUST be an int.')
if value < 0:
raise ValueError(':property minorVersion: MUST be positive.')
if value > 0xFF:
raise ValueError(':property minorVersion: MUST be less than 0x100.')
if value != 0:
logger.warning('Business card minor version set to non-zero value.')
self.__minorVersion = value
@property
def templateID(self) -> BCTemplateID:
"""
The layout of the business card.
"""
return self.__templateID
@templateID.setter
def templateID(self, value: BCTemplateID):
if not isinstance(value, BCTemplateID):
raise TypeError(':property templateID: MUST be an instance of BCTemplateID.')
self.__templateID = value
class FieldInfo:
def __init__(self, data: Optional[bytes] = None, extraInfo: Optional[bytes] = None):
if not data:
self.__textPropertyID = 0
self.__textFormat = BCTextFormat.DEFAULT
self.__labelFormat = BCLabelFormat.NO_LABEL
self.__fontSize = 0
self.__labelText = None
self.__valueFontColor = (0, 0, 0)
self.__labelFontColor = (0, 0, 0)
return
if extraInfo is None:
raise ValueError(':param extraInfo: MUST NOT be None if data is provided.')
unpacked = constants.st.ST_BC_FIELD_INFO.unpack(data)
self.__textPropertyID = unpacked[0]
self.__textFormat = BCTextFormat(unpacked[1])
self.__labelFormat = BCLabelFormat(unpacked[2])
self.__fontSize = unpacked[3]
self.__labelText = None if unpacked[4] == 0xFFFE else BytesReader(extraInfo[unpacked[4]:]).readUtf16String()
self.__valueFontColor = unpacked[5:8]
self.__labelFontColor = unpacked[8:11]
def toBytes(self, offset: int) -> bytes:
"""
Converts to bytes using the offset into the ExtraInfo field.
:raise ValueError: The offset was out of range.
"""
if offset < 0 or offset > 0xFFFF:
raise ValueError('Offset is out of range.')
if not self.__labelText:
offset = 0xFFFE
return constants.st.ST_BC_FIELD_INFO.pack(
self.__textPropertyID,
self.__textFormat,
self.__labelFormat,
self.__fontSize,
offset,
*self.__valueFontColor,
*self.__labelFontColor
)
@property
def fontSize(self) -> int:
"""
An integer that specifies the font size, in points, of the text field.
MUST be between 3 and 32, or MUST be 0 if the text field is displayed as
an empty line.
"""
return self.__fontSize
@fontSize.setter
def fontSize(self, value: int) -> None:
if not isinstance(value, int):
raise TypeError(':property fontSize: MUST be an int.')
# Unsure if this is meant to be inclusive or exclusive.
if value == 0 or 3 <= value <= 32:
self.__fontSize = value
else:
raise ValueError(':property fontSize: MUST be 0 or between 3 and 32, inclusive.')
@property
def labelFontColor(self) -> Tuple[int, int, int]:
"""
A tuple of the RGB value of the color of the label.
Each channel is a number in range [0, 256).
"""
return self.__labelFontColor
@labelFontColor.setter
def labelFontColor(self, value: Tuple[int, int, int]) -> None:
if not isinstance(value, tuple) or len(value) != 3:
raise TypeError(':property labelFontColor: MUST be a tuple of 3 ints.')
# Quickly try to pack the ints and raise a value error if that fails.
try:
constants.st.ST_RGB(*value)
except struct.error:
raise ValueError('Value for :property labelFontColor: not in range.')
self.__labelFontColor = value
@property
def labelFormat(self) -> BCLabelFormat:
"""
The format to use for the label.
"""
return self.__labelFormat
@labelFormat.setter
def labelFormat(self, value: BCLabelFormat) -> None:
if not isinstance(value, BCLabelFormat):
raise TypeError(':property labelFormat: MUST be an instance of BCLabelFormat.')
# Check mutually exclusive flags.
if BCLabelFormat.ALIGN_LEFT in value and BCLabelFormat.ALIGN_RIGHT in value:
raise ValueError('BCLabelFormat.ALIGN_LEFT and BCLabelFormat.ALIGN_RIGHT are mutually exclusive.')
if value and BCLabelFormat.ALIGN_LEFT not in value and BCLabelFormat.ALIGN_RIGHT not in value:
raise ValueError(':property labelFormat: MUST have the ALIGN_LEFT or ALIGN_RIGHT bit set if any other bits are set.')
if BCLabelFormat > 0x110:
raise ValueError('Unknown bits set for :property labelFormat:.')
self.__textPropertyID = value
@property
def labelText(self) -> Optional[str]:
"""
The text of the label, if it exists.
"""
return self.__labelText
@labelText.setter
def labelText(self, value: Optional[str]) -> None:
if not value:
self.__labelText = None
elif not isinstance(value, str):
raise TypeError(':property labelText: MUST be a str or None.')
self.__labelText = value
@property
def textFormat(self) -> BCTextFormat:
"""
An enum value representing the formatting to use for the text.
"""
return self.__textFormat
@textFormat.setter
def textFormat(self, value: BCTextFormat) -> None:
if not isinstance(value, BCTextFormat):
raise TypeError(':property textFormat: MUST be an instance of BCTextFormat.')
# Check mutually exclusive flags.
if BCTextFormat.CENTER in value and BCTextFormat.RIGHT in value:
raise ValueError('BCTextFormat.RIGHT and BCTextFormat.CENTER are mutually exclusive.')
if BCTextFormat > 0x101111:
raise ValueError('Unknown bits set for :property textFormat:.')
self.__textPropertyID = value
@property
def textPropertyID(self) -> int:
"""
The property to be used for the text field.
If the value is 0, it represents an empty field.
"""
return self.__textPropertyID
@textPropertyID.setter
def textPropertyID(self, value: int) -> None:
if not isinstance(value, int):
raise TypeError(':property textPropertyID: MUST be an int.')
if value < 0:
raise ValueError(':property textPropertyID: MUST be positive.')
if value > 0xFFFF:
raise ValueError(':property textPropertyID: MUST NOT be greater than 0xFFFF.')
self.__textPropertyID = value
@property
def valueFontColor(self) -> Tuple[int, int, int]:
"""
A tuple of the RGB value of the color of the text field.
Each channel is a number in range [0, 256).
"""
return self.__valueFontColor
@valueFontColor.setter
def valueFontColor(self, value: Tuple[int, int, int]) -> None:
if not isinstance(value, tuple) or len(value) != 3:
raise TypeError(':property valueFontColor: MUST be a tuple of 3 ints.')
# Quickly try to pack the ints and raise a value error if that fails.
try:
constants.st.ST_RGB(*value)
except struct.error:
raise ValueError('Value for :property valueFontColor: not in range.')
self.__valueFontColor = value