gifts2017

Работа с двоичными данными на примере чтения файлов изображений. Новые возможности 8.3.9

Опубликовал Антон Локтионов (Anton64) в раздел Программирование - Практика программирования

В статье приводятся новые функции по работе с двоичными данными, появившимися в версии платформы 8.3.9 , на примере анализа формата и размера изображений. А также пример отправки изображения через API ВКонтакте с помощью новых объектов (без использования ОбъединитьФайлы())

В данной статье рассмотрим применение новых методов платформы 8.3.9  по работе с двоичными данными. Сначала немного теории.

Как было раньше

Ранее 1С предоставляла для работы с двоичными данными одноименный тип и некоторые методы работы с файлами. Думаю, многим известна ситуация, когда для отправки файла в формате multipart/form-data использовался метод «ОбъединитьФайлы». Это было связано  с отсутствием методов по работе  с внутренним содержимым двоичных данных.

Что нового

В версии 8.3.9 эта ситуация меняется. Разработчики платформы предоставили в наше распоряжение несколько новых типов и методов. Основным из них является обобщенный объект Поток. Потоков бывает три типа: Поток, ФайловыйПоток и ПотокВПамяти.

Основным преимуществом потоков является их способность работать с данными произвольного объема. Но в то же время они предоставляют ограниченные возможности. 

Для получения расширенных возможностей необходимо на основе потока создать объект ЧтениеДанных. С помощью него можно читать отдельные байты, символы, числа.

Побайтовые операции

Для анализа работы новых методов возьмем  за пример анализ параметров изображений для загрузки через API ВКонтакте.
Требования для загрузки фото в товар группы:

Допустимые форматы: JPG, PNG, GIF. 
Ограничения: минимальный размер фото — 200x200px, сумма высоты и ширины не более 14000px, файл объемом не более 50 МБ.
 
Нас будет интересовать формат файла, его разрешение и вес.

Вес файла

Сначала на основе выбранного файла создадим ФайловыйПоток и сможем сразу получить Размер файла в байтах:

ПотокИсходный = ФайловыеПотоки.ОткрытьДляЧтения(Объект.ИмяФайла);

///1. Размер файла
Объект.РазмерФайла = Строка(ПотокИсходный.Размер() / 1024) + " Кб";

Формат файла

 Каждый формат файла отличается друг от друга внутренней структурой. Отличительными особенностями каждого формата будем называть сигнатурами.
Зададим сигнатуры для каждого формата:

///Зададим сигнатуры нужных форматов 
МассивДопустимыхФорматов = Новый Соответствие;
МассивДопустимыхФорматов.Вставить("PNG", 	РазложитьСтрокуВМассивПодстрок("137,80,78,71,13,10,26,10"));
МассивДопустимыхФорматов.Вставить("JPEG", 	РазложитьСтрокуВМассивПодстрок("255,216"));
МассивДопустимыхФорматов.Вставить("GIF1", 	РазложитьСтрокуВМассивПодстрок("71,73,70,56,57,97"));
МассивДопустимыхФорматов.Вставить("BMP", 	РазложитьСтрокуВМассивПодстрок("66,77"));
МассивДопустимыхФорматов.Вставить("TIF", 	РазложитьСтрокуВМассивПодстрок("73,73,42,0"));
МассивДопустимыхФорматов.Вставить("TIF1", 	РазложитьСтрокуВМассивПодстрок("77,77,0,42"));

Каким образом получены сигнатуры?

В hex-записи сигнатура PNG файла выглядит так и состоит из 8 байт:
89 50 4E 47 0D 0A 1A 0A

Это можно проверить, открыв любой PNG файл в HEX-редакторе. Например, возьмем иконку месседжера Telegram размером 32*32.

Переведем эти значения из 16-ной системы в 10-ную и получим значения:
"137,80,78,71,13,10,26,10"

Аналогично поступаем с остальными форматами.

Проверяем формат файла

Создаем объект ЧтениеДанных на основе потока:
ЧтениеДанных = Новый ЧтениеДанных(ПотокИсходный, КодировкаТекста.ANSI, ПорядокБайтов.BigEndian);
ИскомыйТипФайла = ПроверитьСоответствиеТипаФайла(ЧтениеДанных, МассивДопустимыхФорматов);
	
	Если ИскомыйТипФайла = Неопределено Тогда
		Сообщить("Не поддерживаемый тип файла");
		Возврат;
	КонецЕсли;

