Главная > JavaScript > JavaScript image resizer. Изменение размера изображения средствами JavaScript.

JavaScript image resizer. Изменение размера изображения средствами JavaScript.

С выходом HTML5 у разработчиков наконец-то появились более-менее удобные средства для работы с графикой, файлами и медиа-контентом. Но как всегда не обошлось без проблем — все эти новые плюшки очень сильно зависят от реализации технологии различными браузерами, а следовательно, то, что работает в Хроме, по известному закону не будет работать в ИЕ, причем даже несмотря на заявленную поддержку новой фичи. Это касается любого браузера, ИЕ — просто пример.

Сегодня расскажу про реализацию ресайзера картинок на клиенте средствами javascript, я не буду углубляться в подробности алгоритма обработки, а остановлюсь на задачах, которые предстояло решить, и на багах, с которыми пришлось столкнуться. Рабочий код скрипта в конце статьи. В дальнейшем, говоря «браузер», я подразумеваю браузер, который поддерживает необходимые фичи. Код тестирован на IE10, Chrome, Firefox, Safari 5, Android (4.0, 4.1), IOS6.

Рассмотрим вполне стандартную ситуацию, мы «нащелкали» на мобильный девайс кучу фотографий, которые нам необходимо загрузить на наш сайт. По каким-то причинам, мы не можем загружать файлы, размер которых, к примеру, больше 2Мбайт, мы также не хотим возиться с обработкой изображений в сторонней программе, которую еще нужно установить на наш гаджет, да и размер фотографий в 8 Мегапикселей для нас избыточен. Будем использовать HTML5.

Итак, в HTML5 появились такие фичи как FileReader и canvas, первый нам понадобится для «чтения» (загрузки) изображения, второй — для непосредственной обработки. План действий таков:

  1. прочитать изображение;
  2. обработать изображение;
  3. загрузить изображение на сервер;

Я опущу всякие проверки на поддержку фич браузерами, будем считать, что если вы собрались использовать этот скрипт, то все уже проверено. Также сразу оговорюсь, я не помню точно, в каком конкретном браузере была каждая конкретная проблема. Функции, которые нигде не были объявлены, но представленные в «выжимках» из скрипта, присутствуют в оригинальном скрипте.

Читаем изображение или «вляпываемся в первую пачку багов».

Рисуем HTML:

<div id="newUpload">
<input id="files" type="file" name="files" />
<div class="progress2" style="background: #B4F5B4; height: 20px; border-radius: 3px; width: 0;"></div>
</div>

Обработчик изменения инпута:

var reader = new FileReader();
reader.onloadend = function(e) {
	var dataUrl = e.target.result;	
	var image = new Image();
	image.onload = function() {
		var result = processImage(image);
		validateCallback(settings.complete).call(this, { image: image, canvas: result.canvas, data: result.data, blob: result.blob });		
	};
	
	image.src = dataUrl;	
};

reader.readAsDataURL(settings.file);

Работает в Chrome, FF, Safari, IPhone, не работает в Android (Dolphin) и IE10. Логично предположить, что проблема в «onload» объекта image, потому что объект не загружен в DOM. Переписываем:

var reader = new FileReader();
reader.onloadend = function(e) {
	var dataUrl = e.target.result;	
	var image = new Image();
	image.onload = function() {
		var result = processImage(image);
		validateCallback(settings.complete).call(this, { image: image, canvas: result.canvas, data: result.data, blob: result.blob });
		document.body.removeChild(image);
	};
	
	image.src = dataUrl;
	document.body.appendChild(image);                
};

reader.readAsDataURL(settings.file); 

Заработало в IE10, но по-прежнему не работает в Android, плюс мы не хотим показывать загруженное изображение. В чем же проблема в Андроиде ? Выясняется, что для него нужно делать так:

var reader = new FileReader();
reader.onloadend = function(e) {
	var dataUrl = e.target.result;	
	var image = new Image();
	image.onload = function() {
		var result = processImage(image);
		validateCallback(settings.complete).call(this, { image: image, canvas: result.canvas, data: result.data, blob: result.blob });
		document.body.removeChild(image);
	};
	
	image.src = dataUrl.replace('data:base64', 'data:image/jpeg;base64'); /* Android issue workaround. */	
	document.body.appendChild(image);                
};

