【问题标题】:Downsizing image dimensions via pure JS leads to image size inflation (in bytes)通过纯 JS 缩小图像尺寸会导致图像尺寸膨胀(以字节为单位)
【发布时间】:2018-07-15 21:25:49
【问题描述】:

我是一名服务器端开发人员,学习客户端操作的技巧,从纯 JS 开始。

目前我正在使用纯 JS 来调整通过浏览器上传的图像的尺寸。

我遇到了一种情况,将 1018 x 1529 .jpg 文件缩小到 400 x 601 .jpeg 会产生更大尺寸(以字节为单位)的文件。它从70013 字节到74823 字节。

我的期望是应该缩小规模,而不是通货膨胀。这是怎么回事,有什么办法可以解决这种情况吗?

注意:让我特别困惑的一点是,每个图像的压缩都是在没有事先了解目标之前的压缩的情况下开始的。因此,任何低于 100 的质量水平都会进一步降低图像质量。这应该相应地始终减小文件大小。但奇怪的是没有发生?


如果需要,我的相关JS代码是:

var max_img_width = 400;
var wranges = [max_img_width, Math.round(0.8*max_img_width), Math.round(0.6*max_img_width),Math.round(0.4*max_img_width),Math.round(0.2*max_img_width)];

function prep_image(img_src, text, img_name, target_action, callback) { 
    var img = document.createElement('img');
    var fr = new FileReader();
    fr.onload = function(){
      var dataURL = fr.result;
      img.onload = function() {
          img_width = this.width;
          img_height = this.height;
          img_to_send = resize_and_compress(this, img_width, img_height, "image/jpeg");
          callback(text, img_name, target_action, img_to_send);
        }
      img.src = dataURL;
    };
    fr.readAsDataURL(img_src);
}


function resize_and_compress(source_img, img_width, img_height, mime_type){
    var new_width;
    switch (true) {
      case img_width < wranges[4]:
         new_width = wranges[4];
         break;
      case img_width < wranges[3]:
         new_width = wranges[4];
         break;
      case img_width < wranges[2]:
         new_width = wranges[3];
         break;
      case img_width < wranges[1]:
         new_width = wranges[2];
         break;
      case img_width < wranges[0]:
         new_width = wranges[1];
         break;
      default:
         new_width = wranges[0];
         break;
    }
    var wpercent = (new_width/img_width);
    var new_height = Math.round(img_height*wpercent);
    var canvas = document.createElement('canvas');//supported
    canvas.width = new_width;
    canvas.height = new_height;
    var ctx = canvas.getContext("2d");
    ctx.drawImage(source_img, 0, 0, new_width, new_height);
    return dataURItoBlob(canvas.toDataURL(mime_type),mime_type);
}

// converting image data uri to a blob object
function dataURItoBlob(dataURI,mime_type) {
  var byteString = atob(dataURI.split(',')[1]);
  var ab = new ArrayBuffer(byteString.length);
  var ia = new Uint8Array(ab);//supported
  for (var i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); }
  return new Blob([ab], { type: mime_type });
}

如果有必要,这是我使用的测试图片:

Here's图片的原始位置。

请注意,对于我尝试过的其他几个图像,代码的行为确实符合预期。它并不总是把结果搞砸,但现在我不能确定它是否总是有效。在这个问题的范围内,让我们坚持使用纯 JS 解决方案。

【问题讨论】:

  • 链接的问答将为您提供解决方法。如果我有时间,我会尝试为这个骗局添加一个更全面的答案,但基本上,当你在画布上绘制 jpeg 时,你也会绘制 JPEG 工件
  • @Kaiido:在那个骗子中尝试的唯一答案是要求该人使用encoderOptions 参数来设置质量。就我而言,我还是将其保留为默认设置。
  • 这就是你的问题。您的问题仍然相同,了解 JPEG 有损压缩的工作原理 + 了解为什么在画布上绘制有损 JPEG 会产生更多信息 + 了解为什么更大的质量选项会产生更大的 JPEG 文件大小。所以我认为一个人可以在一篇文章中回答这两个问题 => 欺骗。
  • @Kaiido:然后归结为 - 是否可以检测输入图像的 encoderOptions 设置(在处理之前)?这是实现苹果对苹果比较(和调整大小)的一种方法。
  • 不是真的...即使有一种方法可以检测所使用的质量级别,当绘制到画布上时,您将从原始像素数据重新开始,除了所有已被被 JPEG 算法丢弃的将被人工制品取代。无法从创建 jpeg 版本的原始图像开始。因此,您将不得不从这些工件中创建更多工件。但如果你能等几个小时,我会尝试为目标提供更全面的答案。

标签: javascript


【解决方案1】:

为什么 Canvas 不是缩小图像文件大小的最佳选择。

我不会讲太多细节,也不会深入解释,但我会尽量向你解释你所遇到的基本情况。

以下是您需要了解的一些概念(至少部分了解)。

  • 什么是有损图像格式(如 JPEG)
  • 将图像绘制到画布时会发生什么
  • 将画布图像导出为图像格式时会发生什么

Lossy Image Format.

