QR kódy v PDF
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.
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
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"
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"
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
python -m reportlab_qr_code "reportlab" --outfile out.pdf \ --error_correction H
python -m reportlab_qr_code "reportlab" --outfile out.pdf \ --version 10
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
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
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
python -m reportlab_qr_code "Paradajka" --outfile out.pdf \ --radius 3.5 --enhanced-path
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"
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
.
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>
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>
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>
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.
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.
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.
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.
Tento algoritmus rozhodne nie je optimálny. Napadol ma minimálne jeden príklad, pri ktorom by mohol mať kratší výstup.
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)]
.
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:
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 :)
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.
Pre pridávanie komentárov sa musíte prihlásiť.
zaujimavy blog.
ja som lenivy qr si davam generovat cez qr monkey
najcastejsie generujem vcard alebo text