From 72243d9eeb8d7be50c54e35908d4d4aab7b451ce Mon Sep 17 00:00:00 2001 From: nyanpasu64 Date: Thu, 16 Jun 2022 05:37:49 -0700 Subject: [PATCH 1/6] Fix incorrect documentation for syntax directory (#1901) Currently the Configuration docs says to load syntax files into config.toml [markdown] extra_syntaxes. However, ever since commit 23064f57c8 (released in Zola v0.15.0), the extra_syntaxes property was replaced by extra_syntaxes_and_themes, and used as both syntax and color theme search paths. Following the docs and trying to set the extra_syntaxes property does nothing, and #1723 ran into this issue. Change the docs to consistently reference extra_syntaxes_and_themes. --- docs/content/documentation/getting-started/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md index 4ddaac2822..f8f2d473fc 100644 --- a/docs/content/documentation/getting-started/configuration.md +++ b/docs/content/documentation/getting-started/configuration.md @@ -93,8 +93,8 @@ build_search_index = false # When set to "true", all code blocks are highlighted. highlight_code = false -# A list of directories used to search for additional `.sublime-syntax` files. -extra_syntaxes = [] +# A list of directories used to search for additional `.sublime-syntax` and `.tmTheme` files. +extra_syntaxes_and_themes = [] # The theme to use for code highlighting. # See below for list of allowed values. From afc6a71a79346a4a21ca5d0c6c410397c6adddf5 Mon Sep 17 00:00:00 2001 From: Phil Lord Date: Tue, 21 Jun 2022 14:53:07 +0100 Subject: [PATCH 2/6] Allow new lines before anchor (#1905) Previously the heuristic check for links required spaces before the attribute to ensure that attributes suffixed with `id` were not identified. This has now been expanded to any white space character to enable the `id` attribute to start on a new line. --- components/utils/src/anchors.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/utils/src/anchors.rs b/components/utils/src/anchors.rs index 642e58937f..06af56b47b 100644 --- a/components/utils/src/anchors.rs +++ b/components/utils/src/anchors.rs @@ -6,7 +6,7 @@ pub fn has_anchor_id(content: &str, anchor: &str) -> bool { } fn anchor_id_checks(anchor: &str) -> Regex { - Regex::new(&format!(r#" (?i)(id|name) *= *("|')*{}("|'| |>)+"#, anchor)).unwrap() + Regex::new(&format!(r#"\s(?i)(id|name) *= *("|')*{}("|'| |>)+"#, anchor)).unwrap() } #[cfg(test)] @@ -39,5 +39,12 @@ mod tests { // Case variants assert!(m(r#""#)); assert!(m(r#""#)); + + // Newline variants + assert!(m(r#""#)); + + // Non matchers + assert!(!m(r#""#)) } } From 4f6a1c6bcc4a609b6d27b8d2a4f309dd25b40079 Mon Sep 17 00:00:00 2001 From: Phil Lord Date: Tue, 21 Jun 2022 18:08:16 +0100 Subject: [PATCH 3/6] Properly escape anchor names (#1908) --- components/utils/src/anchors.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/components/utils/src/anchors.rs b/components/utils/src/anchors.rs index 06af56b47b..fe86e96082 100644 --- a/components/utils/src/anchors.rs +++ b/components/utils/src/anchors.rs @@ -1,4 +1,5 @@ use libs::regex::Regex; +use libs::regex::escape; pub fn has_anchor_id(content: &str, anchor: &str) -> bool { let checks = anchor_id_checks(anchor); @@ -6,7 +7,8 @@ pub fn has_anchor_id(content: &str, anchor: &str) -> bool { } fn anchor_id_checks(anchor: &str) -> Regex { - Regex::new(&format!(r#"\s(?i)(id|name) *= *("|')*{}("|'| |>)+"#, anchor)).unwrap() + Regex::new(&format!(r#"\s(?i)(id|name) *= *("|')*{}("|'| |>)+"#, + escape(anchor))).unwrap() } #[cfg(test)] @@ -44,7 +46,11 @@ mod tests { assert!(m(r#""#)); + // Escaped Anchors + assert!(check("fred?george", r#""#)); + assert!(check("fred.george", r#""#)); + // Non matchers - assert!(!m(r#""#)) + assert!(!m(r#""#)); } } From 18e8246fbc25bcb01677bd94909c46eb66bd0390 Mon Sep 17 00:00:00 2001 From: Arnaud Grandville Date: Thu, 23 Jun 2022 19:36:33 +0200 Subject: [PATCH 4/6] fix windows EOL in markdown files (#1911) * add a test * CodeBlock text may be split on multiple parts The CodeBlock text events are now concatenated and processed in a single stream at the end. cf https://github.com/raphlinus/pulldown-cmark/issues/457 --- components/markdown/src/markdown.rs | 16 ++++++++++------ components/markdown/tests/codeblocks.rs | 10 ++++++++++ ...blocks__can_add_line_numbers_windows_eol.snap | 9 +++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 components/markdown/tests/snapshots/codeblocks__can_add_line_numbers_windows_eol.snap diff --git a/components/markdown/src/markdown.rs b/components/markdown/src/markdown.rs index 4b1c9bd70c..efbac3621a 100644 --- a/components/markdown/src/markdown.rs +++ b/components/markdown/src/markdown.rs @@ -317,13 +317,12 @@ pub fn markdown_to_html( }; } + let mut accumulated_block = String::new(); for (event, mut range) in Parser::new_ext(content, opts).into_offset_iter() { match event { Event::Text(text) => { - if let Some(ref mut code_block) = code_block { - let html; + if let Some(ref mut _code_block) = code_block { if contains_shortcode(text.as_ref()) { - let mut accumulated_block = String::new(); // mark the start of the code block events let stack_start = events.len(); render_shortcodes!(true, text, range); @@ -341,13 +340,12 @@ pub fn markdown_to_html( } } } - html = code_block.highlight(&accumulated_block); + // remove all the original events from shortcode rendering events.truncate(stack_start); } else { - html = code_block.highlight(&text); + accumulated_block += &text; } - events.push(Event::Html(html.into())); } else { let text = if context.config.markdown.render_emoji { EMOJI_REPLACER.replace_all(&text).to_string().into() @@ -373,6 +371,12 @@ pub fn markdown_to_html( events.push(Event::Html(begin.into())); } Event::End(Tag::CodeBlock(_)) => { + if let Some(ref mut code_block) = code_block { + let html = code_block.highlight(&accumulated_block); + events.push(Event::Html(html.into())); + accumulated_block.clear(); + } + // reset highlight and close the code block code_block = None; events.push(Event::Html("\n".into())); diff --git a/components/markdown/tests/codeblocks.rs b/components/markdown/tests/codeblocks.rs index 6aa9dc7424..d12e0f32bc 100644 --- a/components/markdown/tests/codeblocks.rs +++ b/components/markdown/tests/codeblocks.rs @@ -239,6 +239,16 @@ bar insta::assert_snapshot!(body); } +#[test] +fn can_add_line_numbers_windows_eol() { + let body = render_codeblock( + "```linenos\r\nfoo\r\nbar\r\n```\r\n", + true, + ); + insta::assert_snapshot!(body); +} + + #[test] fn can_add_line_numbers_with_lineno_start() { let body = render_codeblock( diff --git a/components/markdown/tests/snapshots/codeblocks__can_add_line_numbers_windows_eol.snap b/components/markdown/tests/snapshots/codeblocks__can_add_line_numbers_windows_eol.snap new file mode 100644 index 0000000000..1e79755a7e --- /dev/null +++ b/components/markdown/tests/snapshots/codeblocks__can_add_line_numbers_windows_eol.snap @@ -0,0 +1,9 @@ +--- +source: components/markdown/tests/codeblocks.rs +assertion_line: 248 +expression: body +--- +
1foo +
2bar +
+ From cec65d0fa7ad4bbf6439a46c08adb538a8644f57 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 23 Jun 2022 20:56:39 +0200 Subject: [PATCH 5/6] Stop printing invalid files in dirs Too many invalid stuff like emacs files --- components/site/src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index 92c073b387..4b3b94ba2a 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -218,7 +218,7 @@ impl Site { // is it a section or not? if path.is_dir() { // if we are processing a section we have to collect - // index files for all languages and process them simultaniously + // index files for all languages and process them simultaneously // before any of the pages let index_files = WalkDir::new(&path) .follow_links(true) @@ -228,15 +228,12 @@ impl Site { Err(_) => None, Ok(f) => { let path_str = f.path().file_name().unwrap().to_str().unwrap(); + // https://github.com/getzola/zola/issues/1244 if f.path().is_file() && allowed_index_filenames.iter().any(|s| s == path_str) { Some(f) } else { - // https://github.com/getzola/zola/issues/1244 - if path_str.starts_with("_index.") { - println!("Expected a section filename, got `{}`. Allowed values: `{:?}`", path_str, &allowed_index_filenames); - } None } } From 065e8e64e51d435e10238d86ebf95f9567100282 Mon Sep 17 00:00:00 2001 From: bemyak Date: Fri, 8 Jul 2022 00:19:13 +0300 Subject: [PATCH 6/6] Apply orientation transformation based on EXIF data (#1912) --- Cargo.lock | 16 ++++ components/imageproc/Cargo.toml | 1 + components/imageproc/src/lib.rs | 27 +++++++ components/imageproc/tests/resize_image.rs | 74 +++++++++++++++++- .../imageproc/tests/test_imgs/exif_0.jpg | Bin 0 -> 661 bytes .../imageproc/tests/test_imgs/exif_1.jpg | Bin 0 -> 761 bytes .../imageproc/tests/test_imgs/exif_2.jpg | Bin 0 -> 762 bytes .../imageproc/tests/test_imgs/exif_3.jpg | Bin 0 -> 755 bytes .../imageproc/tests/test_imgs/exif_4.jpg | Bin 0 -> 758 bytes .../imageproc/tests/test_imgs/exif_5.jpg | Bin 0 -> 761 bytes .../imageproc/tests/test_imgs/exif_6.jpg | Bin 0 -> 763 bytes .../imageproc/tests/test_imgs/exif_7.jpg | Bin 0 -> 757 bytes .../imageproc/tests/test_imgs/exif_8.jpg | Bin 0 -> 759 bytes 13 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 components/imageproc/tests/test_imgs/exif_0.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_1.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_2.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_3.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_4.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_5.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_6.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_7.jpg create mode 100644 components/imageproc/tests/test_imgs/exif_8.jpg diff --git a/Cargo.lock b/Cargo.lock index dae52756e0..58c7fa4606 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1203,6 +1203,7 @@ version = "0.1.0" dependencies = [ "config", "errors", + "kamadak-exif", "libs", "serde", "tempfile", @@ -1340,6 +1341,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kamadak-exif" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70494964492bf8e491eb3951c5d70c9627eb7100ede6cc56d748b9a3f302cfb6" +dependencies = [ + "mutate_once", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1843,6 +1853,12 @@ dependencies = [ "similar", ] +[[package]] +name = "mutate_once" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" + [[package]] name = "nanorand" version = "0.7.0" diff --git a/components/imageproc/Cargo.toml b/components/imageproc/Cargo.toml index 29c810d801..7f05f975b1 100644 --- a/components/imageproc/Cargo.toml +++ b/components/imageproc/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] serde = { version = "1", features = ["derive"] } +kamadak-exif = "0.5.4" errors = { path = "../errors" } utils = { path = "../utils" } diff --git a/components/imageproc/src/lib.rs b/components/imageproc/src/lib.rs index 179f9b2561..09bf21f56f 100644 --- a/components/imageproc/src/lib.rs +++ b/components/imageproc/src/lib.rs @@ -10,6 +10,7 @@ use image::error::ImageResult; use image::io::Reader as ImgReader; use image::{imageops::FilterType, EncodableLayout}; use image::{ImageFormat, ImageOutputFormat}; +use libs::image::DynamicImage; use libs::{image, once_cell, rayon, regex, svg_metadata, webp}; use once_cell::sync::Lazy; use rayon::prelude::*; @@ -319,6 +320,8 @@ impl ImageOp { None => img, }; + let img = fix_orientation(&img, &self.input_path).unwrap_or(img); + let mut f = File::create(target_path)?; match self.format { @@ -343,6 +346,30 @@ impl ImageOp { } } +/// Apply image rotation based on EXIF data +/// Returns `None` if no transformation is needed +pub fn fix_orientation(img: &DynamicImage, path: &Path) -> Option { + let file = std::fs::File::open(path).ok()?; + let mut buf_reader = std::io::BufReader::new(&file); + let exif_reader = exif::Reader::new(); + let exif = exif_reader.read_from_container(&mut buf_reader).ok()?; + let orientation = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)?.value.get_uint(0)?; + match orientation { + // Values are taken from the page 30 of + // https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf + // For more details check http://sylvana.net/jpegcrop/exif_orientation.html + 1 => None, + 2 => Some(img.fliph()), + 3 => Some(img.rotate180()), + 4 => Some(img.flipv()), + 5 => Some(img.fliph().rotate270()), + 6 => Some(img.rotate90()), + 7 => Some(img.fliph().rotate90()), + 8 => Some(img.rotate270()), + _ => None, + } +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct EnqueueResponse { /// The final URL for that asset diff --git a/components/imageproc/tests/resize_image.rs b/components/imageproc/tests/resize_image.rs index 3f5b7f8551..507de5e242 100644 --- a/components/imageproc/tests/resize_image.rs +++ b/components/imageproc/tests/resize_image.rs @@ -2,7 +2,8 @@ use std::env; use std::path::{PathBuf, MAIN_SEPARATOR as SLASH}; use config::Config; -use imageproc::{assert_processed_path_matches, ImageMetaResponse, Processor}; +use imageproc::{assert_processed_path_matches, fix_orientation, ImageMetaResponse, Processor}; +use libs::image::{self, DynamicImage, GenericImageView, Pixel}; use libs::once_cell::sync::Lazy; static CONFIG: &str = r#" @@ -153,4 +154,75 @@ fn read_image_metadata_webp() { ); } +#[test] +fn fix_orientation_test() { + fn load_img_and_fix_orientation(img_name: &str) -> DynamicImage { + let path = TEST_IMGS.join(img_name); + let img = image::open(&path).unwrap(); + fix_orientation(&img, &path).unwrap_or(img) + } + + let img = image::open(TEST_IMGS.join("exif_1.jpg")).unwrap(); + assert!(check_img(img)); + assert!(check_img(load_img_and_fix_orientation("exif_0.jpg"))); + assert!(check_img(load_img_and_fix_orientation("exif_1.jpg"))); + assert!(check_img(load_img_and_fix_orientation("exif_2.jpg"))); + assert!(check_img(load_img_and_fix_orientation("exif_3.jpg"))); + assert!(check_img(load_img_and_fix_orientation("exif_4.jpg"))); + assert!(check_img(load_img_and_fix_orientation("exif_5.jpg"))); + assert!(check_img(load_img_and_fix_orientation("exif_6.jpg"))); + assert!(check_img(load_img_and_fix_orientation("exif_7.jpg"))); + assert!(check_img(load_img_and_fix_orientation("exif_8.jpg"))); +} + +#[test] +fn resize_image_applies_exif_rotation() { + // No exif metadata + assert!(resize_and_check("exif_0.jpg")); + // 1: Horizontal (normal) + assert!(resize_and_check("exif_1.jpg")); + // 2: Mirror horizontal + assert!(resize_and_check("exif_2.jpg")); + // 3: Rotate 180 + assert!(resize_and_check("exif_3.jpg")); + // 4: Mirror vertical + assert!(resize_and_check("exif_4.jpg")); + // 5: Mirror horizontal and rotate 270 CW + assert!(resize_and_check("exif_5.jpg")); + // 6: Rotate 90 CW + assert!(resize_and_check("exif_6.jpg")); + // 7: Mirror horizontal and rotate 90 CW + assert!(resize_and_check("exif_7.jpg")); + // 8: Rotate 270 CW + assert!(resize_and_check("exif_8.jpg")); +} + +fn resize_and_check(source_img: &str) -> bool { + let source_path = TEST_IMGS.join(source_img); + let tmpdir = tempfile::tempdir().unwrap().into_path(); + let config = Config::parse(CONFIG).unwrap(); + let mut proc = Processor::new(tmpdir.clone(), &config); + + let resp = proc + .enqueue(source_img.into(), source_path, "scale", Some(16), Some(16), "jpg", None) + .unwrap(); + + proc.do_process().unwrap(); + let processed_path = PathBuf::from(&resp.static_path); + let img = image::open(&tmpdir.join(processed_path)).unwrap(); + check_img(img) +} + +// Checks that an image has the correct orientation +fn check_img(img: DynamicImage) -> bool { + // top left is red + img.get_pixel(0, 0)[0] > 250 // because of the jpeg compression some colors are a bit less than 255 + // top right is green + && img.get_pixel(15, 0)[1] > 250 + // bottom left is blue + && img.get_pixel(0, 15)[2] > 250 + // bottom right is white + && img.get_pixel(15, 15).channels() == [255, 255, 255, 255] +} + // TODO: Test that hash remains the same if physical path is changed diff --git a/components/imageproc/tests/test_imgs/exif_0.jpg b/components/imageproc/tests/test_imgs/exif_0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..36beb6863cdd569f6057de07e5c25f7872f42cb3 GIT binary patch literal 661 zcmex=nEb00R>vGcywlGb<|#3s7|}P@aKB zkX1<0(2-3zFp*uUP{gQl;zAB(r;P_igD!qhF-|IK;^Yz&myncFRa4i{)G{$OGqmaka3YSZQ|TeofBv2)jkyBj`3o2bj7%&n%q;96e=#zZ12MB83#+0bn~-B5dt#xm zl2Idvh||P{8xL|S8wY(5O)9#`C8lEXQ1v6oYha%d=dmWTdCS(KLwo-5zW&dU+5B$}|964)KiKV8@&CUG0P`x(H2?qr literal 0 HcmV?d00001 diff --git a/components/imageproc/tests/test_imgs/exif_1.jpg b/components/imageproc/tests/test_imgs/exif_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c0bb69bf4da79db08c90b08a5bbf3197d5ac705f GIT binary patch literal 761 zcmex=kR1VRQz zDF#+Bn}NZLQ5wz;V$^`DVParl5=N*8ss$+rnhe$rrvKk&a0c55vXxu_GK5^caDD#| zFbHxm2rvjRGYT>=2{JMZGX6ipAP;maD?|<0^&q!1F|)9;v2$>8aRU`>6<}auWM*b! zVP<7zVFAk40_7Q41X+a?4ISBp0~6Vm3Pp?>CobercG`GQH0a_772~9$CQdFfaS2H& zRW)@DO)V2sGjj_|D`yv1H+K(Dui%i-u<(e;sN|H?wDgS3tm2Z=vhs?`s^*r~w)T$B zu1S-pOr17;#>`oZ7B5-4Z25|nt2S-kvUS_`9Xod&I(+2lvEwIBp1O4T%GGPvZ`{1~ z@X_NZPoF)1@$%KjPoKYh{r3IG&tD*aF)}d2y$ugFVBa`1POW{AbwT{3~Gnr#t@{4(<8J`}#jaX7j%_{NDxE|6sRY I#sB{%0Ge#oZ~y=R literal 0 HcmV?d00001 diff --git a/components/imageproc/tests/test_imgs/exif_2.jpg b/components/imageproc/tests/test_imgs/exif_2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e6f3ef37f4631841eb7a752e48379a1bd5d7f80a GIT binary patch literal 762 zcmex=kRWMBdk zQVgtMHUonfqcofy#HfK#!z2u4GXhnD0mx*aeg+8r|2Bg&*hY}84D%dK(z{JSR%*4XX z%F4n5R9y>{XJ8Rz6;d>GWD^cdWLGK_F>0K+kVDyN<3Z7&iyu^slZu)+xx~aJB&Af< z)HO7@(s#)lOnKGcaiiG7B=;GyFQV|7p#ChOXkD>(_r(_|I_H;XgxU*MEjE{vYi2hxhG&|KMM^ H{r{T)SPIpY literal 0 HcmV?d00001 diff --git a/components/imageproc/tests/test_imgs/exif_3.jpg b/components/imageproc/tests/test_imgs/exif_3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e5012999fa57e703b17650f70928986405f5c8fa GIT binary patch literal 755 zcmex=kR1d)tV z46I-_1A`Z%G@Kp8r~y^O#K6EL3}rI{Re}M?WT1Wq2>t&ygEQDhkgenbkRjyih3or& zfI*OhL4ZMknNg5|Nsy6Qkn#T!26>=ESs`km?q^_RVrF4wW9Q)H;sz?%D!{d z!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~nmD<{#3dx9 zRMpfqG__1j&CD$Mvj*Ffy^QFtf0O{Kd#r4)VAl z3#+0bn~-B5dt#xml2Idvh||P{8xL|S8wY(5O)9#`C8lEXQ1v6oYha%d=dmWTd(~GO@jos8ePaEe$n!t_?PvY^&k)4__X_|2n*gSE B(yIUf literal 0 HcmV?d00001 diff --git a/components/imageproc/tests/test_imgs/exif_4.jpg b/components/imageproc/tests/test_imgs/exif_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..807020eb05fdf608db82e51b80f7cc2db7c3d29b GIT binary patch literal 758 zcmex=kRWMBaj zQVgtMHUonfqcofy#HayP!^FVABn)LU0#$+m$Yh{?1_=HCHiI+RMv$%K0+1o(>V@n3 ze}F-dgF%2nfSFN{fk}{&S&;Gn5e9jnLs=nepzdd2WMXDvWn<^y(iUwW$pkka<)WpdpCN3c< zrK+Z`p{ZqJYG!U>Y31zV>gMj@=@lFj8WtWA8I_!pnwFlCnN?g;T2@|BS=HRq+ScCD z*)?hMl&RCE&zL!D(c&dbmn~nha@D5ITefc7zGLUELx+zXJ$C%W$y1juU%7hi`i+~n z9zJ^fj z7{Xt-7kRWMBmn zQXmq@W?=AQl!mi|7&V}3m>3wCgrRJZDlh<<4AKLk|KDbC2HOa-m0SQagj~IFeg6+I z2y!q8FbFU+3NkPWGBOJ?{y)MX4|FIiL=Dva42(?7EUawo9GqO-Km}U`7?>EDnVDFa zSy@?FfU0YO@(e73tU`*0j%>n#iR?;+B1Vl97jh^&Z9FI%bn%0VaZ*teCzqJGgrtbvx}>nyN9P&a7buactm7Wa!P7idPZheaY<=ec|~Pab4zPmdq-#2 zq{&mJPMbbs=B!1Fmn>bje8tLDn>KIRx^4T8ox2VlK63Qf@e?OcUAlbb>b2`PZr*zM z=<$=M&z`?{`Re1R&tJZN`~KtSFOa_&8JOW-heiylzkq?j$i%|J%)$=x7b8VP*tI9kRWMBgl zQVgtMHUonfqcofy#HayP!^FVABn)LU0#$+m$Yh{?1_=HCHiI+RMv$%K0+1o(>V@n3 ze}F-dgF%2nfSFN{fk}{&S&;Gn5e9jnLs=nepzdd2WMXDvWn<^y?FU=d^$QZ#gA6AnydS1J@SYMi)`L)mHLLD8U#A5@H!ikdjN#Ka{e zrBv0_H8izMOwG(KEUlbfT;1F~JiUTLLc_u%BBPR1Qq$5iGP89XZ3R<7E#dCS&q+js2Tb?ESsqsNY)IC<*QkRWMBsp zQVgtMHUonfqcofy#HayP!^FVABn)LU0#$+m$Yh{?1_=HCHiI+RMv$%K0+1o(>V@n3 ze}F-dgF%2nfSFN{fk}{&S&;Gn5e9jnLs=nepzdd2WMXDvWn<^y?FU=d^$QZ#gA6AnydS1J@SYMi)`L)mHLLD8U#A5@H!ikdjN#Ka{e zrBv0_H8izMOwG(KEUlbfT;1F~JiUTLLc_u%BBPR1Qq$5iGP89XZ3R<7E#dCS&q+js2Tb?ESsqsNY)IC<*QkR1hN?z zr5ISjYz77|Mrk-Zh*1NohKYfJNf^pz1gZoBkjX&(3=sPNZ3btsjUZde1t3Gn)eG16 z{{Vv^2ZI2E05hW?1Ct;lvmoRDBMkCDhq6M{K;6&4$i&RT%Er#Y$;Ay+uvLJ8iIJI^ ziG`V!m4yYUx)vzUz#_;hq-f~KCLEZ^u2d*u)Hrb=hqBYggQ7tfKd2Zd6*X~kiHS={ zN~x-;YiMejn3|beSXw!|xVpJ}czOkggocGjL`Eg2q^6~3WM&nYl$MoOR8}>&w6?W( zbaqXeJZ0*%=`&`|TC{k{(q+q6tX#Ee^OmjKw(r=v>(JpNM~@vpaq`rq%U7;myME*5 zt%r{uKY9A>`HPpYK7RWA>z(JGL?fo zF37^FXviky7|5PjD6C}E$RXl1apA^;oXW;QA4HRiE^>*fm^@Vd2=W@(XT*7|i7cPN zJ%;etEe0NDMquPI3o_U<{Ih)h?;rou;;)bPe_C(