HTML5上传前预调整图片大小

197

这里有一个难题。

考虑到我们拥有HTML5本地存储和xhr v2等等技术。我想知道是否有人能找到一个可行的示例,甚至只是告诉我这个问题的答案:

使用新的本地存储(或其他技术),是否可以预先调整图像大小,以便那些不知道如何调整图像大小的用户可以将10mb的图像拖入我的网站,它使用新的本地存储调整大小,然后将其上传为较小的尺寸。

我非常清楚你可以使用Flash、Java applets、Active X来做到这一点...问题是你能否使用Javascript + Html5来实现。

期待着对此的回应。

暂时就这样吧。

10个回答

196

可以使用File API,然后使用canvas元素处理图像。

这篇Mozilla Hacks博客文章详细介绍了大部分过程。以下是该博客文章中的源代码:

// from an input element
var filesToUpload = input.files;
var file = filesToUpload[0];

var img = document.createElement("img");
var reader = new FileReader();  
reader.onload = function(e) {img.src = e.target.result}
reader.readAsDataURL(file);

var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);

var MAX_WIDTH = 800;
var MAX_HEIGHT = 600;
var width = img.width;
var height = img.height;

if (width > height) {
  if (width > MAX_WIDTH) {
    height *= MAX_WIDTH / width;
    width = MAX_WIDTH;
  }
} else {
  if (height > MAX_HEIGHT) {
    width *= MAX_HEIGHT / height;
    height = MAX_HEIGHT;
  }
}
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);

var dataurl = canvas.toDataURL("image/png");

//Post dataurl to the server with AJAX

6
非常感谢,先生。我今晚将进行一次游戏...使用文件API。我成功实现了拖放上传,并意识到这也是一个非常好的功能可以加入。耶! - Jimmyt1988
3
我们需要使用mycanvas.toDataURL("image/jpeg", 0.5)来确保质量以减小文件大小。我在Mozilla博客文章的评论中看到了这个评论,并在此处看到了错误的解决方案https://bugzilla.mozilla.org/show_bug.cgi?id=564388。 - sbose
37
在设置图片的src属性之前,应该等待图片的onload事件(在附加处理程序之后),因为浏览器允许异步解码,像素在drawImage之前可能不可用。请注意保持原文意思并尽量使翻译通俗易懂。 - Kornel
7
你可以把画布代码放到文件读取器的onload函数中,因为有可能在你接近末尾的 ctx.drawImage() 之前,大型图片还没被加载出来。 - Greg
4
注意,你需要额外的魔法才能使其在IOS上运行:https://dev59.com/-2ct5IYBdhLWcg3wpe9c - flo850
显示剩余11条评论

117
我在几年前解决了这个问题,并将解决方案上传到github,链接为https://github.com/rossturner/HTML5-ImageUploader
robertc的答案使用了Mozilla Hacks blog post中提出的解决方案,但我发现当缩放比例不是2:1(或其倍数)时,这会导致图像质量非常差。我开始尝试不同的图像缩放算法,虽然大多数都很慢,或者质量也不太好。
最终,我想出了一种解决方案,我相信它执行快速并且性能也很好 - 由于Mozilla的解决方案在1个画布上复制到另一个画布时以2:1的比例快速执行且不会损失图像质量,给定目标宽度为x像素和高度为y像素,我使用此画布调整大小方法,直到图像大小介于x和2 x之间,以及y和2 y之间。在这一点上,我转向算法图像调整,对于最终的“步骤”,将其调整为目标大小。尝试了几种不同的算法后,我选择了从一个不再在线但可以通过互联网档案馆访问的博客中采用的双线性插值,可以得到良好的结果,以下是适用的代码:
ImageUploader.prototype.scaleImage = function(img, completionCallback) {
    var canvas = document.createElement('canvas');
    canvas.width = img.width;
    canvas.height = img.height;
    canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);

    while (canvas.width >= (2 * this.config.maxWidth)) {
        canvas = this.getHalfScaleCanvas(canvas);
    }

    if (canvas.width > this.config.maxWidth) {
        canvas = this.scaleCanvasWithAlgorithm(canvas);
    }

    var imageData = canvas.toDataURL('image/jpeg', this.config.quality);
    this.performUpload(imageData, completionCallback);
};

