A következő címkéjű bejegyzések mutatása: Python. Összes bejegyzés megjelenítése
A következő címkéjű bejegyzések mutatása: Python. Összes bejegyzés megjelenítése

2026/08/31

A felejtés, mint glitch


Biztos jártál már úgy, hogy egy régen látott helyszínre érve azt érezted, hogyha le kellett volna rajzolnod emlékezetből, teljesen másképp helyeztél volna el tárgyakat. Az a fa nem itt volt, annak a sziklának más volt a színe, balról jöttünk, nem jobbról, stb. Mi ez, ha nem egy-két, rég nem használt neuronkapcsolat megszakadása, és az emlékek újra rajzolása a hibás kapcsolat figyelmen kívül hagyásával? Vagyis egy glitch az emlékezetben. 



Ezzel a két képpel dolgozunk, PNG formátumban igyekszünk szanaszét glitchelni, az itt és itt ismertetett módon. Esetleg nézd meg ezt a videót is, a kolléga nagyjából hasonlókat művel és őt is a japán művész inspirálta, mint minket. A 15-20 éve eltűnt városképek, amiknek emlékét agyunkban is lassan kikezdi a felejtés, tökéletes alanyok erre a játékra. 
A bejegyzés végére nagyjából tudni fogjuk, hogy milyen típusú képeket, és milyen módon érdemes megtámadni ahhoz, hogy értelmezhető glitcheket kapjunk.



1. Photoshopban large, medium és smallest mentései az RGB8 PNG filter-swapjére (más filterrel kódoljuk ki, mint amivel bekódoltuk) nem mutatnak különbséget. Mellesleg ezen a fileon, a különböző tömörítéseknél, fileméretben csupán 1-2 % eltérés volt, ellenben mentési időben akár 100% is. Szóval nem érdemes a smallest but slowest tömörítést használni.

Minden filtertípus Averageként kikódolva pont így néz ki.

2. Az viszont meglepett, hogy a program kezeli az alfa csatornát is, a Nap-korongot vágtuk ki a képből. 

A legelső pixelt támadtuk meg, mert a program nem enged menteni támadás nélkül
A filtereket viszont meghagytuk, ahogy az eredeti fileban voltak.

RGBA - összes scanlineja Averagként kibontva, azért ez teljesen mást eredményezett,
mint kettővel fennebb. 

3. RGB16-os PNG-vel nem áll szóba a program, tehát csak a 8 bitesek működnek. De hibaüzenettel lekezeli. 

4. Az Interlaced képeket is megtagadja a program, ügyesen hibaüzenettel.

Innentől csak RGB8 és large mentésű (vegyes filterezésű és olykor normalizált filterezésű) fileokat  támadunk.

5. Pixel támadás mátrixban, vagyis 10*10 pixelenként a teljes kép felületén, filterezést nem bántjuk: 

Az eredmény érdekes, mert sejtetni engedi, hogy a photoshop milyen típusú felületeknek, soroknak milyen filtereket választ leginkább, az ég zömmel Sub filterezésű, a felhő töri ezt meg picit, aztán lefelé, ahogy sűrűsödnek a kábelek Paethre vált,  a házcserepeknél a fene tudja, Average-re tippelnék, mert nem látszik a 10*10-es mintázatban a fehér pixel (amennyiben None lenne ezt várnánk), aztán lefele a ház fala megint Paeth, az aszfalt Average és a csajok megint Paeth. 

Emlékeztetőül ilyen lenne a 10*10-es támadás None-ra normalizált filterű képen. 


A másik képünkön az a nagyon furcsa, hogy az eget nem bántotta, egyetlen pixelt se, erre jelenleg nincs magyarázatunk (még Ps-ben az eredetivel Differenc módban is összenéztük, semmi jele az égben a hibának). Aztán lefele a Paeth, az Average, kicsit megint a Paeth, majd a Sub dominál. 
Gondoltuk sorra megnézzük, hogy ugyanezzel a 10*10-es pixeltámadással elhazudjuk az összes filtert is, mondjuk Average-nek, csak egyetlen filtert hagyunk meg ahogy volt, sorra a None, Sub... és hoppá, ha a Sub filterű scanlineokat békén hagyjuk az összes többi Average, akkor az ég érintetlen marad? Vagy van benne némi átlós textúra?


Nézzük csak meg PS-ben Difference módban az eredetivel és egy durva Curve-t ráhúzva!


Ja, vagy így. Hát ott van a glitch, de mivel minden pixelt 255-re tettünk és az ég eleve nagyon világos, ezért alig látszik a hatás, de ott van. Tehát nemcsak az számít, hogy mennyire homogén zóna scanline-jairól van szó (ami alapján filtert választ a program a soroknak) hanem az is, hogy milyen világos vagy sötét tónust milyen byte-értékkel támadunk az egyes pixelpozíciókon. 

6. Most azt nézzük meg, hogy a három Custom filterünkkel normalizáltan elkódolt képeink hogyan néznének ki, ha a dekódolásukat az 5 valid filter egyikére hazudnánk. Itt már csak a kevésbé zajos képünket teszteljük, hiszen az már világosan látszik, hogy a filterek támadása egyfajta edge-detection szűrőként hatnak. A részletgazdag (nagyfrekvenciás) képek így a filtertámadás során belefulladnak a zajba, és csak a nagyobb egységes felületek maradnak meg.  Mint már várható volt, mindhárom saját elkódolás, None kikódolással egy erős edge detectiont eredményez



Talán a a Custom2 filterünk (ami az RGB színeket külön szomszédaitól veszi) Non-ként interpretálva lehet valamennyire érdekes a színeltolódás miatt, a másik két szűrőnk monokrómabb összhatásban (közelről azért durván színzajos), kb. ugyanez. A Sub és Up filterekkel kikódolás már érdekesebb, nagyon képfüggő lehet, hogy kacat, vagy mestermű lesz belőle.



Bár a Custom1 és Custom3 bemenet se rossz, de a legjobb eredményt itt is a Custom2 bemenetű Sub és Up-ként interpretált képek adták.
Nyilván az Averagetől várjuk a legtöbbet, és nem is csalódunk, mindhármat mutatjuk:

Custom1 filtered  - decoded as Average 

Custom2 filtered  - decoded as Average

Custom3 filtered  - decoded as Average

A Paeth filter meg hozza a szokásost, értelmezhetetlen de pszichedelikus pixel-kristályszerkezet, ami legalább rímel az aminosav kristályaink képeire.



7. Kipróbáltuk a Custom1 (körkörös prediktorunkat is a kikódolásra). Az egyszerű, vegyes filterű, képeink elég semmilyenek lettek, viszont a Custom 1, Custom 2 és Custom 3 elkódolású képek Custom 1 dekódolással már csöppet érdekesebbek:


Továbbra is leginkább a Custom3-nak elkódolt, majd Custom1-ként visszakódolt kép nyújt bármiféle esztétikai élményt, ennél ugyanis, mivel elkódolásnál a pixel RGB színeinek byte értékeit három szomszédjától szedi, a súlyos edge-detection-szerű hatás mellett megfigyelhető egy enyhe pszichedelikus color-shift is.

Körülbelül egyhetes utazásunk végére érve további rengeteg ötletünk is lett, amit egyszer talán érdemes lenne leprogramozni, vagy leprogramoztatni az AI-val. 
Mellesleg az AI szerint ez a teljes projekt, programokkal, szkriptekkel, elemzésekkel, szőröstől-bőröstől pár óra laptophasználat ökológiai lábnyomával ér fel. ennél jóval több számítógéphasználat történt az én kliensoldalamon, legalább 20 óra. Másképpen megközelítve, pár száz palacsintarecept megbeszélése az AI-val ekvivalens öko-lábnyomot eredményez, mint ez a projekt.
További ötletek, saját filterek esetére érdemes lenne kipróbálni azt, hogy az egyes pixel-byteok kiszámolásánál ne csak az aktuális kép már elkódolt bytejaira támaszkodjon, hanem egy másik kép hasonló pozíciójáról is vegyen információkat, például a Red és Green byte értékét vegye az Average szabályai szerint a baloldali és felső pixel átlagából, de a Blue byteot egy másik kép azonos pozíciójú bal és felső pixelátlagából számolja. Ez várhatóan valami olyasmit eredményezne, mintha a két képet valamilyen elcseszett módon egymásra mentettük volna. 

Custom2 to Average + byte attack 1-20

Ugyanakkor van bennünk egy bizonytalanság, hogy ez a módszer valóban glitchnek számít e, a szó szoros és véletlen-kontextusú értelmében, vagy inkább data bending, esetleg egyfajta peremvidéke a hibának és a képfeldolgozásnak? 




2026/08/28

PNG autopsy III

A következőkben a Filterezés támadását tovább gondoljuk és saját filtereket találunk ki. Érdemes az előző bejegyzéssel alapozni, ha nem onnan jöttél. 

Tehát azt már tudni véljük, hogy a PNG 5 filterétől eltérő (más irányokból is építkező) filterek alkalmazásának nincs pragmatikus vonatkozása, de ez nem jelenti azt, hogy nem kísérletezhetünk vele.
Már van olyan eszközünk (köszi Ai, vagy ti mindannyian, akik drágább memória árakkal fizetitek ezt helyettem, vagy az utókornak, akinek ezzel éljük fel a jövőjét), amelyik képes olyan PNG-ket menteni (csak RGB8 semmia alfa és semmi interlace), ahol a scanlineokra kötelezően általunk választott filtert alkalmaz, csak annyit kell hozzáprogramozni, hogy saját algoritmusokat is tudjon kezelni. Ezeknek mondjuk a filter byte-ját 5-6-7 és így tovább, írná bele a képbe, amitől egy normális képnézegető egyből hülyét kap, például a Photoshop, hiszen nem ismert Filtert nem tud használni. De például az Irfan (sőt a windows file explorer nézete) nem esik kétségbe, látszólag az összes nem értelmezhető filterű scanlinet None-nak tekint és megjeleníti. De van olyan eszközünk is, ami adott scanline típus-byteokat el tud hazudni. Az ilyen Custom filterezett képeinknek itt tudunk valid filter-byteot beállítani, ezután már minden képnézegető meg fogja nyitani a képünket a kiválasztott algoritmus szerint, ami természetesen a glitchet fogja okozni.

Három Custom filterrel próbálkoztunk, remélve a szebbnél szebb glitch mintázatokat. Amit ha nem is sikerült elérni, de tapasztalatokat azért szereztünk. Fontos, hogy ezeknél a teszteknél az előző bejegyzésben is említett kis segédprogramunkat használjuk, ami minden scanlinra ráerőlteti ugyanazt a filtert.

