关于delphi:Windows图元文件的尺寸是否受到限制?

 2021-04-26 

Is there a limitation on dimensions of Windows Metafiles?

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

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

以下是显示问题的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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.Enhanced设置为True并将其保存为.emf文件将解决此问题,但是我为其生成文件的目标应用程序不支持增强型图元文件。

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

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

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


您的限制是32767。

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

即使尺寸小于32767,"可放置的标头"也可能具有错误的值(请阅读Tom的答案以及答案注释中有关原因和含义的详细信息),但稍后会对此进行详细介绍...

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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知道,图元文件的尺寸可能会溢出。加载图像以使其正确显示后,您可以执行简单的健全性检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;

  ...


当文档无济于事时,请查看源代码:)。如果宽度或高度太大,则文件创建失败,并且文件无效。在下面的内容中,我仅查看水平尺寸,但垂直尺寸的处理方法相同。

在Vcl.Graphics中:

1
2
3
4
5
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文件:

1
2
3
4
5
6
7
8
9
10
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标头结构指示事物向南移动

1
2
3
4
5
6
7
8
  TMetafileHeader = record
    Key: Longint;
    Handle: SmallInt;
    Box: TSmallRect;  // smallint members
    Inch: Word;
    Reserved: Longint;
    CheckSum: Word;
  end;

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

问题在于:我可以在机器上使用什么尺寸?

FMetaFile.MMWidth和FMetaFile.MMHeight都必须小于或等于

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

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

1
2
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获得负值:

1
2
3
4
5
6
7
8
9
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);

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

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

DestRect的Right和Bottom

具有负值

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