Repository: python-pillow/Pillow

Issue:
QOI support
### What did you do?

Tried to open a QOI image ( https://qoiformat.org/ ) with Pillow (`Image.open("picture.qoi")`)

The picture is in this zip file (a 2-seconds stick-man) : [picture.zip](https://github.com/python-pillow/Pillow/files/10327604/picture.zip)

### What did you expect to happen?

I would love Pillow to recognize the file type and open the image.

### What actually happened?

Pillow can't recognize the QOI format.

```python
>>> from PIL import Image
>>> Image.open("picture.qoi")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.10/site-packages/PIL/Image.py", line 3186, in open
    raise UnidentifiedImageError(
PIL.UnidentifiedImageError: cannot identify image file 'picture.qoi'
```

### What are your OS, Python and Pillow versions?

* OS: Manjaro Linux
* Python: 3.10.8
* Pillow: 9.3.0

### To replicate

```python
from PIL import Image
Image.open("picture.qoi")
```


Discussion:
I would be happy to contribute to Pillow by implementing QOI support btw (if in scope of pillow)
Thanks for the test image, that was a helpful reference point.

I've created PR #6852 to add support for reading QOI images.
Oh thanks, just had a look and this seems like a very clean implementation

Resolving patch:
diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py
new file mode 100644
--- /dev/null
+++ b/src/PIL/QoiImagePlugin.py
@@ -0,0 +1,105 @@
+#
+# The Python Imaging Library.
+#
+# QOI support for PIL
+#
+# See the README file for information on usage and redistribution.
+#
+
+import os
+
+from . import Image, ImageFile
+from ._binary import i32be as i32
+from ._binary import o8
+
+
+def _accept(prefix):
+    return prefix[:4] == b"qoif"
+
+
+class QoiImageFile(ImageFile.ImageFile):
+    format = "QOI"
+    format_description = "Quite OK Image"
+
+    def _open(self):
+        if not _accept(self.fp.read(4)):
+            msg = "not a QOI file"
+            raise SyntaxError(msg)
+
+        self._size = tuple(i32(self.fp.read(4)) for i in range(2))
+
+        channels = self.fp.read(1)[0]
+        self.mode = "RGB" if channels == 3 else "RGBA"
+
+        self.fp.seek(1, os.SEEK_CUR)  # colorspace
+        self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)]
+
+
+class QoiDecoder(ImageFile.PyDecoder):
+    _pulls_fd = True
+
+    def _add_to_previous_pixels(self, value):
+        self._previous_pixel = value
+
+        r, g, b, a = value
+        hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
+        self._previously_seen_pixels[hash_value] = value
+
+    def decode(self, buffer):
+        self._previously_seen_pixels = {}
+        self._previous_pixel = None
+        self._add_to_previous_pixels(b"".join(o8(i) for i in (0, 0, 0, 255)))
+
+        data = bytearray()
+        bands = Image.getmodebands(self.mode)
+        while len(data) < self.state.xsize * self.state.ysize * bands:
+            byte = self.fd.read(1)[0]
+            if byte == 0b11111110:  # QOI_OP_RGB
+                value = self.fd.read(3) + o8(255)
+            elif byte == 0b11111111:  # QOI_OP_RGBA
+                value = self.fd.read(4)
+            else:
+                op = byte >> 6
+                if op == 0:  # QOI_OP_INDEX
+                    op_index = byte & 0b00111111
+                    value = self._previously_seen_pixels.get(op_index, (0, 0, 0, 0))
+                elif op == 1:  # QOI_OP_DIFF
+                    value = (
+                        (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2)
+                        % 256,
+                        (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2)
+                        % 256,
+                        (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256,
+                    )
+                    value += (self._previous_pixel[3],)
+                elif op == 2:  # QOI_OP_LUMA
+                    second_byte = self.fd.read(1)[0]
+                    diff_green = (byte & 0b00111111) - 32
+                    diff_red = ((second_byte & 0b11110000) >> 4) - 8
+                    diff_blue = (second_byte & 0b00001111) - 8
+
+                    value = tuple(
+                        (self._previous_pixel[i] + diff_green + diff) % 256
+                        for i, diff in enumerate((diff_red, 0, diff_blue))
+                    )
+                    value += (self._previous_pixel[3],)
+                elif op == 3:  # QOI_OP_RUN
+                    run_length = (byte & 0b00111111) + 1
+                    value = self._previous_pixel
+                    if bands == 3:
+                        value = value[:3]
+                    data += value * run_length
+                    continue
+                value = b"".join(o8(i) for i in value)
+            self._add_to_previous_pixels(value)
+
+            if bands == 3:
+                value = value[:3]
+            data += value
+        self.set_as_raw(bytes(data))
+        return -1, 0
+
+
+Image.register_open(QoiImageFile.format, QoiImageFile, _accept)
+Image.register_decoder("qoi", QoiDecoder)
+Image.register_extension(QoiImageFile.format, ".qoi")
diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py
--- a/src/PIL/__init__.py
+++ b/src/PIL/__init__.py
@@ -59,6 +59,7 @@
     "PngImagePlugin",
     "PpmImagePlugin",
     "PsdImagePlugin",
+    "QoiImagePlugin",
     "SgiImagePlugin",
     "SpiderImagePlugin",
     "SunImagePlugin",


Test patch:
diff --git a/Tests/images/hopper.qoi b/Tests/images/hopper.qoi
new file mode 100644
Binary files /dev/null and b/Tests/images/hopper.qoi differ
diff --git a/Tests/images/pil123rgba.qoi b/Tests/images/pil123rgba.qoi
new file mode 100644
Binary files /dev/null and b/Tests/images/pil123rgba.qoi differ
diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py
new file mode 100644
--- /dev/null
+++ b/Tests/test_file_qoi.py
@@ -0,0 +1,28 @@
+import pytest
+
+from PIL import Image, QoiImagePlugin
+
+from .helper import assert_image_equal_tofile, assert_image_similar_tofile
+
+
+def test_sanity():
+    with Image.open("Tests/images/hopper.qoi") as im:
+        assert im.mode == "RGB"
+        assert im.size == (128, 128)
+        assert im.format == "QOI"
+
+        assert_image_equal_tofile(im, "Tests/images/hopper.png")
+
+    with Image.open("Tests/images/pil123rgba.qoi") as im:
+        assert im.mode == "RGBA"
+        assert im.size == (162, 150)
+        assert im.format == "QOI"
+
+        assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03)
+
+
+def test_invalid_file():
+    invalid_file = "Tests/images/flower.jpg"
+
+    with pytest.raises(SyntaxError):
+        QoiImagePlugin.QoiImageFile(invalid_file)