Custom1 -- Körkörös prediktor (mind a 8 szomszéd átlaga) - ami körbe minden pixelt figyel, vagyis 4 már elkódoltat a szokásos irányból (balsó, felső irányok), és négy pixelt a jobbra és alul elterülő részről, amit  kikódolásnál azért nem ismerhetünk, mert még nem kódoltuk ki (ha nem világos, tényleg menj vissza az előző posztra). Nyilván emiatt egy ilyen filterrel elkódolt kép nem invertálható (dekódolható) egyszerű szekvenciális dekódolással, de most ez nem is cél, hiszen a Glitch okozására hajtunk, ezért csak ENCODE-ra használjuk.

Custom2 -- RGB-Average (csatornánként MÁSHOVA néző prediktor) - vagyis
- R csatorna <- bal szomszéd R csatornája
- G csatorna <- balsó-fenti szomszéd G csatornája
- B csatorna <- fenti szomszéd B csatornája
Ez az algoritmus elvileg ENCODE-ra és DECODE-ra is használható, hiszen csak valid irányokból veszi az adatot.

Custom3 -- InverseAverage (jobbra / lent szomszédok átlaga) - itt minden hivatkozás jövőbeli, tehát későbbi feldolgozású pixelre mutat, ENCODE-ra tökéletesen használható, de DECODE-ot csak fordított irányban lehetne végrehajtani rajta, tehát ha a jobb alsó pixeltől indulva visszafelé zajlana a dekódolás.

És így néznek ki a Custom Filterekkel elkódolt, de valós Filtereknek (azokkal kikódolt) hazudva:



Emlékeztetésnek és összehasonlításnak (az előző bejegyzésből átemelve),  a valós filterek egymás között elhazudva így néztek ki:

Kis felbontású és JPG-nek mentett nézet, az eredeti jóval gazdagabb.


Az eredmény érdekes, de sajnos nem a reményeink szerint való, azt vártuk volna ugyanis, hogy a merőben eltérő encoder filtereink merőben eltérő hatásokat fognak produkálni, rosszabb esetben annyira összeborzolják a képet, hogy az felismerhetetlenné válik. Ezzel szemben a kikódolásra használt filter hatása a domináns, tehát például egy UP függőlegesen vonalas, egy Average átlósan, stb, és egyáltalán nem felismerhetetlen a kép sem. 

Ezen a ponton végleg el kellett fogadnunk, hogy a végső, meg-glitchet kép zajmintázatát, a glitch geometriáját leginkább nem az határozza meg, hogy eredetileg milyen filterrel volt elkódolva, hanem jellemzően a kikódoló algoritmus. Tehát hiába találunk ki bármilyen irányú, csillag alakú, hatszög, stb. saját-filtert, a végeredmény mégis a kikódoló filter jellegzetességeit fogja mutatni. 

De hogyan lehetne megnézni egy (bármilyen filterezésű) képet a saját filterrel kikódolva, amikor nincs a földön olyan képszerkesztő, amelyik ismerné a mi saját filterünk algoritmusát?  Na erre is írattunk egy python progit a mesterséges intelligenciával, ami bármilyen filterezésű bemeneti képet egy saját filterrel próbál értelmezni. A saját filternek azt választottuk, amelyik a pixel összes (körben mind a 8) szomszédjának átlagával dolgozik, és ugyanúgy, balról jobbra, fentről lefelé járja be a képet. Ilyenkor értelemszerűen az adott pixel kikódolásakor a 8 szomszédból csak négynek (balra és felül található) az értéke ismert, a többi négynek (jobbra és alul található pixeleknek) az értéke még nincs kikódolva. Ez fogja okozni  a glitchet. Ezt egy kis parancssoros pythonnal értük el, ami annyit csinál, hogy bármilyen filterezésű bemeneti képet a mi saját filterünkkel kódol ki, majd az egyszerűség kedvéért sima bmp-nek ment.


Azt vártuk, hogy olyan zaj-katyvaszt kapunk, hogy a fal adja a másikat, ezzel szemben meglepődtünk. Egyrészt igazolódott, hogy mindig a kikódoló filter karakterisztikája lesz a domináns a glitchelt képen. Vagyis közelről megnézve pontszerű (pointilista-szerű) zaj keletkezik, ami nem meglepő hiszen egyes pixelek értéke minden irányból származik, nincs kitüntetett irány. Ugyanakkor az is látszik, hogy az elkódoló filterek is rányomják, bár sokkal csekélyebb mértékben, a kézjegyüket a végleges glitchre. Érdemes megfigyelni, hogy az eredetileg Sub-nak kódolt, majd Customnak kikódolt képen a nagyobb felületek élei dominánsan függőlegesek, míg az Up-nak elkódolt és Customnak kikódoltak esetében vízszintesek. Az Average filterrel elkódolt és Custommal kikódolt képeknél, az összefüggő felületek alsó-jobboldali élei dominánsak. Külön meglepett, hogy a Paeth elkódolás, ami mindig borzalmas kristályszerű szín-glitcheket okozott, itt egyáltalán nem mutatta a szivárványos geometriáit. 

A további 3 kép, amikor az elkódolás során három saját filtert használtunk, külön érdekes. A bejegyzés elején felsoroltuk ezeket a saját filtereket, a C1 az maga a körkörös, amit a kikódolásra is használunk (C1 - C1). Természetesen nem tudja visszaadni az eredeti képet glitchmentesen, hiszen említettük, hogy a 8 szomszédos pixel értékéből, amiből építkezik, 4 értéke nem ismert. Egyfajta edge-detection hatást vélünk felfedezni itt. A C2-C1 esetében az elkódolás során a második saját filterünket próbáljuk kikódolni az első saját filterünkkel, egyszerűsítve, a C2 az RGB értékeinek eltérését három szomszédjától veszi, a pirosat a baltól, a zöldet a bal felsőtől, a kéket pedig a felsőtől.)
A C3-C1 meg hasonló, de itt a C3 egy inverz Average filter, vagyis jobboldali és alsó szomszédaiból építkezik (gyakorlatilag mindkét szomszédja ismeretlen a kikódolás balról és fentről bejárása miatt), ezért látszik úgy, mintha a zajban az edge-detection fordított irányú lenne az Average-C1-hez képest. 

Na így. A programokat, szkipteket elküldöm annak aki kéri, de nem látom értelmét ide felpakolni a blogra, mert valószínű, hogy az elkövetkező 10 évben nem lesz több érdeklődő mint 0.

És végezetül így néz ki a normál RGB8 PNG (vegyes scanline filterrel), Custom 1 filterrel kikódolva:




PNG autopsy II.

Régóta dédelgetett, parkoltatott stb. vágyam, hogy megberheljem a PNG-t, de úgy igazán mélyen.  S bár az AI azt állítja, hogy a PNG-be nem lehet hexeditorban csak úgy belepiszkítani, nekünk régebb mégis sikerült (gondolom az Irfan megengedőbb a sérült CRC-vel).

Kezdjük ezzel. UCNV japán művész, nagyon érdekes munkái vannak, de most csak a PNG-glitchjeire koncentráljunk. Amiről régebb nem találtunk futtatható programot (Rubyhoz még annyira se értünk mint a Pythonhoz), de most a  Claude olyan python-progit ír nekünk, amilyet csak szeretnénk. 

A PNG feldolgozási lánca (hol érdemes megtámadi):

  1. Raw Data (nyers pixelek byte értékei RGB vagy RGBA stb.) — ezt piszkálni valójában BMP glitch, nem PNG-specifikus
  2. Filtered Data — ide lehet nyúlni  - erről fog szólni a bejegyzés
  3. Compressed Data — ide is lehet nyúlni
  4. Formatted PNG (checksummal) — ide NEM tanácsos, mert a sérült CRC miatt érvénytelennek látják a képszerkesztő programok, de  - ilyet csináltunk már hexeditorban és van progi amivel megnyitható.
A nyers megnyitott kép pixeleit, ami mondjuk RGB, RGBA, soronként (ez a scanline) filterezi a képszerkesztő program. RGB8 esetén a pixelek három byteal vannak reprezentálva
<Filter-byte><Pixel1-Red><Pixel1-Green><Pixel1-Blue><Pixel2-Red><Pixel2-Green><Pixel2-Blue>  ... <Pixeln-Red><Pixeln-Green> <Pixeln-Blue> 
RGBA esetén, nyilván 4 byte ír le egy pixelt, RGB16 esetén meg 6 byte. A tovébbiakban RGB8 képekkel dolgozunk, és az Interlace-et is messzire elkerüljük, mert az csak tovább bonyolítaná az egészet.

Az 5 filter típus (amiből választ a képmentő program):

  • None — nincs filter, minden pixel önálló, saját RGB értéket tartalmaz, egy bitcsere csak lokális zajt okoz
  • Sub — az előző (bal oldali) pixel három bytejához viszonyít, és az eltérést tartalmazza → a pixel bytejaiban okozott hiba ezért jobbra "csorog", hiszen a következő pixel bytejai már ebből a sérültből fogja venni az adatot. 
  • Up — a fölötte lévő sorhoz viszonyít, az eltérést tartalmazza → függőlegesen csoroghat a hiba lefelé
  • Average — a Sub és Up átlaga, a balra és fölötte levő szomszédainak átlagára támaszkodik  → átlós, lágy, "üstökös-farok" jellegű, kevésbé "glitches", inkább szép színátmenet
  • Paeth — a legkomplexebb algoritmus → a leglátványosabb, legkiterjedtebb glitch, miközben a kép formája még felismerhető marad 
Ez nagyjából annyit jelent, hogy legtöbbször nem konkrét RGB értékeket tárol a fileban, hanem az adott filter algoritmusa szerint más, szomszédos pixelektől való eltérését, például a Sub esetén az előtte (feldolgozási időben előtte, amúgy balra tőle) levő pixel értékétől való eltérést, Up esetén a fölötte levő pixeltől. Az Average és a Paeth algoritmusai ennél komplexebbek, de a logika ugyanaz. Nagy egybefüggő színfelületek így nagyon sok 0 és más kis értékkel leírhatóak, amik majd jól tömöríthetőek.

A PNG filterezésének logikája az, hogy ha egy irányból bejárjuk a képet, mondjuk bal felső saroktól soronként jobbra, majd lefelé, akkor majd az RGB adat visszanyerésénél is ugyanígy végig tudjuk járni az összes pixelt, ugyanazzal az algoritmussal és mindig valid RGB adatokat találjunk azokon a pozíciókon, amikre támaszkodva vissza kell állítanunk egy pixel értékét. 

