/**
* 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
}