import express from "express" export const htmlTemplateRoute = express.Router() import { mimeToExtension, templateOption } from "./report-template" // import fs from "fs" //import { chromium } from 'playwright' import puppeteer, { PDFOptions } from "puppeteer" import Handlebars from "handlebars" //import { createReport } from "docx-templates" // แก้ package.json ของ LibreOfficeFileConverter // https://github.com/microsoft/TypeScript/issues/52363#issuecomment-1659179354 //import { LibreOfficeFileConverter } from "libreoffice-file-converter" const TEMPLATE_FOLDER_NAME = "templates/html" function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } /** * docxTemplate Uses docx-template to convert input data and template to output buffer. * SPA and lazy load page may not fully render(eg. pantip.com). * You have to handle exception throw by function * handlebars template support only content from Buffer * @param {Buffer|String} t template in buffer format or url to web page * @param {templateOption} templOpt Template Information in JSON format * @param {String} outputMediaType output extension, support pdf, jpeg, png * @return {Promise} output buffer after apply template. */ export async function htmlTemplateX( t: Buffer | String, templOpt: templateOption, outputMediaType: string = "pdf" ): Promise { try { if (!["pdf", "jpeg", "png"].find((e) => e === outputMediaType)) { throw "FormatError" } const browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox"], }) const page = await browser.newPage() if (templOpt.htmlOption?.navigationTimeout) page.setDefaultNavigationTimeout(120000) if (typeof t === "string") { switch (templOpt.htmlOption?.waitUntil) { case "networkidle0": await page.goto(t, { waitUntil: "networkidle0" }) break case "networkidle2": await page.goto(t, { waitUntil: "networkidle2" }) break default: await page.goto(t) } } else { if (templOpt.data) { const template = Handlebars.compile(t.toString()) const html = template(templOpt.data) await page.setContent(html) } else { await page.setContent(t.toString()) } } const totalHeight = await page.evaluate(async (_templOpt) => { //force scroll for lazy load page const _scroll = _templOpt.htmlOption?.preloadScroll ?? 1000 const _wait = _templOpt.htmlOption?.preloadWait ?? 400 const _loop = _templOpt.htmlOption?.preloadLoop ?? 4 for (let i = 0; i < _loop; i++) { window.scrollBy(0, _scroll) await new Promise((resolve) => setTimeout(resolve, _wait)) } //extra wait await new Promise((resolve) => setTimeout(resolve, _wait * 2)) let scrollableSection = _templOpt.htmlOption?.querySelector && document.querySelector(_templOpt.htmlOption.querySelector) ? document.querySelector(_templOpt.htmlOption.querySelector) : null const childElement = scrollableSection ? scrollableSection : document.body if (scrollableSection == null) scrollableSection = document.body //const childElement = scrollableSection.firstElementChild; let scrollPosition = 0 let viewportHeight = window.innerHeight while (scrollPosition < childElement.scrollHeight) { scrollableSection.scrollBy(0, viewportHeight) await new Promise((resolve) => setTimeout(resolve, 500)) scrollPosition += viewportHeight } //return scrollPosition return childElement.scrollHeight }, templOpt) console.log("totalHeight") if (!totalHeight) { throw new Error( `Unable to determine the page height ${totalHeight}. The selector may not correct or no body tag` ) } else { console.log("Page height adjusted to:", totalHeight) } await page.setViewport({ width: Number(templOpt.htmlOption?.pdfOption?.width ?? 1200), height: totalHeight, deviceScaleFactor: 2, isMobile: false, }) ///// output to photo end here if (outputMediaType === "png" || outputMediaType === "jpeg") { const photoBuffer = await page.screenshot({ // path: 'url_pup.png', fullPage: true, type: outputMediaType, // 'webp' }) await browser.close() return photoBuffer } ///// output to PDF //TODO overide option from htmlTemplateOption let o: PDFOptions = { // path: './url_prop.pdf', // format:"A4", width: 1200, height: totalHeight, printBackground: true, scale: 1, displayHeaderFooter: false, margin: { top: 5, right: 5, bottom: 5, left: 5 }, } //Murge with default if (templOpt.htmlOption?.pdfOption) o = { ...o, ...templOpt.htmlOption.pdfOption } const { path, ...pdfOption } = o //remove path if exists console.log("pdfOption:", pdfOption) const buffer = await page.pdf(pdfOption) await browser.close() return buffer } catch (e) { //console.log(e) throw e } } /** javascript-obfuscator:disable * @swagger * /api/v1/report-template/html: * post: * summary: แปลหน้าเวปไปเป็น pdf , png, jpeg (ฟีเจอร์ html template ด้วย handlebars ยังไม่เสร็จ) ค่า template เป็น url ของหน้าเวปที่ต้องการ, reportName เป็นชื่อไฟล์ที่ต้องการ * tags: [report-template] * requestBody: * required: true * content: * application/json: * schema: * $ref: '#/components/schemas/templateOption' * example: * template: https://bma-dashboard.frappet.synology.me/d/ANtkJay4z/4Lic4Li54LmJ4Lie4Li04LiB4Liy4Lij?orgId=1&kiosk * reportName: html-report * htmlOption: {"querySelector": ".scrollbar-view"} * responses: * 201: * description: เอกสารถูกสร้างขึ้น * content: * application/pdf: * schema: * type: string * format: binary * image/png: * schema: * type: string * format: binary * image/jpeg: * schema: * type: string * format: binary * 400: * description: format error * 500: * description: Server error * */ htmlTemplateRoute.post("/", async function (req, res) { try { if (!req.headers["content-type"] || !req.headers["accept"]) throw new Error("Require header content-type, accept") let inputType = mimeToExtension(req.headers["content-type"]) // application/json let outputMediaType = mimeToExtension(req.headers["accept"]) let buffer = await htmlTemplateX( req.body.template, req.body, outputMediaType ) res.statusCode = 201 res.setHeader("Content-Type", req.headers["accept"]) res.setHeader( "Content-Disposition", `attachment;filename=${req.body.reportName}.${outputMediaType}` ) res.setHeader("Content-Length", buffer.length) res.end(buffer) } catch (ex) { if (ex instanceof SyntaxError) { res.statusCode = 400 res.statusMessage = ex.message res.end(res.statusMessage) console.error("report-template/html: ", ex) } else { res.statusCode = 500 res.statusMessage = "Internal Server Error during POST report-template/html" res.end(res.statusMessage) console.error("report-template/html: ", ex) } } })