1С:Предприятие 8.3.9 и выше
07.10.2016
Вы можете скачать приложенную обработку прямо сейчас
Необходимо просканировать каталог с файлами, выбрать файлы с расширением ".jpg" или ".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.Цветное; КонецЕсли; КонецЦикла; КонецОбработкиФайлов = ТекущаяДата(); Сообщить("Время обработки файлов " + Строка(КонецОбработкиФайлов - СтартОбработкиФайлов) + " сек"); КонецЕсли;
КонецПроцедуры
Вы можете скачать приложенную конфигурацию прямо сейчас
В данном примере мы создадим 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", который позволяет идентифицировать сообщения.
Добавим новый 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]); Прервать; КонецЦикла; Возврат Имя;
КонецФункции