Például, a legfelső sor mondjuk Up filterrel kerül elkódolásra. Balról jobbra, fentről lefelé. Az első pixel értéke a felette levő pixeltől való eltérése lenne, de fölötte nincs sor. Ilyenkor a fölötte levő pixel értékét 0-nak veszi a program, így az eltérés gyakorlatilag ugyanaz lesz, mint maga az RGB érték. És így tovább jobbra. A következő sornál, ha történetesen megint Up filterezés van, akkor már tud építkezni a fölötte levő pixelekből. Ugyanez a logika érvényesül a Sub-ra is, csak  vízszintesen. Sőt az Averagnél is, csak itt mind a két, előtte és fölötte levő szomszédját átlagolja, ahol mindig valid (az adott pixel kiszámításánál már visszaállított) adatok vannak. A Paeth esetében szintén, csak itt még bele keveri a bal-felső szomszédot is és nem átlaggal dolgozik. 

Tehát soha nem próbál meg a jobboldalról, vagy alábbi sorokból építkezni, mert amikor vissza kell állítani a képet, mondjuk egy tetszőleges pixelen, akkor mindig csak tőle balra és fölötte találhatóak a már visszaállított értékek, jobbra, és alatta a még kikódolatlan eltéréseket találja, a nemtudni-mihez-viszonyítva. Ezért nincsenek mondjuk körkörös filterek, amik a pixel összes szomszédját használják az algoritmusban, vagy nincsenek olyanok sem, amelyek balról és jobbról is, vagy alulról és felülről is  építkeznének. Mert ezeket egyszerű módon nem is lehetne visszaállítani (bár biztos ennek is meg lehetne találni a matematikáját, de ehhez kicsik vagyunk). 

Minden scanline előtt egy plusz byte jelzi, az adott sor melyik filterrel van képezve (0=None, 1=Sub, 2=Up, 3=Average, 4=Paeth), innen tudja majd a kikódoló program, hogy milyen algoritmust használjon. De egy képszerkesztő programban, felhasználóként, nincs hatásunk arra, hogy melyik sorra milyen filtert használ mentésnél (talán csak nagyon közvetve, amikor a lassúság-minőséget beállítjuk). De az nagyon valószínű, hogy nagy vízszintes hasonló színű felületek ugyanolyan filtert kapnak. Ez is lehetne egy tesztelendő irány, de ragaszkodjunk amennyire lehet az eredeti irányhoz.

A megnyitott kép összes sorát az öt hivatalos és több saját filterrel lehet elmenteni

Mivel minden sor mindenféle filterű lehet, ez nagyon sok változót visz a rendszerbe, ami egyrészt a Glitch-véletlenségének jót tesz, de ha meg akarjuk valamennyire érteni a véletlent, akkor meg pont nem segít. Ezért spéci python-programmal (amit az AI írt nekünk) úgy mentettük el tehát a PNG-ket, hogy minden scanline-ra ugyanazt a filtert erőszakoltuk, majd ezeknek a filtereit szerre elhazudtuk (átállítottuk a scanline elején a filter-byteokat és aszerint kódoltuk ki). Így például egy SUB-nak mentett és SUB-ként megjelenített kép tökéletesen visszaadja a kiindulási képet, de az összes többi filterrel kikódolva a két filter interakciójára jellemző glithet eredményez.


A következő tesztképeket a fenti program 3. pontjánál generáltuk, vagyis benyitottuk szerre az összes None, Sub, Up, Average, Paeth-filterre normalizált - az előbbi programmal generált - képünket és szerre mindegyiknek elhazudtuk a filterét az összes többi lehetséges filterre. Ezért van az, hogy például egy Up-ra normalizált kép Up-ként megnyitva tökéletesen visszaadja az eredetit, de már Average-nek nyitva sérül, és ez mind az ötre igaz:

Kis felbontású és JPG-nek mentett nézet, az eredeti jóval gazdagabb.

Ezzel nagyjából látjuk, hogy milyen hatásra számíthatunk az egyes filterek esetén (ha a PNG minden sora ugyanazzal a filterrel lenne elkódolva). És már az is megsejthető, ami nekünk egyből nem esett le, hogy mindegy mivel volt elkódolva a kép, úgyis a kikódolás / interpretálás filtere fogja rányomni a bélyegét a glitchelt képre. Így a None oszlopban a képek csak zajosak, a Sub oszlopban inkább vízszintes vonalas, Up-nál függőleges struktúrák, Average-nél átlós, Paethnél meg színkristály-zajos. 

Mint az elején láttuk, két helyen érdemes a PNG-t feszegetni, a filterezett adatnál, illetve a tömörített adatnál. Most már érthető, hogy miért van a PNG-glitcheknek ilyen sajátos megjelenése.

Ha a filterezett adatba piszkálunk, az akkor még eléggé pixelspecifikus, konkrét RGB-értéket ugyan nem (a None filtert leszámítva), de pixel-pozíciót, pixel blokkokat, még precízen tudunk támadni x,y koordináta alapján. A filterezett adatfolyamot képmentés során tömörítik (deflate - zlib) ez lesz a compressed data, amit chunkokba szervez, minden chunknak CRC-t számol, ezután már nem tudunk célzottan és közvetlenül támadni egyes pixeleket, vagy kép-zónákat a deflate algoritmus miatt. Elvileg a Compressed-adat támadás után az új IDAT miatt új CRC-t kell számoltatni, hogy a képnéző programok meg tudják jeleníteni. Mivel az Irfan elég megengedő, és mivel régebb Hex-editorban compressed adatot már hajlítgattunk PNG-ben, ezért ezt a vonalat most nem fejlesztjük tovább. Nem kell hozzá semmi, csak egy Hex-editor és szerencsés találat, hogy ne pont az életfontosságú adatba rondítsunk bele. 

A filterezett adatok támadása,
- leginkább pozíciót (x,y koordinátát) lehet támadni. A None filteren kívül, az összes többinél ezek már nem konkrét RGB adatok, hanem valamely szomszédjától való eltérések. 
- de lehet byte mintázatot  (eltérés-mértéket) is támadni, ekkor a képen a támadott pixel koordinátáját egyáltalán nem célozzuk, csak a tárolt értéket cseréljük, bárhol is talál olyat a képen.
- filter-byte támadása, vagyis a scanlinera használt filter típusának elhazudása, ami miatt megnyitáskor nem a megfelelő algoritmussal fogja interpretálni a képnéző. Pl, Sub volt de Average-re változtatjuk, vagy minden sornál random választunk a lehetséges 5-ből, vagyis az összes permutáció kipróbálható. Ezt láttuk fennebb.
- vagy lehetőség van teljesen saját filter-algoritmus létrehozására is, azzal a korláttal, hogy a képnéző programok csak a fenti 5 filtert ismerik és tudják használni. Tehát, ha egy saját algoritmussal filtereztünk, akkor is egy valid filtert kell megnevezzünk a scanline előtt, és a képnéző ezzel fogja visszaalakítani a képet, vagyis ez az eltérés fogja a torzítást vinni a képbe. 

Ezeknek a támadásoknak a végrehajtására az AI írt nekünk egy cukor kis Python programot, ami lehetővé teszi pixel/pixelblokkok értékeinek a megváltoztatását. Illetve byte-mintázatok támadását és filterbyteok meghamisítását is. (Ezeket akár kombinálva.)

1. Pixelpozíció támadása
A következőben a spéci programunkkal létrehozott egységes filterű képeinket felhasználva (hogy az effektusok ne keveredjenek átláthatatlan módon) a 100,85  (bal felső barna folt közepén) pozíciójú pixelt támadjuk meg 10,100,200-as értékkel (R-ben 10-es eltérést, G-ben 100-as eltérést, B-ben 200-as eltérést okozva)
None filternél a 10,100,200 konkrét RGB értéket jelent, nem eltérést. Egyetlen pixelt átszínezett a barna mezőben, szinte nem is látszik.. 

Sub esetén a hiba jobbra végigcsorog.

Up esetén lefele csorog a hiba.

Average esetén átlósan, üstökösen


Paeth esetén meg a pozíciótól jobbra is, lefele is kumulálódik a hiba.

2. Byteok-mintázatok támadása
Ha visszatekintünk a legelső ábrára, az látszik, hogy az Average-filterezett kép az, ami a leglátványosabban támadható (eléggé roncsol, a Sub vagy Up-hoz visszafogott hatásáshoz képest, de nem annyira, mint a Paeth). Ezért az Average-filterre normalizált képünkkel játszunk most tovább. Mivel a képünk sok azonos/nagyon hasonló színű pixelből áll (színpaletta, ez a dolga) és  PNG mentés törekszik a kis értékek halmozására a scanlineban, ezért ha nagyobb byte értékeket (nagyobb eltéréseket reprezentáló) akarunk megtámadni (lecserélni), szinte alig lesz sérülés a képen, mert kevés pixel fog nagyon eltérni a szomszédaitól. A következőben a 2-3-5-7-10 byteértékeket (eltéréseket) cseréljük le egységesen 150-es eltérésre (egységesen 150-et kap az R, G és a B is).  10-től 2 felé nő a roncsolás mértéke.
10 - 150

7-150

5-150

3-150

2-150


A továbbiakban saját szűrőket fogunk kipróbálni.

2026/05/31

Braille-nyilvántartó

Volt egy olyan a bakancslistámon, hogy ebben az életben még egyszer szeretnék programozást művelni (kb 26 év után újra). Remélem ezek nem olyan bezáró-zárójelek, amelyekkel egyes életszakaszaimat zárom le, de ez most mindegy is. 

Kapóra jött, hogy volt a cégnél egy régóta megoldandó feladat, plusz a MI fejlődése is lehetővé teszi már, hogy bárki programozóként tetszeleghessen. Nem mellesleg, emiatt mennek tönkre programozói állások. Ahogyan a fotográfia több rendbéli szekularizálódása is mindannyiszor keményen bevágott a fotós szakmának. 

A braille írások domborító kliséinek előkészítéséről, és nyilvántartásáról van szó. Évekig mondogattam, hogy a csomagolóipar egyetlen (EU-s) releváns húzása a braille rányomtatása a dobozra, mert a csillivilli aranyfóliák, meg amúgy a külcsíny, ellene megy minden józan felfogásnak a reklámról, meg a kapitalizmusról általában, de a braille írás legalább alkalmas arra, hogy a vakok ne vegyenek be hashajtóra nyugtatót. 

Aztán szembesültem azzal az adattal, hogy a vakok alig 10 százaléka érti a braille írást. Nabazmeg.

Szóval, a feladat az volt, hogy a Braille-készítő célgép által fogadott és felismert  dxf-eket előállítsuk. Eddig ezeket QCadben csináltuk, a nyilvántartást pedig nyögvenyelősen Illustratorban, Corelben, szemmel ellenőrizve és egérrel összevillogtatva. Na, ezt akarjuk megkönnyíteni, hogy az egyes klisékkel eltöltött átlagos 10 percet, 1-2 perc környékére redukáljuk.

A 6 pontos braille írás szabványa:
- a pontok 1,6mm átmérőjűek, 2,5 milliméterre vannak egymástól, a braille karakterek távolsága 6mm, a soroké pedig 10mm. 20*4 braille karakterrel dolgozunk, ez 480 pontot jelent.

