めもてう

忘れっぽいTIPs、HowToのメモ帳です。

【WPF】読み込めないJpegファイルを読み込む

WPF(.net 4.6.2)で画像ビューワーを作っていて、開こうとすると ArgumentException をスローして開けない(読み込めない)Jpegファイルに遭遇しました。
件のJpegファイルを他のアプリで開くと問題なく表示できたので、ファイルが壊れているのではなく、どうもWPF側の問題、仕様のようです。

ただ、究極的な話をすると自前でJpegファイルを読み込んで、デコードしてやれば表示できないということはないはずなので、ファイルが壊れていない限り表示は出来るはずです。

目標

読み込めている画像に加えて、読み込めなかったJpegファイルを読み込めるようにする。

前置き

Jpegファイルはセグメントと呼ばれる複数の情報ブロックで構成されています。
その中で Applicationセグメントという種類のブロックがあるんですが、このセグメントは互換性を維持するための情報や、Jpegファイルを保存したアプリケーションやカメラの情報を保持しています。
いってしまえばこの Applicationセグメントは画像データを補完するメタデータです。

Jpegファイルは壊れていないものとして、画像に直接関係しているセグメントを除外していくと、この Applicationセグメントがあやしいです。
Applicationセグメントは画像データを補完すると書きましたが、必須ではないので、読み込めない Jpegファイルから Applicationセグメントブロックを全て省いたデータを用意できれば目標が達成できそうです。

ちなみに Applicationセグメントは複数種類(APP0 ~ APP15)存在します。
一時期、撮影場所がバレると SNS などで話題になった Exif もこの Applicationセグメント(APP1)です。

仕様

  1. 整形されたバイナリデータから BitmapImage を作成し、Image コントロールで表示する。
  2. ファイルパス(string)を受け取って Applicationセグメントを削除したバイナリデータ(byte[ ])を返すメソッドを作成。
    1. 受け取ったファイルパスが null 、空文字列、ファイルが存在しない場合は例外をスローする。
    2. 途中で終端コードに到達した場合は例外をスローする。
    3. 受け取ったファイルパスが Jpegファイルへのパスではなかった場合は例外ではなく null を返す。

実装

プロジェクト名は「ImageViewer」として、

<!-- MainWindow.xaml -->
<!-- xmlnsとかは省略 -->
<Window>
  <Grid AllowDrop="True"
        DragOver="Grid_DragOver"
        Drop="Grid_Drop">
    <ScrollViewer VerticalScrollBarVisibility="Auto"
                  HorizontalScrollBarVisibility="Auto">
      <Image x:Name="ImageView"/>
    </ScrollViewer>
  </Grid>
</Window>
// MainWindow.xaml.cs
// usingは省略
namespace ImageViewer {
  public partial class MainWindow : Window {

    // コンストラクタ
    public MainWindow() {
      InitializeComponent();
    }

    // ドラッグオーバーリスナー
    private void Grid_DragOver(object sender, DragEventArgs e) {
      if(e.Data.GetDataPresent(DataFormats.FileDrop)) {
        e.Effects = DragDropEffects.Copy;
      }
      else {
        e.Effects = DragDropEffects.None;
      }
    }

    // ドロップリスナー
    private void Grid_Drop(object sender, DragEventArgs e) {
      if(e.Data.GetData(DataFormats.FileDrop) is string[] files) {
        BitmapImage bmp = CreateBitmap(files[0]);

        // 読み込んだビットマップをイメージコントロールへ設定
        if(bmp != null) {
          ImageView.Source = bmp;
          ImageView.Width = bmp.PixelWidth;
          ImageView.Height = bmp.PixelHeight;
        }

      }
    }

    // ビットマップイメージ作成
    private BitmapImage CreateBitmap(string path) {
      BitmapImage result = null;

      // 処理を実装

      return result;
    }

    // Jpegファイルの読込みと Applicationセグメントの除外
    private byte[] LoadSubAppsegmentJpegFile(string path) {
      byte[] result = null;

      // 処理を実装

      return result;
    }
  }
}

ファイルをドロップしたら表示するだけのシンプルなアプリです。

CreateBitmapを実装

// ビットマップイメージ作成
private BitmapImage CreateBitmap(string path) {
  BitmapImage result = null;

  try {
    byte[] buff = LoadSubAppsegmentJpegFile(path);
    if(buff == null) {
        buff = File.ReadAllBytes(path);
    }

    using (var Stream = new MemoryStream(buff)) {
      result = new BitmapImage();
      result.BeginInit();
      result.StreamSource = Stream;
      result.CreateOptions = BitmapCreateOptions.None;
      result.CacheOption = BitmapCacheOption.OnLoad;
      result.EndInit();

      if (result.CanFreeze) {
        result.Freeze();
      }
    }

  }
  catch {
    throw;
  }

  return result;
}

LoadSubAppsegmentJpegFileメソッド からトリミングされたバッファを受け取り、それを元に MemoryStream を作成し BitmapImage へ渡しています。
LoadSubAppsegmentJpegFileメソッド が null を返す場合は、渡したファイルパスが Jpegファイルではない場合なので File.ReadAllBytesメソッド で全てを読み込み BitmapImage へ渡します。
この段階で元々読み込めていたJpegファイルを含む画像ファイルをドロップしてやると表示することができます。

一点注意事項として、直接 MemoryStream を BitmapImage へ渡すのはメモリコストの面で非効率です。
大きなサイズのファイルを大量に読み込む場合などはクリティカルな問題ですが、今回はファイル一つなので直接渡す実装にしています。

LoadSubAppsegmentJpegFileを実装

