Setting the DPI resolution of a bitmap file isn’t hard. Many programs already do it. It is frustrating that some don’t.
Here’s a way to fix that.
Background
The early versions of CmdTwain used a library to save scans as bitmap files (*.bmp) or as JPEG files (*.jpg). The program worked really well but one user drew my attention to the “dots per inch” (DPI) value in the saved files. It didn’t matter whether he scanned at 200 DPI or 300 DPI, the scans were always being reported as 96 DPI. The 300 DPI resolution scan was bigger (more pixels) than the 200 DPI resolution one for the same page but instead of the page size being reported as the same, the page size was supposedly bigger. Windows scales images to fit on a printed page so mostly it didn’t matter. But it was wrong.
Sometimes you scan a document and it prints back out at a fraction of the size. It looks crazy. It used to frustrate me and, in my day job, I was using other people’s expensive scanning software and having these problems. I could certainly understand a user’s frustration with CmdTwain getting this wrong.
The fix to CmdTwain is incidental to this article. It was the catalyst but this article is broader than CmdTwain. (If you are only using CmdTwain 1.02 or later, you don’t need to read this article.)
Solution
If the DPI resolution of a bmp file from another program is wrong and you want to fix it, you can use get_dpi to check the file and set_dpi to change it.
Both of these links are to ZIP files containing a program and a Borland C runtime DLL. Their use is straight forward:
- Download them.
- Extract the files to a suitable location (eg somewhere in your PATH, C:\TEST, or even – after checking you’re not overwriting anything – C:\WINDOWS).
- Copy the file you want to check / fix into the same directory.
- Run up a DOS prompt (Start, Run, cmd).
- get_dpi file.bmp
- set_dpi file.bmp 300
“get_dpi” will tell you the current DPI resolution.
“set_dpi” will change it.
What it Looks Like
When run, they look like this:
C:\Test2>get_dpi Get_dpi Ver 1.00 Copyright (c) 2013 GssEzisoft Usage: get_dpi filename.{jpg|jpeg|bmp} C:\Test2>get_dpi 67.bmp x= 400 DPI, y= 400 DPI C:\Test2>set_dpi Set_dpi Ver 1.00 Copyright (c) 2013 GssEzisoft Usage: set_dpi filename.{jpg|jpeg|bmp} xDPI [yDPI] eg1 : set_dpi fred.jpg 300 eg2 : set_dpi demo.bmp 300 400 C:\Test2>set_dpi 67.bmp 300 C:\Test2>get_dpi 67.bmp x= 300 DPI, y= 300 DPI C:\Test2>
Unspecified DPI
When you run get_dpi on a bitmap file which doesn’t have DPI set at all, you’ll see 0 for the values. Shockingly, the fastest way to get an incorrect bitmap like this – is to use MS Paint.
The result looks like:
C:\Test2>get_dpi 20x20-red.bmp x= 0 DPI, y= 0 DPI C:\Test2>
Windows (right-click the file, properties) will say 96 DPI:
The “96” is a made-up value based on typical DPI sizes for typical devices and on Windows not wanting to say zero. Let’s be polite and call it a default number.
Under the Hood
Bail out now if you’re not technical.
Internally, a bitmap file looks like this:
000000: 42 4D A6 0D 13 00 00 00 00 00 36 00 00 00 28 00 BM........6...(. 000010: 00 00 F6 01 00 00 3C 03 00 00 01 00 18 00 00 00 ......<......... 000020: 00 00 70 0D 13 00 23 2E 00 00 23 2E 00 00 00 00 ..p...#...#..... 000030: 00 00 00 00 00 00 FF FF FF FF FF FF FF FF FF FF ................ 000040: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................ 000050: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ................
The structure is:
typedef struct tagBITMAPFILEHEADER { 00: WORD bfType; 02: DWORD bfSize; 06: WORD bfReserved1; 08: WORD bfReserved2; 0a: DWORD bfOffBits; } BITMAPFILEHEADER, *PBITMAPFILEHEADER;
A BITMAPINFO or ... structure immediately follows the BITMAPFILEHEADER structure in the DIB file.
typedef struct tagBITMAPINFO { 0e: BITMAPINFOHEADER bmiHeader; RGBQUAD bmiColors[1]; } BITMAPINFO, *PBITMAPINFO; typedef struct tagBITMAPINFOHEADER { 0e: DWORD biSize; 12: LONG biWidth; 16: LONG biHeight; 1a: WORD biPlanes; 1c: WORD biBitCount; 1e: DWORD biCompression; 22: DWORD biSizeImage; 26: LONG biXPelsPerMeter; <<<<<<<<<<<<<<<<< 2a: LONG biYPelsPerMeter; <<<<<<<<<<<<<<<<< DWORD biClrUsed; DWORD biClrImportant; } BITMAPINFOHEADER, *PBITMAPINFOHEADER;
So:
file[00..01] = "BM" (bfType) file[02..05] = 0013 0DA6 = 1248678 = 502 w x 828 h x 3 bytes per pixel (24bit RGB) file[0e..11] = 0000 0028 = 40 = biSize file[12..15] = 0000 01F6 = 502 = biWidth file[16..19] = 0000 033C = 828 = biHeight file[26..29] = 0000 2E23 = 11811 = biXPelsPerMeter = 118.11 pixels per cm = 299.9994 pixels per inch (1 inch = exactly 2.54 cm) Windows displays this as 299. file[2a..2d] = same
See MSDN or wingdi.h for the structure details.
Reading or writing the DPI resolution for a bitmap file is simply a matter of reading or writing 4 bytes at offsets 0x26 and 0x2a of the file.