プログラム講座 中級編8
- PICT画像フォーマットをBMP画像フォーマットに変換する -
 中級編8です。この講座で最も長いプログラムリストだと思います。画像フォーマットの変換は簡単なようでいて複雑な部分も絡んできます。今回は画像フォーマットの中でも、非常に簡単なBMPを扱います。
◆BMPフォーマットとは?
 BMPフォーマットはご存じの通りWindowsマシンで使用されている標準画像形式です。Macにはご存じの通り共標準画像形式としてPICTがあります。BMP形式はPICT形式と違ってシンプルで簡単にローダーもセーバーも作ることが出来ます。今回はセーバー(画像を保存する)だけを作成します。さらに手抜きをするため、フルカラー形式でのみ保存するようになっています。
◆BMPフォーマット
 BMP形式はモノトーン、16色、256色、フルカラーの4種類の形式で保存する事が出来ます。座標系は一般のパソコンの座標とは異なり「数学座標系」です。つまり左下が(0,0)で右上に行くに従って増加していきます。これを忘れると画像が上下反転表示されてしまいます。
 BMP形式は16色と256色の時にのみ圧縮しフルカラーの場合は「圧縮しません」。圧縮しないためディスクスペースもメモリも膨大に消費します。圧縮方式もあまり圧縮されない方式ですのでアニメ調以外の画像では逆に圧縮すると増えてしまいます。圧縮形式には2種類あり16色圧縮用のRLE4、256色圧縮用のRLE8があります。
TGAのようにフッターのついた画像形式もありますがBMP形式にはフッターはありません。
 以下にBMP形式のヘッダーを示します。
| バイト数 | 名 前 | 概    要 | 
| 2 | bfType | ファイル形式(BMの2文字のアスキーコード) | 
| 4 | bfSize | ファイルサイズ | 
| 2 | bfReserved1 | 予備(ゼロ) | 
| 2 | bfReserved2 | 予備(ゼロ) | 
| 4 | bfOffBits | ファイル内のビットマップデータのオフセット | 
| 4 | biSize | ヘッダー容量(40バイト) | 
| 4 | biWidth | ビットマップの幅 | 
| 4 | biHeight | ビットマップの高さ | 
| 2 | biPlanes | デバイス面数(1) | 
| 2 | biBitCount | 1ピクセル当たりのビット数(1,4,8,24) | 
| 4 | biCompression | 圧縮形式(0=なし,1=RLE8,2=RLE4) | 
| 4 | biSizeImage | ビットマップの容量 | 
| 4 | biPelsPerMeter | 1メータ当たりの水平解像度 | 
| 4 | biPelsPerMeter | 1メータ当たりの垂直解像度 | 
| 4 | biClrUsed | 実際に使用するカラーインデックス数 | 
| 4 | biClrImportant | 通常ゼロ | 
| 1 | rgbRed | パレットデータ赤(0〜255) | 
| 1 | rgbGreen | パレットデータ緑(0〜255) | 
| 1 | rgbBlue | パレットデータ青(0〜255) | 
| 1 | rgbReserved | パレットデータ予備(通常ゼロ) | 
 パレットデータはパレットが不要なフルカラーの場合は存在しません。パレットはRGBxの4バイトで1組となっており、必要なパレット数だけ連続して格納されます。
 また1ラインのデータは奇数バイトで終わってはいけません。この場合、パディングといって0を付加します。
 画像データ本体は圧縮形式によって異なります。今回は説明は省きます。
◆手抜きをしてます・・・
 画像形式変換は毎回ファイルから1バイトづつ読み出して変換するのではなく一度オフスクリーンに描画させてしまいます。その後、形式に沿うように1バイト〜4バイトずつ書いていきます。
 手抜きをしているというのは、1ラインが奇数バイトで終わっていても全く考慮していないと言うことです。さすがにファイルの末尾はまずいとおもって付加してありますが。そこらへんを見ながら改良してみるとよいでしょう。フルカラー画像はBGR順に1バイト毎に保存していけばできあがります。