A részfeladatokat külön programokra bontottuk, a könnyebb tesztelhetőség érdekében, illetve néhány modulra, csak egyszer, illetve csak a tesztidőszakban van szükség.

A felület Tkinter, a dxf fileokhoz szükség van a ezdxf python-csomagra. 

Bulldózer

import os
import tkinter as tk
from tkinter import filedialog, scrolledtext
import ezdxf
from ezdxf import recover
import math


# --- 1. MATEMATIKAI MOTOR ÉS KÓDOLÓ ---
class BrailleEngine:
    def __init__(self):
        # Alapértékek (mm)
        self.dot_dist = 2.5
        self.char_dist = 6.0
        self.row_dist = 10.0
        self.tolerance = 0.5
        # Base64 ábécé a 6-bites Braille cellákhoz
        self.b64_chars = (
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
        )

    def bits_to_base64(self, bit_string):
        """480 bitből 80 darab Base64 karaktert csinál (6 bitenként)."""
        b64_result = ""
        for i in range(0, 480, 6):
            six_bits = bit_string[i : i + 6]
            val = 0
            for bit_pos, bit in enumerate(six_bits):
                if bit == "1":
                    val += 1 << bit_pos
            b64_result += self.b64_chars[val]
        return b64_result

    def get_theoretical_grid(self):
        """Legenerálja a 480 rácspont elméleti koordinátáját."""
        grid = []
        for row in range(4):
            for char in range(20):
                for dot_col in range(2):
                    for dot_row in range(3):
                        x = (char * self.char_dist) + (dot_col * self.dot_dist)
                        y = -((row * self.row_dist) + (dot_row * self.dot_dist))
                        grid.append((x, y))
        return grid

    def fit_points(self, points):
        """Rácsra illesztés és Base64 kódolás."""
        if not points:
            return "A" * 80

        min_x = min(p[0] for p in points)
        max_y = max(p[1] for p in points)
        norm_points = [(p[0] - min_x, p[1] - max_y) for p in points]
        theoretical_grid = self.get_theoretical_grid()

        # ez az offset-lista nem mukodott fuggoleges eltolasban, offsets = [(0, 0), (2.5, 0), (0, -2.5), (2.5, -2.5), (0, -5.0), (2.5, -5.0)]
        # ez az offset-lista nem azonos a BraiLuca offsetlistaajaval, pont mast tologatunk itt

        offsets = [
            (0, 0),      # Alap
            (2.5, 0),    # Ha hiányzik a bal oszlop, a pontokat jobbra toljuk a rács 2. oszlopára
            (0, -2.5),   # Ha hiányzik a felső sor, a pontokat lejjebb toljuk a rács 2. sorára
            (2.5, -2.5), # Mindkettő hiányzik
            (0, -5.0),   # Felső két sor hiányzik
            (2.5, -5.0)  # Bal oszlop és felső két sor hiányzik
]


        for ox, oy in offsets:
            fingerprint_bits = ["0"] * 480
            matched_count = 0
            for px, py in norm_points:
                tx, ty = px + ox, py + oy
                for i, (gx, gy) in enumerate(theoretical_grid):
                    dist = math.sqrt((tx - gx) ** 2 + (ty - gy) ** 2)
                    if dist <= self.tolerance:
                        fingerprint_bits[i] = "1"
                        matched_count += 1
                        break

            if matched_count == len(points):
                return self.bits_to_base64("".join(fingerprint_bits))
        return None


# --- 2. GUI ÉS FÁJLKEZELŐ ---
class BrailleBulldozer:
    def __init__(self, root):
        self.root = root
        self.root.title("Braille Buldózer v1.4 - Base64 Expert")
        self.root.geometry("800x600")
        self.engine = BrailleEngine()
        self.selected_path = ""

        # UI Felépítése
        self.btn_browse = tk.Button(
            root,
            text="Forrásmappa kiválasztása",
            command=self.browse_folder,
            bg="#e1e1e1",
            height=2,
        )
        self.btn_browse.pack(pady=10, fill=tk.X, padx=20)

        self.lbl_path = tk.Label(root, text="Nincs mappa kiválasztva", fg="gray")
        self.lbl_path.pack()

        self.log_area = scrolledtext.ScrolledText(
            root, width=90, height=25, font=("Consolas", 9)
        )
        self.log_area.pack(pady=10, padx=20)

        self.btn_start = tk.Button(
            root,
            text="Feldolgozás és Base64 CSV mentés",
            command=self.process_files,
            state=tk.DISABLED,
            bg="#4CAF50",
            fg="white",
            height=2,
        )
        self.btn_start.pack(pady=10, fill=tk.X, padx=20)

    def log(self, message):
        self.log_area.insert(tk.END, message + "\n")
        self.log_area.see(tk.END)
        self.root.update_idletasks()

    def browse_folder(self):
        """Ez a függvény hiányzott vagy elcsúszott az előbb!"""
        self.selected_path = filedialog.askdirectory()
        if self.selected_path:
            self.lbl_path.config(text=self.selected_path, fg="black")
            self.btn_start.config(state=tk.NORMAL)
            self.log(f"Kiválasztott mappa: {self.selected_path}")

    def manual_dxf_parse(self, dxf_path):
        """Szöveges DXF feldolgozás, ha az ezdxf elakad."""
        points = []
        try:
            with open(dxf_path, "r", encoding="utf-8", errors="ignore") as f:
                content = f.read().splitlines()
            i = 0
            while i < len(content):
                line = content[i].strip()
                if (
                    line == "0"
                    and i + 1 < len(content)
                    and content[i + 1].strip() == "CIRCLE"
                ):
                    current_layer, x, y = "", None, None
                    i += 2
                    while i < len(content) and content[i].strip() != "0":
                        code, val = content[i].strip(), content[i + 1].strip()
                        if code == "8":
                            current_layer = val
                        if code == "10":
                            x = float(val)
                        if code == "20":
                            y = float(val)
                        i += 2
                    if (
                        current_layer.upper() == "LAYER1"
                        and x is not None
                        and y is not None
                    ):
                        points.append((x, y))
                    continue
                i += 1
            return points
        except:
            return None

    def get_layer1_circles(self, dxf_path):
        """Hibrid olvasó: ezdxf -> manual."""
        try:
            doc, auditor = recover.readfile(dxf_path)
            msp = doc.modelspace()
            # Itt egy egyszerűbb lekérdezést használunk a stabilitásért
            pts = []
            for e in msp:
                if e.dxftype() == "CIRCLE" and e.dxf.layer.upper() == "LAYER1":
                    pts.append(e.dxf.center[:2])
            return pts
        except:
            return self.manual_dxf_parse(dxf_path)

    def process_files(self):
        self.log("\n--- FELDOLGOZÁS INDÍTÁSA (Base64 mód) ---")
        results = []
        stats = {"total": 0, "success": 0, "no_grid": 0, "empty": 0}

        for root_dir, dirs, files in os.walk(self.selected_path):
            for file in files:
                if file.lower().endswith(".dxf"):
                    stats["total"] += 1
                    full_path = os.path.join(root_dir, file)
                    circles = self.get_layer1_circles(full_path)

                    if circles is not None and len(circles) > 0:
                        b64_fprint = self.engine.fit_points(circles)
                        if b64_fprint:
                            id_name = os.path.splitext(file)[0]
                            results.append(f"{id_name};{b64_fprint}")
                            self.log(f"OK: {file} -> [ {b64_fprint[:20]}... ]")
                            stats["success"] += 1
                        else:
                            self.log(f"HIBA: {file} -> Nem illeszkedik a rácsra!")
                            stats["no_grid"] += 1
                    else:
                        self.log(f"ÜRES: {file} -> Nincs kör a Layer1-en.")
                        stats["empty"] += 1

        if results:
            csv_path = os.path.join(self.selected_path, "ujjlenyomatok_b64.csv")
            with open(csv_path, "w", encoding="utf-8") as f:
                f.write("\n".join(results))

            self.log(f"\n" + "=" * 40)
            self.log(f"ÖSSZESÍTÉS:")
            self.log(f"  Talált DXF:           {stats['total']}")
            self.log(f"  Sikeres ujjlenyomat:  {stats['success']}")
            self.log(f"  Rossz rács/zaj:       {stats['no_grid']}")
            self.log(f"  Üres fájl:            {stats['empty']}")
            self.log(f"CSV mentve: {csv_path}")
            self.log("=" * 40)
        else:
            self.log("\n--- KÉSZ! (Nem született eredmény) ---")


if __name__ == "__main__":
    root = tk.Tk()
    app = BrailleBulldozer(root)
    root.mainloop()

Ez a program azt tudja, hogy egy kiválasztott könyvtár összes alkönyvtárát bejárva, megkeresi az összes dxf filet, azokban Circle/Spline elemeket keres, azokat megpróbálja megfeleltetni a 20*4 braille-karakteres elméleti pontfelhőnek, majd a talált pozíciókat visszaadva Base64-es ujjlenyomatot készít. A filenév mellett (ami egyben azonosító) az ujjlenyomatokat is egy CSV fileba menti, plusz ad hibalistát azokról a dxf-ekről, amiket nem tudott értelmezni. Ezzel a már létező kliséket adatbázisba tudjuk vinni.

Az ujjlenyomatról:
Itt egy online bináris-Base64 átalakító. Azért a Base64-et választottuk egyéb, rövidebb ujjlenyomatot adó hesseléssekkel szemben, mert jobban átlátjuk a működését, valóban egyedi, másfelől annyira szerencsések vagyunk, hogy a Base64 3 byteos  szegmensei oszthatók 6-tal is, Tehát 24 biten 4 darab, 6 pontos braille-karakter kódolható el. És csodák csodája a 20*4 karakteres 480 pont is osztható 6-tal, meg 24-gyel, szóval ilyen egy szerencsés együttállás. Az ujjlenyomat a braille karakter bal felső pöttyétől halad lefele hármat, majd a karakter jobb felső pozíciójára ugrik, megint lefele halad három pontot, majd ugrik a következő karakter bal felső pontjára, ha elfogyott a 20 karakter, ugrik a következő sor első karakterének első pozíciójára és így tovább. Ahol pontot talál ott 1, ahol nincs pont, ott 0 a visszaadott érték. Tehát így keletkezik egy 480 egybites sorunk, amit 6 bitenként feleltetünk meg a Base64 szótárában levő 2^6 vagyis 64 karakterének. 

