簡體   English   中英

Django - 保存圖像的多個版本

[英]Django - Save multiple versions of an Image

我的應用程序需要保存上傳圖像的多個版本。 一張高質量圖像和另一張僅用於縮略圖(低質量)。 目前這大部分時間都在工作,但有時保存方法會失敗,我的所有縮略圖都被刪除,尤其是當我在表單中使用 remove_cover 復選框時

raise ValueError("'%s' 屬性沒有與之關聯的文件。" % self.field.name) app | ValueError:“postcover_tn”屬性沒有與之關聯的文件。

-> 在此處查看完整跟蹤: https://pastebin.com/hgieMGet

模型.py

class Post(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField()
    content = models.TextField(blank=False)
    postcover = models.ImageField(
        verbose_name="Post Cover",
        blank=True,
        null=True,
        upload_to=image_uploads,
    )
    postcover_tn = models.ImageField(
        verbose_name="Post Cover Thumbnail",
        blank=True,
        null=True,
        upload_to=image_uploads,
    )
    published_date = models.DateTimeField(auto_now_add=True, null=True)

    def save(self, *args, **kwargs):
        super(Post, self).save(*args, **kwargs)
        if self.postcover:
            if not (self.postcover_tn and os.path.exists(self.postcover_tn.path)):
                image = Image.open(self.postcover)
                outputIoStream = BytesIO()
                baseheight = 500
                hpercent = baseheight / image.size[1]
                wsize = int(image.size[0] * hpercent)
                imageTemproaryResized = image.resize((wsize, baseheight))
                imageTemproaryResized.save(outputIoStream, format='PNG')
                outputIoStream.seek(0)
                self.postcover = InMemoryUploadedFile(outputIoStream, 'ImageField',
                                                      "%s.png" % self.postcover.name.split('.')[0], 'image/png',
                                                      sys.getsizeof(outputIoStream), None)
                image = Image.open(self.postcover)
                outputIoStream = BytesIO()
                baseheight = 175
                hpercent = baseheight / image.size[1]
                wsize = int(image.size[0] * hpercent)
                imageTemproaryResized = image.resize((wsize, baseheight))
                imageTemproaryResized.save(outputIoStream, format='PNG')
                outputIoStream.seek(0)
                self.postcover_tn = InMemoryUploadedFile(outputIoStream, 'ImageField',
                                                      "%s.png" % self.postcover.name.split('.')[0], 'image/png',
                                                      sys.getsizeof(outputIoStream), None)
        elif self.postcover_tn:
            self.postcover_tn.delete()

        super(Post, self).save(*args, **kwargs)

看來我也無法正確解決:

  • self.postcover_tn.delete() -> class 'InMemoryUploadedFile' 的未解析屬性引用 'delete'
  • self.postcover_tn.path -> class 'InMemoryUploadedFile' 的未解析屬性引用'path'

forms.py:

def save(self, commit=True):
    instance = super(PostForm, self).save(commit=False)
    if self.cleaned_data.get('remove_cover'):
        try:
            os.unlink(instance.postcover.path)
        except OSError:
            pass
        instance.postcover = None
    if commit:
        instance.save()
    return instance

也許如果我們從另一個角度來看問題,我們可以開箱即用地解決它。 在處理圖像(添加、更新和刪除)以及如何解決您的問題時, signals非常方便:

models.py中:

# from django.template.defaultfilters import slugify


class Post(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField()
    # slug = models.SlugField('slug', max_length=255,
        # unique=True, null=True, blank=True,
        # help_text='If blank, the slug will be generated automatically from the given title.'
    # )
    content = models.TextField(blank=False)

    # ------------------------------------------------------------------------------------

    # rename images with the current post id/pk (which is UUID) and keep the extension
    # for cover thumbnail we append "_thumbnail" to the name 

    # e.g: 
    # img/posts/77b122a3d241461b80c51adc41d719fb.jpg
    # img/posts/77b122a3d241461b80c51adc41d719fb_thumbnail.jpg

    def upload_cover(instance, filename):
        ext = filename.split('.')[-1]
        filename = '{}.{}'.format(instance.id, ext)
        path = 'img/posts/'
        return '{}{}'.format(path, filename)

    postcover = models.ImageField('Post Cover',
        upload_to=upload_cover,  # callback function
        null=True, blank=True,
        help_text=_('Upload Post Cover.')
    )

    def upload_thumbnail(instance, filename):
        ext = filename.split('.')[-1]
        filename = '{}_thumbnail.{}'.format(instance.id, ext)
        path = 'img/posts/'
        return '{}{}'.format(path, filename)

    postcover_tn = models.ImageField('Post Cover Thumbnail',
        upload_to=upload_thumbnail,  # callback function
        null=True, blank=True,
        help_text=_('Upload Post Cover Thumbnail.')
    )

    # ------------------------------------------------------------------------------------

    published_date = models.DateTimeField(auto_now_add=True, null=True)

    def save(self, *args, **kwargs):

        # i moved the logic to signals

        # if not self.slug:
            # self.slug = slugify(self.title)
        super(Post, self).save(*args, **kwargs)

創建新文件並將其重命名為signals.py (靠近models.py ):

import io
import sys

from PIL import Image

from django.core.files.uploadedfile import InMemoryUploadedFile
from django.dispatch import receiver
from django.db.models.signals import pre_save, pre_delete

from .models import Post

#  DRY
def image_resized(image, h):
    name = image.name
    _image = Image.open(image)
    content_type = Image.MIME[_image.format]
    r = h / _image.size[1]  # ratio
    w = int(_image.size[0] * r)
    imageTemproaryResized = _image.resize((w, h))
    file = io.BytesIO()
    imageTemproaryResized.save(file, _image.format)
    file.seek(0)
    size = sys.getsizeof(file)
    return file, name, content_type, size


@receiver(pre_save, sender=Post, dispatch_uid='post.save_image')
def save_image(sender, instance, **kwargs):

    # add image (cover | thumbnail)
    if instance._state.adding:

        #  postcover
        file, name, content_type, size = image_resized(instance.postcover, 500)
        instance.postcover = InMemoryUploadedFile(file, 'ImageField', name, content_type, size, None)

        #  postcover_tn
        file, name, content_type, size = image_resized(instance.postcover_tn, 175)
        instance.postcover_tn = InMemoryUploadedFile(file, 'ImageField', name, content_type, size, None)


    # update image (cover | thumbnail)
    if not instance._state.adding:
        # we have 2 cases:
        # - replace old with new
        # - delete old (when 'clear' checkbox is checked)

        #  postcover
        old = sender.objects.get(pk=instance.pk).postcover
        new = instance.postcover
        if (old and not new) or (old and new and old.url != new.url):
            old.delete(save=False)

        #  postcover_tn
        old = sender.objects.get(pk=instance.pk).postcover_tn
        new = instance.postcover_tn
        if (old and not new) or (old and new and old.url != new.url):
            old.delete(save=False)


@receiver(pre_delete, sender=Post, dispatch_uid='post.delete_image')
def delete_image(sender, instance, **kwargs):
    s = sender.objects.get(pk=instance.pk)

    if (not s.postcover or s.postcover is not None) and (not s.postcover_tn or s.postcover_tn is not None):
        s.postcover.delete(False)
        s.postcover_tn.delete(False)

apps.py中:

我們需要在apps.py中注冊信號,因為我們使用了裝飾器@receiver

from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _


class BlogConfig(AppConfig):  # change to the name of your app
    name = 'blog'  # and here
    verbose_name = _('Blog Entries')

    def ready(self):
        from . import signals

這是post管理區域的第一個屏幕截圖

在此處輸入圖像描述

由於縮略圖是從封面生成的,因此作為 UI/UX 的良好做法,無需為封面縮略圖顯示第二個輸入文件(我在admin.py第二個圖像字段保持為只讀)。

下面是我上傳圖片后的第二張截圖

PS:截圖是從我正在開發的另一個應用程序中截取的,所以變化不大,你應該看到

  • 發布封面而不是特色圖片
  • Currently: img/posts/8b0be417db564c53ad06cb493029e2ca.jpg (參見models.py中的upload_cover()Currently: img/blog/posts/featured/8b0be417db564c53ad06cb493029e2ca.jpg

在此處輸入圖像描述

admin.py

# "img/posts/default.jpg" and "img/posts/default_thumbnail.jpg" are placeholders
# grab to 2 image placeholders from internet and put them under "/static" folder

def get_post_cover(obj):
    src = obj.postcover.url if obj.postcover and \
    hasattr(obj.postcover, 'url') else os.path.join(
        settings.STATIC_URL, 'img/posts/default.jpg')
    return mark_safe('<img src="{}" height="500" style="border:1px solid #ccc">'.format(src))
get_post_cover.short_description = ''
get_post_cover.allow_tags = True

def get_post_cover_thumbnail(obj):
    src = obj.postcover_tn.url if obj.postcover_tn and \
    hasattr(obj.postcover_tn, 'url') else os.path.join(
        settings.STATIC_URL, 'img/posts/default_thumbnail.jpg')
    return mark_safe('<img src="{}" height="175" style="border:1px solid #ccc">'.format(src))
get_post_cover_thumbnail.short_description = ''
get_post_cover_thumbnail.allow_tags = True


class PostAdmin(admin.ModelAdmin):
    list_display = ('title', .. )
    fields = (
        'author', 'title', 'content',
        get_post_cover, get_post_cover_thumbnail, 'postcover',
    )
    readonly_fields = (get_post_cover, get_post_cover_thumbnail)

[..]

最后,您在 forms.py 中的save() forms.py中不需要任何刪除邏輯

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM