pax_global_header00006660000000000000000000000064147611650300014515gustar00rootroot0000000000000052 comment=fcc77171a4611d385072711e46b82a94a8759fb1 icoextract-0.1.6/000077500000000000000000000000001476116503000136665ustar00rootroot00000000000000icoextract-0.1.6/.drone.jsonnet000066400000000000000000000052421476116503000164600ustar00rootroot00000000000000local volumes() = [ # Use this to cache installed Python code between steps { "name": "python_install", "path": "/usr/local/" } ]; # Pipeline template local test_with(version, do_deploy=false) = { kind: "pipeline", type: "docker", name: "py" + version, steps: # std.prune removes skipped pipeline stages, since they evaluate to a null element std.prune([ { name: "install", image: "python:" + version + "-bookworm", commands: [ "pip install -r requirements.txt", "python setup.py install" ], volumes: volumes() }, { name: "test", image: "python:" + version + "-bookworm", commands: [ "apt-get update", "apt-get install -yy imagemagick gcc-mingw-w64 make", "cd tests && make", "python -m unittest discover . --verbose" ], volumes: volumes() }, if do_deploy then { name: "pypi_upload", image: "plugins/pypi", settings: { username: "__token__", password: { "from_secret": "pypi_token" } }, when: { event: ["tag"], } }, if do_deploy then { name: "doc", image: "python:" + version + "-bookworm", commands: [ "pip install pdoc3", "pdoc --html icoextract --template-dir pdoc/templates", "ln html/icoextract/index.html icoextract.html" ], volumes: volumes(), }, if do_deploy then { name: "doc_upload", image: "techknowlogick/drone-b2", settings: { bucket: "jlu5-ci-doc", account: {from_secret: "b2_account"}, key: {from_secret: "b2_key"}, source: "icoextract.html", target: "/", }, when: { branch: ["master", "ci-*"], event: ["push"], }, }, ]), volumes: [ { name: "python_install", temp: {} }, ], }; [ test_with("3.8"), test_with("3.9"), test_with("3.10"), test_with("3.11"), test_with("3.12", do_deploy=true), ] icoextract-0.1.6/.gitignore000066400000000000000000000033301476116503000156550ustar00rootroot00000000000000# 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/ icoextract-0.1.6/CHANGELOG.md000066400000000000000000000023051476116503000154770ustar00rootroot00000000000000# Changelog ## 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.1.6/LIB-USAGE.md000066400000000000000000000012121476116503000154540ustar00rootroot00000000000000For 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 except IconExtractorError: # No icons available, or the icon resource is malformed pass ``` icoextract-0.1.6/LICENSE000066400000000000000000000021701476116503000146730ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015-2016 Fadhil Mandaga Copyright (c) 2019 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.1.6/README.md000066400000000000000000000053461476116503000151550ustar00rootroot00000000000000# icoextract [![Build Status](https://drone.overdrivenetworks.com/api/badges/jlu5/icoextract/status.svg)](https://drone.overdrivenetworks.com/jlu5/icoextract) **icoextract** is an icon extractor for Windows PE files (.exe/.dll/.mun), written in Python. 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 ### 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)](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] [-v] input output Windows PE EXE 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 NUM index 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.1.6/exe-thumbnailer.thumbnailer000066400000000000000000000002771476116503000212210ustar00rootroot00000000000000[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.1.6/icoextract/000077500000000000000000000000001476116503000160335ustar00rootroot00000000000000icoextract-0.1.6/icoextract/__init__.py000066400000000000000000000150701476116503000201470ustar00rootroot00000000000000#!/usr/bin/env python3 """ Windows Portable Executable (PE) icon extractor. .. include:: ../LIB-USAGE.md """ import io import logging import sys import struct import pefile GRPICONDIRENTRY_FORMAT = ('GRPICONDIRENTRY', ('B,Width', 'B,Height','B,ColorCount','B,Reserved', 'H,Planes','H,BitCount','I,BytesInRes','H,ID')) GRPICONDIR_FORMAT = ('GRPICONDIR', ('H,Reserved', 'H,Type','H,Count')) logger = logging.getLogger("icoextract") logging.basicConfig() try: from .version import __version__ except ImportError: __version__ = 'unknown' logger.info('icoextract: failed to read program version') class IconExtractorError(Exception): """Superclass for exceptions raised by IconExtractor.""" 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 IconExtractor(): def __init__(self, filename=None, data=None): """ Loads an executable from the given `filename` or `data` (raw bytes). As with pefile, 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. 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: raise NoIconsAvailableError("File has no group icon resources") self.rticonres = resources.get(pefile.RESOURCE_TYPE["RT_ICON"]) def list_group_icons(self): """ Returns all group icon entries as a list of (name, offset) tuples. """ return [(e.struct.Name, e.struct.OffsetToData) for e in self.groupiconres.directory.entries] def _get_group_icon_entries(self, num=0): """ Returns the group icon entries for the specified group icon in the executable. """ groupicon = self.groupiconres.directory.entries[num] if groupicon.struct.DataIsDirectory: # Select the first language from subfolders as needed. groupicon = groupicon.directory.entries[0] # Read the data pointed to by the group icon directory (GRPICONDIR) struct. rva = groupicon.data.struct.OffsetToData size = groupicon.data.struct.Size data = self._pe.get_data(rva, size) file_offset = self._pe.get_offset_from_rva(rva) grp_icon_dir = self._pe.__unpack_data__(GRPICONDIR_FORMAT, data, file_offset) logger.debug(grp_icon_dir) if grp_icon_dir.Reserved: 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 its data and save it. grp_icons = [] icon_offset = grp_icon_dir.sizeof() for idx in range(grp_icon_dir.Count): grp_icon = self._pe.__unpack_data__(GRPICONDIRENTRY_FORMAT, data[icon_offset:], file_offset+icon_offset) icon_offset += grp_icon.sizeof() grp_icons.append(grp_icon) logger.debug("Got logical group icon %s", grp_icon) return grp_icons def _get_icon_data(self, icon_ids): """ Return a list of raw icon images corresponding to the icon IDs given. """ icons = [] icon_entry_lists = {icon_entry_list.id: icon_entry_list for icon_entry_list in self.rticonres.directory.entries} for icon_id in icon_ids: icon_entry_list = icon_entry_lists[icon_id] icon_entry = icon_entry_list.directory.entries[0] # Select first language rva = icon_entry.data.struct.OffsetToData size = icon_entry.data.struct.Size data = self._pe.get_data(rva, size) logger.debug(f"Exported icon with ID {icon_entry_list.id}: {icon_entry.struct}") icons.append(data) return icons def _write_ico(self, fd, num=0): """ Writes ICO data to a file descriptor. """ group_icons = self._get_group_icon_entries(num=num) icon_images = self._get_icon_data([g.ID for g in group_icons]) icons = list(zip(group_icons, icon_images)) assert len(group_icons) == len(icon_images) fd.write(b"\x00\x00") # 2 reserved bytes fd.write(struct.pack(" 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 # Note: 256x256 is the largest size supported by the .ico format if size == 256: # A large size thumbnail was requested. No downwards resizing is needed, so export any icon as is logger.debug("Writing large size thumbnail for %s to %s", inputfile, outfile) im.save(outfile, "PNG") return # 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)) logger.debug("Writing normal size thumbnail for %s to %s", 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) generate_thumbnail(args.inputfile, args.outfile, size=args.size, force_resize=args.force_resize) icoextract-0.1.6/icoextract/version.py000066400000000000000000000000261476116503000200700ustar00rootroot00000000000000__version__ = '0.1.6' icoextract-0.1.6/pdoc/000077500000000000000000000000001476116503000146135ustar00rootroot00000000000000icoextract-0.1.6/pdoc/templates/000077500000000000000000000000001476116503000166115ustar00rootroot00000000000000icoextract-0.1.6/pdoc/templates/config.mako000066400000000000000000000002671476116503000207340ustar00rootroot00000000000000<%! # Configuration overrides for pdoc show_source_code = False git_link_template = 'https://github.com/jlu5/icoextract/blob/{commit}/{path}#L{start_line}-L{end_line}' %> icoextract-0.1.6/requirements.txt000066400000000000000000000000311476116503000171440ustar00rootroot00000000000000pefile Pillow setuptools icoextract-0.1.6/setup.cfg000066400000000000000000000002271476116503000155100ustar00rootroot00000000000000[metadata] long_description = file: README.md long_description_content_type = text/markdown license_files = LICENSE [options] python_requires = >=3.8 icoextract-0.1.6/setup.py000066400000000000000000000027631476116503000154100ustar00rootroot00000000000000#!/usr/bin/env python3 from setuptools import setup, find_packages with open('icoextract/version.py') as f: exec(f.read()) setup( name="icoextract", description="Windows PE EXE icon extractor", version=__version__, url="https://github.com/jlu5/icoextract", author="James Lu", author_email="james@overdrivenetworks.com", license="MIT/Expat", classifiers=[ # https://pypi.org/classifiers/ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'Intended Audience :: End Users/Desktop', 'Topic :: Software Development :: Libraries :: Python Modules', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Operating System :: POSIX', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', ], packages=find_packages(exclude=['tests']), install_requires=['pefile'], extras_require={ "thumbnailer": ["Pillow"] }, # Executable scripts entry_points={ 'console_scripts': [ 'icoextract = icoextract.scripts.extract:main', 'icolist = icoextract.scripts.icolist:main', 'exe-thumbnailer = icoextract.scripts.thumbnailer:main [thumbnailer]', ], }, ) icoextract-0.1.6/tests/000077500000000000000000000000001476116503000150305ustar00rootroot00000000000000icoextract-0.1.6/tests/.gitignore000066400000000000000000000000531476116503000170160ustar00rootroot00000000000000tmp*.* *.ico testapp*.res *.exe testapp.rc icoextract-0.1.6/tests/Makefile000066400000000000000000000060601476116503000164720ustar00rootroot00000000000000CFLAGS=-mwindows -g PREFIX64=x86_64-w64-mingw32- PREFIX32=i686-w64-mingw32- all: testapp64.exe testapp64-nores.exe testapp64-noicon.exe \ testapp64-smallonly.exe \ testapp64-with128.exe testapp64-with192.exe # icon with standard sizes: 16x16, 32x32, 48x48, 256x256 testapp.ico: testapp.png convert testapp.png -resize 16x16 tmp-testapp-16.bmp convert testapp.png -resize 32x32 tmp-testapp-32.bmp convert testapp.png -resize 48x48 tmp-testapp-48.bmp convert testapp.png -resize 16x16 -depth 8 -remap netscape: -transparent black tmp-testapp8bpp-16.bmp convert testapp.png -resize 32x32 -depth 8 -remap netscape: -transparent black tmp-testapp8bpp-32.bmp convert testapp.png -resize 48x48 -depth 8 -remap netscape: -transparent black tmp-testapp8bpp-48.bmp convert testapp.png tmp-testapp*.bmp testapp.ico # Small icon (only up to 48x48) testapp-smallonly.ico: testapp.png convert tmp-testapp-*.bmp testapp-smallonly.ico # All standard sizes + 128x128 testapp-with128.ico: testapp.png 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.png convert testapp.png -resize 192x192 tmp-testapp-192.png convert tmp-testapp*.bmp tmp-testapp-128.png tmp-testapp-192.png testapp-with192.ico # Build with icon + version resource define build-with-icon = cat testapp-base.rc > tmp-testapp$(ICOSUFFIX).rc echo "2 ICON testapp$(ICOSUFFIX).ico" >> tmp-testapp$(ICOSUFFIX).rc $(PREFIX64)windres tmp-testapp$(ICOSUFFIX).rc -O coff -o tmp-testapp64$(ICOSUFFIX).res $(PREFIX64)gcc $(CFLAGS) -o testapp64$(ICOSUFFIX).exe testapp.c tmp-testapp64$(ICOSUFFIX).res $(PREFIX32)windres tmp-testapp$(ICOSUFFIX).rc -O coff -o tmp-testapp32$(ICOSUFFIX).res $(PREFIX32)gcc $(CFLAGS) -o testapp32$(ICOSUFFIX).exe testapp.c tmp-testapp32$(ICOSUFFIX).res endef testapp64.exe testapp32.exe: testapp.c testapp.ico $(build-with-icon) testapp64-smallonly.exe testapp32-smallonly.exe: ICOSUFFIX=-smallonly testapp64-smallonly.exe testapp32-smallonly.exe: testapp.c testapp-smallonly.ico $(build-with-icon) testapp64-with128.exe testapp32-with128.exe: ICOSUFFIX=-with128 testapp64-with128.exe testapp32-with128.exe: testapp.c testapp-with128.ico $(build-with-icon) testapp64-with192.exe testapp32-with192.exe: ICOSUFFIX=-with192 testapp64-with192.exe testapp32-with192.exe: testapp.c testapp-with192.ico $(build-with-icon) # Build with only version resource testapp64-noicon.exe testapp32-noicon.exe: testapp.c $(PREFIX64)windres testapp-base.rc -O coff -o tmp-testapp64-noicon.res $(PREFIX64)gcc $(CFLAGS) -o testapp64-noicon.exe testapp.c tmp-testapp64-noicon.res $(PREFIX32)windres testapp-base.rc -O coff -o tmp-testapp32-noicon.res $(PREFIX32)gcc $(CFLAGS) -o testapp32-noicon.exe testapp.c tmp-testapp32-noicon.res # Build with no resource info at all testapp64-nores.exe testapp32-nores.exe: testapp.c $(PREFIX64)gcc $(CFLAGS) -o testapp64-nores.exe testapp.c $(PREFIX32)gcc $(CFLAGS) -o testapp32-nores.exe testapp.c clean: $(RM) tmp*.* *.ico *.exe icoextract-0.1.6/tests/README.md000066400000000000000000000005141476116503000163070ustar00rootroot00000000000000## Tests for icoextract To compile these tests you need MinGW and imagemagick. On Debian/Ubuntu this is `apt install gcc-mingw-w64 imagemagick`. ```bash make python3 test_icoextract.py ``` The icon file (`testapp.png`) is sourced from the public domain [Tango icon theme](http://tango-project.org/) (`internet-web-browser.svg`). icoextract-0.1.6/tests/__init__.py000066400000000000000000000000401476116503000171330ustar00rootroot00000000000000# stub to make tests/ a package icoextract-0.1.6/tests/test_extract.py000077500000000000000000000041671476116503000201260ustar00rootroot00000000000000#!/usr/bin/env python3 import filecmp import os.path import unittest import icoextract class UtilsTestCase(unittest.TestCase): def _test_extract(self, infile, target): # Read/write test files in tests/ folder, regardless of where working directory is tests_dir = os.path.dirname(__file__) inpath = os.path.join(tests_dir, infile) target = os.path.join(tests_dir, target) ie = icoextract.IconExtractor(inpath) outfile = f"tmp-{infile}.ico" outpath = os.path.join(tests_dir, outfile) ie.export_icon(outpath) self.assertTrue(filecmp.cmp(outpath, target), f"{outpath} and {target} should be equal") return ie # App has icon + version resource def test_testapp64(self): ie = self._test_extract("testapp64.exe", "testapp.ico") self.assertEqual(len(ie.list_group_icons()), 1) # App has only version resource def test_testapp64_noicon(self): with self.assertRaises(icoextract.NoIconsAvailableError): self._test_extract("testapp64-noicon.exe", "testapp-noicon.ico") # App has no resource info at all def test_testapp64_nores(self): with self.assertRaises(icoextract.NoIconsAvailableError): self._test_extract("testapp64-nores.exe", "testapp-nores.ico") def test_testapp32(self): ie = self._test_extract("testapp32.exe", "testapp.ico") self.assertEqual(len(ie.list_group_icons()), 1) def test_testapp32_noicon(self): with self.assertRaises(icoextract.NoIconsAvailableError): self._test_extract("testapp32-noicon.exe", "testapp-noicon.ico") def test_testapp32_nores(self): with self.assertRaises(icoextract.NoIconsAvailableError): self._test_extract("testapp32-nores.exe", "testapp-nores.ico") def test_fd_as_input(self): 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) if __name__ == '__main__': unittest.main() icoextract-0.1.6/tests/test_thumbnailer.py000077500000000000000000000115351476116503000207630ustar00rootroot00000000000000#!/usr/bin/env python3 import os.path import unittest from icoextract.scripts.thumbnailer import generate_thumbnail from PIL import Image 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(list(im.getdata())[:COMPARE_LENGTH], list(im_orig.getdata())[:COMPARE_LENGTH], "Extracted image should match original") 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.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.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") if __name__ == '__main__': unittest.main() icoextract-0.1.6/tests/testapp-base.rc000066400000000000000000000007721476116503000177540ustar00rootroot00000000000000#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.1.6/tests/testapp.c000066400000000000000000000003251476116503000166540ustar00rootroot00000000000000#include int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, INT nCmdShow) { MessageBoxW(NULL, L"Hello world!", L"Test Application", MB_ICONASTERISK); return 0; } icoextract-0.1.6/tests/testapp.png000066400000000000000000001067251476116503000172310ustar00rootroot00000000000000PNG  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`