Repository: python-pillow/Pillow

Issue:
Some PCX files trigger OSError since 8.1.0
<!--
Thank you for reporting an issue.

Follow these guidelines to ensure your issue is handled properly.

If you have a ...

1.  General question: consider asking the question on Stack Overflow
    with the python-imaging-library tag:

    * https://stackoverflow.com/questions/tagged/python-imaging-library

    Do not ask a question in both places.

    If you think you have found a bug or have an unexplained exception
    then file a bug report here.

2.  Bug report: include a self-contained, copy-pastable example that
    generates the issue if possible. Be concise with code posted.
    Guidelines on how to provide a good bug report:

    * https://stackoverflow.com/help/mcve

    Bug reports which follow these guidelines are easier to diagnose,
    and are often handled much more quickly.

3.  Feature request: do a quick search of existing issues
    to make sure this has not been asked before.

We know asking good questions takes effort, and we appreciate your time.
Thank you.
-->

### What did you do?
In [OpenTTD/nml](https://github.com/OpenTTD/nml) we use pillow to manipulate images, mainly cropping, but I also tested with `show()` and `save()`.

I think it's related to https://github.com/python-pillow/Pillow/commit/2f409261eb1228e166868f8f0b5da5cda52e55bf
### What did you expect to happen?
I expect the images to be loaded as they were with previous (<8.1.0) pillow versions.

### What actually happened?
Depending on the used file it fails to load with `OSError: buffer overrun when reading image file`  or `OSError: image file is truncated (0 bytes not processed)`. But not all images fail to load.

### What are your OS, Python and Pillow versions?

* OS: Any (tested on Ubuntu, MacOS and Windows)
* Python: >3.5 (tested on 3.6, 3.7, 3.8)
* Pillow: 8.1.0

This is a simple test case with images that used to load fine. One still works, the two others trigger the different `OSError`
<!--
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.

The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as Plone, Django, or Buildout, try to replicate the issue just using Pillow.
-->

```python
from PIL import Image

files = ["arctic_railwagons.pcx", "opengfx_generic_trams1.pcx", "opengfx_trains_start.pcx"]

for file in files:
    try:
        with Image.open(file) as im:
            print("{}:{} {} {}".format(file,im.format, im.size, im.mode))
            im.save(file + ".png")
    except OSError as e:
        print("Failed with '{}'".format(str(e)))
```
Images and test code in [pillow-test.zip](https://github.com/python-pillow/Pillow/files/5796519/pillow-test.zip)



Discussion:
Hi. Thanks for being detailed in your report. Are we able to add any of those images to our test suite, and distribute them under the Pillow license?
Sorry for the delay, I had to check with the original author of the images.
You can use any of the images in your test suite and distribute them under the Pillow license.
The [specification](https://web.archive.org/web/20100206055706/http://www.qzx.com/pc-gpe/pcx.txt) states that 'BytesPerLine' 'MUST be an EVEN number.'
The two images that are failing here list the BytesPerLine as 371 and 409. So technically, they are malformed.

I've created PR #5214 to float the idea of reading them anyway.

Resolving patch:
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -66,13 +66,13 @@ def _open(self):
         version = s[1]
         bits = s[3]
         planes = s[65]
-        ignored_stride = i16(s, 66)
+        provided_stride = i16(s, 66)
         logger.debug(
             "PCX version %s, bits %s, planes %s, stride %s",
             version,
             bits,
             planes,
-            ignored_stride,
+            provided_stride,
         )
 
         self.info["dpi"] = i16(s, 12), i16(s, 14)
@@ -110,10 +110,15 @@ def _open(self):
         self.mode = mode
         self._size = bbox[2] - bbox[0], bbox[3] - bbox[1]
 
-        # don't trust the passed in stride. Calculate for ourselves.
+        # Don't trust the passed in stride.
+        # Calculate the approximate position for ourselves.
         # CVE-2020-35653
         stride = (self._size[0] * bits + 7) // 8
-        stride += stride % 2
+
+        # While the specification states that this must be even,
+        # not all images follow this
+        if provided_stride != stride:
+            stride += stride % 2
 
         bbox = (0, 0) + self.size
         logger.debug("size: %sx%s", *self.size)


Test patch:
diff --git a/Tests/images/odd_stride.pcx b/Tests/images/odd_stride.pcx
new file mode 100644
Binary files /dev/null and b/Tests/images/odd_stride.pcx differ
diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py
--- a/Tests/test_file_pcx.py
+++ b/Tests/test_file_pcx.py
@@ -44,6 +44,14 @@ def test_odd(tmp_path):
         _roundtrip(tmp_path, hopper(mode).resize((511, 511)))
 
 
+def test_odd_read():
+    # Reading an image with an odd stride, making it malformed
+    with Image.open("Tests/images/odd_stride.pcx") as im:
+        im.load()
+
+        assert im.size == (371, 150)
+
+
 def test_pil184():
     # Check reading of files where xmin/xmax is not zero.