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.sqrt((px - x) * (px - x) + (py - y) * (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.sqrt((px - x) * (px - x) + (py - y)
231                    * (py - y));
232            if (currentDelta < delta) {
233                delta = currentDelta;
234                pick = i;
235            }
236        }
237
238        if (!mDidAddPoint && (delta * getWidth() > 100)
239                && (spline.getNbPoints() < 10)) {
240            return -1;
241        }
242
243        return pick;
244    }
245
246    private String getFilterName() {
247        return "Curves";
248    }
249
250    @Override
251    public synchronized boolean onTouchEvent(MotionEvent e) {
252        if (e.getPointerCount() != 1) {
253            return true;
254        }
255
256        if (didFinishScalingOperation()) {
257            return true;
258        }
259
260        float margin = Spline.curveHandleSize() / 2;
261        float posX = e.getX();
262        if (posX < margin) {
263            posX = margin;
264        }
265        float posY = e.getY();
266        if (posY < margin) {
267            posY = margin;
268        }
269        if (posX > getWidth() - margin) {
270            posX = getWidth() - margin;
271        }
272        if (posY > getHeight() - margin) {
273            posY = getHeight() - margin;
274        }
275        posX = (posX - margin) / (getWidth() - 2 * margin);
276        posY = (posY - margin) / (getHeight() - 2 * margin);
277
278        if (e.getActionMasked() == MotionEvent.ACTION_UP) {
279            mCurrentControlPoint = null;
280            mCurrentPick = -1;
281            updateCachedImage();
282            mDidAddPoint = false;
283            if (mDidDelete) {
284                mDidDelete = false;
285            }
286            mDoingTouchMove = false;
287            return true;
288        }
289
290        if (mDidDelete) {
291            return true;
292        }
293
294        if (curves() == null) {
295            return true;
296        }
297
298        if (e.getActionMasked() == MotionEvent.ACTION_MOVE) {
299            mDoingTouchMove = true;
300            Spline spline = getSpline(mCurrentCurveIndex);
301            int pick = mCurrentPick;
302            if (mCurrentControlPoint == null) {
303                pick = pickControlPoint(posX, posY);
304                if (pick == -1) {
305                    mCurrentControlPoint = new ControlPoint(posX, posY);
306                    pick = spline.addPoint(mCurrentControlPoint);
307                    mDidAddPoint = true;
308                } else {
309                    mCurrentControlPoint = spline.getPoint(pick);
310                }
311                mCurrentPick = pick;
312            }
313
314            if (spline.isPointContained(posX, pick)) {
315                spline.movePoint(pick, posX, posY);
316            } else if (pick != -1 && spline.getNbPoints() > 2) {
317                spline.deletePoint(pick);
318                mDidDelete = true;
319            }
320            updateCachedImage();
321            invalidate();
322        }
323        return true;
324    }
325
326    public synchronized void updateCachedImage() {
327        if (getImagePreset() != null) {
328            resetImageCaches(this);
329            if (mEditorCurves != null) {
330                mEditorCurves.commitLocalRepresentation();
331            }
332            invalidate();
333        }
334    }
335
336    class ComputeHistogramTask extends AsyncTask<Bitmap, Void, int[]> {
337        @Override
338        protected int[] doInBackground(Bitmap... params) {
339            int[] histo = new int[256 * 3];
340            Bitmap bitmap = params[0];
341            int w = bitmap.getWidth();
342            int h = bitmap.getHeight();
343            int[] pixels = new int[w * h];
344            bitmap.getPixels(pixels, 0, w, 0, 0, w, h);
345            for (int i = 0; i < w; i++) {
346                for (int j = 0; j < h; j++) {
347                    int index = j * w + i;
348                    int r = Color.red(pixels[index]);
349                    int g = Color.green(pixels[index]);
350                    int b = Color.blue(pixels[index]);
351                    histo[r]++;
352                    histo[256 + g]++;
353                    histo[512 + b]++;
354                }
355            }
356            return histo;
357        }
358
359        @Override
360        protected void onPostExecute(int[] result) {
361            System.arraycopy(result, 0, redHistogram, 0, 256);
362            System.arraycopy(result, 256, greenHistogram, 0, 256);
363            System.arraycopy(result, 512, blueHistogram, 0, 256);
364            invalidate();
365        }
366    }
367
368    private void drawHistogram(Canvas canvas, int[] histogram, int color, PorterDuff.Mode mode) {
369        int max = 0;
370        for (int i = 0; i < histogram.length; i++) {
371            if (histogram[i] > max) {
372                max = histogram[i];
373            }
374        }
375        float w = getWidth() - Spline.curveHandleSize();
376        float h = getHeight() - Spline.curveHandleSize() / 2.0f;
377        float dx = Spline.curveHandleSize() / 2.0f;
378        float wl = w / histogram.length;
379        float wh = (0.3f * h) / max;
380        Paint paint = new Paint();
381        paint.setARGB(100, 255, 255, 255);
382        paint.setStrokeWidth((int) Math.ceil(wl));
383
384        Paint paint2 = new Paint();
385        paint2.setColor(color);
386        paint2.setStrokeWidth(6);
387        paint2.setXfermode(new PorterDuffXfermode(mode));
388        gHistoPath.reset();
389        gHistoPath.moveTo(dx, h);
390        boolean firstPointEncountered = false;
391        float prev = 0;
392        float last = 0;
393        for (int i = 0; i < histogram.length; i++) {
394            float x = i * wl + dx;
395            float l = histogram[i] * wh;
396            if (l != 0) {
397                float v = h - (l + prev) / 2.0f;
398                if (!firstPointEncountered) {
399                    gHistoPath.lineTo(x, h);
400                    firstPointEncountered = true;
401                }
402                gHistoPath.lineTo(x, v);
403                prev = l;
404                last = x;
405            }
406        }
407        gHistoPath.lineTo(last, h);
408        gHistoPath.lineTo(w, h);
409        gHistoPath.close();
410        canvas.drawPath(gHistoPath, paint2);
411        paint2.setStrokeWidth(2);
412        paint2.setStyle(Paint.Style.STROKE);
413        paint2.setARGB(255, 200, 200, 200);
414        canvas.drawPath(gHistoPath, paint2);
415    }
416
417    public void setChannel(int itemId) {
418        switch (itemId) {
419            case R.id.curve_menu_rgb: {
420                mCurrentCurveIndex = Spline.RGB;
421                break;
422            }
423            case R.id.curve_menu_red: {
424                mCurrentCurveIndex = Spline.RED;
425                break;
426            }
427            case R.id.curve_menu_green: {
428                mCurrentCurveIndex = Spline.GREEN;
429                break;
430            }
431            case R.id.curve_menu_blue: {
432                mCurrentCurveIndex = Spline.BLUE;
433                break;
434            }
435        }
436        mEditorCurves.commitLocalRepresentation();
437        invalidate();
438    }
439
440    public void setEditor(EditorCurves editorCurves) {
441        mEditorCurves = editorCurves;
442    }
443
444    public void setFilterDrawRepresentation(FilterCurvesRepresentation drawRep) {
445        mFilterCurvesRepresentation = drawRep;
446    }
447}
448