reader.readAsDataURL(settings.file);

По каким-то причинам, в Android 4.1 вместо «data:image/jpeg;base64» записывается «data:base64».

Теперь, как не показывать прочитанное изображение, после загрузки его в DOM ?

display: none;

, но это ничего не решит. Мы можем задать ширину и высоту в один пиксель, но это отрицательно повлияет на процесс ресайзинга в будущем, будут потеряны пропорции изображения, мы можем абсолютно спозиционировать изображение где-нибудь в невидимой области экрана, но это — не наш метод. Что же, будем использовать IFrame.

var reader = new FileReader();
reader.onloadend = function(e) {
	var dataUrl = e.target.result;                
	var iframe = (function() {
		var iframeId = "tmpFrame";
		var tmpIframe = document.createElement("iframe");
		tmpIframe.setAttribute("id", iframeId);
		tmpIframe.setAttribute("name", iframeId);
		tmpIframe.setAttribute("width", "0");
		tmpIframe.setAttribute("height", "0");
		tmpIframe.setAttribute("border", "0");
		tmpIframe.setAttribute("style", "width: 0; height: 0; border: none;");

		document.body.appendChild(tmpIframe);
		window.frames[iframeId].name = iframeId;

		return tmpIframe;
	})();

	var image = new Image();
	image.onload = function() {
		var result = processImage(image, exif['Orientation']);                    
		document.body.removeChild(iframe); /* IE10 issue workaround. */
	};

	image.src = dataUrl.replace('data:base64', 'data:image/jpeg;base64'); /* Android issue workaround. */
	iframe.appendChild(image); /* IE10 issue workaround. */
};

reader.readAsDataURL(settings.file);

Картинка загружена и готова к обработке, не «светится» на экране, работает во всех браузерах, фуууффф, выдохнули, едем дальше.

Обрабатываем изображение или «Iphone, wtf ???».

Казалось бы, задача не очень сложная — используя canvas, необходимо уменьшить размер изображения (применить фильтры по желанию и тд.), примеров в сети полно, однако, как всегда есть одно «НО», теперь это — IPhone (IPad вероятно тоже, версия — IOS6). Что же подложил нам этот агрегат ? А подложил он крайне неприятную и непонятную багу, суть ее в том, что если вы загружаете изображение большее, чем 1 мегапиксель, то оно может быть «сломано», буквально это означает, что будет отрисована лишь часть, оставшаяся часть будет заполнена альфа-каналом, нам нужно проверить эту ситуацию:

function detectSubsampling(image) {
	var width = image.width;
	var height = image.height;
	if (width * height > 1024 * 1024) {
		var canvas = document.createElement('canvas');
		canvas.width = canvas.height = 1;
		var ctx = canvas.getContext('2d');
		ctx.drawImage(image, -width + 1, 0);
		return ctx.getImageData(0, 0, 1, 1).data[3] === 0;
	} else {
		return false;
	}
}

и если это наш случай, то ширина и высота получаемого изображения должна быть уменьшена вдвое:

if (detectSubsampling(image)) {
	currentWidth /= 2;
	currentHeight /= 2;
}

Полный алгоритм описан в оригинальном скрипте, идея и реализация позаимствованы отсюда. Едем дальше.

Загружаем изображение на сервер или «траблы с Firefox, Android и FormData».

После обработки изображения мы хотим загрузить его через ajax на сервер, планировалось использовать связку XMLHttpRequest и FormData, для этого полученное из canvas изображение должно было быть сконвертировано в blob:

var xhr = new XMLHttpRequest();
xhr.open(settings.type, settings.url, true);
xhr.upload.addEventListener("progress", uploadProgress, false);
xhr.addEventListener("load", success, false);
xhr.addEventListener("error", error, false);

var formData = new FormData();
formData.append(settings.formDataName, blob);

if (xhr.sendAsBinary) {
	xhr.sendAsBinary(formData);
} else {
	xhr.send(formData);
}

Но не тут-то было. Firefox на дату написания статьи криво отсылает blob через FormData, на сервер приходит пустой файл. Что же, будем слать как base64:

var xhr = new XMLHttpRequest();
xhr.open(settings.type, settings.url, true);
xhr.upload.addEventListener("progress", uploadProgress, false);
xhr.addEventListener("load", success, false);
xhr.addEventListener("error", error, false);

