content: /*
   * Copyright (C) 2012 CyberAgent
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package jp.co.cyberagent.android.gpuimage;
  
  import android.graphics.Point;
  import android.graphics.PointF;
  import android.opengl.GLES20;
  
  import java.io.*;
  import java.nio.ByteBuffer;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Comparator;
  
  public class GPUImageToneCurveFilter extends GPUImageFilter {
      public static final String TONE_CURVE_FRAGMENT_SHADER = "" +
              " varying highp vec2 textureCoordinate;\n" +
              " uniform sampler2D inputImageTexture;\n" +
              " uniform sampler2D toneCurveTexture;\n" +
              "\n" +
              " void main()\n" +
              " {\n" +
              "     lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);\n" +
              "     lowp float redCurveValue = texture2D(toneCurveTexture, vec2(textureColor.r, 0.0)).r;\n" +
              "     lowp float greenCurveValue = texture2D(toneCurveTexture, vec2(textureColor.g, 0.0)).g;\n" +
              "     lowp float blueCurveValue = texture2D(toneCurveTexture, vec2(textureColor.b, 0.0)).b;\n" +
              "\n" +
              "     gl_FragColor = vec4(redCurveValue, greenCurveValue, blueCurveValue, textureColor.a);\n" +
              " }";
  
      private int[] mToneCurveTexture = new int[]{OpenGlUtils.NO_TEXTURE};
      private int mToneCurveTextureUniformLocation;
  
      private PointF[] mRgbCompositeControlPoints;
      private PointF[] mRedControlPoints;
      private PointF[] mGreenControlPoints;
      private PointF[] mBlueControlPoints;
  
      private ArrayList<Float> mRgbCompositeCurve;
      private ArrayList<Float> mRedCurve;
      private ArrayList<Float> mGreenCurve;
      private ArrayList<Float> mBlueCurve;
  
  
      public GPUImageToneCurveFilter() {
          super(NO_FILTER_VERTEX_SHADER, TONE_CURVE_FRAGMENT_SHADER);
  
          PointF[] defaultCurvePoints = new PointF[]{new PointF(0.0f, 0.0f), new PointF(0.5f, 0.5f), new PointF(1.0f, 1.0f)};
          mRgbCompositeControlPoints = defaultCurvePoints;
          mRedControlPoints = defaultCurvePoints;
          mGreenControlPoints = defaultCurvePoints;
          mBlueControlPoints = defaultCurvePoints;
      }
  
      @Override
      public void onInit() {
          super.onInit();
          mToneCurveTextureUniformLocation = GLES20.glGetUniformLocation(getProgram(), "toneCurveTexture");
          GLES20.glActiveTexture(GLES20.GL_TEXTURE3);
          GLES20.glGenTextures(1, mToneCurveTexture, 0);
          GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mToneCurveTexture[0]);
          GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
          GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
          GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
          GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
      }
  
      @Override
      public void onInitialized() {
          super.onInitialized();
          setRgbCompositeControlPoints(mRgbCompositeControlPoints);
          setRedControlPoints(mRedControlPoints);
          setGreenControlPoints(mGreenControlPoints);
          setBlueControlPoints(mBlueControlPoints);
      }
  
      @Override
      protected void onDrawArraysPre() {
          if (mToneCurveTexture[0] != OpenGlUtils.NO_TEXTURE) {
              GLES20.glActiveTexture(GLES20.GL_TEXTURE3);
              GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mToneCurveTexture[0]);
              GLES20.glUniform1i(mToneCurveTextureUniformLocation, 3);
          }
      }
  
      public void setFromCurveFileInputStream(InputStream input) {
          try {
              int version = readShort(input);
              int totalCurves = readShort(input);
  
              ArrayList<PointF[]> curves = new ArrayList<PointF[]>(totalCurves);
              float pointRate = 1.0f / 255;
  
              for (int i = 0; i < totalCurves; i++) {
                  // 2 bytes, Count of points in the curve (short integer from 2...19)
                  short pointCount = readShort(input);
  
                  PointF[] points = new PointF[pointCount];
  
                  // point count * 4
                  // Curve points. Each curve point is a pair of short integers where
                  // the first number is the output value (vertical coordinate on the
                  // Curves dialog graph) and the second is the input value. All coordinates have range 0 to 255.
                  for (int j = 0; j < pointCount; j++) {
                      short y = readShort(input);
                      short x = readShort(input);
  
                      points[j] = new PointF(x * pointRate, y * pointRate);
                  }
  
                  curves.add(points);
              }
              input.close();
  
              mRgbCompositeControlPoints = curves.get(0);
              mRedControlPoints = curves.get(1);
              mGreenControlPoints = curves.get(2);
              mBlueControlPoints = curves.get(3);
          } catch (IOException e) {
              e.printStackTrace();
          }
      }
  
      private short readShort(InputStream input) throws IOException {
          return (short) (input.read() << 8 | input.read());
      }
  
      public void setRgbCompositeControlPoints(PointF[] points) {
          mRgbCompositeControlPoints = points;
          mRgbCompositeCurve = createSplineCurve(mRgbCompositeControlPoints);
          updateToneCurveTexture();
      }
  
      public void setRedControlPoints(PointF[] points) {
          mRedControlPoints = points;
          mRedCurve = createSplineCurve(mRedControlPoints);
          updateToneCurveTexture();
      }
  
      public void setGreenControlPoints(PointF[] points) {
          mGreenControlPoints = points;
          mGreenCurve = createSplineCurve(mGreenControlPoints);
          updateToneCurveTexture();
      }
  
      public void setBlueControlPoints(PointF[] points) {
          mBlueControlPoints = points;
          mBlueCurve = createSplineCurve(mBlueControlPoints);
          updateToneCurveTexture();
      }
  
      private void updateToneCurveTexture() {
          runOnDraw(new Runnable() {
              @Override
              public void run() {
                  GLES20.glActiveTexture(GLES20.GL_TEXTURE3);
                  GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mToneCurveTexture[0]);
  
                  if ((mRedCurve.size() >= 256) && (mGreenCurve.size() >= 256) && (mBlueCurve.size() >= 256) && (mRgbCompositeCurve.size() >= 256)) {
                      byte[] toneCurveByteArray = new byte[256 * 4];
                      for (int currentCurveIndex = 0; currentCurveIndex < 256; currentCurveIndex++) {
                          // BGRA for upload to texture
                          toneCurveByteArray[currentCurveIndex * 4 + 2] = (byte) ((int) Math.min(Math.max(currentCurveIndex + mBlueCurve.get(currentCurveIndex) + mRgbCompositeCurve.get(currentCurveIndex), 0), 255) & 0xff);
                          toneCurveByteArray[currentCurveIndex * 4 + 1] = (byte) ((int) Math.min(Math.max(currentCurveIndex + mGreenCurve.get(currentCurveIndex) + mRgbCompositeCurve.get(currentCurveIndex), 0), 255) & 0xff);
                          toneCurveByteArray[currentCurveIndex * 4] = (byte) ((int) Math.min(Math.max(currentCurveIndex + mRedCurve.get(currentCurveIndex) + mRgbCompositeCurve.get(currentCurveIndex), 0), 255) & 0xff);
                          toneCurveByteArray[currentCurveIndex * 4 + 3] = (byte) (255 & 0xff);
                      }
  
                      GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, 256 /*width*/, 1 /*height*/, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, ByteBuffer.wrap(toneCurveByteArray));
                  }
  //        Buffer pixels!
  //        GLES20.glTexImage2D(int target,
  //            int level,
  //            int internalformat,
  //            int width,
  //            int height,
  //            int border,
  //            int format,
  //            int type,
  //            java.nio.Buffer pixels);
              }
          });
      }
  
      private ArrayList<Float> createSplineCurve(PointF[] points) {
          if (points == null || points.length <= 0) {
              return null;
          }
  
          // Sort the array
          PointF[] pointsSorted = points.clone();
          Arrays.sort(pointsSorted, new Comparator<PointF>() {
              @Override
              public int compare(PointF point1, PointF point2) {
                  if (point1.x < point2.x) {
                      return -1;
                  } else if (point1.x > point2.x) {
                      return 1;
                  } else {
                      return 0;
                  }
              }
          });
  
          // Convert from (0, 1) to (0, 255).
          Point[] convertedPoints = new Point[pointsSorted.length];
          for (int i = 0; i < points.length; i++) {
              PointF point = pointsSorted[i];
              convertedPoints[i] = new Point((int) (point.x * 255), (int) (point.y * 255));
          }
  
          ArrayList<Point> splinePoints = createSplineCurve2(convertedPoints);
  
          // If we have a first point like (0.3, 0) we'll be missing some points at the beginning
          // that should be 0.
          Point firstSplinePoint = splinePoints.get(0);
          if (firstSplinePoint.x > 0) {
              for (int i = firstSplinePoint.x; i >= 0; i--) {
                  splinePoints.add(0, new Point(i, 0));
              }
          }
  
          // Insert points similarly at the end, if necessary.
          Point lastSplinePoint = splinePoints.get(splinePoints.size() - 1);
          if (lastSplinePoint.x < 255) {
              for (int i = lastSplinePoint.x + 1; i <= 255; i++) {
                  splinePoints.add(new Point(i, 255));
              }
          }
  
          // Prepare the spline points.
          ArrayList<Float> preparedSplinePoints = new ArrayList<Float>(splinePoints.size());
          for (Point newPoint : splinePoints) {
              Point origPoint = new Point(newPoint.x, newPoint.x);
  
              float distance = (float) Math.sqrt(Math.pow((origPoint.x - newPoint.x), 2.0) + Math.pow((origPoint.y - newPoint.y), 2.0));
  
              if (origPoint.y > newPoint.y) {
                  distance = -distance;
              }
  
              preparedSplinePoints.add(distance);
          }
  
          return preparedSplinePoints;
      }
  
      private ArrayList<Point> createSplineCurve2(Point[] points) {
          ArrayList<Double> sdA = createSecondDerivative(points);
  
          // Is [points count] equal to [sdA count]?
  //    int n = [points count];
          int n = sdA.size();
          if (n < 1) {
              return null;
          }
          double sd[] = new double[n];
  
          // From NSMutableArray to sd[n];
          for (int i = 0; i < n; i++) {
              sd[i] = sdA.get(i);
          }
  
  
          ArrayList<Point> output = new ArrayList<Point>(n + 1);
  
          for (int i = 0; i < n - 1; i++) {
              Point cur = points[i];
              Point next = points[i + 1];
  
              for (int x = cur.x; x < next.x; x++) {
                  double t = (double) (x - cur.x) / (next.x - cur.x);
  
                  double a = 1 - t;
                  double b = t;
                  double h = next.x - cur.x;
  
                  double y = a * cur.y + b * next.y + (h * h / 6) * ((a * a * a - a) * sd[i] + (b * b * b - b) * sd[i + 1]);
  
                  if (y > 255.0) {
                      y = 255.0;
                  } else if (y < 0.0) {
                      y = 0.0;
                  }
  
                  output.add(new Point(x, (int) Math.round(y)));
              }
          }
  
          // If the last point is (255, 255) it doesn't get added.
          if (output.size() == 255) {
              output.add(points[points.length - 1]);
          }
          return output;
      }
  
      private ArrayList<Double> createSecondDerivative(Point[] points) {
          int n = points.length;
          if (n <= 1) {
              return null;
          }
  
          double matrix[][] = new double[n][3];
          double result[] = new double[n];
          matrix[0][1] = 1;
          // What about matrix[0][1] and matrix[0][0]? Assuming 0 for now (Brad L.)
          matrix[0][0] = 0;
          matrix[0][2] = 0;
  
          for (int i = 1; i < n - 1; i++) {
              Point P1 = points[i - 1];
              Point P2 = points[i];
              Point P3 = points[i + 1];
  
              matrix[i][0] = (double) (P2.x - P1.x) / 6;
              matrix[i][1] = (double) (P3.x - P1.x) / 3;
              matrix[i][2] = (double) (P3.x - P2.x) / 6;
              result[i] = (double) (P3.y - P2.y) / (P3.x - P2.x) - (double) (P2.y - P1.y) / (P2.x - P1.x);
          }
  
          // What about result[0] and result[n-1]? Assuming 0 for now (Brad L.)
          result[0] = 0;
          result[n - 1] = 0;
  
          matrix[n - 1][1] = 1;
          // What about matrix[n-1][0] and matrix[n-1][2]? For now, assuming they are 0 (Brad L.)
          matrix[n - 1][0] = 0;
          matrix[n - 1][2] = 0;
  
          // solving pass1 (up->down)
          for (int i = 1; i < n; i++) {
              double k = matrix[i][0] / matrix[i - 1][1];
              matrix[i][1] -= k * matrix[i - 1][2];
              matrix[i][0] = 0;
              result[i] -= k * result[i - 1];
          }
          // solving pass2 (down->up)
          for (int i = n - 2; i >= 0; i--) {
              double k = matrix[i][2] / matrix[i + 1][1];
              matrix[i][1] -= k * matrix[i + 1][0];
              matrix[i][2] = 0;
              result[i] -= k * result[i + 1];
          }
  
          ArrayList<Double> output = new ArrayList<Double>(n);
          for (int i = 0; i < n; i++) output.add(result[i] / matrix[i][1]);
  
          return output;
      }
  }
  