From 1c62edcfe64f80491a92b0d24545b131913edd8d Mon Sep 17 00:00:00 2001 From: rodrigorodriguez Date: Sun, 5 Feb 2023 11:57:02 -0300 Subject: [PATCH] new(basic.gblib): FILL keyword can now template images and AS IMAGE can convert a DOCX to a PNG. --- .../services/KeywordsExpressions.ts | 13 +- .../basic.gblib/services/SystemKeywords.ts | 248 +++++++++++------- 2 files changed, 159 insertions(+), 102 deletions(-) diff --git a/packages/basic.gblib/services/KeywordsExpressions.ts b/packages/basic.gblib/services/KeywordsExpressions.ts index 9ed0d7b2..4ea1cd51 100644 --- a/packages/basic.gblib/services/KeywordsExpressions.ts +++ b/packages/basic.gblib/services/KeywordsExpressions.ts @@ -757,11 +757,14 @@ export class KeywordsExpressions { } ]; - - - - - + keywords[i++] = [ + /^\s*(\w+)\s*\=\s*CARD\s*(.*)/gim, + ($0, $1, $2, $3) => { + const params = this.getParams($1, ['doc', 'data']); + return ` + ${$1} = await dk.card({pid: pid, args: [${$2}]})`; + } + ]; return keywords; } diff --git a/packages/basic.gblib/services/SystemKeywords.ts b/packages/basic.gblib/services/SystemKeywords.ts index a9c4aa71..79f4e3d7 100644 --- a/packages/basic.gblib/services/SystemKeywords.ts +++ b/packages/basic.gblib/services/SystemKeywords.ts @@ -30,10 +30,9 @@ | | \*****************************************************************************/ 'use strict'; -import { GBDialogStep, GBLog, GBMinInstance } from 'botlib'; +import { GBLog, GBMinInstance } from 'botlib'; import { GBConfigService } from '../../core.gbapp/services/GBConfigService.js'; import { CollectionUtil } from 'pragmatismo-io-framework'; - import { GBAdminService } from '../../admin.gbapp/services/GBAdminService.js'; import { GBDeployer } from '../../core.gbapp/services/GBDeployer.js'; import { DialogKeywords } from './DialogKeywords.js'; @@ -44,7 +43,6 @@ import { createBrowser } from '../../core.gbapp/services/GBSSR.js'; import urlJoin from 'url-join'; import Excel from 'exceljs'; import { TwitterApi } from 'twitter-api-v2'; -import tesseract from 'node-tesseract-ocr'; import Path from 'path'; import ComputerVisionClient from '@azure/cognitiveservices-computervision'; import ApiKeyCredentials from '@azure/ms-rest-js'; @@ -53,6 +51,9 @@ import PizZip from 'pizzip'; import Docxtemplater from 'docxtemplater'; import pptxTemplaterModule from 'pptxtemplater'; import _ from 'lodash'; +import DocxImager from 'docximager'; +import { pdfToPng, PngPageOutput } from 'pdf-to-png-converter'; +import sharp from 'sharp'; /** * @fileoverview General Bots server core. @@ -87,7 +88,6 @@ export class SystemKeywords { } public async callVM({ pid, text }) { - const min = null; const step = null; const deployer = null; @@ -108,9 +108,7 @@ export class SystemKeywords { * */ public async seeCaption({ pid, url }) { - const { - min, user - } = await DialogKeywords.getProcessInfo(pid); + const { min, user } = await DialogKeywords.getProcessInfo(pid); const computerVisionClient = new ComputerVisionClient.ComputerVisionClient( new ApiKeyCredentials.ApiKeyCredentials({ inHeader: { 'Ocp-Apim-Subscription-Key': process.env.VISION_KEY } }), process.env.VISION_ENDPOINT @@ -160,9 +158,7 @@ export class SystemKeywords { } public async sortBy({ pid, array, memberName }) { - const { - min, user - } = await DialogKeywords.getProcessInfo(pid); + const { min, user } = await DialogKeywords.getProcessInfo(pid); memberName = memberName.trim(); const contentLocale = this.min.core.getParam( min.instance, @@ -177,22 +173,22 @@ export class SystemKeywords { if (date) { return array ? array.sort((a, b) => { - const c = new Date(a[memberName]); - const d = new Date(b[memberName]); - return c.getTime() - d.getTime(); - }) + const c = new Date(a[memberName]); + const d = new Date(b[memberName]); + return c.getTime() - d.getTime(); + }) : null; } else { return array ? array.sort((a, b) => { - if (a[memberName] < b[memberName]) { - return -1; - } - if (a[memberName] > b[memberName]) { - return 1; - } - return 0; - }) + if (a[memberName] < b[memberName]) { + return -1; + } + if (a[memberName] > b[memberName]) { + return 1; + } + return 0; + }) : array; } } @@ -247,7 +243,6 @@ export class SystemKeywords { * @see http://tabulator.info/examples/5.2 */ private async renderTable(pid, data, renderPDF, renderImage) { - if (!data[1]) { return null; } @@ -257,9 +252,7 @@ export class SystemKeywords { // Detects if it is a collection with repeated // headers. - const { - min, user - } = await DialogKeywords.getProcessInfo(pid); + const { min, user } = await DialogKeywords.getProcessInfo(pid); const gbaiName = `${min.botId}.gbai`; const browser = await createBrowser(null); const page = await browser.newPage(); @@ -338,14 +331,67 @@ export class SystemKeywords { return [url, localName]; } - public async asPDF({ pid, data, filename }) { + public async asPDF({ pid, data }) { let file = await this.renderTable(pid, data, true, false); return file[0]; } - public async asImage({ pid, data, filename }) { - let file = await this.renderTable(pid, data, false, true); - return file[0]; + public async asImage({ pid, data }) { + const { min, user } = await DialogKeywords.getProcessInfo(pid); + + // Checks if it is a GBFILE. + + if (data.data) { + const gbfile = data.data; + + let { baseUrl, client } = await GBDeployer.internalGetDriveClient(this.min); + const botId = this.min.instance.botId; + const gbaiName = `${this.min.botId}.gbai`; + const tmpDocx = urlJoin(gbaiName, `${botId}.gbdrive`, `tmp${GBAdminService.getRndReadableIdentifier()}.docx`); + + // Performs the conversion operation. + + await client.api(`${baseUrl}/drive/root:/${tmpDocx}:/content`).put(data.data); + const res = await client.api(`${baseUrl}/drive/root:/${tmpDocx}:/content?format=pdf`).get(); + await client.api(`${baseUrl}/drive/root:/${tmpDocx}:/content`).delete(); + + const streamToString = stream => { + const chunks = []; + return new Promise((resolve, reject) => { + stream.on('data', chunk => chunks.push(chunk)); + stream.on('error', reject); + stream.on('end', () => resolve(Buffer.concat(chunks))); + }); + }; + + gbfile.data = await streamToString(res); + + // Converts the PDF to PNG. + + const pngPages: PngPageOutput[] = await pdfToPng(gbfile.data, { + disableFontFace: false, + useSystemFonts: false, + viewportScale: 2.0, + pagesToProcess: [1], + strictPagesToProcess: false, + verbosityLevel: 0 + }); + + // Prepare an image on cache and return the GBFILE information. + + const localName = Path.join('work', gbaiName, 'cache', `img${GBAdminService.getRndReadableIdentifier()}.png`); + if (pngPages.length > 0) { + const buffer = pngPages[0].content; + const url = urlJoin(GBServer.globals.publicAddress, min.botId, 'cache', Path.basename(localName)); + + Fs.writeFileSync(localName, buffer, { encoding: null }); + + return { localName: localName, url: url, data: buffer }; + } + } else { + let file = await this.renderTable(pid, data, false, true); + return file[0]; + } } public async executeSQL({ pid, data, sql, tableName }) { @@ -418,9 +464,7 @@ export class SystemKeywords { * */ public async talkTo({ pid, mobile, message }) { - const { - min, user - } = await DialogKeywords.getProcessInfo(pid); + const { min, user } = await DialogKeywords.getProcessInfo(pid); GBLog.info(`BASIC: Talking '${message}' to a specific user (${mobile}) (TALK TO). `); await min.conversationalService.sendMarkdownToMobile(min, null, mobile, message); } @@ -432,9 +476,7 @@ export class SystemKeywords { * */ public async sendSmsTo({ pid, mobile, message }) { - const { - min, user - } = await DialogKeywords.getProcessInfo(pid); + const { min, user } = await DialogKeywords.getProcessInfo(pid); GBLog.info(`BASIC: SEND SMS TO '${mobile}', message '${message}'.`); await min.conversationalService.sendSms(min, mobile, message); } @@ -449,10 +491,7 @@ export class SystemKeywords { * */ public async set({ pid, file, address, value }): Promise { - - const { - min, user - } = await DialogKeywords.getProcessInfo(pid); + const { min, user } = await DialogKeywords.getProcessInfo(pid); // Handles calls for HTML stuff @@ -484,8 +523,7 @@ export class SystemKeywords { await client .api( - `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name - }')/range(address='${address}')` + `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='${address}')` ) .patch(body); } @@ -530,9 +568,9 @@ export class SystemKeywords { await client.api(`${baseUrl}/drive/root:/${path}/${file}:/content`).put(data); } catch (error) { if (error.code === 'itemNotFound') { - GBLog.info(`BASIC: CONVERT source file not found: ${file}.`); + GBLog.info(`BASIC: BASIC source file not found: ${file}.`); } else if (error.code === 'nameAlreadyExists') { - GBLog.info(`BASIC: CONVERT destination file already exists: ${file}.`); + GBLog.info(`BASIC: BASIC destination file already exists: ${file}.`); } throw error; } @@ -557,8 +595,7 @@ export class SystemKeywords { await client .api( - `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name - }')/range(address='A2:DX2')/insert` + `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='A2:DX2')/insert` ) .post({}); @@ -579,8 +616,7 @@ export class SystemKeywords { await client .api( - `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name - }')/range(address='${address}')` + `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='${address}')` ) .patch(body); } @@ -615,8 +651,7 @@ export class SystemKeywords { let results = await client .api( - `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name - }')/range(address='${addressOrHeaders}')` + `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='${addressOrHeaders}')` ) .get(); @@ -674,7 +709,6 @@ export class SystemKeywords { const file = args[0]; args.shift(); - const botId = this.min.instance.botId; const path = `/${botId}.gbai/${botId}.gbdata`; @@ -764,8 +798,7 @@ export class SystemKeywords { results = await client .api( - `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name - }')/range(address='A1:CZ${maxLines}')` + `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='A1:CZ${maxLines}')` ) .get(); @@ -784,12 +817,7 @@ export class SystemKeywords { if (parts.length === 2 && !done) { filter = { columnName: parts[0].trim(), - operator: op - .toString() - .replace(/\\b/g, '') - .replace(/\//g, '') - .replace(/\\/g, '') - .replace(/\b/g, ''), + operator: op.toString().replace(/\\b/g, '').replace(/\//g, '').replace(/\\/g, '').replace(/\b/g, ''), value: parts[1].trim() }; @@ -875,13 +903,7 @@ export class SystemKeywords { filterAcceptCount++; } } else { - if ( - result && - result - .toLowerCase() - .trim() - .indexOf(filter.value.toLowerCase().trim()) > -1 - ) { + if (result && result.toLowerCase().trim().indexOf(filter.value.toLowerCase().trim()) > -1) { filterAcceptCount++; } } @@ -892,13 +914,7 @@ export class SystemKeywords { filterAcceptCount++; } } else { - if ( - result && - result - .toLowerCase() - .trim() - .indexOf(filter.value.toLowerCase().trim()) === -1 - ) { + if (result && result.toLowerCase().trim().indexOf(filter.value.toLowerCase().trim()) === -1) { filterAcceptCount++; } } @@ -909,13 +925,7 @@ export class SystemKeywords { filterAcceptCount++; } } else { - if ( - result && - result - .toLowerCase() - .trim() - .indexOf(filter.value.toLowerCase().trim()) > -1 - ) { + if (result && result.toLowerCase().trim().indexOf(filter.value.toLowerCase().trim()) > -1) { filterAcceptCount++; } } @@ -1374,6 +1384,7 @@ export class SystemKeywords { * */ public async fill({ pid, templateName, data }) { + const { min, user } = await DialogKeywords.getProcessInfo(pid); const botId = this.min.instance.botId; const gbaiName = `${botId}.gbai`; const path = `/${botId}.gbai/${botId}.gbdata`; @@ -1382,29 +1393,77 @@ export class SystemKeywords { let { baseUrl, client } = await GBDeployer.internalGetDriveClient(this.min); let template = await this.internalGetDocument(client, baseUrl, path, templateName); - const url = template['@microsoft.graph.downloadUrl']; - const localName = Path.join('work', gbaiName, 'cache', ``); + let url = template['@microsoft.graph.downloadUrl']; + let localName = Path.join('work', gbaiName, 'cache', ``); const response = await fetch(url); Fs.writeFileSync(localName, Buffer.from(await response.arrayBuffer()), { encoding: null }); // Loads the file as binary content. - const content = Fs.readFileSync(localName, 'binary'); - const zip = new PizZip(content); - const doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true }); + let content = Fs.readFileSync(localName, 'binary'); + let zip = new PizZip(content); + let doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true }); if (localName.endsWith('.pptx')) { doc.attachModule(pptxTemplaterModule); } - // Renders the document (Replace {first_name} by John, {last_name} by Doe, ...) + // Replace image path on all elements of data.s + const images = []; + + const process = async (key, value, obj) => { + for (const kind in ['png', 'jpg', 'jpeg']) { + if (value.endsWith(`.${kind}`)) { + const { baseUrl, client } = await GBDeployer.internalGetDriveClient(this.min); + const path = `/${botId}.gbai/${botId}.gbdrive`; + const ref = await this.internalGetDocument(client, baseUrl, path, value); + let url = ref['@microsoft.graph.downloadUrl']; + let localName = Path.join('work', gbaiName, 'cache', ``); + const response = await fetch(url); + const buf = Buffer.from(await response.arrayBuffer()); + Fs.writeFileSync(localName, buf, { encoding: null }); + + const getNormalSize = ({ width, height, orientation }) => { + return (orientation || 0) >= 5 ? { width: height, height: width } : { width, height }; + }; + + const size = getNormalSize(await sharp(buf).metadata()); + url = urlJoin(GBServer.globals.publicAddress, min.botId, 'cache', Path.basename(localName)); + + const imageToken = `{{Insert_Image ${kind} ${size.width} ${size.height}}}`; + obj = imageToken; + } + } + }; + const traverse = (o, func) => { + for (var i in o) { + func.apply(this, [i, o[i], o]); + if (o[i] !== null && typeof o[i] == 'object') { + //going one step down in the object tree!! + traverse(o[i], func); + } + } + }; + + traverse(data, process); doc.render(data); - // Returns the buffer to be used with SAVE AS for example. + if (images) { + // Replaces images within the document. - const buf = doc.getZip().generate({ type: 'nodebuffer', compression: 'DEFLATE' }); + let docxImager = new DocxImager(); + await docxImager.load(localName); + let i = 0; + await CollectionUtil.asyncForEach(images, async image => { + const imageName = images[i++]; + await docxImager.insertImage({ imageName: imageName }); + }); + await docxImager.save(localName); + } - return buf; + const buffer = Fs.readFileSync(localName, 'binary'); + + return { localName: localName, url: url, data: buffer }; } public screenCapture(pid) { @@ -1462,9 +1521,7 @@ export class SystemKeywords { public async merge({ pid, file, data, key1, key2 }): Promise { GBLog.info(`BASIC: MERGE running on ${file} and key1: ${key1}, key2: ${key2}...`); - const { - min, user - } = await DialogKeywords.getProcessInfo(pid); + const { min, user } = await DialogKeywords.getProcessInfo(pid); const botId = min.instance.botId; const path = `/${botId}.gbai/${botId}.gbdata`; @@ -1492,8 +1549,7 @@ export class SystemKeywords { results = await client .api( - `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name - }')/range(address='A1:CZ${maxLines}')` + `${baseUrl}/drive/items/${document.id}/workbook/worksheets('${sheets.value[0].name}')/range(address='A1:CZ${maxLines}')` ) .get(); @@ -1589,9 +1645,7 @@ export class SystemKeywords { } public async tweet({ pid, text }) { - const { - min, user - } = await DialogKeywords.getProcessInfo(pid); + const { min, user } = await DialogKeywords.getProcessInfo(pid); const consumer_key = min.core.getParam(min.instance, 'Twitter Consumer Key', null); const consumer_secret = min.core.getParam(min.instance, 'Twitter Consumer Key Secret', null);