proc.mjs

import fs from 'fs'
import get from 'lodash-es/get.js'
import toLower from 'lodash-es/toLower.js'
import cloneDeep from 'lodash-es/cloneDeep.js'
import isnum from 'wsemi/src/isnum.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import ispm from 'wsemi/src/ispm.mjs'
import cint from 'wsemi/src/cint.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import getFileNameExt from 'wsemi/src/getFileNameExt.mjs'
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
import sharp from 'sharp'
import buf2b64 from './buf2b64.mjs'


/**
 * 圖片處理
 *
 * @param {String} fpInp 輸入來源圖片位置字串
 * @param {String} fpOut 輸入目標圖片位置字串
 * @param {Object} [opt={}] 輸入設定物件,含各種類編碼設定,預設{}
 * @param {Function} [opt.funGetWidth=null] 輸入指定圖片寬度函數,函數輸入會提供圖片metadata物件,預設null
 * @param {Function} [opt.funGetHeight=null] 輸入指定圖片高度函數,函數輸入會提供圖片metadata物件,預設null
 * @param {Function} [opt.funGetSize=null] 輸入指定圖片寬高度函數,函數輸入會提供圖片metadata物件,預設null
 * @param {Integer} [opt.cropLeft=null] 輸入裁切左邊座標整數,單位px,預設null
 * @param {Integer} [opt.cropTop=null] 輸入裁切上邊座標整數,單位px,預設null
 * @param {Integer} [opt.cropWidth=null] 輸入裁切圖寬整數,單位px,預設null
 * @param {Integer} [opt.cropHeight=null] 輸入裁切圖高整數,單位px,預設null
 * @param {Integer} [opt.density=96] 輸入DPI整數,主要用於向量圖轉點陣圖,預設96
 * @param {Boolean} [opt.returnBase64=false] 輸入是否回傳Base64字串布林值,預設false
 * @return {Promise} 回傳Promise,resolve當returnBase64=false時回傳成功訊息,當returnBase64=true時回傳Base64字串,reject回傳錯誤訊息
 * @example
 *
 * 詳見convert、resize、crop、base64範例
 *
 */
