Temple OS源码研究

博客:源码解读

TOS源码: https://github.com/cia-foundation/TempleOS
启动过程: https://minexew.github.io/2020/02/27/templeos-loader-part1.html
建设性看法:http://www.codersnotes.com/notes/a-constructive-look-at-templeos/
移植文字游戏:https://www.jwhitham.org/2015/07/porting-third-party-programs-to-templeos.html

常用命令

TaskRep;  //任务管理器
Kill(0x00100300);  //杀死进程
Unzip("test.HC.Z");
Type("StadiumBG.GR.Z");  //打印DolDoc(文档 或 精灵Spirit)
Reboot;

#include "test.HC" //立即执行

Ctrl+F1 快速输入/召唤自动补全
Ctrl+Alt+X 类似linux下的Ctrl+C,中断当前任务
Ctrl+R 创建/编辑spirit
Ctrl+T 显示doldoc为raw text

图片嵌入

详见~/Demo/Games/Stadium/: https://github.com/cia-foundation/TempleOS/tree/archive/Demo/Games/Stadium

找demo

注释,说是.DD生成了.GR文件。我们可以写一个python,来将任意图片,转换成.DD文件。

DolDoc

打开StadiumBG.DD,文件头部有:

$SP,"<01>",BI=1$

$:类似Minecraft的§,表示非普通文本,不过要头尾都有。
SP: Spirit精灵
"<01>": 占位符,留空则不显示。类似markdown的![占位符文字](图片URL)
BI: 类似唯一id。每创建一个新spirit时+1。

.GR

后面跟着二进制数据,结合GR文件结构: 一个字节,但最多16种颜色。0x00~0x0F,透明为0xFF

首先Ctrl+R,得到这是一个BitMap,长640,高472
TOS Spirit

(472)₁₀=(1D8)₁₆,发现是little endian方式存储(x86)。有点反人类,但对机器友好。
Hex

得到结构:

"$SP,"",BI=1$";  //DD文件,前面全文字,后面全bin

//BI头:
0x00;
0x0100 0000 0000 0000;  //BI值:0x0200 0000 0000 0000
0x129c 0400;  //BI内所有元素的字节长(方便访问下一个BI)
0x0100 0000;  //固定,但可能是某个会递增的id?

//同一BI内,元素头:
0x17;  //固定,代表该元素为bitmap
I32 offset_width;
I32 offset_height;
U32 width;    //建议为8的倍数
U32 height;
U8 body[640*472]={
  0x0F,0x0F,...,0x0F, //每行640个0x0F,默认为白色0x0F
  // 0xFF...    //每行尾随补充一定数量的0xFF,使得一行字节数能**凑齐** 8的倍数。如一行640,则无须补充0xFF;若一行14,则还需补充2个0xFF,凑到16个字节每行
}[472];		//共有472行
0x00;		//每个**元素**的EOF,body最后一行最后一字节置为0x00,后延续一次每行的0xFF数量。

0x00;		//文件末尾的EOF。若为最后一个body,则body的最后一行的最后一字节不必为0xFF。如0x0f 0x0f (0xFF...) 0x00 EOF

py

#!/bin/env python
import struct
import cv2
import numpy as np
import taichi as ti
from typing import Literal
from PIL import Image
import os
import platform
OS = platform.system()

DEBUG=True
WIDTH=640
HEIGHT=480
MAX=255
BAYER_M=(4,1)   # 4x4 bayer matrix, scale by 1
DD_SAVE='/home/n/document/code/templeos-plus/o.DD'
TMP_PNG='tmp_dither_{}.PNG'
TMP_DIR={
    'Linux':'/tmp/',
    'Windows':'C:/Users/lenovo/AppData/Local/Temp/',
}
dither_algorithm:Literal['bayer','floyd']='floyd'
interpolation=cv2.INTER_LANCZOS4
"""
cv2.INTER_NEAREST :最近邻插值。最快,但通常效果最差
cv2.INTER_LINEAR :双线性插值。默认,适用于大多数情况,平衡了速度和图像质量。
cv2.INTER_CUBIC :三次样条插值。计算复杂度较高,但图像质量较好,适用于对图像质量要求较高的场景。
cv2.INTER_AREA :区域插值。适用于缩小图像,可以减少锯齿效应。
cv2.INTER_LANCZOS4 :Lanczos 插值。使用 8x8 邻域进行插值,计算复杂度最高,但图像质量最好,适用于对图像质量要求极高的场景。
"""
BIS = [
    {
        'text': '<1>',
        'elem': [
            {
                'img': './input.jpg',
                'w': 640,
            },
            {
                'img': './transparent.png',
                'h': 640,
                'offset_w': -320,
            },
        ]
    },
    {
        'text': '<2>',
        'elem': [
            {
                'img': './input.jpg',
                'w': 640,	# 同时指定h,w时,强制拉伸
                'h': 640,
            },
            {
                'img': './transparent.png',
                'offset_h': -320,
            },
        ]
    },

]
cmd = f'\\rm {TMP_DIR[OS]}{TMP_PNG.format('*')}'
# cmd = f'\\rm {TMP_DIR[OS]}tmp*.PNG'
print('cmd',cmd)
os.system(cmd) if OS == 'Linux' else None
# Dont't modify below 不要修改以下内容
TEXT_SP='$SP,"{}",BI={}$'
HEAD_BI='''
00
{} 0000 0000
{}
0100 0000
'''

