目次

ライブラリを使用せずにwebp画像の幅と高さを取得する

目次

モギーク
質問: webp画像の幅と高さを知りたい
webp画像の幅と高さ情報を取得したいです。なるべくライブラリは使わずに取得したいのですが、どう実装すればよいでしょうか?

実装

ImageSharpなどのライブラリを利用せずにwebp画像の幅と高さを取得するコードを紹介します。

注意
本記事のコードは複数のLLMでの検討により作成されたコードです。

Windowsフォームアプリケーションを作成します。

UI

以下のフォームを準備します。ボタンを3つテキストボックスを2つ、OpenFileDialogを配置します。

ライブラリを使用せずにwebp画像の幅と高さを取得する:画像1

コード

以下のコードを作成します。


フォームにはシンプルな読み取りロジックである、GetWebPSize_Simple メソッドを実装しています。

namespace WebpGetWidthHeight
{
    public partial class FormMain : Form
    {
        public FormMain()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (openFileDialog1.ShowDialog() == DialogResult.OK) {
                textBox1.Text = openFileDialog1.FileName;
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            GetWebPSize_Simple(textBox1.Text, out int width, out int height);
            textBox2.Text += $"Width: {width}, Height: {height}\r\n";

        }


        private static void GetWebPSize_Simple(string path, out int width, out int height)
        {
            using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
            using (BinaryReader reader = new BinaryReader(fs)) {
                // RIFF ヘッダー確認
                byte[] riff = reader.ReadBytes(4);
                if (System.Text.Encoding.ASCII.GetString(riff) != "RIFF") {
                    throw new InvalidDataException("WebP ファイルではありません");
                }

                reader.ReadInt32(); // ファイルサイズ (スキップ)

                byte[] webp = reader.ReadBytes(4);
                if (System.Text.Encoding.ASCII.GetString(webp) != "WEBP") {
                    throw new InvalidDataException("WebP ファイルではありません");
                }

                byte[] chunkType = reader.ReadBytes(4);
                string chunkStr = System.Text.Encoding.ASCII.GetString(chunkType);

                if (chunkStr == "VP8 ") {
                    // Lossy WebP
                    reader.ReadBytes(7); // チャンクサイズ + フレームタグ
                    byte[] sizeData = reader.ReadBytes(4);
                    width = (sizeData[0] | (sizeData[1] << 8)) & 0x3FFF;
                    height = (sizeData[2] | (sizeData[3] << 8)) & 0x3FFF;
                }
                else if (chunkStr == "VP8L") {
                    // Lossless WebP
                    reader.ReadBytes(5); // チャンクサイズ + signature
                    byte[] sizeData = reader.ReadBytes(4);
                    width = 1 + ((sizeData[0] | (sizeData[1] << 8)) & 0x3FFF);
                    height = 1 + (((sizeData[1] >> 6) | (sizeData[2] << 2) | (sizeData[3] << 10)) & 0x3FFF);
                }
                else if (chunkStr == "VP8X") {
                    // Extended WebP
                    reader.ReadBytes(8); // チャンクサイズ + flags
                    byte[] w = reader.ReadBytes(3);
                    byte[] h = reader.ReadBytes(3);
                    width = 1 + (w[0] | (w[1] << 8) | (w[2] << 16));
                    height = 1 + (h[0] | (h[1] << 8) | (h[2] << 16));
                }
                else {
                    throw new InvalidDataException("未対応の WebP 形式です: " + chunkStr);
                }
            }
        }

        private void button3_Click(object sender, EventArgs e)
        {
          StrictWebpSize.GetWebPSizeStrict(textBox1.Text, out int width, out int height);
          textBox2.Text += $"Width: {width}, Height: {height}\r\n";
        }
    }
}

こちらはより厳密な読み取りをする StrictWebpSize クラスです。

StrictWebpSize.cs
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Text;

namespace WebpGetWidthHeight
{
  internal static class StrictWebpSize
  {

