なんかこんな感じのやつの作り方。
内容
左側にあるカラーパレットを出すボタンはinput type="color"で実現できるので、単純なカラー入力だけならこれでも構わない。現時点ではAndroid版Firefoxを除き、主要ブラウザは
ただ、コードも欲しいことが往々だと思うので、その場合は横にinput type="text"を生やして、次に示すサンプルコードのように双方の入力値をバインディングしてやればよい。
サンプルコード
動きはデモページで確認できる。
<input id="palette" type="color">
<input id="code" type="text">
<script>
const getInput = () => {
return {
palette: document.getElementById('palette'),
code: document.getElementById('code')
};
};
const registerHandlers = (paletteEl, codeEl) => {
paletteEl.addEventListener('input', (e) => {
codeEl.value = e.target.value;
});
codeEl.addEventListener('input', (e) => {
paletteEl.value = e.target.value;
});
};
window.addEventListener('DOMContentLoaded', () => {
const input = getInput();
registerHandlers(input.palette, input.code);
});
</script>
Safariでも表示できるが見た目がだいぶ異なる
PC Safari 18.2とiOS 26.3のSafariで確認した見た目。
一行目が初期表示、二行目がカラーパレット展開表示。iOS Safariの方は素の状態だとUIが小さすぎて見えないので、かなり拡大して表示している。
カスタムキャスト側でCOM3D2のプリセットを再現しようとすると、色周りの設定が数値指定できない関係で難しすぎたので、何とかしようとして見つけた方法。
一撃で同じキャラを持ってこれるのでだいぶ楽。
必要なもの
- スマホとPCを繋ぐためのUSBケーブル
- バイナリエディタ
- COM3D2
- カスタムキャスト
確認環境
| Env | Ver |
|---|---|
| Android | 16 |
| COM3D2 | 2.29.4 |
| カスタムキャスト | 1.04.01 |
やり方
- スマホをUSBケーブルでPCに繋ぐ
- スマホ側の接続形態を充電からファイル転送モードにする
- カスタムキャストを開いて適当に服・体プリセットをセーブする
内部共有ストレージ/Android/data/jp.customcast.cc2/files/Presetを開き、保存したプリセットを取り出す- COM3D2を開き、カスタムキャストで使いたいキャラのエディット画面を開き、服・体プリセットを保存する
- バイナリエディタでCOM3D2のプリセットファイルを開き、
454E44AE426082を検索し、そこより先にあるバイナリコードをすべてコピーする

