Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Converting Jpeg images to Bmp - some images come out blue

There are some Jpg images which Delphi doesn't seem to like. It appears to be specific with the files I'm loading. And the procedure is simple - a) load Jpg image to TJpegImage, b) Assign Jpg object to a TBitmap object, and c) Save and/or display Bmp image. For some reason, these pictures keep coming out with a blueish tint.

These images show perfectly anywhere and everywhere else I load them (windows picture viewer, paint, photoshop, etc.).

And what I'm doing is very simple...

procedure Load;
var
  J: TJpegImage;
  B: TBitmap;
begin
  J:= TJpegImage.Create;
  B:= TBitmap.Create;
  J.LoadFromFile('C:\SomeFile.jpg');
  B.Assign(J);
  //Either save or display `B` and it appears blueish at this point
....

I want to avoid getting any third party stuff as much as possible. This problem has existed in Delphi versions 7, 2010, and XE2. At least the TImage control in XE2 displays it properly (as opposed to the older two) but that doesn't matter if the TBitmap still doesn't work. What is wrong with this file? And/or, what is wrong with Delphi's rendering?

Added Info

I recently found out something about these images. When they came from the vendors (product pictures), they were in CMYK format. At that time, Delphi 7 didn't properly support these files (with access violations and bad images) so all the pictures were filtered through a converter to RGB color format. Many original images were also TIFF and were converted to JPG. So it appears that the software FastStone Image Resizer must not properly save these files when they go through. The blue image doesn't happen on all of them, just some random batches at a time. The software handles thousands of products, so there are thousands of possible pictures.

like image 997
Jerry Dodge Avatar asked Jan 18 '12 18:01

Jerry Dodge


4 Answers

I figured out the issue. It's most likely a bug in Delphi.

The provided image is a peculiar format for a JPEG file called Adobe JPEG. Probably the most peculiar thing about an Adobe JPEG is that it allows storing the image in RGB format, though it also allows other formats. Most JPEGs are JFIF or EXIF format, which do not use RGB.

When copying the RGB data, whatever Delphi's doing, it's reversing the red and blue data when it's loading it onto the canvas. It's loading it as BGR instead of RGB. This may be because Windows (24-bit and 32-bit) DIBs (BMPs) are stored in BGR format.

I'm guessing that the bug will appear in Delphi for any RGB JPEG. Since most JPEGs do not use RGB, the incidence of the bug is low. The easy fix, if you have the source to the JPEG unit, is to reverse the order when loading an RGB JPEG.

If you don't have the source, then continue on.

The Adobe JPEG specifies the order of the colors in a format like this (in Hex) 43 11 00 47 11 00 42 11 00 that looks like this in a hex editor R..G..B. If you reverse the R and B here via a Hex editor, it shows wrong in Windows, and right in Delphi.

To recognize an Adobe JPEG, the first four bytes are either (in Hex) FF D8 FF ED or FF D8 FF EE, with the ED and EE being the differentiating bytes. All JPEG files start with FF D8 FF.

After those bytes are two bytes that represent the length of the type marker, followed by (In ASCII) Adobe, followed by six more bytes (representing the version, etc.) and finally, (the 18th byte) is the byte that specifies the format. 0 means RGB. So, check for those significant bytes, and then act accordingly.

You'll have to reverse the RGB order in the file header (to lie to Delphi), or copy it to a TBitmap and use ScanLine to reverse the RGB to the proper order.

The format details are from reading the libJPEG source in C.

like image 105
Marcus Adams Avatar answered Nov 16 '22 05:11

Marcus Adams


The reason your file is blue is because the encoding is BGR isntead of RGB.
If you modify the jpeg.pas source file and use the pixel swapping (remove {.$IFDEF JPEGSO} in TJPEGImage.GetBitmap) you'll see your sample file correctly brown.

So, I guess the bottom line is that the stock jpeg source does not detect the correct (reverse) encoding; probably in jc.d.out_color_space...

Update:
The C source file (and jpeg.pas) should declare (and use) the Color Spaces with the new Extensions JCS_EXT_...:

enum J_COLOR_SPACE {
  JCS_UNKNOWN, JCS_GRAYSCALE, JCS_RGB, JCS_YCbCr,
  JCS_CMYK, JCS_YCCK, JCS_EXT_RGB, JCS_EXT_RGBX,
  JCS_EXT_BGR, JCS_EXT_BGRX, JCS_EXT_XBGR, JCS_EXT_XRGB
}

Update 2:
jpeg.pas can be found (XE) in C:...\RAD Studio\8.0\source\vcl with the C files in the jpg subfolder.

If you're ready to bet that all Adobe files with an RGB colorspace need to have their bits swapped, you can easily hack the jpeg.pas source to detect your special files and conditionnally do the swap mentioned above in TJPEGImage.GetBitmap

{.$IFDEF JPEGSO}
          if (jc.c.in_color_space=JCS_RGB)and
            (smallint(jc.c.jpeg_color_space)=Ord(JCS_UNKNOWN))and   //comes 1072693248 = $3FF00000 = 111111111100000000000000000000
            jc.d.saw_Adobe_marker  and
            (PixelFormat = jf24bit) then
          begin
