HTML5 Cambiar el tamaño de las imágenes antes de subir

181

Aquí hay un rascador de fideos.

Teniendo en cuenta que tenemos almacenamiento local HTML5 y xhr v2 y otras cosas. Me preguntaba si alguien podría encontrar un ejemplo de trabajo o simplemente darme un sí o un no para esta pregunta:

¿Es posible cambiar el tamaño de una imagen usando el nuevo almacenamiento local (o lo que sea), de modo que un usuario que no tenga idea de cómo cambiar el tamaño de una imagen pueda arrastrar su imagen de 10 mb a mi sitio web, cambiar su tamaño usando el nuevo almacenamiento local y LUEGO cárguelo en el tamaño más pequeño.

Sé muy bien que puedes hacerlo con Flash, applets de Java, X activo ... La pregunta es si puedes hacerlo con Javascript + Html5.

Esperamos la respuesta en este caso.

Ta por ahora.

Jimmyt1988
fuente

Respuestas:

181

Sí, use la API de archivo , luego puede procesar las imágenes con el elemento de lienzo .

Esta publicación de blog de Mozilla Hacks lo guía a través de la mayor parte del proceso. Como referencia, aquí está el código fuente ensamblado de la publicación del blog:

// 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
robertc
fuente
66
Vaya, gracias buen señor. Tendré una obra de teatro esta noche ... Con el archivo api que es. Obtuve la carga de arrastrar y soltar para trabajar y me di cuenta de que esto también sería una característica realmente agradable para incluir. Yippee
Jimmyt1988
3
No necesitamos usar este mycanvas.toDataURL ("image / jpeg", 0.5) para asegurarnos de que la calidad se lleva a fin de disminuir el tamaño del archivo. Me encontré con este comentario en el blog de Mozilla y vi la resolución de errores aquí bugzilla.mozilla.org/show_bug.cgi?id=564388
sbose
33
Debe esperar onloaden la imagen (adjunte el controlador antes de la configuración src), ya que el navegador puede decodificar de forma asincrónica y es posible que los píxeles no estén disponibles antes drawImage.
Kornel
66
Es posible que desee colocar el código del lienzo en la función de carga del lector de archivos; existe la posibilidad de que las imágenes grandes no se carguen antes de que se ctx.drawImage()acerque al final.
Greg
44
tenga cuidado, necesita magia adicional para que funcione en iOS: stackoverflow.com/questions/11929099/…
flo850
107

Abordé este problema hace unos años y subí mi solución a github como https://github.com/rossturner/HTML5-ImageUploader

La respuesta de robertc utiliza la solución propuesta en la publicación del blog de Mozilla Hacks , sin embargo, descubrí que esto proporcionaba una calidad de imagen realmente pobre al cambiar el tamaño a una escala que no era 2: 1 (o un múltiplo de la misma). Comencé a experimentar con diferentes algoritmos de cambio de tamaño de imagen, aunque la mayoría terminó siendo bastante lenta o de lo contrario tampoco fue de gran calidad.

Finalmente, encontré una solución que creo que se ejecuta rápidamente y que también tiene un rendimiento bastante bueno, ya que la solución de Mozilla de copiar de 1 lienzo a otro funciona rápidamente y sin pérdida de calidad de imagen en una proporción de 2: 1, dado un objetivo de x píxeles de ancho y píxeles de alto, uso este método de cambio de tamaño del lienzo hasta que la imagen esté entre x y 2 x , y y y 2 y . En este punto, paso a cambiar el tamaño de la imagen algorítmica para el "paso" final de cambiar el tamaño al tamaño deseado. Después de probar varios algoritmos diferentes, me decidí por la interpolación bilineal tomada de un blog que ya no está en línea pero que es accesible a través de Internet Archive, que da buenos resultados, aquí está el código aplicable:

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

Esto reduce la imagen a un ancho de config.maxWidth, manteniendo la relación de aspecto original. En el momento del desarrollo, esto funcionaba en iPad / iPhone Safari, además de los principales navegadores de escritorio (IE9 +, Firefox, Chrome), por lo que espero que aún sea compatible dada la mayor aceptación de HTML5 en la actualidad. Tenga en cuenta que la llamada canvas.toDataURL () toma un tipo mime y una calidad de imagen que le permitirá controlar la calidad y el formato del archivo de salida (potencialmente diferente de la entrada si lo desea).

