TJpegImage:应用 JPEG 压缩后内部位图未更新

TJpegImage: Internal bitmap not updated after applying JPEG compression

我想将 BMP 转换为 JPG,压缩该 JPG,然后将压缩后的 JPG 放回原始 BMP。 但是,它不会将压缩图像分配给 BMP。我总是将原始图像放入 BMP。

代码如下。为了查看压缩,我设置了 CompressionQuality = 1。这会破坏图像的文学效果。

function CompressBmp2RAM(InOutBMP: TBitmap): Integer;
VAR
   Stream: TMemoryStream;
   Jpg: TJPEGImage;
begin
 Jpg:= TJPEGImage.Create;
 Stream:= TMemoryStream.Create;
 TRY
   Jpg.Assign(InOutBMP);
   Jpg.CompressionQuality:= 1;  // highest compression, lowest quality 
   Jpg.Compress;
   Jpg.SaveToStream(Stream);
   //Stream.SaveToFile('c:\out.jpg'); <---- this gives the correct (heavily compressed) image
   Result:= tmpQStream.Size;
   InOutBMP.Assign(Jpg);
   //InOutBMP.SaveToFile('c:\out.bmp'); <---- this gives the uncompressed image
 FINALLY
   FreeAndNil(Stream);
   FreeAndNil(Jpg);
 END;
end;

我找到了解决方法,但我仍然想知道为什么上面代码中的 InOutBMP.Assign(Jpg) 不起作用。

   ...
   Stream.Position:= 0;
   Jpg.LoadFromStream(Stream);
   InOutBMP.Assign(Jpg);
   ...

对我来说这似乎是一个错误。 JPG 不知道数据被重新压缩,所以内部位图永远不会更新。应该有某种内部 "DirtyData"(或 "HasChanged")标志。

那么,官方对此的解决方案是什么? 让 JPG 从外部数据源(流)重新加载它自己的数据似乎是一个 hack/temporary 错误修复。

PS: Jpg.DIBNeeded 没用。

我刚刚检查了TJpegImage的代码,我在评论中发布的假设似乎是正确的。

TJpegImage 为表示保留一个内部 TBitmap。当您调用 DIBNeeded 时,此位图是基于 Jpeg 图像数据创建的。

GetBitmap(为 DIBNeeded 做腿部工作的私有函数)将首先检查位图是否已经分配,​​如果是则不会重复该过程。因此,仅调用 DIBNeeded 在您的情况下不起作用,因为您基本上可以保证已经拥有此缓存位图。

FreeBitmap 方法将释放内部位图,之后调用 DIBNeeded 将再次创建一个新位图。所以我认为你需要的顺序是:

Jpg.Compress; // Make sure the Jpeg compressed image data is up to date
Jpg.FreeBitmap; // Clear the internal cached bitmap
Jpg.DIBNeeded; // Optional, get a new bitmap. Will happen when you assign to TBitmap.

我之前也提到过JpegNeeded,但是那会做和DIBNeeded类似的事情:检查是否有数据,如果没有,调用Compress。所以你需要像你一样调用 Compress 来强制压缩。

PS:TBitmap(和bmp类似的文件格式),不太了解这种压缩,所以通过将它赋值回位图,你会得到缩小的图像质量 ,但图像 大小 。包括 PNG 在内的某些位图格式通过使用(除其他外)运行 长度编码 (RLE) 进行压缩,这意味着仅花费四个字节来表示 "And now, 54 times a pixel of this color!".这种压缩对于有很多 jpeg 伪像(压缩的 grainy/blurry 副作用)的图像效果不佳,因此压缩后的 Jpg 的 PNG 版本可能比原始的 PNG 版本大,即使原版的质量也更好。对于具有大面积相同颜色的图像尤其如此,例如屏幕截图和某些艺术品。

如果之前没有创建内部位图,内部 TJPEGIMage.GetBitmap 函数只会根据当前 jpeg 图像创建内部位图。

Assign Image A. Do not use Canvas.
Assign Image B. Use Canvas and see Image B.
Assign Image C. Use Canvas and see Image B.
Assign Image D. Use Canvas and see Image B.
etc.

这绝对是 TJPEGImage 中的错误。我正在使用 XE7,所以现在可能已经修复了。

为了始终获得正确的 JPEGImage.Canvas 位图,我的解决方法是在分配之前清除任何现有的内部位图。

TJPEGImage = class(Vcl.Imaging.jpeg.TJPEGImage)
public
  procedure Assign(Source: TPersistent); override;
end;

procedure TJPEGImage.Assign(Source: TPersistent);
begin
  FreeBitmap;
  inherited;
end;

当使用一个 TJPEGImage 处理大量不同的图像时,上面的代码很有帮助。只有少数图像,为每个图像创建一个新的 TJPEGImage 是可行的。