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()

Reply via email to