Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.00% covered (warning)
56.00%
42 / 75
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Stack
56.00% covered (warning)
56.00%
42 / 75
60.00% covered (warning)
60.00%
3 / 5
73.07
0.00% covered (danger)
0.00%
0 / 1
 getUnsupportedDefaultOptions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUniqueOptions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getAvailableConverters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkOperationality
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 doActualConvert
51.52% covered (warning)
51.52%
34 / 66
0.00% covered (danger)
0.00%
0 / 1
60.15
1<?php
2
3namespace WebPConvert\Convert\Converters;
4
5use WebPConvert\Convert\ConverterFactory;
6use WebPConvert\Convert\Converters\AbstractConverter;
7use WebPConvert\Convert\Exceptions\ConversionFailedException;
8use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperationalException;
9use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
10use WebPConvert\Convert\Exceptions\ConversionFailed\ConversionSkippedException;
11use WebPConvert\Options\OptionFactory;
12
13//use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\TargetNotFoundException;
14
15/**
16 * Convert images to webp by trying a stack of converters until success.
17 *
18 * @package    WebPConvert
19 * @author     Bjørn Rosell <it@rosell.dk>
20 * @since      Class available since Release 2.0.0
21 */
22class Stack extends AbstractConverter
23{
24
25    protected function getUnsupportedDefaultOptions()
26    {
27        return [
28            'alpha-quality',
29            'auto-filter',
30            'encoding',
31            'low-memory',
32            'metadata',
33            'method',
34            'near-lossless',
35            'preset',
36            'sharp-yuv',
37            'size-in-percentage',
38            'skip',
39            'default-quality',
40            'quality',
41            'max-quality',
42        ];
43    }
44
45    public function getUniqueOptions($imageType)
46    {
47        return OptionFactory::createOptions([
48            ['converters', 'array', [
49                'title' => 'Converters',
50                'description' => 'Converters to try, ordered by priority.',
51                'default' => self::getAvailableConverters(),
52                'sensitive' => true,
53                'ui' => [
54                    'component' => 'multi-select',
55                    'options' => self::getAvailableConverters(),
56                    'advanced' => true
57                ]
58            ]],
59            ['converter-options', 'array', [
60                'title' => 'Converter options',
61                'description' =>
62                    'Extra options for specific converters.',
63                'default' => [],
64                'sensitive' => true,
65                'ui' => null
66            ]],
67            ['preferred-converters', 'array', [
68                'title' => 'Preferred converters',
69                'description' =>
70                    'With this option you can move specified converters to the top of the stack. ' .
71                    'The converters are specified by id.',
72                'default' => [],
73                'ui' => null
74            ]],
75            ['extra-converters', 'array', [
76                'title' => 'Extra converters',
77                'description' =>
78                    'Add extra converters to the bottom of the stack',
79                'default' => [],
80                'sensitive' => true,
81                'ui' => null
82            ]],
83            ['shuffle', 'boolean', [
84                'title' => 'Shuffle',
85                'description' =>
86                    'Shuffles the converter order on each conversion. ' .
87                    'Can for example be used to spread out requests on multiple cloud converters',
88                'default' => false,
89                'ui' => [
90                    'component' => 'checkbox',
91                    'advanced' => true
92                ]
93            ]],
94        ]);
95
96
97/*
98        return [
99            new SensitiveArrayOption('converters', self::getAvailableConverters()),
100            new SensitiveArrayOption('converter-options', []),
101            new BooleanOption('shuffle', false),
102            new ArrayOption('preferred-converters', []),
103            new SensitiveArrayOption('extra-converters', [])
104        ];*/
105    }
106
107    /**
108     * Get available converters (ids) - ordered by awesomeness.
109     *
110     * @return  array  An array of ids of converters that comes with this library
111     */
112    public static function getAvailableConverters()
113    {
114        return [
115            'cwebp', 'vips', 'imagick', 'gmagick', 'imagemagick', 'graphicsmagick', 'wpc', 'ffmpeg', 'ewww', 'gd'
116        ];
117    }
118
119    /**
120     * Check (general) operationality of imagack converter executable
121     *
122     * @throws SystemRequirementsNotMetException  if system requirements are not met
123     */
124    public function checkOperationality()
125    {
126        if (count($this->options['converters']) == 0) {
127            throw new ConverterNotOperationalException(
128                'Converter stack is empty! - no converters to try, no conversion can be made!'
129            );
130        }
131
132        // TODO: We should test if all converters are found in order to detect problems early
133
134        //$this->logLn('Stack converter ignited');
135    }
136
137    protected function doActualConvert()
138    {
139        $options = $this->options;
140
141        $beginTimeStack = microtime(true);
142
143        $anyRuntimeErrors = false;
144
145        $converters = $options['converters'];
146        if (count($options['extra-converters']) > 0) {
147            $converters = array_merge($converters, $options['extra-converters']);
148            /*foreach ($options['extra-converters'] as $extra) {
149                $converters[] = $extra;
150            }*/
151        }
152
153        // preferred-converters
154        if (count($options['preferred-converters']) > 0) {
155            foreach (array_reverse($options['preferred-converters']) as $prioritizedConverter) {
156                foreach ($converters as $i => $converter) {
157                    if (is_array($converter)) {
158                        $converterId = $converter['converter'];
159                    } else {
160                        $converterId = $converter;
161                    }
162                    if ($converterId == $prioritizedConverter) {
163                        unset($converters[$i]);
164                        array_unshift($converters, $converter);
165                        break;
166                    }
167                }
168            }
169            // perhaps write the order to the log? (without options) - but this requires some effort
170        }
171
172        // shuffle
173        if ($options['shuffle']) {
174            shuffle($converters);
175        }
176
177        //$this->logLn(print_r($converters));
178        //$options['converters'] = $converters;
179        //$defaultConverterOptions = $options;
180        $defaultConverterOptions = [];
181
182        foreach ($this->options2->getOptionsMap() as $id => $option) {
183            // Right here, there used to be a check that ensured that unknown options was not passed down to the
184            // converters (" && !($option instanceof GhostOption)"). But well, as the Stack doesn't know about
185            // converter specific options, such as "try-cwebp", these was not passed down (see #259)
186            // I'm not sure why the check was made in the first place, but it does not seem neccessary, as the
187            // converters simply ignore unknown options. So the check has now been removed.
188            if ($option->isValueExplicitlySet()) {
189                $defaultConverterOptions[$id] = $option->getValue();
190            }
191        }
192
193        //unset($defaultConverterOptions['converters']);
194        //unset($defaultConverterOptions['converter-options']);
195        $defaultConverterOptions['_skip_input_check'] = true;
196        $defaultConverterOptions['_suppress_success_message'] = true;
197        unset($defaultConverterOptions['converters']);
198        unset($defaultConverterOptions['extra-converters']);
199        unset($defaultConverterOptions['converter-options']);
200        unset($defaultConverterOptions['preferred-converters']);
201        unset($defaultConverterOptions['shuffle']);
202
203//        $this->logLn('converters: ' . print_r($converters, true));
204
205        //return;
206        foreach ($converters as $converter) {
207            if (is_array($converter)) {
208                $converterId = $converter['converter'];
209                $converterOptions = isset($converter['options']) ? $converter['options'] : [];
210            } else {
211                $converterId = $converter;
212                $converterOptions = [];
213                if (isset($options['converter-options'][$converterId])) {
214                    // Note: right now, converter-options are not meant to be used,
215                    //       when you have several converters of the same type
216                    $converterOptions = $options['converter-options'][$converterId];
217                }
218            }
219            $converterOptions = array_merge($defaultConverterOptions, $converterOptions);
220            /*
221            if ($converterId != 'stack') {
222                //unset($converterOptions['converters']);
223                //unset($converterOptions['converter-options']);
224            } else {
225                //$converterOptions['converter-options'] =
226                $this->logLn('STACK');
227                $this->logLn('converterOptions: ' . print_r($converterOptions, true));
228            }*/
229
230            $beginTime = microtime(true);
231
232            $this->ln();
233            $this->logLn($converterId . ' converter ignited', 'bold');
234
235            $converter = ConverterFactory::makeConverter(
236                $converterId,
237                $this->source,
238                $this->destination,
239                $converterOptions,
240                $this->logger
241            );
242
243            try {
244                $converter->doConvert();
245
246                //self::runConverterWithTiming($converterId, $source, $destination, $converterOptions, false, $logger);
247
248                $this->logLn($converterId . ' succeeded :)');
249                //throw new ConverterNotOperationalException('...');
250                return;
251            } catch (ConverterNotOperationalException $e) {
252                $this->logLn($e->getMessage());
253            } catch (ConversionSkippedException $e) {
254                $this->logLn($e->getMessage());
255            } catch (ConversionFailedException $e) {
256                $this->logLn($e->getMessage(), 'italic');
257                $prev = $e->getPrevious();
258                if (!is_null($prev)) {
259                    $this->logLn($prev->getMessage(), 'italic');
260                    $this->logLn(' in ' . $prev->getFile() . ', line ' . $prev->getLine(), 'italic');
261                    $this->ln();
262                }
263                //$this->logLn($e->getTraceAsString());
264                $anyRuntimeErrors = true;
265            }
266            $this->logLn($converterId . ' failed in ' . round((microtime(true) - $beginTime) * 1000) . ' ms');
267        }
268
269        $this->ln();
270        $this->logLn('Stack failed in ' . round((microtime(true) - $beginTimeStack) * 1000) . ' ms');
271
272        // Hm, Scrutinizer complains that $anyRuntimeErrors is always false. But that is not true!
273        if ($anyRuntimeErrors) {
274            // At least one converter failed
275            throw new ConversionFailedException(
276                'None of the converters in the stack could convert the image.'
277            );
278        } else {
279            // All converters threw a SystemRequirementsNotMetException
280            throw new ConverterNotOperationalException('None of the converters in the stack are operational');
281        }
282    }
283}