var data = settings.data.replace('data:' + settings.mimeType + ';base64,', ''); /* mime-тип выбранного файла */

if (XMLHttpRequest.prototype.sendAsBinary === undefined) {
	XMLHttpRequest.prototype.sendAsBinary = function(string) {
		var bytes = Array.prototype.map.call(string, function(c) { return c.charCodeAt(0) & 0xff; });
		this.send(new Uint8Array(bytes).buffer);
	};
}

var boundary = 'boundary';
xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
xhr.sendAsBinary([
	'--' + boundary,
	'Content-Disposition: form-data;'
		+ 'name="' + settings.formDataName + '"; '
		+ 'filename="' + settings.fileName + '" ',
	'Content-Type: multipart/form-data',
	'',
	atob(data),
	'--' + boundary + '--'
].join('\r\n') + '\r\n');

Почему этот код не работает в Android 4.1 ? — «Потому что гладиолус»©. По неведомой причине

сanvas.toDataURL("image/jpeg")

в Android 4.1 выдает строку с префиксом

"data:image/png;base64,"

Переписываем вновь:

var xhr = new XMLHttpRequest();
xhr.open(settings.type, settings.url, true);
xhr.upload.addEventListener("progress", uploadProgress, false);
xhr.addEventListener("load", success, false);
xhr.addEventListener("error", error, false);

var data = settings.data.replace('data:' + settings.mimeType + ';base64,', '');
data = data.replace('data:image/png;base64,', ''); /* Android issue workaround. */

if (XMLHttpRequest.prototype.sendAsBinary === undefined) {
	XMLHttpRequest.prototype.sendAsBinary = function(string) {
		var bytes = Array.prototype.map.call(string, function(c) { return c.charCodeAt(0) & 0xff; });
		this.send(new Uint8Array(bytes).buffer);
	};
}

var boundary = 'boundary';
xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
xhr.sendAsBinary([
	'--' + boundary,
	'Content-Disposition: form-data;'
		+ 'name="' + settings.formDataName + '"; '
		+ 'filename="' + settings.fileName + '" ',
	'Content-Type: multipart/form-data',
	'',
	atob(data),
	'--' + boundary + '--'
].join('\r\n') + '\r\n');

Теперь все работает. Полный пример использования:

HTML

<div id="newUpload">
    <input type="file" id="files" name="files" />
    <span class="percent2"></span>
    <div class="progress2" style="background:#B4F5B4;height:20px;border-radius:3px;width: 0"></div>
</div>

JavaScript

$("#files").on("change", function (event) {
	var t = event.target.files[0];
	prj1551.imageResizer.resize({
		file: t,
		maxWidth: 1600,
		maxHeight: 1200,
		complete: function (result) {
			$("#newUpload").append(result.canvas);
			prj1551.imageResizer.upload({
				url: '@Url.Action("Upload", "Upload")',
				data: result.data,
				mimeType: t.type,
				fileName: t.name,
				formDataName: 'files',
				progressIndicator: '.progress2',
				percentIndicator: '.percent2'
			});
		}
	});
});

Скачать скрипты.

Вроде ничего не забыл, писал по памяти, так как скрипт сделан уже давно.

Что такое Project1551 в архиве с исходниками ? — Пока это набор джаваскриптов, репозиторий на Bitbucket, но еще не оформлен как следует, обновление будет отдельным постом.

  1. Виталий
    15 июля 2016 в 10:41 | #1

    Здравствуйте. Я новичок в программировании. Вещь получилась отличная. Подскажите вывод сразу не нужен и определить бы место сохранения нового формата загруженной картинки. у меня в базе в отдельной папке будет храниться одна картинка к каждой записи, типа аватара с max длиной/шириной 300px. Здесь вроде тоже звучало что обновленная картинка пишется в базу. Подскажите что за база и где это момент записи. Пока научился пользоваться базами sql через php. Может направите где получить недостающие знания. Обучаюсь конкретно создавая сайт с нуля: html — css — php — javascript. На яве добиваюсь сайта в одну страничку: регистрации, авторизации, изменения карточки и далее. С уважением.

  2. sdg
    29 августа 2016 в 17:24 | #2

    И нафиг это по памяти нужно?
    Ничего не работает из примера.

  1. Пока что нет уведомлений.