[solved] Cropped scale leads to ValueError 'embedded null byte' in PIL

I have a snippet of TAL like

<tal:def tal:define="scale_func obj/@@images;
                     scaled_image python:scale_func.scale('image', scale='tile_highlight')">
    <img tal:replace="structure python: scaled_image.tag(css_class='newsImage')" />

Which works as intended as long as the name of my scale does not contain an underscore or hyphen. Unfortunately I am too far in the project to consider renaming my scales.

When the scalename does include an underscore or hyphen, I get the following traceback:

Which works as intended as long as the scale has not been cropped.

2021-09-22 14:30:51,052 ERROR   [plone.namedfile.scaling:259][waitress] Could not scale "<plone.namedfile.file.NamedImage object at 0x7f21dce065f0 oid 0x228db in <Connection at 7f21f2c7b910>>" of 'http://localhost:8888/PNZ/de/assets-de/downloads/nachhaltigkeit/verhaltenskodex-fuer-unsere-partner'
Traceback (most recent call last):
  File "/usr/local/Plone52/buildout-cache/eggs/plone.namedfile-5.5.1-py3.9.egg/plone/namedfile/scaling.py", line 249, in __call__
    result = self.create_scale(
  File "/usr/local/Plone52/buildout-cache/eggs/plone.app.imagecropping-2.2.2-py3.9.egg/plone/app/imagecropping/dx.py", line 42, in create_scale
    data = self._crop(data, self.box)
  File "/usr/local/Plone52/buildout-cache/eggs/plone.app.imagecropping-2.2.2-py3.9.egg/plone/app/imagecropping/dx.py", line 31, in _crop
    image = PIL.Image.open(data)
  File "/usr/local/Plone52/buildout-cache/eggs/Pillow-6.2.2-py3.9-linux-x86_64.egg/PIL/Image.py", line 2766, in open
    fp = builtins.open(filename, "rb")
ValueError: embedded null byte
2021-09-22 14:30:51,735 ERROR   [Zope.SiteErrorLog:252][waitress] 1632313851.73454330.5442225585189057 http://localhost:8888/PNZ/de/assets-de/downloads/nachhaltigkeit/verhaltenskodex-fuer-unsere-partner/@@images/image
Traceback (innermost last):
  Module ZPublisher.WSGIPublisher, line 162, in transaction_pubevents
  Module ZPublisher.WSGIPublisher, line 371, in publish_module
  Module ZPublisher.WSGIPublisher, line 250, in publish
  Module ZPublisher.BaseRequest, line 532, in traverse
  Module ZPublisher.HTTPResponse, line 1022, in debugError
zExceptions.NotFound: Cannot locate object at: http://localhost:8888/PNZ/de/assets-de/downloads/nachhaltigkeit/verhaltenskodex-fuer-unsere-partner/@@images/image

I fail to understand why this could happen. Any insights?

The image in question is a generated jpg

class PDFImagePreview(BrowserView):
    """ update a PDF object with a preview image
        protected by zcml permission cmf.ModifyPortalContent

    def __init__(self, context, request):
        self.context = context
        self.request = request

    def __call__(self):
        context = self.context
        request = self.request

        data = get_pdf_preview(context)

        filename = u'pdf_preview'
        filename_extension = u'.pdf'
        pdf_filename = context.getId().lower()
        if pdf_filename.endswith(filename_extension):
            filename = pdf_filename.split(filename_extension)[0]
        #log.info('creating pdf preview with filename: %s' % filename)

        context.image = NamedImage(
                data        = data,
                contentType = 'image/jpeg',
                filename    = u'%s.jpg' % filename)

def get_pdf_preview(context=None, single_file=True):
    """ https://github.com/Belval/pdf2image
        images = convert_from_bytes(pdf_file, dpi=200, output_folder=None
                         , first_page=None, last_page=None, fmt='ppm'
                         , thread_count=1, userpw=None, use_cropbox=False
                         , strict=False, transparent=False
                         , single_file=False, output_file=str(uuid.uuid4())
                         , poppler_path=None, grayscale=False)

        images will be a list of PIL Image representing each page of the PDF document.
    image_file = io.BytesIO()

    data = context.file.data
    image = convert_from_bytes( data, fmt='jpg', dpi=150
                              , single_file=single_file)[0]

    image.save(image_file, format='jpeg')
    image_data = image_file.getvalue()

    return image_data

It does not matter if I write my image as BytesIO(image_data) - but it does matter if the image passed to PIL is a BytesIO object. Forcing this, fixes the issue:

from ZODB.blob import BlobFile

def _crop(self, data, box, default_format='PNG'):
        """crop data (image as open file) to box
        # Force NamedImage data to be a BytesIO object
        if not isinstance(data, BlobFile):
            data = BytesIO(data)

Plone Foundation Code of Conduct