<?php

declare(strict_types=1);

/**
 * Gradient.php
 *
 * @since     2011-05-23
 * @category  Library
 * @package   PdfGraph
 * @author    Nicola Asuni <info@tecnick.com>
 * @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
 * @license   https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
 * @link      https://github.com/tecnickcom/tc-lib-pdf-graph
 *
 * This file is part of tc-lib-pdf-graph software library.
 */

namespace Com\Tecnick\Pdf\Graph;

use Com\Tecnick\Pdf\Graph\Exception as GraphException;

/**
 * Com\Tecnick\Pdf\Graph\Gradient
 *
 * @since     2011-05-23
 * @category  Library
 * @package   PdfGraph
 * @author    Nicola Asuni <info@tecnick.com>
 * @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
 * @license   https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
 * @link      https://github.com/tecnickcom/tc-lib-pdf-graph
 *
 * @phpstan-import-type GradientData from \Com\Tecnick\Pdf\Graph\Base
 * @phpstan-import-type StyleDataOpt from \Com\Tecnick\Pdf\Graph\Base
 *
 * @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
 */
abstract class Gradient extends \Com\Tecnick\Pdf\Graph\Raw
{
    /**
     * Blend mode.
     *
     * @var array<string, bool>
     */
    protected const BLENDMODE = [
        'Color' => true,
        'ColorBurn' => true,
        'ColorDodge' => true,
        'Darken' => true,
        'Difference' => true,
        'Exclusion' => true,
        'HardLight' => true,
        'Hue' => true,
        'Lighten' => true,
        'Luminosity' => true,
        'Multiply' => true,
        'Normal' => true,
        'Overlay' => true,
        'Saturation' => true,
        'Screen' => true,
        'SoftLight' => true,
    ];

    /**
     * Blend mode.
     *
     * @var array<string, string>
     */
    protected const COLSPACE = [
        'CMYK' => 'DeviceCMYK',
        'RGB' => 'DeviceRGB',
        'GRAY' => 'DeviceGray',
    ];

    /**
     * Returns the gradients array
     *
     * @return array<int, GradientData>
     */
    public function getGradientsArray(): array
    {
        return $this->gradients;
    }

    /**
     * Draws a basic rectangle
     *
     * @param float        $posx   Abscissa of upper-left corner.
     * @param float        $posy   Ordinate of upper-left corner.
     * @param float        $width  Width.
     * @param float        $height Height.
     * @param string       $mode   Mode of rendering. @see getPathPaintOp()
     * @param StyleDataOpt $style  Style.
     *
     * @return string PDF command
     */
    public function getBasicRect(
        float $posx,
        float $posy,
        float $width,
        float $height,
        string $mode = 'S',
        array $style = [],
    ): string {
        return $this->getStyleCmd($style) . $this->getRawRect($posx, $posy, $width, $height, $mode);
    }

