Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a limitation on dimensions of Windows Metafiles?

Tags:

winapi

delphi

wmf

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.

like image 209
saastn Avatar asked Dec 14 '15 10:12

saastn


People also ask

What is the difference between Enhanced Metafile and Windows Metafile?

Windows Metafile (WMF) and Enhanced MetaFile (EMF) are 2 linked image formats developed by Microsoft since the 1990s. Enhanced MetaFile is essentially an updated version of WMF which makes it 32 bit and addresses some other issues. Unlike most other image formats, it is a Vector file format.

What is Windows WMF?

Windows Management Framework (WMF) provides a consistent management interface for Windows. WMF provides a seamless way to manage various versions of Windows client and Windows Server. WMF installer packages contain updates to management functionality and are available for older versions of Windows.

What is a WMF file used for?

This image format was designed by Microsoft in the 1990s for their Windows Operating System. WMF files contain both vector graphics and raster components at the same time. It includes sort of programming commands which enables the creation of lines, circles, and rectangles on the viewing applications.

How do I create a Windows Metafile?

Click "File," then "Export" to open a dialog box. Click the "Save as type" drop-down menu and select "WMF." Click "Save," then "OK" in the WMF options window. A copy of your JPG file is now saved in the same folder in the WMF format.


2 Answers

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;

  ...
like image 185
Sertac Akyuz Avatar answered Oct 03 '22 21:10

Sertac Akyuz


When docs don't help, look at the source :). The file creation fails if the either width or height is too big, and the file becomes invalid. In the following I look at the horizontal dimension only, but the vertical dimension is treated the same.

In 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));

If ReferenceDevice is not defined, then the screen (GetDC(0)) is used. On my machine horizontal size is reported as 677 and horizontal resolution as 1920. Thus FMetafile.MMWidth := 40000 * 67700 div 1920 ( = 1410416). Since FMetaFile.MMWidth is an integer, no problems at this point.

Next, let's look at the file writing, which is done with WriteWMFStream because we write to a .wmf file:

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

The WMF header structure indicates where things are going south

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

The Box: TSmallRect field can not hold bigger coordinates than smallint-sized values. Right is calculated as Right := 1410417 * 96 div 2540 ( = 53307 as smallint= -12229). The dimensions of the image overflows and the wmf data can not be 'played' to the file.

The question rizes: What dimensions can I use on my machine?

Both FMetaFile.MMWidth and FMetaFile.MMHeight needs to be less or equal to

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

On my testmachine horizontal display size and resolution are 677 and 1920. Vertical display size and resolution are 381 and 1080. Thus maximum dimensions of a metafile becomes:

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

Verified by testing.


Update after further investigation inspired by comments:

With horizontal and vertical dimension up to 32767, the metafile is readable with some applications, f.ex. GIMP, it shows the image. Possibly this is due to those programs considering the extents of the drawing as word instead of SmallInt. GIMP reported pixels per inch to be 90 and when changed to 96 (which is the value used by Delphi, GIMP chrashed with a 'GIMP Message: Plug-in crashed: "file-wmf.exe".

The procedure in the OP does not show an error message with dimensions of 32767 or less. However, if either dimension is higher than previously presented calculated max value, the drawing is not shown. When reading the metafile, the same TMetafileHeader structure type is used as when saving and the FWidth and FHeight get negative values:

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

The negative values ripple through to the Paint procedure in the DestRect function and the image is therefore not seen.

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

DestRect has negative values for Right and Bottom

I maintain that the only way to find actual limit is to call GetDeviceCaps() for both horizontal and vertical size and resolution, and perform the calculations above. Note however, the file may still not be displayable with a Delphi program on another machine. Keeping the drawing size within 20000 x 20000 is probably a safe limit.

like image 40
Tom Brunberg Avatar answered Oct 03 '22 21:10

Tom Brunberg