文字が滲む問題の決定版:Chroma Subsamplingとアンチブラー設計(JPEG/WebP/AVIF)
スクショやUI画像を圧縮すると文字がにじむ——。犯人の多くはYCbCrのChroma Subsampling(色差の間引き)です。写真ではほぼ無害でも、文字/線画では致命傷。本稿は「どれを4:4:4にするか」を運用で迷わないよう、 ルールとスクリプトで自動化します。
先に結論
- 写真は 4:2:0、UI/文字/図版は 4:4:4 が起点。
- ビルド前に命名規約や簡易検出で自動プリセット。
- 検品は「白フチ/階段」「色のにじみ」を重点チェック。
要点(TL;DR)
- UI/文字=4:4:4(JPEGはQ≈86、WebPはnear-lossless、AVIFは4:4:4)。
- 写真=4:2:0(JPEG Q≈78 / AVIF Q≈50 からABテスト)。
- ルールは前処理で静的化。CSRで凝った補正はINP悪化の元。
1. なぜ“にじむ”のか
多くの画像コーデックは輝度(Y)>色差(CbCr)の性質を利用し、色だけ間引きます(4:2:0)。文字や線は色境界が鋭く、 ここが間引かれると白フチや彩度の滲みとして現れます。4:4:4は色差もフル解像度で保持し、にじみを抑えます。
2. 最短フロー(実務)
- sRGB正規化と回転適用。
- 命名規約(例:
/ui/
や-text
)を決める。 - 上記規約に基づき4:4:4プリセットを自動適用(下記スクリプト)。
- 一覧ページは LQIP を併用、ヒーローは Progressive。
3. 実装レシピ(コピペOK)
3.1 自動振り分けエクスポート
// scripts/export-anti-blur.ts
// 依存: npm i -D sharp tsx
// 規約に合うファイル名/パスは 4:4:4 + やや高品質、それ以外は 4:2:0
import fs from "node:fs/promises";
import path from "node:path";
import sharp from "sharp";
const IN = process.env.IN ?? "input";
const OUT = process.env.OUT ?? "output";
const UI_HINT = /(\/ui\/|\/icons\/|\btext\b|\bchart\b|\bdiagram\b|_cap|_figure)/i;
async function main() {
await fs.mkdir(OUT, { recursive: true });
const list = (await fs.readdir(IN)).filter(f => /\.(png|jpe?g|webp|avif)$/i.test(f));
for (const f of list) {
const src = path.join(IN, f);
const name = path.parse(f).name;
const isUI = UI_HINT.test(src);
const base = sharp(src).rotate().toColourspace("srgb");
// AVIF
await base
.avif({
quality: isUI ? 60 : 50,
chromaSubsampling: isUI ? "4:4:4" : "4:2:0",
effort: 4,
})
.toFile(path.join(OUT, name + ".avif"));
// WebP(UIは nearLossless でエッジ保持)
await base
.webp({
quality: isUI ? 85 : 80,
nearLossless: isUI ? 60 : undefined,
})
.toFile(path.join(OUT, name + ".webp"));
// JPEG(mozjpeg推奨)
await base
.jpeg({
quality: isUI ? 86 : 78,
chromaSubsampling: isUI ? "4:4:4" : "4:2:0",
mozjpeg: true,
progressive: true,
})
.toFile(path.join(OUT, name + ".jpg"));
console.log("→", name, isUI ? "UI(4:4:4)" : "Photo(4:2:0)");
}
}
main().catch(e => { console.error(e); process.exit(1); });
3.2 文字/ライン検出のヒント(任意)
// scripts/detect-textlike.ts
// 目的: 「UI/文字の可能性が高い画像」を検出して4:4:4候補にする
// 方法: ラプラシアンでエッジ密度と方向性(縦横成分)を簡易評価(擬似。実運用はOpenCV等が堅牢)
import fs from "node:fs/promises";
import path from "node:path";
import sharp from "sharp";
const IN = process.env.IN ?? "input";
async function main() {
const files = (await fs.readdir(IN)).filter(f => /\.(png|jpe?g|webp)$/i.test(f));
for (const f of files) {
const src = path.join(IN, f);
const img = sharp(src).greyscale().normalize();
const { width, height } = await img.metadata();
if (!width || !height) continue;
const small = await img.resize({ width: 256 }).raw().toBuffer();
// 粗いヒューリスティック: 明暗の急峻な変化が多い=テキスト/ラインっぽい
let sum = 0;
for (let i = 1; i < small.length; i++) sum += Math.abs(small[i] - small[i - 1]);
const score = sum / small.length;
if (score > 8) console.log("UI-likely:", f, "score=", score.toFixed(1));
}
}
main().catch(console.error);
4. 応用と使いどころ
5. 公開前チェック
- UI/文字/図版は 4:4:4(WebPはnear-lossless)。
- 白フチ・彩度のにじみ・階段がない。
- ヒーロー/一覧の読み込み戦略は INP時代の設計 に準拠。
6. まとめ
文字のにじみは4:4:4の適用範囲でほぼ防げます。機械的な規約化で運用を安定させ、体感と容量のバランスを最適化しましょう。