From 7ce81b072759c8bbb2c11e026b13978f5e30987e Mon Sep 17 00:00:00 2001 From: "Dario P." <83188092+donishki@users.noreply.github.com> Date: Sun, 25 Oct 2026 01:22:14 +0200 Subject: [PATCH] Publish Untrace Desktop 1.6.1 with offline batch workflows --- .gitignore | 21 + AGENTS.md | 31 + CHANGELOG.md | 79 + PUBLISHING.md | 54 + README.md | 162 +- THEME-CREDITS.md | 16 + desktop/brand.json | 4 + desktop/icon.ico | Bin 0 -> 7900 bytes desktop/main.cjs | 54 + desktop/output.cjs | 57 + desktop/preload.cjs | 5 + docs/screenshots/metadata-comparison.png | Bin 0 -> 72633 bytes index.html | 261 +- package-lock.json | 3684 +++++++++++++++++++++- package.json | 75 +- playwright.config.js | 2 + public/favicon-16x16.svg | 2 +- public/favicon-32x32.svg | 2 +- public/favicon.svg | 8 +- public/manifest.json | 12 +- scripts/make-icon.mjs | 27 + scripts/prepare-desktop.mjs | 20 + scripts/smoke-portable.mjs | 31 + src/App.svelte | 128 +- src/app.css | 837 ++--- src/lib/Appearance.svelte | 47 + src/lib/BatchWorkspace.svelte | 153 + src/lib/ExportSettings.svelte | 15 + src/lib/Icon.svelte | 6 + src/lib/MetadataComparison.svelte | 24 + src/lib/ResultPanel.svelte | 4 +- src/lib/Tool.svelte | 2 +- src/lib/Workspace.svelte | 297 ++ src/lib/clean.js | 37 +- src/lib/drop-files.js | 29 + src/lib/export-settings.js | 12 + src/lib/image-worker.js | 18 + src/lib/metadata-comparison.js | 38 + src/lib/metadata.js | 72 +- src/lib/processing.js | 17 + src/lib/theme-catalog.js | 35 + src/lib/theme.js | 79 +- src/themes.css | 254 ++ tests/desktop.spec.js | 119 + tests/features.spec.js | 219 ++ tests/themes.spec.js | 102 + vite.config.js | 15 +- 47 files changed, 5958 insertions(+), 1208 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 PUBLISHING.md create mode 100644 THEME-CREDITS.md create mode 100644 desktop/brand.json create mode 100644 desktop/icon.ico create mode 100644 desktop/main.cjs create mode 100644 desktop/output.cjs create mode 100644 desktop/preload.cjs create mode 100644 docs/screenshots/metadata-comparison.png create mode 100644 playwright.config.js create mode 100644 scripts/make-icon.mjs create mode 100644 scripts/prepare-desktop.mjs create mode 100644 scripts/smoke-portable.mjs create mode 100644 src/lib/Appearance.svelte create mode 100644 src/lib/BatchWorkspace.svelte create mode 100644 src/lib/ExportSettings.svelte create mode 100644 src/lib/MetadataComparison.svelte create mode 100644 src/lib/Workspace.svelte create mode 100644 src/lib/drop-files.js create mode 100644 src/lib/export-settings.js create mode 100644 src/lib/image-worker.js create mode 100644 src/lib/metadata-comparison.js create mode 100644 src/lib/processing.js create mode 100644 src/lib/theme-catalog.js create mode 100644 src/themes.css create mode 100644 tests/desktop.spec.js create mode 100644 tests/features.spec.js create mode 100644 tests/themes.spec.js diff --git a/.gitignore b/.gitignore index 92d3da4..0eac5b8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ yarn-error.log* # Build output dist/ +release/ +test-results/ +playwright-report/ +Untrace-data/ build/ # Environment variables @@ -54,3 +58,20 @@ coverage/ # Temporary folders tmp/ temp/ + +# Publishing hygiene +.env.* +!.env.example +!.env.sample +!.env.template +*.pem +*.key +*.pfx +*.p12 +*.keystore +id_rsa +id_ed25519 +*.zip + +# Vite temporary configuration bundles +*.timestamp-*.mjs diff --git a/AGENTS.md b/AGENTS.md index ff303a3..73feaed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -91,6 +91,37 @@ Light theme: `document.documentElement.setAttribute('data-theme', 'light')`. Dar ## Untrace specifics +### Batch workflows (1.6.0, 2026-09-10) + +`BatchWorkspace.svelte` owns the sequential queue. `processing.js` creates a terminable worker per scan/encode task. `image-worker.js` uses the shared scanner and cleaner with OffscreenCanvas. Keep incomplete scans visibly uncertain. `desktop/preload.cjs` exposes only folder selection and saving; `desktop/output.cjs` validates the sender and creates unique files exclusively in the user-selected directory. Never expose generic IPC or unrestricted filesystem access. Saved export settings use `untrace-export`; theme settings keep their existing key. + + +### Themes and branding (1.3.0, 2026-09-10) + +The derivative is named **Untrace Desktop**, as requested by the user. Branding +lives in `desktop/brand.json`; build metadata in `package.json` must match it. +Keep upstream Untrace attribution and the MIT license. + +The user explicitly requested more themes. `src/themes.css` is the local palette +source: it overrides existing semantic colour tokens for dark, graphite, paper, +cobalt and ember without editing vendored `src/tokens.css`. Palette definitions may +contain raw colours; components must continue using tokens. Exclude both palette +files (`tokens.css`, `themes.css`) from the raw-colour self-check. No new token names +are introduced. The theme catalog and shared pre-paint bootstrap are in +`src/lib/theme-catalog.js`. Appearance is a native dialog with radio controls. + +### Desktop workspace (1.2.0, 2026-09-10) + +The active application is `src/lib/Workspace.svelte`, mounted by `src/App.svelte`. +It uses a full-window preview and inspector, not the historical landing page or +stepper. Older website components remain as upstream reference and are not imported. +The active stylesheet is `src/app.css`; it composes the existing tokens and keeps +comfortable density. There is one primary action: choose an image, clean it, or +save its result. Help is a native dialog. Keep the image processing limitations +accurate when editing copy. `npm test` verifies the workspace in Electron and +`npm run portable` creates the Windows release. Earlier specifics below describe +the upstream baseline. + Applied to this repository on 2026-09-05, design system v2.0.0. - `src/tokens.css` is the vendored copy. `src/app.css` imports it and adds only diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..eb60251 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,79 @@ +# Changelog + +## 1.6.1 - 2026-09-10 + +- Filter folder selections, multiple-file selections and drag-and-drop before opening a batch: only supported images enter the queue. Non-image files no longer appear as skipped rows. +- Show a clear message when a selection contains no supported images, preserving the current workspace. +- Added regression coverage for documents and executables inside nested folders and mixed drag-and-drop selections. + + +## 1.6.0 - 2026-09-10 + +- Added batch cleaning for multiple files, selected folders and dropped folders, including nested contents and per-file results. +- Added native output-folder selection and collision-safe batch saving. Existing files are preserved; failed saves retain prepared outputs for retry. +- Added before/after metadata comparison with field values, detected changes and explicit uncertainty when parsing fails. GPS and camera fields are prioritised. +- Added prominent original-image GPS warnings and coordinate display without online maps. +- Added an exact export-size preview that reuses its prepared output, plus batch output sizes before saving. +- Remember export format and JPEG quality across launches and share settings between single-image and batch modes. +- Moved metadata scanning and image encoding into background workers. Cancel stops active processing; batch saving finishes the current file before stopping the remaining queue. +- Release saved batch image blobs to reduce retained memory; keep comparison details available for the session. +- Preserve previously discarded parsed metadata fields and replace misleading clean claims after parser errors with incomplete-scan reporting. +- Report encoder-generated colour profiles and avoid an unnecessary second JPEG pass for those profiles. +- Added regression tests for the new workflows, file collisions, path validation, cancellation, save failures and retries. Updated README and portable build. + + +## 1.5.0 - 2026-09-10 + +- Added eight VS Code-inspired palettes: One Dark, Dracula, Tokyo Night, Night Owl, GitHub Dark, GitHub Light, Catppuccin Mocha and Catppuccin Latte. Retained all six existing themes and saved preferences. +- Reworked Appearance into a wider, scrollable gallery grouped into editor favourites and Untrace originals, with the heading and Done button kept in view. +- Adapted secondary text and action colours for readable app controls; theme choices still leave image pixels unchanged. +- Added theme inspiration credits and extended the existing contrast checks to all fourteen palettes. + + +## 1.4.0 - 2026-09-10 + +- Replaced Midnight, Forest, Plum and Sand with four redesigned themes: Graphite, Paper, Cobalt and Ember. Light and Dark are unchanged. +- Use neutral surfaces and stronger tonal separation rather than colouring every surface with the accent hue. +- Added a distinct dark navy frame to Cobalt's light workspace; Graphite uses silver controls, Paper uses dark olive ink, and Ember uses warm orange accents. +- Migrate saved selections: Midnight to Cobalt, Forest to Graphite, Plum to Ember, and Sand to Paper. +- Updated previews and documentation, and checked text contrast in both the workspace and Cobalt's contrasting frame. + +## 1.3.0 - 2026-09-10 + +- Rebranded this derivative as **Untrace Desktop**, including the workspace, Help, window title, Windows product metadata and portable executable name. Retained original Untrace attribution and MIT license. +- Added Midnight, Forest, Plum and Sand palettes alongside Light and Dark. +- Replaced the light/dark toggle with an Appearance dialog containing live theme previews and accessible radio controls. +- Added Follow Windows, which tracks system theme changes while the app is open. +- Persist theme choices, retain legacy light/dark preferences, and apply saved themes before first paint using shared startup logic. +- Preserve the existing `Untrace-data` folder and storage origin across the rename. +- Added contrast, persistence, system preference and rebranding checks. Theme changes affect the interface only. + +## 1.2.0 - 2026-09-10 + +- Replaced the landing page with a desktop workspace: application rail, file toolbar, central preview, persistent inspector, export controls and status bar. +- Removed the promotional sections and moved usage guidance into an accessible Help dialog. +- Added original/cleaned preview switching, fit/actual-size viewing and responsive layouts for smaller windows. +- Added automatic, PNG and JPEG export options, with JPEG quality controls. +- Fixed PNG output being unnecessarily converted to JPEG when required IHDR image headers were mistaken for removable metadata; PNG export now preserves transparency. +- Added Ctrl+O (open), Ctrl+V (paste), Ctrl+Enter (clean), Ctrl+S (save), Esc (close image), and F1 (help). +- Accept drag-and-drop and pasted images throughout the workspace; release replaced previews and invalidate exports after setting changes. +- Replaced the misleading “already clean” message with a qualified metadata scan result. +- Simplified startup markup, removed unused website UI from the bundle, and retained offline Electron packaging. +- Extended integration checks for themes, keyboard controls, PNG alpha, export invalidation, preview switching and small-window layout. + +## 1.1.0 - 2026-09-10 + +- Added a Windows x64 portable executable with bundled Electron runtime. +- Added offline packaging, content security policy, sandboxed rendering, denied permissions, blocked network requests, and restricted external links. +- Store preferences in `Untrace-data` beside the executable. +- Fixed black backgrounds when large transparent images become JPEG by applying a white matte. +- Fixed misleading success badges and removal lists when decoding fails. +- Preserve the original filename on failure; safely handle filenames without extensions. +- Accept recognized image extensions when Windows supplies an empty or generic MIME type. +- Prevent repeated cleaning requests while processing. +- Added desktop integration tests, build commands, and Windows documentation. + +## Upstream baseline - 2026-09-10 + +Imported developersharif/untrace at commit `99b4b223525a0b85da8ab0994066b5151d6a83fa`. +This date records the import, not the upstream release date. Original MIT license retained. diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 0000000..02391a5 --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,54 @@ +# Publishing Untrace Desktop + +## Repository layout + +The desktop edition is maintained on `main`. The pre-existing GitHub web edition is preserved on `web-before-desktop`, and its commits remain in the shared history. + +- `origin`: https://github.com/donishki/Untrace.git +- `gitea`: https://git.ptbox.org/dasp/Untrace.git +- `upstream`: https://github.com/developersharif/untrace.git (reference only) + +Create empty repositories at the two publishing destinations before pushing. Do not initialize them with a README or license. Retain the MIT license and upstream attribution when distributing source or binaries. + +## Verify and package + +On Windows x64 with Node.js 22.13 or newer: + +```powershell +npm ci +npm test +npm run portable +node scripts/smoke-portable.mjs +``` + +Review `git status --short` and `git diff` before committing. Keep dependencies, generated builds, local settings, secrets and test outputs out of Git. Inspect all newly added files as well as tracked changes. Commit the reviewed source and publishing documentation before tagging a release. + +## Publish the reviewed commit + +Use the current branch name for both hosts: + +```powershell +$branch = git branch --show-current +git push -u origin $branch +git push gitea $branch +``` + +After validating the built release and ensuring the tag does not already exist: + +```powershell +git tag -a v1.6.1 -m 'Untrace Desktop 1.6.1' +git push origin v1.6.1 +git push gitea v1.6.1 +``` + +Create a release on each host for the same tag. Upload `release/Untrace-Desktop-1.6.1-portable-x64.exe`, a SHA-256 checksum file, and matching source. Include the MIT license, theme credits, version highlights from CHANGELOG.md, Windows x64 requirements, and the unsigned-executable notice. Do not upload `Untrace-data` or test output. + +Generate the checksum after building: + +```powershell +$asset = 'release/Untrace-Desktop-1.6.1-portable-x64.exe' +$hash = (Get-FileHash -LiteralPath $asset -Algorithm SHA256).Hash.ToLowerInvariant() +"$hash $(Split-Path -Leaf $asset)" | Set-Content -Encoding ascii release/SHA256SUMS.txt +``` + +README.md links to the latest releases on both hosts. Publish identical assets and source on both hosts. diff --git a/README.md b/README.md index 31ebb4e..16375ba 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,132 @@ -# Untrace +
6wqDBpy!7 z>(X?EJQqnUjK-?C#Jw#SeB&$+m`%Dnj^64RE3pp2v9ve3{pxb7eH544^|Mc{^K{W4 zsM@)2yvmV)2FU>t+kvxhA+YJu==pu1V8V6W>|x_XYu)j#jF#j z-K^$>qE~DZ;pV~x3Q64vQo!@uZ&{xw^l`deb)?>7BHo;JoP5f+|KzcOm+CJq)f9R{ z!Sm|l-@#g~<}>LjmMrv-XTCJK0l@j^XIy~jzPT;dl ?pTQ^ADrA~+H<(P|KjpopJN~vB^0I7%J{&1BU5Epup2MZ<60YQCC n$*QA(>DPfF7($>zv0atn?(dnGnXcd~nC?^iCdR|$Gde6vkq0HoJ^ zn`ME&3!l%CSQwfX6yQIVy^CL@oIH2_ckF}5IBNq92UTN{FP}+k^TKZ4<>Lb#xUfdG zI|gmLr|WqUIgX(2qf4a0&(Bi+K|@!uUNfS{wog>;2Mc@@4hL?+fL@x2PJ-#PL*!Sx zgpClKX}eBqmEe%)k(tc4q=2&?V3q(2<4+d c4lj)|d!|3e$?ZCmQCTnA z-6y`zV@I6y02HV%3$!JFK6$cQ_sVM~+dNg)OQmHN Q?X_AN ATV`x*w!_srH z-PDA|xJX>yq6O)5#IwUvjF+!f1zW1p@?t*lk@GLcD*o4GStVT3u7;)f`)Y-3hLD-c zsbFlX>%V-U*L66Dqm&@MO5R%`n2GWFeVNR7y}+YEn+7NahP6bL+4O$NR4t$MQF0}` z5 -8H?rH}dp>-c^x%@)x%z16cOA7(t0aZcsCcU(_ z*;tJfK1XuckK@7&te1ApgMwt+6c(VSzKx%NQbZJI++_!?x+ibp3rbW|Z3{}N_PKx` z8ewoQnJ%v|vO5~Yanc2T-ADUMma)_|y&=;K1U1R`GLE}5{HHVnx;EgxMy@{%Xhl0K zXw93^$HDYX4HW)%f9ui#CIj? EfXJ{|6psWoq-xB&h77LPB-n=8+0FrbohHm{+RG$pK90GeVeJ8~{@ zWl4aFL@b=k`=aFQzs=i8B(rfgM#6&VDhh4>4LI6r*DybKxOt|+Mj3MW8a0c!3&uNU zP9q3^gFQk7in=e1%m_^Q9+*S{-{)B+f)fS1-Qwndxbm*)w%BEX9c4(EFn`TTw$S`Q zz#CWZUM@$dRH+x2@;UvNLL`qgoZ!Zn^Hwory}5+K8Wn!8LN&W8XXl)Pt{A8+e)Nr~ zZ_kUHnsA xVT;j$QNVA`db-@nQPx~ZOM5bESZtlp4 2;NM$v!D#0P50q zmi>9ogQpb~zmfu>bDf=!W`%aCK#rp+0I^o`gM7$or9x<9KyvGj0U%LgL51tKka3w^ z>5D)F54xUIK z7UuE~`%`nQDeZCTJmU$Ven-ZfUkHZ(=L~iyAaHP5kRxq?~)3&KTR&q@jNUa(`DY zW>FrDqA9$dM0G0_gI{#mRW?V$ezujJQpc>=ZpoRqtxkkI)ldhhr*wb=2G}?b?in67 zlBfTmEO4)^FVNCTXoE#X57b2}fPL8r98)~Jj@hX|;_Cn}wn+9J=qT~fYh@jU)QZ}4Yu~x zJTHThXuwtRS0!TVGKSPZD4M?^p}-R_l)roF*lUQq0@ElYRs^Y-HGS#4MC{Ncg_Z=< zBfio-9Q2YPr0!jmFIGHJ6L$56CZS+*3<6{Z>s~H%$`v|E0PUV5TTlG@*r)$w=+s)Z z@f?-S?glShFl8WIH*&qriBF>1qB%)xlgrKuAZ+^ebT)jW%sP}EK6t{4x-H*f=U()z zNNZOGL>5!h6PxxAGa78^N!CHFV*v);6rYmsU|UY$i6R>V_cHNNA@=2dt*x6C28l_e zhh>dNMe*)g1Q2ylx19A{f^tdJ-{nW*yN)5foj>$TAy4=RjBUspuFD}3ePg9|9kAtr z4q1m>-2;aW7EKgkq>cowQDdlFwz?^6k;*WI-XYiHNh0?*V#e)64R{>|Zbfl~RXvk` z>KDmCi1CO9Y~9D`iiL3csO$QUS^uKqJqoEzM@Y;P=F9nfl+2okhJObfPa)$+58t*= zlXl4EBV40c`sFQDzdLic@-w>eDj3_36rd;{Al92Os^p?I3@|E^de9eZ0>Dq@N^3y# z=yl6{KVebHc|HH!v_94$YgzNS`d%&tRDFwAtq#3q_`va4E$SY;S|2S{W!zT!IVcia zCO)$iC@r5|F_Hibrtv^Ae58hSSM*<+w?WJ6a<4yOZOJYWCzuOf1Q`Ib4}z5Pq_=zH zNwFEv8v?ejUjS{$(ThzI%jsHW;f^c3Lu8Px+XwekS&p=1ezYEwI(@y~P}HQ+h8>jg z>Ru2v1z>P1qqMpZ3VA~GOWpp@C`HD#RQgm2|6 Iz@RQFLHi7sn*Xk z6$Lli8>>vB$&j@>K{~f(Ny5E@fVzKO@6M6S0eZCDrNoBl>#A4T!{l9qu)>zN6Jvwd z5OpJ=e;!NrH!7o|sA2kc-HX#=4O5Icg0k(ALkQBbMT}y?JzIkaX*5YS`5A+JL#}7W zN<)|G#qISRK5R#RrIvG(S3EFjr=L;N&)g `shc%` zw5tiktV(oP->jvBwY8br=OF#B1n1-lO|LdLj r& z|1dV^1ji|huf&Y9lU(d6dJk=`n`9_2p{FzuS;3KA_0-bt{S7v|4Wo K+ul{+mJC|zDC!P1NGSYs8(_eJ-#sGa8f07RxX%M)VZIOs` z_WF@6^@3%;%EJZtC8v*6f!ED5mZl>(aW#n4B}aUG$3aC}S^1jHpSlD4kJBYX*lS5~ zgu@9GV;;ViQwC>xuQ-Lfwr|5AnzQ 3!mDb6A~et5@@O@H(Z8D z_bF WH7@u-bnedpi21dJTiMR_H~;we+~`04)dRWr z&429BM2r*olf6E_o_E3m5z1_5gt8fIWYle?C%@0K6lzsCF%y}^kL(z=d&Hg;0#Vo1 zrX$yLxgZ9&lpZ3g1HiST%^t5GZ;zEU&Y1egf}rI)yxTJxW@@1NPu1>W9Um18-Kz<| z_rf&?uz!v{ypSyGS(l`hFf31EBTB^i*Wz7`P2;Hna)*O8dJnJunDda!Z~aKIj4OiM zu9<+YyOgH(#(x%XAlo`dHNoOqk0()F?SaXCPHw#LnsC4@pww8mjSINwezLFOb8&vm z7x!WCCH(ELx|yZKyit>AV?cPf!?0m`IF{>xuKNw-=O-2r&loYg&X)j~RpEbA)=wC$ zR)z@v%~5naYbu2P*HjSG(W-_6&0}LdN5ck&+bd<8AO2&_??#f4XYGi>8D@a5%5#6z zC-~3a?RVFyh!W`DmI{)R)cka9ETG%Bv*YMvm+ZUx*X%40a*?VNGPCKX$K4>#Sw<$| zq+@U{VcDz`MkW`Cyhcw%kNf#4E%0*zcf$_0-0NLFQ#F;xK#cl*dV(GP{K|!boMK=K zSd6q-%{A>3saHav)Wa3isI;3!swn0>_ZF1^ygyr8UeQT( Ib@QphoC=zo%u)W|_!E^e{2x)&D z4F29<1D{I>4^Z+$0Ojk}{wXP;*l4a2Zyf*^_cR>Gmf|OaM+gRN2qgj^&u^y8NUa>l znJ> E; q$}_S3Y979cS)P$ zhXs0Y)b>xN< ITd7{@G6cEc#^jl{?cxVby=YK z&nq3D&|X~c9X9a75BrkTD}gllbZRWbk(YgdV$41 Yn?L$3 z!X=uTjAPE54X;39+TOFH!roykEz^>`C9hE3;zH|s&({|(haa`CxpkhpUQwAW8FtXX z7K@cZ8}E@k%9i^<=*6-3E#rUgO*|03sipaiIl1LpiFCy196Q%*0Ud|PQ%V3hJq15b i`$FAlinG6-WI(`s?XT?bmUy8%fSQuFVxhd1@BaYw6=O^Q literal 0 HcmV?d00001 diff --git a/desktop/main.cjs b/desktop/main.cjs new file mode 100644 index 0000000..573828b --- /dev/null +++ b/desktop/main.cjs @@ -0,0 +1,54 @@ +const { app, BrowserWindow, session, protocol, net, shell, dialog, ipcMain } = require('electron'); +const path = require('node:path'); +const fs = require('node:fs'); +const { pathToFileURL } = require('node:url'); +const { name: productName, appId } = require('./brand.json'); +app.setName(productName); +app.setAppUserModelId(appId); + +// Keep preferences alongside the portable executable, including on USB drives. +const home = process.env.PORTABLE_EXECUTABLE_DIR || path.dirname(app.getPath('exe')); +const data = process.env.UNTRACE_DATA_DIR || path.join(home, 'Untrace-data'); +try { + fs.mkdirSync(data, { recursive: true }); + app.setPath('userData', data); + app.setPath('sessionData', data); +} catch { + dialog.showErrorBox(`${productName} cannot start`, `Move ${productName} to a writable folder and try again.`); + app.exit(1); +} +protocol.registerSchemesAsPrivileged([{ scheme: 'untrace', privileges: { standard: true, secure: true, supportFetchAPI: true } }]); +const externalHosts = new Set(['github.com', 'www.buymeacoffee.com', 'artimic.com', 'realbrain.cc']); +function openLink(url) { + try { + const target = new URL(url); + if (target.protocol === 'https:' && externalHosts.has(target.hostname)) shell.openExternal(url); + } catch {} +} +app.whenReady().then(() => { + require('./output.cjs').registerOutput({ ipcMain, dialog, BrowserWindow }); + const root = path.join(app.getAppPath(), 'dist'); + protocol.handle('untrace', request => { + const url = new URL(request.url); + const file = path.resolve(root, '.' + decodeURIComponent(url.pathname === '/' ? '/index.html' : url.pathname)); + if (url.host !== 'app' || !file.startsWith(root + path.sep)) return new Response('Forbidden', { status: 403 }); + return net.fetch(pathToFileURL(file).href); + }); + session.defaultSession.setPermissionRequestHandler((_contents, _permission, callback) => callback(false)); + session.defaultSession.setPermissionCheckHandler(() => false); + session.defaultSession.webRequest.onBeforeRequest({ urls: ['http://*/*', 'https://*/*', 'ws://*/*', 'wss://*/*'] }, (_details, callback) => callback({ cancel: true })); + const win = new BrowserWindow({ + width: 1200, height: 900, minWidth: 640, minHeight: 640, + title: productName, show: false, autoHideMenuBar: true, + webPreferences: { preload: path.join(__dirname, 'preload.cjs'), nodeIntegration: false, contextIsolation: true, sandbox: true, webSecurity: true }, + }); + win.removeMenu(); + win.webContents.setWindowOpenHandler(({ url }) => { openLink(url); return { action: 'deny' }; }); + win.webContents.on('will-navigate', (event, url) => { + const target = new URL(url); + if (target.protocol !== 'untrace:' || target.host !== 'app') { event.preventDefault(); openLink(url); } + }); + win.once('ready-to-show', () => { if (!process.env.UNTRACE_TEST) win.show(); }); + win.loadURL('untrace://app/'); +}); +app.on('window-all-closed', () => app.quit()); diff --git a/desktop/output.cjs b/desktop/output.cjs new file mode 100644 index 0000000..f3cc44f --- /dev/null +++ b/desktop/output.cjs @@ -0,0 +1,57 @@ +const fs = require('node:fs/promises'); +const path = require('node:path'); +const { randomUUID } = require('node:crypto'); + +// Exclusive creation protects originals, existing exports and case-insensitive collisions. +async function saveUnique(directory, filename, bytes) { + if (typeof filename !== 'string' || !/^[^<>:"/\\|?*\x00-\x1f]+\.(png|jpg)$/i.test(filename) || filename.length > 220 || /[. ]$/.test(filename) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])\./i.test(filename)) throw new Error('Invalid output filename.'); + if (!(bytes instanceof Uint8Array) || !bytes.byteLength) throw new Error('No encoded image data.'); + const extension = path.extname(filename), stem = filename.slice(0, -extension.length); + for (let index = 1; index <= 10000; index++) { + const name = `${stem}${index === 1 ? '' : '-' + index}${extension}`; + const target = path.join(directory, name); + let handle; + try { handle = await fs.open(target, 'wx'); } + catch (error) { if (error.code === 'EEXIST') continue; throw error; } + try { + await handle.writeFile(bytes); + await handle.close(); + return { filename: name, path: target }; + } catch (error) { + await handle.close().catch(() => {}); + await fs.unlink(target).catch(() => {}); + throw error; + } + } + throw new Error('Too many files with this name. Choose another output folder.'); +} + +function registerOutput({ ipcMain, dialog, BrowserWindow }) { + const folders = new Map(); + function check(event) { + const url = new URL(event.senderFrame.url); + if (event.senderFrame !== event.sender.mainFrame || url.protocol !== 'untrace:' || url.host !== 'app') throw new Error('Untrusted sender.'); + } + ipcMain.handle('output:choose', async event => { + check(event); + const selection = await dialog.showOpenDialog(BrowserWindow.fromWebContents(event.sender), { title: 'Choose output folder', properties: ['openDirectory', 'createDirectory'] }); + if (selection.canceled || !selection.filePaths[0]) return null; + const directory = await fs.realpath(selection.filePaths[0]); + const token = randomUUID(); + folders.set(event.sender.id, { token, directory }); + return { token, path: directory }; + }); + ipcMain.handle('output:save', async (event, token, filename, bytes) => { + check(event); + const folder = folders.get(event.sender.id); + if (!folder || token !== folder.token) throw new Error('Choose an output folder first.'); + try { return await saveUnique(folder.directory, filename, bytes); } + catch (error) { + if (['EACCES', 'EPERM', 'EROFS'].includes(error.code)) throw new Error('The output folder is not writable. Choose another folder.'); + if (error.code === 'ENOSPC') throw new Error('The drive is full. Free space or choose another folder.'); + if (error.code === 'ENOENT') throw new Error('The output folder is unavailable. Choose it again.'); + throw error; + } + }); +} +module.exports = { saveUnique, registerOutput }; diff --git a/desktop/preload.cjs b/desktop/preload.cjs new file mode 100644 index 0000000..b6fa60e --- /dev/null +++ b/desktop/preload.cjs @@ -0,0 +1,5 @@ +const { contextBridge, ipcRenderer } = require('electron'); +contextBridge.exposeInMainWorld('untraceDesktop', { + chooseOutputFolder: () => ipcRenderer.invoke('output:choose'), + saveOutput: (token, filename, bytes) => ipcRenderer.invoke('output:save', token, filename, bytes), +}); diff --git a/docs/screenshots/metadata-comparison.png b/docs/screenshots/metadata-comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..55c226d0619eab9d94d2de5db06e39304907893d GIT binary patch literal 72633 zcma&OWmr^S)IJO%qJ#+2DInb-At|lW-5?-c(# ?ITKit>zSlYiE67P aZTNWzS6-}T|_zZ{bLmp@{%$%Bt$#p#;1`!-~T(!Mm(Kh9S$zDg^*Tk zbG@oo-FkDixv@pka!d|=ouL`aQuM13x|O~hy2D5hE26_L Hf0XTHI`XIk|{;l)#5!kco-uIW7;QNN1oODHiZa!dPF5kJ4i| z{HZ=cF!A_Jl$$d7F~ Oo+k6wJ9{)|o+T|qv`0 }@Z zIx>cELy!jj2vrlS+X XPg0U{{D!tEca8_Fs!FJ#wlq9RzwnHWN>x4Mg}43 zwF(-3Q**T=y+C}L9FxC8koFR9cz%;q2SVvPRFV{2M^T%_$9bxJ2qSk(cK>h_G|Xq3 zsQwN82wwb6M9Vv8#aZU@I(5uJlm$$x9ywxa#;6j3U};?*F_V9sW;sCm@p5+5HSg4n zoo#EIK$q)PCju)uoEVy<(DIJBa44?G2UuiKo`M gP*1a zo)Ru@s@zUUccv(=u7) ^6{M1P);tKV0AsXHGn;m3`$|Qxp^Ton9Q{^T{<`t z@wkdUA`+y=`ucanirHVYwW=+oB=0YS@`1hWVqat18qI86upa8{td>vz+2CZkJmLSJ z&lbMf;Ih#f9xX2@E`|;Z+seqvjp6KmS!eI^5RWg`s>aU7!*0f9h{ca=&60BIfeM}s z{Ct7Yl$u70{OE9sN$Ta9X=!QJlLaiG2QO|;&h`<=x-2@3V0HV90I5kjAv%)eqx*_@ zDg@%y8G_qsbd>n4Nh;`oDR%I~yKmpx1I`Gbj{{-R@9kMJsN^Z+N3WI}l_x(#gFKr1 znFkXhEQ!+;lT5&G>tL?h($xe0UB@17jQ)A`0;GOm=gdjPmc Gu z)5Q_W@zaZ9CxfaM@66;W#$2CSqCFaxa_pDb`tG!CXXjmh{c_0nj1vVXJu~$;{_(A1 zQwO=}HScL@`T6)Lt9GY~3qwPtkufkaBPGL$DD&E9{QSh 0$sl>9tZy!_&lxDHjs$xRLaI_ z*L|5<+3m7cvoa?Wr;oFtvV!4odwYFvVBQ4#alwRsNG2rQt4kmGZ%PUh-I}CS<#5Q> zw$LS9P*v#4?>OJOfXbcs6B^%Ef6Y>;+jh0d9T&)IOs$e$KU`q3)dfd3JvkxAGn4^m z=c#Io$ c9-qA zwh5_U9)Twf)pTcOJv2D$Yp4}ZXJ}ix@b?23t%W?r&{LMojEtBc&qYuV9SQgR++44L zY=wAjsfmyepI$MoU_8R%`TqQT&7Hgu4F!V*YbHNFeu9#_I 4_R=%ejL1;sYHeIkLJHzhu$Bv|YDR2iv>h ztzjopL$h4{%re1|AuSZdKWt$G<8sRTe}7a+f0ZfT+b>+;JKhP-tDGi*coNSUA!2iK zp{*Bd{>&?!gh({yU=d&BFbi6jt$ihp;t#fZf3o~awX(*}dUOn`h7-|-F7al%7xDeU z&eT~*xFBVEaNyR^aA~fRY?nu7M-Qlm=LGC=h&WidA*a2|6S{a$ClY)SPbxQ2^W z{G )ALaDD4No2G^&p0?QEagq8T`%J%hU(aXv5yD}t*t6ENo1jq-S zVwbp1m6kZL+1Q$T36a{x$x41LD8o`$@{3WXxv#dR6JuK5pMNnLPjeeuJMIUf ({jffa? +sSWS zxZ20hzgid%2Vf$L21VTeCKNQVPLFVvYxL5lm9pcYZXe(N8WNtHJM!Ga+`O&4Cavdl zZ_y5P;LB8{!-b>3Z{qve^_&R{etL{G6PT4C*T>og3uo?pQ=_5z!j 7V_ba~V*EgwIG$qe_Dl`BjAs2|mB&< WL$^x}g%s-WX@9fHiwPn`!_JN}l;3B)gP `!hs zi?~Y<&$at}-|Nilc%9q%^vd73bGjdNbyD~D_p>t33(%ah_%&Pm3nS SD-eKDYbUD7U_=_lm{I#kyRyQI0Ezpw)`1 zQzlD7o@{;D-m&qB09hy6DChfvAEG20#SOYU!6!*_a`jPh#c6}pmmsjuZ?LOSwY`%d zVhZ3HIdb$;Mnez8G&AnpCub#|% I#Z4)W=e>Z|+{Y z_YoVwc bkI|@!m#> z>!@}`ln?bS3n@js+qQr)-?B?(Dd}jt=8$`jTqwbOklFQ&qwL>(9e-}`USDf|>{v)d z-KeitvXfV}6o=>Zf#F!BS`u?mi$LbZhQ0ypJF*>gd))NR>MPj@ImQsNr9j0VcwFkd zxkdNIfe ((S4q>G3y$%_%$XbmG+X}tLSb?XDZ&CX)+g)HwWRJ;JGfth z8Sx@8_*FdWup=536Nx$Rl*fyWQECHsRoYeN>Jt~d brbM5;<+LotF zlF5Bu%6_GC@6{j%DUz71VpNM3b8(KSX9EpvIB<-+yIXfjiG7K8f!}qZ4SFmrLoZK; zQa^?!G`Zzm2QX4cdac5SRRx+hBRP9{#vN=^luDwdk 1@TcixrTi~Sd!kTady zaj7C}R+SIY>DLf2mq|o<-|znJ189L-B>dQ14VBDaeW aXMdH2$PcowvqRY5hsuK>PB|?Mm$_nXHBgI`?PuX4`LKwLg@t2L` zmpb@J0;}+-=BK+dyEDFhX{rcD1m`FI_l>&@YH9}+voSyiw(dBez##AYaybOg aA^@FaBdF{yr#c zktGAjt~8*#>l@PzC!b%mcuz})ceZuB*F$tSk8hnB{;Hst_?qT%cm`(yCLZKQGa>98 z-~aFk3$u(I3#qBC^*-t8LCD6fbtPr7#R$V|#NRe Q;#0W)1c1gn}9+k z=tX$=7=^HK`KSVCo6uS10ZxQ?^WR1a5}l7Rk)Zz 9%>`K-!f+zCH|jeoDu?yH(l?H>N$?G5qYGANv3Bio zCq7DvZle$<=k9*3e!chIwLe-yUqY?>^|l_DWy3;~8$YGtcC%ODoBCI>tb=7`qpF2! zB0p(~1sv&|+XI;IuFq`7bFRRGH-N>oij%4O4j1aROLXe#m0_RuYCSIQxvjNS@|83X zDRrAu;n%xa%70?U2LD$JP*PGaxpm*2()aQN&s3~~UcTgavAMW8ypc=grG7Fz8Dgbl zSKKuMD>HNZX`f_+VF4_khww}c_e@?XO!q@f46A; 9jVBa^u8H6b3WpM%pUv{o~^pZcTM{ 5PT`l&j|&`82Z?(Y2l&V%5$zia4`Rd T;(UW;1^g1nQy3@_~IXTt7>Lev+x-k6(abW8A-s`6@So& zjpPj;JwCqa+JX1|mHJv=bWEwZ6!|H qzJix_85V#q)P*Z+{ z00Rg8k^jeEGg{QtaJL7d7-Hqf-MMZI1no=~Vvx*~DrJXINxc>svzjUjiWJiN0dP3y zeXUG53b|*M>F_U$$$~y}2xNC|ZZ5NJxm_ym#e9`HtkN`r927<(eD9mapM`{k^oD(- zo`9DsQ|{D1a9FS6yLBj@EZ}L=e?$cJ7i)sJ7rk5_k3_lElNC{W*Zb9*Z{gYyb~4e% zdI$WE{Ob7Gvb_RFED}tjqN3vYaq;m6?)b7)^WtFL53<|o^otRs#8Vk!gbmVbt-j?c z+}0Rq#NkXUo9U&f=$=y}vK(*Uva)EDJ)@vA6n=_&{)nTbhD{V=U~&Q1pGtIDjnn$% zCu=df61{k&q>Zk}xtJ~)AF>ln?i
QVN#p-u&K-1&(%*W$l@h~(>XaCYXn*@_4=`q_QZ zgsjq713>m6gDqeMLCYx*EvMhJ9{bfw3n+ &eIctY=_gVHV)1aV2b#x`I!t+txNr zHG87dbx%t(2>$xW2Ku$p Y$HTHNl82oa|v?jmqdEe#uOe~19sOhWRFoPDjr~_1P!J+ zVaKUyg=h>$VaTrL=G(Z8bGSw>8Uu4fL+9uFjxSsvrz5TvX1eK`9#@f{nt|1mqN$}N zbg+8>_e1_?_{~7Px4p68NkzfsBQp_3^+nWEn HiIZ>Ymm-vYSL;l9**XVNSU2jjl@|s&Ctoy>u>Cs@6l$8cdQtF~$?w&xh zrwj-yDKqs?6C2l)W~j2~s$qlrKa&(U@j-lA8dg-&x1Ue86aP*_u=KcEKwN$GfB!6V zgv=Nm?|id2>p-J+4ll5_)mKy?#3LfAv4Pgs)YK63cyF$bt0;{24Gn=gs6DUXs%iW# zTFsv2zGBo*2PRt|pi{lQ36W%i7PhwOrh`>gAAsP=%KE|;Ij|a ybCq^>QDq$3JR=V7BsX_Amr{Zf)xIf*)$*ZxzQ{T z4K1z9)=0?**q3a@tm`<2USc_cpxsz>7?1@#BEDj@t7{!qR?w1le|#~OB>HrNYS%9$ zl$dCiNdV#jvh@TBK?AzW!ZZelFo;wnnXI&QXap=F-dWqsrYu egt_c$3X{mQPgms0dG&;JCb#@|^(#UGbq+Mr>ec_@sL_{?P zeI^j{#DPtf?2{6EsK|v@Eu*ob7b@re;`qpro11Gnh>Ve`874uhKCMMTvC5(+GuFt~ zMf{;z;K%i8QthI5cXw`e>H-upRAwMzI+Th_^KVD1_F&!y>Td9A-TwOKA~I4qyV2E5 zttRrU#<0^uCU2tEWU=MpQ<%kUg`nsz5DUl3jd<$c$Iu4$ A^+O^T^ z#&$>1Oe*R5?ostLOriN1)AB$Pxv{Zvev52XmKCRlK+)U1y&dp?*_%N_CKJ1p4Q4AN zdqIM$^Cu)zz_I@s_~~G8wgv17?*$8b^blYMx?ke^16k#{63(h_y}hcJf_Aji&IHy+ zJ`z3K`E%hjk@IvA**t{5Z)$34xHJLw%5F{2{j{HVDIzp9ftsG49(ai7^|mP%dQcOS zW(CRecmyeb90Ga_m5|$U=eMdN6*1+<=@j)qejEtBzCb}I@cWec4TH#jZ{|1`>~3 3U-F@D^Q@YU?$M|w{V>0<=_xxlH53eh&W7DHlYjFtQ2~Mna 6EE$HhVakoQqkxuVnd*xP-&d2~F?-L+ZM^mMl6mIieX!>Em6{UB?Z z5|pcbfL8diKO$M22H|F;c;|bQt=QaDfj169%CWBgBul+pfHFS|Yt9Ti{!rgo^%DUU z#>~tP(+Ay!Pxr>;8i%>;PFXjJe_7wtA^pggQJ-KU+SU@)6@Wo(_Yd0aHU9!*XIIW& z%=vE{x9xqtPQ85$jqK%aX _)c2HzuVG}o zWw~~%0hAX#_Xls~KWJ84%+6NQ#>BjRO%mk3lfUp8ezE8xC3P4V^DZ@!*DeJ8MVX-0 z;ys#{Mzg1#iHVxbfI+cDU!}-ue@vQJsl_CNa*hn3ExK-xxp`80vUsKv^Vk5}rbT*f zQ4ge5KaL3Mf0OcG+bmp?CWlAB&Nfm?=VxYmkUbvm%r(>rY+wsgYX3~5LUUi8 W z{yc@^>wDfU2P$Kd!yc|S(HPX7KP>}<;^uTa&=Y#GSjz86=e|%kS^-bc_fAVrE~r)^ z_vQs)LT(7{!Emt0jajQo#XBBIXN!u9r6TW#o4p!Y^#lS-ZSQa8v4*LFv}&x7o%xE_ zukLR!F1-~ocGYYLK=+<)3-##+aII w>KEkVg@ax|k6&ke76^*XVL}C4Ms^{~Ap;jP`3yN5}iWmUA`M+iPn<1gzBS zJAengIW+hbs{-VZ%=*m&uU9}f*11d~?wgOO^bd{o`P|mL_`@ZY8d*fk`5a?u^EHRz zt133ft))MImMJeRug}{2L(k98lQr%{E`}IX8CI>&b|w|y+9DyPFE%(i9KyUSd~$QU ziEW!`Hl4Oce3vj@k=K8QxS#3N?41CyBFjHF{ov&8Vv%k>m~!| v8H` zQ+dmV1_r<6aY#rea%35!Zi<3<{oG!tfiLfG!ZnBq2)M0!covH!K8Kk=ns&m-A50Qh z(j*RwT_QYB2Hnve4;IYtu1miod*XR!df$Pg3Z jF!dv2F7;&;p;5&XxlJ-~_F z^N3}I5y P%JW6Stn^Z*_MS=0m+5^dFAh-dU zqT=XRwck&cE_1Nm4y7BCi#750jKM9=a_(^2pRZL9z*>#FMK~Nv;};T=|MqWiP}Qu$ z4d6O0PP -`8q*jNCXIcJ8 z{{R9O8v97TayLLEbb+d-h)) %)7 zRhf?WJ#k(ScbHe!BSBx|7$`pCk%Lf~-w6gycZPncD5S_F6(hA#6fud>(Y1UxHA(zH zm;US{&zZHAg4@xONY5UH6~E^tCLzXG)dH2ME5Ur#LcGh%9ympn*mpWK#jzqyPEX6^ zy2IuvnPWk Cc~oLgqY{{!w_sIy%sJY}O>r zKJ!wk6sqnnPE|FVK`f? $-}17j0 2Q7P;gfQ-j20EiX0(?Ty-FFY_*mlj-m2M;&h_Ca|~ HmVzQfu~sP})d9hsa4!4r-w^CqfrmX46e=?i ze$+3V9Uefic)jlJEl-)2?Sd_?AUf_3&(8&qYA?ee0@zF~_YFosmcKJph|8$f b=KzPIPMm**}TRt(f4VVEmxDzbNOk2%Fad-Y`@tl63<8>nd6E%|vU>+7d1 zyg9g49S)fEi>WP*$-!RX(Tv6h@s4)Q*Cxe;o%+c-edNwgE-kGc!2$Cgs%YE^qka50 zUuPG?#>y)Ymmn}%l;q*5V`wDsH{ds!$Tc+wd>X#axCd?2KcD3USJ9VcNj;G8pW~98 zQYGFVf@o{ZmVHwvqo_bHo}+(yP6 qk0J?YlgUE;7hz 2O5DY~{~;r0tw?~%{O<$~|0*rb1F4|P9|BK$jp;_v z9jB(IHaKqQ)q8bL0eS}o9sJCCF;7uU>}+?k{|@!dYwLx}v`Bg7DEax?!#cZF8lP}X z;ncYjU1H6|7zh1bJ(>!!RUl+kABaT9#brB{#|Hz6*3N|a>@T+WYuQUn?~GY?Xy0u4 z|9PpI1F##N=G15~xa7hO+g^8>N(WNlWiTI3JiRWZ%%_KkS1e9K$>)~wt|A$b=;}SM zzJ6chZUS)aLfh8A*O6{&GE0AEV`T-9Ne|_~x34Q&ZJXHdS2{yO@tC(qv$Xr;nHQjz zL|TdoEV|MC8yg!mkDJ$5varQ6SSf3Rjd=x$sh2R{)~u0p_u&BwnUSj{%>GHOmUE}C z6#KZWG|0`#i$$Hn6oI3rNJ*g#g#TnN{jTnG(JJ3)Q?T3I$m*f^LBb?X%)8g(pJs|? z-(Ja{_L2n$F%}h*7riOUPU8nnqpr C1y{}aJ;|Jpy7k~`4*;L*Uk?6x3 ze7(OS2XfZ}NhoLdjr2jtV7>1(&;k(@F6c9JnPS`Qb+yjkP$+t?*a2tKu6fp)TcL(k zXXu|rl3wVv8=t9gchpWJ+yd?+; 34w)o^q20 z6=`E0;xTPL-X;}}+KLhuE;yGhZMwG 2hG6xDT}T!11zRuromcfk)D< zI130v=GLsGjnZERaq86(K7Le<5Kd2Aj`fJs?Ycsn{-FCI>bM`Mhpt?`t5gp{eGu8q zCW3#AyH{1b$o3 _&EdM+HdQ5KHgfj`8@zCD?VHom?f=z zmn@j1*?yfUJEy5rxJds&P&8 0}*+=()6J3cJZMGlU&H+5O$aM z)1tLCi^uvJYh5g$X{4z>l1|0! rqAn0q$24RScTZ&pIv*Fus7t{{NG+|3Z( zA7J|yX}vEFzz>BpS+ggX1zS`@C058$GAAOnf56Xcr1?m)>2V@dALy-?!Gxvs1I$3n zgTG5TCpkAax7W=%teoni{JCzU^ZC&a9wkDNwu$`Zm+CBfO?4KNL`@S$wDR6kgf`P9 z%&&Kqm6fsVm#96Qgid4@ANfleaGF5BdD=UU?-JP`G|LUE`Eu;-?UUJ-ncWs60OHso z9gj)Oy*5>>y+iDmIP)iCT|^HJkr_tW=C+fcYdi=r32~7I%!P5EH>Z`uLRZ6}E$l zl;!NOor!#3o$_=~4$Fl)b$}3EfCUv15fyiO@9xsOb{I~HMI#8 uSBV*Z=^JX1xKpn{(8ytdUH#cj_Va)Ri3jF+05_KrdvwX7& zW2gZSdJ1C5rOexMcFLWjjBB5aviq8 aD7l}{ zdcQW8`53Q&XNV^e*suNCxL+tCm^aEpL`!#V1XwG?p#2&kxl28IQ<^RJ(gP|kEb_uo z)0^|p*))_f4>v8C_lFO#>4y5uZHygVFn|Q0>+SB0jMeYnm=Dc22cTT}bPFS+F=i4h zd;s-`kKa=&Y5M$%qSCj5HPw5DhRx1ZGg4A+e6W~MV4;}8t@r~#_U-M-RV0=5WTolF zVT=BB!KAM~z9+0R6o2V$uUyKd{c)$)<+6%ff(jErI^H%Jb{d}fV!->Mkd5CIzOnNq z4CFtiiZJ8AnQ44ZxBIo$8j4@50kYb1)b?)?v`9uma+-LsB?d@t-?^2BXqP`#mVtx* zBuD@$77VMmS3NBi)w@iiPD!bxzD{MFSzfxI?f9jhfG2(0vi%vBMFi|sTx~KUS~0xb zsGZM|ZVl$H;w&!Cl*nz(_hPC@^I&sY`cdRbs@~d~=?oRdO+J|}SRpy{O`becwLK%L z-JKe)Y@%BelBL*gUH=lH*NjmZ+#Rg@;i *cIwk+=V_FM=F|Z7 z*1rcu#r^X$DxW+}4Gbv0`p#BT9dU4+r`U3v;hKE^-t=%kaCynu_FcU+M=tSA1W1BC zIApH$hCLkQEOyi?Ce|$+G3=unUhOxjy@cvcOaISwf^nGj__#PpDyq~PvryW_m&eH6 z<;Z8WF%&mxC9QEA^=MUdN1#tmC8$L`@-^D(p)f%U8E_zDgqx^OXjxj0=q1BZnUllI z#Jw^i<;x%LW2;+qPBqy7dA!WzUt6}aE_QnBLXC1Sv|NgFZ`hk~8+vv=pWzy8NR5v! z92Mz=t(yaK9-wQ9Qbj~3ngDGK{Lt2(f2L07zG2s)yw}%eCmg3_>_FbV35D=quSVuB z)4I%*{fj Vf!o{LfRa*a0*Q=1UR`YT{J4I3T-Oy&>=!8i zS$_C8_2mtp)6RRsh?QJTJj2U4_4`U2=!>)WytM@bXr|ih-HJ4=VPLOwLaKDq^p)Ey z@z_7eXEN17M&Fk5fc`B9`vMw!2X_=elFPqe>f3YF9 88os*)$C7{53=C)% ze+8le(AxLcpFaugUqbN)PfG}$sCMF=ro%X1?HCEB3lK(@YTRi~r2MK_>oe_jC_nR- zYADpQJpRGKCE!UDFP`M@&K_&52BPm (=zL{v{=;-Lk^!gwKp!`Qx zDDz;ClqB5X_ENoktjd`OD*LRFSPpk}d>i^ZHfhI(v+al_hyUZ(BWpuF-kCN-fS>SV z+}Ui|v 8UL#?sz> zjI;8Lp_Ez`ZfcMgkOCFJx<~6^%nZY6J*R@|F47964oueF5<!foYb*U@!QE0>p`) zxPEHddW(woz0QOdw(;y*E?$AY3DtaX#(ILKk|6%uT1idBT?e2xzW!d4n_!mdK5j9g zB8F%Vt*xCl^LdOBVp(e5pNq)4j72^OqfXvE9UW+Kaz3|0rJ=Dt10rL|)9o2&%x1qw z(b<`Xmz>UfrLJuw?TaG(0MuZ)%@l@v)M@blwP)p2_)1CIOke*E@CH?Jqci!^d;C78 zB7vYtxE(l(Be%08W4`lJFG-d7a*$+BdCRhj32Sib=dr#2YUaI-9^QG_bG&-2GYbs;Yc+W%-0f}`!BED|N6efDOyv*`heWBugP;Uyz zI#~zd!TR3^URH{&!Xw84o#i-qt!>Hioww;^r81tjCGrYuM_=4YHA7EJ6rIY0`Lvew z<*rTNGbF7TeH8D51lEVPk-*M$l389M{G2%H%zK5F+X`K0{D~WChB9u2dS`1)PnP=I z21`k35PaXPm=HotCRDAH-e)>GhhDb+=ljM$eJO-ntI!S-T71FZ=b+l}dl}E{rGQBO z76r9}RJet#!t0EKQ6Cl0c6*HL@{%{~ANe)g5PX^{3o+CoM%S>@5cGUmM3-8#@r zjH)Q$ciMS}TE*3eI#Z>9sPA`-@nVqj_ot3RE;PuA>NopTbgg=O@HM`q!CI}{A!_$p zqW|qw06HNZTAUZkGKRN7*lqq;W5W8blq8xOk+;E7>9ur+Ek`pJv?a13CDmm9r4&Li z^`jT!$+)zejb`QpZWu*HVzBgwZrInaf{Odt@b*5@>v+eSpL01A>YlS?|Y7*0!h) z_*3<>a&$QBbD?KyJ_7YYMx@5f^b?k^9#%@z=+VN4h3wbftP_8zxV^uZr`P{wZ=rl4 zMtF^VQf+eT&97xCc^D8V&y0`lZ-&C<#Di2sm;Idi4=KqaftD3yQ4w57R8H7TWpyH2 zNg;Uk9e*~0331>hiX{9JyVU@<@jY|%7MF_tnB1X_?8W)y9+y&KHam=vp!La7-I8f$AUv6uWeSGOTZl? zMB`AX%mQ20R4#ng+^j|6;ayLPC*8F4E>taiPoKuSvhSQm__M;un{0ZhYQcZLS+~h> z7JmDmbH X%! zi{m9rFoTAZN0Xp@hnB?#*UokEH=3khBszcj@!NGe12p;SE{YvX2urP+FM05g#&qj~ zb~s%Jf*VG@jI+n8o)qoGo~1P(zu%+^(k|7jGgHsiZB%pRa*USXk<)jwT5MLmIoGSf zm#1FKsj?Q(syY}!ETvuGWHI|H-q$OvoSN*sHS(fVQZ9LmrBkuldO6m8MdZ7g(3?4E zogFc&Q&=|usS7QvUlp&1F1cJQsDAlXO6@2|Cn{XnBFzyBTwVcT@$6iY-4PLKXsj1T zRPoxopsE3nM3@YpfcLB0e4)C_?d0*PBYr^dKUKHTmbYYNHX2LMbnqgLyOfeN2=^Ha zC0xoUcQpYFTwN0DU37tjf%L@CiX0=clH y7{gz%3AUt!qg zwCj>JrPJ`I^+Fym7fu`lhF#6A8_8H9QPJu_g1OExP(~MsC(O2eXP;r1bm4SgN7f}g zznh^kC74)JQZffdE^pB0{p;jTYnuu(ZvoyXcrdAEV1!Kg<&ggw$#A(NSPYg~a0tKc zj1o><8O@va-Ig{CiV~E*e`mHIi9Am<3BS!e;}Fv4g6L<`w>U*_s8d3Fa@p?W;~H|d zg*kE_>ZN4;zW4mhe|Wy>62n>btqb(&RITUaWiA4!z7J UH52je)W{wYs9aX zIh|~j9vJ~GG^Tz2k`FS`CAWj^Kbu~B+G-s8v|E}F>$eX}&uo`3K8Qo$*r&qZ9>shO z|GxGW;rIv@Cz+8ijC{9Hm^DE5iasI111R8=kZbCtW!5G)SsAtYO|MJ*yj%&ZGT)LO zz=zsOG3)c(k3!KWsNsq8!23`mhMV=~+q#oG7+-3|!5JfhKUA6xQ{6gU>~ 5;d0-;zabWo*p8 zgt@G|aogA2xl 3 z!S()2#C !D|MO`y|!t00dJ6;iJ-O>X*&!uGDa#%xsPFzy8^3F5Ba ztDpI=$!^8)4CjDu&jPYgHv`0~hKVfFY;yejG6_i=paBWisn&y=r}OYL^2fcWPn40D zr{4NA?%U8|7MY(d?0p|4s6ZU+uO!@@Ke8ND+2p?S&tPTO2Vfm_l^Bc%eLR~tGfVOn z8J*l!=7jo__F{lwS~z0$8PLz9bt&h&rG6KZ#`Nc~;SWfAcM|vuLxgY3kmr92@Os4F z*jNtz5$2Tz|Ne1&*y8Zeh41Pzo_#?)+W2T%_?iFC{iA8o-a7vnuXW%WpH2)W4#>*N z0#51bjemymI8;s6wmY^|@}rD+T4JIGr2kK&H+ ca%tlj#;5xExPzHZ@JT{K6di 6 zYan@|n<#JF1wEWQu~*uYFAER$Qaub(bUtc$Ll@%(62wadYTq_VG*PH?bzR>GtNtuj zl=kK!P#l4Lddv}7&uRl^udZuGwf9F|9L66k2q^e1GVP%L>bCZmP)X9_Ml&KUHVARR z6CFu?cDv57TLPYSkvxYK>68jKbCFo8;;Ccq3x HIwPWJcU|7q(q zGTJfPMej7K5a8tOhrzgOZA&b|4*Y;uM;9Ai7Uvpnh1c=4_Wk^trZRjZ-;HH7bMnS% z^ocP!N-d-Bs_T34d*2o!*U?oWgt$7ZFkM#urd@YK$T`)|j+Up{g3oWJ4rn_%?yYpD zqwcfBe(cj8{It^x$(P&T{V1p+J8aj;wtzc++h9i2#*%#X*t3BHwTk)r+R0RyA^Z3J z{$!!YsGl|=Az=?C(e@V!lb^z9DpT)t9@Z1~1FnPLC>j{x58jF-521tOq$I@d%(Ng- z{rsxmP^`Flkg!l+mY;vlJf^&T3i&;gf@W4Ta^8qzr)Mm2;hQM>83|61?uYR)j{UtN z)%Cs1FOM3M%BLKs(OdjRvyI?hy2su?N=nXFgvkH2IvKWqPp>pka=iV2b@l)EUcmqN zXPo4Vo|UbOpd-Im%fRF_6U8O=(*^n4KKmD{EBpG2Dat060tyqL<{p2M$fs_hv62gD zrdRoA+Chu!k2+tr;2mN~YPjh3aF^r{uRjtAJ4Ab&T_h6FYxhfR8QPd7eD69pkhJU? z%2w*?OGiREoZh0s11nS;w41F6>b|Sm!hSX?O5u$qL3r}W@`<@mVNF+zXFRG)$p5&1 z_Q>MIi0as0U5hSy^W(5SYMd%nMbDGa{u6ORqL+`BshG7sNPn&yDL3L5$^M<2+xXa> zmpK-IsF&~O*)vzY%eJ()_rY@N26$C9g=4+_^z;-s@_=?jp5N4;pd3LS2?+Tju9tqN zr(u9vBle2&U%oP}o5!>SH f{w5d6ZUX z(}xq!X*u#pKbqE vG zU>n=(QeziclIJKWm{P_hIx7f@=c+LV=%`ZRL_W2=*ZB2zom?b`EBfeEY9d=ceL$ &<9Yng28{85*MU$n zo@Me9vaQV`Dv>!;MfrdP1uQ5pqmGFqXpo*=_f7rQ;Pt$G8S&0;ZzeCQmZXdB@6rzi zVw?}+YGTi`Kg u6{COu5TO@w z44DGDikK6Yduba9v?PK)e0MIC2sPQRuR;=lq)ThsJ~65`NMLxSL$zX7Kwtg4a|KH3 zD?0@Jx6SygoT@_jczWKx6(ow8VfkxZP8F}!(B{MuSWD+S)M?algqYN_(9dLl_1S9; zJPYr)?dZYJrPfa=7xfmgw^YHT3Jg}QvRlRT{E#i1G@}AIadl3ch3&%3d~7GD-IWuM zx_^cy@s*zA;|9$p9A}&kg#v)v_u6@sDK0j`RMg>zZ4>->)$Z4?tk_zJyVG=o3T9v~ z??QqBc9+e34N`lM_mNs&!|zwJC~VMkyITu7LF4B%gu?fe0PeJ-u#de`HfzW;m%Zs@ zWTh|;%#J1<`;i!`p=e1MR!UifC_@=GrfaM5nM5#aL?NMHih>I9W079AhRXBIhVnd$ zfRZ%jge))5mZYSlon3C@yvVrxIOD!)nQ)Mu7QC7;<+Hhh8K1$`)w&KgokxfwXr zbOml{U9zJXU{ZYHp*XE#%RIyVD7bJGluf+{>PB%&z{~68Bx)-i4}iG1uIdmp7&e`N zPn9oSbwC&N9gQd~ls~))?=@r8#XmNC4!AvklPX`+wzWP@_4iNFrF*}cvT~XjFr^B= z>QG42$hhk2vG6{^#9Ko>PSb#Xa8gf!IkRui1H+iK3k31?$U3q&& {f?&*Gxe3nt_ibziZz4sp3 mJ!gmX}e>c5<|m>zYGR6`aSNo=2?` zYD)S;v&1xn7G*i?ia&*ZzM1|jFNE{(SUsJW1w%%{S(mG2`XWdr_fW*pGY@kGqD(Nh zs3;mYH#6yOwAQo8I`3@UM~<%z>~VRXF5=)4Gfq$-GN%k8;|_2u#D8);)N~)Oj`uZu zlecj#x;uLS`-S1=Z3(#h=3Y(+C!dwYrF5w4z#QZSO&`97{RY>Z)cv`m=wjHOZ zG$tIt5R{9UiQ;CziyILiZ#;ul)K7}Q;zT1+rfxYFb~7_U%502>J73s<@eYm#_iL}` zU^%=kgr>q1Bb94;o4mkTfkL%-%RNH^=iZ06`$2W9)#qoDuH(z$v)wh6nvWG)n-r3I za iC<58-6WtoYpF%Ia{)Cer6nN|4E5KmMFiF3Z5Cd?(TvhT#vv@$U%Ogb wR-#cMBzJHOHkt+G+kFZQyqjBn}`~0pC<(y1Z zF7Ct~BVa0{&7B0T-$*e!DgFFAWk<(IZ1^Ye=l{*n_g|b1|IL@_P|*rOV9zfmx}_dP z>+JF4$72-^+P@g#XnqKOX=-g{<2z3JX_cU$ce=>1IowtOV7JDFTjTfF-GQ(BHd221 zm7|)P!?0@P&+%32)`ULQB)I7Ei;E>NLH1d}%j-y|XZ`!X+tPWVdJjYkYHDgXwl*|5 zZLEJ&xe8XgxHA9euZk<$L`_c*TVH3f63LU?U01$&Cx`e;0=vJs`M_&eDm}m`N(kX8 zfnzAw99_G3f4&UZEcV?O_9Jkw{AWY=zj HJN2}dj6ct}(Wto79=W4X|@1>zN7i>4|TwFNd64SP~w|Acjpt%}t z_~!;9k)DxpzBg*os)3RwCjY$g*Y=Sae6$3T=ZUJU%7!~q@a2Dgvs5pHo7B|Q#@MM> z=RKvEemjs1x+kh!ems&*zkKEAev )7PkUm54rNgh+lRn7V1d)xU{Xcq_{xuOF zjRb7?bp*eo Q>7Pa>?ef>=2+1buu0_;GKYYGM6+ O@btyURih|l))MNN&zddMq~*n^=uG);q#kMHJ9ZeeBTsj>QCd>h!6?wCKZJU!B? zIVY(y>JE&G%DRFeXriN&qTJR|?YGFxup*nl#RTC$Z%1XNrB}at`MJ4`-sMro6r;kF zy~p@C-*tc8$m3j4PH@;@vRV|5)wS-lLNYd`?R(c=4p%xoG#=br?ajz=R>VljP_YeMkc^&WT^Jq~mpE_77q<}yoh0>D!`iN@K}(Uo##3w|^_G@Jc(0dxX_ zo-!MRO1Hh?YG9qQ#lAlz$>gvwUs>0J314F-0vtJS1YB~p1XzT90ZU!4^+C`qN1r9; zv77>V Ye}0S%ZiN!Qmp%k5j|8PxM7*=vbnX1{eiIz$H}SVC+qWO^c$@%DnWJ5E z0V|k|6mj&8hGaGq2{>Q&I4~L9lb4F;c %;K9nv%1Vs;pHw#tXw F>1$M{HT1eD>Bp&PZC`I^Iyg3;M=KYm^@yjolH=_~bKun1amBIqPa0IgI4B z4>hJ{6vxLk z|ZRJTrL;Cb6xhs>R8#c;ZMbYtY#{vTU&1C=W6mvNRoa* z+uiA{%2(#O*R<-3ik!&jKz 9qtqN4dJ)Cs0G(n3dka`gr#gkDr5W8f zG49=P# sq9d9}XHOBIF1a6TCk@%%) z5#6JWoZ8f_^G&X;ZR9i?xlobH$t~>SDYtKyl4|4~h1vFhjLyknnBrjaEeII+Fw6T~ zG0v5N!w(r5hcQc`&S|9RT&vpEpfR`%akFSdjQC-jb@OwurIdp8a}6|nRaN6Ac|M=Z z2Y6ieHa0i$!D;=;_z_94qC(w3=(qk3*>iHbj?d)fZSCzxM=R;7x$i)4vkD1drRL)z z8}EZ|*nD?A7`3Pv%sEccD3wJl!eLU!AUGn`udO*+uILof-kornRn0j%TJARq!f~VE zP}K!;y9=WhJXnB=xQCIvdDek)K6!3GQT@jEF&eq<{bv7A;!(57YLl&*N7f5ca&nS9 ze)eF`4z*!^{1I$GIZqDzqZw2!5vy&UMdeu#N;te5beJ@2M6MYkb-nXJtLkpKA1yO8 zGdKl-jd$ c!yuPtGL@jNr}lGakgA9*MWxnHxCKg^Wb zAbr|AIYj0ebXn{o1?N`| X8Fe>r9ZI% zdn@^%!m>|uBgKTn!~6M 88mD{ZzTwLbc*RJn1X**Y5VE1NN)2VqD ze8Ib0PP(AGxOhRY>mBb+vIbL{wy(gA;F;OHWWHuRv>g%k*>;J|lR?c>`dTVmRf73K z2XVMQ05;^I0TBcD$_WEL2Vjxjz6*}{@SD8uC;Ip9X%F|~lj9SpcCL2{$th @d1Z$?#+h)6*y>&l3Kpd z%1gqX0MnNk`jVHcMTYE}(81Gz_3>^V&bKc9)(f&YTpz(N#6?L7BzID!|55jNr4zgM zyfXcrFASo6gHs>C8w`Z6X@9FE-Pf-#iN2@}-N0^*Wq3ws-H9{#3|EHU{yxvinF|M+ z+&rh_i<;A+lJPO)B!2unjdM7u{P*+mk!2c1t2ukl#w&(lKnic=aXVVwS=vYy>^QrT zI`g)oc!faV?&9lbNs1HhXNOX=3Ctb7%_m$GtjeNysM|2mbuoebIaJwycfD5ps B^(Y~9%(Hl-vqyV8 AH#u>eTEOS2 zy4q}iBkXY6FWEBiQh;C?9cBapX{7u>S@~T)K2_}+_gM%AWM^mhIN6Hr>sx6ZqDeBc zoe|?ldmBM`xRhZA^Fl+e+wp*wR`IjW(4i_93+wH9$X#;VAAg^byvN=*n Ar0UZ>Fcg}3YF71l|&QZ@V_ zQ~-V`@%JVz|8SMoUtA3&U@ <)4Cs {B$=9+)#E3bxUCvYsG5~;bvEgh@nY<9f&X14K~(2wBclp`KQG&B<5Ig*mi z91V{g?a6m8A9hy;7dC1-Z{lNGe{bj2a+7QjH(x^xiDA(Oi9LhOcShx~;!mGYknpFM zmd;ukur6PbJUw$!2FbScm~8|m5ug37nZ~nIKe$O=hJ WDAt!)yQ%f;TAA^5>6__ZtsC_hy{;+!bqSZLPl)P;)+Ye&%4@nKX`m1Csvm5a@YU z_9xHJlGm?+(`UqCxeslD?d*8+IS&@Of9v(Xmbb+!HenXgab$x-Aza=}mZ5Lmd@0*1 z>`zl33EG=JzF6A)rAXf0dM|T~Ct1a#i!Qc<2T1q%PH!iSM00DJs+vh}+$ vX 5VIfQ4Un &qE~Ul2hK*712a7^c{ufG#t4)n0*kYj4{h6VF z&5nTJ2UG?HW$Ek3q|?bu(ZzGzD55fb9#KY-nY64w$(GcaeXyOlZZ!-Br}>j${CYp= z-2qgVB(Z?h1oip*Z~JrC)@Ej2# 3&a?RrJ0cV{LV)fSv5=AImOIoKZG|IeAgRg!>=K z=*@raS(##S{D*H)3AsM<$?5XYE~bYN)<6OzkM&$vFg|Gtc$a0LPW><$V4Td?se(V* z0gp}E<6FaQQ5H69 ^ zg=>l4KnvjE$V{SVUcdF~CEGd7=sO1oN(BW4GVvTSHht+D+Ew1*kG5YMQA5XOV7Nd| z#FW{{B-B4!ZVlwr_`8 aD^+U?IfHF1(i6XGQ|J!)S9L?-g^yg z^yu5a7EOf*Iu;hBBxmcb(I)yu-)9s>?Y(ZrKV0Q99U4jxI=cm_Qd)8Pok_H6HVuLO zpE$2NE=RqM0XKOX^)dsMk@oAffZYP+PgiYz-t)h|Cz7Y>D44|`NJ~f6u|Jdj@neNS z60CqvzR#ST9;1;8whoq_f*E4oz>$p&1raYiA_5GZ0~HP|ye+}_y)dd oeV#WX$KNm16&S7^J|(8-Klb5i=l>j1(F#o)+cLfbQ@y*&`!6$B1EcW#|@U| zO~O!DjtW0Pb4yDBOzu)hq0t#K$B6TAtLFjH)f!dG@aW^aM2Z`zsKO?|@Ffa)EWzra zr>hH&L)NGFg!&*yso#2DS|G#I5<_PhLe9j^!=0Y@zGw0m!jJ&J^-*Cb4l?}} kK4ism0=CyG>Jh@rSQ%fZ!W&4o_{F}55rW~Kr;?K zGT|(E>12S0A~p4^$!6 v0l zJzMc|2;b0gKPW6Mb#QP{4XZVzE0sPOefL1`s$WYBb3i~qBuxz^2e%n`+VU_+TXFJz zAPZ_Sgj*}tEY%&Jon&nmVdXh3kJD&KGiy`AgQLbt_j;Z*kNe4Oic}JT^0=PEp@D%C zm?uX|)*hFdUt!a`P7n-xq~fI2!9dO^E30Jq+sLLaC*$EB8qVFLv9h4tCl6Q^QH9f1 zH+fwT;^6HN2O?h3P1f1@?kh*&=r36ypx%Jt%hB;bnz1KWQvp8aJQ7VcctI_BS~8D( zF%)$_x}W-1?hZr&JJ7rb^EiA;&c2t<=(rQU!3tknBn#=&aKX!ui=$_9&= z5g2kn 9lcJ}t4N=vcgSH|Y% zE!A;TsBYf|_$~ud&JZ`j53#h6eDiUQyPkZP*Te69;GQ`J#-+qegJL7&76k{m+*H;^ zl4g?gZxr6!!6x;P;MB3#pF_L)8j@H(YEPPg+W^p!%AFn)|Li+b8?Q=@`(0aJl|jVO z%eWtYr0yb*4}gRRpc9fm%(~v5eF`luwgX)N7&pjBKbxwFfx+B%R5;LLqLK>c#KCMe z4rkl&O4FOdESL%Y$~9-s&&BG4PmlG0<)_KqK}02{Ier#ObT|{o%92$P2oAp+ml6i_ zjOQ NC$2%Ya~rgO_)T5(J>Svp5lIEyOcsK z5+5AQc+~ t2R>XW8?AMuu}CyGrNwmi5oAQ@bYEn-umg>$4A;J-(5C( zH7(r#!~*7h<)qxaJps{Conb(VB )_IWrn!a=sfO@R!4 z-*QWGx;yzE0BksNn#PSD?*Q%31&x>4|E$*lhbl>%g~)w0AZSAufLE4_v=mwy>9? zisQ^?JShD{`C1ae+ rvh)J=}D1n{QaS&a6JG6qZHX} z7m=5#qFgdze5d#dAMDM&+}NZftGVXn@p8LbQftYkF-K|6l643T3AQDY_>f_3nR)9L z#~fsCLU@xZf+O6XqVEBj(9E)if3deA)R)m2$ CKHv(sXgA`(5ptI^r5z*nQT?K zzX)0MN}lbWuVTAg@aU)gv6|jA%StDmbilY8oi9J0j-|nZ`GRLI+Am{kD y6S z(^FsTwjMHUBPA5Lsu#sT#>-sqmnh7fio&M|!mpk<5n>eFTY06*xysT^^l_hWRTq^2 zFI(>7vIJEJ5^VnfO*5+(>9)m~(b1`5$di-5502(mQ%L2zs-C6d7DBte+~2pjczWN# z(WzgNzHKzC$Yij!bveZ^mFyB8dKWH!h$hhaEu6_jT zJ*ZQI#p)eTk3xoqgt`gn`!c74uTZ+W-kM2Ed2)gkWj;}f9dX;RNUv$V@Yy+z@(uFN z7ev?49__CN7>}1nPb{uCN2Go7BP^NrLB^$gD(WUpWpgVI9pUTlvQtYd;$c@uPkPyY zd%eO2 g@ zt+)RAwNCJO^#_0- ;nTw4;4xo`@XvUh z#`UmBF*B3!>=#(gI#e2l1!^{hRu`MNfMH6*=!j5H)NCL-y300SnM)xtZu7@V5R>*& zDkIA@G9uoK5Yka6NLACHP8I(#-^g+{?4?h;INxviEq;D^PdKJMkymDKIqEp5T+kju zrfKyQNFVSAi$AcmM0anzh7oaTY3X(RdBVZlA07x`{>5&GWP!cC5{X82Z*OmZMFvPu zZEPH=Z{2P@@@>T^GK%jhOfizltn5d5Y!n@KkzMIEoV3*0QN7 Zu5G^slVKFf#e^btB0cir<%>GiG^Q;Xv-| l)&yaRTt`znaNMkF4% zY5c^($e6y%qxUcH3##Pylw-0)3Uqkhou&{JK7bWXOgWy+3Ky$zaOcR~y-M2S9 |p^X}6R6wXx z&3)1x@8c}eC&tM^n;;`4g(;SfbMc%&3WOC57Z-c)!GhI$#A*FlN1*vW1ySEHz?BI; znu{!J3MG!&7f9kVE6* D|dKsfS25m{;=i#J`zPC z$O|s$-axEZruW~rNRK25bW0eQf9`|Coz~_^YxFj9<)EOTJk`#uhp` 37!6mVem! zOOgGB=>4M{<2~Ij*1FA(>_0phVL!D6^btUP(;$3rXFYHo%+E*ICnzo|x)rAMOU$eL zrFJG|)^dKWZhpDcIds%+WkJo{{r4~7-~3-!%KjsO1d%=eU67j&Rpzb0&`@kK!>@I9 zk3cA)pkU?TfTKSEh_Mv$-GiSI(iEwF1%HJD{M&%fe@&AAlq~#x4(NaLmo?BaG39ui zzf4Kdk$M0zC6(f3w3+_?*Bk> H1oiOW!g;^k4 zNYmQ7HiFX6u>AlL fc z{1&8aY)^d^1uh)6eFKP=i-^$ZLenjTC;KU*mVmRZq+ 8@neRYYo2gvDC^HcG;UO0AOz3gLhu(Z7AEJK|C zT=TnMk&e+K&%Bvf{bh@dquM*;N1mCtb*}$BmZWaDxEyb8I>7l&nZ}75OYn*CUDp%c zQk}X+X3d?9=H>n$wYOdN#9HDRRI+1o$iFCMTrCCyoRm{Rn@;$+l}IK#Ed3Q#9AAZz zVu9Z8?}@HIn&g=JKvy@FD2dl5K3Q9v*BCBtHr$1cNr!_?h3e|H!CXx`mb%CYoqJzi z1P7yVbC&}B*z&-zJ&p^4T7rTQd=<{%eR}dFQKQt{hYNE5!u;c~a2-CX6{<)b!(|6v zfg?8Coo!> CwJ1e(^$cYiQ)3$3QAsgRw)=V>UxLKY1X zZzOLrd}>NnF414|aCI2t?96M+dA$tx*|#I A9e20UuCnfIGDo@TK)LPXTv=RLQC_3cTT20{k{&z#9!!_Q6;K2{WAC?HKwO@-%K z(__QghYgApNOQ5zQBV4@aFG>mo~JbqnelV978~WjuWx&tb07A7V9!$ZumS!QWEK3j zi(xkf zeB8Avjj|%M15RpqI1eTVFfNUYn+6_wKi0sVe#h20#?nbeeYtoFuP&pGZPbr%*?)z^Ez|O zh!i%j@^XlUYJqUTbsYTS9XGb~OnRYI5F`2?Dgj5cDsS|SO2C!wm7ubYetCHbq(2P3 zy)2cRiEIOq%nbsKgai{9^7Zu2E8S1;Ud{(7gwuE~q~OveH(SDb?VU-FF!!V9HIO|{ z>%;b%3lu!S`&Z_ud@E2Zq;4Gw#9n)o`0-*D 5`=u_U`QIl%z zheXX`@)Cu#>scyH^?zU*!7@`#dmC{mU-?_YXnDn8C45LG?gBT>$iy^0KHe(ZSTpDU znu+F5EWq(}R~42B5)4*J^w)gmYhpVGw#$7>Am71d=I>K&`}PfWKJ`HV_KDN&x?;C0 zN?|KdiX?4i+~4(p{}rSPMj2P5qPBAguzr7M)Yns`oY?*BAAmN@K(Z1zni@*X_yAXX z(H{5m`YaoJVV?t0wcU!nbUViFMSa`Y3{dPVD&=*)saR}{zD3!O&e4jN40Gjh=>`N* zpDpz+yvxX-4A}q1O}J^_^>zyOd_TX=dWy9XY-}PT=c669lc0i` WZd!H9S}WR9|Z@4$=FGZ1bXw z{3D<~iX=&fEK4|PB_zzT#J+cQ4TNGI-p(B WgdYl}{!p8SV{Vju%0O`{+(ttg0w4OS+qK 9cdv1hC;tc4P!8`d!j`9scUo^6IL-g#``A zU|9=tdr|ExZ}0jyeES^Fd(baUKmyk3$&X9W<%V*@*|H^3-F~l1+he@H#O&eRX_Sb! z1khO@HAj^)ne{|>eD2-?K+aaKSPv+4K?UE6_dM*DCRQiyI_7SooIVg02t!DLK z)OWiYbFlhY74$GjPWRt&;QkWjFg^g$2lRAACupF~(SyXJ_EqJnedgO#DvM|?y++B( zkACe*6FJ1J{q+G#Ao_?(Ow5O6QAfvOy!uUv{aP!KD1d!u_PY6hYYcXBnrp^Y_(vd4 zoSXoTI;#)@=3hu0j1jmalaTb*IMbgdxes`Q&Uzg3^4r_gh8Fwln*F;NIQ0ArIo0$}cN zp-4H@uV%w3%lq<0!N20Jtr0sY3cGAaP*7Y;95!e`dvt?y(uIZFRdJc}me`lHdR0tn z;kR~MGgK&)^+$_o9}v)pv`S2K6feEzKGFD~*f6L?IJ>+|VQt}hQtx?<^mK1@o$BEL zR12Op;A(eiZjY8)03kylc^$IN#i*HR7OH8(K`Em7Is6vhP>r<#0;qf1Wm;i;p`@nX z>q)=RQoibE)^-n _`Gso79sRrIYo`Kky%`lW? zYwYKwAW%R7=y;9*^Lk$}zLG&0QZS4iR;H#wNW(gI>!e#ZS)kMPb9!RFl`Q3x@5n;@UN#UaJ+K!vhsbdK3!rk*6~f>*3xS-&NSn zd0Il*_sluXSoD>}*O@7&I~^5D%QBd*&xpxn^x5FD`Q(3??j8Gv^K7k?G^8rd1urI2 zDc#-g=!nnoePW=XohG~f+V|e}UNj|Q-sjWnQPy0nrF4ThH}7gtNIGzoiaoBV808X{~KVTl*~Ly zlDT cBmP z%Uw~DDq8Pg`?U;4mBMZbSx$s;56nxM_0jsLNikpif(qXaz 6$@$Fu7 n2#j;=md9#KrwMF-_apT~ULLA$ zLtQ)<#<-@H@$9I)5p|f*#tzvsgFTE^)Br{9?V449_7>v&fKn2_=i?p6rZS-~9 `XGh6@g=87>kMncQ)s7 zfx3}<_*UMlx4k}l3QFU2w^~F+_kYws5V-32FqT;r7ef6tKU)@5I^JZzQ02D *6fI7Iyf|_nOM2uk@5nn!mI%HdCR+%pshc9XEDSmI zPjZ@2v|hh>isD3%pG8LXsM^)Z+jYERWn;+j`Ew_jmw+gRwpnsxf;(Soz+Kk*cziUA zcDX;xi)io_RTg+&HULpTY=>uMc^TRj?*ETKLO R z>6w_6_|;q2CadGA0{>Bsz-W wtLX8W2!OoSzeY@dbJ+A^smyTHhxO>5QJ zS{L1PKbUU^^e`D273IXV^UoK0Dufh2a#}1=TjzZ>A!CUYokp4tnU0D?=ooD=S>R9| z8+H;niSr%{jg`Xv=0O^{*=ae_a%HQwZ7LEw@Fu%s)Xn=&GKmM}7ig4o`&=&{5ixOb zE-usM?dPP+ZSqS6M+?*p(c%(XkJiTcKa$+7+Mjw({!#gZ?pwv;5p&!#e*w!&Au2h# zw{hqCx>lt6x--R6%37ohQpTHl-j_%6WS6%DcFf82>V-;(zv-{cT3uT?^$(k2VEm1i z)T@=-9?mrUfcG3!O(A#~Cyj)8D^()i?JvGlGf=Q^4L#D}PH{p@3VTmOd_DeLsc9hX z1ChOPmwHQo%Q-KnSc@p}x~V{dUDu5k{)FO_1* 0t@Dq_y1 zDObnN@k_Q$hb6QZRonk`Akk}mSYQb>mZh41a3!7Kx9mx_xf|3~T^Cg%U0rt~Mg`bR z$IC6?3g)yoUHqG>DNR=|3`#{p;^AzK-FFs01gVic&-0#&=G~Uj(p34qA~9kVs1Xx4 z!C6L9!0PXMYsy7O_dp(Fq(;c_{LChG0&Bg*lZ5!6;N31UD0Hg|M3 k|$NM`uDVsL8y{UhdZ$(8#@FG+b*lg)yQd9vGk9Z69n~1FcTQsat0!|Sl626wj zP$|w|BD&NW?=F+++J -uQK?80 DE!dw*Eh}RmV>L7M=P(4;`(QlBc^xOw8cl z?&;@uQFxS^I ygO6;cI{|!6auS4P=@M@i34SxQaMP+sjOq7(!S4v6- zqUnd*ME `R897 z5!EPkBL2hz{?;$~^PkYmLSXjk@l6y|R3)ibku0-Jt>VJsVvJuN-eO{el8~ sOuq^~~mGGwK1!1NaOu9irCBW9^bsjohY}`h650RGl3$fOUa$mNr$g z{x{#D4Q`@{CXH&>Xcb;eBcS~%K08iP724mQ%+c6i+ni1V#wimG5eB$Y(_xbSO$)HX zDTW?dZ^KiO`8R=!L56%0T@!r{osHPVstx+Q7e^Q3mNB~(1}Zpj==}o;e; U=|%J0kJWZa6=mZQOVji3M9F2W* ~ZF`*bBVE;Z3iO6E# zl07Dh>{h(c68$eUfvKCT!;vr^-UX!=2m@XA_kx1QJnhjF%qA+Y>1{1{r}5Q36bpbY zwS~8_esv9Mt9%tmBaaVvdve0&U*FMTQx|RNN?} HtQ6~6qFx*p=0uA+JMqm)1 zkN(^C8kiOY1kaRVD~qHKnJ2Pa8c=1?fdvh=KOC`!R;iyJ&uA3?2pr}om3AvE6v@n* zQq^E6GcWV *0vfS{%mf%O0D0m$ zP11`tKmhwXEt^GDk|odAI@|p8?DUHM13>dHd=MQJw{MF@sXu>?&&4%Btpd{ a>k|5l68~l(brwFsVpbP7W-+jFr4gYx=EH@z9mlt%leGDOp z _1k@Wg zOv>END(2}q@5iV$KIA2}-gleN9V_p}6cDs-Q`VmF`sUvAj&26yZE ;DK*rG5oVTa3fG $4BE;2|1%vurC8HztW$T7DdLB$hAfp6-J0#X+3`eG4{F2A6{7p z(sFQI&9Pe>u{t|R2TfC0KmaT@4KFU8%G-dTtS#DwaY6ajD`qejUc0mmPtN(i2dT&T zsiAPXc8)s9@!s0r)^qw~zIP-(A)BKXHBz#&{=s8xa1bnCkwSsmwMP>}>in)UC*^jN zTA$y#;fSipb7Nz$J_DY@GZRtAgxi`a9LMw=Fye&AYPj$&ycg2DTLX6FOruSLO14YG zT=k|`5U!&T9b`+(LBP39>tpC9h1ylv)`24m4LSV*nHq>+VEE|IjNop(es!nY4BeyK z<6HnXz|#xw=&jZ#!+jqjf#cXJf%ZJZZ}GHx;O=y={ zQpybWY&r!P{Q65JMdzSD=c9v#kX_qQ?M)K?9hg^ Iwqdg*s-wjSf`sqK6hWYZ5xHC-~A-49z ax`~^iWX PDKi^%+OaivyA;^Yv)e4Pv7Rm4*%!q<@cm@oa7DM@l zAeXK=KWmDlaf;IJY0+!l+`NgycpyEYDuawBVfFb!&_{$5Fa4H`{=;0zrQ&iTz1}xG zR;|F(hSyJg-m1vYFSS(Bg_FNo9AcLhGLF#`LhyfVH%G9fe(w~r7%8_)U?k#Rf5Z_a z!tu#+S~s!d9$(xET+c(4`npf-pRaeP9dFNP-EnBYW+#vN%MY=9 Ya@2vQO$~QghVp|^-E7ptulx3-ELY$@##|! zF|VImOo3f6kv5MK-Q8(|$`XdG=RTpzmwkLg>yVpb-`w4Wq@oXZJG1c<5{8FBiqrLu zvie}gr2eI6rhc8_SdGUQI0xy&&THj*e<8o=xCQ)XDc4>mS5zDWckTT?jqUFcU#12% z_FZKo(!#oj)u<@0Bx`!i>U>e&mKZcLzW)L6#lVK%Qv=UZhbl<=qLe&a9mv5t3+KE0 zy;1LDfgPxl4BpM&krIgZ5>0>mmK=&I`J8t?-~9r9IXpHy5|VZGXs}!qdYq>Sx+T>g z8rC~vGYCfUBcY (v2G`TG$tpMl za&kf#S>1nVUrkj}8R0%@m@|6P!9YVJSsjgxP9j+Fh(YiuHR`JlZK#pM+L5LkKYb*M zRJHk1&-^E^*RDZ&!!41M!j`lgvB7tFcxYocp+vjZ+cWckOoD*lk;nbHxS$Suj9k!w zVpfcwD?yxMs+|LlfkSvWp{jP}$7{#PY<|n*s7UwHDLJ4Ko(kfNuqrE!kB>?;fgA>1 z1oU1|ye+qyovhj4re=_`^}A=%|1l4vft(44xJrok@KdZRQ~IaMsvC;>>a3WUFYdo3 z_u{ts{wzc8+^3AJEhF~@rJ +_}|tsH2fYWbmp0ksqDEn zjK#!)Q)^$LQzt<+PR@alcb_cFMNdMkJwl*sv$p#O4CB#tYKiZR#>=rO?(t6l6l&tl zDOri!hK@`P;DIA8F!*E=dBp8UII08wL_&`n0 u9-528qD)9v;vzOfSwHS80~UI42nq4rFC-RbV=$@z1SlN_H1K(|faR8c zbC-{u9T}ni%JqX%NHN9Fa&f^HhdinGavlyXuli@3Y8-$;^0tADM>{(^Vbk{-_y|{l zeoW%FK*G8Hy6USJLNnnPvKZvwQcZ}n^YIBu;B(Niw4{yss8JgF^{c2|q=g7pO$Z*V z<3M(&M3{RT^t{ONchj EJW~2jXKEbQ>$_GjdHhME4PQAB5sKN{7S^ YElyCo|WW(ULS#T7k zQ8!0=b&LK#mA>2=Wj_V0yWkORs^K?+^1-%fTJXg}4&mm63zqaGe`e7=DIIXuS(zH^ z)~ElIPV>nI9ZCw)xn+AR;Q$tf!Uw|>T^uH!mcI!! axo|$r#wHe|(q0AAg-yb1r)!jSY~qfvKwB zg+-={BdjJ!Au04N71$o#2tl*soIMxG3r(JR>r78LC@64fZ=R6R+krZ+{i-a>x7USJ zK&nHtcPIZ3&`rr9vq|0`Yp7r4tf~eJOmDL13;C}5_GoEw6_gb8YykU19EhNMoz Z3Z?Z)Dfb>(sZM$Vm zuL(^LA`i{L923jAzajLZcC@q?NM8K&ndrDUM|e4<2?+^ ykKfr~>_1kF5IjA$kfF0OW7k}9-| zg6&lKqcX+=-r2%?2GTx8^8Ai3mW_p#jen~Rv1#7*fyR1aW(J&zDsa0DJQg#6Y?f^h zO&f#O4W2Py9bV(@IR_EZrVk91F*cT#mUeax%7~+Yc#q|1;3^qnker$S5?d)Rj*s>& zte{8V`wI%ZbE-4I%gZa#3Tkx@nGea9$!QKE^ErlJM6z60hw{;gU4O jF>YrcvR@jjMqJ2AOQa0el!MQm>f91UL)InZT z`_XjDkyA{p#7`kO+pnzFR9(Gi&nSzo+@ZXbv|X}#-LdiC3p};FRO!Kpd4r;GYI>R; zB)OTHpDXx~*-Rj?Fa6J#38TC2f1K3yMib~6nYCpx8k&AVL>LM>(7_l(tMbngPs)kV z1_j_89_U~$yuytdQ9+)8{*Pb8(yBk60wKAcUKqq+)cwZ!UVC}ft&b~2M2t#jhi!K5 zw5o3t`le7X3rQ6{Fi-+xCN0{3gbC@>(L0Fm+TJFgPIYwwe4~;n1~2SSfhqWfDZ@%# z59xEPZ+Agbl0DdO9~!QOS3?-lC}eE{$01LQPEDf?owEWTrzhXNyPZz7J3j{mx4g$+ zyIZSlijh+2BXy hR3JgBU4$BL4%(m^(SI z!St`3-ciQ>Uja){oM>!#Bf^Fb{wgcIUjXB7hls@~_^TNn4i%(X4CcPM7OmsIWol|& zXy1T64uk#0v#o6XpE$nJrG0%vl_n(q4sBEpWG#yP{gnJ?BGmBMx_{81ib{2Lt%iF* z6o9H!S2dd?){5Q-eWdRl%<%@jDWa0Wm15nx{{y6`2EE`qZ*DD5bBMa=vnu*)$|2_~ zi>LTA^6)(!@-AK#7QF@M6&&?>y(Zs_kcW$R(_iWxgp|D28t&;) BYJPzYIm|E*|HOt=Mx5lzeR=cglYNItG z o)02>q1iRQMVqAs& z8XhLw%vhN>osyt51SVaBUx!ss0wQT{v_bR$38S2p6uq&p_f=r@)*zNmOu{^wbLA9V z#Ns-i-A@~#Y;<{|Dl$#4-&MxH-_rwfrj;F(0pOXR9U2}+Bf{HUEtJzxTN;jrN l$4y2Cs8UA1#gi&vAHms&Sxu&ln{q9F55Vjs{-n%LUL}T4pgI;))Ev&Lk zkDEa;>Uy+9!Psxpc^FjtBCPjgw4CkEOUM}L2g0zjEC~-JCwR4OHw7IR0%!=3==mX9 zrx}e4q$4^e+SRU)V3d&Ijd)2qZ8fXqHXAaCY(H?Ndlsyun)y@g^Y4H7Xe5IZ<@%N1 z_+>LE>fgba`pN%n4hICU+H6z>(eDly+1oI4FP`9oU*>Spsdu;B_s75Y^_hS_{FUTH zb1nRkV)z;bsa26m#ZYf+ywH&ugv$ci0+#l<1L;9|u-(_T4lWu0iC;Qm>byHDX%0gq zMF $b= z6y)TXK@4As*=T|JxGx}Tct&@aAz8ug)o-^q$cc?~wbBh9CW9l zR*Y~Jr2+YhBLl}N&7`9q+nKn|{pJ n;|+ zL+e(EHkUtx05SzUQjhCOEjO(3SQkJ9zWz6T>cC|b0uJNZMka{ejGc!Am)r;z=i}33 zUY-VKYq|f$-dl%Nxqthj7>J<25(8;zX=#y^o**DCtu%rl-JytxfOMC1gOv0{LAqPI zJEdU~cc5$Sy?^`ebIv~ZJkR;-u7Bow6m`ybzVG;s@u^{W#$(tPb1o6L00^=e@&Jh2 zIKbTo82-HvAFxsY>jyVH9H_-D^3&&@thM2B$+W4ek$ NT&% z-E_*Nc~AgbGvN_TdJPyzZ%>c>+R6SzIe6_W;#$H;4BMT^OrOJw!YKB-el>~Rs`KQ+ z#b>td(PGIG7|oJ+WXll3Fqo_1T=$++F|F$4WM43z&rxb1;KMV&FzbWYtE)pBl!rdC z3rS)O!XjrymNWQqyrpNy+sXn0GUJZ4*V$o7;pus5+wJ)Ci4G(^d@V{78PQFQ%|w?v zB7X%rnMyq2w44s5l)#~;r8NWp$+Mpt4_Au+=c3i)-l&`HNq_l6pkNum(8Zq3o$>rj z3xn6zz~26cTl_EaOkgE-Z;eAP%+ d(NZswu3hs=E3WaQcsz|JJA${J%A-OJ)DA zQ59OJehEQY|LP!@c{+hWPrNrxkoy@u ^su>enhQyF zruqIs)puN7;-7t)q@3*yXH-*66g~v7@v&*ad2piM+b>@%Wo64 0faFuAh=#M>`zlj7Y(sw)?^%pZmBc!a~Pt^yVNuTtGX7I+7L#riuj_re< z5;n6E^K2QFn#XN=k9NyvY#iOUD17_B;V+-?yPL0D@6B9^`v=i{oAfuL`4 6K0oCgeNC7ci+ejDgLi1i9wL1$Oid}J{U(m_-(-Y^?P0sK@(@OBw=_2g zS X`K5}vgBN|J-x|D&V^X-(TVqGdKv>+joD;D!@ zow5n1*Q_6h%V}NsMVcf)T>-TFX)DW!Joc|1$XMm)AA4nHRmU!>GMG2^Ek{u?zq0IM zKe)zM)ug0eDk!im1JxtC@e7-<@jDhhxH2EvGrw5q;8(wWj$~#kP`kV=4!C=re6&E1 z)_kl2@&g~MU0@e(eJ0q{Jyq1$aOwlkl#$WGGU)9^gCDT7Z#R;;e^ z#$?)8>%i`*De3@1wpZY!`cqcbgXMc-1h6R zu<|LRa;Z6qr>FNa&BQwzHzHQuO$ZV4Cp{6bd%p|glDzJ;7=!|}gTKVQJ=zwsT=(72 zP~izW_Sb=q4QP=NvB4+iTLmWr8LKYw+bK8 %aWR$Nd zWCDQGECm(Xx;abj0$z>RTOIA~x%L~{g|EAl4xwd?fkn~dM@L^_n{4WJ^z&hGOv*&( zxG1^(x1C&+x9K! `vJfarQ} zSnWVA$_~tZFu~vu-6^&;O*!}dN!g8w%B1eow1B