<?php

/**
 * fast-image-size image type jpeg
 *
 * @package       fast-image-size
 * @copyright (c) Marc Alexander <admin@m-a-styles.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Nextend\Framework\FastImageSize\Type;

class TypeJpeg extends TypeBase {

    /** @var int JPEG max header size. Headers can be bigger, but we'll abort
     *            going through the header after this */
    const JPEG_MAX_HEADER_SIZE = 786432; // = 768 kiB

    /** @var string JPEG header */
    const JPEG_HEADER = "\xFF\xD8";

    /** @var string Start of frame marker */
    const SOF_START_MARKER = "\xFF";

    /** @var string End of image (EOI) marker */
    const JPEG_EOI_MARKER = "\xD9";

    /** @var array JPEG SOF markers */
    protected $sofMarkers = array(
        "\xC0",
        "\xC1",
        "\xC2",
        "\xC3",
        "\xC5",
        "\xC6",
        "\xC7",
        "\xC9",
        "\xCA",
        "\xCB",
        "\xCD",
        "\xCE",
        "\xCF"
    );

    /** @var string|bool JPEG data stream */
    protected $data = '';

    /** @var int Data length */
    protected $dataLength = 0;

    /**
     * {@inheritdoc}
     */
    public function getSize($filename) {
        // Do not force the data length
        $this->data = $this->fastImageSize->getImage($filename, 0, self::JPEG_MAX_HEADER_SIZE, false);

        // Check if file is jpeg
        if ($this->data === false || substr($this->data, 0, self::SHORT_SIZE) !== self::JPEG_HEADER) {
            return;
        }

        // Look through file for SOF marker
        $size = $this->getSizeInfo();

        $this->fastImageSize->setSize($size);
        $this->fastImageSize->setImageType(IMAGETYPE_JPEG);
    }

    /**
     * Get size info from image data
     *
     * @return array An array with the image's size info or an empty array if
     *        size info couldn't be found
     */
    protected function getSizeInfo() {
        $size = array();
        // since we check $i + 1 we need to stop one step earlier
        $this->dataLength = strlen($this->data) - 1;

        $sofStartRead = true;

        // Look through file for SOF marker
        for ($i = 2; $i < $this->dataLength; $i++) {
            $marker = $this->getNextMarker($i, $sofStartRead);

            if (in_array($marker, $this->sofMarkers)) {
                // Extract size info from SOF marker
                return $this->extractSizeInfo($i);
            } else {
                // Extract length only
                $markerLength = $this->extractMarkerLength($i);

                if ($markerLength < 2) {
                    return $size;
                }

                $i += $markerLength - 1;
                continue;
            }
        }

        return $size;
    }

    /**
     * Extract marker length from data
     *
     * @param int $i Current index
     *
     * @return int Length of current marker
     */
    protected function extractMarkerLength($i) {
        // Extract length only
        list(, $unpacked) = unpack("H*", substr($this->data, $i, self::LONG_SIZE));

        // Get width and height from unpacked size info
        $markerLength = hexdec(substr($unpacked, 0, 4));

        return $markerLength;
    }

    /**
     * Extract size info from data
     *
     * @param int $i Current index
     *
     * @return array Size info of current marker
     */
    protected function extractSizeInfo($i) {
        // Extract size info from SOF marker
        list(, $unpacked) = unpack("H*", substr($this->data, $i - 1 + self::LONG_SIZE, self::LONG_SIZE));

        // Get width and height from unpacked size info
        $size = array(
            'width'  => hexdec(substr($unpacked, 4, 4)),
            'height' => hexdec(substr($unpacked, 0, 4)),
        );

        return $size;
    }

    /**
     * Get next JPEG marker in file
     *
     * @param int  $i            Current index
     * @param bool $sofStartRead Flag whether SOF start padding was already read
     *
     * @return string Next JPEG marker in file
     */
    protected function getNextMarker(&$i, &$sofStartRead) {
        $this->skipStartPadding($i, $sofStartRead);

        do {
            if ($i >= $this->dataLength) {
                return self::JPEG_EOI_MARKER;
            }
            $marker = $this->data[$i];
            $i++;
        } while ($marker == self::SOF_START_MARKER);

        return $marker;
    }

    /**
     * Skip over any possible padding until we reach a byte without SOF start
     * marker. Extraneous bytes might need to require proper treating.
     *
     * @param int  $i            Current index
     * @param bool $sofStartRead Flag whether SOF start padding was already read
     */
    protected function skipStartPadding(&$i, &$sofStartRead) {
        if (!$sofStartRead) {
            while ($this->data[$i] !== self::SOF_START_MARKER) {
                $i++;
            }
        }
    }
}
