fortran66のブログ

fortran について書きます。

【メモ帳】Windows Intel Fortran で GDI+ による画像ファイル(jpg/png/gif/etc)読み書き

Windows GDI+ API 呼び出しによる画像ファイル読み書き

Win32/64API の GDI では BMP ファイルの読み書きしかできませんでしたが、その後に出た GDI+ で JPG/PNG/GIF/TIFF 等が読み書きできるようになりました。しかしながら C++ のクラス利用などが前提となって GDI の時の様に素朴に C ルーチン呼び出しのインターフェースを書くだけでは利用できず、私にはお手上げでした。

その後、昔から Fortran での画像処理を手掛けていた Fortran Plaza さんが、gfortran から GDI+ を利用できることを示してソースコードも公開されました。その内容はライブラリ全体を包括するような本格的なもので、大規模で少し難しく、そのうち勉強しようと思いつつほったらかしていました。

fortran66.hatenablog.com

最近、AI に 「GDI+ での jpg 読み書き Fortran プログラム書けるか?」と聞いてみたところ、「お安い御用さ!」と言いつつ、ホイホイプログラムを吐き出し、画像サイズを出すところまではわずかな修正で行けました。そこから先は嘘だらけになって若干苦労しましたが、必要最低限の API インターフェースの用意で、JPG/PNG/GIF/TIFF 等の画像ファイルを RGB 形式の配列として読み込み、適当な処理をした後、JPG/PNG/GIF/TIFF 等の好みの形式で書き出せるようになりました。

かったるい C との interface 書きなどを AI がやってくれるので、AI 様様と言ったところです。ただ AI 様は、質問者の私がよく分からないことを、分からない人なりの要領を得ない問で聞くと、嘘と真を混ぜて答えてきて閉口させられます。こちらも真偽の判断がつかないので、すぐ騙されます。この辺は、次の世代の AI を待ちたいと思います。

AI 利用は 八合目あたりまで AI 様のケーブルカーで登って、そこから頂上までは自力登山という感じでしょうか。最近の腐りきった Google 検索などではなかなか欲しい情報にたどり着けないので 、AI 様なしではこのプログラムは書けなかったと思います。なおもっぱら Claude 無料枠に書いてもらいました。ChatGPT をクロスチェックに利用しました。

実行例

jpg ファイルを読み込んで、色を反転させて別名 jpg ファイルに保存しています。

 GDI+ initialized successfully
 Image loaded successfully
           0 width         1280
           0 height         720
 Image saved successfully
 Bitmap disposed successfully
 GDI+ shut down successfully
 GDI+ library unloaded successfully
続行するには何かキーを押してください . . .

入力ファイル

出力ファイル


www.youtube.com

ソース・プログラム

intel fortran ifx 2024.2.0 で実行しました。stdcall などの呼び出し規約の関係で Win64 でなければ動きません。

ファイルの読み込みの方は、拡張子で自動判別して読み込めますが、書き込みの方は jpg 決め打ちになっています。CLSID を変えることで書き出しファイル形式を変えられます。(CLSID は ID 数列を 32bit little endian, 16 bit little endian, 16 bit little endian, 8bit/4bit の並びで解釈します。AIに騙されましたw)ファイル名は末尾に C 言語式の NULL 番兵が必要です。また ASCII 文字列では駄目で、16bit の wide 文字列に変換する必要があります。(AIに騙されましたw)

プログラムの概要:

  1. gdiplus.dll をダイナミックロード。
  2. CreateBitmapFromFile によって 32bit BMP 形式(ARGB 形式)で読み込む。
  3. RGB の配列をいじる。
  4. GdipSaveImageToFile でファイルに書き出す。
module LoadDll_m
    use, intrinsic :: iso_c_binding
    implicit none
    private
    public :: LoadLibrary, GetProcAddress, FreeLibrary
    ! Windows API functions ; load DLL
    interface
        function LoadLibrary(lpFileName) bind(C, name='LoadLibraryA')
            import
            integer(C_INTPTR_T) :: LoadLibrary
            character(kind=C_CHAR), intent(in) :: lpFileName(*)
        end function LoadLibrary

        function GetProcAddress(hModule, lpProcName) bind(C, name='GetProcAddress')
            import
            type(C_FUNPTR) :: GetProcAddress
            integer(C_INTPTR_T), value :: hModule
            character(kind=C_CHAR), intent(in) :: lpProcName(*)
        end function GetProcAddress

        function FreeLibrary(hModule) bind(C, name='FreeLibrary')
            import
            integer(C_INT) :: FreeLibrary
            integer(C_INTPTR_T), value :: hModule
        end function FreeLibrary
    end interface    
