From 65438286ea0ef49ee80cdf49dd3692707d2c7bd1 Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Tue, 2 Jan 2024 11:35:49 +0000 Subject: [PATCH] Split OCR engine into a separate project The ocrs OCR engine has been moved to https://github.com/robertknight/ocrs. This allows the project README, issues, build tools, tags etc. to be more focused in each repo. --- Cargo.lock | 256 ----- Cargo.toml | 5 - Makefile | 6 - js-examples/ocr-node/.gitignore | 1 - js-examples/ocr-node/index.js | 121 --- js-examples/ocr-node/package-lock.json | 552 ----------- js-examples/ocr-node/package.json | 17 - ocrs-cli/CHANGELOG.md | 4 - ocrs-cli/Cargo.toml | 25 - ocrs-cli/README.md | 41 - ocrs-cli/src/main.rs | 304 ------ ocrs-cli/src/models.rs | 92 -- ocrs-cli/src/output.rs | 270 ------ ocrs-cli/test-data/format-json-expected.json | 69 -- ocrs-extension/.gitignore | 3 - ocrs-extension/Makefile | 12 - ocrs-extension/README.md | 50 - ocrs-extension/error.html | 13 - ocrs-extension/images/eye-32.png | Bin 618 -> 0 bytes ocrs-extension/manifest.json | 24 - ocrs-extension/package-lock.json | 71 -- ocrs-extension/package.json | 27 - ocrs-extension/screenshot.html | 18 - ocrs-extension/src/background.ts | 466 --------- ocrs-extension/src/content.ts | 602 ------------ ocrs-extension/src/error.ts | 7 - ocrs-extension/src/screenshot.ts | 49 - ocrs-extension/src/types.ts | 19 - ocrs-extension/tsconfig.json | 20 - ocrs/CHANGELOG.md | 26 - ocrs/Cargo.toml | 26 - ocrs/README.md | 58 -- ocrs/examples/download-models.sh | 4 - ocrs/examples/hello_ocr.rs | 82 -- ocrs/examples/rust-book.jpg | Bin 244386 -> 0 bytes ocrs/src/lib.rs | 954 ------------------- ocrs/src/log.rs | 26 - ocrs/src/page_layout.rs | 771 --------------- ocrs/src/text_items.rs | 187 ---- ocrs/src/wasm_api.rs | 375 -------- 40 files changed, 5653 deletions(-) delete mode 100644 js-examples/ocr-node/.gitignore delete mode 100644 js-examples/ocr-node/index.js delete mode 100644 js-examples/ocr-node/package-lock.json delete mode 100644 js-examples/ocr-node/package.json delete mode 100644 ocrs-cli/CHANGELOG.md delete mode 100644 ocrs-cli/Cargo.toml delete mode 100644 ocrs-cli/README.md delete mode 100644 ocrs-cli/src/main.rs delete mode 100644 ocrs-cli/src/models.rs delete mode 100644 ocrs-cli/src/output.rs delete mode 100644 ocrs-cli/test-data/format-json-expected.json delete mode 100644 ocrs-extension/.gitignore delete mode 100644 ocrs-extension/Makefile delete mode 100644 ocrs-extension/README.md delete mode 100644 ocrs-extension/error.html delete mode 100644 ocrs-extension/images/eye-32.png delete mode 100644 ocrs-extension/manifest.json delete mode 100644 ocrs-extension/package-lock.json delete mode 100644 ocrs-extension/package.json delete mode 100644 ocrs-extension/screenshot.html delete mode 100644 ocrs-extension/src/background.ts delete mode 100644 ocrs-extension/src/content.ts delete mode 100644 ocrs-extension/src/error.ts delete mode 100644 ocrs-extension/src/screenshot.ts delete mode 100644 ocrs-extension/src/types.ts delete mode 100644 ocrs-extension/tsconfig.json delete mode 100644 ocrs/CHANGELOG.md delete mode 100644 ocrs/Cargo.toml delete mode 100644 ocrs/README.md delete mode 100755 ocrs/examples/download-models.sh delete mode 100644 ocrs/examples/hello_ocr.rs delete mode 100644 ocrs/examples/rust-book.jpg delete mode 100644 ocrs/src/lib.rs delete mode 100644 ocrs/src/log.rs delete mode 100644 ocrs/src/page_layout.rs delete mode 100644 ocrs/src/text_items.rs delete mode 100644 ocrs/src/wasm_api.rs diff --git a/Cargo.lock b/Cargo.lock index 0fca419b..b115702c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,12 +14,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "base64" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" - [[package]] name = "bitflags" version = "1.3.2" @@ -44,15 +38,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -151,42 +136,12 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "getrandom" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "hound" version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "image" version = "0.24.7" @@ -232,12 +187,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baff4b617f7df3d896f97fe922b64817f6cd9a756bb81d40f8883f2f66dcb401" -[[package]] -name = "libc" -version = "0.2.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" - [[package]] name = "libm" version = "0.2.8" @@ -299,48 +248,12 @@ dependencies = [ "autocfg", ] -[[package]] -name = "ocrs" -version = "0.2.1" -dependencies = [ - "fastrand", - "lexopt", - "rayon", - "rten", - "rten-imageio", - "rten-imageproc", - "rten-tensor", - "wasm-bindgen", -] - -[[package]] -name = "ocrs-cli" -version = "0.2.1" -dependencies = [ - "image", - "lexopt", - "ocrs", - "png", - "rten", - "rten-imageproc", - "rten-tensor", - "serde_json", - "ureq", - "url", -] - [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - [[package]] name = "png" version = "0.17.10" @@ -392,20 +305,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "ring" -version = "0.17.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684d5e6e18f669ccebf64a92236bb7db9a34f07be010e3627368182027180866" -dependencies = [ - "cc", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys", -] - [[package]] name = "rten" version = "0.1.1" @@ -497,28 +396,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustls" -version = "0.21.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" -dependencies = [ - "log", - "ring", - "rustls-webpki", - "sct", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "ryu" version = "1.0.15" @@ -531,16 +408,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "semver" version = "1.0.20" @@ -590,12 +457,6 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "syn" version = "2.0.39" @@ -622,12 +483,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -649,45 +504,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" -dependencies = [ - "base64", - "flate2", - "log", - "once_cell", - "rustls", - "rustls-webpki", - "url", - "webpki-roots", -] - -[[package]] -name = "url" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - [[package]] name = "wasm-bindgen" version = "0.2.89" @@ -741,75 +557,3 @@ name = "wasm-bindgen-shared" version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" - -[[package]] -name = "webpki-roots" -version = "0.25.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml index eba711c2..d7e3680f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,5 @@ [workspace] members = [ - # RTen inference engine ".", "rten-cli", "rten-examples", @@ -8,10 +7,6 @@ members = [ "rten-imageproc", "rten-tensor", "rten-text", - - # Libs and applications using RTen - "ocrs", - "ocrs-cli", ] default-members = [ ".", diff --git a/Makefile b/Makefile index ea9b4a08..38223a5f 100644 --- a/Makefile +++ b/Makefile @@ -36,12 +36,6 @@ wasm: wasm-bindgen target/wasm32-unknown-unknown/release/rten.wasm --out-dir dist/ --target web --weak-refs tools/optimize-wasm.sh dist/rten_bg.wasm -.PHONY: ocrs-wasm -ocrs-wasm: - RUSTFLAGS="-C target-feature=+simd128" cargo build --release --target wasm32-unknown-unknown --package ocrs - wasm-bindgen target/wasm32-unknown-unknown/release/ocrs.wasm --out-dir dist/ --target web --reference-types --weak-refs - tools/optimize-wasm.sh dist/ocrs_bg.wasm - .PHONY: wasm-nosimd wasm-nosimd: cargo build --release --target wasm32-unknown-unknown diff --git a/js-examples/ocr-node/.gitignore b/js-examples/ocr-node/.gitignore deleted file mode 100644 index c2658d7d..00000000 --- a/js-examples/ocr-node/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ diff --git a/js-examples/ocr-node/index.js b/js-examples/ocr-node/index.js deleted file mode 100644 index 9336ffd8..00000000 --- a/js-examples/ocr-node/index.js +++ /dev/null @@ -1,121 +0,0 @@ -import { readFile } from "fs/promises"; - -import { program } from "commander"; -import sharp from "sharp"; - -import { - OcrEngine, - OcrEngineInit, - default as initOcrLib, -} from "../../dist/ocrs.js"; - -/** - * Load a JPEG or PNG image from `path` and return the RGB image data as an - * `ImageData`-like object. - */ -async function loadImage(path) { - const image = await sharp(path); - const { width, height } = await image.metadata(); - const data = await image.raw().toBuffer(); - return { - data: new Uint8Array(data), - width, - height, - }; -} - -/** - * Detect text in an image and return the result as a JSON-serialiable object. - * - * @param {OcrEngine} ocrEngine - * @param {OcrInput} ocrInput - */ -function detectText(ocrEngine, ocrInput) { - const detectedLines = ocrEngine.detectText(ocrInput); - const lines = detectedLines.map((line) => { - const words = line.words().map((rect) => { - return { - rect: Array.from(rect.boundingRect()), - }; - }); - - return { - words, - }; - }); - return { - lines, - }; -} - -/** - * Detect and recognize text in an image and return the result as a - * JSON-serialiable object. - * - * @param {OcrEngine} ocrEngine - * @param {OcrInput} ocrInput - */ -function detectAndRecognizeText(ocrEngine, ocrInput) { - const textLines = ocrEngine.getTextLines(ocrInput); - const lines = textLines.map((line) => { - const words = line.words().map((word) => { - return { - text: word.text(), - rect: Array.from(word.rotatedRect().boundingRect()), - }; - }); - - return { - text: line.text(), - words, - }; - }); - return { - lines, - }; -} - -program - .name("ocr") - .argument("", "Text detection model path") - .argument("", "Text recognition model path") - .argument("", "Input image path") - .option("-d, --detect-only", "Detect text, but don't recognize it") - .option("-j, --json", "Output JSON") - .action( - async (detectionModelPath, recognitionModelPath, imagePath, options) => { - // Concurrently load the OCR library, text detection and recognition models, - // and input image. - const [_, detectionModel, recognitionModel, image] = await Promise.all([ - readFile("dist/ocrs_bg.wasm").then(initOcrLib), - readFile(detectionModelPath).then((data) => new Uint8Array(data)), - readFile(recognitionModelPath).then((data) => new Uint8Array(data)), - loadImage(imagePath), - ]); - - const ocrInit = new OcrEngineInit(); - ocrInit.setDetectionModel(detectionModel); - - // TODO - Don't require the recognition model when doing only detection. - ocrInit.setRecognitionModel(recognitionModel); - - const ocrEngine = new OcrEngine(ocrInit); - const ocrInput = ocrEngine.loadImage( - image.width, - image.height, - image.data - ); - - if (options.detectOnly) { - const json = detectText(ocrEngine, ocrInput); - console.log(JSON.stringify(json, null, 2)); - } else if (options.json) { - const json = detectAndRecognizeText(ocrEngine, ocrInput); - console.log(JSON.stringify(json, null, 2)); - } else { - const text = ocrEngine.getText(ocrInput); - console.log(text); - } - } - ) - .parse(); diff --git a/js-examples/ocr-node/package-lock.json b/js-examples/ocr-node/package-lock.json deleted file mode 100644 index 03a96f7c..00000000 --- a/js-examples/ocr-node/package-lock.json +++ /dev/null @@ -1,552 +0,0 @@ -{ - "name": "ocr-node", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "ocr-node", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "commander": "^11.0.0", - "sharp": "^0.32.1" - } - }, - "node_modules/b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/commander": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", - "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", - "engines": { - "node": ">=16" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" - }, - "node_modules/node-abi": { - "version": "3.45.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", - "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp": { - "version": "0.32.6", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", - "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", - "hasInstallScript": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.2", - "node-addon-api": "^6.1.0", - "prebuild-install": "^7.1.1", - "semver": "^7.5.4", - "simple-get": "^4.0.1", - "tar-fs": "^3.0.4", - "tunnel-agent": "^0.6.0" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/sharp/node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", - "dependencies": { - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - } - }, - "node_modules/sharp/node_modules/tar-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/streamx": { - "version": "2.15.6", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.6.tgz", - "integrity": "sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==", - "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } -} diff --git a/js-examples/ocr-node/package.json b/js-examples/ocr-node/package.json deleted file mode 100644 index 8894e22c..00000000 --- a/js-examples/ocr-node/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "ocr-node", - "version": "1.0.0", - "description": "", - "main": "index.js", - "type": "module", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "commander": "^11.0.0", - "sharp": "^0.32.1" - } -} diff --git a/ocrs-cli/CHANGELOG.md b/ocrs-cli/CHANGELOG.md deleted file mode 100644 index d73e0743..00000000 --- a/ocrs-cli/CHANGELOG.md +++ /dev/null @@ -1,4 +0,0 @@ -# Changelog - -Changes for this crate are tracked in the `CHANGELOG.md` file for the sibling -ocrs crate. diff --git a/ocrs-cli/Cargo.toml b/ocrs-cli/Cargo.toml deleted file mode 100644 index 888acc5e..00000000 --- a/ocrs-cli/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "ocrs-cli" -version = "0.2.1" -edition = "2021" -authors = ["Robert Knight"] -description = "OCR CLI tool for extracting text from images" -license = "MIT OR Apache-2.0" -homepage = "https://github.com/robertknight/rten" -repository = "https://github.com/robertknight/rten" - -[dependencies] -image = { version = "0.24.6", default-features = false, features = ["png", "jpeg", "jpeg_rayon", "webp"] } -png = "0.17.6" -serde_json = "1.0.91" -rten = { path = "../", version = "0.1.0" } -rten-imageproc = { path = "../rten-imageproc", version = "0.1.0" } -rten-tensor = { path = "../rten-tensor", version = "0.1.0" } -ocrs = { path = "../ocrs", version = "0.2.0" } -lexopt = "0.3.0" -ureq = "2.7.1" -url = "2.4.0" - -[[bin]] -name = "ocrs" -path = "src/main.rs" diff --git a/ocrs-cli/README.md b/ocrs-cli/README.md deleted file mode 100644 index 7e8903db..00000000 --- a/ocrs-cli/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# ocrs-cli - -CLI tool for extracting text from images using the -[**ocrs**](https://github.com/robertknight/rten/tree/main/ocrs) OCR engine. - -## Installation - -These steps assume you have [Rust and cargo](https://www.rust-lang.org/tools/install) installed. - -```sh -cargo install ocrs -``` - -## Usage - -Extract text from an image and print it to standard output: - -```sh -ocrs image.jpeg -``` - -The first time that you run the tool it will download default models for text -detection and recognition. You can override this by using the `--detect-model -` and `--rec-model ` flags to set the path to the models. - -### Layout export - -By default ocrs outputs just the extracted text. Specifying the `--export-boxes -path.json` flag will cause text layout information to be produced in JSON -format. - -### Debug output - -If the `--debug` flag is enabled, ocrs will create an annotated image in the -current directory showing where words and lines were detected, as well as -logging performance information. - -## Language support - -The current version of this tool was trained primarily on English text. -Support for more languages is planned for the future. diff --git a/ocrs-cli/src/main.rs b/ocrs-cli/src/main.rs deleted file mode 100644 index 4f56e4c2..00000000 --- a/ocrs-cli/src/main.rs +++ /dev/null @@ -1,304 +0,0 @@ -use std::collections::VecDeque; -use std::error::Error; -use std::fmt; -use std::fs; -use std::io::BufWriter; - -use ocrs::{DecodeMethod, OcrEngine, OcrEngineParams}; -use rten_tensor::prelude::*; -use rten_tensor::{NdTensor, NdTensorView}; - -mod models; -use models::{load_model, ModelSource}; -mod output; -use output::{ - format_json_output, format_text_output, generate_annotated_png, FormatJsonArgs, - GeneratePngArgs, OutputFormat, -}; - -/// Read an image from `path` into a CHW tensor. -fn read_image(path: &str) -> Result, Box> { - let input_img = image::open(path)?; - let input_img = input_img.into_rgb8(); - - let (width, height) = input_img.dimensions(); - - let in_chans = 3; - let mut float_img = NdTensor::zeros([in_chans, height as usize, width as usize]); - for c in 0..in_chans { - let mut chan_img = float_img.slice_mut([c]); - for y in 0..height { - for x in 0..width { - chan_img[[y as usize, x as usize]] = input_img.get_pixel(x, y)[c] as f32 / 255.0 - } - } - } - Ok(float_img) -} - -/// Write a CHW image to a PNG file in `path`. -fn write_image(path: &str, img: NdTensorView) -> Result<(), Box> { - let img_width = img.size(2); - let img_height = img.size(1); - let color_type = match img.size(0) { - 1 => png::ColorType::Grayscale, - 3 => png::ColorType::Rgb, - 4 => png::ColorType::Rgba, - _ => return Err("Unsupported channel count".into()), - }; - - let hwc_img = img.permuted([1, 2, 0]); // CHW => HWC - - let out_img = image_from_tensor(hwc_img); - let file = fs::File::create(path)?; - let writer = BufWriter::new(file); - let mut encoder = png::Encoder::new(writer, img_width as u32, img_height as u32); - encoder.set_color(color_type); - let mut writer = encoder.write_header()?; - writer.write_image_data(&out_img)?; - - Ok(()) -} - -/// Convert an CHW float tensor with values in the range [0, 1] to `Vec` -/// with values scaled to [0, 255]. -fn image_from_tensor(tensor: NdTensorView) -> Vec { - tensor - .iter() - .map(|x| (x.clamp(0., 1.) * 255.0) as u8) - .collect() -} - -struct Args { - /// Path to a text detection model. - detection_model: Option, - - /// Path to a text recognition model. - recognition_model: Option, - - /// Path to image to process. - image: String, - - /// Enable debug output. - debug: bool, - - output_format: OutputFormat, - - /// Output file path. Defaults to stdout. - output_path: Option, - - /// Use beam search for sequence decoding. - beam_search: bool, -} - -fn parse_args() -> Result { - use lexopt::prelude::*; - - let mut values = VecDeque::new(); - let mut beam_search = false; - let mut debug = false; - let mut detection_model = None; - let mut output_format = OutputFormat::Text; - let mut output_path = None; - let mut recognition_model = None; - - let mut parser = lexopt::Parser::from_env(); - while let Some(arg) = parser.next()? { - match arg { - Value(val) => values.push_back(val.string()?), - Long("beam") => { - beam_search = true; - } - Long("debug") => { - debug = true; - } - Long("detect-model") => { - detection_model = Some(parser.value()?.string()?); - } - Short('j') | Long("json") => { - output_format = OutputFormat::Json; - } - Short('o') | Long("output") => { - output_path = Some(parser.value()?.string()?); - } - Short('p') | Long("png") => { - output_format = OutputFormat::Png; - } - Long("rec-model") => { - recognition_model = Some(parser.value()?.string()?); - } - Long("help") => { - println!( - "Extract text from an image. - -Usage: {bin_name} [OPTIONS] - -Options: - - --debug - - Enable debug output - - --detect-model - - Use a custom text detection model - - -j, --json - - Output text and structure in JSON format - - -o, --output - - Output file path (defaults to stdout) - - -p, --png - - Output annotated copy of input image in PNG format - - --rec-model - - Use a custom text recognition model - - --version - - Display version info - -Advanced options: - - --beam - - Use beam search for decoding. -", - bin_name = parser.bin_name().unwrap_or("ocrs") - ); - std::process::exit(0); - } - Long("version") => { - println!("ocrs {}", env!("CARGO_PKG_VERSION")); - std::process::exit(0); - } - _ => return Err(arg.unexpected()), - } - } - - Ok(Args { - beam_search, - debug, - detection_model, - output_format, - output_path, - image: values.pop_front().ok_or("missing `` arg")?, - recognition_model, - }) -} - -/// Adds context to an error reading or parsing a file. -trait FileErrorContext { - /// If `self` represents a failed operation to read a file, convert the - /// error to a message of the form "{context} from {path}: {original_error}". - fn file_error_context(self, context: &str, path: P) -> Result; -} - -impl FileErrorContext for Result { - fn file_error_context(self, context: &str, path: P) -> Result { - self.map_err(|err| format!("{} from \"{}\": {}", context, path, err)) - } -} - -/// Default text detection model. -const DETECTION_MODEL: &str = "https://ocrs-models.s3-accelerate.amazonaws.com/text-detection.rten"; - -/// Default text recognition model. -const RECOGNITION_MODEL: &str = - "https://ocrs-models.s3-accelerate.amazonaws.com/text-recognition.rten"; - -fn main() -> Result<(), Box> { - let args = parse_args()?; - - // Fetch and load ML models. - let detection_model_src = args - .detection_model - .as_ref() - .map(|path| ModelSource::Path(path)) - .unwrap_or(ModelSource::Url(DETECTION_MODEL)); - let detection_model = load_model(detection_model_src) - .file_error_context("Failed to load text detection model", detection_model_src)?; - - let recognition_model_src = args - .recognition_model - .as_ref() - .map(|path| ModelSource::Path(path)) - .unwrap_or(ModelSource::Url(RECOGNITION_MODEL)); - let recognition_model = load_model(recognition_model_src).file_error_context( - "Failed to load text recognition model", - recognition_model_src, - )?; - - // Read image into CHW tensor. - let color_img = - read_image(&args.image).file_error_context("Failed to read image", &args.image)?; - - let engine = OcrEngine::new(OcrEngineParams { - detection_model: Some(detection_model), - recognition_model: Some(recognition_model), - debug: args.debug, - decode_method: if args.beam_search { - DecodeMethod::BeamSearch { width: 100 } - } else { - DecodeMethod::Greedy - }, - })?; - - let ocr_input = engine.prepare_input(color_img.view())?; - let word_rects = engine.detect_words(&ocr_input)?; - let line_rects = engine.find_text_lines(&ocr_input, &word_rects); - let line_texts = engine.recognize_text(&ocr_input, &line_rects)?; - - let write_output_str = |content: String| -> Result<(), Box> { - if let Some(output_path) = &args.output_path { - std::fs::write(output_path, content.into_bytes())?; - } else { - println!("{}", content); - } - Ok(()) - }; - - match args.output_format { - OutputFormat::Text => { - let content = format_text_output(&line_texts); - write_output_str(content)?; - } - OutputFormat::Json => { - let content = format_json_output(FormatJsonArgs { - input_path: &args.image, - input_hw: color_img.shape()[1..].try_into()?, - text_lines: &line_texts, - }); - write_output_str(content)?; - } - OutputFormat::Png => { - let png_args = GeneratePngArgs { - img: color_img.view(), - line_rects: &line_rects, - text_lines: &line_texts, - }; - let annotated_img = generate_annotated_png(png_args); - let Some(output_path) = args.output_path else { - return Err("Output path must be specified when generating annotated PNG".into()); - }; - write_image(&output_path, annotated_img.view())?; - } - } - - if args.debug { - println!( - "Found {} words, {} lines in image of size {}x{}", - word_rects.len(), - line_rects.len(), - color_img.size(2), - color_img.size(1), - ); - } - - Ok(()) -} diff --git a/ocrs-cli/src/models.rs b/ocrs-cli/src/models.rs deleted file mode 100644 index dcdaadc9..00000000 --- a/ocrs-cli/src/models.rs +++ /dev/null @@ -1,92 +0,0 @@ -use std::error::Error; -use std::fmt; -use std::fs; -use std::path::{Path, PathBuf}; - -use rten::Model; -use url::Url; - -/// Return the path to the directory in which cached models etc. should be -/// saved. -fn cache_dir() -> Result> { - let mut cache_dir: PathBuf = std::env::var("HOME").map(|dir| dir.into())?; - cache_dir.push(".cache"); - cache_dir.push("ocrs"); - - fs::create_dir_all(&cache_dir)?; - - Ok(cache_dir) -} - -/// Extract the last path segment from a URL. -/// -/// eg. "https://models.com/text-detection.rten" => "text-detection.rten". -#[allow(rustdoc::bare_urls)] -fn filename_from_url(url: &str) -> Option { - let parsed = Url::parse(url).ok()?; - let path = Path::new(parsed.path()); - path.file_name() - .and_then(|f| f.to_str()) - .map(|s| s.to_string()) -} - -/// Download a file from `url` to a local cache, if not already fetched, and -/// return the path to the local file. -fn download_file(url: &str, filename: Option<&str>) -> Result> { - let cache_dir = cache_dir()?; - let filename = match filename { - Some(fname) => fname.to_string(), - None => filename_from_url(url).ok_or("Could not get destination filename")?, - }; - let file_path = cache_dir.join(filename); - if file_path.exists() { - return Ok(file_path); - } - - eprintln!("Downloading {}...", url); - - let mut reader = ureq::get(url).call()?.into_reader(); - let mut body = Vec::new(); - reader.read_to_end(&mut body)?; - - fs::write(&file_path, &body)?; - - Ok(file_path) -} - -/// Location that a model can be loaded from. -#[derive(Clone, Copy)] -pub enum ModelSource<'a> { - /// Load model from an HTTP(S) URL. - Url(&'a str), - - /// Load model from a local file path. - Path(&'a str), -} - -impl<'a> fmt::Display for ModelSource<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - ModelSource::Url(url) => url, - ModelSource::Path(path) => path, - } - ) - } -} - -/// Load a model from a given source. -/// -/// If the source is a URL, the model will be downloaded and cached locally if -/// needed. -pub fn load_model(source: ModelSource) -> Result> { - let model_path = match source { - ModelSource::Url(url) => download_file(url, None)?, - ModelSource::Path(path) => path.into(), - }; - let model_bytes = fs::read(model_path)?; - let model = Model::load(&model_bytes)?; - Ok(model) -} diff --git a/ocrs-cli/src/output.rs b/ocrs-cli/src/output.rs deleted file mode 100644 index c702c921..00000000 --- a/ocrs-cli/src/output.rs +++ /dev/null @@ -1,270 +0,0 @@ -use rten_imageproc::{min_area_rect, Painter, Point, PointF, Rgb, RotatedRect}; -use rten_tensor::{NdTensor, NdTensorView}; -use serde_json::json; - -use ocrs::{TextItem, TextLine}; - -pub enum OutputFormat { - /// Output a PNG image containing a copy of the input image annotated with - /// text bounding boxes. - Png, - - /// Output extracted plain text in reading order. - Text, - - /// Output text and layout information in JSON format. - Json, -} - -/// Return the coordinates of vertices of `rr` as an array of `[x, y]` points. -/// -/// This matches the format of the "vertices" data in the HierText dataset. -/// See [RotatedRect::corners] for details of the vertex order. -fn rounded_vertex_coords(rr: &RotatedRect) -> [[i32; 2]; 4] { - rr.corners() - .map(|point| [point.x.round() as i32, point.y.round() as i32]) -} - -/// Format extracted text and hierarchical layout information as JSON. -/// -/// The JSON format roughly follows the structure of the ground truth data in -/// the [HierText](https://github.com/google-research-datasets/hiertext) -/// dataset, on which ocrs's models were trained. -fn ocr_json(args: FormatJsonArgs) -> serde_json::Value { - let FormatJsonArgs { - input_path, - input_hw, - text_lines, - } = args; - - let line_items: Vec<_> = text_lines - .iter() - .filter_map(|line| line.as_ref()) - .map(|line| { - let word_items: Vec<_> = line - .words() - .map(|word| { - json!({ - "text": word.to_string(), - "vertices": rounded_vertex_coords(&word.rotated_rect()), - }) - }) - .collect(); - - json!({ - "text": line.to_string(), - "words": word_items, - "vertices": rounded_vertex_coords(&line.rotated_rect()), - }) - }) - .collect(); - - let [height, width] = input_hw; - - json!({ - "url": input_path, - "image_width": width, - "image_height": height, - - // nb. Since we haven't got layout analysis info here, we just put all - // the lines on one paragraph. - "paragraphs": [{ - "lines": serde_json::Value::Array(line_items), - }] - }) -} - -/// Input data for [format_json_output]. -pub struct FormatJsonArgs<'a> { - pub input_path: &'a str, - pub input_hw: [usize; 2], - - /// Lines of text recognized by OCR engine. - pub text_lines: &'a [Option], -} - -/// Format OCR outputs as plain text. -pub fn format_text_output(text_lines: &[Option]) -> String { - let lines: Vec = text_lines - .iter() - .flatten() - .map(|line| line.to_string()) - .collect(); - lines.join("\n") -} - -/// Format OCR outputs as JSON. -pub fn format_json_output(args: FormatJsonArgs) -> String { - let json_data = ocr_json(args); - serde_json::to_string_pretty(&json_data).expect("JSON formatting failed") -} - -/// Arguments for [generate_annotated_png]. -pub struct GeneratePngArgs<'a> { - /// Input image as a (channels, height, width) tensor. - pub img: NdTensorView<'a, f32, 3>, - - /// Lines of text detected by OCR engine. - pub line_rects: &'a [Vec], - - /// Lines of text recognized by OCR engine. - pub text_lines: &'a [Option], -} - -/// Annotate OCR input image with detected text. -pub fn generate_annotated_png(args: GeneratePngArgs) -> NdTensor { - let GeneratePngArgs { - img, - line_rects, - text_lines, - } = args; - let mut annotated_img = img.to_tensor(); - let mut painter = Painter::new(annotated_img.view_mut()); - - // Colors chosen from https://www.w3.org/wiki/CSS/Properties/color/keywords. - // - // Light colors for text detection outputs, darker colors for - // corresponding text recognition outputs. - const CORAL: Rgb = [255, 127, 80]; - const DARKSEAGREEN: Rgb = [143, 188, 143]; - const CORNFLOWERBLUE: Rgb = [100, 149, 237]; - - const CRIMSON: Rgb = [220, 20, 60]; - const DARKGREEN: Rgb = [0, 100, 0]; - const DARKBLUE: Rgb = [0, 0, 139]; - - const LIGHT_GRAY: Rgb = [200, 200, 200]; - - let u8_to_f32 = |x: u8| x as f32 / 255.; - let floor_point = |p: PointF| Point::from_yx(p.y as i32, p.x as i32); - - // Draw line bounding rects from layout analysis step. - for line in line_rects.iter() { - let line_points: Vec<_> = line - .iter() - .flat_map(|word_rect| word_rect.corners().into_iter()) - .collect(); - if let Some(line_rect) = min_area_rect(&line_points) { - painter.set_stroke(LIGHT_GRAY.map(u8_to_f32)); - painter.draw_polygon(&line_rect.corners().map(floor_point)); - }; - } - - // Draw word bounding rects from text detection step, grouped by line. - let colors = [CORAL, DARKSEAGREEN, CORNFLOWERBLUE]; - for (line, color) in line_rects.iter().zip(colors.into_iter().cycle()) { - for word_rect in line { - painter.set_stroke(color.map(u8_to_f32)); - painter.draw_polygon(&word_rect.corners().map(floor_point)); - } - } - - // Draw word bounding rects from text recognition step. These may be - // different as they are computed from the bounding boxes of recognized - // characters. - let colors = [CRIMSON, DARKGREEN, DARKBLUE]; - for (line, color) in text_lines.iter().zip(colors.into_iter().cycle()) { - let Some(line) = line else { - // Skip lines where recognition produced no output. - continue; - }; - for text_word in line.words() { - painter.set_stroke(color.map(u8_to_f32)); - painter.draw_polygon(&text_word.rotated_rect().corners().map(floor_point)); - } - } - - annotated_img -} - -#[cfg(test)] -mod tests { - use std::fs::read_to_string; - use std::io; - use std::path::PathBuf; - - use ocrs::{TextChar, TextItem, TextLine}; - use rten_imageproc::Rect; - use rten_tensor::prelude::*; - use rten_tensor::NdTensor; - - use super::{ - format_json_output, format_text_output, generate_annotated_png, FormatJsonArgs, - GeneratePngArgs, - }; - - /// Generate dummy OCR output with the given text and character spacing. - fn gen_text_chars(text: &str, width: i32) -> Vec { - text.chars() - .enumerate() - .map(|(i, char)| TextChar { - char, - rect: Rect::from_tlhw(0, i as i32 * width, 25, width), - }) - .collect() - } - - fn read_test_file(path: &str) -> Result { - let mut abs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - abs_path.push("test-data/"); - abs_path.push(path); - read_to_string(abs_path) - } - - #[test] - fn test_format_json_output() { - let lines = &[ - Some(TextLine::new(gen_text_chars("line one", 10))), - None, - Some(TextLine::new(gen_text_chars("line two", 10))), - ]; - - let json = format_json_output(FormatJsonArgs { - input_path: "image.jpeg", - input_hw: [256, 256], - text_lines: lines, - }); - let parsed_json: serde_json::Value = serde_json::from_str(&json).unwrap(); - - let expected_json = read_test_file("format-json-expected.json").unwrap(); - let expected: serde_json::Value = serde_json::from_str(&expected_json).unwrap(); - assert_eq!(parsed_json, expected); - } - - #[test] - fn test_format_text_output() { - let lines = &[ - Some(TextLine::new(gen_text_chars("line one", 10))), - None, - Some(TextLine::new(gen_text_chars("line two", 10))), - ]; - let formatted = format_text_output(lines); - let formatted_lines: Vec<_> = formatted.lines().collect(); - - assert_eq!(formatted_lines, ["line one", "line two",]); - } - - #[test] - fn test_generate_annotated_png() { - let img = NdTensor::zeros([3, 64, 64]); - let text_lines = &[ - Some(TextLine::new(gen_text_chars("line one", 10))), - Some(TextLine::new(gen_text_chars("line one", 10))), - ]; - - let line_rects: Vec<_> = text_lines - .iter() - .filter_map(|line| line.clone().map(|l| vec![l.rotated_rect()])) - .collect(); - - let args = GeneratePngArgs { - img: img.view(), - line_rects: &line_rects, - text_lines, - }; - - let annotated = generate_annotated_png(args); - - assert_eq!(annotated.shape(), img.shape()); - } -} diff --git a/ocrs-cli/test-data/format-json-expected.json b/ocrs-cli/test-data/format-json-expected.json deleted file mode 100644 index 9da95fd8..00000000 --- a/ocrs-cli/test-data/format-json-expected.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "image_height": 256, - "image_width": 256, - "paragraphs": [ - { - "lines": [ - { - "text": "line one", - "vertices": [ - [80, 25], - [0, 25], - [0, 0], - [80, 0] - ], - "words": [ - { - "text": "line", - "vertices": [ - [40, 25], - [0, 25], - [0, 0], - [40, 0] - ] - }, - { - "text": "one", - "vertices": [ - [80, 25], - [50, 25], - [50, 0], - [80, 0] - ] - } - ] - }, - { - "text": "line two", - "vertices": [ - [80, 25], - [0, 25], - [0, 0], - [80, 0] - ], - "words": [ - { - "text": "line", - "vertices": [ - [40, 25], - [0, 25], - [0, 0], - [40, 0] - ] - }, - { - "text": "two", - "vertices": [ - [80, 25], - [50, 25], - [50, 0], - [80, 0] - ] - } - ] - } - ] - } - ], - "url": "image.jpeg" -} diff --git a/ocrs-extension/.gitignore b/ocrs-extension/.gitignore deleted file mode 100644 index 5814488b..00000000 --- a/ocrs-extension/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules/ -build/ -build-extension/ diff --git a/ocrs-extension/Makefile b/ocrs-extension/Makefile deleted file mode 100644 index 1ba41890..00000000 --- a/ocrs-extension/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -.PHONY: build -build: - mkdir -p build - cp ../dist/ocrs.js ../dist/ocrs.d.ts build/ - cp ../dist/ocrs_bg.wasm build/ - cp ../models/ocr/text-detection.rten build/ - cp ../models/ocr/text-recognition.rten build/ - npm run build - -.PHONY: clean -clean: - rm -rf build build-extension diff --git a/ocrs-extension/README.md b/ocrs-extension/README.md deleted file mode 100644 index 8348ae01..00000000 --- a/ocrs-extension/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# ocrs browser extension - -The Ocrs browser extension allows you to copy text from images, videos, PDFs or -any other content displayed in a browser tab. - -It has currently only been tested in Chrome. - -## Building the extension - -1. First, build the WebAssembly OCR library. In the root directory of this - repository run: - - ```sh - make ocrs-wasm - ``` - -2. Download pre-trained models. The easiest way to do this is to run the - ocrs CLI tool, which will download models from the preferred location, and - then copy them from the cache directory to `/models/ocr`. - In the root of the repository run: - - ```sh - cargo run -r -p ocrs-cli test-image.jpeg - mkdir -p models/ocr - cp ~/.cache/ocrs/text-detection.rten ~/.cache/ocrs/text-recognition.rten models/ocr - ``` - - Where `test-image.jpeg` can be any image you have available. - -3. Navigate to this directory and build the browser extension: - - ```sh - cd ocrs-extension - npm install - make build - ``` - -4. In Chrome, go to `chrome://extensions` and select "Load unpacked extension", - then select the `ocrs-extension` directory. - -## Using the extension - -1. After installing the extension, click the puzzle piece icon in Chrome's - toolbar and click the pin icon next to Ocrs to add it to the toolbar. - -2. On any browser tab, click the Ocrs logo in the toolbar to take a screenshot - of the current tab and highlight selectable text. - -3. Click anywhere outside of a text region or press Escape to close the OCR - overlay. diff --git a/ocrs-extension/error.html b/ocrs-extension/error.html deleted file mode 100644 index 7110ab9a..00000000 --- a/ocrs-extension/error.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - Screenshot error - Pixel Reader - - -

- Pixel Reader was unable to capture a screenshot of the tab at - {URI}. -

- - - diff --git a/ocrs-extension/images/eye-32.png b/ocrs-extension/images/eye-32.png deleted file mode 100644 index b13dc7cbaf62ba23e3668deee65850971292e912..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 618 zcmV-w0+s!VP)^>3*tf~Bz$Ekt_}#BZPkf{KEI zg~s>|T!W_AG$Nwn3Tni_TFhHBS!Uks>~7~CxbTKK=brm!-rTti@xNoF1b00Spoaa} zhRtZ<6Fy)L&+rO=236!J&R{8+brVath>iW4Phzozac4AeuvhcTSn0#O9V?hFYkqA2 z#+`99%P8v*Ucjw_0_%8ycj0;$j$*t(cMf-AguVDFe;40yET`}h97i*v|4~%r>jLwA z1v?Ztu_EU3goh%cmSpE`R563^A!ad_x_`1D*Hl&kPa=XhRv&v6Uotpf!##mpkK}vQ zX}$@|5yABocP7HSpW@w!@O}wRsuIXkl8%>$#nieM;T=w`-$!^Gg6F>i28C8xLA!t# z_#NTxNUfhocu!O7oe|!0@LYq(^50qA>LDDH9Jo%-XKW9g3f@NYJiVj@qN!i0DudKnY%5pTs!Nt4NH#TCiUi(2vSv?wdldE748 z<$~mAv=FYxailYO2F~Jc0S8"], - "resources": ["build-extension/content.js"] - } - ] -} diff --git a/ocrs-extension/package-lock.json b/ocrs-extension/package-lock.json deleted file mode 100644 index 6003f585..00000000 --- a/ocrs-extension/package-lock.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "name": "ocr-extension", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "ocr-extension", - "version": "1.0.0", - "license": "BSD-2-Clause", - "dependencies": { - "@types/chrome": "^0.0.242", - "prettier": "^3.0.0", - "typescript": "^5.1.6" - } - }, - "node_modules/@types/chrome": { - "version": "0.0.242", - "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.242.tgz", - "integrity": "sha512-SeMXBSfcAGX9ezTz7Pro7n/AiNdIH3cetkdbM+Kfg3zD24jmbnm0IAEIxzx8ccqrnJenLCfD7fR+4WIYAbeQHw==", - "dependencies": { - "@types/filesystem": "*", - "@types/har-format": "*" - } - }, - "node_modules/@types/filesystem": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz", - "integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==", - "dependencies": { - "@types/filewriter": "*" - } - }, - "node_modules/@types/filewriter": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz", - "integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==" - }, - "node_modules/@types/har-format": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.11.tgz", - "integrity": "sha512-T232/TneofqK30AD1LRrrf8KnjLvzrjWDp7eWST5KoiSzrBfRsLrWDPk4STQPW4NZG6v2MltnduBVmakbZOBIQ==" - }, - "node_modules/prettier": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", - "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - } - } -} diff --git a/ocrs-extension/package.json b/ocrs-extension/package.json deleted file mode 100644 index acd94d6e..00000000 --- a/ocrs-extension/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "ocr-extension", - "version": "1.0.0", - "description": "Browser extension that turns screen pixels into selectable text", - "main": "index.js", - "scripts": { - "build": "tsc", - "watch": "tsc --watch", - "format": "prettier -w **/*.{html,md,ts}", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/robertknight/rten.git" - }, - "author": "Robert Knight ", - "license": "BSD-2-Clause", - "bugs": { - "url": "https://github.com/robertknight/rten/issues" - }, - "homepage": "https://github.com/robertknight/rten#readme", - "dependencies": { - "@types/chrome": "^0.0.242", - "prettier": "^3.0.0", - "typescript": "^5.1.6" - } -} diff --git a/ocrs-extension/screenshot.html b/ocrs-extension/screenshot.html deleted file mode 100644 index b7e38a10..00000000 --- a/ocrs-extension/screenshot.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - Screenshot - Pixel Reader - - -

- Pixel Reader cannot enable text selection directly on - {URL}. Instead - here is a screenshot of the tab with selectable text. -

-
-
- -
- - - diff --git a/ocrs-extension/src/background.ts b/ocrs-extension/src/background.ts deleted file mode 100644 index 5538b457..00000000 --- a/ocrs-extension/src/background.ts +++ /dev/null @@ -1,466 +0,0 @@ -import { - DetectedLine, - OcrEngine, - OcrEngineInit, - TextLine, - default as initOcrLib, -} from "../build/ocrs.js"; -import type { LineRecResult, RotatedRect, WordRecResult } from "./types"; -import type * as contentModule from "./content"; -import type { TextOverlay, TextOverlayOptions } from "./content"; - -type OCRResources = { - detectionModel: Uint8Array; - recognitionModel: Uint8Array; -}; - -let ocrResources: Promise | undefined; - -/** - * Create an OCR engine and configure its models. - */ -async function createOCREngine(): Promise { - if (!ocrResources) { - // Initialize OCR library and fetch models on first use. - const init = async () => { - const [ocrBin, detectionModel, recognitionModel] = await Promise.all([ - fetch("../build/ocrs_bg.wasm").then((r) => r.arrayBuffer()), - fetch("../build/text-detection.rten").then((r) => r.arrayBuffer()), - fetch("../build/text-recognition.rten").then((r) => r.arrayBuffer()), - ]); - - await initOcrLib(ocrBin); - - return { - detectionModel: new Uint8Array(detectionModel), - recognitionModel: new Uint8Array(recognitionModel), - }; - }; - ocrResources = init(); - } - - const { detectionModel, recognitionModel } = await ocrResources; - const ocrInit = new OcrEngineInit(); - ocrInit.setDetectionModel(detectionModel); - ocrInit.setRecognitionModel(recognitionModel); - return new OcrEngine(ocrInit); -} - -/** - * Capture the visible area of the current tab. - */ -async function captureTabImage(): Promise { - const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); - const activeTab = tabs[0]; - - const bitmap = await chrome.tabs - .captureVisibleTab() - .then((dataURL) => fetch(dataURL)) - .then((response) => response.blob()) - .then((blob) => - createImageBitmap(blob, { - // `captureVisibleTab` may return a HiDPI image if - // `window.devicePixelRatio > 1`. Scaling the image down as soon as - // possible makes subsequent operations cheaper. - resizeWidth: activeTab.width, - resizeHeight: activeTab.height, - }), - ); - const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); - const context = canvas.getContext("2d")!; - context.drawImage(bitmap, 0, 0); - return context.getImageData(0, 0, bitmap.width, bitmap.height); -} - -function* chunks(items: T[], chunkSize: number) { - for (let i = 0; i < items.length; i += chunkSize) { - yield items.slice(i, i + chunkSize); - } -} - -/** Return an array of numbers in the range `[start, end)` */ -function range(start: number, end: number) { - return Array(end - start) - .fill(start) - .map((x, i) => x + i); -} - -function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -/** - * Content script function that tests if the tab is displaying Chrome's native - * PDF viewer. - */ -function tabIsPDFViewer() { - return document.querySelector('embed[type="application/pdf"]') !== null; -} - -/** - * Content script function that removes the overlay in the current tab. - */ -async function dismissTextOverlay() { - const contentSrc = chrome.runtime.getURL("build-extension/content.js"); - const content: typeof contentModule = await import(contentSrc); - content.dismissTextOverlay(); -} - -/** - * Content script function that creates an overlay in the current tab. - */ -async function createTextOverlay(lineCoords: RotatedRect[]) { - const contentSrc = chrome.runtime.getURL("build-extension/content.js"); - const content: typeof contentModule = await import(contentSrc); - const overlayOptions: TextOverlayOptions = { - lineCoords, - - // Perform on-demand recognition of a text line. - recognizeText(lineIndex: number): Promise { - return chrome.runtime.sendMessage({ - method: "recognizeText", - args: { lineIndex }, - time: Date.now(), - }); - }, - }; - const overlay = content.createTextOverlay(overlayOptions); - - // Receive eagerly recognized text from the OCR engine. - const onMessage = (message: any, sender: any) => { - if (message.type === "textRecognized") { - overlay.setLineText(message.lineIndex, message.recResult); - } - }; - chrome.runtime.onMessage.addListener(onMessage); - - // Handle removal of overlay. - overlay.dismissed.addEventListener("abort", () => { - chrome.runtime.onMessage.removeListener(onMessage); - chrome.runtime.sendMessage({ - method: "cancelRecognition", - time: Date.now(), - }); - }); -} - -/** - * Map coordinates from a tab screenshot to the coordinate system of the - * document's viewport, as seen by code running in the tab. - * - * This assumes that the screenshot was captured at regular DPI (ie. as if - * `window.devicePixelRatio` was 1), and so we don't need to compensate for - * that here. - */ -function tabImageToDocumentCoords(coords: number[], zoom: number) { - return coords.map((c) => c / zoom); -} - -/** - * Convert a `TextLineList` from the OCR engine to a `LineRecResult` array - * that can be serialized and sent to the tab. - * - * @param zoom - The tab's zoom level, as returned by `chrome.tabs.getZoom` - * @param coords - Coordinates of the text lines from text detection - */ -function textLineToLineRecResult( - lines: TextLine[], - zoom: number, - coords: RotatedRect[], -): Array { - const result: Array = []; - for (let i = 0; i < lines.length; i++) { - const recLine = lines[i]; - const words = recLine.words(); - result.push({ - words: words.map((word) => ({ - text: word.text(), - coords: tabImageToDocumentCoords( - Array.from(word.rotatedRect().corners()), - zoom, - ), - })), - coords: tabImageToDocumentCoords(coords[i], zoom), - }); - } - return result; -} - -/** - * Map of tab ID to controller for canceling background OCR, in tabs where this - * is currently active. - */ -const cancelControllers = new Map(); - -/** - * Callback for recognizing lines on-demand in tabs where extension has most - * recently been activated. - * - * TODO - There should be one of these per tab. - */ -let recognizeText: - | ((lineIndexes: number[]) => Array) - | undefined; - -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if ( - request.method === "recognizeText" && - typeof recognizeText === "function" - ) { - const [result] = recognizeText([request.args.lineIndex]); - sendResponse(result); - return true; - } - if ( - request.method === "cancelRecognition" && - typeof sender.tab?.id === "number" - ) { - const tabId = sender.tab.id; - const cancelCtrl = cancelControllers.get(tabId); - if (cancelCtrl) { - cancelCtrl.abort(); - cancelControllers.delete(tabId); - } - return true; - } - return false; -}); - -chrome.action.onClicked.addListener(async (tab) => { - if (!tab.id) { - return; - } - - // True if the tab is using Chrome's native PDF viewer. - let isPDF = false; - - // The tab in which the screenshot and text overlay are displayed, if we can't - // show the overlay directly in the tab where the extension was activated. - let screenshotTab: chrome.tabs.Tab | null = null; - - // The tab screenshot, if captured as part of creating `screenshotTab`. - let tabImage: ImageData | null = null; - - // Determine whether this tab has content that needs special handling. - try { - const [isPDFResult] = await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: tabIsPDFViewer, - }); - isPDF = isPDFResult.result; - } catch (err) { - // If the script failed to run, this may be because: - // - // 1. It is a tab which does not allow content scripts, even with `activeTab` - // permissions (eg. `chrome://` URLs). - // 2. The page is using a sandbox origin (see - // https://bugs.chromium.org/p/chromium/issues/detail?id=1407986) - // - // Try to capture a screenshot instead and open that in a new tab with an - // overlay. - chrome.action.setBadgeText({ text: "..." }); - try { - // TODO - Capture a HiDPI image here since we're going to show this - // directly to the user. - tabImage = await captureTabImage(); - const screenshotTabURL = new URL("/screenshot.html", location.href); - screenshotTabURL.searchParams.set("url", tab.url ?? ""); - screenshotTab = await chrome.tabs.create({ - openerTabId: tab.id, - url: screenshotTabURL.href, - }); - chrome.tabs.sendMessage(screenshotTab.id!, { - type: "imageLoaded", - image: { - width: tabImage.width, - height: tabImage.height, - data: Array.from(tabImage.data), - }, - }); - } catch (err) { - // We can't even capture a screenshot :( - Just show an error. - const errorTabURL = new URL("/error.html", location.href); - errorTabURL.searchParams.set("url", tab.url ?? ""); - chrome.tabs.create({ - openerTabId: tab.id, - url: errorTabURL.href, - }); - console.warn("Unable to capture tab image:", err); - return; - } finally { - chrome.action.setBadgeText({ text: "" }); - } - } - - // Remove existing overlay, if extension was already activated in current tab. - if (!screenshotTab) { - await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: dismissTextOverlay, - }); - } - - // Cancel background text recognition for existing overlay. - let cancelCtrl = cancelControllers.get(tab.id); - if (cancelCtrl) { - cancelCtrl.abort(); - cancelControllers.delete(tab.id); - } - - cancelCtrl = new AbortController(); - cancelControllers.set(tab.id, cancelCtrl); - cancelCtrl.signal.addEventListener("abort", () => { - chrome.action.setBadgeText({ text: "" }); - }); - - chrome.action.setBadgeText({ text: "..." }); - - // Get the zoom level of the tab. Tab image coordinates need to be scaled by - // 1/zoom to map them to document coordinates. Chrome's PDF viewer is a - // special case because the zoom level applies to the embedded native viewer, - // but not the HTML document which contains it. - const zoom = isPDF || screenshotTab ? 1 : await chrome.tabs.getZoom(tab.id); - - // Cache of line number to recognition result for the current image. - const recognizedLines = new Map(); - - // List of all text lines detected in the current image. - let lines: DetectedLine[]; - - try { - // Init OCR engine concurrently with capturing tab image. - const ocrEnginePromise = createOCREngine(); - - const captureStart = performance.now(); - const image = tabImage ?? (await captureTabImage()); - const captureEnd = performance.now(); - - const ocrEngine = await ocrEnginePromise; - const ocrInput = ocrEngine.loadImage( - image.width, - image.height, - // Cast from `Uint8ClampedArray` to `Uint8Array`. - image.data as unknown as Uint8Array, - ); - - const detStart = performance.now(); - lines = ocrEngine.detectText(ocrInput); - const detEnd = performance.now(); - - console.log( - `Detected ${lines.length} lines. Capture ${ - captureEnd - captureStart - } ms, detection ${detEnd - detStart}ms.`, - ); - - /** - * Perform recognition on a batch of lines and cache the results. - */ - const recognizeLineBatch = (lineIndexes: number[]) => { - const recInput: DetectedLine[] = []; - const lineCoords: RotatedRect[] = []; - - for (const lineIndex of lineIndexes) { - const line = lines[lineIndex]; - if (!line) { - throw new Error("Invalid line number"); - } - lineCoords.push(Array.from(line.rotatedRect().corners())); - recInput.push(line); - } - - const textLines = ocrEngine.recognizeText(ocrInput, recInput); - for (const [i, recLine] of textLineToLineRecResult( - textLines, - zoom, - lineCoords, - ).entries()) { - recognizedLines.set(lineIndexes[i], recLine); - } - }; - - // Set up callback that performs text recognition. - // - // This takes a list of line indexes as input. The recognition time per - // line can be up to ~45% lower when recognizing a batch of similiar-length - // lines. - recognizeText = (lineIndexes: number[]) => { - const unrecognizedLines = lineIndexes.filter( - (idx) => !recognizedLines.has(idx), - ); - recognizeLineBatch(unrecognizedLines); - return lineIndexes.map((li) => recognizedLines.get(li)!); - }; - - // Create the text layer in the current tab. - const lineCoords = lines.map((line) => - tabImageToDocumentCoords(Array.from(line.rotatedRect().corners()), zoom), - ); - - if (screenshotTab) { - chrome.tabs.sendMessage(screenshotTab.id!, { - type: "createTextOverlay", - lineCoords, - }); - } else { - await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: createTextOverlay, - args: [lineCoords], - }); - } - } finally { - if (!cancelCtrl.signal.aborted) { - chrome.action.setBadgeText({ text: "" }); - } - } - - // Eagerly recognize lines, before the text layer has requested them. - if (lines && !cancelCtrl.signal.aborted) { - chrome.action.setBadgeText({ text: "~" }); - - // Sort lines by width, so lines in each batch are likely to have similar - // widths. This optimizes the benefit of performing recognition on a batch - // of lines at once. - const lineWidth = (dl: DetectedLine) => { - const [left, top, right, bottom] = dl.rotatedRect().boundingRect(); - return right - left; - }; - const sortedLines = [...lines]; - sortedLines.sort((a, b) => lineWidth(a) - lineWidth(b)); - - // Recognize lines in batches. - const chunkSize = 4; - for (let indices of chunks(range(0, sortedLines.length), chunkSize)) { - if (cancelCtrl.signal.aborted) { - break; - } - - // Skip any lines that were already recognized in response to a request - // from the tab. - indices = indices.filter((idx) => !recognizedLines.has(idx)); - - const recResults = await recognizeText(indices); - if (cancelCtrl.signal.aborted) { - break; - } - - for (let i = 0; i < indices.length; i++) { - const targetTab = screenshotTab?.id ?? tab.id; - chrome.tabs.sendMessage(targetTab, { - type: "textRecognized", - lineIndex: indices[i], - recResult: recResults[i], - }); - } - - // Pause between batches to allow any messages sent from content scripts - // in the interim to be processed first. - await delay(0); - } - - if (!cancelCtrl.signal.aborted) { - chrome.action.setBadgeText({ text: "" }); - } - } -}); diff --git a/ocrs-extension/src/content.ts b/ocrs-extension/src/content.ts deleted file mode 100644 index 004ebfc0..00000000 --- a/ocrs-extension/src/content.ts +++ /dev/null @@ -1,602 +0,0 @@ -import type { LineRecResult, RotatedRect, WordRecResult } from "./types"; - -type TextPosition = { textNode: Text; offset: number }; - -/** - * Return true if the point `(x, y)` is contained within `r`. - * - * The left/top edges are treated as "inside" the rect and the bottom/right - * edges as "outside". This ensures that for adjacent rects, a point will only - * lie within one rect. - */ -function rectContains(r: DOMRect, x: number, y: number) { - return x >= r.left && x < r.right && y >= r.top && y < r.bottom; -} - -/** - * Return the text node and offset of the character at the point `(x, y)` in - * client coordinates. - */ -function textPositionFromPoint( - container: Element, - x: number, - y: number, -): TextPosition | null { - // TODO - Optimize this using `{Document, ShadowRoot}.elementsFromPoint` to - // filter the node list. - const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT); - let currentNode; - const range = new Range(); - - while ((currentNode = walker.nextNode())) { - const text = currentNode as Text; - const str = text.nodeValue!; - for (let i = 0; i < str.length; i++) { - range.setStart(text, i); - range.setEnd(text, i + 1); - const charRect = range.getBoundingClientRect(); - if (rectContains(charRect, x, y)) { - return { textNode: text, offset: i }; - } - } - } - return null; -} - -/** - * Return the smallest axis-aligned rect that contains all corners of a - * rotated rect. - */ -function domRectFromRotatedRect(coords: RotatedRect): DOMRect { - const [x0, y0, x1, y1, x2, y2, x3, y3] = coords; - const left = Math.min(x0, x1, x2, x3); - const top = Math.min(y0, y1, y2, y3); - const right = Math.max(x0, x1, x2, x3); - const bottom = Math.max(y0, y1, y2, y3); - return new DOMRect(left, top, right - left, bottom - top); -} - -/** Font used for transparent text layer content. */ -const fixedFont = { - size: 16, - family: "sans-serif", -}; - -/** - * Create a line of selectable text in the transparent text layer. - */ -function createTextLine(line: LineRecResult): HTMLElement { - const lineEl = document.createElement("div"); - lineEl.className = "text-line"; - - const { left, top, right, bottom } = domRectFromRotatedRect(line.coords); - Object.assign(lineEl.style, { - // Position transparent line above text - position: "absolute", - left: `${left}px`, - top: `${top}px`, - width: `${right - left}px`, - height: `${bottom - top}px`, - - // Avoid line break if word elements don't quite fit. Also preserve spaces - // at the end of the line. - whiteSpace: "pre", - - // Use a fixed font. This needs to match the font used when measuring the - // natural width of text. - fontSize: `${fixedFont.size}px`, - fontFamily: fixedFont.family, - - // Make text transparent - color: "rgb(0 0 0 / 0)", - }); - - // Create canvas for measuring natural size of text. - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d")!; - context.font = `${fixedFont.size}px ${fixedFont.family}`; - - const spaceWidth = context.measureText(" ").width; - - // Add words to the line as inline-block elements. This allows us to create - // normal text selection behavior while adjusting the positioning and size - // of words to match the underlying pixels. - let prevWordRect: DOMRect | undefined; - let prevWordEl: HTMLElement | undefined; - for (const [wordIndex, word] of line.words.entries()) { - const wordRect = domRectFromRotatedRect(word.coords); - const leftMargin = prevWordRect - ? wordRect.left - prevWordRect.right - spaceWidth - : wordRect.left - left; - - // Create outer element for word. This sets the width and margin used for - // inline layout. - const wordEl = document.createElement("span"); - Object.assign(wordEl.style, { - display: "inline-block", - marginLeft: leftMargin != null ? `${leftMargin}px` : undefined, - marginTop: `${wordRect.top - top}px`, - width: `${wordRect.width}px`, - height: `${wordRect.height}px`, - - // Align top of word box with top of line. - verticalAlign: "top", - }); - - const metrics = context.measureText(word.text); - const xScale = wordRect.width / metrics.width; - const yScale = wordRect.height / fixedFont.size; - - // Create inner element for word. This uses a transform to make the rendered - // size match the underlying text pixels. The transform doesn't affect - // layout. The inner and outer elements are separated so that the scale - // transform is not applied to the hit box for the outer element, as that - // would make the hit box's size `(width * xScale, height * yScale)`, which - // can interfere with selection of subsequent words. - const wordInner = document.createElement("span"); - wordInner.textContent = word.text; - Object.assign(wordInner.style, { - display: "inline-block", - transformOrigin: "top left", - transform: `scale(${xScale}, ${yScale})`, - }); - wordEl.append(wordInner); - - lineEl.append(wordEl); - prevWordEl = wordEl; - prevWordRect = wordRect; - - // Add space between words. We add this even after the last word in a line - // to ensure there is a space between the end of one line and the start of - // the next in a multi-line selection. - const spaceEl = document.createElement("span"); - - let spaceXScale = 1; - if (wordIndex < line.words.length - 1) { - const nextWordRect = domRectFromRotatedRect( - line.words[wordIndex + 1].coords, - ); - const targetSpaceWidth = nextWordRect.left - wordRect.right; - spaceXScale = targetSpaceWidth / spaceWidth; - } - - Object.assign(spaceEl.style, { - display: "inline-block", - - // Align top of space with top of preceding word. - marginTop: `${wordRect.top - top}px`, - verticalAlign: "top", - - // Scale the space to match the height of the word, and the width between - // the current and next words. - transformOrigin: "top left", - transform: `scale(${spaceXScale}, ${yScale})`, - }); - spaceEl.textContent = " "; - - lineEl.append(spaceEl); - } - return lineEl; -} - -export type TextOverlay = { - /** A signal that fires when the overlay is removed. */ - dismissed: AbortSignal; - - /** Remove the overlay. */ - remove(): void; - - /** - * Set the recognized text for a line, or `null` if no text is found. - * - * This can be used by the OCR engine to supply recognition results before - * they have been requested as a result of eg. the user hovering a line, - * avoiding latency when recognition is needed later on. - */ - setLineText(lineIndex: number, recResult: LineRecResult | null): void; -}; - -/** - * Configuration for the text overlay. - */ -export type TextOverlayOptions = { - /** Coordinates of detected text lines. */ - lineCoords: RotatedRect[]; - - /** - * Callback to run recognition on a line of text and return the recognition - * results, or `null` if no text was recognized. - */ - recognizeText(lineIndex: number): Promise; - - /** - * A custom container for the overlay. This should be a positioned element, - * which the overlay will fill. If not specified, the overlay is sized to fill - * the viewport and placed either in `document.body` or the top layer element - * (eg. the fullscreen element) if there is one. - */ - container?: HTMLElement; - - /** - * Whether the overlay is automatically dimissed when you click outside of - * it. Defaults to true. - */ - autoDismiss?: boolean; -}; - -/** The active overlay in the current document. */ -let activeOverlay: TextOverlay | null = null; - -/** - * Remove the active overlay in the current document. - */ -export function dismissTextOverlay() { - activeOverlay?.remove(); - activeOverlay = null; -} - -/** - * Return the container element into which the text overlay should be placed. - * - * By default this is the document body, but if there is a top layer [1] active, - * the overlay needs to be placed in that to be visible. - * - * [1] https://developer.mozilla.org/en-US/docs/Glossary/Top_layer - */ -function getOverlayParent() { - if (document.fullscreenElement) { - return document.fullscreenElement; - } - - // Other ways of creating a top layer that are not yet handled: - // - `HTMLDialogElement.showModal()` - // - `HTMLElement.showPopover()` - - return document.body; -} - -/** - * Create an overlay which shows the location of OCR-ed text in the viewport - * and enables the user to select and copy text from it. - * - * Only one overlay is supported in the document at a time, and if there is - * already an existing active overlay when this function is called, it is - * dismissed. - */ -export function createTextOverlay({ - recognizeText, - lineCoords, - container, - autoDismiss = true, -}: TextOverlayOptions): TextOverlay { - dismissTextOverlay(); - - // Pending recognition requests. These are processed in LIFO order as - // requests are triggered in response to user interactions (eg. hovering a - // text line) and so we want to give priority to the most recently hovered - // line. - const pendingRecRequests: Array<{ - lineIndex: number; - resolve: (result: LineRecResult | null) => void; - }> = []; - let pendingRecTimer: number | undefined; - - const flushPendingRequests = () => { - while (pendingRecRequests.length > 0) { - const req = pendingRecRequests.pop()!; - recognizeText(req.lineIndex).then((result) => req.resolve(result)); - } - pendingRecTimer = undefined; - }; - - // Schedule recognition of a line. We buffer requests that happen close - // together and process them in LIFO order. - const scheduleRecognition = (lineIndex: number) => { - let resolve; - const recResult = new Promise((resolve_) => { - resolve = resolve_; - }); - - pendingRecRequests.push({ lineIndex, resolve: resolve! }); - clearTimeout(pendingRecTimer); - // nb. Node typings for `setTimeout` are incorrectly being used here. - pendingRecTimer = setTimeout( - flushPendingRequests, - 100, - ) as unknown as number; - - return recResult; - }; - - const canvasContainer = document.createElement("div"); - Object.assign(canvasContainer.style, { - // Override default styles from the page. - all: "initial", - - // Display overlay above other elements. If there is a top layer active, - // then we also need to ensure the element is added to that layer. - zIndex: 9999, - }); - - if (container) { - // Make the overlay fill the custom container. - Object.assign(canvasContainer.style, { - position: "absolute", - top: "0", - left: "0", - right: "0", - bottom: "0", - }); - } else { - // Position the overlay so that it fills the viewport, but scrolls with - // the page contents. This allows the user to read parts of the page that - // were OCR-ed, without disrupting the selection in the part that has been. - // - // A known issue with this is that when the page is scrolled, text in the - // overlay will become mis-aligned with underlying pixels that belong to - // fixed-positioned elements. - Object.assign(canvasContainer.style, { - position: "absolute", - top: `${document.documentElement.scrollTop}px`, - left: `${document.documentElement.scrollLeft}px`, - width: `${window.innerWidth}px`, - height: `${window.innerHeight}px`, - }); - } - - // Use a shadow root to insulate children from page styles. - canvasContainer.attachShadow({ mode: "open" }); - - const overlayParent = container ?? getOverlayParent(); - overlayParent.append(canvasContainer); - - const canvas = document.createElement("canvas"); - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - Object.assign(canvas.style, { - position: "absolute", - top: "0", - left: "0", - right: "0", - bottom: "0", - }); - - canvasContainer.shadowRoot!.append(canvas); - - // Draw text layer backdrop. - const ctx = canvas.getContext("2d")!; - ctx.fillStyle = "rgb(0 0 0 / .3)"; - ctx.fillRect(0, 0, window.innerWidth, window.innerHeight); - - // Map of line index to: - // 1) Recognized text, if recognition is complete - // 2) A promise if recognition is in progress - // 3) `null` if recognition completed but no text was recognized - const textCache = new Map< - number, - LineRecResult | Promise | null - >(); - - const rotatedRectPath = (coords: RotatedRect) => { - const [x0, y0, x1, y1, x2, y2, x3, y3] = coords; - const path = new Path2D(); - path.moveTo(x0, y0); - path.lineTo(x1, y1); - path.lineTo(x2, y2); - path.lineTo(x3, y3); - path.closePath(); - return path; - }; - - // Cut out holes in the backdrop where there is detected text. - ctx.save(); - ctx.globalCompositeOperation = "destination-out"; - ctx.fillStyle = "white"; - const linePaths = lineCoords.map(rotatedRectPath); - for (const path of linePaths) { - ctx.fill(path); - } - ctx.restore(); - - const lineIndexFromPoint = (clientX: number, clientY: number) => { - const canvasRect = canvas.getBoundingClientRect(); - const canvasX = clientX - canvasRect.left; - const canvasY = clientY - canvasRect.top; - return linePaths.findIndex((lp) => ctx.isPointInPath(lp, canvasX, canvasY)); - }; - - // Track the start and end coordinates of the current mouse drag operation. - const primaryButtonPressed = (e: MouseEvent) => e.buttons & 1; - let dragStartAt: { x: number; y: number } | null = null; - let dragEndAt: { x: number; y: number } | null = null; - canvasContainer.onmousedown = (e) => { - if (!dragStartAt) { - dragStartAt = { x: e.x, y: e.y }; - } - }; - canvasContainer.onmousemove = (e) => { - if (primaryButtonPressed(e)) { - dragEndAt = { x: e.x, y: e.y }; - } - }; - canvasContainer.onmouseup = (e) => { - dragEndAt = null; - dragStartAt = null; - }; - canvasContainer.onmouseenter = (e) => { - if (primaryButtonPressed(e)) { - dragStartAt = { x: e.x, y: e.y }; - } else { - dragStartAt = null; - } - }; - - /** - * Save recognition results for a line and, if not null, create the - * transparent text line allowing the user to select text. - */ - const initTextLine = (lineIndex: number, recResult: LineRecResult | null) => { - textCache.set(lineIndex, recResult); - if (!recResult) { - return; - } - - const lineEl = createTextLine(recResult); - lineEl.setAttribute("data-line-index", lineIndex.toString()); - - // Insert line such that the DOM order is the same as the output order - // from the OCR lib, which produces lines in reading order. This makes - // text selection across lines and columns flow properly, provided that - // the OCR lib detected the reading order correctly. - const successor = Array.from(textLines.entries()) - .sort((a, b) => a[0] - b[0]) - .find(([entryLine, entryEl]) => entryLine >= lineIndex); - const successorNode = successor ? successor[1] : null; - textLayer.insertBefore(lineEl, successorNode); - textLines.set(lineIndex, lineEl); - - // If a drag operation was in progress, update the selection once text - // recognition is completed, to include the newly recognized text. - if (dragStartAt && dragEndAt) { - const start = textPositionFromPoint( - textLayer, - dragStartAt.x, - dragStartAt.y, - ); - const end = textPositionFromPoint(textLayer, dragEndAt.x, dragEndAt.y); - const selection = document.getSelection(); - if (selection && start && end) { - selection.setBaseAndExtent( - start.textNode, - start.offset, - end.textNode, - end.offset, - ); - } - } - }; - - // Perform on-demand recognition when the user hovers a line of text that has - // not yet been recognized. - const recognizeLine = async ( - lineIndex: number, - ): Promise => { - const cachedResult = textCache.get(lineIndex); - if (cachedResult !== undefined) { - return cachedResult; - } - - const recPromise = scheduleRecognition(lineIndex); - textCache.set(lineIndex, recPromise); - - const recResult = await recPromise; - initTextLine(lineIndex, recResult); - return recResult; - }; - - // Create the hidden text layer in which the user can select text. - const textLayer = document.createElement("div"); - canvasContainer.shadowRoot!.append(textLayer); - - const textLines = new Map(); - - let prevLine = -1; - canvas.onmousemove = async (e) => { - const currentLine = lineIndexFromPoint(e.clientX, e.clientY); - if (currentLine === prevLine) { - return; - } - prevLine = currentLine; - - if (currentLine === -1) { - return; - } - - // Recognize text for the current line, if just hovering, or all lines - // between the start and end point of the current drag operation if a mouse - // button is pressed. - const dragStartLine = dragStartAt - ? lineIndexFromPoint(dragStartAt.x, dragStartAt.y) - : currentLine; - const startLine = - dragStartLine !== -1 ? Math.min(dragStartLine, currentLine) : currentLine; - const endLine = - dragStartLine !== -1 ? Math.max(dragStartLine, currentLine) : currentLine; - - for (let lineIndex = startLine; lineIndex <= endLine; lineIndex++) { - if (!textCache.has(lineIndex)) { - // TODO: We currently recognize a single line at a time here, but - // recognition is more efficient if batches of similarly-sized lines - // are recognized at once. - recognizeLine(lineIndex); - } - } - }; - - // Signal used to remove global listeners etc. when overlay is removed. - const overlayRemoved = new AbortController(); - overlayRemoved.signal.addEventListener("abort", () => { - canvasContainer.remove(); - clearTimeout(pendingRecTimer); - }); - - const removeOverlay = () => overlayRemoved.abort(); - const autoDismissOverlay = () => { - if (autoDismiss) { - removeOverlay(); - } - }; - - // Dismiss overlay when user clicks on the backdrop, but not inside text or - // other UI elements in the overlay. - canvas.onclick = (e) => { - // Don't dismiss the overlay if the user started a drag action (eg. to - // select text), but happened to finish on the canvas instead of inside a - // text element. - if (dragStartAt) { - const dragDist = Math.sqrt( - (e.x - dragStartAt.x) ** 2 + (e.y - dragStartAt.y) ** 2, - ); - if (dragDist >= 20) { - return; - } - } - - // Don't dismiss the overlay if the user clicks inside a line that hasn't - // been recognized yet. - const lineIndex = lineIndexFromPoint(e.clientX, e.clientY); - if (lineIndex !== -1) { - return; - } - - autoDismissOverlay(); - }; - - // When the window is resized, the document layout will change and the OCR - // boxes will likely be incorrect, so just remove the overlay at that point. - window.addEventListener("resize", autoDismissOverlay, { - signal: overlayRemoved.signal, - }); - - document.addEventListener( - "keyup", - (e) => { - if (e.key === "Escape") { - autoDismissOverlay(); - } - }, - { signal: overlayRemoved.signal }, - ); - - activeOverlay = { - dismissed: overlayRemoved.signal, - setLineText: (lineIndex: number, recResult: LineRecResult) => { - if (textCache.has(lineIndex)) { - return; - } - initTextLine(lineIndex, recResult); - }, - remove: removeOverlay, - }; - return activeOverlay; -} diff --git a/ocrs-extension/src/error.ts b/ocrs-extension/src/error.ts deleted file mode 100644 index 55e0ef3f..00000000 --- a/ocrs-extension/src/error.ts +++ /dev/null @@ -1,7 +0,0 @@ -const params = new URLSearchParams(location.search); -const sourceURIField = document.getElementById("sourceTabURI")!; -const url = params.get("url"); -sourceURIField.textContent = url ?? "(unknown URL)"; - -// Tell TS this is an ES module. -export {}; diff --git a/ocrs-extension/src/screenshot.ts b/ocrs-extension/src/screenshot.ts deleted file mode 100644 index 8f8f8620..00000000 --- a/ocrs-extension/src/screenshot.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { createTextOverlay } from "./content.js"; -import type { TextOverlay } from "./content"; -import type { LineRecResult } from "./types"; - -const screenshotCanvas = document.getElementById( - "screenshotImage", -) as HTMLCanvasElement; - -const screenshotContainer = document.getElementById("container")!; - -let overlay: TextOverlay | null = null; - -chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - switch (message.type) { - case "createTextOverlay": - overlay = createTextOverlay({ - autoDismiss: false, - container: screenshotContainer, - lineCoords: message.lineCoords, - recognizeText(lineIndex: number): Promise { - return chrome.runtime.sendMessage({ - method: "recognizeText", - args: { lineIndex }, - time: Date.now(), - }); - }, - }); - break; - case "imageLoaded": - const { width, height, data } = message.image; - screenshotCanvas.width = width; - screenshotCanvas.height = height; - const imgData = new ImageData(new Uint8ClampedArray(data), width, height); - const ctx = screenshotCanvas.getContext("2d")!; - ctx.putImageData(imgData, 0, 0); - break; - case "textRecognized": - overlay?.setLineText(message.lineIndex, message.recResult); - break; - } -}); - -const params = new URLSearchParams(location.search); -const sourceURIField = document.getElementById("sourceTabURI")!; -const url = params.get("url"); -sourceURIField.textContent = url ?? "(unknown URL)"; - -// Tell TS this is an ES module. -export {}; diff --git a/ocrs-extension/src/types.ts b/ocrs-extension/src/types.ts deleted file mode 100644 index 9ba62648..00000000 --- a/ocrs-extension/src/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Array of coordinates of corners of a rotated rect, in the order - * [x0, y0, x1, y1, x2, y2, x3, y3]. - */ -export type RotatedRect = number[]; - -export type WordRecResult = { - text: string; - coords: RotatedRect; -}; - -/** - * Serializable version of the text line recognition result (`TextLine`) from - * the OCR engine. - */ -export type LineRecResult = { - words: WordRecResult[]; - coords: RotatedRect; -}; diff --git a/ocrs-extension/tsconfig.json b/ocrs-extension/tsconfig.json deleted file mode 100644 index 0624524c..00000000 --- a/ocrs-extension/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Language and Environment */ - "target": "es2022", - "outDir": "./build-extension", - "module": "es2020", - - /* Interop Constraints */ - "esModuleInterop": false, - "forceConsistentCasingInFileNames": true, - - /* Type Checking */ - "strict": true, - - /* Completeness */ - "skipLibCheck": true - } -} diff --git a/ocrs/CHANGELOG.md b/ocrs/CHANGELOG.md deleted file mode 100644 index 3856f827..00000000 --- a/ocrs/CHANGELOG.md +++ /dev/null @@ -1,26 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [0.2.1] - 2024-01-01 - - - Update rten to fix incorrect output on non-x64 / wasm32 platforms - -## [0.2.0] - 2024-01-01 - - - Improve layout analysis (ce52b3a1, cefb6c3f). The longer term plan is to use - machine learning for layout analysis, but these incremental tweaks address - some of the most egregious errors. - - Add `--version` flag to CLI (20055ee0) - - Revise CLI flags for specifying output format (97c3a011). The output path - is now specified with `-o`. Available formats are text (default), JSON - (`--json`) or annotated PNG (`--png`). - - Fixed slow OCR model downloads by changing hosting location - (https://github.com/robertknight/rten/issues/22). - -## [0.1.0] - 2023-12-31 - -Initial release. diff --git a/ocrs/Cargo.toml b/ocrs/Cargo.toml deleted file mode 100644 index 5daab80e..00000000 --- a/ocrs/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "ocrs" -version = "0.2.1" -edition = "2021" -authors = ["Robert Knight"] -description = "OCR engine for extracting text from images" -license = "MIT OR Apache-2.0" -homepage = "https://github.com/robertknight/rten" -repository = "https://github.com/robertknight/rten" - -[dependencies] -rayon = "1.7.0" -rten = { path = "../", version = "0.1.1" } -rten-imageproc = { path = "../rten-imageproc", version = "0.1.0" } -rten-tensor = { path = "../rten-tensor", version = "0.1.0" } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2.87" - -[dev-dependencies] -fastrand = "1.9.0" -lexopt = "0.3.0" -rten-imageio = { path = "../rten-imageio", version = "0.1.0" } - -[lib] -crate-type = ["lib", "cdylib"] diff --git a/ocrs/README.md b/ocrs/README.md deleted file mode 100644 index 718b0819..00000000 --- a/ocrs/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# ocrs - -**ocrs** is an OCR engine written in Rust. It extracts text, including layout -information, from images. It uses the [RTen](https://github.com/robertknight/rten) -machine learning runtime. - -ocrs uses neural network models written in PyTorch. See the -[ocrs-models](https://github.com/robertknight/ocrs-models) repository for more -details and tools for training custom models. These models are also available in -ONNX format for use with other machine learning runtimes. - -## Background and status - -The original goal of this project was to create a more modern alternative to -Tesseract, which is still (!) the de-facto open-source OCR engine. See -https://github.com/robertknight/tesseract-wasm/issues/87 for context. Some ways -in which this project aims to improve upon Tesseract are: - -- Reducing the need to manually clean up and pre-process images before feeding - them into engine. This is achieved by replacing manually written components - for text detection and, in future, layout analysis, with machine learning - models. -- Using modern tools (eg. PyTorch) for training. Together with use of open - datasets this should make it easier to recreate and customize models -- Making the models available in portable formats (ONNX) which can be used with - many different runtimes -- Better support for Unicode and large character sets. - -ocrs is currently in an early preview. Expect more errors than commercial OCR -engines. The layout analysis phase is still uses manually written code which -can be brittle. - -## Command-line usage - -This library is wrapped in a CLI tool in the [ocrs-cli](../ocrs-cli) crate, -which you can use as follows: - -```sh -$ cargo install ocrs-cli -$ ocrs image.png -``` - -See the [ocrs-cli README](../ocrs-cli/README.md) for more details. - -## Library usage - -See [examples/hello_ocr.rs]() for a minimal example of using this library in -a Rust application. - -```sh -cd examples/ - -# Download models in .rten format. -./download-models.sh - -# Run OCR on an image and print the extracted text. -cargo run -r --example hello_ocr rust-book.jpg -``` diff --git a/ocrs/examples/download-models.sh b/ocrs/examples/download-models.sh deleted file mode 100755 index fef0efd4..00000000 --- a/ocrs/examples/download-models.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -curl https://s3.amazonaws.com/io.github.robertknight/ocrs-models/text-detection.rten -o text-detection.rten -curl https://s3.amazonaws.com/io.github.robertknight/ocrs-models/text-recognition.rten -o text-recognition.rten diff --git a/ocrs/examples/hello_ocr.rs b/ocrs/examples/hello_ocr.rs deleted file mode 100644 index a3aa145b..00000000 --- a/ocrs/examples/hello_ocr.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::collections::VecDeque; -use std::error::Error; -use std::fs; - -use ocrs::{OcrEngine, OcrEngineParams}; -use rten::Model; -use rten_imageio::read_image; - -struct Args { - image: String, -} - -fn parse_args() -> Result { - use lexopt::prelude::*; - - let mut values = VecDeque::new(); - let mut parser = lexopt::Parser::from_env(); - - while let Some(arg) = parser.next()? { - match arg { - Value(val) => values.push_back(val.string()?), - Long("help") => { - println!( - "Usage: {bin_name} ", - bin_name = parser.bin_name().unwrap_or("detr") - ); - std::process::exit(0); - } - _ => return Err(arg.unexpected()), - } - } - - let image = values.pop_front().ok_or("missing `image` arg")?; - - Ok(Args { image }) -} - -fn main() -> Result<(), Box> { - let args = parse_args()?; - - // Use the `download-models.sh` script to download the models. - let detection_model_data = fs::read("text-detection.rten")?; - let rec_model_data = fs::read("text-recognition.rten")?; - - let detection_model = Model::load(&detection_model_data)?; - let recognition_model = Model::load(&rec_model_data)?; - - let engine = OcrEngine::new(OcrEngineParams { - detection_model: Some(detection_model), - recognition_model: Some(recognition_model), - ..Default::default() - })?; - - // Read image using image-rs library and convert to a - // (channels, height, width) tensor with f32 values in [0, 1]. - let image = read_image(&args.image)?; - - // Apply standard image pre-processing expected by this library (convert - // to greyscale, map range to [-0.5, 0.5]). - let ocr_input = engine.prepare_input(image.view())?; - - // Phase 1: Detect text words - let word_rects = engine.detect_words(&ocr_input)?; - - // Phase 2: Perform layout analysis - let line_rects = engine.find_text_lines(&ocr_input, &word_rects); - - // Phase 3: Recognize text - let line_texts = engine.recognize_text(&ocr_input, &line_rects)?; - - for line in line_texts - .iter() - .flatten() - // Filter likely spurious detections. With future model improvements - // this should become unnecessary. - .filter(|l| l.to_string().len() > 1) - { - println!("{}", line); - } - - Ok(()) -} diff --git a/ocrs/examples/rust-book.jpg b/ocrs/examples/rust-book.jpg deleted file mode 100644 index 33cea950bcb1b1a8b57ff7aadc7b64dd10af412d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 244386 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 }~qDK)yW@T*5P-IfZEdR|mF`cD>}(X!gE z(H()9Jhmdd+Sq%oS5a$1nX%9;<0D{{RrBPK@klEt-P` z4Vev>LwK;MdYfoDs@|+ea_5{AoM{Y+!zrNw>}YeYkU@>OCy{xvKBuHsDlU%x7_CHP zX|V{b@mQ~u{+*Y%5GUngru5hmTg1fatA{<~HQBTd=(*m==?=B;q^CRTaoQ}15!A&lmB6b;svqR3Fl^}X^0#Dj ziT?mQQpIHPaaT#g!WCwB6>)W&4VjdMoYWxUDz{M$p6SkPYW15{!9ZYIi)o-iX|?zCPK(=k@Iu!{cV~O^Do8W?2+|RzzOXt25kf1r5BmH}bJJ zNUd;`sBhYs$TJTPtm2sw*Vxc71EgR6wT|=Jhg{zy~n#SA&lcBfL zEuth(q$rV29!2gPR|Rhyt#_5E-X&IcW64Q_WVPa9Pe*lT{!@Rbtt^V;Tc%=}7>%7a z>^qFk!@7n*81EGn@Rz0Yi4boaxow$E$B87*e1)iXf06Nos;R%|yG=Oqp#J?i&JTCJg{2HQ#M7rOrdG3B&h{m`qlc5XBO z0Od?vX_MLyRR!%}44jAyTLpo#zElW>?hfRG+WNqmAW7jH4>+3wZ1|tNXo&nS3M2C} zMWF9K#M;Ako5|022nxIIonuTWL+((A?bt7QkP36u!u*xvMEP5-w?6tNwbo#wXwP+G z%Z|v7(PX6L8p~Wz84bC_bdU_;3tbRz8>qKpu8zv*JQr^gL+NU&*4f;WIE{ft4s7DP z5{Y%i)@&BO;-ts6BL%_9?Q?3xZQ_X=l$+J4KO2tKc5s_ed^?WBqBh*oFuO+nQL0Gf zRHF)~H1@QEk|5p`GHwn9X%4Tr=146barH(O<1}9m?9b0Jy>_O4R&AmxI-lJfm6M0P$>1Bad zcN6VG15Y#QX&zNhaX6(L5ILw>4*wh}=O?`JI^+Hn0QJxOuAL%p|JI zba4~_xNNgVWyHv-G)Q-BFokJgrx;XjS)kU50kqvHcy6)iIi_tiS;j6>cLA}(C1%ZE zP`IMUi#<(2@XXqFdq!KHD3~U@a>Z4NHniA5TIO&%{bE) z93kqo(D=^1)((lVN2b!B9OJsxt7+V4@{DJ3KJJtq9ufPB*2Np<;g-n_yjm3fV~A01 z&8vuclGAZVM~mT}7i^NomqLA{3<@%YWy~$)}Kuzabu@#TwL z6QMls!!p{m0<0S!Yj&zO*Q%^0c{N(c`>Dr&uu|2Jb-L$|b!(niX4!L581_hA)oQ`C z3#lx4m?}or8;YAvt5+P!_#F0fiGz`3K_>(m9@b})tiv(3?L(?OUI+gG3dYwTwQsS_ z>L}d=^j1P3X6qF6ht}j3SZxh&Do#wIA;j32+{mDxyURfwlvsIMfc?QkreC-%E3Ga4 zqI(dV%l=SNkM2f0g|4x^5QHyv{S%|a;Djce^L7DD@xIcF-F^WV#NqW7A$HY*YDHFU zX8o(hKMQ&8D7L4%vL^3E!&v6!yus~a;Xf)>KPt$(y%mpjWahsM&^h^Gr9-gKdzuG6 z=pU*c&75ZBNV^v)FnjkCmAeIz1&XV#z|a{VdAX|4-7&FM_uz6>Vhb=itl{Did^0e1 zo?2ySG+Gw`KnQi5Q=&bf3CK%f&o?wpM~G--A?dWHaGg7@vTHSR@C0Sn#Co z91RtryV*$Y#LHl^AedI$4BX{0z;6gX8JkYe#>3r)3J~Qpu~!$FCqL>axZD%sW>iO> ziXr7Cq(sIcs1z@M=gVBB5KRCs6o%YEnq&ax*y_u4u=}f;dKAWDFIOv-#W2X#ZYnQ3 z5sd5KcSTfjKExjDLv5C5xZ2`VN2uCXq8YGpEo=DQ`0uRSFmzVdekUZ>xXkkM`bqZg zYL8Q-n$du!X2q+6b=^^XPZ8gbAXyMh6`V73sF*??lkEM1$bxMvIA-Fc>tP$vb8QD; zb!{;n%1RqEa2quT3j&1BOIA3a{7~H8YGglhv~3>{+Rhx*FMsFsP2dWU7B12lbhBOv zLBWP&ofYQtXFE+7tJUiLo0ZDta=BPGEa#J!>|YLZ7gPHY9A|h9}xONXm_pCG(FytU`=4WnJHiw?413Ait&lDLihfEUAbJY z2)s9VaUp3$u5!waBsvnayi-yM@g@RK(H6n6{kZo5BxdBpX+PgKfK;WbZG70Xy5fa}$0jNa;|(0){h z{{Xj-|HJ?*5CH%J00jdB1Oov90RR910096I5Fs%^Q6OP)FoBUkp|SA6(c$q>VE@_x z2mt{A0Y4$>FlrxLl^l9bqXVe)%nVFSPf&&-D-+ePr7*6O1ER8Kz5kD)q9bszZWS=4l31(=-1J0{{V>!))@E!30PJY)mT?WN`$NpaH&`XF|VLH z&bpP?Urk?5eKWZ60^ur?f4W^V`=z}EX(q_-Jj~89MMD1o>H)ee*z+IsAO?G}E9|ZF zmd_Dv=2pA&66l@gLryMI)b@?2j$vvn)ffQ>CO&@yBA6g{nCwX5ekOkNyZDC+DANdC zoFi7qabmx^f2D`|5dQ#bC;OngVB3CS$A=0WY$3@6w(tyDV2#Q--j*x(GFMQAM-^20A&8tlz)60Z8%~S4|$Ov zJna5q#hs7L?Y?Ja2HsPdXEQF0re3{t))muOo`Lj-MT5ulz{b8Xs7k}OHPyB?pG|rv zqqj+kr_o!?w=BuKEz3K}!oFwyn18H>-^hV!@pJPrOcH~L}8KlUcT z`G++h_h!LG-5+T0xDe$0Gh&`$n;@ppzBC{EVgBsw<&~){WlOTs(K@$;V$vK2&huM`aSMcZ7OD&!OEe%%^1+-cqeF%A5*?}e;5)Q zr~D~Kswb7-42moho$R1Go(zb zgNe{}pZSNPblw^Dd`dX`PSfIl#-H%60fwFRMYBDSn1pU03Q7l0}ICHToa$-mw!xGUx7d4ML(h*AlUko4`5$-C^0=F z(YWVO>S6T9{u%Ul32^GUeL8-Obr^erh9)Pbbc8;j_0E&&&;3Ajm}Zf@Fg}=vu5_RL zA%W6E)NS;>^jJd!>JLczK5#$y4KJ$NWL&N$u)xIiPLd)@2!FRNkps1ztT@o&VWB&O z6OwJ{RvUW9p-sPb_4q(Ep?j-z|w&2V31d=v%>j7C@MoZNE~?GMCBp5^U5qh-E}V;*_v0$ z1eqFq3;>A9ItiCD&SB|abL$DusiHqp%|l_(V00fqLLLERd3?}4A@2)L5a~1c(`+ta zO0t;%G$P7+BgNdyhjL?Ipqq61@IM%?~tX75I!Z zGQr8oc_ChNa3RR~Vt=^@{{Rdc3W!{K$5H5kqK<3k4iW)!1JnffPh?>9SD(oecD3(627S+jtCJRo!tn4`82O5|I) zhu>ff{8&%4i|)+f`^knAJC3juS$ITmWl8 zfp?YQ3!xK)W_RyBb_!3ABj8SlTWFeUNN4J=^>`FtXbcDjb- zq;tc#rAHRD-Vwz9>7xO!8|cqdOAWDpiN{~W)N>;P=uWc&RbEgvn#w!<61WjEg1i~C z7LrWk**?hvJSh-7Z#WHpNN>up)Cg8QbgZ~CFvqxS?tWSOl3S`fOk^8HspXUgj-;oET)m3{yupCx^J=uCK+;vpEOB%E z%(UNO1cDd}lof`j!XD;EEp6|_GP}jpIo!8wH;ktg8DD<=4HD5bvnPS|%+d7ENoDk0 zJD{%zNr%;6UEoZm91|mKb8uMy0ELBibFjeQ(yyoh(x+kZ9$$neh2Xk!2H2RQ+vPi~uoG%brdBnK=HFQj4|<2&qj z8-MZRuPnFIUd9?du)xHD6{;s=J9u!Xcma zg?%Ca0Hvp+Z=`V1IsA}O@rj8t_+!iM8Iad|K?eP~K$gfHJ4A|6-ghx(m>HrJ$X^jV z2nl`Xtm9JvXCOqR8aw!$^C|}5Q+bk>xP8Z zf~MkMnl1%j&YLfjG)NDm^mst$Z!WP3mq8qbbRwW3o682OvKF2l1kQ25hbi~OnKsFZ zECqCkqdo=E;6bc(aRn<56ai)iHgZepxL{yUXpZR=;{5*0T$+S4mZj2{87Z_RDz?0#5&C-=8!4Zh8=VN4 z<+F?dg~FLw%c?!4gX|bY)q$q~!|bQqO``IulymV5RduvQCPp!OA!6LbgfR>iWt!&U zomqA7HpS9+OW0`2;Ty{P^G1QD1VyrBLOMZ5w*e;BI{L9-$UWj(>ovS_iM{C&duC7Z z0&to_xP?}^l0NsL(e{V9Us?M+y*c`FXm0iH~ktLhU;6p0wbqjy5`!UT7m0kjo6T^+5u!@Dp>XcY;@`ja*apKk_RWu&;U zTsBMybh7pdc}&vf!5YjHSw~@bF__NBp$xPlI56GBThamkKg9jM2!*aKc_cFJfKVwzB4LbMb=kR(7LieXkY?*~(pCV8UF?5tTEAxe(WNW!Z+cpQayA4}J`$0BIkfm8u;70Fv^nE5*Y{%zr$%z?q9K)U>d3 zG(z>CXu_g$mVh=mn?V}dpha`FGBjg`!_11t==p@aS>M_kK!&3&0<#%W z66Hb4C6F02^6`QL3?s8F0aIJR#~0gzmZKa6BqL(nBqsVH5(8rb4wxk%VZ`kGA(>8} zY*G=t?t;%CCLeD!JVRW<6kBbB2#mLIO$Q)g9|jDb?EBeZH5}(RgAa7h(yBiZ9326N z-y?%hoHUN}C+cN}hne>i+Uq-Dy5au-s3}%Mr29N0q1eH2kq2W25cKr@P^|%h)0D!3 z?p&RvbX3|r@CVHUvJEnt3j4CSD615#56yuw8+X7Oz(1o6Jl}94fGsxB% zJd;d~Ap=VWFSw#hX$SqE|X6tl(ca-oJ zI1m^)qSph<3*7=bOR9>}TMH_v+=~aZ44PGE0yXJZ3hc%gSay*{j}t zjt{YC-$!?n{LA-KNpStQM185xna6#`GhERkB>krtebmqAfSkgNxkgUCrAo3`Rm(K3 zXV~E^AVjlR$_<_^o;PDZe1wu+3pVsLzL@B9AEtd9LmK$8ib~7gWq+1hCQR$3n=-cs zZ6d2D;|k!j7EaTF3WNEg*K*F%RjNyBHTxJm_TmQ&#mfe;kdQ#E&oYQIAT}}4Wtodx zVR9ZBp7?DoT0ypB*(*$2`0%sLnRSy{tR5J#w<&}RG>4*y*{E$))r~oQk=YR=5mVV~ zuLdB)V=_-SfP?jO9Dx@ah@mg+Cch?Vg3FQlpJ{{cm_RMZ@|la-nfZ+NjI<`5u%CT_ zzaleKUhENr^)|!rXWmOJ)B4Qg21qUM2u&>ed?Wcm62XiV)tTir&MheOXEuiXzr-;y z+YKj6Hu?jj+n>)#(|u+a(QdyM3I6~J8C4jdCUf6{$~Bm3~?!(M0e z5OoaCyzD;uef}Cgf08&A#7z+XrqZX}!}G!W_=rm;WtF`c#341I_^_F2$t}+<<;wyh zXpjutw&(K|X?Qi66Gqs5%joHjxIItM=R5)!p0V`DpksK`hs6H?l`_AA68vk-Lcn?G zBa}A619us)22D@WKml2F3ob~C?un7g+gXVs>Qg+pbAo{mB&Z7B&;rleC^h&?LRD!n zCQ6*AV-XcDdQ!llluXcErpz4GdowI(Xv+6Ql$a-=IbdcZysnkHku!@RV+^2O2tJcX zK+kAt(qpISHuSxzA%Xt@fMHqno|)-fzOxJIzJeWp@F)D~qXXE$nkfM04vHM=?>n!U z)EnuyYq&%WkGUJ_a2nD9p1p|Cm5|O>w~zi0W@mwTA3}9J#5tyI8}tLL`UC2ZOzXcu z^>m*~U}5$XKRA`teF-ROcOQmyASew(XkS72h|`oe<{*u(fSV>Rp|Cxrx>4pCAzPRb z6h}qFFD8QVFv_RgDU|;JQa4EUz;YjXmr?5-JK%rsKGV>&dd4LCnVM}Ox9c8&Gl~NM zMWk$jd75YE5&%>+1lDHOQ*wiD)42y(bg~se@u8+Z!(5`ni`^NteCCvKGMbmcdNjFw z#-Yhf5U!NU8nZT5AaXAydMLu^Rt6L^xKv}S9+Cs9+e3ft#{i+FN2LPQ5yVJsnibC; z-`qx_@%`bZ4SofKdSiYd?>lMRLybEr+fL!=pF^kWa<6DJIajRuC6_hb(b64WBZ-aU zLhUZDHeV@R2|ONnrBU=a|eb2BNDd(4t1Xx38~@}lO8 zHJ`k&e=0wUXZJrH0b!a&Wn3Dur|&NCFhJS-k%ilGBM!;U!jE17a zQRHTOV|RfaOqE$yUKUDLGeLl^D8B48VTX8vFhv2a#qBN=Qj*~`DD8^K^Mjmc=y;A8 zqR-kK1`{`VAj3&uN)3=s)iXQA!)G|dJRA_g6bGn)MGAXEOsC9hR#v19^616s<%H7= zU1|CW+I%J1GATV%r0F{8aNm(eYB84U!FLLB1i?JcUGa2CM?f9EReWFji6b*;xW{Z;DX=30$!}rJM7j}yxFr%FJ4kGnck$?7V@p^Lm+73r@)S= z(3uZ;Ssa=nDs3}+(Ssqx;&Fl-su-hPi~`V%oy#nnEr8i_=GvWXbFlq!PVj{;GSZ(g5m+IFZKTb zr7-rLN{?^SkieSVu$U?m=R^mgLBOZrF|XRArf>R9bx7CudlYqO)_h?HQ?XLRljy?yu$%!-`_0XvawR zCQx>8d8lD%$|dm_LYvG+BQ+BiDB&p#d$0)AxZ`?r&*2SXinA?^uFR*01$!fVL|V4> zg!|SXVIBbPNJNW%5SSOl73d#Fqv&?(Fm>NY-U?g{Pogm;VO310kHUG&Pw1N`@dcsy zjOtTZYoI1Z4T8bAtDG5CpYkXCI6E;D03B*qug48n&NaLye6h4&5Z8?=#6p-XRmEAI z^ku>=!p4Pt(ULjCF{V^J5#{-p;^8)iz2D9mu>P2;kA@ALv`IiU%@VvFjaXHxbL})a zA2ZI9-Jn1%M}_kr`d|cQl;FBJHQ_4F8K)U)jFdLf!hu>)iSH45YQAR;t^@9W7=4Z) zS^n24b0;W=5&_@pw0${-kDtjjF+P)ijJT9=OZI$E`BLt$0#*q-#p6jw4ue)@#tN|_ ztwz^|O*36oMw>Bcvdo-!)BH9XP*HCZIMu@mXSf;3uJX-7*^gr7T*tJ1ncfgoZo_D~ z+T8@#JCsL7%+a631Y3D#ZQI@Ou4WAgnL2S1EO`dgv7NWfMo~ynrIv?y|kt0WQ)jdZ^r22CYb}+#DGpB!4(qEx{ zR~*d2j>$6g<>qt5tpL)71w_rPHVui^b(0nU0LC_OhW@8%x9Lud+`-U)@+Yf) zf#{u8(V*#mjGr@NdX@F3)1N|PeOHbBZjY!YLB*k_1_wdwo{|3mmaovbJy-t#gQMvT zOfs$$>8t3@qt>rV{{Zn1qSGIvF(EVd*FlAJSFd0A1TdkcyOe1L<(akP{f${BN7A=f z^ri>?HlL-gil&t-$~ON1WH)910Ad^RZb!7H@!fo|-ucP;C-)7{xBbNGe$$3wJ3i>0 z=eit!hA;V|znVYbX0Rc8G$T)*52wvL36~`J>OQQG(%yE9S9o;BrHY-mhpO1w^`oS~ z`i;LzU}0xqMpUU%v8eQK(^9avm*xZg@E^jU8Xvl7pCSiOnrL3-K=*tQJiLevQchRr z{X%MepY9J|mjZv#CvbZ~zwM;lkH_~HsCz_iAz#~vEcZq1_+r13Xp(5s52V8B5&q;C zL-P(KvyxfN9}_GtE>_ogMO9V9sc&-fTQD)KYu2us^p30g4JS+Moh!rs5yQJ0m6SD) zMN0Ja>%Oz2ZmaJt<(eV!8WQ)w{{Vgt0RI5CXwd%vl16UYt_M@+n{uQ)j!n;=ZoYCG zpKtqz_y_k7^I+P74r%`Y)WQD%u?OGsgKhYP5r4V~F!Kwi!JqTcT`@@T`h2tgloRZE-eel1isi)}KjWX=Kg$0AVJKCdWB&dP{m3u-M@<}zdQOAWF?CB1bga4u5>$TD zuzfuvZky@fGov%11bioUNIYQm7)mEGp#U-bXjux_=#cxvjJK&Wocc=Kr<7VD^X^X zH!Wg~M1($s>#PV7garEs{y3qiD2poZTNqZeZ*0LP`#>W6Kf{W?Sn!wp%0M=EM~LD7M(!b z-dqqq60F(<8n6-7m3t&kQDvD9SCx9EJ5&q}Ft=z%stj48FWJO#dPmbCj!0)*zs?5C zb>AsLVkc-qGUdyOCO4AQ6pzrb1`n0B{-N$NR!jk-N@UcyL6sY>(G-qJ2~Ap-@dwC) z5Ly|QY%oKCb4?8ysrN)wa0#5MK=}yh2C9K)@xXvU)k|zv?-WiTD+JUYOe>%*qYKN> z+YS0{o^0^6VhBUiOk~PxagGC>2p3A4 zVk$?x06Se5+%D5b|u~o(g5;eS4lGAH4 zR2em`Dr9>K?%o?7-^?KZ+oU5|nIyayrfR;(!lsMe223=0L(7!*!pMLgE8?S`7O-=e$PuqOTSAjMVJl&n0>5Hp+2wD*ktZr@0BTsToP#`;GK zm(#YbLl8jf4zPGq8#UZxgeL0nLCy6ICXB|0*c{U}Zl(M^jwK!6WyS~^(`FdD>nTub z4RaZ7PWLyIIR@L*JT}OxxLQH4%++0YWlD+x&OJ`7s+dN#BJV8)9XuhP{pQNCl-~JG zJjSdBs}-C<&}P;GgmXU;GDgmK)Q|Eu`F~M)*Y$nD`yg27+7h8Di}6T=;`o z<41Uqy)qrh-x<)%gsX7ctS)w37g#7L>gD_bmf|%9wrcZ@YI_MFl*)$9J4;YdHsZ#? zoh#*e`hE)=8Y333xIyf_Cg~RhnSs~9n4G(J7Ct)#EZ*VmVVc0 z{SgRavV7*F7+`8yPL{{SQM=|kiKRDP6*~OrhQBKTQ#D%rQl1})r`MTIZaYaHNl8Bz zVVB4o&6UCn)V>+14J&f+JCG$1Di=3`R$U&!IWpI1mUS%B4K~!i3AQ^Him^rU~q#+UpE;qdilSWkWsF zcsts`d(4*HY!D4q7dEt)xdU<$7V;tcPGD3wt~5Dn{O=InUTbnEB^u28vPkW$3vRXm zofi{e25c$HElt)d^1m>j+Iwj;koubud^rCAZ!sARzi^t8{{RFjeZj%yAH8OCm1D$d z!+I-eRdJ$j8sy`=qAHmLGI9oi*K+zQCRcOxyL}nw1+F)pm>>gGTbI4Q@jpGK&3LFxp4KB#u z-eVZT>v@!Y-||WYlXkAy<3+rqppQH0 zU49s+@kM{D71;MhX-k9_(BaxIiLm%%weG{lZq&SgGH@re9q02a(S71(=%;!S-th_U z_)py;di*gd5ZC2ICr4kK5Y}Y%lCSEW=ftGhp3wC&uKg!j-_<<E}!GI+=s6xISX<|0-i<-%Gw651RLM1gL@dpTbIsOzHdf2OX)rw@tRd`mZdqWZ~x zxHqn667wtlaZET{`mA|z{{TsWyEqW-e7HNYH*4aJ8AYp3DFy6(;oJBp2ge<(VtPN+ zcrd({*njMZ>hRPgm-Wy2@h|C!>opq9Ub0iK#VP*)30<(caiL4T^83mPqGbJgdYt-3 z3&dY|72ux)%;2cffI>auwPCWsyi(&hLApfE*Z%+z!mzH2`r=%kF<4g7E%y6v16FO%2nK|C5^mFK%bsZV_(K2+}12Ur4${h5J z-uO|g6xI+1@&Y-+4Okl^FxsKEq3ECdeaq_|OxfuSE?=X41J}>kWv~N+fYGeh7+`5G zw~FPN_hF}D zl8la&c9!l<)^Gu*pnv2TC~jhXO>|VLQl(19u&$jzoT3kOfYopf3;=PAEwG6DLKI5r zSPFn|#NG87gSx_lY`vU!loq!VW)nsrDUlgmAUz8I0Lx1VY4z2H=eRSUhA1QNnX_<> z3|@oc6SVM7=ir~RU}VtuK0<2?f}3J{LqD*Z&zPbeM+*eXJi*=}HI3~|+9BJ7Kv9+o z=0rL{RZv<>>-u8`*i4|u>D3N|7xc|k=T8pdcjV>M~b`jRLO5)j+S zcbS0%xziQ0M%9=rNhryrxR2794Zj!+=!T(eUc;cCOX&cVz+SCDqc2g&ST3;iLuO}_vs4?&b3rmTrR6t>oOdAyRU!gJPXFd>yP*-`MKwQ;2^aAzjJxr3lRIwPv zo`He&M@UFGVzSI@sTK>r)Eyb$v+Xl)0j(KXx>GicH~kTbgQGKclc_Yr)eLWH!8*Mo ztP56sNB)S<(^x*=zodNujoBkJMiDl?jr}jtYt3&f1VyqEttXTgxxh3l0~EQLs{*p> zKAet?Yck~Y$=C*~v_K~CKJhua)s}gjYHDhFhgsG~ImoF3jG;N`GV>Wt`n0lZz(H5J zXU8seLYCKO%fvh(`6DU@+cLuJ*AR{Z^^3_GvLb5M(Gu{^9x~Tbj3CzGG_P|qsjyq6 zL1PZuu@MhlrkEJ~%Ocpxmr$Ib$ZHc<(+qXyWabGWSA+oGeuJbFqI!qWRMXH5Jn1Q5 zyM6Ic(M`LVMwM64&+$JmrP-UN`p}tX8U1B(lFP1)nQU`lweqbhz zpLybu4`lqaREa};nN`cRx2q-3!7^>1R}jQrNLb?2yj9@~kHCyyp1v11h}M%JKJhae zbl;>N@%f^PG9l(D)E1xD`Ar512%K4$WOi40&uF7;p%(9WcQ!D| zDtT0Ud31`c)B)NEU|P{7(a4gl;vu#KhA(eF5!ax>(IaH6G-HhgvnKxlr7%?eri;q_ z!yh@&eJX)#M9GGaZ-~S>1IvF@vu2eMPf<7Q1xLI{*WxI7^C!a%Ii{{UPF zH4ovZb0mjvk?x&NMwio3CddMhP{TcG$a}}oRyB`|1sfFc1JYwCgO+wc!#^gO$8Rk~ ziI%shy%q5jp?CBWzHVnukEc|bw*o-8MsZzIZ@4E48Dq|js~n@rbvG* z9HsOm07_h3vfzreUr%Ni74H`Ys^(XJr7&LM0l$PK z8^t^I8RzpEDY!4(h<%wTxoJpjclti22H-PJ8mj2|&8r^uN-I{=PA zalh^H3$XkjiC^nt7(6s~VZE7e8xZwuK0kOv`b2Q z96xxVy5+06GA*nenb}qP5-*$h>hB?jME8VcWz4^VDpwEMyI~sk>gH|Um{U9>YV=@A zHx}4xQ=}q!Lk`wAG|xImH>)p)`HG71EMupr`W0T+=4su60_a50k2sa7blxXP&=vNT zmM1J^M7is6Uv^~C$Yzzwax0aBcna#jwdFpnO#7GrfGFx;Z=2~zAuBqAinivBu zAUGwxF!dGl;Yz@pU}0NB+Ved0F%YN&c3uGOr{-uaHe~sS%1`!64|xwx;-zh;pZNi5 zDk0U9J>DaewtuMTk=a>Mi7YAjQlYNSKPrAOJKVm={X1^(?H*R*w10A?5|d~Tn$7PF z%(RAnfSeHyMLy4~+;zQUs@_;>jfI{N7#X%+UbGRqg#l2uo~<5g;+Zvo{J7}qd2b6=DZLp0ThK!P$3l?h-zuA zXkne%xv2Y41s%r>x|}-U6rG=a4UE0c_Ds*c6|^qbGJPQo4`{X*CPfunaAF;XqQ(qp zuKxhkP)*Bde{capj1N@^x)S>7jv%k2pz1K@!|g%{l7?e8l*r-0!z~h`RZ7AORD1=#}OM@*3X#W6JqNVG* z*Ff$FC~^QnO&B9a0xhOrL?BAQ!#{3In+G(PP87O8Vsf#r{a@;4UA@(sI$NB#W^&Ej z-VNcWQHlwqHz>)I?|-jEv_nH`2El$l*6hX@5QY?d%3ozvK4aKD9lnggmn^?x;7t$4 zxK)CwQX>amNtE4b=n~3*N;0zPxu)2awC!v^xEeBepLS@tfNeESazoSRm|J;Eb|cxN zB(pb{M|p#k^V1G%y&xfE$c~4!)DBHd;00ZGf9=GkrK;-@IuhEo`hRj6dz=cuh2kZq zxBMU6rjXyCI2v?ZR+DGU6~BY_Wl=_*W;d^Dc9p9SJqHD~d*B}E>_*No^d6D*8w?-y zgu=@AmJ%wm-8;gdJa$~jJpv#QvC5(+&R#Gu*Yi%zh%RRiXPCINea%C12Jg=tO=stk!JxgqXnZhR|06eeR!0Rxr`Q`A0yeWF#8$-{75E*`f8Le5OTJkbqUC9{{Z9Y(~D*z z6llC$UDumf?XGPQ)h7#!RexL0UW-jc)?~hFJiUl;!N&Ms%-B77^dILal-r{n<*@4w zpQmik21D%~qT3bUupfvuIe!kEt~Q)Iu{L$h`v??tfsX!-%C(`Z61qM|PY{3<8W?~| z%Um4C4sFQz-Tp<8NIp?kpZdIobprQG^*lKrEOcpWEsh|x6#*Dd;Ii?7 zqv>z4bMTd8Ljib4*M@WGF!WB74WAHSfrQWejJq7f(Y2ZUx?M1uvdyz?)O{VUF(M59L@*`ir}E7f;rW^Zge3t0o#m>xkg2Q}n?CSEjk`7CDaMpq_oyv_qDp4@ z7*K0i{J&kI8hTX$vJQES)_RUe%vs-zGWODsXuHYmFzH80&i7$gZgIo44l7ZIGu}N3 zY6z4Yn*-AeXf1XoW=(ciD<3oTt+6n5N3YH`I%snPe9bxS04Pw<=r&>8;}<;*ruccX zKzes6tAW**xV}E@t*Q6h!xIIf{FxY#4HIcVHDZzB*C(SZe$_LM-_`G$2x6i)J1pbLp5Y%Ofsa(*2bW96O3yr9az6{`^xQr!>$L4BA8iO5k zENe1mhg-;BZWk^!^||+*M^OP&K$#B?444gZR9h)(!1YI6mw`l4}j^f8uHlAEPKxFXyZK4dkzxQ%1~~=|@6-P^7vexe2bi zQG@4NRmFjxa^eKWf}!>17q)JN{KTpbEd0R-C(NGf`CUtb&SHYnSdcg!9^^3FayMiP zLITbecDa6KsloF90GKd|tt#N8MhG)e9;NTlnrn$Ucb(>%M@4iZtypN9_|cnJ9c9oh z?GzT~$mVKFYw@%A;{X9_{y(TgH~Ke6&aiiJBk7T$%f#mABVhvueWmPY~-z-5#7oHlpj8N@Ts>+s5WOZWc(a4B})8FO6Ud3DKZ_x}K8-Xkj}?0`lZ z23V1-%8v0WplZZxWytX_8tQ_2f}!a*bOT&PECyT40^Tam1L*d}+VA{A15~yw0B0)( zgLMu2%7`Uvj@XlaltK+wR^vk29yymyysreRaouX-=*OhhWueudbWD8DlWP8EMcd&! z0k`nxQl}<|;v^hu-k35qsxBA`tjF+&(vH*JW#u%_Vim|e>G*<_J;D)L(O2k@SZ8_e z$omnRrCcL5#LR-vY@>mORXn@~+|-Nk%lJx4Bbb*53)4j)Xf%$!;a}t_?;K1k2W{Zx zniES6mJ40vZ!B9BBx(_dLC`WqLzxuam(f{5Z$Kq|5zwsnN-=vtYd;c(v8nm!FeZN3 zCha<(G(v03h|&EOiPv=KiB{ zX0S%*?tlY;M9JpeX1uQbE;rnSiy+{7rFk-+PE` z!LWJ)I$%l#`ycKzWG|V)01-4WCqcDhaV-A;to%|+2ms%9RHuf}b3LFgn$-NMnFV=Y z>Ka&c!K-Jye3IzS4U@j9UqJlJjDzKslkPpG0*naz9KP6>g(n_@`9HA(P`W1MwQhP} z^DcvMy!@e!pH$IcqXfKber_aTzR&W*Yy}ZWo6o!3F7sKXw;`9#lD*%ViJW_s%G*tp zR=b9EA0x+A{GfnU!x33VS(>_NyFLYNPOp5kwK|?%%l>#~t;faBL^!!Wur@p+9dMWaU zrej=aJ(B9yv%&hu082$?>9k#JKXh~r`upmTR`6v?m6hOYS%;V!Krs_Vw3mZgd6X;e z=`^zPC11(^01*yBAJ2t;Af|tmx849U0pkZNR1RV3i%YQ@p6C&jm33uGgt#dmIpQ6s z-=9!e!jaF+9rW}Ple}RS=x(px{jR+wgBKazYi2U_=xyoP5Q~Hy?rn zCgZ#3(kWf&?P6d(i4!$PpECkKWx>qPos+T5(^;a701=vh2!fQ&{j%yo5iDj#HF=-J z(*m7O2q<{onTNDt_urEO8Lr3qz?$)^I@9s!Wi2PL^rLOQ>_pH(wrq}N@FgFP>sO?e zJNTuCBk<`gd=e#XW^>%F33YlF#fQEMa~1_)(D}UBz*NM_4_nM(Zzo2XVn#BzfLz^X zOBsD3hmmIPXc0J6eVGV+jdeC-%8Nqt)qhZfhCqg7(^!X2r3I=vmgfr%Fr(ZE{7Tto z<_BOU%pgkN{*2FeYVae!gJH@V|Iqbfu@+U*u!i`(u(6Ur-+( z_lTyDn35VOg&7PiVASfhtr3=UHQ)>{Kc4xoB&8H zZ{MLjX3xAjOma)G^#1@O0L|GPkhuo?k3O9~^df!XD4bN++GOZ-l3@aoHI`R!O3U^U z=#hdvRr#NOkM%E4_7IRyK+6UYi^YG!XgSrdGR#$U{qs4BDyS;1tLJk|hZ!bptKS0( zV{&o{N|gH_+#y9Bu{zIL@-%Qsq)nFwvw*U}vl*no9@naM7#PC3NLcR6(8<5QOM&kKocBUGIUG-*VRIIg#-(9;#xm$% z?(45agkJ9aI6aQ()7#Apj+8m|d`d8vv*HCr9lnjL+6|os70kBSQpwVe$>t@xoc!U` zdM81SRown1sx+wZW&A=OfsxRZm1-w?yF*YCt*s?-Rxz1U2Rj0gw?YQ!r{q5Rq4)3>H#bav4Vp0oYoj?(azLGnTCbD4_86<;+L&HmRv8w?SQXTwZc@zkw(TF}gkE*K zv;d9tX7EApf~*Ls4q0hHcR$H&5#?h!Hx~-eHSLASSbVfPs}roPQ}vv6T-XS0(J4>f zjOC6`h_|%hX*vD`s6*1eqa7sMsnV7~984~-a*C%i#l^sntS2C4^{rhGW47#bD;%u8 z=tpTU>zvL8+bxu{50By)`;rx-$Z+)gNk&8A4vx_dMrS;l_wR;Vq*V+pb3DrJMbd|~ zCCZ;tKJtvv1R*%){{YS`yf=ph(Y%Uc=R`)06Opk#(EZLQ6r>;!-VOGbG|r?mS= zjLseKbP*v#3T(Fn3#g<>js}m^{nB4 zCQ++AZ3T9l=xFWC9M9JV(@iqNZ$gQ7)t9FGDgCes3V`ARESZ|z66Qg)`;n7WwRd_g z4GVl>a2u_!G4sOBhBk}&M@sB0grS^whiu+ats-^Yb#=Qn$W)K;gj>WSx6QYyitU9i@!lK4S#ah(aeyG41LSYRy<{ zE%)B=^iNoq1V#hjh8em9PP*tlOoiOathmKq$k^_|JfsdEe05L3h@bh1-|;l>atKI8 z_EOt1*-+j-b9I{{VkDH?*g3{0yildYsLr%*wJ>#-sE-O3fJ4lv;;7h~6u? z7RPgY+o5Jv{$VRLo);aSf;CmzCS{U6dP14)@5Iw8C8bu_JhLnA(o%~uHkrJayWpRg zivA!?_c_0a7H-7*r397D;sIzm`UGX2Bpx~Q5hzZyk7-{b<01-o$TsmTCp`Gv!U!D@ zz>G!trfm7QVo_x-L2qeac4&?o=((8qfn4SY%0JCBVZI5hy@-JalS!mSPIoUz9l3g2SE19AY>j`wKi}tiEMFhw5{yQOF(@p zmApV$Tz&XJ+C1tsDRZU~oJY|AGn@sD#<^AZT1 z^+R*frS!k9!XUmY$aUs4f+lT{Hp>+?IB3{ zH;vYJaL)l&#Gv=xU(^*(EoabBTBDd#lZFz!+vt4DVmJ_3<6|kOPtDW0EPdIm3e(WI zs@o{+!vToM{LL%(VipKJe;B64(Cp2wTyAvNMGfrq7)LpNfjRY^8SMW66+fw`1(v(U zE&zP6`PR@`~N&8`d4h2ZNcOaL{vK zGM!Ma$4L$1IU|8(V}XSM*P|*O{<7&|6PJ=oKn0jR0mAL@!A(^pxxce2&nQfZ?o8m> z2a=^`kB)W z+8TL)!QZB{NL5c@erB&AXxTHqgGLsN;#07}VK-o!>FwCp-Wia&&oXQ5CY)ckT*Y+Ben{Hw_~v(+a^z+u4F~lN zpbUcoRsvZL^6NBgCChKW^)tTK{!!|7*o^x3bg2i~E7I=?kDR(~g7V9AEz2zG%)PwG z7R|=0qrwBTY|CbOfsj@Jg^f-#O!+WPGTcCJPc%7Tj9HS`c}`iQQ|(|&11|6Ba(HHb zbNu2nIQ|T!5nABoF9WQX39aTO7OQx9NC->o6FcFM+sr73=7OcOcsOk>Ow^x0iLU&s zD>F^%h?t+oW@oJZ@OT4*4Gn}X>0*k|h7tl&I3IXqf#}i8$+VgffE58ZY}da z(oox$R_Z{aS6LrbJ3?PjGvFp^!@?52q1<4U1EiIGr`*7kkq0n4M<8ABMOL$a(rDm( zvD&qK9AQ&Qb;uK+^M=sTn!6wFJ1mkga?5fVli>bff-dG4EtB_X5qUY%keXu_CKW3J zWkQpLCJ@P6c4f0Q%@Q-6SjY0aazco`_5L1wj%@FCJ*AcSQjb)WGGWeDPC#pMus#j~`k z-hNn~Q>X-_hOw~o02E|mYZa+phxwZK+oAqpX-?*l!?^a90QO=JM{5xRp!PS!B?&#{ z;ddNp<&KXNRrzDB=&IVbIPk5~zqoxVu~zi{X=Ix?DPF7OgY zq=p%yd4?^3ha`49%LpG(>w09_C5*-SED3X7k1)c9lO+Jm2!fiP2;C*-Ru_k7LL=wS z@0dWPu@9v;`Fy$G!yNeqf5mJKo_RyQ{Ka|d2=9#pWM#v`?N`>??3R4|}5 zTJwXv)DPuCS2mwB%oKWV239{Tz7awMsa}OlwX*!-7(gMc`@>0BF8Ta(1o?}q;CcX1 z8Dffd7t~2@Dy1%-5I0M{$KVOlblR8gse5s@L;5D5J1obqzCU3i@Girm@}H47jb}~=y2#D zYEE|knfGB(@P8qJ9WTg<4t^pMFl-*Gwwq^Z_nAA0N?0o#GD>GIZSUe})2vu-NL&DM zfe)wd0wu`?oh*~62%N|3(YD!tcBgJ4g$^)98^RmC0G*h_{48(yfagyd`cc!VC8 zQN+m+YTzR1QZN!KwD&-v4$zDbuS}tU=)Q;OPKvvg`HL~%(O&FdzYx+MhR{vjqSrWk z^Zx+M14RQJ8v!8qVKkk!@=Cl&U>$?ZQX`)tAQz{|hYY)(l5jo9JXmu8RRh*ZQuV@O z-=fslCCPSUjcYZNZ}l>IA~Kmjf)FZwR(O<8wTi-T)405q!0jn_6lb`KiYgkLI%v?p zL;S+a?AQ5B;n*L~7wqi_O!mf8ax1yM!!?LgFDOt@(7M0}L1Z+C9!JE~OgrS>0gF4R z{{ZkGL-e$F$L4GK^m+08$8X5PZl5kg2yE{PmN`6TJi_-oMXskO zl3rOv$g4+K^*j!YBGBWQY0h^3C4DJ{O2?TSxuIS-dDJfJ&A!rn)EOF(Y@7kpgd{_?f^ByTpUDEVBOqW_sH!3B({g#qG$o{{SJ< z`Sa>s7b1Q~L8aw>NUixe=)imP>0~>H*6$`Wx|*r94P*n%S9KQQfP~C|>J2Y`k{vn# zG#ZmRGcphpoZB&3YR{s)D`Dmma(SfJ$LhVv!e{{zmc{SL zKb!br2Bwwd%%FLH!fQ<|$7#76@hruf3}>vgt09j zXXJDm?7xI|dvP?Di0aE$CvJD>+IH<cvy{@Y3rpQ?8WkYPqYzA;RhWdE+Q@FHS(KalSL-7upt&d5Zuj~^ij}Y zD|gDF*57`n%WXv78@z!b3Lbi2m^FJis&4t=%Kf1=tn^1~A)!7I(d?0!MM_tSl}^&! zbt(F$QRtbZ+5i>OFL+_LFVU=?=j|?DA4lnUj^QxT6}H*IN{3{uD-P@eEspEECQkx| zz-5Cuff|!HEDOdjd{0KMDXBFfv$kkO4DIRN3B#$t~#R zm228bcv6+^00HF~Jm1UeX`9YiMz#FI100mJQ`#=DzS*@dvug~|GCpy?$B!rOU?*6z zhMj87?Zh5OL|r>T2v`L6n#bfSE>C6z$=QW}h)aM~?)HyC`w-Iy>8P)Gzk$*G$@<;# zG{H4pjb?pDHk8f<%%bhg*cG2rj2+IhOad`bAfnf8yw$`_HO$p-*;l3E$6TldVYu5b{xpve!YSv{2aInnzb zBlxA@bReeE>JXzh+d=u-)P_x8{4wcjhFAv3;nv7LtY%-k5A!dF=+lIWvsmrb?&mXk zmFs*HITmW&*!pAW&qwcwMb4-u&r+f^VL@FD!T?FUpKNR9+bC~i+Y9tk$7ytOmub2q>F^lAW3 z^7+qimV7NI`8GhqmKP$D5W3Jp@D^C{@!40Vx{itxd$Qq5fr$zjZE1 zgXhIj9LJT7SH)1zMMN@}Eui6WRR`NSs8ec&?!EH&m_bK+4qzZ!8BtD?QIMJ~1~Y*AY;DOwS+Qov@| zz0V^J*J#0lslM`wvCAkYEKamE_}s@No1gMSNN;}^6Kq_G_}$}hUhUsiUs^(}&hR%P z;6)C0R;ld*GkZHlYg-}$3m|-&xm zESAWwCVB#~(itwGa)Gx`O=&Xj58S4w&m^ix)_TOCmR;=~X6K;(&e47B zZzy775j9+F!`v_fiB}e~QD{nYO2=|!iO53u4%s8{PGUuV%*1N)jmb(9Y|M}zOr zJw4l5w7zoBwrO(xO^e!I&5v|q_1da#b1rG5_mthhkjS7m!$MZ9K)T8eF~;;{bMq|= zI{mO#xaYc~B|Z1gh2rMYH9=_Injn+>Qt23FooqQ|1ZceFH%P3O+^Z1FD99ii?j;$u z258ebg6ciZna&T=7S}!i%A?HayMRBKMd$+SGzH7v5PwC~m|7rUnker-`5t zufF)8h?X+TF{R=E0C2vOps-mK#k@^T*=-xmcp!^GRfP)s2IlV=jB$hu~|%` zqoG5P3H*$j-d0aTAEBUEC|WXH&WndL?;a~u0y3MD_kL&DsDW%sIGz%m60LK>7PHVC zqk(k2_hBps0uY>ZW1^dNgLrfX1Ypb_Sgt2>L#jwqx3nSX0AdzG2|)8u)q8SW+puu_ zO`t&sDQg>lvkrl&Iz}&TFC(1Dv;_q*()*~r89Hq|NA-&}AGsZO+QomhEBg#IPw$!+ z#Zz|sz?)ET;Rh5uFh_Dy{{RT?4}^*9H1+tUKi-u80CIodO8)?Ie#Z~{GxjXQ(}Wq6Pl|3CpaP?td^>WNvGj2~6J1qtTlH`xIQ)@F76Kyd7m4$Rv&9+jUPSovA z#O#x`J0$HRz)rzC3D}*2aS$96CK*j~!2!q(;58Iv-~tzrou-8^VbfHzeTH1a@HhSM zfe`Q_V#Z`;jY(zQ#zC8n@Hr;&VCxzSDO+cDXzCpzXniDD zKTKa5zVRBWWUV2aS?=0LN9c)SoeS;Ti5tNo5VPk*oc#T^b60B zhx;_YogQ?RsrRfsQDczQ482E#D8v9UvU{V=^~4 zubHefRB7W4HG~&FHTo;ht85RbOeifYe3Og~<@(}#C^NDV=$?b5z)c%{(yMoxm-q#1 zmj3|HY-6;vKQPU$7Gvic$~g`;<00$p%+AS2-Z~D+<7k?0ciEh-O|5%iZ>5vZD`#~K zHI@S=ZXJ-n){$fAGtjz?yh5I$D(t^qQ<8dL_?gmZPZG(v0SJ_bs>D68(45c#r~8744U7b!{n#8IusLQ%rT6_4ph#V0 z9IOfO6^!J2l&!ALCev^HExODZNVkz2#528{n?n;C#@L!WLjy%8)&HC^;GT2qtqiei9leoN*%w+%l-NSAQ|qbe}+U8bi=4 z?D<7YZITO_lo)cBlPcD@^DK6DUKw7ZuF!U$uuD@X662hgE zqm~Vv%K4a=zBa7o37iwH#W~y4bJet~Nsv3u2LM(Y)BdoH zEX;a&;v`7g@N)p_3JG3;SYWle=5P{2zYW~Ewy`Coyfy;AyXk+$?*RGm?#tg z-I+3c+C%S(Y8u#i{V*2p{WXD5qYTWT*N#(erdNP3V7!XtAFr4t_)tNtduJYcF9+;g z&)VAlNP1>BBU@hN(OqbQ3aN-wc5hM-VmjnKObb8`U^iTblO&L6E!iu@KI~!Om?M+J zqHc?D=?WdUfKyE|atf<)DpBp`P)ot3U5Ete{v3SAV4HU}Kq; zl`o~A!ZpVJE71O8ui9~}x3JX{GqU~X9+6tWx61e>vfxhhSY)cf#J^rN^DF2w-18WF z#y59IXNi7o(Bbx;xzy{#{KH?ZlrTKtimA#3uIgkjnfWHbHy<$cfEZiQzjHY2&p_T; zQh6R>f?4c;a=Z=#rdXj8gnY=>_-H`d7);W;6FO$tJYt{-v#l;hX2EOT`gCWVY`wQZ z)(2Qusv5x$h-j=Vz;7DG(rAyKj~L|zUNlLdtCR~5Zd}o0C_g^ww-0&Bra`A{G6i8< z3bJQT?@@%>lLmyx@;1rYOo$7H(>#$+lq#86{APU0hFuyE%&`*;=YZ%zi%Y$ULBk3V zsOmR;snWXUlgu$TZ_h@u*az_tI}@^V4ul7^^q3ednGMb1AGRyDqB*Ug4@$|uFukLq zXd^4{GSJ&+5$5Z)e4IpYS|Ouxf}vEIjB~?S>tpEnX&jFVlel3N$g(bb7hS`h@`?(G|g_ajqOH&3Bpbtoh^8r|An{1++5EXK*2p zb)r#-O!ti15~aj$vvM4>T*c3Tbi-)mE(?v5XQKs@moF%+xQ9|?#MDi^LyeBLAVH|i z{VHW+-wMXI-F}ss&X0%`4wQ>?8F!WLT-qvKz2K+vap+}+m*E!G#pzN~6(fu8DbEVT z^GQn?Q?>V%hSw0NDLMXOrV3!Iu{bP;a$YU>Z{dI!9O(Y%Ayh_LzL6SsPZ|9x$>cus zDe(zdQeq_>nQ#R;6G_Ye0E$SxwG#E{0|G?VbBoZoE-%Y614AiHb|`Zh!Dddz9P?73 zJ01><#TviM4{9(Tk%qlHHPz$LI(Ir!@T<(S01yr0%381F?K7Gse67Z)DQZmg7sLbD zsdYX{OVFm|5z_;p%WN+TYHfyR?4|zg(X$;d%hE<;IYp*0 zAUuT2ciJ2?Lb87G7TOPQp*jt-pUZ(N6N(wK_lKGAiA}OK8&p;DHPUIc%JPMqOA+)t zKa8Q%pi?sIvsL9`TW0=dZI;n#qq8@ISnNP5sV1_6Uw1NQS<~$dJq(4U;>k7W)?DYG}&pg%z zQC}{7N3BweVBZl3u@eGYydu>=O7u&taKev$$6HBua-gsOgq@(xz z(`aWhM@o22pLn}L`h3p-E7CnNV#)`6;r7XVLjuzRn3th4+vY1&wPvx^&m`@8`B;9Mj zDQezt;!v6;^q5sh{j%8RRLyJ>n3^YNR$Jsmg(g*H&TH=KgI{tXkG8{C3JMSmb=OS< z$W|S+EIfSzU}cKQ7*absU&$>Y+84vbM8@d9iM+IZ2w_W?^Tf7Izc5XqMomPNDEEI1 zLt;f(ecivv2wa5Gxmp{~d>>Z;_hK*t(9csd*P>PUjrFv}@8o8u;#Zpsr%ZUwc{cH^ z2AyEl7zS)<7!=x$iAn(;_!Dkd9HCQlEgw0T(@*gm- zbS=1G9JZ%z!wz9hTfm93E+=FQpj+<_Z0^kn6)tfo3baJ3_M(0n*g!ylG?oWlsi>FW zuYzXFVe!N*TR`6b03-nS;AVE$1RF7J7}!&PXin3U-=)lzZ)|dIM3{cpZj9LW43pkj zz1rCXIe+Mty{gh!6*C;|7}X^tQRyStV;`)p?9sR6?14391byMs!pU!v6>t^*092Yz z*=1$Kt=?+0j+DD9&d8LP{Kl#ZU8cPy|~Novk(E2zJ2r%^15DE60eS39KY zEQ=MlDb|6U%p294C=ektHthrz1ytLj%CuB_!Y2)-XB{D=bApQMt|KaSfpZRfzoagJ zpfRHa>_is1FlpWz09cTOz~6DrqS*VQ0OlT*uPp8+Ev)pPm;`!5U^twf2crq zJg)O3zX}o)L68RVj=mA31luT^BlJ$8R6W#EjYB1_xt0AurAGb1g|Y6CM=~gJNWxoh zL5ja-vgvfaH|rP%n7`syqceRfxCRH+pFAHoi*O@#r0>V>qA3lm)#iN1g-sPpMGtPz zc5k{2BTtZbnMrxx1P$JUyw@Ic?Yyo+rzC6@z2JZmk%7^Cys-@|C0(Jp^*K2$MR!h$#*2%5Mj}7g@m( zsZoDe?l8tk_~~iN1|x!6vh5i`*Dhu%wpp}YK(%KiFnL*5lo?k+3w%nis&ycf)Ps7H%v^U&6OX~Ehv0EEJ3>Q58Z7rOvD zMt8$Ho1#0)fi(9w-{Mqnd=bW=vINt6UzwlwHJaZ?Qll|YXv(v0D|1xe$K{W>4f%v8 zg)8qaJwCw{I>OuFBc$wLV@n*_IZ2yBu+{7sFfK+e{$DMWlgRD7^H}c{#=bTSb1n`0 zPcb8B_tGwB9>ft1WNC94mXM%(z5H~&sM`MkIhjpnX$e`) zxAf_}3$NQkiP=RUJhaV%AgnbjztCcS~;bic|w=R2}^a0jgs6pt? zpRdG2F(+WqIK%Raj>nSj-;X-V?Q@w*5A zz5Us0b&&hkNA@Ai$9rX7H}oFJ9l|HI`*$n&?wPN+r70x;7}$6$;K-bd!iUAM;L|tN zBai)kAGC46{qVDR=}n889=`1D#PeT&{xsco3oRV+yBjlStGzeH!#`+TBFRm^gn#cWeSor?oQrw&I%MgH6vTaM?> z2BgnLOkfA!Ja*b_d&;2c`s9Fe7Px_06)9rn?0e`qbjrJVck$nc2b-mswe#U`+^dF% zln>POr&J6)>B;$T`nQ`CGcHPprd3gG?$#M}RSv)B27bHs!eoPWszm{7NG>cb>*nvX z=0364PoHW;H2(L`x9b~zn!XFT|8&h%_RxvAtG7!|-#D>JWA)1~CtZ~uB~_(DzjyA^ ze|hgnx=QJQ@KVk8g8N%q#oSi-n}dBkV>QWb+4JJ>@&)jF=Um@XkEI=E3`0s%r%r!)Rk09ZpX% zVP^?7>#>}QjO&+z__nF`y=8`4KX?K{JJ6`d~S!eE;4qi4I3<%c0^N3Kc=`g|9q zwz=toM&_s+;j6~quYV4xw%8hM)sptS1$S?ihF)Se9P;rfV{G1V{+%ROc;L_p&HW0r z`84NOPocc$=3N@?4V?j3YTV=u?u`3{9r~hrIcFsQ*PeTC+EYyo{+thc{nQHXFnY&1 zTW5Ie@r8dcUL7#zrPbsW(D&|Z3pjfXYwpKL`*^Qu_VAwIx^ab}{q!ut+?pawl_J^k z9rlf1$Mg-2>JMCtFYjti{1szArP{j;t5FHBzwjupTHx=Kakfi2nXu+?qSGop^1p0W zV8v(uS`ub_7VncXum0aLOP!TUq%)zPZnVC*LyEl8 z_(CI`Y!&A@<9Pq}hBXl~Jg+ZW)3?@dY@8E2?>jZ|_-m(9xmBM=-MN9|Pduu-pVhYI z@5q;aE4zC}FE+T$?e@Mb&-U(y7xYXt**$k>Ons5%;BfJno8R46ME!}b-e|v`w@8=_PE&RXl#uY?LcJH z;Va>@Z7(=reGJ9z_9!L2dMRwsUV=VF=I1W#F`<;Iq@bFkD^8TfUa@)4L z{OG(gqKKID{AtUod=gD;7`wbS_<;A)g$P^ZpyBObF@}jT`o79db}WfONujJl4#PD# zt^b?mcbTCR<=el1RIR^%(=BIXLXGZd!KnKB`d3toflxfSOK$Uy*IQq%dpAtJ);XXT z{?7elOg6Q!{U35$@~fOT_@cmy6Lqm43I2f=$A53xl05kH<$R~~qKL1YbnvOwkt_Ct z@7*pHWM8>Q^_UbJN4T8=m%+f)w(?~mm~N#~d822V@@|~4R_peJ$;L}|iBiCb+1JI= zmr1%^!=g`R{suAVGEmO*p`p?O29#H{swEG-n;t=!YU{@H-%UxJP`iu!1AQM^o z#ay(pb#^Xx@wIs5m)U`eigjv$g$!;7g zX~z^7Vs2T6(Y`6ksK}7m0Vd*&;_(InOJ>8hETjdU8&`4^mWtD7x{%k!E^%o-XP045 z(` zc00ngsnsS6q@gLIubCQzC2~xwlpNNuw&WTvc!B}Jxz-S}k_q93VtTI_%jELu*cZ%+ z$OaOe&eA-?4c2F}!l;lbiG?L`bl@4QX)Txzq|ZejZ5d`Y5DU(M<`S4333H_<#xiV_ z>~1d(JcubWdl%BG{vB!B3tDf*(tI!^m;+qKX3DqblnXl$%jhTrO|W7>bWJQlAO@8! z4&}Hejxmiqje41qM;EBMV~9S8Y@`{6drlJd$oG==DVqk)fncvuq_foAYERAH-_x%T9YX7WYSDjhn*> zIQle5!kEQ=h_I&*9mT$-JeeV)CipL@K4BzZQ|cswsv1_fbCBfb=+0Tw8BQilzQ*Df#eNBlR1PMaWe6e?B%~(`Sw#qtDQyq=cJ6FOc z-T;%d$l^+@mSMaeaC|qt_EsoeK=vZS(c8n=tWe?^2ip5$ZzLk)R@hP*CSi?G0IstH zM;uMm_Cb86c#1y6GsRljO}1bxarH^!U>~6&XyK*F;04_Jv2c%vU*Zqr?Pe} z_9r%;ji%&s0jZ13I+0Y}jNZ#w2MVBCs?rZj|N|4$Cj?V96;; ziimfK#}RC7qZITQabwp+tpLq4rAgT^C>ZWTj=+{skHVSQX)zC|24)`l*2tiJ{|Tp6 z+-2C*YRZ=h3gixi0klsVIC{%>o(ZHbsSV1$B@`pLL{@Or4d9pXfDu5wfyAT|Ub#-Y zPligNSo19U?O80MPt5<_K%tf?%XF(#ideCK)kJ@1Ign19M%_O8V< zW^qh0Pte$;;T@`! ziki;Qp@eY*1!A0#>o4T+2mYpupYNft_L!2O_3tE=R35WlDu~@(n}5BM-R(@`Nb}=* z#8_}T27>5dohR_j2bRc$=nNFCZy-`+6G}Yni>=BmmExnh5;`|R%H`9zq1;}d$Q~eP zt|5@3&oXQRk&mwJ=9bnF_(CoPLWcg(zSg-|J4ON#4_8`G-l^t=4rlE zyhI01sHu`DcgC;)yy+uI5K^7g@S6tJ`6XL9skwb-?4&+_A+R zQ}CVYgQ9&70?#EdPTxSl+*D6{%eHy^UA<+)Z?52*bvdq{PCBaMY&~#MC=e3>ODr(N ztsca7Q&KyYl!qZfWxyDi0}-J$P&AXrpmW3CwKKVqG_=MGUnm?-%r|~PpImIr=P^4v z&mT}j+(lxD?O{*3rgSVF&@KZ0-Ny{V@&vy6`Ftk#f1YmCB}sOxycGn71FqDr9?Rq; z*$ly_h)4v{9Z|uG241aWfkuNscrYvEU8A|57h{3sq%CjUF=|=pI+~0E!T0ExEr0#CC_fG%KrQHG<$Kw++Ap35(-#{om-3&p?f#5@KN10B*IQ|y#31p*rc zcF0|b8fn#b7+C$L61WWhJHV)U^wEUvk~+~O5JIdOlh5Gd1(4Pp5`?2@dThl0`s)G) z|9=7k&A%h&JVpmitR5@S21=+9vr5yz<$6RE9rf}Z$CSyZbMd90uqS-X0Ik`i(lT+a zpohO=M0SX({jnJ9wCm%mz2ZDT$2t?f(qdPTE1}3lI;)+iS!88`jVi+ZPu~ zZH($zVHOTda2dAHZci6p-x<9}AHs6v7`^1-Rf8lUHEGVCAcWAum5D9hv*&_<=TG-U z29SC00%HXbuqpmHZj4YAXSuC_0iD~QF>h!tyLWnFA$m_Q;2Q&oY(==(KISP-Cut!b$Y1VrI!Tb)R$^7p=@V2^T(G}kUvGcq8hqz}yD*D~;o3_-J2%wdTnGp%_j`2u& zYtYu`z42s}SW-LS6*YjR8(O^Ud3NW=8^(Gr2eiOtOK)kF8 zYc|#(u_~R`@zncbzfKBHx_F^;aS}mnmlpEuAuMwREdeq;)?i%%Jg){?qHvw+0(G?As3ehT7Clk#6_gNsL-erV|B&_%mzP7t1VcgDBOytE-ieO9S(<8M;yi!<{t)H9j8ui7y*2ASMy z12W!}@B4$%7-OM&!PZ^d_QJ$RN*2awHDyrBRgqU0?1pgK70a;S;tLnIiaZZmw2*d8 zKa)t8ZawZxk+=MJ_(`zg!+)qDuS?xv?nMTWd8QeiM;}fMgbaZc2Y&{Lq78z6PUWqZ zUO}hYukI)K@nWr6f4^jQHk{&IteX}l{KzQXj6_;zl(^%FnD;sbOb!98_B+JU_ECBy z9*`WzOn;jCA5B`i=hdP9qro}MpcI0zbbPHihNYU?ZOFCotKaw8>I0ZfIa}7KX-_ zN8~TV=1c&CaJU{G(D+Uv6x~bqVu-p3|M>#N2d;C#-de0p;t~X0NeuybqzW7`^Won~ z$k37u$2HKC00}c6FX-b7!>%vG{3~(+`kDGJjgaqFTzEN?Zqp;IaZ3N69f9t#|LYQ{ zaPV{#pMxK4;(f(m~PUWYm1;ldpY_v3OuT zD{nE6S>6^#Ym-4FTF^KD;)(@8y%Eru3YYMF5N|IQ3Ct_16$65RyK}9NPi&XokC;+l zfny-R@LnV;m{lb>>J65BiUl-D_z+Jx7)@D*<<)bi@Ba7TiC|ltVVdIiJg#tI%9?@5 z=%8^WOkjRs%A%BnSO-Lg3k~TX$OXhmX!)_VnJ9xtj9L-qH20+NuRgNVGhSlKv`>*h zU*{AkAnW?yd>Nv5Mky2r#GO$_hYhf7Fa-#=G2T5iNijaKF>b&y&X0fPuV>^R5QDG! zrq4yE`+#fOTjV(gP>a zXL`8XrbaluD}-?)0Ll3Cw}ea%Esr!P=rm){fd?Tl2g1yk4*yiWu-Fz@Z}uvE64#u~ zCfIJCBJZPrz%s;q!9XD3W^d{ZiIh7g$>oddv1Q`_6PlFhWyO~)h%6S3#d|Kyjiop) z*`e=zG0l`phtpS72TNo22z!GHf8rQooEbRTZ%*9F0C^|WVF%$?cnE&Y;EJ*NSZUc_ zu+eV!%Qa^wZ&+%@;sxT^F(!|WJV+}f0Dl+Akj67a!0&Rg(q3SK5Pl`n(#1gc{?`jX zR~&H_TPr^MYz`*Fz}eG8V&Gq5) zcn_U>l86fxm!bshJ&XvPR2aw~aIo$)j0fQlp!$VL0-+H9lDiVlctC)BDbOXUc%@^| zmCed`;h=M#P8suo8IiAMluc(WZyAl8h zD{)vHJ6|7*XNo0FLm*ZV7N9`sN~sv+kN^iVaO)WWkB#lnQPQfVdVMKG=nd3=+m+&i1p>Bs zF}6|+z^x?5AP98&E00l;e^PcYCqr`X9XlrSKHa8bg9UXtAAXq z?Fy zvML{Hp1Bq;v*FNJ&DYPz>H041`DcW_fI1oxg#E?6K|l-WKpBBfwL_&AI8K(^r2uR3k>eXfcRw&+Aw_a+Con$|8DGu&~2#rWs@Y3vnF1DR+$a5w8x z3I&7XeZUI15>-&OIHEDrXOm^Q+p@LgHgam+r3T0-^Y*=q17b5Ncc{0pmd?eBa1-|4 zx8u)bGgnJ{gNUvzp#DY6Cs;#R3WP2ddaX|`teoNarT$buY61#g)Q$_gTX@#{H+W0Y_Oe zM#fTTWlH$eX8uHiimMJx4z41PP?FJC$<#@7fDs+k64!tV76^Ysf!2`Dm5!3!e_#K^ zf_S1oyCL=S+IIMbD>6DnXM`*ao*}P8gOCPDcp}nL#T8M&?8b{#PCebH~6EU115nNCOB>gTZ7S5g86DU^pJ_drjEr;{v-T{~Yum z?UG(DK1=t?H=#W(9xt^VtkMJdS)wB%iH)Q{hr=Dps{;8@gcPR#zk&m{n zhC5LucO%{-0r>oq<;8DpJFeCVl0qzfs6AYVD5qmhL?hVfgala)2RO`CZ?|F3`}eL( z5D)>yA?9$p@%s#`p^FAtUV=Q^hOa9o1HNe0tuT0kj0a@^g~&%$$>}A?BVaByn;SXV z`4MGIF+r+;|6aZ-D5ZprV&+dXi~Tl#2>d^9J^C#}9w7@L#sJ!y>_L=)X$B>!c%0?S zx=^54JkT_LnJ8BwD*hbGfGp1-C)xk8j{``6AQkz>!2mzuJxB_eCJEASjs!VU8I`Q94PV zNTEG%^VDsh`5cMQ-WEK2QwpIG+N_=DDmxe+Ab3G|1T_H(S8+fakWppxVfs5lkLn~U z$y?&P_EF5MV$}^IPXRk7B>(DTQrI4=XEED~HStZ*|3V_Kj^ZQ-%TRH=n~!P^b(ZZf z$;Hyx8^yo$ z;TevwJo{Yxas~f)id{vu?WRO1YAM6;mD_l_dDd5n0CoNCy0aS2wfV@p(*ArJ_2Ccs>7o#l^?TSK_YKj}gHK^P#=byU-R_N;BD*Rx-!)gXd!VDb-~-+a2kv=R7E3)ap^()it3f$d$Y=# zIDj;G$E_SKp*Dlc1|_US~SSNwqwj05*RQR;rl|T?}VzL}m zHaw;Dpbn6~>a}>(MgiO+;5N|yraW#vlgwR7m949grnGj$R&54^N#1R5#JWKp(2pO$ zc*rH_VrdWx(X(_t0cG-Ch!cQwTj;WZ%P=ZLpaIJnLWZh97#eEBalUjYalHmE@RQ?P z>%hj-f+|yT4;qn$q$v5$ve6he6EB8vDY}0@()_1DeojRO(>P%yi$VYeh8vtdqJ$m3 zPf3#VTFWq5oe~IHGT=c^XIU{glqcMC@m>Y*Z`wMO+e3*yp(SUqq;|8a25U$n*xTHn zPY|Q^R#Ss7V9vdjH((?6--XKUN3KpsmeR)YcL(^9Fdq!nJq)%g)RKiEL1EO#B@m7? zRS^X(bveM+C#TbIE~TLi2C$gSjj#bQu~ai0U2JqbwWmqv8S&|i4dMp>O=1NJ6eUiI)-r%%BDG6*Phk`ai`XnDy zlKX||e(ev^il%j8u+{HX-fdm$Pnj^t$3D%%KJO?jA)~DXKY%OI9>a97u-OW7R3ykM z5hRC_dgS`?`?F%vqpQrLItflL;U5(P-}xA1p=|Qe3!yDmJ)Y0WfmndycCMvC9#obY zkrhg1>6h&@j}m+9=vq$k0}Xb16CCZu~xb6Q3F!@RW~2%C4W+f zR$qCfU`F*ZBOTqrV42d-N)iJ__+igU3~dOxXC=KDw}IDifhi;MkqsG+<0V6#)cmQL zT3hOFz1fX`P9IFl*%E3yW*PRvK3_n-=^~jSMWzyjT%P^V<>%XhseoC!M=d!o+SPwz z3`OtgL%a%^SEg{xi0xR*nlM_sJQrJX!FAoDNHh@ni9`TazgZ}3>Ftzep=kr0{d@t( zkcFi!;KnT@0a_n=H2_Bayi}Efpn)fz7xCfG`JdWo;!)%R6H8Ce72OY~0cv}&B|?XD zt4V$L-0_Q%_(t_>z{3ACXS-ua=OQ;LU`U`M5&Pef?$&n-TLn-Duu&lJ1Haj7y5)Kt<_Nru52`;|Ud{-2%R)`gR29MBQ9M7m6r8*UgB?*!9rOxgqV5 zGJJ(EhCUxI!~<5~nOs`t-w-b2+HsD>eK(2J7P!A1b6?^=f+VQ?8&mKtY2bGyW0yI7 z66`w00o~gm=!Jw!zzYBeH(+RWT1VxAtw>v#_j(El75q+2Kiz%Y8oDx2zwmnFTOI>P z)v}lO!4XBoI%=3odygXwS~0))rD%m?YrD}TT9K&#VEq`!C5zu_!$BpP5-xa z_)+W<+3Oec{EJ&R@a)5D#9VwvK8`-m{csOxz~M5JWBG&Ecg>8H14H@s3ipT$<`VU7 zA3*ENFth-|48ARF`iccQG4tmA!FQ+whFsiRwfWz%(y6&dj~>do;}%}ZqTL_fZLMs$nWrP*qBwK^zoB=YCTJ2=YQKLtf-+#oURJ`9XeObC zZ@mKoA>o&B&-qVNnv#kg2Z1t!RNlgL>|(@mbK?!kf z-Rk|w_!Y;@4zd{jk~q&XWW>6*#s&g409hh#1EuHl8tAV>Xl@|!TV{f{Bg#^~Zod|$ zqzCnU(U*DScmxyry-700+(8p!afiLl7+fIrB`E!-d`KpJ@_~ivqta)2!D^o!)@R6j zJgEsqkgo=}f}$K#%4f7+t%=~jbCc{hGJ7rtU;!wGq?QN3I0>uwqdw(c$Np!MTvVl0TEj|(TU48cR%o7XlgC{m1PFJ2^nQbHHUUf6m6f=vEX z;CfcVTtYtUIdcGI_WlA;h&eDPB5NZ`V#~$P5@Kl}170z*K|z*>>0kUVkJNs6-(>{l zyptpa1j;bga|a_v-}AiG)A9vKo96fH|4w zbNK6+)4lX>Q*WFgQ)2rTX7FqUb?iMTa94h8CkP3EBR^+|%dqF7KxrUg5uIf~Mk0>1 z_k<}O&Y@-24K)jHybYs*ApI?V!r?44T+~buPU3l6`E&xS!7Y&a`K9ey3+9SIdpMy) z?6bLn|0Z(2J%=NMsO02H!b_>I8QqJ4R2}WIU~5aL;t=rI{WO_84{UYiJr^k-3l>KR zWPB~;2pqV74BAOG$g#Dk=#rRaCZ$1!N0!*!^^DGnf5h=pF|e@pvOxqJF+h8wK{`-e zdhTQ($Esgba$RqrIJJ50sjK=?`D`!BNm{$-GR#mY-q^SOsvWmdAjS!2dGSP0+5yEF z{0hXUi))z@+8lI_URSdbfHNc8P3bT;c8?k9@L3#y@y)Z_@@ar4Oi^fhG@MO-_>wb$XJEuk#kPNnb(p6uvkU8!Pfzztl;yD<*2IhMhWh%xU|OZI4^J)dnA|0LL4!!l zYXC_c7mv@9#LwX}4lu)A=w7q?RLae3F8rgM-XG0EWW{sBCgSK2CVu`ob92!wcVf0@ zN(cd~T}crK!Bp#bE@u0B-^7Z7bVHBNoiqOdzcZ^ys5o+B zU!TRSst&|JLglNdMmf0azq_XmSoF|SXX8b3G9|>D31@hB3Bryt%!^YQPh{W8kJ3f8 z=7bqx$hDx}SK&cp&`kM(HNKBJ-YDz|iV+4?5+655R4IHT!)_>=`2ty^IXV>$cNC4- zLpsN2hXIxH$KaIzy(=} zlU1zQ;OMH8Yw0e9`?lguQb+C!MW@!jlE7^5*S%hF(QZ7 z!9B`xZ&|uGP3w9$nmr1IksQrrKL@GF2az(bQGLybJvYf~c|?wWLgnUF_V`VQ%b)Y# zq3iYS?@P~;>HQa9jV16nV<-b3eNw+E(LIcGJnXKIJ`pl4$*1?QfHfMg>I`_7VCJm4 z84`dc{0-;-EHoXge;xn&$Q_2Rf)U9Vz*{VdrB7molG^oYK4`tpMm2XclG{2l*o<&Q zV|{w;I~3wyno^4|Y?M`2%)zJx@VwX@eQG;`gbbr0!xP-~iAa5t8%(hgKwWGtTl#H_ z-Z0T9XNrxjG)u6&v##SR&@lh#DU|3L*fbPMg+hre4AH@1&3li#K4wq1$hUq0lp=J& z$*Z&vLHgZoS#1SGU*pZamVz9?=Uv(p+}R%h(fYV4fvEv7uI5;hrK6M4<18DzsUn!# zzb?YB8`B=7upvw(5KDAJZkUfoW4{BhQ8Xg?oCFr{zZE=F#n8OnkknwrdshUjUx#?j z$`?z_e2GfgLOJ6$+wMUETv%n;-&-uEZ%9$4&-Ti!Vd*~-5Tz{4hP<_?x4&UK6>fzi zYZfJH5L|{T_%MbxjIShp0&rA6DT6D6nyu>cBIDmF!Q@HpI*SH5YdQ~E%LG3E%QtT9 z{hV2i8}uVN2G#JlO-f|N?;W?Rn9?Yk_h+1Uv_H=#*$g4^H-XOS1yN--8Sw~J5 zL6@dyerHWWZ>KUo+*^hnR6glr3{+e?wSi9prKALX>dka0evKpU z#G~^inGCq1S!u@U{pQfh2?=8ECwaYgOql%0_;} zfeXIT8o<3>O`j)BUnDh~SJ4vIAMRS(+k0;ul ztyN*^?P%C!XPH*n(=dNv2MGiG3*jLs_AgWEax0N@JA=qEmU)BS-tu)Q^?~uYg)H1*j2%b%H9JLfvg6W-`Q6ywXF@D1rD zmC3J8@R+0e)?_ct!TS6)zE!gUGiL*LB6HRDf{l=!AP-Lsrgk0awaz@8eaI0Zi$$GEoHTpz>9AvWAFv5 zijyoMZ46}JUTe7CBIGYX7wmS(qQ{5%Y7MU3`40Jt!JNn4B+pSl*<>+*$s%>)IT_^(hE7x zZ4n#BcWp>K9=|c%Ac<^-ge48{oW;b8YqPj+I;0OJqAifFV_%rlRt_OoP1pDEQ*$eRawjFs#j<)lFa~SEbO`O6s!fN*E4I4<4}d)c*%@9IM3g~f zAmJG@Y6}2XWyr6>_xOeav_b&=NC1A7Fb2W67FBN!d|R(X;O+H6oTj3FWr4}->*YHT z9bhWJ8g%Nh%xo102vnOpEC7ID0{CkKR>CG~dvMbVNU+!4^>#GGaMiLRa@(ydQ zfwW?Q#;B6_i2N(!-$*Y|045xyLxhGx$r2O}T%I+1m|R=~^KTY6{>h+y*X;RzcvOqH1MmPR75IYq z2G(QQ&ME;NwNG4SR~Hg?W9`Kem+ZL~@nUP?XL~6yg*mq{;pBhkY8J5vv&_{S)y4P* zV!NZYm)Qn`WWALf#KIM}4MPiH6C$}?g|7m>bI^uF97dXjpE}2Tb~j<{;#S&dZvt}p zBMhMo_G8P+u9Eq`7Yw{)l$CUKcI|XX27JI?=|}axV_;9avaj%Z2-7!AWDuRA7 z2hZfa4XFJ1v3cZxdnkF_eK_EzceYO{kY7N-kH)YjbE7AlT3ec8+m~MKipgCxF(Il; z&(1J^y=+!60UJ`9*&@}Y#k8hNeiqUYa14EUgNeE%?6;k}WQ!M~?N{5D{=4XJNt zosa2Rh6Uu>*s$!sI)BvN`)apIf56~eV1Ri4JViY(_(q?86XC%nAk4)vzuc6tE2S2h zu{dJD{=k5Ah*3QE^3@5y(~EsgZh_v}jJCOB!6Dgx>~WW5uZ7b!G2BRzLSl?ZL+&lh z5%AD^g93~2C+(A)xk;*EIG3EuVKmR5&QVWXScZ+$-joe96vj)9Z?LnOqo(H9oaWW{ z{Nidp*i>q+y2JP=Eu4}zS`r!>_rmJL2@IzuzHJ$%v3Oy1$Bq0VCuwy!LL~!iz7e@f z)#T;26-$1um1xhO+H}kX{2P&^4xgM^XTKYiT=p!bS*()l-l}bq_9NiGKd)5;7gUs0 zJ9obAww#;Xtf6b4q(jt6l-oXiZZTVBPis;3eY)LaJ)b)G-Ea=`vvhTF>h>kWne>UR z^2z%Dy*y^GGOsJ=Y0_tYG)Y&^>4t%>e9qU)RUD3-lkz$ZJw2VoU6wR8_S;j}&OB2T zZ7HHQnQtSk4}4$qH|TYV?lx;rZHYzBO>NI7FMb}lYLfjk#_Iw@bra=J@%eK%hC%=x z$xVhc_IG#8<@9U!WREsP5V0Ls{M1)BYu(I#b0hv+^L((JxthAtqc?d;Bh?YQ=OR)^ z8@Ck;%p;ASpxy8MZ0K8@Nivil&ckca_Y4O(KREK;`$uk9z>j(5y^YsHgCf@+3zM%p<`V4ZwA0P>io5>YYtVW9h|OmA050UyidX z?J&MhjL9{5^7W0Q0^3h__wLYt)>)K0#YlGm4@L^%Xe zZimF!b6W1rkZ^9HX%> zC7&OW^tU=wX%Szz*K2?CPyER{IlNsrNLw0CXN@+#k`9bE zmfmSz1dnaK34mLkPmX!qx5fggCApJbgU^1x$JNmk(V=eDVADDtP}1D-)6F~AL{;4= zCg2kxI4m|LN;3?flR^b|K|2-gt~8R~sk^t1oD2W2`E*h1n}^NUFN-|FyNgWvwm6YH zlw(R;QpT~`#CIIrcVLaG!0`gRSTj&IHhXFYmj$zJsQwZy?rvN$JTCj`nhV8qHl@)Ib8)s6^6m-r76_Y z#+JJ)io<4JyVJ|xl<3AU8a{KG!Zu(T8%%aBZ8txq?lBd0Y*$Q@vRrh?mA`$S*eery1p=ryB>#))MdCjx#$JB|2F^HvcPUuZ~V8QaC?(op|cZt zQ;v`Sxo5Tqj3=@Xhw}z z`b{x?w=Vhhc}y*t$^CBobl7fYXd8dC-Z|S~T?}(qcR`_Iq}`*h85a@lCoOAquk!_M z^L_B7ngi~{Kl35jzegOkl&clC8+BZ2k5^Wy+d!&%yeVSitIlPZLpMeBj?)E{{Lr0) zDd7*o4~3m~5*nyqz`RE4U2=+3*U$NyUHmI4)ymPh_k`%gxA(6T&#KR(buzA)#_c-% z=V;UAZ)d+7MSc!{QJ&?jn%MtXS@mdkbGNQhUGmzZxHN$`+g3$+EX*}vb^%;EvnfEy z=S%}dee@4lm5}NGeGGSo2rD zMWmAE$o?^cIb2meZ|So z1l|hrFd#UgXOZ(ZaMcFiB3bmGQZ@Cz3QKq+yv#%tHvtTA#7rk)7iSpW>{ZGUR?%lTx9x?14|xlJO2ygu^1pO3p54nF*Q`0krx zy!tA~HFaz7z@*auX*=qvxiwp38 znJ3}3J|5d&)qTp3q0pdMtx)m!baMDJUH3Sj;%dVbyj)j7z{M>MCe2k#Y8}ZQqAiCu zSbfY^i=4N1H$JAvm#>bRt>XA@{eC#l6jSj#(IXlR)#Tcd<}`DbV$E^Wcu@2}EmrG6 zhUbRjk2>}0PIeE+=RHD7$_H&8erk%&RM7hR@uSuDDgQKiO`ZNsO1t`$)3KX2!vQ-i zXQ!f+#<~y1%k^v)9j4wllmBjYo_xSS&-I$ML8^X-W$L5)u9>s81;5mOqbm=TJj(s$ zjcwA>-C@<0G_9IAsr7K?_~8>B!6wRPJL}a{8&3ABrN$+GQnS^Dhu`dam>0eIMastX zyC&7a<>snB&Pw!&;Juy^iQGhMt6;6S<{i8E^4I(ySp5XjNUJ`$o`5m3>i?AVIs|D{ z)qTh(*>kg;e|kfC-LII>I)z)Ehv^SnN+8WzRMGzH+NYHIQ3ZGRI92Gy*`-+O0@L4B zw^gf1`=hC5%v7B6pJtU_Tdh_U=I#vBrK_dCiis%RMD7@BdqfVI**~)@B-f<1uA|7Q z;DNHd(TpegUa zQQsvj&bZ*Re3RD0rmb4)RjDaUR!s^DMGt#B)*e4}DybjWtr@i6@E6oT2a*Xj3J zRh`^k+NLJf{nZzz-^LtFQE+KiY>-~+s-Cyw zbe-)dJN-6Os6IE0SL%AKanEa+>Qg7gGsoI>Icec~;TZ*5l*SxJ* zL7~Sl*8|^Rj*dHIPEvYuH0M(_r7FMwVWJxP4A>yD?&Fu!>$1Wzb+>~}inBuEtj{ht&6|l+sO%&W@a`5~-c-^;R)&S-oH7vGEKl zuO{@&cA81z$um%MoUggQs^W~zJj%n#?Nn;ug!^BcMR0!e>m9S#&tXRtQnlee%?)M> zM!zG}wEmoNjVVGpCv%+J17hz%EUkNLr-%P&&&hd?$Oqi@4^wf<)<0`niy&2H_Q~D8 z@q8PT&A8L`#%ssOoV|08?XK$uYIfls+dkzxVZMGxIkJPFm1w9m_eQm~G}+#{GZUp9 z;&ZltBKyR}aHkvD2B%GoCgl)@RwqR#=axKXzZb>m9{4@29pmA+y~uzw<6`v0ab&OQ zuK6cRt8;K6BjBQunr!&5FSU8(MlR>jdy_;JM7xM-%@lXJqCb1pWF5h^lIHF6K1yj4{1tkZlAWhzp9cHZgm z-v?)sI9;6X@HD$2xY=%S%4AvGuDLb6>c2_K-}DxL2X0=NwBNPQ0-24@u5Vg9OW?rG zx`ZV*PUv;2=8t-x9gk5d7}>wUQNE*i%O~qMI&~C$rfyg5X`hKZ2B*XCEU6l+rMbGz zA7HCA1}*J8)L?iQ<@kJL*FoS5oIm`H(mdmIoSi7B`S`Ze(GzbX7MD|p{_e)u zk6tt~_H&?zCYp^A)>K+v(bDFuvsHH>cU3)B`li+7>bWV?)Z}8(ILbQk_v0|mnpA|+ zi%pId&!i&<3_^}P?75p#eU}kS4%pc+*W;wU^|!^$al+1Hf$TrOFWMJz*C1Rzw?%A9 zePrw7ky_sRU7YdsR(sOVtybY&MMlz-7%%le)D@?NOai-j?&~#_Xj6bWqs?i?3)HUk zZm1X7T?)8y=WB@gWZEzHCGn)*CBNJ2Da$3(&HL5n3jqqlUP}?u>tdU3zloKxg ziVt}KC+tI%T*D8YaFYu^BQot?e|){i8xQ;@f5nDlHbScgP1TmR7^Xt1VarHJ?m&{^ z-Qa>7uU&2y%s#$=py=-G-jeJ}*2%WjP^&lCuG+4u*?ri+_>SaLQ@Ce(1MTAs_2cl} z;k&x(HgaBy3Ip*SfQG*Y?duH526`z4@+rCS6Mj9f6z+8zhB`Yt>s0Sn%GT4l`+OI? zMh+#fbl;h?Yv+SxymE@movFC}VmV3MA15VbMv{u|cGayjjw9<`{S`6dq)$n$orxzR z733o2r3DX~4u;HN&iv*-c=V>srG2!}R%-a+^`^!>eFtxDkasNBHL`MA?-rtj2mQN- zFT&^Tk`8RWc1`1a*WJ7Akg1ubM((BJ=nZlcn+`c%_I$8pp62#3&ZMJ7)A(?kp=YX} z*$?TjU-wG`0w0G5kj)zI&2}6Mr=}Y0|6}@kZM;iz&CV{Ok88L)yvW+o)KTwb;!#4A z-pGUFj+H$j8cH9Tm#WflAL$O$P5Y~xlA11uPw5#>(o78WtQ-A%-Ej2;<+`?(Qj+tb zQ?)uydU|?JrU0y|D>;Q`B{QN%?EIIplYk zPG-gl_x&jW-*nz^69IQ0fdsoSYG!_09g z$|*ywD3of+;cl*B=A83+lhU0qBuUB)MQWo&b!!x@=r$!S)aSR)@B97Zdp-8g_Q!SY zx~|9ddS9>S>v;h2z!jCM3I-3iNkj1TPBhtbmUFa&4(TB%@Nc<-vR6PN(dpqzvUx1Ei$sB{XQBicn;H+ ze=A11Y8(Gy-aqn7Veo5``x4GJFH&E7>uDYmj!Jv>zWI>)oGC2(Dh2nU4~ah8xZC zanV4d=OqE?mPQwSqz}>1k(t|jQ<-bI!NSkOr+c;Mhu-(Jtx|-yBEkx|&-!ZAV4hs< zuV^pY$!ZJ?qj%CuGeo3dAh|i{cmt-*JK<%sfl$s8sj6I!S zH%Qh%nCnLCxM*X8j4+Zo??p*EmLI8zba8beUd21Tc$`ck!@4s6_22NrUm^?Zuh|hO z2%1|r@JZtmSe}1zU+zy}_WB$3oDcI0hIG@guDp2dip@YgVPcasqwQ518CSrz#_XEy zh5odKRWP-gUuSdHj*3w6k7^`0Qjg#~=WPxuyI0#|5O;U994g#I-YT^d)ECQC*V9%n zp9Xj6pT4hPoBYMm*E=D++$F3ip`3qmHMFgX=r8esZ7%nXjey5>&M=1hOjB3AVjCY^ zgkRcU@F%^c&CBkj@_a6-hhy^euZzR3+ZU}qzS4Z7&DEgX+Fog6Ect{W5RQOf(7#Uu zVajgF;OkHBqaDHaHpu4!UG)xF|JIj(@&;_NhG#ki;X#i&V=kg*zUWl!l@XU9-bm=^>TE2}9(t~Dqp`(#zv!dWCr3WKntr;( z%U8`42MyI6S=3nbh^_Hv*}M2#c4zCn)+g`0<{5VzXz7H|nJ^Z6YAlOPe--}!k^kAqIx}gXzF5WLLL4q;D33~Z~ zlj|pQNs^PBuoRg7S6Un;UZwc_?z0xw zM~Yuls!!z%vf~qujH6GZ?T!w@*)bH2bo^R!o7zo~n?{j)+ZJ(39v@namON-09?iT8 z`F7Fl51lwkcURdrzy_)Dftv9V)en z`c@M4$iW85>y$rJ-E~7v?j(nJ-QSM#d`#P4@~h>~4aGlM`gX6$Iz~e;^Q9}5TlwGr zBpxxkJG*Tz22lwlIY~(`3CE<5{DPz{EhSz)l9NSG(3Iq`i`LXv(g$1yl+Nbhs4ec& zOn#tnJt27#PKyoBP;P85N8hMzD0vpwG7A~K$3W*`Gu2bA&yECIKNhxx7J808W$GeD z>Arlv>V?NCee$uHyAq!*|95&dF@!5-;zgeCpy|+q>>6?;F^>y{|Nk zPrk5=D3o?tfMd>_dbZPkxSi9&yXq<|Spq8~(d2VaLFA{8XxC3I zB!n}TPjcA2p3I%6@{6kc&1;q7nGq>VI%b3m>7jHMARYmL+Am1I^UzhV0YhEC;5Hv7L zTg`dSPUlg^VfvHvMq1O=UT5&P_+*%iq-fU>+jx!#O(dB)%MiZ0YHn#Qed98P3u%9G zOb~jluzW=;8wYz!aDCrWg92C12Q$8kbV26X;R>&!wSMr>s3gZGk{X;wh`TUQ%O1L^ z>c=CXb^+cY)Ttgb8%}kyJToQDEW{*($d5V|S@Dw*{lVV!AXf4TR(UXHUIyWu zw6>#C?dB3&R&>{Zdoa)9!DK*&zxt@a+6VT8ua$ry2%r4k@X)AU)#3{x>e%qV9y;N;va{3)*rqY5D%b6snlAdM4Z!tdMW{XS zTws8`CQ8B0W@;f8Cb%E+H#L`d@PPf@;8aKgz#+c)g4)jGB2l2C0~s2BF|be(3gcEtN6gU;As0zzY0l9^;?sOYIitP=N`e<~GCWCIS) z5y#x|YfjBFjDvQ+;$xJ%8c`swn9fQc^&;}26-8K!OpW07_B#&HyhP0 z_vusfTWTLu?6rdt4l^7zpmx9bo(Um6Lvn59mU5ui^jGI{@$GjE@Ug_G000{=^HA#n z_@mG%1YFV=U`+oBQ)@5wFwE2L3MbLbxj8z&HB6mDTE|G9U`k(Tx8-3a0-prDLSZ+B zQdV^~>!*qKv%~ds12n3zcFq$pB;l3!?KdH;Qov|!!@htUZZ{NIS@G9u_!bpq=Z4NK zPcs6XfHi~LW3*c(%|7Ll+7+;}?~%RGD}sVb*TDdGzvqYfGm`srpj-Yn&_7)Zg2lp%9 zVnI*$P*9fD0l$@tbABG70bS>2zSl4~MiD#7c>|_HCXRS$u@!AAK+ZUomujYL`oh=%I#!?H_#RFhIw!;U(-ZglYV zN^oclvEJ?BuqRKJbbzmcV7R-A@98pi3hS`Q0sVx2wNb;C7Ap=2O@K29%!P1QlEAtu zBf!l=D-F7YL_+S|n$WQD!($6>>TwZEhPw(L#w~lgL@wZ0T~>o}l5g`z;NxRye*!)b z!eRy9crg@u0~TAHkXcbNdopa&p8{wg2-E=8>r2L;P-^SU8esiPDYet$ap2@IxcX$?+xV=F9V z+fw34-a8#JC6+Nb+(44$S-O|6t$N=CltHS9k)kox4>!N~Zkst%P`n(xno#ZjFb2lH zu{7SfvI~%U1x=J!Y0TvdxEAq>^KILKRlj?NMga&}N{nBl&r0_ED zKBVfBzE8Z8iH*%*x4~6=uT%HWyj3E==tm1nf>T*^)IQ(eJ7R-9UFLk5qYHblil_U) zV4XsW=`4F&Cks&Sv2>YcYL#wB_)xgICOMoXw%TwBSSLGcjY+~ z672?fLfv&t)97jSN;f<7CZQTr>%KCWJo_o2Z1qx)@aU>Sz3lt+i>HY!V0fz zncug+@2YbJ_7BAL%6n5J3WR17E}Mz&0nJ6%U3=(<8ScLD;vsPn=-hw`q*@hPY$Br^ zgT?5>p>m}0nsRaNnjsYt z%}u~Jcan~PFsdq4#?EBTu8wSciwz%)PpngX7sSLim+2MB8$-b42=&yO`6?m2%KP4G7-~v>QwNTLc?M<+3WiE=Q!FllUU7AmfJ#!&xNf-G ztz)1o2x$hzC~NZg1$n_K=sP5+mPEaV3uhc*e!f&H&F4m5wkOa-%aL{6oA1RQ2VEF4 zhkAv&J(3ods;NR5!LGw<%~z=e+;+w++lZTk;$ba@TeE}lxQV5mqN5Whq48Go7VJrf zw$2)9sG~&k^hw*c#h%nk8OH5Av@l38azUlV#ulhkADx9kC3irCWAmvR!>l3Qvxz~! z5!WyYj5)U~=^+7Aq0VMH8;Ah(w+v9*@-uUCFWxa<(p7!lA)-eMd^?;~B*!QD_KstG z_WWjnaTOp%Ag^20)G>sbxT4M^tmv!K zFI92L*Cy4V!4GOS0f^{uoYuK_t`-hEUPDHXXZ;&g7By6>m1w=6V{1k`8EMM!psvQk z|Aq0FvIyRbRJc)7${NLT?*ky6$m0Ll0QU#L_XpbWQ*Edk!@?{7CI=FRz+=%*pWp)i zrG4`?zg^dD(Y8I8n|qKDi|)xY%pcdZN4%=8DYm9o<{uygYA~|UZ)^z|1Q`T z3zu9;1(glIK<=)zzv238@RN~B$la6FYD3B?eL zr5q5=i-YokG8t6i`~IpP=Zch*<^TJ(%JO}i!hcJibN3u#(f@X`7G$nPveIC z!63J(it?nF`;v}dsPOf4uik+__pU?-e7FIBm!b}=7|B76sbQYtj_@RlLs}L;`*T-6my`A|g_9o(8F7(UpJN?FzuEsiJ-}zbu;;$Mu0sXA*3e{&uYk##D3)n2zL$HhNs(WZ<*@7K#7w4S?m~^*PZFp z(8!n)RXovoK%PLIl_{u?*X=oB1EM^5;tY`h)!K*uwTpj|nrP6bMBdP|yJ~yM82ws1 zp2|O^4N1dZF^$HF0>W#JiSp{wN5JoAUI66jSf302c&7nmiS)CBKRMF2bm)05;veTV zH~6}dvhs!vhlDWYhu^NAdOhci%8ZAQdD~wWytkEjaJJprucpy6eFP)-49SB7M1COo zlHqhuFvqDd;&(}K;)aE^96@1*KdtE1JuY$(U$_{qHJ7Twkeuu}NNMrHek9Uf1(#JB z^w|}sQB~=?kDZ3qXKX{0MSdAt3_lO+H~N=wp6GNj)vIG1vd_L6 zyhdX3xJj|D|Gd+UAHBOZ;Iz6lmJV)l=dCG`fhu^l`zOQpbzyZpnX4qJC)#UJ8b55{ z*)!UbFt*B9g>(&t8fDJ9$(osqOV^$nc<2u1$M*8#CZyMmRB&=M%}0GwpBBJDewElQ zDRQg9uA|9UOaUpX5ZkXNR|TjlcdVvb-s8tf;*#fHD=REz)~Hxpt6zc|bFMeMl0!?< z3p({T8+x#@Lt%qYwBYik9BzQ@672o$23NKsdC5&Z%RxL&beHx|FyO(QUY0d|2*wQP(4Tm9 z9n3ZO$+D)q8N}I~$Nxmiuex?j)skCt=lASyKjhcieIWn>pzN zKX;sm_dmVjP8VEHv0z-wekz)ap}*7>*WAp>@S9vz&jZ`Tx_l_5L|~4PAfjVOZY-6k@HH_n)v0QL4pVRmr`^b&1+rYfjG>C z)kbQ&_F1IvlmuojH4dgL)J*sJc@itHWoA&ERID!)JfK+K71_}P@FzHq{`G4-Qi z7zLIwJ5*Us+-s=&VAqQNc~8kkww){-Y$xQX-vYCRc_M{J@Djrq)-RT{FMe_RV`{%1 zVl+oi+fZ0&tAHB1ib)7@ySFpkr#SA!9C*C=*j!?WHhuysSRoL+T?Y`wFG&5+&pzwU zRzg||v@2U6A4^gFLjE9KY=S$1zyf)=c$@BKEDhGE_90fT9LagDm+8fS+ zOx=li+2I6HF4H9 zz{i>?DO)rAV>CtW22x>2j3HmB(a8Jyi*>{(WXJ@^Lzg^(ebdfiy+~OyrfcvV(5Ry# z){7Pk|8&e1(HUfL0fvv<3E zU)QLkqE1^hd!gil#vdCPTlNx@Af(0T2|TV31Z}W&bhrPIqzGWhNrnxPrKIp_D%|LW z&A6Gb`+a3y#&Q$iXx8D&UUdsW5>EJf1tD#xkc`9anHI1$V>zvs{j==viZH73hfp1zqA9d906q!x zNrAx(Gi5%vcC`w&bO~n_hO3eHHE?Ld=tv%!!ZUun0Ew%w|9oo)&2R&2`=RPs)6Bkp zlD9If<6gKE>R8N01ii{m^$U359wUf&e#G17gp4+)cK5Z-u}AI=n)s&$VnL}zYZ#h%Nyw7=nK?FHZp?__%U!jR^9 zZ?^{ClKT@+qi*GOV;x<1_H8+;JB`sj-G$xy^&+t5oooW!?-b<4$C6Q8v(E@8O~sOD z1$Qv$CiV&@wfZQywqkct)sID*KidjQJ8|q-mF{2Dossg!x0FLkBsR1}=!XMs97nJ2 z5LrrHB-qH#VU*4k_U%!RCXSiHxKllL4@(gG;`@sHlg)mFU~?UX=E{%IKtHcL>SkYx z{R{1Ee#@Nc*wO7iu@L|%x>UZDf5F{je4NWs#TC>xdW~D58rqHJ~wa5 zMGfZ?f~0~te>*#Ae>YsSy~i{aT8-%5w6IsY5*5(z;Wkz#IiHQMzBB$OQ*rT)S2A@f zzoW>K2FUCbUPuHnuu@z0WHyM4D=FxgSH!pm7l~-ORY3D zdY`z=5S(-V@1V-e6BAE87dFc2(V;5V>E$UepEd zFRVYigKFVd%LK@kjj{Cuw2(h=Tqxkce5(cER|r!<+iO+avK?{0?X`!Ob?I^5s!^EQ zguo1qAhmwBy7Z2NCg~BJrA$#)J`|)__7C{i*It;+^$L3T+SHvY?)|f4>dO3L+lmF-7!C47201I zVqy!Aeqwm}KI9i`F;ao~c&9gt;&CWlX=qA2V=J!?9a=Q~hLFl?L9OBcVaF;{OwRg- z(kg`IoC};d85ysl&hkIIeDrmq|Aui{sI!(N1X`|A%%@d-m`NxJFu&TmjS{s|Myt+x z$Cwedo^r7h1@VloE5&CY48`t+=cwi=aQQK22p426lYMiIiX{j!fx?*9pu9OlK%c;@+Y8!oq8B_YQrK}5R)(6WCG*-QgmT7tbEkS5!+rpr+HjttjzZ-!41S3os9B^C3;njj-nI} zPFXl;NaMlaAO&vLX%7icc~4Yq=2(fdM(1gObLfnR#hL8WEkVny?Q#?D(&?AK`==>K zdT%dY%o>$xhJ>oR4(w9}GfH42Zi?aVjNvN74{Bfc@}v^vck8ME&5(5H2HJOWdB^(=^a!b77B&G);gTQ?pYJ zyIL0QyIwico^81#1~A=vf?GTf_TiuF8tuVYj9v+}=+A4dEpDjN5;BXtQ9rrZNA4Za zx34-|xi4+|ONAUGYzr_xj#Kc=Zuqz9iMXP2DlHokOdAZ}L*PqZfH`OCMn#>+Mk?0; zy&5%f4it^90lz{sKql~rCS&5rZH2rRJa&YcgYmg%WXZ^ws;Evvl~A_CCB$#w*n#e> z^=vd+^moVBSHp7UypN*){>O*syhvR|1g9!b#Oh?H(n!C+VPI<`@l-=mKLj!4`M&>% z>V$%Iebk|sV187Kk*9mq7F_zH;D1`|g6lt9n3lNx z(v>rRD-O%452HpAzi&b;J$D4BaX;*TmcH0(IvalO*Ro!@Z0zCvrt$QpAlZfD z?~!0u^>Ff?$KYSbeoAm>F6up)p~|fYE*fGa&%VSr^t|;cY2yF)KfA!kpy4rdp@x@} zt%gz|Ov&Dh7AHJE;kLoBC?}8wyclJ*uZb;gn(y4#BwseId%}0H_)@u|0Q-Lw1^=(4 z;QuNL@CPW7iGG)l7IAq0zouZ@|1<@C*mKf3@KY#pwPi09fQ>G8^x%}Fcq(K_4rxFc z<8x;onV+qBn{y1F;h^{dFqGqsgN|0v8U%w1HyzkpSdR9A2zVg;m%|k)<(fVT0BYY} zxSWm>)<=MwmPq-K5l1UXad4fc4=ohR9ELCqS&0_`Lu9z;lOO#FSz)ZVCMYn#k^|QQ zS*#dS3bgl`GQv+V#egg*L%e153a0Wv9$hCp#&d*}%=eKFSA>4JoZh^(zXFgFQWWRw z4l*me+U)s!@!hwjp5rM9=?8~7dj0PC#d6b}G<19CnFo)Wa1W!uw&{Ox{c`kftEV;L zeMiTGk(ctr`7fkk9-j?YEFDNV9EvEt_|fxM42~&742CNX(Thj<^X>gn#_3}tGrH4JOrdByAK~L(OzBYa3z=!*{=?;aKUxqN|JNi5! z3w-zoQOZy&ZY{ z=P`lVO0FGWl?{K$97yLlS`DR{h?#Yx@I_V6%ZGw^-3qhAd}bu(V)8?y~bSF=>Bh`Mal9z&p_WqEwc_FBbn-NgL1p`{f<~x)T&y zV&7oaNhRU$K0Vc2JLmN^*b^F2%e<;8H8Z;Pgj|~r~v9QY- z9Ua{?UD@%;z;!blR?vR=hlW|~zX$FE#b&T!9}}=r|7Z?7Pxpy?%;f+=Sw+z*D2{SKeHeBs9}xpJ0n!jZtA9^QqH{x5RaOS)5ykj z0&B!=YU@tk_`)UK#}Qf=ja^6Q%NeqgrY~>!bcEKIv?HPSu@`>X^JP}14B`(2|L$A1 zk6qpuTUPD-|2QvRv&eDtXTfi^-*Z=6tshvb^^@^u>!@3YvSZ9T#{SO-UUrDQ@Nw8< z7=%gus~BXk%y5l_s*JLe&ig9MhZ<^4;}*umUyl8zG&Y#CtMbYa0W0Ggv*5szwS9};~<%G z*Nh(=XQbTbyy9*XPAN13lHj>jhLifs-Fs_J&}|UAf7uK_&Iyd*LM0;5r6K+tIq-s9 z?M3|xqNLd6Eb`J|E754QUa{{Zcd9oyxRmR!Zm40j>PsVb+>AOS+KBFYiLTVr>Y$rX z#hZjitO|WR)gpeouktwMm_4Bv$=MaW&t>-Myw+<-U-CodbW!BBJqKR&8xizz(Jmm` z$?0nt*!}tgWg1R%#t?0!pzd*ogNv`5`JB~}5t+DX+w^4ER_TE3`cMz9>Yjb%?Q+pH zv9UG#7xTo+x_Jj3;)!>A(E~W1<(fvUa_2QzpH4_317V(RNm_h|F8s-S9eAV{F&unN*Y1zd+_P9c`VM4VOXEY`h)+Fvw?SIq343FuQz{a# z@>KA(8-6dudVT~KV`vuRJK70!#b_l?MKQYewyj0m z#=S*~$l(wIm7x{R9>TkkMHmN&9 zVEhm`&nGH~NDGDEXMfIMUZ#tzBt8QPzoZp?IL6k(*P<11PtV0Bqb(KYXqB0D5KkbS@1nVz{Ygq>=) z4O=acFh+^1Z`*XFzDTDZxGZ<*-SZZ_m*By5%cSJ2xIS}acFyicy++erJSXMuWKE#? zWTXi`zR&vgAmQ?UmqB~bsiQXuQGuB+wfBPZN`i9G?U|V!>xG$JBi+(EfIy1RWCWLR zpA-`%?S(ZI(Wv)FWPXxYI#JYmvR|)Ac_9JoTN-KmjI;@?= zQ9<03u)vH7WS=!vvuOUw5n*1yfG|vld`>hOZJ$_t_arB3?xnpk%AI4JCH(oW=hcvg zAl6qM9;$ZUDfe@E=wW4>>&#r9wexgG@jdHc!g7XpT*=mM<|G2lTs4(TEtp~X-iZiE zRbAXn>hV^sX=*&KBiHQ0=&=9;Pr5rL3FAZERJhhYTfW`$gbVG?lNtH&H3{EQl4|*m zC#=X}RqJ)kW&ex1uhX7i`M0RQayr$jlOMz-kq%rxY_B0p=ADU90-3{$39%}Tu6ber zoA(F&COFA5P=I#kaQ+5z<6XO-j=b1?{~ZNKCk^$X0~vEx2Q$U>iv6(HyA9f3@egiq zyeP4TPFY*K97-!7rDiW1xLYb}>)8Y^F>{^d$7O!2`mAjw+@v2dJ6)MO^I)63WeWGL z^|`0;vT}E-j=VKq2U*f+CJuZzC2Gv~?6{6;z}lU&r^GL+#o(H?n?xTGSl8)>fo9@& z@f53{1+?f2E8Z-3n>IG;QXN3O0lQS)&h;!Yt0kxU3>sETr}Q|_E!R=!QKe^^d8{C@ zyBqGx%ZG$gg$19xd#*G*_kD%M|9*YS)@3KiZTl0hy(Z8VFEZ9_jx)F>N1|e^k6dW< z4yHEgrSaS#Ij~rr?HbDFLrLeNI|EF3_~ z{&Omg?xAKguXTV6h>z3o`%w*>{9`?+2_PA{2}4<3-+!^K@G19)QzA8(D&7igTx2t= zMC37|U?^?WpOs@j;-vxDbu4DSeycEK5-C?79GK8)^++}bUWq1Ptg78`?N_GLSCA%Q zg}SVS@9)wq=BYmlE4k9A7XX2LV(P0zuW>-_dlEX7iJekj>d1jDMCKNrY9twBOeaS2zV2KJuk&ZYknUuF)`jQ zXIL99$2~{wnx7bX2?lR}Aub!jnev4<& zV9L+!Zy$$=kB3KO^}HaP&%p^^>qb?Y<%jdzOvdVigIAoGQx4K^Y>=1aD}I-tJ;P{}C44J2sc5;?mPkSe7_nQYiL2E>ea>!{VKCjHu^Ajk(I= zu*f7-*YM`>BRBVa{*unu=SwBv-^&aSIaQMNcem2kN5so9sB8BuRKbhkDv@U%fb+ft zQ6g7|AXeuit5&;o{Ju!`pBZ_Gi3qlaHU zS~GZEEF1a|@evz}HB`?3QMT|WuSwiZ;Sq=IpBf*&j~^UdYT`%giu9~A`_1EcSp zkxsk$i~X2%v0hIL{$l0+hr#Ozn@oT9qYbL9Swvx<={qc3Zf2~L-o+CVb=BD=TU8Z6 zYSFsywe*-W3cb12CZgzvnwz_vBwkSw8l;%FAPsjO*M@<;Y=HIs>-U_1QOB!Zd}y7W z==qF77a<0EZTt!#aLXpTDeFdG$srklp3!IIs_Vn1I5esQ8v9q--3wf~?N36#Gcd`Y z@89*vC0gUL)H+r+%&mJum}r=&Bvgle-$h#nIg61?vd2391Vj*E5kjKbF!B^588F)7rx6Lwr!Yn zkF4An=Ug+ow;$kLu+}HUAIoP+jx@Y%bVYi=XXSb^LL^aRsNp3q-rbWK9LLpe@x4gO zQ+9Qgx!r@sHD&v@5cY3p=7a}17#7@P;s)jWw_}gr&2slLgzzk_r~jEn#&y(+_f{=; z*$erey9)0I&wkQDk4+aHc;_B`xvRuMN$DnRIm&1FZ{Z|~d=hVf2JfE9=uNP^o1v&vp=`906(_cyva-Z;p#5cvJg4N}|~5jQGu1sDKmZj=HQ zxw(1BFU}VvoV|P&JE0QQJ*M!ei^AEfy`iRZm}zgES({_)7o7v=_vjag@nU#EdsB?w(dyJ!8ii?8!XM zublI)iUfk-^ka79bm~Aru)3Wh6TIfZA%&HxbFk1GlzHe3Pao_^#F>Nvq_X=Tr|@pmuaWvEYxA0i_;Q2l6N*hyhlWnpx@$v!Wv!XCNBRq!Pow7K z(~sw9n23zmUs42RKr4&3=jayu3=ybEqdjvJlYPSH>Ph~SdZm(zAZie&*;_uUjEl$| z%I}g;9A-8kpXD!<%W$W9-Uh4g+4Jzsz^9`Te-$ZQSIc8j)vE=Y8aa~X)`~_~IA=<= zk-+5gPHg{-Cs%MJF77-PPNS(Xvup1?Ez7wHc!+aGGMvm3X+zw-W5mN5M%G=Y^3>t+ zoc=uMKQ=CL|Lzf0o#d=1A*YwmsGUcx#n4|hVTM*Q1qS^AmE2>RQ`5hHIU-UfK!rgkIo!0Fm!5%jz;0VTe=r)kyK;6y>U-(7*zfB$3Y+*e*}Ocu-> zK4WY6#HZb_{u;$q{V^#mTaS?n!*3&i^hkjR+11QED)m@7G#IF;0AKc_Y-#BDS44^>NbL$YlQc;t{P1K+3no@3>p+%H5>SI16Th<*0R|J3K`$W-jy&GjHo3=;X4zUcyTH zPIyC9uKKK7gW#9Z+3s;v_f=OzNhf#1#gDw~^H_=lm#Of)-K+QHh2={yA-iYP;yf^j zjW~Z;m2Can-1L={ZBY3Dndtr-LCG9^z40?F!^-Cp;Wsetr@v3+eZupbWSrO^fwPMDN)@&Chz7?W?EN zJqv+d%bYLm66|sCi`Wj|nM^pAv-9M~?Ht=@Lx1@u?#z5{7V%qpcIJ{s`EU%%ws-l$ z+nhjh3S_t%U9mXzv}@QaiCa}qwdR*teUf-Wfw>gVz3UiI2Vs++yz}!~`Wku&P=|#UoPS%O0IZ50=TodFOjKm24v8cIbTV~2tWGc+?HYEaGHXf|-UFtBTO9~d_Q$Ty}6w7H=e(v|_ zN1MG9J6q6_^qe}`yu}Y-pUgYZzwPT=J&}t@c z9Yb)9g}ZT=+g!G{oLmdV>H75!@^VjHQl3e2uF(h&%7oj2K0We#p6!!$r^wmL&nJS> z{<2D2I_*7ReD(YRp|3IH$7DUJEQ^bM#%nd`bQ?`_wGmbcxh@TP*)NY}dk%r98te>M zD=YW##c&e~9ItU1i zHr=L#G1@AwdJe&(Rjb(`@&?=;kKnk&3bj7=UtJlA1M zLE8Fqj0h@Oo#%%Y4wv^_rHy6CF7X;wNCHl_kJ&BA&e&%z2k9cZUbRU{XJI2gU&A%GpMPvT<6 zN!788FJ$l>W;o-2q%2xkxUBbW)wA9XRpc0s*$gMzvoplK8ub zE;_Iw8cB~3E~*4jp!G0&@U+0PPiNT!g|0ZId?f3&EF_UI%gjDyX-bctzUPGEYIN*G zqh0>w{;o?EmH=zEdeJDNIo$fu@kra+KVGqMb3LUAmqeCrjeUb@f+D@!Q-;6luB!uB zxT7kpB|TxStsbv>AB@z;#-eC`9 z2mRTtb;#_2&8XgMZU8AhGOZS@G+Sn%XEsovhjctRihT?^s2+uH!-7UxtTLodn#@hBcEUKiU8h4UOCU43O{7XNT^u0 zd*a81gykI*d-^RGlA@gF^d+5b1`-DBM?*2`bsy21;l$F85_T@^d9GO#m8$G`OOg3F z*GroVC9ef)TS!YKt(onm63+GU!ZX;0uI75e?)KiXQE4j#cXtJ8s+Lw@E}=ZU!XaFO zDb-hjM376P?`cDQ>7+UZSa`qFmy$Gr8M5jN*0v88g0vXYyvef%yLyreNhYa9AdIB4 ziQAmj=hG3@a_x7Sf`(0&Xq2FJbGFhx_d9%EngW4 zGs=$hJbpvEk~KpB)1Gs$Lch(O-UGl}v*Lo8iq|e*PCC7(S2rq7f{O$zUw>TS=2YcH zmK>Ne>UN5niqIaHJL@cK$N5t4WnRl#9?~lbwJ@qoF&FIX=;`Okis*G>SZuDb#nV1t=y%3^))Z{r@Y|Q;rHz~uytT0v!E6tP%tAX? ze4cR=d?55c($xEC*@EnRg-FOEymwu2KPmA0>?~MWjxE>5DcatbkVQJp!cG60FPh9{$k=1F=!NQpN`tSqN3TbF zEIsDHHY`$=C0wx9lyQp9%-C@hSObkMNu!F&Un8Ihx7oilp&gcI~E1|IWN6sYNf54}?&1ezd zwz|9;d}4IH*(3kMX(LRsvq*nvC_BD;SZ-cr@K<9pbgf$|XkmWdd6KDrC`!mdNY1%R z- zTJuzwX<){%Auz|11-6e-r?+m{?g`*nuPA&YdfTVD9#LZG+*|BB%b3*A z_InX=`H6Fv9nN5&jTBD zSx3uikEf`eVUNOjHD4>9p5aQ!DiTBHRjb#J--ed)a%Vu*ks+KvJFdtrL=xxjgFTn3 zefUTJtyB8*Pqz~@VPa36xNATwz#S6anJ$Ctz2Mlw_Fk-a4_zmKd_y-~_yMNWi5l(f zBo0XMOwAvKc8Nl_*BJeyQ52$P0xf)fnio6Vdj0s(q-(My@W-60#z1pcQ9pb>PILN7 z=^qI>Um3R4WT+q6fCHO=r2*!B?RBy#%S}|gX5-T6d@N1_7&WJZNHTLaFV;z4bg9S! z=p-c__|+88Ifzds-nYmY=1v{#5 z@-;HYr?7&Bl8C>U&ut>}h}e$T8Ryol8ynO+Im(>GZ;PSzQK=Ah2gG(&eeILoCyM^g z$U)ucH;=xg1@YFp!{b(vdYrdRu$i$u>#bPMevbWe6r8pKexMTFrv^@Z{9I?)L#V%Z@!yGD zwOX^PwSJ^7ALEuv5v1M_ha z9GzV}VV{Qd6us#G_D)-S#;z@)G_|){MeSXqwo)NhX=}t5dlf;| zo_h5z-eRtzl8L}`{uII6rAD2;?Y8w=aY`T%OksELt^ZnT zU<5_!CW+~miD9(WVpXCk-Ag%fYhLuL3Gq8lj5U|E0t)xH7jl!d)*RW;-NKiv?y7rH zyrqCnmN)oaf>^#isj0IYsmyhzky(ooDzo@Ly?V72q?aS?&)^MIiKCj zqN2?H`e>mkj}hTlFUB8g<#XuSVcK+z?u|#CsdWd%u8v1YCd*vC$-*UM&C#KU6@W9?W4-B{qyHK-L4g$%J`mAnG@Z zwHDPoK&GudScDPZA?n~{M>+3wyS@FQqoTyys&IST`Vq`GNWj=~)C1^u*B2x&wXV%% zAqX2&N8LRErNlR6WcCFxS!$}I`QE|AmRSJ-(U_6~r*Qmy_BiD?2A*RrTox<5{PQjI z3BRtRV<{V4L`0_vrYY5^otC0IEwkWwKJdb#R3G==4f+yobHh3V@XGG}tcj!a$INWI zA~KK$LT_UTm#q)J>!{u!pEfxhaewbMtE{BQ6=H@#UKO>FEF^dux9~$rdfO}I(M+6A z@(G{o*(i?@fi6mlIUh8wbZBI{2co953K@I5Yh`S+(cJS^87OP<7ulKUs`fefS^Jlk z9_qaZPx*3D5+h&=_0cH~f~wB-ww@fPvDOnbff& zEk_&G_1(I)-4I1!4l?lvSvw?w@Zx|^r5Pz_>ZT`*;PQn|(uVl_Z5!ic`S|cSbwH~e zTltec?GlKlK24$1429oam4zs^UXf>3?q{+)(|+%yZ)Hj4j46|>CVoSGhrI2mX_qLq zWO++DsX{%P2XTcHIXcj9=(<#Q@660N4$7s{J01r@o`gc5VucJ{CXz_TSn4(%{W)GD z@l^P7S)CW+{*_#BRGl&bz2pH z>$DjL$z&XyxMC+e04wj;JSYodC#edT58=tZ!E01ZVw%1U-y@N?JnW)hpWbFF5c|sh zO8x|&-m4HG{-RvfHMxiTllyZdY^|$S7-0ry z7Tq@5OrRx)A+Zen(4u5oah{e(xM|BRLZ6xpKE0=~4jZO-{^9sC6wMtZe|7XF>05v{ zqnMQQX1-L)1zxtGAIw@Uxs263^F1`Tu^t@6 zYGC?B3K6u5-YJV_%Kr+qm?iW`Cf^!4t3zqmNp18DK6^AWzDXf??zRQ1W#<{gL%&9n z!6&>>oVZyZ|2PGvBoKoplgq7PFJJ3}+m!M_+^=n66#q{fmZ#3eCnMT^-id2GwND8_bCyv$mdj z8A|pr=`j_3tgVDQ12k@pKjA;SUmjw0D_p&Z_A%zZr=Q*Q zO-qv}T;JT^W&H2`6%LOSUa7PlZx2RwB7=!(Z-~g(+hnb=IS#p1>t~Sk6FKDzYBkK@ zlkddI0^tGAKHhIJz|5mR%DTHJ3?D7l-UOJjRn`tYK>bGqRdq81&7tm2?!tlesByr1 ziPjyhA7Q_h)Qg!z)QU+DE*?bQM_+kAMn&CoY|py*h3E%xN% zRM{A~mYDt9t_^zwT*^QCUf%XyA$3>rdEpY+S3nM=d|EH#njS>2$>FgkK6p1_gTCyR zhZS0euj&|H+#4L-{*MS!)YLj(N}fdV-ih85V28S2kdqrt6VCJo10vgMZ>;y4LOX^k zXNP&dHcG38%$Mfi`>Ff8;`K&e$Cn$k@^4M(cCQCRN?qSo z> z0{rB9%Yb!Jw-ZPi^sxYr8C?dw71*?Y314F0BINZpExP~N7T;Zs77Le!-&0fMGv9)C8=Wcuh?-f zKN+Pa8EPzD39%G-JQ)@(kE+5+b`VbGq|(tf)`6TJJeh<%~{&#Q8RnospXjYIg zy5mJ3B7n3=z#%MA6#&;V0#HSwttV@zC7Rz>pyVw}{0U2ZEd$SM-?Mv4wAGrJ=5v8~ z{e&-jb_N9kmT>>mT(^#uwY?9jlv)}&n$J~JXusYSlB4;g1?i!ED`1oePgV>{_Fz7A zSgX+5gFWE+2ewZ^%`=vggI6HFaL(7Bt^@9R-lY{In68oJ%z-ar?3D2jY`7LoAJwXI z?hx{%_sb_Tp$}X)xCITV{zg!nvqThX#4qH3KWBW(JL|6ej+n#NJvmQk7mbST0GjCt zjCizhR=Ovm%Y2dM_Mv#lWkXG}&p z3mI(>5}%EIc^R+tQ=EyRz8PEHFz;T(RO&7M^-+^9LJQEzw!SMBH#atM_>@8R0c`|e z&rSut=48xCd9~UlR$k~_`flw4UZ?#@*o-6Esqy*MeH%6#j{Ng@%y`h*(7V{D&!5P` zEZjxZTdp8aIvtwT&Ge?}mv}oE;+357{}DC%BL&=VNuL}7qsmToey7jgPG6T@6nFA2 zq<`b>6o|s!qATOWbxI;)8=Hf)!og1IK|hner4Oc>*%uu@8OgVpewqj2`D}>d8|H1G zM+HxrNcmR7-zgXAzp!UH`AliP?de42S^fGCr*IZ70V1-UH&!aEwyt{1@|9b=0*+2U zAe$SJj~9ca;Wq{3+FrXJ3Ixe{vF_)6j4N)={CE6R-Xy)LvDaKX(_X|SDzm-*@rK4o zV=r&Q81J0tcD=Gah;)1MTKn#mKx!%J!S;(l^5<8Y#U}N0r>O!2ltG#A4*I@^+ee>4#~I*d6$Lj@_W_OOD#H}9e*0% zKsiJG&b=;;|3$%}af*90%i+)F(FODhnVatTnZPw9`|VTYGvHVL)h$`+TlAggM_h{TQ{G26tUnpyWAJN6$5OuJKbU`*r+d%;bu=Mh zOb_^#Rx`4uTUjm|Sgi?VW4oYy!k=Zo^gl&g)s~1iHYSm*NGor-3Q&cHZxRXBO*4YlD!QMXk>Z!p! zZv`r!Wj)$h7=kS1Xnpz;dG)K8d*E+n{EgS(oVx_>2d=TX_&Zdq{ppws?w~2zB<+SS z&W1`v?1w;gM#kI%y4$Ptig%tWm@^)T7zLvw%P#RD5iFSy1F{njAB4s0#L$;3SMGD@ z+&tiaM8!#zl)oQ5F4KbkR7)sJLU-n93rBjz4QYX@RjT*YHPL>!qT$3)eT~POPC)81 z?fm3@j!j{76luyk7WCw6dAFFg`!UQ#pZ_h)^T0iNxgrQBUGCB4#)D6|JV`(>*Lkr988T^n%z$_Qy(d;JLolM>xrflc)_AVbKK0mmR&iTz*cWf^@ z_TJg0`AT83vekUgfG*=-*_SAqs1v~zw+iO&(t$_uaMFERed!!lnx`wwB5vM{ywC3& zwGN-jS(U!<;rERfikNTl!92E#4>5H1)AD=1N{CVMPw$Sq#-}jlF>+k!J&Q5}%s7-X4^ zqB5)EkoYM%X!U#Yh$pd1`^>vZpSC9ZE5xq3!!d{xJ>41<|6ml=2XTnB&jK~NbZs`Z z>nC|*D$!KNMqjo~rb4-oQVmWb*bl_ohW2#%aX%kL#mlM({x)2gtzAe`JAz4{*)(sP zC<#6?#&z9NMFoj-7i&&)&X>-?Yg3ADftu=8ORHIcE)eSWQ2VvyZqCcGjrU((rqB?^ zrctWUSy}h*&stn)h<=`l-S>EXDz$!IsEzRU7-TWRhY{5zm9XyMnh~s(he2!PcF2U1XMz;0U*XGE}c+^IeA2BEBK^s_k z8c?upf%QNM-c9mvNpKLT!=I?IJnPsVLU7QF-?}rDkITKQ4_2b00Tagax+? zxYev%#S6G9C$3NxYYH%Oa3ouf&Vuw{j=}^k#q(itbn4He24yfL%&41p%^+h1;>nzr zGjrEKN#xUWxf00Jkl@?{QBORcQ{wf0xpCnnI(X#$9kYnmE2VY5_p!9#`}LeQ%6PqL z)w^$ffP`vJ)~eDYGCjptZ8#aAJ@P63Ox)~TtjuZ#VWP9}DWz|7I*xW>{=&Mf2d?6M9}kHh#X4XSu6K_@$)Lt~Sp6DlAxy>0Lk~Wx4;J;29eJ zf$>o9C>k0&=3ODmjDggjgY&WVk)Qp2(t^Wzj;Qh$aJSU&3~}R!HgV?NKzZ40I4kMA zUXLoL-DYw+?#Hj;1ja{T?=GEqwX*PEQwv|+eZ=p58Wy{cqCk=%pcgNy-3LRLoFB77 zI6}<7-g7qxAYt44O*j{+vv5gji?xPoHPBC(uxwhkm!v zcO(RmAdf9y%^EaIb$AVuZk{SPXY%2^KokO2TIDzmr-kVXTw|+))E&*dD{W1r?c)(m zqq zgehZ`kS+hX`%_CZdH9jO`!^*agof{%!yjBL+C^!J-3kC?QGzge9^*(n+9cucW}Ld? zv{$nr>fR56%_L|@mzV6!Ug54T#{bDvAwQrM?8G469>Jd!rpX~=rhP5Ekj=T>kmz~T z5Jl-_i!7X01ebhAyazHpWt>adPW?_bc9?_9T=hcXf`=~<{Cl46*6%5|eZXzwzxZ{x z9aYgglqS27;At_x=}F!9^=IlRh2G@m+{oyXTSMJw^RiO5nK*$OJe4_P8x+cY?_NFH z>3A1?ANK=l>Es6WMMXHZM!2x}KFw!=MeorR> z_suWSp6$XqjT)9vhJ*yVx2j=2FN`*;7CwLIZDw4rJbao$UPJ3i5uzY$*d!(X>gI(2 z+c&EBdo!dnw$EAxBWX8YG=IQKUv zTF~+7gJ~(gXnNRseuc{vp20`7IY%q#;U1`(|Cxb4y>AjB2a~(wcFtt)Qp!YVyQLX5 z7yJ;#<>+~#+bH2{HVg|U)e&nT_f}L$saHUnq|P%{gqQioJIZehCHiXO?go_8ef1`X zd4kiFNhs3Ni8*%dTaYrLtQ%G`O!e+`3$aXmawfU>+q;a>3*9QH%*hDndov;z7C)G( z-^>8P4JJ`Cfh5w9<9bz2a#S?F@AnhxL|F+w45rARz-}?fONcnU^lzwMNf~^xJ2|!|v{!6tRnan1iduKtsi3?) zoib+t15RJx&=RMvcwdnOx7`@Lx_y-&7aMgLXR46scBs3|OfX6}sI^}SO*YM^Z0`<7 zx>v0Qo_KBBw>E!B+A9kH7p_fD|4#UTFG_x}@Q%Z{Ih>*U$-s+(?30LiWQ8)mTVSdo z{vYOtb4l zI6!n`>PDHRUJIv<9N2!cmPa5(!ND9^J@*MmW>-!~B%}8(Rf(Hno4fXU5jz|X9|R7f z)#4Fa&DjzrT3Raeo6&CgsLwb4KfR4ugzlpa7+%kyCwMLr1oT55lq&be7ed1`Ot+td z1q5|Rf;i6inx&lxpDG&yA(x>dSp7Cm!OrCb8d_~n4Zem0KQdlxf3q^2lO!KcbC5<+ zddcc}J2>HD0JH&8C!%r^3D-))I(w`qc-<)F%n?$$Iy6uM`#@yaPS( zBm|XTHFeXk8XoREA&8i?|C#{W>+8|Z4XjcG;P+Z?{6*lpq4FgwejpUk-k^{i9cm~h zAV~d}t(UOhR%Nobh8#(lRZKrwb>IMQs2@sxw&(u>^0$AvN>GgsOK0}F{AFbVIjVF2 zH9VE1Jo5FM>?IojQHvG=S6d>-Z^zfd`tT-yA&e}HM+8~a7xk~bmTMJ%n{VLx@c0K} zm69A28|q)|U#8&u81d`rq>RS`2IfvO-w|HK$F;x$^7u6smdD>>4Fjz4Q=}WT$%j!FqF$8cg;!DVv`ekUJy_PWze`E8Kw&$Gl_u_ft zP2&AIQ)?M)CEa&^-z^>MNr)M*bY+*O?dm*&e9c@jB#Ta^zmKtC@vax<4#&u%&s8mJJO?;ZnyFUEdRl<#FR9zQ`zQGt z&#mD|$0K^hMcTOOxQAV5Y1x7=*|1AdNy}j(PF>j9kG>TLF@MRAY^qy-NQ(U|R!m4+ zkXejn=<(z@p#QCoIeJ(Wsj&ou@?May2B+C{j5sbksmwM)tAJSL(5yGdTwea|!MW_= zFpI!o?oW3Pq^AA(6{>wh;feyFMDAzo$9E_YzT7CHi_FPSyRk~Y# z2)F7(ja>}2`{HpkvE7_!^xfJMoA%r4jn zjCTc8hdWQtchkL>&1tSsH>vgn7LLKU^7xJH=Bx0QQlmh?S)qi_C_v2+!nH_v7-)E3 z)E?96n#Z&wI~5zANnB*k9Mif2X!<=_k4RrqV&&R-=9T_@rdO~zVGGOp>b}TkG?>$P zu1gK7^li^wu^l!WxCgr?mvOtoqhR~+#fqn2=HwF7>r%W2-ux@*@?;)YbZo3~NHTcy%!xN4Ajdj^ z_6k59j3A~NTftuT$U8oq;>q0lW4^SxN(1aEUM7%}C)8#TTY-CmH7I==h zqY-Q94*cLh6i6Ls<0p+0k9ZYGHF1$k`v+le+RSXBR*372VpGxc-P+{(by97g1FkLQ zIbI6bm3E>)MxkC!e4Ap^G3sSZ_6c8@a%q! z?MUR5`&sTRP%TT8=WojadGQ+oI+x7{{BDu3tleBgEB=@OOa9q#FVS>HeAhg}8K{mS zgZhPr;7wJaVpTKj=W+B1jio>}oKn-CQ$uK?y6J_%t)Q@d?44YzEJWxmFJRoHcPy5^ zW4@Ko5)e13PnOtUm0_Lpnk07VCXXDRu4Gxy>4^IDxA`mhVr*w zRLI#eD#7)ls~8Tp@urHgD&tvqxrXk~QP>@Wv2Iw=<>(&n(?kz9sk-6Oe}gK6m*<3M zJ1O^AwRHRkYVK6g&xrOUM=^X7)W0zJwE4N<(VbhxDfZ0i1F*TaZRc!vD?a5i`&($@ zW(e|tf-Uj!KA6@CcyThB_*>9jZ%9vN1ZB#+irw`VO#}k=hLB|QdLf9mB}`pJ!Wz*w zo+CCj+#-PX8Z&NE0WIi1_)2DYt=v5sBb9K|R5u4QSBCT$Y7A}L(r=O(Vn8Ir8bimN zwo37(^<6hGzF(?$I!@@iJ;R{gG9(^~byzvtLDT#i9fuA)M`G9qp#1E1Xrc$g3q_AP zqIGrR8d#i?I!em>LR*N%22xIW1Hyw7K5;t-^vVfMSzQ)r)E&r)p6=Lz{^~9BDV$6$ zfo{hNCS|cbfmGe6JBmvhyTVF0kXn@OMbSUFYt;>wkk9Iq+DM@2U9 z#>tcXnP01DWiF~p}Q~FIUJTHV{=0Bo0Z-hmMvi;BbP%8@?U~f zb(b=x(m*U+T*?Lk;4bAYBB|N`MM{q}%LVZq=-_?_f-XHGlj2F&!Yi3*O6u0{ol~2rL8a~ph zxv^z`;>cGIC^Ood`pvaS+kaUd#K(1+q@F}r)02*s@SFu93Aw35q3})i#AsI~tfZXS z(7R8k5`~Jd8?Q>7k-_1!)j^4VFaqsX^ys0hh4R=ADV>@sS-(<9(V=3`q~%UpP<%r( zx;^BXn~KIbRlJO7D+N|5I1j@ff9L%VlDx$KDRcQncwUDml$a?LhI_2!ZisO6$%qEe zzJ8JC6Wf)rUG0S-Z*by2Cc$;xU?*K5fzerblr1(G9w1sT8pnA9sc}{1mCha6>c%KV zK)bhQS`Q}`mVqOcAo!ElGv{Dq13Ai}7srd35nql-r{H!~9@kU6Ujc0M!EY!^c}RHY zt1IhE>e#N`yMEYVr)QQ`7HczARlXU8j_sTWqF;}5N6s1h+JoAcd;y!T0i)O>o^IIP zKTm6~>frE z=*^fxA~E3(vieUBkUDt*^vD>%?01*%31OS8#bkPB*mj31_8isC`~MNSwQXgFC3+PGXSo+zX4buQ?WIcAFklOj2=f4kp|e4*tIcEm+r{Pcu&@Lyka0mKbK%?p`i^L6TZ zm}nT9)hMMjdAUQ^w--#vJUD9iywY1q`axZO zAVVK!QfeFe|>pf6q@j%U@a@Z%|C||dw#mi@8p)?oE_|# zKSDU*Aq$4ThPDDv;v-w;*|8&)Fh#7ueY zqyeYgpXjc!a|gBQkvn3g;U-ocXsdCRh%nzsaKP|B$A}s!{NT;Y)9Wt;@bOBQ_*(Gk zLlaf2h z5jwC;vK(Eabd6vA#VfnzN-qIlH&j<~jpz7wpfk-y?j*=o3Ar^7%yqzaBp)lD0O+ja zRuHptwqXS$C4LxmM+eN!H^Ze*&+WIfRx4w12NnZbW;Q4Pu6Hv;$o*!`arH+ut*C-b zP-bJX?aZR|ewz8$u_P)@e1RnIpL6cg8Vq6RbPeS0SVY~)HS#5%V=jhGI%=;Y{eUlN zr|&pe*@uU3cC(7k0J`82ll+j653@RVME(0TM#C`RBDbXU5OVe(fuen=O$+CXe12Cy zbD~*Hg>`YRYwNNas2h7!#Z&N45aFO=khtuWe}b=o$b15V7suzw1L^)2F*?hYh9n`0 zJjvD(?;8Fnf@OA&XkoFrN%$(!pUTj_2IMjLAtacvED_Mg+c`q={}GA3sKXuy9uu3> z8u|=wKwD`HwtW;;RWuUvwvI*JRK_OyV-b8V?SD2{PBJY+VLw$rJ8@U=B%mrO$ojKR zx8(;Zq<)${y36hOgH?Ekq@D&O3Q!ud7Zj^@Vl`5)h9KCn;A*!>$RE}{y>LtEl0XsH zV-Z(Za+RrB_G6~8jaD+4ntzK{E{$q4q=}|4coa+(yPM4 zBEk>PUN=XKEhVhHy6O znXTJxrNefG@S%x6#dEjH2fzO?W1hP9j~@lZ(=JwR3i9ytO84@)R#6JIUG?uOo-{&s z(e%~4fY@^pQYt*@J@lQ54m{~7KoS?3k+t1uC+}KjF>F!M%JA7_jJ{K!b)7_RIi+?TLM#y#STdV=|zic}>ndusdaHFcE{$6qHQz0);@TeM_SiR8-IiY&b z-~&1gMl|V-tI`zG{2It*Hf*LE8ZR{a@NYP~QKw&Ymdw!|&KrYXFI?aGceW*O+GDi_fT3TxP-Ghn8YziRk6no4bVgHIk36#Q&J`Nz0}*#nr0qoj%4-PXkGi;KDIq zfKmW)=eF6yUeH7Yw2ao%0<;+h?_b;%Z57RCrus4sw_mG0y&F!Qm8OBAw2!mY^#rgR z0g=Hw^j}QSc%nbV_HM#PhKnF0jEu+^BbS8m2`&vhnH?-PIV;tdw&9ivR^UmphVbJ- zA_?whvndR>pcT8LFg`SPjtrDf|w)F)j zyf{n5vl_nOuH{PY_T*kO#jN*Sk>$QV-VVIW+0KqR&l2S|LDPntfjaaZDgtQoS7Q@4 zxT4)85#~;AYXTbUdRq28O^hT)QY#P|mn%Y1&vS81@*b-$Rb&4wU|+O)^Gv8skQ^ zfY(CovkkaUGTwT03dDpe9fpA=P;@`sCz^;Di|yKc>1(D7xYX;8>L&eOiCQ1 zHfhfi3~;Y7@=3T%Y-v!9Eo^_s8r#sa$*Hm2H&wyQnT4ch=z6sgn$TNCC#MLHn&8At z?G+ldKqD~9H`qBx5ewlRQ=`2RLHpw?xap~A8)_v`O|W}PxwxDzV^R4E5bX9aizmsB zFR}$g-tv+c$ZE6+V!PA30UAS3rC;4iO zRSt=@d8Hhi7rSgm;<-py{5v+c>|C^-DmM3VaE;9|4Vr`|oKo%s`^E1k2FGyTs30NSODDedCpaU+Gd| zsS6PbKLK}HCiuBDBsP3e{_#g7&uJhLP>c*!Y^pw=x)cYtzCSh*z&E+Tj-^{BWFDAF zI0$d9H_y`gU%;06%&NIJl;X)Ak#oVaLqVmAW_+M9a=t@D7p4st)tY9=?F$W_5P zo6Hj+&1g<~4+SPQqy94FPUZEJHaf3ZNX$^VEBkZYD%fnmWl{}H7MxtY8&$_b>dkbS=Y zAJH?(exsK91>vquku>j6*e;IRd8J(BTT1`NddxeklYvT~EvZs_GOLIY;TNd|gn#*U z3^+}7!f)%cK$M<0<}VS+zIwYva0vY(s!)e(QvVcpjqRV~`W>{}@Lp6)(aGNKA=7og zQ0ecypMcHw|A>@E?cikJO{H2?1DI^m>73*qBZv5$CY&Y3Jqg-i5ErEAu!34pTy*Qk zrF1t!4p_DpBeiEa;%Jo{s`fa`s_U(l*t%2|$>x2X&F9;@{;QXQT75FKAF1>dU)d0^ zv{qFL$j!0H5Uo5mlA4#AM=14UTj`qLTt-nSh*-F0$YkX^W_L4IK78f6>e{IR6EXk& zQX_NhAkHGXLW4it#CiGy(5*b_@wd>(mV=^dAK)^da1g0&>WD!9*Ei8^bK{SBfOddtNWh@V?_LOXk{nkuHVg) z*}ZWSkLcHz+yG>#g!wax2F|ig=a-cs zY0BIsWv0xJks*ZTM0UAHIQNYpJ4j<*Ozt1%VINTKn^5JaJB=F%_nIYJ4HpGszZmS_ zN29qTP|b!6g(Kgm^3q%D>dj4IWNvmmPqC8px`tcl z@*~fYa`S&HI!C)frqTE(Fka~y{oq2l=4!E21H*Y*K&^lbGU!1_D@JHG(%_tQ9r-YT zb|`<*E9qdzq~%2gl-jMEwm%C&pKA@9^TYl}lw{}PD!{K|fI>E6X;gcNiz6=bqzn`| zYO_r9@S8`0Ik}G4NV!VtP~=6tWGe=<`-U%y9Uib9&XFibfd~Yf(oR)}W-9i^fEk!> zzQi5BeU0wC@0?f7Ib?W^_j!XSwj|!C@(?8rX(>0rQu|CQk5x4A2Yt(6axlb}GTGQh zjuvPX9XoZ-HZ&f$t066M!I;uhV%fFD$=ak>Ifb~9UZ($0yo9&Ih|`%UuUjpv){Fju=_dT zI|D9$mH*bNJw$V)wnkTOG!aFx7|iU>=Z&MBAQgT0Nbi3jmB~uMJ93P!ydV6Fz*4U7LAFc0TCL@O0QZ*+* zFSo55b$)a8j{-(i>7kne;5nX`%B|QdYRiYFn6A3Z1?5lvrOSB*3Nm`bNnQ<2Vx5U^ z-cBq&I>ylT{Z*4@CJ7!m28QmQj9hJog=Wt!a2aAnlPEgFyeP2k2EW-tzfPW-sFs;b94ZI08MP7^f(IUe1CH-Er!Ef=+hyW z@p69G(wSba6}1OgrH>|^ryX_+?1Y`(oq}B>dOvL)dMVJmS_+^pmzMeL=b0IVBPL*D zA3S0Eq0kw}QKP>bv#-g*3*EGE+la~4*IrjKsEw?+kuKr*GI7pCT~0J*mr~90*I6OP zKYLbj;O0pk8Z~^;O!Q%(>HGKL`R-rVqgVe{Q6W}=YB4W<)WLdnw$4dYE^8%MuC+hh z!nd$;UIj8yAZ8<3ySfl2MAa+J*RA_-0^+*$%u-l&o9`=ph4^64g+Og z=?iS+Aa+k}bv2OCA^YcO{UHDt8ljgER^)hRuUo^eGxPPnFqSIv$&bxQC-2OmhAmso zdg8+Wh)h2O^A{l1U;Iog#JX}<@;K;(!rRHo3}r^ID(@e(n~ie9F|Nuha@2BFIMrY} zH|~;ku^C0jEt!9O-ccm2^B5Yux$|Z~(p6!^q+OLdf*o$Z0o@U*t%tUo5a(m^} z;IIi_ax*S_bt93CM(hh6^S<*_qOAOp&}WYBtGmSW$loY$KMbmT4&BjalBU5N#HC?J z!h3M$_{fJQEIA`Kn){hmfm8?6u*I_`W@p!l2UTP#fAm~Cb$+YHETlP9Inp*a9Hh5f zstj=%zThdC>=Br2g_Qwr`jFIJmpn_D|HWo6=#Z z6-2X*BgZC#7?yJjgJO$oMg%57X+u6*RhUNQH{Wu#@pSPL%^vOa(eF9x+q1_aG{;*N zIsduTB(Qj*IX{`POcG8%6K~xlXNL!zD_nclzXcm_&dW)g%^jZZ6N$dqjf81ymRG%K zt{afBa&5WA{D?bQMa=26D?3c$u|xoq!f?rRr15 zA_*B6Nq1+D-HaH$P4byFE3++LFg;RirmjkY>{dKk2%1BA){2JjFdi9HUU@#P5)I2~ z`9MIuu+rqn>_$Ec$X!iTz_2gV9Lt!ixxCDC4V19IHzcI2kv$WTp;oyJv;DZvf-6e@MIcHYP~-3R8}(Gg##6$H8L~Lswh+>o>rKu*U1PLzos@0H z=SyKdvg)Ud=}h5C2(ETJlIa2``TF_S6uVAAud=AxoaHZ0EW!CQfchJ$alE&`LqGE)%P$u!lF1tHl@;?`FZB&@4CDB$SvuSX|U*s-38n<+#W85w$Z%!nHbh z^24#khr*+%slLZXhu7E{JZ(@&5<>a@HD$9WZA;ax%e_s zVkd*8r@Uf|0gL?oX4YEh)?8lbp;MW<9E*X_wn)-oE^bF;W1+vN%4c1 zG(KI9)EB%21?cz9dgf6t&Jk&)w(f1+b=jngb44n^dA61CfDw8Y7Ud2hB?ObIoMXgzU8F4Q5Kd8=@dZ{ZPtq2#M@buo5ToY>b?C!pWY)SO}$g;cH z@d&={b)C7$5O|}cac-GpV_-1dDjy2xttI)qRm1t~acYrs+^(&*qQ5bXU}k5#CAKAX zm=7;L4j*5OvoLTu+6iDqb|i#l)2)xs(W&T7NV#CcQy=C?P?I~L+8mIop;*op>5FHR zJzw6}>#!Q>pJe^T?$~Y6rSKm-Bv>k9a*_PzwyX&htQhawdUdgHr;aJs2}++{8Y}9K zt!TtkKJGppKN~t`Yg#(0K0i@%D)6O8=}2Yuem^;$V>qB34K;`Qg$>aI4z>edOcD00 zS83-mgz_*g3Ry!siW}Clvv5`y48(OBfa4=7Yj8RB^c{r>&0>v|R)Plh?N6=(` z+iuYq)(iOVm!HMeZT}ICVvNJHN42BM9*!1BPka8Ia8}m-$lK@Op~DQGGV}=3Sm(uj zO5DG!KJ`B;UR?z4sk%=$F1-y-Gd)jAqyM_j{Fl9CHL0W}M|v@*Fr zoYe(O;JkoM)^q##O%6ZYSz*Qjz0?7*>LDePcUF!X{;~I{c}~tl-i)oz#lUUIzB}yH zd3Jv%^%}D4f5O{t@2UvbOkZTh?I^~pT+v9@NHA`6H&!Z*@FK!P3k-T)!mW*##!~>{ zll%j@uO(=;vu(t4upQHqFD|5)eu{6LuqMd=;T{Xku}jU=eiJgd@B+`Y_+h~C@~h;H z)_|PDW0zpC9%!w)_t$H$HmfR%e<4Y{npdZQkypTs2b z?KNV@`nQj8ypjG*Gcm*58NHX?#huEbumA=dJ}YG=@VXh0z~U_RCGtO_KxQ%8V~YQy=&a+KdfPZmcO%j* zA|uB@x)GG_lIF({P-=9F#29HX*pQSONJw`NP#BG%Fi9CDC7r%||2%)~e0I*WJF>c@ z*eWB7c|N{!t8ZR#ET-T?>t$|J%Y@YDn7XG%h92t<58ESs8I%4_uj88eNRs^FdWsiI zTU@E($2T(>`}R1FC4Q6b;c|wdvLpFiEuFaFp*PF@H=S3Q8LT*kUZ_nZfi98d(N& z8~xHkkZF%Zx`PEUsBS+xrGX*C3ypA5L3g}&WkdB7Tc9dgDT7|$-I-;_a?cGQFmP$K6#+40MPta<=`EkL>O5_O+w7>(!S?w-Q z?a8ZS`QtF0&==QJURRpvklZq(_sxd}de^ZS35#00#7$dE3g04%;Cw#K;8TIWUSymg zm`%4VZ7=qjv8~Aw>G!aL36v&{mUP`^e9cU2f+maK=0p*%v2ZDbYJIK<5bALWed-t` z${CO2d8hytsYnO=c+4cJm(NIbWYaUl=7B)S0vGnyV8{QIHr!)}pd&-*N)CzMoNmY1 zF~#|;%-)2v_2{zVM^MTX33DRK*DTgV9VhRf9hs*>#~xVfd`~f56zH6`8%*1<%KSIEs3+9RwWtvq23rFu<2QwC5$ds{lwJ8yg>CZm zx6@b53`5V>OE|!xvPm-ih1@kWzkwsbejtSID)(%PxS0Bz{D}vmZoX;_EXhoZ@&8AV&aqi8ebj0MG4C9A zTX=pr5murlh2@y-1jV;$hT5gJF4L@=e%Jcgf`rJ-;qs&CroU{WiSmz^z~!B?bid`O zx^}p3$Ll}HkvZ~uYSKb~CHjz*1<9m$;V|SG=151E3S5L%BSwX|Q>q)9Y5?XgQYt)1 zVp=_T?0`rbZQ6_ToL+oPj;|3*pQurvjPY^zyjME@b`4JiW_4cXq^)$Wu?M`NaSfqwitgh8Mwfp?d%>x!y*Nm>7m_SpS>LDu>d@-?c1 zjTnZHcK--Mv$xcIQQuE6k9s(h`0!HaU9(D4qJznr@c!!l&nZ_096>ijs0&`q4td)V z;@b6E;XF3m4I0BxRh&}s(FOD+8ap9sZ#l0jg?g!JG~&hn4Xw>;6q=1d7WAlQGy zPMY1DX2l^Ih_GY2g_FY@Jr4x(7kQb>9M>|aN$0_=BnWe39OG!4Se)cG$<-^LV5xl| z+KCGo4$t~@T(JQ(N8`P7w<9V^ftlcQN`3irsNf!zKd?Zb-{h{)>SwZdvEtb*m#ZY~ zNjdcfo<jzdpfvu1U2}rp`LYoamty;vcVa}iVAz0 zGr!PtOyd>nlp#G5$)W9S7X7bG%sPo!_F4Knzd8W=uY7F1MN7BC&09(meYC_zor7D8 zJz7!JjdZ7+Og0Ozc2yMn1vp8`YRjV751DTqiTeTH6-Q@E{!H6*FZx+_efpxM z_+27T+5IX5rbx`|{~fN&iSpJHHSV9s7EuR8>s}_2(szxw(3@4!I-EX*8y6h9<``GE zmJ6Evh}gJ%2!J-38a$6ylQK^FWG}Ib@m0M`1&OpZOe*&8VNe0#H0#JDpCHLlXt^#L z;31Xk>c2fUEUw2ImdAta4EI1p9ZW){RBuVz-$WQ_b=F7(J$wibJQAQ1@9Z>k0TObpP!7N&TDIz=KE3fPx1`uj+cD>lVLE&s9bPe`nuk zu07&$thi2w;>4!Q7Y&pmoL-J)!H=nu-9J`kxIVuB`@^rr7AYA6wl`E3+#5sp)E2;L z`mPv}DJl3==7WS%2ghGRqqZYI{6}jPkUU(TnTv|>bGGR>4(xXG*!$mkrjte2D0beZ zr`(MGI_4e`a{L;cc_iz<xY$w>;ph@M5=B2;tq0HUtU6Y{vXI-JD?gTKTw9MbToe2J#*Hq zO0FyLjpnb0EbCIelRdgFe$$5&Mbn|VFLT2K5{16}BM6T3H<8P3erolxcq6m)AAzfz z&sPMDPSxEvD~&1ZES1wcf^Xbr@Ak>EWV(9*9g_lK_!Iy6_1sUr^5RT#P8*5UP2P;h zT;gm&mGGZ;FWuAF)A+Vy0xoN|qbFN?+iVhgCAl)Kb()T-D|b`JGAN@p!~hPv7y?~( z%h{3d zQM38tgh56?I?nRwh#0W4Vf$?`a=6Gd&QFlB`fa!YVV{BpJ&o1$FgozVux6rOy%fd0 z_D~_T2r%oiEht~}`gQJa^SifkCZ9ezv@MtR+yCTPGsOTlz<-8|EWxF@M|3mnJNt|% zExCULZ&K)h6j*@u>?fiOvDpZ(a)1qp;t1RkS%86*f6I5%8)s?SeMZjO< zl*}y0o8^?G5=dlK zQ*e&zh{`5gVgY+j_Vo|abt|pJMz*Bu%=-Gf^&|I@iqQzk1tJkE-^R(E0_sOp0~h?$ z(zIy@VjjWKY{vn3o2Yw7N>jviMGb?@67P#Gx<|R1E&;1OFPaXb-E*4KFZe9aQ19K} zj6C0s41#-&`6XY&UFbCQFRgK&K6R++Tq&>h2MQbu2U|ne{dKKxB)VOCm3k(gLe9F0 z7)a#J(7&C$0Ya8oR_OT#qZKrrSkIMy$VG&I8*WxNy)u zbD6;6LMGvk`2g<_ov;NX&6oj4GLbV!6p{K^{6T`Of$7mc8}?~%`f9{VK~8RlF+;#e zq6+!ubV`C8Y>g8$AKtom9MJqZlJ)W*0sbZDZL>Kha+zm7+OAgr#DKFHTjLgtaslz~ zJMtW<4>LBiqb*nPv$T>Zj_Cg8YFemxvA$hn1z1D@(fs&82EMPZ7J!efwkB@*(?wBg zSv~n&yDv~WVo{-EUv?_`pTg+0z1-lrlq|aacEp``Mrzz&Lci1+i)}|UXI`{HUP_`M z2Y^xh2SjQ&hTNhd5Eg66EfrC+bHDIGo~2xaCt9;NtYWPz9*tF>T}0V_1V)_QeY(+W zYAg3lP>Rte1X$Jmu6Y>dJW9%{eVH0qneVxLkx{t}Oo-qux)-$I)aKG?xg3=Gx|`Fc z7AsTRxEO;SSTgE9{8^jki>Y?tHaV!};5;1a={607W9L9KVRw;s-J}1FtkI$-rt!fhQ8j=qll7*BSco`6(n+jy>YfV8j^| z7>*y4C5E}czdp5Lw)Fu82t7O}Bg}%4_*f0aDC@Q91IWhT^1rvBQgv^lFmjwOG|*Q6 z*g#bC04dmUjclUP5%fN#(OV>KIuapjFm}nRdI$vh=jqwRjFSp$GR0-xIS)zzYB<#K zf|Mn)j3ZM>P(8t)f!c@!bKOuD+cJCO$YCH&^44g+OY^%cs7#pSLJQN%9`yn~MPe^3 zl_=zXM5re>j}XJz_l7L}BcKeWiLER@W_q0_zfIISaLP($!cE6LF~lD#MecU}k+0L9 z7bwdA;u>w6Z24R(CGyVwg^tKImTIWFv%cw;60@8;(;iU3tQL@LIk(AUd#!9O75R_} zZ-QTu>vb)q=XV!9C2yq0CK1}zp6XjH9*W!*H?Nev)G-f?JC@aQs?bohr?B6=fFH?c zcov|XoS6AA!MR#fHwOI{!yx7Giv%#|(_!0!zlG5${R!quQ(PiR$K+2oj!C%Ipnsu}N{mdBB@01Ym$?iV_@@F=g*V?

@A#4bD)*lfI<`SFs=4Z~NsOOV+DyuDmvB zef;w#jwBf*Awnl>iXi}|cgj9MyRl`!7x-iN=wqX4IEGl3nC-i|$ZNfyc=VeYU89Jf z@)rNKzjfBdNeXAFn&7X1=J^t?aUv?|_f!ePeHzy}`UIi(oa9iS>&8T^Flf+TNwYZf zsBul;(r?I-x$$|-sKgzZTO#61vF#$YWO?a64>CODb6cDEzJTeBr5vPCoYlmOvxVr@ zogkCBMEFTsu!+4T%Brh9LLa!jINl{A2l+wn9V$PMVx6ZjJ!@-XrW)8-Be=o=^JD)qbpDV&v6H)b%dUCm}Zp6WY8)gTnOM*&tT^Korxc3ZN7bmYVjuR+8Tu;0g39wjQY zMafE-e#E)@)1_)w<@$m&?yz3BoSnMJdd_@ztp-@`x_PS30dRT!`yxKuQYTo_!+4#u zk^!o6Yx%6IzZDx@2iWp8Z1Q^fc=(1OFYpoOZSepyg2vd z(L5IkPNl9MYjoG+CKV-f&ZC$pD;qcaLGvZfxH*5x5Q4ZgO!+d47UszV=c+>@TlW;4gW%iwvdX>Q=-O7tCqqOm zkStaR9!TZxVJg&YOYn$5&mPEEpi3iUfr#Bs-@M_;rH7bZ94kfyi(1mppKAWB>YN)? z5paYW&5L5P6667|&K#3SsD|f!$z$&Hg+*ePgiHV%*dKJXKJ;;9A=4s5(Z|IfI~k=i zj1Kp&I9!$-?RunsIf4_JtNm%E_CTUiv%3=PYi|$AQ&QRl+Rby++L?5OqCmKB16;KSo1RvV!{h?vOD=k5JYwf)W3wBd5~m?ld=F+rR2 zy+BTkAL0G(j2BO`)dtKiYQkeo^CYfsG3?(b&Th4J3MIo%LlZ4Dm=2FQTR<%gshK8@ z>tI41rxpFLyJB{S`U%(Tv_uN|o_Ed*guN__F%{*7d_J`3BEm2)#bnufl}EAaIS-MX$=iJn8*&z%>FF-?`h7)hOj2S1%BiRxX^`+6^0KH>15NvR)tJH&Q-J}IgqYjGHO!M*Cgr!GG zDxC;{gA&ha@lb{m=9m*2mXdh7z4GJj0f5z~1?HD#V6h6H3tKKzGi%_e7!umiDW`D{ z0xN-!bs+4Q1+6~PSNbRIDN3$kZgxFJfH2hAi9e4pHJ%Wqe6~WkM{9wj!rb(41_nco zsK~H)wl5R^7)#ldzmh13nT;;$wmZWEZ=5K`XIy}`htxesJmG|{HMKnquMUTy)8$>` zKeH&a^?k1nP*ywL<3W{P*0?^7j4cGZS5rP!+Nk^SuS zz|()(-YLGv4ANYQ1s^WJ4_{D|oPT8W;aR9|~AZu);X|aB9J?k2l?srl1;@FC8 z!26EQff$QDsm+3On1`B7y+AmE)JYF|pB86!2owDm`?L@WpnsBM5NoKiMyprAeDPMen}d5`fvfTk5OtYN=L^FOMS#i>S(qCTt)I>gGk*w%qK{W`IjWulP{+ zP8^%3qb@HZb@br7T3eZra*Nw>7)5z~`8&O$gw7M!+KJ(?81RM2N!*u(b(eX`li=f!q znUZn311UGi*TEXHR{z7m*`Z3^X8D0fu(pcJx98YtM5x4*n6Sf@N$!#YwbWB6CBE0Ag~f zA%=k{9jD<_kS*=lw&J@bABm16h>V@dF;QY&l`hYsp(_($au~AH0Xd?SNQG?($ zPoA>kcQArQvcoIZJ)k^BO4lw|KTPmsd57*i;gu= z8no~%>G<&UI$gK=-OW4E@5|B+ABNa2v&qQ)R4JW3d$FO(i4>Srf%O|H$QqL-f`;r) zm<~U8XIWEz!nKJ5o(ph&J4SZ_1}r9(yp-fVr-)Qb<#Yhz9AsIeEfc#xovx0_!S=M5?rR{`DO#4Xi=}}}jNliVM#3{) zn_jVdOJLGYpO^a@Z>J&|Jp$q$*YoK*;T(CY)CU78?)YKE1;wY|7XDi+q9}S7#`l4u z$pP!1MQ{A`erj-*(t%VL4n13!MBeQDP5yQqX2JdJG8xu$l_lpWR52IG3~e!bt3px} zo%v{=!Qe?Y`b&L{%gn9WF$JT8#K<-;N#&KuZULrOf0i`QdY|vqib<^G?-Q-3kct?H zoH*I*&N&IIJ7)!gd+jwdvTfA{`#}hdh|`O@KZ8rctn3S6h+X26H4fB#{D%FUYD>=(E$krTVn;%j^G6pR!b4ZZi%8T3 z02iyqq?$BT_jGcz7ktI;HTS%i2^_Ye=>f%(@XT0l{-@sh@;Wm%GV8y!$N6X4dz893 z2*BO+YVY^AHA%Pde+0Qjm&yVhxR+i23`X}R4<&!O51wb(9py2<6z95#?fz`S?JauB z;r%Me)hY$@MEeoJ`mXH}d3!&}L!9_2zVJiQ)Vj#{w$GN>bnlHt7vbnBD>F;&!->=} z;A4!%uVsJuU*05&Pk*13g3>PG7BuzCf?|hqqw_dvomt%<$mU7eM)}u|?|v;b-=CryoZsDfx?Ed8$HxE#WgsvM*=O#aD$dx|k#>&0koN z6CNw3dkMwaUq_AWcHo>s7t*uG^zWbCi9`49bA$;TP7zyw|NV2`M4JtM2Ns6B)!Dnz zow$C<&$L4=NJA>o%R?FcEuELTvifr$Sva}+bwA1ln5VuQmw>6-B)i+tDC8EMT2vey z4H*A2s(2GFP#$_fUT!m(X#Wv9r8v5b^>CbFLHP3G9A)oYY)+R|?Pln3Xp#tB#q{QW zbMLA&TU8tpS|^=SU9aD&?c?H^tiKj=LqOt&+=7?9cG<`(`->liC|EP~a((!!kv{oI z;vO$DxwyP^3@f1{7^Y+}kGSECm?{35D8=usaxkl1Q`gT&pX0+7zy=M>S&)Ur+=aVU9X;G(ryV?gWy!Tq|4{}SKwX*33& z#QwKMRS4AyJYwhLgiPB{FN;N+aTdBPnekGvAYMXgkAPSGd5IG#*(L|y-`-*Z+;|bE zfdvd7S!gc@5H}qQ9fo^f3(VFoCPEXdGAIJ>3V+zp__Up8j~Eh+8@HJR7chAEXPr<% zK8_v(I{df^v5ilv((R<&mhSy3b&yOdCl%^jyZT~d8u`R1B7#{q^hS1TLCm42b&IJn z-KDTiwU4IC{jt8a@*Ox(n?B3UwWyx28$>SsWm$Hn;r0|N5a%KHc)ro_;QHauTA@B|-Bk@G300msO?AKPq0Lda)f*(&8$}xVeP>;w z{(H&Y!gIc7D*dk1Rx0_&AsADWm-EE%y03L>Yx&pE-(GT2K*=3tUTJUVvcj7|p8Sbc zDiz&D5sjIxYLy4Phcgs&$?F!$(E=&HJ%MGh)vwZWiPcmUVx`)&R`#YP5^T+Kv2u66 zG&X5y1p=F5mugs)HD!H!V$N;6i!S3sbow70nC#IpS4BHNp#PRE^x5C6SZ9|L`366U z_22=`61lZlVFTw8fpzWtu&n-ERe~dl;BP25slK zPQ7DBTt#||B-@Xs>3)>~x;%`@>OXl5{N! zCxG)EuLnDZ4Jh^#Bg(`u>t76%W(`S^Wf`t4F$}4yhAo(VTNMkg< zzV%;Fqs#Z$ ze4n2oKBPZ9XxyNixqpHSnC)OM{6|o!oW5?`VtOEX>u;J827AGKdIaKH>9L+`H?l4( zkDF0Qm~X_`cRwpp+97#tO{u_Gb6v&(o8w-sFxGm>INRD^eS%n-jkqG)cT39cBT@=} z4Sw_MJQawlFzeX0^@wR~2(EK@@>Xe3C{IwEj@jQtmH0B%V}$AZeN?5Gf$4>;lxP#< zMA?r(7_f}XJ*ZvrI?qVG7{rsb%*V?)Z!**q922kn zPSROkDohQKHJIVN-o=RZ zm_;1d6T%y{@|sYiqoPf7KLa~a;BJ~WbvUy(0nJu3+n)A342iaj!9(fAs>a* zsoAE?YWk7H#_%e|v_;>aW8HVD8Rpevq9x?%YJ8~FM|2t(j7Mp9A^pm_;NUBhUXs9ekt8$-I`+7x>*-czSXM^ z0`~{8noE!BgkcXr+)`?+Y9#m)igJDHF0E#*(qLF}kK>~GmporcmM3L0%8A(#8e`=M z#)Zado0ZO2V3g|IM=@OC_m`i#Es*XzL(9PB+(iolGbOCntetXM9MBk@(+Nfu_b$8a zj%|Z)dla9{Ed~u{R_UB#S*?Az-HDQkNuK`tn#um0a8AMw4sc!Ya#VU=uw!0qw0T4@ zNLV#b<0W`lJnvBKx@<;jLz>`VJc}!2Ow`xHx$h2co=STiWX#h1R}+eJy}75^3J&uS zb~+D(mDd>zO|tcfb_NV4HV`5Mo~}6A<9W`|zwN2vPb9?nIq!;AKfY6AjVD*Dr`U4K6Q3{_ot!PJJJkxqcPa0x-wF|H*t;bK4?jOV*TpV?aCO^#LR2m`9=D#9GsJ( zKaCHq4zoB@0j#-aba#*RjncM9c~ ze}-kW4Q=$HB+ARpOer8JSFRZaBRT(Bh1OQr<7tT_Y@!bvRQjUeJwDi0>_6vLz+B*{ z!pk_LY>ciz|7eifRONU6TosJL`bc(i^p0EHbEr$_5zn%|R-^#o~k2LF zU>;aJ95XT;qn9uf^d2mM#em89%jE8(9A<|f=s-ajey0O55BrEL zfp1x(yN?i?h~QK{ZV-VCY)3{I?0K!hNa8A{*36U> zFZq*ue3tgC;Q|}RtvkTcWZ54%NjLWniLGZ)b-74qpq~b@13D0JUEVVp)Pt=wEK7X( zi+$=HA5dvL7dRXFUEAbx`bW17J8LtO)Mqo;(@fIleeOv)L4xq+~S=L zW}GssiEVw~NW*sO%kv#4S@%jms)a*Jj;5(fFSGtJbnR2@0q0D|5@T)3rvp^=w;3Ir)A#;hB~R&E3kP| zb%Pd61D8IZU#ESLeQcfnQ%(c{q3z|jOU7_^8h&7~i*);wL3ghZ`Xzfx>om8jpi1M@ zX9e&5e*_F;MJm*7lTLQse682cIliG?mN?hpZ#+l1K#6rwirj7rR=9JQD#V-^h;nmm znR9Is+fdTavk?M2GT_K(6`7AEDb^WwWvw}K8`8#YWB=b@7VIDIaqG@cI=Ex_bdm*xKa_Ak zfyRDYuCk2L^N?%4j;##vdQLKHk|HtSleGSRp7%xeU}$n~Aa_XSo{3klS`5sdbd%n! zwEui#3bZ&r#fJ1t>x{cNQLxM+R3LWb;hlwczR@8`0&kjMjAN zCI?*Dl`JRB|7!tKo7N(W7vtn;;w7;~+n|L zoJS>Qfw2=;{4B?5R*e$Pmd9U#MDt_DeEjLZ_x0$bFW&gV>~oOY+qn}l49lCrFtIk&7a1=+@nC9>LE<303(gHy2lsY4H{f_S+fTl=niKG6 zMpowDZG-F*2S$115|MwEVCZEF%Fl;q*x|@+KPG6yX zus2`Ft_X@PJr+%0i~m%SLK~x&ls&$oW0l;mr$f$4CeUBq{Q)>0ohT(xYMSwa_2X6S z!LH1$2GLJY#x7AvLTSujl}?RaU^ueMM->i*V3fmM9SmzHv)Qp2>!Kx<(zf)&fgGHF zgi&kSX1?IUXN+@tEsmg2^D<|XxzodO5?d+a6JcQQwOh5Oc!dwbIwUy(@AyLEhneR5 zFW5Dm)gFiDYFQwWCqw182H|pB;XbU}2Xt;KxSyE)*d@;6M|3d%ZwfI1aL38T9r~oK zD>jYcYvvxiM7l~)%WZ+D`;xh;l^Tlhxoo%IBk@M~LGE zp9pO`59Uo~_dov#yr@P5Yh;gj{))5)W7`2La&h}60P*WS!Tv}Zc30B+a(DD|4vL6A zxwdiPh+d-J0*1#J5dyor^&IhIG^g9~biscI zVG%d#B(f;odfO6l<&Goq&bsq#ayF0KY%XC&rmBpcgTQ7Jg_)T_6G+S&*+cov z{%u73a1l*7W#X!Xknu^gz$|xv9OZwFybSffp~39)FIW(72^aUIpS~W$Z4`ZBtTqRH ze$AYmF$J61S$p)ox^F=cScj2Z+K|81xu&<)2AEJ>Q3cSF;x6rUWIHvX+0RT4Nhv?m&NRP4W3?f8k7qDE0YaMoXv z8KK3>`6dVlq05gyqScHr>|TcLZLe8f_20O1uWsFe%M;x|%*^<-n&V7zMnRa(Qk1M; zKDeBLPQcaCTLFKDkv|d6lm(H;xS5r%9}Q2=)~b*7ck}mD%x!u;KN0R6u5nqO@rZr{ zGAoCt+Kh4|++waxvzNS8y-yXqLhdMyQ8{5=0@yc~&Nb9eRs6hh?nARZA)NvSY1z_y zFHa1$1M~k8%z`WbI@o_1DvgU0s(H}VL951q1t73I05vybypO20F5kP{2KLc8Apd#l zxI&D!f*0Gtf>xH^fJ>`QE3pCk#&G+CE4KE@?iw54WAl3Y*90C}Y(OzE=^bx{%U%kC z`1$z_<{RR|rEpc0XVQqfD9{6U7nkSP=107(!^phh0_KziC3qG-@B`WM z@-&}Z2&zDCBy5%dx6lO9(|rJD{gqsU@i`0Sfpgk5#$1Eo__EusU_E5w?Q>|6mpuM~ z0eM{dRJBa&MIs6f9v}ITP6f1(Emttvs0ubaBexmF3;6G<`_P>5UB$E51ZR$I(vB80 z!OXZ7H>j?K-o45D?GZXd8ox-AbO7Se<6{D;)%p^8MwIM}fL)PD=qXWQZyo|Q57l2y zWWw9@Z$>O@6y8w(JV$+uGYHb836D1qvx!bhx~B`|ngdH$0SE#=Sx8w{5gxiaI}9IY z7ia%9@=bUB1J7IA`2h@o@c8jEq}PvVxoSjVr!4kf3DCY zNAq0Ieb3aSX5xAuh@Q&Ny~w43_Xi}(#oPLpvAMv11&0kij4n+sIZ&3nJbV3H-8fW^ z$a3Qr^I9p+5^>4tT?=L;A?kJfwAQt+&_PnTbc)pf=z(|4^JM*y`A0y5RDs1g$xlN= zVx+Rk-bbwn4XV`{*4Ded>A)Usrwb!9LS0!YAlt*9x&60BzLVPkF1!M#er4YuStj@S zKzUFl^q4NXocnzHI`b+h*I+hA>SiSFR1H0-{>DPy7oLi%`4&zyymGpORjaHBD^`N!chm&kZ^O^eoOL?nzml{u_K zBhiRF%z@8B5@9%HQ&}78Zh&7ksD)Rip%w;iLjK z_I;6irBPdL^n|XM10xC(z%XVTVJ_*IQ=+td!JI|)A3F{YF@b? z^nzM9>7^bQ^h!xPi5B(P+Oyf{CzV2gH%u%`So_OzRDr{X7KTl zec-M*%_+=wZ?UO~E^E{_`$M-HgE4QBf}p*%@bY_EDxX~Lae>EQDs*%{Be>=KWb3Vo z=ZJ(@r)8g#MI`ZHnOWq`)>tjiX@hYrbzfn7m`QRyk2QkfmYW&H_Tk&oa|RMg*L|)A zrd#R>Fn+O=C_r%#xyNX(+2JCTN0ZKK)m1i9yRhBLo%i0ZmMMNoMU}}XfG@JfFfr?l z9>@$WJ2>@O;(c=nQ^j}V%S^p)s`w4^>D90U%AAVl_%!K@?UUv5#tDYpvd~xw`pWBY z*60=qI`;+)ppFr0{N#!njc$u_u_Oq0#TReL?!leuHtyU;nKebWWnzDzq~;9O!}0Y* z4vpd`_&)f%blKrXXRSQDL^5}R^=V|fAD<|JO5xkthlUXbq-69ch$6=MUAB+$d8gZt z?KrDxTYRhF^ts{uHRE+sV(VNfDHqweeO?H~$xb|d`WRBMCM$Qo{u@StJD+c4=v7s#u!7#~(P_V~ET4iF zS7~x#Afl{v#|*Vv1wDEm3c0RKr6(yEc66}f2Rkl{ur!@D-RT}!{uf~eM3vUeF+l3i z?sR#(g_7QDxhqzg6*F?f0UBK=O)Gx2=sf43T)dP~4-RvTCr;u_m6cCaAwk5egl1DE z-cq-P#YtkAOB5~b0^>TF7I(XJoLqDQBw<`CUoziB(pFJS@P#(+>t zHBy+i%;yg7LALlNjPa8=vzv+81M;9x-X@E{oABcdJ8aX$tim4E8Z%mj)}UtH6Y_Jg+JqslRZ15F#nM<>p{czDX21mAE2qs;A0p{T+^C#oX?!DW1+`q@T zjQOoDkF7v3$Pv2hp(_jzxJJO)04pIpKxPY99%$T@ibbO7zHG~96k;V9I^yKPMIrQ- zC(vb{+;E;$OzjOb53(7e;IidZG}~N6H@(r9y=CN5MtOt>20y^Jo!1?vq z;ZX%@Ok2RssB=GuQGA1m)WGh%D%2VVo2amqMOj4w{KmKuLuEMeJ)RE;E8F=-6?)xb z`IDOZE?TVyIu|&cd{#Jwr%!s?-*GA3wTb;B0$Pj)Kom|y=P^}R%p93|;UB(jSxl5O z;ctQG5SXFK84*ryYu*Vgb-@f%pUpC#f(9xDfk0v)?xJQzOqA_-ltYzD;GI9NYLJM({U{0%w zC>J`CIX!`Ff8XU?pBXNuUL)JQ%`5&#kX+CbvYcOSar=u7*>x(^)!<0N?EV1D?L z5pd2*ILUq? ze#-BP$?h&*2s%sCkL%kF_!f%9xjOobpR|~SW3;AR;Bzh{{|JTvocV{R73K|;PEzqV z;oL>7!I@eCMF6r}yah1lA3;#OLcS||PF0%jE)o|pl7XR||3^S;&&ISff%H z-VthUZ{df})f1-Q?^>;4w!E^TGk<7N0t?b+L)Sr_TeK!d~gt*vaJSK;3RXtR? z@H8b1t#qUko2FCFoV3n$n)?|!fgEs*mqScQm$k?RDfRA*Y*T*ApNtPYlW@5q?}hul z=EN9<>uttiK1=%4_m;C<<|RISCO>-D${oi9fBQXB{f>=MY=WUV<46pW+-9u9D z>uCccp1T=sdiZS>&Do3jv&$>B{&;}>qve`w{;@2S0STh=!5mAv-T>$zz(03SFtH0nz)w)|i+5GF@6n(oSnvgTOn9zYF!}JxB|3QZ+H&Z&EXNs#Ef` zfAG?dR2%03Q$wu7mBrh}_J*gIKyvj?2bYVu?|B92XBQW7zwS~;#>4_mAwbIK7d@Em z*om{$`Itu!`|ut|?e{I5cZ%0AJaHHkWbP0X82z+%GtKqY6AzDL={MIg2oJ#}drrU|ahNazNwnS`Ii+ zq&`%}N40aGp<~ubtMgBSf2#xDb$oO|36>{QC?Z#g%lpN^w|ToHlD|v{h=Ho z=bhKTH^Ad&*D7`yJ1qMst8U5fe*peK0l!pOcQE2ox(MR>!z!F_x1vMlJjU*91q)K= z3##^yEUnq~AyzQ;+(2pt$6fE9kcULAsl*g;F$tZuD=zNaw!)oZ zqamZGXFH1f5*hQozGH;i_j_vLmi`wGkXeIj%XsrDA>y@^6ff!m^&QZ(N*A4K2d{Zc zBKuVYw}J~UuRn-$dNmsyE|sI&Jq1#V$;2+np|%F1JpTZRY6MJGM0A0=T+PvUuYVA{ zj?8*v;t3X|@npX6?Fr9lYs9~(eswC;<{E=h?-vsK`Gb7H&C61{xR%T9Fz-Hva%&S6 zuTDHA6`F+?QP!Xt)S{}1RMGJO@IVePGk_Ji%HOGyHP{)qm@mtzrH* z49SC6{F1QQ*8Am#QAgHkz59L4mvVKtW!+kJL}er~E+e^hr6hb*Jth#FGIYYe)Ao^n6B0KWG=` z0oY9OhqRCH^B?w2f{o-k3i^m?f7cM=ka1!9jHxCRtCXx<^FCs3 z2M*v525VlUD(${GxShRZ`Ql^Rm}!^}Mm4Fb4?UB3EE=SMY`PK`OBE ziEuw`!)8R!v{)>XAdaQ};Kb~(j`pw_iKl+h1WMX1w->C!rdz#W>&LXTLL11EFb#^&Eq~V(&0$zGG*m*fm;qy~?1+kbZkIXBo=%ho%>V!#GQ+dEVd+c(O>3j977{Kvg*K3YKGBF=exE31XT z4yo8E(E0ojt>mgAv)p@hfR0c*m$DTAX)O#CIn}@smFHq!IOYgeaw}PeZqayIznb@A z#z19gURk|VUD1hhO`pP+!4UBJTn~T3oyI_d%XW5r#~(-z<;AAQvbfOzKdd9HU}!HL z@BP(CYE_Qfdc}_W+gAO`N;E1A%ed9O6B@lWcZrIfW$btnr$Fc&!864`XZm}>d52#N ztC}Hgoc`tnb(Nf0+7h|?K4QQC(G=C3U(7UhI)?QRs7jABh*<6%uAt!yt@_p_4rlWV z3{*KQf1lK{&D5bgK=p1Lrs;qPnyk6W7ErWXVgm7*vc}J{19XECTwuf}teDzjw;xjq z=6G^qCY^EXQy#p`^?^SVmsp-pLor%bs$g}J_qD{^9iU2h#Cz_l3t7MF0c%_@%)^1Z zb?Y5z%(Ww-#^TFk-gX4Q#8S6iWf5}gSR(*wa#HL1n#3H!^z#t{3PhIJ)pvE6)XD`a z^*?h9+u9+ubZZdXmf5B_*zOT-0wrsNmP$N%xq{(Mk5+h$zqcNAIQWV|Kpya8)Wk2e zAe^r0--ZH016s9Z42Lp}ZznS$^=x~eag0@9#l@7Xdum(61+g0Zz=lXm8y;@8A9bky zP%eViX!ABk>cZe!(A>amchhpSGnjR`M45FmVe#)MfC~zH&Z|4mf_ut1<{aDtw+vGb zU~N3bT%%Wb%*!e14CQi7E*0l>`GHOr_{>`BD%~CHe{uPss*&nDxS?3EbUtH*WvRR6 zVm~n#9ULvtbsKT|U$BaB(QUv?G%jk`t=#w=P^l?o&5zYauu99eTFR zS=Xt=Gk&pd7e8{56$>VA^7f6qI{rq`) zVP)0VKN7)1XNkbnaRC~xx6C0s4IT3dbNHxqH3^tO=2@Pcz@Q9}Jh257!d|CtVfmES z;-xc_rxTXfq(BKkqdY@8faK>N63bPiC(KQ=gs2&=D;>V#&4GIV09R?A0PDTMml~dh zGnnLMl~)&}KM*$^ca?JuNLvdE&}EYGN677KU(d zX1YhzS2_qY5zG}(9vI$vl#TA}Xn5k)_#shltySZHFcho?yfV~6Ur&(3jW7cMU@g@I zVvWe@e~1G8dg;?lz+PuA(2PdmCqEXXYp#hoEaQ z@Ldj7%KX8J`Ri=o_Qpf#%5uo=z0^81&Tkd_m*0#|IGN4!0&3s}yP(nU2|RVBR1MYkjam#|w<$I^yqh6f z9I!Kkqf^WbRHm=l2K6h0a>}kv%w!2!r-)W<$zIa`0EyxLCB1Q9x!w;(R{KNhrV_DG zX~kZlyi4DC=22KB0OK&zd4U)^LYX3f(Qh#648m@E&vp1=n~q!iI<(6`GYn zTnOll1WqCRS5gPK(dR6NU4*hq3N?xm36Y~T()CReH8mho|c z9YD0Dd588W4Oq@21qk|2S9s}ceWQBd*!ar@wT|x`jZsG6&&g1CF&{^Rlx6$KmMazHez zWb*4-#1rF(kjPAGH+bB`)OVNKCNahXdmVsZX+?Ue229ypLc&MdH7mxmzGf4g_W{Fy zre{pSF6nHJ6INi|3TGObVN*f=Vh3oM1<*OT-9%Ecc)APQD8Z3ldQW*%X&oEC;#*Xx zWn`9gajIq8-1P&ww_HXvF{ly;Qk3W0f*7Du7aZ1mMAqocRbAGr=2#R9V65nzKuvlX z4l_qvg?qPNydB^mlp(y^_=QWPP*U>n9c$tggH2U%I=`5vma4B~Ov+v;)2KMF%&Jbg>Uqie+c2w-tOYsuQhZwGU#%#1{=K39CHbez@S~LAh0!B)r z>zFnv=x^Q%W?I)tEt?=PC;f-NXfz!7fD6eQ8kX=m{{URJ*wZq-xtJGKV=v-&$N|IN zThRC4#JU9FRK07;1K#E07S4%T#~X(yJ zIt$Pk31GMkA28S4!F|s7%&d+t06FO0HT*#=6C;o%op(9L&%R)3;Lp*k!*ERGw|d53 z=z{v^;bEq)Ds*C?K=2zJUH+e(#i0@waDMR)u<53mHuk>VVfc`0;I6YKm1he^4|oe{ zwT{OVG@EWp^@wSiw={Im5vuoSA7TJ{oOtxOL1`#LDS@iR)tI?cn*#_Qf84`(Z9>jv z@;7~=ZNgik^4(`WAS!8Xse7(rd-~D#7=Bn#jGPy5wZUgaex#&TR?ikqugAm_m=zCm zC@T)qyNg1Aayxr!UX~PXcT*M@X=_?N@Uapw(x^(WEBTc*WS;Elq;n7odq0>1Bgt58 z@rc%|ADu4=OY|VP?{t(x6>p2Ffp_p)zzIP}d^H8eMC$Mi8VSYo07oG7v7ZwL2MWUJ zJ5FA&F(a&3?f(EV7Mg`x_mzN2q?Yl%(!m0S1{WqEu_hH=uS50HAe2uLx@9_2jZ{OQ zL$n-cnQ*Fo$+yJrnEobH7kywHaS&Nr);NoRcK+r|+xSNKXPET#;qNgI$EIul02LE( zDpy%m@*ad)NLFW#uq#t(*nGo7lk6ojb(GaxkM?KG~> z-ad6KOa0hi+#9vK^KjUYN`V{zxvv)b1TB&43E`bj_RpEu-{vq`sw_3s)A0ZnxX?Tr zn54HdHsE39ZPY>Hqm{$N-a&Eg5h zW)ZDQ{CXFrR(%!bRv<*gHv@iwh*isjxqL7jPR4XzWxZp3^DC%D#{Se_Y3-Or-&6cW zoTmQ(Eii>Hh#& zx!=YA0D1dmAGrS0ByIA+`hfhW@Qv*US^9`8+WxUYiQ@}uhelUcv=9h=ln|`NSBvB(MEoJp{Dl`Sh}>~mAYROPETk!Yd+BL ziHzLEWgviq(P3Pq;k`gOPTunS&bQW3?HP1c;hg=$rNajBOFG0en2>5#;aAOb8oXUY zyPji($Dl7PbIw2A1=iVbg7_D}MRw-B5@-sj+M*0NhPDz@~eB*KF;!`&ane?n?QRr1S zH?LmPtxEN-nuhDN8@Z-n0`@NUAqT)mu@In}K0v=Qv6iSynBY1~H0~2>yj*$$s~CVZ zBN2w(#t-0(WJ;QhP}u}q(y6aY_3v;*#KEsl&@nyXG4<)zy)GVMhF&H7%9)ib<`4WD zgh`x(2~dxrQjR4{hM|lQXB57lxMP-P*msu~yy8*XBl@Aq@S^u`?2gxvX2S4Ue^Cu5 z+5Af`_4G;=7Y;nng5zP+oLg^(T&~{l^8#=$qW#5aZ@v$y$_O3z&O;yh#8dr#qFg`s zv{=BpbNx#gchn!^Ey6r!dR3q89VgTMW0IVP{&6feTdm?|*BL;ym)Sp26{lY8OHJ;@ z%`EK#()ke!3>~v0erUrmdmk9H;nQ(3HFq-=Z{UJA{b-e_%gFF^oFl%k;xEFk{{Z4D zX;*g<(qZ5HOxQddSz@}o;fmgG;%J$t6UtQ-4`uwyR+qmQDFy&M*FVfOLW(DA5iqQK zL(E9MVXYG#{cF>jPHUw0^yVEsr#^>oO;}C$S>I?~kReq@8;;F4c$lY$UU}^b;)IH znDngRnCJfh9cB8xNNQL933!XFW6RL=N>m1G{{RCWZd7a?o92wpcpC!Sb+G zxoXRqYG;TpqLSq(c)0A?i!a^6U%!tLekuIL?$gNrC7EvVi>>DE{wF(sPw^HF71I|* z?Q1r34sDp2Zu1^v&!evhbmA3wpPA34efsq#E>zKTt3T}J4xM3eKO~@S3+-_N*Gho| z0r`cKg|*NH(}_(iR`^A;f9ps7mMrGf{)>Lt^@F%DCT9wGQM_W-4bTaG9K%85y? zVS#bHL5$8l3{QAHVH%uN73p1NbR|qwOw4qi2pwfg^)Y^)BLJ^b9){%*H`ZfBu8_p< zJj_G8IQ2aGW*z5RoJ)hBTugH@>x}i7g>&h>K&TsrIhc%Dec>;UOP4RazeR(&a@lOT zY*$Q5LEIkjaR=THHwV5sfhb~XX^fV_2)?td8`q%jUwMu<7F+2llj1vydKB2ZovTnc zAGxQeiRM-=2IpLQ8m~eSmki^rO0B2dtBTa<#AX|E!&&1nwsiqatr2Vs!8D7$)9ixL zN<3a*rz$#^!uOioojSlg1xpGBc;;LlzL@pDPQI7K9nK}pR%PLYYtUuc+Gp#~mo8i; zV^f$`eJ%}p-Aly3LdaJzw^^RkJ!UPbey64SACE%4IGCA-sRMH)=MtxB=5qIyIAeZ| z%+##FtAlg83cJe3pg5M4`aCrZz9maWuQ=36%k3GctxO-nY^7y#uoD#gL%@C}g~F!( z@!dOwHMk^b;#0`qGNG92E%=L@jYl`$qdCu}b5pa-CAXW0JC3Ds36EUdr-lYI2oMj% z$x^TICx9~mZ;3%;NH~pabnkN+`iweM1=Y-Ov8kP9?+9fW#+rvdAhzC!+D;!!%x{Cy z+Ese>Va{b|c%5#)6dh;*T=I0Qpi)BvyOnQTeRYWPVmowHc zul!=?iN}9JT;tObsm!U&B_5dc&skEl=5|VzD1;Dy-~t9Q4#`Iox(CLhZ0jlME3T!L zH`h~_@jWWomed;tjwLjQ#1Vmx(CEwN`%5Lf&$Osy8~&yiSLPu=UxHr8ePs>3Uh?;8 z`%UfQaT>SN6&b}xz9kTjd4Z2i-0;G~QuEpwY&!$5CV-FfiVYV&KygY^qZ9Thfhq@{m&3OI`j?H z%~a$600yRdPaHw}ne--RIfdt=GfhL+aIE?h9=`JxJ@L%NL(qwgxtAEF7d;`WEz$EI zL)N}?h@dPt^Nq&R%(HNFX|w>TBx|O z&Q)4vuVljh<&IA}KXUIknqs&9&t@%mM`QR(oBB`U4_z$}HIL8pZ!_&k)0%8~JLb>7{rckeA zZ~?*jar%tM2}7=t=x$g^lRjcv3_Qgf7tA(Wa+MqtIyZ1G+zoDZ>*-w<`J88P{CXP9 zIfwk1th3+K8M(t!uWy)YFkY}>+kH4oGhiP{>ong)MsrZ)moWd8+p{LL%EbCAnHybd|F%piE2w#{4 zvIacDRH@D@aUPC@%iZWi%Xbz6Up)QHw`DzL@3++FaO&|fd7MGqD)j~(<@ysB8m(q> zxzu1D4jo3BQ&y=|7P=3((2E0J`oJf{*!MOwi-9nTnO6Gi9IB=^I*&6^t3%VYJ!%}>;rNYP-f?q_^vnn z9``iQ5d%*57IMe8h=KuLB{j8R*(~I zrPlcLRG~($RLsQ39;P|;I>Vdv)#Xy2mzNWRrsqs4SdOpyG z;Z%(A2xb+S!!sOCG3lcT*55@E>l)%<@h?kv^%HGE#0~E*-D3t@RJ_8o^)WiYhNm*l z(W_SORwXsKUmlswR4XSY=PmlrF){E) z)f|%jMrO7$yvw5sfC*SZoFLXFWqicmV_NkJ^fk|?X_=m3oLtYZ6HeZQYKV_(hTsRV zOPJV?dFC62b(ZrqV~9-Vd(K^EAQhR`{h?ibYu3qq=NN|b4(#zMiv&(#defL8pu{Y{s9(G}l?hY0 zIIq7EhPbCU3cpWyLCZMr4rjdaGhWcXJs5X`S6kFMfQ{U?^HU!X>+>6wPkGiQgS2u+ zl2~e2SB^+#$&WDrAksqEgOB#6aChbwJo%J|Ks5sHgDJ;@GqOC!^E$5)sP~!m&&1_t zI0;WM-9{YF*C~VueZccA3#`B?()nOjt8s@`P$}CZNHl-58@Ah?W-oB7K(>Jf$9h=G zGb(dA^r`PQPkEnDe?-RNuSVyNl@;i5GYwC;6buC&st}yR(e$ZM{7gk?;4PTV8nfDQ z3!T!vFRcDy%qFIKO~O<)ox*3IPSD&sN`WylIGMj(lIDJ3RK-Gmp_xnt3^|%-+IZA$ z3cW$NbLq1x3uxQO!vx;MxW}*}DW{*P_noIi>pMd08X=kf)TzcjIfqzk=T;Y) z$EOfIAXTPzyWH-l5(Y zeQi&tk4wxchcg`^jCGAq5~HJ`quN(bQ_}I6XH%J8oZRsMh8!`~x33-{udkSXB}$d( z-Q$_caN=^r2V8nl0F?u-Jwq}2{{X@F=+wmJhe&Q?pH3mEiObA2JZsRKHq_K!8oIzV zDzOYg6YxKM-@%xRtycP%bul`=rXkdo>6`U8INT;?K0OMBU0qJsFx+d6LvwqM321l7 z)9=r6Ac=&`HI^gHWVnbBb%ks$KT)jJ)+KJ9@vdeIjCCIHpq*-QIQ8aso+fUm4IIzR z%W$rJ0iQ$hObx{!VTa}$#LqCqV~8%8O@VhgdZ6nxL9CaS_tqdb#=K5ocOxnqeLGHN z7m1@YGhUS|(E-TJ!V-y&CfR4a$EErbx=JUHN|~=j=^w;7z4`>tp*&9!)?##+;xjV6 zGXi1-w(&9OexP;q)awr3l@C^A7hzLZ0vo=*(1qOu&<$Sp%lh`8m|b6Z>&K>jp;?M` zAYUY-;w|(boPXpn+^3{F&L(nm13vkL<^;Hh3@ZH*>dNWpO7A>O<{h9Mxq+Av=I7CbU&8h=3y4|{^5RsrsNBZ= zrg16;)!~ns%%$A9zLhndGb2{u3TT{95X+tVe^Zmf%tL;c37ME{-gYNGyws)i=THw% z)H(Dj4YJfNF$LGQ@i`FmH|WZhzR;QJ=n~e^KsmN!e5!Jt9v;lNSOP7tP)hrW6H1*s z)alwi4dVc2+oJx|>L=nOf`0E2gipN57gLDXf3;CaKlk)w(DF&AGR!`jRsj<@MwT9YMkkK8Lein6GIhS zpvOJfqZbE6AK3#Sb?IL5DiWXrYVZ)kz?9k7gXNS;*9B)0EQ(>L)K+g4Tq5B{*sp1d z)9;x67$haw;u*l<>+LS|25BRW>m*Mf!1*e?prRHewpTW z^x|^^6RFP-+B{S#^x9MoA#=~6%%TCC;vC+%fr#I<3zbmRr`uAV^BHj}9py^tuU4Wa zIGm@5v??jLrFD&ln=OZK-4A)fznTpw=xdVhGUNfr5y6UG@fV8E0oZg+{{S@}YNkln zvG4gaFW2)c(xp9dI{H+7Gk%_8bo8#CfxDbRzfLC5)H#|-M*dcht9{|BW+S=8H}4wL zENNt2fTQmHxO_3v<4^Ekn54@ z^En3udi0NZn6oicls%wT*RPLEF$nYNnwJPbhUbZk8>~!OREH3YhzOe7=Wwc7gZ#}f zVo^{&B6MM1@$cw#m4;WDn11IkpG;3;HP_OQNvvb~iCnrtoyRnNkrJmgu_Lb@3^6=vw{4IKbpT zwS&D)K{cKl`HE{7X5Y-N-?7(+e~7glm6T;%226GB0bQDo&xt^{7V5FTcu1yOV-4?! z@GOtOLx101kMjXylnLy-Qz8|>lb?Xl^-WtruF7XcD(YaX6(wL9dC0wN- zx5i)rj+vP23mZ?%D>kk3-dK=nRlelWI_{N)lw9HR#MUL-3wG3TRf@@n@ejwN<87+DUo#fQRv`}lm50P(T}jFPR88IE5xyn$Bey%79&iyimodh* z9^E|`uA$TMGw4d`xt?2`O1bap#5eB>yucwuD}I3I4f6}d&# zRtIm)S|&Ib!j0Zfcuy$O<;wib=4NNpsVaz19+vbsi1pW3Y4O3+t-{aLeuQ#C(++e- ztW3C+>DHzm^bIx-{KwKx80x24ps+PH#SuL#iQ(-z`o-CSxpvn zs?EOb`de^{wm##CHef8Kk$0&`nh!YSKM#|n3}aM2fQ4$Q^aiCMH!i+D)>rUeOr5uX zD`qK%U0vprGYe{Nv&q@1`;{s>lZF961r;k2rmnp@u!ZNP&nR;Sp_oqnA%dDN!Tqri zdSHcBc9qf^jX35QKH~lX3zX!P?7kEAANCBT{YIoa$bZO=M~H-10U3C}*C&V&!FCM? zFu*~WwQCU@aYB&lFbwyeXWc}FjZ(jfZX+3A@dA({IkLYnah*U|jn};B3%pF4baOLc zLhTPtCLhE)be-a$bi7~DC{RAoiAxM(D1c?jDK5JHli<{2IGS!ZrGR|S{{Z+7r1R<+ z#Pcc`^i(az)gB$b{I;P zGw5B!?>s>Yw;O?XMq-CU7Pj~^^%11Bc^E7`;2S4ouZ5uOT(^{v1&1qZ3h>optbW^G z-6F>_?_c<@MPeGvXB2JUWq{8@clf%%*e?B-ugdz;$DWo6P(}dW&DH? z&e?#+_RI!UU=V;%Ng<%G6Qpv5tcO01*H< z2!+`5(&iU$HvxK+kWW=O>6idK0|uZ{QoKT$O%xUH+E^i>$B1D0x*ho<)q+;Jmc zu)K^fARh7iUDA!hLUZ>qnNZiJbU?;x{VA!7!~$f>Ka(mB;|lq|xQVKhP4##&;$(bU z+Z|K^d?~G`RdXBX+I8#f$UipzAsHRj@^~xaU0~enJ4$LDrxNGl8HKW-W>OAAJ6NB% zN_$N_KwBnvF24lj18|%dCP%37!};F$;=1}DF|y>QK4y2*k_A!=RNth*)!bX#w_YYw zgyZG&{rmze#UF;=O%FT58-6SHj=Ad)-PY z5EUA_9dSwYv{c1vA0r!o~^^si-9UOG`i=0H01Kj2OL_*MW z(%)ov)C2QBbBGr!Gq4+nv@agM8+_mV&BZPi>H0j{6SF4^iQ?542v_| zgm40@qzPyj{@A$MfZJBZ>~#BwkkP>I(&d8_a9=bG18#H~y6XUf=`Y!ia+RTb?EnhI zqqMF0VKqA%ndV)Zwc%^`77t>h-U|IxY*YTrt#PQC3L1@C^o(b>rkK~G=u16nb@ZrD zFj1M|48XeXzbva!hIt5yQs*<<{r>MO518VXMVTAv6=a2Ie~GL)`Kzq~o4*q<^dDIJh`0J1$ zh$kiF+xE<7&>3J!0eKl3bN<9lUyQ4#DByGbG_Db85j;z<%v-;f1#9*!qvO)ly5m;rSobt`LMH)YW;Yyj_ zieJ3DYaDowGxEyYrx7FTnQwWE)Lr$4u4AOBU8O_6sa#5djJ_dI514a^)VWFSVr?uD z>`ncAVg+bSz|;DS4j$k zO7^d(N8~&~fV*f{wYWX5Qws+OJ~j0W8k7r95Ie)X-lbGEsE!7CD*pg)iHh-$xAhG@ ziIJVd%x-r^3z#uaN`VLuw4Q&VEq3q+v<#v&Tn4Ed!7|m@qO<8xF}P2_86Z}g<1RC} zP%*|G80q7Qg!X9;EN`lilVY1&_EZ$NRDtaaHv%w+qtmygHp8b5C9w5CZ1C*%V61aH zeTrG{aLXe=x;i?3VvL|JXmoPa1-ey7V|KjZHkz{;yA|2@bN4Ju-c)>rO$~Xk+3zt} zhP`>7-i1TgDUnbD_uJ}OD7(K>`G~06yuaC-Uuff0?6_5*%y?6}W_pS4a{gnRpwlZT zFM74`-?Z6b>V*c;FNv-jhgKb+Uzhn~wy)k|T$U~|O@Evc^5Zpf?OB&KN*PV>K0e^O zZsnvZUdzzM!*E#0_2p~l3|N94EEn&O*QdNa4vyl~x&xN4H!QXQd5!0`<~0@xbRpta z!f@kQoJTSA{OSd5kLGbREuLk=Jz>wOx!pf7da&&x;ER<#PPN zid7lacV-Y!6$3XhQl>5ja^uxmOGi?`683Q`tC9FS5bAWE@bdx(Uq;Hvumz+!yU5tb0LA{ZjT)r+)=?mB&IG%&1Kv2WLLM(=wsz9cE@Fd5O7B9n3Cr zl%DgK6LQ&{E^_l-p@sUcJA(-vRzY0%-|>NlP@?KB`Jr6#FC01oq$9Uvl>I3V`=~f!u04}9(@qB*~ z43o8WPVwd+A_#Jrv$rrXSRFg}m)(}gJV$6yxUbYYv{>MC)RqAJi4akmDgD30DC2}Z zdG5o!vAts`+-9<4;#5=U~zZP!3Hwr2tAdScO$^BmCgIW+XIGFuc^nh_VliOG+f#`k=NAP__wG`I7v0wR%s^PMRXdn80l1sBad^QEbh+a1(K?=)^v9=P zh(o+J;vQpikv4V*{xIBdZ__miR}(lPrWdc7Tt4u?+dqkm@IX~W&44~8;fdyug>iQl zDaqY}s6K~%#1+=;m+=IneeIvPs}tH?`>fA13!XmZ`WGq{_Sx`X^DG()j!Q|8m_!@3 zc!>%r2=hF~F|M&548j?N&H7xt{d%6edLM`qiE`dRXOA%(MqI+x8+9yHs_N%VfbSEq zV0P3DZ&Q>VKWoqjRjBCG2NQg9ynM@%feMuL{%pJ5EYzHX1ThB(Xbj9$=Mx;m)2tdY z?hjaQ`picLI>yc&Y|9qrzbrfvQ=2^ZciIkM&ZmDuB&X=haQ|>dBZssv9XCk`$ zxcTcCQuKwx1avf6@-2BLE)2##iAS~`u#*-zQ}Djt^CnO(x#8E_0LXDyvD3s&k|#-6BmfivZi}fR z?T_3Vw`+fo{7yPYyZI1g-Ipk5=Ce@67^k=~^2N-*L(x48&bR4`)Y@~IM+HxLugpYS z*=NLlpkn5phpho?148ZSK+8bg`9IWpV0D%F`-vMVJtX3%h-$%oS06IgqP713HTN4t z*bY^6OhIk)Q2?M@S{}74F-Q8me-L41g4C59wI;O=P!|E#UMvS5(VKDAWq6n3NO%Gi#@ww3+!}(}M%TEz zxQ5t*#8bv92LZShF~lQ+K8H6cE4P{7#nuw0uU^m0T)1Etu6L-DSqb6P90r}$%=UT|IsUt_;jlQ$ z{%$7^127=(O3{nt=^cSj@OKeX!<&E~+{j|R$Qk%7@~QfPsZ{p9e{el$QMG*4hiP#{ zS`z*@E*Y4Bi_Wz!o2rcTk8eT*2Dd$%;(77nHD#UnG59ezMn~cWI2_k#t`*-#19&Sn zm|dLn7BS{H%ylyj&UYxa>*?(pyW$lya+#jEfz}oAF*Ih}}n3a=qXkB!>j!(Dba})DKA9P>Z1Quk{59?HuwzaE<)NdSmDuUx#vvWvUlc z^D5>WL1gB{r#Oue^JP_3xx)NPW(W6@6Z z6R))KxtLx05X|Quhfa{LW+xNRrfblSSS;&u)ZIf&_K8e&;Cg+>)TlbBC@3ca5)O+ID(4+PWKw(drF)@^gKc79)KFrp@-)WygKne z*AEjpsePdI^Af~O;egSBMY!W}_Nz+3H;c(>OMRCLulA$;N0c4>#(*R~4F1V@8sE%H zNN)L~b5Q6P-lAW~_{3rMAP8rSaRxTGQ8)nKRsFLs;f0~bJJWF4uv$p5J%6!_Z@h3+ zx&_tVf-AwX^KJS)y*p+WY8&|ai)2xJtwA$FcnZw0upDk2snPFKWQpL7DUnu1cJ*Ud z+I0?QdPaXR)-^epGgAW2{)q~hyn1Kn(UKHcV&I2p>f)CF0KKIsjbH9rk*?DCC@@Oq z7bvhQy|(YiRf@@L#R=5q$5aPk3R(XEh%1y$)Xxy-(EMGmMmw_10|87u?=4I1zecBN z$DrJ-LRAgbx&yw|44_ir;?HNp3(fmO(5%lMJvp8rnQS|VpSg5*3+gHQj!3K{f$V!7 zdMpPJL-FY#DJtoUqY2ju7ona57;#?{Nv|*$F`nf^Qt>+WnDj4xiIqbJeJW!xxE_Z6 z4^#0j8J$8@CA~DnT!FEht~!Q4CQ^0oJvo$rW(qpq-D8ui1lek|0lvXfl(j;r()*?I z)DD0r$WA3n$GpcrhP^V><-c5T!}XtND-OzrMN5XKnO$Z&!SrYj)&Br)QCkDHy?K3k zfHrZV;q!@X0eBQWgC9_0OcbuSzVS>2JJpBqOXT*2%n-04XG4zg5D6Vc#s10So$`(X)mexqF!`&nS`0{fsdGOK#8Qnm_guQy$za0S>)&S9>jk9b0>*F1BNcwbYz z9ZJ6|{K9c`VhVyov@p_FS)O9F%M1X_-e7Sm z1$qGKrq3$Cq^WV3S}@ZBpc%S@RV`hvs5Y|*w4E;7)|%<}03~~-UafVGzUN<=#HpL@ z3bBFLXRAb`ZjRI6Coh-^&@=&z9Rd#%Fu7ARmSLzLQ8DcdNd#LS6gRK7V#AKCwMVg2 z#2t2OXEL$q+%)!uBu6@mAlUI|`Co~euCOoCDt5kJG3PKWBCO=li<-yGsZ|Ti{Go_n zGl_j?9+`(T6ASb;3iUsJzI`+CJoM5|XGfOPk^L_KwBWa+`QNPQP;=oZ>T&K$NC+j><4=BQ=~{YpSx@ z#W>Ahcr;;L6Q4{(=^afB9KL3SQmXtA5k}?_V^2SFtaHQxf6QcsOx!B^X@R>=-kF@e zZeqO%>4f*psaW(4)XmfB%K3(UKX{ydPJK3G2tpIDqp4D&KorgcUmeqriJ7AfCx}9n zd&BsrG;umuRL7-yH_W%hH)K?6wer{dyay75Gp_#Ai^}u^%JKf(LRg!q_PxI(vm5Cb zTK8D)t>3@IR?c6TZ^3hh9%CAB*uID~Z1#4DMs z8j5j4?7x|AiI0iO8L4Jr17wsW!Axs5)yxs^IP~$v#8#mU@6hIErFD!sxlH5Kg}|In zy($`iw9oE5z}xB^JPb?VZC(N$D3MFEZ)3-Jawt&Q;`&Xlxra|e_?&lK#XM;Gjx!)G zzJdOT3`Jne>btlf5kOtT(DYZPVdxske4Qs&8fxw(W>WK)U5I)kO>W~+Mlc^vV*4`~ zREdLb3QP96P$yM?hAuf^_1Af0A#L*xJLcR)&K$D_a%Ll2=ZH3kx)>%{8`k3whDk6Mjs&F%^bR-RgTcmDt)_u8EJM22z+Uc76l38w+EJHHOnk%B9*sj-^u?Xb%6<;>Je2MD zJ$t^;BNzifVj7e}4)>t9<&g5n@fNzR^=ATEb#R8?Ft(z$oO77w!mI|buD;ntR4WH> zcRz`1wS@`IX0t7p;hoDjQKs!L9Wyar^i-VySy0PYP2TG-#2jrlNutcYT7YtZ!AAf_ zS6Su~y$Dk0P~5~3^f~_k#wL2ogt+O4SWI&>Of?GU(;k_fXFjCs26F5|>&Tyv5fUm+ z#8O$%)v)>=TMXS_v=%A~tL0C{5vpfL9^rEn%NDKpr>97vSkCi6_NTlXE3E$jzK1!P zy}>Dz9{_iO;cJ!XcdzCN)df@&okO*8wo^-#&hWwXOy8Q9EK-Exb-zlKYpV;&z|XuW zJIl=DF{{QkJVU(4NL9m{o#FQ?RwWU&JZJX=pa22KVs6CqEY)d>u60qqBMMN{2@|D5 zyv%VOG2HVV%`@)}_>9D<&!Bo}lD#_gA(4W9qnhIf?0hbAuP_`~gw5Eu`*0|?(OaHZ z033QQGdmt&BO0>6^XI4WD9_y-jXQh8AHXW0@#~0lJ4}{!hlx_LH53NZ4E7*^(2KJ0 z_3fVU)CBT*SRn=yC6a=Bui%1Bjdc)H0Kz~$zvc)400642RS*>-a^P<6-*Z23 zMNO*S>r4A1UN$7gY}4*A@18t#RVQeO2G>abZd{6>M*b#GvB*dAbU5cV3>)B#&k@ur z9)X`)l`GWO{tojVhk3**y)V=yhax`L5a2@UY7?2BeG0jvp??!&WhREb-1jDR-{p^R z`KZ^2J#E5pkKDNQuL3In0GQ`fb=p^6FwF$yym}R=6)WmF&1Ejn>+OKUgSvR|AGxL_ zO6|)JIwQ8_Z|-4r8@RNlddkjbWiw(2d9y3nmMaUd6RGH^9bvu0Y~P{hUWWLc;tG%e zEwAPfVmGaHKi%b>F?Zq)d(2D98bMES75#m?g6MOjtxqkr)%D! zK}v06a-Dh;X$$MKSNUPea4MjjO!`qPc$LHnTbSl`n@4xj^J>02L=3vKDtFWPCCdZ4 z{{WTsWp(uo2_;GBmVRG|wUk*_Ir9~>MUCA(cQdunXHP}vvtIJC+FaiGNC$X-g%YV? zKOzvk1Xe=ZT;!J#J29W_3aaVpi1vn=#v$p>4?=sxTJ<1$W(Qbbtxh3zIDzR;Scf`| zY?)#e&Y~S=pAweCzNZts#BZ#zu)f#H-t#iJK0&1VW3O*7wc)RlR&&m#!5i?2EGVn) zKbwfix$~-GmM#eWSf24}E-wTDyZ&+bU?C4er{@G=w}SbOqfTP0t|yQd`?IX`AIE0~ ze{t@aQHxj^*f#T={$K|5+5D;ome?Uj#my&VR5Z?5Lz|qZjYIP`(Q~QKFw91#0JU68 zq`D65xq7j~j0IvW8^M@oH_UX_;3^eakD}ZsL(*u+n9t04nC1t0iih>=^$^}q zC7Aaw<`hvHG~ZK(7(i?g*R)YDY@?;NeUgm^5iZA)49oMw90-{Chv5K8cr+A4IkTA7 z{T1IVBd7zxh%ul1^D0qOzoFKrKB0_y6N&hS{CZbc)%3<;&Ch9wV~%D4NON-gFfp0&EOb$AJUIK;KztD42%w!bl>7I&IXTY zFxdv{h45P}@72TZ?D=J?X7Y4+w|^5uT{X4t=k63J6lwsV*uFV}=7PNikn-hAnVE$V zm8QHL_ni72s(9NA_=Yx?V|vUsCV&f{5n)hVU$CO@_pcRbjBg10CAPo?+nF8sj9hiFu7MprN{RH%9Z%*J2`rOrLeumjoncM(n2p_6+5q&lJJz%d^rN9I4w=R4>R%pk{aA}LgTi};#K-G8)V`)T@_Ls)@(97BF3L3W>r^Bn9E z`6hOk%u?KpwpA~p6C%6CUaUb=J9i%LBlQ{YA8bOUPIn)9nb=_i(450tftCaz4R;Fs zTt>}6yI1RP{eg$JC2)E%`f%;EJKW0bV$41c1HW5f?ZjNqTf`0K9KN*+oHFVGx^*4| zHrn=XJl!Bes_=E!c#Y`L<$azd26UtciHe%ZHFeSI-tEENLr|^d5|L1SSX0zq^?;EtPFg(!*4k zT5hkY;!+T%et)tX7(+aNL^tL;x^dX_r|2IL{vaZwlkEBss>gwxOV5ZMGZMX;7xFLy z@D6}*5#`6A%wa*06H+9dkiHP2%@tFy1+s=H$ z&R}f1xrk+{Pw8ikU9m^OA4#w+c^mvh6g17i-*!JS@)9546Wp{jFSF&CA{4U5JvRBM zX8lKNxzX~$R?Tx6{ui7FLqw*njIeGzs^5IhOdQJS%Ex(9<|cDHb(NXNr!34lhO)|p zJr?y-x20Ri8^7}wvN}Jtmy6!Mg{37Rzvni6!QW#@N5?&VRMf9ggBEkGzR+ozgEXr? zPnnIF)ZimZl|k|ls$igXQEhEEm8tr_Uo6jgc=#j3XXS{ajmuZdYMp)o?I^HMp9l2? z*9CbIKN6D!USOvTrRpclc z9q7uvphBh{!;9&06%}Fy1k28Hm){Tp+703=;ymVKH^-uMUBq9f8;5o<6DFdeQ9ly4 zK1h|lD5cKfb(!LI>l*~BgJJD19&-mzxJ9NT%y4=f#u~=hcZbx?;#A^$!|T+UgAB|r zm=h^~c*HIQEAGe4EtVR}=Pwxe%(beCiDD>ubT+uena;!&Mi5UBEx4%R##j7j5 z%>2DN!0S3e&)fXlC@9qj#1x?3=M%hRCdN~|a_BFPU~4Ogo;$aa3U1GxjSx?2rmUb*;#5~q?=_c79A*WP>j6Hx2u5HPn1dD?&j zn}3)(7+p$qJRg1_)G(gq*t8#%&LdM&ggxrR15Xf38V(+f)7wzPJjVh+hMuQ>@MIKD zqojM9VKZ}y*U;N@<{>ZYhB+l0c3N5QxqH0a4GFhnr+B<)^5)w@zVR*?)huNTfgb5R zu^3qH_zRfTTE1-j)H-;YJMG#Iw(i@k4*h%imR$h1xAsga8$hpi5`j3s=k8+Tj4Ap* zmLM$M)TfMN^D@fO*6!e7GU}rO8`B+mi-<7XDkL!F>>>AcSoa0hdp82DMb%em4F}>0 z$!!#VbM?x{DZ)BeZ@ZZ26<^H0G9ScufGq$x4?|M#k%96t0ZCkt{CZ>J88Hp3I@`EL zWom7Le9JegBUz=*fF}@dP?kZaznGDzW)x*0@#gwLD_OYhiyq#g*U&!ZO~R#T(DW;r zMQ#;P(*cFbnO7&fU$|AO%WJJiaX2>fE1KwU+9i2LW^R|O0{Fmi8a)VJZdTgq4-j(p zhx2Xp1D0k2E{nCsIkx*it_<&Yq1$yZjFPqY>wWVoaRVNlG!mnMw-W${rrPTWaDX>= zShH@K&D@;zf%1SG>i2ucH!RYCst?~t%>kQr&v)V;HiJ#sqsv(NhKVWU^#U4tA)QGiU9DARTlpp{H#0+3g~3K+r_rk zvy>33<@V25!bsS3k4T!s?MYui#&bDMCS7?idXr1%q;pPSRkv1Gn~acz93yr@?z*b> zX0-dtB}X**qf3M6JqO*CMIK*p;P$8mDGciiTbJePh<}(l60Da!pXOdetNW#}@{wuP z;qeX0Zz)&)IP)T`ujMz7I@CSs|&!sZx-x5h)gu z*sj1b{zIY1RwG_ctqZL3@$(Ct1pD*WFQ|)cC@=YWd>T9EB9jL}%GUvznlQpG zHoqP*Djnrtdf@NU5Ks)XCl5=l?g^;_pb1V1R!!VO1f*{Lm}|=ezgJrEQuDss!FuBR z2*Xwxa=jirFU&)%n3e&R%U90Q8l{macqCBKx>XKDMFAP((X_C>-7XBS!7U(*Z}%+gBxFYaPV znON@ zz{1}S-yfN&nA?LJmD9x9pFj9jx4fVL0E;^t#BDcM@fk`}2%TW4u+!>Gb<#hH`i7aa4a54JO8S?Hk471}n}vMl8I*^5 zqA|jxK3DY-YQXx2P@{a+e3a$!P@@IImzEVN;ehkMK=_9qY0?g^chH5KyVcJ6{{RSM z6e55%sLMEx3WB=LqL%C~*Y=P$yj1?mZLu zQZelty6tk{^_RTOn1jU(m1~gi$*jPxrb3Y&Ct0Srxq-gNv;b8+Cl)FC8+(9*5zyzm zdG5?Lnu?2=s~lJpf8otWuBM@H*3Y<3Wozumy|1(<5Uj0LwV3wDr0*9r8BDdS5f%s7S-C_-NggM+0urS+V9T1z*xhn>Mvg~s?AdZ5WFt@k#THu z{)n}vbl<5<5Y{TbAT$s+b47J5bt}=TdK>XACV7Whh{9Ydrx6Qxn0cXPi@qgC0B665 zR}uG*6}q#;Ra_}MGVOE_S{1_qN?|1sy>k{j)(O@gXPDJ`EI67oTILhXs5aCwec_IK zKrO#hoREb_PkGWnW16{2BlkIb7W+$$n#|`x~=2|>b1Pn))A>k zmP*-eck?f-#_x?wIZJa3E;NJXHBW-rnb}1tI}Xs;44@S&p(=9ptY>h99=?NqXB9IE zz3N;g@d#=VtkgN3<>+5GMjFqwKvqW(1z-lTa7ZrY?Qu{t=8A~5k{*s2fYsKZY{M9+ zx0TD7$HZgWsxaw1N1v(JvoF)CJ?C;7eJW{)Y78v7TV0pntNVP7#+;G8eAjdQ%z;Q=Z9p+r!${Jx1}R-oCWTN z8unNscif{kVZoh8Jk2u#r2;XaL@JA?m+{_Qzx)I`r4ToWPt4g^- zkHPaOc<%)$0_-&5uCOyP_j40kVEhmXR`#YcJk7U9p#G(spsU6DeqfU_fCtoNLs*_6 zSK=O+rV_lu61^%^sYPXpNE|UU2B>Eqg}0e!T*~VZhy;V7UOPsG51^Us!)&95IoiV; z!HG#OJx0j3WK7KR&pwUQNxzxOR#Ia>`7-wC11;+Y~3d)?UX~@dIt;ZSMfGQ(sdB2)99&zJ8}#*AQFag5lnnGai6} zd>juHeZsw5oms=)k@uLwFngbifZLf^NGqTG1P#l9d}9!h3h{!f{FSV*JkZ_R)z?wU z6of)2J?EXxOmv4m<#e1ugeL=}-HtN|w<`GlL#Ir_A=W*j#%v&OA)5Doi(!d37%@cH znYb9oN|l$N)HhIz&h~&;d&H_!F0ab}04Vg*=w=;g$J+$92y4KsWjfu(O5s6#%CB3C zIZJ>w{F*8shz#?IP-}q7cUV4Paf=|u(AV4{q*fMB&&|Xm!Alf2^>YXa zW7=k~O2pTHLlEr_{+v&bLsiye(z%(r;#w{yX}a$kAf>;ULR)~pX=DsTb2W&6*d;cU zL?woV-!Bf0TRU!#bM9he=K2y4;X7{hf?I%*lo%;tDwIQ4F9!*8Jb zhP-3Z=3Z@HFSS?P8TJnH<@at{w+0my64o~uj;)2Kz~kSv6v-d#Vg)vh=B7{%gC5s% z+;uQkqsTwL-FcVSZn04jZ;T&N@dVQRT(GYVHT0w~6>pk)=KeteWB5@6ePhhBFEqGb zci@ESMWb%l-e2Cb(_h&5KlAp4#?@)x4U@@_Xydz3v*->c=j;upWCxX3yebH8FaaoS{Suxy7OfN_9_p zcD8Og4j1n6El^F^{k%jV>u@+Pi(g_rgQE0OCTtq{X`b{Eo0y$_4(zGcA24pOCGL(N zk&ZikvIFEFlko{?FXv4Y(LaB1PNA)@SDWj8heWFB0~rI76}xmrG0gQwjB`)Kxym_;Gd^}aqO*EumVTI<7SeBZRYJ*QmyUL|Ky=z&BI z0ugkkePQ-3ZMm137&f86b?KR82;e>JsD5K3jIxUGW9nQScc0Abe*NYU%KZfVHw81g zetkKVO?yDxcL%I{N?Vf2d8z*ZVSiqgpB}7c{o$Wmx`u2xfthID(qbD>Adz_} zpOZQcu<-*LfIu6k-aGe&RQZ>0G~9iHzM1P6{urtm-86;ujm!T4!7dxgIqeC=Z;I&* z>qlyVYtIs~AkFkY=?;6$T;mayYq-|f_Ho^oGWA3*!*BVx(P=&NOBn>^XxHU_CaIPB^81LEBF3+)h1>BkU-K|+#qIAfYA(26cp}9Y!HBaSYu+No zSe|(q{{U2WrDhtIZ6Mj(*R%cF9M3ZG=*`Cwy#B8*e9`YO(Tt{sRlk~!EwU(3r?JN{ zSCwyLLmVr9(4r|G=x5&ToPsmT<|~$hdx!HEh+gk-zOwxf9--29xR!*wnPnILpvRh9 zWB8Ha3=tqsLAbFoO7Oi4?q)Pp&e!s?%Bfep(B9>hXKVlsUU62DHiK=G?%;ixs6Oba zdtGxdlkVOj9bA9#TGrG69NRV~(b=pxKKe(x)~jKv zNsT`7O*O$YCts-5-S0Esans^HnWOwuCs+fa_LZPVN5Uw(Rf5k7V z0=?hhmS(-7?;1c*x4{9Hnd|{=x&^lS5X_*mv2;ue_h3?h3=~)56Z%S=L4z;UcX$Bf z&q6ioezkud$HW;9Ged$vt5*L5aYRJ_p>ne=_>U^8`L3CIw@us$=Jdj@#EnJDsYq zB~_SP^QnVo&(xvs<=VjYqo!?@UfU_E>gyc!d6tkYI$FlIf!)Oje9P*hLi@yBbQa=m zs>5sb5dyBR!t56CKZTB_P~V{kKlly93~`JD!w!6x?*igh;OZC?F_T<_M-beWx7RgYqJ-SFLLQ`jD%SSQO2^-!D?6FF#wD1#rS@P~ zH>&duvf0iiA^M;-4sVHa@dS)}Pcr`i5~aBqZ^Y-!boyox$1watnNqz)&SnzV&i+Y8 zlmN18vg)z?G~WzisIhHA5-RNe>}b6prVDmcYv^Rb_UM5%og{k zRxleN@vG)tjP7LjcSd)paX=i1@&x;tD|OU;+`JLM{NN9mO!xyq*emu%Eo3rN-)Fd; z;kogqKNyAQ?h}c8Om(=zp{SJ_t~Y`98evSFIPEayhsE6zyo#@Eta`4=DPDYj0tlsA zJ3j{<;-u2pY{zo7<(GfBuz|SkM!yi+Ls)I~m#_I_h|et-pfD4#DyqWV#u%h4_da zdas6L4CRWU!aJ|TAr>wx8Xc4FWvx!pGS?oJ30ux(UigbSPIU^*D?Pm*N|>0HpLi3Q z%p5W$LU@O1c!wjy{{UbG8>JrD^sB+ly_x%lkD>XOlo0Oc_YrjgQO(o&OM9+& zU&OjD{{X$_mhUg-KR~+Du3Z5*%|_x5rGt(nC^hB%L}zi*zVtBi+TNwmrhgt_Q3-ut zRlAt@lp3x#(m2WmeUdIX*)(}|B^pNt_ryW)*+-yElFnxLv$vPwid9xc-*msIYTl2! zMwM_E4}QhQC2TtegW{#v+*dwgh2ATxEdKxzsyQb;4Sr@9Ff{k4@g3A4U2x;#a}(2? zR9+eebYA{o-l=_5a2gAzm#WSnd_Z#Z6Akv5ebE~_@0cs+F7^Gyves4G`29kCPF8_H zHG>-Jve})RPJ)@g0>B6~jgFK$qV`(^gRE-y#9zu^$#9Jp-p{{W<{HeXWP ztJBTnykRhd#{CR0k4v0Gb69|JmvT6${=QRj@5A0O;tLHr3@2q-LqlKy4-`Onb~wM; z`Z&3MMVnc2p05Wly8?O2jU2u2H!as5ocm5DGTinC*_~&48`bFfAjJC%u8%jub1P9Z7lIc9d3ftJ75 z61CXGFC-u25nxV!*;(GDVq>JE=1^W{E*SLjIB-vxA=yFsi1yhBC}u0TSq4J(A-2pi z%6n!4%S&x%7F3%HDW~S(szgIGKUh&=WXemathGV#ItqnD!=Jr$6pu=*Aj@Nvp;Knw7i(E;8|F-Y=c{0hL~a zCrX}zJ0j*W?nCUweqz}O%X4Z`wOhe$^TFQP;tHxTT+SD;CB>)AySS^6ag$Z% z;?x&Wp~~-C&ha%K4+bSljm;;d%+hD1F&|}=%X{5Ux&1)_9A!SXAKY&(fs*62OQW!u z>}`A=A?xosT|)k~QqCH%dyh%a6JlOaub3b-@E)NXCUWb=Vxs1;P#1{cVvW=b3-~3y zU~s=N_PDhdepqkU%^$-DA9;TSBJZ=0;E8I}pqKK$h&9z#()`py1u4fTOqUR#HrH;r z2-y#MKQn%I0=MkjH}FO(&wR?Mu{x2Or}=FBkePl`T(g;uvc9EWE6^9vF$r)sGun51 zH+A=Nm<_6^@XdZ>rV}|s$~cWx&~5VH)Y=*iekv#dc#8>T*Ss~D$GkP@HrF!3%erM@ z%y&EG*@hah3+5RYgb=Zfiv%uRy`_Ci#Gt2vF&N!M&S8AZXQcdI=U(!m+El1_hj?dG znV1l79+sU$#0Bc9_NsnQPcqj_@W9k05yo4;`zYi)KsQyG_9I=6A%^tK3n^-=<^U>~ zAnOuff#q@cEZXO&b>jYFZf6OD+LaV5Fabde7X9Z*;FfZ?5WJG=0ep|Xi|M!-s^MPj zKbE?8%tOAzM|D31f~o=7j$Uu(UxZmebQ%frWa9UQ(W(BMi{SxK5a9E0?DwAt{-#_C z--1;Fy@AW_->^r2nZ?0+Y)#p9o~QhP2JkeA(x~q&G{ywc?fNDd*7Hf5t$e}6Pg_XV zKT|TO3)9!kDFUf6+>Onc3(p&g+ifsopda=Xa{-vttKOka*BFO(* zIEEq5c}sPeoqyF11eIW-bc=YPeXZ@h5$XS zE(b>Y75amHvAfg~t~%5=4dxIth!|zrpqQ9e=Y5Yd)Y|WLPF~-bKwtn0)MlZC7m3=Q z-+QU_!kg56avVRN@ZcF`?a}DO!j~8P!&uc29`)(O?+V1XxbK2yWE?>20TBdv>j1|C zU+)l(GBJZ%*&QU$*%Bj#*NIyJ%|ioJzV|84Cz!@RQRW=hh)U}-}5aSKi*?2ZWh=Gae}w&N5tlkl=rwk;ejy8 zYdH-Ps#gFdZUs+x%z7*!zna1KQ9)lXD)R#W08-R}VV1WzOEEmf+^dyM0z0Cgs0UrI zJ#Gnre&dVw+_0-^8O$;%V7`{Z1R=Rm&v!i26DhU5eFL2A^2gX1x@OII!4 z;!$OYost`**Yj!~y{3>G_D}0DW3uCX%58+US*Sa$La%y{EOULLuL+`xwK&cDPF2UK zexsp3g_-dEOU7_xk=6A$?JtW02tM~S%vd%;9M002YMH2=s_2(puKu9_<}em_e~7x{ zRLwo{6%K2E?mWA1<(5!t0@ueY7HX)`u<@^RHI-I(zrVpW6{rw;{{Z8d#DH3wuPh4! z+E{sY`HB~m9Sh3Oz|M4wh&}m!NI3KZq3x&?qs(^>eLPF%gbZ~qSKXU>6rdsrrWv3G z&R89_F=uCVX#I`)9%ErDFZk4y|~-*^!rjFD18W3kjgD?ZR7L%iXKcvqpe65cVDK8^7mO2(m~xkR#=-SMUS=h`rUn)@)F2np{5 z2w@JT{pJp_hIvjIwUwPh^Jj@ptmEMRXGN`exCEihv3K3tG@?SQ%*D8@+_JHBty@*w z<(J3`_PW{UYze!(p0Ars}J2k1`gX@(?6KGgM|a#r$3o`=p%kd?k#Bcz8=P5iN_{C%w#de zoOx65Lmt6z2^*Q8Ew(Vv-g7sGn)r30gY?`(;TH^DbywuX)6a<1?T> zWjIfn)DTXj5-O?Gs1wWF3^ayUQ)Sz?=TU8HHP^*x`ayF*xD$)p6?1Z@QEEUjX;+(? zV~VIi5opeMg+)uxSRcd|(;`7Gbx)a!XIqt)xRkUOF#(lCLd#*vlW`y-HHPBaCa;+1 z2OU!JooY90Jc#E7%yFug$64nQ>*=0gJDk1!4*s_>xy6vycQj8PhOWIg`HLa6{JEtY zmS$833w$^OBv1vbr(2oYsdguzmF1RQt7`d`C%kwdM13dX-m9ESA-`J_w4_Y4k7iG>j(p9XHzPQA3LKnOCe=z_<%UwL)Puy^YTB`AGKQPfP7mWBnxMa|(6Y&BR2gEO*L)VXZW7EW` zD(?=D{GG3d@A!vkV8j$Q{Kb0x&Vu8-$o$aDPaWX@0L>f(VbHBt!E%sYz*yHWUULe3 z9-7`Lp~m1)gaM`9d4o^^iYjgg#b5LDnTwlihePROwq4;~^EO6hY`g0RGGP>DQQ8s{ z-GpkPWr{a8HZwZj-}M>7=A3qp7o?=C3#aLgh&JEPp_@DruSKcFdT}38xtE;!V~N@X zAzAcFx-_2qz1Vcm+cDl#<^Vc!tp(9~KRm>>nB44-=MvoRH*l2>kmhyF8lljRw7pXwk(dXG7y|c%aR82xA7I zL$BffZ!v*VxT`M@gw#pR5%CM?AlsqOp0O8>uvAQMpXf)xG{A)hdFQo4y5~}uw~Q&g zIQh&DEZB>PWJ-#^f8>m#o=KOfd+V8iyp}EE$XF8Q!P#Yf%cjb~tFp!C4>jHe2H8&t zpSgG77Tp)VNBa;-Dheq704I2f#5T+&HYx8c8kE(Zznh2ZGDV<7PPdVmt?pX13|j5} z1AUNZXv1o9w{hQCyG6z&t%DV6){5#TMh;qniz@VZ>3a8-AQ@J1$$KFZq4RV@lG9rl z@rdJ#ySohi%~4Q+4?oErb5K>D_+QLaBGbaY3|Gtw0a^?4)ym*Ej7&h?>6Yq#W_gz{ z5OHs`+&Lz08HOd?Wbb=Fa)RUXq5RHU=_ssvI)Q%Bs6)_AZG1S2-pUtt_Tw1yI*n9y zhG#w4F#Cuitsgjkt}wJ{`}9dzgey?0n?Kosj?)$C#Jn@lr9AFwFTSBe8Pw`e;&_A7 zsfFD4f#IE|y$jc0Qv@J9K-{N@_S*fDnVwI$YP&MG7|pMJT9ua`CM@f1T6C@xhSUj(^v1Cz^{P$7&JQl2if zO5!)Jn3V?v)H;-H{yz*bXPBY>_Gmk? zJK+m)Xm+R<)@jx?vRCm2{wmmb=kol_2Xha!qQznRhC>)N;IFsOSh-rA5j!zJ)rgw$ zD32IudY8)Lxga!;Qb?w=*1(HUpIj$D9sAEnJ{ob!$zKjg3nnsZv#}#Ig zUU3t9XI<~GBgR~Ps30b0)!u`q;bXLIBKsKoj0DuB4}&%3W;!iaey{Fc0|vZ$G3(6# z06vc9Y8fSb&hV}#j}g_%XUth@W`>-M&6z7b$MGs2(bOeM#Hb#QnNFcIQl5Rhz+(_K zRw>`Fcx~P>3GXN%fqj`J0h{Jp4?%REB38YMn$%ek5|Ldf{?RGl5aKixfGvD;4ebCn zFUR#U5pMN76`B^hL)Q-;A%z|4J)_TvW;JyJsx#J9&ya30f%OlOQap?m-T2G)KM-N-1h9m)YrN-(ubX!D z(F9HayErM}{jsrTQ_JD-K-|^V;ko6pSI69ZVO#I)%k+1ZKmY@ZBMW7Y&&(WxL(+B} zpX(yA)!bjdF5j3|Y22|EH<5cd{6rCmPFvrT?p$4`6ZN@m&h3b!fmbRbp8Rn{ze_b3 zhVsMvm<4)Z`^+q*QDwg_BFfh24~$D9yN4s_j}7lF{^h7Ac*PKT0*?07C1BI(&LteP zjv?uc!+x3ORQ*eYBk79wha}<>2lb34Ztgw5wC6DQk20|V#?m|`FRD+rf|tMH^a2_IfD_y0@Pl*5zONTRXg>Fm5lm~FqL&KCeZYSmz;f> zMGq09)~YfgcqdTSe8$T{$=n(@hcgE+-en(<1jbro9#2E6-~gdF$R|C-?Vkb7m-h( zWp#k_IPi{sX!n>}8rC@=SaW|r+~#$I)*SShi5T{}o#OnoZaUTD_?5O619baC`jv@N z#^8DhQJra&8$<4ehqL>OwNK#~1o0}bQ5tYQ!v6pf-&ZfX{-BxbiON8H4^{f+Q_J#? zW1;Wn4;?)yhT&Gv5N!1}jP_$Ury|q&KFH(KsI=>0&w@NTF|9^~V~S_SWmBf5Zk>G8 z1&@J_d-a{5+$Sg$&7KbRnUf=BEViyo`H7zrguFeV)jK}m|BNpqa0?|q`FYB z`0pq#%(M(y0CWMUHq#G^bM^R@r4aSF=h+?5=*;4}MAbS-YJ1MPo+C3%&!%b5qV9ha zxBf$?sno;zZ*f0zktSEu0af6gl)!UHJ|-wD;41tYZy zOWueHOdH)&?*9P9r&GhxAVA_#3ED9Tl-Y3+yFV$7`;0`(hDxuJTK!ArIPDCB!_2J5 zxK}2HZ46Kw527Ju!sUbqZB&QdkIZFJM#MgOexUN^g?=Ib03qnlhtxB2Y_>4lb5+Xqc%x?%)@(XmU-XmWf?Vfrw9 zO44tu>TB^CW>KCm9@&ILMPvKipHjI}88uHPsXimKR#zu5{-twUP413AaGfZ%mQ!D! zdDOd1u+x}suMR%vY1tptIm~rAxM}Cm==x*d(;k_rd&h`N;vM09GmON^%l08CV}<=x z{+MBbK^jV7f-DK(kHyB!qO~i65(f??W3+XAMa;y{gxV^`hTYG|9`W-xkWafRdaLa; z&?z)DzE88fB}NgQ)BVe`t3vhDnP4X%{KIh8AaCj`Kvt`#quH4$^^qq$59h20aAm$n z`sXjyOy&onDqlHv+i7h zcEMKaah|<+&e1?X(m2;xV`y5-vTJ>8hL->ZW3(?4)0d$+nC4t8=5Y^cjvyY;)tpZ5 zV^;hVpF`8c&so~$Bcz4>_6aBnhCagubZRpZJ8o-!@Li>;R2;&Iy7BpekDxJMjxXM4 zjFo>gjPKKlUY0?Q96y3&Wq1Q9EMWw(-~zX_x`AA-3#jdPe)Sw4K<8l@_?5((R5*2< zL&EO5x?ZF425O*&qP1w%hs@$}4$Mit z&oa73k z^gy{*s)<-8N?w=wp%&L1VpU`CM-W&fJ1}@8e`kJRi&EB1A)}rO6bAQO~NnvEiY>>GC z01CnB#5KQ96W(X8ClKZaJwKmKn3z6gKw!es=D{AFPO!4fe{{`joh|_KvyRC?b*Z*Y+iCNQ+F!03-k;2jl{SRDsfOT zE3B`NOmir?QoT5YHwbDb=0_#u7!63ry`Rjxf&m_SZaMs)fIWWdssnkI48um8lN_1-L1D+nQv;k`Tw}$mRlVF0th8No z)pX8g!C)Vs=_pR;m}BQPy3cO1m4Fy5u#dmY3#R{qz6Ud9 zuz55tw(5809<1js0UZ*Za}-6RmAhP+Il=D>9#wm*=Bi>8T|*8ft?4hMbl0O4h37t= zB}Ov+368UzMi7o}A#YAG!T$gmn6Tam558?RK-J>Umlgd#GsV}m|AM`Ub?!Msa#wX_i^w+v>Um>&RpMUnTEuzG3*?n zJKe_=*XWyoLv@OO-MUSSTrqWZ`HL8>5BcmsHbOZw*Q6#Qq2W%ve}tCwS~c|ZTkPfq zSeJYY=Y)?aQDqJ8)7s&(>xo#@tV-r_{{W_DGN9D!D-edExK!@+%I*N{CO$>qwR}V% zoVIdEJNhaJ>zI2)`+X8CHU{m*A-uGX;;PLP~k&i#o4YmD0A2k zN8b|@k1*K0pVJw=MfY>IHu_H8=32&EgeF4}iOY#M$3n3zgG4 z*8EImBB`~+atxKDduR5KZE1cDLz{rkrL@4(mcya2%JjsfS-MrfH57 z5jR@?;mqbH=AvEKFfrz5{{R@Qbd}msFVOV13(|KcL)ZMvgQb5{%uL6BiBeUC_Wns_ za{i!S5I7;q`cwsI^BsjZgI%}vE6%zV@(Uqh4iLLD_z1xq4SZ}N#_|K7TdmnLE z?|iK2H3VZ45)uK2LSyIV9n4O@QRY{8D(7^65Vlrj8NOmbod}Dwv=$WMaNDHo+czH@ z44R|&^#ZYiksrm&FNkV_9+ew`x!R|gc9jl&9%0X?5T)O@crkImAYY8bmGMizU;0ad zpN`sk@$+#+3Y%AFCE)s%@&GYfhJ`9l-th#tD&4%lUAvNsj@|~u?JIpyDp>wLW3y6W zX_?axo%xjOVq~31qwN7>#pV3#<%L17p?ub_-T;jrC1(WZq$GydHWl-P=()$HVK|io zydrvYzd~_8nw60}dKKOsvU(WBW(5WrjSbwrG$QV)eiCbQt?pt`Hlps~lsmrJj!`M3 zgH}4;`}vKy#&Iv&Fs-$k9y2zWeXf6Ux7t*~?H`SUk(PWzLiLFEwHWUEnW*FPY7LKd z_qYT~LXg4feGzIjzFn8vAiFtBto~ucyan?0GJ57_822pxGc!`0%AwofZS_)vDOOA! zHhaQm#wNwiN11eMe%bU~c+BfKjAP7fJ29$zPCp)*nCryP67e^Q+%$t zlY(q$Babe>2-OfFF;+Z~vd%Xit$Q2K;By0R$o&Rzrk#<_!WRRdefk>otV7E*-K8O(A%DGPQ#?RV&8>vwmansxIywDXB#i4hIm!G8?g9xB#m`RWS+gJVQ^X5RcT) zro9U~TyoIB7TxLKjpGv;tJJsg1@8n%eU3b=K0pc;j}X>85Xm`&FbPPS?mRNW*1}Nm zw?Ih0XA+e5l`_JH4(p)r7}B*()&s$s`Q~wPGSuU| zuZfRN-h^4^bB-lLFgnU5LA#aGHwg+zc|MZ?p{hArTVs?Fx-Yc$wxK zu4c9UT5GrXkJ{S7YV}t?a1{cf&w1hW{D0s*0W0P`WmXK2IzGs)2-unT9Q-U`fZd;z zr8S|bD|NUT(^gl`zUCC^Qp3)VxCwq*%IdCf-WKig2>KmmPAYYdp;VPBb%E2V0WRDu z40}TFJnC1ck3q$bp*fDXI{`14oZ^`2{3fOLoqY%5X1<`JJ=gw`RTMn%vGkIY5b9<0 zHLE+^Rx@0#>l^P(9nphO)G8~w@2s&#``kZrscFFqD(tquqdxMTbpTKTu{>vNQK3)p z0Sw#H6$I}Ug8_assArji>}drKDZH9D__)hf?>1~bx=(pb-@N=*-5~tOEm|{XFyDyw zh}6|(^`yY`izOlD%UG3^b*SdQhw zaHqf@xzGxJQ2URgsm^+SYG+H6_C)mAj*$vSRZznTivxWVF;tddYX<@!nDsJnuX{gr zDwuPLlXbrgeMXQQ55{!@1Vi-|@6G%}8)08)Emv)QLV{{em-u=@{VVI89{&LLE-1V1 zBv_BRa?_X{WlzK*%*nk$6BCa0=*uC^#4~zeD;gzA$Dy~R8Z@w6_#1<+QKdFvc_UoF*!K`*wB4VW?Wffb>L+OR0v>>_)dH}3 zIH{gJNY|kq)J~9Mc}huGhd@Xfl`2!BQ!5^UHZw43`M<=**METiTZS>x7kYz;6vhF9 zBJkNXncf!ugejjGR^aI4a+ggS+_1at)rRV|xzBE8THFW6{D!D|TGjm$nZ9LdjR>)A zj);=6Av9Y$xZkvAHAZ;;kgCMh&ULs<++hT#Eetpc&JY8&aO2DiPjw4F*q8ABs;E@SmAe zti(cAA=(?RVHy5PY?;pmo?q%DuCk5IR93a(sTK1NYi34eOHx$K3zTsj2t7z)$1Do9 z;WhYsz$Z3>wdbThRZ`qOGsjt;_AZycN{pST6BI)xoQQOnDjnr?l`~a5%UWFw{K8-#Jjdu}I*u4sUohNKrx43u>`W4{{8RS^u2u^U z5dHBC*h|}XzGGR~^X4Z>PbH=Ne=!Ue;yD0(UButCs#vI}@A=eihA7}J=rt0rYppf> z!&0sVU&JTBC>J{f73`QrMG(1++Sh#{#KSLe{^KN&2*3{^Q9vxzEtKnc&RxqGuPRmk z5>{DD4z#M({KTYi`RRYj4Ko?*F!5#@@iUm$qkgRXLQ$8*dPnv{OzS%&$3D=(P$fff z+u29WKTx-VJc0erCPw0Sske9!n^D<%en?+1)w+R=_>@&ZqIUG{5y8AWX_Lbt>)bhAa*27rl8MHCx8*l~uQwl26$dAbt=qAn4$nL9%HsZrGpHR;@ z^dvd;bvrdHGh2sZeb=J88-eLCxMT0}9_Z*Fc7E6?p_6J~EPXPjTJjWUZod-vcdLgv z4u>6FO8tg;HdT7wW<3Ty{StP418|Rd#yEv@=yry))}zv9BcfMGT&7ZPQN%S+=g^&F zd`gv%N+30_GPewFc$JC7s6xORdHhS3Fe#C%y-PT5sD7^HiG)R?Gw_ZlAmV7pTLoT5 zB1aDIcDK_74HANMFXALDhJNAwPG>I{?rYs>l>(HYcJkLUh)`JL`+;~i z%JIf|aLgx-0MK~EdIfL<2ji@wYHtq*Y#w2EfICWunSEk?Lj!n@^7-JJ-C|!u zQm0km#C2qDx5^q0e|~RH;zrGaR#sbx^fcR5vAQMOUCR^?oCv znUc}r$GNzjsW=s!)`oNe6hhn!gXUyVna$um)?p}bUAi@c?{cByR$eF=%gH|d^bbcDHCglp2e!m~MkrwVc29iTT8ij`P33eWy7vg!ymgvL(I zUj9jgRcKZBD#()DFfi8{SJNB-d%G8mp7RWADhb;xdha2K@=6uH!JoJwW(rk@s_U-f z%t4iDx^YIk%~Y0i8!ht@s)2$*dNp{4ltRT%=DnlVT7!k(m=-l$Xi%GQxtHm!@MrfD z4kKGV`amo|@JDP0+nE5fSVXgnC9-W^(FEDA!SMhf6g{yO9>6GdOP9qF2S&Y*i1rz} zg=zI16fTB+VVkVt17%^kQL8c1?EY#uw$6}#rX$5_g}(H)c$9|wE!%&%-~y7L5~}`( zH~#YizCf*Xet3!6v5^N}Q&#FVg)#*hl|h znNXv1m}?T&RK52=_hYf5IwKu-nAZpLe^RM>`?Ch06F+l^-9Yz1ET+d0(<&GtJ6-!9 z>`Z)wV;HxZ?`^R`n9jEMte+9}pErx+_=>Shdwnkt)Kxdmi@v2J90L4ceV^r+XyBnq zPU+03sto>L@0dl4%Wu4XW^gW|y&YU0hogn`$DwVk{M;IfvAKFw=i(&`$~pOBL;*`D z=!FytCm5O24#DBm)iN6t^Bsq_J??g#dJm&l=}<8~;5+O1P0#P+m!$C(RJn1-5vo*sEo<(aG4+aqC>yMnbpZ7Ew!9IdJ2(k!t?B zQrJ80sm`A;mt7iV?O<2Y1%ypl{ zx*4qV`TJrYcvN$>Fgx+gAW*7E92ZfJ+VIa2sOOpI+IMW6&ZJ3+&l8;Z^d_dYIq&IH zYJ}Bk^f>+f!&+0%h-=z6Pn<_g@q~P@;sRaAlHpgDd(OxCirync#Z0bMZX1es3mY8g zYdBXOf>kyetw8Bu^YUMBu6w=+MTP9{{pFDJg0MQ9eC8iQg0+SExQ*d@FSE1eIt_Z) zK2Mf73$7UOm-Np0JDBNAsvh7{c`-AiRnmv=>RZR!0{$G!6L98thx@$Yguv}byP)m3 zC6j4o2TxAA9o+&k0+~~*weK(8h3;G`H)pQ>k^64yrN>9*pX^?^0YDw1w~2W4B_)#O znz%zxX1y^TFgwCjze~+@+{tvoCCp?w^dDP?F{3GY=2;c)5}~MKT@YRLikfZYem6Fz ziy+6#FS82AKRf(Np1QK124DgOlS4i8^#M5T-_kZP!T}~LoaEauVBZ6uh-*86SDgN4 zj7GXnozKQ2uLs#Vu8ERm2MKE?obv%}05nwGR`g)0OAWZ4YjaYB1h(UES)2mKwQ|{g z*>jkr&)~Rb@Z*u zbJA2PE79I3Z%le0nc5#v@A&nhiJ5%v&&;CHUr+8A6Zma!+FMLP{yqIcm8?;8=eFWogIxRv&(u7Q z>aEexzoH;n%+-0kg2n>taHTEp@f>Z`I9KLW5G7>0h^MbJK2PC@SXrTS#x!I=*74dYTt)Ha|nmM9v|i`96Ad+CO+jDA~5d<1?Ch z{{XGl-SK}a-nziFAQgker(L7ZX;EHfM*-D?wLPcqJ%`f=Uhs{cDn@M+b(o`T*~Bm+ zw7+myknBI0mIgjwnMJq$e9O!%8rPqBv`^3XH?RKyd(K*8f8J;du5fc7HWOxG-aj){ zPx6P>7jN?iUkb7QV7jU7{$WLwc>e$}=J>M%Sa<&bNsB)V`JOPwAIy3G0L|Vfye@z3 z6>s1$cO>YnF2QkXF!+DXmz@OB`(cewC-g&}hS&T~njES194ZZ0eO-OadQ{EB*vH%f zjjq)AGhZ-aRcJ5bU_p-1&g!mKNqeP#V9OfL-53Y8KF@e-%pSdA62M21#Y9+C*8*du zRlS%|blJsCrf)^U`KW2AZz&cb;uyp-A;x^e)gsoXo^D*1U_~@-H4XMRbq%k^yTfPz z1yExZ0eFL=1*e5|ufCB}Q&eucyM(!)Y%qQwMjm$uh+|H*81MR<<`zf141c+n_2<;m z_~)aE%nU0<=eu(Lr~uQN?~c;fD|WR{$&|>n9V5vZoaY=(Ihkr^8qD{CYHlBoPQ9>0 z8GrCX_sliJ5O_}w_`+6b`P5PMAEi{AYI01%2Y_^b9yWEUJiQo`ux7vf)IxyYkWDRQy2H~xv& zU*n`ub06an^hcNhlkgw7Q(~fDvCZ#?;g3S~dw&rZ51aU%Yu^0*Pr+`lXtIV|irLDq z8HTPFs-NHU4q6qA+OHXxDgOXhXyB%w>rer#n*QQptQfEPRIB2Re}ZCCmmlbvxc>l0 zQaNVxeBV;965h3RtLiC?-{Xm9oh6c?uk=jDEo^RgEE9N96lI^ zv=^wj_Y`4h5q$CH?3uZkD^=|_r9Y-%5BIu{lAm~Qbq-hPbL*qf@u`0s3Ty``^Y3sa zoV(dY7p!B%WCXC1i@$g3y2XlkpP5pJ9?xfy&3SE&Vy&(Oh)3%WP#F-rx&cJ?;>N9%>*FV12#Rm)oZeq-HI zF=xSlad^mbBc$UE6We_^D7OULKG49&!`@(LL5KwwWoOyJe8x&^ULE&(_wCFjQA(>l z3}ET|XArMLo{&y1U;d7=_v&Y;dBc4-002SSEtlbzYS-E-OGX@Nlq*MQzWyk4d=O`1 zn%LqNb~;Kjd=i4oiPNLRri!RIc!0hoY$_RMu2yhrc!AiI&DU&9OG;Jy&dMDx#O4e+ z%Fi|86f|mo%7LE5N_n*&8oWdLNtUbTqB01}!c$jVc!_z5 z04rAk%oONDk>}9r66Y(4MiEF>t+;sfciYfxBVpTB`!(|wpzJzW26$=o8)n(0+p`LE z$<{8`2J!f^ekCK2!%^WAv^M%@Fr0d0Y#6M@NGz-t>Wo*+<4>J6(!{7a@SNtXalVz_ z9&P9~uOrmVOf{hn4y+Qk?IpfT518q-b}K&6Ft&)Su7~UM7#jjT!_qpZK8H2$3C&_P zp0eSo+r7+m%wn*mcTqV+AHfy%Ekl9S9MEq1-lC>VH+EI?x%6Yte9pOI#VJ=1TfWlD z>)v=hI-PatM6Am10Oa(nzN1p%fd*>+BLbhs&HGBV6gu^T@g3m_Z94t@M5_(dF0(8w zpDHB6dSZ7(sL8I-st9f+$oPpAehE-GQVOoT6G_UbH(5$MKt`HM30@_A@mu*kHR2L9 z5YS>$;9-~mGoKFqej`GxCF`Eewaz`}^88_V27cHMvlKUvKimbV0%fBuPwx?bUkpeeV0SFGzO;t|E(xrJ#*L?4lsw?^gf zCmjd?1B`q^7tgd?jl@dr&Gne(X;DRz7m?|6YG+#2!oDGIW?^x{@0fD48HTeRDQ(=v zd_uD^R}2%WE?fg@A2VX;{L6AagM{%j7e`le@h&hF5bYZj)05xQ^Sn(a%)Put9YayB zXI_v13M>5vt{-(2cW&2yyoRHO1I@zv+4)b646oKr7Tt(Ro5Br z;yzRY!EM!V>v2q74`jc%Xtb6bG+y|;RLPopklOw4%v>X+TdiU_=B6!j#lBjMxvtPZ zm?dseJ-s=Z?KqzEkNg^jm}R4ExgTR=({bK&W7XF^Pq~?`c8*WFnJ+dsHO#xVsBf@+ zt(y2q8EOm1jmpk-QxeGuEH63>@dDRY^;ypWWe2ttvVLq%vY(-WgnH`kd5pgoQs1Me zN!C)8d%MH#rK(qVu6et%+k8L}08w2#LdA^uks1`iUCN+KW=DXOlq^)7KO*fx`XA5S&@H&(uAi?$ zb5N8mMOQG?t|3$KPYxJPGpLAbh-Vj|m)DO&5Y|>T?SqMT38q=!*2CT&V#0gO$H))a zycnveznU-J%1AVeY8##Dq=#z;|On091Hz}AtDt`Sg9QT)uO&;+9wZ`Cy zj+RQ?I!;&SX7R*Wqi<~fpnwh>ySNQLU;+9$4%{1qA4EZpPEV-$qpwHqx=#!*E7Cq{ zS%mtdn6VPhSUpDQ_kw~Nh{a$!?GG^N_q}Z?(>-I>IK_)(-u32GPbVxqnb+COwYR=w za?6O$?sVJ)(sp}0)N}4gr7Q9}X`?RTrSMb7qs*a{hdeWZJ zv@0)W3Y7?Ajhf_tf>08~ADD2tswTdnXHx;*91GMbFFEha<|#TY!G-%&+3Leq zEMEm{thKe~BTi+4ej2~jwyr@vmj3`#_2?`$Xr(-k#vy;RKy@)sxKB+6Yu(;wj*!zk zP7oo7u7tX-u8C60xg`%?T}-zQp#^9MqeJhl%AEQYQ#XH8=rmxs2i*##A>mQMhB6E_zM+y@IEiSd>mG^AfPT=>r*a zDto{=D?RBZSF8)os(V4^Tr(EI*~9rhBZykxk)`;lo0+^$wtALX)9w&U+_%i*c$?Jb zTQ#f1awD!_QG?R6a2u81akSaJ+55)eqHrvw`=8IePsv#Yh~LkQ%QDz2y6ShJy=G9s z{{Yx7v*d^9B<%A!%<Xj6XeJ0QtI)45{wT=Dz=Q5pOOI##ZR;Oi;QC(E6Gl|%<)wR)KVotQPV3PLq20x z5E_c>&&VOcx&$aVD30N&<`{{X8QFXGKII>Db_BjXeaD?bCO{!?bJdmJY_2$sB6Y(p zC@-|?cAon5@}431oHg2Ju4j#qzGLZpaXKe?Q0KI@+|1RlnUQdn9@7oa6EU1r17G+E z#_u|Wzh8K3(C5;-&MTz09Wn7aoJu5E$FS4l{v#Ep?e!isK4uqBh)XP5C$PV$(V9Or zaORwPcA2;HJ?D>TUs+X_{$aPH=op-#J5)LC=z0zqc7boswDZkGg=puWk^ANgiDE&D zzvf!fWau+jb!@_S>pZvZ3;n{8zOuSPy1<`_g4E*JGxY{C z`^kgwoblQNY&Rlf?%uK0poFcJd!LwBIuWYPy`wg71zHP1mWq|}Y&g}k?Mp6Xxp`;v za^Nm%vlotY&LE|U8u6eP0`1)q%7W|8Mfbpdf| zXgEBu-?Iv&70!N)i5kii&KO1}1IpiqEen4je{g2v)Jq4<1bbB!jr`wU7=lwE1ERkz z%RG~I=ydJO%l}rcZxvxFBIT<_OF?9Ix#8>X7|tcQt8Jq zysXr4FxEXtdK0q)jwU(O^Tg{1ef1Hqf>Lw%gUgGw5X;UJwOx1b4T}ND`_Io2*VJz) z<_}je$A@y4hs3SNp3?6NhcyPU1LLG1$*-sZM0-vn(tM_RdN8}pbNGzs5||N{oWi9` z*7E#iDzV?qB~&E^#8sv}*cCcxTC@z)!>1{7P^XY0qJNd;G&yID257 zd&cf!obtm^vToqqHuQmUF{sflEpJdBRF|7F0X^Gd6fVsGa-ZU9sTW4wO^~+7FeBa> zTjLX+@}c5ZeJHKZ9;D(>!~N|JVt+6&GQAJPLx^M0c&*0xJi@Dfj;7d3%<~G;8NRZj zZ|Y*RD-bY-?oy8P%+as=3zl^oJ@;L&aT|k(m4|p@LXo52f1fcn#r0{Q)M<(hcP@B= zh&>#_vwof-tjc;~1UKnZ9Q2OSw(1$S#VJRa``Nx9cORK>BZY18Uznxx@ssTYarc|Q zkqDtS0P>k7JZKFiH;~D$%nmHjFEvgK@-K*8P9xK^(Tw6KHP~vXfk7IM;~cQoW-lJRN`*skgh^anENbDb%M#G$_eyJ}_f>~}CDmG}JLK_O`ih9U z<>5*0ekHF8p7UQhuP{>?RopM~ea2y4K0{v`KWeLb_L*zeDnun?w4&3=_I=VRaftuMpI6K2YouJZwL9g=$qBrH7s22V^LgwXj z!MM`aEN-1unOow%BHr0mGp9lFz=y7Bp5=1A^B%I_;vMUi+xINhW>XzpcW;TpLbGYd zATRg1^uaC;H9t}2Q7aOdbKX^I;Z%&&7$ILsmvr}d_Gg8t=prR((-dW!7mlHa;v5>- zbMj?@Wo;b;pyS%eao34&m}@en6ZhyoACSiW5GbQy#eq01N0>GJnr zP|#FNGl^E>D>*>k=L50%hl8T?X9utDekQ%;`dsJNn<2P!ewoZaRx8+m^1(Ao?WWh> zVIs!nDmp`aOzW3_h@b+Z&Z~ulDA@{U6yW%$%r>WJyBU#hbvX1tna(9;c$`im6mo6{ zLF)~@sv{-Ef%49gU?=EhgPz}Wi>{FG5xh-rnTe^4!n$G=s637(9)v5Ij%9*Z2K}Xt zgOzEY!g*fP3364&Jhdq$)?pW%V0F{x1R#KJ&ZO5!LpP617F-OyDp!m&E}OMpEXQtP zPb`H{Vt6`44EvmPW{mS>^fg4?$Iqd;eC(C^W2oyB8S_8H?-Q&$%yACUX~umj z{SKaoeM_8QrgJejr0%Y!Bd6m|Qyl76l{MZw)2;YklB!)f&Q13a=?CLf|O-cK(HU4HC+uO`C4QfyaE7D$ql%}ZFJm%ozBbo^CX8q#a!m(O-=jk36 z3=|c+r#|dTCJF#62Vx}~?Ex{+4z2!&ywd|bh}rJ%>xgdpzy!A6FGIT)G$IoPDB}MB zq#wKhKKs3QDk%Wfm|f0$9%tv$v4~teT^@V^zcmzHKW1F4X#Dt#FG`{g`!RD2o}7Q= zKD|tFEdKxozk*Nzz*IF2N3^;(-V#JaFUc8olmisZtT{@3y8Pc*n*{+w@3=3##bt)4 zS&n6@{vu(fR1|g0aKeHXc*}b7kjBwSD_7Om=5~8({$@4}6$*80`12d6J8QF=arGFA zC5=c>iB>?bd5KR!$aEZ^h&V0%)?LhP_pW})go04Q%3KK5Kdki5GeJWUFl7{g;6ZYTnG!a~~zF_NE$+_<$n zK)hQFw&@uUD^>6j1B`R-<}qI#KwKS1;8zP8MYgB#GH@J=f%PRPyhkY z;Dr%-G0bTVMcKZ*zVvy6JHY@AVlZfyZ}KT{}!_$fQHx0{i?mwng_?oK>UW98|RMg@e_uShR4MUhM z4!(Y-3d*VI*Xy-29h!kw9Zt3H&SBy`s>TRl0@muQtP5||;1$=mf6R7^b+=2`Ki7$e zt9172>i%XOO6ogx{7PkkX_MvJ{K_@{sI~fhPg>8BdZ_+Keoqnj5*$$d8rNJw)M57?9iI~w5pSe9 znDlcgaK)>esDs+4uq?Hzv@YmimLYd%%coJ>zb-G*_idJ5@*I)Mdzr;)oT?n&~a7H^lDVpt#Xg+jH@U*_Ehb+&vF^9}FQi z?bXTn2;_Ft<*&PxU+9i9{{TD3Oc-dpQ;xgoDLKoG_;#B!JjCg##$$neRBF7RkjHP# zIy$b>)8YnXXAWx3HN|Jj5NwMtFRR~}8t($AX5js`MRcoL>ZN6^X?07rPCi zyH5qf$iHMA`?u^*`8>ir&T$i?4r-xUl-qNT^N#ZMisVzz?ibT2+FxEGt2K;Hx6Bf% zuGs76p3KdqdEJ~!_=OUo4@-qgZahHAd?zO(?o1r;LP}8YROyTuS1}GToWn($unS#X zuzVoX%nl81VwllP3`ACIDKUcualg3b2`>gE<+C60U0@>1x3DR|bLM8K)KpsI+B|N4 zgl36Xy*2ulkfsz`F|T;hE{GH~<{%+vrN9zl6m#7W*aS-lR%YwLA66Os!_fp6JnXMk zaDb&VO1;4r>w`s#2eQ-^DpTJ4Plq{^E=4j%&r*g9t`Ry zqDa{jImRuPry3iok8-Msbb`GKeu{HMgV z3!=JAanuWO`-qju-UptJd_*)By<2`k=vsJ$@j>8f?b8|j!6a)x#^Ns=L%zpTf=k+rUW*22gF#|6ve8xXe&2$C~5a**&(^ByrOx}Gvz};#Rne?h;)Wpr= zUNarn8hhMoN>3?I$qz~NVi%f)7eOnv%+77g4aZn>4ST^f-fQXyc|_Hi z>QGq{(Nfz@A26!|387oJNAVRu!~pG|s40vh-8~M!QurG!b%E>BD*LD0yhKv?MfQ`s z^j~R<$85a#{{WEHL!9=9VhSkH7a5iO&pJwBH26+&wo@U-=Mf48i0ix-)sEttTIkwr zx19XLZ+XN^>m^GX1qZpX`+ngb(dMD}YEt4Hq^p}-;Fo=3m{(t5kA21mhdKi`ejX#s zA&k1}W9Qt0#}d~)?$FiFCAQxzVa>u*N=FXmv+RymAlcRYbNP%xLQPS0K5F23-gm>V zbnMj194~baPMj&g&aCvoBdgc~VL`H6T{U&{QE ztYTGXnc8C|=80V;Jz%Y9jmn>rT6ctZ?SY!k1Becq#OgSKkT!G(tuGygJ8^CwDbiq9 zpb2dO%7D9E$_mgLmIYrolNi)!1gTulzJ-xLQJHTk-P`g-u-Bqhyzvcoo4bg~^N_*w zQ9!fR{;#Dr=TOUzWsiEY25I&VIfVeA3Oo=@KBt1_CW9K@{sIhxgIF>528=s!--6f7 zrk?QM-*G%w)XZIjY`bs8{KB(6;TqFD^)I}n!X`Z_sAg7oIK&&MondCY%Id={Q0p3= z`MF}FS9<&nahqT%;A#`Bq9)EK5$^+a4_&gUZ+@Qe(=R%mYtX9f&k;bE0dKS%8ZdW& zB2|+v1Fx8sc8?l6>3s-d(Oiu=TUqY|4|w%zcS5%G{*G_n0)Hz{>QAIM2CKq0}vp z(w(^|^C=ri*@1=X$ki26`dbB70G=GqSo2b+I3d_m-r_L{4^xo7$o#_#TO+~+Re*3D zciHo3(K_l6!nmvX&?Nv1ab9_QEuG~$<}*EDVWx7r$3CbN#%C15abg{M-}1qft(O3$ zCKCQ)a-P&dx(ls%f#fAU9!I>ZsXt}Uyv>ZN#CPYxb5LpT1z*LltVY!>?{cdOrCD3K zTj(>2zxSBCUNskNyz^T7n5wL`C~(3VPkC*z?9=o@0ory(e&Q73n7?8C!?OeHxL)@5 zoNh7K(+=!=PFjJ^aXRCVK-}h;Q>;CR;6IkW@1nt?@N2 z-9&okd_SoE+aY#YpESx!(IyI0tY@ui$71{MWWBs^0M%99?=XjVXqB!*u1Q7A>fbpy z7ktV92`a-%ad0{Kf@7{HH82djgftlH)p(RxuyscN0M|B+!PTHU>Re;l;R~;rWf3qy z7K4HF_$HPwBieXBGNZ?li>!BHWc?E!0uca9%<*>tO_I3auYRy`%y&|wo|H^xS5x&i zekZo(+=ig4?|;Oimg(WTE*P?u1#14^F>EnOn-4XAgjo|eV?CelT3(tmTk-tN)|+QT z+0Wt<$2*md<{}Ifzawi^rUMkvLRz(PaQ7z@+8yT*?oJjZTPq>2>OKIX@#-&+X#OWz zbumw-B}lo(U@DpmW?;eh0V>)&AGwua8^HW1wgvWqLp^8xhz8m135eeer#%_l9`Q_- zpl{ymu1QGUi&!qLvWc8^XRBe`q~Dkc&^bWf4xhw61G^b4SB}w5Up9oVPPpPa#JRA7 z*mf^okS2EHfCKr0bIeTh11l2T-X)<`4lztM`S*a$7p;KLeLnE(;sdA<-Zo1kSmqfh zrN|tfRG!kR-cCo7cWHnq>Hs3o8R)bPCiVCl;i@ja$x9!XI! zPc*Pwsdayzkg^AOVoRm(o%fb4SvFz}-;r1Olr@{KSz%^$ zd_WakOz|_;6WUkeQ^w`9aV2MxP{63)!w#5$@dBH?+$(1?yg`S1)o8+4g(Vj^ zJI#&R#le=HKrb+7%;vGj;td8GT^x5~4@lxP3gEe|hnU%ED!aS0eQ+E>ZnZ^`ZEyMF z83kpaUGtmqWEvYR3LRbW)&^ubcbQr@U4l1hnNDR)g>Y}gqT04`X*pI~V&=FoL(WgK zYTo(wjMiv<$o?U48jb^>iN#R;27&(DHR*f#m*tAO0Ed;J{{Rp}t1<7h`C;Z&d;4l6 z0Hk>>W(MVF>UFjmcLE}iORL<)Fv3yj^n~sEdp(FOp~2?v+I&GafB{qxrln*{h5~5D zrNAeJ{Y^gh2!I7vW!BAC9c3T}vH67saRWR;pVR=CpeN@r-7>oI>DD_b<2Ndxu&p6y zedEXxbOno#3bFG6O^kby{-R35+8n`|U3iCC%U=@%P65h)*_PFHA72*V%tXTtf$PzK zP%|;a;$n5}xT4es+pKwG0;Mtmsc$f{c%IH-i+wb-6#n0(fP#CnXjmBJ&|zm!AJp;bC~eW4TJZs>m|4)E1+==q$Zsf8e8_C79Fvf%DGeV_JW!}kz! zfY<2cPLB};Dy>@UyIvNc?G+EB$o_sM0bw$?@4t1#0W`KJ;uyy4Woj*#VPo=r&wR== zZHH|e&Mi26N`6sF9D`V|+-!AOIB!QO{<(Y(VSgJEf-UrX_Q7GXKeYFy{J(BA0?k__Frw^l0iyhGb62fCM zw5`pRR}asnGXx5_t9%h)c~vf&3)zhE6;-tbK2^zoOzkn~7-3c7h16Sltfw%R0MT?U zUtp9wrS>D8z-uh(0F1OirGb|qvCIgE7PathVZMFE+e{l7V(Q#iMc;&gD5@b&RaSS1 zTUx(BYz3>(gibW<3*~|pvz?=&x%_#V1jerKo~#yuO-ALaAl{pW^XO|jfznZVk9t1^ z?f(FEgSx~6mz>_+ZQM2h0rpwi#(rlWGbjtAQntFh4o>{Sgp>p!fW3W?a3fuHo#!?4 zvSy%22A-kfE>!2U2Nd-+DA=nH2YBf0g=TfA8=S#l>BqA!0B1NI@L__LZO+Eg*Zrx~ zVVjRWB47bq&!Erbk04wXS6ox=n21rvc;Q&Th7N`zm3`4{`}T+Xy#iM@&+#1s48{5ImAJ;d@O3J?QIIG zi1KS4D(wWJl_<&4#%P~WRD8)IM2&!Yub{B?b_#rr=T($>*zjjcq9veRS6#AT94eTcW?bHw(Ty~BIO zN8&<}FB5h66|bsC?3{LIKA3L2ON>FXqloZLP!&jI_{<(W0F=LV*Tkm~k-%c5mbW4S z4fu#|6_eZMKH+|w$g?%O@8S-{00t*1clnthY3nQjRSfxn2%Ez(4G;ul%sWBp3arEK zB}&O{7aH7kLNi$$V!_up2~oHJ#Z(A>U<#Y&TqbY#hN?5G=&mc%!$d_KH*zm>X|MYsyoNX(|F#TM}@V- zIeUFZPxh3KMB);>R#ZFHeqt$$ET<}5a70zg&bWAEQ;AlR`ZZt7bW3d+x~GieXGzxX z^#>r@Mu#t#@{lVIJktCVZrYe+mEgDhs2_#^6L#}%Fw-4;(!Z{=dpm?Vjzm$}!6yfehjdcURc}?h|cz~++W2U43EU41I|r`USN)RaaIAJSb3 z9n}*RW!Ze!82f~odIdFn?(61LAy^tmo4BJ1i&q+)ZWw(2;F@IzgaYUzyhBpOM6+DW z?<_&MV~y%uP%AyR=a`r$88&MBSn4Lu@ZT7gD7qeiqz@^_fG(3#XZs0TX^2XQlT z3%5VS&eY-D7uULQTs#TS%;$b4MWDWDWLVy!`fjhlGP;Rs}qGL|k~ z2%zhMuFT^Ot_OZ(P^U|j#xUdU)LEB%+wXm^w7QD^6yMZb#ni3eW2@R1wIwa|9nt;3 z6U%iU#7MMam^LTFi)-bN&pN#2`J^vXbGhwxH;!hZ;scvb z)zVhSjl%i#qENZxP|p=K_KbU>;s&5sYvzlMg0wb9{{ZOXGqS(2PuRpHcfPgpGcA#h zhJ2Vt(~p_BJQx1|0J5!eN_wx00#^Cdx{RfcgsL;Ld6H!k0Ss)LfWW;zp5J<^jlMUUgIanT}9t`9H+80Xw)W{J-%Bhy~r>*X~g}RA)S4 zU(`ln?7S7Dj0f7m8wiPp_X=1?m{N05A@mN*^ly@@obHqcB4BNSj#3sw)3I70PI_oRD%KUB^yJhUD z^pVO6$BJho0Qh9(RSQQBW;wPU?7+gnyj~SIQtFR9K+C&kI}^sG%rWG}{ve8QQSA*? z?W41p9AyzVk*x$Wl(rOQ_D!dW4-&Z z5>+>-y3a53(gmjgpbi3CUWPN=cTIf8C0pI?z4hNP1|3xps*g1fL1J}E_z#4A0cbBs@ZCpcJ`bMrLAhxI=*&!-UE z&>?8d%MY1b?Gb_)fW|P!l(-WWFPJ$wxD=L4*3nNJ_J$1lVhcb>+RDTdF{)q9#sT;+ z38X3l+9p8`J|NME_8@*)M8~0C^A7iWMZlGI#q>1V{6&CkC^U1{y~w8-i&^rz`HULU zzJ1hfPL-MSz2Az7!1Mt6U)(ubIafYEVrNaErMmCGxB$y5arqM`d0O*5VDpG?9U*rJ z6ZsBK_ny&sQ*XfG#Z#WUBhGO~6vB|}09S{P+B}aH4zP2WB40Nu`Z$fJ1C#8P9VGHJ z9*tQ3#yFuS*B*HvaE3U&i7OQwjr>PC)95N-Qaqj`IiR8jvVOe97dGV4?q6N|#}F{h z{{VeWhR&2`k1r?W{{WMp3_zXpm*Izqqg(jH%0tDt`Xsp6au-^l=`nbo@$*HF>`MCN67G<#bGF z=hyKHtZo_Jm~`n5cj?$BXWciI8azf=Ss-C5^Wgb{MQwZ$V;5)4iWtztbZsgag|d7r z`h>+`Z<`113k8-X-lASnsue=6K43m88009=n0w0ZM_7SdVxzquf(a%F_AxNgu)V*c zcV3={D0lo2Pi+qL9AA_^BMzVzE_cQYed2!tgW-9k`|x|UJ(0F>APN4c2BhZg?;a3IoPBu{&V zY_|bv(}&XtBTO`Mb6h>h5i~mI61-FO)-vEJ)Oq-W))DRh0Dz%r6PTi#Pv&tbx27Rn zRO7@A!%SLB#Gzq`3ITS^Y8oX_NG1tL!hgSrc61A*#(2GZccuoCad7_t*-djD!nu^& zb!<22>Ga7M_jIyl3G+q!M)Gu@%@a31h*hJbfMU~<@mCwVcp28-=n9mj^fo_K~M zJGk_Fk@R~`O#C7QmyxpHJi|n6LC<)KxL@_lobO-FQvU$Lfh21o=u#11dCY-{DP8ts2E>8jC;QH%FTDJVthL;OHQvp|yegQ-(1G(rd6d%&ywvS0%tQUmqOY0Tyu>f8r;liF3_&Dd6tZu< z#u71Lc5z!gLY$+kfWMel5`y-&{{V8^Rdn+#6vM`2P*o-PfRSP&HOT&$Ahy}@nMV=6 zVB!T!fuGOR8O%(%#H_W}T_Hfe2Z*Lg*aljmG`1?A*LVT~1yz2EflFRX9e+Mp_|XOA zx3NdGMU@>Pe<6Mj@#hl>MrQc~I0tU^m*Qf8M!dFFmAmw|>JQWyRd$EYq5Fk%D`2U! z+oWqU{#Z7`@)}^)I?KDRHyBUEqSyZbVDmLJv!%iKNE%-M3vG88=ab|g8_mGkmCOU5H1||^v4^6j)PLl>iWO9GEfjwF=+Gc zP24YjChq?L*8#}RTjvskhlsgrh+CPSc!v9NRql}=GeD)HawBdk}(c4 zy#QbPQrJ)jG;Q`i*k?!%uXn2MRcashT#bGu+H&QDvastAYbNelb2HbpScEJniw{@< zg?x{Px-3Rvm?vHb-YuP>aJaFuzD<7MfCVCka-aL=dFRvF1%l%lT}NXxQ7Wy62UlJW zog+lFEz+L)`9R5U8Ox$vGhtB>tyBD zJ(Yl}L-K)?@IMKBi(8rF5%DV%ubTAcW^r7?aS5E?l4+S;C5E}3XA^9!XN^KTVP77F z5(`K)^&Te;1z=~Xr$+7qVk@i$BAve?Ag(`&S$wLTCm-B?PcKVY8tCW53w)WldKHKj zMX=-N=b2e2Y{J`q*^zxMKoqRrIgM%<=@PY3NR)8ZUA<*0mt(tEJ(Zimj5;qKT==QK z_IIAA$L0kpB|0?mdE0@A511BRe-OvOe(pW7I)jM+0EFDIT?}0wI9WLr_fh*ND9!{mN9O(-)7JHni$@JX~D;LF)79 z#bpuWxbFEQ65Xp->!JFJ)15>hp%R>DaSZ{`I(GN@>kv^ifx8}IRQH<7z_>c{cZjIH zQ{C=#{{XQ81aH2n->$Lrbii8a+83MaewR)lazD#Ina2MBxy~b%zVBksoA#TiFkNN;$DRVa;t1B0%mrd@mf?aAlB03A=2+xrT%=V2jn~Vmifx%56j9qimC9jeL%jR;4yapY=#7ln*8a=%-^#W44JC-zP?G7RMlwFapGqK`Y)x<*; z#Z1}xtomGY%ae!+Q#a(J>9D!MM^9!pfL>)!19dj?m_fYb0 z>4@`G=lEF6P#amkPvQt(EzvK8+y;QxQx!0)3;~xD=U}t`k zA2CA{PMWCErg1Q|TpU5pCBpdW33AGnQ^Z115l%~_>zP-+zvfZ44!!C;e3Ye+d&^Su zEAE}Hi}ZPbqE(>p0Rwes=4NN5ToTi?vYgI*+&Md#IPFu}2(t4B4Z0wgn#N&f3Ct3= z6}g4n=&VPy5Zb;$-?#be7Vmfddc+>>M0y%6e&y$~1sE)`BhfZ*hY$ohCdWkR^lXf- zk~^F|{{ZZ>JNv8f{o-cl8POj6WP|{0rutiLz9U0AcZTFacFmrNiEXe`JwoY+}=5jmUxYF^B#Q4#Z)7x9sBA#%r)syGWTLHJs@Ur zPm0+nT)BN_FB>77 zs5z|JF$AV!v#CWmg+(?H2N1XMOYBtc6DtCQpvppE-uyzRO35K!lNmB9e5 z!sXfC6*RTya^D9qXi@PB)?FBbmAjHVTm3^XXCgN8<)fY=+_duuTDUjwXY()IR39d* z@4PkDdB3OTBwG*zru^L2SXy-IsPeUh;@w?Vepqra1*LpH+__s$(KVKcZw3z1;5Q4< zOXT8ZA^36#^M)%-vJ^nDUBAX*@10AtyVD!(aQsd(Fhf0|0bbZMXU4n0CP*9=bvk#+ zI4ZQ0=Q7EztukNuxPx9cnQRyx510TH1s)~w2d}gn%f4m4W`%v>XwUMW1;frMV zoH*X4vwUJ}Pj6{#HNIkkb(AEfB3<^TCw#4U-)H_hK~}iw9j(apgqpE?uKf?@4U)si zoIM`1%r%Lsp9dq!+l1tUR%yMBiP^$y5`efF_-OV|v&6rPfxC{Ens_OXa%*|CAn3ye z4D6-;C8^||e}!Uiyf2H5m({68XV2hTKc9)OAKwIHXeBGO;%wlHj}gvH)a@Fng2Gg- z_l}xnc7+oa3t?=meqd4NW(VREH9JF*n$*q18<&a02dowlp7O{%T)6KLyvu_z)_coy zY`-|f-OKN(w6eDb+&3>T5MciR5OEf!jJ0N^;^5&2(i+hhFz&i)ZDs3Nm>0Ydl7inve<@EE`VDdoN{r;ke!VZo90K3XFCP+LE9{&LH zS1^To4*4sid6n3h@3?mu)@55j%itw)JM7G$@b@;ZoLJ=k;f8tC6wmJJf;l+p%-MGM z)YEolrfE|OG|<<0+eQuVf5JO^OD_qaYYwmJ`d&wL9OnNRJ zW6aV1C1T*>q0gXUeV|NyejwXzRY1MIB81f11)jHS%g57ho=9-Mj}g#Kl=_8{EMxW@YW8e-!T z@h^Sh-d-mgoMY48H7e#IS&yaS9jBRnrNyaabuMmj)Vx$KoA!nL{-!1~{?r5bg;QG8 zJ5KCg4SIXa3%Ic68M(%0AWnTQrhTRICuwgxm-QEx33bf9!Nhu&aF@6=Afp{1}Q!f%5MVivO;6V26Nm8?mRyPcYQF{KsSb)$d&aF zV$b!&XviJ`99Kv}HMC;POKad|Qaj5U&2`Chl8;8c(a$lvAXuS|4NMD;1{U+5<^c*H z?anN7Gq~i-9cn!;GmJX)A?6oY)bKN{bGT89D9kFRZ%{KiYs90OWy)6&I4={pCT5fK z3trxy?7589LEXYx#5JA6S1w|`FB|l6_=LoFyg_Oc$5LwgTqlTM9=G0pVcu&~i% zyTJy|&`a@|eI@ma4R@C+5y&8jX5}llwmm?A>LHtVrmTbggLp2^whJY z+1h009R1DpoI}3l^z`O;?>gKyxN~q$q%YBN6@!V_GZzZ$J?FgQUJ|D@J@qbMFxEW> zwxQ3DOnR9&4wJ8-+{ClzNR6Iu3?OnsFzpMMh*C}=2lX>NN+l*vddurPL(|M9%oxNO zaJ?p>9b9Eaig-X+V+_Z?XWejvT! z);rw3IhedG;)gJ;JDPSOQ}|3j-8~pW<;+)qO2(y`?djSbisRSu8a?M7qhAr}?KLsX zO~O`XOI|p{4EQB7p0g17n2qV<5X>!8oxsLcd%_OtaRT|BZ-`;XNLuBZU%6O?WI9|S z`>#WulZ{M|X#Qc3LsJk8y}dc4%*D8#2wB9;&!L%3ZdN>0<<92TcRuGtWIZK z%+K4?-h05_ckAz&q8gswoX7DvnW_7Q^&{FS%;z%`61p(kqzWcY3_0yQASE-}4-*ib zUXhio%uL=pL+PIL6PlZ6Thzx9=f0!O_Kw=o7-wFU-eb-uUr?1QBm$nW?Zhil3GH*7 zT;^9LVjbaCnZ_bYEVZDU)}cmAhuoml;r52I#=6Vx>CAPB#PKuCItF-|uS>)^^te|t z;F9`8qH#P<wbqf>DLn#2Xgw(vYvf-nT_gj^^6=n zqWPDO@Rts<$>Z0ILhHmkOlp7FPO~|M?(sXrc%1qXuB>Wftj%Sed%>b!eFe-Skg9w~ zP|FPKnMWAmhelBU0B#C1s669P*Syvn^u8xv^Brg6T*niSOWr%q9o*|3Gt6f;!+vFT zg;$))#^I^M17WfxqpUfV$vG>eWf!f@t;!45A!f^t;9jSNt&wv0LKvKYT=uPq24cFL!$a_cU6{XmyqMH zdA52+6sFN9TZMWRaO(?(A>LxW55(W4dUA=l!scO_oRa-}++ojZm)2tCla%*{EZsml z{{Z9O70a1Nsozt2VLU#DC!;j0$uhLE@#1H5_bWHj8?Q|nmRF^Bh9Sf^{>=O#;ozR! z+&*Sm=?;4CJkKoGqpVK2^m^33p7E^I$2`n>E#T?tc%CLB*_dIK*O*o6R{O$csz2{( zSe}1An5!Y+_k#qs-jg4>%CAw&k1~J42g|YE-_yn1yB(uHnvN=fMlU)0{&80GVG=^RG<8 z7xOro(Y2xm`%W%mrYqCLuS(A0%=esn`1Rtw5@VS2(1Ji7@cySTpxk>$6A!1DYY^1F zkb3&q_?JDU;uEdvVlw){&G8GY%jiLamR&gam&~>3{-M0fyxcF$Ixz1HI>6?0g;X{7 zFuUnQw(Gn(x%!PhoO)Lr%KqmQ{v%q=PZ7Q$-sJIc%IvjNz{2^-aC}h zy~m`ksTo~0FJ$5iiJENOIjLA>K>*0XiJZ*t@WWY^4eoePX~gX54u>0@?qXw^)-_OE zy(9kbHyWr4VbJ1n3`}!-hcgq2c$lwFY8bloFU;n1IhfqOW^QNF;kxv>ZL;xRxcQl! zGP+E`hT)G+W__nv^yx3|a~OA>dQRPE545;ssQ2~E{wii^ncd5CFK4v6?}%wQ^u!+W z{V$2R*=4m;5vWGT9O4|#C8l^cFRAwn{v|Z|f!1TRY#Y_?~+De#uiwm~Lm8o_xR}LaJw}3`KT$pNYgylE0W@CoUyB&iqHE zveewiN!c@*o0YEHl}Q2n6rr+tZ9faSG3-eKFV59+`$=xIz<<{7tFh}Gs9#0-2UbiwN=*#yY&La$zD<{i97jq4LvF4~QC zg$qfDJF1I#M;oihpyh;hciLk{=U&p_@7iNsCTejw?*Zmtq0M_sf~^|VYd6QD^r?79 ztj+HcUL)3B?g^0Na^*EIGWyGfU*Dz6W%1T67Qx-kn1^;^)}iTg;Zo-PIF?l7(#YQ$ zg2T~ni>iaUe=`2JE%dsaw>l-?w07SR&zRLR$$Z1CQ2T{n%%r(~i@vd6#LY#QJB!60 zfw|1TPG3JWyt=FM%5^Rd91v5Jd=y!;ZREWmq}a%|JkPJc>4eV diff --git a/ocrs/src/lib.rs b/ocrs/src/lib.rs deleted file mode 100644 index 304f51bc..00000000 --- a/ocrs/src/lib.rs +++ /dev/null @@ -1,954 +0,0 @@ -use std::collections::HashMap; -use std::error::Error; - -use rayon::prelude::*; - -use rten::ctc::{CtcDecoder, CtcHypothesis}; -use rten::{Dimension, FloatOperators, Model, Operators, RunOptions}; -use rten_imageproc::{bounding_rect, BoundingRect, Line, Point, Polygon, Rect, RotatedRect}; -use rten_tensor::prelude::*; -use rten_tensor::{NdTensor, NdTensorView, Tensor}; - -mod log; -pub mod page_layout; -mod text_items; - -#[cfg(target_arch = "wasm32")] -mod wasm_api; - -use page_layout::{find_connected_component_rects, find_text_lines, line_polygon}; -pub use text_items::{TextChar, TextItem, TextLine, TextWord}; - -/// Return the smallest multiple of `factor` that is >= `val`. -fn round_up< - T: Copy - + std::ops::Add - + std::ops::Sub - + std::ops::Rem, ->( - val: T, - factor: T, -) -> T { - let rem = val % factor; - (val + factor) - rem -} - -// nb. The "E" before "ABCDE" should be the EUR symbol. -const DEFAULT_ALPHABET: &str = " 0123456789!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~EABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - -/// The value used to represent fully black pixels in OCR input images -/// prepared by [prepare_image]. -const BLACK_VALUE: f32 = -0.5; - -/// Convert a CHW image into a greyscale image. -/// -/// This function is intended to approximately match torchvision's RGB => -/// greyscale conversion when using `torchvision.io.read_image(path, -/// ImageReadMode.GRAY)`, which is used when training models with greyscale -/// inputs. torchvision internally uses libpng's `png_set_rgb_to_gray`. -/// -/// `normalize_pixel` is a function applied to each greyscale pixel value before -/// it is written into the output tensor. -fn greyscale_image f32>( - img: NdTensorView, - normalize_pixel: F, -) -> NdTensor { - let [chans, height, width] = img.shape(); - assert!( - chans == 1 || chans == 3 || chans == 4, - "expected greyscale, RGB or RGBA input image" - ); - - let mut output = NdTensor::zeros([1, height, width]); - - let used_chans = chans.min(3); // For RGBA images, only RGB channels are used - let chan_weights: &[f32] = if chans == 1 { - &[1.] - } else { - // ITU BT.601 weights for RGB => luminance conversion. These match what - // torchvision uses. See also https://stackoverflow.com/a/596241/434243. - &[0.299, 0.587, 0.114] - }; - - let mut out_lum_chan = output.slice_mut([0]); - - for y in 0..height { - for x in 0..width { - let mut pixel = 0.; - for c in 0..used_chans { - pixel += img[[c, y, x]] * chan_weights[c]; - } - out_lum_chan[[y, x]] = normalize_pixel(pixel); - } - } - output -} - -/// Prepare an image for use with [detect_words] and [recognize_text_lines]. -/// -/// This converts an input CHW image with values in the range 0-1 to a greyscale -/// image with values in the range `BLACK_VALUE` to `BLACK_VALUE + 1`. -fn prepare_image(image: NdTensorView) -> NdTensor { - greyscale_image(image, |pixel| pixel + BLACK_VALUE) -} - -/// Detect text words in a greyscale image. -/// -/// `image` is a greyscale CHW image with values in the range `ZERO_VALUE` to -/// `ZERO_VALUE + 1`. `model` is a model which takes an NCHW input tensor and -/// returns a binary segmentation mask predicting whether each pixel is part of -/// a text word or not. The image is padded and resized to the model's expected -/// input size before performing detection. -/// -/// The result is an unsorted list of the oriented bounding rectangles of -/// connected components (ie. text words) in the mask. -fn detect_words( - image: NdTensorView, - model: &Model, - debug: bool, -) -> Result, Box> { - let input_id = model - .input_ids() - .first() - .copied() - .ok_or("model has no inputs")?; - let input_shape = model - .node_info(input_id) - .and_then(|info| info.shape()) - .ok_or("model does not specify expected input shape")?; - - let [img_chans, img_height, img_width] = image.shape(); - - // Add batch dim - let image = image.reshaped([1, img_chans, img_height, img_width]); - - let (in_height, in_width) = match input_shape[..] { - [_, _, Dimension::Fixed(h), Dimension::Fixed(w)] => (h, w), - _ => { - return Err("failed to get model dims".into()); - } - }; - - // Pad small images to the input size of the text detection model. This is - // needed because simply scaling small images up to a fixed size may produce - // very large or distorted text that is hard for detection/recognition to - // process. - // - // Padding images is however inefficient because it means that we are - // potentially feeding a lot of blank pixels into the text detection model. - // It would be better if text detection were able to accept variable-sized - // inputs, within some limits. - let pad_bottom = (in_height as i32 - img_height as i32).max(0); - let pad_right = (in_width as i32 - img_width as i32).max(0); - let grey_img = if pad_bottom > 0 || pad_right > 0 { - let pads = &[0, 0, 0, 0, 0, 0, pad_bottom, pad_right]; - image.pad(pads.into(), BLACK_VALUE)? - } else { - image.as_dyn().to_tensor() - }; - - // Resize images to the text detection model's input size. - let resized_grey_img = grey_img.resize_image([in_height, in_width])?; - - // Run text detection model to compute a probability mask indicating whether - // each pixel is part of a text word or not. - let text_mask: Tensor = model - .run_one( - (&resized_grey_img).into(), - if debug { - Some(RunOptions { - timing: true, - verbose: false, - ..Default::default() - }) - } else { - None - }, - )? - .try_into()?; - - // Resize probability mask to original input size and apply threshold to get a - // binary text/not-text mask. - let text_mask = text_mask - .slice(( - .., - .., - ..(in_height - pad_bottom as usize), - ..(in_width - pad_right as usize), - )) - .resize_image([img_height, img_width])?; - let threshold = 0.2; - let binary_mask = text_mask.map(|prob| if *prob > threshold { 1i32 } else { 0 }); - - // Distance to expand bounding boxes by. This is useful when the model is - // trained to assign a positive label to pixels in a smaller area than the - // ground truth, which may be done to create separation between adjacent - // objects. - let expand_dist = 3.; - - let word_rects = - find_connected_component_rects(binary_mask.slice([0, 0]).nd_view(), expand_dist); - - Ok(word_rects) -} - -/// Details about a text line needed to prepare the input to the text -/// recognition model. -#[derive(Clone)] -struct TextRecLine { - /// Index of this line in the list of lines found in the image. - index: usize, - - /// Region of the image containing this line. - region: Polygon, - - /// Width to resize this line to. - resized_width: u32, -} - -/// Prepare an NCHW tensor containing a batch of text line images, for input -/// into the text recognition model. -/// -/// For each line in `lines`, the line region is extracted from `image`, resized -/// to a fixed `output_height` and a line-specific width, then copied to the -/// output tensor. Lines in the batch can have different widths, so the output -/// is padded on the right side to a common width of `output_width`. -fn prepare_text_line_batch( - image: &NdTensorView, - lines: &[TextRecLine], - page_rect: Rect, - output_height: usize, - output_width: usize, -) -> NdTensor { - let mut output = NdTensor::zeros([lines.len(), 1, output_height, output_width]); - output.apply(|_| BLACK_VALUE); - - // Page rect adjusted to only contain coordinates that are valid for - // indexing into the input image. - let page_index_rect = page_rect.adjust_tlbr(0, 0, -1, -1); - - for (group_line_index, line) in lines.iter().enumerate() { - let grey_chan = image.slice([0]); - - let line_rect = line.region.bounding_rect(); - let mut line_img = - NdTensor::zeros([line_rect.height() as usize, line_rect.width() as usize]); - line_img.apply(|_| BLACK_VALUE); - - for in_p in line.region.fill_iter() { - let out_p = Point::from_yx(in_p.y - line_rect.top(), in_p.x - line_rect.left()); - if !page_index_rect.contains_point(in_p) || !page_index_rect.contains_point(out_p) { - continue; - } - line_img[[out_p.y as usize, out_p.x as usize]] = - grey_chan[[in_p.y as usize, in_p.x as usize]]; - } - - let resized_line_img = line_img - .reshaped([1, 1, line_img.size(0), line_img.size(1)]) - .resize_image([output_height, line.resized_width as usize]) - .unwrap(); - - let resized_line_img: NdTensorView = - resized_line_img.squeezed().try_into().unwrap(); - - output - .slice_mut((group_line_index, 0, .., ..(line.resized_width as usize))) - .copy_from(&resized_line_img); - } - - output -} - -/// Return the bounding rectangle of the slice of a polygon with X coordinates -/// between `min_x` and `max_x` inclusive. -fn polygon_slice_bounding_rect( - poly: Polygon, - min_x: i32, - max_x: i32, -) -> Option { - poly.edges() - .filter_map(|e| { - let e = e.rightwards(); - - // Filter out edges that don't overlap [min_x, max_x]. - if (e.start.x < min_x && e.end.x < min_x) || (e.start.x > max_x && e.end.x > max_x) { - return None; - } - - // Truncate edge to [min_x, max_x]. - let trunc_edge_start = e - .to_f32() - .y_for_x(min_x as f32) - .map(|y| Point::from_yx(y.round() as i32, min_x)) - .unwrap_or(e.start); - - let trunc_edge_end = e - .to_f32() - .y_for_x(max_x as f32) - .map(|y| Point::from_yx(y.round() as i32, max_x)) - .unwrap_or(e.end); - - Some(Line::from_endpoints(trunc_edge_start, trunc_edge_end)) - }) - .fold(None, |bounding_rect, e| { - let edge_br = e.bounding_rect(); - bounding_rect.map(|br| br.union(edge_br)).or(Some(edge_br)) - }) -} - -/// Method used to decode sequence model outputs to a sequence of labels. -/// -/// See [CtcDecoder] for more details. -#[derive(Copy, Clone, Default)] -pub enum DecodeMethod { - #[default] - Greedy, - BeamSearch { - width: u32, - }, -} - -#[derive(Clone, Default)] -pub struct RecognitionOpt { - pub debug: bool, - - /// Method used to decode character sequence outputs to character values. - pub decode_method: DecodeMethod, -} - -/// Input and output from recognition for a single text line. -struct LineRecResult { - /// Input to the recognition model. - line: TextRecLine, - - /// Length of input sequences to recognition model, padded so that all - /// lines in batch have the same length. - rec_input_len: usize, - - /// Length of output sequences from recognition model, used as input to - /// CTC decoding. - ctc_input_len: usize, - - /// Output label sequence produced by CTC decoding. - ctc_output: CtcHypothesis, -} - -/// Combine information from the input and output of text line recognition -/// to produce [TextLine]s containing character sequences and bounding boxes -/// for each line. -/// -/// Entries in the result may be `None` if no text was recognized for a line. -fn text_lines_from_recognition_results(results: &[LineRecResult]) -> Vec> { - results - .iter() - .map(|result| { - let line_rect = result.line.region.bounding_rect(); - let x_scale_factor = (line_rect.width() as f32) / (result.line.resized_width as f32); - - // Calculate how much the recognition model downscales the image - // width. We assume this will be an integer factor, or close to it - // if the input width is not an exact multiple of the downscaling - // factor. - let downsample_factor = - (result.rec_input_len as f32 / result.ctc_input_len as f32).round() as u32; - - let steps = result.ctc_output.steps(); - let text_line: Vec = steps - .iter() - .enumerate() - .filter_map(|(i, step)| { - // X coord range of character in line recognition input image. - let start_x = step.pos * downsample_factor; - let end_x = if let Some(next_step) = steps.get(i + 1) { - next_step.pos * downsample_factor - } else { - result.line.resized_width - }; - - // Map X coords to those of the input image. - let [start_x, end_x] = [start_x, end_x] - .map(|x| line_rect.left() + (x as f32 * x_scale_factor) as i32); - - // Since the recognition input is padded, it is possible to - // get predicted characters in the output with positions - // that correspond to the padding region, and thus are - // outside the bounds of the original line. Ignore these. - if start_x >= line_rect.right() { - return None; - } - - let char = DEFAULT_ALPHABET - .chars() - .nth((step.label - 1) as usize) - .unwrap_or('?'); - - Some(TextChar { - char, - rect: polygon_slice_bounding_rect( - result.line.region.borrow(), - start_x, - end_x, - ) - .expect("invalid X coords"), - }) - }) - .collect(); - - if text_line.is_empty() { - None - } else { - Some(TextLine::new(text_line)) - } - }) - .collect() -} - -/// Encapsulates validation and execution of the text line recognition model. -struct RecognitionModel { - model: Model, - input_id: usize, - input_shape: Vec, - output_id: usize, -} - -impl RecognitionModel { - /// Validate that a model has the expected inputs and outputs for - /// a text recognition model and wrap it as a [RecognitionModel]. - fn from_model(model: Model) -> Result> { - let input_id = model - .input_ids() - .first() - .copied() - .ok_or("recognition model has no inputs")?; - let input_shape = model - .node_info(input_id) - .and_then(|info| info.shape()) - .ok_or("recognition model does not specify input shape")?; - let output_id = model - .output_ids() - .first() - .copied() - .ok_or("recognition model has no outputs")?; - Ok(RecognitionModel { - model, - input_id, - input_shape: input_shape.into_iter().collect(), - output_id, - }) - } - - /// Return the expected height of input line images. - fn input_height(&self) -> u32 { - match self.input_shape[2] { - Dimension::Fixed(size) => size.try_into().unwrap(), - Dimension::Symbolic(_) => 50, - } - } - - /// Run text recognition on an NCHW batch of text line images, and return - /// a `[batch, seq, label]` tensor of class probabilities. - fn run(&self, input: NdTensor) -> Result, Box> { - let input: Tensor = input.into(); - let [output] = - self.model - .run_n(&[(self.input_id, (&input).into())], [self.output_id], None)?; - let mut rec_sequence: NdTensor = output.try_into()?; - - // Transpose from [seq, batch, class] => [batch, seq, class] - rec_sequence.permute([1, 0, 2]); - - Ok(rec_sequence) - } -} - -/// Recognize text lines in an image. -/// -/// `image` is a CHW greyscale image with values in the range `ZERO_VALUE` to -/// `ZERO_VALUE + 1`. `lines` is a list of detected text lines, where each line -/// is a sequence of word rects. `model` is a recognition model which accepts an -/// NCHW tensor of greyscale line images and outputs a `[sequence, batch, label]` -/// tensor of log probabilities of character classes, which must be converted to -/// a character sequence using CTC decoding. -/// -/// Entries in the result can be `None` if no text was found in a line. -fn recognize_text_lines( - image: NdTensorView, - lines: &[Vec], - model: &RecognitionModel, - opts: RecognitionOpt, -) -> Result>, Box> { - let RecognitionOpt { - debug, - decode_method, - } = opts; - - let [_, img_height, img_width] = image.shape(); - let page_rect = Rect::from_hw(img_height as i32, img_width as i32); - - // Compute width to resize a text line image to, for a given height. - fn resized_line_width(orig_width: i32, orig_height: i32, height: i32) -> u32 { - // Min/max widths for resized line images. These must match the PyTorch - // `HierTextRecognition` dataset loader. - let min_width = 10.; - let max_width = 800.; - let aspect_ratio = orig_width as f32 / orig_height as f32; - (height as f32 * aspect_ratio).max(min_width).min(max_width) as u32 - } - - // Group lines into batches which will have similar widths after resizing - // to a fixed height. - // - // It is more efficient to run recognition on multiple lines at once, but - // all line images in a batch must be padded to an equal length. Some - // computation is wasted on shorter lines in the batch. Choosing batches - // such that all line images have a similar width reduces this wastage. - // There is a trade-off between maximizing the batch size and minimizing - // the variance in width of images in the batch. - let rec_img_height = model.input_height(); - let mut line_groups: HashMap> = HashMap::new(); - for (line_index, word_rects) in lines.iter().enumerate() { - let line_rect = bounding_rect(word_rects.iter()) - .expect("line has no words") - .integral_bounding_rect(); - let resized_width = - resized_line_width(line_rect.width(), line_rect.height(), rec_img_height as i32); - let group_width = round_up(resized_width, 50); - line_groups - .entry(group_width as i32) - .or_default() - .push(TextRecLine { - index: line_index, - region: Polygon::new(line_polygon(word_rects)), - resized_width, - }); - } - - // Split large line groups up into smaller batches that can be processed - // in parallel. - let max_lines_per_group = 20; - let line_groups: Vec<(i32, Vec)> = line_groups - .into_iter() - .flat_map(|(group_width, lines)| { - lines - .chunks(max_lines_per_group) - .map(|chunk| (group_width, chunk.to_vec())) - .collect::>() - }) - .collect(); - - // Run text recognition on batches of lines. - let mut line_rec_results: Vec = line_groups - .into_par_iter() - .flat_map(|(group_width, lines)| { - if debug { - println!( - "Processing group of {} lines of width {}", - lines.len(), - group_width, - ); - } - - let rec_input = prepare_text_line_batch( - &image, - &lines, - page_rect, - rec_img_height as usize, - group_width as usize, - ); - - // TODO - Propagate errors from recognition model to caller. - let rec_output = model.run(rec_input).expect("recognition failed"); - let ctc_input_len = rec_output.shape()[1]; - - // Apply CTC decoding to get the label sequence for each line. - lines - .into_iter() - .enumerate() - .map(|(group_line_index, line)| { - let decoder = CtcDecoder::new(); - let input_seq = rec_output.slice([group_line_index]); - let ctc_output = match decode_method { - DecodeMethod::Greedy => decoder.decode_greedy(input_seq), - DecodeMethod::BeamSearch { width } => decoder.decode_beam(input_seq, width), - }; - LineRecResult { - line, - rec_input_len: group_width as usize, - ctc_input_len, - ctc_output, - } - }) - .collect::>() - }) - .collect(); - - // The recognition outputs are in a different order than the inputs due to - // batching and parallel processing. Re-sort them into input order. - line_rec_results.sort_by_key(|result| result.line.index); - - let text_lines = text_lines_from_recognition_results(&line_rec_results); - - Ok(text_lines) -} - -/// Configuration for an [OcrEngine] instance. -#[derive(Default)] -pub struct OcrEngineParams { - /// Model used to detect text words in the image. - pub detection_model: Option, - - /// Model used to recognize lines of text in the image. - pub recognition_model: Option, - - /// Enable debug logging. - pub debug: bool, - - pub decode_method: DecodeMethod, -} - -/// Detects and recognizes text in images. -/// -/// OcrEngine uses machine learning models to detect text, analyze layout -/// and recognize text in an image. -pub struct OcrEngine { - detection_model: Option, - recognition_model: Option, - debug: bool, - decode_method: DecodeMethod, -} - -/// Input image for OCR analysis. Instances are created using -/// [OcrEngine::prepare_input] -pub struct OcrInput { - /// CHW tensor with normalized pixel values in [BLACK_VALUE, BLACK_VALUE + 1.]. - pub(crate) image: NdTensor, -} - -impl OcrEngine { - /// Construct a new engine from a given configuration. - pub fn new(params: OcrEngineParams) -> Result> { - let recognition_model = params - .recognition_model - .map(RecognitionModel::from_model) - .transpose()?; - Ok(OcrEngine { - detection_model: params.detection_model, - recognition_model, - debug: params.debug, - decode_method: params.decode_method, - }) - } - - /// Preprocess an image for use with other methods of the engine. - /// - /// The input `image` should be a CHW tensor with values in the range 0-1 - /// and either 1 (grey), 3 (RGB) or 4 (RGBA) channels. - pub fn prepare_input(&self, image: NdTensorView) -> Result> { - Ok(OcrInput { - image: prepare_image(image), - }) - } - - /// Detect text words in an image. - /// - /// Returns an unordered list of the oriented bounding rectangles of each - /// word found. - pub fn detect_words(&self, input: &OcrInput) -> Result, Box> { - let Some(detection_model) = self.detection_model.as_ref() else { - return Err("Detection model not loaded".into()); - }; - detect_words(input.image.view(), detection_model, self.debug) - } - - /// Perform layout analysis to group words into lines and sort them in - /// reading order. - /// - /// `words` is an unordered list of text word rectangles found by - /// [OcrEngine::detect_words]. The result is a list of lines, in reading - /// order. Each line is a sequence of word bounding rectangles, in reading - /// order. - pub fn find_text_lines( - &self, - _input: &OcrInput, - words: &[RotatedRect], - ) -> Vec> { - find_text_lines(words) - } - - /// Recognize lines of text in an image. - /// - /// `lines` is an ordered list of the text line boxes in an image, - /// produced by [OcrEngine::find_text_lines]. - /// - /// The output is a list of [TextLine]s corresponding to the input image - /// regions. Entries can be `None` if no text was found in a given line. - pub fn recognize_text( - &self, - input: &OcrInput, - lines: &[Vec], - ) -> Result>, Box> { - let Some(recognition_model) = self.recognition_model.as_ref() else { - return Err("Recognition model not loaded".into()); - }; - recognize_text_lines( - input.image.view(), - lines, - recognition_model, - RecognitionOpt { - debug: self.debug, - decode_method: self.decode_method, - }, - ) - } - - /// Convenience API that extracts all text from an image as a single string. - pub fn get_text(&self, input: &OcrInput) -> Result> { - let word_rects = self.detect_words(input)?; - let line_rects = self.find_text_lines(input, &word_rects); - let text = self - .recognize_text(input, &line_rects)? - .into_iter() - .filter_map(|line| line.map(|l| l.to_string())) - .collect::>() - .join("\n"); - Ok(text) - } -} - -#[cfg(test)] -mod tests { - use std::error::Error; - - use rten::model_builder::{ModelBuilder, OpType}; - use rten::ops::{MaxPool, Transpose}; - use rten::Dimension; - use rten::Model; - use rten_imageproc::{fill_rect, BoundingRect, Rect, RectF, RotatedRect}; - use rten_tensor::prelude::*; - use rten_tensor::{NdTensor, Tensor}; - - use super::{OcrEngine, OcrEngineParams}; - - /// Generate a dummy CHW input image for OCR processing. - /// - /// The result is an RGB image which is black except for one line containing - /// `n_words` white-filled rects. - fn gen_test_image(n_words: usize) -> NdTensor { - let mut image = NdTensor::zeros([3, 100, 200]); - - for word_idx in 0..n_words { - for chan_idx in 0..3 { - fill_rect( - image.slice_mut([chan_idx]), - Rect::from_tlhw(30, (word_idx * 70) as i32, 20, 50), - 1., - ); - } - } - - image - } - - /// Create a fake text detection model. - /// - /// Takes a CHW input tensor with values in `[-0.5, 0.5]` and adds a +0.5 - /// bias to produce an output "probability map". - fn fake_detection_model() -> Model { - let mut mb = ModelBuilder::new(); - let input_id = mb.add_value( - "input", - Some(&[ - Dimension::Symbolic("batch".to_string()), - Dimension::Fixed(1), - // The real model uses larger inputs (800x600). The fake uses - // smaller inputs to make tests run faster. - Dimension::Fixed(200), - Dimension::Fixed(100), - ]), - ); - mb.add_input(input_id); - - let output_id = mb.add_value("output", None); - mb.add_output(output_id); - - let bias = Tensor::from_scalar(0.5); - let bias_id = mb.add_float_constant(&bias); - mb.add_operator( - "add", - OpType::Add, - &[Some(input_id), Some(bias_id)], - &[output_id], - ); - - let model_data = mb.finish(); - Model::load(&model_data).unwrap() - } - - /// Create a fake text recognition model. - /// - /// This takes an NCHW input with C=1, H=64 and returns an output with - /// shape `[W / 4, N, C]`. In the real model the last dimension is the - /// log-probability of each class label. In this fake we just re-interpret - /// each column of the input as a one-hot vector of probabilities. - fn fake_recognition_model() -> Model { - let mut mb = ModelBuilder::new(); - let input_id = mb.add_value( - "input", - Some(&[ - Dimension::Symbolic("batch".to_string()), - Dimension::Fixed(1), - Dimension::Fixed(64), - Dimension::Symbolic("seq".to_string()), - ]), - ); - mb.add_input(input_id); - - // MaxPool to scale width by 1/4: NCHW => NCHW/4 - let pool_out = mb.add_value("max_pool_out", None); - mb.add_operator( - "max_pool", - OpType::MaxPool(MaxPool { - kernel_size: [1, 4], - padding: [0, 0, 0, 0].into(), - strides: [1, 4], - }), - &[Some(input_id)], - &[pool_out], - ); - - // Squeeze to remove the channel dim: NCHW/4 => NHW/4 - let squeeze_axes = Tensor::from_vec(vec![1]); - let squeeze_axes_id = mb.add_int_constant(&squeeze_axes); - let squeeze_out = mb.add_value("squeeze_out", None); - mb.add_operator( - "squeeze", - OpType::Squeeze, - &[Some(pool_out), Some(squeeze_axes_id)], - &[squeeze_out], - ); - - // Transpose: NHW/4 => W/4NH - let transpose_out = mb.add_value("transpose_out", None); - mb.add_operator( - "transpose", - OpType::Transpose(Transpose { - perm: Some(vec![2, 0, 1]), - }), - &[Some(squeeze_out)], - &[transpose_out], - ); - - mb.add_output(transpose_out); - - let model_data = mb.finish(); - Model::load(&model_data).unwrap() - } - - /// Return expected word locations for an image generated by - /// `gen_test_image(3)`. - /// - /// The output boxes are slightly larger than in input image. This is - /// because the real detection model is trained to predict boxes that are - /// slightly smaller than the ground truth, in order to create a gap between - /// adjacent boxes. The connected components in model outputs are then - /// expanded in post-processing to recover the correct boxes. - fn expected_word_boxes() -> Vec { - let [top, height] = [27, 25]; - [ - Rect::from_tlhw(top, -3, height, 56).to_f32(), - Rect::from_tlhw(top, 66, height, 57).to_f32(), - Rect::from_tlhw(top, 136, height, 57).to_f32(), - ] - .into() - } - - #[test] - fn test_ocr_engine_prepare_input() -> Result<(), Box> { - let image = gen_test_image(3 /* n_words */); - let engine = OcrEngine::new(OcrEngineParams { - detection_model: None, - recognition_model: None, - ..Default::default() - })?; - let input = engine.prepare_input(image.view())?; - - let [chans, height, width] = input.image.shape(); - assert_eq!(chans, 1); - assert_eq!(width, image.size(2)); - assert_eq!(height, image.size(1)); - - Ok(()) - } - - #[test] - fn test_ocr_engine_detect_words() -> Result<(), Box> { - let n_words = 3; - let image = gen_test_image(n_words); - let engine = OcrEngine::new(OcrEngineParams { - detection_model: Some(fake_detection_model()), - recognition_model: None, - ..Default::default() - })?; - let input = engine.prepare_input(image.view())?; - let words = engine.detect_words(&input)?; - - assert_eq!(words.len(), n_words); - - let mut boxes: Vec = words - .into_iter() - .map(|rotated_rect| rotated_rect.bounding_rect()) - .collect(); - boxes.sort_by_key(|b| [b.top() as i32, b.left() as i32]); - - assert_eq!(boxes, expected_word_boxes()); - - Ok(()) - } - - #[test] - fn test_ocr_engine_recognize_lines() -> Result<(), Box> { - let mut image = NdTensor::zeros([1, 64, 32]); - - // Fill a single row of the input image. - // - // The dummy recognition model treats each column of the input as a - // one-hot vector of character class probabilities. Pre-processing of - // the input will shift values from [0, 1] to [-0.5, 0.5]. CTC decoding - // of the output will ignore class 0 (as it represents a CTC blank) - // and repeated characters. - // - // Filling a single input row with "1"s will produce a single char - // output where the char's index in the alphabet is the row index - 1. - // ie. Filling the first row produces " ", the second row "0" and so on, - // using the default alphabet. - image - .slice_mut::<2, _>((.., 2, ..)) - .iter_mut() - .for_each(|x| *x = 1.); - - let engine = OcrEngine::new(OcrEngineParams { - detection_model: None, - recognition_model: Some(fake_recognition_model()), - ..Default::default() - })?; - let input = engine.prepare_input(image.view())?; - - // Create a dummy input line with a single word which fills the image. - let mut line_regions: Vec> = Vec::new(); - line_regions.push( - [Rect::from_tlhw(0, 0, image.shape()[1] as i32, image.shape()[2] as i32).to_f32()] - .map(RotatedRect::from_rect) - .into(), - ); - - let lines = engine.recognize_text(&input, &line_regions)?; - assert_eq!(lines.len(), line_regions.len()); - - assert!(lines.get(0).is_some()); - let line = lines[0].as_ref().unwrap(); - assert_eq!(line.to_string(), "0"); - - Ok(()) - } -} diff --git a/ocrs/src/log.rs b/ocrs/src/log.rs deleted file mode 100644 index 2831b620..00000000 --- a/ocrs/src/log.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Logging macros that work in both WebAssembly + JS environments and native -//! environments. - -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::*; - -#[cfg(target_arch = "wasm32")] -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(js_namespace = console, js_name = log)] - pub fn js_console_log(s: &str); -} - -#[cfg(target_arch = "wasm32")] -#[macro_export] -macro_rules! log { - ($($t:tt)*) => { - $crate::log::js_console_log(&format_args!($($t)*).to_string()) - } -} - -#[cfg(not(target_arch = "wasm32"))] -#[macro_export] -macro_rules! log { - ($($t:tt)*) => (println!($($t)*)) -} diff --git a/ocrs/src/page_layout.rs b/ocrs/src/page_layout.rs deleted file mode 100644 index f91ad888..00000000 --- a/ocrs/src/page_layout.rs +++ /dev/null @@ -1,771 +0,0 @@ -use std::cmp::Ordering; -use std::collections::BinaryHeap; -use std::iter::zip; - -use rten_imageproc::{ - bounding_rect, find_contours, min_area_rect, simplify_polygon, BoundingRect, Coord, Line, - LineF, Point, PointF, Rect, RetrievalMode, RotatedRect, -}; -use rten_tensor::NdTensorView; - -struct Partition { - score: f32, - boundary: Rect, - obstacles: Vec, -} - -impl PartialEq for Partition { - fn eq(&self, other: &Self) -> bool { - self.cmp(other) == Ordering::Equal - } -} - -impl Eq for Partition {} - -impl Ord for Partition { - fn cmp(&self, other: &Self) -> Ordering { - self.score.total_cmp(&other.score) - } -} - -impl PartialOrd for Partition { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -/// Iterator over empty rectangles within a rectangular boundary that contains -/// a set of "obstacles". See [max_empty_rects]. -/// -/// The order in which rectangles are returned is determined by a scoring -/// function `S`. -pub struct MaxEmptyRects -where - S: Fn(Rect) -> f32, -{ - queue: BinaryHeap, - score: S, - min_width: u32, - min_height: u32, -} - -impl MaxEmptyRects -where - S: Fn(Rect) -> f32, -{ - fn new(obstacles: &[Rect], boundary: Rect, score: S, min_width: u32, min_height: u32) -> Self { - let mut queue = BinaryHeap::new(); - - // Sort obstacles by X then Y coord. This means that when we choose a pivot - // from any sub-sequence of `obstacles` we'll also be biased towards picking - // a central obstacle. - let mut obstacles = obstacles.to_vec(); - obstacles.sort_by_key(|o| { - let c = o.center(); - (c.x, c.y) - }); - - if !boundary.is_empty() { - queue.push(Partition { - score: score(boundary), - boundary, - obstacles: obstacles.to_vec(), - }); - } - - MaxEmptyRects { - queue, - score, - min_width, - min_height, - } - } -} - -impl Iterator for MaxEmptyRects -where - S: Fn(Rect) -> f32, -{ - type Item = Rect; - - fn next(&mut self) -> Option { - // Assuming the obstacle list is sorted, eg. by X coordinate, choose - // a pivot that is in the middle. - let choose_pivot = |r: &[Rect]| r[r.len() / 2]; - - while let Some(part) = self.queue.pop() { - let Partition { - score: _, - boundary: b, - obstacles, - } = part; - - if obstacles.is_empty() { - return Some(b); - } - - let pivot = choose_pivot(&obstacles); - let right_rect = Rect::from_tlbr(b.top(), pivot.right(), b.bottom(), b.right()); - let left_rect = Rect::from_tlbr(b.top(), b.left(), b.bottom(), pivot.left()); - let top_rect = Rect::from_tlbr(b.top(), b.left(), pivot.top(), b.right()); - let bottom_rect = Rect::from_tlbr(pivot.bottom(), b.left(), b.bottom(), b.right()); - - let sub_rects = [top_rect, left_rect, bottom_rect, right_rect]; - - for sr in sub_rects { - if (sr.width().max(0) as u32) < self.min_width - || (sr.height().max(0) as u32) < self.min_height - || sr.is_empty() - { - continue; - } - - let sr_obstacles: Vec<_> = obstacles - .iter() - .filter(|o| o.intersects(sr)) - .copied() - .collect(); - - // There should always be fewer obstacles in `sr` since it should - // not intersect the pivot. - assert!(sr_obstacles.len() < obstacles.len()); - - self.queue.push(Partition { - score: (self.score)(sr), - obstacles: sr_obstacles, - boundary: sr, - }); - } - } - - None - } -} - -/// Return an iterator over empty rects in `boundary`, ordered by decreasing -/// value of the `score` function. -/// -/// The `score` function must have the property that for any rectangle R and -/// sub-rectangle S that is contained within R, `score(S) <= score(R)`. A -/// typical score function would be the area of the rect, but other functions -/// can be used to favor different aspect ratios. -/// -/// `min_width` and `min_height` specify thresholds on the size of rectangles -/// yielded by the iterator. -/// -/// The implementation is based on algorithms from [^1]. -/// -/// [^1]: Breuel, Thomas M. “Two Geometric Algorithms for Layout Analysis.” -/// International Workshop on Document Analysis Systems (2002). -pub fn max_empty_rects( - obstacles: &[Rect], - boundary: Rect, - score: S, - min_width: u32, - min_height: u32, -) -> MaxEmptyRects -where - S: Fn(Rect) -> f32, -{ - MaxEmptyRects::new(obstacles, boundary, score, min_width, min_height) -} - -/// Iterator adapter which filters rectangles that overlap rectangles already -/// returned by more than a certain amount. -pub trait FilterOverlapping { - type Output: Iterator; - - /// Create an iterator which filters out rectangles that overlap those - /// already returned by more than `factor`. - /// - /// `factor` is the minimum Intersection-over-Union ratio or Jaccard index [^1]. - /// See also [Rect::iou]. - /// - /// [^1]: - fn filter_overlapping(self, factor: f32) -> Self::Output; -} - -/// Implementation of [FilterOverlapping]. -pub struct FilterRectIter> { - source: I, - - /// Rectangles already found. - found: Vec, - - /// Intersection-over-Union threshold. - overlap_threshold: f32, -} - -impl> FilterRectIter { - fn new(source: I, overlap_threshold: f32) -> FilterRectIter { - FilterRectIter { - source, - found: Vec::new(), - overlap_threshold, - } - } -} - -impl> Iterator for FilterRectIter { - type Item = Rect; - - fn next(&mut self) -> Option { - while let Some(r) = self.source.next() { - if self - .found - .iter() - .any(|f| f.iou(r) >= self.overlap_threshold) - { - continue; - } - self.found.push(r); - return Some(r); - } - None - } -} - -impl> FilterOverlapping for I { - type Output = FilterRectIter; - - fn filter_overlapping(self, factor: f32) -> Self::Output { - FilterRectIter::new(self, factor) - } -} - -fn rects_separated_by_line(a: &RotatedRect, b: &RotatedRect, l: LineF) -> bool { - let a_to_b = LineF::from_endpoints(a.center(), b.center()); - a_to_b.intersects(l) -} - -fn rightmost_edge(r: &RotatedRect) -> LineF { - let mut corners = r.corners(); - corners.sort_by(|a, b| a.x.total_cmp(&b.x)); - Line::from_endpoints(corners[2], corners[3]) -} - -fn leftmost_edge(r: &RotatedRect) -> LineF { - let mut corners = r.corners(); - corners.sort_by(|a, b| a.x.total_cmp(&b.x)); - Line::from_endpoints(corners[0], corners[1]) -} - -/// Group rects into lines. Each line is a chain of oriented rects ordered -/// left-to-right. -/// -/// `separators` is a list of line segments that prevent the formation of -/// lines which cross them. They can be used to specify column boundaries -/// for example. -pub fn group_into_lines(rects: &[RotatedRect], separators: &[LineF]) -> Vec> { - let mut sorted_rects: Vec<_> = rects.to_vec(); - sorted_rects.sort_by_key(|r| r.bounding_rect().left() as i32); - - let mut lines: Vec> = Vec::new(); - - // Minimum amount by which two words must overlap vertically to be - // considered part of the same line. - let overlap_threshold = 5; - - // Maximum amount by which a candidate word to extend a line from - // left-to-right may horizontally overlap the current last word in the line. - // - // This is necessary when the code that produces the input rects can create - // overlapping rects. `find_connected_component_rects` pads the rects it - // produces for example. - let max_h_overlap = 5; - - while !sorted_rects.is_empty() { - let mut line = Vec::new(); - line.push(sorted_rects.remove(0)); - - // Find the best candidate to extend the current line by one word to the - // right, and keep going as long as we can find such a candidate. - loop { - let last = line.last().unwrap(); - let last_edge = rightmost_edge(last); - - if let Some((i, next_item)) = sorted_rects - .iter() - .enumerate() - .filter(|(_, r)| { - let edge = leftmost_edge(r); - r.center().x > last.center().x - && edge.center().x - last_edge.center().x >= -max_h_overlap as f32 - && last_edge.vertical_overlap(edge) >= overlap_threshold as f32 - && !separators - .iter() - .any(|&s| rects_separated_by_line(last, r, s)) - }) - .min_by_key(|(_, r)| r.center().x as i32) - { - line.push(*next_item); - sorted_rects.remove(i); - } else { - break; - } - } - lines.push(line); - } - - lines -} - -/// Find the minimum-area oriented rectangles containing each connected -/// component in the binary mask `mask`. -pub fn find_connected_component_rects( - mask: NdTensorView, - expand_dist: f32, -) -> Vec { - // Threshold for the minimum area of returned rectangles. - // - // This can be used to filter out rects created by small false positives in - // the mask, at the risk of filtering out true positives. The more accurate - // the model producing the mask is, the less this is needed. - let min_area_threshold = 100.; - - find_contours(mask, RetrievalMode::External) - .iter() - .filter_map(|poly| { - let float_points: Vec<_> = poly.iter().map(|p| p.to_f32()).collect(); - let simplified = simplify_polygon(&float_points, 2. /* epsilon */); - - min_area_rect(&simplified).map(|mut rect| { - rect.resize( - rect.width() + 2. * expand_dist, - rect.height() + 2. * expand_dist, - ); - rect - }) - }) - .filter(|r| r.area() >= min_area_threshold) - .collect() -} - -/// A text line is a sequence of RotatedRects for words, organized from left to -/// right. -type TextLine = Vec; - -type TextParagraph = Vec; - -/// Find separators between text blocks. -/// -/// This includes separators between columns, as well as between sections (eg. -/// headings and article contents). -pub fn find_block_separators(words: &[RotatedRect]) -> Vec { - let Some(page_rect) = bounding_rect(words.iter()).map(|br| br.integral_bounding_rect()) else { - return Vec::new(); - }; - - // Estimate spacing statistics - let mut lines = group_into_lines(words, &[]); - lines.sort_by_key(|l| l.first().unwrap().bounding_rect().top().round() as i32); - - let mut all_word_spacings = Vec::new(); - for line in lines.iter() { - if line.len() > 1 { - let mut spacings: Vec<_> = zip(line.iter(), line.iter().skip(1)) - .map(|(cur, next)| { - (next.bounding_rect().left() - cur.bounding_rect().right()).round() as i32 - }) - .collect(); - spacings.sort(); - all_word_spacings.extend_from_slice(&spacings); - } - } - all_word_spacings.sort(); - - let median_word_spacing = all_word_spacings - .get(all_word_spacings.len() / 2) - .copied() - .unwrap_or(10); - let median_height = words - .get(words.len() / 2) - .map(|r| r.height()) - .unwrap_or(10.) - .round() as i32; - - // Scoring function for empty rectangles. Taken from Section 3.D in [1]. - // This favors tall rectangles. - // - // [1] F. Shafait, D. Keysers and T. Breuel, "Performance Evaluation and - // Benchmarking of Six-Page Segmentation Algorithms". - // 10.1109/TPAMI.2007.70837. - let score = |r: Rect| { - let aspect_ratio = (r.height() as f32) / (r.width() as f32); - let aspect_ratio_weight = match aspect_ratio.log2().abs() { - r if r < 3. => 0.5, - r if r < 5. => 1.5, - r => r, - }; - ((r.area() as f32) * aspect_ratio_weight).sqrt() - }; - - // Find separators between columns and articles. - let object_bboxes: Vec<_> = words - .iter() - .map(|r| r.bounding_rect().integral_bounding_rect()) - .collect(); - let min_width = median_word_spacing * 3; - let min_height = (3 * median_height.max(0)) as u32; - - max_empty_rects( - &object_bboxes, - page_rect, - score, - min_width.try_into().unwrap(), - min_height, - ) - .filter_overlapping(0.5) - .take(80) - .collect() -} - -/// Group words into lines and sort them into reading order. -pub fn find_text_lines(words: &[RotatedRect]) -> Vec> { - let separators = find_block_separators(words); - let vertical_separators: Vec<_> = separators - .iter() - .map(|r| { - let center = r.center(); - Line::from_endpoints( - Point::from_yx(r.top(), center.x).to_f32(), - Point::from_yx(r.bottom(), center.x).to_f32(), - ) - }) - .collect(); - - let horizontal_separators: Vec<_> = separators - .iter() - .map(|r| { - let center = r.center(); - Line::from_endpoints( - Point::from_yx(center.y, r.left()).to_f32(), - Point::from_yx(center.y, r.right()).to_f32(), - ) - }) - .collect(); - - let mut lines = group_into_lines(words, &vertical_separators); - - // Approximate a text line by the 1D line from the center of the left - // edge of the first word, to the center of the right edge of the last word. - let midpoint_line = |words: &[RotatedRect]| -> LineF { - assert!(!words.is_empty()); - Line::from_endpoints( - words.first().unwrap().bounding_rect().left_edge().center(), - words.last().unwrap().bounding_rect().right_edge().center(), - ) - }; - - // Sort lines by vertical position. - lines.sort_by_key(|words| midpoint_line(words).center().y as i32); - - let is_separated_by = |line_a: LineF, line_b: LineF, separators: &[LineF]| -> bool { - let a_to_b = Line::from_endpoints(line_a.center(), line_b.center()); - separators.iter().any(|sep| sep.intersects(a_to_b)) - }; - - // Group lines into paragraphs. We repeatedly take the first un-assigned - // line as the seed for a new paragraph, and then add to that para all - // remaining un-assigned lines which are not separated from the seed. - let mut paragraphs: Vec = Vec::new(); - while !lines.is_empty() { - let seed = lines.remove(0); - let mut para = Vec::new(); - para.push(seed.clone()); - - let mut prev_line = midpoint_line(&seed); - - let mut index = 0; - while index < lines.len() { - let candidate_line = midpoint_line(&lines[index]); - if prev_line.horizontal_overlap(candidate_line) > 0. - && !is_separated_by(prev_line, candidate_line, &horizontal_separators) - { - para.push(lines.remove(index)); - prev_line = candidate_line; - } else { - index += 1; - } - } - paragraphs.push(para); - } - - // Flatten paragraphs into a list of lines. - paragraphs - .into_iter() - .flat_map(|para| para.into_iter()) - .collect() -} - -/// Normalize a line so that it's endpoints are sorted from top to bottom. -fn downwards_line(l: Line) -> Line { - if l.start.y <= l.end.y { - l - } else { - Line::from_endpoints(l.end, l.start) - } -} - -/// Return a polygon which contains all the rects in `words`. -/// -/// `words` is assumed to be a series of disjoint rectangles ordered from left -/// to right. The returned points are arranged in clockwise order starting from -/// the top-left point. -/// -/// There are several ways to compute a polygon for a line. The simplest is -/// to use [min_area_rect] on the union of the line's points. However the result -/// will not tightly fit curved lines. This function returns a polygon which -/// closely follows the edges of individual words. -pub fn line_polygon(words: &[RotatedRect]) -> Vec { - let mut polygon = Vec::new(); - - let floor_point = |p: PointF| Point::from_yx(p.y as i32, p.x as i32); - - // Add points from top edges, in left-to-right order. - for word_rect in words.iter() { - let (left, right) = ( - downwards_line(leftmost_edge(word_rect)), - downwards_line(rightmost_edge(word_rect)), - ); - polygon.push(floor_point(left.start)); - polygon.push(floor_point(right.start)); - } - - // Add points from bottom edges, in right-to-left order. - for word_rect in words.iter().rev() { - let (left, right) = ( - downwards_line(leftmost_edge(word_rect)), - downwards_line(rightmost_edge(word_rect)), - ); - polygon.push(floor_point(right.end)); - polygon.push(floor_point(left.end)); - } - - polygon -} - -#[cfg(test)] -mod tests { - use rten_imageproc::{fill_rect, BoundingRect, Point, Polygon, Rect, RectF, RotatedRect, Vec2}; - use rten_tensor::NdTensor; - - use super::max_empty_rects; - use crate::page_layout::{find_connected_component_rects, find_text_lines, line_polygon}; - - /// Generate a grid of uniformly sized and spaced rects. - /// - /// `grid_shape` is a (rows, columns) tuple. `rect_size` and `gap_size` are - /// (height, width) tuples. - fn gen_rect_grid( - top_left: Point, - grid_shape: (i32, i32), - rect_size: (i32, i32), - gap_size: (i32, i32), - ) -> Vec { - let mut rects = Vec::new(); - - let (rows, cols) = grid_shape; - let (rect_h, rect_w) = rect_size; - let (gap_h, gap_w) = gap_size; - - for r in 0..rows { - for c in 0..cols { - let top = top_left.y + r * (rect_h + gap_h); - let left = top_left.x + c * (rect_w + gap_w); - rects.push(Rect::from_tlbr(top, left, top + rect_h, left + rect_w)) - } - } - - rects - } - - /// Return the union of `rects` or `None` if rects is empty. - fn union_rects(rects: &[Rect]) -> Option { - rects - .iter() - .fold(None, |union, r| union.map(|u| u.union(*r)).or(Some(*r))) - } - - #[test] - fn test_max_empty_rects() { - // Create a collection of obstacles that are laid out roughly like - // words in a two-column document. - let page = Rect::from_tlbr(0, 0, 80, 90); - - let left_col = gen_rect_grid( - Point::from_yx(0, 0), - /* grid_shape */ (10, 5), - /* rect_size */ (5, 5), - /* gap_size */ (3, 2), - ); - let left_col_boundary = union_rects(&left_col).unwrap(); - assert!(page.contains(left_col_boundary)); - - let right_col = gen_rect_grid( - Point::from_yx(0, left_col_boundary.right() + 20), - /* grid_shape */ (10, 5), - /* rect_size */ (5, 5), - /* gap_size */ (3, 2), - ); - - let right_col_boundary = union_rects(&right_col).unwrap(); - assert!(page.contains(right_col_boundary)); - - let mut all_cols = left_col.clone(); - all_cols.extend_from_slice(&right_col); - - let max_area_rect = max_empty_rects(&all_cols, page, |r| r.area() as f32, 0, 0).next(); - - assert_eq!( - max_area_rect, - Some(Rect::from_tlbr( - page.top(), - left_col_boundary.right(), - page.bottom(), - right_col_boundary.left() - )) - ); - } - - #[test] - fn test_max_empty_rects_if_none() { - // Case with no empty space within the boundary - let boundary = Rect::from_tlbr(0, 0, 5, 5); - assert_eq!( - max_empty_rects(&[boundary], boundary, |r| r.area() as f32, 0, 0).next(), - None - ); - - // Case where boundary is empty - let boundary = Rect::from_hw(0, 0); - assert_eq!( - max_empty_rects(&[], boundary, |r| r.area() as f32, 0, 0).next(), - None - ); - } - - #[test] - fn test_find_connected_component_rects() { - let mut mask = NdTensor::zeros([400, 400]); - let (grid_h, grid_w) = (5, 5); - let (rect_h, rect_w) = (10, 50); - let rects = gen_rect_grid( - Point::from_yx(10, 10), - (grid_h, grid_w), /* grid_shape */ - (rect_h, rect_w), /* rect_size */ - (10, 5), /* gap_size */ - ); - for r in rects.iter() { - // Expand `r` because `fill_rect` does not set points along the - // right/bottom boundary. - let expanded = r.adjust_tlbr(0, 0, 1, 1); - fill_rect(mask.view_mut(), expanded, 1); - } - - let components = find_connected_component_rects(mask.view(), 0.); - assert_eq!(components.len() as i32, grid_h * grid_w); - for c in components.iter() { - let mut shape = [c.height().round() as i32, c.width().round() as i32]; - shape.sort(); - - // We sort the dimensions before comparison here to be invariant to - // different rotations of the connected component that cover the - // same pixels. - let mut expected_shape = [rect_h, rect_w]; - expected_shape.sort(); - - assert_eq!(shape, expected_shape); - } - } - - #[test] - fn test_find_text_lines() { - // Create a collection of obstacles that are laid out roughly like - // words in a two-column document. - let page = Rect::from_tlbr(0, 0, 80, 90); - let col_rows = 10; - let col_words = 5; - let (line_gap, word_gap) = (3, 2); - let (word_h, word_w) = (5, 5); - - let left_col = gen_rect_grid( - Point::from_yx(0, 0), - /* grid_shape */ (col_rows, col_words), - /* rect_size */ (word_h, word_w), - /* gap_size */ (line_gap, word_gap), - ); - let left_col_boundary = union_rects(&left_col).unwrap(); - assert!(page.contains(left_col_boundary)); - - let right_col = gen_rect_grid( - Point::from_yx(0, left_col_boundary.right() + 20), - /* grid_shape */ (col_rows, col_words), - /* rect_size */ (word_h, word_w), - /* gap_size */ (line_gap, word_gap), - ); - let right_col_boundary = union_rects(&right_col).unwrap(); - assert!(page.contains(right_col_boundary)); - - let mut words: Vec<_> = left_col - .iter() - .chain(right_col.iter()) - .copied() - .map(|r| RotatedRect::from_rect(r.to_f32())) - .collect(); - - let rng = fastrand::Rng::with_seed(1234); - rng.shuffle(&mut words); - let lines = find_text_lines(&words); - - assert_eq!(lines.len() as i32, col_rows * 2); - for line in lines { - assert_eq!(line.len() as i32, col_words); - - let bounding_rect: Option = line.iter().fold(None, |br, r| match br { - Some(br) => Some(br.union(r.bounding_rect())), - None => Some(r.bounding_rect()), - }); - let (line_height, line_width) = bounding_rect - .map(|br| (br.height(), br.width())) - .unwrap_or((0., 0.)); - - // FIXME - The actual width/heights vary by one pixel and hence not - // all match the expected size. Investigate why this happens. - assert!((line_height - word_h as f32).abs() <= 1.); - let expected_width = col_words * (word_w + word_gap) - word_gap; - assert!((line_width - expected_width as f32).abs() <= 1.); - } - } - - #[test] - fn test_line_polygon() { - let words: Vec = (0..5) - .map(|i| { - let center = Point::from_yx(10., i as f32 * 20.); - let width = 10.; - let height = 5.; - - // Vary the orientation of words. The output of `line_polygon` - // should be invariant to different orientations of a RotatedRect - // that cover the same pixels. - let up = if i % 2 == 0 { - Vec2::from_yx(-1., 0.) - } else { - Vec2::from_yx(1., 0.) - }; - RotatedRect::new(center, up, width, height) - }) - .collect(); - let poly = Polygon::new(line_polygon(&words)); - - assert!(poly.is_simple()); - for word in words { - let center = word.bounding_rect().center(); - assert!(poly.contains_pixel(Point::from_yx( - center.y.round() as i32, - center.x.round() as i32 - ))); - } - } -} diff --git a/ocrs/src/text_items.rs b/ocrs/src/text_items.rs deleted file mode 100644 index 65c8e104..00000000 --- a/ocrs/src/text_items.rs +++ /dev/null @@ -1,187 +0,0 @@ -use std::fmt; -use std::fmt::Write; - -use rten_imageproc::{bounding_rect, min_area_rect, Point, Rect, RotatedRect, Vec2}; - -/// A non-empty sequence of recognized characters ([TextChar]) that constitute a -/// logical unit of a document such as a word or line. -pub trait TextItem { - /// Return the sequence of characters that make up this item. - fn chars(&self) -> &[TextChar]; - - /// Return the bounding rectangle of all characters in this item. - fn bounding_rect(&self) -> Rect { - bounding_rect(self.chars().iter().map(|c| &c.rect)).expect("expected valid rect") - } - - /// Return the oriented bounding rectangle of all characters in this item. - fn rotated_rect(&self) -> RotatedRect { - let points: Vec<_> = self - .chars() - .iter() - .flat_map(|c| c.rect.corners()) - .map(Point::to_f32) - .collect(); - let rect = min_area_rect(&points).expect("expected valid rect"); - - // Give the rect a predictable orientation. We currently assume the - // text is horizontal and upright (ie. rotation angle < 180°). - rect.orient_towards(Vec2::from_yx(-1., 0.)) - } -} - -fn fmt_text_item(item: &TI, f: &mut fmt::Formatter) -> fmt::Result { - for c in item.chars().iter().map(|c| c.char) { - f.write_char(c)?; - } - Ok(()) -} - -impl fmt::Display for TextLine { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt_text_item(self, f) - } -} - -/// Details of a single character that was recognized. -#[derive(Clone)] -pub struct TextChar { - /// Character that was recognized. - pub char: char, - - /// Approximate bounding rectangle of character in input image. - pub rect: Rect, -} - -/// Result of recognizing a line of text. -/// -/// This includes the sequence of characters that were found and associated -/// metadata (eg. bounding boxes). -#[derive(Clone)] -pub struct TextLine { - chars: Vec, -} - -impl TextLine { - /// Create a new text line which contains the given characters. - /// - /// Word boundaries are inferred from the presence of characters with - /// [TextChar::char] values that are ASCII spaces. - pub fn new(chars: Vec) -> TextLine { - assert!(!chars.is_empty(), "Text lines must not be empty"); - TextLine { chars } - } - - /// Return an iterator over words in this line. - pub fn words(&self) -> impl Iterator { - self.chars() - .split(|c| c.char == ' ') - .filter(|chars| !chars.is_empty()) - .map(TextWord::new) - } -} - -impl TextItem for TextLine { - /// Return the bounding rects of each character in the line. - fn chars(&self) -> &[TextChar] { - &self.chars - } -} - -/// Subsequence of a [TextLine] that contains a sequence of non-space characters. -pub struct TextWord<'a> { - chars: &'a [TextChar], -} - -impl<'a> TextWord<'a> { - fn new(chars: &'a [TextChar]) -> TextWord { - assert!(!chars.is_empty(), "Text words must not be empty"); - TextWord { chars } - } -} - -impl<'a> TextItem for TextWord<'a> { - fn chars(&self) -> &[TextChar] { - self.chars - } -} - -impl<'a> fmt::Display for TextWord<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt_text_item(self, f) - } -} - -#[cfg(test)] -mod tests { - use rten_imageproc::{BoundingRect, Point, Rect, Vec2}; - - use super::{TextChar, TextItem, TextLine, TextWord}; - - fn gen_text_chars(text: &str, width: i32) -> Vec { - text.chars() - .enumerate() - .map(|(i, char)| TextChar { - char, - rect: Rect::from_tlhw(0, i as i32 * width, 25, width), - }) - .collect() - } - - #[test] - fn test_item_display() { - let chars = gen_text_chars("foo bar baz", 10 /* char_width */); - let line = TextLine::new(chars); - assert_eq!(line.to_string(), "foo bar baz"); - } - - #[test] - fn test_item_rotated_rect() { - // Horizontal word case. The rotated rect and bounding rect are the same. - let char_width = 10; - let chars = gen_text_chars("foo", char_width); - let word = TextWord::new(&chars); - - assert_eq!( - word.bounding_rect(), - Rect::from_tlhw(0, 0, 25, char_width * 3) - ); - - let rot_rect = word.rotated_rect(); - assert_eq!(rot_rect.bounding_rect(), word.bounding_rect().to_f32()); - assert_eq!(rot_rect.up_axis(), Vec2::from_yx(-1., 0.)); - assert_eq!( - word.rotated_rect().corners(), - [(25, 30), (25, 0), (0, 0), (0, 30)].map(|(y, x)| Point::from_yx(y as f32, x as f32)) - ); - - // TODO - Add cases for non-horizontal words. - } - - #[test] - fn test_line_words() { - let char_width = 10; - let chars = gen_text_chars("foo bar baz ", char_width); - let line = TextLine::new(chars); - let words: Vec<_> = line.words().collect(); - - assert_eq!(words.len(), 3); - assert_eq!(words[0].to_string(), "foo"); - assert_eq!( - words[0].bounding_rect(), - Rect::from_tlhw(0, 0, 25, char_width * 3) - ); - - assert_eq!(words[1].to_string(), "bar"); - assert_eq!( - words[1].bounding_rect(), - Rect::from_tlhw(0, char_width * 4, 25, char_width * 3) - ); - - assert_eq!(words[2].to_string(), "baz"); - assert_eq!( - words[2].bounding_rect(), - Rect::from_tlhw(0, char_width * 9, 25, char_width * 3) - ); - } -} diff --git a/ocrs/src/wasm_api.rs b/ocrs/src/wasm_api.rs deleted file mode 100644 index 3d0412de..00000000 --- a/ocrs/src/wasm_api.rs +++ /dev/null @@ -1,375 +0,0 @@ -use wasm_bindgen::prelude::*; - -use rten::ops; -use rten::{Model, OpRegistry}; - -use rten_imageproc::{min_area_rect, BoundingRect, PointF}; -use rten_tensor::prelude::*; -use rten_tensor::NdTensorView; - -use crate::{OcrEngine as BaseOcrEngine, OcrEngineParams, OcrInput, TextItem}; - -/// Options for constructing an [OcrEngine]. -#[wasm_bindgen] -pub struct OcrEngineInit { - op_registry: OpRegistry, - detection_model: Option, - recognition_model: Option, -} - -impl Default for OcrEngineInit { - fn default() -> OcrEngineInit { - OcrEngineInit::new() - } -} - -#[wasm_bindgen] -impl OcrEngineInit { - #[wasm_bindgen(constructor)] - pub fn new() -> OcrEngineInit { - let mut reg = OpRegistry::new(); - - // Register all the operators the OCR models currently use. - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - reg.register_op::(); - - OcrEngineInit { - op_registry: reg, - detection_model: None, - recognition_model: None, - } - } - - /// Load a model for text detection. - #[wasm_bindgen(js_name = setDetectionModel)] - pub fn set_detection_model(&mut self, data: &[u8]) -> Result<(), String> { - let model = Model::load_with_ops(data, &self.op_registry).map_err(|e| e.to_string())?; - self.detection_model = Some(model); - Ok(()) - } - - /// Load a model for text recognition. - #[wasm_bindgen(js_name = setRecognitionModel)] - pub fn set_recognition_model(&mut self, data: &[u8]) -> Result<(), String> { - let model = Model::load_with_ops(data, &self.op_registry).map_err(|e| e.to_string())?; - self.recognition_model = Some(model); - Ok(()) - } -} - -/// OcrEngine is the main API for performing OCR in WebAssembly. -#[wasm_bindgen] -pub struct OcrEngine { - engine: BaseOcrEngine, -} - -#[wasm_bindgen] -impl OcrEngine { - /// Construct a new `OcrEngine` using the models and other settings given - /// by `init`. - /// - /// To detect text in an image, `init` must have a detection model set. - /// To recognize text, `init` must have a recognition model set. - #[wasm_bindgen(constructor)] - pub fn new(init: OcrEngineInit) -> Result { - let OcrEngineInit { - detection_model, - recognition_model, - op_registry: _op_registry, - } = init; - let engine = BaseOcrEngine::new(OcrEngineParams { - detection_model, - recognition_model, - ..Default::default() - }) - .map_err(|e| e.to_string())?; - Ok(OcrEngine { engine }) - } - - /// Prepare an image for analysis by the OCR engine. - /// - /// The image is an array of pixels in row-major, channels last order. This - /// matches the format of the - /// [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) - /// API. Supported channel combinations are RGB and RGBA. The number of - /// channels is inferred from the length of `data`. - #[wasm_bindgen(js_name = loadImage)] - pub fn load_image(&self, width: usize, height: usize, data: &[u8]) -> Result { - let pixels_per_chan = height * width; - let channels = data.len() / pixels_per_chan; - - if ![1, 3, 4].contains(&channels) { - return Err("expected channel count to be 1, 3 or 4".to_string()); - } - - let tensor = NdTensorView::from_slice(data, [height, width, channels], None) - .map_err(|_| "incorrect data length for image size and channel count".to_string())? - .permuted([2, 0, 1]) // HWC => CHW - .map(|x| (*x as f32) / 255.); - self.engine - .prepare_input(tensor.view()) - .map(|input| Image { input }) - .map_err(|e| e.to_string()) - } - - /// Detect text in an image. - /// - /// Returns a list of lines that were found. These can be passed to - /// `recognizeText` identify the characters. - #[wasm_bindgen(js_name = detectText)] - pub fn detect_text(&self, image: &Image) -> Result, String> { - let words = self - .engine - .detect_words(&image.input) - .map_err(|e| e.to_string())?; - Ok(self - .engine - .find_text_lines(&image.input, &words) - .into_iter() - .map(|words| { - DetectedLine::new( - words - .into_iter() - .map(|word| RotatedRect { rect: word }) - .collect(), - ) - }) - .collect()) - } - - /// Recognize text that was previously detected with `detectText`. - /// - /// Returns a list of `TextLine` objects that can be used to query the text - /// and bounding boxes of each line. - #[wasm_bindgen(js_name = recognizeText)] - pub fn recognize_text( - &self, - image: &Image, - lines: Vec, - ) -> Result, String> { - let lines: Vec> = lines - .iter() - .map(|line| { - let words: Vec = - line.words.iter().map(|word| word.rect).collect(); - words - }) - .collect(); - - let text_lines = self - .engine - .recognize_text(&image.input, &lines) - .map_err(|e| e.to_string())? - .into_iter() - .map(|line| { - line.map(|line| TextLine { line: Some(line) }) - .unwrap_or(TextLine { line: None }) - }) - .collect(); - Ok(text_lines) - } - - /// Detect and recognize text in an image. - /// - /// Returns a single string containing all the text found in reading order. - #[wasm_bindgen(js_name = getText)] - pub fn get_text(&self, image: &Image) -> Result { - self.engine - .get_text(&image.input) - .map_err(|e| e.to_string()) - } - - /// Detect and recognize text in an image. - /// - /// Returns a list of `TextLine` objects that can be used to query the text - /// and bounding boxes of each line. - #[wasm_bindgen(js_name = getTextLines)] - pub fn get_text_lines(&self, image: &Image) -> Result, String> { - let words = self - .engine - .detect_words(&image.input) - .map_err(|e| e.to_string())?; - let lines = self.engine.find_text_lines(&image.input, &words); - let text_lines = self - .engine - .recognize_text(&image.input, &lines) - .map_err(|e| e.to_string())? - .into_iter() - .map(|line| { - line.map(|line| TextLine { line: Some(line) }) - .unwrap_or(TextLine { line: None }) - }) - .collect(); - Ok(text_lines) - } -} - -/// A pre-processed image that can be passed as input to `OcrEngine.loadImage`. -#[wasm_bindgen] -pub struct Image { - input: OcrInput, -} - -#[wasm_bindgen] -impl Image { - /// Return the number of channels in the image. - pub fn channels(&self) -> usize { - self.input.image.size(0) - } - - /// Return the width of the image. - pub fn width(&self) -> usize { - self.input.image.size(2) - } - - /// Return the height of the image. - pub fn height(&self) -> usize { - self.input.image.size(1) - } - - /// Return the image data in row-major, channels-last order. - pub fn data(&self) -> Vec { - // Permute CHW => HWC, convert pixel values from [-0.5, 0.5] back to - // [0, 255]. - self.input - .image - .permuted([1, 2, 0]) - .iter() - .map(|x| ((x + 0.5) * 255.) as u8) - .collect() - } -} - -#[wasm_bindgen] -#[derive(Clone)] -pub struct RotatedRect { - rect: rten_imageproc::RotatedRect, -} - -#[wasm_bindgen] -impl RotatedRect { - /// Return an array of the X and Y coordinates of corners of this rectangle, - /// arranged as `[x0, y0, ... x3, y3]`. - pub fn corners(&self) -> Vec { - self.rect - .corners() - .into_iter() - .flat_map(|c| [c.x, c.y]) - .collect() - } - - /// Return the coordinates of the axis-aligned bounding rectangle of this - /// rotated rect. - /// - /// The result is a `[left, top, right, bottom]` array of coordinates. - #[wasm_bindgen(js_name = boundingRect)] - pub fn bounding_rect(&self) -> Vec { - let br = self.rect.bounding_rect(); - [br.left(), br.top(), br.right(), br.bottom()].into() - } -} - -/// A line of text that has been detected, but not recognized. -/// -/// This contains information about the location of the text, but not the -/// string contents. -#[wasm_bindgen] -#[derive(Clone)] -pub struct DetectedLine { - words: Vec, -} - -#[wasm_bindgen] -impl DetectedLine { - fn new(words: Vec) -> DetectedLine { - DetectedLine { words } - } - - #[wasm_bindgen(js_name = rotatedRect)] - pub fn rotated_rect(&self) -> RotatedRect { - let points: Vec = self - .words - .iter() - .flat_map(|word| word.rect.corners().into_iter()) - .collect(); - let rect = min_area_rect(&points).expect("expected non-empty rect"); - RotatedRect { rect } - } - - pub fn words(&self) -> Vec { - self.words.clone() - } -} - -/// Bounding box and text of a word that was recognized. -#[wasm_bindgen] -#[derive(Clone)] -pub struct TextWord { - rect: RotatedRect, - text: String, -} - -#[wasm_bindgen] -impl TextWord { - pub fn text(&self) -> String { - self.text.clone() - } - - /// Return the oriented bounding rectangle containing the characters in - /// this word. - #[wasm_bindgen(js_name = rotatedRect)] - pub fn rotated_rect(&self) -> RotatedRect { - self.rect.clone() - } -} - -/// A sequence of `TextWord`s that were recognized, forming a line. -#[wasm_bindgen] -#[derive(Clone)] -pub struct TextLine { - line: Option, -} - -#[wasm_bindgen] -impl TextLine { - pub fn text(&self) -> String { - self.line - .as_ref() - .map(|l| l.to_string()) - .unwrap_or_default() - } - - pub fn words(&self) -> Vec { - self.line - .as_ref() - .map(|l| { - l.words() - .map(|w| TextWord { - text: w.to_string(), - rect: RotatedRect { - rect: w.rotated_rect(), - }, - }) - .collect() - }) - .unwrap_or_default() - } -}