我应该如何使用 jcrop 在客户端裁剪图像并上传?

How should I crop an image at client side using jcrop and upload it?

我正在开发一个组件,其中有文件上传 HTML 控件,在使用文件上传元素选择图像后,图像将呈现在 HTML5 Canvas元素.

这是带有示例代码的 JSFiddle:https://jsfiddle.net/govi20/spmc7ymp/

id=target => jcrop 元素的选择器
id=photograph => 文件上传元素的选择器
id=preview => canvas 元素的选择器
id=clear_selection => 清除 canvas

按钮的选择器

使用的第三方JS库:

<script src="./js/jquery.min.js"></script>
<script src="./js/jquery.Jcrop.js"></script>
<script src="./js/jquery.color.js"></script>

设置 JCrop:

<script type="text/javascript">

jQuery(function($){
 
var api;

$('#target').Jcrop({
  // start off with jcrop-light class
  bgOpacity: 0.5,
  keySupport: false,
  bgColor: 'black',
  minSize:[240,320],
  maxSize:[480,640],
  onChange : updatePreview,
  onSelect : updatePreview, 
  height:160,
  width:120,
  addClass: 'jcrop-normal'
},function(){
  api = this;
  api.setSelect([0,0,240,320]);
  api.setOptions({ bgFade: true });
  api.ui.selection.addClass('jcrop-selection');
  });

});

clear canvas 将在清除按钮单击事件时触发的事件:

jQuery('#clear_selection').click(function(){
  $('#target').Jcrop({    
      
      setSelect: [0,0,0,0],
    });
});

在 HTML5 Canvas 上呈现图像的代码:

function readURL(input) {
    
    if (input.files && input.files[0]) {
        var reader = new FileReader();
        reader.onload = function (e) {
            $('#target').attr('src', e.target.result);
            setProperties();       
        }
        reader.readAsDataURL(input.files[0]);
    }
}

function setProperties(){
   $('#target').Jcrop({         
              setSelect: [0,0,240,320]
        }); 
}
$("#photograph").change(function(){
    readURL(this);     
});

在 canvas 上裁剪和渲染图像的代码:

    var canvas = document.getElementById('preview'),
    context = canvas.getContext('2d');

    make_base();
    function updatePreview(c) {
        console.log("called");
        if(parseInt(c.w) > 0) {
            // Show image preview
            var imageObj = $("#target")[0];
            var canvas = $("#preview")[0];
            var context = canvas.getContext("2d");
            context.drawImage(imageObj, c.x, c.y, c.w, c.h, 0, 0, canvas.width, canvas.height);
        }
    };

    function make_base() {
        console.log("make_base called");
        var base_image = new Image();
        base_image.src = '';
        base_image.onload = function () {
            context.drawImage(base_image, 0, 0);
        }
    }

以下是我在上述设置中遇到的一系列问题:

  1. updatePreview 函数不会在选择时被调用,因此 canvas 不会被渲染。
  2. 裁剪选择框不可拖动(我用的是bootstrapCSS,我怀疑是missing/mismatching的依赖)。
  3. Canvas 是 HTML5 元素,这意味着最终用户必须有一个 HTML5 兼容的浏览器,我正在开发一个拥有数百万用户的应用程序。强制用户使用最新的浏览器不是一个可行的选择。这里的回退机制应该是什么?

这里是基本的html5代码:

https://jsfiddle.net/zm7e0jev/

此代码裁剪图像、显示预览并将输入元素的值设置为 base64 编码的裁剪图像。

您可以通过以下方式获取php中的图片文件:

//File destination
$destination = "/folder/cropped_image.png";
//Get convertable base64 image string
$image_base64 = $_POST["png"];
$image_base64 = str_replace("data:image/png;base64,", "", $image_base64);
$image_base64 = str_replace(" ", "+", $image_base64);
//Convert base64 string to image data
$image = base64_decode($image_base64);
//Save image to final destination
file_put_contents($destination, $image);

将 base64 图像字符串作为 post 变量提交有它的服务器 post 大小限制和 base64 编码使得裁剪后的图像文件大小甚至比裁剪后的原始数据大 (~33%)图片会使上传时间更长。

要设置 post 大小限制:What is the size limit of a post request?

Keep in mind that an increased post size limit can be abused for a DoS attack as example.

相反,我建议将 base64 裁剪后的图像转换为数据 blob,然后将其作为文件添加到提交时的表单中:

https://jsfiddle.net/g3ysk6sf/

然后您可以通过以下方式获取php中的图像文件:

//File destination
$destination = "/folder/cropped_image.png";
//Get uploaded image file it's temporary name
$image_tmp_name = $_FILES["cropped_image"]["tmp_name"][0];
//Move temporary file to final destination
move_uploaded_file($image_tmp_name, $destination);

更新:

FormData() is only partially support in IE10 and not supported in older versions of IE