&НаСервере
Функция ПроверитьСоответствиеТипаФайла(ЧтениеДанных, МассивДопустимыхФорматов)
	
	МаксРазмер = 0;
	Для Каждого ТипФайла Из МассивДопустимыхФорматов Цикл
		МаксРазмер = Макс(МаксРазмер, ТипФайла.Значение.Количество());		
	КонецЦикла; 
	
	//прочитаем в новый буфер максимальный размер сигнатуры
	БуферПроверка = ЧтениеДанных.ПрочитатьВБуферДвоичныхДанных(МаксРазмер);
	
	Для Каждого ТипФайла Из МассивДопустимыхФорматов Цикл
		//Хотелось бы конечно напрямую сравнить Буфер = Буфер, но так не работает
		//Приходится сравнивать каждый байт
		
		ДвоичнаяСигнатураФормата = ПолучитьСигнатуруФорматаВДвоичномВиде(ТипФайла.Значение);
		БуферПроверкаТипаФайла = БуферПроверка.Прочитать(0, ДвоичнаяСигнатураФормата.Размер);
		
		Сч = 0;
		Равны = Истина;
		Для Каждого Счет Из БуферПроверкаТипаФайла Цикл
			Если Счет <> ДвоичнаяСигнатураФормата[Сч] Тогда
				Равны = Ложь;	
				Прервать;
			КонецЕсли; 
		    Сч = Сч + 1; 
		КонецЦикла; 
		Если Равны Тогда
			Возврат ТипФайла.Ключ;
		КонецЕсли; 
	КонецЦикла; 

	Возврат НЕопределено;

КонецФункции

Т.к. буферы нельзя сравнить между собой, пришлось сравнивать байт за байтом. Если кто знает, как сделать красивее, пишите в комментариях.

Ищем размеры изображения

В каждом формате файла размеры изображения хранятся в разных местах. Например, в PNG они находятся после маркера «IHDR». Зная это, с помощью новых методов, мы можем переместить указатель на нужную позицию :
ЧтениеДанных.ПропуститьДо("IHDR", КодировкаТекста.ANSI);

Так же из спецификации формата PNG известно, что ширина и высота изображения занимают по 4 байта. Сначала прочитаем все 8 байтов, а потом отдельно ширину и высоту:

БуферЗаголовок 				= ЧтениеДанных.ПрочитатьВБуферДвоичныхДанных(8);
ОБъект.ШиринаИзображения 	= БуферЗаголовок.Прочитать(0, 4).ПрочитатьЦелое32(0, ПорядокБайтов.BigEndian);
Объект.ВысотаИзображения 	= БуферЗаголовок.Прочитать(4, 4).ПрочитатьЦелое32(0, ПорядокБайтов.BigEndian);

В методе ПрочитатьВБуферДвоичныхДанных() мы указываем общее количество байтов, которое хотим прочитать в буфер двоичных данных. Далее этот буфер читаем методом Прочитать(0, 4) – где 0 это позиция, а 4 – количество байтов для чтения.

В случае с JPEG поступаем аналогично, идем до нужного маркера и читаем числа размеров изображения:
Маркер = Новый БуферДвоичныхДанных(2);
Маркер.Установить(1, 192);  //192

ЧтениеДанных.ПропуститьДо(Маркер);
БуферРазмеры 				= ЧтениеДанных.ПрочитатьВБуферДвоичныхДанных(9);
Объект.ВысотаИзображения 	= БуферРазмеры.Прочитать(3,2).ПрочитатьЦелое16(0, ПорядокБайтов.BigEndian);
Объект.ШиринаИзображения 	= БуферРазмеры.Прочитать(5,2).ПрочитатьЦелое16(0, ПорядокБайтов.BigEndian);

С GIF ситуация немного другая. Т.к. мы использовали  исходный объект ЧтениеДанных для проверки соответствия форматам изображений, то указатель в этом экземпляре объекта переместился на какое-то количество позиций. 

Если мы продолжим читать этот экземпляр, то не сможем найти размеры изображения. Поэтому используем метод потока Перейти() и создаем новое ЧтениеДанных на основе нашего исходного потока. Дальше как обычно читаем размеры.

