This library provides an in-memory interface for reading/writing subtitle-related files from the game Killer7.
Supported file formats:
- Jimaku Binary File (JMB)
- String Image (STRIMAGE)
- Texture Files (BIN)
Developed for the k7cn project.
A JMB file consists of the following parts:
- Metadata
- Sentences data
- Font parameters (defining
u;v;w;hcoordinates for each character in the atlas texture) - Atlas texture
- Motion data (Japanese version only)
To read a JMB file, specify the input path and version (JP or US), then call BaseGdat.create():
import os
from jmbTool.jmbConst import JmkKind
from jmbTool.jmbData import BaseGdat
input_path = "some_zan_or_charageki_file.jmb"
jmb_name = os.path.basename(input_path)[:-4]
# Detect version based on filename
if 'J' in jmb_name or ('Movie' in input_path and 'E' not in jmb_name):
kind = JmkKind.JA
else:
kind = JmkKind.US
jmb = BaseGdat.create(input_path, kind)Explore the JMB file structure:
from pprint import pprint
from jmbTool.typeUtils import _TYPE_is_US, _TYPE_is_JA
print("\n==== MetaData ====")
print(jmb.meta)
print("\n==== Sentence Info ====")
for i in range(jmb.meta.sentence_num):
if _TYPE_is_JA(jmb):
print(f"st {i}")
for jmk_idx, jmk in enumerate(jmb.sentences[i].jimaku_list):
print(f"\t [{jmk_idx}] char_data {jmk.char_data}")
elif _TYPE_is_US(jmb):
print(f"st {i} char_data {jmb.sentences[i].char_data}")
print("\n==== Font Params ====")
for idx, fParam in enumerate(jmb.fParams):
print(f"[{idx}] {fParam}")
print("\n==== Export DDS Texture ====")
jmb.tex.dump("test.dds")Extract characters from the texture atlas using the font parameters:
from PIL import Image
import io
SCALE_FACTOR = 4 # most jmb textures have been upscaled, but the font params have not been adjusted
output_dir = "atlas_chars"
os.makedirs(output_dir, exist_ok=True)
char_cnt = 0
with Image.open(io.BytesIO(jmb.tex.dds)) as img:
width, height = img.size
print(f"Atlas texture dimensions: {width}x{height}")
for idx, char in enumerate(jmb.fParams):
u_phys = char.u * SCALE_FACTOR
v_phys = char.v * SCALE_FACTOR
w_phys = char.w * SCALE_FACTOR
h_phys = char.h * SCALE_FACTOR
# Validate coordinates
if (u_phys + w_phys > width or v_phys + h_phys > height):
print(f"Warning: Character {idx} has out-of-bounds coordinates: {char}")
continue
print(f"Character {idx} coordinates: u={u_phys}, v={v_phys}, w={w_phys}, h={h_phys}")
char_img = img.crop((u_phys, v_phys, u_phys+w_phys, v_phys+h_phys))
char_cnt += 1
char_img.save(f'{output_dir}/char_{idx:02d}.png')Once you have extracted the individual characters, you can generate preview images for each sentence based on control codes in the sentence:
from jmbTool.jmbStruct import stFontParam, stJimaku # stJimaku = stJimaku_US | stJimaku_JA
from jmbTool import jmbUtils
from jmbTool.jmbNumeric import S16_BE
extracted_chars_dir = "atlas_chars"
preview_dir = "preview"
SCALE_FACTOR = 4
def save_preview(target_path: str, jmk: stJimaku, fParams: list[stFontParam], extracted_chars_dir: str):
SATSU_FLAG = S16_BE("8000")
SHI_FLAG = S16_BE("7000")
SPACE_H_FLAG = S16_BE("fffd")
SPACE_Z_FLAG = S16_BE("fffc")
os.makedirs(os.path.dirname(target_path), exist_ok=True)
char_data = jmbUtils.display_char_data(jmk.char_data)
FONT_HEIGHT = max(param_i.h for param_i in fParams)
if len(char_data) == 0:
img = Image.new('RGB', (35, FONT_HEIGHT*SCALE_FACTOR), (0,0,0))
img.format='png'
img.save(target_path)
img.close()
return
canvas = Image.new('RGB', (70*SCALE_FACTOR*len(char_data), FONT_HEIGHT*SCALE_FACTOR), (0,0,0))
current_x = 0
for ctl in char_data:
ctl_s16 = S16_BE(ctl)
if (ctl_s16 == SPACE_H_FLAG or # Half-Width Space
ctl_s16 == SPACE_Z_FLAG or # Full-Width Space
(ctl_s16 & S16_BE("ff00")) == S16_BE("ff00")): # Controller Related Buttons
current_x += 21*SCALE_FACTOR
continue
if (ctl_s16 & SHI_FLAG) != S16_BE("0000") or \
(ctl_s16 & SATSU_FLAG) != S16_BE("0000"):
mask = S16_BE("0fff")
else:
mask = S16_BE("ffff")
index = (mask & ctl_s16).to_int()
with Image.open(f"{extracted_chars_dir}/char_{index:02d}.png") as char_img:
step = (char_img.width // SCALE_FACTOR) + 1
canvas.paste(char_img, (current_x, 0))
current_x += step * SCALE_FACTOR
canvas = canvas.crop((0, 0, current_x + 16, FONT_HEIGHT * SCALE_FACTOR))
canvas.format='png'
canvas.save(target_path)
canvas.close()
for i in range(jmb.meta.sentence_num):
print(f"generating preview for sentence {i}")
if _TYPE_is_JA(jmb):
sent = jmb.sentences[i]
for jmk_idx, jmk in enumerate(sent.jimaku_list):
if not jmk.valid():
break
target_path = f"{preview_dir}/JA_sent{i}/{jmk_idx:02d}"
save_preview(target_path+".png", jmk, jmb.fParams, extracted_chars_dir)
elif _TYPE_is_US(jmb):
sent = jmb.sentences[i]
if not sent.valid():
break
target_path = f"{preview_dir}/US_sent{i}"
save_preview(target_path+".png", sent, jmb.fParams, extracted_chars_dir)If you've made changes to the JMB file, for example, delaying every subtitle by 1 second, you can save the updated JMB file to test the results:
if _TYPE_is_JA(jmb):
for oneSentence in jmb.sentences:
for jmk in oneSentence.jimaku_list:
jmk.wait += 4800 # +1 second; wait/disp_time is stored as s32 integer,
# representing rounded value of (time_in_seconds * 4800)
elif _TYPE_is_US(jmb):
for jmk in jmb.sentences:
jmk.wait += 4800
jmb.write_to_file("new.jmb")This library provides a basic, non-flexible atlas generation method. If you are working on translation, you may need to implement a more flexible solution to handle various font types. However, the built-in generator allows for a quick test.
For a more complex example, please refer to the k7cn project for an example of CJK atlas generation.
Assuming you have translated subtitles with new control codes and a generated texture:
text:
This is a test
control codes:
0 -> T
1 -> h
2 -> i
3 -> s
4 -> a
5 -> t
6 -> e
valid sentence data:
0 1 2 3 -4 2 3 -4 4 5 6 3 5
(-4 = full-width space; -3 = half-width space)
font params:
prepare your own
The following code shows how to update the JMB file and save the translated version:
text = "This is a test"
codes = [0, 1, 2, 3, -4, 2, 3, -4, 4, 5, 6, 3, 5]
atlas_path = "new.dds"
if _TYPE_is_JA(jmb):
# Update font parameters
jmb.fParams = ... # Replace with your updated font parameters
# Update sentence data
oneSentence_0 = jmb.sentences[0]
jmk_0 = oneSentence_0.jimaku_list[0]
jmk_0.overwrite_ctl(codes)
# Update atlas texture
jmb.reimport_tex(atlas_path)
elif _TYPE_is_US(jmb):
# Update font parameters
jmb.fParams = ... # Replace with your updated font parameters
# Update sentence data
jmk_0 = jmb.sentences[0]
jmk_0.overwrite_ctl(codes)
# Update atlas texture
jmb.reimport_tex(atlas_path)
jmb.write_to_file("new.jmb") # Save translated versionBelow is a quick demo of generating an atlas from translated text using the integrated generator and updating the JMB file:
from jmbTool import atlasGeneration, jmbData, jmbConst
INPUT_PATH = "killer7\\ReadOnly\\CharaGeki\\00010101\\00010101\\00010101.jmb"
CHAR_HEIGHT = 24
FONT_SIZE = 68
FONT_PATH = "SourceHanSerifCN-Bold.otf"
SCALE_FACTOR = 4
jmb = jmbData.BaseGdat.create(INPUT_PATH, jmbConst.JmkKind.US)
text = [
"Это я, ты уже на месте?",
"Ты имеешь в виду эту дыру?",
"Там они все тусуются.",
"Наша информация говорит, что их там 14.",
"И всех надо охотиться?",
"Не, оставь одного в живых,",
"чтобы мы могли спросить,",
"кто их босс.",
"Что ещё мне нужно знать?",
"Да нет, в общем-то, ты поймёшь, когда их увидишь,",
"они, э-э... другие.",
"Будет сделано.",
"Пусть Господь улыбнётся...",
"...а Дьявол смилуется.",
]
# text = [
# "Aquí estoy. ¿Ya llegaste?",
# "¿Te refieres a este maldito agujero?",
# "Ahí es donde todos se reúnen.",
# "Nuestra información indica que son catorce.",
# "¿Y todos están listos para cazar?",
# "No, deja uno con vida",
# "para preguntarle",
# "quién es su jefe.",
# "¿Algo más que deba saber?",
# "Na, en realidad no. Los reconocerás al verlos,",
# "son, eh, diferentes.",
# "De acuerdo.",
# "Que el Señor sonría...",
# "...y el Diablo tenga piedad."
# ]
text_flatten = "".join(text)
ctl2char_lookup, char2ctl_lookup, unique_chars = atlasGeneration.char_register(text_flatten)
canvas, fontParams = atlasGeneration.gen_atlas(
FONT_PATH, unique_chars, CHAR_HEIGHT, FONT_SIZE, SCALE_FACTOR,
debug=False, compact=False
) # debug: show border for each generated character
canvas.save('atlas.png')
command = [
"texconv.exe",
"-f", "BC7_UNORM",
"-ft", "dds",
"-m", "1",
"-y",
"atlas.png",
]
import subprocess
subprocess.run(command, check=True)
jmb.fParams = fontParams
assert len(text) == jmb.meta.sentence_num
if _TYPE_is_JA(jmb):
idx = 0
for sent in jmb.sentences:
for jmk in sent.jimaku_list:
if not jmk.valid():
break
text_sentence = text[idx]
text_codes = [char2ctl_lookup[c] for c in text_sentence]
jmk.overwrite_ctl(text_codes)
idx += 1
elif _TYPE_is_US(jmb):
for idx, jmk in enumerate(jmb.sentences):
text_sentence = text[idx]
text_codes = [char2ctl_lookup[c] for c in text_sentence]
jmk.overwrite_ctl(text_codes)
jmb.reimport_tex("atlas.dds")
jmb.write_to_file("00010101.jmb")Note: Some STRIMAGE files also use the .BIN extension, but they can be distinguished by checking the file's magic numbers. This section specifically covers .BIN files used for storing textures.
from PIL import Image
import io
from jmbTool.jmbStruct import stTex
filename = "file.BIN"
with open(filename, 'rb') as fp:
tex = stTex(fp)
print("tex header =", tex.header)
# Export raw DDS texture
tex.dump("dump.dds")
# Convert and export as PNG
with Image.open(io.BytesIO(tex.dds)) as img:
img.format = 'png'
img.save("dump.png")from PIL import Image
import io
from jmbTool.jmbStruct import stTex
SCALE_FACTOR = 4 # Adjust based on texture scaling (use 1 for non-upscaled textures)
updated_dds_path = "updated.dds"
original_bin_path = "file.BIN"
# Read original BIN file
with open(original_bin_path, 'rb') as fp:
tex = stTex(fp)
# Update texture data
with open(updated_dds_path, 'rb') as fp_dds:
tex.dds = fp_dds.read()
with Image.open(io.BytesIO(tex.dds)) as img:
img_width, img_height = img.size
tex.header.w = img_width // SCALE_FACTOR
tex.header.h = img_height // SCALE_FACTOR
tex.header.dds_size = len(tex.dds)
# Write modified BIN file
with open("new.BIN", 'wb') as bfp:
tex.write(bfp)TODO: examples
Most subtitle textures use BC7 compression following recent updates. However, the ImageMagick library currently only supports BC5 compression. For conversion, it is recommended to use texconv.