Commit 22d8763e authored by vaplas's avatar vaplas Committed by Erwin Derksen
Browse files

[#2957577] by vaplas, fietserwin: Improve the performance of get image size.

parent 7c56ca58
Loading
Loading
Loading
Loading
+1 −0
Changes for CHANGELOG.txt: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ Image Field Repair

8.x-1.x (2018-04-??)
--------------------
- [#2957577] by vaplas, fietserwin: Improve the performance of get image size.
- [#2958147] by vaplas, fietserwin: More accentuated report.
- Typo, missing @param, PhpStorm inspection.
- [#2931750] by vaplas: TypeError: Argument 1 passed to
+5 −3
Changes for image_field_repair.inc: 5 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\image\Plugin\Field\FieldType\ImageItem;
use Drupal\image_field_repair\ImageSizer;

/**
 * Callback that processes a partial batch run to repair image fields.
@@ -163,11 +164,11 @@ function image_field_repair_process_table_range(array $table_info, $rangeStart,
    // Get actual image dimensions and compare with stored image dimensions.
    // Do NOT use the image toolkit: we don't need it and we don't want to
    // depend on it or its current list of supported image types here.
    $image_size = @getimagesize($row['uri']);
    $image_size = ImageSizer::getSize($row['uri']);
    if (is_array($image_size)) {
      if ($row[$table_info['width_field']] != $image_size[0] || $row[$table_info['height_field']] != $image_size[1]) {
        // Not equal: repair current record (identified by its primary key
        // values).
        // Dimensions are not equal: repair current record (identified by its
        // primary key values).
        $update_query = $database->update($table_name)
          ->fields(array(
            $table_info['width_field'] => $image_size[0],
@@ -262,6 +263,7 @@ function image_field_repair_get_field_table_infos(FieldStorageConfigInterface $f
  ];
}

/** @noinspection PhpDocMissingThrowsInspection */
/**
 * Returns the table info for 1 field table.
 *

src/FastImage.php

0 → 100644
+209 −0
Changes for src/FastImage.php: 209 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Drupal\image_field_repair;

/**
 * FastImage - Because sometimes you just want the size!
 * Based on the Ruby Implementation by Steven Sykes (https://github.com/sdsykes/fastimage)
 *
 * Copyright (c) 2012 Tom Moor, http://tommoor.com
 */
class FastImage {
  private $strpos = 0;
  private $str;
  private $type;
  private $handle;

  public function __construct($uri = NULL) {
    if ($uri) {
      $this->load($uri);
    }
  }

  public function load($uri) {
    if ($this->handle) {
      $this->close();
    }

    $this->handle = fopen($uri, 'r');
  }

  public function close() {
    if ($this->handle) {
      fclose($this->handle);
      $this->handle = NULL;
      $this->type = NULL;
      $this->str = NULL;
    }
  }

  /**
   * @return array|bool
   */
  public function getSize() {
    $this->strpos = 0;
    if ($this->getType()) {
      return array_values($this->parseSize());
    }

    return FALSE;
  }

  public function getType() {
    $this->strpos = 0;

    if (!$this->type) {
      switch ($this->getChars(2)) {
        case "BM":
          return $this->type = 'bmp';
        case "GI":
          return $this->type = 'gif';
        case chr(0xFF) . chr(0xd8):
          return $this->type = 'jpeg';
        case chr(0x89) . 'P':
          return $this->type = 'png';
        default:
          return FALSE;
      }
    }

    return $this->type;
  }

  private function parseSize() {
    $this->strpos = 0;

    switch ($this->type) {
      case 'png':
        return $this->parseSizeForPNG();
      case 'gif':
        return $this->parseSizeForGIF();
      case 'bmp':
        return $this->parseSizeForBMP();
      case 'jpeg':
        return $this->parseSizeForJPEG();
    }

    return NULL;
  }

  private function parseSizeForPNG() {
    $chars = $this->getChars(25);

    return unpack("N*", substr($chars, 16, 8));
  }

  private function parseSizeForGIF() {
    $chars = $this->getChars(11);

    return unpack("S*", substr($chars, 6, 4));
  }

  private function parseSizeForBMP() {
    $chars = $this->getChars(29);
    $chars = substr($chars, 14, 14);
    $type = unpack('C', $chars);

    return (reset($type) == 40) ? unpack('L*', substr($chars, 4)) : unpack('L*',
      substr($chars, 4, 8));
  }

  private function parseSizeForJPEG() {
    $state = NULL;

    while (TRUE) {
      switch ($state) {
        default:
          $this->getChars(2);
          $state = 'started';
          break;

        case 'started':
          $b = $this->getByte();
          if ($b === FALSE) {
            return FALSE;
          }

          $state = $b == 0xFF ? 'sof' : 'started';
          break;

        case 'sof':
          $b = $this->getByte();
          if (in_array($b, range(0xe0, 0xef))) {
            $state = 'skipframe';
          }
          elseif (in_array($b,
            array_merge(range(0xC0, 0xC3), range(0xC5, 0xC7), range(0xC9, 0xCB),
              range(0xCD, 0xCF)))) {
            $state = 'readsize';
          }
          elseif ($b == 0xFF) {
            $state = 'sof';
          }
          else {
            $state = 'skipframe';
          }
          break;

        case 'skipframe':
          $skip = $this->readInt($this->getChars(2)) - 2;
          $state = 'doskip';
          break;

        case 'doskip':
          /** @noinspection PhpUndefinedVariableInspection */
          $this->getChars($skip);
          $state = 'started';
          break;

        case 'readsize':
          $c = $this->getChars(7);
          return array(
            $this->readInt(substr($c, 5, 2)),
            $this->readInt(substr($c, 3, 2))
          );
      }
    }
  }

  private function getChars($n) {
    $response = NULL;

    // do we need more data?
    if ($this->strpos + $n - 1 >= strlen($this->str)) {
      $end = ($this->strpos + $n);

      while (strlen($this->str) < $end && $response !== FALSE) {
        // read more from the file handle
        $need = $end - ftell($this->handle);

        if ($response = fread($this->handle, $need)) {
          $this->str .= $response;
        }
        else {
          return FALSE;
        }
      }
    }

    $result = substr($this->str, $this->strpos, $n);
    $this->strpos += $n;

    return $result;
  }

  private function getByte() {
    $c = $this->getChars(1);
    $b = unpack("C", $c);
    return reset($b);
  }

  private function readInt($str) {
    $size = unpack("C*", $str);
    return ($size[1] << 8) + $size[2];
  }

  public function __destruct() {
    $this->close();
  }
}

src/ImageSizer.php

0 → 100644
+45 −0
Changes for src/ImageSizer.php: 45 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Drupal\image_field_repair;

/**
 * Helper for get image size.
 */
class ImageSizer {

  /**
   * FastImage instance.
   *
   * @var \Drupal\image_field_repair\FastImage
   */
  private static $fastImage;

  /**
   * Returns size of image.
   *
   * @param string $uri
   *   Uri of image.
   *
   * @return array|bool
   *   Array with info about image size: [0] => width, [1] => height.
   *   FALSE if image not found or unsupported format.
   */
  public static function getSize($uri) {
    if (!static::$fastImage) {
      static::$fastImage = new FastImage();
    }

    if ($uri[0] === '/' && $uri[1] === '/') {
      $uri = 'http:' . $uri;
    }

    @static::$fastImage->load($uri);

    $size = @static::$fastImage->getSize();
    if (!$size) {
      $size = @getimagesize($uri);
    }
    return $size;
  }

}
+56 −0
Changes for tests/src/Kernel/ImageSizerTest.php: 56 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\image_field_repair\Kernel;

use Drupal\image_field_repair\ImageSizer;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\TestFileCreationTrait;

/**
 * Tests get image size.
 *
 * @group image_field_repair
 */
class ImageSizerTest extends KernelTestBase {

  use TestFileCreationTrait;

  /**
   * {@inheritdoc}
   */
  public static $modules = ['file', 'system', 'image_field_repair', 'simpletest'];

  /**
   * Tests get image size.
   */
  public function testImageSizer() {
    $this->getTestFiles('image');
    $this->getTestFiles('text');
    $data = [
      'public://image-test.png' => [40, 20],
      'public://image-test-transparent-indexed.gif' => [20, 17],
      'public://image-test.gif' => [40, 20],
      'public://image-test-transparent-out-of-range.gif' => [40, 20],
      'public://image-test-no-transparency.gif' => [40, 20],
      'public://image-2.jpg' => [80, 60],
      'public://image-test.jpg' => [40, 20],
      'public://image-1.png' => [360, 240],
      'https://www.drupal.org/files/cta_multiple/featured_image/highlights_drupal.jpg' => [600, 410],
      '//www.drupal.org/files/cta_multiple/featured_image/highlights_drupal.jpg' => [600, 410],
      'public://text-0.txt' => FALSE,
      'public://not-found.jpg' => FALSE,
    ];

    foreach ($data as $uri => $expected_size) {
      $actual_size = ImageSizer::getSize($uri);
      if (is_array($expected_size)) {
        $this->assertSame($expected_size[0], $actual_size[0]);
        $this->assertSame($expected_size[1], $actual_size[1]);
      }
      else {
        $this->assertSame($expected_size, $actual_size);
      }
    }
  }

}