Saving a JPEG image

by Richard Russell, December 2006

The main BBC BASIC for Windows documentation explains how to display a JPEG image and elsewhere you can find out how to load a JPEG image for use in your program. This article describes how to save an image as a JPEG file. It relies on the presence of the GDI Plus library so will work only on Windows XP (or later) or if you have specifically installed GDIPLUS.DLL on the target computer. Microsoft permits you to redistribute this file so you can include it with your program if necessary.

To begin with you need to have a handle to the bitmap you want to save (see below if instead the bitmap is in the form of a DIB). One way of obtaining a handle is to load the image from a file (e.g. a BMP file):

        bmpfile$ = "\Windows\Soap Bubbles.bmp"
        SYS "LoadImage", 0, bmpfile$, 0, 0, 0, 16 TO hbitmap%

Here the image is loaded at its original size. You can alternatively scale the image to different dimensions:

        bmpfile$ = "\Windows\Soap Bubbles.bmp"
        SYS "LoadImage", 0, bmpfile$, 0, dx%, dy%, 16 TO hbitmap%

Where dx% and dy% are the wanted width and height of the image respectively (the scaling quality is not particularly good so for best results you might prefer to scale the image using a third-party program).

There are a number of other ways in which you might obtain a bitmap handle, which are outside the scope of this article. You can easily obtain a handle to whatever is displayed in your program's output window:

        SYS "GetCurrentObject", @memhdc%, 7 TO hbitmap%

but this ordinarily returns the entire 1920 x 1440 bitmap which is probably not what you want. To save just a region of your program's output window the easiest way is probably to save it first as a BMP file (using *GSAVE) then load it using LoadImage as shown above.

Once you've got a handle to the bitmap you simply save it as a JPEG file as follows:

        PROCsavejpeg(hbitmap%, filename$, quality%)

Here filename$ is the name of the JPEG file to create and quality% is a measure of relative quality from 1 (bad) to 100 (good). The better the quality the larger the file that will be created.

One you've saved the file you should delete the bitmap handle:

        SYS "DeleteObject", hbitmap%

Finally, here's the code for PROCsavejpeg itself:

        DEF PROCsavejpeg(hbitmap%, filename$, quality%)
        LOCAL gdiplus%, ole32%
 
        SYS "LoadLibrary", "GDIPLUS.DLL" TO gdiplus%
        IF gdiplus% = 0 ERROR 100, "Couldn't load GDIPLUS.DLL"
        SYS "GetProcAddress", gdiplus%, "GdiplusStartup" TO `GdiplusStartup`
        SYS "GetProcAddress", gdiplus%, "GdiplusShutdown" TO `GdiplusShutdown`
        SYS "GetProcAddress", gdiplus%, "GdipCreateBitmapFromHBITMAP" TO `GdipCreateBitmapFromHBITMAP`
        SYS "GetProcAddress", gdiplus%, "GdipDisposeImage" TO `GdipDisposeImage`
        SYS "GetProcAddress", gdiplus%, "GdipSaveImageToFile" TO `GdipSaveImageToFile`
        SYS "LoadLibrary", "OLE32.DLL" TO ole32%
        SYS "GetProcAddress", ole32%, "CLSIDFromString" TO `CLSIDFromString`
 
        LOCAL tSI{}, tParams{}, lRes%, lGDIP%, lBitmap%, tJpgEncoder%, guid%, filename%
 
        DIM tJpgEncoder% LOCAL 15, guid% LOCAL 79, filename% LOCAL 2*LEN(filename$)+1
        DIM tParams{Count%, Guid&(15), NumberOfValues%, Type%, Value%}
        DIM tSI{GdiplusVersion%, DebugEventCallback%, \
        \       SuppressBackgroundThread%, SuppressExternalCodecs%}
 
        REM Initialize GDI+
        tSI.GdiplusVersion% = 1
        SYS `GdiplusStartup`, ^lGDIP%, tSI{}, 0 TO lRes%
        IF lRes% ERROR 100, "GDI+ error "+STR$(lRes%)
 
        REM Create the GDI+ bitmap from the image handle
        SYS `GdipCreateBitmapFromHBITMAP`, hbitmap%, 0, ^lBitmap% TO lRes%
        IF lRes% ERROR 100, "GDI+ error "+STR$(lRes%)
 
        REM Initialize the encoder GUID
        SYS "MultiByteToWideChar", 0, 0, "{557CF401-1A04-11D3-9A73-0000F81EF32E}", -1, guid%, 40
        SYS `CLSIDFromString`, guid%, tJpgEncoder%
 
        REM Initialize the encoder parameters
        tParams.Count% = 1
        SYS "MultiByteToWideChar", 0, 0, "{1D5BE4B5-FA4A-452D-9CDD-5DB35105E7EB}", -1, guid%, 40
        SYS `CLSIDFromString`, guid%, ^tParams.Guid&(0)
        tParams.NumberOfValues% = 1
        tParams.Type% = 4
        tParams.Value% = ^quality%
 
        REM Save the image
        SYS "MultiByteToWideChar", 0, 0, filename$, -1, filename%, LEN(filename$)+1
        SYS `GdipSaveImageToFile`, lBitmap%, filename%, tJpgEncoder%, tParams{} TO lRes%
        IF lRes% ERROR 100, "GDI+ error "+STR$(lRes%)
 
        REM Destroy the bitmap
        SYS `GdipDisposeImage`, lBitmap%
 
        REM Shutdown GDI+
        SYS `GdiplusShutdown`, lGDIP%
        SYS "FreeLibrary", gdiplus%
 
        ENDPROC