end module LoadDll_m
    
module WideString_m
    use, intrinsic :: iso_c_binding
    implicit none
    ! Windows API constants for multibyte-to-wide
    integer(C_INT), parameter :: CP_ACP = 0
    integer(C_INT), parameter :: MB_ERR_INVALID_CHARS = z'00000008'
    ! Interface for MultiByteToWideChar
    interface
        function MultiByteToWideChar(CodePage, dwFlags, lpMultiByteStr, cbMultiByte, &
                                     lpWideCharStr, cchWideChar) bind(C, name='MultiByteToWideChar')
            import
            integer(C_INT), value :: CodePage, dwFlags
            type(C_PTR), value :: lpMultiByteStr
            integer(C_INT), value :: cbMultiByte
            type(C_PTR), value :: lpWideCharStr
            integer(C_INT), value :: cchWideChar
            integer(C_INT) :: MultiByteToWideChar
        end function MultiByteToWideChar
    end interface

    contains
    
    function to_wide_string(text) result(res)
        character(len = *), intent(in), target :: text
        character(kind=C_CHAR, len=:), allocatable, target :: res
        integer(C_INT) :: len_text, iret
        character(kind=C_CHAR, len=:), pointer :: wide_string
         
        len_text = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, &
                                       c_loc(text), len_trim(text), &
                                       C_NULL_PTR, 0)
        if (len_text == 0) then
            print *, "to_wide_string: Error in getting length." !Error code:", GetLastError()
            stop
        end if

        ! Allocate the wide string return heap
        allocate(character(kind=C_CHAR, len=len_text*2) :: res)

        ! Convert the string
        iret = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, &
                                   c_loc(text), len_trim(text), &
                                   c_loc(res), len_text)
        if (iret == 0) then
            print *, "to_wide_string: Error in conversion." ! Error code:", GetLastError()
            stop
        end if
    end function to_wide_string
end module WideString_m
    
module GdiPlus_m
    use, intrinsic :: iso_c_binding
    use :: LoadDll_m
    use :: WideString_m
    implicit none
    ! Bitmap Constants 
    integer(c_int), parameter :: PixelFormat32bppARGB = 2498570
    integer(c_int), parameter :: ImageLockModeRead = 1, ImageLockModeWrite = 2    

    ! GDI+ data types
    type, bind(C) :: GdiplusStartupInput_t
        integer(C_LONG) :: GdiplusVersion
        type(C_FUNPTR) :: DebugEventCallback
        integer(C_INT) :: SuppressBackgroundThread
        integer(C_INT) :: SuppressExternalCodecs
    end type GdiplusStartupInput_t
    
    ! Additional types
    type, bind(c) :: Rect_t
        integer(c_long) :: X = 0, Y = 0, Width = 0, Height = 0
    end type Rect_t
   
    type, bind(c) :: BitmapData_t
        integer(c_int) :: Width
        integer(c_int) :: Height
        integer(c_int) :: Stride
        integer(c_int) :: PixelFormat
        type(c_ptr) :: Scan0
        type(c_ptr) :: Reserved
    end type BitmapData_t

    ! GDI+ function pointer types
    abstract interface
        function GdiplusStartupFunc(token, input, output) bind(C)
            import
            integer(C_INT) :: GdiplusStartupFunc
            integer(C_INT64_T) :: token
            type(GdiplusStartupInput_t), intent(in) :: input
            type(C_PTR), value :: output
        end function GdiplusStartupFunc

        subroutine GdiplusShutdownFunc(token) bind(C)
            import
            integer(C_INT64_T), value :: token
        end subroutine GdiplusShutdownFunc

        function GdipLoadImageFromFileFunc(filename, image) bind(C)
            import
            integer(C_INT) :: GdipLoadImageFromFileFunc
            character(kind=C_CHAR), intent(in):: filename(*)
            type(C_PTR) :: image
        end function GdipLoadImageFromFileFunc

        function GdipSaveImageToFileFunc(image, filename, clsidEncoder, encoderParams) bind(C)
            import
            integer(C_long) :: GdipSaveImageToFileFunc
            type(C_PTR), value :: image
            character(kind=C_CHAR), intent(in) :: filename(*)
            type(C_PTR), value :: clsidEncoder
            type(C_PTR), value :: encoderParams
        end function GdipSaveImageToFileFunc
        
        function GdipGetImageWidthFunc(image, width) bind(c)
            import
            integer(c_long) :: GdipGetImageWidthFunc
            type(c_ptr), value :: image
            integer(c_int), intent(out) :: width
        end function GdipGetImageWidthFunc

        function GdipGetImageHeightFunc(image, height) bind(c)
            import
            integer(c_long) :: GdipGetImageHeightFunc
            type(c_ptr), value :: image
            integer(c_int), intent(out) :: height
        end function GdipGetImageHeightFunc
        
        function GdipDisposeImageFunc(image) bind(c)
            import
            integer(c_long) :: GdipDisposeImageFunc
            type(c_ptr), value :: image
        end function GdipDisposeImageFunc

        function GdipBitmapLockBitsFunc(bitmap, rect, flags, format, lockedBitmapData) bind(c)
            import
            integer(c_long) :: GdipBitmapLockBitsFunc
            type(c_ptr), value :: bitmap
            type(Rect_t), intent(in) :: rect
            integer(c_int), value :: flags
            integer(c_int), value :: format
            type(BitmapData_t) :: lockedBitmapData
        end function GdipBitmapLockBitsFunc

        function GdipBitmapUnlockBitsFunc(bitmap, lockedBitmapData) bind(c)
            import
            integer(c_long) :: GdipBitmapUnlockBitsFunc
            type(c_ptr), value :: bitmap
            type(BitmapData_t), intent(in) :: lockedBitmapData
        end function GdipBitmapUnlockBitsFunc
             
        function GdipCreateBitmapFromFileFunc(filename, bitmap) bind(c)
            import
            integer(c_long) :: GdipCreateBitmapFromFileFunc
            character(kind=C_CHAR), intent(in):: filename(*)
            type(c_ptr):: bitmap
        end function GdipCreateBitmapFromFileFunc
        
    end interface

    type(C_FUNPTR) :: procStartup, procShutdown 
    !
    type(C_FUNPTR) :: procLoadImage, procSaveImage
    type(c_funptr) :: procGetImageWidth, procGetImageHeight, procDisposeImage
    !
    type(c_funptr) :: procCreateBitmapFromFile
    type(c_funptr) :: procBitmapLockBits, procBitmapUnlockBits
    !
    procedure(GdiplusStartupFunc), pointer :: GdiplusStartup
    procedure(GdiplusShutdownFunc), pointer :: GdiplusShutdown
    !
    procedure(GdipLoadImageFromFileFunc), pointer :: GdipLoadImageFromFile
    procedure(GdipSaveImageToFileFunc), pointer :: GdipSaveImageToFile
    procedure(GdipGetImageWidthFunc), pointer :: GdipGetImageWidth
    procedure(GdipGetImageHeightFunc), pointer :: GdipGetImageHeight
    procedure(GdipDisposeImageFunc), pointer :: GdipDisposeImage
    !
    procedure(GdipCreateBitmapFromFileFunc), pointer :: GdipCreateBitmapFromFile
    procedure(GdipBitmapLockBitsFunc), pointer :: GdipBitmapLockBits
    procedure(GdipBitmapUnlockBitsFunc), pointer :: GdipBitmapUnlockBits
    !  
    !   CLSID
    !    {557CF400-1A04-11D3-9A73-0000F81EF32E}  bmp
    !    {557CF401-1A04-11D3-9A73-0000F81EF32E}  jpeg
    !    {557CF402-1A04-11D3-9A73-0000F81EF32E}  gif
    !    {557CF403-1A04-11D3-9A73-0000F81EF32E}  emf
    !    {557CF404-1A04-11D3-9A73-0000F81EF32E}  wmf
    !    {557CF405-1A04-11D3-9A73-0000F81EF32E}  tiff
    !    {557CF406-1A04-11D3-9A73-0000F81EF32E}  png
    !    {557CF407-1A04-11D3-9A73-0000F81EF32E}  icon
    integer(c_long), target :: clsid_bmp_encoder(4) = [z'557cf400', z'11d31a04', z'0000739a', z'2ef31ef8'] !bmp
    integer(c_long), target :: clsid_jpg_encoder(4) = [z'557cf401', z'11d31a04', z'0000739a', z'2ef31ef8'] !jpg
    integer(c_long), target :: clsid_gif_encoder(4) = [z'557cf402', z'11d31a04', z'0000739a', z'2ef31ef8'] !gif
    integer(c_long), target :: clsid_emf_encoder(4) = [z'557cf403', z'11d31a04', z'0000739a', z'2ef31ef8'] !emf
    integer(c_long), target :: clsid_wmf_encoder(4) = [z'557cf404', z'11d31a04', z'0000739a', z'2ef31ef8'] !wmf
    integer(c_long), target :: clsid_tif_encoder(4) = [z'557cf405', z'11d31a04', z'0000739a', z'2ef31ef8'] !tiff
    integer(c_long), target :: clsid_png_encoder(4) = [z'557cf406', z'11d31a04', z'0000739a', z'2ef31ef8'] !png
    integer(c_long), target :: clsid_ico_encoder(4) = [z'557cf407', z'11d31a04', z'0000739a', z'2ef31ef8'] !icon
    !
    integer(C_INTPTR_T) :: hLib
    integer(C_INT64_T) :: token
    type(GdiplusStartupInput_t) :: input
    !
    ! Bitmap
    type(BitmapData_t), target :: bmpData 
    type(c_ptr), target :: bitmap

    contains
    
    subroutine initialize_GDI()
        integer(C_INT) :: status
        ! Load the GDI+ library
        hLib = LoadLibrary("gdiplus.dll"//C_NULL_CHAR)
        if (hLib == 0) then
            print *, "Failed to load GDI+ library"
            stop
        end if

        ! Get the addresses of GDI+ functions
        procStartup = GetProcAddress(hLib, "GdiplusStartup"//C_NULL_CHAR)
        procShutdown = GetProcAddress(hLib, "GdiplusShutdown"//C_NULL_CHAR)
        !
        procLoadImage = GetProcAddress(hLib, "GdipCreateBitmapFromFile"//C_NULL_CHAR)
        procSaveImage = GetProcAddress(hLib, "GdipSaveImageToFile"//C_NULL_CHAR)
        procGetImageWidth = GetProcAddress(hLib, "GdipGetImageWidth"//C_NULL_CHAR)
        procGetImageHeight = GetProcAddress(hLib, "GdipGetImageHeight"//C_NULL_CHAR)
        procDisposeImage = GetProcAddress(hLib, "GdipDisposeImage"//C_NULL_CHAR)
        !
        procCreateBitmapFromFile = GetProcAddress(hLib, "GdipCreateBitmapFromFile"//C_NULL_CHAR)
        procBitmapLockBits = GetProcAddress(hLib, "GdipBitmapLockBits"//C_NULL_CHAR)
        procBitmapUnlockBits = GetProcAddress(hLib, "GdipBitmapUnlockBits"//C_NULL_CHAR)

        if (.not. c_associated(procStartup) .or. &
            .not. c_associated(procShutdown) .or. &
            .not. c_associated(procLoadImage) .or. &
            .not. c_associated(procSaveImage) .or. &
            .not. c_associated(procGetImageWidth) .or. &
            .not. c_associated(procGetImageHeight) .or. &
            .not. c_associated(procDisposeImage) .or. &
            .not. c_associated(procCreateBitmapFromFile) .or. &
            .not. c_associated(procBitmapLockBits) .or. & 
            .not. c_associated(procBitmapUnlockBits)  ) then
            print *, "Failed to get one or more GDI+ function addresses"
            status = FreeLibrary(hLib)
            stop
        end if

        ! Associate the C function pointers with the Fortran procedure pointers
        call c_f_procpointer(procStartup, GdiplusStartup)
        call c_f_procpointer(procShutdown, GdiplusShutdown)
        !
        call c_f_procpointer(procLoadImage, GdipLoadImageFromFile)
        call c_f_procpointer(procSaveImage, GdipSaveImageToFile)
        call c_f_procpointer(procGetImageWidth, GdipGetImageWidth)
        call c_f_procpointer(procGetImageHeight, GdipGetImageHeight)
        call c_f_procpointer(procDisposeImage, GdipDisposeImage)
        !
        call c_f_procpointer(procBitmapLockBits, GdipBitmapLockBits)
        call c_f_procpointer(procBitmapUnLockBits, GdipBitmapUnLockBits)
        call c_f_procpointer(procCreateBitmapFromFile, GdipCreateBitmapFromFile)
    
        ! Initialize GDI+ startup input
        input%GdiplusVersion = 1
        input%DebugEventCallback = C_NULL_FUNPTR
        input%SuppressBackgroundThread = 0
        input%SuppressExternalCodecs = 0

        ! Call GdiplusStartup
        status = GdiplusStartup(token, input, C_NULL_PTR)
        if (status /= 0) then
            print *, "Failed to initialize GDI+. Error code:", status
            status = FreeLibrary(hLib)
            stop
        else    
            print *, "GDI+ initialized successfully"
        end if
    end subroutine initialize_GDI
    
    subroutine finialize_GDI()
        integer(C_INT) :: status
        ! Shutdown GDI+
        call GdiplusShutdown(token)
        print *, "GDI+ shut down successfully"
        ! Unload the GDI+ library
        status = FreeLibrary(hLib)
        if (status == 0) then
            print *, "Failed to unload GDI+ library"
        else
            print *, "GDI+ library unloaded successfully"
        end if
    end subroutine finialize_GDI
    
    subroutine open_and_read(fn, pixels)
        character(kind=c_char, len=*), target, intent(in) :: fn
        integer(c_int), pointer, intent(out) :: pixels(:, :)
        !
        integer(C_INT) :: status
        integer(C_INT) :: iwidth, iheight
        type(Rect_t) :: rect_in
        !
        status = GdipCreateBitmapFromFile(to_wide_string(fn), bitmap)
        if (status /= 0) then
           print *, "Failed to load image. Error code:", status
           stop
        else
           print *, "Image loaded successfully"
           status = GdipGetImageWidth(bitmap, iwidth)
           print *, status, 'width ', iwidth    
           status = GdipGetImageHeight(bitmap, iHeight)
           print *, status, 'height', iheight     
        end if
        !
        bmpData%Width  = iwidth
        bmpData%Height = iheight
        bmpData%Stride = ((iwidth * 32 + 31) / 32) * 4
        bmpData%PixelFormat = PixelFormat32bppARGB
        ! Lock bitmap bits / c_null_ptr
        rect_in%X = 0
        rect_in%Y = 0
        rect_in%Width  = iwidth
        rect_in%Height = iheight
        !
        status = GdipBitmapLockBits(bitmap, rect_in, ImageLockModeRead + ImageLockModeWrite, &
                                    PixelFormat32bppARGB, bmpData)
        if (status /= 0) then
           print *, 'BitmapLockBits failed. status =', status 
           stop 
        end if 
        !
        ! bind to pixel data
        call c_f_pointer(bmpData%Scan0, pixels, [iwidth, iheight])
    end subroutine open_and_read
    
    subroutine write_and_close(fn)
        character(kind=c_char, len=*), intent(in) :: fn
        !
        integer(C_INT) :: status
        !
        status = GdipBitmapUnLockBits(bitmap, bmpData)
        if (status /= 0) then
           print *, 'BitmapUnLockBits failed. status =', status 
           stop
        end if 
        
        ! Save the image to a new file
        status = GdipSaveImageToFile(bitmap, to_wide_string(fn), C_loc(clsid_jpg_encoder), C_NULL_PTR)

        if (status /= 0) then
            print *, "Failed to save image. Error code:", status
        else
            print *, "Image saved successfully"
        end if
        ! Clean up
        status = GdipDisposeImage(bitmap)   
        if (status /= 0) then
            print *, "Failed to dispose Bitmap. Error code:", status
        else
            print *, "Bitmap disposed successfully"
        end if
    end subroutine write_and_close
end module GdiPlus_m


program test_gdiplus
    use, intrinsic :: iso_c_binding
    use :: LoadDll_m
    use :: GdiPlus_m
    implicit none
    
    call initialize_GDI() 
BMP:block    
        character(kind=c_char, LEN = 256), target :: fn
        integer(c_int), pointer :: pixels(:, :)
        ! load bmp/jpg/gif/png/... file as 32bit ARGB 2d-array      
        fn = "C:\temp\kositantan.jpg"//C_NULL_CHAR
        call open_and_read(fn, pixels)
        !
   RGBA:block 
            integer(c_int) :: ir, ig, ib, ia
            do concurrent (integer::ix = 1:size(pixels, 1), iy = 1:size(pixels, 2))
                ! Extract ARGB values
                ia = iand(ishft(pixels(ix, iy), -24), 255)
                ir = iand(ishft(pixels(ix, iy), -16), 255)
                ig = iand(ishft(pixels(ix, iy),  -8), 255)
                ib = iand(      pixels(ix, iy)      , 255)
                pixels(ix, iy) = (256 - ib) + 256 * (256 - ig + 256 * (256 - ir + 256 * ia))  
            end do
        end block RGBA
        ! 
        ! save as jpg   !  ( bmp/jpg/gif/png/... )       
        fn = "C:\temp\sika.jpg"//C_NULL_CHAR
        call write_and_close(fn)
    end block BMP
    call finialize_GDI()
end program test_gdiplus