Skip to content

Instantly share code, notes, and snippets.

@kuluna
Created April 4, 2026 08:08
Show Gist options
  • Select an option

  • Save kuluna/ca553b21e32799ff08e6dd6babd3abee to your computer and use it in GitHub Desktop.

Select an option

Save kuluna/ca553b21e32799ff08e6dd6babd3abee to your computer and use it in GitHub Desktop.
クリスタファイルのレイヤー名を正規表現で一括リネームする
#!/usr/bin/env python3
"""CLIP STUDIO PAINT (.clip) ファイルのレイヤー名を正規表現で一括リネームするツール"""
import argparse
import os
import re
import sqlite3
import struct
import sys
import tempfile
CSFCHUNK_HEADER = struct.Struct(">8sQQ") # magic, filesize, offset
CHUNK_HEADER = struct.Struct(">8sQ") # type, length
CHUNK_SQLITE = b"CHNKSQLi"
def find_sqlite_chunk(data: bytes) -> tuple[int, int]:
"""CHNKSQLiチャンクのデータ開始位置とサイズを返す。
Returns:
(data_offset, data_size): SQLiteデータの開始オフセットとバイト数
"""
_, filesize, _ = CSFCHUNK_HEADER.unpack_from(data, 0)
pos = CSFCHUNK_HEADER.size
while pos < filesize:
chunk_type, length = CHUNK_HEADER.unpack_from(data, pos)
if chunk_type == CHUNK_SQLITE:
return (pos + CHUNK_HEADER.size, length)
pos += CHUNK_HEADER.size + length
raise ValueError("CHNKSQLi チャンクが見つかりません")
def rename_layers(sqlite_data: bytes, regex: str, replace: str) -> bytes:
"""SQLiteデータ内のレイヤー名を正規表現で置換し、変更後のSQLiteバイト列を返す。"""
pattern = re.compile(regex)
with tempfile.NamedTemporaryFile(suffix=".sqlite3", delete=False) as tmp:
tmp.write(sqlite_data)
tmp_path = tmp.name
try:
conn = sqlite3.connect(tmp_path)
cur = conn.cursor()
cur.execute("SELECT MainId, LayerName FROM Layer")
rows = cur.fetchall()
changed = 0
for main_id, name in rows:
if name is None:
continue
new_name = pattern.sub(replace, name)
if new_name != name:
cur.execute(
"UPDATE Layer SET LayerName = ? WHERE MainId = ?",
(new_name, main_id),
)
print(f" {name} -> {new_name}")
changed += 1
conn.commit()
conn.close()
if changed == 0:
print(" (変更なし)")
with open(tmp_path, "rb") as f:
return f.read()
finally:
os.unlink(tmp_path)
def rebuild_clip(
original: bytes, sqlite_offset: int, old_size: int, new_sqlite: bytes
) -> bytes:
"""元の.clipデータにSQLiteを差し替えて再構築する。"""
size_diff = len(new_sqlite) - old_size
# CSFCHUNKヘッダーのfilesizeを更新
_, old_filesize, offset = CSFCHUNK_HEADER.unpack_from(original, 0)
new_filesize = old_filesize + size_diff
new_header = CSFCHUNK_HEADER.pack(b"CSFCHUNK", new_filesize, offset)
# CHNKSQLiチャンクヘッダーのlengthを更新
chunk_header_offset = sqlite_offset - CHUNK_HEADER.size
new_chunk_header = CHUNK_HEADER.pack(CHUNK_SQLITE, len(new_sqlite))
return (
new_header
+ original[CSFCHUNK_HEADER.size : chunk_header_offset]
+ new_chunk_header
+ new_sqlite
+ original[sqlite_offset + old_size :]
)
def main():
parser = argparse.ArgumentParser(
description=".clip ファイルのレイヤー名を正規表現でリネーム"
)
parser.add_argument("-i", "--input", required=True, help="入力 .clip ファイル")
parser.add_argument("-o", "--output", required=True, help="出力 .clip ファイル")
parser.add_argument("--regex", required=True, help="検索する正規表現パターン")
parser.add_argument("--replace", required=True, help="置換文字列")
args = parser.parse_args()
with open(args.input, "rb") as f:
data = f.read()
print(f"入力: {args.input}")
sqlite_offset, sqlite_size = find_sqlite_chunk(data)
print(f"SQLite チャンク: offset={sqlite_offset}, size={sqlite_size}")
sqlite_data = data[sqlite_offset : sqlite_offset + sqlite_size]
print(f"レイヤー名を置換: s/{args.regex}/{args.replace}/")
new_sqlite = rename_layers(sqlite_data, args.regex, args.replace)
result = rebuild_clip(data, sqlite_offset, sqlite_size, new_sqlite)
with open(args.output, "wb") as f:
f.write(result)
print(f"出力: {args.output} ({len(result)} bytes)")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment