QR kódy v PDF

23.10.2022 | 19:05 | Mirecove dristy | Miroslav Bendík

Cez víkendy robievam rôzne mini projekty. Jedným z nich je môj malý generátor QR kódov použiteľný či už v Reportlab dokumentoch, alebo aj samostatne ako nástroj pre príkazový riadok.

QR kódy boli navrhnuté japonskou automobilovou spoločnosťou Denso. Neprekvapí preto, že práve Japonsko bolo prvou krajinou, kde sa začali objavovať aj na verejných priestranstvách. Dnes už ikonické QR kódy ľahko rozoznateľné podľa troch veľkých štvorcov nájdeme bežne už aj na Slovnesku. Stretneme sa s nimi na verejných priestranstvách v podobe URL odkazov, v rôznych tlačovinách, alebo faktúrach. Práve kvôli poslednému spomenutému spôsobu použitia som sa rozhodol implementovať vlastnú knižnicu.

Inštalácia

Namiesto systémovej inštalácie budem používať lokálnu do python virtual environmentu. Ak by niekto chcel inštaláciu pre celý systém, postačí spustiť príkaz pip pod rootom bez aktivovaného virtualenvironmentu.

python3 -m venv virtualenv
. ./virtualenv/bin/activate
pip install reportlab_qr_code_generator z3c.rml

Zdrojové kódy a dokumentácia

Zdrojové kódy a dokumentáciu mám už tradične uložené na githube. Projekt používa licenciu MIT.

Rozhranie pre príkazový riadok

Nainštalovaný balík sa dá cez python zavolať príkazom python -m <názov balíka>. V tomto prípade teda python -m reportlab_qr_code. Bez ďalších argumentov prijíma dáta na štandardnom vstupe a vypisuje PDF na štandardný výstup. Po napísaní pár znakov, stlačení enter a Ctrl+D sa na štandardný výstup vypíše PDF.

Vstupný text sa dá vložiť aj priamo do príkazu. V tom prípade bude PDF zase vypísané na štandardný výstup:

python -m reportlab_qr_code "Hello world"

Výstupný súbor sa dá určiť parametrom --outfile

python -m reportlab_qr_code "Hello world" --outfile hello.pdf

Príkaz vygeneruje PDF súbor s veľkosťou necelé 2 kB. Ak vynechám hlavičky, samotný QR kód zaberá 595 bytov.

Hello world v QR
Obrázok 1: Hello world v QR

Parametrom --size je možné zmeniť veľkosť a --padding mení veľkosť okraja:

python -m reportlab_qr_code "Padding 0.5cm" --outfile out.pdf \
  --size 6cm --padding 0.5cm
Vlastná veľkosť
Obrázok 2: Vlastná veľkosť

Nastaviť sa dá samozrejme aj vlastná farba. Ak nie je definovaná farba pozadia, výsledný kód je transparentný.

python -m reportlab_qr_code "http://linuxos.sk" --outfile out.pdf \
  --bg "#b5c7e3" --fg "#3f68a7"
Vlastná farba
Obrázok 3: Vlastná farba

QR kód môže byť voliteľne vytvorený aj z gradientu. Ten je definovaný súradnicami x1 y1 x2 y2 a farbami buď vo forme jednoduchého zoznamu, alebo dvojíc pozícia farba. Nasledujúci kód vykreslí gradient začínajúci sa v súradniciach 0.2, 0.2 (0 0 je ľavý horný roh, 1 1 pravý dolný) a končiaci sa v 1, 1. Na začiatku je oranžová farba, v 10% zelená, 20% modrá a gradient končí červenou.

python -m reportlab_qr_code "https://linuxos.sk/" --outfile out.pdf \
  --gradient "linear 0.2 0.2 1 1 #ee8800 0.1 #00ff00 0.2 #0000ff #ff0000"
Gradient
Obrázok 4: Gradient

Nastaviť je možné aj korekciu chýb (možnosti 'L', 'M', 'Q' a 'H') a verziu.

python -m reportlab_qr_code "reportlab" --outfile out.pdf \
  --error_correction L
Nízka korekcia chýb
Obrázok 5: Nízka korekcia chýb
python -m reportlab_qr_code "reportlab" --outfile out.pdf \
  --error_correction H
Vysoká korekcia chýb
Obrázok 6: Vysoká korekcia chýb
python -m reportlab_qr_code "reportlab" --outfile out.pdf \
  --version 10
Manuálne nastavenie verzie
Obrázok 7: Manuálne nastavenie verzie

Implementovaná je aj taká chuťovka ako zaoblenie hrán. Zaoblenie sa určuje v jednotkách relatívnych k veľkosti pixelu, takže veľkosť 0.5 zaoblí jednotlivé pixely kódu do perfektného kruhu.

python -m reportlab_qr_code "Paradajka" --outfile out.pdf \
  --radius 0.5
Jednoduché zaoblenie
Obrázok 8: Jednoduché zaoblenie

Vektorový obrázok sa dá reprezentovať aj pomocou prekrížených slučiek. Bez zaoblenia je rendering identický, akurát kód s prekríženými slučkami je kratší. Pri zaoblení sa prekrížené slučky vypínajú, ale dajú sa zapnúť voľbou --enhanced-path

python -m reportlab_qr_code "Paradajka" --outfile out.pdf \
  --radius 0.5 --enhanced-path
Zaoblenie s prekríženými slučkami
Obrázok 9: Zaoblenie s prekríženými slučkami

Polomer zaoblenia môže byť väčší než jeden pixel. V takom prípade však negarantujem čitateľnosť kódu.

python -m reportlab_qr_code "Paradajka" --outfile out.pdf \
 --radius 3.5
Veľmmi veľký polomer
Obrázok 10: Veľmi veľký polomer
python -m reportlab_qr_code "Paradajka" --outfile out.pdf \
 --radius 3.5 --enhanced-path
Veľa šťastia s načítavaním
Obrázok 11: Veľa šťastia s načítavaním

Je možné vygenerovať kód aj s invertovanými farbami.

python -m reportlab_qr_code "OSLINUX" --outfile out.pdf \
  --invert --radius 0.5 --padding 0 --bg "#000000" --fg "#ffffff"
Invertovaný kód
Obrázok 12: Invertovaný kód

Použitie v RML

Vytvoriť jedno PDF s QR kódom je síce pekné, ale väčšina ľudí chce QR kód použiť v nejakom dokumente.

Jedným zo spôsobov, ako generovať PDF dokument je použitie Reportlab RML dokumente, čo je XML do istej miery podobné HTML. Dokumentácia k jazyku RML je dostupná ako vo forme užívateľskej príručky, tak aj vo forme zjednodušenej príručky "RML for Idiots".

Knižnica Reportlab je open source generátor PDF dokumentov šírený pod BSD licenciou. Jazyk RML je však komerčným doplnkom v Reportlab Plus, ktorého cena začína už od 1500 britských libier ročne. Ehm. Je to kvalitná knižnica a niekto to zaplatiť musí. Ja to však nie som.

Takže nikomu to nehovorte, ale existuje takmer na 100% kompatibilná implementácia RML z3c.rml.

Na začiatok ukážem jednoduchý výpis textu "Ahoj svet!" na jednu stránku A4. Súbor doc.rml bude vyzerať takto:

<!DOCTYPE document SYSTEM "rml_1_0.dtd">
<document filename="out.pdf" invariant="1" compression="1">

<template pagesize="a4">
    <pageTemplate id="main" pagesize="a4 portrait">
        <frame id="main" x1="1cm" y1="1cm" width="19cm" height="27.7cm"/>
    </pageTemplate>
</template>

<story>
    <para>Ahoj svet!</para>
</story>

</document>

PDF dokument out.pdf sa dá vytvoriť príkazom python3 -m z3c.rml.rml2pdf doc.rml.

PDF dokument
Obrázok 13: PDF dokument

Vložiť QR kód do dokumentu je možné zavolaním tagu <plugInGraphic module="reportlab_qr_code" function="qr">.

Obsah tagu je bodkočiarkami oddelný na voľby;formát;obsah. Formát je buď text, alebo base64. Obsah vysvetľovať netreba a voľby sú čiarkou oddelený zoznam vo forme názov=hodnota,názov2=hodnota2. Voľby prakticky zodpovedajú parametrom v CLI. Isté malé rozdiely tam sú, preto odporúčam pozrieť detaily v dokumentácii.

<!DOCTYPE document SYSTEM "rml_1_0.dtd">
<document filename="out.pdf" invariant="1" compression="1">

<template pagesize="a4">
    <pageTemplate id="main" pagesize="a4 portrait">
        <frame id="main" x1="1cm" y1="1cm" width="19cm" height="27.7cm"/>
    </pageTemplate>
</template>

<story>
    <para>Ahoj svet!</para>

    <illustration height="15cm" width="15cm" align="center">
        <plugInGraphic module="reportlab_qr_code" function="qr">size=15cm,padding=0.5cm,error_correction=L,fg=#ff0000;text;http://linuxos.sk</plugInGraphic>
    </illustration>
</story>

</document>
PDF dokument s QR kódom
Obrázok 14: PDF dokument s QR kódom

Jednou z vecí, ktoré nefungujú v RML dokumentoch je gradient. Namiesto neho existuje parameter mask=1, ktorý spôsobí, že všetky objekty vykreslené v bloku illustration nasledujúce za plugInGraphic budú orezané podľa QR kódu. Ako podklad kódu je tak možné použiť farebný gradient, obrázok, vektorovú grafiku, jednoducho čokoľvek, čo sa dá vykresliť v illustration.

Nasledujúci kód využíva masku na vykreslenie QR kódu cez fraktál.

<!DOCTYPE document SYSTEM "rml_1_0.dtd">
<document filename="out.pdf" invariant="1" compression="1">

<template pagesize="a4">
    <pageTemplate id="main" pagesize="a4 portrait">
        <frame id="main" x1="1cm" y1="1cm" width="19cm" height="27.7cm"/>
    </pageTemplate>
</template>

<story>
    <illustration height="15cm" width="15cm" align="center">

        <!-- obrázok pozadia -->
        <image file="fractal.png" x="0" y="0" width="15cm" height="15cm" />

        <!-- prekrytý polopriehľadným bielym štvorcom -->
        <fill color="rgba(255,255,255,0.8)"/>
        <rect x="0" y="0" width="15cm" height="15cm" fill="true" stroke="false" />
        <fill color="#000"/>

        <!-- nastavenie masky -->
        <plugInGraphic module="reportlab_qr_code" function="qr">size=15cm,padding=1cm,mask=1;text;http://linuxos.sk</plugInGraphic>

        <!-- znovu vykreslenie obrázka, ale bez prekrytia -->
        <image file="fractal.png" x="0" y="0" width="15cm" height="15cm" />

    </illustration>
</story>

</document>
QR kód s maskou
Obrázok 15: QR kód s maskou

Samozrejme ten istý efekt sa dá dosiahnuť aj jednoduchšie polopriehľadným invertovaným QR kódom:

<!DOCTYPE document SYSTEM "rml_1_0.dtd">
<document filename="out.pdf" invariant="1" compression="1">

<template pagesize="a4">
    <pageTemplate id="main" pagesize="a4 portrait">
        <frame id="main" x1="1cm" y1="1cm" width="19cm" height="27.7cm"/>
    </pageTemplate>
</template>

<story>
    <illustration height="15cm" width="15cm" align="center">
        <image file="img/mandelbrot_spiral2.png" x="0.05cm" y="0.05cm" width="14.9cm" height="14.9cm" />
        <plugInGraphic module="reportlab_qr_code" function="qr">size=15cm,padding=1cm,fg=#ffffffcc,negative=1,radius=0.5;text;http://linuxos.sk</plugInGraphic>
    </illustration>
</story>

</document>
Transparentný kód
Obrázok 16: Transparentný kód

Python API

Použitie z python kódu je podobne jednoduché ako v RML dokumente. Stačí zavolať funkciu qr_draw s parametrami canvas a content (obsah). Zvyšné parametre sú voliteľné a zodpovedajú predchádzajúcim príkladom.

from reportlab.pdfgen import canvas
from reportlab_qr_code import qr_draw


c = canvas.Canvas("py.pdf", pageCompression=0)
qr_draw(c, "Hello world", x="1cm", y="1cm", size="19cm", bg="#eeeeee")
c.showPage()
c.save()

Porovnanie s inými možnosťami vloženia QR kódu do PDF

Existujú samozrejme aj iné možnosti, ako vložiť QR kód do PDF dokumentu. Je tu napríklad možnosť vygenerovať bitmapový obrázok a vložiť ho do PDF. V tom prípade však hrozí, že obrázok bude v dôsledku vyhladzovania rozmazaný. Do istej miery sa to dá kompenzovať uložením v podstatne vyššom rozlíšení, ale nepovažujem to za práve najelegantnejší spôsob. Moja knižnica generuje vektorový obrázok, preto nemá problém s ľubovoľným zväčšením.

Existujú aj iné knižnice pre reportlab, napríklad reportlab-qrcode. Ten má však niekoľko nevýhod. V prvom rade nemá rozhranie pre vkladanie do RML dokumentu, čo je ale drobnosť.

Táto knižnica skladá QR kód zo štvorcov, čo vedie niekedy k renderingu s medzerami medzi štvorcami. Na nasledujúcom obrázku je napríklad QR kód otvorený v inkscape.

Zlý rendering v inkscape
Obrázok 17: Zlý rendering v inkscape

Moja knižnica má implementovaný algoritmus na spájanie susedných pixelov do jednej veľkej plochy, vďaka čomu nemá žiadne medzery medzi pixelmi.

Spájanie pixelov má aj ďalšiu výhodu - redukciu veľkosti súboru, pretože sa zbytočne neopakujú tie isté súradnice.

Nakoniec posledným významným zlepšením je použitie transformačnej matice namiesto absolútnych súradníc. Nasledujúci kód je ukážka PDF z reportlab-qrcode.

0 0 0 rg
n 39.09856 234.5914 9.77464 9.77464 re f*
n 39.09856 224.8167 9.77464 9.77464 re f*
n 39.09856 215.0421 9.77464 9.77464 re f*
n 39.09856 205.2674 9.77464 9.77464 re f*
n 39.09856 195.4928 9.77464 9.77464 re f*
n 39.09856 185.7182 9.77464 9.77464 re f*
n 39.09856 175.9435 9.77464 9.77464 re f*
n 39.09856 156.3942 9.77464 9.77464 re f*
n 39.09856 146.6196 9.77464 9.77464 re f*
n 39.09856 127.0703 9.77464 9.77464 re f*
n 39.09856 97.7464 9.77464 9.77464 re f*
n 39.09856 87.97176 9.77464 9.77464 re f*
…

Moja knižnica nastavuje najskôr transformačnú maticu, vďaka čomu môže generovať celočíselné súradnice.

0 0 0 rg
20.71472 0 0 20.71472 51.7868 51.7868 cm
n 0 0 m 7 0 l 7 7 l 0 7 l h 8 0 m 10 0 l 10 1 l 12 1 l 12 0 l 11 0 l 11 3 l 13
3 l 13 2 l 12 2 l 12 4 l 13 4 l 13 5 l 8 5 l 8 2 l 9 2 l 9 1 l 8 1 l h 14 0 m 21
0 l 21 7 l 14 7 l h 1 1 m 6 1 l 6 6 l 1 6 l h 15 1 m 20 1 l 20 6 l 15
6 l h 2 2 m 5 2 l 5 5 l 2 5 l h 16 2 m 19 2 l 19 5 l 16 5 l h 9 4 m 10 4 l 10
7 l 11 7 l 11 6 l 8 6 l 8 7 l 9 7 l h 12 6 m 13 6 l 13 10 l 14 10 l 14 11 l 15
11 l 15 13 l 14 13 l 14 12 l 17 12 l 17 10 l 16 10 l 16 16 l 15 16 l 15 18 l 16
18 l 16 20 l 17 20 l 17 21 l 19 21 l 19 20 l 18 20 l 18 18 l 19 18 l 19 19 l 15
19 l 15 21 l 14 21 l 14 20 l 13 20 l 13 16 l 14 16 l 14 15 l 9 15 l 9 12 l 8 12
…

