Converting Jpeg images to Bmp - some images come out blue

13,181

Solution 1

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.

Solution 2

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

Solution 3

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.

Solution 4

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.
Share:
13,181
Jerry Dodge
Author by

Jerry Dodge

I'm a Delphi developer. I work for a software company which does solutions for retail management, including inventory, POS, reporting, BI, Tags, and more. It's been in Delphi since Delphi's been around. I am actively in Stack Overflow monitoring the Delphi tag, and looking for those questions I can answer and also contributing my time to keep Stack Overflow in order. I'm not an expert in anything, a jack of all trades rather. But I love to help people when I'm able to. I've known Delphi since about 2007 now, and before that, I had learned VB6. I havn't gone back to VB since I learned Delphi. I also taught myself QBasic and HTML as a kid. It hasn't been until the past 5 years that I've been diving into programming. Since then I've also become vaguely familiar with ASP.NET with C#, as well as some C# windows apps. But I'm not too fond of the whole .NET idea. .NET is good for web platforms and such, but not for win apps. My latest work has been with Delphi 10 Seattle mobile development. I'm still very raw on the subject, but see a huge potential behind it. My strengths: Understanding the bigger picture of projects Writing Custom Classes, Components, and Controls Code organization (within unit or namespace) Writing purely independent classes (as opposed to cross-referencing units or namespaces) User Friendly UI's Developer Friendly Classes Encapsulating layers of business logic My weaknesses: Lower-level coding (such as Assembly) Platform-specific design (using Firemonkey) Web Design It's always nice to know you're able to do something, even if you never use it.

Updated on June 09, 2022

