Source code for PyPoE.poe.file.idt

"""
Overview
===============================================================================

+----------+------------------------------------------------------------------+
| Path     | PyPoE/poe/file/idt.py                                            |
+----------+------------------------------------------------------------------+
| Version  | 1.0.0a0                                                          |
+----------+------------------------------------------------------------------+
| Revision | $Id: f6432f6155b2d277be48be55bd8a89f893a14485 $                  |
+----------+------------------------------------------------------------------+
| Author   | Omega_K2                                                         |
+----------+------------------------------------------------------------------+

Description
===============================================================================

File Format handler for Grinding Gear Games' .idt format.

.idt files are generally used to link the inventory texture to an object.

Agreement
===============================================================================

See PyPoE/LICENSE

Documentation
===============================================================================

Public API
-------------------------------------------------------------------------------

.. autoclass:: IDTFile

.. autoclass:: TextureRecord

.. autoclass:: CoordinateRecord


Internal API
-------------------------------------------------------------------------------

.. autoclass:: TextureList

.. autoclass:: CoordinateList
"""

# =============================================================================
# Imports
# =============================================================================

# Python
import codecs
import re

# self
from PyPoE.shared.containers import Record, TypedList, TypedContainerMeta
from PyPoE.poe.file.shared import AbstractFile, ParserError

# =============================================================================
# Globals
# =============================================================================

__all__ = ['IDTFile', 'TextureRecord', 'CoordinateRecord']

# =============================================================================
# Classes
# =============================================================================


[docs]class CoordinateRecord(Record): """ Object that represents a single coordinate with the relevant attributes Attributes ---------- x : int x-coordinate y : int y-coordinate """ __slots__ = ['x', 'y']
[docs] def __init__(self, x, y): """ Parameters ---------- x : int x-coordinate y : int y-coordinate """ self.x = int(x) self.y = int(y)
[docs]class CoordinateList(TypedList, metaclass=TypedContainerMeta): """ A list that only accepts :class:`CoordinateRecord` instances. """ ACCEPTED_TYPES = CoordinateRecord
[docs]class TextureRecord(Record): """ Object that represents a single texture with the relevant attributes Attributes ---------- 'name' : str name (internal path) of the texture 'records' : CoordinateList[CoordinateRecord] :class:`CoordinateList` of :class:`CoordinateRecord` instances for this texture. """ __slots__ = ['name', 'records']
[docs] def __init__(self, name, records=None): """ Parameters ---------- name : str name (internal path) of the texture records : None or CoordinateList[CoordinateRecord] :class:`CoordinateList` of :class:`CoordinateRecord` instances for this texture. If None, an empty :class:`CoordinateList` will be created. Raises ------ TypeError If records is of invalid type TypeError If the containing types of records are invalid """ self.name = name if records is None: self.records = CoordinateList() elif isinstance(records, CoordinateList): self.records = records elif isinstance(records, list): self.records = CoordinateList(records) else: raise TypeError('records must a valid CoordinateList.')
[docs]class TextureList(TypedList, metaclass=TypedContainerMeta): """ A list that only accepts TextureRecord instances. """ ACCEPTED_TYPES = TextureRecord
[docs]class IDTFile(AbstractFile): """ Encapsulated in-memory representation of .idt files. """ # complete match _regex_parse = re.compile( r'^' r'version (?P<version>[0-9]+)[\r\n]*' r'image "(?P<image>[\w\./\\_\'\-]+)"[\r\n]*' r'(?P<texture_count>[0-9]+)[\r\n]*' r'(?P<textures>.*)' # Match the rest r'$', re.UNICODE | re.MULTILINE | re.DOTALL ) # for findall _regex_texture = re.compile( r'^' r'(?P<name>[a-zA-Z]+)[ ]+' r'(?P<count>[0-9]+)[ ]+' r'(?P<coordinates>(?:[0-9]+[ ]*)*)' r'[\r\n]*$', re.UNICODE | re.MULTILINE | re.DOTALL ) # for findall _regex_coordinates = re.compile( r'(?P<x>[0-9]+)[ ]+' r'(?P<y>[0-9]+)[ ]*' r'', re.UNICODE | re.MULTILINE | re.DOTALL ) EXTENSION = '.idt'
[docs] def __init__(self, data=None): """ Creates a new IDTFile instance. Optionally data can be specified to initialize the object in memory with the given data. The same can be achieved by simply setting the relevant attributes. Note that :meth:`IDTFile.read` will override any initial data. Parameters ---------- data : dict or None Take a dict containing the data to create this object and it's attributes with. The dict should match the structure of the classes attributes and the respective sub attributes. Raises ------ TypeError if dict contains data of invalid types """ if data is None: self.version = 0 self._image = None self._records = TextureList() else: tex = TextureList() for tex_record in data['records']: x = CoordinateList() for coord_record in tex_record['records']: x.append(CoordinateRecord(**coord_record)) kwargs = tex_record.copy() del kwargs['records'] tex.append(TextureRecord(records=x, **kwargs)) self.version = data['version'] self.image = data['image'] self._records = tex
# Properties def _get_records(self): """ Get records Returns ------- TextureList[TextureRecord] List of stored :class:`TextureRecord` instances """ return self._records def _set_records(self, value): """ Set records Parameters ---------- value : TextureList[TextureRecord] value to set the records to Raises ------ TypeError if the record is an invalid texture list """ if isinstance(value, TextureList): self._records = value elif isinstance(value, list): self._records = TextureList(value) else: raise TypeError('records must be a valid TextureList.') records = property(fget=_get_records, fset=_set_records) def _get_image(self): """ Get image path Returns ------- str image path relative to content.ggpk root """ return self._image def _set_image(self, value): """ Set image path Parameters ---------- value : str image path relative to content.ggpk root """ self._image = value.replace('\\', '/') image = property(fget=_get_image, fset=_set_image) # Private def _write(self, buffer): out = [] out.append('version %s\n' % self.version) out.append('image "%s"\n' % self._image) out.append('%s\n' % len(self._records)) for tex_record in self._records: out.append('%s %s' % (tex_record.name, len(tex_record.records))) for coord_record in tex_record.records: out.append(' %s %s' % (coord_record.x, coord_record.y)) out.append('\n') ''.join(out).encode('utf-16_le') buffer.write(codecs.BOM_UTF16_LE + ''.join(out).encode('utf-16_le')) def _read(self, buffer, *args, **kwargs): # Should detect little endian byte order accordingly and remove the BOM data = buffer.read().decode('utf-16') match = self._regex_parse.match(data) if not match: raise ParserError('Failed to find the base information. File may not be a .idt file or malformed.') textures = TextureList() for tex_match in self._regex_texture.finditer(match.group('textures')): coordinates = CoordinateList() for coord_match in self._regex_coordinates.finditer(tex_match.group('coordinates')): coordinates.append(CoordinateRecord(**coord_match.groupdict())) if len(coordinates) != int(tex_match.group('count')): raise ParserError('Amount of found coordinates (%s) does not match the amount of specified coordinates (%s)' % (len(coordinates), tex_match.group('count'))) textures.append(TextureRecord(tex_match.group('name'), coordinates)) if len(textures) != int(match.group('texture_count')): raise ParserError('Amount of found textures (%s) does not match the amount of specified textures (%s)' % (len(textures), match.group('texture_count'))) self._records = textures self.version = int(match.group('version')) self.image = match.group('image')