vsdx-js
Version:
typescript library to parse a Visio (.vsdx) file into a javascript object
215 lines (214 loc) • 8.52 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
import AdmZip from 'adm-zip';
import * as fs from 'fs';
import { parseStringPromise } from 'xml2js';
const jsonObjects = {};
let masters = [];
let pages = [];
let relationships = [];
let stylesheets = [];
export async function parseVisioFile(filePath) {
const vsdxBuffer = fs.readFileSync(filePath);
const archive = new AdmZip(vsdxBuffer);
const entries = archive.getEntries();
for (const entry of entries) {
let jsonObj = undefined;
if (entry.entryName.endsWith('.rels')) {
const xmlContent = entry.getData().toString('utf-8');
jsonObj = await parseStringPromise(xmlContent);
relationships.push.apply(relationships, parseRelationships(jsonObj));
}
if (entry.entryName.endsWith('.xml')) {
const xmlContent = entry.getData().toString('utf-8');
jsonObj = await parseStringPromise(xmlContent);
const fileName = getEntryName(entry.entryName);
switch (fileName) {
case 'document':
stylesheets = parseDocumentProperties(jsonObj);
break;
case 'masters':
masters = parseMastersFile(jsonObj);
break;
case 'pages':
pages = parsePagesFile(jsonObj);
break;
default:
jsonObjects[fileName] = jsonObj;
}
}
}
for (const page of pages) {
const rel = relationships.find((relation) => relation.Id === page.RelationshipId && relation.Type === 'Page');
if (rel) {
const entryName = getEntryName(rel.Target);
const pageObject = jsonObjects[entryName];
page.Shapes = getShapes(pageObject);
}
}
return {
Masters: masters,
Pages: pages,
Stylesheets: stylesheets,
Relationships: relationships
};
}
const parseRelationships = (jsonObj) => {
const entries = [];
const relObjects = jsonObj['Relationships']['Relationship'];
for (let i = 0; i < relObjects.length; i++) {
const relationship = {};
relationship.Id = relObjects[i]['$']['Id'];
relationship.Target = relObjects[i]['$']['Target'];
const type = getRelationshipType(relObjects[i]['$']['Type']);
if (type) {
relationship.Type = type;
}
entries.push(relationship);
}
return entries;
};
const parseDocumentProperties = (jsonObj) => {
const styleSheets = [];
const stylesheetObjects = jsonObj['VisioDocument']['StyleSheets'][0]['StyleSheet'];
for (let i = 0; i < stylesheetObjects.length; i++) {
const sheet = {};
sheet.ID = stylesheetObjects[i]['$']['ID'];
sheet.Name = stylesheetObjects[i]['$']['Name'];
sheet.LineStyleRefId = stylesheetObjects[i]['$']['LineStyle'];
sheet.FillStyleRefId = stylesheetObjects[i]['$']['FillStyle'];
sheet.TextStyleRefId = stylesheetObjects[i]['$']['TextStyle'];
sheet.Style = stylesheetObjects[i]['Cell'];
styleSheets.push(sheet);
}
return styleSheets;
};
const parseMastersFile = (jsonObj) => {
const masters = [];
const masterObjects = jsonObj['Masters']['Master'];
for (let i = 0; i < masterObjects.length; i++) {
const master = {};
master.Id = masterObjects[i]['$']['ID'];
master.Name = masterObjects[i]['$']['Name'];
master.UniqueID = masterObjects[i]['$']['UniqueID'];
master.MasterType = masterObjects[i]['$']['MasterType'];
master.RelationshipId = masterObjects[i]['Rel'][0]['$']['r:id'];
master.Hidden = masterObjects[i]['$']['Hidden'];
master.LineStyleRefId = masterObjects[i]['PageSheet']['LineStyle'];
master.FillStyleRefId = masterObjects[i]['PageSheet']['FillStyle'];
master.TextStyleRefId = masterObjects[i]['PageSheet']['TextStyle'];
masters.push(master);
}
return masters;
};
const parsePagesFile = (jsonObj) => {
const pages = [];
const objects = jsonObj['Pages']['Page'];
for (let i = 0; i < objects.length; i++) {
const page = {};
page.Id = objects[i]['$']['ID'];
page.Name = objects[i]['$']['Name'];
page.RelationshipId = objects[i]['Rel'][0]['$']['r:id'];
pages.push(page);
}
return pages;
};
const getShapes = (pageObject) => {
let shapes = [];
const shapeObjects = pageObject['PageContents']['Shapes'][0];
const connectObjects = pageObject['PageContents']['Connects'];
try {
const shapeCount = shapeObjects['Shape'].length;
for (let i = 0; i < shapeCount; i++) {
const shape = { Type: 'unknown', IsEdge: false, Label: '' };
const shapeContainer = shapeObjects['Shape'][i];
const cells = shapeContainer['Cell'];
shape.Id = shapeContainer['$']['ID'];
shape.MasterId = shapeContainer['$']['Master'];
const master = masters.find((master) => master.Id === shape.MasterId);
if (master) {
shape.Type = master.Name;
const masterRel = relationships.find((relation) => relation.Id === master.RelationshipId && relation.Type === 'Master');
if (masterRel) {
const masterObj = jsonObjects[getEntryName(masterRel?.Target)];
if (shape.Type === 'Dynamic connector' && connectObjects) {
const { fromNode, toNode } = getConnectorNodes(connectObjects[0]['Connect'], shape.Id);
shape.FromNode = fromNode;
shape.ToNode = toNode;
shape.IsEdge = true;
}
}
}
if (shapeContainer['Text'] && shapeContainer['Text'][0]) {
shape.Label = shapeContainer['Text'][0]['_'].replace(/\r?\n|\r/g, '').trim();
}
shape.Style = getStyleFromObject(cells);
shapes.push(shape);
}
}
catch (e) {
console.log(e);
}
return shapes;
};
const getConnectorNodes = (connectObjects, shapeId) => {
let fromNode = '';
let toNode = '';
try {
const connects = connectObjects.filter(
// @ts-ignore
(connect) => connect['$'].FromSheet === shapeId);
// @ts-ignore
const from = connects.find((c) => c['$'].FromCell === 'BeginX')['$'];
// @ts-ignore
const to = connects.find((c) => c['$'].FromCell === 'EndX')['$'];
fromNode = from.ToSheet;
toNode = to.ToSheet;
}
catch (e) {
console.log(e);
}
return { fromNode, toNode };
};
const getStyleFromObject = (cells) => {
const style = {};
const lineWeightInPixels = parseFloat(getValueFromCell(cells, 'LineWeight')) * 96;
style.LineWeight = lineWeightInPixels;
style.LineColor = getValueFromCell(cells, 'LineColor');
style.LinePattern = parseFloat(getValueFromCell(cells, 'LinePattern'));
style.Rounding = parseFloat(getValueFromCell(cells, 'Rounding'));
style.BeginArrow = parseFloat(getValueFromCell(cells, 'BeginArrow'));
style.BeginArrowSize = parseFloat(getValueFromCell(cells, 'BeginArrowSize'));
style.EndArrow = parseFloat(getValueFromCell(cells, 'EndArrow'));
style.EndArrowSize = parseFloat(getValueFromCell(cells, 'EndArrowSize'));
style.LineCap = parseFloat(getValueFromCell(cells, 'LineCap'));
style.FillForeground = getValueFromCell(cells, 'FillForegnd');
style.FillBackground = getValueFromCell(cells, 'FillBkgnd');
style.TextColor = getValueFromCell(cells, 'Color');
style.FillPattern = parseFloat(getValueFromCell(cells, 'FillPattern'));
return style;
};
const getValueFromCell = (cells, field) => {
let value = '';
for (let i = 0; i < cells.length; i++) {
const cell = cells[i];
if (cell['$']['N'] === field) {
value = cell['$']['V'];
}
}
return value;
};
const getRelationshipType = (type) => {
const index = type.lastIndexOf('/') + 1;
switch (type.substring(index)) {
case 'master':
return 'Master';
case 'page':
return 'Page';
default:
return undefined;
}
};
const getEntryName = (entryName) => {
const nameStartIndex = entryName.lastIndexOf('/') + 1;
return entryName.substring(nameStartIndex).replace('.xml', '');
};