Commit ae736760 authored by Nathanael Dewhurst's avatar Nathanael Dewhurst
Browse files

Issue #3316929 by ndewhurst: Improve scaling and cropping sequences.

parent 118c7537
Loading
Loading
Loading
Loading
+111 −1
Original line number Diff line number Diff line
@@ -87,6 +87,13 @@ class BrandfolderToolkit extends ImageToolkitBase {
//    }
//  }

  /**
   * A record of all image operations applied to the current image.
   *
   * @var array
   */
  protected array $operationsRecord = [];

  /**
   * Parameters to apply to the CDN URL for the source image.
   *
@@ -101,6 +108,14 @@ class BrandfolderToolkit extends ImageToolkitBase {
   */
  protected array $file_data = [];

  /**
   * A semi-immutable copy of the file data array, for reference.
   * This will only be set/reset when parseFile() is called.
   *
   * @var array
   */
  protected array $original_file_data = [];

  /**
   * Constructs a BrandfolderToolkit object.
   *
@@ -170,6 +185,97 @@ class BrandfolderToolkit extends ImageToolkitBase {
//    return $this->resource;
//  }

  /**
   * Record an image processing operation in order to maintain a record of all
   * operations, in sequence. This context can be used to convert traditional
   * image manipulation sequences into a set of CDN URL parameters that will
   * achieve the same end result.
   *
   * @param string $operation_name The name of the operation, e.g. "resize."
   * @param array $operation_arguments All arguments passed to the operation.
   */
  public function recordOperation(string $operation_name, array $operation_arguments) {
    $this->operationsRecord[] = [
      'operation' => $operation_name,
      'arguments' => $operation_arguments,
      'file_data' => $this->file_data,
      'bf_cdn_url_params' => $this->brandfolder_cdn_url_params,
    ];
  }

  /**
   * Get a record of all operations performed so far.
   *
   * @return array
   *   The operations record.
   */
  public function getOperationsRecord() {
    return $this->operationsRecord;
  }

  /**
   * Populate/overwrite the file_data array.
   *
   * @param array $file_data
   *
   * @return void
   */
  public function setFileData(array $file_data) {
    $this->file_data = $file_data;
  }

  /**
   * Set an individual item in the file_data array.
   *
   * @param string $key
   * @param $value
   *
   * @return void
   */
  public function setFileDataItem(string $key, $value) {
    $this->file_data[$key] = $value;
  }

  /**
   * Get the file_data array.
   *
   * @return array
   */
  public function getFileData() {
    return $this->file_data;
  }

  /**
   * Get an individual item from the file_data array.
   *
   * @param string $key
   *
   * @return mixed
   */
  public function getFileDataItem(string $key) {
    return isset($this->file_data[$key]) ? $this->file_data[$key] : NULL;
  }

  /**
   * Get the original_file_data array.
   *
   * @return array
   */
  public function getOriginalFileData() {
    return $this->original_file_data;
  }

  /**
   * Get an individual item from the original_file_data array.
   *
   * @param string $key
   *
   * @return mixed
   */
  public function getOriginalFileDataItem(string $key) {
    return isset($this->original_file_data[$key]) ? $this->original_file_data[$key] : NULL;
  }

  /**
   * Populate an array of Brandfolder CDN URL query parameters relevant to the
   * current image operation.
@@ -269,7 +375,11 @@ class BrandfolderToolkit extends ImageToolkitBase {
        ->condition('bf_attachment_id', $bf_attachment_id);
      if ($query->countQuery()->execute()->fetchField() > 0) {
        $result = $query->execute();
        $this->file_data = $result->fetchAssoc();
        $file_data = $result->fetchAssoc();
        if (empty($this->original_file_data)) {
          $this->original_file_data = $file_data;
        }
        $this->file_data = $file_data;

        $extension = $matches[3] ?? '';
        $this->setType($this->extensionToImageType($extension));
+44 −5
Original line number Diff line number Diff line
@@ -48,7 +48,8 @@ class Crop extends BrandfolderImageToolkitOperationBase {
      throw new \InvalidArgumentException("At least one dimension ('width' or 'height') must be provided to the image 'crop' operation");
    }

    // Preserve aspect.
    // If either the width or height argument is missing, try to calculate the
    // missing arg from the available arg, based on the image aspect ratio.
    $aspect = $this->getToolkit()->getHeight() / $this->getToolkit()->getWidth();
    $arguments['height'] = empty($arguments['height']) ? $arguments['width'] * $aspect : $arguments['height'];
    $arguments['width'] = empty($arguments['width']) ? $arguments['height'] / $aspect : $arguments['width'];
@@ -73,12 +74,50 @@ class Crop extends BrandfolderImageToolkitOperationBase {
   * {@inheritdoc}
   */
  protected function execute(array $arguments) {
    // @todo: check context and figure out if we want precrop, regular crop (crop=123,123), etc.
    // @todo: Make the "safe" crop mode configurable globally for Drupal-BF integration, at least.
    $toolkit = $this->getToolkit();
    $new_width = $crop_width = $arguments['width'];
    $new_height = $crop_height = $arguments['height'];
    $crop_x = $arguments['x'];
    $crop_y = $arguments['y'];
    // Check to see if the image was scaled prior to this crop operation.
    // If so, convert the crop values so they can be applied to the original
    // image (Brandfolder/Fastly applies crop before scale).
    $previous_operations = $toolkit->getOperationsRecord();
    $previously_scaled = FALSE;
    while ($previous_operation = array_pop($previous_operations)) {
      if ($previous_operation['operation'] == 'resize') {
        $previously_scaled = TRUE;
        break;
      }
    }
    if ($previously_scaled) {
      $original_width = $toolkit->getOriginalFileDataItem('width');
      $current_width = $toolkit->getWidth();
      $width_scale_factor = $original_width / $current_width;
      $original_height = $toolkit->getOriginalFileDataItem('height');
      $current_height = $toolkit->getheight();
      $height_scale_factor = $original_height / $current_height;
      $scale_factor = max($width_scale_factor, $height_scale_factor);
      $crop_x = round($crop_x * $scale_factor);
      $crop_y = round($crop_y * $scale_factor);
      $crop_width = round($new_width * $scale_factor);
      $crop_height = round($new_height * $scale_factor);
    }
    $params = [
      'crop' => "{$arguments['width']},{$arguments['height']},x{$arguments['x']},y{$arguments['y']},safe",
      // Note: Brandfolder's Fastly Image Optimizer implementation seems to
      // always apply the "crop" param before width and height scaling, thereby
      // rendering "crop" and "precrop" identical for our purposes. Use precrop
      // in case this changes. Revisit if adding support for other
      // transformations.
      // @todo: Make the "safe" crop mode configurable globally for Drupal-BF integration?
      'precrop' => "{$crop_width},{$crop_height},x{$crop_x},y{$crop_y},safe",
      'width' => $new_width,
      'height' => $new_height,
    ];
    $this->getToolkit()->setCdnUrlParams($params);
    $toolkit->setCdnUrlParams($params);
    $toolkit->setFileDataItem('width', $new_width);
    $toolkit->setFileDataItem('height', $new_height);
    $toolkit->recordOperation("crop", $arguments);

    return TRUE;
  }
+9 −3
Original line number Diff line number Diff line
@@ -52,11 +52,17 @@ class Resize extends BrandfolderImageToolkitOperationBase {
   * {@inheritdoc}
   */
  protected function execute(array $arguments = []) {
    $new_width = $arguments['width'];
    $new_height = $arguments['height'];
    $params = [
      'width' => $arguments['width'],
      'height' => $arguments['height'],
      'width' => $new_width,
      'height' => $new_height,
    ];
    $this->getToolkit()->setCdnUrlParams($params);
    $toolkit = $this->getToolkit();
    $toolkit->setCdnUrlParams($params);
    $toolkit->setFileDataItem('width', $new_width);
    $toolkit->setFileDataItem('height', $new_height);
    $toolkit->recordOperation("resize", $arguments);

    return TRUE;
  }
+15 −12
Original line number Diff line number Diff line
@@ -51,19 +51,18 @@ class ScaleAndCrop extends BrandfolderImageToolkitOperationBase {
      throw new \InvalidArgumentException("Invalid height ('{$arguments['height']}') specified for the image 'scale_and_crop' operation");
    }

    $actualWidth = $this->getToolkit()->getWidth();
    $actualHeight = $this->getToolkit()->getHeight();

    $widthScaleFactor = $arguments['width'] / $actualWidth;
    $heightScaleFactor = $arguments['height'] / $actualHeight;
    $scaleFactor = max($widthScaleFactor, $heightScaleFactor);

    // @todo: Talk with Brandfolder about state of Fastly Image Optimizer API support. They do not currently seem to support upscaling, for instance (a la ?width=150p). See https://developer.fastly.com/reference/io.

    // Translate from "scale, then crop" to "crop, then scale," since
    // Brandfolder/Fastly seems to always apply crop before scale regardless of
    // whether we use the `crop` or `precrop` params.
    // @todo: Test with other crop input modes (focal point, etc.).
    // @todo: Consolidate this code with that in Crop::execute().
    // @todo: Talk with Brandfolder about state of Fastly Image Optimizer API support. They do not currently seem to support upscaling, for instance (a la ?width=150p). See https://developer.fastly.com/reference/io.
    $current_width = $this->getToolkit()->getWidth();
    $current_height = $this->getToolkit()->getHeight();

    $widthScaleFactor = $arguments['width'] / $current_width;
    $heightScaleFactor = $arguments['height'] / $current_height;
    $scaleFactor = max($widthScaleFactor, $heightScaleFactor);

    $arguments['crop_x'] = (int) round($arguments['x'] / $scaleFactor);
    $arguments['crop_y'] = (int) round($arguments['y'] / $scaleFactor);
    $arguments['crop_width'] = (int) round($arguments['width'] / $scaleFactor);
@@ -76,13 +75,17 @@ class ScaleAndCrop extends BrandfolderImageToolkitOperationBase {
   * {@inheritdoc}
   */
  protected function execute(array $arguments = []) {
    // @todo: Make the "safe" crop mode configurable globally for Drupal-BF integration, at least.
    $params = [
      // @todo: Make the "safe" crop mode configurable globally for Drupal-BF integration, at least.
      'precrop' => "{$arguments['crop_width']},{$arguments['crop_height']},x{$arguments['crop_x']},y{$arguments['crop_y']},safe",
      'width' => $arguments['width'],
      'height' => $arguments['height'],
    ];
    $this->getToolkit()->setCdnUrlParams($params);
    $toolkit = $this->getToolkit();
    $toolkit->setCdnUrlParams($params);
    $toolkit->setFileDataItem('width', $arguments['crop_width']);
    $toolkit->setFileDataItem('height', $arguments['crop_height']);
    $toolkit->recordOperation("scale_and_crop", $arguments);

    return TRUE;
  }