ImageUploader.prototype.scaleCanvasWithAlgorithm = function(canvas) {
    var scaledCanvas = document.createElement('canvas');

    var scale = this.config.maxWidth / canvas.width;

    scaledCanvas.width = canvas.width * scale;
    scaledCanvas.height = canvas.height * scale;

    var srcImgData = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
    var destImgData = scaledCanvas.getContext('2d').createImageData(scaledCanvas.width, scaledCanvas.height);

    this.applyBilinearInterpolation(srcImgData, destImgData, scale);

    scaledCanvas.getContext('2d').putImageData(destImgData, 0, 0);

    return scaledCanvas;
};

ImageUploader.prototype.getHalfScaleCanvas = function(canvas) {
    var halfCanvas = document.createElement('canvas');
    halfCanvas.width = canvas.width / 2;
    halfCanvas.height = canvas.height / 2;

    halfCanvas.getContext('2d').drawImage(canvas, 0, 0, halfCanvas.width, halfCanvas.height);

    return halfCanvas;
};

ImageUploader.prototype.applyBilinearInterpolation = function(srcCanvasData, destCanvasData, scale) {
    function inner(f00, f10, f01, f11, x, y) {
        var un_x = 1.0 - x;
        var un_y = 1.0 - y;
        return (f00 * un_x * un_y + f10 * x * un_y + f01 * un_x * y + f11 * x * y);
    }
    var i, j;
    var iyv, iy0, iy1, ixv, ix0, ix1;
    var idxD, idxS00, idxS10, idxS01, idxS11;
    var dx, dy;
    var r, g, b, a;
    for (i = 0; i < destCanvasData.height; ++i) {
        iyv = i / scale;
        iy0 = Math.floor(iyv);
        // Math.ceil can go over bounds
        iy1 = (Math.ceil(iyv) > (srcCanvasData.height - 1) ? (srcCanvasData.height - 1) : Math.ceil(iyv));
        for (j = 0; j < destCanvasData.width; ++j) {
            ixv = j / scale;
            ix0 = Math.floor(ixv);
            // Math.ceil can go over bounds
            ix1 = (Math.ceil(ixv) > (srcCanvasData.width - 1) ? (srcCanvasData.width - 1) : Math.ceil(ixv));
            idxD = (j + destCanvasData.width * i) * 4;
            // matrix to vector indices
            idxS00 = (ix0 + srcCanvasData.width * iy0) * 4;
            idxS10 = (ix1 + srcCanvasData.width * iy0) * 4;
            idxS01 = (ix0 + srcCanvasData.width * iy1) * 4;
            idxS11 = (ix1 + srcCanvasData.width * iy1) * 4;
            // overall coordinates to unit square
            dx = ixv - ix0;
            dy = iyv - iy0;
            // I let the r, g, b, a on purpose for debugging
            r = inner(srcCanvasData.data[idxS00], srcCanvasData.data[idxS10], srcCanvasData.data[idxS01], srcCanvasData.data[idxS11], dx, dy);
            destCanvasData.data[idxD] = r;

            g = inner(srcCanvasData.data[idxS00 + 1], srcCanvasData.data[idxS10 + 1], srcCanvasData.data[idxS01 + 1], srcCanvasData.data[idxS11 + 1], dx, dy);
            destCanvasData.data[idxD + 1] = g;

            b = inner(srcCanvasData.data[idxS00 + 2], srcCanvasData.data[idxS10 + 2], srcCanvasData.data[idxS01 + 2], srcCanvasData.data[idxS11 + 2], dx, dy);
            destCanvasData.data[idxD + 2] = b;

            a = inner(srcCanvasData.data[idxS00 + 3], srcCanvasData.data[idxS10 + 3], srcCanvasData.data[idxS01 + 3], srcCanvasData.data[idxS11 + 3], dx, dy);
            destCanvasData.data[idxD + 3] = a;
        }
    }
};