Výsledné nekomprimované PDF je v priemere o 80% menšie.

Algoritmus

Môj pôvodný generátor mal všetky problémy, ktoré som videl inde. Tie isté problémy mali aj používatelia inkscape. Tak som sa teda rozhodol opraviť svoj rendering a vymyslel som nejaký algoritmus. Neviem ako sa volá, ani či má svoje meno (určite nie som sám, kto to riešil takto). V nasledujúcich pár riadkoch skúsim vysvetliť aspoň ako funguje.

Môj algoritmus by sa dal popísať asi takto:

kým obrázok obsahuje čierne pixely
	spracuj_segment()
koniec
procedúra spracuj_segment:

začiatok := nájdi ľavý horný pixel

cesta := [začiatok]
pozícia := začiatok
smer := doprava

urob_krok() # zmení pozíciu podľa aktuálneho smeru

kým pozícia != začiatok # postupným spracovaním sa posúvame na začiatok

	ak dá_sa_zahnúť_vľavo() potom
		cesta := cesta + [pozícia]
		otoč_smer_doľava()
		urob_krok()

	alebo ak dá_sa_ísť_rovno() potom
		urob_krok()

	inak # ak nie je iná možnosť, musí sa ísť napravo
		cesta := cesta + [pozícia]
		otoč_smer_doprava()
		urob_krok()

	koniec

koniec

vyčisti_pixely_v_segmente()

Nájdenie prvého pixelu vľavo hore zaručuje, že vždy bude prvý smer doprava, pretože minimálna šírka je jeden pixel a dohora sa zahnúť nedá keďže sme na prvom pixeli.

Algoritmus je určite konečný pre segmenty konečnej veľkosti. Pri opisovaní povrchu týmto algoritmom nemôže dôjsť k nekonečnej slučke. Pozrime sa bližšie na jeden príklad.

Príklad algoritmu na štvorci
Obrázok 18: Príklad algoritmu na štvorci

Začína sa vľavo hore na pozícii (1, 1) (súradnice x a y). Súradnice sa zaznamenajú do cesty - cesta = [(1,1)]. Urobí sa krok na pozíciu (2, 1). Doľava sa nedá ísť, takže sa ide rovno. Na pozícii (3, 1) je situácia rovnaká, nedá sa ísť vľavo, takže sa pokračuje rovno. Pozícia (4, 1) je zaujímavejšia, pretože nie je možné pkračovať ani vľavo, ani rovno. Nakoniec jedinou možnou cestou je napravo. Do cesty sa preto zaznamenajú aktuálne súradnice - cesta = [(1,1), (4,1)] a smer sa otočí doprava (teda doprava + otočenie doprava = dole). Rovnako pokračuje algoritmus ďalej až kým nenarazí na súradnice (4, 4), (1, 4) a nakoniec (1, 1), kde algoritmus končí.

Ako teória znie pekne, ale prakticky algoritmus nevie vykresliť duté objekty.

Nesprávne vykreslenie dutého objektu
Obrázok 19: Nesprávne vykreslenie dutého objektu

V počítačovej grafike sa nie len tak prenič-zanič používa pravidlo nepárna - párna. Znamená to, že sa vyplní tá časť objektu, ktorá je oddelená nepárnym počtom hrán. Jedna hrana - vykreslí sa, 2 hrany - nevykreslí. V takom prípade vykreslenie cesty vo vnútri bude znamenať "vykrojenie" diery do objektu.

Dôležitou súčasťou algoritmu je práve vyčistenie pixelov v segmente. Ak by sa nevyčistili, algoritmus by nikdy neskončil. Niekoho by mohla napadnúť jednoduchá implemntácia, pri ktorej sa všetky pixely vo vnútri zmenia na biele, ale v tom prípade by sa už nedali vykresliť vykrojenia objektu. Ak sa však všetky pixely vo vnútri invertujú, tj. z čiernej sa stane biela a opačne, potom po vyčistení zostane práve ten útvar, ktorý je potrebné vykrojiť.

