POC:

Generovanie PDF z XML

Milan Herda 04/2023

Problém

  • stal som sa vlastníkom technológie "PDF server"
  • bolo mi povedané, že treba vymyslieť lepšie riešenie

Problém

  • viacero služieb potrebuje generovať PDF
  • aplikácia chce vygenerovať PDF, ale nechce si to programovať sama
  • rôzne aplikácie chcú využívať rovnakú šablónu

Pokus č. 1

"PDF server"

Samostatná REST služba naprogramovaná v JS s využitím knižnice pdfmake

Pokus č. 1

"PDF server"

Výhody:

  • jednotný výzor naprieč aplikáciami
  • aplikácia potrebuje vedieť iba názov šablóny a dáta, ktoré chce vložiť

Nevýhody:

  • aplikácia nemá v rukách šablónu
  • kvôli zmene šablóny treba upraviť a nasadiť PDF server
  • PDF server vygeneruje iba taký typ dokumentu, k akému má šablónu
  • okrem dát musíme posielať aj preklady a číselníky
  • ❌ silné previazanie na CV

Možnosti

Bez serveru

Každá aplikácia si PDF generuje po svojom. PDF generátor je poskytovaný ako balíček. Šablóny môžu byť v balíčku tiež.

1.

2.

Server bez šablón

Server neobsahuje žiadne šablóny a klient mu posiela dáta a aj definíciu šablóny.

3.

Server so šablónami

Klient pošle na PDF server názov šablóny a dáta, ktoré chce do nej vložiť.

Ako generovať PDF a mať to štandardizované alebo aspoň čiastočne centralizované?

Možnosť č. 1

Bez serveru

Výhody:

  • aplikácia používa pripravený generátor
  • aplikácia môže použiť pripravenú alebo aj vlastnú šablónu

Nevýhody:

  • aplikácia musí byť v rovnakom jazyku, ako generátor
  • treba vymyslieť, ako definovať šablónu v rôznych jazykoch
  • každá aplikácia si musí vymýšľať vlastnú architektúru

Možnosť č. 2

Server bez šablón

Výhody:

  • aplikácia má šablónu vo svojej správe
  • zmena šablóny neovplyvňuje PDF server
  • PDF server vygeneruje hocijaký typ dokumentu

Nevýhody:

  • ťažšie dosiahnuť skutočne jednotný výzor naprieč aplikáciami (požiadavka rovnakých šablón)
  • treba vymyslieť, ako definovať šablónu v rôznych jazykoch

Možnosť č. 3

Server so šablónami

Výhody:

  • jednotný výzor naprieč aplikáciami
  • aplikácia potrebuje vedieť ina názov šablóny a dáta, ktoré chce vložiť

Nevýhody:

  • aplikácia nemá v rukách šablónu
  • kvôli zmene šablóny treba upraviť a nasadiť PDF server
  • ❌ PDF server vygeneruje iba taký typ dokumentu, k akému má šablónu

Problém

  • viacero služieb potrebuje generovať PDF
  • aplikácia chce vygenerovať PDF, ale nechce si to programovať sama
  • rôzne aplikácie chcú využívať rovnakú šablónu

Problém

  • viacero služieb potrebuje generovať PDF
  • aplikácia chce vygenerovať PDF, ale nechce si to programovať sama
  • rôzne aplikácie chcú využívať rovnakú šablónu

Naozaj?

Možnosti

Bez serveru

Každá aplikácia si PDF generuje po svojom. PDF generátor je poskytovaný ako balíček. Šablóny môžu byť v balíčku tiež.

1.

2.

Server bez šablón

Server neobsahuje žiadne šablóny a klient mu posiela dáta a aj definíciu šablóny.

3.

Server so šablónami

Klient pošle na PDF server názov šablóny a dáta, ktoré chce do nej vložiť.

Ako generovať PDF a mať to štandardizované alebo aspoň čiastočne centralizované?

🏆

🏆

Proof of Concept

Generovanie PDF na základe XML

Proof of Concept

Generovanie PDF na základe šablóny zapísanej pomocou XML

  • všetci poznajú
  • ľahko sa generuje v každom jazyku
  • extra jednoducho sa generuje v PHP pomocou twig-u
  • ľahké mapovanie na príkazy knižnice pdfmake

Prečo XML?

Ako môže vyzerať XML

