1С:Предприятие 8.3.9 и выше
07.10.2016

Средства работы с двоичными данными

Пример: анализ файлов в формате JPEG

Вы можете скачать приложенную обработку прямо сейчас

Копировать

Постановка задачи

Необходимо просканировать каталог с файлами, выбрать файлы с расширением ".jpg" или ".jpeg" и для каждого такого файла собрать информацию об изображении. Если при анализе выяснится, что файл не соответствует формату JPEG, то такой файл следует пропустить.

Информация, которая нас интересует:

Краткое описание формата JPEG

Кратко опишем некоторые детали формата, существенные для решения нашей задачи. За более подробным описанием формата JPEG можно обратиться к соответствующим источникам:

Файл JPEG содержит последовательность маркеров, каждый из которых начинается с байта 0xFF, свидетельствующего о начале маркера, и байта-идентификатора. Некоторые маркеры состоят только из этой пары байтов, другие же содержат дополнительные данные, состоящие из двухбайтового поля с длиной информационной части маркера (включая длину этого поля, но за вычетом двух байтов начала маркера, то есть 0xFF и идентификатора) и собственно данных. Такая структура файла позволяет быстро отыскать маркер с необходимыми данными (например, с длиной строки, числом строк и числом цветовых компонентов сжатого изображения).

Решение

Создадим функцию, которая будет сканировать файл, определять его формат и выделять нужную нам информацию.

Копировать в буфер обмена
&НаКлиенте
Функция ПолучитьИнформациюИзФайлаJPEG(Файл)
// Сначала нам надо открыть файл для чтения Поток = ФайловыеПотоки.ОткрытьДляЧтения(Файл.ПолноеИмя); // Далее мы можем читать данные напрямую из потока, // но удобнее использовать объект ЧтениеДанных, т.к. он содержит // большое количество вспомогательных методов // Устанавливаем порядок байтов BigEndian, согласно стандарту Читатель = Новый ЧтениеДанных(Поток,, ПорядокБайтов.BigEndian); // Читаем данные в цикле пока не достигли конца потока // Если в процессе чтения будет найден подходящий фрагмент, // то вернем результат, не дочитывая весь файл до конца Пока Не Читатель.ЧтениеЗавершено Цикл Маркер = ПрочитатьМаркер(Читатель); Если Маркер = Неопределено Тогда // Если не удалось прочитать маркер, то мы либо достигли конца файла, // либо файл имеет некорректный формат // В любом случае, завершаем чтение Прервать; КонецЕсли; Если Маркер.ЭтоОписаниеИзображения Тогда // Секция информации об изображении // Точность данных (в битах на компонент). Нам этом поле не интересно ТочностьДанных = Читатель.ПрочитатьБайт(); // Высота изображения в пикселах ВысотаИзображения = Читатель.ПрочитатьЦелое16(); // Ширина изображения в пикселах ШиринаИзображения = Читатель.ПрочитатьЦелое16(); // Количество компонентов: // 1 = черно-белое изображение // 3 = цветное в схеме YCbCr // 4 = цветное в схеме CMYK КоличествоКомпонентов = Читатель.ПрочитатьБайт(); // Формируем результат ОписаниеJPEG = Новый Структура(); ОписаниеJPEG.Вставить("Имя", Файл.Имя); ОписаниеJPEG.Вставить("Ширина", ШиринаИзображения); ОписаниеJPEG.Вставить("Высота", ВысотаИзображения); ОписаниеJPEG.Вставить("Цветное", ?(КоличествоКомпонентов > 1, Истина, Ложь)); Возврат ОписаниеJPEG; Иначе // Какая-то другая секция, которая нам сейчас не интересна // Передвигаемся к концу секции Читатель.Пропустить(Маркер.РазмерСекции); КонецЕсли; КонецЦикла;
// Не смогли найти информацию об изображении Возврат Неопределено;
КонецФункции

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