Ez egy félkész megfeleltetés, ezt használtuk az ujjlenyomatok ellenőrzésére a tesztfázisban. Természetesen a mi braille-kliséink 6 pontos karakterei nem mindig értelmezhetőek a braille írásnak megfelelően, mert a kliséink normál és 180 fokban fordított állásban is előfordulhatnak, a fordított karakterek vagy teljesen más betűt jelentenek, vagy egyáltalán nem értelmezettek. Így, hogy a vakok 90 százaléka tojik a braille írásra, nincs is értelme braille szöveg-értelmezőt írni. Nekünk az iparban nem fontos mit ír a dobozon, csak hogy minden pont a helyén legyen.

BraiLuca

(a vakok védőszentjéről elnevezve - hogy legyen kulturális adalék is)

# INSUNIT mm-re alllitva, SPLINE es CIRCLE elemeket keres

import os
import tkinter as tk
from tkinter import filedialog, messagebox
import ezdxf
from ezdxf import recover
import math


# --- BRAILLE MOTOR ---
class BrailleEngine:
    def __init__(self):
        self.dot_dist = 2.5
        self.char_dist = 6.0
        self.row_dist = 10.0
        self.tolerance = 0.8  # Kicsit engedékenyebb az új kliséknél
        self.b64_chars = (
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
        )

    def bits_to_base64(self, bit_string):
        b64_result = ""
        for i in range(0, 480, 6):
            six_bits = bit_string[i : i + 6]
            val = 0
            for bit_pos, bit in enumerate(six_bits):
                if bit == "1":
                    val += 1 << bit_pos
            b64_result += self.b64_chars[val]
        return b64_result

    def get_theoretical_grid(self, offset_x=0, offset_y=0):
        """Generál egy rácsot adott eltolással."""
        grid = []
        for row in range(4):
            for char in range(20):
                for dot_col in range(2):
                    for dot_row in range(3):
                        gx = (
                            (char * self.char_dist)
                            + (dot_col * self.dot_dist)
                            + offset_x
                        )
                        gy = (
                            -((row * self.row_dist) + (dot_row * self.dot_dist))
                            + offset_y
                        )
                        grid.append((gx, gy))
        return grid

    def get_best_fit(self, points):
        if not points:
            return None, None, False

        min_x = min(p[0] for p in points)
        max_y = max(p[1] for p in points)

        offsets = [(0, 0), (2.5, 0), (0, 2.5), (2.5, 2.5), (0, 5.0), (2.5, 5.0)]

        for ox, oy in offsets:
            theoretical_grid = self.get_theoretical_grid(min_x - ox, max_y + oy)
            fingerprint_bits = ["0"] * 480
            snapped_points = []
            matched_count = 0

            for px, py in points:
                for i, (gx, gy) in enumerate(theoretical_grid):
                    dist = math.sqrt((px - gx) ** 2 + (py - gy) ** 2)
                    if dist <= self.tolerance:
                        fingerprint_bits[i] = "1"
                        snapped_points.append((gx, gy))
                        matched_count += 1
                        break

            if matched_count == len(points):
                is_vertical_shifted = (oy != 0)
                return self.bits_to_base64("".join(fingerprint_bits)), snapped_points, is_vertical_shifted

        return None, None, False

# --- BRAI LUCA GUI (v1.4 - Spline & Circle Support) ---
class BraiLuca:
    def __init__(self, root):
        self.root = root
        self.root.title("BraiLuca v1.4 - Multi-Entity Support")
        self.root.geometry("500x450")
        self.engine = BrailleEngine()
        self.snapped_points = []
        self.fingerprint = ""

        tk.Label(
            root, text="BraiLuca - Klisé Tisztító", font=("Arial", 12, "bold")
        ).pack(pady=10)

        self.btn_open = tk.Button(
            root, text="DXF Megnyitása", command=self.load_file, height=2, width=30
        )
        self.btn_open.pack(pady=10)

        self.info_frame = tk.LabelFrame(root, text="Státusz", padx=10, pady=10)
        self.info_frame.pack(pady=10, padx=20, fill="x")

        self.lbl_dots = tk.Label(self.info_frame, text="Pontok: -")
        self.lbl_dots.pack(anchor="w")

        self.lbl_fprint = tk.Label(
            self.info_frame, text="Ujjlenyomat: -", font=("Consolas", 9), wraplength=400
        )
        self.lbl_fprint.pack(anchor="w")

        self.btn_copy = tk.Button(
            root,
            text="Ujjlenyomat másolása",
            command=self.copy_to_clipboard,
            state="disabled",
        )
        self.btn_copy.pack(pady=5)

        self.btn_save = tk.Button(
            root,
            text="Tökéletes DXF mentése (Circle)",
            command=self.save_cleaned_dxf,
            state="disabled",
            bg="#4CAF50",
            fg="white",
            height=2,
        )
        self.btn_save.pack(pady=10)

    def get_points_from_entity(self, entity, points_list):
        """Kinyeri a pontokat SPLINE középpontként vagy CIRCLE középpontként."""
        dxftype = entity.dxftype()
       
        # KÖRÖK KEZELÉSE
        if dxftype == "CIRCLE":
            cp = entity.dxf.center
            # Kényszerítsük sima tuple formátumra: (x, y)
            points_list.append((float(cp.x), float(cp.y)))
           
        # SPLINE-OK KEZELÉSE
        elif dxftype == "SPLINE":
            cp = entity.control_points
            if cp:
                # Itt már eleve tuple-t generálsz, ez rendben van
                avg_x = sum(p[0] for p in cp) / len(cp)
                avg_y = sum(p[1] for p in cp) / len(cp)
                points_list.append((avg_x, avg_y))
               
        # BLOKKOK KEZELÉSE
        elif dxftype == "INSERT":
            # A virtual_entities() jó megoldás a beágyazott elemekre
            for sub_entity in entity.virtual_entities():
                self.get_points_from_entity(sub_entity, points_list)

    def load_file(self):
        path = filedialog.askopenfilename(filetypes=[("DXF fájlok", "*.dxf")])
        if not path:
            return

        filename = os.path.basename(path)

        try:
            doc = ezdxf.readfile(path)
            msp = doc.modelspace()
            all_raw_points = []

            # Minden entitást megvizsgálunk (Spline és Circle is bekerül)
            for entity in msp:
                self.get_points_from_entity(entity, all_raw_points)

            if not all_raw_points:
                messagebox.showwarning("Figyelem", "Nem találtam sem SPLINE, sem CIRCLE elemet a fájlban!")
                return

            # --- DEDUPLIKÁCIÓ ---
            unique_points = []
            dup_count = 0
            for p in all_raw_points:
                is_duplicate = False
                for u in unique_points:
                    if math.dist(p, u) < 0.1:
                        is_duplicate = True
                        break
                if is_duplicate:
                    dup_count += 1
                else:
                    unique_points.append(p)

            status_text = f"Fájl: {filename}\nPontok: {len(unique_points)} db"
            if dup_count > 0:
                status_text += f" ({dup_count} duplikátum törölve)"
            self.lbl_dots.config(text=status_text, fg="black")

            fprint, snapped, is_shifted = self.engine.get_best_fit(unique_points)

            if fprint:
                self.fingerprint = fprint
                self.snapped_points = snapped
               
                if is_shifted:
                    display_text = f"Ujjlenyomat: {self.fingerprint}\n⚠️ FÜGGŐLEGES ELTOLÁS! Célgépen kézi bevitel szükséges!"
                    self.lbl_fprint.config(text=display_text, fg="red")
                else:
                    self.lbl_fprint.config(text=f"Ujjlenyomat: {self.fingerprint}", fg="black")
               
                self.btn_copy.config(state="normal")
                self.btn_save.config(state="normal")
            else:
                self.lbl_fprint.config(text="HIBA: Nem illeszkedik a rácsra!", fg="red")
                self.btn_save.config(state="disabled")

        except Exception as e:
            messagebox.showerror("Hiba", f"Beolvasási hiba: {str(e)}")

    def copy_to_clipboard(self):
        self.root.clipboard_clear()
        self.root.clipboard_append(self.fingerprint)
        messagebox.showinfo("Siker", "Vágólapra másolva!")

    def save_cleaned_dxf(self):
        path = filedialog.asksaveasfilename(
            defaultextension=".dxf", filetypes=[("DXF fájlok", "*.dxf")]
        )
        if not path:
            return

        new_doc = ezdxf.new("R2000")
       
        try:
            new_doc.header['$INSUNITS'] = 4
            new_doc.header['$MEASUREMENT'] = 1
        except:
            try:
                new_doc.header.set('$INSUNIT', 4)
            except:
                pass
       
        new_doc.layers.new(name="Layer1", dxfattribs={"color": 7})
        msp = new_doc.modelspace()

        for p in self.snapped_points:
            msp.add_circle(p, radius=0.8, dxfattribs={"layer": "Layer1"})

        new_doc.saveas(path)
        messagebox.showinfo("Siker", "A klisé tisztítva és körökké alakítva mentve!")


if __name__ == "__main__":
    root = tk.Tk()
    app = BraiLuca(root)
    root.mainloop()

Ez a progi a kiválasztott dxf filet értelmezi, Spline és Circle elemeket keres benne (mivel a régebbi Illustrator és a CorelDraw nem tud Circle elemeket exportálni, viszont az újabb Illustrátorok már csak ezt tudnak), ezeket felfeszíti a képzeletbeli 480 pontos gridre, ha kell (mert például a legbaloldalibb karakter bal sorában nincs egyetlen pont se, vagy a legfölső sor legfelső pontsorában sincs) akkor addig offseteli, amíg minden Spline egybeesik a képzeletbeli grid megfelelő pontjaival (minimális tolerancián belül) Ezeknek ujjlenyomatot gyárt (Base64), amire rá tudok keresni az adatbázisunkban, hogy nem e létezik már ugyanilyen klisé. Ha nem találtunk egyezőséget (a másik programban), a program engedi elmenteni a dxf-et új néven, amiben kitakarítja a Splineokat (akkor is ha egymáson több van) és Circle elemekre cseréli a Layer1 rétegre, mert ezt keresi a CNC-gép. Továbbá riportot ad, a pontok számáról, illetve ha függőleges offsetre került sor, ugyanis az acélklisé gyártó szoftver nem tud offsetelni, az olyan kliséket a saját terminálján kell begépelni.

BrAlajos 

import os
import re
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import ezdxf
from ezdxf import recover
import math
import traceback


# --- 1. MOTOR ---
class AlajosEngine:
    def __init__(self):
        self.tolerance = 0.8

    def get_grid_info(self, points):
        if not points:
            return None
        grid_base = []
        for r in range(4):
            for c in range(20):
                for dc in range(2):
                    for dr in range(3):
                        grid_base.append(
                            (float(c * 6.0 + dc * 2.5), float(-(r * 10.0 + dr * 2.5)))
                        )

        min_x, max_x = float(min(p[0] for p in points)), float(
            max(p[0] for p in points)
        )
        max_y = float(max(p[1] for p in points))

        for ox, oy in [(0, 0), (2.5, 0)]:
            matched = 0
            for px, py in points:
                tx, ty = float(px - min_x + ox), float(py - max_y + oy)
                for gx, gy in grid_base:
                    if math.sqrt((tx - gx) ** 2 + (ty - gy) ** 2) <= self.tolerance:
                        matched += 1
                        break
            if matched == len(points):
                return {
                    "ref_x": float(min_x - ox),
                    "ref_y": float(max_y - oy),
                    "max_x": float(max_x),
                    "min_x_orig": min_x,
                    "max_y_orig": max_y,
                }
        return None


