/** * PptxGenJS: Utility Methods */ import { EMU, REGEX_HEX_COLOR, DEF_FONT_COLOR, ONEPT, SchemeColor, SCHEME_COLORS } from './core-enums' import { PresLayout, TextGlowProps, PresSlide, ShapeFillProps, Color, ShapeLineProps, Coord, ShadowProps } from './core-interfaces' /** * Translates any type of `x`/`y`/`w`/`h` prop to EMU * - guaranteed to return a result regardless of undefined, null, etc. (0) * - {number} - 12800 (EMU) * - {number} - 0.5 (inches) * - {string} - "75%" * @param {number|string} size - numeric ("5.5") or percentage ("90%") * @param {'X' | 'Y'} xyDir - direction * @param {PresLayout} layout - presentation layout * @returns {number} calculated size */ export function getSmartParseNumber (size: Coord, xyDir: 'X' | 'Y', layout: PresLayout): number { // FIRST: Convert string numeric value if reqd if (typeof size === 'string' && !isNaN(Number(size))) size = Number(size) // CASE 1: Number in inches // Assume any number less than 100 is inches if (typeof size === 'number' && size < 100) return inch2Emu(size) // CASE 2: Number is already converted to something other than inches // Assume any number greater than 100 sure isnt inches! Just return it (assume value is EMU already). if (typeof size === 'number' && size >= 100) return size // CASE 3: Percentage (ex: '50%') if (typeof size === 'string' && size.includes('%')) { if (xyDir && xyDir === 'X') return Math.round((parseFloat(size) / 100) * layout.width) if (xyDir && xyDir === 'Y') return Math.round((parseFloat(size) / 100) * layout.height) // Default: Assume width (x/cx) return Math.round((parseFloat(size) / 100) * layout.width) } // LAST: Default value return 0 } /** * Basic UUID Generator Adapted * @link https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript#answer-2117523 * @param {string} uuidFormat - UUID format * @returns {string} UUID */ export function getUuid (uuidFormat: string): string { return uuidFormat.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0 const v = c === 'x' ? r : (r & 0x3) | 0x8 return v.toString(16) }) } /** * Replace special XML characters with HTML-encoded strings * @param {string} xml - XML string to encode * @returns {string} escaped XML */ export function encodeXmlEntities (xml: string): string { // NOTE: Dont use short-circuit eval here as value c/b "0" (zero) etc.! if (typeof xml === 'undefined' || xml == null) return '' return xml.toString().replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''') } /** * Convert inches into EMU * @param {number|string} inches - as string or number * @returns {number} EMU value */ export function inch2Emu (inches: number | string): number { // NOTE: Provide Caller Safety: Numbers may get conv<->conv during flight, so be kind and do some simple checks to ensure inches were passed // Any value over 100 damn sure isnt inches, so lets assume its in EMU already, therefore, just return the same value if (typeof inches === 'number' && inches > 100) return inches if (typeof inches === 'string') inches = Number(inches.replace(/in*/gi, '')) return Math.round(EMU * inches) } /** * Convert `pt` into points (using `ONEPT`) * @param {number|string} pt * @returns {number} value in points (`ONEPT`) */ export function valToPts (pt: number | string): number { const points = Number(pt) || 0 return isNaN(points) ? 0 : Math.round(points * ONEPT) } /** * Convert degrees (0..360) to PowerPoint `rot` value * @param {number} d degrees * @returns {number} calculated `rot` value */ export function convertRotationDegrees (d: number): number { d = d || 0 return Math.round((d > 360 ? d - 360 : d) * 60000) } /** * Converts component value to hex value * @param {number} c - component color * @returns {string} hex string */ export function componentToHex (c: number): string { const hex = c.toString(16) return hex.length === 1 ? '0' + hex : hex } /** * Converts RGB colors from css selectors to Hex for Presentation colors * @param {number} r - red value * @param {number} g - green value * @param {number} b - blue value * @returns {string} XML string */ export function rgbToHex (r: number, g: number, b: number): string { return (componentToHex(r) + componentToHex(g) + componentToHex(b)).toUpperCase() } /** TODO: FUTURE: TODO-4.0: * @date 2022-04-10 * @tldr this s/b a private method with all current calls switched to `genXmlColorSelection()` * @desc lots of code calls this method * @example [gen-charts.tx] `strXml += '' + createColorElement(seriesColor, ``) + ''` * Thi sis wrong. We s/b calling `genXmlColorSelection()` instead as it returns `BLAH`!! */ /** * Create either a `a:schemeClr` - (scheme color) or `a:srgbClr` (hexa representation). * @param {string|SCHEME_COLORS} colorStr - hexa representation (eg. "FFFF00") or a scheme color constant (eg. pptx.SchemeColor.ACCENT1) * @param {string} innerElements - additional elements that adjust the color and are enclosed by the color element * @returns {string} XML string */ export function createColorElement (colorStr: string | SCHEME_COLORS, innerElements?: string): string { let colorVal = (colorStr || '').replace('#', '') if ( !REGEX_HEX_COLOR.test(colorVal) && colorVal !== SchemeColor.background1 && colorVal !== SchemeColor.background2 && colorVal !== SchemeColor.text1 && colorVal !== SchemeColor.text2 && colorVal !== SchemeColor.accent1 && colorVal !== SchemeColor.accent2 && colorVal !== SchemeColor.accent3 && colorVal !== SchemeColor.accent4 && colorVal !== SchemeColor.accent5 && colorVal !== SchemeColor.accent6 ) { console.warn(`"${colorVal}" is not a valid scheme color or hex RGB! "${DEF_FONT_COLOR}" used instead. Only provide 6-digit RGB or 'pptx.SchemeColor' values!`) colorVal = DEF_FONT_COLOR } const tagName = REGEX_HEX_COLOR.test(colorVal) ? 'srgbClr' : 'schemeClr' const colorAttr = 'val="' + (REGEX_HEX_COLOR.test(colorVal) ? colorVal.toUpperCase() : colorVal) + '"' return innerElements ? `${innerElements}` : `` } /** * Creates `a:glow` element * @param {TextGlowProps} options glow properties * @param {TextGlowProps} defaults defaults for unspecified properties in `opts` * @see http://officeopenxml.com/drwSp-effects.php * { size: 8, color: 'FFFFFF', opacity: 0.75 }; */ export function createGlowElement (options: TextGlowProps, defaults: TextGlowProps): string { let strXml = '' const opts = { ...defaults, ...options } const size = Math.round(opts.size * ONEPT) const color = opts.color const opacity = Math.round(opts.opacity * 100000) strXml += `` strXml += createColorElement(color, ``) strXml += '' return strXml } /** * Create color selection * @param {Color | ShapeFillProps | ShapeLineProps} props fill props * @returns XML string */ export function genXmlColorSelection (props: Color | ShapeFillProps | ShapeLineProps): string { let fillType = 'solid' let colorVal = '' let internalElements = '' let outText = '' if (props) { if (typeof props === 'string') colorVal = props else { if (props.type) fillType = props.type if (props.color) colorVal = props.color if (props.alpha) internalElements += `` // DEPRECATED: @deprecated v3.3.0 if (props.transparency) internalElements += `` } switch (fillType) { case 'solid': outText += `${createColorElement(colorVal, internalElements)}` break default: // @note need a statement as having only "break" is removed by rollup, then tiggers "no-default" js-linter outText += '' break } } return outText } /** * Get a new rel ID (rId) for charts, media, etc. * @param {PresSlide} target - the slide to use * @returns {number} count of all current rels plus 1 for the caller to use as its "rId" */ export function getNewRelId (target: PresSlide): number { return target._rels.length + target._relsChart.length + target._relsMedia.length + 1 } /** * Checks shadow options passed by user and performs corrections if needed. * @param {ShadowProps} ShadowProps - shadow options */ export function correctShadowOptions (ShadowProps: ShadowProps): ShadowProps | undefined { if (!ShadowProps || typeof ShadowProps !== 'object') { // console.warn("`shadow` options must be an object. Ex: `{shadow: {type:'none'}}`") return } // OPT: `type` if (ShadowProps.type !== 'outer' && ShadowProps.type !== 'inner' && ShadowProps.type !== 'none') { console.warn('Warning: shadow.type options are `outer`, `inner` or `none`.') ShadowProps.type = 'outer' } // OPT: `angle` if (ShadowProps.angle) { // A: REALITY-CHECK if (isNaN(Number(ShadowProps.angle)) || ShadowProps.angle < 0 || ShadowProps.angle > 359) { console.warn('Warning: shadow.angle can only be 0-359') ShadowProps.angle = 270 } // B: ROBUST: Cast any type of valid arg to int: '12', 12.3, etc. -> 12 ShadowProps.angle = Math.round(Number(ShadowProps.angle)) } // OPT: `opacity` if (ShadowProps.opacity) { // A: REALITY-CHECK if (isNaN(Number(ShadowProps.opacity)) || ShadowProps.opacity < 0 || ShadowProps.opacity > 1) { console.warn('Warning: shadow.opacity can only be 0-1') ShadowProps.opacity = 0.75 } // B: ROBUST: Cast any type of valid arg to int: '12', 12.3, etc. -> 12 ShadowProps.opacity = Number(ShadowProps.opacity) } // OPT: `color` if (ShadowProps.color) { // INCORRECT FORMAT if (ShadowProps.color.startsWith('#')) { console.warn('Warning: shadow.color should not include hash (#) character, , e.g. "FF0000"') ShadowProps.color = ShadowProps.color.replace('#', '') } } return ShadowProps }