브라우저에서 자바 스크립트를 통해 이미지를 압축하는 방법은 무엇입니까?


91

TL; DR;

이미지 (대부분 jpeg, png 및 gif)를 업로드하기 전에 브라우저에서 직접 압축하는 방법이 있습니까? JavaScript가 이것을 할 수 있다고 확신하지만 그것을 달성하는 방법을 찾을 수 없습니다.


구현하려는 전체 시나리오는 다음과 같습니다.

  • 사용자가 내 웹 사이트로 이동하여 input type="file"요소 를 통해 이미지를 선택합니다 .
  • 이 이미지는 JavaScript를 통해 검색되며 올바른 파일 형식, 최대 파일 크기 등과 같은 몇 가지 확인을 수행합니다.
  • 모든 것이 정상이면 페이지에 이미지 미리보기가 표시됩니다.
  • 사용자는 이미지를 90 ° / -90 ° 회전, 미리 정의 된 비율에 따라 자르기 등과 같은 몇 가지 기본 작업을 수행하거나 다른 이미지를 업로드하고 1 단계로 돌아갈 수 있습니다.
  • 사용자가 만족하면 편집 된 이미지가 압축되고 로컬로 "저장"됩니다 (파일에 저장되지 않고 브라우저 메모리 / 페이지에 저장 됨).
  • 사용자는 이름, 나이 등과 같은 데이터로 양식을 채 웁니다.
  • 사용자가 "마침"버튼을 클릭하면 데이터 + 압축 이미지가 포함 된 양식이 서버로 전송됩니다 (AJAX 제외).

마지막 단계까지의 전체 프로세스는 클라이언트 측에서 수행되어야하며 최신 Chrome 및 Firefox, Safari 5+ 및 IE 8+에서 호환되어야합니다 . 가능하다면 자바 스크립트 만 사용해야합니다 (하지만 이것이 불가능하다고 확신합니다).

지금은 아무것도 코딩하지 않았지만 이미 생각했습니다. 로컬에서 파일 읽기는 File API 를 통해 가능 하며 이미지 미리보기 및 편집은 Canvas 요소를 사용하여 수행 할 수 있지만 이미지 압축 부분을 수행하는 방법을 찾을 수 없습니다 .

html5please.comcaniuse.com 에 따르면 이러한 브라우저를 지원하는 것은 상당히 어렵지만 (IE 덕분에) FlashCanvasFileReader 와 같은 폴리 필을 사용하여 수행 할 수 있습니다 .

실제로 목표는 파일 크기를 줄이는 것이므로 이미지 압축을 해결책으로 봅니다. 하지만 업로드 된 이미지가 내 웹 사이트에 매번 같은 위치에 표시 될 것임을 알고 있으며이 표시 영역의 크기 (예 : 200x400)도 알고 있습니다. 따라서 해당 크기에 맞게 이미지 크기를 조정하여 파일 크기를 줄일 수 있습니다. 이 기술의 압축 비율이 얼마인지 모르겠습니다.

어떻게 생각해 ? 나에게 해줄 조언이 있습니까? JavaScript에서 이미지 브라우저 측을 압축하는 방법을 알고 있습니까? 답장 해 주셔서 감사합니다.

답변:


164

요컨대 :

  • .readAsArrayBuffer와 함께 HTML5 FileReader API를 사용하여 파일 읽기
  • 파일 데이터로 Blob을 만들고 window.URL.createObjectURL (blob)을 사용 하여 URL을 가져옵니다.
  • 새 이미지 요소를 만들고 src를 파일 blob URL로 설정합니다.
  • 캔버스로 이미지를 보냅니다. 캔버스 크기는 원하는 출력 크기로 설정됩니다.
  • canvas.toDataURL ( "image / jpeg", 0.7)을 통해 캔버스에서 축소 된 데이터를 다시 가져옵니다 (자신의 출력 형식 및 품질 설정).
  • 새로운 숨겨진 입력을 원본 양식에 연결하고 dataURI 이미지를 기본적으로 일반 텍스트로 전송
  • 백엔드에서 dataURI를 읽고 Base64에서 디코딩 한 다음 저장합니다.

출처 : code .


1
고마워요! 이것이 제가 찾던 것입니다. 이 기술로 압축비가 얼마나 좋은지 아십니까?
pomeh 2013

2
@NicholasKyriakides canvas.toDataURL("image/jpeg",0.7)효과적으로 압축하는 것을 확인할 수 있으며 JPEG를 품질 70 (기본값 인 품질 100과 반대)으로 저장합니다.
user1111929