# --- 2. GUI ---
class BrAlajos:
    def __init__(self, root):
        self.root = root
        self.root.title("BrAlajos v1.7.1 - Ultimate Hybrid")
        self.root.geometry("600x750")
        self.engine = AlajosEngine()
        self.current_dxf = ""
        self.grid_data = None
        self.snapped_points = []

        tk.Label(root, text="BrAlajos - Finalizáló", font=("Arial", 12, "bold")).pack(
            pady=10
        )
        btn_frame = tk.Frame(root)
        btn_frame.pack(fill="x", padx=20)
        tk.Button(
            btn_frame, text="DXF Megnyitása", command=self.load_dxf, height=2
        ).pack(side="left", expand=True, fill="x")
        tk.Button(
            btn_frame,
            text="Vágólap Beolvasása",
            command=self.parse_clipboard,
            height=2,
            bg="#e1f5fe",
        ).pack(side="left", expand=True, fill="x")

        self.fields = []
        labels = [
            "1. Filenév maradvány:",
            "2. St: azonosító:",
            "3. Terméknév (- C.):",
            "4. BT dátum:",
        ]
        for i in range(4):
            f_frame = tk.Frame(root)
            f_frame.pack(fill="x", padx=20, pady=5)
            tk.Label(f_frame, text=labels[i], width=20, anchor="w").pack(side="left")
            ent = tk.Entry(f_frame, font=("Arial", 10))
            ent.pack(side="right", expand=True, fill="x")
            self.fields.append(ent)

        self.info_area = scrolledtext.ScrolledText(
            root, height=15, font=("Consolas", 9), bg="#f0f0f0"
        )
        self.info_area.pack(pady=10, padx=20, fill="both")
        self.btn_save = tk.Button(
            root,
            text="DXF FINALIZÁLÁSA",
            command=self.save_dxf,
            state="disabled",
            bg="#4CAF50",
            fg="white",
            height=2,
        )
        self.btn_save.pack(pady=10, fill="x", padx=20)

    def is_pac_mode(self, path):
        """Központosított PAC ellenőrzés: névben VAGY végződésben."""
        name = os.path.basename(path).upper()
        # Igaz, ha benne van a -PAC- VAGY a kiterjesztés előtt -PAC van
        return "-PAC-" in name or name.replace(".DXF", "").endswith("-PAC")

    def log(self, msg, color="black"):
        tag = f"tag_{color}"
        self.info_area.tag_configure(tag, foreground=color)
        self.info_area.insert(tk.END, msg + "\n", tag)
        self.info_area.see(tk.END)
        self.root.update()

    def load_dxf(self):
        path = filedialog.askopenfilename(filetypes=[("DXF fájlok", "*.dxf")])
        if not path:
            return
        self.current_dxf = path
        self.btn_save.config(state="disabled")
        try:
            try:
                doc, auditor = recover.readfile(path)
            except:
                doc = ezdxf.readfile(path)
            msp = doc.modelspace()
            circles = [
                (float(e.dxf.center.x), float(e.dxf.center.y))
                for e in msp
                if e.dxftype() == "CIRCLE" and e.dxf.layer.upper() == "LAYER1"
            ]
            if not circles:
                self.log("HIBA: Layer1 üres!", "red")
                return

            self.snapped_points = circles
            self.grid_data = self.engine.get_grid_info(circles)

            if self.grid_data:
                mode_str = "PAC-MÓD" if self.is_pac_mode(path) else "NORMÁL-MÓD"
                self.log(f"Fájl: {os.path.basename(path)}\nMód: {mode_str}")
                self.btn_save.config(state="normal")
                parts = os.path.basename(path).replace(".dxf", "").split("-", 1)
                if len(parts) > 1:
                    self.fields[0].delete(0, tk.END)
                    self.fields[0].insert(0, parts[1])
                    self.fields[1].delete(0, tk.END)
                    self.fields[1].insert(0, f"St: {parts[0]}")
            else:
                self.log("HIBA: Nem rács-szerkezet!", "red")
        except:
            self.log(traceback.format_exc(), "red")

    def parse_clipboard(self):
        try:
            text = self.root.clipboard_get()
            if "- C." in text:
                part = text.split("- C.")[1].strip().split("\n")[0]
                self.fields[2].delete(0, tk.END)
                self.fields[2].insert(0, part[:15])
            date_match = re.search(r"BT:\s*(\d{2}\.\d{2}\.\d{4})", text)
            if date_match:
                self.fields[3].delete(0, tk.END)
                self.fields[3].insert(0, f"BT: {date_match.group(1)}")
            self.log("Vágólap OK.")
        except:
            self.log("Vágólap hiba.")

    def find_text_space(self, points, box_w, box_h):
        min_x = self.grid_data["min_x_orig"]
        right_limit = self.grid_data["max_x"] + 5.0
        row_coords = sorted(list(set(round(p[1], 1) for p in points)), reverse=True)
        base_y = row_coords[0] if row_coords else self.grid_data["max_y_orig"]
        y_corridors = [base_y - (i * 10.0) - 5.0 for i in range(4)]

        curr_x = min_x - 3.0
        while (curr_x + box_w) <= right_limit:
            for mid_y in y_corridors:
                top, bottom = mid_y + (box_h / 2), mid_y - (box_h / 2)
                collision = any(
                    curr_x <= p[0] <= curr_x + box_w and bottom <= p[1] <= top
                    for p in points
                )
                if not collision:
                    return (curr_x, mid_y + (box_h / 2))
            curr_x += 2.0
        return None

    def save_dxf(self):
        if not self.current_dxf or not self.grid_data:
            return
        try:
            doc = ezdxf.readfile(self.current_dxf)
            doc.header["$DWGCODEPAGE"] = "UTF-8"
            msp = doc.modelspace()

            is_pac = self.is_pac_mode(self.current_dxf)
            y_top_pac = float(self.grid_data["max_y_orig"] + 4.0)

            for i in range(4):
                val = self.fields[i].get().strip()
                if not val:
                    continue
                box_w, box_h = (len(val) * 2.75) + 3.0, 8.0

                if is_pac:
                    space = self.find_text_space(self.snapped_points, box_w, box_h)
                    if space:
                        tx, ty = space
                        final_pos = (float(tx + 1.5), float(ty - box_h + 1.5))
                    else:
                        # Mentőöv: Felső vonalra pakolás eltolva
                        self.log(f"Vigyázat: '{val}' a keret fölé került.", "orange")
                        final_pos = (
                            float(self.grid_data["min_x_orig"] + (i * 35)),
                            float(y_top_pac + 1.0),
                        )
                else:
                    # NORMÁL MÓD: Fix pozíciók
                    rx, ry = self.grid_data["ref_x"], self.grid_data["ref_y"]
                    positions = [
                        (rx + 364, ry - 10),
                        (rx + 364, ry - 20),
                        (rx + 364, ry - 30),
                        (rx + 364, ry - 40),
                    ]
                    final_pos = positions[i]

                t = msp.add_text(val, dxfattribs={"layer": "Layer1", "height": 3.5})
                t.set_placement(final_pos)

            if is_pac:
                x_s, x_e = float(self.grid_data["min_x_orig"] - 3.0), float(
                    self.grid_data["max_x"] + 5.0
                )
                for y in [y_top_pac, y_top_pac - 42.9]:
                    msp.add_line((x_s, y), (x_e, y), dxfattribs={"layer": "Layer1"})

            doc.saveas(self.current_dxf, encoding="utf-8")
            self.log("Sikeres mentés.")
            messagebox.showinfo("Kész", "DXF frissítve!")
            self.btn_save.config(state="disabled")
        except Exception as e:
            self.log(f"Hiba: {str(e)}", "red")


if __name__ == "__main__":
    root = tk.Tk()
    app = BrAlajos(root)
    root.mainloop()

Ez a progi a BraiLuca által kicsinosított dxf filet készíti elő véglegesre. A filenévből (ami az azonosító is) illetve vágólapról kapott metaadattal felcímkézi a dxf-et. Négy editálható szövegdobozba gyűjti ki az adatokat, ezek utólag szerkeszthetőek, ha szükség lenne rá, illetve mentésnél a megfelelő pozícióba írja ezeket a dxf-be. A célprogram kétféle módot képes kezelni, ezért a BrAlajos is megkülönbözteti ezeket.

# NORMÁL MÓD: Fix pozíciók
                    rx, ry = self.grid_data["ref_x"], self.grid_data["ref_y"]
                    positions = [
                        (rx + 364, ry - 10),
                        (rx + 364, ry - 20),
                        (rx + 364, ry - 30),
                        (rx + 364, ry - 40),
                    ]
                    final_pos = positions[i]

A normál módot a célgép úgy értelmezi, hogy egy hosszú, fix méretű acéllemez-abroncsra kétszer teszi rá a pontfelhőt és a klisé végére marja az azonosítókat. Mellesleg rotációs torzítást is alkalmaz.

A kivételes módban a klisé szélességét két vízszintes vonallal kell a dxf-ben reprezentálni, a klisé hossza pedig a pontfelhő hosszától dinamikusan függ. Ezeket a vonalakat is berajzolja a program, illetve szövegelemnek (hosszát a karakterek számából saccolja meg), helyet keres a klisé mind a négy sorában. Ha nem talál, figyelmeztet, kézi beavatkozás szükségességére.

Az ujjlenyomatkezelő

import tkinter as tk
from tkinter import messagebox, scrolledtext, filedialog
import sqlite3
import csv

# --- KONFIGURÁCIÓ ---
# Ha hálózati meghajtón használjátok, ide írd az elérést, pl: "Z:/adatok/ujjlenyomatok.db"
DB_PATH = "Z:\Temp\!!!INSTALL\Makrok_Hasznos_Kiskesek\BraiLucaBrAlajos\ujjlenyomatok.db"


def init_db():
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute(
        """CREATE TABLE IF NOT EXISTS fingerprints
                 (identifier TEXT PRIMARY KEY, fingerprint TEXT)"""
    )
    conn.commit()
    conn.close()


