Top.Mail.Ru
26 февраля в 17:00 МСК
онлайн-митап
Как мы перенесли игру «Герои меча и магии 3» на 1С

Установка штампа в файл PDF средствами платформы 1С без применения ImageMagick

На проекте столкнулся с PDF файлами, объем которых в десятки страниц. Их конвертация доходила до получаса. После оптимизации, которая описана в данной статье, обработка файлов ускорились до 2 минут. Так появилась идея статьи. Рассмотрим редактирование файла PDF: вставим в PDF картинку со штампом. И немного помучаем ImageMagick. Будет полезно для программистов любого уровня.

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

Обычно вставка картинки в PDF — это установка штампа, который чаще всего устанавливается снизу документа. Если мы не знаем размеры файла PDF, а платформа нам не дает такой информации, то штамп можно разместить неверно.

Сейчас приходится пользоваться сторонними приложениями, чтобы это выяснить размер файла PDF. Рассмотрим ImageMagick — программа для конвертации файлов (опенсорс), как самое распространенное решение совмещенное с 1С. Практически все у кого есть 1С: Документооборот (далее ДО), плюс ЭЦП — пользуются этой программой.

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

Пример функции установки картинки штампа в PDF файл:

// Установка штампа в PDF файл средствами платформы.
//
// Параметры:
//   Штамп - определяемый тип ПрисоединенныйФайл, Двоичные данные, Строка - данные штампа, должна быть картинка.
//   ДанныеPDF - определяемый тип ПрисоединенныйФайл, Двоичные данные, Строка - данные файла PDF куда вставляем штамп.
//
// Возвращаемое значение:
//   Строка - Адрес хранилища на двоичные данные итогового PDF
//
Функция УстановкаШтампаБезКонвертированияPDF(Штамп, ДанныеPDF) Экспорт

	Если ОбъектВСпискеТиповПрисоединенныеФайлы(Штамп) Тогда
		ДанныеШтампа = РаботаСФайлами.ДвоичныеДанныеФайла(Штамп, Ложь);
	ИначеЕсли ТипЗнч(Штамп) = Тип("ДвоичныеДанные") Тогда
		ДанныеШтампа = Штамп;
	Иначе
		ДанныеШтампа = ПолучитьИзВременногоХранилища(Штамп); 
	КонецЕсли;
	
	Если ОбъектВСпискеТиповПрисоединенныеФайлы(ДанныеPDF) Тогда
		ДанныеФайлаPdf = РаботаСФайлами.ДвоичныеДанныеФайла(ДанныеPDF, Ложь);
	ИначеЕсли ТипЗнч(ДанныеPDF) = Тип("ДвоичныеДанные") Тогда
		ДанныеФайлаPdf = ДанныеPDF;
	Иначе
		ДанныеФайлаPdf = ПолучитьИзВременногоХранилища(ДанныеPDF); 
	КонецЕсли;
	
	Если ЗначениеЗаполнено(ДанныеШтампа) И ЗначениеЗаполнено(ДанныеФайлаPdf) Тогда
		
		ШтампКартинка = Новый Картинка(ДанныеШтампа, Истина);
		ДокументPDF = Новый ДокументPDF;
		
		ДокументPDF.Прочитать(ДанныеФайлаPdf.ОткрытьПотокДляЧтения());
			
		РасположениеШтампаЭПВPdf = ПолучитьРасположениеШтампаЭПВPdf();
		ОриентацияЛандшафт = ОриентацияPDFфайла(ДанныеФайлаPdf) = ОриентацияСтраницы.Ландшафт;
		
		Если РасположениеШтампаЭПВPdf = Перечисления.pdf_РасположениеШтампаЭПВPdf.ЛевыйВерхний Тогда
			ОтступX = "0";
			ОтступY = "0";
		ИначеЕсли РасположениеШтампаЭПВPdf = Перечисления.pdf_РасположениеШтампаЭПВPdf.ЛевыйНижний Тогда
			ОтступX = "0";
			ОтступY = ?(ОриентацияЛандшафт = Истина, "170", "260");
		ИначеЕсли РасположениеШтампаЭПВPdf = Перечисления.pdf_РасположениеШтампаЭПВPdf.ПравыйВерхний Тогда
			ОтступX = ?(ОриентацияЛандшафт = Истина, "210", "140");
			ОтступY = "0";
		ИначеЕсли РасположениеШтампаЭПВPdf = Перечисления.pdf_РасположениеШтампаЭПВPdf.ПравыйНижний Тогда
			ОтступX = ?(ОриентацияЛандшафт = Истина, "210", "140");
			ОтступY = ?(ОриентацияЛандшафт = Истина, "170", "260");
		КонецЕсли;
		
		ОписаниеШтампа = Новый ОписаниеОтображаемогоОбъектаPDF;
		ОписаниеШтампа.Лево = ОтступX;
		ОписаниеШтампа.Верх = ОтступY;
		ОписаниеШтампа.Объект = ШтампКартинка;
		
		ДокументPDF.ДобавитьОтображаемыйОбъект(ОписаниеШтампа);
		
		ПотокЗаписи = Новый ПотокВПамяти;
		ДокументPDF.Записать(ПотокЗаписи);
		
		Возврат ПоместитьВоВременноеХранилище(ПотокЗаписи.ЗакрытьИПолучитьДвоичныеДанные());
		
	КонецЕсли;
	
КонецФункции

Было добавлено перечисление расположения штампа pdf_РасположениеШтампаЭПВPdf.

Рассмотрим и функцию Ориентация PDF-файла, в ней мы и будем работать с ImageMagick.

// Вычисляет ориентацию файла PDF через ImageMagick.
//
// Параметры:
//   Адрес - Двоичные данные, Строка - либо двоичные данные файла, либо адрес хранилища с двоичными данными.
//
// Возвращаемое значение:
//   ОриентацияСтраницы - портрет или ландшафт
//
Функция ОриентацияPDFфайла(Адрес) Экспорт

	ФайлНаДиске = ПолучитьИмяВременногоФайла("pdf");
	Если ТипЗнч(Адрес) = Тип("ДвоичныеДанные") Тогда
		ДанныеФайла = Адрес; 
	Иначе
		ДанныеФайла = ПолучитьИзВременногоХранилища(Адрес); 
	КонецЕсли;
	ДанныеФайла.Записать(ФайлНаДиске); 

	Текст = Новый ТекстовыйДокумент;
	ИнфаНаДиске = ПолучитьИмяВременногоФайла("txt"); 
	Текст.Записать(ИнфаНаДиске);

	ПараметрыImageMagick = СтрШаблон("magick %1 -verbose -format '[%%[printsize.x]]' info:%2", ФайлНаДиске, ИнфаНаДиске);
	ЗапуститьImageMagick(ПараметрыImageMagick, ФайлНаДиске);
	
	json = Новый ЧтениеJSON;
	json.ОткрытьФайл(ИнфаНаДиске);
	json.Прочитать();
	
	РаботаСФайламиБЭД.УдалитьВременныеФайлы(ИнфаНаДиске);

	Попытка
		Ширина = Окр(25.4 * ПрочитатьJSON(json), 0);
	Исключение
		Ширина = 0;
	КонецПопытки;
	
	Ориентация = ОриентацияСтраницы.Портрет;
	Если Ширина > 290 Тогда
		Ориентация = ОриентацияСтраницы.Ландшафт;
	КонецЕсли;

	Возврат Ориентация;

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

