Passed
Pull Request — master (#41)
by Burhan
02:06
created

FieldRendererComplexText::wrap()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 9.7875

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 13
nc 10
nop 4
dl 0
loc 25
c 2
b 0
f 0
cc 7
ccs 8
cts 13
cp 0.6153
crap 9.7875
rs 8.8333
1
<?php
2
3
namespace Graze\CiffRenderer\Renderer\FieldRenderer;
4
5
use Graze\CiffRenderer\Parser\FieldParser\FieldParserComplexText;
6
use Graze\CiffRenderer\Parser\FieldParser\FieldParserInterface;
7
use Intervention\Image\Gd\Font;
8
use Intervention\Image\ImageManager;
9
10
class FieldRendererComplexText implements FieldRendererInterface
11
{
12
    /**
13
     * @var float
14
     */
15
    const LETTER_SPACING_ADJUSTMENT = 0.97;
16
17
    /**
18
     * @var Font
19
     */
20
    private $font;
21
22
    /**
23
     * @param Font $font
24
     */
25 2
    public function __construct(Font $font)
26
    {
27 2
        $this->font = $font;
28 2
    }
29
30
    /**
31
     * @param ImageManager $imageManager
32
     * @param FieldParserInterface $parser
33
     * @param null|callable $fontResolver
34
     * @param null|callable $graphicResolver
35
     * @return \Intervention\Image\Image
36
     */
37 1
    public function render(
38
        ImageManager $imageManager,
39
        FieldParserInterface $parser,
40
        callable $fontResolver = null,
41
        callable $graphicResolver = null
42
    ) {
43 1
        $fontPath = $fontResolver($parser->getFontFace());
44
45
        // Input may be multiline so grab the raw lines but preserve empty lines by injecting spaces
46 1
        $rawLines = explode("\n", str_replace("\n\n", "\n \n", $parser->getText()));
47 1
        $lines = [];
48 1
        foreach($rawLines as $rawLine) {
49
            // Pass each raw line through the wrapping function
50 1
            $lines = array_merge($lines, explode("\n", $this->wrap($parser->getFontSize(), $fontPath, $rawLine, $parser->getWidth())));
51
        }
52
53
        $renderedTexts = [];
54
        $width = 0;
55
        $height = 0;
56
57
        foreach ($lines as $line) {
58
            $renderedText = $this->renderLine($imageManager, $parser, $fontPath, $line);
59
            if (is_null($renderedText)) {
60
                continue;
61
            }
62
63
            // store dimensions ready to create a canvas
64
            $width = max($width, $renderedText->getWidth());
65
            $height += $renderedText->getHeight();
66
67
            $renderedTexts[] = $renderedText;
68
        }
69
70
        if (empty($renderedTexts)) {
71
            // nothing to render
72
            return null;
73
        }
74
75
        // create a canvas to add each line of text to
76
        $canvas = $imageManager->canvas($width, $height);
77
78
        $offsetX = 0;
79
        $offsetY = 0;
80
        foreach ($renderedTexts as $renderedText) {
81
            // add the text to the canvas
82
            $canvas->insert($renderedText, 'top-left', (int) $offsetX, (int) $offsetY);
83
84
            // increase the offset for the next line
85
            $offsetY += $renderedText->getHeight();
86
        }
87
88
        $orientation = $parser->getOrientation();
89
        if ($orientation) {
90
            $canvas->rotate(360 - $orientation);
91
        }
92
93
        return $canvas;
94
    }
95
96
    /**
97
     * @param ImageManager $imageManager
98
     * @param FieldParserInterface $parser
99
     * @param string $fontPath
100
     * @param string $text
101
     * @return \Intervention\Image\Image
102
     */
103
    private function renderLine(ImageManager $imageManager, FieldParserInterface $parser, $fontPath, $text)
104
    {
105
        // create the text here so it can be measured
106
        $this->font->text($text);
107
108
        $this->font->file($fontPath);
109
        $this->font->size($parser->getFontSize());
110
        $this->font->color('#000');
111
        $this->font->align($parser->getTextAlignment());
112
        $this->font->valign('top');
113
114
        $size = $this->font->getBoxSize();
115
116
        // If we are trying to render a single space, it will have zero height and be skipped. Temporarily use the
117
        // dimensions of the letter 'l' to ensure it isn't skipped.
118
        if ($text == ' ') {
119
            $this->font->text('l');
120
            $size = $this->font->getBoxSize();
121
            $this->font->text($text);
122
        }
123
124
        // if text consists of invisible chars, measured size will be zero, nothing to print
125
        if ($size['width'] < 1 || $size['height'] < 1) {
126
            return null;
127
        }
128
129
        $fontCallback = function (&$font) {
130
            // override the font created by Canvas::text()
131
            $font = $this->font;
132
        };
133
134
        $canvas = $imageManager->canvas($parser->getWidth(), $size['height']);
135
136
        // figure out the correct reference point for this alignment
137
        switch ($parser->getTextAlignment()) {
138
            case FieldParserComplexText::ALIGN_CENTRE:
139
                $posX = round($parser->getWidth()/2);
140
                break;
141
142
            case FieldParserComplexText::ALIGN_RIGHT:
143
                $posX = $parser->getWidth();
144
                break;
145
146
            default:
147
                $posX = 0;
148
                break;
149
        }
150
151
        // no need to pass the text in here as we override the font in the callback
152
        $canvas->text('', $posX, 0, $fontCallback);
0 ignored issues
show
Bug introduced by
It seems like $posX can also be of type double; however, parameter $x of Intervention\Image\Image::text() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

152
        $canvas->text('', /** @scrutinizer ignore-type */ $posX, 0, $fontCallback);
Loading history...
153
154
        // attempt to correct differences between this and Clarisoft
155
        $width = (int) round($parser->getWidth() * self::LETTER_SPACING_ADJUSTMENT);
156
        $canvas->resize($width, $size['height']);
157
158
        return $canvas;
159
    }
160
161
    /**
162
     * Wrap the given text such that it doesn't exceed the given width using the given font and size.
163
     *
164
     * Derived from https://www.php.net/manual/en/function.imagettftext.php#89505
165
     *
166
     * @param string $fontSize
167
     * @param string $fontFace
168
     * @param string $string
169
     * @param int $width
170
     * @return string
171
     */
172 1
    private function wrap($fontSize, $fontFace, $string, $width){
173
        // Font size needs to be scaled a bit to render to the same width as in Clarisoft.
174 1
        $fontSize = $fontSize * 0.8;
175 1
        $wrappedString = "";
176
177 1
        if ($string == ' ') {
178
            return $string;
179
        }
180
181 1
        $words = explode(' ', $string);
182
183 1
        foreach ($words as $word){
184
185 1
            $teststring = $wrappedString=="" ? $word : $wrappedString.' '.$word;
186 1
            $testbox = imagettfbbox($fontSize, 0, $fontFace, $teststring);
187
            // Check that adding the new word does not exceed the width we want.
188
            // This isn't exact for various (unknown) reasons, so there is a 10 pixel buffer.
189
            if ( $testbox[2] > ($width+10) ){
190
                $wrappedString.=($wrappedString=="" ? "" : "\n").$word;
191
            } else {
192
                $wrappedString.=($wrappedString=="" ? "" : ' ').$word;
193
            }
194
        }
195
196
        return $wrappedString;
197
    }
198
199
    /**
200
     * @return FieldRendererInterface
201
     */
202 1
    public static function factory()
203
    {
204 1
        return new static(
205 1
            new Font()
206
        );
207
    }
208
}
209