Теперь, когда мы написали основную функцию для анализа JPEG-файла, напишем вспомогательную функцию - ПрочитатьМаркер, которая читает очередной маркер и возвращает информацию о нем:

Копировать в буфер обмена
&НаКлиенте
Функция ПрочитатьМаркер(Читатель)
Байт = Читатель.ПрочитатьБайт(); // Первый байт маркера всегда равен 255 Если Байт <> 255 Тогда Возврат Неопределено; КонецЕсли; Байт = Читатель.ПрочитатьБайт(); РазмерСекции = 0; // Про возможные значения маркеров можно прочитать в статье // в Википедии: https://ru.wikipedia.org/wiki/JPEG Если Байт = 216 Или Байт = 217 Или (Байт >= 208 И Байт <= 215) Тогда // Данный тип маркера не содержит данных РазмерСекции = 0; Иначе // Размер секции включает в себя два байта, задающие размер // Так как мы их уже прочитали, то возвращаем значение, уменьшенное на 2 РазмерСекции = Читатель.ПрочитатьЦелое16() - 2; КонецЕсли; ЭтоОписаниеИзображения = Ложь; Если Байт >= 192 И Байт <= 194 Тогда ЭтоОписаниеИзображения = Истина; КонецЕсли; Маркер = Новый Структура(); Маркер.Вставить("ЭтоОписаниеИзображения", этоОписаниеИзображения); Маркер.Вставить("РазмерСекции", размерСекции); Возврат Маркер;
КонецФункции

И, наконец, соберем всё вместе и напишем функцию, которая будет анализировать все файлы в заданной папке и выдавать полученную информацию в виде таблицы.

Копировать в буфер обмена
&НаКлиенте
Процедура ВыбратьИОбработатьДиректорию(Команда)
ДиалогОткрытияФайла = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.ВыборКаталога); Если ДиалогОткрытияФайла.Выбрать() Тогда СписокКартинок.Очистить(); СтартПоискаФайлов = ТекущаяДата(); Файлы = НайтиФайлы(ДиалогОткрытияФайла.Каталог, "*", Истина); КонецПоискаФайлов = ТекущаяДата(); Сообщить("Время поиска файлов " + Строка(КонецПоискаФайлов - СтартПоискаФайлов)); Сообщить("Всего файлов " + Файлы.Количество()); СтартОбработкиФайлов = ТекущаяДата(); Для каждого Файл из Файлы Цикл Если Файл.Расширение = ".jpg" ИЛИ Файл.Расширение = ".jpeg" Тогда ОписаниеJPEG = ПолучитьИнформациюИзФайлаJPEG(Файл); Если ОписаниеJPEG = Неопределено Тогда Продолжить; КонецЕсли; Строка = СписокКартинок.Добавить(); Строка.Имя = ОписаниеJPEG.Имя; Строка.Ширина = ОписаниеJPEG.Ширина; Строка.Высота = ОписаниеJPEG.Высота; Строка.Цветное = ОписаниеJPEG.Цветное; КонецЕсли; КонецЦикла; КонецОбработкиФайлов = ТекущаяДата(); Сообщить("Время обработки файлов " + Строка(КонецОбработкиФайлов - СтартОбработкиФайлов) + " сек"); КонецЕсли;
КонецПроцедуры

Пример: Работа с составными (multipart) HTTP-сообщениями

Вы можете скачать приложенную конфигурацию прямо сейчас

Постановка задачи

В данном примере мы создадим HTTP-сервис, который будет в ответ на запрос от клиента выдавать текстовое сообщение с вложенными картинками. Затем на клиенте мы отобразим полученный ответ.

Ответ от сервиса будет иметь следующий вид:

Копировать в буфер обмена
  HTTP/1.1 200 OK
Content-Length: 1559385
Content-Type: multipart/form-data; boundary=Asrf456BGe4h
Server: Microsoft-IIS/7.5
X-Powered-By: ASP.NET
Date: Fri, 25 Dec 2015 12:27:00 GMT
<Пустая строка>==Asrf456BGe4h
Content-Disposition: form-data; name=MessageText
<Пустая строка>
Уважаемый клиент!
Будем рады видеть Вас на нашей выставке.
Во вложении две схемы проезда
(на автомобиле и на городском транспорте)
.==Asrf456BGe4h
Content-Disposition: form-data; name=image1; filename=auto.jpg
Content-Type: image/jpeg
<Пустая строка>
<Двоичные данные первой картинки>==Asrf456BGe4h
Content-Disposition: form-data; name=image2; filename=metro.jpg
Content-Type: image/jpeg
<Пустая строка>
<Двоичные данные второй картинки>==Asrf456BGe4h==

В этом сообщении надо обратить внимание на тип содержимого (заголовок Content-Type) - "multipart/form-data". Первое слово "multipart" указывает на то, что HTTP-сообщение является составным, т.е. содержит внутри себя несколько вложенных сообщений. Второе слово - "form-data" - указывает на конкретный стандарт составных сообщений, который часто используется для кодирования почтовых сообщений.

В любых составных сообщениях в заголовке Content-Type обязательно должен присутствовать атрибут boundary, определяющий строку, которая отделяет друг от друга вложенные сообщения внутри составного сообщения.

В случае стандарта "multipart/form-data", каждое вложенное сообщение в свою очередь должно содержать заголовок Content-Disposition со значением "form-data" и атрибутом "name", который позволяет идентифицировать сообщения.

Создание cервиса для формирования составного сообщения

Добавим новый HTTP-сервис и назовем его "TestMultipart". Для простоты будем считать, что наш сервис будет возвращать заданное составное сообщение в ответ на любой GET-запрос. Поэтому добавляем Шаблон URL с именем "ДляВсех" и значением "/*". Т.е. данный шаблон соответствует любому запросу. Далее добавляем в шаблон HTTP-метод GET с именем "Get".

В качестве обработчика для метода создаем в модуле сервиса функцию ДляВсех_Get:

Копировать в буфер обмена
Функция ДляВсех_Get(Запрос)
        Ответ = Новый HTTPСервисОтвет(200);         
        Сообщение = СоздатьСообщение();
        Ответ.Заголовки = Сообщение.Заголовки;
        Ответ.УстановитьТелоИзДвоичныхДанных(Сообщение.Тело);        
        Возврат Ответ;
КонецФункции

Вся работа по созданию сообщения выполняется в функции СоздатьСообщение. Данная функция определяется следующим образом:

Копировать в буфер обмена
// Содание тестового multipart HTTP-сообщения
// Возвращается Структура {Заголовки, ДвоичныеДанные}

Функция СоздатьСообщение()
        // Создаем вложенные сообщения 
        // Каждое вложенное сообщение представлено экземпляром типа
        //   ДвоичныеДанные
        // Это удобно, т.к. в данном случае внутренняя структура этих   
        //  сообщений нам не интересна  
        Текст = СоздатьСообщение_Текст("MessageText", 

               "Уважаемый клиент!" + Символы.ПС + 

               " Будем рады видеть Вас на нашей выставке.." + Символы.ПС +

               " Во вложении две схемы проезда" + Символы.ПС +

               "(на автомобиле и на городском транспорте)");

        Автомобиль = СоздатьСообщение_Изображение("image1", "auto.jpg", БиблиотекаКартинок.Автомобиль);
        Транспорт = СоздатьСообщение_Изображение("image2", "metro.jpg", БиблиотекаКартинок.Транспорт);        

        // Формируем основное составное сообщение
        Разделитель = "Asrf456BGe4h";
        Результат = Новый Структура();
        Заголовки = Новый Соответствие();
        Результат.Вставить("Заголовки", Заголовки);
        Заголовки.Вставить("Content-Type", "multipart/form-data; boundary=" + разделитель);
        Тело = Новый ПотокВПамяти();
        ЗаписьДанных = Новый ЗаписьДанных(Тело);               
        ЗаписьДанных.ЗаписатьСтроку("==" + Разделитель);
        ЗаписьДанных.Записать(текст);        
        ЗаписьДанных.ЗаписатьСтроку("==" + Разделитель);
        ЗаписьДанных.Записать(Автомобиль);
        ЗаписьДанных.ЗаписатьСтроку("==" + Разделитель);
        ЗаписьДанных.Записать(Транспорт);
        ЗаписьДанных.ЗаписатьСтроку("==" + Разделитель + "==");
        ЗаписьДанных.Закрыть();
        ДанныеТела = Тело.ЗакрытьИПолучитьДвоичныеДанные();
        Результат.Вставить("Тело", ДанныеТела);
        Возврат Результат;
