Привет, Хабр! Меня зовут Алексей Грохотов, я разрабатываю продукт Сфера.Архитектура в ИТ‑холдинге Т1. Перед нашей командой стояла задача перенести документы из Orbus iServer в Сфера.Архитектуру. Iserver — это набор инструментов для описания, поддержки и трансформации архитектуры предприятия. Он в значительной степени интегрирован с Microsoft Office, например, все схемы в этом инструментарии создаются в Visio.
Я должен был проанализировать схемы Visio и извлечь необходимую информацию из этих документов. Объекты, соответствующие «прямоугольничкам и стрелочкам» Visio, уже хранились у нас в базе. Мне нужно было соотнести их с фигурами и стрелками схемы, записать для этих объектов геометрическое и текстовое содержание фигур, а также некоторые их специфические свойства. Ещё нужно было определить порты — «стыковочные места» по периметрам фигур, к которым присоединяются стрелки, а также найти надписи у стрелок и фигур. И после этого сохранить в базу данных всю найденную информацию.
Забегая вперёд, покажу результат успешного переноса в Сфера.Архитектуру схемы, нарисованной в Visio.
До:

После:

Первые подходы
В первую очередь, я поискал в интернете имеющиеся решения. Нашёл Apache POI и Aspose.Diagram. POI хорошо работал с файлами Excel, но крайне ограниченно — с документами Visio: мне удалось извлечь только название схемы, имя создавшего и некоторые другие, не особо полезные данные.
Aspose — платное решение, предоставляющее API для создания, парсинга и преобразования документов Visio в собственные форматы приложений. Он нам не подошёл, во‑первых, из‑за того, что он платный, а во‑вторых, не хотелось привязываться к стороннему ПО. К слову, у этого продукта есть очень хороший форум, на котором служба поддержки отвечает на вопросы пользователей. Эта информация очень помогла мне, когда я пытался в массе одинаковых XML‑тэгов найти ответ «как же это здесь реализовано».
Тогда я поискал по Хабру и с удивлением обнаружил, что статей про парсинг схем Visio нет. Видимо, до недавнего времени никто не занимался такой бесполезной ерундой уникальной задачей. Отчасти это, но в большей степени затраченное время заставило меня описать свои действия и результат. Может, этот опыт поможет кому-то сэкономить своё время.
Структура документа Visio
Ранние версии Visio сохраняли схемы в формате VSD. Это бинарный формат, таких документов у нас было мало, и с ними я не работал. Сейчас же используется формат VSDX, который, как и другие файлы Microsoft Office, представляет собой ZIP‑архив. В нём содержатся XML‑файлы, описывающие содержимое документа.
Для статьи я создал небольшой документ example.vsdx, представляющий собой организационную схему с гендиром, руководителем и тремя разработчиками.

Разархивируем его, чтобы посмотреть структуру файлов:
\example\docProps\app.xml
\example\docProps\core.xml
\example\docProps\custom.xml
\example\docProps\thumbnail.emf
\example\visio\document.xml
\example\visio\masters\master1.xml
\example\visio\masters\master10.xml
\example\visio\masters\master11.xml
\example\visio\masters\master12.xml
\example\visio\masters\master13.xml
\example\visio\masters\master14.xml
\example\visio\masters\master15.xml
\example\visio\masters\master2.xml
\example\visio\masters\master3.xml
\example\visio\masters\master4.xml
\example\visio\masters\master5.xml
\example\visio\masters\master6.xml
\example\visio\masters\master7.xml
\example\visio\masters\master8.xml
\example\visio\masters\master9.xml
\example\visio\masters\masters.xml
\example\visio\masters\_rels\master1.xml.rels
\example\visio\masters\_rels\master2.xml.rels
\example\visio\masters\_rels\master3.xml.rels
\example\visio\masters\_rels\master4.xml.rels
\example\visio\masters\_rels\master5.xml.rels
\example\visio\masters\_rels\master6.xml.rels
\example\visio\masters\_rels\master7.xml.rels
\example\visio\masters\_rels\masters.xml.rels
\example\visio\media\image1.jpeg
\example\visio\media\image2.bmp
\example\visio\pages\page1.xml
\example\visio\pages\pages.xml
\example\visio\pages\_rels\page1.xml.rels
\example\visio\pages\_rels\pages.xml.rels
\example\visio\solutions\solution1.xml
\example\visio\solutions\solutions.xml
\example\visio\solutions\_rels\solutions.xml.rels
\example\visio\theme\theme1.xml
\example\visio\windows.xml
\example\visio\_rels\document.xml.rels
\example\[Content_Types].xml
\example\_rels\.rels
Нас интересует папка \example\visio\pages\. В ней есть файл pages.xml, содержащий описание страниц документа, и файл page1.xml, где описаны элементы на странице, фигуры и их связи друг с другом. Если файл Visio — многостраничный документ, то в папке pages будут содержаться файлы page1.xml, page2.xml, page3.xml и так далее.
Структура файла страницы
Рассмотрим подробнее структуру файла page1.xml. В нём есть элемент PageContents, у которого есть дочерние элементы Shapes и Connects. Shapes описывают свойства фигур и соединителей («стрелочек»), их местоположение на листе, размеры, содержащийся в них текст и прочее. В упрощенном виде структура файла выглядит так:
<?xml version='1.0' encoding='utf-8' ?>
<PageContents xmlns='http://schemas.microsoft.com/office/visio/2012/main'
xmlns:r='http://schemas.openxmlformats.org/officeDocument/2006/relationships' xml:space='preserve'>
<Shapes>
<Shape ID='31' NameU='Dynamic connector' Name='Динамический соединитель' Type='Shape' Master='15' UniqueID='{104EEDCC-FAF4-437C-B860-C89095023E2A}'>
<Cell N='TxtPinX' V='0.09842519462108615'/>
<Cell N='TxtPinY' V='-0.5270669460296633'/>
<!-- еще несколько элементов Cell -->
<Section N='Geometry' IX='0'>
<Row T='MoveTo' IX='1'>
<Cell N='X' V='0.09842519685039353'/>
</Row>
<Row T='LineTo' IX='2'>
<Cell N='X' V='0.09842519685039353'/>
<Cell N='Y' V='-1.054133857858449'/>
</Row>
<Row T='LineTo' IX='3' Del='1'/>
<Row T='LineTo' IX='4' Del='1'/>
</Section>
</Shape>
<Shape ID='32' Type='Shape' Master='15' UniqueID='{736D6637-EACE-43D3-B3F1-6087F0A60B11}'>
<!-- содержимое фигуры, опустил для краткости -->
</Shape>
</Shapes>
<Connects>
<Connect FromSheet='54' FromCell='EndX' FromPart='12' ToSheet='43' ToCell='Connections.Top.X' ToPart='100'/>
<!-- еще соединители, опустил для краткости -->
</Connects>
</PageContents>
У каждой фигуры есть свои атрибуты. В первую очередь, меня интересуют ID — идентификатор фигуры внутри родительского элемента PageContents, и UniqueID — уникальный идентификатор UUID, или, как называет его Microsoft, GUID. Фигура также содержит дочерние элементы Cell, Section, Text и вложенные элементы Shapes. Элемент Section содержит элементы Row. Здесь меня интересует ячейки с именами PinX и PinY, определяющие местоположение фигуры на листе, а также элемент Text. Все размеры в фигурах Microsoft Visio указываются в дюймах, я умножал извлечённые значения на некоторый коэффициент, чтобы получить размеры в пикселях.
Описание соединителей
Соединители описывают, какие фигуры соединяются друг с другом. Рассмотрим в качестве примера два первых элемента Connect с атрибутом FromSheet, равным 54:
<Connects>
<!-- Строка показывает, что фигруа 54 (FromSheet='54') соединяется с фигурой 43 (ToSheet='43') -->
<Connect FromSheet='54' FromCell='EndX' FromPart='12' ToSheet='43' ToCell='Connections.Top.X' ToPart='100'/>
<!-- Эта строка показывает, что та же фигруа 54 также соединяется с фигурой 11 -->
<Connect FromSheet='54' FromCell='BeginX' FromPart='9' ToSheet='11' ToCell='Connections.Bottom.X' ToPart='103'/>
<Connect FromSheet='53' FromCell='EndX' FromPart='12' ToSheet='33' ToCell='Connections.Top.X' ToPart='100'/>
<Connect FromSheet='53' FromCell='BeginX' FromPart='9' ToSheet='11' ToCell='Connections.Left.X' ToPart='101'/>
<Connect FromSheet='32' FromCell='EndX' FromPart='12' ToSheet='21' ToCell='Connections.Top.X' ToPart='100'/>
<Connect FromSheet='32' FromCell='BeginX' FromPart='9' ToSheet='11' ToCell='Connections.Right.X' ToPart='102'/>
<Connect FromSheet='31' FromCell='EndX' FromPart='12' ToSheet='11' ToCell='Connections.Float.X' ToPart='104'/>
<Connect FromSheet='31' FromCell='BeginX' FromPart='9' ToSheet='1' ToCell='Connections.Bottom.X' ToPart='103'/>
</Connects>
Атрибут FromSheet указывает на номер фигуры, содержащей описание соединителя. Из этого описания можно извлечь координаты соединителя (ячейки с именами PinX и PinY), а также координаты изгибов этого соединителя (секция Geometry, ряды MoveTo и, LineTo). Забегая вперёд, скажу, что мне не удалось перевести полученные значения в нужные мне единицы — стрелки‑соединители по этим значениям отображались криво, и мы не стали использовать эти координаты.
Атрибут ToSheet указывает на номера фигур, соединяемых этим соединителем‑стрелкой. В Connects есть два элемента Connect с номером 54, у первого атрибут ToSheet равен 43, у второго — 11. Получаем такую картину: фигура 54 описывает стрелку на схеме, соединяющую две фигуры с номерами 43 и 11. Это «Разработчик ПО Семён Шарпов» и «Руководитель разработки». Ниже я привёл содержимое этих фигур, вырезав некоторые элементы, которые я не использовал. Из этих фигур я беру информацию о содержимом фигуры (элемент Text), её размерах (ячейки Height и Width, в этом примере отсутствуют) и о расположении на схеме (ячейки PinX и PinY). Также из них можно извлечь информацию о расположении текста относительно страницы (TxtPinX и TxtPinY) и фигуры (TxtLocPinX и TxtLocPinY).
Описание фигуры
Вот элемент с номером 54, описывающий соединитель:
<Shape ID='54' NameU='Dynamic connector.54' Name='Динамический соединитель.54' Type='Shape' Master='15' UniqueID='{63A888AC-0A5D-4E4B-9738-168F9973DA8D}'>
<! -- 'PinX', 'PinY' - координаты центра фигуры-стрелки -->
<Cell N='PinX' V='6.628444882' F='Inh'/>
<Cell N='PinY' V='4.21579724416911' F='Inh'/>
<! -- 'BeginX', 'BeginY', 'EndX', 'EndY' - координаты начала и конца стрелки -->
<Cell N='BeginX' V='6.628444882' F='PAR(PNT(Sheet.11!Connections.Bottom.X,Sheet.11!Connections.Bottom.Y))'/>
<Cell N='BeginY' V='4.927657480141551' F='PAR(PNT(Sheet.11!Connections.Bottom.X,Sheet.11!Connections.Bottom.Y))'/>
<Cell N='EndX' V='6.628444882' F='PAR(PNT(Sheet.43!Connections.Top.X,Sheet.43!Connections.Top.Y))'/>
<Cell N='EndY' V='3.50393700819667' F='PAR(PNT(Sheet.43!Connections.Top.X,Sheet.43!Connections.Top.Y))'/>
<! -- PinX, PinY - координаты центра фигуры-стрелки. MoveTo, LineTo – координаты изгибов -->
<Section N='Geometry' IX='0'>
<Row T='MoveTo' IX='1'>
<Cell N='X' V='-0.09842519685039353'/>
</Row>
<Row T='LineTo' IX='2'>
<Cell N='X' V='-0.09842519685039441'/>
<Cell N='Y' V='-1.423720471944882'/>
</Row>
</Section>
</Shape>
Элемент 43, описывающий фигуру «Разработчик ПО»:
<Shape ID='43' NameU='Position Belt.43' Name='Пояс должности.43' Type='Group' Master='5' UniqueID='{670A7028-8CC9-4BD9-9531-9AA1200B6158}'>
<! -- 'PinX', 'PinY' - координаты центра фигуры «Разработчик ПО» -->
<Cell N='PinX' V='6.628444882' F='PNTX(LOCTOPAR(User.PageLoc,ThePage!PageWidth,Width))'/>
<Cell N='PinY' V='3.06643700819667' F='PNTY(LOCTOPAR(User.PageLoc,ThePage!PageWidth,Width))'/>
<Text>
<cp IX='0'/>
Разработчик ПО
</Text>
<Shapes>
<Shape ID='44' Type='Group' MasterShape='6' UniqueID='{E8AEAC34-763B-41E8-8BBE-C1FCEE857515}'>
<! -- 'Height ' - высота фигуры -->
<Cell N='Height' V='0.1333828247070313' F='Inh'/>
<! -- 'TxtPinY' - координата текстового блока по оси Y -->
<Cell N='TxtPinY' V='0.06669141235351563' F='Inh'/>
<Cell N='TxtHeight' V='0.1333828247070313' F='Inh'/>
<! -- текстовый блок с содержимым -->
<Text>
<cp IX='0'/>
Семен Шарпов
</Text>
</Shape>
</Shapes>
</Shape>
Элемент 11, описывающий фигуру «Руководитель разработки»:
<Shape ID='11' NameU='Manager Belt' Name='Лента менеджера' Type='Group' Master='4' UniqueID='{E4A2BCB3-9C78-425F-8D4B-C6C4654D0F6E}'>
<Cell N='PinX' V='6.628444882' F='PNTX(LOCTOPAR(User.PageLoc,ThePage!PageWidth,Width))'/>
<Cell N='PinY' V='5.365157480141551' F='PNTY(LOCTOPAR(User.PageLoc,ThePage!PageWidth,Width))'/>
<Text>
<cp IX='0'/>
Руководитель разработки
</Text>
<Shapes>
<Shape ID='12' Type='Group' MasterShape='6' UniqueID='{87F8D211-514F-4119-8F90-05FC62DD1193}'>
<Cell N='Height' V='0.1333828247070313' F='Inh'/>
<Cell N='LocPinY' V='0.06669141235351563' F='Inh'/>
<Cell N='TxtPinY' V='0.06669141235351563' F='Inh'/>
<Cell N='TxtHeight' V='0.1333828247070313' F='Inh'/>
<Cell N='TxtLocPinY' V='0.06669141235351563' F='Inh'/>
<Text>
<cp IX='0'/>
Ольга Петрова
</Text>
</Shape>
</Shapes>
</Shape>
Парсинг
Открываем ZIP-файл
Как уже говорил, файл с расширением *.vsdx представляет собой ZIP-архив. Прежде, чем начать парсинг, распакуем архив, создав временный файл. Не забудьте удалить созданный файл после работы с ним:
private File extractXmlFile(ZipInputStream zipInputStream) throws IOException {
File tempFile = File.createTempFile("temp", ".xml"); // создаем временный файл
try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = zipInputStream.read(buffer)) != -1) { // читаем данные из стрима, пока не достигнем конца файла
fileOutputStream.write(buffer, 0, bytesRead);
}
}
return tempFile;
}
DOM-объекты
У любого элемента в XML-документе есть свои атрибуты, такие как ID у фигур, или имена и значения (N, V) у ячеек. Создадим абстрактный класс Dom с полем attributes. Также создадим его классы-наследники, соответствующие элементам XML-документа Shape, Section, Cell, Row, элементам верхнего уровня Masters и Pages.
public abstract class Dom {
private Map<String, String> attributes
}
public class Pages extends Dom {
private List<Page> pageList;
}
public class Shape extends Dom {
private List<Section> sections;
private List<Cell> cells;
private Text text;
private List<Connect> connects;
private List<Shape> shapes;
}
public class Section extends Dom {
private List<Row> rows;
}
public class Text extends Dom {
private String contents;
private CharRowProperties cp;
}
При парсинге я использовал классы для обработки XML-документов DocumentBuilderFactory и DocumentBuilder из пакета javax.xml.parsers и интерфейсы Entity, Node и NodeList из пакета org.w3c.dom, представляющие «составные части» XML-документа.
Пример парсинга
Рассмотрим для примера парсинг файла page1.xml, в нём содержится максимальное количество полезной информации о схеме.
public Dom parseXml(File xmlFile) throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // часть, общая для парсинга любого XML-файла Visio
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(xmlFile);
Element root = document.getDocumentElement();
// Здесь мы можем определить, какой файл пришел на вход в метод parseXml(), запросив имя внешнего элемента структуры.
// Под каждый XML-файл Visio я создал отдельный метод, чтобы не перебирать всевозможные элементы структуры, а использовать только те, которые имеются в этом файле.
switch (root.getNodeName()) {
case "Pages" -> {
return parsePages(root); // метод для получения данных из элемента Pages (файл pages.xml)
}
case "PageContents" -> {
return parsePageContents(root); // метод для получения данных из элемента PageContents (файлы page1.xml, page2.xml и т.д.)
}
case "Masters" -> {
return parseMasters(root); // метод для получения данных из элемента Masters (файл masters.xml)
}
}
// ...
}
Так как XML-файл Visio содержит много повторяющихся структур со схожими элементами, создадим параметризированный метод, возвращающий список нужных нам элементов. Я частенько сталкивался с ситуацией, когда «здесь должен быть этот элемент вот прям железно», а его не было. Поэтому почти всё проверяю на null.
private <T extends Dom> List<T> populateElements(String tagName, Element parent, Class<T> domClass) {
List<T> domList = new ArrayList<>();
if (parent == null) {
return domList;
}
NodeList nodes = parent.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
try {
var element = (Element) nodes.item(i);
if (tagName.equals(element.getTagName())) { // проверяем по имени, что элемент в списке - тот, что нам нужен
T dom = domClass.getDeclaredConstructor().newInstance();
dom.setAttributes(getAttributes(element));
domList.add(dom);
}
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
// пишем сообщение в лог
}
}
return domList;
}
Например, для получения соединителей я вызываю это метод со следующими параметрами:
populateElements(
"Connect", // Название искомого элемента
(Element) root.getElementsByTagName("Connects").item(0), // родительский элемент
Connect.class // DOM-класс, коллекцию из которых возвращает метод
);
Напомню, что у элемента PageContents есть два дочерних типа элементов: фигуры и соединители. Нахожу первые с помощью нашего параметризированного populateElements(). Для нахождения вложенных фигур использую отдельный метод populateShapes().
private PageContents parsePageContents(Element root) {
// находим соединители (элементы структуры, которые описывают, как фигуры соединяются друг с другом)
var connects = populateElements(
"Connect",
(Element) root.getElementsByTagName("Connects").item(0),
Connect.class
);
// фигуры могут иметь вложенные фигуры, потому ищу вложенные через отдельный метод populateShapes()
var rootNodes = root.getChildNodes();
List<Shape> outerShapes = new ArrayList<>();
for (int i = 0; i < rootNodes.getLength(); i++) {
if ("Shapes".equals(rootNodes.item(i).getNodeName())) {
outerShapes = populateShapes((Element) rootNodes.item(i));
}
}
// возвращаю родительский элемент файла page1.xml
var pageContents = new PageContents();
pageContents.setShapes(outerShapes);
pageContents.setConnects(connects);
return pageContents;
}
Подобным же образом нахожу у фигуры содержимое секций и атрибутов (getSections(), getAttributes()). Содержимое методов не привожу, действуем по тому же алгоритму: ищем элементы по тегу, итерируем по найденному, в случае совпадения имени добавляем элемент к заранее созданному списку, возвращаем список элементов.
private List<Shape> populateShapes(Element element) {
List<Shape> shapes = new ArrayList<>();
List<Shape> innerShapes = new ArrayList<>();
var nodeList = element.getChildNodes();
for (int i = 0; i < nodeList.getLength(); i++) {
var shapeElement = (Element) nodeList.item(i);
var shape = new Shape();
List<Cell> cells = populateElements("Cell", shapeElement, Cell.class);
var children = shapeElement.getChildNodes();
for (int j = 0; j < children.getLength(); j++) {
if ("Shapes".equals(children.item(j).getNodeName())) {
innerShapes = populateShapes((Element) children.item(j)); // рекурсивно нахожу вложенные шейпы
}
}
shape.setSections(getSections(shapeElement));
setShapeText(shapeElement, shape);
shape.setCells(cells);
shape.setAttributes(getAttributes(shapeElement));
shape.setShapes(innerShapes);
shapes.add(shape);
}
return shapes;
}
Метод для поиска текста внутри фигуры. Я также сохранял значения свойств символов (character properties, cp), на практике они нигде нам не потребовались:
private void setShapeText(Element shapeElement, Shape shape) {
var textElement = (Element) shapeElement.getElementsByTagName("Text").item(0);
if (textElement != null) {
var text = new Text();
var contents = textElement.getTextContent();
var cpNode = textElement.getElementsByTagName("cp").item(0);
var cpElement = (Element) cpNode;
var cp = new CharRowProperties();
cp.setAttributes(getAttributes(cpElement));
text.setCp(cp);
text.setContents(contents);
shape.setText(text);
}
}
Подобным же образом можно искать и сохранять другие элементы. Также можно добавить логику для работы с многостраничными документами. В нашем случае подавляющее большинство схем были одностраничными, и такую доработку я не делал.
Запись в базу данных
Для записи я использовал уже имеющийся сервис интеграции. Полученные в результате парсинга параметры сопоставлял с соответствующим DTO и с помощью клиента feign отправлял на соответствующие endpoint-ы сервиса.
Заключение
Я рассказал о своём опыте парсинга информации из схем Visio в базу данных. Подходящих готовых решений не нашёл, поэтому сделал своё: сопоставлял разные элементы с объектами в базе и извлекал связанную с ними информацию. Если хотите дополнить или покритиковать, добро пожаловать в комментарии :)
Комментарии (4)

itGuevara
15.10.2025 06:41Делал так:
1 на входе список файлов visio, на выходе заполненный excel со всеми объектами на схемах visio со ссылками на каждый объект (на файл, лист, объект).2 excel поочередно сканирует макросом (VBA) каждый файл (открывает его, чтобы не изучать формат файла) и заполняет табличку excel, включая shp.id, shp.name, shp.master и т.п.
3 в итоге получаем репозитарий объектов. Схемы в visio рисовались по корпоративному шаблону (VAD, EPC), поэтому получался хорошо структурируемый репозитарий (по shp.master понятен тип объекта: процесс, событие и т.п.), конечно нужно было убрать двойные \ тройные пробелы в названиях, спецсимволы и т.п. (провести очистку наименований).
4 при сканировании автоматом добавлялась привязка к фигуре через штатную связку MS "excel-visio", что обеспечивало переход от строки в реестре - репозитарии к схеме (схемам), где он был найден.
Полагаю, что такой путь (VBA) проще.

Bagatur
15.10.2025 06:41Мда... Какое-то время назад была другая задача - сгенерировать visio схему исходя из табличных данных. В visio есть готовый шаблон подобного, но только для организационной диаграммы. А вот чтобы, условно, отрисовать схему сетевых подключений исходя из условного ACL, взятого из файрволлов/роутера/L3 коммутатора - вот тут и споткнулись. Разрабов, чтобы такое на VBA пытаться реализовать, в нашем подразделении нет. А штатным разрабам в организации некогда. Пытался на PlantUML - да, неплохо, но только для схем с малой вложенностью. Не подошло, увы.

itGuevara
15.10.2025 06:41сгенерировать visio схему исходя из табличных данных.
Делал подобное, но для схем VAD. В excel составляешь табличку, из нее формируешь фигуры нужного типа и коннектор между ними. Причем все это без координат (точнее у всех одна координата). Потом даешь штатную команду visio: "размести" (плюс уточнения слева - направо и т.п.) и visio сам координаты пересчитывает. Фактически тот же PlantUML, но из таблицы (отношения между объектами в табличном виде).
Если говорить не про visio, то эта же задача решалась (два варианта в excel и js) в ВРМ. Смарт-инструменты «Таблица -> Схема». Там был промежуточный этап формирования dot (в visio он был не нужен). В PlantUML "под капотом" GraphViz (dot).
Surrogate
Очень давно (в 2019 году) была написана статья Из Visio в Excel через Power Query, на основе ветки обсуждения в русскоязычном форуме MS Visio.