Commit | Line | Data |
---|---|---|
02d1d628 AMH |
1 | #include "image.h" |
2 | ||
3 | #include <sys/types.h> | |
4 | #include <sys/stat.h> | |
5 | #include <fcntl.h> | |
6 | ||
7 | #include <stdio.h> | |
8 | #include <stdlib.h> | |
9 | ||
10 | ||
11 | ||
12 | ||
13 | ||
14 | ||
15 | /* | |
16 | =head1 NAME | |
17 | ||
18 | font.c - implements font handling functions for t1 and truetype fonts | |
19 | ||
20 | =head1 SYNOPSIS | |
21 | ||
22 | i_init_fonts(); | |
23 | ||
24 | #ifdef HAVE_LIBT1 | |
25 | fontnum = i_t1_new(path_to_pfb, path_to_afm); | |
26 | i_t1_bbox(fontnum, points, "foo", 3, int cords[6]); | |
27 | rc = i_t1_destroy(fontnum); | |
28 | #endif | |
29 | ||
30 | #ifdef HAVE_LIBTT | |
31 | handle = i_tt_new(path_to_ttf); | |
32 | rc = i_tt_bbox(handle, points, "foo", 3, int cords[6]); | |
33 | i_tt_destroy(handle); | |
34 | ||
35 | // and much more | |
36 | ||
37 | =head1 DESCRIPTION | |
38 | ||
39 | font.c implements font creation, rendering, bounding box functions and | |
40 | more for Imager. | |
41 | ||
42 | =head1 FUNCTION REFERENCE | |
43 | ||
44 | Some of these functions are internal. | |
45 | ||
46 | =over 4 | |
47 | ||
48 | =cut | |
49 | ||
50 | */ | |
51 | ||
52 | ||
53 | ||
54 | ||
55 | ||
56 | ||
57 | ||
58 | ||
59 | ||
60 | /* | |
61 | =item i_init_fonts() | |
62 | ||
63 | Initialize font rendering libraries if they are avaliable. | |
64 | ||
65 | =cut | |
66 | */ | |
67 | ||
68 | undef_int | |
69 | i_init_fonts() { | |
70 | mm_log((1,"Initializing fonts\n")); | |
71 | ||
72 | #ifdef HAVE_LIBT1 | |
73 | init_t1(); | |
74 | #endif | |
75 | ||
76 | #ifdef HAVE_LIBTT | |
77 | init_tt(); | |
78 | #endif | |
79 | ||
faa9b3e7 TC |
80 | #ifdef HAVE_FT2 |
81 | if (!i_ft2_init()) | |
82 | return 0; | |
83 | #endif | |
84 | ||
02d1d628 AMH |
85 | return(1); /* FIXME: Always true - check the return values of the init_t1 and init_tt functions */ |
86 | } | |
87 | ||
88 | ||
89 | ||
90 | ||
91 | #ifdef HAVE_LIBT1 | |
92 | ||
93 | ||
94 | ||
95 | /* | |
96 | =item i_init_t1() | |
97 | ||
98 | Initializes the t1lib font rendering engine. | |
99 | ||
100 | =cut | |
101 | */ | |
102 | ||
103 | undef_int | |
104 | init_t1() { | |
105 | mm_log((1,"init_t1()\n")); | |
106 | if ((T1_InitLib(LOGFILE|IGNORE_CONFIGFILE|IGNORE_FONTDATABASE) == NULL)){ | |
107 | mm_log((1,"Initialization of t1lib failed\n")); | |
108 | return(1); | |
109 | } | |
110 | T1_SetLogLevel(T1LOG_DEBUG); | |
111 | i_t1_set_aa(1); /* Default Antialias value */ | |
112 | return(0); | |
113 | } | |
114 | ||
115 | ||
116 | /* | |
117 | =item i_close_t1() | |
118 | ||
119 | Shuts the t1lib font rendering engine down. | |
120 | ||
121 | This it seems that this function is never used. | |
122 | ||
123 | =cut | |
124 | */ | |
125 | ||
126 | void | |
faa9b3e7 | 127 | i_close_t1(void) { |
02d1d628 AMH |
128 | T1_CloseLib(); |
129 | } | |
130 | ||
131 | ||
132 | /* | |
133 | =item i_t1_new(pfb, afm) | |
134 | ||
135 | Loads the fonts with the given filenames, returns its font id | |
136 | ||
137 | pfb - path to pfb file for font | |
138 | afm - path to afm file for font | |
139 | ||
140 | =cut | |
141 | */ | |
142 | ||
143 | int | |
144 | i_t1_new(char *pfb,char *afm) { | |
145 | int font_id; | |
146 | mm_log((1,"i_t1_new(pfb %s,afm %s)\n",pfb,(afm?afm:"NULL"))); | |
147 | font_id = T1_AddFont(pfb); | |
148 | if (font_id<0) { | |
149 | mm_log((1,"i_t1_new: Failed to load pfb file '%s' - return code %d.\n",pfb,font_id)); | |
150 | return font_id; | |
151 | } | |
152 | ||
153 | if (afm != NULL) { | |
154 | mm_log((1,"i_t1_new: requesting afm file '%s'.\n",afm)); | |
155 | if (T1_SetAfmFileName(font_id,afm)<0) mm_log((1,"i_t1_new: afm loading of '%s' failed.\n",afm)); | |
156 | } | |
157 | return font_id; | |
158 | } | |
159 | ||
160 | /* | |
161 | =item i_t1_destroy(font_id) | |
162 | ||
163 | Frees resources for a t1 font with given font id. | |
164 | ||
165 | font_id - number of the font to free | |
166 | ||
167 | =cut | |
168 | */ | |
169 | ||
170 | int | |
171 | i_t1_destroy(int font_id) { | |
172 | mm_log((1,"i_t1_destroy(font_id %d)\n",font_id)); | |
173 | return T1_DeleteFont(font_id); | |
174 | } | |
175 | ||
176 | ||
177 | /* | |
178 | =item i_t1_set_aa(st) | |
179 | ||
180 | Sets the antialiasing level of the t1 library. | |
181 | ||
182 | st - 0 = NONE, 1 = LOW, 2 = HIGH. | |
183 | ||
184 | =cut | |
185 | */ | |
186 | ||
187 | void | |
188 | i_t1_set_aa(int st) { | |
189 | int i; | |
190 | unsigned long cst[17]; | |
191 | switch(st) { | |
192 | case 0: | |
193 | T1_AASetBitsPerPixel( 8 ); | |
194 | T1_AASetLevel( T1_AA_NONE ); | |
195 | T1_AANSetGrayValues( 0, 255 ); | |
196 | mm_log((1,"setting T1 antialias to none\n")); | |
197 | break; | |
198 | case 1: | |
199 | T1_AASetBitsPerPixel( 8 ); | |
200 | T1_AASetLevel( T1_AA_LOW ); | |
201 | T1_AASetGrayValues( 0,65,127,191,255 ); | |
202 | mm_log((1,"setting T1 antialias to low\n")); | |
203 | break; | |
204 | case 2: | |
205 | T1_AASetBitsPerPixel(8); | |
206 | T1_AASetLevel(T1_AA_HIGH); | |
207 | for(i=0;i<17;i++) cst[i]=(i*255)/16; | |
208 | T1_AAHSetGrayValues( cst ); | |
209 | mm_log((1,"setting T1 antialias to high\n")); | |
210 | } | |
211 | } | |
212 | ||
213 | ||
214 | /* | |
215 | =item i_t1_cp(im, xb, yb, channel, fontnum, points, str, len, align) | |
216 | ||
217 | Interface to text rendering into a single channel in an image | |
218 | ||
219 | im pointer to image structure | |
220 | xb x coordinate of start of string | |
221 | yb y coordinate of start of string ( see align ) | |
222 | channel - destination channel | |
223 | fontnum - t1 library font id | |
224 | points - number of points in fontheight | |
225 | str - string to render | |
226 | len - string length | |
227 | align - (0 - top of font glyph | 1 - baseline ) | |
228 | ||
229 | =cut | |
230 | */ | |
231 | ||
232 | undef_int | |
233 | i_t1_cp(i_img *im,int xb,int yb,int channel,int fontnum,float points,char* str,int len,int align) { | |
234 | GLYPH *glyph; | |
235 | int xsize,ysize,x,y; | |
236 | i_color val; | |
237 | ||
238 | unsigned int ch_mask_store; | |
239 | ||
240 | if (im == NULL) { mm_log((1,"i_t1_cp: Null image in input\n")); return(0); } | |
241 | ||
242 | glyph=T1_AASetString( fontnum, str, len, 0, T1_KERNING, points, NULL); | |
faa9b3e7 TC |
243 | if (glyph == NULL) |
244 | return 0; | |
02d1d628 AMH |
245 | |
246 | mm_log((1,"metrics: ascent: %d descent: %d\n",glyph->metrics.ascent,glyph->metrics.descent)); | |
247 | mm_log((1," leftSideBearing: %d rightSideBearing: %d\n",glyph->metrics.leftSideBearing,glyph->metrics.rightSideBearing)); | |
248 | mm_log((1," advanceX: %d advanceY: %d\n",glyph->metrics.advanceX,glyph->metrics.advanceY)); | |
249 | mm_log((1,"bpp: %d\n",glyph->bpp)); | |
250 | ||
251 | xsize=glyph->metrics.rightSideBearing-glyph->metrics.leftSideBearing; | |
252 | ysize=glyph->metrics.ascent-glyph->metrics.descent; | |
253 | ||
254 | mm_log((1,"width: %d height: %d\n",xsize,ysize)); | |
255 | ||
256 | ch_mask_store=im->ch_mask; | |
257 | im->ch_mask=1<<channel; | |
258 | ||
259 | if (align==1) { xb+=glyph->metrics.leftSideBearing; yb-=glyph->metrics.ascent; } | |
260 | ||
261 | for(y=0;y<ysize;y++) for(x=0;x<xsize;x++) { | |
262 | val.channel[channel]=glyph->bits[y*xsize+x]; | |
263 | i_ppix(im,x+xb,y+yb,&val); | |
264 | } | |
265 | ||
266 | im->ch_mask=ch_mask_store; | |
267 | return 1; | |
268 | } | |
269 | ||
270 | ||
271 | /* | |
272 | =item i_t1_bbox(handle, fontnum, points, str, len, cords) | |
273 | ||
274 | function to get a strings bounding box given the font id and sizes | |
275 | ||
276 | handle - pointer to font handle | |
277 | fontnum - t1 library font id | |
278 | points - number of points in fontheight | |
279 | str - string to measure | |
280 | len - string length | |
281 | cords - the bounding box (modified in place) | |
282 | ||
283 | =cut | |
284 | */ | |
285 | ||
286 | void | |
287 | i_t1_bbox(int fontnum,float points,char *str,int len,int cords[6]) { | |
288 | BBox bbox; | |
289 | BBox gbbox; | |
290 | ||
291 | mm_log((1,"i_t1_bbox(fontnum %d,points %.2f,str '%.*s', len %d)\n",fontnum,points,len,str,len)); | |
292 | T1_LoadFont(fontnum); /* FIXME: Here a return code is ignored - haw haw haw */ | |
293 | bbox = T1_GetStringBBox(fontnum,str,len,0,T1_KERNING); | |
294 | gbbox = T1_GetFontBBox(fontnum); | |
295 | ||
296 | mm_log((1,"bbox: (%d,%d,%d,%d)\n", | |
297 | (int)(bbox.llx*points/1000), | |
298 | (int)(gbbox.lly*points/1000), | |
299 | (int)(bbox.urx*points/1000), | |
300 | (int)(gbbox.ury*points/1000), | |
301 | (int)(bbox.lly*points/1000), | |
302 | (int)(bbox.ury*points/1000) )); | |
303 | ||
304 | ||
305 | cords[0]=((float)bbox.llx*points)/1000; | |
306 | cords[2]=((float)bbox.urx*points)/1000; | |
307 | ||
308 | cords[1]=((float)gbbox.lly*points)/1000; | |
309 | cords[3]=((float)gbbox.ury*points)/1000; | |
310 | ||
311 | cords[4]=((float)bbox.lly*points)/1000; | |
312 | cords[5]=((float)bbox.ury*points)/1000; | |
313 | } | |
314 | ||
315 | ||
316 | /* | |
317 | =item i_t1_text(im, xb, yb, cl, fontnum, points, str, len, align) | |
318 | ||
319 | Interface to text rendering in a single color onto an image | |
320 | ||
321 | im - pointer to image structure | |
322 | xb - x coordinate of start of string | |
323 | yb - y coordinate of start of string ( see align ) | |
324 | cl - color to draw the text in | |
325 | fontnum - t1 library font id | |
326 | points - number of points in fontheight | |
327 | str - char pointer to string to render | |
328 | len - string length | |
329 | align - (0 - top of font glyph | 1 - baseline ) | |
330 | ||
331 | =cut | |
332 | */ | |
333 | ||
334 | undef_int | |
335 | i_t1_text(i_img *im,int xb,int yb,i_color *cl,int fontnum,float points,char* str,int len,int align) { | |
336 | GLYPH *glyph; | |
337 | int xsize,ysize,x,y,ch; | |
338 | i_color val; | |
339 | unsigned char c,i; | |
340 | ||
341 | if (im == NULL) { mm_log((1,"i_t1_cp: Null image in input\n")); return(0); } | |
342 | ||
343 | glyph=T1_AASetString( fontnum, str, len, 0, T1_KERNING, points, NULL); | |
faa9b3e7 TC |
344 | if (glyph == NULL) |
345 | return 0; | |
02d1d628 AMH |
346 | |
347 | mm_log((1,"metrics: ascent: %d descent: %d\n",glyph->metrics.ascent,glyph->metrics.descent)); | |
348 | mm_log((1," leftSideBearing: %d rightSideBearing: %d\n",glyph->metrics.leftSideBearing,glyph->metrics.rightSideBearing)); | |
349 | mm_log((1," advanceX: %d advanceY: %d\n",glyph->metrics.advanceX,glyph->metrics.advanceY)); | |
350 | mm_log((1,"bpp: %d\n",glyph->bpp)); | |
351 | ||
352 | xsize=glyph->metrics.rightSideBearing-glyph->metrics.leftSideBearing; | |
353 | ysize=glyph->metrics.ascent-glyph->metrics.descent; | |
354 | ||
355 | mm_log((1,"width: %d height: %d\n",xsize,ysize)); | |
356 | ||
357 | if (align==1) { xb+=glyph->metrics.leftSideBearing; yb-=glyph->metrics.ascent; } | |
358 | ||
359 | for(y=0;y<ysize;y++) for(x=0;x<xsize;x++) { | |
360 | c=glyph->bits[y*xsize+x]; | |
361 | i=255-c; | |
362 | i_gpix(im,x+xb,y+yb,&val); | |
363 | for(ch=0;ch<im->channels;ch++) val.channel[ch]=(c*cl->channel[ch]+i*val.channel[ch])/255; | |
364 | i_ppix(im,x+xb,y+yb,&val); | |
365 | } | |
366 | return 1; | |
367 | } | |
368 | ||
369 | ||
370 | #endif /* HAVE_LIBT1 */ | |
371 | ||
372 | ||
373 | ||
374 | ||
375 | ||
376 | ||
377 | ||
378 | ||
379 | ||
380 | ||
381 | /* Truetype font support */ | |
382 | ||
383 | #ifdef HAVE_LIBTT | |
384 | ||
385 | ||
386 | /* Defines */ | |
387 | ||
388 | #define USTRCT(x) ((x).z) | |
389 | #define TT_VALID( handle ) ( ( handle ).z != NULL ) | |
390 | ||
391 | ||
392 | /* Prototypes */ | |
393 | ||
394 | static int i_tt_get_instance( TT_Fonthandle *handle, int points, int smooth ); | |
395 | static void i_tt_init_raster_map( TT_Raster_Map* bit, int width, int height, int smooth ); | |
396 | static void i_tt_done_raster_map( TT_Raster_Map *bit ); | |
397 | static void i_tt_clear_raster_map( TT_Raster_Map* bit ); | |
398 | static void i_tt_blit_or( TT_Raster_Map *dst, TT_Raster_Map *src,int x_off, int y_off ); | |
399 | static int i_tt_get_glyph( TT_Fonthandle *handle, int inst, unsigned char j ); | |
400 | static void i_tt_render_glyph( TT_Glyph glyph, TT_Glyph_Metrics* gmetrics, TT_Raster_Map *bit, TT_Raster_Map *small_bit, int x_off, int y_off, int smooth ); | |
401 | static void i_tt_render_all_glyphs( TT_Fonthandle *handle, int inst, TT_Raster_Map *bit, TT_Raster_Map *small_bit, int cords[6], char* txt, int len, int smooth ); | |
402 | static void i_tt_dump_raster_map2( i_img* im, TT_Raster_Map* bit, int xb, int yb, i_color *cl, int smooth ); | |
403 | static void i_tt_dump_raster_map_channel( i_img* im, TT_Raster_Map* bit, int xb, int yb, int channel, int smooth ); | |
404 | static int i_tt_rasterize( TT_Fonthandle *handle, TT_Raster_Map *bit, int cords[6], float points, char* txt, int len, int smooth ); | |
405 | static undef_int i_tt_bbox_inst( TT_Fonthandle *handle, int inst ,const char *txt, int len, int cords[6] ); | |
406 | ||
407 | ||
408 | /* static globals needed */ | |
409 | ||
410 | static TT_Engine engine; | |
411 | static int LTT_dpi = 72; /* FIXME: this ought to be a part of the call interface */ | |
412 | static int LTT_hinted = 1; /* FIXME: this too */ | |
413 | ||
414 | ||
415 | /* | |
416 | * FreeType interface | |
417 | */ | |
418 | ||
419 | ||
420 | /* | |
421 | =item init_tt() | |
422 | ||
423 | Initializes the freetype font rendering engine | |
424 | ||
425 | =cut | |
426 | */ | |
427 | ||
428 | undef_int | |
429 | init_tt() { | |
430 | TT_Error error; | |
431 | mm_log((1,"init_tt()\n")); | |
432 | error = TT_Init_FreeType( &engine ); | |
433 | if ( error ){ | |
434 | mm_log((1,"Initialization of freetype failed, code = 0x%x\n",error)); | |
435 | return(1); | |
436 | } | |
437 | return(0); | |
438 | } | |
439 | ||
440 | ||
441 | /* | |
442 | =item i_tt_get_instance(handle, points, smooth) | |
443 | ||
444 | Finds a points+smooth instance or if one doesn't exist in the cache | |
445 | allocates room and returns its cache entry | |
446 | ||
447 | fontname - path to the font to load | |
448 | handle - handle to the font. | |
449 | points - points of the requested font | |
450 | smooth - boolean (True: antialias on, False: antialias is off) | |
451 | ||
452 | =cut | |
453 | */ | |
454 | ||
455 | static | |
456 | int | |
457 | i_tt_get_instance( TT_Fonthandle *handle, int points, int smooth ) { | |
458 | int i,idx; | |
459 | TT_Error error; | |
460 | ||
461 | mm_log((1,"i_tt_get_instance(handle 0x%X, points %d, smooth %d)\n",handle,points,smooth)); | |
462 | ||
463 | if (smooth == -1) { /* Smooth doesn't matter for this search */ | |
464 | for(i=0;i<TT_CHC;i++) if (handle->instanceh[i].ptsize==points) { | |
465 | mm_log((1,"i_tt_get_instance: in cache - (non selective smoothing search) returning %d\n",i)); | |
466 | return i; | |
467 | } | |
468 | smooth=1; /* We will be adding a font - add it as smooth then */ | |
469 | } else { /* Smooth doesn't matter for this search */ | |
470 | for(i=0;i<TT_CHC;i++) if (handle->instanceh[i].ptsize==points && handle->instanceh[i].smooth==smooth) { | |
471 | mm_log((1,"i_tt_get_instance: in cache returning %d\n",i)); | |
472 | return i; | |
473 | } | |
474 | } | |
475 | ||
476 | /* Found the instance in the cache - return the cache index */ | |
477 | ||
478 | for(idx=0;idx<TT_CHC;idx++) if (!(handle->instanceh[idx].order)) break; /* find the lru item */ | |
479 | ||
480 | mm_log((1,"i_tt_get_instance: lru item is %d\n",idx)); | |
481 | mm_log((1,"i_tt_get_instance: lru pointer 0x%X\n",USTRCT(handle->instanceh[idx].instance) )); | |
482 | ||
483 | if ( USTRCT(handle->instanceh[idx].instance) ) { | |
484 | mm_log((1,"i_tt_get_instance: freeing lru item from cache %d\n",idx)); | |
485 | TT_Done_Instance( handle->instanceh[idx].instance ); /* Free instance if needed */ | |
486 | } | |
487 | ||
488 | /* create and initialize instance */ | |
489 | /* FIXME: probably a memory leak on fail */ | |
490 | ||
491 | (void) (( error = TT_New_Instance( handle->face, &handle->instanceh[idx].instance ) ) || | |
492 | ( error = TT_Set_Instance_Resolutions( handle->instanceh[idx].instance, LTT_dpi, LTT_dpi ) ) || | |
493 | ( error = TT_Set_Instance_CharSize( handle->instanceh[idx].instance, points*64 ) ) ); | |
494 | ||
495 | if ( error ) { | |
496 | mm_log((1, "Could not create and initialize instance: error 0x%x.\n",error )); | |
497 | return -1; | |
498 | } | |
499 | ||
500 | /* Now that the instance should the inplace we need to lower all of the | |
501 | ru counts and put `this' one with the highest entry */ | |
502 | ||
503 | for(i=0;i<TT_CHC;i++) handle->instanceh[i].order--; | |
504 | ||
505 | handle->instanceh[idx].order=TT_CHC-1; | |
506 | handle->instanceh[idx].ptsize=points; | |
507 | handle->instanceh[idx].smooth=smooth; | |
508 | TT_Get_Instance_Metrics( handle->instanceh[idx].instance, &(handle->instanceh[idx].imetrics) ); | |
509 | ||
510 | /* Zero the memory for the glyph storage so they are not thought as cached if they haven't been cached | |
511 | since this new font was loaded */ | |
512 | ||
513 | for(i=0;i<256;i++) USTRCT(handle->instanceh[idx].glyphs[i])=NULL; | |
514 | ||
515 | return idx; | |
516 | } | |
517 | ||
518 | ||
519 | /* | |
520 | =item i_tt_new(fontname) | |
521 | ||
522 | Creates a new font handle object, finds a character map and initialise the | |
523 | the font handle's cache | |
524 | ||
525 | fontname - path to the font to load | |
526 | ||
527 | =cut | |
528 | */ | |
529 | ||
530 | TT_Fonthandle* | |
531 | i_tt_new(char *fontname) { | |
532 | TT_Error error; | |
533 | TT_Fonthandle *handle; | |
534 | unsigned short i,n; | |
535 | unsigned short platform,encoding; | |
536 | ||
537 | mm_log((1,"i_tt_new(fontname '%s')\n",fontname)); | |
538 | ||
539 | /* allocate memory for the structure */ | |
540 | ||
541 | handle=mymalloc( sizeof(TT_Fonthandle) ); | |
542 | ||
543 | /* load the typeface */ | |
544 | error = TT_Open_Face( engine, fontname, &handle->face ); | |
545 | if ( error ) { | |
cd9f9ddc AMH |
546 | if ( error == TT_Err_Could_Not_Open_File ) { mm_log((1, "Could not find/open %s.\n", fontname )) } |
547 | else { mm_log((1, "Error while opening %s, error code = 0x%x.\n",fontname, error )); } | |
02d1d628 AMH |
548 | return NULL; |
549 | } | |
550 | ||
551 | TT_Get_Face_Properties( handle->face, &(handle->properties) ); | |
552 | /* First, look for a Unicode charmap */ | |
553 | ||
554 | n = handle->properties.num_CharMaps; | |
555 | USTRCT( handle->char_map )=NULL; /* Invalidate character map */ | |
556 | ||
557 | for ( i = 0; i < n; i++ ) { | |
558 | TT_Get_CharMap_ID( handle->face, i, &platform, &encoding ); | |
559 | if ( (platform == 3 && encoding == 1 ) || (platform == 0 && encoding == 0 ) ) { | |
560 | TT_Get_CharMap( handle->face, i, &(handle->char_map) ); | |
561 | break; | |
562 | } | |
563 | } | |
564 | ||
565 | /* Zero the pointsizes - and ordering */ | |
566 | ||
567 | for(i=0;i<TT_CHC;i++) { | |
568 | USTRCT(handle->instanceh[i].instance)=NULL; | |
569 | handle->instanceh[i].order=i; | |
570 | handle->instanceh[i].ptsize=0; | |
571 | handle->instanceh[i].smooth=-1; | |
572 | } | |
573 | ||
574 | mm_log((1,"i_tt_new <- 0x%X\n",handle)); | |
575 | return handle; | |
576 | } | |
577 | ||
578 | ||
579 | ||
580 | /* | |
581 |