ImageCrop.java revision c5590eb1a20b112e67e4c43684790587f844fc6b
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.gallery3d.filtershow.imageshow;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.Matrix;
25import android.graphics.Paint;
26import android.graphics.RectF;
27import android.graphics.drawable.Drawable;
28import android.util.AttributeSet;
29import android.util.Log;
30
31import com.android.gallery3d.R;
32
33public class ImageCrop extends ImageGeometry {
34    private static final boolean LOGV = false;
35    private static final int MOVE_LEFT = 1;
36    private static final int MOVE_TOP = 2;
37    private static final int MOVE_RIGHT = 4;
38    private static final int MOVE_BOTTOM = 8;
39    private static final int MOVE_BLOCK = 16;
40
41    //Corners
42    private static final int TOP_LEFT = MOVE_TOP | MOVE_LEFT;
43    private static final int TOP_RIGHT = MOVE_TOP | MOVE_RIGHT;
44    private static final int BOTTOM_RIGHT = MOVE_BOTTOM | MOVE_RIGHT;
45    private static final int BOTTOM_LEFT = MOVE_BOTTOM | MOVE_LEFT;
46
47    private static final float MIN_CROP_WIDTH_HEIGHT = 0.1f;
48    private static final int TOUCH_TOLERANCE = 30;
49
50    private boolean mFirstDraw = true;
51    private float mAspectWidth = 1;
52    private float mAspectHeight = 1;
53    private boolean mFixAspectRatio = false;
54
55    private final Paint borderPaint;
56
57    private int movingEdges;
58    private final Drawable cropIndicator;
59    private final int indicatorSize;
60
61    private static final String LOGTAG = "ImageCrop";
62
63    private static final Paint gPaint = new Paint();
64
65    public ImageCrop(Context context) {
66        super(context);
67        Resources resources = context.getResources();
68        cropIndicator = resources.getDrawable(R.drawable.camera_crop);
69        indicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size);
70        int borderColor = Color.argb(128, 255,  255,  255);
71        borderPaint = new Paint();
72        borderPaint.setStyle(Paint.Style.STROKE);
73        borderPaint.setColor(borderColor);
74        borderPaint.setStrokeWidth(2f);
75    }
76
77    public ImageCrop(Context context, AttributeSet attrs) {
78        super(context, attrs);
79        Resources resources = context.getResources();
80        cropIndicator = resources.getDrawable(R.drawable.camera_crop);
81        indicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size);
82        int borderColor = Color.argb(128, 255,  255,  255);
83        borderPaint = new Paint();
84        borderPaint.setStyle(Paint.Style.STROKE);
85        borderPaint.setColor(borderColor);
86        borderPaint.setStrokeWidth(2f);
87    }
88
89    @Override
90    public String getName() {
91        return "Crop";
92    }
93
94    private boolean switchCropBounds(int moving_corner, RectF dst) {
95        RectF crop = getCropBoundsDisplayed();
96        float dx1 = 0;
97        float dy1 = 0;
98        float dx2 = 0;
99        float dy2 = 0;
100        if ((moving_corner & MOVE_RIGHT) != 0) {
101            dx1 = mCurrentX - crop.right;
102        } else if ((moving_corner & MOVE_LEFT) != 0) {
103            dx1 = mCurrentX - crop.left;
104        }
105        if ((moving_corner & MOVE_BOTTOM) != 0) {
106            dy1 = mCurrentY - crop.bottom;
107        } else if ((moving_corner & MOVE_TOP) != 0) {
108            dy1 = mCurrentY - crop.top;
109        }
110        RectF newCrop = null;
111        //Fix opposite corner in place and move sides
112        if (moving_corner == BOTTOM_RIGHT) {
113            newCrop = new RectF(crop.left, crop.top, crop.left + crop.height(), crop.top
114                    + crop.width());
115        } else if (moving_corner == BOTTOM_LEFT) {
116            newCrop = new RectF(crop.right - crop.height(), crop.top, crop.right, crop.top
117                    + crop.width());
118        } else if (moving_corner == TOP_LEFT) {
119            newCrop = new RectF(crop.right - crop.height(), crop.bottom - crop.width(),
120                    crop.right, crop.bottom);
121        } else if (moving_corner == TOP_RIGHT) {
122            newCrop = new RectF(crop.left, crop.bottom - crop.width(), crop.left
123                    + crop.height(), crop.bottom);
124        }
125        if ((moving_corner & MOVE_RIGHT) != 0) {
126            dx2 = mCurrentX - newCrop.right;
127        } else if ((moving_corner & MOVE_LEFT) != 0) {
128            dx2 = mCurrentX - newCrop.left;
129        }
130        if ((moving_corner & MOVE_BOTTOM) != 0) {
131            dy2 = mCurrentY - newCrop.bottom;
132        } else if ((moving_corner & MOVE_TOP) != 0) {
133            dy2 = mCurrentY - newCrop.top;
134        }
135        if (Math.sqrt(dx1*dx1 + dy1*dy1) > Math.sqrt(dx2*dx2 + dy2*dy2)){
136             Matrix m = getCropBoundDisplayMatrix();
137             Matrix m0 = new Matrix();
138             if (!m.invert(m0)){
139                 if (LOGV)
140                     Log.v(LOGTAG, "FAILED TO INVERT CROP MATRIX");
141                 return false;
142             }
143             if (!m0.mapRect(newCrop)){
144                 if (LOGV)
145                     Log.v(LOGTAG, "FAILED TO MAP RECTANGLE TO RECTANGLE");
146                 return false;
147             }
148             float temp = mAspectWidth;
149             mAspectWidth = mAspectHeight;
150             mAspectHeight = temp;
151             dst.set(newCrop);
152             return true;
153        }
154        return false;
155    }
156
157    public void apply(float w, float h){
158        mFixAspectRatio = true;
159        mAspectWidth = w;
160        mAspectHeight = h;
161        setLocalCropBounds(getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
162                getLocalStraighten()));
163        cropSetup();
164        saveAndSetPreset();
165        invalidate();
166    }
167
168    public void applyOriginal() {
169        mFixAspectRatio = true;
170        RectF photobounds = getLocalPhotoBounds();
171        float w = photobounds.width();
172        float h = photobounds.height();
173        float scale = Math.min(w, h);
174        mAspectWidth = w / scale;
175        mAspectHeight = h / scale;
176        setLocalCropBounds(getUntranslatedStraightenCropBounds(photobounds,
177                getLocalStraighten()));
178        cropSetup();
179        saveAndSetPreset();
180        invalidate();
181    }
182
183    public void applyClear() {
184        mFixAspectRatio = false;
185        setLocalCropBounds(getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
186                getLocalStraighten()));
187        cropSetup();
188        saveAndSetPreset();
189        invalidate();
190    }
191
192    private float getScaledMinWidthHeight() {
193        RectF disp = new RectF(0, 0, getWidth(), getHeight());
194        float scaled = Math.min(disp.width(), disp.height()) * MIN_CROP_WIDTH_HEIGHT
195                / computeScale(getWidth(), getHeight());
196        return scaled;
197    }
198
199    protected Matrix getCropRotationMatrix(float rotation, RectF localImage) {
200        Matrix m = getLocalGeoFlipMatrix(localImage.width(), localImage.height());
201        m.postRotate(rotation, localImage.centerX(), localImage.centerY());
202        if (!m.rectStaysRect()) {
203            return null;
204        }
205        return m;
206    }
207
208    protected Matrix getCropBoundDisplayMatrix(){
209        Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
210        if (m == null) {
211            if (LOGV)
212                Log.v(LOGTAG, "FAILED TO MAP CROP BOUNDS TO RECTANGLE");
213            m = new Matrix();
214        }
215        float zoom = computeScale(getWidth(), getHeight());
216        m.postTranslate(mXOffset, mYOffset);
217        m.postScale(zoom, zoom, mCenterX, mCenterY);
218        return m;
219    }
220
221    protected RectF getCropBoundsDisplayed() {
222        RectF bounds = getLocalCropBounds();
223        RectF crop = new RectF(bounds);
224        Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
225
226        if (m == null) {
227            if (LOGV)
228                Log.v(LOGTAG, "FAILED TO MAP CROP BOUNDS TO RECTANGLE");
229            m = new Matrix();
230        } else {
231            m.mapRect(crop);
232        }
233        m = new Matrix();
234        float zoom = computeScale(getWidth(), getHeight());
235        m.setScale(zoom, zoom, mCenterX, mCenterY);
236        m.preTranslate(mXOffset, mYOffset);
237        m.mapRect(crop);
238        return crop;
239    }
240
241    private RectF getRotatedCropBounds() {
242        RectF bounds = getLocalCropBounds();
243        RectF crop = new RectF(bounds);
244        Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
245
246        if (m == null) {
247            if (LOGV)
248                Log.v(LOGTAG, "FAILED TO MAP CROP BOUNDS TO RECTANGLE");
249            return null;
250        } else {
251            m.mapRect(crop);
252        }
253        return crop;
254    }
255
256    private RectF getUnrotatedCropBounds(RectF cropBounds) {
257        Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
258
259        if (m == null) {
260            if (LOGV)
261                Log.v(LOGTAG, "FAILED TO GET ROTATION MATRIX");
262            return null;
263        }
264        Matrix m0 = new Matrix();
265        if (!m.invert(m0)) {
266            if (LOGV)
267                Log.v(LOGTAG, "FAILED TO INVERT ROTATION MATRIX");
268            return null;
269        }
270        RectF crop = new RectF(cropBounds);
271        if (!m0.mapRect(crop)) {
272            if (LOGV)
273                Log.v(LOGTAG, "FAILED TO UNROTATE CROPPING BOUNDS");
274            return null;
275        }
276        return crop;
277    }
278
279    private RectF getRotatedStraightenBounds() {
280        RectF straightenBounds = getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
281                getLocalStraighten());
282        Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
283
284        if (m == null) {
285            if (LOGV)
286                Log.v(LOGTAG, "FAILED TO MAP STRAIGHTEN BOUNDS TO RECTANGLE");
287            return null;
288        } else {
289            m.mapRect(straightenBounds);
290        }
291        return straightenBounds;
292    }
293
294    /**
295     * Sets cropped bounds; modifies the bounds if it's smaller than the allowed
296     * dimensions.
297     */
298    public void setCropBounds(RectF bounds) {
299        // Avoid cropping smaller than minimum width or height.
300        RectF cbounds = new RectF(bounds);
301        float minWidthHeight = getScaledMinWidthHeight();
302        float aw = mAspectWidth;
303        float ah = mAspectHeight;
304        if (mFixAspectRatio) {
305            minWidthHeight /= aw * ah;
306            int r = (int) (getLocalRotation() / 90);
307            if (r % 2 != 0) {
308                float temp = aw;
309                aw = ah;
310                ah = temp;
311            }
312        }
313
314        float newWidth = cbounds.width();
315        float newHeight = cbounds.height();
316        if (mFixAspectRatio) {
317            if (newWidth < (minWidthHeight * aw) || newHeight < (minWidthHeight * ah)) {
318                newWidth = minWidthHeight * aw;
319                newHeight = minWidthHeight * ah;
320            }
321        } else {
322            if (newWidth < minWidthHeight) {
323                newWidth = minWidthHeight;
324            }
325            if (newHeight < minWidthHeight) {
326                newHeight = minWidthHeight;
327            }
328        }
329        RectF pbounds = getLocalPhotoBounds();
330        if (pbounds.width() < minWidthHeight) {
331            newWidth = pbounds.width();
332        }
333        if (pbounds.height() < minWidthHeight) {
334            newHeight = pbounds.height();
335        }
336
337        cbounds.set(cbounds.left, cbounds.top, cbounds.left + newWidth, cbounds.top + newHeight);
338        RectF straightenBounds = getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
339                getLocalStraighten());
340        cbounds.intersect(straightenBounds);
341
342        if (mFixAspectRatio) {
343            fixAspectRatio(cbounds, aw, ah);
344        }
345        setLocalCropBounds(cbounds);
346        invalidate();
347    }
348
349    private void detectMovingEdges(float x, float y) {
350        RectF cropped = getCropBoundsDisplayed();
351        movingEdges = 0;
352
353        // Check left or right.
354        float left = Math.abs(x - cropped.left);
355        float right = Math.abs(x - cropped.right);
356        if ((left <= TOUCH_TOLERANCE) && (left < right)) {
357            movingEdges |= MOVE_LEFT;
358        }
359        else if (right <= TOUCH_TOLERANCE) {
360            movingEdges |= MOVE_RIGHT;
361        }
362
363        // Check top or bottom.
364        float top = Math.abs(y - cropped.top);
365        float bottom = Math.abs(y - cropped.bottom);
366        if ((top <= TOUCH_TOLERANCE) & (top < bottom)) {
367            movingEdges |= MOVE_TOP;
368        }
369        else if (bottom <= TOUCH_TOLERANCE) {
370            movingEdges |= MOVE_BOTTOM;
371        }
372        // Check inside block.
373        if (cropped.contains(x, y) && (movingEdges == 0)) {
374            movingEdges = MOVE_BLOCK;
375        }
376        if (mFixAspectRatio && (movingEdges != MOVE_BLOCK)) {
377            movingEdges = fixEdgeToCorner(movingEdges);
378        }
379        invalidate();
380    }
381
382    private int fixEdgeToCorner(int moving_edges){
383        if (moving_edges == MOVE_LEFT) {
384            moving_edges |= MOVE_TOP;
385        }
386        if (moving_edges == MOVE_TOP) {
387            moving_edges |= MOVE_LEFT;
388        }
389        if (moving_edges == MOVE_RIGHT) {
390            moving_edges |= MOVE_BOTTOM;
391        }
392        if (moving_edges == MOVE_BOTTOM) {
393            moving_edges |= MOVE_RIGHT;
394        }
395        return moving_edges;
396    }
397
398    private RectF fixedCornerResize(RectF r, int moving_corner, float dx, float dy){
399        RectF newCrop = null;
400        //Fix opposite corner in place and move sides
401        if (moving_corner == BOTTOM_RIGHT) {
402            newCrop = new RectF(r.left, r.top, r.left + r.width() + dx, r.top + r.height()
403                    + dy);
404        } else if (moving_corner == BOTTOM_LEFT) {
405            newCrop = new RectF(r.right - r.width() + dx, r.top, r.right, r.top + r.height()
406                    + dy);
407        } else if (moving_corner == TOP_LEFT) {
408            newCrop = new RectF(r.right - r.width() + dx, r.bottom - r.height() + dy,
409                    r.right, r.bottom);
410        } else if (moving_corner == TOP_RIGHT) {
411            newCrop = new RectF(r.left, r.bottom - r.height() + dy, r.left
412                    + r.width() + dx, r.bottom);
413        }
414        return newCrop;
415    }
416
417    private void moveEdges(float dX, float dY) {
418        RectF cropped = getRotatedCropBounds();
419        float minWidthHeight = getScaledMinWidthHeight();
420        float scale = computeScale(getWidth(), getHeight());
421        float deltaX = dX / scale;
422        float deltaY = dY / scale;
423        int select = movingEdges;
424        if (mFixAspectRatio && (select != MOVE_BLOCK)) {
425            if (select == MOVE_LEFT) {
426                select |= MOVE_TOP;
427            }
428            if (select == MOVE_TOP) {
429                select |= MOVE_LEFT;
430            }
431            if (select == MOVE_RIGHT) {
432                select |= MOVE_BOTTOM;
433            }
434            if (select == MOVE_BOTTOM) {
435                select |= MOVE_RIGHT;
436            }
437            RectF blank = new RectF();
438            if(switchCropBounds(select, blank)){
439                setCropBounds(blank);
440                return;
441            }
442        }
443
444        if (select == MOVE_BLOCK) {
445            RectF straight = getRotatedStraightenBounds();
446            // Move the whole cropped bounds within the photo display bounds.
447            deltaX = (deltaX > 0) ? Math.min(straight.right - cropped.right, deltaX)
448                    : Math.max(straight.left - cropped.left, deltaX);
449            deltaY = (deltaY > 0) ? Math.min(straight.bottom - cropped.bottom, deltaY)
450                    : Math.max(straight.top - cropped.top, deltaY);
451            cropped.offset(deltaX, deltaY);
452        } else {
453            float dx = 0;
454            float dy = 0;
455
456            if ((select & MOVE_LEFT) != 0) {
457                dx = Math.min(cropped.left + deltaX, cropped.right - minWidthHeight) - cropped.left;
458            }
459            if ((select & MOVE_TOP) != 0) {
460                dy = Math.min(cropped.top + deltaY, cropped.bottom - minWidthHeight) - cropped.top;
461            }
462            if ((select & MOVE_RIGHT) != 0) {
463                dx = Math.max(cropped.right + deltaX, cropped.left + minWidthHeight)
464                        - cropped.right;
465            }
466            if ((select & MOVE_BOTTOM) != 0) {
467                dy = Math.max(cropped.bottom + deltaY, cropped.top + minWidthHeight)
468                        - cropped.bottom;
469            }
470
471            if (mFixAspectRatio) {
472                RectF crop = getCropBoundsDisplayed();
473                float [] l1 = {crop.left, crop.bottom};
474                float [] l2 = {crop.right, crop.top};
475                if(movingEdges == TOP_LEFT || movingEdges == BOTTOM_RIGHT){
476                    l1[1] = crop.top;
477                    l2[1] = crop.bottom;
478                }
479                float[] b = { l1[0] - l2[0], l1[1] - l2[1] };
480                float[] disp = {dx, dy};
481                float[] bUnit = GeometryMath.normalize(b);
482                float sp = GeometryMath.scalarProjection(disp, bUnit);
483                dx = sp * bUnit[0];
484                dy = sp * bUnit[1];
485                RectF newCrop = fixedCornerResize(crop, select, dx * scale, dy * scale);
486                Matrix m = getCropBoundDisplayMatrix();
487                Matrix m0 = new Matrix();
488                if (!m.invert(m0)){
489                    if (LOGV)
490                        Log.v(LOGTAG, "FAILED TO INVERT CROP MATRIX");
491                    return;
492                }
493                if (!m0.mapRect(newCrop)){
494                    if (LOGV)
495                        Log.v(LOGTAG, "FAILED TO MAP RECTANGLE TO RECTANGLE");
496                    return;
497                }
498                setCropBounds(newCrop);
499                return;
500            } else {
501                if ((select & MOVE_LEFT) != 0) {
502                    cropped.left += dx;
503                }
504                if ((select & MOVE_TOP) != 0) {
505                    cropped.top += dy;
506                }
507                if ((select & MOVE_RIGHT) != 0) {
508                    cropped.right += dx;
509                }
510                if ((select & MOVE_BOTTOM) != 0) {
511                    cropped.bottom += dy;
512                }
513            }
514        }
515        movingEdges = select;
516        Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
517        Matrix m0 = new Matrix();
518        if (!m.invert(m0)) {
519            if (LOGV)
520                Log.v(LOGTAG, "FAILED TO INVERT ROTATION MATRIX");
521        }
522        if (!m0.mapRect(cropped)) {
523            if (LOGV)
524                Log.v(LOGTAG, "FAILED TO UNROTATE CROPPING BOUNDS");
525        }
526        setCropBounds(cropped);
527    }
528
529    private void drawIndicator(Canvas canvas, Drawable indicator, float centerX, float centerY) {
530        int left = (int) centerX - indicatorSize / 2;
531        int top = (int) centerY - indicatorSize / 2;
532        indicator.setBounds(left, top, left + indicatorSize, top + indicatorSize);
533        indicator.draw(canvas);
534    }
535
536    @Override
537    protected void setActionDown(float x, float y) {
538        super.setActionDown(x, y);
539        detectMovingEdges(x, y);
540    }
541
542    @Override
543    protected void setActionUp() {
544        super.setActionUp();
545        movingEdges = 0;
546    }
547
548    @Override
549    protected void setActionMove(float x, float y) {
550        if (movingEdges != 0){
551            moveEdges(x - mCurrentX, y - mCurrentY);
552        }
553        super.setActionMove(x, y);
554    }
555
556    private void cropSetup() {
557        if (mFixAspectRatio) {
558            RectF cb = getRotatedCropBounds();
559            fixAspectRatio(cb, mAspectWidth, mAspectHeight);
560            RectF cb0 = getUnrotatedCropBounds(cb);
561            setCropBounds(cb0);
562        } else {
563            setCropBounds(getLocalCropBounds());
564        }
565    }
566
567    @Override
568    protected void gainedVisibility() {
569        cropSetup();
570        mFirstDraw = true;
571    }
572
573    @Override
574    public void resetParameter() {
575        super.resetParameter();
576        cropSetup();
577    }
578
579    @Override
580    protected void lostVisibility() {
581    }
582
583    @Override
584    protected void drawShape(Canvas canvas, Bitmap image) {
585        // TODO: move style to xml
586        gPaint.setAntiAlias(true);
587        gPaint.setFilterBitmap(true);
588        gPaint.setDither(true);
589        gPaint.setARGB(255, 255, 255, 255);
590
591        if (mFirstDraw) {
592            cropSetup();
593            mFirstDraw = false;
594        }
595        float rotation = getLocalRotation();
596        drawTransformedBitmap(canvas, image, gPaint, true);
597
598        gPaint.setARGB(255, 125, 255, 128);
599        gPaint.setStrokeWidth(3);
600        gPaint.setStyle(Paint.Style.STROKE);
601        drawStraighten(canvas, gPaint);
602        RectF scaledCrop = unrotatedCropBounds();
603        int decoded_moving = decoder(movingEdges, rotation);
604        canvas.save();
605        canvas.rotate(rotation, mCenterX, mCenterY);
606        boolean notMoving = decoded_moving == 0;
607        if (((decoded_moving & MOVE_TOP) != 0) || notMoving) {
608            drawIndicator(canvas, cropIndicator, scaledCrop.centerX(), scaledCrop.top);
609        }
610        if (((decoded_moving & MOVE_BOTTOM) != 0) || notMoving) {
611            drawIndicator(canvas, cropIndicator, scaledCrop.centerX(), scaledCrop.bottom);
612        }
613        if (((decoded_moving & MOVE_LEFT) != 0) || notMoving) {
614            drawIndicator(canvas, cropIndicator, scaledCrop.left, scaledCrop.centerY());
615        }
616        if (((decoded_moving & MOVE_RIGHT) != 0) || notMoving) {
617            drawIndicator(canvas, cropIndicator, scaledCrop.right, scaledCrop.centerY());
618        }
619        canvas.restore();
620    }
621
622    private int bitCycleLeft(int x, int times, int d) {
623        int mask = (1 << d) - 1;
624        int mout = x & mask;
625        times %= d;
626        int hi = mout >> (d - times);
627        int low = (mout << times) & mask;
628        int ret = x & ~mask;
629        ret |= low;
630        ret |= hi;
631        return ret;
632    }
633
634    protected int decoder(int movingEdges, float rotation) {
635        int rot = constrainedRotation(rotation);
636        switch (rot) {
637            case 90:
638                return bitCycleLeft(movingEdges, 3, 4);
639            case 180:
640                return bitCycleLeft(movingEdges, 2, 4);
641            case 270:
642                return bitCycleLeft(movingEdges, 1, 4);
643            default:
644                return movingEdges;
645        }
646    }
647}
648