From a6fe340a2d441f56fe31c794175a1b0431446537 Mon Sep 17 00:00:00 2001 From: bigfoodK <38313680+bigfoodK@users.noreply.github.com> Date: Mon, 3 Jul 2023 20:14:44 +0900 Subject: [PATCH] Render cg with layer blending (#566) Co-authored-by: namse --- .../client/src/components/cg_render.rs | 9 +- luda-editor/psd/.circleci/config.yml | 158 ++++ luda-editor/psd/.gitignore | 5 + luda-editor/psd/CHANGELOG.md | 28 + luda-editor/psd/Cargo.toml | 24 + luda-editor/psd/LICENSE-APACHE | 201 +++++ luda-editor/psd/LICENSE-MIT | 25 + luda-editor/psd/README.md | 95 +++ luda-editor/psd/book/.gitignore | 1 + luda-editor/psd/book/book.toml | 5 + luda-editor/psd/book/src/SUMMARY.md | 13 + .../psd/book/src/contributing/README.md | 3 + .../contributing/internal-design/README.md | 16 + .../internal-design/major-sections/README.md | 16 + .../src/controlling-exported-layers/README.nd | 20 + luda-editor/psd/book/src/introduction.md | 15 + .../psd/book/src/layer-groups/README.md | 9 + luda-editor/psd/book/src/user-guide/README.md | 8 + luda-editor/psd/examples/README.md | 8 + .../psd/examples/drag-drop-browser/Cargo.toml | 38 + .../psd/examples/drag-drop-browser/README.md | 18 + .../examples/drag-drop-browser/build-dev.sh | 11 + .../drag-drop-browser/build-release.sh | 11 + .../drag-drop-browser/demo-screenshot.png | Bin 0 -> 63659 bytes .../psd/examples/drag-drop-browser/demo.psd | Bin 0 -> 251751 bytes .../psd/examples/drag-drop-browser/index.html | 22 + .../psd/examples/drag-drop-browser/src/lib.rs | 410 ++++++++++ luda-editor/psd/src/blend.rs | 428 ++++++++++ luda-editor/psd/src/lib.rs | 343 ++++++++ luda-editor/psd/src/psd_channel.rs | 386 +++++++++ luda-editor/psd/src/render.rs | 108 +++ .../psd/src/sections/file_header_section.rs | 364 +++++++++ .../psd/src/sections/image_data_section.rs | 226 +++++ .../src/sections/image_resources_section.rs | 773 ++++++++++++++++++ .../image_resources_section/image_resource.rs | 26 + .../groups.rs | 38 + .../layer.rs | 474 +++++++++++ .../layers.rs | 54 ++ .../layer_and_mask_information_section/mod.rs | 486 +++++++++++ luda-editor/psd/src/sections/mod.rs | 326 ++++++++ luda-editor/psd/tests/README.md | 3 + luda-editor/psd/tests/blend.rs | 224 +++++ luda-editor/psd/tests/channels.rs | 62 ++ luda-editor/psd/tests/compression.rs | 74 ++ luda-editor/psd/tests/file_header_section.rs | 37 + .../fixtures/16x16-rle-partially-opaque.psd | Bin 0 -> 23160 bytes .../psd/tests/fixtures/3x3-opaque-center.psd | Bin 0 -> 22678 bytes luda-editor/psd/tests/fixtures/README.md | 87 ++ .../blending/blue-red-1x1-color-burn.psd | Bin 0 -> 23050 bytes .../blending/blue-red-1x1-color-dodge.psd | Bin 0 -> 23050 bytes .../fixtures/blending/blue-red-1x1-darken.psd | Bin 0 -> 23052 bytes .../blending/blue-red-1x1-difference.psd | Bin 0 -> 23052 bytes .../fixtures/blending/blue-red-1x1-divide.psd | Bin 0 -> 23514 bytes .../blending/blue-red-1x1-exclusion.psd | Bin 0 -> 23052 bytes .../blending/blue-red-1x1-hard-light.psd | Bin 0 -> 23052 bytes .../blending/blue-red-1x1-lighten.psd | Bin 0 -> 23052 bytes .../blending/blue-red-1x1-linear-burn.psd | Bin 0 -> 23052 bytes .../blending/blue-red-1x1-linear-dodge.psd | Bin 0 -> 23052 bytes .../blending/blue-red-1x1-multiply.psd | Bin 0 -> 23052 bytes .../fixtures/blending/blue-red-1x1-normal.psd | Bin 0 -> 23052 bytes .../blending/blue-red-1x1-overlay.psd | Bin 0 -> 23028 bytes .../fixtures/blending/blue-red-1x1-screen.psd | Bin 0 -> 23052 bytes .../blending/blue-red-1x1-soft-light.psd | Bin 0 -> 23052 bytes .../blending/blue-red-1x1-subtract.psd | Bin 0 -> 23506 bytes .../psd/tests/fixtures/fifteen-letters.psd | Bin 0 -> 20957 bytes luda-editor/psd/tests/fixtures/green-1x1.png | Bin 0 -> 275 bytes luda-editor/psd/tests/fixtures/green-1x1.psd | Bin 0 -> 22483 bytes .../fixtures/green-chinese-layer-name-1x1.psd | Bin 0 -> 22527 bytes .../tests/fixtures/green-clipping-10x10.psd | Bin 0 -> 24110 bytes .../green-cyrillic-layer-name-1x1.psd | Bin 0 -> 22531 bytes .../green-1x1-one-group-inside-another.psd | Bin 0 -> 24045 bytes ...one-group-one-layer-inside-one-outside.psd | Bin 0 -> 23635 bytes .../green-1x1-one-group-one-layer-inside.psd | Bin 0 -> 23269 bytes ...green-1x1-one-group-with-two-subgroups.psd | Bin 0 -> 28973 bytes ...green-1x1-two-groups-two-layers-inside.psd | Bin 0 -> 24377 bytes .../groups/rle-compressed-empty-channel.psd | Bin 0 -> 1176 bytes luda-editor/psd/tests/fixtures/luni.psd | Bin 0 -> 1300 bytes .../fixtures/negative-top-left-layer.psd | Bin 0 -> 22595 bytes .../tests/fixtures/non-utf8-pascal-string.psd | Bin 0 -> 282 bytes .../fixtures/odd-length-pascal-string.psd | Bin 0 -> 1528 bytes .../psd/tests/fixtures/one-channel-1x1.psd | Bin 0 -> 23892 bytes .../psd/tests/fixtures/rle-3-layer-8x8.psd | Bin 0 -> 23740 bytes .../psd/tests/fixtures/slices-resource/1.psd | Bin 0 -> 20929 bytes .../psd/tests/fixtures/slices-resource/12.psd | Bin 0 -> 20931 bytes .../tests/fixtures/slices-resource/123.psd | Bin 0 -> 20933 bytes .../tests/fixtures/slices-resource/1234.psd | Bin 0 -> 20935 bytes .../tests/fixtures/slices-resource/README.md | 3 + luda-editor/psd/tests/fixtures/slices-v8.psd | Bin 0 -> 21869 bytes .../fixtures/transparent-above-opaque.psd | Bin 0 -> 21963 bytes .../fixtures/transparent-top-layer-2x1.psd | Bin 0 -> 22370 bytes .../psd/tests/fixtures/two-channel-8x8.psd | Bin 0 -> 24482 bytes .../fixtures/two-layers-red-green-1x1.psd | Bin 0 -> 22385 bytes luda-editor/psd/tests/flatten_layers.rs | 40 + luda-editor/psd/tests/image_data_section.rs | 13 + .../psd/tests/image_resources_section.rs | 85 ++ .../layer_and_mask_information_section.rs | 251 ++++++ luda-editor/psd/tests/layer_groups.rs | 141 ++++ luda-editor/psd/tests/slices_resource.rs | 74 ++ luda-editor/psd/tests/transparency.rs | 138 ++++ luda-editor/server/server-bin/Cargo.lock | 2 - luda-editor/server/server-core/Cargo.lock | 2 - luda-editor/server/server-core/Cargo.toml | 2 +- .../server-core/src/services/cg/layer_tree.rs | 499 +++++++++++ .../server/server-core/src/services/cg/mod.rs | 1 + .../cg/parse_psd_to_inter_cg_parts.rs | 164 ++-- .../src/services/cg/psd_to_cg_file.rs | 76 +- 106 files changed, 7046 insertions(+), 133 deletions(-) create mode 100644 luda-editor/psd/.circleci/config.yml create mode 100644 luda-editor/psd/.gitignore create mode 100644 luda-editor/psd/CHANGELOG.md create mode 100644 luda-editor/psd/Cargo.toml create mode 100644 luda-editor/psd/LICENSE-APACHE create mode 100644 luda-editor/psd/LICENSE-MIT create mode 100644 luda-editor/psd/README.md create mode 100644 luda-editor/psd/book/.gitignore create mode 100644 luda-editor/psd/book/book.toml create mode 100644 luda-editor/psd/book/src/SUMMARY.md create mode 100644 luda-editor/psd/book/src/contributing/README.md create mode 100644 luda-editor/psd/book/src/contributing/internal-design/README.md create mode 100644 luda-editor/psd/book/src/contributing/internal-design/major-sections/README.md create mode 100644 luda-editor/psd/book/src/controlling-exported-layers/README.nd create mode 100644 luda-editor/psd/book/src/introduction.md create mode 100644 luda-editor/psd/book/src/layer-groups/README.md create mode 100644 luda-editor/psd/book/src/user-guide/README.md create mode 100644 luda-editor/psd/examples/README.md create mode 100644 luda-editor/psd/examples/drag-drop-browser/Cargo.toml create mode 100644 luda-editor/psd/examples/drag-drop-browser/README.md create mode 100755 luda-editor/psd/examples/drag-drop-browser/build-dev.sh create mode 100755 luda-editor/psd/examples/drag-drop-browser/build-release.sh create mode 100644 luda-editor/psd/examples/drag-drop-browser/demo-screenshot.png create mode 100644 luda-editor/psd/examples/drag-drop-browser/demo.psd create mode 100644 luda-editor/psd/examples/drag-drop-browser/index.html create mode 100644 luda-editor/psd/examples/drag-drop-browser/src/lib.rs create mode 100644 luda-editor/psd/src/blend.rs create mode 100644 luda-editor/psd/src/lib.rs create mode 100644 luda-editor/psd/src/psd_channel.rs create mode 100644 luda-editor/psd/src/render.rs create mode 100644 luda-editor/psd/src/sections/file_header_section.rs create mode 100644 luda-editor/psd/src/sections/image_data_section.rs create mode 100644 luda-editor/psd/src/sections/image_resources_section.rs create mode 100644 luda-editor/psd/src/sections/image_resources_section/image_resource.rs create mode 100644 luda-editor/psd/src/sections/layer_and_mask_information_section/groups.rs create mode 100644 luda-editor/psd/src/sections/layer_and_mask_information_section/layer.rs create mode 100644 luda-editor/psd/src/sections/layer_and_mask_information_section/layers.rs create mode 100644 luda-editor/psd/src/sections/layer_and_mask_information_section/mod.rs create mode 100644 luda-editor/psd/src/sections/mod.rs create mode 100644 luda-editor/psd/tests/README.md create mode 100644 luda-editor/psd/tests/blend.rs create mode 100644 luda-editor/psd/tests/channels.rs create mode 100644 luda-editor/psd/tests/compression.rs create mode 100644 luda-editor/psd/tests/file_header_section.rs create mode 100644 luda-editor/psd/tests/fixtures/16x16-rle-partially-opaque.psd create mode 100644 luda-editor/psd/tests/fixtures/3x3-opaque-center.psd create mode 100644 luda-editor/psd/tests/fixtures/README.md create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-color-burn.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-color-dodge.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-darken.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-difference.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-divide.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-exclusion.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-hard-light.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-lighten.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-linear-burn.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-linear-dodge.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-multiply.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-normal.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-overlay.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-screen.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-soft-light.psd create mode 100644 luda-editor/psd/tests/fixtures/blending/blue-red-1x1-subtract.psd create mode 100644 luda-editor/psd/tests/fixtures/fifteen-letters.psd create mode 100644 luda-editor/psd/tests/fixtures/green-1x1.png create mode 100644 luda-editor/psd/tests/fixtures/green-1x1.psd create mode 100644 luda-editor/psd/tests/fixtures/green-chinese-layer-name-1x1.psd create mode 100644 luda-editor/psd/tests/fixtures/green-clipping-10x10.psd create mode 100644 luda-editor/psd/tests/fixtures/green-cyrillic-layer-name-1x1.psd create mode 100644 luda-editor/psd/tests/fixtures/groups/green-1x1-one-group-inside-another.psd create mode 100644 luda-editor/psd/tests/fixtures/groups/green-1x1-one-group-one-layer-inside-one-outside.psd create mode 100644 luda-editor/psd/tests/fixtures/groups/green-1x1-one-group-one-layer-inside.psd create mode 100644 luda-editor/psd/tests/fixtures/groups/green-1x1-one-group-with-two-subgroups.psd create mode 100644 luda-editor/psd/tests/fixtures/groups/green-1x1-two-groups-two-layers-inside.psd create mode 100644 luda-editor/psd/tests/fixtures/groups/rle-compressed-empty-channel.psd create mode 100644 luda-editor/psd/tests/fixtures/luni.psd create mode 100644 luda-editor/psd/tests/fixtures/negative-top-left-layer.psd create mode 100644 luda-editor/psd/tests/fixtures/non-utf8-pascal-string.psd create mode 100644 luda-editor/psd/tests/fixtures/odd-length-pascal-string.psd create mode 100644 luda-editor/psd/tests/fixtures/one-channel-1x1.psd create mode 100644 luda-editor/psd/tests/fixtures/rle-3-layer-8x8.psd create mode 100644 luda-editor/psd/tests/fixtures/slices-resource/1.psd create mode 100644 luda-editor/psd/tests/fixtures/slices-resource/12.psd create mode 100644 luda-editor/psd/tests/fixtures/slices-resource/123.psd create mode 100644 luda-editor/psd/tests/fixtures/slices-resource/1234.psd create mode 100644 luda-editor/psd/tests/fixtures/slices-resource/README.md create mode 100644 luda-editor/psd/tests/fixtures/slices-v8.psd create mode 100644 luda-editor/psd/tests/fixtures/transparent-above-opaque.psd create mode 100644 luda-editor/psd/tests/fixtures/transparent-top-layer-2x1.psd create mode 100644 luda-editor/psd/tests/fixtures/two-channel-8x8.psd create mode 100644 luda-editor/psd/tests/fixtures/two-layers-red-green-1x1.psd create mode 100644 luda-editor/psd/tests/flatten_layers.rs create mode 100644 luda-editor/psd/tests/image_data_section.rs create mode 100644 luda-editor/psd/tests/image_resources_section.rs create mode 100644 luda-editor/psd/tests/layer_and_mask_information_section.rs create mode 100644 luda-editor/psd/tests/layer_groups.rs create mode 100644 luda-editor/psd/tests/slices_resource.rs create mode 100644 luda-editor/psd/tests/transparency.rs create mode 100644 luda-editor/server/server-core/src/services/cg/layer_tree.rs diff --git a/luda-editor/client/src/components/cg_render.rs b/luda-editor/client/src/components/cg_render.rs index 71f7a3ed8..7e40f0465 100644 --- a/luda-editor/client/src/components/cg_render.rs +++ b/luda-editor/client/src/components/cg_render.rs @@ -9,10 +9,13 @@ pub struct CgRenderProps { } pub fn render_cg(props: CgRenderProps, screen_cg: &ScreenCg, cg_file: &CgFile) -> RenderingTree { - render(screen_cg.parts.iter().map(|part| { + render(screen_cg.parts.iter().rev().map(|screen_part| { try_render(|| { - let cg_part = cg_file.parts.iter().find(|part| part.name == part.name)?; - Some(render_cg_part(&props, part, cg_part)) + let cg_part = cg_file + .parts + .iter() + .find(|part| part.name == screen_part.name())?; + Some(render_cg_part(&props, screen_part, cg_part)) }) })) } diff --git a/luda-editor/psd/.circleci/config.yml b/luda-editor/psd/.circleci/config.yml new file mode 100644 index 000000000..718f0ccbb --- /dev/null +++ b/luda-editor/psd/.circleci/config.yml @@ -0,0 +1,158 @@ +version: 2 + +jobs: + + test: + docker: + - image: rust:latest + steps: + - checkout + + - restore_cache: + keys: + - v3-cargo-cache-test-{{ arch }}-{{ .Branch }} + - v3-cargo-cache-test-{{ arch }} + + # Install nightly & wasm + - run: + name: Install Rust nightly + command: rustup update nightly && rustup default nightly + - run: + name: Add wasm32 target + command: rustup target add wasm32-unknown-unknown + + # Install wasm tools + - run: + name: Install wasm-pack + command: > + curl -L https://github.com/rustwasm/wasm-pack/releases/download/v0.9.1/wasm-pack-v0.9.1-x86_64-unknown-linux-musl.tar.gz + | tar --strip-components=1 --wildcards -xzf - "*/wasm-pack" + && chmod +x wasm-pack + && mv wasm-pack $CARGO_HOME/bin/ + + # Show versions + - run: + name: Show versions + command: rustc --version && cargo --version && wasm-pack --version + + # Run tests + - run: + name: Run all tests + command: cargo test --all + + # Save cache + - save_cache: + key: v3-cargo-cache-test-{{ arch }}-{{ .Branch }} + paths: + - target + - /usr/local/cargo + - save_cache: + key: v3-cargo-cache-test-{{ arch }} + paths: + - target + - /usr/local/cargo + + site-build: + docker: + - image: rust:latest + steps: + - checkout + - restore_cache: + keys: + - v3-cargo-cache-site-{{ arch }}-{{ .Branch }} + - v3-cargo-cache-site-{{ arch }} + + # Install nightly + - run: + name: Install Rust nightly + command: rustup update nightly && rustup default nightly + + # Show versions + - run: + name: Show versions + command: rustc --version && cargo --version + + # Install wasm tools + - run: + name: Install wasm-pack + command: > + curl -L https://github.com/rustwasm/wasm-pack/releases/download/v0.9.1/wasm-pack-v0.9.1-x86_64-unknown-linux-musl.tar.gz + | tar --strip-components=1 --wildcards -xzf - "*/wasm-pack" + && chmod +x wasm-pack + && mv wasm-pack $CARGO_HOME/bin/ + + # Install mdbook + - run: + name: Install mdbook + command: > + (test -x $CARGO_HOME/bin/cargo-install-update || cargo install cargo-update) + && (test -x $CARGO_HOME/bin/mdbook || cargo install --vers "^0.4" mdbook) + && mv ~/.gitconfig ~/.gitconfig.disabled # Workaround for https://github.com/nabijaczleweli/cargo-update/issues/100 + && cargo install-update -a + && mv ~/.gitconfig.disabled ~/.gitconfig + + # Build Site + - run: + name: Build Site + command: > + (cd book && mdbook build) + && cargo doc --no-deps -p psd + && cp -R target/doc book/book/api + && (cd examples/drag-drop-browser && ./build-release.sh) + && cp -R examples/drag-drop-browser/public book/book/drag-drop-demo + && rm book/book/drag-drop-demo/.gitignore + && sed -i -e 's/drag_drop_browser/psd\/drag-drop-demo\/drag_drop_browser/g' book/book/drag-drop-demo/index.html # Fix script path + && sed -i -e 's/app.css/psd\/drag-drop-demo\/app.css/g' book/book/drag-drop-demo/index.html # Fix script path + + - persist_to_workspace: + root: book + paths: book + + # Save cache + - save_cache: + key: v3-cargo-cache-site-{{ arch }}-{{ .Branch }} + paths: + - target + - /usr/local/cargo + - save_cache: + key: v3-cargo-cache-site-{{ arch }} + paths: + - target + - /usr/local/cargo + + site-deploy: + docker: + - image: node:10 + steps: + - checkout + - attach_workspace: + at: book + - run: + name: Disable jekyll builds + command: touch book/book/.nojekyll + - run: + name: Install and configure dependencies + command: | + npm install -g --silent gh-pages@2.0.1 + git config user.email "ci-build@klukas.net" + git config user.name "ci-build" + + - add_ssh_keys: + fingerprints: + - "74:9d:18:d2:11:b2:15:39:9f:d3:77:84:10:cd:f9:c1" + - run: + name: Deploy site to gh-pages branch + command: gh-pages --dotfiles --message "[skip ci] Updates" --dist book/book + +workflows: + version: 2 + build: + jobs: + - test + - site-build + - site-deploy: + requires: + - site-build + filters: + branches: + only: master diff --git a/luda-editor/psd/.gitignore b/luda-editor/psd/.gitignore new file mode 100644 index 000000000..d3b0f220a --- /dev/null +++ b/luda-editor/psd/.gitignore @@ -0,0 +1,5 @@ +/target +**/*.rs.bk +Cargo.lock +.idea +.DS_Store diff --git a/luda-editor/psd/CHANGELOG.md b/luda-editor/psd/CHANGELOG.md new file mode 100644 index 000000000..954a646f6 --- /dev/null +++ b/luda-editor/psd/CHANGELOG.md @@ -0,0 +1,28 @@ +# psd Changelog + +Types of changes: + +- `[added]` for new features. +- `[changed]` for changes in existing functionality. +- `[deprecated]` for once-stable features removed in upcoming releases. +- `[removed]` for deprecated features removed in this release. +- `[fixed]` for any bug fixes. +- `[security]` to invite users to upgrade in case of vulnerabilities. + +## Not Yet Published + +_Here we list notable things that have been merged into the master branch but have not been released yet._ + +- [added] Public information on the position of each layer (E.g. `layer_top`, `layer_bottom`. `layer_left`, `layer_right`). + +## 0.1.8 - April 23, 2020 + +- [fixed] Parsing of slices resource section [PR][17] + +## 0.1.7 - April 11, 2020 + +- [added] Support for PSD groups [PR][13] + - @tdakkota + +[13]: https://github.com/chinedufn/psd/pull/13 +[17]: https://github.com/chinedufn/psd/pull/17 diff --git a/luda-editor/psd/Cargo.toml b/luda-editor/psd/Cargo.toml new file mode 100644 index 000000000..6c783d6be --- /dev/null +++ b/luda-editor/psd/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "psd" +version = "0.3.4" +authors = ["Chinedu Francis Nwafili "] +description = "A Rust API for parsing and working with PSD files." +keywords = ["psd", "photoshop", "texture", "png", "image"] +license = "MIT/Apache-2.0" +repository = "https://github.com/chinedufn/psd" +edition = "2018" + +[dependencies] +thiserror = "1.0" + +[dev-dependencies] +anyhow = "1.0" + +[workspace] +members = [ + "examples/drag-drop-browser" +] + +[profile.release] +# We can re-enable lto for the demo when wasm-pack 0.2.38 is released. There's a bug in 0.2.37 +# lto = true diff --git a/luda-editor/psd/LICENSE-APACHE b/luda-editor/psd/LICENSE-APACHE new file mode 100644 index 000000000..137ade010 --- /dev/null +++ b/luda-editor/psd/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2019 Chinedu Francis Nwafili + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/luda-editor/psd/LICENSE-MIT b/luda-editor/psd/LICENSE-MIT new file mode 100644 index 000000000..94e7f0670 --- /dev/null +++ b/luda-editor/psd/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2018 Chinedu Francis Nwafili + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/luda-editor/psd/README.md b/luda-editor/psd/README.md new file mode 100644 index 000000000..454b37213 --- /dev/null +++ b/luda-editor/psd/README.md @@ -0,0 +1,95 @@ +psd +=== + +[![Build status](https://circleci.com/gh/chinedufn/psd.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/chinedufn/psd) [![docs](https://docs.rs/psd/badge.svg)](https://docs.rs/psd) + +> A Rust API for parsing and working with PSD files. + +## Live Demo + +The `psd` crate can be compiled to WebAssembly and used in a browser. + +[In the live demo you can visualize a PSD in the browser](https://chinedufn.github.io/psd/drag-drop-demo/), +toggle layers on and off and drag and drop a new PSD into the demo. + +![Demo screenshot](./examples/drag-drop-browser/demo-screenshot.png) + +Check out the [examples/drag-drop-browser](examples/drag-drop-browser) directory for instructions on running the demo locally. + +## The Psd Book + +The _WIP_ [The Psd Book](https://chinedufn.github.io/psd) will contain information about getting started with the `psd` crate, +a description of the architecture and information on how to get started. + +## API Docs + +Check out the [API documentation](https://chinedufn.github.io/psd/api/psd) to see everything that you can currently access. + +## Background / Initial Motivation + +I'm working on a game and part of my asset compilation process was a script that did the following: + +1. Iterate over all PSD files + +2. Export every PSD into a PNG, ignoring any layers that begin with an `_` + +3. Combine PNGs into a texture atlas + +For a couple of years I was using `imagemagick` to power step 2, but after getting a new laptop and upgrading `imagemagick` versions it stopped working. + +After a bit of Googling I couldn't land on a solution for my problem so I decided to make this crate. + +My approach was to support as much of the PSD spec as I needed, so there might be bits of information that you'd like to make use of that aren't currently supported. + +That said, if there's anything missing that you need [please feel very free to open an issue](https://github.com/chinedufn/psd/issues)! + +## Usage + +```rust +use psd::{ColorMode, Psd, PsdChannelCompression}; + +fn main () { + // .. Get a byte slice of PSD file data somehow .. + let psd = include_bytes!("./my-psd-file.psd"); + + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(psd.color_mode(), ColorMode::Rgb); + + // For this PSD the final combined image is RleCompressed + assert_eq!(psd.compression(), &PsdChannelCompression::RleCompressed); + + assert_eq!(psd.width(), 500); + assert_eq!(psd.height(), 500); + + // Get the combined final image for the PSD. + let final_image: Vec = psd.rgba(); + + for layer in psd.layers().iter() { + let name = layer.name(); + + let pixels: Vec = layer.rgba().unwrap(); + } + + let green_layer = psd.layer_by_name("Green Layer").unwrap(); + + // In this layer the red channel is uncompressed + assert_eq!(green_layer.compression(&PsdChannelKind::Red).unwrap(), PsdChannelCompression::RawData); + + // In this layer the green channel is RLE compressed + assert_eq!(green_layer.compression(&PsdChannelKind::Green).unwrap(), PsdChannelCompression::RleCompressed); + + // Combine the PSD layers top to bottom, ignoring any layers that begin with an `_` + let pixels: Vec = psd.flatten_layers_rgba(&|(_idx, layer)| { + !layer.name().starts_with("_") + }).unwrap(); +} +``` + +## See Also + +- [PSD specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) - the basis of our API + +## License + +MIT diff --git a/luda-editor/psd/book/.gitignore b/luda-editor/psd/book/.gitignore new file mode 100644 index 000000000..7585238ef --- /dev/null +++ b/luda-editor/psd/book/.gitignore @@ -0,0 +1 @@ +book diff --git a/luda-editor/psd/book/book.toml b/luda-editor/psd/book/book.toml new file mode 100644 index 000000000..7db1d6e04 --- /dev/null +++ b/luda-editor/psd/book/book.toml @@ -0,0 +1,5 @@ +[book] +authors = ["Chinedu Francis Nwafili"] +multilingual = false +src = "src" +title = "The Psd Book" diff --git a/luda-editor/psd/book/src/SUMMARY.md b/luda-editor/psd/book/src/SUMMARY.md new file mode 100644 index 000000000..bcbb8cc8f --- /dev/null +++ b/luda-editor/psd/book/src/SUMMARY.md @@ -0,0 +1,13 @@ +# Summary + +[Introduction](./introduction.md) + +--- + +- [User Guide](./user-guide/README.md) + - [Only Exporting Certain Layers](./controlling-exported-layers/README.nd) + - [Layer Groups](./layer-groups/README.md) + +- [Contributing](./contributing/README.md) + - [Internal Design](./contributing/internal-design/README.md) + - [Major Sections](./contributing/internal-design/major-sections/README.md) diff --git a/luda-editor/psd/book/src/contributing/README.md b/luda-editor/psd/book/src/contributing/README.md new file mode 100644 index 000000000..0b8d31770 --- /dev/null +++ b/luda-editor/psd/book/src/contributing/README.md @@ -0,0 +1,3 @@ +# Contributing + +This section dives into contributing to the `psd` crate. diff --git a/luda-editor/psd/book/src/contributing/internal-design/README.md b/luda-editor/psd/book/src/contributing/internal-design/README.md new file mode 100644 index 000000000..35b304385 --- /dev/null +++ b/luda-editor/psd/book/src/contributing/internal-design/README.md @@ -0,0 +1,16 @@ +# Internal Design + +This section discusses the internal design of the `psd` crate. + +--- + +Before reading this section it is recommended that you read through +the [psd specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_89817). + +Reading through the spec will bring context to the organization of the codebase and the +approach that we take to parsing `.psd` data + +--- + +After reading this section you should have a good sense of how everything works and be better prepared +to dive into the codebase. diff --git a/luda-editor/psd/book/src/contributing/internal-design/major-sections/README.md b/luda-editor/psd/book/src/contributing/internal-design/major-sections/README.md new file mode 100644 index 000000000..16043f6ee --- /dev/null +++ b/luda-editor/psd/book/src/contributing/internal-design/major-sections/README.md @@ -0,0 +1,16 @@ +# Major Sections + +You can think of a Photoshop file as a byte slice `&[u8]`. + +These bytes are organized into 5 major sections, each of which have their own sub-sections. + +We represent this in our code using the `MajorSections` type. + +```rust +// Imported into the book from `src/sections/mod.rs` + +{{#include ../../../../../src/sections/mod.rs:11:35}} +``` + +Our parsing comes down to reading through the bytes in this byte slice and +using them to create these five major sections. diff --git a/luda-editor/psd/book/src/controlling-exported-layers/README.nd b/luda-editor/psd/book/src/controlling-exported-layers/README.nd new file mode 100644 index 000000000..a5e4a193f --- /dev/null +++ b/luda-editor/psd/book/src/controlling-exported-layers/README.nd @@ -0,0 +1,20 @@ +# Only Exporting Certain Layers + +One use case that the `psd` crate is designed to support is an asset pipeline where you're extracting image data from PSD files in order to convert it into the format that your +application works with (such as PNG). + +In order to support that use case we grant control over exactly which layers in your PSD get exported via the [flatten_layers_rgba][flatten-fn] funcion. + +You provide a filter function and any layers that pass your filter (return true) will get blended from top to bottom into a final image. + +Here's an example of compositing a final image from layers that do not have names that begin with an underscore. + +```rust,no_run,ignore +let psd = include_bytes!("./my-psd-file.psd"); + +let pixels: Vec = psd.flatten_layers_rgba(&|(_idx, layer) { + !layer.name().started_with("_") +}).unwrap(); +``` + +[flatten-fn]: https://chinedufn.github.io/psd/api/psd/struct.Psd.html#method.flatten_layers_rgba diff --git a/luda-editor/psd/book/src/introduction.md b/luda-editor/psd/book/src/introduction.md new file mode 100644 index 000000000..be854878a --- /dev/null +++ b/luda-editor/psd/book/src/introduction.md @@ -0,0 +1,15 @@ +# Introduction + +`psd` provides a Rust API for parsing and working with the [Adobe Photoshop File Format](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_89817). + +`psd` seeks to make it easy for you to write scripts that work with Photoshop files. + +--- + +The Photoshop specification is large so, while we support the main parts of it, not every little bit +is supported. + +If there's something that you'd like to get your hands on please feel free to [open an issue]. + +[texture_packer]: https://github.com/PistonDevelopers/texture_packer +[open an issue]: https://github.com/chinedufn/psd/issues diff --git a/luda-editor/psd/book/src/layer-groups/README.md b/luda-editor/psd/book/src/layer-groups/README.md new file mode 100644 index 000000000..1ea029197 --- /dev/null +++ b/luda-editor/psd/book/src/layer-groups/README.md @@ -0,0 +1,9 @@ +# Layer Groups + +`psd` has support for parsing and getting information about layer groups. + +Here are some examples: + +```rust +{{#include ../../../tests/layer_groups.rs}} +``` diff --git a/luda-editor/psd/book/src/user-guide/README.md b/luda-editor/psd/book/src/user-guide/README.md new file mode 100644 index 000000000..e51797e0c --- /dev/null +++ b/luda-editor/psd/book/src/user-guide/README.md @@ -0,0 +1,8 @@ +# User Guide + +This chapter covers the basics on how to work with some of the more common aspects of PSD files. + +Use this chapter to get a higher level introduction to the features of the `psd` crate. + +After that you can explore the [full API documentation](https://docs.rs/psd) to see all of the +types and functions that are available. diff --git a/luda-editor/psd/examples/README.md b/luda-editor/psd/examples/README.md new file mode 100644 index 000000000..c9ee08b71 --- /dev/null +++ b/luda-editor/psd/examples/README.md @@ -0,0 +1,8 @@ +# Examples + +Examples of using `psd` + +### [Drag and Drop Browser Demo](./drag-drop-browser) + +This demo showcases visualizing a demo PSD file in the browser, toggling the layers on and off and +being able to drag and drop a new PSD to visualize. diff --git a/luda-editor/psd/examples/drag-drop-browser/Cargo.toml b/luda-editor/psd/examples/drag-drop-browser/Cargo.toml new file mode 100644 index 000000000..ead844e2e --- /dev/null +++ b/luda-editor/psd/examples/drag-drop-browser/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "drag-drop-browser" +version = "0.1.0" +authors = ["Chinedu Francis Nwafili "] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +console_error_panic_hook = "0.1.6" +css-rs-macro = "0.1.0" +js-sys = "0.3.40" +psd = {path = "../../"} +percy-dom = "0.7" +wasm-bindgen = "0.2.63" + +[dependencies.web-sys] +version = "0.3" +features = [ + "Blob", + "CanvasRenderingContext2d", + "CssStyleDeclaration", + "DataTransfer", + "Document", + "DragEvent", + "Element", + "Event", + "File", + "FileList", + "FileReader", + "HtmlCanvasElement", + "HtmlInputElement", + "ImageData", + "MouseEvent", + "Window", + "console" +] diff --git a/luda-editor/psd/examples/drag-drop-browser/README.md b/luda-editor/psd/examples/drag-drop-browser/README.md new file mode 100644 index 000000000..1ee45d6ad --- /dev/null +++ b/luda-editor/psd/examples/drag-drop-browser/README.md @@ -0,0 +1,18 @@ +# Drag an Drop PSD Demo + +The demo can be [viewed live](https://chinedufn.github.io/psd/drag-drop-demo/). + +To run it locally + +``` +git clone git@github.com:chinedufn/psd.git +cd examples/drag-drop-browser + +# ./build-dev.sh +./build-release.sh + +npm install -g http-server # Or any other wasm compatible server +http-server -o -c -p 12000 public +``` + +![Demo screenshot](./demo-screenshot.png) diff --git a/luda-editor/psd/examples/drag-drop-browser/build-dev.sh b/luda-editor/psd/examples/drag-drop-browser/build-dev.sh new file mode 100755 index 000000000..703369f1d --- /dev/null +++ b/luda-editor/psd/examples/drag-drop-browser/build-dev.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +cd "$(dirname "$0")" + +mkdir -p public + +CSS_FILE="$(pwd)/public/app.css" +OUTPUT_CSS=$CSS_FILE wasm-pack build --no-typescript --dev --target web --out-dir ./public +cp index.html public/ diff --git a/luda-editor/psd/examples/drag-drop-browser/build-release.sh b/luda-editor/psd/examples/drag-drop-browser/build-release.sh new file mode 100755 index 000000000..1acdf5454 --- /dev/null +++ b/luda-editor/psd/examples/drag-drop-browser/build-release.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +cd "$(dirname "$0")" + +mkdir -p public + +CSS_FILE="$(pwd)/public/app.css" +OUTPUT_CSS=$CSS_FILE wasm-pack build --no-typescript --release --target web --out-dir ./public +cp index.html public/ diff --git a/luda-editor/psd/examples/drag-drop-browser/demo-screenshot.png b/luda-editor/psd/examples/drag-drop-browser/demo-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..24de8797115d218b2063a094636e16431f9a4627 GIT binary patch literal 63659 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 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/examples/drag-drop-browser/demo.psd b/luda-editor/psd/examples/drag-drop-browser/demo.psd new file mode 100644 index 0000000000000000000000000000000000000000..8a3bb377652538665854cdd3a2cbffe8aa619ea8 GIT binary patch literal 251751 zcmeEv1z=RixA)xLjeCM?2(AedBsjr>yHgq?1Y$&RDTP9dTe0F^v_Nrdafjeq0_Tr+xzoLmYp6LKrRgb|*;F zzb}ur>=e~oAmB1?m3V~UpKxV0xibGe)hJEqujRIFpPq2=YTT1ETwM)X*Rolg_EXZL zV<*IC2FJuFB&M_tQJlRP5}X*Vq3+9v~TCx zW=h1A>TZD#%hlVw87}hu9=}#I(%B)RbUKH##OYE3<7#2yLkNuTZY!wBm+RG8%C*Hi}J64xJL6 z7TUN`Sm>W2#l;rmq-CWiajeC~hQ=qwC&#B`W&p48pW;EKy?Ym<{%Lc`$$yHOk=bPu zBq-F#pJHYVnwl0LIv_qHH7h+fzRM(VQvJ`EPaBt-nVK;!^l}y9+JOCt z$n^N=%+&Njsi{fr9R1#}h+c4HB<<&x!F75^$0nxGM(VZ+Es6B84?fb1?;M>O-wwE~ z8iqA#*tpf8CXFK+H;)Kw(J(ALA}s6^A`ZwBG4GNNg-PpSM4 zwj`a@xWutj{}Pptke|?qi;WnYnw}h;*)BRQEh#ZJni^o}q?EWqvnp0?o6wJ-pWt)m z5fPP=kr|y58z0rV9SRyHCdNfHX&M{XxM_=+hGB8>jT<(Lj*V*=-88;s!xqiro5i$< z4UcXf8^bB7ckfTp|3JEPYHSv@&_dF2Ala+-7m@x1{ST!3rYFL?L??X$L%ILYHt{E9{?JtS#0*&A)K4rE_R0B=f$HwwZ3!o=!(DmQ(Yw#^7*V&U7y!aiCU++N+{&>S*N-_ub&dNPIZ+~$mg?8b$wnxC2F1ODxr|i zXPxT$ynag5I@MJ|A)n7W)%AJ(l&E#8tAs*6pLMG1^ZF@K>r__>g?v8iRM+SAQ=-MEg-&u5+L`n-Nh)H>BwLLr~eI@R@g{gkM6s;h)TKA&}}>+||4QR`G!359$< z`&o5)e!M_1J_T0=PR8Yb-1UPI+_i-weEQ?Na9H*$s%c#6!A6e_}@B{HB} zr$}(aT`W2vE;u+YH7lk=Y7XU)`|FmT@`ticNq>avlpZ%|a7t#^>I0H!-xmLMN{S2q zv+@BMNts-EzbQ!_Vv4~%C&kC&@+@3MhpR3Nt)gE-zlZ^}056+hNNrbM%Y zQq%sl*@3Z1j`Du#u^om}3f!N6YBD?Z0wd zy~|f~WuAnv++X1}y*^wiz1nvKhhM%pUxjN+!{t4=Pkb<(?bfCi3;TE2JT2y6bzEt8 z5;}G)YLf8j%9SvMCEM0-C@yJ2vB(H~7QuvbC3HL#mT)wU$P+r=99)oo0)!%;wS@T}SHUXW@{f1%+2}f2`wDp8yos6f#8fDOpL>CsM&&am{i{ zTt=aKhsZ-WdtMpORdolQIWIC;Se|H#R;gX;A!> z%&3g+gL?NWa(eDX#lHjg7?+wptz%MRLeZS$=McVoQ86TUi;Eu{oy9p}k4f?AnSTO4 zq^S7!;9fBa+!c49C^x`Czf-s33ZRfasVP*~9+|0Wn9MWcKlO5+NpOdMDD{rv+~*(4 zz0mFBN`iBvtqyGnvo*}Qy^>2iW?4U)_S;C`^fGB4%s&}39Hz10VZ%oRyM4gODh<3^ zBse-YBdue^U4v52j0$}_703Q>V5u1$k&lp|v zV$;$w1-iCGdG*PeX*4|w=~C%n5a~3UPH?2>(R7R>y^ez!GywCO>j6UCy`!VkG1>T@ zLV56{*aXn=eU5ZkN?c+J%bc5vNRGJ1HsT1=ForF))(a|x*G^3*v ziqj5RfQg0+ubSjC|D7MnNm%{{S=|Ra;CYo|Ga~6+QwHT>anYT7B3%jTt|>`0JjD!lx&5(GBH||LwTMD2hwd8#2cYqJ2#19EgqP_%Da_srZzRg*xGX5uY)P@&wA+DfZX8Isn z4n75a(lYx|9YQWa%=qXY)b7ELU<&C)B1vZwj5s=~UBPZIA*PfC$<)O01$h`AFG zYd0(`DZ&`a)uiBaAelyFBn2=T#dw{#+ITVv<+kEd2W~^e@098!4r2nzMLL=EBuU6; zAsr8#qSl5GAxaX9^F^)4g4ZOxY26>oIzp;mXq7@`710g$D}r?G;@a7@yK7h1x`GOV zc7oP|Uij>QcN0NNL8~HeKf=4@x@5b&cX{HHYvQQc-eUaurm8w_<@=@!Ba2{bEJdd&- z9X#rk#PqG_8(z#I)u}@t4y`$OjVab^5eI*4|6@%ei36WK3wbSu4*T)mkGl|78Me;_YgtkfTAR}ylSbmt*K_wG2 zE$o&=rlw8h=APh=@ci+?Q7N&FxckhsB81K(6owE@8E$@H%G~-`_VpV=+73rVdhTOc zFO1Wl^AJEwKbF%}Sr-peLYTE{L=JFrvznDx^B819_T|=Am>q>0~mQLFSP8WD!;c-;)hw3)w;Tl7r+ZIZ4iuU&%Fcn>-*-$SYWn zh!hYd(Gv^9Fm8+w6U3Bfsxmd1`b-n1CDV@S!t`PWFvFQInFJ<<$zon=9S`A=GEeb@>=pb@_Ocm+HipYXl-W%$+j4f!qkk^H{=5&W_Ibp9;V6WhW;ELdp;GIAzu)FxURC1~B z65$f%GR!5xWwOhBmo+ZCU5>k4#b^?_=v`f1%edBdZGq7^+;zO`bl1hMn_LgO{^FYL z`oUG_=H^!3t-f1Zx4v%CZkcZL-PXGuaJ%UC&`soKboX+v>fXe?tNRG|6!)*)*SPO< zzu^ARUF>f0@b{?c5#iC-BhF*0$1;zd9%nr6dx$(No&lbz-zeWW-`T!D_@3~6=qva0@T=+9(eDetX?|<`j``j9 zllpu3*Yc0_kM^JC|AYT&|7ZTXfS`cJ0eu6K0~QDD3%D7O7w8sPE3ivoT;ROG9f6kv zKLqiEss}{|#Rkm{+8J~;NL0$TRP9nxrN)<9RO(=}614}n6J*4!M(m#|wUpl7@ zzf7$%QDu_KEGu)g%+oUFvX#qrEE`|;yRwJMJ}he}SFv1&a`ELBmOEVTNjY=*;PPF{ zPbj~l{K@ifDhMjntI)r~FYkJgd zS#w;?wKcETG}NkHYiO+nwNBQ`t6i>kRPAZC_t$=1$EQyFIuq+`uk*OBYu%Q0C)C|k z_dz{gz3_VD>itmfetlm3=JgZnZ>pc&z@HiuSsl^ADTRF>eIAq)7ec= zHB~jMkDKdkZuUGpD7<(0g78b>w&pFGr#C;;JikSa7GJj5*y35sQZ4(nT-@?TE0N!(FjHBhOH;G-q$*>P3<=EZFaW#&^DxPblYuhbJ_*B`?B5Eb~){< zwU2JUz5V+R)jPy>*xfw6T0s2s_YitZF;v~y1R7m*?mR#XHk`-VxxYHQub)kV`h&lJ-vGl?75-m z`(E{WrT041o7cNX?^V5D_o>+@rO(N}q;FK;Rej&|tKDy6zq9>a`}gbrL;u_XjR(va zaD8CVz%c{&4b%_nG-%nNSA**e&Ki7ah~JP=Lw+1$7}|O0%Aq;KLWj*9c6)e*;fcdf zjc^+=WW??fx{+N*em_z)Dty%ZQBOzL9zA9B%`Yl^k@Us+F@9sB#~l08<;$U8?)%aj z-8XuBv@RwpW@C&pHZpc?Y(ZRyxYcp_@onQ*#*4?c9lLUDUP9Z1RSEgy+K*c^PMR2* zxFJzBzWews;|&w~PS`VnO&XeXIN3e<%jDB3fhiMGuB28;osycJRzGciTF%6XiQi9D zruR(Woxx;`&N!7>3IY3_tU6iWWWAfzZqg5vjFX2#ZBcZFurSrymY)thDjFjfPEen;vfNu=&uI%3Bs~F>g)W z`g~irZ6~(Z+P-Fo>yAk~a(52dd3{&QT|e%wuzS&N+n$s?IeQ1}z4Bv=AAj6eY2UK_ zy!~1G^AC(V@ZezQgQpHPJhbg_nZt{IB0pvQBsucsk*7y{9=&v|<*`FQ*Zg_o@u1@i zPcSE@o=~0~cQWVH@Kf2Rdz`*>ruCWQXB(Z}bFSLCAI_IPzwCnNg?Sg)i_1k%a5=0zjE(t&#O1Cb-i}^ddKS*Z?wH}?qEUmtaTbnS7}1k z*ypT_cyqUf8xz?Zc{%arawP6605R+ySsb1dwY0z2Y7jU1(f#l_Vz1XE-0vU zP*Ax5Z|<+~%aJMh$@qAA`S|+y`}+Eq^7ZvCML&E?IZy)r3IX;P65tL2w(^+}!V6&d z0Sx;Ff_k!7nD*e(1;H2oz%c#e6NV>nadmU|@bp55{=SkSJbqE7KVbw6pC{nCxVgH! z@V%R&GJr3r7TDOOWB=%&kco4exRzS6?MSEUHA)ZoHKu8^^sloc-D<8JsC=i2%?K~E z{pj3UomUNt>vCyFX7jQS2fvS3ADegiQPu}d*V?Ohe)IG9R~`$syN+LdBGL^RJ8Ax! z-6yU+&DFQ)HZ)=Kg0*{2UVkPw1P~q%+zL3J++1D4ITorl4itcci6KEQP3Ej9MLGC& zK(?}JjhJ`oU$2ZDSUNVNnX0BMWx%ag_@#$nVn^AyE`ytA#=kFS;a_u5`y&hNOXAJv z+6*A=iF)VCsG4arRb%CVl~FZT|I32D?1TRlbaqMTahdW3n;!2yVNG7q-{++` zxzWUmC!X!TuX%mt#l5gf1462w9=)uRy660WCTqt@GBX~I+%oix|E*gW_22X<9lPYv zf!iKO-geoeIe);wl2_wzx7*wHd_T9=v-?z0tD}XpUr!FdpcrTmxNyI(c8jt{dCj`$ zR-FdyD5sjrk|FJ%CVeo@Zn`qyX1P%bjr_{7WW<=qQ+KW%`qaMvjHTf0?jg4>--v&5 z@9D`)xh$z|t3GV*(^&^&qqV!=EIMt7U7k9%eeXTp+CO@hu-kS~cTpS^eLs5b2}Tze ze)*}Jx!~xGrt24+={2+Eg0f|!uG_{a+C1#p)1I1D?UD5Uk+1g^RJ65N*`T3Zf3&?O z_v(fNT_?to?Aor6e8W#4Y=7S6#E}nXozIOe!_B3=x4r2W zuzmXton+kn<$c}`UShqT(oVm3mLXtW%$i35Yg#SYb?tejd7|4r+w?hgXUE#|rX9bY zy&clDVy#=7va;Ihnq59HA!=Nk%bVNoJvF*T>o(_4eP6Nnmu0Vz+Ig0ZBc42)IqaIG z+^|U>M2`~}wRjxmI@x`8cI`ar_{mN0T=({HZL++2_SFH~Pp%T~x)T==(r@0p{lUvh zwcKGFyXDDmYabn()~Z!Y+4Q@IUGGL}eO?{^pvkgtf0=*2b=BP;HqJ;FH@@FIQ0=j@ z!rQw()~EGH?L3l^5p*N|NBPPbU{!<*WEZowzE41d4h@5jW~`92x*@17lTf3m#k zwC7Rn%J1>19#Z|w<$gYoe7_h|W^{bH_3rDg4X8A#P2UYem#m6UIdNvQ`FTwR^ua&7 zvE9N7+LS%|wOZ-?*k&&>3L3;R%XUdK`^t~pOMiFtOz5C1TZM74r@!YhnRA1#$qr3= za{QKN#T}2ezD;K|URHKr-xl|*?M9vEwHe;cuT*Zee#>mNb`2h$wdBz5h})H-4~(m^ zO8DTQ&UeN4kM`|)F>++Q<-}uOF?s$(bm9Ia#c!jBKH2=kklK9@*B;lZeDBu=gLmy0 zBWGuwovC2SoiekZE*`bmIwK)5way*bnLV9@7I1++s3lC)68tY{sFnQ+77$P zU-WHS&l>G#KX_n1`*7M**C{7n&TKQi!pKSD6S4U%oc-Vb_TF_l_EDNCe)Pz5YeOrQ>l~9XK=-uo2G@~S z;i2dJyz|@Y%e!42nOAw>))<}seb%wwS+5q0v&Z)Gj5H)?RF%y7ZrRM)4s;7aqK~jGJ{yb9+PA+_GEl&3-gOQt8yaR<{CwtMx;< zrrny?5iNaGcHf#7Pe(P{vuW9oc0qi}>oIAspRVls{j!H;re~$U=p}nF>8sJMOqhqX zQr@JungLgrf7m#d)M`C(#@^hp>&@%Nu2>!0@J_o_%bwa2dzS+{^k2mK3}3fo`LkJB zoo`op=9hYG($zgPwXp*V)Ky;YOMN)~qGa||x$S9q!k9_5yY72!y>2$0ZgcIMZ0)$~ zsb{mV?Z+IXE8TPHeMzgCojy$JdHZOLzQs*`Vw!l^|*8aSTu%zzA(+TSDXQfu@wQljm4Q(&ZYJcjbuxZTY z%}sAtsk3C6yyxy7_Wdv7vU()mvebGOk@IVG)y`$MXfv$2Z|`^6-#+kRWRg!tuU3a1 z%(WiAJA6}*4QKB6J-krYWnRdYb(1=Vyjqj-{YsxKa4(pf6s)@S8TP;pGl3mpZ|7H{*@ak zeFndO7xmjyS6R}@*Uz4dt8Rb$x?{q#THP+JQbSX3*DZW|XxJvnl=gnsyEm?vZR=iX zqTAe`-W6Dmj%;($>-O@oE0&##*j(+z>;AUd!{3bAb!_1JZa3e()m2>6mL-E4H)*I- z9qrw0q+jy%79A#^eg0lz+j92&VB3$n3D3$`C)19#)!$#X<7_rdwjR7#u%?Y={KR&R z+nd9xSE_Wa`jqM`Yb+a6)n(@9UB_};jWTBpyjm^Dd@)J*-h4aTUjN9WGm~2Mxo&CK zvUSvJL(AJ6KkOFQ?>Dq=>a&V|nyjkkhAg@8a&O$sxcg^1yv%+Xf8D-b%MxX?Y^8l{ zdXr83hIf`6oxN*noBsaS<2v_STz!mo^TXG7#?NUqrSXlFBeq!WNZX!zS-RH~de1+x zcJRD|?h<;kgyYi_lS-kyKq zm~Kt%plL&@9>2P6ZNTOF-=?&ly*hU`29=w1)P}Y1p6@BwBW{I9!BRKF?5X=zNk;L? zuy4P+xp>H^BeuGBv9VrMjqeQ&*(b#@Y41Z@9v?Mly_Y6*wCXF5cxO2gK1Q;p|B90b zGZooC`0ZKW@$2ESa~|7mts3asH^pdw|FlVurVDGu#6~5D7F;&O-LCYo+`Xm;LKn7| zsApN*%U7?As#^{pHFhDIt_xq?BYyDVUGHz*d-mPj*WJIXdTWHOY28h=cfZ>Byw&Nr zM+@CI9Y~6Ro1SdCdmzDDbGB+x`5JfDUAgY@e8p8;bk*#*t80ecFE#5+KM&i@o{z5E zM86r@URBh*H?iiuT2ZxE_J~{AH+BG;*}^_87V57A@z4HK(EquR|Api9xEi-88;wtH zh2}>(a>o%dI3@_jDtR1^3^MV~;8JM-EBhZn&|`*3;5H#)q%mnp2|HQ}p)E1Zd-o>w zkZ>n%Od$6oD{fWKRKO_!1;xoRQ?WQbo~cs^!$W0b?uQ&WEhOno6E02H)N2+ba;s{J zdjRj}q_hcN_ytel_g}cJI-&PUj|giL(IhOm=q~4=LHJu4NBb;`B|5LD9Y(vA|1nJg z@#o55qaWW4{e{a%d6-%s$j2c`lWGF(^~u$ z{vt*|c?()dOYjK{d_r463+XOH3htlILfL?(bm(6l`fH-U^7ikPga;i2cftq#eM(9k zkSWcitdv9~X+QXrPB?9f#@WmS5{$GXm4H(kdP0~&;yC!J+?h4q`QXufY*I`T(gG?R z%_pXeokH_?bNLA=6H-6TCuO8zSLkDXQfv}-W;mhn<-o}pmrU8?bj7uimBF2Ap{yC+ z*qz~Y2`u`s1$uM)!V(YM4LLr2P-1dC{heROXBFq^kIWy<@iBHvIyHETTk>xX3SiGn zSG*q*t_P}~q(GDWXgdX^X%Ec$hH~;r4?y$W-(T(l*RJvD>4_PiYMpZ9iZju8=$M|; zX3#d0K5CfzW!67m!*hS9;SSN^Q3qyvPwEgG;y&bbm6y<=yYuDLfKvniuWMiqOJ7U4 zPv^H&4^DeAq5w6n@IQ@Xr4>G?nP{7!pt+Jn;`oGSvSA8`5sryp?o0jD2u`T?gOaQXqK zANYIx0CwU6NdT#gXB&o)YIxTqRq!zW5`L5oY7PkC5*yEVJMJU17m>!V76qIggN0xQq7uJUkk(acx(7 zVdYKY+V`4)RiBP1AWRjBiDfM93W$5HJq4`wG443&pm~dsv7l)jEG6WKfn}a}%Lo%L zLeb(vh}(pztTD5Ud4!yh1RYCO$_WzzdZfLB#BD;R89{xFk}x%tI+n3_L#n;h$dWw{ zh&zPwcmd+Jcr_kZaZbZBZ-S7DRN&^a8dCLZPP#jIj0kSpJ3)i{k>WnB=q(xAC% z#ns0(EJ@Shi5^-dOZp?xTc>527w&U8{JVsyXH?^^vIfGqUQ@7)s2oxQ4QP$+=Kv8C zmsV>o{kpQLjF5$(G2MvUO3GOBqmeMf%xVB0ml2ohbI)Je++!0*G>o05XL)K&oa+@Ln$q61uXyN2bEdO@@?we=Q7sLk};@geiqEzsV^vIaud{|p%B5@RGhk2!jDLZ2caG5}i18EPU-IT;6JYmRjLThQ~2Vwf39 zmf2O=LP*jv*2a=`7Q*vgXht9YSPZhC_Q#iY&<}r3fq1emes;YRrCWVwDH#odMK0 zgkSa{o>9Z{EvE;0Y6;`@mIE}^&=S5Na<2vhC9lC*4w9_4vu2j}YE~^hVd@yyvF}*Y zLr-|+lkUOx&+^UzYCf#swVgGxe9f-z0^C@)>skty0nN8smyfZd4=x1r6WUoF%e$Le z7WU$?j)F0#f~OGPG23|RkYw)g*>Jj zvy+gSg)n>15(`ZCEc)UBPlnpF9j$ocBY*vkIDb5jNB}FDUI@AkY5sjY5|meYvt7@U zjZ(rdZGP%y1H20!Tl98%m5ao^#*%B<=N9y@KR+}$An2xVn7>$;pF*u;1{-VlzehvxAJdFUneA0!3sWHoIh1%1&M2{ zi}l-SCw{ZRSFXdOjQzL4gkF0AxxU%X@{PFj?gv6H!DZ!)=iemcdl8UuEBy1%2(QsS z*c-Wu&X^1~S8$LU;D9c*!IYC#2)Ow<;rEcT6!J$<<=p~kULe6Zt35`I=Z#|kSQh!* zA*6L7!WSkK5X4=R>95O^OvI*`cs!0B$bP@KLnpDfIN?O~HXCt${NAj_{%-JOsJglS?GbDWVO zuQ6j4m?=hk1vq{Slu4LVe9u`}atSD#;2d*WW)sHkHAOkXGMcJy(DpqOi`H$;6ojN< z2Hgp>_BaI=wbh`3H0m%rXA`os5a&9B2Z}B?QWRrtOk$-)z_|>k!t6nV2|(#C14^fd zgv2{g$Rf1T#>%1StHC)g#Ta;HMaTfmbCAA{5g4+jkHEhH={tXs&)j(TO24GqX zH2_CuBTOGV2ArV|b)7;f%e<`wBXoI8v5vyul=%&hL)CLwfrtnMmoeeXVZ!A_NGt$v zmoQ~saD7b34}~bdFmC~Om%~v3hfKrx0t(rM*-YrCC1j%n(=9MYI55Z@v=OPJZRGia z`F18CU>+xt7*P`dpCMrxYuhJ;sbPv5~`gO?`faw+KA)c!YlmW9Nm34rZ2* zY0LEq9==L3jHuWC40Y)`I#9rji=s%mrJQ z^H>rqC8a$2&R@J}+LYATxPkq8v}x10QT?j*t~vU))Vmf{>V}54Zqv1Y|CqSssZ+jP zy!eY!(84;7swxmx&a4N$F=Qkx$xt7yY|LYs8+;6eX7|;IDaK)B#QVBWfc zilYHzE!bh;Dk;meg%cu&MP>Waq4^$MNlL-Kj~C)R6#(ZF*NV9ztZ?MZ0+x3j_a0Zk zr1k=ZxkLpC2gE=6Ccsl6LXrcTtcF&jwd@lZ4($6UG;JP&D61F1=2D08`Vn-q ziDlFwFt9L-j(y5958VMeU5v5bA49yZ5v1qC9VI$I$tsw$+e->bdO|5JY!q@RQeGkl zB;)n!F{o~auL;KRTdQNAv1B($2_EEPn55dEAV6lpla6x$l9d>QU37T39wVv%K$0p% zWD71f18xkQwCAfQptuEmR>Kf?f2Id0>4zYZ1PFyJe_v@b36LEyQzSS5$qFbqOV7Sw z$zn5}OhS@Au>2>@+u1QtwV2+qn0N|`TS2iZv|FaYz`jIq!3eP2=wziZ+u)icun054Ip%FMEx{0RKllLD z1F_t@RmgCDDa6doF$;|iZ-r=R48&iB2dpecoWPW6&~J@HcEL}Bn^bUtY~@~9X_R|3 zKuxeTnrwee^{FOq0d=}eSbY4U#9<_Eil?Vnr1ff03eK{L(njO$6#^BeOr~!xZ1H;| zapzZW7yHfL+d|7-@Vb_Dw~Ox|UYyvuwi`I;ZLzZ=m`|(%^DXUNRuZ>lR)kEE*O0K93ad+0f=s9VGp}1N)V;;s%^IC zU=@qC->E{7mmaXja4QRE@@7aL9uv>BI}1H!`v7J|A2)~VuW#RwMw-ylzI2w5e<8>5 zRrr^A7;Y=BfXq^sInKj4?`%U{&nuYW{~iyY=kt1|* ztVF-Lz~nQnCcuz!uK>b$JpkwHf?!YXmu~`S6{fuMa5_F)3@lc^i(V2$4^1$OF)#W9 zsH*4|fL0@xj{s1^J9;du$nF;eam+Bd{lksn#3C)XIT(@vkkN7-){D#;j~C=MA|M&o z>+9-4;}a>&_be6=Xlt#8#rpha;B!36oJ_}Lpx@L4-gw;|z^q|i&bt%RCR>fA9`m5w zGx8D-pTIiWdc3bIY$?+LBv?Y+W|~xVvHYaUQ}P1gv{}OP9?h%+i8Cw?Al|c%mlSlN z{y6vvo}R+=l3@LAIPpapGKY$_h9!0|$Udn0m^?!ObqnU5$7y8_X4wjl2x0`Ub%y}? zH6Ch20v-|sOr!FDLcv^?+2fT>9$~gTbOiZ%EN>BJ54@P@++!%1j~ODxj{F0{tGM2F z97PLQM%>kgqI-ntaPb66zQZ(lzJ?V=*p?afdzvdjF9@TbQ4TBKrX0?p{rgkx7X+bw zou?vb2yj=Y?xeuc*pwiEwwtU0w4HUxk)%?trATI6t1{^bE zj8tcHFeXUG8}g7a6YNKe3KHKy&18fS*wgN#5OdA-*Fp-~?hWk_ezu6^Z}leG4^X+C z<*TNK6zExAe$OLn+T>er4k|DokJOxpsjv?P+rjd8*CXR~tY8|pxU5mWi6^1l!eG5ir}Jl8Dp+3G*OaI80G$DI^Sz5O)|LbKJs8t{F$!AR z(6YW6Y@NADG06B=6k#+F_G8r66vIgvzq|GxC_l2i3;lU$@CQhg23*hgT8u>IEYu0} za=(XIBR>d8?e#jAzm5#OCyi+wHTa?wE9b`z*aQe_!0oa zv1)cTHzcIpb`^~~53xMSYJ_RnoWqi)3>qG&p@Vwl3v>iYMr*ft@QUdO`VU~$9Ww19 z4U&IidD&SN;9kAoKz?&a_{5bO7y{Yw934u&MPpGIoC~2RTLgASTp~A$rNBFe{@Bx9 zKwU0WoCz@m+1%jI_I?iAAWJb{TxNi|1(>H9GvSvWaYU&A<_Uzi*U}L9FoOF~;Excc zp+!S|f;#jW42>*8WH$hFj>}Ofu8)E6E5{vEsDXKkg!uVc+0`9qD!CGnbkLZv&>{*uoYDFh^#B&T{21eTqZ8A627!zdm6AS>7 zg}GroBHXgs5N5oZ2!eWk|HN(r>IFna%R6}@B?&Np-~Gru}FGzV)Ch;M}M8|H!b zGAVHh>^%1u84$0s{G1)5f^#uJf!{0cXx8I3opGYLLEf$aF5222m=P+}8yX=tD%*oM z_q=2tdy^GN&&_P(gH3}Au$tUpG};SGm>Cbr4GiMfz`TdBo#|=-<~b%0@tjMk_F4O1 zo4;f4vVwvOUw12$gFQ3gTDl`t--i{`0s6?G>zFjIW0!R*f|S8#h%+(|L6xgV{h^D_ z3T;c-hpdY&=jgOfrQT3A!|<}uN>vS(bB_Z#-{p4}%MV>cpvTxj{e56Q;vH9?CL@=e z%rUM)xa?AJ`TH*$d%eI|2Kr7+9Oz%3vJvbT`!AlmfPP7U(ln~ee1sxyo^^Up-*HE5 zUypFo)h@ccc5KU1PhePg;K}a!p?&ukpfHp=@XIh_IlK@ob|zZ4wviyba0?9WKYjC+ zxB4vz?_3MA_st$0>JPIY4SZQWG&K+_%=y$EV<#yYTQrQ8&UxQ~zm?Rc{kL zcjwht>fLM}>t=X+ZO5$PP0PVBybuR6_I`jqkJZ~z9wqWC_RrKHa@hsN>mcKz0?f*u z9+eySP5yTGuTLcA!&v2bSR{`xY@Ru$O^EkZO#K*(GZEy4!{K||T_ zI0xXpd~)E~$2o5$$4ZtICMsENZqrp1r0L#vB0P_2@?6(M*>*?SU*Qfya z6JqSsP@b_9Absz!&s?<}N&YVOjt40IgtpJQQAnuJZ7U%2s%r?p+6T5H1w2al0a9Hf z0Coo8t8z*J9)uqT+zn3+;k7WWcYyJFu!(4xe+7O~9j*Wv++KnK{H|3KUcX=5bsRNj z?Lo{yeZd{h>axjxgnV?f?+d7*Y7xj6K_JAt1~)ZF2AF-=B*;eNiaH{Y*ow_S)a;X@ z=0~io&!FatORl5lBs|MqUs|;n!mLCqGic2=Y&csmGQ~9tP_-MuGIjf5Vy=p;MUAPm z1U0)57N|kuk{}ON+tD)x7!>33QMD8Leg&5^-cdthP_Zlz6+2*I*FYh^Y(9rWE?{V! z0D){uz;7?$R?!;3)iT1Gh2_Mk0!O@U05mm_aCKxN#>jwN)NF+zqlWA*tT+b?-z-AK z7ASWs2w0jF00sP1RKbE@`qR42sM`d$!}4DEtePE>lIc%zv1;7OE)o22tHuN^b!C!;ET@S zbrSaaBNVq2?>*4jc8I->d{0)B@5o#nv@F2!)Hh@yS%lXTvWzUo_cwT6WgHnwMv`G< z1P-^qAfrhPj=1B+{H0sLR3w5K~A3Ua@#o z-voRP2Op!s*9cJR0U9mwV}h2XH~A8EUy#9QVFaKTkTp=)aw9UQ8**oe5$pG3A+xOgX&DqRfkNWB7Or5Pz8Q{6Pa^0X3#{2@bwwL`mex(cK0G z^7&XzK0yd@4vzLDIcl)F+to6?-eNH*49Xz2UaQq>Woo@fq176+T8%+rwi}5>X(J|) z-p!y;tJDgEO=0Dk)H-*WLa$QFq)NTY=&#i)MIxnMsZuM|YL&`h*VtUuvV5IFqtRQn zHg~%sKUb75ml^TEZ>xtvr<3Xm6bh4FYxB3NwQ7x4t=1}4Vv$s;w`y!YcF@;JrD~N_ zq12*17ZaLR>YrN$W}{gvCnmebpQ7lMN{vEp(Cal)rA8;^6=+RXBQaTQ|I>o$iv&{& zg(8tqm~Sc;O#i=HFpXS`9wQcmnP=5}u!wjK}m-|t76TBkKJX0=ts7v}10GAkn$nvl*j znoTBRHd$F4PcIhQMXZpKTeVhy%RhOb`IeAPts%JMd+vUCA|kWOdN@^mtxQACs`3^$XJr_UAYA)m%-hFzMij7b42 z6-tRwZwSz<;35C9PV$e^nGLYae7%ThOjeVLkqC9vFb!raiY*o^-=@u_qUREw&fx#Q zOE#s!pl4JNh$q!sY&OPXu$UR8P=l_MS`x(7P+Y)i`}F(+08sto=^oL#YUUiRj#4sYPE|h! zx4>Z52N~7HBI*>F7V`@X|EO>*P%fh3R<2Z~p>ZY^QEM_VVo2yAl$q^njmBitOOeso zEo!;cW{@dGtcX!-EgByTL`VN=G>TlIO~jMiFy;S4Voifa&lprDk(XFiD4ANVv0{#( z-Q^)vLNv9?oG+A#+=aPjy;32ys}&-FNGR2)tr{-~&dH`zN-!t#getSv=wmYcjZw5m z(el8c(?W~__2Df8Vmevq&JlTB?Dul{dk-mAD1v}!Crd2$ihXT%sD1aF>BBmP-r6L~4HPL*OJ(<<`e+?WEn zLFOk{{9`!R7<4>6b<1=_U>~W+Opg^c7?+ATr!3JJ)&8jeTRHdssB?`% zWzZm;Q&BIZP-(F~&pQUjJ%nPlLLt>b-E{iQR~i-mO1boq9_m_ z5m`ho;`{=rS%ERBGwB0#GTGnDug@oHhI=>37-_CdWEcJzcz%*=QUoe>^1qo; z-`_DR)0nkHy-xH2OiEQ&WspLt|GPQ#`yGc;CD`Jrq{8*0_acVlQLX!%dDLiqWpcfm z+dk83(A_GFk!O;FMXE=M&a4giltZNygYDn952nNl%_tSKxqjjTg&aG44i*h6iLHR- zIrs&A*LJi_*+D8l!u=7EpG`s4x%KyPVPY=gtu12ZCvbA{U`3KTjxJBgz4b0z{393L$gOsEuklgTWwNQ+fr5c}jKV*4v>Xr-9C zG%S9UYx&Qgwfvq9sKY2FV!NY1#HM_|Jh@cyuhu{I76AZ3F7C%dHY7{}WDZ3Z7C`VE+s+Zd5AGS6K0Iup*bMq*fV`6lq1Ox9ND;+WC)o(W#*m3kJDC$%D0n6~iBn1yy>) zFaPqq^P{PH7O|pGqMHOt$FQM&Bs1C#e4|SGS$147!ApdWT?}|%6<;mUV{6f1b#olP zn#~4XDZM;jfiu^Cb@*Bd6OUS>gp$!ew2SB~b~h-M()_%9 zsobvgR~Iv+)*DrYPQ^VpUoOGE0_BHQc*>P>sSF4bg`wyi*TsbG9WnOONr6E@)CR3U zB@)W@cCEqc;@}0_&RUg;F78Es!dwYfpMQ2T%`+%OR$(boZeBiytx^Hi$mDryqr=nR z6a`{=YEf{k$g#{T$j{3KJWr&uy(m0U7MRr#7h4G$ol#442ECgcQXn$Ze98*WnN*Bg zVa5Jwt_<5HpPrGcY>eF~RAVpFj76I_c##yy6*5JhG9PDfb41rgzIj^Oe+2~+v6!A2 zn-PnO3>F!VdlU{f6g)Mz8JnxtnY4PHyFMRqGAmB!Kk9>LG005{M7UOL7vxH`I7s^R zoZgzN;c4aAJa7m9;yg(n-RY#QW5F(g`azH$RTSuCTB7HgwdnFx^mIn4Fd~G;evnGHv{2W$d6dLMroz_aTiBVAs}F|V7^))mN4PYR?EO%B0Kdb(kN zT>-5Tj?##AjuIN-3$f2p*oMJswJ>IdMaq*&aE8eWT_q}gQ4RSq`K}P=g8E@x zrXW}IR7R{b1Qwhli4awY4f#ZFKul&8@x=J~0ace=r%+o|jLvAqrj`nUt1Au@HAcrT z8bH);Hfa?aMwLsc{sjJ1dbLiV*JjVcplq?|Lt0*jT0*awHlxk`(QRT7mA7A%L=;war<7u!4?E#ylIa8881I;k*^ zYlM*)4F)fbG+$EO2!2l>15w(BUTNUzDfieswb0{ux^W}dSk*inQrKYk8tgXlgt=<5 z&AX@pxNwcyq#+6_#ZjtCI3RDOLRQ>_O=o3n27{vo6Jkf6QBIE+#WpL?rbKJlFECi? z>48~Ja^oT}j%Kqs>4;Wp{vzD#S?5H;k;lcE`k)vC3Sk_{PEBDob-Bo)$@j&aJ?2dc$b z?EkJQ3*BVR2iZIkqvf1{0uEkaGMgmaZnl_)uLi4;XO;`4Xj@^?m`%1apUPxZV+dkx zfJv6>j++;jmkS(?cB&04ROhNm`{<1;dcg z-=F9>*#b*8m?zcQsRMB65!VVU%P6Fn2>#Hdi5j#NvS9FtEfQY70Y|cYn@J20&(iZ1 zEn3aDSgphiXQe`$aKV^tOtx}=XfwY+h0}n1Fh<2JCpMYQI%)nNTeaH|>gq5=xmI%# zu1YNy9yF@Kp$%iyP=~36XtUf*+$4S$iMmUQYPPy1 z%aSG8vZ_@rSrQA%f*=Tx1VI2q@4fdz^xjBdJFvZ%v-|$v%vlg>i6rMIOYeuaID5{_ zeE<3Sd^3xsW}-7_l%+mT6ljp1JfVqPOZ1`?jZFh)4HCNl?lsXC?s&& zf_7FZv95`=Tm8@rWHzO0GpY^D#4kcXYBz`Ah_7Rs-P}?a zP@y8C39v;Lh}5{T(q*GyfW#I{s~g*BxdNhYphUXf4FX65Sx_W^0+yf&i(yC*!_R~U zA|GKK4V}RaMthLyTx!Oq*`f9N{FG#L>exsuUI|vvVzeGCQVV{pLpdQ0;ANymr7mWH zjI5Sv~G_brBO!8m_zV^CWHVD1xx}$O#=|sd!4Aa@vTSN08@cq zF+M(;Sgpflb|8sDs?O;38j)!Nm;0em+-@u)K%W@b{YC;v=CN&(WCjC}c_xBlus8yl zgmRot_27-art=nl_nz9r|Pz z51OFC$j{VjV$4pf$xLD@*??nUK@pftu(Kz#sZ+08@JQXB0K!bWOEZT{(C31mmeugE z8Y^Z#QnTdMc_5`;mqbUsW}{ahY;zzOAn$<8!yImM0y@fKegUbCV z!0~HVXka!!w`cwh^$13Wg$4V5gm6e#v%#T7BUTtgL2x%Uc|mi7+JGkQ8+Jpa&2^Ur zY$9>_kOCzJ3Auwoaw`q$QM3rp*n9BZ4O&dtE9|C8JCEjUFbRO+dTSdfq5&J);5lBS z1|0!c5PoXFXHt(Lp=lQ!UqFQTt|3}X>&}2toP-;6Sn_#Ki*_$iF~WCWCC}OJ^1Fg? ztc9QjdI2`ZBe-wyZE4eKv>VZ;*~nS)%LatAb_1Ufvt!-0fokjkA`2*O0c%Oo@a-K= zwCN2>L-q-tID@9MQ9eLZ|DGZ==Uis%KCy z;Z2<%niFXwpIzOF=ik!oT(kHoXbJ{ACWzWDwa)BwnzuZ^gFSDs0iIHHso{N@FkEY(OFTQ{_ckNL>fkM7?dhz8Ag%ng#E4%QzxC8 z9!(7FsKaVDhE}7EX0V8ER3q1!?WXN3W4%2iOAtI=I(nlJ$nA0=sMlC%Rm)^`VU6wG z{q4KnAJ&d+ScT9-UbJ4IFF|>v)S>Ov^lQR~1FKUtCm%ohz3+VU55M`HfBNAY^{aN% zBxXuJlYjr;?EJ=o=%{^P|NFr%s}Dupg^pteyZY+VAN(qgRr>Fbv|Bf3U@?sAec6xynO73^?YCELTG(T?DdY=z zqP&!-heNGCs5ir}_<%ldcvqh;6d9Vi^MTnzAvQ{R&1>D7L5*BLSYx5X`Le%&j$@Aw z*|m!rtjnmKA?*Gs`MOsk|I=Kn5r$E{9`Z+fA-I$%acaH*R--zjkJ7o@;&fXKW}pps zKnTeFgt;IK8r3}-Qt$@at2+Ms*wK6ANt=EJI>%@z(qM0T`05{FfAAewE$VZ`Qhq@Q zIKl3eymD*4q}HxGu;ll_WBAg0008^Bz(@uyvH?5Q4w^NDpr}9$qL$RFQ}<#dQmr3* z;>PH3z(+UqP|q7niUJ?)2AM^uddZrHkx-!E5@_wi zkCDJ>fKI;AU@r$Wu=&RBc8sM6TL%AR z6}BMCu>1|lk3BRNl}jS~JEJzAEPyW*FguaW@v=G@ggSv6gC9DU8$Vzy*o%wG-+&uI z-7dsuHa-*-%MvJFEgOpU!47Onos?p0;%g2J#ep*sPOzcWVxMZ6<&q{A86K}0S!54t zi(nF=sc!In7$omi8ITyK7cq(~HN)pg|mb-<;Ty_N1Jp8Bl1}?O<|~-EQ(i_nXwinlW&~{7sS} z3}Q%%Lv;SdtcB;RQ-2G?zyWh$kJFH(*X>0xa_a$~1wo|)MIz9B;M)C;4;;h$fp7%8 z0+_Y&A~;&7KRPrhh0-B z|1b}r+Ze;#mn4!s*PyDd?nTGHwy(oeGyEk^$GDd%&68k=Y+E4$5%h`O9)csdU--zK zI?*X=CMdIkb&Gy;JK2z-Sm{ z2%f1opXVbY>Ig<}*nAs0bvf(mPSCL9A`Effb{yXHJ#;0t6ysouFJXtUwC!6s^yypI zbeZ5-9MOwqj=g@%N1Hg-jo0GTvuL~(D6c&EeTmF0 zk$rjAa0ZB51?(UD7~O2cqKMgPLm7RQ24RZU_`iIT=-9tjD!vCxyT^>#07;9z3<8!<$Y1yS zyurNz2ePwp3{3&I4WTrPe%>$OPVnb0-U(DKRbJK6wUZIM8Zw8hHOm? zqAutkKTd-1#G(!znHY!Q#q{h#w?QnHQEcEtJm?L6a96t{T_KfX)~o9oBRq+|UMD%y zt$maA^{rKXjwV)%(1GF-36?@*QJ9TwxembwYZOGp?fB3x_FeA+E(#n4brbbsy@+1Q zEKn0RY^?{w{ks);8y!>=s;1FH$R5QySo+Y0_it&~?m{J)UfqnAWri6Xh$;KRtO0FX z^H|%zvY(}eHUst}e8k2NysL4S3FYS67U)=v7LCVQMdj|mK4#gr?u|2WS+V%~5gVtG zo17Gbpia!~!(J_GGuoD!+GBChSFRQ2%br3_OXDmmDkMi3`yT;<(2Dp5%<03dczwvp zg+lUh$O1q{(bkWzN@RboMZ!y_)BlZ=j&Ix1WkSP9ESa&$wiW9A5wk~&=SWz3;oS*g zWP(E>*f4nRCuKfIGZg6JqQ=PNOVZh z!m1GE_aG77pt+)vSvzjQF~#3rLF6ab+K=O;OuIxbd(xy&fqC7AG&9Ppz^F?>w-64^ z3s@aAfX}axei{55okaevakCobty<^H9-O8u|C@5F?i8F9#9l}e(sNfRKF~uBhcU_K zwMXPeiTryLX7x2ZWP8y|2j~8As@{5ns<#mnv1dkTe;NWzIIdF^WEt#Le{aI+7SHN8 z)W{=g?YUp`V@rJ0|43i6>yE=;1#XVek}wgB3HT7E=n&qa6kLs(fLVlwQ5NI*zlWO_ zwdby0yB@hZ`}D&wIetm>H}0v1ImHqkstg2U*dWy-Axc{`jfl|G9`vqx)OtjkPEOG(dboz)q2O9VW4rn%)rffXRanz8uFVX!mgk;MIdv)} zH8t&YPEph7HIQvVCyToApafaW{i_{WDe0<=3{_f6Mq$?i`nam;)a=q*?Ygn?1-%9N zD=m0QXWWo)ppco-QYHG9++q?_zcQ$dPIDj_b*NXMf))6+0N75$3@ zlMR{ekaizxMb@#g0F??3t7%=gg!rLZa|;HbnQF`hdISN!3BR=b(dnLrrBP5V!63G0 zq-QMK*4JSbTr3{62N(T(!YRu}RY@LCz>u8^O6mR^Gkc2CQdwi^DXC`)nkTMNor+6y zS@SiFVgnm>XcV;Qxj50+TA7>r%2W3#{`GIZ^?x)-pSNkgPJ^0my`(w?TqXS7KBT{{O!W1PUqDQV&*U^K~s#BVAQ{UdH;4P z&h9)gW^_X#d`o9Atd2M4r8CatlLnm9(sC+$h9<6#^chX-)}B)s9tps<)C*WlaUsBQ zu)x(3!gvCde5s*-c(8x$>hQ49xNf|ZCNL&X&zN&t z?Z}jxuz*d)F(y=Ljbq{h7O?dGcrI13X^k`5jaR9}YXOFdIPJT*KxFwj4? zbaewk>&%2(VQPr@B1r0q&c%{X7yHoAnRbG@z> z9z!9{Qj&eK*5=SmYxZh^opx!su^ThQJ=^pmS9t!)yKh^Bov#Xh7CKXApF9Ls1I)j?GBRT*hQOLRjUcU4*ou z)8w1>4rBGu3YQ&At3n{;{Uc%RVhW=iERxcvUsJD5O<}No9pi0%3&S1dxtVFa;|vKD z;6A9{6jgRXUGEe|3~04&aIB`|;fp2AT5VYR+5>Ej`qi1f+Dm63SBy0!7=d*a&iJY-?c6oZ+M4EC(-a$Z&&=>}e{W|?$vKFE3U`E5Qa-~t4K`(W`q}*I&ankG z{1c468+j>n&YwAvH_tJzt5LrRkAMhJFI;IYJD-sP3c&8ahoKSpX{l*hms{&P24`D` z7dA=gA6n4`kqR$KrG8>nCtJPn2;Ct#4V0{uYp_Fa3c$;KE}M~RbxfWR7JfZ~ch~Yc zJcEWaGE$oLA@vtD^K%g*279{u$0ufb%L=nqvedM6;-@fv1l0j>O-fp3ZfRrR~`B$9o9(~~7vo$vOMMk0pZkcXv{z_ZL#Y{{`T98aT13Hv(3(QVh#`&u5 ziMhq5w!Y~tG>s3;tOS?jSI3E>?W&BFVpjm+0*Znp1}c(qz(NiOW8j|K;DVx3!GUR! z`h6j9OlbNvssB)hJUw%EHMA1Gc5u2E+=O`nNqr-u^JAS;O^q@2c{v%WY0#RXj3_pu zkZMnQ8q`X9R&HrS@A&+xHe`^4H0>yAPi^MW+!yZq;e#*bkDIOVGLV$g9wg%Q>g9>P zhLQ`AWGHkMcvQ&vePX2a)YOdZ!piF2<@wp^*~WL#H$StyvKU<2d9^5&sm9ct^*{*q zFO*1iV#;v%g(WcIA4DJ^55ql2aR|z}1W6E6=3mH2V*sjBispl}2WLjw$F37MH_|rV z)OB#Qb-btX@`X$cg1H_bP!+`V!GTE&rR+@0JXcuN(m%d%O@qZTM&vY{eWTSLvdJ74 zB;-&L120N2Y}c;NkM}l~UCc@krAWX8LdGBS`J;iSr>13`Evjyr80>85S(ur551oq| zyWh3bx5$lOx@t*4J*E$32FN#4@zF*&I1%VW1cHTp3ucv-fkL`=`#o1q(?jSYWjq*k zAMhAQTQ?_=(b#!lxM}Lj=wL@zV^z_)Gifkf=@2#YkRWaXn2U)!rZY1qzpTDJ z+%Ko9GPCoFDw{e7$7YtUu8|5if=G~!9fnJAR&mC>W`@=SL+Rp3B4%v6;NP;{Z#68RNeRm&n~ZfnCp4fsWw-@yw7Hso)86=qwsYR zg`U3NE7M)QgVQZtHD#AC5=N~;^&dIm=(W*3&P zuBx>X7Xn)^yTT}N3BhuSrT=yE~8<$f_AEgxF^fy#KBkUq5Y=OX7pclo- zg3o6G7F73z`Y?DJyL_JJ6hcsyu9~(5OnWW%wJZHhd2uZMuPsXG`&uV@ z>*lZY5BGgIAj`wo++N2H-xM?XsaZNfAcX+#umTb!E&N{Zn70?j&jQkcu!hj}S}z<0 z#v*PfoK{eik#gDX^}Ba_4dbAFWRzp_$M46at**OrsCKe#t$Mz?x2~+}>;*sr&pACc zz-S0&U}mX7GUOm9J)9;09r-E5_(Do3RgOUalm{FM(!0D%fu8P;lDBozYVjctU3 z*XN+6U|N8ItoQ^^RKQY~0=*;Dk*6W86XLH;mYX9wZU>7m0kJ-?@qtB_9P7}J$=UMv+lH8ul5a#tnpX>5h(Lj z6D_@?7bhy_uGAWuDy~+S&eWZ4s)X7&C!F4$flxm^1KJPLl+Kg^kYL;cxS|0NzM0$4 z@MAau%Je78i-f$S=-2mzj}u1|&i; zrlf?@Lg}P|p={CvDbiGQ49S9A?SV3d%E`>kI+Kx|d%5i2vhr%XvJ1PGF4wMh7A-Uu zEH+k6UL5W!U#tdM z)3Bq|g5Dhg`(>t3PBoc4hHdM{xf$}FIu#!amWpdlIWs*M`sVA-U!7_=)4JAEnm%&1 zzxG_->Qr}Q>4lV{;r_E|j#<+oInmse(!n|ih&zp%EhZKxytY)ND9 z!rYnC_PO7@uvl<+s6Qd9waY^Gat)`tVY3 zP0pqD#evp}+>`ZY-RMw%WqD;j;gV8m3h8cvkFrq=m6(3Mvb=h5Xm))mJ-fVhXzl8m zlGbV6Kj)V8uUshV*Zvl`G>=;l9fjSoC6G*2`UuYFl&xE_!9nWQ5iUlYmKf19ROW19 zxH$X)9~ee3<^mQ~N}&ryTrn=+`q0R4!7lHC#jw!dQl5LV*046*-&0Xmaf!iFVF<0= zqF!6mBUDf!qgGbg*FUA3OV2KA8N9agUj<8D_w?etdtt}?@*rgzPIY^*??%E-(4taZ z=wF;7$xIUIUoS+-PG<(lv~F@)2pW%}0MTy7sXlDuu|?xl9!+h>Cl;jtDrElYw`G{A z`5CRoxwc)sb_vv-s`2@8KrU7+WdS`ZW>`(0Z&QpsfCbyJnEm!+(Ie>H9$Z4+HX}Vt z?{s0`s&lDZF~og&@?u5Ljd>}|K5v?$h|1lfTmGvc`N!W9lIiK$75nnG)nV{SYMw3N zf{f7iK*Z!&(2ZCjH(4P{Xv3+6q9G31=Fyx-R_|o1;P6RWjR2gLM( z#<~)F6>O1~b=8IHeGl6tMQj1(ii2{=wI4jMSKJU9olmqf zv7xohw2YJ*)SN4f>xQAB-ysP9>$d`7S!dD47%Nj$C;NmT+nb6~eJbwLdt93yh}}y& z4haDx?WjR1uXmfzJDHW1p1F$RbH)0GX>F|gcgF|61q5x4Jy++g3$aTRndwNSveqD}Mkbj!{*oQrg#?D}j|b^~9C#-;H_v+fPV(=k&z1X)1cQH~mx(Hh)U} zn0P9GB^_W6*|$tQavMUBYA)byIxzG2GypFm+8DpB;P|EGCD)>V@$izmXZUyG9sG|^ zME=;|kd8JS8tUG;Z1*CI;lf@ekWX8<;W`H1Pru+d8A$%IoD!r>chTOf=Tj!Nt~Kx4 zt*et0Jf-#NI!1r}$w_% z(>UomAoan?j`Gf_>aV$$yi2z(&s=L~xx7!8@W1*bgm*2QjaZkQIx*r$lz@X!d`@QZ zq;8xJcU{bPid1iyQ0`8$K*w=F*Lmf_beY<<>RJ84ny#yd5}2QTqMLgf+KjF8_R@ME z$`x5j4job82wJwxGxR#mb7(_uLgg5djI)#lvh^~*VZu4*n*093T=%C=?{`1`BzF{_ z*JsLAg)U4NQgL)Zg#nu<$fGxJnPQP(VIyqkJ`eWDQLrrYYiFEGuBGp2?qdS^|bGVdlFi#$rhjTf&0aTJLqpT$<0Ps&XRD zw2sY`;L?EpgRBotE>&m=SY9VYHkiRO%{mudi{D?G|9qGrQq0j%%s8JBbLFJN#NZ?Q zbRSM|G%D4xtqV-AB^S)K?o_+f-`6g8d>%ZEPNG*1dT}y7Wf7_H&W9<eJ`!FW=DlUI0A$| zJSKqSl?08%z3y}QzK?=iLx20H>nMK`Lh~~9XXL6%KlW0@x^SEi?T6NG;bEjjv1QhW zLb`3CAe{z@qlh;=cF({1ePeC4ShN4svf+R6@!jI~B3-^Lx5?+m`m`N#=MHJM7#mKS zRfrAXG&h_E;o9)nJYnxY>nb`v(_~GK+jI!yP&XXCf%z~8)UYSrf-PvkW}0YhWyX0; zCYQza&%TwhlF#t`bS*nQ8l0~X7esNYv8I(R0pv#7?acF{3o~bTKO2tI6jSKqHlk^kq?apP>LMo(c(0Q zRY-a!jz%QE(&91W5R~tEyi*g+pU%Ml{9_Hgch+v3w#soZnQ)B_J35H+bBE33G&=vc zf4S+?8S|fgoG}lNo841RRB8IJV#N43SR3|{x$%Xw=_EnvF6ja37o<1h!%c6J-h!{M zN`DWpZe~}$g8#oH{RTb;_4n`rsJBReU;5wh>Z{T}#I>(Vza#x)>35}njlbWKehcsX zv-ErT|F5L~fdAv9|0w-G_^p)QB|R)XCQX*!EqzM*L+QQJN2O0lU&Pgi@%I5-{fYDi z{H6bYEd7Br30Loy-hrQ=OCORxBYhJ0KPvsH^d9Ns(mQd-kMNtW-Hq2uw5O2XhSuYy z|At=w1-<-ljN$L3{}UtoTa4_FrGFs(OX(kBV2T`ze&F? z{U`kVq4dwCe~Rb+8uxr1RDK&jH{S8vnYdF}G@5>D}P$vkUg+KJeig1YjW+*z*`pBSgE z>ffG`J5rLL@%F{Z702@arQXrY%G=6D3ahG%Dyxf2M*J@44wtj;_|N*!G&j>pyDrxb zkN@oBw<~{^erCz!!4Y8=91*sCZurXa-dA&S7JV+)0k^HXy}rJ^p{}#I>i*>8Z;fNG zq;t3TYQs?PiIl4Hlymt#eycOavv#TBa?$9VGuowJG_;<%@oHmBOGkBW$N0s9mUCGX z!xQz#3+qq1Ps!ftS{0UM*x(ez*}0z*!NXacV7EK$*?ulDPN9g8f9mSHDIR?7|2Gsj zp88o`Mn@Y>v2+8c&FykD+?uRZD3yvs*?ycmEyJgkc`?tUlP79kypa2OpXX5lpQ+&W z72J1Bap%vf5es1Fuh$ggoVzO#G=~&XNs2+M&X0pt4IbzAm5$f$QXKv9udn$(*W6v& zqj6o^p{q$kuGce&Rs5#FnG_c$qGe6 zT!vj@ahnfV)<3UuUEb8;Zi#MdzB56o2q~izk7Uo9aV($L5hYX|QYZz51amGsq|%DS zM_CNR_rwL2vgCvTiP^0`pqr@qoT#(|@6q8EwXJ=gp1s2KX0s?od ze<|wPmAey_UWGh4q1o+UmskS$xQ(?7M-u!>>7h3zS3R=_=daW>d|qjoj5o-@j1pKZ_+y&GhLnl9)J*p6}te&iSczrE*`^_CbWk2!wc~t#cmNQ+@`$s zq&=vY8EpV0*5(?xBc3Zqx+6}i>hZQ7>>RHByl}_C_ew_1<(^hwdvx#fFz%55kaCwo znUt7o$A{hf=6lT{{f*}7V~H?<_!fmnsYs}@h4g!^B@IE;*DYQ=LU0nEa{Nh#sQqEz5Wel7&~YgciMj^#(&j+a@Tb^$#Scyz|-h z3vXB&ZhBdHpy>qAH$(Ltw?#O4* zUtV%7-gxb-62fx$wqIA(Q-DCYv`tS5lQGld50az=lwYDbS1Oa^($>S5YnxVlBZ?$` zi6rb6#t}+Ie2w4Oob{@2E-jHB<5x+HBrrwC>bip+H@5fJw{wPRY0kM5y(T23;Pg%R zZvVBP*Nm3bWMkkanY_+ zGH9AxPTvM=7E4dXKQuGvp4l@yUf=Rr0B4sNbLUM|pE#A`Jr$jD^6q3MlJRwO4TC6n!9OhrnCegg1$ zWJ$@%#BET+Xp##a#44=3CGP1f{r!UlBvnnVDYubJz#v6yadm3<@}BX+`p*PDJ0sjP z-cgt1DTpaP$@Hrq)a^^SJGY{;_ELRK-9Y<~k}!UOQ;FYT1W+V!fgp^6ctC;+;WFgzm$O#Ke~iS}wJmPixGKr`ZpP#+V5afPlmh&0+QvQ0$4&%8|hhkTC+0zLm;7 z_jVk@Xn?Wm6B2w>K12c*mk=eojF$*A8hX%02!`dh!gM~CSFQPy6>r%-aZn?L@30d4KwX~ zSI<~rQ1ONkp=jn2cR{=Y?#Hnn$yREamqw~Q8KAmD%z#V)6)DQ|J zHY9A^+v9x2n1j2(1lIK&1SxV-w@knob4Y+4i4$q2Kj5h0ck*p5V^@az3Yr-_m)kn4 zUcEgrF1)Pj;1zOu!kmYQbAS$q zI;{8_fKgtqZ|G_%C16gSFUWX2IgYx<_pL7)^$uOEVXV36Q&{8V(B!ujm!&&0qp}}L zR4BM@xgqiJoxeJJ>16p-e`W5-)%pa|1x$;Gt_f!dF;pTY!*O7Gm$b-rY2(q7U$pql zY2-SXo2BiHY$jaPrk~Oo%7j<)e^Xgl+|oUL`mJa0iASg(0B0uM)ivns-_|!#U0Cud zf>T-eOUKhu&-@4hwHOv6g_j(sc;vaV#o=mIUH|Ke1WpiB%#9Cs?1E&3NL7R>2Z0;- zFZzow`iEWOK(bpShMl0~o?yiM(M_T>lIjU6cgMZ5c&Vj%8R32tX(EAeE%Mx!eFUZnzkd0%hbA?|-?r zBJacl2?^)}|BI+6-Bn)gs@h&rn_Bd#K(uocqRPxt*TtxUpF$6_=_GZrU72|3$j=`p z77s;2CJ+~K6HjVmxwaq*~~L8&Aq%^Kw~3HOnB2uFSaSwp7Sc}vO1 zUiQAa`<3TP)6VDo4q&=iRlPLjn~Yj`0hwTM1t}h4ejaT4K_!`V_D$pUQ{syK?~9;7 z(@pLXNs|OOFuoCW4!cCGMbaon7b#QN*mI@i*?bWp8wT@^YqqTNvqL$;IInonISeeiezX2>GJAWyEl2P#`TX4xYgw zY8zq=COtv?5rYVkG|og^LP-$miHKc8z`Z$*?kVoN>;{CV_& zclhiOugQ{r{OjPW`(OF>t7*qS0$RB_jjtSwi~mteo2OMW;{UIL>U{1yj|YDd^~;~Y z=jQ+rixFAS!jx*~hMqBp$Qr~*WSHm%5Oo~@qKb(ku@-w07vKmI@1aQogr}*4n=Uzz zWa$pHBu)`2!p(4x$s&n1Prt@LT^!8cUyyVD)Q4bp!}a>hl8%%|6ySlwZ!UT!r4vh^ z7_)=#VXyPqs;p;)=b~PC91O-eU`xOXiQ_y+$VF^jkRmO@fwK?rAX#Hykzb;tMYn`D z*Dr{#d}xpOFM!sFd3j%=k)$_>b%|BQDdD0`7nwGVf4WGxbRfU-jf|B4471sy<4-1$ zZ=;OQ8}$uF4UDg?EC*I(OS%szTCE=aPpWipcpq%13tq=bsZr@KB1oZ8(%?z?eu?8E zB0;dwOZrRrvGu*kKlBLE%C#3;7^OQIXW=Ox?zkgXG7$0daSy#kjfF%@NiA+sY3x6O z_h#IC__dMV-oAmF%;Ts36VMuaD}I^?7YIX6x;wAXS0Kx~vg%!t&Uv<$`OaMMUcBi- z>3DbBe^}Ri^ZOCIaG@l>prH6Tg5FKv=DO5zeeZ{AVgeIpbx&+CG$5jnSq#xI5||=G z;vF9BGNj2P;WkrLSR|@UKa%c4)5LgWDMUj;Vvakhi;&`G{5kyg)tQ;8rSi8_6Yl~;lrbwdNbp_?Ys9?NN+>iZ8$ z!2wxgO=D4aO?Abki=~C<&S#&#aPF*?wbt(w1irug!kkN@-+Z=WtGET}3v&!BpzpmB7u3=731RBooX zi7FzQ831ApU>8v_OdZqe30LF^r zA`#O~$#?uB^_Pt$RScu^xo0nBzWvUVk3ad#OOu;r_AysmdeiTXW9#F@vx%==^qrIC zRJQxtqPq%`52;2&{nsHftoK&<>uzc+QYI(H-Su4QOv?vtESS(v-Mo;8Ff<`PKKaq32L)rnJQ(~oDo z_VgpSE8{~6^7x}gfpvU(t+-QRo4)+{ILa1bpkfeG*YE3*b#)K<2Dgo?4kz8-)YskZ zaYl1S3mIgVp38yKo62*K;-RF3yHcw!cD;9hhHs%(*YM@0y8B7W={qqY{>WWFe&N-O z3#n(H&i_RmoNNqqQ+qI><6_Fg81By#Zrt|}IWgjNMDEAC@2ZR-FQ2qKxoZ)Q6nzJ# zzqkn{z(kG6GU{xIOcioKsh6)P6Mo!L&{|ph`m4`9@Ix%PC$MxVk`zbAjAm?-*NYIg zU5F~Xo7DtSe8%P{`S$8=e}}BSXVBZbZRp8(<)M={qj4*Q8xv>z=VW;gqbPTO(xJOw z=;|MT*9I54XQQ=6-x}MUahSLO+_4vhAn{2@?tA39pWa0auL>fMlPNAUf^5opZuPae zXNnbHJix!me_-nfL^CdN?u;xxh}kT1=(CO-%z2xB)aN1sE}5; zhz}CwR9v;iPEn?btUFaa5dGpGxF2K;xK?6Xny}nVvlG!=>Y4D-9J3@K9^t7PnhW}& zoA`~J@jMYpFm1^+Zn}m94VC<0B$(U;mQG?L!Y|gvp;xSCJKI zd1u*CRG`Tc&vgN@VC|bfdbB6l7H*fO;`n!2+8alBl7evCk$mW})&d>dzGrdaAZ2f~ z&(lzSx!PH>qqO9H&YRpZ5XEmZ#9=NeNeWRZe$iMN#S!Tu@c8U7w%{{SE#z!FclI7$Z%c}4n<6RU z&BZl_<2JXmrsUp3&o%eBJ9hSE(=HoMq!{`{RF>%6@wYkG@j!ym;pquB$Mi2jK|(J! z!SUlkD(R_+!^5-{-OKGc`A42;+M9oZ!kbLK(Kl_>gaoMBQ(}D5mvAHZHs%bW)$u$9N zo86ds^#`~o?w!_Py|k_=*dE>1M_Wzk7Ooj$QMLW#q?$N#J0bZ%LeeXjYCTmkH3hd} zP?##_2(U>e99R;OQ9!NmD3k%tgIV(TXfcX0FK*z5h*hgGA|swlIASU2DghlyIYpMm ziTl9FY@}$6O${~1buo=!EE&MHh|U#t2!}H=kMviTc&XFZx~rwNqx^Mc;*rAEJfqcN z-tOwTld%g0=fn|oD=cR6ZRuzIXQXG_{oUan=_3goXJz6^AnIRu^boOE{Li~cWlCy; zbzf|Lo*b9RH~m!}da%z4M0YYo6OTUie8bY5Zzen&^)RiHQC~b5<*dM^PfUYz?&Ic$ zvk1qKlpcRi%&)OB9EJ-$&oSUOhR-k2gEWKTr~DP9^a`@xz>+#SP$ z-nB~v_UUKeF76oc_3s#a1O2g^c$)+=Iwt^`2E|(-NsJoM4TA>1X~H*EY{Z_TB~Zx3 zh+Ozcz{olfo8_jM2w-)Y)HUmU3#UU4*i8AEoQwGyYMQ1$Qr18n5qp!<#*`2>6LC*5 z$oOB(d#88Jww!q8c@FQo!)2CRoT;~p%S4<6vZqitNp}v-2ByPP{>k0bBM%U((U&s* zS-}~0?|S`KahUO$9ZjKTS!35?a7w;V#T|1|Tk-fb{`Oj*+vwU8f%&N&*VaBzHzs+MFpGd3d1>6BUfzG`h-Cc$Gsn11V^|~A} z{xO6d)$X`7wp(j=?h!5!;&B6q74r#U1HWmM`Xr zW16O_~; z=@Bj)=$N-b-bV6;`e60`+KR5};)3g-dQ6`6Xp{L_EDc6t63!CT88pZPfd^&xyf7H- zkLnw0IF=+994JGsy+TI9%No8Exr<)$>Z)@_1p1r z?fbR>TrfP-)-)a*j2?N6>sz+i@$Pgnt%-;opc7`PFvu-#C{Zdp-r^swlzr%x*pQj~ z&6)BmzQo&$v?(@^KG;a<*#M@Rv?dffZ<|Q)p|Rj5Z{qfdL`N)b(%rcQLjJ+xi=#7R zTfq&?wt72deYpt%mg}QzDS9BQ9#r@fWj`n|1h2{0R-cFqG3kLKoRCkCJ-|xtnP!hW z)z=nkmvuCp5i<9kIs4ewU2rsLrWX%AkI$^P`EjBKIR}YHpMLJ-<$~db;v;8TnnO*o z9lwUdfiKPyk(?u_W4<`e9!W3#i6kaS-o^AP#zh8;zNt4ZrJ~y1w1%Q5%h!3C35wLd zQ>$CDKeSj%%7L6BycJ+900;-%2O$xDFJx8&rOS-as*EW>G=O z#pAzJCcOt?k9VQN-xD?T26O&7D~N)^4T&u2zL$b8%AT9_3C<|L=6>2<&CCX4g=6xe zD^Eg^@D(TwF#fR(TgW1{X@YILI!l2)9~zk@CZk-zJcaD3UZykS1DI}lFVc!9pLxBa ztGD-wc+cjN9!>S8Y9+F`#bE}YC=M&AQi*kS9VzNlgooY{pW%>+4j5OsyrMtiAn+I& z&oZ!iEuuGUqGt5!rNovKan>6ry)~eBRi=;NZaU>0gI_v+b?d zj-f$(i-}AOkl00bsM6y$OJD^5a}d2|`e~BzAwLp_J8qmVy7gmr$O|oxN zXEasxuwVJrvK9n?Xp^n*@cTrynDFE7ZeN$I zbJ*=?oL%u?;?ZBekXl+ieyuW2@#OPwTpU@zfgkj6kmsCuC+MCh_#*$}FW0i{-bgG! zK6GU5h(^qL-@93Q%AbzlP4JQ2Fu{X*;YA&MagQ+;VH`2JBJn_iUXeW`86z7(-H_`W zR(>_%?$K+PjuY4e%hTn>sux~F@KMs1x9)LyOobPOi?R7BtWp`%LvDxY@1rStB|UOS z==NPlp18PyPjNS%rm&7HDH5WjCuMzi!_#Af<#mU+8kaE^>X#4n=9~#-#b!T5{xj)p z5^I=l+(6%li&p$I^4V#%$M3q??W;d?PtwuHo_Ix-R?(E1pm_4R*YhfV!9pBHTEyyb zmlw)pGbcfWi>X4fWpqZoM!*3-F@{5*G&wB6h`%g8qjAv~2ce$%sNDcJz?dg(TZmPk4N;R{kURGKu>ixm3s#8QAW0y?HA z(3P0WrGL-@^|F~#!6iR@OVQ~kGlv`ek!4c4l@!vhgrh&Y^Vl(3<4jCS3MEt8IpMwx zJ3frn@0}bAjqjPGv@C0!bwQ9ZW8ufj#^FPUZp~`DXuRC?DvGetL4XGYvgA81N?&EB zl1XG_*T>rDH;~x$k^-glno9Bb01)b|%(+Ftt_((+`o~g9tB<$LubT+-UJ^uDV-RkPvT6u0L zPk#BS1Q16mgjQY<#o&$`lW%+V%{N}}ZoFtKe1g|h5MiPdoAA@om}hCjEG^*UgaLXH zcg~{M>2>E%;zPhs97PtHc$f?&Uo26^A8!m)%c~m73Imt-m;HwJXEO$&R`BAwt!F9| zmm)zm8X}e@!ig{zMHAZeH0yvIWuQg80KH4?@erMZ6`^di=-@te4b zi*HDI04m|GMZIyNy{V}EK8|@L3PJm_LuXcE@8NtYHZIU2PvWu3klXFk_74q>wB3d& z7Pt+gUeE(;K^gy4SD;l3J^0*G_;7$B!-V{OCXxA z|BwJcY4y;Q8TO7=I)|0OTo0LhQ@A z8w$>c{zPvEub3P~f+*>okA!|Kd*IE>p-WLkfE?`*HPCQqllX~7y(9U~!MWP{{^l1H z34SJ|WW?pk%FZR47gD5LCD54mC=Wlr;d8pp>2LhzH*epU#JPy;10EwJKG4?}=#}=( z`scS_`xQHXfFVoyFLA;nt4u@!<0WbtEs=gxFnXwC%CNHsM8HH(*~19nY_*xQq?VWp zpa=L5eiL;;Ofl0TFgW;Egoj%RE@GtXc|+_GVtCHtj{lwcR)o?e<|6mzMfvEod! zTi>*p*3EN6BO}w##%)RHGDcz}KKlGC!I$MPz0!qk^U+gnAxk6kP|@l+y| z3(-iILz}Mb;cAm+8$~dTd@ga=KZLKt>85pq_U!SKsXwEfhj<765(QlM#21eS24sDU zmVjAq)zPX9@%$F#n30LtMv_q|TmTo*C|s4&6I)8Kh--1?B>KaBL@vnzb6G^MAtJ|g zom#wZFh#O?cAsp5xGfqtMoe=gXcAMqjI2oKtVgk9YMdKbmTVw6IfKyfco>ZBLwEMi zhh}86=U=C7$v@-UlA#rWvgl*6#W{uIwig~#!iVFmPP`Ra`Am3ZYGyj-Cve>Hl*kq| z0uZ$`Iq{O(VjpSl>^m>|E?nY*YR4dPT6=NNJJ*?6m<`VK{Lv5rQx%?Sd7Wr6(jXcH;79I@!`ATGh#SAN@|{! zz2M=vR9r%UQ8SU~n0JZ7A3ib; zFuR8ga1_FElUe$TC?#xkbSEi)%tlgTRoExAWnVg;gbN%XZcgON~5+YnenE=pT#48c8B~Z8)WfWoT^e&;x{cWNKmuNU7h4e$(gx$y8 zFV5{88)}VN7Lf*C_wpV`x*eu667di-BA7_JqjOxiA|2@#P!?8s8>&E= zw}c&5T#L|cWb?Q*Mg)Vdh-N+R34_sKke!SN4R{-0d>U`0*C++Ly?AP-p`^Nkk{_w&Ejz$l^}TzDoAxenR2B;1wxFW zI8=-TYKoAjhM2RKjR~zVGeB%zb7R5Tj1EO6;=eUy`1KtKZ|A&o)J%a)Tv6sbkX| z-NNs}ORf_5;W6kRt+31j-9-2CUbwBLd1fmHVh{M2-sWbRYeclip9n@$3DgnN8+R@` z1hdSxQXZ^|Zg`DC6S5E_NL&>8bBv3ai^@iZVY7tkttSIG<}`W`sUsqj@?&aRPIjA=+k;u~lE_|&>fCad4F%Ui|bRvHlT9y()j35h&W!QD>Leu+tR z(oO6>f{-|Y^9O+@-bnn;j29a*8X=pZQm^k7nAN>!WY4HQmt)gn5-IFc*AY{EAz9U1oZ?HuTPkXIluVhNat zaVMS-vm-Pd%H=QsN%ln=Iiu4fgaKn58n<68Gb9|mo2fTG2wqq(P8=aja>#AObuEFO z88hKF%4u)9&crR7o??VQh|^gl#u`PJc$%0^k#yIJJz$pFj6uWpbpy^*rSaf-8Y|5V z!)Y^33yC6$#NtVO2R=q_n??H&_wl&H$eDySot*GwPycgC+~RPFjIcnms?=8`D`>ys z>yH}E7o8NKMl;E&0Fr$Ec+5<1Fu#t^ISB=Br>3vVCwqjlBAY_XDcQ1tHmLj>BaSAD zUhu!{pU7)+1$d^3Yd1a}ETO<2;BC@_#MOLe2oT(dj>&G&s6;L&))xJkO#sD9hfgi} zrzE5PD>2h&s74$Q6FSrkflge-lo@wqBVO7MvI3r zb1|tU{m2D@tS^zUDUu?udT{Lq^uv0?AL^68j?C!w-q{;O1&1)QN+Wt>cyu#)jaQqS z9VlkJkvB{Yh~J9DyPGqk;BS!6HJY8WZC(EjJ28c(|tUvMs7C~90E<$T$`fLZ8<@T zc_P7LMB+x8V6t>Fey2tthJuySai6yBg zg^y1v>yLg#qFG4d;h)Sp{g$v}zkeDvjC?#|AGGh8#uJ^)%IL%e)5vyRW!$g30?j*H z8{auy^?L1d!*do}$h^(E{!`-Qh(!~X#C5|+?9XXrs*VUDf8%^@z@j&m9v6ZyXvfYst#0e7*h`jnCBe%}!5FEEL4yF=~OJ zAsMGplAaR_f~bi|Edsq40i8sHh>nN{b0#n(w%rh8q0xzBWCLZt*z+7$nmX!6%pkJ+ z3C2z3!E`&PDUzLK6D;~UbRP>1&@i<_!xsgJnGZso#>S5bx;4jXG@6X_!!I_zQ;~mK z;|L0YsBkUnJc%AEjOG)_Bk_o46nEn&Yu|#Eie|uRb1xY z7GCls?f=K!cfdt;bba651?=7ANmD`ET2N{BSYogAUZSzZZemH)*sxa=5K%$Iii!oS zAc7PH?1&W+1nG6zzRT|NoilSUV9N96$@9J`-)|RK?%q3d&i~XoGk31rw6rYklTRK! z-gV&!BEKqO^~Eh0-&K`Yl>dnFxS^Pc>dH2QM-d6^gE|uh#u|i>_yG@hDI5pWCW#*% zq7$2^IX|2T=Mm+@NP&qd{Xu1c^^sIKfEx16t+c_z)Hk8d(S(Uk*5I`b>-Ufds#RQa z)SJ*ne!+Gyk8)n;JWJV_>X+ajb2dEgKyi6-QBi5ZGMIxU3~)dJf!H@#EaK2Uc5>7b-}i1rpC^aK>O6w0c*&ahvTfrI(YDrMk=Vw zTVdZ={zEWG%%G8qyA=km(778zT`&6mn>RVxZ!R^(-Y+b8eP#VaH_YX6(v7^a6WS$e z0eN%9k%m4HfQIo1XMcDqhCnXBA~n!CRm_GY46Pl}`GoC`a7DW04q)RAKr`G&G(0xf zhCbMFJN7{RATd0eg%gvaDo7&_0*Vb1`o3^J`|j1rjW^bC=#T&-l$6_# zN@G3*K4eQ$4dgdS6B0%PCLM`mYcv}{oT%61JYQ39jvks?9Q95>c*ANYE}J2oP=%rx zKwo73m7qZ`r;~9=2d&g$S04zP`dRLF0OGx|C!CX@8iU>xcfBIK;BWMc$b+HFoclK- zH#fz4-{Y7Um8IpS>mgTx>#?CGl}22diS#%ukG)?Cc;lTDYGtTB-f)7<5(TF|iE_^+ z8q`j89-rHXWM@9jd|K3iciigaH8(aNIpi0*2CYxqxQ7qXJZzVy`_$1Ppu~%C%pi00 zt0p5O9HF4w{czbNU811jat{IzU_p}-`j=$_Cr6MPbmEk%CUKNvN8AW!ct`|W2_RP> zyQp^D(DDm+mleUGTjSO}+?>1*JbL`@d;2oaW>x3r6=uzc-Kgm*K2QcWUEt&korBoi z9M<2yeHZaoN%a)k3-0i63do00D{-fN{6vHW3=uidKZJm8ZeM4Wyf1iPQC=K#Ip|76 zMC|ssAJ47WvSjugSnUoM;;xI>8?{8LmYDpK_Gv;)I>HQya)q1{YsHH(En~T4EtonK z0x}_r5JTI?h)3(Pgf{B^Nhm|Qu`?u}b_8T`eu|weYv2+2#s<+-afH(Z4VfST2bML} zd31%y>jSQv4qWJU#bJT~$#bemo6F?VVk%LzpB7HjuD`sMz=7OCUK4uV&jFVKvULlR zh>`PwL_i!!rBd(Y^3B`IvhtGh^2)NqbH8zQb9QodQIIG`oeUz11Y*4mKNE4m5fBzM z-r!|fAUT(gYJ=v7tL33@PRZdQol4~XG%6llM1x2o(d+=Gae&Wh2{9zD`awexNkvcw z*h^!91_^+cXyV9OB6V$4)u}V(ez$aSb#`=f{d&pfY^_daAeDaD2$eYP)Leeree*=?P+?`HnSkz~sPGPo%3uWx0>J`e`!h-ju!H%#+0LFRF z8tCYbY6OWpv@gV2O*}9=oN2fKMNiBHbrXC6(Pg4wG(Dw(iS=!)XFx0ABukAO{RmQr z{UF7VgP;%)JjyrdF}9E2VL+x{9HR*S&)&TSXTT}Y4l=||0TXwIl_6{%r=#56obCN8 zRG`dKrRtQM8_J8^bxS(HCP=4kJY1X)rJSs6f^RFpH}&?7gk^9uCve&{-KJdV=upak(wG?CK?8J zcs@HN1fM|6gcb6eAPHYmmx_5e^+w$Ph#`6=3Lpsq2sLXooHijc#OBD=gk$K4B>HeE zvWY8OQLak7l+b{UPB~uk?p*LHfPKgNF|v{R!5Up)%5MQ9&@7L{wOEP?3lz5H?f`_=?{0 z!>$8C<_O`KX(K|^EmHG<21(P%0U2Jw|L~^c4s5v52+-7!I%3wi@g3(Ynn;T}GTH_8 zg4848L?;I=9_OIq)=^-u=<*1i$Q*=Hpu%azgS0!3#1cu}eg}k##u=(z*lRl%w@s(+ zyh-~tjPKk_I=jx(3DpAyf?gd14}8$zo)c3KN-HXAFD+fUa`k*yB#%w6;4~cZ;E)W@ zMuy4y9q5!Y&aM+x(={M0TF5awX^ofRbu>(4f7l|3%o9b(`e+h6aMxn&15KE~iIQsK zpqqJkdU<(zq7o9kK&&)GP+ho;PcQ1paKA2mJ1omC zDtKRbKQSpOJ&uuo^tD|@T>7fRU!eBikO$6Xu`oh!jB2?QZdQCknOL)Vq zrI(MdkGChRxb*b)@$vEigK3X_z>B`IN46SIs*Pv~2rA#(*So!&!;~rZE?z#~T;JTS zDR2T;k6`Bw(KApFz!?Ro3}`Z4M}lhK5J6CBTRE(M^X$cw^o%F4QXTt4hCU}}VP8%j z0cY+$I`(tazi2@H!kJ6AE-rz6i{WJU1;S!Ia0?ITX)o1cu|$-mdn$Nw`)jzhlbC7)pr)q6^?;hFBYH=rj8x+%4=20HgS)kF)4Ca-Z_wtG-eWv`;b7xZKso?oPe%1^ za1m@6rPQ(8g3)RswE=_`2t>;nGIngt|;j+XQZoF*m!ixY$h2v}o6Bq7Ur-1TVY>)fa;x z@!6yU1gN=(i^H;`7b8~NDj7ki0d78AE`p+Nh}h5sMg*GiL4%wHLWAgJr~{(;44q(r zMGN@CUk(R~i;G@9eVV-q=20hZXA818Q{Pk-b9N*ntGltzOiaeJ@0}MVBBH2 z5A3%><7?xP06hdPxI*;7Yw6X56mDS$Z4bvfxKA573D1I$BnN{{2sV_KmA$*0dOr&9 zm^Eo^1LnRRX9r)6jk)%#Sbz2**3tKmB|JL5!W)gJ8ukT-naP^!Y<$Q4W*brv-NO_s`o zBph%aqKbYDyMjy&){QPKQYtIb-W24of`JBFAK3^UeAG0*oA<9?{taM1{{^tI_hQe7 z|1gc*PXUcX9Kel|U2cfWORMfZjtaY9pwLNLh*jA$aTNke7T17rw`p_XoLT|khQ0w4 zH=3ZasTQhZgMI@iXwh^#X84D+>hg7gqMn!8R?6u2_JRiq!3k_c#qvAyjiW+j+}pX^ zMOA%$J@0vY1;g(&#Rm!E);7`B=o}lKjNefR34)|4G_5Ankd^Pe+NcfL1cScyCt+jETb|EMJR$BUr zh?|F>7HZ$ME{gX=1;;makYt$#dj!YR8nP}7&p<}W(14p@qY;dX0r{h zvU+BxglT1lNdjpWP4`2=;IQ7yskMi)URTd-mNWd8lfBV7G>k$Kh=zq_KnG?GT##u{ zZETYwMnf>VI{Y8+rE)q`S|Yl9?Ed5L>`^=>+N6s^n5^iK&FGG{)`7N4E}|i_y?t;+S)L>&>cUL2&mWYCpfT8x z*2VK)z4~Ed>Tl}o*k8l;WOPoA?6@7PZk)zAPdk`&AJIJ+otRmzkV<8$AUjk@)JaM< zo1CFDAezFqKnG;p7OHF+fI^Lm0PG=DG-e}<=wtXEc43lwW|4%^=Z}UPrQi^oIg+6x z<_id`?@-1@r>xi3GrlsW`A8q6g@}lV7O5lu5#iEqFxBF+a)g}CLEuYF+DIR`lOsJD z`nBIsqbRzsyt&nzU5rKa8GWvEWSvx^c$)m1;QpG=jfts-UD8C$U=Fb{41DC`yf@_u zLK|~C?oNeTCRK=F;|jI}KpjA1i5@tlh$8vi3N5M#7`V^@Agt68P<=;;0T(b=&vDlQ z)+jNf-@y$=Y($xRACiN+MTqtpDN`l=%tH9H-_>p_3;3klbAw+pJN(3O8&mGYaAcKfBiW?apwsi>?J zKlH}M9oVli+p6RI^XHEvrU|!hWQt*KC3gn##Mos6dGL`8=eboVv;d6ugYY81a$2`S zR@mAlB1!xrggeh(48In0*A(>r#d{+MA9DDCSrk z!-@f3%()k;qV#*>DKTB7vwd`6gEsIVYM7HaVXuSxBT0LEN_m}%L@6%-Q> zVjfECEBLRwho@5uhS^U*ydz~=8T#N+@)f8&x(!--q)roIpt1xL07_6%X*xos3X8YBnLLF=kD-*fm|Z(@FeBe>L$O0FsV*1Z7r)$p9YIl z5hkSnWA+pLadDouYFA9uA>0WKplg`n(Ro3r@PA1ip%YqicJp){F zf&mW~QoB02&RT*uwv*ma+>K%dib)ZJd`apja8lD_h%088>?W;<1bx8~8c9h+zoY($(hxRehbDuT(aF%|d0;=JM)*n1bE2pu|ZOifRFy^MA068|@L(6Y^ct z%oVc;I)mgvBOrS}6*#JW!cH!ja&;rE;{>I%5rzOMVLsJE8Srjpqfl=5Zwpyu7s`z3Q_SI^a9c4 zESAtmuH;aKOIZ@bWtsDjZSvLpI(z+{GB z&FHms0vx%)C`uRo2c52`0fbb6sa+5w!Eb>a1)4L)cy@USof&X4acDbN{CEOQJ|@HG;?>FzFVr4$r-s$ z!6AC1 z28f~=9=Ut*d}>_SQa7yI(6oxid=wy?a-o!gD>ef{{i2d>G(2K}1W=rfBZzUk{SY06 zORzmEav}|1!yKlCInKtA6EHwfAH(;Wq9+E}_${0~o2(!J@zJMXpi1En=>~q2431!n zl0x7>D5z^FAJz}EkA!u1TXW)YRjsJ3RY}pStcUT}pCu;WP^qNtWJ*PCRgL)l%i5B6 z`R@uvvfl))a|39}m(N`aTkJ@l=GBn>00m^mzk!e@<1CaU9i^fWxwMVkQ_a>%FxXNf zKve{eab!+1IpR%xEOHp0jW6O)d=z!)BnfVZh&5}E$qjT(H_n5rsg7e=firhZ1hHUB zLk3Y_O_Pr#V32aHf|30; zP#v296&V$G_v9Q$c5p#Pavv-UrkNPgeIi{{LtHIIGZjn)Q1OeBm~2*pEQ4`12>jS&K=SDa=?JQiw77)lOuZ0Ejz>UCN7_)2^&qcc|In& z;KIErv(#9q`rtx~_7bL=;R_~v5v@hlK?bRui06YYXcmCWT*=q)o18^cE+Pf?{w(;K zdV&UhXXOn85J%s@YuA1UJh#Dpe=M)wcj@(^)TqdWOOfZ3AHU10C_fF2BiSTX#E;bJ z;$JETLI|C<27<}C)M3Mg03zWEzd#DH@dgguaye zQ|bfQJC_hLnjFLJg1$Z9F@laBo^VlPP$CJaC-bqCsbQMwO~xmCaSDKlai9`?!C$m& zNpIyQF#x%z3d4p3q#50KLrw`ob@pf#lKAkKo+mLTHT}lf6OnQEZokN`mPw@_NlGfj z3YlD`P%0!prK{38tz@eJz!&)h{UX%pvygFW)P#A|G#b57k>Px9 zcnWy9wX$Cn`QSzq-`hc66PT2IJ?-(s)R(U-L^3D^ilsb-93EdHFMgGBb`kD6fQm)# zRA&tq4!|4|d_kDtyeZ{^xL`DVR9rTr#E6YZQ6oUV0;EwZM8Yw8!>JH5yzN@OXIgak zM#Axp%%k6;U(+PmcPLNJK=n`P4Y1O52+gy}SzI>_BSkt+=1dUXH2yTXidqimA(7Np zFjuiNCcZI9wULRR7u3{<#0rH{DVG&V_2ngRlUF&SeP}2gEYSomY!L*C;gE7pJt2lr za+qGm63SLtE`1cr|K;Sd0(a_9R@{kViN zXzu{=2Q`jk4K7r{58uPrTEV~+@5iHk00v}24Q-%o&=(M=OUgzxok5o#zC7vFM?)*-@7<3NzlDjK9ho@D0q}`Y$llr+if6=6o1}Y5JnvtM}U9CXj1wNj`KB;94^vx!N9l!xO{9xG@JH&PU z0w1TIP+e6s`fbd|dir{z$U!M-m`80SJ7=;ilj{hnRQlLGoYD7gub)Xf%W8Adk9e?C=h!fIM3k(Kgn2LLF@s0hBSP>E_DOs=>Htv^ z?4R}xTsIEz=;20dY_ND#8uCW=@B$yNv8Jt{YNh`#Ydc?9l*BD6VrvQj6PX6VS>9$| zK8^#swQI(2&l5Cn-?iUZS09q6L4))$mODI!#s25bf7C?l$Z;RF8RZaYOeBm5 zJ5RF2mfj6r#o7}gLf_NZ+udP`!Q`oq?yzQ@bL1#@>Rc$@s0-wO*2p++(4G-9^dTyb zW?t@&Q^t-MY^$@!1F&1c1mtdgXj^vCjwYEBkuz(I1H{e@)WF7Sd?toSmH*r6V%waN4fW& zWw*{*ip$>!_(&~RFEDU(^pDPPv#7JJw`qqMSzGrXHo*ym0o2mqz%2o$3PhOJ+H#t#KNG`6j5Y^{5ZTylAg6#%3I=9=5~{pyIHGmHVLWPt;6izX7J=k7c| z^Lg&G=dTQ2znDw!3#Agl8WA{smx%nhrzcT~l+aXbTWhQSUygI2agUCZ*||Q7FvX3< zhAn3{WJ9QI*eUfKRPE>y>hDQD&2CKu@}fBfTSfrZ5fNO1LJbGy$(5y91w@4Kg(#6g zgjcXx8gZeR8IV;fL@Pnku9JuNx3;OX{T#&K$96&VgPj(rW%Q>j+OTmnY7Y=V-9Ct5f+=YS$-6CsKx zqJ$iQt&!)2BWU=hY6|khsT`<@czU_pj~Zlc4T{tOFaoan_VZeko_^1x53$CUaL~df z``x+2^WiDXoY$o;7SbB$+3&?~tDjwO9={UT?mjLm&vESy4ddHJ zYx`+`%RWQ^-~dNXa;BjX`9%dl18MZ+Tmpg8W>t==R>M+VdM?IOI>uldK_~FR?)z|e zn-?E$v(ovt_wn}fgo$55E%`-_l53c1FCAq8O-c|Dn~lRITJ-mXP52GaIgA+$eq3h@ zt5I#+AX1ht$1eKEC7$eWttAJWTlP;ZN!k(k9Xv9NjgkaA6sLW(IH&Aco_;YrzXOed z3wWptT3Bf6;pBJ!&fRjc;`vCB1&g1p^}yZU{rXy?-Pc>|^&dLc-c9Gp`XL(ONf{SK z0W`9FqSVorFvM^*UyKR((=fQ<%#k9&<_Lfr?!ETWM(6&0thCU23y|5MkrSQ3$zhhjV5JbK$LxUyO3dY`E|Re; zg^`_WGcN1M$!-+#GjPmo0r- zt*?%swBz}QEJs5De!m`CkF1W6uCsuMm|#SG8V7j=YsQ$|5{Ytq z59qHG@l@8<8Fkqq5lP|87me&^VWqTf4$1ETM97^Fc|H{c$4%o_2Gj`A*n!<9Tc4mf z8lI4ecp`zMMBBNPG3e}02I;g>)F2WoxXcrVi*PpOh^tQn>4Zr*uJi2S?l^u}fA9%Z z!mOk1+vm$gVduj_4uk}sh`;mZU0&JoUXVz%vgs92RalalasSbB*l&jFDsiD`69UKY za?4&9Wl9ZHRXgni)Wl5Quwk%{F+%XmYN&(s_k_qPI z?mY<>JQ%c?;*HE>y$rMqfo=ed_)$9kLfOzOQ5tQ)9eKJtPaZYc7E%T94TLt^7B>BQ z_jlR6DJ}Vo?m)|s{imMfy?pWNrCT43ZD$+Xo{LM0i(lWplNRR__}S(@DP1R|b`g zev7fNkQ)39&_b1jEZpM;r7V=l3_2vt1~3uuaGpA5Xn%kPNK)9gwzleHGkL?duv=Rb z;sF^!$iC>ey4Qk&OMO9uT3gt?DlaTax)6Qc--(Ee9r%G;61X{^D}I^#1QtH1G$)`0 z(S%AIk?DCjuBfQcmDP*&B;wp@J*fhH+{g3Dqlb@dUY|8d21shm_-oZ1C`LaKZW6(_i8Xe)cb-%v0V;39?yLTY?+7^Ns zfB&`T+Nb=NJ{BY`we2@3vAU=@KkI4MOUN^cVp8Q4*zbLx_cHepK6w0?E5{Q*1IP8(~}udfvW43$l1 z-PUHn(67eZ!4f61H3W(2Tqzp^7cxuU=-wOS7garhm-m+);TDBX!@OZyky>>&=Q{fCpEKGr=F zJbwB$lm*)LSTCunsKlp1RUE}FWyvCYg1?^gjEsVu7jGX*n-`{E-{Of?P_ERAqC3JX;DA^$!5Pe;Y-1=p;>oAoaA3wZ>M^%eM;d4*sYxzugoks)$5kzEa>8U-|xd;X`T5 zvKMKW&(0!`N7s<8BPi8wZZ|97Zut@^?Eg@IaRB7zAToi~;LGW{5N^v*C{^{@^Fj3*${0uuOszPqQouvu;BHHuq>Vhz7NA1)vhcZn zMR&RL)tIAOPBkKREn?H@wYyp$_dC63rPsv4mVLqX1Fj%hlEk-%zsN-YL0^6~Zn8b# zrUTp>MoT)dQ8w4cY_#$IX@JdF=fn4fElNP)ae4qPmnay`#gSApECA|peBHtW_IR2& zX2g&IASNyJ&46kdLoDn!V7S+!b!QIk_`yFmCgx|j-Fi@3RaefRdzrLDG4C5#EsZTmh0Yi%qx~kBy!QwftCmt zjK@!_wAJ-B9bTO9YfvnNYt8nLLf2dfKXGW=&L0+dj~>zwa#XCQ*~mc9?Eo!+Yv9lk zqsIeY@W>R;darsYx6qu3WTxyba6krv*0Av_g4B>d!C?{_02a@ddo z5QwcAuqo=XHMSNIv#cNl4;bY+({Fd+wQXy6pWeUg;;(?QKjwJc^}1^vuHFjnORS>D zclkNl**VX%bkFqOghNe$*0>-easE16n<-C|8Aw%yyX`U4WYZMLO&m7Vlevo4)GLfsHWq$`Wa-hhCk(L44Xys&=vlG&bE9CrzBF9p7o(m@%Wj`f9}RVP6gzJa9mNEZf736C@Ylh3T*Z^#;tyx}Rkqm^2vs z)i=%?W(WJ730!_XJordZ&~HMM5PG*-Ri;-eiNc{*i@w93XvAO+B~Nsjf>%*a01T}g`I-4?vSw0QL2gNuJ9&U9Mi)JJ+Rv+6T_wMwycPhet%PB()>hEltEyIA z-KO^Wscjg(C^|MjM`y#HP0Rh~g)KS1>Hfm&3j>$UTsVBv2pEF*>IVWQSuye2#CfAq zX#g^wHQ0(C_#`W9jg`u}8Tn)l6&tD}s2@aCAUfK932j*QwdvE>)_RED#BV2WU%G#O z=-k6|L)IMgUwZZ5!V{eGUa1tfmsLArwb9Dz8u($cwBo7uMdutpd}eXO+ipv(xTYqv zCQa5Hz-W0j~%SIyLF#d@lOrM&?ie$UM+Rds7w>Dg`jIBbUe zIyRw;BiAR*yRqP+U&uMT^Yahy_Soz_%4O7m{{3w&`darxlpr8u0Zaxz=s=V(4h4!a z-r?vA^$Ya`Pnm?rP=g?HTUuFI_U}Jnl-=;fGpAiz7`rWOTKLAmdHZJ{`1Zj6h|aY$ z1(k}@_L6`e*h#OgvYP7Z8qxh5x;TS`bDo%B!_p{+@5P-KChs*T^XV(Rsm>M3aE@HO_`=jv0ka=1y|yLT_wf4PhtvAdThCrS zSG{QU{7vdmoWkp|sG?j~$}h`)qRY~In&XcFova9P_DxrcbhVka5ULO>U9O-gYdLHX zLRX|<9EB$vxjzzV%Q}=+!dOHi&YaU5h1q9fkwjI~QY6W_aFiOE{qF$m+}ul{3+}I+ zoxI@Nduv9=9hh+7){1eL!WRykzH6?%t&@-am?8bY96ERq_#4aKy>)$cRt$K(_FxCS zEqcS-qBqX6+FI+Z>if3s)7!$f|A0Y*zZ}|s_rFVwCGi#?j1j`2ux6*D)H=z7PvZB@uK}SUN8C;)#{+D9 zt1Hji8imL}T6|*te}mcW zsAWUfrtFw*8I~BZXq3~dgo7JqO)_7W_vA!a@VwdcoUKsebMu~RvOat314C}BvGZmx zIC$u4&Mm92X8DC>qz?XiV@&4Q$Y#PkSSy@KpYqFVF zn&*O-PiDbAk~pY_yL&f?6?L#!jMcBYT)lV25za2$w7S5}C9XLin=@mMBY|l)K9=jVXI==?m^m}% z2JOF)b(EH( zxTK&)-DrpET)n)KXb)#hr05bQ=jqRDG-bRR30Pt6bJyNr89f85W$*P$-#uKm^$%O% z{|-!}|67U`{=nuD$`2%(%LF5JS??rfio%4fOR=ff2CG6YLhZo zza;S+NGG`4sbD<3PNL9A89=F}wC>hO5Ul0!tTI`lptwAKxBDSF7QxJ$F8gVLz~5~WKQuO#1=BsNbw0;*(TSEK}0kXF5VseNUTcZNKK1l^Os&FPj- z*$9#XwrhN_z@Dwl;=Hh3ghqxKX~fNI9*zUs?ct}9NO_sc8ESnV&uXXJl}&&8XM zQ^d*5Q&OG!*!CNiQmHB76~EHFsDH&<0{J$Xju1E;uc=g$p$hTb@?8DGmn)}YxT99) z=9;RhlWN2*q{p1?uuUj)pE{|cDvae0NcMa`>j^s}+Dyj)kS z74gN{H#N5mQbUOYBD11)yWmDrNv4GL)3RKH_j!Ba?gx@%K{>#AW}doES|@I%%^1?h zw%4K@@iX@qubv$a`^!KX_FsUq%s(-+=25ff-}biY5vY@CrF@wvUsGUEnExGaD*@}Z z9G1Q)lNVoKfqfnd=I()YJbbX}$#6Fg{)YY0ArdBO&tBb(>Y2oee z=6Fsmmo|nRNp2^&OIY`MA*@Bh7rbF2R;sDwRn&mV$n@v;?7cPPwk%1T7v_m-oBB?QMall)^&X%#)Jp7sD6E1UzqCYCtS7!|vn*8>uZ*`#yqLOW=bsHk z|33o6j)dIuSNeJ9&5kL{AcRytxm}ms`tAy{mpE$tBaNu3llT`=hm!pP2e$gq{eb}n zyMUSsO&L>JCl+Wg&bUuhAuS_wm;XNjfi-(qZY=lH+xpE4g-j#i%PMo~-U{Bwxwt__ z;pTF+wx)4}C&_Kgu^)IaaxSFru-Mnl@v0iiEc{w23}E=OXXm6>I$Y3ykqRq5lJfkz0)ANn zEE9wpw%yi}n#!80%qljSl;nQa$o=)ygr6LUlYoiZ{_R_-MQ7Zg&LeR3da0}v z*VpPv|AB&Uj)I56dhL7FyH;gEum}PD{ar;>Wp!qC1LGY(U_2$a^oB3&KM(U{4nJk9 zUg(u6|DhVM6Z1;))dj7Ja^~R80I(;lN>U-t6gM#5`9sFv<`!oyv;)^5aCbVAUYD$& zbhkS5A7|`T=F*CSLiPKW75LOmNoh`w>TQen>+!|`hc#k(S=0FTOTMe>#GCD5TE%f^s`?f` zKJKzKUKT$&;UDAM4S;EBSxTZRsYS{SUznwInwKvwB{R3AmL|#V@^dYJTP0J8_rWs= zJzRaRr>RnyBx$lNd2(v(wtql8$9Sd>Jd5+Pl{wAdM}nB$oUhAEnx=3xgDu{>dyoH> zTX9J_Z*!_-GUeMfF!#u}-(96DVM?VXvXaTA@1LJuOJ?i;tXlIQ$zYIUy#Wi<1rHRD zn?G9zgZHU{@~RIQCTW{Y^y=lyR|Q3d#V|21fk(Di7Y4!8#@)Og7F6a+b0_5$-`=}* zBP>n$J2PuPq;#mjWyL0}ZCB6Tgv#3)kC8@lWRM{Pa`@0=I>Oa)DfRjP{Uk5eYA2`!t z##@cNP?4!n$xgtQc#@KfZZ4PZR3}v>jZVFtF#j~H$@#x0zu$jGet2m9%eMx3Sw2q5 zTG@LjQ&8H)c_j%xN3yCPR6ZE>=t(dxC;Go9z3+cSdf@5k7Z>y|opL=}Co5<|cy3w3 z#rxIimFe|qBeQO-`P&g5c04id#T~u0gd-J-e4v}*F;Yee+qEc}df28!$K&&OJU*j? z|J95NzN=yindi&{<{^{8q%+sZE0#%MVwua#Rpugdk~zX0WP;!)oC#x&GH01%Oep+4 z0{@3Fdzm%NQf3LWg83ePmci#AnAOZCW)-uJ+0OVgyP2KLW@bI($INED8BfNJnaVgY zlbJEhSY{&pH;Va+nZh_R&Ws~`>&3V+zVLe{e0E{RGozVFaLzKgegm_O*$VCLWcEOx zdzk}FApEzT*~M&xzE(lMOPNK?V)(&NbC|iz*U*v=Jh9%M8OID~Y#3W;X*x3vj!%V_ zv7a^2;x=Xr&=vrk90ra~0KXZ4#1rNbQv}$kc|1nNYtA#}nemKxU3rGQj=XlfHoO+_ z67clk#RpXD7!5%aw}DZ?OF>>TK(>-8VV*IMnY&Cp;CW41(?X;USeh7HTy%GXa?7V1 zgX2$~jE!En|5n7BbLSGyM<35vm-969aiLf&;!9p87AH4L+Y%adXnlrMR3oT_l~KrKqJ; z{?j-M^YP1;KPpl{38<`AC2Da$Hg4|auYdaKkz6L~D6R-Ob0jP>JTz+e{(+|7&b(a> z55Z|8dlr5p=Gz6q0rS>wy{Ht`G?%>G6uxtJJUnEiMkLa&&3%-AYWYW55hqWa4LN-F z_Qvfe*Q`v6OFA-Vd)T+~x%?UDUurY?kF#qvqCb!4o2!s#rAS`eS|au{HZm7>H=2}H z^iG-2&uifO`n3PQ_>P!2_UN+6)5tedsK^&o9sk@+D-@V_T_;ym@hYp8q89gVCCxkH zv3~vk7vfIP$lyjY`Hq3Th5g1Kl8b@uGG&RN=AM;tolwuzJhmi9{g#)Lrx3NsIy>D; z*mL-_=Zb&6n(w`oEqm57C&bj)+}x~N?+YrGh+kV(sxA|hSIp|75(*4Qq`p)?kZdS!WW{S7rjzFGI{OtCz#V{Y<4it~;?^XY+yPsS)N@^38erWWd( znEHz(B5e&%RHl9+couJMq7Vwqj84?nz2_H{iZnHPwXgilyQ_t~uCth@ii}S8Zyr4I zPm=pDxIYw;qkL8Wnm55nh3rSvR#t0k_|@4jv@ZqOZ|u6sgaV_Hciw7V@n4pTbT#^- zT1~X2u}a7@wd7q%RVLLZclf6xuqj!2OK>N+J1Ba+X^XiD?`uUO@w+?Ox|f1iZ#NoB zg#6DJS7`GDxdjz~fL?{@dY|qJA=8bw;-czQ=ac7R5C5Z7p!3fS;Lwf>Cr_&*`4?Rc z<-(Sx#+xg}B9Z*z)e>EvpyY0EQwc;nqeGRtcP+~TPu7WQD<5v`(OoWNjQJxHlhsL` zQ;(ht`$r+r>B*(!0J9z;x7oBV@X+?NkJQfv**ANd$%O4py9cNxweqKD-W0wo${uf6 zD{R+&*pni-jd)F{RxDG5_A-_U8FRkz>0I^m&UxXd8o2il9b~#-_TM_e6K8h1e!|;a zIQ`_F5R%gd9@^u7IlJyT|J~Xy5@8$D?#q+%HAj!$&dV>|)}=<+=8GAUikgQjg6hgj z&h#(_W9a6cUU&PW#D_;BIlixKp4-oCqDPwO@0-|WN*;goe2vghC^YQ-&HgY<=7I6O zk#|zHDS|tp#5C?jPUC^BU zDDrX`<^1%CW9#}s5^QYpeT`fyX`_7c_e<$FW+X`y1$P6CO|-(crY3`S?>j&^4+x8h z(w^nVY%!98D(i$T&AM&Y>^l~|AN0LS*wWbMO>FS`>GeV8CbdF6q2agZj%!b}Idy7kjo9D?#|2AWpmfZL){pLAY6#uFRg!R_OJ*MpqM}Y@~uD_*= z6~x_|LipASo0}QUb~<@<@uF@O!sgxF=N?$+dDPTI1UwrL%gWKcZk_Y=05TnU{P>T( zjnqOx_hD%n^0c<;iAPTUZFp|Y^1LNEWX}8r%K3r?bFIu&!j5Lf{kI1M1J{SPzs`R3 zDmSi&d9~2M+_cZY0TxDOLIdMo{jGa(lW3p*GFW!<}X|8C+z3Qjzag`?&t5%sOnF>zGOVAeLfFp4%y(DBo`Ay}&t#+~KUmncQdn}sT~$yH#ety#~l!C{+y&xb}__M6((OasPTC*+&V zSgrh_{fc!PxBMNo^fthMLwSiGyP=yz*viD%d+YH{$JZ}8y3**1P$z8FZO+YYOJ|&q zIQ4B$BM3J}j;EDJ+aL2=y8Um&OWQ^Pg>MQBKcWufA9v_37Pd6+Zn<{*+1)eOj5bDQ zTAB<#xOVmI>+2?TH4_W@-6!r=ZSAmS_tGE!PRz7v!pzp7L{$R+j-8Q6*vz!M^C}P9 z9;TOtTKLCg;3T(id%$slsa14>=4Pk(n^$Jd`|{v7Z`$d|?f+h1U!NKX9 z8yj~wHIEU(=+@lS*bJSXz^u>u&6-V}{I%mu+Z6xZ--_yM?Z5X+z{=^C+h@Xete>?jV25@mfA2iggGjZ}oW0uZowo&FYeMyb zp!w_m+S+H%kqhk?u8s}f-az%^>GU{KJ$(@{|@;R$iG zUB2vV(RcD`?O#%APAjT~J5gRt>|*jSb2DQji!tsyGNMpR0`8nY9d-6x)G*^y5V~Zb zDqooOI+YN2BWBz2^GN)TlV=Z3voJ9+GdJrxFkN0--CX`=+*v-;p}$dgbIAKlt1Zxty&}d zsnweG%lG*2`Ll@*3<`8~lsW6U_6MS&h+=MRGGM`xBP$n-GdTeNGv+v^8tipAXw$-> z-Hpwq0I_voi0nYCpu>xI|9RxLW-$&73|{WPK@Kav#~2HR5}|=fw;of5ckdo7d?&0o z*89@FkD+m`P+&43;jTQj_2ZCrKl~*`25b#nC*P>I4T-E3^3A)O1POB?yCu=vWZ<2L ziu-LcPWb)wXHwUu0m6VSYa=cwqVz6183~0Og!v%la;TJ9_)b?%YxlKV;F9%g{!GH! zHX&@|fsjYa6uk$Y-H@_$A!DZO(*6shDXUj(`295PIXX@L4d46Re=*V6z6nj6{Qcsd z=BVH36@AkcsmcI>_WM#br2XKrTh}cAy>#6-M0F$ zmrvc-dfyB-MeAkEcO!LX1MSsQJB(j8d(H19FD)f%p;gyaY05-F!tK{tnm;^2+wlYP zwy)VZ(!7hN8?URW4hFRwkvP+#o^@^=z1)`0`+d}%+OX!BuSK`+wkIN0r}!sN-Bkat z5x2o_&Ik?o>tW#fHh{-w%(GymW4E(zbH_F77R~)_`Z_nxZu_5EG{U@_nXv1ehsu=t zWZsjfb$^(?PCo;0!%xe8n53PoH+3Wscm>GA7?K6?kTK5$LZ|M~ZtA>c|2NAQ{~J=< za-^p1oI3@k2z8LrZoQ?t#=n~QQuUPoBvDD|UD-nmcdaLKjfP z_dwuLly$dn;&jm;T{*aF?!Tq66{Ru!(t**&MjDJTo3^R8@;BamsmkJK-v5xow6#jV zPuJ~DJb&unu8#N>3O8)nyLs!@U30-FqY;1Jb^g=z^wcMTbLRb9>RKRm=fk%J9bvPj4?9yY6K2m4CB#1E@QC=;)4% z2Sb9kY}&hh&4!;=Y*;tjKX6aLB96LlGvdz0#2?&0m!odoDu~r5_xXRg(4sj|cmB9{ zPeaIl_)zUs9T%K9xP7~NhyKp3Jtn+-qkG**-@C%sYra!2`}9ZWE=EJwp8o#8Z>}x* zB5mi+Srf+C&0Vo$$Myre))M;WZjVTKZ~zL?1U<99_RH-f_5>69*7>d6^23~Y6JdwK zt;ruO1Tn}uaq0VkhGq(gem;JxwSv|A&zx4D(vR9<-eq~bE~W`}?+PykszW~w-_yg~ zxSN&3-n%D$ZRJaH@trRoZ0>E?-Pp*;)MCWsxodvh?6+#ux`oTehGlxN=yfs~6kM}5 zbZ_wd1*^WFyUf>aq=nE(*Im!3r@vBCC1zlrj9sXr_cq9_n+arL<~_nLgJjQNxURa^ zJaKt9Q;Ue03m23%2CQ&lN1oE?U3L3D4cgSxTqra(w*3C!#;BjK%#l4fToNvb2pinJ zPT1be*x0Ct<#0FO<$jBQ8o$M^n+~8$7q;#`ENaWi{fDOeI1jbysWYxO2c0+Wk@&7U zoA)}8rR?+?{(h@&kZzgvK6GAnhJW$eB~^6u>ywOxT^5(;=QpKGP?WTMb=^;VzY)e- zVF%MLHm>Jm5$W6Q^`z%&`hJE>d-zfAso8^?kv;=Ucn#go1A0 zUOLToo}5&zEftg(`hBZez+d8Iq7t?-HgevVaDrPoRVxyR%kIRhZ`I!xC-O5kbuAW# z0QVKbOVG5-AxWS?5agG;AIne%|?av6MpcDeG( z%6%Gt{;tapHR=4PJG&@^tqjeUop{2o)Di)&Ij7a<1V_S8L`aVbPWtzNqV5Ll&S=Lq z$whs{6ROjKa{)cgwcvBsombX06KM`jw;g@) zv^G+3CUW6C?R>$)uX~s)U;?De*wb6zlC@zqBBr(uw&s=`4cQUwFWV#V-!s?*ySxf~ z0_kS`f+D0x436(P80A+Ytrh9jzT9RGJOQ6w0&`W*c$wL%xBQZoU0{`jq4!znRjOXv z-Ox7psP>2;>{wE=Hc60r-4bfL&5e4^c>f&Nqm_!9*VY`|HgeCyv=l|EAm!0F-LS`K zp!=&m@?9-KNF|L>iPWH$CK1I1qn9>nm~^x0}rWT zoq}5QT{oRM?*L3$@y~5rt^G-`c9o5(6w7+E9={|#wydo=w0l5Tm*bbto>iXX$E-Cf z5#9q9W3k(A^ONM)>*Jd(H!|LKYU8`N;@5)0vpt~$mL$Of*^U0~NkjWO<(=^vsDZhc_A>%=~0*ya23d|82@s9vZs%s;Sar)H0S;0o}X=3v@}eQ&BuVTlkjYSLrEq;Gbey{fvx zzcs5H_WBaoeG6+U?C#%H-D#b6V$Q@VY}Zw?QbC#M`!7`3v#DilnmSdLBDfnr1Xo)a zIcaMI;@WRMhu~@S;~DKS!O^HkDcV&2gF^rW0}8BXWDzP8my6nR9Z#OK&Hvcpa7~2Z z=;@zWj``gdUs0cHd*S@HO$#Qo9T&i6SOP_oDa;s=j<2QGCGlWlKZXBrX;-C?XVzZ_ z8m`*Z#T@!JbiJcV;3w~0s`-)sgEdxBjjWR7<)U^?dfgnC6tF|HRj^}CZ}T!?Do~dw zOorbQNM3mxLNy^B4+TZt*}b(11tss5g@U3I7wE56$m=n*ZkS+@$F(b(%Yqw6Ens$s z>7lC?XwG*B8lh|(lcBxCOMR@#5j(RKt~z;K7r~F5VMsgw z+|co6%;lIXHxCRtpY}oTvivKmtAcBBlMEHWJ51l02~31n9%?fL_p;U+N`-naU%m3^ z3Zd0x$h}PMW8R~?n*010OS@DG1zmnBdMC@{<=51SKK-!&pf7~8_Xh6q4_aypgVMXu z>@7gx3&SOm5l2r%EbUl;Ha_ZPfk|A;v11N zd$5sA$TRQ#|JeHuxF)Z?|MNTvd#|931j0&U-J_1$T34-GYireRt+m!wN43^Hs^XTZ zxCe@W2#Sb%fRiB`WW$rNAqjgV{J$q?$8GQXzxTfX|Gn@1ytipE$#c$co!>f}7ZW=k z;^Dcz$Kf&nrs`$Kd0v>0rx)ZRnp{-eT*wxbVX59<3dY52%=@!^!lEvZ7gtK|fR=dB zB6NEzyC66A>#-B1f1pKGbuFUfGB5JV2)8b322ImU;fvW{Psf?-pb5bVF*__&Wr=Le9G?bBeU6Y7B-?-6P zw)?Aze?p0}@xm5w%`V>FAH0xSP{9nGqM2kga#dNATHT`fNLVjnM6=j1>Tr2(L$BS| z-e7N@KdL#-o*C&@g{kXz!Y{Rkvlp);Hgr?f1sxpbE*(F4;U4e1X@?<}NuO@MnfM;& zg{iR!-TK{0-}W^n<9bs3!QD$IPl21-b74zKy|P}hbCYH>`|}d_MyjzgO;2O5ahv6N z1;s^cg^&W{@je^X&jZoP<%ioikU#qJ`Zd}g*`MA5o2+pEi`K_%e0F`SO3-v{^*|5l zyQ4q;ep$e|pTu6HC(haGGu64vISXAz9{73ggb7lQgQtR4egDa*(LFsS%TE0a0+bEm z2mG~u?6Db81*JsmW$fX3=;xB{M?+?akl--l4a^}-R2g|lZq>@=it@m9+AZwP&(Ywl z>*PCaH#%b(PT5u~XnH!@9h!>;pB^4=Zs>Bo(X;o!K5jJbMoBECV%eZRJ$p(d?s!IL zkTIR-He^LrOJieG!^6AUd+gJ%uslvBG4m2SJ3FmEzolf?$i zEzZf#&P}4!9-Z$>$A@HRuD4w!*IS?SlI{*glOaZXcq)MMvV`wR(bP+fTzg&5%goHm zI<$!rK0(}w);9BcWA1_lm>1nrwQIZ!&F|dC++sdNptJIlc!@Tb7Zem0Z1{HZ;w1}u zz~))k?@qP{@&ZmA*xByQ?w==0mBc{X6C_7L)r_Nle!l+uw`+f9_e|75H*xQuR8Vy9 z{OR3cLqIs|{8K8^-?LU??%p%5L#=MgkB*JK74in`mnWVS7J|SY9C`nCJ5J-C;gSmC zlL0#!exvtL?GSdr7rV6ESnn>njXnCtJUe>)%9#(n;3;^XlCb0|4Bw99nw0hBOF#YM zi?7~h}k=ds&|;Xl*rGwa>ZB>(1_c z+YRLLT&5*GrMai6)BJJ;SN8P!ty^&)y6EIQ9jBG!_Kom6?KjzndtXCz`XNL6yfZ=b zm`(j!NZ}cH_T0K>;Y2q#I1XVDm&3dP7h>uqS}89r%3nMG+r?wuE0A3ry4`+_y_!_f zUdF4+8!1i$ea9#*(a#?}di-?bC)zLBd1K)x)o_MAJ{l`=1r)yc-G`6knRnf*BqpBj zJG0BHZ=DH^+KQ0Cx-a!^|C!zHeX%{1ck${PQ5QVcd0oA>j8|T!i!+zJ(KJd7$kKsD zzS9x4Lcx44s>BjK@exgvCS=`?Z#@9Y^ZgIoK47O_z`^?%b{Zp22WiKVLG%i}+i{ab zXu^1x1A4$=WJ&Z~CKQzv6)}qu5slq??tl2``2GOj{?shJMo(^=$a^m!@lpF@_Qf{g zDNf2#$_8KgeANWlGy*_PrIi z+OF|pZj2UZfRqy;rB70gri`sjIn?3L9{U_#Js;|V^+QfeTUA;m8$AwF=d}N1=pkDd zv1iG;3Di_QzFgh5g12nDqOz@mZ8###&<)-s10XA0_PqTbiEqEgyLp60tP0eI&l3-N zR;l6G`0c_nI$^!c?|jaDAs~tU?zP6UH?K#wMe(jiz2%k(vQE+*E2XMkK`Psed8LKp zU7IB@p`I>b>JB-tJnwCBnMB`HJU-&;G%6pDRX(lzc)LSxTxq|`yK>9hwF?I8ydE1(OaahK0}Ko?k#fva0r>HVeeF3g@9(&HnywySvh$h2<6Cgdd#KT$+(SZ zy=8ePI%?nSQwO82MqG-BxD>P9HA`|6a(E(%z~2T0o(-xFMQ0`NdBmM7?GbFuLL^st z2#%v3*S3}E)zvmXmbuH3-oY!eqPd|Rr%lhCJ2~%1|1W~3-FRAF1wE`R9pjn}S)Gj}@!_e3KHe+yYTGoeyv~y}$1IfS zNyTs6Y*$j<;HjIwcSxct6nRf`yYq(47rv`0$cg7B`poi8l^2&3Pj<}#jdb`uPU7JH z=8dFhNe?c46h8CZLZ3Zr&n(L;$;-_x$Qb9+hRF>*scS#~V(e+qD0vN~-w2Z>&aRWs z9zAyK=$ZXKi~L`Ko)j?shm~9EYgC`PP|#;2j^YW4&r<^D1WmmBu@3CH&#!KXfBNWA z!qr|L?UFj+u1D((y4|5{u0#KzE%klp7m#W|rZ&<3dBVZpf}XK%@kdU{2KDyYhT^VSJl?G?h#@{qgcWh4Lwt)Y^v+P+UtspZyYdm>FoaInefoi z3pWaVe-C--XXnd5(21DNrfMaQZbPDjcF#Qd8{psE^o=hqEH0Rd4orzePc;5mbOE5Z z1A24)Pfnic5+~=?&+gv4bMIw=&nxIBCS9C-eCCGbvsO>{$^zA=0C>XEb+b=Rjh%gQ zk=jLAf=FZOhWhrSn4~Anjgo9TVejI!*f`V?Xq-#sGyxGGN58#pCvW;lFoN_}8}<-1UCg zEXOr9xBH;4EteR&i(`$vhr7o+&muv(qTYs#o{ip z^X$?_jNT|48+UlZ+nTzGf4sO^{u?+(fD?8hDENI>_X3Hj>+r(bit79qskz6UJ>vjq zuf(MLw%pXz6s}tSbh)?+i^zn-*1x>6{{%@7^fW$rJu3R#_p4T| zohp6^C~}Z<5rZ-lV=iC1QC(Npx&o`~EZjUl*&3dB`FEhqxLW368@c=Et$|r-FJmQ$<5r%+p*tRI3jPal7O%ivJ$Yc? zhAD1hOrz5EZDykPpT5k@%DEOC5)!%8`Jp79b`WKqQmq%>vT_tEm#|Wq)X>x2P3-0= zmC5`iCc=-OKa9TGcyK(1xxyNAu(njpq-PY99+}OxKU*FK!(q7~xlcr1bTpgWk_zc5mwT!-=v5in7 zlOB^<|PY~m^!b@ z%gcRq_ujKlG5J<65xBbyUlVjZYUiI2VD~!$uK3>mdCfFWu~!||Q9A#am95QUbMiTj zoTsQ>6bIBD%7&u9#$0cUYvkHoe z(k_Kv+v|q3yHV23!)3_wz+=8g{*(^;-_zl9ZDa6_&EtExd8#nV`h&8zvZ}nIC_&Yv zQSln9X9*vH6xQY4{MJq;)xD9hylyg#vq;^f32mKhXOR@QkXm}mPC|{}&24BXiA>5$ zpXQ>)bYFMZz8{~84)FggWOVxj87HnC-Lq~&AMA@T^Kv~=Ra_{)c5CmNYnhEW^|?7= zfG(HGk{}Ga_sMGRWKztrMc77K2x8ogFSmAfb}|{3pvF)vJB{U!l6&p-MW@bXC(IOS zB+U{B(Rd%f)0h1J8adtnNKSO%j-AVXoQSpgW*&VGB%VAI6}4rzoS^`o$*iTwIwVh;p6Jhyc=~=iam)!+<6@KfZZ;!#iWeXzH;G|UV z;BbbS5FlvQ=*<3%9I zUA!ptHl@e+{E6_=7FAtC#SGyyN|3`95EKT247`x@Nj>$*qGn1=U?U+$pv7f)j#jbe zO8M6=SffoRGchFXw5+T|TP#q7{#AOM{z%W&z@+k0j;oICf$a^pUM|BA?%71?Id|sh zy&AQ$HMPHPy-hM!O%eo8*2^}jzLQC3K$<|NfShazIjAEo1}g3>3y~N>oi!QHmY^M^ij#GLv4E7RK_^J`PEtp!%C;l) z;GdK8`+#r-elRersZGgmRQ}-XsYK{>>Gf$qSoq;(Cyp#jP?Xm!b=8sL!7>RTC4m7c zCQ^{%r4Le;=|pn`hG#De!}P-}Mf$e$gIv&Gz?8OdLRPcVR#g+~_ooi|4}@$uUZieA zl2i3yjhlO&#KP0H$Bf-Ut4A&af{x3ZeB`Hvn?u49%LZ48AeRnCk`ZA%0kYPGP zz62r0(knW%(--{^b@Aig*omVrk&1@hYEd^UZCaxP{*)H$zf_xbp>54|{D$_Eo){1| z^K|tX`C;Ih9|N~W&G$glfDWlCA#*^-tU9WylniSPVGJ{Men;g1&xpnem*|Nn>>uL> z#p6qa?`5hRm7OZ9+hKneC;v#o#g-!ki?Q4bD~|h2ndh@-ji*O~BvqoX(@gVX z_`M8X?p-xVQM z&$84xp4mo|9QR2q)U1}F(*ioA&R za{QPLdzhzITGwYi*#0N!$e)S1vCdFc_Kk1)+`+C`(@nXw087T_5c`9TY5-YA#}nFt zQrJqEZWr1+nR~K2!s>fTk0O(D{H})WXi%x@tQw0p|IH|kfb!7ub>ZVAZdY_T&A`sr z=^A*%`BfArSd=+76TM;Hkyj2)By25`5D)1wY?>EtX=1^z}a|yps z(f?eYW?kqmA3PNNsZ@;a7e#2A_p0S&A{`#aU3x;I@7i~6U87P>)Y^K|sIB6?%*$4_ z)mf=ZPu;n@^E5Y=BtY5g0NJ9eME^z&d>w_7@7PI-hC$}nsR#nquR zl_h}^8rBqGcF4Nuu)u&rCU-+3Lk}PJxkIt$|5eUxe~0F9WZe3%-*!Pip1>8P!rVQ0 z1v)WOm*=FzW+!`yr}=cL=lC-xf_(l)M0USH^s~2L*!2}h&tmEGU3gdUQ9z?@nS|)q z6DU68Dm#XC2`;^s1P1;M;6qv(>j3`igU?ok@7Zzo(2l*Kx6&V!7Vnbiz_U_UrjR;F z8d1AZ1Ul!z^KcvHZSpwuzmU?gsQpoE!T=po`qn4e@9w#W$;rhf4UMgx?Hg$ynP8CR z1)y3L5n{keh*1N9qhMU6ff#0!nEY@q_K!_)-se)((Op}1oVa%3Q9?O&mvsfDwGHa} zrpCsGT19?F+HxmMxM2iH>Sb6=Gf0JmmASHw0L|F-@Q1TC5x%=CCg?Y9Yo#v#_I&7( zs9Uk&kDrxR0v0`@rlB75Mh5Eqry)Du5qaIhFgq)A(<>#6xr_tR4vhqU)KcmzF_DPH z^LRZVnpiM+YSQ{z8 zbRt_4zP0rD{VtVjQtCD1qFR-zP{~)z35{zAkF}FX^MTn>uMJ24@QfV1Wc!hZ^WDXx&T8`;)9VbC z)tNWWZX76*(*3Lrl1?cjk!GSxNT()c5+`F7fLMxU7~++ASR(+o5j{Oi5&GtIncWLX z3se_#%U+H%67rv=W4TgQ(&e>YZWv{fdJ4NcyGf+dmw-k`t@$#k$dV5~&l(BbWYTPj zfmG(+&CGyhj0nRTnA(XvWisrUlA1|8T!kW$i>sT57wT7`PsQ`{ShughI~}8KRXMwQ zx+^56Qcq`lJAs{`yU0T#&BIVer_9{b$;yyr`G%%8j&2ynz@P$#FudHvonpq_ODiMD zm<*&653$IpyP)09P+;H9QRL<&mDbYLtNrr(r{ik$1|Ki1S+Q^zyL7V?2<+_zz~SKH zsg)M#`V5aL*)j<>R!AtoL`Y04jl^GlPnYf}Yu6!QEo|+zf=)YqK{sc2i4;>#O%i6r zn1a#ixW@gg$fI0h;3XQf+6K_uP>g5qNWqs%?n<=MM_5AADa9^YFxM*JTbqyVZ=(?y zzNW%etrLdi2?rN2 zA;nq(sK;i>H2~G~5^?~KUiblEYLUH(;Bu=iWMpJ)RWEW`A;+Ac<~R0 zQLrxalH%fWOTIGF3W$9N-mL-y$W%xrUyWgF;&oOMOC<=dy9w+d6k~fk3#)hM-@bGA z9ScxzJ~Tbsd&O|1{ZKXWknHOGvWy_!(UVg!>1rn2eCzV^7h27D*0$LHwn%J3H4fQ#(HayDL-j33$ z6&Tsu*k9!?eX?M57jox-s>1xDj1=CBoIoKagIs4_yA)BXY)o{wfDG&$){eBYga+u_ z3G5x9Sz1`tMIB~4ikVLKi(RPl*;-qe7#f&hcImE;aSxFLRHsc~2~un=>?SPsei(Qt z=+q2z+6H(Vn(inqeQ@&lo0uuXW@_O_b#-|)=@{j&n}mrP3-@71j|7~mQ8$M>SW#Zu z1qP-IA1)r+-NL3#U}z_R7^qS-V;2COZnUC%i9|xjZnoA|X2yp4dOQZh2R5=MEH^>x zO57=423GYnO4z$&*OGyD=C)lXr)TdV1O-|t zk%Yme3KR%0vPP`RrAmm=V`w3zzC~bdYicv-^DVn~Z1IWNy*K$q>We`ZP){4d`O2dF zu!xvNSV@7dvGWpj?aS&^IQz;?E=V6Nz1)IRpFVwFrBXJog8ZpocG_CsYfIa{KYI74 zGu^GsZFH{1o@%L%8V^u&37t*Da&)d>X^HrbE$1$dFf?j2ZLk?j3(?Fy$9HSD8ErXq zIW{9VNA3f87@KaaEG)fu?m3EF@MyC|zL}FLA5^W=fCcl7q zjH!5FG?b@SsHTIRm6`pp@0Z+)KD=$~wr!jJ9_PGBPDvhaf#(_lV?0w^qMsCTDe7bo zxEzePxc9u2ll>wEoi4@w6(Y|X3g#GZD!p_wb;I!6UO-=murY<&TKUX;6$=|Njk_fka)TY9qa`--MZ76DXA&Br!CtBruJt2Unq*WkbQ>^FA!se6`h{lpPQBP0{!xq_Mcrn zY9t0O6N^iV3JYn^AbT806PS;96!$Q0@4}PwPF&o$`oxk?Kakj2Si@1%W;B4!JG;`j zAiV(sMS4i*^u_2=<=__(C>Buy8)&=RSzFjkXB_mtyLC&*G97VSH(#Jjmy$-ALu_XD zRt`~Bg~d7d@7;ei1)I4+o%5pX)U1>ws2I1^1H_pN18Pf4OUiKMO#9t_=FpGsW?P=$ zyMOQ4@wq21Zde|$bW`Y_M^9E7e|yP=id0f`=F7+Wy&kg4Do zG*SaE4_9YOqDtUmV`1jdYwGT$zL7hlH@%{9^XC1*=YxaKh3&SpqeR-7?XD~=EXhjC z$i?|Dn8%ZdCM0BJrX(u)FYm0Edemb0f5_vEl$7LrEInyW@-Ty6a<=SoBmVw__?z>N zQ6iW8bY|b~&Ffb!7%LN4TH1n8ND^-?f^#sqp(IRCfhQ2WnU%JJa zF7@djot2TCsMO23a(K1`!yl~kxAEHRhOxyerAm2KW}y|hAPfpj7A8J;`0!C!a7Y)$ zdv~n;af^S@*$w00cDLhMSVO)bx>IMQoiwZH?g$}!LZpw8^msX#vTcOVGj){6Y9(bL z-3U9W5dz-xWvae`WA#Kdt86dlsUxhBbMS0zqcmC-sncY!#oy4CWsdVYYtky8D0%Jq zQ=Bl^=j>gamY$xPhlXjW+27r|L%_2$3vAI;+0;}g`z-vWQ79ktS6X?0eSDA4juk(A zIb)!i3DgUWICP_&&@kGSa(UU=SXf#* zOU6(7bj67sKb_jN^wU2wv*hCCt8G#2jrYtn0t-9SA1b-hQpL-Z)YQxe{jdg_6^$t* z&8f;KO1-+4$~lfmZLK_H(G|tTMI|s9rCPJo+!iw9S@bPdc2rw6B>%YTwUTx5WAFX- z(Or8sFI&HLoBxqzpG|(>O>ARhX>EgKk_wyJzQ)cRe06uEjJaW97k0Bs)9@x(UF7NE zE_M|;JHkz&9Bo!=b{&Eqb~aWP*4;fvj9;{7=ER?HxcThC@QZC1*vPGx z8u$dG*}0i%X_?RNYag%)8!_~*4{YMz&)bPARs9G<#W@R{F>Y-soL!FfKZW__%DOsb z^*$?GD2UP0vW{%4titD~g7$QoQ5T2KuQ>c^!-jpQR;}NDV&R&PCyeOj;bd)XVq<07 zl@$^DM$oTCzSph0gQK%hgslhe9-g|za-HC`RH~)if?YSUtBVi}>vgAb11(BjMm?Ip zm$j{ht&NlW;7MOiJMH6raLeWsi@#p|=X|eUpP15`#7jUscFn;BTH&K$ae1C_#18%sM2OM5$~0X?VmUh&E1>AR-xnzDWV!L_r4|4aV2 zT+eT6sWYyxdea;{*w}<<=rE`Jo;IHMC~*#!Vd^=3UQu0Mo>CsK)X#j8nt!%8T?H$e z#8qk)Oj(Vl&a^r26H`0d4>tb3xw@^=R9$-H`}ME6f8)=&_x>nwLG;wHX(t!%Jks~r zls#+SUOvKesE4+jvB1pI&I)p{vqjoO(^_I*hj-;Gel=)mLpxi6jk&drnU%KN5YGXw zGsllUGb3z;_n19PHcs0-Y1`y&|5v_40`n`HIi6~h5p>ANWQDrAs;ass=BV}r@5~-4 zR?O*nNkn6cTIenE@k)ct7bz)u(ZhsTf-2mu(BN3!YN$Q0p?0 zrd_XEecXS=cj&aBZ#Ez5b9B;$y&omcn|$+wac4hvT`}wdLERb7(ff&c@QzuA75ncgcuey%&slGhpV7Q+<6mjE|ok z^8J>PJHPweB=Md1<%AcBtw~miX|W<}L`~z56bh)eBI%aq9-HtTT_5k(GrCcOJ6z(G zOeS3-@$%t#9B79VYj0N;X^V{t6)H^n);*nqEQ)V$(W^++T4h|L%s8>HOaDLk8zd1l z>CCPV9(+DAX6m$i^E@wY>9OPTC$ckpr;En@IJKW($moF*CwugZAsT_TshNePmDYyX zYG5Qbrfp_=wloQ|)7o@enp>D6Hgrb}7umb@9x?uXmwq3waUZto%7~e_SG^rJZ%D-4 zX~+8PS@ZtgSr`6xDINbV;@;h++eVM4m;jU6(4r=~UPSruruH`PerRuOqkwxnR#&RQ zr2{%4X;S#vNuM~Om&S7)@g!TD#-_fk#mczW(hY>ttIm&XUER_{3?+{k3DQd}6nJl9`R#yY;J2J{~e~u#Ea8 z8|eqy$<9+IPx-*Z24Zp@JaFjjPnPcf!Pafm7aPJuoJTDUNg3C7%#M4#-r4qi%m*>w z4?FYu;LtDsZaFPkd*)GgMpL>`L9i7P4=bnZE!yhN8s^?rO$>W~H5PGVoy%6-afv%M zbW-X_>C9x83eoT5Ij?9`YHK@d2ogBHsxD)^i56Lp#l@E9YJ(bevCjesY2!aCr0Aow zoxY7(Im&8p)cOzHhNMJp|90FPhM#9WKD2Z5Nsed~SahV#4MoqjsD&&bW!dcM8+@#ueCNb~*kl#PfH2Je|+AjkS(t|pC1 zPwc>Uq}y-OBXNZvSK;E!lvWWJ8$I)g6|0_IAc=tFZ8bSCSuwEro34Dl8-B ziAHCXx8W`fopjn#_^J5;5+oe(;EPz}E!9nhdIj>_xH-%J zA1TkjP&_9to{fKaxAC6QgS&$)1opMC5igJsnPBg7c57597E(uy0HbnzQ?~z81Me~gmE%F{<`{F=XL$T z76L1m*a}T4k1Kf+r$H+Bc(k~c`rqy4T+3?%YD(efJf7-^W&S)*x6AGAIECA&zA4nx zTwvpPW90b4@4@_&R!(_DPuCs-`$;?c3;>H|K<2g>rc3zafWD zeu{b$uZlB_Pn>EhFxsrC*Qz?zOjSllCNDdE4Ag=r92;NUl$VmHa~lmQ{0z0XNEeJ| zR5i6@*}b8$C0#{P_L9OeNm_$3?ODt992ym0^aLgB)#*KPsw_ zYZa`#thQarYL4S30&KTytV%-w>kVDmcoW*n~wU|1sI%|V{Seuy`o4{%oLVmw|7J7aIC=Od3eTjG~lXA zoi5a;=vAn-F}>kQ`Cg7|o3X!912HtOH?ek)%W6o|rmaiONDJ8XEUY72+pCyb#7DMcE4G1SzG#0F)*igdQtMOnD^yC&^LJcmI1wMIZ7*di zsTk_j9~hf1zk9VYS`+=#)#$rN_pbfdgwUb?m=rdxI2m-Z=7iq4jn;-g)wh>3)Dn2j z$vr$E1Lq%_>y-_<&M!KB^(lssfwCiQl{X+uq|JnO#Tzp(@yk6ob?As-ExS+hM7BP^gY?|*x=TN7VsMWZn`IRLY zQ3*7ZEFJHG2{gXz-~?5^q5yFc&=z$e}=(wgA3q52^ePR!!k3z?Eq zO$lFl-tM#OjS(#oKU_H(yKMD8pZ`Js2>+|1vMOKjb58INHMJ|)io0jqFRcMF!x=|#h((-K9orifR_)xr(%+D}b*(0IG~_Nu#&SO4_y!TLAr zzF1WG9e>$-pEsyGR2c0`#JmMcuE>Zu*+{ZSie zRN*$0`h=i{3yl|6grEQMpRV%*|Nb^#FBoDcZ>IbSuv zvD}3xnAr^f#;Qs)+OzZugT>h1PIowbkylYGPm#a!a~8jHe)!_q4}^5zhG=|tPKGwq zC?l(+wyv)J(W$7$$i~POS400jm>(nr^Y$|uV6LpXAhkV%mzn>$5DU(v!U;v{n#z>Q zSG-&PhWFremsg9h2bCvUm!H*H+`ZwlW3uDa)X~UNFAQ^61avuw zSBzW#j`52r1t)sq4lbVPgXfPs9_hzFDrx?A)ON#Hoad*v;kK!yA@p1@PnVrdmA`s8 zo8L2zt*sBzltfOgY)G2!46cRWas}1-4f)Hnp9d_^%|rk5hI;>z zyoIvMx1Z6Pd9C@T;r)+rx~Ic3mHaQ6RaHHC{P;=c%j{f5nW6^$G)4CJ;MP-iBeVKN z!;9r9IpJ%UFTtey|7>QFq&f3!KP--JzHWFUVxYUkWlVOB;@2R}RA73Hs5H&k9@L=4 zF`-(ek-DvVp^KM9_;qG=qB?O|O3tCBv;T+lxom)Yxd%@;J>fy~LxX3>#S#y%m^S&V zo_2Em3$R%O#wxB&7kPQ~z5cW&UKPLe(SwM|tEc@>ZolJxm%_`!I>VbT>0kTCMdG}& zgv&|EiC2;;btS2*QX1BBMVp*GMJr=#E-NoDiHzCrGxndyPyfHS)3EkrNVMV#K`hk2 zIori~W_~W0osu1|RH-U3Al%%%#YHsc?%moub$1ruj(fQ1XWfF1f5wc_-^q@A4vA@0~ao5UgBRqx4xvI7~&?n{|1kIf6~-1SFWUA=;yJM zR(yw#*!kSqsVhK8KU`OD_`m*TMQn&O@gO*jhIkPv@zlKr;~M+7@E(L;yzt}=;!Yfi z6<#j*)(hX?(EatquQo(Ly5r>tI8MZs^dJMsC^D6NOeT_Hq%V0BuOVbG;JrnLlc9Lg zPlNFqj^E!RUyu*U+xRq0_w-G)OCQwgqx<`Y?%$rE35%k2^z}ylw^61SD3hV~RPqs- z0tg?F$>e=9fs7-Q@b4txo{INmyho9dfIN)!2W35UwJ7yo_$(tm0jD>x4MLqy$vpBI znGQ&^@M$C2OT5VzvYGsh_fC8}K>SD$IYrKs5ORauB3JP_i1?FjWH}iK=mYTXqP-X3 zQcUzs(nK{hc%s6HqGldz&yvuxmZxiu-G~ORF)0xkiNhm#zc?X}zF+-T)facD02cGVX z1*1?^=l}6;|0D@eJ9j}mm+9i$8#;@B1DZMpk#QV! z_bo(J89aat@zjP`qpTF#OXKYzL@)`ycSAgPK)iAxV!-f1$$?}nqU>jgi|-+74+WIB zVF{z*L*9cujK%li`1d`08iU{FlCQ}$JRPIM_BO0z0A54zWFYVi!K*iL_R>Yi-iVlE z@fw9^Z{TTPMBmwn=CgEhcp{?rBwci$j_*_P`*i#=9OEkC zUD?BU_%`|)kFqqPuO#cpTC##HhHs;v*5I`fG2I(}?q}kINbU=tw;tYa7s|c~m;+x$ z^nSoXZJXu_G_GTV5j?3YB;>4%=}xfn|JeEe&)D?`*qf1(ZAMD=pMU=&hyUoo|7JVb zLG-3wOv#9zPuMLwhp?0AZ{|3;oa30IhFf(%og=KMk0W;Be_YxwXSfaJglF5^UH_!+ z;d!Dz{z{?zWyqU2xym`MP0q@T->M?|?m_ADlKWqt(iOTu$d?^*mMfZGO_;M>o17=- z9+=`xrvAyCR^G{xt*3S6LU6)OZaWGV&%nVvUvaJSP9WTgZR3N}If{Gunb+mM$3v!g z7Pf1Mu3es zJg0*dN*HO8T8{Jau-W}8M~kssvVAjQrWD}kNnNGliQXXZOW`~E;)JNpoJvksv=C-B zXrNpkZ6WN3#aul;p06Q#u0LM9bYS#d9rQ=Ua9J^Jo}zUo;N2n_bs26A&JP<<&>(Na z=a2DuW~oxn6iJjgLv?;8PFQ93^X}`aKPF@n1a#{ioYgjqt3&Y(9XLPq4%kA=ap^)F ze)g&E_f0s+r$?wx=-!n~97Hy-v`Ws8PkLF*spZXbUTJRfgDMa+8>g-I zzd~hREcvCDw4_1K9Kh*7%swtlo)3^w3{HYIT9XgqGTivr1=&e*1xh}jj)U6lvohs* zX!!oEgdG+O`pELv#h8^;WPy(DIC72CmE!ic6Q0?oE`bOAQtlYaq2*?4A)tQo8Gy&* zvans0up{s39^K?U)N%F{$1dE3(D>Z3IX>}e@*KHd;?Ylca8!=jcCX7Y_SrRZEte|K z22?ptQ6__4KO%!~$nR73U;ltMxGBFUC)>JyC?Y(YK0VE05_#=XHsjjB>$bVIu@h)vcVfo`9+;JL;N6`)WT{#(x0x<=iugkLfcR8h8kCR6{ z;JEAZJE+G`xLCyDM<&PNmr{Cnp}mBQmfytR8a(R$%Of_LJ0%a6GcSz0e!>Y^5naC| z8y1jfgz)2$HsRNcUBAZb6_7;2GoQ8FwCe#mdXm<^g|pI<317DH8Mi1-SA!hUJ-CSn zDMWwd@j{Nvw0`wy_euGAG;4T1NhgMS6EDfBi64E~Rd44n4>Jj?H$9fRh?%dRZRmP- zDxYK#-sJ0YYOn>a)r2_YpzF<|YyN-|yAs4w1V{7Ho#zb;<8-EZn}rKz!rpmF9uVIC zn9nTYa3mJtL(i7KD(_oM2u>i)B|N7?9QByofogdF;y9CB)Y1Evcb z?}V+N=xGSwFk2@Yvay~R@jzi$Jlxf6a*QA%{*Mj7V#4T zgB+RO!RgD3QELiBA1&-1%eAn)LqqAwdUw!rhcNHvALQ%(3A zIeDI29^f%<#W+x!%l_Hr8G&v&W`a>IU}oRk|5;xP*lw>I+)4R4Ig{|7mKd2xXKs(m zgs64mC>_v53JLpFHub!PLU>-@4LtLua9Ps?4!1dQS!-<*0GL^TGKOfRq)R_7$>B}_ zq9Iqh{`=r(rE*%E+tWmFT${xq>O!92+%ZO4j1X1il~3*yCKBj)T*6#l5(VY0)5W9m z(;yUT%0)Dh62iW5Tj$xYB492=zVYuT3P+k7&QZCCL z8sJe(D;6wHqpf<9+ePu+W}66(6`EaAB_}hXW44(z(x+M$5~#J2FPJ1FP}MjQ?)iju z2CexGYVHSc??#M9F=L7=48c;PFs*aUsbjta&I6ja;OfkON9oeLg6QRyBB{Lz?H@NN@&|$!)F@ zh5F*Wy>C&K;Q|U_Lafn6YH4{Q5d1A#jpedOX2T4aK+c;|9K+DcaMu-RbWa3o+ZVdZ zR|?5Xm|PGPfZ@JCVahJZX||*F$Rk>l`B4WY zoJzQ;78Nu5zNA>aiYZnyu7OnHWTI#eHM1=c;(Hc0$w@{b?3bB9U2vf%PBJ!0u0hSG zj=8AG>OJ6gQ>58;v@SE9E5m0?BuM-yL|2koNX@oeGOf(5heAYFqwXdBJdE%$`%(2E zzFuY;2t9h}=IhFEA4sNjb(sKwK@_zB;zI~}NH}O+GQ%5Hk+{^OCV59LXHv|-)UYDh zb5^rLDrF~#r4#{Zu&3IA6E*J=mSGy`h5H1mvjC*`tZJInX+(HDKv1M|y61KG# ztww87yEuu8S7^lomv+KVNTwCJ%>|GZc>)c}>x1TWFQ&4(jGC8T)r4(6)dx8`ltmsI z!n4{Tr+7;H>#Al_&An?5K1>rU@G-*%rbY94iWZe0;2=i$5TGsYfjIHeBP0e877`rb z&7#?95Ryscy_DO&sQcA#NZtXGPol2ALPo|QdQ(OJB%jtyri!#08h-#r$xWxnOFerf zCGeUjZX#qeC+C-VN6K}2^VEb^W9|SA`5dOb7{*M#0iR$0B3qq5WaglyIw>*SygXXt zH2qvWy}e7OYtS5~Phfjp&B45MMN{@Feh!xI8S2U}M|t$fCc(3N6#>UuXvJ5rDo}^M z3x;Lzh)cl7ak&)1z*6{z!s$4T*(ABEJZth8j)7u8grlhosYJ!auH)Nhl)Zu~+37?iM0QulyX8 zN*j_!1D4?#oix5cH-1D(7jZ!PHHt0aa#8GAzf>9%4Nq|?Gb;N^LPeMlF{7h@}(;QdhEf!23WNNjupEjZ-*5h$ zydZ49%Q~Svq)(=T5Ly<#qI5Y3S*&xn$}5y4M%U@D1i9s4+B9A?KG>at+$ocggfT ztE`Md%d*^g78H&EP;^CBTF|Lk9{b+;QpBCoJ@7jE;Sj<_l&ffdbteqZ?vSdy5zO(q z%8D>Kex=vK@IEi9IN1TMA6{qKLi5HQS zw!OO7gimyG@Eg6@h1c(K9XhFqVK4gwT(?UA$8uLBw=jRq3Xf1=83T~! z4J)XShl2n5R~NI)tu1SYtaw#9J*VKO?Z`?T<7=YjG>(1@K(YrqKuN-zhY^TvVT?eXY#0|`9BR!>{ zUd{%neTZhEa)LWG$P$dC@gtQj6q5QF>91|*x(?;M@|v9Xk3#j(Q~R!zvcr893plLF z9kd0@?X^Zt!z-|ka5-BUf4}Q86ZVag@>U3iUr`_?i|`w6Ghv5SDq>*N@MGOH2(dn> zkDTE;CZL%<$geOwHwnriOxA%?E`0F%_K_X+I#Bg%2{{N*^!AavD{Y zaC;CUP507%LyYw`EGS@B=xjFsU~gn0G%<^SY+iOJuP^Oj->X%$qs7=VPDU^806cna z$4>Qic_YT~E*TOsQOa%$qewJiy<(v4aJ#9IRe;0RMKBP~>-#Of zQ|Z7|q}qXOy}K|--Tf9z)Uj~GEEnB966=?E#{1+nvw^B#mNV*Y;NaJbDrR&+sJYF9 zZ*fY6i_l$FF{Un7UJMAAEH9HN4D+TTjMBsoej@=Q366OnzSNI z062o|3SCiUFp6$L11e~%hxH00nAotG`?3fM&QLh)bmcF0QUu2nUmc6w0!iQs!%O5W zVZJS*McJDB2FQlbDs>jB_ANrMx$ZjJl!+Z00g%jgAio5L76C>dMHwx|mX!~|n2vE= z4K0W>O14`>f>M_62aYa*(2{-_Ejp*9zKE@TXn_&CZR!{~&7b1ON0AHgh{up;zzfzU zIAf@1R-K}i1}^nc9uD9CrmieTUO)q)Z}+PZ6>Am7<;lb3tnz^~9``M*q^_5zP#<71 zb;JHpQ+^GD*>q?-nzn#LELtE8R4JbiDI+{tYAq5o)}_fyduv5xSVf!u}}uK|8wLdMA1yc za)tWpJ2ixFzP`K~T@9F@LVZ@GB>cevrRuUMc^1$_y57K&DRyWEvKPIw3a+AD(S`(p zUsrkh9bDwWH~;dny1YS-But&T*shW2iNB7nP?bj^mX?zWO=V{VQ?8(ryKZn3YRis) zi3G!_tm5U3GuP7OmE|26oz*K-RY!m6gp>Ep-rAm^Lbg#Rx2{yDADkw!)@Ka`vWY)k zEmYSkqS2wl%?IUNj_KoDw{Aap@*=yqtg=E;EGO^cLR`$D6e{GFipsp}LB9UJ0l{~( z%PPxg(q*NnRp*{x(9hCJ)OW&)lL=MoDw-jgRjQSxDUaixrgL@mwdGgkhGmtdSqXO| z&mOVx_3`ob^YcG)GVIoiVlWMvEx3uLQ_R;8gk{NtW4-}_hYtt(`yRd+ms?gz%1+Bo z%a!V~l+@hvI<>M&Q480gUshRNT~%3jMQ);~sK|d99O&ok=O4%qJRIm3pbaz(z-7%R zucay~^zJUBK&-V!s2 z)Ym`ouqMzdFu-r;ejP!F0|R^mEqOsZhaYMI2kW=P0AVDK=H#jNH>iZeDI?s<4q{-o)AtpJRL(49 zs0}p1b%FjTqn_qb;rO8Ms#RE)AVB{_O9kgA(BD zcQoX7T5&nhpO6~>bu~^OR@no0U3G;#FYU?QD`)+-um0xa_lL;F$;+;Fk&=1$e4r1+ z{Hpx|1N;NG4rjF>hF#(ZQV8VF_|8H^G)klyb{Kp?Bz)>EN==$|JK+z4UP5F{AWC=l z{ho~6c*IYq4-kuQz^SN5IUHoKIE#VxqTJ;88xbcD7;j!S_k+;`y_{`L=)lu*)pe@o zm0ZT1^M`4h(km8Q4jmZ#Sr&cwnkcwqCW-i|M z??Q{}#)1Bxg!EoUZAOD8G6g9|f@7YeaX?B4dSd+d9juIyC}0fHI;HZa+@Y+Zq9pZZ zaDYzdy4nRo@%Q&x;MGbD$R|Fahw3!zx8D{S$t2oOnHfWU$2Ukn(4EFn*aov{tAbJ# z3-np1Ar6jnb_I5|H2vM5w$Y8mJUJCe7=qQ-HW0vUsJ|n(hF+BACq$p71W;5uDEd%@ zfL$NjVhY;ipua|EgaQ5#39}O!5X0@i2G7WBL|c%tt;A@d4{GU@iWe9VFrb6z^ZPD6 z;17>+*jo2MN5O>~3At#Jas?u5xqLu7y_{zNSAJh^3>lPWJ_tXCcBAHrCe=`5^Y`;v zJ(Sl-^rup_r`$2VbI@nNWGRwS?vCj-IC^HQZrCe|X(jv>zPeK6Fdw?TkwM#7j#=jy zfJQvr7HE#P@CypQ_BgAgoVLbQ1j7eb9rTW!Zcl3C(50=YROCLpcJ{C@t&b4+bf`{j^jkedt_yk77!|%bwAQPs|py1DV`PczQqjpw)@aTl&9^?8*MUb-uuk zR)KIj0?p74hfiO=_aa}8n1GnesxO*ru-cWik5N65TPPq0MSkMVkRW(jw1gHKWTF#m z05sD3YcEXBj^fcL@H2k9tw6~4kOanQuOhHwJc&AuyGXkEW;^~5d+!||`B~nJzSA_L zw%XpSq?KmXrO~MGuC{24c2}C-XS6l}PEHb%n|cU`+#Gxoz_{*en+@1tAjTN%V6Xv` z0Lh``BsmZ|27^1jxbLcr&iDH~?{6f|{rld3u0I+{zxn;%^7Q9KnJ?ej_r>S`74GLJKZWl8 z`)5D=o(tzMQU?Hx5s|Kf-rx9!fhuGaUH{!RD`xWIY$r0bu20EW=f;N+6qq;BM!uf2 zZf~C@3IOHu<=OZProLhLjqkkUqBeR3_GC2-*zD!IKK!XKe+$5XF3)}bS?_PY9XAGp zqQrN9^VMfpfiOS&$y49?$|HaGZjex9!~mf3Is%LZ6q#CYvjcOwt)?Y;X~KSw@lwZQev_y5=D9(@0mSpY^U0{|q~#S0fN zzO5AWAxHPN3(}_h^0_}mQsu2A($^aql)0uJqc-3Es`0j-yKsdt0fgEA{0kFk23=i&t*>ljB>T!>SnGfpeezn*az_{VxFRsbdmZ zB78#H;Hfbr5qi&Ef*_W!xG!C7{izA88JKy?`71zgQR@oTR$aOX?Yi`y4?OUhFMs{9 zr=BbS)C2II``WYSC-u+&>T4hP>o>mcKi_cg*MIe!A3qKwxQ+n+$K=rRLTmk5puUxoo2WWRvhWLN_p!pD@!msF9%nuti zHhWRMDYgDNvu1T~>Q8QFJL2nr`Q-)r%PW^ImS<}(-ub@!KJkzL^7ZdO`Rq?DnBRHU z`|NW+`^^hw9PRS!pCNe&XTA8->gS$%?CW2C^uZ6l=TdprfD?;Z0ObhH?5U?roqx~x^?!BSoflyy zFG23;e<3G}S9YKTS$*Nsop;~+;HUrj-@ftvC!U670K-YrCoz6pdT!~*dq6^%^QWKq z-hX`MA0E2zeRqm@2*L!9{m=uMOXZ8cJ8ynVvZvv37_q;Bg=8={n+2ddzfVBFu=W8A zPTURm6VS)oF`qD1jlhZxT(})`z^iIo4qyLQ@45pv=o0y~c)6bK^ErPmf~H$9T)y+J z58eOJXaD(M|KmH4Kk+oJ$Ik#A0RD0Lx!+(=QU=6JKSAnG?mqj}4e!S(Gr=R%Y55D`&ul~<3 zef~2Kee~XY-g5^-V5g0f`gbQ=*L=(PyzgYFBBl)v7%QlYjD8x4;`-s6%r?Zy^jr6o#&g zenx=(WHvnW)2yM{kJPC8LhJrZzwm(Z8+*AKGTLM}tnQ@+fP=8skuj3B4Cj3j){qwa|Ni6X z(Y@>b)l}8q0+V^=lI!A8q`*Fg$u0LYA8_Aq?lSJF$0UvTao<&gVD?S3Pz2Zc-~EcI z@vm>)fAWog^yZt+Au{nn=%9NGpaKFnvJF4{sJmvMOL;^v!cc$)P<)0ISX6&THmrdm z0J2x^xODEuKmViDp}x&EU&NT}JQy@Qv~gQtosNyu(*f+O-$II6S8XepPeIP zFI^n_g4yA1*xWNRUi!T^{nbskp1XX<9aj+6U%CPn0P`=2i~$lMC;&u+Bd`jnf`9-D zTrO?C0_mHD7(txwynO!FoBr|-e)sfP|F+fM&l!J{Sj6*hMrQE>DCYWZ_kGw{<8J;e zq59FlHG?#x?=NRL61Z^dnopT+-r6;d-QnSr*T4REfA6jDyz%B+Z@+TKod6E}oh@H1 zU!pQW952n!x-Os^_zUiCfm%Q)@4WMli?`o$(>rf?(|>w>;lN;T%i3D+!^Zzs%GK;m z4Is^~#{k1Ki4U81BO3m&n9a}QRi+CjQ2k#;uFn7QeP*?-B$NZYaO4`i8f??S|ib{Ttu#hS!&0*HD_tr>DdHyE*bZUZ;L=b z;9JDn1OJX2FF;IYcbsFT@$~5rVi%;l_bcS+GjHXCs+QYe=+FJh zIV`TL^8W3G%NNhJE}$iM&6m&%29qDaUP||lhgsw4lLWb{9TCW-%dO{8!#92r@^f^a z%N|Nn{*HSf%rLXVyi&tj5GCv1e%sBy zi!AvUx4yGlO5TI%OpHVQcL8IU{~Q_o^~M*CXUA=`0~Z032N{^wS0KD*oFw@2ruRZB z%wJyRTIEM-kmBy1{X>>>AGkgFG8N795L$G7FZ3SlfpO3L2ve_|8RmS^k-za@LP^(u z`m2{v5KC{ksv-4m?2K`beHE*vI6#13%c?uD702s;2NFh{!T(b} zUBX(ZC;9YiO;@nh*8L`3BG~;-eI|>yo;Y;b++bYE$BeY|aqo?I=rQZQCF`TEzl4o@ zrqA_dJjCi4K69I*Jo=l9{Ic_5KKaJRcgd3n032r2ynN&_4R=bxckr33`3XMy>ZS|$ zf<%G)BA-3kb`zid9;&&XnVQ8lXyH9CU+>@}-$#MZGw09al2pIICr|d=OrW;<9>b$A zt>!a~T0OV3si<&XaVytCl7oi(rRta(lTU)y2#njLt}4!mXuUb6%LU)urfYz!w-@!TO~a1rwJ z44SG`GM352~P$X$!2o7q4!Bk_nN?obRv^ZM{|d;Li21SJD>4nVtH)+2(;y=)0s>n zmUzDu#af09`9wBT&bpJCA#CJ28OdZ9Gp=lA3H@VdguA;KTgbZ8iN1Smh1%mWbe7Fz zvdPpqcJ=KjB(rlUjxu3PKLq26Ih+cT*W&k?`oQ&ZK?mR_QztPZap$7ttWR3V0D<@n zm-zSRl8c!t7EBd3J6jxmLkV;P;FdC;Y%;wc3x!U`%UTQ%mNH9O*L3bh;~UAxg&ICK z5IgDA_vS!i5UHH?X0oXyR*={2%_c!we24=ZaAy+3FB^Yfie0fUK$cAu{muqTPh&Qn zT|hxYBBc^h#NNJK9IYC;%d7V@2!*+H~^xk&`WzU6_*@ z;2F)H2>izQ>Sl=Gd^SJ;ve|SD3vvP-$&`L2E#wK>iyxjd^@9l(=R^LMvz|;kj|Irp zuA^DBnMh?Tn>F1-$y7Px1s#)}&l}JFcn09sqaF4`q|>n!=C1uS5D%~wx5`6wXYAx|=QrWONfVe%rV;bEVP z+4Y%p{ERoSacd$b4eFnAW*ZwKKGFQPP1WvG>6Ar}cgqlOcj9avHyNHtt2AY@@lwFq zYw7i8(phlan@uNDp_h%fBb&~M1T0DAKsJ+#Ebr0ThFdiTG}IdlIa+hT)NWHa%LlNVYB?;f}cz}nMzY6 zg$_z(2+us_Bb82*LKGv12_m?$BKE1S_MakGr9HM28*+O0b&G(^v57=#2+Oe67E|bR zQCq=Z@C+3fGZ0P%VgY4|pJ2F@6@kaI`O4KI8%yV!ubDN$QWP|mdh9ooJX+~^D#|-- z3?h>)C(Syn4mcbq`14lmDJ0e^u}xB%x>wGu!fp8|M1XEJzLHY-@JS|`Kx;K;GI1d{ z?kOVinA5*2p3pWyOA3c4ok)+Bq2hbaCP+}S&!AzNCMCe4SOOARZ9*BquhxgsElW{u z(N^-ho@%t!F%wC%6H$4zk|??VS&ZqwU{~Muw5HVkc@j4jPmIMC;XxaP)Lko%mP+07PG!|GLeo0bv|BD{$^aoy-x2N z+H-7DT2!jpw#5(N_5%7Q@x@*Ul;pdT9cm~Q**)Q9ujLAigmhkjqO=H%R09fGBJA|< z8U~#eaFM>uRmZZB=kw@@-Os67qYFz&LLjb(Tm~2xfieYO&{$wId`Qd%CV>?eH=u-y*NoOJi0TvWHOyeM~Zu$edzV+GeW`)yhGv4_E(H=a}g00fT0$tED|(v;-n14a9&i1 zooGjDQwTY~Q_O|D=vsgvY>F_^A)118I@|Y(S+(X+F&$%%_)O1K_Po1&1w;xWLdj@8 z(dof1lTd;Hh`>OuSc4>35;BU2E&#R)rz!5sTvqQfXKjI4M`*P4ie9SaLfT}LbF?ej z#2D6S>^@b9BN#)(nN5`{TiFACI#D7eVv&Xy8eEMmsXv+|gbQGSDwSi}AbS*(rOYOY2BkiXE{hLMmS5sv2-B4!jLB89R%feRT)oxdil|Wk{TKX{WRA-s{i7 z8&Z-H-lP-BQfi;^kIke|nKy0x1PD}=2*je$RD(WRM~Btyk`xDd@FT987$Wo&jBGiy zsQd)%w6(U}Vzv%Jc5!@_*?zj1NT3r4sKq~3+U|fG@KQh600pT!n~dagdGbIbWj?UF zs4y%GpY~;YN6Vxie?hN#04~uqp%L3HPbwHt)aLPsV$a|0fGm+HWpgnJ+1Zog2tPD~ zmCqdXJY!aQ04g2{wCIr9=ugL!a*x!ctgC5|rw}_!H5Et73AI^byE^CKU2JtwY-->*4f5Nq)@R3d{CRBbfnIDd9&16qZR6cBc(caz5wZD}_O zexz?;3$j#{nPqw^a|Os_hvX6E+xdf(=qY)K;oe#?Xr6be9}c zxPaTLCt$V2B$kXSu%s*@BXh*NSS0av8DN?p5g0^RdWkk9gmb>je>%)m3I-oXc{EO? zBFFW-4%Z0Esw)v5Qt}8pW0$hSZa!y#xonA+t>lW)0+2C|aiU186gaR_lCMc4;ttf6 zsY}2U8k2by5r?rDjR0I6FcN}Yh!Sx<%fSk4o}w)RqX8fbpm9eR zl&2yZYBc1z#3<6PLMB26j3A|K5q=TZSW;~vg15ms@u?z-m(Sjaw z;yH?8krIUF#D*QBn4~d@KVkRSsmFUowbqq3Kv#zQ|*k# z!iz#ZfTWaA!ORl)Z<`uVUol1FnvIWH@F(DJS(Z&uJvLj=@RjA1Nb;X(Xf^~l9h|5| zN_~l{Sc6g`woP64D~=_xDG1_Q5l|;yWk;Mv6G@e08MlOyGbmDo| z5D^s{#Ga|tR_kedAWiH=g&|tvximSRTVuG)R&}Q`l0oDsx?p9JezZi5Jdnl;d_bpUD=pr+T;f zke@poBPOo^Y!wIV`8XO|5j$8@%u%OmDe+_l*w-wt<`HR8;Hgj4wuR+PlaLyZ)Gg8^ zXd(@`IgHbK@UrPJj{WhhZ$B0<7Ej?^sUKjqMlmM26PotqFfi&#N6ru=y|ihydPl|Q0Se~CyYwjK+8>Y9hAa`|k$ zj6r>Nf`=6Ow-;gzw{0i5>iLCPin>#|Z9G<}dNZG;68Xeto>#PP{aFeyKgmQqmrqX( z?5@Fbh~3x4uA!@n%!|}Hu?qP`1)*j!TH^Og@T0ceDUYX>43_#URuUGApvyr?UuoO6pY_vBO$sbwG6PBuuM9C3wsvfzwcq&(j9UdqkvBDeyMV6Rpkj!A2 z5<)H#911Ugk^MNe{EBD~uikT(XK*hA7&9a4xi9D`HntrZOnjfjnVnZ7t;lTVRXePWd5W#JQ}9 zXbC-Qt{91ljIk_DH8ZAY0E|k=z(OFxAJL!4>v*D7b<1^5LQ+N!AXWMors@pDA{&j8 z2vw&!ze3~VNyKON$q84JQ61K^ee^`e6Ml_f0D>TZ|017O$%&edS_rU=-=tl%P5Rgv z#=0hA!1f#;%ZvilMcJiDTVo+8;=vkijDH0x{I_BHqD@LF_F8}qiTcq%#;4_^uE9KJ zW0<2LBVFm^m?LuKFpGhKXT2QSwQB_H;!ZG6rJ#9Y=~xZV?(zm9Gq@>wC~(Y!#Tu1x zh(^RI8RQDDK^8UYWZt|JCM*+2aaYGqa2NuKF|=K)c@vBkTg8~#^xK{uP*i*keK_Sw z8AaeUaLL3E@We6S(KKhL=BQ|R22qQj!FHL@n^HNUD5zHx%oS^D7$uTAX@3M5esQE&b$~pVU`ENda$3b=PXNVrIN6fShKTy55@AEAN(nOAohokQv1-0UY37eKugO{zSXX2U^vOkV ziMgn(9JF>i5i732{LD@y!azy}MzPE|k6Wvzq1E1$c(mrUX^zj50LO+v-|uw@p=bfmyol7mXk3Bx!@{?#-kU zrPVx8u4aOgr+g9$sG3uZWE@CH@Kgm?qd`8isUziv8cvG4ZGk1U8`c)kWZ{u`-T_XnT9Za5vTVK?Y338i zQLqL4UYcBgP@8^iOij_2CA_Ua$h1Yxg{;r+z}`f!7Hbu7FIB`TkM&1rs{|PShdjixgFFSXI*az?fmSBT#HUL9 z#V3H!hLUJa5XD~=6A5zJtIjqliXzGk2Ay$$v~ODlYLo%ikr_PcfMBhVLO>^ecushr zQp_T#!?X=3j}}Nw2B;*i2K=~X(+&9Vj+gXU$HcToB1(GN5Y$Q)agagX8j%ozCVrMj zHrBeb)3)(AR;If{8t7T=*O)$@>E`*5t^-LOZvYG(f-?TlsR)V!2i;a{0HA}iM#I-E z;FzEkw3O|Rzz6^pzeBLC!2+^6=SBk0YZPTA(NdIvENd2-qyjC*jV8zD7tm`o!hcY4nYsm#a&0~kP(wcv@f<| zLt;)WkwT=4N)0RVn~m}qO=x~zdX`oJzdMu08Mr?;UJvuKP9&>rR24}0v3<7@^U!Du z?2hx;&iYX-2hk?5NMzSbrmAK}f*+Es!a2V#dQn`nsxA$PH2uQ@wg)mLTH_DBAED8GL02bu&9Kmt$I9|g;thVKRDgQd@u~c1n(x?I4~CjT2N0f77Wd*qkm5tZiV7_CPS~vF*vg z;4CLiW5=*+X&i%E;j$V_4+gq@Jdn0#Ymw^)1+bOQhj)}hR?)2Yp;ocBmj%FcGP&4`^@7bIl!A}V2OZQzE@UcxUEpO?+XtGl5KD?NvFwhW z)zfzi@mDo~lTyk!5g5WmD@~e+4Pw)r1C-X5(PXn0H$Jg-6kqGum15)&M>Ql5hrArV z;HyxITW0uuB=;jw`f3OORDfhqnbXKBJbhBxd)-bnh;zxHxo?ThLJ5ptBB3PV)X zH9ONV6m1E_@ll=f(RPMOdB#ARPqR!5YBRX)AGVLOTsmvAPLmg);z-7Jx0V zF4E`eOo)VXHCeG=$i6Ca0}{Q014RYlz(jG09>Gb{ z{+@7m-{9cL=;+93r)&L&@}|K09VLlK0iU&-xYD7mRMYiL*mi6UL z{tfH9M~8vj(vY{WyLS-BjC)RD_(s|(5<)L^G8BP$63C~)2(nM$RX(kz(BcfV{_ofYM+G;P7hg7Y|z zs`2}gxn`4Mso*)VyjDmV5hr0IN{zsy*Ynh-gINFjqFq{Iy1?szE&splZk(O9P<@uN`|KkW!mf_i3s?aN_HjU@HJjJTR_b_H=;8 zItjFGUnvEa-wOMt7D}9CI ztmR;i8+IxZ&zu1f>Qf3ix+4M=i^$rLS(xb{4*`*3mvljGATKc&t{u9fsaH%xo&u>Aq!@wF z*nlcs+CGmx0|lk86Yio}?EK(K?_7w^#?UMQ5x{GWRK+qPSx@ZSYg6v|<&M zhB@ljm7yxJQiSa|qd*Xv!&#c3N>FyqVHpal?qI`(jniET5B6ddFw|7*YKMDtJSdVb z?!>YC^^=*zoah`qCPIUKB&>@rn|9dIbNc&esUC+8`12`^g-CJA8EsGU?CRf`KtWcj zgUDJ}CCL(2+Oo1j^yb7-Kr>Y8sJ|iwmaZZrj7unH%H2wihqznEDV0=r1*rvaRct3U zju!$U870mNJHwx}lD3hW?tpDPC`D)J!(i3UU_UEEW~ z>c>3R%0a4#jw*);PK@hMe`Gv}ovgMn)_`f7F7W*XGqDv}FOe2d6C0I~e(_FF>V;(z zcmT9YnuGcUknIGi6oc*&IQb)BYP4qq9}Qicz(L)VN~93FVtXlAT4niyJRpw&7b&HN zhggW-QzgW-5mpMjcBLd(vD8Ep0a}E^L`N?m64S5)t^$%x;TX~fJ>^ci+OyCpRO4(EaYqH&kbOk#}r9>shk|)ofN@tOl zP(h}gQ|$Rk%v6)wRe6NNsAdH~)QzD;61$~blQxi0Es`1EY-$`i{KGL&k%k>?20*0f z@`eevE)=_pM7zA>2|*hR)iq9ZyP1K(QjFbV(Bt zlg=qssK`W^Ld}+xYjV4y?t+5$P1&&vov_jzufwYu=e#h9!`ja1PkX{tyY{54kuszP zhLrM#4@UuH;)O-ktGQG0{WyiOJ!M6PD^#ha?ri$NuT4EhUs9G$t6cH14Iqh(x+!Oc z4JuNqRQdQ&EuiD`)vL&0P8)Q>De9G&tt^F2X|FV({wq1T=*zLPoR{a_|06(HmU>!P z!eJ`_spV0GwY#~k7_T?xu@*^h08h2l>fW?lM=O<6f=BGEp9FOui#y_DMMYvnOxx!N zak}dcB(RlD*5;9n5P|f5%*P!?gzIph=A1S_(;Var;ZO;tc2u&lb3jS*${PQRS=2^J zW;P>ZS$2wF^_#eqVU~zs#l4X8)&bIrYdFLv=DBN$)!Bl8!si?1j^0?lRBDlFLL-!O z6o>W%yCD-8efglv60k^P_@fyp#mj+=k(+wD#4z72<7r1B@=^)Cj$8t z3RA`jkN}Fo2dRo_yg0*c$VEjim5FP#H2>NkAsCC=nF2>-snt@Z%rTV~mj9?AR*+(+ z&SgmBS4m7G0oVp~c7%Xaw{I8v;TO?oYS`d8i)Lu846wp5*BiS25M8{C3Rs zM;Q78b+)VerR8x$A_?giz1r2$(=&VxkI#c8RwSy%)?#_ETrGPPXX#)UvdW}0!V!LE zk~pHf_N0vZhy*1IT}be+V*@qC6|bs-hjJpgWB>=b%L_>)&&`G?&ARMCxae3jvG;cL z%MKlwGzbuTCX2*fQ3%7(BURY(f(iJY{pF4Wwl1==`$LR1H7y>k;TDIK(FAstpWp&WR3q6@U&9h>lvHAz7-lNPcUF0iM^KQoP@5)o zRQkNpvu#}|-I}3*V_U=7jlaNzev#HeKqhdN`E$8P^(f22Vz#Q3mStarNIqDgNHkQ> zs8NmyrzPRX(Kif@alKwEDzPfpY+QHq3gPU1wyN}UQu<<1SQjmcmq$tJ6P!7rq52_Z zsmM400-j`X6OK$+i#__1i;<#y?8Jrhb`&O^!}&}X|QMC?nsKX*`v20Gd z5=*lD%$+QFevZXTkXnU=?vyN50nqAdKXnXFcF=>U7ZE$FG?bm#BCw*IET^d@nk|t# zQill#k*A6dD?dm*o$V3YaCNuUQNk~mj>*)NqP1YMah1e$2uO0N`{1}LgPi9i#SDah zRi#BK4*YbqB|&PmxR!54TOGu7tdLb(quHe~LS)S)fOK2pVs()!%EU51!}304_ock- z;-hd7LBLF7r_qE%Msl9~M!O`2gdwP_F`^UX(DdmrQk4gqNg6p!(f`ORIdr5jvo7(e z?5r1~DW(9uNv}@0Wihh3MYZhKNsD76Cq%6Xr+lD$g-yRQRcmG#+hqI(0IfwGpaxl4s0^X-7w zsflD(kfuALtSHsX&Ws)I$j4zhS>o7T?MlLbwnOCEul|Y~z)dF(5!=Ed8WgY?*Rg1{ z=ZtjpIoYNm7kFo5FY2m{Af<>J6=$fZJWvT+1{@F(CW@OYK7&_3w&auSO?$-nP+B5@ zmF@VL{p^ufGWuf?wV76L)Bvg7r;(@Q7eru3qQ`J9Q5cg5A}qEoqNz<=7f8}*EnJAc zvENGkZO@?*^3aevCXgj9ODjna^^QbW2mnAb^wHWS;o^s_S4bxUgxgdN8)~8tI~LTc z{N;Ce1l{Ld#=j7AS5^WI!Cybs; ztAB3%F031OSXE)J8Z2-w6D@Ax>pTOVT!agzNhIl9t$r;kA`e;Qw&mjwq*l(}1(y3(*|}FPRw8?2?dcJ7V@w^ zs)R}-qc_lyNNZ!%m6evCT8Jk$XRJ8LJxT;^^+F~2AP{Kt=mHNlwLn zF<6e7#1cq5%kZ#8x@HG(jopcaSaO7mFb3HgtfVUDredMcrd2q6d0#Fe_ClHl{FTH8 z3VW~x&LgjY?d0iYY)M^>!+h!_&x%n4g-t|P&3gCxmf`VCq?Au(;+V+hS}x9v@eN8@M$E|vM&h~( z5&$d@Kd1tjc0SJ6;#O@ri$g%D?9zd_W^o1pNt`(92|tsCZD{XMs+3A6;;H1TLPVi| zMx6&WS-0Ek;trCFb!`sHMlLkjN`9h79pd^`bo*Nl#9BRTf5iNfrZR;<@fLt9?v25C zg_>69R8AQWTh?j;<)D)ckZMls@g1}))B(q1}A?zNX!K{UouwhebSJcDl{O@!et9IS_7&c1MV>$wE10RMf zNr^eJkv1x?)F5Lkfd-UWWK{S{_y`(HSery~7YSur6NT8ciW@^kMlI}!cPaFa__85- z3!w@hIjK7`o8O(4-Xi2^h8Q{1dZZAFF`#Apl9x&+A~UsfX8mED^dogFRJ<0%a2{Kt zP|H^=`&yyNy6sc!JxZ#zV56e?2u{(Kdn}4$uS7+!DP|UBn^#jLog!-80_36^)Jk;>c-C{W8^JjV}2TYU**Pi28xL&-3+gd#20T8gVCI3hzL zP-c~X!X9+#q&$cu&y^7VT_wAnAx`EDUSs8*z^D?p1PA(g5E??eiSr#ImE1 zDtSr(qK^6c?rT`|q*9~zAqFUqYeC(1N$~8j#DP#(#*5Wb?<%k{03{LBbg8U6f<=YX zlZ{9c{IC;kd0C{@*DVi^94%*qmL+5#G2t!tdJwR_Z_0Zrbvh61dWvdO1|2$ z;*19Zg~bOtLCT`acyyRdC)dh*x;IYe;^YDeOmwZ$kf>uSgLf~SNwIqytXRNwoJCYg zY*{9OwiQDv3JjybRv`z0(g7V|MJ)+^am6mlB$Fe$+C9ob^27wtc2V6#5-CiTkpVIS zxqLo{^p$n!sGugD4JnVU@=FKG4yzPsMpKHSRmw8}#kdW_DR3 z8u2Mc`RPc0h_5R5wsHb~`M?S_MLG+B;m%C$Ra1?%>(T?MqQhl;#g&yX8bTIO4YQO6 zm1IRfNvuCC#X6KZ62c0lB5DDv%rE7!e6Nth3bb6ISj=a!_(S9nRe9nABPv|kysIRp zR4$MRo@&dFRg9NC8kau=x%M?vTk)l!@8}l;p6ENXef>pO^G7fB@yM! zULg_z ziq+6QS>)nz@}7zeRHzVEGEtnE2eQfNpWgkz``!}93fFam>7vRBg=fX8HMBtsEPj(M zI({AewJs1hH533T#okNSQ@Iq4z$~7P3OY&}I;CVhKl}yrC3u#B8JzE>4zdz;L?f8G zOic@ds3T+}P$DvLA<{2tqvD}-0c!dKY!#Z{63Jylwd@o<@%+6nFD|*S-n9`V^E6FS zdhoIKSNhd~C|l+u@W-1=mOeeVu;lvK9#Cz~!6G)CtA|<+gsG;QotG`rPHjO5ccc?- zj}bj&z)nW76Tw&PRHWdZHygTS^Okl`2(qbI>16Ha&Hpd~cW*M!&0fG*yc7V2`o^;_ zVB6^o(-qEEBGi&@NN2~Y+0=~mh(BmWBNmH=LvAXqrMyTTN~(*(QoJ`ki&vC?z4>KK zxDOR`R32JX%{y}eu{WDfo%#mKJb^cd)$L6e(2IVc9hLZ!j>O5sE(;WyN_kPzN_r6G zG9(tGM0N&vkpC~@BwBZXYuGB<8PA{G^p89pqVaT27F(k*>QOfnBRI)&3(kL5Iy-Bt zR5exBR2Z!p(6u#{SWZ1hB?kZ)=~KC~S{MgJk~2@?&F}w#U1`3KJPuAp0U(H{nj9tM z9G&o1A+hTLl>HXY5O%LSQ>1ZNHqiu7rzcqS|1=y5BUOMQ1i;|DgGMT9y5u$$1)V2T z?I6*HVoZmgEC5o(ayve2K4+@ePc!(_PDlpqQE7SJ)apJb-c}PL>2sktZZQ(HJ1GPg zVyR}7o9je2SAzTQiMG#=*U|awmoY8(Nu5YLVbab z1_jgmUD#}SECypmx{EEMa;U>pKG?k!iV=!&=2J;X0HvM=&yE`qxCLuv?Szn2) z*e5&>TdgEZSr7}G@dl(p9-Tw0AgY+m^nb?ugIO1wrm)BlYx3AEs8ls~EbDjeQ36v6 zA$ngK_YtN=`bKoht{A7XfBO}n@lx$K!DFfklQ!>paml<1yYJjbi+C>CCn|;XLMf+n zDvg=`@=Hsm^fJ;9MRtvwnhU^{fJfG;AHzd&?YPhSln^IsBPAtD5ECGqB&4Zqq~u#L zHT#`~8fpt*QYJnP!nkW@FwL=S%aDpnHGqc!_&ysosn#s#HPNb)gS}Vg5=j-BTxt!k zReR`BDxo1HH+TFIkmPAp27dI{<6N%&Gg|zaADm5Mbl=-aj={znQkQHU1b1Xmj zqN&}6o1jW0gMeS) zfWT{a%05MngpvFdnUg=Y`dN_Rhn^Qqb$to~!JBeu8Qi>l_R(D3aChvnw2n_Q3FXwDW_dh!|o}ydY=OJ zk9+XKgHfO*;qHo%Ufouy5AVby|33xa?3>0YLzpFUN4q0*pcAkeS%nV02=|v0SYkj} z+_H}ZMvswH*;ECv%@478GPVosS z;Y@Pio8rHmU!r^oek-;cRh$H^1PiD%d2@+?p=SJ>>vJd~5pp7aO z7HX){-a0YyA++J*|Dp-JWV0Z)P*@*5wbEcB3PDxqBg#4ai=kPV3@@_-47R3)5QOgcqol@ zMX4R{p$vQ*5ytadK?l$ab)Ddft$`!j_fO;^Yf%BooDSe}kFgMtlg9j>z#4wE++V95O;; zkL7`!j9mbqS~UT_GFz_s3el%r0v2-BlR)~329PF1}vCx%JJgXkDE^#_vSQ~ zjXM620>ITlsd%b@*(#J){;AqgMO056l$TSO%ZUT38xu9;m0jZ?@DVw(i%$guR{=&| zQ2m`>8UH>;Un~NI?QUy)&qV_Z#^w7Q#xg(J@Vs%gl?aeMumIX8%1g;1XQ&X=e}Jj0 z$yVf3h@tkaCQMFtXHk~MSutz>-h2WP6_(Pn0}1|!hz>m0W^jfs%b_@bcqtuxxa3JjNB3TQZgb;Q_;?AE$!p*h) z>lpF=xF2WNY%5-~f=>yt@JjF?EOS;}0;ZQFr9-%d?tDXm3NnY$?y0l&51Nk~PpE)Y ziLJ`V;GR91SG#Icm21yo5^D*Ipx*jvcKGYtS;{rM+Nl%?E+ahpMqr zf2)j8jjYPlVX-?~ID%LDZoYSZ$@TfRUz@tsY5S@W(qBog(5#&2bzZ>3F7P9fmt0#x z29!F2tc*!?z>F#L=~B%D=I@PXYb1j`Q$ltp89npElJauAA&8g&wND5ZQUp|HMA=``ZbYIrrs};?pqM47H2aInt2cL?DZ9GCs zD*I^$DcX1|16$>fv@0F(3P1QP8&2ub$)lsxa^hSkyoJ>H&3N6Ae=wWr_y25J7;Ju3 ziA=T?4?b=Ja2DDrxvzr@S{s6=p%F!qAd=$LBL1hvR;Qw(zkp@L?nLIy{pMrFw?2}E zairB0v*gUOp`x05o79u3AnF4}!Om6Sf5jR>SJYecIRu|yE1K>qY{kjLyI|vJRakE;aC9Pt&6UCC6mMZJ*^sylXr)D|+9cK^WmTbc1h8Li#T)tV;fl}b1R?mly$@edbc za^88xZK5^1BF8qMlkI|>NGEkr6P!Lpz#Lj=BH=N^9uL{E4y!7&1q&yoiZ~SP~+W$~Ar1;DvqF zC-9ODWwZpyzqRB_eKq-MeIXgYBGmw3s5&H{dN&(CVXA^;b%q0C!;9lTz_EJ>LgWGY zEcQ~kRBZmC3G86xPyCdGM1f11ebNxWHKEo-idfrE^Nvckl3);5hK(S38cXjt8cv}aaVjjnOr;oTmmqrL}f}wh&@~LkXapEK!sA==~t0E#9iexg?bietasj53@ z-^4|ETmdcH;0IV-Q)JWw161}+Vl!r9=Uj=*z>B7pB5PYAr#!m~>~?uxgEojWhpV5! zTg*@Aoh@5JHYK*IzqV4D#3X(2L1)CFk|q<4U~EYGI4cRY6u(0fWBKD%A2T1o`z7T- zs}+m?hy|gsVxhr|PH`w7R^+awdsSfeWOMg`N9~#{6%%R^#KI&Fsqh|TV5z{eC($nv z`D}6c3FF%(GY6F=w4^jYsv1f_wSR(|=B!8qs4zGq#L!`r%BK$e@ES3^ByryZ;Hy6q z5fgywq!)%L75MBGcf4xU%Q-r7pH@OGg>rXAGDjT;GUlZ=yhCg)wtdi)kWTc05?18Q z#;359`ZVU~(3vuXm^;J~nE8jDQdl%-5G%}uLE4N+G$AHeD{%rCnaCvT{vPUC`8B{s))?n=#$m>+zFl}R0CHN z>9wDLZ*i~6#wZm|9?b%#Em}G{A-y(4dk^yJ##DkqaQMi&AC1JQpOB@(sk&!P)w+~e zT4$T75Tn8rcWKb|6XSDr7Es99wx;$BC=hXynH~Xzq)jKLD;AA%&<1$qA=SxYv#!|} zI+GXfNNM7+U$5vNU7s!VW5n5)z1<~1;C*pTChV8 zZfjMdFcyQq7F@7ia_1xFBiL|WjFKu8B{IoMriz>L%|Vqiq{T=Y(I7suH55bY*b3`J zb*@-T@V}60bN$SCx5j|9S_4wi38-0F<-OR8v-j7GUmS>nq+WHyuQ4?r&j<#_K zsbUN>n8#bC0Tdu(dRKxeQW2vE6)ZebBO}6s0_ssn1`Ut^k8~^K(%vl(A>f{3?!e~f zO_i_ZG~^yPAocjGF%Q2ZZZ5{#aHylNzV%qHlp}n=O|1-N5IHmwjg;emy+nr;Ek_d- z6fx9v9wujUi}`X+jihQAy_lmdXlLsBsz)(4T-}?rQ%c%8bq2l9OL(47OpF|fkUZ>^ zf^9>~$~-dfkq9fck*K0MtWg6}G0%bnQR5*s#r(0owXb42VN+ZV?(rdSKjbvC`NWZdzEEHP=x`%k(|A$FIWu{ zDC%;hLMD3V^i(W^wQ$S!TF!MqlM1&rKU`8ZXaXm3s)5CX4Uwm+3029&iNR2Re>v8(L-ZiXb|7_h^wxLwesX~R#Qq*y`PA*nc6dQ7d680b@vV~F+(+pA@@X#xf2`dJN z>POg$t_sHr&b(J-+L`iohSEgGaS^hCfUP<$3&9pMn@~n{MegTv$rGdfp}tUm*xMiO z?+z{Y*F!3Xj!b9q<`B|HrIKRhXnnT(QRLyUYZPzeBcK{xLrnB-&!`j49qSE+(cVJX z3lbhU82}BnQ*x1!1lpr+k$~4A=}Ld%D6)d5Dypa_9k>mPp4c#p_#_pG6X?AZd=T%2W=d*SB%=F>GP$(Sk?+=GJgnPRtkM`nLmlsooa?_Hk3^Dd~Hi0Ry4cVwg_C3|GJ$@0<@W_4`PqP^fy~z{z+v$NVWV z;Lav^4Ea`Z=afx46>Fu{XHZs_<7>uqsO21>g|^h2C}_l2jS1wEED%yE3&}=iC7+iX zDt`+aH#lvQS6sGBUP*)@S;m|%eqs#7SO~lO!=XU?{_y7UK9Hvj`@9rwSN_Y~Lwk4zUd#X-dE`9sLYQ<*&ob zOQ^Hf!BHPwu?$|5JGqbbNvawMQ(t)W=yWQVqiEPsAMjE3${DSI#eo@6RaOa-OAK?B zI(9ro+-#@TrJ+smpXjKrQQu5XZL=CGx(XZVC>^!1I2d^~l0yx3M{Enx6F8d7XCg<2 zLm>!HHPiw~b@v?IvxJex@h%`(4*PJo^Qa#uspRl}EHUSRLbD)XaiX~XGcZj49Cu!$ z6S9h;FRC4x!t$NEi4L^NPyb?g%Rp}zulr%{4xLJf7qp~NrC>c(AyQB6ksZ@G0wt>8 z$T#tE$O#D82({ksv9o$hsz2r3*Pm3j~L8MB1tZ3Cf2x$ExjZ^fJ8Tv?ozZ__f;7pz`n^3x!j z_mB>(9p^?lkO5LWjd$(s8U+h1>B1r3!0F>X-61OZQph_nb}W)E=V*O|^*FU-869$0 zJ}NFsNlltKHs6kmHPlk+QSWXe0ZXxsTxws)VVC8ZlM~}3p{`)#_Kj<{3$aAdq#Ld_ z;RSbsYAXy`)uP#`9YEp?#(Xw*Y%JUtx&|bCq3-@uZLTsX<~iI+x3$#o>cHfu8}9`Y zeXHbdVEZCP4+-Yu>MmMaJLtC77g2Wd>?&+#=o;u@OR$Um{*F_o^WZ>N-y#~W2B(G( zO~zr*G!n3q;kZKztqP!X{5lBeB%?J2Ov!YNxk_jNFTIPLK5_KGU`=m(%kHh~8)^ey z7uJ?`6k*m`o`ki^BBG?-UBwfPGvy2~l1|7+nL0KK^1!b6`g^;E4+gQFz^p&eYsIPU zvjtHG#K@DYeR3o;K(Bg$Gwx&l3HbovEmT~{@7XfoT)j|$xwr>huz_}DJ-zsaPUZf zr*;@FL)ZKRV@J;<;6*CA6-K~LN=id2$D6TXRbmdDXo+l$FFA!`-y9EM?=8`*Tgt&y zNyrK!LTGf9T-ktxHsIHE>?7OvFsw?PIXv9oR|e*F3U)Zu(RX;D0Smrsj&%zn{o%g; zu6eVh$BYtfkwY*wl6e>dI8PE2nGRnmE)w|QzbiR_^GPT7p>Yo_k0%@ob>Tgao?S;q zdpi5BhHDYE4w4+1Twc7LYJ_U4@E>G$Vhcfi#hzw9T5yh&AUg;aBD7p){k*|hE9)~d zrdF}DhLGf>L`bxa@`{O*wpE5oQ{~)NygKW^02E#MX$@z0*T{(u4;Ew|Y=``~``Zs= z0kY?;>=mbq%7h;&6f4#|j6uKm%(P7M+6YrHY- z405457i$**S@o2dM+Zn0A9_wTy7ll+xlH2pkr6Pk-x|bnxT?RWW8m-z-bsh^)sJ^A zkjZV6co)kimDT4Vnl-^mM&yR;_M7 zHrm_S%Rq{sa=07{gonqEPbUF{zCXQ!s2W*nHh(t@LF^yWMn3$<1ctkKxN6bhjIHWp zSYapzM#WTU;0y~1pjr?S6J|Z=oU@6kiLn9T4v1b2SB3hz`p1uM`;9S!u5OA!=ivsJ zL!4XY2pb{8wxffeKsIny0>k_j@NoQtU0v5<+qS!IvaL)D3T}6u#H%Iz4QA2{x%<0Te*&#PKofsJ%g=t zI4)uar#daiUs#p?5OTqRNxm$Kt?FQ1h6~ZjtEOu0IQ%xlI#=fboH$n(96dPP)77hX zT5M-s|ImJi7*5Eh%%@Vz=uiDIF;m&qVUfzSMu6U89Ny{$)37_iRH$M{fHVSTRg66Z zLOhiXUHpREi#cC9cIwF3KsiK%xte64r4RRYbqpOC-RJv-akYmGf+~2CQ0IOe9o)hr z#FtwG2crf5Lzn~b?Z@j1HKR}dAmT}sHeAKauVg5}Akf`Cj`OfIQ6&13pQ(T~vzoqdc{PH1E90&2Eo)umvU)aAEOK8^}C}8$VwMR9{ zMlrS}1QjAVxjHKU)UkuZj3odVye6QSN0i#z**$u6e2e#|c*Qmb!>)Z`qc@H%oX~G9 z8yB)Aa@%Frr;|%06$sHS?In`NS@w;Gy27v~q-Oi^Uzk9BYv{;We^*c6qVzP!9z$#T z2SyG~oQfvtD>NObw!AWlL?jpVU9Id}dKVz5|G=%7agS~HJ4MaAv$b7;lF0q1+J4U zCi|7nGwIZTOyTX5xeq|LJP1vvVCp=<{vKckM)wWv{;8>}Z5kUrFxb`2L395rd=jed z9~?P&6eoqGq5jOAINGGPnMnpFK4m^dFom&dDCQ#vdxI_eLgUeb&T~-u?UG3i-Zd1Z ztH$IecLpOOKQ(c1814=nW?VFf@mpV4*T8{8p{@R>jeDXqya*e0U>o+#4I^hFJ7t8# z*kwNTp!tyTb`-$)%E*}=L8zmz^S^4ALZ&Amx)Sc3a6M;g>YIl4j}P~D!_aAHAYtQb zv3H^Vf#Lm!kDrbJ=`xTOb4*bAnsyuAF^S>Cr0gYEER~Dy5_a)I>+J>&Wv|)=JIR11 zICiVX36zn^iSf}vka!VY8}Jn&eOJ%m$kCyu2F&?}zyu6cA@BnTz%hkE!%p&hveO0E zgP3ymZJ(J2r$w+x4ko@ckq8bU4-W=V;pk070XcB2XTp&RzH9R1nyNc(+oEe z5^&Zpm2Q3oOR8O6eGvEVqpN>t>Z`Zz8#y+-zYniq>Fe)*9;RtMa?$XRL!$={O`JSE z9ZO>IH*y&Hg`93QRd1uSx{TPbW)}eUQchm>9XT_3?9hSHL74xLf%K`3SqRs|koLg| z3?7&m>D^ZKJ&eh^L0Uu_`%Yrt)M*Ug96#nxWLrOG?!n5Pe1xHf^=jxadc7BK*g4kI z$9TfE@AUV~D&NM&-jT7f(ca$nz8(&{m-^jc&RlpNGO?L>3=EHs9Xvd7V)FEKB!)Mn zAf(A<=W=uTI@wAOrIF8kZY?xDnTSQFPEQ`6ICNla1V}7Eu3Q{CQnbqbwSA$kzTT0M z@v)vwtGQkgZP~$`2}=!Ekx4Q4-O3jy#wQi4j-AAIJR(NY0`UY^3>$y z;)!*~Cnk;_K0JQ#!2Z#Zp}_$rFOhLygxYhQ%LK$+xB)kNdb^M}Jv4Brt$khf*AVea zxY|A1h^>4>ali}3gUr*dM56bbyAkQ9^0q5XPHISQ1s}OZ&M7t@L&G zcXfwD!xO`YTR`J~#ak1PG2PMU>KlI9tUilP^kVMi?{vw1zqt#?hvYD%aaQ-ZV+G$; zgZ#=w2Sbpy6JIgaRdqW%T8@u}j_)rI)P_5|Lp=l;CXyrQmxWR^l}noJ0`>5DRb-c> zTm%_;ixh@2?d|LAEe{0tAMT#$ZQj>V{Y6uSp%F^7o5PrTYtAxKK*l3Fv*sh_y~f>D zz+1*>M&Y6&gv{eU!J(8JdiDjg(_391Y~Fg{VAm*yDMQ1913ewxec|p-NHZK3$W)~k z7nE8Okc6A@rKRFmW7TnpkrpEI*}Ln!%n+XE`=$n@V+FGmEC$cf5lrcO6>&DYMP%_iu0By>Z{M(a_!l zlL(wbcyALx4fl3*l)KA)43$8szWR>N4tzR0IO7lwmHU>uw{&%Mh6e^A#Ur7SLnkKN zLkA9R+0nEINq~oq>ts7Bb{t!U4N04Gv|c&{Ld5-H^KRo^U4m`a@RFh^hs7*lm+_%C za(41Qv%%~0`M2-fwR791hNg)_V`C$IU{7PY+11!>rpl)_pFVZ^OlMMn?}G z8CtVp+s<9PHkN$`3+I-4s$e2|1{w`t#-%6A(bYf|68D-rvE3qvqiO6i6J$~&**UyS z*j;~`aEH1>x0!dDYVRpn;lAE}W1vy13s@A-cncdSGVXiL6&x&JpGJs(OfnD%|pm{ z_l|8n$0h~=MCYMZY{JKPP;0^n+sj5jXf7J}zGB3t9XQ^aPOw~6<3XUlZ|a=68QB1U z&@!@2VC;GlQ3P;-wt zXWXe67($-Hv*Tr=1`%`nXiwj8)2z7#FJT`FwVk+zik?$g-=OPo;`v?gH@9OmP*TP{ znum#G4Gu#X=?=9Ymx>2FJ116M11sy+%IjMhhNhAO?=!a<59|??U1FT+sTYj*M0@Xn z%@@qAaA}9SdU~#+R&9pID$wqxV<+y$qxy~#lD_=GC!I7C=*@Mat>ZB3dAfRor(cES z)i&ERtiZ$UT|l62OH4Mau%&F$mFYpX`iZ?0zKg6j*>pU>YOb@>>RsK@T-9A5SS@xs z$#y1I!xmERa^7;Vt^FdwUv)6N=M{r3PhGm@3F{F{8{SJvKf*ksz=#90iq&{0a%<=q z>$v+`j=T)BQHxWtSOBmj+uvhuG=agK?(v5(W~Ng($$jut)g{z$jo_8TNL!91(_mIQ zIsR_*PMkV}c{+F^C?N85FBsPbExQNf9Lz(xi{LE+;OXd{<{iepvxu|hjLk!K)%7~4)eFhivx{QP%cV!>@W83FP#{j#WkFtXBu|h6)EtMkCHW&Kaa~r zK zj3Q@l#{(Q$m5rvNr{(rpPb_lQ$J-n{#-MArk~&pT#r%%ja2p}&iBvKucemzU@zGmx z6DD&cGcEU8QYLx}ZoQ0`QFhCnos&1?JAYpL{~8@$lY`gv;59q&njLt}4!mXuUb6%L z@7n>y&u#ttnuq#~>unN%y~9);YueKqY~17O^WPx1K8XE^yKpSKdvsgNV$ig@b{@fL z_tiUBdG40ye`m_=o?ush)6cPczN=|I=-%7X`E#6NJl+*->kqz93f*f0`}a1rwzlkQ zAHmeiu^n?kPjGh_o1FtKy*u`{wl?iP{C+8kJr`@cnihiYrq&J|?tNf;>s)Xt=-Pf1 zU)uV1<3USf=RLNlE4&L&n@t$UOl<3EzJ`+SmOWkgw!67;F6i2MSWDiA@#=v+*Mjcm zz2g|5xJPzf4VHuMJwpxn*4@>FCwsd$en479+3v>HtHGtcrsGB98r}VBa53oGhS3_5 zu#K+ck7;Bd<(WV;z`p$)d!_bY&6)lujOTwZ9IzM zyKe~ffJx)fdVCADHQ`BXQ@@t|qVeq=*tv6P4;Bm@-SHyp4B@=eBRgIW&VvqzG4>t_ zHoXWq4*k;j)($l9=-T=@>E%l%Fxt917~I_+@`D7mgH11>_O203cn0_GWpzL|jBopz zn(?Hi9j_g&b`5lb-%ZU&JztPUzKnEMQ}bfbv$wfxFOC@K+w&5b4q~yyY>S8mTiZfc3g+R98c4}L5w0@qteV`FwnAR$m3tP z?%=K$gV%zAT?f{!^Sci1ZT>Zy9IV4D>O4bjd(eL4fd8Kap09vhoh{dbo|dMreWdKZ z)?WpyTKBg0^>^%fA$T=d)zlv9+S}OjO8_#6EYpsIJNJU0EnOQb4Q%ZOZ#>N{BS_-9 z4(E|ZrMa)Kacm#%Lwp-5C2P7`ejW6+HZ?WwZT)8OFFMko2yey_ z4Bb-M-8kmQxqqwLLS4uaZvTN4eA?8m+rOi^wWVoKN8b(~(v<#zrbmOn0P(&2rpIk9*O4vrL0?PL-thj-7)7q_ZXp=%*5E#z=2PD{vUgVtWP1By zymOb{;u+)ihMTVjeNDT%MjG8XFQKFHGr`XT#eGOQZS3jY)7VN)@7cIqzO&_(pl|Q) zV5qqs@?16CR0dH0h)V8V6Af7IS=-;&*0>kC+|s>ixmI_}OTn7frslpwO-NFWHh((! zO;r13u%@ZMqt0v`4mUQnzJ#87m+N?jo1PC=HSgLt5^ik#RPY~zOHJN^ku&B} zUuhfVz^K;7|J~k~H%W3__f=)}eP305)_rAF-#y*aJ<~JObL?WF@8Hj%&?m415mE%H z%NBJoY=>mQw9F7ilMpF_M@X_w(v(f}CU`8s-334byEur0z}~yv{=Mqi1-^j)!ymH< zbY;GLnfb1K`QA(CS^kgs5^dXuzl+D_)BL|;=9DxpQo?h0AIfkPua4k&A%CS6q;)=$=}ddcZ9D$ z6cW8vPq)Uo?RU}jp5{LVfFEPXRxDo z4n*S^?-OPDVfqDhihHU#J*<6;Jh!#Q-roMgRbr4^3nLoNLt&m2D;m@3{=vBOKAz8# zhC7A_ZQ%HT!hLl~P&NVBP9OAZ{~I2ayc-ynZJKUxcV}$oAh)y22pQ(|%W%!6+k7(X za&v7lGk^*~R&rydJD@c`2=Io-k?wZuzW~&9HSjri?ZI*F+wkGA@~(BCXFS_BgTcPloh29c zLmqS0alh~#fETCwEuQr~$8v+|sIL!<|HNOz_WU!?n%(|%z6d~^o)0Oh?cWk!!V-3E z2WrY*CVnj>TA|O~zIFvQLB$2ojJeiq#=b{zm*%?=MUG?p?qBi$0={-ku*tiCgG;lQ zv6n}>{~J)r90tNl1AXWqEP$CnuS>RhY<-`g9BLoqw}8%nK+D-JPy`zf^mq8iBLbL% zeVH@KUKM_X>M(t{7Y|fOe#2d*en=~42HwZ{AA|CCZjrP&@T|81W`V9+=tIlf{i}F@ zLDI&1BGi35@X^^*_xO*fb8_l@oc}3?CS^E8ElA{{&V2hVE~iJdoVK`{Fq#{r~1eFzr{lBinj!WB+Ly5I?K^v*c7J@_Q~1FTmyr!)D!yJ z7dZOI2jEf(Mnmx@1Yp;E2E)GqAfmpyDO?xWC;$xRhc!_8LMH|9de^`}$;^coXvWyZ z5}X>)B<;i2PXU6pyWmH`|0Kb11~-Kp2rKPc(0^XPjc1!v`(!rRlWTaohh;}9*2G!@ z#teJMv>E>y!H6&H-{=31xU+myz>8*?mG%ZLiAr^xbmFY#H83YjUDSw&M^$*Nr#9z4 zsKn?^ehwakZSed2OF#tL)I1UijCkCENOMOWm@dT`Ofu12UBjDTOgwG_Clh{eu?y@3 zYzmf?UnVf)2oq@7_XRxf)OS4sF(h?#tliIv>ONDNlpjm*a-CNiaM(@qk{|Z5A47@+&?*h)JNciXk2Da^W+EfX%Rd!7D^!YW& zD8?4B0GWA!Bt#Z^WIE}q_*Hm>#eM730CAmvf@nPT9wJ^@de7cMLF42~dmltMjiB^g zthZ<`@Oz=_zX8K`b(Jvb^xQw>R{`c(3?2+4AZ;H&p`|3)0OCR;)AyKwz%~bE+dCQj zp5C`XgVsEKjW(pVG@~W8!9NsuJ9J(F{?z|TjmCAuYa|eBLl@%5ID|WT;Jyw)w0Kj< zCEQL;(G9QH8&927DxEJBON}FyD868z0(z-XNT>PExZm?PMyw$#K^`kQF)@NC!Gshi zTQN4IA;f89ocRW#P4+Dir+Emaln>?(Rv*V9*R^eT!{fdFU^1{jPGbaCuruuUId|~M z^)@7K?1>E=pU^Re3%f>299a;RWaCDI>=~eG{UU>PJ21&E2o3OZZK~bGfoSbRS$_;t zFXoY{dzX#@W*-^>2)#<%jW{c*vM?ZtnZ1V+5y1Kc>>l&EukYb}qIY3yh~^aBJ%s}K zb8OCA{3#p)M1AiO6ldIqV3_Ry5m7z6N-%mJIK=6C(0k1;_!8-_86N@p?g#E4d=jQ;zN`{Ns?a8Y}eC%t}M(D!T`yrn_$U@jLqgHng_Jrl(Aybd z`aAlg&Z7DxwsdO!3WgvVb6|kzUHDTSXu9SU;$C-OgI>cJC;!?E2KL&X&O7xs6b2$ka5Bvx51Z9+3;v7rsFL7r8{me=;`zK(%u}Y7JvO~@P zE+#*P;@ibJnHnJ7%z8&j2J@pcR@%TGqa}!C`=e-(Bt}2rq}8u=~Bmgjug}z zLM&5sM-Vv#8dA=lx{b7irKn7e7hzdxhuP>|wu?Rvzs&8oej{wMX-}IC22&1hy34-< zl)udX5B~ic95xvk5#!zNk)@u6@nK{&dhWYH@!5wu`54uO)dFXLt z?QUQ(5C#u%q$%Juse4|qJ6g7Q|k>3pv}7UNKj1L z8Cl*-kPQF`>a<=#+iw8+|KJcg#vz`^5ih~ZA=%zRfnLLjRc4{-|B{#kYMQ&${wjYE z{4KS&7jcMgk3(u=M<#>l{TF@Tws4*$=LXXcGU&BZU*VtPLa+`u!P>`P_-lwG0j5Sa z2m{ZtE!zvi@UsL`I~F0n4)nL7L`=TQ&;uyaaB5kwOYFW2ozm$!uj05;wc5>EE?3B; zlZiwsQ>yEJ7`($@;YqXIsm8<@TZ@T!rmFEU_&odtkOw0m+x7f+Fgs^%(32o>wlgy$ z(d%iukd1TGyUzcXrvj^e@!x#%6TbszK`c=*!{9gk27Yz4-}=43`Op6vJ~b)T@WW>S zR#cC$WM|sDpeAG5+dh{Ot~|?!D)Lp>jpAA7I{yrh1^UjXZa;d*LJ|vh_#O^NvA`UC z>dvEwDw3Tb_yUK0r>^j-1oLb#UCcFR^9(&D5f{B#*M{#&G%rHTCCJwN40i40%J~Zq zOlpMh^Vh-CcJQ6^7ads13%|%ott~15nOqrgGMU3kjox06TBE)TzJ_Fn#abUk;`uKB zJ0jwroxhMER)d?^87Xiw&pdhvA!UXCz%fC`L$(>K9m8?O^t}c>Rltf9co?B;bU@@j zw8P(lp8w`3ng4O(pnTB884 zmO5;`&w>h8zd0|xFV?~cSeTU_)z}v^5iqfH3|RdM;pOll)x;Oi+N4Q_eK;mBbO@5z zehS5Pe;c;47T)3H(!iNBY(1_|FLuIbVc9bE1Qq33w`TzuT#(y3@&`IRaE^7HhcHPV zgJS@%^+mu#wRbTIc7F?7>%||Hb{EoL;uzkT$z-Uk47i2pFer8osSZ1H0(ey=-n zt~H1}N$r*?dQK4$Ku;C7l zduDjhMQLiWhe>}P>(vwxpzb_ufD#4{<_)MpPIz3NSq-;9E3j%Fmt&@xe-)U?kq#V*;o=3ax!4ol@79p**? z=#3{nT-ncHZt4f+fW`oHA}lt4#XJuF5wdlMI3n=D8h@CXW?4){vGp;BwL>8C80v8$SCK$~y2JK|3%4@KMRzD+SGz<7SEa$ofM*!JgUp9Z zp+4%|!D$vG@HHq3@XRTdEsa$cf#qM0`s98gi02axbu<;JDGmr`uT2#|C4JYko{t#w z=fI?Z)}{01HR0;qB11OnQH4rqO1*^+r?LQ*Llyu{sfsS=8=HVdoAAF1%>o-ukIGS| zmSfYt8a0Ifn>GwOB#l{xrI#o!NGNS>EfX@RTf}97By~TyZiXvW}TOM1Z_($Qf9KC^0o##hJ`+6}2(8IzYVwV>)k zD-Cy8(t$F8&D2$3EvGf;x0f-M`w;Txemko7Wi0gP`DOh0WAyRMXqI)tTTr(UcW~q; zn;eQ32DLt!F{+Bif|+(1)`S#n9QL`adHaV@po{m)AGZ=hkqMdWT#H0qa=N- z2c(AH=bJ#}K>af~hj1S9P6oGRtYpDrxWT2+tsplw9xv%IZGMUNKY(p@H@FsYVN`61 zd9M8m>=jQ#Ff|W?2Y^K9uvi%Ar5GZ3Sz58lsJYI|LFhE{D0d`?ESZyaFT^AsVt6-e zA`;p#Z5MYY!OKtxu0^Ga*bdoWxxiEFvLY)@-Q~d==G`VNc%5>Ju~;fsZQC&H$QX;k zuJi4+1z-0%6oN1EWWZfZ?=+SBa!RQ;+PV#oH6~G~Qa|j-hc#SwECap~rCwJsK+Eug zKnSj20{?>EVaRdS!wJ~0K~WEazsF*wJfn%W=(AQ>u~uIz8#$pgR0lVHuX3RkmBHE9 z-5WgV>$`hZz`lnjtll;RZyh|%*TD^VH8(+w+nClpC9YHo^Ir9#l6r)LiG71tgKD8L zwlr=);2IZ_!lZDM#sVNLL+~-U(l!Bejn*AStjqD76)0}?juJ219&{&J%(HX^qA<4! zpnbN6J}J|P^=Q6nnnlY1;;@qh|1bET0bty>I(1nF%z_e|w=w8#C6PK4gkb*(IO0x}uT|;#|X+<^ha2 z%`Dzl5Q^G7-6@yk2Y{f|TcDbC@Ru0vVW@;O9Qh+HgXpSOK}CgR*8hiSny%i$ir$O% zu~w89wn()j*iM0H1b<>~2`eWth5}~VDk>KN_W^thD9DjRWNWusCdBV6$=phCyUk)1 zuw(@@rv0XyyYGXhaLz-LK_gwOl=GQXA}+y8FIHigVehy@DBM(H)ndD6QEi5*UAe2U z%mLM9Xii|Ez`*TLB?jgOn7UhkcRGO+qEBZF751|jv%o8$4xet!eSJxl4%ktf$kJS)tN~l* zYiOioRtN;H%zAR6w&jy}y`^EibpT5~RHAYOa`;ct`r9zlGQ;9M zg;f>2XeJ&-e!WvFA;2ye5!WmX9)1ahy7|JlqFzpF8A~ww2S2hZh%Xn=s zQ7~slC11HoG*xeOaNfuK;Jyx3MrrIUrib6cz)9}eTC1)^Q(h}sVkSif`|-Ux^3!*O z(tU7IzKi^HabjU)-<(lWI=f)H7}+=?Uqx=7c|h{9Fr^gqoHcjkVg-ar^i*%DCP`Hg z5DWDZY6p#7_tn+~Qd9>MVo_NDA^cHm-M z$P65m%K`D)Lq*KvLMfH8dVafvao6?RN*W8NgvOu z5`1pc{91_tbQ;q`m<@)vhfT6nnC3-SddJB3Gd$2k0^3wqvdcGxbk6dICKens$KacY zbT^~|n{6zBhw1(cn9FCa95^v1<=Q-a4mhL{e*vk6MUd~K4>1~KQMFXA`?FpZR!yML z_JJOmFtceaS3~K<7#7b_x9Bo&%t-U6s)18s)G>fH*4CxA(8#xVJ zI+}?qARzNym=w7%a8S6XSgv=RAiRuUu_BJCwSmrBzJ+Pzz^!tv2$JagHIx))Bid`J zUUXVmnhCrKAP7nuX5mbwgT@K+gxN?gZ%_I6`RFR3;tF;My z{1>}wlmr|_mR5z>DQl$!|FPxL>Lm3e@jb@vWv2h*4`JLbm zn3H#cJeI_DEK^t8U>7VOEhCZG10^e0%f(959(RL!{4yMPP$H62PviO>p4Z#aO&;94 zlu;|C&s36H+6Cm%vZHeCfg+}lFAMotwe1A1)u~jn)pBjCm4y0DjI&lsz^K($HQlsq z$MtYCfZ&fn;5UP}VHH+<4~W^8X=p&L34;!s5)txySln!-R;*P!Mt9&>@;`x;ofe3A zawAe9@dfTm5U)3mGG#9qa2iBG1Cp?lC99(*c+Ux?OwqTOHaBW=u^^Xgviyc3%E|+3 zP~S8%%~q@3>8Prv=?{#J`M`W=2>M#vsGH8FwP|h|#8Df0gTH%Nu6OX?T&Ys1)f7(~ zcbq~Buhj47N!+uNZeqy>2t|W$DawOA@+d~m>6w%On_gvPvfy$vWWZLES(K*fhZT!8CKN|Wrlu=Eo7An-^s)&4VJB%qEGy8}s~X@;iHkmc zf1Ah}PBpN*b`oXRvi)?lM}z+#9(LcB)r}+`W9f zhNXo)Q{9xB#T@vp3EtNfo$t&QeY_}CRXrJ(o)waF=-_&Ogw#|f!z)dcx=Tr^Qt`Ay z;albmzIgbGQiBYnP(5>mY*Z;G#oLe>)qK2pNOciR-E0-sMKRwI4%d5~{k{FeX1)kY zHgyYm&dU*Mvo_kQ%wjQ|^;5kvgi*O=Pzuya$C$o)VVj0}K|@fPs9xBnpYCNbq+A?s z(@z(WLA44VRRmKPCl0!42iwfkQ`3o949{l62rBzz+vlg-oYO%`uFDx*w1Mb%O?1*1 zg(%tdz$d=6`^_lJR2gHT#mp+oGL7|$2>CmRvP`oJ3%7jM(53`aHW?ro0Wk~PywbV` z0E3qGfDeJrK}m-OwxVa z%3&RjDT`F9AGImK!?UMR{-|XalxoX}@<(?{a;<`kQK+R6uKo}UOPQlm?xKo7te7=w zM>(TmyI9XIqMXsaoRWrTf2&e4hPFsY38R;EETN#mdV$hIr=?nMFKRp~6b87MhSgNrMOX>~N+*;W%H};<0l$5Sw9r+y zV%4Lx&{R;Y^eHV=yo6{LSnh|E63T+PL18Ku9>IgES!8naC>fMjQm2&F3CqaEJ-d35 z5<#WLnBo@&NAyFq&JAM&q!1zp^ngvaIiOE;09xCR@;@bM;G#7ylsd@#Yf9$}^<>rz;o7)nMHm76>D73Y6k0D+q9x@3 zfn8Hd>}0|c!-7dEbrUgn5twm-Qa71!rlXs-jM6$|LgBDO6EOCpyv};x!-eL&_9(CO zqK^5v-FB4Md88p0#F}jLI+M~=#W(Fjl-IdJICtyfqrA>b_~Hdll)PDuPaJM6ALn(R z7(UmIqfe8V#Sw%5D6ey@`|hwAC2;OZ23MEod7W_e>C5QnGs#d-W9NCDXNJBXk8(5P z17?gm=XsrnuuY<08v{|Fo#%D#gsW{9rz4!5=XGj+^h<01qx_Bk|M?p%b L2Osgl9|`{tNjPOA literal 0 HcmV?d00001 diff --git a/luda-editor/psd/examples/drag-drop-browser/index.html b/luda-editor/psd/examples/drag-drop-browser/index.html new file mode 100644 index 000000000..785f39fcf --- /dev/null +++ b/luda-editor/psd/examples/drag-drop-browser/index.html @@ -0,0 +1,22 @@ + + + + + + + PSD demo + + + + + + + diff --git a/luda-editor/psd/examples/drag-drop-browser/src/lib.rs b/luda-editor/psd/examples/drag-drop-browser/src/lib.rs new file mode 100644 index 000000000..dfba8c18a --- /dev/null +++ b/luda-editor/psd/examples/drag-drop-browser/src/lib.rs @@ -0,0 +1,410 @@ +use console_error_panic_hook; + +use percy_dom::prelude::*; +use wasm_bindgen::prelude::*; +use wasm_bindgen::Clamped; +use wasm_bindgen::JsCast; +use web_sys::*; + +use css_rs_macro::css; + +use psd::Psd; +use std::cell::RefCell; +use std::collections::HashMap; +use std::ops::Deref; +use std::rc::Rc; + +/// Wraps our application so that we can return it to the caller of this WebAssembly module. +/// This ensures that our closures that we're holding on to in the App struct don't get dropped. +/// +/// If we we didn't do this our closures would get dropped and wouldn't work. +#[wasm_bindgen] +struct AppWrapper(Rc>); + +#[wasm_bindgen] +impl AppWrapper { + /// Create a new AppWrapper. We'll call this in a script tag in index.html + #[wasm_bindgen(constructor)] + pub fn new() -> AppWrapper { + console_error_panic_hook::set_once(); + + let mut app = App::new(); + + let closure_holder = Rc::clone(&app.raf_closure_holder); + + let store = Rc::clone(&app.store); + + let app = Rc::new(RefCell::new(app)); + let app_clone = Rc::clone(&app); + + // Whenever state gets updated we'll re-render the page + { + let on_msg = move || { + let store = Rc::clone(&store); + let app = Rc::clone(&app); + let closure_holder = Rc::clone(&closure_holder); + + let re_render = move || { + let store = Rc::clone(&store); + let app = Rc::clone(&app); + + let vdom = app.borrow().render(); + app.borrow_mut().update(vdom); + + store.borrow_mut().msg(&Msg::SetIsRendering(false)); + }; + let mut re_render = Closure::wrap(Box::new(re_render) as Box); + + window().request_animation_frame(&re_render.as_ref().unchecked_ref()); + + *closure_holder.borrow_mut() = Some(Box::new(re_render)); + }; + + { + let mut app = app_clone.borrow_mut(); + app.store.borrow_mut().on_msg = Some(Box::new(on_msg)); + app.start(); + } + } + + AppWrapper(app_clone) + } +} + +/// Our client side web application +#[wasm_bindgen] +struct App { + store: Rc>, + dom_updater: PercyDom, + /// Holds the most recent RAF closure + raf_closure_holder: Rc>>>>, +} + +#[wasm_bindgen] +impl App { + /// Create a new App + fn new() -> App { + let vdom = html! {
}; + let mut dom_updater = PercyDom::new_append_to_mount(vdom, &body()); + + let state = State { + psd: None, + layer_visibility: HashMap::new(), + is_rendering: false, + }; + + let on_msg = None; + let store = Store { state, on_msg }; + let store = Rc::new(RefCell::new(store)); + + App { + store, + dom_updater, + raf_closure_holder: Rc::new(RefCell::new(None)), + } + } + + /// Start the demo + fn start(&mut self) { + let demo_psd = include_bytes!("../demo.psd"); + + self.store.borrow_mut().msg(&Msg::ReplacePsd(demo_psd)); + + let vdom = self.render(); + self.update(vdom); + } + + /// Render the virtual-dom + fn render(&self) -> VirtualNode { + let store = &self.store; + let store_clone = Rc::clone(store); + + let store = store.borrow(); + + let psd = store.psd.as_ref().unwrap(); + + let mut layers: Vec = psd + .layers() + .iter() + .enumerate() + .map(|(idx, layer)| { + let store = Rc::clone(&store_clone); + + let checked = *store.borrow().layer_visibility.get(layer.name()).unwrap(); + + let background_color_class = if checked { + "layer-dark-background" + } else { + "layer-light-background" + }; + + let checked = if checked { "true" } else { "false" }; + + let name = layer.name(); + + html! { +
+ +
+ } + }) + .collect(); + layers.reverse(); + + let vdom = html! { +
+ +
+ +
); + + file_reader.set_onload(Some(onload.as_ref().unchecked_ref())); + onload.forget(); + } + > + { "Drag and drop here to upload a PSD" } +
+
+ +
+

Layers

+ { layers } +
+
+ }; + + vdom + } + + /// Patch the DOM with a new virtual dom and update our Canvas' pixels + fn update(&mut self, vdom: VirtualNode) -> Result<(), JsValue> { + self.dom_updater.update(vdom); + + let psd = &self.store.borrow(); + let psd = &psd.psd; + let psd = psd.as_ref().unwrap(); + + // Flatten the PSD into only the pixels from the layers that are currently + // toggled on. + let mut psd_pixels = psd + .flatten_layers_rgba(&|(idx, layer)| { + let layer_visible = *self + .store + .borrow() + .layer_visibility + .get(layer.name()) + .unwrap(); + + layer_visible + }) + .unwrap(); + + let psd_pixels = Clamped(&psd_pixels[..]); + let psd_pixels = + ImageData::new_with_u8_clamped_array_and_sh(psd_pixels, psd.width(), psd.height())?; + + let canvas: HtmlCanvasElement = document() + .get_element_by_id("psd-visual") + .unwrap() + .dyn_into()?; + let context = canvas + .get_context("2d")? + .unwrap() + .dyn_into::()?; + + canvas.set_width(psd.width()); + canvas.set_height(psd.height()); + + context.put_image_data(&psd_pixels, 0., 0.)?; + + Ok(()) + } +} + +/// A light wrapper around State, useful when you want to accept a Msg and handle +/// anything impure (such as working with local storage) before passing the Msg +/// along the State. Allowing you to keep State pure. +struct Store { + state: State, + on_msg: Option>, +} + +/// You'll usually just want the underlying State, so we Deref for convenience. +impl Deref for Store { + type Target = State; + + fn deref(&self) -> &Self::Target { + &self.state + } +} + +/// Handles application state +struct State { + /// The current PSD that is being displayed + psd: Option, + /// Layer name -> whether or not it currently toggled on + layer_visibility: HashMap, + /// Whether or not we've already requested to render on the next animation frame + is_rendering: bool, +} + +impl Store { + /// Send a new message to our Store, usually to update State + fn msg(&mut self, msg: &Msg) { + let is_rendering = self.state.is_rendering; + + self.state.msg(msg); + + if !is_rendering { + self.state.msg(&Msg::SetIsRendering(true)); + self.on_msg.as_ref().unwrap()(); + } + } +} + +impl State { + /// Update State given some new Msg + fn msg(&mut self, msg: &Msg) { + match msg { + // Replace the current PSD with a new one + // Happens on page load and after drag/drop + Msg::ReplacePsd(psd) => { + let psd = Psd::from_bytes(psd).unwrap(); + + // When we upload a new PSD we set all layers to visible + let mut layer_visibility = HashMap::new(); + for layer in psd.layers().iter() { + layer_visibility.insert(layer.name().to_string(), true); + } + + self.psd = Some(psd); + self.layer_visibility = layer_visibility; + } + // Set whether or not a layer is currently toggled on/off + Msg::SetLayerVisibility(idx, visible) => { + let visibility = self + .layer_visibility + .get_mut(self.psd.as_mut().unwrap().layer_by_idx(*idx).name()) + .unwrap(); + + *visibility = *visible; + } + // Have we already queued up a re-render? + Msg::SetIsRendering(is_rendering) => { + self.is_rendering = *is_rendering; + } + } + } +} + +/// All of our Msg variants that are used to update application state +enum Msg<'a> { + /// Replace the current PSD with a new one, usually after drag and drop + ReplacePsd(&'a [u8]), + /// Set whether or not a layer (by index) should be visible + SetLayerVisibility(usize, bool), + /// Set that the application is planning to render on the next request animation frame + SetIsRendering(bool), +} + +fn window() -> web_sys::Window { + web_sys::window().unwrap() +} + +fn document() -> web_sys::Document { + window().document().unwrap() +} + +fn body() -> web_sys::HtmlElement { + document().body().unwrap() +} + +static APP_CONTAINER: &'static str = css! {r#" +:host { + display: flex; + width: 100%; + height: 100%; +} +"#}; + +static _LAYOUT: &'static str = css! {r#" +.left-column { +} + +.right-column { + background-color: #f7f7f7; + padding-left: 5px; + padding-right: 5px; +} + +.layer-dark-background { + background-color: #b8b8b8; +} + +.layer-light-background { + background-color: #e0e0e0; +} +"#}; + +// Just like println! but works in the browser +// +// clog!("Hello world {}", some_variable); +#[macro_export] +macro_rules! clog { + ( $( $t:tt )* ) => { + web_sys::console::log_1(&format!( $( $t )* ).into()); + } +} diff --git a/luda-editor/psd/src/blend.rs b/luda-editor/psd/src/blend.rs new file mode 100644 index 000000000..1a4ee1f27 --- /dev/null +++ b/luda-editor/psd/src/blend.rs @@ -0,0 +1,428 @@ +use crate::sections::layer_and_mask_information_section::layer::BlendMode; + +pub(crate) type Pixel = [u8; 4]; + +// Multiplies the pixel's current alpha by the passed in `opacity` +pub(crate) fn apply_opacity(pixel: &mut Pixel, opacity: u8) { + let alpha = opacity as f32 / 255.; + pixel[3] = (pixel[3] as f32 * alpha) as u8; +} + +/// +/// https://www.w3.org/TR/compositing-1/#simplealphacompositing +/// `Cs = (1 - αb) x Cs + αb x B(Cb, Cs)` +/// `cs = Cs x αs` +/// `cb = Cb x αb` +/// `co = cs + cb x (1 - αs)` +/// Where +/// - Cs: is the source color +/// - Cb: is the backdrop color +/// - αs: is the source alpha +/// - αb: is the backdrop alpha +/// - B(Cb, Cs): is the mixing function +/// +/// `αo = αs + αb x (1 - αs)` +/// Where +/// - αo: the alpha value of the composite +/// - αs: the alpha value of the graphic element being composited +/// - αb: the alpha value of the backdrop +/// +/// Final: +/// `Co = co / αo` +/// +/// *The backdrop is the content behind the element and is what the element is composited with. This means that the backdrop is the result of compositing all previous elements. +pub(crate) fn blend_pixels(top: Pixel, bottom: Pixel, blend_mode: BlendMode, out: &mut Pixel) { + // TODO: make some optimizations + let alpha_s = top[3] as f32 / 255.; + let alpha_b = bottom[3] as f32 / 255.; + let alpha_output = alpha_s + alpha_b * (1. - alpha_s); + + let (r_s, g_s, b_s) = ( + top[0] as f32 / 255., + top[1] as f32 / 255., + top[2] as f32 / 255., + ); + let (r_b, g_b, b_b) = ( + bottom[0] as f32 / 255., + bottom[1] as f32 / 255., + bottom[2] as f32 / 255., + ); + + let blend_f = map_blend_mode(blend_mode); + let (r, g, b) = ( + composite(r_s, alpha_s, r_b, alpha_b, blend_f) * 255., + composite(g_s, alpha_s, g_b, alpha_b, blend_f) * 255., + composite(b_s, alpha_s, b_b, alpha_b, blend_f) * 255., + ); + + // NOTE: make all assignments _after_ all reads to avoid issues when top or bottom is out + out[0] = (r.round() / alpha_output) as u8; + out[1] = (g.round() / alpha_output) as u8; + out[2] = (b.round() / alpha_output) as u8; + out[3] = (255. * alpha_output).round() as u8; +} + +type BlendFunction = dyn Fn(f32, f32) -> f32; + +/// Returns blend function for given BlendMode +fn map_blend_mode(blend_mode: BlendMode) -> &'static BlendFunction { + // Modes are sorted like in Photoshop UI + // TODO: make other modes + match blend_mode { + BlendMode::PassThrough => &pass_through, // only for groups + // -------------------------------------- + BlendMode::Normal => &normal, + BlendMode::Dissolve => &dissolve, + // -------------------------------------- + BlendMode::Darken => &darken, + BlendMode::Multiply => &multiply, + BlendMode::ColorBurn => &color_burn, + BlendMode::LinearBurn => &linear_burn, + BlendMode::DarkerColor => &darker_color, + // -------------------------------------- + BlendMode::Lighten => &lighten, + BlendMode::Screen => &screen, + BlendMode::ColorDodge => &color_dodge, + BlendMode::LinearDodge => &linear_dodge, + BlendMode::LighterColor => &lighter_color, + // -------------------------------------- + BlendMode::Overlay => &overlay, + BlendMode::SoftLight => &soft_light, + BlendMode::HardLight => &hard_light, + BlendMode::VividLight => &vivid_light, + BlendMode::LinearLight => &linear_light, + BlendMode::PinLight => &pin_light, + BlendMode::HardMix => &hard_mix, + // -------------------------------------- + BlendMode::Difference => &difference, + BlendMode::Exclusion => &exclusion, + BlendMode::Subtract => &subtract, + BlendMode::Divide => ÷, + // -------------------------------------- + BlendMode::Hue => &hue, + BlendMode::Saturation => &saturation, + BlendMode::Color => &color, + BlendMode::Luminosity => &luminosity, + } +} + +fn pass_through(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +/// https://www.w3.org/TR/compositing-1/#blendingnormal +/// This is the default attribute which specifies no blending. The blending formula simply selects the source color. +/// +/// `B(Cb, Cs) = Cs` +#[inline(always)] +fn normal(color_b: f32, color_s: f32) -> f32 { + color_s +} + +fn dissolve(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +// Darken modes + +/// https://www.w3.org/TR/compositing-1/#blendingdarken +/// Selects the darker of the backdrop and source colors. +/// +/// The backdrop is replaced with the source where the source is darker; otherwise, it is left unchanged. +/// +/// `B(Cb, Cs) = min(Cb, Cs)` +#[inline(always)] +fn darken(color_b: f32, color_s: f32) -> f32 { + color_b.min(color_s) +} + +/// https://www.w3.org/TR/compositing-1/#blendingmultiply +/// The source color is multiplied by the destination color and replaces the destination. +/// The resultant color is always at least as dark as either the source or destination color. +/// Multiplying any color with black results in black. Multiplying any color with white preserves the original color. +/// +/// `B(Cb, Cs) = Cb x Cs` +#[inline(always)] +fn multiply(color_b: f32, color_s: f32) -> f32 { + color_b * color_s +} + +/// https://www.w3.org/TR/compositing-1/#blendingcolorburn +/// +/// Darkens the backdrop color to reflect the source color. Painting with white produces no change. +/// +/// ```text +/// if(Cb == 1) +/// B(Cb, Cs) = 1 +/// else +/// B(Cb, Cs) = max(0, (1 - (1 - Cs) / Cb)) +///``` +#[inline(always)] +fn color_burn(color_b: f32, color_s: f32) -> f32 { + if color_b == 1. { + 1. + } else { + (1. - (1. - color_s) / color_b).max(0.) + } +} + +/// See: http://www.simplefilter.de/en/basics/mixmods.html +/// psd_tools impl: https://github.com/psd-tools/psd-tools/blob/master/src/psd_tools/composer/blend.py#L139 +/// +/// This variant of subtraction is also known as subtractive color blending. +/// The tonal values of fore- and background that sum up to less than 255 (i.e. 1.0) become pure black. +/// If the foreground image A is converted prior to the operation, the result is the mathematical subtraction. +/// +/// `B(Cb, Cs) = max(0, Cb + Cs - 1)` +#[inline(always)] +fn linear_burn(color_b: f32, color_s: f32) -> f32 { + (color_b - color_s - 1.).max(0.) +} + +fn darker_color(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +// Lighten modes + +/// https://www.w3.org/TR/compositing-1/#blendinglighten +/// Selects the lighter of the backdrop and source colors. +/// +/// The backdrop is replaced with the source where the source is lighter; otherwise, it is left unchanged. +/// +/// `B(Cb, Cs) = max(Cb, Cs)` +#[inline(always)] +fn lighten(color_b: f32, color_s: f32) -> f32 { + color_b.max(color_s) +} + +/// https://www.w3.org/TR/compositing-1/#blendingscreen +/// Multiplies the complements of the backdrop and source color values, then complements the result. +/// +/// The result color is always at least as light as either of the two constituent colors. +/// Screening any color with white produces white; screening with black leaves the original color unchanged. +/// The effect is similar to projecting multiple photographic slides simultaneously onto a single screen. +/// +/// `B(Cb, Cs) = 1 - [(1 - Cb) x (1 - Cs)] = Cb + Cs - (Cb x Cs)` +#[inline(always)] +fn screen(color_b: f32, color_s: f32) -> f32 { + color_b + color_s - (color_b * color_s) +} + +/// https://www.w3.org/TR/compositing-1/#blendingcolordodge +/// +/// Brightens the backdrop color to reflect the source color. Painting with black produces no changes. +/// +/// ```text +/// if(Cb == 0) +/// B(Cb, Cs) = 0 +/// else if(Cs == 1) +/// B(Cb, Cs) = 1 +/// else +/// B(Cb, Cs) = min(1, Cb / (1 - Cs)) +/// ``` +#[inline(always)] +fn color_dodge(color_b: f32, color_s: f32) -> f32 { + if color_b == 0. { + 0. + } else if color_s == 1. { + 1. + } else { + (color_b / (1. - color_s)).min(1.) + } +} + +/// See: http://www.simplefilter.de/en/basics/mixmods.html +/// +/// Adds the tonal values of fore- and background. +/// +/// Also: Add +/// `B(Cb, Cs) = Cb + Cs` +#[inline(always)] +fn linear_dodge(color_b: f32, color_s: f32) -> f32 { + (color_b + color_s).min(1.) +} + +fn lighter_color(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +// Contrast modes + +/// https://www.w3.org/TR/compositing-1/#blendingoverlay +/// Multiplies or screens the colors, depending on the backdrop color value. +/// +/// Source colors overlay the backdrop while preserving its highlights and shadows. +/// The backdrop color is not replaced but is mixed with the source color to reflect the lightness or darkness of the backdrop. +/// +/// `B(Cb, Cs) = HardLight(Cs, Cb)` +/// Overlay is the inverse of the hard-light blend mode. See the definition of hard-light for the formula. +#[inline(always)] +fn overlay(color_b: f32, color_s: f32) -> f32 { + hard_light(color_s, color_b) // inverted hard_light +} + +/// https://www.w3.org/TR/compositing-1/#blendingsoftlight +/// +/// Darkens or lightens the colors, depending on the source color value. +/// The effect is similar to shining a diffused spotlight on the backdrop. +/// +/// ```text +/// if(Cs <= 0.5) +/// B(Cb, Cs) = Cb - (1 - 2 x Cs) x Cb x (1 - Cb) +/// else +/// B(Cb, Cs) = Cb + (2 x Cs - 1) x (D(Cb) - Cb) +/// ``` +/// with +/// ```text +/// if(Cb <= 0.25) +/// D(Cb) = ((16 * Cb - 12) x Cb + 4) x Cb +/// else +/// D(Cb) = sqrt(Cb) +/// ``` +fn soft_light(color_b: f32, color_s: f32) -> f32 { + // FIXME: this function uses W3C algorithm which is differ from Photoshop's algorithm + // See: https://en.wikipedia.org/wiki/Blend_modes#Soft_Light + let d = if color_b <= 0.25 { + ((16. * color_b - 12.) * color_b + 4.) * color_b + } else { + color_b.sqrt() + }; + + if color_s <= 0.5 { + color_b - (1. - 2. * color_s) * color_b * (1. - color_b) + } else { + color_b + (2. * color_s - 1.) * (d - color_b) + } +} + +/// https://www.w3.org/TR/compositing-1/#blendinghardlight +/// +/// Multiplies or screens the colors, depending on the source color value. +/// The effect is similar to shining a harsh spotlight on the backdrop. +/// +/// ```text +/// if(Cs <= 0.5) +/// B(Cb, Cs) = Multiply(Cb, 2 x Cs) = 2 x Cb x Cs +/// else +/// B(Cb, Cs) = Screen(Cb, 2 x Cs -1) +/// ``` +/// See the definition of `multiply` and `screen` for their formulas. +#[inline(always)] +fn hard_light(color_b: f32, color_s: f32) -> f32 { + if color_s < 0.5 { + multiply(color_b, 2. * color_s) + } else { + screen(color_b, 2. * color_s - 1.) + } +} + +fn vivid_light(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +fn linear_light(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +#[inline(always)] +fn pin_light(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +#[inline(always)] +fn hard_mix(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +// Inversion modes + +/// https://www.w3.org/TR/compositing-1/#blendingdifference +/// +/// Subtracts the darker of the two constituent colors from the lighter color. +/// Painting with white inverts the backdrop color; painting with black produces no change. +/// +/// `B(Cb, Cs) = | Cb - Cs |` +#[inline(always)] +fn difference(color_b: f32, color_s: f32) -> f32 { + (color_b - color_s).abs() +} + +/// https://www.w3.org/TR/compositing-1/#blendingexclusion +/// +/// Produces an effect similar to that of the Difference mode but lower in contrast. +/// Painting with white inverts the backdrop color; painting with black produces no change +/// +/// `B(Cb, Cs) = Cb + Cs - 2 x Cb x Cs` +#[inline(always)] +fn exclusion(color_b: f32, color_s: f32) -> f32 { + color_b + color_s - 2. * color_b * color_s +} + +/// https://helpx.adobe.com/photoshop/using/blending-modes.html +/// +/// Looks at the color information in each channel and subtracts the blend color from the base color. +/// +/// `B(Cb, Cs) = Cb - Cs` +#[inline(always)] +fn subtract(color_b: f32, color_s: f32) -> f32 { + (color_b - color_s).max(0.) +} + +/// https://helpx.adobe.com/photoshop/using/blending-modes.html +/// +/// Looks at the color information in each channel and divides the blend color from the base color. +/// In 8- and 16-bit images, any resulting negative values are clipped to zero. +/// +/// `B(Cb, Cs) = Cb / Cs` +#[inline(always)] +fn divide(color_b: f32, color_s: f32) -> f32 { + if color_s == 0. { + color_b + } else { + color_b / color_s + } +} + +fn hue(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +fn saturation(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +fn color(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +fn luminosity(color_b: f32, color_s: f32) -> f32 { + unimplemented!() +} + +/// https://www.w3.org/TR/compositing-1/#generalformula +/// +/// `Cs = (1 - αb) x Cs + αb x B(Cb, Cs)` +/// `cs = Cs x αs` +/// `cb = Cb x αb` +/// `co = cs + cb x (1 - αs)` +/// Where +/// - Cs: is the source color +/// - Cb: is the backdrop color +/// - αs: is the source alpha +/// - αb: is the backdrop alpha +/// - B(Cb, Cs): is the mixing function +/// +/// *The backdrop is the content behind the element and is what the element is composited with. This means that the backdrop is the result of compositing all previous elements. +fn composite( + color_s: f32, + alpha_s: f32, + color_b: f32, + alpha_b: f32, + blend_f: &BlendFunction, +) -> f32 { + let color_s = (1. - alpha_b) * color_s + alpha_b * blend_f(color_b, color_s); + let cs = color_s * alpha_s; + let cb = color_b * alpha_b; + cs + cb * (1. - alpha_s) +} diff --git a/luda-editor/psd/src/lib.rs b/luda-editor/psd/src/lib.rs new file mode 100644 index 000000000..cd27b0871 --- /dev/null +++ b/luda-editor/psd/src/lib.rs @@ -0,0 +1,343 @@ +//! Data structures and methods for working with PSD files. +//! +//! You are encouraged to read the PSD specification before contributing to this codebase. +//! This will help you better understand the current approach and discover ways to improve it. +//! +//! psd spec: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/ + +#![deny(missing_docs)] + +use std::collections::HashMap; +use std::ops::Deref; + +use thiserror::Error; + +use sections::file_header_section::FileHeaderSectionError; +use sections::image_data_section::ImageDataSectionError; +use sections::image_resources_section::ImageResourcesSectionError; +use sections::layer_and_mask_information_section::layer::PsdLayerError; + +use crate::psd_channel::IntoRgba; +pub use crate::psd_channel::{PsdChannelCompression, PsdChannelKind}; +pub use crate::sections::file_header_section::{ColorMode, PsdDepth}; +use crate::sections::image_data_section::ChannelBytes; +use crate::sections::image_data_section::ImageDataSection; +pub use crate::sections::image_resources_section::ImageResource; +use crate::sections::image_resources_section::ImageResourcesSection; +pub use crate::sections::image_resources_section::{DescriptorField, UnitFloatStructure}; +pub use crate::sections::layer_and_mask_information_section::layer::BlendMode; +pub use crate::sections::layer_and_mask_information_section::layer::PsdGroup; +pub use crate::sections::layer_and_mask_information_section::layer::PsdLayer; +use crate::sections::layer_and_mask_information_section::LayerAndMaskInformationSection; +use crate::sections::MajorSections; + +use self::sections::file_header_section::FileHeaderSection; + +mod blend; +mod psd_channel; +mod render; +mod sections; + +/// An list of errors returned when processing PSD file. +/// +/// This list is intended to grow over time and it is not recommended to exhaustively match against it. +#[derive(PartialEq, Debug, Error)] +#[non_exhaustive] +pub enum PsdError { + /// Failed to parse PSD header + #[error("Failed to parse PSD header: '{0}'.")] + HeaderError(FileHeaderSectionError), + /// Failed to parse PSD layer + #[error("Failed to parse PSD layer: '{0}'.")] + LayerError(PsdLayerError), + /// Failed to parse PSD data section + #[error("Failed to parse PSD data section: '{0}'.")] + ImageError(ImageDataSectionError), + /// Failed to parse PSD resource section + #[error("Failed to parse PSD resource section: '{0}'.")] + ResourceError(ImageResourcesSectionError), +} + +/// Represents the contents of a PSD file +/// +/// ## PSB Support +/// +/// We do not currently support PSB since the original authors didn't need it, but adding +/// support should be trivial. If you'd like to support PSB please open an issue. +#[derive(Debug)] +pub struct Psd { + file_header_section: FileHeaderSection, + image_resources_section: ImageResourcesSection, + layer_and_mask_information_section: LayerAndMaskInformationSection, + image_data_section: ImageDataSection, +} + +impl Psd { + /// Create a Psd from a byte slice. + /// + /// You'll typically get these bytes from a PSD file. + /// + /// # Example + /// + /// ```ignore + /// let psd_bytes = include_bytes!("./my-psd-file.psd"); + /// + /// let psd = Psd::from_bytes(psd_bytes); + /// ``` + pub fn from_bytes(bytes: &[u8]) -> Result { + let major_sections = MajorSections::from_bytes(bytes).map_err(PsdError::HeaderError)?; + + let file_header_section = FileHeaderSection::from_bytes(major_sections.file_header) + .map_err(PsdError::HeaderError)?; + + let psd_width = file_header_section.width.0; + let psd_height = file_header_section.height.0; + let channel_count = file_header_section.channel_count.count(); + + let layer_and_mask_information_section = LayerAndMaskInformationSection::from_bytes( + major_sections.layer_and_mask, + psd_width, + psd_height, + ) + .map_err(PsdError::LayerError)?; + + let image_data_section = ImageDataSection::from_bytes( + major_sections.image_data, + file_header_section.depth, + psd_height, + channel_count, + ) + .map_err(PsdError::ImageError)?; + + let image_resources_section = + ImageResourcesSection::from_bytes(major_sections.image_resources) + .map_err(PsdError::ResourceError)?; + + Ok(Psd { + file_header_section, + image_resources_section, + layer_and_mask_information_section, + image_data_section, + }) + } +} + +// Methods for working with the file section header +impl Psd { + /// The width of the PSD file + pub fn width(&self) -> u32 { + self.file_header_section.width.0 + } + + /// The height of the PSD file + pub fn height(&self) -> u32 { + self.file_header_section.height.0 + } + + /// The number of bits per channel + pub fn depth(&self) -> PsdDepth { + self.file_header_section.depth + } + + /// The color mode of the file + pub fn color_mode(&self) -> ColorMode { + self.file_header_section.color_mode + } +} + +// Methods for working with layers +impl Psd { + /// Get all of the layers in the PSD + pub fn layers(&self) -> &Vec { + &self.layer_and_mask_information_section.layers + } + + /// Get a layer by name + pub fn layer_by_name(&self, name: &str) -> Option<&PsdLayer> { + self.layer_and_mask_information_section + .layers + .item_by_name(name) + } + + /// Get a layer by index. + /// + /// index 0 is the bottom layer, index 1 is the layer above that, etc + pub fn layer_by_idx(&self, idx: usize) -> &PsdLayer { + self.layer_and_mask_information_section + .layers + .get(idx) + .unwrap() + } + + /// Get all of the groups in the PSD, in the order that they appear in the PSD file. + pub fn groups(&self) -> &HashMap { + &self.layer_and_mask_information_section.groups + } + + /// Get the group ID's in the order that they appear in Photoshop. + /// (i.e. from the bottom of layers view to the top of the layers view). + pub fn group_ids_in_order(&self) -> &Vec { + self.layer_and_mask_information_section + .groups + .group_ids_in_order() + } + + /// Returns sub layers of group by group id + pub fn get_group_sub_layers(&self, id: &u32) -> Option<&[PsdLayer]> { + match self.groups().get(id) { + Some(group) => Some( + &self.layer_and_mask_information_section.layers.deref() + [group.contained_layers.clone()], + ), + None => None, + } + } + + /// Given a filter, combine all layers in the PSD that pass the filter into a vector + /// of RGBA pixels. + /// + /// We'll start from the top most layer and iterate through the pixels. + /// + /// If the pixel is transparent, recursively blend it with the pixels below it until + /// we hit an opaque pixel or we hit the bottom of the stack. + /// + /// TODO: Take the layer's blend mode into account when blending layers. Right now + /// we just use ONE_MINUS_SRC_ALPHA blending regardless of the layer. + pub fn flatten_layers_rgba( + &self, + filter: &dyn Fn((usize, &PsdLayer)) -> bool, + ) -> Result, PsdError> { + // When you create a PSD but don't create any new layers the bottom layer might not + // show up in the layer and mask information section, so we won't see any layers. + // + // TODO: We should try and figure out where the layer name is so that we can return + // a completely transparent image if it is filtered out. But this should be a rare + // use case so we can just always return the final image for now. + if self.layers().is_empty() { + return Ok(self.rgba()); + } + + // Filter out layers based on the passed in filter. + let layers_to_flatten_top_down: Vec<&PsdLayer> = self + .layers() + .iter() + .enumerate() + // here we filter transparent layers and invisible layers + .filter(|(_, layer)| (layer.opacity > 0 && layer.visible) || layer.clipping_mask) + .filter(|(idx, layer)| filter((*idx, layer))) + .map(|(_, layer)| layer) + .collect(); + + let pixel_count = self.width() * self.height(); + + // If there aren't any layers left after filtering we return a complete transparent image. + if layers_to_flatten_top_down.is_empty() { + return Ok(vec![0; pixel_count as usize * 4]); + } + + // During the process of flattening the PSD we might need to look at the pixels on one of + // the layers below if an upper layer is transparent. + // + // Anytime we need to calculate the RGBA for a layer we cache it so that we don't need + // to perform that operation again. + let renderer = render::Renderer::new(&layers_to_flatten_top_down, self.width() as usize); + + let mut flattened_pixels = Vec::with_capacity((pixel_count * 4) as usize); + + // Iterate over each pixel and, if it is transparent, blend it with the pixel below it + // recursively. + for pixel_idx in 0..pixel_count as usize { + let left = pixel_idx % self.width() as usize; + let top = pixel_idx / self.width() as usize; + let pixel_coord = (left, top); + + let blended_pixel = renderer.flattened_pixel(pixel_coord); + + flattened_pixels.push(blended_pixel[0]); + flattened_pixels.push(blended_pixel[1]); + flattened_pixels.push(blended_pixel[2]); + flattened_pixels.push(blended_pixel[3]); + } + + Ok(flattened_pixels) + } +} + +// Methods for working with the final flattened image data +impl Psd { + /// Get the RGBA pixels for the PSD + /// [ R,G,B,A, R,G,B,A, R,G,B,A, ...] + pub fn rgba(&self) -> Vec { + self.generate_rgba() + } + + /// Get the compression level for the flattened image data + pub fn compression(&self) -> &PsdChannelCompression { + &self.image_data_section.compression + } +} + +// Methods for working with the image resources section +impl Psd { + /// Resources from the image resources section of the PSD file + pub fn resources(&self) -> &Vec { + &self.image_resources_section.resources + } +} + +impl IntoRgba for Psd { + /// The PSD's final image is always the same size as the PSD so we don't need to transform + /// indices like we do with layers. + fn rgba_idx(&self, idx: usize) -> Option { + Some(idx) + } + + fn red(&self) -> &ChannelBytes { + &self.image_data_section.red + } + + fn green(&self) -> Option<&ChannelBytes> { + match self.color_mode() { + // For 16 bit grayscale images I'm sometimes seeing two channels. + // Really not sure what the second channel is so until we know what it is we're ignoring it.. + ColorMode::Grayscale => None, + _ => self.image_data_section.green.as_ref(), + } + } + + fn blue(&self) -> Option<&ChannelBytes> { + self.image_data_section.blue.as_ref() + } + + fn alpha(&self) -> Option<&ChannelBytes> { + self.image_data_section.alpha.as_ref() + } + + fn psd_width(&self) -> u32 { + self.width() + } + + fn psd_height(&self) -> u32 { + self.height() + } +} + +#[cfg(test)] +mod tests { + use crate::sections::file_header_section::FileHeaderSectionError; + + use super::*; + + // Makes sure non PSD files get caught right away before getting a chance to create problems + #[test] + fn psd_signature_fail() { + let psd = include_bytes!("../tests/fixtures/green-1x1.png"); + + let err = Psd::from_bytes(psd).expect_err("Psd::from_bytes() didn't catch the PNG file"); + + assert_eq!( + err, + PsdError::HeaderError(FileHeaderSectionError::InvalidSignature {}) + ); + } +} diff --git a/luda-editor/psd/src/psd_channel.rs b/luda-editor/psd/src/psd_channel.rs new file mode 100644 index 000000000..f5cefc260 --- /dev/null +++ b/luda-editor/psd/src/psd_channel.rs @@ -0,0 +1,386 @@ +use crate::sections::image_data_section::ChannelBytes; +use crate::sections::PsdCursor; +use thiserror::Error; + +pub trait IntoRgba { + /// Given an index of a pixel in the current rectangle + /// (top left is 0.. to the right of that is 1.. etc) return the index of that pixel in the + /// RGBA image that will be generated. + /// + /// If the final image or layer is the size of the PSD then this will return the same idx, + /// otherwise it will get transformed. + /// + /// index could be `None` if layer's top or left is negative. + /// + /// index could be bigger than the size of the image if layer's bottom, right, width, height is bigger than image. + fn rgba_idx(&self, idx: usize) -> Option; + + /// The first channel + fn red(&self) -> &ChannelBytes; + + /// The second channel + fn green(&self) -> Option<&ChannelBytes>; + + /// The third channel + fn blue(&self) -> Option<&ChannelBytes>; + + /// The fourth channel + fn alpha(&self) -> Option<&ChannelBytes>; + + /// The width of the PSD + fn psd_width(&self) -> u32; + + /// The height of the PSD + fn psd_height(&self) -> u32; + + fn generate_rgba(&self) -> Vec { + let rgba_len = (self.psd_width() * self.psd_height() * 4) as usize; + + let red = self.red(); + let green = self.green(); + let blue = self.blue(); + let alpha = self.alpha(); + + // TODO: We're assuming that if we only see two channels it is a 16 bit grayscale + // PSD. Instead we should just check the Psd's color mode and depth to see if + // they are grayscale and sixteen. As we run into more cases we'll clean things like + // this up over time. + // if green.is_some() && blue.is_none() && alpha.is_none() { + // return self.generate_16_bit_grayscale_rgba(); + // } + + let mut rgba = vec![0; rgba_len]; + + use crate::psd_channel::PsdChannelKind::*; + + self.insert_channel_bytes(&mut rgba, Red, red); + + // If there is a green channel we use it, otherwise we use the red channel since this is + // a single channel grey image (such as a heightmap). + if let Some(green) = green { + self.insert_channel_bytes(&mut rgba, Green, green); + } else { + self.insert_channel_bytes(&mut rgba, Green, red); + } + + // If there is a blue channel we use it, otherwise we use the red channel since this is + // a single channel grey image (such as a heightmap). + if let Some(blue) = blue { + self.insert_channel_bytes(&mut rgba, Blue, blue); + } else { + self.insert_channel_bytes(&mut rgba, Blue, red); + } + + if let Some(alpha_channel) = alpha { + self.insert_channel_bytes(&mut rgba, TransparencyMask, alpha_channel); + } else { + // If there is no transparency data then the image is opaque + for idx in 0..rgba_len / 4 { + rgba[idx * 4 + 3] = 255; + } + } + + rgba + } + + /// Generate an RGBA Vec from a composite image or layer that uses 16 bits per + /// pixel. We do this by mapping the 16 bits back down to 8 bits. + /// + /// The 16 bits are stored across the red and green channels (first and second). + fn generate_16_bit_grayscale_rgba(&self) -> Vec { + match self.red() { + ChannelBytes::RawData(red) => match self.green().unwrap() { + ChannelBytes::RawData(green) => sixteen_to_eight_rgba(red, green), + ChannelBytes::RleCompressed(green) => { + let green = &rle_decompress(green); + + sixteen_to_eight_rgba(red, green) + } + }, + ChannelBytes::RleCompressed(red) => { + let red = &rle_decompress(red); + + match self.green().unwrap() { + ChannelBytes::RawData(green) => sixteen_to_eight_rgba(red, green), + ChannelBytes::RleCompressed(green) => { + let green = &rle_decompress(green); + sixteen_to_eight_rgba(red, green) + } + } + } + } + } + + /// Given some vector of bytes, insert the bytes from the given channel into the vector. + /// + /// Doing it this way allows us to allocate for one vector and insert all 4 (RGBA) channels into + /// it. + fn insert_channel_bytes( + &self, + rgba: &mut Vec, + channel_kind: PsdChannelKind, + channel_bytes: &ChannelBytes, + ) { + match channel_bytes { + ChannelBytes::RawData(channel_bytes) => { + let offset = channel_kind.rgba_offset().unwrap(); + + for (idx, byte) in channel_bytes.iter().enumerate() { + if let Some(rgba_idx) = self.rgba_idx(idx) { + rgba[rgba_idx * 4 + offset] = *byte; + } + } + } + // https://en.wikipedia.org/wiki/PackBits + ChannelBytes::RleCompressed(channel_bytes) => { + self.insert_rle_channel(rgba, channel_kind, &channel_bytes); + } + } + } + + /// rle decompress a channel (R,G,B or A) and insert it into a vector of RGBA pixels. + /// + /// We use the channels offset to know where to put it.. So red would go in 0, 4, 8.. + /// blue would go in 1, 5, 9.. etc + /// + /// https://en.wikipedia.org/wiki/PackBits - algorithm used for decompression + fn insert_rle_channel( + &self, + rgba: &mut Vec, + channel_kind: PsdChannelKind, + channel_bytes: &[u8], + ) { + let mut cursor = PsdCursor::new(&channel_bytes[..]); + + let mut idx = 0; + let offset = channel_kind.rgba_offset().unwrap(); + let len = cursor.get_ref().len() as u64; + + while cursor.position() < len { + let header = cursor.read_i8() as i16; + + if header == -128 { + continue; + } else if header >= 0 { + let bytes_to_read = 1 + header; + if cursor.position() + bytes_to_read as u64 > len { + break; + } + for byte in cursor.read(bytes_to_read as u32) { + if let Some(rgba_idx) = self.rgba_idx(idx) { + if let Some(buffer) = rgba.get_mut(rgba_idx * 4 + offset) { + *buffer = *byte; + } + } + + idx += 1; + } + } else { + let repeat = 1 - header; + + if cursor.position() + 1 > len { + break; + } + let byte = cursor.read_1()[0]; + for _ in 0..repeat { + if let Some(rgba_idx) = self.rgba_idx(idx) { + if let Some(buffer) = rgba.get_mut(rgba_idx * 4 + offset) { + *buffer = byte; + } + } + + idx += 1; + } + }; + } + } +} + +/// Rle decompress a channel +fn rle_decompress(bytes: &[u8]) -> Vec { + let mut cursor = PsdCursor::new(&bytes[..]); + + let mut decompressed = vec![]; + + while cursor.position() != cursor.get_ref().len() as u64 { + let header = cursor.read_i8() as i16; + + if header == -128 { + continue; + } else if header >= 0 { + let bytes_to_read = 1 + header; + for byte in cursor.read(bytes_to_read as u32) { + decompressed.push(*byte); + } + } else { + let repeat = 1 - header; + let byte = cursor.read_1()[0]; + for _ in 0..repeat { + decompressed.push(byte); + } + }; + } + + decompressed +} + +/// Take two 8 bit channels that together represent a 16 bit channel and convert them down +/// into an 8 bit channel. +/// +/// We store the final bytes in the first channel (overwriting the old bytes) +fn sixteen_to_eight_rgba(channel1: &[u8], channel2: &[u8]) -> Vec { + let mut eight = Vec::with_capacity(channel1.len()); + + for idx in 0..channel1.len() { + if idx % 2 == 1 { + continue; + } + + let sixteen_bit = [channel1[idx], channel1[idx + 1]]; + let sixteen_bit = u16::from_be_bytes(sixteen_bit); + + let eight_bit = (sixteen_bit / 256) as u8; + + eight.push(eight_bit); + eight.push(eight_bit); + eight.push(eight_bit); + eight.push(255); + } + + for idx in 0..channel2.len() { + if idx % 2 == 1 { + continue; + } + + let sixteen_bit = [channel2[idx], channel2[idx + 1]]; + let sixteen_bit = u16::from_be_bytes(sixteen_bit); + + let eight_bit = (sixteen_bit / 256) as u8; + + eight.push(eight_bit); + eight.push(eight_bit); + eight.push(eight_bit); + eight.push(255); + } + + eight +} + +/// Indicates how a channe'sl data is compressed +#[derive(Debug, Eq, PartialEq)] +#[allow(missing_docs)] +pub enum PsdChannelCompression { + /// Not compressed + RawData = 0, + /// Compressed using [PackBits RLE compression](https://en.wikipedia.org/wiki/PackBits) + RleCompressed = 1, + /// Currently unsupported + ZipWithoutPrediction = 2, + /// Currently unsupported + ZipWithPrediction = 3, +} + +impl PsdChannelCompression { + /// Create a new PsdLayerChannelCompression + pub fn new(compression: u16) -> Option { + match compression { + 0 => Some(PsdChannelCompression::RawData), + 1 => Some(PsdChannelCompression::RleCompressed), + 2 => Some(PsdChannelCompression::ZipWithoutPrediction), + 3 => Some(PsdChannelCompression::ZipWithPrediction), + _ => None, + } + } +} + +/// The different kinds of channels in a layer (red, green, blue, ...). +#[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] +#[allow(missing_docs)] +pub enum PsdChannelKind { + Red = 0, + Green = 1, + Blue = 2, + TransparencyMask = -1, + UserSuppliedLayerMask = -2, + RealUserSuppliedLayerMask = -3, +} + +/// Represents an invalid channel +#[derive(Debug, Error)] +pub enum PsdChannelError { + #[error("Channel {channel:#?} not present")] + ChannelNotFound { channel: PsdChannelKind }, +} + +impl PsdChannelKind { + /// Create a new PsdLayerChannel + pub fn new(channel_id: i16) -> Option { + match channel_id { + 0 => Some(PsdChannelKind::Red), + 1 => Some(PsdChannelKind::Green), + 2 => Some(PsdChannelKind::Blue), + -1 => Some(PsdChannelKind::TransparencyMask), + -2 => Some(PsdChannelKind::UserSuppliedLayerMask), + -3 => Some(PsdChannelKind::RealUserSuppliedLayerMask), + _ => None, + } + } + + /// R -> 0 + /// G -> 1 + /// B -> 2 + /// A -> 3 + pub fn rgba_offset(self) -> Result { + match self { + PsdChannelKind::Red => Ok(0), + PsdChannelKind::Green => Ok(1), + PsdChannelKind::Blue => Ok(2), + PsdChannelKind::TransparencyMask => Ok(3), + _ => Err(format!("{:#?} is not an RGBA channel", &self)), + } + } +} + +#[cfg(test)] +mod tests { + use crate::sections::layer_and_mask_information_section::layer::{ + BlendMode, LayerChannels, LayerProperties, + }; + use crate::PsdLayer; + + use super::*; + + /// Verify that when inserting an RLE channel's bytes into an RGBA byte vec we do not attempt to + /// read beyond the channel's length. + #[test] + fn does_not_read_beyond_rle_channels_bytes() { + let layer_properties = LayerProperties { + name: "".into(), + layer_top: 0, + layer_left: 0, + layer_bottom: 0, + layer_right: 0, + visible: true, + opacity: 0, + clipping_mask: false, + psd_width: 1, + psd_height: 1, + blend_mode: BlendMode::Normal, + group_id: None, + }; + + let layer = PsdLayer { + channels: LayerChannels::from([( + PsdChannelKind::Red, + ChannelBytes::RleCompressed(vec![0, 0, 0]), + )]), + layer_properties, + }; + + let mut rgba = vec![0; (layer.width() * layer.height() * 4) as usize]; + + layer.insert_channel_bytes(&mut rgba, PsdChannelKind::Red, layer.red()); + + assert_eq!(rgba, vec![0; 4]); + } +} diff --git a/luda-editor/psd/src/render.rs b/luda-editor/psd/src/render.rs new file mode 100644 index 000000000..f84aee018 --- /dev/null +++ b/luda-editor/psd/src/render.rs @@ -0,0 +1,108 @@ +use crate::blend; +use crate::sections::layer_and_mask_information_section::layer::BlendMode; +use crate::PsdLayer; +use std::cell::RefCell; +use std::iter::repeat_with; + +pub(crate) struct Renderer<'a> { + layers_to_flatten_top_down: &'a [&'a PsdLayer], + cached_layer_rgba: Vec>>>, + width: usize, + pixel_cache: RefCell>, +} + +impl<'a> Renderer<'a> { + pub(crate) fn new( + layers_to_flatten_top_down: &'a [&'a PsdLayer], + width: usize, + ) -> Renderer<'a> { + Renderer { + layers_to_flatten_top_down: layers_to_flatten_top_down, + cached_layer_rgba: repeat_with(|| RefCell::new(None)) + .take(layers_to_flatten_top_down.len()) + .collect(), + width: width, + pixel_cache: RefCell::new(Vec::with_capacity(layers_to_flatten_top_down.len())), + } + } + + fn pixel_rgba_for_layer( + &'a self, + flattened_layer_top_down_idx: usize, + pixel_coord: (usize, usize), + ) -> blend::Pixel { + let layer = self.layers_to_flatten_top_down[flattened_layer_top_down_idx]; + + // If we haven't already calculated the RGBA for this layer, calculate and cache it + if self.cached_layer_rgba[flattened_layer_top_down_idx] + .borrow() + .is_none() + { + let pixels = layer.rgba(); + + self.cached_layer_rgba[flattened_layer_top_down_idx].replace(Some(pixels)); + } + + let cached_layer_rgba = self.cached_layer_rgba[flattened_layer_top_down_idx].borrow(); + let layer_rgba = cached_layer_rgba.as_deref().unwrap(); + + let (pixel_left, pixel_top) = pixel_coord; + let pixel_idx = ((self.width * pixel_top) + pixel_left) * 4; + + let (start, end) = (pixel_idx, pixel_idx + 4); + + let pixel = &layer_rgba[start..end]; + let mut copy = [0; 4]; + copy.copy_from_slice(pixel); + + blend::apply_opacity(&mut copy, layer.opacity); + copy + } + + /// Get the pixel at a coordinate within this image. + /// + /// If that pixel has transparency, recursively blending it with the pixel + /// below it until we reach a pixel with no transparency or the bottom of the stack. + pub(crate) fn flattened_pixel( + &'a self, + // (left, top) + pixel_coord: (usize, usize), + ) -> [u8; 4] { + let (pixel_left, pixel_top) = pixel_coord; + let mut pixels = self.pixel_cache.borrow_mut(); + pixels.clear(); + for (idx, layer) in self.layers_to_flatten_top_down.iter().enumerate() { + // If this pixel is out of bounds of this layer we return the pixel below it. + // If there is no pixel below it we return a transparent pixel + if (pixel_left as i32) < layer.layer_properties.layer_left + || (pixel_left as i32) > layer.layer_properties.layer_right + || (pixel_top as i32) < layer.layer_properties.layer_top + || (pixel_top as i32) > layer.layer_properties.layer_bottom + { + continue; + } + + let pixel = self.pixel_rgba_for_layer(idx, pixel_coord); + pixels.push((pixel, layer.blend_mode)); + + // This pixel is fully opaque, no point in going deeper + if pixel[3] == 255 && layer.opacity == 255 { + break; + } + } + + match pixels.pop() { + Some((bottom_pixel, _)) => { + pixels + .iter() + .rev() + .fold(bottom_pixel, |mut pixel_below, (pixel, blend_mode)| { + blend::blend_pixels(*pixel, pixel_below, *blend_mode, &mut pixel_below); + + pixel_below + }) + } + None => [0; 4], + } + } +} diff --git a/luda-editor/psd/src/sections/file_header_section.rs b/luda-editor/psd/src/sections/file_header_section.rs new file mode 100644 index 000000000..7195c0852 --- /dev/null +++ b/luda-editor/psd/src/sections/file_header_section.rs @@ -0,0 +1,364 @@ +use crate::sections::PsdCursor; +use thiserror::Error; + +/// Bytes representing the string "8BPS". +pub const EXPECTED_PSD_SIGNATURE: [u8; 4] = [56, 66, 80, 83]; +/// Bytes representing the number 1 +const EXPECTED_VERSION: [u8; 2] = [0, 1]; +/// Bytes representing the Reserved section of the header +const EXPECTED_RESERVED: [u8; 6] = [0; 6]; + +/// The FileHeaderSection comes from the first 26 bytes in the PSD file. +/// +/// We don't store information that isn't useful. +/// +/// For example, after validating the PSD signature we won't store it since it is always the +/// same value. +/// +/// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) +/// +/// The file header contains the basic properties of the image. +/// +/// +/// | Length | Description | +/// |--------|------------------------------------------------------------------------------------------------------------------------------------------------------| +/// | 4 | Signature: always equal to '8BPS' . Do not try to read the file if the signature does not match this value. | +/// | 2 | Version: always equal to 1. Do not try to read the file if the version does not match this value.
(**PSB** version is 2.) | +/// | 6 | Reserved: must be zero. | +/// | 2 | The number of channels in the image, including any alpha channels. Supported range is 1 to 56. | +/// | 4 | The height of the image in pixels. Supported range is 1 to 30,000.
(**PSB** max of 300,000.) | +/// | 4 | The width of the image in pixels. Supported range is 1 to 30,000.
(**PSB** max of 300,000) | +/// | 2 | Depth: the number of bits per channel. Supported values are 1, 8, 16 and 32. | +/// | 2 | The color mode of the file. Supported values are: Bitmap = 0; Grayscale = 1; Indexed = 2; RGB = 3; CMYK = 4; Multichannel = 7; Duotone = 8; Lab = 9. | +#[derive(Debug)] +pub struct FileHeaderSection { + pub(in crate) version: PsdVersion, + pub(in crate) channel_count: ChannelCount, + pub(in crate) width: PsdWidth, + pub(in crate) height: PsdHeight, + pub(in crate) depth: PsdDepth, + pub(in crate) color_mode: ColorMode, +} + +/// Represents an malformed file section header +#[derive(Debug, PartialEq, Error)] +pub enum FileHeaderSectionError { + #[error("A file section header is comprised of 26 bytes, you provided {length} bytes.")] + IncorrectLength { length: usize }, + #[error( + r#"The first four bytes (indices 0-3) of a PSD must always equal [56, 66, 80, 83], + which in string form is '8BPS'."# + )] + InvalidSignature {}, + #[error( + r#"Bytes 5 and 6 (indices 4-5) must always be [0, 1], Representing a PSD version of 1."# + )] + InvalidVersion {}, + #[error(r#"Bytes 7-12 (indices 6-11) must be zeroes"#)] + InvalidReserved {}, + #[error("Invalid channel count: {channel_count}. Must be 1 <= channel count <= 56")] + ChannelCountOutOfRange { channel_count: u8 }, + #[error("Invalid width: {width}. Must be 1 <= width <= 30,000")] + WidthOutOfRange { width: u32 }, + #[error("Invalid height: {height}. Must be 1 <= height <= 30,000")] + HeightOutOfRange { height: u32 }, + #[error("Depth {depth} is invalid. Must be 1, 8, 16 or 32")] + InvalidDepth { depth: u8 }, + #[error("Invalid color mode {color_mode}. Must be 0, 1, 2, 3, 4, 7, 8 or 9")] + InvalidColorMode { color_mode: u8 }, +} + +impl FileHeaderSection { + /// Create a FileSectionHeader from the first 26 bytes of a PSD + /// + /// TODO: Accept a ColorModeSection along with the bytes so that we can add + /// any ColorModeSection data to the ColorMode if necessary. Rename this method + /// to "new" in the process. + pub fn from_bytes(bytes: &[u8]) -> Result { + let mut cursor = PsdCursor::new(bytes); + + // File header section must be 26 bytes long + if bytes.len() != 26 { + return Err(FileHeaderSectionError::IncorrectLength { + length: bytes.len(), + } + ); + } + + // First four bytes must be '8BPS' + let signature = cursor.read_4(); + if signature != EXPECTED_PSD_SIGNATURE { + return Err(FileHeaderSectionError::InvalidSignature {}); + } + + // The next 2 bytes represent the version + let version = cursor.read_2(); + if version != EXPECTED_VERSION { + return Err(FileHeaderSectionError::InvalidVersion {}); + } + + // The next 6 bytes are reserved and should always be 0 + let reserved = cursor.read_6(); + if reserved != EXPECTED_RESERVED { + return Err(FileHeaderSectionError::InvalidReserved {}); + } + + // The next 2 bytes represent the channel count + let channel_count = cursor.read_u16() as u8; + let channel_count = ChannelCount::new(channel_count) + .ok_or(FileHeaderSectionError::ChannelCountOutOfRange { channel_count })?; + + // 4 bytes for the height + let height = cursor.read_u32(); + let height = + PsdHeight::new(height).ok_or(FileHeaderSectionError::HeightOutOfRange { height })?; + + // 4 bytes for the width + let width = cursor.read_u32(); + let width = + PsdWidth::new(width).ok_or(FileHeaderSectionError::WidthOutOfRange { width })?; + + // 2 bytes for depth + let depth = cursor.read_2()[1]; + let depth = PsdDepth::new(depth).ok_or(FileHeaderSectionError::InvalidDepth { depth })?; + + // 2 bytes for color mode + let color_mode = cursor.read_2()[1]; + let color_mode = ColorMode::new(color_mode) + .ok_or(FileHeaderSectionError::InvalidColorMode { color_mode })?; + + let file_header_section = FileHeaderSection { + version: PsdVersion::One, + channel_count, + width, + height, + depth, + color_mode, + }; + + Ok(file_header_section) + } +} + +/// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) +/// +/// Version: always equal to 1. Do not try to read the file if the version does not match this value. (**PSB** version is 2.) +/// +/// via: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/ +#[derive(Debug)] +pub enum PsdVersion { + /// Regular PSD (Not a PSB) + One, +} + +/// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) +/// +/// The number of channels in the image, including any alpha channels. Supported range is 1 to 56. +/// +/// via: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/ +#[derive(Debug)] +pub struct ChannelCount(u8); + +impl ChannelCount { + /// Create a new ChannelCount + pub fn new(channel_count: u8) -> Option { + if channel_count < 1 || channel_count > 56 { + return None; + } + + Some(ChannelCount(channel_count)) + } + + /// Return the channel count + pub fn count(&self) -> u8 { + self.0 + } +} + +/// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) +/// +/// The height of the image in pixels. Supported range is 1 to 30,000. +/// (**PSB** max of 300,000.) +/// +/// via: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/ +#[derive(Debug)] +pub struct PsdHeight(pub(in crate) u32); + +impl PsdHeight { + /// Create a new PsdHeight + pub fn new(height: u32) -> Option { + if height < 1 || height > 30000 { + return None; + } + + Some(PsdHeight(height)) + } +} + +/// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) +/// +/// The width of the image in pixels. Supported range is 1 to 30,000. +/// (*PSB** max of 300,000) +/// +/// via: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/ +#[derive(Debug, Clone, Copy)] +pub struct PsdWidth(pub(in crate) u32); + +impl PsdWidth { + /// Create a new PsdWidth + pub fn new(width: u32) -> Option { + if width < 1 || width > 30000 { + return None; + } + + Some(PsdWidth(width)) + } +} + +/// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) +/// +/// Depth: the number of bits per channel. Supported values are 1, 8, 16 and 32. +/// +/// via: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/ +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[allow(missing_docs)] +pub enum PsdDepth { + One = 1, + Eight = 8, + Sixteen = 16, + ThirtyTwo = 32, +} + +impl PsdDepth { + /// Create a new PsdDepth + pub fn new(depth: u8) -> Option { + match depth { + 1 => Some(PsdDepth::One), + 8 => Some(PsdDepth::Eight), + 16 => Some(PsdDepth::Sixteen), + 32 => Some(PsdDepth::ThirtyTwo), + _ => None, + } + } +} + +/// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) +/// +/// The color mode of the file. Supported values are: Bitmap = 0; Grayscale = 1; Indexed = 2; RGB = 3; CMYK = 4; Multichannel = 7; Duotone = 8; Lab = 9. +/// +/// via: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/ +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[allow(missing_docs)] +pub enum ColorMode { + Bitmap = 0, + Grayscale = 1, + // TODO: Indexed(Vec) + // Where the vector is the data from the color mode data section + Indexed = 2, + Rgb = 3, + Cmyk = 4, + Multichannel = 7, + // TODO: DuoTone(Vec) + // Where the vector is the data from the color mode data section. + Duotone = 8, + Lab = 9, +} + +impl ColorMode { + /// Create a new ColorMode + pub fn new(color_mode: u8) -> Option { + match color_mode { + 0 => Some(ColorMode::Bitmap), + 1 => Some(ColorMode::Grayscale), + 2 => Some(ColorMode::Indexed), + 3 => Some(ColorMode::Rgb), + 4 => Some(ColorMode::Cmyk), + 7 => Some(ColorMode::Multichannel), + 8 => Some(ColorMode::Duotone), + 9 => Some(ColorMode::Lab), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Valid: + // >= 1, <= 56 + #[test] + fn valid_channel_count() { + for channel_count in 1..=56 { + assert!(ChannelCount::new(channel_count).is_some()); + } + } + + // Invalid: + // < 1, > 56 + #[test] + fn invalid_channel_count() { + assert!(ChannelCount::new(0).is_none()); + assert!(ChannelCount::new(57).is_none()); + } + + // We're passing in 25 bytes even though we're supposed to pass in 26 bytes + #[test] + fn incorrect_file_header_section_length() { + let too_short = [0; 25]; + + match error_from_bytes(&too_short) { + FileHeaderSectionError::IncorrectLength { length } => assert_eq!(length, 25), + _ => panic!("Should have returned incorrect length error"), + }; + } + + #[test] + fn first_four_bytes_incorrect() { + let bytes = make_bytes(); + + let error = error_from_bytes(&bytes); + + match error { + FileHeaderSectionError::InvalidSignature {} => {} + _ => panic!("Should have returned invalid signature error"), + }; + } + + #[test] + fn version_incorrect() { + let mut bytes = make_bytes(); + bytes[0..4].copy_from_slice(&EXPECTED_PSD_SIGNATURE); + + match error_from_bytes(&bytes) { + FileHeaderSectionError::InvalidVersion {} => {} + _ => panic!("Should have returned invalid version error"), + }; + } + + #[test] + fn invalid_reserved_section() { + let mut bytes = make_bytes(); + bytes[0..4].copy_from_slice(&EXPECTED_PSD_SIGNATURE); + bytes[4..6].copy_from_slice(&EXPECTED_VERSION); + + match error_from_bytes(&bytes) { + FileHeaderSectionError::InvalidReserved {} => {} + _ => panic!("Should have returned reserved section error"), + }; + } + + fn error_from_bytes(bytes: &[u8]) -> FileHeaderSectionError { + FileHeaderSection::from_bytes(&bytes).expect_err("error") + } + + // [0, 1, 2, ..., 25] + fn make_bytes() -> [u8; 26] { + let mut bytes = [0; 26]; + for i in 0..26 { + bytes[i] = i as u8; + } + + bytes + } +} diff --git a/luda-editor/psd/src/sections/image_data_section.rs b/luda-editor/psd/src/sections/image_data_section.rs new file mode 100644 index 000000000..7411b065b --- /dev/null +++ b/luda-editor/psd/src/sections/image_data_section.rs @@ -0,0 +1,226 @@ +use crate::psd_channel::PsdChannelCompression; +use crate::sections::PsdCursor; +use crate::PsdDepth; +use thiserror::Error; + +/// Represents an malformed image data +#[derive(Debug, PartialEq, Error)] +pub enum ImageDataSectionError { + #[error( + r#"Only 8 and 16 bit depths are supported at the moment. + If you'd like to see 1 and 32 bit depths supported - please open an issue."# + )] + UnsupportedDepth, + + #[error("{compression} is an invalid layer channel compression. Must be 0, 1, 2 or 3")] + InvalidCompression { compression: u16 }, +} + +/// The ImageDataSection comes from the final section in the PSD that contains the pixel data +/// of the final PSD image (the one that comes from combining all of the layers). +/// +/// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) +/// +/// The last section of a Photoshop file contains the image pixel data. +/// Image data is stored in planar order: first all the red data, then all the green data, etc. +/// Each plane is stored in scan-line order, with no pad bytes, +/// +/// | Length | Description | +/// |----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +/// | 2 | Compression method:
0 = Raw image data
1 = RLE compressed the image data starts with the byte counts for all the scan lines (rows * channels), with each count stored as a two-byte value. The RLE compressed data follows, with each scan line compressed separately. The RLE compression is the same compression algorithm used by the Macintosh ROM routine PackBits , and the TIFF standard.
2 = ZIP without prediction
3 = ZIP with prediction. | +/// | Variable | The image data. Planar order = RRR GGG BBB, etc. | +#[derive(Debug)] +pub struct ImageDataSection { + /// The compression method for the image. + pub(crate) compression: PsdChannelCompression, + /// The red channel of the final image + pub(crate) red: ChannelBytes, + /// The green channel of the final image + pub(crate) green: Option, + /// the blue channel of the final image + pub(crate) blue: Option, + /// the alpha channel of the final image. + /// If there is no alpha channel then it is a fully opaque image. + pub(crate) alpha: Option, +} + +impl ImageDataSection { + /// Create an ImageDataSection from the bytes in the corresponding section in a PSD file + /// (including the length market) + pub fn from_bytes( + bytes: &[u8], + depth: PsdDepth, + psd_height: u32, + channel_count: u8, + ) -> Result { + let mut cursor = PsdCursor::new(bytes); + let channel_count = channel_count as usize; + + let compression = cursor.read_u16(); + let compression = PsdChannelCompression::new(compression) + .ok_or(ImageDataSectionError::InvalidCompression { compression })?; + + let (red, green, blue, alpha) = match compression { + PsdChannelCompression::RawData => { + // First 2 bytes were compression bytes + let channel_bytes = &bytes[2..]; + let channel_byte_count = channel_bytes.len(); + + let bytes_per_channel = channel_byte_count / channel_count; + + // First bytes are red + let mut red = channel_bytes[..bytes_per_channel].into(); + + // Next bytes are green + let green = if channel_count >= 2 { + Some(ChannelBytes::RawData( + channel_bytes[bytes_per_channel..2 * bytes_per_channel].into(), + )) + } else { + None + }; + + // Then comes blue + let blue = if channel_count >= 3 { + Some(ChannelBytes::RawData( + channel_bytes[2 * bytes_per_channel..3 * bytes_per_channel].into(), + )) + } else { + None + }; + + // And optionally alpha bytes + let alpha = if channel_count == 4 { + Some(ChannelBytes::RawData( + channel_bytes[3 * bytes_per_channel..4 * bytes_per_channel].to_vec(), + )) + } else { + None + }; + + match depth { + PsdDepth::Eight => (ChannelBytes::RawData(red), green, blue, alpha), + // If this is a 16bit image there will be two bytes per pixel. We + // currently only support one byte per pixel so we convert the 2 bytes + // back down into 1 byte by mapping 0-65535 down to 0-255 + PsdDepth::Sixteen => { + for idx in 0..red.len() / 2 { + let bytes = [red[2 * idx], red[2 * idx + 1]]; + let bits16 = u16::from_be_bytes(bytes); + red[idx] = (bits16 / 256) as u8; + } + red.truncate(red.len() / 2); + + (ChannelBytes::RawData(red), green, blue, alpha) + } + _ => return Err(ImageDataSectionError::UnsupportedDepth), + } + } + // # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) + // + // RLE compressed the image data starts with the byte counts for all the scan lines + // (rows * channels), with each count stored as a two-byte value. The RLE compressed + // data follows, with each scan line compressed separately. The RLE compression is + // the same compression algorithm used by the Macintosh ROM routine PackBits, + // and the TIFF standard. + PsdChannelCompression::RleCompressed => { + let mut red_byte_count = 0; + let mut green_byte_count = if channel_count >= 2 { Some(0) } else { None }; + let mut blue_byte_count = if channel_count >= 3 { Some(0) } else { None }; + let mut alpha_byte_count = if channel_count == 4 { Some(0) } else { None }; + + for _ in 0..psd_height { + red_byte_count += cursor.read_u16() as usize; + } + + if let Some(ref mut green_byte_count) = green_byte_count { + for _ in 0..psd_height { + *green_byte_count += cursor.read_u16() as usize; + } + } + + if let Some(ref mut blue_byte_count) = blue_byte_count { + for _ in 0..psd_height { + *blue_byte_count += cursor.read_u16() as usize; + } + } + + if let Some(ref mut alpha_byte_count) = alpha_byte_count { + for _ in 0..psd_height { + *alpha_byte_count += cursor.read_u16() as usize; + } + } + + // 2 bytes for compression level, then 2 bytes for each scanline of each channel + // We're skipping over the bytes that describe the length of each scanling since + // we don't currently use them. We might re-think this in the future when we + // implement serialization of a Psd back into bytes.. But not a concern at the + // moment. + let channel_data_start = 2 + (channel_count * psd_height as usize * 2); + + let (red_start, red_end) = + (channel_data_start, channel_data_start + red_byte_count); + + let red = bytes[red_start..red_end].into(); + + let green = match green_byte_count { + Some(green_byte_count) => { + let green_start = red_end; + let green_end = green_start + green_byte_count; + Some(ChannelBytes::RleCompressed( + bytes[green_start..green_end].into(), + )) + } + None => None, + }; + + let blue = match blue_byte_count { + Some(blue_byte_count) => { + let blue_start = red_end + green_byte_count.unwrap(); + let blue_end = blue_start + blue_byte_count; + Some(ChannelBytes::RleCompressed( + bytes[blue_start..blue_end].into(), + )) + } + None => None, + }; + + let alpha = match alpha_byte_count { + Some(alpha_byte_count) => { + let alpha_start = + red_end + green_byte_count.unwrap() + blue_byte_count.unwrap(); + let alpha_end = alpha_start + alpha_byte_count; + Some(ChannelBytes::RleCompressed( + bytes[alpha_start..alpha_end].into(), + )) + } + None => None, + }; + + (ChannelBytes::RleCompressed(red), green, blue, alpha) + } + PsdChannelCompression::ZipWithoutPrediction => unimplemented!( + r#"Zip without prediction compression is currently unsupported. + Please open an issue"# + ), + PsdChannelCompression::ZipWithPrediction => unimplemented!( + r#"Zip with prediction compression is currently unsupported. + Please open an issue"# + ), + }; + + Ok(ImageDataSection { + compression, + red, + green, + blue, + alpha, + }) + } +} + +#[derive(Debug, Clone)] +pub enum ChannelBytes { + RawData(Vec), + RleCompressed(Vec), +} diff --git a/luda-editor/psd/src/sections/image_resources_section.rs b/luda-editor/psd/src/sections/image_resources_section.rs new file mode 100644 index 000000000..f7400085c --- /dev/null +++ b/luda-editor/psd/src/sections/image_resources_section.rs @@ -0,0 +1,773 @@ +use std::collections::HashMap; +use std::ops::Range; + +use thiserror::Error; + +pub use crate::sections::image_resources_section::image_resource::ImageResource; +use crate::sections::image_resources_section::image_resource::SlicesImageResource; +use crate::sections::PsdCursor; + +const EXPECTED_RESOURCE_BLOCK_SIGNATURE: [u8; 4] = [56, 66, 73, 77]; +const EXPECTED_DESCRIPTOR_VERSION: u32 = 16; +const RESOURCE_SLICES_INFO: i16 = 1050; + +mod image_resource; + +struct ImageResourcesBlock { + resource_id: i16, + name: String, + data_range: Range, +} + +#[derive(Debug)] +pub struct ImageResourcesSection { + pub(crate) resources: Vec, +} + +/// Represents an malformed resource block +#[derive(Debug, PartialEq, Error)] +pub enum ImageResourcesSectionError { + #[error( + r#"The first four bytes (indices 0-3) must always equal [56, 66, 73, 77], + which in string form is '8BIM'."# + )] + InvalidSignature {}, + + #[error("Invalid resource descriptor: {0}")] + InvalidResource(ImageResourcesDescriptorError), +} + +impl ImageResourcesSection { + pub fn from_bytes(bytes: &[u8]) -> Result { + let mut cursor = PsdCursor::new(bytes); + + let mut resources = vec![]; + + let length = cursor.read_u32() as u64; + + while cursor.position() < length { + let block = ImageResourcesSection::read_resource_block(&mut cursor)?; + + let rid = block.resource_id; + match rid { + _ if rid == RESOURCE_SLICES_INFO => { + let slices_image_resource = ImageResourcesSection::read_slice_block( + &cursor.get_ref()[block.data_range], + ) + .map_err(ImageResourcesSectionError::InvalidResource)?; + resources.push(ImageResource::Slices(slices_image_resource)); + } + _ => {} + } + } + + assert_eq!(cursor.position(), length + 4); + + Ok(ImageResourcesSection { resources }) + } + + /// +----------+--------------------------------------------------------------------------------------------------------------------+ + /// | Length | Description | + /// +----------+--------------------------------------------------------------------------------------------------------------------+ + /// | 4 | Signature: '8BIM' | + /// | 2 | Unique identifier for the resource. Image resource IDs contains a list of resource IDs used by Photoshop. | + /// | Variable | Name: Pascal string, padded to make the size even (a null name consists of two bytes of 0) | + /// | 4 | Actual size of resource data that follows | + /// | Variable | The resource data, described in the sections on the individual resource types. It is padded to make the size even. | + /// +----------+--------------------------------------------------------------------------------------------------------------------+ + fn read_resource_block( + cursor: &mut PsdCursor, + ) -> Result { + // First four bytes must be '8BIM' + let signature = cursor.read_4(); + if signature != EXPECTED_RESOURCE_BLOCK_SIGNATURE { + return Err(ImageResourcesSectionError::InvalidSignature {}); + } + + let resource_id = cursor.read_i16(); + let name = cursor.read_pascal_string(); + + let data_len = cursor.read_u32(); + let pos = cursor.position() as usize; + // Note: data length is padded to even. + let data_len = data_len + data_len % 2; + let data_range = Range { + start: pos, + end: pos + data_len as usize, + }; + cursor.read(data_len); + + Ok(ImageResourcesBlock { + resource_id, + name, + data_range, + }) + } + + /// Slice header for version 6 + /// + /// +----------+--------------------------------------------------------------------------------------+ + /// | Length | Description | + /// +----------+--------------------------------------------------------------------------------------+ + /// | 4 | Version ( = 6) | + /// | 4 * 4 | Bounding rectangle for all of the slices: top, left, bottom, right of all the slices | + /// | Variable | Name of group of slices: Unicode string | + /// | 4 | Number of slices to follow. See Slices resource block in the next table | + /// +----------+--------------------------------------------------------------------------------------+ + fn read_slice_block( + bytes: &[u8], + ) -> Result { + let mut cursor = PsdCursor::new(bytes); + + let version = cursor.read_i32(); + if version == 6 { + let _top = cursor.read_i32(); + let _left = cursor.read_i32(); + let _bottom = cursor.read_i32(); + let _right = cursor.read_i32(); + + let group_of_slices_name = cursor.read_unicode_string_padding(1); + + let number_of_slices = cursor.read_u32(); + + let mut descriptors = Vec::new(); + + for _ in 0..number_of_slices { + match ImageResourcesSection::read_slice_body(&mut cursor)? { + Some(v) => descriptors.push(v), + None => {} + } + } + + return Ok(SlicesImageResource { + name: group_of_slices_name, + descriptors, + }); + } + if version == 7 || version == 8 { + let descriptor_version = cursor.read_i32(); + if descriptor_version != 16 { + unimplemented!( + "Only the version 16 (descriptors) resource format for slices is currently supported" + ); + } + let descriptor = DescriptorStructure::read_descriptor_structure(&mut cursor)?; + return Ok(SlicesImageResource { + name: descriptor.name.clone(), + descriptors: vec![descriptor], + }); + } + unimplemented!("Slices resource format {version} is currently not supported"); + } + + /// Slices resource block + /// + /// +------------------------------------------------------+-----------------------------------------------+ + /// | Length | Description | + /// +------------------------------------------------------+-----------------------------------------------+ + /// | 4 | ID | + /// | 4 | Group ID | + /// | 4 | Origin | + /// | 4 | Associated Layer ID | + /// | Only present if Origin = 1 | | + /// | Variable | Name: Unicode string | + /// | 4 | Type | + /// | 4 * 4 | Left, top, right, bottom positions | + /// | Variable | URL: Unicode string | + /// | Variable | Target: Unicode string | + /// | Variable | Message: Unicode string | + /// | Variable | Alt Tag: Unicode string | + /// | 1 | Cell text is HTML: Boolean | + /// | Variable | Cell text: Unicode string | + /// | 4 | Horizontal alignment | + /// | 4 | Vertical alignment | + /// | 1 | Alpha color | + /// | 1 | Red | + /// | 1 | Green | + /// | 1 | Blue | + /// | Additional data as length allows. See comment above. | | + /// | 4 | Descriptor version ( = 16 for Photoshop 6.0). | + /// | Variable | Descriptor (see See Descriptor structure) | + /// +------------------------------------------------------+-----------------------------------------------+ + fn read_slice_body( + cursor: &mut PsdCursor, + ) -> Result, ImageResourcesDescriptorError> { + let _slice_id = cursor.read_i32(); + let _group_id = cursor.read_i32(); + let origin = cursor.read_i32(); + + // if origin = 1, Associated Layer ID is present + if origin == 1 { + cursor.read_i32(); + } + + let _name = cursor.read_unicode_string_padding(1); + + let _type = cursor.read_i32(); + + let _top = cursor.read_i32(); + let _left = cursor.read_i32(); + let _bottom = cursor.read_i32(); + let _right = cursor.read_i32(); + + let _url = cursor.read_unicode_string_padding(1); + + let _target = cursor.read_unicode_string_padding(1); + + let _message = cursor.read_unicode_string_padding(1); + + let _alt_tag = cursor.read_unicode_string_padding(1); + + let _cell_text_html = cursor.read_1(); + let _cell_text = cursor.read_unicode_string_padding(1); + + let _horizontal_alignment = cursor.read_i32(); + let _vertical_alignment = cursor.read_i32(); + let _argb_color = cursor.read_i32(); + + let pos = cursor.position(); + let descriptor_version = cursor.peek_u32(); + + Ok(if descriptor_version == EXPECTED_DESCRIPTOR_VERSION { + cursor.read_4(); + + let descriptor = DescriptorStructure::read_descriptor_structure(cursor)?; + if descriptor.class_id.as_slice() == [0, 0, 0, 0] { + cursor.seek(pos); + } + + Some(descriptor) + } else { + None + }) + } +} + +/// +-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +/// | Variable | Unicode string: name from classID | +/// | Variable | classID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | +/// | 4 | Number of items in descriptor | +/// | The following is repeated for each item in descriptor | | +/// | Variable | Key: 4 bytes ( length) followed either by string or (if length is zero) 4-byte key | +/// | 4 | Type: OSType key | +/// | | 'obj ' = Reference | +/// | | 'Objc' = Descriptor | +/// | | 'VlLs' = List | +/// | | 'doub' = Double | +/// | | 'UntF' = Unit float | +/// | | 'TEXT' = String | +/// | | 'enum' = Enumerated | +/// | | 'long' = Integer | +/// | | 'comp' = Large Integer | +/// | | 'bool' = Boolean | +/// | | 'GlbO' = GlobalObject same as Descriptor | +/// | | 'type' = Class | +/// | | 'GlbC' = Class | +/// | | 'alis' = Alias | +/// | | 'tdta' = Raw Data | +/// | Variable | Item type: see the tables below for each possible type | +/// +-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +#[derive(Debug)] +pub struct DescriptorStructure { + pub name: String, + pub fields: HashMap, + pub class_id: Vec, +} + +/// One of +#[derive(Debug)] +pub enum DescriptorField { + /// Descriptor as field + Descriptor(DescriptorStructure), + /// A list of special fields + /// There are can be Property, Identifier, Index, Name fields + Reference(Vec), + /// Float field with unit + UnitFloat(UnitFloatStructure), + /// Double-precision floating-point number + Double(f64), + /// + Class(ClassStructure), + /// Text + String(String), + /// + EnumeratedReference(EnumeratedReference), + /// + Offset(OffsetStructure), + /// Boolean value + Boolean(bool), + /// + Alias(AliasStructure), + /// A list of fields + List(Vec), + /// 64bit integer number + LargeInteger(i64), + /// 32bit integer number + Integer(i32), + /// + EnumeratedDescriptor(EnumeratedDescriptor), + /// Raw bytes data + RawData(Vec), + + /// Only Reference fields + /// + /// + Property(PropertyStructure), + /// + Identifier(i32), + /// + Index(i32), + /// + Name(NameStructure), +} + +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Variable | Unicode string: name from classID | +/// | Variable | classID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | +/// | Variable | KeyID: 4 bytes (length), followed either by string or (if length is zero) 4-byte keyID | +/// +----------+--------------------------------------------------------------------------------------------+ +#[derive(Debug)] +pub struct PropertyStructure { + pub name: String, + pub class_id: Vec, + pub key_id: Vec, +} + +/// +------------------------------------+--------------------------------------------------------+ +/// | Length | Description | +/// +------------------------------------+--------------------------------------------------------+ +/// | 4 | Units the following value is in. One of the following: | +/// | | '#Ang' = angle: base degrees | +/// | | '#Rsl' = density: base per inch | +/// | | '#Rlt' = distance: base 72ppi | +/// | | '#Nne' = none: coerced. | +/// | | '#Prc'= percent: unit value | +/// | | '#Pxl' = pixels: tagged unit value | +/// | 8 | Actual value (double) | +/// +------------------------------------+--------------------------------------------------------+ +#[derive(Debug)] +pub enum UnitFloatStructure { + /// Base degrees + Angle(f64), + /// Base per inch + Density(f64), + /// Base 72ppi + Distance(f64), + /// Base coerced + None, + /// Unit value + Percent(f64), + /// Tagged unit value + Pixels(f64), +} + +/// Unit float structure units keys +/// '#Ang' = angle: base degrees +const UNIT_FLOAT_ANGLE: &[u8; 4] = b"#Ang"; +/// '#Rsl' = density: base per inch +const UNIT_FLOAT_DENSITY: &[u8; 4] = b"#Rsl"; +/// '#Rlt' = distance: base 72ppi +const UNIT_FLOAT_DISTANCE: &[u8; 4] = b"#Rlt"; +/// '#Nne' = none: coerced. +const UNIT_FLOAT_NONE: &[u8; 4] = b"#Nne"; +/// '#Prc'= percent: unit value +const UNIT_FLOAT_PERCENT: &[u8; 4] = b"#Prc"; +/// '#Pxl' = pixels: tagged unit value +const UNIT_FLOAT_PIXELS: &[u8; 4] = b"#Pxl"; + +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Variable | Unicode string: name from classID | +/// | Variable | ClassID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | +/// +----------+--------------------------------------------------------------------------------------------+ +#[derive(Debug)] +pub struct ClassStructure { + pub name: String, + pub class_id: Vec, +} + +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Variable | Unicode string: name from ClassID. | +/// | Variable | ClassID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | +/// | Variable | TypeID: 4 bytes (length), followed either by string or (if length is zero) 4-byte typeID | +/// | Variable | enum: 4 bytes (length), followed either by string or (if length is zero) 4-byte enum | +/// +----------+--------------------------------------------------------------------------------------------+ +#[derive(Debug)] +pub struct EnumeratedReference { + pub name: String, + pub class_id: Vec, + pub key_id: Vec, + pub enum_field: Vec, +} + +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Variable | Unicode string: name from ClassID | +/// | Variable | ClassID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | +/// | 4 | Value of the offset | +/// +----------+--------------------------------------------------------------------------------------------+ +#[derive(Debug)] +pub struct OffsetStructure { + pub name: String, + pub class_id: Vec, + pub offset: u32, +} + +/// +----------+--------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+--------------------------------------------------------------------------+ +/// | 4 | Length of data to follow | +/// | Variable | FSSpec for Macintosh or a handle to a string to the full path on Windows | +/// +----------+--------------------------------------------------------------------------+ +#[derive(Debug)] +pub struct AliasStructure { + pub data: Vec, +} + +/// +----------+----------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+----------------------------------------------------------------------------------------+ +/// | Variable | Type: 4 bytes (length), followed either by string or (if length is zero) 4-byte typeID | +/// | Variable | Enum: 4 bytes (length), followed either by string or (if length is zero) 4-byte enum | +/// +----------+----------------------------------------------------------------------------------------+ +#[derive(Debug)] +pub struct EnumeratedDescriptor { + pub type_field: Vec, + pub enum_field: Vec, +} + +/// NOTE: This struct is not documented in the specification +/// So it's based on https://github.com/psd-tools/psd-tools/blob/master/src/psd_tools/psd/descriptor.py#L691 +/// +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Variable | Unicode string: name from ClassID | +/// | Variable | ClassID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | +/// | Variable | Unicode string: value | +/// +----------+--------------------------------------------------------------------------------------------+ +#[derive(Debug)] +pub struct NameStructure { + pub name: String, + pub class_id: Vec, + pub value: String, +} + +/// Descriptor structure OSType keys +/// 'obj ' = Reference +const OS_TYPE_REFERENCE: &[u8; 4] = b"obj "; +/// 'Objc' = Descriptor +const OS_TYPE_DESCRIPTOR: &[u8; 4] = b"Objc"; +/// 'VlLs' = List +const OS_TYPE_LIST: &[u8; 4] = b"VlLs"; +/// 'doub' = Double +const OS_TYPE_DOUBLE: &[u8; 4] = b"doub"; +/// 'UntF' = Unit float +const OS_TYPE_UNIT_FLOAT: &[u8; 4] = b"UntF"; +/// 'TEXT' = String +const OS_TYPE_TEXT: &[u8; 4] = b"TEXT"; +/// 'enum' = Enumerated +const OS_TYPE_ENUMERATED: &[u8; 4] = b"enum"; +/// 'long' = Integer +const OS_TYPE_INTEGER: &[u8; 4] = b"long"; +/// 'comp' = Large Integer +const OS_TYPE_LARGE_INTEGER: &[u8; 4] = b"comp"; +/// 'bool' = Boolean +const OS_TYPE_BOOL: &[u8; 4] = b"bool"; +/// 'GlbO' = GlobalObject same as Descriptor +const OS_TYPE_GLOBAL_OBJECT: &[u8; 4] = b"GlbO"; +/// 'type' = Class +const OS_TYPE_CLASS: &[u8; 4] = b"type"; +/// 'GlbC' = Class +const OS_TYPE_CLASS2: &[u8; 4] = b"GlbC"; +/// 'alis' = Alias +const OS_TYPE_ALIAS: &[u8; 4] = b"alis"; +/// 'tdta' = Raw Data +const OS_TYPE_RAW_DATA: &[u8; 4] = b"tdta"; + +/// Reference structure OSType keys +/// 'prop' = Property +const OS_TYPE_PROPERTY: &[u8; 4] = b"prop"; +/// 'Clss' = Class +const OS_TYPE_CLASS3: &[u8; 4] = b"Clss"; +/// 'Clss' = Class +const OS_TYPE_ENUMERATED_REFERENCE: &[u8; 4] = b"Enmr"; +/// 'rele' = Offset +const OS_TYPE_OFFSET: &[u8; 4] = b"rele"; +/// 'Idnt' = Identifier +const OS_TYPE_IDENTIFIER: &[u8; 4] = b"Idnt"; +/// 'indx' = Index +const OS_TYPE_INDEX: &[u8; 4] = b"indx"; +/// 'name' = Name +const OS_TYPE_NAME: &[u8; 4] = b"name"; + +#[derive(Debug, PartialEq, Error)] +pub enum ImageResourcesDescriptorError { + #[error(r#"Invalid TypeOS field."#)] + InvalidTypeOS {}, + #[error(r#"Invalid unit name."#)] + InvalidUnitName {}, +} + +impl DescriptorStructure { + fn read_descriptor_structure( + cursor: &mut PsdCursor, + ) -> Result { + let name = cursor.read_unicode_string_padding(1); + let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); + let fields = DescriptorStructure::read_fields(cursor, false)?; + + Ok(DescriptorStructure { + name, + fields, + class_id, + }) + } + + fn read_fields( + cursor: &mut PsdCursor, + sub_list: bool, + ) -> Result, ImageResourcesDescriptorError> { + let count = cursor.read_u32(); + let mut m = HashMap::with_capacity(count as usize); + + for n in 0..count { + let key = DescriptorStructure::read_key_length(cursor); + let key = String::from_utf8_lossy(key).into_owned(); + + m.insert(key, DescriptorStructure::read_descriptor_field(cursor)?); + } + + Ok(m) + } + + fn read_list( + cursor: &mut PsdCursor, + sub_list: bool, + ) -> Result, ImageResourcesDescriptorError> { + let count = cursor.read_u32(); + let mut vec = Vec::with_capacity(count as usize); + + for n in 0..count { + let field = DescriptorStructure::read_descriptor_field(cursor)?; + vec.push(field); + } + + Ok(vec) + } + + fn read_descriptor_field( + cursor: &mut PsdCursor, + ) -> Result { + let mut os_type = [0; 4]; + os_type.copy_from_slice(cursor.read_4()); + + let r: DescriptorField = match &os_type { + OS_TYPE_REFERENCE => { + DescriptorField::Reference(DescriptorStructure::read_reference_structure(cursor)?) + } + OS_TYPE_DESCRIPTOR => { + DescriptorField::Descriptor(DescriptorStructure::read_descriptor_structure(cursor)?) + } + OS_TYPE_LIST => { + DescriptorField::List(DescriptorStructure::read_list_structure(cursor)?) + } + OS_TYPE_DOUBLE => DescriptorField::Double(cursor.read_f64()), + OS_TYPE_UNIT_FLOAT => { + DescriptorField::UnitFloat(DescriptorStructure::read_unit_float(cursor)?) + } + OS_TYPE_TEXT => DescriptorField::String(cursor.read_unicode_string_padding(1)), + OS_TYPE_ENUMERATED => DescriptorField::EnumeratedDescriptor( + DescriptorStructure::read_enumerated_descriptor(cursor), + ), + OS_TYPE_LARGE_INTEGER => DescriptorField::LargeInteger(cursor.read_i64()), + OS_TYPE_INTEGER => DescriptorField::Integer(cursor.read_i32()), + OS_TYPE_BOOL => DescriptorField::Boolean(cursor.read_u8() > 0), + OS_TYPE_GLOBAL_OBJECT => { + DescriptorField::Descriptor(DescriptorStructure::read_descriptor_structure(cursor)?) + } + OS_TYPE_CLASS => { + DescriptorField::Class(DescriptorStructure::read_class_structure(cursor)) + } + OS_TYPE_CLASS2 => { + DescriptorField::Class(DescriptorStructure::read_class_structure(cursor)) + } + OS_TYPE_ALIAS => { + DescriptorField::Alias(DescriptorStructure::read_alias_structure(cursor)) + } + OS_TYPE_RAW_DATA => { + DescriptorField::RawData(DescriptorStructure::read_raw_data(cursor)) + } + _ => return Err(ImageResourcesDescriptorError::InvalidTypeOS {}), + }; + + Ok(r) + } + + /// +------------------------------------------------------+------------------------------------------------------------------+ + /// | Length | Description | + /// +------------------------------------------------------+------------------------------------------------------------------+ + /// | 4 | Number of items | + /// | The following is repeated for each item in reference | | + /// | 4 | OSType key for type to use: | + /// | 'prop' = Property | | + /// | 'Clss' = Class | | + /// | 'Enmr' = Enumerated Reference | | + /// | 'rele' = Offset | | + /// | 'Idnt' = Identifier | | + /// | 'indx' = Index | | + /// | 'name' =Name | | + /// | Variable | Item type: see the tables below for each possible Reference type | + /// +------------------------------------------------------+------------------------------------------------------------------+ + fn read_reference_structure( + cursor: &mut PsdCursor, + ) -> Result, ImageResourcesDescriptorError> { + let count = cursor.read_u32(); + let mut vec = Vec::with_capacity(count as usize); + + for n in 0..count { + DescriptorStructure::read_key_length(cursor); + + let mut os_type = [0; 4]; + os_type.copy_from_slice(cursor.read_4()); + vec.push(match &os_type { + OS_TYPE_PROPERTY => { + DescriptorField::Property(DescriptorStructure::read_property_structure(cursor)) + } + OS_TYPE_CLASS3 => { + DescriptorField::Class(DescriptorStructure::read_class_structure(cursor)) + } + OS_TYPE_ENUMERATED_REFERENCE => DescriptorField::EnumeratedReference( + DescriptorStructure::read_enumerated_reference(cursor), + ), + OS_TYPE_OFFSET => { + DescriptorField::Offset(DescriptorStructure::read_offset_structure(cursor)) + } + OS_TYPE_IDENTIFIER => DescriptorField::Identifier(cursor.read_i32()), + OS_TYPE_INDEX => DescriptorField::Index(cursor.read_i32()), + OS_TYPE_NAME => DescriptorField::Name(DescriptorStructure::read_name(cursor)), + _ => return Err(ImageResourcesDescriptorError::InvalidTypeOS {}), + }); + } + + Ok(vec) + } + + fn read_property_structure(cursor: &mut PsdCursor) -> PropertyStructure { + let name = cursor.read_unicode_string(); + let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); + let key_id = DescriptorStructure::read_key_length(cursor).to_vec(); + + PropertyStructure { + name, + class_id, + key_id, + } + } + + fn read_unit_float( + cursor: &mut PsdCursor, + ) -> Result { + let mut unit_float = [0; 4]; + unit_float.copy_from_slice(cursor.read_4()); + + Ok(match &unit_float { + UNIT_FLOAT_ANGLE => UnitFloatStructure::Angle(cursor.read_f64()), + UNIT_FLOAT_DENSITY => UnitFloatStructure::Density(cursor.read_f64()), + UNIT_FLOAT_DISTANCE => UnitFloatStructure::Distance(cursor.read_f64()), + UNIT_FLOAT_NONE => UnitFloatStructure::None, + UNIT_FLOAT_PERCENT => UnitFloatStructure::Percent(cursor.read_f64()), + UNIT_FLOAT_PIXELS => UnitFloatStructure::Pixels(cursor.read_f64()), + _ => return Err(ImageResourcesDescriptorError::InvalidUnitName {}), + }) + } + + fn read_class_structure(cursor: &mut PsdCursor) -> ClassStructure { + let name = cursor.read_unicode_string(); + let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); + + ClassStructure { name, class_id } + } + + fn read_enumerated_reference(cursor: &mut PsdCursor) -> EnumeratedReference { + let name = cursor.read_unicode_string(); + let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); + let key_id = DescriptorStructure::read_key_length(cursor).to_vec(); + let enum_field = DescriptorStructure::read_key_length(cursor).to_vec(); + + EnumeratedReference { + name, + class_id, + key_id, + enum_field, + } + } + + fn read_offset_structure(cursor: &mut PsdCursor) -> OffsetStructure { + let name = cursor.read_unicode_string(); + let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); + let offset = cursor.read_u32(); + + OffsetStructure { + name, + class_id, + offset, + } + } + + fn read_alias_structure(cursor: &mut PsdCursor) -> AliasStructure { + let length = cursor.read_u32(); + let data = cursor.read(length).to_vec(); + + AliasStructure { data } + } + + fn read_list_structure( + cursor: &mut PsdCursor, + ) -> Result, ImageResourcesDescriptorError> { + DescriptorStructure::read_list(cursor, true) + } + + fn read_enumerated_descriptor(cursor: &mut PsdCursor) -> EnumeratedDescriptor { + let type_field = DescriptorStructure::read_key_length(cursor).to_vec(); + let enum_field = DescriptorStructure::read_key_length(cursor).to_vec(); + + EnumeratedDescriptor { + type_field, + enum_field, + } + } + + fn read_raw_data(cursor: &mut PsdCursor) -> Vec { + let length = cursor.read_u32(); + cursor.read(length).to_vec() + } + + // Note: this structure is not documented + fn read_name(cursor: &mut PsdCursor) -> NameStructure { + let name = cursor.read_unicode_string(); + let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); + let value = cursor.read_unicode_string(); + + NameStructure { + name, + class_id, + value, + } + } + + fn read_key_length<'a>(cursor: &'a mut PsdCursor) -> &'a [u8] { + let length = cursor.read_u32(); + let length = if length > 0 { length } else { 4 }; + + cursor.read(length) + } +} diff --git a/luda-editor/psd/src/sections/image_resources_section/image_resource.rs b/luda-editor/psd/src/sections/image_resources_section/image_resource.rs new file mode 100644 index 000000000..6611d3d95 --- /dev/null +++ b/luda-editor/psd/src/sections/image_resources_section/image_resource.rs @@ -0,0 +1,26 @@ +use crate::sections::image_resources_section::DescriptorStructure; + +/// An image resource from the image resources section +#[derive(Debug)] +#[allow(missing_docs)] +pub enum ImageResource { + Slices(SlicesImageResource), +} + +/// Comes from a slices resource block +#[derive(Debug)] +pub struct SlicesImageResource { + pub(crate) name: String, + pub(crate) descriptors: Vec, +} + +#[allow(missing_docs)] +impl SlicesImageResource { + pub fn name(&self) -> &String { + &self.name + } + + pub fn descriptors(&self) -> &Vec { + &self.descriptors + } +} diff --git a/luda-editor/psd/src/sections/layer_and_mask_information_section/groups.rs b/luda-editor/psd/src/sections/layer_and_mask_information_section/groups.rs new file mode 100644 index 000000000..d9fc43af4 --- /dev/null +++ b/luda-editor/psd/src/sections/layer_and_mask_information_section/groups.rs @@ -0,0 +1,38 @@ +use crate::PsdGroup; +use std::collections::HashMap; +use std::ops::Deref; + +#[derive(Debug)] +pub(crate) struct Groups { + groups: HashMap, + group_ids_in_order: Vec, +} + +impl Groups { + pub fn with_capacity(capacity: usize) -> Self { + Groups { + groups: HashMap::with_capacity(capacity), + group_ids_in_order: Vec::with_capacity(capacity), + } + } + + /// Add a group to the list of groups, making it last in the order. + pub fn push(&mut self, group: PsdGroup) { + self.group_ids_in_order.push(group.id); + + self.groups.insert(group.id, group); + } + + /// Get the group ID's in order (from bottom to top in a PSD file). + pub fn group_ids_in_order(&self) -> &Vec { + &self.group_ids_in_order + } +} + +impl Deref for Groups { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.groups + } +} diff --git a/luda-editor/psd/src/sections/layer_and_mask_information_section/layer.rs b/luda-editor/psd/src/sections/layer_and_mask_information_section/layer.rs new file mode 100644 index 000000000..5461f66f4 --- /dev/null +++ b/luda-editor/psd/src/sections/layer_and_mask_information_section/layer.rs @@ -0,0 +1,474 @@ +use std::collections::HashMap; +use std::ops::{Deref, Range}; + +use thiserror::Error; + +use crate::psd_channel::IntoRgba; +use crate::psd_channel::PsdChannelCompression; +use crate::psd_channel::PsdChannelError; +use crate::psd_channel::PsdChannelKind; +use crate::sections::image_data_section::ChannelBytes; + +/// Information about a layer in a PSD file. +/// +/// TODO: I set all of these pub during a late evening of getting to get things working. +/// Replace with private and accessor methods so that this crate is as locked down as possible +/// (to allow us to be strict). +#[derive(Debug, Clone)] +pub struct LayerProperties { + /// The name of this layer + pub(crate) name: String, + /// The position of the top of the layer + pub(crate) layer_top: i32, + /// The position of the left of the layer + pub(crate) layer_left: i32, + /// The position of the bottom of the layer + pub(crate) layer_bottom: i32, + /// The position of the right of the layer + pub(crate) layer_right: i32, + /// If true, the layer is marked as visible + pub(crate) visible: bool, + /// The opacity of the layer + pub(crate) opacity: u8, + /// If true, the layer is clipping mask + pub(crate) clipping_mask: bool, + /// The width of the PSD + pub(crate) psd_width: u32, + /// The height of the PSD + pub(crate) psd_height: u32, + /// Blending mode of the layer + pub(crate) blend_mode: BlendMode, + /// If layer is nested, contains parent group ID, otherwise `None` + pub(crate) group_id: Option, +} + +impl LayerProperties { + pub fn from_layer_record( + name: String, + layer_record: &LayerRecord, + psd_width: u32, + psd_height: u32, + group_id: Option, + ) -> Self { + LayerProperties { + name, + layer_top: layer_record.top, + layer_left: layer_record.left, + layer_bottom: layer_record.bottom, + layer_right: layer_record.right, + opacity: layer_record.opacity, + clipping_mask: layer_record.clipping_base, + visible: layer_record.visible, + blend_mode: layer_record.blend_mode, + psd_width, + psd_height, + group_id, + } + } + + /// Get the name of the layer + pub fn name(&self) -> &str { + &self.name + } + + /// The width of the layer + pub fn width(&self) -> u16 { + // If left is at 0 and right is at 4, the width is 5 + (self.layer_right - self.layer_left) as u16 + 1 + } + + /// The height of the layer + pub fn height(&self) -> u16 { + // If top is at 0 and bottom is at 3, the height is 4 + (self.layer_bottom - self.layer_top) as u16 + 1 + } + + /// If true, the layer is marked as visible + pub fn visible(&self) -> bool { + self.visible + } + + /// The position of the top of the layer + pub fn layer_top(&self) -> i32 { + self.layer_top + } + + /// The position of the left of the layer + pub fn layer_left(&self) -> i32 { + self.layer_left + } + + /// The position of the bottom of the layer + pub fn layer_bottom(&self) -> i32 { + self.layer_bottom + } + + /// The position of the right of the layer + pub fn layer_right(&self) -> i32 { + self.layer_right + } + + /// The opacity of the layer + pub fn opacity(&self) -> u8 { + self.opacity + } + + /// If true, the layer is clipping mask + pub fn is_clipping_mask(&self) -> bool { + self.clipping_mask + } + + /// Returns blending mode of the layer + pub fn blend_mode(&self) -> BlendMode { + self.blend_mode + } + + /// If layer is nested, returns parent group ID, otherwise `None` + pub fn parent_id(&self) -> Option { + self.group_id + } +} + +/// PsdGroup represents a group of layers +#[derive(Debug, Clone)] +pub struct PsdGroup { + /// Group unique identifier + pub(crate) id: u32, + /// Idx range of contained layers + pub(crate) contained_layers: Range, + /// Common layer properties + pub(crate) layer_properties: LayerProperties, +} + +impl PsdGroup { + /// Create a new photoshop group layer + pub fn new( + name: String, + id: u32, + contained_layers: Range, + layer_record: &LayerRecord, + psd_width: u32, + psd_height: u32, + group_id: Option, + ) -> Self { + let layer_properties = + LayerProperties::from_layer_record(name, layer_record, psd_width, psd_height, group_id); + + PsdGroup { + id, + contained_layers, + layer_properties, + } + } + + /// A unique identifier for the layer within the PSD file + pub fn id(&self) -> u32 { + self.id + } +} + +impl Deref for PsdGroup { + type Target = LayerProperties; + + fn deref(&self) -> &Self::Target { + &self.layer_properties + } +} + +/// Channels represents channels of the layer, stored separately. +pub type LayerChannels = HashMap; + +/// PsdLayer represents a pixel layer +#[derive(Debug, Clone)] +pub struct PsdLayer { + /// The channels of the layer, stored separately. + /// + /// You can combine these channels into a final image. For example, you might combine + /// the Red, Green and Blue channels, or you might also combine the TransparencyMask (alpha) + /// channel, or you might make use of the layer masks. + /// + /// Storing the channels separately allows for this flexability. + pub(crate) channels: LayerChannels, + /// Common layer properties + pub(crate) layer_properties: LayerProperties, +} + +/// An error when working with a PsdLayer +#[derive(Debug, PartialEq, Error)] +pub enum PsdLayerError { + #[error( + r#"Could not combine Red, Green, Blue and Alpha. + This layer is missing channel: {channel:#?}"# + )] + MissingChannels { channel: PsdChannelKind }, + #[error("{channel_id} is an invalid channel id, must be 0, 1, 2, -1, -2, or -3.")] + InvalidChannel { channel_id: i16 }, + #[error(r#"Unknown blending mode: {mode:#?}"#)] + UnknownBlendingMode { mode: [u8; 4] }, + #[error("{compression} is an invalid layer channel compression. Must be 0, 1, 2 or 3")] + InvalidCompression { compression: u16 }, +} + +impl PsdLayer { + /// Create a new photoshop layer + pub fn new( + layer_record: &LayerRecord, + psd_width: u32, + psd_height: u32, + group_id: Option, + channels: LayerChannels, + ) -> PsdLayer { + PsdLayer { + layer_properties: LayerProperties::from_layer_record( + layer_record.name.clone(), + layer_record, + psd_width, + psd_height, + group_id, + ), + channels, + } + } + + /// Get the compression level for one of this layer's channels + pub fn compression( + &self, + channel: PsdChannelKind, + ) -> Result { + match self.channels.get(&channel) { + Some(channel) => match channel { + ChannelBytes::RawData(_) => Ok(PsdChannelCompression::RawData), + ChannelBytes::RleCompressed(_) => Ok(PsdChannelCompression::RleCompressed), + }, + None => Err(PsdChannelError::ChannelNotFound { channel }), + } + } + + /// Create a vector that interleaves the red, green, blue and alpha channels in this PSD + /// + /// vec![R, G, B, A, R, G, B, A, ...] + pub fn rgba(&self) -> Vec { + self.generate_rgba() + } + + // Get one of the PsdLayerChannels of this PsdLayer + fn get_channel(&self, channel: PsdChannelKind) -> Option<&ChannelBytes> { + self.channels.get(&channel) + } +} + +impl Deref for PsdLayer { + type Target = LayerProperties; + + fn deref(&self) -> &Self::Target { + &self.layer_properties + } +} + +/// GroupDivider represents tag type of Section divider. +#[derive(Debug, Clone)] +pub(super) enum GroupDivider { + /// 0 = any other type of layer + Other = 0, + /// 1 = open "folder" + OpenFolder = 1, + /// 2 = closed "folder" + CloseFolder = 2, + /// 3 = bounding section divider, hidden in the Photoshop UI + BoundingSection = 3, +} + +impl GroupDivider { + pub(super) fn match_divider(divider: i32) -> Option { + match divider { + 0 => Some(GroupDivider::Other), + 1 => Some(GroupDivider::OpenFolder), + 2 => Some(GroupDivider::CloseFolder), + 3 => Some(GroupDivider::BoundingSection), + _ => None, + } + } +} + +/// Describes how to blend a layer with the layer below it +#[derive(Debug, Clone, Copy)] +#[allow(missing_docs)] +pub enum BlendMode { + PassThrough = 0, + Normal = 1, + Dissolve = 2, + Darken = 3, + Multiply = 4, + ColorBurn = 5, + LinearBurn = 6, + DarkerColor = 7, + Lighten = 8, + Screen = 9, + ColorDodge = 10, + LinearDodge = 11, + LighterColor = 12, + Overlay = 13, + SoftLight = 14, + HardLight = 15, + VividLight = 16, + LinearLight = 17, + PinLight = 18, + HardMix = 19, + Difference = 20, + Exclusion = 21, + Subtract = 22, + Divide = 23, + Hue = 24, + Saturation = 25, + Color = 26, + Luminosity = 27, +} + +impl BlendMode { + pub(super) fn match_mode(mode: [u8; 4]) -> Option { + match &mode { + b"pass" => Some(BlendMode::PassThrough), + b"norm" => Some(BlendMode::Normal), + b"diss" => Some(BlendMode::Dissolve), + b"dark" => Some(BlendMode::Darken), + b"mul " => Some(BlendMode::Multiply), + b"idiv" => Some(BlendMode::ColorBurn), + b"lbrn" => Some(BlendMode::LinearBurn), + b"dkCl" => Some(BlendMode::DarkerColor), + b"lite" => Some(BlendMode::Lighten), + b"scrn" => Some(BlendMode::Screen), + b"div " => Some(BlendMode::ColorDodge), + b"lddg" => Some(BlendMode::LinearDodge), + b"lgCl" => Some(BlendMode::LighterColor), + b"over" => Some(BlendMode::Overlay), + b"sLit" => Some(BlendMode::SoftLight), + b"hLit" => Some(BlendMode::HardLight), + b"vLit" => Some(BlendMode::VividLight), + b"lLit" => Some(BlendMode::LinearLight), + b"pLit" => Some(BlendMode::PinLight), + b"hMix" => Some(BlendMode::HardMix), + b"diff" => Some(BlendMode::Difference), + b"smud" => Some(BlendMode::Exclusion), + b"fsub" => Some(BlendMode::Subtract), + b"fdiv" => Some(BlendMode::Divide), + b"hue " => Some(BlendMode::Hue), + b"sat " => Some(BlendMode::Saturation), + b"colr" => Some(BlendMode::Color), + b"lum " => Some(BlendMode::Luminosity), + _ => None, + } + } +} + +/// A layer record within the layer info section +/// +/// TODO: Set all ofo these pubs to get things working. Replace with private +/// and accessor methods +#[derive(Debug, Clone)] +pub struct LayerRecord { + /// The name of the layer + pub(super) name: String, + /// The channels that this record has and the number of bytes in each channel. + /// + /// Each channel has one byte per pixel in the PSD. + /// + /// So a 1x1 image would have 1 byte per channel. + /// + /// A 2x2 image would have 4 bytes per channel. + pub(super) channel_data_lengths: Vec<(PsdChannelKind, u32)>, + /// The position of the top of the image + pub(super) top: i32, + /// The position of the left of the image + pub(super) left: i32, + /// The position of the bottom of the image + pub(super) bottom: i32, + /// The position of the right of the image + pub(super) right: i32, + /// If true, the layer is marked as visible + pub(super) visible: bool, + /// The opacity of the layer + pub(super) opacity: u8, + /// If true, the layer is clipping mask + pub(super) clipping_base: bool, + /// Blending mode of the layer + pub(super) blend_mode: BlendMode, + /// Group divider tag + pub(super) divider_type: Option, +} + +impl LayerRecord { + /// The height of this layer record + pub fn height(&self) -> i32 { + (self.bottom - self.top) + 1 + } +} + +impl IntoRgba for PsdLayer { + /// A layer might take up only a subsection of a PSD, so if when iterating through + /// the pixels in a layer we need to transform the pixel's location before placing + /// it into the RGBA for the entire PSD. + /// + /// Given this illustration: + /// + /// ┌──────────────────────────────────────┐ + /// │ │ + /// │ Entire Psd │ + /// │ │ + /// │ ┌─────────────────────────┐ │ + /// │ │ │ │ + /// │ │ Layer │ │ + /// │ │ │ │ + /// │ │ │ │ + /// │ └─────────────────────────┘ │ + /// │ │ + /// └──────────────────────────────────────┘ + /// + /// The top left pixel in the layer will have index 0, but within the PSD + /// that idx will be much more than 0 since there are some rows of pixels + /// above it. + /// + /// So we transform the pixel's index based on the layer's left and top + /// position within the PSD. + fn rgba_idx(&self, idx: usize) -> Option { + let left_in_layer = idx % self.width() as usize; + let left_in_psd = self.layer_properties.layer_left + left_in_layer as i32; + + let top_in_layer = idx / self.width() as usize; + let top_in_psd = self.layer_properties.layer_top + top_in_layer as i32; + + let idx = top_in_psd + .checked_mul(self.layer_properties.psd_width as i32) + .unwrap() + + left_in_psd; + + if idx < 0 { + None + } else { + Some(idx as usize) + } + } + + fn red(&self) -> &ChannelBytes { + self.get_channel(PsdChannelKind::Red).unwrap() + } + + fn green(&self) -> Option<&ChannelBytes> { + self.get_channel(PsdChannelKind::Green) + } + + fn blue(&self) -> Option<&ChannelBytes> { + self.get_channel(PsdChannelKind::Blue) + } + + fn alpha(&self) -> Option<&ChannelBytes> { + self.get_channel(PsdChannelKind::TransparencyMask) + } + + fn psd_width(&self) -> u32 { + self.layer_properties.psd_width + } + + fn psd_height(&self) -> u32 { + self.layer_properties.psd_height + } +} diff --git a/luda-editor/psd/src/sections/layer_and_mask_information_section/layers.rs b/luda-editor/psd/src/sections/layer_and_mask_information_section/layers.rs new file mode 100644 index 000000000..775ac9df8 --- /dev/null +++ b/luda-editor/psd/src/sections/layer_and_mask_information_section/layers.rs @@ -0,0 +1,54 @@ +use crate::PsdLayer; +use std::collections::HashMap; +use std::ops::{Deref, Range}; + +/// `NamedItems` is immutable container for storing items with order-preservation +/// and indexing by id and name +#[derive(Debug)] +pub(crate) struct Layers { + items: Vec, + // TODO: This is incorrect since layers can have the same name. Perhaps a Vec instead of + // usize. Add a failing test with a PSD with two layers with the same name. + // Take inspiration from the `Groups` type. + item_indices: HashMap, +} + +impl Layers { + /// Creates a new `NamedItems` + pub fn new() -> Self { + Layers { + items: vec![], + item_indices: HashMap::new(), + } + } + + /// Creates a new `NamedItems` with the specified capacity + pub fn with_capacity(capacity: usize) -> Self { + Layers { + items: Vec::with_capacity(capacity), + item_indices: HashMap::with_capacity(capacity), + } + } + + #[allow(missing_docs)] + pub fn item_by_name(&self, name: &str) -> Option<&PsdLayer> { + match self.item_indices.get(name) { + Some(item_idx) => self.items.get(*item_idx), + None => None, + } + } + + #[allow(missing_docs)] + pub(in crate) fn push(&mut self, name: String, item: PsdLayer) { + self.items.push(item); + self.item_indices.insert(name, self.items.len() - 1); + } +} + +impl Deref for Layers { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.items + } +} diff --git a/luda-editor/psd/src/sections/layer_and_mask_information_section/mod.rs b/luda-editor/psd/src/sections/layer_and_mask_information_section/mod.rs new file mode 100644 index 000000000..4f126775e --- /dev/null +++ b/luda-editor/psd/src/sections/layer_and_mask_information_section/mod.rs @@ -0,0 +1,486 @@ +use std::collections::HashMap; +use std::ops::Range; +use std::vec; + +use crate::psd_channel::PsdChannelCompression; +use crate::psd_channel::PsdChannelKind; +use crate::sections::image_data_section::ChannelBytes; +use crate::sections::layer_and_mask_information_section::groups::Groups; +use crate::sections::layer_and_mask_information_section::layer::{ + BlendMode, GroupDivider, LayerChannels, LayerRecord, PsdGroup, PsdLayer, PsdLayerError, +}; +use crate::sections::layer_and_mask_information_section::layers::Layers; +use crate::sections::PsdCursor; + +/// One of the possible additional layer block signatures +const SIGNATURE_EIGHT_BIM: [u8; 4] = [56, 66, 73, 77]; +/// One of the possible additional layer block signatures +const SIGNATURE_EIGHT_B64: [u8; 4] = [56, 66, 54, 52]; + +/// Additional Layer Information constants. +/// Key of `Unicode layer name (Photoshop 5.0)`, "luni" +const KEY_UNICODE_LAYER_NAME: &[u8; 4] = b"luni"; +/// Key of `Section divider setting (Photoshop 6.0)`, "lsct" +const KEY_SECTION_DIVIDER_SETTING: &[u8; 4] = b"lsct"; + +pub mod groups; +pub mod layer; +pub mod layers; + +/// The LayerAndMaskInformationSection comes from the bytes in the fourth section of the PSD. +/// +/// When possible we'll make the data easier to work with by storing it structures such as HashMaps. +/// +/// # Note +/// +/// We do not currently store all of the information that is present in the layer and mask +/// information section of the PSD. If something that you need is missing please open an issue. +/// +/// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) +/// +/// The fourth section of a Photoshop file contains information about layers and masks. This section of the document describes the formats of layer and mask records. +/// +/// The complete merged image data is not stored here. The complete merged/composite image resides in the last section of the file. See See Image Data Section. If maximize compatibility is unchecked then the merged/composite is not created and the layer data must be read to reproduce the final image. +/// +/// See Layer and mask information section shows the overall structure of this section. If there are no layers or masks, this section is just 4 bytes: the length field, which is set to zero. (**PSB** length is 8 bytes +/// +/// 'Layr', 'Lr16' and 'Lr32' start at See Layer info. NOTE: The length of the section may already be known.) +/// +/// When parsing this section pay close attention to the length of sections. +/// +/// | Length | Description | +/// |----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +/// | 4 | Length of the layer and mask information section.
(**PSB** length is 8 bytes.) | +/// | Variable | Layer info (see See Layer info for details). | +/// | Variable | Global layer mask info (see See Global layer mask info for details). | +/// | Variable | (Photoshop 4.0 and later)
Series of tagged blocks containing various types of data. See See Additional Layer Information for the list of the types of data that can be included here. | +#[derive(Debug)] +pub struct LayerAndMaskInformationSection { + pub(crate) layers: Layers, + pub(crate) groups: Groups, +} + +/// Frame represents a group stack frame +#[derive(Debug)] +struct Frame { + start_idx: usize, + name: String, + group_id: u32, + parent_group_id: u32, + layer_record: LayerRecord, +} + +impl LayerAndMaskInformationSection { + /// Create a LayerAndMaskInformationSection from the bytes in the corresponding section in a + /// PSD file (including the length marker). + pub fn from_bytes( + bytes: &[u8], + psd_width: u32, + psd_height: u32, + ) -> Result { + let mut cursor = PsdCursor::new(bytes); + + // The first four bytes of the section is the length marker for the layer and mask + // information section. + // + // We do not currently use it since the number of bytes passed into this function was + // the exact number of bytes in the layer and information mask section of the PSD file, + // so there's no way for us to accidentally read too many bytes. If we did the program + // would panic. + let len = cursor.read_u32(); + + if len == 0 { + return Ok(LayerAndMaskInformationSection { + layers: Layers::new(), + groups: Groups::with_capacity(0), + }); + } + + // Read the next four bytes to get the length of the layer info section. + let _layer_info_section_len = cursor.read_u32(); + + // Next 2 bytes is the layer count + // + // NOTE: Appears to be -1 when we create a new PSD and don't create any new layers but + // instead only manipulate the default background layer. + // + // # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) + // + // Layer count. If it is a negative number, its absolute value is the number of layers and + // the first alpha channel contains the transparency data for the merged result. + let layer_count = cursor.read_i16(); + + // TODO: If the layer count was negative we were supposed to treat the first alpha + // channel as transparency data for the merged result.. So add a new test with a transparent + // PSD and make sure that we're handling this case properly. + let layer_count: u16 = layer_count.abs() as u16; + let (group_count, layer_records) = + LayerAndMaskInformationSection::read_layer_records(&mut cursor, layer_count)?; + + LayerAndMaskInformationSection::decode_layers( + layer_records, + group_count, + (psd_width, psd_height), + ) + } + + fn decode_layers( + layer_records: Vec<(LayerRecord, LayerChannels)>, + group_count: usize, + psd_size: (u32, u32), + ) -> Result { + let mut layers = Layers::with_capacity(layer_records.len()); + let mut groups = Groups::with_capacity(group_count); + + // Create stack with root-level + let mut stack: Vec = vec![Frame { + start_idx: 0, + name: String::from("root"), + group_id: 0, + parent_group_id: 0, + layer_record: LayerRecord { + name: "root".to_string(), + channel_data_lengths: vec![], + top: 0, + left: 0, + bottom: 0, + right: 0, + visible: false, + opacity: 0, + clipping_base: true, + blend_mode: BlendMode::Normal, + divider_type: None, + }, + }]; + + // Viewed group counter + let mut already_viewed = 0; + + // Read each layer's channel image data + for (layer_record, channels) in layer_records.into_iter() { + // get current group from stack + let current_group_id = stack.last().unwrap().group_id; + + match layer_record.divider_type { + // open the folder + Some(GroupDivider::CloseFolder) | Some(GroupDivider::OpenFolder) => { + already_viewed = already_viewed + 1; + + let frame = Frame { + start_idx: layers.len(), + name: layer_record.name.to_string(), + group_id: already_viewed, + parent_group_id: current_group_id, + layer_record, + }; + + stack.push(frame); + } + + // close the folder + Some(GroupDivider::BoundingSection) => { + let frame = stack.pop().unwrap(); + + let range = Range { + start: frame.start_idx, + end: layers.len(), + }; + + groups.push(PsdGroup::new( + frame.name, + frame.group_id, + range, + &frame.layer_record, + psd_size.0, + psd_size.1, + if frame.parent_group_id > 0 { + Some(frame.parent_group_id) + } else { + None + }, + )); + } + + _ => { + let psd_layer = LayerAndMaskInformationSection::read_layer( + &layer_record, + current_group_id, + psd_size, + channels, + )?; + + layers.push(psd_layer.name.clone(), psd_layer); + } + }; + } + + Ok(LayerAndMaskInformationSection { layers, groups }) + } + + fn read_layer_records( + cursor: &mut PsdCursor, + layer_count: u16, + ) -> Result<(usize, Vec<(LayerRecord, LayerChannels)>), PsdLayerError> { + let mut groups_count = 0; + + let mut layer_records = vec![]; + // Read each layer record + for _layer_num in 0..layer_count { + let layer_record = read_layer_record(cursor)?; + + match layer_record.divider_type { + Some(GroupDivider::BoundingSection) => { + groups_count = groups_count + 1; + } + _ => {} + } + + layer_records.push(layer_record); + } + + let mut result = vec![]; + for layer_record in layer_records { + let channels = read_layer_channels( + cursor, + &layer_record.channel_data_lengths, + layer_record.height() as usize, + )?; + + result.push((layer_record, channels)); + } + + // Photoshop stores layers in reverse order + result.reverse(); + Ok((groups_count, result)) + } + + fn read_layer( + layer_record: &LayerRecord, + parent_id: u32, + psd_size: (u32, u32), + channels: LayerChannels, + ) -> Result { + Ok(PsdLayer::new( + &layer_record, + psd_size.0, + psd_size.1, + if parent_id > 0 { Some(parent_id) } else { None }, + channels, + )) + } +} + +/// Reads layer channels +fn read_layer_channels( + cursor: &mut PsdCursor, + channel_data_lengths: &Vec<(PsdChannelKind, u32)>, + scanlines: usize, +) -> Result { + let capacity = channel_data_lengths.len(); + let mut channels = HashMap::with_capacity(capacity); + + for (channel_kind, channel_length) in channel_data_lengths.iter() { + let compression = cursor.read_u16(); + let compression = PsdChannelCompression::new(compression) + .ok_or(PsdLayerError::InvalidCompression { compression })?; + + let compression = if *channel_length > 0 { + compression + } else { + PsdChannelCompression::RawData + }; + + let channel_data = cursor.read(*channel_length); + let channel_bytes = match compression { + PsdChannelCompression::RawData => ChannelBytes::RawData(channel_data.into()), + PsdChannelCompression::RleCompressed => { + // We're skipping over the bytes that describe the length of each scanline since + // we don't currently use them. We might re-think this in the future when we + // implement serialization of a Psd back into bytes.. But not a concern at the + // moment. + // Compressed bytes per scanline are encoded at the beginning as 2 bytes + // per scanline + let channel_data = &channel_data[2 * scanlines..]; + ChannelBytes::RleCompressed(channel_data.into()) + } + _ => unimplemented!("Zip compression currently unsupported"), + }; + + channels.insert(*channel_kind, channel_bytes); + } + + Ok(channels) +} + +/// Read bytes, starting from the cursor, until we've processed all of the data for a layer in +/// the layer records section. +/// +/// At the moment we skip over some of the data. +/// +/// Please open an issue if there is data in here that you need that we don't currently parse. +/// +/// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) +/// +/// Information about each layer. +/// +/// | Length | Description | +/// |------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +/// | 4 * 4 | Rectangle containing the contents of the layer. Specified as top, left, bottom, right coordinates | +/// | 2 | Number of channels in the layer | +/// | 6 * number of channels | Channel information. Six bytes per channel,
consisting of: 2 bytes for Channel ID: 0 = red, 1 = green, etc.;
-1 = transparency mask; -2 = user supplied layer mask, -3 real user supplied layer mask (when both a user mask and a vector mask are present)
4 bytes for length of corresponding channel data. (**PSB** 8 bytes for length of corresponding channel data.) See See Channel image data for structure of channel data. | +/// | 4 | Blend mode signature: '8BIM' | +/// | 4 | Blend mode key:
'pass' = pass through, 'norm' = normal, 'diss' = dissolve, 'dark' = darken, 'mul ' = multiply, 'idiv' = color burn, 'lbrn' = linear burn, 'dkCl' = darker color, 'lite' = lighten, 'scrn' = screen, 'div ' = color dodge, 'lddg' = linear dodge, 'lgCl' = lighter color, 'over' = overlay, 'sLit' = soft light, 'hLit' = hard light, 'vLit' = vivid light, 'lLit' = linear light, 'pLit' = pin light, 'hMix' = hard mix, 'diff' = difference, 'smud' = exclusion, 'fsub' = subtract, 'fdiv' = divide 'hue ' = hue, 'sat ' = saturation, 'colr' = color, 'lum ' = luminosity, | +/// | 1 | Opacity. 0 = transparent ... 255 = opaque | +/// | 1 | Clipping: 0 = base, 1 = non-base | +/// | 1 | Flags:
bit 0 = transparency protected;
bit 1 = visible;
bit 2 = obsolete;
bit 3 = 1 for Photoshop 5.0 and later, tells if bit 4 has useful information;
bit 4 = pixel data irrelevant to appearance of document | +/// | 1 | Filler (zero) | +/// | 4 | Length of the extra data field ( = the total length of the next five fields). | +/// | Variable | Layer mask data: See See Layer mask / adjustment layer data for structure. Can be 40 bytes, 24 bytes, or 4 bytes if no layer mask. | +/// | Variable | Layer blending ranges: See See Layer blending ranges data. | +/// | Variable | Layer name: Pascal string, padded to a multiple of 4 bytes. | +fn read_layer_record(cursor: &mut PsdCursor) -> Result { + let mut channel_data_lengths = vec![]; + + // FIXME: + // Ran into a bug where a PSD file had a top and left of over 4billion. + // The PSD file was 128x128 yet the single layer in the file was 1024x1024. + // Manually changing the layer's dimensions fixed the problem.. but this is something + // that we should look into handling automatically since the file opened just fine in + // Photoshop. + + // Read the rectangle that encloses the layer mask. + let top = cursor.read_i32(); + + let left = cursor.read_i32(); + + // Subtract one in order to zero index. If a layer is fully transparent it's bottom will + // already be 0 so we don't subtract + let bottom = cursor.read_i32(); + let bottom = if bottom == 0 { 0 } else { bottom - 1 }; + + // Subtract one in order to zero index. If a layer is fully transparent it's right will + // already be zero so we don't subtract. + let right = cursor.read_i32(); + let right = if right == 0 { 0 } else { right - 1 }; + + // Get the number of channels in the layer + let channel_count = cursor.read_u16(); + + // Read the channel information + for _ in 0..channel_count { + let channel_id = cursor.read_i16(); + let channel_id = + PsdChannelKind::new(channel_id).ok_or(PsdLayerError::InvalidChannel { channel_id })?; + + let channel_length = cursor.read_u32(); + // The first two bytes encode the compression, the rest of the bytes + // are the channel data. + let channel_data_length = channel_length - 2; + + channel_data_lengths.push((channel_id, channel_data_length)); + } + + // We do not currently parse the blend mode signature, skip it + cursor.read_4(); + + let mut key = [0; 4]; + key.copy_from_slice(cursor.read_4()); + let blend_mode = match BlendMode::match_mode(key) { + Some(v) => v, + None => return Err(PsdLayerError::UnknownBlendingMode { mode: key }), + }; + + let opacity = cursor.read_u8(); + + let clipping_base = cursor.read_u8(); + let clipping_base = clipping_base == 0; + + // We do not currently parse all flags, only visible + // Flags: + // - bit 0 = transparency protected; + // - bit 1 = visible; + // - bit 2 = obsolete; + // - bit 3 = 1 for Photoshop 5.0 and later, tells if bit 4 has useful information; + // - bit 4 = pixel data irrelevant to appearance of document + let visible = cursor.read_u8() & (1 << 1) != 0; // here we get second bit - visible + + // We do not currently parse the filler, skip it + cursor.read_1(); + + // We do not currently use the length of the extra data field, skip it + cursor.read_4(); + + // We do not currently use the layer mask data, skip it + let layer_mask_data_len = cursor.read_u32(); + cursor.read(layer_mask_data_len); + + // We do not currently use the layer blending range, skip it + let layer_blending_range_data_len = cursor.read_u32(); + cursor.read(layer_blending_range_data_len); + + // Read the layer name + let name_len = cursor.read_u8(); + let name = cursor.read(name_len as u32); + let name = String::from_utf8_lossy(name); + let mut name = name.to_string(); + + // Layer name is padded to the next multiple of 4 bytes. + // So if the name length is 9, there will be three throwaway bytes + // after it. Here we skip over those throwaday bytes. + // + // The 1 is the 1 byte that we read for the name length + let bytes_mod_4 = (name_len + 1) % 4; + let padding = (4 - bytes_mod_4) % 4; + cursor.read(padding as u32); + + let mut divider_type = None; + // There can be multiple additional layer information sections so we'll loop + // until we stop seeing them. + while cursor.peek_4() == SIGNATURE_EIGHT_BIM || cursor.peek_4() == SIGNATURE_EIGHT_B64 { + let _signature = cursor.read_4(); + let mut key = [0; 4]; + key.copy_from_slice(cursor.read_4()); + let additional_layer_info_len = cursor.read_u32(); + + match &key { + KEY_UNICODE_LAYER_NAME => { + let pos = cursor.position(); + name = cursor.read_unicode_string_padding(1); + cursor.seek(pos + additional_layer_info_len as u64); + } + KEY_SECTION_DIVIDER_SETTING => { + divider_type = GroupDivider::match_divider(cursor.read_i32()); + + // data present only if length >= 12 + if additional_layer_info_len >= 12 { + let _signature = cursor.read_4(); + let _key = cursor.read_4(); + } + + // data present only if length >= 16 + if additional_layer_info_len >= 16 { + cursor.read_4(); + } + } + + // TODO: Skipping other keys until we implement parsing for them + _ => { + cursor.read(additional_layer_info_len); + } + } + } + + Ok(LayerRecord { + name, + channel_data_lengths, + top, + left, + bottom, + right, + visible, + opacity, + clipping_base, + blend_mode, + divider_type, + }) +} diff --git a/luda-editor/psd/src/sections/mod.rs b/luda-editor/psd/src/sections/mod.rs new file mode 100644 index 000000000..11642aebb --- /dev/null +++ b/luda-editor/psd/src/sections/mod.rs @@ -0,0 +1,326 @@ +use std::io::Cursor; + +use self::file_header_section::{FileHeaderSectionError, EXPECTED_PSD_SIGNATURE}; + +/// The length of the entire file header section +const FILE_HEADER_SECTION_LEN: usize = 26; + +pub mod file_header_section; +pub mod image_data_section; +pub mod image_resources_section; +pub mod layer_and_mask_information_section; + +/// References to the different major sections of a PSD file +#[derive(Debug)] +pub struct MajorSections<'a> { + pub(crate) file_header: &'a [u8], + pub(crate) color_mode_data: &'a [u8], + pub(crate) image_resources: &'a [u8], + pub(crate) layer_and_mask: &'a [u8], + pub(crate) image_data: &'a [u8], +} + +impl<'a> MajorSections<'a> { + /// Given the bytes of a PSD file, return the slices that correspond to each + /// of the five major sections. + /// + /// ┌──────────────────┐ + /// │ File Header │ + /// ├──────────────────┤ + /// │ Color Mode Data │ + /// ├──────────────────┤ + /// │ Image Resources │ + /// ├──────────────────┤ + /// │ Layer and Mask │ + /// ├──────────────────┤ + /// │ Image Data │ + /// └──────────────────┘ + /// + /// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) + /// + /// The Photoshop file format is divided into five major parts, as shown in the Photoshop + /// file structure. The Photoshop file format has many length markers. Use these length markers + /// to move from one section to the next. The length markers are usually padded with bytes to + /// round to the nearest 2 or 4 byte interval. + /// + /// The file header has a fixed length; the other four sections are variable in length. + /// + /// When writing one of these sections, you should write all fields in the section, as Photoshop may try to read the entire section. Whenever writing a file and skipping bytes, you should explicitly write zeros for the skipped fields. + /// + /// When reading one of the length-delimited sections, use the length field to decide when you should stop reading. In most cases, the length field indicates the number of bytes, not records, following. + /// + /// The values in "Length" column in all tables are in bytes. + /// + /// All values defined as Unicode string consist of: + /// + /// A 4-byte length field, representing the number of characters in the string (not bytes). + /// + /// The string of Unicode values, two bytes per character. + pub fn from_bytes(bytes: &[u8]) -> Result { + // File header section must be 26 bytes long. + if bytes.len() < FILE_HEADER_SECTION_LEN { + return Err(FileHeaderSectionError::IncorrectLength { + length: bytes.len(), + }); + } + + let mut cursor = PsdCursor::new(bytes); + + // First four bytes must be '8BPS' + let signature = cursor.peek_4(); + if signature != EXPECTED_PSD_SIGNATURE { + return Err(FileHeaderSectionError::InvalidSignature {}); + } + + // File Header Section + let file_header = &bytes[0..FILE_HEADER_SECTION_LEN]; + cursor.read(FILE_HEADER_SECTION_LEN as u32); + + let (color_start, color_end) = read_major_section_start_end(&mut cursor); + let (img_res_start, img_res_end) = read_major_section_start_end(&mut cursor); + let (layer_mask_start, layer_mask_end) = read_major_section_start_end(&mut cursor); + + // The remaining bytes are the image data section. + let image_data = &bytes[cursor.position() as usize..]; + + Ok(MajorSections { + file_header, + color_mode_data: &bytes[color_start..color_end], + image_resources: &bytes[img_res_start..img_res_end], + layer_and_mask: &bytes[layer_mask_start..layer_mask_end], + image_data, + }) + } +} + +/// Get the start and end indices of a major section +fn read_major_section_start_end(cursor: &mut PsdCursor) -> (usize, usize) { + let start = cursor.position() as usize; + let data_len = cursor.read_u32(); + cursor.read(data_len); + let end = cursor.position() as usize; + + (start, end) +} + +/// A Cursor wrapping bytes from a PSD file. +/// +/// Provides methods that abstract common ways of parsing PSD bytes. +pub(crate) struct PsdCursor<'a> { + cursor: Cursor<&'a [u8]>, +} + +impl<'a> PsdCursor<'a> { + /// Create a new PsdCursor + pub fn new(bytes: &[u8]) -> PsdCursor { + PsdCursor { + cursor: Cursor::new(bytes), + } + } + + /// Get the cursor's position + pub fn position(&self) -> u64 { + self.cursor.position() + } + + pub fn seek(&mut self, pos: u64) { + self.cursor.set_position(pos); + } + + /// Get the underlying bytes in the cursor + pub fn get_ref(&self) -> &[u8] { + self.cursor.get_ref() + } + + /// Advance the cursor by count bytes and return those bytes + pub fn read(&mut self, count: u32) -> &[u8] { + let start = self.cursor.position() as usize; + let end = start + count as usize; + let bytes = &self.cursor.get_ref()[start..end]; + + self.cursor.set_position(end as u64); + bytes + } + + pub fn peek_u32(&self) -> u32 { + let bytes = self.peek_4(); + u32_from_be_bytes(bytes) + } + + /// Peek at the next four bytes + pub fn peek_4(&self) -> &[u8] { + self.peek(4) + } + + /// Get the next n bytes without moving the cursor + fn peek(&self, n: u8) -> &[u8] { + let start = self.cursor.position() as usize; + let end = start + n as usize; + let bytes = &self.cursor.get_ref()[start..end]; + bytes + } + + /// Read 1 byte + pub fn read_1(&mut self) -> &[u8] { + self.read(1) + } + + /// Read 2 bytes + pub fn read_2(&mut self) -> &[u8] { + self.read(2) + } + + /// Read 4 bytes + pub fn read_4(&mut self) -> &[u8] { + self.read(4) + } + + /// Read 6 bytes + pub fn read_6(&mut self) -> &[u8] { + self.read(6) + } + + /// Read 8 bytes + pub fn read_8(&mut self) -> &[u8] { + self.read(8) + } + + /// Read 1 byte as a u8 + pub fn read_u8(&mut self) -> u8 { + self.read_1()[0] + } + + /// Read 2 bytes as a u16 + pub fn read_u16(&mut self) -> u16 { + let bytes = self.read_2(); + + let mut array = [0; 2]; + array.copy_from_slice(bytes); + + u16::from_be_bytes(array) + } + + /// Read 4 bytes as a u32 + pub fn read_u32(&mut self) -> u32 { + let bytes = self.read_4(); + u32_from_be_bytes(bytes) + } + + /// Read 1 byte as a i8 + pub fn read_i8(&mut self) -> i8 { + let bytes = self.read_1(); + + let mut array = [0; 1]; + array.copy_from_slice(bytes); + + i8::from_be_bytes(array) + } + + /// Read 2 bytes as a i16 + pub fn read_i16(&mut self) -> i16 { + let bytes = self.read_2(); + + let mut array = [0; 2]; + array.copy_from_slice(bytes); + + i16::from_be_bytes(array) + } + + /// Read 4 bytes as a i32 + pub fn read_i32(&mut self) -> i32 { + let bytes = self.read_4(); + + let mut array = [0; 4]; + array.copy_from_slice(bytes); + i32::from_be_bytes(array) + } + + /// Read 8 bytes as a f64 + pub fn read_f64(&mut self) -> f64 { + let bytes = self.read_8(); + + let mut array = [0; 8]; + array.copy_from_slice(bytes); + + f64::from_be_bytes(array) + } + + /// Read 8 bytes as a i64 + pub fn read_i64(&mut self) -> i64 { + let bytes = self.read_8(); + + let mut array = [0; 8]; + array.copy_from_slice(bytes); + + i64::from_be_bytes(array) + } + + /// Reads 'Unicode string' + /// + /// Unicode string is + /// A 4-byte length field, representing the number of UTF-16 code units in the string (not bytes). + /// The string of Unicode values, two bytes per character and a two byte null for the end of the string. + pub fn read_unicode_string(&mut self) -> String { + self.read_unicode_string_padding(4) + } + + /// Reads 'Unicode string' using specified padding + /// + /// Unicode string is + /// A 4-byte length field, representing the number of UTF-16 code units in the string (not bytes). + /// The string of Unicode values, two bytes per character and a two byte null for the end of the string. + pub fn read_unicode_string_padding(&mut self, padding: usize) -> String { + let length = self.read_u32() as usize; + // UTF-16 encoding - two bytes per character + let length_bytes = length * 2; + + let data = self.read(length_bytes as u32); + let result = String::from_utf16(&u8_slice_to_u16(data).as_slice()[..length]).unwrap(); + + self.read_padding(4 + length_bytes, padding); + + result + } + + fn read_padding(&mut self, size: usize, divisor: usize) -> &[u8] { + let remainder = size % divisor; + if remainder > 0 { + let to_read = divisor - remainder; + self.read(to_read as u32) + } else { + &[] as &[u8] + } + } + + /// Reads 'Pascal string' + /// + /// Pascal string is UTF-8 string, padded to make the size even + /// (a null name consists of two bytes of 0) + pub fn read_pascal_string(&mut self) -> String { + let len = self.read_u8(); + let data = self.read(len as u32); + let result = String::from_utf8_lossy(data).into_owned(); + + if len % 2 == 0 { + // If the total length is odd, read an extra null byte + self.read_u8(); + } + + result + } +} + +fn u8_slice_to_u16(bytes: &[u8]) -> Vec { + return Vec::from(bytes) + .chunks_exact(2) + .into_iter() + .map(|a| u16::from_be_bytes([a[0], a[1]])) + .collect(); +} + +fn u32_from_be_bytes(bytes: &[u8]) -> u32 { + let mut array = [0; 4]; + array.copy_from_slice(bytes); + + u32::from_be_bytes(array) +} diff --git a/luda-editor/psd/tests/README.md b/luda-editor/psd/tests/README.md new file mode 100644 index 000000000..282df268d --- /dev/null +++ b/luda-editor/psd/tests/README.md @@ -0,0 +1,3 @@ +# Tests + +We verify that we can parse the correct information from different [PSDs in the fixtures directory.](./fixtures) diff --git a/luda-editor/psd/tests/blend.rs b/luda-editor/psd/tests/blend.rs new file mode 100644 index 000000000..a48218ca8 --- /dev/null +++ b/luda-editor/psd/tests/blend.rs @@ -0,0 +1,224 @@ +//! FIXME: Combine these all into one test that iterates through a vector of +//! (PathBuf, [f32; 4]) + +use anyhow::Result; +use psd::Psd; + +const BLEND_NORMAL_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 170, 192]; +const BLEND_MULTIPLY_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 85, 192]; +const BLEND_SCREEN_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 170, 192]; +const BLEND_OVERLAY_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; + +const BLEND_DARKEN_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 85, 192]; +const BLEND_LIGHTEN_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 170, 192]; + +const BLEND_COLOR_BURN_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; +const BLEND_COLOR_DODGE_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; + +const BLEND_LINEAR_BURN_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 85, 192]; +const BLEND_LINEAR_DODGE_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 170, 192]; + +const BLEND_HARD_LIGHT_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 170, 192]; +const BLEND_SOFT_LIGHT_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; +const BLEND_VIVID_LIGHT_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 170, 192]; +const BLEND_LINEAR_LIGHT_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 169, 192]; +const BLEND_PIN_LIGHT_BLUE_RED_PIXEL: [u8; 4] = [85, 0, 170, 192]; +const BLEND_HARD_MIX_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; + +const BLEND_SUBTRACT_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; +const BLEND_DIVIDE_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 85, 192]; + +const BLEND_DIFFERENCE_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 170, 192]; +const BLEND_EXCLUSION_BLUE_RED_PIXEL: [u8; 4] = [170, 0, 170, 192]; + +/// cargo test --test blend normal -- --exact +#[test] +fn normal() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-normal.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_NORMAL_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend multiply -- --exact +#[test] +fn multiply() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-multiply.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_MULTIPLY_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend screen -- --exact +#[test] +fn screen() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-screen.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_SCREEN_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend overlay -- --exact +#[test] +fn overlay() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-overlay.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_OVERLAY_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend darken -- --exact +#[test] +fn darken() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-darken.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_DARKEN_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend lighten -- --exact +#[test] +fn lighten() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-lighten.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_LIGHTEN_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend color_burn -- --exact +#[test] +fn color_burn() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-color-burn.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_COLOR_BURN_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend color_dodge -- --exact +#[test] +fn color_dodge() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-color-dodge.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_COLOR_DODGE_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend linear_burn -- --exact +#[test] +fn linear_burn() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-linear-burn.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_LINEAR_BURN_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend linear_dodge -- --exact +#[test] +fn linear_dodge() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-linear-dodge.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_LINEAR_DODGE_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend hard_light -- --exact +#[test] +fn hard_light() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-hard-light.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_HARD_LIGHT_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend soft_light -- --exact +#[test] +fn soft_light() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-soft-light.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_SOFT_LIGHT_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend divide -- --exact +#[test] +fn divide() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-divide.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_DIVIDE_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend subtract -- --exact +#[test] +fn subtract() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-subtract.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_SUBTRACT_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend difference -- --exact +#[test] +fn difference() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-difference.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_DIFFERENCE_BLUE_RED_PIXEL); + + Ok(()) +} + +/// cargo test --test blend exclusion -- --exact +#[test] +fn exclusion() -> Result<()> { + let psd = include_bytes!("./fixtures/blending/blue-red-1x1-exclusion.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(image[0..4], BLEND_EXCLUSION_BLUE_RED_PIXEL); + + Ok(()) +} diff --git a/luda-editor/psd/tests/channels.rs b/luda-editor/psd/tests/channels.rs new file mode 100644 index 000000000..cf19f59d3 --- /dev/null +++ b/luda-editor/psd/tests/channels.rs @@ -0,0 +1,62 @@ +use anyhow::Result; +use psd::ColorMode; +use psd::Psd; +use psd::PsdDepth; + +/// cargo test --test channels one_channel_grayscale_raw_data -- --exact +#[test] +fn one_channel_grayscale_raw_data() -> Result<()> { + let psd = include_bytes!("./fixtures/one-channel-1x1.psd"); + let psd = Psd::from_bytes(psd)?; + + assert_eq!(psd.color_mode(), ColorMode::Grayscale); + assert_eq!(psd.depth(), PsdDepth::Sixteen); + + let final_image = psd.rgba(); + assert_eq!(final_image, [175, 175, 175, 255]); + + // There is one layer which should have the same RGBA as the final image + let layer_rgba = psd.flatten_layers_rgba(&|_| true)?; + assert_eq!(layer_rgba, [175, 175, 175, 255]); + + Ok(()) +} + +/// Right now we just make sure that nothing throws when we try to parse a psd that +/// is 16 bit grayscale. +/// +/// After exporting this PSD into a png from Photoshop, the colors appeared to be slightly off. +/// Similarly, our colors appear to be slightly off. Usually by 10 or so units (out of 255). +/// +/// We can investigate this in further in the future. +/// +/// cargo test --test channels two_channel_grayscale_raw_data -- --exact +#[test] +fn two_channel_grayscale_raw_data() -> Result<()> { + let psd = include_bytes!("./fixtures/two-channel-8x8.psd"); + let psd = Psd::from_bytes(psd)?; + + assert_eq!(psd.color_mode(), ColorMode::Grayscale); + assert_eq!(psd.depth(), PsdDepth::Sixteen); + + // The 64th pixel in the image. So the top right corner pixel + // + // We used the eyedropper tool in photoshop to get the exact color of the top right pixel. + // Verify that it appear here. + // TODO: + /* + let top_right_pixel = 7 * 4; + let expected_top_right = &[27, 27, 27, 255]; + + let composite = psd.rgba(); + let composite_top_right = &composite[top_right_pixel..top_right_pixel + 4]; + + let layer = psd.flatten_layers_rgba(&|_| true)?; + let layer_top_right = &layer[top_right_pixel..top_right_pixel + 4]; + + assert_eq!(composite_top_right, expected_top_right); + assert_eq!(layer_top_right, expected_top_right); + */ + + Ok(()) +} diff --git a/luda-editor/psd/tests/compression.rs b/luda-editor/psd/tests/compression.rs new file mode 100644 index 000000000..853c291e4 --- /dev/null +++ b/luda-editor/psd/tests/compression.rs @@ -0,0 +1,74 @@ +use anyhow::Result; +use psd::{Psd, PsdChannelCompression}; + +const RED_PIXEL: [u8; 4] = [255, 0, 0, 255]; +const GREEN_PIXEL: [u8; 4] = [0, 255, 0, 255]; +const BLUE_PIXEL: [u8; 4] = [0, 0, 255, 255]; + +/// cargo test --test compression rle_decompress_final_image -- --exact +#[test] +fn rle_decompress_final_image() -> Result<()> { + let psd = include_bytes!("./fixtures/rle-3-layer-8x8.psd"); + let psd = Psd::from_bytes(psd)?; + + assert_eq!(psd.compression(), &PsdChannelCompression::RleCompressed); + + let image_data_rgba = psd.rgba(); + + // Final image is blue since the top layer is blue + assert_eq!(image_data_rgba, make_blue_8x8_rgba()); + + Ok(()) +} + +/// cargo test --test compression rle_decompress_layer -- --exact +#[test] +fn rle_decompress_layer() -> Result<()> { + let psd = include_bytes!("./fixtures/rle-3-layer-8x8.psd"); + let psd = Psd::from_bytes(psd)?; + + for (layer_name, expected_pixels) in [ + ("Red Layer", make_red_8x8_rgba()), + ("Green Layer", make_green_8x8()), + ("Blue Layer", make_blue_8x8_rgba()), + ] + .iter() + { + test_rle_layer(&psd, &layer_name, expected_pixels); + } + + Ok(()) +} + +fn test_rle_layer(psd: &Psd, layer_name: &str, expected_pixels: &[u8]) { + let layer = psd.layer_by_name(layer_name).unwrap(); + assert_eq!(&layer.rgba().as_slice(), &expected_pixels); +} + +// Below are methods to make different expected final pixels so that we can text our generated +// pixels against these expected pixels below. + +fn make_red_8x8_rgba() -> Vec { + make_8x8_rgba(RED_PIXEL) +} + +fn make_blue_8x8_rgba() -> Vec { + make_8x8_rgba(BLUE_PIXEL) +} + +fn make_green_8x8() -> Vec { + make_8x8_rgba(GREEN_PIXEL) +} + +fn make_8x8_rgba(color: [u8; 4]) -> Vec { + let mut pixels = vec![]; + + for _ in 0..8 * 8 { + pixels.push(color[0]); + pixels.push(color[1]); + pixels.push(color[2]); + pixels.push(color[3]); + } + + pixels +} diff --git a/luda-editor/psd/tests/file_header_section.rs b/luda-editor/psd/tests/file_header_section.rs new file mode 100644 index 000000000..940f3287b --- /dev/null +++ b/luda-editor/psd/tests/file_header_section.rs @@ -0,0 +1,37 @@ +use anyhow::Result; +use psd::PsdDepth; +use psd::{ColorMode, Psd}; + +/// cargo test --test file_header_section file_header_section -- --exact +#[test] +fn file_header_section() -> Result<()> { + let psd = include_bytes!("./fixtures/green-1x1.psd"); + + let psd = Psd::from_bytes(psd)?; + + assert_eq!(psd.width(), 1); + assert_eq!(psd.height(), 1); + + assert_eq!(psd.depth(), PsdDepth::Eight); + + assert_eq!(psd.color_mode(), ColorMode::Rgb); + + Ok(()) +} + +/// cargo test --test file_header_section negative_top_left -- --exact +#[test] +fn negative_top_left() -> Result<()> { + let psd = include_bytes!("./fixtures/negative-top-left-layer.psd"); + + let psd = Psd::from_bytes(psd)?; + + assert_eq!(psd.width(), 1); + assert_eq!(psd.height(), 1); + + assert_eq!(psd.depth(), PsdDepth::Eight); + + assert_eq!(psd.color_mode(), ColorMode::Rgb); + + Ok(()) +} diff --git a/luda-editor/psd/tests/fixtures/16x16-rle-partially-opaque.psd b/luda-editor/psd/tests/fixtures/16x16-rle-partially-opaque.psd new file mode 100644 index 0000000000000000000000000000000000000000..d98c1a0e663ee78cbdaf0fc6f7868b9ce2be16fe GIT binary patch literal 23160 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 NCNIIUtV7n*e*&b!y~zLo literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/README.md b/luda-editor/psd/tests/fixtures/README.md new file mode 100644 index 000000000..64189ef20 --- /dev/null +++ b/luda-editor/psd/tests/fixtures/README.md @@ -0,0 +1,87 @@ +## Fixtures + +Here we describe all of our test fixtures along with the original reason that they were created. + +Note that over time we might re-use our existing test fixtures in different ways - so these only +only describe the original motivation for creating them, not every way that they may be used right now. + +#### green-1x1.psd + +A PSD file with a single green pixel. + +The original `Background` layer was deleted and a layer called `First Layer` was created. + +This allowed for the layer to show up in the layer and mask information section of the PSD file. + +#### two-layers-red-green-1x1.psd + +A PSD file with two layers. The bottom layer is green and the top layer is red. + +This was originally created to test our final flattened image data in the image data section +by ensuring what we return a red image. + +#### transparent-top-layer-2x1.psd + +Three layers. + +Top layer has left pixel transparent, right pixel blue. + +Middle layer green, bottom layer red. + +This was originally created in order to test our `flatten_layers` method. +If it worked properly we should see blue on right right but a lower layer on the left +since the left of the top most layer is transparent. + +#### 3x3-opaque-center.psd + +3x3 grid of pixels with all transparent except for an opaque middle blue pixel and top right blue pixel. + +Originally created to test having a layer where the layer's dimensions are smaller than the PSD dimensions. + +#### 16x16-rle-partially-opaque.psd + +16x16 grid of pixels with all transparent except for an opaque block of 9x9 rle compressed red pixels, + +Originally created to test having an rle compressed layer where the layer's dimensions are smaller +than the PSD dimensions. + +#### transparent-above-opaque.psd + +1x1 pixel PSD with top layer transparent bottom layer blue. + +Originally created to test an error where we were borrowing a RefCell twice while recursively flattening a transparent pixel. + +## one-channel-1x1.psd + +1x1 gray image that only has one channel. + +It was getting an index out of range for slice error since we assumed that there were always 3+ channels. + +Color mode is grayscale. + +## two-channel-8x8.psd + +8x8 image with different shades of black/white/grey + +It has two channels, red and green. + +Color mode is grayscale. + +## negative-top-left-layer.psd + +A PSD file with a single layer that has an X,Y position of (-4px, -4px) and a width and height of +9x9 even though the PSD's size has been set to 1x1 + +This happened while resizing a PSD from 1024x1024 down to 1x1. + +## green-chinese-layer-name-1x1.psd + +Support for unicode encoded layer names + +https://github.com/chinedufn/psd/issues/4 + +## green-cyrillic-layer-name-1x1.psd + +Support for unicode layer names + +https://github.com/chinedufn/psd/issues/4 diff --git a/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-color-burn.psd b/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-color-burn.psd new file mode 100644 index 0000000000000000000000000000000000000000..dae18d55de88dd1355b7b33934b5c7f79aaa527e GIT binary patch literal 23050 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 Dc=z>a literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-color-dodge.psd b/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-color-dodge.psd new file mode 100644 index 0000000000000000000000000000000000000000..68581227057f19ab086e699d7e3c78c0e531274a GIT binary patch literal 23050 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 E1>XAi#sB~S literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-darken.psd b/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-darken.psd new file mode 100644 index 0000000000000000000000000000000000000000..b1bbaa8a5a8d045121a1dbb54ef643fc97454970 GIT binary patch literal 23052 zcmeHP2Y6Fe-#<6AY12Je1wspL8EKPrrJ*~W(9+VDDGE2qZ5o;;A)_rg7$Pe8IuH>7 z2SbJ^D98{|0YMzR2q?q(77+nOEFgk{_WPf6vj`aZzSsBpzVCT*pWb`_`~1)EjC=0w zbD}J+tw$7M4<6hwO`#yfWksW3R~A=R^RNaXVLS&R_(McCfXE`wT$pevZCCeY%kS2| zvR8LxJJI#Wx}58|1??7E+sxP{8b)t0DL?O#QSG z4cckV+8kX%L4I&z ziP~&sB$=sMQms5)Dap=Bm1pKC6&XV$3Yk1BO_q};&ydRGDp`(7CYLymg#2Kja;>^X zRb5G$vpC@L5}NFGiz+Rxt*tG!Ej`t2)u+jGa&ppSiZq2n3Kmk^RFj=~pf=sqlQpZ%Sxh1Wgwxr2ZWofr^(rMj3 z7KhbHQqyVE7$eidnCv$2li$)0Ag!);yWX<6mX=$3+U%vRPy;6-xAe3%Otmm+b&SpI zuxgpoR>&mrR>@nM%yzS_$^0kvcB|Rilg42&%Z2n+YAZwA&DI99*_h9^d#x*;M6Jf{ ztdyiw(^`WG7cww6&DY7(2A+6K32kTc!8b=LQ%L2R4Kk%FU8$01N@bZUnanE)Yv~iC z8v3J-w$sii3Yk=vAyvp46bh9h9Xi`!8C;!FZm{)@W7ZiOr`}c+kEK@(omSOowzkmr zeA;3$8niU-fN8BJowHlHq0LS6n0on>?xCtQ+3d7Q%T$)+gCNyl(5W)AG87tZhEgh% zDdp01Sq3f5k;^nvEv->z!C$64gCQVQS9`nP6uQK$b>JQfq1nN#GixEdJ1t09a}90uvcYxHL19W|c7{@x zsgNqNv{}*&4V@vSvlyMUF*{q6qiM|2X>uA}Mcg9HO+{50Y|tO4di7K2tz%Z3!x~wdvLY9>wWn>u|X$GCsD5Wzq)1^7t*_k;R+RSt%o#kri|3O1!H^{S9@^n=O zJoMOk?0>T%JUv<4M4R-CEdb8XG_pBK$n{+)!UT7PF$pICuHd;sy!2H^6K zkB>G0mk%KR*#KPr@$u0H;PL^)KO2C{KR!O%09-zR_-6xf`Nzjc8-U9P5dUldF8}!W zXajKh0OFqwz~vtwA8i0GA3*%G0l56*MP@e|&tj0l0hs@y`a}@{f;?HUO6o zApY3^T>kO#(FWl10mMHWfXhEVKH30WK7jaV{lXRO+33lbV0UL5Y~v*RKviUq=oPp| zz{&124Y62ZgI`4XBDOHr0d@kI8|xXn!&0M}tOaE(uP;x=D}*zbm&q#zYB-Yc z%6JOG?T*WpxPR%o2k>=s@;s*YEpX`likBqX^b|vW`y3}>Sjt? zj3sUckoANv);8;%(6G3ny7FQ*WJ9(A*Xbk@o!Ox&G4)#aFep+WgsnyroT>S*o*ck3O zZB)fHvSECRZ7^GIS!})5$jWQ2+QM--0y!eIR~O2aeb!*oyWk7MiaN-t*lf3( zTa0Ft-svKCX>bZ&@=%w&&Y*AdR)n||kek~cj~?!iY!2vgxKpt4+Ic4?isB1@ZQwFqa#@0(0D67sT~h*sYl3-Cr?Ir4#jG5-Z*nSFoTG95RqV{K*lL# zPbccQb-iQt#4Hyhz}6kJ#E-;t(M0HVaZUn6f@yEwE%dS{q%$E*n;b?wFXM^{VE?;G zXY-j|w01e@NR{!BboYA@`ivV?>=|DqjXKZp!Mwl*yPX-^Xk!(g0H_-0 zrn1DVETP_Nc35r@3Cv{suxoBFt;f;Xy}%G7ly=z7<&253;(fRvKzI+Y(>Rzl#uBWl zY|%^L_Mfgl7k&ibhDRO&|3*o*O@HHKQ9v8*4Yd9St1vBNG&V5p_DWktLv@vFnhkP^ zZ!i`$nXS``j0U~yDHzUHzQQGjnhSMIBkds5zNnS4+HYY#+9kfxJVc`>yZ*eujb!5& zm%A0=^q*ietGHw|S4$!Ea7L->(IGjn1ZR0s3_qU9+_oD?D@sN{}bE*)5o!0CbEM5(L_U zX+2BN#cyT3XDQ20N6Z@rtAOqG?*=j!DycH_g@sJfUFc&P~GnotGA)1Mnh85?sYi)`~i~ z#{h1}o6W;94ed>xGigTD01dMTb=5$IJEl3oO^%9DtUV`QYg6NAO&rK&I=Td}7~_F1 zGZ{zV_QrBeqoJ%4Xy_rFc7wgL0ca?T^AKYztp^&%TxvDfIm_ZKZM3S3oWAR6D^RTc z9!K*iEXVPWH!+p2_&-l;Y8V4@$eA15Y8X8NXjmC=<800KI3LJ?+kaY<8q0}|)lrA@ z?+Fppz1ZH=fZG_yd09)7{JR6ajWHED zG2!0D*v8`Az@D>I$CQ);4Rz-pWK5%+_2qV0?8Ue&?0eB-Bx3{YdBKda9Lp1cj<>be zJAHZic54HcL!P`57JDtm5bDL#Os0q7eh>NZ+EEo!qY@-R4mdT)j3y()fpc5W0mo{} z5RI%zj~G}Duo9QY7@{%3wH{4FKOz&@*xY`_M9ZL7kpJ$Mvc6K(NVKiQqZxe%CA6U7 z$OyaxXa;;-rHw}4g2dqFU8QRwS0kKQceAVrYE=cLVk?I$t|Z)LDJT_`2r2|+f`PoA zu#OwbtAZnv#c%)S3FdWI-ndDD+X}%sxvRi z`HqBFj1Zd_$LootybLI*2;Q28!uwMWWXskD*WQJ^cdguYKpxbcb|RUGBkCq96s7oj zh7AhKbZ3Y$W$|E9V{_HGadl;IbNL>P0qG#m;m%y$mfVN9o4EV9ySN{4cW}2N3HKfD zHtv4zTbOp1Uq)I{?=G#5v~e@EjKOW=sxJ)^Tqf=ZHrYDaS=n_qiXEP`&~K62Y?(@) zJ(41r`58&2Nt;T3vnU54{2akH2wjRJ&j%{*Lyv6MR|w^egLUZ+kE{yD>FZ};0e#LR zOI`;Hm$wmm{{gMT+RDb`;Ts>;+2JsvdZ0ci5v4$V6etS~MTMvo4MR1k0gXd)607(I_xpf%_fv=MDZJJ2rl9{LC!L`TpU(0fjyA5bUy1zn>k zN=SuL(NqtrFExM~OerWOl~0vYRa6}{j+#X2DHG+O?xF6d=27#h#ndutE%h4p2DOX& zfI38dMxCI}QWvS;I9!gH6T|7vN#dk&l$;{Ya83i~PL7^4g)@yaoAW4V5obB)WzH7P zF3v}sBb*M-51h+f#0}xbar<$lTqRe{t>KR6Hgc`p`?zzt3%Sd=>$%&Zoj>7raL;jn zchxZBZ1h13#2S1b_&mYWJ@hkab`FegE ze-3{ke-(c-e=m%tQ~X~90zsT0S&$8*ahzbX;BLVqf@cLA1Um(X1SbWTg+gHu;b38& zutrD=?ZP?2XM`^c-w_@bo)TUS3JK~PqzEbt8Xsf|niaG#=%t{ygANCs4!S0a5DgHi zL^UFvXsYNj(Mr*F(Z`}wqHDpC!O6jS!41KagJ%XW3SJ+)H~7ooOCcd42_ZQlBSQ=! zGeQ=Hyc+U;$cd0&#ZlrxVzqdJ*dcyYyjr|d{F(StXlQ6sXkqC1P7r&wt%!Oz>SQ!GIx$)ut&P4vdS&$9 z=uaq`1mBW87nL8{>|~{no8_ zx1w%Lw}-o}?{>J`mF~T|7j|d5&+WdU`;qRy^^o)^?a|!hi5^>feAScJb5PHbJ==Od z*K=>rbG>4E4eh1vHMiHsUZ2Nv;s?do#oryjB7T4T<=%aIm-n{xex~=kz0dWD>r>cg za-Ri#-tKd@Z*<@MzJ|UF`o7cmdr7RMNMe*MlI)Rm_KWXV(a+IuS-%7Qt|cTVj7pf5 zup!~A#Nb3_Vq@Zh#NCOV{rmQ>?tf4Jb^SjdAR3??&@|xb0s97ANlH!{lk{NH)}%AZ z-IFVmrzNjT{xT&rr66TW%JP&?2MPu%2R09UcHqH5oI#m`ng%Ty^zmTM;H<%h!Ospp zG=x7SXNYOY@*&5hVrh}IRk}`kA~i0xDs@)s*3|Q9{nN&$J(0FA?GIU|%qUwa`$8Ti zA10qA-!8wX7^KiDmMA_=4^1yipPs%g{X)jz3?}2bjN_TnnbnyOX70}XJu5rQnzcUb zYCu_c{O|G3%`{qc& z$l8%hMt)Z(ue+yiUww4_#QK-&e`zRgc&y=zQ7NMwqjrxDA3b68+R<0Wl#H1_rekc{ z*n7u*G_L13!?D)?ktnX%x6wF<~7c5JfqLkKdC?4 zRM51r>6}4rSZug3xnlD3ldm+_G_P*%GLA8BXbEbW)bfTY%G7MyWA0;aHy^SLw#>0~ zOi@jFYD%YdxOJ6{vfXLhW{-gd`+i4?;{nHs*8J8btykMdw{2>VY&W&-MqWCckjNZ-#s($IdN~%z02;S?$g}2dwSyZ+0#$WD4(%rX7J4Bng5(6n>Byd z&-ahHfBWp-vuDgc`9S#t>*j>bvCcX6;Lr!3e~9;x;h_T$r$7A6!+*@x&HZqmY~I3o z*B{Y7^5H+^|5)^ou16amJ@8o8W6wPEn-(8gQoLlt()gthFTL`t?%Bi76+XBA z`QFdZd;Yf<3@;pCR=#Y@@}%VpR|r^v19^c5ffLee;{CZ?4|aZ^x3iV&8h~?cld( zzuooDw0AD=wCp^$OTX*n?uomge4zT^ zjSrO{Zv80hqb>V0_iz4Z=07(d$U3m)IIcT><}>4G7d~(O{Mr{YzT|&7_p6An7JeQ7_40oW{MUw#td6%% zl$_ZAP5n1toYbED;al6c*H6vdq#0)$Jw&82fx4L`%^zyezxHh1k6h|;Y4y(;KkvR=d->~MT7J29W$x7;SJ(Wi{B_@N zV}3h*t?jz-`l8;eUMD)zZ}kzd%NlwE!2c9QdM!F0O`k0(|?7J_zfFJJ|O; zVnMO*ra)896N`Ol_FWJwU=4xR!woOj)jJV-70#p?5F2>BRBtFmob+_}2@-tR8q*bo zpu|>Knq2Mbx&+@Dh1(JO-P+Z4?b)ub-&exB!e!^Pt8*zVEMvC+}pBgN!!-dM)>N`;1mgocGigoQ=K zgoTC0;7eEx>k@f80lIdg$RMZ$#ibGuCz9euQe9s`C8(}FQ~_kkhs75hkT5>Dh~n`0 z0%4FSI0SrdRKnL~u1gt#C?3V-@Hl*-Ac)Tur-L$*%j*{<=NFBnqZ6jgR0v|8SoT_R zV*l8>U7GX^>#Rd+;eh$|ohL46ZJBY)H{PFA@??XqboUB-R=2~WzF{tIn*Hu4j+2+l zk{7Ie;Pt2WeERLrFK*s@-HBG_ys`?z6vBma+Hk&vn3_qk|;%WH5vp&5E4>RsGwru=AfEPIKGN>209vTY5hk%0RIov7yUD{I@ zD#241YLItbIFu^}rQmpClVEVc-z0E-oOkXJ+~d3caH*YIh(Z3FflDSsf?NS9#Km)i z34TFy1^^AAa^ZrH6!!lg*vtPZX8#i;siLPcFoBU4G&)QMAmMure4i*L9Rqib7HI4$ z_+~H_x{MWU&p@ z-MF9R(b#Uq9US}q*@3I@7pUuKYqJw|oC!W|8+8%>?|~C?f+UMLd{sgscz-p+|98Bw zcD9NKZSwz=|MO^Xvi}9zxN{QR;NnYQRBCBEX=_ZERoj}u44#5yBw#mq?oKb5zVp8T Dv{3WG literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-difference.psd b/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-difference.psd new file mode 100644 index 0000000000000000000000000000000000000000..72a5d1f2a5c52ef98bc10a915cdb02170aca89a5 GIT binary patch literal 23052 zcmeHP2Y6Fe-#<4uqiNGUSOr3Xwv4n%y3){{PH1Uq%M^u-n=~{{LPlF~0U|2+IuH>7 z2SbJ^xELbJ5X8ZYfHItK5fMAm;A&;R_+xaZzJ zCyJ7~21FtD;KB{l6beB!D;o2LqNJ*Zi!}%d5;+LLA0o1WL>6`W{KSjtJG(Diey8E( zJ-WkNiEaSaN* zR#|L}I4dn%qLpPR#5uWXvaDQ%Jaed6E|q1cOLNm@nG&f?Da}<%Wn$NnR1gYOo=vA$ z)|ZyMiUTe`smbB6D$~>3+S=0EGSVzIL%J+CH#c1>PnXLjU?H(jGdtAn60^NO8^j%_ zl(B1VMytbUF^h3rwZ`IfNJf;@+-UM*HnL1udzsbiYf(vse;wx-L{r0F+v(rGqWTlik!0Ag!tKxZbq5mX@1(+8t%BPy-htH}$kPPO~!U^^D!( zv}u{LR>-9P&62k^SsWI7ljTq9?NPI}Cym2mmIvu8RW?TLu-FHbdMHt@z{O4Sag0DN;LQn^Hy)hJacGZac$mPDGRluCVqu$F!? zYM?*r)DE>Pid-s@W=iDJM!8&>k*Um(50PegqFiU|AIG9I>ZjdO6tAUE44qb~x7b?L zjsmsSYBFloxC5rQnsu&j<$*RY-D~ROPr8S)%4~P2&040av;YKYMx#!tm&)}rDL|vn zRwyLd+RPk@TAm@3XmfHkS#m~`n0vQ>C>kin-^YjJZHXz>7Gbb0C5Nxf-#JY8ZB2|h(C}eH5vxBBVz=lWZE71+Y=9neYh#;@d%dH70eR_o1~wW015II9 zxBhz#q07!z$fent5=NS-kz}fK^%8YfR)!=uCnqa6Q=65cP-nXv`hU<6IgPR$r7T04 z2@gGX9{b;H2yai;HmS`9MpuyTuA5u%|EcY6ZBG2}528PPdi-r|mm^LcYc%UDZFYD( z-pUaA+ggqti`UPK?z#5Hc{I=aTu@m|7F&%)#}s7brQb~K?KwVPRVr1T&7wD&n1WKH z-D*-#^L6&oy1d!+d=_tH&S0gmG=x=Yx@TE>!*t@AcmGbpTdlt{s$Z;NA%1`aW`l4A z#>Y<^gv$?*z-$n%!1(xSgK+r)5||Cb6&N2sZ4fR$KmxNtxB}zjrwzj82S{Kx2v=Zy z{Io&1`~V5e2H^^fkDoROmmeU3*&tkj@$u6J;qn6{FdKv`Fg||TAY6Wc1ZIP91;)ou z8-&XbkicvZuE6;CX@hY20TP%E!W9@FKWz{$KR^PrLAV0reEhUQ zxcmSK%m(2KjE|o-2$vrqf!QEjf${Ov2I2ApBrqF^=|ZJ%&@z&4YqNTeV|IRNAxmW zqu^xsnTA99H-Amm|_I=iDm1KawDMU~mXFlJ{9R$v<6RVEweBZ+T=u^n5K z7#-LLtJ#>0ZKS$|X)GJp2qLyH)&X_`So95y!)dM6Owocep4*oz_+U^=_KMm;-wj);tPZogT2O6`&BN3i>@^-nWizY1jM?F3sx>CNM>EpkX!S5< zEv8Zr1IT(qmuQ;}E@)WXP<=&-3bG;Ffa`T)vCiVu6j?fOhUBQQnQxGlm~ONyvFRE| zn;qr->rJ?|Jx7U2C%&1y-fnUbd0o4yNaHaNZDq8u)eiPY!hTv;KdCd++1>pF)7Tj9 zIBitT=-DuS#Wq^3H!Ze7YhvYfHf_;(9Dy8BTARf>7IwJR;XZ3L8{F_kU`0LTRAO;B zEG;IB+2C>!xivTiA9=W2UT-us`6|NP3dqf4k4F#vBbx(y4DJ+cymsBm$Vw)<2v?zS zE1bdZ%cjo}S-9xq&U2?aJ1+lt?)-&oY)_0Qnm%w1hyIUycn%o@72JHFi(wp8yY_Zx z5zA<}LU-uuA{;_GijC8o@MKRo$@2o?L$M`3Trt8uRuf3ZJv@9L;&qYmLUM%*8#@J$ z5g2joD8pi6dx=OZQMz6a8WUXkL_)bF%bs5Bk;t(ZngCa~4XzD^@CbBCNX#q1BV=`F z&F1dC(x zJzc2d*7c3m6SFi%fUP@bi64pQrisw!;+h1A1k=8}N9bcuNLNCbHakstUd9#U!~S=( z&h9t6XdN=pkt*XM>FM_%^cy#*#5=x78ui}cgL%Fkb~`inF{Wxf0Z`YK!y`Hn$G|Bd zOjW5*SptL2;L9N+kd+LH2es_b&os({`Ha?yW#rBB42HCG^!2PSw(0Wlc|wucU0Lc8*8fF(`<-a ze4Vkd$zq#cY%&_$Pr*pG@|A8e)Lfur^lB%W_JysC&2ba+F>dko=3yEG+4biGZZsRe zq{5>B7c|mh##jp-7Aq_$?2PZc8ft4}`?tU|6&{@8apNb51t(zC#DiZl605a#YjIs2%&rKz@0fNS(2ajN z!j7>|Bsi8^af|n89sK45^!+-p*XZop7NFmN(KTCZwZTJ|uK;=fHis3{6M>GiL4rVA zF>PS!*_hU_^de%@SPxH9{!1|Fh}3EuJl-Oa1BavKDBVJ*X>2GTbrE32ky#j%MvSiCVh~KWh>|F4d_^@rp4K z=yJ1ZByMjk*XWJqRX{@z;j|kaRgFMHS)2zMdszd}IObBDrQTH*XQ|$%Dt7sxI9dQ;s|J?7P6kgxzP_c8LO}_rWZS!8gU!rI4^0{B>(O}Z(+>E zE==e<82dP!8`yJ}>X_0pprP*c0meMqRbRTp>L|fwVc!c@6B!#|&kbcv6aM&8L9P;Fjv^wfAhEOlAW{P?^?)Q)nw;fd@6)Hty z3G29F+-kVu)t#KH;O4rs_1g2k;~nIE!#m9D;C(j9ILqd?aK}S6r~}Gjv0?y6pE`4s zUGGSE#R##v3EZAY!p($|is7wk7`#8_LbhyeaP8g5`_{@+2joTFWhayhIl^wjB4Mh( zXT;!$EKi0QQx*>vH8xj`2Um9nHKz7XT()zZ5sK_qFjXVa|GKUbTNTEAE<;6y|P(fA(THJ)}`CLvT7KoZ=8k&^jWVg zWgRSB-a+X7`?XG6D;tlCZ#-CMN5Y8ef%>5SC>81>N7-l?DneywII2aBXgs=_KzLKA)e!PvPglXdKUi$d3j?g{-e^kP_8SW;MS*r+gL*o?46 zVXuU}A9g(KS5b^;ut+7EC~}G(5v>;O5Pc@P7#<#;99|SYA>0xENch_Dcf-F9zY-A} zkrq)Ep^LaTVoAiNh(i(QB88CyBa0*Nh@2j|F!J@tk0Z}U38RvuR8i`vd!m*^y%}{h z>QZ!Uv@E(dx+VJI=oh0uh&~-7h)Iqqi_yi*j#&}&Zp?{TI<|kTDpnhNU+l`*J+UX_ z_;D$5RdG||=Ec1nw?D2kJ|;dheoTCO{F3;$<2w@Qgye*(1XIGJ39ls_N%*Z>?{3B2 zm~IbsTi@+ax69pobua49bf4ROL-)hof9oOcQP!im$KyRV_xP$Ox98xVqk6XWe75JF zo@aZ-^%~Yo+iPyG*Lr=P$VnWWSf6-j;)=w5iI;l!>0Qy=+WYC=@Af|1C!tSKpDBG7 z^m(VxnZB`o3;G)SF6g_x@Au+(ak1DWUL@Wv?(CP?ud<)B-?D!D`&~^+NgACrD``X0 zSN%i#EBfpEFX+Fkf9HU{18N4`HDKL<&j$(zDh4(Ud}`p{ftQn0lE)@Lki0qhbV~P> zs+8#|>r%c<4NomhotnBl_0vK8L5e}mgPs|5U@&KJ*5IbWO9p>Dgfk?2h;hg>Lk60Y3F(ig?@j+hnk6+!S4zK-#mI)s zX34h7F31PVweltMPcy56+N!lxox^j6 z-#vWy2+@d!5sOEBQ$4uaR=u@`Q!~8giJC7*4jgG7xw#hAR@FXH`&C^^-PF3bM)610 zjaoA5yLwsuUG;k#VjCtkyx8zdV@czqjbDsT9qk;wYfR*riDTA|xjeRX?EJAE07 zJ?^9NJ;xizZR*%hi`ORhp%mPOVD&l=cT*k#2$R43p2yXHMz!_4D1O zn-(^mHL8q@jpwISPI+$1<>uPv)y-X|v8D|zAuW?z-ZaOUo6Wl|eJt&kgVrI|Io6J; z%BfFI?X-=st+G@0+wEH%aj;ydG(7L4cO52#W*W07p&F%Z94W9PUv{Tc| zr@wF~_fGAdyYA|D*UY<)-(7t7vU{j|H23VfxBtDf?>#xAV#b=8p);Fj{&SXe*8EvN z-#7NYt+RX2o-zBx{T27Gn-ej|Hs|OA!yb6GMxKQS-#61>yyB7ySI> zq$fXEn7Qz|r$V1{KGm_PdeN4r2R!}Q)7KU^Ek3-YWXXo5iAx_^difdMGl!lndUpMD zy`P)++;7htpFg&&V%etU$;%h6;IC+1@!iTXEBC&jcwz0To~!1q`hB%|b;p|eHM`g5 ztX=zJ;){>2BiWNb2m|&rfurn+_br4%lIt^-yHttuC2qiZhR~4t<~H5ZCmno{M(Pd6Z+2V zce=Ju-+pO_b;sGAhMgyNP1^OvyW`&dboZ#;`}d64^Zt9~@4dUXc<=W2^WWe0f%1bl zKU93U`J?QQHtoyWxAC7@|J=Agd;g}7b3Wd3Aosx5gLwzvIaGLP=O?9~y!UCU(PKY5D1GXUfkU`2M!H-0MmXcR{RxH3V7@H@sX|?Lg=iIFn~UY~b-yy|D;!GBP|TNbq6TXUGXb ziEOYmxzg2j5xz4Dwj%Vqt*h(mGhJQ3uY`An&k))%m3_a&)_i!cc)kczhr8=$;JfcZ z-rwPk0S>(25fUEW6rjSHaB5Ck7{}YJfk7A_#|cGr*4O z`U1^HqL7dfVTed56h()HhDFCmibRp|-D6|pV`IBVi^$=+v5fzf3J(hlj|h*7h=_`d zh=_>8mxwslCHhtZbnQUVAy5g5rjigRnxdnruCJgHRM&2*5VGXK;tLK)7#}pEI9wiI z5F!i>1E1@a@O7DXE29v_rDzV9!xQjBc(f=3l+iS|UyO`bJW3s#Gv#KrQI z1uO4=GuT;wurp(T=D2Ovd{sld)}DEOARi|nNY2`I2$ERQ zO;cHm-vOtcP*yPO_7@iT1wjSAawJ7El!F7aUzn0`NtC>%1`P%x1PQ1Z@(&p@ z9;u$>UuU>^HB>JTJ%vhj*>J#DMn1vfI5tX7XgnGgJYXdK9s90<^HUW6E8O7QyX$`Q zAJ_Ex?Jy*l%B4zarcy2yyMO5ty4dp+TQ>e5!1EkW8PxN%mxhAy!KYw(4tENFm-g0$ zO7hl)8thva4&{zP$vNKGBp6)qHwj!H*PR}Udwka)ZnaAbG01;2aLHsykSidCXxz4D z_yx@w05pWkg9|=V+5dlFufV5>{ZEj%T0M<{35>L$$!RtM3Eykr`$P%p7`SV+Kx0?K zH-l-=Wo)=y(=_sR$umc3O&SyUaB+pO-e}ghV;)YzH=CO+zP!n9h56g7H)%~c1zZtw z;28F%79B7IAIxi=cJlrSGCUW;JWB8bFZ>g`_$N(W5+VF{8e?lTwlMgAfW=2w-%}qq zZa2w8-)_Sl9Q*#+fvfNrsKXDR>RqVgOz?5bs0;Cb4_uHFBw57as}d5z`>O^1zvF|o zt5v*clmDmupGSNA+P^>>cTR#ET>J@)N}bw4+8Wd4HTGsOgQp-F3D^yuyW0!5@Axmr CS@aM9 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-divide.psd b/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-divide.psd new file mode 100644 index 0000000000000000000000000000000000000000..3ed772072769a8ad8186242d39f8a15cf35bde44 GIT binary patch literal 23514 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 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-exclusion.psd b/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-exclusion.psd new file mode 100644 index 0000000000000000000000000000000000000000..33d88af1b75515565da1fb5958bf29c780347e2e GIT binary patch literal 23052 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 C*Y-XD literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-hard-light.psd b/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-hard-light.psd new file mode 100644 index 0000000000000000000000000000000000000000..b443a4f007bcd98217416edf7af354256dcf9158 GIT binary patch literal 23052 zcmeHP2Y6Fe-#<6AY12Je1wspL8EKQWY17c1PH1Uq%M^u-n=~{{LPlF~Fho@FRY61q z91Izv;9`g@AZAY?|Yuyr}y6fKL7JOsJ+ExwkzB7lhitkhE@WN#TiK=V&Z7dPb$U=(b$?g zk;-DDMe_7)u~wR?5ar~iOXax=S=JDdOd`$BkmP1av&0gqQj)8bNJXwAsUQ@nJey9h ztS>Eh6$e~?Qj^1BRc2(gwY8NJf;@+-UM*HnL1ue8sbjj<(vs1xwq{7vB^h^e(rG&mzPRU!FEDnpk$?^yF_NbZKlg42%%Y*clDjThKSZs|Ji>ZKV_d0hxkxGTz zSs_ZRQEQE6T*$z@41XtY8+hZSxZ-y7Jwk#Xw)g?dbw6h)0yH- ztyU|}(PhcRYKcZJ*2?wSYK=^;(aN$3NHsOS?ze?5wP>BVheBv(FzYN@2=B>DsZpr) z3R$jHtjNsNiKS9nE!Ie(8FXryE;}bvp-@P3-39psyRD#Fn-S(3waLc@*F^_~DHJ(b z3W;1MmSt%{t;98Io9pRLp6>fJ@$ART}NkAE)~CQ|PT^Hff-z z+$wb|Ohz&*Sk*Q=jR#GBl6%y6rV-C9fqlr}Q>wMNISaIS0MNR;j9aAMv^<_JH{D;`_M&aT#q*v^mi5VL}nBKMrEI=pAip8(nOG9`fhLHWT+&NB;uyGVly+GX4je z!me)p_ZmW%ovn~bva`grBugXCQs?T$YPmd9oSTy)&&|@xGZktWSO4RN$Z3@3D5aUo zEO_WK^Vt7pLwI|#wn=R^(7J*Qcir5A|4(iA=jO!!e%t-w)8j8|yBty47^7KdX|u!Q zk%=032>oR($Be~W=SBBid;2_^=Y1}yEGCPs#-gJOGV?O-B=+_kAFnEvs?KK78%=aU zsnKpVsi*on`)FO>OnN?xH!^21QdkeEhUQxcmSK%m(2KjE|o-2$vrqf!QEjf${Ov2I2ApBrqFV0`?vLAd+?3CsrJ3XG4RHVBs=Ac5I{aD{p|deUat-Ps1)ILSUxCD|i- z4X#meGW$%!tTx!7SL<|GoemIku}PiX(V&5CeZ->5?4W70vjr2g2aW_TF*(ex9EBRE(d00iNg<#Y5NlP-q0o5W`heF1~*h+QKEuu$Tr}5ok*m!I5kC<4xAx5Ds1N4WF@BC?MiIA z#!+TRd2+o8x3=dfG3i8ilGoc!4kE8>Hx+3-=Ao^$7Pi{K{z%wQ>*^sQsjb?)zz6h+Shnz|* z4u_@1WHB3DE<(2kr{E(GcgyRIh9+M{m|Fq4dF=7%VgJbFfS!aq1sktjcQUe4h%Ula zDBKEXF#EFE7l|xf_(|u*vz;B+e!O_;@=c~E#uH5+xQ0Xj$2~lUjDZSHKG4N54ys*y zyXC|(8m`bCy1EF5kd9*F^d>yn6HfBHO!!c2i4RwdaF5k^l5r0Y--mczA-sTG;ljjD z#bX3U95c$WnAlz=(n^$}*Mr6cS00g2F3Iv|mv|&H?1jd|)op`oLm@l@T@n)WI`9Zt z-B~lad#~(sSncI~seeaDu)v}5R(eu94&G9akOxnWgzzki(c-*u=6GO+5bqEo!AOCO zRm7Yw)N$+j#_EY#Hb#J{J7$R=iRY$?(C6Zs1c(IFzPv~1V^2s|LYOu?O?Y0$730DF zceBp!H@j#ZQqYkq<00wk_aO8eH>kurzDOGN-r<9Jo*i~O)ArG(YCHi@x0J&rIuOUe z$tO%zsZUvagU#Z!-Xh{z$o66P++NmzqceMfAx5~`>9ADLX4;1L;er6+J-jaCP{tTb zu%@cTAcEU}y8dkV5rA7Bc?A4hB{g=#t&c^X+T>_d8*Z_R(9$MTBi-((vR5|NRJ*6y z5V!aiV?mR}Hm%rXG`OFFkxb<)-D0RYUq|cJPBQHaT4|f(4(6lX;#!v!HBoUFo5 zLNl{qB&H*m;S+DUXF>OGfoCc_I7MT}jTiAx!>EY|zZ4`=YwgzJx;mI$5pv%#?K+?v z|9qSoW1os~EVtrM-lKK!n-kFY>%d;4vuj&`ehWs|Y^~J>4_%%D1c*)kl{(B9Mqvt~ps&@g*Y*NtSjW11D(W{vcP<=86)aAs_ZVdRuwD%ih*P)>Ak)_9IQkD(s8tC61;>+{QT0D_S+lzdO)d zX>+j)6ZSo{eJsum>{-inbZHsTPE&ur*>i^hF-jVQ%5n^)^I6aY=lLaLe!&}o(cz?=;Y?<2N+PjhWt(B(^$cwtmP9PDm1lw| zV((yYWglR_i)mN+<)jr2p3>?`8#hDC7(F)b`l>;K%f$V_E?F-*FS*G?aln%n`Ylpf ztW(LeM^p?mKP{>cC-t52h@pv zMmH#m;#1*NEY*YROAVj~Q!+|H6;NeVHC0cIqb5-X%1k+_`=|%0dDH@G3ALPBN4-wH zP3@vSqz+S`QKzW$)Me^d7Mmqx#j$#`Qdk)*1*@1foYly>n`K~4VNGMrW~X zAI#6^*YeeT2Y(L#S^g{h9sDEwGyLlzVIh4(WFh4t<3r3LvqBbyyd3gg$dQn?M^r`VBBn#6gMmiT5V1OgxZywRfN16}_#!pY6S;_k}(Q zeTw=_?z6DZdwtILjqO{|*VuPq-yMCw7sZQ;MJCZ=(Oyw!zr=o({ha-l_dD3{MpA0h zsH9m*8ysBI?@sRQ-?x8F|NHu{@BjG#!2rd8rUB0k*gxP}N@~iOl!sHc zq?}9bo?4YUEp>hBmucZ?g=tgLR-_#r$Q!5_*gWvLfrkdM2FV9C4O%+rlfkUP*@KOP zpBsF52zN;C5c7}~Lr#c=;$m^Dc)j>kdO~`2`mFRV=@&ElXN=F7pRqsVcZpnLlB|+^ zA&rp^lg^TElU|k$l4)g2Wk)l^Gs`olXKu~Blr=bu&U!xUq&!w$BY#-FTmD;iPPQ$3 zL-zTc{yCF!mgSsK#3||&k10OL<>r>;PS4$*`-?JL=}^9*{O3^d(B`2p4?UNcnn&lY z$~&E(l&{WTp5IX*DwtI8VnIh?ze084io$P-l8bajtBcMS4=kQs{7Uh~5=n`zWK+pC zl~Q%TYG*0Cw4!uw>A|v?vQcGA%Dyg7DmRp`FYm07SKM2%vyxjmymEf!iK^aJ+NyO` zox^g5-9K#aaN+QV;Y)^pQ$48KR=ur;RWq#SshTfG3>aY^v85K(R@FXL`&C_P-ITg_ zM)F41ja)kNyLxH;ef9erVjCtlyxj0}V@cx^jbDsP8|56edvxUJ38UAIzBZ8CyKlbBsJ;xcxZ5_`aKYIM?@mKFEyX)z@&PwVblQg7R@*82-S(}HI9RYBaHcsQa-M1}XkFTRy=`>co9)r<=Jo?q2Tgr+ z>e*@K(_XrlbFcQ^-S_poZ{~fc?k~Q7`2*Agng@1IPo6$|`k5IOGuF-wo!LC|@3SPc z7R>tT!7&eRo85c%jM=9js(5JqoQOHLIVT<-`tS>na2_!}a`4g2N1uK4_qn>cAI+1@ zTQu+HW7@|)`kVA`i~rX3xc>2jPh>yw{FA&V%}*YmUpRm5g6IX)7My;n=BdpKMGNOH z{ORe5Pk*>5YtakOgg)bZreksS;;qm2fA+~|Z!T$Ca%^eI(v8a!mp!`d+H<<+jyzxV z{Dv2LzcBBGUtcu7cyf8g^35w!RxDb{TiLquyH%rC?SDz}(z?|>SI=Ah+Zywlj+lFr2^iKLaYqs~>zVzMrcb|AK z^u5{db?un8*qJVnDHg|%eh}geYNQ8#IINUW8gnFc4T+F zd#d!*fo~eV`Qo(p^bg3v*@1(I>&aNxn#T4b@{O?J+7?zDeI@*SL?2R{d3FDH?GaS-sAe(UlhOW z|8>l-XK%FK)~W|hc^txrDNJxHSaH6UP_=_lla7Z@; z?5M6U&}<|O2?-H|2n7ORbXaIubbO>x7#ZI^Ha0#swtKXY9IhL~_+P2;u(0rm@TiE0 zsJMuTh&X(Sh+|x$|4e|cohUj4DnYTSB*coQ*wIwiS5OJ6YcEv@S#n|V1qURI4>qD$ z94?O^A_xrwpIepib(!r}Mj?tru~{4zm(L5~vW1zTjAnEC#YnluBh|4Agq4{pqDm`;MKtI$Ga4XVL1-?;ro}=bVZ$hPH>FS+iyTiL=+D5sL-c za!5{m9#>9M=qHWgKn7EiV!5)J^W$&^yXp^jX7<;dvdvncYKYg`vn~zb;RN_8^4&)u ziIv@SWuvkk^f#Uq{y2kFZwg(XBO#lV8I20j2UXUA>GO|H@A{1NpMC_+yWvW(jYP%d zMM47$d=d2TCRpq{;ItFU3TEB@g#~^=P=T)uNstuf;K0lmrX*YvC9A1HgMbJ@d@6?g zLxzk;swer^8E#$;)yqRqrqWzCEbx_*Pp~+Sjgk=>kA{U08%cl1zH8w;isFBP8+?0r z-S7P4nm)fBhGY_%L@CKq$|NH9FI_?xdy!(w#{UC&k>x3adXep=p&)$lC|I7uox+3Iymg@l`PPL+xnoc=mNzyD1{eHI0@ufNXAi+WzUz0l+NFgUDn8WZ?%aD}kmXx6u59!|nHo0~1Zyvc5b`P-{EX-zl< zToH2M81|+X9WVqR%xj%?^8N`jJQu<|O7H_O{1dzQCrw=v0sM9vZEG~P(D;9V#V6K0 zQ2csU63Ii~Zo?fM`~J~^tMD&SZ>lyrT&Uwr@bRZn7vTRMxF9D;GKj-hB_x3NR}1`q z#|LXyt9a2S|4;e9kM^E_fi~`(1UI<&6Bv~`wS%-Zrps&W&0q#kK{67s8$5TX7w*{k EU#(X4Q~&?~ literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-lighten.psd b/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-lighten.psd new file mode 100644 index 0000000000000000000000000000000000000000..a075b738f922d88e7ddde18a37f857a4710cf90d GIT binary patch literal 23052 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 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-linear-dodge.psd b/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-linear-dodge.psd new file mode 100644 index 0000000000000000000000000000000000000000..38ade80ef87b47e2f1a6a37fac187adbd076febb GIT binary patch literal 23052 zcmeHP33wCLzCV+-Y14gSD-c>}%aS%pw={H5SK89jmQ@rc$utd3laQq?hzf|P;B`Sn z1YB5T5d}9E5oHm?g^Pf)xZWZnpvWyCf`az_&zWol%;ml7{oZ@um-#v~|NZ>W?<{j> z`kg3?>lzS+*nuX}4I;9LGZ!XaO54?Q`HH*s zuk6(w*+z5&u`cI&Zb65I*0wNqiH6Y|OnC|ae0xWN#GuPd7@JumtFfq=W<$kvE7LH2 zWTSR^i#A7>P>>&-+o9@cwX`yJTGG*KG}%-gc?q=6tYK6@V{ux7gqYY{@)C-$LNczV zPNFtj8A)bpmQ*WGS4y&TQstRBN=3#ni9#mNN|WWJ$up!fxk{F!lF22`BOyN+s9dYA zNmXA`<}41lyo6@E-J(iMYj1B)ZBI`%TlH!3oSd9AnIcW0kb;HOHqB(GJESICA{)dN zr-ZR-tp>JpejngblT0LVk zJFHr!v<)&zyjAj+X0zRFYc~H$z1?cI_M~xG%yJ=pmD{4nnNnG%N+$CP!dm*o zsDb{dqwTaaib5upWk?mWMukF^u29J`hsx63QEssHjbqjsnx@@W6py7>44qchWVW`_ z_I%o6F&eZq?tp1+CY`fexuMNX^O$=1lkTCaG}-L5Ny}80_8CqSoT$d%4 zWo0o^U3!L6N@rzfNi!IkEIW&)vojb40jZ|O+x@1{C1$My_fQDU4rZNM3*p^)sd8jl znhY(Jp-fNLN#$~emTKhC3_4n&%gRnyDwXmaS3zFEZYrqOYJjQWdDWLB`yRvUu{OZ$lz1aTHKuZT08(4U2fV9QcqfLPp6yV=H|RT z2-x^u?tflJdoyG5H+7j?*-q>d2-stFCKyDhIfsKa$KvUT0 zwtuf7bXi$Sg)A#W%E&S_(hNGMNlIsCrb~0Ovomutw3+EjI?L72|AU6eZj@)M{+u-qd zJ45I%YdLl--Z(G1=GvR*(Ol1SL2WjgtuMP@e|&tj0l0hs@y`a}@{f;?HUO6oApY3^T>kO# z(FWl10mMHWfXhEVKH30WK7jaV1917r$4480%LfquYyd9*`1oi8aQOh@pAEp}A0HoW z04^Uu{IdbL{Nv-J4Z!6Ch<`Q!mw$YGv;nw$0P)WT;PQ`;k2V084

809^j@@zDn0 z@&Uv@8-U9{K0ew2Tt0yKX9IBg$Hzw-fXfFE|7-v*|M>W51915O;-3w`V`-xcuYeqYc321Bib%0GEG!e6#_$d;sy!`h_dlv(b|=!S2p>*v3irfvU(J(JOF` zfRo*48e*}+2EAH`-Quu=kdIC3Z1x5XZ0jQyl_opGm>jKGfoXhK8?BfRC%z4a4s20u zuwx&rW@9qek#sB5SURo|L~LQK1MCDaH#IPJhox3CMGMMUUVom9R|sb=FOydcR27o& z%6JOGva-|&g{?>nmciZKaFz z>}84dM%>!&qu8jE+)7?=Gunx~uESWUahnIXFd8kWXZ_qb;D?(ff$jxn!M-TTWHV5=L+$q?2?Yxtbl|*!5&O+f< zFq7Sv%{@$Q3SZ>|{Jf zV8pSb42y~FWg@LWX-!R_F~U_qB$QLK{Fxb_Y78hcyH?8(Ge_gXuOr4)Q*R@)Fb4^lP4oQhhnujZ=5+Em_fulh)6I}AmbFW zrxSJDy56yRW0s2%VC#-q;z#1SXd?8wI41!j!L&E;7JAte(wPvZO%5ZTmvO}eu>akp zv-!*}TDu%{q{?_my8Ar{eZ~zc_KYu*M!jeFU|wK@-Oh|{jIkO|0MrfT@QDt@QE&m41@jhG-AiRgyX&lTNV+qz& zw(2Et`%l-O3qJyI!y}J?f1{+vroZvAD4>n@Mp}P^RhX7B8XK7od!?}yU&TM8}+iQ(iFbP<}J z1;a5Nz6_su%RL9We=9sw;lU{xH-3UdcoIfUEchiM39YqRit6fMc16f_$F%c+Zv4wp zc8q;0#j#w9+q_5W;5R3r@7IC7MrYHu0{te8t~pwZ6&|_*CCC%o?G{W=1UkkF2?A}w zw4SBsU|Pe{i-}ERJv>PTFTa8-T_!ms!pA&aybmnyl&~r|$;Z3KVO< z$I&tx%W?eU%}k{${?F5!8^?kia^?oN8ODqR8de6}I9p2t&IfYf4xHYs#&Tj~b=2eh z2ZCJ1n97TRhJ3hpHCfB*o%YsNvYx`ZaUW_nRAOIDFR?c_;x@){Ue?ki|DHf^WlTj* zOt^P3wsAN&u;(n(F(suyL*2Ot8PjNIeYu?$doeBx`(Csd$=CpUUNB=U$MOWA<85sX zPG4TW-P(xdkSA}X#a@RognIEbQ|J-6-$OpU4pfcQs02xn15OPxqbbO6;M|sTz_FS# zL?bKGBL-Fjti<~G z-;wZ&5n}V=c)gL7mjNXe!CTXCcz?=)Y}wl2+PjeVu9dqE$b-7mP9zg?L_I`>q7+}x zupwcY?hG-eEFLUsY_1wNuC5GjF5jauARXjc<;>M>$$g0X26rEK7xzQ%cJ3A=;qKsW z8@E8q7~D3l`qCi5W#WEdldYGXm0f3}*x^YF{T8XsmTBbK zBPoKJpOI9Ww5jAbi*gXc&k<~c(4{!?e4ye!^2la?jZofrSeI`1$f{wSzIg@~(C0j| zI8L`x=8)T;c~>B7*1bK5+{wL

qLIE|b;IeN}i&UDTk&ZC^goE4mxIh#4V zI3IJ4a5_0ZaxQZbH-sC<9l(`xm0UHqmOFvl#IFvJ;(i( zC*Vc%Bs?kf;0oSoo|ZS2cQ5Y|-V)wg-e%q&-lx11ye{4!{7`;8e<)wYujG&8>-p{c zx%@@^)%;ETy)c?i@qZBr1aX37K{kxW@q#IWy9JL3o)v5q>=YaloD^IZ3WdFdLxp+5 zS|Kg83+D=-5xy+kAv`QRCA=CG64XCP5mXj5A;=UoJ7`hROF{1j9S%AjbWIc?8YEJQ zYDGHHG|^+CRibU8Pei9g*McL1lY{eu8-u3=&k9}~ydijR@K?c?LPA0kLUKYzg&0C+ zhAa+wHROYk6CuBfqr^kRYVky|L;R?Cjd-W{bMd9n(9opN!q5q!_RvQ|*M+_p`c3H7 zu;{SVu*xu9*nMG3!#0N<4m%$%3Lg|+6n$SQ_zG z#IcCWk67C!@L1iP7q4ZS?)otD^Tt zpNbL0B*#?7Oo^Ev^GeKtn6B8U*o@dQu^q8XW8aDGjN`^7#Z|@`;~tB9J??1SZ$0|< zDC)uVc(})g9*28e>Di}eVNa&#yq+6-9_jg8FG;V`UM;9?@oyZz4gkM5t}-_U&O3T0(Nd=!Dq` z8xy`x3{F%gHYF}h+@07pu>ZiCfiniKANa)}(IDlZ=0Q&n+BfJ*QgYJRqz99>B%Mj_ znOvDXJ$ZfdS1F+>1u0WgR-}A3STI;QxMlFOgAWej49OhQJY?yRPlj@aW(_qAeRk-f zVfERJHu*)x5QSE;RPkAQXnI-ted$}%FJuhOU^1S|IG!1uS(Eu-=I+ej;n$n2SsSv> zW)IAsoV_gjm@-CLr+h^DK@LBsIOo2c?K!`yvQ&1}>#Bbamkw_k{?hO>xyiXq?yB6A zc?o%R-txT8d`bS~{1@^&3kDR>1uF`^Ele!b6|OEkT{O68O3}+j=Zj^<*5XaYSJW!? zJ?fn$+>-K=c_jx*qe@4YE-C${ETK$aw!W;ZJhS}n@|_j@imHkyDvnk5t<+YotLz$) zJ>s4bd#c1$4OL63zO5coZLQu`!>Jij^JL9eBL|H%joea;YAb7>to^z!xo&FR+oJ@d z>P9Uc^?kj(en$PihUkV#4KFqP(pcR1SmT$YQ${;R?;aCAX5yH2W3G%X8M|O?=eV?S z_l)~^eDCpw@mnVdCybe}dcx&9O7D2;j#CpeC(fOC^v>ivJMR4OuHJVU?|Nra_#}GL z8szKMQ;TVjxu3bie8@7?GS|{M zRWuMWiyVJJS9s>*Z{f-pJ1CA4I`E5(vuC|Y9f1@L^!_={V+K_1vPdh!m zZ2F6L^X}H(y?e%h8M9`bxTolz<@ZweYVO^AU*dgp?mIQJeCFC&!LwRs{e8A<_JY|z z-#_;LZFBn0nK|d=1LY5_pBpyUI``Ov!ykP9A>KoVhYmcP{_ryo|1nQD@1yy$`HSXX ze?faans{RpD1`@?SjY!(-)k4vgXOn3ndHZE&TbZ zNl$&aC}Yv{PX|Bkc)D|O_2RA141DJCXRa@4UUFn<@zRaU;+H+V?8>vcXAeJD_}qr) z`#wMa`QKhJyl{MZ`SQ&xl2$BQDOlOI^7~a|R_%LH`Qp0Oy;sj){reizn$ETLYxk_n zUbpV0_?I4A&spzSfA-~xFCToRRrIRk)gNA){Mymit6tyzhVqRK zZw`8M*{0}C^EOkPr)}=q(!8Z}>-epQ-Wu`N?rp=jZF)QP?KRs6Y+w3L>^qOW8~pB^ zce{5?-*I`TW#_qF`due?Pul(Ed*j~wY|p4Y2liI&{owtw_ut!Bv~R};c^_>5Q1#(k zA1OcD@^RM3oA+n#-}Lv)zi&E_bzt)+*`I7Zm~(L3q1;379xgb%>(i1?-~X)gvyYFA zJaXvh=%dGvO+40lTzCA;=f=-3e9`vBwJ&FW#s6yF*AZVY`X>IH75^CgkByyKo$s6| zIkErShHt+-sXh7Qced}YpPKW1==Y0G_dmVvjN;7pvt?%w{&2?+r+&2jc>nq z5?f(ua<#kr5`1SAZbRsIYj^jxXS=(9Uj^?9pChz$D*Jwktp)I2@j@Y{j`Y;c#CPw5 zyuZU60~~n4BO*M!DL{oY;Nq$QzGFR{oSyJTLUkYS{t1c6PYg~{*8qPWg%J+vW`Z5n z{Uw@%#6dwpq9CzIB#sOT4vCBn7mLGVdqzjcMo0II6qCbwV;SEo6&ex}8WtK678Vf` z78VwRFJUpPOXTeY=-!DUgP;-=mr6jKNQxUtb$<<&pt|=^1&}2l7GH2c!ua4Kio@dz zgh8U<5b(KC3164FE@cFwcodhzdo&T{r;Ei^0E5%2cKTEW#6&WS0WLI1KILO zPC@}clcX>}9>s$UrY1!56|dfb*J#>867tUxuncURDf13xMW{+?Px*w=cNA(AaLFZSAuOgDk3iu z8d%^+vrv@C$+(d=*HB#c`~Zg3x$0EPT*F`aAYr3+HDj_E)&Uw|D3L z)<3T4_1j@cAyddyvJ90%CUO1JC3LYDD7I|;KY$lF?lPzsxE>k`!iRu@1K%f#NyorlqZJyv z8on7!gDzvmpCH3?AKTVMnfxu{|8ufY|XvW zo-;<1JeoSJxPxQgKRa+0{srnrolfsW9cO}%+eTf4|9jwsoFK^}4qug!2;N`K@c$hz ztevgmL7V(P<^Mj~SN;XsxN{QR;NnYQRO)CuX=_ZE)!16V44#5yBw#mq?(QhqvGczG D0UPv; literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-multiply.psd b/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-multiply.psd new file mode 100644 index 0000000000000000000000000000000000000000..117c6b4c391e5e7b4b62b0285554aa6053629f9d GIT binary patch literal 23052 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 B_$dGY literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-normal.psd b/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-normal.psd new file mode 100644 index 0000000000000000000000000000000000000000..1e03aed33e44f516ca30b255be8a88e6dc2b26b0 GIT binary patch literal 23052 zcmeHP2Y6Fe-#<6AY12Je1wspL8EKPrrJ*~Ww56pjQxr08($F*s8Erw7A)XUj3_k zwMVxRU4N|0xshAYZc%BPX}d^G>kX#7grDEpksvZ?^Abj9)<|kBO1jBVG1W>pOdZ~+ zncA$$(Iyn+2j{jc+FL9wv|S}?Z!wx|iuSw&mDa4L6+mNgT7rm}*qiebim*a7rlwA$ zG+Sv=W@?sLBTbi!vU5_UnK^P<#t@NABF##Z^AU|-qsHwt*LRl-nO`wmfL#T?4_+x11BQ4^|UokvCwJtw9V|W zYUt8d$RzQ0$y=JtcC)R?{1^3htC`x9#$hnah4d9lE3LAdt&L{0F`sGoI#)cAQiyubyUH0wCJ{?A#4<^vOs0@Y6&cxsCF$-cH`)5eF>4LFDR&gbW9bz`t5N98))tjL zUuCfv4H^~hfN8BJt+QLXq0LS6n0on>?xCnO+3YHlhOR8h2SKXApjD_;aqq$dP8M zWolY2mdkWnu~bS!K~iW2txBfN%1)Qd<Qq(L!Nzd3J_e zk|`6*vNT!Z47DmltjeObVqJE&I!CR`(yDWGt|D#|=9Z!=3^wSGQ@r{q^j0#PG|-c8 zmbw&11DO>pDyxmggC;M*HEP_`h;@<4&TR?6ao`H>q|3FjN zRIUGBLuj+IHr&@n!RG(OZLVN)6&j#S~ zkB^Tw0GAIS{@DOr{_*kA2H^4m#6KH=%RfFo+5lWWfcR$vaQVl_M;n042N3^k051Rd z_-F%g`2gad4Z!6eA0KT1E+0VrvjMpLMP@e|&tj0l0hs@y`a}@{f;? zHUO6oApY3^T>kO#(FWl10mMHWfXhEVKH30WK7jaV1917r$4480%LfquYyd9*`1oi8 zaQOh@pAEp}A0HoW04^Uu{IdbL{Nv-J4Z!6Ch<`Q!mw$YGv;nw$0P)WT;PQ`;k2V08 z4

809^j@@zDn0@&Uv@8-U9{K0ew2Tt0yKXZ^wz?Aho^n_zcm8*Jkw`#=?BkLXpn zM!?DJGYzp=VS`?+!)|fdLCD1>bvAp08n*Qji%OH7rcI6(tiUwBD~(pnhZElhLp!!8 zHrTNbRx>ea>u^;I-B>!N5kyR3j05ZhFzXsX~ET=C=!YPC^my^jU2C52) zIAt6e;qo~JAj#)QL5h!Hi`ySK61doCH#u_@svQQS-C!bxfL=hXl{I6ln0>NCaR`SpTowi-91w3?J2#$@*})oP>7tr@Pjx4N0q z7GsH<0c1U)i#5%9Co~LhsJ^^d3E7Zs!1Y>@NNaYe3(XxkLvoZ`O}EI3jknqrTeXcN zP4=?HdLwRa_fc%rif$*bw;An3Ue|6cRJ+ZCTWJk!wS)bUu%Fi1PwMn_HdjBvG&Y7i zP8(IzIwp)yv5jWSZHsNt7#Vq;RZ}OCIWy*BkUr-ii>H0&;WP=|Dqje5`U!933fyPav_paoBrm&hES%qn6qp^{0w^!OK8f&Uu(`=AS ze3P-D$!wikWHjhqPr-1e@)a&I)SR!Sbt(s$_64o9)qWfEQ7-Y#<{@f5+4biIZX^@G zxZJG(7c|^#!dMIJW(zDRY_#{h8f=6K_!g;9O(yJHedNHwStcNEl?-hiCgesL49&ce=K`v_5=pjzn37{n=t-%EK$MaD>?Bj(2 z5nciGkd_uRroj%SsJ-Ioy=aw6?T)EZ>D@F_7x09FojW%P^LJiaj1Ity97%8$Hd!m` z;T{9HooF^!VH(<-I&V@BuLT-r59*qM40lYkf}0!_BN=;EyvC-)&zd-pOSGyIykd+8 zy3Ax8j@ui{)jC62CD71ASnURTWh2l~7VBZ!R@wkGj=9Wgu6LHjTBfrqi=4h2R92uE z`#p~4kywu7pJ<{hUGaaJ+SE82%V8_{-8*o051H1p!CMA{=8>^!p z=ieXX3ffd&3^e4!zDH**t9RO4TgZ9}=f-}x$xw-XF}=jz)QH;{$9YAgBKda*dMj-z za$>^1hqjHuxq&@vnU*do1sdwkK1iEJI_t~su-J=nS=jfI#Yn~m*mHtuV>y;503B~@ zZE*T>^6l0}EQdTf!!7nYj3LyEqn@N1hWkC_!)Zs=NQp|22sz+XBQu(W3wr9{JM9D#0ZY(LP$)?8 z^$Z&nmg&wAW6I#cpvL5?cH`>G;MVdz8UxZoo>k6V-InZ!*>AG(9OcXmjX`$aDrP(rt zJbOe%F!R%*N|Pp){AN)OLijm?Z4kN~N1hK<+{Yf-tgjKu8w=~w?H*Y*jMKNy!UFod zN0z)E7B250^x=aVhqaZ7$H6x)th2*mMD;*@P$Ei!`p8fg8j1>0DH?`qQ6n0Q?txy{ z1pV2H+R!vK6U{;M;63DNv;@6?R-(1&RrChhg0`bw=mYc#I*5*qpiVHe!dcJw86+WNegFl#` z$FJq9_;&sr{nDUT|aZq~Mvsi-R`=?+yMc_;N@{NJ2 z?`}oi=x&d6+tBTBx2xTIbua8rcc0sRWA~%ofA1mcQQD)q$NU~!dVJlJ({oVI5k1>_ zKHqb1&-1-vdJXNR={2|48@;}WXT=YSuaCbser5dr_$$5p^e*ph>HTc)_j{l36W6D( z&!j#J`@GlZT;J%v`F#z27xvxJ_Xkm|s7PcKEf(z&b@q$zSJBVWZ+X81{jMh@CyY#( zm9R14>%`zhd7>_HVdCz@&i;M-*Yuy(e|`Ti1_%bo2Q&?MX28AySCf*HMkhU#v?b|m za`)uQdT4rC`tvBVUw1G5^KFx5_U;>$=s3yrBS6LOP7>>Qnb~k zWe>Y=*q$n3RYTR1s&A_YRa>jK)v#)Y)jU=6)$jqsO~bd;qT0&Zr)s~hORk$-_s$63 zh`JF=M|@u|t)EuEuOYf&Lc_}qzcv;(KGFE)$dr+ek-JBQj~YK}-KeXhOGYmk-7zL@ z%za}%8QXKLVeHm%{BfhktsZyfuF|`nzU%b(%<*%^AGfE>E{WcUGULe@cI@ zsi0|5(|Lo^u*7h2QpKbfCS7f=ZC=ydWgKnX*b>w-vE^-3l&RUY$K1!xm^yE%!m36g^vfXXlYL9^h`+i4?<3Y#C*8J9`t=HN{wY}LM*=}mzKV{IAN2Z*a zS~m5idpY-N?%h4D-?W+2PTp5^-}3vZ`_=dFo}M^;_Vm*;%4e*d89cLj=09dhW-XZY z%LAhy*fzWO>>0C9Jy`zW`Z-~9taFY(H1wet9_Bo3c=*60>5n}7$e(kyb3dLZnYU=( zjYl<)e*Bp9vBi&dJ+6EFz!OLgyC8DG)CH%Ws(EVjLeavx3x9cf z!qXou%2@QmGr`X|p6OUzy?E=h{hxjE*&9ommK#FKEL6G z-Y?91;rAB}FP>OlzI^kFq!o)+@>aI4{C?G_Rr_9&zqD?3&(-r*|FOojrekgW+CA&C z*R6Xw{^cjuv(`J-pL=EeD+gaKd3Eas@rLCa<2KHF4ZY@g?T6PVzJBbDsyBAODSvar zTLa!&wkdkk+|AVHDVsaDG;QhFI(F-!w}-vGd)v@$o8C!%XU+D0+n2r@`|cC(1;01@ zy{;WocU;+N*?E4Ke%Gnp6Lx?3{+RcV>>07=z}~98AAV5w!TbA)_U-sE@5AjMDL#7p zWBJEhKFRuI^Zv~JoBomck4*=%4s8B3`_rula}I7hlzZsC!v%+TeOB_>2S+N8d~$U7 z(L={Z9y@V-{PB(x+7o9#H-3Kci`Fl$e>vkT?pJfaj`(`fH}T)B_~*cXZtTeFc=u$< z$^G9peEa1o&8Z*1vwe5t^z83Lzh88w@0oRHWoNgaD?4}ahr50_{iEf_>*weE6#3J# z3n>@g`nmAu{hebvPhYfN?7H;mhyM`-6{=KT^|^WeSW#X?LS?XI1H@7@P_ ze}^{)IPijp`FaL#3Q*w;xVUP7?^q8ft2?}rP+cdwenLX>6N3|!HNam$VT4248DK|s zeTilxVNg(zAV??>2qQy+Ln33tg~IUI?$Ob)(b3%_h2(JF7{>QXg@%NLhJ{9ig+;`K zg@wi7OIQr!5_u;9x^|+-AgBbzrVhuijtV2rvfCUYmCogJjnQ<%Lcp$0dsYY$-?v?heZih#HOJ90(_WPeXPF*fb zUbyPPx1Qc}Q5?u%azZp$HgkRq&R|#lq0aRF>XX)43zQA98e7K20X&=lKPhwfVMtQl333Ib5F59x z34TFy1^^AAa^ZrH6z2aQ*vtPZWd0K*s#Z;*VFDv9XmpqiK*IML_&!lgItK0J{(*jtT&i+?U;v?@Xe-Xvo~+FSz!M5=#3g9P61bh z95{xpsYMG6!3XmihmE{{f(*}vFpm=azzhGxF8)bVr$hk1okm+54J|bOA7Ih(HTNyJ zls1jzp=-C|4vu~Q>cCaFbJTGr__$-#1^B-QPRI$84C3%r2?^l+)eQgN@xt2KDju|h x2onF-(U!>m1=_fC65Qb8OW+#TsqCb!FjzSsBpzVCT*pWb`_`~1)EjC=0Q zbE+(=sY4WE4<6hwO`#yfWkutcD~l?scvypwFrI@D{2?M6LSzwVFHX6Xw!8O=mA4yT z+^0Xfo#+N*o$A+|{8k&SYh;`fEn_rUaua@dV`qZIq|Z&5kXa?GvT2wGQ~69gQ#W%= zy>4csPNh%C&kN3JRkt?TniwZ7X>BrF9O~BG1X^#^GHRf)I4wa!Oq`9m358f8nOIdL z(OB(_Br`Qjs*|THCE2P}d8SIK$QU6}$mCgRGF6&9Ln@Q2Wh%8yE^!|TdBH&C*!2c= zZE>l)IN)*<8k|m>IxVfGr6si`J=JPArpZ+*RhmqZrcg-1Lh5LuG{;z%WROx0PN(pqU-nmkpOb~7iv&g)}y z+07(1y)KP0Gfj-e=>R|ZP5l7Usw%JRO^a)4x~ZqbS<(zOa3gY4Pe*;5jY+Fz99EZI z$CNZfCW$vo-qv7sS{)76Kd84?&DNeY4vSe1q_5W48QN*J*ITXTJht6yJnu4qg#?PT)6Qzextr1H#qnNppuRLe7^vP`v1)-4EY=@X+0 zdZV6p((Whl??aHyPUgQj{*sZZY&awZUp{qMdoP z&1N>~Xx#nMnk{;Fr}6@ulh$S0&7X7*b%n*@q%At8qBsu(sV0+NE!WBnTCGYYWn`J@ zQiDE&kL)j@b~UTUpEp=C0a za;Y*M%8<($TB@R!OFkl*;T3r7Tk+ zRb=V1q#0T|LrP~cdZ{5hTdUF&v8`J#g}zGWk~(HaPpL;? zHjz2OM%x_>9x}NJo-yN{M7(nZ_8~)0P3v%T=IL2{uR*}Z z@8r56reZbgedCqW+G1q%d1;=yc?AET+U~E-iU0jU^oLK6zpd?hJRbkb z5c=C%jvb3V=S9z4d*eKs)Ad}?Sj|>@l~vE=!SsJKaaYgj=2fB5)Yz>ClbOjYHaTo& zx~;o&H?7;7O|RS1jm#OW6c&cCCQb7$N^h7>yz}ngNpw}~?~LjbD^Q3JApY3^T>kO# z(FWl10mMHWfXhEVKH30WK7jaV1917r$4480%LfquYyd9*`1oi8aQOh@pAEp}A0HoW z04^Uu{IdbL{Nv-J4Z!6Ch<`Q!mw$YGv;nw$0P)WT;PQ`;k2V084

809^j@@zDn0 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 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-screen.psd b/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-screen.psd new file mode 100644 index 0000000000000000000000000000000000000000..fe3ac978bc4aded5733464657dbcd76735f3efc5 GIT binary patch literal 23052 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 G$A19;QuTiT literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-subtract.psd b/luda-editor/psd/tests/fixtures/blending/blue-red-1x1-subtract.psd new file mode 100644 index 0000000000000000000000000000000000000000..09b34ceaad1c195283658936b8264a8fc7cf7404 GIT binary patch literal 23506 zcmeHP2Y3|KzCW`yo8D;wmIRUjF4=6_W=Y>vNJvOR6@}T|+0Bw|%VVtCQ6OE{KRotHS!vpnrSjtPG^~h>7yF8 z)0?%qy2QeQ;Jh|fo7rY&9JI8}Y_ix@ZTX3`&Z=QlKx1)QqLi38n)4Hju|hh&rcSE1 zvWzr)m|Uin%N5d`++pe2nHkyg+oTG)LY^j9q@`!bhr^~yOKWXy9oCvL%*qt7q(1C#z-3 zS|F07n+0!cvO28xChINA?NM{3CzZotmIvXh)GR|gtZbvzYAWFBz0U1Vs#Zfir{_ph z#~Cd;YpZ>5UYc(bZw+|8F{QMFDS(u@GPy#Qp4BKp_^5%_ zsG}XU%ZoxTlV?N9#&m@$GhL-n%H)|IFWqc?{aAHIefyvC;7=vo!)PfAxZuZ})`08RVtm z@z-Sh4^)MnZu$2rLYJMbR6x&?F>+|>nRKpRMrUPZ$Z~UXvT`%ESs6+?+g-~4K}F;= zrst^AGgO(G?s4pYvm(4LS=&Tg42-TI&0RLP;Qv$I{kbvmU++Y>oIU=6x>LZi;9u`S ze_6$GU9tPH=pJfs97glJ(}LP+va&T+9aE5zmv%F;x8Zb2Ri#$fu~xmxbo?KTtL z-ZgO-tt&MbUYC^{88SF2tP5dDn&w%P-Y}YYhTXrC@D}T@^y=d)kckf<{@DOr{_*kA z2H^4m#6KH=%RfFo+5lWWfcR$vaQVl_M;n042N3^k051Rd_-F%g`2gad4Z!6eA0KT1 zE+0VrvjMpLMP@e|&tj0l0hs@y`a}@{f;?HUO6oApY3^T>kO#(FWl1 z0mMHWfXhEVKH30WK7jaV1917r$4480%LfquYyd9*`1oi8aQOh@pAEp}A0HoW04^Uu z{IdbL{Nv-J4Z!6Ch<`Q!mw$YGv;nw$0P)WT;PQ`;k2V084

809^j@@zDn0@&Uv@ z8-U9{K0ew2Tt0yKX9IBg$Hzw-fXfFE|EyoQg1x&u84GOfY=u3X+zwF{*&%uwu90wZ zyG%oDENtSbbvkTL2MC4Oq|WYW(7>iXVo_yrFpR}%#tKa1yV}HJK7yoeFt%Zf5~BmB z!D`MY!;YfOOk>&jMi6nCaS326fK}hXIGnax%~UNYLs1=Kkrn>*NQxQ|gB-|-Y}geF zwt1|9BW_Ae4vQs`Qau_YdCFmu@T3s_?Bv<0$*ac4p7jKE9yOg+=>@^-n&05r6#^UfY)f$uCqZwszw0M{@v#Hd>0FGYQCE8|#3kVJ+ zR9{h|hG@u^-+G-?sCNQz zc9Vn1>)K338jpE!3!{aNb+8)}_RhLmMV+C}?rs&B#>Q~RVWVnB&$;o*w$W<4X|@eo z6DO}@wM7%K2XaJeS*vXv>}RXPE!1c+xZw-Oih78t#OiQZ%_ghG;7Ste*5DAj$iv+7 zdZVGKt0Kg$fY?0tc*ivV2QCKmINT}tbKS{q>SUq|cV!B>lgO8nAb6CouR3qn!?G1!G}YybBiu2=Cx^83%L5Sb{ZGW`h)N z|LOAc;R68OCOiUucS()i(0x)A(I!VDZRlndu4PQ7MyAbCWv^_ksdf*$L2hw3V@Z>h zonCA*8r-vD1XuVe`~=W)i_U0)^5apf0Cw?0hlr>I`ZlxK zifOPzshTc+3|(lN)_8ow@lBfi(P3q(kxJ%{a$DD7%a#B zPc$)AZvQ_|Z)zL|a)_B9++rL%3TRj<@Z;^x4LBZ%fj@A1lN!s34ePAO@ec&Kim_Cb z01ff*@6@y9^)7qXOx9O8HvU6R#wwf^(@Pvpjku1npBJ?>iN7b%+Zao+3lsjGjD0+g z4eWW#bWCX(&`@^%A;vPsRbGCF%~68$!fAi9nMmINdqFT`s=)F@pcCvZ4X(6;0tefO z=N`H05ONV<594 zCvm43M>H0=HlXR~2Us03uvT)XD{-xvT0s80N6Mv@!4m{tQMHg1&FEX8&1fXdz6`{{ zt+ZsW{KukiKw^YBxO?teh}8rq*103{%8H>>)sQQeINg4w5pGLSnW$7$DJmBY7W5Vr z2!;!);aUWzLZB4nx}){l3%?Z}7Je-}D(nz`I@vgjbz8Xop&HZy`EXbk}{PlR~&%HhGG#>J}f;OdUx#{9h+y#BF5J-Ly0MZkZE{~CWke>eYq{to_DB<1hq zZ{r`}zlCX6{^g_=4W8WUp{6aU8EVGpv2mA|1_{m+w*$L;gZ!NQI_Jd!GcB}Rq_*1H z$=o9?hLN9KfiR2fxG)l(CwDU^Y-P)_P@>V9e-HJ@5SEvME|uTXDL zyQ%l7!_=qLN$MQ+6ZIR9&kNnJ?=m0pL-_Ih0el%>$yf7h`4jniKFhzCKbOCdzk+dTNn0D z*jHg!!(+mSg;#~^!tV=T8onj`NcfKtl88YO#SwQ#OpjO?@mj=35$7W%k;#$jNILS~ z$fc2QL>`a492FCl9#tD~W;Wm7aZi7WHI$&h5FW=h2?O^^*1~>($)riC$ZKec4;kdr0rm zy<2-f+k0Q{^L=9b4DX}uGq=yHeLhRzB@9WZPq-^#Wx|1k%YFOxt>|m(ySVQ=eb4uc z?^o1sYQF{j-tKp^{>J_b`tR)joit8bEHz0NN%u-G3`iJIIlwt!`GA81t|g`< zj!B%AxGC|=q~IiFl0Ioc(w?LX1N#rG8F=@=4Ff+LBpIX})HLYnLHh??Nlr-~m;7Mz z*5tD(JyWVurl)L3`64wewJ>#B>Wb7)28#wO2R9FXX7HgQydhabnuaVL^3hP<(Cnec zq0bCGe4FsL+}kX-t+?&DEL2u3Ymse`og5ZFta{k2VOxj&m^LtNV%igF`_ulAXUR?S zRr1f%qti#E&r08({*z*eLaSJ+_#`7NqdeojjBObgGlym}na^gP$co9T$$BtrPuB0* zIoWLX#_V%B19PV2EXz5rj8)btA5p%SE6gp)y)So1?ystBl|%Ka>L0^p!<&b%AAUA3 zC6CElm3JyXF`v#~p5IX*Etpd9d_hOyfI_-(Md8;)NkzJ%)kSBD2NzE*ezEw+5_t(* zvbp4nTBW{6y{nX8T2VT;^k7+Z*_g5=WnYyimK(}9lwYXGs<^9SSEaCWWaSf;$E*5Q zX{**%T^Nxw;+_$EM~04U7`bHR*VRL++3M{zyqXa;Pu6@fYS1XlsI9fAwyO5Y+Ar%; z>ZaAbIa)NjZuHX8-`1zs-(A1IA*Nw+!}^9_8cP}KNykJ!2!rP8z#z?3Hn) zy14TKm1ONVh%`{GiUVq`uX~^ zhJ3@5hI37YO$(dO8`Z`o#*0%cr#?6JN^@=Vn&wW^IMXI`ka>#v4NJ78*|OK#&)Q}^ zY#VBuW9yivn)cMR3+zaCwVkrxVc+J6g$4ToXR7l7=gF3WmZdFMTgSG())v)fX*Auj;k@gQXdn6DZ|Q$q^taAO^^YEWEc>x%9~V7tdHmQDg-@)VA2oma{8LZXJh^3o zbiv#OKR-43srMIVE`09k;HRBWcPy%2v~BUg#g8w(zNBf%(WNCzH!Vw8_VBVR&*+{x z@@&zw8=vd@+`Q+0d*1l`iRBf`x2#BBv2dkmWy{KMSB+h@{{`g>>sI$(J#Y2zYbRF`MUZp|-Sdxv;fqYsa<;+YY}m;*CAqhi~8f=CC){>=>|P>05Dc zJ@$6++q2*9+&O*c;4JKxKH zZ^!$p_uu$H`N7r?vp?K&AnU;9zi0h@^TF(cTRzJ9XxpLOL)#DM9e(>r;gQ`Rmwx>2 zCsm((cy!d!!^g%PJ8^u{@s1O^6K6j)eR}b;md~zzKI04F7jwUi{Bq$}316-FufhMd zsUy4Nt&^oE4}9J5_2;Lwr@sHj{>}B%v%d}dcHx=+XV#rnoZWG*{M@1MZvXD|_qOk^ zouBhV)DO#kO#Sioe-!=Wz=iP_PG4j%cK-CprCyiT{G9pop38NYzxu`e%e5xbL9=R9J8e~aV}e7^ubPLSZk zuFp^qf(m6}X>zr*^Ah~-DBh0H?`&u1wP!jze_sX93ZEjhYZ~`_iLFKOT=9GnrjGX1 z&A|7r2YG&nCk8n1f=5Dlcv64@XTrr@8hpokIC(wciG=Dr(fI=kC7&0ZtgZq6M-)yt zq?-YDROjbtHVO?23X%kcN+h9CA;BS0aS@@R5pg|ZV&YWz*&lB*3Vo{KgADRKmD867obh@y3G#!&TZKgsL z`^55BN|FY~)$i71WU{jktHp!nH(WS*QEShNU-9bw$)!&=>dN-4bY%B9GUjXMr`Kk` z^Re^PrSg;os~&j$slA_k^YaUv_Z>Zbd91!=&cfAO-aYp1FF6(C46P47y=LqF<7ci! zAs!E+6_A+3B4HK@VL*Dc03w){7$a27d?FS{u)F^7g^YoklkBYd>V`P2J@euq5e`6{ zoVDi&1hKM*u53)UgZbJM!mT4n@rKa(84Bf-Jflz{`lzb?$cPV*?d<%Nw4VV4PW-%0 zu#G^)2Dmel=$OK`w{p5pSx?+%{ldGes1=X+_e!-t51g*n_Q{JXWc zEL5VmEYy&$W#Lh7AC!XU^-Y|?1^*g>%j3EWuH&}e`G;HW(t;22UIXV$Is>@^QizWc zx4@S(X8_RPDi1FBNafyy;8gz4Q0|?Ow3=>bVCW(>XmVPNK*BFI@XJIAX%e_mm=S)4 zq~W~_gIJuesh#|;GlSm-79Zd8jAikz zBoc?djm1qHr@hsIi||iS*V~;M7wR|?eEezDCHTF83vz-ahdBJ6ge36fYK8YdU9fi5 ziWhD2zUBWu+BN?KZQM8sZgBA>Fe-JlgVZ&q%WLe-UGuGc#vP6~Rl9zMeAB&Jc_O)Kig__L01XVT!b*{rO?ZOg?7o+Wmd0(aPGw>5aZ VRe}gtDDX=;4jGc|EWB^me*=9CVN(DA literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/fifteen-letters.psd b/luda-editor/psd/tests/fixtures/fifteen-letters.psd new file mode 100644 index 0000000000000000000000000000000000000000..1449100ee26e97c841d203ffc91d080bc31a350c GIT binary patch literal 20957 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 YOsmAL;e(u|1W*Hmr>mdKI;Vst09Bww)&Kwi literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/green-1x1.psd b/luda-editor/psd/tests/fixtures/green-1x1.psd new file mode 100644 index 0000000000000000000000000000000000000000..e0cde84e38ae93a54b52c6838f9773ff214aea96 GIT binary patch literal 22483 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 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/green-cyrillic-layer-name-1x1.psd b/luda-editor/psd/tests/fixtures/green-cyrillic-layer-name-1x1.psd new file mode 100644 index 0000000000000000000000000000000000000000..883c7c34241f6181c2d584e92cb54f50764241ce GIT binary patch literal 22531 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 Jc%JV<{|%@%kjVf5 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/groups/green-1x1-one-group-inside-another.psd b/luda-editor/psd/tests/fixtures/groups/green-1x1-one-group-inside-another.psd new file mode 100644 index 0000000000000000000000000000000000000000..ab234237903591702269ba4136f3a284db1b5235 GIT binary patch literal 24045 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 Y+(R&3R^@C2D|n(N;{iJ;U~fl%2ar&`v;Y7A literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/groups/green-1x1-one-group-one-layer-inside-one-outside.psd b/luda-editor/psd/tests/fixtures/groups/green-1x1-one-group-one-layer-inside-one-outside.psd new file mode 100644 index 0000000000000000000000000000000000000000..2c955e1d01f1c878c72f75f9b9a2eb0f124029e2 GIT binary patch literal 23635 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 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/groups/green-1x1-one-group-one-layer-inside.psd b/luda-editor/psd/tests/fixtures/groups/green-1x1-one-group-one-layer-inside.psd new file mode 100644 index 0000000000000000000000000000000000000000..5f5281377357b4a037a62e795f062144996cd7d9 GIT binary patch literal 23269 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 kCkPnv>&FP-SW0E`SFga7~l literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/groups/green-1x1-one-group-with-two-subgroups.psd b/luda-editor/psd/tests/fixtures/groups/green-1x1-one-group-with-two-subgroups.psd new file mode 100644 index 0000000000000000000000000000000000000000..027f64037ae884752333b043ad84048705de41bd GIT binary patch literal 28973 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 D;|Qn8 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/groups/green-1x1-two-groups-two-layers-inside.psd b/luda-editor/psd/tests/fixtures/groups/green-1x1-two-groups-two-layers-inside.psd new file mode 100644 index 0000000000000000000000000000000000000000..6c3c3529d36b41cc84ab613fab7674fc2ab4bfd9 GIT binary patch literal 24377 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 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/luni.psd b/luda-editor/psd/tests/fixtures/luni.psd new file mode 100644 index 0000000000000000000000000000000000000000..2d743035b9b74c7a3a9c0ed281ec4c5d8ff60593 GIT binary patch literal 1300 zcmchW&ubGw6vw~2iD|(fsZx)^2qG1%tm(lDLNz_8P$AZ$t$1*=JBcg1lQ6r{YJ2G4 zAbQZFy@;2hUi=G05D#kc$K9j7dMHYKZ>D7vJV+t7p{w;Z`h54*iRWx z`9az~SF6qN2#)2xUw(eJtNAp}{^iXNs}Ejm++SN-e|K~9`S|xi6!VODU(@ny=DOv#%)u?4 zP|R22BSBkp{$IpXb`3ka z9o0Ko)KWasS){+w{mr~3_9Yg(UD8X&Rx)p+1eY!R_EbwJ>?RSWrN%i4urKbYh*YW# z#gnZ^c9xkcY^78YQzX6F50GaF6$npxue^1SZ+$L1n@4O9bSCdrQDp|e18DM3ATv< literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/negative-top-left-layer.psd b/luda-editor/psd/tests/fixtures/negative-top-left-layer.psd new file mode 100644 index 0000000000000000000000000000000000000000..8b0bd732c6919cf1cf17c83ea3327377e1173de2 GIT binary patch literal 22595 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 G;{O4ax;4W9 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/non-utf8-pascal-string.psd b/luda-editor/psd/tests/fixtures/non-utf8-pascal-string.psd new file mode 100644 index 0000000000000000000000000000000000000000..0157492fef846e75570c916e8728f1d9aafd1587 GIT binary patch literal 282 zcmcC;3J7LkWPku>AO&JGGH@_}*dXxC!pYN@{Q}qN)z=s68e|a2Z6KJ!FqPpg09j5kC;$Ke literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/odd-length-pascal-string.psd b/luda-editor/psd/tests/fixtures/odd-length-pascal-string.psd new file mode 100644 index 0000000000000000000000000000000000000000..fee3692da04438aef1484c628b5828255501d7aa GIT binary patch literal 1528 zcmcC;3J7LkWPku>AO&JGGH@_}*g(K~#KOtbmwN}Ve^F+7W}Zu8NupuA!$%&Vgr4@c1gEJSb?NCRB#fJ#1KNzDX?DGXB?-U0wXcXf6E literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/one-channel-1x1.psd b/luda-editor/psd/tests/fixtures/one-channel-1x1.psd new file mode 100644 index 0000000000000000000000000000000000000000..842fa1297c65e98403bcca332760fdd6736066cd GIT binary patch literal 23892 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 AUH||9 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/rle-3-layer-8x8.psd b/luda-editor/psd/tests/fixtures/rle-3-layer-8x8.psd new file mode 100644 index 0000000000000000000000000000000000000000..9297934382ba079e2b0b84c72c0bdf1f032337c3 GIT binary patch literal 23740 zcmeHP349aPx<8Xi(k4y!g{?qn=>nK0UD73VPgmN~(w0>eCdniXO_MN5+JYM(qJq~2 z5fN}wEXTUj}C8+s}9YXPGn8 z-$`a+O&uZ-caU)NXHS2GX*kA7GYiYB_ZG!G&8k62wCs|$JmgJ17h zcVL%DU~S&CS+H+3s~d`SS?$?*tyW6gNZX|v+F&&2#QgBu8!=L&E+=NZvPw~9RnrZ| zvY8BBH*<8oc4nhCOBa)uE6#3JwKiFsXgeisZ8Di{s@9wsN@vl~Dxk4g9wTKl9AElDk@NfyQ+PtMB9k}Fc=DJe3rklALK?NqDG zY>VZBxZ)JiHZ5bc+Km>o6vw4B7Kc42CI%PeKAiPxvbqa0+mhHiCTT5A@>a?!Pfk+E zdvnrh-9AIaZkRk>YzEv~7lS5KS0xEX5TM5I?wTm1|x zEw80*76+rHi<=>n*xr)2HdyQyTZ82~^>(Yd+Ov(rVU`W)tJDll*)2@H#bV0k+P%gV zPpVeqcFvS0R#94`85c4vTkh%9*#@2Q=pxEa=YnsROpzi>&Zd?N|i#{f1Qv*n$pUHsYZp(;1q?37tcmlx%NAjxRdsWNH3E>)4Km!+krP_nd?482UF zOih)g>N1oWsWgyT6bn*SRaf^Op^Ge92kxQH&^id6tkh*HmFWsua)u^LmZs39%d#}u zRGA_@jZUU?y3{mfiYs&%_a33E86(Utl&Omi&i>C9(My;fMU@$C&;w_5>6y?!+4-Z6 zp59&RQkabFJYc058;yraPK;}uxF-(xynub!;ijUrxB+vuxXaVJYLKX*Ds@`fi`;^FkC3s>iR)!7QMZNV(0<`^obiA&0O5>j-CZ%%ki9RGX4!s zVWXP=T0>;%Qd4y)8CqFJGR!n-8QKh)MxUmWr7EaQje^Qjq-N<|4gKG3h_rN7s=_tr z{BJfyXHV8PP-X+I%ayz8<`Vp0YP%bo1^>z*@_1hSzi2ydGy)FwjcGlpeupESy zsNB62?J=RaXWoA&(OIp3XH<_^zCt_z@y`0-@{W&()(4jdAl_LYT;B2V(E8x=0K_}% zgUdTU9$Ft<9)NgfeQy|2O!>AA6(w?@zDC<@&Lp;>x0WX zJ|0>hTpoaUXMJ#a$HznKgUbUD@2n3l@A!CVeQ(JL`kXJ3by- zA6y=QcxS!BCGOnZNt=X_^viFm=9)s>x`|~qR?o^K3L7gq?yrF6J1|Cp&mqBVVnc( z1hDAqXuHE&t(m3;r3BR=23g>r9!XIhqLCdrkQIkQVyoL4I5te7$!>OLB+)pGCcDwh zh6KHcwN_V6tl)OSj>I8&GPrcsIIK&JtJ&S4HqiIMmTIfr?5YG*Qp32JT7#|1&8Qi( zx|1>6JDEz2$>!FKHrSipOmUN`$jtzbouLc0jRq$W97?FRv``J%up54BbyBI$;?U$< z+Hi*KQOcNm$O=t8?Ft!P{aCZTB(~Os+tz&)nsn0M6eUJvpd zR`;sv_$xXV3mh76Qs=Q8-mLD*jT@^9Rzea4hYBC(jxF$@;Ilq>C%k(k^YS4WoKEBi zVG6Ok;#u4Nm?bd^T>UZY+Dp&T&cVhp&TCg0x9`528iU;Pz|H5Bn;j-R1LDdFVRyM% zXY&}@T6;3+*fjBQaQ8V7dh}UT*x9$)ir02_UCaw@u$P#&jWbo^!AEo}N5JYp90sQ! z%aj*&Da+5mSRB@FBB6!ds_U9pi|cT7Zr?4$2&5c#ODS!p8NBBe1PJe!bsCE~V=Td% z@+N~6Zhv?EN!ViF?cstj_}wK{HbeIZkdQLj>nTGws~{~6esrt7+*Vd!Rq2}I{9WR1 z#-aubGqb>CG`OBX!Cd9bTw`J4h#ESKU2@1Z*QMF;5nHDIsN*|beSzXGFcuGY%H15cO<^4J!;71NV}j$j}`pskoT zaP(YEYdCrdYg1nfPch*}m@Fg|#lWK_XgkQI%~}J*2|5Y1!mKlzq5gOn=E9y?7!bi_ zK;P8VWWhApp+rrWIEF4XMQPkIDazocxw?QS6ztq1$uj@WOOwd~c(F%3TqO-mSuNZn z0JoEkmI_QmdlMJTn$guj!|Xv^F|xxQ(>!s5qiif^&x_XD)cAQ51#*RsDyjfF8t4+U zX*6zcEZ69bCFMXv58<^M?dA1ALs`80Xj^d|&^YFD#!~Ani?>|Qs0*CFnqIpoP7ZMD~64540p%`|Eh?)Q)nzZF#?H7Y_ z2g%nl?JU29ZAF8-v|6@}8=+;4ZW~v9DUjeYaX+vrHYm<3e&eFp;Ykbq7O5@P8SJx1 zS^zUYEiE@|lh|Jb%0dV~N3ab-7o*tc0}=IZr)^d35lj-gMX_nblBqIUEPx(2IQ zKO&F_C;AbCi8x{ekwRn=xkNEhNz@V(iK&EvFcS{qcH(Yg0kM!+N~|E(6E7045_^bu zh$Fs5xjxCc%Gb>$t&Ph@alQD@C>}^yqUbYya#zpcq@4ud0TmV zc<=F!@!ELb@-C5x3?QS(A*77VB-La!If>Mh40$IxpIl6?BsY;epq)P?+sF&#RlblP z&X@9K(1XkPWBFSCbpD-byw`}rU8Px0INe+U8v(Si{Im7rWOL0}NH2<8bE z3)Tp>2;PLzbVl%tP$-NNCI~ZNG)@#w6W%6#K=`C^vv9ZYi14)VlAoVnKfe)vIeyiC zl%L&ip5GIG8~xt!JL-4F?}~qb|6usiHBpEtPNWi5 zi*%wHqK8DQMLR?vh|Y+vi9^K+;v8|kc$#>&c!_wE_)YO=;)?+R0Wkqt0b>G;0kZ;@ z1iTRNcEG8CtCBFuaEV$nS>lj9C|N7nE%{h-F)%PNJ}^IUQlLHX!NB!_`vSiRyb=^1 zloV7Rqzk$$Xj#zKprb+G2aAH^f(wFg4W1diIQZq@4}vd*h(h8+)FD*JogvFYUJW@B zaw#-CG&!_7v?=s~(C0$m2|X9)7Zx8@9HtAK8@4KJU)bqzGCVe19j*<(JA8Hco8f07 zgb@i5I^N_YJ%-C~8ptplO2^ z4cay6{NV7xxr2>^7Y%-6@Hf&(X@S%vT_W8tZ66Xnq-=;|$ciC{hFptDh#4C*CuVcZ z=dt40%vgQwqS(E$?L!9-tr~j!&<#UBi4(80j(Bp!k(&fJ zW!+@HY2{5PWD;3{tXZ}}b}A_9%ZcqI&ZA2QK_Eg$QWw^3Rd9QM>^7r(NbS8aM`uU8Z z8B;TsXPn54$gIhHAoJ}kK~`bbU0FM`uBy^icGXL&A4bYXHjaF5!Q&>6TR-md_@eO($G1(8Pq<^k zdlUOlG)~+;$#2rQNoyuuy1Dq~$8J6|Svh&$JGb_~)pYCYQ-Y^ZQ(m4b zm^yyyb5nn#s;QOKB~7_zxu#vK);_NNR+q0^q&rXN&rH`4%2wk<|hB9sZFn%!_1B5{gy$NR?89V23d4=*)T6?9y9O6y(8~^`ab@B#`_N4pL+ij_x~|pH~-xQiUo@o{PuwM zfp`Cr{EsF7=y*{7;Gu`oAA0Ix;lt*Kk3W+4$hw803ui7o{b<#rTNg7%)vnvWK4bm* z=c1o`XajG9W5fB4lQ$lIzUcYwn`E0-Y>wKz;05%8fRkAcWikr>9w^xhwNPTdgSX5?Go>ryQ|}k znQvU$ZQXrgk73X0y;Jsnx^Ke1kM@t*f9TDMH{X7%bsfm zZhJ5Ny{!k82e-Vhe1FTK^g~-e$oOFU;jF_uj$|L%bu{njo)3#YeCwm~kKQ{r`q+`< zV~?LaG5JK>N!`hFADce@@ss9Hu6;V|Gr?!`KM(nQ@fXowto-M&e{OC|Z+rbz(W!%9 z)_wWuY3=E6zp{Px+nKpv2Y$Wy?BKKO&!wE(dA{WQ;cssK=FGR&Z?9dL_g(0B%fCm7aq5aH{%#R&EJ#ew##kD`D{k->5&807XY5L{b<@s0oU0HWE^Xh?L$Nzfv zTFY;Kzb*NF*zY_3sQBZvj;4+d__Z&JT?>$K<-u1q^wU~cC%|{d@WZfvxS4y;V=eG^ zKkmTy>(Jc0AZwP6&?X@8a$UU}p%>tcp9Qgj$4mA4e8fvlb)O)?hfSZF!V-jpfu+fn zj*g21;TysZgnnl_I<7t0(ee9gcvtusq21HD_e*Rog!hVP@-cO+uWlB;cRkqmcX(rf z121?4EDvuAP+|N<4A%t9@rk7hkoSW(5~AZ|$9G7={(#>Ubrta6qac<;x>;aHbbN~D zB8k7hzsO%A5=lY>!~vm^!4gSuWZ&@c$nfyKp%V6R-Z;kdN(2T31O^3$1Oh=Ms7&0XlZ0P=83CPZBYR7fO(!M91gA6CL}BJjhZ2i!V4JVSJE?;PC}QKYx)p z02;Krl0ZDtr3^s?pCEaBp1@D&FCZnUpbRDXL&A~;1!Ji2nCY`qgb|Odc(E{cXk_gk zO==o5=ZM-bZed;fsUNjAWz@=-?v5{dv|d-dca=T8&(X18(m%aCci)GO(-%t;7OlSL zmB;pf^wrPLZh7pJ&GQzo+4|P;uYbuX9dBs4_wlvc4xBi9ITZ1DkS(9h$xkRy zvMCHn4&y@x(__K~DYGAmz!~hRJ<^^!RC9`%vrt_ZskNp37$?LD_{A&t9)%=U_0biN zO}Eoux>LA*1__-hbbNv&BwJ=E%0s8ZCajlNOz1!zA9G)RhOmI%0#_E@U{t`qN@!q- zFNGf70Ly(loHmxSf?1z`VS!%})Zm)}T`(DC;=o)sv7oC(AdQ8!cUVlg$dVccIc<;s##$=WFrLg*qi7_>D4}sW&#!_^)pTCk7}-h^r^EdFWdi z+_|yu^^QM>HtzWNxM8#dN*Vb0&f(n!ZTLQh0#GUY&dq%(xIW?}i^hLE;>~{#;(xk- ziwjK-+S$RoAddUD7!|@75C=O6y76XhI{P>K_pAT>{=L9E+VEuP{_ko>_kS>!{=5ac z+U;L2F}mqtzW#ek`PX&%pULCTmjay-_qQ6#&dzO^E~&CLf*4kJ?2Hd2JY;*|=Gj`F NeD{{=h4aAdzW|T$yb%BZ literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/slices-resource/1.psd b/luda-editor/psd/tests/fixtures/slices-resource/1.psd new file mode 100644 index 0000000000000000000000000000000000000000..f976c4595ec55a2363f770db33c613bf919d98c9 GIT binary patch literal 20929 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 WHuU;e20_kH;7SY%8JfZW!2bg_S~i^k literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/slices-resource/12.psd b/luda-editor/psd/tests/fixtures/slices-resource/12.psd new file mode 100644 index 0000000000000000000000000000000000000000..51b92020ff94905ded14535dc4e24ce54adec192 GIT binary patch literal 20931 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 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/slices-resource/123.psd b/luda-editor/psd/tests/fixtures/slices-resource/123.psd new file mode 100644 index 0000000000000000000000000000000000000000..2d9e2b4c8130fe4f01d105fe1806de776abd51af GIT binary patch literal 20933 zcmeHPd3+N^-=EE$Ha*gd8`8TJ+N9|ndZ#BPwDjT-g-xsZB8lU%l-v2WB?Cx)V*Zk(YzuDQ$=QEp8 zP+1Ky;3!;pK{N(L0MW2iY|JPq)9|91N+5G)ofugZVx&=7xh+;)*GSMZEnzT{*%3dz z`$2@vsLzfVnW|A}tZJgcSlUVv)vd#8bghlLOnpRdj!#yLs>N(I6ErSsF`G!6swF!D z*ITrN3et!zkB~7cbYpfzJ`%`AX)0xE3q{D%5)}zLg+eJy&rD28OG!>s43;SsN`+jZ zlqV%8D3Vl)bd^#mbC!r4A4p|U`g&DWVX?D0Sj&!RplPd0E^lsbPHaw2v`_|lQf6i* za-&oxK#c_3B$CEk5=dL5!->mJAz{-|Mk{T!kTT>K*IMj!c0>ef$X%TMGF#mZk+wvp zkBK^qS>A$M2> zh!Z}OTE!V_GSS(s z|FUDYJ8vUwG(6H?xI99{R`9~bM#CfRh07yEYy~e|Y&1O5Ubs9$#8&Xa#YV#;?S;!D zL~I2wTx>Kv(q6bcLc~_^!o^0zBkhIDBSdTkFI;RiJknmcJVL})@WRDL!z1m5%OgZ= z1utA|G(6H?xI99{R`9~bM#CfRh07yEYy~e|Y&1O5Ubs9$#8&Xa#YV#;?S;!DL~I2w zTx>Kv(q6bcLc~_^!o^0zBkhIDBSdTkFI;RiJknmcJVL})@WRDL!z1m5%OgZ=1utA| zG(6H?xI99{R`9~bM#CfRh07yEYy~e|Y&1O5Ubs9$#8&Xa#YV#;{kL%WbZ##rNVuo0 z8Ezh9cAu%3?Pyov8SPGU>}2z`QgCNbg`KwAX~^Uwl}a03t%aM37>zQLCJ54QMgl~m ztJ*}NdH`cvZEQgr1x6a#AhE+IK@G>vL`~7C8pv`q<}iRe0W9^^1Z}rgXea2PPy#9e z1uXDa4`iSk5P$}DU`4KgkJYUWYmA!$6HPiJl4$Kl6Ky0JmryQdwAGr?<$&q&5afcB z08gDg4r2AVg4vH|gYq6oQ*EV5S0`X6wUoP3Ww2@76*WbwJ1ZpJSsA7^+1!%h2D-^z zDKeW1-4%#qr|SY;qrnM;10_&ZQlN&>Fxv{N^fH;=V%O$b+E9c{DWS-Rcm<}1^$I9` zO)W_mM^>58y>*uYlU~*xzshE!8GdDpDNpNG_h}+@aDNZnh6VTRIkTeDP-$~z1)`BM zyrQtdFrwb!=7DBwEY@zDt=5?w{7OofHyU|hN}!IiSVzKbHI*nsjikW^pFa{*!I%mx zG;J}PETq9{B5_Gj2rRyzi(h3lG_VD}E&+_qt&cK?`-39}@FKio@aMcTSrx;`{GE-$ zOYRil$TvTR$AjlFjAk!)y+7mi7E>29XL!OGt5}qqFw-5`jo6ItZH8M4}GP6MjFaO{D=faJ;T_y;C!0*b@*bH4K5dm(ZYj8ssEq@&W?T8k- z%vM^X8RmLSiCpY1%HjqK)tYZI8eFqTfTQ!JE;j7BP*2q3cIL4uZXzhU8}&LDyQ{jd z*1+tMV}UDm_%A4N3!n)YZXprYV%lPb)31$SKbL$=@B#J^SE6Mecn|S?;cahtP@Nfb zai}3Uzru9RZD(l5_zFO@9~_Z>BD6D>1JmCO=Oj4s$VQDGBNKiD)87w>K@^bVI-51W zvT_)*a9t7YEHI7loOKMHQwhkIOYje`Qa#)k4s*W}>TC5jof*>az@uGfrQoE+T?6@% z&9oKKM<5+S!2ltB5zz(*jd~WMWoRB6mLXJ46`YB9Jpm9&aGZkEgCq*_WlcH*^dl*R zw1U(dNl3#w$k8?6dPu(m>A_~R1<`Ggj?=RI7+5rpYu!F^+~B4ieSwy+Vdp+art;rB znN43RmD|WuoQtqZn?eGd0-w1o;q8sd`wJus`Q=>V9vDM>+t&1C5dI>BAE5K^7 z9=rjzf*oKF*atoYpMc}w47dQ=z!mU4Xa{$|eGJ2dm>(95^}+^V(bynNiDh6pSP?c1 ztHMTO<1qt9Vs>mY_B1vNn};pMR$?2lx3FEK{jQz&pawMD(PCrfz zN6yLM|NxVcY87r|% zUXZMl?3H{jx#j2Q7vq=bH^z_ld%@L9l(KyhG9pgIr_d@^uZ;I6=Pfw!f>(j;kx)GVDX-6;J?dM!v8 z6cbbwqz{@Nv^wZO(B)umaAdGLSQq?s@S5O5!B;{AA+aH4ArnGog}f1RETla&C^RLs zF0>_dS?GJAZDHK7n6R=iQ`nratzl=we(TY%M}7~Y$8$Y4_c+<(ZqGhF^Li3JXZGCE z^Gwg*ddYef^=j-jx7UtdU-stpj_*C9cXRJodLQb2qfbbmA$@dxX7<_I=Zn6azVUsl z`aa%wb>E|XZ};opucV)~-;#a@`rYUs)<3WRg#HWqzu*7*fZzc+1B?R}4ESKccd}4f zzRV=#1!w=ta?oqwmJV#*B=4HfBf6wb-7qWwEWXn_@4<`NieNO^jO=cY2^;V8+14 zfiDj{9?ywSjco) zPoK9o?`rF|Y~6;syCVxn&Kub_NW=NRFbx-o0V+JR#$1Ctv_-$>OcDc4)r`9dfeXq~c zFVJ5nvWa=b)%xuEdG*%}*@pRs>kYXL3ma}2)yAd9n-fYWte9}Ov7&KZV~1&^X^UB8 z9&g@729b^AK}&y2i{*rMkadQ&ZK7)8OB36va%!y&vps6tNr%9fqoejX`!n`SO*u`= zn(j5%HNV{=Z6RBZPKuxO+@!0m#jUSB&U;+<`2NY^lc!F;^hEv>E1$%k)IPcYsmQ0M zKXqkF$&~d|eWo@}{dAgQ+PrDMJU#O1-P8L`pECXOGbPV#n&Cf#nsM&gA3-vD?o0B%@l@|psk}sZ}n>%;? zJn6jFd6(yF=5JphTQGCMFE5RI>7#`y3s)@iS!7?-ws_d$olByYytw4|r438ZEGt;H zWqIG_&n>_Evi{|hujIY5c}2e!vsV1}s`1tHD@#^xUlp@z;cCI^rq$oBsatdSwT#y` ztnIya*4jVTk?Y#lSFJy|A$`M!jeR%H*~Hmo-*o-;v9BM0qwtNLn-ey#+!D5B)|=o> z`>yt&F@6NvwU0dwwc?p?US~*?`YW3wsZ8(6T61)+P{0q?rrZT zzPoNu_?~6&g}yiEeV_NIzu)me>j$^@TKC@AXV`an|G51Z4vac*`rwFz#}1VrI`U!h zhX)SlAO7G-_K`gwsXp5EamL3xK1usz`_a^++dfVGblb7CW7|JV|7_>+%;URHWSw~b zWbVm*rwUJfc)INLCufGAIdQi3?D=zJ&$XS`pTG9G>GPXkG<|XZ!jy~ri!;9r{Bq$} zeZN}u?}7i`(w5ft-lf7zN58KA`od-1EHT&yYT9Os~fHBeEs-$ zk9>FKd+Yc2Z_M~X`or=c<9>YSr@WtzwvTGRa+A8*@$>9ky>6}hCFPg>w<~Xdb;o?? z{@t1Pdfi+9YsRmKe;fJR)%(rA3x8ky$G|^!|5^U$#SU{v2mC|}&b$k7;h6(hA;8b; z;Qt1=P7@5@A$%Tjyi+q8Xr07S(Dg59$9p%UmIlCPNT63E^PISD> zBklh1f5jtGz`tNTuRX<>vro)_r4WY0gJ)*_g(pFEPBN;q4kU*Mrw4p%!#d7)`~W1( zCzr;lHL(68@Mmg3KLy!zTmaL7L?jZ4MG~=CBK7t0m4*gLBmtp4gM&kZgL_IPOmSWu zl?Tt5pRcc6!A@XYECO((7*~pQd&bsWTU zVU&FM@&yYF7!@wSI6S^UC=&blLYuBa3~;zEVIaVG7?;E2@Pz^qpDRg*LMfLQ9+bq- zAAtu)Oq{9|gv?#}RzYM`Xw^P#atbx=gjyIque$xxO`R<@Y}MAMV+!Zj=!^ERrqgQJ36 z4XYhpn`Jw(euUNZFfRHTK*cxFSj(HRrQ$gJbmQ8u#6!ktTsV&|Lj2^Pw^YKM&ZW+UllK>Pu>-q^xg z)e~E7a9zm;zjF1@e1<;g6E(u}iGuJ&%M#8{?{!%S6Tt6i*(_ui9w;K+>tYNI9f;t! z_pInMf{^wnNL*_o)J~3b`B32CLbdMopyk$k!riuYN2SCt|Lb1 fS!{C(^so}-(uQ9D${@fQ3S5anAwx6xANYR&Jc=`Y literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/slices-resource/1234.psd b/luda-editor/psd/tests/fixtures/slices-resource/1234.psd new file mode 100644 index 0000000000000000000000000000000000000000..829d585cf89fe4d9decd539742e12b456e2bc712 GIT binary patch literal 20935 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 MZ3iofYPeeaZy)nup#T5? literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/transparent-above-opaque.psd b/luda-editor/psd/tests/fixtures/transparent-above-opaque.psd new file mode 100644 index 0000000000000000000000000000000000000000..3b8f62d48933652425406bd540c60e18fe526c01 GIT binary patch literal 21963 zcmeHPd3Y05*S|B9rD@ZBVJi??x&XFGSK81$U1>{8TUJrXGEGC%ButXF)C~|3!B+(l z*<4rz5d;-kL{vZ&7hVLE#q}#90*YEi1V!z4?wur^0>1J6zV~~c?~mL*Gjr~_=bU@a z@2)d_ZnBDM>k)*t!BSv8|xxTV)j=*drpe*3sg=sMwzlN9 zv}7x1RHkHSXDd~y%GA^(kVvx6v^ccwNfvu7Z^UhLUlg(kWS`^r>R%dlOa${m} zMV{n}tHtK2$YM_>F(&J+Ey{MSO_`FcQr=9-p!ew5oSd1sX3#5HGuy&i9Cpx4xv3sN zT3zi?y=iqVEjQJ)J4#w10v94T)wDOvw6V%M)^2rjdbXq$Jc+%T^R_0d!)kA`{zcrL zVm^A(I6P)K;J#YJv08_fYp`0)d3?Lqy6q`68r;rVio|NI-ekd*49ihouhi29J@(jQ zt%J=2-RvY)YEo)eLu!gTB}JW@k)+B{t5gCbyySH@s-Zs`v<~gxvC>QSdOKEwsd47t zveF|duwl@v8?9W6){&>R*~})r7I#2ptHt2zRvu_`ls%#XebPPD6&AZgYtge6#d*L; zHkk}+?Z`A;Mq`>biA`g(lhV@~vyyb#Bh!<#+Dw%`#gML2>GcGp>T03-4W^5&dMECo zF4Km8)3iYS2Gcd131%0qSs;VG|97jniJ2R!DmU4o2hJ4qOz59v{-|eX^yaz?%qB7q z*tDFT#ls{w#yw6v6NhJBz&d2OskM6CfO&e{Mg3YO8_GOUqH-jNMbeK&wKdspYJVCNrB?Y_i+T+L=OSL8(idcTccz5bt>| zEK9H=Dm_bL@4!6G{~soL!uk&z7Mk-_B1FS0?SsoJL_z@{TtYOw(muGnLL?OM!6ih) zEA4~JD?~y8A6!B-ywX0nyh0=t@WCZS!z=BB%PT}e0UumKG`!M2xV%Cn6!5_%M8hlX zgUc&KLIEFKLNvV6KDfL>Boy$$B}Bt3?SsoJL_z@{TtYOw(muGnLL?OM!6ih)EA4~J zD?~y8A6!B-ywX0nyh0=t@WCZS!z=BB%PT}e0UumKG`!M2xV%Cn6!5_%M8hlXgUc&K zLIEFKLNvV6KDfL>Boy$$B}Bt3?SsoJL_z@{TtYOw(muGnLL?OM!6ih)EA4~JD?~y8 zA6!B-ywd*?F8`iGW~>EHmbJkVGI9n@O%A1kuyJrWsK)8AIUT?hW06|B zqh1F`iikvo#lf-`XA3UCG=6H#9L@(5-Fj0ymMAhgunsQfZL-`LZ428_GO+ zoCL5o*0T<$twuLp4~246i#TM3zec1$^@v3dl4s>WubkPe#L8 zG(8!MqbF0PGuu5SV~mbgPo|{BTvzI_XcFQWtHWV!FOld(xy5a2EV-#qfT=P=*# zKA=b7Nx`4%NqSX0DGPE{3Xl9*i0^NKgeQXgWFpzmlOIH^ULtuJxx* zeAA-t|4kXe0-J`x=t2sfkT6m_aw<$Anj*1_u;18oJRoSI?MFBmDd5os{N>Ue1YDu5 z{y58E1o+r-mgo_CZkjL!H`m}J983%I9;QH^kS>QXZE>3M?1uxBzzJ82!G7JS);m(5 zjD(Dbf~UU$^Sb_uihBAraigxM+v2>$4(AzJ`*?E|9(Yu*dc>p**kN!G_- z&%iO?UJDBWzc;7aZtT7INVH~0gVxwfDoD?o%?)h3qrzU^P+jGo-u&F`Uc#~_E7wtI zHW}TkPcR>Rxtk4a(x4>Ek3zlNyq{#~D85lK@pcju6TD{#?SX*0#HQY~3yApKcUrz92?878%%U$p{ zpP>dgRStc>7UXpXyS@eJmtl0x)!R5&>?B#hk8N|T1&?%r*7K6zG@#Fa@Pv4}40TEmd^ertdR!oB& zO4JGL7zMOetMk~@YK4^V`_kg*@L=jBEuciw11Pcd>k)NN9*kxyw*ekUuDo1R{|Xkbg9KW2Ddln>l#g^ z6+lA|q1#Q4iUy#eE_yy|FR2F_+g!z2>s)ows~R~?p-Xp*mII2HKj3U0hxyq4sV26< zZU5(vriKZ?2cH@LR@3+~K*Ng6MA@6`u|MDgGqj^ggZV^;bJk)1hXP;CTFQ!m27j2_ z8@bXtmps=(_A%HuW`2{Y0_$RWg`=qfw=uS}S+6Dj_XTM%Yy+xj1f( z38cgV_j=TUzC#v}v3vB2NGXe2f&Yhx%j+h=P90lqB^u4>YoJ?DB{Dw=Aw*RLwPGo!+pZ$mEh#CH6idn_rIKNy{-Qk5C{Y#M3*br> zWr?!g-uB3gzZM@Ae|zcvuVLC%e<^82 zqo=ky(#FluGA56VJ6Lcc{Ct8FG(dlZE*^RtQyi$0nB zDoHj%cpbqq2wjRI>jM?_UJq~1=LqFaf*tCf9$poU)0fY~2Kr(TFJTjGTwX`$o%{4o zu9deZ!VfX*vx8wo^+SVDEJ}oUQc)%vg$hs!8jWgD1Db?xhhEnN{h32;=uR{n%|i=e z|Nj_Tfu2HZ(X;4z^b*>M_Mm;}ZS+1mf{vq4q4%6c-=Z$`3%W*8l#~jj!l{1LU@DFp zL8VezR324ARZ(@+Bx)LEq%4$^x|6z>T1YLXR#2;{jns?OE7U&fUFsbAh zMl-aWj-Ut9@wAf8q6_Itx`DopHqtZb4tg&A5WS3ELvN;c(EI53>Em=K{Vjc&K}-M> z#SCGRm@G!a)G(8oMuubVW)?6@nKjH7W;e9+hfF7Pk@;055rvBsq9o|S<)U#Sy=aE$ zZqb9H6{2TFJ46RWABs+kx1OF0(qq!I z(yM*}euMo|{Yw2N`&sO7#JT|5I8x|5%^Hx#=tiNzX-e<6dsfuR1stdx+iF5(2k&E zLEi_fxxDqE1Bp)@NX!!ai)D2l{O3bF9ymz61Ie z^kw@l=)1M=@xH(HQ}iq8*W7PWzn%R)?=R{8uNLqe{5E4W9*XH{jps` z2M?_tdgsthLqCa=#bw1c#XTN(Fz!lxLi~jI`{Q@UpHJwUP?6A)uqolQ#K6S-#2JZe z5U>T@4x6(s=S*%)t~PgdZfBk%Z(82d zd7b$~^0oPE^1m#IEie?UFF02?tZ;ha=ECobR7G6T_M$5qwdO9(-eRV>taw53;gYbD zaV0BCz9@|;HI{BF?JCPCyQ6GxxwyQtd{Ozyih&jSij5UrqeqUuYxIFid1Za&ipnpm zhF5V_yQ}Hy(bbPue>Ns=jAhKu8dOtJ^JvZIwF$K|YF`~I8CyGc<=C(5QtIxkJ6IoH zKehh3`d=D~8Xj)=bX?*%=eYgjgU3%9zj6GP3B?l@Pw1SeoOsv7_b2tAWSX>VvUKwJ z$?GRyzP04m$8J44C1c9GDJO1AxUK!RcW>{1yZQFlrUp;dPTe+5JZ-|X=cfIxt8C58)ZF1wgL*O=S5xbeI(*Z8RMLQ{Uz(x!_hjcJAH$LZzM zpPGK9xu$tTbGLbdd25Sb%e0nPEMb;r%K_^kYrFNRZG>%}t#gKY#$z+OxJqulowDC% z-{pva4f`QyqVqoI>DIi~m91CX#0IX~Y!;ojYI2hN=}_so4| z_idUNG>@Bi^8QixKQ&)8-!%X518EN|f8fsrh6V2}R4rV(@b?Gx55D)0lz%MyNB2XG z4;_9u^Wi5Skvw8~`T0V67Bg=nZ(X`_D%A%E9S4FRSVAYi;3{M<;vf#-rPYrx(;ZwgoZF>6D>ax{4 z*2J$_x>mBbb?w*d#;-g0Ox80S*Y{t)aQz<}EE_tXt$X&s#*rI0J{SGm!<*<$&P^9K zPuYCr`QqnyZAsd)dTZ3yg)g8NoG*Oy;(ecLRy|%p<_Zjz{ z**|svr*BSt^P>Y}4;+50@~wB?E`9sWgM|m*cqjLrJ@2aDedWEZ_jbOY`TmYW8Hcw2 zGvlAz4`&|U@xjOsb{)w+vioSx(btdVAKUj~@rQ4JRPoXK$HyE$dScv(QzxgK>^x;S zb^c@X$3K43`pLCVXMHCAY{BOtpD+C)`inLHHSE8(c4l_IcDnfVp)c#d{Pc|e%(q|J zzxw^`+^++_UV3iuxsB&j&+oZVdf~`7w|;Z>TidtSF3$Td^t)BxCw~9(4+TFQ>YCVf z_DAl=?w=mK)bG-UpVNQdf4TPZ7r(Upa_!23tNpG%`)k&(2Y;LJ+qr9Pze|5#_Q$Y4 zcK=!V=V#q5-QDnuLt3&IVBk)}7d7_zAWxZ-DlZOG%TdP4!C)6zT_aB#9Wrlk^sl5?;%x!T=*X&`(<*p1L1 zTzB`iC%U`;SO@P4A0xDP2LFDEr6urQ@pJ*Ej`uap!cSo$@9*%&010n+7?OuK1qc{_ z5pzd_pST<@x*xoeP~E4xze95JBVtoE)sX)l1(6&w%mO*8`%^R*$^HENWPWm)OdcBG z9}pTDESCpI_6-k@3=i)cDksVH;4|0Xslb4Mz@Wg8prDY5prD`#d<%-;RYL!cf$qI1 z)DME77%B$Qp%fEJb$<>)P~8WpeDG2Xn=eSjW2$*!BDb)ts}F~v2kCrKW&@)=7-KRmr4_sth?{!#}0h-)z8mtf9v?! z%i|kc=Pg~o{g0b!X$2aUec=FtpP(;(_rjSgMD>JyV8d0PIGe>Yw9EQ_Vgd)B-jCIe8&D`;KbTKhLUla4)#lr3xDlFLXQjG zpCCCy>I_Bs=yce`jmpZ2-KhIx{>#r00_bgUC+G&FLh>r1gDt)SdUz9T_Z@KA31tJZ zK3*j7D}n}eQ;`a#pe$^duPFxCL}gZ2qv1gKAt@C`{uM$-B-Nk%iwZZejvC;hr&Eb8 z85(p`$Vb?rAj46qgvR4x>HQ|sv%-HGv}gvrSF?B)GS4L6lkluVzUk}DIy2BB>>e&RSsL4M9xjq^ zwlrIXd9&RHGj>n8S#O3}lQ<0zUdP_lVgQn0fomKR#Sct5&-Tw`vsLVeA literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/transparent-top-layer-2x1.psd b/luda-editor/psd/tests/fixtures/transparent-top-layer-2x1.psd new file mode 100644 index 0000000000000000000000000000000000000000..d9231068889834aaecd939a63734819bf4637888 GIT binary patch literal 22370 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 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/two-channel-8x8.psd b/luda-editor/psd/tests/fixtures/two-channel-8x8.psd new file mode 100644 index 0000000000000000000000000000000000000000..4f736f7d4aedd0a81a37587fd2f6c4b91e9a119b GIT binary patch literal 24482 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 ScdVm+E$gl2TkC6W?>_-9BqtL9 literal 0 HcmV?d00001 diff --git a/luda-editor/psd/tests/fixtures/two-layers-red-green-1x1.psd b/luda-editor/psd/tests/fixtures/two-layers-red-green-1x1.psd new file mode 100644 index 0000000000000000000000000000000000000000..0361acdbc255475a7f6f0e4385534484098e0283 GIT binary patch literal 22385 zcmeHPcX(6f{(nzSM$@Ky0Hr`^>0qQu)0KwqbkdfVwoFmTI88&dI7!-q3lLGk>p(;V z91KAOK}Cj$G8G3G0cALE5fM<-Dk3Oq@ArK(8Up5WpL_4~`~8vAC+B?k`+Yy}IPW=q z-lHgOXhIZn9yDAqO`#x!X*ilo6{Xd6++@N;kh9l`=!Rf{A}hbB)2i0CF?O+r(HqPK zi9fu)D^YCF6(o+&u9MYSRZOd)Y9`Ax%^cmVo!O?%($-+5@;-zCW?uPy{#az1S`Z7>Keo<3(JVJ({m)+j4YWrH!nRSN0ybHF+wbt zW#md_*-}}KM3$kHDU|XIvFk`I31@TRN#@y4GTncB-w?jC7gwMov1d$H(emjU+XlR>~L|6Jxg9z%S#5egJ7*oyYZt z#hFYu^t9Q_+o1+7L~iJ5Yo1|cq>YTt;$XE*c{^m1bfe_0trok*)@u2SdVAE)+LOj{ zVwMl-D^)C`wp-X{i^W*zZ1)CtJh4iJ+gTw_t5a(YW?ab7e5tQfZyWT+W6IQarVxDd zBr>@qL(weHP|7ovnKFqiTPc%y1#w#X#HfS*s8ie3H;$5(;T`1~Ti-YqouOsMO-1Ro z^opU=DqAe9No_AwTdhWeR*gHLwB4+8bt@0F`O;ofFMrZKl+|XNU2WDf)n$bsNH-XC z$`*w}m&Gtyk~~GOT#}WikW17GjY6W9F(A*@F`B$Q0#aR_xBGRW%Pd+4?xC*GItVSx z%+Thja}^R@u3Rn2QfRX!dGZ{sM5EP0d+6kv9GS))+ROdA(DkeVW*4>5%LZrv?-p@` zFxM4TWw1dHoZ;0op?{M3qluY*t<KWe7URsy8Gd-`HKIwT*DJ&^qH6!&bXRhm%p3(c?Bzmj$-;C-LD^Q3JApY3^T>kO# z(FWl10mMHWfXhEVKH30WK7jaV1917r$4480%LfquYyd9*`1oi8aQOh@pAEp}A0HoW z04^Uu{IdbL{Nv-J4Z!6Ch<`Q!mw$YGv;nw$0P)WT;PQ`;k2V084

809^j@@zDn0 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 Result<()> { + let psd = include_bytes!("./fixtures/transparent-top-layer-2x1.psd"); + let psd = Psd::from_bytes(psd)?; + + let flattened = psd.flatten_layers_rgba(&|(_, layer)| { + layer.name() == "Blue Layer" || layer.name() == "Red Layer" + })?; + + assert_eq!(&flattened[0..4], &RED_PIXEL); + assert_eq!(&flattened[4..8], &BLUE_PIXEL); + + Ok(()) +} + +/// Make sure that if we're flattening with a filter that returns zero layers we get back +/// a transparent image. +/// +/// cargo test --test flatten_layers no_matching_layers -- --exact +#[test] +fn no_matching_layers() -> Result<()> { + let psd = include_bytes!("./fixtures/transparent-top-layer-2x1.psd"); + let psd = Psd::from_bytes(psd)?; + + let flattened = psd.flatten_layers_rgba(&|(_, _)| false)?; + + assert_eq!(&flattened[0..8], &[0, 0, 0, 0, 0, 0, 0, 0]); + + Ok(()) +} diff --git a/luda-editor/psd/tests/image_data_section.rs b/luda-editor/psd/tests/image_data_section.rs new file mode 100644 index 000000000..425b63b9b --- /dev/null +++ b/luda-editor/psd/tests/image_data_section.rs @@ -0,0 +1,13 @@ +use psd::Psd; + +const RED_PIXEL: [u8; 4] = [255, 0, 0, 255]; + +/// cargo test --test image_data_section image_data_section -- --exact +#[test] +fn image_data_section() { + let psd = include_bytes!("./fixtures/two-layers-red-green-1x1.psd"); + + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(&psd.rgba(), &RED_PIXEL); +} diff --git a/luda-editor/psd/tests/image_resources_section.rs b/luda-editor/psd/tests/image_resources_section.rs new file mode 100644 index 000000000..77d7f6ddb --- /dev/null +++ b/luda-editor/psd/tests/image_resources_section.rs @@ -0,0 +1,85 @@ +use psd::{DescriptorField, ImageResource, Psd}; + +/// In this test we check that root descriptor's `bounds` field is equal to 1 +/// So, then fields parsed correctly +/// +/// cargo test --test image_resources_section image_check_1x1p_bound_field -- --exact +#[test] +fn image_check_1x1p_bound_field() { + let psd = include_bytes!("./fixtures/two-layers-red-green-1x1.psd"); + + let psd = Psd::from_bytes(psd).unwrap(); + + let descriptors = match &psd.resources()[0] { + ImageResource::Slices(s) => s.descriptors(), + }; + let descriptor = descriptors.get(0).unwrap(); + let bounds = descriptor.fields.get("bounds").unwrap(); + + if let DescriptorField::Descriptor(d) = bounds { + match d.fields.get("Rght").unwrap() { + DescriptorField::Integer(v) => assert_eq!(*v, 1), + _ => panic!("expected integer"), + } + + match d.fields.get("Btom").unwrap() { + DescriptorField::Integer(v) => assert_eq!(*v, 1), + _ => panic!("expected integer"), + } + } else { + panic!("expected descriptor"); + } +} + +/// In this test we check that root descriptor's `bounds` field is equal to 16 +/// So, then fields parsed correctly +/// +/// cargo test --test image_resources_section image_check_16x16p_bound_field -- --exact +#[test] +fn image_check_16x16p_bound_field() { + let psd = include_bytes!("./fixtures/16x16-rle-partially-opaque.psd"); + + let psd = Psd::from_bytes(psd).unwrap(); + + let descriptors = match &psd.resources()[0] { + ImageResource::Slices(s) => s.descriptors(), + }; + let descriptor = descriptors.get(0).unwrap(); + let bounds = descriptor.fields.get("bounds").unwrap(); + + if let DescriptorField::Descriptor(d) = bounds { + match d.fields.get("Rght").unwrap() { + DescriptorField::Integer(v) => assert_eq!(*v, 16), + _ => panic!("expected integer"), + } + + match d.fields.get("Btom").unwrap() { + DescriptorField::Integer(v) => assert_eq!(*v, 16), + _ => panic!("expected integer"), + } + } else { + panic!("expected descriptor"); + } +} + +/// The image contains a non-UTF-8 Pascal string of even length in its image resource block. +/// +/// cargo test --test image_resources_section image_non_utf8_pascal_string -- --exact +#[test] +fn image_non_utf8_pascal_string() { + let psd = include_bytes!("./fixtures/non-utf8-pascal-string.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert!(psd.layers().is_empty()); +} + +/// The image contains a Pascal string of odd length in its image resource block. +/// +/// cargo test --test image_resources_section image_odd_length_pascal_string -- --exact +#[test] +fn image_odd_length_pascal_string() { + let psd = include_bytes!("./fixtures/odd-length-pascal-string.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert!(psd.layers().is_empty()); +} diff --git a/luda-editor/psd/tests/layer_and_mask_information_section.rs b/luda-editor/psd/tests/layer_and_mask_information_section.rs new file mode 100644 index 000000000..6b934a436 --- /dev/null +++ b/luda-editor/psd/tests/layer_and_mask_information_section.rs @@ -0,0 +1,251 @@ +use psd::{Psd, PsdGroup}; + +const GREEN_PIXEL: [u8; 4] = [0, 255, 0, 255]; + +/// cargo test --test layer_and_mask_information_section layer_and_mask_information_section -- --exact +#[test] +fn layer_and_mask_information_section() { + let psd = include_bytes!("./fixtures/green-1x1.psd"); + + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(psd.layers().len(), 1); + + let layer = psd.layer_by_name("First Layer").unwrap(); + + assert_eq!(&layer.rgba()[..], &GREEN_PIXEL); +} + +/// cargo test --test layer_and_mask_information_section layer_with_cyrillic_name -- --exact +#[test] +fn layer_with_cyrillic_name() { + let psd = include_bytes!("fixtures/green-cyrillic-layer-name-1x1.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(psd.layers().len(), 1); + psd.layer_by_name("привет").unwrap(); +} + +/// cargo test --test layer_and_mask_information_section layer_with_chinese_name -- --exact +#[test] +fn layer_with_chinese_name() { + let psd = include_bytes!("fixtures/green-chinese-layer-name-1x1.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(psd.layers().len(), 1); + psd.layer_by_name("圆角矩形").unwrap(); +} + +/// cargo test --test layer_and_mask_information_section layer_unicode_string -- --exact +#[test] +fn layer_unicode_string() { + let psd = include_bytes!("fixtures/luni.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + let mut layer_names: Vec<&str> = psd.layers().iter().map(|l| l.name()).collect(); + layer_names.sort(); + assert_eq!(&layer_names[..], &["1\u{0}", "2 のコピー\u{0}", "3"]); +} + +/// cargo test --test layer_and_mask_information_section layer_with_clipping -- --exact +#[test] +fn layer_with_clipping() { + let psd = include_bytes!("fixtures/green-clipping-10x10.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(psd.layers().len(), 3); + assert_eq!( + psd.layer_by_name("Clipping base") + .unwrap() + .is_clipping_mask(), + true + ); + assert_eq!( + psd.layer_by_name("First clipped layer") + .unwrap() + .is_clipping_mask(), + false + ); +} + +const TOP_LEVEL_ID: u32 = 1; + +/// cargo test --test layer_and_mask_information_section one_group_one_layer_inside -- --exact +#[test] +fn one_group_one_layer_inside() { + let psd = include_bytes!("fixtures/groups/green-1x1-one-group-one-layer-inside.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(psd.layers().len(), 1); + assert_eq!(psd.groups().len(), 1); + + // Check group + let group = group_by_name(&psd, "group"); + assert_eq!(group.id(), TOP_LEVEL_ID); + + let layer_parent_id = psd.layers().get(0).unwrap().parent_id().unwrap(); + + assert_eq!(layer_parent_id, group.id()); +} + +/// cargo test --test layer_and_mask_information_section one_group_one_layer_inside_one_outside -- --exact +#[test] +fn one_group_one_layer_inside_one_outside() { + let psd = + include_bytes!("fixtures/groups/green-1x1-one-group-one-layer-inside-one-outside.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + // 1 layer outside + 1 layer inside + assert_eq!(psd.layers().len(), 2); + assert_eq!(psd.groups().len(), 1); + + // Check layer outside group + let layer = psd.layer_by_name("Second Layer").unwrap(); + assert!(layer.parent_id().is_none()); + + // Check group + let group = group_by_name(&psd, "group"); + assert_eq!(group.id(), TOP_LEVEL_ID); + + // Check layer inside group + let layer = psd.layer_by_name("First Layer").unwrap(); + assert_eq!(layer.parent_id().unwrap(), group.id()); +} + +/// cargo test --test layer_and_mask_information_section two_groups_two_layers_inside -- --exact +#[test] +fn two_groups_two_layers_inside() { + let psd = include_bytes!("fixtures/groups/green-1x1-two-groups-two-layers-inside.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + // 2 group layer + assert_eq!(psd.groups().len(), 2); + + // Check first group + let group = group_by_name(&psd, "group"); + assert_eq!(group.id(), TOP_LEVEL_ID); + + // Check layer inside group + let layer = psd.layer_by_name("First Layer").unwrap(); + assert_eq!(layer.parent_id().unwrap(), group.id()); + + // Check second group + let group = group_by_name(&psd, "group2"); + assert_eq!(group.id(), TOP_LEVEL_ID + 1); +} + +/// +/// group structure +/// +---------------+----------+---------+ +/// | name | group_id | parent | +/// +---------------+----------+---------+ +/// | group inside | 2 | Some(1) | refers to 'group outside' +/// | group outside | 1 | None | +/// +------------------------------------+ +/// +/// layer structure +/// +-------------+-----+---------+ +/// | name | idx | parent | +/// +-------------+-----+---------+ +/// | First Layer | 0 | Some(1) | refers to 'group inside' +/// +-------------+-----+---------+ +#[test] +fn one_group_inside_another() { + let psd = include_bytes!("fixtures/groups/green-1x1-one-group-inside-another.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(psd.layers().len(), 1); + // parent group + children group + assert_eq!(psd.groups().len(), 2); + + // Check group + let group = group_by_name(&psd, "group outside"); + assert_eq!(group.id(), TOP_LEVEL_ID); + + // Check subgroup + let children_group = group_by_name(&psd, "group inside"); + assert_eq!(children_group.parent_id().unwrap(), group.id()); + + let layer = psd.layer_by_name("First Layer").unwrap(); + assert_eq!(children_group.id(), layer.parent_id().unwrap()); +} + +/// +/// PSD file structure +/// group: outside group, parent: `None` +/// group: first group inside, parent: `outside group` +/// layer: First Layer, parent: `first group inside` +/// +/// group: second group inside, parent: `outside group` +/// group: sub sub group, parent: `second group inside` +/// layer: Second Layer, parent: `sub sub group` +/// +/// layer: Third Layer, parent: `second group inside` +/// +/// group: third group inside, parent: `outside group` +/// +/// layer: Fourth Layer, parent: `outside group` +/// layer: Firth Layer, parent: `None` +/// +/// group: outside group 2, parent: `None` +/// layer: Sixth Layer, parent: `outside group 2` +/// +#[test] +fn one_group_with_two_subgroups() { + let psd = include_bytes!("fixtures/groups/green-1x1-one-group-with-two-subgroups.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(6, psd.layers().len()); + assert_eq!(6, psd.groups().len()); + + // Check first top-level group + let outside_group = group_by_name(&psd, "outside group"); + assert_eq!(outside_group.id(), 1); + + // Check first subgroup + let children_group = group_by_name(&psd, "first group inside"); + assert_eq!(children_group.parent_id().unwrap(), outside_group.id()); + + let layer = psd.layer_by_name("First Layer").unwrap(); + assert_eq!(children_group.id(), layer.parent_id().unwrap()); + + // Check second subgroup + let children_group = group_by_name(&psd, "second group inside"); + assert_eq!(children_group.parent_id().unwrap(), outside_group.id()); + + // Check `sub sub group` + let sub_sub_group = group_by_name(&psd, "sub sub group"); + assert_eq!(sub_sub_group.parent_id().unwrap(), children_group.id()); + + let layer = psd.layer_by_name("Second Layer").unwrap(); + assert_eq!(sub_sub_group.id(), layer.parent_id().unwrap()); + + let layer = psd.layer_by_name("Third Layer").unwrap(); + assert_eq!(children_group.id(), layer.parent_id().unwrap()); + + // Check third subgroup + let children_group = group_by_name(&psd, "third group inside"); + assert_eq!(children_group.parent_id().unwrap(), outside_group.id()); + + let layer = psd.layer_by_name("Fourth Layer").unwrap(); + assert_eq!(outside_group.id(), layer.parent_id().unwrap()); + + // Check top-level Firth Group + let layer = psd.layer_by_name("Firth Layer").unwrap(); + assert_eq!(layer.parent_id(), None); + + // Check second top-level group + let outside_group = group_by_name(&psd, "outside group 2"); + assert_eq!(outside_group.id(), 6); + + let layer = psd.layer_by_name("Sixth Layer").unwrap(); + assert_eq!(layer.parent_id().unwrap(), outside_group.id()); +} + +fn group_by_name<'a>(psd: &'a Psd, name: &str) -> &'a PsdGroup { + psd.groups() + .iter() + .find(|group| group.1.name() == name) + .unwrap() + .1 +} diff --git a/luda-editor/psd/tests/layer_groups.rs b/luda-editor/psd/tests/layer_groups.rs new file mode 100644 index 000000000..b5dc9bcd6 --- /dev/null +++ b/luda-editor/psd/tests/layer_groups.rs @@ -0,0 +1,141 @@ +use psd::{Psd, PsdGroup}; +const TOP_LEVEL_ID: u32 = 1; + +/// Verify that we can get a group by it's ID. +#[test] +fn group_by_id() { + let psd = include_bytes!("fixtures/groups/green-1x1-one-group-inside-another.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert!(psd.groups().get(&0).is_none()); + + assert_eq!(psd.group_ids_in_order(), &[2, 1]); + + assert_eq!(psd.groups().get(&1).unwrap().name(), "group outside"); + assert_eq!(psd.groups().get(&2).unwrap().name(), "group inside"); +} + +/// group structure +/// +---------------+----------+---------+ +/// | name | group_id | parent | +/// +---------------+----------+---------+ +/// | group inside | 2 | Some(1) | refers to 'group outside' +/// | group outside | 1 | None | +/// +------------------------------------+ +/// +/// layer structure +/// +-------------+-----+---------+ +/// | name | idx | parent | +/// +-------------+-----+---------+ +/// | First Layer | 0 | Some(1) | refers to 'group inside' +/// +-------------+-----+---------+ +/// +/// cargo test --test layer_and_mask_information_section one_group_inside_another -- --exact +#[test] +fn one_group_inside_another() { + let psd = include_bytes!("fixtures/groups/green-1x1-one-group-inside-another.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(psd.layers().len(), 1); + // parent group + children group + assert_eq!(psd.groups().len(), 2); + + // Check group + let group = group_by_name(&psd, "group outside"); + assert_eq!(group.id(), TOP_LEVEL_ID); + + // Check subgroup + let children_group = group_by_name(&psd, "group inside"); + assert_eq!(children_group.parent_id().unwrap(), group.id()); + + let layer = psd.layer_by_name("First Layer").unwrap(); + assert_eq!(children_group.id(), layer.parent_id().unwrap()); +} + +/// PSD file structure +/// group: outside group, parent: `None` +/// group: first group inside, parent: `outside group` +/// layer: First Layer, parent: `first group inside` +/// +/// group: second group inside, parent: `outside group` +/// group: sub sub group, parent: `second group inside` +/// layer: Second Layer, parent: `sub sub group` +/// +/// layer: Third Layer, parent: `second group inside` +/// +/// group: third group inside, parent: `outside group` +/// +/// layer: Fourth Layer, parent: `outside group` +/// layer: Firth Layer, parent: `None` +/// +/// group: outside group 2, parent: `None` +/// layer: Sixth Layer, parent: `outside group 2` +/// +/// cargo test --test layer_and_mask_information_section one_group_with_two_subgroups -- --exact +#[test] +fn one_group_with_two_subgroups() { + let psd = include_bytes!("fixtures/groups/green-1x1-one-group-with-two-subgroups.psd"); + let psd = Psd::from_bytes(psd).unwrap(); + + assert_eq!(6, psd.layers().len()); + assert_eq!(6, psd.groups().len()); + + // Check first top-level group + let outside_group = group_by_name(&psd, "outside group"); + assert_eq!(outside_group.id(), 1); + + // Check first subgroup + let children_group = group_by_name(&psd, "first group inside"); + assert_eq!(children_group.parent_id().unwrap(), outside_group.id()); + + let layer = psd.layer_by_name("First Layer").unwrap(); + assert_eq!(children_group.id(), layer.parent_id().unwrap()); + + // Check second subgroup + let children_group = group_by_name(&psd, "second group inside"); + assert_eq!(children_group.parent_id().unwrap(), outside_group.id()); + + // Check `sub sub group` + let sub_sub_group = group_by_name(&psd, "sub sub group"); + assert_eq!(sub_sub_group.parent_id().unwrap(), children_group.id()); + + let layer = psd.layer_by_name("Second Layer").unwrap(); + assert_eq!(sub_sub_group.id(), layer.parent_id().unwrap()); + + let layer = psd.layer_by_name("Third Layer").unwrap(); + assert_eq!(children_group.id(), layer.parent_id().unwrap()); + + // Check third subgroup + let children_group = group_by_name(&psd, "third group inside"); + assert_eq!(children_group.parent_id().unwrap(), outside_group.id()); + + let layer = psd.layer_by_name("Fourth Layer").unwrap(); + assert_eq!(outside_group.id(), layer.parent_id().unwrap()); + + // Check top-level Firth Group + let layer = psd.layer_by_name("Firth Layer").unwrap(); + assert_eq!(layer.parent_id(), None); + + // Check second top-level group + let outside_group = group_by_name(&psd, "outside group 2"); + assert_eq!(outside_group.id(), 6); + + let layer = psd.layer_by_name("Sixth Layer").unwrap(); + assert_eq!(layer.parent_id().unwrap(), outside_group.id()); +} + +/// Verify that we can properly load an RLEcompressed empty channel (caused by a group from GIMP) +#[test] +fn rle_compressed_empty_channel() { + let psd = include_bytes!("fixtures/groups/rle-compressed-empty-channel.psd"); + let psd = Psd::from_bytes(psd); + assert!(psd.is_ok()); +} + +fn group_by_name<'a>(psd: &'a Psd, name: &str) -> &'a PsdGroup { + psd.groups() + .iter() + .find(|group| group.1.name() == name) + .unwrap() + .1 +} diff --git a/luda-editor/psd/tests/slices_resource.rs b/luda-editor/psd/tests/slices_resource.rs new file mode 100644 index 000000000..1c4e7bb76 --- /dev/null +++ b/luda-editor/psd/tests/slices_resource.rs @@ -0,0 +1,74 @@ +use anyhow::Result; +use psd::{DescriptorField, ImageResource, Psd}; +use std::path::PathBuf; + +/// Verify that we properly read the name of a slices resources section. +/// +/// For a default PNG there is a slices resource section that has the same name of the PSD file. +/// +/// So a file with the name "123.psd" would have a slices resource named "123". +/// +/// So, by making fixture files with different name lengths we can verify that we properly parse +/// slice group names of different lengths. +/// +/// https://github.com/chinedufn/psd/pull/17 +/// https://github.com/chinedufn/psd/pull/18 +/// +/// cargo test --test slices_resource name_of_slices_resource_group -- --exact +#[test] +fn name_of_slices_resource_group() { + let fixtures = ["1.psd", "12.psd", "123.psd", "1234.psd"]; + + for fixture in fixtures.iter() { + let file = fixtures_dir().join(fixture); + let expected_slices_name = file.file_stem().unwrap().to_str().unwrap(); + + let psd = std::fs::read(&file).unwrap(); + let psd = Psd::from_bytes(&psd).unwrap(); + + match &psd.resources()[0] { + ImageResource::Slices(slices) => { + assert_eq!(slices.name().as_str(), expected_slices_name); + } + }; + } +} + +fn fixtures_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/slices-resource") +} + +/// cargo test --test slices_resource slices_v7_8 -- --exact +#[test] +fn slices_v7_8() -> Result<()> { + let psd = include_bytes!("./fixtures/slices-v8.psd"); + let psd = Psd::from_bytes(psd)?; + + match &psd.resources()[0] { + ImageResource::Slices(slices) => { + assert_eq!(slices.name().as_str(), "\u{0}"); + } + }; + + let descriptors = match &psd.resources()[0] { + ImageResource::Slices(s) => s.descriptors(), + }; + let descriptor = descriptors.get(0).unwrap(); + let bounds = descriptor.fields.get("bounds").unwrap(); + + if let DescriptorField::Descriptor(d) = bounds { + match d.fields.get("Rght").unwrap() { + DescriptorField::Integer(v) => assert_eq!(*v, 1), + _ => panic!("expected integer"), + } + + match d.fields.get("Btom").unwrap() { + DescriptorField::Integer(v) => assert_eq!(*v, 1), + _ => panic!("expected integer"), + } + } else { + panic!("expected descriptor"); + } + + Ok(()) +} diff --git a/luda-editor/psd/tests/transparency.rs b/luda-editor/psd/tests/transparency.rs new file mode 100644 index 000000000..156780054 --- /dev/null +++ b/luda-editor/psd/tests/transparency.rs @@ -0,0 +1,138 @@ +use anyhow::{anyhow, Result}; +use psd::Psd; +use psd::PsdChannelCompression; +use psd::PsdChannelKind; +use std::collections::HashMap; + +const RED_PIXEL: [u8; 4] = [255, 0, 0, 255]; +// const GREEN_PIXEL: [u8; 4] = [0, 255, 0, 255]; +const BLUE_PIXEL: [u8; 4] = [0, 0, 255, 255]; + +// Transparent pixels in the image data section start [255, 255, 255, 0] +// const TRANSPARENT_PIXEL_IMAGE_DATA: [u8; 4] = [255, 255, 255, 0]; + +// In the layer and mask info section we fill in transparent rgba pixels ourselves as [0, 0, 0, 0] +// const TRANSPARENT_PIXEL_LAYER: [u8; 4] = [0, 0, 0, 0]; + +// Test that images that have transparent pixels and don't use compression +// return the correct RGBA +#[test] +fn transparency_raw_data() -> Result<()> { + let psd = include_bytes!("./fixtures/3x3-opaque-center.psd"); + let psd = Psd::from_bytes(psd)?; + + let blue_pixels = vec![(1, 1, BLUE_PIXEL), (2, 0, BLUE_PIXEL)]; + + assert_colors(psd.rgba(), &psd, &blue_pixels); + + assert_colors( + psd.layer_by_name("OpaqueCenter") + .ok_or(anyhow!("layer not found"))? + .rgba(), + &psd, + &blue_pixels, + ); + + Ok(()) +} + +// Test that images that have transparent pixels and use rle compression +// return the correct RGBA +#[test] +fn transparency_rle_compressed() -> Result<()> { + let psd = include_bytes!("./fixtures/16x16-rle-partially-opaque.psd"); + let psd = Psd::from_bytes(psd)?; + + let mut red_block = vec![]; + for left in 0..9 { + for top in 0..9 { + red_block.push((left + 1, top + 1, RED_PIXEL)); + } + } + + assert_eq!(psd.compression(), &PsdChannelCompression::RleCompressed); + + assert_colors(psd.rgba(), &psd, &red_block); + + assert_eq!( + psd.layer_by_name("OpaqueCenter") + .ok_or(anyhow!("layer not found"))? + .compression(PsdChannelKind::Red)?, + PsdChannelCompression::RleCompressed + ); + + assert_colors( + psd.layer_by_name("OpaqueCenter") + .ok_or(anyhow!("layer not found"))? + .rgba(), + &psd, + &red_block, + ); + + Ok(()) +} + +// Fixes an `already borrowed: BorrowMutError` that we were getting in the `flattened_pixel` +// method when we were recursing into the method and trying to borrow when we'd already borrowed. +#[test] +fn transparent_above_opaque() -> Result<()> { + let psd = include_bytes!("./fixtures/transparent-above-opaque.psd"); + let psd = Psd::from_bytes(psd)?; + + let image = psd.flatten_layers_rgba(&|_| true)?; + + assert_eq!(image[0..4], BLUE_PIXEL); + + Ok(()) +} + +// Ensure that the specified, zero-indexed left, top coordinate has the provided pixel color. +// Otherwise it should be fully transparent. +// (left, top, pixel) +fn assert_colors(image: Vec, psd: &Psd, assertions: &[(usize, usize, [u8; 4])]) { + let pixel_count = (psd.width() * psd.height()) as usize; + let width = psd.width() as usize; + + let mut asserts = HashMap::new(); + for assertion in assertions { + asserts.insert((assertion.0, assertion.1), assertion.2); + } + + for idx in 0..pixel_count { + let left = idx % width; + let top = idx / width; + + let pixel_color = &image[idx * 4..idx * 4 + 4]; + + match asserts.get(&(left, top)) { + Some(expected_color) => { + assert_eq!(expected_color, pixel_color); + } + None => { + assert_eq!(pixel_color[3], 0, "Pixel should be transparent"); + } + }; + } +} + +fn make_image(pixel: [u8; 4], pixel_count: u32) -> Vec { + let pixel_count = pixel_count as usize; + let mut image = vec![0; pixel_count * 4]; + + for idx in 0..pixel_count { + image[idx * 4] = pixel[0]; + image[idx * 4 + 1] = pixel[1]; + image[idx * 4 + 2] = pixel[2]; + image[idx * 4 + 3] = pixel[3]; + } + + image +} + +fn put_pixel(image: &mut Vec, width: usize, left: usize, top: usize, new: [u8; 4]) { + let idx = (top * width) + left; + image[idx * 4] = new[0]; + image[idx * 4 + 1] = new[1]; + image[idx * 4 + 2] = new[2]; + image[idx * 4 + 3] = new[3]; +} diff --git a/luda-editor/server/server-bin/Cargo.lock b/luda-editor/server/server-bin/Cargo.lock index a38ff6eb2..4aabde28b 100644 --- a/luda-editor/server/server-bin/Cargo.lock +++ b/luda-editor/server/server-bin/Cargo.lock @@ -1678,8 +1678,6 @@ dependencies = [ [[package]] name = "psd" version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1316f4ee59cdc95fc98e4e892a7edacaf1575620ca9e96abd421a2766fe45984" dependencies = [ "thiserror", ] diff --git a/luda-editor/server/server-core/Cargo.lock b/luda-editor/server/server-core/Cargo.lock index f41d2e38d..8a6ba538d 100644 --- a/luda-editor/server/server-core/Cargo.lock +++ b/luda-editor/server/server-core/Cargo.lock @@ -1678,8 +1678,6 @@ dependencies = [ [[package]] name = "psd" version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1316f4ee59cdc95fc98e4e892a7edacaf1575620ca9e96abd421a2766fe45984" dependencies = [ "thiserror", ] diff --git a/luda-editor/server/server-core/Cargo.toml b/luda-editor/server/server-core/Cargo.toml index 6331448d6..56ae3cf22 100644 --- a/luda-editor/server/server-core/Cargo.toml +++ b/luda-editor/server/server-core/Cargo.toml @@ -23,7 +23,7 @@ env_logger = "0.10" aws-smithy-async = "0.47" migration = { path = "../../migration" } document-macro = { path = "../document-macro" } -psd = "0.3" +psd = { path = "../../psd" } libwebp-sys = { version = "0.9", features = ["avx2", "sse41", "neon"] } rayon = "1.7.0" image = "0.24.6" diff --git a/luda-editor/server/server-core/src/services/cg/layer_tree.rs b/luda-editor/server/server-core/src/services/cg/layer_tree.rs new file mode 100644 index 000000000..b0db1eb2b --- /dev/null +++ b/luda-editor/server/server-core/src/services/cg/layer_tree.rs @@ -0,0 +1,499 @@ +use image::Rgba; +use std::collections::VecDeque; + +pub enum LayerTree<'psd> { + Group { + item: &'psd psd::PsdGroup, + children: Vec>, + }, + Layer { + item: &'psd psd::PsdLayer, + }, +} +impl LayerTree<'_> { + pub fn name(&self) -> &str { + match self { + LayerTree::Group { item, .. } => item.name(), + LayerTree::Layer { item } => item.name(), + } + } + pub fn has_no_selection(&self) -> bool { + match self { + LayerTree::Group { .. } => { + let mut child_group_queue = VecDeque::new(); + child_group_queue.push_back(self); + while let Some(child_group) = child_group_queue.pop_front() { + match child_group { + LayerTree::Group { item, children } => { + let name = item.name(); + let child_has_selection_group = + name.ends_with("_s") || name.ends_with("_m"); + if child_has_selection_group { + return false; + } + + for child_group in children.iter().filter(|child| match child { + LayerTree::Group { .. } => true, + LayerTree::Layer { .. } => false, + }) { + child_group_queue.push_back(child_group) + } + } + LayerTree::Layer { .. } => unreachable!("It should be group"), + } + } + return true; + } + LayerTree::Layer { .. } => true, + } + } + pub fn is_clipping(&self) -> bool { + match self { + LayerTree::Group { item, .. } => item.is_clipping_mask(), + LayerTree::Layer { item } => item.is_clipping_mask(), + } + } + fn calculate_alpha(&self, force_visible: bool) -> f32 { + let (visible, opacity) = match self { + LayerTree::Group { item, .. } => (!item.visible(), item.opacity()), + LayerTree::Layer { item } => (!item.visible(), item.opacity()), + }; + match force_visible || visible { + true => (opacity as f32) / 255.0, + false => 0.0, + } + } + fn blend_mode(&self) -> psd::BlendMode { + match self { + LayerTree::Group { item, .. } => item.blend_mode(), + LayerTree::Layer { item } => item.blend_mode(), + } + } + fn get_image_buffer( + &self, + psd: &psd::Psd, + ) -> (i32, i32, image::ImageBuffer, Vec>) { + match self { + LayerTree::Group { children, .. } => { + let group_render_result = render_layer_tree(psd, children, false); + ( + group_render_result.x, + group_render_result.y, + group_render_result.image_buffer, + ) + } + LayerTree::Layer { item } => { + let image_buffer = { + let whole_layer_image_buffer = + image::ImageBuffer::, Vec>::from_vec( + psd.width() as u32, + psd.height() as u32, + item.rgba(), + ) + .expect("Failed to create image buffer"); + let mut cropped_layer_image_buffer = + image::ImageBuffer::, Vec>::new( + item.width() as u32, + item.height() as u32, + ); + let cropped_x = item.layer_left(); + let cropped_y = item.layer_top(); + for y in cropped_y..cropped_y + item.height() as i32 { + for x in cropped_x..cropped_x + item.width() as i32 { + let cropped_pixel = get_pixel(&whole_layer_image_buffer, x, y); + cropped_layer_image_buffer.put_pixel( + (x - cropped_x) as u32, + (y - cropped_y) as u32, + cropped_pixel, + ); + } + } + cropped_layer_image_buffer + }; + (item.layer_left(), item.layer_top(), image_buffer) + } + } + } +} +pub fn make_tree<'psd>(psd: &'psd psd::Psd) -> anyhow::Result>> { + let mut tree = vec![]; + + fn open_group_children<'psd, 'tree>( + psd: &'psd psd::Psd, + tree: &'tree mut Vec>, + group_id: Option, + ) -> anyhow::Result<&'tree mut Vec>> { + let group_ids_bottom_to_top = { + let mut group_ids_bottom_to_top = vec![]; + let mut group = group_id.and_then(|group_id| psd.groups().get(&group_id)); + while let Some(group_) = group { + group_ids_bottom_to_top.push(group_.id()); + group = group_ + .parent_id() + .and_then(|parent_id| psd.groups().get(&parent_id)); + } + group_ids_bottom_to_top + }; + + let group_children = group_ids_bottom_to_top + .iter() + .rev() + .fold(tree, |tree, group_id| { + let group_tree = if let Some(group_index) = + tree.iter_mut().position(|tree| match tree { + LayerTree::Group { item, .. } => item.id() == *group_id, + LayerTree::Layer { .. } => false, + }) { + &mut tree[group_index] + } else { + tree.push(LayerTree::Group { + item: psd.groups().get(group_id).expect("No group exist"), + children: vec![], + }); + tree.last_mut().unwrap() + }; + + match group_tree { + LayerTree::Group { children, .. } => children, + LayerTree::Layer { .. } => unreachable!("It should be group"), + } + }); + Ok(group_children) + } + + for layer in psd.layers() { + let group_children = open_group_children(psd, &mut tree, layer.parent_id())?; + group_children.push(LayerTree::Layer { item: layer }) + } + + Ok(tree) +} + +pub(crate) struct RenderResult { + pub(crate) x: i32, + pub(crate) y: i32, + pub(crate) image_buffer: image::ImageBuffer, Vec>, +} +pub(crate) fn render_layer_tree<'psd>( + psd: &'psd psd::Psd, + layer_tree: &Vec>, + force_visible: bool, +) -> RenderResult { + let mut layer_tree_iter = layer_tree.iter().rev().peekable(); + let mut bottom: Option<(i32, i32, image::ImageBuffer, Vec>)> = None; + while let Some(upper_layer_tree) = layer_tree_iter.next() { + assert!(upper_layer_tree.is_clipping()); + let (mut upper_x, mut upper_y, upper_blend_mode, mut upper_image_buffer) = { + let (x, y, mut image_buffer) = upper_layer_tree.get_image_buffer(psd); + apply_alpha( + &mut image_buffer, + upper_layer_tree.calculate_alpha(force_visible), + ); + (x, y, upper_layer_tree.blend_mode(), image_buffer) + }; + + 'blend_clipping_layers_into_upper_layer: while let Some(clipping_layer_tree) = + layer_tree_iter.peek() + { + if clipping_layer_tree.is_clipping() { + break 'blend_clipping_layers_into_upper_layer; + } + + let (clipping_x, clipping_y, clipping_image_buffer) = { + let (x, y, image_buffer) = clipping_layer_tree.get_image_buffer(psd); + let RenderResult { + x, + y, + mut image_buffer, + } = clip_buffer(x, y, &image_buffer, upper_x, upper_y, &upper_image_buffer); + apply_alpha( + &mut image_buffer, + clipping_layer_tree.calculate_alpha(false), + ); + (x, y, image_buffer) + }; + + let RenderResult { + x: blended_x, + y: blended_y, + image_buffer: blended_image_buffer, + } = blend_buffer( + clipping_x, + clipping_y, + &clipping_image_buffer, + upper_x, + upper_y, + &upper_image_buffer, + clipping_layer_tree.blend_mode(), + ); + + upper_x = blended_x; + upper_y = blended_y; + upper_image_buffer = blended_image_buffer; + layer_tree_iter.next(); + } + + match bottom { + Some((bottom_x, bottom_y, bottom_image_buffer)) => { + let RenderResult { + x: blended_x, + y: blended_y, + image_buffer: blended_image_buffer, + } = blend_buffer( + upper_x, + upper_y, + &upper_image_buffer, + bottom_x, + bottom_y, + &bottom_image_buffer, + upper_blend_mode, + ); + + bottom = Some((blended_x, blended_y, blended_image_buffer)); + } + None => { + bottom = Some((upper_x, upper_y, upper_image_buffer)); + } + } + } + + match bottom { + Some((x, y, image_buffer)) => RenderResult { x, y, image_buffer }, + None => RenderResult { + x: 0, + y: 0, + image_buffer: image::ImageBuffer::, Vec>::new(0, 0), + }, + } +} + +fn clip_buffer( + source_x: i32, + source_y: i32, + source_image_buffer: &image::ImageBuffer, Vec>, + destination_x: i32, + destination_y: i32, + destination_image_buffer: &image::ImageBuffer, Vec>, +) -> RenderResult { + let source_rect = namui_type::Rect::Xywh { + x: source_x, + y: source_y, + width: source_image_buffer.width() as i32, + height: source_image_buffer.height() as i32, + }; + let destination_rect = namui_type::Rect::Xywh { + x: destination_x, + y: destination_y, + width: destination_image_buffer.width() as i32, + height: destination_image_buffer.height() as i32, + }; + let clipped_rect = source_rect.intersect(destination_rect).unwrap_or_default(); + let mut clipped_image_buffer = image::ImageBuffer::, Vec>::new( + clipped_rect.width() as u32, + clipped_rect.height() as u32, + ); + for y in clipped_rect.y()..clipped_rect.y() + clipped_rect.height() { + for x in clipped_rect.x()..clipped_rect.x() + clipped_rect.width() { + let mut source_pixel = get_pixel(source_image_buffer, x - source_x, y - source_y); + let destination_alpha = get_pixel( + destination_image_buffer, + x - destination_x, + y - destination_y, + ) + .0[3] as f32 + / 255.0; + source_pixel.0[3] = (source_pixel.0[3] as f32 * destination_alpha) as u8; + clipped_image_buffer.put_pixel( + (x - clipped_rect.x()) as u32, + (y - clipped_rect.y()) as u32, + source_pixel, + ) + } + } + RenderResult { + x: clipped_rect.x(), + y: clipped_rect.y(), + image_buffer: clipped_image_buffer, + } +} + +fn blend_buffer( + source_x: i32, + source_y: i32, + source_image_buffer: &image::ImageBuffer, Vec>, + destination_x: i32, + destination_y: i32, + destination_image_buffer: &image::ImageBuffer, Vec>, + blend_mode: psd::BlendMode, +) -> RenderResult { + let source_rect = namui_type::Rect::Xywh { + x: source_x, + y: source_y, + width: source_image_buffer.width() as i32, + height: source_image_buffer.height() as i32, + }; + let destination_rect = namui_type::Rect::Xywh { + x: destination_x, + y: destination_y, + width: destination_image_buffer.width() as i32, + height: destination_image_buffer.height() as i32, + }; + let blended_rect = source_rect.get_minimum_rectangle_containing(destination_rect); + let mut blended_image_buffer = image::ImageBuffer::, Vec>::new( + blended_rect.width() as u32, + blended_rect.height() as u32, + ); + let blend_function = blend_function(blend_mode); + for y in blended_rect.y()..blended_rect.y() + blended_rect.height() { + for x in blended_rect.x()..blended_rect.x() + blended_rect.width() { + let source_pixel = get_pixel(source_image_buffer, x - source_x, y - source_y); + let destination_pixel = get_pixel( + destination_image_buffer, + x - destination_x, + y - destination_y, + ); + let blended_pixel = blend_pixel(source_pixel, destination_pixel, blend_function); + blended_image_buffer.put_pixel( + (x - blended_rect.x()) as u32, + (y - blended_rect.y()) as u32, + blended_pixel, + ); + } + } + RenderResult { + x: blended_rect.x(), + y: blended_rect.y(), + image_buffer: blended_image_buffer, + } +} + +pub(crate) fn get_pixel( + image_buffer: &image::ImageBuffer, Vec>, + x: i32, + y: i32, +) -> Rgba { + if x < 0 || y < 0 || x >= image_buffer.width() as i32 || y >= image_buffer.height() as i32 { + Rgba([0, 0, 0, 0]) + } else { + image_buffer.get_pixel(x as u32, y as u32).clone() + } +} + +fn apply_alpha(image_buffer: &mut image::ImageBuffer, Vec>, alpha: f32) { + for pixel in image_buffer.pixels_mut() { + pixel.0[3] = (pixel.0[3] as f32 * alpha) as u8; + } +} + +fn blend_pixel( + source_pixel: Rgba, + destination_pixel: Rgba, + blend_function: BlendFunction, +) -> Rgba { + fn alpha_blend( + source_color: f32, + source_alpha: f32, + destination_color: f32, + destination_alpha: f32, + blend_function: BlendFunction, + blended_alpha: f32, + ) -> u8 { + let blended_source_color = (1.0 - destination_alpha) * source_color + + destination_alpha * blend_function(source_color, destination_color); + let blended_color = source_alpha * blended_source_color + + destination_alpha * destination_color * (1.0 - source_alpha); + ((blended_color / blended_alpha).clamp(0.0, 1.0) * 255.0).round() as u8 + } + + let source_alpha = source_pixel.0[3] as f32 / 255.0; + let destination_alpha = destination_pixel.0[3] as f32 / 255.0; + let blended_alpha = source_alpha + destination_alpha * (1.0 - source_alpha); + Rgba([ + alpha_blend( + source_pixel.0[0] as f32 / 255.0, + source_alpha, + destination_pixel.0[0] as f32 / 255.0, + destination_alpha, + blend_function, + blended_alpha, + ), + alpha_blend( + source_pixel.0[1] as f32 / 255.0, + source_alpha, + destination_pixel.0[1] as f32 / 255.0, + destination_alpha, + blend_function, + blended_alpha, + ), + alpha_blend( + source_pixel.0[2] as f32 / 255.0, + source_alpha, + destination_pixel.0[2] as f32 / 255.0, + destination_alpha, + blend_function, + blended_alpha, + ), + (blended_alpha.clamp(0.0, 1.0) * 255.0).round() as u8, + ]) +} + +type BlendFunction = fn(f32, f32) -> f32; +fn blend_function(blend_mode: psd::BlendMode) -> BlendFunction { + fn normal(source: f32, _destination: f32) -> f32 { + source + } + fn multiply(source: f32, destination: f32) -> f32 { + source * destination + } + fn linear_burn(source: f32, destination: f32) -> f32 { + (source + destination - 1.0).max(0.0) + } + fn screen(source: f32, destination: f32) -> f32 { + 1.0 - (1.0 - source) * (1.0 - destination) + } + fn linear_dodge(source: f32, destination: f32) -> f32 { + source + destination + } + fn overlay(source: f32, destination: f32) -> f32 { + match destination < 0.5 { + true => 2.0 * source * destination, + false => 1.0 - (2.0 * (1.0 - source) * (1.0 - destination)), + } + } + fn hard_light(source: f32, destination: f32) -> f32 { + match source < 0.5 { + true => 2.0 * source * destination, + false => 1.0 - (2.0 * (1.0 - source) * (1.0 - destination)), + } + } + match blend_mode { + psd::BlendMode::PassThrough => todo!(), + psd::BlendMode::Normal => normal, + psd::BlendMode::Dissolve => todo!(), + psd::BlendMode::Darken => todo!(), + psd::BlendMode::Multiply => multiply, + psd::BlendMode::ColorBurn => todo!(), + psd::BlendMode::LinearBurn => linear_burn, + psd::BlendMode::DarkerColor => todo!(), + psd::BlendMode::Lighten => todo!(), + psd::BlendMode::Screen => screen, + psd::BlendMode::ColorDodge => todo!(), + psd::BlendMode::LinearDodge => linear_dodge, + psd::BlendMode::LighterColor => todo!(), + psd::BlendMode::Overlay => overlay, + psd::BlendMode::SoftLight => todo!(), + psd::BlendMode::HardLight => hard_light, + psd::BlendMode::VividLight => todo!(), + psd::BlendMode::LinearLight => todo!(), + psd::BlendMode::PinLight => todo!(), + psd::BlendMode::HardMix => todo!(), + psd::BlendMode::Difference => todo!(), + psd::BlendMode::Exclusion => todo!(), + psd::BlendMode::Subtract => todo!(), + psd::BlendMode::Divide => todo!(), + psd::BlendMode::Hue => todo!(), + psd::BlendMode::Saturation => todo!(), + psd::BlendMode::Color => todo!(), + psd::BlendMode::Luminosity => todo!(), + } +} diff --git a/luda-editor/server/server-core/src/services/cg/mod.rs b/luda-editor/server/server-core/src/services/cg/mod.rs index cbd18fc74..393fe17a8 100644 --- a/luda-editor/server/server-core/src/services/cg/mod.rs +++ b/luda-editor/server/server-core/src/services/cg/mod.rs @@ -1,4 +1,5 @@ pub mod documents; +mod layer_tree; mod parse_psd_to_inter_cg_parts; mod psd_to_cg_file; diff --git a/luda-editor/server/server-core/src/services/cg/parse_psd_to_inter_cg_parts.rs b/luda-editor/server/server-core/src/services/cg/parse_psd_to_inter_cg_parts.rs index 41ac665a2..0d580418f 100644 --- a/luda-editor/server/server-core/src/services/cg/parse_psd_to_inter_cg_parts.rs +++ b/luda-editor/server/server-core/src/services/cg/parse_psd_to_inter_cg_parts.rs @@ -1,3 +1,4 @@ +use super::layer_tree::{make_tree, LayerTree}; use rpc::data::PartSelectionType; pub struct InterCgPart<'a> { @@ -9,80 +10,115 @@ pub struct InterCgPart<'a> { pub struct InterCgVariant<'a> { pub part_name: String, pub variant_name: String, - pub layers: Vec<&'a psd::PsdLayer>, + pub layer_tree: Vec>, } pub fn parse_psd_to_inter_cg_parts<'a>(psd: &'a psd::Psd) -> Vec> { - let mut parts: Vec> = vec![]; - for layer in psd.layers() { - let full_names = layer_full_names(psd, layer); - - let (split_index, selection_type) = full_names - .iter() - .enumerate() - .find_map(|(index, name)| { - if name.ends_with("_s") { - Some((index, PartSelectionType::Single)) - } else if name.ends_with("_m") { - Some((index, PartSelectionType::Multi)) - } else if full_names.len() - 1 == index { - Some((index, PartSelectionType::AlwaysOn)) - } else { - None - } - }) - .expect(format!("fail to parse psd {:?}", full_names).as_str()); + let layer_tree = make_tree(psd).expect("Failed to make tree"); + let parts = create_inter_cg_part_from_layer_tree(layer_tree, vec![]); + parts +} - let part_name = full_names[..split_index + 1].join("."); - let variant_name = full_names - .get(split_index + 1) - .unwrap_or(&"".to_string()) - .to_string(); +fn create_inter_cg_part_from_layer_tree<'psd>( + layer_tree: Vec>, + layer_full_names: Vec, +) -> Vec> { + let mut merging_layer_tree = vec![]; + let mut parts = vec![]; - if let Some(part) = parts.iter_mut().find(|x| x.part_name == part_name) { - if let Some(variant) = part - .variants - .iter_mut() - .find(|x| x.variant_name == variant_name) - { - variant.layers.push(layer); - } else { - part.variants.push(InterCgVariant { - part_name, - variant_name, - layers: vec![layer], - }); - } - } else { - parts.push(InterCgPart { - part_name: part_name.clone(), - selection_type, - variants: vec![InterCgVariant { - part_name, - variant_name, - layers: vec![layer], - }], - }); + fn push_merging_layer_tree_as_always_on_part<'psd>( + merging_layer_tree: &mut Vec>, + parts: &mut Vec>, + layer_full_names: &Vec, + ) { + if merging_layer_tree.is_empty() { + return; } + let part_name = layer_full_names.join("."); + let variant_name = merging_layer_tree + .iter() + .map(|layer_tree| layer_tree.name()) + .fold(String::new(), |acc, name| format!("{acc}_{name}")); + let layer_tree = { + let mut result = vec![]; + result.append(merging_layer_tree); + result + }; + parts.push(InterCgPart { + part_name: part_name.clone(), + selection_type: PartSelectionType::AlwaysOn, + variants: vec![InterCgVariant { + part_name, + variant_name, + layer_tree, + }], + }); } - parts -} + for layer_tree in layer_tree { + if layer_tree.has_no_selection() { + merging_layer_tree.push(layer_tree); + continue; + } + push_merging_layer_tree_as_always_on_part( + &mut merging_layer_tree, + &mut parts, + &layer_full_names, + ); + match layer_tree { + LayerTree::Group { item, children } => { + let name = item.name(); + if name.ends_with("_s") || name.ends_with("_m") { + let selection_type = if name.ends_with("_s") { + PartSelectionType::Single + } else { + PartSelectionType::Multi + }; + let mut layer_full_names = layer_full_names.clone(); + layer_full_names.push(name.to_string()); + let part_name = layer_full_names.join("."); + let mut variants = vec![]; + let mut layer_tree = vec![]; + for child in children { + let clipping = !child.is_clipping(); + let variant_name = child.name().to_string(); + layer_tree.push(child); + if clipping { + continue; + } + variants.push(InterCgVariant { + part_name: part_name.clone(), + variant_name, + layer_tree, + }); + layer_tree = vec![]; + } -fn layer_full_names(psd: &psd::Psd, layer: &psd::PsdLayer) -> Vec { - let mut parent_group_id = layer.parent_id(); - let mut group_names = vec![layer.name().to_string()]; + parts.push(InterCgPart { + part_name, + selection_type, + variants, + }); + continue; + } - while let Some(group_id) = parent_group_id { - let (_, parent_group) = psd - .groups() - .into_iter() - .find(|(x, _)| **x == group_id) - .unwrap(); - group_names.push(parent_group.name().to_string()); - parent_group_id = parent_group.parent_id(); + let mut layer_full_names = layer_full_names.clone(); + layer_full_names.push(name.to_string()); + parts.append(&mut create_inter_cg_part_from_layer_tree( + children, + layer_full_names, + )); + } + LayerTree::Layer { .. } => { + unreachable!("It might be group") + } + } } + push_merging_layer_tree_as_always_on_part( + &mut merging_layer_tree, + &mut parts, + &layer_full_names, + ); - group_names.reverse(); - group_names + parts } diff --git a/luda-editor/server/server-core/src/services/cg/psd_to_cg_file.rs b/luda-editor/server/server-core/src/services/cg/psd_to_cg_file.rs index 9f8be8223..6ef26d8e7 100644 --- a/luda-editor/server/server-core/src/services/cg/psd_to_cg_file.rs +++ b/luda-editor/server/server-core/src/services/cg/psd_to_cg_file.rs @@ -1,5 +1,8 @@ -use super::{parse_psd_to_inter_cg_parts::InterCgVariant, *}; -use image::ImageBuffer; +use super::{ + layer_tree::{render_layer_tree, RenderResult}, + parse_psd_to_inter_cg_parts::InterCgVariant, + *, +}; use libwebp_sys::WebPEncodeLosslessRGBA; use namui_type::*; use rayon::prelude::*; @@ -159,73 +162,24 @@ fn inter_cg_variant_to_cg_variant_and_image_buffer( let width = psd.width(); let height = psd.height(); - let rect_in_pixel = { - let rect = inter_cg_variant.layers.iter().fold( - Rect::::Ltrb { - left: width as i32, - top: height as i32, - right: 0, - bottom: 0, - }, - |acc, layer| { - let rect = Rect::Xywh { - x: layer.layer_left(), - y: layer.layer_top(), - width: layer.width() as i32, - height: layer.height() as i32, - }; - acc.get_minimum_rectangle_containing(rect) - }, - ); - Rect::Xywh { - x: rect.x().px(), - y: rect.y().px(), - width: rect.width().px(), - height: rect.height().px(), - } - }; - let rect_in_percent: Rect = Rect::Xywh { - x: (100.0 * rect_in_pixel.x().as_f32() / width as f32).percent(), - y: (100.0 * rect_in_pixel.y().as_f32() / height as f32).percent(), - width: (100.0 * rect_in_pixel.width().as_f32() / width as f32).percent(), - height: (100.0 * rect_in_pixel.height().as_f32() / height as f32).percent(), - }; - - let images = inter_cg_variant - .layers - .into_par_iter() - .map(|layer| image::ImageBuffer::from_vec(width, height, layer.rgba()).unwrap()) - .collect::>(); - - let mut bottom = image::ImageBuffer::, _>::new(width, height); - for image in images.into_iter().rev() { - image::imageops::overlay(&mut bottom, &image, 0, 0); - } - - let cropped = { - let mut cropped = ImageBuffer::, _>::new( - rect_in_pixel.width().as_f32() as u32, - rect_in_pixel.height().as_f32() as u32, - ); - image::imageops::overlay( - &mut cropped, - &bottom, - -rect_in_pixel.x().as_f32() as i64, - -rect_in_pixel.y().as_f32() as i64, - ); - cropped - }; + let RenderResult { x, y, image_buffer } = + render_layer_tree(psd, &inter_cg_variant.layer_tree, true); ( CgPartVariant { id, name: inter_cg_variant.variant_name, - rect: rect_in_percent, + rect: Rect::Xywh { + x: (100.0 * x as f32 / width as f32).percent(), + y: (100.0 * y as f32 / height as f32).percent(), + width: (100.0 * image_buffer.width() as f32 / width as f32).percent(), + height: (100.0 * image_buffer.height() as f32 / height as f32).percent(), + }, }, VariantImageBuffer { variant_id: id, - image_buffer: cropped, - xy: rect_in_pixel.xy(), + image_buffer, + xy: Xy::new(x.px(), y.px()), }, ) }