Display .BMP files in RGB colors - WIN 10 ONLY
Posted: 05 Dec 2020 06:56
Recently I read across the specification of the BMP file format and I found it simple enough for processing in a Batch code. We already have a thread about how to process PPM files: viewtopic.php?f=3&t=8087&p=53745#p53745 The structure of uncompressed bitmaps with 24 bits color depth (and this is what the following code is for) is quite similar. It's a proof of concept which certainly has room for improvements. So, everyone is free to publish their updates here. For me this is just for fun. I don't have any use for it and I'll leave it alone ...
How does it work?
BMP files are binary files. Since we can't process binary data in Batch directly, I create a HEX dump using CERTUTIL. The file content begins with two data structures, BITMAPFILEHEADER and BITMAPINFOHEADER (links to their spec can be found in the code comments). This data contains values to verify that we got the right file format. And it contains values like the image size and the offset to the pixel data in the file that are necessary for the conversion we have to perform.
The way the pixels are saved makes the processing quite slow though. For the ANSI sequences we need the colors in RGB order, and we need the pixel rows top-down. However in the pixel array of the bitmap colors are in BGR order (which is the easy task), and the pixel rows are buttom-up. The latter means that the first 3 bytes we read in the pixel array belong to the bottom left pixel of the image, which requires an expensive reordering using temporary files.
To avoid this time-consuming conversion all the time new, I decided to create an intermediate .ansi file. It contains the ANSI sequences for the whole image. The TYPE command is now sufficient to print the image into the window. Only the internal color rendition is the task leftover for the console host to be performed.
The ANSI sequences I used seem to be unnecessarily long, but that's for a reason. I could have used the full block character along with the foreground color only. But depending on the font size, block characters may not fill the whole character cell and we may still see some spacing around it. I could have used a space character along with the background color only. This would look reasonable but it doesn't survive resizing of the window. Eventually I had to set both foreground and background to the same color, and to use any non-whitespace character to write the pixel (I have randomly chosen to use #).
The code doesn't resize the picture. Keep in mind that the pixels are represented by filling an entire character cell each. However, a temporary registry key is created (and removed afterwards) to change font and font size in order to get reasonable results.
Steffen
Code and bitmap file:
How does it work?
BMP files are binary files. Since we can't process binary data in Batch directly, I create a HEX dump using CERTUTIL. The file content begins with two data structures, BITMAPFILEHEADER and BITMAPINFOHEADER (links to their spec can be found in the code comments). This data contains values to verify that we got the right file format. And it contains values like the image size and the offset to the pixel data in the file that are necessary for the conversion we have to perform.
The way the pixels are saved makes the processing quite slow though. For the ANSI sequences we need the colors in RGB order, and we need the pixel rows top-down. However in the pixel array of the bitmap colors are in BGR order (which is the easy task), and the pixel rows are buttom-up. The latter means that the first 3 bytes we read in the pixel array belong to the bottom left pixel of the image, which requires an expensive reordering using temporary files.
To avoid this time-consuming conversion all the time new, I decided to create an intermediate .ansi file. It contains the ANSI sequences for the whole image. The TYPE command is now sufficient to print the image into the window. Only the internal color rendition is the task leftover for the console host to be performed.
The ANSI sequences I used seem to be unnecessarily long, but that's for a reason. I could have used the full block character along with the foreground color only. But depending on the font size, block characters may not fill the whole character cell and we may still see some spacing around it. I could have used a space character along with the background color only. This would look reasonable but it doesn't survive resizing of the window. Eventually I had to set both foreground and background to the same color, and to use any non-whitespace character to write the pixel (I have randomly chosen to use #).
The code doesn't resize the picture. Keep in mind that the pixels are represented by filling an entire character cell each. However, a temporary registry key is created (and removed afterwards) to change font and font size in order to get reasonable results.
Steffen
Code: Select all
@echo off &setlocal
rem Name of the bitmap file without extension
set "filename=123"
rem Window title necessary to identify it for the updated registry values
set "title=BMP Test"
if "%~1"=="" (
rem If the .ansi file was not yet created, we will call :make_ansi to convert the .bmp content
if not exist "%filename%.ansi" call :make_ansi || (pause&goto :eof)
rem Update the size of the character cells which represent the pixels of the image
>nul reg add "HKCU\Console\%title:\=_%" /f /v "FaceName" /t REG_SZ /d "Terminal"
>nul reg add "HKCU\Console\%title:\=_%" /f /v "FontFamily" /t REG_DWORD /d 0x00000030
>nul reg add "HKCU\Console\%title:\=_%" /f /v "FontSize" /t REG_DWORD /d 0x00040006
rem Restart the script to get the new settings applied
start "%title%" /max "%comspec%" /c "%~fs0" #
goto :eof
) else >nul reg delete "HKCU\Console\%title:\=_%" /f
rem Print the image
type "%filename%.ansi"
pause
goto :eof
rem Subroutine to convert the pixel array of the bitmap file into a file containing "true color" ANSI escape sequences
:make_ansi
setlocal EnableDelayedExpansion
set "outfile=!filename!.ansi"
2>nul del "!outfile!.raw.hex"
rem Batch isn't able to work with the binary data of a BMP file. Thus, we use certutil to create a HEX dump.
>nul certutil -encodehex "!filename!.bmp" "!outfile!.raw.hex" 4||(echo Unable to dump the bitmap&goto err)
rem rn is CR + LF
set "rn="&for /f "skip=1" %%i in ('echo(^|replace ? . /u /w') do if not defined rn (set rn=%%i^
%= don't alter =%
)
rem Certutil wrote 16 space-separated byte values per line. This is inconvenient because we have to reassemble them into sequences of 2, 4, or 3 bytes later on.
rem This will get easier if we write every value into a separate line first.
>"!outfile!.separate.hex" (for /f "usebackq tokens=1-16" %%a in ("!outfile!.raw.hex") do echo %%a!rn!%%b!rn!%%c!rn!%%d!rn!%%e!rn!%%f!rn!%%g!rn!%%h!rn!%%i!rn!%%j!rn!%%k!rn!%%l!rn!%%m!rn!%%n!rn!%%o!rn!%%p)
del "!outfile!.raw.hex"
rem Control Sequence Introducer
for /f %%e in ('echo prompt $E^|cmd') do set "csi=%%e["
rem We use SET /P to read one byte value at once. Sequences that represent numeric values are in little endian byte order.
rem For the variable naming refer to the structure documentations linked in the remarks.
<"!outfile!.separate.hex" (
rem BITMAPFILEHEADER https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapfileheader
set /p "bfType_1="&set /p "bfType_2="
if "!bfType_1!!bfType_2!" neq "424d" (echo Wrong file type. Magic number must be 424d (ASCII "BM"^)&goto err)
set /p "b4="&set /p "b3="&set /p "b2="&set /p "b1="
set /a "bfSize=0x!b1!!b2!!b3!!b4!"
for %%i in ("!filename!.bmp") do if !bfSize! neq %%~zi (echo File size doesn't match (!bfSize! vs. %%~zi^)&goto err)
set /p "="&set /p "="&set /p "="&set /p "=" &rem bfReserved1 and bfReserved2 with two unused bytes each
set /p "b4="&set /p "b3="&set /p "b2="&set /p "b1="
set /a "bfOffBits=0x!b1!!b2!!b3!!b4!" &rem Offset in bytes between the beginning of the file and the pixel array
rem BITMAPINFOHEADER https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
set /p "b4="&set /p "b3="&set /p "b2="&set /p "b1="
set /a "biSize=0x!b1!!b2!!b3!!b4!"
if !biSize! neq 40 (echo Wrong DIB header&goto err)
set /p "b4="&set /p "b3="&set /p "b2="&set /p "b1="
set /a "biWidth=0x!b1!!b2!!b3!!b4!" &rem Width of the bitmap in pixels
set /p "b4="&set /p "b3="&set /p "b2="&set /p "b1="
set /a "biHeight=0x!b1!!b2!!b3!!b4!" &rem Height of the bitmap in pixels
set /a "chk=((biWidth>>31)&1)+((biHeight>>31)&1)"
if !chk! neq 0 (echo Negative image size not supported&goto err)
set /p "b2="&set /p "b1="
set /a "biPlanes=0x!b1!!b2!"
if !biPlanes! neq 1 (echo Number of planes must be 1&goto err)
set /p "b2="&set /p "b1="
set /a "biBitCount=0x!b1!!b2!"
if !biBitCount! neq 24 (echo Only 24 bits color depth supported&goto err)
set /p "b4="&set /p "b3="&set /p "b2="&set /p "b1="
set /a "biCompression=0x!b1!!b2!!b3!!b4!"
if !biCompression! neq 0 (echo Only uncompressed bitmaps supported&goto err)
set /a "skip=bfOffBits-34" &rem We already read 34 bytes. Some further bytes are not of interest. We ignore them to get to the begin of the pixel array.
for /l %%i in (1 1 !skip!) do set /p "="
set /a "pad=(biWidth*3)%%4" &rem The pixel rows might be padded because the number of bytes has to be a multiple of 4
if !pad! neq 0 set /a "pad=4-pad"
rem A pixel consists of 3 bytes which represent the color in BGR order.
rem The first 3 bytes belong to the lower left corner of the image. This means the order of the pixel array is bottom-up.
rem Since we need both the colors in RGB order and the upper left corner first, we have to reorder literally everything.
rem After these nested loops we got a file containing the "true color" ANSI sequences.
echo The BMP file will be converted for you. This may take a while ...
>"!outfile!" type nul
for /l %%Y in (1 1 !biHeight!) do (
>"!outfile!.~tmp" (
set "row="
for /l %%X in (1 1 !biWidth!) do (
set /p "b="&set /p "g="&set /p "r="
set /a "r=0x!r!,g=0x!g!,b=0x!b!"
set "row=!row!%csi%38;2;!r!;!g!;!b!m%csi%48;2;!r!;!g!;!b!m#"
)
echo !row!%csi%0m
)
for /l %%i in (1 1 !pad!) do set /p "="
>nul copy /b "!outfile!.~tmp" + "!outfile!"
>nul move /y "!outfile!.~tmp" "!outfile!"
)
)
2>nul del "!outfile!.separate.hex"
endlocal&exit /b 0
:err
2>nul del "!outfile!.separate.hex"
endlocal&exit /b 1
Code and bitmap file: