Nano QR kód generáló

Fórumok

Mivel több HUP-os fórumtárs is mikrokontrollerezik, ezért gondoltam megosztom, hátha másnak is hasznos lesz.

Összedobtam egy minimalista QR kód generálót ANSI C-ben, kifejezetten URL-ek kódolására:
- Szabad és Nyílt Forráskódú, MIT licenszű
- irtó kicsi (kb. 150 SLoC), egyetlen egy függvény csak
- nincs semmi függősége (még libc se, de még csak libc fejlécek sem kellenek neki)
- nem foglal memóriát egyáltalán
- csak pár bájtot eszik a veremből
- pofon egyszerű használni, nem kell konfigurálni
- egy 53 x 53 pixeles szürkeárnyalatos képet köp ki (csak 0 (előtér) ill. 255 (háttér) színekkel)
- a bemenete egy legfeljebb 128 bájtos URL (vagy hát bármilyen UTF-8 sztring igazából)

Példa használatra (csak példa, semmilyen domain sem query paraméter nincs a függvénykönyvtárban, bármi lehet):

#define NANOQR_IMPLEMENTATION
#include "nanoqr.h"

uint8_t image[53][53];
nanoqr(image, "https://gitlab.com/bztsrc/nanoqr?errcode=1234&errpos=1234&when=1234");

Ennyi. Aztán a megadott URL-re lehet rakni egy JavaScriptet, ami megcsócsálja és ember számára érthető hibaüzenetekké formálja az URL paraméterében megadott kódokat.

Technikai részletek: QR version 9-et generál (mert a 128 bájtos tárhely elfogadhatónak tűnt, a 53 x 53 pixeles méret meg elég kicsi ahhoz, hogy elférjen LCD-kre, de akár LED-es kijezlőkre is). A hibajavító kód QUARTILE (eggyel rosszabb csak, mint a legjobb opció), a maszkszint fixen 1-es, az adathalmaza pedig bájt (mert az alfanumerikusból hiányzik a ?, &, =, szóval nemigazán kódolható vele URL). Ezeket bedrótoztam az egyszerűség kedvéért, hogy ne kelljen semmit állítgatni. Ha valakinek ez nem felelne meg, akkor nekik ott a Nayuki QR kódgeneráló, de az többet eszik és bonyibb használni is.

SZERK: lett nagytesója, MiniQR, ez kb ugyanaz használni, de az összes QR verziót tudja (V1-től V40-ig), dinamikusan méretezi a kimeneti képet (21-től 177 pixelig), a bemenete meg egészen elképesztő 1663 bájtos is lehet. Ja, és ez bűntetőpontokat számol a maszkokra, és asszerint választja ki. Mindezért cserébe kb. kétszer akkora a kód, 300 SLoC, és több memóriát is igényel, valamint már nemcsak egyetlen függvényből áll (a publikus API-ja továbbra is egyetlen fgv.).

#define MINIQR_IMPLEMENTATION
#include "miniqr.h"

uint8_t image[177 * 177];
int wh = miniqr(image, "https://gitlab.com/bztsrc/nanoqr?errcode=1234&errpos=1234&when=1234");

A visszaadott kép "wh" x "wh" pixeles, lehet hogy csak 53 x 53.

Hozzászólások

Szep :) Tenyleg teljesen warning-mentesen fordul 4 architekturan is, olyan 1.5k-2k koruli kod lesz minden esetben. 

Hm... az mennyire bonyolitana el a kodot hogyha 2809 bytenyi 0-255 kep helyett ~352 ... ~371 bytenyi bitmaszkot allitana elo? 

Szep :)

Kösz! :)

Hm... az mennyire bonyolitana el a kodot hogyha 2809 bytenyi 0-255 kep helyett ~352 ... ~371 bytenyi bitmaszkot allitana elo?

Nem annyira vészes. A képet két lépcsőben állítja elő:
1. először 255-el tölti fel (133. sor), majd bizonyos pixeleket 0-ára állít, hogy a jól ismert alap mintázat rákerüljön (134.-150. sor).
2. az adatpixelek berakásakor (153.-191. sor) szintén csak 0-ra állítás van, illetve itt előfordul még XOR.
Ezek triviálisan átírhatók bitenkénti "dst &= ~x" és "dst ^= x" utasításokra, ez nem okoz problémát.

A gond inkább azzal van, hogy az 53 nem osztható 8-cal, így nem egyértelmű, mennyire kéne venni a sorhosszt. Lehet 56-ra (azaz 7 bájtra), de az az alignment miatt gondot okoz sok helyen (pl. ha BMP-be akarod menteni), vagy még több paddinggal 64-re, aminél meg lehetne akár uint64_t-t is használni egy sorra (mondjuk mikrokontrollereknél ritkán van 64 bites szó, de nem kizárt). Viszont az is gond még ennél, hogy minden rendszer máshogy veszi, balra vagy jobbra legyen-e a legalacsonyabb bit, mármint az endianess problémán túl (a PNG és a BMP pl. eltérő sorrendet használ). Bájtoknál nincs ilyen probléma, annál mindenki balról jobbra olvassa a sort, és az endianess is tök mindegy.

Aztán meg az is van, hogy macerás egy bitmapből kirakni a képet. Ha fájlba akarod menteni, akkor grayscale-nek simán megy így bájtokkal egy-az-egyben, ha indexelt palettás a formátumod, akkor elég minden pixelre gondolkodás nélkül &1-et nyomni és meg is vagy (az indexméret úgyis 8 bit), és RGB-re konvertálni is triviális (csak megismétled 3szor a bájtot ha true-color, vagy kétszer, ha hi-color), stb. Bitmapet sokkal több kóddal és macerávan lehetne csak átalakítani a cél pixelformátumra, ráadásul mint említettem volt, annál folyamatos probléma, hogy mi is a bitsorrend és hogy mennyi legyen a padding, meg még endianess függő is, mert nemcsak 1 bájt egy sor.

Szóval mindezeket megfontolva jutottam arra, hogy jobb, ha bájt alapú a kép, tisztább, szárazabb érzés.

Meg egy gyors szakmai kerdes :) Ez a kod mennyire altalanosithato, akar #define-okkal, akar mint fuggvenyparameterrel hogy ne csak Version 9-et tudjon hanem (akar) kisebbet is? Igen, az biztos ront a hatekonysagan hogyha fuggvenyparameterrel adod at a negyzet meretet, de a forditaskor ismert meret mint megoldas is erdekes/hasznos lehet. Akkor masok a GF(2^8) parameterek? 

Igen, az biztos ront a hatekonysagan hogyha fuggvenyparameterrel adod at a negyzet meretet

A QR kód nem egészen így működik. Vannak belőle "verziók", amik igazából nem is verziók, hanem pixelméretek és fixen a hozzájuk tartozó kapacitások.

Ez a kod mennyire altalanosithato, akar #define-okkal, akar mint fuggvenyparameterrel hogy ne csak Version 9-et tudjon hanem (akar) kisebbet is?

Hát lássuk...

1. A legtöbb statikus tömb marad, ahogy van. A "message" és az "interleved_output" tömb mérete változhat, mert ezek a bemenet méretétől függenek. A "message_parameters"-ből és a "version_info"-ból kétdimenziós tömböt kell csinálni, mert ezek az értékek QR verzió függőek.

2. Az üzenet formázása rész elé kell egy ciklus, ami kikeresi, hogy a bemenet méretéhez melyik verzió passzol. Vagy hát ez jöhet paraméterből, #define-ból akár, a lényeg, hogy a kapacitás függvényében kell egy QR verziószám (és indirekt így meghatározza a pixelméretet is, az ugyanis "(verzió-1)*4+21" fixen).

3. Ha csak kissebb QR kódokat akarsz, akkor az üzenet formázás rész marad ugyanúgy, ahogy van. Ha nagyobbakat is, akkor kell egy if a 97. sor köré, mert a magasabb verzióknál 3 bájtos az üzenet fejléce, nem 2, egyébként az üzenet formázás algoritmusa azoknál is marad ugyanaz (persze más, verziófüggő "message_parameters" értékekkel fog dolgozni, de a kódon nem kell módosítani).

4. A különböző dobozok kirajzolása rész megint marad ugyanaz, csak 53 helyett változóból kell jöjjön a méret, csak a ciklusok iterációs száma lesz más (illetve az is csak kettőnél, a többinél csak a pozíció változik).

5. Az adatpixelek berakása rész szintén marad ugyanaz, mint most, megint csak az 53 helyett kell változó, egyébként mást nem kell módosítani.

Ami a legnagyobb módosítás lenne szvsz, hogyha dinamikusan kezeled a verziót, akkor oda kell figyelni a maszkokra. Ez most elegánsan fixre lett véve az egyszerűség nevében, de egy rendes implementációban ki kéne generálni az összes lehetséges maszkkal a QR kódot, mindhez hibaértéket számolni, és a legalacsonyabb pontszámú változatot kéne visszaadni. Ha dinamikus a méret, akkor (mivel a deketálódobozok mérete fix) bizonyos méretváltozatoknál drasztikusan megnő az esélye annak, hogy a dobozokhoz hasonló minta alakulhat ki az adatpixelek részben, azért fontos ilyenkor a korrekt maszk kezelés. Bár a tesztjeim során úgy vettem észre, a legtöbb olvasót nem zavarja, simán megeszik, ha az adatpixelek részben felbukkan itt-ott egy detektálódoboz minta, nem sikerült még olyan QR kódot előállítanom, ahol ez gondot okozott volna (de biztos létezik ilyen, gondolom nem ok nélkül rakták bele a QR spec-be a maszkokat).

Lehet olyat is, hogy nem kozvetlenul dst[x][y]=0; es hasonlo modon allitod a pixeleket, hanem mondjuk 3 inline-ositott fuggvenyen keresztul. pl.:
inline void set_backgound(void *dst, int x, int y);
inline void set_foreground(void *dst, int x, int y);
inline void toggle(void *dst, int x, int y);

Aztan a default implementacio szepen beallitja a megfelelo koordinatat 0-ra/255-re vagy xor-olja 255-tel. Ahol meg nincs memoria, vagy amugy is bites kep kell, ott a megfelelo kepformatumot megirja hozza a user. (nem bonyolult)

Egy ismerosom vett egy kinai hopapiros nyomtatot mikrokontrolleres projecthez, de nem birt vele, ugyhogy en deritettem ki a hasznalatat. Egy Arduino nano-hoz kotottem, abban 2kB RAM van, egy QR kod a te byte-os formatumodban nem fert volna bele. Bitenkent meg a nyomtato formatumaiban igen (3-fele mod volt benne, sorfolytonos, oszloponkent 8-8 bit, meg valami egeszen fura). (o PIC-et tett a vegen a kutyube, mert azt szereti, nem tudom mennyi RAM-mal)

LCD eseten megint valami mas formatum lesz a megfelelo - szinte biztos, hogy nem 0 es 255.

De az is teny, hogy innen mar kb. trivialis atirni a megfelelo formatumra. Osszessegeben jo project, tetszik! A sok konstans tomb gondolom a pixelek helye (kigyo forma) meg a CRC szamitas. Az kicsit nehezebben ertheto, de a formatum sajatja.

A strange game. The only winning move is not to play. How about a nice game of chess?

Lehet olyat is, hogy nem kozvetlenul dst[x][y]=0; es hasonlo modon allitod a pixeleket, hanem mondjuk 3 inline-ositott fuggvenyen keresztul.

Lehet, de az bonyolítaná az API-t. Így most csak egy függvény, oszt jóccakát. (Egyébként tényleg gondoltam erre, fontolgattam, hogy define-al lehessen-e függvényeket megadni pixelállításhoz.)

De az is teny, hogy innen mar kb. trivialis atirni a megfelelo formatumra.

Jaja, a cél pont ez volt, egy olyan köztes valami, ami könnyedén kezelhető. Ebből bitmapet is könnyedén lehet csinálni, csak lecsíped a legalacsonyabb helyiértékű bitet és kész is (aztán hogy azt merre mennyivel shifteled, az meg már úgyis implementációfüggő, a hőnyomtatódon se tudom még abból, hogy a 8-as csoportból állt, hogy akkor az 1 vagy a 0x80 volt-e balra).

A sok konstans tomb gondolom a pixelek helye (kigyo forma) meg a CRC szamitas. Az kicsit nehezebben ertheto, de a formatum sajatja.

Hát, ez is, az is. Az első négy a blokkcsoportok mérete és száma (két csoport van), aztán Reed Solomon ECC polinóm paraméterei (az egyik indirekten a gen_poly újabb konstans tömbből veszi ki az offszetet a még újabb konstans tömb polinóm táblából), aztán a végén az utolsó két érték az már tényleg pixelminta, ami a képre kerül (innen tudja az olvasó, hogy milyen QR verzióról van szó).
A "kígyó forma", már amennyiben a bekeretezett négyzetet meg a két keresztvonalat érted ez alatt, na az pont nincs benne ezekben :-) Azok ugyanis éppenséggel verziófüggetlenek (csak a pozíciójuk változik, a mintájuk nem).

Ja, jó bonyi az egész, de igazából egy-két dolgot leszámítva elég ötletes és hatékony (annak például nem látom értelmét, miért kell zig-zag sorrendben olvasni a pixeleket és minek van benne numerikus, alfanumerikus stb. adathalmaz, miért nem elég a bájt és hogy minek bitekre szétbontani a bájtokat külön két bájtra, miért ne lehetnének egy-az-egyben az üzenet bájtjai; minden más tök oké és ötletes).

Igen, ertheto az a dontes is, hogy 1 fuggvennyel meg akartad oldani az egeszet.

Kigyo alatt pont a zig-zag-et ertem, mert hogy kigyozik az adat. A fix bekeretezett negyzetet lattam, hogy kodbol rakod ki, arra az a jo megoldas.

Japanbol indult, 2 bit adja meg, hogy byte (talan utf-8), numerikus, alfanumerikus, vagy kanji van-e belekodolva. A keszito kitalalta, hogy ebbol a 4 variaciobol hany darab fer bele 16 bitbe, onnantol meg bitenkent kezelheto az info. 8 bitnel kevesbe lett volna hatekony a szam meg a kanji tarolas. Ha a hatekony kanji nem kellett volna (eleg ugyanazt utf-8-ban kodolni opciokent), akkor valoszinuleg byte szervezesu lenne.

A strange game. The only winning move is not to play. How about a nice game of chess?

Kigyo alatt pont a zig-zag-et ertem, mert hogy kigyozik az adat.

Na, az se pont a statikus tömbökből jön, hanem sok idióta x, y léptetésből :-)

Japanbol indult, 2 bit adja meg, hogy byte (talan utf-8), numerikus, alfanumerikus, vagy kanji van-e belekodolva.

Na ennek tökre nem látom értelmét. Nagyjából a következő lépésekből áll a dolog:

1. a bemeneti bitkolbászból egy másik bitkolbász lesz (bájt, numerikus, alfanumerikus, kanji, ráadásul biteként szétrobbantva). Na ennek tökre semmi értelme, bájt típus önmagában elég lenne, és nem látom azt sem, hogy a különböző átkódolással nagyon változna a bitek eloszlása (az látszik, hogy ez volt vele a szándék, csak hát minek, lásd 5. pont).

2. a keletkezett bitkolbászokat blokkokra darabolja, blokkonként hibajavító kóddal látja el. Na ez a része tök jó, és ha beleássa valaki magát a Reed Solomonba, akkor rájön, nagyon leleményes és trükkös megoldás.

3. a kimenetre rá kell rajzolni a dobozokat (a spec ezeket finder, timing stb. patterneknek hívja). Ez megint tök oké, és ötletes. Mondjuk a három sarok minta lehetne képarány tartó, akkor nem lenne gond azzal, hogy esetleg az adatban előjöhet ez a minta, na de akkor meg azt nem tudná az olvasó, hogy mekkora az egy pixel. Szóval érthető, miért így csinálták.

4. ezután kell a hibajavító kódokkal teletüzdelt bitkolbászt belerakni a maradék pixelekre. Na itt nem értem, minek a zig-zag bejárás, miért nem jó a sima balról-jobbra, fentről-le pixelsorrend. Nem javít semmit sem a pixelek eloszlásán az, hogy össze-vissza sorrendben kerülnek a képre. De biztos volt valami oka, hogy ilyen idiótára találták ki.

5. ami ezután jön, az megint kifejezetten zseniális: az adatpixeleket különböző mintákkal kell XOR-olni, hogy elkerüljük azt, hogy a 3. pontban rárajzolt doboz minták fordulhassanak elő az adatpixelekben. Na emiatt is tök felesleges az 1. pont bitmahinálása és a 4. pont zig-zag-je. Amit meg igazán ötletesnek tartok ebben az az, ahogy a bitminták előállnak pár képlettel. Ha megjeleníted ugyanis ezeket a képleteket, akkor olyan mintákat adnak, amik feltünően hasonlítanak a 3. pont dobozaira, de mégsem azok. Ha tehát ezzel a mintával XOR-olod az adatpixeleket, akkor nagyon jó eséllyel elkerülöd benne a doboz mintákat, ez egyszerűen zseniális, irtó ötletes megoldás.

Ehhh, hogy magyarázzam ki magának... :-) Itt egy ábra: qr.png. A legbaloldalibb képen látszanak a 3. pont dobozai, meg a 4. pont pixelsorrendje. A maradék három kép pedig vicces XOR minta, amit ezek a képletek eredményeznek (az utolsó három).

Gratulálok, szép munka!

 

A detektálhatóság miatt nem kétszínűnek kellene lennie az eredményképnek? Jó kontrasztos, fekete-fehér?

WAT?

Egyetlen fuggveny, ami a parameterul kapott 2D tombbe beteszi a parameterul kapott stringbol generalt QR kodot. Aztan hogy a 0-at es 255-ot hogy jelenited meg, rad van bizva. Azt is leirta, miert nem 1 bit egy pixel 1 byte helyett.

Ha 0 es 255 van benne, mi nem ketszinu? Az meg, hogy hopapiros nyomtato vagy kisnyuszik csinalnak belole a vegen beolvashato kepet, nem ezen a szinten dol el. Ha ott nem eleg kontrasztos, az baj ugyan, de nem a C kod hibaja.

A strange game. The only winning move is not to play. How about a nice game of chess?

Szerkesztve: 2025. 10. 18., szo – 14:07

@apal felcsigázta az érdeklődésem a kérdéseivel, hogy mennyi lenne teljesen QR specifikáció konformra felhozni.

Összedobtam hát egy fork-ot, a MiniQR-t. Ez tudja az összes V1 - V40 verziót, a bemenet hosszának megfelelően változó méretű képet generál, és a maszkot is dinamikusan, a specifikációban szereplő büntetőpontszámítással választja.

Az eredmény: kb. kétszer annyi kód, 300 SLoC, több statikus tábla, a "message" mérete 1666 lett, az "interleaved_output" meg 3704 bájt. Jóval több, mint a NanoQR-é, de még mindig röhejesen kevés igazából. A legnagyobb kép, amit kiköp, az 177 x 177 pixeles szürkeárnyalatos (ez a V40, szintén csak 0 vagy 255 szín szerepel benne, átmenetek nem).

Nagyon szep :)

Mar nekem is nagyon hianyzik egy olyan parancs aminek megadom argumentumnak a sztringet (URL) es vagy az stdout-ra, de megjobb hogy direkt a vagolapra (`xclip`-pel vagy barmi ekvivalens modon) kiteszi a kepet. Es akkor mar mehet is a doksiba/prezentacioba. Szoval ha atirom a peldadban a 

f = fopen("qrcode.png", "wb");

sort egy 

f = popen("/usr/bin/xclip -selection clipboard -target image/png","w");

sorra akkor kb meg is lehetunk...!

Parancs szinten nem tudom milyen, de library API szinten egy borzalom a libqrencode. Iszonyat erőforrászabáló, lassú, és ember legyen a talpán aki kiigazodik a függvényhívásai között. Mikrokontrolleren használni esélytelen.
Méghogy "fast and compact", hát a lótúrót. Tetű lassú, kismillió fájl és a több, mint 6500 SLoC (!) aligha hívható kompaktnak.

(Még anno RPi2-n akartam használni, de már az a hw is kevés neki, ezért kezdtem el keresgélni, és úgy találtam a Nayuki-s implementációkra egyébként. Na, a Nayuki-sra már tényleg rá lehet mondani, hogy "fast and compact", két fájl, 1300 SloC. Ahová most kellett nekem, oda viszont már ez is elfogadhatatlanul sok, azért álltam neki a NanoQR-nek.)

Gratula, szuper, foleg a dinamikus verzio! egy eve en is ezzel kuzdottem, csak en (nem rohog) pythonban. Kiprobaltam csomo qr libet de mind iszonyu bloat vagy maskepp volt szar, vegul kenytelen voltam en is irni osszeollozni egy minimalistat.