Как выглядит внутренняя структура файла GIF:
W и H - это ширина и высота. Чтобы до них добраться читаем в буфер 10 байт и получаем ширину и высоту с позиций 6 и 8, прочитав в каждом случае по 2 байта.
ТекПозиция 		= ПотокИсходный.ТекущаяПозиция();
ПотокИсходный.Перейти(-ТекПозиция, ПозицияВПотоке.Текущая);
ЧтениеДанныхGIF = Новый ЧтениеДанных(ПотокИсходный, КодировкаТекста.ANSI, ПорядокБайтов.LittleEndian);
БуферРазмеры 				= ЧтениеДанныхGIF.ПрочитатьВБуферДвоичныхДанных(10);
Объект.ШиринаИзображения 	= БуферРазмеры.Прочитать(6,2).ПрочитатьЦелое16(0, ПорядокБайтов.LittleEndian);
Объект.ВысотаИзображения 	= БуферРазмеры.Прочитать(8,2).ПрочитатьЦелое16(0, ПорядокБайтов.LittleEndian);

 Таким образом, зная спецификации нужных форматов, мы проверяем формат файла, размеры изображения.

Отправка изображения через API ВКонтакте

Привожу пример работающего кода  отправки сообщения multipart/form-data (проверено на отправке фото товара в группу ВКонтакте) (в обработке не приведен этот код):

//здесь получается объект с типом Картинка
Изображение = Номенклатура.ОсновноеИзображение.Хранилище.Получить();
//в функцию передаем двоичные данные картинки стандартным методом ПолучитьДвоичныеДанные()
ДвоичныеДанныеТело = СобратьИзображениеИзДвоичныхДанных(Изображение.ПолучитьДвоичныеДанные(), Boundary, ИмяФайлаДляЗагрузки);


Функция СобратьИзображениеИзДвоичныхДанных(ДвоичныеДанныеИзображения, Boundary, ИмяФайлаДляЗагрузки)
	
	ПотокТело = Новый ПотокВПамяти();
	ЗаписьДанных = Новый ЗаписьДанных(ПотокТело);
	
	ЗаписьДанных.ЗаписатьСтроку("--" + Boundary);
	ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""" + ИмяФайлаДляЗагрузки + """");	
	
	ЗаписьДанных.ЗаписатьСтроку("Content-Type: image/jpeg");
	ЗаписьДанных.ЗаписатьСтроку("");

	ЗаписьДанных.Записать(ДвоичныеДанныеИзображения);		
	//Завершение раздела двоичных данных
	ЗаписьДанных.ЗаписатьСтроку("--" + Boundary);
	
	//Завершение сообщения для сервера
	ЗаписьДанных.ЗаписатьСтроку("--" + Boundary + "--");	
	
	ЗаписьДанных.Закрыть();
	
	ДвоичныеДанныеТело = ПотокТело.ЗакрытьИПолучитьДвоичныеДанные();
	
	Возврат ДвоичныеДанныеТело;
	
	
КонецФункции      

Скачать файлы

Наименование Файл Версия Размер
Пример работы с двоичными данными 4
.epf 8,25Kb
11.11.16
4
.epf 8,25Kb Скачать

См. также

Подписаться Добавить вознаграждение
Комментарии
1. Константин Гейнрих (CyberCerber) 14.11.16 09:27
Добрый день

Спасибо за информацию, записывать файл в multipart/form-data стало теперь намного красивее.

Вот только:

Объект.РазмерФайла = Строка(ПотокИсходный.Размер() / 1000) + " Кб";