    public static void GetWebPSizeStrict(string path, out int width, out int height)
    {
      if (!TryGetWebPSizeStrict(path, out width, out height, out var error))
        throw new InvalidDataException(error);
    }

    public static bool TryGetWebPSizeStrict(string path, out int width, out int height, out string error)
    {
      width = height = 0;
      error = "";

      using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read,
          bufferSize: 4096, options: FileOptions.SequentialScan);

      long fileLen = fs.Length;
      if (fileLen < 12) { error = "ファイルが短すぎます"; return false; }

      Span<byte> b4 = stackalloc byte[4];

      // RIFF
      if (!ReadExactly(fs, b4) || !IsFourCC(b4, "RIFF")) { error = "RIFFヘッダ不正"; return false; }

      if (!ReadExactly(fs, b4)) { error = "RIFFサイズ読取失敗"; return false; }
      uint riffSize = BinaryPrimitives.ReadUInt32LittleEndian(b4); // RIFF payload size (after this field)
                                                                   // riffSize は "WEBP" 以降も含む。ファイル全体は 8 + riffSize が基本。
                                                                   // 厳密:ファイルがそれより短いのは不正。長い場合は「後ろにゴミ」扱いで拒否するか許容するか方針次第。
      long expectedLen = 8L + riffSize;
      if (fileLen != expectedLen) {
        error = $"RIFFサイズ不整合: fileLen={fileLen}, expected={expectedLen}";
        return false;
      }

      // WEBP
      if (!ReadExactly(fs, b4) || !IsFourCC(b4, "WEBP")) { error = "WEBP識別子不正"; return false; }

      long riffPayloadEnd = 8L + riffSize; // ファイル末尾と一致させたのでこれが終端

      bool sawVP8X = false;
      bool sawImageChunk = false;
      int canvasW = 0, canvasH = 0;

