【问题标题】:Resize image with javascript canvas (smoothly)使用 javascript 画布调整图像大小(平滑)
【发布时间】:2013-10-16 05:28:47
【问题描述】:

我正在尝试使用画布调整一些图像的大小,但我对如何平滑它们一无所知。 在 Photoshop、浏览器等上。他们使用了一些算法(例如双三次、双线性),但我不知道这些算法是否内置在画布中。

这是我的小提琴:http://jsfiddle.net/EWupT/

var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
canvas.width=300
canvas.height=234
ctx.drawImage(img, 0, 0, 300, 234);
document.body.appendChild(canvas);

第一个是正常调整大小的图像标签,第二个是画布。请注意画布是如何不那么光滑的。我怎样才能实现“平滑”?

【问题讨论】:

  • 与相比,画布中的结果模糊的问题吗?如果是这样,很可能您有视网膜屏幕(DPR >= 2),您可以放大画布以实现与 中看到的类似的结果,例如像这样 — jsfiddle.net/c8uo3pda/4

标签: javascript html image canvas html5-canvas


【解决方案1】:
export const resizeImage = (imageFile, size = 80) => {
    
    let resolver = ()=>{};

    let reader = new FileReader();

    reader.onload = function (e) {
        let img = document.createElement("img");
        img.onload = function (event) {
            // Dynamically create a canvas element
            let canvas = document.createElement("canvas");

            canvas.width=size;
            canvas.height=size;

            // let canvas = document.getElementById("canvas");
            let ctx = canvas.getContext("2d");

            // Actual resizing
            ctx.drawImage(img, 0, 0, size, size);

            // Show resized image in preview element
            let dataurl = canvas.toDataURL(imageFile.type);

            resolver(dataurl);
        }
        img.src = e.target.result;
    }

    reader.readAsDataURL(imageFile);

    
    return new Promise((resolve, reject) => {
        resolver = resolve;
    })
};