4
@Nicholas Kyriakides, 이것은 좋은 구별이 아닙니다. 대부분의 코덱은 무손실이 아니므로 "다운 스케일링"정의에 적합합니다 (즉, 100으로 되돌릴 수 없음).
Billybobbonnet 2015-06-24

5
축소는 높이와 너비 측면에서 더 작은 크기의 이미지를 만드는 것을 말합니다. 이것은 정말로 압축입니다. 손실 압축이지만 확실히 압축입니다. 픽셀을 축소하는 것이 아니라 일부 픽셀을 동일한 색상으로 밀어서 압축이 더 적은 비트로 해당 색상에 도달 할 수 있도록합니다. 어쨌든 JPEG는 픽셀에 대한 압축 기능을 내장했지만 손실 모드에서는 몇 가지 색상을 동일한 색상이라고 부를 수 있다고 말합니다. 그것은 여전히 ​​압축입니다. 그래픽과 관련된 축소는 일반적으로 실제 크기의 변경을 의미합니다.
Tatarize

3
나는 단지 이것을 말하고 싶다 : 파일은 URL.createObjectUrl()blob으로 바꾸지 않고 곧바로 갈 수있다 ; 파일은 blob으로 계산됩니다.
hellol11

20

다른 답변에서 누락 된 두 가지가 있습니다.

  • canvas.toBlob(사용 가능한 경우)는보다 성능이 canvas.toDataURL뛰어나고 비동기식입니다.
  • 파일-> 이미지-> 캔버스-> 파일 변환으로 인해 EXIF ​​데이터가 손실됩니다. 특히 최신 휴대폰 / 태블릿에서 일반적으로 설정되는 이미지 회전에 대한 데이터입니다.

다음 스크립트는 두 가지 사항을 모두 다룹니다.

// From https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob, needed for Safari:
if (!HTMLCanvasElement.prototype.toBlob) {
    Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
        value: function(callback, type, quality) {

            var binStr = atob(this.toDataURL(type, quality).split(',')[1]),
                len = binStr.length,
                arr = new Uint8Array(len);

            for (var i = 0; i < len; i++) {
                arr[i] = binStr.charCodeAt(i);
            }

            callback(new Blob([arr], {type: type || 'image/png'}));
        }
    });
}

window.URL = window.URL || window.webkitURL;

// Modified from https://stackoverflow.com/a/32490603, cc by-sa 3.0
// -2 = not jpeg, -1 = no data, 1..8 = orientations
function getExifOrientation(file, callback) {
    // Suggestion from http://code.flickr.net/2012/06/01/parsing-exif-client-side-using-javascript-2/:
    if (file.slice) {
        file = file.slice(0, 131072);
    } else if (file.webkitSlice) {
        file = file.webkitSlice(0, 131072);
    }

    var reader = new FileReader();
    reader.onload = function(e) {
        var view = new DataView(e.target.result);
        if (view.getUint16(0, false) != 0xFFD8) {
            callback(-2);
            return;
        }
        var length = view.byteLength, offset = 2;
        while (offset < length) {
            var marker = view.getUint16(offset, false);
            offset += 2;
            if (marker == 0xFFE1) {
                if (view.getUint32(offset += 2, false) != 0x45786966) {
                    callback(-1);
                    return;
                }
                var little = view.getUint16(offset += 6, false) == 0x4949;
                offset += view.getUint32(offset + 4, little);
                var tags = view.getUint16(offset, little);
                offset += 2;
                for (var i = 0; i < tags; i++)
                    if (view.getUint16(offset + (i * 12), little) == 0x0112) {
                        callback(view.getUint16(offset + (i * 12) + 8, little));
                        return;
                    }
            }
            else if ((marker & 0xFF00) != 0xFF00) break;
            else offset += view.getUint16(offset, false);
        }
        callback(-1);
    };
    reader.readAsArrayBuffer(file);
}

// Derived from https://stackoverflow.com/a/40867559, cc by-sa
function imgToCanvasWithOrientation(img, rawWidth, rawHeight, orientation) {
    var canvas = document.createElement('canvas');
    if (orientation > 4) {
        canvas.width = rawHeight;
        canvas.height = rawWidth;
    } else {
        canvas.width = rawWidth;
        canvas.height = rawHeight;
    }

    if (orientation > 1) {
        console.log("EXIF orientation = " + orientation + ", rotating picture");
    }

    var ctx = canvas.getContext('2d');
    switch (orientation) {
        case 2: ctx.transform(-1, 0, 0, 1, rawWidth, 0); break;
        case 3: ctx.transform(-1, 0, 0, -1, rawWidth, rawHeight); break;
        case 4: ctx.transform(1, 0, 0, -1, 0, rawHeight); break;
        case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
        case 6: ctx.transform(0, 1, -1, 0, rawHeight, 0); break;
        case 7: ctx.transform(0, -1, -1, 0, rawHeight, rawWidth); break;
        case 8: ctx.transform(0, -1, 1, 0, 0, rawWidth); break;
    }
    ctx.drawImage(img, 0, 0, rawWidth, rawHeight);
    return canvas;
}

