Preskoči na sadržaj

Štampanje logoa na termalnom štampaču za priznanice iz React Nativea

Termalni štampač prima samo tačke, pa logo mora postati jednobitna bitmapa, spakovana po osam tačaka u bajt i ručno pozicionirana u jedinicama papira, bez pregleda prije štampe.

5 min čitanja7 sekcija

Godine 2022. radio sam na Android platnim terminalima za servis prekograničnih novčanih doznaka. Primalac podiže gotovinu na benzinskoj pumpi pomoću koda s kupona, a terminal taj kupon štampa. Preko stotinu tih terminala završilo je na terenu, zaključano na jednu aplikaciju i ažurirano preko mreže.

Najveći dio tog projekta ticao se kontrole: launcher koji je zamijenio Android početni ekran, ažuriranja koja su mogla pasti, a da to niko ne primijeti, kupon koji ne važi dok sistem ne kaže da važi. Ovdje pišem o najmanjem dijelu tog projekta, za koji sam očekivao da će mi trebati jedno popodne: kako odštampati logo na priznanici.

01

Uređaj

Terminal je bio kineski uređaj s Androidom. Dokumentacija za njegov SDK bila je oskudna i na španskom, pa sam je preveo da bih uopšte mogao raditi s njom. Funkcijama terminala, od kartice i štampača do ostalog hardvera, React Native je pristupao preko Turbo Modulea napisanog u Javi.

Štampač je, dakle, bio dostupan, a štampanje teksta nije bilo problem. Problem je počeo kad se u dizajnu pojavio logo.

02

Štampač za priznanice prima samo tačke

Štampač za priznanice ne zna za slike, fontove ne poznaje onako kako ih poznaje browser, a raspored mu ne znači ništa. Prima samo tačke.

Sve ostalo, od same ideje slike i margina do toga da nešto bude centrirano, morate sami složiti od tačaka. Na ekranu telefona logo je PNG koji ubacite u komponentu i pustite layout engine da ga smjesti. Na papiru nema layout enginea. Postoji red grijaćih elemenata i traka papira koja prolazi pored njega.

U toj razlici je poteškoća: skoro svaka navika koju web ili mobilni developer ima kad radi sa slikama pretpostavlja da sliku iscrtava renderer, a na ovom hardveru ga nema.

03

Malo opšte pozadine

Ono što slijedi je opšte znanje o termalnim štampačima, a ne opis terminala koji smo koristili. Navodim ga jer objašnjava zašto ovaj posao izgleda ovako.

Termalna glava za štampanje je fiksni red tačaka preko širine papira. Svaka tačka je ili zagrijana ili nije, pa je svaki odštampani piksel jedan bit: crno ili ništa. Nema sive i nema boje.

Štampači iz porodice ESC/POS, kao i mnogi SDK-ovi proizvođača koji služe kao omotač oko njih, slike uglavnom primaju kao raster: niz redova u kojem je svaki red spakovan u bajtove, osam tačaka po bajtu. Širinu određuje glava, a visina je onoliko redova koliko pošaljete.

Zato logo, prije nego što išta stigne do štampača, mora proći kroz tri koraka koja ekran od vas nikad ne bi tražio:

  1. Skaliranje na širinu mjerenu u tačkama štampača, a ne u pointima ili pikselima
  2. Svođenje na jedan bit po pikselu, što je odluka o tome šta se računa kao crno
  3. Pakovanje u format reda koji štampač ili njegov SDK očekuje
04

Pretvaranje logotipa u tačke

Na drugom koraku logotipi krenu po zlu, jer logo dizajniran za ekran skoro uvijek ima anti-aliasing na ivicama, meke sjene ili boju. Svedeni na jedan bit, ti detalji postaju nazubljene konture ili sive mrlje koje se odštampaju kao šum.

Najjednostavnije je postaviti prag osvijetljenosti. Skica ideje u Javi, na Androidu:

