You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django 3.2 API将GIF返回为JPG问题排查求助

问题描述

向Django 3.2 API上传.gif文件时,经Postman确认Flutter应用发送的是GIF文件,但后端返回结果却是JPG文件。排查后确认问题出在后端,怀疑程序误将GIF的第一帧当作图片上传,现附上检查文件MIME子类型并转换GIF为视频的相关代码,寻求问题定位与解决方法。

相关代码
def add_media(self, file, order=None):
    check_can_add_media(post=self)

    is_in_memory_file = isinstance(file, InMemoryUploadedFile) or isinstance(file, SimpleUploadedFile)

    if is_in_memory_file:
        file_mime = magic.from_buffer(file.read())
    elif isinstance(file, TemporaryUploadedFile):
        file_mime = magic.from_file(file.temporary_file_path())
    else:
        file_mime = magic.from_file(file.name)

    check_mimetype_is_supported_media_mimetypes(file_mime)
    # Mime check moved pointer
    file.seek(0)

    file_mime_types = file_mime.split('/')

    file_mime_type = file_mime_types[0]
    file_mime_subtype = file_mime_types[1]

    temp_files_to_close = []

    if file_mime_subtype == 'gif':
                if is_in_memory_file:
                    file = write_in_memory_file_to_disk(file)

                temp_dir = tempfile.gettempdir()
                converted_gif_file_name = os.path.join(temp_dir, str(uuid.uuid4()) + '.mp4')

                ff = ffmpy.FFmpeg(
                    inputs={file.temporary_file_path() if hasattr(file, 'temporary_file_path') else file.name: None},
                    outputs={converted_gif_file_name: None})
                ff.run()
                converted_gif_file = open(converted_gif_file_name, 'rb')
                temp_files_to_close.append(converted_gif_file)
                file = File(file=converted_gif_file)
                file_mime_type = 'video'

            has_other_media = self.media.exists()
    
    if file_mime_type == 'image':
        post_image = self._add_media_image(image=file, order=order)
        if not has_other_media:
            self.media_width = post_image.width
            self.media_height = post_image.height
            self.media_thumbnail = file

    elif file_mime_type == 'video':
        post_video = self._add_media_video(video=file, order=order)
        if not has_other_media:
            self.media_width = post_video.width
            self.media_height = post_video.height
            self.media_thumbnail = post_video.thumbnail.file
    else:
        raise ValidationError(
            _('Unsupported media file type')
        )

    for file_to_close in temp_files_to_close:
        file_to_close.close()
            
        
    self.save()       
def _add_media_image(self, image, order):
    return PostImage.create_post_media_image(image=image, post_id=self.pk, order=order)

def _add_media_video(self, video, order):
    return PostVideo.create_post_media_video(file=video, post_id=self.pk, order=order)


@classmethod
    def create_post_media_image(cls, image, post_id, order):
        hash = sha256sum(file=image.file)
        post_image = cls.objects.create(image=image, post_id=post_id, hash=hash, thumbnail=image)
        PostMedia.create_post_media(type=PostMedia.MEDIA_TYPE_IMAGE,
                                    content_object=post_image,
                                    post_id=post_id, order=order)
        return post_image


@classmethod
    def create_post_media_video(cls, file, post_id, order):
        hash = sha256sum(file=file.file)
        video_backend = get_backend()

        if isinstance(file, InMemoryUploadedFile):
            # If its in memory, doing read shouldn't be an issue as the file should be small.
            in_disk_file = write_in_memory_file_to_disk(file)
            thumbnail_path = video_backend.get_thumbnail(video_path=in_disk_file.name, at_time=0.0)
        else:
            thumbnail_path = video_backend.get_thumbnail(video_path=file.file.name, at_time=0.0)

        with open(thumbnail_path, 'rb+') as thumbnail_file:
            post_video = cls.objects.create(file=file, post_id=post_id, hash=hash, thumbnail=File(thumbnail_file), )
        PostMedia.create_post_media(type=PostMedia.MEDIA_TYPE_VIDEO,
                                    content_object=post_video,
                                    post_id=post_id, order=order)
        return post_video
        
    
问题定位与修复建议

1. 代码缩进错误(核心问题)

add_media方法中has_other_media = self.media.exists()的缩进错误,被包含在if file_mime_subtype == 'gif':代码块内,导致两种异常:

  • 上传非GIF文件时,has_other_media变量未定义,后续if not has_other_media:会抛出NameError。
  • 处理GIF文件时,缩进混乱可能导致file_mime_type = 'video'赋值未生效,程序仍走图片处理分支,而Django的ImageField默认会将GIF转换为JPG格式存储。

修复方式:将该行代码移到GIF处理块外,保持同级缩进:

if file_mime_subtype == 'gif':
        # ... 原有GIF转换逻辑 ...
        file_mime_type = 'video'

    # 修正缩进,移到GIF处理块外
    has_other_media = self.media.exists()

2. FFmpeg转换参数缺失

默认调用FFmpeg转换GIF到MP4时未指定编码格式、帧率等关键参数,可能导致转换后的文件无法被正确识别为视频。建议补充参数:

ff = ffmpy.FFmpeg(
    inputs={file.temporary_file_path() if hasattr(file, 'temporary_file_path') else file.name: None},
    outputs={converted_gif_file_name: '-f mp4 -vcodec libx264 -pix_fmt yuv420p -r 10'}
)

参数说明:

  • -vcodec libx264:使用H.264编码,保证MP4兼容性
  • -pix_fmt yuv420p:使用通用像素格式,避免播放器识别失败
  • -r 10:设置帧率为10fps,匹配GIF常规帧率

3. Django File对象缺少name属性

将转换后的文件包装为Django的File对象时,未指定name属性,可能导致后续视频处理逻辑出错。建议补充文件名:

file = File(file=converted_gif_file, name=os.path.basename(converted_gif_file_name))

4. 调试MIME类型检测

添加日志打印file_mime的值,确认magic库是否正确识别GIF的MIME类型为image/gif,避免子类型判断失效:

import logging
logger = logging.getLogger(__name__)

# 在获取file_mime后添加
logger.info(f"Detected file MIME type: {file_mime}")

5. 图片字段格式处理(可选)

若需直接保存GIF图片而非转换为视频,需修改PostImage模型的thumbnail字段配置,确保Pillow保留GIF格式:

from django.db import models
from PIL import Image

class PostImage(models.Model):
    thumbnail = models.ImageField(upload_to='thumbnails')

    def save(self, *args, **kwargs):
        if self.thumbnail:
            img = Image.open(self.thumbnail)
            if img.format == 'GIF':
                self.thumbnail.format = 'GIF'
        super().save(*args, **kwargs)

内容的提问来源于stack exchange,提问作者Ananth

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 00:22:32