function reduceFileSize(file, acceptFileSize, maxWidth, maxHeight, quality, callback) {
    if (file.size <= acceptFileSize) {
        callback(file);
        return;
    }
    var img = new Image();
    img.onerror = function() {
        URL.revokeObjectURL(this.src);
        callback(file);
    };
    img.onload = function() {
        URL.revokeObjectURL(this.src);
        getExifOrientation(file, function(orientation) {
            var w = img.width, h = img.height;
            var scale = (orientation > 4 ?
                Math.min(maxHeight / w, maxWidth / h, 1) :
                Math.min(maxWidth / w, maxHeight / h, 1));
            h = Math.round(h * scale);
            w = Math.round(w * scale);

            var canvas = imgToCanvasWithOrientation(img, w, h, orientation);
            canvas.toBlob(function(blob) {
                console.log("Resized image to " + w + "x" + h + ", " + (blob.size >> 10) + "kB");
                callback(blob);
            }, 'image/jpeg', quality);
        });
    };
    img.src = URL.createObjectURL(file);
}

사용 예 :

inputfile.onchange = function() {
    // If file size > 500kB, resize such that width <= 1000, quality = 0.9
    reduceFileSize(this.files[0], 500*1024, 1000, Infinity, 0.9, blob => {
        let body = new FormData();
        body.set('file', blob, blob.name || "file.jpg");
        fetch('/upload-image', {method: 'POST', body}).then(...);
    });
};

ToBlob은 나를 위해 트릭을 수행하여 파일을 만들고 서버의 $ _FILES 배열을 수신했습니다. 감사합니다!
Ricardo Ruiz Romero

멋지네요! 그러나 이것이 모든 브라우저, 웹 및 모바일에서 작동합니까? (IE를 무시합시다)
Garvit Jain

14

@PsychoWoods의 대답은 좋습니다. 나만의 솔루션을 제공하고 싶습니다. 이 자바 스크립트 함수는 이미지 데이터 URL과 너비를 가져 와서 새 너비로 조정하고 새 데이터 URL을 반환합니다.

// Take an image URL, downscale it to the given width, and return a new image URL.
function downscaleImage(dataUrl, newWidth, imageType, imageArguments) {
    "use strict";
    var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;

    // Provide default values
    imageType = imageType || "image/jpeg";
    imageArguments = imageArguments || 0.7;

    // Create a temporary image so that we can compute the height of the downscaled image.
    image = new Image();
    image.src = dataUrl;
    oldWidth = image.width;
    oldHeight = image.height;
    newHeight = Math.floor(oldHeight / oldWidth * newWidth)

    // Create a temporary canvas to draw the downscaled image on.
    canvas = document.createElement("canvas");
    canvas.width = newWidth;
    canvas.height = newHeight;

    // Draw the downscaled image on the canvas and return the new data URL.
    ctx = canvas.getContext("2d");
    ctx.drawImage(image, 0, 0, newWidth, newHeight);
    newDataUrl = canvas.toDataURL(imageType, imageArguments);
    return newDataUrl;
}

이 코드는 데이터 URL이 있고 축소 된 이미지에 대한 데이터 URL이 필요한 모든 곳에서 사용할 수 있습니다.


이 예제, 함수 호출 방법 및 결과 반환 방법에 대해 자세히 설명해 주시겠습니까?
visulo

다음은 예입니다. danielsadventure.info/Html/scaleimage.html 페이지의 소스를 읽고 작동 방식을 확인하세요.
Vivian River

1
web.archive.org/web/20171226190510/danielsadventure.info/Html/... 링크를 읽고 싶어 다른 사람들을 위해이 @DanielAllenLangdon에 의해 제안
세드릭 ARNOULD

참고로 image.width / height는로드되지 않았으므로 0을 반환합니다. 이를 비동기 함수로 변환하고 image.onload를 수신하여 및 높이가있는 올바른 이미지를 가져와야 할 수 있습니다.
Matt Pope


4

이미지로드가 비동기 이기 때문에 및 속성을 즉시 사용할 수 없다는 downscaleImage()점에서 @ daniel-allen-langdon에 의해 위에 게시 된 함수에 문제가있었습니다 .image.widthimage.height

