[整合]头像上传

国庆过去了,skidu终于不负自己滴期望又玩过去了T_T
不过之前预告的东东好歹还是要放出来才好的。
嗯,下面进入正题

实现目标:仿 Flash 用户头像上传。
准备工作 -- 需要用到的插件or知识:


先放出一份skidu假期里写的一份demo: http://www.skidu.me/demo/avatar/index.php
PS:
·demo是随意写的,神马兼容性啥的skidu没有特意处理了,建议使用FireFox、Chrome、IE8+ 浏览器访问。
·头像上传区域选择框那里还有很多需要处理的地方skidu并没有去完善,比如上传了一张很大的图片后那里该怎么显示以及这种情况下后端裁剪应该怎么处理等,亲们不必纠结这个问题 :)

下面就简单分享一下这个demo的思路及实现流程:

①点击“选择”按钮弹出文件选择框->②选择文件->③点击“打开”,程序上传图像文件到服务器->④页面弹出一个层让用户可以选择需要上传的实际尺寸及区域->⑤点击“提交”完成图像的裁剪 或者点击“关闭”则从服务器中删除该文件

① 话说默认的文件上传按钮太难看。。skidu这里简单处理了一下。。具体处理方法见“准备工作”最后一条中所提到的日志

②、③
鉴于网络上传可能比较慢,skidu加了一个巨丑无比的“上传中”提示字样

④嗯。。选择需要上传的区域

⑤这里skidu就直接点确定了。图像在后台裁剪完成后页面会弹一个同样丑陋的弹框,然后页面中的图像随即被替换掉

好了。这个demo的大致操作流程如此,下面放出一些关键代码
1、上传按钮处理
html部分

<form id="myform" action="upload.php" method="post" target="upload_area" enctype="multipart/form-data">
    <a id="submit">选择</a>
    <input type="file" id="file_button" name="avatar" />
</form>

css部分

#submit{
    width:50px;
    height:20px;
    line-height: 20px;
    display:inline-block;
    border:1px solid #aaa;
    text-align: center;
    float: left;
    margin-left: 10px;
    cursor: pointer;
}

#submit.active,
#submit:hover {
    background: #8DC221;
    border:1px solid #8DC221;
    color:#fff;
}
/*原始文件选择按钮*/
#file_button{
    cursor: pointer;
    opacity: 0;               /*这里设置透明度为0*/
    filter: alpha(opacity=0); /*IE中得这样弄透明度*/
    position: absolute;  /*设置定位方式*/
}

接下来就是调整原始文件选择按钮的放置位置,可以在css中直接设置好right、bottom的值让它刚刚和我们制作的“选择”按钮重合,这样咱就能点上它了。
skidu这里是用js在处理,让按钮的位置在“选择”按钮区域内跟随鼠标移动。这样一来,不论这个“选择”按钮做得有多大,咱都能点上它了

$(function(){
$("#submit").mousemove(function(e){
    $("#file_button").css({
            "top" :(e.pageY - 5) + "px",
            "left":( e.pageX - 180 ) + "px"
        });
    });
});

嗯。其他html应该没有太需要特别写出来的了,如果的确有需要的话直接去demo页查看源码吧

2、自动上传
思路:监听那个文件选择按钮的值,如果它发生了变化那么我们就让form表单在iframe中执行上传(具体原理详见准备工作第三条)。为了方便操作,这里skidu用js在处理这部分

$(function(){
/*监听上传框*/
$("#file_button").live("change", function(){
    /*这里还是简单处理一下,判断上传文件的类型*/
    $allowTypes = /.jpg|.gif|.png|.jpeg/;
    $imgValue = $(this).val();
    /*上传文件不是咱想要的,那么久不准提交咯~*/
    if ($imgValue.search($allowTypes) == -1) {
        alert("您只能上传扩展名为png、gif、jpg、jpeg的图片文件,请重新选择!");
        return;
    }
    /*好吧,这个文件貌似是咱想要的类型,让你上传吧~*/
    /*先调用一下upload_loading添加一个“上传中”字样*/
    upload_loading();
    });
});

/*这里我们先加一个“正在上传中”的效果吧,看着顺眼些*/
function upload_loading(){
    var _loading = $('<span style="position:relative;bottom:80px;left:22px;background:#fff;"><img src="./loading.gif" /> 图像上传中</span>');
    $("#main-preview").append(_loading);
    do_upload();//执行上传
}
/*执行上传*/
function do_upload(){
    var formWrap = $("#myform");
    formWrap[0].submit();
}
/**
 * 为了能够多次上传同一个文件,这里还需简单处理一下文件选择按钮(还是为了兼容IE)
 * 每次上传完成后调用一下这个函数
 * 并将那个input控件的对象传给它即可“清空”该input的值
 * 这样就能再次上传之前上传过的文件了
 *
 */
