Angular and PDF Part 3: Server-side rendering of PDF

1. Angular PDf server-side rendering

1. Environment preparation

    _                     _                 ____ _     ___
  / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
  / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |   | |
/ ___ \| | | | (_| | |_| | | (_| | |     | |___| |___ | |
/_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
              |___/
Angular CLI: 16.0.0
Node: 16.14.0
Package Manager: npm 8.3.1
OS: darwin arm64
Angular: 
Package                     Version
------------------------------------------------------
@angular-devkit/architect    0.1600.0 (cli-only)
@angular-devkit/core         16.0.0 (cli-only)
@angular-devkit/schematics   16.0.0 (cli-only)
@schematics/angular          16.0.0 (cli-only)


"express": "^4.18.2",
"handlebars": "^4.7.7",
"puppeteer": "^20.1.2",

2. Create a new angular project and start an express service under the project for data acquisition and PDF rendering and export

1. Create a new project

ng new server-pdf

2. Modify app.component.html and app.component.ts

Add a button everywhere to export the list that is currently retrieved from the backend

app.component.html
<div>
   <h1>angular PDF server 渲染</h1>
   <button (click)="downloadPdf()"> Export to PDF</button>
   <div class="line">

   </div>
   <table id="list">
       <thead>
           <tr>
               <th *ngFor="let item of titleList">{
    
    {item}}</th>
           </tr>
       </thead>
       <tbody>
           <tr *ngFor="let item of data">
               <td *ngFor="let item of data; let i = index">{
    
    { getcloumName(i, item) }}</td>
           </tr>
       </tbody>
   </table>
</div>
app.component.ts

When init , get data from the backend and render it to the page

  import { Component, OnInit } from '@angular/core';
  import { HttpClient } from '@angular/common/http';
  @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.less']
  })
  export class AppComponent implements OnInit {
      data: {
          name: string;
          age: string;
          gender: string;
      }[] = []
      constructor(private http: HttpClient) { }
      ngOnInit() {
          this.getData();
      }
      getData(): void {
          this.http.get('/api/data').subscribe((data: any) => {
              this.data = data.users;
          });
      }
      downloadPdf(): void {
          this.http.get('/api/pdf', {
            responseType: 'blob'
          }).subscribe((pdfBlob: Blob) => {
            const pdfUrl = URL.createObjectURL(pdfBlob);
            const downloadLink = document.createElement('a');
            downloadLink.href = pdfUrl;
            downloadLink.download = 'angular-pdf.pdf';
            downloadLink.click();
          });
        }
  }

3. Create a server folder in the root directory of the project

Create template.hbs template

Create HTML elements to be converted to PDF