TRANSPARENT = 0xFF
PALETTE = [
0x000000, 0x0000AA, 0x00AA00, 0x00AAAA,
0xAA0000, 0xAA00AA, 0xAA5500, 0xAAAAAA,
0x555555, 0x5555FF, 0x55FF55, 0x55FFFF,
0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF
]
PALETTE = [(c & 0xFF, c >> 8 & 0xFF, c>>16 & 0xFF) for c in PALETTE] #BGR for opencv default color space
# print('🎨 palette',palette) if DEBUG else None
CHANNEL = len(PALETTE[0])
PALETTE_SET = [ sorted(list(set(c[ch] for c in PALETTE))) for ch in range(CHANNEL)]
PALETTE_SET = [ [0] if ch[0]!=0 else [] + ch + [0xFFFFFF] for ch in PALETTE_SET]
print('PALETTE_SET',PALETTE_SET) if DEBUG else None

def show_image(img,lib:Literal['cv2','pil','sys']='pil'):
    """
    - cv2: 无法显示alpha透明通道,但不会生成临时文件
    - pil: 可以显示alpha通道。在Linux下由于调用xdg-open,会生成临时文件
    """
    if lib == 'sys':
        if OS == 'Linux':
            os.environ["QT_QPA_PLATFORM"] = "dxcb"
            os.system(f'xdg-open "{img}"')
        elif OS == 'Windows':
            os.system(f'start "{img}"')
        return
    if isinstance(img, str):
        img = cv2.imread(img)
    elif img.dtype != 'uint8':
        img = np.clip(img, 0, MAX)  # 将图片像素值限制在 0~255 之间
        img = img.astype(np.uint8)

    if lib == 'cv2':
        os.environ.pop("QT_QPA_PLATFORM", None) if OS == 'Linux' else None
        cv2.imshow('Image', img)
        while True:
            key = cv2.waitKey(1) & 0xFF
            if key == 27 or key == 32:      # ESC或空格
                break
        cv2.destroyAllWindows()
    elif lib == 'pil':
        os.environ["QT_QPA_PLATFORM"] = "dxcb" if OS == 'Linux' else None   # 根据不同操作系统,灵活调整
        img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)
        pil_img = Image.fromarray(img)
        pil_img.show()

def get_fit_hw(img,hw) -> tuple[int,int]:
    h,w = hw
    old_h, old_w = img.shape[:2]
    if h and w:     # 强制拉伸,局部dic['h'] > 全局HEIGHT
        return h, w
    elif w:
        new_w = w
        new_h = int(new_w * old_h / old_w)
    elif h:
        new_h = h
        new_w = int(new_h * old_w / old_h)
    elif WIDTH>0 and HEIGHT>0:
        return WIDTH,HEIGHT
    elif WIDTH>0:
        new_w = WIDTH
        new_h = int(new_w * old_h / old_w)
    elif HEIGHT>0:
        new_h = HEIGHT
        new_w = int(new_h * old_w / old_h)
    return new_h, new_w

def hsv(img, h=0, s=1, v=1):
    """调整图像的色调、饱和度、亮度"""
    # channel = img.shape[2]
    # if channel == 4:
    #     alpha = img[:, :, 3]
    #     img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    img[:, :, 0] = np.clip((img[:, :, 0] + h)%MAX, 0, 255)
    img[:, :, 1] = np.clip(img[:, :, 1] * s, 0, 255)
    img[:, :, 2] = np.clip(img[:, :, 2] * v, 0, 255)
    img = cv2.cvtColor(img, cv2.COLOR_HSV2BGR)
    # if channel == 4:
    #     img = np.dstack((img, alpha))
    return img