- カスタムキャストのプリセットを開き、
454E44AE426082を検索し、そこより先にあるバイナリコードを、先ほどコピーしたCOM3D2のバイナリコードですべて上書きし、保存する - 保存したカスタムキャストのプリセットファイルを
内部共有ストレージ/Android/data/jp.customcast.cc2/files/Presetに戻す - カスタムキャストを開き、キャラエディットからプリセットをロードする
備考
恐らくファイルヘッダ以外ほぼ互換だと思われるが、ヘッダのリプレースが面倒なのでボディを書き換えることで達成した。
当たり前だがMODとかは反映されない。
おまけ
データを個別に取得して弄れるようにしようとツールを書こうとしていたが、パラメーターが多いのと、フォーマットが異なるものがあり、解析に飽きたので調べた残骸として残しておく。
using System.Diagnostics;
using System.Text;
class Program {
static void Main(string[] args) {
string filePath = @"C:\path\to\pre_Dummymaid_20260222134142.preset";
if (!File.Exists(filePath)) {
Debug.WriteLine($"ファイルが見つかりません: {filePath}");
return;
}
byte[] fileData = File.ReadAllBytes(filePath);
Debug.WriteLine($"ファイル読み込み完了: {fileData.Length} bytes");
var obj = new ParamFinder(fileData);
var MuneL = obj.FindBodyValue("MuneL");
var MuneS = obj.FindBodyValue("MuneS");
var MuneTare = obj.FindBodyValue("MuneTare");
var RegFat = obj.FindBodyValue("RegFat");
var Hara = obj.FindBodyValue("Hara");
var RegMeet = obj.FindBodyValue("RegMeet");
var KubiScl = obj.FindBodyValue("KubiScl");
var UdeScl = obj.FindBodyValue("UdeScl");
var EyeSclX = obj.FindBodyValue("EyeSclX");
var EyeSclY = obj.FindBodyValue("EyeSclY");
var EyePosX = obj.FindBodyValue("EyePosX");
var EyePosY = obj.FindBodyValue("EyePosY");
var EyeClose = obj.FindBodyValue("EyeClose");
var EyeBallPosX = obj.FindBodyValue("EyeBallPosX");
var EyeBallPosY = obj.FindBodyValue("EyeBallPosY");
var EyeBallSclX = obj.FindBodyValue("EyeBallSclX");
var EyeBallSclY = obj.FindBodyValue("EyeBallSclY");
var FaceShape = obj.FindBodyValue("FaceShape");
var MayuY = obj.FindBodyValue("MayuY");
var HeadX = obj.FindBodyValue("HeadX");
var HeadY = obj.FindBodyValue("HeadY");
var DouPer = obj.FindBodyValue("DouPer");
var Sintyou = obj.FindBodyValue("sintyou");
var Koshi = obj.FindBodyValue("koshi");
var Kata = obj.FindBodyValue("kata");
var West = obj.FindBodyValue("west");
var MuneUpDown = obj.FindBodyValue("MuneUpDown");
var MuneYori = obj.FindBodyValue("MuneYori");
Debug.WriteLine($"MuneL: {MuneL}, MuneS: {MuneS}, MuneTare: {MuneTare}, RegFat: {RegFat}, Hara: {Hara}, RegMeet: {RegMeet}, KubiScl: {KubiScl}, UdeScl: {UdeScl}, EyeSclX: {EyeSclX}, EyeSclY: {EyeSclY}, EyePosX: {EyePosX}, EyePosY: {EyePosY}, EyeClose: {EyeClose}, EyeBallPosX: {EyeBallPosX}, EyeBallPosY: {EyeBallPosY}, EyeBallSclX: {EyeBallSclX}, EyeBallSclY: {EyeBallSclY}, FaceShape: {FaceShape}, MayuY: {MayuY}, HeadX: {HeadX}, HeadY: {HeadY}, DouPer: {DouPer}, Sintyou: {Sintyou}, Koshi: {Koshi}, Kata: {Kata}, West: {West}, MuneUpDown: {MuneUpDown}, MuneYori: {MuneYori}");
}
}
class ParamFinder {
private Byte[] FileData;
static byte[] CreateBinaryStringBytes(string text) {
using var ms = new MemoryStream();
using var writer = new BinaryWriter(ms, Encoding.UTF8);
writer.Write(text);
writer.Flush();
return ms.ToArray();
}
/// <summary>
/// バイト配列内から指定パターンの最初の出現位置を返す (見つからなければ -1)
/// </summary>
static int FindPattern(byte[] data, byte[] pattern) {
int limit = data.Length - pattern.Length;
for (int i = 0; i <= limit; i++) {
bool match = true;
for (int j = 0; j < pattern.Length; j++) {
if (data[i + j] != pattern[j]) {
match = false;
break;
}
}
if (match)
return i;
}
return -1;
}
public ParamFinder(Byte[] fileData) {
this.FileData = fileData;
}
public int FindBodyValue(string subject) {
byte[] signature = CreateBinaryStringBytes(subject);
int sigOffset = FindPattern(this.FileData, signature);
if (sigOffset < 0) {
throw new Exception($"エラー: データセクションのシグネチャ '${subject}' が見つかりませんでした。");
}
using var ms = new MemoryStream(this.FileData, sigOffset, this.FileData.Length - sigOffset);
using var reader = new BinaryReader(ms, Encoding.UTF8);
reader.ReadString();
reader.ReadString();
reader.ReadInt32();
reader.ReadInt32();
reader.ReadString();
reader.ReadInt32();
reader.ReadInt32();
return reader.ReadInt32();
}
}
既存CGIを一切触らずに、前段にアクセス制御やアクセスロガーをつけたいとかの用途でラッパーCGIを使うと上手くいくので、その方法を書く。
なお前段が動くことしか試していないが、備考に後段で処理をさせる方法についても軽く触れている。
動作機序
CGIはコマンドライン引数、環境変数、標準入力を受け取り、何かを処理した結果を標準出力するプログラムである。
つまりラッパーCGIはコマンドライン引数、環境変数、標準入力を受け取り、それをラッピングするCGIにそのまま受け渡し、このCGIの標準出力をリダイレクトできればよい。
やり方
以下のようなコードを書き、exec()前に前段の処理を書けばよい。
#!/usr/bin/perl
use strict;
use warnings;
#
# ここに実行前に挟みたい処理
#
my $original_cgi = './hoge.cgi';
exec($original_cgi) or die "Cannot exec $original_cgi: $!";
今回実際に作ったサンプル
adiaryにはアクセス制限をする機能がなく、本体を弄るのが嫌だったので前段に処理を入れることで実現した。
#!/usr/bin/perl
use strict;
use warnings;
# 以下のコマンドでIP::Geolocation::MMDBをインストールしていることが前提
# cpanm -l extlib IP::Geolocation::MMDB
use lib './extlib/lib/perl5';
use IP::Geolocation::MMDB;
# https://download.db-ip.com/free/dbip-city-lite-YYYY-MM.mmdb.gz
# ex. https://download.db-ip.com/free/dbip-city-lite-2026-01.mmdb.gz
my $db = IP::Geolocation::MMDB->new(file => './DBIP-City.mmdb');
my $country_code = $db->getcc($ENV{REMOTE_ADDR});
# 日台韓は許可する方針(怪しい挙動を見たことがないため)
my @allow_country_codes = ('JP', 'TW', 'KR');
my $user_agent = $ENV{HTTP_USER_AGENT};
# 許可する国コードかチェック
my $is_allowed_country = grep { $_ eq $country_code } @allow_country_codes;
# 許可するBOTのUAパターン
my @allowed_bot_patterns = (
qr/bot/i,
qr/curl/i,
qr/wget/i,
qr/google/i,
qr/bing/i,
qr/mastodon/i,
qr/misskey/i,
qr/pleroma/i,
qr/akkoma/i,
qr/lemmy/i,
qr/activitypub/i,
qr/hatena/i,
qr/github/i,
qr/tumblr/i,
qr/meta/i
);
# 許可するBOTかチェック
my $is_allowed_bot = 0;
for my $pattern (@allowed_bot_patterns) {
if ($user_agent =~ $pattern) {
$is_allowed_bot = 1;
last;
}
}
# 許可国でもなく、許可BOTでもなければエラーにする
if (!$is_allowed_country && !$is_allowed_bot) {
# 未知のSNS BOTを将来的に許可するために、BOTくさいUAのログを集めておく
if ($user_agent !~ /Windows|Mac OS|Linux|Android|iOS|iPhone|iPad/i) {
# OGP取得BOTに間違いなく含まれない文字列が入ってるものはログに入れない
my $deny_ua_log_file = './deny_ua.log';
if (open my $fh, '>>', $deny_ua_log_file) {
my $time = localtime();
my $remote = $ENV{REMOTE_ADDR} // 'unknown';
my $uri = $ENV{REQUEST_URI} // 'unknown';
print $fh "[$time]\t\"$user_agent\"\t$country_code\t$remote\t$uri\n";
close $fh;
}
}
print "Status: 403 Forbidden\n";
print "Content-Type: text/plain; charset=UTF-8\n\n";
print "Access denied.\n";
exit;
}
# adiary呼び出し
my $original_cgi = './adiary.cgi';
exec($original_cgi) or die "Cannot exec $original_cgi: $!";
備考
perldocを読んだ感じ、互換性に問題が出る可能性も少なからずあるようだ。
perldocのexec関数の説明を見る感じ、ENDブロックや、オブジェクトのDESTROYメソッドを起動しないとあるので、実装方法次第では正しく動かない可能性もあるのかもしれない。
また「戻って欲しい場合には、execではなく system関数を使ってください」とあるため、もし後処理をしたい場合はexec関数でなくsystem関数を使うとよいと思う。
あとがき
レンタルサーバーではWAFが自由に使えないため、なんちゃってWAFの様なものを作りたいとか、レンタルサーバーを新規に始めたく、CGIにバナー広告を差し込みたいといったケースがある場合に、今回のような手法は便利だろう。
今時、往年のレンタルサーバーを新規に始め、それもバナー広告を出したいと考える人物がいるかどうかは謎だが、共通的に何かを差し込みたいなど、何かしら活用方法はあるかもしれない。
ググって出てきた記事が軒並み古くて役に立たなかったので、令和八年最新版として書いておく。
やり方
- CPANMのインストール
curl -L https://cpanmin.us | perl - App::cpanminus - CPANMのパスを
.cshrcに書く
私はZSHを使っているため.zshrcに書いているが、デフォルト環境はcshのはずなので.cshrcに書けば成り立つと思う。echo PATH=${HOME}/perl5/bin:${PATH} - local::libをインストールする
local::libはroot以外にあるCPANモジュールを使うためのものらしい。これを自分のホームディレクトリ配下に入れるようにコマンドを流す。cpanm --local-lib=~/perl5 local::lib && eval $(perl -I ~/perl5/lib/perl5/ -Mlocal::lib)
使い方
以下のようなコマンドを流すとパスを指定してCPANモジュールを取得・展開できる。
# 書式
cpanm -l <パス> <モジュール名>
例えば以下を流すとpwd配下にextlibディレクトリが生成され、IP::Geolocation::MMDBがダウンロードされて展開される。勿論、依存関係も勝手に解決してくれる。
cpanm -l extlib IP::Geolocation::MMDB
モジュールを利用するときはライブラリの配置されているルートを指定し、次にモジュール名を指定するとうまくいくようだ。
use lib './extlib/lib/perl5';
use IP::Geolocation::MMDB;
あとがき
https://cpanmin.us/を見に行くと以下の記述があり、辿ってみると日本の人が作っていてちょっと驚いた。
# This is a pre-compiled source code for the cpanm (cpanminus) program.
# For more details about how to install cpanm, go to the following URL:
#
# https://github.com/miyagawa/cpanminus