// Запускает ImageMagick с указанными параметрами. В случае ошибки вызывается исключение,
// в остальных случаях возвращается код возврата ImageMagick.
//
// Параметры:
//   Параметры - Строка - параметры запуска ImageMagick.
//   ФайлыКУдалению - Массив - необязательный параметр, временные файлы, требующие удаления.
//
// Возвращаемое значение:
//   Число - 0 в случае успешного выполнения, 300+ - при завершении с предупреждением.
//     Полный список предупреждений см. в документации к ImageMagick.
//
Функция ЗапуститьImageMagick(Параметры, ФайлыКУдалению) Экспорт

	СисИнфо = Новый СистемнаяИнформация;
	ТипПлатформыСервера = СисИнфо.ТипПлатформы;
	
	ПолныйПуть = ПолучитьФункциональнуюОпцию("pdf_ПутьКImageMagick");
	
	Если (ТипПлатформыСервера <> ТипПлатформы.Linux_x86 
		И ТипПлатформыСервера <> ТипПлатформы.Linux_x86_64)
		И Не ЗначениеЗаполнено(ПолныйПуть) Тогда
			
		РаботаСФайламиБЭД.УдалитьВременныеФайлы(ФайлыКУдалению);
		ВызватьИсключение НСтр("ru = 'В настройках программы не указан полный путь к программе ImageMagick.'");
	КонецЕсли;
	
	Если ПолныйПуть = "convert.exe" Тогда
		РаботаСФайламиБЭД.УдалитьВременныеФайлы(ФайлыКУдалению);
		ВызватьИсключение НСтр("ru = 'В настройках программы указан полный путь к устаревшей версии ImageMagick (convert.exe).'");
	КонецЕсли;
		
	Если (ТипПлатформыСервера = ТипПлатформы.Linux_x86 
		Или ТипПлатформыСервера = ТипПлатформы.Linux_x86_64) Тогда
		
		Если ПолныйПуть <> "" Тогда
			СтрокаКоманды = СокрЛП(ПолныйПуть) + "" + Параметры;
		Иначе	
			СтрокаКоманды = Параметры;
		КонецЕсли;

	Иначе	
		СтрокаКоманды = """" + СокрЛП(ПолныйПуть) + """ " + Параметры;
	КонецЕсли;
	
	СтрокаЗапуска = СтрокаКоманды;
	
	КодВозврата = Неопределено;
	Попытка
		ЗапуститьПриложение(СтрокаЗапуска, , Истина, КодВозврата);
	Исключение
		РаботаСФайламиБЭД.УдалитьВременныеФайлы(ФайлыКУдалению);
		ТекстИсключения = СтрШаблон(НСтр("ru = 'Ошибка при вызове ImageMagick с командной строкой:
			|%1
			|(%2)'"),
			СтрокаКоманды,
			ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
		ВызватьИсключение ТекстИсключения;
	КонецПопытки;
	
	РаботаСФайламиБЭД.УдалитьВременныеФайлы(ФайлыКУдалению);
	
	// Успех.
	Если КодВозврата = 0 Тогда
		Возврат КодВозврата;
	КонецЕсли;
	
	// Не ImageMagick.
	Если КодВозврата = Неопределено Тогда
		ТекстИсключения = СтрШаблон(НСтр("ru = 'Нет кода возврата при вызове ImageMagick с командной строкой:
			|%1
			|Возможно, указанный путь не является путем к ImageMagick.'"),
			СтрокаКоманды);
		ВызватьИсключение ТекстИсключения;
	КонецЕсли;
	
	// Предупреждение.
	Если КодВозврата >= 300 
		И КодВозврата < 400 Тогда
		ТекстПредупреждения = СтрШаблон(НСтр("ru = 'Предупреждение %1 при вызове ImageMagick с командной строкой:
			|%2
			|Подробности см. в документации к ImageMagick.'"),
			КодВозврата,
			СтрокаКоманды);
		ЗаписьЖурналаРегистрации(НСтр("ru = 'Работа с картинками'"),
			УровеньЖурналаРегистрации.Предупреждение,,,
			ТекстПредупреждения);
		Возврат КодВозврата;
	КонецЕсли;
	
	// Ошибка ОС или иного приложения.
	Если КодВозврата < 300 Тогда
		ТекстИсключения = СтрШаблон(НСтр("ru = 'Ошибка %1 при вызове ImageMagick с командной строкой:
			|%2
			|Возможно, нарушена структура командной строки.'"),
			КодВозврата,
			СтрокаКоманды);
		ВызватьИсключение ТекстИсключения;
	КонецЕсли;
	
	// Ошибка ImageMagick.
	ТекстИсключения = СтрШаблон(НСтр("ru = 'Ошибка %1 при вызове ImageMagick с командной строкой:
		|%2
		|Подробности см. в документации к ImageMagick.'"),
		КодВозврата,
		СтрокаКоманды);
	ВызватьИсключение ТекстИсключения;
	
	
КонецФункции


Функция ПолучитьРасположениеШтампаЭПВPdf() Экспорт
	
	Возврат ПолучитьФункциональнуюОпцию("pdf_РасположениеШтампаЭПВPdf");
	
КонецФункции

Функция ОбъектВСпискеТиповПрисоединенныеФайлы(Объект)
	
	Возврат Метаданные.ОпределяемыеТипы.ПрисоединенныйФайл.Тип.СодержитТип(ТипЗнч(Объект));
	
КонецФункции

Параметр info: командной строки ImageMagick, позволяет сохранить данные об обрабатываемом файле.

А -format позволяет указать, какие именно данные нам интересны. Список полей опции -format можно посмотреть здесь.

Программист продвинутого уровня, который работает в ДО скажет, что в ДО уже есть типовой функционал, проставляющий штампы в файлы PDF. Тут у меня есть возражения.

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

Во-вторых, если исходный PDF в векторном формате, т.е. слои документа с текстом, то они так и остаются. В типовом функционале, из-за конвертации текстовые слои пропадают.

Comments

So empty here ... leave a comment!

Добавить комментарий

Sidebar