Converting Jpeg images to Bmp - some images come out blue
Asked Answered
U

4

10

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.

Udo answered 18/1, 2012 at 18:37 Comment(9)
It seems a bug of the Delphi JPEG decoder implementation, I've had another issues with some jpeg images in the past too. My recomendation is use a component specific for jpeg images like NativeJpg v.1.30Ray
@CosminPrund FWIW ImageMagick is the tool of choice for this. It's a one-liner at the command line: for /f %f in (*.jpg) do @convert %f %~nf.bmpLavonna
Delphi does not detect all types of JPG images (CYMK, RGB are 2 of those) which results into a wrong color pallete upon conversion. I am afraid you will need something like NativeJPG from RRUZ's commentEbonyeboracum
The linked image is a JPG with no embedded color profile using the sRGB color space, at least that's what Gimp is telling me. If that's true, it's a very mundane JPG file. The fact that it renders blue in Delphi and green in PictureViewer tells me there's a subtle bug in the JPG file. I doubt the actual format is not supported by Delphi, it's more like there's a bug in the file and other decoders recover better then Delphi. Because of this I would not trust a smallish JPG decoder implementation, there's no guarantee it's more robust. I'd look for a wide spread library, nothing less.Merilynmeringue
Not only color palletes, but also 32 bit color depth fails here with D7 jpeg.Dilation
By the way, my final fix is actually to use NativeJpg. TsdJpegGraphic works like butter.Udo
I'd say the source of the problem is both Delphi and the Jpeg file. Delphi doesn't seem to have a way of properly handling unrecognized formats, whereas the File is, well, an unrecognized format. I'll have to blame the software FastStone Image Resizer for this one.Udo
I'm shooting myself in the foot for not discovering the TPicture much sooner...Udo
For the record, the file in the link above is no longer available.Udo
N
7

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.

Nicholnichola answered 19/1, 2012 at 3:13 Comment(13)
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?Rebbeccarebe
@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.Stadiometer
@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.Rebbeccarebe
@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.Nicholnichola
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?Udo
@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.Nicholnichola
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?Udo
+1 and accepted btw, very informative on the exact cause of the issue :DUdo
@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.Nicholnichola
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.Udo
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. */ Outline
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 }Outline
@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?Stadiometer
O
11

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
Outline answered 18/1, 2012 at 20:9 Comment(5)
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!Udo
@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.Outline
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.Nicholnichola
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?Udo
The (good) thing is that FireMonkey does not have the problem with the same file, it comes normally brown.Outline
L
8

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.

Lavonna answered 18/1, 2012 at 19:48 Comment(17)
Looking promising now, gotta get back to the office and give it a try, thanks!Udo
wow, nice one! +1 for you David. is available from XP: microsoft.com/download/en/details.aspx?id=32Ebonyeboracum
+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.Merilynmeringue
@CosminPrund TWICImage is in D2010 too I just discovered.Lavonna
HALLELUJAH! +5,000 and possibly a job, but that's not up to me :P Much thanks David!Udo
Very cool. Anybody know what happens if you run this on XP without installing WIC?Nicholnichola
@MarcusAdams Could not create COM object type of error would be my guessLavonna
@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.Nicholnichola
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...Udo
@jerry you can install WIC on XP but you may not want to. And you can always fall back on built inLavonna
+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.Rebbeccarebe
Download for XP is here: microsoft.com/download/en/details.aspx?displaylang=en&id=32Lavonna
@Ken Yes I think it's highly likely that XP machines that have been around a bit will already have it.Lavonna
It appears to be in the WinXP Virtual Mode installation in it's default configuration (well, with all updates as of last week, anyway).Rebbeccarebe
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.Udo
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.Outline
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.Comradery
N
7

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.

Nicholnichola answered 19/1, 2012 at 3:13 Comment(13)
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?Rebbeccarebe
@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.Stadiometer
@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.Rebbeccarebe
@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.Nicholnichola
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?Udo
@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.Nicholnichola
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?Udo
+1 and accepted btw, very informative on the exact cause of the issue :DUdo
@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.Nicholnichola
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.Udo
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. */ Outline
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 }Outline
@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?Stadiometer
M
1

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.
Merilynmeringue answered 20/1, 2012 at 8:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.