• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dg / texy / 38492668891

14 Oct 2026 12:52AM UTC coverage: 92.573% (-0.5%) from 93.115%
38492668891

push

github

dg
added upgrading.md

2306 of 2491 relevant lines covered (92.57%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.74
/src/Texy/Modules/ImageModule.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the Texy! (https://texy.deegee.dev)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Texy\Modules;
9

10
use Texy;
11
use Texy\{Helpers, Image, Patterns};
12
use function is_int;
13

14

15
/**
16
 * Processes image syntax and detects image dimensions.
17
 */
18
final class ImageModule extends Texy\Module
19
{
20
        /** root of relative images (http) */
21
        public ?string $root = 'images/';
22

23
        /** @deprecated */
24
        public ?string $linkedRoot = 'images/';
25

26
        /** physical location of images on server */
27
        public ?string $fileRoot = null;
28

29
        /** left-floated images CSS class */
30
        public ?string $leftClass = null;
31

32
        /** right-floated images CSS class */
33
        public ?string $rightClass = null;
34

35
        /** @deprecated */
36
        public ?string $defaultAlt = '';
37

38
        /** @var array<string, Image> image references */
39
        private array $references = [];
40

41

42
        public function __construct(Texy\Texy $texy)
43
        {
44
                $this->texy = $texy;
1 ✔
45

46
                $texy->allowed['image/definition'] = true;
1 ✔
47
                $texy->addHandler('image', $this->solve(...));
1 ✔
48
                $texy->addHandler('beforeParse', $this->beforeParse(...));
1 ✔
49

50
                // [*image*]:LINK
51
                $texy->registerLinePattern(
1 ✔
52
                        $this->patternImage(...),
1 ✔
53
                        '#\[\*\ *+([^\n' . Patterns::MARK . ']{1,1000})' . Patterns::MODIFIER . '?\ *+(\*|(?<!<)>|<)\]' // [* urls .(title)[class]{style} >]
54
                        . '(?::(' . Patterns::LINK_URL . '|:))??()#Uu',
1 ✔
55
                        'image',
1 ✔
56
                );
57
        }
1 ✔
58

59

60
        /**
61
         * Text pre-processing.
62
         */
63
        private function beforeParse(Texy\Texy $texy, string &$text): void
64
        {
65
                if (!empty($texy->allowed['image/definition'])) {
1 ✔
66
                        // [*image*]: urls .(title)[class]{style}
67
                        $text = Texy\Regexp::replace(
1 ✔
68
                                $text,
69
                                '#^\[\*([^\n]{1,100})\*\]:[\ \t]+(.{1,1000})[\ \t]*' . Patterns::MODIFIER . '?\s*()$#mUu',
1 ✔
70
                                $this->patternReferenceDef(...),
1 ✔
71
                        );
72
                }
73
        }
1 ✔
74

75

76
        /**
77
         * Callback for: [*image*]: urls .(title)[class]{style}.
78
         * @param  string[]  $matches
79
         */
80
        private function patternReferenceDef(array $matches): string
81
        {
82
                [, $mRef, $mURLs, $mMod] = $matches;
1 ✔
83
                // [1] => [* (reference) *]
84
                // [2] => urls
85
                // [3] => .(title)[class]{style}<>
86

87
                $image = $this->factoryImage($mURLs, $mMod, tryRef: false);
1 ✔
88
                $this->addReference($mRef, $image);
1 ✔
89
                return '';
1 ✔
90
        }
91

92

93
        /**
94
         * Callback for [* small.jpg 80x13 || big.jpg .(alternative text)[class]{style}>]:LINK.
95
         * @param  string[]  $matches
96
         */
97
        public function patternImage(Texy\LineParser $parser, array $matches): Texy\HtmlElement|string|null
98
        {
99
                [, $mURLs, $mMod, $mAlign, $mLink] = $matches;
1 ✔
100
                // [1] => URLs
101
                // [2] => .(title)[class]{style}<>
102
                // [3] => * < >
103
                // [4] => url | [ref] | [*image*]
104

105
                $image = $this->factoryImage($mURLs, $mMod . $mAlign);
1 ✔
106

107
                if ($mLink) {
1 ✔
108
                        if ($mLink === ':') {
1 ✔
109
                                $link = new Texy\Link($image->linkedURL ?? $image->URL);
1 ✔
110
                                $link->raw = ':';
1 ✔
111
                                $link->type = $link::IMAGE;
1 ✔
112
                        } else {
113
                                $link = $this->texy->linkModule->factoryLink($mLink, null, null);
1 ✔
114
                        }
115
                } else {
116
                        $link = null;
1 ✔
117
                }
118

119
                return $this->texy->invokeAroundHandlers('image', $parser, [$image, $link]);
1 ✔
120
        }
121

122

123
        /**
124
         * Adds new named reference to image.
125
         */
126
        public function addReference(string $name, Image $image): void
127
        {
128
                $image->name = Helpers::toLower($name);
1 ✔
129
                $this->references[$image->name] = $image;
1 ✔
130
        }
1 ✔
131

132

133
        /**
134
         * Returns named reference.
135
         */
136
        public function getReference(string $name): ?Image
137
        {
138
                $name = Helpers::toLower($name);
1 ✔
139
                if (isset($this->references[$name])) {
1 ✔
140
                        return clone $this->references[$name];
1 ✔
141
                }
142

143
                return null;
1 ✔
144
        }
145

146

147
        /**
148
         * Parses image's syntax. Input: small.jpg 80x13 || linked.jpg
149
         */
150
        public function factoryImage(string $content, string $mod, bool $tryRef = true): Image
151
        {
152
                $image = $tryRef ? $this->getReference(trim($content)) : null;
1 ✔
153

154
                if (!$image) {
1 ✔
155
                        $texy = $this->texy;
1 ✔
156
                        $content = explode('|', $content);
1 ✔
157
                        $image = new Image;
1 ✔
158

159
                        // dimensions
160
                        if ($matches = Texy\Regexp::match($content[0], '#^(.*) (\d+|\?) *(X|x) *(\d+|\?) *()$#U')) {
1 ✔
161
                                $image->URL = trim($matches[1]);
1 ✔
162
                                $image->asMax = $matches[3] === 'X';
1 ✔
163
                                $image->width = $matches[2] === '?' ? null : (int) $matches[2];
1 ✔
164
                                $image->height = $matches[4] === '?' ? null : (int) $matches[4];
1 ✔
165
                        } else {
166
                                $image->URL = trim($content[0]);
1 ✔
167
                        }
168

169
                        if (!$texy->checkURL($image->URL, $texy::FILTER_IMAGE)) {
1 ✔
170
                                $image->URL = null;
×
171
                        }
172

173
                        // linked image
174
                        if (isset($content[2])) {
1 ✔
175
                                $tmp = trim($content[2]);
1 ✔
176
                                if ($tmp !== '' && $texy->checkURL($tmp, $texy::FILTER_ANCHOR)) {
1 ✔
177
                                        $image->linkedURL = $tmp;
1 ✔
178
                                }
179
                        }
180
                }
181

182
                $image->modifier->setProperties($mod);
1 ✔
183
                return $image;
1 ✔
184
        }
185

186

187
        /**
188
         * Finish invocation.
189
         */
190
        public function solve(
191
                ?Texy\HandlerInvocation $invocation,
192
                Image $image,
193
                ?Texy\Link $link = null,
194
        ): Texy\HtmlElement|string|null
195
        {
196
                if ($image->URL === null) {
1 ✔
197
                        return null;
×
198
                }
199

200
                $texy = $this->texy;
1 ✔
201

202
                $mod = $image->modifier;
1 ✔
203
                $alt = $mod->title;
1 ✔
204
                $mod->title = null;
1 ✔
205
                $hAlign = $mod->hAlign;
1 ✔
206
                $mod->hAlign = null;
1 ✔
207

208
                $el = new Texy\HtmlElement('img');
1 ✔
209
                $el->attrs['src'] = null; // trick - move to front
1 ✔
210
                $mod->decorate($texy, $el);
1 ✔
211
                $el->attrs['src'] = Helpers::prependRoot($image->URL, $this->root);
1 ✔
212
                if (!isset($el->attrs['alt'])) {
1 ✔
213
                        $el->attrs['alt'] = $alt === null
1 ✔
214
                                ? $this->defaultAlt
1 ✔
215
                                : $texy->typographyModule->postLine($alt);
1 ✔
216
                }
217

218
                if ($hAlign) {
1 ✔
219
                        $var = $hAlign . 'Class'; // leftClass, rightClass
1 ✔
220
                        if (!empty($this->$var)) {
1 ✔
221
                                settype($el->attrs['class'], 'array');
1 ✔
222
                                $el->attrs['class'][] = $this->$var;
1 ✔
223

224
                        } elseif (empty($texy->alignClasses[$hAlign])) {
1 ✔
225
                                settype($el->attrs['style'], 'array');
1 ✔
226
                                $el->attrs['style']['float'] = $hAlign;
1 ✔
227

228
                        } else {
229
                                settype($el->attrs['class'], 'array');
1 ✔
230
                                $el->attrs['class'][] = $texy->alignClasses[$hAlign];
1 ✔
231
                        }
232
                }
233

234
                if (!is_int($image->width) || !is_int($image->height) || $image->asMax) {
1 ✔
235
                        $this->detectDimensions($image);
1 ✔
236
                }
237

238
                $el->attrs['width'] = $image->width;
1 ✔
239
                $el->attrs['height'] = $image->height;
1 ✔
240

241
                $texy->summary['images'][] = (string) $el->attrs['src'];
1 ✔
242

243
                if ($link) {
1 ✔
244
                        return $texy->linkModule->solve(null, $link, $el);
1 ✔
245
                }
246

247
                return $el;
1 ✔
248
        }
249

250

251
        private function detectDimensions(Image $image): void
252
        {
253
                // absolute URL & security check for double dot
254
                if ($image->URL === null || !Helpers::isRelative($image->URL) || str_contains($image->URL, '..')) {
1 ✔
255
                        return;
1 ✔
256
                }
257

258
                $file = rtrim((string) $this->fileRoot, '/\\') . '/' . $image->URL;
1 ✔
259
                if (!@is_file($file) || !($size = @getimagesize($file))) { // @ the path may be outside open_basedir, the image unreadable
1 ✔
260
                        return;
1 ✔
261
                }
262

263
                if ($image->asMax) {
1 ✔
264
                        $ratio = 1;
×
265
                        if (is_int($image->width)) {
×
266
                                $ratio = min($ratio, $image->width / $size[0]);
×
267
                        }
268

269
                        if (is_int($image->height)) {
×
270
                                $ratio = min($ratio, $image->height / $size[1]);
×
271
                        }
272

273
                        $image->width = (int) round($ratio * $size[0]);
×
274
                        $image->height = (int) round($ratio * $size[1]);
×
275

276
                } elseif (is_int($image->width)) {
1 ✔
277
                        $image->height = (int) round($size[1] / $size[0] * $image->width);
1 ✔
278

279
                } elseif (is_int($image->height)) {
1 ✔
280
                        $image->width = (int) round($size[0] / $size[1] * $image->height);
1 ✔
281

282
                } else {
283
                        $image->width = $size[0];
1 ✔
284
                        $image->height = $size[1];
1 ✔
285
                }
286
        }
1 ✔
287
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc