chiark / gitweb /
WIP yppsc-parsedb-updatereceiver; wip pipeval
[ypp-sc-tools.db-test.git] / pctb / ocr.c
1 /*
2  * Core OCR algorithm (first exact bitmap match)
3  */
4 /*
5  *  This is part of ypp-sc-tools, a set of third-party tools for assisting
6  *  players of Yohoho Puzzle Pirates.
7  * 
8  *  Copyright (C) 2009 Ian Jackson <ijackson@chiark.greenend.org.uk>
9  * 
10  *  This program is free software: you can redistribute it and/or modify
11  *  it under the terms of the GNU General Public License as published by
12  *  the Free Software Foundation, either version 3 of the License, or
13  *  (at your option) any later version.
14  * 
15  *  This program is distributed in the hope that it will be useful,
16  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  *  GNU General Public License for more details.
19  * 
20  *  You should have received a copy of the GNU General Public License
21  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  * 
23  *  Yohoho and Puzzle Pirates are probably trademarks of Three Rings and
24  *  are used without permission.  This program is not endorsed or
25  *  sponsored by Three Rings.
26  */
27
28 #include "ocr.h"
29
30 typedef struct {
31   Pixcol col;
32   struct DatabaseNode *then;
33 } DatabaseLink;
34
35 #define MAXGLYPHCHRS 7
36
37 typedef struct DatabaseNode {
38   char s[MAXGLYPHCHRS+1]; /* null-terminated; "" means no match here */
39   int nlinks, alinks;
40   unsigned endsword:1;
41   DatabaseLink *links;
42 } DatabaseNode;
43
44 static const char *context_names[]= {
45   "Lower",
46   "Upper",
47   "Digit"
48 };
49 struct OcrCellTypeInfo {
50   /* bitmaps of indices into context_names: */
51   unsigned initial, nextword, midword;
52   int space_spaces;
53   const char *name;
54 };
55 const struct OcrCellTypeInfo ocr_celltype_number= {
56   4,4,4,
57   .space_spaces= 5,
58   .name= "number"
59 };
60 const struct OcrCellTypeInfo ocr_celltype_text= {
61   .initial=2, /* Uppercase */
62   .nextword=3, /* Either */
63   .midword=1, /* Lower only */
64   .space_spaces= 4,
65   .name= "text"
66 };
67
68
69 #define NCONTEXTS (sizeof(context_names)/sizeof(context_names[0]))
70
71 struct OcrReader {
72   int h;
73   DatabaseNode contexts[NCONTEXTS];
74   OcrResultGlyph *results;
75   int aresults, nresults;
76 };
77
78 DEBUG_DEFINE_DEBUGF(ocr)
79
80 #define FGETSLINE (dbfile_getsline(lbuf,sizeof(lbuf),__FILE__,__LINE__))
81
82 static void cleardb_node(DatabaseNode *n) {
83   int i;
84   n->s[0]= 0;
85   for (i=0; i<n->nlinks; i++)
86     cleardb_node(n->links[i].then);
87 }
88
89 static void readdb(OcrReader *rd) {
90   int nchrs;
91   DatabaseNode *current, *additional;
92   char chrs[MAXGLYPHCHRS+1];
93   Pixcol cv;
94   int j,ctxi;
95   int h, endsword;
96   char lbuf[100];
97
98   for (ctxi=0; ctxi<NCONTEXTS; ctxi++)
99     cleardb_node(&rd->contexts[ctxi]);
100
101   char *dbfname=0;
102   asprintf(&dbfname,"%s/charset-%d.txt",get_vardir(),rd->h);
103   sysassert(dbfname);
104   
105   if (!dbfile_open(dbfname))
106     goto x;
107
108   FGETSLINE;
109   dbassert(!strcmp(lbuf,"# ypp-sc-tools pctb font v1"));
110
111   dbassert( dbfile_scanf("%d", &h) == 1);
112   dbassert(h==rd->h);
113
114   for (;;) {
115     FGETSLINE;
116     if (!lbuf[0] || lbuf[0]=='#') continue;
117     if (!strcmp(lbuf,".")) break;
118
119     for (ctxi=0; ctxi<NCONTEXTS; ctxi++)
120       if (!strcmp(lbuf,context_names[ctxi]))
121         goto found_ctx;
122     /* not found, just skip */
123     for (;;) { FGETSLINE; if (!lbuf[0]) break; }
124     continue;
125
126   found_ctx:
127     for (nchrs=0;;) {
128       int c= fgetc(dbfile); sysassert(!ferror(dbfile)); dbassert(c!=EOF);
129       if (c=='\n') { dbassert(nchrs); break; }
130       dbassert(nchrs<MAXGLYPHCHRS);
131       if (c=='\\') {
132         unsigned cr;
133         c= fgetc(dbfile); sysassert(!ferror(dbfile)); dbassert(c=='x');
134         dbassert( dbfile_scanf("%2x", &cr) == 1);
135         assert(cr>0 && cr<=255);
136         c= cr;
137       }
138       chrs[nchrs++]= c;
139     }
140     endsword= 0;
141     if (nchrs>1 && chrs[nchrs-1]==' ') {
142       endsword= 1;
143       nchrs--;
144     }
145     chrs[nchrs]= 0;
146
147     current= &rd->contexts[ctxi];
148     for (;;) {
149       FGETSLINE;
150       if (!lbuf[0]) { dbassert(current != &rd->contexts[ctxi]); break; }
151       char *ep;
152       cv= strtoul(lbuf,&ep,16);  dbassert(!*ep);
153       dbassert(!(cv & ~((1UL << rd->h)-1)));
154       
155       for (j=0; j<current->nlinks; j++)
156         if (current->links[j].col == cv) {
157           current= current->links[j].then;
158           goto found_link;
159         }
160
161       additional= mmalloc(sizeof(*additional));
162       additional->s[0]= 0;
163       additional->nlinks= additional->alinks= 0;
164       additional->links= 0;
165       if (current->nlinks==current->alinks) {
166         current->alinks++;
167         current->alinks<<=1;
168         current->links= mrealloc(current->links,
169                                  sizeof(*current->links) * current->alinks);
170       }
171       current->links[current->nlinks].col= cv;
172       current->links[current->nlinks].then= additional;
173       current->nlinks++;
174       current= additional;
175
176     found_link:;
177     }
178
179     dbassert(!current->s[0]);
180     strcpy(current->s, chrs);
181     current->endsword= endsword;
182   }
183  x:
184   dbfile_close();
185   free(dbfname);
186 }
187
188 static void cu_pr_ctxmap(FILE *resolver, unsigned ctxmap) {
189   fprintf(resolver,"{");
190   const char *spc="";
191   int ctxi;
192   for (ctxi=0; ctxi<NCONTEXTS; ctxi++) {
193     if (!(ctxmap & (1u << ctxi))) continue;
194     fprintf(resolver,"%s%s",spc,context_names[ctxi]);
195     spc=" ";
196   }
197   fprintf(resolver,"}");
198 }
199
200 static void callout_unknown(OcrReader *rd, int w, Pixcol cols[],
201                             int unk_l, int unk_r, unsigned unk_ctxmap) {
202   int c,i, x,y;
203   const OcrResultGlyph *s;
204   const char *p;
205   Pixcol pv;
206
207   FILE *resolver= resolve_start();
208   if (!resolver)
209     fatal("OCR failed - unrecognised characters or ligatures.\n"
210           "Character set database needs to be updated or augmented.\n"
211           "See README.charset.\n");
212   
213   fprintf(resolver,
214           "char\n"
215           "%d %d ",unk_l,unk_r);
216   cu_pr_ctxmap(resolver,unk_ctxmap);
217   for (i=0, s=rd->results; i<rd->nresults; i++, s++) {
218     if (!strcmp(s->s," ")) continue;
219     fprintf(resolver," %d %d ",s->l,s->r);
220     cu_pr_ctxmap(resolver,s->ctxmap);
221     fprintf(resolver," ");
222     for (p=s->s; (c= *p); p++) {
223       if (c=='\\') fprintf(resolver,"\\%c",c);
224       else if (c>=33 && c<=126) fputc(c,resolver);
225       else fprintf(resolver,"\\x%02x",(unsigned char)c);
226     }
227   }
228   fputc('\n',resolver);
229
230   fprintf(resolver,
231           "/* XPM */\n"
232           "static char *t[] = {\n"
233           "/* columns rows colors chars-per-pixel */\n"
234           "\"%d %d 2 1\",\n"
235           "\"  c black\",\n"
236           "\"o c white\",\n",
237           w,rd->h);
238   for (y=0, pv=1; y<rd->h; y++, pv<<=1) {
239     fputc('"',resolver);
240     for (x=0; x<w; x++)
241       fputc(cols[x] & pv ? 'o' : ' ', resolver);
242     fputs("\",\n",resolver);
243   }
244   fputs("};\n",resolver);
245
246   resolve_finish();
247   readdb(rd);
248 }
249
250 static void add_result(OcrReader *rd, const char *s, int l, int r,
251                        unsigned ctxmap) {
252   if (rd->nresults >= rd->aresults) {
253     rd->aresults++; rd->aresults<<=1;
254     rd->results= mrealloc(rd->results, sizeof(*rd->results)*rd->aresults);
255   }
256   rd->results[rd->nresults].s= s;
257   rd->results[rd->nresults].l= l;
258   rd->results[rd->nresults].r= r;
259   rd->results[rd->nresults].ctxmap= ctxmap;
260   rd->nresults++;
261 }
262
263
264 const char *ocr_celltype_name(OcrCellType ct) { return ct->name; }
265
266 OcrResultGlyph *ocr(OcrReader *rd, OcrCellType ct, int w, Pixcol cols[]) {
267   int nspaces;
268   unsigned ctxmap;
269   int ctxi, i, x;
270
271  restart:
272
273   nspaces=- w;
274   ctxmap= ct->initial;
275   rd->nresults=0;
276   debugf("OCR h=%d w=%d",rd->h,w);
277   for (x=0; x<w; x++) debugf(" %"PSPIXCOL(PRIx),cols[x]);
278   debugf("\n");
279   debug_flush();
280
281   x=0;
282   for (;;) {
283     debug_flush();
284     /* skip spaces */
285     if (x>=w)
286       break;
287
288     if (!cols[x]) {
289       nspaces++;
290       x++;
291       if (nspaces == ct->space_spaces) {
292         debugf("OCR  x=%x nspaces=%d space\n",x,nspaces);
293         ctxmap= ct->nextword;
294       }
295       continue;
296     }
297
298     /* something here, so we need to add the spaces */
299     if (nspaces >= ct->space_spaces)
300       add_result(rd," ",x-nspaces,x+1,0);
301     nspaces=0;
302
303     /* find character */
304     int lx=x;
305
306     DatabaseNode *uniquematch= 0;
307     int uniquematch_rx=-1;
308     
309     debugf("OCR  lx=%d ctxmap=%x  ",lx,ctxmap);
310
311     for (ctxi=0; ctxi<NCONTEXTS; ctxi++) {
312       DatabaseNode *current= &rd->contexts[ctxi];;
313       DatabaseNode *bestmatch= 0;
314       int bestmatch_rx=-1;
315
316       x= lx;
317       if (!(ctxmap & (1u << ctxi))) continue;
318       debugf(" || %s",context_names[ctxi]);
319
320       for (;;) {
321         debug_flush();
322         debugf(" | x=%d",x);
323         if (x>w) break;
324         Pixcol cv= cols[x];
325         debugf(" cv=%"PSPIXCOL(PRIx),cv);
326         for (i=0; i<current->nlinks; i++)
327           if (current->links[i].col == cv)
328             goto found;
329         /* not found */
330         debugf(" ?");
331         break;
332
333       found:
334         current= current->links[i].then;
335         if (current->s[0]) {
336           debugf(" \"%s\"%s",current->s,current->endsword?"_":"");
337           bestmatch= current;
338           bestmatch_rx= x;
339         } else {
340           debugf(" ...");
341         }
342
343         x++;
344       }
345       
346       if (bestmatch) {
347         if (uniquematch && strcmp(bestmatch->s, uniquematch->s)) {
348           debugf( " ambiguous");
349           uniquematch= 0;
350           break;
351         }
352         uniquematch= bestmatch;
353         uniquematch_rx= bestmatch_rx;
354       }
355     }
356
357     if (uniquematch) {
358       debugf(" || YES");
359       add_result(rd, uniquematch->s, lx, uniquematch_rx, ctxmap);
360       x= uniquematch_rx+1;
361       if (uniquematch->s[0]) ctxmap= ct->midword;
362       else debugf(" (empty)");
363       if (uniquematch->endsword) {
364         nspaces= ct->space_spaces;
365         debugf("_");
366         ctxmap= ct->nextword;
367       }
368       debugf("\n");
369     } else {
370       int rx;
371       debugf(" || UNKNOWN");
372       for (rx=lx; rx<w && cols[rx]; rx++);
373       debugf(" x=%d ctxmap=%x %d..%d\n",x, ctxmap, lx,rx);
374       debug_flush();
375       callout_unknown(rd, w,cols, lx,rx-1, ctxmap);
376       goto restart;
377     }
378   }
379   add_result(rd, 0,-1,-1,0);
380   debugf("OCR  finished %d glyphs\n",rd->nresults);
381   debug_flush();
382   return rd->results;
383 }
384
385 OcrReader *ocr_init(int h) {
386   OcrReader *rd;
387
388   rd= mmalloc(sizeof(*rd));
389   memset(rd,0,sizeof(*rd));
390   rd->h= h;
391   readdb(rd);
392   return rd;
393 }