      while (fs.Position < riffPayloadEnd) {
        // チャンクヘッダ
        if (!ReadExactly(fs, b4)) { error = "チャンクFourCC読取失敗"; return false; }
        var chunkType = b4.ToArray(); // 4 bytes
        if (!ReadExactly(fs, b4)) { error = "チャンクSize読取失敗"; return false; }
        uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(b4);

        long chunkDataStart = fs.Position;
        long chunkDataEnd = chunkDataStart + chunkSize;

        // 厳密:チャンクがRIFF終端を超えるのは不正
        if (chunkDataEnd > riffPayloadEnd) {
          error = "チャンクサイズがRIFF終端を超えています";
          return false;
        }

        // ---- VP8X ----
        if (IsFourCC(chunkType, "VP8X")) {
          // VP8X が複数回は不正(厳密)
          if (sawVP8X) { error = "VP8Xが複数あります"; return false; }
          // VP8X chunk payload は少なくとも 10 bytes(flags(1)+rsv(3)+w(3)+h(3)):contentReference[oaicite:4]{index=4}
          if (chunkSize < 10) { error = "VP8Xチャンクが短すぎます"; return false; }

          Span<byte> vp8x = stackalloc byte[10];
          if (!ReadExactly(fs, vp8x)) { error = "VP8X読取失敗"; return false; }

          byte flags = vp8x[0];
          // 予約ビットの厳密検証:仕様では予約は0であること(MUST/SHOULD):contentReference[oaicite:5]{index=5}
          // flags内の予約ビット位置は仕様依存(Rsv等)。ここでは reserved 4bit を0期待、などの厳密検証を入れたい場合は
          // RFC9649 / container spec のビット配置に合わせてマスクしてください。
          // まずは「reserved 24bits must be 0」を厳密に検証(vp8x[1..4])
          if (vp8x[1] != 0 || vp8x[2] != 0 || vp8x[3] != 0) {
            error = "VP8X reserved(24) が 0 ではありません";
            return false;
          }

          int wMinus1 = vp8x[4] | (vp8x[5] << 8) | (vp8x[6] << 16);
          int hMinus1 = vp8x[7] | (vp8x[8] << 8) | (vp8x[9] << 16);

          canvasW = checked(wMinus1 + 1);
          canvasH = checked(hMinus1 + 1);

          // RFC9649: CanvasWidth*CanvasHeight <= 2^32 - 1 :contentReference[oaicite:6]{index=6}
          ulong prod = (ulong)canvasW * (ulong)canvasH;
          if (prod > 0xFFFF_FFFFUL) {
            error = "Canvas幅高の積が仕様上限を超えています";
            return false;
          }

          sawVP8X = true;

          // 余剰があれば unknown fields として無視(ただし chunkSizeに従ってスキップ)
          long remain = chunkSize - 10;
          if (!SkipExactly(fs, remain)) { error = "VP8X余剰スキップ失敗"; return false; }
        }
        // ---- VP8  (Lossy) ----
        else if (IsFourCC(chunkType, "VP8 ")) {
          if (sawImageChunk) { error = "画像チャンクが複数あります"; return false; }
          sawImageChunk = true;

          // VP8 frame header を読む:最低 10 bytes 必要(3 bytes frame tag + 3 bytes start code + 4 bytes W/H)
          if (chunkSize < 10) { error = "VP8チャンクが短すぎます"; return false; }

          Span<byte> header = stackalloc byte[10];
          if (!ReadExactly(fs, header)) { error = "VP8ヘッダ読取失敗"; return false; }

          // frame tag (3 bytes little-endian bits)
          int frameTag = header[0] | (header[1] << 8) | (header[2] << 16);
          bool isKeyFrame = (frameTag & 0x1) == 0; // 0 = key frame in VP8
          if (!isKeyFrame) { error = "VP8がキーフレームではありません(静止画WebPとして不正)"; return false; }

          // start code 9D 01 2A
          if (header[3] != 0x9D || header[4] != 0x01 || header[5] != 0x2A) {
            error = "VP8 start code 不正";
            return false;
          }

          ushort wRaw = (ushort)(header[6] | (header[7] << 8));
          ushort hRaw = (ushort)(header[8] | (header[9] << 8));

          int w = wRaw & 0x3FFF;
          int h = hRaw & 0x3FFF;
          int hScale = (wRaw >> 14) & 0x3;
          int vScale = (hRaw >> 14) & 0x3;

          // scale bits は通常 0..3 の範囲だが、厳密に「静止画WebPなら0であるべき」等のポリシーを入れるならここで判定
          // ここでは仕様に反しない範囲確認のみ
          if (w <= 0 || h <= 0) { error = "VP8の幅高が不正"; return false; }

          // 残りのVP8ビットストリームをスキップ
          long remain = chunkSize - 10;
          if (!SkipExactly(fs, remain)) { error = "VP8残りスキップ失敗"; return false; }

          // VP8Xがある場合、canvasと一致すべき(厳密)
          if (sawVP8X && (w != canvasW || h != canvasH)) {
            error = $"VP8の幅高({w}x{h})がVP8X canvas({canvasW}x{canvasH})と一致しません";
            return false;
          }

          width = w;
          height = h;
        }
        // ---- VP8L (Lossless) ----
        else if (IsFourCC(chunkType, "VP8L")) {
          if (sawImageChunk) { error = "画像チャンクが複数あります"; return false; }
          sawImageChunk = true;

          // 1-byte signature (0x2f) + 4 bytes size bits = minimum 5
          if (chunkSize < 5) { error = "VP8Lチャンクが短すぎます"; return false; }

          Span<byte> head = stackalloc byte[5];
          if (!ReadExactly(fs, head)) { error = "VP8Lヘッダ読取失敗"; return false; }

          if (head[0] != 0x2F) { error = "VP8L signature(0x2f) 不正"; return false; } // :contentReference[oaicite:7]{index=7}

          // 次の4バイトに 14bit width-1, 14bit height-1 等が入る(bitstream spec):contentReference[oaicite:8]{index=8}
          uint bits = (uint)(head[1] | (head[2] << 8) | (head[3] << 16) | (head[4] << 24));
          int w = (int)(bits & 0x3FFF) + 1;
          int h = (int)((bits >> 14) & 0x3FFF) + 1;

          if (w <= 0 || h <= 0) { error = "VP8Lの幅高が不正"; return false; }

          long remain = chunkSize - 5;
          if (!SkipExactly(fs, remain)) { error = "VP8L残りスキップ失敗"; return false; }

          if (sawVP8X && (w != canvasW || h != canvasH)) {
            error = $"VP8Lの幅高({w}x{h})がVP8X canvas({canvasW}x{canvasH})と一致しません";
            return false;
          }

          width = w;
          height = h;
        }
        else {
          // 未知/メタ系チャンクは strict でも「仕様に従ってスキップ」(unknown chunks ignored)
          if (!SkipExactly(fs, chunkSize)) { error = "未知チャンクのスキップ失敗"; return false; }
        }

        // チャンクは偶数境界にパディング(奇数サイズなら1バイト)
        if ((chunkSize & 1) == 1) {
          if (!SkipExactly(fs, 1)) { error = "パディングスキップ失敗"; return false; }
        }

        // 念のため、チャンク終端にいることを確認(strict)
        long expectedPos = chunkDataEnd + ((chunkSize & 1) == 1 ? 1 : 0);
        if (fs.Position != expectedPos) {
          error = "内部位置がチャンク境界と一致しません(実装またはファイル不正)";
          return false;
        }
      }