所以我建议发送 base64 字符串作为备用,尽管这会导致较大图像出现问题,因此它需要检查文件大小并在图像超过特定大小时显示错误弹出窗口。

我会post在它正常工作后使用下面的回退代码进行更新。

更新 2:

我为 IE10 及以下版本添加了回退:

https://jsfiddle.net/oupxo3pu/

唯一的限制是使用IE10及以下版本时可以提交的图片尺寸,如果图片尺寸过大js代码会报错。 post 值的最大工作大小在每个服务器之间是不同的,js 代码有一个变量来设置最大大小。

下面的 php 代码适用于上述回退:

//File destination
$destination = "/folder/cropped_image.png";
if($_POST["png"]) {//IE10 and below
    //Get convertable base64 image string
    $image_base64 = $_POST["png"];
    $image_base64 = str_replace("data:image/png;base64,", "", $image_base64);
    $image_base64 = str_replace(" ", "+", $image_base64);
    //Convert base64 string to image data
    $image = base64_decode($image_base64);
    //Save image to final destination
    file_put_contents($destination, $image);
} else if($_FILES["cropped_image"]) {//IE11+ and modern browsers
    //Get uploaded image file it's temporary name
    $image_tmp_name = $_FILES["cropped_image"]["tmp_name"][0];
    //Move temporary file to final destination
    move_uploaded_file($image_tmp_name, $destination);
}
There is no fallback code for the canvas element yet, I'm looking into it.
The post size limitation in the fallback for older browsers is one of the reasons I dropped support for older browsers myself.

更新 3:

我为 IE8 中的 canvas 元素推荐的后备方案:

http://flashcanvas.net/

它支持裁剪代码所需的所有 canvas 功能。

请记住它需要闪光灯。有一个 canvas 后备方案 (explorercanvas),它不需要 flash,但它不支持我们需要保存裁剪图像的函数 toDataURL()。

Seahorsepip 的回答很棒。我对非后备答案做了很多改进。

http://jsfiddle.net/w1Lh4w2t/

我建议不要做那种奇怪的隐藏 png 的事情,当 Image 对象工作得很好时(只要我们不支持回退)。

var jcrop_api;
var canvas;
var context;
var image;
var prefsize;

尽管如此,您最好还是在最后从 canvas 中获取数据,然后仅在最后将其放入该字段。

function loadImage(input) {
  if (input.files && input.files[0]) {
    var reader = new FileReader();
    reader.onload = function(e) {
      image = new Image();
      image.src = e.target.result;
      validateImage();
    }
    reader.readAsDataURL(input.files[0]);
  }
}

但是,如果您想要的功能不仅仅是裁剪,如果我们将 jcrop 附加到插入的 canvas(我们在刷新时用 jcrop 销毁它)。我们可以轻松地做任何我们可以用 canvas 做的事情,然后再次 validateImage() 并使更新后的图像可见。

function validateImage() {
  if (canvas != null) {
    image = new Image();
    image.src = canvas.toDataURL('image/png');
  }
  if (jcrop_api != null) {
    jcrop_api.destroy();
  }
  $("#views").empty();
  $("#views").append("<canvas id=\"canvas\">");
  canvas = $("#canvas")[0];
  context = canvas.getContext("2d");
  canvas.width = image.width;
  canvas.height = image.height;
  context.drawImage(image, 0, 0);
  $("#canvas").Jcrop({
    onSelect: selectcanvas,
    onRelease: clearcanvas,
    boxWidth: crop_max_width,
    boxHeight: crop_max_height
  }, function() {
    jcrop_api = this;
  });
  clearcanvas();
}

然后在提交时我们提交任何待处理的操作,如 applyCrop() 或 applyScale(),将数据添加到隐藏字段以备后备之用,如果我们需要这些东西的话。然后我们有了一个系统,我们可以轻松地以任何方式修改 canvas,然后当我们提交 canvas 数据时,数据就会正确发送。

function applyCrop() {
  canvas.width = prefsize.w;
  canvas.height = prefsize.h;
  context.drawImage(image, prefsize.x, prefsize.y, prefsize.w, prefsize.h, 0, 0, canvas.width, canvas.height);
  validateImage();
}

已将 canvas 添加到 div 次观看。

 <div id="views"></div>

为了在 PHP (drupal) 中捕获附件,我使用了类似的东西:

    function makeFileManaged() {
        if (!isset($_FILES['croppedfile']))
            return NULL;
        $path = $_FILES['croppedfile']['tmp_name'];
        if (!file_exists($path))
            return NULL;
        $result_filename = $_FILES['croppedfile']['name'];
        $uri = file_unmanaged_move($path, 'private://' . $result_filename, FILE_EXISTS_RENAME);
        if ($uri == FALSE)
            return NULL;
        $file = File::Create([
                    'uri' => $uri,
        ]);
        $file->save();
        return $file->id();
    }