Hur komprimerar man en bild via Javascript i webbläsaren?


Sammanfattning;

Finns det ett sätt att komprimera en bild (främst jpeg, png och gif) direkt i webbläsaren innan den laddas upp? Jag är ganska säker på att JavaScript kan göra detta, men jag kan inte hitta ett sätt att uppnå det.

Här är hela scenariot jag skulle vilja genomföra:

  • användaren går till min webbplats och väljer en bild via ett input type="file"-element,
  • Den här bilden hämtas via JavaScript, vi gör viss verifiering såsom korrekt filformat, maximal filstorlek etc.
  • om allt är OK, visas en förhandsvisning av bilden på sidan,
  • användaren kan utföra några grundläggande operationer såsom att rotera bilden med 90°/-90°, beskära den enligt ett fördefinierat förhållande, etc., eller så kan användaren ladda upp en annan bild och återgå till steg 1,
  • när användaren är nöjd komprimeras den redigerade bilden och "sparas" sedan lokalt (sparas inte till en fil, utan i webbläsarens minne/sida),-
  • användaren fyller i ett formulär med uppgifter som namn, ålder osv.
  • användaren klickar på knappen "Slutför", sedan skickas formuläret som innehåller data + komprimerad bild till servern (utan AJAX),

Hela processen upp till det sista steget bör göras på klientsidan och bör vara kompatibel med senaste versionerna av Chrome och Firefox, Safari 5+ och IE 8+. Om möjligt bör endast JavaScript användas (men jag är ganska säker på att detta inte är möjligt).

Jag har inte kodat något just nu, men jag har redan tänkt på det. Filinläsning lokalt är möjlig via File API, förhandsgranskning och redigering av bilder kan göras med Canvas-elementet, men jag kan inte hitta ett sätt att göra bildkomprimeringsdelen.

Enligt html5please.com och caniuse.com är det ganska svårt att stödja dessa webbläsare (tack vare IE), men det kan göras med hjälp av polyfill som FlashCanvas och FileReader.

Faktiskt är målet att minska filstorleken, så jag ser bildkomprimering som en lösning. Men jag vet att uppladdade bilder kommer att visas på min webbplats, varje gång på samma plats, och jag känner till dimensionerna för detta visningsområde (t.ex. 200x400). Så jag kan ändra storleken på bilden för att passa dessa dimensioner, och därigenom minska filstorleken. Jag har ingen aning om vilken komprimeringsgrad denna teknik skulle ge.

Vad tycker du? Har du några råd att ge mig? Känner du till något sätt att komprimera en bild på klientsidan i JavaScript? Tack för dina svar.

Svar #1

Kort sagt:

  1. Läs filerna med HTML5 FileReader API med .readAsArrayBuffer
  2. Skapa en Blob med filens data och få dess URL med window.URL.createObjectURL(blob)
  3. Skapa ett nytt bildelement och sätt dess src till filblobbens URL
  4. Skicka bilden till canvasen. Canvasstorleken är inställd på önskad utmatningsstorlek
  5. Få tillbaka de nedskalade uppgifterna från canvas via canvas.toDataURL("image/jpeg",0.7) (ange ditt eget utmatningsformat och kvalitet)
  6. Fäst nya dolda inmatningar till det ursprungliga formuläret och överför dataURI-bilderna i princip som vanlig text
  7. På backend, läs dataURI, avkoda från Base64 och spara det

Källa: kod.

Svar #2

Jag ser två saker som saknas från de andra svaren:

  • canvas.toBlob (när tillgängligt) är mer prestandaeffektivt än canvas.toDataURL, och också asynkront.
  • fil -> bild -> duk -> fil-konvertering förlorar EXIF-data; särskilt data om bildrotation som ofta ställs in av moderna telefoner/surfplattor.

Följande manus tar upp båda punkterna:

// 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);
}

Exempel på användning:

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(...);
    });
};

Svar #3

@PsychoWoods svar är bra. Jag skulle vilja erbjuda min egen lösning. Denna Javascript-funktion tar en bilddata-URL och en bredd, skalar den till den nya bredden och returnerar en ny data-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;
}

Denna kod kan användas var som helst där du har en data-URL och vill ha en data-URL för en nedskalad bild.

Svar #4

Du kan ta en titt på bildkonvertering, prova det här --> demosida

Svar #5

Prova detta anpassningsbara rena JS-exempel - Komprimera över 90% :

   <div id="root">
        <p>Upload an image and see the result</p>
        <input id="img-input" type="file" accept="image/*" style="display:block" />
    </div>

    <script>
        const MAX_WIDTH = 320;
        const MAX_HEIGHT = 180;
        const MIME_TYPE = "image/jpeg";
        const QUALITY = 0.7;

        const input = document.getElementById("img-input");
        input.onchange = function (ev) {
            const file = ev.target.files[0]; // get the file
            const blobURL = URL.createObjectURL(file);
            const img = new Image();
            img.src = blobURL;
            img.onerror = function () {
                URL.revokeObjectURL(this.src);
                // Handle the failure properly
                console.log("Cannot load image");
            };
            img.onload = function () {
                URL.revokeObjectURL(this.src);
                const [newWidth, newHeight] = calculateSize(img, MAX_WIDTH, MAX_HEIGHT);
                const canvas = document.createElement("canvas");
                canvas.width = newWidth;
                canvas.height = newHeight;
                const ctx = canvas.getContext("2d");
                ctx.drawImage(img, 0, 0, newWidth, newHeight);
                canvas.toBlob(
                    (blob) => {
                        // Handle the compressed image. es. upload or save in local state
                        displayInfo('Original file', file);
                        displayInfo('Compressed file', blob);
                    },
                    MIME_TYPE,
                    QUALITY
                );
                document.getElementById("root").append(canvas);
            };
        };

        function calculateSize(img, maxWidth, maxHeight) {
            let width = img.width;
            let height = img.height;

            // calculate the width and height, constraining the proportions
            if (width > height) {
                if (width > maxWidth) {
                    height = Math.round((height * maxWidth) / width);
                    width = maxWidth;
                }
            } else {
                if (height > maxHeight) {
                    width = Math.round((width * maxHeight) / height);
                    height = maxHeight;
                }
            }
            return [width, height];
        }

        // Utility functions for demo purpose

        function displayInfo(label, file) {
            const p = document.createElement('p');
            p.innerText = `${label} - ${readableBytes(file.size)}`;
            document.getElementById('root').append(p);
        }

        function readableBytes(bytes) {
            const i = Math.floor(Math.log(bytes) / Math.log(1024)),
                sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

            return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
        }
    </script>

Svar #6

För moderna webbläsare, använd createImageBitmap() istället för img.onload

async function compressImage(blobImg, percent) {
  let bitmap = await createImageBitmap(blobImg);
  let canvas = document.createElement("canvas");
  let ctx = canvas.getContext("2d");
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  ctx.drawImage(bitmap, 0, 0);
  let dataUrl = canvas.toDataURL("image/jpeg", percent/100);
  return dataUrl;
}

inputImg.addEventListener('change', async(e) => {
  let img = e.target.files[0];
  console.log('File Name: ', img.name)
  console.log('Original Size: ', img.size.toLocaleString())
  
  let imgCompressed = await compressImage(img, 75) // set to 75%
  let compSize = atob(imgCompressed.split(",")[1]).length;
  console.log('Compressed Size: ', compSize.toLocaleString())
  //console.log(imgCompressed)
})
<input type="file" id="inputImg">

Relaterade artiklar