#!/usr/bin/node // // generate_img.js version 0.0.99.20211018 (2021-10-18) written by cleemy desu wayo // // this is a personal work and is a PDS (Public Domain Software) // // -------- // requirements: Node.js v10 or later, node-canvas // (and maybe this code works under only Linux) // -------- // // see https://twitter.com/cleemy4545/status/1450188684045787142 and the thread // // old version: https://pastebin.com/T5VxqDGX // 'use strict'; const fs = require('fs'); const canvas = require('canvas'); if ((process.argv[2] !== '--stdin') && (! fs.existsSync(process.argv[2]))) { console.log('please specify a valid file name (or specify "--stdin")'); process.exit(1); } let dirImg = './'; let dirTextimg = './'; let isDirTextimgOverridden = false; let mainCanvas = null; let mainCanvasContext = null; let pos = new Map([['x', 0], ['y', 0]]); let margin = new Map([['x', 0], ['y', 0]]); let step = new Map([['x', 1], ['y', 0]]); let textSize = 16; let textColor = []; let textFont = 'unifont'; let textStep = 1; // // pasteImg(params): draw new image // const pasteImg = function(params) { let imgPiece = new canvas.Image; imgPiece.src = params.get('src'); mainCanvasContext.drawImage(imgPiece, params.get('x'), params.get('y')); return [imgPiece.width, imgPiece.height]; } // // parseArgs(sourceStr): parse sourceStr and return an Array // // '(100,100,20,#ffffff)' --> return ['100', '100', '20', '#ffffff'] // '(100)' --> return ['100'] // ' (100) ' --> return ['100'] // '( 100 , 100 )' --> return ['100', '100'] // '( 100 , 10 0 )' --> return ['100', '10 0'] // '(,,20)' --> return ['', '', '20'] // '(20,,)' --> return ['20', '', ''] // '(,,,)' --> return ['', '', '', ''] // '( , , , )' --> return ['', '', '', ''] // '()' --> return [''] // '( )' --> return [''] // '' --> return [] // '(())' --> return ['()'] // '(100,100' (invalid string) --> return [] // [100, 100] (not string) --> return [] // const parseArgs = function(sourceStr) { if (Object.prototype.toString.call(sourceStr) !== '[object String]') { return []; } const args_str = sourceStr.trim().replace(/^\((.*)\)$/, '$1'); // ' (100,100) ' --> '100,100' if (args_str === sourceStr) { return []; } return args_str.split(',').map(s => s.trim()); } // // properFilePath(filePath, baseFilePath): return a proper file path // // this ignores base_file_path_src if file_path starts with '/', 'file:///', // 'https://', 'ftp://' or some such // const properFilePath = function(filePath, baseFilePathSrc = '') { // has a control code? if (`${filePath}${baseFilePathSrc}`.match(/[\x00-\x1f\x7f-\x9f]/)) { return '' } let resultStr = ''; if (filePath.startsWith('/') || filePath.startsWith('file:///')) { resultStr = filePath.replace(/^(file:)?\/+/, '/'); } else if (filePath.match(/^[a-zA-Z]+:\/\//)) { resultStr = filePath.replace(/^([a-zA-Z]+):\/\/+/, '$1://'); } else { let baseFilePath = baseFilePathSrc; if (baseFilePath === null || baseFilePath === undefined || baseFilePath === '') { baseFilePath = '.'; } else if (baseFilePath.startsWith('/') || baseFilePath.startsWith('file:///')) { baseFilePath = baseFilePath.replace(/^(file:)?\/+/, '/'); } else if (baseFilePath.match(/^[a-zA-Z]+:\/\//)) { baseFilePath = baseFilePath.replace(/^([a-zA-Z]+):\/\/+/, '$1://'); } else { baseFilePath = './' + baseFilePath.replace(/^(\.\/+)+/, ''); } resultStr = baseFilePath.replace(/\/+$/, '') + '/' + filePath.replace(/^(\.\/+)+/, ''); } const filePrefixAllowlist = ['./', '/']; if (!filePrefixAllowlist.some(str => resultStr.startsWith(str))) { resultStr = ''; } return resultStr; } let lines = []; if (process.argv[2] === '--stdin') { lines = fs.readFileSync('/dev/stdin', 'utf8').toString().split('\n'); } else { lines = fs.readFileSync(process.argv[2], 'utf8').toString().split('\n'); } lines.forEach(line => { let m = null; if (m = line.match(/^dir *: *([-_a-zA-Z0-9./%:]+) *$/)) { dirImg = properFilePath(m[1]); if (!isDirTextimgOverridden) { dirTextimg = dirImg } } else if (m = line.match(/^dir *\( *textimg *\) *: *([-_a-zA-Z0-9./%:]+) *$/)) { dirTextimg = properFilePath(m[1]); isDirTextimgOverridden = true; } else if (m = line.match(/^bg *: *([-_a-zA-Z0-9./%:]+) *$/)) { const bgSrc = properFilePath(m[1], dirImg); if (! fs.existsSync(bgSrc)) { return false } let imgPiece = new canvas.Image; imgPiece.src = bgSrc; mainCanvas = canvas.createCanvas(imgPiece.width, imgPiece.height); mainCanvasContext = mainCanvas.getContext('2d'); mainCanvasContext.drawImage(imgPiece, 0, 0); } else if ((m = line.match(/^paste *(\(.*?\))? *: *([-_a-zA-Z0-9./%:]+) *$/)) && mainCanvas) { const filePath = properFilePath(m[2], dirImg); if (! fs.existsSync(filePath)) { return false } let pasteParams = new Map([['src', filePath ], ['x', pos.get('x')], ['y', pos.get('y')]]); const pasteArgs = parseArgs(m[1]); const argsLength = pasteArgs.length; if (argsLength >= 1) { if (pasteArgs[0].match(/^-?[0-9]+(\.[0-9]+)?$/)) { pasteParams.set('x', pos.get('x') + parseFloat(pasteArgs[0])); } if ((argsLength >= 2) && pasteArgs[1].match(/^-?[0-9]+(\.[0-9]+)?$/)) { pasteParams.set('y', pos.get('y') + parseFloat(pasteArgs[1])); } } let newImgPieceSize = { x: 0, y: 0 }; [newImgPieceSize['x'], newImgPieceSize['y']] = pasteImg(pasteParams); ['x', 'y'].forEach(axis => { pos.set(axis, pos.get(axis) + newImgPieceSize[axis] * step.get(axis) + margin.get(axis)); }); } else if ((m = line.match(/^textimg *(\(.*?\))? *: *(.+)$/)) && mainCanvas) { let xPosBuff = pos.get('x'); // "textimg:" does not change pos let yPosBuff = pos.get('y'); // "textimg:" does not change pos const textimgArgs = parseArgs(m[1]); const argsLength = textimgArgs.length; if (argsLength >= 1) { if (textimgArgs[0].match(/^-?[0-9]+(\.[0-9]+)?$/)) { xPosBuff += parseFloat(textimgArgs[0]); } if ((argsLength >= 2) && textimgArgs[1].match(/^-?[0-9]+(\.[0-9]+)?$/)) { yPosBuff += parseFloat(textimgArgs[1]); } } Array.from(m[2]).forEach(c => { let filePath = properFilePath('textimg_' + c.codePointAt(0).toString(16) + '.png', dirTextimg); if (! fs.existsSync(filePath)) { return false } let pasteParams = new Map([['src', filePath], ['x', xPosBuff], ['y', yPosBuff]]); let newImgPieceSize = { x: 0, y: 0 }; [newImgPieceSize['x'], newImgPieceSize['y']] = pasteImg(pasteParams); xPosBuff = xPosBuff + newImgPieceSize['x']; // "textimg:" does not change pos }); } else if ((m = line.match(/^text *(\(.*?\))? *: *(.+)$/)) && mainCanvas) { let textParams = new Map([['body', m[2] ], ['x', pos.get('x') ], ['y', pos.get('y') ], ['size', parseFloat(textSize)], ['color', textColor ]]); const textArgs = parseArgs(m[1]); const argsLength = textArgs.length; if (argsLength >= 1) { if (textArgs[0].match(/^-?[0-9]+(\.[0-9]+)?$/)) { textParams.set('x', pos.get('x') + parseFloat(textArgs[0])); } if ((argsLength >= 2) && textArgs[1].match(/^-?[0-9]+(\.[0-9]+)?$/)) { textParams.set('y', pos.get('y') + parseFloat(textArgs[1])); } if ((argsLength >= 3) && textArgs[2].match(/^[0-9]+(\.[0-9]+)?$/)) { textParams.set('size', parseFloat(textArgs[2])); } if ((argsLength >= 4) && textArgs[3].startsWith('#')) { const matched_rgb = textArgs[3].match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/) if (matched_rgb !== null) { const matched_rgb_array = matched_rgb.slice(1, 4).map(s => parseInt(s, 16)); textParams.set('color', matched_rgb_array); } } } mainCanvasContext.font = textParams.get('size').toString() + 'px ' + textFont; mainCanvasContext.fillStyle = 'rgb(' + textParams.get('color').join(',') + ')'; mainCanvasContext.fillText(textParams.get('body'), textParams.get('x'), textParams.get('y') + textParams.get('size')); pos.set('y', pos.get('y') + parseFloat(textSize) * parseFloat(textStep) + margin.get('y')); } else if (m = line.match(/^text_([a-z]+) *: *(.+?) *$/)) { if (m[1] === 'font') { textFont = m[2]; } else if (m[1] === 'size') { textSize = parseFloat(m[2]); } else if (m[1] === 'color') { let regex; let radix; if (m[2].startsWith('#')) { regex = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2}) *$/; radix = 16; } else { regex = /^([0-9]+) *, *([0-9]+) *, *([0-9]+) *$/; radix = 10; } const matched_rgb = m[2].match(regex); if (matched_rgb !== null) { textColor = matched_rgb.slice(1, 4).map(s => parseInt(s, radix)); } } else if (m[1] === 'step') { textStep = parseFloat(m[2]); } } else if (m = line.match(/^(blank|margin|pos|step) *(\(.*?\))? *: *([-0-9.]+?) *$/)) { const args = parseArgs(m[2]); if ((args.length !== 0) && (['x', 'y', ''].indexOf(args[0]) < 0)) { return false; } let axes = ['x', 'y']; if (['x', 'y'].indexOf(args[0]) >= 0) { axes = [args[0]]; } axes.forEach(axis => { switch (m[1]) { case 'blank' : pos.set (axis, pos.get(axis) + parseFloat(m[3])); break; case 'margin' : margin.set (axis, parseFloat(m[3])); break; case 'pos' : pos.set (axis, parseFloat(m[3])); break; case 'step' : step.set (axis, parseFloat(m[3])); break; } }); } }); if (mainCanvas === null) { console.log('error: no background image'); process.exit(2); } mainCanvas.createPNGStream().pipe(fs.createWriteStream('generated.png'));