Graphics.cpp revision 1a9c27c312ba20b2ceafcde18ce451724782d2a5
1#define LOG_TAG "GraphicsJNI"
2
3#include "jni.h"
4#include "GraphicsJNI.h"
5#include "NIOBuffer.h"
6#include "SkPicture.h"
7#include "SkRegion.h"
8#include <android_runtime/AndroidRuntime.h>
9
10//#define REPORT_SIZE_TO_JVM
11//#define TRACK_LOCK_COUNT
12
13void doThrow(JNIEnv* env, const char* exc, const char* msg) {
14    // don't throw a new exception if we already have one pending
15    if (env->ExceptionCheck() == JNI_FALSE) {
16        jclass npeClazz;
17
18        npeClazz = env->FindClass(exc);
19        LOG_FATAL_IF(npeClazz == NULL, "Unable to find class %s", exc);
20
21        env->ThrowNew(npeClazz, msg);
22    }
23}
24
25void doThrowNPE(JNIEnv* env) {
26    doThrow(env, "java/lang/NullPointerException");
27}
28
29void doThrowAIOOBE(JNIEnv* env) {
30    doThrow(env, "java/lang/ArrayIndexOutOfBoundsException");
31}
32
33void doThrowRE(JNIEnv* env, const char* msg) {
34    doThrow(env, "java/lang/RuntimeException", msg);
35}
36
37void doThrowIAE(JNIEnv* env, const char* msg) {
38    doThrow(env, "java/lang/IllegalArgumentException", msg);
39}
40
41void doThrowISE(JNIEnv* env, const char* msg) {
42    doThrow(env, "java/lang/IllegalStateException", msg);
43}
44
45void doThrowOOME(JNIEnv* env, const char* msg) {
46    doThrow(env, "java/lang/OutOfMemoryError", msg);
47}
48
49bool GraphicsJNI::hasException(JNIEnv *env) {
50    if (env->ExceptionCheck() != 0) {
51        LOGE("*** Uncaught exception returned from Java call!\n");
52        env->ExceptionDescribe();
53        return true;
54    }
55    return false;
56}
57
58///////////////////////////////////////////////////////////////////////////////
59
60AutoJavaFloatArray::AutoJavaFloatArray(JNIEnv* env, jfloatArray array,
61                                       int minLength, JNIAccess access)
62: fEnv(env), fArray(array), fPtr(NULL), fLen(0) {
63    SkASSERT(env);
64    if (array) {
65        fLen = env->GetArrayLength(array);
66        if (fLen < minLength) {
67            sk_throw();
68        }
69        fPtr = env->GetFloatArrayElements(array, NULL);
70    }
71    fReleaseMode = (access == kRO_JNIAccess) ? JNI_ABORT : 0;
72}
73
74AutoJavaFloatArray::~AutoJavaFloatArray() {
75    if (fPtr) {
76        fEnv->ReleaseFloatArrayElements(fArray, fPtr, fReleaseMode);
77    }
78}
79
80AutoJavaIntArray::AutoJavaIntArray(JNIEnv* env, jintArray array,
81                                       int minLength)
82: fEnv(env), fArray(array), fPtr(NULL), fLen(0) {
83    SkASSERT(env);
84    if (array) {
85        fLen = env->GetArrayLength(array);
86        if (fLen < minLength) {
87            sk_throw();
88        }
89        fPtr = env->GetIntArrayElements(array, NULL);
90    }
91}
92
93AutoJavaIntArray::~AutoJavaIntArray() {
94    if (fPtr) {
95        fEnv->ReleaseIntArrayElements(fArray, fPtr, 0);
96    }
97}
98
99AutoJavaShortArray::AutoJavaShortArray(JNIEnv* env, jshortArray array,
100                                       int minLength, JNIAccess access)
101: fEnv(env), fArray(array), fPtr(NULL), fLen(0) {
102    SkASSERT(env);
103    if (array) {
104        fLen = env->GetArrayLength(array);
105        if (fLen < minLength) {
106            sk_throw();
107        }
108        fPtr = env->GetShortArrayElements(array, NULL);
109    }
110    fReleaseMode = (access == kRO_JNIAccess) ? JNI_ABORT : 0;
111}
112
113AutoJavaShortArray::~AutoJavaShortArray() {
114    if (fPtr) {
115        fEnv->ReleaseShortArrayElements(fArray, fPtr, fReleaseMode);
116    }
117}
118
119AutoJavaByteArray::AutoJavaByteArray(JNIEnv* env, jbyteArray array,
120                                       int minLength)
121: fEnv(env), fArray(array), fPtr(NULL), fLen(0) {
122    SkASSERT(env);
123    if (array) {
124        fLen = env->GetArrayLength(array);
125        if (fLen < minLength) {
126            sk_throw();
127        }
128        fPtr = env->GetByteArrayElements(array, NULL);
129    }
130}
131
132AutoJavaByteArray::~AutoJavaByteArray() {
133    if (fPtr) {
134        fEnv->ReleaseByteArrayElements(fArray, fPtr, 0);
135    }
136}
137
138///////////////////////////////////////////////////////////////////////////////
139
140static jclass   gRect_class;
141static jfieldID gRect_leftFieldID;
142static jfieldID gRect_topFieldID;
143static jfieldID gRect_rightFieldID;
144static jfieldID gRect_bottomFieldID;
145
146static jclass   gRectF_class;
147static jfieldID gRectF_leftFieldID;
148static jfieldID gRectF_topFieldID;
149static jfieldID gRectF_rightFieldID;
150static jfieldID gRectF_bottomFieldID;
151
152static jclass   gPoint_class;
153static jfieldID gPoint_xFieldID;
154static jfieldID gPoint_yFieldID;
155
156static jclass   gPointF_class;
157static jfieldID gPointF_xFieldID;
158static jfieldID gPointF_yFieldID;
159
160static jclass   gBitmap_class;
161static jfieldID gBitmap_nativeInstanceID;
162static jmethodID gBitmap_constructorMethodID;
163static jmethodID gBitmap_allocBufferMethodID;
164
165static jclass   gBitmapConfig_class;
166static jfieldID gBitmapConfig_nativeInstanceID;
167
168static jclass   gCanvas_class;
169static jfieldID gCanvas_nativeInstanceID;
170
171static jclass   gPaint_class;
172static jfieldID gPaint_nativeInstanceID;
173
174static jclass   gPicture_class;
175static jfieldID gPicture_nativeInstanceID;
176
177static jclass   gRegion_class;
178static jfieldID gRegion_nativeInstanceID;
179static jmethodID gRegion_constructorMethodID;
180
181static jobject   gVMRuntime_singleton;
182static jmethodID gVMRuntime_trackExternalAllocationMethodID;
183static jmethodID gVMRuntime_trackExternalFreeMethodID;
184
185///////////////////////////////////////////////////////////////////////////////
186
187void GraphicsJNI::get_jrect(JNIEnv* env, jobject obj, int* L, int* T, int* R, int* B)
188{
189    SkASSERT(env->IsInstanceOf(obj, gRect_class));
190
191    *L = env->GetIntField(obj, gRect_leftFieldID);
192    *T = env->GetIntField(obj, gRect_topFieldID);
193    *R = env->GetIntField(obj, gRect_rightFieldID);
194    *B = env->GetIntField(obj, gRect_bottomFieldID);
195}
196
197void GraphicsJNI::set_jrect(JNIEnv* env, jobject obj, int L, int T, int R, int B)
198{
199    SkASSERT(env->IsInstanceOf(obj, gRect_class));
200
201    env->SetIntField(obj, gRect_leftFieldID, L);
202    env->SetIntField(obj, gRect_topFieldID, T);
203    env->SetIntField(obj, gRect_rightFieldID, R);
204    env->SetIntField(obj, gRect_bottomFieldID, B);
205}
206
207SkIRect* GraphicsJNI::jrect_to_irect(JNIEnv* env, jobject obj, SkIRect* ir)
208{
209    SkASSERT(env->IsInstanceOf(obj, gRect_class));
210
211    ir->set(env->GetIntField(obj, gRect_leftFieldID),
212            env->GetIntField(obj, gRect_topFieldID),
213            env->GetIntField(obj, gRect_rightFieldID),
214            env->GetIntField(obj, gRect_bottomFieldID));
215    return ir;
216}
217
218void GraphicsJNI::irect_to_jrect(const SkIRect& ir, JNIEnv* env, jobject obj)
219{
220    SkASSERT(env->IsInstanceOf(obj, gRect_class));
221
222    env->SetIntField(obj, gRect_leftFieldID, ir.fLeft);
223    env->SetIntField(obj, gRect_topFieldID, ir.fTop);
224    env->SetIntField(obj, gRect_rightFieldID, ir.fRight);
225    env->SetIntField(obj, gRect_bottomFieldID, ir.fBottom);
226}
227
228SkRect* GraphicsJNI::jrectf_to_rect(JNIEnv* env, jobject obj, SkRect* r)
229{
230    SkASSERT(env->IsInstanceOf(obj, gRectF_class));
231
232    r->set(SkFloatToScalar(env->GetFloatField(obj, gRectF_leftFieldID)),
233           SkFloatToScalar(env->GetFloatField(obj, gRectF_topFieldID)),
234           SkFloatToScalar(env->GetFloatField(obj, gRectF_rightFieldID)),
235           SkFloatToScalar(env->GetFloatField(obj, gRectF_bottomFieldID)));
236    return r;
237}
238
239SkRect* GraphicsJNI::jrect_to_rect(JNIEnv* env, jobject obj, SkRect* r)
240{
241    SkASSERT(env->IsInstanceOf(obj, gRect_class));
242
243    r->set(SkIntToScalar(env->GetIntField(obj, gRect_leftFieldID)),
244           SkIntToScalar(env->GetIntField(obj, gRect_topFieldID)),
245           SkIntToScalar(env->GetIntField(obj, gRect_rightFieldID)),
246           SkIntToScalar(env->GetIntField(obj, gRect_bottomFieldID)));
247    return r;
248}
249
250void GraphicsJNI::rect_to_jrectf(const SkRect& r, JNIEnv* env, jobject obj)
251{
252    SkASSERT(env->IsInstanceOf(obj, gRectF_class));
253
254    env->SetFloatField(obj, gRectF_leftFieldID, SkScalarToFloat(r.fLeft));
255    env->SetFloatField(obj, gRectF_topFieldID, SkScalarToFloat(r.fTop));
256    env->SetFloatField(obj, gRectF_rightFieldID, SkScalarToFloat(r.fRight));
257    env->SetFloatField(obj, gRectF_bottomFieldID, SkScalarToFloat(r.fBottom));
258}
259
260SkIPoint* GraphicsJNI::jpoint_to_ipoint(JNIEnv* env, jobject obj, SkIPoint* point)
261{
262    SkASSERT(env->IsInstanceOf(obj, gPoint_class));
263
264    point->set(env->GetIntField(obj, gPoint_xFieldID),
265               env->GetIntField(obj, gPoint_yFieldID));
266    return point;
267}
268
269void GraphicsJNI::ipoint_to_jpoint(const SkIPoint& ir, JNIEnv* env, jobject obj)
270{
271    SkASSERT(env->IsInstanceOf(obj, gPoint_class));
272
273    env->SetIntField(obj, gPoint_xFieldID, ir.fX);
274    env->SetIntField(obj, gPoint_yFieldID, ir.fY);
275}
276
277SkPoint* GraphicsJNI::jpointf_to_point(JNIEnv* env, jobject obj, SkPoint* point)
278{
279    SkASSERT(env->IsInstanceOf(obj, gPointF_class));
280
281    point->set(SkFloatToScalar(env->GetIntField(obj, gPointF_xFieldID)),
282               SkFloatToScalar(env->GetIntField(obj, gPointF_yFieldID)));
283    return point;
284}
285
286void GraphicsJNI::point_to_jpointf(const SkPoint& r, JNIEnv* env, jobject obj)
287{
288    SkASSERT(env->IsInstanceOf(obj, gPointF_class));
289
290    env->SetFloatField(obj, gPointF_xFieldID, SkScalarToFloat(r.fX));
291    env->SetFloatField(obj, gPointF_yFieldID, SkScalarToFloat(r.fY));
292}
293
294SkBitmap* GraphicsJNI::getNativeBitmap(JNIEnv* env, jobject bitmap) {
295    SkASSERT(env);
296    SkASSERT(bitmap);
297    SkASSERT(env->IsInstanceOf(bitmap, gBitmap_class));
298    SkBitmap* b = (SkBitmap*)env->GetIntField(bitmap, gBitmap_nativeInstanceID);
299    SkASSERT(b);
300    return b;
301}
302
303SkBitmap::Config GraphicsJNI::getNativeBitmapConfig(JNIEnv* env,
304                                                    jobject jconfig) {
305    SkASSERT(env);
306    if (NULL == jconfig) {
307        return SkBitmap::kNo_Config;
308    }
309    SkASSERT(env->IsInstanceOf(jconfig, gBitmapConfig_class));
310    int c = env->GetIntField(jconfig, gBitmapConfig_nativeInstanceID);
311    if (c < 0 || c >= SkBitmap::kConfigCount) {
312        c = SkBitmap::kNo_Config;
313    }
314    return static_cast<SkBitmap::Config>(c);
315}
316
317SkCanvas* GraphicsJNI::getNativeCanvas(JNIEnv* env, jobject canvas) {
318    SkASSERT(env);
319    SkASSERT(canvas);
320    SkASSERT(env->IsInstanceOf(canvas, gCanvas_class));
321    SkCanvas* c = (SkCanvas*)env->GetIntField(canvas, gCanvas_nativeInstanceID);
322    SkASSERT(c);
323    return c;
324}
325
326SkPaint* GraphicsJNI::getNativePaint(JNIEnv* env, jobject paint) {
327    SkASSERT(env);
328    SkASSERT(paint);
329    SkASSERT(env->IsInstanceOf(paint, gPaint_class));
330    SkPaint* p = (SkPaint*)env->GetIntField(paint, gPaint_nativeInstanceID);
331    SkASSERT(p);
332    return p;
333}
334
335SkPicture* GraphicsJNI::getNativePicture(JNIEnv* env, jobject picture)
336{
337    SkASSERT(env);
338    SkASSERT(picture);
339    SkASSERT(env->IsInstanceOf(picture, gPicture_class));
340    SkPicture* p = (SkPicture*)env->GetIntField(picture, gPicture_nativeInstanceID);
341    SkASSERT(p);
342    return p;
343}
344
345SkRegion* GraphicsJNI::getNativeRegion(JNIEnv* env, jobject region)
346{
347    SkASSERT(env);
348    SkASSERT(region);
349    SkASSERT(env->IsInstanceOf(region, gRegion_class));
350    SkRegion* r = (SkRegion*)env->GetIntField(region, gRegion_nativeInstanceID);
351    SkASSERT(r);
352    return r;
353}
354
355///////////////////////////////////////////////////////////////////////////////////////////
356
357jobject GraphicsJNI::createBitmap(JNIEnv* env, SkBitmap* bitmap, bool isMutable,
358                                  jbyteArray ninepatch, int density)
359{
360    SkASSERT(bitmap != NULL);
361    SkASSERT(NULL != bitmap->pixelRef());
362
363    jobject obj = env->AllocObject(gBitmap_class);
364    if (obj) {
365        env->CallVoidMethod(obj, gBitmap_constructorMethodID,
366                            (jint)bitmap, isMutable, ninepatch, density);
367        if (hasException(env)) {
368            obj = NULL;
369        }
370    }
371    return obj;
372}
373
374jobject GraphicsJNI::createRegion(JNIEnv* env, SkRegion* region)
375{
376    SkASSERT(region != NULL);
377    jobject obj = env->AllocObject(gRegion_class);
378    if (obj) {
379        env->CallVoidMethod(obj, gRegion_constructorMethodID, (jint)region, 0);
380        if (hasException(env)) {
381            obj = NULL;
382        }
383    }
384    return obj;
385}
386
387#include "SkPixelRef.h"
388
389static JNIEnv* vm2env(JavaVM* vm)
390{
391    JNIEnv* env = NULL;
392    if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK || NULL == env)
393    {
394        SkDebugf("------- [%p] vm->GetEnv() failed\n", vm);
395        sk_throw();
396    }
397    return env;
398}
399
400#ifdef TRACK_LOCK_COUNT
401    static int gLockCount;
402#endif
403
404///////////////////////////////////////////////////////////////////////////////
405
406#include "SkMallocPixelRef.h"
407
408/*  Extend SkMallocPixelRef to inform the VM when we free up the storage
409*/
410class AndroidPixelRef : public SkMallocPixelRef {
411public:
412    /** Allocate the specified buffer for pixels. The memory is freed when the
413        last owner of this pixelref is gone. Our caller has already informed
414        the VM of our allocation.
415    */
416    AndroidPixelRef(JNIEnv* env, void* storage, size_t size,
417            SkColorTable* ctable) : SkMallocPixelRef(storage, size, ctable) {
418        SkASSERT(storage);
419        SkASSERT(env);
420
421        if (env->GetJavaVM(&fVM) != JNI_OK) {
422            SkDebugf("------ [%p] env->GetJavaVM failed\n", env);
423            sk_throw();
424        }
425    }
426
427    virtual ~AndroidPixelRef() {
428        JNIEnv* env = vm2env(fVM);
429//        SkDebugf("-------------- inform VM we're releasing %d bytes\n", this->getSize());
430        jlong jsize = this->getSize();  // the VM wants longs for the size
431        env->CallVoidMethod(gVMRuntime_singleton,
432                            gVMRuntime_trackExternalFreeMethodID,
433                            jsize);
434        if (GraphicsJNI::hasException(env)) {
435            env->ExceptionClear();
436        }
437    }
438
439private:
440    JavaVM* fVM;
441};
442
443bool GraphicsJNI::setJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,
444                                  SkColorTable* ctable, bool reportSizeToVM) {
445    Sk64 size64 = bitmap->getSize64();
446    if (size64.isNeg() || !size64.is32()) {
447        doThrow(env, "java/lang/IllegalArgumentException",
448                     "bitmap size exceeds 32bits");
449        return false;
450    }
451
452    size_t size = size64.get32();
453    jlong jsize = size;  // the VM wants longs for the size
454    if (reportSizeToVM) {
455        //    SkDebugf("-------------- inform VM we've allocated %d bytes\n", size);
456        bool r = env->CallBooleanMethod(gVMRuntime_singleton,
457                                    gVMRuntime_trackExternalAllocationMethodID,
458                                    jsize);
459        if (GraphicsJNI::hasException(env)) {
460            return false;
461        }
462        if (!r) {
463            LOGE("VM won't let us allocate %zd bytes\n", size);
464            doThrowOOME(env, "bitmap size exceeds VM budget");
465            return false;
466        }
467    }
468    // call the version of malloc that returns null on failure
469    void* addr = sk_malloc_flags(size, 0);
470    if (NULL == addr) {
471        if (reportSizeToVM) {
472            //        SkDebugf("-------------- inform VM we're releasing %d bytes which we couldn't allocate\n", size);
473            // we didn't actually allocate it, so inform the VM
474            env->CallVoidMethod(gVMRuntime_singleton,
475                                 gVMRuntime_trackExternalFreeMethodID,
476                                 jsize);
477            if (!GraphicsJNI::hasException(env)) {
478                doThrowOOME(env, "bitmap size too large for malloc");
479            }
480        }
481        return false;
482    }
483
484    SkPixelRef* pr = reportSizeToVM ?
485                        new AndroidPixelRef(env, addr, size, ctable) :
486                        new SkMallocPixelRef(addr, size, ctable);
487    bitmap->setPixelRef(pr)->unref();
488    // since we're already allocated, we lockPixels right away
489    // HeapAllocator behaves this way too
490    bitmap->lockPixels();
491    return true;
492}
493
494///////////////////////////////////////////////////////////////////////////////
495
496JavaPixelAllocator::JavaPixelAllocator(JNIEnv* env, bool reportSizeToVM)
497    : fEnv(env), fReportSizeToVM(reportSizeToVM) {}
498
499bool JavaPixelAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) {
500    return GraphicsJNI::setJavaPixelRef(fEnv, bitmap, ctable, fReportSizeToVM);
501}
502
503////////////////////////////////////////////////////////////////////////////////
504
505static jclass make_globalref(JNIEnv* env, const char classname[])
506{
507    jclass c = env->FindClass(classname);
508    SkASSERT(c);
509    return (jclass)env->NewGlobalRef(c);
510}
511
512static jfieldID getFieldIDCheck(JNIEnv* env, jclass clazz,
513                                const char fieldname[], const char type[])
514{
515    jfieldID id = env->GetFieldID(clazz, fieldname, type);
516    SkASSERT(id);
517    return id;
518}
519
520int register_android_graphics_Graphics(JNIEnv* env)
521{
522    jmethodID m;
523    jclass c;
524
525    gRect_class = make_globalref(env, "android/graphics/Rect");
526    gRect_leftFieldID = getFieldIDCheck(env, gRect_class, "left", "I");
527    gRect_topFieldID = getFieldIDCheck(env, gRect_class, "top", "I");
528    gRect_rightFieldID = getFieldIDCheck(env, gRect_class, "right", "I");
529    gRect_bottomFieldID = getFieldIDCheck(env, gRect_class, "bottom", "I");
530
531    gRectF_class = make_globalref(env, "android/graphics/RectF");
532    gRectF_leftFieldID = getFieldIDCheck(env, gRectF_class, "left", "F");
533    gRectF_topFieldID = getFieldIDCheck(env, gRectF_class, "top", "F");
534    gRectF_rightFieldID = getFieldIDCheck(env, gRectF_class, "right", "F");
535    gRectF_bottomFieldID = getFieldIDCheck(env, gRectF_class, "bottom", "F");
536
537    gPoint_class = make_globalref(env, "android/graphics/Point");
538    gPoint_xFieldID = getFieldIDCheck(env, gPoint_class, "x", "I");
539    gPoint_yFieldID = getFieldIDCheck(env, gPoint_class, "y", "I");
540
541    gPointF_class = make_globalref(env, "android/graphics/PointF");
542    gPointF_xFieldID = getFieldIDCheck(env, gPointF_class, "x", "F");
543    gPointF_yFieldID = getFieldIDCheck(env, gPointF_class, "y", "F");
544
545    gBitmap_class = make_globalref(env, "android/graphics/Bitmap");
546    gBitmap_nativeInstanceID = getFieldIDCheck(env, gBitmap_class, "mNativeBitmap", "I");
547    gBitmap_constructorMethodID = env->GetMethodID(gBitmap_class, "<init>",
548                                            "(IZ[BI)V");
549
550    gBitmapConfig_class = make_globalref(env, "android/graphics/Bitmap$Config");
551    gBitmapConfig_nativeInstanceID = getFieldIDCheck(env, gBitmapConfig_class,
552                                                     "nativeInt", "I");
553
554    gCanvas_class = make_globalref(env, "android/graphics/Canvas");
555    gCanvas_nativeInstanceID = getFieldIDCheck(env, gCanvas_class, "mNativeCanvas", "I");
556
557    gPaint_class = make_globalref(env, "android/graphics/Paint");
558    gPaint_nativeInstanceID = getFieldIDCheck(env, gPaint_class, "mNativePaint", "I");
559
560    gPicture_class = make_globalref(env, "android/graphics/Picture");
561    gPicture_nativeInstanceID = getFieldIDCheck(env, gPicture_class, "mNativePicture", "I");
562
563    gRegion_class = make_globalref(env, "android/graphics/Region");
564    gRegion_nativeInstanceID = getFieldIDCheck(env, gRegion_class, "mNativeRegion", "I");
565    gRegion_constructorMethodID = env->GetMethodID(gRegion_class, "<init>",
566        "(II)V");
567
568    // Get the VMRuntime class.
569    c = env->FindClass("dalvik/system/VMRuntime");
570    SkASSERT(c);
571    // Look up VMRuntime.getRuntime().
572    m = env->GetStaticMethodID(c, "getRuntime", "()Ldalvik/system/VMRuntime;");
573    SkASSERT(m);
574    // Call VMRuntime.getRuntime() and hold onto its result.
575    gVMRuntime_singleton = env->CallStaticObjectMethod(c, m);
576    SkASSERT(gVMRuntime_singleton);
577    gVMRuntime_singleton = (jobject)env->NewGlobalRef(gVMRuntime_singleton);
578    // Look up the VMRuntime methods we'll be using.
579    gVMRuntime_trackExternalAllocationMethodID =
580                        env->GetMethodID(c, "trackExternalAllocation", "(J)Z");
581    gVMRuntime_trackExternalFreeMethodID =
582                            env->GetMethodID(c, "trackExternalFree", "(J)V");
583
584    NIOBuffer::RegisterJNI(env);
585
586    return 0;
587}
588
589