def import_csv_to_db():
    file_path = filedialog.askopenfilename(filetypes=[("CSV fájlok", "*.csv")])
    if not file_path:
        return
    if not messagebox.askyesno(
        "Megerősítés", "Töröljek minden adatot és betöltsem a CSV-t?"
    ):
        return

    try:
        conn = sqlite3.connect(DB_PATH)
        c = conn.cursor()
        c.execute("DELETE FROM fingerprints")
        with open(file_path, mode="r", encoding="utf-8-sig") as f:
            reader = csv.reader(f, delimiter=";")
            count = 0
            for row in reader:
                if len(row) >= 2:
                    c.execute(
                        "INSERT OR REPLACE INTO fingerprints (identifier, fingerprint) VALUES (?, ?)",
                        (row[0].strip(), row[1].strip()),
                    )
                    count += 1
        conn.commit()
        conn.close()
        messagebox.showinfo("Siker", f"Beöltött sorok: {count}")
    except Exception as e:
        messagebox.showerror("Hiba", str(e))


def search_fingerprint():
    fp = entry_fp.get().strip()
    if len(fp) != 80:
        messagebox.showwarning("Figyelem", "Az ujjlenyomat 80 karakter kell legyen!")
        return

    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT identifier FROM fingerprints WHERE fingerprint = ?", (fp,))
    results = c.fetchall()
    conn.close()

    entry_res.config(state="normal")
    entry_res.delete(0, tk.END)

    if results:
        ids = ", ".join([r[0] for r in results])
        entry_res.insert(0, ids)
        # Nem szürkítjük ki a gombot, mert lehet, hogy új azonosítóval is felvinnék
    else:
        entry_res.insert(0, "NINCS MÉG ILYEN UJJLENYOMAT")

    entry_res.config(state="readonly")


def save_new():
    fp = entry_fp.get().strip()
    new_id = entry_new_id.get().strip()

    if not new_id or len(fp) != 80:
        messagebox.showerror("Hiba", "Hiányzó azonosító vagy rossz ujjlenyomat hossz!")
        return

    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    try:
        # Ellenőrizzük, hogy ez az ujjlenyomat már létezik-e más néven (csak tájékoztatás)
        c.execute("SELECT identifier FROM fingerprints WHERE fingerprint = ?", (fp,))
        exists = c.fetchall()

        if exists:
            msg = f"Ez az ujjlenyomat már szerepel a következő azonosítókkal: {', '.join([x[0] for x in exists])}\n\nBiztosan felveszed új azonosítóval ({new_id}) is?"
            if not messagebox.askyesno("Duplikátum figyelmeztetés", msg):
                return

        c.execute(
            "INSERT INTO fingerprints (identifier, fingerprint) VALUES (?, ?)",
            (new_id, fp),
        )
        conn.commit()
        messagebox.showinfo("Siker", "Adat elmentve!")
        entry_new_id.delete(0, tk.END)
        search_fingerprint()  # Frissítjük a nézetet
    except sqlite3.IntegrityError:
        messagebox.showerror(
            "Hiba",
            "Ez az AZONOSÍTÓ már foglalt! Minden ujjlenyomatnak egyedi azonosító kell.",
        )
    finally:
        conn.close()


def check_duplicates():
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    query = """SELECT fingerprint, GROUP_CONCAT(identifier, ' | ')
               FROM fingerprints
               GROUP BY fingerprint
               HAVING COUNT(identifier) > 1"""
    c.execute(query)
    dupes = c.fetchall()
    conn.close()

    top = tk.Toplevel(root)
    top.title("Duplikátumok")
    txt = scrolledtext.ScrolledText(top, width=100, height=20)
    txt.pack(padx=10, pady=10)
    if not dupes:
        txt.insert(tk.END, "Nincs duplikált ujjlenyomat.")
    else:
        for f, ids in dupes:
            txt.insert(tk.END, f"{f} -> {ids}\n\n")


# --- UI ---
root = tk.Tk()
root.title("Ujjlenyomat Kezelő")
root.geometry("700x500")

# Keresés
tk.Label(root, text="Ujjlenyomat (80 karakter):").pack(pady=5)
entry_fp = tk.Entry(root, width=90)
entry_fp.pack(pady=5)
tk.Button(root, text="ELLENŐRZÉS", command=search_fingerprint, bg="#d1e7ff").pack(
    pady=5
)

# Eredmény
tk.Label(root, text="Jelenlegi azonosítók az adatbázisban:").pack(pady=5)
entry_res = tk.Entry(root, width=70, font=("Arial", 10, "bold"), justify="center")
entry_res.config(state="readonly")
entry_res.pack(pady=5)

# Mentés
tk.Frame(root, height=2, bd=1, relief="sunken").pack(fill="x", padx=20, pady=10)
tk.Label(root, text="Új azonosító rögzítése (ha szükséges):").pack(pady=5)
entry_new_id = tk.Entry(root, width=40)
entry_new_id.pack(pady=5)
tk.Button(root, text="MENTÉS ÚJ AZONOSÍTÓKÉNT", command=save_new, bg="#d4edda").pack(
    pady=5
)

# Admin
admin_frame = tk.Frame(root)
admin_frame.pack(side="bottom", fill="x", pady=20)
tk.Button(admin_frame, text="Duplikátum riport", command=check_duplicates).pack(
    side="left", padx=20
)
tk.Button(admin_frame, text="CSV Import", command=import_csv_to_db, bg="#f8d7da").pack(
    side="right", padx=20
)

init_db()
root.mainloop()

SQlite adatbázist használ. Ez a program vágólapon fogadja a BraiLuca által generált ujjlenyomatot és egyezőséget keres a már létező és a Bulldózer által készített adatbázisban. Többlépcsős figyelmeztetés után engedi az adatbázis újratöltését a megadott CSV-ből (amennyiben valami adatveszteség, vagy legelső használat miatt be kellene dózerolni a Bulldózerrel az összes létező klisét). Ugyanazt az ujjlenyomatot több kódnéven is engedi elmenteni (praktikus szükségesség a rendszerünkben, hogy ugyanaz a pontfelhő több típusú klisén is szerepelhet), találat esetén minden egyezőséget megmutat. Sőt képes az adatbázisban duplikátumok listázására is, amivel első körben arra voltunk kíváncsiak, hogy a régi nyögvenyelős azonosságkereső módszerünk mennyire volt megbízható. 
Ezt a progit nem dolgoztuk ki túlságosan, mert az ujjlenyomatok kezelését várhatóan a céges PHP rendszerbe integrálnánk később, csupán a teszt-időszakra volt rá szükségünk. 

Az ujjlenyomatot is kezelő BraiLuca

Mivel nagyon hosszas és beszédes hallgatást tapasztaltunk a vezetőség oldaláról, ezért elengedtük azt a lehetőséget, hogy az ujjlenyomatkezelést a céges PHP-ba implementáljuk. Ezért a BraiLucába belekerült az Ujjlenyomatkezelő néhány funkciója, az azonosítás és az adatbázisba mentés. Így nincs szükség külön program elindítására. A duplikátumkezelést, CSV importot nem tartottuk meg, mert minek, illetve az adatbázis szerkesztgetését (például elgépelési hibák javítása), egy külső ingyenes adatbáziskezelővel  (SQLiteDatabaseBrowserPortable) oldjuk majd meg. 
A módosítást ezennel a Claude AI végezte, második nekifutásra jó is lett. Sőt cuki volt, mert az ikon-mizériát (ha nincs az exe mellett crashel a program) is megoldotta csendben (try: except:). Ezt alkalomadtán a BrAlajosba is bele kellene kompillálni. 

# BraiLuca v11 - Spline & Circle vadász + Ujjlenyomat adatbázis
# Egyesített verzió: BraiLuca v8 + Ujjlenyomatkereső funkciói

import os
import tkinter as tk
from tkinter import filedialog, messagebox
import ezdxf
import math
import sqlite3

# --- KONFIGURÁCIÓ ---
DB_PATH = (
    r"Z:\Temp\!!!INSTALL\Makrok_Hasznos_Kiskesek\BraiLucaBrAlajos\ujjlenyomatok.db"
)


# --- ADATBÁZIS INICIALIZÁLÁS ---
def init_db():
    try:
        conn = sqlite3.connect(DB_PATH)
        c = conn.cursor()
        c.execute("""CREATE TABLE IF NOT EXISTS fingerprints
                     (identifier TEXT PRIMARY KEY, fingerprint TEXT)""")
        conn.commit()
        conn.close()
    except Exception as e:
        # Ha az adatbázis nem elérhető, csak figyelmeztetünk, nem állunk le
        print(f"DB inicializálási hiba: {e}")


# --- BRAILLE MOTOR (VÁLTOZATLAN) ---
class BrailleEngine:
    def __init__(self):
        self.dot_dist = 2.5
        self.char_dist = 6.0
        self.row_dist = 10.0
        self.tolerance = 0.8
        self.b64_chars = (
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
        )

    def bits_to_base64(self, bit_string):
        b64_result = ""
        for i in range(0, 480, 6):
            six_bits = bit_string[i : i + 6]
            val = 0
            for bit_pos, bit in enumerate(six_bits):
                if bit == "1":
                    val += 1 << bit_pos
            b64_result += self.b64_chars[val]
        return b64_result

    def get_theoretical_grid(self, offset_x=0, offset_y=0):
        grid = []
        for row in range(4):
            for char in range(20):
                for dot_col in range(2):
                    for dot_row in range(3):
                        gx = (
                            (char * self.char_dist)
                            + (dot_col * self.dot_dist)
                            + offset_x
                        )
                        gy = (
                            -((row * self.row_dist) + (dot_row * self.dot_dist))
                            + offset_y
                        )
                        grid.append((gx, gy))
        return grid

    def get_best_fit(self, points):
        if not points:
            return None, None, False

        min_x = min(p[0] for p in points)
        max_y = max(p[1] for p in points)

        offsets = [(0, 0), (2.5, 0), (0, 2.5), (2.5, 2.5), (0, 5.0), (2.5, 5.0)]

        for ox, oy in offsets:
            theoretical_grid = self.get_theoretical_grid(min_x - ox, max_y + oy)
            fingerprint_bits = ["0"] * 480
            snapped_points = []
            matched_count = 0

            for px, py in points:
                for i, (gx, gy) in enumerate(theoretical_grid):
                    dist = math.sqrt((px - gx) ** 2 + (py - gy) ** 2)
                    if dist <= self.tolerance:
                        fingerprint_bits[i] = "1"
                        snapped_points.append((gx, gy))
                        matched_count += 1
                        break

            if matched_count == len(points):
                is_vertical_shifted = oy != 0
                return (
                    self.bits_to_base64("".join(fingerprint_bits)),
                    snapped_points,
                    is_vertical_shifted,
                )

        return None, None, False


