JPEG loading color difference FPReadJPEG lazarus / 39-Graphics ---------------------------------------------------------------------- ttomas · 2020-11-20 13:58 I'm developing image resize web service with BGRABitmap and my customers complain of color difference in resized images. There is not a problem in BGRA lib or resize. Source image is Jpeg and color difference is with loading with FPReadJPEG. I have the same problem with FPMemoryImage, just load image and save to bmp or jpeg, result image have color difference. For testing I load image with libjpegturbo decompressor and the image is also bed. Original jpeg image is displayed correctly in Chrome browser and MS Photos Viewer in W10. Loading same image with MS Paint, LazPaint, IrfanView display incorrect colors. Someone have this problem with jpeg image, any hint how to solve the problem. This is the link of original jpeg image http://halkyon.com/download/p11010_4org.jpg And this is the same image loaded and saved with Quality 100, same color diff if I save to bmp http://halkyon.com/download/06-Q100.jpg I can't change format of source images i suspect in format of source jpeg, but if browser and MS Photos Viewer can open correctly image why not FreePascal application. ---------------------------------------------------------------------- lainz · 2020-11-20 14:18 Hi, maybe it's the color profile. ---------------------------------------------------------------------- wp · 2020-11-20 14:43 Looking at the EXIF data it can be found that the original image contains a ColorSpace tag value of 65535 which means "uncalibrated". Most of the jpg images that I have looked at, however, have a ColorSpace of 1 which means sRGB (except for the images of a friend who is an ambitious photographer and usually works with raw format; here the ColorSpace is uncalibrated again). Please check the other images that your customers report to have false colors whether the ColorSpace is uncalibrated as well. Many image processing programs display EXIF (e.g. IrfanView mentioned by you). ---------------------------------------------------------------------- ttomas · 2020-11-20 15:18 Opening the image in Photoshop ask me to use custom color space or use default. Is there a embedded custom pallet color space in jpeg format? How to use this color space? What library use Chrome to read jpeg? @wp Yes all images are the same format (catalog of products for web shop) IrfanView have the same problem, displayed colors are not correct. Only browser Firefox, Chrome, Edge and MS Photo Viewer and Windows File Explorer thumbnail display correct colors. ---------------------------------------------------------------------- wp · 2020-11-20 16:13 I cannot answer these questions, maybe ask on the Lazarus Mailinglist where the dev who wrote the jpeg unit may be around (in fact, I don't know who he is...). Googling for "Delphi jpeg colorspace" I came across the NativeJpg library which looks very complete: https://www.simdesign.nl/nativejpg.html. Unfortunately it is a commercial license (although not expensive), but maybe you can contact the author and ask him to load your two images and check whether they look different. And whether his software works for Lazarus, of course. ---------------------------------------------------------------------- circular · 2020-11-20 16:37 If the colorspace is undefined, there may not be correct/incorrect colors. The second image seems darker, which is something you get when converting from sRGB to RGB. If you call ConvertFromLinearRGB function of TBGRABitmap, do you get the same colors as at the beginning? ---------------------------------------------------------------------- ttomas · 2020-11-20 18:03 Opening the image with Gimp on Manjaro display this dialog. Any options Keep or Convert to Adobe RGB or GEMP build in sRGB open image with correct colors. @circular My customers have the products and they see the difference in colors. If Gimp and other app can show the correct color must be the way for freepascal to load correct colors. ---------------------------------------------------------------------- circular · 2020-11-21 13:30 I don't think you understood what I said. Correct means that it is fully defined. The EXIF mentioned by wp seem to be undefined. Though from the Gimp dialog, it seems that the source colorspace is AdobeRGB colorspace. Doing the following gives something quite close. There is a little difference, maybe the color matrix for AdobeRGB isn't quite right. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i uses BGRABitmap, BGRABitmapTypes;var  bmp: TBGRABitmap;  p: PBGRAPixel;  i: Integer;begin  bmp := TBGRABitmap.Create(ExtractFilePath(Application.E xeName)+'p11010_4org.jpg');  p := bmp.Data;  for i := 0 to bmp.NbPixels-1 do  begin    p^ := TAdobeRGBA.New(p^.red, p^.green, p^.blue, p^.alpha);    inc(p);  end;  bmp.SaveToFile(ExtractFilePath(A pplication.ExeName)+'p11010_4org_fromadobergb.jpg');  bmp.Free;end; So if it is possible to detect that the file is in AdobeRGB, that could be a way to fix it. ---------------------------------------------------------------------- ttomas · 2020-11-21 15:13 @circular Thanks a lot, I don't know of this TAdobeRGBA function. Source image is in Adobe RGB 98 profile. This info is in ICC Profile in jpeg and now I need a way to read this info from jpeg header. Delphi/FPC jpeg reader don't read this info. Just to inform LazPaint also need to check profile of jpeg image and correct colors when editing like Gimp, Photoshop, etc. Any Adobe RGB profile jpeg is loaded incorrect. ---------------------------------------------------------------------- circular · 2020-11-21 17:51 I am interested as well if someone finds a way to identify this. I could then add a patch in BGRABitmap (and so to LazPaint). ---------------------------------------------------------------------- ttomas · 2020-11-21 20:53 I research jpeg icc data and find this nice tool https://github.com/wp-xyz/exif_spy It is sample in https://github.com/cutec-chris/dexif All in Lazarus If you build exifSpy you can look at APP2 marker, if exists inside you have ICC_PROFILE. Look at FindMarker function. APP2 marker is $E2 and you need to search for $FF$E2 in jpeg file. Next in stream you have 2 bytes for length of ICC_PROFILE and you can extract icc_profile from jpeg. Now next step is to parse and read icc_profile. ---------------------------------------------------------------------- circular · 2020-11-22 12:24 What does this ICC profile look like? Is is text or binary? ---------------------------------------------------------------------- lucamar · 2020-11-22 12:53 Easy to ascertain: ICC Profile Format Specification ---------------------------------------------------------------------- ttomas · 2020-11-22 22:31 I can now detect Adobe RGB (1998) jpeg image and apply color correction. TAdobeRGBA.New is not ideal there is still color difference. I search for jpeg markers to find APP2 marker. Inside is ICC_PROFILE and pars icc tags for desc (description) tag to find "Adobe RGB (1998)". This is just detection of adobe rgb image. Real implementation will be to read icc_profile and convert image to sRGB using embedded profile as default. If no profile exist in jpeg sRGB is default. I attach source for loading jpeg and auto correct adobe rgb. There is $define to enable libjpeg-turbo for loading. Did someone use libjpeg-turbo in multithreaded app? I need to use this on ubuntu server with mORMot method based service. Thanks all for your help! Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i unit hkJpegLoad; {.$define libjpeg-turbo} {$mode objfpc}{$H+} interface uses  Classes, SysUtils,  {$ifdef libjpeg- turbo}  turbojpeg, ctypes,  {$else}  FPReadJPEG,  {$EndIf}  BGRABitmap,  BGRABitmapTypes; function LoadJpegFromFile(const aFileName: string): TBGRABitmap;function LoadJpegFromMemStream(aStream: TMemoryStream): TBGRABitmap; implementation function IsAdobeRGB(aStream: TMemoryStream): boolean;var pCnt, pSize: Int64;    p, picc, pdesc: PByte;    pw: PWord;    pdw: PDWord;    len, offset, tagcount: integer;    pFoundApp2: boolean;    s, sdesc: ansistring;begin  Result := False;  // Find APP2 marker $FF $E2   // Check valid Jpeg file must start with $FF $D8  p := aStream.Memory;  if p^ <> $FF then exit;  inc(p);  if p^ <> $D8 then exit;  pFoundApp2 := False;  pCnt := 2;  pSize := aStream.Size;  inc(p);  while (pCnt < pSize) do begin    if p^ <> $FF then       exit;    inc(p);    if p^ = $DA then begin       // $DA SOS marker stop loop, app2 not found       exit;    end;    if p^ = $E2 then begin       pFoundApp2 := True;       break;    end;    inc(p);    pw := PWord(p);    len := BEToN(pw^);    inc(p, len);    pCnt := pCnt + 2 + len;  end;  if not pFoundApp2 then     exit;  inc(p);  pw := PWord(p);  len := BEToN(pw^);  // length of app2 data  inc(p,2); // point to ICC_PROFILE#0 12-bytes  inc(p, 12);  // implemented only 65535 length icc_profile  if p^<>1 then     exit;  // first chunk  inc(p);  if p^<>1 then     exit;  // total num of chunks  inc(p);  pdw := PDWord(p);  len := BEToN(pdw^);  // Now we have ICC_Profile pointing in p with length=len  picc := p;  // icc_parsing looking for description tag desc with value "Adobe RGB (1998)"  inc(p, $80); // go to tag count in tag table  pdw := PDWord(p);  tagcount := BEToN(pdw^); // Number of tags;  inc(p,4);  SetLength(s, 4);  while tagcount>0 do begin    Move(p^, s[1], 4);    if s='desc' then begin     inc(p, 4);       pdw := PDWord(p);       offset := BEToN(pdw^);     inc(p, 4);       pdw := PDWord(p);       len := BEToN(pdw^);       pdesc := picc + offset;       inc(pdesc, 8); // skip desc and 4 $0     pdw := PDWord(pdesc);       len := BEToN(pdw^);       inc(pdesc, 4);       SetLength(sdesc, len);       Move(pdesc^, sdesc[1], len);     if AnsiStrComp(PChar(sdesc), PChar('Adobe RGB (1998)'))=0 then begin          Result := True;          break;       end;    end;    inc(p, 12);    dec(tagcount);  end;end; function LoadJpegFromFile(const aFileName: string): TBGRABitmap;var ms: TMemoryStream;begin  ms := TMemoryStream.Create;  try    ms.LoadFromFile(aFileName);    Result := LoadJpegFromMemStream(ms);  finally    ms.Free;  end;end; function LoadJpegFromMemStream(aStream: TMemoryStream): TBGRABitmap;var b: TBGRABitmap;    p: PBGRAPixel;    i: Integer;   {$ifdef libjpeg-turbo}  function LoadTurbo: TBGRABitmap;  var jpegDecompressor: tjhandle;      jpegWidth,jpegHeight:  cint;      jpegSubSamp:           cint;  begin    jpegDecompressor := tjInitDecompress();    tjDecompressHeader2(jpegDecompressor, PCUChar(aStream.Memory), aStream.Size, @jpegWidth, @jpegHeight, @jpegSubsamp);    Result := TBGRABitmap.Create(jpegWidth, jpegHeight);    tjDecompress2(jpegDecompressor, PCUChar(aStream.Memory), aStream.Size, pcuchar(Result.Data), jpegWidth, 0, jpegHeight, cint(TJPF_BGRA), {TJFLAG_FASTDCT}TJFLAG_ACCURATEDCT);    tjDestroy(jpegDecompressor);  end;  {$else}  function LoadBGRA: TBGRABitmap;  var pReader: TFPReaderJPEG;  begin    pReader := TFPReaderJPEG.Create;    try      pReader.Performance:=jpBestQuality;      Result := TBGRABitmap.Create;      Result.LoadFromStream(aStream, pReader, []);    finally      pReader.Free;    end;  end;  {$endif}begin  {$ifdef libjpeg-turbo}  b := LoadTurbo;  {$else}  b := LoadBGRA;  {$EndIf}  if IsAdobeRGB(aStream) then begin    p := b.Data;    for i := 0 to b.NbPixels-1 do    begin      p^ := TAdobeRGBA.New(p^.red, p^.green, p^.blue);      inc(p);    end;  end;  Result := b;end; end. ---------------------------------------------------------------------- circular · 2020-11-22 23:15 Cool. That's a good starting point.  If the color profile is just applied to the RGBA byte values, this can be an opportunity to fix TAdobeRGBA to actually match the colorspace, as that's what it is supposed to be. I've used matrices that are almost the same (to 3 or 4 significant digits) as the one in Adobe documentation. Same thing for the gamma value. See AdobeRGBAToXYZA function in extendedcolorspaces.inc. There is probably something else to take into account. Changing the reference white to D65 doesn't do much either even though that's probably better as it avoids a chromatic adaptation: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i SetReferenceWhite(ReferenceWhite2D65); //before using TAdobeRGBA ---------------------------------------------------------------------- ttomas · 2020-11-23 08:52 @circular Yes i play with AdobeRGBAToXYZA. Is StdRGB same as sRGB? I think we need AdobeRGBA -> XYZA -> sRGB transformation. In extendedcolorspaces.inc is missing function XYZAToStdRGBA. I copy code from XYZAToAdobeRGBA to XYZAToStdRGBA and change matrix for sRGB from http://brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html Then try p^ := XYZAToStdRGBA(AdobeRGBAToXYZA(p^)).ToBGRAPixel; Result is corrupted image, need some help. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i function XYZAToStdRGBA(const AXYZA: TXYZA; const AReferenceWhite: TXYZReferenceWhite): TStdRGBA;var R,G,B: single;  ad: TXYZA;begin  ad := AXYZA;  if AReferenceWhite.Illuminant = 'D50' then  begin    ad.ChromaticAdapt(AReferenceWhite, ReferenceWhite2D50);    with ad do    begin      R := Clamp(3.1338561*X - 1.6168667*Y - 0.4906146*Z,0,1);      G := Clamp(-0.9787684*X + 1.9161415*Y + 0.0334540*Z,0,1);      B := Clamp(0.0719453*X - 0.2289914*Y + 1.4052427*Z,0,1);    end;  end else  begin    ad.ChromaticAdapt(AReferenceWhite, ReferenceWhite2D65);    with ad do    begin      R := Clamp(3.2404542*X - 1.5371385*Y - 0.4985314*Z,0,1);      G := Clamp(-0.9692660*X + 1.8760108*Y + 0.0415560*Z,0,1);      B := Clamp(0.0556434*X - 0.2040259*Y + 1.0572252*Z,0,1);    end;  end;  result.red := GammaCompressionTab[round(R*65535)];  result.green := GammaCompressionTab[round(G*65535)];  result.blue := GammaCompressionTab[round(B*65535)];  result.alpha := ClampInt(round(AXYZA.alpha*255),0,255);end; ---------------------------------------------------------------------- ttomas · 2020-11-23 09:51 I change result to Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i   result.red := R;  result.green := G;  result.blue := B;  Now I have correct image, but colors are same as using TAdobeRGBA.New(p^.red, p^.green, p^.blue); ---------------------------------------------------------------------- circular · 2020-11-23 13:35 Yes StdRGB stands for sRGB (s means standard). Ok so you find the same result. ---------------------------------------------------------------------- ttomas · 2020-11-23 15:27 Yes without correction just open and save image result is noticeable lot darker. TAdobeRGBA and XYZAToStdRGBA(AdobeRGBAToXYZA(p^)) visually have same result, little lighter then original. Maybe client will not notice :-). ---------------------------------------------------------------------- torumyax · 2021-01-18 04:16 I'm also interested in this since I developed an image viewer. Recently I noticed a color difference too. When I display a image in a browser or some other windows viewer developed in C#, it looks a lot different. After some googling, I learned about the ICC profile. Any progress? Thank you!