]>
Commit | Line | Data |
---|---|---|
cabec872 RR |
1 | /***************************************************************************/ |
2 | /* */ | |
3 | /* fonddrvr.c */ | |
4 | /* */ | |
5 | /* Mac FOND font driver. Written by just@letterror.com. */ | |
6 | /* */ | |
7 | /* Copyright 1996-2000 by */ | |
8 | /* Just van Rossum, David Turner, Robert Wilhelm, and Werner Lemberg. */ | |
9 | /* */ | |
10 | /* This file is part of the FreeType project, and may only be used, */ | |
11 | /* modified, and distributed under the terms of the FreeType project */ | |
12 | /* license, LICENSE.TXT. By continuing to use, modify, or distribute */ | |
13 | /* this file you indicate that you have read the license and */ | |
14 | /* understand and accept it fully. */ | |
15 | /* */ | |
16 | /***************************************************************************/ | |
17 | ||
18 | ||
19 | /* | |
20 | Notes | |
21 | ||
22 | Mac suitcase files can (and often do!) contain multiple fonts. To | |
23 | support this I use the face_index argument of FT_(Open|New)_Face() | |
24 | functions, and pretend the suitcase file is a collection. | |
25 | Warning: although the FOND driver sets face->num_faces field to the | |
26 | number of available fonts, but the Type 1 driver sets it to 1 anyway. | |
27 | So this field is currently not reliable, and I don't see a clean way | |
28 | to resolve that. The face_index argument translates to | |
29 | Get1IndResource( 'FOND', face_index + 1 ); | |
30 | so clients should figure out the resource index of the FOND. | |
31 | (I'll try to provide some example code for this at some point.) | |
32 | ||
33 | The Mac FOND driver works roughly like this: | |
34 | ||
35 | - Check whether the offered stream points to a Mac suitcase file. | |
36 | This is done by checking the file type: it has to be 'FFIL' or 'tfil'. | |
37 | The stream that gets passed to our init_face() routine is a stdio | |
38 | stream, which isn't usable for us, since the FOND resources live | |
39 | in the resource fork. So we just grab the stream->pathname field. | |
40 | ||
41 | - Read the FOND resource into memory, then check whether there is | |
42 | a TrueType font and/or (!) a Type 1 font available. | |
43 | ||
44 | - If there is a Type 1 font available (as a separate 'LWFN' file), | |
45 | read its data into memory, massage it slightly so it becomes | |
46 | PFB data, wrap it into a memory stream, load the Type 1 driver | |
47 | and delegate the rest of the work to it, by calling the init_face() | |
48 | method of the Type 1 driver. | |
49 | (XXX TODO: after this has been done, the kerning data from the FOND | |
50 | resource should be appended to the face: on the Mac there are usually | |
51 | no AFM files available. However, this is tricky since we need to map | |
52 | Mac char codes to ps glyph names to glyph ID's...) | |
53 | ||
54 | - If there is a TrueType font (an 'sfnt' resource), read it into | |
55 | memory, wrap it into a memory stream, load the TrueType driver | |
56 | and delegate the rest of the work to it, by calling the init_face() | |
57 | method if the TrueType driver. | |
58 | ||
59 | - In both cases, the original stream gets closed and *reinitialized* | |
60 | to become a memory stream. Additionally, the face->driver field -- | |
61 | which is set to the FOND driver upon entering our init_face() -- | |
62 | gets *reset* to either the TT or the T1 driver. I had to make a minor | |
63 | change to ftobjs.c to make this work. | |
64 | ||
65 | - We might consider creating an FT_New_Face_Mac() API call, as this | |
66 | would avoid some of the mess described above. | |
67 | */ | |
68 | ||
69 | #include <truetype/ttobjs.h> | |
70 | #include <type1z/z1objs.h> | |
71 | ||
72 | #include <Resources.h> | |
73 | #include <Fonts.h> | |
74 | #include <Errors.h> | |
75 | ||
76 | #include <ctype.h> /* for isupper() and isalnum() */ | |
77 | #include <stdlib.h> /* for malloc() and free() */ | |
78 | ||
79 | ||
80 | /* set PREFER_LWFN to 1 if LWFN (Type 1) is preferred over | |
81 | TrueType in case *both* are available */ | |
82 | #ifndef PREFER_LWFN | |
83 | #define PREFER_LWFN 1 | |
84 | #endif | |
85 | ||
86 | ||
87 | static | |
88 | FT_Error init_driver( FT_Driver driver ) | |
89 | { | |
90 | /* we don't keep no stinkin' state ;-) */ | |
91 | return FT_Err_Ok; | |
92 | } | |
93 | ||
94 | static | |
95 | FT_Error done_driver( FT_Driver driver ) | |
96 | { | |
97 | return FT_Err_Ok; | |
98 | } | |
99 | ||
100 | ||
101 | /* MacRoman glyph names, needed for FOND kerning support. */ | |
102 | /* XXX which is not implemented yet! */ | |
103 | static const char* mac_roman_glyph_names[256] = { | |
104 | ".null", | |
105 | }; | |
106 | ||
107 | ||
108 | /* The FOND face object is just a union of TT and T1: both is possible, | |
109 | and we don't need anything else. We still need to be able to hold | |
110 | either, as the face object is not allocated by us. Again, creating | |
111 | an FT_New_Face_Mac() would avoid this kludge. */ | |
112 | typedef union FOND_FaceRec_ | |
113 | { | |
114 | TT_FaceRec tt; | |
115 | T1_FaceRec t1; | |
116 | } FOND_FaceRec, *FOND_Face; | |
117 | ||
118 | ||
119 | /* given a pathname, fill in a File Spec */ | |
120 | static | |
121 | int make_file_spec( char* pathname, FSSpec *spec ) | |
122 | { | |
123 | Str255 p_path; | |
124 | int path_len; | |
125 | ||
126 | /* convert path to a pascal string */ | |
127 | path_len = strlen( pathname ); | |
128 | if ( path_len > 255 ) | |
129 | return -1; | |
130 | p_path[0] = path_len; | |
131 | strncpy( (char*)p_path+1, pathname, path_len ); | |
132 | ||
133 | if ( FSMakeFSSpec( 0, 0, p_path, spec ) != noErr ) | |
134 | return -1; | |
135 | else | |
136 | return 0; | |
137 | } | |
138 | ||
139 | ||
140 | /* is_suitcase() returns true if the file specified by 'pathname' | |
141 | is a Mac suitcase file, and false if it ain't. */ | |
142 | static | |
143 | int is_suitcase( FSSpec *spec ) | |
144 | { | |
145 | FInfo finfo; | |
146 | ||
147 | if ( FSpGetFInfo( spec, &finfo ) != noErr ) | |
148 | return 0; | |
149 | if ( finfo.fdType == 'FFIL' || finfo.fdType == 'tfil' ) | |
150 | return 1; | |
151 | else | |
152 | return 0; | |
153 | } | |
154 | ||
155 | ||
156 | /* Quick 'n' Dirty Pascal string to C string converter. | |
157 | Warning: this call is not thread safe! Use with caution. */ | |
158 | static | |
159 | char * p2c_str( unsigned char *pstr ) | |
160 | { | |
161 | static char cstr[256]; | |
162 | ||
163 | strncpy( cstr, (char*)pstr+1, pstr[0] ); | |
164 | cstr[pstr[0]] = '\0'; | |
165 | return cstr; | |
166 | } | |
167 | ||
168 | ||
169 | /* Given a PostScript font name, create the Macintosh LWFN file name */ | |
170 | static | |
171 | void create_lwfn_name( char* ps_name, Str255 lwfn_file_name ) | |
172 | { | |
173 | int max = 5, count = 0; | |
174 | unsigned char* p = lwfn_file_name; | |
175 | char* q = ps_name; | |
176 | ||
177 | lwfn_file_name[0] = 0; | |
178 | ||
179 | while ( *q ) | |
180 | { | |
181 | if ( isupper(*q) ) | |
182 | { | |
183 | if ( count ) | |
184 | max = 3; | |
185 | count = 0; | |
186 | } | |
187 | if ( count < max && (isalnum(*q) || *q == '_' ) ) | |
188 | { | |
189 | *++p = *q; | |
190 | lwfn_file_name[0]++; | |
191 | count++; | |
192 | } | |
193 | q++; | |
194 | } | |
195 | } | |
196 | ||
197 | ||
198 | /* Suck the relevant info out of the FOND data */ | |
199 | static | |
200 | FT_Error parse_fond( char* fond_data, | |
201 | short *have_sfnt, | |
202 | short *sfnt_id, | |
203 | Str255 lwfn_file_name ) | |
204 | { | |
205 | AsscEntry* assoc; | |
206 | FamRec* fond; | |
207 | ||
208 | *sfnt_id = *have_sfnt = 0; | |
209 | lwfn_file_name[0] = 0; | |
210 | ||
211 | fond = (FamRec*)fond_data; | |
212 | assoc = (AsscEntry*)(fond_data + sizeof(FamRec) + 2); | |
213 | ||
214 | if ( assoc->fontSize == 0 ) | |
215 | { | |
216 | *have_sfnt = 1; | |
217 | *sfnt_id = assoc->fontID; | |
218 | } | |
219 | ||
220 | if ( fond->ffStylOff ) | |
221 | { | |
222 | unsigned char* p = (unsigned char*)fond_data; | |
223 | StyleTable* style; | |
224 | unsigned short string_count; | |
225 | unsigned char* name_table = 0; | |
226 | char ps_name[256]; | |
227 | unsigned char* names[64]; | |
228 | int i; | |
229 | ||
230 | p += fond->ffStylOff; | |
231 | style = (StyleTable*)p; | |
232 | p += sizeof(StyleTable); | |
233 | string_count = *(unsigned short*)(p); | |
234 | p += sizeof(short); | |
235 | ||
236 | for ( i=0 ; i<string_count && i<64; i++ ) | |
237 | { | |
238 | names[i] = p; | |
239 | p += names[i][0]; | |
240 | p++; | |
241 | } | |
242 | strcpy(ps_name, p2c_str(names[0])); /* Family name */ | |
243 | ||
244 | if ( style->indexes[0] > 1 ) | |
245 | { | |
246 | unsigned char* suffixes = names[style->indexes[0]-1]; | |
247 | for ( i=1; i<=suffixes[0]; i++ ) | |
248 | strcat( ps_name, p2c_str(names[suffixes[i]-1]) ); | |
249 | } | |
250 | create_lwfn_name( ps_name, lwfn_file_name ); | |
251 | } | |
252 | return FT_Err_Ok; | |
253 | } | |
254 | ||
255 | ||
256 | /* Read Type 1 data from the POST resources inside the LWFN file, return a | |
257 | PFB buffer -- apparently FT doesn't like a pure binary T1 stream. */ | |
258 | static | |
259 | unsigned char* read_type1_data( FT_Memory memory, FSSpec* lwfn_spec, unsigned long *size ) | |
260 | { | |
261 | short res_ref, res_id; | |
262 | unsigned char *buffer, *p, *size_p; | |
263 | unsigned long total_size = 0; | |
264 | unsigned long post_size, pfb_chunk_size; | |
265 | Handle post_data; | |
266 | char code, last_code; | |
267 | ||
268 | res_ref = FSpOpenResFile( lwfn_spec, fsRdPerm ); | |
269 | if ( ResError() ) | |
270 | return NULL; | |
271 | UseResFile( res_ref ); | |
272 | ||
273 | /* first pass: load all POST resources, and determine the size of | |
274 | the output buffer */ | |
275 | res_id = 501; | |
276 | last_code = -1; | |
277 | for (;;) | |
278 | { | |
279 | post_data = Get1Resource( 'POST', res_id++ ); | |
280 | if ( post_data == NULL ) | |
281 | break; | |
282 | code = (*post_data)[0]; | |
283 | if ( code != last_code ) | |
284 | { | |
285 | if ( code == 5 ) | |
286 | total_size += 2; /* just the end code */ | |
287 | else | |
288 | total_size += 6; /* code + 4 bytes chunk length */ | |
289 | } | |
290 | total_size += GetHandleSize( post_data ) - 2; | |
291 | last_code = code; | |
292 | } | |
293 | ||
294 | buffer = memory->alloc( memory, total_size ); | |
295 | if ( !buffer ) | |
296 | goto error; | |
297 | ||
298 | /* second pass: append all POST data to the buffer, add PFB fields */ | |
299 | p = buffer; | |
300 | res_id = 501; | |
301 | last_code = -1; | |
302 | pfb_chunk_size = 0; | |
303 | for (;;) | |
304 | { | |
305 | post_data = Get1Resource( 'POST', res_id++ ); | |
306 | if ( post_data == NULL ) | |
307 | break; | |
308 | post_size = GetHandleSize( post_data ) - 2; | |
309 | code = (*post_data)[0]; | |
310 | if ( code != last_code ) | |
311 | { | |
312 | if ( last_code != -1 ) | |
313 | { | |
314 | /* we're done adding a chunk, fill in the size field */ | |
315 | *size_p++ = pfb_chunk_size & 0xFF; | |
316 | *size_p++ = (pfb_chunk_size >> 8) & 0xFF; | |
317 | *size_p++ = (pfb_chunk_size >> 16) & 0xFF; | |
318 | *size_p++ = (pfb_chunk_size >> 24) & 0xFF; | |
319 | pfb_chunk_size = 0; | |
320 | } | |
321 | *p++ = 0x80; | |
322 | if ( code == 5 ) | |
323 | *p++ = 0x03; /* the end */ | |
324 | else if ( code == 2 ) | |
325 | *p++ = 0x02; /* binary segment */ | |
326 | else | |
327 | *p++ = 0x01; /* ASCII segment */ | |
328 | if ( code != 5 ) | |
329 | { | |
330 | size_p = p; /* save for later */ | |
331 | p += 4; /* make space for size field */ | |
332 | } | |
333 | } | |
334 | memcpy( p, *post_data + 2, post_size ); | |
335 | pfb_chunk_size += post_size; | |
336 | p += post_size; | |
337 | last_code = code; | |
338 | } | |
339 | ||
340 | CloseResFile( res_ref ); | |
341 | ||
342 | *size = total_size; | |
343 | /* printf( "XXX %d %d\n", p - buffer, total_size ); */ | |
344 | return buffer; | |
345 | ||
346 | error: | |
347 | CloseResFile( res_ref ); | |
348 | return NULL; | |
349 | } | |
350 | ||
351 | ||
352 | /* Finalizer for the sfnt stream */ | |
353 | static | |
354 | void sfnt_stream_close( FT_Stream stream ) | |
355 | { | |
356 | Handle sfnt_data = stream->descriptor.pointer; | |
357 | HUnlock( sfnt_data ); | |
358 | DisposeHandle( sfnt_data ); | |
359 | ||
360 | stream->descriptor.pointer = NULL; | |
361 | stream->size = 0; | |
362 | stream->base = 0; | |
363 | stream->close = 0; | |
364 | } | |
365 | ||
366 | ||
367 | /* Finalizer for the LWFN stream */ | |
368 | static | |
369 | void lwfn_stream_close( FT_Stream stream ) | |
370 | { | |
371 | stream->memory->free( stream->memory, stream->base ); | |
372 | stream->descriptor.pointer = NULL; | |
373 | stream->size = 0; | |
374 | stream->base = 0; | |
375 | stream->close = 0; | |
376 | } | |
377 | ||
378 | ||
379 | /* Main entry point. Determine whether we're dealing with a Mac | |
380 | suitcase or not; then determine if we're dealing with Type 1 | |
381 | or TrueType; delegate the work to the proper driver. */ | |
382 | static | |
383 | FT_Error init_face( FT_Stream stream, | |
384 | FT_Face face, | |
385 | FT_Int face_index, | |
386 | FT_Int num_params, | |
387 | FT_Parameter* parameters ) | |
388 | { | |
389 | FT_Error err; | |
390 | FSSpec suit_spec, lwfn_spec; | |
391 | short res_ref; | |
392 | Handle fond_data, sfnt_data; | |
393 | short res_index, sfnt_id, have_sfnt; | |
394 | Str255 lwfn_file_name; | |
395 | ||
396 | if ( !stream->pathname.pointer ) | |
397 | return FT_Err_Unknown_File_Format; | |
398 | ||
399 | if ( make_file_spec( stream->pathname.pointer, &suit_spec ) ) | |
400 | return FT_Err_Invalid_Argument; | |
401 | ||
402 | if ( !is_suitcase( &suit_spec ) ) | |
403 | return FT_Err_Unknown_File_Format; | |
404 | ||
405 | res_ref = FSpOpenResFile( &suit_spec, fsRdPerm ); | |
406 | if ( ResError() ) | |
407 | return FT_Err_Invalid_File_Format; | |
408 | UseResFile( res_ref ); | |
409 | ||
410 | /* face_index may be -1, in which case we | |
411 | just need to do a sanity check */ | |
412 | if ( face_index < 0) | |
413 | res_index = 1; | |
414 | else | |
415 | { | |
416 | res_index = face_index + 1; | |
417 | face_index = 0; | |
418 | } | |
419 | fond_data = Get1IndResource( 'FOND', res_index ); | |
420 | if ( ResError() ) | |
421 | { | |
422 | CloseResFile( res_ref ); | |
423 | return FT_Err_Invalid_File_Format; | |
424 | } | |
425 | /* Set the number of faces. Not that it helps much: the t1 driver | |
426 | just sets it to 1 anyway :-( */ | |
427 | face->num_faces = Count1Resources('FOND'); | |
428 | ||
429 | HLock( fond_data ); | |
430 | err = parse_fond( *fond_data, &have_sfnt, &sfnt_id, lwfn_file_name ); | |
431 | HUnlock( fond_data ); | |
432 | if ( err ) | |
433 | { | |
434 | CloseResFile( res_ref ); | |
435 | return FT_Err_Invalid_Handle; | |
436 | } | |
437 | ||
438 | if ( lwfn_file_name[0] ) | |
439 | { | |
440 | /* We look for the LWFN file in the same directory as the suitcase | |
441 | file. ATM would look in other places, too, but this is the usual | |
442 | situation. */ | |
443 | err = FSMakeFSSpec( suit_spec.vRefNum, suit_spec.parID, lwfn_file_name, &lwfn_spec ); | |
444 | if ( err != noErr ) | |
445 | lwfn_file_name[0] = 0; /* no LWFN file found */ | |
446 | } | |
447 | ||
448 | if ( lwfn_file_name[0] && ( !have_sfnt || PREFER_LWFN ) ) | |
449 | { | |
450 | FT_Driver t1_driver; | |
451 | unsigned char* type1_data; | |
452 | unsigned long size; | |
453 | ||
454 | CloseResFile( res_ref ); /* XXX still need to read kerning! */ | |
455 | ||
456 | type1_data = read_type1_data( stream->memory, &lwfn_spec, &size ); | |
457 | if ( !type1_data ) | |
458 | { | |
459 | return FT_Err_Out_Of_Memory; | |
460 | } | |
461 | ||
462 | #if 0 | |
463 | { | |
464 | FILE* f; | |
465 | char * path; | |
466 | ||
467 | path = p2c_str( lwfn_file_name ); | |
468 | strcat( path, ".PFB" ); | |
469 | f = fopen(path, "wb"); | |
470 | if ( f ) | |
471 | { | |
472 | fwrite( type1_data, 1, size, f ); | |
473 | fclose( f ); | |
474 | } | |
475 | } | |
476 | #endif | |
477 | ||
478 | /* reinitialize the stream */ | |
479 | if ( stream->close ) | |
480 | stream->close( stream ); | |
481 | stream->close = lwfn_stream_close; | |
482 | stream->read = 0; /* it's now memory based */ | |
483 | stream->base = type1_data; | |
484 | stream->size = size; | |
485 | stream->pos = 0; /* just in case */ | |
486 | ||
487 | /* delegate the work to the Type 1 module */ | |
488 | t1_driver = (FT_Driver)FT_Get_Module( face->driver->root.library, "type1z" ); | |
489 | if ( t1_driver ) | |
490 | { | |
491 | face->driver = t1_driver; | |
492 | return t1_driver->clazz->init_face( stream, face, face_index, 0, NULL ); | |
493 | } | |
494 | else | |
495 | return FT_Err_Invalid_Driver_Handle; | |
496 | } | |
497 | else if ( have_sfnt ) | |
498 | { | |
499 | FT_Driver tt_driver; | |
500 | ||
501 | sfnt_data = Get1Resource( 'sfnt', sfnt_id ); | |
502 | if ( ResError() ) | |
503 | { | |
504 | CloseResFile( res_ref ); | |
505 | return FT_Err_Invalid_Handle; | |
506 | } | |
507 | DetachResource( sfnt_data ); | |
508 | CloseResFile( res_ref ); | |
509 | HLockHi( sfnt_data ); | |
510 | ||
511 | /* reinitialize the stream */ | |
512 | if ( stream->close ) | |
513 | stream->close( stream ); | |
514 | stream->close = sfnt_stream_close; | |
515 | stream->descriptor.pointer = sfnt_data; | |
516 | stream->read = 0; /* it's now memory based */ | |
517 | stream->base = (unsigned char *)*sfnt_data; | |
518 | stream->size = GetHandleSize( sfnt_data ); | |
519 | stream->pos = 0; /* just in case */ | |
520 | ||
521 | /* delegate the work to the TrueType driver */ | |
522 | tt_driver = (FT_Driver)FT_Get_Module( face->driver->root.library, "truetype" ); | |
523 | if ( tt_driver ) | |
524 | { | |
525 | face->driver = tt_driver; | |
526 | return tt_driver->clazz->init_face( stream, face, face_index, 0, NULL ); | |
527 | } | |
528 | else | |
529 | return FT_Err_Invalid_Driver_Handle; | |
530 | } | |
531 | else | |
532 | { | |
533 | CloseResFile( res_ref ); | |
534 | } | |
535 | return FT_Err_Invalid_File_Format; | |
536 | } | |
537 | ||
538 | ||
539 | static | |
540 | void done_face( FT_Face face ) | |
541 | { | |
542 | /* nothing to do */ | |
543 | } | |
544 | ||
545 | /* The FT_DriverInterface structure is defined in ftdriver.h. */ | |
546 | ||
547 | const FT_Driver_Class fond_driver_class = | |
548 | { | |
549 | { | |
550 | ft_module_font_driver | ft_module_driver_scalable, | |
551 | sizeof ( FT_DriverRec ), | |
552 | ||
553 | "fond", /* driver name */ | |
554 | 0x10000L, /* driver version == 1.0 */ | |
555 | 0x20000L, /* driver requires FreeType 2.0 or above */ | |
556 | ||
557 | (void*)0, | |
558 | ||
559 | (FT_Module_Constructor) init_driver, | |
560 | (FT_Module_Destructor) done_driver, | |
561 | (FT_Module_Requester) 0 | |
562 | }, | |
563 | ||
564 | sizeof ( FOND_FaceRec ), | |
565 | 0, | |
566 | 0, | |
567 | ||
568 | (FTDriver_initFace) init_face, | |
569 | (FTDriver_doneFace) done_face, | |
570 | (FTDriver_initSize) 0, | |
571 | (FTDriver_doneSize) 0, | |
572 | (FTDriver_initGlyphSlot) 0, | |
573 | (FTDriver_doneGlyphSlot) 0, | |
574 | ||
575 | (FTDriver_setCharSizes) 0, | |
576 | (FTDriver_setPixelSizes) 0, | |
577 | (FTDriver_loadGlyph) 0, | |
578 | (FTDriver_getCharIndex) 0, | |
579 | ||
580 | (FTDriver_getKerning) 0, | |
581 | (FTDriver_attachFile) 0, | |
582 | (FTDriver_getAdvances) 0 | |
583 | }; | |
584 | ||
585 | ||
586 | ||
587 | /*************************************************************************/ | |
588 | /* */ | |
589 | /* <Function> */ | |
590 | /* getDriverInterface */ | |
591 | /* */ | |
592 | /* <Description> */ | |
593 | /* This function is used when compiling the FOND driver as a */ | |
594 | /* shared library (`.DLL' or `.so'). It will be used by the */ | |
595 | /* high-level library of FreeType to retrieve the address of the */ | |
596 | /* driver's generic interface. */ | |
597 | /* */ | |
598 | /* It shouldn't be implemented in a static build, as each driver must */ | |
599 | /* have the same function as an exported entry point. */ | |
600 | /* */ | |
601 | /* <Return> */ | |
602 | /* The address of the TrueType's driver generic interface. The */ | |
603 | /* format-specific interface can then be retrieved through the method */ | |
604 | /* interface->get_format_interface. */ | |
605 | /* */ | |
606 | #ifdef FT_CONFIG_OPTION_DYNAMIC_DRIVERS | |
607 | ||
608 | FT_EXPORT_FUNC(const FT_Driver_Class*) getDriverClass( void ) | |
609 | { | |
610 | return &fond_driver_class; | |
611 | } | |
612 | ||
613 | #endif /* CONFIG_OPTION_DYNAMIC_DRIVERS */ | |
614 | ||
615 | ||
616 | /* END */ |