ある朝、自分の壁紙アプリでプレビューした画像と、実際に設定した後のロック画面の色が、どうも違って見えました。プレビューでは深い青だった空が、設定後は少し灰色がかってくすんで見えるのです。最初はディスプレイの明るさの問題かと思ったのですが、明るさを揃えても差は消えませんでした。
原因は広色域、いわゆる Display P3 と sRGB の取り違えでした。2019年に吉祥寺駅の上空で光の輪を見て以来、私は色や光の見え方にずいぶん敏感になったのですが、まさか自分の配信パイプラインで色が痩せているとは気づいていませんでした。この記事は、6本の壁紙アプリを個人開発で運用しながら色域を一つずつ詰めてきたときの手順と、確認に使ったコマンドの記録です。単に「プロファイルを埋める」だけでなく、画素が本当に広色域かを見分ける段階、配信前に取りこぼしを止めるゲート、そして Android 側の事情まで含めて整理します。
なぜ実機で色がくすんで見えたのか
iPhone は iPhone 7 の頃から Display P3 という広色域ディスプレイを積んでいます。sRGB より一回り広い範囲の色を表示できるので、鮮やかな赤や緑が素直に出ます。問題は、画像にカラープロファイル(ICCプロファイル)が埋め込まれていないときに起こります。
iOS はプロファイルのない画像を「これは sRGB だろう」とみなして表示します。ところが私が配信していた一部の壁紙は、書き出しのときにプロファイルが剥がれた状態で、しかも中身は P3 の色域で塗られていました。P3 の数値を sRGB の物差しで読むと、彩度が低い方へずれます。これが「くすむ」の正体でした。逆にプロファイルが正しく埋まっていれば、iOS が自動で色域を合わせてくれるので、P3 非対応の古い端末でも破綻しません。
ここで大事な区別が一つあります。色には「画素の数値」と「その数値をどの物差しで読むかというプロファイル」の二つがあり、どちらかが欠けると意図した色になりません。くすみは、画素は P3 なのにプロファイルが無いせいで sRGB の物差しで読まれた、という食い違いから生まれていました。
まず色域を確認する
推測で直すと泥沼にはまります。最初にやったのは、配信中の画像が実際にどの色域を持っているかを機械的に確認することでした。macOS の sips で埋め込みプロファイルを読めます。
# 単体ファイルのカラープロファイルを確認する
sips --getProperty profile wallpaper_001.jpg
# 配信フォルダ全体をざっと棚卸しする
for f in ./assets/*.jpg; do
echo "$f"
sips --getProperty profile "$f" 2>/dev/null | grep -i "profile:" || echo " (プロファイルなし)"
done
6アプリ合わせておよそ2,000枚を棚卸ししたところ、3割近くがプロファイルなし、あるいは sRGB タグのまま中身が P3 という状態でした。書き出しツールの設定が「カラープロファイルを埋め込まない」になっていたのが直接の原因です。ここを直すだけで、くすみの大半は消えました。
プロファイルの有無だけでは足りない
棚卸しを進めるうちに、もう一段やっかいな問題に気づきました。プロファイルが「sRGB」と埋まっていても、画素そのものが sRGB の範囲を超えた鮮やかさで塗られている画像があったのです。この場合、P3 を貼り直すべきか sRGB のままでよいかは、タグだけ見ても判断できません。中身を覗く必要があります。
私は ImageMagick で、画素が sRGB の枠をどれくらいはみ出しているかをざっくり測ることにしました。一度 sRGB に丸めてから元と比べ、差が大きければ「中身は広色域」と当たりを付けます。
# 元画像を sRGB に変換し、元との差分の大きさ(RMSE)を測る
# 差が大きい = sRGB に収まらない鮮やかさを持つ = 広色域の中身
orig="wallpaper_001.jpg"
magick "$orig" -profile sRGB.icc /tmp/as_srgb.jpg
diff=$(magick compare -metric RMSE "$orig" /tmp/as_srgb.jpg null: 2>&1 | sed 's/ .*//')
echo "$orig RMSE=$diff"
# 経験則: 手元では RMSE が 1500 を超える画像は P3 由来であることが多かった
判定の経験則はアプリの画づくりによって変わるので、数枚を目視で確かめて閾値を合わせるのが現実的です。要点は、タグを鵜呑みにせず「画素が広色域を使っているか」を一度測ること。ここを飛ばすと、次の貼り直しで色を壊します。
配信前に色域をそろえる
方針は、広色域の中身を持つ壁紙には Display P3 のプロファイルを明示的に埋めて配信する、というものに落ち着きました。P3 を埋めておけば、P3 端末では鮮やかに、sRGB 端末では iOS が縮めて表示してくれます。両対応を端末側に任せられるのが楽です。
# 中身が P3 の画像に Display P3 プロファイルを割り当て直す
# (前段の判定で「広色域」と確認できた画像にのみ行う)
sips --matchTo "/System/Library/ColorSync/Profiles/Display P3.icc" \
input.jpg --out output.jpg
中身が sRGB の画像に P3 プロファイルだけ貼ると、今度は色が不自然に膨らみます。だからこそ前段の「中身が広色域か」の判定が効いてきます。元データがどちらの色域で作られたかを把握したうえで割り当てるのが大事だと、何枚か失敗してから学びました。
配信前ゲートをスクリプトにする
一度直しても、次の更新で書き出し設定を戻してしまえば同じくすみがぶり返します。私は「プロファイルなし」をゼロにする小さなゲートを配信スクリプトの手前に挟むことにしました。プロファイルの無い画像が一枚でもあれば、配信を止めて知らせる作りです。
#!/usr/bin/env bash
# gamut-gate.sh — 配信フォルダにプロファイルなし画像があれば exit 1
set -euo pipefail
DIR="${1:-./assets}"
missing=0
while IFS= read -r -d '' f; do
if ! sips --getProperty profile "$f" 2>/dev/null | grep -qi "profile:"; then
echo "⚠️ プロファイルなし: $f"
missing=$((missing + 1))
fi
done < <(find "$DIR" -type f \( -iname '*.jpg' -o -iname '*.png' \) -print0)
if [ "$missing" -gt 0 ]; then
echo "🚫 $missing 枚がプロファイルなし。再書き出ししてから配信してください。"
exit 1
fi
echo "✅ 全画像にプロファイルあり。配信OK。"
このゲートを CI や配信前の手元スクリプトに一行足すだけで、取りこぼしは構造的に止まります。色域のような「忘れた頃に戻る」問題は、人の注意ではなく仕組みで止めるのが結局いちばん壊れにくいというのが、6アプリを並行で回してきた実感です。
配信前の段取りを整理すると、私の手順は次の3つに落ち着いています。
- 棚卸し —
sips --getProperty profile で全画像のプロファイルの有無を確認する。
- 判定と統一 — ImageMagick の RMSE で中身が広色域かを測り、広色域のものだけ Display P3 を埋め直す。
- ゲート —
gamut-gate.sh を配信スクリプトの手前に置き、プロファイルなしが一枚でもあれば配信を止める。
Xcode のアセットと React Native 側
アプリにバンドルするアイコンやオンボーディング画像は、Asset Catalog の挙動も関わります。Xcode のアセットには表示色域の設定があり、Any & sRGB のままだと広色域の画像が sRGB に丸められます。広色域で見せたい画像は色域の指定を Display P3 に切り替えておきます。
Rork で組んだ画面では、リモートから読み込む壁紙の表示に expo-image を使っています。iOS では内部的に画像のプロファイルを尊重してくれるので、配信側で正しく P3 を埋めておけば、コード側で特別な色変換は不要でした。ここはあえて凝らず、配信パイプラインの責務として色域をそろえる方を選んでいます。
import { Image } from "expo-image";
// 色変換はコード側で行わない。配信画像が正しいプロファイルを
// 持っている前提で、素直に表示するだけに留める
export function WallpaperPreview({ uri }: { uri: string }) {
return (
<Image
source={{ uri }}
style={{ flex: 1 }}
contentFit="cover"
cachePolicy="memory-disk"
/>
);
}
読み込みやキャッシュの詰め方そのものは別の話になるので、画像表示の性能面は壁紙アプリの画像読み込みを expo-image で詰めた記録に分けて書いています。色域は「正しいデータを配る」段階で決着させ、表示側は素直にしておくのが、6アプリを並行で回す私には一番壊れにくい形でした。
Android の広色域はまた別の話
iOS で色域が落ち着いても、Android はまた事情が違います。Android は端末によって広色域ディスプレイの有無が分かれ、しかもアプリ側で「広色域で描く」と宣言しないと、広色域の表示に乗ってくれない端末があります。Expo なら app.json でウィンドウの色モードを指定できます。
{
"expo": {
"android": {
"manifestPlaceholders": {},
"wideColorGamut": true
}
}
}
設定によっては AndroidManifest.xml の Activity に android:colorMode="wideColorGamut" を持たせる形になります。注意したいのは、広色域を有効にすると省電力面で多少の代償があり、すべての画面で常時オンにする必要はない点です。壁紙のプレビューのように「色が主役」の画面だけ広色域に乗せ、それ以外は通常のままにする、という割り切りが現実的でした。
下表は、私がアセット種別ごとに落ち着けた方針です。
| アセット種別 | 埋めるプロファイル | 備考 |
| リモート配信の壁紙(広色域) | Display P3 | 配信前ゲートで必須化。中身が P3 と確認したもののみ |
| リモート配信の壁紙(sRGB原画) | sRGB | 無理に P3 を貼らない。膨らんで不自然になる |
| アプリアイコン | sRGB | 互換性優先。広色域の旨味が小さい |
| オンボーディング画像 | 原画に合わせる | Xcode アセットの色域指定を Display P3 に |
| App Store スクリーンショット | sRGB(プロファイル必須) | 剥がれやすい。提出前に必ず確認 |
実機とスクリーンショットでの確認
数字だけ見て安心せず、最後は目で確かめます。私が回しているのは2段階の確認です。まず P3 端末(手元では iPhone の比較的新しい機種)と、sRGB しか持たない古い端末の両方で同じ壁紙を開き、彩度が破綻していないかを見ます。プロファイルを正しく埋めていれば、古い端末では少しおとなしく、新しい端末では鮮やかに、それでもどちらも自然に見えるはずです。
もう一つ気をつけているのが App Store のスクリーンショットです。スクリーンショット用に書き出した PNG も、配信画像と同じ理由でプロファイルが剥がれることがあります。ストアの製品ページで色がくすんで見えると、それだけで第一印象が変わってしまいます。私はスクリーンショットも sips --getProperty profile を通してから提出するようにしました。
# 提出前にスクリーンショットの色域も確認しておく
for f in ./store-screenshots/*.png; do
sips --getProperty profile "$f" 2>/dev/null | grep -i "profile:" \
|| echo "$f: プロファイルなし(要再書き出し)"
done
詰めてみて気づいたこと
色域は、派手な機能ではないぶん後回しにされがちです。私自身、個人開発でアプリを作り続けてきた中で、壁紙という「見た目がすべて」のジャンルで何年も色を取りこぼしていたのは、正直に書けば少し悔しい発見でした。
一方で、原因が分かってしまえば対処は単純でした。配信画像のプロファイルを棚卸しし、中身が広色域かを測り、P3 で統一して埋め直す。表示側は素直なまま、配信前にゲートで取りこぼしを止める。新しい技術を足すのではなく、すでに配っているデータの素性を確認することが効いた、という点が印象に残っています。
次の一手は明確です。書き出しスクリプトの末尾に gamut-gate.sh を一行差し込み、「プロファイルなし」をゼロにしてから配信する習慣にすること。同じように壁紙や写真を扱うアプリを作っている方が、配信前に一度だけ色域を測ってみるきっかけになれば幸いです。