def bayer_matrix(n: int = 4):
    """
    生成任意大小的bayer矩阵
    但其实,n=2,3,4,8
    为什么8是最大值?因为256=2^8,所以8x8的bayer矩阵可以覆盖256色
    """
    M2 = np.array([[0, 2],
                   [3, 1]], dtype=np.uint8)
    if n == 3:
        return [[0, 7, 3],
                [6, 5, 2],
                [4, 1, 8]]
    elif np.log2(n) % 1 != 0:  # n不是2的幂次方,%1取小数部分
        raise ValueError('n must be a power of 2')
    elif n == 2:
        return M2
    else:
        half = n // 2
        M = np.zeros((n, n), dtype=np.uint8)
        M[:half, :half] = bayer_matrix(half) * 4
        M[:half, half:] = bayer_matrix(half) * 4 + 2 * np.eye(half)
        M[half:, :half] = bayer_matrix(half) * 4 + 3 * np.eye(half)
        M[half:, half:] = bayer_matrix(half) * 4 + np.eye(half)
        return M

def gen_bayerM(n=4,scale=1):
    bayerM = ti.ndarray(shape=(n*scale, n*scale), dtype=ti.u8)
    bayerRaw = np.matrix(bayer_matrix(n), dtype=np.uint8)
    bayerRaw = np.kron(bayerRaw, np.ones((scale,scale), dtype=np.uint8))
    print(bayerRaw,'← bayerM') if DEBUG else None
    bayerM.from_numpy(bayerRaw)
    return bayerM

type_2d_vec = ti.types.ndarray(element_dim=1,ndim=2)
type_2d = ti.types.ndarray(element_dim=0,ndim=2)
# type_2d_vec = object
# type_2d = object

@ti.func
def get_closest_bgr(old_pixel):
    min_distance = 0xFFFFFFF
    new_pixel = ti.Vector([0,0,0],dt=ti.i32)
    index = 0
    i = 0

    r, g, b = old_pixel
    for c in ti.static(PALETTE):
        cr, cg, cb = c
        distance = ti.int32((r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2)
        if distance < min_distance:
            min_distance = distance
            new_pixel = c
            index = i
        i+=1
    return [new_pixel, index]

@ti.kernel
def dither_bayer(img:type_2d_vec, bayerM:type_2d,hex:ti.template(),alpha:type_2d):
    h = img.shape[0]
    w = img.shape[1]
    n = bayerM.shape[0]
    has_alpha = alpha.shape[0] > 1
    # print('has_alpha',has_alpha) if DEBUG else None
    for i in range(h):
      for j in range(w):
        th = bayerM[i % n, j % n] / (n**2)
        for ch in ti.static(range(CHANNEL)):
            for b in ti.static(range(len(PALETTE_SET[ch])-1)):
                if PALETTE_SET[ch][b] < img[i, j][ch]/th < PALETTE_SET[ch][b+1]:
                    img[i, j][ch] = PALETTE_SET[ch][b]
        img[i, j],index = get_closest_bgr(img[i,j])
        
        if has_alpha:
            if alpha[i, j] > th * MAX:
                alpha[i, j] = MAX         # 透明度大于阈值,设为不透明
                hex[i, j] = index
            else:
                alpha[i, j] = 0
                hex[i, j] = TRANSPARENT
        else:
            hex[i, j] = index

    # fix taichi
    h = img.shape[0]
    w = img.shape[1]
    for i in range(h):
        if i%512==0:    # taichi cpu fix
            for j in range(w):
                img[i, j],index = get_closest_bgr(img[i, j])

@ti.kernel
def dither_floyd(img:type_2d_vec,hex:ti.template(),alpha:type_2d):
    h = img.shape[0]
    w = img.shape[1]
    has_alpha = alpha.shape[0] > 1
    # print('has_alpha',has_alpha) if DEBUG else None
    for i in range(h):
        for j in range(w):
            oldpixel = img[i, j]
            oldpixel = ti.math.clamp(img[i, j],0,MAX)
            newpixel,index = get_closest_bgr(oldpixel)
            quant_error = oldpixel - newpixel
            img[i, j] = newpixel

            if j+1<w:
                img[i, j + 1] += quant_error * 7 // 16
            if i+1<h:
                if j-1>=0:
                    img[i + 1, j - 1] += quant_error * 3 // 16
                img[i + 1, j] += quant_error * 5 // 16
                if j+1<w:
                    img[i + 1, j + 1] += quant_error // 16
            
            if has_alpha:
                old_a = alpha[i, j]
                if alpha[i, j] * 2 > MAX:
                    alpha[i, j] = MAX
                    hex[i, j] = index
                else:
                    alpha[i, j] = 0
                    hex[i, j] = TRANSPARENT

                alpha_error = old_a - alpha[i, j]
                if j+1<w:
                    alpha[i, j + 1] += alpha_error * 7 // 16
                if i+1<h:
                    if j-1>=0:
                        alpha[i + 1, j - 1] += alpha_error * 3 // 16
                    alpha[i + 1, j] += alpha_error * 5 // 16
                    if j+1<w:
                        alpha[i + 1, j + 1] += alpha_error // 16
            else:
                hex[i, j] = index

    # fix taichi
    h = img.shape[0]
    w = img.shape[1]
    for i in range(h):
        if i%512==0:    # taichi cpu fix
            for j in range(w):
                img[i, j],index = get_closest_bgr(img[i, j])
                hex[i, j] = index

def dither_main(img,is_last_one:False):
    h,w = img.shape[:2]
    hex = bytearray()
    FF_count = 8 - w%8 if w%8 else 0
    FF = b'\xFF' * FF_count
    hex2d = ti.field(dtype=ti.u8,shape=(h,w))
    alpha=np.zeros((1,1),dtype=np.int32)
    if img.shape[2] == 4:           # 有alpha通道
        alpha = img[:,:,3]
        alpha = alpha.astype(np.int32)
        img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)

    if dither_algorithm == 'bayer':
        img = hsv(img, s=1, v=.3)   # 💡 这里默认调低了亮度,可以根据实际情况调整
    img = img.astype(np.int32)
    
    alpha = np.ascontiguousarray(alpha,dtype=np.int32)

    if dither_algorithm == 'floyd':
        dither_floyd(img,hex2d,alpha)
    elif dither_algorithm == 'bayer':
        dither_bayer(img, gen_bayerM(*BAYER_M), hex2d, alpha)
    hex2d = hex2d.to_numpy()

    # 将img与alpha合并
    if alpha.any():
        img = np.dstack((img, alpha))
    img = img.astype(np.uint8)

    for y in range(h):
        for x in range(w):
            hex.extend(struct.pack('<B', hex2d[y, x]))
        hex.extend(FF)

    if not is_last_one:
        hex[-FF_count-1] = 0x00
    
    return img,bytes(hex)

