olokelo / jxlpy

Cython bindings and Pillow plugin for JPEG XL
MIT License
44 stars 12 forks source link

Pillow ValueError #22

Open BigBoyBarney opened 4 months ago

BigBoyBarney commented 4 months ago

Hi! First of all, thank you for this project! I was trying to get this to work with Pillow using the encode-decode example.

from PIL import Image
from jxlpy import JXLImagePlugin

im = Image.open('1.jxl')
im = im.resize((im.width*2, im.height*2))
im.save('test_2x.jxl', lossless=True, effort=7)

but I get the following error:

/usr/bin/python3.12 /home/barney/Documents/Programming/Random/Pillow jxl.py 
Traceback (most recent call last):
  File "/home/barney/Documents/Programming/Random/Pillow jxl.py", line 5, in <module>
    im = im.resize((im.width*2, im.height*2))
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/Image.py", line 2164, in resize
    self.load()
  File "/home/barney/.local/lib/python3.12/site-packages/jxlpy/JXLImagePlugin.py", line 63, in load
    return super().load()
           ^^^^^^^^^^^^^^
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/ImageFile.py", line 227, in load
    self.im = Image.core.map_buffer(
              ^^^^^^^^^^^^^^^^^^^^^^
ValueError: buffer is not large enough

then I tried

from PIL import Image
from jxlpy import JXLImagePlugin

with open("1.jxl", "rb") as f:
    with Image.open(f) as im:
        im.show()

which results in

/usr/bin/python3.12 /home/barney/Documents/Programming/Random/Pillow jxl.py 
Traceback (most recent call last):
  File "/home/barney/Documents/Programming/Random/Pillow jxl.py", line 6, in <module>
    im.show()
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/Image.py", line 2494, in show
    _show(self, title=title)
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/Image.py", line 3539, in _show
    ImageShow.show(image, **options)
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/ImageShow.py", line 62, in show
    if viewer.show(image, title=title, **options):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/ImageShow.py", line 86, in show
    return self.show_image(image, **options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/ImageShow.py", line 113, in show_image
    return self.show_file(self.save_image(image), **options)
                          ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/ImageShow.py", line 109, in save_image
    return image._dump(format=self.get_format(image), **self.options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/Image.py", line 604, in _dump
    self.save(filename, format, **options)
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/Image.py", line 2439, in save
    save_handler(self, fp, filename)
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/PngImagePlugin.py", line 1224, in _save_all
    _save(im, fp, filename, save_all=True)
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/PngImagePlugin.py", line 1237, in _save
    for im_frame in ImageSequence.Iterator(im_seq):
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/ImageSequence.py", line 56, in __next__
    self.im.seek(self.position)
  File "/home/barney/.local/lib/python3.12/site-packages/jxlpy/JXLImagePlugin.py", line 41, in seek
    raise NotImplementedError(
NotImplementedError: Seeking more than one frame forward is currently not supported.

Any help would be appreciated! Thanks!

olokelo commented 4 months ago

Hello, Thank you for reporting this issue. The first part about the buffer error seems to be more related to Pillow itself. see here Could you please try doing the same with non-jxl image to verify that?

I was able to replicate your second issue. It seems to be a bug in jxlpy Pillow integration. This line in JXLImagePlugin obviously should also take into consideration user seeking to the same frame. I changed that but from what I can tell, Pillow for some reason tries to seek to next frame which doesn't exist when calling im.show(). This confuses jxlpy as it doesn't know how many frames does a jxl image have until its fully decoded. I've also ran into issues where Pillow is getting stuck in a loop seeking frame by frame up to infinity. When calling im.save() everything seems fine and Pillow doesn't issue unnecessary seeks.

I will probably take a look at it tomorrow.

BigBoyBarney commented 4 months ago

Thank you for taking a look! Using .png instead of .jxl

from PIL import Image
from jxlpy import JXLImagePlugin

im = Image.open('1.png')
im = im.resize((im.width*2, im.height*2))
im.save('test_2x.jxl', lossless=True, effort=7)

the above works as intended^ :D

olokelo commented 4 months ago

Hi again,

Sorry for my late response. I am still not sure what's going on with this ValueError: buffer is not large enough issue.

However I discovered that Pillow tries to seek back to the first frame when doing .show(). If the image was opened before this means it needs to go backwards to the beginning of already loaded image. .show() tries to save the image with save_all=True which is why all frames are being saved.

I'm currently in a process of getting jxl support into Pillow via libjxl and Python C API. I was successful in implementing frame seeking there. There's JxlDecoderRewind method in libjxl which makes it easy. However a bigger problem is to know the number of frames in the picture in advance. It isn't specified in libjxl bitstream however it's possible to know that image is animated (JxlBasicInfo.have_animation). Without knowing how many frames there are Pillow will just hang iterating over non-existent frames when save is called with save_all. Probably the best solution would be to count all JXL_DEC_FRAME events when opening the image and then set n_frames accordingly. In jxlpy it would probably be necessary to decode all frames first, then rewind and set number of frames for Pillow to work with.

I'm pretty sure save_all was working before so maybe there was a change in how Pillow handles it now (it doesn't call ImageFile.load(), only seeking).

olokelo commented 4 months ago

Hi! Please try the most recent version of jxlpy. I think I figured out the issue.

BigBoyBarney commented 4 months ago

Hi! Thanks for looking into it I tried it with jxlpy version 0.9.4, and Pillow no longer recognises jxl images. for both

from PIL import Image
from jxlpy import JXLImagePlugin

with open("1.jxl", "rb") as f:
    with Image.open(f) as im:
        im.show()

and

from PIL import Image
from jxlpy import JXLImagePlugin

im = Image.open('1.jxl')
im = im.resize((im.width*2, im.height*2))
im.save('test_2x.jxl', lossless=True, effort=7)

I get the following error:

/usr/bin/python3.12 /home/barney/Documents/Programming/Random/Pillow jxl.py 
Traceback (most recent call last):
  File "/home/barney/Documents/Programming/Random/Pillow jxl.py", line 5, in <module>
    with Image.open(f) as im:
         ^^^^^^^^^^^^^
  File "/home/barney/.local/lib/python3.12/site-packages/PIL/Image.py", line 3298, in open
    raise UnidentifiedImageError(msg)
PIL.UnidentifiedImageError: cannot identify image file <_io.BufferedReader name='1.jxl'>
olokelo commented 4 months ago

Hi, I honestly have no idea what might be going on now. I've tested jxlpy from PyPI on Python 3.12 and it seems to work fine with Pillow. Could you tell me what OS are you using? Maybe also what's your input image? Could you please test once again on different set of images for example from jpegxl.info.

BigBoyBarney commented 4 months ago

I'm using Linux (fedora 39), and I tested it on 3 different images, same error on all of them. However, after updating Pillow as well (from 9.5.0 to 10.2.0), it now works 🥳

I have no idea why 9.5.0 didn't recognise the image, maybe there's something in the plugin that's not compatible? In any case, it might be worth noting in the readme that if somebody encounters that error, they should make sure that Pillow is up to date.

Thank you for the fix!