이를 고려하고 async기능을 사용 하며 너비가 아닌 가장 긴 치수를 기준으로 이미지 크기를 조정하는 아래 업데이트 된 TypeScript 예제를 참조하십시오.

function getImage(dataUrl: string): Promise<HTMLImageElement> 
{
    return new Promise((resolve, reject) => {
        const image = new Image();
        image.src = dataUrl;
        image.onload = () => {
            resolve(image);
        };
        image.onerror = (el: any, err: ErrorEvent) => {
            reject(err.error);
        };
    });
}

export async function downscaleImage(
        dataUrl: string,  
        imageType: string,  // e.g. 'image/jpeg'
        resolution: number,  // max width/height in pixels
        quality: number   // e.g. 0.9 = 90% quality
    ): Promise<string> {

    // Create a temporary image so that we can compute the height of the image.
    const image = await getImage(dataUrl);
    const oldWidth = image.naturalWidth;
    const oldHeight = image.naturalHeight;
    console.log('dims', oldWidth, oldHeight);

    const longestDimension = oldWidth > oldHeight ? 'width' : 'height';
    const currentRes = longestDimension == 'width' ? oldWidth : oldHeight;
    console.log('longest dim', longestDimension, currentRes);

    if (currentRes > resolution) {
        console.log('need to resize...');

        // Calculate new dimensions
        const newSize = longestDimension == 'width'
            ? Math.floor(oldHeight / oldWidth * resolution)
            : Math.floor(oldWidth / oldHeight * resolution);
        const newWidth = longestDimension == 'width' ? resolution : newSize;
        const newHeight = longestDimension == 'height' ? resolution : newSize;
        console.log('new width / height', newWidth, newHeight);

        // Create a temporary canvas to draw the downscaled image on.
        const canvas = document.createElement('canvas');
        canvas.width = newWidth;
        canvas.height = newHeight;

        // Draw the downscaled image on the canvas and return the new data URL.
        const ctx = canvas.getContext('2d')!;
        ctx.drawImage(image, 0, 0, newWidth, newHeight);
        const newDataUrl = canvas.toDataURL(imageType, quality);
        return newDataUrl;
    }
    else {
        return dataUrl;
    }

}

나는의 설명 추가 할 것 quality, resolution그리고 imageType(이의 형식)
바라 바스

3

편집 :이 답변에 대한 Mr Me 의견에 따라 이제 JPG / WebP 형식에 대해 압축을 사용할 수있는 것 같습니다 ( https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL 참조 ) .

내가 아는 한 캔버스를 사용하여 이미지를 압축 할 수 없으며 대신 크기를 조정할 수 있습니다. canvas.toDataURL을 사용하면 사용할 압축 비율을 선택할 수 없습니다. 원하는 것을 정확하게 수행하는 canimage를 살펴볼 수 있습니다. https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

실제로 이미지 크기를 조정하여 크기를 줄이는 것만으로도 충분하지만 더 나아가고 싶다면 새로 도입 된 file.readAsArrayBuffer 메서드를 사용하여 이미지 데이터를 포함하는 버퍼를 가져와야합니다.

