React 및 Puppeteer: PDF 생성(pdf 생성 API)

27418 단어 nxpdfpuppeteerreact

PDF 생성



목표:
  • 서버측 pdf 생성
  • 호출자에게 PDF 파일 반환

  • 1 단계



    일부 도우미를 만들 수 있습니다.

    글꼴을 등록하고 pdf 본문을 보간하기 위해 전체 html 문서를 렌더링하려면 도우미가 필요합니다.

    // file: apps/pdf-server/src/pdf/helpers/render-pdf-document.ts
    
    import { Font, PDFData } from '@pdf-generation/constants';
    
    const renderFontFaces = (font: Font) => {
      return (
        font?.fontFaces?.map((fontFace) => {
          const family = font.family ? `font-family: ${font.family};` : '';
          const style = fontFace.style ? `font-style: ${fontFace.style};` : '';
          const weight = fontFace.weight ? `font-weight: ${fontFace.weight};` : '';
          const format = fontFace.format ? `format(${fontFace.format})` : '';
    
          return `
            @font-face {
              ${family}
              ${style}
              ${weight}
              src: url(${fontFace.src}) ${format};
    
            }
          `;
        }) || ''
      );
    };
    
    export const renderPdfDocument = (data: PDFData, body: string) => {
      const font = renderFontFaces(data.font);
    
      return `
        <html>
          <head>
            <meta charset="utf-8" />
            <title>${data.metadata.title}</title>
            <meta name="viewport" content="width=device-width, initial-scale=1" />
    
            <style>
                ${font}
                body {
                    -webkit-print-color-adjust: exact !important;
                    color-adjust: exact !important;
                    print-color-adjust: exact !important;
                }
    
            </style>
          </head>
          <body>
            ${body}
          </body>
        </html>
        `;
    };
    


    이제 PDF 파일의 메타데이터를 설정하는 도우미가 필요합니다. 불행하게도 Puppeteer는 필요할 수 있는 메타데이터를 지정할 방법이 없습니다. 따라서 아래에서 pdf-lib를 사용하여 이 메타데이터를 설정하고 추가 사용을 위해 Buffer를 반환합니다.

    import { PDFDocument } from 'pdf-lib';
    
    const metadataSetters = {
      title: 'setTitle',
      author: 'setAuthor',
      creator: 'setCreator',
      producer: 'setProducer',
      subject: 'setSubject',
    };
    
    export async function setPdfMetadata(pdf: Buffer, metadata) {
      const setterEntries = Object.entries(metadataSetters);
    
      if (!setterEntries.some(([key]) => !!metadata[key])) return pdf;
    
      const document = await PDFDocument.load(pdf);
    
      setterEntries.forEach(([key, setter]) => {
        const value = metadata[key];
        if (value) document[setter](value);
      });
    
      const pdfBytes = await document.save();
    
      return Buffer.from(pdfBytes);
    }
    


    사용하려는 글꼴을 정의할 수도 있습니다.

    export const DEFAULT_PDF_FONT = {
      family: 'Open Sans',
      familyDisplay: "'Open Sans', sans-serif",
      fontFaces: [
        {
          src: 'https://fonts.gstatic.com/s/opensans/v18/mem5YaGs126MiZpBA-UN_r8-VeJoCqeDjg.ttf',
          weight: 300,
        },
        {
          src: 'https://fonts.gstatic.com/s/opensans/v18/mem8YaGs126MiZpBA-U1UpcaXcl0Aw.ttf',
        },
        {
          src: 'https://fonts.gstatic.com/s/opensans/v18/mem6YaGs126MiZpBA-UFUJ0ef8xkA76a.ttf',
          style: 'italic',
        },
        {
          src: 'https://fonts.gstatic.com/s/opensans/v18/mem5YaGs126MiZpBA-UNirk-VeJoCqeDjg.ttf',
          weight: 600,
        },
        {
          src: 'https://fonts.gstatic.com/s/opensans/v18/memnYaGs126MiZpBA-UFUKXGUehsKKKTjrPW.ttf',
          weight: 600,
          style: 'italic',
        },
      ],
    };
    


    2 단계



    작업을 수행할 서비스를 만들 수 있습니다.

    // file: apps/pdf-server/src/pdf/pdf.service.ts
    
    import { Injectable } from '@nestjs/common';
    import * as puppeteer from 'puppeteer';
    import { writeFile } from 'fs/promises';
    
    import { PDFDocumentData } from '@pdf-generation/constants';
    import { PdfDocProps, renderPdfDoc } from '@pdf-generation/pdf-doc';
    import { environment } from '../environments/environment';
    import { renderPdfDocument, setPdfMetadata, DEFAULT_PDF_FONT } from './helpers';
    
    @Injectable()
    export class PdfService {
      async generatePdf(data: PDFDocumentData<PdfDocProps>) {
        const browser = await puppeteer.launch({
          /**
           * Adding `--no-sandbox` here is for easier deployment. Please see puppeteer docs about
           * this argument and why you may not want to use it.
           */
          args: [`--no-sandbox`],
        });
    
        const pdfData = {
          ...data,
          font: data.font ?? DEFAULT_PDF_FONT,
        };
    
        const documentBody = renderPdfDoc(pdfData.document, pdfData.font);
        const content = renderPdfDocument(pdfData, documentBody);
        const pdfOptions = pdfData?.options || {};
    
        const page = await browser.newPage();
        await page.setContent(content, {
          waitUntil: 'networkidle2',
        });
        const pdf = await page.pdf({
          format: 'a4',
          printBackground: true,
          preferCSSPageSize: true,
          ...pdfOptions,
          margin: {
            left: '40px',
            right: '40px',
            top: '40px',
            bottom: '40px',
            ...(pdfOptions?.margin || {}),
          },
        });
    
        // Give the buffer to pdf-lib
        const result = await setPdfMetadata(pdf, pdfData?.metadata);
    
    // write to disk for debugging
        if (!environment.production) {
          const now = Date.now();
          const filePath = `${environment.tmpFolder}/${now}-tmp-file.pdf`;
          await page.screenshot({
            fullPage: true,
            path: `${environment.tmpFolder}/${now}-tmp-file.png`,
          });
          await writeFile(filePath, result);
        }
    
        await browser.close();
    
        return result;
      }
    }
    
    


    지금 여기서 무슨 일이 일어나고 있나요?
  • 헤드리스 크롬 브라우저를 여는 중입니다
  • .
  • 생성한 pdf의 내용을 렌더링합니다. renderPdfDoc
  • 전체 html 문서를 renderPdfDocument로 렌더링합니다.
  • 가능한 경우 메타데이터를 setPdfMetadata로 설정합니다
  • .
  • 브라우저를 닫은 다음 Buffer를 반환합니다.

  • 3단계



    이제 Controller를 업데이트하여 액세스할 수 있도록 서비스를 연결해야 합니다.

    // file: apps/pdf-server/src/pdf/pdf.controller.ts
    
    import { Body, Controller, Post } from '@nestjs/common';
    import { PdfService } from './pdf.service';
    
    @Controller('pdf')
    export class PdfController {
      constructor(private service: PdfService) {}
    
      @Post('/generate')
      async generatePdf(@Body() data) {
        return this.service.generatePdf(data);
      }
    }
    


    4단계



    이제 마지막으로 PDF 모듈을 앱에 연결합니다.
    apps/pdf-server/src/app/app.module.ts에서
    PdfModule 배열에 imports 추가

    @Module({
      imports: [PdfModule], // <== here
      controllers: [AppController],
      providers: [AppService],
    })
    


    그게 다야!

    이제 API를 시작하여 테스트할 수 있습니다.

    pnpm nx run pdf-server:serve
    



    curl --location --request POST 'localhost:3333/api/pdf/generate' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "id": "sample",
        "metadata": {
            "title": "Some random pdf",
            "subject": "Some radom pdf",
            "author": "Statale",
            "producer": "pdf-generation-server",
            "creator": "My awesome creator"
        },
        "document": {
            "images": [
                {
                    "id": 0,
                    "url": "https://source.unsplash.com/category/technology/1600x900"
                },
                {
                    "id": 1,
                    "url": "https://source.unsplash.com/category/current-events/1600x900"
                }
            ],
            "title": "Some random pdf",
            "description": "<p>Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat,...</p>"
        }
    }'
    

    좋은 웹페이지 즐겨찾기