◆ビッグエンディアンとリトルエンディアン
 BMP形式に限らずインテルのCPUを使用したマシンの場合、2バイト以上のデータの扱いが68K, PPCなどと異なっています。これがビッグエンディアンとリトルエンディアンという言葉で表現されます。これはメモリ内でデータがどういう順番に格納されるかを表しています。例えば&H12345678という数値があるとすると以下のように格納されます。
 要するに格納順序が逆になっているという事です。画像の横幅やファイルサイズなどを保存する場合は、この格納順序を考慮しないといけません。今回は2バイト、4バイト用のリトルエンディアン書き込み関数を作成しました。リストは以下のようになってます。難しくないので、見ればわかると思います。
'--------------------------------------------------------
'   "リトルエンディアンで2バイト書き出す"
'--------------------------------------------------------
CLEAR LOCAL
LOCAL FN theWORD(adrs&,byte&)
  POKE adrs&, byte& AND &HFF
  POKE (adrs&+1),INT(byte& / 256)
END FN = adrs&+2
'--------------------------------------------------------
'   "リトルエンディアンで4バイト書き出す"
'--------------------------------------------------------
CLEAR LOCAL
LOCAL FN theLONG(adrs&,byte&)
  POKE adrs&, byte& AND &HFF
  POKE (adrs&+1),INT(byte& / 2^8)
  POKE (adrs&+2),INT(byte& / 2^16)
  POKE (adrs&+3),INT(byte& / 2^24)
END FN = adrs& + 4
◆終わりに
 画像ローダーとセーバーは結構面白いし結果が目に見えますのでいろいろ作成して見るとよいでしょう。次回はドラッグ&ドロップに挑戦してみましょう。その後、TIFFセーバーを作成する予定です。
◆今回のプログラムリスト
'----------------------------------------------------
' "PICT to BMP   ...1997 Program By KaZuhiro FuRuhata"
'----------------------------------------------------
RESOURCES "about.res":                            ' "リソースファイルを読み込む"
OUTPUT FILE "PICTtoBMP":                          ' "保存するアプリケーション名"
'--------------------- "定数"-------------------------
_fileMenu = 1:                                    ' "ファイルメニュー"
_editMenu = 2:                                    ' "エディット(編集)メニュー"
_effectMenu = 3:                                  ' "加工メニュー"
_fileOpen = 1:                                    ' "ファイルメニュー:開く"
_fileSave = 3:                                    ' "ファイルメニュー:保存"
_fileQuit = 5:                                    ' "ファイルメニュー:終了"
_ABOUT = 128:                                     ' "アバウト画面用アラートリソース番号"
_MEMORYERROR = 129:                               ' "メモリ不足アラート番号"
_BMPHeaderSize = 54:                              ' "BMP形式ヘッダーサイズ (14 + 40 Bytes)"
'----------------- "グローバル変数"-------------------
DIM cport&
gRowBytes% = 0:                                   ' "オフスクリーンのrowBytes"
gGRAM& = 0:                                       ' "オフスクリーンのアドレス"
gImageX% = 320:                                   ' "画像の横の長さ"
gImageY% = 240:                                   ' "画像の縦の長さ"
gOffScreen& = 0:                                  ' "0の時は確保されていない!"
gQuit_flag = _false:                              '"終了フラグ"
fileName$ = "":                                   ' "ファイル名"
END GLOBALS:                                      ' "グローバル変数定義の終了宣言"
'-----------------------------------------------
' "オフスクリーンのrowBytesを求める"
'-----------------------------------------------
CLEAR LOCAL
LOCAL FN getRowBytes
  PixMapH& = FN GETGWORLDPIXMAP(gOffScreen&):     ' "オフスクリーンの画像ハンドルを求める"
  err% = FN LOCKPIXELS(PixMapH&):                 ' "画像ハンドルをロック!"
  LONG IF err%
    gGRAM& = FN GETPIXBASEADDR(PixMapH&):         ' "画像が格納されている先頭のアドレスを求める"
    gRowBytes% = {[PixMapH&] + _rowBytes} AND &H3FFF:' "rowBytesを求める"
  END IF