这个功能将图像缩小到config.maxWidth的宽度,保持原始宽高比。在开发时,它适用于iPad/iPhone Safari以及主流桌面浏览器(IE9+,Firefox,Chrome),因此我希望它仍然兼容HTML5的更广泛应用。请注意,canvas.toDataURL()调用需要一个MIME类型和图像质量,这将允许您控制质量和输出文件格式(如果需要,可能与输入不同)。
唯一没有涵盖的是保持方向信息,如果没有这些元数据,图像会按原样调整大小并保存,从而丢失图像内任何有关方向的元数据,这意味着在平板电脑上拍摄的“倒置”图像仍将呈现为倒置状态,尽管在设备的相机取景器中它们已经被翻转了。如果这是一个问题,这篇博客文章提供了一个很好的指南和代码示例,说明如何完成此操作,我相信可以将其集成到上述代码中。

4
我已经为你授予赏金,因为我认为它对答案有很大的补充作用,但是我们还需要把那些定位的东西整合进去 :) - Sam Saffron
3
一个非常乐于助人的人现在已经合并了一些关于方向元数据的功能 :) - Ross Taylor-Turner
@RossTaylor-Turner,请问包含方向元数据合并的代码在哪里? - Adam D

30

以上内容有误,需要更正:

<img src="" id="image">
<input id="input" type="file" onchange="handleFiles()">
<script>

function handleFiles()
{
    var filesToUpload = document.getElementById('input').files;
    var file = filesToUpload[0];

    // Create an image
    var img = document.createElement("img");
    // Create a file reader
    var reader = new FileReader();
    // Set the image once loaded into file reader
    reader.onload = function(e)
    {
        img.src = e.target.result;

        var canvas = document.createElement("canvas");
        //var canvas = $("<canvas>", {"id":"testing"})[0];
        var ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0);

        var MAX_WIDTH = 400;
        var MAX_HEIGHT = 300;
        var width = img.width;
        var height = img.height;

        if (width > height) {
          if (width > MAX_WIDTH) {
            height *= MAX_WIDTH / width;
            width = MAX_WIDTH;
          }
        } else {
          if (height > MAX_HEIGHT) {
            width *= MAX_HEIGHT / height;
            height = MAX_HEIGHT;
          }
        }
        canvas.width = width;
        canvas.height = height;
        var ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0, width, height);

        var dataurl = canvas.toDataURL("image/png");
        document.getElementById('image').src = dataurl;     
    }
    // Load files into file reader
    reader.readAsDataURL(file);


    // Post the data
    /*
    var fd = new FormData();
    fd.append("name", "some_filename.jpg");
    fd.append("image", dataurl);
    fd.append("info", "lah_de_dah");
    */
}</script>

