diff --git a/libs/html-templates-lib.bak2.ts b/libs/html-templates-lib.bak2.ts new file mode 100644 index 0000000..45d8391 --- /dev/null +++ b/libs/html-templates-lib.bak2.ts @@ -0,0 +1,215 @@ +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 e from "express" +//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" +const width_px = 1200; //TODO read from htmlOption + +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} tdata 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, tdata: 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 (typeof t === 'string') { + // await page.goto(t, { waitUntil: 'networkidle0' }); + await page.goto(t, { waitUntil: 'load' }); + } else { + if (tdata.data) { + const template = Handlebars.compile(t.toString()); + const html = template(tdata.data); + await page.setContent(html); + } else { + await page.setContent(t.toString()) + } + } + + + /* + // try to load whole page + let x = await page.evaluate(async (tdata) => { + const scrollableSection = + (tdata.htmlOption?.querySelector && document.querySelector(tdata.htmlOption.querySelector)) ? + document.querySelector(tdata.htmlOption.querySelector) : document.body + if (scrollableSection) { + const childElement = scrollableSection.firstElementChild; + let scrollPosition = 0; + let viewportHeight = window.innerHeight; + if (childElement) + while (scrollPosition < childElement.scrollHeight) { + scrollableSection.scrollBy(0, viewportHeight); + await new Promise(resolve => setTimeout(resolve, 500)); + scrollPosition += viewportHeight; + } + return scrollPosition + } + return 0 + }, tdata); + //console.log("scrollPosition=" + x) + + */ + //force scroll for lazy load page + //tdata.htmlOption? + await page.evaluate(async (_viewportHeight, _ms, _loop) => { + for (let i = 0; i < _loop; i++) { + window.scrollBy(0, _viewportHeight); + await new Promise(resolve => setTimeout(resolve, 500)); + } + }, 1000, 300, 5); + + + + //find real page height + const totalHeight = await page.evaluate(async (tdata) => { + let scrollableSection = + (tdata.htmlOption?.querySelector && + document.querySelector(tdata.htmlOption.querySelector) && document.querySelector(tdata.htmlOption.querySelector)) ? + document.querySelector(tdata.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 childElement.scrollHeight + }, tdata); + + 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); + } + + console.log("set viewport ") + await page.setViewport({ + width: width_px, + 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 + //const updatedFruit = {...afruit ,...req.body} + //TODO overide option from htmlTemplateOption + let pdfOption = tdata.htmlOption?.pdfOption??{ + // path: './url_prop.pdf', + // format:"A4", + width: width_px, + height: totalHeight, + printBackground: true, + scale: 1, + displayHeaderFooter: false, + margin: { top: 5, right: 5, bottom: 5, left: 5 } + } + 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) + } + } +}) + diff --git a/test-run/lazy-load-puppeteer.ts b/test-run/lazy-load-puppeteer.ts new file mode 100644 index 0000000..cdd8e8e --- /dev/null +++ b/test-run/lazy-load-puppeteer.ts @@ -0,0 +1,91 @@ +import puppeteer, { Browser, PDFOptions } from 'puppeteer' +function wait(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export default async function capture(browser: Browser, url: string) { + // Load the specified page + const page = await browser.newPage(); + // if(!page) return + await page.goto(url, { waitUntil: 'load' }); + + /* + //force load page + for(let i=0; i< 50;i++){ + await page.evaluate(_viewportHeight => { + window.scrollBy(0, _viewportHeight); + }, 1000); + await wait(500); + }*/ + + //force load page and find page height + const totalHeight = await page.evaluate(async (_viewportHeight, _ms, _loop) => { + const querySelector = ".scrollbar-view" + //const querySelector = "body" + let scrollableSection = document.querySelector(querySelector) ?? window + for (let i = 0; i < _loop; i++) { + scrollableSection.scrollBy(0, _viewportHeight); + await new Promise(resolve => setTimeout(resolve, 500)); + } + return document.body.scrollHeight + }, 1000, 2000, 50); + + // Get the height of the rendered page + const bodyHandle = await page.$('body'); + if (!bodyHandle) return + const boundingBox = await bodyHandle.boundingBox(); + if (!boundingBox) return + const { height } = boundingBox; + await bodyHandle.dispose(); + console.log("height", height) + // Scroll one viewport at a time, pausing to let content load + const viewportHeight = page.viewport()?.height; + if (!viewportHeight) return + let viewportIncr = 0; + + + /* + while (viewportIncr + viewportHeight < boundingBox.height) { + await page.evaluate(_viewportHeight => { + window.scrollBy(0, _viewportHeight); + }, viewportHeight); + await wait(300); + viewportIncr = viewportIncr + viewportHeight; + } + */ + // Scroll back to top + await page.evaluate(() => { + window.scrollTo(0, 0); + }); + + // Some extra delay to let images load + await wait(100); + + // return await page.screenshot({ path: '.output/url_pup.png',type: 'png',fullPage: true }); + let pdfOption: PDFOptions = { + path: '.output/url_prop.pdf', + format: "A4", + width: 1200, + height: totalHeight, + printBackground: true, + scale: 1, + displayHeaderFooter: false, + margin: { top: 5, right: 5, bottom: 5, left: 5 } + } + const buffer = await page.pdf(pdfOption); + return buffer + + + +} + +(async () => { + const browser = await puppeteer.launch({ + // executablePath: '/usr/bin/chromium', + headless: true, + //args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'] + }); + + await capture(browser, "https://bma-dashboard.frappet.synology.me/d/ANtkJay4z/4Lic4Li54LmJ4Lie4Li04LiB4Liy4Lij?orgId=1&kiosk") + await browser.close(); +})(); \ No newline at end of file