|
1 /***************************************************************************/ |
|
2 /* */ |
|
3 /* cidload.c */ |
|
4 /* */ |
|
5 /* CID-keyed Type1 font loader (body). */ |
|
6 /* */ |
|
7 /* Copyright 1996-2001, 2002, 2003, 2004, 2005, 2006, 2009 by */ |
|
8 /* 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 #include <ft2build.h> |
|
20 #include FT_INTERNAL_DEBUG_H |
|
21 #include FT_CONFIG_CONFIG_H |
|
22 #include FT_MULTIPLE_MASTERS_H |
|
23 #include FT_INTERNAL_TYPE1_TYPES_H |
|
24 |
|
25 #include "cidload.h" |
|
26 |
|
27 #include "ciderrs.h" |
|
28 |
|
29 |
|
30 /*************************************************************************/ |
|
31 /* */ |
|
32 /* The macro FT_COMPONENT is used in trace mode. It is an implicit */ |
|
33 /* parameter of the FT_TRACE() and FT_ERROR() macros, used to print/log */ |
|
34 /* messages during execution. */ |
|
35 /* */ |
|
36 #undef FT_COMPONENT |
|
37 #define FT_COMPONENT trace_cidload |
|
38 |
|
39 |
|
40 /* read a single offset */ |
|
41 FT_LOCAL_DEF( FT_Long ) |
|
42 cid_get_offset( FT_Byte* *start, |
|
43 FT_Byte offsize ) |
|
44 { |
|
45 FT_Long result; |
|
46 FT_Byte* p = *start; |
|
47 |
|
48 |
|
49 for ( result = 0; offsize > 0; offsize-- ) |
|
50 { |
|
51 result <<= 8; |
|
52 result |= *p++; |
|
53 } |
|
54 |
|
55 *start = p; |
|
56 return result; |
|
57 } |
|
58 |
|
59 |
|
60 /*************************************************************************/ |
|
61 /*************************************************************************/ |
|
62 /***** *****/ |
|
63 /***** TYPE 1 SYMBOL PARSING *****/ |
|
64 /***** *****/ |
|
65 /*************************************************************************/ |
|
66 /*************************************************************************/ |
|
67 |
|
68 |
|
69 static FT_Error |
|
70 cid_load_keyword( CID_Face face, |
|
71 CID_Loader* loader, |
|
72 const T1_Field keyword ) |
|
73 { |
|
74 FT_Error error; |
|
75 CID_Parser* parser = &loader->parser; |
|
76 FT_Byte* object; |
|
77 void* dummy_object; |
|
78 CID_FaceInfo cid = &face->cid; |
|
79 |
|
80 |
|
81 /* if the keyword has a dedicated callback, call it */ |
|
82 if ( keyword->type == T1_FIELD_TYPE_CALLBACK ) |
|
83 { |
|
84 keyword->reader( (FT_Face)face, parser ); |
|
85 error = parser->root.error; |
|
86 goto Exit; |
|
87 } |
|
88 |
|
89 /* we must now compute the address of our target object */ |
|
90 switch ( keyword->location ) |
|
91 { |
|
92 case T1_FIELD_LOCATION_CID_INFO: |
|
93 object = (FT_Byte*)cid; |
|
94 break; |
|
95 |
|
96 case T1_FIELD_LOCATION_FONT_INFO: |
|
97 object = (FT_Byte*)&cid->font_info; |
|
98 break; |
|
99 |
|
100 case T1_FIELD_LOCATION_FONT_EXTRA: |
|
101 object = (FT_Byte*)&face->font_extra; |
|
102 break; |
|
103 |
|
104 case T1_FIELD_LOCATION_BBOX: |
|
105 object = (FT_Byte*)&cid->font_bbox; |
|
106 break; |
|
107 |
|
108 default: |
|
109 { |
|
110 CID_FaceDict dict; |
|
111 |
|
112 |
|
113 if ( parser->num_dict < 0 ) |
|
114 { |
|
115 FT_ERROR(( "cid_load_keyword: invalid use of `%s'\n", |
|
116 keyword->ident )); |
|
117 error = CID_Err_Syntax_Error; |
|
118 goto Exit; |
|
119 } |
|
120 |
|
121 dict = cid->font_dicts + parser->num_dict; |
|
122 switch ( keyword->location ) |
|
123 { |
|
124 case T1_FIELD_LOCATION_PRIVATE: |
|
125 object = (FT_Byte*)&dict->private_dict; |
|
126 break; |
|
127 |
|
128 default: |
|
129 object = (FT_Byte*)dict; |
|
130 } |
|
131 } |
|
132 } |
|
133 |
|
134 dummy_object = object; |
|
135 |
|
136 /* now, load the keyword data in the object's field(s) */ |
|
137 if ( keyword->type == T1_FIELD_TYPE_INTEGER_ARRAY || |
|
138 keyword->type == T1_FIELD_TYPE_FIXED_ARRAY ) |
|
139 error = cid_parser_load_field_table( &loader->parser, keyword, |
|
140 &dummy_object ); |
|
141 else |
|
142 error = cid_parser_load_field( &loader->parser, |
|
143 keyword, &dummy_object ); |
|
144 Exit: |
|
145 return error; |
|
146 } |
|
147 |
|
148 |
|
149 FT_CALLBACK_DEF( FT_Error ) |
|
150 parse_font_matrix( CID_Face face, |
|
151 CID_Parser* parser ) |
|
152 { |
|
153 FT_Matrix* matrix; |
|
154 FT_Vector* offset; |
|
155 CID_FaceDict dict; |
|
156 FT_Face root = (FT_Face)&face->root; |
|
157 FT_Fixed temp[6]; |
|
158 FT_Fixed temp_scale; |
|
159 |
|
160 |
|
161 if ( parser->num_dict >= 0 ) |
|
162 { |
|
163 dict = face->cid.font_dicts + parser->num_dict; |
|
164 matrix = &dict->font_matrix; |
|
165 offset = &dict->font_offset; |
|
166 |
|
167 (void)cid_parser_to_fixed_array( parser, 6, temp, 3 ); |
|
168 |
|
169 temp_scale = FT_ABS( temp[3] ); |
|
170 |
|
171 /* Set units per EM based on FontMatrix values. We set the value to */ |
|
172 /* `1000/temp_scale', because temp_scale was already multiplied by */ |
|
173 /* 1000 (in `t1_tofixed', from psobjs.c). */ |
|
174 root->units_per_EM = (FT_UShort)( FT_DivFix( 0x10000L, |
|
175 FT_DivFix( temp_scale, 1000 ) ) ); |
|
176 |
|
177 /* we need to scale the values by 1.0/temp[3] */ |
|
178 if ( temp_scale != 0x10000L ) |
|
179 { |
|
180 temp[0] = FT_DivFix( temp[0], temp_scale ); |
|
181 temp[1] = FT_DivFix( temp[1], temp_scale ); |
|
182 temp[2] = FT_DivFix( temp[2], temp_scale ); |
|
183 temp[4] = FT_DivFix( temp[4], temp_scale ); |
|
184 temp[5] = FT_DivFix( temp[5], temp_scale ); |
|
185 temp[3] = 0x10000L; |
|
186 } |
|
187 |
|
188 matrix->xx = temp[0]; |
|
189 matrix->yx = temp[1]; |
|
190 matrix->xy = temp[2]; |
|
191 matrix->yy = temp[3]; |
|
192 |
|
193 /* note that the font offsets are expressed in integer font units */ |
|
194 offset->x = temp[4] >> 16; |
|
195 offset->y = temp[5] >> 16; |
|
196 } |
|
197 |
|
198 return CID_Err_Ok; /* this is a callback function; */ |
|
199 /* we must return an error code */ |
|
200 } |
|
201 |
|
202 |
|
203 FT_CALLBACK_DEF( FT_Error ) |
|
204 parse_fd_array( CID_Face face, |
|
205 CID_Parser* parser ) |
|
206 { |
|
207 CID_FaceInfo cid = &face->cid; |
|
208 FT_Memory memory = face->root.memory; |
|
209 FT_Error error = CID_Err_Ok; |
|
210 FT_Long num_dicts; |
|
211 |
|
212 |
|
213 num_dicts = cid_parser_to_int( parser ); |
|
214 |
|
215 if ( !cid->font_dicts ) |
|
216 { |
|
217 FT_Int n; |
|
218 |
|
219 |
|
220 if ( FT_NEW_ARRAY( cid->font_dicts, num_dicts ) ) |
|
221 goto Exit; |
|
222 |
|
223 cid->num_dicts = (FT_UInt)num_dicts; |
|
224 |
|
225 /* don't forget to set a few defaults */ |
|
226 for ( n = 0; n < cid->num_dicts; n++ ) |
|
227 { |
|
228 CID_FaceDict dict = cid->font_dicts + n; |
|
229 |
|
230 |
|
231 /* default value for lenIV */ |
|
232 dict->private_dict.lenIV = 4; |
|
233 } |
|
234 } |
|
235 |
|
236 Exit: |
|
237 return error; |
|
238 } |
|
239 |
|
240 |
|
241 /* by mistake, `expansion_factor' appears both in PS_PrivateRec */ |
|
242 /* and CID_FaceDictRec (both are public header files and can't */ |
|
243 /* changed); we simply copy the value */ |
|
244 |
|
245 FT_CALLBACK_DEF( FT_Error ) |
|
246 parse_expansion_factor( CID_Face face, |
|
247 CID_Parser* parser ) |
|
248 { |
|
249 CID_FaceDict dict; |
|
250 |
|
251 |
|
252 if ( parser->num_dict >= 0 ) |
|
253 { |
|
254 dict = face->cid.font_dicts + parser->num_dict; |
|
255 |
|
256 dict->expansion_factor = cid_parser_to_fixed( parser, 0 ); |
|
257 dict->private_dict.expansion_factor = dict->expansion_factor; |
|
258 } |
|
259 |
|
260 return CID_Err_Ok; |
|
261 } |
|
262 |
|
263 |
|
264 static |
|
265 const T1_FieldRec cid_field_records[] = |
|
266 { |
|
267 |
|
268 #include "cidtoken.h" |
|
269 |
|
270 T1_FIELD_CALLBACK( "FDArray", parse_fd_array, 0 ) |
|
271 T1_FIELD_CALLBACK( "FontMatrix", parse_font_matrix, 0 ) |
|
272 T1_FIELD_CALLBACK( "ExpansionFactor", parse_expansion_factor, 0 ) |
|
273 |
|
274 { 0, T1_FIELD_LOCATION_CID_INFO, T1_FIELD_TYPE_NONE, 0, 0, 0, 0, 0, 0 } |
|
275 }; |
|
276 |
|
277 |
|
278 static FT_Error |
|
279 cid_parse_dict( CID_Face face, |
|
280 CID_Loader* loader, |
|
281 FT_Byte* base, |
|
282 FT_Long size ) |
|
283 { |
|
284 CID_Parser* parser = &loader->parser; |
|
285 |
|
286 |
|
287 parser->root.cursor = base; |
|
288 parser->root.limit = base + size; |
|
289 parser->root.error = CID_Err_Ok; |
|
290 |
|
291 { |
|
292 FT_Byte* cur = base; |
|
293 FT_Byte* limit = cur + size; |
|
294 |
|
295 |
|
296 for (;;) |
|
297 { |
|
298 FT_Byte* newlimit; |
|
299 |
|
300 |
|
301 parser->root.cursor = cur; |
|
302 cid_parser_skip_spaces( parser ); |
|
303 |
|
304 if ( parser->root.cursor >= limit ) |
|
305 newlimit = limit - 1 - 17; |
|
306 else |
|
307 newlimit = parser->root.cursor - 17; |
|
308 |
|
309 /* look for `%ADOBeginFontDict' */ |
|
310 for ( ; cur < newlimit; cur++ ) |
|
311 { |
|
312 if ( *cur == '%' && |
|
313 ft_strncmp( (char*)cur, "%ADOBeginFontDict", 17 ) == 0 ) |
|
314 { |
|
315 /* if /FDArray was found, then cid->num_dicts is > 0, and */ |
|
316 /* we can start increasing parser->num_dict */ |
|
317 if ( face->cid.num_dicts > 0 ) |
|
318 parser->num_dict++; |
|
319 } |
|
320 } |
|
321 |
|
322 cur = parser->root.cursor; |
|
323 /* no error can occur in cid_parser_skip_spaces */ |
|
324 if ( cur >= limit ) |
|
325 break; |
|
326 |
|
327 cid_parser_skip_PS_token( parser ); |
|
328 if ( parser->root.cursor >= limit || parser->root.error ) |
|
329 break; |
|
330 |
|
331 /* look for immediates */ |
|
332 if ( *cur == '/' && cur + 2 < limit ) |
|
333 { |
|
334 FT_PtrDist len; |
|
335 |
|
336 |
|
337 cur++; |
|
338 len = parser->root.cursor - cur; |
|
339 |
|
340 if ( len > 0 && len < 22 ) |
|
341 { |
|
342 /* now compare the immediate name to the keyword table */ |
|
343 T1_Field keyword = (T1_Field)cid_field_records; |
|
344 |
|
345 |
|
346 for (;;) |
|
347 { |
|
348 FT_Byte* name; |
|
349 |
|
350 |
|
351 name = (FT_Byte*)keyword->ident; |
|
352 if ( !name ) |
|
353 break; |
|
354 |
|
355 if ( cur[0] == name[0] && |
|
356 len == (FT_PtrDist)ft_strlen( (const char*)name ) ) |
|
357 { |
|
358 FT_PtrDist n; |
|
359 |
|
360 |
|
361 for ( n = 1; n < len; n++ ) |
|
362 if ( cur[n] != name[n] ) |
|
363 break; |
|
364 |
|
365 if ( n >= len ) |
|
366 { |
|
367 /* we found it - run the parsing callback */ |
|
368 parser->root.error = cid_load_keyword( face, |
|
369 loader, |
|
370 keyword ); |
|
371 if ( parser->root.error ) |
|
372 return parser->root.error; |
|
373 break; |
|
374 } |
|
375 } |
|
376 keyword++; |
|
377 } |
|
378 } |
|
379 } |
|
380 |
|
381 cur = parser->root.cursor; |
|
382 } |
|
383 } |
|
384 return parser->root.error; |
|
385 } |
|
386 |
|
387 |
|
388 /* read the subrmap and the subrs of each font dict */ |
|
389 static FT_Error |
|
390 cid_read_subrs( CID_Face face ) |
|
391 { |
|
392 CID_FaceInfo cid = &face->cid; |
|
393 FT_Memory memory = face->root.memory; |
|
394 FT_Stream stream = face->cid_stream; |
|
395 FT_Error error; |
|
396 FT_Int n; |
|
397 CID_Subrs subr; |
|
398 FT_UInt max_offsets = 0; |
|
399 FT_ULong* offsets = 0; |
|
400 PSAux_Service psaux = (PSAux_Service)face->psaux; |
|
401 |
|
402 |
|
403 if ( FT_NEW_ARRAY( face->subrs, cid->num_dicts ) ) |
|
404 goto Exit; |
|
405 |
|
406 subr = face->subrs; |
|
407 for ( n = 0; n < cid->num_dicts; n++, subr++ ) |
|
408 { |
|
409 CID_FaceDict dict = cid->font_dicts + n; |
|
410 FT_Int lenIV = dict->private_dict.lenIV; |
|
411 FT_UInt count, num_subrs = dict->num_subrs; |
|
412 FT_ULong data_len; |
|
413 FT_Byte* p; |
|
414 |
|
415 |
|
416 /* reallocate offsets array if needed */ |
|
417 if ( num_subrs + 1 > max_offsets ) |
|
418 { |
|
419 FT_UInt new_max = FT_PAD_CEIL( num_subrs + 1, 4 ); |
|
420 |
|
421 |
|
422 if ( FT_RENEW_ARRAY( offsets, max_offsets, new_max ) ) |
|
423 goto Fail; |
|
424 |
|
425 max_offsets = new_max; |
|
426 } |
|
427 |
|
428 /* read the subrmap's offsets */ |
|
429 if ( FT_STREAM_SEEK( cid->data_offset + dict->subrmap_offset ) || |
|
430 FT_FRAME_ENTER( ( num_subrs + 1 ) * dict->sd_bytes ) ) |
|
431 goto Fail; |
|
432 |
|
433 p = (FT_Byte*)stream->cursor; |
|
434 for ( count = 0; count <= num_subrs; count++ ) |
|
435 offsets[count] = cid_get_offset( &p, (FT_Byte)dict->sd_bytes ); |
|
436 |
|
437 FT_FRAME_EXIT(); |
|
438 |
|
439 /* now, compute the size of subrs charstrings, */ |
|
440 /* allocate, and read them */ |
|
441 data_len = offsets[num_subrs] - offsets[0]; |
|
442 |
|
443 if ( FT_NEW_ARRAY( subr->code, num_subrs + 1 ) || |
|
444 FT_ALLOC( subr->code[0], data_len ) ) |
|
445 goto Fail; |
|
446 |
|
447 if ( FT_STREAM_SEEK( cid->data_offset + offsets[0] ) || |
|
448 FT_STREAM_READ( subr->code[0], data_len ) ) |
|
449 goto Fail; |
|
450 |
|
451 /* set up pointers */ |
|
452 for ( count = 1; count <= num_subrs; count++ ) |
|
453 { |
|
454 FT_ULong len; |
|
455 |
|
456 |
|
457 len = offsets[count] - offsets[count - 1]; |
|
458 subr->code[count] = subr->code[count - 1] + len; |
|
459 } |
|
460 |
|
461 /* decrypt subroutines, but only if lenIV >= 0 */ |
|
462 if ( lenIV >= 0 ) |
|
463 { |
|
464 for ( count = 0; count < num_subrs; count++ ) |
|
465 { |
|
466 FT_ULong len; |
|
467 |
|
468 |
|
469 len = offsets[count + 1] - offsets[count]; |
|
470 psaux->t1_decrypt( subr->code[count], len, 4330 ); |
|
471 } |
|
472 } |
|
473 |
|
474 subr->num_subrs = num_subrs; |
|
475 } |
|
476 |
|
477 Exit: |
|
478 FT_FREE( offsets ); |
|
479 return error; |
|
480 |
|
481 Fail: |
|
482 if ( face->subrs ) |
|
483 { |
|
484 for ( n = 0; n < cid->num_dicts; n++ ) |
|
485 { |
|
486 if ( face->subrs[n].code ) |
|
487 FT_FREE( face->subrs[n].code[0] ); |
|
488 |
|
489 FT_FREE( face->subrs[n].code ); |
|
490 } |
|
491 FT_FREE( face->subrs ); |
|
492 } |
|
493 goto Exit; |
|
494 } |
|
495 |
|
496 |
|
497 static void |
|
498 t1_init_loader( CID_Loader* loader, |
|
499 CID_Face face ) |
|
500 { |
|
501 FT_UNUSED( face ); |
|
502 |
|
503 FT_MEM_ZERO( loader, sizeof ( *loader ) ); |
|
504 } |
|
505 |
|
506 |
|
507 static void |
|
508 t1_done_loader( CID_Loader* loader ) |
|
509 { |
|
510 CID_Parser* parser = &loader->parser; |
|
511 |
|
512 |
|
513 /* finalize parser */ |
|
514 cid_parser_done( parser ); |
|
515 } |
|
516 |
|
517 |
|
518 static FT_Error |
|
519 cid_hex_to_binary( FT_Byte* data, |
|
520 FT_Long data_len, |
|
521 FT_ULong offset, |
|
522 CID_Face face ) |
|
523 { |
|
524 FT_Stream stream = face->root.stream; |
|
525 FT_Error error; |
|
526 |
|
527 FT_Byte buffer[256]; |
|
528 FT_Byte *p, *plimit; |
|
529 FT_Byte *d, *dlimit; |
|
530 FT_Byte val; |
|
531 |
|
532 FT_Bool upper_nibble, done; |
|
533 |
|
534 |
|
535 if ( FT_STREAM_SEEK( offset ) ) |
|
536 goto Exit; |
|
537 |
|
538 d = data; |
|
539 dlimit = d + data_len; |
|
540 p = buffer; |
|
541 plimit = p; |
|
542 |
|
543 upper_nibble = 1; |
|
544 done = 0; |
|
545 |
|
546 while ( d < dlimit ) |
|
547 { |
|
548 if ( p >= plimit ) |
|
549 { |
|
550 FT_ULong oldpos = FT_STREAM_POS(); |
|
551 FT_ULong size = stream->size - oldpos; |
|
552 |
|
553 |
|
554 if ( size == 0 ) |
|
555 { |
|
556 error = CID_Err_Syntax_Error; |
|
557 goto Exit; |
|
558 } |
|
559 |
|
560 if ( FT_STREAM_READ( buffer, 256 > size ? size : 256 ) ) |
|
561 goto Exit; |
|
562 p = buffer; |
|
563 plimit = p + FT_STREAM_POS() - oldpos; |
|
564 } |
|
565 |
|
566 if ( ft_isdigit( *p ) ) |
|
567 val = (FT_Byte)( *p - '0' ); |
|
568 else if ( *p >= 'a' && *p <= 'f' ) |
|
569 val = (FT_Byte)( *p - 'a' ); |
|
570 else if ( *p >= 'A' && *p <= 'F' ) |
|
571 val = (FT_Byte)( *p - 'A' + 10 ); |
|
572 else if ( *p == ' ' || |
|
573 *p == '\t' || |
|
574 *p == '\r' || |
|
575 *p == '\n' || |
|
576 *p == '\f' || |
|
577 *p == '\0' ) |
|
578 { |
|
579 p++; |
|
580 continue; |
|
581 } |
|
582 else if ( *p == '>' ) |
|
583 { |
|
584 val = 0; |
|
585 done = 1; |
|
586 } |
|
587 else |
|
588 { |
|
589 error = CID_Err_Syntax_Error; |
|
590 goto Exit; |
|
591 } |
|
592 |
|
593 if ( upper_nibble ) |
|
594 *d = (FT_Byte)( val << 4 ); |
|
595 else |
|
596 { |
|
597 *d = (FT_Byte)( *d + val ); |
|
598 d++; |
|
599 } |
|
600 |
|
601 upper_nibble = (FT_Byte)( 1 - upper_nibble ); |
|
602 |
|
603 if ( done ) |
|
604 break; |
|
605 |
|
606 p++; |
|
607 } |
|
608 |
|
609 error = CID_Err_Ok; |
|
610 |
|
611 Exit: |
|
612 return error; |
|
613 } |
|
614 |
|
615 |
|
616 FT_LOCAL_DEF( FT_Error ) |
|
617 cid_face_open( CID_Face face, |
|
618 FT_Int face_index ) |
|
619 { |
|
620 CID_Loader loader; |
|
621 CID_Parser* parser; |
|
622 FT_Memory memory = face->root.memory; |
|
623 FT_Error error; |
|
624 |
|
625 |
|
626 t1_init_loader( &loader, face ); |
|
627 |
|
628 parser = &loader.parser; |
|
629 error = cid_parser_new( parser, face->root.stream, face->root.memory, |
|
630 (PSAux_Service)face->psaux ); |
|
631 if ( error ) |
|
632 goto Exit; |
|
633 |
|
634 error = cid_parse_dict( face, &loader, |
|
635 parser->postscript, |
|
636 parser->postscript_len ); |
|
637 if ( error ) |
|
638 goto Exit; |
|
639 |
|
640 if ( face_index < 0 ) |
|
641 goto Exit; |
|
642 |
|
643 if ( FT_NEW( face->cid_stream ) ) |
|
644 goto Exit; |
|
645 |
|
646 if ( parser->binary_length ) |
|
647 { |
|
648 /* we must convert the data section from hexadecimal to binary */ |
|
649 if ( FT_ALLOC( face->binary_data, parser->binary_length ) || |
|
650 cid_hex_to_binary( face->binary_data, parser->binary_length, |
|
651 parser->data_offset, face ) ) |
|
652 goto Exit; |
|
653 |
|
654 FT_Stream_OpenMemory( face->cid_stream, |
|
655 face->binary_data, parser->binary_length ); |
|
656 face->cid.data_offset = 0; |
|
657 } |
|
658 else |
|
659 { |
|
660 *face->cid_stream = *face->root.stream; |
|
661 face->cid.data_offset = loader.parser.data_offset; |
|
662 } |
|
663 |
|
664 error = cid_read_subrs( face ); |
|
665 |
|
666 Exit: |
|
667 t1_done_loader( &loader ); |
|
668 return error; |
|
669 } |
|
670 |
|
671 |
|
672 /* END */ |