    /**
     * Get a linear colour gradient command.
     *
     * @param float        $posx       Abscissa of the top left corner of the rectangle.
     * @param float        $posy       Ordinate of the top left corner of the rectangle.
     * @param float        $width      Width of the rectangle.
     * @param float        $height     Height of the rectangle.
     * @param string       $colorstart Starting color.
     * @param string       $colorend   Ending color.
     * @param array<float> $coords     Gradient vector (x1, y1, x2, y2).
     *
     * @return string PDF command
     *
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    public function getLinearGradient(
        float $posx,
        float $posy,
        float $width,
        float $height,
        string $colorstart,
        string $colorend,
        array $coords = [0, 0, 1, 0],
    ): string {
        return (
            $this->getStartTransform()
            . $this->getClippingRect($posx, $posy, $width, $height)
            . $this->getGradientTransform($posx, $posy, $width, $height)
            . $this->getGradient(
                2,
                $coords,
                [
                    [
                        'color' => $colorstart,
                        'exponent' => 1.0,
                        'offset' => 0.0,
                        'opacity' => 1.0,
                    ],
                    [
                        'color' => $colorend,
                        'exponent' => 1.0,
                        'offset' => 1.0,
                        'opacity' => 1.0,
                    ],
                ],
                '',
                false,
            )
            . $this->getStopTransform()
        );
    }

    /**
     * Get a radial colour gradient command.
     *
     * @param float        $posx       Abscissa of the top left corner of the rectangle.
     * @param float        $posy       Ordinate of the top left corner of the rectangle.
     * @param float        $width      Width of the rectangle.
     * @param float        $height     Height of the rectangle.
     * @param string       $colorstart Starting color.
     * @param string       $colorend   Ending color.
     * @param array<float> $coords     Array of the form (fx, fy, cx, cy, r) where
     *                                 (fx, fy) is the starting point of the
     *                                 gradient with $colorstart (be inside the
     *                                 circle), (cx, cy) is the center of the
     *                                 circle with $colorend, and r is the radius
     *                                 of the circle.
     *
     * @return string PDF command
     *
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    public function getRadialGradient(
        float $posx,
        float $posy,
        float $width,
        float $height,
        string $colorstart,
        string $colorend,
        array $coords = [0.5, 0.5, 0.5, 0.5, 1],
    ): string {
        return (
            $this->getStartTransform()
            . $this->getClippingRect($posx, $posy, $width, $height)
            . $this->getGradientTransform($posx, $posy, $width, $height)
            . $this->getGradient(
                3,
                $coords,
                [
                    [
                        'color' => $colorstart,
                        'exponent' => 1.0,
                        'offset' => 0.0,
                        'opacity' => 1.0,
                    ],
                    [
                        'color' => $colorend,
                        'exponent' => 1.0,
                        'offset' => 1.0,
                        'opacity' => 1.0,
                    ],
                ],
                '',
                false,
            )
            . $this->getStopTransform()
        );
    }

    /**
     * Rectangular clipping area.
     *
     * @param float $posx   Abscissa of the top left corner of the rectangle.
     * @param float $posy   Ordinate of the top left corner of the rectangle.
     * @param float $width  Width of the rectangle.
     * @param float $height Height of the rectangle.
     * @param bool  $eoclip If true, set clipping path using even-odd rule.
     */
    public function getClippingRect(float $posx, float $posy, float $width, float $height, bool $eoclip = false): string
    {
        $mode = $eoclip ? 'CEO' : 'CNZ';
        return $this->getRawRect($posx, $posy, $width, $height, $mode);
    }

    /**
     * Rectangular clipping area.
     *
     * @param float $posx   Abscissa of the top left corner of the rectangle.
     * @param float $posy   Ordinate of the top left corner of the rectangle.
     * @param float $width  Width of the rectangle.
     * @param float $height Height of the rectangle.
     */
    public function getGradientTransform(float $posx, float $posy, float $width, float $height): string
    {
        $ctm = [
            $width * $this->kunit,
            0,
            0,
            $height * $this->kunit,
            $posx * $this->kunit,
            ($this->pageh - ($posy + $height)) * $this->kunit,
        ];
        return $this->getTransformation($ctm);
    }

    /**
     * Get a color gradient PDF command.
     *
     * @param int               $type      Type of gradient (Not all types are currently supported):
     *                                     1 = Function-based shading; 2 = Axial shading; 3 = Radial
     *                                     shading; 4 = Free-form Gouraud-shaded triangle mesh; 5 =
     *                                     Lattice-form Gouraud-shaded triangle mesh; 6 = Coons
     *                                     patch mesh; 7 Tensor-product patch mesh
     * @param array<float>      $coords    Array of coordinates.
     * @param array<int, array{
     *            'color': string,
     *            'exponent'?: float,
     *            'opacity'?: float,
     *            'offset'?: float,
     *        }>  $stops     Array gradient color components:
     *                          color = color; offset = (0 to 1)
     *                          represents a location along the
     *                          gradient vector; exponent =
     *                          exponent of the exponential
     *                          interpolation function (default
     *                          = 1).
     * @param string            $bgcolor   Background color
     * @param bool              $antialias Flag indicating whether to filter the
     *                                     shading function to prevent aliasing artifacts.
     *
     * @return string PDF command
     *
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    public function getGradient(
        int $type,
        array $coords,
        array $stops,
        string $bgcolor,
        bool $antialias = false,
    ): string {
        if ($this->pdfa) {
            return '';
        }

        if (!isset($stops[0]['color'])) {
            throw new GraphException('Invalid gradient stops');
        }

        $model = $this->pdfColor->getColorObject($stops[0]['color']);
        if (!$model instanceof \Com\Tecnick\Color\Model) {
            throw new GraphException('Invalid color');
        }

        $colspace = self::COLSPACE[$model->getType()] ?? 'DeviceRGB';

        $ngr = 1 + \count($this->gradients);
        $this->gradients[$ngr] = $this->getGradientStops([
            'antialias' => $antialias,
            'background' => $this->pdfColor->getColorObject($bgcolor),
            'colors' => [],
            'colspace' => $colspace,
            'coords' => $coords,
            'id' => 0,
            'pattern' => 0,
            'stream' => '',
            'transparency' => false,
            'type' => $type,
        ], $stops);

        $out = '';
        if ($this->gradients[$ngr]['transparency']) {
            // paint luminosity gradient
            $out .= '/TGS' . $ngr . ' gs' . "\n";
        }

        // paint the gradient
        $out .= '/Sh' . $ngr . ' sh' . "\n";

        return $out;
    }

    /**
     * Returns the last gradient ID to be used with XOBjects.
     *
     * @return ?int
     */
    public function getLastGradientID(): ?int
    {
        return \array_key_last($this->gradients);
    }