Почему делите на 1000, а не 1024?
корум; frkbvfnjh; aximo; +3 Ответить 1
2. Антон Локтионов (Anton64) 14.11.16 10:33
(1) CyberCerber, Спасибо, ошибка закралась. Исправил
3. Armando Armando (Armando) 14.11.16 11:14
Подсчет количества страниц TIFF
&НаКлиенте
Процедура Команда1(Команда)
	
	ПапкаСФайлами = "";
	МассивФайлов = НайтиФайлы(ПапкаСФайлами, "*.tif*");
	Для Каждого Файл Из МассивФайлов Цикл
		ФайловыйПоток = ФайловыеПотоки.ОткрытьДляЧтения(Файл.ПолноеИмя);
		Буфер = Новый БуферДвоичныхДанных( 4 );
		ФайловыйПоток.Прочитать(Буфер, 0, Буфер.Размер);
		Если Буфер[0] = 73 И Буфер[1] = 73 И Буфер[2] = 42 И Буфер[3] = 0 Тогда
			ПорядокБ = ПорядокБайтов.LittleEndian;
		ИначеЕсли Буфер[0] = 77 И Буфер[1] = 77 И Буфер[2] = 0 И Буфер[3] = 42 Тогда
			ПорядокБ = ПорядокБайтов.BigEndian;
		Иначе
			Сообщить(Файл.ПолноеИмя + ": формат не поддерживается");
			Продолжить;
		КонецЕсли;
		ФайловыйПоток.Перейти(4, ПозицияВПотоке.Начало);
		Буфер = Новый БуферДвоичныхДанных( 8 );
		ФайловыйПоток.Прочитать(Буфер, 0, Буфер.Размер);
		СмещениеПервогоIFD = Буфер.ПрочитатьЦелое32(0, ПорядокБ);
		
		Количество = Команда1Фрагмент(СмещениеПервогоIFD, ФайловыйПоток, 0, ПорядокБ);
		Сообщить(Файл.ПолноеИмя + ": " + Количество);
		
		ФайловыйПоток.Закрыть();
	КонецЦикла;
	
КонецПроцедуры

&НаКлиенте
Функция Команда1Фрагмент(СмещениеСледущего, Знач ФайловыйПоток, Счетчик, ПорядокБ)
	
	Перем Буфер, КоличествоТэгов, СмещениеСледущегоСмещения;
	
	Пока СмещениеСледущего > 0 Цикл
		Если Счетчик = 999 Тогда
			Прервать; // что-то не так
		КонецЕсли;
		Счетчик = Счетчик + 1;
		ФайловыйПоток.Перейти(СмещениеСледущего, ПозицияВПотоке.Начало);
		Буфер = Новый БуферДвоичныхДанных( 8 );
		ФайловыйПоток.Прочитать(Буфер, 0, Буфер.Размер);
		КоличествоТэгов = Буфер.ПрочитатьЦелое16(0, ПорядокБ);
		СмещениеСледущегоСмещения = СмещениеСледущего + 2 + (КоличествоТэгов * 12);
		ФайловыйПоток.Перейти(СмещениеСледущегоСмещения, ПозицияВПотоке.Начало);
		Буфер = Новый БуферДвоичныхДанных( 8 );
		ФайловыйПоток.Прочитать(Буфер, 0, Буфер.Размер);
		СмещениеСледущего = Буфер.ПрочитатьЦелое32(0, ПорядокБ);
		Команда1Фрагмент(СмещениеСледущего, ФайловыйПоток, Счетчик, ПорядокБ);
	КонецЦикла;
	
	Возврат Счетчик;
	
КонецФункции
...Показать Скрыть
swiss-garant; VasilVtoroy; Anton64; DrAku1a; +4 Ответить
4. Евгений Кредько (kredko) 15.11.16 04:32
Переведем эти значения из 16-ной системы в двоичную и получим значения:
"137,80,78,71,13,10,26,10"

Может всё же в десятиричную систему счисления?
5. Антон Локтионов (Anton64) 15.11.16 07:23
(4) kredko, да, всё верно, спасибо
6. Serj (Serj1C) 16.11.16 08:06
Как же мне не хватало работы с двоичными данными в 2010 году...
Но BMP читать и писать уже тогда получалось средствами платформы) http://infostart.ru/public/77713/
7. Олег Веселов (sml) 17.11.16 09:44
Плюсанул за подробное описание форматов файлов
8. Максим Шадрин (mixperm) 18.11.16 19:11
Отправка картинки в ВК не удалась. Буду очень благодарен если сообщите кусок кода где на upload_url отправляется эта картинка
9. Антон Локтионов (Anton64) 19.11.16 17:57
(8) mixperm,
Код
10. Яков Коган (Yashazz) 21.11.16 17:10
Ай-ай, такие прогрессивные вещи описываете, а всё РазложитьСтрокуВМассивПодстрок вместо СтрРазделить пользуете)))

Если честно, не особенно понимаю прикладную пользу этих новшеств. Где это реально сильно и позарез надо?
11. Дмитрий Шпаковский (Godman) 23.11.16 16:29
Ну и зачем это надо? Теперь вирусы писать станет проще - прямо средствами нативного языка. Как щас вижу баннер от Касперского: "Надежная защита от 1С. Доступно и всерьёз!"
Для написания сообщения необходимо авторизоваться
Прикрепить файл
Дополнительные параметры ответа