КонецФункции

У нас есть функция, формирующая составное сообщение и теперь нам осталось только определить две вспомогательные функции для создания вложенных сообщения:


Копировать в буфер обмена
// Возвращается HTTP-сообщение в виде ДвоичныеДанные

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


// Возвращается HTTP-сообщение в виде ДвоичныеДанные

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

Разбор составного сообщения на стороне клиента

Теперь посмотрим, как мы можем работать с составными сообщениями на стороне клиента. Нам необходимо распаковать вложенные сообщения и показать их содержимое.

Создаем новую общую форму. На форму добавляем реквизиты типа Строка:

Далее добавляем элементы управления:

Также создаем новую команду формы с именем ОтправитьЗапрос и привязываем команду к кнопке.

Создаем обработчик команды:

Копировать в буфер обмена
&НаКлиенте
Процедура ОтправитьЗапрос(Команда)
        ВыполнитьЗапрос();
КонецПроцедуры

Вся работа по запросу сервиса и отображению результата выполняется в серверной функции ВыполнитьЗапрос:

Копировать в буфер обмена
&НаСервере
Процедура ВыполнитьЗапрос()        
// Предполагаем, что сервис развернут на той же машине,
// где запущена конфигурация Соединение = Новый HTTPСоединение("localhost"); // Формируем запрос, отправляем на сервер и получаем ответ Запрос = Новый HTTPЗапрос("TestMultipart/hs/Service"); Ответ = Соединение.Получить(Запрос); // Разбираем ответ на составные части // Результат представляет собой структуру, содержащую // текст и картинки из составного HTTP-сообщения Результат = ПрочитатьСообщение(Ответ.Заголовки, Ответ.ПолучитьТелоКакДвоичныеДанные()); Сообщение = Результат.Сообщение; // Создаем объекты Картинка из двоичных данных // изображений, полученных с сервера Картинка1 = Новый Картинка(Результат.Картинка1); Картинка2 = Новый Картинка(Результат.Картинка2); // Для отображения картинок на форме нужно предварительно // поместить их во временное хранилище АдресКартинки1 = ПоместитьВоВременноеХранилище(Картинка1); АдресКартинки2 = ПоместитьВоВременноеХранилище(Картинка2); КонецПроцедуры

Функция ПрочитатьСообщение содержит самое интересное - разбор полученного от сервиса составного сообщения с использованием новых средств работы с двоичными данными:

Копировать в буфер обмена
/// Читаем составное HTTP-сообщение