    /**
     * Process the gradient stops.
     *
     * @param GradientData      $grad Array containing gradient info
     * @param array<int, array{
     *                'color': string,
     *                'exponent'?: float,
     *                'opacity'?: float,
     *                'offset'?: float,
     *             }> $stops Array gradient color components:
     *         color = color;
     *         offset = (0 to 1) represents a location along the gradient vector;
     *         exponent = exponent of the exponential interpolation function (default = 1).
     *
     * @return GradientData Gradient array.
     */
    protected function getGradientStops(array $grad, array $stops): array
    {
        $num_stops = \count($stops);
        $last_stop_id = $num_stops - 1;

        foreach ($stops as $key => $stop) {
            $grad['colors'][$key] = ['color' => $stop['color']];
            $grad['colors'][$key]['exponent'] = 1.0;
            if (array_key_exists('exponent', $stop)) {
                // exponent for the interpolation function
                $grad['colors'][$key]['exponent'] = $stop['exponent'];
            }

            $grad['colors'][$key]['opacity'] = 1.0;
            if (array_key_exists('opacity', $stop)) {
                $grad['colors'][$key]['opacity'] = $stop['opacity'];
                $grad['transparency'] = $grad['transparency'] || $stop['opacity'] < 1.0;
            }

            // offset represents a location along the gradient vector
            $offset = array_key_exists('offset', $stop) ? $stop['offset'] : null;
            if ($offset === null && $key === 0) {
                $offset = 0.0;
            } elseif ($offset === null && $key === $last_stop_id) {
                $offset = 1.0;
            } elseif ($offset === null && $key > 0 && array_key_exists('offset', $grad['colors'][$key - 1] ?? [])) {
                $prevoffset = $grad['colors'][$key - 1]['offset'] ?? 0.0;
                $offsetstep = (1.0 - $prevoffset) / ($num_stops - $key);
                $offset = $prevoffset + $offsetstep;
            }

            if ($offset !== null) {
                $grad['colors'][$key]['offset'] = $offset;
            }
        }

        return $grad;
    }

    /**
     * Paints a coons patch mesh.
     *
     * @param float        $posx       Abscissa of the top left corner of the rectangle.
     * @param float        $posy       Ordinate of the top left corner of the rectangle.
     * @param float        $width      Width of the rectangle.
     * @param float        $height     Height of the rectangle.
     * @param string       $colll      Lower-Left corner color.
     * @param string       $collr      Lower-Right corner color.
     * @param string       $colur      Upper-Right corner color.
     * @param string       $colul      Upper-Left corner color.
     * @param array<float> $coords     Coordinates
     * @param float        $coords_min Minimum value used by the coordinates.
     *                                 If a coordinate's value is smaller
     *                                 than this it will be cut to
     *                                 coords_min.
     * @param float        $coords_max Maximum value used by the coordinates.
     *                                 If a coordinate's value is greater
     *                                 than this it will be cut to
     *                                 coords_max.
     * @param bool         $antialias  Flag indicating whether to filter the
     *                                 shading function to prevent aliasing artifacts.
     *
     * @return string PDF command
     *
     * @SuppressWarnings("PHPMD.ExcessiveParameterList")
     * @SuppressWarnings("PHPMD.ExcessiveMethodLength")
     *
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    public function getCoonsPatchMeshWithCoords(
        float $posx,
        float $posy,
        float $width,
        float $height,
        string $colll = 'yellow',
        string $collr = 'blue',
        string $colur = 'green',
        string $colul = 'red',
        array $coords = [
            0.00,
            0.00,
            0.33,
            0.00,
            0.67,
            0.00,
            1.00,
            0.00,
            1.00,
            0.33,
            1.00,
            0.67,
            1.00,
            1.00,
            0.67,
            1.00,
            0.33,
            1.00,
            0.00,
            1.00,
            0.00,
            0.67,
            0.00,
            0.33,
        ],
        float $coords_min = 0.0,
        float $coords_max = 1.0,
        bool $antialias = false,
    ): string {
        if ($this->pdfa) {
            return '';
        }

        // simple array -> convert to multi patch array

        $patch_array = [
            0 => [
                'f' => 0,
                'points' => $coords,
                'colors' => [
                    0 => [
                        'red' => 1,
                        'green' => 1,
                        'blue' => 0,
                        'alpha' => 1,
                    ],
                    1 => [
                        'red' => 0,
                        'green' => 0,
                        'blue' => 1,
                        'alpha' => 1,
                    ],
                    2 => [
                        'red' => 0,
                        'green' => 1,
                        'blue' => 0,
                        'alpha' => 1,
                    ],
                    3 => [
                        'red' => 1,
                        'green' => 0,
                        'blue' => 0,
                        'alpha' => 1,
                    ],
                ],
            ],
        ];

        $colllobj = $this->pdfColor->getColorObject($colll);
        if (!$colllobj instanceof \Com\Tecnick\Color\Model) {
            throw new GraphException('Invalid Lower-Left corner color');
        }

        $patch_array[0]['colors'][0] = $colllobj->toRgbArray();

        $collrobj = $this->pdfColor->getColorObject($collr);
        if (!$collrobj instanceof \Com\Tecnick\Color\Model) {
            throw new GraphException('Invalid Lower-Right corner color');
        }

        $patch_array[0]['colors'][1] = $collrobj->toRgbArray();

        $colurobj = $this->pdfColor->getColorObject($colur);
        if (!$colurobj instanceof \Com\Tecnick\Color\Model) {
            throw new GraphException('Invalid Upper-Right corner color');
        }

        $patch_array[0]['colors'][2] = $colurobj->toRgbArray();

        $colulobj = $this->pdfColor->getColorObject($colul);
        if (!$colulobj instanceof \Com\Tecnick\Color\Model) {
            throw new GraphException('Invalid Upper-Left corner color');
        }

        $patch_array[0]['colors'][3] = $colulobj->toRgbArray();

        return $this->getCoonsPatchMesh(
            $posx,
            $posy,
            $width,
            $height,
            $patch_array,
            $coords_min,
            $coords_max,
            $antialias,
        );
    }

    /**
     * Paints a coons patch mesh.
     *
     * @param float        $posx       Abscissa of the top left corner of the rectangle.
     * @param float        $posy       Ordinate of the top left corner of the rectangle.
     * @param float        $width      Width of the rectangle.
     * @param float        $height     Height of the rectangle.
     * @param array<array{
     *            'f': int,
     *            'points': array<float>,
     *            'colors': array<int, array<string, float>>,
     *        }> $patch_array     For one patch mesh:
     *                                 array(float x1,
     *                                 float y1, ....
     *                                 float x12, float
     *                                 y12): 12 pairs of
     *                                 coordinates
     *                                 (normally from 0 to
     *                                 1) which specify
     *                                 the Bezier control
     *                                 points that define
     *                                 the patch. First
     *                                 pair is the lower
     *                                 left edge point,
     *                                 next is its right
     *                                 control point
     *                                 (control point 2).
     *                                 Then the other
     *                                 points are defined
     *                                 in the order:
     *                                 control point 1,
     *                                 edge point, control
     *                                 point 2 going
     *                                 counter-clockwise
     *                                 around the patch.
     *                                 Last (x12, y12) is
     *                                 the first edge
     *                                 point's left
     *                                 control point
     *                                 (control point 1).
     *                                 For two or more
     *                                 patch meshes:
     *                                 array[number of
     *                                 patches] - arrays
     *                                 with the following
     *                                 keys for each
     *                                 patch: f: where to
     *                                 put that patch (0 =
     *                                 first patch, 1, 2,
     *                                 3 = right, top and
     *                                 left) points: 12
     *                                 pairs of
     *                                 coordinates of the
     *                                 Bezier control
     *                                 points as above for
     *                                 the first patch, 8
     *                                 pairs of
     *                                 coordinates for the
     *                                 following patches,
     *                                 ignoring the
     *                                 coordinates already
     *                                 defined by the
     *                                 precedent patch
     *                                 colors: must be 4
     *                                 colors for the
     *                                 first patch, 2
     *                                 colors for the
     *                                 following patches
     * @param float        $coords_min Minimum value used by the coordinates.
     *                                 If a coordinate's value is smaller
     *                                 than this it will be cut to
     *                                 coords_min.
     * @param float        $coords_max Maximum value used by the coordinates.
     *                                 If a coordinate's value is greater
     *                                 than this it will be cut to
     *                                 coords_max.
     * @param bool         $antialias  Flag indicating whether to filter the
     *                                 shading function to prevent aliasing artifacts.
     *
     * @return string PDF command
     *
     * @SuppressWarnings("PHPMD.ExcessiveParameterList")
     * @SuppressWarnings("PHPMD.ExcessiveMethodLength")
     * @SuppressWarnings("PHPMD.CyclomaticComplexity")
     *
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    public function getCoonsPatchMesh(
        float $posx,
        float $posy,
        float $width,
        float $height,
        array $patch_array = [],
        float $coords_min = 0.0,
        float $coords_max = 1.0,
        bool $antialias = false,
    ): string {
        if ($this->pdfa) {
            return '';
        }

        if ($coords_max <= $coords_min) {
            throw new GraphException('coords_max must be greater than coords_min');
        }

        $ngr = 1 + \count($this->gradients);
        $this->gradients[$ngr] = [
            'antialias' => $antialias,
            'colors' => [],
            'background' => null,
            'colspace' => 'DeviceRGB',
            'coords' => [],
            'id' => 0,
            'pattern' => 0,
            'stream' => '',
            'transparency' => false,
            'type' => 6, //coons patch mesh
        ];

        $bpcd = 65_535; // 16 bits per coordinate

        foreach ($patch_array as $par) {
            $this->gradients[$ngr]['stream'] .= \chr($par['f'] & 0xFF); // start with the edge flag as 8 bit
            foreach ($par['points'] as $point) {
                // each point as 16 bit
                $point = \floor(\max(0, \min($bpcd, (($point - $coords_min) / ($coords_max - $coords_min)) * $bpcd)));
                $this->gradients[$ngr]['stream'] .=
                    \chr((int) \floor($point / 256) & 0xFF) . \chr((int) \floor($point % 256) & 0xFF);
            }

            foreach ($par['colors'] as $color) {
                // each color component as 8 bit
                $red = $color['red'] ?? 0.0;
                $green = $color['green'] ?? 0.0;
                $blue = $color['blue'] ?? 0.0;
                $this->gradients[$ngr]['stream'] .=
                    \chr((int) \floor($red * 255) & 0xFF)
                    . \chr((int) \floor($green * 255) & 0xFF)
                    . \chr((int) \floor($blue * 255) & 0xFF);
            }
        }

        return (
            $this->getStartTransform()
            . $this->getClippingRect($posx, $posy, $width, $height)
            . $this->getGradientTransform($posx, $posy, $width, $height)
            . '/Sh'
            . $ngr
            . ' sh'
            . "\n"
            . $this->getStopTransform()
        );
    }

    /**
     * Paints registration bars with color transtions
     *
     * @param float                     $posx     Abscissa of the top left corner of the rectangle.
     * @param float                     $posy     Ordinate of the top left corner of the rectangle.
     * @param float                     $width    Width of the rectangle.
     * @param float                     $height   Height of the rectangle.
     * @param bool                      $vertical If true prints bar vertically, otherwise horizontally.
     * @param array<int, array<string>> $colors   Array of colors to print,
     *                                            each entry is a color
     *                                            string or an array of two
     *                                            transition colors;
     *
     * @return string PDF command
     *
     * @SuppressWarnings("PHPMD.CyclomaticComplexity")
     *
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    public function getColorRegistrationBar(
        float $posx,
        float $posy,
        float $width,
        float $height,
        bool $vertical = false,
        array $colors = [
            // GRAY : black   to white
            ['g(0%)',              'g(100%)'],
            // RGB  : red     to white
            ['rgb(100%,0%,0%)',    'rgb(100%,100%,100%)'],
            // RGB  : green   to white
            ['rgb(0%,100%,0%)',    'rgb(100%,100%,100%)'],
            // RGB  : blue    to white
            ['rgb(0%,0%,100%)',    'rgb(100%,100%,100%)'],
            // CMYK : cyan    to white
            ['cmyk(100%,0%,0,0%)', 'cmyk(0%,0%,0,0%)'],
            // CMYK : magenta to white
            ['cmyk(0%,100%,0,0%)', 'cmyk(0%,0%,0,0%)'],
            // CMYK : yellow  to white
            ['cmyk(0%,0%,100,0%)', 'cmyk(0%,0%,0,0%)'],
            // CMYK : black   to white
            ['cmyk(0%,0%,0,100%)', 'cmyk(0%,0%,0,0%)'],
        ],
    ): string {
        $numbars = \count($colors);
        if ($numbars <= 0) {
            return '';
        }

        // set bar measures
        // default (non-vertical)
        $coords = [0, 0, 1, 0];
        $wbr = $width;
        $hbr = $height / $numbars;
        $xdt = 0.0;
        $ydt = $hbr;

        if ($vertical) {
            $coords = [0, 1, 0, 0]; // coordinates for gradient transition
            $wbr = $width / $numbars; // bar width
            $hbr = $height; // bar height
            $xdt = $wbr; // delta x
            $ydt = 0.0; // delta y
        }

        $xbr = $posx;
        $ybr = $posy;

        $out = '';
        foreach ($colors as $color) {
            if ($color !== [] && isset($color[0]) && $color[0] !== '') {
                if (!isset($color[1])) {
                    $color[1] = $color[0];
                }

                if ($color[0] === $color[1] || $this->pdfa) {
                    // colored rectangle
                    $out .= $this->getStartTransform();

                    $colobj = $this->pdfColor->getColorObject($color[0]);
                    if ($colobj instanceof \Com\Tecnick\Color\Model) {
                        $out .= $colobj->getPdfColor();
                    }

                    $out .= $this->getBasicRect($xbr, $ybr, $wbr, $hbr, 'F') . $this->getStopTransform();
                } else {
                    // color gradient
                    $out .= $this->getLinearGradient($xbr, $ybr, $wbr, $hbr, $color[0], $color[1], $coords);
                }
            }

            $xbr += $xdt;
            $ybr += $ydt;
        }

        return $out;
    }

    /**
     * Get a crop-mark.
     *
     * @param float        $posx   Abscissa of the crop-mark center.
     * @param float        $posy   Ordinate of the crop-mark center.
     * @param float        $width  Width of the crop-mark.
     * @param float        $height Height of the crop-mark.
     * @param string       $type   Type of crop mark - one symbol per type:
     *                             T = TOP, B = BOTTOM, L = LEFT, R = RIGHT
     * @param StyleDataOpt $style  Line style to apply.
     *
     * @return string PDF command
     */
    public function getCropMark(
        float $posx,
        float $posy,
        float $width,
        float $height,
        string $type = 'TBLR',
        array $style = [],
    ): string {
        $crops = \array_unique(\str_split(\strtoupper($type), 1));
        $space_ratio = 4;
        $dhw = $width / $space_ratio; // horizontal space to leave before the intersection point
        $dvh = $height / $space_ratio; // vertical space to leave before the intersection point

        $out = '';
        foreach ($crops as $crop) {
            $draw = true;
            $posx1 = $posx;
            $posy1 = $posy;
            $posx2 = $posx;
            $posy2 = $posy;
            switch ($crop) {
                case 'T':
                    $posx1 = $posx;
                    $posy1 = $posy - $height;
                    $posx2 = $posx;
                    $posy2 = $posy - $dvh;
                    break;
                case 'B':
                    $posx1 = $posx;
                    $posy1 = $posy + $dvh;
                    $posx2 = $posx;
                    $posy2 = $posy + $height;
                    break;
                case 'L':
                    $posx1 = $posx - $width;
                    $posy1 = $posy;
                    $posx2 = $posx - $dhw;
                    $posy2 = $posy;
                    break;
                case 'R':
                    $posx1 = $posx + $dhw;
                    $posy1 = $posy;
                    $posx2 = $posx + $width;
                    $posy2 = $posy;
                    break;
                default:
                    $draw = false;
                    break;
            }

            if (!$draw) {
                continue;
            }

            $out .= $this->getRawPoint($posx1, $posy1) . $this->getRawLine($posx2, $posy2) . $this->getPathPaintOp('S');
        }

        if ($out === '') {
            return '';
        }

        return $this->getStartTransform() . $this->getStyleCmd($style) . $out . $this->getStopTransform();
    }

