pax_global_header00006660000000000000000000000064152107474750014526gustar00rootroot0000000000000052 comment=0cb29fec4ef23cd8b69ec2357ebca7f9d331daca icoextract-0.3.0/000077500000000000000000000000001521074747500136735ustar00rootroot00000000000000icoextract-0.3.0/.gitignore000066400000000000000000000033411521074747500156640ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # Autogenerated docs html/ .vscode icoextract-0.3.0/.woodpecker/000077500000000000000000000000001521074747500161135ustar00rootroot00000000000000icoextract-0.3.0/.woodpecker/check.yml000066400000000000000000000010371521074747500177140ustar00rootroot00000000000000steps: - name: Compile Win16 binaries image: ghcr.io/arlaneenalra/watcom-docker commands: - apk add --no-cache imagemagick - cd tests && make win16 - name: Run integration tests image: python:${PYTHON_VERSION}-alpine commands: - apk add --no-cache imagemagick make mingw-w64-gcc i686-mingw-w64-gcc - pip install .[thumbnailer,win16] - cd tests && make - python3 -m unittest discover . --verbose when: event: [tag, push, pull_request] matrix: PYTHON_VERSION: - 3.10 - 3.14 icoextract-0.3.0/.woodpecker/doc-upload.yml000066400000000000000000000010311521074747500206600ustar00rootroot00000000000000steps: - commands: - pip install .[dev] awscli - pdoc --no-show-source -o ./html/ icoextract/*.py '!icoextract.version' - aws s3 cp html/ s3://jlu5-ci-doc/ --recursive image: python:3.14 name: Upload API docs environment: AWS_ACCESS_KEY_ID: from_secret: AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY: from_secret: AWS_SECRET_ACCESS_KEY AWS_ENDPOINT_URL_S3: https://s3.us-west-000.backblazeb2.com when: - event: tag - event: push branch: wip/doc-* depends_on: - check icoextract-0.3.0/.woodpecker/pypi-upload.yml000066400000000000000000000004511521074747500211010ustar00rootroot00000000000000steps: - commands: - pip install build twine - python3 -m build - twine upload --username __token__ dist/* image: python:3.14 name: Upload release to PyPI environment: TWINE_PASSWORD: from_secret: pypi_token when: event: [tag] depends_on: - check icoextract-0.3.0/CHANGELOG.md000066400000000000000000000050761521074747500155140ustar00rootroot00000000000000# Changelog ## icoextract 0.3.0 (2026-06-06) - Add support for Win16 NE EXEs via the nefile module (optional dependency) - Add support for string resource IDs in addition to integers (previously string IDs were always read as raw numbers) - `icolist` is now more verbose and shows each icon's image info (dimensions, size, and offsets) - `exe_thumbnailer` now prefers icons with higher bit depths. In practice, this mostly affects older programs like Internet Explorer 6 and Windows 3.x system apps. - **Notes for developers**: - `icoextract.IconExtractor()` is now a factory function instead of a class. By default it will autoselect the `IconExtractor` type based on the input executable format - this can be overridden using the `exe_type` parameter. - `list_group_icons()` now returns `(resource ID, group icon dir entry)` tuples instead of a simple ID and offset pair. - ResourceID is now a class type with `int()` and `str()` conversions, as these IDs can be either a string or a number. ## icoextract 0.2.0 (2025-06-02) - Add `-i/--id` option to extract icons by resource ID - Refactor / streamline extraction code - Add new `IconNotFoundError` exception, raised when the requested icon index or resource ID does not exist - cli: warn when extracting from Windows system DLLs for which icons have been moved to the SystemResources directory - Bump minimum supported Python version to 3.9 ## icoextract 0.1.6 (2025-03-02) - exe-thumbnailer: add `--force-resize` convenience option - exe-thumbnailer: fix handling of icons containing non-standard sizes like 192x192 - cli: warn when exporting images with a wrong extension (.jpg or .png). This aims to address a common source of confusion ## icoextract 0.1.5 (2024-04-28) - Add `application/vnd.microsoft.portable-executable` to supported MIME types - Bump minimum supported Python version to 3.8 - Add pdoc3 config, for autogenerated API docs ## icoextract 0.1.4 (2022-08-08) - IconExtractor: support raw bytes as input, in addition to a filename - Refresh function descriptions ## icoextract 0.1.3 (2022-06-12) - Fix thumbnail resizing; use native 128x128 icons when available (GH-7) - Clarify installation steps for thumbnailer - setup.py: exclude `tests` from installed packages (GH-9) ## icoextract 0.1.2 (2020-12-22) - Declare Pillow as an optional dependency (for icoextract-thumbnailer) - Fix autodiscovery for tests ## icoextract 0.1.1 (2020-07-01) - Refactor scripts to use setuptools entrypoints (adds Windows support) - Raise an error when seeing invalid icon definitions ## icoextract 0.1.0 (2019-11-22) - Initial release icoextract-0.3.0/LIB-USAGE.md000066400000000000000000000014221521074747500154640ustar00rootroot00000000000000For help on icoextract's frontend scripts, see `icoextract --help` and `icolist --help`. ## Using icoextract as a library ```python from icoextract import IconExtractor, IconExtractorError try: extractor = IconExtractor('/path/to/your.exe') # Export the first group icon to a .ico file extractor.export_icon('/path/to/your.ico', num=0) # Or read the .ico into a buffer, to pass it into other code data = extractor.get_icon(num=0) from PIL import Image im = Image.open(data) # ... manipulate a copy of the icon # In icoextract 0.2.0+, you can also extract icons by resource ID extractor.export_icon('/path/to/your.ico', resource_id=1234) except IconExtractorError: # No icons available, or the icon resource is malformed pass ``` icoextract-0.3.0/LICENSE000066400000000000000000000021751521074747500147050ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015-2016 Fadhil Mandaga Copyright (c) 2019-2026 James Lu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. icoextract-0.3.0/README.md000066400000000000000000000061711521074747500151570ustar00rootroot00000000000000# icoextract [![Build Status](https://woodpecker.overdrivenetworks.com/api/badges/5/status.svg)](https://woodpecker.overdrivenetworks.com/repos/5) [![Packaging status](https://repology.org/badge/tiny-repos/icoextract.svg)](https://repology.org/project/icoextract/versions) **icoextract** is an icon extractor library for Windows executables (.exe/.dll/.mun). It also includes a thumbnailer script (`exe-thumbnailer`) for Linux desktops. This project is inspired by [extract-icon-py](https://github.com/firodj/extract-icon-py), [icoutils](https://www.nongnu.org/icoutils/), and others. icoextract aims to be: - Lightweight - Portable (cross-platform) - Fast on large files ## Installation ### Dependencies - Python 3.10+ - [pefile](https://github.com/erocarrera/pefile) - [Pillow](https://pillow.readthedocs.io/en/stable/) - optional, for exe-thumbnailer - [nefile](https://github.com/npjg/nefile) - optional, for Win16 / NE EXE support ### Installing from source You can install the project via pip: `pip3 install icoextract[thumbnailer]` On Linux, you can activate the thumbnailer by copying [`exe-thumbnailer.thumbnailer`](/exe-thumbnailer.thumbnailer) into the thumbnailers directory: - `/usr/local/share/thumbnailers/` if you installed `icoextract` globally - `~/.local/share/thumbnailers` if you installed `icoextract` for your user only The thumbnailer should work with any file manager that implements the [Freedesktop Thumbnails Standard](https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html): this includes Nautilus, Caja, Nemo, Thunar (when Tumbler is installed), and PCManFM. KDE / Dolphin uses a different architecture and is *not* supported here. ### Distribution packages You can install icoextract from any of these distribution repositories: [![Packaging status](https://repology.org/badge/vertical-allrepos/icoextract.svg?columns=5)](https://repology.org/project/icoextract/versions) ## Usage icoextract ships `icoextract` and `icolist` scripts to extract and list icon resources inside a file. **Note**: recent versions of Windows (Windows 10 1903+) have moved icons from system libraries (`shell32.dll`, etc.) into a new [`C:\Windows\SystemResources`](https://superuser.com/questions/1480268/) folder. icoextract can extract these `.mun` files natively, but the `.dll`s themselves no longer contain icons. For API docs, see https://projects.jlu5.com/icoextract.html ``` usage: icoextract [-h] [-V] [-n NUM] [-i ID] [-v] input output Windows executable icon extractor. positional arguments: input input filename (.exe/.dll/.mun) output output filename (.ico) options: -h, --help show this help message and exit -V, --version show program's version number and exit -n, --num NUM index of icon to extract -i, --id ID resource ID of icon to extract -v, --verbose enables debug logging ``` ``` usage: icolist [-h] [-V] [-v] input Lists group icons present in a program. positional arguments: input input filename options: -h, --help show this help message and exit -V, --version show program's version number and exit -v, --verbose enables debug logging ``` icoextract-0.3.0/exe-thumbnailer.thumbnailer000066400000000000000000000002771521074747500212260ustar00rootroot00000000000000[Thumbnailer Entry] Exec=exe-thumbnailer -v -s %s %i %o MimeType=application/x-ms-dos-executable;application/x-dosexec;application/x-msdownload;application/vnd.microsoft.portable-executable icoextract-0.3.0/icoextract/000077500000000000000000000000001521074747500160405ustar00rootroot00000000000000icoextract-0.3.0/icoextract/__init__.py000066400000000000000000000057421521074747500201610ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # Copyright (c) 2015-2016 Fadhil Mandaga # Copyright (c) 2019-2026 James Lu """ Windows executable icon extractor. .. include:: ../LIB-USAGE.md """ from contextlib import ExitStack import enum import logging import mmap from .base_extractor import BaseIconExtractor from .exceptions import ( IconExtractorError, IconNotFoundError, NoIconsAvailableError, InvalidIconDefinitionError, UnknownExecutableError, ) from .ne_extractor import NEIconExtractor from .pe_extractor import PEIconExtractor logger = logging.getLogger("icoextract") logging.basicConfig() try: from .version import __version__ except ImportError: __version__ = 'unknown' logger.info('icoextract: failed to read program version') __all__ = [ 'IconExtractor', 'IconExtractorError', 'IconNotFoundError', 'NoIconsAvailableError', 'InvalidIconDefinitionError', 'UnknownExecutableError', 'ExecutableType', ] class ExecutableType(enum.Enum): """Enum for supported executable formats.""" AUTO = 0 PE = 1 NE = 2 def detect_executable_type(buf: bytearray | mmap.mmap) -> ExecutableType: if buf[:2] != b'MZ': raise UnknownExecutableError("Unknown executable type (no MZ header)") # Get the offset to the real header (located at 0x3C) e_lfanew = int.from_bytes(buf[0x3C:0x40], byteorder='little') signature = buf[e_lfanew:e_lfanew+4] if signature.startswith(b'PE\x00\x00'): return ExecutableType.PE if signature.startswith(b'NE'): return ExecutableType.NE raise UnknownExecutableError("Unknown / unsupported executable type") def IconExtractor(filename: str | None = None, data: bytearray | mmap.mmap | None = None, exe_type: ExecutableType = ExecutableType.AUTO) -> BaseIconExtractor: """ IconExtractor factory function. When `exe_type` is set to *AUTO*, this will return an `icoextract.pe_extractor.PEIconExtractor` for Win32/Win64 PE executables and an `icoextract.ne_extractor.NEIconExtractor` for Win16 NE executables. """ if filename is None and data is None: raise ValueError("filename and data cannot both be None") if exe_type == ExecutableType.AUTO: if data is not None: exe_type = detect_executable_type(data) else: assert filename with ExitStack() as stack: f = stack.enter_context(open(filename, 'rb')) mm = stack.enter_context(mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)) exe_type = detect_executable_type(mm) logger.debug("Detected executable type as %s", exe_type) if exe_type == ExecutableType.PE: return PEIconExtractor(filename, data) if exe_type == ExecutableType.NE: return NEIconExtractor(filename, data) raise UnknownExecutableError("Unknown executable type") __pdoc__ = { 'scripts': False, 'version': False, } icoextract-0.3.0/icoextract/base_extractor.py000066400000000000000000000053021521074747500214170ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # Copyright (c) 2019-2026 James Lu import abc import io import struct from .types import ExtractedGroupIcon, GroupIconWithIconOffsets, ResourceID class BaseIconExtractor(abc.ABC): """Base icon extractor class containing .ico writing logic.""" @abc.abstractmethod def _extract_icon(self, index: int = 0, resource_id: int | str | None = None) -> ExtractedGroupIcon: """Extract an icon by its index or resource ID.""" @abc.abstractmethod def list_group_icons(self) -> list[tuple[ResourceID, GroupIconWithIconOffsets]]: """ Returns all group icon entries as a list of (resource ID, group icon dir entry) tuples. """ def _write_ico(self, fd, icons: ExtractedGroupIcon): """Writes ICO data to a file descriptor.""" fd.write(b"\x00\x00") # 2 reserved bytes fd.write(struct.pack(" None: """ Exports ICO data for the requested group icon to `filename`. Icons can be selected by index (`num`) or resource ID. By default, the first icon in the binary is exported. """ group_icon = self._extract_icon(index=num, resource_id=resource_id) with open(filename, 'wb') as f: self._write_ico(f, group_icon) def get_icon(self, num: int = 0, resource_id: int | str | None = None) -> io.BytesIO: """ Exports ICO data for the requested group icon as a `io.BytesIO` instance. Icons can be selected by index (`num`) or resource ID. By default, the first icon in the binary is exported. """ group_icon = self._extract_icon(index=num, resource_id=resource_id) f = io.BytesIO() self._write_ico(f, group_icon) return f icoextract-0.3.0/icoextract/exceptions.py000066400000000000000000000013531521074747500205750ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # Copyright (c) 2019-2026 James Lu class IconExtractorError(Exception): """Superclass for exceptions raised by IconExtractor.""" class IconNotFoundError(IconExtractorError): """Exception raised when extracting an icon index or resource ID that does not exist.""" class NoIconsAvailableError(IconExtractorError): """Exception raised when the input program has no icon resources.""" class InvalidIconDefinitionError(IconExtractorError): """Exception raised when the input program has an invalid icon resource.""" class UnknownExecutableError(IconExtractorError): """Exception raised when the executable type is invalid or not supported.""" icoextract-0.3.0/icoextract/ne_extractor.py000066400000000000000000000106621521074747500211140ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # Copyright (c) 2019-2026 James Lu import logging try: import nefile except ImportError: nefile = None from .base_extractor import BaseIconExtractor from .exceptions import ( IconNotFoundError, NoIconsAvailableError ) from .types import ( ExtractedGroupIcon, GroupIconDirEntry, GroupIconWithIconOffsets, ResourceID, ) logger = logging.getLogger("icoextract") class NEIconExtractor(BaseIconExtractor): """Win16 New Executable (NE) icon extractor.""" def __init__(self, filename=None, data=None): """ Loads an Win16 New Executable from the given `filename` or `data` (raw buffer). If both `filename` and `data` are given, `filename` takes precedence. If the executable has contains no icons, this will raise `NoIconsAvailableError`. """ if nefile is None: raise ImportError("nefile module must be installed to extract NE programs") self._ne = nefile.NE(filename, data) if not self._ne.resource_table: raise NoIconsAvailableError("Executable does not have any resources") if not (group_icon_resources := self._ne.resource_table.resources.get( nefile.resource_table.ResourceType.RT_GROUP_ICON)): raise NoIconsAvailableError("Executable does not have any group icons") self._group_icon_resources = group_icon_resources @staticmethod def _convert_nefile_icon_dir_entry(nefile_icon_dir_entry) -> GroupIconDirEntry: assert nefile is not None assert isinstance(nefile_icon_dir_entry, nefile.resources.icon.IconDirectoryEntry) return GroupIconDirEntry( Width=nefile_icon_dir_entry.width, Height=nefile_icon_dir_entry.height, ColorCount=nefile_icon_dir_entry.total_palette_colors, Planes=nefile_icon_dir_entry.color_planes, BitCount=nefile_icon_dir_entry.bits_per_pixel, BytesInRes=nefile_icon_dir_entry.icon_size_in_bytes, ID=nefile_icon_dir_entry.icon_resource_id, ) def list_group_icons(self) -> list[tuple[ResourceID, GroupIconWithIconOffsets]]: assert nefile is not None result = [] resources_offset = self._ne.resource_table.resource_table_start_offset icon_rtt = self._ne.resource_table.resource_type_tables[ nefile.resource_table.ResourceType.RT_ICON ] icon_resource_declarations = { rdecl.id: rdecl for rdecl in icon_rtt.resource_declarations } for resource_id, nefile_group_icon in self._group_icon_resources.items(): if isinstance(resource_id, str): resource_id_wrapper = ResourceID(raw_id=None, name=resource_id) else: resource_id_wrapper = ResourceID(resource_id) icon_infos = [] for nefile_icon_dir_entry in nefile_group_icon.icon_directory.directory_entries: grp_icon_dir_entry = self._convert_nefile_icon_dir_entry(nefile_icon_dir_entry) icon_rdecl = icon_resource_declarations[grp_icon_dir_entry.ID] offset = icon_rdecl.data_start_offset file_offset = offset + resources_offset icon_infos.append((grp_icon_dir_entry, offset, file_offset)) result.append((resource_id_wrapper, icon_infos)) return result def _extract_icon(self, index: int = 0, resource_id: int | str | None = None) -> ExtractedGroupIcon: if resource_id is not None: try: nefile_group_icon = self._group_icon_resources[resource_id] except KeyError: raise IconNotFoundError(f"No icon exists with resource ID {resource_id!r}") from None else: icons_by_index = list(self._group_icon_resources.values()) try: nefile_group_icon = icons_by_index[index] except IndexError: raise IconNotFoundError(f"No icon exists at index {index}") from None results = [] for nefile_icon_dir_entry in nefile_group_icon.icon_directory.directory_entries: resource_id = nefile_icon_dir_entry.icon_resource_id grp_icon_dir_entry = self._convert_nefile_icon_dir_entry(nefile_icon_dir_entry) icondata = nefile_group_icon.icons[resource_id].data results.append((grp_icon_dir_entry, icondata)) return results icoextract-0.3.0/icoextract/pe_extractor.py000066400000000000000000000167001521074747500211150ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # Copyright (c) 2015-2016 Fadhil Mandaga # Copyright (c) 2019-2026 James Lu import ctypes import logging import os import pathlib import platform import pefile from .base_extractor import BaseIconExtractor from .exceptions import ( IconNotFoundError, InvalidIconDefinitionError, NoIconsAvailableError ) from .types import ( ExtractedGroupIcon, GroupIconDir, GroupIconDirEntry, GroupIconWithIconOffsets, ResourceID, ) logger = logging.getLogger("icoextract") class PEIconExtractor(BaseIconExtractor): """Win32/Win64 Portable Executable (PE) icon extractor.""" def __init__(self, filename=None, data=None): """ Loads an Win32/Win64 Portable Executable from the given `filename` or `data` (raw buffer). If both `filename` and `data` are given, `filename` takes precedence. If the executable has contains no icons, this will raise `NoIconsAvailableError`. """ # Use fast loading and explicitly load the RESOURCE directory entry. This saves a LOT of time # on larger files self._pe = pefile.PE(name=filename, data=data, fast_load=True) self._pe.parse_data_directories(pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE']) if not hasattr(self._pe, 'DIRECTORY_ENTRY_RESOURCE'): raise NoIconsAvailableError("File has no resources") # Reverse the list of entries before making the mapping so that earlier values take precedence # When an executable includes multiple icon resources, we should use only the first one. # pylint: disable=no-member resources = {rsrc.id: rsrc for rsrc in reversed(self._pe.DIRECTORY_ENTRY_RESOURCE.entries)} self._groupiconres = resources.get(pefile.RESOURCE_TYPE["RT_GROUP_ICON"]) if not self._groupiconres: if filename and platform.system() == "Windows": self._print_windows_usage_hint(filename) raise NoIconsAvailableError("File has no group icon resources") self._rticonres = resources.get(pefile.RESOURCE_TYPE["RT_ICON"]) self._group_icons = self._groupiconres.directory.entries self._group_icons_by_id: dict[int | str, pefile.ResourceDirEntryData] = {} for entry in self._group_icons: self._group_icons_by_id[entry.struct.Name] = entry # For resources with a string name, track them by both the string and the underlying # numerical index if entry.name: self._group_icons_by_id[str(entry.name)] = entry self._icons = {icon_entry_list.id: icon_entry_list.directory.entries[0] # Select first language for icon_entry_list in self._rticonres.directory.entries} def list_group_icons(self) -> list[tuple[ResourceID, GroupIconWithIconOffsets]]: results = [] for entry in self._group_icons: resource_id = ResourceID( raw_id=entry.struct.Name, name=str(entry.name) if entry.name else None ) grp_icon_resource = self._group_icons_by_id[entry.struct.Name] grp_icon_data_full = self._extract_from_pefile(grp_icon_resource, skip_data=True) # Return only the metadata and offsets grp_icon_data = [ (grp_icon_dir_entry, resource_offset, file_offset) for (grp_icon_dir_entry, _, resource_offset, file_offset) in grp_icon_data_full ] results.append((resource_id, grp_icon_data)) return results def _extract_from_pefile( self, resource_dir_entry_data: pefile.ResourceDirEntryData, skip_data: bool = False, ) -> list[tuple[GroupIconDirEntry, bytes, int, int]]: """ Returns the specified group icon in the binary. Result is a list of (group icon dir entry, icon data) tuples. """ icon_lang = None if resource_dir_entry_data.struct.DataIsDirectory: # Select the first language from subfolders as needed. resource_dir_entry_data = resource_dir_entry_data.directory.entries[0] icon_lang = resource_dir_entry_data.struct.Name logger.debug("Picking first language %s", icon_lang) # Read the data pointed to by the group icon directory (GRPICONDIR) struct. rva = resource_dir_entry_data.data.struct.OffsetToData grp_icon_data = self._pe.get_data(rva, resource_dir_entry_data.data.struct.Size) grp_icon_dir = GroupIconDir.from_buffer_copy(grp_icon_data) logger.debug("Group icon has %d images: %s", grp_icon_dir.Count, grp_icon_dir) # pylint: disable=no-member if grp_icon_dir.Reserved: # pylint: disable=no-member raise InvalidIconDefinitionError("Invalid group icon definition (got Reserved=%s instead of 0)" % hex(grp_icon_dir.Reserved)) # For each group icon entry (GRPICONDIRENTRY) that immediately follows, read the struct and look up the # corresponding icon image extracted_grp_icons = [] icon_offset = ctypes.sizeof(grp_icon_dir) for grp_icon_index in range(grp_icon_dir.Count): grp_icon_dir_entry = GroupIconDirEntry.from_buffer_copy(grp_icon_data, icon_offset) icon_offset += ctypes.sizeof(grp_icon_dir_entry) logger.debug("Got group icon entry %d: %s", grp_icon_index, grp_icon_dir_entry) icon_entry = self._icons[grp_icon_dir_entry.ID] logger.debug("Got icon data for ID %d: %s", grp_icon_dir_entry.ID, icon_entry.data.struct) icon_res_offset = icon_entry.data.struct.OffsetToData icon_file_offset = self._pe.get_offset_from_rva(icon_res_offset) icon_data = b'' if not skip_data: icon_data = self._pe.get_data(icon_res_offset, icon_entry.data.struct.Size) extracted_grp_icons.append((grp_icon_dir_entry, icon_data, icon_res_offset, icon_file_offset)) return extracted_grp_icons def _extract_icon(self, index: int = 0, resource_id: int | str | None = None) -> ExtractedGroupIcon: if resource_id is not None: try: resource_dir_entry_data = self._group_icons_by_id[resource_id] except KeyError: raise IconNotFoundError(f"No icon exists with resource ID {resource_id!r}") from None else: try: resource_dir_entry_data = self._group_icons[index] except IndexError: raise IconNotFoundError(f"No icon exists at index {index}") from None # Return only the GroupIconDirEntry metadata and icon bytes return [ (grp_icon_dir_entry, icon_data) for (grp_icon_dir_entry, icon_data, _, _) in self._extract_from_pefile(resource_dir_entry_data) ] @staticmethod def _print_windows_usage_hint(filename): path = pathlib.Path(filename) systemroot = pathlib.Path(os.getenv('SYSTEMROOT')) if path.is_relative_to(systemroot / 'System32') or \ path.is_relative_to(systemroot / 'SysWOW64'): mun_path = pathlib.Path(systemroot / 'SystemResources' / (path.name + '.mun')) if mun_path.is_file(): logger.warning( 'System DLL files in Windows 10 1903+ no longer contain icons. ' 'Try extracting from %s instead.', mun_path) icoextract-0.3.0/icoextract/scripts/000077500000000000000000000000001521074747500175275ustar00rootroot00000000000000icoextract-0.3.0/icoextract/scripts/__init__.py000066400000000000000000000000601521074747500216340ustar00rootroot00000000000000# Stub so that scripts/ is treated as a package icoextract-0.3.0/icoextract/scripts/extract.py000077500000000000000000000027221521074747500215610ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # Copyright (c) 2019-2026 James Lu """ Windows executable icon extractor. """ import argparse import logging import os.path from icoextract import IconExtractor, logger, __version__ _WRONG_EXTENSIONS_HINT = {'.jpg', '.jpeg', '.png'} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("-V", "--version", action='version', version=f'icoextract {__version__}') parser.add_argument("-n", "--num", type=int, help="index of icon to extract", default=0) parser.add_argument("-i", "--id", type=str, help="resource ID of icon to extract", default=None) parser.add_argument("-v", "--verbose", action="store_true", help="enables debug logging") parser.add_argument("input", help="input filename (.exe/.dll/.mun)") parser.add_argument("output", help="output filename (.ico)") args = parser.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) extractor = IconExtractor(args.input) try: resource_id = int(args.id) except (ValueError, TypeError): resource_id = args.id extractor.export_icon(args.output, num=args.num, resource_id=resource_id) file_ext = os.path.splitext(args.output)[1].lower() if file_ext in _WRONG_EXTENSIONS_HINT: logger.warning('This tool outputs .ico files, not %s. The resulting file will have the wrong file extension.', file_ext) icoextract-0.3.0/icoextract/scripts/icolist.py000077500000000000000000000031401521074747500215500ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # Copyright (c) 2019-2026 James Lu """ Lists group icons present in a program. """ import argparse import logging from icoextract import IconExtractor, logger, __version__ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("-V", "--version", action='version', version=f'icoextract {__version__}') parser.add_argument("-v", "--verbose", action="store_true", help="enables debug logging") parser.add_argument("input", help="input filename") args = parser.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) extractor = IconExtractor(args.input) for idx, entry in enumerate(extractor.list_group_icons()): resource_id, grp_icon_dir_entries = entry print(f"Group Icon Index: {idx} ", end='') print(f"ID: {resource_id}", end='') if resource_id.raw_id: print(f"({hex(resource_id.raw_id)})", end='') print(" ", end='') print(f"Count: {len(grp_icon_dir_entries)}") for (grp_icon_dir_entry, resource_offset, file_offset) in grp_icon_dir_entries: print(f" Icon ID: {grp_icon_dir_entry.ID}" # Width and Height are u8 values where 0 means 256 f" Width: {grp_icon_dir_entry.Width or 256}" f" Height: {grp_icon_dir_entry.Height or 256}" f" Resource Offset: {hex(resource_offset)}" f" File Offset: {hex(file_offset)}" f" Size: {grp_icon_dir_entry.BytesInRes}") icoextract-0.3.0/icoextract/scripts/thumbnailer.py000077500000000000000000000065411521074747500224240ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # Copyright (c) 2019-2026 James Lu """ Linux (freedesktop.org) thumbnailer for Windows PE files (.exe/.dll) """ import argparse import logging import sys from PIL import Image from icoextract import IconExtractor, IconExtractorError, logger, __version__ def generate_thumbnail(inputfile, outfile, size=256, force_resize=False): """ Generates a thumbnail for an .exe file. inputfile: the input file path (%i) outfile: output filename (%o) size: determines the thumbnail output size (%s) """ try: extractor = IconExtractor(inputfile) except RuntimeError: logger.debug("Failed to extract icon for %s:", inputfile, exc_info=True) sys.exit(1) data = extractor.get_icon() im = Image.open(data) # Open up the .ico from memory if force_resize: logger.debug("Force resizing icon to %dx%d", size, size) im = im.resize((size, size)) else: if size > 256: logger.warning('Icon sizes over 256x256 are not supported') size = 256 elif size not in (128, 256): logger.warning('Unsupported size %d, falling back to 128x128', size) size = 128 if size == 128: # If large size thumbnail wasn't requested but one is available, pick an 128x128 icon if available; # otherwise scale down from 256x256 to 128x128. 128x128 is the largest resolution allowed for # "normal" size thumbnails. if (128, 128) in im.info['sizes']: logger.debug("Using native 128x128 icon") im.size = (128, 128) elif im.size > (128, 128): logger.debug("Downsizing icon to 128x128") im = im.resize((128, 128)) # For legacy apps, try to export the icon with the highest size & bit depth # Some programs (e.g. Internet Explorer 6) put a 16 color icon earlier in the file if im.size <= (48, 48): ico_frames = list(enumerate(im.ico.entry)) best_frame_idx, ico_header = max(ico_frames, key=lambda pair: (pair[1].square, pair[1].color_depth)) logger.debug("Legacy app: selecting icon %s", ico_header) im = im.ico.frame(best_frame_idx) logger.debug("Writing size %s thumbnail for %s to %s", size, inputfile, outfile) im.save(outfile, "PNG") def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("-V", "--version", action='version', version=f'exe-thumbnailer, part of icoextract {__version__}') parser.add_argument("-s", "--size", type=int, help="size of desired thumbnail", default=256) parser.add_argument("-v", "--verbose", action="store_true", help="enables debug logging") parser.add_argument("-f", "--force-resize", action="store_true", help="force resize thumbnail to the specified size") parser.add_argument("inputfile", help="input file name (.exe/.dll/.mun)") parser.add_argument("outfile", help="output file name (.png)") args = parser.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) try: generate_thumbnail(args.inputfile, args.outfile, size=args.size, force_resize=args.force_resize) except IconExtractorError as e: logger.error("Failed to thumbnail %s: %s", args.inputfile, e) logger.debug("Backtrace:", exc_info=True) icoextract-0.3.0/icoextract/types.py000066400000000000000000000040071521074747500175570ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # Copyright (c) 2019-2026 James Lu """ icoextract common types. See https://devblogs.microsoft.com/oldnewthing/20120720-00/?p=7083 for a reference on ICO structures. """ import ctypes BYTE = ctypes.c_uint8 WORD = ctypes.c_uint16 DWORD = ctypes.c_uint32 class ResourceID: """Resource ID wrapper. For resources with a string ID, str() will return its string value (e.g. "IDI_MAIN_ICON") and int() will return its underlying numeric ID. Numerical icon resource IDs can be accessed via int(), and str() will return the number casted to a string. """ def __init__(self, raw_id: int | None = None, name: str | None = None): if raw_id is None and name is None: raise ValueError("raw_id and name cannot both be None") self.raw_id = raw_id self.name = name def __str__(self): return self.name or str(self.raw_id) def __int__(self): if self.raw_id is not None: return self.raw_id raise ValueError(f"Resource ID {self.name!r} cannot be converted to an int") def __repr__(self): if self.name: return f'{self.__class__.__name__}({self.name!r})' return f'{self.__class__.__name__}({self.raw_id})' class GroupIconDirEntry(ctypes.LittleEndianStructure): """ICO GRPICONDIRENTRY / RESDIR structure""" _pack_ = 1 # Ensures tight packing (no padding) _fields_ = [ ("Width", BYTE), ("Height", BYTE), ("ColorCount", BYTE), ("Reserved", BYTE), ("Planes", WORD), ("BitCount", WORD), ("BytesInRes", DWORD), ("ID", WORD), ] class GroupIconDir(ctypes.LittleEndianStructure): """ICO GRPICONDIR / NEWHEADER structure""" _pack_ = 1 _fields_ = [ ("Reserved", WORD), ("Type", WORD), ("Count", WORD), ] ExtractedGroupIcon = list[tuple[GroupIconDirEntry, bytes]] GroupIconWithIconOffsets = list[tuple[GroupIconDirEntry, int, int]] icoextract-0.3.0/icoextract/version.py000066400000000000000000000000261521074747500200750ustar00rootroot00000000000000__version__ = '0.3.0' icoextract-0.3.0/pyproject.toml000066400000000000000000000026641521074747500166170ustar00rootroot00000000000000[build-system] requires = ["setuptools>=61.0.0"] build-backend = "setuptools.build_meta" [project] name = "icoextract" description = "Windows EXE icon extractor" readme = "README.md" requires-python = ">=3.10" authors = [ { name = "James Lu", email = "james@overdrivenetworks.com" } ] license = "MIT" license-files = ["LICENSE"] classifiers = [ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Intended Audience :: End Users/Desktop', 'Topic :: Software Development :: Libraries :: Python Modules', 'Operating System :: OS Independent', 'Operating System :: POSIX', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.14', ] dependencies = [ "pefile", ] dynamic = ["version"] [project.optional-dependencies] thumbnailer = ["Pillow"] win16 = ["nefile"] dev = [ "icoextract[thumbnailer]", "icoextract[win16]", "pdoc", ] [project.scripts] icoextract = "icoextract.scripts.extract:main" icolist = "icoextract.scripts.icolist:main" exe-thumbnailer = "icoextract.scripts.thumbnailer:main [thumbnailer]" [tool.setuptools.packages.find] exclude = ["tests"] [tool.setuptools.dynamic] version = { attr = "icoextract.version.__version__" } [tool.pylint] max-line-length = 120 icoextract-0.3.0/tests/000077500000000000000000000000001521074747500150355ustar00rootroot00000000000000icoextract-0.3.0/tests/.gitignore000066400000000000000000000000501521074747500170200ustar00rootroot00000000000000tmp*.* *.ico *.exe testapp.rc *.o *.res icoextract-0.3.0/tests/Makefile000066400000000000000000000113601521074747500164760ustar00rootroot00000000000000CFLAGS=-mwindows -g MINGW64=x86_64-w64-mingw32- MINGW32=i686-w64-mingw32- .PHONY: all win64 win32 win16 clean WATCOM ?= /opt/watcom WCL_EXE := $(shell command -v wcl 2>/dev/null) DOCKER_EXE := $(shell command -v docker 2>/dev/null) WCL_FLAGS := -k -bt=windows -ml -l=windows WRC_FLAGS := -q -bt=windows # OpenWatcom is not available in most distros. For convenience, this will try a # community Docker image if it is not already installed ifneq ($(WCL_EXE),) WCL = INCLUDE="$(WATCOM)/h:$(WATCOM)/h/win" wcl $(WCL_FLAGS) WRC = INCLUDE="$(WATCOM)/h:$(WATCOM)/h/win" wrc $(WRC_FLAGS) else ifneq ($(DOCKER_EXE),) OPENWATCOM_DOCKER = docker run --rm \ -u "$(shell id -u):$(shell id -g)" \ --network none \ -v $(CURDIR):/src \ -e INCLUDE="$(WATCOM)/h:$(WATCOM)/h/win" \ ghcr.io/arlaneenalra/watcom-docker WCL = $(OPENWATCOM_DOCKER) wcl $(WCL_FLAGS) WRC = $(OPENWATCOM_DOCKER) wrc $(WRC_FLAGS) endif # Win16 is excluded by default as it requires a different compiler all: win64 win32 win64: testapp64.exe testapp64-nores.exe testapp64-noicon.exe \ testapp64-with128.exe testapp64-with192.exe \ testapp64-string-res.exe win32: testapp32.exe testapp32-nores.exe testapp32-noicon.exe \ testapp32-smallonly.exe testapp32-bpp.exe win16: testapp16.exe testapp16-nores.exe testapp16-noicon.exe tmp-testapp-16.bmp: testapp.png convert testapp.png -resize 16x16 tmp-testapp-16.bmp tmp-testapp-32.bmp: testapp.png convert testapp.png -resize 32x32 tmp-testapp-32.bmp tmp-testapp-48.bmp: testapp.png convert testapp.png -resize 48x48 tmp-testapp-48.bmp # icon with standard sizes: 16x16, 32x32, 48x48, 256x256 testapp.ico: testapp.png tmp-testapp-16.bmp tmp-testapp-32.bmp tmp-testapp-48.bmp convert testapp.png tmp-testapp*.bmp testapp.ico # Small icon (only up to 48x48) testapp-smallonly.ico: testapp.png tmp-testapp-16.bmp tmp-testapp-32.bmp tmp-testapp-48.bmp convert tmp-testapp-*.bmp testapp-smallonly.ico # All standard sizes + 128x128 testapp-with128.ico: testapp.png tmp-testapp-16.bmp tmp-testapp-32.bmp tmp-testapp-48.bmp convert testapp.png -resize 128x128 tmp-testapp-128.png convert testapp.png tmp-testapp*.bmp tmp-testapp*.png testapp-with128.ico # All small sizes + 128x128 + 192x192 (excluding 256x256) testapp-with192.ico: testapp-with128.ico convert testapp.png -resize 192x192 tmp-testapp-192.png convert tmp-testapp*.bmp tmp-testapp-128.png tmp-testapp-192.png testapp-with192.ico # $(1) = Toolchain prefix # $(2) = Base output name (e.g., testapp64-with128) define build-mingw-target = $(1)windres tmp-$(2).rc -O coff -o $(2).o $(1)gcc $(CFLAGS) -o $(2).exe testapp.c $(2).o endef # Build with (optional) icon + version resource # $(1) = architecture (64, 32, or empty for both) # $(2) = resource ID (defaults to 2) define build-testapp = $(eval ICON_FILE := $(filter %.ico,$^)) $(eval APP_NAME := $(basename $@)) $(eval RC_NAME := tmp-$(APP_NAME).rc) cp testapp-base.rc $(RC_NAME) $(if $(ICON_FILE), \ echo "$(if $(2),$(2),2) ICON $(ICON_FILE)" >> $(RC_NAME) \ ) $(if $(filter 64 $(1),$(if $(1),$(1),64)), \ $(call build-mingw-target,$(MINGW64),$(APP_NAME)) \ ) $(if $(filter 32 $(1),$(if $(1),$(1),32)), \ $(call build-mingw-target,$(MINGW32),$(APP_NAME)) \ ) endef testapp64.exe testapp32.exe: testapp.c testapp.ico $(call build-testapp,,) testapp32-smallonly.exe: testapp.c testapp-smallonly.ico $(call build-testapp,32,) testapp64-with128.exe: testapp.c testapp-with128.ico $(call build-testapp,64,) testapp64-with192.exe: testapp.c testapp-with192.ico $(call build-testapp,64,) testapp64-string-res.exe: testapp.c testapp.ico $(call build-testapp,32,MY_APP_ICON) # Build with only version resource testapp64-noicon.exe testapp32-noicon.exe: testapp.c $(call build-testapp,,) # Build with no resource info at all testapp64-nores.exe testapp32-nores.exe: testapp.c $(MINGW64)gcc $(CFLAGS) -o testapp64-nores.exe testapp.c $(MINGW32)gcc $(CFLAGS) -o testapp32-nores.exe testapp.c ## BPP tests (monochrome et al.) testapp-bpp.ico: bmp-monochrome.bmp bmp-color.bmp convert bmp-monochrome.bmp bmp-color.bmp testapp-bpp.ico testapp32-bpp.exe: testapp-bpp.ico testapp.c $(call build-testapp,32,3) ## Win16 testapp16-nores.exe: testapp.c $(WCL) testapp.c -fe=testapp16-nores.exe -fo=testapp16.o testapp16-noicon.exe: testapp16-nores.exe cp testapp16-nores.exe tmp-testapp16-noicon.exe $(WRC) testapp16-base.rc tmp-testapp16-noicon.exe mv tmp-testapp16-noicon.exe testapp16-noicon.exe testapp16.exe: testapp16-nores.exe testapp-bpp.ico cp testapp16-base.rc tmp-testapp16.rc echo 'IDI_APPICON ICON "testapp-bpp.ico"' >> tmp-testapp16.rc cp testapp16-nores.exe tmp-testapp16.exe $(WRC) tmp-testapp16.rc tmp-testapp16.exe mv tmp-testapp16.exe testapp16.exe clean: $(RM) tmp*.* *.ico *.exe *.o *.res icoextract-0.3.0/tests/README.md000066400000000000000000000014141521074747500163140ustar00rootroot00000000000000# Tests for icoextract ## Basic tests To compile these tests you need MinGW (x86_64 + i686) and imagemagick. On Debian/Ubuntu this is `apt install gcc-mingw-w64 imagemagick`. ```bash make python3 -m unittest discover . ``` ## Win16 tests To compile the binaries for `test_win16.py` you need the OpenWatcom C compiler (wcl, wrc). This target is not enabled by default as OpenWatcom is not available in most distros. If Docker is installed and wcl/wrc are not, the Makefile will try to build the Win16 test files with a public OpenWatcom Docker image instead. ``` bash make win16 python3 -m unittest discover . ``` ## 3rd Party Credits The `testapp.png` icon file is sourced from the public domain [Tango icon theme](http://tango-project.org/) (`internet-web-browser.svg`). icoextract-0.3.0/tests/__init__.py000066400000000000000000000000401521074747500171400ustar00rootroot00000000000000# stub to make tests/ a package icoextract-0.3.0/tests/bmp-color.bmp000066400000000000000000000060661521074747500174370ustar00rootroot00000000000000BM6 6(    icoextract-0.3.0/tests/bmp-monochrome.bmp000066400000000000000000000002761521074747500204640ustar00rootroot00000000000000BM>(   ?G{1?icoextract-0.3.0/tests/test_extract.py000077500000000000000000000146541521074747500201350ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # Copyright (c) 2019-2026 James Lu import filecmp import os.path import unittest try: import nefile except ImportError: nefile = None import icoextract class _BaseIconExtractorTestCase(unittest.TestCase): def get_test_file(self, filename): # Read/write test files in tests/ folder, regardless of where working directory is tests_dir = os.path.dirname(__file__) return os.path.join(tests_dir, filename) def _test_extract(self, infile, compare_against=None, **kwargs): """ Wrapper to test extracting a single icon from infile, and comparing the output with an existing .ico file """ inpath = self.get_test_file(infile) ie = icoextract.IconExtractor(inpath) outpath = self.get_test_file(f"tmp-extract-{infile}.ico") ie.export_icon(outpath, **kwargs) assert compare_against, \ "Successful extractions should have a file to compare against" compare_against = self.get_test_file(compare_against) self.assertTrue(filecmp.cmp(outpath, compare_against), f"{outpath} and {compare_against} should be equal") return ie class PEIconExtractorTestCase(_BaseIconExtractorTestCase): def test_basic(self): """Test basic extraction cases""" for app in ["testapp64.exe", "testapp32.exe"]: with self.subTest(app=app): ie = self._test_extract(app, "testapp.ico") # Nonexistent icon index with self.assertRaises(icoextract.IconNotFoundError): self._test_extract(app, num=10) def test_list(self): """Test list_group_icons() behaviour""" for app in ["testapp64.exe", "testapp32.exe"]: with self.subTest(app=app): inpath = self.get_test_file(app) ie = icoextract.IconExtractor(inpath) icon_list = ie.list_group_icons() self.assertEqual(len(icon_list), 1) resource_id, grp_icons_with_offsets = icon_list[0] self.assertEqual(int(resource_id), 2) # ID self.assertEqual(len(grp_icons_with_offsets), 4) # number of icons expected_sizes = [(0, 0), (16, 16), (32, 32), (48, 48)] real_sizes = [ (grp_icons_dir_entry.Width, grp_icons_dir_entry.Height) for (grp_icons_dir_entry, resource_offset, file_offset) in grp_icons_with_offsets ] self.assertCountEqual(expected_sizes, real_sizes) def test_no_icons(self): """Test that NoIconsAvailableError is raised when the input binary has no icons""" cases = [ # App has only version resource "testapp64-noicon.exe", "testapp32-noicon.exe", # App has no resource info at all "testapp32-nores.exe", "testapp32-nores.exe" ] for app in cases: with self.subTest(app=app): with self.assertRaises(icoextract.NoIconsAvailableError): self._test_extract(app) def test_fd_as_input(self): """Test passing binary input into IconExtractor directly""" tests_dir = os.path.dirname(__file__) with open(os.path.join(tests_dir, "testapp64.exe"), 'rb') as f: ie = icoextract.IconExtractor(data=f.read()) self.assertEqual(len(ie.list_group_icons()), 1) def test_extract_icon_id(self): """Test extracting an icon by its resource ID""" self._test_extract("testapp64.exe", "testapp.ico", resource_id=2) # ID does not exist with self.assertRaises(icoextract.IconNotFoundError): self._test_extract("testapp64.exe", resource_id=1337) # ID is not an icon with self.assertRaises(icoextract.IconNotFoundError): self._test_extract("testapp64.exe", resource_id=1) def test_extract_icon_id_string(self): """Test extracting an icon with a string resource ID""" # Default index should pick up the icon self._test_extract("testapp64-string-res.exe", "testapp.ico") ie = self._test_extract("testapp64-string-res.exe", "testapp.ico", resource_id="MY_APP_ICON") # ID does not exist with self.assertRaises(icoextract.IconNotFoundError): self._test_extract("testapp64.exe", resource_id="nonexistent") with self.assertRaises(icoextract.IconNotFoundError): self._test_extract("testapp64.exe", resource_id=5) icon_list = ie.list_group_icons() self.assertEqual(len(icon_list), 1) self.assertEqual(str(icon_list[0][0]), "MY_APP_ICON") self.assertGreater(int(icon_list[0][0]), 0) @unittest.skipUnless(nefile, "nefile library is not installed") class NEIconExtractorTestCase(_BaseIconExtractorTestCase): def test_win16_basic(self): """Test basic extraction cases""" self._test_extract("testapp16.exe", "testapp-bpp.ico") self._test_extract("testapp16.exe", "testapp-bpp.ico", resource_id="IDI_APPICON") # Invalid IDs with self.assertRaises(icoextract.IconNotFoundError): self._test_extract("testapp64.exe", resource_id=1337) with self.assertRaises(icoextract.IconNotFoundError): self._test_extract("testapp64.exe", resource_id="FOO") def test_win16_no_icons(self): """Test programs with no icons""" with self.assertRaises(icoextract.NoIconsAvailableError): self._test_extract("testapp64-noicon.exe") with self.assertRaises(icoextract.NoIconsAvailableError): self._test_extract("testapp64-nores.exe") def test_list(self): """Test that list_group_icons() returns the right metadata""" inpath = self.get_test_file("testapp16.exe") ie = icoextract.IconExtractor(inpath) icon_list = ie.list_group_icons() self.assertEqual(len(icon_list), 1) resource_id, grp_icons_with_offsets = icon_list[0] self.assertEqual(str(resource_id), "IDI_APPICON") # ID self.assertEqual(len(grp_icons_with_offsets), 2) # number of icons for (grp_icons_dir_entry, _, _) in grp_icons_with_offsets: self.assertEqual(grp_icons_dir_entry.Width, 32) self.assertEqual(grp_icons_dir_entry.Height, 32) if __name__ == '__main__': unittest.main() icoextract-0.3.0/tests/test_resource_id.py000077500000000000000000000017641521074747500207640ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # Copyright (c) 2019-2026 James Lu import unittest import icoextract.types class ResourceIDTestCase(unittest.TestCase): def test_num_resource_id(self): numeric_id = icoextract.types.ResourceID(123) self.assertEqual(int(numeric_id), 123) self.assertEqual(str(numeric_id), '123') def test_string_resource_id(self): string_id = icoextract.types.ResourceID(456, 'MY_APP_ICON') self.assertEqual(int(string_id), 456) self.assertEqual(str(string_id), 'MY_APP_ICON') def test_error(self): with self.assertRaises(ValueError): icoextract.types.ResourceID(None, None) def test_string_only_id(self): str_only_id = icoextract.types.ResourceID(None, 'MY_APP_ICON') self.assertEqual(str(str_only_id), 'MY_APP_ICON') with self.assertRaises(ValueError): int(str_only_id) if __name__ == '__main__': unittest.main() icoextract-0.3.0/tests/test_thumbnailer.py000077500000000000000000000131771521074747500207740ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MIT # Copyright (c) 2019-2026 James Lu import os.path import unittest from PIL import Image, ImageChops from icoextract.scripts.thumbnailer import generate_thumbnail TESTS_DIR = os.path.dirname(__file__) COMPARE_LENGTH = 1024 # for efficiency class ThumbnailerTestCase(unittest.TestCase): def _generate_thumbnail(self, infile, outfile, **kwargs): infile_path = os.path.join(TESTS_DIR, infile) outfile_path = os.path.join(TESTS_DIR, outfile) generate_thumbnail(infile_path, outfile_path, **kwargs) return outfile_path def _compare_equal(self, im, orig): with Image.open(os.path.join(TESTS_DIR, orig)) as im_orig: self.assertEqual(im.size, im_orig.size, "Images are not the same size") self.assertEqual(im.mode, im_orig.mode, "Images are not the same size") diff = ImageChops.difference(im, im_orig) self.assertIsNone(diff.getbbox(), "Diff should be empty") def test_thumbnailer_normal(self): outfile = self._generate_thumbnail("testapp64.exe", "tmp-thumbnail-test-normal.png", size=128) with Image.open(outfile) as im: self.assertEqual(im.width, 128) self.assertEqual(im.height, 128) def test_thumbnailer_large(self): outfile = self._generate_thumbnail("testapp64.exe", "tmp-thumbnail-test-large.png", size=256) with Image.open(outfile) as im: self.assertEqual(im.width, 256) self.assertEqual(im.height, 256) self._compare_equal(im, "testapp.png") def test_thumbnailer_with128_large(self): outfile = self._generate_thumbnail("testapp64-with128.exe", "tmp-thumbnail-test-with-128-large.png", size=256) with Image.open(outfile) as im: self.assertEqual(im.width, 256) self.assertEqual(im.height, 256) self._compare_equal(im, "testapp.png") def test_thumbnailer_with128_normal(self): outfile = self._generate_thumbnail("testapp64-with128.exe", "tmp-thumbnail-test-with-128-normal.png", size=128) with Image.open(outfile) as im: self.assertEqual(im.width, 128) self.assertEqual(im.height, 128) self._compare_equal(im, "tmp-testapp-128.png") def test_thumbnailer_smallonly(self): outfile = self._generate_thumbnail("testapp32-smallonly.exe", "tmp-thumbnail-test-smallonly.png", size=128) with Image.open(outfile) as im: self.assertEqual(im.width, 48) self.assertEqual(im.height, 48) self._compare_equal(im, "tmp-testapp-48.bmp") def test_thumbnailer_force_resize(self): outfile = self._generate_thumbnail("testapp32-smallonly.exe", "tmp-thumbnail-force-resize.png", size=128, force_resize=True) with Image.open(outfile) as im: self.assertEqual(im.width, 128) self.assertEqual(im.height, 128) def test_192_normal(self): """Test that exe files with oddly sized icons (192x192) are wrapped to the expected dimensions""" outfile = self._generate_thumbnail("testapp64-with192.exe", "tmp-thumbnail-192-normal.png", size=128) with Image.open(outfile) as im: self.assertEqual(im.width, 128) self.assertEqual(im.height, 128) self._compare_equal(im, "tmp-testapp-128.png") def test_192_large(self): """Test that exe files with oddly sized icons (192x192) are wrapped to the expected dimensions""" outfile = self._generate_thumbnail("testapp64-with192.exe", "tmp-thumbnail-192-large.png", size=256) with Image.open(outfile) as im: self.assertEqual(im.width, 192) self.assertEqual(im.height, 192) def test_unsupported_output_size_too_large(self): """Test an invalid requested icon size (> 256)""" outfile = self._generate_thumbnail("testapp64.exe", "tmp-thumbnail-test-unsupported-size-too-large.png", size=300) with Image.open(outfile) as im: self.assertEqual(im.width, 256) self.assertEqual(im.height, 256) self._compare_equal(im, "testapp.png") def test_unsupported_output_size_too_small(self): """Test an invalid requested icon size (< 128)""" outfile = self._generate_thumbnail("testapp64-with128.exe", "tmp-thumbnail-test-unsupported-size-too-small.png", size=64) with Image.open(outfile) as im: self.assertEqual(im.width, 128) self.assertEqual(im.height, 128) self._compare_equal(im, "tmp-testapp-128.png") def test_unsupported_output_size_between(self): """Test an invalid requested icon size (> 128, < 256)""" outfile = self._generate_thumbnail("testapp64-with128.exe", "tmp-thumbnail-test-unsupported-size-between.png", size=200) with Image.open(outfile) as im: self.assertEqual(im.width, 128) self.assertEqual(im.height, 128) self._compare_equal(im, "tmp-testapp-128.png") def test_color_depth_sort(self): """ Test that exe-thumbnailer prefers outputting the icon with the highest bit depth if there are multiple images with the same size. """ outfile = self._generate_thumbnail("testapp32-bpp.exe", "tmp-thumbnail-test-bpp.png") with Image.open(outfile) as im: self.assertEqual(im.width, 32) self.assertEqual(im.height, 32) rgb_img = im.convert("RGB") # The input file has no transparency self._compare_equal(rgb_img, "bmp-color.bmp") if __name__ == '__main__': unittest.main() icoextract-0.3.0/tests/testapp-base.rc000066400000000000000000000007721521074747500177610ustar00rootroot00000000000000#include 1 VERSIONINFO FILEVERSION 0,1,0,0 PRODUCTVERSION 0,1,0,0 FILEOS VOS_NT FILETYPE VFT_APP { BLOCK "StringFileInfo" { BLOCK "040904b0" { VALUE "CompanyName", "The icoextract authors" VALUE "FileDescription", "icoextract Test Application" VALUE "FileVersion", "0.1.0" VALUE "InternalName", "testapp" VALUE "ProductName", "icoextract" VALUE "OriginalFilename", "testapp.exe" VALUE "ProductVersion", "0.1.0" } } BLOCK "VarFileInfo" { VALUE "Translation", 0x0409, 1252 } } icoextract-0.3.0/tests/testapp.c000066400000000000000000000005461521074747500166660ustar00rootroot00000000000000#include #ifdef __WATCOMC__ /* Windows 3.x (Win16) */ int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) #else int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, int nCmdShow) #endif { MessageBox(NULL, "Hello world!", "Test Application", MB_ICONASTERISK); return 0; } icoextract-0.3.0/tests/testapp.png000066400000000000000000001067251521074747500172360ustar00rootroot00000000000000PNG  IHDR\rfiCCPICC profile(}=H@_SR*"v␡:Yq*BZu0 GbYWWAqquRtZxp܏wwQe5hmfRI1_C#2f%) u_<ܟW-X 30muMOeeY%>'5ď\W<~\rYQ3#V:Mx8j: 9U[j_)K\9 "PA6X~srU1 h]?*N{I$8@hhq'@0Izŏm⺭){0dȦJAB7偁[ 7!0R5wtV?KrbKGD pHYs B(xtIME 8QO#6IDATxw|յǿwfvZu*,nnW0`:@ ) )/텚HWSllnuc쬚"Zwv; HF2d$#HF2d$#HF2d$#HF2d$#HF2d$#HF2d$#HF2d$#HF2d$#HF2d$#HF-tʬYE"j)eP %RB!d!HئN!p)?d#PNN+[V.s3 G˫irr(äBP U[An .WKJHȭʌGK)g 9؏ 5R=#otCkkpbș@N?R B+3dҤ+ d|(z)yRڞ};e |d l6,)y<@*y9+%//lܞ,\D"Z$B{G)5:47LS}3u45 VhBHDw 2in~ jp:SVQDqq.9/ Zӕc9Ko]m?o&LBEE!55e F@U1e (*xw%#h2A dXƎ]u[ǶM; I7IJg!"?[ⷻ2O[>+Պ0WYcɒ3?ISPTD^}!&F4HzN@A&>NK yw+^yo%Dz΍/GMM[Rrѝ}js-1vd5iNWV 6E`SX=]v e_"p;Uӆǥq&AZz F! u-yc^}f{v入`ozki_R 5! @V _33ΨSQYn~;: p1i|_+zg";w>BQ΄k,>,;yTE*~ MJm?w/񰪆o}|y:3pdK]~&!-@vg^9'Pv;Rb;;xY8j: oe+Vkp5!@5F0%b(_ aANIIωӮ@됒V~YJ\jY?]m5}/Y,5Pv7%KdJKk CtAEppe( &v;\O iH=#-&LJ. S$7b_FAR=i$5M㏶cK\n!mV-fњ&t/Z .NpVBRJ6'60{8l1cs9q2}Ή YX8 =L I(p|}6,* =\qjPW`'k%7y'g2iҕv~6ٶ5ןň$HUU*HƦf.7͎(#j)?TAEV;U{n񽸲 Nq%5CEaBb{]MM<#:^@[I'|v\Kۚy3-1㆙la#3r耄ՏD"D!"0Z$iF Av6ij&h6‚vwї((KMZctqfrڂ44!/R/m"+0SJɆ ;xٱ˲MBpwy3V|'n;_u1ӆ(]ζ@!4 [WǮ n~YΜvTwpݍʯ~#.BZOBD4hh D ,R(lFTv΃<}+?䉇BA NU"g3Ӂ*|SDMuq."Mi1oGشG{:hGjx_!c#?NYY9A >͐*N='|E:NC0Y\]UKAII}K ؁Ӯui"jMa[4Mr8TVN_s[;2@F_/?\60i| dcSRJB EQpJo^Bc{بiDK!ۥZ[gLAܹ_0`#Bp#`tr/+O!(?nO)yw).nSPDd&_"ɲQ+o =›^ei Ji|k^r NW-{Ze:X pDbn#@`r@-Z]ߎˮ/t{\L16p8NNl?~Рil߾35(Ӧ]3NQC r,f7v>1E#ז]jkh@MaWE'!'e.TE9+- Wd뭺7qX# i{ &==HJr9\0M$/ʣ>w?L RsVke>#2cƒ @$ݯt.pA"hITVUz~= O^)mŢY.  ;BϚ4D ~k1e r=vz,t1M7NUT0!Ch1̐RK(J+V}g}vUVe̺K)oZkXxV"$H\_=?\.'Ehjh 7?UHGp)~]}=<zq<>z|ދ:_["lСwI|YN]udluS|iD"<b7p0m0a-ߔW{nH׏׭^}_(|dҤ+AtvY8ߚhkniʢ#u1}YI*ni)ƏeZϝMaA۹s9UDX{q8*#?_=k/!7i:sO<ջ0DKZz,-IC6\Se|Tқ !.II2hDhinǗcK; ^^}_S>%2eʵ6)VjfvmCC#:jW~lh1|:>xg%^R[nCqĉ& vCBo,[zu?'1d$W $l?2:#Oqۙ6gާSze6 n5Mݯ&H۲lٖ~.ӧ_;R4j& 凷_A~nK0/-JlY{#cVpe7R^UK d$Ďu+?ƤQ0-ISXal;|^+u?^W^AC3S7iB()sV?`0ˮyo3̢.p&Kcb-*6Ep`vl|3/eQг5{]H~aMPgUmv^^/2;w&30v.% .z71v]~|=&/қF$njG1+eO7o%PZZzlAvA98D?|A6rʊw.4g ((4xٸ\.-g8O>1Aǿq3Qըu6*٩U =ٔT_D%ɹ?enǴ473s<\XuV1my?Ϧ[>f\t9g_p)FÓ"igФ4ɰR7>-opL~1OXUX2]ɟ|o~l۱ٵ֥Ҫr󯿃㡡r )@Q`dӦ cڏOiZyAyUv$DfμnY ϼruYL#$ !55=2{~Gs[s/.p;(8TǡQ#UCח2bēwS=x(7r>_^G*$)#x]lo\u7~6lj{^z'OyF¤|&S̀\,JlۜluYTt t3 sWӮ!;/_OQ57t=>wJt8?kZ3p`%%qz|`HKE۝Ǐeu$UVNYs窏2!1ϗ'1Bpgp3[G6rF{rEqy_bSc#bl& -dteUE. )$ckkߺ^6oL._~uLR>cVo>sΙݥ74pnab)+Ȳ7^uy?O@_~X+shǛkF*+>lՃ<3g“m(qBl#O~ϪWsWo #aaEB?9@hO! N>Ҳ ̃}Ga'# )TC߾R#+sS<@!9s&iX8b;Wn1&f-i)87t.g9KRԳ/pwog߁\| ;MOiar "^R VX rhkoaTTV!loj|Dbz<E.%nK1 wrI1k„GbpąQQ J ۹KK0u-*j̾,RD`a* }a뺬;=(Sf/E硪 sDGcZ=nj lQC!l6[ng z{- gVVN[s~"ԅ׍RY*L! .hAцM{Xn=5pWPZ^s#[RS`OKNGȨSRk/=p8KWi3;q8$^c!P$n~~7.KׇT?9Yfkxr)g1rqz~-Mof7({vSϾʡ8ˑ( |aN3]xBD>_ev6bL[&*qGr%]߿?=pԔx?PcѷܞxC* @ ,t3S$Y_U.+@ZCǃ)|D+<{عòX)3$ʧG4R /\ЩK)y'9Kx?eE􃻘4cQ E/8l},Rhnd^"I^}\eBџ_2c'M]HT»7y.".?'x?gRc 9> m=0tь;_~if$A$aOxeO׎&<<Ճ\D/iZ~afcd{I'v-# R$$}c`ABF XvRxĵ_&O?'.O++۝Ϟ}5if`|Y|~{TA0s0r^~iqv\/J~SA4n()㮻ofS;&O Uu!3//[5rt%&'L|Oٱ8 n~lBːP0 4aB4Ʋay9jW[a)hhNǿ˯sN?؋o,=XWfik2w篙0mmm%hƎ7VmOf\ F9b4Qa;quΚ9ޱTwsOv td!3` 'ABI<!! 1w`t٬}kiH8ƶ0}>vW~G NHM$Y{( 7="H B!ò,?kYK)>}W3pH?MSR͹G1K().bԠRpYy'ׇzCz2Zš _Wt 00Wtg J(l і=QP31N #3/.<F/矏>A8ggȨ\n MRR@j)%---D"ּe񅖙!Xh3},;w[1ϼ2vL?.7\}9ӫ]TdbhT|Ie۶lۺ}{vw{{SMFP >UBQ1B>ƦN;$V|zbl6;abu.# !;+^FҚ5[~|?IQP),W./@ MRΥ*/QPhNmlg c^>~`fq?.Y.UN&t2=7`W^1%6)8zI޳RjRǬPʟ|RBD%Y(t98Pz ."'(rLj3Y`П[& Q~7ؽF^7a,: A@ktgqf>kӧ_(7#KeEgv N "^ ;^kKXca ]Ԋ&S{y.fuؽc+—_@y {t[TT$2nW sB!05Ӣ`PfRC0t x`(e)G|9JJ&eϞ8nal3jdqpNl iWq9"gQ61,b G(zg"tb+PY>u;غ%:tΕȄӯ_$Լ 0a\JxIT%هV9vUkC>V"7IWk#t!lZ[Sz+u" #ʳQ# 8~d!S|\LĚDScw\.kkk#XnwSTditΜ>}EUM;򪪒^]ZR{((pc(`ۓ#0_ڌWOu"N//}w׬+pS'm}ig~wmnn6d@5; H4'A>"ߟ_P?yr  _!Cگw|b Dd<v7u_;ގT9ʋsG`,jO` RVVʕW-DUfe_2]J9S;gaS ,g7qybZz뚌TdQ]gÆMm\KNQ~~]-j}nn.v=9ajl⊟b ltG[ǎaT~!?Ǵ7SOSWWoX^WW;! 1dHξ̟=܍. Uwz.HxPʮ:0>:5_羻~$ƌ_(wOߧ[fowinn&???e}qq!q=bޛg<42k5y(/?Zn>S={ɧAjP;gE%m9L< /ZfW vG'O`KpM XߕDs /ӄZ_ImوC1cd Y#uj=BaץtWnU/[vgǬ?0+:,$ѡO>~^^d΂E|9tiC6YUel޲%,9@EtX/ftDa4/:dѧkSC bTv 1ێ۩UksV~˕l*pʖ5d<DS֚M=ݻyw{8N*R1r yK lp{?NCt:xM79g\Z}ϟqvE0i#)/@Aaaׯw,_v\%(Ц͆hPY )礓syYϝ01#붑j}];G#D/Qc[_<E ̍@}o7?˶;>"Ow|MhiiK/97_QմcMg̸~i^~-1oL?7r o,}ۄU1Esȵ]:ziz&C ?;=bGafcdێ]ЕvluRB99O$43@n 3zcJ~{ٳg/]}#v5 !![tLdчn fl; Mgߖ`ڴ&IIJm﷾u̕PwK}o-O<4'v6CGtIfR5hp!cma."9'@x P HG, :/$ rKN&JttΌ-h Μ98F ٷo?˖kJsy}Dv?Bdɻ#U8@8"iSO_]l;`IQVgQʏR[{pM׃G%ŹǬm6lȦ͟qf6m'?N$UfԢLa_]"8Ay߶}H}}g,6Ni??0>_g.>GaL@OX엓CVVVopd l]o☩D"_̚5ϕ/彵կ KU`j2mΉT LɀjEyQWZ@UO\ Z*oCAEgKKy`=vDUUJʫ9ulr0~hٹ+.:wVV~_~-- yp 1My9`̫m VzTU9W]֟މ]G*֜XA~ϤnJM,Gϣ~gwm̹cXYѭsߝc|>n&@4 |D1|68)יEJ~u]7fT4ޑd"$HBZ}iMxJM'?e&j,rhm|~ϘLAdyص}u Dk*?S$ܹ@~ټkOXg{WYACu`X\I\눠2s!95{U_خ+*:ulDr quЖ幹9,X8!g| O 22vbܹsyo6j '.>kV8cXk +xG4sFⱿJ#MnwHK z}܋1ExSs9iln^i~fH@)I}~1ül}Y֬YC'qʙ%Hn{^@vmMg:ڭTV e",@G&qS0$Bgq3?GCk4~Z;u5&-WBg tttr UV1&#ez`֬'9vN:ok{+s."O_+5__O/Maee'A}rsN<)O_&et$ >2~?&͢5hx.;o/})b'΢Ksɴ0f-H$rd'pN/xG9|[?',J%?` ӾONZݽ'zŸ3T㴱}nP5d4^_>0Oiv#$ J wwvI5⡀H 0L43 > ]XJ枸j^ (MWDf2PW-ߧeZWid6~νr\v²hGvbs$z59bE:>wԔABB S|a2c?3Eaduz{)]Ra^{{=?8x.--9pɳI!x{;hhj5UÌs@nm|:vMN^Qqb}Gjoo4dE>J𨑀N>y1H>,SjIkF0O/K8\%e2$~ǣ/3N΢y1:f;ձ#ڤ7+5##d:ؼ "Fh63tn6l|9^rݿjFGGnq>x/}@<0DN< ^}c9qقE K"Dt{M!A&-nZ#fM@qIsO<)%6a{+Ï8n\xCiWȲ8m,GtZ,J{ {Է`" E0p|Ԝ ) @C{FHOAA.W^rgvaBp)sy 8^&Mի }`릂/5lH)y'cn+$ L)>t )08z V_@SSd$(NGW8i_ S^M*y"vܲ! sE{7$<ar}C|! !%.[l!ۇ ʧL'%oHCX' oSvǢB!"{~q+v4)yb.F v@ 0,/).BzEcTǙ1cu{`(σ7*kX%#D]PD̛?eLSM}}%Ct1: si&CT!a1T_RW:7w>06U0܍&hRO<z|^ [e"ONpr[sf?6y_J|)5 !&XffDCPiARހ)߹!KnK\sbj' a&@ ;O}dc]c;P]{S#%Lt0T 2Nnko:~ ٹKeTt ŕԞnǛT4@ `89>`ͲkR|kuLLlf_LI4!6t+p D2e(6m OP3%@u|3 *m `trƦ^[6%՘o֬kdeWUtm۾ JlC<5M1%Hi .>}vT1!IX3ZFAb/CNr`8$ii)^B, eoIAijj{HV@Ԕ,rvLHӏQO[Cn o$&jhq-1YmWHy1)>ưgVag/_'`G-"=#1R(t챴=@D¦:&b>Xe;0_t^wm۱_aJ[zRNzW_Z)&%m!I$T8%&!-} "6XmzoJ)=WUjDȹe4̟>+2)cG 0`"[>Z%(Rynށ/Ғzvfedij4MZXo j$Yp P"j[@I2LN C\N)~qp" nák5;?N^˶𦵁@w'7K/gDasI" < EH^iX2UQ!KsW#T!(q|ꔿ0wf7$Bw@Km]G$bx(!0}\:ZZRR1,]RwׄYqҥ&' WLM."uOCJ6c@dj eOC%#pQ~u/N!mO>v@B N'pdYW,,ڸqW GG[q`T9~d> sarV\#:쿦IK<Ҩ "%@H~ԁ~uk4 n N(BZ+KRbSUF[Y[kqx5H PGtY*K/CCckhKc{.eO~5WԐ[%0Ή?IͬtIE4ڃZBL->)D@!(!NAz}MAS>7[ߏlٮ .PL6v=Q۾NeU!94#Bڽ܂nyMPHubqBry(,i DQɔ iҍT^Ң,EZwnx o_1O"zsS3m>e9/{76gatv! %ꧥ= 0QRn QUŸ*ngj;{2P2WWߟhEΔA6B!녈_OČ@[DP[rL9#t̓yÒn?ʛI3@dr:wG黫mfOɛw-#a3N,5(5C-x4B7pXOmRUUihdLM1-g|Koko.scAt_jEH ,<4\GJlagSRϛm<4"+os҂~"}mC -E[s-r"@08h>>v!ۄ4hb[_&HgPt@qg1ˮ@7N <eR3'D?"!}@@U6%^ Sb!ʠc1̰,g .țZ8{ra]{ݟE{(GLB!9wOɑL*P%kk%[ҵiwݵ&׶dhddQ$i8pr 3ЍF@*@aAuP͢߁-D6ȲGMRRfsB 4Eΰ{4Eq0_g3xߍLljE$2D@1„ )2qQ$_IBtN- Dnk4SR5>y^|ہS#1~|MEWD$A'@_~!$RVݝ^ esY:=f;zw܃2 Bx5<o{D/ʊк#ݰ!ʏosIqaP%,FD5!,#)iKd1Xrʾ)qt5֕_ 9TXׅc-n7iv 'Y?b`pF 5Qg?0  '@4 +hk{?4urm9|T{D&>oN`8l@6'$A.Q>)h2o\0 $g6ɘn)1Ha(,(O 5IjeOO~=#pa``xoLFhx2koc@HgB \~`- UX4$>of;k1bqJ{3onc۽swoi},z"+$ʡm(RϿX+>ZzrzSGs8Lbf!.$%_f6}X9XLMF ‚b@_u^JJ +eF1$:D|43#[U㰿 y]t?nv;M><ܜXnblЉⵯDR^A8,/(4 _}ODwW~؁B)i5վ?3l8(ȮcP8Hg4aB-Z4t*gǺZ+BpI87zEac%?;A2]ħ ,&>JW']x!afۼuysӂPp^e'Ѻ H&{_d+d^T9!y^nj_k똞 ӹ ^%v8 &,y`YfTz?3_tz-F?Q'Z_ G?A$/JR=_z0p}qAIp^pqLDz 48%i|^Lsr$<$^H@e(*$={uсP8ۣطgfWGFo=Ζ9p nĢq5fQS `EL'@V, ìV%s_ouxm`Wrm߬q&f`|%ڈI[6J-"ZspxaoJmm|^|cSY̅3Әf Dן 7mbM|*pЪ{-G 6wO{InA{k&&Oփ]|GOx$(nCسsۊ@-͈s oۧ~X`bvw;z%)ߞ*^ {=lOgj>qi^?b,ی:}-F s~xhyA4Dzq bu!BB00㧇/+?|gV;u gxٕ3CܼrM__{'ڛVWS+x XM0;ij)+ Q/;/4O#Jϕ$ٗ+f~ӦGl>KKz"mR /ŀЍwuX-Ĉu RT>$BxE#^?v?O}9<̳hَſNoD$C0re>x0Y%l5cy8gLp;튾z\@Y? DΏ&X0'לV֓a˲@xZ\e5&ըCŎ oc570xݲߙޘ4yֆN-._@3y~|No(O»KχS,H3*MS5c&,mo~NgfέvL2 5 ut+lV坝  BoX ,O3F48VхD_DFEN7'@$?@d&Pr Qj]{\YBqX%=+h8B֯pؓoLJJXI2 mx$%鸣cu~)TJxAe4Ha L%Vn[Ձ%JR_$Zv)V#/lowKoÏav6q˽”Ex]Q!󳋴[&a25[ț8)=64x[|Q ױ*ʫJ:\v9bF"thJR52Iyr6TZ|$<î $-|1Xy 'dl20G-?S诹~ q\zl0E:&JnBm`:Vlֿe Cc;J|te ʽ7%Az&+ZM `w&8K*& y$V@4@ \& OcC@5;OڡcPI<㯿}0X\n.?uYe?H"KҴLTʖdXi|B|6'\>{C+r]ö6Epy,t,M *zi2K!D(aJ/`6Yj zW1rg ٤U^[0 ҟf?yz{+j9\ /ױc70sy_V mm=Ќ=]v,(-//) Ӌy@L@&jBФSN&x, aUWU_GsJץh`5`1E p?I1XH_Pȷeݹkf%u-PCsB0IDe٭f<_z{:`PJ! %2T\+ /=&ivj* C 0BHUேWy>f_RpMynh\qx]H01O] sIb@YLޖfƣ;@ :`*yZT@a!Lܸ5bÄI Y2P`a-N8p5o4y p0$0]C3g@ilxmٴZXhR^jt5Z^W^#hnng3 ia$Wg*id(3Q &<"ގ;cxz}Mr_˴^&"=Ij!^z)M񋧮o(MxR;mӱXc[ JI@G AJ0$ikÄO,^(~ W[䔰Zf/BTى_O5Pid* ehOͺՁiy6||w3V.t5:_Tc2A J=o',5- 0ĉsU~5@%__^Z0u"?0T._).CE.f48M(7 Ju#-hv%R[ib^XC%*B/˫T_NP ܝ;)4E E!B0=6G? ˲upȓ@ 8y)ꒃ!hva7snZM^'n&n~>W~(}Dʍk/ u zh6dNgcͮB;qOݙI cS~.-Z"{Uv3Wyh~OQN a0M/[5UXnԱP~-_d txD S HA2{TnnX+d9N no5rhvN)ZFgn$YXnS9*ٹ<%kpm[ȁ4%X ˕ZΧ_OI8,[L4ST0 y%S7O{LNP} ›Ez㬷f1矇aH9B! dYJՄ, _M/)iKy+P\1S@jxh(\&4̉ߺ!B#tgO%B{*}{fP"P۴FV>??G0OOJʃAyTbHǨ2L:KsJ"oM~Au }fO%PP,=2<\u(G(qIz`u7R~e(M{0 !HmѣcPwf@5W~6`Kwk4Q EQ\8{]_>jK^9v-~;w;`E`-k?Uι9|_3hG/Sy ZhsjIfGYӫVNAɗ7A+BK  ԝYģ?d=:A& nOHטIJ8z=,-ifKW O E)N zT@H!ȗsjM?::lk5ijQτ!Br"KY,Ir< y+5#{xt^'S4l':)_99`&ĤLOPr@$,&/7ZX~~Ѿp7uA8'H4%Ha`hЦְ칂C0!y_ }ƪwk%Bh ]m^7v012ZvS4TXm|׎"H$9AGxigxǥ:$~>Bs?P/PM}uLZSPbXlj[W?DoWsk._Z )O˒Q:>/覣۶4s<\.J/ޗs I4aCpҾ9Fs3Ms#8 A'nA)ģIZw]Lдp8WD쀯>Lx?zݪc%7D2N& 矧w,\XWԞ&A@<_| 5f5+ !H'L&ۚ##GKBpWn(0ۚ ryPYYV<#fzдpH#8r4Wj>@M6PT{1cA!x"}ӳ?T$ґIC%CwIgh_9]Z^0=L_h̀nIX *  솎edE?_BQ3ϝ9g%7/CZ? 7'f-'Y6O. 2ZU_"[/HDjvv'Oyh_Pϟww/Fpm736vLZd%f:J&CvQ^-Fe&NlmRTҦs18_ @_7y>$O`Z}$Jv6Bx\WT]Mx0!ȠsiV 8^֞vȋ}4I?]ri,e`gf.OdYvLS_XR I4hDnH{p[:-BBoxڷ,! ^4XF}HP @zvL]BV3;010KsAׁ[KK Fea.s*2 :K{halB0X-hly 6bɬ8@ 2YnU]f hO~ p;>Ez4HuW(Ɂ!=o 'Xuhrj>[H. <ƃLR[Ë?><7&i>BɘWւ (h<L G_=ɀa<F-SRa9Wc d2/Dc|bPKTī %* Cd$ C4p`2)̆[x^xp,;{~%?Mtl_˽n _WN :ҲɲR9r6v^/Eӌ0bL F"N0؊B0rg}p/k|~ٹ< &;C (;@y9CjIwH}))Yn1u0rk׮_V?;qˀy/?r^&…<d5&/?}a)NпgK乷H<4́`1+D_x+囘Oakن.n$vq<_6+W"u0gA<\%!thuqu3r^x̰:FMmhor_ .A7d2d~ֿ81֦ @$C m晅'j >fkМg<iw)Ct)ةiLAEK_F YМ|(\a?ⱻøC0Jsղ-[(l.|kcY>=}aBɯ7[]3 As`}BƓ#5sfP@0>VJPx#^AO~p ؾ4 '@ZAX5!Q&sڬdآ8/bhd =h=AKtBzvzG~wiEj`DssCMM:aGcK7lw:|6bii(T:ÛgVj 0:[@)ȥ@d;ʢM_APHB|!#2\;j4n6Sr3 ܂ Â{􁣀?{W$(~><6޿ jI"M12 :/ $t:q9Lcˮ`Yܳ ~xSA'?WCCo_WM#ЭA_1ߚp:ۂFABΣoGKu5IdyNBǁNc],Cx{?u+lv/ )Jj WӜ{tS" TS`:cyՖHLeqǾCe![t&f _Y&&%2k7}-v޴C዁ !aXh!-ƋP-\1TK !?0.]F}_;C6 i"t jD] >R_xx`[qYYd {%,x=wgN#<+g1=:G__gV5 X- WD33|[}zi  w[oXa1r\m wz@CQ>h! Pyg䪊7p}h/c]:1.?]x/dIiO_7P ȫ|]?аO3|c_a1цR@j3- J?~hm]rNK!4?[(qȉC.eZ@ݧ9HX(Uq8^o}гi ': \:+WXJX J`*yկ#$'=͛8&s,1zs ][a0!#@:ljuSxyPp1g{ ӪBS+Iy)z9Yr>*iv)@ r,XQOSg/C[6=lZܓ|pp?OI`%Tnzn.!ssx;8)RXW$,à`o }ouoMfS HD ءL DT NA=2 ADJ|8'?I%a& #y?lr֭_< =`OO$1qQtоb S'ofܠL&l6dYҀt~E8VWFW"j6!z.p(PPhswv vsE·qU-*ޤDի?F$2 s T Vx> 3!Ż`d>Q\Fk nC B,l6, 2 b8!8,V"'QPV.")=H.]3qXw{:tG_t7`02rm|(4ˁ KW!uC02enXVgׂ?'o Fjt:XV;u. uP+HuT(]*iЫ!wU |.fSAsA\8>ԂJ881qꙑ秐@V`݄2:ht?ew?o|vvoh0;{s¼W03-r:I@:ÁTPMWz]V7*~=n^9իH/ݸ=Wsx!͡_yſsTvvv<fֆ iH462 ENP]h21l@O |Z4YS NB'2I@7DSpAʆ== P 4y߉M]MES /RMkYd~68l""e$0Z0eǦљv{K+ǙF,:8"L!O`tl Lm>;z[]s 2ٜh$r`+:PL gB |߉V/Lx5`z̏ 13WͬL"*J |_j@ ځa"板DzMsؼz<վLL"5)$ckM "eLY Wfs@Cz_@yV4hߝD}^2U#𯆆9jjc]kbS0jufMnlӉ3dZm39L" a:D+h +˃&Áw;,hip A/' 0bA%'b4: ZƺPc]+mAmkں gj+O66`hi2eI aB 9QY_E HD?exV4zhmphڂ?ȵ z}fsSS_<{/W37`Vغjkl-6 nC߶\%.rsQL""KR 5Od^VL:Pzl ,FG9ͦZpR]1֞~h}X)Z \iGn 39cXc1/85^5ӱpMp;88,빢'H~<LܚA:p[px[L.F4PkmMKHu-dlM>p4="+ m شk:z[%Z`'tM&&&KO\zRHAN Z@.<&nM}+Fgώ3%zspVP/ Z6v{!D^vtn@KGXrWbXOxJb2k*%R[ږW;7viGϷeڸW;kLaYXXVX&XlVFz zMz / _JMH3Ȥ2HH'3HHGGضDQgfG )Jh!3V kKfOydٖ}O>ŻL,dFb. %}0\uyv@E_5dӗh\%.Wo5o1]Fg]!|:Fg., Jzjgkk|,kt9dwJ\kT*r'Ocl" zZ&zYMpa;|EЗ\&jڢNbT(JŤS4ʣzh+A$^W-y FD%Y (_xz!֤B՘ OZAϢ2\'Ƞ ԛjsʛzkJ}5U$T J@ @]K" kUݿ @?ʯFZZJ@M0wZ/F/Wc3j́Z ,GݼV -PH@du h*5 jA cjhk5!Z6 *%ށaZ Hzs _PCp%-ZV*_䯅Pi$@K^@}5ȻAk[+X_UeZJoUDXNxVjT_.(X\Cqo5RW4 \_j+6r{^2kW+ۿ0`=C}coQQMk zP:P+m`cm kg  k,!TWBJjT[Pk7nP_M(p9 ɾAe9 \EA6ArF2{k j;6`c|kcm6Xkcm% 2{}IENDB`icoextract-0.3.0/tests/testapp16-base.rc000066400000000000000000000014261521074747500201250ustar00rootroot00000000000000#include 1 VERSIONINFO FILEVERSION 0,1,0,0 PRODUCTVERSION 0,1,0,0 FILEOS 0x00010001L // VOS_DOS_WINDOWS16 FILETYPE 0x00000001L // VFT_APP BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904E4" // 04E4 (Windows ANSI) BEGIN VALUE "CompanyName", "The icoextract authors\0" VALUE "FileDescription", "icoextract Test Application (Win16)\0" VALUE "FileVersion", "0.1.0\0" VALUE "InternalName", "testapp16\0" VALUE "ProductName", "icoextract\0" VALUE "OriginalFilename", "testapp16.exe\0" VALUE "ProductVersion", "0.1.0\0" END END BLOCK "VarFileInfo" BEGIN // 0x04E4 (Multilingual ANSI) VALUE "Translation", 0x0409, 0x04E4 END END