mirror of
https://github.com/reactos/reactos.git
synced 2024-08-28 14:18:41 +00:00
623 lines
16 KiB
C
623 lines
16 KiB
C
/*
|
|
* PROJECT: ReactOS Win32k Subsystem
|
|
* LICENSE: GPL - See COPYING in the top level directory
|
|
* FILE: win32k/objects/line.c
|
|
* PURPOSE: Line functions
|
|
* PROGRAMMERS: ...
|
|
*/
|
|
|
|
#include <win32k.h>
|
|
|
|
#define NDEBUG
|
|
#include <debug.h>
|
|
|
|
// Some code from the WINE project source (www.winehq.com)
|
|
|
|
// Should use Fx in Point
|
|
//
|
|
BOOL FASTCALL
|
|
IntGdiMoveToEx(DC *dc,
|
|
int X,
|
|
int Y,
|
|
LPPOINT Point,
|
|
BOOL BypassPath)
|
|
{
|
|
BOOL PathIsOpen;
|
|
PDC_ATTR pdcattr = dc->pdcattr;
|
|
if ( Point )
|
|
{
|
|
if ( pdcattr->ulDirty_ & DIRTY_PTLCURRENT ) // Double hit!
|
|
{
|
|
Point->x = pdcattr->ptfxCurrent.x; // ret prev before change.
|
|
Point->y = pdcattr->ptfxCurrent.y;
|
|
IntDPtoLP ( dc, Point, 1); // Reconvert back.
|
|
}
|
|
else
|
|
{
|
|
Point->x = pdcattr->ptlCurrent.x;
|
|
Point->y = pdcattr->ptlCurrent.y;
|
|
}
|
|
}
|
|
pdcattr->ptlCurrent.x = X;
|
|
pdcattr->ptlCurrent.y = Y;
|
|
pdcattr->ptfxCurrent = pdcattr->ptlCurrent;
|
|
CoordLPtoDP(dc, &pdcattr->ptfxCurrent); // Update fx
|
|
pdcattr->ulDirty_ &= ~(DIRTY_PTLCURRENT|DIRTY_PTFXCURRENT|DIRTY_STYLESTATE);
|
|
|
|
if (BypassPath) return TRUE;
|
|
|
|
PathIsOpen = PATH_IsPathOpen(dc->dclevel);
|
|
|
|
if ( PathIsOpen )
|
|
return PATH_MoveTo ( dc );
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL FASTCALL
|
|
GreMoveTo( HDC hdc,
|
|
INT x,
|
|
INT y,
|
|
LPPOINT pptOut)
|
|
{
|
|
PDC dc;
|
|
if (!(dc = DC_LockDc(hdc)))
|
|
{
|
|
EngSetLastError(ERROR_INVALID_HANDLE);
|
|
return FALSE;
|
|
}
|
|
return IntGdiMoveToEx(dc, x, y, pptOut, TRUE);
|
|
}
|
|
|
|
// Should use Fx in pt
|
|
//
|
|
VOID FASTCALL
|
|
IntGetCurrentPositionEx(PDC dc, LPPOINT pt)
|
|
{
|
|
PDC_ATTR pdcattr = dc->pdcattr;
|
|
|
|
if ( pt )
|
|
{
|
|
if (pdcattr->ulDirty_ & DIRTY_PTFXCURRENT)
|
|
{
|
|
pdcattr->ptfxCurrent = pdcattr->ptlCurrent;
|
|
CoordLPtoDP(dc, &pdcattr->ptfxCurrent); // Update fx
|
|
pdcattr->ulDirty_ &= ~(DIRTY_PTFXCURRENT|DIRTY_STYLESTATE);
|
|
}
|
|
pt->x = pdcattr->ptlCurrent.x;
|
|
pt->y = pdcattr->ptlCurrent.y;
|
|
}
|
|
}
|
|
|
|
BOOL FASTCALL
|
|
IntGdiLineTo(DC *dc,
|
|
int XEnd,
|
|
int YEnd)
|
|
{
|
|
SURFACE *psurf;
|
|
BOOL Ret = TRUE;
|
|
PBRUSH pbrLine;
|
|
RECTL Bounds;
|
|
POINT Points[2];
|
|
PDC_ATTR pdcattr = dc->pdcattr;
|
|
|
|
if (PATH_IsPathOpen(dc->dclevel))
|
|
{
|
|
Ret = PATH_LineTo(dc, XEnd, YEnd);
|
|
if (Ret)
|
|
{
|
|
// FIXME: PATH_LineTo should maybe do this? No
|
|
pdcattr->ptlCurrent.x = XEnd;
|
|
pdcattr->ptlCurrent.y = YEnd;
|
|
pdcattr->ptfxCurrent = pdcattr->ptlCurrent;
|
|
CoordLPtoDP(dc, &pdcattr->ptfxCurrent); // Update fx
|
|
pdcattr->ulDirty_ &= ~(DIRTY_PTLCURRENT|DIRTY_PTFXCURRENT|DIRTY_STYLESTATE);
|
|
}
|
|
return Ret;
|
|
}
|
|
else
|
|
{
|
|
psurf = dc->dclevel.pSurface;
|
|
if (NULL == psurf)
|
|
{
|
|
EngSetLastError(ERROR_INVALID_HANDLE);
|
|
return FALSE;
|
|
}
|
|
|
|
Points[0].x = pdcattr->ptlCurrent.x;
|
|
Points[0].y = pdcattr->ptlCurrent.y;
|
|
Points[1].x = XEnd;
|
|
Points[1].y = YEnd;
|
|
|
|
IntLPtoDP(dc, Points, 2);
|
|
|
|
/* The DCOrg is in device coordinates */
|
|
Points[0].x += dc->ptlDCOrig.x;
|
|
Points[0].y += dc->ptlDCOrig.y;
|
|
Points[1].x += dc->ptlDCOrig.x;
|
|
Points[1].y += dc->ptlDCOrig.y;
|
|
|
|
Bounds.left = min(Points[0].x, Points[1].x);
|
|
Bounds.top = min(Points[0].y, Points[1].y);
|
|
Bounds.right = max(Points[0].x, Points[1].x);
|
|
Bounds.bottom = max(Points[0].y, Points[1].y);
|
|
|
|
/* Get BRUSH from current pen. */
|
|
pbrLine = dc->dclevel.pbrLine;
|
|
ASSERT(pbrLine);
|
|
|
|
if (!(pbrLine->flAttrs & BR_IS_NULL))
|
|
{
|
|
Ret = IntEngLineTo(&psurf->SurfObj,
|
|
&dc->co.ClipObj,
|
|
&dc->eboLine.BrushObject,
|
|
Points[0].x, Points[0].y,
|
|
Points[1].x, Points[1].y,
|
|
&Bounds,
|
|
ROP2_TO_MIX(pdcattr->jROP2));
|
|
}
|
|
|
|
}
|
|
|
|
if (Ret)
|
|
{
|
|
pdcattr->ptlCurrent.x = XEnd;
|
|
pdcattr->ptlCurrent.y = YEnd;
|
|
pdcattr->ptfxCurrent = pdcattr->ptlCurrent;
|
|
CoordLPtoDP(dc, &pdcattr->ptfxCurrent); // Update fx
|
|
pdcattr->ulDirty_ &= ~(DIRTY_PTLCURRENT|DIRTY_PTFXCURRENT|DIRTY_STYLESTATE);
|
|
}
|
|
|
|
return Ret;
|
|
}
|
|
|
|
BOOL FASTCALL
|
|
IntGdiPolyBezier(DC *dc,
|
|
LPPOINT pt,
|
|
DWORD Count)
|
|
{
|
|
BOOL ret = FALSE; // Default to FAILURE
|
|
|
|
if ( PATH_IsPathOpen(dc->dclevel) )
|
|
{
|
|
return PATH_PolyBezier ( dc, pt, Count );
|
|
}
|
|
|
|
/* We'll convert it into line segments and draw them using Polyline */
|
|
{
|
|
POINT *Pts;
|
|
INT nOut;
|
|
|
|
Pts = GDI_Bezier ( pt, Count, &nOut );
|
|
if ( Pts )
|
|
{
|
|
ret = IntGdiPolyline(dc, Pts, nOut);
|
|
ExFreePoolWithTag(Pts, TAG_BEZIER);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
BOOL FASTCALL
|
|
IntGdiPolyBezierTo(DC *dc,
|
|
LPPOINT pt,
|
|
DWORD Count)
|
|
{
|
|
BOOL ret = FALSE; // Default to failure
|
|
PDC_ATTR pdcattr = dc->pdcattr;
|
|
|
|
if ( PATH_IsPathOpen(dc->dclevel) )
|
|
ret = PATH_PolyBezierTo ( dc, pt, Count );
|
|
else /* We'll do it using PolyBezier */
|
|
{
|
|
POINT *npt;
|
|
npt = ExAllocatePoolWithTag(PagedPool,
|
|
sizeof(POINT) * (Count + 1),
|
|
TAG_BEZIER);
|
|
if ( npt )
|
|
{
|
|
npt[0].x = pdcattr->ptlCurrent.x;
|
|
npt[0].y = pdcattr->ptlCurrent.y;
|
|
memcpy(npt + 1, pt, sizeof(POINT) * Count);
|
|
ret = IntGdiPolyBezier(dc, npt, Count+1);
|
|
ExFreePoolWithTag(npt, TAG_BEZIER);
|
|
}
|
|
}
|
|
if ( ret )
|
|
{
|
|
pdcattr->ptlCurrent.x = pt[Count-1].x;
|
|
pdcattr->ptlCurrent.y = pt[Count-1].y;
|
|
pdcattr->ptfxCurrent = pdcattr->ptlCurrent;
|
|
CoordLPtoDP(dc, &pdcattr->ptfxCurrent); // Update fx
|
|
pdcattr->ulDirty_ &= ~(DIRTY_PTLCURRENT|DIRTY_PTFXCURRENT|DIRTY_STYLESTATE);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
BOOL FASTCALL
|
|
IntGdiPolyline(DC *dc,
|
|
LPPOINT pt,
|
|
int Count)
|
|
{
|
|
SURFACE *psurf;
|
|
BRUSH *pbrLine;
|
|
LPPOINT Points;
|
|
BOOL Ret = TRUE;
|
|
LONG i;
|
|
PDC_ATTR pdcattr = dc->pdcattr;
|
|
|
|
psurf = dc->dclevel.pSurface;
|
|
if (!psurf)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (PATH_IsPathOpen(dc->dclevel))
|
|
return PATH_Polyline(dc, pt, Count);
|
|
|
|
DC_vPrepareDCsForBlit(dc, NULL, NULL, NULL);
|
|
|
|
if (pdcattr->ulDirty_ & (DIRTY_FILL | DC_BRUSH_DIRTY))
|
|
DC_vUpdateFillBrush(dc);
|
|
|
|
if (pdcattr->ulDirty_ & (DIRTY_LINE | DC_PEN_DIRTY))
|
|
DC_vUpdateLineBrush(dc);
|
|
|
|
/* Get BRUSHOBJ from current pen. */
|
|
pbrLine = dc->dclevel.pbrLine;
|
|
ASSERT(pbrLine);
|
|
|
|
if (!(pbrLine->flAttrs & BR_IS_NULL))
|
|
{
|
|
Points = EngAllocMem(0, Count * sizeof(POINT), GDITAG_TEMP);
|
|
if (Points != NULL)
|
|
{
|
|
RtlCopyMemory(Points, pt, Count * sizeof(POINT));
|
|
IntLPtoDP(dc, Points, Count);
|
|
|
|
/* Offset the array of points by the DC origin */
|
|
for (i = 0; i < Count; i++)
|
|
{
|
|
Points[i].x += dc->ptlDCOrig.x;
|
|
Points[i].y += dc->ptlDCOrig.y;
|
|
}
|
|
|
|
Ret = IntEngPolyline(&psurf->SurfObj,
|
|
&dc->co.ClipObj,
|
|
&dc->eboLine.BrushObject,
|
|
Points,
|
|
Count,
|
|
ROP2_TO_MIX(pdcattr->jROP2));
|
|
|
|
EngFreeMem(Points);
|
|
}
|
|
else
|
|
{
|
|
Ret = FALSE;
|
|
}
|
|
}
|
|
|
|
DC_vFinishBlit(dc, NULL);
|
|
|
|
return Ret;
|
|
}
|
|
|
|
BOOL FASTCALL
|
|
IntGdiPolylineTo(DC *dc,
|
|
LPPOINT pt,
|
|
DWORD Count)
|
|
{
|
|
BOOL ret = FALSE; // Default to failure
|
|
PDC_ATTR pdcattr = dc->pdcattr;
|
|
|
|
if (PATH_IsPathOpen(dc->dclevel))
|
|
{
|
|
ret = PATH_PolylineTo(dc, pt, Count);
|
|
}
|
|
else /* Do it using Polyline */
|
|
{
|
|
POINT *pts = ExAllocatePoolWithTag(PagedPool,
|
|
sizeof(POINT) * (Count + 1),
|
|
TAG_SHAPE);
|
|
if ( pts )
|
|
{
|
|
pts[0].x = pdcattr->ptlCurrent.x;
|
|
pts[0].y = pdcattr->ptlCurrent.y;
|
|
memcpy( pts + 1, pt, sizeof(POINT) * Count);
|
|
ret = IntGdiPolyline(dc, pts, Count + 1);
|
|
ExFreePoolWithTag(pts, TAG_SHAPE);
|
|
}
|
|
}
|
|
if ( ret )
|
|
{
|
|
pdcattr->ptlCurrent.x = pt[Count-1].x;
|
|
pdcattr->ptlCurrent.y = pt[Count-1].y;
|
|
pdcattr->ptfxCurrent = pdcattr->ptlCurrent;
|
|
CoordLPtoDP(dc, &pdcattr->ptfxCurrent); // Update fx
|
|
pdcattr->ulDirty_ &= ~(DIRTY_PTLCURRENT|DIRTY_PTFXCURRENT|DIRTY_STYLESTATE);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
BOOL FASTCALL
|
|
IntGdiPolyPolyline(DC *dc,
|
|
LPPOINT pt,
|
|
PULONG PolyPoints,
|
|
DWORD Count)
|
|
{
|
|
ULONG i;
|
|
LPPOINT pts;
|
|
PULONG pc;
|
|
BOOL ret = FALSE; // Default to failure
|
|
pts = pt;
|
|
pc = PolyPoints;
|
|
|
|
if (PATH_IsPathOpen(dc->dclevel))
|
|
return PATH_PolyPolyline( dc, pt, PolyPoints, Count );
|
|
|
|
for (i = 0; i < Count; i++)
|
|
{
|
|
ret = IntGdiPolyline ( dc, pts, *pc );
|
|
if (ret == FALSE)
|
|
{
|
|
return ret;
|
|
}
|
|
pts+=*pc++;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
BOOL
|
|
APIENTRY
|
|
NtGdiLineTo(HDC hDC,
|
|
int XEnd,
|
|
int YEnd)
|
|
{
|
|
DC *dc;
|
|
BOOL Ret;
|
|
RECT rcLockRect ;
|
|
|
|
dc = DC_LockDc(hDC);
|
|
if (!dc)
|
|
{
|
|
EngSetLastError(ERROR_INVALID_HANDLE);
|
|
return FALSE;
|
|
}
|
|
if (dc->dctype == DC_TYPE_INFO)
|
|
{
|
|
DC_UnlockDc(dc);
|
|
/* Yes, Windows really returns TRUE in this case */
|
|
return TRUE;
|
|
}
|
|
|
|
rcLockRect.left = dc->pdcattr->ptlCurrent.x;
|
|
rcLockRect.top = dc->pdcattr->ptlCurrent.y;
|
|
rcLockRect.right = XEnd;
|
|
rcLockRect.bottom = YEnd;
|
|
|
|
IntLPtoDP(dc, &rcLockRect, 2);
|
|
|
|
/* The DCOrg is in device coordinates */
|
|
rcLockRect.left += dc->ptlDCOrig.x;
|
|
rcLockRect.top += dc->ptlDCOrig.y;
|
|
rcLockRect.right += dc->ptlDCOrig.x;
|
|
rcLockRect.bottom += dc->ptlDCOrig.y;
|
|
|
|
DC_vPrepareDCsForBlit(dc, &rcLockRect, NULL, NULL);
|
|
|
|
if (dc->pdcattr->ulDirty_ & (DIRTY_LINE | DC_PEN_DIRTY))
|
|
DC_vUpdateLineBrush(dc);
|
|
|
|
Ret = IntGdiLineTo(dc, XEnd, YEnd);
|
|
|
|
DC_vFinishBlit(dc, NULL);
|
|
|
|
DC_UnlockDc(dc);
|
|
return Ret;
|
|
}
|
|
|
|
// FIXME: This function is completely broken
|
|
BOOL
|
|
APIENTRY
|
|
NtGdiPolyDraw(
|
|
IN HDC hdc,
|
|
IN LPPOINT lppt,
|
|
IN LPBYTE lpbTypes,
|
|
IN ULONG cCount)
|
|
{
|
|
PDC dc;
|
|
PDC_ATTR pdcattr;
|
|
POINT bzr[4];
|
|
volatile PPOINT line_pts, line_pts_old, bzr_pts;
|
|
INT num_pts, num_bzr_pts, space, space_old, size;
|
|
ULONG i;
|
|
BOOL result = FALSE;
|
|
|
|
dc = DC_LockDc(hdc);
|
|
if (!dc) return FALSE;
|
|
pdcattr = dc->pdcattr;
|
|
|
|
if (pdcattr->ulDirty_ & (DIRTY_FILL | DC_BRUSH_DIRTY))
|
|
DC_vUpdateFillBrush(dc);
|
|
|
|
if (pdcattr->ulDirty_ & (DIRTY_LINE | DC_PEN_DIRTY))
|
|
DC_vUpdateLineBrush(dc);
|
|
|
|
if (!cCount)
|
|
{
|
|
DC_UnlockDc(dc);
|
|
return TRUE;
|
|
}
|
|
|
|
line_pts = NULL;
|
|
line_pts_old = NULL;
|
|
bzr_pts = NULL;
|
|
|
|
_SEH2_TRY
|
|
{
|
|
ProbeArrayForRead(lppt, sizeof(POINT), cCount, sizeof(LONG));
|
|
ProbeArrayForRead(lpbTypes, sizeof(BYTE), cCount, sizeof(BYTE));
|
|
|
|
if (PATH_IsPathOpen(dc->dclevel))
|
|
{
|
|
result = PATH_PolyDraw(dc, (const POINT *)lppt, (const BYTE *)lpbTypes, cCount);
|
|
_SEH2_LEAVE;
|
|
}
|
|
|
|
/* Check for valid point types */
|
|
for (i = 0; i < cCount; i++)
|
|
{
|
|
switch (lpbTypes[i])
|
|
{
|
|
case PT_MOVETO:
|
|
case PT_LINETO | PT_CLOSEFIGURE:
|
|
case PT_LINETO:
|
|
break;
|
|
case PT_BEZIERTO:
|
|
if((i + 2 < cCount) && (lpbTypes[i + 1] == PT_BEZIERTO) &&
|
|
((lpbTypes[i + 2] & ~PT_CLOSEFIGURE) == PT_BEZIERTO))
|
|
{
|
|
i += 2;
|
|
break;
|
|
}
|
|
default:
|
|
_SEH2_LEAVE;
|
|
}
|
|
}
|
|
|
|
space = cCount + 300;
|
|
line_pts = ExAllocatePoolWithTag(PagedPool, space * sizeof(POINT), TAG_SHAPE);
|
|
if (line_pts == NULL)
|
|
{
|
|
result = FALSE;
|
|
_SEH2_LEAVE;
|
|
}
|
|
|
|
num_pts = 1;
|
|
|
|
line_pts[0].x = pdcattr->ptlCurrent.x;
|
|
line_pts[0].y = pdcattr->ptlCurrent.y;
|
|
|
|
for ( i = 0; i < cCount; i++ )
|
|
{
|
|
switch (lpbTypes[i])
|
|
{
|
|
case PT_MOVETO:
|
|
if (num_pts >= 2) IntGdiPolyline( dc, line_pts, num_pts );
|
|
num_pts = 0;
|
|
line_pts[num_pts++] = lppt[i];
|
|
break;
|
|
case PT_LINETO:
|
|
case (PT_LINETO | PT_CLOSEFIGURE):
|
|
line_pts[num_pts++] = lppt[i];
|
|
break;
|
|
case PT_BEZIERTO:
|
|
bzr[0].x = line_pts[num_pts - 1].x;
|
|
bzr[0].y = line_pts[num_pts - 1].y;
|
|
RtlCopyMemory( &bzr[1], &lppt[i], 3 * sizeof(POINT) );
|
|
|
|
if ((bzr_pts = GDI_Bezier( bzr, 4, &num_bzr_pts )))
|
|
{
|
|
size = num_pts + (cCount - i) + num_bzr_pts;
|
|
if (space < size)
|
|
{
|
|
space_old = space;
|
|
space = size * 2;
|
|
line_pts_old = line_pts;
|
|
line_pts = ExAllocatePoolWithTag(PagedPool, space * sizeof(POINT), TAG_SHAPE);
|
|
if (!line_pts) _SEH2_LEAVE;
|
|
RtlCopyMemory(line_pts, line_pts_old, space_old * sizeof(POINT));
|
|
ExFreePoolWithTag(line_pts_old, TAG_SHAPE);
|
|
line_pts_old = NULL;
|
|
}
|
|
RtlCopyMemory( &line_pts[num_pts], &bzr_pts[1], (num_bzr_pts - 1) * sizeof(POINT) );
|
|
num_pts += num_bzr_pts - 1;
|
|
ExFreePoolWithTag(bzr_pts, TAG_BEZIER);
|
|
bzr_pts = NULL;
|
|
}
|
|
i += 2;
|
|
break;
|
|
}
|
|
if (lpbTypes[i] & PT_CLOSEFIGURE) line_pts[num_pts++] = line_pts[0];
|
|
}
|
|
|
|
if (num_pts >= 2) IntGdiPolyline( dc, line_pts, num_pts );
|
|
IntGdiMoveToEx( dc, line_pts[num_pts - 1].x, line_pts[num_pts - 1].y, NULL, TRUE );
|
|
result = TRUE;
|
|
}
|
|
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
|
|
{
|
|
SetLastNtError(_SEH2_GetExceptionCode());
|
|
}
|
|
_SEH2_END;
|
|
|
|
if (line_pts != NULL)
|
|
{
|
|
ExFreePoolWithTag(line_pts, TAG_SHAPE);
|
|
}
|
|
|
|
if ((line_pts_old != NULL) && (line_pts_old != line_pts))
|
|
{
|
|
ExFreePoolWithTag(line_pts_old, TAG_SHAPE);
|
|
}
|
|
|
|
if (bzr_pts != NULL)
|
|
{
|
|
ExFreePoolWithTag(bzr_pts, TAG_BEZIER);
|
|
}
|
|
|
|
DC_UnlockDc(dc);
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* @implemented
|
|
*/
|
|
_Success_(return != FALSE)
|
|
BOOL
|
|
APIENTRY
|
|
NtGdiMoveTo(
|
|
IN HDC hdc,
|
|
IN INT x,
|
|
IN INT y,
|
|
OUT OPTIONAL LPPOINT pptOut)
|
|
{
|
|
PDC pdc;
|
|
BOOL Ret;
|
|
POINT Point;
|
|
|
|
pdc = DC_LockDc(hdc);
|
|
if (!pdc) return FALSE;
|
|
|
|
Ret = IntGdiMoveToEx(pdc, x, y, &Point, TRUE);
|
|
|
|
if (Ret && pptOut)
|
|
{
|
|
_SEH2_TRY
|
|
{
|
|
ProbeForWrite(pptOut, sizeof(POINT), 1);
|
|
RtlCopyMemory(pptOut, &Point, sizeof(POINT));
|
|
}
|
|
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
|
|
{
|
|
SetLastNtError(_SEH2_GetExceptionCode());
|
|
Ret = FALSE; // CHECKME: is this correct?
|
|
}
|
|
_SEH2_END;
|
|
}
|
|
|
|
DC_UnlockDc(pdc);
|
|
|
|
return Ret;
|
|
}
|
|
|
|
/* EOF */
|