Snippet.java
Java · 18 linija
static boolean[][] toMonochrome(Bitmap src, int threshold) {    int w = src.getWidth();    int h = src.getHeight();    boolean[][] dots = new boolean[h][w];     for (int y = 0; y < h; y++) {        for (int x = 0; x < w; x++) {            int c = src.getPixel(x, y);            int alpha = Color.alpha(c);            int lum = (Color.red(c) * 299                     + Color.green(c) * 587                     + Color.blue(c) * 114) / 1000;            // transparent counts as paper, not as ink            dots[y][x] = alpha > 127 && lum < threshold;        }    }    return dots;}

I pakovanje jednog reda u bajtove, po osam tačaka, počevši od najznačajnijeg bita:

Snippet.java
Java · 9 linija
static byte[] packRow(boolean[] row) {    byte[] out = new byte[(row.length + 7) / 8];    for (int x = 0; x < row.length; x++) {        if (row[x]) {            out[x / 8] |= (byte) (0x80 >> (x % 8));        }    }    return out;}

Ništa od ovoga nije posebno domišljato. Kod je kratak, a dobru priznanicu vam i dalje ne daje. Linija koja provjerava prozirnost je važna jer prozirna pozadina nema vlastitu boju, a konverter koji je tretira kao tamnu štampa pun crni pravougaonik.

Bolje rješenje treba tražiti prije koda: logo koji je od početka dizajniran u jednom bitu bolji je od bilo kakve konverzije u trenutku štampe. Ako od onoga ko je vlasnik brenda možete dobiti jednobitnu verziju logotipa, prag prestaje biti važan.

05

Ručno pozicioniranje, bez pregleda

Dobiti prave tačke je pola posla. Druga polovina je staviti ih na pravo mjesto.

Na ekranu logo centrirate jednim svojstvom. Na priznanici ga pozicionirate ručno, u jedinicama papira: gdje počinje po širini, koliko prostora ima iznad i ispod njega, kako se poravnava s tekstom koji slijedi. Nema ni pregleda: nema inspektora, nema hot reloada i nema se šta vidjeti dok papir ne izađe.

Ostaje samo jedan način: štampati i mjeriti. Promijenite vrijednost, odštampajte kupon, prislonite ga uz lenjir ili uz prethodni kupon, pa ponovo promijenite vrijednost. Zvuči trivijalno dok ne probate, a onda vam ode popodne i sto se napuni papirom.

06

Gdje držati mjere za štampu

Ovdje se štampanje povezuje s ostatkom projekta.

Na tom terminalu pristup je bio isti kao na svakom hardveru koji sam povezivao s React Nativeom: dio koji zavisi od uređaja napisati nativno, JavaScriptu dati uzak interfejs i paziti da detalji platforme ne procure u gornje slojeve. Širine u tačkama, pomaci u jedinicama papira i pragovi su detalji platforme. Vezani su za ovaj štampač i ovaj papir.

Zato bih JavaScript strani dao otprilike ovakav interfejs, u obliku Turbo Module specifikacije:

snippet.ts
TypeScript · 8 linija
import type { TurboModule } from 'react-native'import { TurboModuleRegistry } from 'react-native' export interface Spec extends TurboModule {  printCoupon(code: string, lines: ReadonlyArray<string>): Promise<void>} export default TurboModuleRegistry.getEnforcing<Spec>('TerminalPrinter')

JavaScript kaže šta treba odštampati. Nativna strana zna kako, uključujući i gdje ide logo i koliko je velik. Da su mjere u JavaScriptu, svaki ekran koji išta štampa imao bi brojke koje važe samo za jedan uređaj, a uz promjenu papira ili terminala morali biste ih tražiti po cijeloj aplikaciji. Ako taj interfejs prema JavaScriptu pogrešno postavite, to plaća svaka funkcionalnost koju kasnije dodate.

07

Šta bih rekao nekome ko počinje

Zatražite jednobitni logo prije nego što napišete konverter. Tako najtežu odluku izbacujete iz koda.

Prvo štampanje tretirajte kao mjerenje. Odštampajte nešto čije dimenzije znate, izmjerite to i zapišite brojke pored koda koji ih koristi. Kalibrisat ćete štampanjem u svakom slučaju, pa to uradite jednom i namjerno, umjesto četrdeset puta slučajno.

Svaku jedinicu papira držite na nativnoj strani. JavaScript interfejs ne treba ni znati da papir postoji.

Problem je mali, ali dijelovi projekta koji zvuče trivijalno često su upravo oni na koje ne možete primijeniti nijedan od uobičajenih alata.

Kako je nastao ovaj tekstRad na Android platnim terminalima za servis prekograničnih novčanih doznaka. Preko stotinu uređaja bilo je zaključano na jednu aplikaciju i ažurirano preko mreže.Pročitajte više o projektu →Povezana uslugaHardver u React NativeuPogledajte uslugu →Sljedeći tekstNaš dizajn sistem upakovan kao skill ubrzao je AI agenta barem dvostrukoPročitajte →

Blog

Ako radite na nečemu sličnom, javite nam se s detaljima.

Prvi poziv traje 30 minuta i ni na šta vas ne obavezuje.

Opišite nam svoj projekatcontact@microtech.consulting