1 /* $Id: iptcutil.c,v 1.11 2015-06-21 01:09:09 bfriesen Exp $ */
2
3 #include "tif_config.h"
4
5 #include <stdio.h>
6 #include <stdlib.h>
7 #include <string.h>
8 #include <ctype.h>
9
10 #ifdef HAVE_STRINGS_H
11 # include <strings.h>
12 #endif
13
14 #ifdef HAVE_IO_H
15 # include <io.h>
16 #endif
17
18 #ifdef HAVE_FCNTL_H
19 # include <fcntl.h>
20 #endif
21
22 #ifdef WIN32
23 #define STRNICMP strnicmp
24 #else
25 #define STRNICMP strncasecmp
26 #endif
27
28 typedef struct _tag_spec
29 {
30 short
31 id;
32
33 char
34 *name;
35 } tag_spec;
36
37 static tag_spec tags[] = {
38 { 5,"Image Name" },
39 { 7,"Edit Status" },
40 { 10,"Priority" },
41 { 15,"Category" },
42 { 20,"Supplemental Category" },
43 { 22,"Fixture Identifier" },
44 { 25,"Keyword" },
45 { 30,"Release Date" },
46 { 35,"Release Time" },
47 { 40,"Special Instructions" },
48 { 45,"Reference Service" },
49 { 47,"Reference Date" },
50 { 50,"Reference Number" },
51 { 55,"Created Date" },
52 { 60,"Created Time" },
53 { 65,"Originating Program" },
54 { 70,"Program Version" },
55 { 75,"Object Cycle" },
56 { 80,"Byline" },
57 { 85,"Byline Title" },
58 { 90,"City" },
59 { 95,"Province State" },
60 { 100,"Country Code" },
61 { 101,"Country" },
62 { 103,"Original Transmission Reference" },
63 { 105,"Headline" },
64 { 110,"Credit" },
65 { 115,"Source" },
66 { 116,"Copyright String" },
67 { 120,"Caption" },
68 { 121,"Local Caption" },
69 { 122,"Caption Writer" },
70 { 200,"Custom Field 1" },
71 { 201,"Custom Field 2" },
72 { 202,"Custom Field 3" },
73 { 203,"Custom Field 4" },
74 { 204,"Custom Field 5" },
75 { 205,"Custom Field 6" },
76 { 206,"Custom Field 7" },
77 { 207,"Custom Field 8" },
78 { 208,"Custom Field 9" },
79 { 209,"Custom Field 10" },
80 { 210,"Custom Field 11" },
81 { 211,"Custom Field 12" },
82 { 212,"Custom Field 13" },
83 { 213,"Custom Field 14" },
84 { 214,"Custom Field 15" },
85 { 215,"Custom Field 16" },
86 { 216,"Custom Field 17" },
87 { 217,"Custom Field 18" },
88 { 218,"Custom Field 19" },
89 { 219,"Custom Field 20" }
90 };
91
92 /*
93 * We format the output using HTML conventions
94 * to preserve control characters and such.
95 */
formatString(FILE * ofile,const char * s,int len)96 void formatString(FILE *ofile, const char *s, int len)
97 {
98 putc('"', ofile);
99 for (; len > 0; --len, ++s) {
100 int c = *s;
101 switch (c) {
102 case '&':
103 fputs("&", ofile);
104 break;
105 #ifdef HANDLE_GT_LT
106 case '<':
107 fputs("<", ofile);
108 break;
109 case '>':
110 fputs(">", ofile);
111 break;
112 #endif
113 case '"':
114 fputs(""", ofile);
115 break;
116 default:
117 if (iscntrl(c))
118 fprintf(ofile, "&#%d;", c);
119 else
120 putc(*s, ofile);
121 break;
122 }
123 }
124 fputs("\"\n", ofile);
125 }
126
127 typedef struct _html_code
128 {
129 short
130 len;
131 const char
132 *code,
133 val;
134 } html_code;
135
136 static html_code html_codes[] = {
137 #ifdef HANDLE_GT_LT
138 { 4,"<",'<' },
139 { 4,">",'>' },
140 #endif
141 { 5,"&",'&' },
142 { 6,""",'"' }
143 };
144
145 /*
146 * This routine converts HTML escape sequence
147 * back to the original ASCII representation.
148 * - returns the number of characters dropped.
149 */
convertHTMLcodes(char * s,int len)150 int convertHTMLcodes(char *s, int len)
151 {
152 if (len <=0 || s==(char*)NULL || *s=='\0')
153 return 0;
154
155 if (s[1] == '#')
156 {
157 int val, o;
158
159 if (sscanf(s,"&#%d;",&val) == 1)
160 {
161 o = 3;
162 while (s[o] != ';')
163 {
164 o++;
165 if (o > 5)
166 break;
167 }
168 if (o < 5)
169 strcpy(s+1, s+1+o);
170 *s = val;
171 return o;
172 }
173 }
174 else
175 {
176 int
177 i,
178 codes = sizeof(html_codes) / sizeof(html_code);
179
180 for (i=0; i < codes; i++)
181 {
182 if (html_codes[i].len <= len)
183 if (STRNICMP(s, html_codes[i].code, html_codes[i].len) == 0)
184 {
185 strcpy(s+1, s+html_codes[i].len);
186 *s = html_codes[i].val;
187 return html_codes[i].len-1;
188 }
189 }
190 }
191
192 return 0;
193 }
194
formatIPTC(FILE * ifile,FILE * ofile)195 int formatIPTC(FILE *ifile, FILE *ofile)
196 {
197 unsigned int
198 foundiptc,
199 tagsfound;
200
201 char
202 *readable,
203 *str;
204
205 long
206 tagindx,
207 taglen;
208
209 int
210 i,
211 tagcount = sizeof(tags) / sizeof(tag_spec);
212
213 int
214 c,
215 dataset,
216 recnum;
217
218 foundiptc = 0; /* found the IPTC-Header */
219 tagsfound = 0; /* number of tags found */
220
221 c = getc(ifile);
222 while (c != EOF)
223 {
224 if (c == 0x1c)
225 foundiptc = 1;
226 else
227 {
228 if (foundiptc)
229 {
230 return -1;
231 }
232 else
233 {
234 c = getc(ifile);
235 continue;
236 }
237 }
238
239 /* we found the 0x1c tag and now grab the dataset and record number tags */
240 dataset = getc(ifile);
241 if ((char) dataset == EOF)
242 return -1;
243 recnum = getc(ifile);
244 if ((char) recnum == EOF)
245 return -1;
246 /* try to match this record to one of the ones in our named table */
247 for (i=0; i< tagcount; i++)
248 {
249 if (tags[i].id == recnum)
250 break;
251 }
252 if (i < tagcount)
253 readable = tags[i].name;
254 else
255 readable = "";
256
257 /* then we decode the length of the block that follows - long or short fmt */
258 c = getc(ifile);
259 if (c == EOF)
260 return 0;
261 if (c & (unsigned char) 0x80)
262 {
263 unsigned char
264 buffer[4];
265
266 for (i=0; i<4; i++)
267 {
268 c = getc(ifile);
269 if (c == EOF)
270 return -1;
271 buffer[i] = c;
272 }
273 taglen = (((long) buffer[ 0 ]) << 24) |
274 (((long) buffer[ 1 ]) << 16) |
275 (((long) buffer[ 2 ]) << 8) |
276 (((long) buffer[ 3 ]));
277 }
278 else
279 {
280 int
281 x = c;
282
283 taglen = x << 8;
284 x = getc(ifile);
285 if (x == EOF)
286 return -1;
287 taglen |= (long) x;
288 }
289 /* Place limits on tag length */
290 if ((taglen <= 0) || (taglen > 1048576))
291 {
292 printf("Inappropriate IPTC tag length %ld\n",taglen);
293 return -1;
294 }
295 /* make a buffer to hold the tag data and snag it from the input stream */
296 str = (char *) malloc((unsigned int) (taglen+1));
297 if (str == (char *) NULL)
298 {
299 printf("Memory allocation failed");
300 return 0;
301 }
302 for (tagindx=0; tagindx<taglen; tagindx++)
303 {
304 c = getc(ifile);
305 if (c == EOF)
306 {
307 free(str);
308 return -1;
309 }
310 str[tagindx] = c;
311 }
312 str[ taglen ] = 0;
313
314 /* now finish up by formatting this binary data into ASCII equivalent */
315 if (strlen(readable) > 0)
316 fprintf(ofile, "%d#%d#%s=",(unsigned int)dataset, (unsigned int) recnum, readable);
317 else
318 fprintf(ofile, "%d#%d=",(unsigned int)dataset, (unsigned int) recnum);
319 formatString( ofile, str, taglen );
320 free(str);
321
322 tagsfound++;
323
324 c = getc(ifile);
325 }
326 return tagsfound;
327 }
328
329 int tokenizer(unsigned inflag,char *token,int tokmax,char *line,
330 char *white,char *brkchar,char *quote,char eschar,char *brkused,
331 int *next,char *quoted);
332
super_fgets(char * b,int * blen,FILE * file)333 char *super_fgets(char *b, int *blen, FILE *file)
334 {
335 int
336 c,
337 len;
338
339 char
340 *q;
341
342 len=*blen;
343 for (q=b; ; q++)
344 {
345 c=fgetc(file);
346 if (c == EOF || c == '\n')
347 break;
348 if (((long)q - (long)b + 1 ) >= (long) len)
349 {
350 long
351 tlen;
352
353 tlen=(long)q-(long)b;
354 len<<=1;
355 b=(char *) realloc((char *) b,(len+2));
356 if ((char *) b == (char *) NULL)
357 break;
358 q=b+tlen;
359 }
360 *q=(unsigned char) c;
361 }
362 *blen=0;
363 if ((unsigned char *)b != (unsigned char *) NULL)
364 {
365 int
366 tlen;
367
368 tlen=(long)q - (long)b;
369 if (tlen == 0)
370 return (char *) NULL;
371 b[tlen] = '\0';
372 *blen=++tlen;
373 }
374 return b;
375 }
376
377 #define BUFFER_SZ 4096
378
main(int argc,char * argv[])379 int main(int argc, char *argv[])
380 {
381 /* unsigned int */
382 /* length; */
383
384 /*unsigned char
385 *buffer;*/
386
387 int
388 i,
389 mode; /* iptc binary, or iptc text */
390
391 FILE
392 *ifile = stdin,
393 *ofile = stdout;
394
395 char
396 c,
397 *usage = "usage: iptcutil -t | -b [-i file] [-o file] <input >output";
398
399 if( argc < 2 )
400 {
401 puts(usage);
402 return 1;
403 }
404
405 mode = 0;
406 /* length = -1; */
407 /* buffer = (unsigned char *)NULL; */
408
409 for (i=1; i<argc; i++)
410 {
411 c = argv[i][0];
412 if (c == '-' || c == '/')
413 {
414 c = argv[i][1];
415 switch( c )
416 {
417 case 't':
418 mode = 1;
419 #ifdef WIN32
420 /* Set "stdout" to binary mode: */
421 _setmode( _fileno( ofile ), _O_BINARY );
422 #endif
423 break;
424 case 'b':
425 mode = 0;
426 #ifdef WIN32
427 /* Set "stdin" to binary mode: */
428 _setmode( _fileno( ifile ), _O_BINARY );
429 #endif
430 break;
431 case 'i':
432 if (mode == 0)
433 ifile = fopen(argv[++i], "rb");
434 else
435 ifile = fopen(argv[++i], "rt");
436 if (ifile == (FILE *)NULL)
437 {
438 printf("Unable to open: %s\n", argv[i]);
439 return 1;
440 }
441 break;
442 case 'o':
443 if (mode == 0)
444 ofile = fopen(argv[++i], "wt");
445 else
446 ofile = fopen(argv[++i], "wb");
447 if (ofile == (FILE *)NULL)
448 {
449 printf("Unable to open: %s\n", argv[i]);
450 return 1;
451 }
452 break;
453 default:
454 printf("Unknown option: %s\n", argv[i]);
455 return 1;
456 }
457 }
458 else
459 {
460 puts(usage);
461 return 1;
462 }
463 }
464
465 if (mode == 0) /* handle binary iptc info */
466 formatIPTC(ifile, ofile);
467
468 if (mode == 1) /* handle text form of iptc info */
469 {
470 char
471 brkused,
472 quoted,
473 *line,
474 *token,
475 *newstr;
476
477 int
478 state,
479 next;
480
481 unsigned char
482 recnum = 0,
483 dataset = 0;
484
485 int
486 inputlen = BUFFER_SZ;
487
488 line = (char *) malloc(inputlen);
489 token = (char *)NULL;
490 while((line = super_fgets(line,&inputlen,ifile))!=NULL)
491 {
492 state=0;
493 next=0;
494
495 token = (char *) malloc(inputlen);
496 newstr = (char *) malloc(inputlen);
497 while(tokenizer(0, token, inputlen, line, "", "=", "\"", 0,
498 &brkused,&next,"ed)==0)
499 {
500 if (state == 0)
501 {
502 int
503 state,
504 next;
505
506 char
507 brkused,
508 quoted;
509
510 state=0;
511 next=0;
512 while(tokenizer(0, newstr, inputlen, token, "", "#", "", 0,
513 &brkused, &next, "ed)==0)
514 {
515 if (state == 0)
516 dataset = (unsigned char) atoi(newstr);
517 else
518 if (state == 1)
519 recnum = (unsigned char) atoi(newstr);
520 state++;
521 }
522 }
523 else
524 if (state == 1)
525 {
526 int
527 next;
528
529 unsigned long
530 len;
531
532 char
533 brkused,
534 quoted;
535
536 next=0;
537 len = strlen(token);
538 while(tokenizer(0, newstr, inputlen, token, "", "&", "", 0,
539 &brkused, &next, "ed)==0)
540 {
541 if (brkused && next > 0)
542 {
543 char
544 *s = &token[next-1];
545
546 len -= convertHTMLcodes(s, strlen(s));
547 }
548 }
549
550 fputc(0x1c, ofile);
551 fputc(dataset, ofile);
552 fputc(recnum, ofile);
553 if (len < 0x10000)
554 {
555 fputc((len >> 8) & 255, ofile);
556 fputc(len & 255, ofile);
557 }
558 else
559 {
560 fputc(((len >> 24) & 255) | 0x80, ofile);
561 fputc((len >> 16) & 255, ofile);
562 fputc((len >> 8) & 255, ofile);
563 fputc(len & 255, ofile);
564 }
565 next=0;
566 while (len--)
567 fputc(token[next++], ofile);
568 }
569 state++;
570 }
571 free(token);
572 token = (char *)NULL;
573 free(newstr);
574 newstr = (char *)NULL;
575 }
576 free(line);
577
578 fclose( ifile );
579 fclose( ofile );
580 }
581
582 return 0;
583 }
584
585 /*
586 This routine is a generalized, finite state token parser. It allows
587 you extract tokens one at a time from a string of characters. The
588 characters used for white space, for break characters, and for quotes
589 can be specified. Also, characters in the string can be preceded by
590 a specifiable escape character which removes any special meaning the
591 character may have.
592
593 There are a lot of formal parameters in this subroutine call, but
594 once you get familiar with them, this routine is fairly easy to use.
595 "#define" macros can be used to generate simpler looking calls for
596 commonly used applications of this routine.
597
598 First, some terminology:
599
600 token: used here, a single unit of information in
601 the form of a group of characters.
602
603 white space: space that gets ignored (except within quotes
604 or when escaped), like blanks and tabs. in
605 addition, white space terminates a non-quoted
606 token.
607
608 break character: a character that separates non-quoted tokens.
609 commas are a common break character. the
610 usage of break characters to signal the end
611 of a token is the same as that of white space,
612 except multiple break characters with nothing
613 or only white space between generate a null
614 token for each two break characters together.
615
616 for example, if blank is set to be the white
617 space and comma is set to be the break
618 character, the line ...
619
620 A, B, C , , DEF
621
622 ... consists of 5 tokens:
623
624 1) "A"
625 2) "B"
626 3) "C"
627 4) "" (the null string)
628 5) "DEF"
629
630 quote character: a character that, when surrounding a group
631 of other characters, causes the group of
632 characters to be treated as a single token,
633 no matter how many white spaces or break
634 characters exist in the group. also, a
635 token always terminates after the closing
636 quote. for example, if ' is the quote
637 character, blank is white space, and comma
638 is the break character, the following
639 string ...
640
641 A, ' B, CD'EF GHI
642
643 ... consists of 4 tokens:
644
645 1) "A"
646 2) " B, CD" (note the blanks & comma)
647 3) "EF"
648 4) "GHI"
649
650 the quote characters themselves do
651 not appear in the resultant tokens. the
652 double quotes are delimiters i use here for
653 documentation purposes only.
654
655 escape character: a character which itself is ignored but
656 which causes the next character to be
657 used as is. ^ and \ are often used as
658 escape characters. an escape in the last
659 position of the string gets treated as a
660 "normal" (i.e., non-quote, non-white,
661 non-break, and non-escape) character.
662 for example, assume white space, break
663 character, and quote are the same as in the
664 above examples, and further, assume that
665 ^ is the escape character. then, in the
666 string ...
667
668 ABC, ' DEF ^' GH' I ^ J K^ L ^
669
670 ... there are 7 tokens:
671
672 1) "ABC"
673 2) " DEF ' GH"
674 3) "I"
675 4) " " (a lone blank)
676 5) "J"
677 6) "K L"
678 7) "^" (passed as is at end of line)
679
680
681 OK, now that you have this background, here's how to call "tokenizer":
682
683 result=tokenizer(flag,token,maxtok,string,white,break,quote,escape,
684 brkused,next,quoted)
685
686 result: 0 if we haven't reached EOS (end of string), and
687 1 if we have (this is an "int").
688
689 flag: right now, only the low order 3 bits are used.
690 1 => convert non-quoted tokens to upper case
691 2 => convert non-quoted tokens to lower case
692 0 => do not convert non-quoted tokens
693 (this is a "char").
694
695 token: a character string containing the returned next token
696 (this is a "char[]").
697
698 maxtok: the maximum size of "token". characters beyond
699 "maxtok" are truncated (this is an "int").
700
701 string: the string to be parsed (this is a "char[]").
702
703 white: a string of the valid white spaces. example:
704
705 char whitesp[]={" \t"};
706
707 blank and tab will be valid white space (this is
708 a "char[]").
709
710 break: a string of the valid break characters. example:
711
712 char breakch[]={";,"};
713
714 semicolon and comma will be valid break characters
715 (this is a "char[]").
716
717 IMPORTANT: do not use the name "break" as a C
718 variable, as this is a reserved word in C.
719
720 quote: a string of the valid quote characters. an example
721 would be
722
723 char whitesp[]={"'\"");
724
725 (this causes single and double quotes to be valid)
726 note that a token starting with one of these characters
727 needs the same quote character to terminate it.
728
729 for example,
730
731 "ABC '
732
733 is unterminated, but
734
735 "DEF" and 'GHI'
736
737 are properly terminated. note that different quote
738 characters can appear on the same line; only for
739 a given token do the quote characters have to be
740 the same (this is a "char[]").
741
742 escape: the escape character (NOT a string ... only one
743 allowed). use zero if none is desired (this is
744 a "char").
745
746 brkused: the break character used to terminate the current
747 token. if the token was quoted, this will be the
748 quote used. if the token is the last one on the
749 line, this will be zero (this is a pointer to a
750 "char").
751
752 next: this variable points to the first character of the
753 next token. it gets reset by "tokenizer" as it steps
754 through the string. set it to 0 upon initialization,
755 and leave it alone after that. you can change it
756 if you want to jump around in the string or re-parse
757 from the beginning, but be careful (this is a
758 pointer to an "int").
759
760 quoted: set to 1 (true) if the token was quoted and 0 (false)
761 if not. you may need this information (for example:
762 in C, a string with quotes around it is a character
763 string, while one without is an identifier).
764
765 (this is a pointer to a "char").
766 */
767
768 /* states */
769
770 #define IN_WHITE 0
771 #define IN_TOKEN 1
772 #define IN_QUOTE 2
773 #define IN_OZONE 3
774
775 int _p_state; /* current state */
776 unsigned _p_flag; /* option flag */
777 char _p_curquote; /* current quote char */
778 int _p_tokpos; /* current token pos */
779
780 /* routine to find character in string ... used only by "tokenizer" */
781
sindex(char ch,char * string)782 int sindex(char ch,char *string)
783 {
784 char *cp;
785 for(cp=string;*cp;++cp)
786 if(ch==*cp)
787 return (int)(cp-string); /* return postion of character */
788 return -1; /* eol ... no match found */
789 }
790
791 /* routine to store a character in a string ... used only by "tokenizer" */
792
chstore(char * string,int max,char ch)793 void chstore(char *string,int max,char ch)
794 {
795 char c;
796 if(_p_tokpos>=0&&_p_tokpos<max-1)
797 {
798 if(_p_state==IN_QUOTE)
799 c=ch;
800 else
801 switch(_p_flag&3)
802 {
803 case 1: /* convert to upper */
804 c=toupper((int) ch);
805 break;
806
807 case 2: /* convert to lower */
808 c=tolower((int) ch);
809 break;
810
811 default: /* use as is */
812 c=ch;
813 break;
814 }
815 string[_p_tokpos++]=c;
816 }
817 return;
818 }
819
tokenizer(unsigned inflag,char * token,int tokmax,char * line,char * white,char * brkchar,char * quote,char eschar,char * brkused,int * next,char * quoted)820 int tokenizer(unsigned inflag,char *token,int tokmax,char *line,
821 char *white,char *brkchar,char *quote,char eschar,char *brkused,
822 int *next,char *quoted)
823 {
824 int qp;
825 char c,nc;
826
827 *brkused=0; /* initialize to null */
828 *quoted=0; /* assume not quoted */
829
830 if(!line[*next]) /* if we're at end of line, indicate such */
831 return 1;
832
833 _p_state=IN_WHITE; /* initialize state */
834 _p_curquote=0; /* initialize previous quote char */
835 _p_flag=inflag; /* set option flag */
836
837 for(_p_tokpos=0;(c=line[*next]);++(*next)) /* main loop */
838 {
839 if((qp=sindex(c,brkchar))>=0) /* break */
840 {
841 switch(_p_state)
842 {
843 case IN_WHITE: /* these are the same here ... */
844 case IN_TOKEN: /* ... just get out */
845 case IN_OZONE: /* ditto */
846 ++(*next);
847 *brkused=brkchar[qp];
848 goto byebye;
849
850 case IN_QUOTE: /* just keep going */
851 chstore(token,tokmax,c);
852 break;
853 }
854 }
855 else if((qp=sindex(c,quote))>=0) /* quote */
856 {
857 switch(_p_state)
858 {
859 case IN_WHITE: /* these are identical, */
860 _p_state=IN_QUOTE; /* change states */
861 _p_curquote=quote[qp]; /* save quote char */
862 *quoted=1; /* set to true as long as something is in quotes */
863 break;
864
865 case IN_QUOTE:
866 if(quote[qp]==_p_curquote) /* same as the beginning quote? */
867 {
868 _p_state=IN_OZONE;
869 _p_curquote=0;
870 }
871 else
872 chstore(token,tokmax,c); /* treat as regular char */
873 break;
874
875 case IN_TOKEN:
876 case IN_OZONE:
877 *brkused=c; /* uses quote as break char */
878 goto byebye;
879 }
880 }
881 else if((qp=sindex(c,white))>=0) /* white */
882 {
883 switch(_p_state)
884 {
885 case IN_WHITE:
886 case IN_OZONE:
887 break; /* keep going */
888
889 case IN_TOKEN:
890 _p_state=IN_OZONE;
891 break;
892
893 case IN_QUOTE:
894 chstore(token,tokmax,c); /* it's valid here */
895 break;
896 }
897 }
898 else if(c==eschar) /* escape */
899 {
900 nc=line[(*next)+1];
901 if(nc==0) /* end of line */
902 {
903 *brkused=0;
904 chstore(token,tokmax,c);
905 ++(*next);
906 goto byebye;
907 }
908 switch(_p_state)
909 {
910 case IN_WHITE:
911 --(*next);
912 _p_state=IN_TOKEN;
913 break;
914
915 case IN_TOKEN:
916 case IN_QUOTE:
917 ++(*next);
918 chstore(token,tokmax,nc);
919 break;
920
921 case IN_OZONE:
922 goto byebye;
923 }
924 }
925 else /* anything else is just a real character */
926 {
927 switch(_p_state)
928 {
929 case IN_WHITE:
930 _p_state=IN_TOKEN; /* switch states */
931
932 case IN_TOKEN: /* these 2 are */
933 case IN_QUOTE: /* identical here */
934 chstore(token,tokmax,c);
935 break;
936
937 case IN_OZONE:
938 goto byebye;
939 }
940 }
941 } /* end of main loop */
942
943 byebye:
944 token[_p_tokpos]=0; /* make sure token ends with EOS */
945
946 return 0;
947 }
948 /*
949 * Local Variables:
950 * mode: c
951 * c-basic-offset: 2
952 * fill-column: 78
953 * End:
954 */
955