webp画像の幅と高さ情報を取得したいです。なるべくライブラリは使わずに取得したいのですが、どう実装すればよいでしょうか?
本記事のコードは複数のLLMでの検討により作成されたコードです。
ImageSharpなどのライブラリを利用せずにwebp画像の幅と高さを取得するコードを紹介します。
Windowsフォームアプリケーションを作成します。
以下のフォームを準備します。ボタンを3つテキストボックスを2つ、OpenFileDialogを配置します。
以下のコードを作成します。
フォームにはシンプルな読み取りロジックである、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 クラスです。
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ファイルを選択します。
上部のテキストボックスにwebpファイルのパスが表示されます。
[Exec]ボタンをクリックします。下部のテキストボックスにwebpファイルの画像の幅と高さの値が表示されます。
同様に[Exec Strict]ボタンをクリックしても、webpファイルの画像の幅と高さの値が表示されます。
ライブラリを利用せずにwebpファイルの画像と高さを取得できました。