diff --git a/src/renderer/src/AixPPTist/src/hooks/useImport.ts b/src/renderer/src/AixPPTist/src/hooks/useImport.ts index b990686..5aa3c32 100644 --- a/src/renderer/src/AixPPTist/src/hooks/useImport.ts +++ b/src/renderer/src/AixPPTist/src/hooks/useImport.ts @@ -1,6 +1,8 @@ import { ref } from 'vue' import { storeToRefs } from 'pinia' -import { parse, type Shape, type Element, type ChartItem } from 'pptxtojson' +// import { parse, type Shape, type Element, type ChartItem } from 'pptxtojson' +// import { parse, utils, fill as fillUtil } from '@/plugins/pptTojson' +import { parse, fill as fillUtil, type Shape, type Element, type ChartItem } from '@/plugins/pptTojson' import { nanoid } from 'nanoid' import { useSlidesStore } from '../store' import { decrypt } from '../utils/crypto' @@ -9,6 +11,7 @@ import useAddSlidesOrElements from '../hooks/useAddSlidesOrElements' import useSlideHandler from '../hooks/useSlideHandler' import message from '../utils/message' import { getSvgPathRange } from '../utils/svgPathParser' +import { calculatePathDimensions } from '@/utils/ppt/svgUtils' import type { Slide, TableCellStyle, @@ -75,57 +78,18 @@ const parseLineElement = (el: Shape) => { return data } -export default () => { - const exporting = ref(false) - - // 导入pptist文件 - const importSpecificFile = (files: FileList, cover = false) => { - const file = files[0] - - const reader = new FileReader() - reader.addEventListener('load', () => { - try { - const slides = JSON.parse(decrypt(reader.result as string)) - if (cover) { - slidesStore.updateSlideIndex(0) - slidesStore.setSlides(slides) - } - else if (isEmptySlide.value) slidesStore.setSlides(slides) - else addSlidesFromData(slides) +// PPT json二次加工处理 +const parsePptJsonSlides = (slides: Slide[]|any, zip) => { + return new Promise(async (resolve, reject) => { + try { + const shapeList: ShapePoolItem[] = [] + for (const item of SHAPE_LIST) { + shapeList.push(...item.children) } - catch { - message.error('无法正确读取 / 解析该文件') - } - }) - reader.readAsText(file) - } - - - - // 导入PPTX文件 - const importPPTXFile = (files: FileList) => { - const file = files[0] - if (!file) return - - exporting.value = true - - const shapeList: ShapePoolItem[] = [] - for (const item of SHAPE_LIST) { - shapeList.push(...item.children) - } - - const reader = new FileReader() - reader.onload = async e => { - const json = await parse(e.target!.result as ArrayBuffer) - const ratio = 96 / 72 - const width = json.size.width - - slidesStore.setViewportSize(width * ratio) - - const slides: Slide[] = [] - for (const item of json.slides) { + const newSlides: Slide[] = [] + for (const item of slides) { const { type, value } = item.fill let background: SlideBackground if (type === 'image') { @@ -156,20 +120,20 @@ export default () => { color: value, } } - + const slide: Slide = { id: nanoid(10), elements: [], background, } - - const parseElements = (elements: Element[]) => { + + const parseElements = async (elements: Element[]) => { for (const el of elements) { const originWidth = el.width || 1 const originHeight = el.height || 1 const originLeft = el.left const originTop = el.top - + el.width = el.width * ratio el.height = el.height * ratio el.left = el.left * ratio @@ -219,6 +183,7 @@ export default () => { rotate: el.rotate, flipH: el.isFlipH, flipV: el.isFlipV, + zipPath: el.zipPath, }) } else if (el.type === 'audio') { @@ -235,6 +200,7 @@ export default () => { color: theme.value.themeColor, loop: false, autoplay: false, + zipPath: el.zipPath, }) } else if (el.type === 'video') { @@ -248,6 +214,7 @@ export default () => { top: el.top, rotate: 0, autoplay: false, + zipPath: el.zipPath, }) } else if (el.type === 'shape') { @@ -257,7 +224,7 @@ export default () => { } else { const shape = shapeList.find(item => item.pptxShapeType === el.shapType) - + const vAlignMap: { [key: string]: ShapeTextAlign } = { 'mid': 'middle', 'down': 'bottom', @@ -325,6 +292,34 @@ export default () => { element.viewBox = [maxX || originWidth, maxY || originHeight] } } + // auth: zdg + if (el.fill) { + const { type, ...opt } = el.fill + if (type === 'gradient') { // 线性渐变色 + element.gradient = { + type: 'linear', + colors: opt.colors.map(item => ({ + ...item, + pos: parseInt(item.pos), + })), + rotate: opt.rot, + } + } else if (type == 'image') { // 背景图填充 + const pathPos = calculatePathDimensions(element.path) + const url = opt.picBase64 || (!!zip ? await fillUtil.getPicFillBase64(opt.zipPath, zip) : '') + element.gradient = { + type: 'image', + image: { + src: url, + width: opt.w||el.width, + height: opt.h||el.height, + path_W: Math.round(pathPos.width), // 获取path 的宽高 + path_h: Math.round(pathPos.height), // 获取path 的宽高 + ...opt + } + } + } + } if (element.path) slide.elements.push(element) } @@ -342,17 +337,17 @@ export default () => { const rowCells: TableCell[] = [] for (let j = 0; j < col; j++) { const cellData = el.data[i][j] - + let textDiv: HTMLDivElement | null = document.createElement('div') textDiv.innerHTML = cellData.text const p = textDiv.querySelector('p') const align = p?.style.textAlign || 'left' - + const span = textDiv.querySelector('span') const fontsize = span?.style.fontSize ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px' : '' const fontname = span?.style.fontFamily || '' const color = span?.style.color || cellData.fontColor - + rowCells.push({ id: nanoid(10), colspan: cellData.colSpan || 1, @@ -409,11 +404,11 @@ export default () => { legends = data.map(item => item.key) series = data.map(item => item.values.map(v => v.y)) } - + const options: ChartOptions = {} let chartType: ChartType = 'bar' - + switch (el.chartType) { case 'barChart': case 'bar3DChart': @@ -468,18 +463,91 @@ export default () => { }) } else if (el.type === 'group' || el.type === 'diagram') { - const elements = el.elements.map(_el => ({ - ..._el, - left: _el.left + originLeft, - top: _el.top + originTop, - })) - parseElements(elements) + const elements = el.elements.map(_el => { + const isGroup = el.type === 'group' // 子级是否为分组 + const isFlipH = !!(_el.isFlipH ^ el.isFlipH) // 水平翻转(分组有值进行异或运算) + const isFlipV = !!(_el.isFlipV ^ el.isFlipV) // 垂直翻转(分组有值进行异或运算) + const isPleft = el.isFlipH_def // 是否父级翻转-改变子元素坐标left + const isPtop = el.isFlipV_def // 是否父级翻转-改变子元素坐标top + const left = originLeft + (isPleft ? originWidth - _el.left - _el.width : _el.left) + const top = originTop + (isPtop ? originHeight - _el.top - _el.height : _el.top) + return { + ..._el, + left, + top, + isFlipH, + isFlipV, + isFlipH_def: _el.isFlipH, // 保留默认 + isFlipV_def: _el.isFlipV, // 保留默认 + } + }) + await parseElements(elements) } } } - parseElements(item.elements) - slides.push(slide) + // 设置默认翻转默认属性 + item.elements.forEach(o => { o.isFlipH_def = o.isFlipH; o.isFlipV_def = o.isFlipV }) + item.bgElements.forEach(o => { o.isFlipH_def = o.isFlipH; o.isFlipV_def = o.isFlipV }) + item.bgElements.length && await parseElements(item.bgElements) // 加载当前幻灯片对应的母版 + item.elements.length && await parseElements(item.elements) // 加载当前幻灯片 + newSlides.push(slide) } + resolve(newSlides) + } catch (error) { + reject(error) + } + }) +} + +export default () => { + + const exporting = ref(false) + + // 导入pptist文件 + const importSpecificFile = (files: FileList, cover = false) => { + const file = files[0] + + const reader = new FileReader() + reader.addEventListener('load', () => { + try { + const slides = JSON.parse(decrypt(reader.result as string)) + if (cover) { + slidesStore.updateSlideIndex(0) + slidesStore.setSlides(slides) + } + else if (isEmptySlide.value) slidesStore.setSlides(slides) + else addSlidesFromData(slides) + } + catch { + message.error('无法正确读取 / 解析该文件') + } + }) + reader.readAsText(file) + } + + + + // 导入PPTX文件 + const importPPTXFile = (files: FileList) => { + const file = files[0] + if (!file) return + + exporting.value = true + + const shapeList: ShapePoolItem[] = [] + for (const item of SHAPE_LIST) { + shapeList.push(...item.children) + } + + const reader = new FileReader() + reader.onload = async e => { + const json = await parse(e.target!.result as ArrayBuffer) + + const ratio = 96 / 72 + const width = json.size.width + slidesStore.setViewportSize(width * ratio) + // json数据二次加工 + const slides = await parsePptJsonSlides(json.slides, json.zip) slidesStore.updateSlideIndex(0) slidesStore.setSlides(slides) exporting.value = false @@ -537,365 +605,7 @@ export const PPTXFileToJson = (data: File|ArrayBuffer) => { resData.def = json // 保留原始数据 resData.width = width * ratio resData.ratio = slidesStore.viewportRatio - - const slides: Slide[] = [] - for (const item of json.slides) { - const { type, value } = item.fill - let background: SlideBackground - if (type === 'image') { - background = { - type: 'image', - image: { - src: value.picBase64, - size: 'cover', - }, - } - } - else if (type === 'gradient') { - background = { - type: 'gradient', - gradient: { - type: 'linear', - colors: value.colors.map(item => ({ - ...item, - pos: parseInt(item.pos), - })), - rotate: value.rot, - }, - } - } - else { - background = { - type: 'solid', - color: value, - } - } - - const slide: Slide = { - id: nanoid(10), - elements: [], - background, - } - - const parseElements = (elements: Element[]) => { - for (const el of elements) { - const originWidth = el.width || 1 - const originHeight = el.height || 1 - const originLeft = el.left - const originTop = el.top - - el.width = el.width * ratio - el.height = el.height * ratio - el.left = el.left * ratio - el.top = el.top * ratio - - if (el.type === 'text') { - const textEl: PPTTextElement = { - type: 'text', - id: nanoid(10), - width: el.width, - height: el.height, - left: el.left, - top: el.top, - rotate: el.rotate, - defaultFontName: theme.value.fontName, - defaultColor: theme.value.fontColor, - content: convertFontSizePtToPx(el.content, ratio), - lineHeight: 1, - outline: { - color: el.borderColor, - width: el.borderWidth, - style: el.borderType, - }, - fill: el.fillColor, - vertical: el.isVertical, - } - if (el.shadow) { - textEl.shadow = { - h: el.shadow.h * ratio, - v: el.shadow.v * ratio, - blur: el.shadow.blur * ratio, - color: el.shadow.color, - } - } - slide.elements.push(textEl) - } - else if (el.type === 'image') { - slide.elements.push({ - type: 'image', - id: nanoid(10), - src: el.src, - width: el.width, - height: el.height, - left: el.left, - top: el.top, - fixedRatio: true, - rotate: el.rotate, - flipH: el.isFlipH, - flipV: el.isFlipV, - }) - } - else if (el.type === 'audio') { - slide.elements.push({ - type: 'audio', - id: nanoid(10), - src: el.blob, - width: el.width, - height: el.height, - left: el.left, - top: el.top, - rotate: 0, - fixedRatio: false, - color: theme.value.themeColor, - loop: false, - autoplay: false, - }) - } - else if (el.type === 'video') { - slide.elements.push({ - type: 'video', - id: nanoid(10), - src: (el.blob || el.src)!, - width: el.width, - height: el.height, - left: el.left, - top: el.top, - rotate: 0, - autoplay: false, - }) - } - else if (el.type === 'shape') { - if (el.shapType === 'line' || /Connector/.test(el.shapType)) { - // 从返回对象中解构出 xx 函数并调用 - const lineElement = parseLineElement(el) - slide.elements.push(lineElement) - } - else { - const shape = shapeList.find(item => item.pptxShapeType === el.shapType) - - const vAlignMap: { [key: string]: ShapeTextAlign } = { - 'mid': 'middle', - 'down': 'bottom', - 'up': 'top', - } - - const element: PPTShapeElement = { - type: 'shape', - id: nanoid(10), - width: el.width, - height: el.height, - left: el.left, - top: el.top, - viewBox: [200, 200], - path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z', - fill: el.fillColor || 'none', - fixedRatio: false, - rotate: el.rotate, - outline: { - color: el.borderColor, - width: el.borderWidth, - style: el.borderType, - }, - text: { - content: convertFontSizePtToPx(el.content, ratio), - defaultFontName: theme.value.fontName, - defaultColor: theme.value.fontColor, - align: vAlignMap[el.vAlign] || 'middle', - }, - flipH: el.isFlipH, - flipV: el.isFlipV, - } - if (el.shadow) { - element.shadow = { - h: el.shadow.h * ratio, - v: el.shadow.v * ratio, - blur: el.shadow.blur * ratio, - color: el.shadow.color, - } - } - - if (shape) { - element.path = shape.path - element.viewBox = shape.viewBox - - if (shape.pathFormula) { - element.pathFormula = shape.pathFormula - element.viewBox = [el.width, el.height] - - const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula] - if ('editable' in pathFormula && pathFormula.editable) { - element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue) - element.keypoints = pathFormula.defaultValue - } - else element.path = pathFormula.formula(el.width, el.height) - } - } - if (el.shapType === 'custom') { - if (el.path!.indexOf('NaN') !== -1) element.path = '' - else { - element.special = true - element.path = el.path! - - const { maxX, maxY } = getSvgPathRange(element.path) - element.viewBox = [maxX || originWidth, maxY || originHeight] - } - } - - if (element.path) slide.elements.push(element) - } - } - else if (el.type === 'table') { - const row = el.data.length - const col = el.data[0].length - - const style: TableCellStyle = { - fontname: theme.value.fontName, - color: theme.value.fontColor, - } - const data: TableCell[][] = [] - for (let i = 0; i < row; i++) { - const rowCells: TableCell[] = [] - for (let j = 0; j < col; j++) { - const cellData = el.data[i][j] - - let textDiv: HTMLDivElement | null = document.createElement('div') - textDiv.innerHTML = cellData.text - const p = textDiv.querySelector('p') - const align = p?.style.textAlign || 'left' - - const span = textDiv.querySelector('span') - const fontsize = span?.style.fontSize ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px' : '' - const fontname = span?.style.fontFamily || '' - const color = span?.style.color || cellData.fontColor - - rowCells.push({ - id: nanoid(10), - colspan: cellData.colSpan || 1, - rowspan: cellData.rowSpan || 1, - text: textDiv.innerText, - style: { - ...style, - align: ['left', 'right', 'center'].includes(align) ? (align as 'left' | 'right' | 'center') : 'left', - fontsize, - fontname, - color, - bold: cellData.fontBold, - backcolor: cellData.fillColor, - }, - }) - textDiv = null - } - data.push(rowCells) - } - - const colWidths: number[] = new Array(col).fill(1 / col) - - slide.elements.push({ - type: 'table', - id: nanoid(10), - width: el.width, - height: el.height, - left: el.left, - top: el.top, - colWidths, - rotate: 0, - data, - outline: { - width: el.borderWidth || 2, - style: el.borderType, - color: el.borderColor || '#eeece1', - }, - cellMinHeight: 36, - }) - } - else if (el.type === 'chart') { - let labels: string[] - let legends: string[] - let series: number[][] - - if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') { - labels = el.data[0].map((item, index) => `坐标${index + 1}`) - legends = ['X', 'Y'] - series = el.data - } - else { - const data = el.data as ChartItem[] - labels = Object.values(data[0].xlabels) - legends = data.map(item => item.key) - series = data.map(item => item.values.map(v => v.y)) - } - - const options: ChartOptions = {} - - let chartType: ChartType = 'bar' - - switch (el.chartType) { - case 'barChart': - case 'bar3DChart': - chartType = 'bar' - if (el.barDir === 'bar') chartType = 'column' - if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true - break - case 'lineChart': - case 'line3DChart': - if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true - chartType = 'line' - break - case 'areaChart': - case 'area3DChart': - if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true - chartType = 'area' - break - case 'scatterChart': - case 'bubbleChart': - chartType = 'scatter' - break - case 'pieChart': - case 'pie3DChart': - chartType = 'pie' - break - case 'radarChart': - chartType = 'radar' - break - case 'doughnutChart': - chartType = 'ring' - break - default: - } - - slide.elements.push({ - type: 'chart', - id: nanoid(10), - chartType: chartType, - width: el.width, - height: el.height, - left: el.left, - top: el.top, - rotate: 0, - themeColors: [theme.value.themeColor], - textColor: theme.value.fontColor, - data: { - labels, - legends, - series, - }, - options, - }) - } - else if (el.type === 'group' || el.type === 'diagram') { - const elements = el.elements.map(_el => ({ - ..._el, - left: _el.left + originLeft, - top: _el.top + originTop, - })) - parseElements(elements) - } - } - } - parseElements(item.elements) - slides.push(slide) - } - resData.slides = slides + resData.slides = await parsePptJsonSlides(json.slides, json.zip) resolve(resData) }) } \ No newline at end of file diff --git a/src/renderer/src/AixPPTist/src/types/slides.ts b/src/renderer/src/AixPPTist/src/types/slides.ts index b936499..bf2151c 100644 --- a/src/renderer/src/AixPPTist/src/types/slides.ts +++ b/src/renderer/src/AixPPTist/src/types/slides.ts @@ -41,7 +41,7 @@ export const enum ElementTypes { * * rotate: 渐变角度(线性渐变) */ -export type GradientType = 'linear' | 'radial' +export type GradientType = 'linear' | 'radial' | 'image' export type GradientColor = { pos: number color: string diff --git a/src/renderer/src/AixPPTist/src/views/Editor/Toolbar/ElementStylePanel/ShapeStylePanel.vue b/src/renderer/src/AixPPTist/src/views/Editor/Toolbar/ElementStylePanel/ShapeStylePanel.vue index 7b83762..de5f5ec 100644 --- a/src/renderer/src/AixPPTist/src/views/Editor/Toolbar/ElementStylePanel/ShapeStylePanel.vue +++ b/src/renderer/src/AixPPTist/src/views/Editor/Toolbar/ElementStylePanel/ShapeStylePanel.vue @@ -46,11 +46,12 @@ :options="[ { label: '线性渐变', value: 'linear' }, { label: '径向渐变', value: 'radial' }, + { label: '背景图', value: 'image' }, ]" /> -