1 /***************************************************************************
2                           ADM_vidDecTelecide  -  description
3                              -------------------
4 
5     email                : fixounet@free.fr
6 
7     Port of Donal Graft Decimate which is (c) Donald Graft
8     http://www.neuron2.net
9     http://puschpull.org/avisynth/decomb_reference_manual.html
10 
11  ***************************************************************************/
12 
13 /*
14 	Decimate plugin for Avisynth -- performs 1-in-N
15 	decimation on a stream of progressive frames, which are usually
16 	obtained from the output of my Telecide plugin for Avisynth.
17 	For each group of N successive frames, this filter deletes the
18 	frame that is most similar to its predecessor. Thus, duplicate
19 	frames coming out of Telecide can be removed using Decimate. This
20 	filter adjusts the frame rate of the clip as
21 	appropriate. Selection of the cycle size is selected by specifying
22 	a parameter to Decimate() in the Avisynth scipt.
23 
24 	Copyright (C) 2003 Donald A. Graft
25 
26 	This program is free software; you can redistribute it and/or modify
27 	it under the terms of the GNU General Public License as published by
28 	the Free Software Foundation.
29 
30 	This program is distributed in the hope that it will be useful,
31 	but WITHOUT ANY WARRANTY; without even the implied warranty of
32 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
33 	GNU General Public License for more details.
34 
35 	You should have received a copy of the GNU General Public License
36 	along with this program; if not, write to the Free Software
37 	Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
38 
39 	The author can be contacted at:
40 	Donald Graft
41 	neuron2@attbi.com.
42 */
43 
44 #include "ADM_default.h"
45 #include "decimate.h"
46 //#define BENCH 1
47 typedef uint32_t decimateDeltaLine(uint8_t *ptr1,uint8_t *ptr2,int size,int inc,unsigned int *sums);
48 /**
49     \fn decimateDeltaLineC
50     \brief C version
51 */
decimateDeltaLineC(uint8_t * ptr1,uint8_t * ptr2,int size,int inc,unsigned int * sums)52 static inline uint32_t decimateDeltaLineC(uint8_t *ptr1,uint8_t *ptr2,int size,int inc,unsigned int *sums)
53 {
54         uint32_t total=0;
55         for (int x = 0; x < size;)
56         {
57             unsigned int quadsum=0;
58             quadsum += abs((int)ptr1[x+0] - (int)ptr2[x+0]);
59             quadsum += abs((int)ptr1[x+1] - (int)ptr2[x+1]);
60             quadsum += abs((int)ptr1[x+2] - (int)ptr2[x+2]);
61             quadsum += abs((int)ptr1[x+3] - (int)ptr2[x+3]);
62             sums[(x+0)/BLKSIZE]+=quadsum; // 4 increment , BLKSIZE=32 => they are all in the same block
63 #ifdef BENCH
64             total+=quadsum;
65 #endif
66             x+=inc;
67         }
68         return total;
69 }
70 #ifdef ADM_CPU_X86
71 /**
72     \fn decimateDeltaLineSSE
73     \brief SSE/MMX version
74  Only works for inc=4, BLKSIZE=32
75 */
decimateDeltaLineSSE(uint8_t * ptr1,uint8_t * ptr2,int size,int inc,unsigned int * sums)76 static inline uint32_t decimateDeltaLineSSE(uint8_t *ptr1,uint8_t *ptr2,int size,int inc,unsigned int *sums)
77 {
78         uint32_t total1=0,total2=0;
79 #ifdef BENCH
80         total2=decimateDeltaLineC(ptr1,ptr2,size,inc,sums);
81 #endif
82         int size32=size>>5;
83         int left=(size & 31);
84         ADM_assert(inc==4);
85         ADM_assert(BLKSIZE==32);
86         uint64_t out;
87         uint64_t sum;
88         for(int i=0;i<size32;i++)
89         {
90 
91             __asm__(
92                     "pxor           %%mm2,%%mm2 \n"
93                     "\n"
94                     "movq           0(%1),%%mm0  \n"
95                     "movq           0(%2),%%mm1  \n"
96                     "psadbw         %%mm1,%%mm0 \n"
97                     "paddq          %%mm0,%%mm2 \n"
98                     "\n"
99                     "movq           8(%1),%%mm0  \n"
100                     "movq           8(%2),%%mm1  \n"
101                     "psadbw         %%mm1,%%mm0 \n"
102                     "paddq          %%mm0,%%mm2 \n"
103                     "\n"
104                     "movq           16(%1),%%mm0  \n"
105                     "movq           16(%2),%%mm1  \n"
106                     "psadbw         %%mm1,%%mm0 \n"
107                     "paddq          %%mm0,%%mm2 \n"
108                     "\n"
109                     "movq           24(%1),%%mm0  \n"
110                     "movq           24(%2),%%mm1  \n"
111                     "psadbw         %%mm1,%%mm0 \n"
112                     "paddq          %%mm0,%%mm2 \n"
113                     // move mm2 to sum
114                     "movq           %%mm2,%0\n"
115             : "=m"(sum):  "r" (ptr1),"r" (ptr2));
116             sums[i]+=sum;
117             total1+=sum;
118           //  printf("Sum : %d\n",(int)sum);
119             ptr1+=32;
120             ptr2+=32;
121         }
122         // collect leftover
123         for(int x=0;x<left;)
124         {
125             unsigned int quadsum=0;
126             quadsum += abs((int)ptr1[x+0] - (int)ptr2[x+0]);
127             quadsum += abs((int)ptr1[x+1] - (int)ptr2[x+1]);
128             quadsum += abs((int)ptr1[x+2] - (int)ptr2[x+2]);
129             quadsum += abs((int)ptr1[x+3] - (int)ptr2[x+3]);
130             sums[size32+(x+0)/BLKSIZE]+=quadsum; // 4 increment , BLKSIZE=32 => they are all in the same block
131             x+=inc;
132             total1+=quadsum;
133         }
134 
135         __asm__(
136                     "emms\n"
137                 ::
138                 );
139 #ifdef BENCH
140         if(total1!=total2)
141         {
142             ADM_error("SSE version does not match %d(C) vs %d(SSE)\n",(int)total2,(int)total1);
143         }else
144         {
145             ADM_info("SSE matches C version\n");
146         }
147 #endif
148         return total1;
149 }
150 #endif
151 /**
152     \fn computeDiff
153     \brief compute difference between image and its predecessor
154 */
computeDiff(ADMImage * current,ADMImage * previous)155 uint32_t Decimate::computeDiff(ADMImage *current,ADMImage *previous)
156 {
157     uint8_t *prevY = previous->GetReadPtr(PLANAR_Y);
158     uint8_t *currY = current->GetReadPtr(PLANAR_Y);
159     uint32_t prevPitch=previous->GetPitch(PLANAR_Y);
160     uint32_t curPitch=current->GetPitch(PLANAR_Y);
161     deciMate *_param=&configuration;
162     // Zero
163     memset(sum,0,sizeof(unsigned int)*xblocks*yblocks);
164     // Raw diff
165     int height=info.height;
166     int width=info.width;
167 
168     if (_param->quality == 0 || _param->quality == 1) // subsampled
169     {
170         for (int y = 0; y < height; y++)
171         {
172             unsigned int *xsum=sum+((y/BLKSIZE)*xblocks);
173             decimateDeltaLineC(currY,prevY,width,4+12,xsum);
174             prevY += prevPitch;
175             currY += curPitch;
176         }
177     }else
178     {
179         int inc=4;
180         decimateDeltaLine *func=decimateDeltaLineC;
181 #ifdef ADM_CPU_X86
182          if(CpuCaps::hasSSE())
183             func=decimateDeltaLineSSE;
184 #endif
185 
186         for (int y = 0; y < height; y++)
187         {
188             unsigned int *xsum=sum+((y/BLKSIZE)*xblocks);
189             func(currY,prevY,width,4,xsum);
190             prevY += prevPitch;
191             currY += curPitch;
192         }
193     }
194 //#warning DO CHROMA SAMPLING
195 #if 0
196     if (_param->quality == 1 || _param->quality == 3)
197     {
198         // also do u & v
199         prevU = storepU[f-1];
200         prevV = storepV[f-1];
201         currU = storepU[f];
202         currV = storepV[f];
203         for (y = 0; y < heightUV; y++)
204         {
205             for (x = 0; x < row_sizeUV;)
206             {
207                 sum[((2*y)/BLKSIZE)*xblocks+(2*x)/BLKSIZE] += abs((int)currU[x] - (int)prevU[x]);
208                 sum[((2*y)/BLKSIZE)*xblocks+(2*x)/BLKSIZE] += abs((int)currV[x] - (int)prevV[x]);
209                 x++;
210                 if (_param->quality == 1)
211                 {
212                     if (!(x%4)) x += 12;
213                 }
214             }
215             prevU += pitchUV;
216             currU += pitchUV;
217             prevV += pitchUV;
218             currV += pitchUV;
219 
220         }
221     }
222 #endif
223     uint32_t highest_sum = 0;
224     for (int y = 0; y < yblocks; y++)
225     {
226         for (int x = 0; x < xblocks; x++)
227         {
228             if (sum[y*xblocks+x] > highest_sum)
229             {
230                 highest_sum = sum[y*xblocks+x];
231             }
232         }
233     }
234     return highest_sum;
235 }
236 /**
237     \fn FindDuplicate
238 */
FindDuplicate(int frame,int * chosen,double * metric,bool * forced)239 void Decimate::FindDuplicate(int frame, int *chosen, double *metric, bool *forced)
240 {
241 	int f;
242 	ADMImage  * store[MAX_CYCLE_SIZE+1];
243     deciMate  *_param=&configuration;
244 	int          lowest_index, div;
245 	unsigned int count[MAX_CYCLE_SIZE], lowest;
246 	bool         found;
247 	unsigned int highest_sum=0;
248 
249 	/* Only recalculate differences when a new set is needed. */
250 	if (frame == last_request)
251 	{
252 		*chosen = last_result;
253 		*metric = last_metric;
254 		return;
255 	}
256 	last_request = frame;
257 
258 	/* Get cycle+1 frames starting at the one before the asked-for one. */
259     ADMImage *lastImage=NULL;
260 	for (f = 0; f <= _param->cycle; f++)
261 	{
262 		GETFRAME(frame + f - 1, store[f]);
263         if(store[f]) lastImage=store[f];
264             else store[f]=lastImage;
265         hints_invalid=GetHintingData(lastImage->GetReadPtr(PLANAR_Y),&hints[f]);
266 	}
267 
268     if(!lastImage)
269     {
270         *chosen=-1;
271         ADM_info("Cannot get input image\n");
272         return;
273     }
274 
275     int row_sizeY = info.width; //store[0]->GetRowSize(PLANAR_Y);
276     int heightY = info.height; //store[0]->GetHeight(PLANAR_Y);
277 
278 	int use_quality=_param->quality;
279 
280 
281 	switch (use_quality)
282 	{
283 	case 0: // subsample, luma only
284 		div = (BLKSIZE * BLKSIZE / 4) * 219;
285 		break;
286 	case 1: // subsample, luma and chroma
287 		div = (BLKSIZE * BLKSIZE / 4) * 219 + ( (BLKSIZE * BLKSIZE / 8)) * 224;
288 		break;
289 	case 2: // fully sample, luma only
290 		div = (BLKSIZE * BLKSIZE) * 219;
291 		break;
292 	case 3: // fully sample, luma and chroma
293 		div = (BLKSIZE * BLKSIZE) * 219 + ( BLKSIZE * BLKSIZE/2) * 224;
294 		break;
295 	}
296 
297 	xblocks = row_sizeY / BLKSIZE;
298 	if (row_sizeY % BLKSIZE) xblocks++;
299 	yblocks = heightY / BLKSIZE;
300 	if (heightY % BLKSIZE) yblocks++;
301 
302 	/* Compare each frame to its predecessor. */
303 	for (f = 1; f <= _param->cycle; f++)
304 	{
305 		count[f-1] = computeDiff(store[f],store[f-1]);
306 		showmetrics[f-1] = (count[f-1] * 100.0) / div;
307 	}
308 
309 	/* Find the frame with the lowest difference count but
310 	   don't use the artificial duplicate at frame 0. */
311 	if (frame == 0)
312 	{
313 		lowest = count[1];
314 		lowest_index = 1;
315 	}
316 	else
317 	{
318 		lowest = count[0];
319 		lowest_index = 0;
320 	}
321 	for (int x = 1; x < _param->cycle; x++)
322 	{
323 		if (count[x] < lowest)
324 		{
325 			lowest = count[x];
326 			lowest_index = x;
327 		}
328 	}
329 	last_result = frame + lowest_index;
330 	if (_param->quality == 1 || _param->quality == 3)
331 		last_metric = (lowest * 100.0) / div;
332 	else
333 		last_metric = (lowest * 100.0) / div;
334 	*chosen = last_result;
335 	*metric = last_metric;
336 
337 	found = false;
338 	last_forced = false;
339     return;
340 }
341 /**
342     \fn FindDuplicate2
343     \brief only used for anime mode (find longest dupe sequence)
344 */
FindDuplicate2(int frame,int * chosen,bool * forced)345 void Decimate::FindDuplicate2(int frame, int *chosen, bool *forced)
346 {
347 	int f, g, fsum, bsum, highest, highest_index;
348 	ADMImage * store[MAX_CYCLE_SIZE+1];
349 	const unsigned char *prevY, *prevU, *prevV, *currY, *currU, *currV;
350 	int x, y;
351 	double lowest;
352 	unsigned int lowest_index;
353 	char buf[255];
354 	unsigned int highest_sum;
355 	bool found;
356 #define BLKSIZE 32
357     deciMate *_param=&configuration;
358 	/* Only recalculate differences when a new cycle is started. */
359 	if (frame == last_request)
360 	{
361 		*chosen = last_result;
362 		*forced = last_forced;
363 		return;
364 	}
365 	last_request = frame;
366 
367 	if (firsttime == true || frame == 0)
368 	{
369 		firsttime = false;
370 		for (f = 0; f < MAX_CYCLE_SIZE; f++) Dprev[f] = -1;
371 		for (f = 1; f <= _param->cycle; f++)
372 		{
373 			GETFRAME(frame + f - 1, store[f]);
374 		}
375 
376 		int row_sizeY = info.width; //store[0]->GetRowSize(PLANAR_Y);
377 		int heightY = info.height; //store[0]->GetHeight(PLANAR_Y);
378 
379 		switch (_param->quality)
380 		{
381 		case 0: // subsample, luma only
382 			div = (BLKSIZE * BLKSIZE / 4) * 219;
383 			break;
384 		case 1: // subsample, luma and chroma
385 			div = (BLKSIZE * BLKSIZE / 4) * 219 + (BLKSIZE * BLKSIZE / 8) * 224;
386 			break;
387 		case 2: // fully sample, luma only
388 			div = (BLKSIZE * BLKSIZE) * 219;
389 			break;
390 		case 3: // fully sample, luma and chroma
391 			div = (BLKSIZE * BLKSIZE) * 219 + (BLKSIZE * BLKSIZE / 2) * 224;
392 			break;
393 		}
394 		xblocks = row_sizeY / BLKSIZE;
395 		if (row_sizeY % BLKSIZE) xblocks++;
396 		yblocks = heightY / BLKSIZE;
397 		if (heightY % BLKSIZE) yblocks++;
398 
399 		/* Compare each frame to its predecessor. */
400 		for (f = 1; f <= _param->cycle; f++)
401 		{
402 			highest_sum = computeDiff(store[f],store[f-1]);
403 			metrics[f-1] = (highest_sum * 100.0) / div;
404 		}
405 
406 		Dcurr[0] = 1;
407 		for (f = 1; f < _param->cycle; f++)
408 		{
409 			if (metrics[f] < _param->threshold2) Dcurr[f] = 0;
410 			else Dcurr[f] = 1;
411 		}
412 
413 		if (configuration.debug)
414 		{
415 			OutputDebugString(buf,"Decimate: %d: %3.2f %3.2f %3.2f %3.2f %3.2f\n",
416 					0, metrics[0], metrics[1], metrics[2], metrics[3], metrics[4]);
417 
418 		}
419 	} // / !frame || first time
420 	else
421 	{
422 		GETFRAME(frame + _param->cycle - 1, store[0]);
423 		for (f = 0; f < MAX_CYCLE_SIZE; f++) Dprev[f] = Dcurr[f];
424 		for (f = 0; f < MAX_CYCLE_SIZE; f++) Dcurr[f] = Dnext[f];
425 	}
426 	for (f = 0; f < MAX_CYCLE_SIZE; f++) Dshow[f] = Dcurr[f];
427 	for (f = 0; f < MAX_CYCLE_SIZE; f++) showmetrics[f] = metrics[f];
428 
429 	for (f = 1; f <= _param->cycle; f++)
430 	{
431 		GETFRAME(frame + f + _param->cycle - 1, store[f]);
432 	}
433 
434 	/* Compare each frame to its predecessor. */
435 	for (f = 1; f <= _param->cycle; f++)
436 	{
437         highest_sum=computeDiff(store[f],store[f-1]);
438 		metrics[f-1] = (highest_sum * 100.0) / div;
439 	}
440 
441 	/* Find the frame with the lowest difference count but
442 	   don't use the artificial duplicate at frame 0. */
443 	if (frame == 0)
444 	{
445 		lowest = metrics[1];
446 		lowest_index = 1;
447 	}
448 	else
449 	{
450 		lowest = metrics[0];
451 		lowest_index = 0;
452 	}
453 	for (f = 1; f < _param->cycle; f++)
454 	{
455 		if (metrics[f] < lowest)
456 		{
457 			lowest = metrics[f];
458 			lowest_index = f;
459 		}
460 	}
461 
462 	for (f = 0; f < _param->cycle; f++)
463 	{
464 		if (metrics[f] < _param->threshold2) Dnext[f] = 0;
465 		else Dnext[f] = 1;
466 	}
467 
468 	if (configuration.debug)
469 	{
470 		OutputDebugString("Decimate: %d: %3.2f %3.2f %3.2f %3.2f %3.2f\n",
471 		        frame + 5, metrics[0], metrics[1], metrics[2], metrics[3], metrics[4]);
472 
473 	}
474 
475 	if (configuration.debug)
476 	{
477 		OutputDebugString("Decimate: %d: %d %d %d %d %d\n",
478 		        frame, Dcurr[0], Dcurr[1], Dcurr[2], Dcurr[3], Dcurr[4]);
479 //		sprintf(buf,"Decimate: %d: %d %d %d %d %d - %d %d %d %d %d - %d %d %d %d %d\n",
480 //		        frame, Dprev[0], Dprev[1], Dprev[2], Dprev[3], Dprev[4],
481 //					   Dcurr[0], Dcurr[1], Dcurr[2], Dcurr[3], Dcurr[4],
482 //					   Dnext[0], Dnext[1], Dnext[2], Dnext[3], Dnext[4]);
483 
484 	}
485 
486 	/* Find the longest strings of duplicates and decimate a frame from it. */
487 	highest = -1;
488 	for (f = 0; f < _param->cycle; f++)
489 	{
490 		if (Dcurr[f] == 1)
491 		{
492 			bsum = 0;
493 			fsum = 0;
494 		}
495 		else
496 		{
497 			bsum = 1;
498 			g = f;
499 			while (--g >= 0)
500 			{
501 				if (Dcurr[g] == 0)
502 				{
503 					bsum++;
504 				}
505 				else break;
506 			}
507 			if (g < 0)
508 			{
509 				g = _param->cycle;
510 				while (--g >= 0)
511 				{
512 					if (Dprev[g] == 0)
513 					{
514 						bsum++;
515 					}
516 					else break;
517 				}
518 			}
519 			fsum = 1;
520 			g = f;
521 			while (++g < _param->cycle)
522 			{
523 				if (Dcurr[g] == 0)
524 				{
525 					fsum++;
526 				}
527 				else break;
528 			}
529 			if (g >= _param->cycle)
530 			{
531 				g = -1;
532 				while (++g < _param->cycle)
533 				{
534 					if (Dnext[g] == 0)
535 					{
536 						fsum++;
537 					}
538 					else break;
539 				}
540 			}
541 		}
542 		if (bsum + fsum > highest)
543 		{
544 			highest = bsum + fsum;
545 			highest_index = f;
546 		}
547 //		sprintf(buf,"Decimate: bsum %d, fsum %d\n", bsum, fsum);
548 //		OutputDebugString(buf);
549 	}
550 
551 	f = highest_index;
552 	if (Dcurr[f] == 1)
553 	{
554 		/* No duplicates were found! Act as if mode=0. */
555 		*chosen = last_result = frame + lowest_index;
556 	}
557 	else
558 	{
559 		/* Prevent this decimated frame from being considered again. */
560 		Dcurr[f] = 1;
561 		*chosen = last_result = frame + highest_index;
562 	}
563 	last_forced = false;
564 	if (configuration.debug)
565 	{
566 		OutputDebugString("Decimate: dropping frame %d\n", last_result);
567 
568 	}
569 
570 
571 	found = false;
572 
573 	if (found == true)
574 	{
575 		*chosen = last_result ;
576 		*forced = last_forced = true;
577 		if (configuration.debug)
578 		{
579 			OutputDebugString("Decimate: overridden drop frame -- drop %d\n", last_result);
580 		}
581 	}
582 }
583 /**
584     \fn DrawShow
585 */
DrawShow(ADMImage * src,int useframe,bool forced,int dropframe,double metric,int inframe)586 void Decimate::DrawShow(ADMImage  *src, int useframe, bool forced, int dropframe,
587 						double metric, int inframe)
588 {
589 	char buf[80];
590     deciMate *_param=&configuration;
591 	int start = (useframe / _param->cycle) * _param->cycle;
592 #define pg(i) (hints[i] & PROGRESSIVE) ? "p" : "i"
593 	if (configuration.show == true)
594 	{
595 		sprintf(buf, "Decimate %d", 0); 	DrawString(src, 0, 0, buf);
596 		sprintf(buf, "Copyright 2003 Donald Graft");	    DrawString(src, 0, 1, buf);
597 		sprintf(buf,"%d: [%s] %3.2f", start + 0,pg(0), showmetrics[0]);DrawString(src, 0, 3, buf);
598 		sprintf(buf,"%d: [%s] %3.2f", start + 1,pg(1), showmetrics[1]);DrawString(src, 0, 4, buf);
599 		sprintf(buf,"%d: [%s] %3.2f", start + 2,pg(2), showmetrics[2]);DrawString(src, 0, 5, buf);
600 		sprintf(buf,"%d: [%s] %3.2f", start + 3,pg(3), showmetrics[3]);DrawString(src, 0, 6, buf);
601 		sprintf(buf,"%d: [%s] %3.2f", start + 4,pg(4), showmetrics[4]);DrawString(src, 0, 7, buf);
602 		if (all_video_cycle == false)
603 		{
604 			sprintf(buf,"in frm %d, use frm %d", inframe, useframe);
605 			DrawString(src, 0, 8, buf);
606 			if (forced == false)
607 				sprintf(buf,"chose %d, dropping", dropframe);
608 			else
609 				sprintf(buf,"chose %d, dropping, forced!", dropframe);
610 			DrawString(src, 0, 9, buf);
611 		}
612 		else
613 		{
614 			sprintf(buf,"in frm %d", inframe);			                    DrawString(src, 0, 8, buf);
615 			sprintf(buf,"chose %d, decimating all-video cycle", dropframe);	DrawString(src, 0, 9, buf);
616 		}
617 	}
618 	if (configuration.debug)
619 	{
620 		if (!(inframe%_param->cycle))
621 		{
622 			OutputDebugString(buf,"Decimate: %d: %3.2f\n", start, showmetrics[0]);
623 			OutputDebugString(buf,"Decimate: %d: %3.2f\n", start + 1, showmetrics[1]);
624 			OutputDebugString(buf,"Decimate: %d: %3.2f\n", start + 2, showmetrics[2]);
625 			OutputDebugString(buf,"Decimate: %d: %3.2f\n", start + 3, showmetrics[3]);
626 			OutputDebugString(buf,"Decimate: %d: %3.2f\n", start + 4, showmetrics[4]);
627 
628 		}
629 		if (all_video_cycle == false)
630 		{
631 			OutputDebugString(buf,"Decimate: in frm %d useframe %d\n", inframe, useframe);
632 			if (forced == false)
633             {
634 				OutputDebugString("Decimate: chose %d, dropping\n", dropframe);
635             }
636 			else
637             {
638 				OutputDebugString("Decimate: chose %d, dropping, forced!\n", dropframe);
639             }
640 		}
641 		else
642 		{
643 			OutputDebugString("Decimate: in frm %d\n", inframe);
644 			OutputDebugString("Decimate: chose %d, decimating all-video cycle\n", dropframe);
645 		}
646 	}
647 }
648 // EOF
649 
650