import JSZip from 'jszip' import { readXmlFile } from './readXmlFile' import { getBorder } from './border' import { getSlideBackgroundFill, getShapeFill, getSolidFill, getPicFill } from './fill' import { getChartInfo } from './chart' import { getVerticalAlign } from './align' import { getPosition, getSize } from './position' import { genTextBody } from './text' import { getCustomShapePath } from './shape' import { extractFileExtension, base64ArrayBuffer, getTextByPathList, angleToDegrees, getMimeType, isVideoLink, escapeHtml, hasValidText } from './utils' import { getShadow } from './shadow' import { getTableBorders, getTableCellParams, getTableRowParams } from './table' import { RATIO_EMUs_Points } from './constants' import { findOMath, latexFormart, parseOMath } from './math' export async function parse (file) { const slides = [] const zip = await JSZip.loadAsync(file) const filesInfo = await getContentTypes(zip) const { width, height, defaultTextStyle } = await getSlideInfo(zip) // 获取所有主题文件 const themeMap = await getAllThemes(zip) for (const filename of filesInfo.slides) { // 为每个幻灯片获取对应的主题 const { themeContent, themeColors } = await getSlideTheme( zip, filename, themeMap ) const singleSlide = await processSingleSlide( zip, filename, themeContent, defaultTextStyle ) slides.push({ ...singleSlide, themeColors // 为每个幻灯片添加其对应的主题颜色 }) } return { slides, size: { width, height } } } async function getSlideInfo (zip) { const content = await readXmlFile(zip, 'ppt/presentation.xml') const sldSzAttrs = content['p:presentation']['p:sldSz']['attrs'] const defaultTextStyle = content['p:presentation']['p:defaultTextStyle'] return { width: parseInt(sldSzAttrs['cx']) * RATIO_EMUs_Points, height: parseInt(sldSzAttrs['cy']) * RATIO_EMUs_Points, defaultTextStyle } } async function getContentTypes (zip) { const ContentTypesJson = await readXmlFile(zip, '[Content_Types].xml') const subObj = ContentTypesJson['Types']['Override'] let slidesLocArray = [] let slideLayoutsLocArray = [] let themeLocArray = [] for (const item of subObj) { switch (item['attrs']['ContentType']) { case 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml': slidesLocArray.push(item['attrs']['PartName'].substr(1)) break case 'application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml': slideLayoutsLocArray.push(item['attrs']['PartName'].substr(1)) break case 'application/vnd.openxmlformats-officedocument.theme+xml': themeLocArray.push(item['attrs']['PartName'].substr(1)) break default: } } const sortSlideXml = (p1, p2) => { const n1 = +/(\d+)\.xml/.exec(p1)[1] const n2 = +/(\d+)\.xml/.exec(p2)[1] return n1 - n2 } slidesLocArray = slidesLocArray.sort(sortSlideXml) slideLayoutsLocArray = slideLayoutsLocArray.sort(sortSlideXml) themeLocArray = themeLocArray.sort(sortSlideXml) return { slides: slidesLocArray, slideLayouts: slideLayoutsLocArray, themes: themeLocArray } } // 获取所有主题文件并建立映射关系 async function getAllThemes (zip) { const themeMap = new Map() const filesInfo = await getContentTypes(zip) // 从 Content_Types.xml 中获取主题文件路径 for (const themePath of filesInfo.themes) { const themeContent = await readXmlFile(zip, themePath) themeMap.set(themePath, { themeContent, masterId: null }) } // 获取母版与主题的对应关系 const preResContent = await readXmlFile( zip, 'ppt/_rels/presentation.xml.rels' ) const presentationContent = await readXmlFile(zip, 'ppt/presentation.xml') const masterIdList = getTextByPathList(presentationContent, [ 'p:presentation', 'p:sldMasterIdLst', 'p:sldMasterId' ]) if (masterIdList) { const relationshipArray = preResContent['Relationships']['Relationship'] const masters = Array.isArray(masterIdList) ? masterIdList : [masterIdList] for (const master of masters) { const masterId = master['attrs']['r:id'] const masterRel = Array.isArray(relationshipArray) ? relationshipArray.find((rel) => rel['attrs']['Id'] === masterId) : relationshipArray['attrs']['Id'] === masterId ? relationshipArray : null if (masterRel) { const masterTarget = masterRel['attrs']['Target'].replace( '../', 'ppt/' ) const masterRelsPath = masterTarget.replace( 'slideMasters/slideMaster', 'slideMasters/_rels/slideMaster' ) + '.rels' const masterRels = await readXmlFile(zip, masterRelsPath) if (masterRels && masterRels['Relationships']) { const relationships = masterRels['Relationships']['Relationship'] const themeRel = Array.isArray(relationships) ? relationships.find((rel) => rel['attrs']['Type'].endsWith('/theme') ) : relationships['attrs']['Type'].endsWith('/theme') ? relationships : null if (themeRel) { const themeURI = themeRel['attrs']['Target'].replace('../', '') const themePath = 'ppt/' + themeURI // 更新主题映射中的 masterId if (themeMap.has(themePath)) { const themeData = themeMap.get(themePath) themeData.masterId = masterId themeMap.set(masterTarget, themeData) } } } } } } return themeMap } // 获取单个幻灯片对应的主题 async function getSlideTheme (zip, slideFilename, themeMap) { try { // 获取幻灯片的布局信息 const slideRelsPath = slideFilename.replace('slides/slide', 'slides/_rels/slide') + '.rels' const slideRels = await readXmlFile(zip, slideRelsPath) if (!slideRels?.Relationships?.Relationship) { return { themeContent: {}, themeColors: [] } } // 获取布局文件路径 const relationships = slideRels['Relationships']['Relationship'] const layoutRel = Array.isArray(relationships) ? relationships.find((rel) => rel['attrs']['Type'].endsWith('/slideLayout') ) : relationships['attrs']['Type'].endsWith('/slideLayout') ? relationships : null if (!layoutRel) return { themeContent: {}, themeColors: [] } const layoutPath = 'ppt/' + layoutRel['attrs']['Target'].replace('../', '') const layoutRelsPath = layoutPath.replace( 'slideLayouts/slideLayout', 'slideLayouts/_rels/slideLayout' ) + '.rels' // 获取母版文件路径 const layoutRels = await readXmlFile(zip, layoutRelsPath) if (!layoutRels?.Relationships?.Relationship) { return { themeContent: {}, themeColors: [] } } const masterRelationships = layoutRels['Relationships']['Relationship'] const masterRel = Array.isArray(masterRelationships) ? masterRelationships.find((rel) => rel['attrs']['Type'].endsWith('/slideMaster') ) : masterRelationships['attrs']['Type'].endsWith('/slideMaster') ? masterRelationships : null if (!masterRel) return { themeContent: {}, themeColors: [] } const masterPath = 'ppt/' + masterRel['attrs']['Target'].replace('../', '') const masterRelsPath = masterPath.replace( 'slideMasters/slideMaster', 'slideMasters/_rels/slideMaster' ) + '.rels' // 获取主题关系 const masterRels = await readXmlFile(zip, masterRelsPath) if (!masterRels?.Relationships?.Relationship) { return { themeContent: {}, themeColors: [] } } const themeRelationships = masterRels['Relationships']['Relationship'] const themeRel = Array.isArray(themeRelationships) ? themeRelationships.find((rel) => rel['attrs']['Type'].endsWith('/theme') ) : themeRelationships['attrs']['Type'].endsWith('/theme') ? themeRelationships : null if (!themeRel) return { themeContent: {}, themeColors: [] } const themePath = 'ppt/' + themeRel['attrs']['Target'].replace('../', '') const themeData = themeMap.get(themePath) if (!themeData?.themeContent) return { themeContent: {}, themeColors: [] } // 提取主题颜色 const themeColors = [] const clrScheme = getTextByPathList(themeData.themeContent, [ 'a:theme', 'a:themeElements', 'a:clrScheme' ]) if (clrScheme) { for (let i = 1; i <= 6; i++) { if (clrScheme[`a:accent${i}`] === undefined) break const color = getTextByPathList(clrScheme, [ `a:accent${i}`, 'a:srgbClr', 'attrs', 'val' ]) if (color) themeColors.push('#' + color) } } return { themeContent: themeData.themeContent, themeColors } } catch (error) { return { themeContent: {}, themeColors: [] } } } async function processSingleSlide ( zip, sldFileName, themeContent, defaultTextStyle ) { const resName = sldFileName.replace('slides/slide', 'slides/_rels/slide') + '.rels' const resContent = await readXmlFile(zip, resName) let relationshipArray = resContent['Relationships']['Relationship'] if (relationshipArray.constructor !== Array) { relationshipArray = [relationshipArray] } let noteFilename = '' let layoutFilename = '' let masterFilename = '' let themeFilename = '' let diagramFilename = '' const slideResObj = {} const layoutResObj = {} const masterResObj = {} const themeResObj = {} const diagramResObj = {} for (const relationshipArrayItem of relationshipArray) { switch (relationshipArrayItem['attrs']['Type']) { case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout': layoutFilename = relationshipArrayItem['attrs']['Target'].replace( '../', 'ppt/' ) break case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide': noteFilename = relationshipArrayItem['attrs']['Target'].replace( '../', 'ppt/' ) break case 'http://schemas.microsoft.com/office/2007/relationships/diagramDrawing': diagramFilename = relationshipArrayItem['attrs']['Target'].replace( '../', 'ppt/' ) slideResObj[relationshipArrayItem['attrs']['Id']] = { type: relationshipArrayItem['attrs']['Type'].replace( 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/', '' ), target: relationshipArrayItem['attrs']['Target'].replace( '../', 'ppt/' ) } break case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image': case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart': case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink': default: slideResObj[relationshipArrayItem['attrs']['Id']] = { type: relationshipArrayItem['attrs']['Type'].replace( 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/', '' ), target: relationshipArrayItem['attrs']['Target'].replace( '../', 'ppt/' ) } } } const slideNotesContent = await readXmlFile(zip, noteFilename) const note = getNote(slideNotesContent) const slideLayoutContent = await readXmlFile(zip, layoutFilename) const slideLayoutTables = await indexNodes(slideLayoutContent) const slideLayoutResFilename = layoutFilename.replace( 'slideLayouts/slideLayout', 'slideLayouts/_rels/slideLayout' ) + '.rels' const slideLayoutResContent = await readXmlFile(zip, slideLayoutResFilename) relationshipArray = slideLayoutResContent['Relationships']['Relationship'] if (relationshipArray.constructor !== Array) { relationshipArray = [relationshipArray] } for (const relationshipArrayItem of relationshipArray) { switch (relationshipArrayItem['attrs']['Type']) { case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster': masterFilename = relationshipArrayItem['attrs']['Target'].replace( '../', 'ppt/' ) break default: layoutResObj[relationshipArrayItem['attrs']['Id']] = { type: relationshipArrayItem['attrs']['Type'].replace( 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/', '' ), target: relationshipArrayItem['attrs']['Target'].replace( '../', 'ppt/' ) } } } const slideMasterContent = await readXmlFile(zip, masterFilename) const slideMasterTextStyles = getTextByPathList(slideMasterContent, [ 'p:sldMaster', 'p:txStyles' ]) const slideMasterTables = indexNodes(slideMasterContent) const slideMasterResFilename = masterFilename.replace( 'slideMasters/slideMaster', 'slideMasters/_rels/slideMaster' ) + '.rels' const slideMasterResContent = await readXmlFile(zip, slideMasterResFilename) relationshipArray = slideMasterResContent['Relationships']['Relationship'] if (relationshipArray.constructor !== Array) { relationshipArray = [relationshipArray] } for (const relationshipArrayItem of relationshipArray) { switch (relationshipArrayItem['attrs']['Type']) { case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme': themeFilename = relationshipArrayItem['attrs']['Target'].replace( '../', 'ppt/' ) break default: masterResObj[relationshipArrayItem['attrs']['Id']] = { type: relationshipArrayItem['attrs']['Type'].replace( 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/', '' ), target: relationshipArrayItem['attrs']['Target'].replace( '../', 'ppt/' ) } } } if (themeFilename) { const themeName = themeFilename.split('/').pop() const themeResFileName = themeFilename.replace(themeName, '_rels/' + themeName) + '.rels' const themeResContent = await readXmlFile(zip, themeResFileName) if (themeResContent) { relationshipArray = themeResContent['Relationships']['Relationship'] if (relationshipArray) { if (relationshipArray.constructor !== Array) { relationshipArray = [relationshipArray] } for (const relationshipArrayItem of relationshipArray) { themeResObj[relationshipArrayItem['attrs']['Id']] = { type: relationshipArrayItem['attrs']['Type'].replace( 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/', '' ), target: relationshipArrayItem['attrs']['Target'].replace( '../', 'ppt/' ) } } } } } let digramFileContent = {} if (diagramFilename) { const diagName = diagramFilename.split('/').pop() const diagramResFileName = diagramFilename.replace(diagName, '_rels/' + diagName) + '.rels' digramFileContent = await readXmlFile(zip, diagramFilename) if (digramFileContent) { const digramFileContentObjToStr = JSON.stringify( digramFileContent ).replace(/dsp:/g, 'p:') digramFileContent = JSON.parse(digramFileContentObjToStr) } const digramResContent = await readXmlFile(zip, diagramResFileName) if (digramResContent) { relationshipArray = digramResContent['Relationships']['Relationship'] if (relationshipArray.constructor !== Array) { relationshipArray = [relationshipArray] } for (const relationshipArrayItem of relationshipArray) { diagramResObj[relationshipArrayItem['attrs']['Id']] = { type: relationshipArrayItem['attrs']['Type'].replace( 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/', '' ), target: relationshipArrayItem['attrs']['Target'].replace( '../', 'ppt/' ) } } } } const tableStyles = await readXmlFile(zip, 'ppt/tableStyles.xml') const slideContent = await readXmlFile(zip, sldFileName) const nodes = slideContent['p:sld']['p:cSld']['p:spTree'] const warpObj = { zip, slideLayoutContent, slideLayoutTables, slideMasterContent, slideMasterTables, slideContent, tableStyles, slideResObj, slideMasterTextStyles, layoutResObj, masterResObj, themeContent, themeResObj, digramFileContent, diagramResObj, defaultTextStyle } const layoutElements = await getLayoutElements(warpObj) const fill = await getSlideBackgroundFill(warpObj) const elements = [] for (const nodeKey in nodes) { if (nodes[nodeKey].constructor !== Array) nodes[nodeKey] = [nodes[nodeKey]] for (const node of nodes[nodeKey]) { const ret = await processNodesInSlide( nodeKey, node, nodes, warpObj, 'slide' ) if (ret) elements.push(ret) } } return { fill, elements, layoutElements, note } } function getNote (noteContent) { let text = '' let spNodes = getTextByPathList(noteContent, [ 'p:notes', 'p:cSld', 'p:spTree', 'p:sp' ]) if (!spNodes) return '' if (spNodes.constructor !== Array) spNodes = [spNodes] for (const spNode of spNodes) { let rNodes = getTextByPathList(spNode, ['p:txBody', 'a:p', 'a:r']) if (!rNodes) continue if (rNodes.constructor !== Array) rNodes = [rNodes] for (const rNode of rNodes) { const t = getTextByPathList(rNode, ['a:t']) if (t && typeof t === 'string') text += t } } return text } async function getLayoutElements (warpObj) { const elements = [] const slideLayoutContent = warpObj['slideLayoutContent'] const slideMasterContent = warpObj['slideMasterContent'] const nodesSldLayout = getTextByPathList(slideLayoutContent, [ 'p:sldLayout', 'p:cSld', 'p:spTree' ]) const nodesSldMaster = getTextByPathList(slideMasterContent, [ 'p:sldMaster', 'p:cSld', 'p:spTree' ]) const showMasterSp = getTextByPathList(slideLayoutContent, [ 'p:sldLayout', 'attrs', 'showMasterSp' ]) if (nodesSldLayout) { for (const nodeKey in nodesSldLayout) { if (nodesSldLayout[nodeKey].constructor === Array) { for (let i = 0; i < nodesSldLayout[nodeKey].length; i++) { const ph = getTextByPathList(nodesSldLayout[nodeKey][i], [ 'p:nvSpPr', 'p:nvPr', 'p:ph' ]) if (!ph) { const ret = await processNodesInSlide( nodeKey, nodesSldLayout[nodeKey][i], nodesSldLayout, warpObj, 'slideLayoutBg' ) if (ret) elements.push(ret) } } } else { const ph = getTextByPathList(nodesSldLayout[nodeKey], [ 'p:nvSpPr', 'p:nvPr', 'p:ph' ]) if (!ph) { const ret = await processNodesInSlide( nodeKey, nodesSldLayout[nodeKey], nodesSldLayout, warpObj, 'slideLayoutBg' ) if (ret) elements.push(ret) } } } } if (nodesSldMaster && showMasterSp !== '0') { for (const nodeKey in nodesSldMaster) { if (nodesSldMaster[nodeKey].constructor === Array) { for (let i = 0; i < nodesSldMaster[nodeKey].length; i++) { const ph = getTextByPathList(nodesSldMaster[nodeKey][i], [ 'p:nvSpPr', 'p:nvPr', 'p:ph' ]) if (!ph) { const ret = await processNodesInSlide( nodeKey, nodesSldMaster[nodeKey][i], nodesSldMaster, warpObj, 'slideMasterBg' ) if (ret) elements.push(ret) } } } else { const ph = getTextByPathList(nodesSldMaster[nodeKey], [ 'p:nvSpPr', 'p:nvPr', 'p:ph' ]) if (!ph) { const ret = await processNodesInSlide( nodeKey, nodesSldMaster[nodeKey], nodesSldMaster, warpObj, 'slideMasterBg' ) if (ret) elements.push(ret) } } } } return elements } function indexNodes (content) { const keys = Object.keys(content) const spTreeNode = content[keys[0]]['p:cSld']['p:spTree'] const idTable = {} const idxTable = {} const typeTable = {} for (const key in spTreeNode) { if (key === 'p:nvGrpSpPr' || key === 'p:grpSpPr') continue const targetNode = spTreeNode[key] if (targetNode.constructor === Array) { for (const targetNodeItem of targetNode) { const nvSpPrNode = targetNodeItem['p:nvSpPr'] const id = getTextByPathList(nvSpPrNode, ['p:cNvPr', 'attrs', 'id']) const idx = getTextByPathList(nvSpPrNode, [ 'p:nvPr', 'p:ph', 'attrs', 'idx' ]) const type = getTextByPathList(nvSpPrNode, [ 'p:nvPr', 'p:ph', 'attrs', 'type' ]) if (id) idTable[id] = targetNodeItem if (idx) idxTable[idx] = targetNodeItem if (type) typeTable[type] = targetNodeItem } } else { const nvSpPrNode = targetNode['p:nvSpPr'] const id = getTextByPathList(nvSpPrNode, ['p:cNvPr', 'attrs', 'id']) const idx = getTextByPathList(nvSpPrNode, [ 'p:nvPr', 'p:ph', 'attrs', 'idx' ]) const type = getTextByPathList(nvSpPrNode, [ 'p:nvPr', 'p:ph', 'attrs', 'type' ]) if (id) idTable[id] = targetNode if (idx) idxTable[idx] = targetNode if (type) typeTable[type] = targetNode } } return { idTable, idxTable, typeTable } } async function processNodesInSlide (nodeKey, nodeValue, nodes, warpObj, source) { let json switch (nodeKey) { case 'p:sp': // Shape, Text json = await processSpNode(nodeValue, nodes, warpObj, source) break case 'p:cxnSp': // Shape, Text json = await processCxnSpNode(nodeValue, nodes, warpObj, source) break case 'p:pic': // Image, Video, Audio json = await processPicNode(nodeValue, warpObj, source) break case 'p:graphicFrame': // Chart, Diagram, Table json = await processGraphicFrameNode(nodeValue, warpObj, source) break case 'p:grpSp': json = await processGroupSpNode(nodeValue, warpObj, source) break case 'mc:AlternateContent': if ( getTextByPathList(nodeValue, ['mc:Fallback', 'p:grpSpPr', 'a:xfrm']) ) { json = await processGroupSpNode( getTextByPathList(nodeValue, ['mc:Fallback']), warpObj, source ) } else if (getTextByPathList(nodeValue, ['mc:Choice'])) { json = await processMathNode(nodeValue, warpObj, source) } break default: } return json } async function processMathNode (node, warpObj, source) { const choice = getTextByPathList(node, ['mc:Choice']) const fallback = getTextByPathList(node, ['mc:Fallback']) const order = node['attrs']['order'] const xfrmNode = getTextByPathList(choice, ['p:sp', 'p:spPr', 'a:xfrm']) const { top, left } = getPosition(xfrmNode, undefined, undefined) const { width, height } = getSize(xfrmNode, undefined, undefined) const oMath = findOMath(choice)[0] const latex = latexFormart(parseOMath(oMath)) const blipFill = getTextByPathList(fallback, [ 'p:sp', 'p:spPr', 'a:blipFill' ]) const picBase64 = await getPicFill(source, blipFill, warpObj) return { type: 'math', top, left, width, height, latex, picBase64, order } } async function processGroupSpNode (node, warpObj, source) { const order = node['attrs']['order'] const xfrmNode = getTextByPathList(node, ['p:grpSpPr', 'a:xfrm']) if (!xfrmNode) return null const x = parseInt(xfrmNode['a:off']['attrs']['x']) * RATIO_EMUs_Points const y = parseInt(xfrmNode['a:off']['attrs']['y']) * RATIO_EMUs_Points const chx = parseInt(xfrmNode['a:chOff']['attrs']['x']) * RATIO_EMUs_Points const chy = parseInt(xfrmNode['a:chOff']['attrs']['y']) * RATIO_EMUs_Points const cx = parseInt(xfrmNode['a:ext']['attrs']['cx']) * RATIO_EMUs_Points const cy = parseInt(xfrmNode['a:ext']['attrs']['cy']) * RATIO_EMUs_Points const chcx = parseInt(xfrmNode['a:chExt']['attrs']['cx']) * RATIO_EMUs_Points const chcy = parseInt(xfrmNode['a:chExt']['attrs']['cy']) * RATIO_EMUs_Points const isFlipV = getTextByPathList(xfrmNode, ['attrs', 'flipV']) === '1' const isFlipH = getTextByPathList(xfrmNode, ['attrs', 'flipH']) === '1' let rotate = getTextByPathList(xfrmNode, ['attrs', 'rot']) || 0 if (rotate) rotate = angleToDegrees(rotate) const ws = cx / chcx const hs = cy / chcy const elements = [] for (const nodeKey in node) { if (node[nodeKey].constructor === Array) { for (const item of node[nodeKey]) { const ret = await processNodesInSlide( nodeKey, item, node, warpObj, source ) if (ret) { // 如果是嵌套的组合,需要调整其子元素的位置和大小 if (ret.type === 'group') { ret.elements = ret.elements.map((element) => ({ ...element, left: element.left * ws, top: element.top * hs, width: element.width * ws, height: element.height * hs })) } elements.push(ret) } } } else { const ret = await processNodesInSlide( nodeKey, node[nodeKey], node, warpObj, source ) if (ret) { // 如果是嵌套的组合,需要调整其子元素的位置和大小 if (ret.type === 'group') { ret.elements = ret.elements.map((element) => ({ ...element, left: element.left * ws, top: element.top * hs, width: element.width * ws, height: element.height * hs })) } elements.push(ret) } } } return { type: 'group', top: y, left: x, width: cx, height: cy, rotate, order, isFlipV, isFlipH, elements: elements.map((element) => ({ ...element, left: (element.left - chx) * ws, top: (element.top - chy) * hs, width: element.width * ws, height: element.height * hs })) } } async function processSpNode (node, pNode, warpObj, source) { const name = getTextByPathList(node, [ 'p:nvSpPr', 'p:cNvPr', 'attrs', 'name' ]) const idx = getTextByPathList(node, [ 'p:nvSpPr', 'p:nvPr', 'p:ph', 'attrs', 'idx' ]) let type = getTextByPathList(node, [ 'p:nvSpPr', 'p:nvPr', 'p:ph', 'attrs', 'type' ]) const order = getTextByPathList(node, ['attrs', 'order']) let slideLayoutSpNode, slideMasterSpNode if (type) { if (idx) { slideLayoutSpNode = warpObj['slideLayoutTables']['typeTable'][type] slideMasterSpNode = warpObj['slideMasterTables']['typeTable'][type] } else { slideLayoutSpNode = warpObj['slideLayoutTables']['typeTable'][type] slideMasterSpNode = warpObj['slideMasterTables']['typeTable'][type] } } else if (idx) { slideLayoutSpNode = warpObj['slideLayoutTables']['idxTable'][idx] slideMasterSpNode = warpObj['slideMasterTables']['idxTable'][idx] } if (!type) { const txBoxVal = getTextByPathList(node, [ 'p:nvSpPr', 'p:cNvSpPr', 'attrs', 'txBox' ]) if (txBoxVal === '1') type = 'text' } if (!type) { type = getTextByPathList(slideLayoutSpNode, [ 'p:nvSpPr', 'p:nvPr', 'p:ph', 'attrs', 'type' ]) } if (!type) { type = getTextByPathList(slideMasterSpNode, [ 'p:nvSpPr', 'p:nvPr', 'p:ph', 'attrs', 'type' ]) } if (!type) { if (source === 'diagramBg') type = 'diagram' else type = 'obj' } return await genShape( node, pNode, slideLayoutSpNode, slideMasterSpNode, name, type, order, warpObj, source ) } async function processCxnSpNode (node, pNode, warpObj, source) { const name = node['p:nvCxnSpPr']['p:cNvPr']['attrs']['name'] const type = node['p:nvCxnSpPr']['p:nvPr']['p:ph'] === undefined ? undefined : node['p:nvSpPr']['p:nvPr']['p:ph']['attrs']['type'] const order = node['attrs']['order'] return await genShape( node, pNode, undefined, undefined, name, type, order, warpObj, source ) } async function genShape ( node, pNode, slideLayoutSpNode, slideMasterSpNode, name, type, order, warpObj, source ) { const xfrmList = ['p:spPr', 'a:xfrm'] const slideXfrmNode = getTextByPathList(node, xfrmList) const slideLayoutXfrmNode = getTextByPathList(slideLayoutSpNode, xfrmList) const slideMasterXfrmNode = getTextByPathList(slideMasterSpNode, xfrmList) const shapType = getTextByPathList(node, [ 'p:spPr', 'a:prstGeom', 'attrs', 'prst' ]) const custShapType = getTextByPathList(node, ['p:spPr', 'a:custGeom']) const { top, left } = getPosition( slideXfrmNode, slideLayoutXfrmNode, slideMasterXfrmNode ) const { width, height } = getSize( slideXfrmNode, slideLayoutXfrmNode, slideMasterXfrmNode ) const isFlipV = getTextByPathList(slideXfrmNode, ['attrs', 'flipV']) === '1' const isFlipH = getTextByPathList(slideXfrmNode, ['attrs', 'flipH']) === '1' const rotate = angleToDegrees( getTextByPathList(slideXfrmNode, ['attrs', 'rot']) ) const txtXframeNode = getTextByPathList(node, ['p:txXfrm']) let txtRotate if (txtXframeNode) { const txtXframeRot = getTextByPathList(txtXframeNode, ['attrs', 'rot']) if (txtXframeRot) txtRotate = angleToDegrees(txtXframeRot) + 90 } else txtRotate = rotate let content = '' if (node['p:txBody']) { content = genTextBody( node['p:txBody'], node, slideLayoutSpNode, type, warpObj ) } const { borderColor, borderWidth, borderType, strokeDasharray } = getBorder( node, type, warpObj ) const fill = (await getShapeFill(node, pNode, undefined, warpObj, source)) || '' let shadow const outerShdwNode = getTextByPathList(node, [ 'p:spPr', 'a:effectLst', 'a:outerShdw' ]) if (outerShdwNode) shadow = getShadow(outerShdwNode, warpObj) const vAlign = getVerticalAlign( node, slideLayoutSpNode, slideMasterSpNode, type ) const isVertical = getTextByPathList(node, ['p:txBody', 'a:bodyPr', 'attrs', 'vert']) === 'eaVert' const data = { left, top, width, height, borderColor, borderWidth, borderType, borderStrokeDasharray: strokeDasharray, fill, content, isFlipV, isFlipH, rotate, vAlign, name, order } if (shadow) data.shadow = shadow if (custShapType && type !== 'diagram') { const ext = getTextByPathList(slideXfrmNode, ['a:ext', 'attrs']) const w = parseInt(ext['cx']) * RATIO_EMUs_Points const h = parseInt(ext['cy']) * RATIO_EMUs_Points const d = getCustomShapePath(custShapType, w, h) if (data.content && !hasValidText(data.content)) data.content = '' return { ...data, type: 'shape', shapType: 'custom', path: d } } if (shapType && (type === 'obj' || !type)) { if (data.content && !hasValidText(data.content)) data.content = '' return { ...data, type: 'shape', shapType } } return { ...data, type: 'text', isVertical, rotate: txtRotate } } async function processPicNode (node, warpObj, source) { let resObj if (source === 'slideMasterBg') resObj = warpObj['masterResObj'] else if (source === 'slideLayoutBg') resObj = warpObj['layoutResObj'] else resObj = warpObj['slideResObj'] const order = node['attrs']['order'] const rid = node['p:blipFill']['a:blip']['attrs']['r:embed'] const imgName = resObj[rid]['target'] const imgFileExt = extractFileExtension(imgName).toLowerCase() const zip = warpObj['zip'] const imgArrayBuffer = await zip.file(imgName).async('arraybuffer') const xfrmNode = node['p:spPr']['a:xfrm'] const mimeType = getMimeType(imgFileExt) const { top, left } = getPosition(xfrmNode, undefined, undefined) const { width, height } = getSize(xfrmNode, undefined, undefined) const src = `data:${mimeType};base64,${base64ArrayBuffer(imgArrayBuffer)}` const isFlipV = getTextByPathList(xfrmNode, ['attrs', 'flipV']) === '1' const isFlipH = getTextByPathList(xfrmNode, ['attrs', 'flipH']) === '1' let rotate = 0 const rotateNode = getTextByPathList(node, [ 'p:spPr', 'a:xfrm', 'attrs', 'rot' ]) if (rotateNode) rotate = angleToDegrees(rotateNode) const videoNode = getTextByPathList(node, [ 'p:nvPicPr', 'p:nvPr', 'a:videoFile' ]) let videoRid, videoFile, videoFileExt, videoMimeType, uInt8ArrayVideo, videoBlob let isVdeoLink = false if (videoNode) { videoRid = videoNode['attrs']['r:link'] videoFile = resObj[videoRid]['target'] if (isVideoLink(videoFile)) { videoFile = escapeHtml(videoFile) isVdeoLink = true } else { videoFileExt = extractFileExtension(videoFile).toLowerCase() if ( videoFileExt === 'mp4' || videoFileExt === 'webm' || videoFileExt === 'ogg' ) { uInt8ArrayVideo = await zip.file(videoFile).async('arraybuffer') videoMimeType = getMimeType(videoFileExt) videoBlob = URL.createObjectURL( new Blob([uInt8ArrayVideo], { type: videoMimeType }) ) } } } const audioNode = getTextByPathList(node, [ 'p:nvPicPr', 'p:nvPr', 'a:audioFile' ]) let audioRid, audioFile, audioFileExt, uInt8ArrayAudio, audioBlob if (audioNode) { audioRid = audioNode['attrs']['r:link'] audioFile = resObj[audioRid]['target'] audioFileExt = extractFileExtension(audioFile).toLowerCase() if ( audioFileExt === 'mp3' || audioFileExt === 'wav' || audioFileExt === 'ogg' ) { uInt8ArrayAudio = await zip.file(audioFile).async('arraybuffer') audioBlob = URL.createObjectURL(new Blob([uInt8ArrayAudio])) } } if (videoNode && !isVdeoLink) { return { type: 'video', top, left, width, height, rotate, blob: videoBlob, order } } if (videoNode && isVdeoLink) { return { type: 'video', top, left, width, height, rotate, src: videoFile, order } } if (audioNode) { return { type: 'audio', top, left, width, height, rotate, blob: audioBlob, order } } let rect const srcRectAttrs = getTextByPathList(node, [ 'p:blipFill', 'a:srcRect', 'attrs' ]) if ( srcRectAttrs && (srcRectAttrs.t || srcRectAttrs.b || srcRectAttrs.l || srcRectAttrs.r) ) { rect = {} if (srcRectAttrs.t) rect.t = srcRectAttrs.t / 1000 if (srcRectAttrs.b) rect.b = srcRectAttrs.b / 1000 if (srcRectAttrs.l) rect.l = srcRectAttrs.l / 1000 if (srcRectAttrs.r) rect.r = srcRectAttrs.r / 1000 } const geom = getTextByPathList(node, ['p:spPr', 'a:prstGeom', 'attrs', 'prst']) || 'rect' const { borderColor, borderWidth, borderType, strokeDasharray } = getBorder( node, undefined, warpObj ) return { type: 'image', top, left, width, height, rotate, src, isFlipV, isFlipH, order, rect, geom, borderColor, borderWidth, borderType, borderStrokeDasharray: strokeDasharray } } async function processGraphicFrameNode (node, warpObj, source) { const graphicTypeUri = getTextByPathList(node, [ 'a:graphic', 'a:graphicData', 'attrs', 'uri' ]) let result switch (graphicTypeUri) { case 'http://schemas.openxmlformats.org/drawingml/2006/table': result = await genTable(node, warpObj) break case 'http://schemas.openxmlformats.org/drawingml/2006/chart': result = await genChart(node, warpObj) break case 'http://schemas.openxmlformats.org/drawingml/2006/diagram': result = await genDiagram(node, warpObj) break case 'http://schemas.openxmlformats.org/presentationml/2006/ole': let oleObjNode = getTextByPathList(node, [ 'a:graphic', 'a:graphicData', 'mc:AlternateContent', 'mc:Fallback', 'p:oleObj' ]) if (!oleObjNode) { oleObjNode = getTextByPathList(node, [ 'a:graphic', 'a:graphicData', 'p:oleObj' ]) } if (oleObjNode) { result = await processGroupSpNode(oleObjNode, warpObj, source) } break default: } return result } async function genTable (node, warpObj) { const order = node['attrs']['order'] const tableNode = getTextByPathList(node, [ 'a:graphic', 'a:graphicData', 'a:tbl' ]) const xfrmNode = getTextByPathList(node, ['p:xfrm']) const { top, left } = getPosition(xfrmNode, undefined, undefined) const { width, height } = getSize(xfrmNode, undefined, undefined) const getTblPr = getTextByPathList(node, [ 'a:graphic', 'a:graphicData', 'a:tbl', 'a:tblPr' ]) let getColsGrid = getTextByPathList(node, [ 'a:graphic', 'a:graphicData', 'a:tbl', 'a:tblGrid', 'a:gridCol' ]) if (getColsGrid.constructor !== Array) getColsGrid = [getColsGrid] const colWidths = [] if (getColsGrid) { for (const item of getColsGrid) { const colWidthParam = getTextByPathList(item, ['attrs', 'w']) || 0 const colWidth = parseInt(colWidthParam) * RATIO_EMUs_Points colWidths.push(colWidth) } } const firstRowAttr = getTblPr['attrs'] ? getTblPr['attrs']['firstRow'] : undefined const firstColAttr = getTblPr['attrs'] ? getTblPr['attrs']['firstCol'] : undefined const lastRowAttr = getTblPr['attrs'] ? getTblPr['attrs']['lastRow'] : undefined const lastColAttr = getTblPr['attrs'] ? getTblPr['attrs']['lastCol'] : undefined const bandRowAttr = getTblPr['attrs'] ? getTblPr['attrs']['bandRow'] : undefined const bandColAttr = getTblPr['attrs'] ? getTblPr['attrs']['bandCol'] : undefined const tblStylAttrObj = { isFrstRowAttr: firstRowAttr && firstRowAttr === '1' ? 1 : 0, isFrstColAttr: firstColAttr && firstColAttr === '1' ? 1 : 0, isLstRowAttr: lastRowAttr && lastRowAttr === '1' ? 1 : 0, isLstColAttr: lastColAttr && lastColAttr === '1' ? 1 : 0, isBandRowAttr: bandRowAttr && bandRowAttr === '1' ? 1 : 0, isBandColAttr: bandColAttr && bandColAttr === '1' ? 1 : 0 } let thisTblStyle const tbleStyleId = getTblPr['a:tableStyleId'] if (tbleStyleId) { const tbleStylList = warpObj['tableStyles']['a:tblStyleLst']['a:tblStyle'] if (tbleStylList) { if (tbleStylList.constructor === Array) { for (let k = 0; k < tbleStylList.length; k++) { if (tbleStylList[k]['attrs']['styleId'] === tbleStyleId) { thisTblStyle = tbleStylList[k] } } } else { if (tbleStylList['attrs']['styleId'] === tbleStyleId) { thisTblStyle = tbleStylList } } } } if (thisTblStyle) thisTblStyle['tblStylAttrObj'] = tblStylAttrObj let borders = {} const tblStyl = getTextByPathList(thisTblStyle, ['a:wholeTbl', 'a:tcStyle']) const tblBorderStyl = getTextByPathList(tblStyl, ['a:tcBdr']) if (tblBorderStyl) borders = getTableBorders(tblBorderStyl, warpObj) let tbl_bgcolor = '' let tbl_bgFillschemeClr = getTextByPathList(thisTblStyle, [ 'a:tblBg', 'a:fillRef' ]) if (tbl_bgFillschemeClr) { tbl_bgcolor = getSolidFill( tbl_bgFillschemeClr, undefined, undefined, warpObj ) } if (tbl_bgFillschemeClr === undefined) { tbl_bgFillschemeClr = getTextByPathList(thisTblStyle, [ 'a:wholeTbl', 'a:tcStyle', 'a:fill', 'a:solidFill' ]) tbl_bgcolor = getSolidFill( tbl_bgFillschemeClr, undefined, undefined, warpObj ) } let trNodes = tableNode['a:tr'] if (trNodes.constructor !== Array) trNodes = [trNodes] const data = [] const rowHeights = [] for (let i = 0; i < trNodes.length; i++) { const trNode = trNodes[i] const rowHeightParam = getTextByPathList(trNodes[i], ['attrs', 'h']) || 0 const rowHeight = parseInt(rowHeightParam) * RATIO_EMUs_Points rowHeights.push(rowHeight) const { fillColor, fontColor, fontBold } = getTableRowParams( trNodes, i, tblStylAttrObj, thisTblStyle, warpObj ) const tcNodes = trNode['a:tc'] const tr = [] if (tcNodes.constructor === Array) { for (let j = 0; j < tcNodes.length; j++) { const tcNode = tcNodes[j] let a_sorce if (j === 0 && tblStylAttrObj['isFrstColAttr'] === 1) { a_sorce = 'a:firstCol' if ( tblStylAttrObj['isLstRowAttr'] === 1 && i === trNodes.length - 1 && getTextByPathList(thisTblStyle, ['a:seCell']) ) { a_sorce = 'a:seCell' } else if ( tblStylAttrObj['isFrstRowAttr'] === 1 && i === 0 && getTextByPathList(thisTblStyle, ['a:neCell']) ) { a_sorce = 'a:neCell' } } else if ( j > 0 && tblStylAttrObj['isBandColAttr'] === 1 && !(tblStylAttrObj['isFrstColAttr'] === 1 && i === 0) && !(tblStylAttrObj['isLstRowAttr'] === 1 && i === trNodes.length - 1) && j !== tcNodes.length - 1 ) { if (j % 2 !== 0) { let aBandNode = getTextByPathList(thisTblStyle, ['a:band2V']) if (aBandNode === undefined) { aBandNode = getTextByPathList(thisTblStyle, ['a:band1V']) if (aBandNode) a_sorce = 'a:band2V' } else a_sorce = 'a:band2V' } } if (j === tcNodes.length - 1 && tblStylAttrObj['isLstColAttr'] === 1) { a_sorce = 'a:lastCol' if ( tblStylAttrObj['isLstRowAttr'] === 1 && i === trNodes.length - 1 && getTextByPathList(thisTblStyle, ['a:swCell']) ) { a_sorce = 'a:swCell' } else if ( tblStylAttrObj['isFrstRowAttr'] === 1 && i === 0 && getTextByPathList(thisTblStyle, ['a:nwCell']) ) { a_sorce = 'a:nwCell' } } const text = genTextBody( tcNode['a:txBody'], tcNode, undefined, undefined, warpObj ) const cell = await getTableCellParams( tcNode, thisTblStyle, a_sorce, warpObj ) const td = { text } if (cell.rowSpan) td.rowSpan = cell.rowSpan if (cell.colSpan) td.colSpan = cell.colSpan if (cell.vMerge) td.vMerge = cell.vMerge if (cell.hMerge) td.hMerge = cell.hMerge if (cell.fontBold || fontBold) td.fontBold = cell.fontBold || fontBold if (cell.fontColor || fontColor) { td.fontColor = cell.fontColor || fontColor } if (cell.fillColor || fillColor || tbl_bgcolor) { td.fillColor = cell.fillColor || fillColor || tbl_bgcolor } if (cell.borders) td.borders = cell.borders tr.push(td) } } else { let a_sorce if ( tblStylAttrObj['isFrstColAttr'] === 1 && tblStylAttrObj['isLstRowAttr'] !== 1 ) { a_sorce = 'a:firstCol' } else if ( tblStylAttrObj['isBandColAttr'] === 1 && tblStylAttrObj['isLstRowAttr'] !== 1 ) { let aBandNode = getTextByPathList(thisTblStyle, ['a:band2V']) if (!aBandNode) { aBandNode = getTextByPathList(thisTblStyle, ['a:band1V']) if (aBandNode) a_sorce = 'a:band2V' } else a_sorce = 'a:band2V' } if ( tblStylAttrObj['isLstColAttr'] === 1 && tblStylAttrObj['isLstRowAttr'] !== 1 ) { a_sorce = 'a:lastCol' } const text = genTextBody( tcNodes['a:txBody'], tcNodes, undefined, undefined, warpObj ) const cell = await getTableCellParams( tcNodes, thisTblStyle, a_sorce, warpObj ) const td = { text } if (cell.rowSpan) td.rowSpan = cell.rowSpan if (cell.colSpan) td.colSpan = cell.colSpan if (cell.vMerge) td.vMerge = cell.vMerge if (cell.hMerge) td.hMerge = cell.hMerge if (cell.fontBold || fontBold) td.fontBold = cell.fontBold || fontBold if (cell.fontColor || fontColor) { td.fontColor = cell.fontColor || fontColor } if (cell.fillColor || fillColor || tbl_bgcolor) { td.fillColor = cell.fillColor || fillColor || tbl_bgcolor } if (cell.borders) td.borders = cell.borders tr.push(td) } data.push(tr) } return { type: 'table', top, left, width, height, data, order, borders, rowHeights, colWidths } } async function genChart (node, warpObj) { const order = node['attrs']['order'] const xfrmNode = getTextByPathList(node, ['p:xfrm']) const { top, left } = getPosition(xfrmNode, undefined, undefined) const { width, height } = getSize(xfrmNode, undefined, undefined) const rid = node['a:graphic']['a:graphicData']['c:chart']['attrs']['r:id'] let refName = getTextByPathList(warpObj['slideResObj'], [rid, 'target']) if (!refName) { refName = getTextByPathList(warpObj['layoutResObj'], [rid, 'target']) } if (!refName) { refName = getTextByPathList(warpObj['masterResObj'], [rid, 'target']) } if (!refName) return {} const content = await readXmlFile(warpObj['zip'], refName) const plotArea = getTextByPathList(content, [ 'c:chartSpace', 'c:chart', 'c:plotArea' ]) const chart = getChartInfo(plotArea, warpObj) if (!chart) return {} const data = { type: 'chart', top, left, width, height, data: chart.data, colors: chart.colors, chartType: chart.type, order } if (chart.marker !== undefined) data.marker = chart.marker if (chart.barDir !== undefined) data.barDir = chart.barDir if (chart.holeSize !== undefined) data.holeSize = chart.holeSize if (chart.grouping !== undefined) data.grouping = chart.grouping if (chart.style !== undefined) data.style = chart.style return data } async function genDiagram (node, warpObj) { const order = node['attrs']['order'] const xfrmNode = getTextByPathList(node, ['p:xfrm']) const { left, top } = getPosition(xfrmNode, undefined, undefined) const { width, height } = getSize(xfrmNode, undefined, undefined) const dgmDrwSpArray = getTextByPathList(warpObj['digramFileContent'], [ 'p:drawing', 'p:spTree', 'p:sp' ]) const elements = [] if (dgmDrwSpArray) { for (const item of dgmDrwSpArray) { const el = await processSpNode(item, node, warpObj, 'diagramBg') if (el) elements.push(el) } } return { type: 'diagram', left, top, width, height, elements, order } }