그런 다음 DataView를 사용하여 이미지 형식 사양 ( http://en.wikipedia.org/wiki/JPEG 또는 http://en.wikipedia.org/wiki/Portable_Network_Graphics ) 에 따라 콘텐츠를 읽습니다 .

이미지 데이터 압축을 처리하는 것은 어렵지만 시도하는 것이 더 나쁩니다. 반면에 PNG 헤더 또는 JPEG exif 데이터를 삭제하여 이미지를 더 작게 만들 수 있습니다. 그렇게하는 것이 더 쉽습니다.

다른 버퍼에 또 다른 DataWiew를 만들고 필터링 된 이미지 콘텐츠로 채워야합니다. 그런 다음 window.btoa를 사용하여 이미지 콘텐츠를 DataURI로 인코딩하기 만하면됩니다.

비슷한 것을 구현하면 코드를 살펴 보는 것이 흥미로울 것입니다.


이 글을 게시 한 후 변경된 사항이있을 수 있지만 언급 한 canvas.toDataURL 함수에 대한 두 번째 인수 는 적용하려는 압축의 양입니다.
wp-overwatch.com

2

받아 들여진 대답에 비해 더 간단한 해결책이 있다는 것을 알았습니다.

  • HTML5 FileReader API를 사용하여 파일 읽기 .readAsArrayBuffer
  • 파일 데이터로 Blob을 만들고 다음을 사용하여 URL을 가져옵니다. window.URL.createObjectURL(blob)
  • 새 이미지 요소를 만들고 src를 파일 blob URL로 설정합니다.
  • 캔버스로 이미지를 보냅니다. 캔버스 크기는 원하는 출력 크기로 설정됩니다.
  • 캔버스에서 축소 된 데이터를 다시 가져 오기 를 통해을 canvas.toDataURL("image/jpeg",0.7)(자신의 출력 형식과 품질을 설정)
  • 숨겨진 새 입력을 원본 양식에 연결 하고 dataURI 이미지를 기본적으로 일반 텍스트로 전송
  • 백엔드에서 dataURI를 읽고 Base64에서 디코딩 한 다음 저장합니다.

귀하의 질문에 따라 :

이미지를 업로드하기 전에 브라우저에서 직접 이미지 (대부분 jpeg, png 및 gif)를 압축하는 방법이 있습니까?

내 솔루션 :

  1. .NET을 사용하여 직접 파일로 Blob을 만듭니다 URL.createObjectURL(inputFileElement.files[0]).

  2. 수락 된 답변과 동일합니다.

  3. 수락 된 답변과 동일합니다. 그것을 언급 할 가치, 캔버스 크기가 필요하고 사용하는 것입니다 img.widthimg.height세트에 canvas.widthcanvas.height. 아닙니다 img.clientWidth.

  4. 축소 이미지를 canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5). 설정 'image/jpg'이 적용되지 않습니다. image/png지원됩니다. 새로운 확인 File내부 객체 callbackfunction 과를 let compressedImageBlob = new File([blob]).

  5. 새로운 숨겨진 입력을 추가하거나 javascript를 통해 보냅니다 . 서버는 아무것도 디코딩 할 필요가 없습니다.

모든 정보는 https://javascript.info/binary 를 확인 하십시오 . 이 장을 읽은 후 해결책을 찾았습니다.


암호:

    <!DOCTYPE html>
    <html>
    <body>
    <form action="upload.php" method="post" enctype="multipart/form-data">
      Select image to upload:
      <input type="file" name="fileToUpload" id="fileToUpload" multiple>
      <input type="submit" value="Upload Image" name="submit">
    </form>
    </body>
    </html>

이 코드는 다른 답변보다 훨씬 덜 무섭게 보입니다 ..

최신 정보:

하나는 모든 것을 안에 넣어야 img.onload합니다. 그렇지 않으면 canvas시간 canvas이 할당 될 때 이미지의 너비와 높이를 올바르게 가져올 수 없습니다 .

    function upload(){
        var f = fileToUpload.files[0];
        var fileName = f.name.split('.')[0];
        var img = new Image();
        img.src = URL.createObjectURL(f);
        img.onload = function(){
            var canvas = document.createElement('canvas');
            canvas.width = img.width;
            canvas.height = img.height;
            var ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0);
            canvas.toBlob(function(blob){
                    console.info(blob.size);
                    var f2 = new File([blob], fileName + ".jpeg");
                    var xhr = new XMLHttpRequest();
                    var form = new FormData();
                    form.append("fileToUpload", f2);
                    xhr.open("POST", "upload.php");
                    xhr.send(form);
            }, 'image/jpeg', 0.5);
        }
    }

3.4MB .pngimage/jpeg인수가 설정된 파일 압축 테스트 .

    |0.9| 777KB |
    |0.8| 383KB |
    |0.7| 301KB |
    |0.6| 251KB |
    |0.5| 219kB |


0

나는 다음과 같이 기능을 개선했습니다.

var minifyImg = function(dataUrl,newWidth,imageType="image/jpeg",resolve,imageArguments=0.7){
    var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;
    (new Promise(function(resolve){
      image = new Image(); image.src = dataUrl;
      log(image);
      resolve('Done : ');
    })).then((d)=>{
      oldWidth = image.width; oldHeight = image.height;
      log([oldWidth,oldHeight]);
      newHeight = Math.floor(oldHeight / oldWidth * newWidth);
      log(d+' '+newHeight);

      canvas = document.createElement("canvas");
      canvas.width = newWidth; canvas.height = newHeight;
      log(canvas);
      ctx = canvas.getContext("2d");
      ctx.drawImage(image, 0, 0, newWidth, newHeight);
      //log(ctx);
      newDataUrl = canvas.toDataURL(imageType, imageArguments);
      resolve(newDataUrl);
    });
  };

그것의 사용 :

minifyImg(<--DATAURL_HERE-->,<--new width-->,<--type like image/jpeg-->,(data)=>{
   console.log(data); // the new DATAURL
});

즐겨 ;)

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.