Pillow icon indicating copy to clipboard operation
Pillow copied to clipboard

EMF files: OSError: cannot render metafile

Open petsuter opened this issue 2 years ago • 10 comments

What did you do?

  • Download example EMF file test_libuemf_ref.emf from https://bugs.documentfoundation.org/attachment.cgi?id=135791 (via https://bugs.documentfoundation.org/show_bug.cgi?id=55058)

  • Run:

import PIL.Image
with PIL.Image.open("test_libuemf_ref.emf") as im:
    im.save("out.png")

What did you expect to happen?

out.png should be created.

What actually happened?

C:\Python311\Lib\site-packages\PIL\Image.py:3167: DecompressionBombWarning: Image size (139177600 pixels) exceeds limit of 89478485 pixels, could be decompression bomb DOS attack.
  warnings.warn(
{'wmf_bbox': (0, 0, 14030, 9920), 'dpi': (1199.9124549648136, 1199.9047573693986)}
Traceback (most recent call last):
  File "test.py", line 3, in <module>
    im.save("test_libuemf_ref.emf")
  File "C:\Python311\Lib\site-packages\PIL\Image.py", line 2394, in save
    self._ensure_mutable()
  File "C:\Python311\Lib\site-packages\PIL\Image.py", line 611, in _ensure_mutable
    self._copy()
  File "C:\Python311\Lib\site-packages\PIL\Image.py", line 604, in _copy
    self.load()
  File "C:\Python311\Lib\site-packages\PIL\WmfImagePlugin.py", line 162, in load
    return super().load()
           ^^^^^^^^^^^^^^
  File "C:\Python311\Lib\site-packages\PIL\ImageFile.py", line 345, in load
    image = loader.load(self)
            ^^^^^^^^^^^^^^^^^
  File "C:\Python311\Lib\site-packages\PIL\WmfImagePlugin.py", line 53, in load
    Image.core.drawwmf(im.fp.read(), im.size, self.bbox),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OSError: cannot render metafile

What are your OS, Python and Pillow versions?

  • OS: Windows 10
  • Python: 3.11
  • Pillow: 9.4.0

petsuter avatar Feb 28 '23 16:02 petsuter

(It seems Pillow calls the GDI routine PlayEnhMetaFile which fails.

The same EMF file works with this C# code:

var emf_filename = @"test_libuemf_ref.emf";
var png_filename = @"out.png";
var metafile = new Metafile(emf_filename);
var metafileHeader = metafile.GetMetafileHeader();
var bitmap = new Bitmap(metafile.Width, metafile.Height);
using (var gfx = Graphics.FromImage(bitmap))
{
	gfx.Clear(Color.White);
	float sx = metafileHeader.DpiX / gfx.DpiX;
	float sy = metafileHeader.DpiY / gfx.DpiY;
	gfx.ScaleTransform(sx, sy);
	gfx.DrawImage(metafile, 0, 0);
}
bitmap.Save(png_filename);

C# seems to also uses GDI (but PlayMetafileRecord instead of PlayEnhMetaFile? not sure) here I think: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Drawing.Common/src/System/Drawing/Imaging/Metafile.cs Just in case this helps.)

petsuter avatar Feb 28 '23 16:02 petsuter

I think the first record that fails record ~13 is a EMR_STROKEANDFILLPATH with "bounds" arguments 0,0,29699,20999 which is probably rejected because it is larger than the EMR_HEADER bounds 0,0,14030,9920.

In other files there are EMR_POLYBEZIER16 records with count 0 which also fails to load in Pillow. These empty records could just be dropped e.g. like this:

import pathlib
import struct

input_path = pathlib.Path("test.emf")
output_path = pathlib.Path("filtered.emf")
input_bytes = input_path.read_bytes()
output_records = []
offset = 0
while offset < len(input_bytes):
    rec_type, rec_size = struct.unpack_from('<ii', input_bytes, offset)
    if rec_type == 88:        # EMR_POLYBEZIER16
        count = struct.unpack_from('<i', input_bytes, offset + 24)[0]
        if count == 0:
            offset += rec_size
            continue
    rec_bytes = input_bytes[offset:offset+rec_size]
    output_records.append(rec_bytes)
    offset += rec_size
output_bytes = b''.join(output_records)
output_path.write_bytes(
    output_bytes[:0x30] +
    struct.pack('<i', len(output_bytes)) +
    struct.pack('<i', len(output_records)) +
    output_bytes[0x38:]
)

petsuter avatar Mar 01 '23 12:03 petsuter

I think the first record that fails record ~13 is a EMR_STROKEANDFILLPATH with "bounds" arguments 0,0,29699,20999 which is probably rejected because it is larger than the EMR_HEADER bounds 0,0,14030,9920.

So, if I understand you correctly, the image actually has bugs in it? You're requesting that Pillow be more flexible when reading?

radarhere avatar Mar 03 '23 11:03 radarhere

Other programs can open and display the image without a problem, including Windows Paint and IrfanView and the C# reader above. I don't know if the image can be considered to have bugs, but maybe "unusual edge cases"?

A few days ago I didn't know anything about EMF files. I wanted to load some seemingly valid files. They were rejected for unknown reasons. If you can make Pillow more flexible to allow reading these files, I think that would be valuable.

petsuter avatar Mar 03 '23 11:03 petsuter