      if (!sawImageChunk) {
        error = "VP8/VP8L画像チャンクが見つかりません";
        return false;
      }

      return true;
    }

    private static bool IsFourCC(ReadOnlySpan<byte> four, string s) =>
        four.Length == 4 &&
        (byte)s[0] == four[0] && (byte)s[1] == four[1] &&
        (byte)s[2] == four[2] && (byte)s[3] == four[3];

    private static bool IsFourCC(byte[] four, string s) =>
        four.Length == 4 &&
        (byte)s[0] == four[0] && (byte)s[1] == four[1] &&
        (byte)s[2] == four[2] && (byte)s[3] == four[3];

    private static bool ReadExactly(Stream s, Span<byte> buffer)
    {
      int total = 0;
      while (total < buffer.Length) {
        int read = s.Read(buffer.Slice(total));
        if (read <= 0) return false;
        total += read;
      }
      return true;
    }

    private static bool SkipExactly(Stream s, long bytes)
    {
      if (bytes < 0) return false;
      Span<byte> junk = stackalloc byte[1024];
      long remaining = bytes;
      while (remaining > 0) {
        int take = (int)Math.Min(junk.Length, remaining);
        if (!ReadExactly(s, junk.Slice(0, take))) return false;
        remaining -= take;
      }
      return true;
    }

  }
}

解説

(省略)

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。
ライブラリを使用せずにwebp画像の幅と高さを取得する:画像2

[参照]ボタンをクリックし、webpファイルを選択します。
ライブラリを使用せずにwebp画像の幅と高さを取得する:画像3

上部のテキストボックスにwebpファイルのパスが表示されます。
ライブラリを使用せずにwebp画像の幅と高さを取得する:画像4

[Exec]ボタンをクリックします。下部のテキストボックスにwebpファイルの画像の幅と高さの値が表示されます。
ライブラリを使用せずにwebp画像の幅と高さを取得する:画像5

同様に[Exec Strict]ボタンをクリックしても、webpファイルの画像の幅と高さの値が表示されます。
ライブラリを使用せずにwebp画像の幅と高さを取得する:画像6

ライブラリを利用せずにwebpファイルの画像と高さを取得できました。

AuthorPortraitAlt
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
作成日: 2025-12-22