前言

最近在一个Discord群里跟一群大佬们折腾老的日系传统平台手机,有群友发现SH-03E这个MOAP(Symbian)设备使用的是eMMC,并且成功进行了Dump,历经一些研究,我和另一位群友一起研究了ROFS相关内容,成功解压了它的Z分区,Z分区中很多图符资源都是使用MBM(MultiBitmap/多重位图)格式打包并压缩的,需要使用塞班SDK里面的bmconv小工具来解压,但是其使用逻辑很奇怪,所以这次尝试写了一个图形界面。又由于群内很多人使用的是Linux,所以这个小软件支持Windows/Linux双平台使用。

注意

请使用ARCHIVE.ORG取得塞班SDK,安装之后在<安装目录>\epoc32\bin下找到bmconv的Windows和Linux可执行文件

截图

Windows下
MBME_Windows
Linux下
MBME_Linux

代码

import tkinter as tk
from tkinter import filedialog, messagebox, Toplevel, Text, Scrollbar
from tkinter.ttk import Progressbar
from subprocess import Popen, PIPE
from PIL import Image, ImageTk
import os
import re


class mbmExtractor:
    def __init__(self, root):
        self.root = root
        self.uiInit()
        self.mbmFile = ""
        self.currentImageIndex = 0
        self.bmpFiles = []
        self.tempDir = "temp"
        if not os.path.exists(self.tempDir):
            os.makedirs(self.tempDir)

    def uiInit(self):
        self.root.title("MultiBitMap Extractor")
        self.root.geometry('800x600')

        self.previewAreaPanel = tk.Label(self.root, text="Preview Area", relief=tk.SUNKEN)
        self.previewAreaPanel.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        navigationFrame = tk.Frame(self.root)
        navigationFrame.pack(side=tk.TOP, fill=tk.X)

        self.counterLabel = tk.Label(navigationFrame, text="No Bitmaps")
        self.counterLabel.pack(side=tk.LEFT, padx=5)

        self.prevButton = tk.Button(navigationFrame, text="Prev", command=self.prevImage)
        self.prevButton.pack(side=tk.LEFT, padx=5)

        self.nextButton = tk.Button(navigationFrame, text="Next", command=self.nextImage)
        self.nextButton.pack(side=tk.LEFT, padx=5)

        actionFrame = tk.Frame(self.root)
        actionFrame.pack(side=tk.BOTTOM, fill=tk.X)

        self.openButton = tk.Button(actionFrame, text="Open", command=self.askOpenFile)
        self.openButton.pack(side=tk.LEFT, padx=5)

        self.extractButton = tk.Button(actionFrame, text="Extract", state='disabled', command=self.extractMbm)
        self.extractButton.pack(side=tk.LEFT, padx=5)

        self.debugButton = tk.Button(actionFrame, text="Debug", command=self.debugInfoForm)
        self.debugButton.pack(side=tk.LEFT, padx=5)

        self.progressBar = Progressbar(actionFrame, orient=tk.HORIZONTAL, length=100, mode='determinate')
        self.progressBar.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)

    def askOpenFile(self):
        self.mbmFile = filedialog.askopenfilename(
            title="Choose MBM file",
            filetypes=(("MultiBitMap File", "*.mbm"), ("All Files", "*.*"))
        )
        if self.mbmFile:
            self.extractButton['state'] = 'normal'
            mbmFilename = os.path.basename(self.mbmFile)
            self.root.title(f"MultiBitMap Extractor - {mbmFilename}")

    def extractMbm(self):
        if not self.mbmFile:
            messagebox.showerror("ERROR", "No MBM file selected.")
            return

        bitmapCount = self.counterBitmap()
        if bitmapCount > 0:
            bmpArgs = " ".join(f'"{os.path.join(self.tempDir, f"{i}.bmp")}"' for i in range(1, bitmapCount + 1))
            for i in range(1, bitmapCount + 1):
                self.bmconvAdapter(f'/u "{self.mbmFile}" {bmpArgs}')
                self.progressBar['value'] = (i / bitmapCount) * 100
                self.root.update_idletasks()
            self.loadBitmap()
            self.displayBitmap(self.currentImageIndex)
            self.progressBar['value'] = 0
        else:
            messagebox.showinfo("INFO", "No bitmaps found in the MBM file.")

    def counterBitmap(self):
        output = self.bmconvAdapter(f'/v "{self.mbmFile}"')
        match = re.search(r'containing (\d+) bitmaps', output)
        return int(match.group(1)) if match else 0

    def loadBitmap(self):
        self.bmpFiles = self.sortNumber(os.listdir(self.tempDir))
        self.currentImageIndex = 0
        self.updateNavigationButtonStatus()
        self.updateCounterLabel()

    @staticmethod
    def sortNumber(data):
        convert = lambda text: int(text) if text.isdigit() else text.lower()
        alphanumKey = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
        return sorted(data, key=alphanumKey)

    def displayBitmap(self, index):
        if 0 <= index < len(self.bmpFiles):
            imgPath = os.path.join(self.tempDir, self.bmpFiles[index])
            try:
                img = Image.open(imgPath)
                imgtk = ImageTk.PhotoImage(image=img)
                self.previewAreaPanel.config(image=imgtk)
                self.previewAreaPanel.image = imgtk
            except IOError as e:
                messagebox.showerror("ERROR", f"Can't open bitmap file {imgPath}:\n{e}")

    def prevImage(self):
        if self.currentImageIndex > 0:
            self.currentImageIndex -= 1
            self.displayBitmap(self.currentImageIndex)
            self.updateNavigationButtonStatus()
            self.updateCounterLabel()

    def nextImage(self):
        if self.currentImageIndex < len(self.bmpFiles) - 1:
            self.currentImageIndex += 1
            self.displayBitmap(self.currentImageIndex)
            self.updateNavigationButtonStatus()
            self.updateCounterLabel()

    def updateCounterLabel(self):
        text = f"{self.currentImageIndex + 1}/{len(self.bmpFiles)}"
        self.counterLabel.config(text=text)

    def updateNavigationButtonStatus(self):
        self.prevButton['state'] = 'normal' if self.currentImageIndex > 0 else 'disabled'
        self.nextButton['state'] = 'normal' if self.currentImageIndex < len(self.bmpFiles) - 1 else 'disabled'

    def debugInfoForm(self):
        debugForm = Toplevel(self.root)
        debugForm.title("Debug Info")
        debugForm.geometry('400x300')

        scrollbar = Scrollbar(debugForm)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        text = Text(debugForm, yscrollcommand=scrollbar.set)
        text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.config(command=text.yview)

        debugOutput = self.bmconvAdapter(f'/v "{self.mbmFile}"')
        text.insert(tk.END, debugOutput)

    def bmconvAdapter(self, command):
        bmconvLocalPath = os.path.join(os.path.dirname(__file__), 'bmconv')
        process = Popen(f'{bmconvLocalPath} {command}', stdout=PIPE, stderr=PIPE, shell=True)
        output, error = process.communicate()
        if error:
            messagebox.showerror("ERROR",
                                 f"Not Found BMCONV Executable File! Please Download S60 SDK and Copy bmconv to {bmconvLocalPath}")
        return output.decode("utf-8")

    def getBinaryVersion(self):
        output = self.bmconvAdapter('')
        match = re.search(r'version (\d+)', output)
        return match.group(1) if match else 'Unknown'

    def clearTempDir(self):
        for file in os.listdir(self.tempDir):
            os.remove(os.path.join(self.tempDir, file))
            os.rmdir(self.tempDir)

    def Starter(self):
        version = self.getBinaryVersion()
        self.root.title(f"MultiBitMap Extractor - BMCONV Version {version}")
        self.root.mainloop()

if __name__ == "__main__":
    root = tk.Tk()
    exec = mbmExtractor(root)
    exec.Starter()
    exec.clearTempDir()

整合包

整合包是一个彩蛋
可在OBJect找到
其中的可执行文件来自于Symbian^3 SDK for Belle
感谢Symbian Foundation(虽然其已在法理上消失)

知识共享许可协议
本文及其附件均采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

添加新评论