Comments

  • Jerry Dodge
    Jerry Dodge about 2 years

    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.

  • Jerry Dodge
    Jerry Dodge over 12 years
    Looking promising now, gotta get back to the office and give it a try, thanks!
  • whosrdaddy
    whosrdaddy over 12 years
    wow, nice one! +1 for you David. is available from XP: microsoft.com/download/en/details.aspx?id=32
  • Cosmin Prund
    Cosmin Prund over 12 years
    +1. I was installing Delphi XE2 on my home computer right now to give this a try. If that works, it uses the same JPG engine as the default picture viewer in Windows. It no longer matters if it's the correct behavior or not, it's the expected behavior and that's perfect.
  • David Heffernan
    David Heffernan over 12 years
    @CosminPrund TWICImage is in D2010 too I just discovered.
  • Jerry Dodge
    Jerry Dodge over 12 years
    HALLELUJAH! +5,000 and possibly a job, but that's not up to me :P Much thanks David!
  • Jerry Dodge
    Jerry Dodge over 12 years
    Superb discovery, +1 :D Although the TWICImage is something I've been dreaming of for years, and finally found it thanks to David's answer above. Thanks for the valuable tip though!
  • Francesca
    Francesca over 12 years
    @David. I think so too, but somehow I'm not sure it'll have much effect... Unless it can be a problem on the FireMonkey side of things.
  • Marcus Adams
    Marcus Adams over 12 years
    Very cool. Anybody know what happens if you run this on XP without installing WIC?
  • David Heffernan
    David Heffernan over 12 years
    @MarcusAdams Could not create COM object type of error would be my guess
  • Marcus Adams
    Marcus Adams over 12 years
    @David, I remembered I had an XP VM, so I just tried your code on Windows XP SP2, and I got an access violation. So, you'll definitely need to fall back on an exception and test on XP.
  • Jerry Dodge
    Jerry Dodge over 12 years
    Had to unaccept because of XP :( At least until I get it to successfully work on XP anyway - or unless there's another answer which can be even better...
  • David Heffernan
    David Heffernan over 12 years
    @jerry you can install WIC on XP but you may not want to. And you can always fall back on built in
  • Ken White
    Ken White over 12 years
    +1. Just as an extra comment, Vcl.Graphics.pas in XE2 says "Requires Windows XP SP2 with .NET 3.0.", so it should work there as well with no other installation requirements.
  • David Heffernan
    David Heffernan over 12 years
  • David Heffernan
    David Heffernan over 12 years
    @Ken Yes I think it's highly likely that XP machines that have been around a bit will already have it.
  • Ken White
    Ken White over 12 years
    It appears to be in the WinXP Virtual Mode installation in it's default configuration (well, with all updates as of last week, anyway).
  • Marcus Adams
    Marcus Adams over 12 years
    Where do we get jpeg.pas? Also, I think this might be backwards. The image is an Adobe JPEG, which is RGB. Probably Delphi is expecting BGR.
  • Ken White
    Ken White over 12 years
    Not disagreeing, but curious... How do you decide that a non-standard format (in your own words, "peculiar thing") by Adobe constitutes a bug in Delphi? If "most JPEGs are JFIF or EXIF", and Adobe does something different, doesn't that mean there's a bug in the Adobe JPEGs instead?
  • onemasse
    onemasse over 12 years
    @Ken White The JPEG standard doesn't specify color format, so someone came up with the JFIF standard that most JPEG's are compliant with. Adobe JPEG's although ugly are still compliant with the JPEG standard.
  • Ken White
    Ken White over 12 years
    @onemasse, that might mean there's no bug in Adobe's implementation just because it's not typical, but it also doesn't mean that there is a bug in Delphi because Adobe's non-typical format is not supported; that's the point I was making.
  • Marcus Adams
    Marcus Adams over 12 years
    @Ken, to clarify, in addition to onemasse's response, I'm guessing that all RGB JPEGs will experience the bug, Adobe or not (I don't have the source code for jpeg.pas to confirm). It would be odd to implement RGB JPEGs at all and then to implement it backwards because as of today, according to libJPEG, only Adobe JPEGs support RGB. It's not like Delphi's attempting CMYK, YCrCb, etc. decoding on it and failing. It's definitely attempting RGB, only backwards. If you want to downgrade this to say Delphi doesn't support the Adobe JPEG standard, I'm cool with that.
  • Jerry Dodge
    Jerry Dodge over 12 years
    So yes, the WIC image was in fact already in XP (virtual mode). Problem is, even the TWICImage in XP is making them blue... Whereas my exact same project with same image do work fine in Vista and 7. I'm about to just build a utility to convert all these 700+ pictures to a standard format again.
  • Jerry Dodge
    Jerry Dodge over 12 years
    So finally in the end - my solution will be to convert these poor images to a more standard format. How to accomplish? I understand the concept of what you're saying clearly, but how to implement that fix (pixel conversion) in the programming world?
  • Marcus Adams
    Marcus Adams over 12 years
    @Jerry, I hope that you'll create a new question and provide more details. Is the Adobe JPEG in a file, in memory, etc.? If you haven't created a new question by time I can answer this, I'll try adding some tips to my answer.
  • Jerry Dodge
    Jerry Dodge over 12 years
    I mean how to programatically determine whether I need to swap bytes or not? I got the byte swapping down (although sluggish) but how to know if I have to do it or not?
  • Jerry Dodge
    Jerry Dodge over 12 years
    +1 and accepted btw, very informative on the exact cause of the issue :D
  • Marcus Adams
    Marcus Adams over 12 years
    @Jerry, if the first four bytes of the file are FF D8 FF ED or FF D8 FF EE, and the 18th byte is 0, then you'll need to swap.
  • Jerry Dodge
    Jerry Dodge over 12 years
    Understood, just I have never ever worked with reading raw bytes from the header of a jpeg, nor anything close, so I was hoping for a code sample and how to get that far to be able to read these bytes in the first place.
  • Francesca
    Francesca over 12 years
    I would disagree that the bug is in Delphi, as the QuickTime PictureViewer has also a problem with the same files. OTOH, Adobe has been so wellknown to take liberties with the stanbdard that you have special flags in libJPEG to signal ADOBE files and this comment:/* Unfortunately, some bozo at Adobe saw no reason to be bound by the standard; * the PostScript DCT filter can emit files with many more than 10 blocks/MCU.[...] * we strongly discourage changing C_MAX_BLOCKS_IN_MCU; just because Adobe * sometimes emits noncompliant files doesn't mean you should too. */
  • Francesca
    Francesca over 12 years
    the special flag for Adobe are: boolean write_Adobe_marker; /* should an Adobe marker be written? */, boolean saw_Adobe_marker; /* TRUE iff an Adobe APP14 marker was found */ and UINT8 Adobe_transform; /* Color transform code from Adobe marker */. On Delphi side in jpeg.pas: jpeg_compress_struct.write_Adobe_marker : LongBool; { should an Adobe marker be written? }, jpeg_decompress_struct.saw_Adobe_marker : LongBool; { TRUE iff an Adobe APP14 marker was found } and jpeg_decompress_struct.Adobe_transform : UINT8; { Color transform code from Adobe marker }
  • onemasse
    onemasse over 12 years
    @François It would be interesting to know what color format that would result in more than 10 du's per mcu? CMYK with a subsampled alpha channel? Can you have more than 4 channels?
  • Francesca
    Francesca over 12 years
    So I guess it proves this is not a Delphi bug "per se" but a missing feature in not implementing the "Adobe interpretation" of the standard that it should handle now.
  • Jerry Dodge
    Jerry Dodge over 12 years
    I will consider this and try it if you can let me know how I can hack the Jpeg unit in Delphi XE2. It doesn't exist there in the Source - inaccessible. Maybe they knew it had problems and didn't want people to hack it and change it around?
  • Francesca
    Francesca over 12 years
    The (good) thing is that FireMonkey does not have the problem with the same file, it comes normally brown.
  • Celal Ergün
    Celal Ergün about 12 years
    There is a serious speed difference. We used to create jpeg image lists, decode the image, draw on screen. Our clients started to whine about performance issues. It became intolerably slow after some updates (I don't exactly know which one). This TWICImage is superb. I think they (EMBT) messed up things to be more compatible. They just screwed up performance.