Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.46% covered (warning)
59.46%
22 / 37
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
JpegQualityDetector
59.46% covered (warning)
59.46%
22 / 37
25.00% covered (danger)
25.00%
1 / 4
71.04
0.00% covered (danger)
0.00%
0 / 1
 detectQualityOfJpgUsingImagick
20.00% covered (danger)
20.00%
2 / 10
0.00% covered (danger)
0.00%
0 / 1
32.09
 detectQualityOfJpgUsingImageMagick
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
7.77
 detectQualityOfJpgUsingGraphicsMagick
54.55% covered (warning)
54.55%
6 / 11
0.00% covered (danger)
0.00%
0 / 1
14.01
 detectQualityOfJpg
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3namespace WebPConvert\Convert\Helpers;
4
5use ExecWithFallback\ExecWithFallback;
6
7/**
8 * Try to detect quality of a jpeg image using various tools.
9 *
10 * @package    WebPConvert
11 * @author     Bjørn Rosell <it@rosell.dk>
12 * @since      Class available since Release 2.0.0
13 */
14class JpegQualityDetector
15{
16
17    /**
18     * Try to detect quality of jpeg using imagick extension.
19     *
20     * Note that the detection might fail for two different reasons:
21     * 1) Imagick is not installed
22     * 2) Imagick for some reason fails to detect quality for some images
23     *
24     * In both cases, null is returned.
25     *
26     * @param  string  $filename  A complete file path to file to be examined
27     * @return int|null  Quality, or null if it was not possible to detect quality
28     */
29    private static function detectQualityOfJpgUsingImagick($filename)
30    {
31        if (extension_loaded('imagick') && class_exists('\\Imagick')) {
32            try {
33                $img = new \Imagick($filename);
34
35                // The required function is available as from PECL imagick v2.2.2
36                if (method_exists($img, 'getImageCompressionQuality')) {
37                    $quality = $img->getImageCompressionQuality();
38                    if ($quality === 0) {
39                        // We have experienced that this Imagick method returns 0 for some images,
40                        // (even though the imagemagick binary is able to detect the quality)
41                        // ie "/test/images/quality-undetectable-with-imagick.jpg". See #208
42                        $quality = null;
43                    }
44                    return $quality;
45                }
46            } catch (\Exception $e) {
47                // Well well, it just didn't work out.
48                // - But perhaps next method will work...
49            } catch (\Throwable $e) {
50            }
51        }
52        return null;
53    }
54
55
56    /**
57     * Try to detect quality of jpeg using imagick binary.
58     *
59     * Note that the detection might fail for three different reasons:
60     * 1) exec function is not available
61     * 2) the 'identify' command is not available on the system
62     * 3) imagemagick for some reason fails to detect quality for some images
63     *
64     * In the first two cases, null is returned.
65     * In the third case, 92 is returned. This is what imagemagick returns when it cannot detect the quality.
66     *    and unfortunately we cannot distinguish between the situation where the quality is undetectable
67     *    and the situation where the quality is actually 92 (at least, I have not found a way to do so)
68     *
69     * @param  string  $filename  A complete file path to file to be examined
70     * @return int|null  Quality, or null if it was not possible to detect quality
71     */
72    private static function detectQualityOfJpgUsingImageMagick($filename)
73    {
74        if (ExecWithFallback::anyAvailable()) {
75            // Try Imagick using exec, and routing stderr to stdout (the "2>$1" magic)
76
77            try {
78                ExecWithFallback::exec(
79                    "identify -format '%Q' " . escapeshellarg($filename) . " 2>&1",
80                    $output,
81                    $returnCode
82                );
83                //echo 'out:' . print_r($output, true);
84                if ((intval($returnCode) == 0) && (is_array($output)) && (count($output) == 1)) {
85                    return intval($output[0]);
86                }
87            } catch (\Exception $e) {
88                // its ok, there are other fish in the sea
89            } catch (\Throwable $e) {
90            }
91        }
92        return null;
93    }
94
95
96    /**
97     * Try to detect quality of jpeg using graphicsmagick binary.
98     *
99     * It seems that graphicsmagick is never able to detect the quality! - and always returns
100     * the default quality, which is 75.
101     * However, as this might be solved in future versions, the method might be useful one day.
102     * But we treat "75" as a failure to detect and shall return null in that case.
103     *
104     * @param  string  $filename  A complete file path to file to be examined
105     * @return int|null  Quality, or null if it was not possible to detect quality
106     */
107    private static function detectQualityOfJpgUsingGraphicsMagick($filename)
108    {
109        if (ExecWithFallback::anyAvailable()) {
110            // Try GraphicsMagick
111            try {
112                ExecWithFallback::exec(
113                    "gm identify -format '%Q' " . escapeshellarg($filename) . " 2>&1",
114                    $output,
115                    $returnCode
116                );
117                if ((intval($returnCode) == 0) && (is_array($output)) && (count($output) == 1)) {
118                    $quality = intval($output[0]);
119
120                    // It seems that graphicsmagick is (currently) never able to detect the quality!
121                    // - and always returns 75 as a fallback
122                    // We shall therefore treat 75 as a failure to detect. (#209)
123                    if ($quality == 75) {
124                        return null;
125                    }
126                    return $quality;
127                }
128            } catch (\Exception $e) {
129            } catch (\Throwable $e) {
130            }
131        }
132        return null;
133    }
134
135
136    /**
137     * Try to detect quality of jpeg.
138     *
139     * Note: This method does not throw errors, but might dispatch warnings.
140     * You can use the WarningsIntoExceptions class if it is critical to you that nothing gets "printed"
141     *
142     * @param  string  $filename  A complete file path to file to be examined
143     * @return int|null  Quality, or null if it was not possible to detect quality
144     */
145    public static function detectQualityOfJpg($filename)
146    {
147
148        //trigger_error('warning test', E_USER_WARNING);
149
150        // Test that file exists in order not to break things.
151        if (!file_exists($filename)) {
152            // One could argue that it would be better to throw an Exception...?
153            return null;
154        }
155
156        // Try Imagick extension, if available
157        $quality = self::detectQualityOfJpgUsingImagick($filename);
158
159        if (is_null($quality)) {
160            $quality = self::detectQualityOfJpgUsingImageMagick($filename);
161        }
162
163        if (is_null($quality)) {
164            $quality = self::detectQualityOfJpgUsingGraphicsMagick($filename);
165        }
166
167        return $quality;
168    }
169}