Crop.java

/*
 * Copyright 2022 The Android Open Source Project
 *
 * 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 androidx.media3.effect;

import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkStateNotNull;

import android.graphics.Matrix;
import android.util.Pair;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.UnstableApi;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;

/**
 * Specifies a crop to apply in the vertex shader.
 *
 * <p>The background color of the output frame will be black, with alpha = 0 if applicable.
 */
@UnstableApi
public final class Crop implements MatrixTransformation {

  private final float left;
  private final float right;
  private final float bottom;
  private final float top;

  private @MonotonicNonNull Matrix transformationMatrix;

  /**
   * Crops a smaller (or larger) frame, per normalized device coordinates (NDC), where the input
   * frame corresponds to the square ranging from -1 to 1 on the x and y axes.
   *
   * <p>{@code left} and {@code bottom} default to -1, and {@code right} and {@code top} default to
   * 1, which corresponds to not applying any crop. To crop to a smaller subset of the input frame,
   * use values between -1 and 1. To crop to a larger frame, use values below -1 and above 1.
   *
   * @param left The left edge of the output frame, in NDC. Must be less than {@code right}.
   * @param right The right edge of the output frame, in NDC. Must be greater than {@code left}.
   * @param bottom The bottom edge of the output frame, in NDC. Must be less than {@code top}.
   * @param top The top edge of the output frame, in NDC. Must be greater than {@code bottom}.
   */
  public Crop(float left, float right, float bottom, float top) {
    checkArgument(
        right > left, "right value " + right + " should be greater than left value " + left);
    checkArgument(
        top > bottom, "top value " + top + " should be greater than bottom value " + bottom);
    this.left = left;
    this.right = right;
    this.bottom = bottom;
    this.top = top;

    transformationMatrix = new Matrix();
  }

  @Override
  public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) {
    checkArgument(inputWidth > 0, "inputWidth must be positive");
    checkArgument(inputHeight > 0, "inputHeight must be positive");

    transformationMatrix = new Matrix();
    if (left == -1f && right == 1f && bottom == -1f && top == 1f) {
      // No crop needed.
      return Pair.create(inputWidth, inputHeight);
    }

    float scaleX = (right - left) / GlUtil.LENGTH_NDC;
    float scaleY = (top - bottom) / GlUtil.LENGTH_NDC;
    float centerX = (left + right) / 2;
    float centerY = (bottom + top) / 2;

    transformationMatrix.postTranslate(-centerX, -centerY);
    transformationMatrix.postScale(1f / scaleX, 1f / scaleY);

    int outputWidth = Math.round(inputWidth * scaleX);
    int outputHeight = Math.round(inputHeight * scaleY);
    return Pair.create(outputWidth, outputHeight);
  }

  @Override
  public Matrix getMatrix(long presentationTimeUs) {
    return checkStateNotNull(transformationMatrix, "configure must be called first");
  }
}