【讨论】:

    【解决方案2】:

    我通过使用画布的比例解决了这个问题,我的情况下图像质量变得非常好。

    首先我缩放画布内的内容:

    ctx.scale(2, 2)
    

    然后用css将canvas标签横向扩展:

    #myCanvas { transform: scale(0.5); }
    

    【讨论】:

      【解决方案3】:

      这是我的代码,希望对 SO 社区中的某些人有用:

      您可以将目标图像尺寸作为参数包含在脚本调用中。这将是图像宽度或高度的结果值,以较大者为准。较小的尺寸会调整大小,保持图像纵横比不变。您还可以在脚本中硬编码您的默认目标大小。

      您可以轻松更改脚本以满足您的特定需求,例如您想要的输出图像类型(默认为“image/png”),并决定您希望按百分比调整图像大小以获得更精细的图像类型结果(参见代码中的 const percentStep)。

         const ResizeImage = ( _ => {
      
      const MAX_LENGTH = 260;     // default target size of largest dimension, either witdth or height
      const percentStep = .3;     // resizing steps until reaching target size in percents (30% default)
      const canvas = document.createElement("canvas");
      const canvasContext = canvas.getContext("2d");
      const image = new Image();
      
      const doResize = (callback, maxLength) => {
      
          // abort with error if image has a dimension equal to zero
          if(image.width == 0 || image.height == 0) {
              return {blob: null, error: "either image width or height was zero "};
          }
      
          // use caller dimension or default length if none provided
          const length = maxLength == null  ? MAX_LENGTH : maxLength;
      
          canvas.width = image.width;
          canvas.height = image.height;
          canvasContext.drawImage(image, 0, 0, image.width, image.height);
          // if image size already within target size, just copy and return blob
          if(image.width <= length && image.height <= length) {
              canvas.toBlob( blob => {
                  callback({ blob: blob, error: null });
              }, "image/png", 1);
              return;
          }
      
          var startDim = Math.max(image.width, image.height);
          var startSmallerDim = Math.min(image.width, image.height);
      
          // gap to decrease in size until we reach the target size,
          // be it by decreasing the image width or height,
          // whichever is largest
          const gap = startDim - length;
          // step length of each resizing iteration
          const step = parseInt(percentStep*gap);
          //  no. of iterations
          var nSteps = 0;
          if(step == 0) {
              step = 1;
          } else {
              nSteps = parseInt(gap/step);
          }
          // length of last additional resizing step, if needed
          const lastStep = gap % step;
          // aspect ratio = value by which we'll  multiply the smaller dimension
          // in order to keep the aspect ratio unchanged in each iteration
          const ratio = startSmallerDim/startDim;
      
          var newDim;          // calculated new length for the bigger dimension of the image, be it image width or height
          var smallerDim;     // length along the smaller dimension of the image, width or height
          for(var i = 0; i < nSteps; i++) {
              // decrease longest dimension one step in pixels
              newDim = startDim - step;
              // decrease shortest dimension proportionally, so as to keep aspect ratio
              smallerDim = parseInt(ratio*newDim);
              // assign calculated vars to their corresponding canvas dimension, width or height
              if(image.width > image.height) {
                  [canvas.width, canvas.height]  = [newDim, smallerDim];
              } else {
                  [canvas.width, canvas.height] = [smallerDim, newDim];
              }
              // draw image one step smaller
              canvasContext.drawImage(canvas, 0, 0, canvas.width, canvas.height);
              // cycle var startDim for new loop
              startDim = newDim;
          }
      
          // do last missing resizing step to finally reach target image size
          if(lastStep > 0) {
              if(image.width > image.height) {
                  [canvas.width, canvas.height]  = [startDim - lastStep, parseInt(ratio*(startDim - lastStep))];
              } else {
                  [canvas.width, canvas.height] = [parseInt(ratio*(startDim -lastStep)), startDim - lastStep];
              }
              canvasContext.drawImage(image, 0, 0, canvas.width, canvas.height);
          }
      
          // send blob to caller
          canvas.toBlob( blob => {
              callback({blob: blob, error: null});
          }, "image/png", 1);
      
      };
      
      const resize = async (imgSrc, callback, maxLength) => {
          image.src = imgSrc;
          image.onload = _ => {
             doResize(callback, maxLength);
          };
      };
      
      return { resize: resize }
      
      })();
      

      用法:

      ResizeImage.resize("./path/to/image/or/blob/bytes/to/resize", imageObject => {
          if(imageObject.error != null) {
            // handle errors here
            console.log(imageObject.error);
            return;
          }
          // do whatever you want with the blob, like assinging it to
          // an img element, or uploading it to a database
          // ...
          document.querySelector("#my-image").src = imageObject.blob;
          // ...
      }, 300);
      

      【讨论】:

        【解决方案4】:

        我不明白为什么没有人建议createImageBitmap

        createImageBitmap(
            document.getElementById('image'), 
            { resizeWidth: 300, resizeHeight: 234, resizeQuality: 'high' }
        )
        .then(imageBitmap => 
            document.getElementById('canvas').getContext('2d').drawImage(imageBitmap, 0, 0)
        );
        

        效果很好(假设您为图像和画布设置了 ID)。

        【讨论】:

        • 因为没有被广泛支持caniuse.com/#search=createImageBitmap
        • 73% 的用户支持 createImageBitmap。根据您的用例,它可能已经足够好了。只是 Safari 拒绝添加对它的支持。我认为这是一个可能的解决方案,值得一提。
        • 不错的解决方案,但不幸的是它在 Firefox 上不起作用
        【解决方案5】:

        因为Trung Le Nguyen Nhat's fiddle 根本不正确 (它只是在最后一步中使用原始图像)
        我用性能比较写了我自己的一般小提琴:

        FIDDLE

        基本上是:

        img.onload = function() {
           var canvas = document.createElement('canvas'),
               ctx = canvas.getContext("2d"),
               oc = document.createElement('canvas'),
               octx = oc.getContext('2d');
        
           canvas.width = width; // destination canvas size
           canvas.height = canvas.width * img.height / img.width;
        
           var cur = {
             width: Math.floor(img.width * 0.5),
             height: Math.floor(img.height * 0.5)
           }
        
           oc.width = cur.width;
           oc.height = cur.height;
        
           octx.drawImage(img, 0, 0, cur.width, cur.height);
        
           while (cur.width * 0.5 > width) {
             cur = {
               width: Math.floor(cur.width * 0.5),
               height: Math.floor(cur.height * 0.5)
             };
             octx.drawImage(oc, 0, 0, cur.width * 2, cur.height * 2, 0, 0, cur.width, cur.height);
           }
        
           ctx.drawImage(oc, 0, 0, cur.width, cur.height, 0, 0, canvas.width, canvas.height);
        }
        

        【讨论】:

        • 有史以来最被低估的答案。
        • 作为一个魅力!非常感谢!
        • 这应该是公认的答案。只需确保将“宽度”替换为所需的宽度值即可。
        【解决方案6】:

        虽然其中一些 code-sn-ps 简短且有效,但遵循和理解它们并非易事。

        由于我不喜欢 stack-overflow 中的“复制粘贴”,我希望开发人员能够理解他们推送到软件中的代码,希望您会发现以下内容对您有用。

        DEMO:使用 JS 和 HTML Canvas Demo fiddler 调整图像大小。

        您可能会发现 3 种不同的方法来调整大小,这将帮助您了解代码的工作原理以及原因。

        https://jsfiddle.net/1b68eLdr/93089/

        完整的演示代码,以及您可能希望在代码中使用的 TypeScript 方法,都可以在 GitHub 项目中找到。

        https://github.com/eyalc4/ts-image-resizer

        这是最终代码:

        export class ImageTools {
        base64ResizedImage: string = null;
        
        constructor() {
        }
        
        ResizeImage(base64image: string, width: number = 1080, height: number = 1080) {
            let img = new Image();
            img.src = base64image;
        
            img.onload = () => {
        
                // Check if the image require resize at all
                if(img.height <= height && img.width <= width) {
                    this.base64ResizedImage = base64image;
        
                    // TODO: Call method to do something with the resize image
                }
                else {
                    // Make sure the width and height preserve the original aspect ratio and adjust if needed
                    if(img.height > img.width) {
                        width = Math.floor(height * (img.width / img.height));
                    }
                    else {
                        height = Math.floor(width * (img.height / img.width));
                    }
        
                    let resizingCanvas: HTMLCanvasElement = document.createElement('canvas');
                    let resizingCanvasContext = resizingCanvas.getContext("2d");
        
                    // Start with original image size
                    resizingCanvas.width = img.width;
                    resizingCanvas.height = img.height;
        
        
                    // Draw the original image on the (temp) resizing canvas
                    resizingCanvasContext.drawImage(img, 0, 0, resizingCanvas.width, resizingCanvas.height);
        
                    let curImageDimensions = {
                        width: Math.floor(img.width),
                        height: Math.floor(img.height)
                    };
        
                    let halfImageDimensions = {
                        width: null,
                        height: null
                    };
        
                    // Quickly reduce the dize by 50% each time in few iterations until the size is less then
                    // 2x time the target size - the motivation for it, is to reduce the aliasing that would have been
                    // created with direct reduction of very big image to small image
                    while (curImageDimensions.width * 0.5 > width) {
                        // Reduce the resizing canvas by half and refresh the image
                        halfImageDimensions.width = Math.floor(curImageDimensions.width * 0.5);
                        halfImageDimensions.height = Math.floor(curImageDimensions.height * 0.5);
        
                        resizingCanvasContext.drawImage(resizingCanvas, 0, 0, curImageDimensions.width, curImageDimensions.height,
                            0, 0, halfImageDimensions.width, halfImageDimensions.height);
        
                        curImageDimensions.width = halfImageDimensions.width;
                        curImageDimensions.height = halfImageDimensions.height;
                    }
        
                    // Now do final resize for the resizingCanvas to meet the dimension requirments
                    // directly to the output canvas, that will output the final image
                    let outputCanvas: HTMLCanvasElement = document.createElement('canvas');
                    let outputCanvasContext = outputCanvas.getContext("2d");
        
                    outputCanvas.width = width;
                    outputCanvas.height = height;
        
                    outputCanvasContext.drawImage(resizingCanvas, 0, 0, curImageDimensions.width, curImageDimensions.height,
                        0, 0, width, height);
        
                    // output the canvas pixels as an image. params: format, quality
                    this.base64ResizedImage = outputCanvas.toDataURL('image/jpeg', 0.85);
        
                    // TODO: Call method to do something with the resize image
                }
            };
        }}
        

        【讨论】:

          【解决方案7】:

          我创建了一个可重用的 Angular 服务来为任何感兴趣的人处理高质量的图像/画布大小调整:https://gist.github.com/transitive-bullshit/37bac5e741eaec60e983

          该服务包括两种解决方案,因为它们都有各自的优缺点。 lanczos 卷积方法以较慢为代价获得更高质量,而逐步缩小方法可产生合理的抗锯齿结果,并且速度明显更快。

          示例用法:

          angular.module('demo').controller('ExampleCtrl', function (imageService) {
            // EXAMPLE USAGE
            // NOTE: it's bad practice to access the DOM inside a controller, 
            // but this is just to show the example usage.
          
            // resize by lanczos-sinc filter
            imageService.resize($('#myimg')[0], 256, 256)
              .then(function (resizedImage) {
                // do something with resized image
              })
          
            // resize by stepping down image size in increments of 2x
            imageService.resizeStep($('#myimg')[0], 256, 256)
              .then(function (resizedImage) {
                // do something with resized image
              })
          })
          

          【讨论】:

          【解决方案8】:

          我写了一个小的 js-utility 来在前端裁剪和调整图像大小。这是 GitHub 项目上的link。您也可以从最终图像中获取 blob 以发送它。

          import imageSqResizer from './image-square-resizer.js'
          
          let resizer = new imageSqResizer(
              'image-input',
              300,
              (dataUrl) => 
                  document.getElementById('image-output').src = dataUrl;
          );
          //Get blob
          let formData = new FormData();
          formData.append('files[0]', resizer.blob);
          
          //get dataUrl
          document.getElementById('image-output').src = resizer.dataUrl;
          

          【讨论】:

            【解决方案9】:

            您可以使用向下步进来获得更好的结果。大多数浏览器在调整图像大小时似乎会use linear interpolation rather than bi-cubic

            更新在规范中添加了一个质量属性,imageSmoothingQuality,目前仅在 Chrome 中可用。)

            除非选择不平滑或最近邻,否则浏览器将始终在缩小图像后对图像进行插值,因为此功能用作低通滤波器以避免锯齿。

            双线性使用 2x2 像素进行插值,而双三次使用 4x4,因此通过逐步执行此操作,您可以在使用双线性插值的同时获得接近双三次结果的结果图像。

            var canvas = document.getElementById("canvas");
            var ctx = canvas.getContext("2d");
            var img = new Image();
            
            img.onload = function () {
            
                // set size proportional to image
                canvas.height = canvas.width * (img.height / img.width);
            
                // step 1 - resize to 50%
                var oc = document.createElement('canvas'),
                    octx = oc.getContext('2d');
            
                oc.width = img.width * 0.5;
                oc.height = img.height * 0.5;
                octx.drawImage(img, 0, 0, oc.width, oc.height);
            
                // step 2
                octx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5);
            
                // step 3, resize to final size
                ctx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5,
                0, 0, canvas.width, canvas.height);
            }
            img.src = "//i.imgur.com/SHo6Fub.jpg";
            <img src="//i.imgur.com/SHo6Fub.jpg" width="300" height="234">
            <canvas id="canvas" width=300></canvas>

            根据您调整大小的剧烈程度,如果差异较小,您可以跳过第 2 步。

            在演示中,您可以看到新结果现在与图像元素非常相似。

            【讨论】:

            • @steve heh,有时会发生这些事情 :) 对于图像,您通常可以通过设置 css BTW 来覆盖它。
            • Ken,第一个结果很好,但是当我更改图像时,您会发现它太模糊了jsfiddle.net/kcHLG在这种情况下和其他情况下可以做什么?
            • @steve 您可以将步骤数减少到只有 1 或没有(对于某些图像,这可以正常工作)。另请参阅this answer,它与此类似,但在这里我添加了锐化卷积,以便您可以在缩小后使生成的图像更清晰。
            • @steve 是一个修改过的小提琴,只使用了一个额外的步骤:jsfiddle.net/AbdiasSoftware/kcHLG/1
            • @neaumusic 该代码是 OPs 代码的延续。如果你打开小提琴,你会看到 ctx 被定义。为了避免误解,我在这里内联了。
            【解决方案10】:

            根据 K3N 的回答,我一般会为任何人重写代码

            var oc = document.createElement('canvas'), octx = oc.getContext('2d');
                oc.width = img.width;
                oc.height = img.height;
                octx.drawImage(img, 0, 0);
                while (oc.width * 0.5 > width) {
                   oc.width *= 0.5;
                   oc.height *= 0.5;
                   octx.drawImage(oc, 0, 0, oc.width, oc.height);
                }
                oc.width = width;
                oc.height = oc.width * img.height / img.width;
                octx.drawImage(img, 0, 0, oc.width, oc.height);
            

            更新 JSFIDDLE 演示

            这是我的ONLINE DEMO

            【讨论】:

            • 这不起作用:每次调整画布大小时,它都会清除其上下文。你需要2张画布。这里和直接用最终尺寸调用drawImage是一样的。
            【解决方案11】:

            我创建了一个库,允许您在保留所有颜色数据的同时降低任何百分比。

            https://github.com/danschumann/limby-resize/blob/master/lib/canvas_resize.js

            您可以在浏览器中包含该文件。结果看起来像 Photoshop 或图像魔术,保留所有颜色数据,平均像素,而不是取附近的并丢弃其他的。它不使用公式来猜测平均值,而是取精确的平均值。

            【讨论】:

            • 我现在可能会使用 webgl 来调整大小
            猜你喜欢
            • 2012-08-05
            • 1970-01-01
            • 2011-02-19
            • 1970-01-01
            • 2019-04-29
            • 2020-01-21
            • 2011-05-09
            • 1970-01-01
            相关资源
            最近更新 更多