Ak má vnútro objektu viac bielych pixelov než čiernych, môže sa stať, že po vyčistení bude tých čiernych viac než pred vyčistením. Na mieste je tu otázka: bude algoritmus konečný? Aj keď sa môže počet čierných pixelov zvýšiť, určite sa plocha segmentu zmenší minimálne o jeden pixel, pretože algoritmus sa pohybuje po obvode, ktorý musí tvoriť minimálne jeden čierny pixel.

Zostatok po inverzii vnútra
Obrázok 20: Zostatok po inverzii vnútra

Tento algoritmus rozhodne nie je optimálny. Napadol ma minimálne jeden príklad, pri ktorom by mohol mať kratší výstup.

Uzol
Obrázok 21: Uzol

Algoritmus by v tomto prípade zostavil cestu [(1,1), (3,1), (3,2), (4,2), (4,4), (1,4), (1,1), (2,2), (3,2), (3,3), (2,3), (2,2)].

Optimalizované prechádzanie uzlom
Obrázok 22: Optimalizované prechádzanie uzlom

Optimálne by bolo [(1,1), (3,1), (3,3), (2,3), (2,2), (4,2), (4,4), (1,4), (1,1)], teda 9 bodov namiesto 12.

Doplnenie algoritmu je pomerne jednoduché. Stačí rozpoznať nasledujúci vzor:

Kríženie ciest
Obrázok 23: Kríženie ciest

Podľa pôvodného algoritmu by sa pri smere dole v strede mal zmeniť smer doprava. Efektívnejšie by v tomto prípade bolo ísť rovno. Úprava spočíva len v tejto jednoduchej detekcii a otočení smeru algoritmu (namiesto preferovaného ľavého smeru sa bude preferovať pravý) po prechode cez prekríženie.

Po zmene je namieste zase otázka, či algoritmus bude konečný.

Ak algoritmus prejde uzol, potom sa musí vrátiť späť na pôvodné miesto (pretože zase ide po prvchu ako z vonkajšej strany). Je ľahko dokázateľné, že po návrate na pôvodné miesto bude smer kolmý na pôvodný smer. Vždy pri prvom prechode dôjde k prepólovaniu smeru, pričom pri druhom prechode sa prepóluje na pôvodný smer otáčania. Po slučke bude algoritmus pokračovať presne tak isto, ako keby v slučke vôbec nebol. Keď toto platí pre ľubovoľnú vnorenú slučku, bude to platiť aj pre slučky vnorené v slučkách. Algoritmus je teda konečný a nemôže dôjsť k zacykleniu. Či je optimálny, to neviem posúdiť, ale nenašiel som žiaden príklad, kedy by výstup nebol optimálny.

Zoblenie

Hrany sú voliteľne zaoblené pomocou beziérovych kriviek. Moja cesta k výsledku nebola tak priamočiara, ako by sa mohlo zdať. Nech sa páči, tu je pár mojich neúspešných pokusov :)

Pokusy o zaoblenie
Obrázok 24: Pokusy o zaoblenie

Dokumentácia k beziérovym krivkám mi bola trochu nejasná, takže som najskôr išiel systémom pokus omyl. Niektoré pokusy boli dokonca celkom celkom elegantné, ale väčšinou absolútne nečitateľné. Nakoniec som to nejakým spôsobom doklepal.

Mimochodom, vždy som si myslel, že pre vykreslenie presného kruhu majú byť kontrolné body beziérovej krivky presne v rohoch štvorca, ktorý opíše kruh. Ak by to niekto náhodou riešil upozorňujem, že body majú byť vo vzdialenosti (4/3)*tan(π/8) = 0,552 a nie v rohoch (1).

Nakoniec som teda dopísal asi najlepšiu knižnicu na generovanie QR kódov pre reportlab.

    • RE: QR kódy v PDF 27.10.2022 | 14:46
      Avatar redhawk1975 Windows 11 nonsystemd edition  Používateľ

      zaujimavy blog.

      ja som lenivy qr si davam generovat cez qr monkey

      najcastejsie generujem vcard alebo text

      Do or do not. There is to no try.​