<?xml version='1.0' standalone='yes'?>
<document>
    <images>
        <image-url
            name="logo"
            src="https://www.monkeyuser.com/assets/images/2017/62-design-patterns-bureaucracy.png"
        />
        <image-url
            name="cover"
            src="https://www.monkeyuser.com/assets/images/2020/177-devs-journey.png"
        />
    </images>
    <content>
        <text fontSize="18" color="grey">Hello World!</text>
        <image name="cover" width="250" />

        <text pageBreak="before" fontSize="16" color="blue">Table Example</text>

        <table>
            <tr>
                <td>Bunka 1</td>
                <td>Bunka 2</td>
            </tr>
            <tr>
                <td>Bunka 3</td>
                <td><image name="logo" fit="[500, 500]"/></td>
            </tr>
        </table>
    </content>
</document>

Použitie 1: Konverzia v konzole

yarn ts-node generate-from-file.ts --xml=test.xml --pdf=/tmp/a.pdf

Skript generate-from-file.ts

function readXmlFile(xmlFile: string) {
    const buffer = fs.readFileSync(xmlFile);

    return buffer.toString();
}

async function main() {
    const { xmlFile, pdfFile } = parseCommandLineArguments();

    const xml = readXmlFile(xmlFile);

    const pdfCommands = await createPdfFromXmlString(xml);

    printPdf(pdfCommands, pdfFile);
}

main();

Použitie 2: Konverzia cez REST API

yarn server
XML="
<?xml version='1.0' standalone='yes'?>
<document>
    <content>
        <text fontSize='18' color='blue'>Hello World</text>
        <qr>Have a nice day!</qr>
    </content>
</document>
"

XML=`echo $XML | jq -Rsa`

curl -X POST http://127.0.0.1:8080/pdf/create/moj-zivotopis.pdf \
   -H 'Content-Type: application/json' \
   -d "{\"xml\": $XML}"

Kód skriptu server.ts

import { pipePdfToResponse } from './js/server';

app.post('/pdf/create/:filename', async (req, res) => {
    res.status(200);

    try {
        const fonts = {
            Roboto: {
                normal: './node_modules/@fontsource/roboto/files/roboto-all-700-normal.woff',
                bold: './node_modules/@fontsource/roboto/files/roboto-all-900-normal.woff',
                italics: './node_modules/@fontsource/roboto/files/roboto-all-700-italic.woff',
                bolditalics: './node_modules/@fontsource/roboto/files/roboto-all-900-italic.woff',
            },
        };
      
        pipePdfToResponse(
            req.body.xml,
            fonts,
            res,
            req.params.filename
        );
    } catch (e: any) {
        // ...
    }
});

Použitie 3: Konverzia na webe

yarn dev

Kód pre web

import showWithPdfjs from './js/browser';

const xml = `...`;

const fonts = {
    Roboto: {
        normal: `${document.location.protocol}//${document.location.host}/node_modules/@fontsource/roboto/files/roboto-all-700-normal.woff`,
        bold: `${document.location.protocol}//${document.location.host}/node_modules/@fontsource/roboto/files/roboto-all-900-normal.woff`,
        italics: `${document.location.protocol}//${document.location.host}/node_modules/@fontsource/roboto/files/roboto-all-700-italic.woff`,
        bolditalics: `${document.location.protocol}//${document.location.host}/node_modules/@fontsource/roboto/files/roboto-all-900-italic.woff`,
    },
};

async function main() {
    await showWithPdfjs(
        xml,
        <HTMLCanvasElement>document.getElementById('canvas'),
        fonts,
        './node_modules/pdfjs-dist/build/pdf.worker.js',
        1
    );
}

main();
  • API pre jednotlivé použitia ešte nie je ustálená

Disclaimer

  • použitie ako RESTový mikroservice (server bez šablón)
  • použitie ako konzolový skript (bez serveru)
  • použitie ako knižnica vo webstránke (bez serveru)

Videli sme

Čo všetko vieme do PDF vložiť?

Text

<?xml version='1.0' standalone='yes'?>
<document>
    <content>
        Hello World
        <text bold='true' fontSize='18' color='blue'>Farebný Hello World</text>
        <text
            decoration='underline'
            decorationStyle='wavy'
            decorationColor='green'
        >
            Zelená vlnovka
        </text>
    </content>
</document>
# PRESENTING CODE

Stĺpce a stacky