def get_bitmap(elem,is_last_one=False) -> bytes:
    BITMAP = struct.pack('<B',0x17)
    bi = b''
    for dic in elem:
        img_path = dic['img']
        old_hw = [dic.get('h', None),dic.get('w', None)]
        offset_wh = (dic.get('offset_w', 0),dic.get('offset_h', 0))
        wh = struct.pack('<i',offset_wh[0])
        wh += struct.pack('<i',offset_wh[1])
        
        img = cv2.imread(img_path,cv2.IMREAD_UNCHANGED)
        if img is None:
            raise Exception('Image not found: ' + img_path)
        print('raw_size',img.shape) if DEBUG else None
        new_h,new_w = get_fit_hw(img,old_hw)
        img = cv2.resize(img, (new_w,new_h), interpolation=interpolation)
        wh += struct.pack('<I',new_w)
        wh += struct.pack('<I',new_h)
        print('fitted_size',img.shape) if DEBUG else None
        # print('hex_wh',struct.pack('<H',new_w),struct.pack('<H',new_h))    

        img,body = dither_main(img,is_last_one)
        if DEBUG:
            if OS == 'Windows':
                show_image(img,'cv2')
            else:
                save_path = TMP_DIR[OS] + TMP_PNG.format(os.path.basename(img_path))
                print('save_path:',save_path)
                cv2.imwrite(save_path,img)
        print('bitmap head:',(BITMAP + wh).hex(' ',2)) if DEBUG else None
        bi += BITMAP + wh + body
    return bi

def get_BIs(bis) -> bytes:
    hex = b''
    for i in range(len(bis)):
        hex+=TEXT_SP.format(bis[i]['text'],i+1).encode()

    for i in range(len(bis)):
        id = i+1
        bmap=get_bitmap(bis[i]['elem'], id == len(bis))
        hex_len=struct.pack('<I',len(bmap)+1).hex(' ',2)  # +1
        id_bi = struct.pack('<I',id).hex(' ',2)
        head_bi=bytes.fromhex(HEAD_BI.format(id_bi,hex_len).strip())  # BI_ID's max is unsigned short = 2^16
        print('BI head:',HEAD_BI.format(id_bi,hex_len))  if DEBUG else None
        hex+=head_bi
        hex+=bmap
    return hex + b'\x00'

ti.init(arch=ti.cpu,debug=False)
hex=get_BIs(BIS)
with open(DD_SAVE, 'wb') as f:
    f.write(hex)

if OS != 'Windows':
    pngs = os.popen(f'\\ls {TMP_DIR[OS]}{TMP_PNG.format('*')}').read().split('\n')
    show_image(pngs[0],'sys')
posted @ 2024-11-13 16:03  Nolca  阅读(7)  评论(0编辑  收藏  举报