From 6d82460a02dec3393490bcab7b619f668f03e6c6 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Fri, 2 Dec 2022 15:22:05 -0800 Subject: [PATCH] feat: implement a new image comparison function (#19166) This patch implements a new image comparison function, codenamed "ssim-cie94". The goal of the new comparison function is to cancel out browser non-determenistic rendering. To use the new comparison function: ```ts await expect(page).toHaveScreenshot({ comparator: 'ssim-cie94', }); ``` As of Nov 30, 2022, we identified the following sources of non-determenistic rendering for Chromium: - Anti-aliasing for certain shapes might be different due to the way skia rasterizes certain shapes. - Color blending might be different on `x86` and `aarch64` architectures. The new function employs a few heuristics to fight these differences. Consider two non-equal image pixels `(r1, g1, b1)` and `(r2, g2, b2)`: 1. If the [CIE94] metric is less then 1.0, then we consider these pixels **EQUAL**. (The value `1.0` is the [just-noticeable difference] for [CIE94].). Otherwise, proceed to next step. 1. If all the 8 neighbors of the first pixel match its color, or if the 8 neighbors of the second pixel match its color, then these pixels are **DIFFERENT**. (In case of anti-aliasing, some of the direct neighbors have to be blended up or down.) Otherwise, proceed to next step. 1. If SSIM in some locality around the different pixels is more than 0.99, then consider this pixels to be **EQUAL**. Otherwise, mark them as **DIFFERENT**. (Local SSIM for anti-aliased pixels turns out to be very close to 1.0). [CIE94]: https://en.wikipedia.org/wiki/Color_difference#CIE94 [just-noticeable difference]: https://en.wikipedia.org/wiki/Just-noticeable_difference --- .gitattributes | 1 - docs/src/api/class-locatorassertions.md | 6 + docs/src/api/class-pageassertions.md | 6 + docs/src/api/class-snapshotassertions.md | 6 + docs/src/api/params.md | 6 + docs/src/test-api/class-testconfig.md | 6 +- docs/src/test-api/class-testproject.md | 6 +- package-lock.json | 13 ++ package.json | 1 + packages/playwright-core/package.json | 4 + .../src/image_tools/colorUtils.ts | 99 ++++++++++++++ .../src/image_tools/compare.ts | 107 +++++++++++++++ .../src/image_tools/imageChannel.ts | 61 +++++++++ .../playwright-core/src/image_tools/stats.ts | 127 ++++++++++++++++++ .../playwright-core/src/protocol/validator.ts | 1 + packages/playwright-core/src/utils/DEPS.list | 1 + .../playwright-core/src/utils/comparators.ts | 30 ++--- .../src/matchers/toMatchSnapshot.ts | 2 + packages/playwright-test/types/test.d.ts | 78 +++++++++-- packages/protocol/src/channels.ts | 2 + packages/protocol/src/protocol.yml | 1 + tests/image_tools/fixtures.spec.ts | 91 +++++++++++++ .../should-fail/julia-ssim-trap/1-actual.png | Bin 0 -> 114 bytes .../julia-ssim-trap/1-expected.png | Bin 0 -> 114 bytes .../should-fail/julia-ssim-trap/README.md | 11 ++ .../should-fail/original-ssim-trap/README.md | 8 ++ .../original-ssim-trap/sample-actual.png | Bin 0 -> 365 bytes .../original-ssim-trap/sample-expected.png | Bin 0 -> 391 bytes .../fixtures/should-fail/trivial/README.md | 3 + .../should-fail/trivial/equal-luma-actual.png | Bin 0 -> 365 bytes .../trivial/equal-luma-expected.png | Bin 0 -> 365 bytes .../should-fail/trivial/opposite-actual.png | Bin 0 -> 113 bytes .../should-fail/trivial/opposite-expected.png | Bin 0 -> 114 bytes .../trivial/single-red-pixel-actual.png | Bin 0 -> 41422 bytes .../trivial/single-red-pixel-expected.png | Bin 0 -> 41414 bytes .../should-match/crbug-919955/README.md | 5 + .../crbug-919955/example-1-actual.png | Bin 0 -> 13903 bytes .../crbug-919955/example-1-expected.png | Bin 0 -> 13911 bytes .../crbug-919955/example-2-actual.png | Bin 0 -> 211 bytes .../crbug-919955/example-2-expected.png | Bin 0 -> 204 bytes .../tiny-antialiasing-sample/README.md | 5 + .../tiny-antialiasing-sample/tiny-actual.png | Bin 0 -> 867 bytes .../tiny-expected.png | Bin 0 -> 868 bytes .../fixtures/should-match/trivial/README.md | 3 + .../should-match/trivial/black-actual.png | Bin 0 -> 113 bytes .../should-match/trivial/black-expected.png | Bin 0 -> 113 bytes .../should-match/trivial/white-actual.png | Bin 0 -> 114 bytes .../should-match/trivial/white-expected.png | Bin 0 -> 114 bytes tests/image_tools/unit.spec.ts | 119 ++++++++++++++++ tests/image_tools/utils.ts | 61 +++++++++ tests/playwright-test/golden.spec.ts | 74 ++++++++++ tests/playwright-test/playwright.config.ts | 11 +- .../to-have-screenshot.spec.ts | 72 ++++++++++ 53 files changed, 996 insertions(+), 31 deletions(-) create mode 100644 packages/playwright-core/src/image_tools/colorUtils.ts create mode 100644 packages/playwright-core/src/image_tools/compare.ts create mode 100644 packages/playwright-core/src/image_tools/imageChannel.ts create mode 100644 packages/playwright-core/src/image_tools/stats.ts create mode 100644 tests/image_tools/fixtures.spec.ts create mode 100644 tests/image_tools/fixtures/should-fail/julia-ssim-trap/1-actual.png create mode 100644 tests/image_tools/fixtures/should-fail/julia-ssim-trap/1-expected.png create mode 100644 tests/image_tools/fixtures/should-fail/julia-ssim-trap/README.md create mode 100644 tests/image_tools/fixtures/should-fail/original-ssim-trap/README.md create mode 100644 tests/image_tools/fixtures/should-fail/original-ssim-trap/sample-actual.png create mode 100644 tests/image_tools/fixtures/should-fail/original-ssim-trap/sample-expected.png create mode 100644 tests/image_tools/fixtures/should-fail/trivial/README.md create mode 100644 tests/image_tools/fixtures/should-fail/trivial/equal-luma-actual.png create mode 100644 tests/image_tools/fixtures/should-fail/trivial/equal-luma-expected.png create mode 100644 tests/image_tools/fixtures/should-fail/trivial/opposite-actual.png create mode 100644 tests/image_tools/fixtures/should-fail/trivial/opposite-expected.png create mode 100644 tests/image_tools/fixtures/should-fail/trivial/single-red-pixel-actual.png create mode 100644 tests/image_tools/fixtures/should-fail/trivial/single-red-pixel-expected.png create mode 100644 tests/image_tools/fixtures/should-match/crbug-919955/README.md create mode 100644 tests/image_tools/fixtures/should-match/crbug-919955/example-1-actual.png create mode 100644 tests/image_tools/fixtures/should-match/crbug-919955/example-1-expected.png create mode 100644 tests/image_tools/fixtures/should-match/crbug-919955/example-2-actual.png create mode 100644 tests/image_tools/fixtures/should-match/crbug-919955/example-2-expected.png create mode 100644 tests/image_tools/fixtures/should-match/tiny-antialiasing-sample/README.md create mode 100644 tests/image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-actual.png create mode 100644 tests/image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-expected.png create mode 100644 tests/image_tools/fixtures/should-match/trivial/README.md create mode 100644 tests/image_tools/fixtures/should-match/trivial/black-actual.png create mode 100644 tests/image_tools/fixtures/should-match/trivial/black-expected.png create mode 100644 tests/image_tools/fixtures/should-match/trivial/white-actual.png create mode 100644 tests/image_tools/fixtures/should-match/trivial/white-expected.png create mode 100644 tests/image_tools/unit.spec.ts create mode 100644 tests/image_tools/utils.ts diff --git a/.gitattributes b/.gitattributes index 653dde78b..c2515b4ff 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,4 @@ # text files must be lf for golden file tests to work * text=auto eol=lf - # make project show as TS on GitHub *.js linguist-detectable=false diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index f7c5e7a2f..c578608a7 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -1362,6 +1362,9 @@ Snapshot name. ### option: LocatorAssertions.toHaveScreenshot#1.scale = %%-screenshot-option-scale-default-css-%% * since: v1.23 +### option: LocatorAssertions.toHaveScreenshot#1.comparator = %%-assertions-comparator-%% +* since: v1.29 + ### option: LocatorAssertions.toHaveScreenshot#1.maxDiffPixels = %%-assertions-max-diff-pixels-%% * since: v1.23 @@ -1405,6 +1408,9 @@ Note that screenshot assertions only work with Playwright test runner. ### option: LocatorAssertions.toHaveScreenshot#2.scale = %%-screenshot-option-scale-default-css-%% * since: v1.23 +### option: LocatorAssertions.toHaveScreenshot#2.comparator = %%-assertions-comparator-%% +* since: v1.29 + ### option: LocatorAssertions.toHaveScreenshot#2.maxDiffPixels = %%-assertions-max-diff-pixels-%% * since: v1.23 diff --git a/docs/src/api/class-pageassertions.md b/docs/src/api/class-pageassertions.md index b1e68dc2f..d52d56243 100644 --- a/docs/src/api/class-pageassertions.md +++ b/docs/src/api/class-pageassertions.md @@ -170,6 +170,9 @@ Snapshot name. ### option: PageAssertions.toHaveScreenshot#1.scale = %%-screenshot-option-scale-default-css-%% * since: v1.23 +### option: PageAssertions.toHaveScreenshot#1.comparator = %%-assertions-comparator-%% +* since: v1.29 + ### option: PageAssertions.toHaveScreenshot#1.maxDiffPixels = %%-assertions-max-diff-pixels-%% * since: v1.23 @@ -218,6 +221,9 @@ Note that screenshot assertions only work with Playwright test runner. ### option: PageAssertions.toHaveScreenshot#2.scale = %%-screenshot-option-scale-default-css-%% * since: v1.23 +### option: PageAssertions.toHaveScreenshot#2.comparator = %%-assertions-comparator-%% +* since: v1.29 + ### option: PageAssertions.toHaveScreenshot#2.maxDiffPixels = %%-assertions-max-diff-pixels-%% * since: v1.23 diff --git a/docs/src/api/class-snapshotassertions.md b/docs/src/api/class-snapshotassertions.md index a61a4752b..faac9465a 100644 --- a/docs/src/api/class-snapshotassertions.md +++ b/docs/src/api/class-snapshotassertions.md @@ -43,6 +43,9 @@ Note that matching snapshots only work with Playwright test runner. Snapshot name. +### option: SnapshotAssertions.toMatchSnapshot#1.comparator = %%-assertions-comparator-%% +* since: v1.29 + ### option: SnapshotAssertions.toMatchSnapshot#1.maxDiffPixels = %%-assertions-max-diff-pixels-%% * since: v1.22 @@ -79,6 +82,9 @@ Learn more about [visual comparisons](../test-snapshots.md). Note that matching snapshots only work with Playwright test runner. +### option: SnapshotAssertions.toMatchSnapshot#2.comparator = %%-assertions-comparator-%% +* since: v1.29 + ### option: SnapshotAssertions.toMatchSnapshot#2.maxDiffPixels = %%-assertions-max-diff-pixels-%% * since: v1.22 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index e74d4847d..a4bd5ab17 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -800,6 +800,12 @@ Time to retry the assertion for. An acceptable amount of pixels that could be different. Default is configurable with `TestConfig.expect`. Unset by default. +## assertions-comparator +* langs: js +- `comparator` <[string]> Either `"pixelmatch"` or `"ssim-cie94"`. + +A comparator function to use when comparing images. + ## assertions-max-diff-pixel-ratio * langs: js - `maxDiffPixelRatio` <[float]> diff --git a/docs/src/test-api/class-testconfig.md b/docs/src/test-api/class-testconfig.md index bcf5c05e4..d799f1c0c 100644 --- a/docs/src/test-api/class-testconfig.md +++ b/docs/src/test-api/class-testconfig.md @@ -39,14 +39,16 @@ export default config; - type: ?<[Object]> - `timeout` ?<[int]> Default timeout for async expect matchers in milliseconds, defaults to 5000ms. - `toHaveScreenshot` ?<[Object]> Configuration for the [`method: PageAssertions.toHaveScreenshot#1`] method. - - `threshold` ?<[float]> an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`. + - `comparator` ?<[string]> a comparator function to use, either `"pixelmatch"` or `"ssim-cie94"`. Defaults to `"pixelmatch"`. + - `threshold` ?<[float]> an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`. `"ssim-cie94"` comparator computes color difference by [CIE94](https://en.wikipedia.org/wiki/Color_difference#CIE94) and defaults `threshold` value to `0.01`. - `maxDiffPixels` ?<[int]> an acceptable amount of pixels that could be different, unset by default. - `maxDiffPixelRatio` ?<[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default. - `animations` ?<[ScreenshotAnimations]<"allow"|"disabled">> See [`option: animations`] in [`method: Page.screenshot`]. Defaults to `"disabled"`. - `caret` ?<[ScreenshotCaret]<"hide"|"initial">> See [`option: caret`] in [`method: Page.screenshot`]. Defaults to `"hide"`. - `scale` ?<[ScreenshotScale]<"css"|"device">> See [`option: scale`] in [`method: Page.screenshot`]. Defaults to `"css"`. - `toMatchSnapshot` ?<[Object]> Configuration for the [`method: SnapshotAssertions.toMatchSnapshot#1`] method. - - `threshold` ?<[float]> an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`. + - `comparator` ?<[string]> a comparator function to use, either `"pixelmatch"` or `"ssim-cie94"`. Defaults to `"pixelmatch"`. + - `threshold` ?<[float]> an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`. `"ssim-cie94"` comparator computes color difference by [CIE94](https://en.wikipedia.org/wiki/Color_difference#CIE94) and defaults `threshold` value to `0.01`. - `maxDiffPixels` ?<[int]> an acceptable amount of pixels that could be different, unset by default. - `maxDiffPixelRatio` ?<[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default. diff --git a/docs/src/test-api/class-testproject.md b/docs/src/test-api/class-testproject.md index 00dbc5741..ca1c99d51 100644 --- a/docs/src/test-api/class-testproject.md +++ b/docs/src/test-api/class-testproject.md @@ -110,14 +110,16 @@ export default config; - type: ?<[Object]> - `timeout` ?<[int]> Default timeout for async expect matchers in milliseconds, defaults to 5000ms. - `toHaveScreenshot` ?<[Object]> Configuration for the [`method: PageAssertions.toHaveScreenshot#1`] method. - - `threshold` ?<[float]> an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`. + - `comparator` ?<[string]> a comparator function to use, either `"pixelmatch"` or `"ssim-cie94"`. Defaults to `"pixelmatch"`. + - `threshold` ?<[float]> an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`. `"ssim-cie94"` comparator computes color difference by [CIE94](https://en.wikipedia.org/wiki/Color_difference#CIE94) and defaults `threshold` value to `0.01`. - `maxDiffPixels` ?<[int]> an acceptable amount of pixels that could be different, unset by default. - `maxDiffPixelRatio` ?<[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default. - `animations` ?<[ScreenshotAnimations]<"allow"|"disabled">> See [`option: animations`] in [`method: Page.screenshot`]. Defaults to `"disabled"`. - `caret` ?<[ScreenshotCaret]<"hide"|"initial">> See [`option: caret`] in [`method: Page.screenshot`]. Defaults to `"hide"`. - `scale` ?<[ScreenshotScale]<"css"|"device">> See [`option: scale`] in [`method: Page.screenshot`]. Defaults to `"css"`. - `toMatchSnapshot` ?<[Object]> Configuration for the [`method: SnapshotAssertions.toMatchSnapshot#1`] method. - - `threshold` ?<[float]> an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`. + - `comparator` ?<[string]> a comparator function to use, either `"pixelmatch"` or `"ssim-cie94"`. Defaults to `"pixelmatch"`. + - `threshold` ?<[float]> an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`. `"ssim-cie94"` comparator computes color difference by [CIE94](https://en.wikipedia.org/wiki/Color_difference#CIE94) and defaults `threshold` value to `0.01`. - `maxDiffPixels` ?<[int]> an acceptable amount of pixels that could be different, unset by default. - `maxDiffPixelRatio` ?<[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default. diff --git a/package-lock.json b/package-lock.json index bbbe20672..79eb26b42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "react-dom": "^18.1.0", "rimraf": "^3.0.2", "socksv5": "0.0.6", + "ssim.js": "^3.5.0", "typescript": "^4.7.3", "vite": "^3.2.1", "ws": "^8.5.0", @@ -4876,6 +4877,12 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/ssim.js": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ssim.js/-/ssim.js-3.5.0.tgz", + "integrity": "sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==", + "dev": true + }, "node_modules/stack-trace": { "version": "0.0.10", "dev": true, @@ -9174,6 +9181,12 @@ "dev": true, "optional": true }, + "ssim.js": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ssim.js/-/ssim.js-3.5.0.tgz", + "integrity": "sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==", + "dev": true + }, "stack-trace": { "version": "0.0.10", "dev": true diff --git a/package.json b/package.json index 23801a83f..4493da908 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "react-dom": "^18.1.0", "rimraf": "^3.0.2", "socksv5": "0.0.6", + "ssim.js": "^3.5.0", "typescript": "^4.7.3", "vite": "^3.2.1", "ws": "^8.5.0", diff --git a/packages/playwright-core/package.json b/packages/playwright-core/package.json index a34ea7b2f..a5a487051 100644 --- a/packages/playwright-core/package.json +++ b/packages/playwright-core/package.json @@ -23,6 +23,10 @@ "./lib/grid/gridServer": "./lib/grid/gridServer.js", "./lib/outofprocess": "./lib/outofprocess.js", "./lib/utils": "./lib/utils/index.js", + "./lib/image_tools/stats": "./lib/image_tools/stats.js", + "./lib/image_tools/compare": "./lib/image_tools/compare.js", + "./lib/image_tools/imageChannel": "./lib/image_tools/imageChannel.js", + "./lib/image_tools/colorUtils": "./lib/image_tools/colorUtils.js", "./lib/common/userAgent": "./lib/common/userAgent.js", "./lib/containers/docker": "./lib/containers/docker.js", "./lib/utils/comparators": "./lib/utils/comparators.js", diff --git a/packages/playwright-core/src/image_tools/colorUtils.ts b/packages/playwright-core/src/image_tools/colorUtils.ts new file mode 100644 index 000000000..1f2a6227c --- /dev/null +++ b/packages/playwright-core/src/image_tools/colorUtils.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function blendWithWhite(c: number, a: number): number { + return 255 + (c - 255) * a; +} + +export function rgb2gray(r: number, g: number, b: number): number { + // NOTE: this is the exact integer formula from SSIM.js. + // See https://github.com/obartra/ssim/blob/ca8e3c6a6ff5f4f2e232239e0c3d91806f3c97d5/src/matlab/rgb2gray.ts#L56 + return (77 * r + 150 * g + 29 * b + 128) >> 8; +} + +// Percieved color difference defined by CIE94. +// See https://en.wikipedia.org/wiki/Color_difference#CIE94 +// +// The result of 1.0 is a "just-noticiable difference". +// +// Other results interpretation (taken from http://zschuessler.github.io/DeltaE/learn/): +// < 1.0 Not perceptible by human eyes. +// 1-2 Perceptible through close observation. +// 2-10 Perceptible at a glance. +// 11-49 Colors are more similar than opposite +// 100 Colors are exact opposite +export function colorDeltaE94(rgb1: number[], rgb2: number[]) { + const [l1, a1, b1] = xyz2lab(srgb2xyz(rgb1)); + const [l2, a2, b2] = xyz2lab(srgb2xyz(rgb2)); + const deltaL = l1 - l2; + const deltaA = a1 - a2; + const deltaB = b1 - b2; + const c1 = Math.sqrt(a1 ** 2 + b1 ** 2); + const c2 = Math.sqrt(a2 ** 2 + b2 ** 2); + const deltaC = c1 - c2; + let deltaH = deltaA ** 2 + deltaB ** 2 - deltaC ** 2; + deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH); + // The k1, k2, kL, kC, kH values for "graphic arts" applications. + // See https://en.wikipedia.org/wiki/Color_difference#CIE94 + const k1 = 0.045; + const k2 = 0.015; + const kL = 1; + const kC = 1; + const kH = 1; + + const sC = 1.0 + k1 * c1; + const sH = 1.0 + k2 * c1; + const sL = 1; + + return Math.sqrt((deltaL / sL / kL) ** 2 + (deltaC / sC / kC) ** 2 + (deltaH / sH / kH) ** 2); +} + +// sRGB -> 1-normalized XYZ (i.e. Y ∈ [0, 1]) with D65 illuminant +// See https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ +export function srgb2xyz(rgb: number[]): number[] { + let r = rgb[0] / 255; + let g = rgb[1] / 255; + let b = rgb[2] / 255; + r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; + g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; + b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; + return [ + (r * 0.4124 + g * 0.3576 + b * 0.1805), + (r * 0.2126 + g * 0.7152 + b * 0.0722), + (r * 0.0193 + g * 0.1192 + b * 0.9505), + ]; +} + +const sigma_pow2 = 6 * 6 / 29 / 29; +const sigma_pow3 = 6 * 6 * 6 / 29 / 29 / 29; + +// 1-normalized CIE XYZ with D65 to L*a*b* +// See https://en.wikipedia.org/wiki/CIELAB_color_space#From_CIEXYZ_to_CIELAB +export function xyz2lab(xyz: number[]): number[] { + const x = xyz[0] / 0.950489; + const y = xyz[1]; + const z = xyz[2] / 1.088840; + + const fx = x > sigma_pow3 ? x ** (1 / 3) : x / 3 / sigma_pow2 + 4 / 29; + const fy = y > sigma_pow3 ? y ** (1 / 3) : y / 3 / sigma_pow2 + 4 / 29; + const fz = z > sigma_pow3 ? z ** (1 / 3) : z / 3 / sigma_pow2 + 4 / 29; + + const l = 116 * fy - 16; + const a = 500 * (fx - fy); + const b = 200 * (fy - fz); + + return [l, a, b]; +} diff --git a/packages/playwright-core/src/image_tools/compare.ts b/packages/playwright-core/src/image_tools/compare.ts new file mode 100644 index 000000000..2748d5a91 --- /dev/null +++ b/packages/playwright-core/src/image_tools/compare.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { blendWithWhite, colorDeltaE94, rgb2gray } from './colorUtils'; +import { ImageChannel } from './imageChannel'; +import { ssim, FastStats } from './stats'; + +const SSIM_WINDOW_RADIUS = 5; +const VARIANCE_WINDOW_RADIUS = 1; + +function drawPixel(width: number, data: Buffer, x: number, y: number, r: number, g: number, b: number) { + const idx = (y * width + x) * 4; + data[idx + 0] = r; + data[idx + 1] = g; + data[idx + 2] = b; + data[idx + 3] = 255; +} + +type CompareOptions = { + maxColorDeltaE94: number; +}; + +export function compare(actual: Buffer, expected: Buffer, diff: Buffer, width: number, height: number, options: CompareOptions) { + const { + maxColorDeltaE94 + } = options; + const [r1, g1, b1] = ImageChannel.intoRGB(width, height, expected); + const [r2, g2, b2] = ImageChannel.intoRGB(width, height, actual); + + const drawRedPixel = (x: number, y: number) => drawPixel(width, diff, x, y, 255, 0, 0); + const drawYellowPixel = (x: number, y: number) => drawPixel(width, diff, x, y, 255, 255, 0); + const drawGrayPixel = (x: number, y: number) => { + const gray = rgb2gray(r1.get(x, y), g1.get(x, y), b1.get(x, y)); + const value = blendWithWhite(gray, 0.1); + drawPixel(width, diff, x, y, value, value, value); + }; + + let fastR, fastG, fastB; + + let diffCount = 0; + for (let y = 0; y < height; ++y){ + for (let x = 0; x < width; ++x) { + // Fast-path: equal pixels. + if (r1.get(x, y) === r2.get(x, y) && g1.get(x, y) === g2.get(x, y) && b1.get(x, y) === b2.get(x, y)) { + drawGrayPixel(x, y); + continue; + } + + // Compare pixel colors using the dE94 color difference formulae. + // The dE94 is normalized so that the value of 1.0 is the "just-noticeable-difference". + // Color difference below 1.0 is not noticeable to a human eye, so we can disregard it. + // See https://en.wikipedia.org/wiki/Color_difference + const delta = colorDeltaE94( + [r1.get(x, y), g1.get(x, y), b1.get(x, y)], + [r2.get(x, y), g2.get(x, y), b2.get(x, y)] + ); + + if (delta <= maxColorDeltaE94) { + drawGrayPixel(x, y); + continue; + } + + // if this pixel is a part of a flood fill of a 3x3 square then it cannot be + // anti-aliasing pixel so it must be a pixel difference. + if (!fastR || !fastG || !fastB) { + fastR = new FastStats(r1, r2); + fastG = new FastStats(g1, g2); + fastB = new FastStats(b1, b2); + } + const [varX1, varY1] = r1.boundXY(x - VARIANCE_WINDOW_RADIUS, y - VARIANCE_WINDOW_RADIUS); + const [varX2, varY2] = r1.boundXY(x + VARIANCE_WINDOW_RADIUS, y + VARIANCE_WINDOW_RADIUS); + const var1 = fastR.varianceC1(varX1, varY1, varX2, varY2) + fastG.varianceC1(varX1, varY1, varX2, varY2) + fastB.varianceC1(varX1, varY1, varX2, varY2); + const var2 = fastR.varianceC2(varX1, varY1, varX2, varY2) + fastG.varianceC2(varX1, varY1, varX2, varY2) + fastB.varianceC2(varX1, varY1, varX2, varY2); + if (var1 === 0 && var2 === 0) { + drawRedPixel(x, y); + ++diffCount; + continue; + } + + const [ssimX1, ssimY1] = r1.boundXY(x - SSIM_WINDOW_RADIUS, y - SSIM_WINDOW_RADIUS); + const [ssimX2, ssimY2] = r1.boundXY(x + SSIM_WINDOW_RADIUS, y + SSIM_WINDOW_RADIUS); + const ssimRGB = (ssim(fastR, ssimX1, ssimY1, ssimX2, ssimY2) + ssim(fastG, ssimX1, ssimY1, ssimX2, ssimY2) + ssim(fastB, ssimX1, ssimY1, ssimX2, ssimY2)) / 3.0; + const isAntialiassed = ssimRGB >= 0.99; + if (isAntialiassed) { + drawYellowPixel(x, y); + } else { + drawRedPixel(x, y); + ++diffCount; + } + } + } + + return diffCount; +} diff --git a/packages/playwright-core/src/image_tools/imageChannel.ts b/packages/playwright-core/src/image_tools/imageChannel.ts new file mode 100644 index 000000000..a9c4687f8 --- /dev/null +++ b/packages/playwright-core/src/image_tools/imageChannel.ts @@ -0,0 +1,61 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { blendWithWhite } from './colorUtils'; + +export class ImageChannel { + data: Uint8Array; + width: number; + height: number; + + static intoRGB(width: number, height: number, data: Buffer): ImageChannel[] { + const r = new Uint8Array(width * height); + const g = new Uint8Array(width * height); + const b = new Uint8Array(width * height); + for (let y = 0; y < height; ++y) { + for (let x = 0; x < width; ++x) { + const index = y * width + x; + const offset = index * 4; + const alpha = data[offset + 3] === 255 ? 1 : data[offset + 3] / 255; + r[index] = blendWithWhite(data[offset], alpha); + g[index] = blendWithWhite(data[offset + 1], alpha); + b[index] = blendWithWhite(data[offset + 2], alpha); + } + } + return [ + new ImageChannel(width, height, r), + new ImageChannel(width, height, g), + new ImageChannel(width, height, b), + ]; + } + + constructor(width: number, height: number, data: Uint8Array) { + this.data = data; + this.width = width; + this.height = height; + } + + get(x: number, y: number) { + return this.data[y * this.width + x]; + } + + boundXY(x: number, y: number) { + return [ + Math.min(Math.max(x, 0), this.width - 1), + Math.min(Math.max(y, 0), this.height - 1), + ]; + } +} diff --git a/packages/playwright-core/src/image_tools/stats.ts b/packages/playwright-core/src/image_tools/stats.ts new file mode 100644 index 000000000..b35371b4a --- /dev/null +++ b/packages/playwright-core/src/image_tools/stats.ts @@ -0,0 +1,127 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ImageChannel } from './imageChannel'; + +export interface Stats { + c1: ImageChannel; + c2: ImageChannel; + + // Compute mean value. See https://en.wikipedia.org/wiki/Mean + meanC1(x1: number, y1: number, x2: number, y2: number): number; + meanC2(x1: number, y1: number, x2: number, y2: number): number; + // Compute **population** (not sample) variance. See https://en.wikipedia.org/wiki/Variance + varianceC1(x1: number, y1: number, x2: number, y2: number): number; + varianceC2(x1: number, y1: number, x2: number, y2: number): number; + // Compute covariance. See https://en.wikipedia.org/wiki/Covariance + covariance(x1: number, y1: number, x2: number, y2: number): number; +} + +// Image channel has a 8-bit depth. +const DYNAMIC_RANGE = 2 ** 8 - 1; + +export function ssim(stats: Stats, x1: number, y1: number, x2: number, y2: number): number { + const mean1 = stats.meanC1(x1, y1, x2, y2); + const mean2 = stats.meanC2(x1, y1, x2, y2); + const var1 = stats.varianceC1(x1, y1, x2, y2); + const var2 = stats.varianceC2(x1, y1, x2, y2); + const cov = stats.covariance(x1, y1, x2, y2); + const c1 = (0.01 * DYNAMIC_RANGE) ** 2; + const c2 = (0.03 * DYNAMIC_RANGE) ** 2; + return (2 * mean1 * mean2 + c1) * (2 * cov + c2) / (mean1 ** 2 + mean2 ** 2 + c1) / (var1 + var2 + c2); +} + +export class FastStats implements Stats { + c1: ImageChannel; + c2: ImageChannel; + + private _partialSumC1: number[]; + private _partialSumC2: number[]; + private _partialSumMult: number[]; + private _partialSumSq1: number[]; + private _partialSumSq2: number[]; + + constructor(c1: ImageChannel, c2: ImageChannel) { + this.c1 = c1; + this.c2 = c2; + const { width, height } = c1; + + this._partialSumC1 = new Array(width * height); + this._partialSumC2 = new Array(width * height); + this._partialSumSq1 = new Array(width * height); + this._partialSumSq2 = new Array(width * height); + this._partialSumMult = new Array(width * height); + + const recalc = (mx: number[], idx: number, initial: number, x: number, y: number) => { + mx[idx] = initial; + if (y > 0) + mx[idx] += mx[(y - 1) * width + x]; + if (x > 0) + mx[idx] += mx[y * width + x - 1]; + if (x > 0 && y > 0) + mx[idx] -= mx[(y - 1) * width + x - 1]; + }; + + for (let y = 0; y < height; ++y) { + for (let x = 0; x < width; ++x) { + const idx = y * width + x; + recalc(this._partialSumC1, idx, this.c1.data[idx], x, y); + recalc(this._partialSumC2, idx, this.c2.data[idx], x, y); + recalc(this._partialSumSq1, idx, this.c1.data[idx] * this.c1.data[idx], x, y); + recalc(this._partialSumSq2, idx, this.c2.data[idx] * this.c2.data[idx], x, y); + recalc(this._partialSumMult, idx, this.c1.data[idx] * this.c2.data[idx], x, y); + } + } + } + + _sum(partialSum: number[], x1: number, y1: number, x2: number, y2: number): number { + const width = this.c1.width; + let result = partialSum[y2 * width + x2]; + if (y1 > 0) + result -= partialSum[(y1 - 1) * width + x2]; + if (x1 > 0) + result -= partialSum[y2 * width + x1 - 1]; + if (x1 > 0 && y1 > 0) + result += partialSum[(y1 - 1) * width + x1 - 1]; + return result; + } + + meanC1(x1: number, y1: number, x2: number, y2: number): number { + const N = (y2 - y1 + 1) * (x2 - x1 + 1); + return this._sum(this._partialSumC1, x1, y1, x2, y2) / N; + } + + meanC2(x1: number, y1: number, x2: number, y2: number): number { + const N = (y2 - y1 + 1) * (x2 - x1 + 1); + return this._sum(this._partialSumC2, x1, y1, x2, y2) / N; + } + + varianceC1(x1: number, y1: number, x2: number, y2: number): number { + const N = (y2 - y1 + 1) * (x2 - x1 + 1); + return (this._sum(this._partialSumSq1, x1, y1, x2, y2) - (this._sum(this._partialSumC1, x1, y1, x2, y2) ** 2) / N) / N; + } + + varianceC2(x1: number, y1: number, x2: number, y2: number): number { + const N = (y2 - y1 + 1) * (x2 - x1 + 1); + return (this._sum(this._partialSumSq2, x1, y1, x2, y2) - (this._sum(this._partialSumC2, x1, y1, x2, y2) ** 2) / N) / N; + } + + covariance(x1: number, y1: number, x2: number, y2: number): number { + const N = (y2 - y1 + 1) * (x2 - x1 + 1); + return (this._sum(this._partialSumMult, x1, y1, x2, y2) - this._sum(this._partialSumC1, x1, y1, x2, y2) * this._sum(this._partialSumC2, x1, y1, x2, y2) / N) / N; + } +} + diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 5a5133206..4422903c9 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -987,6 +987,7 @@ scheme.PageExpectScreenshotParams = tObject({ selector: tString, })), comparatorOptions: tOptional(tObject({ + comparator: tOptional(tString), maxDiffPixels: tOptional(tNumber), maxDiffPixelRatio: tOptional(tNumber), threshold: tOptional(tNumber), diff --git a/packages/playwright-core/src/utils/DEPS.list b/packages/playwright-core/src/utils/DEPS.list index e97f2d379..514f09d40 100644 --- a/packages/playwright-core/src/utils/DEPS.list +++ b/packages/playwright-core/src/utils/DEPS.list @@ -2,5 +2,6 @@ ./ ../third_party/diff_match_patch ../third_party/pixelmatch +../image_tools/compare.ts ../utilsBundle.ts ../zipBundle.ts diff --git a/packages/playwright-core/src/utils/comparators.ts b/packages/playwright-core/src/utils/comparators.ts index 029764bbe..a1279d6e3 100644 --- a/packages/playwright-core/src/utils/comparators.ts +++ b/packages/playwright-core/src/utils/comparators.ts @@ -17,26 +17,17 @@ import { colors, jpegjs } from '../utilsBundle'; import pixelmatch from '../third_party/pixelmatch'; +import { compare } from '../image_tools/compare'; import { diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL } from '../third_party/diff_match_patch'; import { PNG } from '../utilsBundle'; -export type ImageComparatorOptions = { threshold?: number, maxDiffPixels?: number, maxDiffPixelRatio?: number }; +export type ImageComparatorOptions = { threshold?: number, maxDiffPixels?: number, maxDiffPixelRatio?: number, comparator?: string }; export type ComparatorResult = { diff?: Buffer; errorMessage: string; } | null; export type Comparator = (actualBuffer: Buffer | string, expectedBuffer: Buffer, options?: any) => ComparatorResult; -let customPNGComparator: Comparator | undefined; -if (process.env.PW_CUSTOM_PNG_COMPARATOR) { - try { - customPNGComparator = require(process.env.PW_CUSTOM_PNG_COMPARATOR); - if (typeof customPNGComparator !== 'function') - customPNGComparator = undefined; - } catch (e) { - } -} - export function getComparator(mimeType: string): Comparator { if (mimeType === 'image/png') - return customPNGComparator ?? compareImages.bind(null, 'image/png'); + return compareImages.bind(null, 'image/png'); if (mimeType === 'image/jpeg') return compareImages.bind(null, 'image/jpeg'); if (mimeType === 'text/plain') @@ -68,9 +59,18 @@ function compareImages(mimeType: string, actualBuffer: Buffer | string, expected }; } const diff = new PNG({ width: expected.width, height: expected.height }); - const count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, { - threshold: options.threshold ?? 0.2, - }); + let count; + if (options.comparator === 'ssim-cie94') { + count = compare(expected.data, actual.data, diff.data, expected.width, expected.height, { + maxColorDeltaE94: (options.threshold ?? 0.01) * 100, + }); + } else if ((options.comparator ?? 'pixelmatch') === 'pixelmatch') { + count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, { + threshold: options.threshold ?? 0.2, + }); + } else { + throw new Error(`Configuration specifies unknown comparator "${options.comparator}"`); + } const maxDiffPixels1 = options.maxDiffPixels; const maxDiffPixels2 = options.maxDiffPixelRatio !== undefined ? expected.width * expected.height * options.maxDiffPixelRatio : undefined; diff --git a/packages/playwright-test/src/matchers/toMatchSnapshot.ts b/packages/playwright-test/src/matchers/toMatchSnapshot.ts index ba4b21952..f08ea3778 100644 --- a/packages/playwright-test/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright-test/src/matchers/toMatchSnapshot.ts @@ -145,6 +145,7 @@ class SnapshotHelper { maxDiffPixels: options.maxDiffPixels, maxDiffPixelRatio: options.maxDiffPixelRatio, threshold: options.threshold, + comparator: options.comparator, }; this.kind = this.mimeType.startsWith('image/') ? 'Screenshot' : 'Snapshot'; } @@ -305,6 +306,7 @@ export async function toHaveScreenshot( const helper = new SnapshotHelper( testInfo, snapshotPathResolver, 'png', { + comparator: config?.comparator, maxDiffPixels: config?.maxDiffPixels, maxDiffPixelRatio: config?.maxDiffPixelRatio, threshold: config?.threshold, diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index 956d8afc6..fdd1d0a70 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -534,8 +534,16 @@ interface TestConfig { */ toHaveScreenshot?: { /** - * an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the - * same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`. + * a comparator function to use, either `"pixelmatch"` or `"ssim-cie94"`. Defaults to `"pixelmatch"`. + */ + comparator?: string; + + /** + * an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and + * `1` (lax). `"pixelmatch"` comparator computes color difference in + * [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`. `"ssim-cie94"` + * comparator computes color difference by [CIE94](https://en.wikipedia.org/wiki/Color_difference#CIE94) and defaults + * `threshold` value to `0.01`. */ threshold?: number; @@ -576,8 +584,16 @@ interface TestConfig { */ toMatchSnapshot?: { /** - * an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the - * same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`. + * a comparator function to use, either `"pixelmatch"` or `"ssim-cie94"`. Defaults to `"pixelmatch"`. + */ + comparator?: string; + + /** + * an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and + * `1` (lax). `"pixelmatch"` comparator computes color difference in + * [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`. `"ssim-cie94"` + * comparator computes color difference by [CIE94](https://en.wikipedia.org/wiki/Color_difference#CIE94) and defaults + * `threshold` value to `0.01`. */ threshold?: number; @@ -3845,6 +3861,11 @@ interface LocatorAssertions { */ caret?: "hide"|"initial"; + /** + * A comparator function to use when comparing images. + */ + comparator?: string; + /** * Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink * box `#FF00FF` that completely covers its bounding box. @@ -3922,6 +3943,11 @@ interface LocatorAssertions { */ caret?: "hide"|"initial"; + /** + * A comparator function to use when comparing images. + */ + comparator?: string; + /** * Specify locators that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink * box `#FF00FF` that completely covers its bounding box. @@ -4170,6 +4196,11 @@ interface PageAssertions { height: number; }; + /** + * A comparator function to use when comparing images. + */ + comparator?: string; + /** * When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to * `false`. @@ -4277,6 +4308,11 @@ interface PageAssertions { height: number; }; + /** + * A comparator function to use when comparing images. + */ + comparator?: string; + /** * When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to * `false`. @@ -4407,6 +4443,11 @@ interface SnapshotAssertions { * @param options */ toMatchSnapshot(name: string|Array, options?: { + /** + * A comparator function to use when comparing images. + */ + comparator?: string; + /** * An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1`. Default is * configurable with `TestConfig.expect`. Unset by default. @@ -4455,6 +4496,11 @@ interface SnapshotAssertions { * @param options */ toMatchSnapshot(options?: { + /** + * A comparator function to use when comparing images. + */ + comparator?: string; + /** * An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1`. Default is * configurable with `TestConfig.expect`. Unset by default. @@ -4581,8 +4627,16 @@ interface TestProject { */ toHaveScreenshot?: { /** - * an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the - * same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`. + * a comparator function to use, either `"pixelmatch"` or `"ssim-cie94"`. Defaults to `"pixelmatch"`. + */ + comparator?: string; + + /** + * an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and + * `1` (lax). `"pixelmatch"` comparator computes color difference in + * [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`. `"ssim-cie94"` + * comparator computes color difference by [CIE94](https://en.wikipedia.org/wiki/Color_difference#CIE94) and defaults + * `threshold` value to `0.01`. */ threshold?: number; @@ -4623,8 +4677,16 @@ interface TestProject { */ toMatchSnapshot?: { /** - * an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the - * same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`. + * a comparator function to use, either `"pixelmatch"` or `"ssim-cie94"`. Defaults to `"pixelmatch"`. + */ + comparator?: string; + + /** + * an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and + * `1` (lax). `"pixelmatch"` comparator computes color difference in + * [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`. `"ssim-cie94"` + * comparator computes color difference by [CIE94](https://en.wikipedia.org/wiki/Color_difference#CIE94) and defaults + * `threshold` value to `0.01`. */ threshold?: number; diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 6b451a99a..985c8c405 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1825,6 +1825,7 @@ export type PageExpectScreenshotParams = { selector: string, }, comparatorOptions?: { + comparator?: string, maxDiffPixels?: number, maxDiffPixelRatio?: number, threshold?: number, @@ -1850,6 +1851,7 @@ export type PageExpectScreenshotOptions = { selector: string, }, comparatorOptions?: { + comparator?: string, maxDiffPixels?: number, maxDiffPixelRatio?: number, threshold?: number, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index b630f0d18..eb1ee8a66 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1266,6 +1266,7 @@ Page: comparatorOptions: type: object? properties: + comparator: string? maxDiffPixels: number? maxDiffPixelRatio: number? threshold: number? diff --git a/tests/image_tools/fixtures.spec.ts b/tests/image_tools/fixtures.spec.ts new file mode 100644 index 000000000..11dbf1e29 --- /dev/null +++ b/tests/image_tools/fixtures.spec.ts @@ -0,0 +1,91 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from '../playwright-test/stable-test-runner'; +import { PNG } from 'playwright-core/lib/utilsBundle'; +import { compare } from 'playwright-core/lib/image_tools/compare'; +import fs from 'fs'; +import path from 'path'; + +function listFixtures(root: string, fixtures: Set = new Set()) { + for (const item of fs.readdirSync(root, { withFileTypes: true })) { + const p = path.join(root, item.name); + if (item.isDirectory()) + listFixtures(p, fixtures); + else if (item.isFile() && p.endsWith('-actual.png')) + fixtures.add(p.substring(0, p.length - '-actual.png'.length)); + } + return fixtures; +} + +const FIXTURES_DIR = path.join(__dirname, 'fixtures'); + +function declareFixtureTest(fixtureRoot: string, fixtureName: string, shouldMatch: boolean) { + test(path.relative(fixtureRoot, fixtureName), async ({}, testInfo) => { + const [actual, expected] = await Promise.all([ + fs.promises.readFile(fixtureName + '-actual.png'), + fs.promises.readFile(fixtureName + '-expected.png'), + ]); + testInfo.attach(fixtureName + '-actual.png', { + body: actual, + contentType: 'image/png', + }); + testInfo.attach(fixtureName + '-expected.png', { + body: expected, + contentType: 'image/png', + }); + const actualPNG = PNG.sync.read(actual); + const expectedPNG = PNG.sync.read(expected); + expect(actualPNG.width).toBe(expectedPNG.width); + expect(actualPNG.height).toBe(expectedPNG.height); + + const diffPNG = new PNG({ width: actualPNG.width, height: actualPNG.height }); + const diffCount = compare(actualPNG.data, expectedPNG.data, diffPNG.data, actualPNG.width, actualPNG.height, { + maxColorDeltaE94: 1.0, + }); + + testInfo.attach(fixtureName + '-diff.png', { + body: PNG.sync.write(diffPNG), + contentType: 'image/png', + }); + + if (shouldMatch) + expect(diffCount).toBe(0); + else + expect(diffCount).not.toBe(0); + }); +} + +test.describe('basic fixtures', () => { + test.describe.configure({ mode: 'parallel' }); + + for (const fixtureName of listFixtures(path.join(FIXTURES_DIR, 'should-match'))) + declareFixtureTest(FIXTURES_DIR, fixtureName, true /* shouldMatch */); + for (const fixtureName of listFixtures(path.join(FIXTURES_DIR, 'should-fail'))) + declareFixtureTest(FIXTURES_DIR, fixtureName, false /* shouldMatch */); +}); + +const customImageToolsFixtures = process.env.IMAGE_TOOLS_FIXTURES; +if (customImageToolsFixtures) { + test.describe('custom fixtures', () => { + test.describe.configure({ mode: 'parallel' }); + + for (const fixtureName of listFixtures(path.join(customImageToolsFixtures, 'should-match'))) + declareFixtureTest(customImageToolsFixtures, fixtureName, true /* shouldMatch */); + for (const fixtureName of listFixtures(path.join(customImageToolsFixtures, 'should-fail'))) + declareFixtureTest(customImageToolsFixtures, fixtureName, false /* shouldMatch */); + }); +} diff --git a/tests/image_tools/fixtures/should-fail/julia-ssim-trap/1-actual.png b/tests/image_tools/fixtures/should-fail/julia-ssim-trap/1-actual.png new file mode 100644 index 0000000000000000000000000000000000000000..11bce6daa31a86e8dac1e44036b579fffd4dc69d GIT binary patch literal 114 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~9Zwg>5DWk0 zKmY&#uV>R@Iu^*R$#?dm!4eA&Nw>oVCOjuO8ZV|eyDeCl$;jYu=YRC^;;!pJeGHzi KelF{r5}E)Nu^|uu literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-fail/julia-ssim-trap/1-expected.png b/tests/image_tools/fixtures/should-fail/julia-ssim-trap/1-expected.png new file mode 100644 index 0000000000000000000000000000000000000000..16646f5e63226eca432b29ac90893714f34cd5f2 GIT binary patch literal 114 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~9Zwg>5DWk0 zKmQ(IuV>R@Iu^*R$#?dm!4eA&Nw>oVCOjuO8ZV|eyDeCl$;hDa-p}+j)5=#seGHzi KelF{r5}E-1g&;Km literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-fail/julia-ssim-trap/README.md b/tests/image_tools/fixtures/should-fail/julia-ssim-trap/README.md new file mode 100644 index 000000000..b34cebb95 --- /dev/null +++ b/tests/image_tools/fixtures/should-fail/julia-ssim-trap/README.md @@ -0,0 +1,11 @@ +# Julia SSIM trap + +[SSIM](https://en.wikipedia.org/wiki/Structural_similarity) is a metric used to compare image similarity. + +While original SSIM is computed against the luma channel (i.e. in a gray-scale), +the Julia language [computes a weighted combination of per-channel SSIM's](https://github.com/JuliaImages/ImageQualityIndexes.jl/blob/e014cee9bef7023a1047b6eb0cbe49fbf28f2fed/src/ssim.jl#L39-L41). + +This sample is a white image and a gray image that are reported equal by Julia SSIM. +It also traps all the suggestions for color-weighted SSIM given here: +https://dsp.stackexchange.com/questions/75187/how-to-apply-the-ssim-measure-on-rgb-images + diff --git a/tests/image_tools/fixtures/should-fail/original-ssim-trap/README.md b/tests/image_tools/fixtures/should-fail/original-ssim-trap/README.md new file mode 100644 index 000000000..1bea83201 --- /dev/null +++ b/tests/image_tools/fixtures/should-fail/original-ssim-trap/README.md @@ -0,0 +1,8 @@ +# Original SSIM trap + +[SSIM](https://en.wikipedia.org/wiki/Structural_similarity) is a metric used to compare image similarity. + +The sample provides two different images. However, since the original SSIM implementation +[converts images into +gray-scale](https://github.com/obartra/ssim/blob/ca8e3c6a6ff5f4f2e232239e0c3d91806f3c97d5/src/index.ts#L104), +SSIM metric will yield a perfect match for these images. diff --git a/tests/image_tools/fixtures/should-fail/original-ssim-trap/sample-actual.png b/tests/image_tools/fixtures/should-fail/original-ssim-trap/sample-actual.png new file mode 100644 index 0000000000000000000000000000000000000000..467a057dec6edec87c04e348156d1c6c20aa92bf GIT binary patch literal 365 zcmeAS@N?(olHy`uVBq!ia0vp^DIm14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>Y#{W#Z_kbMs5>H=O_GjF3B4Qdp?}mab6-Z9>D`Q~b-oU`X3NjiD;@*T= z0vRHnE{-7NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^> z;_2(k{)}5rM9hd~|C&mmkU(;xUl{`f_XY+AR**?x5cej`63DRkba4#HxcBy~BO}li zECwp#v0a~Uajn_@>Dj{x9x6heE=m(|QYvp4d;%Fg-e3IN4m3)JNMC9x#cD!C{X zNHG{07#Zmr8tNJwgcurGnHX9b7-<_ASQ!|6V-a#d(U6;;l9^VCTSI%~sy{#t8gLs* oGILXlOA>Pnko6cDSQ!~vnHoSW5$Gx12h_vh>FVdQ&MBb@0ISz%uK)l5 literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-fail/trivial/README.md b/tests/image_tools/fixtures/should-fail/trivial/README.md new file mode 100644 index 000000000..01e9e6af5 --- /dev/null +++ b/tests/image_tools/fixtures/should-fail/trivial/README.md @@ -0,0 +1,3 @@ +# Trivial failing examples + +Some trivial failing examples diff --git a/tests/image_tools/fixtures/should-fail/trivial/equal-luma-actual.png b/tests/image_tools/fixtures/should-fail/trivial/equal-luma-actual.png new file mode 100644 index 0000000000000000000000000000000000000000..b405bbe3c023e35ffa14d755f4a8b0dec1855768 GIT binary patch literal 365 zcmeAS@N?(olHy`uVBq!ia0vp^DIm14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>Y#{W#Z_kbMs5>H=O_GjF3BGL?%$C|eQg#?ll{mK{^xHkYb=K%R&z!P$< z56BSlba4!^IGvmz!MZp>q^Bv+!JmPFwSs|BvvB7hpuB2{YeY#(Vo9o1a#1RfVlXl= zGSW3P)HOB;F*LF=F|;x;(l#)#GBEhYBIJOgAvZrIGp!Q0hW5x+AR9E`Hk4%MrWThZ h<`y99F*2|+G6xzDv1Fg?hIv3e44$rjF6*2UngE6DTY&%o literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-fail/trivial/equal-luma-expected.png b/tests/image_tools/fixtures/should-fail/trivial/equal-luma-expected.png new file mode 100644 index 0000000000000000000000000000000000000000..816f9db7816be6c7dfe7575b4989d02e75a2ca9d GIT binary patch literal 365 zcmeAS@N?(olHy`uVBq!ia0vp^DIm14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>) zAW(!c$=lt9@jsL9Js^j@#M9T6{Ta8Mh%`sJEO#4FNFX`UuZ)3#djn8&4v-H9JR#Tm zfD92&7sn8b)5!@Etcw#wdYS?q{23TnD;O9x3wQnj%Bz;RMwFx^mZVxG7o`Fz1|tI_ zBV9v7U1Ng~LnA8_Ln{L#Z36=<1A}iYLJlYza`RI%(<*UmXpdY4vOxoGLrG?CYH>+o iZUM3$BLgcVb1MT2h$X$N{`~~%VeoYIb6Mw<&;$Usgjr+& literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-fail/trivial/opposite-actual.png b/tests/image_tools/fixtures/should-fail/trivial/opposite-actual.png new file mode 100644 index 0000000000000000000000000000000000000000..cfe88317b82d7b51feb5ed45629940d6fd640cfc GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~ZBG}+5DWk0 zl!SyI=N()RSY#<2U1%xQCc>_GI76bZO;J+OU{=lv4ighy2Aw7m#x+j$Qb27Cp00i_ I>zopr0Da^fQ2+n{ literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-fail/trivial/opposite-expected.png b/tests/image_tools/fixtures/should-fail/trivial/opposite-expected.png new file mode 100644 index 0000000000000000000000000000000000000000..11bce6daa31a86e8dac1e44036b579fffd4dc69d GIT binary patch literal 114 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~9Zwg>5DWk0 zKmY&#uV>R@Iu^*R$#?dm!4eA&Nw>oVCOjuO8ZV|eyDeCl$;jYu=YRC^;;!pJeGHzi KelF{r5}E)Nu^|uu literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-fail/trivial/single-red-pixel-actual.png b/tests/image_tools/fixtures/should-fail/trivial/single-red-pixel-actual.png new file mode 100644 index 0000000000000000000000000000000000000000..f102657460feb0dd0fd7fcd7985ef40b4544fdd5 GIT binary patch literal 41422 zcmeEtXHZjJ+b$|Rpol14s)9g}CP=R$y@n#aDOGy!O+iKJy<_MgRch!_frO6qPK3}C z2t5!8oZ$1$eCOwxIX}*vnRkW^duM00d);ka*S)`JYblf6p}#{wKtQIdqM%DaaPtrT zQ%Q0Y{|ze#NEQF@hL^7L%Nw(OJYo2o+wLmHUIYX*?AMmN?8+u;ab@W7o@-!H!_w>$q$htSs4}|}d z!2vP75sbWNK_4^>Kiq#w@)&c;HBL>UcApA={X|So`_+H1{wst3>cRh&Ep$k_?BuhA zo<1IL++!eAB$9jF!4n|r77G&UDJUoq92aK&V8au1vU>%OmtI1{uMoi2_Sx4@+pRb50xF`V)xsn+lP|!`*GzAa*>|M!DE)_Iqd#PfVl$>44LiVBK{GLs# zZf!(fnN&ge!GV+8!))^;Mp+NR>qeA>AMaE2wSSC`mB%FjmHxLME~wX(7--*Pl1|&Q zWv$-Y-fcSFMGI8>JO+G7P_Y{^eapB_e2+P6OReW#ctLpHBft&dNooa_U)TrAgWh^T z^-|@@#;~Q(yJqT5T|T$~1u1RrcX!Vho58i^^8^xI~j7c_qW z`BxfWGJ!G--@v+f8QQigJ^6Y4I~nS~g1tc={*q)%@2}sY*!!Oc9s?M7SlG$JSK&K}a-#U0geSOgvyU%K7B3Z7UX ziY8~4@Wjl!A$W(rPE8p&ZE*l|^RH4yAQxH3Iv?B((`9P1^MWxLG%!bkx0sAk>eV*S z+FfQ5v#B57#_cJQqJB7~xXQ8Gs~M>!hG%bo8R|{jRi*3aUeJHn)z*H?yR~c$t9n4R zW;oyE1r9!MV+p336)V*MHTulQIZeAg`R>tAFBZD8TF^6KLz?I!KNVk@v-NzVw?75` zD`=@pIZkGQ%ImP5Q^xBgM|i*GkhYz(GcnmoGp9T;ig|Y$!O^~da$dK+3SsM{leWJs zsF<$ieD>N}?H*O#Q)cEH)lyP7S2t(}vz`M(NzW85uIAegDA6+=r6o;MwF&*@j%VK6 zgR{CfzI}40D}=?Rg(BZkwNV+8=`v_4ImM zX;xMi7(K%d$wQ4M-jqGvv_69`2Av2Ci9p*XFwCf5+|xFFKjr!pZ`#h<3iprdZjaAS z?ue0I>+=lT`C-(gQ`lKHJV@&Jup4~|IIkKN;>fu=Jguop0tI7@W$QOn6LAaHYU15= z=AU*3vm_x^(yJRmwj=4c`r|_nyubyQ80&a`lh*dej>YMuw$S@&6)JCpgA2k&kjB~>`_^`lhQS@bc;B|QO(IT zoAFIM#%lKK+K1AL`Ieu)74{w;UnaUO=5i%_rp&BfV$n#QP+Qx{cXIL_NUplgCVsj^ zYQ72;tq<+#CihBx5enB4l5K5`8iMN@Uz#uLOnx;z@x*do=ynw9@mRyD8O^Lu^zOp` zaAEw_?>uH;q#zLAO zmktY%(k^;SbxN|9LgGHsRj-fIsoz%0v>I`xjE>sUQ*+v}_i0g_Be+t3s%bK)1ZX6_ zQ@BM9IXYop#vA$PyCz23{-&VC*!K~Ut!E!PmeVz-M?6DB9Gx{X+d|o38*+t~m55I| zK=Jan)SP#Dy*?m7rM)F!yHv2kPz&EDA{<3~X?a3T+ zi8P|swu!deCky0{ltp4@BZb;T4E#GYE7n}YgW2N_=Ry!j5cv9K_Dkn_`y)0-bwjAhW#GAWyFKT0``7Ca4cxb+|rIf2k`0tgNj2HDWDluNfxgqWsnDsm_> z{!_3O$l_j@H58a7?e;!LQ=$o@3nIkZLp+lGV3M*k>uEHbB@T7SKhK%K(o>i}}-PtlfuDkmYMxNTMBo=3I%q_R(S;Ji@8;Bz12Qe&w2TdFSL%DsvE^Y1C=e=uyB{ z@N6Y|6lyaz*j>-Tn6#=VMbm~sJzqkw@mWDw>uk={U{-5lwi;ZpN`_ISTEDFC1q0&Q zQ{p-srO%#JRA*5PO`t8$6Y8L#M@}QPMjmR`YGZs&SaCRhf=Ea6DelV$SNnO#NfI^Wg3=z z^Y%CIy2*aRqQ7rtq>w}*9&9W{xJp3#su9@qLoLJ~fJ@SR&*LECo8rpxe&4EQz(V5h zXsDejeuzMUP|V9#Tr* zXb#+I-S@dg+_sdIfSVI*Ol4X8UMs`|Kxx*zO+XC&Y&1shAoqY_mlcTF_FhmemxM)* zyYs0ub9SBRWJDwx7-;8xU|u_xFCJf1`>k)rl}VZS`?OwKb$k1-su>llJ5l+{%(>vz zBYBW+%v466^u!yU&mQNylQ43*TD(+PH9yOAIM*$i6R-ZZ_D;+el!mIK zq>?=41WX>qlWt*{8GDps&vfs7et;t8KzMrC(0WGa0O;%soTN+RGgpav+RlHip}3(( zC#2`lHA?Zp0Qki7ECF#l@g<%;${JqWrspzXVF--X6!NT8!Uw99peCn@U&jpM z0+5x3Gg2&%r^qZpK#i0(CFo>Kn#@MBhJZw-vcEQTIFYZ@<>9=pjAd9$RC}CYHz03Fc zW_p8G^pf!0Kj(|mIKA^@I%`_V;MF&x8fR@dV!ZFmw|H#@teVF-fr%53D&sm8Y6IrG z_!PDzwdpDh(fkhawGILCg0H>xLe3h&sPP$w%M76R03oz7vkMj%G_t~E%`3h{FS6Z& z{_|Lt9mWk1@FR{7-W!Dl>I@iG#)(Jw7VEsPJMg^Dj}y(>y=UJ>goH+8`qHk7%(PLZ z#`PP#Dku*M#2|E|upau8d;PT~v#PH4g2cjB045^-$XMa~5pd1Oxl#kXoO(pJp&MGh zOlPfr;>9ZmvT$?_&PvYSfCSWAnTdDz;nCe)!~!7dj~}%Q+RNs?2FzIJ$%Rv#RPyhqe}mX7RL}uiEp8clryRd9bU> z%{Duz57yAhIz-7tk7~|{dEaA{ngBMMnwT85tUlVb=L?ECw$%2QT<3vG6CuWTBz?C) z)$rz7pz!L25oqA@NBREm*irvY@}Q9{ud7ufSpdV76XPZ&vleuQfMlR!z0kRi+5lg(IWQnP5hu>7V4{XmAeaQ z_)T@p%vMemiOHEpOEF<TAw(UT9!H6aI76J%Nv5wtL@~`}pG!A$+)p=Hk3fmz|g& za!51_*;zNXZD~{iLZ45r#TLzirms)h3(Y;aY0V4>X9IW~p9*|M*TZFoNBF;!=dVUG z(@^zYM`UKCjilejR_rp}#e!K=!3bdJ#&rl?sE=)5g>-%J*tZ;}pr~*$v7|AA|9%#< zbW;}bMqL;+z>KC_j*}V-Ul9|1x}MTykk&%IxrMOn!;SStjnku&4WhNBh+$wI89qsvupSWF zEpO{E`oWB_o27)Dv;8|OGSX#{(-LWDv-BZN>%D`W+=e?*sYzpX`2Fsao9Yr@Wh+dN z@JRCBKAL@QKhhHo<1Na-i;ylNuYKYw7N1gL{;)Hvw&c-S_YRwX7kl5s#m840Rx_F{ zR!6_~r^3zc_?TRhc{{$)6#OHG;p|JtADN_Y&+U{X0@I}CroX4$b=&`J;Ey>KvC;Ru_Wd?vHoXQ+TDHOL*J5UbL@U-%&i_grV%{# zyLpjIneMM1^H@?J&nz|Nw&C5dN6}0}fRk0@B;F>aUn554K;Z?Y1qGgqpEToYI-dvb zJ?!$~Je$fVO!ANus2&04CZ2u{zWrn^+)Aow+a7lsADYj1(6C)~5e!yZi)0D?ot@KL z--vzTb;I*4&AIc05l#=$_(wV+hBRsvwzOmZ+(E6MsK>vc8?hLATLJx^;bERZ zm`^k=N0-HdUGPuMCCM1x?vuX7EMbZ*y}=pXNu9S8^9Xx3iaD%*1X(ZmStnZWHch*& z66$GJrlcFv*?W@*2Fs#;nw#qthCPL^d2MPT#5ca^mEa*IlBlAF%E1}b#a#_omNb~m z&2J-gv=q`yi}$}=ezr>r_Z$)7>#Uu2S*X*z^1dkI>I*i!DL}|>N2JRKs1u1{^B3g5 ziWcRfO<_EOcWBJEyugG?f60v?GkPfSfc3@QUPPESk( zZ|;poM8@?7)aO}hsleZ}YKK$kaj*(7CkZ+Wu5S)1#ViNh1OPD1?X3NqgMQqyaO^~L z_QU$cS}Xr}XCIQNJK+>x_UC|TFj&E={piW^J|8=Dz{!d^J(i}I4?q?hOHG~n;36jH zjhVt|TTCwsDmM!R6Nsm%J5fE(oCYk_-;TPZ`)rM01;#mkeNOpCtunoc6B&sNETw^S z4Zm!T?$F_k;!EsG^CHnRzH=m7mJvA}(t%vDgX%XOT56e=&R9S~3LEeyZJ(!hlvYra zIO@f1HCYz^Qz1U?J5Xd}>~5_qTCc%M3N>yW&T5+HQ$|%7d&W$Th^iB)tKVOlB$vU8 z*&U<0c>1(HTzkAkx-OA{rM0RHD1#%mr9E@e{!{GfOaA^#K@sT_!!L70Jz^pIp$k8n z1-a-9ImmvpJbPP)DORtgMC?(a_ZkCZXvd&Z&x+->QT$XfEsC+RG7ybZEj0;3+n+1N zSi*wRCG%(n<(SSfWEz6LtVsGr#hRO}yEv#`$WbE>>|`;^74`KsbMYL$aso?ykK4-TUD(8} z>7KiW3)Bk43JeB7h~4shBqRCw_ypoK3!%Xe;oG0JLP8?;cb44Vsz>Jao`F0V6D=>s z)|_EuHmQ8ZonvM7IUZO`8|kh4DOqzSg@JY6u4t8HUfAYi02b--(m8Czg%JD`8&h3L zb07&d8_P)SpZA;0S&)K#;pVQ0QU7w{%IzSbO{l5Odftp!BRnhI-_>}7M_{2gN-R{P z+A!oB-7H2H6nqK4mz{4fUJwy|bX9!lwzF#=^!5eMkj(DD7rjy@by6Si85X@}|E&XQ z!k#%-IJ3!Ws;nT2B#_Dua*1(segyuo9AW95p&>c7m*@gUP8l%Nss z0J8bV%2f1R8o1-S0OB&)uUS$I?kqGrM6XHL754XacXN%SCb_wI)$P`LjiYgXi7e{F zeOcM2s1c$fd3~3R%Y=SS5eG2@#wPj72Ws%)XAdn6Ex_??m;~@fy=+u72kVN_*3V zYq;V0?}4AODu%5MsO!l)w=eW-?i)&(5EsS-w?m07Bv7$)ew3HRX~gNpSp)h;VfziL zCqS@3`YM{aOQ6auIrXAn0J}*?$Jy;bkrFiO)*gMvVEnm{ef*n4*K8yiWGR4KR-Y;?vY0lm)CozmZ1qFvpW;{t-fO3gXXM| zCy&uS1~?SgDm|lWusT-zl^=3qu&OWT+7@97WJ1W~b=bfl0~YtUwWqEBX4k;x`tfFQ z3d-4VTSJY`Ut@cxEcKIgnO*x_%(DS2foF;kVonWarq|KY>KAvFE9!%LwpXVIrRTkr z{+X_Z|K`*J<*5|zE(79v>D5nXt5wFH;60<+QQ$f5h6nF((5-*YwwkeLn;y`ZC2Kl* zw`3YTZvNoV^kQQd45trauohfRe!vW_)eZf-9`O8mLckw^33TzmtPE))o4)IkCXQd4 z3t+yuh&83Zc!Y#mZz}2-ArdGk1)8?A=qm`X~DPB331gYP3OU!6}5pZLG3c8c?z z=b}o%0uri{E$LcvEtovwcLLulVGjcE=ipF_u#l0HsfN%dWvQ4PF`u~%nKK>B9Ra1X z!xw2n>*I{yq_qjvv{(7-fIU()s4vWi47S2h%dnWW4G}2Q$)A>V zi{rzz2>hw<{HPtDh({f4AF=|TcIa!z#DNm!p5m0}sKqzD+KL{BrL$>C{L_^%^om{` zOiGP+IS%H_zePjPrJ;2-X1KS#oUUzAJkdF5=y4W>gEOjQm1niN2~T2Vz%Yb(W&xnt ziSvCodiwC@gDvm%+{RIy5UCq&01(c+Z3OYCpC@{1Jn7lV*P1yocF6`!b6%kLiwX5a zZ!o>ULq>S#fSGwo!(*|n98>wZm!7-(oJ;Rq<#C_X$y#Db$vU2a@kdQlasYXGqYDRK zmEWVn^09dRxM`hWApd)3w1V?sTl)Rv-T)s@wHthqb%4ETdWt!!#SFt7(=FlAF9g$n z@)5mM&p-BDeyZqlv{L*q$xPkg$TU+o9F~%*KZ)Sf^f0qQssx?(>RfRadrHCEUD_hI zW0_>Xvp0TZsW}&D`90NseWVFZN4Q4Pafgj z>TGgl(MUs6qyDQ`{SP#?+AoA=9HK5Xbcsg`mJP(*+j!`r zZk*j6`?!Kt6kX!{s&1HQc2Me5$B6%`Hb=IC&Eb6}snC@o)SL{Q;16BmM9xCHymEYB zzHZ!)4Sutvnmxe%>$4-t<05{}T2sy^A^)NJw+7_2!)dPVu*I2Gc{E+a6@ZYijd-Y#w{J+wOt>f?m97c8lw~JnchfW7z+T1Z< zd|{5 zsJQlknu-sEZ(oBqqW}e>k4x9>x&P(H`%eGCw@J8npDI;ZN4ptv3*QEhhpp*QQSGAf zZQO|PZK#MoZuk)p)Wiw_n>~=gQ8sD=@h^cv$It(f*fn8XZwaxf+U@=8VZB6d}1&lzKAX<3gnPfFgrQ?nZP9Y5&2bv4Q$o1^n_cf^ zr~QMqG2Bz&JS&vh@|9WEJ`qZ?(gG}p&Su<34LgV6>v8^(+fse$Lq--v{)*qsV1KP> z1@jM$)K!$wkK!(wnk<3Rvx(Wbri4Q?VIN*ve&x6P@j)B6^`}kh!Arf3z(?@;;FVW9 zQIo03rKxx6O-`+@=4(3j1^tbBPHX5fOeB-%JL5~Evx{kA^JL$Y1}TaH4fNSoFD}y4 z@_N;t%jlc6E{sF0Pj`o+^QB1{4C5<}_ui~6V4jME>icQpxNGbTaw zT&-|4vA(K~lQNwD>4rv-yt&~`r%GvWa6=wq>}OMlI(?CW?wgn}qraDvONS(U z54@uXe*SE1X#G?O0}f|Ax4aXuTYRZI9L_$VKET~Y{*T7M5Pyt2@`OpYSXiiWSJK}G z^`Q?viar7oOZ%Hsu}$&TGrXdA`NqM)iwz4`G%}) zYkp^Q{f%S2ok#=GK0dbd;jBbqVnbnb$%o}tpKB410Qcmy0|^|mi_iFj&&WDpWw{Jj zXVG)QND1eW#ndnXC8r`z*!j-nA&p6WG`VqDI)6F5uEYPwYqW`C=c|dQCsrbj{`}^H z6nDM`$k^F3Zqa6YXwvG}_}15th$XGV z4}(mrfRMo2hN`M_{M*6Fbu^g{ZQ!#uVkABt6o9FK6q;AH|d+t8p4M_>40t`hk zj(3^sc$rI2?FvK_v+9OA9%JXxrev!>;$^=$eK`t}x1uiK!m6A}u-v2H-l193Z;Vc> zPBDOc_~Hb+>hzsLWKt?DLC-SU+#S4}+Ae8Fsp1Q=Oy-W>`0jj;P1WxXnR+7IWYOE- zh(z!{e%Lfp4s;%G4H-ffz2pf--mOWp#T<$0ztCn3&pRT(Th6JQ6R9PN#nEBhzQzLW0kGFWr z?}-^+jyIYdL%t!6hg+c`F6m2Ul3ETV{RQz)JDhw1O`Qg97r>RHBRBg4@8XVy*#>d~^ePVLh&hTQV%uZlRHlISmh`6!{47y$|B>oL6lsl+ND%IG{_rjy z4-KLfw7amug?YJBH>VoBGApmhh?HWI4i#ML*h-zgvP<2&RFu?QU@l{>#xpWI$5TO_M4K|}byq}U(rv{b~qo^s=$QA|~ z=6q#535Dxg((GDlude>|NcNTYB@22?5+#2>h4}{smBTrA>5#KTXkAqvfU3o0ewL}c z@|$@J^mlmL^Np%m$zSF=EIs3E1-w^HGHxo8xG|z)@6A0ZE^^M(?Td|q44JTyFtb?j zAsDz-Z#ZJQxo^%YUnT14L+^`wqNQpGaBrDgM_}VjEjQ{}ZmjEN;<4X$@ywsErv4Jlq zG+6{bH6?hdYbU|qP&Ha99Pc2@uhc?Zrc(=dpdD4?lI`9wcb+%s0_X)wTR~wJLS*=| zu4pQ8@jg|4Q=Pg&WzU+tOP~W*S)vXl5T+?l>KN?T5cLNDIHchn7=ZASxhVSV?1m7> zPKD@CO)m*2Dg~~jY(>W9CcI>=(Yxp4o33xbZ_=bDGZu|ik{tRizznD62o~OP)UpU_ zuBfgxCj7PU`8hl9F1^G;vqqf(9y$4%|05pvj4zop9`*fLjr14hd#u9wS}7ExGe)OHT&s?P4x>MU{|wW1`L9_rCk z#aYAFVfSRV1j>iIt2*ZZFLy z;%m7isde=cZB0Wl5`nG8A3Y*B7nmbWZ^x3E>#~^pp$r}@&wVLl5UPVJC^MEim!YG) zhrR@i06qfuE6v(0+?WtMTMo1-RgxIG@z3^~aR)O9;Ag;0bdoO?1H=U^LU~=&jee2y z*=@p)!I*!R9 zn1H73{T7812lb?mCv%}C76xy>W`17K2oYD?<=)L0|Lg3-<#+JqreM#8F5{tAKdz{VzKod-f*jhhEAW^V~tU<0Z71Ti#|>4Z^^=5 zIo`1`aFZ{RSWw`dNxIh4H=J_cs=PyWUt=rC2T~4pz;-eBSUlT(&vnHow)J*R*Pq#% zZdP}CCsr^mIn9K&HzmK6%M`Hh40U<5G)pG;P~5j2VWV`OJJ`0LR@FZiUDp=3uVn|e zV4~%-jO79pIAOVQ3iag+=e_Z{_a_T`NNn+=TGOZjn0X0iyWCOx1+9DFf#J1STx z<4!@sCg~{uvslwz-VSj$ey(~MN0~4=G^y2~k+4(s=g->WE+zYkOFKf#={s9s5Z*oy zWUbNnsS^Le_15_)bzqLX;6XF`Yrh?*FYkkEl$pL8goIwbYYYvZV8VNDi^w2(ZK->e z%Jl`i4`=zWHhsPxE49oEXOjfcN=s$&KSb>n8^$%k8lk~SFXcwvfKQ%%s(cJcueR{Z zsVwf0he#rP*Sg3Jy$+7t8sg{bB)GTxa;-UmZPdrvoS+k$6w1=!ZAz51e|Mo3nI z{uO&>pf9h@$K>plT_=Kl>V5LYOAYr=8bUF#Y-hP&UMJz<4&~=GMe}J72niJ}7gfr8 zmM|;fSbTb;+g^_hXWi+5rC`~fFX;tZOECnmnZybF?B;rYG_T4mT1CTazQqfG;%e57 zq}45gkmgP+jh1c)EeiTRWLU8CTq_$pgR0dN9zgz{c}TX@TO_c=`pDX;EWY*br#vx2 zxve)@BbH_6^_`yEgGoQTik2Z~^QGPeglh--Axa(mD&vWWDL4|mGqF)_iswVh*2Zj+ z=m4P9mQF>}#r(0QTXmLnjdOP|K2crMsmLn!Sg_k=!mD^=Cf0MdQna6K(7$RhZfR!V zVXEP>Ih^mR`>bV+h{a$D9f~v-kk@WUCvE0huWFZty|SFQ4Q^|E7dP;BK@%MRqL>^e zxZy_q4f|LZ004!`%sJABT}^-xx#qMfk3pfk0&A_Yrbl)&Z8ORCBmAbR!pjv7=rAFL zhAl$~E+P&?_8_wddo<)Hdvux>p$zmVjq;dYj~|h;e~j#pli9Z~gr1p27O&|rIr-Wi zH9~LWpTow3Y}gRDJ0$~jBzdln9mV#Wn@nHU{Qd?0I(DgP=B#|CZvUdEdB zpvmy}rm42Lj*Do4IBKUB&kp}T_pchaBMh~VA`IIXAV2<`@wi&cr-#glW>yRri++iF z3|Ioze0DU_a|JQ^j3`1_lL(+5B8(wjZ(}!vi<(vP^EAGSsH3ePpi!-P8aaYpP6+W} z4YyBD42;9y<#QL?Fia`y@J*(b_qaPzOHMZ3y|v8x{hfhbrrJl`tosMMkr7gzCL1Qh znjuPw^{-qwZzp`uTWDomW9$*_(4tUSJN{C`7+d%7uWFP39;JyAkEvwBsaof6{nkMi z7Kp<2*(2LfU1{DIh$QDe7|0~75?;4xAz&UDyn-vLl|$c8IYwOE6JA#tBz^pGT0^#3 zwz51v>S6eb^nTJ${izgg+32KC&n1UA%Mh&S@PH9@$ft%NxqfCA&HOwbi_ZS5k7Saq zc|69obJl8wdfTz>!PenvP1=KRY<6ZqLQ|4$gBj1^g=#&;)Jx&l&K$43{3l`K8xm4J zC*hc^EmaB{Dr**R20ww9L4fVh-}^O%d^W7t0c=cD+1{@&FaIRXBIP=ZDCc@!ZZ}fD zcRxkj$0g;X)Fu?}xmJEmJMhXx3luw8Ku&1SLOkXFR%LRVpP!nXQCHxSyThRM*0G#i z;;pn$7wdBrwt0_vhzGSwtUQIKuQZnx)4EWP@mu6&K62-x38oYF99$OGF;{BRsEBsD z5o@XrH4gyY8aaRbF}%(Jd9TCA+0kRC6Y-f3fPZykKV!_FJ-?w}2U%W!j(g8CpB(1Ql9=j%pz9lhLI_^2*jZX<=ZrlrD-!==uS@Dv`dUcxB?W{5GB@;#O5riL<>#0El_w$JfaUVA(MM>@YVky z-(5;jJMWdS=({803TVjP6)1 z)^P>bLa=Zb$?SVfUoOt4lJNwzW@NlEB`rCfGu7mGYw)Ex*?b2x^De?rJI3C{YN_sv zHLk%I$(FLI!r~~cXAkfw(Vkm|JkpT2W?t+R?#K?skipUJ0ECQEklm$ zyTClsemq=(U(~0U4sWB9$UADP-FY`kOvOLx&k!kwVe&B*E;bf!aslTN@f6Lrl>FG@ z!VSv+X-)V_GJdm88p}dp=hSPd>^jnpHMuc!qxY#I9|5+MAR{Y_t)filP*ZIMwo2dt zOoK#N$oOjBLWjxGG4vF&mWm}~3mUh;Zf#(PIO0vVdQLRu1pZOfoRqKsHCL;HFRXe1 z+GPPq^GDHz>ze$Tlmnt^J+0&?hS2NlWt5U!B7JLfQaj7Sr4MUT*^RRl?+~#wE>=uj z)pI`ZmfP=y=0S{RWLR@#CMx}44-lgaZF!q)ZM_8nRJIFCb)@}8-=0?iDe0a-IWN-} z6QgIR6+G_BFbWC1H{^~?zE6d?5h`xrui<>8#LxTrKYqnzYZEnP`WCnUT2lY{x<9F! zsa)c*!=+wWEnlWljTm3xp*a#8b-5yq_+lHPmfwFU@L`>@RFDgRzN><&&{GM}nGgfa zDzEq(7ZR5?DR1>OWn$buqRhwgu0}7jn3?3YJ7-t1c8nghA^jDwLB-!JFGZ?rp1j+y zdwgy!^X1JEhpUtNwr<+eUD$b1EgxX)w?y0<7C9F8HGBjp4WrsdO)35iDiXxivMI|M zz%Z)hw=)-KmMd(&4oCX~uj?ewIX`-e+^|U*>+tZM;*rpaugj(uU4b z;*}N}W%-twd-OLU$ZDBOwGmRavy9v1{PZnST)D1uk_CYwcw}xcC{JIOE&?(XSSWRH zPxf3g`EW6EQk2kk-E_@WZ#Ro&O`848E;CU1&uR_DT5h!97uzmfuXLEdT7CJGZ))_@ zPqyi^aU)JKPq-DuwG8y&Wk8M%47RjD7+ZS2KmDsnIu5TJMFvf#Y7;DPq)W%-d`mTirmkt(TIM3SEvAyDsq%lXbYI zb%-)tM@sg^RoUb8$fwUH8Kag8JjW(x!pCq`h+A?~L==d{o(HZFY+Bc}=2tsS_p6&R z1I@{?#PM{+$KUfNc#Pi+eZ_PqGd$+Xw!5z}|BvQ0%wj~UtIc6v>*|m`d`09FE=m}A)E5_OeNVO~-c#f! zPUhkF2YX9@MM&=IDOS2R7GEXWOR+UhMb*=pZ(u(CmgOCsEX1g#dx;>yM6NHM zdANHdJD(>8qtfh>20dpv;SpkiC|@HQMGPHaSV(4bU{}T?syl`;SN(i6apAgccDVwx zIMKUKh8bR0$g3wwhf~ca=t*sw3|LYt{en2 zm*+a+cT16#&@wZh;!*~ErWDiq?(6+(IvF2t+2&H&bYOMt5fzM}-?gMs!QVW56i^Cb zq4qiBxLgwKUQPI`G0m8)>^8{k1jUvQvP|J+U)6UVa3L~_$wjForoFIxr>9o}%0S~& ztNzY|vebxd9uKe~!j#VEV|pG-v=%fG7ZM5Yq7~?byqjf)dbro4olWK>QEJN^0H|<2 z9s@s}%#jKS$3r9Y(VkQ(7X?;@WsB$JEmg7VhBeB8=YjFBopk1Zr?l%98ECIo{+K}w zg2UUaa|mL*0#g-+GkvZ7zWX`+M|6c>I^Y}DC+$;C;ycunRp~WTVI8y0kt@6aUc+jJ zPN3G*?~3ww8Te%NLjR^6gu29W zr8_YfuwhfPRdj-Ag)J7~9O{N9_d9*talLaK)-$cYoT}56nug0nw>iqa{WX&-`wr&c zoYgQU9*7%P%bK@UZ2?yvP-fdncT=9rsf?yJ@#MBScf-XZ6iE z`4+du7FdQeo=6y5Qr8?d@3-i1jaW(+Ey?@*_~ES)Owe9QK0aT*#3NA)pDewosNP^v z0nn5%eA#b#WzAZJqGeHF-FIEnj!!iZTqmfp6yvcU>XJbGjhael1CJTfF(aSbc$o{s3AvgeQmNEN!S-Duv`S$6r0F}X)f$M%LT~X#F z?E-2EX%<&U6}0M$&{8+2Htw56K<6;k{K7nc1g<&(#kTXQu)AAcdzEow6Nrdqk#ler z88O)D78Y8F#=#rrntBEd&vFKIw59xyhx`_@mPO&X-@x0SzKPw5(tc&;*xm>mKCEfq zRo2cjoj+Di$NkaMKDy%{&x(+XBI@x?AAgVu^;nvka~%trDrs{2mcHdY3j z#dZe7_4xDG7UnI$6B}S&yXVU06VGXxTpr`bdouuW-rl~nVb1_1xeYlorNH&408qSE za^8oR%NZXrGr&v`li=>L|Lda&O4?Dhwm&19HHBJfHm2iw6dyph&SSLag&ij0c63&h zR>UKpbaO7i{=oA3e4D+e>{Xju2TzC z+}>~K#wm#}%lz)r&2|+|(HW*R+Tl?*eUI+VP%k}rhw(31`P{p~-jy=zFTjN2r z`%xa3Yj^=%^g0I%xw**swDX8wPxtMD--|2V+$1^q`2lqN7Yh-qSN{;PzeKK8;Bx`0 z%Qbe`+{r4C%U*rnembZjafRIySlQuQu(&zmw(wP~LBdzkQ7Wo^zd{11hu>m;PLU=t zi<_C}7K%QZ=*u2+44f3b^Jd_aK4^B9!VAzsLhbhps`v7f_>Se0ks+lrW}^h^`fa&? z#IY#pWGz-*yIbW5eB=_}$-MQcno3Yz)p!l>)U4fjiXAYadcY6sCvmffwBjOQ-CrMS zN*bla#5Qk5`jhnIG=-=v&W}stWzRGqRcvYXb~vT8va;q^S5X(T-&j`Q7C}?K zu6LQ7+&vGwvLV+b;#l&BZ3a_5i!_cFM<{B0_7FMc@>U{rED&s2f=3_RI=;>~2i5c{ zuQg=)RMd5w#E?FJU0R!8@ked*sA-C+o&+CopAN6%8 z0*{o-)-`QSt;J=ai9O`C?x0scu9@0w`3}2?UHW*PB%aJqOPZKaJpGOg#VjLB=i=>z z-Lj4OLup!6;no!cWVk9H2zG}>>L20SXN2qrFw>8U#tb4rkTh`4{HzeB@+9unwD4E) zFo(rYyvZfQITBvGc(A{?%0&zN=lAakU1Pmc%U`3)@OY%=ur0Md4EWW!Q^M(D)xnE~ zXQ{y_(mx4A9L#g1C2J+63m&{Yaww?nM6UPz-X|NXBWE^+=LE^DPQe$*{xaYG3LaEc zD)0Pc8Eu4fOnp&t;vI!~Qcrttnxuy9vPY_a-$JnM9Q4iAh}s706aFo)yl zHSc%CKqJ6kfUmG;)wR+O-TcfShzi1E_eSvZYlz6JxO@ap*yEY^cmRpoo!OwhUOaL5 zAjS|od%H4EsIg&foiH6zNB_x%3mBX!9uD|f< zgDtM4N{KukPi;C)xlQam32}S+>B-+}pLfbsgzS2R2HFPI<~7{CZeN43_~T$aqvYi= z7(UH%Gs=Ciics zb40z|{l8TN@03$62AGVk9a$Oi0^G+QF+nwR4)!@gieh+x4{<#D4XAw(xB3=_oMtq|_e7BC!%pq@Kni706oa2Y&(lS1SL1)gzGHCrYI$*4M-n z@>y?5{lGrnioO1}H;Mi&rXY+Q5HG$u0HKX9rlXSuEsDL}*=sZ`2k}I*_P;e5FWjFI zuGx>ojrgaI{hY1vyHADZ)~~BJ?(V(R|#IbOO#%MKudOb z#{P)weu|8i^0fKCB^$R0*&~h~$cg)r$$7j+EJYtgv;8gnpnC*(SQJkZm0Gelu0uMJrwcW< z-Lp%zsVZlXtaMaJur#BkLg+t}=t^ zW~x+u_>>lxF%E9G{UvozWjF1j)>^|sTtbuIIEVj)-Yw7%wpZVj}}%l?_dLsyt# zIfp-rsfp;E7UfAN76#$4vD+gtG9gvH#u+G;s>+8w&{cljSWu{cw+pUD z#waJwbWvF-ZID9pLHYI-Hu~pMz54U&u*EWQ z$ry?v9XB$cudQZp`Gb4X#vQQ5<=du5c*<+bX3)5axp&IG)~{1MH$-yK8ftK#>b~#S zZ!xpIiV4qyz%S#>tB&=0oh;4ETzs2TSnFXq(_PQ#O@=BSeacf|+c9tMIGY}|W#uph zrkxw_e9Ci{)L%A7c_D_23Jvh3{v}O(wSyV#z2sm_c#WRByte0VL-)drlp3-pybif3 z><0RNdh9eFSP5{G`*KDFG*vbnlxP>S>AN}{05Zx_c&LuYAD8;so5By;z;@6ai>D5n zS2LEJt(u>%jrGy+GDOppl1e-kW7!Lom9m1oSyk4Sy3ZtVXM(7Qa{$w3@yA2&hJzK; z74u6MlWf7f5%lCWUh<_6wf~@)lUN#KBk3#GclD$%Co+fQEE|zY9(? zd|ABxg^_;L=&{531m{CkeXsN?DlAZfn~%3~`PddUOgx90bMs}WUWR!Z|2P3<{?TIM zn3}PnxJ{vh0lVq262MZ`#%LA*W{V+5Uie)E?2g`q==Zqu1W+ST!SepMOIL@ESJkbe zjaQH8u`^G-Q~GEzfd^0pJYZDe$=BxUJiND;dSO~WR9SoUzDLVo4gB#9khpzqrSTDm zsl>8z-dkY*L*!FTWSLCghZy|$oO6V>y{orl4HpaGFYY+Ey&-r6m7=Yz^W&5Dw3XJY zi@DT99Ocx3(t#A0;xP+O;DQwoXGxIv!`dd3<;jiYqC5M3Waud*1aIVM9kjE;MsV2h zbepLXp*$63`ZoBcyrPV3=Q8xW%-4 zP0VMjy|#N#(QDWcj)_2xu7SSo&3g2UEm(@@U8J5O+@?U-1l^wB`4mRfF2&vrYMxTf z(<u?J-~bC@Vt`Hm3OhI_X((fOPe&Vw?VnhxH}%5BZIOEHSGCS!(OSz zTYhm4Yp2O(p}v7`f2R|$8&(f11M~7pUr(MEioCk6wrzs-6qh-wx31ta&fm+M+NoUP>U0 zwrud4IvEFx)6d+WNXrhFx!8*Y3tV#jrA*jJB5faJ&A}IH1YB2wUwP zBCqr@K^kmO$Fr$tDn;K@U$m9GHD6u~_R=#TrPyq4q+$p~HX5|TQ&t9D?hI#=Z91hS zX-($%3{}+Xm#$#On|SA*K0?zs_bH_rRk_}kaF;)mOP;ht4(1AoJfsT399|oW$As-s zc(vX8T}|8@&+b#86E==QcKa7n_jHe7J3MctMW0UWBc~qL_zHBV0OcC$;`=uQGG{EY zoQt{*X6@s1jtg9eR{B!085;Oyj_ES$t9Z=pyXVhT)p^L>-&Tb|4#l z%;PAs-qrT_VRAmlhdA1DKD;}n=1bAx(l61GLzC%V`7(RA(ud->?gg6bf8c2MC-PW! z?gjB7jjolz;#OPLzkYRDsQ>N zqtzFUm!?hbQ=UxaMXlnQpO&%-wetQhfH8+d%lDNUjc3}%IZ*;k=-D^jB)R$G6uO## z=VBJT8s%huSzPO#IJgUCg%q|#LyvXooo*AQ7Cspfa%{J+^id0hFdX5vlW%T`rz`{3nK!as?$*V8kAHE`AMnu70HT|=TK<@ZpxMY<||Wee8kusxbkUucwkvWdvAVvq%J zvCSCLHq?H4b>0ICFHWc0tBHzSGsFvA>I3nP-fVruQz7h>%|8zT8S# zwztA__u9aYPa0cT1j7ipy^MOe40?&&;zh#GGUAhL&mE0#V}r@0@_Mc^ zv_G&sDqP*m@3s4FJf2pq)?bWR5JafttImbH1KwPttn%e?kv;OD8AWdKf5RlL_ugeQ zqG8<&{nF4i?}=P}ajgfIyr1Zycqs720WEz}7wV?oZVQ^m&z~BClfFIkiK=Z^)=a#* zkyE(dpG#X5G<{{)_tsO+1< z6EeMutz8wVLJiQQ7V~|>dWWVOqE|0q8rurzWXql%lq&k-k>oqy9XyTn_`TRQ4Am2k zMfc|LeJ?*wuO4VUsj^oBYK8|VnPZprd@ug0d}hzvg)8s=DgSc%p(}^h`<&5bwNFYG zmqkjAS$~$#+{0vjMtfYh&P)7z-1%42ov_<`a4NI%DuGp)v2l0i5nM(j%O4?VZ78(c z?wL9F6Cm22b2m79ayYhBz>lGSj4sY`&& zBLZ^!D#(P+p18(xcaFc7HX;7iScx2~3kmZ`CA|#htaO-2f26*_TTJ@iM!# zxG$ai>JXMnh8X5$|De-G+DJ74o;xQSjTA6eZp`|@lc4lP55n-}I`I{8)Vb!gVAPGJ zkYTEgxSTDwaWif|z z@4KIx&Rhvw{8L7jPw0-9*xym;b8}pj-sJS(+^eb6p5j{*HFY8F9{wEnbTsmKZMbG; zJ$hpk?Jfpz<-nyhM)Z&i!Rzy`2CM}@LD>4B4lgVtv~`$cAjv~4O)FDzH-ukR`cjqU zYHbmcx!P(q?CEJdE%d<-AjvBpzh9=PxD5Oie(e>vd0iCZna7t5>7uh>8jR3qWaxac znTbOiT91XdY=7&YAaAd^4xi6DS+QU&Rw{9Mkx8NwmHG#35v-j^OlLmF>pOp=RGvF$d0?nXh+b$Gos=AbCif`*6 zv5fS~%qu_axYiuYQvL^n4k8ZA(MZ5$R5o2>Ew^N1C>D0O-d=0hHmyt9sU#?DVGuXd zX?ZOBm5AzmP`gBr>0n!${)mMgKXT6|h%&f=Ud?xo1lwCK`BFDxXWhSIhZA4gn0m$9 zxcb()|AqnP440x*r`+ zN6Jnu9AA@YY8&w1MkXg{ieur8BgWU#M4UCXB1CEAiBcX-d^)g|RBX)3E7pXtZ5%O> z`%$y3xzL-f{Mos1yFd~Tl>?~qPf7LUwv%#(s+~(TS~q0k$b!fVwXoawnPlQ_gmCcG zA>2SN0hn(Dwn$NrU5EM`*RrGa7K4fbr+6G^kRP>=PIF^_wauj2 zS_SwqYp~}*W-}qW2gp%mw(8SQmCXjxhZe)QZ{gs?f1Q>uY~XJ_|Cv75-u$T*dm$$v3*feX4tz8g=RM{@ECi}aeNq4BiqMFc1oB6g?8+{O%3AHM9JR<;MBMWW=JOkU zNqRJn?%CcC-L=&*PYPsoEM#F2FL41}g3riKfYGIWbI^~+xBZ1p$SIe#BCOtJwavcJ z6bRgaIH^P1Uh*l`DerZ~tGqqRr23lAmItwKERWV>RZATTYzNmovYV@NMvRwr-(_@8 z3Ruu-*@_C94z#aNH7I_l;j@~%1F{!k%$wx$l!4N|Dt}w*&KwC86k*!^_YY8$Boy{H z4|WNJ;ZRi!71j@*A0YAUt{XwFYpbLLFRLF0D1L&6VgUqLxva8+rcvQwQcL5nl&ro~ zC*dOv<`TfcyE6k1)23vI(#OQzEeBGUpC%?(n@&>G^kggZ1A1p8FE#L9Onp|P!@kT) z3m2QLc3mmM<`p#5>Hkj)pDv*qZPt?CnnD0_g!!o6kIxGw00H%5Q_K?iv3$YsEa*pB z4wFPtt>ESm5)>5U-#RY$Z=3XEmoGUJ*TXYKplgzqEwk znw&Xsx0rLVp#z1iF3jd+7xQc$pg?7*{7s`1{lbg~ARrTYD~pqH1>NsgPB4Kl#dqR= zBP>pd>lu<}#6JXh^g6$RdZ6aL#o-Sh zuplUr{tYUUlk)*Nr9-rd@08idl&qv&7To`QTR_NTlT!f$)UL=E&<_HY6jzhjt`LO( zc`{?k0WpQg1M&(2`SaCDkSFBs@GgATyG@RPiS^20k?xC-PIf^%$13E1wfCKZ?&pab zjSjbSixwgzfXH!aFum$8d+COH(oMyO1pp4BHXFG9uPNAIpuV~O9mC;C@?!zl%K-Zp zdT8;5f;KM@LuJXxZ4MwCFn_y&z;AauU+I01*f&Jq2~eOO2k$E199biTuR~OyDFzu`FNy%p+^6zEs*71rF=| zn0_j{@a^j3Y_EWVg8bJkfF*XcUIFkQerQa`;+5hsBmU1UVnwD4r89~1LzLdD0<7H7 z@BTm`Cmi&@rh2?0RsNoZc6aGmN_ywgx%IzhvNPq1M4g%;Vx-)ie}~nS{^K;5{{jma z>Kpj)q9!u@Gk=x<_prCHgTrbLKyRA-9X|VG1Mm0h8&~U5<#nVrdgt(<#4=%C`s|*O zrVHN;2Z+#a_n-xOARtzu@!;!OQsJ5}0mueQL$}Q^P`g%tryB#LrrG3h1d&$IQcmN1 zv^_y*7oxQ4+U^dUVasd<_7)qMKD}%TcNQ-$su;N~f&Kq}_etn|*#&^4*`NpFFiidM zjCJG^O(q^qYZ)(^pH6n6J65krZs{q+KW67DOplw=pvZq@@mOnvDgO4(Z7mCBfiZME5BFIWp1 zD(gSo@Bb`>>;KjO1DM-Oc{Na54z>l>Khz$evw`C4{CizM%R;%+zZ7;eFt~Kwd-U>A z0)^FI|NY>lF5Scv7AT&62Ly71&ugp6B08#hRu7r{2FHnl1~%Q@5y=?D9L8_=0|;}B z#27Z&Ul}8A_%ZC7k`P8Gc*#QRNI_c=HWK*7zV z#$))nW%voW7zV_AH`McBXnWd(&|a>5LI#j$m{yh@cAzhCWnaR;hpC(a&wHc#@;E|y z)|cnii$Nbm~ck6|bBncE;%J8=J#{^7oy{@W4BtHtL$~v z2tc&q2Yna}eJ7VE!<28f%XQ7i()&cmIS2$~0}C6jhilm`{f4@kKH@;+~D!>)aw*@ z9zWdW4<$qq3$~x6+CE+vuXWs>TSjFUBpz2JVHbt zhaHjn->i1k*tWMh-8=I#QoMC_Jx)Xodi5;YZ!aV=Nx`vy5}v>!sHYyMRCBW^$ZNY0 z)Cjp2^v^o?c}y)lHKKy_dFgxap2m!hj%w2?grNc&L=BJOv)e>(F&X=ZX9@F?pC5}V z`Q+oQ;Rbt0bf=}pi^Xd4Qs1dNhNtv@^ED;Bs-Y%T|;$~ z+u?0!2PE^kJ`audawSZ|Vy{C8XcX4JM#=h!(CuHR`aD@>b7_3iKf0G0g&z!xc*fpi z(f;vvMT$5zWg7SCGSL@k6Jc?2alHZ#&nNb_Hitrw4c&&P-%00%G<~=xpI2vBMJ$Kk z_9;@A>9mXrpM}O12+*~Z7)5y01izy2mA6y8D{ox$m0|f*llLwgP}Lf~oR){KZAJ#| zvDcrAP2SNRfOD(;gJ0#Q3h8@M*BU0Xf-oobzPob5%g?CDuH0`^mg${vW4ZqRys9SlS*`Kr_ps z#9v3g+Em!Nz?~VYkiw{{kopj5^ZxKWpU&=j9!qk?n2S`AjhwhR^{iaQ8Kn1VIz6!9GN);LK0DH_3c71BYYnc{eUi=+kT4r@KlC z(g2Ixf^Gbj0Ox04NkhhG;|&68ha^0^9?`k*BxX%-iUezo?;oQ`1ig*sON#Uvvt&Iu zb<_@D40ncDlhb{}hu|Ll4B*rNTmk<;!_#2NnVaZe379nazUNDY-j$<>+OCsugIPC0 zMPq`%#{M3j;HRFP;bk`iBI6ue9GY`z*dZ2#DtG!7*f<6w)lIGEi;e-4bXpPl80}3Y z4`V)?RotkHtc;n2Ftl3jGAuB(6ZH{Ov@yWoAd!SD0z$(^w6e16F>g*2G2y-3D7y0M z5Lhj$o4I47ze6XDMw-zr_G;tJE+U0Ua+T6W)FZ9|nY&(*1GXFP)J6;&Ayj9`JD9Vb z=PIk@e)K$ajRF=p*+0KUB68X4MEi$paWo6j>u_?fzR{KEq+57*2%zh3j;BtJToAG| zzmdz+9XQ-0ZM2i{*(Qrvu^!Jeu z6t!coJzRiCf#IY`e%A|=4=C*%+M!-AeSO|r*w$pe=LfCUe4^T&_0ZFmHw2oGU*CIq z$~Im9UYERLy5V8@9yFvi9b8Wm5BJ-dRk%+3_pRM-ReyXSKT?Te`-=5&8kb62uEN67 z4~ts&0bm)KgXvK2!cLf})t?^18TV<6R@~F>G02PTUv!p9jo9$YJ|30Iz;<*q>ecZK z+=g6)wN~$!o8#h`Bav4i)AlE9Ft&eo$)?ZD&Bb(7ufrJjS_9(>>plBOm~JKz?) z!YT6vIVw;Vd=ED4xzEv}lLz%43hsNSx}oOu;vcqme~Ps#^|%TsE1Z^1nYQT5zW!UM{IaKH@^&qVN7sot8$pJw0{E# zdnht)_G=hZl(W*OmB;1exI$$TK3d|gfb)SSVz1+cmN8Z(DHqc;cDq)g7(w5f!zH|( zF?#6@Ow#M*>pD1;Uj1Hq8#LEone`BS@j4z>4CdAvv){3-#_hV*@@-e~7F<_vbAp{S zQL%JC*j0Ws?;t@(PnuuDDfb9xvY?C$|tfqFC%qYh-xC7mDSjfQaH9h z-Rm=xTWk4FQ&Ndz4th>a1{spRexe&4)LwNw2~x}OgI5Q9m%?>1hE6>n?#Xao$?y|u zg?3`{bHy{Wi{Z-DXlMkbGa6O*dgUt;1`;OnqYP~v_hxgMjZJNa^bor=vKq0Zs@Cc+f?f5_uaOJ#$|zps2=cJ@vV>kDomKG0i0D z;KHi;#)r05 zE3_=N=C%tDMzXi!9?6U(HU3(ckHLI~0FrB^6qvgag}lB+{`HpHS)AUhSbKlSH)_4dn97eX{fTMt8) z=Dz2?NGuBkboI1g(4OmN6AEa!w}0eKdCnVbtZvV20!lJU+QN}EVL-!gt}8{--Qk0W zCmXX}Sxy#$_&$hf$mtBY_8$q8?1 z*1B*FwE#Kwf;FF0F{u`i0o(AbT`rgc`pO`0`-$}^Ul#UV2^gTRI>Cig=iNqMY|HIKHNSp1E}RK*Y$5$3&Qe#`5azTF zMuZZME+eZR#WtHU5sKp+F7zjq3rkm@tI#7*>g#AZrDzD(NdOk(0cTImZ|u-2M+ri0 zUhPV&+wck@@Ns=7puLg_yMyX~t_m>#9Unl#%^sv`l-T+_8LB@<_?xyWPH{2OLYeVI zYnBtZh+L09Xkw7hcK+YAv_wo(*wfBGSPpq3+Mnuw&|J#d8S3HM3IcY|3{3=yxRr1e zfUa-bBO8la#*gAp4l$-?(|fW@%#zKXV(a4$ZKS{m@=^W?v!9Cy8Wt?~!8(@QR50Ds zbU}uOv;2S`9)Kaa%wf#@^9~iiSdMCoeTe@-FD+KsJ~msC7(Pr-BKzuk_4VIw#`9Rs z6>oi79A2Q70FJQwd~npT)``t!-8|WG-;54~2gmSebe#n#)L0SL+Pb8a*JU5h7cO9E z^Z7gF1Im1wfN^5Oc||S~aDnDUr>R({eVvI)GXgk+)oTKJlysL3iUEzQ-t@_3wXdl^ zID2Uf`6WdR4e%>1_+Nhj$EtqmdS48RK6b%6Ua{-5J_oD47D-#t`DhsDB+fI$p3+|8 zLEodrZZn}aV$s9&y&bQ#84^Hi>vFP+=MvC9cF4i^Wr4VE9qD?+bcSBJgim^AYI7H6 zYj>>g&<$z0A4oFD%VdSDZztF8T)w4vMOM%|;gYFKee*NkTS=+hUG{zJcEYY#?3CH4 zYNbRuy9{%PnD|rR1QBkMjld2yD1a=$sz(PZ-bWL~Nz>aMV?%e7K9Z*stuYKVv|o_cl_1UhHDY zoP$?21LtTW;U=0R%Y6NpyCEiKEtRGk9#pY*L^>D}mTHo%{jNB>MRn3aNvh3ngM>X+ zk=pHnE0vX}$lC!i(#aFU8WHbwbI`RO7gJ!OMmkW~w8A zXw_IwDg?|O=M)H*h}{|k(DRp`zzekA+0oC zj2q9IrE31p9mVN8!(E2ncXdGC;q*n*}LBJAjuGq8_nDu8o63hZs@=tIsnMuk1d z*l;f{WM~E@kmKXcM*JzuW@+HVs<7=Y+fybAkcf83nbW|v{6Qei#STL4ib*FK3)zbSxnFiV^$+xt|$An&iG{t^QA|4nKc`T(pPeJIy6vlX5m!QlE8}YQ@xZ`rP z%JpTNbOk4>on}DNI<}5&6*UiCURmkel(eWQ^cMXj3!O0uuHm2KT?c*mP0|%w@oN$N zi+F2ZA?tI4abRy8;r|MF8kC3?~4cl5p9Ms2w4+G>=${rz=%KdSVVy(kX%f>*A5 zvklCmpg=c_4)q|HA#~gbT6LH73RU2Q#Oci^WIQFKQ`wopKl&cQDUns6Us{pYwt5l? z2L^qmDvIeMQ@fV5EPtf3tnb$ozeEAciIx&1)snU}3C`dd7Oh5#p@WGjA~XDe^BtAI z>0-Rzxy|rHQ56iWsY6tY(bmnXN(_wjk4Y5Pui|Z|jVh1&QB`h3ygZ}VgXzG+6)@50 zA->Vw!So6*KJG^>k&gInGy6uloH>gE*9nuzlT+51c-?Yk1#agwhUu!aZqzZz8*h6F ziEGEC1u3b}giA(yuf#@ldf^~Tv7}z+sI{jQKQ0EsPFMTeLFh+Ji_6Kr^8zasJ&bhy z%1iFR*%wAGqk_*Ji#m9da97eT1pUSkdNUMUsMZZBy$^JSJH+zRUKn^=s0ph)ISl0B6hTGHw*5GGbG;6M;jH3qcts zR+g4d7<$Ls%{f7fk1$0nM>fIS*`76$pF%D~d zYn(DT339&%!i~K8YA~7*F(HzCw|K7vy2v$UcSY}y8-!E=?cEOWEd0M3h98NnQ6h{$ z29ykoP>G&oi3CON^H%{knAXyTq|C4Q7*p0|O{NqQgQEEOo~xyLlRiv$7ZH;6&K&b& zWacynxp){aQ4eghC6GNxZM2r$00Jh;6u?W)Z$RV6Vz6YE1ytH0Opm=$7dhD6Vo@Bi z8}zBkMW)qqV}&=Zw=Fm0O)!HR^G&(f=@rFt%vjlD z%Sv+HXHY)Zc+J0l2{sGni;Md-+t4M*uYpUmLYO4!KV5;val9@K$6rs z#%>9bKB1BH$oA4Wn|*Y2)$v0{>Ichha@TnwwcyM)G492v^HQ zzJJ7Bu-)Tl$r7`$w5iUts6fT#Ml^a_Q-ulh!c4fh*d9JH>vODZgHI7?)}I78#f7>7yf&+`;uz`7ugEQ&HCHoWp)e z@P6%UEf{!t?$35F=+K8Oq+ImsXXaGG{Iqk0n( zO<%h@J>-+TWIW;I<#9A(0H1APAs+pS@|+ctfVr79BI&BQQ0;h=-6cESVbaPYM!C#e z_OX8POk!e{XejBpan9k#rxL6Ece}6z!OKlB^@@$D$_HRcU%PpvYMaU?$N*c6tXQ~u zGPd5R5T}R-^V+~R{!AZnU8VbWrv1ADnIs;Uc1SsaXIr-`s{J2Kl)|C&+0eMc$S2QX z)Z~%p&~2=%esTjaoC7sTO@8wQNNRmaoq!>CZxHW@u~?;gwAI08U~}=}kQt(t{AQX? zB8L6KwixUTwda+n*$j?e@OLpe3!abYGQQNqQrM~o1TU^HLXYco2CY*wd_=fF(If`( zFyYw%$R=)}xxX|ZxhkI-4}Y0KB0=@X{7=8}aXbDlL8fTlrr8uvFRG4sk5!ldNVK4ZD)Tj(9 zAM|mT$mqiyXZJJ;*PH1aJue^zM9M#d>8*_|x@b{XtH2@n}{6L|nVWP+eQ? zCF?h0)H@sBoLB_k~#Tv4H<;G<*+Z87GeB4b!e{5AV6u9R5gE#e0hI;5W*9DHsAvgg8fLK*A7_ zO7B90rum-wCgK-<_d;^09aW3nnHtvAG5_}R-9=7HF4)u{>O96;q@BR^^fY^-P;!1i zw%IL>;E>m(WT&xzF*t#@Jc=-=N?iO^*TZf|;H{K0Jj(l)IVQEg04LTpbSU?unR|$c z;#Gz3G3Z2^PC121*{z3Nud%r=Fh)TF|Er4lJIpWw)4Y=nMKywND$;V+`0T zU$fiEb0t>{wWzxZ48GaSoB_Cl&GPM*HS~K5z022~bN*6F1J>~B(zo#!%>jT}+XM2k z0>*=o5~ZA>YE?41Kekg7r%BBfa~~7&W|YJfnm9hS!K`|%RT0d^wpS+; zix|~y;RE?a_ebee5RmXa#RBxb)9JWj^%2L!RyZwJY5K)ZoO@~bhk^o4kL@yc4P=Q5 zIQe~15)4LQxxUXrT2EVeg}VMEuDBVl7cRcLQb-}-@s$KZrLT{<&9dh=t8QD{`pHGk z+{FN2(l?ef9XSz+fI+Oj*3n1wa|27EM9D5tPd7R{c>L*>!TCDu^boa;WldnxJn>k) z8x@lkb_gL?hwsU9L62pl(DX2$M!9c<1+Kl5bgJ*(^WezklyRo^)AXlIv;O(OL}GWA zg=KFxyv+w+(@t%|9XBdiNs24M4<$0a+cU*>QFuk*m$1e)f5XR-0l++c_8nV0m3 zV(AatW_uNjsKRm9qaBHZTg+OQXxhX$_JoB59~*qMj_1E0dxU)Ks`oEjbRj5#zGB|- z2El-Sy^OVLR>)=t4w0MEDQ1ZdU$G?O;e1F|>vk1=7ncAeOY-`oqW%UWNR95M2H$t1 zy4)dnQ`YU~k;=z=45gWL>!463_-gI`sGZdI@Q4#GUnVFv6KRE0iuYCSi zH+uXk_gKQ~Pa6Jy{M_ocN){AglZj zEcpwX=8X(>-j>1}*m3LZ%6f|QFr!eIgkKzD49)wqAmQH{^l?ttQUM6{!}V{e!7*H@ z5LGDwAcuHl-l3GnT@M@pNjr9Id{FgCNROFfVEe%XIe`ADpJM>La4~&rLFRUQI+1(y zT`-Uy9ss}mk11Oa_j~<=F$man$$5Pvh7n=OHHwHd678yu&+M~a;yW-*K4`hs>EP$D zu3aVI*qU!L_+1MnCAq9q-*sU2U@fAcIsWtm>2!|YK(ZB=Gw$RbA0B`fx}&iKkZlnt z;3;+`yu8pFKg&2gLL&59ZptdOQF$H!*a1K{v7dVlYJ>#ImjN(Pg7nM5J%!iCLRMCk zZ>!^1XZqL5%0VT<13>V9N;g!fc#-kAaufVJbUeI;?E#OV=cx@WCObPleLUYpLQ6El zCN)rRHFx0-1C_E2q_J}TxXPXargc0{`Qksa0{(Q*FYs70wJ<`57V>|zh3c$|+`#mP zcN?((^BFSirma(3)<)(5YC4UP|5_>ce|&|2iE%rfqtni1tJtKUCmO{6u^c)B{EcL%X*1kSMaZlYf}>2_rrGzPZ`Zr} z+2j07WXH=*_rQklv`+^eSTC3gr;gTdw4Ts{?F4(AL4UuS&GyH@Q7o#vz=(20z`z%y z!~+K=dNmqp0LdGFQBuexbLBIHPepjG^ag;iX=EO~CSpy|6Dxnv*8@4wO2n&90AV;! zJfe^AHT9oyxo;7DI9tzEV-6#VQ=BbkD+F*=iJg$>htA$v|G&@*pjvk_+U{S#XG=!D zAOQ0&uh70fRW&zBDdTd>0b;Gkme+Ak@S~d6@6@uQ>Ct!_$bqf_`R&m(;TynOQ0Iq6 zGXgw-YZD#wb17YqmM3Y;v9vc|+czX)VFHB=tE#VFNJB)v4yWVm29IOU1^LQ;RU7vx z3WH@_cey~`YQN?2$~L3<`L}pB7ajqCb^by2|JQvBB;Wx0XVVD^w)Tg`HH5aC?aXJ$ zCD&MBkB(k7FN^(b@&gcn8L{udJE>hwO-Sr8$h>9*_2VFr=wbkB5FjQ2xjSN>zUZjn zOnwh#DP_6?v*8SLutAe?B8S&(a_c$JQBEDi{LUsE@&DagXWw%`84(zGE4_=@@wjRS z8Q3;l56~EqvX08lX7EGoZJC?&Ji(s)pL{uBT#*UuGJx!tNbUu)tZUHY(;@B{Oja ziqCet>}(h>qz%)>VZ1dhv9_jG->S5_`o_u%Z9&W7A3(0P&?VEG0{!0l*PH^tJegmx zI3@Mpd9+`93Jgztb#MK;X5wa~F**~#=QAsctJlaZ)!ObONcgVe&m#Pg22Aw+;85)V zku54Khrm{8xiF5@JOKK+GPFj(qY}K}yh1Sg+eOuz9lF&_)->+wjzbL;4Pz;SYmNvZ zDnWgH3c!)wA57@<-UXq)kPL9IfE@IC%kP8VBvhrhVv(tJXJo(@m*{aA6vYzIGHx;{ zw~@9TT)U7f62s8n`PntY_m)&P!k<17twL>VuJz9V2Xs-0dRq@bt?EYm?&HuOHAzpP zP}A&O_XF7+xR^KP&?v~>kA2;2r$?vRXRZB0A{3SeoI2(mfxz(rrDspF&pR3qU&cK) zMc53Qt=(4Omp(X(Y5TT<3`C8uh2C4T#W5JW!+t0Fu?ax7(E~NVC@8mH09zrE<3O}s ze{LGXd?ZtZN|*@3Fj{^1u&GNON5DF5oHHH5NWbA-Vs;S_*?Yl{WauUvqBICV+Z$ZEB^xI~n=#*nO-y5gDVt~!D!2Am3=$B;h(aAV$CagP5)F(OgNkpga zj7WXTvslD7J}DJ+SnNb_pf5hg2XX5Y0%&-Wz&C2<2l+)utHL233WG&PA;xl3)S%OK zDa#xx6j%YpHk84Et&&cOzcC^Frk1Kc2AN2#xB!q7$u5A0j_?b~nj-?29HFNX$bca& z_U%-rvG9cLgg6uMD2v@q2CQ}i-v|yJ+1-`5NAH<9M;;Mhxg{F+@5UnbGj6UJQ-f?) zn*;T`pOD7kR^c#+<-u=hD2RmX!r#OP=Fx$G%wkrjp8k&~It{5ghx6$@_Ue38+)I?> zW6YS_@10#?vY9eheq6) z1_@7-dd1rItpPdxmjhpR!tVYG6~k3apH!A&3g^leBc)N+TST9?(`Us_)v5S$N?G&4 zh18w^Ri`R`reX__(>IS{m<$@CWJ+^n)W`WMo4|gJ`NTUve~m}lZR8bG|0?D(3zS~1 zb;%@FpKf1V?foozcMu1NO}oK5X~>s+VZ#=ubbJJ#4tkmfT)&Dgc?z zpW$=`HOP=4QK4RbvzGyUn@*pe*r~L;peCGY0d6}0w~<*wzcaE9B+HK_e$kog1tZztOAS z5gW~B2*eMv-O~;AK}A?65iK|d4U6Gc0N~JI1CZXkigL{kc{D#_4Hc)Lypy=B1NA; zQ6<3^>4r+jL}Yr4wB?U0dmVCtwk=~-Ybl}&Bpill8_>H1T6Wn>88lO4zyuqiqq?Pn zM06^fdXX`V89NJGosXJ{(Hq5@CXErUZxcNCTU@)rAO0{!X|JjBSf#xr)b)L-iGMwm zvk?E~{&C94h`vFH>5W`qKxL1t-jOPz;KsxczXVK6k76$%Tao-&``xe+iT3PuW=8_X z2mxPGs^!9>#Kb+mSaVDW??!ljxq!qoubJ%B&a zeY7apyoLVEtWO#6At~rvFLs#upWSMA?p(2jS{DOtx~ZI|d<|~+%4@GK?M30Q!iIIm zQwkH|Y#nAF2jCH3k>kfiAh5>3z`w~L{?$H616MH=3sQWsUO@lxM=u)P6bTp%hiJ)V zOT4kqoHhyDw(1L!EA(6FSVO;JQx1Q{JYN0o$mOo=e7r!C@SAU~95?1@lwYd0D5?Qa zuhA+)8Sq})Yv*C38G>wTk;8U`K+icWW$~z%BjRv!tbC8rQsa=s8cX#3d@Yjr0};8y zs`Bx|zXO07$cG2OeQoteE@Znzrf5!mZ*+YjqoxUFBw1zT@W2x+qI8ccYEx1XaEOtO0~X)Pbr&0J z_^C_VE9F#vStZb0Yg`;&L#`}VB-Gi6)J7?N>OV0eE6gabFU5NorWi>cd{b`y0>f$8 z#FMcMK4v!J8y~1@tQ#z-tfRT>3x`hx4nVKkb~#!LYc~% zGYZ%#Qnvv>i1%qBhmq0nG2wz@NrpKMEhhS4^(zhXm^k)EX7XLU}X?FaoBM8 zO9(@BvYR6Nl+kfqNelN1_k0yt!65=CU z|5Zn~{iJ`XWa;-9xh;Wdl>1%f2!UlH<8oMtjkb#bj5rO%BHrY6DTS*|A&|D@ z!Rj0F2iObsHzPS>#Enm;GAn`ZiN_yhSujykGv42*{i%sZhR>xd@+g+$Z)ql=5!-yx zO;`%v9RDfvD)G?Ek_?WV(I3_qIPIg<@bY=V-qm)|348g3J!Z)`8d1#jux^? zXRL}i_ap|mHHyJN)t)CPM%Kt0M0deF@$lLPLWW} z1piW}^Z_~kjjj(!y+N=}!=lASr45wnF4u^g4`k^9cW*lA-B_yj8d17>Y6khYzb?t( z3ph`G3$Vo9{-2u8JRHil4dWU6$UYg~u`63M)(_R25Q8X&CZ#MHM3M|+jVU#wF=NR# zIYNznvL@c9vSgo1wz8E}$eu0Pm%e*?d;fg?dhX+RuKT+0>paij@w{-5<)JR##Ia!F zl&f;M)eOjiCnl6QQ@k$rhp)dsR=bxyI~|I`DWulu3ofBBK4_l4!x?^Sb@*la*M*th zWIx|ryKl_D7nktOKW=B$Jo8+0qg56njQ18Ge)1-73&KX`KX{XmnATz7XMiN3adkyZ zn7mn{li$(ijx+nyon#Tx*-_s}hXi1iceCiCA(iQtZ#_>lELQn-#nq+Y3lQBev7Hwz ztXWsYwLX4g{iN}VM+Z|6^=l3;<=@XNbzY;9`|k<8+&%EmFQlu-(K1!smz?GGCC{`Y z9xY;0AgroY=!hg144d0SVIC&cgpII7IFz>jWoINw`M3u!^&A@p_VH$7yZq~CoB_EO z;tj&f4s&Fa0vB=q7-c&%8kSG9>FUkJM$;IH4+v!7RJ*p+@vc7qPnHoTRuNX0v{y zAKbKy5CjFa4M}z7=Hvn+QzUa*Bm{a8!#Io;4WH3W3X~bIFW$$YiU2z`H8iOgTuUL? z4fOJug$?fGk4c$vW6gcG!M04CjGl6GfwX#~ob#RaAP0i32RDFhNuN+i%_~9;o!4F4 zL9%pxGLoG|x$4q3_sP#_Os?mapETW=VCy$wJaA)%>rA3?!JdQ$(7f64nG6M$K5wpN z8k%k&Fro}=i5^V--1o5H4t z-%+M4R`QH+AZyh89W=r5>6Uq$=q-D^APnxqhr8|pf@q|t*VkOMVC+Xz*A$1#y_?%^ zJQ5Vy2!A%FLP<{pQtZm184>ukoY2OmRywO}vwCG)yM&ej?#7{SGn484P9rLcJ;OWk zgF_ObRLl#oLV;k1s7UsT2p&s57U3Kiqg;=E{3fq_VQMxL%?0!<2O-!98JS#v+d4jh zWF9aq7X*$r-zNHfJ9fADG9_RjD5pMC zFs;YACh-{i@+Wo)o$Fe2qTf<+MJ)454agM_gYaM-dXTI0vXY&qQ&Ns8P3fov`b9#R z(hUf@^xlE2;hnhDmxQopkXcYp{jAvqMA<&rC!Z}G@9(9h0i{Iu0PRR#`8lgwpKYsi zfOd_}0=N6htR%GrS^L?A$p*6zXIotu3==VhSG&EZI#8p@wkH@2guveqwM`G945)E? zUr2ephD8vt#anM^1t3EB&!Zk}l4>KLXZi#^;0^?OXVY?=M?84i(OgWqFFo*!vSGMT zk*Zc(JK%0G=gqpKp92v;zgp%Wpch!U<v;;Q&gu07ZTDh(BOLC(Jj3_O`Aypl< z(6r7@JNz-M^0q|)VFQO(6Av=P$nw~{A&3#gIFP`1&1$sYNrG~+n&ZlTAbf&QiQ~xJ zp;%{i@(i^BRFH$$WapL*tIwX4InZjw08K_{8B*b=WNS8eJnR|pfey9l_jt=+DUDfz zt6yN9rTUo3_m{fD-=-QF8akbkGtlg`(14~{+|w|UKP+}fm9o^;08GUZh_iRiVH>0;iNe1I?C(t{()D*?2AJde_?_?E# zRaGWjQb!(_X}***rj6-toGq&zPqyvj^KNL+8(S0Jnp)qQ8w!ip@KMFcn-JseS{>t> zj_13ZRYnX7t$NP=6tP^}UwA7yPWM{f)=$LtNH?hZuR)%q$DYl7ReRCh578a@Jxs$d zpiG{yf;G_LU8u54`iWpu4*P!)qN|`e8i3F`bSRg zM!G9-?ei&`(3q58d!3tkde z-%N4$BDM&6$3@8-Wpe$i4(y$zL3*P(w0uVzsg56E2w({Z# z&K@don{VvtJqORWNBEzpG|f@P{VUzBD!Em7ybSK!kMsZ{^Og)5z(|zz$BuQE+xuUk z1dGiq&$Cr|7!_z@(*$xU%sRDNmPFweA^&oc8-5Ml9SzC@r;o2wdvZ*;lDfWD)iHSx zOK(403PtFAHH%G>zN)Z4{~hwKGvq@hpyLAU{df;2F?lN4wbO>QV632z8lBy6Rp9&p8$xRdtE4sTePWFL|%p}{18F)0y7Ly zz{tK+Bj!B}5+gV8N|b=QD~~S*TW;-x!hZ#LCYYW-*LowM@9u{np6)!ZE1qF`mn6&8 zM%4Zq>r;CgFtodHw(_$6ZZ_V}nES62*aCn(Igl3l77ev}2PUU2s%mfjP;EnbO!R54 zdx6tSyeZxvD)8G!;=s@Lkf!g*8)`#q(%`HA=AEf<56mbo0S4^&f1zoZ#+Cl^(|xZw zH(iNyiv@oGYeyu&+MdI!hFzwl=fX5;29neC!W93FQ5th$7Z+QVzrEAHv=dN{5j;! j*|Vi6%QckLuEOiJI^q4`XW0PH-*W0Kfk6&{2~8XR@0!7&y7-s%+$O>((Qdo6kQgI?Q{wZsj_t$jiL; z&D?5T^`ITk+6^o_8NIq=aynbX(*U>M^s{f(^FE&%tKe$nyqHSnaK~nUdiN_i4a}oT)|_##e6HIOZ3jie_nQzLm5O-{rtu4N6fD%&H^@dpA!uSu6Szt zK!G1Aob*l{fvEApDPdYA;j>RzcTn=J@~(}g`=I? zy0DBcgm zo=1j7Dd7!8WzExz%?MP(*ykKFS;jO;mt40J1`*$PMH~;s2_uL?LUQmVCJib#5F;ax zYg&qx0ACT8DIPp1GEz1Doqaedf(}msUq(jjHa&)n-{Ro)?)n2t*)K^7kts$>9~Ypp z*qz|L2QNuKD0= z+@!}U6^{w8&1C_64)+Ifeei2~m-K5ek5Svf}xe*y_e6}kr6Hf$f+`BWIW zySsxm%yo?G14h%)iqX-Wy?{`U2fu2zr3>ly-1G59nk<}Xp-xO8HfWO?;DdoG?HS28r><@fU1|z&m7Z;!cMnNyFTW z(3wmexhcQpM-~4d$^Kd&ueC-qM8Y zm@~_x%=5@fv*zp{@1xm8wYo}ueeoI&_6Wa1&0jbSNSt#mQTgFqc6&5iz^B>iQ^qLS z=VhhK!{7LA)$j|Rz$~KZ#m!EJwUm@B=eZj*16dqq$6=LL3{+MM-HMFTmba%agG^0ZkH5XL z>t{i{?sKuTx1VbRr(A}>l$3}r|K!Im2m9=LxQKZ2$}T;y-T6^Bz@uh1Aoy=u_Gx-G6R&p`3S zBbI%c4D9*1p5I~Tvi{^NbT>)ToCG9|?MBZ=VCQ2c+up4ojGcI!tL?yzS z1)3?zJP;f-fVB<`Oqnp~_@tkK7YI+Ns9h!gg5 zb_p$cq-E?=wZL5&es)B@N%f|Xz|Zl3;SB;qrpniA%~%51NX}jYEq>7eY#V z^o_m3$;tiG{#>LrB$*<+(s}jnbVYV&CyNdR#Lj1j(!?YD-a4X6={zY(@6O%($Cbrh zdDCrg<@FoN8ATr)nAwYFPDNAPSX)$LioxXxsV2lFAFH8bs+$ntG|UWwg((j&dKg)a zb2WLOF4Ssjs{Cssk#3@CDSry|wS*jJwu&>g!EI4~x4BmR}L&(9gqjPPO7|X5mQ?dv?_i^8p#;%E z)iE@RlFKDK`tG+w7>hwt9PG3rz7vpd?O9p8rYPCu#b2Fj*sU8)$z!{Sn7?Z{zlfW$ z*w;wMQ!t)sNLEO0t?fHL>1g{Hw#db`P$|t$nJ9WlbtSCuHes|T zMJ1C1QjT3GDCb4GSsbafHMe=4Vtse&b8xjc?ymGa{rDVP|94ap7dTNt`pZe!H+=lhCqqlrbnD;t zMr+et5Yj)SKG2F$G!wHr6<>xth@$X3k1mg8TszI!oy6-rD?~Wb_!UO$9FQXqz+nu!Fy9=#P%&7&E8{W~Es7&cH&^)s*<%|h9u0bOlp)Z3d z;g-w)qO0qN1T?b6n1-}}RNrqiwKB75j!P)heBtxc63$aMZ&vLLn&9_1kqGQbO*x$z z8l!igsj@at?teKpw^ogzJ9V&QN8onEAs^O7v>MJ6F7aSq{ptLgL8>Td`-C{4AkQ=M z`W7dA)$YZdYDsKy!D7|$@!;yewjch4MT_?67Q7U_QkJCKW$b=LJ#IWS9=b)zm&TpK zi(i;qJ!qCl``UV%j5sd8@Jm+Ub-dn!i2oR=^ySk-F=q|;CPwRsB82eUuV9?Vp|c{9 zj1~~DI8M|~VUO2Y=~aS)KHx4D(_5|VGe~3xeR_jPE3>8ye)$AnH@jguCeVjgeE9+v z=MkgG@R%>ZzVIm0B&hIHpwnHI<2CmUx8`clzOk_lUvLp!@yi@^kx7_D-a^_MXZb1) z_Unm#m#Fp<+OGJ=&w8}AmN=Xzf{J=44YsyFluM#4JK3Yzmm{Mb?hgI0`g{1#qzgl2YFt!J$3pxYgE%V_m1ZU9RU%l z$Uw-em7=gV0Gm^CQZYEuXKZY|wYskqo3(eojiXNu8x&Oc_M6<_*45Y7pNy=VH_noD zu}~6mG(S{47EhIM3CevQtso8DG-M zF!G*IT%7;*pZz&ebe7^O@IY12A2TXW2&`G9!z$NX9qXR5F*I>IGd z$+^OM%lVCWG2{GO}_8i;r-dD+eD7vr`_^blRa_3jOYh>wj+l;bG{3==E=ke6O7wbz^mG&apzSr zwug+t$f7l0w_nu@r@oFobFWG1g=VDE2D^PaRl`;!;*mwH;ASscx*ukmMok9?L~u){ zaCT5Ay`Se?{4i;tDz7m(OOOcYTqC7ErT_;Gq9t)}9$ag${d?}vuwYGX??NkO5(uIeW@ zfof;6Qk!_(WvSyb!ef^Q^D9Z6qf86MS`dftoe)m5ss z8eeJuC;Tcz5#`gVv#+Du9w-(+>fS>uty64Cy1uZfirJ!rhk^`Z3UtSw+Te)3tbYmQ-rU_QB5~=_GJ< zHQ#Kg!4No8?fhFPIErf$U56Lw|E+P~4k$gYSNbE|mjB$intMk(Bqw~wHabND^_mwk z`SU&r!580hC6Phfy2G8aOD!|9h@+&oJD&%SrXaOzM%C`oQq`#KMhGdEaFas3@)c&< zI_0!RD>9|x_bS$L#8BF!QNeh*WcHFHY<8xk813d}Nfhxcz@_!#16n3G>Qi&mk^3jW zMx^W`bqdWa(dktme^5h2iTr> z9JF!iY;kZ2mBfs1jVa>dMOa=IuwywDg)K0@MS*-=deqs@Zg4gJV-B=&i+3zoW??(E zhWeB;N)x_2Q5f75gt3tx{dx!vX#d*0h~Hyk$3MEv)jE0TzrN}M_!bonc7xtBq=**fbi7g1w%o2AFQ#CTJS>;M>Pl0}5wrY=QB#il19^wCE zyY-)sH?^I@O_CC5iE*N&=S9Y+OL=Hm)~Sc(T<*1#`orag<@JS}Cszi$2d-Vy=z)mH zQ!VIGrDXQ#UF~FO&KeynIqL#}^|stBDznqq|JT__W$eAZ^EEfc>RF%N4T?1!!Lc*% z(3JxAMO}SeBUg$PwP|1PJ$l|slQY0F8? zx4#oY+|l)_bPpnpTO1YK+`^}+=p;Plp>x+s(wJPeZ!bdfLULvzgW7V;CFEou%e`g^ zpb}DN>JBh$$xUAdnRk0&^QqUK)RYO`*%gPL=qxJ(LY0 z5_8&kWb3(HJAu8JyF*Zc2o_kP*xdD3qQKdIk`jsL3~F)cQ~h<})k7TRmqQqn(=m!c z`L>~hE<~geKcqgzGx!^7G2cXFzw(6bnf+qnQ1G6I$7F<5z5u~Z2z5sb9mHRgP#`;l zMb(tc>+J%K-UiCGZ}|~lMul;We&>O-fIo>JC=*AZe%D^zi^(|)PWeg@6JjYbXfL^4 z=p#HxkY?CqIock$%D|N6>7~@F_1ozdjkL;D(u(4c&&`z1c17LL-hQJE)|nYT{0qYw z*;I)TQZeanp)DM}RxHW24MSkSasphHcTAl2knO@UNDD2rd5&IncQl22#qok14*)%ORFg z&%}LxF5*2ZZfNMZYE^Vu?6Ob%Sk=}#IxfBCTOu9fJbRnpd5P^L6KE6 z@_!TW@Z!LK4@C#5()bvG#G1O{^v6#( z3RcMcA0gxgFA3!hreP$9@h+yd_^ZdT0KE|~WZHWNaKp$r%i)!Z@KJp#O6k-0(Mkk? zL=o8lCgepUB-(}@So>-F?|9adNf@~@62G%SO4>_tk?DZtYa(&P{?}6IF-98uxv6Wo z6n=0x`y`1W=uF1Noe@2@ai{;>Ro)Fb<6)OfiPxwv$~jxc5Ic8zNwy+mUAF? z^VW(pasG;syyC(_>-nE@F}n+Vikt$p4ri{wEvAAFx%arz{OOs zg%L0PvR455GxTiTp$O&sgXbTa>KiwKnT~GVsnG@>YetWUoWA7a1}QQ?;6D8Y>_8rd z2`7*BxnLn{C2+QSZ;Tt|I`L2q^iAq|kH9i119q=m#OFVs5;Qn#Nv?(^q5MEj2~A0l z`e!%VZ9ZBdrg7Q~5l8V#rdr3$%p!A8a%~^?oxsOSJ^JcfKnglnJbE>gO(QMzrnHG4 zg7JZC4*6d+1{%^|5Ee**-zXpX`p7Y3nbK8=dL*YC&X8tsw_YLK@P0xO^P%az4zk45 zhsupPT~jY&{&cro^2`;y(Td1ddVC^1l?q*p)?fNShf8%vpjp)GxXFAFIQ=SSs+2g%X_M?(!O-~D))TvPR-}1;SF8Amy^UqgY0zf>**{07M6|o=*8+`_PPN>R^bdYYHpCb?7A4>RyYEGo)sS5XdBV_!v zIY&T1NdNU~#XWxaP`3vPyL^l_Gz8Ytv$vPDxGS_jS()YgqFF&$@Y5KT#+;cN$#e$> z{0@4%iE_cGXSBLQ*@?tGk{M(dqTpogW+cX&>PN#?RqQ)U$+zXjl9b0fFTpK`pUmG{ zb*}BLt$sE_)^sSEcL~seZnJ=J0WE)q8_Ge{RpUZUsHRrU!HE6unz8pH96yZZ>O={x zKqR6#dXQZM$Vda}vo%cWXbou%v-pD>k0)>W?=&YQ1%5!|QnJ2!VB-yaEVM}63cBc> zIGQkLu?KYGsPn?@4yLEz?VB_Kyx<=>xpOJpof=W?x-ISF8@y)9G>1PDNo1-KS}#@} z2?}8^`4+_4*U1v`BU3ggzGVCRaFNsTVpSnC>%+T)Yoz4Ovt)zYZ=g~O)jr2xUUnR} zPo8exWMd(~VnSF!Look;rI{EW^JG;cPA0#;-Bp4e$GKpS%X6ruHgIdvw5L%VAmn5U zZhLYL1s&k_Ku0*9{4FlRX|1D#^n~Qy>`~#nGwed%c4_cJmVE-fsNM|ROjy;xK%2dE z`UHNbZ}X?EJZE_HQj|qs!#!jNQULj46-VFlVQibW32-|iLeWnStpm!K#Ey^eBjFH4 zE*D)0<7zO{O+IK}Ev1G|EkUoz>RdH{z#X|B9CutGr)6*N5WUeo)Y!_TUQe4Jma|rc zx>t>pgg=z2A^=yvWu)dCj{JZu_S z-ke+`gBHQB78VP8Pl0-sFyi6G#2&1AAYI&C+O_vtpYO;E*B!B75F(gpSnB5h~w31J|VxF^6P!zwaF{mo2 zC8P6wf|pQxni zZX#C6YpaTZY`E6cRM&F#PP32oecOtQDXGNIr_J8PL@wgah0el_1rL2Pr7Q~h7e&av zX9}kzrI=9Nc5v8S=`*)7*Fj+v#Yiasp-c=S#jtdZ#I3zWjau>ZWD$bcUx@=R(hOz_-U+q$F=`Bhf(#` zTbWwLw-;?0h2B3MzT5R*!S7zn7qca$(p~osHB9JG3}bk3bnO+8QH^nx(@D&vB*7i= zi)x6*1Xc%`FBYb!kNG2cnSxWM8-Ksai{bUHMBNH?=An9p`oJg>!pPC#JTZ6C z0A?r}sB}gdEycp#yJzxB35su=yk#?rQ$zW;T~3+A8Q?c=lQ>IyMP0lO^+$Q!f=` zHdgadkNN#$E#9@L8>dQmDfYSSL!0M=9FCl#y1!~F`pzI#&;PDx-g#GU0-C{v78|`mrB7RT2A#<$3&@veL49}9Pa!gAPdBuqeeZh zH?W!2gq$BYaQ7aVN-R6Fr{|wr&{T#;W-bOR-pjuE}eHvf$6*C2p z7?$+&6v_azAaH&%{f{EXmQcQ6IL&8F@pPE8#dJwB5K;ohA^V-FO04vH&zsh4bkr_l zsZC&R4l}&i?pCPJEj>L0bh`LFnAqsM?>7cs7$c)YIoWRUt8J#w%!d;xO6lq800Xm7 zZLXOrQCG_u!>#*GOlz$i7XGH|7;?5?s?JpFc_=I;${fj*I(DHCQU5YUSI zEaV0GW(WH)tw9`R8wA~^jQ-as0jpHA(x>LmWi;qwt$L3hK$rAlp#q(5aTQG43rtEl zXkyQPz+wG1eW|ebWkJZV>66C~D3j)%gV@8j^4zzQ?*{F*-2s4bN`TrhGu5RyOuJ%c z692KCCk{Zcvu)t<4n2h)A`8 zY;lCs>JC620dCbw6BaKuJT<1Tt%8&K1@`#z1448Je7?l2*rQT{@N5O4PD?Vj1*j$a3|lo^@Hgi z@P}T!&ti6@`WYsJ^46`F2TwuITVHB@ zr?_=%O%OPaZORF_zT*vC+e(0^u&hl*iOm8obGZ$0+XeWaZM|>Z%1QYEkmF4uvg0>{ z0&fOY=Dc`9bL-ZT6d7fb(rc}TfSd8;!1xCbDgB|q_$r`R=cZEtLqi6fiM`Z%O|Kxc za76-)(Z3m^t<`|I1B?NHlx1y7%Ix`nZoQua7_9>kkt$n0>h`UdJbo074=pxk;0ODUs3MxDNbVx1Nds zvnB-kpnnx_##7>aW`%R8MzzVYCs*&*_M*nwy8iA_K~jvXfs&H#rznHFKjP9nsEnOf6CZo< znL*7|N@^}nHF1;1=v5xmZr`Na#5X(t68o;V7lEz1wusxu-kM-9D}foS=QdUHV)QmD zwxm5rjM3<}JyoUg*(d|{)c>9lJ60x4MoB0lrxajOm(IV^hxQHh=e1-G_CN7+aB`V% z<-zC?k*s>dHG=CnZ~ylcWiu`Nlq&8Zm$#pgcf$DFD7L`YPSbTZxPgs<>`Je5x7pGcr6KNvOAS3578zj}rto>1!Ic|O+qg6G;BS@tOh zPk@jJa7Fl>IvgN6Qy&G@avS%nMeE1rKib-Bcv{Fy#2pA#Uw+Ja`Bg^i2V-4`JMTq$ z5WV^9H|hKMTjJ$raGBi<+KsIN$3U{g_jhI1Rb;*H--nRC=6R#Pzg)0*diRmUb%el= z!gi^ubl&3gk;$0)uZM30y|}1&l-}{gLRRnTjT_g%=ep`NpUzJQEWX~37)?qjPWtzW z@tNiIbXA)kpRZx(S=EzMI>OP5?cuq>$=T%K>HFUl`^XFY_4SyRE-zD(bGlUdrS#r3 z%?u;JXFL6oxst>*2C?Nvdlt(xr_Y6g^nBbs)P5#uIh2j()|VFO+C*u2GM%^<`nZeH zYd{?|%nc5|*G`X*7F&O|_%;#oVASJEzxtpiz1<<**>q>GGKq=hoQ`jIs#-9TKu<;6 zL1~`n`RXf(oTP(z~yE`+1^VH&5uK3k3!0cEo+Hwmx<5l3p6B zCh!5MR-4yqHjUqTc;n~3lIexf%oaQ%mDHX^4RD5$Q0>%Ir4`0=x;8xn%;RXPw|cC* z9i}hb&CPT%kRB&UU?6BJ{71b+J8q_{g{>lGG?Tg{O2L*6RD??XIyx+v}0%Cze8WzC5OV(x+L{^qm?#F$Omya=8sb`Y1j*4&>hSg8m-L)ujd$1i2aHobjHAPx>WgqT!lBK)gs zD=IEF!|P(|o?NdEWqJgehzypC?!Io|%5k-KPTYQfk^bH1oeNU9d0+(_Ar$k z+u@BQVAKh+KgLe)n%rOd5i1?;5Pk%avwWDxj#WMvV<365vHfUSuP!pVGEslt%^Szp z4%2f8lu9f!hrUQ{cD3_xXuhHvq>Rl=H=a7O@ZSCsoutJ6sP6bV`{k5!bLI?8%FL-s<4xZ{pBrJp(TvMBnc5%Z1i-hhn)!V~_lfQj+q~ z89G8%VG?wbL40$q>q+C+Hc5L|N^;~A3>2(%Av&gGqN^@u#tarn2WxA1 z78iw#8)3-MorZf6LD#aY-q-N;4SR_BmKZ2vG6XdZGP7wEjN-yzBT zs}+;tzf83mI)|6@xUTD^T$IIeL->WBYkNps*p$0VxRt!reL(?1dXa!bxaN9|0or73 z-;`0VLfGBwiT7578dFEBtaXwYHn6&M@-Rx1saCq7CfixV$%!|HZzwb6(In}T^&cGX5|J6fH zD}LUBvd&Weczd(-S~bvVJSl%0*;+9y-r|XJ<+Rw%Ltbua$;&Sx@C{^P1!HluWR$t} zFg5-1&Sg0#e><#_7;K9-SY3|TKES6o;tvRP_=pRL=3Muk6uh>90ycUqP;YE}PB2c< ze=%`AEGFx#EMt`}iI;bZo<5Ipy{gnuBvw(p|2HrF{6p3N!EJjDGe|>OWwjCBuYLC~ z89Dzx5u0gv1#{bsS=6Z>YIdT_;!AlZ2h&*eC-#h&mf`EFv~c7Xw-Gj+9t|Pom=&t< zP6@QV*eX%ugQ@hkJzgi)GnizVuCUnHM2TmvKl{&XJxIJPz8d=Omx1aewX;61Z#nxY zgDv)%yZV7Gan{qPHLqZoT`)I`z#=0yGNIQnn=8xnu<5NuMZT0Ex6TT-DyCK&=E))? zH|}4RDLWK*Wg$KtHm+;VL4NSKq85k!r)+Pdz0RMFr#zTBtT#qu)p!!$dO-Z4jvl7D zzCTLLzscycTiDtReVEC;==-KR45mI?`VSVS!b@lbU`Tl-+G59&-3ZVAa6dHYv*v#J zn`Sc?I?VRE9aUn5_$l@97u&U%g9(i07tlmxyf+p|sr+VEV ze7x_CQ#77iu*rO@@C?##@gRbA#1qF=I0iN3|3?io`K~k<%lehDDlIMEO zEBjcnaau5g5JDv>k8CrXE#?42YMN8FbNPei2&!43tu7=95`o*eZt*$9ONd zlNxv&9J$oSPQk=DHoCLGY?{pvk2BbyCyx>-iU&4;J`&%Ke9QalT-Xk(nyT|RdB4O9 zu~8SKbn4xT@zmXf5kd31Ei?Jxu|9Z@q|jTu&S!+Gu50g{#xpzu`^Yg&xT04joEvjm z7u%N79-Rt5533|`)Th^lxa62%gNkMLOEN#I&Z( zeWNe_XM4c{;(WT;GY@b1Krc|Sl}C9v4l_2t3Ev)BEj5Av#Fi`%S#R$8X-ceXm(^cR zADg>Wrb|{ic60%W>auoOdZF8l%?=%);&o|Q_sMeM9;QCuioTe+iQdOa1`DS1+}9oF zjm!89`g6NMn?}5HS}nWrYgynWt&-r^=F`>z&2{f%df&~c!((3-lA`!lT^@eJKGOk# zpg~eo_D_PZN1&K2Q>w&g(4ZaO<)&zpBb$lli3D3Tk4ci?LYdueuz-B+x&Z?BG3NCC zgS1ZUQNNG$(OL3GB~4%A2)FT-ShR%gv(26useRjg)x36M zyZ7~b&P)^SGHs`Ds|LCJ@^N?-Z8YrhU)3i4J@_a>G^(5qrvlDh|E;~5pC>%8#}d}O z)t>BmiHUdYMnR2(%jaRUX1u2U0gJeTY8e1@ImTR)2(Bpi5kLDp{z|$*y1XB#s{e(`>`5)9*Rh#y)F5n2n8>7i#(&&}a9YwNlGd|$jNhtsHf3apy1 zyAje=79F{oCFWAib;5#gq)_YtB^mU z1T%Qj`0&a?KpR27_p9=`tr%|t*pRxCtxtDO?n$y);!PG&%FbVEgRa>lOVsjmO8hLb zhMad_En z=!<8cLtu8BB&}YK_HNs4m@nKQphnb6#P-51= ze1Ks27?u?MgzDU9$%sCXw*F%GXZVhQEyJ;1U;DQDCx5m{DG-PTQ96vzQulc}Oeyk@ zko}h!x04koSz-bST13ZM6T~g}HCslii(q7U(6IeQ`G~|X3XN|$)TE^y5NsuJ&t+$? zM3KNxcHT)m zgM=>p@?tCj-KC@+7HdR7MM~{RIr<$8zcRf)-AYftgE7#GvUReYgN1`}wceXdiEGL< zE}jQ)Fk!^$5teE@lg4YqChGT}eV80{4ct~i9rvdO>q ztdqKuK-9&<LhFDI*-e-agn_<^ivHh&E#_OFY>N31{71T%NYJN>sYqy$2J%H{ofFyY$s6%v& z|BT9L?ozp1%8m5zuB?<$h_efIFHcEqF9;Stu1aDtN>?DlXQ7=f8@aAwd*CUv--gUV z7*0qrW=f5e`=B0R25Fje)|i^R^86^RXXaqUJq6$RD>NynnUQQ)DYJ2rljHJk|4Pvc z2z)T$2umQN#QYN^s_*;C@ko(}>&yT6731}_tud2#xc#@{di*QC#HuDTamRL7y1~`l zX@*rI-2R8Ao7jk}MM+G!b)af)&mr%p6^dd$cF^v>%3Ec+%6{4-BA`j7MPH+Qg5r9m z_0IaVQgp|wz-8du; zi%$-mBkUZSAYs&WUYeO=G%ojGO-JOMBy{3&F9VO_AS-QDsE_+}rrNWrEiUhN%;}FM za`v4G{<>3R-nqkZ&$HC&#N2eTeBCdmTE9$vj+?p0?jZGhog@t$B}B{3UX|pUr|mtt zjo7S|x>7|GtDdLcBjtJ0D8ZiPJSCpz9|$0GeUKbIY3h%Ne*b)l0}|;A@r1+Kuu)+= z>lKq_XWgB2hGj{XbDK1Or9VqmhA2bmGN{tx7qn4V{NS`4LB=_N-`5x#JDlKO>wPbY`z%~ z;v7tc-rgh;8lIY}Fn^kM7}I_)&y6FnojFXneX=naHM`83c%|{vZHEUG{?wSYm%PR! zc0b_jX>%fYXlANXI=$I$TI2fgNyws5C@w;9^Qb!}2uvbf73(ha6DRSLTC`tV>yxdy zuL2@#=?p7b9i1A&*hRiROi9_6X$QK!lc zPRYm%h7(Q!287ZLqESH83P%N|HTbuuKBXiwh`R3KeiRd;(`=K)JBbti*TEpw<9hQt zU6t4NLu+Pe|MZca{dhTCQx{>~+^R3h35sq$RSdkC!Kd3v86H-GpiJf1Pk3AsrNuPf zn2xb4LBCLlXngnfd_A53#9OARBqnVXjOAk)E%bLaaYVp3H!t}u`QRYi%wb#>A$G4S zb>4BhNEYO#c_Qf9 z+M20rlBX#7fYkL;tQER0k}^&_jnCq~Zde-U!Ts8mh53m^3v}xW$DZOqpT~qv%23vZ zPzjpN%(JQ)7?ZVmx8S;_SiuubzZ@!46zc~{gg01AJ$=;^%DWGyEzYZGN?KHUMlnGA(qC zG^eIMVA^BW>ilsoLAWUA%d^LK(5H~SqFf+fz5qxj8sT; zBfU$}=6Gie{6xBrT>7^;3d0IoHjmoZP0zxrPq&O`gOvMZ{a1Vv+apW~TX&~G9{5n8X`*tgW71`exQc9gWzO{R~PQgDBC zwT_5xakZyBhW6ofginFU z`M6q?$IgY4=?Fg+ozpYia91iQ#?#ZAD)4at) zl#Xxb*!S(xM+&OJT`gbQUB<+Rijz^T{1MzB9hlo-=S!Q@ulFL;BQyg0CR2T-bB715 zr3Nt{_O(9JG_K0c#gM1uYz_H4s1ENOI+{@>+SRPA2O4cfQHzd&DPgdljnt;Yi{dLs ziIYWE&;o&^hdJ=ZD80ANYvamm7PJtu_T+HZ6k%6(I<)Q^iy)V2(|{JX^cc9Xi32Ur z-<>*`u0zP%f)xi_-C*M+)E50+PvG0Mf5A$A&srbD{I)5U9QTb^4{gERfEasZ!t=@UFYiO65Y>`?qZ*hBxh?e~_F`O>2#XM|6^&)1s(=N;uywY~B zI!#aKiFdaxdPDV~lau5gphm)nKEIH@4 z3AQ>QY*+P4g;)br?qx z^S>>w&Mo_+x^`4QMpr{4FSWGhv29gyW+U=huHPRh9&))nPkRQv?F!)NQfXNI`q*+z z>MntsoCeYE(&uFpt99=ICy^^JkCV8Q>2YymWAf+UH-km z%X;tQD!dTbZ3cetp&m*z+o(;<;d05&;}Pdvtyh?DJd_}S z^GHM5QbGsbcXec!SKYR`()oM;em{(q-ef)#BDFL&KXd;t2ktBHMoFps-bb3&N-)#J zdn-n?H9tqi|8xNB=WMMoXu&V%q13<`FMXp2ScH zqmHFs(ApOs|1AiB-5btd+(1Mgg{A0_o6hE2u^>WKqDjbJ7eE|7h%&%V-Yd@$sHdvQ_Qpm96$7T4SVi2IvCMTF%!Wl%$qv8*_j~X@oDA%A#m4;I zWI(yBzP2%e-jZ#!MCQofbU57H=qV`h5nkuNA-BaIdgW4#+ox=EVXgK@rGwmbs9y*} zrjRDhXuZ0yUF;O9X=jepY0|C8{Q#T6qbYGZ&VwdN54PUlnVx@BR%S;8K|@&KS4+oY zGOF(!H)k%@&%-=m!NQjrJmX9B&BhLTFIh8S3&|tuJW>Z3wcxG%`Qs@O$)X!nleLrD5!`&d5rIP zdd^C^^7(WeRp_LBFt42Uf6-j2;uN97@NLwbfGbfoCJVl~)_8PPq?4vx@hLPpCUqEI z>tj4jEO_4uTl_%gyo!qL=Dh?W`4amzUkH|^)rjwG{0dU++@fc;>a~`lSs1qmvg*zj zcRp0*w^3&`W4yBHyO@M+@=eY-eMZh-uM#(NuLw^O&$zbqrLSB=R&xaQ@>FB$;>lyo zzU`Yy6Y%J%KzKNQg4aLUDni@zmpET}7wC^gS%MF!z9BWGB^K+i3>Y{ylqx!;T5Wyg zfPY$n-zTQC$lGlAI*zn6Jm=b5?vZH{f>OU$QhcS>WWjQRzOXK=8H>9JroLRCT!q+} zAKnm?hfEkG;wxKA40 zY=^YVJPB2GO;QYP`pB2UrXfGa*&CjLM171cogL{5k1b^;y^hl1nueFyIy*`IM0id| zKtypxK8FpR*%R2(_4zVe3`2NB!8!^%++0)Paz1Oq57yvGsx1oMs-l=>mAh%;B3s>? zXKlO}k=b*>vsBDLUp@cgfC)tZC59RwU{n*9FHCv%8os?3sE!Ga^;1ED4toU~dCOMK zwH8Z9!yWEb`U%UduVGJwq1k)p1IaFy7n$6k-TtOa7nhw_qjDv?Rz0nysF~`|SHVzP zJC~OKhrPG#sv}yug$WV}?h-6OaEIXT8Z>xt_u%gC!5ud4?jBr&y9Rd)?zbW5j`O_# z;Jsr!{fWWeyL(BkTC3)qRozxb93T=fMMClNH>{KQY2S}g&Gf!%){3vhOWr20JMVkg zVjru=hFS?y%h!-sXX24|!G;^reeZ?9TfZs2{d+RudbGv&QE0SM`{6)vOHoW9`n|-? zB;4KhPyHY|mANkgLYhu?p{ZEMR7~xLD-#MiufvRD2InLNb@W_o;uMX(G`*UqjUMv- zdJfo@i*}=rQkoMgu_t>U`+P3S_cDhhT%-z@Y(&(k&RW*jN_kb?$r{=>2%TC|7Mo>u zr^9&mL-h~#?(=jf51?x)3D&fcc2w} z5f@X=(Ko#YSWI@dljXH+g9LpQ^NdHDgXeS_kr4tJE37e3-m|?#HJ8*5%0!2{8^J4J z&C9}bJ&VIh%DecjUb?0kFWp)6v9^71*Y61pnx^`6S>43cAvYPXjjI2gr0pl*kBY4sts8?e zof{+qL8=Wt>RioNhwp67tbP**uHW9kI0{6Yn&ogEgsOW~5lec{;?dcBcQ=K}i)dBY zn@K&bF`T=~#&hf2V~3TTMG1lpwv=9r<8$AyBPdj2Zz^y;s;6oO0)DEs5*emXK=8~k3ASbKrH{UEW+sLKbaNisDrYeWY%ua^e48(eK; zthkMjy9Dj79n*vzP83_VG?5be8U?G2dP9NOvdBQfj~DdPf`V%h#&zl3Xuo4UYu5In7No7rpJY zR;_|WJF-Jp zyrevC`25Ihb==)vP9GU%v$BL7*8|D8T?b1}PH}IyGZuNy`E`WabfUv>POW0b3PzHt zL;m*J8*K%UWihI@x+|s3dPQY9X+K?=PlfXkD~fQ$RD#5NFQeL5Jv#6Ao*2!qqTYvV z7<<@n9sRy6yY|uMeat3&xnb`&eLlsVDl-L~YfsCYLVmvnU8(L+>`^Fj6Srls>&!c! zb?&^UtKJ;rUaT1;Zl3G)#eJWJZEXB>6nnD_2V+X?raEU0l96r_`x~o$DbIJNyTepq z+FXn-O^bd-t#KJ-X-`DUwyC_%-Xrn#Cs-cEk{`Cds8=Qm2^UZJa8DYoD!}5Vw5E51 zzE`YxdHfTS8gLjz?&w4k@UI_Vfr<94CX`(Sd3~|}E81^GO}RUNr*U8Xz0H&t(EF@{ zWGj#5!l@PB?8lhHk)?R9c*(hxaY76~K5*P^BVu9_6h&JCP>VG}r~S>$_tR<}Qik^4 zERem&(XlYD9Wo7KMI!s6!cOd0WaZQp5IcSC7n97u@DouSuNMy6hB@^&mX}xf&~sO; z@B8YVT0^2lrnMEZ`QIxAv5A%@HC*o%9W4=gHS{T{#*qLV=DB#}D%4giW$INFZ*ud( zy~&(>jh_o_^c6_^$GvT*mA>zSIig19^DzB;wuXLeMch}Z@jcD(&Dy!wb~i-Z_Rm;{ z(~t-;hs%E1%cyW&{wwkbr_7Q`zRBJ$4Y^k8e7?wsMHQ?gv|#MJL~j^IfUiq6=JQZ- z%U53i44KD%U!B%(SqF1}(f352dDE0k>QYza#O%I^DONR(Af$e>C|bdbOi4c^Qw%9W z_*I6g)a#4h{cHvby6#r#@jVN{W>&$At4+++zPHVxUcy_RsRyPh3Yxh;{3T)%;KomQ zsu+)k&P$LgunUyrp6gz;PDA(o@|Tu6n49Dg>J9ay*$_OM$efnd1GF+5AxU@a>K!M) zmHa^Uwau$k%VgZ~5GpEWsk-s&YzE!#?=B@Sw7%FHIHMvN_zrXIs0y3X4uKLs4CO5s zu3wF6y4?n=og7m=k?^&>9vxR?xa30W4CYVLZg`4w2>!ZO1&sNwJS7NfSmcb zOP!A0?G{Bg6-}qFvR5-a2aluC+PKWV`&KXXn-ssAAgZr|Jl4#LbB?96HiQQ@K~YD4 z+_lrWH?-}0uLIk4W9Is&;umzJs$iE2qB@cNOC|vn&&RdR(u&v_!zYi)n6`VCg6q-w zm4eq$1ZdC~!BwD=gidSQn44aA=%rQJK4#p3Xhn3&Yb$35DF zDMsAo%uu`KZAJXFK&H*=wczRg70O}9_t-;5Qr{Sc$!0IRsz1X}EyqIBZsGMAC)uu< zAZcwIek_y{zsPSuO8hRf$MM&H2G1zt*hcvGQs;2cwhiyJy^y&9T~+ZTkJ%J0aIk94 z*qF$)W7KMDJu~I4gm3lb0S%ZcG6$=|Sw4UQjWS+2`h3Sjl@c(zM~Q`>&}&Huj*|7Tn>A zO~&maTzQhcqZ{`!8#5@<>4hhNZN?7&h}ExQO`v(XJ{&IQ=vMvOi1ocHb7N?*^YO$EB!N<}Z z<=8v=4(zm|Y+$3HS+ZBqCMwI4d5Rac@*7!vMG@lKN6z5)U5Cef15WBi8*VNEGCB&%=0Wg$ z$n#@l+he7r@bz=-*wPxf9V?)qK3#zDjcHe3I3x>jj_NkofjQh$;t+*iU5dAKRf+fd zd(O|~iyF&?aT7%Cp?j9?v9{I7Lq$xu|26-)QX(bPHb(_E^lVN5?rOXGR_TTE8SZU- zcq5R&lcJ*xZVTnbKe=P-F5Stz)6`#H1)oAQC|V z2}UwkPmyhv(=~6$UgoK~O{C8)$?JX{GLR?;jgt?jOXUKyus=kBm~pHA|p8gQI9Kil?Qxj(x8 z;J>_NX6)q;X?XEG9O+x_EOZpX-32(xH%tN#*OqM=a{J)esBO56EfU|>O;oPchAWDV z6Wk4iE8E;JAfZ@BJl$ab@uRMoL9eCb<_yPbFil@6k}oRS3e}^|mt9jTNoS|<#%K z)%Lq@x&C6}avwFg@DlT~XHQuM_}aMMc8gDspoiiK-Pwo_Cly{TiYow;xie5&1{JBF zpQj`z40ZW#`$$~hovxx&A7dX$-0jQ!{Sq=@hNi{w&tcA7p+O*+;hU8ZHMYAu_f>8| zIX%=wrB*(pvi?+iU=usL9+>eElk1WZygu^2do~u6BX}?114E1u=7<5zcW2n<7(w&* zV;%x?x7(9MbK4cVyvNIK5`x_!UMLYld_KG=(13Qr39W5uYVAS;wLV{=En~+-BdNi8 zPvs_g#oXYRlDc&Xy<%^zyB{XL!N&$`^OmnI~ zBiw#>7V$V+{Q-Z&ef;|BWlS$v@u;7esI8Y5mzAKyqO^)A9rrE&h}tMUJj2b{deR?< z)p=@DG59Wb>sG#;c14H2@HsH7J;1;o|G{ei zzk?omegSYYnM^XT>A?t)$wQ}JZ+1A!qhhsHAJh^?4CIB}G65N1lCPL- z9KoO4LFT9DCq8%rx5tnfHC**Y!H{?kuXCo7!cv4(=Js$fu%B-!(|OQ8(Boi8`enpR zN2)`qN`3-`YK;NWB968+7??Q0>jdq;qx;@|-*24pFk}7liA>ii?y#u=mD&-2)s=}9 zjYdXq^?(Pz=ET8BA}Pvia=9$hXv+14XQrvj0}_5$4kyVWrgnSaiJxERI0hxdt*C<( z6QBk8ySAlMIH#zL$H&)kwCR8c0_ks10)sGswz1B-b}Xk+S{u$DPg;ft7Fm+PRZbfyWjm1Oo;A=a=dNP4-vKT8njQsUOg@o$BjMOw4}Y zHiLuh0iZ1~IHC-Yc|gsFPb5P$^%1O9JN1=ClWp@Z>&&uY~! zE+^zrDc=3}Q%cc@)WVWq00bYiA5v{Gquy2vnbH25`S+hYhaNQd^%HmrHu&^=J+%NN zF0b2jHX!dr4GoogO01W_l~pb*vJP+H{cn5WCnqu0Yb|B~N@3CJeOI9k+7njY%^wtx z)h;fZfv^7Y=5tZ0?7g1``@?`S)Oy`HNNZ|?S`Z0%beB&g_~q(NtqIYGO3awsO#s^f zd+UY(A|CHGkUiCT{x^idY6Ja6qzn*VXEv1OFmFmh12xv)7HRUvV;*&2G(bZFNd&_m z>ob2`Sh2U7c=;Y zxNZn;H<`z9(m36SHV zH}8L%g$^s})_7PD#bbq0XV81lGxR@Z(H{^Y7f8xQdZHXD+Xko`FafZCP0fG^ zQY57i;(c1C5_?Yn!SUaj46ihL>WhPs)VC|q8U>OGKVY%wkGcbn_!E#~^lG=*fW({h z9gx>|=Rd60pc|$%dYXA}$E4(XT;_D_tDr0&f9?PUHeT@7=M3^)WgohCfC+I{*{Tbz z17<}`n$yN27G;5}+odwa)Lsq-cK`8px+fvx;*`pP`VkY8@j%&-mnAm{c%*^FN{%YM zt6_4WDzg<63%AEfwx%3au{m@5!GC{;jf8RdxHDLwA+!!T^o1^SYk5RAKahEs?h6&G z!_IPe1sgD8FmH!OFe_i;KpTMN(la!&oVSQ)wmrK2Y<{~e^>h{b1k7qlk@d)}D+Jgc zz;wKNu%C7%pZ+rO-JNcBJg)%MJ)LBg+e=nYU#^Pb>WFqbC$*rGr3cdtGXWH;(U^AJ zdHj2@akf_vfX)!;iWX*OVm9bp2DFsywTIw`Y?e<`HDem3Y8^l>CEyBbUZ3*6NwohD zCK^X&gX~wI=>9+1*!$0zfS2`N1ro@Ctm~nb)8~wH1Q@{I;RDkQ{J{O?2?=M-8%V64 z1OmC&ukhRtcD<~vCiAMv=bArea%i6}TQ@tc?t}7w)GyxH|?I zDrJF!tV(pUM(uZG5D*YTA@5<)Gl?-MzHjU502jp|zt4tZE_gr*H~i~GibEmi9{LTG z+n5;S16*0?5Ky7A=dkE9*$l5&oEC`xoKnjtCYux%xE+MhAQW)FY)lGJx|SghHgHK& z$boLrTln|gM*;}m16SbW2XqSFUOG92FQWgRnf3ZiaBxzQh^}A2B}e50-dpemQnJ{* zEa;lztsT7tKszKwLC!z4k=~vo4-GsA6^){(e4F6@hyGjJCtvU53f>FB6i`F?_A?Qz z*PkhYAe|-iHaolBYUSRZ80_)dY8M|t;H4MnC_rn_DS(;;;_kqIv-n@j6KnvzM1q0^ z{zN4N?m+SOc8pu#_CjyJ=>Z;22#XH<{4+1mJ2{{pJq*z~(CQ%Y0N~FMr2oC;|BmJV z14iUB>3q94@eC2v_mzTBmGw4HMa!#QXLPP7Qf-X)22oFVk#+_Deyg3*x(`UN_b2-wh2` zYolb_5YR|tPqv-o>o3q8k^N%#B%)6q6ETJb=4RCr*II7y(f*41b1~MY8n&4ztHFsmIA|x zA)a85W@=_7kA}lgN#mD>PC5RYTBS^M&GhaI#o(0jZ%b3luWaYIY*yLK^|{L}vK_^l zkvp4B%L{dGNY%Q7p9L5GAbq6MmSEeCEs(i67!3ZL&><>jIyAuh|m zU89PM_9?NzK!Q>5tx?;1WBtM_o)=?YwD$S1BPAYd15Nx3mFL>T=}>T31*zitim@qO zna@lNK$}QdSn3>2v){w#H>jN0%eoKG65=U7+ixZL!v9`cToQ}G*-eu0XEC2^BImW4 z{MK)vqEu=ldp*8RBKIMZvg7KSYdjcAIsu&$@ML*Za-tFV6)H!=Dw71X@$iEN={8>+ z&&GA_WBW^F4OnTWpkM`?OHgI=`HSxN3*FZ6{Ob8GWECGib0{I*B=oOmCU`eO5z%X=Y|cDPEoHoCSDh37^T;3Eecx^qZM3 zLE|zV8qNCe9vB@D-GoagE5p2MIs5{L$q9@@XcTg0V`&JfBSAlUaDPF=9;SWbJqi)} zbP%#h4>9~cQ8t6q{`Lh2?R0(de1Cw^l_2AE#!c{Uiyq69$8yJFYxef`=k#@)ic0BB zh3kFtZlF7NtNT!`z$wDrm377>2_C0=PX=J^yAgMHKlUndYIfI;Bk=hq*x`IDOx12i z6svO!|Fzfm{Abl@jwYS`ENo!X`D&x7olFc1EV}<#N76>d0MO3+%R~Cr`7rv9!19Kw zCXQgBT$JPGuKsOH@ZBVJg@(e((UCA4J>A{b6pHhpKw5tkVa08`;ES2@So+u|GxeU` z`*GQ3mXWJNwo5fk?sQVa@7a8FT_RH7aPR7`uWitVMDPj|A|GyODGv$tw)U#J7+^v> zDWa&Hjti!E$`A6YB0n4V(7w zw{@i3vb^|2k{0uyVRKWF;~Z0ItFlAATd!2BbLU^DkH@F_DAjrfw$;HcZ_yx!rB-$; zfl3gxH8dA20{YxsZ`1twZ|(;P);8lEnW)%Ku zm4QUn>+HCKiws1&9pl4c(PPAr`WZ~*oX$5r5TbSP%dP>wZq?(y?P>JWCEB>qpn;|p zp}?}!<_<03ZF<}8uL!*#^d2KDXte6_NaP>txNeSCY7I-U{58QpZu}wle}YCdO5kbX z7vSF^K^s&B0Zv=3)?fkfe8ca(yxA^xGG9U)zUW_bbECAkYT1sYDBW#PLY#6r5i4rr82z>u#Cbd zgo(7zJwyzKGQAi@+92yRqU>HeAy`HMpA{^P8Qi4NM!ZG2fT!S;wc3`#&s`LKJk)r+ z%EMB<(*Le_gl(;?CsB|^%tK(Eki}^E5h@IkuxRM46VLg!u%`Lme|N+B*Dt2!hSOB* zEa&i4xkypjx;R5IBeTNk(CVeKQ#5E)};H@}_C=k*B?*^ET7KMKqlkH?unKd0{= zpbgaIT&PvOh&v(16&TnB%WVN>?l6YkCzCLM&0QEB0jH!_wTjBHGcuEM^;@H1*@HYg zG%E{HN)je642yoBtG<=r2UU{t13DvSPlsQ>8dTSu-o??xwzt0scxRkij3@S317WA_kk04F1NQf15&w@Cnj2VJwVndL`2uxyxd}pC1Kt!3-I& zil$^B28x3W9e*fRZgjgTzmFa6;uFnmj_cEbTl4~67S!T+D$sCq@(`Hio)DC}$A#49 zjY4D3@!*tscO;eFl&VjmuOY~$Sp2JdizT0M>EELzuV19G9MI{I?WL~f;oaeiByjCo z*`leOD_5zZPkqU>n%hX1T7vN(`DT|+;T1MPEI*{ZTy7SMY%+Is!il~!W(6B%8a>C< zF#x{WwVwgsnp@>v=mK_AQ=U?-2(fPx{UvxVMDA8?a?PuU=WZRG_`K$8!|{OyA7MD{ z4(^%p98Ukj_ufSMD-8N(K*hBOgkQI1Jl8ZFqu^@@N9yAXUhL_~Xi2eHF%aNbB^kEHaWAv!X zHUd4Q;e1*R{8Ml!kXF*Uy?Taf>2Vng-9)I&@Kfza8>_?|+TehMS>-Cav~w^JbW9z| ztXfxTO1QB?;GI1(iA0s%!D8KE_WrFC)^oQbyHC~8+%Q*+;ru#Y8AwgJpxV*wl4Y|gK zgQI}bZCoo%o~hU}i3$UD@`_^+~Cbm0hhMKfsKP`%@r}CVlkm?n0+_)-z;h@ok!8c|R1V zl#~1ojbDR)fvX}WKIQu%{`KqaJJbYUI$XpaVx6dD=MutRXGJ18;&jv%ud`1k#;@p+ z5NIU*CSD!Ai;OSpsrTn%HOfiPpklU1Pn}}H*q71YTab)e%T2}=n*n;Bt0ux~6h1MN zr7p<+Gp@P(D9YfGt15~q5VP|7eIYPiV2TDKLSKsdc*@mmfpa)u;^44@D=kL!yn11Z zOlrFs?{q_P_w9Jr_IMeF-h1gx1S|x@SKX#AB=0(%EYC#Dk`!FR#apO3 z_+nvYih;P-6Fz*YW=Gpm+~zr_I&AtGgvk6u={oAjmy_*?SdREXm!`Zbr~$WVAwDLVG!aYwL^5Y;+l}U#{QO3*{{(oW$YPt98w?!Kn8J zha!PkNm8l%6oO8n_p#4|q_A>gA{_?q0b)}|gt0e$c2dF!G{NU~@(4^({_nILw~Yy> z&o04VW3GGMd};*-i$45l-rFSyJqoAv8d&%dhc5#{&k~)qoR-f6tNrE~F~7ka-f801 zc-wqCLsMrsjWV-Awfk(Qpyw%4Iz`%S6s_soNTrg8rFYox@c_hYnvKqs_VK%Q?_Byw zf|FISd{A9%&1PgfYHfjlmGqNb51oL?84!X8LQPyRtl&3?o6BR6%cQvG9uypT41~&b zdEn~~)mkZ{iwXf#5Etj@ZJBM(?0D8k3>A7-&2S@Cqi=!F*vc%^7OZA?Zq#!x^S(_b z;R`G4K^T5Bg0m!=!UGbXZHJV*D((X`vaa=Q={lae^@6M4GvY!9|2aj znZ%=;qVt6wSwy__FU%e8!qCj$P}hoGP|bvx{2{KduMa+QqVij^JyhsHa11c`^H8A^y|(J_(i3uW`3u^OPuq)_ zjTXix9JIZWgNr`iaG0%+b(_#>{Q$Puro`iw0>bxU(cmH$9o2;D`NRq*4?7xtCS6RF z>@M=9r>fUPH(-KZd+$5C1dQjq4`7~**H#k90q;G1Q=Dt~lg^@QWf!*xq{i1o3GYDE z{X(JNZ`?7RT3}Ekd~D^X#4v-uv3Ig$qQs}*QcuH#<=XpmL&(k! zyjjTOxo_q6`=@XsI<3|%=d42Y-7)ejb{k|0%2<%F7%HXQI0axUV87!WB>gF`VX#}(V{j^$K^!Rx(usP%(OXBel}q4-oCoy;a=lhHdg zBeR3pjODmll);Ds^uSNVfy7%L^c@KPBD}NeH@y``IQ@UehDGRfn$qONU*TWb6V;a%r*DhhP{%l885mH{d3yQ=2`)@% zKEj%iJlK%x9kT;7vSKU#)+Z5msULh~ybEn*Vox^OvW&#dFC*c{GmK(gwdAt{(Z!j> z541c%Q0Nq2f74zw|90^4-Qgd3;gaO9Lh7>hTur-MIdVR!a{@^yS$}jn@+Rqu>*-ai z3`!DUB*0+?4#oCi<9)w1nD(;lpjW{} z`|6E}4h}V2Et<^E%`TryQ|$4$w0KTS0yCo0Iz^(D zsgie?5lh*dY^W5o zhm*znjIFo17M8Q`RsF7&#vYjw2d80bc1Be?u6!#Jv8{-JhDGzsKuSuQ;=QgLl~?FQp08v3lQv0LKq0ny=tTHA4FyIOXHLsECt-F7 zk*8WVg)YT|j)#dw=DwsVR0_(nz>-c1IsasFVzZ|W;OILJ!j4+3$(3mU*~mjH-3$2P zknrLRSu&|%xx_P|)!XNF`QnNUFu`Kssv>{bv22 zB$7>jirnXB$OwZAvY3Yla@tYH`#=XPUu@ljDOqWV3*k zIsj0=Y_*a36onfF79Ab6q@?*~%a&k^b1p14pGlh+&m=#b9R5yz$(v#v$B+C{<|QXv zRDWs0Sq_`O`W3Bx2|}y}u?Hv_bscCyJr&N2X6!9y>gZ6OH|BT$4LA}3@0WXf z7_K{Uwx|o=l|bpMM@+Sc@yYxvlx}2JiUHjuLi{S^%z+|)AF~^Ek$(hd7w@O&{55H$ ziJ+oYmT~-pwt*fA#a_w^?EFLJn??p(i2>geN|+CV6VQFI6g}9s`#EF&4x6&ekg)7- zqeqE@6mne~IT-9N2lfm|;f!BWm{F!fx`5R|3+M)8R=-&z3-^4LIg5;f|3!p~Z)Ox%(q=gC9<|X7BLB+s`KnFvToYgA4fgkB-od*gll-GnPBeLo7y8 zt5k6A^v^RH6|qNfyHA^<3q^NMPzTQQ;Nt9zAYIV~2Pz3$nOP0)EGQ+e;y{P--`&u* zSBO*Bi4QN+tSGr<8OQv-yHRLz-ww@qC#6q}xOwS2e0@5E-!kce6m8z<_*3cOM34pK zxxI`e2}Z>Zj5Q-Hy3L~#F-gS-0zsBTGKW2Hd|Z^cIlOBFXHhXi`+q{DA(-q=$oxq_GDGrd1J|tV4D=D_csLAGTItsz<^L zqg4WcImF8z$Cmxq2t!&hmX3|gL;Sq=qN@jWO$YY9^}hgW$?#`zWw(=>g3pnvQdQq% zN;m-*Z%st!!K%1_t$UFnSPJcB-F1$lJdOM>CieF*A}T8%i=kT&45~*ng2zD#O_*Z- zjJ8Gse4*k6S?kqLM-3sd`K+ugO%1Dq1 zd0lJUYzxkELGR&>#_x{8vXTH%l-pU3)oD8 zhSVV)J(@%{D+U^h3`Zlw~8`~84OL@+qV(&HsZ zmKzuSlJTcgt0)r*ArV8z^RCn9m1w%u}@4dGg%3`$enpEH*}11 z5P$Bdth#zdNtJwTcaEA)l}o8InJmwOgtT3^ue4uwigw+u2pB3YdI~$})vHU_|6<(a z0vPD#6-aBo&Q12Y`YD&O(xUR-%IhYGkJtU1nFynP%pm90cARX6`W_xijQ0GzD^>Zgd#}5qW|^p?3?ui zymlPqP;$*Kuv@6)enDZ%%g>+9pgEMzFfq5D>kvOqt_c`b`7W*i`biGYT2JsA*ptWs z+uCEv%iUV-v;?xUySA=GvICjd<^G(uM?=T+Z^auzZVLPS4Z#@j0B+s?bRWT|T6eR9 z=6XT8Cnps2ovxEKj{EXRnEM#ENU?m)=@_bJ1bi|ZD#u=+ZzM z`FU&?U42ExKRzTd0Q)hcUp@NM_h&9Yku0`_1<$M7QU{7@dT3CD10sm>s-}hWzd=!) zSDJh`i(JBAW^ojdW6&wKKKMnHLH|3E%cr0?9xbOU!)i2GVo|;V`<{uQQU0~pc>@@6 zr!~UAW@~H!OZ@`S1@6ziuT-#zD{hO;#T$k>K0m$n)K*nxv%t3I$!R+R%rsC2SL<}n z>lffzkM~Qz*SSu{?E}Q%&h{%RTOggdLyh#aq5guWI#b=Yj?XE3$P zBNHW;hPe}OxdV}HC#h(GdtIHeFU%|HhZN9IrL(>P@Vq`^zlQNx93KHbD!@|M+RSR< zy8BwqZr5!T?0oR-*7<`Wv_eb0&&L3xIoBdI=IKSNB znB+V*a(Nmk)CCN-mI>|vH0M)w9t%HFXT?j>EmS)`@0Sd$n^pMv&yOA*!LYHHZnq>3gjK4wD+|Eky3) zD`DH;-}dvJ$4{frrRZ5Bf0RHdLRCE_{5C1P<|ja$_gyPM6I9r1RbM~ef( z_wm6Hz#m8vzpb31N?;lW?w76RX}BH)VgLSIP`_Z>ERq8-zMV z7!R^m{zse&b>T+)B7q2Rg5GELqH;RPHb|aA0^_~27sX9}a)A$7Rfuk|mo2nqWu4pO z`Oek}mdE1S5t%p=vHSuY0wm^?ZU6=H;(pY~V;DE~=^4;Y?5qsmqKswA9wcr~fVyei zHc&NXd?prfBboti(oB+^U_xnj4LapgL{Wf93PDp{S(*FL7q?!4EFR~*$|ro+KX&}G ze^6+O1nxnR^HURWpSAviZUXm;bon=8su;#M1gL|LpNK&^Ih7&@7H#NtD=pAA))F{M z@HRB2>X5L!Y2shM2rE0`CK55-$KSk?Gn`AbDhzOHM!e zx;CGSu3&jDwo$fR>1wcUEPdi;et}f>n_(zAoHJh!MA5OqWfyIc{HB**M5ng!IGTUK zI1F3eq#=iZ1N?Z{JmI^PU#WitghEh^w@7k=9IFDVe@;V6x`{!5YaE7~TgwgIQbRlz}OJt?DKrZ7~A~>3EL?Stc zaVKjuKW_n{Bt@_2*n!SGtZ~*XX4Mn2X;jTNpjNf#O%hBTfZw-pLkYv9@BTpw;^eKP zbDSbB`hfm$_RSxV>fk3-FV?-0!^2@h z%L{@Ur3g)KDoq($_)*qw0H+q^e()M{nc8!#=KgsVuU7D*qz|>68qeUUj?-%TdV4xe zC)og$DK3|%-u(Ei2?UXW4*|pI>)GbELe(~fd5yuzS3T&x)#Av4Nehv7cv)r6H-&D% zXZ{$fO?KwjZy_@Znb-?|XvM?9R%M+}dZ+J&gXWVRh!li)p_OBUBXTKmEBezOP)|U; zFyv!4E$R_1XoB`g4d#tWh!SmXD)%#sscJS`2Gslkt=>9*Rne#yfK7j-@8J><10293 z<0cvQ9O4;Sszx)dbBHyd?P#q`^tL5cxKKt52Q?Nm0%|+L$4h_-;EonQSio`>xGvM| zc(;v=@y;@M9Y2sA>C3^lWxeV7^TH4yykt$ttW2hr@`2CzNCL^7xoq*|UON%j-N_UuGOOjoYDlDTn2I{lhwjP&dY!N-(<|$Nh>v zsoZ$eYU9FhNfm8vxwY0LZ2rR{^23;Qh8QAnC~advL(_$fYbe{n)l8VvR$pb_Nyx|v zaBm3A=d%ST<9Yk${Ne)GK}7sJp-y?lssmh``1IOMdhQTx)+obG zQ};J-0i|Gen$Y7u+hUbngE&G1FWsJnL_4Ur$R%(1WQ(umx~&6R7a&B3O%&^}vcsk`Md7jdI9UY!t!#Z@meV zBz44#tno5gSqm1-=!DrdE{J|H27*;jO2pp&Y*K9#K757 zP2&;o%CF3<66YusCKVC1WX;e|(dT**xN+mBp&?eAb55U4Q7QJEA_S)(AcRo}6_ld# zooM&skNqaP#fp1KbNV8YQfb-DSQwFCe(W(^sceo$1C)NL2W{Dkg>}GZRO|QMsg>ky zfPSJ;(@m_Qb=|8M&R-603VB>r@H`xkM>)y}odQ^(aKi@bUm9vd^Z(4WTVqSL1eKGA zJ6gC>x}I;(g;O{E`PJD?Ts`rNQ~?C)RV0~Laq<7c0>*;cNW{uhh)ND;X zo8mLF#O+piISsAXUpi;Z=Xyw^Wx)ND5xZ7^qRxPS9p!QBMt$6Ai6^dJ;RrIpyqa*7 zhJ^iVx=;fed(_aHSg_j_8H&b1BmsIZdR$2C-(q7cXiqzyu4{+-Zclkeih%)vx!}xO z6s5jaugVr6%nm95-uLQviOaLeys_(+#{*K~2dfpWqGu}qoE5@%e_6LS*R`+#QH9o!=gRpnA*c1DzF z(s|1Dgw}|t(jT|C*BmWjTo**X%E-cb0is?C{bjs>qjph9*tA}llvyPe^$bQ}3g zp0~Y*D9`0{ou2@~TTqF!g`EySexQY1q^A51PaT@yZ$)Rai z{;=61rC{C&JG^krMqgly%xi8&1)V!wL^f(`n3TfL)E>Ytmd! z3E_ZmLqLIIVvhPCy12L`LTPKiP7(fyNvlRi9ISx8@RV~|d>-Mp{QYeopJvyEV^b;D zQp$M(@&oB)@c}jHE0i33pljdo6HPdsk?sHvpk52LvaM^-t91ZvPqb<1t^>M(URtQ* zLPo50yN0Q(oMk$o%`}dpaSfP|$!h8`i;RaU_ zW5)nRTu8Rj=yC?A7p~jY(8V`rJ4Omyg;Dae7R|Nv1aRoOv3%Y*=)UG%DmU zk}3=e$2xMz&~sslc0t#xCIV2PP#bQqkj5kO`@d2+eK;U(zF4k>H2`Xm zI}9K1W@TtXcKHSKLS!Wn_e-#_bXq`$vS;@+4&{`ef@X#qKVm`x81V)MMc0i`d1T@E z;GajT^?epMA7UextGgd?dO4cr8XBw|S^Si7{8j7-Xu^^Z8U89wM0yIR@2JS>Bu!VI3&a? znu7Q@(y!#aI%LQHDdOD2nQs3$t|2R#ZAf{_VJH=I${~@335BJUCl*U6F{ud;kyBU> ztvpN0d4z{*H0CsCc}iNEOgW33kK@sV-#z{D`}2EU-|PC`*LC0b=e|Gh_xtq)DRFxP zaufz3kDI2VAoLRRcKLHz7As=F_d?&dkxxuaoQ&ZIne@HHPP^XvC z!!jWDBY)x3jT|IwIq~}f#7>+bI)y#FEgLx9e?$8VDK|Eo6va|}WEz3$HK{Jfi519B z<`mt7sOFW(=Var;W$wS}Sv{=|{au}+-Gy@>CqIXaMWA*+`n;HpF7J#B_Su=zwxM_c z(lR`ZR~-As4t@ZgxTDdCeYQpoqbuh4*&*Iu5CO zm&NpPlDuhHg2TatJ&lbZtlDy(-16#8$hdU`YC4vR-t1T~BOo@bO@E!WP%FkDrc%K^ z#t-=SDDO4+y~70I<)Yg87b!=EK_cMLHOe~-mMH;0u}2MuT8m1BN_+tlP7Y`%0QHt5zHF^XmmBSp76cV*b>&Preu#vLn5S5py=*e*WkG9va-}DEMdk*o zafi)-!2sWXV1pDjJE9T}!Co~i)$7Z)BZ%%i8auho9zTje=k{J?2&f_fJ9T%> z#WP8k(lL>irZ^6(27`0xn=0>fjwf@U7fSY#PiHZURUaHZZJA|3^n-Chw&bsDqU=** zcj@4is&}N!rpsKa6g}}hGu?T{vqt$6PNJzamT3K<<_F*2B)8z8$LLg>MzA} zr&X!Dh0(7e7Q1H)*y+D^q)g84-2f@`Bw6y6%-VL_Q2=+n@zi$`+%b;zBD>swFE8us zusBq$vm7G5AdF{uMudDbHT}$&O5=1czR+*;5ISObxEcNx{0EonH@! zil|goXCgwJ8|@G_Pk{JLK$Gp~nEgsgMeKu`;%HoR1@uBNXL`B%?+ab%{f z*dg)yZ-90kCnXhuqAo4aC?nU`*Y_E!trew;OY_8rg?loq3M;Y3w?J5xBux0RBno|z zXQ@p&*^N?vib_th32+Ujr%=C?>+7XWep2DO6?gV_&fqXZxPgM_idWEhZ>Slq3OVcC zyxnhz)7%nNS(__ta|BwY*$rkoML+j~{%Ta)X;PqOqshs`@(^so*#-ZWqMY?q8dq@I zav-W6MpLk+Q|}x6wzZnqCoV;Rx*^bzh|1k{EiRGt%H&F5&!R~)_*|_YXHuw`_|Q-* zhoPi)<`4a-msqP{Y2j+;G7!7{_j#;BSN_XS1qc2i+^{av4A%?e&gu_|t8}hUfgbqc zh`8n)|9TeXHZr%myLoR}9{uKJQ8ITP*%Oyth%J_3Twh{rjL`OXIUJUUBB$Nc`CPN*N~Gt$EB@bI*QbaOTs_)V}KP3?5{RQ$YUl z)!T$R48qqS4SSdRKIH1iZZ;qVD(Q-KTJ6Rv3IN9AzqK zQJB@l@~0Og06ui6Bi1H5vn>(rsy?e?*0CMbWb3D}NLV9)1Y+?qS=92Jf>D`7S!9C(il^b!HXcVQdE>{+458k8}VpdXS7vWtmy=AU3@oscbX6Z zG>nOdyx0ud(PX*)*i<8Tsu5wwOu>;63l61@;ng9hpgZk!9IEL4>-D>apuIo=Uzl5iYD6H zzGwZsUs4S~}WeC4re?QKtJY0hP>!m;k@y!~Bby4ds^= zJUuGcHRcSK$;usc9(1lUugj+u0b1^78X*n0XnBU^Z1JjRBM`x!kb|gpbvh)l$5d1MkG6fyyOaqR3L^Qeop)oG8s7v%ec$7s-N&e7?F};fd38;L*F$M@Tc{@QZpt~EFf`$nA+Fmyu`L23 zqxLngZ64^v)N{k`C|o^fZyB%M`CXl%XPfw}7k*g?oU=dxcfeCsiH?cWkX09}l3G?weti#F6V!7{c5XFCqO{#o1nNrg z>d25naLU-oiB*^+pg5#_-xzlODG~k$Pd;th>SMsfn1%1(3APDjoM~0V0iYc=%(mtO zKO*5)BlXyBb(d-DZ3MPk?~0ChyP;kZE5-r4FPbJ_b;E+Bk4Bp(xUf1uNIGievN^pF zI`Tr1fp3porRqt&HmfOav=EQcX3u@Dx&gcXh#Cd=ug{(zD9SzlXM?K?k<IE6@u!)>D6PMygWmJZG28R3 zN@`kT>UPOFOD=3ES2PST{m=EyjZ|uF^=f$samH-JE5$-PBGJCU`zC!4=fB7B_DLPI z#aDVWJo-z=>)pglcHEaM_;#b)k6RX~z{62yHv+dErh$0_e%}befcQ0Zo;~zW_e+n# zlT2=dvN?U|wmGGE;B_HTz~B-lp=xp|@bl8dM9FP=4}>Jh?_?&5RbjmV&jj7`vkD*c zlb6bfP2~#Z7ajLAeDq4A43hr~ui-a;@%^^BepuhPwNBTr#0#SwU-Tv*M3oHh7wT_~ z;q#5tuTd}J`kR@m?KPJ~Gm8PeVU|j<-6r6q90l<6cWX>)`sm!%HF@CRZ1K)k3#eT- zF2$59ApElhO+Q3=fIs^b8T|w|T_=b*3oP6%z?vKbu(paol5x=L>shnfS(%%l*NN$H zgMpKCi@SDEkCXb^H4lsk8#lfRHUfzee39mqYIN6=n=}jLm6#4Z@&9k{5q>Nau+B*15 zscX#ZPj#0wSE3VD(ll!^!*ziI2;sk9zN<)-_j)AZ`q($|6>aJAR7m8D;AYlsy3;`> zxYcVIOizM|j1c~G=!s2C!QtWo$7lr#0WKZva3p1moS1^yV-{AY4z;#AS@$O1EHtrGQjMauMrd^%N>-tc0 z-`DXV0;|v9-qJA4L7{;t&QJH&#Bi@jCR8YGrG)weMcCpiH=lv(4Rj*FGwomXkU+9-;DcEI|G_kh~yh95{A zx9jMWR`NEZW#16@4+BZn)b;eh>w+BM%wmjEnhOV+k-6n?Pe&ptuGM9&yLOyDKO3*2 za}$kW@#)AHW&^+cTG1kX<)R<9KiHqDh)s>aC+NY0WHZmvC%`3&=k>iIG#=1HidYm- zf2&aJ*f~lr+s{C?ZLD>heB9#^R)1OhoBPY;tk^2S`#o8x;Oy|wUTd9+Em$cwgSho= z=O3Z)HGD*Q^2q7U4aqe&-d9C-mYv(ukD+vosK<7!Zdby7R}eng!M`;(p7kmbn;Hv? zmL7K0k5diYZmkia6GBlHA#9n!z}1&q7blNk0+SD<9n?+NCFSF3XxbG9NiN`DSmEVJ zCZG|ETCGb6U%cFNAIdn%$o_sj;%75c#%B>b4FXMdzVzNg!d2D_dQyn5MkmGs3ARbE zj!G*yFGK&d&UXF%(r?3*@OZ=4)!Kq+KyNZ)&qb{rR}se0M({9>pIM<)By(XW8bI?5 z%KllrJ2^Yya>p&)&y=)DraSN2o0Nfq2 zDq=7&J^FIX zi3&#_CGnAJ+NtF~Y*G+5RgrIXjL&G!y{3L;%bkU(34tsR+qJ~V95xV0-ds90uyGOB z7%{9&vbg@KOXG@K=58>*{mHk}G~9V5Z0>E>$d{pU)@Wq8cExL(+Gks%oNbixRvZ@}^Io~Lj*GMFi(p~h%L zF&TPb;VgOxhGLU|KqI^7?%sp1+^>e9RhzQtz)cbo5`%ntW0#YPvZPF%FzWW8VNM>` z7FL$x7D~7q^X>?tXVu=7#7UY)yAKG=%&&qBiX>B1BA`X{fhm<7ATe$AXCe<~Z>8QmImxxPk(M>0*_h`?3Np#_`mSm{)8vK2 zV8ZG9+ZwIQ))oXZWM`MBql|Ma_s9+}+ZzdeB zI>`3`B{q_XFEL^AO!f_h8fz_AyjIv*L3J%iC&eHsVG#s4?oihf_D93Ur43e@zO`fK z$32TpY<&L?zRSF`)-brBcK4*~I1G6=f)dvv+O^oKqs)&&c;^&kXxXFpBN_TuB@()e z@KqI!gZE2)@kMaW#~9B~mt6|iPo{H*m|DMmwUG-bEE#p~*tQT-_Zp(J7_z^b|7dM^ z$a%ly=LBzsW3DR<{{5SJdUtcIU(WlN4^w3S!1CsP476y$q*|CFqN?a-W~5Mh=>1}j z)g`G|MVR9#36Q_18-`7O>{D)=%Ul@Bn6TaMF$*DwrOrz1<+oLqeMn1s!~1Bc)P*Qyb4ViHq%{1O0KiBqcp#i1{K>_bosru>FMsEbNBLGNv_K0 z{In(uL_IgZO?d7_ivfCLv zPwk+69LC$T#A^B5H6Z(k>sFIUQ+wlB#`EEec#g0zzIV~lkx*9-_G@{AO}bB*01s^S611w{ zj^TF$uaNpYKJ@WBjbA!xJQt>0hu7 zt=vF;@6g=jvqK9`H&!;Qx2J2Ih(oa`!fE_O)1eRZW6#|m%G@|OJyN#i9&dH)Bp8#H z{JyaUG)e?~9k+D9J!L=cpvP%n>@8BI%(h+(Sn>zmxj;H`Bc!f2!l;jjhz(Z?n5e>N zP~A6A+41mk{b5OL(27@N9!$VmR%Y+St(fZJx2r*NhAdROM&iaE5d~s0a>JlwWsvx;?l*Eyf zC-Uq4xe%ZL-_z4s*OkfQ(%?loaGWzF4`+u}=CmmuwokU;m0T~O^c3Z2EBAr|$=w3FsyZXrk-XA(i^t>8@{9c+)5v6yy?>wS{E}95^ z+qD*d5Op(IDs2s*0XH4;;}5s_Etndlw)-dRJtO4IysgM=zHc)SZN2R#LPWV*?e5jc zRVeKyZoHgrdU+@tEesj%-u0D&l=EPExFHeVCjm)@r2ln6o1ax+_w>)&=6PRN`$rds zjfStMNAl5K*N}9j+DDcy19Zee%7Eg>RO3j?wFh&B_4j;~RaXWL1Cz&gAZx0{3j>rr zKVq%fmNJ(+Z|{r+Vz{NY<7|h+J?(I9!cYRz)GOy+#b884BNIn1k;Q6aH^pq^)CvAG zR$gul5d(wpxR~wZixw3C5DkeolLi?W!at^miRnV4G5qe}Lmp>>e23Y)Mm|tW@6_<3 zwwOj+7AsAz?gF9JIie6JzPB42$=aK6-6s$|eUJHZ*%$Xu7xOI(5`tkP`2m82fX%o{ zX7c18PZI=80b%^dTT}*LDrRU#;Muc7)2<6hJoM^kl{5oE{?cM2?rkp;$<^heSS{-_ z;_Ssh{~u^TUJ&2Nrx60U-?rQ(X7yXHYmMFjiZl5J+q zg|Ag)!z02GKU^UJQA|>MsEuk%xAyp%iA&QbcvSTMk7MJkN2_LCLUT)XPtLd`(E(2O zs}_1f&HxM9SCuI#xL}EIjJX(r{%hhvxXSMr{m!(36>v^Rx^iuhUeQqW0?+@wde$>=`1uAnqo`fBN@D4ot(cLb- zevghU*`mbgU<$4ugan~jl9H<2C03sk3{}w86|goI+!30D=I|(%;!+71Q)~^Bi`D9t zl)^()?Zk5`*w?s73uCCmK!%1CZ+vFAuQGE>%0l48ER^Esf5mpq6!;P%Vp3DY)fY4O zeOHku%fc*Z6;(7NW740sf?nA4NXyXDX`v+)pB|WhUaXwr(;RzUY&^0>bLX)u883Y$$v{AVby3A(;cU_Wk`q$ERxEL_*dl&6 z`w7>h?`K`3EUomK>G<--^DuK2!mpNCh+dK6(*0yS44O?!UO+lIhDwkk^#XjM$WedE5rZr*J*qK`T<3 z3xv=2dP<%@vWw{+s_XEER8##R#W~eM~e42;H7f&lQ#-8U`Av9X8 zVTCt0#DM+gw_8uxW8Xj?0Bq4_>h{B|#AKliRMaa=)nIfrh6!I@9Rk8e<_hW~~Lae-{J9D^&G z&`#_{zxsL1{kXJvi;K(Q!|ct39o(lMxdiP61^RDbtXl}_BD(lK&wDUuWO&Gm54om0 zIo>8p9N0Jo276Ad+?t6LF-C4ejkIELjrFDVx@?=St&;r_1;MbH+;#yyTUXzqJsR@LG{De&$y)9Ngohvwj3iN!~qWd$A;q47BBZ&c;B~=IjU=&1Wxj*w{}2J35-(P9;95 zG}ZBwnQPu~u0*nU_%y>?{&{+)m2rj#R{t zXU}U~SPPdqHL;D4THnAK57>!q7ZOSdaX}t#n*&=4u%46S7`GYP{nqKx5Mo3|I5C)y z{iX0C{Bf8oUGq8R49?GUCzs5s>>vbq`3kqY-wLly_I~)YYgk;P2rI))X*T7nPzlt7MmJ8R5Yy}6j& z-f1BrrLewm%gJW9+S#Q|lI3_w3-`j(l0=jRtriQbkM(8G7}&{ra!S=nY;H9WufZ9A zOtS?iPCOU5OITa#yCeC?Us^5eV?l%}=FMZ^a`D_kzgKybLL!1&^|1{(Co0|>Zc2pr zL9AJAf|69@%Ym8csB~aN&syZN=RL#g7qn`ioGtTfevM7?RaGe+zwC1+N9C(Y10M4v z3FruX&|8$Hlg%h?AMBRJSerHLMeT77<+y&zB0L^*eg-+az7FsG3aJTGjgbG$5{o2MxmwKdpUfj$d-Nji zIB0CQ()Mr$@suEo74czpI;x%Hr21>1Jd(36-rF-!1-%X1gJkC|JHv%OxIGiX?c8n~0R-hQ+$*GdJtP z*LqTVsUAh^Jzo8^m5Ar}dpYVr`JQPT1PX#fjyWY}9IP{4KOGx!{b7)~V5>d8nUmy{ zFu7v&@uxdZe@H&(QiYos{uYu;3wqJU{kVyT%wmOhqri+?a*g^#zT@T#0Nh}|%OM7( zFy#AHE1Ro*nv z={*}x`_VCv?>nG1PfcF`rk52@?zp|9=^!0qnF=TNqXfVGp;<6lg)Pa%Vh*$?>PoRw zH$))CPDr$}jx&|3U3LKBA1M8ME2*xJ;74uOXam*1xG5yy!xIX7n@7t#W$kbuLE(-S zhl~h!qrsZS6^Z9KEai@g9Rwubp6U&!+@{{M&fuS#gAY;K!YC(wAo)H|$X9QWn3HYD zOvBEdTwe!!rrs9T)%r$jF2E;Ma|L$opCFnA!7-UP2mbniot|U~6jlQlH?C`tf62W0 zl9MHBAgRnsz;Txd08N!P)lH^}KTOMufNpzJgnbu!mgOmL!NqwUIu2nbdw+bBL$6ZG zjmTc{<*ete4^x-g9LkCcRz3=lRc}@@^JM(xD=m7iiR2O7`aTgyckd~YTr{>NxHSA? zhtCwHfA!f)!9_YWn1+i1)_L>$Ua<5qJ%2+{Axu|=iiX^`>*ICfQ2Rl3)+r zL)m)x!#>MbU(b3~Qjk&?*&!;A8R4Z?G|_LT?IB53jj&;-Ah^NJfx!c%v+oxkhC3@{ z${+Z%i`voU3MN-i!t4DMR@|Zvz?D!1@w1Ba&icXZ9 z4rE}3nOGw=i)S4-$idnM=!hg1Cn9^sk(7LHG2qShtn3m%kLd(TXA5uu#0W7cgP|-! zzbgcIGX~NRh)8h0d|2Yk(A&}YG!!2v@{-F;>6yZ>iiru75PJb?14&k)y4eJG5CFtL zF_s}DC(8)fJ`OH!qTy&5LEpjzT`7cu0UBpwV#>7lLzw)BiRvzcXyKV*xN-d48^&h} z+ub9)#me=rIA>`?ZlZAHHit%{P^cf}GuK0g;!|~IcOPP)kQR$(1&w{cra#L9e&NKkako#FFzpREx+U}5haIkpJgRE%d0YO3gIe=D2CST{j z$nUb=N|2=uXsAaOI|MrnQl5#X%=^qw{ve7)_zl;`mDu04pR0&=GZGI)qK&sTFCe zZhs^9xP!2}sk;2y??hzk2#yaoyuRS|Pb4*-ee76+CBhjtLW+R#AylhP`4_|AVr`c2 zsCUk8UA`Y$<0CQ?$JSkUvE;bLIpoE5fsXEU4|9HXHu;+`Ug3a@Iw~1p6v8)@TK5fk z@4C-?SswRTh&R_J0N>WvKM-~>RyOvBK6JGdVR7}_UzVn;>6b7{%M`+AuPVM1estqJSv&q_*$Eg1c+C z&$JH%Que`gMB0sK-zP4BD6aBe&||=Ax|^t4h&(Oi%&%G;=%)6!xM{=}6Stuh5-{#-C0As`3kB^5toh%rT zo7|b?MBhXQ`W?a}rU(l_heQL-+8!?eWBhs60 z>B3?ES8woP{Yh64Uw+w7?Cn?4r^phAsi=?Xp+J|ltv0Cv6&!;V^H_k4GU*G$p>rGj z&HQkL67DmP*K)!b%$EA`rRjDrL0ne_G6>VduV5VNoD5^_Ru@wb16?mB9n1_Tw$C|0 zDQ_I$VPSfTgmiR#(g8E-%e52t$5AxN%1?{F(TXJSso%~0{7N4UiI!I|bRE1q3~A!f z#U3|_b6>B+W9kUQ7c%C($S2V*6_H3Ufdd}#D*7O-{u}!+JDRo-l}ClG%=*wc$@x7s z?r$?rN5dOr^8+6Y01d*ZQz3f-A=c-U8hYAA&x0PC5n<6))BQQuZnTYFgfN1fx=Nt6 zq!BTKJhZ75pZM|DGbUSayfNHWQ+IO3jrN>ZvmfUlfXsv_F3{myt>ixDIo%5--d48d^)^pnT+#mZubu@WVD{X zpbYKl=Df}^*xaBEx=b;=tQZ-GM7W8uFqf8e5PMzFRn#K}$_A&5D6yn@Vo0#w*)LQm z7%svYFXHOA0(E=p=C~DL*Y@7Mi419#~VElM?;0_c6i{w>X(EpFyW4_ z!Iu+`_UY3%V|a-t4Uer8G58;)FZ4g3kNH2)`BMC!#K43Bj{nC4tz;E{oLZ)bQIRo& zgGZOgJ##NqIZW(@4DQ{9PvqnV`)|CwRx9GlI!0@_r<*?W8EBnz05Vlsvc8BPm21&= zBS@xOO7fwQGYNhbiE3|5ZG+7}Ho9>Zgj0r0EJ=AHYp@%l+u`64CVyKkGF0INMhCt@ z2ElF$G*l^q>&WAtK!cUy)A{jxhGOU62!fx<&01O$1vUgQ%a1~eppun@TyN4KR%=AW z`3j}d&KAtce^YIoGQXfdYnl`0__T(a@mByP2;;ZH({lLq?qSRNs!~R#?T!{hru7}( zT8M)DiyGnM9?dv?`5oY)egcT8YLU7r-f}8R%MY6ZY0hV_Ld)zM|&Dwhb(@N7#^pp>qn$y5d3kvHa8sTle)V5 zYNEmxF&?%ddxhdBC=VHhXnJh6?>k=BQX7k!G=EtJlVV-n!DQZ22rBlGn+0?~0w4CA zcwmi0jUd(9b&!#qguG;BWEy$=9{Ha0NkXy@6?vUc7!YHCnAHZVkN6q4CyVMdhopPQ=uSD%v&wSS$6flar&9u-%clywKJ-9J57^nU#4Q5Wn{c!$FHn z^0u8gE{CfHoyypd(QJKHX8-#xlb`kbuf4%;)FRV2&+dGhs*aM=@yG~aw0cq9KdVu` zPMe&faEyzI90bQPl(c03{K-XjEGeC^Vl|&W)BWF$By%o;G!Tdg9kv(XzmehpEj+{Z zt6;5`+P-kq7P~qW1{GyZ*aeTm z(PNKPp@-iC!hxzfhZ1_>?EZAcM3^TJ-00+MdY|h?fGOP85C+s2hn*2oJnvhQv)chW zR1-z@r<~M4bvB*qqpM9hcHo{5{Ual@Zb;Re>a`GD*ihBG!vM2(UduQ=nyjuRtK#A{ z@oig(>PS?X3{ux#$Rd}_1(Ur z{i6Lr$I)PVw!l6DHp}8emp5=f=3L0iq?5x8z#e_^l@G>O(Q!e;^>8F74|3`6!k;eI z>@c4L0pCk@+=S7p$g(Ee^s|UV%E$Gw(w`poMrn#5{!mfdXV8d43>R^HvF=NJ?4K~p zR2q;(Q68gu0c==kFF-aD8Yd4Bhl7Q1?PD>XYo648%)zkjv!soOzPD!>PMa0-U z4lwg7*H_rPSbp8fKg+=NE96tmO{H89?)8{;fHv>l`QjIc?Bl15RL9+50~-f-VLH zooIbYYbr0kpkQ@;SpHG3rS(xx4Cn&^oF$SmFd(D)9z!B1kmIn2oa1};@yGMgZ<9s< zW+tJcw)<@j@cl6HWt*5?f&mrCzOmA(MRfdai_V&i^h_wXv6P*!J_uX>ZuxKJ-%Fp# z*E}nt>91E!jPaU;g&hW_M`02_42QSF!D(@s*Zr{8H>H0#0`N`46&vOOF|@$d#{95Z z=wgiQr&c|PX^cBQArS-E5A%Xv#$@t)M4_vJfE)HjAw|gVBBrc*uPSXQ)CyW&S=0RA z6YT}h7c;pFSMw`c_tosi+W3W>Z|TNg&#tlq@TGUrTJncn2#`@(vfdyZ`c@&i4W`5@ zqe(VQ@4!NBDwORxjZF!1*uV}MB=ZaI;-8?u{X7N4f=K{T^4TM^^OIm1bTafTirAs5 z5*3g@t#lq9lFNXd3|EVv-Y=Ca(X2a@Ej--4Z#wFe1TX++X{W)=%y_emmIHIT# zTG8wmd)?`T)cq!700Z8%j#W zsf2?4=2n+in4Sn(I*vF(&(JC4g(GT#CH;USY*F?277Lug4`7GY#cb=2wdGDhn*q>L z=5VW&tQd%@c~_CA6msRfsaR7}*D)AC8}ek-2EeFfNlvYvwlB~ur+}R%)dLF-F98;* z4!xHk-qd_(WjeLmY`~p9m`g)7HHk?&?^#&?wrAhCb+5?(KNfBOAtHZHAg zdqMOkiH(DQ(!J=D#0iJ>0vMwpDG5~-;QSD~0J&;KC6h9#EV4K3jKr>DH|PPr-tH+Ry$Xq@)lThou3_fa>7r z#J=}08bPJ&srkGo&XeIH0mD@~eA(fuV}D5B=0}k#9bbN2@S#?wr7c`T0QHwf#}WE2 zXI=`FQJ3?G;?-%^<@H>7#%EVZ9?z6kGz_I2A)z3qBm=ZQZ$f*!nIFo{bzWf$6@bq2 z@$uuoB!?b%eo=ys_|N5;L=pTIY;a^#Xyn}$alLA;QN1ywVtbB{D9eCvQKK{di$+v5 zijyB&7rRq0tRCYKzMSM{C;<0*@bwQYGu9hV zpavt8ec&oEF;#84fHLLA57*TDd7~WD+v3x^0vF_}Mq~H zGM}ldO_%*{>i51);;k)gpp%rqCB=`Ph?b`i^&(nL{6Y~4p)#MXdX-@VEk^}AyZxw_ zBqukbczCvF=aF|C$K)%nPNC9G(k65^A zQseuCt5Hd_KQi;97=3!@crb~%A)i=&KsZeLO;T_leZxf;ecKgz*r{Dw;NJ|r?}mTj zj41FE`Ex{pZB}uBPXqL2Dj0lqs(VP&I&p_MYA`d>CWaXv*)@gIBIDvCyhEOTbbZi# zb((IMeqbW|93PNG#7oC%M-+WHjg4WB9UBxjGCdavkS%*x+qhD7tysnGT5K< zdh1Fum=wp`=t7rf|6O=9lS#x!lPme~UAZkr$(!@eoD>V|`QOw*rlvIb5dc`AfWh?# z@G*e}4;kY1weiiBLpV0peoagBFP!-nR3O>*L>>Bo;KZD@NPv|}u-qD5!|ObT1RNf} zHzP9ewoaPA0kS5W^+GL%pWZE_cXLyv#Vl*z3${STsq62$MN^$1qm3@KX@?`+A!5Qd zAh){>r$rRU5Byo(3Z{i|TyL2$1OE1?dzrr(kJUG>u<`2X55a)(DXQTNw2ajED?l?D)TZ z8_v}N646;H4G9~T+@gM35NU4gBfsv2>Z3Hkv=A9OFrw%)0SCG=!Z^V&P zBY^}uQku@n@nsz$$1DbtYpBhlv4tK*A`UT(^zgnL{B2=v`xbyZEh0z4@ZLUx!ZtB5 z5Ad8=7o-8%doJ%<^fT98rO7h(#N$v0i+RxErt$L00dP zW3K60a~&3J47wd3VXkzYmBR>Q-Rxj<6R8hs=5MK<`F|g#a@(1jdTJTAb|y^YoW3Bs zn`{;K;(BjRXO-Yn1$D-sT@wI2;e}Qv>@lS#+&s@beg$@6-9H|>V&3k&xKbpWUia+s zrGo8W`y%28Z(?{!6&(*ONqG3pmwLXJ=Oh%NwA4cWp{m!#a}MZ0%aBvnG<4v3fFOy- z!$g|BaJ{KZ>~e})h9d=6*1nM8qAv1S3IV*#4qLO?Sxxk;`@k2%aBKcs64!9>)p zuj3AvgB9^6BjyYrWSe>?Q25cQ>a^X84bh-a!! z*;_?L>{x2ig{glUY!_YN1j1*};6P?W84*iMOH>%)f0L4uj4u!7nORx)ca@%u)KE6y zCRU@v3gWol`b0}h3-X_woD_+DqUVZl`Oz|;&eHqe<>|)P4J8iC`}^gr7hC47Q{@>Y zm6ULMqDc|h+1aB$&CYwD4$6M`AR{e}M8s}@`9lpI*!|5j-Q>X?7#PT5HNgyAMi~(i z5gr>`r#*GN+6xDYB4ULHeKa?xfBqavt^mK#ApC<3C zaKi#;`7I?SJb_*Y9#m3NQu%p`?e1*+LyE9hT{jmcFCSliQxk=>wDi_g1vMzAzMjOh znTnH>)Aje1vVj4WqoZSvMk(#L0cCA%%gA>XQ<4n$xg{m&!2V!gU*CD(#V1E?ee09u z?@Wk`3lbF-RnycIR1tme7hV41S~o&KR<~hcy1dJ(f`Gt;?mO2a6I~>0rpn0 z0u^z(1hG=cY>WZ}J^*j^>g{vKPuA8xhOPdK4>|XMcf8os&JgyRJabc1BkYNzwyTbo zqHCXbTf_pv=5J~FLQYN&D2{+2j!RCydvr0odvbgnAkP>iS0M4}({tcOj*iaGgv?B= zB)Ly%rug{yCYQoklmdh^Gc$$XQ9&y!D@)7E#%}sg5xJ|K%|x^B#5K#5@rn?=Z{0EK z6H7#FY|m)EjIxpvh>wpCHPFAfn7I*JE%ie!4_K&|Nifs5w6vh2qX)_r1OhjvK}E&H zthvpao1f>jUlRNH=@T3XQC!T#Byaxw$qCBcGzU%H5fBjY$jBo1_N%CLYpdwo++5E1wme#7{+BOb zXi(udeV@{WKI{?%&>q`hd-RAec16AKy(g&c*QWtbreC{Kw|3#qC{P%r3@`sg=@e6H zg*tg&-1r9t@J)c!4>s?8^6-E6>dn7#e*aUHd@36g4Oc2ANJ&Fu(zIR4e19Lhu<$hW z6qkdhCMFh^G!SmChYRzG$MMf{b8~$t;bBuy{ta+fq}*nhQgnEL>jA;&#l^TI#`cY# zg#|PJZOqPpNoHu_61i<_e-gd8i2gCqCn1G?2b| zS`C?dTcG%?bm6BN$mc(zpJ=2AE@ukNE$T z`1!RL$uu4FzB%aZ3X) PihU0mu=&XO9&d=U6KGHkj6uBfyA&D@nAU%!K|SwpR$h^mVwunF?qOJ=m958= zit2*)R99$PFEgXB`nFysB94eGm5^)dIsc!kt0*ebKpEKtkSAn#jpp~)!{04$wiXla z8m^Mw`nKtDdI!Uhw13rBotlC?A_gdjG8y*RYb5i2s}Gde zmzC@O9TumcqJ^HI>}!*a_bdqe{YR*$AX&;-z1>n-mh&3b4+}YoulmnF<357j^aOkf zBM)sneucUwCERfYf5yXD*mB>eOZ=wTc!D#2^Bm`{j>dEnN9>&-8Fj*m4eR~!`}OAX z%mvrcLSt%+8L>*Egq9^laceq>`+J5u7pAmAb6-hzI8c_17v`>oN`vdXp=k8^!O@FI z6p&PlK;$YWO8QW#xAG3=#{5dK^Es9fE#JdqgS1-Dbb_a2)&bdpiOHp|78kEJv-o8H zoT3vm3iWS9xbG-bn*ncdeoGJb?Idd27(ce82V;rMzm+EQ9NufbC1KjxQPss(V?Jp= z4Vq;;I$VQFI2|_ED#m>?JgFY(TEjhSPhwxtIfOU~?=y*B)70utIcUD*S-aS)U{|Yw z8;uac0Fe|E?6usIe9X_vU9*)G{=j5F<91J2WM>SccRl;$h>;Nkh^b+50X+ENkm$1#;^wqB12KCw>%xFaCb}u{Mv*)|#z4r&X zI9UdFMr@38x9D~{P%K+(HlM3wqE|Ys!nbP_!Zu$;$Dxa;ppnT}?QZHAc<;3JE7?WE z2xQ~>ahlBNJ3YJ6k&B@y-sue~O~yzFLmaMz&1)s}`H`JU<#!krI79+ z))FTsLAcHLJZrlbX5)|F0vc;CL1fPScNM?CmRXdXe6GvLLH6>z(=KYfBFESto7<`H z{WPN8fz#lcE?qMbtpiTQ)t)n1P~3{h$(gi6P#Wb!+G|2R?dvtoc0J}UY| zwLB7ZF+cace<-t=Rc>rCVmVCaN9tHZ$!IP|ox^(38j$oUFEJ4pk0^MiR!$7MoyWd| zuf=mTgC3$DzQm@1Se{4zwSpIw$twyV+`RPY@$K@<25Y08WCNU#s<2liq3AXtNC{X{EgOpe|e{vXPv=R zSK+=#{*T=?Bum(R4T0&VYgA*XNhRsTGaT0MC)&_QCV7O(iM@Lna;f!p(;P209?f~@ z8#llSoXFX!6qZZE@X^rAclgjNPCZVdQf}x8``ChisGte1-4)|?ByUQvE7mSjDsIzg zyoN=Ah}Ilpd1=Go!!nT2;F?a*=^n~2ayCxm(=HiW)P7h<1?FTd+PCkR3OsikrZL^b zxn3}~&^?N2TJdrdD8M$+6#QLVOEJB-wR&pjUT;W!F zGY&=r_|ZH0uZoR3;B!oE$dBR06YHaE$MwpYNhhD|gE_6984p}g!QSpks%)a|n-xsBX^YVb`z`l_^(|soUeq( z%JoKeW0H>D#<(YHnHZg+Zh2?S%bV;0!`tfTk7wG^*KWTKYm_M(z1uG!`s<@{X~kwj zmxW7do7;4z1*9wFq~UqFxrvv5Xlx6CfTBWscMtcM(z?^@JXk~K=WbP1WK99wj2D#P z^`E%3LN(D*ULwU|nV{kAX1VINm^zhh=q5FyjC5xV+;9f8h1}b$he?^D&p)vNpXw?~ zwDkFR5YZ8u&g~e%t1LoclHIWQEn|&hX(h#|{kYo9(=7}zHQb_T2JXYX1*3s4P@Rdz zdvETX^5ogPg$w=OLlrs&cu7oD$x7T={&bZ&FY?u15TzF+aqe+pRowX``HGYMOWBST zbhdbo^1IY=i!?!-cDm4MLZ$On7{|%qwW#)`o?LCR)WhkJWpB{ELzf{=h}iXQPwMgF zPmYs5C3z7X#Kxwa@KHI8!rX*zrI_>Fdk+`EVuUNvkIqV(l*Ew8sjxAlT+SgDlh*5(I~xUh1P+W)?(#3jI=J`|ju5yrMBvvWu>}L6!7vRJ?WJ=7wG=VMx1U{Z-H% zbB$ff&DhPgr?v8gr%Pm`r2v^lY6rTd>S4^?z4%qpAAQ-&AMCM!d+~N{!5@x-^=0e? zZ*|)4GLkaIAF-vlBQCb7$ye|o?v#1&-PD(qn3q0wkCI}nEzj#t-}U4)A!}Pci;TE` ziN_;=e5B~X+Wob9h<^R?E8%U^BPsY>;aw!n$@$&Z!)7cohH0OwFz!M#RC8tyd;j9> zPOFBRua$xWQV!3i$yT#JV{%r|NWiSB0$jTEH^->2s1#0Qc&FDf@XkKu+LWmj5| zL6KWGPb`ldS|6VRS+i%jr)LL<cM_AV3NOlV@*ve%uEX(6fYPLx(OG_`5_VO0R* z)`IRMeW}Fp0*$O_0B~p^UMFH?OILUUU8f(Gf|2JvQ-<3qTa@TmevXy&9!9SAPH3XtgMZWXhCJ}7Z2}CZ?#fW&wVt}U? z6jOOAXZbWt3zw6L{IHx#Qul@npG%nN@&SFMu)5dNrNQ=n)p9$q;Gk%renUzC<#)p9z%oe0UW#)>T(`qGqzLPq)g1WkK#gzr#-P0; z?fgpggG<2R1xjb95PgiE#a}%zr_L8U)flM&>6;sMP8Ym;4|mEqDciZ=6)-wl=V;{? zYsuMN&`*P%Bn^e$jpz^AL~orrcS%ST6}2PgGYs+y3OM&BwU#$lB9}p4GNf-O?e8ii zv)i*9-3JowQQ$p1sN()`x(t{iUf-_vCcKpEIl@qJc#Q!P@$=pP=|DS#b&*M_*XzyK zDIp<3A8T+QP0RT3V9UI~xARwZuEy#m4>KNs*n-6loFH0vflbZ7f7Zxh`bnUKM7>pB z%37A3XU=Q>;4?jpaBIwFAJNUy*p%`sG~LY;Tf-kB#?9qR815^aDJm)vlh5v;gH!>% ze7!o!>eDM~T)iNU#^fr1 zFZ%IR4x#B)RHB4?yo}Vufc9!g+OpqKvDH*cMwF+o0CTIo%6NivNi5)Trj5$JIxO^B zY-_!qdc~<{p{3_%om$H~hge?L)&q4$jn!xHL5BMUp6p95$UtT9_-aEtFR%gD;=%ox zT|1#(RNd6%>1YpipxxYDHTz+~51%^f!MVDeihFkz!1!=Vbh@;-rS9j5tJTxev^JMM zZ2Wc!Y07G8k)LdVFsSq5J#^c&H|t}#Gzms_3q(%07nYwH~6#_Csr;Z&Mj~7T;oXZbEMJcU3#y`_8#dr)-QZY5^ zDCPw|gS7?V4n0H_E+=?e&ZAzi$lV1i{g%3D1f?cc#)|=p$H<{jF zpUzQYUy1I-^(4DS86(2MDIvExeS!qfXM=S(@o*4d=`f2w+`#93?Z>@=j8UFk`LhlX zHl-vPaQ(JuRQH|zJ`;R@U@!S&5$BE(=WfjtuYb?aesrYf{f zY0A8t&YowXn@V$Z^^3=@=E&j;Q?c+(c zmNGoVI$PSl8j3tO?0kJdvc45oFv}0BnY%P|nWJ!?yu@_#FpQ1mxSY)XRiU}wSdQ?z zhh0=j1KV$ssIqy6s8Xl6Eg+XS<0|ZoGOEIZZYWbXZzjvcMVV4Fbnb}IL+4i%fCZU!JJP1+zhpWLOUT-n0d&^>bJ2HI_D`_0`?rTHH%t#D^iUphk);;)GR+A!+42mbYwHwMHgRYaeQ5c?6*(`nqOz zamBNy916fy5{zTwXXqREF(44&rnvRa@KO*+Xm4(=hHfdT`rF3?vZYHMr}KuigWJo= zo$cmn7QX5IBO!<`aCjojF?!DK0^f#Hy5ieH@=6Lt9fhd~hJSsjY%ckQunM3mp!JKX zcV`p04QdBEBlujp_Lp3L4Q_H+us=#BJZj$6-v<49EDp9izP89b;NNXLWjRS$5>`A% zt#O7w!f>%qM0d2J4qnIGo*;QeNDPV73Q~ac&Zym3Q%^PzmmlrFy(06I zQ|;kp3AJSVz}*TemYcG2QG2&S*SkdFOPN+yG;ny}`hIO+60%$J!5c$uw|uImm=%-a zWmsEZvwo>{$%JdRq|$_U+Kedft2w@Lh3W@dF|_=h9mtocViXKy4Cf;%#te8kI6nS$ z>>O91mCU3n#@UG~GOvvQuxa%14w^Ie%g6`^chBX<8su1kUBY6PF={7)M?b+Et}5On#~PI?zdF8e13~x3D5^0 zyl?ny#Xo~I`}PI`?SA`KuPrUY(7^i^^jTOgYst!ixAD}qxFY_-DP(7)wHAcYy?a5C z*;r3v@{%J%{xe4dp%H(!C9AJ@JmDtgeY%#S11EwCBx`NI{dKUr9Wyn#HKbOYmbY7A z_Zr$ZsHN}l%(wGKDWqmko&N(0hzNtzG^vI2OAQ%IYFyw?mW$nU$NpdMP>PenUJ;43YXYdo_`M6XMAGs=_CDmrBt8_SSQ7cH&QSWma|(!A64|j9Za}ZvwTxB7T^q z(iB@bsK28U;pOE8`+_1Q3^k1{83{p*2;pjqXHrsR1(4D@B7ml-v`#c7d6uMY?*I;r z$~}>;&&cVnVy)fY^`DPsQENf#@001r0EMb-fG>|0%YWGAHMim)9<&K`*NhwtOqN&AZ4#%USL;NLCH&eZLgV zd*}53gm@LzG|@clw?6Kf4gk#xKpjmLrQ}#VUk6AB*>@J@PVRGV3o}RX?{MAK` z(7YwN_W)5c7Yhdx4?l?%^3c?e>oT={t9B^x>T}hN058#qbNTo_mbjW(@>Mo_5jUIp zK-vA9>^eKA@{?~H4_S|&;LZm%#U3u@#v0@6U%IH)E+ky-nJ!fua;DvmAR!Ktm`S}0 zcKz!R=N*g)C*=is0Z9$b*)#^yox#;ghq0P;HXbUonOWMz4$k&gkbkpBnFACKKVNxd z*IsgXs5^DZ1^ai`fxH}oM+5~w@yrZh^JN)hdO4F%AieuBVTNxE7TpnN0gC$ zl>$bCzqcX{zIVb-W2Zy8?Iut@ghT;#>VTf+ZNZ@odn2-jvqd${35fA}*=;;^u&31l z`|Wwx#LNSX-O(XGoOU_J*(FT`xYV#@2nDyG;xHbNn2aDOGIHL#d-$)i2t?F!B_4k@ zjhN3aJs?_CHTgYQSVJ9@rxDiDhg7JJ1{VM>Fa6S)AQU1PK62N?uQJTAecj22X3ze!u78`i5;0-4ib60!1TuR(=VCR9iYUr0qCGX^ov z)N4#I$ch@s&*`^3NcH@$HA4#(j}JY^=EG05O>A5zAtTX>hyb>=bf6*m8#)k39)gdT zhhC^RvQ2rkf!(5oLU}h~Jrb`0573GeK`(9X{ubsz;*M<8E%&Nk^|S+#?JjX4K4iX> z@w*JKKG1l*#SO$}^jcwvZ1l#lyw%)>mKH9h*UOEgN(IxDsYGIN?v7PwUGW?<@G5w* z*TWFyS6D&1;sn)R>74Cpq?-Q7E>#QntHN8GoD;qUFY@PZ5oeK_5Z&1tpZA~g#*U_9 znkzrxq$TmTZ$7K^J{0T@nZ-kB4LfQKI^{WoPx0D=fV|87p4mdf-)fVOw|^NszBv?h zJ{+tC96u~zEQgwgfv}#XR*C_G%(t`|3db<^^7!-_OPTYejQekE$m8!5CDT_Vh&MO- zeet8gXqc92PNmT}G87Q3M8|_b)!g^)sf+6TCIme0cVv@RcWBtdiDB;v20Atu+>YC| z%y9tH4tUC{h+N5zBlOh8Nq_GV!P&5b!F1FVH(hx(K^&WscI3vkZ0Pjtviq!`yfcO7 z&$Y!CI}Dkyf2X2W56P}Hp(-sYh*Q$=A20gXD3pa>r5_R#^ZLg$gBr>7A-EZj75wc)04 zzoWnY;KobYx<8sdAZNWvP}+(-Y4IP?P`@tYfj*MC$eFjK{sHk<;sxte2UU(*yZa ze+*GWK`IUTZ!$Kz*OJ=CNxV3&P`>LW9-Lca*@g-L{>6AYeug^uzZxe*m=g5epIAz> zS%|{9tEX|lu>@ z2`U~h#;1lCanK4(1}hKZzc_1#&U}^HsX%Q4mDyqCn3A1?@V9XyINU2;$3Tpy8Avtb>>SjlRR1?&y%~ZnDEkuN`)d~ zx1IyILxg;$JU1NIAy997-)JXMVedXB{e`+Grv2wHHf{|a7#@IXY(x4qyK-bH z{+=(jnjUHV5fPVp$8v>n5G|mOC_sO%=}|!N8|yA`d(-IXx_LCptEtVqJ`mac^uUP- zpR|A7uRb|32xF1PYoSF%eu`lct~@O(177R$P*hGI7cmArnw}mH*}?MHyk2S4O-yM< zXm6&fcLDOG_4%6DR-5Q_qC5VHhwGXTu@nFKx&(DbBJMLBm{><`m#}xpr2eGW*`FEw zZ^~UDD)>p>`;GDM_Y>1oKHa^}EhF@g2xt`VjRU(WrvE>Y0RR6k)Tyob=dk0w4d({_ zE-C5C(OQJYGX{fgZ@$LI6XbRL=ZUcwSC`$osp>wNCd2w?KyLifWq5pYUZv@{lQc8^ zJt_S8)x4!<^WKQQWT$})iD2JuO$h95w{ORG5XDqf0s!enz1p6uQBWXSS2HwKK1UB& zy-2ej5d?cEK#Fskud?M_z#6P@EqcnE0Sy^a-bu5W{u?B+S)Qje)%M~SM3kobo~c+Y z;iylgS*0l!5NI$uK+H46#H`fTzku*82(v3c9G$byX71_C$G;97dav`0r=^t>Txdot zJkJfhL8mI>7Go#jFZrAHJcm*1&Fgu-umH;>1 z&wUsFu;FX{jQaqzi(j)T#I-n-Yah&+k<8RX-Z zsKL$Y#Nwpb>F!&^uZ#)i*f=mRg&whCOD7LF0Jm3ng?_Mm{Pc}H6o)VH8O2s&yMM1nqZP;VT??>{ zR?pMDsGZv+&ma5U!3J_FPur*`wx3Dxkp6%@`#)G@O0XO zxzc#iKUd9?es8hmow3ws@V~q;Ikho9Oq3CMdYlz}vYb%WLa`mZbmK?}0zrJ4>)03pgQd+%VaTty9~ZuHuDevM0OmzDt%8>T^nwct{W^ zEJZiKMa*A2n@7r@-t<4KD3wb)v@cQOdWx^ni_A6D;6?9wufqzRa+z=+^Ekt0lzvLd z`F&Le$$8FWKO>+_zY*i!`yf3jJ;#sF{uDD8jaUqn8x6y0zgtg7Sa`9CS9t}HQ@P5I zi5Gyf&t9A04^V}$;`lD}4#v;GK8_JFE4L7J#btI(-7%g1=QaHJQ@q`sW_Qhz{os5l z;~#5c{i|f($@C+_;mkgNoj?7x#8BAj??1B{5>Xa&-xzcr8YY}AnjC5B7iSkWcwO&d z3u$MILd5da%oQ9)ZvXsfE=Ss;aHl|}5JVepivg9;Q+>hq0t4u|)$UZ$iyj;HA<#7x zUZ{~Zg0|1S6Cdt(0L4m7RXBZyJ73IKNLM%f#SiZfneS)%90nSJi43ftRY!XiUcMsC zT|deV>x+gC&!gmdsC$R8!%PM(u4ZBMgIXEh)o@+qT+PyGjqcUNl;j8b+FRGUk0FFV zA?x-{LzlX0hi_q<<*mS?qnvopE&IX$b@7MZv18n zVar6Oi%;e&)Y6QONsho=az}}wT#i(vAuGsbdH&-EHBUucf9;)lrN!Y3*2K3cAO%Gw zKS;|1yXvAFHuB}pLcZ&a7wmatW9f)lzsvg4?SPj~(mf>yv>)T2{us_%!eG2wEINwf z@i!ZQIj6o!XG`ubQ9y!nm+d@#nBelQE!rm^?rf_19z;nc&Bq=MZsmk;DBPu->Y2eOUPr|IrNm0UdoI5@s z_d4%32N+vg7h1k;|9c5^d}Rl%aIikN{x>m|k59bEp4fR*q}i0$l}xg4RE^hI$zrEx zxwCzn`xuYr8b^L!Ymcs1m_aO>@S~?XpC$9+QpNKtxn$p!6NIGxz#r7V zyE}344QC1UVXzPAANgb5K|)`@M06O<70>Aw>*B+zbmRGT=JC}DS7C?FO!ZuiFnjE3R}Z<57;E>>rn|Dk!UJc|8`0~3QA@vo zql$0u-QABLe-?wWj(hs$i-f{d!`R#|;gJ0XuWlYrbdOuZcY;$W0-t@^fi*7xs?+=Q z5wCf=0sBfO0V83YZw5jqi11EFCpawR(58WZmdBu+XzB6%B_M?ohnWnwb8IPM{JRW#vCr4N8Ej4Br0R>oZg!FhI<8CEiVOg(_sc5E( z*vs`CH|&AeeR-mjs-X-A`e^@*9)tL=l@>DS$m4`g1RlQZbkS`jPj#(QyJ2K}9)lMp z5FeJ47Pk3+c$+E@J?#F@rp;0jHKQ8ha2d`ALPN9fg}hK?^4loGt)WD=Ff8;A4=1^v zJYiIjH+3vp-09iLs$o?8w}?3jjGoa-6WjCsH}h}w8^36~83h3YC3c>0DY}<9?gD~v zAYkC6f+V%+cJ=Pkr=S3>e`^6r-pL8n8m`s7w%--Tvvd_*_qa`ADZ|)JhuQs@*<*7n zLch7_NeU~t03Sm1k`EUY{f}faKu*3a`L<^Fd69h`8IrKjN9ZFFj$z{%H_sPNo}<|a zVDiDA2@qD`4Kk^5ZL^1J0RB#{x|pV9K?kO;x!eFq4HeMC;6UGd!n;?-Ol;N%i02*V zidFXt@=JSAv}#v|P}kV7&idGx1A)zA52vDYU^_Xx)EUlBm#bIaoXItz0C_)yR6Ga| zXPWPW1MwUobg(KJA!B0a*z2fN{Ig^*(+$WQP%dZ!bX|}99QT4)2Ic(^V%ZA0o5|O= z_7vPNcMFTS=-q*#h@1_8gO)8_)~jqmcyLz)2F`$-xN>qL`Ob~N5SbyPz65IqDS&_> zzSiO3(=XM+DX;xww=db}E&QLPXu;8vv?Hd~IYz>so&<(AHI`%(j&23aYd9U|N6ml- zre)O6nvrQHV^O&Oe(vEWZU8j!1pPo?T{EX+@2noQ1_LhjqxOt&G0aWL%txC{wa!-v zAfQKyDO=%6(mIb+auTNX(UI#Kk=ox31YomX2k+9#&lOQz5>tUcKSCp9PXMYtP=vi! zBdDpuoAQRF?cgsw3 zE^>XUyB99yz85#hJtc)iEKGhUdN|zQ(sOha040`oyNoop1hjsV*V~H0^K8)Tv@Rn4 zcyCba@wKx*OYC$#)cZ@qX&>Td(&A>9PFhwjO1N1OZc`%JTRAc$x|I_aV<>{OVOinB) z+GVZ4=tJ%^23ww&&!QFuDL#gDnk^J}bkANISt;5Qipc`owl~~XfL~qY?JVzG$cAmS zay=6mV#zAK2JZkp^x&v;&IUlfCQFH|xx1sMhXc4zvufF_czH`(OMRszNV!Xrh1gO5|{0lM_xikmTYEwq{m$;9@l4cN52jY zBV$EZf+JVy2Pk56rShWvsQfYGgjSBq-Fye7kak>TyLhck3rNz?+D$}*ylIQ)`wcHWYSK!mLjvri^xX(#utvuX}D6}~# z0Q#z}pTZpsC#0-SMBe1q{AGL8KV9^A;tI0-*%evG(;#5s7AWu>JV%QWkP;WtW*wb) z$PL!#=LABz=3ak`$M*C=oR;(I`vT5j!n4!mwT*zg9C^{ty%X!UV)sH83tk|aRTL59 zwSuO~q%S>@Wm7fU`ZtXYFsd0=b*oIPf)$ug1~O1^@Jt)u{;h!mBT+u8xgA=mmOO=` z%rVQlg`OrgGPHOrF-T`+E?4A@!041eBC^3sReoLc_x^NPvwmJkNmPO%wWO33ke?n2 z<t0L0h^%HI(z|~M-8kDcko@G;uwjij;otz9 z2`V$&F%bLoQa`y~e(%Ni$s4|ZAv?5xX2Ec%wmi_rkTrjP!D(4`L5R8e-M^TQ0tB$4 z(U-?T#vSDtKq<`qgft|%t1v)j#7U&^DF+_p%xEF6(1N>-i9A03&jfDTv^qTndyVVE z`|ImHYryS-19RyAjryo@nPLDUCYfO?N7v?@040kj%0H17aVeK@bA01p`Ii@xqsiVnPW)JTSxu#HXvLJRO z{2%k}G>6-vva+&#wyTJtp`mH2g$>;FW}@tIG_0%`9YNSZBI(;ZHcve#e>ULUkra@~ zq9Rs$iUpZsRT>r+^qwe+qaB5RZYTzBKtlrt{Ks-+MQvV!wZyw;y*&@_#Ci|9!q;=a?hiN!7*WjpP2b zevJ(McPd6kFwOf&|G2ogF79HhmIueIoSclJA`Emwc7J7NBoLq$SS&UY3kwSqy>sUS zzUJfak0>TCK2x-JeX<7fzB$`M0CB&38IzQRk^M1bXDk;1B$veJGd`|7@ar}D0KoyDcM@4-vI(WfBt;B$&J_Z@y28zo~y$kp@xK%R8dni)YljO{BVI3 zq@b=221cQ`w@n-7%vl&1P&70&6bdJz`!^UlIC#6VruG1lKtWLvu3Q}h0%?(Jv4%Nn zhHfUgywEHf80?*zO2N#+LdHx%OdMj^6G_FvG5o`OePaV)XT{#X&ung9T(Lw!L4g@; zV`BrbmbX;4Z)qtNTt0sM)^YO0xGsdd3c$VbdK4H+g;?Ld<>tmS z><%y2kf4rPZoWH20KxWOS4Si+E)I-y0tGQRB*fO)t=Qq_`dWzoyKwf$^v1>)05h<% zveG*+;4hWUJ;;TMidue`f{aPxKQJJp^brwMUS58Dd|cM>L`;kh7WF)fo_s@G{A}ns z8q&Jm?=J42fSp&iM>Bi-`~4CVNfCvGK7Yys1kHtcP3HTwAYfH5(}jg51ZL~}oL z6SudwD<~`bx3`P<`}%DmX;zjt+p9Fxhs5m)sfE55Yy|}nwWMUH5)1#c9pMOzc zK9VMk6B0g1N=k|@FF%ac^|ZeI%imUbI3P(G8UHrDR=^vRH96?%kufnb^9lvO|9!+u(Glm*|E<- z>{1K~3Bf`1`3iZ;1|>K^VTA)-;+yE3nOX;_&6nvTfb{-+qg!}3Bb#x-E5`3<34@dX z)lu`i+u=3hp7Hq1x*zPj+s5schEJ*3?!U{_%LF~cNsvJX0mr#hp_kV*d1-0+Pw}hO@^%z{OP}#<+vez=g@4h)0IdSj3C$eH-%(8`MKI6Wxs!B@y<{x9y#r792iAgtE_8`go z3w!Q)(Xi$13D3)S6*@48adhTOu*JC#ARibJ-Q+JwX`gN(N6? KKbLh*2~7ZW^-z%j literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-match/crbug-919955/example-2-expected.png b/tests/image_tools/fixtures/should-match/crbug-919955/example-2-expected.png new file mode 100644 index 0000000000000000000000000000000000000000..5b760ccbfe18ba4bb8164c1f9d408422a08d283e GIT binary patch literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4flRaG=Lp(a)UNz)uHehhLnC~IR zyK?Rug$kDH|IrmJin4dENLi~)@MC=`wR_4R<~?7<4UbvuV%@zy=iuZ?)4H~qlw4)C z+|_#g^~t|gizjJumK`)cBg}B=uk@(1%xw=P}yFVdQ&MBb@ E02W13(EtDd literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-match/tiny-antialiasing-sample/README.md b/tests/image_tools/fixtures/should-match/tiny-antialiasing-sample/README.md new file mode 100644 index 000000000..f65b71ed4 --- /dev/null +++ b/tests/image_tools/fixtures/should-match/tiny-antialiasing-sample/README.md @@ -0,0 +1,5 @@ +# Tiny anti-aliasing sample + +This is a 10x10 image sample with a 3 anti-aliased pixels in-between. This is actually +a cropped down snapshot of one of the [ubuntu-x86-vs-ubunu-arm samples](../ubuntu-x86-vs-ubuntu-arm/samples/stylings_stories-Stylings-stories-Texture-bar-should-use-custom-path-chrome/stylings-stories/texture/bar/should-use-custom-path-actual.png) handy for debugging +purposes. diff --git a/tests/image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-actual.png b/tests/image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-actual.png new file mode 100644 index 0000000000000000000000000000000000000000..1626202d59e0d1ae4d9e1007754543aeaaad139a GIT binary patch literal 867 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4v7|ftIx;Y9?C1WI$O_~uBzpw; zGB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkphQ4wglC$sFM}44&B4IL zD8#@FWHACELwhCz3z*Ho05k~*m=`c2WEU{OWw{nGBiJB?noFm>W?*14$_$ApiSYHY zO3u&KOH9d6O4X~#EdVKCu&J;DGILW)5)~?PbMlI9LmNm2q7CdXh;=p~!>j@_D^hbJT{3f1 z^NN8^vokg`umM|xA%k!zM5l9pPJU5vL1J>Mogq{Uk`TITussn7d(2Htk?lc}LDC6w zx0QcUW-2g9K>-J{2ty32-bNo3sz{*>i9)a_Fb?gwZ1myj!H!FE?NSL~5()8iaSV}= zJllVex50sfMf`Aon#PB&CG)u-WSX>nFx+>CpOGzM!t^sbj7wJCTPn2uYR%I359SC9 zUR<}O_g$YM+q8pB<`ZTrDk&^tlAFA|?n9~Rw-jsjXMthW?UvS{H0bH-=d#Wzp$Pz5 CB=?H| literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-expected.png b/tests/image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-expected.png new file mode 100644 index 0000000000000000000000000000000000000000..7f3d87a00b765d550710ac4004bb63d3724f84e1 GIT binary patch literal 868 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4v7|ftIx;Y9?C1WI$O_~uBzpw; zGB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkphQ4wglC$sFM}44&B4IL zD8#@FWHACELwhCz3z*Ho05k~*m=`c2WEU{OWw{nGBiJB?noFm>W?*14$_$ApiSYHY zO3u&KOH9d6O4X~#EdVKCu&J;DGILW)5)~?PbMlI9LmNm2q7CdXh;=p~!>j@_D^hbJT{3f1 z^NN8^vokg`umM|xA%k!zM5l9pPJU5vL1J>Mogq{Uk`TITussn7d(2Htk?lc}LDC6w zx0QcUW-2g9K>-J{2ty32-bNo3sz{*>i9)a_Fb?gwZ1myj!H!FE?NSL~5()KmaSV}= zO#Z>oDy|pvbP0l+XkK De?a%$ literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-match/trivial/README.md b/tests/image_tools/fixtures/should-match/trivial/README.md new file mode 100644 index 000000000..919307f0f --- /dev/null +++ b/tests/image_tools/fixtures/should-match/trivial/README.md @@ -0,0 +1,3 @@ +# Equal small images + +Simple equal images. diff --git a/tests/image_tools/fixtures/should-match/trivial/black-actual.png b/tests/image_tools/fixtures/should-match/trivial/black-actual.png new file mode 100644 index 0000000000000000000000000000000000000000..cfe88317b82d7b51feb5ed45629940d6fd640cfc GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~ZBG}+5DWk0 zl!SyI=N()RSY#<2U1%xQCc>_GI76bZO;J+OU{=lv4ighy2Aw7m#x+j$Qb27Cp00i_ I>zopr0Da^fQ2+n{ literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-match/trivial/black-expected.png b/tests/image_tools/fixtures/should-match/trivial/black-expected.png new file mode 100644 index 0000000000000000000000000000000000000000..cfe88317b82d7b51feb5ed45629940d6fd640cfc GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~ZBG}+5DWk0 zl!SyI=N()RSY#<2U1%xQCc>_GI76bZO;J+OU{=lv4ighy2Aw7m#x+j$Qb27Cp00i_ I>zopr0Da^fQ2+n{ literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-match/trivial/white-actual.png b/tests/image_tools/fixtures/should-match/trivial/white-actual.png new file mode 100644 index 0000000000000000000000000000000000000000..11bce6daa31a86e8dac1e44036b579fffd4dc69d GIT binary patch literal 114 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~9Zwg>5DWk0 zKmY&#uV>R@Iu^*R$#?dm!4eA&Nw>oVCOjuO8ZV|eyDeCl$;jYu=YRC^;;!pJeGHzi KelF{r5}E)Nu^|uu literal 0 HcmV?d00001 diff --git a/tests/image_tools/fixtures/should-match/trivial/white-expected.png b/tests/image_tools/fixtures/should-match/trivial/white-expected.png new file mode 100644 index 0000000000000000000000000000000000000000..11bce6daa31a86e8dac1e44036b579fffd4dc69d GIT binary patch literal 114 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~9Zwg>5DWk0 zKmY&#uV>R@Iu^*R$#?dm!4eA&Nw>oVCOjuO8ZV|eyDeCl$;jYu=YRC^;;!pJeGHzi KelF{r5}E)Nu^|uu literal 0 HcmV?d00001 diff --git a/tests/image_tools/unit.spec.ts b/tests/image_tools/unit.spec.ts new file mode 100644 index 000000000..f565a5d0a --- /dev/null +++ b/tests/image_tools/unit.spec.ts @@ -0,0 +1,119 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test } from '../playwright-test/stable-test-runner'; +import { ssim, FastStats } from 'playwright-core/lib/image_tools/stats'; +import { ImageChannel } from 'playwright-core/lib/image_tools/imageChannel'; +import { srgb2xyz, xyz2lab, colorDeltaE94 } from 'playwright-core/lib/image_tools/colorUtils'; +import referenceSSIM from 'ssim.js'; +import { randomPNG, assertEqual, grayChannel } from './utils'; + +test('srgb to lab conversion should work', async () => { + const srgb = [123, 81, 252]; + const [x, y, z] = srgb2xyz(srgb); + // Values obtained with http://colormine.org/convert/rgb-to-xyz + assertEqual(x, 0.28681495837305815); + assertEqual(y, 0.17124087944445404); + assertEqual(z, 0.938890585081072); + const [l, a, b] = xyz2lab([x, y, z]); + // Values obtained with http://colormine.org/convert/rgb-to-lab + assertEqual(l, 48.416007793699535); + assertEqual(a, 57.71275605467668); + assertEqual(b, -79.29993619401066); +}); + +test('colorDeltaE94 should work', async () => { + const rgb1 = [123, 81, 252]; + const rgb2 = [43, 201, 100]; + // Value obtained with http://colormine.org/delta-e-calculator/cie94 + assertEqual(colorDeltaE94(rgb1, rgb2), 71.2159); +}); + +test('fast stats and naive computation should match', async () => { + const N = 13, M = 17; + const png1 = randomPNG(N, M, 239); + const png2 = randomPNG(N, M, 261); + const [r1] = ImageChannel.intoRGB(png1.width, png1.height, png1.data); + const [r2] = ImageChannel.intoRGB(png2.width, png2.height, png2.data); + const fastStats = new FastStats(r1, r2); + + for (let x1 = 0; x1 < png1.width; ++x1) { + for (let y1 = 0; y1 < png1.height; ++y1) { + for (let x2 = x1; x2 < png1.width; ++x2) { + for (let y2 = y1; y2 < png1.height; ++y2) { + assertEqual(fastStats.meanC1(x1, y1, x2, y2), computeMean(r1, x1, y1, x2, y2)); + assertEqual(fastStats.varianceC1(x1, y1, x2, y2), computeVariance(r1, x1, y1, x2, y2)); + assertEqual(fastStats.covariance(x1, y1, x2, y2), computeCovariance(r1, r2, x1, y1, x2, y2)); + } + } + } + } +}); + +test('ssim + fastStats should match "weber" algorithm from ssim.js', async () => { + const N = 200; + const png1 = randomPNG(N, N, 239); + const png2 = randomPNG(N, N, 261); + const windowRadius = 5; + const refSSIM = referenceSSIM(png1 as any, png2 as any, { + downsample: false, + ssim: 'weber', + windowSize: windowRadius * 2 + 1, + }); + const gray1 = grayChannel(png1); + const gray2 = grayChannel(png2); + const fastStats = new FastStats(gray1, gray2); + for (let y = windowRadius; y < N - windowRadius; ++y) { + for (let x = windowRadius; x < N - windowRadius; ++x) { + const customSSIM = ssim(fastStats, x - windowRadius, y - windowRadius, x + windowRadius, y + windowRadius); + const reference = refSSIM.ssim_map.data[(y - windowRadius) * refSSIM.ssim_map.width + x - windowRadius]; + assertEqual(customSSIM, reference); + } + } +}); + +function computeMean(c: ImageChannel, x1: number, y1: number, x2: number, y2: number) { + let result = 0; + const N = (x2 - x1 + 1) * (y2 - y1 + 1); + for (let y = y1; y <= y2; ++y) { + for (let x = x1; x <= x2; ++x) + result += c.get(x, y); + } + return result / N; +} + +function computeVariance(c: ImageChannel, x1: number, y1: number, x2: number, y2: number) { + let result = 0; + const mean = computeMean(c, x1, y1, x2, y2); + const N = (x2 - x1 + 1) * (y2 - y1 + 1); + for (let y = y1; y <= y2; ++y) { + for (let x = x1; x <= x2; ++x) + result += (c.get(x, y) - mean) ** 2; + } + return result / N; +} + +function computeCovariance(c1: ImageChannel, c2: ImageChannel, x1: number, y1: number, x2: number, y2: number) { + const N = (x2 - x1 + 1) * (y2 - y1 + 1); + const mean1 = computeMean(c1, x1, y1, x2, y2); + const mean2 = computeMean(c2, x1, y1, x2, y2); + let result = 0; + for (let y = y1; y <= y2; ++y) { + for (let x = x1; x <= x2; ++x) + result += (c1.get(x, y) - mean1) * (c2.get(x, y) - mean2); + } + return result / N; +} diff --git a/tests/image_tools/utils.ts b/tests/image_tools/utils.ts new file mode 100644 index 000000000..b4c61d538 --- /dev/null +++ b/tests/image_tools/utils.ts @@ -0,0 +1,61 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PNG } from 'playwright-core/lib/utilsBundle'; +import { ImageChannel } from 'playwright-core/lib/image_tools/imageChannel'; + +// mulberry32 +export function createRandom(seed) { + return function() { + let t = seed += 0x6D2B79F5; + t = Math.imul(t ^ t >>> 15, t | 1); + t ^= t + Math.imul(t ^ t >>> 7, t | 61); + return ((t ^ t >>> 14) >>> 0) / 4294967296; + }; +} + +export function randomPNG(width, height, seed) { + const random = createRandom(seed); + const png = new PNG({ width, height }); + for (let i = 0; i < height; ++i) { + for (let j = 0; j < width; ++j) { + for (let k = 0; k < 4; ++k) + png.data[(i * width + j) * 4 + k] = (random() * 255) | 0; + } + } + return png; +} + +export function assertEqual(value1, value2) { + if (Math.abs(value1 - value2) >= 1e-3) + throw new Error(`ERROR: ${value1} is not equal to ${value2}`); +} + +// NOTE: this is exact formula from SSIM.js and it DOES NOT include alpha. +// We use it to better compare with original SSIM implementation. +export function grayChannel(image: PNG) { + const width = image.width; + const height = image.height; + const gray = new Uint8Array(image.width * image.height); + for (let y = 0; y < image.height; ++y) { + for (let x = 0; x < image.width; ++x) { + const index = y * image.width + x; + const offset = index * 4; + gray[index] = (77 * image.data[offset] + 150 * image.data[offset + 1] + 29 * image.data[offset + 2] + 128) >> 8; + } + } + return new ImageChannel(width, height, gray); +} diff --git a/tests/playwright-test/golden.spec.ts b/tests/playwright-test/golden.spec.ts index 8cb05152f..102fa5553 100644 --- a/tests/playwright-test/golden.spec.ts +++ b/tests/playwright-test/golden.spec.ts @@ -620,6 +620,80 @@ test('should respect project threshold', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); }); +test('should respect comparator name', async ({ runInlineTest }) => { + const expected = fs.readFileSync(path.join(__dirname, '../image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-expected.png')); + const actual = fs.readFileSync(path.join(__dirname, '../image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-actual.png')); + const result = await runInlineTest({ + ...files, + 'a.spec.js-snapshots/snapshot.png': expected, + 'a.spec.js': ` + const { test } = require('./helper'); + test('should pass', ({}) => { + expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', { + threshold: 0, + comparator: 'ssim-cie94', + }); + }); + test('should fail', ({}) => { + expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', { + threshold: 0, + comparator: 'pixelmatch', + }); + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.report.suites[0].specs[0].title).toBe('should pass'); + expect(result.report.suites[0].specs[0].ok).toBe(true); + expect(result.report.suites[0].specs[1].title).toBe('should fail'); + expect(result.report.suites[0].specs[1].ok).toBe(false); +}); + +test('should respect comparator in config', async ({ runInlineTest }) => { + const expected = fs.readFileSync(path.join(__dirname, '../image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-expected.png')); + const actual = fs.readFileSync(path.join(__dirname, '../image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-actual.png')); + const result = await runInlineTest({ + ...files, + 'playwright.config.ts': ` + module.exports = { + snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}', + projects: [ + { + name: 'should-pass', + expect: { + toMatchSnapshot: { + comparator: 'ssim-cie94', + } + }, + }, + { + name: 'should-fail', + expect: { + toMatchSnapshot: { + comparator: 'pixelmatch', + } + }, + }, + ], + }; + `, + '__screenshots__/a.spec.js/snapshot.png': expected, + 'a.spec.js': ` + const { test } = require('./helper'); + test('test', ({}) => { + expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', { + threshold: 0, + }); + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.report.suites[0].specs[0].tests[0].projectName).toBe('should-pass'); + expect(result.report.suites[0].specs[0].tests[0].status).toBe('expected'); + expect(result.report.suites[0].specs[0].tests[1].projectName).toBe('should-fail'); + expect(result.report.suites[0].specs[0].tests[1].status).toBe('unexpected'); +}); + test('should sanitize snapshot name when passed as string', async ({ runInlineTest }) => { const result = await runInlineTest({ ...files, diff --git a/tests/playwright-test/playwright.config.ts b/tests/playwright-test/playwright.config.ts index 518d56907..64570a8f1 100644 --- a/tests/playwright-test/playwright.config.ts +++ b/tests/playwright-test/playwright.config.ts @@ -22,15 +22,20 @@ import * as path from 'path'; const outputDir = path.join(__dirname, '..', '..', 'test-results'); const config: Config = { - testDir: __dirname, - testIgnore: ['assets/**', 'stable-test-runner/**'], timeout: 30000, forbidOnly: !!process.env.CI, workers: process.env.CI ? 2 : undefined, preserveOutput: process.env.CI ? 'failures-only' : 'always', projects: [ { - name: 'playwright-test' + name: 'playwright-test', + testDir: __dirname, + testIgnore: ['assets/**', 'stable-test-runner/**'], + }, + { + name: 'image_tools', + testDir: path.join(__dirname, '../image_tools'), + testIgnore: [path.join(__dirname, '../fixtures/**')], }, ], reporter: process.env.CI ? [ diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts index 0436b49b1..dd121e830 100644 --- a/tests/playwright-test/to-have-screenshot.spec.ts +++ b/tests/playwright-test/to-have-screenshot.spec.ts @@ -1022,6 +1022,78 @@ test('should update expectations with retries', async ({ runInlineTest }, testIn expect(comparePNGs(data, whiteImage)).toBe(null); }); +test('should respect comparator name', async ({ runInlineTest }) => { + const expected = fs.readFileSync(path.join(__dirname, '../image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-expected.png')); + const actualURL = pathToFileURL(path.join(__dirname, '../image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-actual.png')); + const result = await runInlineTest({ + ...playwrightConfig({ + snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}', + }), + '__screenshots__/a.spec.js/snapshot.png': expected, + 'a.spec.js': ` + pwt.test('should pass', async ({ page }) => { + await page.goto('${actualURL}'); + await expect(page.locator('img')).toHaveScreenshot('snapshot.png', { + threshold: 0, + comparator: 'ssim-cie94', + }); + }); + pwt.test('should fail', async ({ page }) => { + await page.goto('${actualURL}'); + await expect(page.locator('img')).toHaveScreenshot('snapshot.png', { + threshold: 0, + comparator: 'pixelmatch', + }); + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.report.suites[0].specs[0].title).toBe('should pass'); + expect(result.report.suites[0].specs[0].ok).toBe(true); + expect(result.report.suites[0].specs[1].title).toBe('should fail'); + expect(result.report.suites[0].specs[1].ok).toBe(false); +}); + +test('should respect comparator in config', async ({ runInlineTest }) => { + const expected = fs.readFileSync(path.join(__dirname, '../image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-expected.png')); + const actualURL = pathToFileURL(path.join(__dirname, '../image_tools/fixtures/should-match/tiny-antialiasing-sample/tiny-actual.png')); + const result = await runInlineTest({ + ...playwrightConfig({ + snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}', + projects: [ + { + name: 'should-pass', + expect: { + toHaveScreenshot: { + comparator: 'ssim-cie94', + } + }, + }, + { + name: 'should-fail', + expect: { + toHaveScreenshot: { + comparator: 'pixelmatch', + } + }, + }, + ], + }), + '__screenshots__/a.spec.js/snapshot.png': expected, + 'a.spec.js': ` + pwt.test('test', async ({ page }) => { + await page.goto('${actualURL}'); + await expect(page.locator('img')).toHaveScreenshot('snapshot.png', { threshold: 0, }); + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.report.suites[0].specs[0].tests[0].projectName).toBe('should-pass'); + expect(result.report.suites[0].specs[0].tests[0].status).toBe('expected'); + expect(result.report.suites[0].specs[0].tests[1].projectName).toBe('should-fail'); + expect(result.report.suites[0].specs[0].tests[1].status).toBe('unexpected'); +}); + function playwrightConfig(obj: any) { return { 'playwright.config.js': `