HTML5 Предварительно изменить размер изображения перед загрузкой
Вот лапша скребок.
Имея в виду, у нас есть локальное хранилище HTML5 и xhr v2, а что нет. Мне было интересно, сможет ли кто-нибудь найти рабочий пример или просто дать мне ответ "да" или "нет" на этот вопрос:
Можно ли предварительно настроить размер изображения, используя новое локальное хранилище (или что-то еще), чтобы пользователь, который не имеет понятия о том, как изменить размер изображения, может перетащить свое изображение размером 10 МБ на мой веб-сайт, изменить его размер с помощью нового локального хранилища и Затем загрузите его в меньшем размере.
Я прекрасно знаю, что вы можете сделать это с помощью Flash, Java-апплетов, активного X... Вопрос в том, сможете ли вы сделать это с Javascript + Html5.
Ждем ответа на этот.
Та пока.
10 ответов
Да, используйте File API, тогда вы можете обрабатывать изображения с помощью элемента canvas.
Это сообщение в блоге Mozilla Hacks проведет вас через большую часть процесса. Для справки вот собранный исходный код из поста блога:
// 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
Я решил эту проблему несколько лет назад и загрузил свое решение на github как https://github.com/rossturner/HTML5-ImageUploader
В ответе Роберта используется решение, предложенное в сообщении в блоге Mozilla Hacks, однако я обнаружил, что это приводит к очень низкому качеству изображения при изменении размера до 2:1 (или кратного). Я начал экспериментировать с различными алгоритмами изменения размера изображения, хотя большинство из них оказались довольно медленными или не очень хорошими по качеству.
Наконец, я пришел к решению, которое, на мой взгляд, выполняется быстро и имеет довольно хорошую производительность - поскольку решение Mozilla по копированию с одного холста на другой работает быстро и без потери качества изображения при соотношении 2: 1 с учетом целевого значения x пикселей в ширину и y пикселей в высоту, я использую этот метод изменения размера холста, пока изображение не станет между x и 2 x, и y и 2 y. Затем я перехожу к алгоритмическому изменению размера изображения для заключительного "шага" изменения размера до целевого размера. Попробовав несколько разных алгоритмов, я остановился на билинейной интерполяции, взятой из блога, который больше не находится в сети, но доступен через Интернет-архив, который дает хорошие результаты, вот соответствующий код:
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;
}
}
};
Это масштабирует изображение до ширины config.maxWidth
, сохраняя исходное соотношение сторон. На момент разработки это работало на iPad/iPhone Safari в дополнение к основным настольным браузерам (IE9+, Firefox, Chrome), поэтому я ожидаю, что он все еще будет совместим, учитывая более широкое распространение HTML5 сегодня. Обратите внимание, что вызов canvas.toDataURL() принимает тип MIME и качество изображения, что позволит вам контролировать качество и формат выходного файла (потенциально отличающийся от ввода, если хотите).
Единственный момент, который не распространяется на это, - сохранение информации об ориентации, без знания этих метаданных размер изображения изменяется и сохраняется как есть, теряя все метаданные внутри изображения для ориентации, что означает, что изображения, сделанные на планшетном устройстве "вверх ногами", были отображаются как таковые, хотя они были бы перевернуты в видоискателе камеры устройства. Если это вызывает озабоченность, в этом блоге есть хорошее руководство и примеры кода о том, как этого добиться, что, я уверен, может быть интегрировано в приведенный выше код.
Исправление к вышесказанному:
<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>
Модификация к ответу Джастина, которая работает для меня:
- Добавлен img.onload
- Разверните запрос POST на реальном примере
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);
}
Машинопись
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;
}
Если вы не хотите изобретать велосипед, вы можете попробовать ht tp://www.plupload.com/
Принятый ответ прекрасно работает, но логика изменения размера игнорирует случай, когда изображение больше максимума только по одной из осей (например, высота> maxHeight, но ширина <= maxWidth).
Я думаю, что следующий код заботится обо всех случаях более простым и функциональным способом (игнорируйте аннотации типов машинописного текста, если используете простой javascript):
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 };
}
fd.append("image", dataurl);
Это не будет работать. На стороне PHP вы не можете сохранить файл с этим.
Используйте этот код вместо:
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
Изменение размера изображения в элементе canvas обычно плохая идея, поскольку в нем используется самая дешевая интерполяция блоков. Полученное изображение заметно ухудшается по качеству. Я бы рекомендовал использовать http://nodeca.github.io/pica/demo/ который может вместо этого выполнять преобразование Ланцоша. Демонстрационная страница выше показывает разницу между подходами canvas и Lanczos.
Он также использует веб-работников для параллельного изменения размера изображений. Также есть реализация WEBGL.
Есть некоторые онлайн-ресайзеры изображений, которые используют pica для выполнения этой работы, например https://myimageresizer.com/
Вы можете использовать dropzone.js, если вы хотите использовать простой и удобный менеджер загрузки с изменением размера перед загрузкой функций.
Он имеет встроенные функции изменения размера, но вы можете предоставить свои собственные, если хотите.