Last active
November 2, 2025 19:26
-
-
Save tresf/b548ab5287f0106005f43f9afb742bdc to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // TODO: This successfully replaces the icon but the EXE won't run afterwards | |
| package com.company; | |
| import com.kichik.pecoff4j.PE; | |
| import com.kichik.pecoff4j.ResourceDirectory; | |
| import com.kichik.pecoff4j.ResourceDirectoryTable; | |
| import com.kichik.pecoff4j.ResourceEntry; | |
| import com.kichik.pecoff4j.constant.ResourceType; | |
| import com.kichik.pecoff4j.io.DataReader; | |
| import com.kichik.pecoff4j.io.DataWriter; | |
| import com.kichik.pecoff4j.io.PEParser; | |
| import com.kichik.pecoff4j.resources.GroupIconDirectory; | |
| import com.kichik.pecoff4j.resources.GroupIconDirectoryEntry; | |
| import com.kichik.pecoff4j.resources.IconDirectoryEntry; | |
| import com.kichik.pecoff4j.resources.IconImage; | |
| import com.kichik.pecoff4j.util.IconFile; | |
| import com.kichik.pecoff4j.util.PaddingType; | |
| import java.io.*; | |
| import java.lang.reflect.Field; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| public class ReplaceIcon { | |
| public static void main(String... args) throws IOException { | |
| String inputExePath = "/Users/owner/tray/out/jlink/jdk-bellsoft-aarch64-windows-25.37.0/jdk-25/bin/java.exe"; | |
| String newIconPath = "/Users/owner/tray/assets/branding/windows-icon.ico"; | |
| String outputExePath = "/Users/owner/Desktop/java_qz2.exe"; | |
| replaceIcon(inputExePath, newIconPath, outputExePath); | |
| } | |
| // Any value should work, but 0 is what java.exe used when testing | |
| private static final int ICON_ID = 1033; | |
| // Any value should work, but 2000 is what java.exe used when testing | |
| private static final int GROUP_ID = 2000; | |
| public static void replaceIcon(String inputExePath, String newIconPath, String outputExePath) throws IOException { | |
| // 1. Parse the existing executable file | |
| System.out.println("Parsing input EXE: " + inputExePath); | |
| PE pe = PEParser.parse(new FileInputStream(inputExePath)); | |
| ResourceDirectory directory = pe.getImageData().getResourceTable(); | |
| if (directory == null) { | |
| System.err.println("Error: Executable has no resource table to modify."); | |
| return; | |
| } | |
| // 2. Load the new icon file (.ico) | |
| System.out.println("Loading new icon: " + newIconPath); | |
| IconFile iconFile = IconFile.read(new DataReader(new FileInputStream(newIconPath))); | |
| // TODO: REMOVE THIS: Try to debug existing layout | |
| System.out.println("### BEFORE:\n"); | |
| debugDirectory(directory, ""); | |
| // 3. Remove existing icon resources (Type 3: ICON, Type 14: GROUP_ICON) | |
| System.out.println("Removing existing icon resources..."); | |
| directory.getEntries().removeIf( | |
| entry -> entry.getId() == ResourceType.ICON || | |
| entry.getId() == ResourceType.GROUP_ICON | |
| ); | |
| // 4. Add the new ICON entries (Resource Type 3) | |
| // Each image in the ICO file becomes a separate ICON resource | |
| List<ResourceEntry> iconEntries = new ArrayList<>(); | |
| for (IconDirectoryEntry entry : iconFile.getDirectory().getEntries()) { | |
| // The directory structure for an ICON resource is: Type 3 -> Icon ID -> Raw Icon Data | |
| iconEntries.add(entry(iconEntries.size() + 1, | |
| directory(entry(ICON_ID, iconFile.getImage(entry).toByteArray())))); | |
| } | |
| // Add ICON in index 0 (seems to fix preview in explorer) | |
| directory.getEntries().add(0, entry(ResourceType.ICON, | |
| directory(iconEntries.toArray(new ResourceEntry[0])))); | |
| // 5. Add the new GROUP_ICON entry (Resource Type 14) | |
| // This directory acts as the 'pointer' to the individual ICON resources | |
| System.out.println("Adding new GROUP_ICON resource..."); | |
| byte[] iconDirData = createIconDirectory(iconFile).toByteArray(); | |
| // The directory structure for GROUP_ICON is: Type 14 -> Icon Index (usually 1) -> Group Icon Directory Data | |
| // Add GROUP_ICON in index 1 (seems to fix preview in explorer) | |
| directory.getEntries().add(1, entry(ResourceType.GROUP_ICON, | |
| directory(entry(GROUP_ID, directory(entry(ICON_ID, iconDirData)))))); | |
| // 6. Rebuild the PE structure and update offsets | |
| System.out.println("Rebuilding PE structure..."); | |
| pe.rebuild(PaddingType.PATTERN); | |
| //pe.rebuild(PaddingType.ZEROS); | |
| // TODO: REMOVE THIS: Try to debug existing layout | |
| System.out.println("### AFTER:\n"); | |
| debugDirectory(directory, ""); | |
| // 7. Write the modified PE to a new file | |
| System.out.println("Writing modified EXE to: " + outputExePath); | |
| DataWriter writer = new DataWriter(new FileOutputStream(outputExePath)); | |
| pe.write(writer); | |
| System.out.println("Icon replacement complete!"); | |
| } | |
| // --- Helper methods copied and simplified from ResourceDirectoryTest --- | |
| private static GroupIconDirectory createIconDirectory(IconFile iconFile) { | |
| GroupIconDirectory directory = new GroupIconDirectory(); | |
| directory.setReserved(0); | |
| directory.setType(1); // 1 for ICON | |
| int id = 1; | |
| for (IconDirectoryEntry iconEntry : iconFile.getDirectory().getEntries()) { | |
| IconImage icon = iconFile.getImage(iconEntry); | |
| if(icon.getHeader() == null) continue; | |
| GroupIconDirectoryEntry entry = new GroupIconDirectoryEntry(); | |
| entry.setWidth(icon.getHeader().getWidth()); | |
| // The height in the GroupIconDirectory is half the height in the BITMAPINFOHEADER (part of IconImage) | |
| entry.setHeight(icon.getHeader().getHeight() / 2); | |
| entry.setColorCount(0); // If 0, actual color count is stored elsewhere | |
| entry.setReserved(0); | |
| entry.setPlanes(icon.getHeader().getPlanes()); | |
| entry.setBitCount(icon.getHeader().getBitCount()); | |
| entry.setBytesInRes(icon.sizeOf()); | |
| entry.setId(id++); // Reference ID to the individual ICON resource (Type 3) | |
| directory.getEntries().add(entry); | |
| } | |
| return directory; | |
| } | |
| private static ResourceEntry entry(int id, ResourceDirectory directory) { | |
| ResourceEntry entry = new ResourceEntry(); | |
| entry.setId(id); | |
| entry.setDirectory(directory); | |
| return entry; | |
| } | |
| private static ResourceEntry entry(int id, byte[] data) { | |
| ResourceEntry entry = new ResourceEntry(); | |
| entry.setId(id); | |
| entry.setCodePage(1252); // Default Windows ANSI code page | |
| entry.setData(data); | |
| return entry; | |
| } | |
| private static ResourceDirectory directory(ResourceEntry... entries) { | |
| ResourceDirectory dir = new ResourceDirectory(); | |
| ResourceDirectoryTable table = new ResourceDirectoryTable(); | |
| table.setMajorVersion(4); | |
| dir.setTable(table); | |
| for (ResourceEntry entry : entries) { | |
| dir.getEntries().add(entry); | |
| } | |
| return dir; | |
| } | |
| private static String getResourceType(int i) { | |
| // Check resource classifications | |
| for(Field f : ResourceType.class.getFields()) { | |
| try { | |
| if (i == f.getInt(null)) { | |
| return f.getName() + " (" + i + ")"; | |
| } | |
| } catch(IllegalArgumentException | IllegalAccessException e) { | |
| e.printStackTrace(); | |
| } | |
| } | |
| // Check language | |
| switch(i) { | |
| case 1025: return "Arabic" + " (" + i + ")"; | |
| case 1026: return "Bulgarian - Cyrillic" + " (" + i + ")"; | |
| case 1027: return "Catalan" + " (" + i + ")"; | |
| case 1028: return "Traditional Chinese Taiwan" + " (" + i + ")"; | |
| case 1029: return "Czech" + " (" + i + ")"; | |
| case 1030: return "Danish Norwegian" + " (" + i + ")"; | |
| case 1031: return "German Standard (Germany)" + " (" + i + ")"; | |
| case 1032: return "Greek" + " (" + i + ")"; | |
| case 1033: return "English (United States)" + " (" + i + ")"; | |
| case 1035: return "Finnish Swedish (Finland)" + " (" + i + ")"; | |
| case 1036: return "French (France)" + " (" + i + ")"; | |
| case 1037: return "Hebrew" + " (" + i + ")"; | |
| case 1038: return "Hungarian" + " (" + i + ")"; | |
| case 1040: return "Italian (Italy)" + " (" + i + ")"; | |
| case 1041: return "Japanese - Stoke" + " (" + i + ")"; | |
| case 1042: return "Korean" + " (" + i + ")"; | |
| case 1043: return "Dutch (Netherlands)" + " (" + i + ")"; | |
| case 1044: return "Danish Norwegian - Bokmaal" + " (" + i + ")"; | |
| case 1045: return "Polish" + " (" + i + ")"; | |
| case 1046: return "Brazilian Portuguese" + " (" + i + ")"; | |
| case 1048: return "Romanian" + " (" + i + ")"; | |
| case 1049: return "Russian (Russia) - Cyrillic" + " (" + i + ")"; | |
| case 1050: return "Croatian" + " (" + i + ")"; | |
| case 1051: return "Slovak" + " (" + i + ")"; | |
| case 1053: return "Finnish Swedish (Sweden)" + " (" + i + ")"; | |
| case 1054: return "Thai" + " (" + i + ")"; | |
| case 1055: return "Turkish" + " (" + i + ")"; | |
| case 1057: return "Indonesian" + " (" + i + ")"; | |
| case 1058: return "Ukrainian" + " (" + i + ")"; | |
| case 1060: return "Slovenian" + " (" + i + ")"; | |
| case 1061: return "Estonian" + " (" + i + ")"; | |
| case 1062: return "Latvian" + " (" + i + ")"; | |
| case 1063: return "Lithuanian" + " (" + i + ")"; | |
| case 1066: return "Vietnamese" + " (" + i + ")"; | |
| case 1069: return "Basque" + " (" + i + ")"; | |
| case 1081: return "Hindi - Latin character" + " (" + i + ")"; | |
| case 1086: return "Malay" + " (" + i + ")"; | |
| case 1087: return "Kazakh" + " (" + i + ")"; | |
| case 1110: return "Galician" + " (" + i + ")"; | |
| case 2052: return "Simplified Chinese (China)" + " (" + i + ")"; | |
| case 2070: return "Portuguese (Portugal)" + " (" + i + ")"; | |
| case 2074: return "Serbian - Latin character set" + " (" + i + ")"; | |
| case 3076: return "Traditional Chinese Hong Kong" + " (" + i + ")"; | |
| case 3082: return "Modern Spanish (Spain)" + " (" + i + ")"; | |
| case 3098: return "Serbian - Cyrillic" + " (" + i + ")"; | |
| } | |
| return "[" + i + "]"; // fallback, assume key id | |
| } | |
| private static void debugDirectory(ResourceDirectory directory, String indent) { | |
| // DEBUG | |
| for(ResourceEntry entry : directory.getEntries()) { | |
| if(entry.getDirectory() != null) { | |
| System.out.println(indent + "- " + getResourceType(entry.getId())); | |
| debugDirectory(entry.getDirectory(), " " + indent); | |
| } else { | |
| System.out.println(indent + " - " + getResourceType(entry.getId())); | |
| } | |
| /*switch(entry.getId()) { | |
| case ResourceType.ICON: | |
| //System.out.println(entry.getId() + ": " + (entry.getData() == null ? null : entry.getData().length)); | |
| break; | |
| case ResourceType.GROUP_ICON: | |
| //System.out.println(entry.getId() + ": " + (entry.getData() == null ? null : entry.getData().length)); | |
| break; | |
| default: | |
| }*/ | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment