diff --git a/1.py b/1.py new file mode 100644 index 0000000..241523b --- /dev/null +++ b/1.py @@ -0,0 +1,284 @@ +import json +import struct +import sys +from pathlib import Path + +# Base64 字母表(CC:Tweaked 使用的自定义顺序) +B64STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +B64MAP = {ch: i for i, ch in enumerate(B64STR)} + + +def custom_b64decode(s): + """使用自定义字母表解码 base64 字符串(忽略 '=')""" + val = 0 + bits = 0 + out = bytearray() + for ch in s: + if ch == '=': + continue + val = (val << 6) | B64MAP[ch] + bits += 6 + if bits >= 8: + bits -= 8 + out.append((val >> bits) & 0xFF) + return bytes(out) + + +def decode_frame(data): + # 验证头部 + assert data[:4] == b'\x00\x00\x00\x00', "Invalid frame header (first 4 bytes)" + assert data[8:16] == b'\x00\x00\x00\x00\x00\x00\x00\x00', "Invalid frame header (bytes 8-15)" + + width, height = struct.unpack(' 0: + char_byte = data[pos] + count = data[pos + 1] + pos += 2 + text_bytes.extend([char_byte] * count) + remaining -= count + assert len(text_bytes) == total_chars, "Character RLE length mismatch" + + # === 解码颜色 RLE 流 === + color_bytes = [] + remaining = total_chars + while remaining > 0: + color_byte = data[pos] + count = data[pos + 1] + pos += 2 + color_bytes.extend([color_byte] * count) + remaining -= count + assert len(color_bytes) == total_chars, "Color RLE length mismatch" + + # === 读取调色板(16 色 × RGB)=== + palette_raw = data[pos:pos + 48] + assert len(palette_raw) == 48, "Palette must be 48 bytes" + palette = [] + for i in range(16): + r = palette_raw[i * 3] + g = palette_raw[i * 3 + 1] + b = palette_raw[i * 3 + 2] + palette.append([r, g, b]) + + # === 构建行数据 === + text_rows = [] + fg_rows = [] + bg_rows = [] + idx = 0 + for y in range(height): + row_text_parts = [] + row_fg = '' + row_bg = '' + for x in range(width): + b = text_bytes[idx] + row_text_parts.append(f"\\{b:03d}") # 如 \130 + + c = color_bytes[idx] + fg = c & 0x0F + bg = (c >> 4) & 0x0F + row_fg += f"{fg:x}" + row_bg += f"{bg:x}" + idx += 1 + text_rows.append(''.join(row_text_parts)) + fg_rows.append(row_fg) + bg_rows.append(row_bg) + + return { + "width": width, + "height": height, + "text": text_rows, + "foreground": fg_rows, + "background": bg_rows, + "palette": palette + } + + +def encode_frame(frame): + width = frame["width"] + height = frame["height"] + total = width * height + + # === 从 \ddd 序列还原字节 === + full_text = ''.join(frame["text"]) + text_bytes = [] + i = 0 + while i < len(full_text): + if full_text[i] == '\\' and i + 4 <= len(full_text): + try: + num_str = full_text[i+1:i+4] + num = int(num_str) + if 0 <= num <= 255: + text_bytes.append(num) + i += 4 + continue + except ValueError: + pass + raise ValueError(f"Invalid escape sequence at position {i}: expected \\ddd, got {full_text[i:i+5]}") + assert len(text_bytes) == total, "Text byte count mismatch" + + # === 编码字符 RLE === + char_rle = [] + i = 0 + while i < total: + b = text_bytes[i] + j = i + while j < total and text_bytes[j] == b and (j - i + 1) <= 255: + j += 1 + count = j - i + char_rle.extend([b, count]) + i = j + + # === 构建颜色流 === + color_stream = [] + for y in range(height): + fg_row = frame["foreground"][y] + bg_row = frame["background"][y] + assert len(fg_row) == len(bg_row) == width, f"Row {y} length mismatch" + for x in range(width): + fg = int(fg_row[x], 16) & 0x0F + bg = int(bg_row[x], 16) & 0x0F + color_stream.append((bg << 4) | fg) + assert len(color_stream) == total + + # === 编码颜色 RLE === + color_rle = [] + i = 0 + while i < total: + c = color_stream[i] + j = i + while j < total and color_stream[j] == c and (j - i + 1) <= 255: + j += 1 + count = j - i + color_rle.extend([c, count]) + i = j + + # === 调色板 === + palette_bytes = [] + for rgb in frame["palette"]: + assert len(rgb) == 3 + r, g, b = rgb + assert 0 <= r <= 255 and 0 <= g <= 255 and 0 <= b <= 255 + palette_bytes.extend([r, g, b]) + assert len(palette_bytes) == 48 + + # === 拼接二进制数据 === + header = b'\x00\x00\x00\x00' + struct.pack('= 6: + bits -= 6 + b64 += B64STR[(val >> bits) & 0x3F] + if bits: + b64 += B64STR[(val << (6 - bits)) & 0x3F] + # 补齐到 4 的倍数(虽然 CC 可能不检查,但保持兼容) + while len(b64) % 4: + b64 += '=' + + # 使用 !CPD 格式(12 位十六进制长度) + hex_len = f"{len(b64):012x}" + return f"!CPD{hex_len}{b64}" + + +def vid_to_json(vid_path, json_path): + with open(vid_path, 'r', encoding='latin1') as f: + lines = [line.rstrip('\n\r') for line in f] + + if not lines or lines[0] != "32Vid 1.1": + raise ValueError("Not a valid .32vid file: missing header") + + fps_str = lines[1] if len(lines) > 1 else "0" + try: + fps = float(fps_str) + except ValueError: + fps = 0.0 + + frame_lines = [] + for line in lines[2:]: + if line.strip() == "": + continue + if line.startswith("!CP"): + frame_lines.append(line) + + frames = [] + for line in frame_lines: + mode = line[3] + if mode == 'C': + length = int(line[4:8], 16) + b64data = line[8:8 + length] + elif mode == 'D': + length = int(line[4:16], 16) + b64data = line[16:16 + length] + else: + raise ValueError(f"Unknown frame mode: {mode}") + + binary_data = custom_b64decode(b64data) + frame = decode_frame(binary_data) + frames.append(frame) + + result = { + "header": { + "magic": "32Vid 1.1", + "fps": fps + }, + "frames": frames + } + + with open(json_path, 'w', encoding='utf-8') as f: + json.dump(result, f, indent=2, ensure_ascii=False) + + +def json_to_vid(json_path, vid_path): + with open(json_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + fps = data["header"]["fps"] + frames = data["frames"] + + with open(vid_path, 'w', encoding='latin1') as f: + f.write("32Vid 1.1\n") + f.write(f"{fps}\n") + for frame in frames: + line = encode_frame(frame) + f.write(line + "\n") + + +# ====================== +# CLI 入口 +# ====================== +def main(): + if len(sys.argv) != 4: + print("Usage:") + print(" python 32vid_converter.py to-json input.32vid output.json") + print(" python 32vid_converter.py to-vid input.json output.32vid") + sys.exit(1) + + command = sys.argv[1] + input_path = sys.argv[2] + output_path = sys.argv[3] + + if command == "to-json": + vid_to_json(input_path, output_path) + print(f"✅ Converted: {input_path} → {output_path}") + elif command == "to-vid": + json_to_vid(input_path, output_path) + print(f"✅ Converted: {input_path} → {output_path}") + else: + print(f"❌ Unknown command: {command}") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file