# --- EGYESÍTETT GUI ---
class BraiLuca:
    def __init__(self, root):
        self.root = root
        self.root.title("BraiLuca v11 - Klisé Tisztító & Ujjlenyomat Kezelő")
        self.root.geometry("520x620")
        self.engine = BrailleEngine()
        self.snapped_points = []
        self.fingerprint = ""

        # ── Fejléc ──────────────────────────────────────────────────
        tk.Label(
            root, text="BraiLuca - Klisé Tisztító", font=("Arial", 12, "bold")
        ).pack(pady=8)

        # ── DXF megnyitás ────────────────────────────────────────────
        self.btn_open = tk.Button(
            root, text="DXF Megnyitása", command=self.load_file, height=2, width=30
        )
        self.btn_open.pack(pady=8)

        # ── Státusz keret ────────────────────────────────────────────
        self.info_frame = tk.LabelFrame(root, text="Státusz", padx=10, pady=6)
        self.info_frame.pack(pady=6, padx=20, fill="x")

        self.lbl_dots = tk.Label(self.info_frame, text="Pontok: -")
        self.lbl_dots.pack(anchor="w")

        self.lbl_fprint = tk.Label(
            self.info_frame,
            text="Ujjlenyomat: -",
            font=("Consolas", 9),
            wraplength=440,
        )
        self.lbl_fprint.pack(anchor="w")

        # Ujjlenyomat vágólapra gomb (megmarad)
        self.btn_copy = tk.Button(
            root,
            text="Ujjlenyomat másolása vágólapra",
            command=self.copy_to_clipboard,
            state="disabled",
        )
        self.btn_copy.pack(pady=4)

        # ── Adatbázis találat keret ──────────────────────────────────
        db_frame = tk.LabelFrame(root, text="Adatbázis találat", padx=10, pady=6)
        db_frame.pack(pady=6, padx=20, fill="x")

        self.entry_res = tk.Entry(
            db_frame,
            width=60,
            font=("Arial", 10, "bold"),
            justify="center",
            state="readonly",
        )
        self.entry_res.pack(pady=4)

        # ── Új azonosító rögzítése ───────────────────────────────────
        save_frame = tk.LabelFrame(root, text="Új azonosító rögzítése", padx=10, pady=6)
        save_frame.pack(pady=6, padx=20, fill="x")

        id_row = tk.Frame(save_frame)
        id_row.pack(fill="x")
        tk.Label(id_row, text="Azonosító:").pack(side="left")
        self.entry_new_id = tk.Entry(id_row, width=35)
        self.entry_new_id.pack(side="left", padx=6)

        self.btn_save_db = tk.Button(
            save_frame,
            text="MENTÉS ÚJ AZONOSÍTÓKÉNT",
            command=self.save_new_to_db,
            bg="#d4edda",
            state="disabled",
        )
        self.btn_save_db.pack(pady=4)

        # ── DXF mentés ───────────────────────────────────────────────
        self.btn_save_dxf = tk.Button(
            root,
            text="Tökéletes DXF mentése (Circle)",
            command=self.save_cleaned_dxf,
            state="disabled",
            bg="#4CAF50",
            fg="white",
            height=2,
        )
        self.btn_save_dxf.pack(pady=10)

    # ── SEGÉDFÜGGVÉNYEK (VÁLTOZATLAN) ───────────────────────────────

    def get_points_from_entity(self, entity, points_list):
        dxftype = entity.dxftype()

        if dxftype == "CIRCLE":
            cp = entity.dxf.center
            points_list.append((float(cp.x), float(cp.y)))

        elif dxftype == "SPLINE":
            cp = entity.control_points
            if cp:
                avg_x = sum(p[0] for p in cp) / len(cp)
                avg_y = sum(p[1] for p in cp) / len(cp)
                points_list.append((avg_x, avg_y))

        elif dxftype == "INSERT":
            for sub_entity in entity.virtual_entities():
                self.get_points_from_entity(sub_entity, points_list)

    # ── FÁJL BETÖLTÉS (logika változatlan, + auto DB lekérdezés) ────

    def load_file(self):
        path = filedialog.askopenfilename(filetypes=[("DXF fájlok", "*.dxf")])
        if not path:
            return

        filename = os.path.basename(path)

        try:
            doc = ezdxf.readfile(path)
            msp = doc.modelspace()
            all_raw_points = []

            for entity in msp:
                self.get_points_from_entity(entity, all_raw_points)

            if not all_raw_points:
                messagebox.showwarning(
                    "Figyelem", "Nem találtam sem SPLINE, sem CIRCLE elemet a fájlban!"
                )
                return

            # Deduplikáció (változatlan)
            unique_points = []
            dup_count = 0
            for p in all_raw_points:
                is_duplicate = False
                for u in unique_points:
                    if math.dist(p, u) < 0.1:
                        is_duplicate = True
                        break
                if is_duplicate:
                    dup_count += 1
                else:
                    unique_points.append(p)

            status_text = f"Fájl: {filename}\nPontok: {len(unique_points)} db"
            if dup_count > 0:
                status_text += f" ({dup_count} duplikátum törölve)"
            self.lbl_dots.config(text=status_text, fg="black")

            fprint, snapped, is_shifted = self.engine.get_best_fit(unique_points)

            if fprint:
                self.fingerprint = fprint
                self.snapped_points = snapped

                if is_shifted:
                    display_text = f"Ujjlenyomat: {self.fingerprint}\n⚠️ FÜGGŐLEGES ELTOLÁS! Célgépen kézi bevitel szükséges!"
                    self.lbl_fprint.config(text=display_text, fg="red")
                else:
                    self.lbl_fprint.config(
                        text=f"Ujjlenyomat: {self.fingerprint}", fg="black"
                    )

                self.btn_copy.config(state="normal")
                self.btn_save_dxf.config(state="normal")
                self.btn_save_db.config(state="normal")

                # ── ÚJ: automatikus DB lekérdezés ──
                self._search_in_db(self.fingerprint)

            else:
                self.lbl_fprint.config(text="HIBA: Nem illeszkedik a rácsra!", fg="red")
                self.btn_save_dxf.config(state="disabled")
                self.btn_save_db.config(state="disabled")
                self._clear_db_result()

        except Exception as e:
            messagebox.showerror("Hiba", f"Beolvasási hiba: {str(e)}")

    # ── VÁGÓLAP (VÁLTOZATLAN) ────────────────────────────────────────

    def copy_to_clipboard(self):
        self.root.clipboard_clear()
        self.root.clipboard_append(self.fingerprint)
        messagebox.showinfo("Siker", "Vágólapra másolva!")

    # ── DB LEKÉRDEZÉS (a második programból áthozva, metódusba szervezve) ──

    def _clear_db_result(self):
        self.entry_res.config(state="normal")
        self.entry_res.delete(0, tk.END)
        self.entry_res.config(state="readonly")

    def _search_in_db(self, fingerprint):
        """Lekérdezi az ujjlenyomatot az adatbázisból és megjeleníti az eredményt."""
        self._clear_db_result()
        try:
            conn = sqlite3.connect(DB_PATH)
            c = conn.cursor()
            c.execute(
                "SELECT identifier FROM fingerprints WHERE fingerprint = ?",
                (fingerprint,),
            )
            results = c.fetchall()
            conn.close()

            self.entry_res.config(state="normal")
            if results:
                ids = ", ".join([r[0] for r in results])
                self.entry_res.insert(0, ids)
                self.entry_res.config(fg="navy")
            else:
                self.entry_res.insert(0, "NINCS MÉG ILYEN UJJLENYOMAT")
                self.entry_res.config(fg="gray")
            self.entry_res.config(state="readonly")

        except Exception as e:
            self.entry_res.config(state="normal")
            self.entry_res.insert(0, f"DB hiba: {e}")
            self.entry_res.config(state="readonly", fg="red")

    # ── DB MENTÉS (a második programból áthozva) ─────────────────────

    def save_new_to_db(self):
        """Elmenti az ujjlenyomatot az adatbázisba az új azonosítóval.
        Visszatér True-val ha sikeres, False-szal ha nem."""
        fp = self.fingerprint
        new_id = self.entry_new_id.get().strip()

        if not new_id:
            messagebox.showerror("Hiba", "Kérlek add meg az azonosítót!")
            return False
        if len(fp) != 80:
            messagebox.showerror("Hiba", "Érvénytelen ujjlenyomat (nem 80 karakter)!")
            return False

        try:
            conn = sqlite3.connect(DB_PATH)
            c = conn.cursor()

            # Duplikátum figyelmeztetés (ha ez az ujjlenyomat már létezik)
            c.execute(
                "SELECT identifier FROM fingerprints WHERE fingerprint = ?", (fp,)
            )
            exists = c.fetchall()
            if exists:
                msg = (
                    f"Ez az ujjlenyomat már szerepel a következő azonosítókkal: "
                    f"{', '.join([x[0] for x in exists])}\n\n"
                    f"Biztosan felveszed új azonosítóval ({new_id}) is?"
                )
                if not messagebox.askyesno("Duplikátum figyelmeztetés", msg):
                    conn.close()
                    return False

            c.execute(
                "INSERT INTO fingerprints (identifier, fingerprint) VALUES (?, ?)",
                (new_id, fp),
            )
            conn.commit()
            conn.close()

            messagebox.showinfo("Siker", f"'{new_id}' elmentve az adatbázisba!")
            self._search_in_db(fp)  # Frissítjük a találat mezőt
            return True

        except sqlite3.IntegrityError:
            messagebox.showerror(
                "Hiba",
                "Ez az AZONOSÍTÓ már foglalt! Minden ujjlenyomatnak egyedi azonosító kell.",
            )
            return False
        except Exception as e:
            messagebox.showerror("Hiba", f"DB mentési hiba: {str(e)}")
            return False

    # ── DXF MENTÉS (Circle logika változatlan, mentési rész bővítve) ─

    def save_cleaned_dxf(self):
        path = filedialog.asksaveasfilename(
            defaultextension=".dxf", filetypes=[("DXF fájlok", "*.dxf")]
        )
        if not path:
            return

        new_doc = ezdxf.new("R2000")

        try:
            new_doc.header["$INSUNITS"] = 4
            new_doc.header["$MEASUREMENT"] = 1
        except:
            try:
                new_doc.header.set("$INSUNIT", 4)
            except:
                pass

        new_doc.layers.new(name="Layer1", dxfattribs={"color": 7})
        msp = new_doc.modelspace()

        for p in self.snapped_points:
            msp.add_circle(p, radius=0.8, dxfattribs={"layer": "Layer1"})

        new_doc.saveas(path)
        messagebox.showinfo("Siker", "A klisé tisztítva és körökké alakítva mentve!")


# ── BELÉPÉSI PONT ────────────────────────────────────────────────────

if __name__ == "__main__":
    init_db()
    root = tk.Tk()
    try:
        root.iconbitmap("BrLu.ico")
    except:
        pass  # Ha nincs ico fájl, csendben továbblépünk
    app = BraiLuca(root)
    root.mainloop()