END FN
' -----------------------------------------------
'  "オフスクリーンを確保する"
' gOffScreen& = "オフスクリーンのアドレス"
' -----------------------------------------------
CLEAR LOCAL
LOCAL FN setOffscreen
  DIM rect;8
  
  LONG IF gOffScreen& > 0
    CALL DISPOSEGWORLD(gOffScreen&):              ' "オフスクリーンを破棄"
    WINDOW CLOSE #1:                              ' "ウィンドウを閉じて、新しいウィンドウを開く"
    WINDOW #1,"Image",(16,45)-(16+gImageX%,45+gImageY%),_docNoGrow
  END IF
  CALL SETRECT(rect,0,0,gImageX,gImageY):         '"オフスクリーンを作成"
  err% = FN NEWGWORLD(gOffScreen&,32,rect,0,0,0):' "オフスクリーンを確保する"
  LONG IF err%
    err% = FN ALERT(_MEMORYERROR,0):              ' "多くの場合、メモリ不足"
  END IF
  CALL SETGWORLD(gOffScreen&,0):                  '"オフスクリーンに切り替える"
  COLOR _zWhite
  CALL PAINTRECT(rect)
  CALL SETGWORLD(cport&,0):                       '"ウィンドウに切り替える"
  FN getRowBytes:                                 ' "rowBytesを求める"
END FN
'-------------------------------------------------------------
' "PICTファイルをオープンしてオフスクリーンに描画する"
'-------------------------------------------------------------
CLEAR LOCAL
LOCAL FN openPictFile
  DIM rect;8
  
  fileName$ = FILES$(_fOpen,"PICT",,vRefNum%):    ' "ファイル選択ダイアログの表示"
  LONG IF fileName$<>""
    OPEN "I",#1, fileName$,,vRefNum%:             ' "PICTファイルオープン"
    fileSize& = LOF(1,1):                         ' "ファイルサイズを求める"
    pictHandle& = FN NEWHANDLE(fileSize&+4)
    LONG IF pictHandle&
      err = FN HLOCK(pictHandle&):                ' "PICTハンドルをロック!"
      LONG IF err = 0
        READ FILE#1, [pictHandle&], fileSize&:    ' "ファイルサイズ分だけファイルから読み込む"
        BLOCKMOVE [pictHandle&]+512,[pictHandle&],fileSize& - 512:' "先頭512バイトを消す"
        err = FN HUNLOCK(pictHandle&):            ' "ハンドルロック解除"
        err = FN SETHANDLESIZE(pictHandle&, fileSize&-512):' "メモリサイズを512減らす"
        err = FN HLOCK(pictHandle&):              ' "ハンドルをロック!"
        rect;8 = [pictHandle&]+_picFrame:         ' "PICTの画像の矩形を取り出す"
        gImageX% = rect.right:                    ' "右側の座標を取り出す"
        gImageY% = rect.bottom:                   ' "下側の座標を取り出す"
        '----------------------------------------------------
        FN setOffscreen:                          ' "オフスクリーンを確保する!"
        CALL SETGWORLD(gOffScreen&,0):            '"オフスクリーンに切り替える"
        CALL DRAWPICTURE(pictHandle&,rect)
        CALL SETGWORLD(cport&,0):                 '"ウィンドウに切り替える"
        '----------------------------------------------------
        err = FN HUNLOCK(pictHandle&)
      END IF
      err = FN DISPOSHANDLE(pictHandle&):         ' "PICTハンドルを破棄"
    XELSE 
      BEEP:                                       ' "ハンドルが確保できない〜エラーいこっちゃ"
    END IF
    CLOSE #1:                                     ' "ファイルを閉じる"
  END IF
