2023/11/14

DXL Step-by-Step:#13)イメージの形式とサイズの取得

DXL 活用の調査・検証で、実現できたことや発見したことご紹介する『DXL Step-by-Step』シリーズの第 13 回です。

これまで数回にわたりイメージリソースを題材に画像データの取り扱いをまとめてきました。DXL で画像を操作していると、画像データのファイル形式を判定する必要が出てくることがあります。ファイルの拡張子で判定するのが簡単ですが、必ずしも正確にフォーマットを表しているとは限らないですよね。

そこで、ファイルの中身(バイナリーデータ)から情報を取得する方法についてまとめます。今回は DXL で画像を処理する上で必要となる情報のファイルフォーマットと画像サイズの取得だけに絞って記載いたします。

各フォーマットの画像ファイルをバイナリエディタで参照した画像を添付しています。画像内の黄色アンダーラインがフォーマットの判定、赤色が画像サイズを取得している箇所となります。


PNG

◇ フォーマットの判定

PNG は、ファイルの先頭 8 バイトが固定されています。特に 2 ~ 4 バイト目には Ascii コードの "PNG" という文字がセットされています。


◇ 画像サイズ

幅が 17 ~ 20 バイト目、高さが 21 ~ 24 バイト目となっています。これを 10 進数変換すれば取得できます。4 バイト分も確保されているので、ずいぶん大きな画像にも対応しているのですね。


GIF

◇ フォーマットの判定

GIF は、ファイルの先頭 3 バイトが Ascii コードの "GIF" という文字がセットされています。


◇ 画像サイズ

幅が 6 ~ 7 バイト目、高さが 8 ~ 9 バイト目となっています。GIF の画像サイズは、下位バイト、上位バイトの順となっていますので、サイズ(10進数)に変換時する際は注意が必要です。


JPEG

◇ フォーマットの判定

JPEG ファイルは、ファイルの中身をマーカと呼ばれる特定の値で管理する仕様です。JPEG ファイルのスタートを表すマーカは、FF D8 です。ファイルの最初の 2 バイトがこの値となっていれば JPEG と判定できます。


◇ 画像ファイル

JPEG ファイルの中身は、マーカーで区切られたセグメントに分割され管理されています。セグメントには、画像圧縮条件などのパラメータ、画像の大きさなどの基本情報、画像データの本体などそれぞれ役割が定義されています。

マーカーは 2 バイトで構成されており、1 バイト目が FF、2 バイト目でその役割が判定できます(水色)。セグメントのマーカーでは、その次の 2 バイトでセグメントのサイズを表しています(紫)。これを利用すると次のマーカーが簡単に取得できます。

今回必要となる画像サイズを持つマーカーは FF C0 となります。そのマーカーまでたどり着くには、次のような流れとなります。

  1. ファイル形式を判定した次の 2 バイトを取得します
  2. FF E0 なので目的のマーカーではない
  3. 次の 2 バイトからセグメントのサイズを取得して、次のマーカーまでファイルを空読みする
  4. 次のマーカーが目的のマーカー FF C0 であるか判定し、違う場合は 3 に戻る
  5. 画像サイズを取得する
画像サイズは、マーカーの開始位置 FF を 0 とすると 5 ~ 6 バイト目が高さ、7 ~ 8 バイト目が幅となっています。PNG や GIF とは高さと幅の並びが逆なので注意が必要です。

なお、実際にはこのマーカーは C0 ~ C3 と幅があるようです。


サンプルプログラム

上記の仕様に従い画像フォーマットの判定とサイズを取得する関数を作成してみました。

まずは、メインプログラムです。画像ファイルをストリームでオープンし、そのファイル形式と画像サイズを取得して、メッセージボックスで表示しています。

なお、画像形式は定数として定義しています。

Option Declare

Public Const DXL_Image_Unknown = 0
Public Const DXL_Image_JPEG = 1
Public Const DXL_Image_GIF = 2
Public Const DXL_Image_PNG = 3

Sub Initialize
   Dim ns As New NotesSession
   Dim nst As NotesStream
   Dim s As String
   Dim iType As Integer
   Dim iSizeX As Integer
   Dim iSizeY As Integer

   Set nst = ns.CreateStream()
   Call nst.Open("D:\TitleS.png")

   iType = xGetImageFileInfo(nst, iSizeX, iSizeY)
   If iType > 0 Then
      If iType = DXL_Image_PNG Then s = "PNG"
      If iType = DXL_Image_GIF Then s = "GIF"
      If iType = DXL_Image_JPEG Then s = "JPEG"

      s = "画像形式 = " & s
      s = s & Chr(10) & "幅 = " & CStr(iSizeX)
      s = s & ", 高さ = " & CStr(iSizeY)

      MsgBox s, 64
   Else
      MsgBox "画像形式を判定できませんでした。", 48
   End If
End Sub


関数のネスト順とは逆転するのですが、まず、画像形式を判定する関数から紹介します。

最初に整理した通り、画像形式の判定であればファイルの先頭から 4 バイトあれば判定できます。その部分だけをファイルから読み取り判定に使用しています。

Function xGetImageFileType(vnstImage As NotesStream) As Integer
   Dim vTmp As Variant
   Dim iT As Integer

   iT = DXL_Image_Unknown

   '先頭 4 バイト取得
   vnstImage.Position = 0
   vTmp = vnstImage.Read(4)

   If vTmp(0) = &h47 And vTmp(1) = &h49 Then
      'GIF
      iT = DXL_Image_GIF
   ElseIf vTmp(0) = &hFF And vTmp(1) = &HD8 Then
      'jpeg
      iT = DXL_Image_JPEG
   ElseIf vTmp(1) = &h50 And vTmp(2) = &h4E And vTmp(3) = &h47 Then
      'PNG
      iT = DXL_Image_PNG
   End If

   xGetImageFileType = iT
End Function


次の関数がメインルーチンからコールされている画像形式と画像サイズを取得している関数です。引数は、画像ファイルをオープンしたストリームとサイズ情報です。サイズ情報の riSizeX と riSizeY は値を返すための引数です。また、戻り値は画像の形式を返します。

まず、上記 xGetImageFileType をコールして画像形式を取得して、形式毎に画像サイズを取得しています。セグメントを捜索する処理のある JPEG が少し複雑になっていますが、上記画像フォーマットの説明の通りのコードとなっています。

Function xGetImageFileInfo(vnstImage As NotesStream, riSizeX As Integer, riSizeY As Integer) As Integer
   Dim vTmp As Variant
   Dim iT As Integer
   Dim iX As Integer
   Dim iY As Integer

   On Error GoTo ErrProc

   '画像形式取得
   iT = xGetImageFileType(vnstImage)
   If iT = 0 Then GoTo ExitProc

   '画像サイズ取得
   vnstImage.Position = 0
   If iT = DXL_Image_PNG Then
      vTmp = vnstImage.Read(16)'空読み

      '大きな画像はない前提に下2バイトのみ使用
      '幅

      vTmp = vnstImage.Read(4)
      iX = CInt(vTmp(2)) * 256 + CInt(vTmp(3))
      '高さ
      vTmp = vnstImage.Read(4)
      iY = CInt(vTmp(2)) * 256 + CInt(vTmp(3))
   ElseIf iT = DXL_Image_GIF Then
      vTmp = vnstImage.Read(6) '空読み
      vTmp = vnstImage.Read(4)
      '幅
      iX = CInt(vTmp(1)) * 256 + CInt(vTmp(0))
      '高さ
      iY = CInt(vTmp(3)) * 256 + CInt(vTmp(2))
   ElseIf iT = DXL_Image_JPEG Then
      vTmp = vnstImage.Read(2) 'フォーマットのマーカ空読み

SearchMarker:
      'マーカー確認
      vTmp = vnstImage.Read(2)
      If vTmp(0) = &HFF And vTmp(1) >= &HC0 And vTmp(1) <= &HC3 Then
         'マーカー発見
         vTmp = vnstImage.Read(8)
         iX = CInt(vTmp(5)) * 256 + CInt(vTmp(6))
         iY = CInt(vTmp(3)) * 256 + CInt(vTmp(4))
      Else
         'セグメントのサイズを取得し空読みして次のマーカを確認
         vTmp = vnstImage.Read(2)
         vnstImage.Position = vnstImage.Position + vTmp(0) * 256 + vTmp(1) - 2
         GoTo SearchMarker
      End If
   End If

ExitProc:
   riSizeX = iX
   riSizeY = iY
   xGetImageFileInfo = iT

   Exit Function
ErrProc:
   iT = DXL_Image_Unknown
   iX = 0
   iY = 0

   Resume ExitProc
End Function


どちらの関数もなのですが、ファイルを確実に先頭から読み込むためにストリームの Position を 0 (=先頭)に設定しています。このように、読み込み位置を自在に扱えるのでストリームは便利ですね。


まとめ

今回は、画像形式と画像サイズを取得する方法をまとめました。

ローコードとかノーコードとかが話題となっている昨今において、まさかバイナリーファイルをバイト単位で読み込むような処理を作ることになるとは思っていませんでした。今どきの開発者の方にはなじまないのかもしれないですね。

私は?というと、バイナリエディタでファイルを開くなんて久しぶりの経験でした。35 年ほど前、某ファ○コンのスーパー○リオのジャンプ力や走力を魔改造してた頃を思い出し、懐かしさを感じながら調査していました(笑)

前回 DXL Step-by-Step 次回

0 件のコメント:

コメントを投稿