El único punto que esto no cubre es mantener la información de orientación, sin el conocimiento de estos metadatos, la imagen se redimensiona y se guarda tal cual, perdiendo cualquier metadato dentro de la imagen para orientación, lo que significa que las imágenes tomadas en un dispositivo de tableta "al revés" fueron renderizado como tal, aunque se habrían volteado en el visor de la cámara del dispositivo. Si esto es una preocupación, esta publicación de blog tiene una buena guía y ejemplos de código sobre cómo lograr esto, que estoy seguro podría integrarse en el código anterior.

Ross Taylor-Turner
fuente
44
Le otorgé la recompensa porque siento que agrega un montón a la respuesta, aunque tengo que integrar esa orientación :)
Sam Saffron
3
Un alma muy útil ahora se ha fusionado en alguna funcionalidad para los metadatos de orientación :)
Ross Taylor-Turner
26

Corrección a lo anterior:

<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>
Justin Levene
fuente
2
¿Cómo maneja la parte de PHP después de agregarlo a FormData ()? ¿No estaría buscando $ _FILES ['nombre'] ['tmp_name'] [$ i], por ejemplo? Estoy intentando if (isset ($ _ POST ['image'])) ... pero dataurlno está allí.
denikov
2
no funciona en firefox la primera vez que intentas subir una imagen. La próxima vez que lo intentes funciona. ¿Como arreglarlo?
Fred
devuelve si el archivo es matriz es nulo o está vacío.
Sheelpriy
2
En Chrome cuando intentas acceder img.widthe img.heightinicialmente lo están 0. Deberías poner el resto de la función adentroimg.addEventListener('load', function () { // now the image has loaded, continue... });
Íñigo el
2
@Justin, ¿puedes aclarar "lo anterior", como en, de qué es esta publicación una corrección? Saludos
Martin
16

Modificación de la respuesta de Justin que funciona para mí:

  1. Agregado img.onload
  2. Expande la solicitud POST con un ejemplo real

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);
}
Será
fuente
2
información de orientación (exif) se pierde
Konstantin Vahrushev
8

Si no desea reinventar la rueda, puede probar plupload.com

nanospeck
fuente
3
También uso plupload para cambiar el tamaño del lado del cliente. Lo mejor de todo es que puede usar flash, html5, silverlight, etc. Lo que el usuario tenga disponible, porque cada usuario tiene una configuración diferente. Si solo quieres html5, creo que no funcionará en algunos navegadores antiguos y Opera.
jnl
6

Mecanografiado

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;
}
jales cardoso
fuente
1
Cualquiera que se encuentre con esta publicación, esta respuesta basada en promesas es mucho más confiable en mi opinión. Me convertí a JS normal y funciona a las mil maravillas.
gbland777
5
fd.append("image", dataurl);

Esto no funcionará. En el lado de PHP no puede guardar el archivo con esto.

Use este código en su lugar:

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
robot
fuente
5

Cambiar el tamaño de las imágenes en un elemento de lienzo generalmente es una mala idea, ya que utiliza la interpolación de cajas más barata. La imagen resultante notable se degrada en calidad. Recomiendo usar http://nodeca.github.io/pica/demo/ que puede realizar la transformación de Lanczos en su lugar. La página de demostración anterior muestra la diferencia entre el lienzo y los enfoques de Lanczos.

También utiliza trabajadores web para cambiar el tamaño de las imágenes en paralelo. También hay implementación de WEBGL.

Hay algunos redimensionadores de imágenes en línea que usan pica para hacer el trabajo, como https://myimageresizer.com

Ivan Nikitin
fuente
5

La respuesta aceptada funciona muy bien, pero la lógica de cambio de tamaño ignora el caso en que la imagen es más grande que el máximo en solo uno de los ejes (por ejemplo, altura> maxHeight pero ancho <= maxWidth).

Creo que el siguiente código se ocupa de todos los casos de una manera más directa y funcional (ignore las anotaciones de tipo mecanografiado si usa JavaScript simple):

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 };
  }
Taro
fuente
0

Puede usar dropzone.js si desea usar un administrador de carga simple y fácil con el cambio de tamaño antes de las funciones de carga.

Tiene funciones de cambio de tamaño incorporadas, pero puede proporcionar las suyas si lo desea.

Michał Zalewski
fuente