icoextract 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- icoextract/__init__.py +100 -0
- icoextract/base_extractor.py +65 -0
- icoextract/exceptions.py +18 -0
- icoextract/ne_extractor.py +107 -0
- icoextract/pe_extractor.py +170 -0
- icoextract/scripts/__init__.py +1 -0
- icoextract/scripts/extract.py +39 -0
- icoextract/scripts/icolist.py +41 -0
- icoextract/scripts/thumbnailer.py +82 -0
- icoextract/types.py +70 -0
- icoextract/version.py +1 -0
- icoextract-0.3.0.dist-info/METADATA +112 -0
- icoextract-0.3.0.dist-info/RECORD +17 -0
- icoextract-0.3.0.dist-info/WHEEL +5 -0
- icoextract-0.3.0.dist-info/entry_points.txt +4 -0
- icoextract-0.3.0.dist-info/licenses/LICENSE +22 -0
- icoextract-0.3.0.dist-info/top_level.txt +1 -0
icoextract/__init__.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2015-2016 Fadhil Mandaga
|
|
4
|
+
# Copyright (c) 2019-2026 James Lu <james@overdrivenetworks.com>
|
|
5
|
+
"""
|
|
6
|
+
Windows executable icon extractor.
|
|
7
|
+
|
|
8
|
+
.. include:: ../LIB-USAGE.md
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from contextlib import ExitStack
|
|
12
|
+
import enum
|
|
13
|
+
import logging
|
|
14
|
+
import mmap
|
|
15
|
+
|
|
16
|
+
from .base_extractor import BaseIconExtractor
|
|
17
|
+
from .exceptions import (
|
|
18
|
+
IconExtractorError,
|
|
19
|
+
IconNotFoundError,
|
|
20
|
+
NoIconsAvailableError,
|
|
21
|
+
InvalidIconDefinitionError,
|
|
22
|
+
UnknownExecutableError,
|
|
23
|
+
)
|
|
24
|
+
from .ne_extractor import NEIconExtractor
|
|
25
|
+
from .pe_extractor import PEIconExtractor
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("icoextract")
|
|
28
|
+
logging.basicConfig()
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from .version import __version__
|
|
32
|
+
except ImportError:
|
|
33
|
+
__version__ = 'unknown'
|
|
34
|
+
logger.info('icoextract: failed to read program version')
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
'IconExtractor',
|
|
38
|
+
'IconExtractorError',
|
|
39
|
+
'IconNotFoundError',
|
|
40
|
+
'NoIconsAvailableError',
|
|
41
|
+
'InvalidIconDefinitionError',
|
|
42
|
+
'UnknownExecutableError',
|
|
43
|
+
'ExecutableType',
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
class ExecutableType(enum.Enum):
|
|
47
|
+
"""Enum for supported executable formats."""
|
|
48
|
+
AUTO = 0
|
|
49
|
+
PE = 1
|
|
50
|
+
NE = 2
|
|
51
|
+
|
|
52
|
+
def detect_executable_type(buf: bytearray | mmap.mmap) -> ExecutableType:
|
|
53
|
+
if buf[:2] != b'MZ':
|
|
54
|
+
raise UnknownExecutableError("Unknown executable type (no MZ header)")
|
|
55
|
+
|
|
56
|
+
# Get the offset to the real header (located at 0x3C)
|
|
57
|
+
e_lfanew = int.from_bytes(buf[0x3C:0x40], byteorder='little')
|
|
58
|
+
signature = buf[e_lfanew:e_lfanew+4]
|
|
59
|
+
|
|
60
|
+
if signature.startswith(b'PE\x00\x00'):
|
|
61
|
+
return ExecutableType.PE
|
|
62
|
+
if signature.startswith(b'NE'):
|
|
63
|
+
return ExecutableType.NE
|
|
64
|
+
raise UnknownExecutableError("Unknown / unsupported executable type")
|
|
65
|
+
|
|
66
|
+
def IconExtractor(filename: str | None = None,
|
|
67
|
+
data: bytearray | mmap.mmap | None = None,
|
|
68
|
+
exe_type: ExecutableType = ExecutableType.AUTO) -> BaseIconExtractor:
|
|
69
|
+
"""
|
|
70
|
+
IconExtractor factory function.
|
|
71
|
+
|
|
72
|
+
When `exe_type` is set to *AUTO*, this will return an `icoextract.pe_extractor.PEIconExtractor`
|
|
73
|
+
for Win32/Win64 PE executables and an `icoextract.ne_extractor.NEIconExtractor` for Win16 NE
|
|
74
|
+
executables.
|
|
75
|
+
"""
|
|
76
|
+
if filename is None and data is None:
|
|
77
|
+
raise ValueError("filename and data cannot both be None")
|
|
78
|
+
|
|
79
|
+
if exe_type == ExecutableType.AUTO:
|
|
80
|
+
if data is not None:
|
|
81
|
+
exe_type = detect_executable_type(data)
|
|
82
|
+
else:
|
|
83
|
+
assert filename
|
|
84
|
+
with ExitStack() as stack:
|
|
85
|
+
f = stack.enter_context(open(filename, 'rb'))
|
|
86
|
+
mm = stack.enter_context(mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ))
|
|
87
|
+
exe_type = detect_executable_type(mm)
|
|
88
|
+
logger.debug("Detected executable type as %s", exe_type)
|
|
89
|
+
|
|
90
|
+
if exe_type == ExecutableType.PE:
|
|
91
|
+
return PEIconExtractor(filename, data)
|
|
92
|
+
if exe_type == ExecutableType.NE:
|
|
93
|
+
return NEIconExtractor(filename, data)
|
|
94
|
+
|
|
95
|
+
raise UnknownExecutableError("Unknown executable type")
|
|
96
|
+
|
|
97
|
+
__pdoc__ = {
|
|
98
|
+
'scripts': False,
|
|
99
|
+
'version': False,
|
|
100
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2019-2026 James Lu <james@overdrivenetworks.com>
|
|
4
|
+
|
|
5
|
+
import abc
|
|
6
|
+
import io
|
|
7
|
+
import struct
|
|
8
|
+
|
|
9
|
+
from .types import ExtractedGroupIcon, GroupIconWithIconOffsets, ResourceID
|
|
10
|
+
|
|
11
|
+
class BaseIconExtractor(abc.ABC):
|
|
12
|
+
"""Base icon extractor class containing .ico writing logic."""
|
|
13
|
+
|
|
14
|
+
@abc.abstractmethod
|
|
15
|
+
def _extract_icon(self, index: int = 0, resource_id: int | str | None = None) -> ExtractedGroupIcon:
|
|
16
|
+
"""Extract an icon by its index or resource ID."""
|
|
17
|
+
|
|
18
|
+
@abc.abstractmethod
|
|
19
|
+
def list_group_icons(self) -> list[tuple[ResourceID, GroupIconWithIconOffsets]]:
|
|
20
|
+
"""
|
|
21
|
+
Returns all group icon entries as a list of (resource ID, group icon dir entry) tuples.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def _write_ico(self, fd, icons: ExtractedGroupIcon):
|
|
25
|
+
"""Writes ICO data to a file descriptor."""
|
|
26
|
+
fd.write(b"\x00\x00") # 2 reserved bytes
|
|
27
|
+
fd.write(struct.pack("<H", 1)) # 0x1 (little endian) specifying that this is an .ICO image
|
|
28
|
+
fd.write(struct.pack("<H", len(icons))) # number of images
|
|
29
|
+
|
|
30
|
+
dataoffset = 6 + (len(icons) * 16)
|
|
31
|
+
# First pass: write the icon dir entries
|
|
32
|
+
for datapair in icons:
|
|
33
|
+
group_icon, icon_data = datapair
|
|
34
|
+
# Elements in ICONDIRENTRY and GRPICONDIRENTRY are all the same
|
|
35
|
+
# except the last value, which is a 2 byte ID in GRPICONDIRENTRY and
|
|
36
|
+
# the 4 byte offset from the beginning of the file in ICONDIRENTRY.
|
|
37
|
+
fd.write(bytes(group_icon)[:12])
|
|
38
|
+
fd.write(struct.pack("<I", dataoffset))
|
|
39
|
+
dataoffset += len(icon_data) # Increase offset for next image
|
|
40
|
+
|
|
41
|
+
# Second pass: write the icon data
|
|
42
|
+
for datapair in icons:
|
|
43
|
+
group_icon, icon_data = datapair
|
|
44
|
+
fd.write(icon_data)
|
|
45
|
+
|
|
46
|
+
def export_icon(self, filename: str, num: int = 0, resource_id: int | str | None = None) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Exports ICO data for the requested group icon to `filename`.
|
|
49
|
+
|
|
50
|
+
Icons can be selected by index (`num`) or resource ID. By default, the first icon in the binary is exported.
|
|
51
|
+
"""
|
|
52
|
+
group_icon = self._extract_icon(index=num, resource_id=resource_id)
|
|
53
|
+
with open(filename, 'wb') as f:
|
|
54
|
+
self._write_ico(f, group_icon)
|
|
55
|
+
|
|
56
|
+
def get_icon(self, num: int = 0, resource_id: int | str | None = None) -> io.BytesIO:
|
|
57
|
+
"""
|
|
58
|
+
Exports ICO data for the requested group icon as a `io.BytesIO` instance.
|
|
59
|
+
|
|
60
|
+
Icons can be selected by index (`num`) or resource ID. By default, the first icon in the binary is exported.
|
|
61
|
+
"""
|
|
62
|
+
group_icon = self._extract_icon(index=num, resource_id=resource_id)
|
|
63
|
+
f = io.BytesIO()
|
|
64
|
+
self._write_ico(f, group_icon)
|
|
65
|
+
return f
|
icoextract/exceptions.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2019-2026 James Lu <james@overdrivenetworks.com>
|
|
4
|
+
|
|
5
|
+
class IconExtractorError(Exception):
|
|
6
|
+
"""Superclass for exceptions raised by IconExtractor."""
|
|
7
|
+
|
|
8
|
+
class IconNotFoundError(IconExtractorError):
|
|
9
|
+
"""Exception raised when extracting an icon index or resource ID that does not exist."""
|
|
10
|
+
|
|
11
|
+
class NoIconsAvailableError(IconExtractorError):
|
|
12
|
+
"""Exception raised when the input program has no icon resources."""
|
|
13
|
+
|
|
14
|
+
class InvalidIconDefinitionError(IconExtractorError):
|
|
15
|
+
"""Exception raised when the input program has an invalid icon resource."""
|
|
16
|
+
|
|
17
|
+
class UnknownExecutableError(IconExtractorError):
|
|
18
|
+
"""Exception raised when the executable type is invalid or not supported."""
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2019-2026 James Lu <james@overdrivenetworks.com>
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import nefile
|
|
9
|
+
except ImportError:
|
|
10
|
+
nefile = None
|
|
11
|
+
|
|
12
|
+
from .base_extractor import BaseIconExtractor
|
|
13
|
+
from .exceptions import (
|
|
14
|
+
IconNotFoundError,
|
|
15
|
+
NoIconsAvailableError
|
|
16
|
+
)
|
|
17
|
+
from .types import (
|
|
18
|
+
ExtractedGroupIcon,
|
|
19
|
+
GroupIconDirEntry,
|
|
20
|
+
GroupIconWithIconOffsets,
|
|
21
|
+
ResourceID,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("icoextract")
|
|
25
|
+
|
|
26
|
+
class NEIconExtractor(BaseIconExtractor):
|
|
27
|
+
"""Win16 New Executable (NE) icon extractor."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, filename=None, data=None):
|
|
30
|
+
"""
|
|
31
|
+
Loads an Win16 New Executable from the given `filename` or `data` (raw buffer).
|
|
32
|
+
If both `filename` and `data` are given, `filename` takes precedence.
|
|
33
|
+
|
|
34
|
+
If the executable has contains no icons, this will raise `NoIconsAvailableError`.
|
|
35
|
+
"""
|
|
36
|
+
if nefile is None:
|
|
37
|
+
raise ImportError("nefile module must be installed to extract NE programs")
|
|
38
|
+
self._ne = nefile.NE(filename, data)
|
|
39
|
+
|
|
40
|
+
if not self._ne.resource_table:
|
|
41
|
+
raise NoIconsAvailableError("Executable does not have any resources")
|
|
42
|
+
if not (group_icon_resources := self._ne.resource_table.resources.get(
|
|
43
|
+
nefile.resource_table.ResourceType.RT_GROUP_ICON)):
|
|
44
|
+
raise NoIconsAvailableError("Executable does not have any group icons")
|
|
45
|
+
|
|
46
|
+
self._group_icon_resources = group_icon_resources
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def _convert_nefile_icon_dir_entry(nefile_icon_dir_entry) -> GroupIconDirEntry:
|
|
50
|
+
assert nefile is not None
|
|
51
|
+
assert isinstance(nefile_icon_dir_entry, nefile.resources.icon.IconDirectoryEntry)
|
|
52
|
+
return GroupIconDirEntry(
|
|
53
|
+
Width=nefile_icon_dir_entry.width,
|
|
54
|
+
Height=nefile_icon_dir_entry.height,
|
|
55
|
+
ColorCount=nefile_icon_dir_entry.total_palette_colors,
|
|
56
|
+
Planes=nefile_icon_dir_entry.color_planes,
|
|
57
|
+
BitCount=nefile_icon_dir_entry.bits_per_pixel,
|
|
58
|
+
BytesInRes=nefile_icon_dir_entry.icon_size_in_bytes,
|
|
59
|
+
ID=nefile_icon_dir_entry.icon_resource_id,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def list_group_icons(self) -> list[tuple[ResourceID, GroupIconWithIconOffsets]]:
|
|
63
|
+
assert nefile is not None
|
|
64
|
+
result = []
|
|
65
|
+
resources_offset = self._ne.resource_table.resource_table_start_offset
|
|
66
|
+
icon_rtt = self._ne.resource_table.resource_type_tables[
|
|
67
|
+
nefile.resource_table.ResourceType.RT_ICON
|
|
68
|
+
]
|
|
69
|
+
icon_resource_declarations = {
|
|
70
|
+
rdecl.id: rdecl for rdecl in icon_rtt.resource_declarations
|
|
71
|
+
}
|
|
72
|
+
for resource_id, nefile_group_icon in self._group_icon_resources.items():
|
|
73
|
+
if isinstance(resource_id, str):
|
|
74
|
+
resource_id_wrapper = ResourceID(raw_id=None, name=resource_id)
|
|
75
|
+
else:
|
|
76
|
+
resource_id_wrapper = ResourceID(resource_id)
|
|
77
|
+
|
|
78
|
+
icon_infos = []
|
|
79
|
+
for nefile_icon_dir_entry in nefile_group_icon.icon_directory.directory_entries:
|
|
80
|
+
grp_icon_dir_entry = self._convert_nefile_icon_dir_entry(nefile_icon_dir_entry)
|
|
81
|
+
icon_rdecl = icon_resource_declarations[grp_icon_dir_entry.ID]
|
|
82
|
+
offset = icon_rdecl.data_start_offset
|
|
83
|
+
file_offset = offset + resources_offset
|
|
84
|
+
icon_infos.append((grp_icon_dir_entry, offset, file_offset))
|
|
85
|
+
result.append((resource_id_wrapper, icon_infos))
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
def _extract_icon(self, index: int = 0, resource_id: int | str | None = None) -> ExtractedGroupIcon:
|
|
89
|
+
if resource_id is not None:
|
|
90
|
+
try:
|
|
91
|
+
nefile_group_icon = self._group_icon_resources[resource_id]
|
|
92
|
+
except KeyError:
|
|
93
|
+
raise IconNotFoundError(f"No icon exists with resource ID {resource_id!r}") from None
|
|
94
|
+
else:
|
|
95
|
+
icons_by_index = list(self._group_icon_resources.values())
|
|
96
|
+
try:
|
|
97
|
+
nefile_group_icon = icons_by_index[index]
|
|
98
|
+
except IndexError:
|
|
99
|
+
raise IconNotFoundError(f"No icon exists at index {index}") from None
|
|
100
|
+
|
|
101
|
+
results = []
|
|
102
|
+
for nefile_icon_dir_entry in nefile_group_icon.icon_directory.directory_entries:
|
|
103
|
+
resource_id = nefile_icon_dir_entry.icon_resource_id
|
|
104
|
+
grp_icon_dir_entry = self._convert_nefile_icon_dir_entry(nefile_icon_dir_entry)
|
|
105
|
+
icondata = nefile_group_icon.icons[resource_id].data
|
|
106
|
+
results.append((grp_icon_dir_entry, icondata))
|
|
107
|
+
return results
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2015-2016 Fadhil Mandaga
|
|
4
|
+
# Copyright (c) 2019-2026 James Lu <james@overdrivenetworks.com>
|
|
5
|
+
|
|
6
|
+
import ctypes
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import pathlib
|
|
10
|
+
import platform
|
|
11
|
+
|
|
12
|
+
import pefile
|
|
13
|
+
|
|
14
|
+
from .base_extractor import BaseIconExtractor
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
IconNotFoundError,
|
|
17
|
+
InvalidIconDefinitionError,
|
|
18
|
+
NoIconsAvailableError
|
|
19
|
+
)
|
|
20
|
+
from .types import (
|
|
21
|
+
ExtractedGroupIcon,
|
|
22
|
+
GroupIconDir,
|
|
23
|
+
GroupIconDirEntry,
|
|
24
|
+
GroupIconWithIconOffsets,
|
|
25
|
+
ResourceID,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("icoextract")
|
|
29
|
+
|
|
30
|
+
class PEIconExtractor(BaseIconExtractor):
|
|
31
|
+
"""Win32/Win64 Portable Executable (PE) icon extractor."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, filename=None, data=None):
|
|
34
|
+
"""
|
|
35
|
+
Loads an Win32/Win64 Portable Executable from the given `filename` or `data` (raw buffer).
|
|
36
|
+
If both `filename` and `data` are given, `filename` takes precedence.
|
|
37
|
+
|
|
38
|
+
If the executable has contains no icons, this will raise `NoIconsAvailableError`.
|
|
39
|
+
"""
|
|
40
|
+
# Use fast loading and explicitly load the RESOURCE directory entry. This saves a LOT of time
|
|
41
|
+
# on larger files
|
|
42
|
+
self._pe = pefile.PE(name=filename, data=data, fast_load=True)
|
|
43
|
+
self._pe.parse_data_directories(pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE'])
|
|
44
|
+
|
|
45
|
+
if not hasattr(self._pe, 'DIRECTORY_ENTRY_RESOURCE'):
|
|
46
|
+
raise NoIconsAvailableError("File has no resources")
|
|
47
|
+
|
|
48
|
+
# Reverse the list of entries before making the mapping so that earlier values take precedence
|
|
49
|
+
# When an executable includes multiple icon resources, we should use only the first one.
|
|
50
|
+
# pylint: disable=no-member
|
|
51
|
+
resources = {rsrc.id: rsrc for rsrc in reversed(self._pe.DIRECTORY_ENTRY_RESOURCE.entries)}
|
|
52
|
+
|
|
53
|
+
self._groupiconres = resources.get(pefile.RESOURCE_TYPE["RT_GROUP_ICON"])
|
|
54
|
+
if not self._groupiconres:
|
|
55
|
+
if filename and platform.system() == "Windows":
|
|
56
|
+
self._print_windows_usage_hint(filename)
|
|
57
|
+
raise NoIconsAvailableError("File has no group icon resources")
|
|
58
|
+
self._rticonres = resources.get(pefile.RESOURCE_TYPE["RT_ICON"])
|
|
59
|
+
|
|
60
|
+
self._group_icons = self._groupiconres.directory.entries
|
|
61
|
+
self._group_icons_by_id: dict[int | str, pefile.ResourceDirEntryData] = {}
|
|
62
|
+
for entry in self._group_icons:
|
|
63
|
+
self._group_icons_by_id[entry.struct.Name] = entry
|
|
64
|
+
# For resources with a string name, track them by both the string and the underlying
|
|
65
|
+
# numerical index
|
|
66
|
+
if entry.name:
|
|
67
|
+
self._group_icons_by_id[str(entry.name)] = entry
|
|
68
|
+
|
|
69
|
+
self._icons = {icon_entry_list.id: icon_entry_list.directory.entries[0] # Select first language
|
|
70
|
+
for icon_entry_list in self._rticonres.directory.entries}
|
|
71
|
+
|
|
72
|
+
def list_group_icons(self) -> list[tuple[ResourceID, GroupIconWithIconOffsets]]:
|
|
73
|
+
results = []
|
|
74
|
+
for entry in self._group_icons:
|
|
75
|
+
resource_id = ResourceID(
|
|
76
|
+
raw_id=entry.struct.Name,
|
|
77
|
+
name=str(entry.name) if entry.name else None
|
|
78
|
+
)
|
|
79
|
+
grp_icon_resource = self._group_icons_by_id[entry.struct.Name]
|
|
80
|
+
grp_icon_data_full = self._extract_from_pefile(grp_icon_resource, skip_data=True)
|
|
81
|
+
# Return only the metadata and offsets
|
|
82
|
+
grp_icon_data = [
|
|
83
|
+
(grp_icon_dir_entry, resource_offset, file_offset)
|
|
84
|
+
for (grp_icon_dir_entry, _, resource_offset, file_offset)
|
|
85
|
+
in grp_icon_data_full
|
|
86
|
+
]
|
|
87
|
+
results.append((resource_id, grp_icon_data))
|
|
88
|
+
return results
|
|
89
|
+
|
|
90
|
+
def _extract_from_pefile(
|
|
91
|
+
self,
|
|
92
|
+
resource_dir_entry_data: pefile.ResourceDirEntryData,
|
|
93
|
+
skip_data: bool = False,
|
|
94
|
+
) -> list[tuple[GroupIconDirEntry, bytes, int, int]]:
|
|
95
|
+
"""
|
|
96
|
+
Returns the specified group icon in the binary.
|
|
97
|
+
|
|
98
|
+
Result is a list of (group icon dir entry, icon data) tuples.
|
|
99
|
+
"""
|
|
100
|
+
icon_lang = None
|
|
101
|
+
if resource_dir_entry_data.struct.DataIsDirectory:
|
|
102
|
+
# Select the first language from subfolders as needed.
|
|
103
|
+
resource_dir_entry_data = resource_dir_entry_data.directory.entries[0]
|
|
104
|
+
icon_lang = resource_dir_entry_data.struct.Name
|
|
105
|
+
logger.debug("Picking first language %s", icon_lang)
|
|
106
|
+
|
|
107
|
+
# Read the data pointed to by the group icon directory (GRPICONDIR) struct.
|
|
108
|
+
rva = resource_dir_entry_data.data.struct.OffsetToData
|
|
109
|
+
grp_icon_data = self._pe.get_data(rva, resource_dir_entry_data.data.struct.Size)
|
|
110
|
+
|
|
111
|
+
grp_icon_dir = GroupIconDir.from_buffer_copy(grp_icon_data)
|
|
112
|
+
logger.debug("Group icon has %d images: %s",
|
|
113
|
+
grp_icon_dir.Count, grp_icon_dir)
|
|
114
|
+
|
|
115
|
+
# pylint: disable=no-member
|
|
116
|
+
if grp_icon_dir.Reserved:
|
|
117
|
+
# pylint: disable=no-member
|
|
118
|
+
raise InvalidIconDefinitionError("Invalid group icon definition (got Reserved=%s instead of 0)"
|
|
119
|
+
% hex(grp_icon_dir.Reserved))
|
|
120
|
+
|
|
121
|
+
# For each group icon entry (GRPICONDIRENTRY) that immediately follows, read the struct and look up the
|
|
122
|
+
# corresponding icon image
|
|
123
|
+
extracted_grp_icons = []
|
|
124
|
+
icon_offset = ctypes.sizeof(grp_icon_dir)
|
|
125
|
+
for grp_icon_index in range(grp_icon_dir.Count):
|
|
126
|
+
grp_icon_dir_entry = GroupIconDirEntry.from_buffer_copy(grp_icon_data, icon_offset)
|
|
127
|
+
icon_offset += ctypes.sizeof(grp_icon_dir_entry)
|
|
128
|
+
logger.debug("Got group icon entry %d: %s", grp_icon_index, grp_icon_dir_entry)
|
|
129
|
+
|
|
130
|
+
icon_entry = self._icons[grp_icon_dir_entry.ID]
|
|
131
|
+
logger.debug("Got icon data for ID %d: %s", grp_icon_dir_entry.ID, icon_entry.data.struct)
|
|
132
|
+
|
|
133
|
+
icon_res_offset = icon_entry.data.struct.OffsetToData
|
|
134
|
+
icon_file_offset = self._pe.get_offset_from_rva(icon_res_offset)
|
|
135
|
+
icon_data = b''
|
|
136
|
+
if not skip_data:
|
|
137
|
+
icon_data = self._pe.get_data(icon_res_offset, icon_entry.data.struct.Size)
|
|
138
|
+
extracted_grp_icons.append((grp_icon_dir_entry, icon_data, icon_res_offset, icon_file_offset))
|
|
139
|
+
return extracted_grp_icons
|
|
140
|
+
|
|
141
|
+
def _extract_icon(self, index: int = 0, resource_id: int | str | None = None) -> ExtractedGroupIcon:
|
|
142
|
+
if resource_id is not None:
|
|
143
|
+
try:
|
|
144
|
+
resource_dir_entry_data = self._group_icons_by_id[resource_id]
|
|
145
|
+
except KeyError:
|
|
146
|
+
raise IconNotFoundError(f"No icon exists with resource ID {resource_id!r}") from None
|
|
147
|
+
else:
|
|
148
|
+
try:
|
|
149
|
+
resource_dir_entry_data = self._group_icons[index]
|
|
150
|
+
except IndexError:
|
|
151
|
+
raise IconNotFoundError(f"No icon exists at index {index}") from None
|
|
152
|
+
|
|
153
|
+
# Return only the GroupIconDirEntry metadata and icon bytes
|
|
154
|
+
return [
|
|
155
|
+
(grp_icon_dir_entry, icon_data)
|
|
156
|
+
for (grp_icon_dir_entry, icon_data, _, _)
|
|
157
|
+
in self._extract_from_pefile(resource_dir_entry_data)
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _print_windows_usage_hint(filename):
|
|
162
|
+
path = pathlib.Path(filename)
|
|
163
|
+
systemroot = pathlib.Path(os.getenv('SYSTEMROOT'))
|
|
164
|
+
if path.is_relative_to(systemroot / 'System32') or \
|
|
165
|
+
path.is_relative_to(systemroot / 'SysWOW64'):
|
|
166
|
+
mun_path = pathlib.Path(systemroot / 'SystemResources' / (path.name + '.mun'))
|
|
167
|
+
if mun_path.is_file():
|
|
168
|
+
logger.warning(
|
|
169
|
+
'System DLL files in Windows 10 1903+ no longer contain icons. '
|
|
170
|
+
'Try extracting from %s instead.', mun_path)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Stub so that scripts/ is treated as a package
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2019-2026 James Lu <james@overdrivenetworks.com>
|
|
4
|
+
"""
|
|
5
|
+
Windows executable icon extractor.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import logging
|
|
10
|
+
import os.path
|
|
11
|
+
|
|
12
|
+
from icoextract import IconExtractor, logger, __version__
|
|
13
|
+
|
|
14
|
+
_WRONG_EXTENSIONS_HINT = {'.jpg', '.jpeg', '.png'}
|
|
15
|
+
def main():
|
|
16
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
17
|
+
parser.add_argument("-V", "--version", action='version', version=f'icoextract {__version__}')
|
|
18
|
+
parser.add_argument("-n", "--num", type=int, help="index of icon to extract", default=0)
|
|
19
|
+
parser.add_argument("-i", "--id", type=str, help="resource ID of icon to extract", default=None)
|
|
20
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="enables debug logging")
|
|
21
|
+
parser.add_argument("input", help="input filename (.exe/.dll/.mun)")
|
|
22
|
+
parser.add_argument("output", help="output filename (.ico)")
|
|
23
|
+
args = parser.parse_args()
|
|
24
|
+
|
|
25
|
+
if args.verbose:
|
|
26
|
+
logger.setLevel(logging.DEBUG)
|
|
27
|
+
|
|
28
|
+
extractor = IconExtractor(args.input)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
resource_id = int(args.id)
|
|
32
|
+
except (ValueError, TypeError):
|
|
33
|
+
resource_id = args.id
|
|
34
|
+
|
|
35
|
+
extractor.export_icon(args.output, num=args.num, resource_id=resource_id)
|
|
36
|
+
file_ext = os.path.splitext(args.output)[1].lower()
|
|
37
|
+
if file_ext in _WRONG_EXTENSIONS_HINT:
|
|
38
|
+
logger.warning('This tool outputs .ico files, not %s. The resulting file will have the wrong file extension.',
|
|
39
|
+
file_ext)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2019-2026 James Lu <james@overdrivenetworks.com>
|
|
4
|
+
"""
|
|
5
|
+
Lists group icons present in a program.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from icoextract import IconExtractor, logger, __version__
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
15
|
+
parser.add_argument("-V", "--version", action='version', version=f'icoextract {__version__}')
|
|
16
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="enables debug logging")
|
|
17
|
+
parser.add_argument("input", help="input filename")
|
|
18
|
+
args = parser.parse_args()
|
|
19
|
+
|
|
20
|
+
if args.verbose:
|
|
21
|
+
logger.setLevel(logging.DEBUG)
|
|
22
|
+
|
|
23
|
+
extractor = IconExtractor(args.input)
|
|
24
|
+
for idx, entry in enumerate(extractor.list_group_icons()):
|
|
25
|
+
resource_id, grp_icon_dir_entries = entry
|
|
26
|
+
print(f"Group Icon Index: {idx} ", end='')
|
|
27
|
+
|
|
28
|
+
print(f"ID: {resource_id}", end='')
|
|
29
|
+
if resource_id.raw_id:
|
|
30
|
+
print(f"({hex(resource_id.raw_id)})", end='')
|
|
31
|
+
print(" ", end='')
|
|
32
|
+
|
|
33
|
+
print(f"Count: {len(grp_icon_dir_entries)}")
|
|
34
|
+
for (grp_icon_dir_entry, resource_offset, file_offset) in grp_icon_dir_entries:
|
|
35
|
+
print(f" Icon ID: {grp_icon_dir_entry.ID}"
|
|
36
|
+
# Width and Height are u8 values where 0 means 256
|
|
37
|
+
f" Width: {grp_icon_dir_entry.Width or 256}"
|
|
38
|
+
f" Height: {grp_icon_dir_entry.Height or 256}"
|
|
39
|
+
f" Resource Offset: {hex(resource_offset)}"
|
|
40
|
+
f" File Offset: {hex(file_offset)}"
|
|
41
|
+
f" Size: {grp_icon_dir_entry.BytesInRes}")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2019-2026 James Lu <james@overdrivenetworks.com>
|
|
4
|
+
"""
|
|
5
|
+
Linux (freedesktop.org) thumbnailer for Windows PE files (.exe/.dll)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from PIL import Image
|
|
13
|
+
|
|
14
|
+
from icoextract import IconExtractor, IconExtractorError, logger, __version__
|
|
15
|
+
|
|
16
|
+
def generate_thumbnail(inputfile, outfile, size=256, force_resize=False):
|
|
17
|
+
"""
|
|
18
|
+
Generates a thumbnail for an .exe file.
|
|
19
|
+
|
|
20
|
+
inputfile: the input file path (%i)
|
|
21
|
+
outfile: output filename (%o)
|
|
22
|
+
size: determines the thumbnail output size (%s)
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
extractor = IconExtractor(inputfile)
|
|
26
|
+
except RuntimeError:
|
|
27
|
+
logger.debug("Failed to extract icon for %s:", inputfile, exc_info=True)
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
data = extractor.get_icon()
|
|
30
|
+
|
|
31
|
+
im = Image.open(data) # Open up the .ico from memory
|
|
32
|
+
if force_resize:
|
|
33
|
+
logger.debug("Force resizing icon to %dx%d", size, size)
|
|
34
|
+
im = im.resize((size, size))
|
|
35
|
+
else:
|
|
36
|
+
if size > 256:
|
|
37
|
+
logger.warning('Icon sizes over 256x256 are not supported')
|
|
38
|
+
size = 256
|
|
39
|
+
elif size not in (128, 256):
|
|
40
|
+
logger.warning('Unsupported size %d, falling back to 128x128', size)
|
|
41
|
+
size = 128
|
|
42
|
+
|
|
43
|
+
if size == 128:
|
|
44
|
+
# If large size thumbnail wasn't requested but one is available, pick an 128x128 icon if available;
|
|
45
|
+
# otherwise scale down from 256x256 to 128x128. 128x128 is the largest resolution allowed for
|
|
46
|
+
# "normal" size thumbnails.
|
|
47
|
+
if (128, 128) in im.info['sizes']:
|
|
48
|
+
logger.debug("Using native 128x128 icon")
|
|
49
|
+
im.size = (128, 128)
|
|
50
|
+
elif im.size > (128, 128):
|
|
51
|
+
logger.debug("Downsizing icon to 128x128")
|
|
52
|
+
im = im.resize((128, 128))
|
|
53
|
+
|
|
54
|
+
# For legacy apps, try to export the icon with the highest size & bit depth
|
|
55
|
+
# Some programs (e.g. Internet Explorer 6) put a 16 color icon earlier in the file
|
|
56
|
+
if im.size <= (48, 48):
|
|
57
|
+
ico_frames = list(enumerate(im.ico.entry))
|
|
58
|
+
best_frame_idx, ico_header = max(ico_frames, key=lambda pair: (pair[1].square, pair[1].color_depth))
|
|
59
|
+
logger.debug("Legacy app: selecting icon %s", ico_header)
|
|
60
|
+
im = im.ico.frame(best_frame_idx)
|
|
61
|
+
|
|
62
|
+
logger.debug("Writing size %s thumbnail for %s to %s", size, inputfile, outfile)
|
|
63
|
+
im.save(outfile, "PNG")
|
|
64
|
+
|
|
65
|
+
def main():
|
|
66
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
67
|
+
parser.add_argument("-V", "--version", action='version', version=f'exe-thumbnailer, part of icoextract {__version__}')
|
|
68
|
+
parser.add_argument("-s", "--size", type=int, help="size of desired thumbnail", default=256)
|
|
69
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="enables debug logging")
|
|
70
|
+
parser.add_argument("-f", "--force-resize", action="store_true", help="force resize thumbnail to the specified size")
|
|
71
|
+
parser.add_argument("inputfile", help="input file name (.exe/.dll/.mun)")
|
|
72
|
+
parser.add_argument("outfile", help="output file name (.png)")
|
|
73
|
+
args = parser.parse_args()
|
|
74
|
+
|
|
75
|
+
if args.verbose:
|
|
76
|
+
logger.setLevel(logging.DEBUG)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
generate_thumbnail(args.inputfile, args.outfile, size=args.size, force_resize=args.force_resize)
|
|
80
|
+
except IconExtractorError as e:
|
|
81
|
+
logger.error("Failed to thumbnail %s: %s", args.inputfile, e)
|
|
82
|
+
logger.debug("Backtrace:", exc_info=True)
|
icoextract/types.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2019-2026 James Lu <james@overdrivenetworks.com>
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
icoextract common types.
|
|
7
|
+
|
|
8
|
+
See https://devblogs.microsoft.com/oldnewthing/20120720-00/?p=7083
|
|
9
|
+
for a reference on ICO structures.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import ctypes
|
|
13
|
+
|
|
14
|
+
BYTE = ctypes.c_uint8
|
|
15
|
+
WORD = ctypes.c_uint16
|
|
16
|
+
DWORD = ctypes.c_uint32
|
|
17
|
+
|
|
18
|
+
class ResourceID:
|
|
19
|
+
"""Resource ID wrapper.
|
|
20
|
+
|
|
21
|
+
For resources with a string ID, str() will return its string value
|
|
22
|
+
(e.g. "IDI_MAIN_ICON") and int() will return its underlying numeric ID.
|
|
23
|
+
|
|
24
|
+
Numerical icon resource IDs can be accessed via int(), and str() will return
|
|
25
|
+
the number casted to a string.
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self, raw_id: int | None = None, name: str | None = None):
|
|
28
|
+
if raw_id is None and name is None:
|
|
29
|
+
raise ValueError("raw_id and name cannot both be None")
|
|
30
|
+
self.raw_id = raw_id
|
|
31
|
+
self.name = name
|
|
32
|
+
|
|
33
|
+
def __str__(self):
|
|
34
|
+
return self.name or str(self.raw_id)
|
|
35
|
+
|
|
36
|
+
def __int__(self):
|
|
37
|
+
if self.raw_id is not None:
|
|
38
|
+
return self.raw_id
|
|
39
|
+
raise ValueError(f"Resource ID {self.name!r} cannot be converted to an int")
|
|
40
|
+
|
|
41
|
+
def __repr__(self):
|
|
42
|
+
if self.name:
|
|
43
|
+
return f'{self.__class__.__name__}({self.name!r})'
|
|
44
|
+
return f'{self.__class__.__name__}({self.raw_id})'
|
|
45
|
+
|
|
46
|
+
class GroupIconDirEntry(ctypes.LittleEndianStructure):
|
|
47
|
+
"""ICO GRPICONDIRENTRY / RESDIR structure"""
|
|
48
|
+
_pack_ = 1 # Ensures tight packing (no padding)
|
|
49
|
+
_fields_ = [
|
|
50
|
+
("Width", BYTE),
|
|
51
|
+
("Height", BYTE),
|
|
52
|
+
("ColorCount", BYTE),
|
|
53
|
+
("Reserved", BYTE),
|
|
54
|
+
("Planes", WORD),
|
|
55
|
+
("BitCount", WORD),
|
|
56
|
+
("BytesInRes", DWORD),
|
|
57
|
+
("ID", WORD),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
class GroupIconDir(ctypes.LittleEndianStructure):
|
|
61
|
+
"""ICO GRPICONDIR / NEWHEADER structure"""
|
|
62
|
+
_pack_ = 1
|
|
63
|
+
_fields_ = [
|
|
64
|
+
("Reserved", WORD),
|
|
65
|
+
("Type", WORD),
|
|
66
|
+
("Count", WORD),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
ExtractedGroupIcon = list[tuple[GroupIconDirEntry, bytes]]
|
|
70
|
+
GroupIconWithIconOffsets = list[tuple[GroupIconDirEntry, int, int]]
|
icoextract/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.3.0'
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: icoextract
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Windows EXE icon extractor
|
|
5
|
+
Author-email: James Lu <james@overdrivenetworks.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
10
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Operating System :: POSIX
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: pefile
|
|
23
|
+
Provides-Extra: thumbnailer
|
|
24
|
+
Requires-Dist: Pillow; extra == "thumbnailer"
|
|
25
|
+
Provides-Extra: win16
|
|
26
|
+
Requires-Dist: nefile; extra == "win16"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: icoextract[thumbnailer]; extra == "dev"
|
|
29
|
+
Requires-Dist: icoextract[win16]; extra == "dev"
|
|
30
|
+
Requires-Dist: pdoc; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# icoextract
|
|
34
|
+
|
|
35
|
+
[](https://woodpecker.overdrivenetworks.com/repos/5)
|
|
36
|
+
[](https://repology.org/project/icoextract/versions)
|
|
37
|
+
|
|
38
|
+
**icoextract** is an icon extractor library for Windows executables (.exe/.dll/.mun). It also includes a thumbnailer script (`exe-thumbnailer`) for Linux desktops.
|
|
39
|
+
|
|
40
|
+
This project is inspired by [extract-icon-py](https://github.com/firodj/extract-icon-py), [icoutils](https://www.nongnu.org/icoutils/), and others.
|
|
41
|
+
|
|
42
|
+
icoextract aims to be:
|
|
43
|
+
|
|
44
|
+
- Lightweight
|
|
45
|
+
- Portable (cross-platform)
|
|
46
|
+
- Fast on large files
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
### Dependencies
|
|
51
|
+
|
|
52
|
+
- Python 3.10+
|
|
53
|
+
- [pefile](https://github.com/erocarrera/pefile)
|
|
54
|
+
- [Pillow](https://pillow.readthedocs.io/en/stable/) - optional, for exe-thumbnailer
|
|
55
|
+
- [nefile](https://github.com/npjg/nefile) - optional, for Win16 / NE EXE support
|
|
56
|
+
|
|
57
|
+
### Installing from source
|
|
58
|
+
|
|
59
|
+
You can install the project via pip: `pip3 install icoextract[thumbnailer]`
|
|
60
|
+
|
|
61
|
+
On Linux, you can activate the thumbnailer by copying [`exe-thumbnailer.thumbnailer`](/exe-thumbnailer.thumbnailer) into the thumbnailers directory:
|
|
62
|
+
|
|
63
|
+
- `/usr/local/share/thumbnailers/` if you installed `icoextract` globally
|
|
64
|
+
- `~/.local/share/thumbnailers` if you installed `icoextract` for your user only
|
|
65
|
+
|
|
66
|
+
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.
|
|
67
|
+
|
|
68
|
+
### Distribution packages
|
|
69
|
+
|
|
70
|
+
You can install icoextract from any of these distribution repositories:
|
|
71
|
+
|
|
72
|
+
[](https://repology.org/project/icoextract/versions)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
icoextract ships `icoextract` and `icolist` scripts to extract and list icon resources inside a file.
|
|
78
|
+
|
|
79
|
+
**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.
|
|
80
|
+
|
|
81
|
+
For API docs, see https://projects.jlu5.com/icoextract.html
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
usage: icoextract [-h] [-V] [-n NUM] [-i ID] [-v] input output
|
|
85
|
+
|
|
86
|
+
Windows executable icon extractor.
|
|
87
|
+
|
|
88
|
+
positional arguments:
|
|
89
|
+
input input filename (.exe/.dll/.mun)
|
|
90
|
+
output output filename (.ico)
|
|
91
|
+
|
|
92
|
+
options:
|
|
93
|
+
-h, --help show this help message and exit
|
|
94
|
+
-V, --version show program's version number and exit
|
|
95
|
+
-n, --num NUM index of icon to extract
|
|
96
|
+
-i, --id ID resource ID of icon to extract
|
|
97
|
+
-v, --verbose enables debug logging
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
usage: icolist [-h] [-V] [-v] input
|
|
102
|
+
|
|
103
|
+
Lists group icons present in a program.
|
|
104
|
+
|
|
105
|
+
positional arguments:
|
|
106
|
+
input input filename
|
|
107
|
+
|
|
108
|
+
options:
|
|
109
|
+
-h, --help show this help message and exit
|
|
110
|
+
-V, --version show program's version number and exit
|
|
111
|
+
-v, --verbose enables debug logging
|
|
112
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
icoextract/__init__.py,sha256=z5OWuLNir146vZW21b3a8SOQEL8iyLbvPsrJgB9FW6Q,3042
|
|
2
|
+
icoextract/base_extractor.py,sha256=hbg8gGsIvPIiSc50WINKPb-ilbhcn2lyIeLHOiTZB3w,2754
|
|
3
|
+
icoextract/exceptions.py,sha256=QyQBOXh-gQIMS-lLCWN7IJ5OsECc9X0kir5S6AbBpqc,747
|
|
4
|
+
icoextract/ne_extractor.py,sha256=DjpsD62HKs6vBeiBcQVkH9cJyKXEkbvIXrRulYzarsc,4530
|
|
5
|
+
icoextract/pe_extractor.py,sha256=uqqS2pBz_7P7dDDoW31KsSzFjgMicPugoG5ut5QSoZo,7616
|
|
6
|
+
icoextract/types.py,sha256=L9dSrbUV-kWQYQYyg7N8TowiXNIlsKw_1ZWq3rLfB6Y,2055
|
|
7
|
+
icoextract/version.py,sha256=3wVEs2QD_7OcTlD97cZdCeizd2hUbJJ0GeIO8wQIjrk,22
|
|
8
|
+
icoextract/scripts/__init__.py,sha256=sdmatvCNSK7IH6O60271YMOvBgiQNuGCKHMYzmpbvOE,48
|
|
9
|
+
icoextract/scripts/extract.py,sha256=sdIp9lT397mJwPVZE-xHpskE-Ix7n4zqA6MhfzsGgIY,1490
|
|
10
|
+
icoextract/scripts/icolist.py,sha256=JCPBWF-S-zdym52m_1Ort1Aq6v1lwmEOBM9MBJFtRAQ,1632
|
|
11
|
+
icoextract/scripts/thumbnailer.py,sha256=YPELdFojgctUk1P0CV9n0iCM0fVOyhPE2G5VJzC0gRU,3425
|
|
12
|
+
icoextract-0.3.0.dist-info/licenses/LICENSE,sha256=fEExFMN-Gl3DjZptI7fHEbCD8_J1ONExzTRyqyKWrkk,1149
|
|
13
|
+
icoextract-0.3.0.dist-info/METADATA,sha256=2zoUdXiAR4LEFvv08I_V2FY7gd134gdT7u2s0xDFj08,4394
|
|
14
|
+
icoextract-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
15
|
+
icoextract-0.3.0.dist-info/entry_points.txt,sha256=7D4Ml66GpVxArdjGH8FkiU6vGRuFWfcyEBnSUCEDNHo,173
|
|
16
|
+
icoextract-0.3.0.dist-info/top_level.txt,sha256=vDrv1SnIBDoCSBkNAzC6EJdwNhNhWTwBXP2kMC9o3rQ,11
|
|
17
|
+
icoextract-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-2016 Fadhil Mandaga
|
|
4
|
+
Copyright (c) 2019-2026 James Lu <james@overdrivenetworks.com>
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
icoextract
|