<?php declare(strict_types=1);
/**
 * This file is part of escpos-php: PHP receipt printer library for use with
 * ESC/POS-compatible thermal and impact printers.
 *
 * Copyright (c) 2014-20 Michael Billington < michael.billington@gmail.com >,
 * incorporating modifications by others. See CONTRIBUTORS.md for a full list.
 *
 * This software is distributed under the terms of the MIT license. See LICENSE.md
 * for details.
 */

namespace Mike42\Escpos;

use Exception;
use Imagick;

/**
 * Implementation of EscposImage using the Imagick PHP plugin.
 */
class ImagickEscposImage extends EscposImage
{
    /**
     * Load actual image pixels from Imagick object
     *
     * @param Imagick $im Image to load from
     */
    public function readImageFromImagick(Imagick $im)
    {
        /* Strip transparency */
        $im = self::alphaRemove($im);
        /* Threshold */
        $im -> setImageType(Imagick::IMGTYPE_TRUECOLOR); // Remove transparency (good for PDF's)
        $max = $im->getQuantumRange();
        $max = $max["quantumRangeLong"];
        $im -> thresholdImage(0.5 * $max);
        /* Make a string of 1's and 0's */
        $imgHeight = $im -> getimageheight();
        $imgWidth = $im -> getimagewidth();
        $imgData = str_repeat("\0", $imgHeight * $imgWidth);
        for ($y = 0; $y < $imgHeight; $y++) {
            for ($x = 0; $x < $imgWidth; $x++) {
                /* Faster to average channels, blend alpha and negate the image here than via filters (tested!) */
                $cols = $im -> getImagePixelColor($x, $y);
                $cols = $cols -> getcolor();
                $greyness = (int)(($cols['r'] + $cols['g'] + $cols['b']) / 3) >> 7;  // 1 for white, 0 for black
                $imgData[$y * $imgWidth + $x] = (1 - $greyness); // 1 for black, 0 for white
            }
        }
        $this -> setImgWidth($imgWidth);
        $this -> setImgHeight($imgHeight);
        $this -> setImgData($imgData);
    }

    /**
     * @param string $filename
     *  Filename to load from
     * @param boolean $highDensityVertical
     *  True for high density output (24px lines), false for regular density (8px)
     * @return string[]|NULL
     *  Column format data as array, or NULL if optimised renderer isn't
     *  available in this implementation.
     */
    protected function getColumnFormatFromFile($filename = null, $highDensityVertical = true)
    {
        if ($filename === null) {
            return null;
        }
        $im = $this -> getImageFromFile($filename);
        $this -> setImgWidth($im -> getimagewidth());
        $this -> setImgHeight($im -> getimageheight());
        
        /* Strip transparency */
        $im = self::alphaRemove($im);
        $im -> setformat('pbm');
        $im -> getimageblob(); // Forces 1-bit rendering now, so that subsequent operations are faster
        $im -> rotateImage('#fff', 90.0);
        $im -> flopImage();
        $lineHeight = $highDensityVertical ? 3 : 1;
        $blobs = $this -> getColumnFormatFromImage($im, $lineHeight * 8);
        return $blobs;
    }

    /**
     * Load an image from disk, into memory, using Imagick.
     *
     * @param string|null $filename The filename to load from
     * @throws Exception if the image format is not supported,
     *  or the file cannot be opened.
     */
    protected function loadImageData(string $filename = null)
    {
        if ($filename === null) {
            /* Set to blank image */
            return parent::loadImageData($filename);
        }
    
        $im = $this -> getImageFromFile($filename);
        $this -> readImageFromImagick($im);
    }

    /**
     * Return data in column format as array of slices.
     * Operates recursively to save cloning larger image many times.
     *
     * @param Imagick $im
     * @param int $lineHeight
     *          Height of printed line in dots. 8 or 24.
     * @return string[]
     */
    private function getColumnFormatFromImage(Imagick $im, int $lineHeight)
    {
        $imgWidth = $im->getimagewidth();
        if ($imgWidth == $lineHeight) {
            // Return glob of this panel
            return [$this -> getRasterBlobFromImage($im)];
        } elseif ($imgWidth > $lineHeight) {
            // Calculations
            $slicesLeft = (int)ceil($imgWidth / $lineHeight / 2); // TODO avoid use of floats with intdiv()
            $widthLeft = $slicesLeft * $lineHeight;
            $widthRight = $imgWidth - $widthLeft;
            // Slice up (left)
            $left = clone $im;
            $left -> extentimage($widthLeft, $left -> getimageheight(), 0, 0);
            // Slice up (right - ensure width is divisible by lineHeight also)
            $right = clone $im;
            $widthRightRounded = $widthRight < $lineHeight ? $lineHeight : $widthRight;
            $right -> extentimage($widthRightRounded, $right -> getimageheight(), $widthLeft, 0);
            // Recurse
            $leftBlobs = $this -> getColumnFormatFromImage($left, $lineHeight);
            $rightBlobs = $this -> getColumnFormatFromImage($right, $lineHeight);
            return array_merge($leftBlobs, $rightBlobs);
        } else {
            /* Image is smaller than full width */
            $im -> extentimage($lineHeight, $im -> getimageheight(), 0, 0);
            return [$this -> getRasterBlobFromImage($im)];
        }
    }

