Skip to content

Instantly share code, notes, and snippets.

@tresf
Last active November 2, 2025 19:26
Show Gist options
  • Select an option

  • Save tresf/b548ab5287f0106005f43f9afb742bdc to your computer and use it in GitHub Desktop.

Select an option

Save tresf/b548ab5287f0106005f43f9afb742bdc to your computer and use it in GitHub Desktop.
// 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