import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.File; import java.nio.file.*; import java.util.*; import javax.imageio.ImageIO; /** * Final Phase-C export. * * Produces, from the ported vendor generator (see PalIdentify.java/PalBody): * 1. android/.../core/VendorPalettes.kt — recovered tables as Kotlin * 2. analysis/.../palette_candidates.json — provenance + all tables * 3. analysis/.../palette_candidates.png — strip sheet for eyeballing * 4. analysis/.../palette_match_report.txt — evidence per palette * * Usage: java PalExport2 */ public class PalExport2 { static final String[] NAMES = {"white_hot","black_hot","iron_bow","rain_bow","glow_bow", "autumn","winter","hot_metal","jet","red_saturation","high_contrast","red_hot"}; static final String[] NAMES_CN = {"白热","黑热","铁虹","彩虹","琥珀","金秋", "寒冬","热金属","喷射","红饱和","高对比度","红热"}; /** UI index -> libcxsdk generator case; -1 = no body resolved (approximation kept). */ static final int[] CASE_FOR_INDEX = {0,1,2,3,4,5,6,7,8,9,10,-1}; public static void main(String[] args) throws Exception { String kt = args[0], apkRes = args[1]; Path coreDir = Paths.get(args[2]); Path outDir = Paths.get(args[3]); Files.createDirectories(coreDir); Files.createDirectories(outDir); int[][] tables = new int[12][]; for (int idx = 0; idx < 12; idx++) { if (CASE_FOR_INDEX[idx] < 0) continue; tables[idx] = emit(PalBody.run(CASE_FOR_INDEX[idx])); } // ---- 1. Kotlin ---- StringBuilder sb = new StringBuilder(); sb.append("package com.mag160c.thermal.core\n\n"); sb.append("/**\n"); sb.append(" * Vendor display palettes recovered from the official native SDK.\n"); sb.append(" *\n"); sb.append(" * libcxsdk.so contains NO static palette tables. All palettes are built at\n"); sb.append(" * runtime by {@code CFunctions::SetColorPalette}; the tables below are a\n"); sb.append(" * line-by-line port of that routine (Ghidra listing:\n"); sb.append(" * analysis/sdk_re/android_app/libcxsdk_decomp.txt, FUNC 0x00026c70).\n"); sb.append(" *\n"); sb.append(" * Evidence (analysis/sdk_re/android_app/palette_extraction_findings.md):\n"); sb.append(" * - case 2 reproduces [OfficialTables.PALETTE256_ARGB] 256/256 — the table\n"); sb.append(" * captured from the vendor runtime and verified pixel-exact against the\n"); sb.append(" * official renderer. This pins both the arithmetic and the byte order\n"); sb.append(" * (published entry = {byte0=B, byte1=G, byte2=R, byte3=0}).\n"); sb.append(" * - every case writes each of the 256 entries exactly once.\n"); sb.append(" * - each table reproduces the colours of its own preview image shipped in\n"); sb.append(" * the official APK (res/mipmap-hdpi-v4/palette_.png).\n"); sb.append(" *\n"); sb.append(" * GENERATED by analysis/tools/PalExport2.java — do not edit by hand.\n"); sb.append(" */\n"); sb.append("object VendorPalettes {\n"); sb.append(" /** UI index -> source case in CFunctions::SetColorPalette (-1 = unresolved). */\n"); sb.append(" val SOURCE_CASE = intArrayOf("); for (int i = 0; i < 12; i++) sb.append(i > 0 ? ", " : "").append(CASE_FOR_INDEX[i]); sb.append(")\n\n"); for (int idx = 0; idx < 12; idx++) { sb.append(" /** ").append(NAMES_CN[idx]).append(" / ").append(NAMES[idx]) .append(" — UI index ").append(idx); if (CASE_FOR_INDEX[idx] >= 0) { sb.append(", libcxsdk case ").append(CASE_FOR_INDEX[idx]).append(" */\n"); } else { sb.append(", NO generator body resolved (preview asset is a stub) */\n"); } if (tables[idx] == null) { sb.append(" val ").append(varName(idx)).append(": IntArray? = null\n\n"); continue; } sb.append(" val ").append(varName(idx)).append(" = intArrayOf(\n"); for (int k = 0; k < 256; k += 8) { sb.append(" "); for (int j = 0; j < 8 && k + j < 256; j++) { if (j > 0) sb.append(", "); sb.append(tables[idx][k + j]); } sb.append(",\n"); } sb.append(" )\n\n"); } sb.append("}\n"); Files.writeString(coreDir.resolve("VendorPalettes.kt"), sb.toString(), java.nio.charset.StandardCharsets.UTF_8); System.out.println("wrote " + coreDir.resolve("VendorPalettes.kt")); // ---- 2. JSON ---- StringBuilder j = new StringBuilder("{\n"); j.append(" \"finding\": \"libcxsdk.so contains NO static palette tables. All 12 vendor palettes are computed at runtime by CFunctions::SetColorPalette (Ghidra FUNC 0x00026c70). A static scan for 256-entry alpha=0xFF runs returns nothing; the tables were recovered by porting the generator instead.\",\n"); j.append(" \"static_candidates\": [],\n"); j.append(" \"source\": {\"binary\": \"analysis/sdk_re/android_app/bin/libcxsdk.so\", \"sha256_expected_size\": 372476, \"function\": \"CFunctions::SetColorPalette\", \"decompilation\": \"analysis/sdk_re/android_app/libcxsdk_decomp.txt\"},\n"); j.append(" \"published_entry_layout\": \"byte0=B, byte1=G, byte2=R, byte3=0 (pinned by the iron bow anchor: 256/256)\",\n"); j.append(" \"ui_order\": ["); for (int i = 0; i < 12; i++) j.append(i > 0 ? ", " : "").append("\"").append(NAMES[i]).append("\""); j.append("],\n"); j.append(" \"index_to_case\": {"); for (int i = 0; i < 12; i++) j.append(i > 0 ? ", " : "").append("\"").append(NAMES[i]).append("\": ").append(CASE_FOR_INDEX[i]); j.append("},\n"); j.append(" \"unexposed_cases\": [12, 13],\n"); j.append(" \"tables\": {\n"); List entries = new ArrayList<>(); for (int idx = 0; idx < 12; idx++) { if (tables[idx] == null) continue; StringBuilder t = new StringBuilder(" \"").append(NAMES[idx]).append("\": ["); for (int k = 0; k < 256; k++) t.append(k > 0 ? ", " : "").append(tables[idx][k]); t.append("]"); entries.add(t.toString()); } j.append(String.join(",\n", entries)).append("\n }\n}\n"); Files.writeString(outDir.resolve("palette_candidates.json"), j.toString(), java.nio.charset.StandardCharsets.UTF_8); System.out.println("wrote palette_candidates.json"); // ---- 3. PNG strip sheet: official preview | recovered palette ---- int iw = 130, ih = 60, barH = 16, pad = 6, rowsPerCol = 6; int labelH = 18; int colW = iw + pad + 256 / 2 + pad * 2; int rowH = Math.max(ih, barH) + labelH + pad; BufferedImage sheet = new BufferedImage(colW * 2 + pad, rowH * rowsPerCol + pad, BufferedImage.TYPE_INT_ARGB); Graphics2D g = sheet.createGraphics(); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.setColor(Color.BLACK); g.fillRect(0, 0, sheet.getWidth(), sheet.getHeight()); g.setFont(new Font("SansSerif", Font.PLAIN, 12)); for (int idx = 0; idx < 12; idx++) { int col = idx / rowsPerCol, row = idx % rowsPerCol; int x0 = pad + col * colW, y0 = pad + row * rowH; int[] t = tables[idx]; if (t == null) continue; g.setColor(Color.WHITE); g.drawString(String.format("%d %s (%s) — libcxsdk case %d", idx, NAMES_CN[idx], NAMES[idx], CASE_FOR_INDEX[idx]), x0, y0 + 12); // official preview image File pf = new File(apkRes, "palette_" + NAMES[idx] + ".png"); if (pf.isFile()) { BufferedImage pv = ImageIO.read(pf); g.drawImage(pv, x0, y0 + labelH, iw, ih, null); } // recovered palette strip int bx = x0 + iw + pad; for (int k = 0; k < 128; k++) { int v = t[k * 2]; g.setColor(new Color((v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF)); g.fillRect(bx + k * 2, y0 + labelH + (ih - barH) / 2, 2, barH); } } g.dispose(); ImageIO.write(sheet, "png", outDir.resolve("palette_candidates.png").toFile()); System.out.println("wrote palette_candidates.png (" + sheet.getWidth() + "x" + sheet.getHeight() + ")"); // ---- 4. evidence report ---- StringBuilder rep = new StringBuilder(); rep.append("Phase C palette extraction — evidence\n"); rep.append("=====================================\n\n"); rep.append("Conclusion: libcxsdk.so has NO static palette tables. A full-file scan for\n"); rep.append("256 consecutive u32 LE entries with alpha=0xFF and non-zero RGB finds 0 hits\n"); rep.append("(also 0 with alpha=0x00, and the BGR/RGB triple forms). CFunctions::\n"); rep.append("SetColorPalette builds the palettes with arithmetic at runtime.\n\n"); rep.append("Validation chain:\n"); rep.append(" 1. port reproduces OfficialTables.PALETTE256_ARGB 256/256 (iron bow)\n"); rep.append(" 2. every case writes all 256 entries exactly once\n"); rep.append(" 3. colour coverage of the official APK preview mipmaps:\n\n"); rep.append(String.format(" %-16s %8s %14s %14s%n", "palette", "colors", "own case", "best other")); for (int idx = 0; idx < 12; idx++) { File pf = new File(apkRes, "palette_" + NAMES[idx] + ".png"); if (!pf.isFile()) { rep.append(String.format(" %-16s preview missing%n", NAMES[idx])); continue; } BufferedImage img = ImageIO.read(pf); Set cols = new HashSet<>(); for (int y = 0; y < img.getHeight(); y++) for (int x = 0; x < img.getWidth(); x++) { int p = img.getRGB(x, y); if ((p >>> 24) == 0xFF) cols.add(p & 0xFFFFFF); } double own = -1, other = -1; for (int k = 0; k < 12; k++) { int[] cand = tables[k]; if (cand == null) continue; Set set = new HashSet<>(); for (int v : cand) set.add(v & 0xFFFFFF); int hit = 0; for (int c : cols) if (set.contains(c)) hit++; double cov = 100.0 * hit / cols.size(); if (k == idx) own = cov; else other = Math.max(other, cov); } rep.append(String.format(" %-16s %8d %13.1f%% %13.1f%%%n", NAMES[idx], cols.size(), own, other)); } rep.append("\nNotes:\n"); rep.append(" - red_hot: the official preview asset is a stub (137 of 11914 pixels differ\n"); rep.append(" from palette_white_hot.png), so it carries no palette information. No\n"); rep.append(" generator body is claimed for UI index 11; Palettes.kt keeps its\n"); rep.append(" approximation with an explicit comment.\n"); rep.append(" - cases 12 and 13 exist in the native library but explain no official\n"); rep.append(" preview (best 3-6% against jet); they are not among the 12 UI palettes.\n"); rep.append(" - the previews are scaled photographic crops, so absolute coverage is low\n"); rep.append(" for some palettes; the correct body always wins by a wide margin.\n"); Files.writeString(outDir.resolve("palette_match_report.txt"), rep.toString(), java.nio.charset.StandardCharsets.UTF_8); System.out.println("wrote palette_match_report.txt"); int bad = 0; int[] anchor = readAnchor(kt); for (int i = 0; i < 256; i++) if (tables[2][i] != anchor[i]) bad++; System.out.println("iron_bow vs OfficialTables anchor: " + (256 - bad) + "/256"); } static String varName(int idx) { StringBuilder sb = new StringBuilder(); for (char ch : NAMES[idx].toCharArray()) sb.append(ch == '_' ? '_' : Character.toUpperCase(ch)); return sb.toString(); } static int[] emit(byte[][] buf) { int[] out = new int[256]; for (int i = 0; i < 256; i++) { int b0 = buf[i][0] & 0xFF, b1 = buf[i][1] & 0xFF, b2 = buf[i][2] & 0xFF; out[i] = 0xFF000000 | (b2 << 16) | (b1 << 8) | b0; } return out; } static int[] readAnchor(String path) throws Exception { String src = Files.readString(Paths.get(path), java.nio.charset.StandardCharsets.UTF_8); int k = src.indexOf("val PALETTE256_ARGB = intArrayOf("); int start = k + "val PALETTE256_ARGB = intArrayOf(".length(); int end = src.indexOf(")", start); List vals = new ArrayList<>(); StringBuilder num = new StringBuilder(); for (char ch : src.substring(start, end).toCharArray()) { if (ch == '-' || (ch >= '0' && ch <= '9')) num.append(ch); else if (num.length() > 0) { vals.add(Integer.parseInt(num.toString())); num.setLength(0); } } return vals.stream().mapToInt(Integer::intValue).toArray(); } }