gazou-compressor.jp

文字が滲む問題の決定版:Chroma Subsamplingとアンチブラー設計(JPEG/WebP/AVIF)

スクショやUI画像を圧縮すると文字がにじむ——。犯人の多くはYCbCrのChroma Subsampling(色差の間引き)です。写真ではほぼ無害でも、文字/線画では致命傷。本稿は「どれを4:4:4にするか」を運用で迷わないよう、 ルールとスクリプトで自動化します。

先に結論
  • 写真は 4:2:0、UI/文字/図版は 4:4:4 が起点。
  • ビルド前に命名規約や簡易検出で自動プリセット。
  • 検品は「白フチ/階段」「色のにじみ」を重点チェック。

要点(TL;DR)

1. なぜ“にじむ”のか

多くの画像コーデックは輝度(Y)>色差(CbCr)の性質を利用し、色だけ間引きます(4:2:0)。文字や線は色境界が鋭く、 ここが間引かれると白フチ彩度の滲みとして現れます。4:4:4は色差もフル解像度で保持し、にじみを抑えます。

2. 最短フロー(実務)

  1. sRGB正規化と回転適用。
  2. 命名規約(例:/ui/-text)を決める。
  3. 上記規約に基づき4: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. 公開前チェック

6. まとめ

文字のにじみは4:4:4の適用範囲でほぼ防げます。機械的な規約化で運用を安定させ、体感と容量のバランスを最適化しましょう。

公開:2025-09-03

gazou-compressor.jp 編集部

画像圧縮・変換・背景除去などの実践テクニックと、Webで“速く・軽く・崩さない”ためのノウハウを発信しています。

関連記事