function [info,msg] = imbmpinfo(filename)
%IMBMPINFO Get information about the image in a BMP file.
%   [INFO,MSG] = IMBMPINFO(FILENAME) returns information about
%   the image contained in a BMP file.  If the attempt fails for
%   some reason (e.g. the file does not exist or is not a BMP
%   file), then INFO is empty and MSG is a string containing a
%   diagnostic message.
%
%   See also IMREAD, IMWRITE, IMFINFO.

%   Steven L. Eddins, June 1996
%   Copyright (c) 1984-98 by The MathWorks, Inc.
%   $Revision: 1.6 $  $Date: 1997/11/21 23:36:01 $

%   Required reading before editing this file: Encyclopedia of
%   Graphics File Formats, 2nd ed., pp. 572-591, pp. 630-650.

% This function should not call error()!  -SLE

info = [];
msg = '';

if (~isstr(filename))
    msg = 'FILENAME must be a string';
    return;
end

[fid,m] = fopen(filename, 'r', 'ieee-le');  % BMP files are little-endian
if (fid == -1)
    info = [];
    msg = m;
    return;
end
filename = fopen(fid);  % Get the full path name if not in pwd
d = dir(filename);      % Read directory information

%
% Initialize universal structure fields to fix the order
%
info.Filename = filename;
info.FileModDate = d.date;
info.FileSize = d.bytes;
info.Format = 'bmp';
info.FormatVersion = [];
info.Width = [];
info.Height = [];
info.BitDepth = [];
info.ColorType = [];
info.FormatSignature = [];

%
% Initialize BMP-specific structure fields to fix the order
%
info.NumColormapEntries = [];
info.Colormap = [];
info.RedMask = [];
info.GreenMask = [];
info.BlueMask = [];