图像格式可以分为三类:

  • 原始图像格式
  • 无损图像格式(tiff、png、gif、bmp、webp ...)
  • 有损图像格式(jpeg,...)

无损图像格式通常只是简单地压缩表格中的数据,将像素颜色映射到使用该颜色的像素位置。

另一方面,有损图像格式会丢弃信息并从原始图像中生成近似数据(伪影),以创建感知上相似的图像渲染,使用更少的数据。

近似(伪影)之所以有效,是因为解压缩算法知道它必须将颜色信息传播到给定区域,因此它不必保留每个像素信息。

但是一旦算法处理了原始图像并生成了新图像,就无法找回丢失的数据。


在画布上绘制图像。

当您在画布上绘制图像时,浏览器会将图像信息转换为原始图像格式
它不会存储有关传递给它的图像格式的任何信息,并且在有损图像的情况下,artifacts 中包含的每个像素都将像其他所有像素一样成为一等公民。


导出画布图像

canvas 2D API 具有三种导出其原始数据的方法:

  • getImageData。这将返回原始像素 RGBA 值
  • toDataURL。它将同步应用与您作为参数传递的 MIME 对应的压缩算法。
  • toBlob。类似于toDataURL,但异步。

我们感兴趣的案例是toDataURLtoBlob 之一以及"image/jpeg" MIME。
请记住,当调用此方法时,浏览器只能看到它在画布上的当前原始像素数据。因此它将再次应用 jpeg 算法,删除一些数据,并从该原始图像中生成新的近似值(artifacts)。

所以,是的,在这些方法中有一个 0-1 quality 参数可用于有损压缩,因此可以认为我们可以尝试知道用于生成原始图像的原始损失级别是多少,但即使那么,由于我们实际上是在 drawing-to-canvas 步骤中生成了新的图像数据,因此该算法可能无法为这些伪影生成良好的传播方案。

要考虑的另一件事,主要针对toDataURL,是浏览器在执行这些操作时必须尽可能快,因此它们通常更喜欢速度而不是压缩质量。


好吧,画布不适合它。然后呢?

jpeg 图像并不容易...jpegtran 声称它可以对您的 jpeg 图像进行无损缩放,所以我想也应该可以制作一个 js 端口,但我不知道。 .



关于无损格式的特别说明

请注意,您的调整大小算法也可以生成更大的 png 文件,这是一个示例,但我会让读者猜一下为什么会发生这种情况:

var ctx= c.getContext('2d');
c.width = 501;
for(var i = 0; i<500; i+=10) {
  ctx.moveTo(i+.5, 0);
  ctx.lineTo(i+.5, 150);
}
ctx.stroke();

c.toBlob(b=>console.log('original', b.size));

c2.width = 500;
c2.height = (500 / 501) * c.height;
c2.getContext('2d').drawImage(c, 0, 0, c2.width, c2.height);
c2.toBlob(b=>console.log('resized', b.size));
<canvas id="c"></canvas>
<canvas id="c2"></canvas>

【讨论】:

  • TL;DR “不要将已经是 JPG 的内容重新编码为 JPG(有损压缩),因为它会添加更多伪像”。我不太同意这个答案:有时没有其他方法可以使 JPG 比重新编码更小。所以是的,当你有一个 2MB 的 JPG 时,将它重新导出为质量 = 60% 的 JPG 似乎完全可以得到一个 400KB 的 JPG :)
  • @Basj 我认为这个答案中没有这样的 TL;DR。 TL;DR 如果应该有一个,宁愿是,不要在画布上做,因为它更喜欢速度而不是压缩。
  • 您从哪里知道知名浏览器(例如 FF 的 Chrome)在 toDataURL 内部的 JPEG 压缩算法实现不佳,导致比“普通”编辑器所做的更多伪影? (比方说toDataURL JPG 压缩与 MS Paint JPG 压缩)我可以相信这一点,但如果看到 Chromium 的基准/或源代码显示 JPEG 算法实施不当的地方会很酷。
  • @Basj 请不要再把我没有说的话放在我嘴里。我从来没有说过他们确实有糟糕的实现,也没有说它会产生比 MS Paint 更多的伪影,我说他们会权衡压缩质量以获得更快的结果。请记住 toDataURL 是一个同步方法。他们无法执行将 UI 卡住 5 秒的多通道。其他软件,如 Photoshop、Gimp,可能还有 ImageMagic 甚至 MS Paint 都不会那么在意,并且通常会提供“更好的压缩,更慢”的选项。 canvas API 没有这样的选项。
  • 1/2 @Kaiido 我没有在你嘴里说什么,我只是想理解你的意思 “它更喜欢速度而不是压缩质量”。偏爱“速度胜过质量”隐含地意味着质量低于其他软件的预期。再一次,我相信你对 Other softs [...] won't care as much, and will generally offer an option for "better compression, slower" 的看法,但我希望看到一个基准或来源来确认 var fullQuality = canvas.toDataURL('image/jpeg', 1.0); 比其他软件的压缩更差。
【解决方案2】:

这是一个建议,并不是真正的修复(或解决方案)。

如果您遇到此问题,请确保在完成调整大小操作后比较两个图像的文件大小。如果新文件较大,则只需回退到源图像。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2012-09-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-02-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多