<?xml version='1.0' standalone='yes'?>
<document>
    <content>
        <columns>
            <stack>
                <text>Prvý riadok</text>
                <text>Druhý riadok</text>
                <text>Tretí riadok</text>
            </stack>
            <text
                width='100'
                bold='true' 
                fontSize='18'
                color='blue'
            >Farebný Hello World</text>
            <text
                width='50'
                decoration='underline'
                decorationStyle='wavy'
                decorationColor='green'
             >Zelená vlnovka</text>
        </columns>
    </content>
</document>
# PRESENTING CODE

Obrázky

<?xml version='1.0' standalone='yes'?>
<document>
    <images>
        <image-url name='logo' src='https://www.monkeyuser.com/assets/images/2017/62-design-patterns-bureaucracy.png' />
        <image-url name='cover' src='https://www.monkeyuser.com/assets/images/2020/177-devs-journey.png' />
    </images>
    <content>
        <image name='logo' width='250' />
        <image name='cover' fit='[200, 200]' />
    </content>
</document>
# PRESENTING CODE

SVG obrázky

<?xml version='1.0' standalone='yes'?>
<document>
    <content>
        <svg-wrapper width='15'>
            <svg
                xmlns='http://www.w3.org/2000/svg'
                width='30'
                height='30'
                viewBox='0 0 30 30'
            >
                <g transform='translate(-517 -1486)'><g transform='translate(517 1486)'><circle cx='15' cy='15' r='15' fill='#f7af26'/></g><g transform='translate(524 1493)'><path d='M14,1H2A2,2,0,0,0,0,3v.4L8,7.9l8-4.4V3A2,2,0,0,0,14,1Z' fill='#fff'/><path d='M7.5,9.9,0,5.7V13a2,2,0,0,0,2,2H14a2,2,0,0,0,2-2V5.7L8.5,9.9A1.243,1.243,0,0,1,7.5,9.9Z' fill='#fff'/></g></g>
            </svg>
        </svg-wrapper>
    </content>
</document>
# PRESENTING CODE

Zoznamy

<?xml version='1.0' standalone='yes'?>
<document>
    <content>
        <ul type='disc'>
            <text>Prvá položka</text>
            <text>Druhá položka</text>
            <ul type='circle'>
                <text>Podpoložka 1</text>
                <text>Podpoložka 2</text>
            </ul>
        </ul>

        <ol type='lower-alpha' start='5'>
            <text>Prvá položka</text>
            <text>Druhá položka</text>
            <text>Tretia položka</text>
        </ol>
    </content>
</document>
# PRESENTING CODE

Tabuľky

<?xml version='1.0' standalone='yes'?>
<document>
    <content>
       <table widths='["30%", "*"]'>
           <tr>
               <td borderColor='["red", "green", "blue", "violet"]'>Bunka 1</td>
               <td fillColor='gold'>Bunka 2</td>
           </tr>
           <tr>
               <td colspan='2'>Bunka 3</td>
               <td></td>
           </tr>
           <tr>
               <td rolspan='2'>Bunka 5</td>
               <td>Bunka 6</td>
           </tr>
           <tr>
               <td></td>
               <td>Bunka 8</td>
           </tr>
       </table>
    </content>
</document>
# PRESENTING CODE

QR kódy

<?xml version='1.0' standalone='yes'?>
<document>
    <content>
        <qr>Hello World!</qr>
        <qr foreground='red' background='gold'>Hello World!</qr>
        <qr fit='50'>Hello World!</qr>
    </content>
</document>
# PRESENTING CODE

Tabuľka obsahu

<?xml version='1.0' standalone='yes'?>
<document>
    <content>
        <toc id='main'>Obsah</toc>

        <text marginTop='50' tocItem='main' bold='true'>Nadpis 1</text>
        <text>Lorem Ipsum...</text>

        <text tocItem='main' bold='true'>Nadpis 2</text>
        <text>Lorem Ipsum...</text>

        <text tocItem='main' tocMargin='[20, 0, 0, 0]' bold='true'>Podnadpis 2.2</text>
        <text>Lorem Ipsum...</text>

    </content>
</document>
# PRESENTING CODE

Vieme toho viac

  • upratovanie kódu a písanie testov
  • vyriešenie problému s fontami
  • NPM balíček
  • prenos existujúcej CV šablóny do twigu?

Ďalšie kroky

Ďakujem za pozornosť

POC: generovanie PDF z XML

By Milan Herda

POC: generovanie PDF z XML

Výsledok proof-of-concept projektu, v ktorom bol cieľ zistiť, či vieme na základe XML šablóny generovať ľubovoľné PDF

  • 172