我的网站搭建(第45天) 上传头像

  • 发布时间:2017年2月24日 15:02
  • 作者:杨仕航
* 该文是基于Python2.7开发的,最新Python3.x和Django2.x视频教程可以前往 >> Django2.0视频教程

前面我初步添加了相关设置:我的网站搭建(第44天) 添加头像字段

现在需要进一步处理。前端我用cropper获取裁剪图片参数和提供UI界面。后台用PIL处理图片。

在前端只是提供一个页面选择图片,获取裁剪的参数,并非实际裁剪图片。

将前端的图片和相关参数上传到后台之后,再用PIL处理裁剪等功能。


1、安装或下载相关文件

cropper:https://github.com/fengyuanchen/cropper

(Python2.7) PIL:pip install pillow

另外,我前端页面引用了bootstrap和jquery。


2、设计前端页面

我刚接触cropper的时候,懵圈了。大量的代码不知道如何下载。摸索了几天,终于弄清楚了。

新建一个html模版页面,先引用相关文件。

<link rel="stylesheet" type="text/css" href="/static/css/cropper/cropper.min.css">
<script src="/static/js/cropper/cropper.min.js"></script>

再加入如下css样式,该样式是简单设置cropper的样式。

/*头像编辑*/
.avatar-wrapper {
  height: 370px;
  width: 100%;
  margin-top: 15px;
  box-shadow: inset 0 0 5px rgba(0,0,0,.25);
  background-color: #fcfcfc;
  overflow: hidden;
}

.avatar-wrapper img { display: block; height: auto; max-width: 100%;}

/*头像预览*/
.avatar-preview {
  margin-top: 15px;
  margin-right: 15px;
  border: 1px solid #eee;
  border-radius: 4px;
  background-color: #fff;
  overflow: hidden;
  height: 96px;
  width: 96px;
}

.avatar-preview:hover { border-color: #ccf; box-shadow: 0 0 5px rgba(0,0,0,.15);}


cropper界面分两部分,编辑部分和预览部分。在body主体加入cropper容器。

<div class="col-sm-10">
    <!--编辑区域-->
    <div class="avatar-wrapper" id='avatar-wrapper'>
        <img src="">
    </div>
</div>

<div class="col-sm-2">
    <!--头像预览-->
    <div class="avatar-preview"><img style="width: 96px; height: 96px;" src="/{{user.get_avatar_url}}"></div>
    <a id="avatar-upload" href="#" class="btn btn-primary disabled">上传头像</a>
</div>

<!--编辑相关数据-->
<form id="avatar_form">
    {%csrf_token%}
    <div>
        <input type="hidden" id="avatar_x" name="avatar_x">
        <input type="hidden" id="avatar_y" name="avatar_y">
        <input type="hidden" id="avatar_width" name="avatar_width">
        <input type="hidden" id="avatar_height" name="avatar_height">
    </div>
</form>

并在js代码部分,加入cropper初始化代码:

<script type="text/javascript">
    $(function(){
        //初始化裁剪器
        var image = $('#avatar-wrapper img');
        image.cropper({
            checkImageOrigin: true, //检查图片来源
            dragMode: 'move',   //图片可移动
            restore:false,      //窗体调整大小之后不自动恢复裁剪区域
            zoomOnWheel: false, //不允许通过鼠标滚轮缩放
            zoomOnTouch: false, //不允许通过触摸缩放
            aspectRatio: 1 / 1, //裁剪比例
            autoCropArea: 0.5,  //裁剪背景透明度
            autoCropArea: 1,    //自动裁剪的比例
            
            //文本的jQuery选择表达式,一个div
            preview: $(".avatar-preview").selector,
            crop: function (e) {
                //返回图片编辑相关数据
                $('#avatar_x').val(e.x);
                $('#avatar_y').val(e.y);
                $('#avatar_width').val(e.width);
                $('#avatar_height').val(e.height);
            },
        });
    });
</script>


这里还需要在Django后台写代码打开该页面。

#coding:utf-8
from django.shortcuts import render_to_response
from django.template import RequestContext

#装饰器,登录判断
def check_login(func):
    def wrapper(request):
        #登录判断
        if not request.user.is_authenticated():
            data = {}
            data['goto_url'] = '/'
            data['goto_time'] = 3000
            data['goto_page'] = True
            data['message'] = u'您尚未登录,请先登录'
            return render_to_response('message.html',data)
        else:
            return func(request)
    return wrapper
    
@check_login
def user_avatar(request):
    """修改头像页面"""
    data = {}
    data['user'] = request.user
    return render_to_response(
        'user/avatar.html', 
        data, 
        context_instance=RequestContext(request))


因为我们页面上传头像的时候需要POST提交,需要csrf验证。所以加入context_instance。

由于编辑区域的img标签没有设置src图片来源。

设置urls路由之后,该页面加载的效果如下:

20170224/20170224140911405.png


接下来,加一个选择本地图片并加载图片的功能按钮到form标签中。

<label class="btn btn-primary" for="avatar-input">本地图片</label>
<input style="display:none" type="file" class="avatar-input" id="avatar-input" name="avatar_file" accept=".jpg,.jpeg,.png">

加载图片代码:

//选择图片
$("#avatar-input").change(function(){
    var URL = window.URL || window.webkitURL;
    if(URL){
        var files = this.files;
        if (files && files.length){
            var file = files[0];
            if (/^image\/\w+$/.test(file.type)) {
              var blobURL = URL.createObjectURL(file);
              image.cropper('reset').cropper('replace', blobURL);
              $('.avatar_crop .disabled').removeClass('disabled');
              normal_tip('本地图片:可调整到最佳状态再上传');
            } else {
              error_tip('请选择一张图片');
            }
        }
    }
});

其中,第10行代码中的image变量已经是前面获取到的cropper对象。

var image = $('#avatar-wrapper img');

点击选择一张图片,效果如下图:

20170224/20170224142329902.png


3、上传图片到后台

接下来,将编辑好的图片上传到后台。后台裁剪处理之后,保存头像。

这里我采用ajax上传图片。后台需要写一个接口给该ajax使用。

#coding:utf-8
from django.http import HttpResponse
from django.conf import settings

import os
import uuid
import json

@check_login
def user_avatar_upload(request):
    """上传头像"""
    data = {}
    
    #判断图片来源(本地上传 or 在线图片)

    #获取临时路径
    if request.FILES.has_key('avatar_file'):
        #本地上传
        avatar_file = request.FILES['avatar_file']
        temp_folder = os.path.join(settings.BASE_DIR, 'static', 'temp')
        if not os.path.isdir(temp_folder):
            os.makedirs(temp_folder)

        temp_filename = uuid.uuid1().hex + os.path.splitext(avatar_file.name)[-1]
        temp_path = os.path.join(temp_folder, temp_filename)

        #保存上传的文件
        with open(temp_path, 'wb') as f:
            for chunk in avatar_file.chunks():
                f.write(chunk)
    else:
        #在线图片
        #该功能尚未开发,先留好位置
        data['success'] = False
        return HttpResponse(json.dumps(data), content_type="application/json")

    #裁剪图片
    top = int(float(request.POST['avatar_y']))
    buttom = top + int(float(request.POST['avatar_height']))
    left = int(float(request.POST['avatar_x']))
    right = left + int(float(request.POST['avatar_width']))

    #python2.7 pillow
    from PIL import Image
    im = Image.open(temp_path)
    #裁剪图片
    crop_im = im.convert("RGBA").crop((left, top, right, buttom)).resize((64, 64), Image.ANTIALIAS)

    #设置背景颜色为白色
    out = Image.new('RGBA', crop_im.size, (255,255,255))
    out.paste(crop_im, (0, 0, 64, 64), crop_im)

    #保存图片
    out.save(temp_path)

    #保存记录
    avatar = request.user.set_avatar_url(temp_path)
    os.remove(temp_path)
    
    data['success'] = True
    data['avatar_url'] = avatar.avatar.url
    return HttpResponse(json.dumps(data), content_type="application/json")

此处,有好几个地方需要注意。

1)check_login 装饰器同样是前面代码中同一个装饰器。

2)我打算提供两种图片来源:本地图片和在线图片。在线图片尚未开发,先留空

3)图片处理需要安装pil

4)本地图片处理流程:下载图片,保存临时图片,裁剪图片,保存头像记录,删除临时图片

5)保存记录我使用user.set_avatar_url方法。该方法不是django自带的,是我后期绑定上去的方法,方便操作。该方法可以参考我的网站搭建(第44天) 添加头像字段


打开User_Avatar模型所在的models.py。加入set_avatar_url方法的代码:

#coding:utf-8
from django.db import models
from django.contrib.auth.models import User
from django.conf import settings

