python-pillow / Pillow

Python Imaging Library (Fork)
https://python-pillow.org
Other
12.35k stars 2.24k forks source link

EMF files: OSError: cannot render metafile #6980

Open petsuter opened 1 year ago

petsuter commented 1 year ago

What did you do?

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?

petsuter commented 1 year ago

test_libuemf_ref.emf

petsuter commented 1 year ago

(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 commented 1 year ago

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:]
)
radarhere commented 1 year ago

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?

petsuter commented 1 year ago

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.

radarhere commented 1 month ago

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

Do you have examples of such files? I thought you might be referring to attachment 75058 at 2013-02-18 16:41:51 UTC in https://bugs.documentfoundation.org/show_bug.cgi?id=55058, but I was able to open load that file with Pillow without a problem.

petsuter commented 1 month ago

Right, those are different EMF files. I have to check if I can find a public one.

petsuter commented 1 month ago

libUEMF-0.2.8.tar.gz from https://sourceforge.net/projects/libuemf/ contains more EMF test files. Almost none of them work with Pillow.

petsuter commented 1 month ago

there are EMR_POLYBEZIER16 records with count 0 which also fails to load in Pillow

Here is a test file test_20241010.zip As described above it can be opened for example in Windows Paint and IrfanView, but not in Pillow (v10.4.0)

radarhere commented 1 month ago

Thanks. Is that image one that can be included in our test suite and distributed under the Pillow license?

radarhere commented 1 month ago

I've created #8506 to resolve this. With it, I'm able to render test_20241010.emf and test_libuemf_ref.emf.

Here are the results, saved as PNGs.

test_20241010

test_libuemf_ref

petsuter commented 1 month ago

Sounds great, thanks!

petsuter commented 1 month ago

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

(Apparently they moved this in the meantime. In version 7 it was here I guess: https://github.com/dotnet/runtime/blob/v7.0.20/src/libraries/System.Drawing.Common/src/System/Drawing/Imaging/Metafile.cs#L525

In version 9 it's now here: https://github.com/dotnet/winforms/blob/release/9.0/src/System.Drawing.Common/src/System/Drawing/Imaging/Metafile.cs#L541 )