将使用wand生成的图像保存到Django ImageField

5

我正在尝试为在Django模型中存储的“叠加层”配置生成预览,稍后将应用于其他模型。我没有太多使用Python操作文件的经验...=(

以下是我的代码:

import io
from django.conf import settings
from django.db import models
from wand.image import Image
from PIL.ImageFile import ImageFile, Parser, Image as PilImage

class Overlay(models.Model):
    RELATIVE_POSITIONS = (...)
    SIZE_MODES = (...)

    name = models.CharField(max_length=50)
    source = models.FileField(upload_to='overlays/%Y/%m/%d')
    sample = models.ImageField(upload_to='overlay_samples/%Y/%m/%d', blank=True)
    px = models.SmallIntegerField(default=0)
    py = models.SmallIntegerField(default=0)
    position = models.CharField(max_length=2, choices=RELATIVE_POSITIONS)
    width = models.SmallIntegerField(default=0)
    height = models.SmallIntegerField(default=0)
    size_mode = models.CharField(max_length=1, choices=SIZE_MODES, default='B')
    last_edit = models.DateTimeField(auto_now=True)

    def generate_sample(self):
        """
        Generates the sample image and saves it in the "sample" field model
        :return: void
        """
        base_pic = Image(filename=os.path.join(settings.BASE_DIR, 'girl.jpg'))
        overlay_pic = Image(file=self.source)
        result_pic = io.BytesIO()
        pil_parser = Parser()

        if self.width or self.height:
            resize_args = {}
            if self.width:
                resize_args['width'] = self.width
            if self.height:
                resize_args['height'] = self.height
            overlay_pic.resize(**resize_args)
            base_pic.composite(overlay_pic, self.px, self.py)
            base_pic.save(file=result_pic)

        result_pic.seek(0)
        while True:
            s = result_pic.read(1024)
            if not s:
                break
            pil_parser.feed(s)

        pil_result_pic = pil_parser.close()
        self.sample.save(self.name, pil_result_pic, False)

    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
        self.generate_sample()
        super(Overlay, self).save(force_insert, force_update, using, update_fields)

但是我在 Django 调试数据中看到了 AttributeError,具体如下:

 /usr/local/lib/python2.7/dist-packages/django/core/files/utils.py in <lambda>

    """
    encoding = property(lambda self: self.file.encoding)
    fileno = property(lambda self: self.file.fileno)
    flush = property(lambda self: self.file.flush)
    isatty = property(lambda self: self.file.isatty)
    newlines = property(lambda self: self.file.newlines)
    read = property(lambda self: self.file.read)
    readinto = property(lambda self: self.file.readinto)
    readline = property(lambda self: self.file.readline)
    readlines = property(lambda self: self.file.readlines)
    seek = property(lambda self: self.file.seek)
    softspace = property(lambda self: self.file.softspace)
    tell = property(lambda self: self.file.tell)

▼ 本地变量 变量 值

self    <File: None>



/usr/local/lib/python2.7/dist-packages/PIL/Image.py in __getattr__

        # numpy array interface support
        new = {}
        shape, typestr = _conv_type_shape(self)
        new['shape'] = shape
        new['typestr'] = typestr
        new['data'] = self.tobytes()
        return new
    raise AttributeError(name)

def __getstate__(self):
    return [
        self.info,
        self.mode,
        self.size,

▼ 本地变量 变量 值

self    <PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=1080x1618 at 0x7F1429291248>
name    'read'

出了什么问题?

3个回答

3

问题已解决!

就像 @Alexey Kuleshevich 提到的那样,django FileField 需要一个文件对象,但是缺少的是我们必须先将图像保存到磁盘或内存中的文件中,我猜测最好使用内存...这里是最终的解决方案。 我认为它可以改进成不使用两个步骤的"转换"

from django.core.files.base import ContentFile

并且在该方法内:

    result_pic = io.BytesIO()
    pil_parser = Parser()

    ...
    overlay_pic.resize(**resize_args)
    base_pic.composite(overlay_pic, self.px, self.py)
    base_pic.save(file=result_pic)

    result_pic.seek(0)
    while True:
        s = result_pic.read(1024)
        if not s:
            break
        pil_parser.feed(s)

    result_pic = io.BytesIO()
    pil_result_pic = pil_parser.close()
    pil_result_pic.save(result_pic, format='JPEG')
    django_file = ContentFile(result_pic.getvalue())
    self.sample.save(self.name, django_file, False)

感谢这个答案:如何将 PIL Image 转换为 Django File


1
每当你将文件保存到ImageFieldFileField时,你需要确保它是Django的File对象。这里是文档的参考链接: https://docs.djangoproject.com/en/1.7/ref/models/fields/#filefield-and-fieldfile
from django.core.files import File

在方法内部:
def generate_sample(self):
    ...
    pil_result_pic = pil_parser.close()
    self.sample.save(self.name, File(pil_result_pic), False)

否则看起来不错,尽管我可能会漏掉一些东西。试一下看是否解决了问题,如果没有,我会更深入地研究它。
编辑
实际上你不需要一个解析器。我认为这应该解决问题:
from django.core.files import ContentFile

class Overlay(models.Model):
    ...

    def generate_sample(self):
        base_pic = Image(filename=os.path.join(settings.BASE_DIR, 'girl.jpg'))
        overlay_pic = Image(file=self.source)
        result_pic = io.BytesIO()

        if self.width or self.height:
            resize_args = {}
            if self.width:
                resize_args['width'] = self.width
            if self.height:
                resize_args['height'] = self.height
            overlay_pic.resize(**resize_args)
        base_pic.composite(overlay_pic, self.px, self.py)
        base_pic.save(file=result_pic)

        content = result_pic.getvalue()
        self.sample.save(self.name, ContentFile(content), False)
        result_pic.close()
        base_pic.close()
        overlay_pic.close()

有一件事情可能会成为潜在的问题,每次保存 Overlay 模型时都会执行此操作,即使原始图像相同。但如果很少保存,这不应该是一个问题。


已尝试使用 "from django.core.files import File" 和 "from django.core.files.images import ImageFile",并尝试了许多其他方法,但似乎没有任何作用。非常感谢。 - netomo
还尝试了以下代码: pil_result_pic = pil_parser.close() django_file = File(open(pil_result_pic)) self.sample.save(self.name, django_file, False) 现在出现以下错误:强制转换为Unicode:需要字符串或缓冲区,但发现实例。 我不知道,但不知怎么的我觉得我离成功更近了 @alexey-kuleshevich - netomo
我认为强制转换为Unicode是因为它没有以二进制模式打开,但我不确定百分之百。无论如何,请让我知道新的建议解决方案是否适用于您。 - lehins
嘿!那太棒了! - netomo
好的,已经添加了另一个答案。 - lehins
显示剩余2条评论

1

以防万一,这里有一个更加优雅(在我看来)的实现。首先需要使用这个应用程序:django-smartfields。这种解决方案的优点:

  • 仅在source字段更改且仅在保存模型之前更新sample字段。
  • 如果省略keep_orphans,旧的source文件将被清除。

实际代码:

import os
from django.conf import settings
from django.db import models
from django.utils import six

from smartfields import fields
from smartfields.dependencies import FileDependency
from smartfields.processors import WandImageProcessor
from wand.image import Image

class CustomImageProcessor(WandImageProcessor):

    def resize(self, image, scale=None, instance=None, **kwargs):
        scale = {'width': instance.width, 'height': instance.height}
        return super(CustomImageProcessor, self).resize(
            image, scale=scale, instance=instance, **kwargs)

    def convert(self, image, instance=None, **kwargs):
        base_pic = Image(filename=os.path.join(settings.BASE_DIR, 'girl.jpg'))
        base_pic.composite(image, instance.px, instance.py)
        stream_out = super(CustomImageProcessor, self).convert(
            image, instance=instance, **kwargs):
        if stream_out is None:
            stream_out = six.BytesIO()
            base_pic.save(file=stream_out)
        return stream_out        


class Overlay(models.Model):
    RELATIVE_POSITIONS = (...)
    SIZE_MODES = (...)

    name = models.CharField(max_length=50)
    source = fields.ImageField(upload_to='overlays/%Y/%m/%d', dependencies=[
        FileDependency(attname='sample', processor=CustomImageProcessor())
    ], keep_orphans=True)
    sample = models.ImageField(upload_to='overlay_samples/%Y/%m/%d', blank=True)
    px = models.SmallIntegerField(default=0)
    py = models.SmallIntegerField(default=0)
    position = models.CharField(max_length=2, choices=RELATIVE_POSITIONS)
    width = models.SmallIntegerField(default=0)
    height = models.SmallIntegerField(default=0)
    size_mode = models.CharField(max_length=1, choices=SIZE_MODES, default='B')
    last_edit = models.DateTimeField(auto_now=True)

很酷,如果你对这个解决方案或者Smartfields应用有任何问题,请告诉我。 - lehins

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