like image 21
Francesca Avatar answered Nov 16 '22 06:11

Francesca


WIC (available for XP and up) can handle this image. This component is wrapped up nicely in Delphi 2010 and up. For earlier Delphi versions it is easy enough to call WIC using the COM interfaces.

Here's my proof of concept code:

var
  Image: TWICImage;
  Bitmap: TBitmap;
begin
  Image := TWICImage.Create;
  Image.LoadFromFile('C:\desktop\ABrownImage.jpg');
  Bitmap := TBitmap.Create;
  Bitmap.Assign(Image);
  Bitmap.SaveToFile('C:\desktop\ABrownImage.bmp');
end;

Note 1: WIC is delivered with Vista but has to be re-distributed for XP. One obvious option would be to use WIC if available, but fall back to the Delphi JPEG decoder otherwise.

Note 2: I can't find a re-distributable package for WIC. I suspect it may require end-user download for XP. That said I would not be at all surprised if the vast majority of XP machines had it installed by now.

like image 8
David Heffernan Avatar answered Nov 16 '22 07:11

David Heffernan


Prompted by your other question, here's some code to load a JPG file to a bitmap and conditionally apply a correction to the resulting bitmap. Please note this works for your Brown.JPG image, but I have no idea what's in those first 18 bytes, so I have no idea if this is going to work long-term or not. I'd personally prefer the use of ready-made, known-to-work, widely used library. Alternatively I'd use David's idea of using WIC if available and reverting to this style of hacky code if not available.

Here's the full unit code, so you can see all the used units. The form only expects a single TImage named Image1 on the form, so you can create your form first, put the TImage there, then switch to source code view and copy-paste my code over the Delphi-produced code.

How the code works:

The code opens the file with the JPG image, and loads it into a TJpgImage. It then compares the first 18 bytes of the file to a known marker. If there's a match it applies a transformation to each and every pixel of the produced bitmap. Because writing the actual marker constants is difficult there's a routine (CopyConstToClipboard) that takes the bytes from the file, transforms them into a Delphi-style constant and copies that to the clipboard. When you find a new file that doesn't work you should use this routine to prepare a new constant.

Actual code:

unit Unit9;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, Jpeg, Clipbrd;

type
  TForm9 = class(TForm)
    Image1: TImage;
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form9: TForm9;

implementation

{$R *.dfm}

type
  TRGB_Pixel = packed record
    B1: Byte;
    B2: Byte;
    B3: Byte;
  end;
  TScanLine = array[0..(System.MaxInt div SizeOf(TRGB_Pixel))-1] of TRGB_Pixel;
  PScanLine = ^TScanLine;

procedure CopyConstToClipboard(const FB:array of byte);
var s: string;
    i: Integer;
begin
  s := 'Name: array[0..' + IntToStr(High(FB)) + '] of Byte = ($' + IntToHex(FB[0], 2);
  for i:=1 to High(FB) do
    s := s + ', $' + IntToHex(FB[i],2);
  s := s + ');';
  Clipboard.AsText := s;
end;

function LoadJpegIntoBitmap(const FileName:string): TBitmap;
var F: TFileStream;
    Jpg: TJPEGImage;
    FirstBytes:array[0..17] of Byte;
    y,x: Integer;
    ScanLine: PScanLine;
const Marker_1: array[0..17] of Byte = ($FF, $D8, $FF, $EE, $00, $0E, $41, $64, $6F, $62, $65, $00, $64, $00, $00, $00, $00, $00);

  procedure SwapBytes(var A, B: Byte);
  var T: Byte;
  begin
    T := A;
    A := B;
    B := T;
  end;

begin
  F := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Jpg := TJPEGImage.Create;
    try
      Jpg.LoadFromStream(F);
      F.Position := 0;
      F.Read(FirstBytes, SizeOf(FirstBytes));

      // CopyConstToClipboard(FirstBytes); // Uncomment this to copy those first bytes to cliboard

      Result := TBitmap.Create;
      Result.Assign(Jpg);

      if (Result.PixelFormat = pf24bit) and CompareMem(@Marker_1, @FirstBytes, SizeOf(FirstBytes)) then
      begin
        for y:=0 to Result.Height-1 do
        begin
          ScanLine := Result.ScanLine[y];
          for x:=0 to Result.Width-1 do
          begin
            SwapBytes(ScanLine[x].B1, ScanLine[x].B3);
          end;
        end;
      end;

    finally Jpg.Free;
    end;
  finally F.Free;
  end;
end;

procedure TForm9.FormCreate(Sender: TObject);
var B: TBitmap;
begin
  B := LoadJpegIntoBitmap('C:\Users\Cosmin Prund\Downloads\ABrownImage.jpg');
  try
    Image1.Picture.Assign(B);
  finally B.Free;
  end;
end;

end.
like image 1
Cosmin Prund Avatar answered Nov 16 '22 05:11

Cosmin Prund