├── .editorconfig ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── bin └── phpunit.bat ├── composer.json ├── src ├── ApiRequestException.php ├── InsightsCaller.php ├── InsightsResponse.php ├── InvalidJsonException.php ├── Result │ ├── InsightsException.php │ ├── InsightsResult.php │ ├── InsightsResultException.php │ ├── Map │ │ ├── FormattedResults.php │ │ ├── FormattedResults │ │ │ ├── AbstractRuleResult.php │ │ │ ├── Arg.php │ │ │ ├── ArgException.php │ │ │ ├── ArgTypeInterface.php │ │ │ ├── DefaultRuleResult.php │ │ │ ├── FormatException.php │ │ │ ├── FormattedBlock.php │ │ │ ├── Header.php │ │ │ ├── Summary.php │ │ │ ├── Url.php │ │ │ ├── UrlBlock.php │ │ │ └── UrlResult.php │ │ ├── PageStats.php │ │ ├── RuleGroup.php │ │ └── Screenshot.php │ ├── ScreenshotNotAvailableException.php │ └── UsabilityScoreNotAvailableException.php └── autoload.php └── tests ├── InsightsCallerTest.php ├── InsightsResponseTest.php ├── Result └── InsightsResultTest.php └── example-com-response.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending for every file 7 | # Indent with 4 spaces 8 | [php] 9 | end_of_line = lf 10 | indent_style = space 11 | indent_size = 4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | phpunit.xml 2 | composer.phar 3 | composer.lock 4 | vendor/ 5 | example/ 6 | bin/phpunit.bat 7 | .idea -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for taking the time to help us - Contributions are **welcome** and will be fully **credited**. 4 | 5 | We do all our work on GitHub. If you'd like to help, you can create a 6 | [free GitHub account here](https://github.com/join). 7 | 8 | ## Reporting an issue 9 | 10 | For bug reports or feature requests, [please create a new issue](https://github.com/dsentker/phpinsights/issues). 11 | 12 | Before filing an issue: 13 | 14 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 15 | - Check to make sure your feature suggestion isn't already present within the project. 16 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 17 | - Check the pull requests tab to ensure that the feature isn't already in progress. 18 | 19 | ## Submitting changes 20 | 21 | The best way to submit a bug fix or improvement is through a [pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/). 22 | 23 | **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Daniel Sentker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhpInsights 2 | 3 | An easy-to-use API Wrapper for [Googles PageSpeed Insights](https://developers.google.com/speed/docs/insights/v2/reference/pagespeedapi/runpagespeed). The JSON response is mapped to objects for an headache-free usage. 4 | 5 | ## Installation 6 | 1. Get an api key from the google developer console for [Page Speed Insights](https://console.developers.google.com/apis/api/pagespeedonline-json.googleapis.com/overview). 7 | 2. ```composer require dsentker/phpinsights``` 8 | 3. Have fun with this library. 9 | 10 | ## Usage 11 | 12 | ### Simple Usage 13 | ```php 14 | $url = 'http://example.com'; 15 | 16 | $caller = new \PhpInsights\InsightsCaller('your-google-api-key-here', 'de'); 17 | $response = $caller->getResponse($url, \PhpInsights\InsightsCaller::STRATEGY_MOBILE); 18 | $result = $response->getMappedResult(); 19 | 20 | var_dump($result->getSpeedScore()); // 100 21 | var_dump($result->getUsabilityScore()); // 100 22 | ``` 23 | 24 | ### Using Concurrent Requests 25 | ```php 26 | $urls = array( 27 | 'http://example.com', 28 | 'http://example2.com', 29 | 'http://example3.com' 30 | ); 31 | 32 | $caller = new \PhpInsights\InsightsCaller('your-google-api-key-here', 'fr'); 33 | $responses = $caller->getResponses($urls, \PhpInsights\InsightsCaller::STRATEGY_MOBILE); 34 | 35 | foreach ($responses as $url => $response) { 36 | $result = $response->getMappedResult(); 37 | 38 | var_dump($result->getSpeedScore()); // 100 39 | var_dump($result->getUsabilityScore()); // 100 40 | } 41 | ``` 42 | 43 | ### Result details 44 | #### Full result 45 | ```php 46 | /** @var \PhpInsights\Result\InsightsResult $result */ 47 | foreach($result->getFormattedResults()->getRuleResults() as $rule => $ruleResult) { 48 | 49 | /* 50 | * If the rule impact is zero, it means that the website has passed the test. 51 | */ 52 | if($ruleResult->getRuleImpact() > 0) { 53 | 54 | var_dump($rule); // AvoidLandingPageRedirects 55 | var_dump($ruleResult->getLocalizedRuleName()); // "Zielseiten-Weiterleitungen vermeiden" 56 | 57 | /* 58 | * The getDetails() method is a wrapper to get the `summary` field as well as `Urlblocks` data. You 59 | * can use $ruleResult->getUrlBlocks() and $ruleResult->getSummary() instead. 60 | */ 61 | foreach($ruleResult->getDetails() as $block) { 62 | var_dump($block->toString()); // "Auf Ihrer Seite sind keine Weiterleitungen vorhanden" 63 | } 64 | 65 | } 66 | 67 | } 68 | ``` 69 | #### Result details by Rule group 70 | ```php 71 | /** @var \PhpInsights\Result\InsightsResult $result */ 72 | foreach($result->getFormattedResults()->getRuleResultsByGroup(RuleGroup::GROUP_SPEED) as $rule => $ruleResult) { 73 | $ruleResult->getSummary()->toString(); 74 | } 75 | ``` 76 | 77 | ### Screenshot 78 | ```php 79 | print $result->screenshot->getImageHtml(); // html image element 80 | print $result->screenshot->getData(); // base64 screenshot representation 81 | ``` 82 | 83 | ## Testing 84 | ``` $ phpunit --bootstrap "path/to/phpinsights/src/autoload.php"``` 85 | 86 | ## Credits 87 | * [Daniel Sentker](https://github.com/dsentker) 88 | * [Nils](https://github.com/nlzet) 89 | * [Joe Dawson](https://github.com/JoeDawson) 90 | * [tlafon](https://github.com/tlafon) 91 | * [baileyherbert](https://github.com/baileyherbert) 92 | 93 | 94 | ## Submitting bugs and feature requests 95 | Bugs and feature request are tracked on GitHub. 96 | 97 | ## ToDo 98 | * Write more tests 99 | * Improve my english skills 100 | 101 | ## External Libraries 102 | This library depends on [JsonMapper by cweiske](https://github.com/cweiske/jsonmapper) to map json fields to php objects and [Guzzle](https://github.com/guzzle/guzzle) (surprise!). 103 | 104 | ## Copyright and license 105 | PhpInsights is licensed for use under the MIT License (MIT). Please see LICENSE for more information. 106 | -------------------------------------------------------------------------------- /bin/phpunit.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | vendor/bin/phpunit --bootstrap "src\autoload.php" --report-useless-tests 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dsentker/phpinsights", 3 | "description": "A php wrapper for Googles page speed insights", 4 | "version": "0.2.3", 5 | "require": { 6 | "php": "^5.4 || ^7.0", 7 | "netresearch/jsonmapper": "^1.1", 8 | "guzzlehttp/guzzle": "^6.2" 9 | }, 10 | "require-dev": { 11 | "phpunit/phpunit": "^5.7" 12 | }, 13 | "autoload": { 14 | "psr-4": {"PhpInsights\\": "src"} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ApiRequestException.php: -------------------------------------------------------------------------------- 1 | client = new Client($config); 39 | $this->apiKey = $apiKey; 40 | $this->locale = $locale; 41 | $this->captureScreenshot = true; 42 | 43 | } 44 | 45 | /** 46 | * @param string $url 47 | * @param string $strategy 48 | * 49 | * @return InsightsResponse 50 | */ 51 | public function __invoke($url, $strategy = self::STRATEGY_MOBILE) 52 | { 53 | return $this->getResponse($url, $strategy); 54 | } 55 | 56 | 57 | /** 58 | * @param string $url 59 | * @param string $strategy 60 | * 61 | * @return InsightsResponse 62 | * 63 | * @throws ApiRequestException 64 | */ 65 | public function getResponse($url, $strategy = 'mobile') 66 | { 67 | $apiEndpoint = $this->createApiEndpointUrl($url, $strategy); 68 | 69 | try { 70 | $response = $this->client->request('GET', $apiEndpoint); 71 | } catch (TransferException $e) { 72 | throw new ApiRequestException($e->getMessage()); 73 | } 74 | 75 | return InsightsResponse::fromResponse($response); 76 | 77 | } 78 | 79 | /** 80 | * @param array $urls 81 | * @param string $strategy 82 | * 83 | * @return InsightsResponse 84 | * 85 | * @throws ApiRequestException 86 | */ 87 | public function getResponses(array $urls, $strategy = 'mobile') 88 | { 89 | 90 | try { 91 | $promises = array(); 92 | 93 | foreach ($urls as $k=>$url) { 94 | $apiEndpoint = $this->createApiEndpointUrl($url, $strategy); 95 | $promises[$k] = $this->client->getAsync($apiEndpoint); 96 | } 97 | 98 | $results = Promise\unwrap($promises); 99 | $results = Promise\settle($promises)->wait(); 100 | 101 | $responses = array(); 102 | 103 | foreach ($urls as $k=>$url) { 104 | $response = $results[$k]['value']; 105 | $responses[$url] = InsightsResponse::fromResponse($response); 106 | } 107 | 108 | 109 | } catch (TransferException $e) { 110 | throw new ApiRequestException($e->getMessage()); 111 | } 112 | 113 | return $responses; 114 | 115 | } 116 | 117 | /** 118 | * @return boolean 119 | */ 120 | public function isCaptureScreenshot() 121 | { 122 | return $this->captureScreenshot; 123 | } 124 | 125 | /** 126 | * @param boolean $captureScreenshot 127 | */ 128 | public function setCaptureScreenshot($captureScreenshot) 129 | { 130 | $this->captureScreenshot = $captureScreenshot; 131 | } 132 | 133 | 134 | /** 135 | * @param string $url 136 | * @param string $strategy 137 | * 138 | * @return string 139 | */ 140 | protected function createApiEndpointUrl($url, $strategy = 'mobile') 141 | { 142 | $screenshot = ($this->isCaptureScreenshot()) ? 'true' : 'false'; 143 | 144 | return sprintf(self::GI_API_ENDPOINT, $url, $strategy, $this->apiKey, $this->locale, $screenshot); 145 | } 146 | 147 | 148 | } -------------------------------------------------------------------------------- /src/InsightsResponse.php: -------------------------------------------------------------------------------- 1 | rawJsonResponse = $jsonResponse; 25 | $this->decodedResponse = static::validateResponse($jsonResponse); 26 | } 27 | 28 | /** 29 | * @param string $json 30 | * 31 | * @return \stdClass 32 | * 33 | * @throws InvalidJsonException 34 | */ 35 | public static function validateResponse($json) 36 | { 37 | 38 | $result = json_decode($json); 39 | 40 | // switch and check possible JSON errors 41 | switch (json_last_error()) { 42 | case JSON_ERROR_NONE: 43 | return $result; 44 | break; 45 | case JSON_ERROR_DEPTH: 46 | $error = 'The maximum stack depth has been exceeded.'; 47 | break; 48 | case JSON_ERROR_STATE_MISMATCH: 49 | $error = 'Invalid or malformed JSON.'; 50 | break; 51 | case JSON_ERROR_CTRL_CHAR: 52 | $error = 'Control character error, possibly incorrectly encoded.'; 53 | break; 54 | case JSON_ERROR_SYNTAX: 55 | $error = 'Syntax error, malformed JSON.'; 56 | break; 57 | // PHP >= 5.3.3 58 | case JSON_ERROR_UTF8: 59 | $error = 'Malformed UTF-8 characters, possibly incorrectly encoded.'; 60 | break; 61 | // PHP >= 5.5.0 62 | case JSON_ERROR_RECURSION: 63 | $error = 'One or more recursive references in the value to be encoded.'; 64 | break; 65 | // PHP >= 5.5.0 66 | case JSON_ERROR_INF_OR_NAN: 67 | $error = 'One or more NAN or INF values in the value to be encoded.'; 68 | break; 69 | case JSON_ERROR_UNSUPPORTED_TYPE: 70 | $error = 'A value of a type that cannot be encoded was given.'; 71 | break; 72 | default: 73 | $error = 'Unknown JSON error occured.'; 74 | break; 75 | } 76 | 77 | throw new InvalidJsonException($error); 78 | } 79 | 80 | /** 81 | * @param ResponseInterface $response 82 | * 83 | * @return InsightsResponse 84 | */ 85 | public static function fromResponse(ResponseInterface $response) 86 | { 87 | return new static($response->getBody()->getContents()); 88 | } 89 | 90 | /** 91 | * @param string $json 92 | * 93 | * @return InsightsResponse 94 | */ 95 | public static function fromJson($json) 96 | { 97 | return new static($json); 98 | } 99 | 100 | /** 101 | * @return InsightsResult 102 | */ 103 | public function getMappedResult() 104 | { 105 | 106 | $mapper = new \JsonMapper(); 107 | 108 | /** @var InsightsResult $map */ 109 | $map = $mapper->map($this->decodedResponse, new InsightsResult()); 110 | 111 | return $map; 112 | } 113 | 114 | /** 115 | * @return string 116 | */ 117 | public function getRawResult() 118 | { 119 | return $this->rawJsonResponse; 120 | } 121 | 122 | } -------------------------------------------------------------------------------- /src/InvalidJsonException.php: -------------------------------------------------------------------------------- 1 | id; 44 | } 45 | 46 | /** 47 | * @param string $id 48 | */ 49 | public function setId($id) 50 | { 51 | $this->id = $id; 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getTitle() 58 | { 59 | return $this->title; 60 | } 61 | 62 | /** 63 | * @param string $title 64 | */ 65 | public function setTitle($title) 66 | { 67 | $this->title = $title; 68 | } 69 | 70 | /** 71 | * @return string 72 | */ 73 | public function getKind() 74 | { 75 | return $this->kind; 76 | } 77 | 78 | /** 79 | * @param string $kind 80 | */ 81 | public function setKind($kind) 82 | { 83 | $this->kind = $kind; 84 | } 85 | 86 | /** 87 | * @return int 88 | */ 89 | public function getResponseCode() 90 | { 91 | return $this->responseCode; 92 | } 93 | 94 | /** 95 | * @param int $responseCode 96 | */ 97 | public function setResponseCode($responseCode) 98 | { 99 | $this->responseCode = $responseCode; 100 | } 101 | 102 | /** 103 | * @param FormattedResults $formattedResults 104 | */ 105 | public function setFormattedResults(FormattedResults $formattedResults) 106 | { 107 | $this->formattedResults = $formattedResults; 108 | } 109 | 110 | /** 111 | * @return Map\RuleGroup[] 112 | */ 113 | public function getRuleGroups() 114 | { 115 | return empty($this->ruleGroups) 116 | ? [] 117 | : $this->ruleGroups; 118 | } 119 | 120 | /** 121 | * @return Map\PageStats 122 | */ 123 | public function getPageStats() 124 | { 125 | return $this->pageStats; 126 | } 127 | 128 | /** 129 | * @return Map\FormattedResults 130 | */ 131 | public function getFormattedResults() 132 | { 133 | return $this->formattedResults; 134 | } 135 | 136 | /** 137 | * @return \stdClass 138 | */ 139 | public function getVersion() 140 | { 141 | return $this->version; 142 | } 143 | 144 | /** 145 | * @return int 146 | * 147 | * @throws UsabilityScoreNotAvailableException 148 | */ 149 | public function getUsabilityScore() 150 | { 151 | $ruleGroups = $this->getRuleGroups(); 152 | 153 | if (!array_key_exists(RuleGroup::GROUP_USABILITY, $ruleGroups)) { 154 | throw new UsabilityScoreNotAvailableException('Usability score is only available with mobile strategy API call.'); 155 | } 156 | 157 | return $ruleGroups[RuleGroup::GROUP_USABILITY]->getScore(); 158 | } 159 | 160 | /** 161 | * @return int 162 | */ 163 | public function getSpeedScore() 164 | { 165 | $ruleGroups = $this->getRuleGroups(); 166 | 167 | return $ruleGroups[RuleGroup::GROUP_SPEED]->getScore(); 168 | 169 | } 170 | 171 | /** 172 | * @return bool 173 | */ 174 | public function hasScreenshot() 175 | { 176 | return !empty($this->screenshot); 177 | } 178 | 179 | /** 180 | * @return Screenshot 181 | * 182 | * @throws ScreenshotNotAvailableException 183 | */ 184 | public function getScreenshot() 185 | { 186 | 187 | if (!$this->hasScreenshot()) { 188 | ScreenshotNotAvailableException::raise(); 189 | } 190 | 191 | return $this->screenshot; 192 | } 193 | 194 | 195 | } -------------------------------------------------------------------------------- /src/Result/InsightsResultException.php: -------------------------------------------------------------------------------- 1 | locale; 21 | } 22 | 23 | /** 24 | * @param string $locale 25 | */ 26 | public function setLocale($locale) 27 | { 28 | $this->locale = $locale; 29 | } 30 | 31 | /** 32 | * @return DefaultRuleResult[] 33 | */ 34 | public function getRuleResults() 35 | { 36 | return $this->ruleResults; 37 | } 38 | 39 | /** 40 | * @param \PhpInsights\Result\Map\FormattedResults\DefaultRuleResult[] $ruleResults 41 | */ 42 | public function setRuleResults($ruleResults) 43 | { 44 | $this->ruleResults = $ruleResults; 45 | } 46 | 47 | /** 48 | * @param string $group 49 | * 50 | * @return DefaultRuleResult[] 51 | */ 52 | public function getRuleResultsByGroup($group) 53 | { 54 | $results = []; 55 | foreach ($this->getRuleResults() as $rule => $ruleResult) { 56 | if (in_array($group, $ruleResult->getGroups())) { 57 | $results[$rule] = $ruleResult; 58 | } 59 | } 60 | 61 | return $results; 62 | } 63 | 64 | 65 | } -------------------------------------------------------------------------------- /src/Result/Map/FormattedResults/AbstractRuleResult.php: -------------------------------------------------------------------------------- 1 | localizedRuleName = $localizedRuleName; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getLocalizedRuleName() 34 | { 35 | return $this->localizedRuleName; 36 | } 37 | 38 | /** 39 | * @return float 40 | */ 41 | public function getRuleImpact() 42 | { 43 | return $this->ruleImpact; 44 | } 45 | 46 | /** 47 | * @param float $ruleImpact 48 | */ 49 | public function setRuleImpact($ruleImpact) 50 | { 51 | $this->ruleImpact = $ruleImpact; 52 | } 53 | 54 | /** 55 | * @return Summary 56 | */ 57 | public function getSummary() 58 | { 59 | return $this->summary; 60 | } 61 | 62 | /** 63 | * @param Summary $summary 64 | */ 65 | public function setSummary($summary) 66 | { 67 | $this->summary = $summary; 68 | } 69 | 70 | /** 71 | * @return bool 72 | */ 73 | public function hasSummary() 74 | { 75 | return !empty($this->summary); 76 | } 77 | 78 | /** 79 | * @return UrlBlock[] 80 | */ 81 | public function getUrlBlocks() 82 | { 83 | return $this->urlBlocks; 84 | } 85 | 86 | /** 87 | * @return bool 88 | */ 89 | public function hasUrlBlocks() 90 | { 91 | return !empty($this->urlBlocks) && is_array($this->urlBlocks); 92 | } 93 | 94 | /** 95 | * @return FormattedBlock[] 96 | */ 97 | public function getDetails() 98 | { 99 | 100 | $details = []; 101 | 102 | if($this->hasUrlBlocks()) { 103 | foreach($this->getUrlBlocks() as $urlBlock) { 104 | $details[] = $urlBlock->header; 105 | foreach($urlBlock->getUrls() as $url) { 106 | $details[] = $url->result; 107 | } 108 | } 109 | } 110 | 111 | if($this->hasSummary()) { 112 | $details[] = $this->getSummary(); 113 | } 114 | 115 | return $details; 116 | 117 | } 118 | 119 | /** 120 | * @return array 121 | */ 122 | public function getGroups() 123 | { 124 | return $this->groups; 125 | } 126 | 127 | /** 128 | * @param array $groups 129 | */ 130 | public function setGroups($groups) 131 | { 132 | $this->groups = $groups; 133 | } 134 | 135 | 136 | /** 137 | * @return string 138 | */ 139 | public function toString() 140 | { 141 | 142 | return sprintf('%s (Impact %s)', $this->getLocalizedRuleName(), $this->getRuleImpact()); 143 | } 144 | 145 | /** 146 | * @return string 147 | */ 148 | public function __toString() 149 | { 150 | return $this->toString(); 151 | } 152 | 153 | 154 | 155 | } -------------------------------------------------------------------------------- /src/Result/Map/FormattedResults/Arg.php: -------------------------------------------------------------------------------- 1 | type; 23 | } 24 | 25 | /** 26 | * @param string $type 27 | */ 28 | public function setType($type) 29 | { 30 | $this->type = $type; 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getKey() 37 | { 38 | return $this->key; 39 | } 40 | 41 | /** 42 | * @param string $key 43 | */ 44 | public function setKey($key) 45 | { 46 | $this->key = $key; 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getValue() 53 | { 54 | return $this->value; 55 | } 56 | 57 | /** 58 | * @param string $value 59 | */ 60 | public function setValue($value) 61 | { 62 | $this->value = $value; 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /src/Result/Map/FormattedResults/ArgException.php: -------------------------------------------------------------------------------- 1 | toString(null); 22 | } 23 | 24 | /** 25 | * @return \Closure 26 | */ 27 | protected static function getDefaultLinkFormatter() { 28 | return function(Arg $arg, $format) { 29 | return strtr($format, [ 30 | '{{BEGIN_LINK}}' => sprintf('', $arg->getValue()), 31 | '{{END_LINK}}' => '', 32 | ]); 33 | }; 34 | } 35 | 36 | /** 37 | * @return \Closure 38 | */ 39 | protected static function getRemoveLinkFormatter() { 40 | return function(Arg $arg, $format) { 41 | return strtr($format, [ 42 | '{{BEGIN_LINK}}' => '', 43 | '{{END_LINK}}' => '', 44 | ]); 45 | }; 46 | } 47 | 48 | /** 49 | * @return \Closure 50 | */ 51 | protected static function getPlaceholderFormatter() { 52 | return function(Arg $arg, $format) { 53 | $placeholder = sprintf("{{%s}}", $arg->getKey()); 54 | return str_replace($placeholder, $arg->getValue(), $format); 55 | }; 56 | } 57 | 58 | /** 59 | * @param \Closure $linkFormatterCallback 60 | * 61 | * @return string 62 | * 63 | * @throws ArgException 64 | * @throws FormatException 65 | */ 66 | public function toString(\Closure $linkFormatterCallback = null) 67 | { 68 | 69 | $format = $this->getFormat(); 70 | 71 | $linkFormatter = (null !== $linkFormatterCallback) ? $linkFormatterCallback : self::getDefaultLinkFormatter(); 72 | $placeholderFormatter = self::getPlaceholderFormatter(); 73 | 74 | foreach ($this->getArgs() as $arg) { 75 | switch ($arg->getType()) { 76 | case ArgTypes::ARG_TYPE_HYPERLINK: 77 | $format = $linkFormatter($arg, $format); 78 | break; 79 | case ArgTypes::ARG_TYPE_BYTES: 80 | case ArgTypes::ARG_TYPE_DISTANCE: 81 | case ArgTypes::ARG_TYPE_DURATION: 82 | case ArgTypes::ARG_TYPE_INT_LITERAL: 83 | case ArgTypes::ARG_TYPE_PERCENTAGE: 84 | case ArgTypes::ARG_TYPE_SNAPSHOT_RECT: 85 | case ArgTypes::ARG_TYPE_STRING_LITERAL: 86 | case ArgTypes::ARG_TYPE_URL: 87 | case ArgTypes::ARG_TYPE_VERBATIM_STRING: 88 | $format = $placeholderFormatter($arg, $format); 89 | break; 90 | default: 91 | throw new ArgException(sprintf('Unknown argument type: "%s"!', $arg->getType())); 92 | } 93 | } 94 | 95 | return $format; 96 | } 97 | 98 | /** 99 | * @return string 100 | */ 101 | public function getFormat() 102 | { 103 | return $this->format; 104 | } 105 | 106 | /** 107 | * @param string $format 108 | */ 109 | public function setFormat($format) 110 | { 111 | $this->format = $format; 112 | } 113 | 114 | /** 115 | * @return Arg[] 116 | */ 117 | public function getArgs() 118 | { 119 | return is_array($this->args) 120 | ? $this->args 121 | : []; 122 | } 123 | 124 | /** 125 | * @param Arg[] $args 126 | */ 127 | public function setArgs($args) 128 | { 129 | $this->args = $args; 130 | } 131 | 132 | 133 | } -------------------------------------------------------------------------------- /src/Result/Map/FormattedResults/Header.php: -------------------------------------------------------------------------------- 1 | urls) && is_array($this->urls)) 21 | ? $this->urls 22 | : []; 23 | } 24 | 25 | 26 | 27 | 28 | } -------------------------------------------------------------------------------- /src/Result/Map/FormattedResults/UrlResult.php: -------------------------------------------------------------------------------- 1 | numberResources; 47 | } 48 | 49 | /** 50 | * @param int $numberResources 51 | */ 52 | public function setNumberResources($numberResources) 53 | { 54 | $this->numberResources = $numberResources; 55 | } 56 | 57 | /** 58 | * @return int 59 | */ 60 | public function getNumberHosts() 61 | { 62 | return $this->numberHosts; 63 | } 64 | 65 | /** 66 | * @param int $numberHosts 67 | */ 68 | public function setNumberHosts($numberHosts) 69 | { 70 | $this->numberHosts = $numberHosts; 71 | } 72 | 73 | /** 74 | * @return int 75 | */ 76 | public function getTotalRequestBytes() 77 | { 78 | return $this->totalRequestBytes; 79 | } 80 | 81 | /** 82 | * @param int $totalRequestBytes 83 | */ 84 | public function setTotalRequestBytes($totalRequestBytes) 85 | { 86 | $this->totalRequestBytes = $totalRequestBytes; 87 | } 88 | 89 | /** 90 | * @return int 91 | */ 92 | public function getNumberStaticResources() 93 | { 94 | return $this->numberStaticResources; 95 | } 96 | 97 | /** 98 | * @param int $numberStaticResources 99 | */ 100 | public function setNumberStaticResources($numberStaticResources) 101 | { 102 | $this->numberStaticResources = $numberStaticResources; 103 | } 104 | 105 | /** 106 | * @return int 107 | */ 108 | public function getHtmlResponseBytes() 109 | { 110 | return $this->htmlResponseBytes; 111 | } 112 | 113 | /** 114 | * @param int $htmlResponseBytes 115 | */ 116 | public function setHtmlResponseBytes($htmlResponseBytes) 117 | { 118 | $this->htmlResponseBytes = $htmlResponseBytes; 119 | } 120 | 121 | /** 122 | * @return int 123 | */ 124 | public function getCssResponseBytes() 125 | { 126 | return $this->cssResponseBytes; 127 | } 128 | 129 | /** 130 | * @param int $cssResponseBytes 131 | */ 132 | public function setCssResponseBytes($cssResponseBytes) 133 | { 134 | $this->cssResponseBytes = $cssResponseBytes; 135 | } 136 | 137 | /** 138 | * @return int 139 | */ 140 | public function getImageResponseBytes() 141 | { 142 | return $this->imageResponseBytes; 143 | } 144 | 145 | /** 146 | * @param int $imageResponseBytes 147 | */ 148 | public function setImageResponseBytes($imageResponseBytes) 149 | { 150 | $this->imageResponseBytes = $imageResponseBytes; 151 | } 152 | 153 | /** 154 | * @return int 155 | */ 156 | public function getJavascriptResponseBytes() 157 | { 158 | return $this->javascriptResponseBytes; 159 | } 160 | 161 | /** 162 | * @param int $javascriptResponseBytes 163 | */ 164 | public function setJavascriptResponseBytes($javascriptResponseBytes) 165 | { 166 | $this->javascriptResponseBytes = $javascriptResponseBytes; 167 | } 168 | 169 | /** 170 | * @return int 171 | */ 172 | public function getOtherResponseBytes() 173 | { 174 | return $this->otherResponseBytes; 175 | } 176 | 177 | /** 178 | * @param int $otherResponseBytes 179 | */ 180 | public function setOtherResponseBytes($otherResponseBytes) 181 | { 182 | $this->otherResponseBytes = $otherResponseBytes; 183 | } 184 | 185 | /** 186 | * @return int 187 | */ 188 | public function getNumberJsResources() 189 | { 190 | return $this->numberJsResources; 191 | } 192 | 193 | /** 194 | * @param int $numberJsResources 195 | */ 196 | public function setNumberJsResources($numberJsResources) 197 | { 198 | $this->numberJsResources = $numberJsResources; 199 | } 200 | 201 | /** 202 | * @return int 203 | */ 204 | public function getNumberCssResources() 205 | { 206 | return $this->numberCssResources; 207 | } 208 | 209 | /** 210 | * @param int $numberCssResources 211 | */ 212 | public function setNumberCssResources($numberCssResources) 213 | { 214 | $this->numberCssResources = $numberCssResources; 215 | } 216 | 217 | 218 | } -------------------------------------------------------------------------------- /src/Result/Map/RuleGroup.php: -------------------------------------------------------------------------------- 1 | score; 19 | } 20 | 21 | /** 22 | * @param int $score 23 | */ 24 | public function setScore($score) 25 | { 26 | $this->score = $score; 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/Result/Map/Screenshot.php: -------------------------------------------------------------------------------- 1 | data, [ 29 | '-' => '+', 30 | '_' => '/', 31 | ]); 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getMimeType() 38 | { 39 | return $this->mime_type; 40 | } 41 | 42 | /** 43 | * @param string $alt 44 | * 45 | * @return string 46 | */ 47 | public function getImageHtml($alt = '') 48 | { 49 | return sprintf('%s', $this->getMimeType(), $this->getData(), $alt); 50 | } 51 | 52 | 53 | } -------------------------------------------------------------------------------- /src/Result/ScreenshotNotAvailableException.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('\PhpInsights\InsightsCaller', $caller); 11 | 12 | } 13 | 14 | public function testInvalidApiKey() 15 | { 16 | $this->expectException(\PhpInsights\ApiRequestException::class); 17 | $caller = new \PhpInsights\InsightsCaller('foo'); 18 | $caller->getResponse('foo'); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /tests/InsightsResponseTest.php: -------------------------------------------------------------------------------- 1 | exampleComResponse = InsightsResponse::fromJson(file_get_contents(__DIR__ . '/example-com-response.json')); 18 | 19 | } 20 | 21 | public function testValidResponse() 22 | { 23 | $this->assertInstanceOf(InsightsResponse::class, InsightsResponse::fromResponse(new Response(200, [], '{}'))); 24 | } 25 | 26 | public function testEmptyResponse() 27 | { 28 | $emptyResponse = InsightsResponse::fromResponse(new Response(200, [], '{}')); 29 | $this->assertEquals(null, $emptyResponse->getMappedResult()->getResponseCode()); 30 | $this->assertEquals(null, $emptyResponse->getMappedResult()->getFormattedResults()); 31 | $this->assertEquals([], $emptyResponse->getMappedResult()->getRuleGroups()); 32 | 33 | } 34 | 35 | public function testInvalidResponse() 36 | { 37 | $this->expectException(\PhpInsights\InvalidJsonException::class); 38 | InsightsResponse::fromResponse(new Response(200, [], 'malformed_json')); 39 | 40 | } 41 | 42 | public function testRawResult() 43 | { 44 | $this->assertSame(file_get_contents(__DIR__ . '/example-com-response.json'), $this->exampleComResponse->getRawResult()); 45 | } 46 | 47 | public function testMappedResult() 48 | { 49 | $this->assertInstanceOf(InsightsResult::class, $this->exampleComResponse->getMappedResult()); 50 | } 51 | 52 | 53 | } -------------------------------------------------------------------------------- /tests/Result/InsightsResultTest.php: -------------------------------------------------------------------------------- 1 | exampleComResponse = InsightsResponse::fromJson(file_get_contents(__DIR__ . '/../example-com-response.json')); 18 | 19 | } 20 | 21 | /** 22 | * @return InsightsResult 23 | */ 24 | protected function getMappedResult() { 25 | return $this->exampleComResponse->getMappedResult(); 26 | } 27 | 28 | public function testResponseCode() 29 | { 30 | $this->assertEquals(200, $this->getMappedResult()->getResponseCode()); 31 | } 32 | 33 | public function testKind() 34 | { 35 | $this->assertEquals('pagespeedonline#result', $this->getMappedResult()->getKind()); 36 | } 37 | 38 | public function testId() 39 | { 40 | $this->assertEquals('http://example.com/', $this->getMappedResult()->getId()); 41 | } 42 | 43 | public function testScreenshot() 44 | { 45 | $this->assertEquals('image/jpeg', $this->getMappedResult()->getScreenshot()->getMimeType()); 46 | } 47 | 48 | public function testPageStats() 49 | { 50 | $this->assertEquals(33, $this->getMappedResult()->getPageStats()->getTotalRequestBytes()); 51 | $this->assertEquals(1599, $this->getMappedResult()->getPageStats()->getHtmlResponseBytes()); 52 | 53 | } 54 | 55 | 56 | } -------------------------------------------------------------------------------- /tests/example-com-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "pagespeedonline#result", 3 | "id": "http://example.com/", 4 | "responseCode": 200, 5 | "title": "Example Domain", 6 | "ruleGroups": { 7 | "SPEED": { 8 | "score": 100 9 | }, 10 | "USABILITY": { 11 | "score": 100 12 | } 13 | }, 14 | "pageStats": { 15 | "numberResources": 1, 16 | "numberHosts": 1, 17 | "totalRequestBytes": "33", 18 | "htmlResponseBytes": "1599" 19 | }, 20 | "formattedResults": { 21 | "locale": "de", 22 | "ruleResults": { 23 | "AvoidLandingPageRedirects": { 24 | "localizedRuleName": "Zielseiten-Weiterleitungen vermeiden", 25 | "ruleImpact": 0.0, 26 | "groups": [ 27 | "SPEED" 28 | ], 29 | "summary": { 30 | "format": "Auf Ihrer Seite sind keine Weiterleitungen vorhanden. {{BEGIN_LINK}}Weitere Informationen zum Vermeiden von Zielseiten-Weiterleitungen{{END_LINK}}", 31 | "args": [ 32 | { 33 | "type": "HYPERLINK", 34 | "key": "LINK", 35 | "value": "https://developers.google.com/speed/docs/insights/AvoidRedirects" 36 | } 37 | ] 38 | } 39 | }, 40 | "AvoidPlugins": { 41 | "localizedRuleName": "Plug-ins vermeiden", 42 | "ruleImpact": 0.0, 43 | "groups": [ 44 | "USABILITY" 45 | ], 46 | "summary": { 47 | "format": "Ihre Seite verwendet anscheinend keine Plug-ins. Plug-ins können die Nutzung von Inhalten auf vielen Plattformen verhindern. Erhalten Sie weitere Informationen über die Wichtigkeit, {{BEGIN_LINK}}Plug-ins zu vermeiden{{END_LINK}}.", 48 | "args": [ 49 | { 50 | "type": "HYPERLINK", 51 | "key": "LINK", 52 | "value": "https://developers.google.com/speed/docs/insights/AvoidPlugins" 53 | } 54 | ] 55 | } 56 | }, 57 | "ConfigureViewport": { 58 | "localizedRuleName": "Darstellungsbereich konfigurieren", 59 | "ruleImpact": 0.0, 60 | "groups": [ 61 | "USABILITY" 62 | ], 63 | "summary": { 64 | "format": "Ihre Seite spezifiziert ein Darstellungsfeld, das der Größe des Gerätes angepasst ist. Dies ermöglicht eine korrekte Darstellung auf allen Geräten. Weitere Informationen zur {{BEGIN_LINK}}Konfiguration von Darstellungsfeldern{{END_LINK}}.", 65 | "args": [ 66 | { 67 | "type": "HYPERLINK", 68 | "key": "LINK", 69 | "value": "https://developers.google.com/speed/docs/insights/ConfigureViewport" 70 | } 71 | ] 72 | } 73 | }, 74 | "EnableGzipCompression": { 75 | "localizedRuleName": "Komprimierung aktivieren", 76 | "ruleImpact": 0.0, 77 | "groups": [ 78 | "SPEED" 79 | ], 80 | "summary": { 81 | "format": "Die Komprimierung ist aktiviert. {{BEGIN_LINK}}Weitere Informationen zum Aktivieren der Komprimierung{{END_LINK}}", 82 | "args": [ 83 | { 84 | "type": "HYPERLINK", 85 | "key": "LINK", 86 | "value": "https://developers.google.com/speed/docs/insights/EnableCompression" 87 | } 88 | ] 89 | } 90 | }, 91 | "LeverageBrowserCaching": { 92 | "localizedRuleName": "Browser-Caching nutzen", 93 | "ruleImpact": 0.0, 94 | "groups": [ 95 | "SPEED" 96 | ], 97 | "summary": { 98 | "format": "Sie haben das Browser-Caching aktiviert. {{BEGIN_LINK}}Empfehlungen für das Browser-Caching{{END_LINK}}", 99 | "args": [ 100 | { 101 | "type": "HYPERLINK", 102 | "key": "LINK", 103 | "value": "https://developers.google.com/speed/docs/insights/LeverageBrowserCaching" 104 | } 105 | ] 106 | } 107 | }, 108 | "MainResourceServerResponseTime": { 109 | "localizedRuleName": "Antwortzeit des Servers reduzieren", 110 | "ruleImpact": 0.0, 111 | "groups": [ 112 | "SPEED" 113 | ], 114 | "summary": { 115 | "format": "Ihr Server hat schnell geantwortet. {{BEGIN_LINK}}Weitere Informationen zur Optimierung der Serverantwortzeit{{END_LINK}}", 116 | "args": [ 117 | { 118 | "type": "HYPERLINK", 119 | "key": "LINK", 120 | "value": "https://developers.google.com/speed/docs/insights/Server" 121 | } 122 | ] 123 | } 124 | }, 125 | "MinifyCss": { 126 | "localizedRuleName": "CSS reduzieren", 127 | "ruleImpact": 0.0, 128 | "groups": [ 129 | "SPEED" 130 | ], 131 | "summary": { 132 | "format": "Ihre CSS-Ressource wurde reduziert. {{BEGIN_LINK}}Weitere Informationen zum Reduzieren von CSS-Ressourcen{{END_LINK}}", 133 | "args": [ 134 | { 135 | "type": "HYPERLINK", 136 | "key": "LINK", 137 | "value": "https://developers.google.com/speed/docs/insights/MinifyResources" 138 | } 139 | ] 140 | } 141 | }, 142 | "MinifyHTML": { 143 | "localizedRuleName": "HTML reduzieren", 144 | "ruleImpact": 0.0, 145 | "groups": [ 146 | "SPEED" 147 | ], 148 | "summary": { 149 | "format": "Ihre HTML-Ressource wurde reduziert. {{BEGIN_LINK}}Weitere Informationen zum Reduzieren von HTML-Ressourcen{{END_LINK}}", 150 | "args": [ 151 | { 152 | "type": "HYPERLINK", 153 | "key": "LINK", 154 | "value": "https://developers.google.com/speed/docs/insights/MinifyResources" 155 | } 156 | ] 157 | } 158 | }, 159 | "MinifyJavaScript": { 160 | "localizedRuleName": "JavaScript reduzieren", 161 | "ruleImpact": 0.0, 162 | "groups": [ 163 | "SPEED" 164 | ], 165 | "summary": { 166 | "format": "Ihre JavaScript-Ressource wurde reduziert. {{BEGIN_LINK}}Weitere Informationen zum Reduzieren von JavaScript-Ressourcen{{END_LINK}}", 167 | "args": [ 168 | { 169 | "type": "HYPERLINK", 170 | "key": "LINK", 171 | "value": "https://developers.google.com/speed/docs/insights/MinifyResources" 172 | } 173 | ] 174 | } 175 | }, 176 | "MinimizeRenderBlockingResources": { 177 | "localizedRuleName": "JavaScript- und CSS-Ressourcen, die das Rendering blockieren, in Inhalten \"above the fold\" (ohne Scrollen sichtbar) beseitigen", 178 | "ruleImpact": 0.0, 179 | "groups": [ 180 | "SPEED" 181 | ], 182 | "summary": { 183 | "format": "Sie haben keine Ressourcen, die das Rendering blockieren. {{BEGIN_LINK}}Weitere Informationen zum Entfernen von Ressourcen, die das Rendering blockieren{{END_LINK}}", 184 | "args": [ 185 | { 186 | "type": "HYPERLINK", 187 | "key": "LINK", 188 | "value": "https://developers.google.com/speed/docs/insights/BlockingJS" 189 | } 190 | ] 191 | } 192 | }, 193 | "OptimizeImages": { 194 | "localizedRuleName": "Bilder optimieren", 195 | "ruleImpact": 0.0, 196 | "groups": [ 197 | "SPEED" 198 | ], 199 | "summary": { 200 | "format": "Ihre Bilder wurden optimiert. {{BEGIN_LINK}}Weitere Informationen zum Optimieren von Bildern{{END_LINK}}", 201 | "args": [ 202 | { 203 | "type": "HYPERLINK", 204 | "key": "LINK", 205 | "value": "https://developers.google.com/speed/docs/insights/OptimizeImages" 206 | } 207 | ] 208 | } 209 | }, 210 | "PrioritizeVisibleContent": { 211 | "localizedRuleName": "Sichtbare Inhalte priorisieren", 212 | "ruleImpact": 0.0, 213 | "groups": [ 214 | "SPEED" 215 | ], 216 | "summary": { 217 | "format": "Die Inhalte \"above the fold\" (ohne Scrollen sichtbar) wurden ordnungsgemäß priorisiert. {{BEGIN_LINK}}Weitere Informationen zum Priorisieren sichtbarer Inhalte{{END_LINK}}", 218 | "args": [ 219 | { 220 | "type": "HYPERLINK", 221 | "key": "LINK", 222 | "value": "https://developers.google.com/speed/docs/insights/PrioritizeVisibleContent" 223 | } 224 | ] 225 | } 226 | }, 227 | "SizeContentToViewport": { 228 | "localizedRuleName": "Anpassung von Inhalten auf einen Darstellungsbereich", 229 | "ruleImpact": 0.0, 230 | "groups": [ 231 | "USABILITY" 232 | ], 233 | "summary": { 234 | "format": "Die Inhalte Ihrer Seite passen in den Darstellungsbereich. Erhalten Sie weitere Informationen über die {{BEGIN_LINK}}Größenanpassung von Inhalten zum Darstellungsbereich{{END_LINK}}.", 235 | "args": [ 236 | { 237 | "type": "HYPERLINK", 238 | "key": "LINK", 239 | "value": "https://developers.google.com/speed/docs/insights/SizeContentToViewport" 240 | } 241 | ] 242 | } 243 | }, 244 | "SizeTapTargetsAppropriately": { 245 | "localizedRuleName": "Optimale Größe von Links oder Schaltflächen auf Mobilgeräten einhalten", 246 | "ruleImpact": 0.0, 247 | "groups": [ 248 | "USABILITY" 249 | ], 250 | "summary": { 251 | "format": "Alle Links oder Schaltflächen auf Ihrer Seite sind so groß, dass ein Nutzer auf dem Touchscreen eines Mobilgeräts ganz einfach darauf tippen kann. Weitere Informationen zur {{BEGIN_LINK}}optimalen Größe von Links oder Schaltflächen auf Mobilgeräten{{END_LINK}}.", 252 | "args": [ 253 | { 254 | "type": "HYPERLINK", 255 | "key": "LINK", 256 | "value": "https://developers.google.com/speed/docs/insights/SizeTapTargetsAppropriately" 257 | } 258 | ] 259 | } 260 | }, 261 | "UseLegibleFontSizes": { 262 | "localizedRuleName": "Lesbare Schriftgrößen verwenden", 263 | "ruleImpact": 0.0, 264 | "groups": [ 265 | "USABILITY" 266 | ], 267 | "summary": { 268 | "format": "Der Text auf Ihrer Seite ist lesbar. Weitere Informationen zur {{BEGIN_LINK}}Verwendung lesbarer Schriftgrößen{{END_LINK}}.", 269 | "args": [ 270 | { 271 | "type": "HYPERLINK", 272 | "key": "LINK", 273 | "value": "https://developers.google.com/speed/docs/insights/UseLegibleFontSizes" 274 | } 275 | ] 276 | } 277 | } 278 | } 279 | }, 280 | "version": { 281 | "major": 1, 282 | "minor": 15 283 | }, 284 | "screenshot": { 285 | "mime_type": "image/jpeg", 286 | "data": "_9j_4AAQSkZJRgABAQAAAQABAAD_2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj_2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj_wAARCAI5AUADASIAAhEBAxEB_8QAHAABAAIDAQEBAAAAAAAAAAAAAAUGAwQHCAIB_8QAPxAAAQQCAgECBQIEBAQFAwUAAQACAwQFEQYSIRMxBxQiQVEyYRUjcYEIM0KRFiRyoRc3Q1JiY3TDdaKxtPD_xAAXAQEBAQEAAAAAAAAAAAAAAAAAAQID_8QAHREBAAMAAgMBAAAAAAAAAAAAAAECESFRAxIxE__aAAwDAQACEQMRAD8A9UoiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICo3xu5DkeKfC_OZrCyMiv1WxGJ74w8DtMxp8HwfDiryuX_wCJr_yO5P8A9EH_APYiQc44_kfj3nsHQy2Pu4A1LsLJ4i9sTXdXDY2Ovgrq3w4t8uxfHslb-Kt7EwvilDmTxPZHEyLqPLnDQH1flcb4D8OPiXkuE4K7ifiRNRx89OKSCqPU1CwtHVnj8Dwu48B4zlsbw2bEc1y45FPM-QSzStJD4nADoQ77e_8AugsZzuJGG_i5ylAYnr3-dNhnodd633311vx7rJHlcfLi25KO9UfjnM9QWmzNMRb_AO7vvWv32vFrKFR_xQj-FsnJ3O4EzMGVrdnRk1v0O_57fR-OxJ9yu_8Ax7m4Vj-L4bD8ngyM0L7DPkcTiiWvsFgADOo8Fg2B_ca8oOkYnlPH8xZNfEZzFX7AGzFVtxyuA_o0krLPyDDQZN2Ony2PjyDYzM6q-ywShgGy4sJ311537Lx9zmL-Hc24LksVwCfhDDkWMjlfKGyWfrj2DGP06B9z79iFduZcdx_J_wDFvXxuYiM1A45sskIcWiXrG4hrte7d62PvpB6Io8lwd-nYt0czjLNWv_nTQ2mPZF_1OB03-6z4jM4zNQOnw-RpX4Wnq6SrO2VoP4JaT5Xlut8P-PS_4o7_ABttERcfFNtx-Pje5sMjhGxwa4A-W9j217eFYvhli6vF_wDFXy3BYOP5TEHGtm-VYT0Di2B_gfsXu1-NoO6DmPGTSluDkWGNSKT0ZJ_nYujH-_Uu7aDv291JQ3693HfOY-zDZgewujmheHsd49wR4K8rf4ZeB8d5W_ll3kdKPJOrXTDFXmcTHGHbJf13-o6A39tKzfAiN3Gfit8RuGY2WV-AqNNivE9xcIXbA0D_AEfo_noEF0_w081zXO-C3snyKeKa3FkX12OjibGAwRxuA0P3cfK6FLyjARZP-HS5zFsyG-vyrrcYl3-Om97Xl74OZq7x7_CrzfJ4t7o7sWQkZHI33YXsrsLh-4Dif7KD4_h8be-GcdQ_CfkuSydyuZRnI2Oc58rhsSMdr9O9ePuPfaD2quI_4h-b8r4xn-GYnhtitDZzc0tfU8TXtc_vE1nl36Rt52rT8AZuQP8AhljIOW1btbKVS-uRcY5sr2NP0OPbyfpIG_vpc_8A8SH_AJsfB3_9V_8Az1kGnkst8f8AjdSTKXqmFylOsDJNBAxjnFg8k6b1d7fjyuxfCvnNP4g8Pq5ylGYHuJisVydmGVv6m7-48gg_ghWm3NDBVlmsPayCNpfI53s1oGyT-2trxBh87lMB8AOWW8M-WCvlOQtptmjOi2Ixuc7qftvq1v8AdB7Mh5Vx-fJfw-HO4qS_vr8sy5GZd_jrve1s5fN4rCxwvzGSpUGTP9ON1qdsQe7_ANo7EbP7LiPK_gvwrHfBu5LTpRxZGjjXXY8o159V8rGd-xdvyHEa17Dfhct-I-bv8q-AHw6t5aV77j8lJWdOT9TwzswOJ_OgPP5QevIeQ4WfLOxcOXx0mTZvtUZZYZhr32wHf_ZKXIsLeyMmPpZfHWL8e-9aGyx8jNe-2g7C8wf4jOAYDir-DjjVQ46zYumpLahkcJZB9H1udvZdsk79_K3fjLw7B8A5x8L7PEaLcZPJkRBI-Fx3I1r4gC4k-SQ9wJ-4KD0vl8zjMLA2bMZGlQhcdB9qdsTSfwC4hMTmcZma7p8PkaV-Fp0ZKs7ZWg_glpK4Z8V7vCb_AMUY60_G8zzPk9Sr6bsbWJfWgafO3A-A7yN68fUN-dKqfA9k-N_xHZykzAu4zBPjHSPxIn9RsfiJzSSPGzsu19uxCDunC7-Ws8w5NBkeTYLKU4ZtVqNEt-YpDu76Zted60PP3BU6zmHGn0JbzOQ4d1KKT0pLAux-mx-t9S7toHX291xX4Df-eXxb_wDvv_zSKqf4XuAce5XjeSXuR49mR9K-6vDDO4mOMFoLnBoOux2Bv38BB6px96pkakdrH2YLVaQbZNBIHscP2cPBWwvO_wDhNYcdlviNhIJJDj8dlGsrxucT1-qZpP8AUiNv-y9EICIiAiIgIiICIiAiIgIiICqnxT4pJzfgeV49FbZTfdbGBO6MvDOsjX_pBG_069_urWiDznQ-B3P8fSgp0fivkq1WBgjihibM1jGjwGgCTwArzx3gvNMRwbNYibnMt_L3pP5GStxSSOqsLQ1waC_e_cg78E70upog5BN8DME_4Us4ix_W6x3zTcp6f8z5vXmX33o-3Xf6dedja_OX_CTJcr49xs5HkzoeWYE7r5evAQH-QQXMLt9vpadg-4P5XYEQcD5B8D-R8imxeSz3PZslmqFhksLpqQbXYxp3psbCNOJDdu37BXR_w4nf8a4ueHJRem2n8qaYhOyehbvv2_fetLpCIOb1vhxND8a7fOzkozFPTFUU_RPYfQ1u--9f6d-33X7jPh1NR-NmX547JRvhv021RTEJDmabE3ZfvR_yz9vuujog8gf4feH5rN1OUZPinKbXHspHkXVnubC2eGaIjtpzHf6gSdO-2yu-fCv4ZVeCUcnI-_NlM3lXepeyE7dOkd58AbOhtxPuSSf6aumIwuLwzJWYjG0qDZn-pIKsDYg93_uPUDZ_crfQct-Ffwnh4h8O8vxPM24stUyViSSUtiMQ6PjYwt1snf0b3-4_Cr-P-D_M8DSdiOL_ABMu0eP7Pp15KTJJYmk7IbJvY9_tpdyRBFcXxdnDYStRu5S3lrEQ0-3b6-pIf30AP_8Ae5XPvjb8Lb3xDyHHbuMzow1nDulkjlELnv7uMZa5pDh1LTHva6siDzvP8BuX5dnynJPijl72Mf8A5tfUh7j8fVIR_uCuozfDHjUnw4_4JFRzMN06gh38wP329Xtr9fbzvX7a14V3RBweT4JcotYiPjd_4j5CbiTNMFIVGiUxj2YZN7IGh4Ox4Hjwp_4i_B6vyPiXGuP4K5HiqWEnbLGHxGXsA3Wj5HknZJ_ddZRBzf4v_Debn8vHXw5KOj_CrhtEPhMnqfp8DRGv0_8AdPiv8OJudZnid2LJR024S4bTmOhLzNt0Z0CCNf5f7-66QiDjvKvhFlJfiBb5dwjlk3HsleZ0ttNYTsk8AbAJ156g6IPkbC_OFfB27xn4lt5bLyabKyz1nw3fnIdyzPd_qa4HTQNM0NHQBH9Oxog5x8PvhxNxTnnMeQyZKOyzPWPWbA2EsMP8xztE7Pb9WvYeyfBT4cTfDjE5WnPko75u3DaD2QmPr9IGtEnfsujog5z8KvhzNwfP8vyMuSjuNz1wWmxthLDDp0rtEknt_mD8e37royIgIiICIiAiIgIiICIiAiIgIijOQZWTD0nWmY27fYwOdIKpi2xoGyT6j2-PH22UEmiqtDm1KcUnX6lvFR3YXWIJLzoWtcxoYd7bI4D_ADG-Do-6nXZKtGLL55GQwQBrnTSSsDNOG977eB_XX7bQbqLHXniswMmryslheNtfG4Oa4fkEe6jIORY6xmHY6vOyWZkDp3vjc1zGBrw0gkHwQT7IJdFp_wAVx_yTbnz1X5R2-s3rN6HWydO3r7H_AGK-oMjSsWPQgt15Z_TEvpsla53Q-ztA7159_ZBtItBuWpyxyPqWIbXpysikEMzD0c5wb5-oAa3vXv48AnQWWPI0pLhqR267rQBJhbK0v0Donrvfg-EG0i05chBC60bL2QRVw1z5ZJGBuiP-rY_uB-21sVrENqBk1aVksLxtr43BzXD8gjwUGRFqMyVF9eewy5WdBAS2WQStLYyPcOO9DX7rSdyXEDIY-mL9d02QY-St1laWyhhaD1O_J24aA99H8IJhFD5HkmMpUbNr5mOwytIyKZtd7XuY57wwbG_Hl33_AAV9WuQUII6Ukcosx3LPysT67mvb36ud5O9D9BQSyLRblaYNRk9iGCe00OihkmZ3fsewAJ7e_wDp2FhxOex-UklirTsFiOWaJ0DntEn8qV0Tndd769mnR_GkEoi1612ta9b5axDL6LiyT05A7o4e4Oj4P7FadDPUMhftVac7JjWhjnfLG5ro-r3SNADgfcGJ2_x4QSiKGxXJ8PlMRBk6uQrGlM4RskdK0Dv_AO33_V-3utjNZirhoK810uEc1iOuHDWmuedAnZGh-SgkUWr_ABGn_D_nvm6_yXXt6_qt9PX57b1r-6zGeIV_XMrBD17-p2HXX537aQZEWtbyFOnHJJbtV4GR67ulka0N37bJPjaTZCnDFBLNarxxzkNic6RoEhPsGknzv9kGyii-QZ2hgcfPbyEzWiKGSYRBzfUkaxvZwY0kdjoeyw5nklHFZXFY2cufcyUhjhja5jdAa7PPZw8DYGhsknwCgmkWqzI05BOWW67hA7pLqVp9N34d58H9ivw5CEw15YHNnineGNfHIwt8787JGx4-2z-yDbRQWC5Tj81M6Kt6sbxGyUesA3sHSTMAHnydwSHX41_aRsZShWMQsXa0RleY2d5Wt7uB0WjZ8kH7INxFDVeT4azPkYmZCs1-PnNewHytb0eGh3nz-_8AuD-Ctuzl8fWlsRTXazJoIjNLGZWh7GAbLiN7A190G8iicJyChmq1ezQmZJWsRRyRSCRhD-4ceug7YcOp2CB_fR1u2b9OqN2bUEQ9QRbkka36yNhvk-_kePdBsoiICIiAtHPRPnwmQihYXyPryNa0e5JYQAt5amWyVTE0ZLmQmbDXZoFxBOyToAAeSSSAAPJJQU2rx91m_wAKOSxolho4yZkomYHNhlMcDQCD99CQf2KrmK4xabxSeHJY7IRPjq4p0fy8TJJGywwtBPRx0_o4eWne_sCukU-S4e3DHLDei6PjklBeCzTYzqTfYDRafcHRH3Cwu5bhG3YapufzZfT1qJ5awvALGvd16scQRoOIPkePKCEwNLK2_h3k6fy7KF-dlqKs8VxVc8u7Bkro2-GOJOyP7-N6VUuYWXIZW3NiuJWqFVmNrQzwvjZF816dtkj640dO_lte3sfDu3vpXt3PuNNmMIyIdKHviaxkErjI9ji17GAN-tzSDtrdka2RpfcnOuNRyV2HKQ7ngZaaWseQInkhsjiG6a3bSNu0ARo6KCrVsA7J56jcODfXwzsw202pYgazp0pTsdM6P_T2e5gH3JAJHlasfE7FPFY3-G4KJtxtvLFzNCLbJG2REHPaQQx24gNHwOvtoaujuXUKr8n_ABE_LMqXvko-odK-d3oxy7axrS7ennwAfDdrLb5jgahq-rkGObZhFiN8Ub5GiInQkc5oIY0nx2cQPB_BQcxrYTKzZau6vi8kKzIabZHS0oajO8d-u8hsbPP0sa87JPj2Pvu04rjclbIY243FiKz_AMSX7U8wjaH-i9loNeT79TuIf3CueRykFC7UgmkhYJxK76i7sQxvY9QAQdD32R_da2A5ThuQPc3E3G2CImTj-W9neN36Xt7Admn8jYQVXkeCkuW-US2Kd50Mj6M1eSrGyR5fECewY7w8NOttIOx9it3j1PK2vh9lKfy7cffnZajrSis2q55cCGTPjb4Y4k7I_bfjelvZvmuLx2VqYyORti9PdipmMdgGuefI79S0ua09ize9fhSGO5Rhr9qetUvxOlga57tgtaWtOnOa4gNcGnwS0kA-6Dm-M41YsYrLC1RysVf5WGL0IaFes71I5WvaWxgkS9CPcnq4baO219Y3G5P16FqfAmbr8_WhlNFtV8jpGQGOWVjDuLbmSNLxrw1rtDsuiY7lWFyPpCpda4zTNgja6N7C9zmOe3QcASC1jiHexAOite_zbjuPkEdrIxsfqVxAje7q2KQxSOOmnTWvBBcfA_OvKDkuQ4_lrdGaKLAZKeIYh1eaA04ascjxZqu9FjQdu-lsv1OcRrej77tc2ItXMoL-Lwtihjn26-q74hE7syCy18pYD9IPqRM_foPtoq5VuY4CxVuWYshF6FRjZJXuY5o6OJDHN2B3a4ggFuwSNDa_JOZ4COnWsOu_RZlfBEwQyGR0jAXOZ6Yb3DgAT1I2goGQwl6nU4_NUxNubJ_wynWmrS0YrFaUx-0b3E9oS0ucS4aHkH6i3SkncVtivVkoUW1MnLmctNJbDAHsbKy42ORzh56ntD_-3x4U_F8QcFJftw-rJ8tBUr222hDI6ORsxc1rWkN8u2GgNGyS7QGwQLFiMpTy9T5mhKZIw4scHMcxzHD3a5rgHNP7EBByfEcYy8uNvxw1bUL2UGVZK76kNNk5bKxxiBYT6m2tkaHkhv8AMPvs6tXCKjDy3kd-tx-xiKNqvSjb60LYvXewz9z0B8aDmDz76B9iprI8mZjZslBaquFmBrH1Y2u2bYeerQ3x4d3-kj7bB9is7OSYxmVixVi1DHk3dWOib2cxshb29Pv169teQ0kOI86Qc5wuHkx1PGnKcZtXatapPQfUZVY7U5eD3DSdFr2_T6g9tedBTvI8Ne_8O-PUb1CXLWKljHuuV49PMgje0v8Ac6drW_31-6uNPN4278iK1qOT56N8tfQP8xrCA4jx9uw9_wAqOv8AN-O0L09O3kmRzwSiCQGN5DZC0ODC4N12IcCG72d-AUFKyeEyM9F96lQsU8cct822iKTJpGx_LiP1Pl3HWzJt_X9Q3vW1L_wO-fhJlMayvM-3PBZMNeRkcbtPc5zWBjfpZ4I03fj22rC7mOCGPhtC4XsmkfEyJkEjpS5n6x6Qb3Bb99t8bG_cLbh5FiZsdPeiuxPqQdfUkbsgdmtc37bOw5vt-de6DneZo2M5kMxlZcTmqkbZqUlOQU45JOzI5mueYHk9mj1C0tI35DgPAK0r-EzNinirFvGPZA6jLVFWti4ZejjK89jE52oTIwsJ0SGkEEjQXV7mYx9OWzFZsMjfXrG5K0g_TCCQXnx7eD_stOLlOGlyoxsdvtZLgwfyn9O5YHhnfr079SD13vR9kHN-X8fut4_mqN7A3M9kL2Prw07DWslMb44g0hzzrqRIHSbGg4u8efCunLcNayPI8TNVhAMVS6wWS0EQSPbH6Z37g7BI1-FMYvkeIy16epQuMmnhDiQGuAcA7q4sJGngHwS0kA-CoG_8Q8dSy81KaCdggybcdLI6KQDbq5mDmaYe5LtMDW7J3v2I2FQtcesXsI6pT41YpiHFNoXI3xtb81L60B0NH-YGhkx7_wD1PySr9yrGSSNwEdCoDFXykEzmxNAEbB327X2Hn_uvuLl-HkimtC9WFCKr80-R3dr2tEjmHbS3xpzC3X6uwI0pTDZilmYJJaEjniN_SRskT4nsdoHTmPAcPBB8j7oOU47DZbGW8bkJ8RdkipvrSSMijD5NCXIg9W72SBPESB9nbX5k8PfOKZkW4bIsyjp8j6dZ9CK3FJHLafIyKZhP0d9tPcEaBOz9lejznEychq4urNHMJGTySzEuYyNkQG3AlvV7dnRcDoFbTOacedjp7xyMbasD445HPje0gyODYz1LQSHEjq4DR-xQVCtx6Wbl8YuYT-U3MuvySGBpiLH0HMBDv9WpOzfyCQfuo9-Fyc3IqjHYm2xzctPJN0qxiu2F7JgHOmdt8vbszY3oE6IAACvtHm_Hr12GrWvh1iWT0A10MjOkvn-W8loDHkDw12ifsF-S8zwroLz6t2Fz60EtgPkZIyJ7YwS4tk6kPA15LO2vwgpXHePXzhZnxYuandqYvG_KtliEZNqs6cuaP2JOifYiT91jzXHMs0Yy7crSzmzXmdbhhoRXCyxM8PcC15AA66jDh7BgBIC6DPyzCQZp2JkutbebMyB7BG8hkj2hzGucG9WlwcNbI3vQ8rJluRY_E2LQydqtXr1qotyve53ZjO5b2I6667Hvvf7fdBtcdrTUsBjatp0j7ENaOOR0jg5xcGgEkjQJ39wpBRuFzdDNMndjpzIYH-nKx0bo3MdoEba4A-QQQdaIPhSSAiIgKt87wljNYqu2k97bNWyy0xjLDq5k6hwLfUb5aSHHR_IG_Csij85locRUbNLHNO-SRsMUEDQ6SV7vZrQSB9idkgAAkkAIKL_wVkrmJZVm9Gu51p2Rc-ay-2_1Wta2ON73_qaQ369aBH0geSViz_GOQZS_O414HssW6lprjkHsirtY-Jz2ek1unv8Aofp599jetAK0_wDGmNjg9S9FbpdTLHI2dgBjlYATEepO3lpDm62HD2JXzd5pVpWYo7WPyEULpYoJLEjY2silk69WEF4c4gvaCWBwBPv4Og0sTxq9VscefKINUbl-eXq_f0zOlLNePJ-sb_v7qrYvC5-pkMnhq9OlL62Fr1J55Ji0RF01w7H0_WA1-yBo71-dq1x8_ry2IIa-EzUrrM89asWxRATyQuc2QAmQdQOjiHO6tIHg78L8Z8Q8fKQa-Ny80TaUeQnkZA3rXhc6Ru37dvsDE_bWhx8bGx5QQ2Q4Tk4rb7tR8krorbjHFDffVfLC6tXi7GRo8ODoN9T4IJ870vnJcPy9bGY5uGrsiy0UL2i9DlJYzE90jpNSBwd68Yc7ene_1aA34n6_L3Nu5Kuas1-Zt816dekxvd8Qrwyl5LnBuh6v6iQPLR7nzkPOaUlmrBRoZK9LNW-ac2CNgdCzuWEOa97XFwc1wLWhxHXyPbYZeVYW5k8jjZ6xi6V4rTH93aO5IujdePz7rBxzj93H5LEzz-j6dXCRUH9X7Pqtc0nXj9Pj3_7KXzOfp4eUsuiUAVZrfZrQQWxdezR5_V9Q0P6qKj5nSuWsLFTbaLr7GTNb6TfZ0czhG4lw6O3C78-W6--wETd4_nfnKVCvWoSYyHN_xV1t85EnR0rpXMDOv6w5-t70Wj8rWh4bl7uIp4S-KlWnjMfYoQ2oZS91j1IfSa4s0Ommns4bO3a14WHDfEm9JBBbyeEuCscKzJzNrsjcYgHuD5CfU106gEN_WdO8K3ScuptyhqNq3ZIGWI6kl1kY9COZ4HWNx7dtns0bDSAXAEgoIvI1-S36mMtSYvHx2sbdZO2sy4dTNEUkbtO6ab_mAgEHwDvSjKHEc0amYddZSZZu4-_A1scxc1sk9qaVo2Wjx1kbs_nfhSfI-exUuKnJ4ynYsWZq1mevE9rWj-QdO77cND7-D5A_PhTGb5KzD0cXNZxuRks5Gw2rFUgax8okLHv6uPfoAAx23dtD868oK7nuIZC8Xugexhjo0GRiOw6Fzpa8sj3N7tG2AhwAcPv9l9ce4tkKuUxl6wwMLLc9mds16S1IA6u2Ju3u_Ufp-2gB-fJMvHzOpPZpVa9DIyW7Prh8IjYHVjC6Nsnq7cANeq0-Cdj23sbws5pVjxeOmbVyF982PiyMxrwNBhgcPEj2l3jenfS0ud9LtA6QQFDiOUq4-xTtUKd2GfHQROb846ItlhnmkHVwbsEiUFrx7Ob_AHVm4XVzOPxghy3eXvZeY2zW_mJIIev0h0hAMh7A_wBA4DZ6-dzE8jr5bK3KdGtakiqdA-51aICXRRytDT27HbZAfbXg7-28cfKKz7ltnyl1tGq6VkmQLG_Lh0Y3IN9uw1ojZb12CAdoGfw0mQz_AB69GyEtx880j3PP1AOhc0dfHn6i0_239lByceyzchNRiiqHFz5aPLOuOlPqN6yMlMfp68nszXbeup_IUjFzjH_LTz3auQoNZXFuJtmEB1iIuDQ6MNJJPZzR1OnAubsDa-Zub1oA2KfFZWPIOsx1RRMcZl7SMc5jth_TqQx3nt4IO9aOgiuJ8fztHKYNl-vTjpYmtbrCaOwXunMjoyxwb1HUaadgnYK27HGb0kttw9DUvIIMm3b_AP0mNiB34_V9B8f08pnebOrzVIcfj7rmyZStj5LbomugY98rGyMJD-2wC4dtFocNbJ8L4j-INSHGVZzRyl3eLblZpYYI2iOA7Bc4GTwfpJ6guP43pBp3OM5qK--xBG2xXfftTugivOrOe2T0uhc9rd9R0d2Z7HbT56hRGDwGSxuQ4rgZPliWwMny7IXOe2NsEr5IC1x9w6R3Xz5Ib-yu17mFOnkY60tO76D7EdT5stY2L1ZC0NaA54e7Zc0ba0gE-T4OofjHO_naLbecglxvSlYtysexjm9I5QwvDmvcfvrrrZIP7bD75xhM3bvZCbC16lgX8RLjT69gxei8lxa46aezfq-3nx-6xM47mK3K6dnHQipXL4zcmZkHmOwwRBjg-uW9fU8AB4I8AHfjSlJucVa9aV9vFZavZY-uwVHws9Z4nkEcbmgPI12Ojsgt0dgKSxnJKlzF3r08U9BtB8kduO0Gh0JYA52y0uaR1IOwT4KCq8J4hkMTkqPz5kfDjIHV4ppclNP6oIDQWxHTY_pHkHfnwPHlb8uByT-XGz6Vc0P4szJiUy_Vr5E1yzpr3Dg0735B_bznk55SgqSzW8bla0jWwyRQSQt9SdksrYmuYA4_6ntBBIcNjY8hTN_M_JYuC1NQvGeZzWNqMa10vc7PUkO6DQBJcXdRr3QVGrxC6ybNG3WpW4LcFmNsEkzmteZLs87QSG7b9EjPI8g_02pjhGMy1LH34MvJM2GSX_lYprhtywx9ACDKQC76uxAO9D7rAz4g0JfloquPyFm9NNNAakRh7xviDS8FxkDD4ezQa4k78DwdbeI5G-zlsrBZYWMilrsqwmMtld3hbIWuBPuC479tAeUFUm4hnr9fH4i1DQgoUMXbxrbjZy58vqRNjY_p1HUabtw37-ykp8DncxebkMjWpVJmSUYxBFYMgLIbTZpH9uo_Gmt1_Uja2o-d146tXpRyeRmmr2Lf_Lwxt6xwy9Hk9pAAfI0NknXjz4WbKc_x1Gs-1HSydylE2F01mtC0shMwaWB3ZwO9PaToHqCCdbQYZuNX3S2nN9DUvIIcmPrP-S1sQO_H6voPj-nlVK3wbkd2tHBNFXM0OPvVHWZcg94lklgMbDHF1DYo-3nqB9I0BsDzduT8guY3lGIpRRGPGur2bt20WscGRwmMEeXtIH8zZIDj7aB86-ZOe0YKUs9vHZWs5rIZYoZIW-pYjkkbG1zAHH_U9u2nThsbHkIF3j12aTMuZ6P_ADeaoX49v_8ASh-V778e_wDJfof08jfjW5lxjIZe7kZanodLGOiqs9R5H1tseod-Pbr_AN1cqcz7FaKWSvLWe9uzFL17MP4PUkb_AKEhZkEJiMZYqcjz12X0_QuvhdFp2zpkQadj7eVNoiAiIgKC5lx2HkuLZVmFcvhmZYi-ZhE0Re3fh7CR2aQSNbB87B2FOqv8wzdzDRY1uNoR3rV66ymxkk_pNZ2a9xeXdT4AZsjW9e3lBDQcBZ_DI6MtqvXgbM656ePqNrMbZ8em9oB9mdQQHbJOiT4AWvleBW8hkJrD8hQ7WJ61iaZ9AvnBidGTFG8v-iMmMkDWwXnyV95L4gDHwx1rkWPp5b5qSpKLl30qzDGxry4S9dkFskZA67-rzrRWIfEOxZpz3Mdi4ZatXFPyc732teWOmYY2aaQ7boTp3gEHf42E3juKmnNhpDcD_wCH2bdgj0tep67nnXv469_fzvX2VaocNzVbJ3qFbIxw4uTFwUpZn1exm_m2XOMf1_S5olA8hw-oHS2r3NOQUY8k-zx-i0Y6gzKWOuQJ_kO9T6G_y_Mo9J__AMfA-ryv3Kc3lxVETsqwsiflLdOS1ftubBD6b3AFz2scW9teBrQ9t-2wyZT4dV7Ur52HHzSttGaCK_T-YhbGYIYSxzewJP8AIa4OBB-3tvfzmfh867g6GMrz4tkdaJzGzSYxvqV3ucXGWuWOb6TgT4HkeBvfnea1zixDyHHY51XHQttwQSsdPf6-u6QkObA4MLJC3Q9yO2xoeVmq8muTw1KuIoNsX7M14gWrZaxkdewY3OLw0nyXMAaG-O3vpu0ElyrjLM_PiJH2XQijaE0jQ3frx68xnz4BIYSfwCPuozH8FbSz78ky-5zTk_n2ROj_AMtnozM9IHft3nkfv99a-60LXxAvOrWbmPw8MlOljo8jbM1vo9oL5mPiaAwhzgYHaJIB_ZdEHlBQsfwKevh79CbJxSCbDnDxPbXLekYMnV7h2O3ak860Dr7bW6_iVs33tZko24me5FkJ6_y-5HSxljtNf20GF0bXEdSfcA6KuCIOdS_D29Yq_JWMzCaMVa9WgayoQ8Cx57Pd30S3yPAAO_spblmMzd1nF_kbMEeQq5D157Da5dC0CtO07YX9uri4N8O2Ow8q3ogq-H4q6jlIsjNcE1p0dsWCIuokkndAezRs9WtEAaB5JH3376DeG36mPpV8Vl4oJG4qHE2pJKvqepHGCGyMHYdXju_W-w-ryDpXdEELxrARYEX468hdBYljfGwjzG1leKENJ-_-Vvfj3_ZaEfGbQdkaMl-J-BuusPdW-X1MDP2L2-p2117Pc4fTvzregrSiCkP4VbvwPbmsu2eWKoKdOWCt6XpASMkEjwXEOeXRRE-zfp9hsrNFxK5Plq-WyuShmyDLUMzvQrGOP04o5mtja0uJB3O9xcSffWtaVxRBSr_D781uKKploYMQMpHlX13Ve8rniUSujD-4AYXgu_TsE--vCx1OBugwdjHnIhxlwLcL6no60QJP5mu3_wBT9P7e6vKIKDkOBWLOYfajvUWxyXa110klH1LOoXxu9Fshf9MZ9PwANjZ919VPh-RBZrXsg2WrJSs0IxFCWPbHLKJASS4guadj2APjwFfEQUz_AIRv3LIuZjKwT3RLUIdDVMbPTgm9XXUvP1OPud6HjQUnDxqI0eQU7Uxlr5iaZ8ga3qWNkjbGW72dnTT5_dWBEFIk4bfvPjmzGXisWYBXigfFV9MCOKxHM4uHY7e8xNBI0BrwFNcwwTs_jYa7JYWGKds_p2IjLBN139EjAR2b53rfu1p-ynUQc7_8P7P8IuU5rGDu_NXpLj4beJDq7e0bGBrGB4LS3p4cHbIPnz5W3jOAtoSutjIPmyjW1BDckYS8ehEIz2-ryJAHdta32--gVeUQU3GcKNJrB88H9aFql_k6_wA6b1O36vt7a-_5Cq2d49mY68_HcR816FxtQTTPptdFIY2RMc9svqfywWRBpa5pP0_T77XW00N-yCCzfHoszeZLalIrmhboSRNHlzZzFsh320IyPb_V-3mEl4Zfv-nJl8vFYsV2wQ13xVfTAjjsRTOLx3O3vMLASNAa8BXhEBERAREQEREBVrm2An5A3DR17MtUVMgy2-aGTpIwNjkALdggns5vgjRG9_hWVEFbPEaorQiC9fgvxzPsfxCN7DO97xp5dtpYQQGjr10OrdAdRo7h-PdVvQPmuvF3HnGzPfN3e5hMhLuxBPcmVx37e3jwrIiCEyfGqWRjyrJ3zgZLHtxs3RwGom-rot8eHfznefPsPCxnjLI6r4qGTyVFz7U1syQPYSXSuLnNLXsc0t246BaSPyp9EFUl4RTdjaONZkMnFjK0UMJqNlYY5mxODm9uzC4EkDZYW7WxNxKqYYBVvX6dmCazMy1A9nqD15HSSM-ppaWlzvYjx1aQdjasaIK1_wAF4puMv0IvmI69zHsxsgbJsiNhlIcCQfrJmeSTvZ0rFFG5jpS6V7w93YNdrTBoDQ0B42CfOzsnzrQH2iAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAo3N5ivh460lpshZPO2DswbDNgns7_4gNJJUkobkVB9-TGMbF6sLLXaYbHhhikaT_u4f7rVc3lJZ7uYr1Mvj8c9sjrF0vDeo2GBrXO278A9SB-6zMyuPkZO9l6o5kB6yuEzSIz7ad58H-qqTOP5WWKu608fOid8PrNd-iFtaaON_52XPDjrzt37LBaw169Sjibh_lW16cdR8bnR6mPrROOtHyxoY8gnW-58Lr-dPmpsroMrjy-u0X6hdZ8wj1m7l_wCnz9X9lksW44J4o5HMaHte7s6RrdBoBPgnZ9_t7fdU_kOEv2Mvf9AW3wXWxNYYfQDI-vuHue0vbo_UC3fk-AD5UtybH2rdyu-tCXtZWtRkggac9jQ0eT9yFn0rxybL7y_LMRj8dJbZdq2Q2RkPWKxH-t5-kEl2m_c7J9gSpWPIVH2jVFqv801vd0AlaXtH5I3vX7qpuwVtk2NENQNjhr0I3BpaA0xzdnD3-w8__wALJXxl1luCsaDmmDIS3HXuzOsjHF5AHnt2IcGEEa0339lqaUziTZWB2exDYw92VoBhOg42GaPt99__ACb_ALj8rNDlMfN8x6N6rJ8v_ndZmn0v-rz4_uqvj8BPFWxLJKTAa-DfUcPp-iVwj23-_U-fbwtefBXoqNRlei3cWLrwOaBGdOZKxzg0O-kuADiN_TvW1PSnZsrJNyXDxPoj-IVXtuzOgieyZhaXhpcRvf7Af1IH3W0cpTjq1ZrVqvXFkNMYkmaA4kA6ad6d7_ZVDH4_KR5c3paN58Xz8Ug9aSJ0rmfLSRF5AIA0542B9hsb9lkixV2nQpNlxRvOfhoqDog5n8mRoPYO7HXV2xsjf6B4PhWfHXtNlbX5ShHdbTkvVW23Hq2B0zQ8nW9Bu970QVuKmVcBbgqW2SQiay69j5PW8bkZEK4c7Z8-CyT38-_5VrNl4B_5ac6D_YN_0nx9_wDV9v8Avpc7ViPktRPbYRfjT2aCQRsb0fcL9WFEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREH__Z", 287 | "width": 320, 288 | "height": 569, 289 | "page_rect": { 290 | "left": 0, 291 | "top": 0, 292 | "width": 411, 293 | "height": 731 294 | } 295 | } 296 | } --------------------------------------------------------------------------------