13

我正在创建一些 .wmf 文件,但其中一些似乎已损坏并且无法在任何图元文件查看器中显示。经过反复试验,我发现问题是由它们的尺寸引起的。如果我按一个因子缩放相同的图形以减小尺寸,它将显示出来。

现在,我想知道绘图的大小是否有限制,或者问题是否出在其他地方。我知道这些文件有一个 16 位的数据结构,所以我猜每个维度的限制是 2^16 个单位,(如果有符号,则为 2^15 个单位)。但在我的测试中,它大约是 25,000。所以我不能依赖这个值,因为限制可以是任何东西(宽度*高度可能,或者绘图的分辨率可能会影响它)。我找不到有关描述此内容的 .wmf 文件的可靠资源。

这是显示问题的示例代码:

procedure DrawWMF(const Rect: TRect; const Scale: Double; FileName: string);
var
  Metafile: TMetafile;
  Canvas: TMetafileCanvas;
  W, H: Integer;
begin
  W := Round(Rect.Width * Scale);
  H := Round(Rect.Height * Scale);

  Metafile := TMetafile.Create;
  Metafile.SetSize(W, H);

  Canvas := TMetafileCanvas.Create(Metafile, 0);
  Canvas.LineTo(W, H);
  Canvas.Free;

  Metafile.SaveToFile(FileName);
  Metafile.Free;
end;

procedure TForm1.Button1Click(Sender: TObject);
const
  Dim = 40000;
begin
  DrawWMF(Rect(0, 0, Dim, Dim), 1.0, 'Original.wmf');
  DrawWMF(Rect(0, 0, Dim, Dim), 0.5, 'Scaled.wmf');

  try
    Image1.Picture.LoadFromFile('Original.wmf');
  except
    Image1.Picture.Assign(nil);
  end;

  try
    Image2.Picture.LoadFromFile('Scaled.wmf');
  except
    Image2.Picture.Assign(nil);
  end;
end;

PS:我知道将其设置Metafile.EnhancedTrue保存为 .emf 文件可以解决问题,但我为其生成文件的目标应用程序不支持增强型元文件。

编辑: 正如下面的答案中提到的,这里有两个不同的问题:

主要问题是关于文件本身,它对每个维度都有 2^15 的限制。如果绘图的宽度或高度超过此值,delphi 将写入损坏的文件。您可以在Sertac 的回答中找到更多详细信息。

第二个问题是关于将文件加载到TImage. 当您想在 delphi VCL 应用程序中显示图像时,还有另一个限制。这是系统相关的,并且与将要在其上绘制绘图的 DC 的 dpi 相关。汤姆的回答详细描述了这一点。传递 0.7ScaleDrawWMF(上面的代码示例)在我的 PC 上重现了这种情况。生成的文件没问题,可以用其他图元文件查看器(我使用 MS Office 图片管理器)查看,但 VCL 无法显示它,但是,在加载文件时不会引发异常。

4

2 回答 2

8

您的限制是 32767。

跟踪 VCL 代码,输出文件在TMetafile.WriteWMFStream. VCL 写入WmfPlaceableFileHeaderTMetafileHeader在 VCL 中)记录,然后调用GetWinMetaFileBits将 'emf' 记录转换为 'wmf' 记录。如果边界矩形的任何尺寸(在调用时使用CreateEnhMetaFile)大于 32767,则此函数将失败。不检查返回值,VCL 不会引发任何异常并关闭仅 22 个字节的文件 - 只有“可放置标题” .

即使对于小于 32767 的尺寸,“可放置标题”也可能具有错误的值(从汤姆的答案和对答案的评论中阅读有关原因和含义的详细信息),但稍后会详细介绍...

我使用下面的代码来找到限制。请注意,GetWinMetaFileBits在 VCL 代码中不会使用增强的图元文件来调用它。

function IsDimOverLimit(W, H: Integer): Boolean;
var
  Metafile: TMetafile;
  RefDC: HDC;
begin
  Metafile := TMetafile.Create;
  Metafile.SetSize(W, H);
  RefDC := GetDC(0);
  TMetafileCanvas.Create(Metafile, RefDC).Free;
  Result := GetWinMetaFileBits(MetaFile.Handle, 0, nil, MM_ANISOTROPIC, RefDC) > 0;
  ReleaseDC(0, RefDC);
  Metafile.Free;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  i: Integer;
begin
  for i := 20000 to 40000 do
    if not IsDimOverLimit(100, i) then begin
      ShowMessage(SysErrorMessage(GetLastError)); // ReleaseDc and freeing meta file does not set any last error
      Break;
    end;
end;

错误是 534(“算术结果超过 32 位”)。显然有一些有符号整数溢出。某些“mf3216.dll”(“32 位到 16 位元文件转换 DLL”)在调用GetWinMetaFileBits其导出ConvertEmfToWmf函数期间设置错误,但这不会导致任何有关溢出的文档。我能找到的关于 wmf 限制的唯一官方文档是这个(它的要点是“仅在 16 位可执行文件中使用 wmf” :))。


如前所述,虚假的“可放置标头”结构可能具有“虚假”值,这可能会阻止 VCL 正确播放元文件。具体来说,VCL 所知道的元文件的维度可能会溢出。您可以在加载图像以使其正确显示后执行简单的完整性检查:

var
  Header: TEnhMetaHeader;
