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

Save an image generated with wand to django ImageField

我正在尝试为存储在 django 模型中的 "overlay" 配置生成预览,稍后将应用于其他模型。我没有太多使用 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)

但我正在读取这里的 AttributeError 是我的 Django 调试数据的一部分:

 /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'

怎么了?

无论何时将文件保存到 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 模型时执行此操作,即使原始图像相同。但如果很少保存,应该不是问题。

已解决!

例如@Alexey Kuleshevich 说django FileField 需要一个文件objeto,但缺少的是我们必须首先将图像保存到磁盘或内存中的文件中,正如我们猜测的那样这是更好的记忆...所以这是最终的解决方案。我认为可以改进不使用两步 "conversion"

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)

感谢这个回答: How do you convert a PIL Image to a Django File?

以防万一,这里有一个更优雅(在我看来)的实现。首先,它需要这个应用程序: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)