При разработке интеграций (например, загрузке прайс-листов или каталогов товаров из файлов поставщиков) разработчики 1С часто сталкиваются со следующей проблемой: в файле Excel есть колонка с изображениями товаров, но при загрузке документа стандартными средствами связать картинку с конкретной строкой оказывается крайне сложно. Объект ОбластьТабличногоДокумента не имеет встроенного свойства Картинки или Рисунки, а вся графика хранится в общей коллекции Рисунки табличного документа и позиционируется на отдельном графическом слое поверх ячеек.
Кроме того, при конвертации файлов Excel в формат табличного документа 1С (MXL) часто возникает дублирование картинок, а попытка прочитать некоторые графические элементы (рамки, логотипы, элементы разметки) вызывает ошибку "Поле объекта недоступно для чтения". В этой статье мы подробно разберем три надежных способа, которые позволяют извлечь изображения и точно сопоставить их с нужными строками табличной части.
Этот способ уникален тем, что работает исключительно штатными средствами платформы 1С на любом сервере (даже под управлением Linux) и не требует установленного Microsoft Excel или сторонних библиотек. Идея заключается в сопоставлении физических координат ячеек и координат картинок.
Проблема в том, что у рисунков в коллекции ТабличныйДокумент.Рисунки координаты задаются в миллиметрах (свойства Верх, Лево, Высота, Ширина), тогда как ячейки адресуются по строкам и колонкам. Мы можем преодолеть это ограничение с помощью динамического создания временного текстового рисунка.
Алгоритм реализации метода:
Текст.Расположить(), чтобы позиционировать временный рисунок точно по границам ячейки. При этом платформа автоматически пересчитает строковые координаты в миллиметры.Рисунки с сохраненными координатами ячеек. Если геометрический центр картинки попадает внутрь границ ячейки — мы нашли соответствие.Рассмотрим пример кода, реализующий этот подход:
&НаСервере
Процедура СопоставитьКартинкиЧерезКоординаты(ТабДок)
// Соответствие для хранения миллиметровых границ каждой строки
КартаГраницСтрок = Новый Соответствие;
НомерКолонкиСКартинками = 1; // Предположим, картинки находятся в первой колонке
// Шаг 1: Вычисляем физические границы ячеек для каждой строки
Для НомСтроки = 2 По ТабДок.ВысотаТаблицы Цикл
ОбластьЯчейки = ТабДок.Область(НомСтроки, НомерКолонкиСКартинками);
// Создаем временный невидимый объект для вычисления координат
ВремРисунок = ТабДок.Рисунки.Добавить(ТипРисункаТабличногоДокумента.Текст);
ВремРисунок.Расположить(ОбластьЯчейки);
// Фиксируем координаты границ ячейки в миллиметрах
ГраницыЯчейки = Новый Структура;
ГраницыЯчейки.Вставить("Верх", ВремРисунок.Верх);
ГраницыЯчейки.Вставить("Низ", ВремРисунок.Верх + ВремРисунок.Высота);
ГраницыЯчейки.Вставить("Лево", ВремРисунок.Лево);
ГраницыЯчейки.Вставить("Право", ВремРисунок.Лево + ВремРисунок.Ширина);
КартаГраницСтрок.Вставить(НомСтроки, ГраницыЯчейки);
// Удаляем временный рисунок из коллекции
ТабДок.Рисунки.Удалить(ВремРисунок);
КонецЦикла;
// Шаг 2: Анализируем реальные картинки и сопоставляем их со строками
Для Каждого Рисунок Из ТабДок.Рисунки Цикл
// Отсекаем служебные объекты и ошибки чтения недоступных свойств
Попытка
Если Рисунок.ТипРисунка <> ТипРисункаТабличногоДокумента.Картинка Тогда
Продолжить;
КонецЕсли;
ТекущаяКартинка = Рисунок.Картинка;
Исключение
// Сюда попадут служебные объекты с недоступными для чтения свойствами
Продолжить;
КонецПопытки;
// Находим геометрический центр текущего рисунка
ЦентрY = Рисунок.Верх + (Рисунок.Высота / 2);
ЦентрX = Рисунок.Лево + (Рисунок.Ширина / 2);
// Ищем, в какую строку попадает этот центр
Для Каждого СтрокаКарты Из КартаГраницСтрок Цикл
НомерСтроки = СтрокаКарты.Ключ;
Границы = СтрокаКарты.Значение;
Если (ЦентрY >= Границы.Верх И ЦентрY <= Границы.Низ) И
(ЦентрX >= Границы.Лево И ЦентрX <= Границы.Право) Тогда
// Картинка успешно сопоставлена со строкой
ДвоичныеДанныеКартинки = ТекущаяКартинка.ПолучитьДвоичныеДанные();
Если ДвоичныеДанныеКартинки <> Неопределено Тогда
ЗаписатьКартинкуДляТовара(НомерСтроки, ДвоичныеДанныеКартинки);
КонецЕсли;
Прервать; // Переходим к следующему рисунку
КонецЕсли;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Современные файлы Excel с расширением .xlsx представляют собой обычные ZIP-архивы, содержащие XML-файлы разметки и медиаресурсы. Мы можем распаковать файл средствами 1С с помощью объекта ЧтениеZipФайла и программно связать картинки с ячейками.
Структура файлов внутри архива .xlsx:
xl/media/ (например, image1.png, image2.jpeg).xl/worksheets/_rels/sheet1.xml.rels. Он указывает, какой внутренний ID (например, rId1) соответствует файлу разметки графики.xl/drawings/drawing1.xml) описывает точное расположение рисунков на листе с помощью узлов xdr:twoCellAnchor или xdr:oneCellAnchor.Внутри узла xdr:twoCellAnchor содержатся теги координат левого верхнего угла картинки xdr:from:
xdr:col — порядковый номер колонки ячейки (индексация начинается с 0).xdr:row — порядковый номер строки ячейки (индексация начинается с 0).А также тег xdr:pic, в котором через атрибут r:embed указан идентификатор связи. В файле связей xl/drawings/_rels/drawing1.xml.rels этот идентификатор сопоставляется с конкретным файлом из папки xl/media/.
Проанализируем пример кода, выполняющего разбор структуры OpenXML:
&НаСервере
Процедура ИмпортИзXLSXЧерезOpenXML(ПутьКФайлу)
// Создаем временную папку для распаковки архива
ПутьКПапкеРаспаковки = КаталогВременныхФайлов() + "XLSX_Unpack_" + Формат(ТекущаяДата(), "ДФ=yyyyMMddHHmmss") + "/";
СоздатьКаталог(ПутьКПапкеРаспаковки);
// Распаковываем XLSX как ZIP-архив
Архив = Новый ЧтениеZipФайла(ПутьКФайлу);
Архив.ИзвлечьВсе(ПутьКПапкеРаспаковки, РежимВосстановленияПутейФайловZIP.Восстанавливать);
Архив.Закрыть();
// Пути к ключевым файлам разметки
ПутьКРазметке = ПутьКПапкеРаспаковки + "xl/drawings/drawing1.xml";
ПутьКСвязям = ПутьКПапкеРаспаковки + "xl/drawings/_rels/drawing1.xml.rels";
// Проверим наличие файлов графики
ФайлРазметки = Новый Файл(ПутьКРазметке);
Если Не ФайлРазметки.Существовать() Тогда
ВызватьИсключение "В файле Excel отсутствуют графические элементы или они расположены не на первом листе.";
КонецЕсли;
// Шаг 1: Читаем связи рисунков (определяем соответствие rId -> имя файла в media)
СоответствиеСвязей = Новый Соответствие;
ЧтениеXMLСвязей = Новый ЧтениеXML;
ЧтениеXMLСвязей.ОткрытьФайл(ПутьКСвязям);
ПостроительСвязей = Новый ПостроительDOM;
ДокументСвязейDOM = ПостроительСвязей.Прочитать(ЧтениеXMLСвязей);
ЧтениеXMLСвязей.Закрыть();
УзлыСвязей = ДокументСвязейDOM.ПолучитьЭлементыПоИмени("Relationship");
Для Каждого УзелСвязи Из УзлыСвязей Цикл
Атрибуты = УзелСвязи.ПолучитьАтрибуты();
ИдСвязи = Атрибуты.ПолучитьИменованныйУзел("Id").ЗначениеУзла;
ЦелевойПуть = Атрибуты.ПолучитьИменованныйУзел("Target").ЗначениеУзла; // Пример: "../media/image1.png"
// Очищаем путь до имени файла
ИмяФайла = СтрЗаменить(ЦелевойПуть, "../media/", "");
СоответствиеСвязей.Вставить(ИдСвязи, ИмяФайла);
КонецЦикла;
// Шаг 2: Читаем координаты размещения рисунков из drawing1.xml
ЧтениеXMLРазметки = Новый ЧтениеXML;
ЧтениеXMLРазметки.ОткрытьФайл(ПутьКРазметке);
ПостроительРазметки = Новый ПостроительDOM;
ДокументРазметкиDOM = ПостроительРазметки.Прочитать(ЧтениеXMLРазметки);
ЧтениеXMLРазметки.Закрыть();
АнкерныеУзлы = ДокументРазметкиDOM.ПолучитьЭлементыПоИмени("xdr:twoCellAnchor");
Для Каждого Анкер Из АнкерныеУзлы Цикл
Попытка
// Извлекаем строку и колонку привязки (левый верхний угол картинки)
УзелFrom = Анкер.ПолучитьЭлементыПоИмени("xdr:from")[0];
СтрокаXML = УзелFrom.ПолучитьЭлементыПоИмени("xdr:row")[0].ТекстовоеСодержимое;
КолонкаXML = УзелFrom.ПолучитьЭлементыПоИмени("xdr:col")[0].ТекстовоеСодержимое;
// Excel считает индексы с 0, переводим в формат 1С (начиная с 1)
НомерСтроки1С = Число(СтрокаXML) + 1;
НомерКолонки1С = Число(КолонкаXML) + 1;
// Находим ID связи картинки
УзелPic = Анкер.ПолучитьЭлементыПоИмени("xdr:pic")[0];
УзелBlip = УзелPic.ПолучитьЭлементыПоИмени("xdr:blipFill")[0].ПолучитьЭлементыПоИмени("a:blip")[0];
ИдКартинки = УзелBlip.ПолучитьАтрибуты().ПолучитьИменованныйУзел("r:embed").ЗначениеУзла;
// Составляем полный физический путь к файлу картинки на диске
ИмяФайлаКартинки = СоответствиеСвязей.Получить(ИдКартинки);
ПолныйПутьКФайлу = ПутьКПапкеРаспаковки + "xl/media/" + ИмяФайлаКартинки;
// Загружаем картинку в двоичные данные
ФайлИзображения = Новый Файл(ПолныйПутьКФайлу);
Если ФайлИзображения.Существовать() Тогда
ДвоичныеДанныеИзображения = Новый ДвоичныеДанные(ПолныйПутьКФайлу);
ЗаписатьКартинкуДляТовара(НомерСтроки1С, ДвоичныеДанныеИзображения);
КонецЕсли;
Исключение
// Игнорируем ошибки при разборе поврежденных или неподдерживаемых узлов разметки
Продолжить;
КонецПопытки;
КонецЦикла;
// Очищаем временные файлы
УдалитьФайлы(ПутьКПапкеРаспаковки);
КонецПроцедуры
Если обработка выполняется на операционной системе Windows и на компьютере гарантированно установлен Microsoft Office Excel, наиболее простым и надежным решением будет использование COM-интерфейса. Этот способ избавляет разработчика от необходимости писать сложные XML-парсеры.
Для определения точного положения картинок мы используем свойства TopLeftCell.Row и TopLeftCell.Column, которые присутствуют у каждого графического объекта (Shape) в коллекции Excel.
Для экспорта картинок во внешние файлы применим быстрый и эффективный трюк — сохранение книги Excel в формате веб-страницы (HTML). При этом Excel автоматически конвертирует и сохраняет все изображения документа в отдельную папку на диске с последовательным наименованием.
Рассмотрим по шагам реализацию этой логики:
&НаКлиенте
Процедура ИмпортКартинокЧерезCOM(ПутьКФайлуExcel)
// Инициализируем COM-объект Excel
Попытка
ExcelПриложение = Новый COMОбъект("Excel.Application");
Исключение
Предупреждение("На компьютере не установлен Microsoft Excel или у пользователя нет прав на его запуск.");
Возврат;
КонецПопытки;
ExcelПриложение.Visible = Ложь;
ExcelПриложение.DisplayAlerts = Ложь;
Попытка
Книга = ExcelПриложение.Workbooks.Open(ПутьКФайлуExcel);
Лист = Книга.Sheets(1);
Исключение
ExcelПриложение.Quit();
ВызватьИсключение "Не удалось открыть файл Excel: " + ОписаниеОшибки();
КонецПопытки;
// Массив структур для хранения связей индексов картинок с их ячейками
КартаКартинок = Новый Массив;
// Шаг 1: Фиксируем координаты каждой картинки из коллекции Pictures
ИндексРисунка = 1;
Для Каждого Рисунок Из Лист.Pictures Цикл
ДанныеКартинки = Новый Структура;
ДанныеКартинки.Вставить("ИндексРисунка", ИндексРисунка);
ДанныеКартинки.Вставить("Строка", Рисунок.TopLeftCell.Row);
ДанныеКартинки.Вставить("Колонка", Рисунок.TopLeftCell.Column);
КартаКартинок.Добавить(ДанныеКартинки);
ИндексРисунка = ИндексРисунка + 1;
КонецЦикла;
// Шаг 2: Сохраняем книгу как HTML-документ для автоматического экспорта картинок
ВременныйКаталог = КаталогВременныхФайлов() + "ExcelHTML_" + Формат(ТекущаяДата(), "ДФ=yyyyMMddhhmmss") + "\";
СоздатьКаталог(ВременныйКаталог);
ФайлHTML = ВременныйКаталог + "page.html";
ПапкаРесурсов = ВременныйКаталог + "page.files\"; // Эту папку Excel создаст сам
// Код формата сохранения 44 соответствует константе xlHTML в Excel
Книга.SaveAs(ФайлHTML, 44);
// Закрываем Excel и освобождаем память
Книга.Close(Ложь);
ExcelПриложение.Quit();
ExcelПриложение = Неопределено;
// Шаг 3: Читаем файлы картинок с диска и сопоставляем их с ячейками
Для Каждого ЭлементКарты Из КартаКартинок Цикл
// Excel сохраняет картинки по маске "imageXXX.ext" (например, "image001.png", "image002.jpg")
СтроковоеИмяФайла = "image" + Прав("000" + СокрЛП(ЭлементКарты.ИндексРисунка), 3);
// Поиск файла с любым расширением графического формата
МассивФайлов = НайтиФайлы(ПапкаРесурсов, СтроковоеИмяФайла + ".*");
Если МассивФайлов.Количество() > 0 Тогда
ФайлКартинки = МассивФайлов[0];
ДвоичныеДанныеИзображения = Новый ДвоичныеДанные(ФайлКартинки.ПолноеИмя);
// Передаем полученные двоичные данные на сервер для записи в базу данных
СохранитьКартинкуНаСервере(ЭлементКарты.Строка, ЭлементКарты.Колонка, ДвоичныеДанныеИзображения);
КонецЕсли;
КонецЦикла;
// Очищаем временные файлы
УдалитьФайлы(ВременныйКаталог);
ПоказатьПредупреждение(, "Импорт картинок успешно завершен!");
КонецПроцедуры
Каждый из рассмотренных нами методов имеет свои достоинства и ограничения, поэтому выбор оптимального подхода зависит от архитектуры вашей информационной системы:
.xlsx. Он работает в фоновом режиме на сервере без привлечения внешних приложений. Единственным ограничением является необходимость поддержки формата XLSX (старый формат XLS данный метод обработать не сможет).