chromium/ui/file_manager/image_loader/image_orientation.ts

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

export interface ImageTransformParam {
  scaleX: number;
  scaleY: number;

  /** Clockwise degrees / 90. */
  rotate90: number;
}

/**
 * Class representing image orientation.
 */
export class ImageOrientation {
  /**
   * The constructor takes 2x2 matrix value that cancels the image orientation:
   * |a, c|
   * |b, d|
   */
  constructor(
      public readonly a: number, public readonly b: number,
      public readonly c: number, public readonly d: number) {}

  /**
   * @param orientation 1-based orientation number defined by EXIF.
   */
  static fromExifOrientation(orientation: number): ImageOrientation {
    switch (~~orientation) {
      case 1:
        return new ImageOrientation(1, 0, 0, 1);
      case 2:
        return new ImageOrientation(-1, 0, 0, 1);
      case 3:
        return new ImageOrientation(-1, 0, 0, -1);
      case 4:
        return new ImageOrientation(1, 0, 0, -1);
      case 5:
        return new ImageOrientation(0, 1, 1, 0);
      case 6:
        return new ImageOrientation(0, 1, -1, 0);
      case 7:
        return new ImageOrientation(0, -1, -1, 0);
      case 8:
        return new ImageOrientation(0, -1, 1, 0);
      default:
        console.error(`Invalid orientation number: ${orientation}`);
        return new ImageOrientation(1, 0, 0, 1);
    }
  }

  /**
   * @param rotation90 Clockwise degrees / 90.
   */
  static fromClockwiseRotation(rotation90: number): ImageOrientation {
    switch (~~(rotation90 % 4)) {
      case 0:
        return new ImageOrientation(1, 0, 0, 1);
      case 1:
      case -3:
        return new ImageOrientation(0, 1, -1, 0);
      case 2:
      case -2:
        return new ImageOrientation(-1, 0, 0, -1);
      case 3:
      case -1:
        return new ImageOrientation(0, -1, 1, 0);
      default:
        console.error(`Invalid rotation number: ${rotation90}`);
        return new ImageOrientation(1, 0, 0, 1);
    }
  }

  /** Builds a transformation matrix from the image transform parameters. */
  static fromRotationAndScale(transform: ImageTransformParam):
      ImageOrientation {
    const scaleX = transform.scaleX;
    const scaleY = transform.scaleY;
    const rotate90 = transform.rotate90;

    const orientation = ImageOrientation.fromClockwiseRotation(rotate90);

    // Flip X and Y.
    // In the Files app., CSS transformations are applied like
    // "transform: rotate(90deg) scaleX(-1)".
    // Since the image is scaled based on the X,Y axes pinned to the original,
    // it is equivalent to scale first and then rotate.
    // |a c| |s_x 0 | |x|   |a*s_x c*s_y| |x|
    // |b d| | 0 s_y| |y| = |b*s_x d*s_y| |y|
    return new ImageOrientation(
        orientation.a * scaleX, orientation.b * scaleX, orientation.c * scaleY,
        orientation.d * scaleY);
  }

  /** Obtains the image size after cancelling its orientation. */
  getSizeAfterCancelling(imageWidth: number, imageHeight: number):
      {width: number, height: number} {
    const projectedX = this.a * imageWidth + this.c * imageHeight;
    const projectedY = this.b * imageWidth + this.d * imageHeight;
    return {
      width: Math.abs(projectedX),
      height: Math.abs(projectedY),
    };
  }

  /**
   * Applies the transformation that cancels the image orientation to the given
   * context.
   */
  cancelImageOrientation(
      context: CanvasRenderingContext2D, imageWidth: number,
      imageHeight: number) {
    // Calculate where to project the point of (imageWidth, imageHeight).
    const projectedX = this.a * imageWidth + this.c * imageHeight;
    const projectedY = this.b * imageWidth + this.d * imageHeight;

    // If the projected point coordinates are negative, add offset to cancel it.
    const offsetX = projectedX < 0 ? -projectedX : 0;
    const offsetY = projectedY < 0 ? -projectedY : 0;

    // Apply the transform.
    context.setTransform(this.a, this.b, this.c, this.d, offsetX, offsetY);
  }

  /**
   * Checks if the orientation represents identity transformation or not.
   */
  isIdentity(): boolean {
    return this.a === 1 && this.b === 0 && this.c === 0 && this.d === 1;
  }
}

export function isImageTransformParam(
    orientation: ImageTransformParam|ImageOrientation|
    undefined): orientation is ImageTransformParam {
  return !!orientation && 'scaleX' in orientation && 'scaleY' in orientation;
}

export function isImageOrientation(orientation: ImageTransformParam|
                                   ImageOrientation|
                                   undefined): orientation is ImageOrientation {
  return !!orientation && 'a' in orientation && 'b' in orientation &&
      'c' in orientation && 'd' in orientation;
}