<div>
   <h1>angular PDF server 渲染</h1>
   <div class="line">
   </div>
   <table id="list">
       <thead>
           <tr>
              {
    
    {#each cloumKey}}
               <th>{
    
    {this}}</th>
              {
    
    {/each}}
           </tr>
       </thead>
       <tbody>
          {
    
    {#each users}}
           <tr>
              {
    
    {#each this}}
               <td>{
    
    {this}} {
    
    {[`cloum--${@index}`]}}</td>
              {
    
    {/each}}
           </tr>
          {
    
    {/each}}
       </tbody>
   </table>
</div>

Create server.js

When the front end is requesting the /api/pdf interface, the back end gets the data rendered by the current page, uses the template defined by handlebars to convert the data into HTML format, and then uses puppeteer to create a headless browser, create a blank page, and Put the HTML created by handlebars into the browser and convert it to PDF and transmit it to the front end in Buffer format. After the transmission is complete, close the virtual browser

Import handlebars express fs and created handlebars template

const express = require('express');
const handlebars = require('handlebars');
const fs = require('fs');
const templatePath = './server/template.hbs';
// 创建一个Express应用程序
const app = express();
const port = 3000;
// 创建一个 50 * 50 的 table 表格数据
const json = [];
for (var i = 0; i < 50; i++) {
 var row = {};
 for (var j = 0; j < 50; j++) {
   var cellKey = 'cloum--' + j;  // 列的属性名
   var cellValue = 'Cell ' + i + '-' + j;  // 列的值
   row[cellKey] = cellValue;
}
 json.push(row);
}
const data = {
   users: json
}
// 设置路由处理程序
app.get('/api/data', (req, res) => {
   res.json(data);
});
app.get('/api/pdf', async (req, res) => {
   // 读取模板文件
   let html;
   fs.readFile(templatePath, 'utf8', (err, template) => {
       if (err) {
           console.error('Error reading template:', err);
           // 处理错误
           return;
      }
       // 编译模板
       const compiledTemplate = handlebars.compile(template);
       // 应用数据到模板
       html = compiledTemplate(data);
  });
   const puppeteer = require('puppeteer');
   // 创建一个无头浏览器
   const browser = await puppeteer.launch();
   // 创建一个新页面
   const page = await browser.newPage();
   // 将 handlebars 生成的 html 放入浏览器中
   await page.setContent(html);
   // 将当前页面 转化成 PDF buffer
   const pdfBuffer = await page.pdf({ format: 'A4' });
   res.setHeader('Content-Type', 'application/pdf');
   res.setHeader('Content-Disposition', 'attachment; filename="example.pdf"');
   // 发送给前端
   res.send(pdfBuffer);
   // 关闭浏览器实例
   await browser.close();
});
// 启动服务器
app.listen(port, () => {
   console.log(`Server listening on port ${port}`);
});

​​​​​​​​Create proxy.conf.json to forward all front-end requests to server.js

{
   "/api/*": {
   "target": "http://localhost:3000",
   "secure": false,
   "logLevel": "debug"
  }
}

Modify the front-end startup command in package.json and add the server startup command

   "start": "ng serve --proxy-config proxy.conf.json",
   "server": "node ./server/server.js"

The above is the complete process of angular PDF server rendering

2. How to paginate when there are too many rows

Here is a 5*100 table form , which stipulates that only 20 pieces of data can be displayed on each page and processed by paging

1. Cut the 100 * 100 table data into five 100 * 20 data

  function cutData(data) {
        const pageSize = 20;
        const listData = [];
        const len = data.length;
        const count = Math.ceil(len / pageSize);
        const cloumKey = [];
        for (let i = 0; i < count; i++) {
          const start = i * pageSize;
          const end = start + pageSize;
          const arr = data.slice(start, end);
          cloumKey.push(Object.keys(arr[0]))
          listData.push(arr);
        }
        return { listData, cloumKey };
  }

​​​​​​​​2. Modify the get('/api/pdf') method

Use the cutData method to divide the original 100 * 100 data into five 100 * 20 , and then put the loop-generated data into the template of handlebars in turn to generate the html of each page, get the html and use puppeteer to convert it into PDF and save it Create a data locally and save the currently created PDF name for subsequent PDF merging . After generating all the paged PDFs , use pdf-lib to merge the PDFs just generated into one

app.get('/api/pdf', async (req, res) => {
   // 读取模板文件
   let html;
   const puppeteer = require('puppeteer');
   const browser = await puppeteer.launch();
   const page = await browser.newPage();
   const pdfFiles=[];
   const tableData = cutData(data.users);
   console.log('%c [ tableData ]-39', 'font-size:13px; background:pink; color:#bf2c9f;', tableData)
   for (let i = 0; i < tableData.cloumKey.length + 1; i++) {
     fs.readFile(templatePath, 'utf8', (err, template) => {
         if (err) {
             console.error('Error reading template:', err);
             // 处理错误
             return;
        }
         // 编译模板
         const compiledTemplate = handlebars.compile(template);
         // 应用数据到模板
         const pdfData = {
           users: tableData.listData[i],
           cloumKey: tableData.cloumKey[i]
        }
         html = compiledTemplate(pdfData);
    });
     await page.setContent(html);
     var pdfFileName =  'sample'+(i)+'.pdf';
     await page.pdf({path: __dirname + pdfFileName,format: 'A4' });
     pdfFiles.push(pdfFileName);
  }
   res.setHeader('Content-Type', 'application/pdf');
   res.setHeader('Content-Disposition', 'attachment; filename="example.pdf"');
   // 关闭浏览器实例
   await browser.close();
   const pdfBytes = await mergePDF(pdfFiles);
   res.send(pdfBytes);

});


const mergePDF = async (sourceFiles) => {
 const pdfDoc = await PDFDocument.create()
 for(let i = 0;i<sourceFiles.length;i++) {
   const localPath = __dirname + sourceFiles[i]
   const PDFItem = await PDFDocument.load(fs.readFileSync(localPath))
   for(let j = 0;j<PDFItem.getPageCount();j++) {
     const [PDFPageItem] = await pdfDoc.copyPages(PDFItem, [j])
     pdfDoc.addPage(PDFPageItem)
  }
}
 const pdfBytes = await pdfDoc.save()
 fs.writeFileSync('merge.pdf', pdfBytes)
 return pdfBytes;
}

front page

PDF export page

3. How to split the data when there are too many columns

A total of 20 columns, cut each ten columns into an array, and use the above get('/api/pdf') method for rendering and transmission

function cutData(data) {
     const pageSize = 10;
     const listData = [];
     const columKey = Object.keys(data[0]);
     const len = columKey.length;
     const count = Math.ceil(len / pageSize);
     const cloumKey = [];
     for (let i = 0; i < count; i++) {
       const start = i * pageSize;
       const end = start + pageSize;
       const arr = data.slice(start, end);
       for (let j = 0; j < arr.length; j++) {
         arr[j] = _.pick(arr[j], columKey.slice(start, end));
      }
       cloumKey.push(columKey.slice(start, end));
       listData.push(arr);
    }
     return { listData, cloumKey };
  }

front page

PDF export page

Guess you like

Origin blog.csdn.net/KenkoTech/article/details/130683829
pdf