// Jpegファイルの読込みと Applicationセグメントの除外
private byte[] LoadSubAppsegmentJpegFile(string path) {

  if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path is Null or Empty"); }
  if (!File.Exists(path)) { throw new FileNotFoundException(path + " Not Found"); }

  long resultSize = 0;
  byte[] result = null;

  // Key   : Applicationセグメントのポジション
  // Value : Applicationセグメントのサイズ
  Dictionary<long, long> segmentList = null;

  try {
    using (Stream stream = File.OpenRead(path)) {
      // Appセグメントの位置とサイズを取得
      segmentList = MakeAppsegmentDictionary(stream);
      if (segmentList != null) {

        // Appセグメントを除くサイズを計測
        resultSize = stream.Length;
        foreach (var segment in segmentList) {
          resultSize -= segment.Value;
        }

        result = new byte[resultSize];

        stream.Seek(0, SeekOrigin.Begin);

        long current = 0;
        int dat = 0;

        while (true) {
          // Appセグメント位置に来たらサイズ分カーソルを進める
          if (segmentList.ContainsKey(stream.Position)) {
            stream.Seek(segmentList[stream.Position], SeekOrigin.Current);
          }
          else {
            // 1バイトづつ読み込む
            dat = stream.ReadByte();

            // 終端に到達したら処理を終了
            if (dat == -1) {
              break;
            }

            result[current] = (byte)dat;
            current++;

          }
        }

      }
    }
  } catch { throw; }


  return result;
}

// ストリームを走査して全てのApplicationセグメントの位置とサイズを返す
private Dictionary<long, long> MakeAppsegmentDictionary(Stream stream) {

  long current = stream.Position;
  stream.Seek(0, SeekOrigin.Begin);

  const int MarkerExt = 0xFF;
  const int MarkerNon = 0x00;
  const int MarkerRestart0 = 0xD0;
  const int MarkerRestart7 = 0xD7;

  const int SegmentStartOfImage = 0xFFD8;
  const int SegmentEndOfImage = 0xFFD9;
  const int SegmentStartOfScan = 0xFFDA;
  const int SegmentApp0 = 0xFFE0;
  const int SegmentApp15 = 0xFFEF;

  int mark, size;
  bool isImageData = false;

  mark = Read2ByteData(stream);

  Dictionary<long, long> result = null;

  // ストリームの先頭2バイトがJpeg識別子である
  if (mark == SegmentStartOfImage) {

    result = new Dictionary<long, long>();

    while (true) {
      if (isImageData) {
        int h = 0;
        int l = 0;

        while (true) {
          h = stream.ReadByte();

          if (h == -1) {
            throw new EndOfStreamException("ファイルが壊れている可能性があります。");
          }
          else if (h == MarkerExt) {
            l = stream.ReadByte();

            if (l == -1) {
              throw new EndOfStreamException("ファイルが壊れている可能性があります。");
            }
            else if (l != MarkerNon && !(l >= MarkerRestart0 && l <= MarkerRestart7)) {
              break;
            }
            else if (l == MarkerExt) {
              stream.Seek(-1, SeekOrigin.Current);
            }
          }
        }

        stream.Seek(-2, SeekOrigin.Current);

        isImageData = false;
      }
      else {
        mark = Read2ByteData(stream);

        if (mark == SegmentEndOfImage) {
          break;
        }

        size = Read2ByteData(stream);

        if (mark >= SegmentApp0 && mark <= SegmentApp15) {
          result.Add(stream.Position - 4, size + 2);
        }
        else if (mark == SegmentStartOfScan) {
          isImageData = true;

        }
        stream.Seek(size - 2, SeekOrigin.Current);

      }
    }
  }

  stream.Seek(current, SeekOrigin.Begin);

  return result;

}

// ストリームから2バイト読み込む
private int Read2ByteData(Stream stream) {
  int h = stream.ReadByte();
  int l = stream.ReadByte();

  if(h == -1 || l == -1) {
    throw new EndOfStreamException("ファイルが壊れている可能性があります。");
  }

  return (h << 8 | l);
}

今回のキモとなるメソッドです。
LoadSubAppsegmentJpegFileメソッドの目的は Applicationセグメントの除外ですので、ファイルストリームを走査して Applicationセグメントの位置とサイズを知る必要があります。
そこで、以下の二つのメソッドを追加します。

  • MakeAppsegmentDictionaryメソッド
    • 渡されたストリームを先頭から走査し、Dictionary<long, long> へ位置(Key)とサイズ(Value)を格納して返します。
    • このメソッドが null を返す場合はストリームが Jpegファイルでは無い場合です。
  • Read2ByteDataメソッド
    • このメソッドはセグメントマーカー、セグメントサイズを取得するためのユーティリティメソッドです。
    • 渡されたストリームのカーソル位置から2バイト読込み結合して返します。

LoadSubAppsegmentJpegFileメソッドではMakeAppsegmentDictionaryメソッドから返ってきた辞書を元に最終的なバッファーのサイズを計算して、辞書と照らし合わせながらストリームのデータをコピーしていく作業を行います。

以上のコードで目標は達成できました。

まとめ

このソースコードは厳密にデコードしているわけではないのでもしかしたら、別の不具合(色味がおかしいなど)を起こしてしまうかもしれません。ですが、Jpegファイルは第三者が拡張できるフォーマットになっているため全てを追うのはちょっと現実的ではありません。なので、不具合が起きたらその都度、対処していくしかないのかなぁと思います。

あと、例外を上へ上へと投げて結局処理していないので Grid_Dropメソッドでいい感じに処理してください。

ここで記載されているソースコードを使用する場合は自己責任でお願いします。

プライバシーポリシー (Privacy policy)