begin
  DrawWMF(Rect(0, 0, Dim, Dim), 1.0, 'Original.wmf');
  DrawWMF(Rect(0, 0, Dim, Dim), 0.5, 'Scaled.wmf');

  try
    Image1.Picture.LoadFromFile('Original.wmf');
    if (TMetafile(Image1.Picture.Graphic).Width < 0) or
        (TMetafile(Image1.Picture.Graphic).Height < 0) then begin
      GetEnhMetaFileHeader(TMetafile(Image1.Picture.Graphic).Handle,
          SizeOf(Header), @Header);
      TMetafile(Image1.Picture.Graphic).Width := MulDiv(Header.rclFrame.Right,
          Header.szlDevice.cx, Header.szlMillimeters.cx * 100);
      TMetafile(Image1.Picture.Graphic).Height := MulDiv(Header.rclFrame.Bottom,
          Header.szlDevice.cy, Header.szlMillimeters.cy * 100);
  end;

  ...
于 2015-12-15T18:02:15.747 回答
1

当文档没有帮助时,请查看源代码:)。如果宽度或高度太大,则文件创建失败,并且文件无效。下面我只看水平尺寸,但垂直尺寸的处理方式相同。

在 Vcl.Graphics 中:

constructor TMetafileCanvas.CreateWithComment(AMetafile : TMetafile;
  ReferenceDevice: HDC; const CreatedBy, Description: String);

        FMetafile.MMWidth := MulDiv(FMetafile.Width,
          GetDeviceCaps(RefDC, HORZSIZE) * 100, GetDeviceCaps(RefDC, HORZRES));

如果ReferenceDevice未定义,则使用屏幕 ( GetDC(0))。在我的机器上,水平尺寸报告为 677,水平分辨率报告为 1920。因此FMetafile.MMWidth := 40000 * 67700 div 1920 ( = 1410416)。由于FMetaFile.MMWidth是整数,此时没有问题。

接下来,我们看一下文件写入,WriteWMFStream因为我们写入一个.wmf文件,所以完成了:

procedure TMetafile.WriteWMFStream(Stream: TStream);
var
  WMF: TMetafileHeader;
  ...
begin
  ...
        Inch := 96          { WMF defaults to 96 units per inch }
  ...
        Right := MulDiv(FWidth, WMF.Inch, HundredthMMPerInch);
  ...

WMF 标头结构表明事情的发展方向

  TMetafileHeader = record
    Key: Longint;
    Handle: SmallInt;
    Box: TSmallRect;  // smallint members
    Inch: Word;
    Reserved: Longint;
    CheckSum: Word;
  end;

Box: TSmallRect字段不能容纳比smallint-sized 值更大的坐标。右计算为Right := 1410417 * 96 div 2540 ( = 53307 as smallint= -12229)。图像尺寸溢出,无法将 wmf 数据“播放”到文件中。

问题很重要:我的机器可以使用哪些尺寸?

FMetaFile.MMWidth 和 FMetaFile.MMHeight 都需要小于或等于

MaxSmallInt * HundredthMMPerInch div UnitsPerInch or
32767 * 2540 div 96 = 866960

在我的测试机器上,水平显示尺寸和分辨率分别为 677 和 1920。垂直显示尺寸和分辨率分别为 381 和 1080。因此图元文件的最大尺寸变为:

Horizontal: 866960 * 1920 div 67700 = 24587
Vertical:   866960 * 1080 div 38100 = 24575

通过测试验证。


受评论启发进一步调查后更新:

水平和垂直维度高达 32767,元文件可通过某些应用程序 f.ex 读取。GIMP,它显示图像。这可能是由于那些程序将绘图的范围视为word而不是SmallInt. GIMP 报告每英寸像素为 90,当更改为 96(这是 Delphi 使用的值时,GIMP 因“GIMP 消息:插件崩溃:“file-wmf.exe”而崩溃。

OP 中的过程不显示尺寸为 32767 或更小的错误消息。但是,如果任一尺寸高于先前给出的计算最大值,则不会显示图纸。读取元文件时,使用与保存时相同的 TMetafileHeader 结构类型,FWidthFHeight获得负值:

procedure TMetafile.ReadWMFStream(Stream: TStream; Length: Longint);
  ...
    FWidth := MulDiv(WMF.Box.Right - WMF.Box.Left, HundredthMMPerInch, WMF.Inch);
    FHeight := MulDiv(WMF.Box.Bottom - WMF.Box.Top, HundredthMMPerInch, WMF.Inch);

procedure TImage.PictureChanged(Sender: TObject);

  if AutoSize and (Picture.Width > 0) and (Picture.Height > 0) then
    SetBounds(Left, Top, Picture.Width, Picture.Height);

负值会波及函数中的Paint过程,DestRect因此看不到图像。

procedure TImage.Paint;
  ...
      with inherited Canvas do
        StretchDraw(DestRect, Picture.Graphic);

DestRect 的 Right 和 Bottom 具有负值

我认为找到实际限制的唯一方法是同时调用GetDeviceCaps()水平和垂直尺寸和分辨率,并执行上述计算。但是请注意,该文件可能仍然无法在另一台机器上的 Delphi 程序中显示。将图纸尺寸保持在 20000 x 20000 以内可能是一个安全的限制。

于 2015-12-15T12:09:48.050 回答