Опубликован: 10.04.2013 | Уровень: для всех | Доступ: платный
Лекция 6:

Списки воспроизведения

< Лекция 5 || Лекция 6: 123456 || Лекция 7 >

Манипуляция изображениями и их кодирование

Для того, чтобы сделать с изображениями нечто большее, чем загрузка и отображение (где вы можете применить различные CSS-трансформации для реализации эффектов), вам нужно получить доступ к пикселям с помощью декодера (decoder). Это уже происходит внутри механизмов вывода изображения, когда вы присваиваете URI img.src, но для того, чтобы получить непосредственный доступ к пикселям, нужно выполнить ручное декодирование изображения. С другой стороны, сохранение пиксельной информации в виде файла требует использования кодера.

WinRT предоставляет API и для того, и для другого в пространстве имен Windows.Graphics.Imaging (http://msdn.microsoft.com/library/windows/apps/windows.graphics.imaging.aspx), а именно, в классах BitmapDecoder, BitmapTransform, и BitmapEncoder. Загрузка, выполнение манипуляций с изображением и сохранение его в файл часто включает в себя использование всех трех классов, хотя объект BitmapTransform предназначен для поворота и масштабирования, таким образом, вы не будете пользоваться им, если выполняете другие манипуляции.

Демонстрацию этого API можно найти в Сценарии 2 примера "Простая работа с изображениями" (http://code.msdn.microsoft.com/windowsapps/Simple-Imaging-Sample-a2dec2b0). Я оставляю вам этот код для того, чтобы вы просмотрели его в редакторе, так как он довольно сложен – до 11 promise-вызовов, объединенных в цепочку для сохранения файла! Так же он выполняет декодирование, манипуляцию и кодирование внутри одной функции, такой, как saveHandler (js/scenario2.js). Вот какую последовательность действий реализует этот код:

  • Открыть файл с помощью StorageFile.openAsync, получив поток.
  • Передать поток статическому методу BitmapDecoder.createAsync, что позволит получить конкретный экземпляр BitmapDecoder для потока.
  • Переать декодер статическому методу BitmapEncoder.createForTranscodingAsync, который предоставляет экземпляр BitmapEncoder. Этот кодировщик создается с помощью InMemoryRandomAccessStream.
  • Установить свойства в свойстве кодировщика bitmapTransform property (объект BitmapTransform) для настройки масштабирования и поворота. Это позволит создать трансформированное изображение в потоке в памяти.
  • Создать набор свойств (Windows.Graphics.Imaging.BitmapPropertySet), который включает в себя System.Photo.Orientation и использует команду кодера bitmapProperties.setPropertiesAsync для того, чтобы это сохранить.
  • Копирование потока из памяти в выходной файловых поток с использованием Windows.Storage.Stream.RandomAccessStream.copyAsync.
  • Закрытие обоих потоков с помощью соответствующих методов (тем самым закрывается файл).

В сложных сценариях, подобных этому, полезно рассмотреть отдельные стадии этого процесса, для этой цели в материалах к этой лекции есть упражнение ImageManipulation. Он позволит вам выбрать и загрузить изоражение, конвертировать его в оттенки серого и сохранить конвертированное изображение в новый файл. Результат работы показан на рис. 6.3. Кроме того, это дает вам возможность, как мы можем отправлять декодированное изображение в элемент HTML canvas и сохранять содержимое элемента в виде файла.

Результаты работы упражнения ImageManipulation из дополнительных материалов к лекции

Рис. 6.3. Результаты работы упражнения ImageManipulation из дополнительных материалов к лекции

Обработчик кнопки Load Image (Загрузить изображение) (loadImage в js/default.js) предоставляет первоначальное отображение. Он позволяет вам выбирать изображение с помощью средства вабра файлов, отображает полноразмерное изображение в элементе img с помощью URL.createObjectURL, вызывает StorageFile.properties.getImagePropertiesAsync для получения свойств title и dateTaken, и применяет StorageFile.getThumbnailAsync для получения эскиза, который отображается в верхней части. Мы уже видели все эти API в действии.

При щелчке на кнопке Grayscale (Оттенки серого), мы входим в обработчик setGrayscale, где происходят интересные действия. Мы вызываем StorageFile.openReadAsync для того, чтобы получить поток, вызываем BitmapDecoder.createAsync с этим потоком для того, чтобы получить декодер, кэшируем некоторые подробности из декодера в локальном объекте (encoding), вызываем BitmapDecoder.getPixelDataAsync и копируем эти пиксели в элемент canvas (и здесь лишь три асинхронных операции, объединенных в цепочку!):

 var Imaging = Windows.Graphics.Imaging; //Короткое имя	
var imageFile;            //Полученный из средства выбора файла	
var decoder;           //Полученный из BitmapDecoder.createAsync	
var encoding = {};        //Для кэширования некоторых деталей из декодера

function setGrayscale() {
//Декодируем файл изображения в пиксельные данные для canvas

//Получаем входной поток длф файла (объект StorageFile полученный после открытия)
imageFile.openReadAsync().then(function (stream) {	
//Создаем декодер, используя статический метод createAsync и поток файла
return Imaging.BitmapDecoder.createAsync(stream);	
}).then(function (decoderArg) {	
decoder = decoderArg;	

//Настраиваем декодер, если хотим. 
Параметры по умолчанию, это BitmapPixelFormat.rgba8 и
//BitmapAlphaMode.ignore. 
Параметризованная версия getPixelDataAsync может так же
//управлять трансформацией, а так же ExifOrientationMode, 
и ColorManagementMode если нужно.

//Кэшируем эти параметры для кодирования	
encoding.dpiX = decoder.dpiX;	
encoding.dpiY = decoder.dpiY;	
encoding.pixelFormat = decoder.bitmapPixelFormat;
encoding.alphaMode = decoder.bitmapAlphaMode;	
encoding.width = decoder.pixelWidth;	
encoding.height = decoder.pixelHeight;	

return decoder.getPixelDataAsync();
}).done(function (pixelProvider) {
//detachPixelData получает реальные пиксели (массив нельзя вернуть из 
//асинхронной операции)
copyGrayscaleToCanvas(pixelProvider.detachPixelData(), decoder.pixelWidth, decoder.pixelHeight);
});
}

Метод декодера getPixelDataAsync (http://msdn.microsoft.com/library/windows/apps/windows.graphics.imaging.bitmapdecoder.getpixeldataasync.aspx) существует в двух формах. Более простая форма, показанная здесь, выполняет декодирование с использованием настроек по умолчанию. Версия, которую можно полностью контролировать, позволяет вам задать другие параметры, как пояснено в комментариях к вышеприведенному коду. Обычное использование этого метода ведет к выполнению трансформации с использованием объекта Windows.Graphics.Imaging.BitmapTransform (как упомянуто выше), что включает в себя масштабирование (с различными моделями интерполяции), поворот (с приращением в 90 градусов), обрезку и отражение.

В любом случае, то, что вы получаете из getPixelDataAsync, это не массив пикселей из-за ограничений механизма языковой проекцииWinRT, где асинхронные операции не могут возвращать массивы. Вместо этого операция возвращает объект PixelDataProvider (http://msdn.microsoft.com/library/windows/apps/windows.graphics.imaging.pixeldataprovider.aspx), единственный восхитительный синхронный метод которого detachPixelData дает вам необходимый массив. (И этот метод можно вызвать лишь один раз, он не будет работать при последующих вызовах, отсюда и имя "detach" (отсоединить)). В итоге мы получаем данные, которые нам нужны для манипуляции пикселями и отображения результатов в элементе canvas, что и демонстрирует функция copyGrayscaleToCanvas. Вы можете, конечно, заменить эту функцию любой подпрограммой для манипуляции пикселями:

function copyGrayscaleToCanvas(pixels, width, height) {
//Настраиваем контекст canvas и получаем его пиксельный массив
var canvas = document.getElementById("canvas1");	
canvas.width = width;	
canvas.height = height;	
var ctx = canvas.getContext("2d");	

//Обходим данные и копируем значения пикселей 
в canvas после конвертации в оттенки серого
var imgData = ctx.createImageData(canvas.width, canvas.height);	
var colorOffset = { red: 0, green: 1, blue: 2, alpha: 3 };	
var r, g, b, gray;	
var data = imgData.data; //Очень влияет на производительность!	

for (var i = 0; i < pixels.length; i += 4) {
r = pixels[i + colorOffset.red];	
g = pixels[i + colorOffset.green];	
b = pixels[i + colorOffset.blue];	

//Присваиваем каждое rgb-значение яркости
gray = Math.floor(.3 * r + .55 * g + .11 * b);

data[i + colorOffset.red] = gray;	
data[i + colorOffset.green] = gray;	
data[i + colorOffset.blue] = gray;	
data[i + colorOffset.alpha] = pixels[i + colorOffset.alpha];
}	

//Показываем результат в элементе canvas ctx.putImageData(imgData, 0, 0);

//Активируем кнопку сохранения 
document.getElementById("btnSave").disabled = false;
}
 

Это – хороший случай убедиться в том, что JavaScript – это не лучшия язык для работы с большими группами пикселей вроде этой, хотя в данном случае производительность Release build (построения Выпуска), запущенного вне отладчика, весьма неплоха. Подобные подпрограммы лучше реализовывать в виде WinRT-компонентов на языках вроде C# или C++ и делать вызываемыми из JavaScript. У нас будет возможность сделать это в лекции 5 курса "Программная логика приложений для Windows 8, созданных с использованием HTML, CSS и JavaScript и их взаимодействие с системой", где мы так же увидим ограничения элемента canvas, которые требуют несколько иного подхода.

Сохранение данных из canvas в файл происходит в функции saveGrayscale, где мы используем средство выбора файла для получения StorageFile, открываем поток, берем пиксельные данные из элемента canvas и передаем их BitmapEncoder:

function saveGrayscale() {	
var picker = new Windows.Storage.Pickers.FileSavePicker();	
picker.suggestedStartLocation =	
Windows.Storage.Pickers.PickerLocationId.picturesLibrary;
picker.suggestedFileName = imageFile.name + " - grayscale";	
picker.fileTypeChoices.insert("PNG file", [".png"]);	

var imgData, fileStream = null;

picker.pickSaveFileAsync().then(function (file) {
if (file) {
return file.openAsync(Windows.Storage.FileAccessMode.readWrite);
} else {
return WinJS.Promise.wrapError("No file selected");
}
}).then(function (stream) {
fileStream = stream;
var canvas = document.getElementById("canvas1");
var ctx = canvas.getContext("2d");
imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

return Imaging.BitmapEncoder.createAsync( Imaging.BitmapEncoder.pngEncoderId, stream);
}).then(function (encoder) {
//Задаем пиксельные данные, подразумевая, что объект "encoding" 
имеет необходимые параметры.
//Конверсия из данных элемента canvas в Uint8Array необходима, 
так как тип массива из элемента canvas
//не соответствует тому, что нужно здесь WinRT.
encoder.setPixelData(encoding.pixelFormat, encoding.alphaMode,
encoding.width, encoding.height, encoding.dpiX, encoding.dpiY,
new Uint8Array(imgData.data));

//Начинаем кодирование
return encoder.flushAsync();
}).done(function () {
fileStream.close();
}, function () {
//Пустой обработчик ошибки (если пользователь закрыл средство выбора файлов, 
ничего не происходит)
});
}
 

Обратите внимание на то, как BitmapEncoder принимает идентификатор кодека в первом параметре. Мы используем pngEncoderId, который, как вы можете видеть, определен как статическое свойство класса Windows.Graphics.Imaging.BitmapEncoder; другие возможные значения – это bmpEncoderId, gifEncoderId, jpegEncoderId, jpegXREncoderId, и tiffEncoderId. Все это – форматы, поддерживаемые API. Вы можете установить дополнительные свойства BitmapEncoder прежде чем зададите пиксельные данные, такие, как его BitmapTransform, эти установки будут применены при кодировании.

Одна особенность, о которой нужно знать, заключается в том, что пиксельный массив, полученный из элемента canvas (тип DOM CanvasPixelArray (http://msdn.microsoft.com/library/windows/apps/hh465731.aspx) не совместима напрямую с байтовым массивом WinRT, который нужен кодировщику. По этой причине мы используем вызов new Uint8Array в последнем параметре.

Перекодировка и пользовательские форматы изображений

В предыдущем разделе мы, в основном, видели использование BitmapEncoder, созданного с помощью статического метода этого класса createAsync для записи нового файла. Все это хорошо, но вам может захотеться узнать о некоторых других возможностях кодировщиков.

Во-первых, это метод BitmapEncoder.createForTranscodingAsync (http://msdn.microsoft.com/library/windows/apps/windows.graphics.imaging.bitmapencoder.createfortranscodingasync.aspx), который был кратко упомянут в контексте примера "Простая обработка изображений". С его помощью создается новый кодировщик, который инициализируется на основе существующего объекта BitmapDecoder. Это, в основном, используется для манипуляции некоторыми аспектами исходного файла изображения, оставляя оставшиеся данные нетронутыми. Говоря точнее, вы можете сначала изменить эти аспекты, посредством метода кодировщика setPixelData: формат пикселей (rgba8, rgba16, и bgra8, смотрите BitmapPixelFormat (http://msdn.microsoft.com/library/windows/apps/windows.graphics.imaging.bitmappixelformat.aspx), параметры прозрачности (premultiplied, straight, или ignore, смотрите BitmapAlphaMode ( http://msdn.microsoft.com/library/windows/apps/windows.graphics.imaging.bitmapalphamode.aspx ) размер изображения, DPI изображения, и, конечно, сами пиксельные данные. Помимо этого, вы можете изменить другие свойства посредством метода кодировщика bitmapProperties.setPropertiesAsync. На самом деле, если все, что вам нужно – это изменить несколько свойств, и вы не собираетесь работать с данными пикселей, вы можете использовать вместо этого BitmapEncoder.createForInPlacePropertyEncodingAsync (http://msdn.microsoft.com/library/windows/apps/windows.graphics.imaging.bitmapencoder.createforinplacepropertyencodingasync.aspx) (ничего себе имя метода!). Кодировщик позволяет вызывать лишь bitmapProperties.setPropertiesAsync, bitmapProperties. getPropertiesAsync, и flushAsync, и так как это подразумевает неизменность исходных данных, это выполняется быстрее, чем при использовании его более гибкого эквивалента и требует меньше памяти.

Кодировщик из createForTranscodingAsync не приспособлен для изменения формата файла изображения (например, JPEG на PNG); для этого вам нужно использовать createAsync, где вы можете задать конкретный тип кодирования. Как мы уже видели, первый аргумент createAsync это идентификатор кодека, куда обычно передают одно из статических свойств из Windows.Graphics.Imaging.BitmapEncoder. Я еще не упоминал о том, что вы можете так же задавать пользовательские кодеки в этом первом параметре, и что createAsync может так же поддерживать необязательный третий аргумент, в котором можно передать параметры для конкретного кодека. Однако здесь имеются некоторые сложности и ограничения.

Позвольте мне сначала остановиться на параметрах. Существующая документация по значениям кодеков BitmapEncoder (наподобие pngEncoderId) не содержит многих подробностей о доступных параметрах. Для того, чтобы их узнать, вам нужно обратиться к документации по Windows Imaging Component (WIC), в частности, к материалу Native WIC Codecs (http://msdn.microsoft.com/library/windows/desktop/gg430027.aspx), который посвящен тому, что WinRT предоставляет приложениям для Магазина Windows. Если вы пройдете на страницу конкретного кодека, вы увидите раздел "Encoder Options" (Свойства кодера), который содержит описания того, что можно использовать. Например, кодек JPEG (http://msdn.microsoft.com/library/windows/desktop/gg430026.aspx) поддерживает свойства наподобие ImageQuality (качество изображения, значение в диапазоне от 0.0 до 1.0), а так же встроенную функцию поворота. Кодек PNG (http://msdn.microsoft.com/library/windows/desktop/gg430028.aspx ) поддерживает свойства наподобие FilterOption для различных оптимизаций сжатия файлов.

Для того, чтобы задать эти свойства, вам нужно создать новый BitmapPropertySet и вставить в этот набор запись для каждого необходимого параметра. Если, например, у вас есть переменная, содержащая данные о качестве изображения, которое вы хотите использовать при кодировании JPEG, вы можете создать кодер, таким образом:

var options = new Windows.Graphics.Imaging.BitmapPropertySet();	
options.insert("ImageQuality", quality);	
var encoderPromise = Imaging.BitmapEncoder.createAsync(Imaging.BitmapEncoder.jpegEncoderId,
stream, options);	
 

Вы используете тот же самый BitmapPropertySet для любых свойств, которые вы можете передать при вызове метода кодера bitmapProperties.setPropertiesAsync. Здесь мы используем тот же механизм для свойств кодировщика.

Что касается пользовательских кодеков, это просто означает, что первый аргумент BitmapEncoder.createAsync (как и BitmapDecoder.createAsync) это GUID (идентификатор класса или CLSID) для этого кодека, реализация которого должна быть выполнена в виде DLL. Подробности о том, как писать подобные кодеки, можно найти в материале "Как написать WIC-кодек" (http://msdn.microsoft.com/library/windows/desktop/ee719883.aspx). Загвоздка здесь в том, что включение пользовательских кодеков для изображений в пакет приложения не поддерживается. Если кодек уже присутствует в системе (то есть, установлен традиционным образом), это будет работать. Однако, политика Магазина Windows не позволяет одним приложениям зависеть от других, так что маловероятно, что вы сможете хотя бы отправить подобное приложение, если только оно не предустановлено на каком-то конкретном OEM-устройстве и нужная DLL не является частью образа системы (Приложения, написанные на C++ могут большее в да нной области, но это выходит за границы данного курса).Короче говоря, в случае с приложениями, написанными на JavaScript и HTML, вы, в работе с различными форматами изображений, по-настоящему ограничены применением кодеков, которые изначально поддерживаются системой.

Заметьте, что эти ограничения не применяются для пользовательских аудио- и видеокодеков. Пример "Расширения мультимедиа" (http://code.msdn.microsoft.com/windowsapps/Media-extensions-sample-7b466096) показывает, как работать с пользовательским видеокодеком, что мы увидим в следующем разделе.

< Лекция 5 || Лекция 6: 123456 || Лекция 7 >
Дмитрий Мельник
Дмитрий Мельник
Беларусь
Сергей Ширяев
Сергей Ширяев
Россия, г. Москва