WHc2png.mjs

import fs from 'fs'
import path from 'path'
import puppeteer from 'puppeteer'
import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
import find from 'lodash-es/find.js'
import isnum from 'wsemi/src/isnum.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import cdbl from 'wsemi/src/cdbl.mjs'
import now2strp from 'wsemi/src/now2strp.mjs'
import genID from 'wsemi/src/genID.mjs'
import fsTreeFolder from 'wsemi/src/fsTreeFolder.mjs'
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
// import fsCopyFolder from 'wsemi/src/fsCopyFolder.mjs'


//已不須自動複製chromium, node_modules/puppeteer/.local-chromium/win64-884014


//提供給網頁用的highcharts程式碼
let code_highcharts = fs.readFileSync('./node_modules/highcharts/highcharts.js', 'utf8')
let code_stock = fs.readFileSync('./node_modules/highcharts/modules/stock.js', 'utf8')
let code_heatmap = fs.readFileSync('./node_modules/highcharts/modules/heatmap.js', 'utf8')
let code_annotations = fs.readFileSync('./node_modules/highcharts/modules/annotations.js', 'utf8')
let code_boost = fs.readFileSync('./node_modules/highcharts/modules/boost.js', 'utf8')
let code_boost_canvas = fs.readFileSync('./node_modules/highcharts/modules/boost-canvas.js', 'utf8')

//wd
let wd = process.cwd()
// console.log('process.cwd()', wd)


/**
 * 轉Highcharts設定檔(含數據)為png圖
 *
 * @class
 * @param {Number} [width=700] 輸入圖片原始寬度數字,單位px,預設700
 * @param {Number} [height=400] 輸入圖片原始高度數字,單位px,預設400
 * @param {Number} [scale=3] 輸入欲將圖片放大比例數字,單位px,預設3
 * @param {Object|String} [opt={}] 輸入Highcharts設定物件或字串,可給予iife執行程式碼直接回傳Highcharts設定物件,預設{}
 * @param {Object} [whOpt={}] 輸入設定物件,預設{}
 * @param {Array} [whOpt.addScripts=[]] 輸入引用js程式碼網址陣列,預設[]
 * @param {String} [whOpt.addCode=''] 輸入插用js程式碼字串,可提供編譯後函數例如getOpt之iife或umd等格式程式碼,預設''
 * @param {String} [whOpt.executablePath=''] 輸入puppeteer的executablePath字串,預設''
 * @param {String} [whOpt.executableFolder=''] 輸入不提供executablePath時則提供搜索chrome.exe所在資料夾字串,找到後將自動給予puppeteer的executablePath,預設''
 * @returns {Promise} 回傳Promise,resolve為回傳base64圖片,reject為錯誤訊息
 * @example
 *
 * function genPlotHtml(fp, b64) {
 *     let h = `
 * <html>
 *     <head></head>
 *     <body>
 *         <img style="border:1px dashed #ddd;" src="data:image/png;base64, {b64}">
 *     </body>
 * </html>
 *     `
 *     h = h.replace('{b64}', b64)
 *     fs.writeFileSync(fp, h, 'utf8')
 * }
 *
 *
 * async function testa() {
 *
 *     let width = 500
 *     let height = 400
 *     let scale = 3
 *     let opt = {
 *
 *         title: {
 *             text: 'Logarithmic axis demo'
 *         },
 *
 *         xAxis: {
 *             tickInterval: 1,
 *             type: 'logarithmic',
 *             accessibility: {
 *                 rangeDescription: 'Range: 1 to 10'
 *             }
 *         },
 *
 *         yAxis: {
 *             type: 'logarithmic',
 *             minorTickInterval: 0.1,
 *             accessibility: {
 *                 rangeDescription: 'Range: 0.1 to 1000'
 *             }
 *         },
 *
 *         tooltip: {
 *             headerFormat: '<b>{series.name}</b><br />',
 *             pointFormat: 'x = {point.x}, y = {point.y}'
 *         },
 *
 *         series: [{
 *             data: [1, 2, 4, 8, 16, 32, 64, 128, 256, 512],
 *             pointStart: 1
 *         }]
 *
 *     }
 *
 *     let b64 = await WHc2png(width, height, scale, opt)
 *     // console.log('b64', b64)
 *
 *     fs.writeFileSync('./test-scla.b64', b64)
 *     genPlotHtml('./test-scla.html', b64)
 *
 *     console.log('finish')
 * }
 * testa()
 *     .catch((err) => {
 *         console.log(err)
 *     })
 *
 * async function testb() {
 *
 *     let width = 500
 *     let height = 400
 *     let scale = 3
 *     let cOpt = `
 *     let ds = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
 *     for (let i = 0; i < ds.length; i++) {
 *         ds[i] = Math.sin(ds[i]/360*Math.PI)
 *     }
 *     let opt = {
 *
 *         title: {
 *             text: 'Logarithmic axis demo'
 *         },
 *
 *         xAxis: {
 *             tickInterval: 1,
 *             type: 'logarithmic',
 *             accessibility: {
 *                 rangeDescription: 'Range: 1 to 10'
 *             }
 *         },
 *
 *         yAxis: {
 *             type: 'logarithmic',
 *             minorTickInterval: 0.1,
 *             accessibility: {
 *                 rangeDescription: 'Range: 0.1 to 1000'
 *             }
 *         },
 *
 *         tooltip: {
 *             headerFormat: '<b>{series.name}</b><br />',
 *             pointFormat: 'x = {point.x}, y = {point.y}'
 *         },
 *
 *         series: [{
 *             data: ds,
 *             pointStart: 1
 *         }]
 *
 *     }
 *     `
 *     let whOpt = {}
 *
 *     let b64 = await WHc2png(width, height, scale, cOpt, whOpt)
 *     // console.log('b64', b64)
 *
 *     fs.writeFileSync('./test-sclb.b64', b64)
 *     genPlotHtml('./test-sclb.html', b64)
 *
 *     console.log('finish')
 * }
 * testb()
 *     .catch((err) => {
 *         console.log(err)
 *     })
 *
 */
async function WHc2png(width = 700, height = 400, scale = 3, opt = {}, whOpt = {}) {
    // console.log('WHc2png', width, height, scale, opt, whOpt)

    //width
    if (!isnum(width)) {
        return Promise.reject('width is not a number')
    }
    width = cdbl(width)
    if (width <= 0) {
        return Promise.reject('width <= 0')
    }

    //height
    if (!isnum(height)) {
        return Promise.reject('height is not a number')
    }
    height = cdbl(height)
    if (height <= 0) {
        return Promise.reject('height <= 0')
    }

    //scale
    if (!isnum(scale)) {
        return Promise.reject('scale is not a number')
    }
    scale = cdbl(scale)
    if (scale <= 0) {
        return Promise.reject('scale <= 0')
    }

    //opt
    if (!iseobj(opt) && !isestr(opt)) {
        return Promise.reject('opt is not an effective object or string')
    }

    //addScripts
    let addScripts = get(whOpt, 'addScripts')
    if (!isearr(addScripts)) {
        addScripts = []
    }

    //caddCode
    let caddCode = get(whOpt, 'addCode')
    if (!isearr(caddCode)) {
        caddCode = ''
    }

    //executablePath
    let executablePath = get(whOpt, 'executablePath', '')
    //若不給則由puppeteer偵測取得或給executableFolder搜尋取得

    //executableFolder
    let executableFolder = get(whOpt, 'executableFolder', '')
    if (isestr(executableFolder) && !isestr(executablePath)) {
        //executablePath='C:\\Users\\user\\.cache\\puppeteer\\chrome\\win64-116.0.5845.96\\chrome-win64\\chrome.exe'
        //executableFolder='C:\\Users\\user\\.cache\\puppeteer'
        let fps = fsTreeFolder(executableFolder, null)
        let r = find(fps, (v) => {
            return v.name === 'chrome.exe'
        })
        // console.log('r', r)
        if (iseobj(r)) {
            executablePath = r.path
        }
        else {
            throw new Error(`can not find chrome.exe in executableFolder[${executableFolder}]`)
        }
    }

    //cOpt
    let cOpt = ''
    if (isestr(opt)) {
        cOpt = opt //外部給予opt字串時須自行給予let opt=ooo
    }
    else {
        let j = JSON.stringify(opt)
        cOpt = `let opt=${j};`
    }

    //fnOut
    let fnOut = `./whpic-${now2strp()}-${genID()}.png` //一定要給副檔名, 否則puppeteer的screenshot會無法識別格式

    //fpOut
    let fpOut = path.resolve(wd, fnOut)
    // console.log('fpOut', fpOut)

    //fnHtml
    let fnHtml = `./whweb-${now2strp()}-${genID()}.html`

    //fpHtml
    let fpHtml = path.resolve(wd, fnHtml)
    // console.log('fpHtml', fpHtml)

    //cAddScripts
    let cAddScripts = ''
    each(addScripts, (v) => {
        let c = `<script src="${v}"></script>`
        cAddScripts += c
    })

    //html
    let g = `
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>highcharts to png</title>

  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

  <script>{code_highcharts}</script>
  <script>{code_stock}</script>
  <script>{code_heatmap}</script>
  <script>{code_annotations}</script>
  <script>{code_boost}</script>
  <script>{code_boost_canvas}</script>

  {add_scripts}

  <script>{add_code}</script>

</head>
<body style="padding:0; margin:0;">

    <div id="hc" style="width:{width}px; height:{height}px;"></div>

    <script>

        {cOpt}

        if(_.isObject(opt)){

            //強制關閉動畫避免過慢顯示無法截到數據圖
            _.set(opt, 'chart.animation', false)
            _.set(opt, 'plotOptions.series.animation', false)
            // console.log('opt', opt)
        
            //plot
            Highcharts.chart('hc', opt)

        }
        else{
            console.log('invalid opt')
        }

    </script>

</body>
</html>
`

    //客製化程式得先取代, 避免取代到外部引入程式碼
    g = g.replace('{width}', width)
    g = g.replace('{height}', height)
    g = g.replace('{cOpt}', cOpt)

    //引入外部程式碼
    g = g.replace('{code_highcharts}', code_highcharts)
    g = g.replace('{code_stock}', code_stock)
    g = g.replace('{code_heatmap}', code_heatmap)
    g = g.replace('{code_annotations}', code_annotations)
    g = g.replace('{code_boost}', code_boost)
    g = g.replace('{code_boost_canvas}', code_boost_canvas)
    g = g.replace('{add_scripts}', cAddScripts)
    g = g.replace('{add_code}', caddCode)

    //writeFileSync
    fs.writeFileSync(fpHtml, g, 'utf8')

    //b64
    let b64 = ''
    let core = async () => {

        //puppeteerOpt
        let puppeteerOpt = {
            headless: 'new', //true,
            slowMo: 20,
        }
        if (isestr(executablePath)) {
            puppeteerOpt.executablePath = executablePath
        }

        //browser
        let browser = await puppeteer.launch(puppeteerOpt)

        //page
        let page = await browser.newPage()

        //viewport
        let viewport = {
            x: 0,
            y: 0,
            width: Number(width),
            height: Number(height),
            deviceScaleFactor: Number(scale),
        }
        //console.log('viewport',viewport)

        //show page
        await page.goto(fpHtml)
        await page.setViewport(viewport)

        // //delay 3s for highchart rendered
        // await page.waitFor(3000)

        //screenshot
        await page.screenshot({ path: fpOut }) //fullPage: true

        //close
        // await page.close()
        await browser.close()

        //readFileSync
        b64 = fs.readFileSync(fpOut, { encoding: 'base64' })

    }
    await core()
        .catch((err) => {
            console.log(err)
        })

    //delete
    if (fsIsFile(fpHtml)) {
        fs.unlinkSync(fpHtml)
    }

    //delete
    if (fsIsFile(fpOut)) {
        fs.unlinkSync(fpOut)
    }

    return b64
}


export default WHc2png