Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added QOI reading #6852

Merged
merged 4 commits into from
Mar 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Tests/images/hopper.qoi
Binary file not shown.
Binary file added Tests/images/pil123rgba.qoi
Binary file not shown.
28 changes: 28 additions & 0 deletions Tests/test_file_qoi.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1544,6 +1544,13 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum

.. versionadded:: 5.3.0

QOI
^^^

.. versionadded:: 9.5.0

Pillow identifies and reads images in Quite OK Image format.

XV Thumbnails
^^^^^^^^^^^^^

Expand Down
5 changes: 5 additions & 0 deletions docs/releasenotes/9.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ TODO
API Additions
=============

QOI file format
^^^^^^^^^^^^^^^

Pillow can now read images in Quite OK Image format.

Added ``dpi`` argument when saving PDFs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
105 changes: 105 additions & 0 deletions src/PIL/QoiImagePlugin.py
Original file line number Diff line number Diff line change
@@ -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")
1 change: 1 addition & 0 deletions src/PIL/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"PngImagePlugin",
"PpmImagePlugin",
"PsdImagePlugin",
"QoiImagePlugin",
"SgiImagePlugin",
"SpiderImagePlugin",
"SunImagePlugin",
Expand Down