Is there a limitation on dimensions of Windows Metafiles?
Asked Answered
C

2

13

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.

Compositor answered 14/12, 2015 at 10:4 Comment(3)
I re-tagged as winapi since this is, I believe, a question about the WMF format rather than anything related to Delphi.Valise
Are you sure your limit is around 25000? Can it be perhaps, exactly, 32767?Distefano
It seems plausible that it's a memory allocation failure based on the total area (width × height).Lavona
D
8

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;

  ...
Distefano answered 15/12, 2015 at 18:2 Comment(5)
So it is reasonable to suppose that as a 16 bit era standard, certain binary metadata fields may be limited to the range of a signed 16 bit integer. Sounds reasonable!Stoneware
Note that the limit is less if you intend to show the .wmf with a TImage, as I wrote.Asbestosis
@Tom - That limit is imposed by VCL. It uses the same "placeable header" while reading the file. My opinion is, it shouldn't be there in the first place. Anyway, since the reading will be done with Delphi, stripping 22 bytes does not seem to be an option. I'll delete my answer in favor of yours if I can't think of any usable solution. Meanwhile, +1 to you..Distefano
No, no Sertac, don't remove yours. It gives valuable insight with another perspective. ThanksAsbestosis
16 bit GDI had 16 bit coords, and Win 9x GDI also, so this is quite reasonableValise
A
1

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.

Asbestosis answered 15/12, 2015 at 12:9 Comment(4)
What VCL calls TMetaFileHeader is in fact an WmfPlaceableFileHeader which actually is not supported/used by the api (read remarks section). If you look at the produced file ('original.wmf'), you'll notice only the placable header is written (22bytes). What matters that fails, as I see it, is the GetWinMetaFileBits. The VCL, unsurprisingly, does not check the return, but with the example in the question it actually fails with "arithmetic overflow", which I believe to be related with the "RefDC".Distefano
Try with 30000 (Dim) in the example. The "Box" ("BoundingBox") right/bottom members still overflow, yet the metafiles are valid.Distefano
@Sertac Indeed, there's something to investigate still. But, is the file really valid (with 30000)? It doesn't show when read back. I'll check. However, for the purpose of this question, limiting drawing to less than calculated max values (that doesn't overflow) must be right, don't you think?Asbestosis
I only tested to see if XnView showed the files and assumed they are fine. I'd agree with your limits if the files aren't really valid.Distefano

© 2022 - 2024 — McMap. All rights reserved.