from types import MethodType #类动态绑定方法
import os, shutil

#给user设置头像的方法
def set_avatar_url(self, src_path):
    try:
        avatar = User_Avatar.objects.get(user=self.id)
        old_path = os.path.join(settings.BASE_DIR, avatar.avatar.url) #旧的头像路径
        old_filename = os.path.splitext(os.path.split(old_path)[-1])[0]

        #获得起始编号
        start_num = int(old_filename.split('_')[-1]) + 1
    except Exception as e:
        avatar = User_Avatar(user=self)
        start_num = 0
        old_path = ''

    #根据user id设置新的头像名称
    filename = os.path.split(src_path)[-1]
    img_format = os.path.splitext(filename)[-1]

    while True:
        new_filename = '%s_64_%s%s' % (self.id, start_num, img_format)
        new_path = os.path.join(settings.BASE_DIR, AVATAR_ROOT, new_filename)
        if not os.path.isfile(new_path):
            break
        start_num += 1

    #保存头像
    shutil.copy(src_path, new_path)
    avatar.avatar = os.path.join(AVATAR_ROOT, new_filename)
    avatar.save()

    #删除旧文件
    if os.path.isfile(old_path):
        os.remove(old_path)
    return avatar

#动态绑定方法
User.set_avatar_url = MethodType(set_avatar_url, None, User)

为了解决图片缓存问题。让前端及时更新图片,我生成不同的头像名称,即不同的头像地址。

相关User_Avatar模型可见我的网站搭建(第44天) 添加头像字段


4、解决csrf问题

由于我该form表单的数据提交方式是POST,会有403拒绝访问的处理问题。

这个需要通过jquery.cookies.js处理。

<!--引用cookie处理js-->
<script src="/static/js/jquery.cookie.js"></script>

<!--获取csrf信息,并设置ajax-->
function csrfSafeMethod(method) {
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
    beforeSend: function(xhr, settings) {
        if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            var csrftoken = $.cookie('csrftoken');
            xhr.setRequestHeader("X-CSRFToken", csrftoken);
        }
    }
});

该代码在加载页面的时候,执行即可。


5、ajax上传头像

设置urls路由,让前端可以访问刚刚写的响应方法。

ajax提交文件需要通过FormData对象,代码如下:

//上传头像
$("#avatar-upload").click(function(){
    if($('#avatar-wrapper img').attr('src')==''){
        alert('亲~ 你是不是忘记选择图片了');
        return false;
    }

    //注意:FormData中的[0]是必备的
    var formData = new FormData($("#avatar_form")[0]);  
    $.ajax({  
        url: "{%url 'user_avatar_upload'%}",  
        type: 'POST',  
        data: formData,  
        async: false, 
        cache: false, 
        contentType: false,  
        processData: false, 
        success: function (data){ 
            //更新导航右上角的头像
            $('.navbar-avatar').attr('src', '/' + data['avatar_url']);
            alert("上传头像成功");
        },  
        error: function (err) {
            alert("提交失败,请重试!" + err);
        }
    });
    return false;
});


6、美化和优化

前面的代码是核心的代码。你可以再加一些东西优化一下,完善用户体验。

cropper提供缩放,旋转的功能。个人认为旋转其实没必要,就没添加。

最后效果如下,当然“在线图片”功能还没开发,先放一个按钮在那里。

20170224/20170224150043276.png


缩放和复位按钮对应代码如下:

//缩放按钮
var zoom = 1;
$("#zoom-in").click(function(){
    if(zoom<1.5){
        zoom += 0.1;
        image.cropper("zoom", 0.1);
    }                
});
$("#zoom-out").click(function(){
    if(zoom>0.5){
        zoom -= 0.1;
        image.cropper("zoom", -0.1);
    }                
});

//复位按钮
$('#reset').click(function(){
    image.cropper("reset");
    zoom = 1;
});


上一篇:我的网站搭建(第46天) 在线头像

下一篇:Python用win32api操作注册表

相关专题: Django评论库开发   

评论列表

744421791@qq.com

744421791@qq.com

⁣博主,我按照您的步骤,现在我想不明白,之前加载到网页中裁剪的那张图片是怎么通过 ajax 传递给 view 视图的?

2017-08-21 18:26 回复

  • 744421791@qq.com

    已经解决了,少写了几个参数。

    2017-08-21 19:46 回复

梦落の无声

梦落の无声

晕乎乎的

2018-12-25 22:13 回复

新的评论

清空