├── .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('
', $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 | }
--------------------------------------------------------------------------------