OpenMiInk/extractors/eink.ts
Hellings b8c8ff98e2 Hi
2026-10-16 09:05:56 -04:00

189 lines
8.2 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
const root = path.resolve(import.meta.dir, "..");
const apk = path.resolve(
process.argv[2] ?? path.join(root, "inputs", "MoanNote.apk"),
);
const output = path.resolve(
process.argv[3] ?? path.join(root, "extracted", "xiaomi-eink"),
);
const decoded = path.join(output, ".decoded");
const apktool = path.join(root, "java", "apktool_2.4.1.jar");
function copyTree(source: string, destination: string): void {
fs.mkdirSync(destination, { recursive: true });
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
const from = path.join(source, entry.name);
const to = path.join(destination, entry.name);
if (entry.isDirectory()) copyTree(from, to);
else fs.copyFileSync(from, to);
}
}
function write(file: string, content: string): void {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, content.trimStart());
}
function main(): void {
if (!fs.existsSync(apk)) throw new Error(`APK not found at ${apk}`);
fs.rmSync(decoded, { recursive: true, force: true });
fs.mkdirSync(output, { recursive: true });
const result = spawnSync(
"java",
["-jar", apktool, "d", "-f", "-r", apk, "-o", decoded],
{ stdio: "inherit" },
);
if (result.status !== 0)
throw new Error(`Apktool decode failed with status ${result.status}`);
const nativeSource = path.join(decoded, "lib");
if (!fs.existsSync(nativeSource))
throw new Error("MoanNote contains no native libraries");
const jniLibs = path.join(output, "android", "src", "main", "jniLibs");
for (const abi of fs.readdirSync(nativeSource)) {
const library = path.join(nativeSource, abi, "libfakeRKEpdUtils.so");
if (!fs.existsSync(library)) continue;
fs.mkdirSync(path.join(jniLibs, abi), { recursive: true });
fs.copyFileSync(library, path.join(jniLibs, abi, path.basename(library)));
}
const javaRoot = path.join(output, "android", "src", "main", "java");
write(
path.join(javaRoot, "com/moan/fakerkepdlib/EpdUtil.java"),
nativeBinding,
);
write(
path.join(javaRoot, "rip/crit/ms/eink/XiaomiEinkRenderer.java"),
facade,
);
write(path.join(output, "SYMBOLS.md"), symbolMap);
write(path.join(output, "README.md"), extractedReadme);
fs.rmSync(decoded, { recursive: true, force: true });
console.log(`[ok] Extracted E Ink compatibility library to ${output}`);
}
const nativeBinding = String.raw`
package com.moan.fakerkepdlib;
import android.graphics.Bitmap;
import android.graphics.PointF;
import android.graphics.RectF;
public final class EpdUtil {
static { System.loadLibrary("fakeRKEpdUtils"); }
private EpdUtil() {}
private static native int JniCall_ClearOverlayBuffer(int id, int color, int sendOsd);
private static native int JniCall_CommitOverlay(Bitmap bitmap, int id, float left, float top, float right, float bottom, float x, float y);
private static native int JniCall_ObtainEBC(float[] bounds);
private static native void JniCall_ReleaseEBC(int id);
private static native int JniCall_SetOverLayBgControl(int id, int mode);
private static native int JniCall_SetOverLayCommit(int id, int enabled);
private static native int JniCall_test(int[] values);
public static EBCData sInitEBC() {
float[] bounds = new float[4];
int id = JniCall_ObtainEBC(bounds);
if (id < 0) return null;
EBCData data = new EBCData();
data.mDataID = id;
data.mCommitBufRect.set(bounds[0], bounds[1], bounds[2], bounds[3]);
return data;
}
public static void sRecycleEBC(EBCData data) { if (data != null) JniCall_ReleaseEBC(data.mDataID); }
public static int sSetFastBgRefresh(EBCData data, boolean enabled) { return JniCall_SetOverLayBgControl(data.mDataID, enabled ? 0 : 1); }
public static int sSetFastHandWrite(EBCData data, boolean enabled) { return JniCall_SetOverLayCommit(data.mDataID, enabled ? 1 : 0); }
public static int sCommitColor(EBCData data, EBCCommitRequest request, EBCCommitResult result) {
return JniCall_ClearOverlayBuffer(data.mDataID, request.mCommitColor, request.mSendOsd);
}
public static int sCommitImage(EBCData data, EBCCommitRequest request, EBCCommitResult result) {
int code = request.mCommitType == EBCCommitRequest.COMMIT_CLEAR_COLOR
? JniCall_ClearOverlayBuffer(data.mDataID, request.mCommitColor, request.mSendOsd)
: JniCall_CommitOverlay(request.mCommitImg, data.mDataID, request.mCommitSrcRect.left,
request.mCommitSrcRect.top, request.mCommitSrcRect.right, request.mCommitSrcRect.bottom,
request.mCommitPt.x, request.mCommitPt.y);
if (code > 0) result.mResultCode = 0;
return code;
}
public static final class EBCData {
public int mDataID;
public final RectF mCommitBufRect = new RectF();
public int getEBCRect(RectF destination) { destination.set(mCommitBufRect); return 0; }
}
public static final class EBCCommitResult { public int mResultCode; }
public static final class EBCCommitRequest {
public static final int COMMIT_NORMAL = 0, COMMIT_WITH_SCALE = 1, COMMIT_WITH_ROTATE = 2, COMMIT_CLEAR_COLOR = 4;
public int mCommitColor, mCommitType, mSendOsd;
public final RectF mCommitDstRect = new RectF();
public Bitmap mCommitImg;
public final PointF mCommitPt = new PointF();
public final RectF mCommitSrcRect = new RectF();
}
}
`;
const facade = String.raw`
package rip.crit.ms.eink;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.view.View;
import com.moan.fakerkepdlib.EpdUtil;
public final class XiaomiEinkRenderer implements AutoCloseable {
private final EpdUtil.EBCData buffer;
public XiaomiEinkRenderer() {
buffer = EpdUtil.sInitEBC();
if (buffer == null) throw new IllegalStateException("The Xiaomi E Ink buffer is unavailable");
}
public void setFastHandwriting(boolean enabled) { EpdUtil.sSetFastHandWrite(buffer, enabled); }
public void setFastBackgroundRefresh(boolean enabled) { EpdUtil.sSetFastBgRefresh(buffer, enabled); }
public int commit(Bitmap bitmap, Rect dirtyArea) {
EpdUtil.EBCCommitRequest request = new EpdUtil.EBCCommitRequest();
request.mCommitImg = bitmap;
request.mCommitSrcRect.set(dirtyArea);
request.mCommitPt.set(dirtyArea.left, dirtyArea.top);
return EpdUtil.sCommitImage(buffer, request, new EpdUtil.EBCCommitResult());
}
public int commit(View view, Rect dirtyArea) {
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
view.draw(new Canvas(bitmap));
try { return commit(bitmap, dirtyArea); } finally { bitmap.recycle(); }
}
public int clear(int color) {
EpdUtil.EBCCommitRequest request = new EpdUtil.EBCCommitRequest();
request.mCommitType = EpdUtil.EBCCommitRequest.COMMIT_CLEAR_COLOR;
request.mCommitColor = color;
return EpdUtil.sCommitColor(buffer, request, new EpdUtil.EBCCommitResult());
}
@Override public void close() { EpdUtil.sRecycleEBC(buffer); }
}
`;
const symbolMap = `# Resolved symbol map
The compatibility class keeps its original binary names because the JNI exports depend on them. New callers use the renamed facade.
| Original symbol | Resolved name |
| --- | --- |
| EpdUtil.sInitEBC | XiaomiEinkRenderer constructor |
| EpdUtil.sRecycleEBC | XiaomiEinkRenderer.close |
| EpdUtil.sCommitImage | XiaomiEinkRenderer.commit |
| EpdUtil.sCommitColor | XiaomiEinkRenderer.clear |
| EpdUtil.sSetFastBgRefresh | XiaomiEinkRenderer.setFastBackgroundRefresh |
| EpdUtil.sSetFastHandWrite | XiaomiEinkRenderer.setFastHandwriting |
| EBCData | Native E Ink buffer handle |
| EBCCommitRequest | Image commit request |
| EBCCommitResult | Image commit result |
`;
const extractedReadme = `# Xiaomi Mi Reader E Ink compatibility library
This artifact is reconstructed from the supplied MoanNote APK. Copy the contents of \`android/src/main\` into an Android app module. The original JNI binding package is retained for binary compatibility. Application code should import \`rip.crit.ms.eink.XiaomiEinkRenderer\`.
The native library is device-specific and should only be exercised on the intended Xiaomi Mi Reader firmware.
`;
main();