    /**
     * Load Imagick file from image
     *
     * @param string $filename Filename to load
     * @throws Exception Wrapped Imagick error if image can't be loaded
     * @return Imagick Loaded image
     */
    private function getImageFromFile($filename)
    {
        $im = new Imagick();
        try {
            $im -> setResourceLimit(6, 1); // Prevent libgomp1 segfaults, grumble grumble.
            $im -> readimage($filename);
        } catch (\ImagickException $e) {
            /* Re-throw as normal exception */
            throw new Exception($e -> getMessage());
        }
        return $im;
    }

    /**
     * Pull blob (from PBM-formatted image only!), and spit out a blob or raster data.
     * Will crash out on anything which is not a valid 'P4' file.
     *
     * @param Imagick $im Image which has format PBM.
     * @return string raster data from the image
     */
    private function getRasterBlobFromImage(Imagick $im)
    {
        $blob = $im -> getimageblob();
        /* Find where header ends */
        $i = strpos($blob, "P4\n") + 2;
        while ($blob[$i + 1] == '#') {
            $i = strpos($blob, "\n", $i + 1);
        }
        $i = strpos($blob, "\n", $i + 1);
        /* Return raster data only */
        $subBlob = substr($blob, $i + 1);
        return $subBlob;
    }

    /**
     * @param string $filename
     *  Filename to load from
     * @return string|NULL
     *  Raster format data, or NULL if no optimised renderer is available in
     *  this implementation.
     */
    protected function getRasterFormatFromFile($filename = null)
    {
        if ($filename === null) {
            return null;
        }
        $im = $this -> getImageFromFile($filename);
        $this -> setImgWidth($im -> getimagewidth());
        $this -> setImgHeight($im -> getimageheight());
        /* Convert to PBM and extract raster portion */
        $im = self::alphaRemove($im);
        $im -> setFormat('pbm');
        return $this -> getRasterBlobFromImage($im);
    }

    /**
     * Load a PDF for use on the printer
     *
     * @param string $pdfFile
     *  The file to load
     * @param int $pageWidth
     *  The width, in pixels, of the printer's output. The first page of the
     *  PDF will be scaled to approximately fit in this area.
     * @throws Exception Where Imagick is not loaded, or where a missing file
     *  or invalid page number is requested.
     * @return array Array of images, retrieved from the PDF file.
     */
    public static function loadPdf($pdfFile, int $pageWidth = 550)
    {
        if (!EscposImage::isImagickLoaded()) {
            throw new Exception(__FUNCTION__ . " requires imagick extension.");
        }
        /*
         * Load first page at very low density (resolution), to figure out what
         * density to use to achieve $pageWidth
         */
        try {
            $image = new Imagick();
            $testRes = 2; // Test resolution
            $image -> setresolution($testRes, $testRes);
            /* Load document just to measure geometry */
            $image -> readimage($pdfFile);
            $geo = $image -> getimagegeometry();
            $image -> destroy();
            $width = $geo['width'];
            $newRes = $pageWidth / $width * $testRes;
            /* Load entire document in */
            $image -> setresolution($newRes, $newRes);
            $image -> readImage($pdfFile);
            $pages = $image -> getNumberImages();
            /* Convert images to Escpos objects */
            $ret = [];
            for ($i = 0; $i < $pages; $i++) {
                $image -> setIteratorIndex($i);
                $ep = new ImagickEscposImage();
                $ep -> readImageFromImagick($image);
                $ret[] = $ep;
            }
            return $ret;
        } catch (\ImagickException $e) {
            /* Wrap in normal exception, so that classes which call this do not
             * themselves require imagick as a dependency. */
            throw new Exception($e -> getMessage());
        }
    }

    /**
     * Paste image over white canvas to stip transparency reliably on different
     * versions of ImageMagick.
     *
     * There are other methods for this:
     * - flattenImages() is deprecated
     * - setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE) is not available on
     *      ImageMagick < 6.8.
     *
     * @param Imagick $im Image to flatten
     * @return Imagick Flattened image
     */
    private static function alphaRemove(Imagick $im)
    {
        $flat = new Imagick();
        $flat -> newImage($im -> getimagewidth(), $im -> getimageheight(), "white", $im -> getimageformat());
        $flat -> compositeimage($im, Imagick::COMPOSITE_OVER, 0, 0);
        return $flat;
    }
}
