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.graphics.Bitmap;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Paint;
24import android.graphics.Path;
25import android.graphics.PorterDuff;
26import android.graphics.PorterDuffXfermode;
27import android.os.AsyncTask;
28import android.util.AttributeSet;
29import android.view.MenuItem;
30import android.view.MotionEvent;
31import android.view.View;
32import android.widget.Button;
33import android.widget.LinearLayout;
34import android.widget.PopupMenu;
35
36import com.android.gallery3d.R;
37import com.android.gallery3d.filtershow.FilterShowActivity;
38import com.android.gallery3d.filtershow.editors.Editor;
39import com.android.gallery3d.filtershow.editors.EditorCurves;
40import com.android.gallery3d.filtershow.filters.FilterCurvesRepresentation;
41import com.android.gallery3d.filtershow.filters.FiltersManager;
42import com.android.gallery3d.filtershow.filters.ImageFilterCurves;
43import com.android.gallery3d.filtershow.pipeline.ImagePreset;
44
45import java.util.HashMap;
46
47public class ImageCurves extends ImageShow {
48
49    private static final String LOGTAG = "ImageCurves";
50    Paint gPaint = new Paint();
51    Path gPathSpline = new Path();
52    HashMap<Integer, String> mIdStrLut;
53
54    private int mCurrentCurveIndex = Spline.RGB;
55    private boolean mDidAddPoint = false;
56    private boolean mDidDelete = false;
57    private ControlPoint mCurrentControlPoint = null;
58    private int mCurrentPick = -1;
59    private ImagePreset mLastPreset = null;
60    int[] redHistogram = new int[256];
61    int[] greenHistogram = new int[256];
62    int[] blueHistogram = new int[256];
63    Path gHistoPath = new Path();
64
65    boolean mDoingTouchMove = false;
66    private EditorCurves mEditorCurves;
67    private FilterCurvesRepresentation mFilterCurvesRepresentation;
68
69    public ImageCurves(Context context) {
70        super(context);
71        setLayerType(LAYER_TYPE_SOFTWARE, gPaint);
72        resetCurve();
73    }
74
75    public ImageCurves(Context context, AttributeSet attrs) {
76        super(context, attrs);
77        setLayerType(LAYER_TYPE_SOFTWARE, gPaint);
78        resetCurve();
79    }
80
81    @Override
82    protected boolean enableComparison() {
83        return false;
84    }
85
86    @Override
87    public boolean useUtilityPanel() {
88        return true;
89    }
90
91    private void showPopupMenu(LinearLayout accessoryViewList) {
92        final Button button = (Button) accessoryViewList.findViewById(
93                R.id.applyEffect);
94        if (button == null) {
95            return;
96        }
97        if (mIdStrLut == null){
98            mIdStrLut = new HashMap<Integer, String>();
99            mIdStrLut.put(R.id.curve_menu_rgb,
100                    getContext().getString(R.string.curves_channel_rgb));
101            mIdStrLut.put(R.id.curve_menu_red,
102                    getContext().getString(R.string.curves_channel_red));
103            mIdStrLut.put(R.id.curve_menu_green,
104                    getContext().getString(R.string.curves_channel_green));
105            mIdStrLut.put(R.id.curve_menu_blue,
106                    getContext().getString(R.string.curves_channel_blue));
107        }
108        PopupMenu popupMenu = new PopupMenu(getActivity(), button);
109        popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_curves, popupMenu.getMenu());
110        popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
111            @Override
112            public boolean onMenuItemClick(MenuItem item) {
113                setChannel(item.getItemId());
114                button.setText(mIdStrLut.get(item.getItemId()));
115                return true;
116            }
117        });
118        Editor.hackFixStrings(popupMenu.getMenu());
119        popupMenu.show();
120        ((FilterShowActivity)getContext()).onShowMenu(popupMenu);
121    }
122
123    @Override
124    public void openUtilityPanel(final LinearLayout accessoryViewList) {
125        Context context = accessoryViewList.getContext();
126        Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect);
127        view.setText(context.getString(R.string.curves_channel_rgb));
128        view.setVisibility(View.VISIBLE);
129
130        view.setOnClickListener(new OnClickListener() {
131                @Override
132            public void onClick(View arg0) {
133                showPopupMenu(accessoryViewList);
134            }
135        });
136
137        if (view != null) {
138            view.setVisibility(View.VISIBLE);
139        }
140    }
141
142    public void nextChannel() {
143        mCurrentCurveIndex = ((mCurrentCurveIndex + 1) % 4);
144        invalidate();
145    }
146
147    private ImageFilterCurves curves() {
148        String filterName = getFilterName();
149        ImagePreset p = getImagePreset();
150        if (p != null) {
151            return (ImageFilterCurves) FiltersManager.getManager().getFilter(ImageFilterCurves.class);
152        }
153        return null;
154    }
155
156    private Spline getSpline(int index) {
157        return mFilterCurvesRepresentation.getSpline(index);
158    }
159
160    @Override
161    public void resetParameter() {
162        super.resetParameter();
163        resetCurve();
164        mLastPreset = null;
165        invalidate();
166    }
167
168    public void resetCurve() {
169        if (mFilterCurvesRepresentation != null) {
170            mFilterCurvesRepresentation.reset();
171            updateCachedImage();
172        }
173    }
174
175    @Override
176    public void onDraw(Canvas canvas) {
177        super.onDraw(canvas);
178        if (mFilterCurvesRepresentation == null) {
179            return;
180        }
181
182        gPaint.setAntiAlias(true);
183
184        if (getImagePreset() != mLastPreset && getFilteredImage() != null) {
185            new ComputeHistogramTask().execute(getFilteredImage());
186            mLastPreset = getImagePreset();
187        }
188
189        if (curves() == null) {
190            return;
191        }
192
193        if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.RED) {
194            drawHistogram(canvas, redHistogram, Color.RED, PorterDuff.Mode.SCREEN);
195        }
196        if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.GREEN) {
197            drawHistogram(canvas, greenHistogram, Color.GREEN, PorterDuff.Mode.SCREEN);
198        }
199        if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.BLUE) {
200            drawHistogram(canvas, blueHistogram, Color.BLUE, PorterDuff.Mode.SCREEN);
201        }
202        // We only display the other channels curves when showing the RGB curve
203        if (mCurrentCurveIndex == Spline.RGB) {
204            for (int i = 0; i < 4; i++) {
205                Spline spline = getSpline(i);
206                if (i != mCurrentCurveIndex && !spline.isOriginal()) {
207                    // And we only display a curve if it has more than two
208                    // points
209                    spline.draw(canvas, Spline.colorForCurve(i), getWidth(),
210                            getHeight(), false, mDoingTouchMove);
211                }
212            }
213        }
214        // ...but we always display the current curve.
215        getSpline(mCurrentCurveIndex)
216                .draw(canvas, Spline.colorForCurve(mCurrentCurveIndex), getWidth(), getHeight(),
217                        true, mDoingTouchMove);
218
219    }
220
221    private int pickControlPoint(float x, float y) {
222        int pick = 0;
223        Spline spline = getSpline(mCurrentCurveIndex);
224        float px = spline.getPoint(0).x;
225        float py = spline.getPoint(0).y;
226        double delta = Math.hypot(px - x, py - y);
227        for (int i = 1; i < spline.getNbPoints(); i++) {
228            px = spline.getPoint(i).x;
229            py = spline.getPoint(i).y;
230            double currentDelta = Math.hypot(px - x, py - y);
231            if (currentDelta < delta) {
232                delta = currentDelta;
233                pick = i;
234            }
235        }
236
237        if (!mDidAddPoint && (delta * getWidth() > 100)
238                && (spline.getNbPoints() < 10)) {
239            return -1;
240        }
241
242        return pick;
243    }
244
245    private String getFilterName() {
246        return "Curves";
247    }
248
249    @Override
250    public synchronized boolean onTouchEvent(MotionEvent e) {
251        if (e.getPointerCount() != 1) {
252            return true;
253        }
254
255        if (didFinishScalingOperation()) {
256            return true;
257        }
258
259        float margin = Spline.curveHandleSize() / 2;
260        float posX = e.getX();
261        if (posX < margin) {
262            posX = margin;
263        }
264        float posY = e.getY();
265        if (posY < margin) {
266            posY = margin;
267        }
268        if (posX > getWidth() - margin) {
269            posX = getWidth() - margin;
270        }
271        if (posY > getHeight() - margin) {
272            posY = getHeight() - margin;
273        }
274        posX = (posX - margin) / (getWidth() - 2 * margin);
275        posY = (posY - margin) / (getHeight() - 2 * margin);
276
277        if (e.getActionMasked() == MotionEvent.ACTION_UP) {
278            mCurrentControlPoint = null;
279            mCurrentPick = -1;
280            updateCachedImage();
281            mDidAddPoint = false;
282            if (mDidDelete) {
283                mDidDelete = false;
284            }
285            mDoingTouchMove = false;
286            return true;
287        }
288
289        if (mDidDelete) {
290            return true;
291        }
292
293        if (curves() == null) {
294            return true;
295        }
296
297        if (e.getActionMasked() == MotionEvent.ACTION_MOVE) {
298            mDoingTouchMove = true;
299            Spline spline = getSpline(mCurrentCurveIndex);
300            int pick = mCurrentPick;
301            if (mCurrentControlPoint == null) {
302                pick = pickControlPoint(posX, posY);
303                if (pick == -1) {
304                    mCurrentControlPoint = new ControlPoint(posX, posY);
305                    pick = spline.addPoint(mCurrentControlPoint);
306                    mDidAddPoint = true;
307                } else {
308                    mCurrentControlPoint = spline.getPoint(pick);
309                }
310                mCurrentPick = pick;
311            }
312
313            if (spline.isPointContained(posX, pick)) {
314                spline.movePoint(pick, posX, posY);
315            } else if (pick != -1 && spline.getNbPoints() > 2) {
316                spline.deletePoint(pick);
317                mDidDelete = true;
318            }
319            updateCachedImage();
320            invalidate();
321        }
322        return true;
323    }
324
325    public synchronized void updateCachedImage() {
326        if (getImagePreset() != null) {
327            resetImageCaches(this);
328            if (mEditorCurves != null) {
329                mEditorCurves.commitLocalRepresentation();
330            }
331            invalidate();
332        }
333    }
334
335    class ComputeHistogramTask extends AsyncTask<Bitmap, Void, int[]> {
336        @Override
337        protected int[] doInBackground(Bitmap... params) {
338            int[] histo = new int[256 * 3];
339            Bitmap bitmap = params[0];
340            int w = bitmap.getWidth();
341            int h = bitmap.getHeight();
342            int[] pixels = new int[w * h];
343            bitmap.getPixels(pixels, 0, w, 0, 0, w, h);
344            for (int i = 0; i < w; i++) {
345                for (int j = 0; j < h; j++) {
346                    int index = j * w + i;
347                    int r = Color.red(pixels[index]);
348                    int g = Color.green(pixels[index]);
349                    int b = Color.blue(pixels[index]);
350                    histo[r]++;
351                    histo[256 + g]++;
352                    histo[512 + b]++;
353                }
354            }
355            return histo;
356        }
357
358        @Override
359        protected void onPostExecute(int[] result) {
360            System.arraycopy(result, 0, redHistogram, 0, 256);
361            System.arraycopy(result, 256, greenHistogram, 0, 256);
362            System.arraycopy(result, 512, blueHistogram, 0, 256);
363            invalidate();
364        }
365    }
366
367    private void drawHistogram(Canvas canvas, int[] histogram, int color, PorterDuff.Mode mode) {
368        int max = 0;
369        for (int i = 0; i < histogram.length; i++) {
370            if (histogram[i] > max) {
371                max = histogram[i];
372            }
373        }
374        float w = getWidth() - Spline.curveHandleSize();
375        float h = getHeight() - Spline.curveHandleSize() / 2.0f;
376        float dx = Spline.curveHandleSize() / 2.0f;
377        float wl = w / histogram.length;
378        float wh = (0.3f * h) / max;
379        Paint paint = new Paint();
380        paint.setARGB(100, 255, 255, 255);
381        paint.setStrokeWidth((int) Math.ceil(wl));
382
383        Paint paint2 = new Paint();
384        paint2.setColor(color);
385        paint2.setStrokeWidth(6);
386        paint2.setXfermode(new PorterDuffXfermode(mode));
387        gHistoPath.reset();
388        gHistoPath.moveTo(dx, h);
389        boolean firstPointEncountered = false;
390        float prev = 0;
391        float last = 0;
392        for (int i = 0; i < histogram.length; i++) {
393            float x = i * wl + dx;
394            float l = histogram[i] * wh;
395            if (l != 0) {
396                float v = h - (l + prev) / 2.0f;
397                if (!firstPointEncountered) {
398                    gHistoPath.lineTo(x, h);
399                    firstPointEncountered = true;
400                }
401                gHistoPath.lineTo(x, v);
402                prev = l;
403                last = x;
404            }
405        }
406        gHistoPath.lineTo(last, h);
407        gHistoPath.lineTo(w, h);
408        gHistoPath.close();
409        canvas.drawPath(gHistoPath, paint2);
410        paint2.setStrokeWidth(2);
411        paint2.setStyle(Paint.Style.STROKE);
412        paint2.setARGB(255, 200, 200, 200);
413        canvas.drawPath(gHistoPath, paint2);
414    }
415
416    public void setChannel(int itemId) {
417        switch (itemId) {
418            case R.id.curve_menu_rgb: {
419                mCurrentCurveIndex = Spline.RGB;
420                break;
421            }
422            case R.id.curve_menu_red: {
423                mCurrentCurveIndex = Spline.RED;
424                break;
425            }
426            case R.id.curve_menu_green: {
427                mCurrentCurveIndex = Spline.GREEN;
428                break;
429            }
430            case R.id.curve_menu_blue: {
431                mCurrentCurveIndex = Spline.BLUE;
432                break;
433            }
434        }
435        mEditorCurves.commitLocalRepresentation();
436        invalidate();
437    }
438
439    public void setEditor(EditorCurves editorCurves) {
440        mEditorCurves = editorCurves;
441    }
442
443    public void setFilterDrawRepresentation(FilterCurvesRepresentation drawRep) {
444        mFilterCurvesRepresentation = drawRep;
445    }
446}
447