# -*- coding: utf-8 -*-
"""
EngineData structure.
PSD file embeds text formatting data in its own markup language referred
EngineData. The format looks like the following::
<<
/EngineDict
<<
/Editor
<<
/Text (˛ˇMake a change and save.)
>>
>>
/Font
<<
/Name (˛ˇHelveticaNeue-Light)
/FillColor
<<
/Type 1
/Values [ 1.0 0.0 0.0 0.0 ]
>>
/StyleSheetSet [
<<
/Name (˛ˇNormal RGB)
>>
]
>>
>>
"""
from __future__ import absolute_import, unicode_literals
import attr
import codecs
import logging
import re
from enum import Enum
from psd_tools.psd.base import (
BooleanElement, DictElement, IntegerElement, ListElement, NumericElement,
ValueElement
)
from psd_tools.utils import new_registry, write_bytes
logger = logging.getLogger(__name__)
TOKEN_CLASSES, register = new_registry()
def compile_re(pattern):
return re.compile(pattern.encode('macroman'), re.S)
class EngineToken(Enum):
ARRAY_END = compile_re(r'^\]$')
ARRAY_START = compile_re(r'^\[$')
BOOLEAN = compile_re(r'^(true|false)$')
DICT_END = compile_re(r'^>>(\x00)*$') # Buggy one?
DICT_START = compile_re(r'^<<$')
NOOP = compile_re(r'^$')
NUMBER = compile_re(r'^-?\d+$')
NUMBER_WITH_DECIMAL = compile_re(r'^-?\d*\.\d+$')
PROPERTY = compile_re(r'^\/[a-zA-Z0-9_]+$')
STRING = compile_re(r'^\((\xfe\xff([^\)]|\\\))*)\)$')
# Unknown tags: b'(hwid)', b'(fwid)', b'(aalt)'
UNKNOWN_TAG = compile_re(r'^\([a-zA-Z0-9]*\)$')
UNKNOWN_TAG2 = compile_re(r'^--\(\.-0$')
class Tokenizer(object):
"""
Tokenize engine data.
Example::
tokenizer = Tokenizer(data)
for token, token_type in tokenizer:
print('%s: %r' % (token_type.name, token))
"""
DIVIDER = compile_re(r'[ \n\t]+')
UTF16_START = b'(\xfe\xff'
UTF16_END = compile_re(r'[^\\]\)')
def __init__(self, data):
self.data = data
self.index = 0
def __iter__(self):
return self
def __len__(self):
return len(self.data) - self.index
def next(self):
return self.__next__()
def __next__(self):
if len(self) == 0:
raise StopIteration
index = self.index
if self.data[index:].startswith(self.UTF16_START):
match = self.UTF16_END.search(self.data[index:])
if match is None:
raise ValueError('Invalid token: %r' % (self.data[index:]))
token = self.data[index:index + match.end()]
self.index += match.end()
else:
match = self.DIVIDER.search(self.data[index:])
if match is None:
token = self.data[index:]
self.index = len(self.data)
else:
token = self.data[index:index + match.start()]
self.index += match.end()
if token == b'':
return self.__next__()
for token_type in EngineToken:
if token_type.value.search(token):
return token, token_type
raise ValueError("Unknown token: %r" % (token))
[docs]@register(EngineToken.DICT_START)
class Dict(DictElement):
"""
Dict-like element.
"""
@classmethod
def read(cls, fp, **kwargs):
return cls.frombytes(fp.read())
@classmethod
def frombytes(cls, data, **kwargs):
tokenizer = data if isinstance(data, Tokenizer) else Tokenizer(data)
self = cls()
for k_token, k_token_type in tokenizer:
if k_token_type == EngineToken.PROPERTY:
key = Property.frombytes(k_token)
v_token, v_token_type = next(tokenizer)
kls = TOKEN_CLASSES.get(v_token_type)
if v_token_type in (
EngineToken.ARRAY_START, EngineToken.DICT_START
):
value = kls.frombytes(tokenizer)
elif kls:
value = kls.frombytes(v_token)
else:
raise ValueError('Invalid token: %r' % (v_token))
self[key] = value
elif k_token_type == EngineToken.DICT_END:
return self
return self
def write(self, fp, indent=0, write_container=True):
inner_indent = indent if indent is None else indent + 1
written = 0
if write_container:
if indent == 0:
written += self._write_newline(fp, indent)
written += self._write_newline(fp, indent)
written += self._write_indent(fp, indent)
written += write_bytes(fp, b'<<')
written += self._write_newline(fp, indent)
for key in self:
written += self._write_indent(fp, inner_indent)
written += key.write(fp)
value = self[key]
if isinstance(value, Dict):
written += value.write(fp, indent=inner_indent)
else:
written += write_bytes(fp, b' ')
if isinstance(value, List):
if len(value) > 0 and isinstance(value[0], Dict):
written += value.write(fp, indent=inner_indent)
else:
written += value.write(fp, indent=None)
else:
written += value.write(fp)
written += self._write_newline(fp, indent)
if write_container:
written += self._write_indent(fp, indent)
written += write_bytes(fp, b'>>')
return written
def _write_indent(self, fp, indent, default=b' '):
if indent is None:
return write_bytes(fp, default)
return write_bytes(fp, b'\t' * (indent))
def _write_newline(self, fp, indent):
if indent is None:
return 0
return write_bytes(fp, b'\n')
def __getitem__(self, key):
key = key if isinstance(key, Property) else Property(key)
return super(Dict, self).__getitem__(key)
def __setitem__(self, key, value):
key = key if isinstance(key, Property) else Property(key)
super(Dict, self).__setitem__(key, value)
def __detitem__(self, key):
key = key if isinstance(key, Property) else Property(key)
super(Dict, self).__delitem__(key)
def __contains__(self, key):
key = key if isinstance(key, Property) else Property(key)
return self._items.__contains__(key)
def get(self, key, *args):
key = key if isinstance(key, Property) else Property(key)
return super(Dict, self).get(key, *args)
[docs]class EngineData(Dict):
"""
Dict-like element.
TYPE_TOOL_OBJECT_SETTING tagged block contains this object in its
descriptor.
"""
pass
[docs]class EngineData2(Dict):
"""
Dict-like element.
TEXT_ENGINE_DATA tagged block has this object.
"""
def write(self, fp, indent=None, write_container=False, **kwargs):
return super(EngineData2, self).write(
fp, indent=indent, write_container=write_container
)
[docs]@register(EngineToken.ARRAY_START)
class List(ListElement):
"""
List-like element.
"""
@classmethod
def read(cls, fp):
return cls.frombytes(fp.read())
@classmethod
def frombytes(cls, data):
tokenizer = data if isinstance(data, Tokenizer) else Tokenizer(data)
self = cls()
for token, token_type in tokenizer:
if token_type == EngineToken.ARRAY_END:
return self
kls = TOKEN_CLASSES.get(token_type)
if token_type in (EngineToken.ARRAY_START, EngineToken.DICT_START):
value = kls.frombytes(tokenizer)
else:
value = kls.frombytes(token)
self.append(value)
return self
def write(self, fp, indent=None):
written = write_bytes(fp, b'[')
if indent is None:
for item in self:
if isinstance(item, Dict):
written += item.write(fp, indent=None)
else:
written += write_bytes(fp, b' ')
written += item.write(fp)
written += write_bytes(fp, b' ')
else:
for item in self:
written += item.write(fp, indent=indent)
written += self._write_newline(fp, indent)
written += self._write_indent(fp, indent)
written += write_bytes(fp, b']')
return written
def _write_indent(self, fp, indent):
if indent is None:
return write_bytes(fp, b' ')
return write_bytes(fp, b'\t' * (indent))
def _write_newline(self, fp, indent):
if indent is None:
return 0
return write_bytes(fp, b'\n')
[docs]@register(EngineToken.STRING)
class String(ValueElement):
"""
String element.
"""
_ESCAPED_CHARS = (b'\\', b'(', b')')
@classmethod
def read(cls, fp):
return cls.frombytes(fp.read())
@classmethod
def frombytes(cls, data):
value = data[1:-1]
for c in cls._ESCAPED_CHARS:
value = value.replace(b'\\' + c, c)
return cls(value.decode('utf-16'))
def write(self, fp):
value = self.value.encode('utf-16-be')
for c in self._ESCAPED_CHARS:
value = value.replace(c, b'\\' + c)
return write_bytes(fp, b'(' + codecs.BOM_UTF16_BE + value + b')')
[docs]@register(EngineToken.BOOLEAN)
class Bool(BooleanElement):
"""
Bool element.
"""
@classmethod
def read(cls, fp):
return cls.frombytes(fp.read())
@classmethod
def frombytes(cls, data):
return cls(data == b'true')
def write(self, fp, indent=0):
return write_bytes(fp, b'true' if self.value else b'false')
[docs]@register(EngineToken.NUMBER)
class Integer(IntegerElement):
"""
Integer element.
"""
@classmethod
def read(cls, fp):
return cls.frombytes(fp.read())
@classmethod
def frombytes(cls, data):
return cls(int(data))
def write(self, fp, indent=0):
return write_bytes(fp, b'%d' % (self.value))
[docs]@register(EngineToken.NUMBER_WITH_DECIMAL)
class Float(NumericElement):
"""
Float element.
"""
@classmethod
def read(cls, fp):
return cls.frombytes(fp.read())
@classmethod
def frombytes(cls, data):
return cls(float(data))
def write(self, fp):
value = b'%.8f' % (self.value)
value = value.rstrip(b'0')
value = value + b'0' if value.endswith(b'.') else value
if 0.0 < abs(self.value) and abs(self.value) < 1.0:
value = value.replace(b'0.', b'.')
return write_bytes(fp, value)
[docs]@register(EngineToken.PROPERTY)
@attr.s(repr=False, frozen=True, eq=False, order=False)
class Property(ValueElement):
"""
Property element.
"""
@classmethod
def read(cls, fp):
return cls.frombytes(fp.read())
@classmethod
def frombytes(cls, data):
return cls(data.replace(b'/', b'').decode('macroman'))
def write(self, fp):
return write_bytes(fp, b'/' + self.value.encode('macroman'))
@register(EngineToken.UNKNOWN_TAG)
@register(EngineToken.UNKNOWN_TAG2)
class Tag(ValueElement):
"""
Tag element.
"""
@classmethod
def read(cls, fp):
return cls(fp.read())
def write(self, fp):
return write_bytes(fp, self.value)