2
你在将它添加到FormData()之后如何处理PHP部分?例如,您不会寻找$_FILES['name']['tmp_name'][$i]吗?我正在尝试if(isset($_POST['image']))...但dataurl不存在。 - denikov
2
第一次在Firefox上尝试上传图像时它无法工作。下一次尝试时它就可以了。如何解决? - Fred
如果文件是数组为空或空的,则返回。 - Sheelpriy
3
在Chrome浏览器中,当尝试访问img.widthimg.height时,它们最初为0。应该将函数的其余部分放在img.addEventListener('load',function(){ //现在图像已加载,继续...});中。 - Inigo
2
@Justin,你能澄清一下“上面”是指什么吗?也就是说,这篇帖子是对什么的更正呢?谢谢。 - Martin
@Inigo,亲爱的,你应该得到一枚奖章。=) - Andrey Bulezyuk

16

对Justin的回答进行修改,使其适用于我:

  1. 添加了img.onload事件
  2. 使用实际示例扩展POST请求

function handleFiles()
{
    var dataurl = null;
    var filesToUpload = document.getElementById('photo').files;
    var file = filesToUpload[0];

    // Create an image
    var img = document.createElement("img");
    // Create a file reader
    var reader = new FileReader();
    // Set the image once loaded into file reader
    reader.onload = function(e)
    {
        img.src = e.target.result;

        img.onload = function () {
            var canvas = document.createElement("canvas");
            var ctx = canvas.getContext("2d");
            ctx.drawImage(img, 0, 0);

            var MAX_WIDTH = 800;
            var MAX_HEIGHT = 600;
            var width = img.width;
            var height = img.height;

            if (width > height) {
              if (width > MAX_WIDTH) {
                height *= MAX_WIDTH / width;
                width = MAX_WIDTH;
              }
            } else {
              if (height > MAX_HEIGHT) {
                width *= MAX_HEIGHT / height;
                height = MAX_HEIGHT;
              }
            }
            canvas.width = width;
            canvas.height = height;
            var ctx = canvas.getContext("2d");
            ctx.drawImage(img, 0, 0, width, height);

            dataurl = canvas.toDataURL("image/jpeg");

            // Post the data
            var fd = new FormData();
            fd.append("name", "some_filename.jpg");
            fd.append("image", dataurl);
            fd.append("info", "lah_de_dah");
            $.ajax({
                url: '/ajax_photo',
                data: fd,
                cache: false,
                contentType: false,
                processData: false,
                type: 'POST',
                success: function(data){
                    $('#form_photo')[0].reset();
                    location.reload();
                }
            });
        } // img.onload
    }
    // Load files into file reader
    reader.readAsDataURL(file);
}

2
方向信息(EXIF)丢失。 - Konstantin Vahrushev

9
如果您不想重复造轮子,可以尝试使用plupload.com

3
我也使用plupload进行客户端的图片大小调整。它最好的一点是可以使用flash、html5、silverlight等多种技术,根据用户的个人设置选择可用的技术。但如果你仅希望使用html5,在一些旧版浏览器和Opera上可能会出现不兼容的问题。 - jnl

8

Typescript

async resizeImg(file: Blob): Promise<Blob> {
    let img = document.createElement("img");
    img.src = await new Promise<any>(resolve => {
        let reader = new FileReader();
        reader.onload = (e: any) => resolve(e.target.result);
        reader.readAsDataURL(file);
    });
    await new Promise(resolve => img.onload = resolve)
    let canvas = document.createElement("canvas");
    let ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0);
    let MAX_WIDTH = 1000;
    let MAX_HEIGHT = 1000;
    let width = img.naturalWidth;
    let height = img.naturalHeight;
    if (width > height) {
        if (width > MAX_WIDTH) {
            height *= MAX_WIDTH / width;
            width = MAX_WIDTH;
        }
    } else {
        if (height > MAX_HEIGHT) {
            width *= MAX_HEIGHT / height;
            height = MAX_HEIGHT;
        }
    }
    canvas.width = width;
    canvas.height = height;
    ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0, width, height);
    let result = await new Promise<Blob>(resolve => { canvas.toBlob(resolve, 'image/jpeg', 0.95); });
    return result;
}

2
任何可能看到这篇文章的人,我认为这个基于承诺的答案更加可靠。我将其转换为普通JS,它运行得非常好。 - gbland777

7

采纳的答案非常好,但是调整大小逻辑忽略了以下情况:图像在一个轴上较大(例如,高度>maxHeight,但宽度<=maxWidth)。

我认为以下代码以更直观、更实用的方式处理了所有情况(如果使用纯javascript,请忽略typescript类型注释):

private scaleDownSize(width: number, height: number, maxWidth: number, maxHeight: number): {width: number, height: number} {
    if (width <= maxWidth && height <= maxHeight)
      return { width, height };
    else if (width / maxWidth > height / maxHeight)
      return { width: maxWidth, height: height * maxWidth / width};
    else
      return { width: width * maxHeight / height, height: maxHeight };
  }

5
在画布元素中调整图像大小通常不是一个好主意,因为它使用最便宜的框插值。结果图像明显降低了质量。我建议使用http://nodeca.github.io/pica/demo/,它可以执行Lanczos转换。上面的演示页面显示了画布和Lanczos方法之间的区别。
它还使用Web workers并行地调整图像大小。还有WEBGL实现。
有一些在线图像大小调整器使用pica来完成任务,例如https://myimageresizer.com

5
fd.append("image", dataurl);

这样做不起作用。在PHP端,您无法使用此方法保存文件。

请改用以下代码:

var blobBin = atob(dataurl.split(',')[1]);
var array = [];
for(var i = 0; i < blobBin.length; i++) {
  array.push(blobBin.charCodeAt(i));
}
var file = new Blob([new Uint8Array(array)], {type: 'image/png', name: "avatar.png"});

fd.append("image", file); // blob file

0

如果您想使用简单易用的上传管理器以及上传前的调整大小功能,可以使用dropzone.js

它内置了调整大小的功能,但如果需要的话,您也可以提供自己的函数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接