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.
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.
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.
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.
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.
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;
...
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With