[WIN32SS] Rewrite font selection code. Patch by Katayama Hirofumi MZ. CORE-6621
[reactos.git] / reactos / win32ss / gdi / ntgdi / xformobj.c
1 /*
2 * PROJECT: ReactOS win32 kernel mode subsystem
3 * LICENSE: GPL - See COPYING in the top level directory
4 * FILE: win32ss/gdi/ntgdi/xformobj.c
5 * PURPOSE: XFORMOBJ API
6 * PROGRAMMER: Timo Kreuzer
7 */
8
9 /** Includes ******************************************************************/
10
11 #include <win32k.h>
12 #define NDEBUG
13 #include <debug.h>
14
15 #define DOES_VALUE_OVERFLOW_LONG(x) \
16 (((__int64)((long)(x))) != (x))
17
18 /** Inline helper functions ***************************************************/
19
20 /*
21 * Inline helper to calculate pfo1 * pfo2 + pfo3 * pfo4
22 */
23 FORCEINLINE
24 VOID
25 MulAdd(
26 PFLOATOBJ pfoDest,
27 PFLOATOBJ pfo1,
28 PFLOATOBJ pfo2,
29 PFLOATOBJ pfo3,
30 PFLOATOBJ pfo4)
31 {
32 FLOATOBJ foTmp;
33
34 *pfoDest = *pfo1;
35 FLOATOBJ_Mul(pfoDest, pfo2);
36 foTmp = *pfo3;
37 FLOATOBJ_Mul(&foTmp, pfo4);
38 FLOATOBJ_Add(pfoDest, &foTmp);
39 }
40
41 /*
42 * Inline helper to calculate pfo1 * l2 + pfo3 * l4
43 */
44 FORCEINLINE
45 VOID
46 MulAddLong(
47 PFLOATOBJ pfoDest,
48 PFLOATOBJ pfo1,
49 LONG l2,
50 PFLOATOBJ pfo3,
51 LONG l4)
52 {
53 FLOATOBJ foTmp;
54
55 *pfoDest = *pfo1;
56 FLOATOBJ_MulLong(pfoDest, l2);
57 foTmp = *pfo3;
58 FLOATOBJ_MulLong(&foTmp, l4);
59 FLOATOBJ_Add(pfoDest, &foTmp);
60 }
61
62 /*
63 * Inline helper to calculate pfo1 * pfo2 - pfo3 * pfo4
64 */
65 FORCEINLINE
66 VOID
67 MulSub(
68 PFLOATOBJ pfoDest,
69 PFLOATOBJ pfo1,
70 PFLOATOBJ pfo2,
71 PFLOATOBJ pfo3,
72 PFLOATOBJ pfo4)
73 {
74 FLOATOBJ foTmp;
75
76 *pfoDest = *pfo1;
77 FLOATOBJ_Mul(pfoDest, pfo2);
78 foTmp = *pfo3;
79 FLOATOBJ_Mul(&foTmp, pfo4);
80 FLOATOBJ_Sub(pfoDest, &foTmp);
81 }
82
83 /*
84 * Inline helper to get the complexity hint from flAccel
85 */
86 FORCEINLINE
87 ULONG
88 HintFromAccel(ULONG flAccel)
89 {
90 switch (flAccel & (XFORM_SCALE|XFORM_UNITY|XFORM_NO_TRANSLATION))
91 {
92 case (XFORM_SCALE|XFORM_UNITY|XFORM_NO_TRANSLATION):
93 return GX_IDENTITY;
94 case (XFORM_SCALE|XFORM_UNITY):
95 return GX_OFFSET;
96 case XFORM_SCALE:
97 return GX_SCALE;
98 default:
99 return GX_GENERAL;
100 }
101 }
102
103 /** Internal functions ********************************************************/
104
105 ULONG
106 NTAPI
107 XFORMOBJ_UpdateAccel(
108 IN XFORMOBJ *pxo)
109 {
110 PMATRIX pmx = XFORMOBJ_pmx(pxo);
111
112 /* Copy Dx and Dy to FIX format */
113 pmx->fxDx = FLOATOBJ_GetFix(&pmx->efDx);
114 pmx->fxDy = FLOATOBJ_GetFix(&pmx->efDy);
115
116 pmx->flAccel = 0;
117
118 if (FLOATOBJ_Equal0(&pmx->efDx) &&
119 FLOATOBJ_Equal0(&pmx->efDy))
120 {
121 pmx->flAccel |= XFORM_NO_TRANSLATION;
122 }
123
124 if (FLOATOBJ_Equal0(&pmx->efM12) &&
125 FLOATOBJ_Equal0(&pmx->efM21))
126 {
127 pmx->flAccel |= XFORM_SCALE;
128 }
129
130 if (FLOATOBJ_Equal1(&pmx->efM11) &&
131 FLOATOBJ_Equal1(&pmx->efM22))
132 {
133 pmx->flAccel |= XFORM_UNITY;
134 }
135
136 if (FLOATOBJ_IsLong(&pmx->efM11) && FLOATOBJ_IsLong(&pmx->efM12) &&
137 FLOATOBJ_IsLong(&pmx->efM21) && FLOATOBJ_IsLong(&pmx->efM22))
138 {
139 pmx->flAccel |= XFORM_INTEGER;
140 }
141
142 return HintFromAccel(pmx->flAccel);
143 }
144
145
146 ULONG
147 NTAPI
148 XFORMOBJ_iSetXform(
149 OUT XFORMOBJ *pxo,
150 IN const XFORML *pxform)
151 {
152 PMATRIX pmx = XFORMOBJ_pmx(pxo);
153
154 /* Check parameters */
155 if (!pxo || !pxform) return DDI_ERROR;
156
157 /* Check if the xform is valid */
158 if ((pxform->eM11 == 0) || (pxform->eM22 == 0)) return DDI_ERROR;
159
160 /* Copy members */
161 FLOATOBJ_SetFloat(&pmx->efM11, pxform->eM11);
162 FLOATOBJ_SetFloat(&pmx->efM12, pxform->eM12);
163 FLOATOBJ_SetFloat(&pmx->efM21, pxform->eM21);
164 FLOATOBJ_SetFloat(&pmx->efM22, pxform->eM22);
165 FLOATOBJ_SetFloat(&pmx->efDx, pxform->eDx);
166 FLOATOBJ_SetFloat(&pmx->efDy, pxform->eDy);
167
168 /* Update accelerators and return complexity */
169 return XFORMOBJ_UpdateAccel(pxo);
170 }
171
172
173 /*
174 * Multiplies pxo1 with pxo2 and stores the result in pxo.
175 * returns complexity hint
176 * | efM11 efM12 0 |
177 * | efM21 efM22 0 |
178 * | efDx efDy 1 |
179 */
180 ULONG
181 NTAPI
182 XFORMOBJ_iCombine(
183 IN XFORMOBJ *pxo,
184 IN XFORMOBJ *pxo1,
185 IN XFORMOBJ *pxo2)
186 {
187 MATRIX mx;
188 PMATRIX pmx, pmx1, pmx2;
189
190 /* Get the source matrices */
191 pmx1 = XFORMOBJ_pmx(pxo1);
192 pmx2 = XFORMOBJ_pmx(pxo2);
193
194 /* Do a 3 x 3 matrix multiplication with mx as destinantion */
195 MulAdd(&mx.efM11, &pmx1->efM11, &pmx2->efM11, &pmx1->efM12, &pmx2->efM21);
196 MulAdd(&mx.efM12, &pmx1->efM11, &pmx2->efM12, &pmx1->efM12, &pmx2->efM22);
197 MulAdd(&mx.efM21, &pmx1->efM21, &pmx2->efM11, &pmx1->efM22, &pmx2->efM21);
198 MulAdd(&mx.efM22, &pmx1->efM21, &pmx2->efM12, &pmx1->efM22, &pmx2->efM22);
199 MulAdd(&mx.efDx, &pmx1->efDx, &pmx2->efM11, &pmx1->efDy, &pmx2->efM21);
200 FLOATOBJ_Add(&mx.efDx, &pmx2->efDx);
201 MulAdd(&mx.efDy, &pmx1->efDx, &pmx2->efM12, &pmx1->efDy, &pmx2->efM22);
202 FLOATOBJ_Add(&mx.efDy, &pmx2->efDy);
203
204 /* Copy back */
205 pmx = XFORMOBJ_pmx(pxo);
206 *pmx = mx;
207
208 /* Update accelerators and return complexity */
209 return XFORMOBJ_UpdateAccel(pxo);
210 }
211
212
213 ULONG
214 NTAPI
215 XFORMOBJ_iCombineXform(
216 IN XFORMOBJ *pxo,
217 IN XFORMOBJ *pxo1,
218 IN XFORML *pxform,
219 IN BOOL bLeftMultiply)
220 {
221 MATRIX mx;
222 XFORMOBJ xo2;
223
224 XFORMOBJ_vInit(&xo2, &mx);
225 XFORMOBJ_iSetXform(&xo2, pxform);
226
227 if (bLeftMultiply)
228 {
229 return XFORMOBJ_iCombine(pxo, &xo2, pxo1);
230 }
231 else
232 {
233 return XFORMOBJ_iCombine(pxo, pxo1, &xo2);
234 }
235 }
236
237 /*
238 * A^-1 = adj(A) / det(AT)
239 * A^-1 = 1/(a*d - b*c) * (a22,-a12,a21,-a11)
240 */
241 ULONG
242 NTAPI
243 XFORMOBJ_iInverse(
244 OUT XFORMOBJ *pxoDst,
245 IN XFORMOBJ *pxoSrc)
246 {
247 PMATRIX pmxDst, pmxSrc;
248 FLOATOBJ foDet;
249 XFORM xformSrc;
250
251 pmxDst = XFORMOBJ_pmx(pxoDst);
252 pmxSrc = XFORMOBJ_pmx(pxoSrc);
253
254 XFORMOBJ_iGetXform(pxoSrc, (XFORML*)&xformSrc);
255
256 /* det = M11 * M22 - M12 * M21 */
257 MulSub(&foDet, &pmxSrc->efM11, &pmxSrc->efM22, &pmxSrc->efM12, &pmxSrc->efM21);
258
259 if (FLOATOBJ_Equal0(&foDet))
260 {
261 /* Determinant is 0! */
262 return DDI_ERROR;
263 }
264
265 /* Calculate adj(A) / det(A) */
266 pmxDst->efM11 = pmxSrc->efM22;
267 FLOATOBJ_Div(&pmxDst->efM11, &foDet);
268 pmxDst->efM22 = pmxSrc->efM11;
269 FLOATOBJ_Div(&pmxDst->efM22, &foDet);
270
271 /* The other 2 are negative, negate foDet for that */
272 FLOATOBJ_Neg(&foDet);
273 pmxDst->efM12 = pmxSrc->efM12;
274 FLOATOBJ_Div(&pmxDst->efM12, &foDet);
275 pmxDst->efM21 = pmxSrc->efM21;
276 FLOATOBJ_Div(&pmxDst->efM21, &foDet);
277
278 /* Calculate the inverted x shift: Dx' = -Dx * M11' - Dy * M21' */
279 pmxDst->efDx = pmxSrc->efDx;
280 FLOATOBJ_Neg(&pmxDst->efDx);
281 MulSub(&pmxDst->efDx, &pmxDst->efDx, &pmxDst->efM11, &pmxSrc->efDy, &pmxDst->efM21);
282
283 /* Calculate the inverted y shift: Dy' = -Dy * M22' - Dx * M12' */
284 pmxDst->efDy = pmxSrc->efDy;
285 FLOATOBJ_Neg(&pmxDst->efDy);
286 MulSub(&pmxDst->efDy, &pmxDst->efDy, &pmxDst->efM22, &pmxSrc->efDx, &pmxDst->efM12);
287
288 /* Update accelerators and return complexity */
289 return XFORMOBJ_UpdateAccel(pxoDst);
290 }
291
292
293 /*!
294 * \brief Transforms fix-point coordinates in an array of POINTL structures using
295 * the transformation matrix from the XFORMOBJ.
296 *
297 * \param pxo - Pointer to the XFORMOBJ
298 *
299 * \param cPoints - Number of coordinates to transform
300 *
301 * \param pptIn - Pointer to an array of POINTL structures containing the
302 * source coordinates.
303 *
304 * \param pptOut - Pointer to an array of POINTL structures, receiving the
305 * transformed coordinates. Can be the same as pptIn.
306 *
307 * \return TRUE if the operation was successful, FALSE if any of the calculations
308 * caused an integer overflow.
309 *
310 * \note If the function returns FALSE, it might still have written to the
311 * output buffer. If pptIn and pptOut are equal, the source coordinates
312 * might have been partly overwritten!
313 */
314 static
315 BOOL
316 NTAPI
317 XFORMOBJ_bXformFixPoints(
318 _In_ XFORMOBJ *pxo,
319 _In_ ULONG cPoints,
320 _In_reads_(cPoints) PPOINTL pptIn,
321 _Out_writes_(cPoints) PPOINTL pptOut)
322 {
323 PMATRIX pmx;
324 INT i;
325 FLOATOBJ fo1, fo2;
326 FLONG flAccel;
327 LONG lM11, lM12, lM21, lM22, lTemp;
328 register LONGLONG llx, lly;
329
330 pmx = XFORMOBJ_pmx(pxo);
331 flAccel = pmx->flAccel;
332
333 if ((flAccel & (XFORM_SCALE|XFORM_UNITY)) == (XFORM_SCALE|XFORM_UNITY))
334 {
335 /* Identity transformation */
336 RtlCopyMemory(pptOut, pptIn, cPoints * sizeof(POINTL));
337 }
338 else if (flAccel & XFORM_INTEGER)
339 {
340 if (flAccel & XFORM_UNITY)
341 {
342 /* 1-scale integer transform, get the off-diagonal elements */
343 if (!FLOATOBJ_bConvertToLong(&pmx->efM12, &lM12) ||
344 !FLOATOBJ_bConvertToLong(&pmx->efM21, &lM21))
345 {
346 NT_ASSERT(FALSE);
347 return FALSE;
348 }
349
350 i = cPoints - 1;
351 do
352 {
353 /* Calculate x in 64 bit and check for overflow */
354 llx = Int32x32To64(pptIn[i].y, lM21) + pptIn[i].x;
355 if (DOES_VALUE_OVERFLOW_LONG(llx))
356 {
357 return FALSE;
358 }
359
360 /* Calculate y in 64 bit and check for overflow */
361 lly = Int32x32To64(pptIn[i].x, lM12) + pptIn[i].y;
362 if (DOES_VALUE_OVERFLOW_LONG(lly))
363 {
364 return FALSE;
365 }
366
367 /* Write back the results */
368 pptOut[i].x = (LONG)llx;
369 pptOut[i].y = (LONG)lly;
370 }
371 while (--i >= 0);
372 }
373 else if (flAccel & XFORM_SCALE)
374 {
375 /* Diagonal integer transform, get the diagonal elements */
376 if (!FLOATOBJ_bConvertToLong(&pmx->efM11, &lM11) ||
377 !FLOATOBJ_bConvertToLong(&pmx->efM22, &lM22))
378 {
379 NT_ASSERT(FALSE);
380 return FALSE;
381 }
382
383 i = cPoints - 1;
384 do
385 {
386 /* Calculate x in 64 bit and check for overflow */
387 llx = Int32x32To64(pptIn[i].x, lM11);
388 if (DOES_VALUE_OVERFLOW_LONG(llx))
389 {
390 return FALSE;
391 }
392
393 /* Calculate y in 64 bit and check for overflow */
394 lly = Int32x32To64(pptIn[i].y, lM22);
395 if (DOES_VALUE_OVERFLOW_LONG(lly))
396 {
397 return FALSE;
398 }
399
400 /* Write back the results */
401 pptOut[i].x = (LONG)llx;
402 pptOut[i].y = (LONG)lly;
403 }
404 while (--i >= 0);
405 }
406 else
407 {
408 /* Full integer transform */
409 if (!FLOATOBJ_bConvertToLong(&pmx->efM11, &lM11) ||
410 !FLOATOBJ_bConvertToLong(&pmx->efM12, &lM12) ||
411 !FLOATOBJ_bConvertToLong(&pmx->efM21, &lM21) ||
412 !FLOATOBJ_bConvertToLong(&pmx->efM22, &lM22))
413 {
414 NT_ASSERT(FALSE);
415 return FALSE;
416 }
417
418 i = cPoints - 1;
419 do
420 {
421 /* Calculate x in 64 bit and check for overflow */
422 llx = Int32x32To64(pptIn[i].x, lM11);
423 llx += Int32x32To64(pptIn[i].y, lM21);
424 if (DOES_VALUE_OVERFLOW_LONG(llx))
425 {
426 return FALSE;
427 }
428
429 /* Calculate y in 64 bit and check for overflow */
430 lly = Int32x32To64(pptIn[i].y, lM22);
431 lly += Int32x32To64(pptIn[i].x, lM12);
432 if (DOES_VALUE_OVERFLOW_LONG(lly))
433 {
434 return FALSE;
435 }
436
437 /* Write back the results */
438 pptOut[i].x = (LONG)llx;
439 pptOut[i].y = (LONG)lly;
440 }
441 while (--i >= 0);
442 }
443 }
444 else if (flAccel & XFORM_UNITY)
445 {
446 /* 1-scale transform */
447 i = cPoints - 1;
448 do
449 {
450 /* Calculate x in 64 bit and check for overflow */
451 fo1 = pmx->efM21;
452 FLOATOBJ_MulLong(&fo1, pptIn[i].y);
453 if (!FLOATOBJ_bConvertToLong(&fo1, &lTemp))
454 {
455 return FALSE;
456 }
457 llx = (LONGLONG)pptIn[i].x + lTemp;
458 if (DOES_VALUE_OVERFLOW_LONG(llx))
459 {
460 return FALSE;
461 }
462
463 /* Calculate y in 64 bit and check for overflow */
464 fo2 = pmx->efM12;
465 FLOATOBJ_MulLong(&fo2, pptIn[i].x);
466 if (!FLOATOBJ_bConvertToLong(&fo2, &lTemp))
467 {
468 return FALSE;
469 }
470 lly = (LONGLONG)pptIn[i].y + lTemp;
471 if (DOES_VALUE_OVERFLOW_LONG(lly))
472 {
473 return FALSE;
474 }
475
476 /* Write back the results */
477 pptOut[i].x = (LONG)llx;
478 pptOut[i].y = (LONG)lly;
479 }
480 while (--i >= 0);
481 }
482 else if (flAccel & XFORM_SCALE)
483 {
484 /* Diagonal float transform */
485 i = cPoints - 1;
486 do
487 {
488 fo1 = pmx->efM11;
489 FLOATOBJ_MulLong(&fo1, pptIn[i].x);
490 if (!FLOATOBJ_bConvertToLong(&fo1, &pptOut[i].x))
491 {
492 return FALSE;
493 }
494
495 fo2 = pmx->efM22;
496 FLOATOBJ_MulLong(&fo2, pptIn[i].y);
497 if (!FLOATOBJ_bConvertToLong(&fo2, &pptOut[i].y))
498 {
499 return FALSE;
500 }
501 }
502 while (--i >= 0);
503 }
504 else
505 {
506 /* Full float transform */
507 i = cPoints - 1;
508 do
509 {
510 /* Calculate x as FLOATOBJ */
511 MulAddLong(&fo1, &pmx->efM11, pptIn[i].x, &pmx->efM21, pptIn[i].y);
512
513 /* Calculate y as FLOATOBJ */
514 MulAddLong(&fo2, &pmx->efM12, pptIn[i].x, &pmx->efM22, pptIn[i].y);
515
516 if (!FLOATOBJ_bConvertToLong(&fo1, &pptOut[i].x))
517 {
518 return FALSE;
519 }
520
521 if (!FLOATOBJ_bConvertToLong(&fo2, &pptOut[i].y))
522 {
523 return FALSE;
524 }
525 }
526 while (--i >= 0);
527 }
528
529 if (!(pmx->flAccel & XFORM_NO_TRANSLATION))
530 {
531 /* Translate points */
532 i = cPoints - 1;
533 do
534 {
535 llx = (LONGLONG)pptOut[i].x + pmx->fxDx;
536 if (DOES_VALUE_OVERFLOW_LONG(llx))
537 {
538 return FALSE;
539 }
540 pptOut[i].x = (LONG)llx;
541
542 lly = (LONGLONG)pptOut[i].y + pmx->fxDy;
543 if (DOES_VALUE_OVERFLOW_LONG(lly))
544 {
545 return FALSE;
546 }
547 pptOut[i].y = (LONG)lly;
548 }
549 while (--i >= 0);
550 }
551
552 return TRUE;
553 }
554
555 /** Public functions **********************************************************/
556
557 // www.osr.com/ddk/graphics/gdifncs_0s2v.htm
558 ULONG
559 APIENTRY
560 XFORMOBJ_iGetXform(
561 IN XFORMOBJ *pxo,
562 OUT XFORML *pxform)
563 {
564 PMATRIX pmx = XFORMOBJ_pmx(pxo);
565
566 /* Check parameters */
567 if (!pxo || !pxform)
568 {
569 return DDI_ERROR;
570 }
571
572 /* Copy members */
573 pxform->eM11 = FLOATOBJ_GetFloat(&pmx->efM11);
574 pxform->eM12 = FLOATOBJ_GetFloat(&pmx->efM12);
575 pxform->eM21 = FLOATOBJ_GetFloat(&pmx->efM21);
576 pxform->eM22 = FLOATOBJ_GetFloat(&pmx->efM22);
577 pxform->eDx = FLOATOBJ_GetFloat(&pmx->efDx);
578 pxform->eDy = FLOATOBJ_GetFloat(&pmx->efDy);
579
580 /* Return complexity hint */
581 return HintFromAccel(pmx->flAccel);
582 }
583
584
585 // www.osr.com/ddk/graphics/gdifncs_5ig7.htm
586 ULONG
587 APIENTRY
588 XFORMOBJ_iGetFloatObjXform(
589 IN XFORMOBJ *pxo,
590 OUT FLOATOBJ_XFORM *pxfo)
591 {
592 PMATRIX pmx = XFORMOBJ_pmx(pxo);
593
594 /* Check parameters */
595 if (!pxo || !pxfo)
596 {
597 return DDI_ERROR;
598 }
599
600 /* Copy members */
601 pxfo->eM11 = pmx->efM11;
602 pxfo->eM12 = pmx->efM12;
603 pxfo->eM21 = pmx->efM21;
604 pxfo->eM22 = pmx->efM22;
605 pxfo->eDx = pmx->efDx;
606 pxfo->eDy = pmx->efDy;
607
608 /* Return complexity hint */
609 return HintFromAccel(pmx->flAccel);
610 }
611
612
613 // www.osr.com/ddk/graphics/gdifncs_027b.htm
614 BOOL
615 APIENTRY
616 XFORMOBJ_bApplyXform(
617 IN XFORMOBJ *pxo,
618 IN ULONG iMode,
619 IN ULONG cPoints,
620 IN PVOID pvIn,
621 OUT PVOID pvOut)
622 {
623 MATRIX mx;
624 XFORMOBJ xoInv;
625 PPOINTL pptlIn, pptlOut;
626 INT i;
627
628 /* Check parameters */
629 if (!pxo || !pvIn || !pvOut || cPoints < 1)
630 {
631 return FALSE;
632 }
633
634 /* Use inverse xform? */
635 if (iMode == XF_INV_FXTOL || iMode == XF_INV_LTOL)
636 {
637 XFORMOBJ_vInit(&xoInv, &mx);
638 if (XFORMOBJ_iInverse(&xoInv, pxo) == DDI_ERROR)
639 {
640 return FALSE;
641 }
642 pxo = &xoInv;
643 }
644
645 /* Convert POINTL to POINTFIX? */
646 if (iMode == XF_LTOFX || iMode == XF_LTOL || iMode == XF_INV_LTOL)
647 {
648 pptlIn = pvIn;
649 pptlOut = pvOut;
650 for (i = cPoints - 1; i >= 0; i--)
651 {
652 pptlOut[i].x = LONG2FIX(pptlIn[i].x);
653 pptlOut[i].y = LONG2FIX(pptlIn[i].y);
654 }
655
656 /* The input is in the out buffer now! */
657 pvIn = pvOut;
658 }
659
660 /* Do the actual fixpoint transformation */
661 if (!XFORMOBJ_bXformFixPoints(pxo, cPoints, pvIn, pvOut))
662 {
663 return FALSE;
664 }
665
666 /* Convert POINTFIX to POINTL? */
667 if (iMode == XF_INV_FXTOL || iMode == XF_INV_LTOL || iMode == XF_LTOL)
668 {
669 pptlOut = pvOut;
670 for (i = cPoints - 1; i >= 0; i--)
671 {
672 pptlOut[i].x = FIX2LONG(pptlOut[i].x);
673 pptlOut[i].y = FIX2LONG(pptlOut[i].y);
674 }
675 }
676
677 return TRUE;
678 }
679
680 /* EOF */