【问题标题】:Django: changing image size and upload to S3Django:更改图像大小并上传到 S3
【发布时间】:2016-12-08 10:00:48
【问题描述】:

我继承了一个 Django 项目,我们已将图像移至 S3

其中一个模型是典型的用户配置文件

class Profile(UUIDBase):

    first_name = models.CharField(_("First Name"), max_length=20)
    last_name = models.CharField(_("Last Name"), max_length=20, null=True)
    profile_image = models.ImageField(
        _("Profile Image"),
        upload_to=profile_image_name,
         max_length=254,
        blank=True,
        null=True
    )
    profile_image_thumb = models.ImageField(
        _("Profile Image Thumbnail"),
        upload_to=profile_image_name,
        max_length=254,
        blank=True,
       null=True
    )
    ... other fields

profile_image_name 是一个函数:

def profile_image_name(instance, filename):
    if filename:
        target_dir = 'uploads/profile_img/'
        _, ext = filename.rsplit('.', 1)
        filename = str(instance.uid) + '.' + ext
        return '/'.join([target_dir, filename])

我有一些有效的代码:

@shared_task
def resize_image(image_path, dim_x, append_str='_resized', **kwargs):
    '''
    resize any image_obj while maintaining aspect ratio
    '''
    orig = storage.open(image_path, 'r')
    im = Image.open(orig, mode='r')
    new_y = (float(dim_x) * float(im.height)) / float(im.width)
    new_im = im.resize((dim_x, int(new_y)), Image.ANTIALIAS)
    img_path, img_name = path.split(image_path)
    file_name, img_ext = img_name.rsplit('.', 1)
    new_img_path = path.join(img_path, file_name + append_str + '.' + img_ext)
    try:
        new_f = storage.open(new_img_path, 'w')
    except IOError as e:
        logger.critical("Caught IOError in {}, {}".format(__file__, e))
        ravenclient.captureException()
        return None
    try:
        new_im.save(new_f)
    except IOError as e:
        logger.critical("Caught IOError in {}, {}".format(__file__, e))
        ravenclient.captureException()
       return None
    except Exception as e:
       logger.critical("Caught unhandled exception in {}. {}".format(
        __file__, e)
       )
       ravenclient.captureException()
       return None
    im.close()
    new_im.close()
    new_f.close()
    return new_img_path

从 post_save 信号处理程序调用:

@receiver(post_save, sender=Profile, dispatch_uid='resize_profile_image')
def resize_profile_image(sender, instance=None, created=False, **kwargs):
    if created:
        if instance.profile_image:
            width, height = image_dimensions(instance.profile_image.name)
            print(width, height)
            if width > MAX_WIDTH:
                result = resize_image.delay(instance.profile_image.name, MAX_WIDTH)
                instance.profile_image.name = result.get()
            if width > THUMB_WIDTH:
                result = resize_image.delay(
                    instance.profile_image.name,
                    THUMB_WIDTH, 
                    append_str='_thumb'
                )
                instance.profile_image_thumb.name = result.get()
            try:
                instance.save()
            except Exception as e:
                log.critical("Unhandled exception in {}, {}".format(__name__, e))
                ravenclient.captureException()

其目的是获取上传的图像并将其大小调整为 1) 移动设备可以显示的最大宽度和 2) 50 像素的缩略图,以便在移动应用中使用。

当我在 S3 上查看时,我看不到我调整大小的图像或缩略图。然而,单元测试(彻底)没有给出任何错误。

当我得到图像尺寸时:

def image_dimensions(image_path):
    f = storage.open(image_path, 'r')
    im = Image.open(f, 'r')
    height = im.height
    width = im.width
    im.close()
    f.close()
    return (width, height)

访问对象的 ImageField 没有问题。当我使用 default_storage 打开实例的 profile_image 时,我没有收到任何错误。 PIL方法

new_im = im.resize((dim_x, int(new_y)), Image.ANTIALIAS) 确实返回了“PIL.Image.Image”类的新实例。

事实上(请原谅我的冗长)

这不会引发错误:

>>> u = User(email="root@groupon.com", password="sdfbskjfskjfskjdf")
>>> u.save()
>>> p = Profile(user=u, profile_image=create_image_file())
>>> p.save()
>>> from django.core.files.storage import default_storage as storage
>>> orig = storage.open(p.profile_image.name, 'r')
>>> orig
<S3BotoStorageFile: uploads/profile_img/b0fd4f00-cce6-4dd3-b514-4c46a801ab19.jpg>
>>> im = Image.open(orig, mode='r')
>>> im
<PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=5000x5000 at 0x10B8F1FD0>
>>> im.__class__
<class 'PIL.JpegImagePlugin.JpegImageFile'>
>>> dim_x = 500
>>> new_y = (float(dim_x) * float(im.height)) / float(im.width)
>>> new_im = im.resize((dim_x, int(new_y)), Image.ANTIALIAS)
>>> new_im.__class__
<class 'PIL.Image.Image'>
>>> img_path, img_name = path.split(p.profile_image.name)
>>> file_name, img_ext = img_name.rsplit('.', 1)
>>> append_str='_resized'
>>> new_img_path = path.join(img_path, file_name + append_str + '.' + img_ext)
>>> new_f = storage.open(new_img_path, 'w')
>>> new_f
<S3BotoStorageFile: uploads/profile_img/b0fd4f00-cce6-4dd3-b514-4c46a801ab19_resized.jpg>
>>> new_im.save(new_f)  #### This does NOT create an S3 file!!!!
>>> im.close()
>>> new_im.close()
>>> new_f.close()