END FN
'--------------------------------
' "オフスクリーンからウィンドウへ転送"
'--------------------------------
CLEAR LOCAL
LOCAL FN transfer
  DIM rect;8
  
  LONG IF gOffScreen& > 0
    CALL SETRECT(rect,0,0,gImageX%,gImageY%):     ' "転送サイズを設定"
    CALL COPYBITS(#gOffScreen&+2,#cport&+2,rect,rect,_srcCopy,0):' "オフスクリーンからウィンドウに転送!"
  END IF
END FN
'--------------------------------------------------------
'   "リトルエンディアンで2バイト書き出す"
'--------------------------------------------------------
CLEAR LOCAL
LOCAL FN theWORD(adrs&,byte&)
  POKE adrs&, byte& AND &HFF
  POKE (adrs&+1),INT(byte& / 256)
END FN = adrs&+2
'--------------------------------------------------------
'   "リトルエンディアンで4バイト書き出す"
'--------------------------------------------------------
CLEAR LOCAL
LOCAL FN theLONG(adrs&,byte&)
  POKE adrs&, byte& AND &HFF
  POKE (adrs&+1),INT(byte& / 2^8)
  POKE (adrs&+2),INT(byte& / 2^16)
  POKE (adrs&+3),INT(byte& / 2^24)
END FN = adrs& + 4
'===============================================
'   "BMPヘッダーを書き出す"
'===============================================
CLEAR LOCAL
LOCAL FN saveBMPheader
  '---------------------------------------------------------------------------------------
  bfType% = _"BM":                                ' "BMP認識コード"
  bfSize& = gImageY%*(gImageX%*3) + _BMPHeaderSize:' "ヘッダーサイズ(14+40Bytes)"
  '------------------- パディング処理 --------------------------
  pad% = bfSize& MOD 4
  IF pad% = 1 THEN bfSize& = bfSize& + 3
  IF pad% = 2 THEN bfSize& = bfSize& + 2
  IF pad% = 3 THEN bfSize& = bfSize& + 1
  
  bfReserved1% = 0:                               ' "予備1"
  bfReserved2% = 0:                               ' "予備2"
  bfOffBits& = _BMPHeaderSize:                    ' "BMP画像本体のアドレス"
  '---------------------------------------------------------------------------------------
  biSize& = 40:                                   ' "ヘッダーサイズ"
  biWidth& = gImageX%:                            ' "画像の横幅(数学座標)"
  biHeight& = gImageY%:                           ' "画像の縦幅(数学座標)"
  biPlanes% = 1:                                  ' "デバイスの面数(通常1)"
  biBitCount% = 24:                               ' "1ピクセルあたりのビット数(フルカラーなので24)"
  biCompression& = 0:                             ' "圧縮なし(RLE8 = 1, RLE4 = 2。フルカラーは圧縮形式はない)"
  biSizeImage& = 0:                               ' "ビットマップの容量"
  biXPelsPerMeter& = 2834:                        ' "1mあたりのピクセル数(水平解像度)"
  biYPelsPerMeter& = 2834:                        ' "1mあたりのピクセル数(垂直解像度)"
  biClrUsed& = 0:                                 ' "実際に使用するカラーインデックス数"
  biClrImportant& = 0:                            ' "通常ゼロ"
  '---------------------------------------------------------------------------------------
  
  BMPHeader& = FN NEWHANDLE(bfSize& + 256):       ' "メモリを確保(予備バイトも念のため確保)"
  LONG IF BMPHeader&
    err% = FN HLOCK(BMPHeader&):                  ' "ハンドルをロック!"
    LONG IF err% = 0
      address& = [ BMPHeader& ]:                  ' "ポインタ設定"
      
      POKE WORD address&,bfType%
      address& = address& + 2
      address& = FN theLONG(address&,bfSize&)
      address& = FN theWORD(address&,bfReserved1%)
      address& = FN theWORD(address&,bfReserved2%)
      address& = FN theLONG(address&,bfOffBits&)
      '---------------------------------------------
      address& = FN theLONG(address&,biSize&)
      address& = FN theLONG(address&,biWidth&)
      address& = FN theLONG(address&,biHeight&)
      address& = FN theWORD(address&,biPlanes%)
      address& = FN theWORD(address&,biBitCount%)
      address& = FN theLONG(address&,biCompression&)
      address& = FN theLONG(address&,biSizeImage&)
      address& = FN theLONG(address&,biXPelsPerMeter&)
      address& = FN theLONG(address&,biYPelsPerMeter&)
      address& = FN theLONG(address&,biClrUsed&)
      address& = FN theLONG(address&,biClrImportant&)
      '---------------------------------------------
      WRITE FILE #1, [ BMPHeader& ], _BMPHeaderSize:'"まとめて一気に書き込む"
    END IF
    err% = FN DISPOSHANDLE(BMPHeader&):           ' "ハンドルを破棄"
  END IF
END FN
'===============================================
'   "BMP形式で保存"
'===============================================
CLEAR LOCAL
LOCAL FN saveBMP
  fname$ = fileName$ + ".BMP":                    ' "保存するときに自動的に拡張子.BMP)を付加する"
  saveFile$ = FILES$(_fSave,"保存ファイル名:",fname$,volRefNum%)
  LONG IF LEN(saveFile$):                         '"ファイル名の長さが1以上、つまりファイル名が入力された場合"
    DEF OPEN "BMP 8BIM":                          ' "BMP形式。フォトショップで開けるようにしておきます"
    OPEN "O",#1,saveFile$,,volRefNum%:            '"保存するファイル名で新規に開く"
    FN saveBMPheader:                             ' "ヘッダー書き込み"
    
    saveMemory& = gImageX%*3 + 32:                ' "メモリを確保(予備バイトも念のため確保)"
    saveData& = FN NEWHANDLE(saveMemory&)
    LONG IF saveData&
      err% = FN HLOCK(saveData&):                 ' "ハンドルをロック!"
      
      FOR y = gImageY%-1 TO 0 STEP -1:            ' "BMP形式は数学座標なので下から保存しなければならない"
        srcAddress& = gGRAM& + y*gRowBytes%:      ' "ピクセルの左端を求める"
        saveAddress& = [ saveData& ]:             ' "ポインタ設定:1ライン毎にファイルに書き出す"
        FOR x = 0 TO gImageX%-1
          R% = PEEK(srcAddress&+1):               ' "32ビットオフスクリーンはaRGB順に並んでいる。"
          G% = PEEK(srcAddress&+2)
          B% = PEEK(srcAddress&+3)
          srcAddress& = srcAddress& + 4:          '"32ビットオフスクリーンなので1pixel=4byte。そのため4を足している"
          
          POKE saveAddress&, B%:                  ' "BMPフォーマットはBGR順に並んでいる"
          POKE saveAddress&+1, G%
          POKE saveAddress&+2, R%
          saveAddress& = saveAddress& + 3:        ' "BMPフォーマットで24ビットの場合、1ピクセルで3バイト必要"
        NEXT
        WRITE FILE #1, [ saveData& ], gImageX%*3:'"まとめて一気に書き込む"
      NEXT
      saveAddress& = [ saveData& ]:               ' "手抜きパディング"
      POKE LONG saveAddress&,0
      bfSize& = gImageY%*(gImageX%*3) + _BMPHeaderSize:' "ヘッダーサイズ(14+40Bytes)"
      '------------------- パディング処理 --------------------------
      pad% = 4 - (bfSize& MOD 4)
      WRITE FILE #1, [ saveData& ], pad%:         '"パディング処理"
    END IF
    err% = FN DISPOSHANDLE(saveData&):            ' "ハンドルを破棄する"
  END IF
  BEEP:                                           ' "変換が終了したことをビープ音で知らせる!"
END FN
'--------------------------------------------------------
' "アップデートなどのイベントを取得する"
'--------------------------------------------------------
CLEAR LOCAL
LOCAL FN doDialog
  evnt = DIALOG(0)
  id = DIALOG(evnt):                              '"発生したイベントの種類"
  SELECT evnt
    CASE _wndRefresh:                             '"ウィンドウリフレッシュ(アップデートイベント)"
      FN transfer:                                '"アップデートイベントなので画面を再描画する"
  END SELECT
END FN
'--------------------------------------------------------
' "アバウト画面の表示"
'--------------------------------------------------------
CLEAR LOCAL
LOCAL FN about
  err = FN ALERT(_ABOUT,0)
END FN
'--------------------------------------------------------
' "メニューを構築する"
'--------------------------------------------------------
CLEAR LOCAL
LOCAL FN initMenu
  APPLE MENU "PICT to BMPについて..."
  
  '"ファイルメニュー"
  MENU _fileMenu,0,_enable,"ファイル"
  MENU _fileMenu,_fileOpen,_enable,"/O開く..."
  MENU _fileMenu,2,_enable,";"
  MENU _fileMenu,_fileSave,_enable,"/SBMP形式で保存..."
  MENU _fileMenu,4,_enable,";"
  MENU _fileMenu,_fileQuit,_enable,"/Q終 了"
  
  ' "クリップボード等のコピー&ペーストを行う場合は EDIT MENU 2 とします。
  MENU _editMenu,0,_disable,"編集"
  MENU _editMenu,1,_disable,"取り消し"
  MENU _editMenu,2,_disable,";"
  MENU _editMenu,3,_disable,"/Xカット"
  MENU _editMenu,4,_disable,"/Cコピー"
  MENU _editMenu,5,_disable,"/Vペースト"
  MENU _editMenu,6,_disable,"消去"
  MENU _editMenu,7,_disable,";"
  MENU _editMenu,8,_disable,"/A全てを選択"
  
END FN
'---------------------------------------------
' "メニューの選択"
'---------------------------------------------
CLEAR LOCAL
LOCAL FN doMenus
  menuID = MENU(_menuID):                         '"選択されたメニューバー項目の番号"
  itemID = MENU(_itemID):                         '"プルダウンメニューで選択された項目番号"
  
  SELECT menuID
    CASE _appleMenu:                              ' "アバウト画面の表示(_appleMenuはあらかじめ定義されています)"
      FN about
    CASE _fileMenu :                              ' "ファイルメニュー"
      SELECT itemID
        CASE _fileOpen:                           ' "画像を読み込む(開く)"
          FN openPictFile
        CASE _fileSave:                           ' "画像の保存"
          FN saveBMP
        CASE _fileQuit:                           ' "終了が選択された"
          gQuit_flag = _true
      END SELECT
  END SELECT
  MENU:                                           ' "これがないとメニューバーの項目が強調表示されたままになってしまいます"
END FN
'------------------------- Main Routine ----------------------------------
WINDOW OFF
WINDOW #1,"Image",(0,0)-(gImageX, gImageY),_docNoGrow
CALL GETPORT(cport&):                             ' "ウィンドウのグラフポートを確保しておきます"
ON MENU FN doMenus:                               '"メニューが選択された時の飛び先"
ON DIALOG FN doDialog:                            '"ダイアログイベントが発生した時の飛び先"
FN initMenu:                                      '"メニューの初期化"
FN setOffscreen:                                  '"オフスクリーンの確保"
FN openPictFile
FN transfer:                                      '"オフスクリーンからウィンドウへ画像を転送"
DO
  HANDLEEVENTS:                                   ' "イベント処理は自動"
UNTIL gQuit_flag
CALL DISPOSEGWORLD(gOffScreen&):                  ' "オフスクリーンの破棄"
END