let proc = async (fpInp, fpOut, opt = {}) => {
    //注意sharp不能使用toFile須改使用fs讀寫buf, 否則會容易鎖死檔案

    //check
    if (!fsIsFile(fpInp)) {
        throw new Error(`fpInp[${fpInp}] does not exist`)
    }

    //funGetWidth, funGetHeight, funGetSize
    let funGetWidth = get(opt, 'funGetWidth')
    let funGetHeight = get(opt, 'funGetHeight')
    let funGetSize = get(opt, 'funGetSize')

    //cropLeft, cropTop, cropWidth, cropHeight
    let cropLeft = get(opt, 'cropLeft')
    let cropTop = get(opt, 'cropTop')
    let cropWidth = get(opt, 'cropWidth')
    let cropHeight = get(opt, 'cropHeight')

    //density
    let density = get(opt, 'density')
    if (!isnum(density)) {
        density = 96 //96dpi與瀏覽器比例一致
    }
    density = cint(density)

    //returnBase64
    let returnBase64 = get(opt, 'returnBase64')
    if (!isbol(returnBase64)) {
        returnBase64 = false
    }

    //extOut
    let extOut = getFileNameExt(fpOut)
    extOut = toLower(extOut)
    // console.log('extOut', extOut)

    //bufIn
    let bufIn = fs.readFileSync(fpInp)

    //width, height
    let width = null
    let height = null
    if (isfun(funGetSize) || isfun(funGetWidth) || isfun(funGetHeight)) {

        //meta
        let meta = await sharp(bufIn).metadata() //metadata()讀取後內部stream會被消耗, 不能再用於resize

        if (isfun(funGetSize)) {

            //funGetSize
            let s = funGetSize(cloneDeep(meta))
            if (ispm(s)) {
                s = await s
            }

            //check
            if (!isnum(get(s, 'width'))) {
                throw new Error(`invalid ret.width for funGetSize`)
            }
            if (!isnum(get(s, 'height'))) {
                throw new Error(`invalid ret.height for funGetSize`)
            }
            width = cint(s.width)
            height = cint(s.height)

        }
        else if (isfun(funGetWidth)) {

            //funGetWidth
            width = funGetWidth(cloneDeep(meta))
            if (ispm(width)) {
                width = await width
            }
            width = cint(width)

        }
        else if (isfun(funGetHeight)) {

            //funGetHeight
            height = funGetHeight(cloneDeep(meta))
            if (ispm(height)) {
                height = await height
            }
            height = cint(height)

        }
    }

    //img
    let img = await sharp(bufIn, { density })
    let bProcWidth = isnum(width)
    let bProcHeight = isnum(height)
    let bProcCrop = isnum(cropLeft) && isnum(cropTop) && isnum(cropWidth) && isnum(cropHeight)
    if (bProcWidth && bProcHeight) {

        //resize
        img = img.resize({
            width,
            height,
            withoutEnlargement: true, //拒絕放大圖片
        })

    }
    else if (bProcWidth && !bProcHeight) {

        //resize
        img = img.resize({
            width, //等比例縮放
            withoutEnlargement: true, //拒絕放大圖片
        })

    }
    else if (!bProcWidth && bProcHeight) {

        //resize
        img = img.resize({
            height, //等比例縮放
            withoutEnlargement: true, //拒絕放大圖片
        })

    }
    if (bProcCrop) {

        //cint
        cropLeft = cint(cropLeft)
        cropTop = cint(cropTop)
        cropWidth = cint(cropWidth)
        cropHeight = cint(cropHeight)

        //extract
        img = img.extract({
            left: cropLeft,
            top: cropTop,
            width: cropWidth,
            height: cropHeight,
        })

    }

    //kpFormat
    let kpFormat = {
        png: () => img.png({
            compressionLevel: 9, // 範圍 0–9
            adaptiveFiltering: false,
            palette: false, // 是否使用 palette(會變成8-bit)
            quality: 100, // palette=true 才有用
            colors: 256, // palette=true 才有用
            ...opt,
        }),
        jpg: () => img.flatten({ background: get(opt, 'background', '#fff') }).jpeg({
            quality: 100, // 1–100
            progressive: false,
            chromaSubsampling: '4:2:0',
            optimiseCoding: true, // 有助於壓縮
            mozjpeg: false,
            overshootDeringing: false,
            ...opt,
        }),
        jpeg: () => img.flatten({ background: get(opt, 'background', '#fff') }).jpeg({
            quality: 100, // 1–100
            progressive: false,
            chromaSubsampling: '4:2:0',
            optimiseCoding: true, // 有助於壓縮
            mozjpeg: false,
            overshootDeringing: false,
            ...opt,
        }),
        webp: () => img.webp({
            quality: 100,
            alphaQuality: 100, // 透明通道品質
            lossless: true, // 無損支援透明
            ...opt,
        }),
        avif: () => img.avif({
            quality: 50, // 1–100
            lossless: false,
            speed: 5, // 0–9 (0 最慢最好)
            chromaSubsampling: '4:2:0',
            ...opt,
        }),
        gif: () => img.gif({
            effort: 7, // 1–10 (壓縮努力程度)
            interlace: false,
            ...opt,
        }),
        tiff: () => img.tiff({
            quality: 100,
            compression: 'jpeg', // 預設是 JPEG 壓縮的 TIFF
            predictor: 'horizontal',
            pyramid: false,
            tile: false,
            ...opt,
        }),
        bmp: () => img.bmp({
            ...opt,
        }),
    }

    //toFormat
    if (haskey(kpFormat, extOut)) {
        img = kpFormat[extOut]()
    }
    else {
        img = img.toFormat(extOut)
    }

    //bufOut
    let bufOut = await img.toBuffer()

    //returnBase64
    if (returnBase64) {

        //pb64
        let pb64 = buf2b64(bufOut, extOut)
        // console.log('b64', b64)

        return pb64
    }

    //writeFileSync
    fs.writeFileSync(fpOut, bufOut)

    return 'ok'
}


export default proc