function do_reset( target ){
    target.after(target.clone(true).val("));
    target.remove();
}

这样,自动上传就实现了

接下来就是文件上传部分的内容了,skidu用PHP实现
upload.php

<?php

$uploaddir = './upload/'; //指定上传目录
$file_type = $_FILES['avatar']['type'];//获取文件类型
$type = is_image( $file_type );//判断上传文件是否是图像
//如果上传的不是图像文件,抱歉咯~
if( $type === false ) {
    echo json_encode( $notImage );
    exit;
}
//组合临时文件名,当然你也可以不这样做
$uploadfile = 'temp_' . get_filename() . '.' . $type;

//将上传的文件从临时目录中移动到上传目录
if (move_uploaded_file($_FILES['avatar']['tmp_name'], $uploaddir . $uploadfile)) {
    $fileSize = getImageSize( $uploaddir . $uploadfile );
    $uploadData = array(
       "filename" => $uploadfile,
       "width" => $fileSize[0],
       "height"=> $fileSize[1],
    );
    //上传成功,将文件名发送给页面中的upload_success()函数,以便执行后续操作
    echo "<script>top.upload_success('".$uploadfile."', '".$fileSize[0]."', '".$fileSize[1]."')</script>";
} else {
    //上传失败。执行页面中的upload_failed()函数提示错误
    //这个函数skidu没有去完成了,大家知道这么一回事就行啦
    echo '<script>top.upload_failed();</script>';
}

/**
 * 判断上传的文件是否是图像文件
 * 这里skidu是直接使用的PHP的$_FILES中的信息进行判断
 * 该方法容易被伪造
 * 建议使用别的方法实现
 * 之后还会提到 这里就不累赘了
 */
function is_image( $file_type ){
    $png_mimes  = array('image/x-png');
    $jpeg_mimes = array('image/jpg', 'image/jpe', 'image/jpeg', 'image/pjpeg');

    if (in_array($file_type, $png_mimes)){
        $file_type = 'image/png';
    }

    if (in_array($file_type, $jpeg_mimes)) {
        $file_type = 'image/jpeg';
    }

    $img_mimes = array(
        'image/gif',
        'image/jpeg',
        'image/png',
    );

    return in_array( $file_type, $img_mimes ) ? substr($file_type, strpos($file_type, '/')+1 ) : false;
}
//蛋疼版生成随即文件名
function get_filename(){
    $name = time() . uniqid();
    return $name;
}

至此,一个简单版的上传就完成了。

下面我们继续完成上传过程中提及的一个js函数upload_success()

function upload_success( filename, _width, _height ){
    //找到刚刚我们添加的“上传中”,将它干掉
    var _loading = $("#main-preview").find("span");
    _loading.remove();
    //将稍后需要的弹出层里面的图像替换成上传上去的文件
    $("#editor").find("img").attr( "src", "./upload/" + filename );
    $("#editor").find("input").val(");
    //skidu准备了一个隐藏input以备后面使用,这里将上传文件的名字放在这里
    $("#avatar_file").val( filename );
    //弹出层的准备工作完毕,下面我们让它显示出来
    reset_iframe( $("#upload_bg"), $("#editor") );
    //弹出层显示完毕,初始化选择区域
    selector_init( _width, _height );
}

/*重置弹出层的位置、背景遮罩等*/
function reset_iframe( _bg, _ifr ){
    var _bg = _bg||$("#upload_bg");
    var _ifr = _ifr||$("#upload_area");

    /*获取当前浏览器的宽高数据,用于设置半透明背景遮罩的显示以及弹出层的显示位置*/
    var iCurW = $(window).width();
    var iCurH = $(window).height();
    var iCurBody = $(document.body).height();
    iCurH = iCurBody>iCurH?iCurBody:iCurH;
    var iCurScr = $(window).scrollTop();
    var oIframeW = _ifr.width();
    var oIframeH = _ifr.height();
    var iframeL = (iCurW-oIframeW)/2;
    var iframeT = iCurScr+(iCurH-oIframeH)/2;
    /*设置弹出层的显示位置让它窗口居中显示*/
    _ifr.css({"left":iframeL,"top":iframeT});
    /*设置遮罩背景的大小*/
    _bg.css({"width":iCurW,"height":iCurH, "z-index":500});

    /*显示背景遮罩和弹出层*/
    _bg.show();
    _ifr.show();
}

/**
 * 准备选择框
 * 该功能使用JCrop插件完成
 * 具体使用方法请参阅官方文档
 * 这里skidu不做过多描述
 * 只提供本demo使用到的内容
 */
function selector_init( _width, _height ) {
    var jcrop_api, boundx, boundy, _select;

    /*准备选择框大小*/
    if( _width >= _height ) {
        _select = _width;
    } else {
        _select = _height;
    }


    /*预览区域图像初始缩放设置*/
    $("#preview1").css({
        width: _width + "px",
        height: _height + "px",
    });

    $("#preview2").css({
        width: _width*(50/150) + "px",
        height: _height*(50/150) + "px",
    });

    /**
     * 页面中放置了四个隐藏input用来保存图像裁剪时需要的四个值
     * 裁剪开始位置的x、y坐标值
     * 需要裁剪区域的宽高值
     * 这里做一次初始化,防止提交失败
     */
    $("#x").val(0);
    $("#y").val(0);
    $("#w").val(_select);
    $("#h").val(_select);

    /*插件开始*/
    $('#selector').Jcrop({
        minSize: [50,50],
        setSelect: [0, 0, _select, _select],
        /*指定选区大小变化后需要执行的函数*/
        onChange: updatePreview,
        /*指定选区变化后需要执行的函数*/
        onSelect: updatePreview,
        /*指定选区变化后需要执行的函数*/
        onSelect: updateCoords,
        aspectRatio: 1
    }, function(){
        /*使用插件API获取图像宽高参数*/
        var bounds = this.getBounds();
        boundx = bounds[0];
        boundy = bounds[1];
        jcrop_api = this;
    });

    /*选择区域每次变动后调用此函数:设置4个input的值*/
    function updateCoords(c){
        $('#x').val(c.x);
        $('#y').val(c.y);
        $('#w').val(c.w);
        $('#h').val(c.h);
    };
    /*检查值*/
    function checkCoords(){
        if (parseInt($('#w').val())) return true;
        alert('Please select a crop region then press submit.');
        return false;
    };
    /*选择区域每次变动后调用此函数:更改预览图*/
    function updatePreview(c){
        if (parseInt(c.w) > 0){
            /*设置大预览头像的大小*/
            var rx2 = 150 / c.w;
            var ry2 = 150 / c.h;
            $('#preview1').css({
                width: Math.round(rx2 * boundx) + 'px',
                height: Math.round(ry2 * boundy) + 'px',
                marginLeft: '-' + Math.round(rx2 * c.x) + 'px',
                marginTop: '-' + Math.round(ry2 * c.y) + 'px'
            });

            /*设置小预览头像的大小*/
            var rx = 50 / c.w;
            var ry = 50 / c.h;
            $('#preview2').css({
                width: Math.round(rx * boundx) + 'px',
                height: Math.round(ry * boundy) + 'px',
                marginLeft: '-' + Math.round(rx * c.x) + 'px',
                marginTop: '-' + Math.round(ry * c.y) + 'px'
            });
        }
    };
}

至此,图像上传以及选择框的准备工作完成

接着就该是提交了

选择完成后,咱点击“提交”按钮,ajax将文件名以及图像裁剪的四个参数发送给PHP,然后我们再借助GD库来完成图像的裁剪。

JS部分:

/*点击“提交”按钮激活ajax*/
$("#select_complete").live("click", function(){
    //先获取我们需要的五个值
    //文件名
    var _filename = $("#avatar_file").val();
    //x坐标
    var _x = $("#x").val();
    //y坐标
    var _y = $("#y").val();
    //宽度
    var _width = $("#w").val();
    //高度
    var _height = $("#h").val();
    //ajax开始(具体参数请查阅JQuery手册)
    $.ajax({
        url: "./crop.php",
        type:"get",
        data:"filename=" + _filename + "&x="+ _x +"&y="+_y+"&width="+_width+"&height="+_height,
        dataType:"json",
        success: function(ret){
            if( ret.status == 'ok' ) {
                crop_success( ret.thumb );
            }
        }
    });
});

PHP部分 crop.php

<?php
$filename = $_GET['filename'];
$x = $_GET['x'];
$y = $_GET['y'];
$width = $_GET['width'];
$height = $_GET['height'];

//执行裁剪
$newFile = imageAct( $filename, $x, $y, $width, $height );
//制作150*150缩略图
imageAct( $newFile, 0,0,150,150,true);
//制作50*50缩略图
imageAct( $newFile, 0,0,50,50,true);

//删除上传的临时文件
unlink( './upload/' . $filename );
//删除多余图片,只保留我需要的两个缩略图
unlink( './upload/' . $newFile );
//返回json数据给前面的js函数使用
//这里还可以根据实际情况做更多的判断
//skidu这里就偷偷懒了
echo json_encode( array('status'=>'ok', 'thumb'=>$newFile.'?'.time() ) );

/**
 * 图像操作
 */
function imageAct( $filename, $x, $y, $width, $height, $resize=false ){
    $ori_width = $width;
    $ori_height = $height;
    if($resize) {
        $x = $y = 0;
        $imageInfo = getimagesize('./upload/'.$filename);
        $ori_width = $imageInfo[0];
        $ori_height = $imageInfo[1];
    }

    //获取图像类型
    $filetype = getImageType($filename);
    if( !$filetype ) return false;
    //相关gd函数准备
    if ( function_exists('imagecreatetruecolor') ) {
        $create    = 'imagecreatetruecolor';
        $copy    = 'imagecopyresampled';
    } else {
        $create    = 'imagecreate';
        $copy    = 'imagecopyresized';
    }
    //从文件绘制图层
    $src_img = createImage( $filename, $filetype );
    //根据需要制作的图像大小绘制图层
    $dst_img = $create( $width, $height );
    //如果是png格式图形,调整透明度
    if( $filetype=='png') {
        imagealphablending($dst_img, FALSE);
        imagesavealpha($dst_img, TRUE);
    }
    //执行裁剪或者压缩
    $copy($dst_img, $src_img, 0, 0, $x, $y, $width, $height, $ori_width, $ori_height);
    //指定新文件的名字
    $newName = $resize ? $height . '_' . $filename : str_replace( "temp_", ", $filename);
    //保存新图像
    saveImage( $dst_img, $filetype, $newName );
    //销毁图层,释放资源
    imagedestroy($dst_img);
    imagedestroy($src_img);
    //给新文件赋予必要的可读写权限
    chmod( './upload/' . $newName, 0777 );
    //返回文件名
    return $newName;
}

/**
 * 根据目标图像类型绘制图床
 */
function createImage( $filename='', $filetype='' ){
    if( !$filename || !$filetype ) return false;
    switch( $filetype ) {
        case "jpg":
            $image = imageCreateFromJpeg( './upload/' . $filename );
            break;
        case "png":
            $image = imageCreateFromPng( './upload/' . $filename );
            break;
        case "gif":
            $image = imageCreateFromGif( './upload/' . $filename );
            break;
        default:
            $image = false;
            break;
    }
    return $image;
}

/**
 * 保存图像
 */
function saveImage( $resrouce, $filetype, $filename ){
    if( !$resrouce || !$filetype || !$filename ) return false;
    switch( $filetype ) {
        case "gif":
            imagegif( $resrouce, './upload/'.$filename );
            break;
        case "jpg":
            imagejpeg( $resrouce, './upload/'.$filename );
            break;
        case "png":
            imagepng( $resrouce, './upload/'.$filename );
            break;
        default:
            return false;
            break;
        }
}

function getImageType( $filename ) {
    //使用PHP的fileinfo扩展来获取文件mime类型
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mimetype = finfo_file($finfo, './upload/' . $filename);
    //如果使用的是linux服务器,还可以使用系统的file函数来判断文件类型
    //该方法需要php开启exec或者shell_exec之类的函数
    //例如:
    //    $cmd = 'file --brief --mime ./upload/'.$filename;
    //    $mimetype = shell_exec( $cmd );

    $type = explode( '/', $mimetype );
    if( trim($type[0]) == 'image' ) {
        return ( trim($type[1])=='jpeg')?"jpg":trim($type[1]);
    } else {
        return false;
    }
}

至此,图像裁剪完成

因为刚刚是使用的ajax在请求裁剪
skidu在ajax请求中指明裁剪完成后执行什么操作。。
嗯,到这里应该不用在多说了,无非就是隐藏掉刚刚我们显示出来的弹出层,然后替换掉显示的图片,方法什么的都是雷同的了
有兴趣的童鞋依然可以去demo中看这部分的代码:)

不知不觉就敲了这么多东西了。。好吧,其实代码都是直接从demo文件中copy过来简单加了一些注释而已
本文提供的并非完整代码,有兴趣的朋友还需自己多折腾哟:)

今天就写到这里,欢迎留言讨论~

标签: jcrop, 头像上传, 头像裁剪, 无刷新上传

已有 4 条评论

  1. 翻了一半以后我实在木有兴趣继续看了,于是翻到最下面准备留下点玩意儿~哈哈,好久不见,我最近也加入到备案的行列了。纠结啊。

    1. skidu

      嗨~~真是好久不见哈:)
      这个其实也是闲来无聊写上去的~又有好久都没有更新了~_~
      博客前次搬家的时候不小心把附件搞丢了呢
      好蛋疼0.0

  2. 其實頭像截取原理都差不多呢,不過UI做的好的話,就會顯得高級些 :woo:

    1. skidu

      @kita
      是滴~原理都是相似的。。
      不过skidu在UI这方面就是一渣了。。哈哈 :yaa:

评论已关闭