При импорте каталогов, договоров или спецификаций из таблиц Excel в базу данных 1С разработчики часто сталкиваются с нестандартной задачей. В одной из колонок таблицы вместо привычного текста или ссылок на картинки содержатся встроенные объекты — например, файлы PDF. Попытки прочитать значение ячейки стандартным путем вроде Лист.Cells(Строка, Колонка).Value возвращают пустоту, а при анализе объектов через COM-соединение выясняется, что у этих файлов нет физического пути на диске.
В этой статье мы подробно разберем, почему возникает эта сложность, и рассмотрим два рабочих и эффективных метода извлечения встроенных PDF-документов для последующей загрузки в информационную базу 1С.
Для начала проанализируем ситуацию и поймем, как Excel хранит подобные данные. Когда пользователь добавляет PDF-документ непосредственно в ячейку листа (через меню Вставка -> Объект -> Создать из файла), операционная система и MS Excel используют технологию OLE (Object Linking and Embedding).
Если при добавлении файла пользователь не установил флажок "Связать с файлом", то физический файл на диске больше никак не связан с таблицей. Excel упаковывает бинарные данные этого PDF-документа внутрь структуры самой книги. Следовательно, никакого пути вида "C:\Docs\contract.pdf" на компьютере пользователя больше не существует — файл «живет» исключительно внутри документа Excel.
В объектной модели MS Excel при обходе коллекции графических элементов Shapes такой объект определяется типом GraphicObject.Type = 10 (что соответствует системной константе OLE-объекта). Попытки получить его адрес через свойства OLE-формата не дадут результата, так как объект является внедренным, а не связанным.
Давайте рассмотрим два способа решения этой задачи: современный кроссплатформенный (без использования COM) и классический (через COM-соединение Windows).
Современные файлы Excel с расширением .xlsx представляют собой обычные упакованные ZIP-архивы. Если изменить расширение файла на .zip и распаковать его, мы получим доступ к внутренней XML-структуре документа и ко всем внедренным файлам.
Этот метод обладает огромными преимуществами: он не требует наличия установленного MS Excel на компьютере (или сервере 1С), работает во много раз быстрее, чем COM-соединение, и полностью совместим с операционными системами Linux и macOS.
Разберем алгоритм работы по шагам:
ЧтениеZipФайла распаковываем файл .xlsx во временный каталог на диске.\xl\embeddings\. Там мы найдем файлы с именами вида oleObject1.bin, oleObject2.bin и так далее..bin — это упакованные двоичные данные (так называемый OLE-контейнер Microsoft). Внутри него находится наш чистый PDF-файл, но «обернутый» служебными заголовками и окончаниями Microsoft. Если мы просто переименуем .bin в .pdf, файл может не открыться в некоторых программах-просмотрщиках из-за лишнего бинарного «мусора».%PDF и заканчивается маркером окончания файла %%EOF. Нам нужно прочитать файл .bin в буфер двоичных данных, найти эти маркеры, вырезать все данные между ними и сохранить в новый файл с расширением .pdf.Ниже представлен готовый программный код на языке 1С, который реализует эту логику:
Функция ИзвлечьPDFИзExcel(Знач ПутьКФайлуExcel, Знач КаталогВыгрузки) Экспорт
МассивПутейКФайлам = Новый Массив;
// Создаем уникальный временный каталог для распаковки ZIP
УникальныйИдентификатор = Новый УникальныйИдентификатор;
ВременныйКаталогРаспаковки = КаталогВременныхФайлов() + УникальныйИдентификатор + "\";
СоздатьКаталог(ВременныйКаталогРаспаковки);
// Шаг 1. Распаковываем XLSX как ZIP-архив
Попытка
Архив = Новый ЧтениеZipФайла(ПутьКФайлуExcel);
Архив.ИзвлечьВсе(ВременныйКаталогРаспаковки, РежимВосстановленияПутейФайловZIP.Восстанавливать);
Архив.Закрыть();
Исключение
ВызватьИсключение "Ошибка при распаковке файла Excel: " + ОписаниеОшибки();
КонецПопытки;
// Шаг 2. Проверяем наличие папки со встроенными OLE-объектами
КаталогВнедрений = ВременныйКаталогРаспаковки + "xl\embeddings\";
ФайлПапкиВнедрений = Новый Файл(КаталогВнедрений);
Если Не ФайлПапкиВнедрений.Существует() Тогда
// Внедренных OLE-объектов в файле нет
УдалитьФайлы(ВременныйКаталогРаспаковки);
Возврат МассивПутейКФайлам;
КонецЕсли;
НайденныеФайлыBin = НайтиФайлы(КаталогВнедрений, "*.bin");
СчетчикДокументов = 1;
Для Каждого ФайлBin Из НайденныеФайлыBin Цикл
// Читаем бинарные данные OLE-файла
ДвоичныеДанныеBin = Новый ДвоичныеДанные(ФайлBin.ПолноеИмя);
Буфер = ПолучитьБуферИзДвоичныхДанных(ДвоичныеДанныеBin);
РазмерБуфера = Буфер.Размер();
// Шаг 3. Ищем сигнатуру начала PDF (%PDF) -> в байтах: 25 50 44 46
МаркерНачала = Новый БуферДвоичныхДанных(4);
МаркерНачала.Записать(0, 37); // %
МаркерНачала.Записать(1, 80); // P
МаркерНачала.Записать(2, 68); // D
МаркерНачала.Записать(3, 70); // F
ИндексНачала = Буфер.Найти(МаркерНачала);
Если ИндексНачала = Неопределено Тогда
// Этот конкретный бинарный файл не является документом PDF, пропускаем его
Продолжить;
КонецЕсли;
// Шаг 4. Ищем сигнатуру конца PDF (%%EOF) -> в байтах: 25 25 45 4F 46
МаркерКонца = Новый БуферДвоичныхДанных(5);
МаркерКонца.Записать(0, 37); // %
МаркерКонца.Записать(1, 37); // %
МаркерКонца.Записать(2, 69); // E
МаркерКонца.Записать(3, 79); // O
МаркерКонца.Записать(4, 70); // F
ИндексКонца = Буфер.Найти(МаркерКонца, ИндексНачала);
Если ИндексКонца = Неопределено Тогда
// Если маркер окончания не найден, считываем данные до самого конца файла
ИндексКонца = РазмерБуфера - 1;
Иначе
// Сдвигаем индекс, чтобы захватить символы самого маркера конца
ИндексКонца = ИндексКонца + 5;
КонецЕсли;
// Вырезаем чистый поток байтов PDF из OLE-контейнера
РазмерPDF = ИндексКонца - ИндексНачала;
ЧистыйБуферPDF = Новый БуферДвоичныхДанных(РазмерPDF);
Буфер.Копировать(ИндексНачала, ЧистыйБуферPDF, 0, РазмерPDF);
// Сохраняем результат на диск в виде готового PDF-файла
ИмяНовогоФайла = "ИзвлеченныйДокумент_" + СчетчикДокументов + ".pdf";
ПолныйПутьКФайлуPDF = КаталогВыгрузки + ИмяНовогоФайла;
ДвоичныеДанныеPDF = ПолучитьДвоичныеДанныеИзБуфера(ЧистыйБуферPDF);
ДвоичныеДанныеPDF.Записать(ПолныйПутьКФайлуPDF);
МассивПутейКФайлам.Добавить(ПолныйПутьКФайлуPDF);
СчетчикДокументов = СчетчикДокументов + 1;
КонецЦикла;
// Очищаем временную папку распаковки
Попытка
УдалитьФайлы(ВременныйКаталогРаспаковки);
Исключение
// Ошибка удаления временных файлов не является критичной для работы алгоритма
КонецПопытки;
Возврат МассивПутейКФайлам;
КонецФункции
Используя этот алгоритм, мы получаем массив чистых, работоспособных PDF-файлов, извлеченных прямо из книги Excel. Теперь их можно легко прикрепить к карточкам договоров или контрагентов в справочниках 1С с помощью стандартного механизма присоединенных файлов.
Рассмотрим ситуацию, когда загрузка происходит на стороне Windows-клиента, а файл Excel сохранен в старом двоичном формате (расширение .xls версии 97-2003). Распаковать его как ZIP-архив не получится, так как этот формат имеет совершенно другую структуру (двоичный формат файлов документов Microsoft Compound File Binary Format).
В этом случае придется задействовать установленный на компьютере Microsoft Excel через механизм COM-соединения. Для извлечения и обработки OLE-объекта мы будем использовать объектное свойство OLEFormat.
Посмотрим на пример кода, обходящего коллекцию фигур листа Excel:
Процедура ПрочитатьВстроенныеОбъектыЧерезCOM(ПутьКФайлуExcel)
// Инициализируем COM-соединение с MS Excel
ExcelApp = Новый COMОбъект("Excel.Application");
ExcelApp.Visible = Ложь;
ExcelApp.DisplayAlerts = Ложь;
Попытка
Книга = ExcelApp.Workbooks.Open(ПутьКФайлуExcel);
Лист = Книга.WorkSheets(1);
Для Каждого Рисунок Из Лист.Shapes Цикл
// Проверяем тип объекта. Тип 10 соответствует OLE-объекту
Если Рисунок.Type = 10 Тогда
ProgID = "";
Попытка
ProgID = Рисунок.OLEFormat.ProgID;
Исключение
// Свойство может быть недоступно для некоторых типов объектов
Продолжить;
КонецПопытки;
// Проверяем, относится ли OLE-объект к типу PDF (обычно это AcroExch.Document)
Если Найти(ВРег(ProgID), "ACRO") > 0 Или Найти(ВРег(ProgID), "PDF") > 0 Тогда
// Активируем объект в родительском приложении
Попытка
// Метод Verb запускает действие над OLE-объектом.
// Параметр 1 обычно означает открытие файла (xlOpen)
Рисунок.OLEFormat.Verb(1);
Сообщить("Документ " + Рисунок.Name + " успешно открыт в стороннем просмотрщике PDF.");
Исключение
Сообщить("Не удалось запустить приложение для просмотра PDF: " + ОписаниеОшибки());
КонецПопытки;
КонецЕсли;
КонецЕсли;
КонецЦикла;
Книга.Close(Ложь);
Исключение
Сообщить("Произошла критическая ошибка работы с Excel: " + ОписаниеОшибки());
КонецПопытки;
ExcelApp.Quit();
ExcelApp = Неопределено;
КонецПроцедуры
Важное ограничение метода COM: Прямого метода сохранения бинарного содержимого OLE-объекта на диск через COM-модель Excel Microsoft не предоставляет. Метод OLEFormat.Verb(1) лишь открывает файл во внешнем просмотрщике (например, Adobe Acrobat Reader), откуда пользователю придется сохранять его вручную.
Поэтому, если вам нужно автоматизировать процесс полностью «без участия человека», настоятельно рекомендуется использовать первый способ с ZIP-архивом. Если исходный файл имеет старый формат .xls, лучшим решением будет предварительно программно сконвертировать его в формат .xlsx (это можно сделать одной командой сохранения в Excel через COM-соединение Книга.SaveAs(НовыйПуть, 51), где 51 — это код формата XLSX), а затем обработать его быстрым и надежным бессерверным ZIP-методом.