Created
April 4, 2026 08:08
-
-
Save kuluna/ca553b21e32799ff08e6dd6babd3abee to your computer and use it in GitHub Desktop.
クリスタファイルのレイヤー名を正規表現で一括リネームする
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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