Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-icoextract for openSUSE:Factory checked in at 2025-11-24 14:14:20 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-icoextract (Old) and /work/SRC/openSUSE:Factory/.python-icoextract.new.14147 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-icoextract" Mon Nov 24 14:14:20 2025 rev:5 rq:1319661 version:0.2.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-icoextract/python-icoextract.changes 2025-02-04 18:15:20.414486936 +0100 +++ /work/SRC/openSUSE:Factory/.python-icoextract.new.14147/python-icoextract.changes 2025-11-24 14:16:52.012701434 +0100 @@ -1,0 +2,18 @@ +Sun Nov 23 10:11:00 UTC 2025 - Martin Hauke <[email protected]> + +- Update to version 0.2.0 + * 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 +- Update to version 0.1.6 + * 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. + +------------------------------------------------------------------- Old: ---- icoextract-0.1.5.tar.gz New: ---- icoextract-0.2.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-icoextract.spec ++++++ --- /var/tmp/diff_new_pack.7Yyl2f/_old 2025-11-24 14:16:53.796776390 +0100 +++ /var/tmp/diff_new_pack.7Yyl2f/_new 2025-11-24 14:16:53.804776726 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-icoextract # -# Copyright (c) 2025 SUSE LLC +# Copyright (c) 2025 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-icoextract -Version: 0.1.5 +Version: 0.2.0 Release: 0 Summary: Extract icons from Windows PE files (.exe/.dll) License: MIT ++++++ icoextract-0.1.5.tar.gz -> icoextract-0.2.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/.drone.jsonnet new/icoextract-0.2.0/.drone.jsonnet --- old/icoextract-0.1.5/.drone.jsonnet 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/.drone.jsonnet 2025-06-03 06:12:36.000000000 +0200 @@ -19,7 +19,7 @@ image: "python:" + version + "-bookworm", commands: [ "pip install -r requirements.txt", - "python setup.py install" + "pip install ." ], volumes: volumes() }, @@ -72,6 +72,7 @@ }, when: { branch: ["master", "ci-*"], + event: ["push"], }, }, ]), @@ -84,9 +85,7 @@ }; [ - test_with("3.8"), test_with("3.9"), - test_with("3.10"), - test_with("3.11"), - test_with("3.12", do_deploy=true), + test_with("3.12"), + test_with("3.13", do_deploy=true), ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/CHANGELOG.md new/icoextract-0.2.0/CHANGELOG.md --- old/icoextract-0.1.5/CHANGELOG.md 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/CHANGELOG.md 2025-06-03 06:12:36.000000000 +0200 @@ -1,5 +1,19 @@ # Changelog +## 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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/LIB-USAGE.md new/icoextract-0.2.0/LIB-USAGE.md --- old/icoextract-0.1.5/LIB-USAGE.md 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/LIB-USAGE.md 2025-06-03 06:12:36.000000000 +0200 @@ -11,15 +11,18 @@ # Export the first group icon to a .ico file extractor.export_icon('/path/to/your.ico', num=0) - # Or save the .ico to a buffer, to pass it into another library + # 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 directly + # ... 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 resource is malformed + # No icons available, or the icon resource is malformed pass ``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/LICENSE new/icoextract-0.2.0/LICENSE --- old/icoextract-0.1.5/LICENSE 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/LICENSE 2025-06-03 06:12:36.000000000 +0200 @@ -1,7 +1,7 @@ The MIT License (MIT) Copyright (c) 2015-2016 Fadhil Mandaga -Copyright (c) 2019 James Lu <[email protected]> +Copyright (c) 2019-2025 James Lu <[email protected]> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/README.md new/icoextract-0.2.0/README.md --- old/icoextract-0.1.5/README.md 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/README.md 2025-06-03 06:12:36.000000000 +0200 @@ -40,19 +40,20 @@ For API docs, see https://projects.jlu5.com/icoextract.html ``` -usage: icoextract [-h] [-V] [-n NUM] [-v] input output +usage: icoextract [-h] [-V] [-n NUM] [-i ID] [-v] input output Windows PE EXE icon extractor. positional arguments: - input input filename (.exe/.dll/.mun) - output output filename (.ico) + 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 + -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 ``` ``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/icoextract/__init__.py new/icoextract-0.2.0/icoextract/__init__.py --- old/icoextract-0.1.5/icoextract/__init__.py 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/icoextract/__init__.py 2025-06-03 06:12:36.000000000 +0200 @@ -7,7 +7,9 @@ import io import logging -import sys +import os +import pathlib +import platform import struct import pefile @@ -29,6 +31,9 @@ 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.""" @@ -53,77 +58,89 @@ # 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: + 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._rticonres = resources.get(pefile.RESOURCE_TYPE["RT_ICON"]) + + # Populate resources by ID + self._group_icons = {entry.struct.Name: idx for idx, entry in enumerate(self._groupiconres.directory.entries)} + 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): + def list_group_icons(self) -> list[tuple[int, int]]: """ - Returns all group icon entries as a list of (name, offset) tuples. + Returns all group icon entries as a list of (resource ID, offset) tuples. """ return [(e.struct.Name, e.struct.OffsetToData) - for e in self.groupiconres.directory.entries] + for e in self._groupiconres.directory.entries] - def _get_group_icon_entries(self, num=0): + def _get_icon(self, index=0) -> list[tuple[pefile.Structure, bytes]]: """ - Returns the group icon entries for the specified group icon in the executable. + Returns the specified group icon in the binary. + + Result is a list of (group icon structure, icon data) tuples. """ - groupicon = self.groupiconres.directory.entries[num] + try: + groupicon = self._groupiconres.directory.entries[index] + except IndexError: + raise IconNotFoundError(f"No icon exists at index {index}") from None + resource_id = groupicon.struct.Name + icon_lang = None if groupicon.struct.DataIsDirectory: # Select the first language from subfolders as needed. groupicon = groupicon.directory.entries[0] + icon_lang = groupicon.struct.Name + logger.debug("Picking first language %s", icon_lang) # 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) + grp_icon_data = self._pe.get_data(rva, groupicon.data.struct.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) + grp_icon_dir = self._pe.__unpack_data__(GRPICONDIR_FORMAT, grp_icon_data, file_offset) + logger.debug("Group icon %d has ID %s and %d images: %s", + # pylint: disable=no-member + index, resource_id, grp_icon_dir.Count, grp_icon_dir) + # pylint: disable=no-member if grp_icon_dir.Reserved: - raise InvalidIconDefinitionError("Invalid group icon definition (got Reserved=%s instead of 0)" % hex(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 its data and save it. + # For each group icon entry (GRPICONDIRENTRY) that immediately follows, read the struct and look up the + # corresponding icon image 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) + for grp_icon_index in range(grp_icon_dir.Count): + grp_icon = self._pe.__unpack_data__( + GRPICONDIRENTRY_FORMAT, grp_icon_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) + logger.debug("Got group icon entry %d: %s", grp_icon_index, grp_icon) + icon_entry = self._icons[grp_icon.ID] + icon_data = self._pe.get_data(icon_entry.data.struct.OffsetToData, icon_entry.data.struct.Size) + logger.debug("Got icon data for ID %d: %s", grp_icon.ID, icon_entry.data.struct) + grp_icons.append((grp_icon, icon_data)) 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): + def _write_ico(self, fd, num=0, resource_id=None): """ 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) + if resource_id is not None: + try: + num = self._group_icons[resource_id] + except KeyError: + raise IconNotFoundError(f"No icon exists with resource ID {resource_id}") from None + + icons = self._get_icon(index=num) fd.write(b"\x00\x00") # 2 reserved bytes fd.write(struct.pack("<H", 1)) # 0x1 (little endian) specifying that this is an .ICO image fd.write(struct.pack("<H", len(icons))) # number of images @@ -144,26 +161,43 @@ group_icon, icon_data = datapair fd.write(icon_data) - def export_icon(self, filename, num=0): + def export_icon(self, filename, num=0, resource_id=None): """ - Exports ICO data for the requested group icon (`num`) to `filename`. + 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. """ with open(filename, 'wb') as f: - self._write_ico(f, num=num) + self._write_ico(f, num=num, resource_id=resource_id) - def get_icon(self, num=0): + def get_icon(self, num=0, resource_id=None) -> io.BytesIO: """ - Exports ICO data for the requested group icon (`num`) as a `io.BytesIO` instance. + 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. """ f = io.BytesIO() - self._write_ico(f, num=num) + self._write_ico(f, num=num, resource_id=resource_id) return f + @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) + __all__ = [ 'IconExtractor', 'IconExtractorError', + 'IconNotFoundError', 'NoIconsAvailableError', - 'InvalidIconDefinitionError' + 'InvalidIconDefinitionError', ] __pdoc__ = { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/icoextract/scripts/extract.py new/icoextract-0.2.0/icoextract/scripts/extract.py --- old/icoextract-0.1.5/icoextract/scripts/extract.py 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/icoextract/scripts/extract.py 2025-06-03 06:12:36.000000000 +0200 @@ -4,13 +4,16 @@ """ 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=int, 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)") @@ -20,4 +23,8 @@ logger.setLevel(logging.DEBUG) extractor = IconExtractor(args.input) - extractor.export_icon(args.output, num=args.num) + extractor.export_icon(args.output, num=args.num, resource_id=args.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) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/icoextract/scripts/thumbnailer.py new/icoextract-0.2.0/icoextract/scripts/thumbnailer.py --- old/icoextract-0.1.5/icoextract/scripts/thumbnailer.py 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/icoextract/scripts/thumbnailer.py 2025-06-03 06:12:36.000000000 +0200 @@ -10,13 +10,13 @@ from icoextract import IconExtractor, logger, __version__ -def generate_thumbnail(inputfile, outfile, large_size=True): +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) - large_size: determines whether to write a large size (256x256) thumbnail (%s) + size: determines the thumbnail output size (%s) """ try: extractor = IconExtractor(inputfile) @@ -26,9 +26,20 @@ data = extractor.get_icon() im = Image.open(data) # Open up the .ico from memory - if (256, 256) in im.info['sizes']: - if large_size: - # A large size thumbnail was requested + 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 + + # 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 @@ -39,25 +50,24 @@ if (128, 128) in im.info['sizes']: logger.debug("Using native 128x128 icon") im.size = (128, 128) - else: - logger.debug("Resizing icon from 256x256 to 128x128") + 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) - 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)", nargs='?') + parser.add_argument("outfile", help="output file name (.png)") args = parser.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) - large_size = (args.size >= 256) - generate_thumbnail(args.inputfile, args.outfile, large_size) + generate_thumbnail(args.inputfile, args.outfile, size=args.size, force_resize=args.force_resize) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/icoextract/version.py new/icoextract-0.2.0/icoextract/version.py --- old/icoextract-0.1.5/icoextract/version.py 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/icoextract/version.py 2025-06-03 06:12:36.000000000 +0200 @@ -1 +1 @@ -__version__ = '0.1.5' +__version__ = '0.2.0' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/setup.cfg new/icoextract-0.2.0/setup.cfg --- old/icoextract-0.1.5/setup.cfg 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/setup.cfg 2025-06-03 06:12:36.000000000 +0200 @@ -4,4 +4,4 @@ license_files = LICENSE [options] -python_requires = >=3.8 +python_requires = >=3.9 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/setup.py new/icoextract-0.2.0/setup.py --- old/icoextract-0.1.5/setup.py 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/setup.py 2025-06-03 06:12:36.000000000 +0200 @@ -17,7 +17,7 @@ license="MIT/Expat", classifiers=[ # https://pypi.org/classifiers/ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Intended Audience :: End Users/Desktop', @@ -26,11 +26,11 @@ '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', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ], packages=find_packages(exclude=['tests']), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/tests/Makefile new/icoextract-0.2.0/tests/Makefile --- old/icoextract-0.1.5/tests/Makefile 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/tests/Makefile 2025-06-03 06:12:36.000000000 +0200 @@ -3,24 +3,34 @@ PREFIX32=i686-w64-mingw32- all: testapp64.exe testapp64-nores.exe testapp64-noicon.exe \ - testapp64-smallonly.exe \ - testapp64-with128.exe + testapp64-smallonly.exe testapp64-with128.exe testapp64-with192.exe -# icon with standard sizes: 16x16, 32x32, 48x48, 256x256 -testapp.ico: testapp.png +bmps: 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 + +# icon with standard sizes: 16x16, 32x32, 48x48, 256x256 +testapp.ico: testapp.png bmps convert testapp.png tmp-testapp*.bmp testapp.ico -# small icon + +# Small icon (only up to 48x48) +testapp-smallonly.ico: testapp.png bmps convert tmp-testapp-*.bmp testapp-smallonly.ico + # All standard sizes + 128x128 +testapp-with128.ico: testapp.png bmps 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 + # Build with icon + version resource define build-with-icon = cat testapp-base.rc > tmp-testapp$(ICOSUFFIX).rc @@ -35,11 +45,15 @@ $(build-with-icon) testapp64-smallonly.exe testapp32-smallonly.exe: ICOSUFFIX=-smallonly -testapp64-smallonly.exe testapp32-smallonly.exe: testapp.c testapp.ico +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.ico +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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/tests/test_extract.py new/icoextract-0.2.0/tests/test_extract.py --- old/icoextract-0.1.5/tests/test_extract.py 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/tests/test_extract.py 2025-06-03 06:12:36.000000000 +0200 @@ -6,55 +6,72 @@ import icoextract -class UtilsTestCase(unittest.TestCase): - def _test_extract(self, infile, target): +class IconExtractorTestCase(unittest.TestCase): + 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 + """ # 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) + ie.export_icon(outpath, **kwargs) - self.assertTrue(filecmp.cmp(outpath, target), - f"{outpath} and {target} should be equal") + assert compare_against, \ + "Successful extractions should have a file to compare against" + compare_against = os.path.join(tests_dir, compare_against) + self.assertTrue(filecmp.cmp(outpath, compare_against), + f"{outpath} and {compare_against} 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_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") + self.assertEqual(len(ie.list_group_icons()), 1) + + # Nonexistent icon index + with self.assertRaises(icoextract.IconNotFoundError): + self._test_extract(app, num=10) + + def test_no_icon_resource(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) + if __name__ == '__main__': unittest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/icoextract-0.1.5/tests/test_thumbnailer.py new/icoextract-0.2.0/tests/test_thumbnailer.py --- old/icoextract-0.1.5/tests/test_thumbnailer.py 2024-04-28 21:56:52.000000000 +0200 +++ new/icoextract-0.2.0/tests/test_thumbnailer.py 2025-06-03 06:12:36.000000000 +0200 @@ -1,6 +1,4 @@ #!/usr/bin/env python3 - -import filecmp import os.path import unittest @@ -23,38 +21,87 @@ "Extracted image should match original") def test_thumbnailer_normal(self): - outfile = self._generate_thumbnail("testapp64.exe", "tmp-thumbnail-test-normal.png", large_size=False) + 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", large_size=True) + 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", large_size=True) + 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", large_size=False) + 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", large_size=False) + 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()
