diff --git a/README.rst b/README.rst index 646fa3b..326176a 100644 --- a/README.rst +++ b/README.rst @@ -116,7 +116,10 @@ Changelog Version 1.6.1 (next version) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -* (no changes yet) +* Optimized lazy loading of ``Resource`` objects. Previously, resource data would be read from disk whenever a ``Resource`` object was looked up, even if the data itself is never used. Now the resource data is only loaded once the ``data`` (or ``data_raw``) attribute is accessed. + + * The same optimization applies to the ``name`` attribute, although this is unlikely to make a difference in practice. + * As a result, it is no longer possible to construct ``Resource`` objects without a resource file. This was previously possible, but had no practical use. Version 1.6.0 ^^^^^^^^^^^^^ diff --git a/rsrcfork/api.py b/rsrcfork/api.py index b3964ae..b77e765 100644 --- a/rsrcfork/api.py +++ b/rsrcfork/api.py @@ -97,24 +97,31 @@ class ResourceAttrs(enum.Flag): class Resource(object): """A single resource from a resource file.""" + _resfile: "ResourceFile" type: bytes id: int - name: typing.Optional[bytes] + name_offset: int + _name: typing.Optional[bytes] attributes: ResourceAttrs - data_raw: bytes + data_raw_offset: int + _data_raw: bytes _compressed_info: compress.common.CompressedHeaderInfo _data_decompressed: bytes - def __init__(self, resource_type: bytes, resource_id: int, name: typing.Optional[bytes], attributes: ResourceAttrs, data_raw: bytes) -> None: - """Create a new resource with the given type code, ID, name, attributes, and data.""" + def __init__(self, resfile: "ResourceFile", resource_type: bytes, resource_id: int, name_offset: int, attributes: ResourceAttrs, data_raw_offset: int) -> None: + """Create a resource object representing a resource stored in a resource file. + + External code should not call this constructor manually. Resources should be looked up through a ResourceFile object instead. + """ super().__init__() + self._resfile = resfile self.type = resource_type self.id = resource_id - self.name = name + self.name_offset = name_offset self.attributes = attributes - self.data_raw = data_raw + self.data_raw_offset = data_raw_offset def __repr__(self) -> str: try: @@ -145,6 +152,30 @@ class Resource(object): warnings.warn(DeprecationWarning("The resource_id attribute has been deprecated and will be removed in a future version. Please use the id attribute instead.")) return self.id + @property + def name(self) -> typing.Optional[bytes]: + try: + return self._name + except AttributeError: + if self.name_offset == 0xffff: + self._name = None + else: + self._resfile._stream.seek(self._resfile.map_offset + self._resfile.map_name_list_offset + self.name_offset) + (name_length,) = self._resfile._stream_unpack(STRUCT_RESOURCE_NAME_HEADER) + self._name = self._resfile._read_exact(name_length) + + return self._name + + @property + def data_raw(self) -> bytes: + try: + return self._data_raw + except AttributeError: + self._resfile._stream.seek(self._resfile.data_offset + self.data_raw_offset) + (data_raw_length,) = self._resfile._stream_unpack(STRUCT_RESOURCE_DATA_HEADER) + self._data_raw = self._resfile._read_exact(data_raw_length) + return self._data_raw + @property def compressed_info(self) -> typing.Optional[compress.common.CompressedHeaderInfo]: """The compressed resource header information, or None if this resource is not compressed. @@ -238,18 +269,7 @@ class ResourceFile(typing.Mapping[bytes, typing.Mapping[int, Resource]], typing. name_offset, attributes, data_offset = self._submap[key] - if name_offset == 0xffff: - name = None - else: - self._resfile._stream.seek(self._resfile.map_offset + self._resfile.map_name_list_offset + name_offset) - (name_length,) = self._resfile._stream_unpack(STRUCT_RESOURCE_NAME_HEADER) - name = self._resfile._read_exact(name_length) - - self._resfile._stream.seek(self._resfile.data_offset + data_offset) - (data_length,) = self._resfile._stream_unpack(STRUCT_RESOURCE_DATA_HEADER) - data = self._resfile._read_exact(data_length) - - return Resource(self._restype, key, name, attributes, data) + return Resource(self._resfile, self._restype, key, name_offset, attributes, data_offset) def __repr__(self) -> str: if len(self) == 1: