Spaces:
No application file
No application file
| /** | |
| * PptxGenJS: Slide Object Generators | |
| */ | |
| import { | |
| BARCHART_COLORS, | |
| CHART_NAME, | |
| CHART_TYPE, | |
| DEF_CELL_BORDER, | |
| DEF_CELL_MARGIN_IN, | |
| DEF_CHART_BORDER, | |
| DEF_FONT_COLOR, | |
| DEF_FONT_SIZE, | |
| DEF_SHAPE_LINE_COLOR, | |
| DEF_SLIDE_MARGIN_IN, | |
| EMU, | |
| IMG_PLAYBTN, | |
| MASTER_OBJECTS, | |
| PIECHART_COLORS, | |
| SCHEME_COLOR_NAMES, | |
| SHAPE_NAME, | |
| SHAPE_TYPE, | |
| SLIDE_OBJECT_TYPES, | |
| TEXT_HALIGN, | |
| TEXT_VALIGN, | |
| } from './core-enums' | |
| import { | |
| AddSlideProps, | |
| BackgroundProps, | |
| FormulaProps, | |
| IChartMulti, | |
| IChartOptsLib, | |
| IOptsChartData, | |
| ISlideObject, | |
| ImageProps, | |
| MediaProps, | |
| ObjectOptions, | |
| OptsChartGridLine, | |
| PresLayout, | |
| PresSlide, | |
| ShapeLineProps, | |
| ShapeProps, | |
| SlideLayout, | |
| SlideMasterProps, | |
| TableCell, | |
| TableProps, | |
| TableRow, | |
| TextProps, | |
| TextPropsOptions, | |
| } from './core-interfaces' | |
| import { getSlidesForTableRows } from './gen-tables' | |
| import { encodeXmlEntities, getNewRelId, getSmartParseNumber, inch2Emu, valToPts, correctShadowOptions } from './gen-utils' | |
| /** counter for included charts (used for index in their filenames) */ | |
| let _chartCounter = 0 | |
| /** | |
| * Transforms a slide definition to a slide object that is then passed to the XML transformation process. | |
| * @param {SlideMasterProps} props - slide definition | |
| * @param {PresSlide|SlideLayout} target - empty slide object that should be updated by the passed definition | |
| */ | |
| export function createSlideMaster(props: SlideMasterProps, target: SlideLayout): void { | |
| // STEP 1: Add background if either the slide or layout has background props | |
| // if (props.background || target.background) addBackgroundDefinition(props.background, target) | |
| if (props.bkgd) target.bkgd = props.bkgd // DEPRECATED: (remove in v4.0.0) | |
| // STEP 2: Add all Slide Master objects in the order they were given | |
| if (props.objects && Array.isArray(props.objects) && props.objects.length > 0) { | |
| props.objects.forEach((object, idx) => { | |
| const key = Object.keys(object)[0] | |
| const tgt = target as PresSlide | |
| if (MASTER_OBJECTS[key] && key === 'chart') addChartDefinition(tgt, object[key].type, object[key].data, object[key].opts) | |
| else if (MASTER_OBJECTS[key] && key === 'image') addImageDefinition(tgt, object[key]) | |
| else if (MASTER_OBJECTS[key] && key === 'line') addShapeDefinition(tgt, SHAPE_TYPE.LINE, object[key]) | |
| else if (MASTER_OBJECTS[key] && key === 'rect') addShapeDefinition(tgt, SHAPE_TYPE.RECTANGLE, object[key]) | |
| else if (MASTER_OBJECTS[key] && key === 'text') addTextDefinition(tgt, [{ text: object[key].text }], object[key].options, false) | |
| else if (MASTER_OBJECTS[key] && key === 'placeholder') { | |
| // TODO: 20180820: Check for existing `name`? | |
| object[key].options.placeholder = object[key].options.name | |
| delete object[key].options.name // remap name for earlier handling internally | |
| object[key].options._placeholderType = object[key].options.type | |
| delete object[key].options.type // remap name for earlier handling internally | |
| object[key].options._placeholderIdx = 100 + idx | |
| addTextDefinition(tgt, [{ text: object[key].text }], object[key].options, true) | |
| // TODO: ISSUE#599 - only text is supported now (add more below) | |
| // else if (object[key].image) addImageDefinition(tgt, object[key].image) | |
| /* 20200120: So... image placeholders go into the "slideLayoutN.xml" file and addImage doesn't do this yet... | |
| <p:sp> | |
| <p:nvSpPr> | |
| <p:cNvPr id="7" name="Picture Placeholder 6"> | |
| <a:extLst> | |
| <a:ext uri="{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}"> | |
| <a16:creationId xmlns:a16="http://schemas.microsoft.com/office/drawing/2014/main" id="{CE1AE45D-8641-0F4F-BDB5-080E69CCB034}"/> | |
| </a:ext> | |
| </a:extLst> | |
| </p:cNvPr> | |
| <p:cNvSpPr> | |
| */ | |
| } | |
| }) | |
| } | |
| // STEP 3: Add Slide Numbers (NOTE: Do this last so numbers are not covered by objects!) | |
| if (props.slideNumber && typeof props.slideNumber === 'object') target._slideNumberProps = props.slideNumber | |
| } | |
| /** | |
| * Generate the chart based on input data. | |
| * OOXML Chart Spec: ISO/IEC 29500-1:2016(E) | |
| * | |
| * @param {CHART_NAME | IChartMulti[]} `type` should belong to: 'column', 'pie' | |
| * @param {[]} `data` a JSON object with follow the following format | |
| * @param {IChartOptsLib} `opt` chart options | |
| * @param {PresSlide} `target` slide object that the chart will be added to | |
| * @return {object} chart object | |
| * { | |
| * title: 'eSurvey chart', | |
| * data: [ | |
| * { | |
| * name: 'Income', | |
| * labels: ['2005', '2006', '2007', '2008', '2009'], | |
| * values: [23.5, 26.2, 30.1, 29.5, 24.6] | |
| * }, | |
| * { | |
| * name: 'Expense', | |
| * labels: ['2005', '2006', '2007', '2008', '2009'], | |
| * values: [18.1, 22.8, 23.9, 25.1, 25] | |
| * } | |
| * ] | |
| * } | |
| */ | |
| export function addChartDefinition(target: PresSlide, type: CHART_NAME | IChartMulti[], data: IOptsChartData[], opt: IChartOptsLib): object { | |
| function correctGridLineOptions(glOpts: OptsChartGridLine): void { | |
| if (!glOpts || glOpts.style === 'none') return | |
| if (glOpts.size !== undefined && (isNaN(Number(glOpts.size)) || glOpts.size <= 0)) { | |
| console.warn('Warning: chart.gridLine.size must be greater than 0.') | |
| delete glOpts.size // delete prop to used defaults | |
| } | |
| if (glOpts.style && !['solid', 'dash', 'dot'].includes(glOpts.style)) { | |
| console.warn('Warning: chart.gridLine.style options: `solid`, `dash`, `dot`.') | |
| delete glOpts.style | |
| } | |
| if (glOpts.cap && !['flat', 'square', 'round'].includes(glOpts.cap)) { | |
| console.warn('Warning: chart.gridLine.cap options: `flat`, `square`, `round`.') | |
| delete glOpts.cap | |
| } | |
| } | |
| const chartId = ++_chartCounter | |
| const resultObject = { | |
| _type: null, | |
| text: null, | |
| options: null, | |
| chartRid: null, | |
| } | |
| // DESIGN: `type` can an object (ex: `pptx.charts.DOUGHNUT`) or an array of chart objects | |
| // EX: addChartDefinition([ { type:pptx.charts.BAR, data:{name:'', labels:[], values[]} }, {<etc>} ]) | |
| // Multi-Type Charts | |
| let tmpOpt = null | |
| let tmpData = [] | |
| if (Array.isArray(type)) { | |
| // For multi-type charts there needs to be data for each type, | |
| // as well as a single data source for non-series operations. | |
| // The data is indexed below to keep the data in order when segmented | |
| // into types. | |
| type.forEach(obj => { | |
| tmpData = tmpData.concat(obj.data) | |
| }) | |
| tmpOpt = data || opt | |
| } else { | |
| tmpData = data | |
| tmpOpt = opt | |
| } | |
| tmpData.forEach((item, i) => { | |
| item._dataIndex = i | |
| // Converts the 'labels' array from string[] to string[][] (or the respective primitive type), if needed | |
| if (item.labels !== undefined && !Array.isArray(item.labels[0])) { | |
| item.labels = [item.labels as string[]] | |
| } | |
| }) | |
| const options: IChartOptsLib = tmpOpt && typeof tmpOpt === 'object' ? tmpOpt : {} | |
| // STEP 1: TODO: check for reqd fields, correct type, etc | |
| // `type` exists in CHART_TYPE | |
| // Array.isArray(data) | |
| /* | |
| if ( Array.isArray(rel.data) && rel.data.length > 0 && typeof rel.data[0] === 'object' | |
| && rel.data[0].labels && Array.isArray(rel.data[0].labels) | |
| && rel.data[0].values && Array.isArray(rel.data[0].values) ) { | |
| obj = rel.data[0]; | |
| } | |
| else { | |
| console.warn("USAGE: addChart( 'pie', [ {name:'Sales', labels:['Jan','Feb'], values:[10,20]} ], {x:1, y:1} )"); | |
| return; | |
| } | |
| */ | |
| // STEP 2: Set default options/decode user options | |
| // A: Core | |
| options._type = type | |
| options.x = typeof options.x !== 'undefined' && options.x != null && !isNaN(Number(options.x)) ? options.x : 1 | |
| options.y = typeof options.y !== 'undefined' && options.y != null && !isNaN(Number(options.y)) ? options.y : 1 | |
| options.w = options.w || '50%' | |
| options.h = options.h || '50%' | |
| options.objectName = options.objectName | |
| ? encodeXmlEntities(options.objectName) | |
| : `Chart ${target._slideObjects.filter(obj => obj._type === SLIDE_OBJECT_TYPES.chart).length}` | |
| // B: Options: misc | |
| if (!['bar', 'col'].includes(options.barDir || '')) options.barDir = 'col' | |
| // barGrouping: "21.2.3.17 ST_Grouping (Grouping)" | |
| // barGrouping must be handled before data label validation as it can affect valid label positioning | |
| if (options._type === CHART_TYPE.AREA) { | |
| if (!['stacked', 'standard', 'percentStacked'].includes(options.barGrouping || '')) options.barGrouping = 'standard' | |
| } | |
| if (options._type === CHART_TYPE.BAR) { | |
| if (!['clustered', 'stacked', 'percentStacked'].includes(options.barGrouping || '')) options.barGrouping = 'clustered' | |
| } | |
| if (options._type === CHART_TYPE.BAR3D) { | |
| if (!['clustered', 'stacked', 'standard', 'percentStacked'].includes(options.barGrouping || '')) options.barGrouping = 'standard' | |
| } | |
| if (options.barGrouping?.includes('tacked')) { | |
| if (!options.barGapWidthPct) options.barGapWidthPct = 50 | |
| } | |
| // Clean up and validate data label positions | |
| // REFERENCE: https://docs.microsoft.com/en-us/openspecs/office_standards/ms-oi29500/e2b1697c-7adc-463d-9081-3daef72f656f?redirectedfrom=MSDN | |
| if (options.dataLabelPosition) { | |
| if (options._type === CHART_TYPE.AREA || options._type === CHART_TYPE.BAR3D || options._type === CHART_TYPE.DOUGHNUT || options._type === CHART_TYPE.RADAR) { delete options.dataLabelPosition } | |
| if (options._type === CHART_TYPE.PIE) { | |
| if (!['bestFit', 'ctr', 'inEnd', 'outEnd'].includes(options.dataLabelPosition)) delete options.dataLabelPosition | |
| } | |
| if (options._type === CHART_TYPE.BUBBLE || options._type === CHART_TYPE.BUBBLE3D || options._type === CHART_TYPE.LINE || options._type === CHART_TYPE.SCATTER) { | |
| if (!['b', 'ctr', 'l', 'r', 't'].includes(options.dataLabelPosition)) delete options.dataLabelPosition | |
| } | |
| if (options._type === CHART_TYPE.BAR) { | |
| if (!['stacked', 'percentStacked'].includes(options.barGrouping || '')) { | |
| if (!['ctr', 'inBase', 'inEnd'].includes(options.dataLabelPosition)) delete options.dataLabelPosition | |
| } | |
| if (!['clustered'].includes(options.barGrouping || '')) { | |
| if (!['ctr', 'inBase', 'inEnd', 'outEnd'].includes(options.dataLabelPosition)) delete options.dataLabelPosition | |
| } | |
| } | |
| } | |
| options.dataLabelBkgrdColors = options.dataLabelBkgrdColors || !options.dataLabelBkgrdColors ? options.dataLabelBkgrdColors : false | |
| if (!['b', 'l', 'r', 't', 'tr'].includes(options.legendPos || '')) options.legendPos = 'r' | |
| // 3D bar: ST_Shape | |
| if (!['cone', 'coneToMax', 'box', 'cylinder', 'pyramid', 'pyramidToMax'].includes(options.bar3DShape || '')) options.bar3DShape = 'box' | |
| // lineDataSymbol: http://www.datypic.com/sc/ooxml/a-val-32.html | |
| // Spec has [plus,star,x] however neither PPT2013 nor PPT-Online support them | |
| if (!['circle', 'dash', 'diamond', 'dot', 'none', 'square', 'triangle'].includes(options.lineDataSymbol || '')) options.lineDataSymbol = 'circle' | |
| if (!['gap', 'span'].includes(options.displayBlanksAs || '')) options.displayBlanksAs = 'span' | |
| if (!['standard', 'marker', 'filled'].includes(options.radarStyle || '')) options.radarStyle = 'standard' | |
| options.lineDataSymbolSize = options.lineDataSymbolSize && !isNaN(options.lineDataSymbolSize) ? options.lineDataSymbolSize : 6 | |
| options.lineDataSymbolLineSize = options.lineDataSymbolLineSize && !isNaN(options.lineDataSymbolLineSize) ? valToPts(options.lineDataSymbolLineSize) : valToPts(0.75) | |
| // `layout` allows the override of PPT defaults to maximize space | |
| if (options.layout) { | |
| ['x', 'y', 'w', 'h'].forEach(key => { | |
| const val = options.layout[key] | |
| if (isNaN(Number(val)) || val < 0 || val > 1) { | |
| console.warn('Warning: chart.layout.' + key + ' can only be 0-1') | |
| delete options.layout[key] // remove invalid value so that default will be used | |
| } | |
| }) | |
| } | |
| // Set gridline defaults | |
| options.catGridLine = options.catGridLine || (options._type === CHART_TYPE.SCATTER ? { color: 'D9D9D9', size: 1 } : { style: 'none' }) | |
| options.valGridLine = options.valGridLine || (options._type === CHART_TYPE.SCATTER ? { color: 'D9D9D9', size: 1 } : {}) | |
| options.serGridLine = options.serGridLine || (options._type === CHART_TYPE.SCATTER ? { color: 'D9D9D9', size: 1 } : { style: 'none' }) | |
| correctGridLineOptions(options.catGridLine) | |
| correctGridLineOptions(options.valGridLine) | |
| correctGridLineOptions(options.serGridLine) | |
| correctShadowOptions(options.shadow) | |
| // C: Options: plotArea | |
| options.showDataTable = options.showDataTable || !options.showDataTable ? options.showDataTable : false | |
| options.showDataTableHorzBorder = options.showDataTableHorzBorder || !options.showDataTableHorzBorder ? options.showDataTableHorzBorder : true | |
| options.showDataTableVertBorder = options.showDataTableVertBorder || !options.showDataTableVertBorder ? options.showDataTableVertBorder : true | |
| options.showDataTableOutline = options.showDataTableOutline || !options.showDataTableOutline ? options.showDataTableOutline : true | |
| options.showDataTableKeys = options.showDataTableKeys || !options.showDataTableKeys ? options.showDataTableKeys : true | |
| options.showLabel = options.showLabel || !options.showLabel ? options.showLabel : false | |
| options.showLegend = options.showLegend || !options.showLegend ? options.showLegend : false | |
| options.showPercent = options.showPercent || !options.showPercent ? options.showPercent : true | |
| options.showTitle = options.showTitle || !options.showTitle ? options.showTitle : false | |
| options.showValue = options.showValue || !options.showValue ? options.showValue : false | |
| options.showLeaderLines = options.showLeaderLines || !options.showLeaderLines ? options.showLeaderLines : false | |
| options.catAxisLineShow = typeof options.catAxisLineShow !== 'undefined' ? options.catAxisLineShow : true | |
| options.valAxisLineShow = typeof options.valAxisLineShow !== 'undefined' ? options.valAxisLineShow : true | |
| options.serAxisLineShow = typeof options.serAxisLineShow !== 'undefined' ? options.serAxisLineShow : true | |
| options.v3DRotX = !isNaN(options.v3DRotX) && options.v3DRotX >= -90 && options.v3DRotX <= 90 ? options.v3DRotX : 30 | |
| options.v3DRotY = !isNaN(options.v3DRotY) && options.v3DRotY >= 0 && options.v3DRotY <= 360 ? options.v3DRotY : 30 | |
| options.v3DRAngAx = options.v3DRAngAx || !options.v3DRAngAx ? options.v3DRAngAx : true | |
| options.v3DPerspective = !isNaN(options.v3DPerspective) && options.v3DPerspective >= 0 && options.v3DPerspective <= 240 ? options.v3DPerspective : 30 | |
| // D: Options: chart | |
| options.barGapWidthPct = !isNaN(options.barGapWidthPct) && options.barGapWidthPct >= 0 && options.barGapWidthPct <= 1000 ? options.barGapWidthPct : 150 | |
| options.barGapDepthPct = !isNaN(options.barGapDepthPct) && options.barGapDepthPct >= 0 && options.barGapDepthPct <= 1000 ? options.barGapDepthPct : 150 | |
| options.chartColors = Array.isArray(options.chartColors) | |
| ? options.chartColors | |
| : options._type === CHART_TYPE.PIE || options._type === CHART_TYPE.DOUGHNUT | |
| ? PIECHART_COLORS | |
| : BARCHART_COLORS | |
| options.chartColorsOpacity = options.chartColorsOpacity && !isNaN(options.chartColorsOpacity) ? options.chartColorsOpacity : null | |
| // DEPRECATED: v3.11.0 - use `plotArea.border` vvv | |
| options.border = options.border && typeof options.border === 'object' ? options.border : null | |
| if (options.border && (!options.border.pt || isNaN(options.border.pt))) options.border.pt = DEF_CHART_BORDER.pt | |
| if (options.border && (!options.border.color || typeof options.border.color !== 'string')) options.border.color = DEF_CHART_BORDER.color | |
| // DEPRECATED: (remove above in v4.0) ^^^ | |
| options.plotArea = options.plotArea || {} | |
| options.plotArea.border = options.plotArea.border && typeof options.plotArea.border === 'object' ? options.plotArea.border : null | |
| if (options.plotArea.border && (!options.plotArea.border.pt || isNaN(options.plotArea.border.pt))) options.plotArea.border.pt = DEF_CHART_BORDER.pt | |
| if (options.plotArea.border && (!options.plotArea.border.color || typeof options.plotArea.border.color !== 'string')) { options.plotArea.border.color = DEF_CHART_BORDER.color } | |
| if (options.border) options.plotArea.border = options.border // @deprecated [[remove in v4.0]] | |
| options.plotArea.fill = options.plotArea.fill || { color: null, transparency: null } | |
| if (options.fill) options.plotArea.fill.color = options.fill // @deprecated [[remove in v4.0]] | |
| // | |
| options.chartArea = options.chartArea || {} | |
| options.chartArea.border = options.chartArea.border && typeof options.chartArea.border === 'object' ? options.chartArea.border : null | |
| if (options.chartArea.border) { | |
| options.chartArea.border = { | |
| color: options.chartArea.border.color || DEF_CHART_BORDER.color, | |
| pt: options.chartArea.border.pt || DEF_CHART_BORDER.pt, | |
| } | |
| } | |
| options.chartArea.roundedCorners = typeof options.chartArea.roundedCorners === 'boolean' ? options.chartArea.roundedCorners : true | |
| // | |
| options.dataBorder = options.dataBorder && typeof options.dataBorder === 'object' ? options.dataBorder : null | |
| if (options.dataBorder && (!options.dataBorder.pt || isNaN(options.dataBorder.pt))) options.dataBorder.pt = 0.75 | |
| if (options.dataBorder && options.dataBorder.color) { | |
| const isHexColor = typeof options.dataBorder.color === 'string' && options.dataBorder.color.length === 6 && /^[0-9A-Fa-f]{6}$/.test(options.dataBorder.color) | |
| const isSchemeColor = Object.values(SCHEME_COLOR_NAMES).includes(options.dataBorder.color as SCHEME_COLOR_NAMES) | |
| if (!isHexColor && !isSchemeColor) { | |
| options.dataBorder.color = 'F9F9F9' // Fallback if neither hex nor scheme color | |
| } | |
| } | |
| // | |
| if (!options.dataLabelFormatCode && options._type === CHART_TYPE.SCATTER) options.dataLabelFormatCode = 'General' | |
| if (!options.dataLabelFormatCode && (options._type === CHART_TYPE.PIE || options._type === CHART_TYPE.DOUGHNUT)) { options.dataLabelFormatCode = options.showPercent ? '0%' : 'General' } | |
| options.dataLabelFormatCode = options.dataLabelFormatCode && typeof options.dataLabelFormatCode === 'string' ? options.dataLabelFormatCode : '#,##0' | |
| // | |
| // Set default format for Scatter chart labels to custom string if not defined | |
| if (!options.dataLabelFormatScatter && options._type === CHART_TYPE.SCATTER) options.dataLabelFormatScatter = 'custom' | |
| // | |
| options.lineSize = typeof options.lineSize === 'number' ? options.lineSize : 2 | |
| options.valAxisMajorUnit = typeof options.valAxisMajorUnit === 'number' ? options.valAxisMajorUnit : null | |
| if (options._type === CHART_TYPE.AREA || options._type === CHART_TYPE.BAR || options._type === CHART_TYPE.BAR3D || options._type === CHART_TYPE.LINE) { | |
| options.catAxisMultiLevelLabels = !!options.catAxisMultiLevelLabels | |
| } else { | |
| delete options.catAxisMultiLevelLabels | |
| } | |
| // STEP 4: Set props | |
| resultObject._type = 'chart' | |
| resultObject.options = options | |
| resultObject.chartRid = getNewRelId(target) | |
| // STEP 5: Add this chart to this Slide Rels (rId/rels count spans all slides! Count all images to get next rId) | |
| target._relsChart.push({ | |
| rId: getNewRelId(target), | |
| data: tmpData, | |
| opts: options, | |
| type: options._type, | |
| globalId: chartId, | |
| fileName: `chart${chartId}.xml`, | |
| Target: `/ppt/charts/chart${chartId}.xml`, | |
| }) | |
| target._slideObjects.push(resultObject) | |
| return resultObject | |
| } | |
| /** | |
| * Adds an image object to a slide definition. | |
| * This method can be called with only two args (opt, target) - this is supposed to be the only way in future. | |
| * @param {ImageProps} `opt` - object containing `path`/`data`, `x`, `y`, etc. | |
| * @param {PresSlide} `target` - slide that the image should be added to (if not specified as the 2nd arg) | |
| * @note: Remote images (eg: "http://whatev.com/blah"/from web and/or remote server arent supported yet - we'd need to create an <img>, load it, then send to canvas | |
| * @see: https://stackoverflow.com/questions/164181/how-to-fetch-a-remote-image-to-display-in-a-canvas) | |
| */ | |
| export function addImageDefinition(target: PresSlide, opt: ImageProps): void { | |
| const newObject: ISlideObject = { | |
| _type: null, | |
| text: null, | |
| options: null, | |
| image: null, | |
| imageRid: null, | |
| hyperlink: null, | |
| } | |
| // FIRST: Set vars for this image (object param replaces positional args in 1.1.0) | |
| const intPosX = opt.x || 0 | |
| const intPosY = opt.y || 0 | |
| const intWidth = opt.w || 0 | |
| const intHeight = opt.h || 0 | |
| const sizing = opt.sizing || null | |
| const objHyperlink = opt.hyperlink || '' | |
| const strImageData = opt.data || '' | |
| const strImagePath = opt.path || '' | |
| let imageRelId = getNewRelId(target) | |
| const objectName = opt.objectName ? encodeXmlEntities(opt.objectName) : `Image ${target._slideObjects.filter(obj => obj._type === SLIDE_OBJECT_TYPES.image).length}` | |
| // REALITY-CHECK: | |
| if (!strImagePath && !strImageData) { | |
| console.error('ERROR: addImage() requires either \'data\' or \'path\' parameter!') | |
| return null | |
| } else if (strImagePath && typeof strImagePath !== 'string') { | |
| console.error(`ERROR: addImage() 'path' should be a string, ex: {path:'/img/sample.png'} - you sent ${String(strImagePath)}`) | |
| return null | |
| } else if (strImageData && typeof strImageData !== 'string') { | |
| console.error(`ERROR: addImage() 'data' should be a string, ex: {data:'image/png;base64,NMP[...]'} - you sent ${String(strImageData)}`) | |
| return null | |
| } else if (strImageData && typeof strImageData === 'string' && !strImageData.toLowerCase().includes('base64,')) { | |
| console.error('ERROR: Image `data` value lacks a base64 header! Ex: \'image/png;base64,NMP[...]\')') | |
| return null | |
| } | |
| // STEP 1: Set extension | |
| // NOTE: Split to address URLs with params (eg: `path/brent.jpg?someParam=true`) | |
| let strImgExtn = ( | |
| strImagePath | |
| .substring(strImagePath.lastIndexOf('/') + 1) | |
| .split('?')[0] | |
| .split('.') | |
| .pop() | |
| .split('#')[0] || 'png' | |
| ).toLowerCase() | |
| // However, pre-encoded images can be whatever mime-type they want (and good for them!) | |
| if (strImageData && /image\/(\w+);/.exec(strImageData) && /image\/(\w+);/.exec(strImageData).length > 0) { | |
| strImgExtn = /image\/(\w+);/.exec(strImageData)[1] | |
| } else if (strImageData?.toLowerCase().includes('image/svg+xml')) { | |
| strImgExtn = 'svg' | |
| } | |
| // STEP 2: Set type/path | |
| newObject._type = SLIDE_OBJECT_TYPES.image | |
| newObject.image = strImagePath || 'preencoded.png' | |
| // STEP 3: Set image properties & options | |
| // FIXME: Measure actual image when no intWidth/intHeight params passed | |
| // ....: This is an async process: we need to make getSizeFromImage use callback, then set H/W... | |
| // if ( !intWidth || !intHeight ) { var imgObj = getSizeFromImage(strImagePath); | |
| newObject.options = { | |
| x: intPosX || 0, | |
| y: intPosY || 0, | |
| w: intWidth || 1, | |
| h: intHeight || 1, | |
| altText: opt.altText || '', | |
| rounding: typeof opt.rounding === 'boolean' ? opt.rounding : false, | |
| sizing, | |
| placeholder: opt.placeholder, | |
| rotate: opt.rotate || 0, | |
| flipV: opt.flipV || false, | |
| flipH: opt.flipH || false, | |
| transparency: opt.transparency || 0, | |
| objectName, | |
| shadow: correctShadowOptions(opt.shadow), | |
| } | |
| // STEP 4: Add this image to this Slide Rels (rId/rels count spans all slides! Count all images to get next rId) | |
| if (strImgExtn === 'svg') { | |
| // SVG files consume *TWO* rId's: (a png version and the svg image) | |
| // <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image1.png"/> | |
| // <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image2.svg"/> | |
| target._relsMedia.push({ | |
| path: strImagePath || strImageData + 'png', | |
| type: 'image/png', | |
| extn: 'png', | |
| data: strImageData || '', | |
| rId: imageRelId, | |
| Target: `../media/image-${target._slideNum}-${target._relsMedia.length + 1}.png`, | |
| isSvgPng: true, | |
| svgSize: { w: getSmartParseNumber(newObject.options.w, 'X', target._presLayout), h: getSmartParseNumber(newObject.options.h, 'Y', target._presLayout) }, | |
| }) | |
| newObject.imageRid = imageRelId | |
| target._relsMedia.push({ | |
| path: strImagePath || strImageData, | |
| type: 'image/svg+xml', | |
| extn: strImgExtn, | |
| data: strImageData || '', | |
| rId: imageRelId + 1, | |
| Target: `../media/image-${target._slideNum}-${target._relsMedia.length + 1}.${strImgExtn}`, | |
| }) | |
| newObject.imageRid = imageRelId + 1 | |
| } else { | |
| // PERF: Duplicate media should reuse existing `Target` value and not create an additional copy | |
| const dupeItem = target._relsMedia.filter(item => item.path && item.path === strImagePath && item.type === 'image/' + strImgExtn && !item.isDuplicate)[0] | |
| target._relsMedia.push({ | |
| path: strImagePath || 'preencoded.' + strImgExtn, | |
| type: 'image/' + strImgExtn, | |
| extn: strImgExtn, | |
| data: strImageData || '', | |
| rId: imageRelId, | |
| isDuplicate: !!(dupeItem?.Target), | |
| Target: dupeItem?.Target ? dupeItem.Target : `../media/image-${target._slideNum}-${target._relsMedia.length + 1}.${strImgExtn}`, | |
| }) | |
| newObject.imageRid = imageRelId | |
| } | |
| // STEP 5: Hyperlink support | |
| if (typeof objHyperlink === 'object') { | |
| if (!objHyperlink.url && !objHyperlink.slide) throw new Error('ERROR: `hyperlink` option requires either: `url` or `slide`') | |
| else { | |
| imageRelId++ | |
| target._rels.push({ | |
| type: SLIDE_OBJECT_TYPES.hyperlink, | |
| data: objHyperlink.slide ? 'slide' : 'dummy', | |
| rId: imageRelId, | |
| Target: objHyperlink.url || objHyperlink.slide.toString(), | |
| }) | |
| objHyperlink._rId = imageRelId | |
| newObject.hyperlink = objHyperlink | |
| } | |
| } | |
| // STEP 6: Add object to slide | |
| target._slideObjects.push(newObject) | |
| } | |
| /** | |
| * Adds a media object to a slide definition. | |
| * @param {PresSlide} `target` - slide object that the media will be added to | |
| * @param {MediaProps} `opt` - media options | |
| */ | |
| export function addMediaDefinition(target: PresSlide, opt: MediaProps): void { | |
| const intPosX = opt.x || 0 | |
| const intPosY = opt.y || 0 | |
| const intSizeX = opt.w || 2 | |
| const intSizeY = opt.h || 2 | |
| const strData = opt.data || '' | |
| const strLink = opt.link || '' | |
| const strPath = opt.path || '' | |
| const strType = opt.type || 'audio' | |
| let strExtn = '' | |
| const strCover = opt.cover || IMG_PLAYBTN | |
| const objectName = opt.objectName ? encodeXmlEntities(opt.objectName) : `Media ${target._slideObjects.filter(obj => obj._type === SLIDE_OBJECT_TYPES.media).length}` | |
| const slideData: ISlideObject = { _type: SLIDE_OBJECT_TYPES.media } | |
| // STEP 1: REALITY-CHECK | |
| if (!strPath && !strData && strType !== 'online') { | |
| throw new Error('addMedia() error: either `data` or `path` are required!') | |
| } else if (strData && !strData.toLowerCase().includes('base64,')) { | |
| throw new Error('addMedia() error: `data` value lacks a base64 header! Ex: \'video/mpeg;base64,NMP[...]\')') | |
| } else if (strCover && !strCover.toLowerCase().includes('base64,')) { | |
| throw new Error('addMedia() error: `cover` value lacks a base64 header! Ex: \'data:image/png;base64,iV[...]\')') | |
| } | |
| // Online Video: requires `link` | |
| if (strType === 'online' && !strLink) { | |
| throw new Error('addMedia() error: online videos require `link` value') | |
| } | |
| // FIXME: 20190707 | |
| // strType = strData ? strData.split(';')[0].split('/')[0] : strType | |
| strExtn = opt.extn || (strData ? strData.split(';')[0].split('/')[1] : strPath.split('.').pop()) || 'mp3' | |
| // STEP 2: Set type, media | |
| slideData.mtype = strType | |
| slideData.media = strPath || 'preencoded.mov' | |
| slideData.options = {} | |
| // STEP 3: Set media properties & options | |
| slideData.options.x = intPosX | |
| slideData.options.y = intPosY | |
| slideData.options.w = intSizeX | |
| slideData.options.h = intSizeY | |
| slideData.options.objectName = objectName | |
| // STEP 4: Add this media to this Slide Rels (rId/rels count spans all slides! Count all media to get next rId) | |
| /** | |
| * NOTE: | |
| * - rId starts at 2 (hence the intRels+1 below) as slideLayout.xml is rId=1! | |
| * | |
| * NOTE: | |
| * - Audio/Video files consume *TWO* rId's: | |
| * <Relationship Id="rId2" Target="../media/media1.mov" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/video"/> | |
| * <Relationship Id="rId3" Target="../media/media1.mov" Type="http://schemas.microsoft.com/office/2007/relationships/media"/> | |
| */ | |
| if (strType === 'online') { | |
| const relId1 = getNewRelId(target) | |
| // A: Add video | |
| target._relsMedia.push({ | |
| path: strPath || 'preencoded' + strExtn, | |
| data: 'dummy', | |
| type: 'online', | |
| extn: strExtn, | |
| rId: relId1, | |
| Target: strLink, | |
| }) | |
| slideData.mediaRid = relId1 | |
| // B: Add cover (preview/overlay) image | |
| target._relsMedia.push({ | |
| path: 'preencoded.png', | |
| data: strCover, | |
| type: 'image/png', | |
| extn: 'png', | |
| rId: getNewRelId(target), | |
| Target: `../media/image-${target._slideNum}-${target._relsMedia.length + 1}.png`, | |
| }) | |
| } else { | |
| // PERF: Duplicate media should reuse existing `Target` value and not create an additional copy | |
| const dupeItem = target._relsMedia.filter(item => item.path && item.path === strPath && item.type === strType + '/' + strExtn && !item.isDuplicate)[0] | |
| // A: "relationships/video" | |
| const relId1 = getNewRelId(target) | |
| target._relsMedia.push({ | |
| path: strPath || 'preencoded' + strExtn, | |
| type: strType + '/' + strExtn, | |
| extn: strExtn, | |
| data: strData || '', | |
| rId: relId1, | |
| isDuplicate: !!(dupeItem?.Target), | |
| Target: dupeItem?.Target ? dupeItem.Target : `../media/media-${target._slideNum}-${target._relsMedia.length + 1}.${strExtn}`, | |
| }) | |
| slideData.mediaRid = relId1 | |
| // B: "relationships/media" | |
| target._relsMedia.push({ | |
| path: strPath || 'preencoded' + strExtn, | |
| type: strType + '/' + strExtn, | |
| extn: strExtn, | |
| data: strData || '', | |
| rId: getNewRelId(target), | |
| isDuplicate: !!(dupeItem?.Target), | |
| Target: dupeItem?.Target ? dupeItem.Target : `../media/media-${target._slideNum}-${target._relsMedia.length + 0}.${strExtn}`, | |
| }) | |
| // C: Add cover (preview/overlay) image | |
| target._relsMedia.push({ | |
| path: 'preencoded.png', | |
| type: 'image/png', | |
| extn: 'png', | |
| data: strCover, | |
| rId: getNewRelId(target), | |
| Target: `../media/image-${target._slideNum}-${target._relsMedia.length + 1}.png`, | |
| }) | |
| } | |
| // LAST | |
| target._slideObjects.push(slideData) | |
| } | |
| /** | |
| * Adds Notes to a slide. | |
| * @param {PresSlide} `target` slide object | |
| * @param {string} `notes` | |
| * @since 2.3.0 | |
| */ | |
| export function addNotesDefinition(target: PresSlide, notes: string): void { | |
| target._slideObjects.push({ | |
| _type: SLIDE_OBJECT_TYPES.notes, | |
| text: [{ text: notes }], | |
| }) | |
| } | |
| /** | |
| * Adds a formula (Office Math / OMML) object to a slide definition. | |
| * @param {PresSlide} target slide object that the formula should be added to | |
| * @param {FormulaProps} opts formula options | |
| */ | |
| export function addFormulaDefinition(target: PresSlide, opts: FormulaProps): void { | |
| const newObject: ISlideObject = { | |
| _type: SLIDE_OBJECT_TYPES.formula, | |
| options: { | |
| x: opts.x || 0, | |
| y: opts.y || 0, | |
| w: opts.w, | |
| h: opts.h, | |
| objectName: opts.objectName | |
| ? encodeXmlEntities(opts.objectName) | |
| : `Formula ${target._slideObjects.filter((obj) => obj._type === SLIDE_OBJECT_TYPES.formula).length}`, | |
| fontSize: opts.fontSize, | |
| color: opts.color, | |
| }, | |
| formula: opts.omml, | |
| formulaAlign: opts.align || 'center', | |
| } | |
| target._slideObjects.push(newObject) | |
| } | |
| /** | |
| * Adds a shape object to a slide definition. | |
| * @param {PresSlide} target slide object that the shape should be added to | |
| * @param {SHAPE_NAME} shapeName shape name | |
| * @param {ShapeProps} opts shape options | |
| */ | |
| export function addShapeDefinition(target: PresSlide, shapeName: SHAPE_NAME, opts: ShapeProps): void { | |
| const options = typeof opts === 'object' ? opts : {} | |
| options.line = options.line || { type: 'none' } | |
| const newObject: ISlideObject = { | |
| _type: SLIDE_OBJECT_TYPES.text, | |
| shape: shapeName || SHAPE_TYPE.RECTANGLE, | |
| options, | |
| text: null, | |
| } | |
| // Reality check | |
| if (!shapeName) throw new Error('Missing/Invalid shape parameter! Example: `addShape(pptxgen.shapes.LINE, {x:1, y:1, w:1, h:1});`') | |
| // 1: ShapeLineProps defaults | |
| const newLineOpts: ShapeLineProps = { | |
| type: options.line.type || 'solid', | |
| color: options.line.color || DEF_SHAPE_LINE_COLOR, | |
| transparency: options.line.transparency || 0, | |
| width: options.line.width || 1, | |
| dashType: options.line.dashType || 'solid', | |
| beginArrowType: options.line.beginArrowType || null, | |
| endArrowType: options.line.endArrowType || null, | |
| } | |
| if (typeof options.line === 'object' && options.line.type !== 'none') options.line = newLineOpts | |
| // 2: Set options defaults | |
| options.x = options.x || (options.x === 0 ? 0 : 1) | |
| options.y = options.y || (options.y === 0 ? 0 : 1) | |
| options.w = options.w || (options.w === 0 ? 0 : 1) | |
| options.h = options.h || (options.h === 0 ? 0 : 1) | |
| options.objectName = options.objectName | |
| ? encodeXmlEntities(options.objectName) | |
| : `Shape ${target._slideObjects.filter(obj => obj._type === SLIDE_OBJECT_TYPES.text).length}` | |
| // 3: Handle line (lots of deprecated opts) | |
| if (typeof options.line === 'string') { | |
| const tmpOpts = newLineOpts | |
| tmpOpts.color = String(options.line) // @deprecated `options.line` string (was line color) | |
| options.line = tmpOpts | |
| } | |
| if (typeof options.lineSize === 'number') options.line.width = options.lineSize // @deprecated (part of `ShapeLineProps` now) | |
| if (typeof options.lineDash === 'string') options.line.dashType = options.lineDash // @deprecated (part of `ShapeLineProps` now) | |
| if (typeof options.lineHead === 'string') options.line.beginArrowType = options.lineHead // @deprecated (part of `ShapeLineProps` now) | |
| if (typeof options.lineTail === 'string') options.line.endArrowType = options.lineTail // @deprecated (part of `ShapeLineProps` now) | |
| // 4: Create hyperlink rels | |
| createHyperlinkRels(target, newObject) | |
| // LAST: Add object to slide | |
| target._slideObjects.push(newObject) | |
| } | |
| /** | |
| * Adds a table object to a slide definition. | |
| * @param {PresSlide} target - slide object that the table should be added to | |
| * @param {TableRow[]} tableRows - table data | |
| * @param {TableProps} options - table options | |
| * @param {SlideLayout} slideLayout - Slide layout | |
| * @param {PresLayout} presLayout - Presentation layout | |
| * @param {Function} addSlide - method | |
| * @param {Function} getSlide - method | |
| */ | |
| export function addTableDefinition( | |
| target: PresSlide, | |
| tableRows: TableRow[], | |
| options: TableProps, | |
| slideLayout: SlideLayout, | |
| presLayout: PresLayout, | |
| addSlide: (options?: AddSlideProps) => PresSlide, | |
| getSlide: (slideNumber: number) => PresSlide | |
| ): PresSlide[] { | |
| const slides: PresSlide[] = [target] // Create array of Slides as more may be added by auto-paging | |
| const opt: TableProps = options && typeof options === 'object' ? options : {} | |
| opt.objectName = opt.objectName ? encodeXmlEntities(opt.objectName) : `Table ${target._slideObjects.filter(obj => obj._type === SLIDE_OBJECT_TYPES.table).length}` | |
| // STEP 1: REALITY-CHECK | |
| { | |
| // A: check for empty | |
| if (tableRows === null || tableRows.length === 0 || !Array.isArray(tableRows)) { | |
| throw new Error('addTable: Array expected! EX: \'slide.addTable( [rows], {options} );\' (https://gitbrent.github.io/PptxGenJS/docs/api-tables.html)') | |
| } | |
| // B: check for non-well-formatted array (ex: rows=['a','b'] instead of [['a','b']]) | |
| if (!tableRows[0] || !Array.isArray(tableRows[0])) { | |
| throw new Error( | |
| 'addTable: \'rows\' should be an array of cells! EX: \'slide.addTable( [ [\'A\'], [\'B\'], {text:\'C\',options:{align:\'center\'}} ] );\' (https://gitbrent.github.io/PptxGenJS/docs/api-tables.html)' | |
| ) | |
| } | |
| // TODO: FUTURE: This is wacky and wont function right (shows .w value when there is none from demo.js?!) 20191219 | |
| /* | |
| if (opt.w && opt.colW) { | |
| console.warn('addTable: please use either `colW` or `w` - not both (table will use `colW` and ignore `w`)') | |
| console.log(`${opt.w} ${opt.colW}`) | |
| } | |
| */ | |
| } | |
| // STEP 2: Transform `tableRows` into well-formatted TableCell's | |
| // tableRows can be object or plain text array: `[{text:'cell 1'}, {text:'cell 2', options:{color:'ff0000'}}]` | `["cell 1", "cell 2"]` | |
| const arrRows: TableCell[][] = [] | |
| tableRows.forEach(row => { | |
| const newRow: TableCell[] = [] | |
| if (Array.isArray(row)) { | |
| row.forEach((cell: number | string | TableCell) => { | |
| // A: | |
| const newCell: TableCell = { | |
| _type: SLIDE_OBJECT_TYPES.tablecell, | |
| text: '', | |
| options: typeof cell === 'object' && cell.options ? cell.options : {}, | |
| } | |
| // B: | |
| if (typeof cell === 'string' || typeof cell === 'number') newCell.text = cell.toString() | |
| else if (cell.text) { | |
| // Cell can contain complex text type, or string, or number | |
| if (typeof cell.text === 'string' || typeof cell.text === 'number') newCell.text = cell.text.toString() | |
| else if (cell.text) newCell.text = cell.text | |
| // Capture options | |
| if (cell.options && typeof cell.options === 'object') newCell.options = cell.options | |
| } | |
| // C: Set cell borders | |
| newCell.options.border = newCell.options.border || opt.border || [{ type: 'none' }, { type: 'none' }, { type: 'none' }, { type: 'none' }] | |
| const cellBorder = newCell.options.border | |
| // CASE 1: border interface is: BorderOptions | [BorderOptions, BorderOptions, BorderOptions, BorderOptions] | |
| if (!Array.isArray(cellBorder) && typeof cellBorder === 'object') newCell.options.border = [cellBorder, cellBorder, cellBorder, cellBorder] | |
| // Handle: [null, null, {type:'solid'}, null] | |
| if (!newCell.options.border[0]) newCell.options.border[0] = { type: 'none' } | |
| if (!newCell.options.border[1]) newCell.options.border[1] = { type: 'none' } | |
| if (!newCell.options.border[2]) newCell.options.border[2] = { type: 'none' } | |
| if (!newCell.options.border[3]) newCell.options.border[3] = { type: 'none' } | |
| // set complete BorderOptions for all sides | |
| const arrSides = [0, 1, 2, 3] | |
| arrSides.forEach(idx => { | |
| newCell.options.border[idx] = { | |
| type: newCell.options.border[idx].type || DEF_CELL_BORDER.type, | |
| color: newCell.options.border[idx].color || DEF_CELL_BORDER.color, | |
| pt: typeof newCell.options.border[idx].pt === 'number' ? newCell.options.border[idx].pt : DEF_CELL_BORDER.pt, | |
| } | |
| }) | |
| // LAST: | |
| newRow.push(newCell) | |
| }) | |
| } else { | |
| console.log('addTable: tableRows has a bad row. A row should be an array of cells. You provided:') | |
| console.log(row) | |
| } | |
| arrRows.push(newRow) | |
| }) | |
| // STEP 3: Set options | |
| opt.x = getSmartParseNumber(opt.x || (opt.x === 0 ? 0 : EMU / 2), 'X', presLayout) | |
| opt.y = getSmartParseNumber(opt.y || (opt.y === 0 ? 0 : EMU / 2), 'Y', presLayout) | |
| if (opt.h) opt.h = getSmartParseNumber(opt.h, 'Y', presLayout) // NOTE: Dont set default `h` - leaving it null triggers auto-rowH in `makeXMLSlide()` | |
| opt.fontSize = opt.fontSize || DEF_FONT_SIZE | |
| opt.margin = opt.margin === 0 || opt.margin ? opt.margin : DEF_CELL_MARGIN_IN | |
| if (typeof opt.margin === 'number') opt.margin = [Number(opt.margin), Number(opt.margin), Number(opt.margin), Number(opt.margin)] | |
| // NOTE: dont add default color on tables with hyperlinks! (it causes any textObj's with hyperlinks to have subsequent words to be black) | |
| if (JSON.stringify({ arrRows: arrRows }).indexOf('hyperlink') === -1) { | |
| if (!opt.color) opt.color = opt.color || DEF_FONT_COLOR // Set default color if needed (table option > inherit from Slide > default to black) | |
| } | |
| if (typeof opt.border === 'string') { | |
| console.warn('addTable `border` option must be an object. Ex: `{border: {type:\'none\'}}`') | |
| opt.border = null | |
| } else if (Array.isArray(opt.border)) { | |
| [0, 1, 2, 3].forEach(idx => { | |
| opt.border[idx] = opt.border[idx] | |
| ? { type: opt.border[idx].type || DEF_CELL_BORDER.type, color: opt.border[idx].color || DEF_CELL_BORDER.color, pt: opt.border[idx].pt || DEF_CELL_BORDER.pt } | |
| : { type: 'none' } | |
| }) | |
| } | |
| opt.autoPage = typeof opt.autoPage === 'boolean' ? opt.autoPage : false | |
| opt.autoPageRepeatHeader = typeof opt.autoPageRepeatHeader === 'boolean' ? opt.autoPageRepeatHeader : false | |
| opt.autoPageHeaderRows = typeof opt.autoPageHeaderRows !== 'undefined' && !isNaN(Number(opt.autoPageHeaderRows)) ? Number(opt.autoPageHeaderRows) : 1 | |
| opt.autoPageLineWeight = typeof opt.autoPageLineWeight !== 'undefined' && !isNaN(Number(opt.autoPageLineWeight)) ? Number(opt.autoPageLineWeight) : 0 | |
| if (opt.autoPageLineWeight) { | |
| if (opt.autoPageLineWeight > 1) opt.autoPageLineWeight = 1 | |
| else if (opt.autoPageLineWeight < -1) opt.autoPageLineWeight = -1 | |
| } | |
| // autoPage ^^^ | |
| // Set/Calc table width | |
| // Get slide margins - start with default values, then adjust if master or slide margins exist | |
| let arrTableMargin = DEF_SLIDE_MARGIN_IN | |
| // Case 1: Master margins | |
| if (slideLayout && typeof slideLayout._margin !== 'undefined') { | |
| if (Array.isArray(slideLayout._margin)) arrTableMargin = slideLayout._margin | |
| else if (!isNaN(Number(slideLayout._margin))) { arrTableMargin = [Number(slideLayout._margin), Number(slideLayout._margin), Number(slideLayout._margin), Number(slideLayout._margin)] } | |
| } | |
| // Case 2: Table margins | |
| /* FIXME: add `_margin` option to slide options | |
| else if ( addNewSlide._margin ) { | |
| if ( Array.isArray(addNewSlide._margin) ) arrTableMargin = addNewSlide._margin; | |
| else if ( !isNaN(Number(addNewSlide._margin)) ) arrTableMargin = [Number(addNewSlide._margin), Number(addNewSlide._margin), Number(addNewSlide._margin), Number(addNewSlide._margin)]; | |
| } | |
| */ | |
| /** | |
| * Calc table width depending upon what data we have - several scenarios exist (including bad data, eg: colW doesnt match col count) | |
| * The API does not require a `w` value, but XML generation does, hence, code to calc a width below using colW value(s) | |
| */ | |
| if (opt.colW) { | |
| const firstRowColCnt = arrRows[0].reduce((totalLen, c) => { | |
| if (c?.options?.colspan && typeof c.options.colspan === 'number') { | |
| totalLen += c.options.colspan | |
| } else { | |
| totalLen += 1 | |
| } | |
| return totalLen | |
| }, 0) | |
| if (typeof opt.colW === 'string' || typeof opt.colW === 'number') { | |
| // Ex: `colW = 3` or `colW = '3'` | |
| opt.w = Math.floor(Number(opt.colW) * firstRowColCnt) | |
| opt.colW = null // IMPORTANT: Unset `colW` so table is created using `opt.w`, which will evenly divide cols | |
| } else if (opt.colW && Array.isArray(opt.colW) && opt.colW.length === 1 && firstRowColCnt > 1) { | |
| // Ex: `colW=[3]` but with >1 cols (same as above, user is saying "use this width for all") | |
| opt.w = Math.floor(Number(opt.colW) * firstRowColCnt) | |
| opt.colW = null // IMPORTANT: Unset `colW` so table is created using `opt.w`, which will evenly divide cols | |
| } else if (opt.colW && Array.isArray(opt.colW) && opt.colW.length !== firstRowColCnt) { | |
| // Err: Mismatched colW and cols count | |
| console.warn('addTable: mismatch: (colW.length != data.length) Therefore, defaulting to evenly distributed col widths.') | |
| opt.colW = null | |
| } | |
| } else if (opt.w) { | |
| opt.w = getSmartParseNumber(opt.w, 'X', presLayout) | |
| } else { | |
| opt.w = Math.floor(presLayout._sizeW / EMU - arrTableMargin[1] - arrTableMargin[3]) | |
| } | |
| // STEP 4: Convert units to EMU now (we use different logic in makeSlide->table - smartCalc is not used) | |
| if (opt.x && opt.x < 20) opt.x = inch2Emu(opt.x) | |
| if (opt.y && opt.y < 20) opt.y = inch2Emu(opt.y) | |
| if (opt.w && typeof opt.w === 'number' && opt.w < 20) opt.w = inch2Emu(opt.w) | |
| if (opt.h && typeof opt.h === 'number' && opt.h < 20) opt.h = inch2Emu(opt.h) | |
| // STEP 5: Loop over cells: transform each to ITableCell; check to see whether to unset `autoPage` while here | |
| arrRows.forEach(row => { | |
| row.forEach((cell, idy) => { | |
| // A: Transform cell data if needed | |
| /* Table rows can be an object or plain text - transform into object when needed | |
| // EX: | |
| var arrTabRows1 = [ | |
| [ { text:'A1\nA2', options:{rowspan:2, fill:'99FFCC'} } ] | |
| ,[ 'B2', 'C2', 'D2', 'E2' ] | |
| ] | |
| */ | |
| if (typeof cell === 'number' || typeof cell === 'string') { | |
| // Grab table formatting `opts` to use here so text style/format inherits as it should | |
| row[idy] = { _type: SLIDE_OBJECT_TYPES.tablecell, text: String(row[idy]), options: opt } | |
| } else if (typeof cell === 'object') { | |
| // ARG0: `text` | |
| if (typeof cell.text === 'number') row[idy].text = row[idy].text.toString() | |
| else if (typeof cell.text === 'undefined' || cell.text === null) row[idy].text = '' | |
| // ARG1: `options`: ensure options exists | |
| row[idy].options = cell.options || {} | |
| // Set type to tabelcell | |
| row[idy]._type = SLIDE_OBJECT_TYPES.tablecell | |
| } | |
| // B: Check for fine-grained formatting, disable auto-page when found | |
| // Since genXmlTextBody already checks for text array ( text:[{},..{}] ) we're done! | |
| // Text in individual cells will be formatted as they are added by calls to genXmlTextBody within table builder | |
| // if (cell.text && Array.isArray(cell.text)) opt.autoPage = false | |
| // TODO: FIXME: WIP: 20210807: We cant do this anymore | |
| }) | |
| }) | |
| // If autoPage = true, we need to return references to newly created slides if any | |
| const newAutoPagedSlides: PresSlide[] = [] | |
| // STEP 6: Auto-Paging: (via {options} and used internally) | |
| // (used internally by `tableToSlides()` to not engage recursion - we've already paged the table data, just add this one) | |
| if (opt && !opt.autoPage) { | |
| // Create hyperlink rels (IMPORTANT: Wait until table has been shredded across Slides or all rels will end-up on Slide 1!) | |
| createHyperlinkRels(target, arrRows) | |
| // Add slideObjects (NOTE: Use `extend` to avoid mutation) | |
| target._slideObjects.push({ | |
| _type: SLIDE_OBJECT_TYPES.table, | |
| arrTabRows: arrRows, | |
| options: Object.assign({}, opt), | |
| }) | |
| } else { | |
| if (opt.autoPageRepeatHeader) opt._arrObjTabHeadRows = arrRows.filter((_row, idx) => idx < opt.autoPageHeaderRows) | |
| // Loop over rows and create 1-N tables as needed (ISSUE#21) | |
| getSlidesForTableRows(arrRows, opt, presLayout, slideLayout).forEach((slide, idx) => { | |
| // A: Create new Slide when needed, otherwise, use existing (NOTE: More than 1 table can be on a Slide, so we will go up AND down the Slide chain) | |
| if (!getSlide(target._slideNum + idx)) slides.push(addSlide({ masterName: slideLayout?._name || null })) | |
| // B: Reset opt.y to `option`/`margin` after first Slide (ISSUE#43, ISSUE#47, ISSUE#48) | |
| if (idx > 0) opt.y = inch2Emu(opt.autoPageSlideStartY || opt.newSlideStartY || arrTableMargin[0]) | |
| // C: Add this table to new Slide | |
| { | |
| const newSlide: PresSlide = getSlide(target._slideNum + idx) | |
| opt.autoPage = false | |
| // Create hyperlink rels (IMPORTANT: Wait until table has been shredded across Slides or all rels will end-up on Slide 1!) | |
| createHyperlinkRels(newSlide, slide.rows) | |
| // Add rows to new slide | |
| newSlide.addTable(slide.rows, Object.assign({}, opt)) | |
| // Add reference to the new slide so it can be returned, but don't add the first one because the user already has a reference to that one. | |
| if (idx > 0) newAutoPagedSlides.push(newSlide) | |
| } | |
| }) | |
| } | |
| return newAutoPagedSlides | |
| } | |
| /** | |
| * Adds a text object to a slide definition. | |
| * @param {PresSlide} target - slide object that the text should be added to | |
| * @param {string|TextProps[]} text text string or object | |
| * @param {TextPropsOptions} opts text options | |
| * @param {boolean} isPlaceholder whether this a placeholder object | |
| * @since: 1.0.0 | |
| */ | |
| export function addTextDefinition(target: PresSlide, text: TextProps[], opts: TextPropsOptions, isPlaceholder: boolean): void { | |
| const newObject: ISlideObject = { | |
| _type: isPlaceholder ? SLIDE_OBJECT_TYPES.placeholder : SLIDE_OBJECT_TYPES.text, | |
| shape: (opts?.shape) || SHAPE_TYPE.RECTANGLE, | |
| text: !text || text.length === 0 ? [{ text: '', options: null }] : text, | |
| options: opts || {}, | |
| } | |
| function cleanOpts(itemOpts: ObjectOptions): TextPropsOptions { | |
| // STEP 1: Set some options | |
| { | |
| // A.1: Color (placeholders should inherit their colors or override them, so don't default them) | |
| if (!itemOpts.placeholder) { | |
| itemOpts.color = itemOpts.color || newObject.options.color || target.color || DEF_FONT_COLOR | |
| } | |
| // A.2: Placeholder should inherit their bullets or override them, so don't default them | |
| if (itemOpts.placeholder || isPlaceholder) { | |
| itemOpts.bullet = itemOpts.bullet || false | |
| } | |
| // A.3: Text targeting a placeholder need to inherit the placeholders options (eg: margin, valign, etc.) (Issue #640) | |
| if (itemOpts.placeholder && target._slideLayout && target._slideLayout._slideObjects) { | |
| const placeHold = target._slideLayout._slideObjects.filter( | |
| item => item._type === 'placeholder' && item.options && item.options.placeholder && item.options.placeholder === itemOpts.placeholder | |
| )[0] | |
| if (placeHold?.options) itemOpts = { ...itemOpts, ...placeHold.options } | |
| } | |
| // A.4: Other options | |
| itemOpts.objectName = itemOpts.objectName | |
| ? encodeXmlEntities(itemOpts.objectName) | |
| : `Text ${target._slideObjects.filter(obj => obj._type === SLIDE_OBJECT_TYPES.text).length}` | |
| // B: | |
| if (itemOpts.shape === SHAPE_TYPE.LINE) { | |
| // ShapeLineProps defaults | |
| const newLineOpts: ShapeLineProps = { | |
| type: itemOpts.line.type || 'solid', | |
| color: itemOpts.line.color || DEF_SHAPE_LINE_COLOR, | |
| transparency: itemOpts.line.transparency || 0, | |
| width: itemOpts.line.width || 1, | |
| dashType: itemOpts.line.dashType || 'solid', | |
| beginArrowType: itemOpts.line.beginArrowType || null, | |
| endArrowType: itemOpts.line.endArrowType || null, | |
| } | |
| if (typeof itemOpts.line === 'object') itemOpts.line = newLineOpts | |
| // 3: Handle line (lots of deprecated opts) | |
| if (typeof itemOpts.line === 'string') { | |
| const tmpOpts = newLineOpts | |
| if (typeof itemOpts.line === 'string') tmpOpts.color = itemOpts.line // @deprecated [remove in v4.0] | |
| // tmpOpts.color = itemOpts.line!.toString() // @deprecated `itemOpts.line`:[string] (was line color) | |
| itemOpts.line = tmpOpts | |
| } | |
| if (typeof itemOpts.lineSize === 'number') itemOpts.line.width = itemOpts.lineSize // @deprecated (part of `ShapeLineProps` now) | |
| if (typeof itemOpts.lineDash === 'string') itemOpts.line.dashType = itemOpts.lineDash // @deprecated (part of `ShapeLineProps` now) | |
| if (typeof itemOpts.lineHead === 'string') itemOpts.line.beginArrowType = itemOpts.lineHead // @deprecated (part of `ShapeLineProps` now) | |
| if (typeof itemOpts.lineTail === 'string') itemOpts.line.endArrowType = itemOpts.lineTail // @deprecated (part of `ShapeLineProps` now) | |
| } | |
| // C: Line opts | |
| itemOpts.line = itemOpts.line || {} | |
| itemOpts.lineSpacing = itemOpts.lineSpacing && !isNaN(itemOpts.lineSpacing) ? itemOpts.lineSpacing : null | |
| itemOpts.lineSpacingMultiple = itemOpts.lineSpacingMultiple && !isNaN(itemOpts.lineSpacingMultiple) ? itemOpts.lineSpacingMultiple : null | |
| // D: Transform text options to bodyProperties as thats how we build XML | |
| itemOpts._bodyProp = itemOpts._bodyProp || {} | |
| itemOpts._bodyProp.autoFit = itemOpts.autoFit || false // DEPRECATED: (3.3.0) If true, shape will collapse to text size (Fit To shape) | |
| itemOpts._bodyProp.anchor = !itemOpts.placeholder ? TEXT_VALIGN.ctr : null // VALS: [t,ctr,b] | |
| itemOpts._bodyProp.vert = itemOpts.vert || null // VALS: [eaVert,horz,mongolianVert,vert,vert270,wordArtVert,wordArtVertRtl] | |
| itemOpts._bodyProp.wrap = typeof itemOpts.wrap === 'boolean' ? itemOpts.wrap : true | |
| // E: Inset | |
| // @deprecated 3.10.0 (`inset` - use `margin`) | |
| if ((itemOpts.inset && !isNaN(Number(itemOpts.inset))) || itemOpts.inset === 0) { | |
| itemOpts._bodyProp.lIns = inch2Emu(itemOpts.inset) | |
| itemOpts._bodyProp.rIns = inch2Emu(itemOpts.inset) | |
| itemOpts._bodyProp.tIns = inch2Emu(itemOpts.inset) | |
| itemOpts._bodyProp.bIns = inch2Emu(itemOpts.inset) | |
| } | |
| // F: Transform @deprecated props | |
| if (typeof itemOpts.underline === 'boolean' && itemOpts.underline === true) itemOpts.underline = { style: 'sng' } | |
| } | |
| // STEP 2: Transform `align`/`valign` to XML values, store in _bodyProp for XML gen | |
| { | |
| if ((itemOpts.align || '').toLowerCase().indexOf('c') === 0) itemOpts._bodyProp.align = TEXT_HALIGN.center | |
| else if ((itemOpts.align || '').toLowerCase().indexOf('l') === 0) itemOpts._bodyProp.align = TEXT_HALIGN.left | |
| else if ((itemOpts.align || '').toLowerCase().indexOf('r') === 0) itemOpts._bodyProp.align = TEXT_HALIGN.right | |
| else if ((itemOpts.align || '').toLowerCase().indexOf('j') === 0) itemOpts._bodyProp.align = TEXT_HALIGN.justify | |
| if ((itemOpts.valign || '').toLowerCase().indexOf('b') === 0) itemOpts._bodyProp.anchor = TEXT_VALIGN.b | |
| else if ((itemOpts.valign || '').toLowerCase().indexOf('m') === 0) itemOpts._bodyProp.anchor = TEXT_VALIGN.ctr | |
| else if ((itemOpts.valign || '').toLowerCase().indexOf('t') === 0) itemOpts._bodyProp.anchor = TEXT_VALIGN.t | |
| } | |
| // STEP 3: ROBUST: Set rational values for some shadow props if needed | |
| correctShadowOptions(itemOpts.shadow) | |
| return itemOpts | |
| } | |
| // STEP 1: Create/Clean object options | |
| newObject.options = cleanOpts(newObject.options) | |
| // STEP 2: Create/Clean text options | |
| newObject.text.forEach(item => (item.options = cleanOpts(item.options || {}))) | |
| // STEP 3: Create hyperlinks | |
| createHyperlinkRels(target, newObject.text || '') | |
| // LAST: Add object to Slide | |
| target._slideObjects.push(newObject) | |
| } | |
| /** | |
| * Adds placeholder objects to slide | |
| * @param {PresSlide} slide - slide object containing layouts | |
| */ | |
| export function addPlaceholdersToSlideLayouts(slide: PresSlide): void { | |
| // Add all placeholders on this Slide that dont already exist | |
| (slide._slideLayout._slideObjects || []).forEach(slideLayoutObj => { | |
| if (slideLayoutObj._type === SLIDE_OBJECT_TYPES.placeholder) { | |
| // A: Search for this placeholder on Slide before we add | |
| // NOTE: Check to ensure a placeholder does not already exist on the Slide | |
| // They are created when they have been populated with text (ex: `slide.addText('Hi', { placeholder:'title' });`) | |
| if (slide._slideObjects.filter(slideObj => slideObj.options && slideObj.options.placeholder === slideLayoutObj.options.placeholder).length === 0) { | |
| addTextDefinition(slide, [{ text: '' }], slideLayoutObj.options, false) | |
| } | |
| } | |
| }) | |
| } | |
| /* -------------------------------------------------------------------------------- */ | |
| /** | |
| * Adds a background image or color to a slide definition. | |
| * @param {BackgroundProps} props - color string or an object with image definition | |
| * @param {PresSlide} target - slide object that the background is set to | |
| */ | |
| export function addBackgroundDefinition(props: BackgroundProps, target: SlideLayout): void { | |
| // A: @deprecated | |
| if (target.bkgd) { | |
| if (!target.background) target.background = {} | |
| if (typeof target.bkgd === 'string') target.background.color = target.bkgd | |
| else { | |
| if (target.bkgd.data) target.background.data = target.bkgd.data | |
| if (target.bkgd.path) target.background.path = target.bkgd.path | |
| if (target.bkgd.src) target.background.path = target.bkgd.src // @deprecated (drop in 4.x) | |
| } | |
| } | |
| if (target.background?.fill) target.background.color = target.background.fill | |
| // B: Handle media | |
| if (props && (props.path || props.data)) { | |
| // Allow the use of only the data key (`path` isnt reqd) | |
| props.path = props.path || 'preencoded.png' | |
| let strImgExtn = (props.path.split('.').pop() || 'png').split('?')[0] // Handle "blah.jpg?width=540" etc. | |
| if (strImgExtn === 'jpg') strImgExtn = 'jpeg' // base64-encoded jpg's come out as "data:image/jpeg;base64,/9j/[...]", so correct exttnesion to avoid content warnings at PPT startup | |
| target._relsMedia = target._relsMedia || [] | |
| const intRels = target._relsMedia.length + 1 | |
| // NOTE: `Target` cannot have spaces (eg:"Slide 1-image-1.jpg") or a "presentation is corrupt" warning comes up | |
| target._relsMedia.push({ | |
| path: props.path, | |
| type: SLIDE_OBJECT_TYPES.image, | |
| extn: strImgExtn, | |
| data: props.data || null, | |
| rId: intRels, | |
| Target: `../media/${(target._name || '').replace(/\s+/gi, '-')}-image-${target._relsMedia.length + 1}.${strImgExtn}`, | |
| }) | |
| target._bkgdImgRid = intRels | |
| } | |
| } | |
| /** | |
| * Parses text/text-objects from `addText()` and `addTable()` methods; creates 'hyperlink'-type Slide Rels for each hyperlink found | |
| * @param {PresSlide} target - slide object that any hyperlinks will be be added to | |
| * @param {number | string | TextProps | TextProps[] | ITableCell[][]} text - text to parse | |
| */ | |
| function createHyperlinkRels( | |
| target: PresSlide, | |
| text: number | string | ISlideObject | TextProps | TextProps[] | TableCell[][], | |
| options?: TextPropsOptions[], | |
| ): void { | |
| let textObjs = [] | |
| // Only text objects can have hyperlinks, bail when text param is plain text | |
| if (typeof text === 'string' || typeof text === 'number') return | |
| // IMPORTANT: "else if" Array.isArray must come before typeof===object! Otherwise, code will exhaust recursion! | |
| else if (Array.isArray(text)) textObjs = text | |
| else if (typeof text === 'object') textObjs = [text] | |
| textObjs.forEach((text: TextProps, idx: number) => { | |
| // IMPORTANT: `options` are lost due to recursion/copy! | |
| if (options && options[idx] && options[idx].hyperlink) text.options = { ...text.options, ...options[idx] } | |
| // NOTE: `text` can be an array of other `text` objects (table cell word-level formatting), continue parsing using recursion | |
| if (Array.isArray(text)) { | |
| const cellOpts = [] | |
| text.forEach((tablecell) => { | |
| if (tablecell.options && !tablecell.text.options) { | |
| cellOpts.push(tablecell.options) | |
| } | |
| }) | |
| createHyperlinkRels(target, text, cellOpts) | |
| } else if (Array.isArray(text.text)) { | |
| createHyperlinkRels(target, text.text, options && options[idx] ? [options[idx]] : undefined) | |
| } else if (text && typeof text === 'object' && text.options && text.options.hyperlink && !text.options.hyperlink._rId) { | |
| if (typeof text.options.hyperlink !== 'object') { | |
| console.log('ERROR: text `hyperlink` option should be an object. Ex: `hyperlink: {url:\'https://github.com\'}` ') | |
| } | |
| else if (!text.options.hyperlink.url && !text.options.hyperlink.slide) { | |
| console.log('ERROR: \'hyperlink requires either: `url` or `slide`\'') | |
| } | |
| else { | |
| const relId = getNewRelId(target) | |
| target._rels.push({ | |
| type: SLIDE_OBJECT_TYPES.hyperlink, | |
| data: text.options.hyperlink.slide ? 'slide' : 'dummy', | |
| rId: relId, | |
| Target: encodeXmlEntities(text.options.hyperlink.url) || text.options.hyperlink.slide.toString(), | |
| }) | |
| text.options.hyperlink._rId = relId | |
| } | |
| } | |
| else if (text && typeof text === 'object' && text.options && text.options.hyperlink && text.options.hyperlink._rId) { | |
| // NOTE: auto-paging will create new slides, but skip above as _rId exists, BUT this is a new slide, so add rels! | |
| if (target._rels.filter(rel => rel.rId === text.options.hyperlink._rId).length === 0) { | |
| target._rels.push({ | |
| type: SLIDE_OBJECT_TYPES.hyperlink, | |
| data: text.options.hyperlink.slide ? 'slide' : 'dummy', | |
| rId: text.options.hyperlink._rId, | |
| Target: encodeXmlEntities(text.options.hyperlink.url) || text.options.hyperlink.slide.toString(), | |
| }) | |
| } | |
| } | |
| }) | |
| } | |