&НаСервере
Функция ПрочитатьСообщение(заголовки, тело)
        Разделитель = ПолучитьРазделительСоставногоСообщения(заголовки);
        Маркеры = Новый Массив();
        Маркеры.Добавить("==" + Разделитель);
        Маркеры.Добавить("==" + Разделитель + Символы.ПС);
        Маркеры.Добавить("==" + Разделитель + Символы.ВК);
        Маркеры.Добавить("==" + Разделитель + Символы.ВК + Символы.ПС);
        Маркеры.Добавить("==" + Разделитель + "==");               
        Текст = Неопределено;
        Изображение1 = Неопределено;
        Изображение2 = Неопределено;      
        ЧтениеДанных = Новый ЧтениеДанных(Тело);               
        // Переходим к началу первой части
        ЧтениеДанных.ПропуститьДо(Маркеры);
        // Далее в цикле читаем все части
        Пока Истина Цикл
               Часть = чтениеДанных.ПрочитатьДо(Маркеры);
               Если Не Часть.МаркерНайден Тогда
                       // Неправильно сформированное сообщение
                       Прервать;
               КонецЕсли;
               ЧтениеЧасти = Новый ЧтениеДанных(Часть.ОткрытьПотокДляЧтения());
               ЗаголовкиЧасти = ПрочитатьЗаголовки(ЧтениеЧасти);
               ИмяЧасти = ПолучитьИмяСообщения(ЗаголовкиЧасти);
               Если ИмяЧасти = "MessageText" Тогда
                       Текст = чтениеЧасти.ПрочитатьСимволы();
               ИначеЕсли ИмяЧасти = "image1" Тогда
                       Изображение1 = ЧтениеЧасти.Прочитать().ПолучитьДвоичныеДанные();
               ИначеЕсли ИмяЧасти = "image2" Тогда
                       Изображение2 = ЧтениеЧасти.Прочитать().ПолучитьДвоичныеДанные();
               КонецЕсли;                            
               Если Часть.ИндексМаркера = 4 Тогда
                       // Прочитали последнюю часть
                       Прервать;
               КонецЕсли;
        КонецЦикла;

        Возврат Новый Структура("Сообщение,Картинка1,Картинка2", 
                                         Текст, 
                                         Изображение1, 
                                         Изображение2); 
КонецФункции

Осталось определить вспомогательные функции:

Копировать в буфер обмена
&НаСервере
Функция ПрочитатьЗаголовки(Чтение)
        Заголовки = Новый Соответствие();
        Пока Истина Цикл
               Стр = Чтение.ПрочитатьСтроку();
               Если Стр = "" Тогда
                       Прервать;
               КонецЕсли;
               Части = СтрРазделить(Стр, ":");
               ИмяЗаголовка = СокрЛП(Части[0]);
               Значение = СокрЛП(Части[1]);
               Заголовки.Вставить(ИмяЗаголовка, Значение);
        КонецЦикла;
        Возврат Заголовки;
КонецФункции

// Поиск строки-разделителя составного сообщения из заголовков
// Предполагается, что значение разделителя задается в заголовке
// Content-Type в следующем виде:
// Content-Type: multipart/form-data; boundary=<Разделитель>

&НаСервере
Функция ПолучитьРазделительСоставногоСообщения(Заголовки)
        ТипСодержимого = Заголовки.Получить("Content-Type");
        Свойства = СтрРазделить(ТипСодержимого, ";", Ложь);
        Граница = Неопределено;
        Для Каждого Свойство Из Свойства Цикл
               Части = СтрРазделить(Свойство, "=", Ложь);
               ИмяСвойства = СокрЛП(Части[0]);
               Если ИмяСвойства <> "boundary" Тогда
                       Продолжить;
               КонецЕсли;
               Граница = СокрЛП(Части[1]);    
               Прервать;
        КонецЦикла;
        Возврат Граница;
КонецФункции

// Имя сообщения получается из заголовка
// Content-Disposition
// Content-Disposition: form-data; name=<Имя сообщения>

&НаСервере
Функция ПолучитьИмяСообщения(Заголовки)
        Описание = Заголовки.Получить("Content-Disposition");
        Свойства = СтрРазделить(Описание, ";", Ложь);
        Имя = Неопределено;
        Для Каждого Свойство Из Свойства Цикл
               Части = СтрРазделить(Свойство, "=", Ложь);
               ИмяСвойства = СокрЛП(Части[0]);               
               Если ИмяСвойства <> "name" Тогда
                       Продолжить;
               КонецЕсли;       
               Имя = СокрЛП(Части[1]);       
               Прервать;
        КонецЦикла;
        Возврат Имя;
КонецФункции