将生成的 PIL 图像保存到 Django 的 ImageField 中

6

我正在使用qrcode来生成二维码。当购买了门票或者购买被确认后,我希望生成一个二维码图片并使用PIL进行一些修改。最后将修改后的画布保存到模型的Image字段中。

class Ticket(models.Model):
    booked_at = models.DateTimeField(default=timezone.now)
    qrcode_file = models.ImageField(upload_to='qrcode', blank=True, null=True)
    bought = models.BooleanField(default=False)

    def save(self, *args, **kwargs):
        if self.bought:
            ...
            ...
            qrcode_img = qrcode.make('some data')
            canvas = Image.new('RGB', (total_width, total_height), 'white')
            draw = ImageDraw.Draw(canvas)
            position = (left, top)
            canvas.paste(qrcode_img, position)

            self.qrcode_file = canvas
            self.booked_at = timezone.now()
            super(Ticket, self).save(*args, **kwargs)
            canvas.close()
            qrcode_img.close()
        else:
            self.booked_at = timezone.now()
            super(Ticket, self).save(*args, **kwargs)

但是这会抛出一个错误:

AttributeError: 'Image' 对象没有属性 '_committed'

我该如何将生成的PIL图像保存到Django的ImageField中?


这行代码 canvas = Image.new("RGB", (total_width, total_height), white) 中的 "Image" 是从哪里来的? - Martins
@Martins,这是来自PIL的。 - Zorig
在阅读文档后,我意识到了这一点。 - Martins
2个回答

13
你可以使用 BytesIO 将 Pillow 文件保存到内存 blob 中,然后创建一个 File 对象,并将其传递给模型实例 ImageField 的 save 方法。
from io import BytesIO
from django.core.files import File

canvas = Image.new('RGB', (total_width, total_height), 'white')
...
blob = BytesIO()
canvas.save(blob, 'JPEG')  
self.qrcode_file.save('ticket-filename.jpg', File(blob), save=False) 

查看django文档中的File对象。 https://docs.djangoproject.com/en/2.0/ref/files/file/#the-file-object

您需要使用save=False,因为默认值save=True会导致在图像保存后调用父模型的save方法。在此处不要进行递归,因为通常会陷入无限循环。


我需要关闭博客对象吗,还是不必要的? - Aamu
1
您不需要关闭 BytesIO。在 blob 被垃圾回收时,内存会被释放。 - Håken Lid
你的方法可能会在大量请求时引起内存使用问题。 - M.javid
1
如果您在单个服务器上同时创建数百万个QR码,可能会发生这种情况。如果您担心这一点,可以在完成后立即关闭blob对象,以立即释放内存。但是,当保存方法完成并且当前范围中不再引用它时,blob应该会自动关闭和删除。 - Håken Lid

5

更改您的代码并按以下方式使用Django 文件对象

from django.core.files import File


class Ticket(models.Model):
    booked_at = models.DateTimeField(default=timezone.now)
    qrcode_file = models.ImageField(upload_to='qrcode', blank=True, null=True)
    bought = models.BooleanField(default=False)

    def save(self, *args, **kwargs):
        if self.bought:
            ...
            ...
            qrcode_img = qrcode.make('some data')
            canvas = Image.new('RGB', (total_width, total_height), 'white')
            draw = ImageDraw.Draw(canvas)
            position = (left, top)
            canvas.paste(qrcode_img, position)

            canvas.save('path/of/dest.png', 'PNG')
            destination_file = open('path/of/dest.png', 'rb')
            self.qrcode_file.save('dest.png', File(destination_file), save=False)
            destination_file.close()

            self.booked_at = timezone.now()
            super(Ticket, self).save(*args, **kwargs)
            canvas.close()
            qrcode_img.close()
        else:
            self.booked_at = timezone.now()
            super(Ticket, self).save(*args, **kwargs)

你可以将canvas保存在media_rootupload_to路径下,或者保存在临时目录中,也可以使用BytesIO对象。

这会在磁盘上创建一个文件吗?我还需要删除这个文件,对吧? - Aamu
您可以将文件保存在临时目录中,或者使用BytesIO。 - M.javid
如果两个工作进程尝试并行写入和读取,使用硬编码的 path/of/dest.png 可能会出现竞争条件的理论可能性。Python 的标准库提供了一个 tempfile 模块,可以在这里使用。上下文管理器 with tempfile.NamedTemporaryFile(suffix='.png') as fp: 将在系统的 tmp 文件夹中创建一个随机文件名,并在完成后将其删除。https://docs.python.org/3/library/tempfile.html#tempfile.NamedTemporaryFile - Håken Lid
是的,每种方法都有特殊的问题,但使用磁盘保存文件可能比保存在内存中更好,这与调用此保存方法的频率有关。 - M.javid
1
谢谢!这段代码 destination_file = open('path/of/dest.png', 'rb') self.qrcode_file.save('dest.png', File(destination_file), save=False) 真的帮助我将生成的 QR 码保存到我的数据模型上的空 ImageField 中(使用 post_save 信号)。 - RealScatman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接