    /**
     * Get overprint mode for stroking (OP) and non-stroking (op) painting operations.
     * (Check the "Entries in a Graphics State Parameter Dictionary" on PDF 32000-1:2008).
     *
     * @param bool $stroking    If true apply overprint for stroking operations.
     * @param bool|null $nonstroking If true apply overprint for painting operations other than stroking.
     * @param int  $mode        Overprint mode:
     *                          0 = each source
     *                          colour
     *                          component value
     *                          replaces the
     *                          value
     *                          previously
     *                          painted for the
     *                          corresponding
     *                          device
     *                          colorant; 1 = a
     *                          tint value of
     *                          0.0 for a
     *                          source colour
     *                          component shall
     *                          leave the
     *                          corresponding
     *                          component of
     *                          the previously
     *                          painted colour
     *                          unchanged.
     *
     * @return string PDF command
     */
    public function getOverprint(bool $stroking = true, ?bool $nonstroking = null, int $mode = 0): string
    {
        if ($nonstroking === null) {
            $nonstroking = $stroking;
        }

        return $this->getExtGState([
            'OP' => $stroking,
            'op' => $nonstroking,
            'OPM' => \max(0, \min(1, $mode)),
        ]);
    }

    /**
     * Set alpha for stroking (CA) and non-stroking (ca) operations.
     *
     * @param float        $stroking    Alpha value for stroking operations:
     *                                  real value from 0 (transparent) to 1 (opaque).
     * @param string       $bmv         Blend mode, one of the following:
     *                                  Normal, Multiply, Screen,
     *                                  Overlay, Darken, Lighten,
     *                                  ColorDodge, ColorBurn, HardLight,
     *                                  SoftLight, Difference, Exclusion,
     *                                  Hue, Saturation, Color,
     *                                  Luminosity.
     * @param float|string $nonstroking Alpha value for non-stroking operations:
     *                                  real value from 0 (transparent) to 1
     *                                  (opaque).
     *
     * @return string PDF command
     */
    public function getAlpha(
        float $stroking = 1,
        string $bmv = 'Normal',
        float|string $nonstroking = '',
        bool $ais = false,
    ): string {
        if ($nonstroking === '') {
            $nonstroking = $stroking;
        }

        if ($bmv !== '' && $bmv[0] === '/') {
            // remove leading slash
            $bmv = \substr($bmv, 1);
        }

        if (!array_key_exists($bmv, self::BLENDMODE)) {
            $bmv = 'Normal';
        }

        if (\is_string($nonstroking)) {
            $nonstroking = \is_numeric($nonstroking) ? (float) $nonstroking : $stroking;
        }

        return $this->getExtGState([
            'CA' => $stroking,
            'ca' => $nonstroking,
            'BM' => '/' . $bmv,
            'AIS' => $ais,
        ]);
    }
}