%
% First we must determine which variant of BMP we have.
% Use the algorithm from Encyclopedia of Graphics File Formats,
% 2ed, pp. 584-585
%
info.FormatSignature = char(fread(fid, 2, 'uint8')');
if (isempty(info.FormatSignature))
    info = [];
    msg = 'Empty file';
    fclose(fid);
    return;
end
    
if (strcmp(info.FormatSignature, 'BM'))
    % We have a single-image BMP file. It may be 
    % a Windows or an OS/2 file.
    info.FileSize = fread(fid, 1, 'uint32');
    fseek(fid, 4, 'cof');  % skip 2 reserved 16-bit words
    info.ImageDataOffset = fread(fid, 1, 'uint32');
    info.BitmapHeaderSize = fread(fid, 1, 'uint32');

    if (isempty(info.BitmapHeaderSize))
        info = [];
        msg = 'Truncated header';
        fclose(fid);
        return;
    end

    compression = [];
    switch info.BitmapHeaderSize
    case 12
        info.Width = fread(fid, 1, 'int16');
        info.Height = fread(fid, 1, 'int16');
        if (isempty(info.Width) | isempty(info.Height))
            info = [];
            msg = 'Truncated header';
            fclose(fid);
            return;
        end
        if ((info.Width < 0) | (info.Height < 0))
            info.FormatVersion = 'Version 1 (IBM OS/2 1.x)';
            fseek(fid, -4, 'cof');
            info.Width = fread(fid, 1, 'uint16');
            info.Height = fread(fid, 1, 'uint16');
        else
            info.FormatVersion = 'Version 2 (Microsoft Windows 2.x)';
        end
        
        info.NumPlanes = fread(fid, 1, 'uint16');
        info.BitDepth = fread(fid, 1, 'uint16');
        
    case 40
        info.Width = fread(fid, 1, 'uint32');
        info.Height = fread(fid, 1, 'uint32');
        info.NumPlanes = fread(fid, 1, 'uint16');
        info.BitDepth = fread(fid, 1, 'uint16');
        
        compression = fread(fid, 1, 'uint32');
        if (isempty(compression))
            info = [];
            msg = 'Truncated header';
            fclose(fid);
            return;
        end
        if (compression == 3)
            info.FormatVersion = 'Version 3 (Microsoft Windows NT)'
        else
            info.FormatVersion = 'Version 3 (Microsoft Windows 3.x)';
        end
        
    case 108
        
        info.FormatVersion = 'Version 4 (Microsoft Windows 95)';
        compression = fread(fid, 1, 'uint32');
        if (isempty(compression))
            info = [];
            msg = 'Truncated header';
            fclose(fid);
            return;
        end

    otherwise
        
        if ((info.HeaderSize > 12) & (info.HeaderSize <= 64))
            info.FormatVersion = 'Version 2 (IBM OS/2 2.x)';
            compression = fread(fid, 1, 'uint32');
            if (isempty(compression))
                info = [];
                msg = 'Truncated header';
                fclose(fid);
                return;
            end
            
        else
            info = [];
            msg = 'Corrupted header';
            fclose(fid);
            return;
        end
        
    end
    
    if (~isempty(compression))
        switch compression
        case 0
            info.CompressionType = 'none';
            
        case 1
            info.CompressionType = '8-bit RLE';
            
        case 2
            info.CompressionType = '4-bit RLE';
            
        case 3
            if (strcmp(info.FormatVersion, 'Version 3 (Microsoft Windows NT)'))
                info.CompressionType = 'bitfields';
            else
                % OS/2 2.x
                info.CompressionType = 'Huffman 1D';
            end
            
        case 4
            % Only valid for OS/2 2.x
            info.CompressionType = '24-bit RLE';
            
        otherwise
            info = [];
            msg = 'Unrecognized compression type';
            fclose(fid);
            return;
            
        end
    else
        info.CompressionType = 'none';
    end
    
elseif (strcmp(info.FormatSignature, 'BA'))            
    info = [];
    msg = 'Unsupported format; may be an OS/2 bitmap array';
    fclose(fid);
    return;
    
else
    info = [];
    msg = 'Not a BMP file';
    fclose(fid);
    return;
    
end

switch info.FormatVersion
    
case 'Version 1 (IBM OS/2 1.x)'
    
    info.NumColormapEntries = (info.ImageDataOffset - ftell(fid))/3;
    if (rem(info.NumColormapEntries, 1) ~= 0)
        info = [];
        msg = 'Corrupt file';
        fclose(fid);
        return;
    end
    if (info.NumColormapEntries > 0)
        [map,count] = fread(fid, info.NumColormapEntries*3, 'uint8');
        if (count ~= info.NumColormapEntries*3)
            info = [];
            msg = 'Truncated colormap data';
            fclose(fid);
            return;
        end
        map = reshape(map, 3, info.NumColormapEntries);
        info.Colormap = double(flipud(map)')/255;
    end
    
case 'Version 2 (Microsoft Windows 2.x)';
    
    info.NumColormapEntries = (info.ImageDataOffset - ftell(fid))/3;
    if (rem(info.NumColormapEntries, 1) ~= 0)
        info = [];
        msg = 'Corrupt file';
        fclose(fid);
        return;
    end
    if (info.NumColormapEntries > 0)
        [map,count] = fread(fid, info.NumColormapEntries*3, 'uint8');
        if (count ~= info.NumColormapEntries*3)
            info = [];
            msg = 'Truncated colormap data';
            fclose(fid);
            return;
        end
        map = reshape(map, 3, info.NumColormapEntries);
        info.Colormap = double(flipud(map)')/255;
    end

case 'Version 3 (Microsoft Windows 3.x)';
    
    info.BitmapSize = fread(fid, 1, 'uint32');
    info.HorzResolution = fread(fid, 1, 'int32');
    info.VertResolution = fread(fid, 1, 'int32');
    info.NumColorsUsed = fread(fid, 1, 'uint32');
    info.NumImportantColors = fread(fid, 1, 'uint32');
    
    info.NumColormapEntries = (info.ImageDataOffset - ftell(fid))/4;
    if (rem(info.NumColormapEntries, 1) ~= 0)
        info = [];
        msg = 'Corrupt file';
        fclose(fid);
        return;
    end
    
    if (info.NumColormapEntries > 0)
        [map,count] = fread(fid, info.NumColormapEntries*4, 'uint8');
        if (count ~= info.NumColormapEntries*4)
            info = [];
            msg = 'Truncated colormap data';
            fclose(fid);
            return;
        end
        map = reshape(map, 4, info.NumColormapEntries);
        info.Colormap = double(flipud(map(1:3,:))')/255;
    end

case 'Version 3 (Microsoft Windows NT)'
    
    info.BitmapSize = fread(fid, 1, 'uint32');
    info.HorzResolution = fread(fid, 1, 'int32');
    info.VertResolution = fread(fid, 1, 'int32');
    info.NumColorsUsed = fread(fid, 1, 'uint32');
    info.NumImportantColors = fread(fid, 1, 'uint32');
    
    if (((info.BitDepth == 16) | (info.BitDepth == 32)) & ...
                (~strcmp(info.CompressionType,'bitfields')))
        info = [];
        msg= 'BMP Version 3 (Microsoft Windows NT) file appears to be corrupt';
        fclose(fid);
        return;
    end
    
    if (strcmp(info.CompressionType, 'bitfields'))
        info.NumColormapEntries = 0;
        info.Colormap = [];
        
        info.RedMask = fread(fid, 1, 'uint32');
        info.GreenMask = fread(fid, 1, 'uint32');
        info.BlueMask = fread(fid, 1, 'uint32');
        
    else
        info.NumColormapEntries = (info.ImageDataOffset - ftell(fid))/4;
        if (rem(info.NumColormapEntries,1) ~= 0)
            info = [];
            msg= 'BMP Version 3 (Microsoft Windows NT) file appears to be corrupt';
            fclose(fid);
            return;
        end
        
        if (info.NumColormapEntries > 0)
            [map,count] = fread(fid, info.NumColormapEntries*4, 'uint8');
            if (count ~= info.NumColormapEntries*4)
                info = [];
                msg = 'Truncated colormap data';
                fclose(fid);
                return;
            end
            map = reshape(map, 4, info.NumColormapEntries);
            info.Colormap = double(flipud(map(1:3,:))')/255;
        end
        
    end

case 'Version 4 (Microsoft Windows 95)';
    
    info.BitmapSize = fread(fid, 1, 'uint32');
    info.HorzResolution = fread(fid, 1, 'int32');
    info.VertResolution = fread(fid, 1, 'int32');
    info.NumColorsUsed = fread(fid, 1, 'uint32');
    info.NumImportantColors = fread(fid, 1, 'uint32');
    
    % Fields added for Version 4
    info.RedMask = fread(fid, 1, 'uint32');
    info.GreenMask = fread(fid, 1, 'uint32');
    info.BlueMask = fread(fid, 1,  'uint32');
    info.AlphaMask = fread(fid, 1, 'uint32');
    info.ColorspaceType = fread(fid, 1, 'uint32');
    info.RedX = fread(fid, 1, 'int32');
    info.RedY = fread(fid, 1, 'int32');
    info.RedZ = fread(fid, 1, 'int32');
    info.GreenX = fread(fid, 1, 'int32');
    info.GreenY = fread(fid, 1, 'int32');
    info.GreenZ = fread(fid, 1, 'int32');
    info.BlueX = fread(fid, 1, 'int32');
    info.BlueY = fread(fid, 1, 'int32');
    info.BlueZ = fread(fid, 1, 'int32');
    info.GammaRed = fread(fid, 1, 'uint32');
    info.GammaGreen = fread(fid, 1, 'uint32');
    info.GammaBlue = fread(fid, 1, 'uint32');
    
    if (((info.BitDepth == 16) | (info.BitDepth == 32)) & ...
                (~strcmp(info.CompressionType,'bitfields')))
        info = [];
        msg= 'BMP Version 4 (Microsoft Windows 95) file appears to be corrupt';
        fclose(fid);
        return;
    end
    
    if (strcmp(info.CompressionType, 'bitfields'))
        info.NumColormapEntries = 0;
        info.Colormap = [];
        
    else
        info.NumColormapEntries = (info.ImageDataOffset - ftell(fid))/4;
        if (rem(info.NumColormapEntries,1) ~= 0)
            info = [];
            msg= 'BMP Version 4 (Microsoft Windows 95) file appears to be corrupt';
            fclose(fid);
            return;
        end
        
        if (info.NumColormapEntries > 0)
            [map,count] = fread(fid, info.NumColormapEntries*4, 'uint8');
            if (count ~= info.NumColormapEntries*4)
                info = [];
                msg = 'Truncated colormap data';
                fclose(fid);
                return;
            end
            map = reshape(map, 4, info.NumColormapEntries);
            info.Colormap = double(flipud(map(1:3,:))')/255;
        end
        
    end


case 'Version 2 (IBM OS/2 2.x)'
    
    info.BitmapSize = fread(fid, 1, 'uint32');
    info.HorzResolution = fread(fid, 1, 'uint32');
    info.VertResolution = fread(fid, 1, 'uint32');
    info.NumColorsUsed = fread(fid, 1, 'uint32');
    info.NumImportantColors = fread(fid, 1, 'uint32');
    units = fread(fid, 1, 'uint16');
    if (isempty(units))
        info = [];
        msg = 'Truncated header';
        fclose(fid);
        return;
    end
    if (units == 0)
        info.Units = 'pixels/meter';
    else
        info.Units = 'unknown';
    end
    fseek(fid, 2, 'cof');  % skip 2-byte pad
    info.Recording = fread(fid, 1, 'uint16');

    halftoning = fread(fid, 1, 'uint16');
    if (isempty(halftoning))
        info = [];
        msg = 'Truncated header';
        fclose(fid);
        return;
    end

    switch halftoning
    case 0
        info.HalftoningAlgorithm = 'none';
        
    case 1
        info.HalftoningAlgorithm = 'error diffusion';
        
    case 2
        info.HalftoningAlgorithm = 'PANDA';
        
    case 3
        info.HalftoningAlgorithm = 'super-circle';
        
    otherwise
        info.HalftoningAlgorithm = 'unknown';
        
    end
    
    info.HalftoneField1 = fread(fid, 1, 'uint32');
    info.HalftoneField2 = fread(fid, 1, 'uint32');
    
    encoding = fread(fid, 1, 'uint32');
    if (isempty(encoding))
        info = [];
        msg = 'Truncated header';
        fclose(fid);
        return;
    end

    if (encoding == 0)
        info.ColorEncoding = 'RGB';
    else
        info.ColorEncoding = 'unknown';
    end
    
    info.ApplicationIdentifier = fread(fid, 1, 'uint32');
    
    info.NumColormapEntries = (info.ImageDataOffset - ftell(fid))/4;
    if (rem(info.NumColormapEntries,1) ~= 0)
        info = [];
        msg= 'BMP Version 2 (IBM OS/2 2.x) file appears to be corrupt';
        fclose(fid);
        return;
    end
    
    if (info.NumColormapEntries > 0)
        map = fread(fid, info.NumColormapEntries*4, 'uint8');
        map = reshape(map, 4, info.NumColormapEntries);
        info.Colormap = double(flipud(map(1:3,:))')/255;
    end
    
otherwise
    info = [];
    msg = 'Problem identifying format version';
    fclose(fid);
    return;
    
end

fclose(fid);

if (isempty(info.NumColormapEntries))
    info.NumColormapEntries = 0;
end

if (info.NumColormapEntries > 0)
    info.ColorType = 'indexed';
    
else
    if (info.BitDepth <= 8)
        info.ColorType = 'grayscale';
    else
        info.ColorType = 'truecolor';
    end
end


%
% Other validity checks
%
if ((info.Width < 0) | (info.Height < 0))
    info = [];
    msg = 'Corrupt BMP file: bad image dimensions';
    return;
end
