url 2 pdf support, update test run, reduse imge size,
This commit is contained in:
parent
e921875bde
commit
f6b68e4379
32 changed files with 3851 additions and 1108 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,7 +7,7 @@ yarn-error.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
dist2
|
dist2
|
||||||
|
.output
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
|
|
||||||
32
Dockerfile
32
Dockerfile
|
|
@ -1,22 +1,42 @@
|
||||||
# docker build -t docker.frappet.com/demo/report-server .
|
# docker build -t docker.frappet.com/demo/report-server .
|
||||||
# docker push docker.frappet.com/demo/report-server
|
# docker push docker.frappet.com/demo/report-server
|
||||||
# docker run --name rserver -p 80:3000 docker.frappet.com/demo/report-server
|
# docker run --name rserver -p 80:3000 docker.frappet.com/demo/report-server
|
||||||
FROM node:20
|
FROM node:22-bookworm-slim
|
||||||
# ENV PANDOC_VERSION 3.1.7
|
|
||||||
|
|
||||||
ENV TZ=Asia/Bangkok
|
ENV TZ=Asia/Bangkok
|
||||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
RUN mkdir -p /usr/share/fonts/truetype/th
|
RUN mkdir -p /usr/share/fonts/truetype/th
|
||||||
COPY ./ThaiFonts/*.ttf /usr/share/fonts/truetype/th/
|
COPY ./ThaiFonts/*.ttf /usr/share/fonts/truetype/th/
|
||||||
# RUN fc-cache -f -v
|
RUN apt-get update && apt-get -y install wget fonts-noto fonts-noto-cjk libreoffice --no-install-recommends
|
||||||
RUN apt-get -qq update && apt-get -qq -y install wget fonts-noto fonts-noto-cjk libreoffice --no-install-recommends
|
## old work library chrominum 2.29GB
|
||||||
|
# RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 \
|
||||||
|
# libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 \
|
||||||
|
# libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 \
|
||||||
|
# libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation \
|
||||||
|
# libappindicator1 libnss3 lsb-release xdg-utils chromium --no-install-recommends
|
||||||
|
|
||||||
|
# new test with chrominum 1.77GB ตรวจขนาดหน้า blognone ผิดติดเป็นสองหน้า
|
||||||
|
# RUN apt-get -y install --no-install-recommends libx11-xcb1 libxcomposite1 libasound2 libatk1.0-0 libatk-bridge2.0-0 \
|
||||||
|
# libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 \
|
||||||
|
# libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 \
|
||||||
|
# libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6
|
||||||
|
|
||||||
|
## new test with chrome 1.81GB
|
||||||
|
RUN apt-get install -y --no-install-recommends ca-certificates fonts-liberation libasound2 libatk-bridge2.0-0 \
|
||||||
|
libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 \
|
||||||
|
libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
|
||||||
|
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils
|
||||||
|
RUN npx puppeteer install chrome --install-deps
|
||||||
|
|
||||||
|
|
||||||
RUN fc-cache -f && rm -rf /var/cache/* && apt -y autoremove && rm -rf /var/lib/apt/lists/* && apt-get clean
|
RUN fc-cache -f && rm -rf /var/cache/* && apt -y autoremove && rm -rf /var/lib/apt/lists/* && apt-get clean
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY templates templates
|
COPY templates templates
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV=production
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
|
|
||||||
COPY dist .
|
COPY dist .
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
CMD ["node","app.js"]
|
CMD ["node","app.js"]
|
||||||
|
|
|
||||||
229
README.md
229
README.md
|
|
@ -1,29 +1,24 @@
|
||||||
# report-server-ts
|
# report-server-ts
|
||||||
เป็น Web API ออกแบบมาเพื่อสร้างเอกสารจาก templste สามารถใช้ frontend โดยตรง เพื่อจะได้ไม่ต้องทำ backend เพื่อสร้างเอกสารเฉพาะตัวออกมา
|
เป็น Microservice(Web API) ออกแบบมาเพื่อสร้างเอกสารจาก templste หรือแปลงไฟล์
|
||||||
|
สามารถใช้ซ้ำได้หลายโปรเจ็ก
|
||||||
|
|
||||||
|
|
||||||
## Feature & Change
|
## Feature & Change
|
||||||
- แก้ไขจากเดิมตัวเดิมเป็น JavaScript เป็น TypeScript ตัด pandoc ออก
|
- ใช้ไฟล์ docx/xlsx เป็น template คล้าย Mail Merge ยูสเซอร์ออกแบบเองได้ รวมกับ ข้อมูลใน JSON เพื่อออกเป็น docx/xlsx
|
||||||
- ใช้ docx xlsx เป็น template เพื่อสร้างเอกสารคล้าย Mail Merge ลูกค้าออกแบบเองได้
|
- JSON + template จะได้ไฟล์ output ฟอร์แม็ตจะเป็นไฟล์ของ template(docx/xlsx) สามารถแปลเป็นฟอร์แม็ต png, pdf, jpeg ฯลฯ ได้ด้วยความสามารถของ Libreoffice(soffice)
|
||||||
- รายงานที่ได้เป็นไฟล์แบบเดียวกับ template หรือแปลงเป็น pdf หรือฟอร์แม็ตของ LibreOffice ได้
|
- ใช้ url ของหน้าเวปแปลงเป็นไฟล์รองรับ pdf, png, jpeg เพื่อออกรายงานจากหน้าเวปได้ รองรับ Query Selector
|
||||||
- แปลงไฟล์จาก MS Office เป็น PDF รองรับการตัดคำไทย
|
- แปลงไฟล์เป็น ภาพหรือ PDF รองรับการตัดคำไทย (ตัวออกรายงานตัวอื่นๆมักจะมีปัญหาการตัดคำ)
|
||||||
- โค้ดมีการ obfuscator เพื่อลดขนาดและกันลูกค้าเอาโค้ดไปใช้
|
- โค้ดมีการ obfuscator เพื่อลดขนาดและกันลูกค้าเอาโค้ดไปใช้
|
||||||
- มี Swagger ไว้ทดสอบ API มี libs/swagger-specs.json เพื่อนำเข้า Postman หรือเครื่องมื่อื่นๆได้
|
- มี Swagger ไว้ทดสอบ API มี libs/swagger-specs.json เพื่อนำเข้า Postman หรือเครื่องมื่อื่นๆได้
|
||||||
- มีโปรแกรมช่วยทดสอบ template แบบง่ายๆ ให้ทดสอบก่อนเอาเข้าเซิร์ฟเวอร์
|
- มีโปรแกรมช่วยทดสอบ template แบบง่ายๆ ให้ทดสอบก่อนเอาเข้าเซิร์ฟเวอร์
|
||||||
- Docker Image จะใช้แบบ Standalone หรือเป็น Microservice ร่วมกับโปรเจ็กอื่นๆได้ ใช้งานทันที
|
- build เป็น Docker Image เพื่อใช้ใน Microservice ในโปรเจ็กอื่นๆ ใช้ prefix /api/v1/report-template/* ลดขนาดแล้วด้วย node:22-bookworm-slim
|
||||||
- Docker image เหลือ 1.76GB จากเดิม 3.5GB
|
|
||||||
|
|
||||||
## ติดตั้ง
|
## development
|
||||||
รันโปรเจ็กบน Linux ,clone project ติดตั้ง Fonts และ LibreOffice ตามวิธีใน Dockerfile แล้วไปหัวข้อการใช้งานได้เลย หัวนี้ไว้เพื่ออ้างอิงเท่านั้น ใช้ node 20.7.0 บน Linux AMD x86-64
|
พัฒนาและทดสอบบน node 22 ,Linux AMD x86-64 ให้ clone project ติดตั้ง Fonts และ LibreOffice ตามวิธีใน [Dockerfile](./Dockerfile)
|
||||||
ตั้งค่าของ TypeScript [ตามเวปนี้](https://www.geeksforgeeks.org/how-to-use-express-in-typescript/) ให้ใช้ ES module ได้ด้วย
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm i express
|
# clone project
|
||||||
npm i -D typescript @types/express @types/node ts-node
|
npm install
|
||||||
npm i docx-templates xlsx-template-next swagger-ui-express swagger-jsdoc yaqrcode cors libreoffice-file-converter
|
|
||||||
npm i -D @types/swagger-ui-express @types/swagger-jsdoc @types/cors
|
|
||||||
# obfuscate code tools
|
|
||||||
npm i -D javascript-obfuscator
|
|
||||||
# add type support for yaqrcode
|
# add type support for yaqrcode
|
||||||
cd node_modules/yaqrcode
|
cd node_modules/yaqrcode
|
||||||
wget https://raw.githubusercontent.com/zenozeng/node-yaqrcode/master/index.d.ts
|
wget https://raw.githubusercontent.com/zenozeng/node-yaqrcode/master/index.d.ts
|
||||||
|
|
@ -44,7 +39,7 @@ docker compose up -d
|
||||||
npm run push:docker
|
npm run push:docker
|
||||||
```
|
```
|
||||||
|
|
||||||
## ทดสอบ template
|
## ทดสอบ template/unit test
|
||||||
|
|
||||||
ไปที่โฟลเดอร์ test-run มีโปรแกรมเพื่อทดสอบ template ให้ทดสอบที่นี้ก่อนเอา template ไปวางในเซิร์ฟเวอร์ มีค่า default สำหรับการทดสอบที่ใช้ได้เลย สามารถแปลงไปไฟล์แบบต่างๆที่ Libreoffice รองรับ(จำเป็นต้องติดตั้ง ) ควรทดสอบรูปแบบข้อมูล(json) ให้เข้ากับ template(docx,xlsx) ถ้าเกิดปัญหา ถ้าค่าไม่ครบ template แบบ docx จะ error log ที่เซิร์ฟเวอร์ ส่วน xlsx ไม่แจ้งปัญหา แค่ไม่แสดงค่านั้นๆ คู่มือการใช้งานที่สมบูรณ์ให้ไปที่เวปของ library ที่ใช้
|
ไปที่โฟลเดอร์ test-run มีโปรแกรมเพื่อทดสอบ template ให้ทดสอบที่นี้ก่อนเอา template ไปวางในเซิร์ฟเวอร์ มีค่า default สำหรับการทดสอบที่ใช้ได้เลย สามารถแปลงไปไฟล์แบบต่างๆที่ Libreoffice รองรับ(จำเป็นต้องติดตั้ง ) ควรทดสอบรูปแบบข้อมูล(json) ให้เข้ากับ template(docx,xlsx) ถ้าเกิดปัญหา ถ้าค่าไม่ครบ template แบบ docx จะ error log ที่เซิร์ฟเวอร์ ส่วน xlsx ไม่แจ้งปัญหา แค่ไม่แสดงค่านั้นๆ คู่มือการใช้งานที่สมบูรณ์ให้ไปที่เวปของ library ที่ใช้
|
||||||
[docx-templates](https://www.npmjs.com/package/docx-templates) และ
|
[docx-templates](https://www.npmjs.com/package/docx-templates) และ
|
||||||
|
|
@ -61,20 +56,200 @@ $ npx ts-node xlsx-template.ts
|
||||||
Output extension(xlsx,pdf): ods
|
Output extension(xlsx,pdf): ods
|
||||||
JSON data path(./xlsx.json):
|
JSON data path(./xlsx.json):
|
||||||
Base path of templates-docx(..):
|
Base path of templates-docx(..):
|
||||||
|
|
||||||
|
$ npx ts-node html-template.ts
|
||||||
|
```
|
||||||
|
## API
|
||||||
|
หลัง npm run dev ไปดูที่ [http://localhost:3001/swagger](http://localhost:3001/swagger)
|
||||||
|
หรือใช้ Rest Client ดูไฟล์ [api.http](./api.http)
|
||||||
|
|
||||||
|
ตรง http header จะใช้ accept เป็นตัวบอกว่าต้องการผลเป็นไฟล์แบบไหนโดยใช้ [Mime type](https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types) เพื่อเป็นมาตรฐาน ให้ดูที่รองรับในฟังก์ชั่น [mimeToExtension](./libs/report-template.ts)
|
||||||
|
|
||||||
|
### HTML
|
||||||
|
แปลงจาก URL เป็น pdf,png,jpg รองรับการตัดคำไทย ฟีเจอร์ template ยังไม่เสร็จ
|
||||||
|
``` sh
|
||||||
|
# Grafana dashboard to pdf
|
||||||
|
curl -X 'POST' \
|
||||||
|
'http://localhost:3001/api/v1/report-template/html' \
|
||||||
|
-H 'accept: application/pdf' -H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"template": "https://bma-dashboard.frappet.synology.me/d/ANtkJay4z/4Lic4Li54LmJ4Lie4Li04LiB4Liy4Lij?orgId=1&kiosk",
|
||||||
|
"reportName": "html-grafana",
|
||||||
|
"htmlOption": {
|
||||||
|
"querySelector": ".scrollbar-view"
|
||||||
|
}
|
||||||
|
}' -o html-grafana.pdf
|
||||||
|
|
||||||
|
# url to png
|
||||||
|
curl -X 'POST' 'http://localhost:3001/api/v1/report-template/html' \
|
||||||
|
-H 'accept: image/png' -H 'Content-Type: application/json' \
|
||||||
|
-d '{"template": "https://www.blognone.com/","reportName": "html-blognone"}' -o html-blognone.png
|
||||||
|
# url to jpeg
|
||||||
|
curl -X 'POST' 'http://localhost:3001/api/v1/report-template/html' \
|
||||||
|
-H 'accept: image/jpeg' -H 'Content-Type: application/json' \
|
||||||
|
-d '{"template": "https://pantip.com/","reportName": "html-blognone"}' -o html-pantip.jpeg
|
||||||
```
|
```
|
||||||
|
|
||||||
# Build docker
|
### docx
|
||||||
```bash
|
แปลงจากเทมเพลทไฟล์ .docx เป็น docx,pdf,png
|
||||||
docker build -t docker.frappet.com/demo/report-server .
|
```sh
|
||||||
docker push docker.frappet.com/demo/report-server
|
curl -X 'POST' \
|
||||||
docker run --name rserver -p 80:3000 docker.frappet.com/demo/report-server
|
'https://report-server.frappet.synology.me/api/v1/report-template/docx?folder=command' \
|
||||||
|
-H 'accept: application/vnd.openxmlformats-officedocument.wordprocessingml.document' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"template": "C-PM-01_cover",
|
||||||
|
"reportName": "command-C-PM-01_cover",
|
||||||
|
"data": {
|
||||||
|
"issue": "............",
|
||||||
|
"title": "......",
|
||||||
|
"commandNo": "......",
|
||||||
|
"commandYear": "......",
|
||||||
|
"commandTitle": "คำสั่งบรรจุและแต่งตั้งผู้สอบแข่งขันได้",
|
||||||
|
"detailHeader": "",
|
||||||
|
"detailBody": "อาศัยอำนาจตามความในมาตรา ๔๔ มาตรา ๕๒ (๔) แห่งพระราชบัญญัติระเบียบข้าราชการกรุงเทพมหานครและบุคลากรกรุงเทพมหานคร พ.ศ.๒๕๕๔ ประกอบกับกฎ ก.ก. ว่าด้วยการทดลองปฏิบัติหน้าที่ราชการและการพัฒนาข้าราชการกรุงเทพมหานครสามัญที่อยู่ระหว่างทดลองปฏิบัติหน้าที่ราชการ พ.ศ. ๒๕๕๕ มติคณะกรรมการข้าราชการกรุงเทพมหานครและบุคลากรกรุงเทพมหานคร ครั้งที่ ๑/๒๕๕๔ เมื่อวันที่ ๒๒ ธันวาคม ๒๕๕๔ มติ อ.ก.ก. วิสามัญเกี่ยวกับระบบราชการ การจัดส่วนราชการและค่าตอบแทน ครั้งที่ ๙/๒๕๕๖ เมื่อ ๑๘ กันยายน ๒๕๕๖ ประกาศสำนักงาน ก.ก. ลงวันที่ ………………………………….. เรื่อง รับสมัครสอบแข่งขันเพื่อบรรจุและแต่งตั้งบุคคลเข้ารับราชการเป็นข้าราชการการกรุงเทพมหานครสามัญ ครั้งที่ ………………………………….. และประกาศสำนักงาน ก.ก. ลงวันที่ ………………………………….. เรื่อง ผลการสอบแข่งขันเพื่อบรรจุและแต่งตั้งบุคคลเข้ารับราชการเป็นข้าราชการกรุงเทพมหานครสามัญ ครั้งที่ ………………………………….. ตำแหน่ง………………………………….. ให้บรรจุผู้สอบแข่งขันได้เข้ารับราชการเป็นข้าราชการกรุงเทพมหานครสามัญ และแต่งตั้งให้ดำรงตำแหน่ง………………………………….. จำนวน ………………………………….. ราย โดยให้ทดลองปฏิบัติหน้าที่ราชการในตำแหน่งที่ได้รับแต่งตั้งดังบัญชีรายละเอียดแนบท้ายคำสั่งนี้",
|
||||||
|
"detailFooter": "",
|
||||||
|
"commandDate": "..................",
|
||||||
|
"commandAffectDate": "..................",
|
||||||
|
"commandExcecuteDate": "..................",
|
||||||
|
"name": "....................................",
|
||||||
|
"position": "ผู้อำนวยการสำนัก/เขต",
|
||||||
|
"authorizedUserFullName": "............",
|
||||||
|
"authorizedPosition": "..................."
|
||||||
|
}
|
||||||
|
}' -o docx-command-C-PM-01_cover.docx
|
||||||
|
|
||||||
|
|
||||||
|
curl -X 'POST' \
|
||||||
|
'https://report-server.frappet.synology.me/api/v1/report-template/docx?folder=command' \
|
||||||
|
-H 'accept: application/pdf' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"template": "C-PM-01_cover",
|
||||||
|
"reportName": "command-C-PM-01_cover",
|
||||||
|
"data": {
|
||||||
|
"issue": "............",
|
||||||
|
"title": "......",
|
||||||
|
"commandNo": "......",
|
||||||
|
"commandYear": "......",
|
||||||
|
"commandTitle": "คำสั่งบรรจุและแต่งตั้งผู้สอบแข่งขันได้",
|
||||||
|
"detailHeader": "",
|
||||||
|
"detailBody": "อาศัยอำนาจตามความในมาตรา ๔๔ มาตรา ๕๒ (๔) แห่งพระราชบัญญัติระเบียบข้าราชการกรุงเทพมหานครและบุคลากรกรุงเทพมหานคร พ.ศ.๒๕๕๔ ประกอบกับกฎ ก.ก. ว่าด้วยการทดลองปฏิบัติหน้าที่ราชการและการพัฒนาข้าราชการกรุงเทพมหานครสามัญที่อยู่ระหว่างทดลองปฏิบัติหน้าที่ราชการ พ.ศ. ๒๕๕๕ มติคณะกรรมการข้าราชการกรุงเทพมหานครและบุคลากรกรุงเทพมหานคร ครั้งที่ ๑/๒๕๕๔ เมื่อวันที่ ๒๒ ธันวาคม ๒๕๕๔ มติ อ.ก.ก. วิสามัญเกี่ยวกับระบบราชการ การจัดส่วนราชการและค่าตอบแทน ครั้งที่ ๙/๒๕๕๖ เมื่อ ๑๘ กันยายน ๒๕๕๖ ประกาศสำนักงาน ก.ก. ลงวันที่ ………………………………….. เรื่อง รับสมัครสอบแข่งขันเพื่อบรรจุและแต่งตั้งบุคคลเข้ารับราชการเป็นข้าราชการการกรุงเทพมหานครสามัญ ครั้งที่ ………………………………….. และประกาศสำนักงาน ก.ก. ลงวันที่ ………………………………….. เรื่อง ผลการสอบแข่งขันเพื่อบรรจุและแต่งตั้งบุคคลเข้ารับราชการเป็นข้าราชการกรุงเทพมหานครสามัญ ครั้งที่ ………………………………….. ตำแหน่ง………………………………….. ให้บรรจุผู้สอบแข่งขันได้เข้ารับราชการเป็นข้าราชการกรุงเทพมหานครสามัญ และแต่งตั้งให้ดำรงตำแหน่ง………………………………….. จำนวน ………………………………….. ราย โดยให้ทดลองปฏิบัติหน้าที่ราชการในตำแหน่งที่ได้รับแต่งตั้งดังบัญชีรายละเอียดแนบท้ายคำสั่งนี้",
|
||||||
|
"detailFooter": "",
|
||||||
|
"commandDate": "..................",
|
||||||
|
"commandAffectDate": "..................",
|
||||||
|
"commandExcecuteDate": "..................",
|
||||||
|
"name": "....................................",
|
||||||
|
"position": "ผู้อำนวยการสำนัก/เขต",
|
||||||
|
"authorizedUserFullName": "............",
|
||||||
|
"authorizedPosition": "..................."
|
||||||
|
}
|
||||||
|
}' -o docx-command-C-PM-01_cover.pdf
|
||||||
|
|
||||||
|
|
||||||
|
curl -X 'POST' \
|
||||||
|
-H 'accept: application/pdf' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
'http://localhost:3001/api/v1/report-template/docx' \
|
||||||
|
-d '{
|
||||||
|
"template": "hello",
|
||||||
|
"reportName": "docx-report",
|
||||||
|
"data": {
|
||||||
|
"docNo": "๑๒๓๔๕",
|
||||||
|
"me": "กระผม",
|
||||||
|
"prefix": "นาย",
|
||||||
|
"name": "สรวิชญ์",
|
||||||
|
"surname": "พลสิทธิ์",
|
||||||
|
"position": "Chief Technology Officer",
|
||||||
|
"org": {
|
||||||
|
"type": "บริษัท",
|
||||||
|
"name": "เฟรปเป้ที",
|
||||||
|
"url": "https://frappet.com"
|
||||||
|
},
|
||||||
|
"employees": [
|
||||||
|
{
|
||||||
|
"name": "ภาวิชญ์",
|
||||||
|
"surname": "พลสิทธิ์"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "วิชญาภา",
|
||||||
|
"surname": "พลสิทธิ์"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}' -o docx-report.pdf
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Bun Note
|
### xlsx
|
||||||
เริ่มแรกในการพอร์ตจาก JavaScript ลองใช้ Bun(TypeScript) แทน Node.js ตัว Bun ค่อนข้างน่าประทับใจใช้ TypeScript โดยตรงไม่ต้องตั้งค่า หรือติดตั้งเพิ่ม แต่มีปัญหากับ libreoffice-file-converter ต้องแก้ค่าใน package.json
|
แปลงจากเทมเพลทไฟล์ .xlsx เป็น xlsx,pdf,png
|
||||||
docker-template ฟังก์ชั่นพื้นฐานใช้งานพอได้ ส่วน EXEC กับ custom function ทำงานไม่ได้ คาดว่าเป็นปัญหาจาก eval เลยกลับมาใช้ node เหมือนเดิม
|
|
||||||
|
|
||||||
## Todo
|
```
|
||||||
- รองรับ Authentication Header เพื่อให้ยูสเซอร์ในระบบใช้งานได้เท่านั้น
|
curl -X 'POST' \
|
||||||
|
'http://localhost:3001/api/v1/report-template/xlsx' \
|
||||||
|
-H 'accept: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"template": "hello",
|
||||||
|
"reportName": "xlsx-report",
|
||||||
|
"data": {
|
||||||
|
"docNo": "๑๒๓๔๕",
|
||||||
|
"me": "กระผม",
|
||||||
|
"prefix": "นาย",
|
||||||
|
"name": "สรวิชญ์",
|
||||||
|
"surname": "พลสิทธิ์",
|
||||||
|
"position": "Chief Technology Officer",
|
||||||
|
"org": {
|
||||||
|
"type": "บริษัท",
|
||||||
|
"name": "เฟรปเป้ที",
|
||||||
|
"url": "https://frappet.com"
|
||||||
|
},
|
||||||
|
"employees": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "ภาวิชญ์",
|
||||||
|
"surname": "พลสิทธิ์",
|
||||||
|
"score": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "วิชญาภา",
|
||||||
|
"surname": "พลสิทธิ์",
|
||||||
|
"score": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "ฐิตาภา",
|
||||||
|
"surname": "พลสิทธิ์",
|
||||||
|
"score": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "สรวิชญ์ พลสิทธิ์",
|
||||||
|
"surname": "พลสิทธิ์",
|
||||||
|
"score": 99
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}' -o xlsx-report.xlsx
|
||||||
|
```
|
||||||
|
|
||||||
|
# Known Issue
|
||||||
|
- soffice แปลงเป็น png หรือ jpeg ได้แค่ 96dpi มันฮาร์ดโค้ดอยู่ สามารถแก้โค้ด compile ใหม่(ยากไปหน่อย) work around(ยังไม่ได้ทำ) ภาพความละเอียดต่ำให้แปลงเป็น pdf แล้ว[แปลงเป็นภาพ](https://ask.libreoffice.org/t/change-default-resolution-in-batch-png-conversion/18464/2) ใช้ Imagemagick ในการแปลงข้อดีคือรองรับฟอร์แม็ตได้หลายแบบ แต่มัน [disable PDF เป็น default](https://stackoverflow.com/questions/52998331/imagemagick-security-policy-pdf-blocking-conversion) ต้องแก้คอนฟิกก่อน ค่อนช้างกิน CPU ถ้ามีการแปลงหลายต่อ วิธีการนี้อาจจะทำเป็น API ในอนาคต คำสั่งด้านล่างลองแล้วใช้ได้
|
||||||
|
```
|
||||||
|
convert -density 300 -background white -alpha remove report-docx.pdf report-docx2.png
|
||||||
|
convert -density 300 -background white -alpha remove report-docx.pdf report-docx2.jpeg
|
||||||
|
```
|
||||||
|
- url แปลงเป็น pdf หรือภาพ จะเป็นหน้าเดียวเลย ยังไม่สามารถกำหนดขนาดทำเป็นหลายๆหน้าได้ อาจจะเป็นไปได้ยังหาวิธีอยู่เลยทำหน้าเดียวไปก่อน ยังมีปัญหากับการโหลดที่เป็น lazy load ยังไม่รองรับ Authentication header
|
||||||
|
|
||||||
|
## Note
|
||||||
|
- ยังไม่สามารถใช้ bun runtime มีปัญหากับ libreoffice-file-converter และ docker-template เลยไม่ได้เอามาใช้ รุ่นใหม่อาจจะแก้ปัญหานี้แล้วต้องลองอีกที
|
||||||
|
- Playwright ยังตรวจสอบความสูงของหน้าไม่ถูกต้องเลยใช้ puppeteer แทน
|
||||||
|
- น่าจะทำให้รองรับ Authentication Header เพื่อให้ยูสเซอร์ในระบบใช้งานได้เท่านั้น
|
||||||
- หาทางสร้างเอกสารจาก text เช่น Markdown เป็นเอกสาร MS Office
|
- หาทางสร้างเอกสารจาก text เช่น Markdown เป็นเอกสาร MS Office
|
||||||
- น่าจะทำ license key เผื่อขายให้ลูกค้าติดตั้งใช้งานต่อ (โค้ดที่ผ่าน obfuscator แล้วย้อนกลับมาได้ง่ายหรือเปล่า ?)
|
- น่าจะทำ license key เผื่อขายให้ลูกค้าติดตั้งใช้งานต่อ (โค้ดที่ผ่าน obfuscator แล้วย้อนกลับมาได้ง่ายหรือเปล่า ?)
|
||||||
|
- อาจจะลอง inkscape รองรับ command line อาจจะเอามาทำอะไรได้ "inkscape -z -e out.png -w 1024 in.svg"
|
||||||
|
- การแปลงภาพสามารใช้ image magick ได้ยังไม่ได้ลอง
|
||||||
|
- ถ้าใช้อิมเมจแบบ Distroless น่าจะเล็กลงแต่ตอนนี้ยังไม่มีรายการ library ที่จำเป็นทั้งหมดอีกทั้งอาจจะต้องใช้ shell เพื่อรัน soffice ใช้แบบ slim น่าจะเหมาะกับการนี้
|
||||||
|
- docker image น่าจะทำให้ติดตั้ง font เพิ่มได้
|
||||||
|
- ควรจำกัด domain ที่เรียกใช้ได้
|
||||||
120
api.http
Normal file
120
api.http
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
@api_host = http://localhost:3001/api/v1/report-template
|
||||||
|
|
||||||
|
############### docx
|
||||||
|
### แสดงรายการในโฟลเดอร์ templates/docx
|
||||||
|
GET {{api_host}}/docx
|
||||||
|
|
||||||
|
### สร้างเอกสารจากรายการ template docx ปกติจะได้ docx แล้วจะใช้ libreoffice แปลงเป็นเอกสารอื่นๆโดยกำหนดที่ accept
|
||||||
|
# accept: application/msword , image/png, image/jpeg, application/pdf
|
||||||
|
# accept: application/vnd.oasis.opendocument.text,application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||||
|
#POST {{api_host}}/docx?folder=test
|
||||||
|
POST {{api_host}}/docx
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept: application/pdf
|
||||||
|
|
||||||
|
{
|
||||||
|
"template": "hello",
|
||||||
|
"reportName": "docx-report",
|
||||||
|
"data": {
|
||||||
|
"docNo": "๑๒๓๔๕",
|
||||||
|
"me": "กระผม",
|
||||||
|
"prefix": "นาย",
|
||||||
|
"name": "สรวิชญ์",
|
||||||
|
"surname": "พลสิทธิ์",
|
||||||
|
"position": "Chief Technology Officer",
|
||||||
|
"org": {
|
||||||
|
"type": "บริษัท",
|
||||||
|
"name": "เฟรปเป้ที",
|
||||||
|
"url": "https://frappet.com"
|
||||||
|
},
|
||||||
|
"employees": [
|
||||||
|
{
|
||||||
|
"name": "ภาวิชญ์",
|
||||||
|
"surname": "พลสิทธิ์"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "วิชญาภา",
|
||||||
|
"surname": "พลสิทธิ์"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
############### xlsx
|
||||||
|
### แสดงรายการในโฟลเดอร์ templates/xlsx
|
||||||
|
GET {{api_host}}/xlsx
|
||||||
|
|
||||||
|
### สร้างเอกสารจากรายการ template docx ปกติจะได้ docx แล้วจะใช้ libreoffice แปลงเป็นเอกสารอื่นๆโดยกำหนดที่ accept
|
||||||
|
# accept: application/msword , image/png, image/jpeg, application/pdf
|
||||||
|
# accept: application/vnd.oasis.opendocument.text,application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||||
|
#POST {{api_host}}/docx?folder=test
|
||||||
|
POST {{api_host}}/xlsx
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept: application/pdf
|
||||||
|
|
||||||
|
{
|
||||||
|
"template": "hello",
|
||||||
|
"reportName": "xlsx-report",
|
||||||
|
"data": {
|
||||||
|
"docNo": "๑๒๓๔๕",
|
||||||
|
"me": "กระผม",
|
||||||
|
"prefix": "นาย",
|
||||||
|
"name": "สรวิชญ์",
|
||||||
|
"surname": "พลสิทธิ์",
|
||||||
|
"position": "Chief Technology Officer",
|
||||||
|
"org": {
|
||||||
|
"type": "บริษัท",
|
||||||
|
"name": "เฟรปเป้ที",
|
||||||
|
"url": "https://frappet.com"
|
||||||
|
},
|
||||||
|
"employees": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "ภาวิชญ์",
|
||||||
|
"surname": "พลสิทธิ์",
|
||||||
|
"score": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "วิชญาภา",
|
||||||
|
"surname": "พลสิทธิ์",
|
||||||
|
"score": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "ฐิตาภา",
|
||||||
|
"surname": "พลสิทธิ์",
|
||||||
|
"score": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "สรวิชญ์ พลสิทธิ์",
|
||||||
|
"surname": "พลสิทธิ์",
|
||||||
|
"score": 99
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
### convert Grafana dashboard to pdf
|
||||||
|
POST {{api_host}}/html
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept: application/pdf
|
||||||
|
|
||||||
|
{
|
||||||
|
"template": "https://bma-dashboard.frappet.synology.me/d/ANtkJay4z/4Lic4Li54LmJ4Lie4Li04LiB4Liy4Lij?orgId=1&kiosk",
|
||||||
|
"reportName": "html-grafana",
|
||||||
|
"htmlOption": {
|
||||||
|
"querySelector": ".scrollbar-view"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
### convert blognone to pdf
|
||||||
|
POST {{api_host}}/html
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept: image/png
|
||||||
|
|
||||||
|
{
|
||||||
|
"template": "https://www.blognone.com",
|
||||||
|
"reportName": "html-blognone"
|
||||||
|
}
|
||||||
2
app.ts
2
app.ts
|
|
@ -10,6 +10,7 @@ import swaggerUi from "swagger-ui-express"
|
||||||
import express, { Express, Request, Response } from 'express'
|
import express, { Express, Request, Response } from 'express'
|
||||||
import { docxTemplateRoute } from './libs/docx-templates-lib'
|
import { docxTemplateRoute } from './libs/docx-templates-lib'
|
||||||
import { xlsxTemplateRoute } from './libs/xlsx-template-lib'
|
import { xlsxTemplateRoute } from './libs/xlsx-template-lib'
|
||||||
|
import { htmlTemplateRoute } from "./libs/html-templates-lib"
|
||||||
import { convertTemplateRoute } from './libs/convert-libs'
|
import { convertTemplateRoute } from './libs/convert-libs'
|
||||||
const app: Express = express()
|
const app: Express = express()
|
||||||
const port: number = Number(process.env.PORT) || 80;
|
const port: number = Number(process.env.PORT) || 80;
|
||||||
|
|
@ -25,5 +26,6 @@ app.get('/', (req: Request, res: Response) => {
|
||||||
})
|
})
|
||||||
app.use('/api/v1/report-template/docx', docxTemplateRoute);
|
app.use('/api/v1/report-template/docx', docxTemplateRoute);
|
||||||
app.use('/api/v1/report-template/xlsx', xlsxTemplateRoute);
|
app.use('/api/v1/report-template/xlsx', xlsxTemplateRoute);
|
||||||
|
app.use('/api/v1/report-template/html', htmlTemplateRoute);
|
||||||
app.use('/api/v1/report-template/convert', convertTemplateRoute);
|
app.use('/api/v1/report-template/convert', convertTemplateRoute);
|
||||||
app.listen(port, () => console.log(`Application is running on port ${port}`))
|
app.listen(port, () => console.log(`Application is running on port ${port}`))
|
||||||
|
|
@ -85,17 +85,30 @@ convertTemplateRoute.post("/", async function (req, res) {
|
||||||
timeout: 60 * 1000,
|
timeout: 60 * 1000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const buffer = await libreOfficeFileConverter.convertBuffer(req.body, outputMediaType);
|
//const buffer = await libreOfficeFileConverter.convertBuffer(req.body, outputMediaType);
|
||||||
|
const buffer = await libreOfficeFileConverter.convert({
|
||||||
|
buffer:Buffer.from(req.body),
|
||||||
|
format: outputMediaType,
|
||||||
|
input: "buffer",
|
||||||
|
output: "buffer"
|
||||||
|
})
|
||||||
res.statusCode = 201;
|
res.statusCode = 201;
|
||||||
res.setHeader('Content-Type', req.headers['accept']);
|
res.setHeader('Content-Type', req.headers['accept']);
|
||||||
res.setHeader('Content-Disposition', `attachment;filename=${reportName}.${outputMediaType}`);
|
res.setHeader('Content-Disposition', `attachment;filename=${reportName}.${outputMediaType}`);
|
||||||
res.setHeader('Content-Length', buffer.length);
|
res.setHeader('Content-Length', buffer.length);
|
||||||
res.end(buffer);
|
res.end(buffer);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
res.statusCode = 500;
|
if(ex instanceof SyntaxError){
|
||||||
res.statusMessage = 'Internal Server Error';
|
res.statusCode = 400
|
||||||
res.end(res.statusMessage);
|
res.statusMessage = ex.message
|
||||||
console.error(`Error during convert with soffice:`, ex);
|
res.end(res.statusMessage)
|
||||||
|
console.error("report-template/convert: ", ex)
|
||||||
|
}else{
|
||||||
|
res.statusCode = 500
|
||||||
|
res.statusMessage = "Internal Server Error during POST report-template/convert"
|
||||||
|
res.end(res.statusMessage)
|
||||||
|
console.error("report-template/html: ", ex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -17,8 +17,7 @@ const swaggerOptions = {
|
||||||
servers: [
|
servers: [
|
||||||
{url: "https://report-server.frappet.synology.me"},
|
{url: "https://report-server.frappet.synology.me"},
|
||||||
{url:"https://bma-ehr.frappet.synology.me/"},
|
{url:"https://bma-ehr.frappet.synology.me/"},
|
||||||
{url: "http://localhost:3000"},
|
{url: "http://localhost:3001"},
|
||||||
{url: "http://192.168.2.101:3001"},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
apis: ["./libs/*.ts"],
|
apis: ["./libs/*.ts"],
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,29 @@
|
||||||
import express from "express"
|
import express from "express"
|
||||||
export const docxTemplateRoute = express.Router()
|
export const docxTemplateRoute = express.Router()
|
||||||
|
|
||||||
import { mimeToExtension, templateData } from "./report-template"
|
import { mimeToExtension, templateOption } from "./report-template"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { createReport } from "docx-templates"
|
import { createReport } from "docx-templates"
|
||||||
const qrcode = require("yaqrcode")
|
import qrcode from "yaqrcode"
|
||||||
const axios = require("axios")
|
import axios from "axios"
|
||||||
// แก้ package.json ของ LibreOfficeFileConverter
|
// แก้ package.json ของ LibreOfficeFileConverter
|
||||||
// https://github.com/microsoft/TypeScript/issues/52363#issuecomment-1659179354
|
// https://github.com/microsoft/TypeScript/issues/52363#issuecomment-1659179354
|
||||||
import { LibreOfficeFileConverter } from "libreoffice-file-converter"
|
import { LibreOfficeFileConverter } from "libreoffice-file-converter"
|
||||||
const TEMPLATE_FOLDER_NAME = "templates/docx"
|
const TEMPLATE_FOLDER_NAME = "templates/docx"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* docxTemplate Uses docx-template to convert input data and template to output buffer.
|
* docxTemplate Uses docx-template to convert input data and template to output buffer.
|
||||||
* You have to handle exception throw by function
|
* You have to handle exception throw by function
|
||||||
* template keep in folder templates
|
* template keep in folder templates
|
||||||
* @param {String} base base path of caller relate to template-docx foler (no trail slash)
|
* @param {Buffer|String} t template in buffer format or path to file
|
||||||
* @param {templateData} tdata Template Information in JSON format
|
* @param {templateOption} tdata Template Information in JSON format
|
||||||
* @param {String} outputMediaType output extension
|
* @param {String} outputMediaType output extension
|
||||||
* @return {Promise<Uint8Array>} output buffer after apply template.
|
* @return {Promise<Uint8Array>} output buffer after apply template.
|
||||||
*/
|
*/
|
||||||
export async function docxTemplateX(template: Buffer, tdata: templateData, outputMediaType: string = "docx"): Promise<Uint8Array> {
|
export async function docxTemplateX(t: Buffer|String, tdata: templateOption, outputMediaType: string = "docx"): Promise<Uint8Array> {
|
||||||
try {
|
try {
|
||||||
// let template = await fs.promises.readFile(`${base}/${TEMPLATE_FOLDER_NAME}/${tdata.template}.docx`)
|
const template = Buffer.isBuffer(t)?t: await fs.promises.readFile(String(t))
|
||||||
const buffer = await createReport({
|
const buffer = await createReport({
|
||||||
template,
|
template,
|
||||||
data: tdata.data,
|
data: tdata.data,
|
||||||
|
|
@ -36,7 +37,6 @@ export async function docxTemplateX(template: Buffer, tdata: templateData, outpu
|
||||||
const response = await axios.get(imageUrl, { responseType: "arraybuffer" })
|
const response = await axios.get(imageUrl, { responseType: "arraybuffer" })
|
||||||
const imageData = Buffer.from(response.data).toString("base64") // Convert image to base64
|
const imageData = Buffer.from(response.data).toString("base64") // Convert image to base64
|
||||||
const ext = ".png" // Assuming PNG format; adjust based on actual image type
|
const ext = ".png" // Assuming PNG format; adjust based on actual image type
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
|
@ -48,12 +48,18 @@ export async function docxTemplateX(template: Buffer, tdata: templateData, outpu
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (outputMediaType === "docx") return buffer
|
if (outputMediaType === "docx") return buffer
|
||||||
|
|
||||||
const libreOfficeFileConverter = new LibreOfficeFileConverter({
|
const libreOfficeFileConverter = new LibreOfficeFileConverter({
|
||||||
childProcessOptions: {
|
childProcessOptions: {
|
||||||
timeout: 60 * 1000,
|
timeout: 60 * 1000,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const lbuffer = await libreOfficeFileConverter.convertBuffer(Buffer.from(buffer), outputMediaType)
|
const lbuffer = await libreOfficeFileConverter.convert({
|
||||||
|
buffer:Buffer.from(buffer),
|
||||||
|
format: outputMediaType,
|
||||||
|
input: "buffer",
|
||||||
|
output: "buffer"
|
||||||
|
})
|
||||||
return lbuffer
|
return lbuffer
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e
|
throw e
|
||||||
|
|
@ -111,7 +117,7 @@ docxTemplateRoute.get("/", async function (req, res) {
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* $ref: '#/components/schemas/templateData'
|
* $ref: '#/components/schemas/templateOption'
|
||||||
* example:
|
* example:
|
||||||
* template: hello
|
* template: hello
|
||||||
* reportName: docx-report
|
* reportName: docx-report
|
||||||
|
|
@ -147,26 +153,28 @@ docxTemplateRoute.get("/", async function (req, res) {
|
||||||
docxTemplateRoute.post("/", async function (req, res) {
|
docxTemplateRoute.post("/", async function (req, res) {
|
||||||
try {
|
try {
|
||||||
if (!req.headers["content-type"] || !req.headers["accept"]) throw new Error("Require header content-type, accept")
|
if (!req.headers["content-type"] || !req.headers["accept"]) throw new Error("Require header content-type, accept")
|
||||||
let inputType = mimeToExtension(req.headers["content-type"])
|
let inputType = mimeToExtension(req.headers["content-type"]) // application/json
|
||||||
let outputMediaType = mimeToExtension(req.headers["accept"])
|
let outputMediaType = mimeToExtension(req.headers["accept"])
|
||||||
let template = null
|
const include_folder= req.query["folder"]?"/"+req.query["folder"]:''
|
||||||
// Save the converted file to disk
|
console.log(req.body)
|
||||||
if (req.query["folder"]) {
|
let buffer = await docxTemplateX(`./${TEMPLATE_FOLDER_NAME}${include_folder}/${req.body.template}.docx`, req.body, outputMediaType)
|
||||||
template = await fs.promises.readFile(`./${TEMPLATE_FOLDER_NAME}/${req.query["folder"]}/${req.body.template}.docx`)
|
|
||||||
} else {
|
|
||||||
template = await fs.promises.readFile(`./${TEMPLATE_FOLDER_NAME}/${req.body.template}.docx`)
|
|
||||||
}
|
|
||||||
let buffer = await docxTemplateX(template, req.body, outputMediaType)
|
|
||||||
res.statusCode = 201
|
res.statusCode = 201
|
||||||
res.setHeader("Content-Type", req.headers["accept"])
|
res.setHeader("Content-Type", req.headers["accept"])
|
||||||
res.setHeader("Content-Disposition", `attachment;filename=${req.body.reportName}.${outputMediaType}`)
|
res.setHeader("Content-Disposition", `attachment;filename=${req.body.reportName}.${outputMediaType}`)
|
||||||
res.setHeader("Content-Length", buffer.length)
|
res.setHeader("Content-Length", buffer.length)
|
||||||
res.end(buffer)
|
res.end(buffer)
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
res.statusCode = 500
|
if(ex instanceof SyntaxError){
|
||||||
res.statusMessage = "Internal Server Error during get docx template list"
|
res.statusCode = 400
|
||||||
res.end(res.statusMessage)
|
res.statusMessage = ex.message
|
||||||
console.error("Error during apply template: ", ex)
|
res.end(res.statusMessage)
|
||||||
|
console.error("report-template/docx: ", ex)
|
||||||
|
}else{
|
||||||
|
res.statusCode = 500
|
||||||
|
res.statusMessage = "Internal Server Error during POST report-template/docx"
|
||||||
|
res.end(res.statusMessage)
|
||||||
|
console.error("report-template/docx: ", ex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -202,7 +210,7 @@ docxTemplateRoute.post("/", async function (req, res) {
|
||||||
* 201:
|
* 201:
|
||||||
* description: file was converted.
|
* description: file was converted.
|
||||||
* content:
|
* content:
|
||||||
* application/octet-stream:
|
* application/vnd.openxmlformats-officedocument.wordprocessingml.document:
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
* format: binary
|
* format: binary
|
||||||
|
|
@ -210,6 +218,18 @@ docxTemplateRoute.post("/", async function (req, res) {
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
* format: binary
|
* format: binary
|
||||||
|
* application/pdf:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: binary
|
||||||
|
* image/png:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: binary
|
||||||
|
* image/jpeg:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: binary
|
||||||
* 400:
|
* 400:
|
||||||
* description: Invalid format
|
* description: Invalid format
|
||||||
* 500:
|
* 500:
|
||||||
|
|
@ -269,7 +289,7 @@ docxTemplateRoute.post("/upload", async function (req, res) {
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* $ref: '#/components/schemas/templateData'
|
* $ref: '#/components/schemas/templateOption'
|
||||||
* example:
|
* example:
|
||||||
* template: docx-report
|
* template: docx-report
|
||||||
* responses:
|
* responses:
|
||||||
|
|
|
||||||
205
libs/html-templates-lib.ts
Normal file
205
libs/html-templates-lib.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Uint8Array>} output buffer after apply template.
|
||||||
|
*/
|
||||||
|
export async function htmlTemplateX(t: Buffer | String, tdata: templateOption, outputMediaType: string = "pdf"): Promise<Uint8Array> {
|
||||||
|
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();
|
||||||
|
page.setDefaultNavigationTimeout(120000);
|
||||||
|
await page.setViewport({
|
||||||
|
width: width_px,
|
||||||
|
height: 800,
|
||||||
|
deviceScaleFactor: 2,
|
||||||
|
isMobile: false
|
||||||
|
});
|
||||||
|
if (typeof t === 'string') {
|
||||||
|
await page.goto(t, { waitUntil: 'networkidle0' });
|
||||||
|
} 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)
|
||||||
|
|
||||||
|
*/
|
||||||
|
//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
|
||||||
|
//TODO overide option from htmlTemplateOption
|
||||||
|
let pdfOption:PDFOptions = {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
@ -1,8 +1,29 @@
|
||||||
export interface templateData {
|
|
||||||
|
/**
|
||||||
|
* @prop {string} template template ID
|
||||||
|
* @prop {string} reportName outputname
|
||||||
|
* @prop {htmlTemplateOption} htmlOption? support only html-template
|
||||||
|
* @prop {object} data json data for apply template
|
||||||
|
*/
|
||||||
|
export interface templateOption {
|
||||||
template: string
|
template: string
|
||||||
reportName: string
|
reportName: string
|
||||||
|
htmlOption?:htmlTemplateOption
|
||||||
data: object
|
data: object
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @prop {string} querySelector template ID
|
||||||
|
* @prop {object} pdfOption outputname
|
||||||
|
* @prop {number} width support only html-template
|
||||||
|
*/
|
||||||
|
export interface htmlTemplateOption {
|
||||||
|
querySelector: string
|
||||||
|
pdfOption?: object
|
||||||
|
width?:number
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface IDictionary<TValue> {
|
export interface IDictionary<TValue> {
|
||||||
[key: string]: TValue
|
[key: string]: TValue
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +67,7 @@ export function mimeToExtension(mime: string): string {
|
||||||
* @swagger
|
* @swagger
|
||||||
* components:
|
* components:
|
||||||
* schemas:
|
* schemas:
|
||||||
* templateData:
|
* templateOption:
|
||||||
* type: object
|
* type: object
|
||||||
* required:
|
* required:
|
||||||
* - template
|
* - template
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
||||||
import express from "express"
|
import express from "express"
|
||||||
export const xlsxTemplateRoute = express.Router()
|
export const xlsxTemplateRoute = express.Router()
|
||||||
|
|
||||||
import { mimeToExtension, templateData } from "./report-template"
|
import { mimeToExtension, templateOption } from "./report-template"
|
||||||
import { ExcelTemplate } from "xlsx-template-next"
|
import { ExcelTemplate } from "xlsx-template-next"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { LibreOfficeFileConverter } from "libreoffice-file-converter"
|
import { LibreOfficeFileConverter } from "libreoffice-file-converter"
|
||||||
|
|
@ -11,26 +11,36 @@ const TEMPLATE_FOLDER_NAME = "templates/xlsx"
|
||||||
* xlsxTemplate Uses xlsx-template-next to convert input data and template to output buffer.
|
* xlsxTemplate Uses xlsx-template-next to convert input data and template to output buffer.
|
||||||
* You have to handle exception throw by function
|
* You have to handle exception throw by function
|
||||||
* template keep in folder templates
|
* template keep in folder templates
|
||||||
* @param {String} base base path of caller relate to template-docx foler (no trail slash)
|
* @param {Buffer|String} t template in buffer format or path to file
|
||||||
* @param {templateData} tdata Template Information in JSON format
|
* @param {templateOption} tdata Template Information in JSON format
|
||||||
* @param {String} outputMediaType output extension
|
* @param {String} outputMediaType output extension
|
||||||
* @param {Number} tab tab page of spread sheet , default = 1
|
* @param {Number} tab tab page of spread sheet , default = 1
|
||||||
* @return {Promise<Uint8Array>} output buffer after apply template.
|
* @return {Promise<Uint8Array>} output buffer after apply template.
|
||||||
*/
|
*/
|
||||||
export async function xlsxTemplateX(templateBuff: Buffer, tdata: templateData, outputMediaType: string = "xlsx", tab: number = 1): Promise<Uint8Array> {
|
export async function xlsxTemplateX(t: Buffer|String, tdata: templateOption, outputMediaType: string = "xlsx", tab: number = 1): Promise<Uint8Array> {
|
||||||
try {
|
try {
|
||||||
|
const templateBuff = Buffer.isBuffer(t)?t: await fs.promises.readFile(String(t))
|
||||||
const template = new ExcelTemplate()
|
const template = new ExcelTemplate()
|
||||||
// let templateBuff = await fs.promises.readFile(`${base}/${TEMPLATE_FOLDER_NAME}/${tdata.template}.xlsx`)
|
|
||||||
await template.load(templateBuff)
|
await template.load(templateBuff)
|
||||||
await template.process(tab, tdata.data)
|
await template.process(tab, tdata.data)
|
||||||
const buffer = await template.build({ type: "uint8array" })
|
const buffer = await template.build({ type: "uint8array" }) as Uint8Array
|
||||||
if (outputMediaType === "xlsx") return buffer as Uint8Array
|
if (outputMediaType === "xlsx") return buffer
|
||||||
|
|
||||||
const libreOfficeFileConverter = new LibreOfficeFileConverter({
|
const libreOfficeFileConverter = new LibreOfficeFileConverter({
|
||||||
childProcessOptions: {
|
childProcessOptions: {
|
||||||
timeout: 60 * 1000,
|
timeout: 60 * 1000,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const lbuffer = await libreOfficeFileConverter.convertBuffer(Buffer.from(buffer as Uint8Array), outputMediaType)
|
//const lbuffer = await libreOfficeFileConverter.convertBuffer(Buffer.from(buffer as Uint8Array), outputMediaType)
|
||||||
|
|
||||||
|
const lbuffer = await libreOfficeFileConverter.convert({
|
||||||
|
buffer:Buffer.from(buffer),
|
||||||
|
format: outputMediaType,
|
||||||
|
input: "buffer",
|
||||||
|
output: "buffer"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
return lbuffer
|
return lbuffer
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e
|
throw e
|
||||||
|
|
@ -88,7 +98,7 @@ xlsxTemplateRoute.get("/", async function (req, res) {
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* $ref: '#/components/schemas/templateData'
|
* $ref: '#/components/schemas/templateOption'
|
||||||
* example:
|
* example:
|
||||||
* template: hello
|
* template: hello
|
||||||
* reportName: xlsx-report
|
* reportName: xlsx-report
|
||||||
|
|
@ -105,6 +115,10 @@ xlsxTemplateRoute.get("/", async function (req, res) {
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
* format: binary
|
* format: binary
|
||||||
|
* application/vnd.ms-excel:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* format: binary
|
||||||
* application/vnd.oasis.opendocument.spreadsheet:
|
* application/vnd.oasis.opendocument.spreadsheet:
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
|
|
@ -126,8 +140,6 @@ xlsxTemplateRoute.post("/", async function (req, res) {
|
||||||
if (!req.headers["content-type"] || !req.headers["accept"]) throw new Error("Require header content-type, accept")
|
if (!req.headers["content-type"] || !req.headers["accept"]) throw new Error("Require header content-type, accept")
|
||||||
let inputType = mimeToExtension(req.headers["content-type"])
|
let inputType = mimeToExtension(req.headers["content-type"])
|
||||||
let outputMediaType = mimeToExtension(req.headers["accept"])
|
let outputMediaType = mimeToExtension(req.headers["accept"])
|
||||||
console.log("content-type: ", inputType)
|
|
||||||
console.log("accept: ", outputMediaType)
|
|
||||||
let template = null
|
let template = null
|
||||||
// Save the converted file to disk
|
// Save the converted file to disk
|
||||||
if (req.query["folder"]) {
|
if (req.query["folder"]) {
|
||||||
|
|
@ -142,10 +154,17 @@ xlsxTemplateRoute.post("/", async function (req, res) {
|
||||||
res.setHeader("Content-Length", buffer.length)
|
res.setHeader("Content-Length", buffer.length)
|
||||||
res.end(buffer)
|
res.end(buffer)
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
res.statusCode = 500
|
if(ex instanceof SyntaxError){
|
||||||
res.statusMessage = "Internal Server Error"
|
res.statusCode = 400
|
||||||
res.end(res.statusMessage)
|
res.statusMessage = ex.message
|
||||||
console.error("Error during apply template: ", ex)
|
res.end(res.statusMessage)
|
||||||
|
console.error("report-template/xlsx: ", ex)
|
||||||
|
}else{
|
||||||
|
res.statusCode = 500
|
||||||
|
res.statusMessage = "Internal Server Error during POST report-template/xlsx"
|
||||||
|
res.end(res.statusMessage)
|
||||||
|
console.error("report-template/xlsx: ", ex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -246,7 +265,7 @@ xlsxTemplateRoute.post("/upload", async function (req, res) {
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* $ref: '#/components/schemas/templateData'
|
* $ref: '#/components/schemas/templateOption'
|
||||||
* example:
|
* example:
|
||||||
* template: xlsx-report
|
* template: xlsx-report
|
||||||
* responses:
|
* responses:
|
||||||
|
|
|
||||||
2454
package-lock.json
generated
2454
package-lock.json
generated
File diff suppressed because it is too large
Load diff
57
package.json
57
package.json
|
|
@ -3,39 +3,40 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "docx-template มีปัญหากับ bun คาดว่าเป็นปัญหาจาก eval",
|
"description": "docx-template มีปัญหากับ bun คาดว่าเป็นปัญหาจาก eval",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "PORT=3001 nodemon app.ts",
|
"dev": "PORT=3001 nodemon app.ts",
|
||||||
"swaggergen": "ts-node libs/create-swagger-spec.ts ",
|
"swaggergen": "ts-node libs/create-swagger-spec.ts ",
|
||||||
"build": "ts-node libs/create-swagger-spec.ts && tsc && cp libs/swagger-specs.json dist/libs",
|
"build": "ts-node libs/create-swagger-spec.ts && tsc && cp libs/swagger-specs.json dist/libs && npm run obfuscator",
|
||||||
"serve": "PORT=3000 node dist/app.js",
|
"serve": "PORT=3000 node dist/app.js",
|
||||||
"obfuscator": "tsc && javascript-obfuscator ./dist --output ./dist2 && cp libs/swagger-specs.json dist/libs",
|
"obfuscator": "javascript-obfuscator ./dist --output ./dist2 && cp libs/swagger-specs.json dist2/libs",
|
||||||
"preview": "PORT=3000 node dist2/app.js",
|
"preview": "PORT=3000 node dist2/app.js",
|
||||||
"build:docker": "ts-node libs/create-swagger-spec.ts && tsc && javascript-obfuscator ./dist --output ./dist2&& cp libs/swagger-specs.json dist2/libs && docker build -t docker.frappet.com/demo/report-server .",
|
"build:docker": "npm run build && docker build -t docker.frappet.com/demo/report-server .",
|
||||||
"push:docker": "docker push docker.frappet.com/demo/report-server"
|
"push:docker": "npm run build:docker && docker push docker.frappet.com/demo/report-server"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"docx-templates": "^4.13.0",
|
"docx-templates": "^4.13.0",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"libreoffice-file-converter": "^2.3.3",
|
"handlebars": "^4.7.8",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"libreoffice-file-converter": "^3.2.0",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"puppeteer": "^24.2.1",
|
||||||
"xlsx-template-next": "^1.0.3",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"yaqrcode": "^0.2.1"
|
"swagger-ui-express": "^5.0.1",
|
||||||
|
"xlsx-template-next": "^1.0.3",
|
||||||
|
"yaqrcode": "^0.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.14.9",
|
"@types/node": "^20.14.9",
|
||||||
"@types/swagger-jsdoc": "^6.0.4",
|
"@types/swagger-jsdoc": "^6.0.4",
|
||||||
"@types/swagger-ui-express": "^4.1.6",
|
"@types/swagger-ui-express": "^4.1.6",
|
||||||
"javascript-obfuscator": "^4.1.1",
|
"javascript-obfuscator": "^4.1.1",
|
||||||
"nodemon": "^3.1.4",
|
"nodemon": "^3.1.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
BIN
templates/docx/h/hello2.docx
Normal file
BIN
templates/docx/h/hello2.docx
Normal file
Binary file not shown.
|
|
@ -1,9 +1,9 @@
|
||||||
|
|
||||||
// npx ts-node docx-templates.ts
|
// npx ts-node docx-template.ts
|
||||||
import * as readline from 'node:readline/promises'; // This uses the promise-based APIs
|
import * as readline from 'node:readline/promises'; // This uses the promise-based APIs
|
||||||
import { stdin as input, stdout as output } from 'node:process';
|
import { stdin as input, stdout as output } from 'node:process';
|
||||||
import { docxTemplateX} from '../libs/docx-templates-lib';
|
import { docxTemplateX} from '../libs/docx-templates-lib';
|
||||||
import {templateData} from '../libs/report-template'
|
import {templateOption} from '../libs/report-template'
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
(async ()=>{
|
(async ()=>{
|
||||||
const rl = readline.createInterface({ input, output });
|
const rl = readline.createInterface({ input, output });
|
||||||
|
|
@ -12,10 +12,11 @@ import fs from 'fs';
|
||||||
const dpath = await rl.question('JSON data path(./docx.json): ');
|
const dpath = await rl.question('JSON data path(./docx.json): ');
|
||||||
const datapath = dpath?dpath:"./docx.json"
|
const datapath = dpath?dpath:"./docx.json"
|
||||||
const data_raw = fs.readFileSync(datapath);
|
const data_raw = fs.readFileSync(datapath);
|
||||||
const tdata:templateData = JSON.parse(data_raw.toString());
|
const tdata:templateOption = JSON.parse(data_raw.toString());
|
||||||
const bpath = await rl.question('Base path of templates-docx(..): ');
|
const bpath = await rl.question('templates path(../templates/docx): ');
|
||||||
const basepath = bpath?bpath:".."
|
const basepath = bpath?bpath:"../templates/docx"
|
||||||
let buffer = await docxTemplateX(basepath,tdata,ext)
|
//const template = await fs.promises.readFile(`${basepath}/${tdata.template}.docx`)
|
||||||
fs.writeFileSync(tdata.reportName+"."+ext, buffer);
|
let buffer = await docxTemplateX(`${basepath}/${tdata.template}.docx`, tdata,ext)
|
||||||
|
fs.writeFileSync(".output/"+tdata.reportName+"."+ext, buffer);
|
||||||
rl.close();
|
rl.close();
|
||||||
})()
|
})()
|
||||||
|
|
|
||||||
267
test-run/grafana_pdf.js
Normal file
267
test-run/grafana_pdf.js
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const puppeteer = require('puppeteer');
|
||||||
|
//const fetch = require('node-fetch');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
console.log("Script grafana_pdf.js started...");
|
||||||
|
|
||||||
|
/*
|
||||||
|
const url = process.argv[2];
|
||||||
|
const auth_string = process.argv[3];
|
||||||
|
let outfile = process.argv[4];
|
||||||
|
*/
|
||||||
|
const url = 'https://bma-dashboard.frappet.synology.me/d/5EwyjelSk/1408ef66-0081-5b3f-aa00-5e70aa9bdbf1?orgId=1&kiosk=true'
|
||||||
|
const auth_string = 'admin:xxx';
|
||||||
|
let outfile = "./url_gf.pdf";
|
||||||
|
|
||||||
|
const width_px = parseInt(process.env.PDF_WIDTH_PX, 10) || 1200;
|
||||||
|
console.log("PDF width set to:", width_px);
|
||||||
|
|
||||||
|
const auth_header = 'Basic ' + Buffer.from(auth_string).toString('base64');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
console.log("URL provided:", url);
|
||||||
|
console.log("Checking URL accessibility...");
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {'Authorization': auth_header}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Unable to access URL. HTTP status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (!contentType || !contentType.includes('text/html')) {
|
||||||
|
throw new Error("The URL provided is not a valid Grafana instance.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalUrl = url;
|
||||||
|
if(process.env.FORCE_KIOSK_MODE === 'true') {
|
||||||
|
console.log("Checking if kiosk mode is enabled.")
|
||||||
|
if (!finalUrl.includes('&kiosk')) {
|
||||||
|
console.log("Kiosk mode not enabled. Enabling it.")
|
||||||
|
finalUrl += '&kiosk=true';
|
||||||
|
}
|
||||||
|
console.log("Kiosk mode enabled.")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
console.log("Starting browser...");
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||||
|
headless: true,
|
||||||
|
// args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
console.log("Browser started...");
|
||||||
|
|
||||||
|
/*
|
||||||
|
await page.setExtraHTTPHeaders({'Authorization': auth_header});
|
||||||
|
await page.setDefaultNavigationTimeout(process.env.PUPPETEER_NAVIGATION_TIMEOUT || 120000);
|
||||||
|
|
||||||
|
await page.setViewport({
|
||||||
|
width: width_px,
|
||||||
|
height: 800,
|
||||||
|
deviceScaleFactor: 2,
|
||||||
|
isMobile: false
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
console.log("Navigating to URL...");
|
||||||
|
await page.goto(finalUrl, {waitUntil: 'networkidle0'});
|
||||||
|
console.log("Page loaded...");
|
||||||
|
/*
|
||||||
|
await page.evaluate(() => {
|
||||||
|
let infoCorners = document.getElementsByClassName('panel-info-corner');
|
||||||
|
for (let el of infoCorners) {
|
||||||
|
el.hidden = true;
|
||||||
|
}
|
||||||
|
let resizeHandles = document.getElementsByClassName('react-resizable-handle');
|
||||||
|
for (let el of resizeHandles) {
|
||||||
|
el.hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
let dashboardName = 'output_grafana';
|
||||||
|
let date = new Date().toISOString().split('T')[0];
|
||||||
|
let addRandomStr = false;
|
||||||
|
/*
|
||||||
|
if (process.env.EXTRACT_DATE_AND_DASHBOARD_NAME_FROM_HTML_PANEL_ELEMENTS === 'true') {
|
||||||
|
console.log("Extracting dashboard name and date from the HTML page...");
|
||||||
|
let scrapedDashboardName = await page.evaluate(() => {
|
||||||
|
const dashboardElement = document.getElementById('display_actual_dashboard_title');
|
||||||
|
return dashboardElement ? dashboardElement.innerText.trim() : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
let scrapedDate = await page.evaluate(() => {
|
||||||
|
const dateElement = document.getElementById('display_actual_date');
|
||||||
|
return dateElement ? dateElement.innerText.trim() : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
let scrapedPanelName = await page.evaluate(() => {
|
||||||
|
const scrapedPanelName = document.querySelectorAll('h6');
|
||||||
|
if (scrapedPanelName.length > 1) { // Multiple panels detected
|
||||||
|
console.log("Multiple panels detected. Unable to fetch a unique panel name. Using default value.")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (scrapedPanelName[0] && scrapedPanelName[0].innerText.trim() === '') {
|
||||||
|
console.log("Empty panel name detected. Using default value.")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return scrapedPanelName[0] ? scrapedPanelName[0].innerText.trim() : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scrapedPanelName && !scrapedDashboardName) {
|
||||||
|
console.log("Panel name fetched:", scrapedPanelName);
|
||||||
|
dashboardName = scrapedPanelName;
|
||||||
|
addRandomStr = false;
|
||||||
|
} else if (!scrapedDashboardName) {
|
||||||
|
console.log("Dashboard name not found. Using default value.");
|
||||||
|
addRandomStr = true;
|
||||||
|
} else {
|
||||||
|
console.log("Dashboard name fetched:", scrapedDashboardName);
|
||||||
|
dashboardName = scrapedDashboardName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrapedPanelName && !scrapedDate) {
|
||||||
|
const urlParts = new URL(url);
|
||||||
|
const from = urlParts.searchParams.get('from');
|
||||||
|
const to = urlParts.searchParams.get('to');
|
||||||
|
if (from && to) {
|
||||||
|
const fromDate = isNaN(from) ? from.replace(/[^\w\s-]/g, '_') : new Date(parseInt(from)).toISOString().split('T')[0];
|
||||||
|
const toDate = isNaN(to) ? to.replace(/[^\w\s-]/g, '_') : new Date(parseInt(to)).toISOString().split('T')[0];
|
||||||
|
date = `${fromDate}_to_${toDate}`;
|
||||||
|
} else {
|
||||||
|
// using date in URL
|
||||||
|
date = new Date().toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
} else if (!scrapedDate) {
|
||||||
|
console.log("Date not found. Using default value.");
|
||||||
|
} else {
|
||||||
|
console.log("Date fetched:", date);
|
||||||
|
date = scrapedDate;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Extracting dashboard name and date from the URL...");
|
||||||
|
const urlParts = new URL(url);
|
||||||
|
const pathSegments = urlParts.pathname.split('/');
|
||||||
|
dashboardName = pathSegments[pathSegments.length - 1] || dashboardName;
|
||||||
|
const from = urlParts.searchParams.get('from');
|
||||||
|
const to = urlParts.searchParams.get('to');
|
||||||
|
if (from && to) {
|
||||||
|
const fromDate = isNaN(from) ? from.replace(/[^\w\s-]/g, '_') : new Date(parseInt(from)).toISOString().split('T')[0];
|
||||||
|
const toDate = isNaN(to) ? to.replace(/[^\w\s-]/g, '_') : new Date(parseInt(to)).toISOString().split('T')[0];
|
||||||
|
date = `${fromDate}_to_${toDate}`;
|
||||||
|
} else {
|
||||||
|
date = new Date().toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
console.log("Dashboard name fetched from URL:", dashboardName);
|
||||||
|
console.log("Trying to fetch the panel name from the page...")
|
||||||
|
let scrapedPanelName = await page.evaluate(() => {
|
||||||
|
const scrapedPanelName = document.querySelectorAll('h6');
|
||||||
|
console.log(scrapedPanelName)
|
||||||
|
if (scrapedPanelName.length > 1) { // Multiple panels detected
|
||||||
|
console.log("Multiple panels detected. Unable to fetch a unique panel name. Using default value.")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (scrapedPanelName[0] && scrapedPanelName[0].innerText.trim() === '') {
|
||||||
|
console.log("Empty panel name detected. Using default value.")
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return scrapedPanelName[0] ? scrapedPanelName[0].innerText.trim() : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scrapedPanelName) {
|
||||||
|
console.log("Panel name fetched:", scrapedPanelName);
|
||||||
|
dashboardName = scrapedPanelName;
|
||||||
|
addRandomStr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Date fetched from URL:", date);
|
||||||
|
}
|
||||||
|
|
||||||
|
//outfile = `./${dashboardName.replace(/\s+/g, '_')}_${date.replace(/\s+/g, '_')}${addRandomStr ? '_' + Math.random().toString(36).substring(7) : ''}.pdf`;
|
||||||
|
|
||||||
|
const loginPageDetected = await page.evaluate(() => {
|
||||||
|
const resetPasswordButton = document.querySelector('a[href*="reset-email"]');
|
||||||
|
return !!resetPasswordButton;
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loginPageDetected) {
|
||||||
|
throw new Error("Login page detected. Check your credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(process.env.DEBUG_MODE === 'true') {
|
||||||
|
const documentHTML = await page.evaluate(() => {
|
||||||
|
return document.querySelector("*").outerHTML;
|
||||||
|
});
|
||||||
|
if (!fs.existsSync('./debug')) {
|
||||||
|
fs.mkdirSync('./debug');
|
||||||
|
}
|
||||||
|
const filename = `./debug/debug_${dashboardName.replace(/\s+/g, '_')}_${date.replace(/\s+/g, '_')}${'_' + Math.random().toString(36).substring(7)}.html`;
|
||||||
|
fs.writeFileSync(filename, documentHTML);
|
||||||
|
console.log("Debug HTML file saved at:", filename);
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const totalHeight = await page.evaluate(() => {
|
||||||
|
const scrollableSection = document.querySelector('.scrollbar-view');
|
||||||
|
return scrollableSection ? scrollableSection.firstElementChild.scrollHeight : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totalHeight) {
|
||||||
|
throw new Error("Unable to determine the page height. The selector '.scrollbar-view' might be incorrect or missing.");
|
||||||
|
} else {
|
||||||
|
console.log("Page height adjusted to:", totalHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = await page.evaluate(async () => {
|
||||||
|
const scrollableSection = document.querySelector('.scrollbar-view');
|
||||||
|
if (scrollableSection) {
|
||||||
|
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 0
|
||||||
|
});
|
||||||
|
console.log("scrollPosition="+x)
|
||||||
|
|
||||||
|
await page.setViewport({
|
||||||
|
width: width_px,
|
||||||
|
height: totalHeight,
|
||||||
|
deviceScaleFactor: 2,
|
||||||
|
isMobile: false
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Generating PDF...");
|
||||||
|
await page.pdf({
|
||||||
|
path: outfile,
|
||||||
|
width: width_px + 'px',
|
||||||
|
height: totalHeight + 'px',
|
||||||
|
printBackground: true,
|
||||||
|
scale: 1,
|
||||||
|
displayHeaderFooter: false,
|
||||||
|
margin: {top: 0, right: 0, bottom: 0, left: 0}
|
||||||
|
});
|
||||||
|
console.log(`PDF generated: ${outfile}`);
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log("Browser closed.");
|
||||||
|
|
||||||
|
//process.send({ success: true, path: outfile });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during PDF generation:", error.message);
|
||||||
|
//process.send({ success: false, error: error.message });
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
25
test-run/html-template.ts
Normal file
25
test-run/html-template.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// npx ts-node html-templates.ts
|
||||||
|
import * as readline from 'node:readline/promises'; // This uses the promise-based APIs
|
||||||
|
import { stdin as input, stdout as output } from 'node:process';
|
||||||
|
import { htmlTemplateX} from '../libs/html-templates-lib';
|
||||||
|
import {templateOption} from '../libs/report-template'
|
||||||
|
import fs from 'fs';
|
||||||
|
(async ()=>{
|
||||||
|
const rl = readline.createInterface({ input, output });
|
||||||
|
const e = await rl.question('Output extension(pdf,png,jpeg): ');
|
||||||
|
const ext =e?e:"pdf"
|
||||||
|
const dpath = await rl.question('JSON data path(./html.json): ');
|
||||||
|
const datapath = dpath?dpath:"./html.json"
|
||||||
|
const data_raw = fs.readFileSync(datapath);
|
||||||
|
const tdata:templateOption = JSON.parse(data_raw.toString());
|
||||||
|
const bpath = await rl.question('templates path(../templates/html): ');
|
||||||
|
const basepath = bpath?bpath:"../templates/html"
|
||||||
|
// const template = await fs.promises.readFile(`${basepath}/${tdata.template}.docx`)
|
||||||
|
let url = "https://bma-dashboard.frappet.synology.me/d/ANtkJay4z/4Lic4Li54LmJ4Lie4Li04LiB4Liy4Lij?orgId=1&kiosk"
|
||||||
|
//let url = "https://pantip.com"
|
||||||
|
// let url = "https://google.com"
|
||||||
|
|
||||||
|
let buffer = await htmlTemplateX(url, tdata,ext)
|
||||||
|
fs.writeFileSync(".output/"+tdata.reportName+"."+ext, buffer);
|
||||||
|
rl.close();
|
||||||
|
})()
|
||||||
7
test-run/html.json
Normal file
7
test-run/html.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"template": "hello_html",
|
||||||
|
"reportName": "report-html",
|
||||||
|
"htmlOption": {
|
||||||
|
"querySelector": ".scrollbar-view"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
80
test-run/url_play2pdf.ts
Normal file
80
test-run/url_play2pdf.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
// // ใช้เพื่อทดสอบ Plwywright
|
||||||
|
import {chromium} from 'playwright'
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
//const targetUrl = "https://bma-dashboard.frappet.synology.me/d/5EwyjelSk/1408ef66-0081-5b3f-aa00-5e70aa9bdbf1?orgId=1&kiosk=true"
|
||||||
|
//const targetUrl = "https://bma-dashboard.frappet.synology.me/d/OLZwQhPVz/4Liq4Lit4Lia4LmB4LiC4LmI4LiH4LiC4Lix4LiZ?orgId=1&kiosk"
|
||||||
|
const targetUrl = "https://blognone.com"
|
||||||
|
|
||||||
|
const width_px = Number(process.env.PDF_WIDTH_PX) || 800;
|
||||||
|
const browser = await chromium.launch()
|
||||||
|
const page = await browser.newPage()
|
||||||
|
await page.setViewportSize({ width: width_px, height: 800 });
|
||||||
|
await page.goto(targetUrl, { waitUntil: 'networkidle' })
|
||||||
|
|
||||||
|
console.log("document.body.scrollHeight "+ await await page.evaluate(() => document.body.scrollHeight) )
|
||||||
|
let x = await page.evaluate(async () => {
|
||||||
|
const scrollableSection = document.querySelector('.scrollbar-view');
|
||||||
|
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
|
||||||
|
});
|
||||||
|
console.log("scrollPosition=" + x)
|
||||||
|
|
||||||
|
const totalHeight = await page.evaluate(() => {
|
||||||
|
const scrollableSection = document.querySelector('.scrollbar-view');
|
||||||
|
if(scrollableSection&&scrollableSection.firstElementChild){
|
||||||
|
return scrollableSection.firstElementChild.scrollHeight
|
||||||
|
}
|
||||||
|
return document.body.scrollHeight
|
||||||
|
});
|
||||||
|
console.log("totalHeight="+totalHeight)
|
||||||
|
if (!totalHeight) {
|
||||||
|
throw new Error("Unable to determine the page height. The selector '.scrollbar-view' might be incorrect or missing.");
|
||||||
|
} else {
|
||||||
|
console.log("Page height adjusted to:", totalHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
console.log("set viewport ")
|
||||||
|
await page.setViewportSize({
|
||||||
|
width: width_px,
|
||||||
|
height: totalHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
page.emulateMedia({ media: 'print' })
|
||||||
|
|
||||||
|
await page.screenshot({
|
||||||
|
path: 'url_play.png',
|
||||||
|
fullPage: true,
|
||||||
|
//quality: 100,
|
||||||
|
// type:"png"
|
||||||
|
});
|
||||||
|
await page.pdf({
|
||||||
|
path: '.output/url_play.pdf',
|
||||||
|
width: width_px + 'px',
|
||||||
|
height: totalHeight + 'px',
|
||||||
|
printBackground: true,
|
||||||
|
// format: 'A4',
|
||||||
|
displayHeaderFooter: true,
|
||||||
|
// headerTemplate: '<span style="font-size:10px;">SaaS Report Header</span>',
|
||||||
|
// footerTemplate: '<span style="font-size:10px;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>',
|
||||||
|
margin: { top: '20px', bottom: '20px', right: '10px', left: '10px' }
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
})();
|
||||||
|
*/
|
||||||
84
test-run/url_pup2pdf.ts
Normal file
84
test-run/url_pup2pdf.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
// ใช้เพื่อทดสอบ puppeteer
|
||||||
|
import puppeteer from 'puppeteer'
|
||||||
|
(async () => {
|
||||||
|
//const targetUrl = "https://bma-dashboard.frappet.synology.me/d/5EwyjelSk/1408ef66-0081-5b3f-aa00-5e70aa9bdbf1?orgId=1&kiosk=true"
|
||||||
|
//const targetUrl = "https://bma-dashboard.frappet.synology.me/d/OLZwQhPVz/4Liq4Lit4Lia4LmB4LiC4LmI4LiH4LiC4Lix4LiZ?orgId=1&kiosk"
|
||||||
|
const targetUrl = "https://blognone.com"
|
||||||
|
const width_px = Number(process.env.PDF_WIDTH_PX) || 1200;
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
// executablePath: '/usr/bin/chromium',
|
||||||
|
headless: true,
|
||||||
|
//args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
page.setDefaultNavigationTimeout(120000);
|
||||||
|
|
||||||
|
await page.setViewport({
|
||||||
|
width: width_px,
|
||||||
|
height: 800,
|
||||||
|
deviceScaleFactor: 2,
|
||||||
|
isMobile: false
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(targetUrl, { waitUntil: 'networkidle0' });
|
||||||
|
//console.log("Page loaded...");
|
||||||
|
let x = await page.evaluate(async () => {
|
||||||
|
const scrollableSection =
|
||||||
|
document.querySelector('.scrollbar-view')?document.querySelector('.scrollbar-view'):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
|
||||||
|
});
|
||||||
|
console.log("scrollPosition=" + x)
|
||||||
|
|
||||||
|
const totalHeight = await page.evaluate(() => {
|
||||||
|
const scrollableSection = document.querySelector('.scrollbar-view');//only Grafana ?
|
||||||
|
return scrollableSection ? scrollableSection.firstElementChild?.scrollHeight : document.body.scrollHeight;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!totalHeight) {
|
||||||
|
throw new Error("Unable to determine the page height. The selector '.scrollbar-view' might be incorrect or missing.");
|
||||||
|
} else {
|
||||||
|
console.log("Page height adjusted to:", totalHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("set viewport ")
|
||||||
|
await page.setViewport({
|
||||||
|
width: width_px,
|
||||||
|
height: totalHeight,
|
||||||
|
deviceScaleFactor: 2,
|
||||||
|
isMobile: false
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.screenshot({
|
||||||
|
path: 'url_pup.png',
|
||||||
|
fullPage: true,
|
||||||
|
type: 'png' // | 'jpeg' | 'webp'
|
||||||
|
})
|
||||||
|
await page.pdf({
|
||||||
|
path: ".outputurl_prop.pdf",
|
||||||
|
// format:"A4",
|
||||||
|
width: width_px,
|
||||||
|
height: totalHeight,
|
||||||
|
printBackground: true,
|
||||||
|
scale: 1,
|
||||||
|
displayHeaderFooter: false,
|
||||||
|
margin: { top: 0, right: 0, bottom: 0, left: 0 }
|
||||||
|
});
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import * as readline from 'node:readline/promises'; // This uses the promise-based APIs
|
import * as readline from 'node:readline/promises'; // This uses the promise-based APIs
|
||||||
import { stdin as input, stdout as output } from 'node:process';
|
import { stdin as input, stdout as output } from 'node:process';
|
||||||
import {xlsxTemplateX} from '../libs/xlsx-template-lib'
|
import {xlsxTemplateX} from '../libs/xlsx-template-lib'
|
||||||
import {templateData} from '../libs/report-template'
|
import {templateOption} from '../libs/report-template'
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
(async ()=>{
|
(async ()=>{
|
||||||
const rl = readline.createInterface({ input, output });
|
const rl = readline.createInterface({ input, output });
|
||||||
|
|
@ -12,10 +12,11 @@ import fs from 'fs';
|
||||||
const dpath = await rl.question('JSON data path(./xlsx.json): ');
|
const dpath = await rl.question('JSON data path(./xlsx.json): ');
|
||||||
const datapath = dpath?dpath:"./xlsx.json"
|
const datapath = dpath?dpath:"./xlsx.json"
|
||||||
const data_raw = fs.readFileSync(datapath);
|
const data_raw = fs.readFileSync(datapath);
|
||||||
const tdata:templateData = JSON.parse(data_raw.toString());
|
const tdata:templateOption = JSON.parse(data_raw.toString());
|
||||||
const bpath = await rl.question('Base path of templates-docx(..): ');
|
const bpath = await rl.question('template path(../templates/xlsx): ');
|
||||||
const basepath = bpath?bpath:".."
|
const basepath = bpath?bpath:"../templates/xlsx"
|
||||||
let buffer = await xlsxTemplateX(basepath,tdata,ext)
|
// const template = await fs.promises.readFile(`${basepath}/${tdata.template}.xlsx`)
|
||||||
|
let buffer = await xlsxTemplateX(`${basepath}/${tdata.template}.xlsx`,tdata,ext)
|
||||||
fs.writeFileSync(tdata.reportName+"."+ext, buffer);
|
fs.writeFileSync(tdata.reportName+"."+ext, buffer);
|
||||||
rl.close();
|
rl.close();
|
||||||
})()
|
})()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"target": "esnext",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"rootDir": "./",
|
"rootDir": "./",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue