Search code examples
delphiwinapiwmf

Is there a limitation on dimensions of Windows Metafiles?


I'm creating some .wmf files, but some of them seem corrupted and can't be shown in any metafile viewer. After some trial and error, I found that the problem is caused by their dimensions. If I scale the same drawing by a factor to reduce the dimensions, it will be shown.

Now, I want to know if there's a limitation on the size of drawing or if the problem is something else. I know that these files have a 16-bit data structure, so I guess that the limitation would be 2^16 units in each dimension, (or 2^15 if it's signed). But in my tests it is around 25,000. So I can't rely on this value since the limitation can be on anything (Width*Height maybe, or maybe the resolution of the drawing may affect it). I can't find a reliable resource about .wmf files that describes this.

Here is sample code that shows the problem:

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: I know that setting Metafile.Enhanced to True and saving it as an .emf file will solve the problem, but the destination application that I'm generating files for doesn't support Enhanced Metafiles.

Edit: As mentioned in answers below, there are two different problems here:

The main problem is about the file itself, it has a 2^15 limitation on each dimension. If either width or height of the drawing overpasses this value, delphi will write a corrupted file. You can find more details in Sertac's answer.

The second problem is about loading the file in a TImage. There's another limitation when you want to show the image in a delphi VCL application. This one is system dependent and is related to dpi of DC that the drawing is going to be painted on. Tom's answer describes this in details. Passing 0.7 as Scale to DrawWMF (code sample above) reproduces this situation on my PC. The generated file is OK and can be viewed with other Metafile viewers (I use MS Office Picture Manager) but VCL fails to show it, however, no exception is raised while loading the file.


Solution

  • Your limit is 32767.

    Tracing VCL code, the output file gets corrupt in TMetafile.WriteWMFStream. VCL writes a WmfPlaceableFileHeader (TMetafileHeader in VCL) record and then calls GetWinMetaFileBits to have 'emf' records converted to 'wmf' records. This function fails if any of the dimensions of the bounding rectangle (used when calling CreateEnhMetaFile) is greater than 32767. Not checking the return value, VCL does not raise any exception and closes the file with only 22 bytes - having only the "placeable header".

    Even for dimensions less than 32767, the "placeable header" may have possible wrong values (read details about the reason and implications from Tom's answer and comments to the answer), but more on this later...

    I used the below code to find the limit. Note that GetWinMetaFileBits does not get called with an enhanced metafile in VCL code.

    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;
    

    The error is a 534 ("Arithmetic result exceeded 32 bits"). Obviously there's some signed integer overflow. Some 'mf3216.dll' ("32-bit to 16-bit Metafile Conversion DLL") sets the error during a call by GetWinMetaFileBits to its exported ConvertEmfToWmf function, but that doesn't lead to any documentation regarding the overflow. The only official documentation regarding wmf limitations I can find is this (its main point is "use wmf only in 16 bit executables" :)).


    As mentioned earlier, the bogus "placeable header" structure may have "bogus" values and this may prevent the VCL from correctly playing the metafile. Specifically, dimensions of the metafile, as the VCL know them, may overflow. You may perform a simple sanity check after you have loaded the images for them to be displayed properly:

    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;
    
      ...