&gt;&gt;&gt; p.save() 将新的配置文件图像上传到 S3。我期待&gt;&gt;&gt; new_im.save(new_f) 将图像文件写入 S3。但事实并非如此。

非常感谢您提供任何见解或帮助,并感谢您抽出时间来研究这个问题。

编辑 ...

我的设置:

AWS_STORAGE_BUCKET_NAME = 'testthis'
AWS_S3_CUSTOM_DOMAIN = '%s.s3.amazonaws.com' % AWS_STORAGE_BUCKET_NAME
MEDIAFILES_LOCATION = 'media'
MEDIA_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, MEDIAFILES_LOCATION)
DEFAULT_FILE_STORAGE = 'custom_storages.MediaStorage'

custom_storage.py 在哪里

from django.conf import settings
from storages.backends.s3boto import S3BotoStorage

class MediaStorage(S3BotoStorage):
    location = settings.MEDIAFILES_LOCATION
    bucket_name = settings.AWS_STORAGE_BUCKET_NAME

【问题讨论】:

  • 有一个新问题:AttributeError: 'S3BotoStorageFile' object has no attribute 'startswith' changed: orig = default_storage.open(image_path, 'r') im = Image.open(orig) 现在我明白了错误任务 common.image_lib.resize_image[48340adf-f3aa-4227-ba24-1917d4e703ef] 引发意外:AttributeError("'S3BotoStorageFile' object has no attribute 'startswith'",)
  • 我已经用PNG文件测试了代码,它可以工作: >>> new_im.save(new_f, 'PNG') 实际上在S3上创建了一个新文件但是对应的不适用于JPEG!这可能是 PIL 错误吗?

标签: python django amazon-s3 python-imaging-library image-resizing


【解决方案1】:

我认为整个设置很疯狂。我强烈建议您考虑使用像 django-versatileimagefield 这样的库。实现如下所示:

from versatileimagefield.fields import VersatileImageField
from versatileimagefield.image_warmer import VersatileImageFieldWarmer

class Profile(UUIDBase):
    first_name = models.CharField(_("First Name"), max_length=20)
    last_name = models.CharField(_("Last Name"), max_length=20, null=True)
    image = VersatileImageFied(upload_to='uploads/profile_img/', blank=True, null=True)


@receiver(models.signals.post_save, sender=Profile)
def warm_profile_image(sender, instance, **kwargs):
    if instance.image:
        VersatileImageFieldWarmer(instance_or_queryset=instance, rendition_key_set='profile_image', image_attr='image', verbose=True).warm()

在您的设置中:

VERSATILEIMAGEFIELD_RENDITION_KEY_SETS = {
    'profile_image': [
        ('cropped', 'crop__400x400'),
        ('thumbnail', 'thumbnail__20x20')
    ]
}

Profile warmer 可以毫无问题地创建保存到 S3 的表示。您可以使用profile.image 访问完整图像,也可以使用profile.image.croppedprofile.image.thumbnail 访问不同版本。该库甚至允许您设置兴趣点,以便在图像中的特定中心点周围进行裁剪。

如果使用 DRF,则为序列化器:

from versatileimagefield.serializers import VersatileImageFieldSerializer   

class ProfileSerializer(serializers.ModelSerializer):
    image = VersatileImageFieldSerializer(
        sizes=[
            ('cropped', 'crop__400x400'),
            ('thumbnail', 'thumbnail__20x20')
        ],
        required=False
    )
    ... other fields and the Meta class

【讨论】:

    【解决方案2】:

    这个问题似乎与 PIL 的 JPEG 库有关:

    >>> u = User(email="root@groupon.com", password="sdfbskjfskjfskjdf")
    >>> u.save()
    >>> p = Profile(user=u, profile_image=create_image_file())
    >>> p.save()
    >>> from django.core.files.storage import default_storage as storage
    >>> orig = storage.open(p.profile_image.name, 'r')
    >>> orig
    <S3BotoStorageFile: uploads/profile_img/b0fd4f00-cce6-4dd3-b514-4c46a801ab19.png>
    >>> im = Image.open(orig, mode='r')
    >>> im.__class__
    <class 'PIL.PngImagePlugin.PngImageFile'>
    >>> dim_x = 500
    >>> new_y = (float(dim_x) * float(im.height)) / float(im.width)
    >>> new_im = im.resize((dim_x, int(new_y)), Image.ANTIALIAS)
    >>> new_im.__class__
    <class 'PIL.Image.Image'>
    >>> img_path, img_name = path.split(p.profile_image.name)
    >>> file_name, img_ext = img_name.rsplit('.', 1)
    >>> append_str='_resized'
    >>> new_img_path = path.join(img_path, file_name + append_str + '.' + img_ext)
    >>> new_f = storage.open(new_img_path, 'w')
    >>> new_f
    <S3BotoStorageFile: uploads/profile_img/b0fd4f00-cce6-4dd3-b514-4c46a801ab19_resized.png>
    >>> new_im.save(new_f)  #### This does create a file on S3!
    >>> im.close()
    >>> new_im.close()
    >>> new_f.close()
    

    【讨论】:

      【解决方案3】:

      问题出在 django-storages 中。 S3BotoStorageFile 的 _file 属性是一个 SpooledTemporaryFile,适用于 png 文件,但不适用于 jpg 文件。

      见:https://github.com/jschneier/django-storages/issues/155

      【讨论】:

        猜你喜欢
        • 2023-04-11
        • 2011-09-18
        • 2015-06-18
        • 2015-12-13
        • 2011-10-04
        • 1970-01-01
        • 2015-05-19
        • 2015-04-08
        • 2021-12-09
        相关资源
        最近更新 更多