If, instead of a bitmap handle, you have a bitmap (DIB) stored in memory you can use this alternative routine:

        DEF PROCsavejpegdib(dib%, bmi%, filename$, quality%)
        LOCAL gdiplus%, ole32%
 
        SYS "LoadLibrary", "GDIPLUS.DLL" TO gdiplus%
        IF gdiplus% = 0 ERROR 100, "Couldn't load GDIPLUS.DLL"
        SYS "GetProcAddress", gdiplus%, "GdiplusStartup" TO `GdiplusStartup`
        SYS "GetProcAddress", gdiplus%, "GdiplusShutdown" TO `GdiplusShutdown`
        SYS "GetProcAddress", gdiplus%, "GdipCreateBitmapFromGdiDib" TO `GdipCreateBitmapFromGdiDib`
        SYS "GetProcAddress", gdiplus%, "GdipDisposeImage" TO `GdipDisposeImage`
        SYS "GetProcAddress", gdiplus%, "GdipSaveImageToFile" TO `GdipSaveImageToFile`
        SYS "LoadLibrary", "OLE32.DLL" TO ole32%
        SYS "GetProcAddress", ole32%, "CLSIDFromString" TO `CLSIDFromString`
 
        LOCAL tSI{}, tParams{}, lRes%, lGDIP%, lBitmap%, tJpgEncoder%, guid%, filename%
 
        DIM tJpgEncoder% LOCAL 15, guid% LOCAL 79, filename% LOCAL 2*LEN(filename$)+1
        DIM tParams{Count%, Guid&(15), NumberOfValues%, Type%, Value%}
        DIM tSI{GdiplusVersion%, DebugEventCallback%, \
        \       SuppressBackgroundThread%, SuppressExternalCodecs%}
 
        REM Initialize GDI+
        tSI.GdiplusVersion% = 1
        SYS `GdiplusStartup`, ^lGDIP%, tSI{}, 0 TO lRes%
        IF lRes% ERROR 100, "GDI+ error "+STR$(lRes%)
 
        REM Create the GDI+ bitmap from the DIB
        SYS `GdipCreateBitmapFromGdiDib`, bmi%, dib%, ^lBitmap% TO lRes%
        IF lRes% ERROR 100, "GDI+ error "+STR$(lRes%)
 
        REM Initialize the encoder GUID
        SYS "MultiByteToWideChar", 0, 0, "{557CF401-1A04-11D3-9A73-0000F81EF32E}", -1, guid%, 40
        SYS `CLSIDFromString`, guid%, tJpgEncoder%
 
        REM Initialize the encoder parameters
        tParams.Count% = 1
        SYS "MultiByteToWideChar", 0, 0, "{1D5BE4B5-FA4A-452D-9CDD-5DB35105E7EB}", -1, guid%, 40
        SYS `CLSIDFromString`, guid%, ^tParams.Guid&(0)
        tParams.NumberOfValues% = 1
        tParams.Type% = 4
        tParams.Value% = ^quality%
 
        REM Save the image
        SYS "MultiByteToWideChar", 0, 0, filename$, -1, filename%, LEN(filename$)+1
        SYS `GdipSaveImageToFile`, lBitmap%, filename%, tJpgEncoder%, tParams{} TO lRes%
        IF lRes% ERROR 100, "GDI+ error "+STR$(lRes%)
 
        REM Destroy the bitmap
        SYS `GdipDisposeImage`, lBitmap%
 
        REM Shutdown GDI+
        SYS `GdiplusShutdown`, lGDIP%
        SYS "FreeLibrary", gdiplus%
 
        ENDPROC

You would call it as follows:

        PROCsavejpegdib(dibits%, bmi{}, filename$, quality%)

where dibits% is the address of the bitmap data and bmi{} is a BITMAPINFO structure containing the dimensions etc. and (optionally) colour palette for the bitmap.