From 5613f08213bf781f62f073c317e8b374e2ec2980 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Mon, 15 Jun 2026 15:37:33 +0800 Subject: [PATCH] docs: add project planning documentation --- docs/01-product-vision.md | 34 ++ docs/02-prd.md | 311 ++++++++++++++++++ docs/03-technical-stack.md | 120 +++++++ docs/04-development-rules.md | 202 ++++++++++++ docs/05-project-architecture.md | 421 ++++++++++++++++++++++++ docs/06-ui-mockup-prompt-claude.md | 169 ++++++++++ docs/06-ui-mockup-prompt.md | 94 ++++++ docs/07-ui-design.md | 511 +++++++++++++++++++++++++++++ docs/08-image-editor-design.md | 473 ++++++++++++++++++++++++++ docs/09-packaging-release.md | 262 +++++++++++++++ docs/ui-v1.html | 345 +++++++++++++++++++ docs/ui-v1.png | Bin 0 -> 302904 bytes 12 files changed, 2942 insertions(+) create mode 100644 docs/01-product-vision.md create mode 100644 docs/02-prd.md create mode 100644 docs/03-technical-stack.md create mode 100644 docs/04-development-rules.md create mode 100644 docs/05-project-architecture.md create mode 100644 docs/06-ui-mockup-prompt-claude.md create mode 100644 docs/06-ui-mockup-prompt.md create mode 100644 docs/07-ui-design.md create mode 100644 docs/08-image-editor-design.md create mode 100644 docs/09-packaging-release.md create mode 100644 docs/ui-v1.html create mode 100644 docs/ui-v1.png diff --git a/docs/01-product-vision.md b/docs/01-product-vision.md new file mode 100644 index 0000000..23970e9 --- /dev/null +++ b/docs/01-product-vision.md @@ -0,0 +1,34 @@ +# 自动合成印花服饰效果图工具项目愿景 + +## 项目是什么 + +自动合成印花服饰效果图工具是一款本地桌面软件,用于把印花图片叠加到衣服底图的指定位置,生成服饰印花效果图。 + +它面向日常打样、上架、沟通和批量制图场景,核心价值是让用户用更少步骤完成衣服底图、印花图和蒙版图之间的合成工作。 + +## 给谁用 + +- 服饰定制、印花和打样相关工作人员。 +- 需要批量生成 T 恤、卫衣等服饰效果图的设计、运营或生产支持人员。 +- 在局域网多台电脑上共同使用同一类制图工具的小团队。 + +## 为什么存在 + +手工使用图片编辑软件逐张制作印花效果图效率低、重复操作多,也容易因为坐标、尺寸和角度不一致导致效果不稳定。 + +本项目存在的目的是把这类重复性制图流程工具化: + +- 降低非专业修图人员生成效果图的门槛。 +- 提高单张和批量合成效率。 +- 让印花位置、尺寸、旋转和模板参数更容易复用。 +- 减少多台电脑、多名使用者之间的操作差异。 +- 保持本地运行,适应不依赖公网服务的工作环境。 + +## 不做什么 + +- 不做完整的专业图片编辑软件,不替代 Photoshop 等工具。 +- 不做在线 SaaS 平台,第一阶段不依赖云端账号体系。 +- 不做复杂的组织权限、审批流或素材资产管理系统。 +- 不做智能设计生成工具,不负责自动创作印花图案。 +- 不把图像识别、自动抠图、自动识别衣服版型作为第一阶段目标。 +- 不优先支持跨平台移动端,第一阶段聚焦 Windows 桌面使用。 diff --git a/docs/02-prd.md b/docs/02-prd.md new file mode 100644 index 0000000..800b340 --- /dev/null +++ b/docs/02-prd.md @@ -0,0 +1,311 @@ +# 自动合成印花服饰效果图工具 PRD + +## 1. 背景 + +本项目是一款 Windows 本地桌面软件,用于把印花图片叠加到衣服底图上,生成服饰印花效果图。参考旧版工具已经具备衣服图、印花图加载,模板合成,自定义位置、尺寸、旋转,单张合成和批量合成等能力。 + +新版需要复刻旧版核心工作流,并增强可视化编辑能力,让用户可以直接在预览区拖动、缩放、旋转印花图片。 + +## 2. 产品目标 + +- 用户可以快速加载衣服底图和印花图。 +- 用户可以通过模板或手动编辑决定印花在衣服上的位置、尺寸和角度。 +- 用户可以在预览区直观看到合成效果。 +- 用户可以导出单张效果图。 +- 用户可以批量生成多张效果图。 +- 用户可以保存并复用常用合成模板。 +- 软件可以在无 Python 环境的 Windows 电脑上运行。 + +## 3. 目标用户 + +- 服饰定制、印花和打样工作人员。 +- 需要批量生成服饰效果图的设计、运营或生产支持人员。 +- 在局域网多台电脑上共同使用工具的小团队。 + +## 4. 使用场景 + +### 4.1 单张效果图制作 + +用户选择一张衣服底图和一张印花图,在预览区拖动、缩放和旋转印花,确认效果后导出一张合成图。 + +### 4.2 批量效果图制作 + +用户导入一个衣服图片文件夹和一个印花图片文件夹,选择模板或当前编辑参数,批量生成合成图。 + +### 4.3 模板复用 + +用户把常用的印花位置、尺寸和旋转角度保存为模板,下次直接选择模板完成同类衣服的合成。 + +### 4.4 多人本地使用 + +多台 Windows 电脑安装同一工具。不同电脑可以使用相同或不同版本,但主程序本身应保持本地可运行,不依赖公网服务。 + +## 5. 核心概念 + +### 5.1 衣服底图 + +衣服底图是合成画布的基础图片。印花图会叠加到衣服底图之上。 + +要求: + +- 支持常见图片格式:PNG、JPG、JPEG、WEBP。 +- 支持批量从文件夹加载。 +- 保留原图尺寸作为默认导出尺寸。 + +### 5.2 印花图 + +印花图是需要叠加到衣服底图上的图案。 + +要求: + +- 支持透明 PNG。 +- 支持缩放、拖动和旋转。 +- 支持批量从文件夹加载。 +- 合成时保持透明区域正确。 + +### 5.3 模板 + +模板是一组可复用的合成参数。 + +模板字段: + +```json +{ + "name": "左胸小号模板", + "x": 120, + "y": 180, + "width": 160, + "height": 160, + "rotation": 0 +} +``` + +## 6. 功能需求 + +### 6.1 图片加载 + +用户可以分别选择衣服图片文件夹和印花图片文件夹。 + +要求: + +- 选择文件夹后,导入该文件夹及其所有子文件夹中的支持格式图片。 +- 加载后在对应列表中显示图片文件。 +- 支持全选、取消全选。 +- 支持单独选中或取消选中某张衣服图片。 +- 支持单独选中或取消选中某张印花图片。 +- 支持选择当前预览使用的衣服图和印花图。 +- 加载失败时显示可理解的错误信息。 +- 忽略不支持的文件格式,并在日志中记录。 + +### 6.2 预览区 + +预览区用于显示当前衣服底图和印花叠加效果。 + +要求: + +- 衣服底图按比例适配预览区域。 +- 印花图显示在衣服底图之上。 +- 预览区操作不应改变原始素材文件。 +- 用户切换衣服图或印花图时,预览区及时刷新。 +- 预览效果应尽量接近最终导出效果。 + +### 6.3 拖动印花 + +用户可以用鼠标拖动印花图改变位置。 + +要求: + +- 拖动过程中实时更新预览。 +- 拖动结束后更新 X/Y 坐标输入框。 +- 坐标以衣服底图原始像素坐标为准。 +- 不强制限制印花必须完全位于衣服底图内,但导出时只保留画布范围内内容。 + +### 6.4 缩放印花 + +用户可以调整印花尺寸。 + +要求: + +- 支持通过宽度、高度输入框设置尺寸。 +- 支持在预览区通过交互方式缩放。 +- 默认保持宽高比例。 +- 可以提供解除宽高比例锁定的选项。 +- 缩放结束后更新宽度和高度输入框。 + +### 6.5 旋转印花 + +用户可以旋转印花。 + +要求: + +- 支持角度输入。 +- 支持向左旋转和向右旋转。 +- 支持在预览区通过交互方式旋转。 +- 角度单位为度。 +- 旋转后导出结果与预览一致。 + +### 6.6 模板选择 + +系统提供常用预设模板。 + +第一批内置模板: + +- 正方形模板。 +- 纵向长方形模板。 +- 横向长方形模板。 +- 左胸小号模板。 + +要求: + +- 用户点击模板后,印花位置、尺寸、旋转参数立即应用到当前预览。 +- 模板参数应基于衣服底图尺寸计算,避免只适配单一图片分辨率。 + +### 6.7 自定义模板 + +用户可以保存当前参数为自定义模板。 + +要求: + +- 支持新增模板。 +- 支持重命名模板。 +- 支持删除自定义模板。 +- 支持把模板保存到本地配置文件。 +- 自定义模板在下次启动后仍可使用。 + +### 6.8 单张合成导出 + +用户可以导出当前预览图。 + +要求: + +- 输出图片尺寸默认与衣服底图一致。 +- 支持选择输出目录。 +- 支持 JPG、PNG 输出。 +- 支持输出质量设置。 +- 文件名应避免覆盖已有文件,除非用户确认。 + +### 6.9 批量合成导出 + +用户可以批量生成合成效果图。 + +批量规则需要支持两种模式: + +- 一一匹配:第 1 张衣服图配第 1 张印花图,第 2 张衣服图配第 2 张印花图。 +- 全组合:每张衣服图分别与每张印花图合成。 + +要求: + +- 用户可以选择批量模式。 +- 批量导出使用当前模板或当前编辑参数。 +- 显示合成进度。 +- 支持取消批量任务。 +- 单个文件失败不应中断整个批量任务。 +- 批量结束后显示成功数量和失败数量。 +- 失败原因写入日志。 + +### 6.10 日志 + +软件应记录运行日志。 + +要求: + +- 记录程序启动、图片加载、模板加载、导出开始、导出完成、异常信息。 +- 日志保存到本地 `logs` 目录。 +- 日志文件按日期或启动时间区分。 + +## 7. 非功能需求 + +### 7.1 平台与运行环境 + +- 第一阶段支持 Windows 桌面系统。 +- 目标系统为 Windows 10 及以上,优先适配 Windows 10 / Windows 11。 +- 开发语言为 Python。 +- 指定 Python 版本为 Python 3.7。 +- 软件应可在没有 Python 环境的电脑上运行。 + +### 7.2 性能 + +- 单张预览操作应保持流畅。 +- 批量合成时界面不应无响应。 +- 大图处理应避免不必要的重复加载和重复缩放。 + +### 7.3 稳定性 + +- 图片加载失败、路径不存在、输出目录无权限等情况应给出明确提示。 +- 批量任务中单张失败时继续处理剩余图片。 +- 程序异常时应写入日志。 + +### 7.4 可维护性 + +- 图片处理逻辑应与界面逻辑分离。 +- 模板、配置、日志应使用清晰的本地文件结构。 +- 核心合成逻辑应可以被单独测试。 + +## 8. 推荐技术方案 + +- 开发语言:Python 3.7。 +- GUI:PySide6。 +- 预览交互:QGraphicsView / QGraphicsScene。 +- 图片合成:Pillow。 +- 配置存储:JSON。 +- 打包:PyInstaller,优先使用 onedir 模式。 + +说明:技术方案用于指导第一版实现。若后续出现明确约束,可以调整。 + +## 9. 数据与文件结构 + +建议本地目录结构: + +```text +AutoPrint/ + AutoPrint.exe + config/ + app_config.json + templates.json + logs/ + output/ + resources/ +``` + +模板配置建议: + +```json +{ + "templates": [ + { + "name": "左胸小号模板", + "type": "builtin", + "x_ratio": 0.35, + "y_ratio": 0.28, + "width_ratio": 0.12, + "height_ratio": 0.12, + "rotation": 0 + } + ] +} +``` + +## 10. 验收标准 + +- 可以加载衣服图片文件夹和印花图片文件夹。 +- 可以在列表中选择参与合成的图片。 +- 可以在预览区显示衣服底图和印花图。 +- 可以拖动印花并同步坐标。 +- 可以缩放印花并同步尺寸。 +- 可以旋转印花并同步角度。 +- 可以选择内置模板并应用到预览。 +- 可以保存和复用自定义模板。 +- 可以导出当前单张合成图。 +- 可以按一一匹配模式批量导出。 +- 可以按全组合模式批量导出。 +- 批量导出时显示进度,失败项写入日志。 +- 打包后的程序可在无 Python 环境的 Windows 电脑上运行。 + +## 11. 暂不做 + +- 不做云端账号和在线素材库。 +- 不做复杂权限和审批流。 +- 不做完整图片编辑器能力。 +- 不做 AI 自动生成印花图。 +- 不做第一阶段自动抠图和自动识别衣服区域。 +- 不做移动端应用。 diff --git a/docs/03-technical-stack.md b/docs/03-technical-stack.md new file mode 100644 index 0000000..4c1695e --- /dev/null +++ b/docs/03-technical-stack.md @@ -0,0 +1,120 @@ +# 技术栈说明 + +## 目标运行环境 + +- 操作系统:Windows 10 及以上,优先适配 Windows 10 / Windows 11。 +- 开发语言:Python。 +- 指定 Python 版本:Python 3.7。 +- 发布形态:打包为 Windows 桌面程序,目标电脑无需预装 Python 环境。 + +## GUI 框架 + +使用 PySide6 作为桌面 GUI 框架。 + +选择原因: + +- PySide6 是 Qt 官方 Python 绑定。 +- 授权相对适合闭源或内部工具场景。 +- Qt 的 `QGraphicsView` / `QGraphicsScene` 适合实现图片预览、拖动、缩放和旋转。 +- 可以通过 PyInstaller 打包为 Windows 可执行程序。 + +## PySide6 版本约束 + +由于项目指定使用 Python 3.7,PySide6 需要锁定在仍支持 Python 3.7 的版本。 + +推荐版本: + +```text +PySide6==6.5.3 +shiboken6==6.5.3 +``` + +原因: + +- `PySide6 6.5.3` 支持 `Python >=3.7, <3.12`。 +- `PySide6 6.6.0` 起要求 `Python >=3.8`,不适用于当前 Python 3.7 约束。 +- 固定 PySide6 和 shiboken6 的版本可以避免依赖解析到不兼容版本。 + +安装时建议使用: + +```bash +pip install PySide6==6.5.3 +``` + +`shiboken6` 会作为依赖自动安装对应版本;在正式依赖文件中仍建议显式锁定。 + +## 图片处理 + +使用 Pillow 处理最终图片合成。 + +用途: + +- 读取衣服底图和印花图。 +- 保持透明 PNG 的 alpha 通道。 +- 对印花执行缩放、旋转。 +- 将处理后的印花叠加到衣服底图。 +- 导出 PNG 或 JPG。 + +可选使用 numpy 辅助处理图像数组,但第一阶段不应把 numpy 作为必须依赖,除非实际实现需要。 + +## 预览交互 + +预览区建议使用: + +```text +QGraphicsView +QGraphicsScene +QGraphicsPixmapItem +自定义可变换印花 Item +``` + +设计原则: + +- 衣服底图作为基础图层。 +- 印花图作为可交互图层。 +- 印花支持拖动、缩放和旋转。 +- 预览交互状态需要转换为衣服底图原始像素坐标。 +- 导出时以原始像素坐标为准,确保预览和导出结果一致。 + +## 打包工具 + +使用 PyInstaller 打包。 + +推荐使用 onedir 模式: + +```bash +pyinstaller --onedir --windowed main.py +``` + +选择 onedir 的原因: + +- PySide6 / Qt 依赖文件较多,onedir 更容易排查缺失插件或 DLL 问题。 +- 启动速度通常优于 onefile。 +- 后续局域网分发和增量替换更容易。 +- 配置文件、模板文件和日志目录更容易管理。 + +## 依赖锁定 + +精确依赖版本应放在项目根目录的 `requirements.txt` 中维护,而不是放在 PRD 中。 + +建议初始依赖: + +```text +PySide6==6.5.3 +shiboken6==6.5.3 +Pillow +PyInstaller +``` + +注意: + +- Python 3.7 已停止官方维护,部分第三方库的新版本不再支持 Python 3.7。 +- 实际开发前需要验证 Pillow、PyInstaller 等依赖的最新可用兼容版本。 +- 一旦验证通过,应把这些依赖固定为明确版本,避免后续安装环境变化导致打包失败。 + +## 文档分工 + +- `docs/01-product-vision.md`:定义项目是什么、给谁用、为什么存在、不做什么。 +- `docs/02-prd.md`:定义用户需求、功能范围、交互要求和验收标准。 +- `docs/03-technical-stack.md`:定义技术选型、版本约束和工程环境。 +- `requirements.txt`:定义实际安装的精确依赖版本。 diff --git a/docs/04-development-rules.md b/docs/04-development-rules.md new file mode 100644 index 0000000..ab42d85 --- /dev/null +++ b/docs/04-development-rules.md @@ -0,0 +1,202 @@ +# 开发规则 + +## 1. 文档定位 + +本文档用于约束 AI 和开发者在本项目中的代码修改行为。后续任何开发、重构、检查、修复任务,都必须遵守本文档。 + +本文档优先级高于个人习惯和临时偏好。若任务说明与本文档冲突,必须先向用户确认,不能自行改变规则。 + +## 2. 开始任务前必须阅读 + +执行编码任务前,必须先阅读: + +- `docs/01-product-vision.md` +- `docs/02-prd.md` +- `docs/03-technical-stack.md` +- `docs/04-development-rules.md` + +若任务涉及图片预览、拖动、缩放、旋转、坐标换算或导出一致性,还必须阅读后续的图像编辑器设计文档。 + +若任务涉及打包、发布、局域网更新、多版本分发,还必须阅读后续的打包发布文档。 + +## 3. 技术栈约束 + +必须遵守: + +- 使用 Python 3.7。 +- 使用 PySide6 作为 GUI 框架。 +- PySide6 使用兼容 Python 3.7 的版本,推荐 `PySide6==6.5.3`。 +- 使用 Pillow 处理最终图片合成。 +- 使用 JSON 保存配置和模板。 +- 使用 PyInstaller 打包 Windows 桌面程序。 +- 第一阶段目标系统为 Windows 10 / Windows 11。 + +禁止事项: + +- 禁止切换到 Electron、Web 前端、C#、C++、Tkinter、wxPython 或其他 GUI 技术栈。 +- 禁止将项目改造成前后端分离应用。 +- 禁止依赖公网服务完成核心功能。 +- 禁止在未说明必要性的情况下新增第三方依赖。 +- 禁止使用只支持 Python 3.8 及以上的依赖版本。 + +## 4. 代码结构规则 + +必须遵守: + +- UI 逻辑、业务逻辑、图片处理逻辑、配置逻辑应分离。 +- 图片合成逻辑必须可以脱离 GUI 单独调用。 +- 批量任务逻辑必须与单张合成逻辑复用同一套核心合成能力。 +- 模板读写必须集中在专门模块中,不能散落在 UI 事件里。 +- 日志写入必须集中管理,不能使用零散 `print` 作为正式日志。 + +禁止事项: + +- 禁止把所有功能写进一个 `main.py` 或一个窗口类。 +- 禁止在按钮点击事件中直接堆叠大量图片处理代码。 +- 禁止复制粘贴两套近似的单张合成和批量合成逻辑。 +- 禁止在多个模块中重复定义同一类配置结构。 +- 禁止把临时调试代码提交为正式代码。 + +## 5. 图片处理规则 + +必须遵守: + +- 衣服底图是最终导出画布的基础。 +- 印花图叠加到衣服底图之上。 +- 导出尺寸默认与衣服底图原始尺寸一致。 +- 坐标、宽高、旋转角度应以衣服底图原始像素坐标系为准保存。 +- 预览区缩放不应改变真实合成参数。 +- 预览结果和最终导出结果必须尽量一致。 +- 透明 PNG 的 alpha 通道必须正确保留。 +- 文件路径必须兼容 Windows 中文路径。 + +禁止事项: + +- 禁止修改原始衣服图片和原始印花图片。 +- 禁止只按预览控件尺寸保存合成参数。 +- 禁止把屏幕坐标直接当作最终导出坐标。 +- 禁止在未确认的情况下裁剪、拉伸或压缩原始素材。 +- 禁止忽略透明通道导致 PNG 印花出现黑底、白底或异常背景。 + +## 6. UI 与交互规则 + +必须遵守: + +- UI 应优先服务批量制图工作流,避免装饰性复杂设计。 +- 关键操作必须有明确按钮或输入入口。 +- 拖动、缩放、旋转印花后,参数输入框必须同步更新。 +- 用户通过参数输入框修改坐标、尺寸、角度后,预览必须同步更新。 +- 批量处理时界面不能长时间无响应。 +- 失败、缺文件、格式不支持、导出失败等情况必须给出用户可理解的提示。 + +禁止事项: + +- 禁止为了视觉效果牺牲操作效率。 +- 禁止隐藏核心操作入口。 +- 禁止让用户只能通过拖拽完成精确定位,必须保留参数输入能力。 +- 禁止批量处理期间阻塞主界面且没有进度反馈。 +- 禁止使用含糊错误提示,例如只显示“失败”。 + +## 7. 配置与模板规则 + +必须遵守: + +- 配置文件使用 JSON。 +- 自定义模板必须保存到本地配置或模板文件中。 +- 内置模板和用户自定义模板需要能区分。 +- 配置读取失败时应使用安全默认值,并记录日志。 +- 配置写入前应尽量避免破坏已有用户配置。 + +禁止事项: + +- 禁止把用户模板硬编码进业务逻辑。 +- 禁止在程序启动时无提示清空用户配置。 +- 禁止因为单个模板损坏导致整个程序无法启动。 +- 禁止将配置写入程序安装目录中不适合写入的位置,除非该目录明确可写。 + +## 8. 日志与异常规则 + +必须遵守: + +- 程序启动、图片加载、模板加载、导出开始、导出完成、异常信息都应记录日志。 +- 日志保存到本地 `logs` 目录。 +- 用户可理解的错误提示和开发者可排查的日志信息应同时存在。 +- 批量任务中单个文件失败时,应记录失败原因并继续处理剩余文件。 + +禁止事项: + +- 禁止吞掉异常不记录。 +- 禁止只在控制台输出异常。 +- 禁止将完整异常堆栈直接作为普通用户提示。 +- 禁止因单张图片失败中断整个批量任务,除非是全局不可恢复错误。 + +## 9. 文件修改规则 + +必须遵守: + +- 每次任务只修改与当前任务直接相关的文件。 +- 修改已有文件前必须先理解现有结构和调用关系。 +- 新增文件应放在符合架构文档约定的位置。 +- 删除文件、移动文件、重命名文件前必须确认不会破坏引用。 + +禁止事项: + +- 禁止顺手重构与当前任务无关的代码。 +- 禁止大范围格式化无关文件。 +- 禁止删除用户素材、输出图片、配置文件或日志文件。 +- 禁止把测试素材、临时输出、缓存文件混入源代码目录。 + +## 10. 依赖管理规则 + +必须遵守: + +- 依赖版本应写入 `requirements.txt`。 +- 新增依赖前必须说明原因。 +- 新增依赖必须确认支持 Python 3.7 和 Windows。 +- GUI、图片处理、打包相关依赖需要优先选择稳定版本。 + +禁止事项: + +- 禁止直接使用未锁定版本的核心依赖进行正式开发。 +- 禁止引入体积巨大但只使用很小功能的依赖。 +- 禁止引入需要复杂系统安装步骤的依赖,除非用户明确同意。 +- 禁止引入与 PySide6 事件循环冲突的 GUI 依赖。 + +## 11. 版本号规则 + +必须遵守: + +- 软件名称和版本号必须从统一模块读取,例如 `src/version.py`。 +- 标题栏显示版本号、打包产物版本号、发布目录版本号必须保持一致。 +- 修改版本号时必须同步检查 UI 显示和打包发布配置。 + +禁止事项: + +- 禁止在 UI、打包脚本、业务代码中重复硬编码软件名称和版本号。 +- 禁止让多个模块各自维护不同版本号。 + +## 12. 验证规则 + +完成开发任务后,必须根据任务范围做验证: + +- 文档修改:检查文件路径、标题、引用和明显格式问题。 +- 依赖修改:验证依赖是否可在 Python 3.7 环境安装。 +- GUI 修改:至少启动程序检查窗口是否能打开。 +- 图片处理修改:至少验证一张衣服图和一张印花图的合成结果。 +- 批量处理修改:至少验证成功、失败和进度反馈路径。 +- 打包修改:验证 PyInstaller 打包产物能启动。 + +如果因为环境限制无法验证,必须在最终说明中明确说明未验证项和原因。 + +## 13. AI 执行规则 + +AI 执行任务时必须遵守: + +- 先读相关文档,再改代码。 +- 先说明将修改哪些文件,再执行修改。 +- 遇到不明确需求时,优先根据现有文档保守实现;影响范围较大时必须询问用户。 +- 不得自行扩大需求范围。 +- 不得自行替换技术栈。 +- 不得把临时方案伪装成最终方案。 +- 不得为了完成任务隐藏失败、跳过验证或省略风险说明。 +- 最终回复必须说明改了什么、验证了什么、还有什么未完成或未验证。 diff --git a/docs/05-project-architecture.md b/docs/05-project-architecture.md new file mode 100644 index 0000000..901a591 --- /dev/null +++ b/docs/05-project-architecture.md @@ -0,0 +1,421 @@ +# 项目架构 + +## 1. 文档定位 + +本文档定义项目代码结构、模块职责和依赖方向。后续 AI 或开发者在新增功能、修复问题、重构代码时,必须遵守本文档。 + +本文档不描述具体 UI 细节和图像编辑算法细节。拖动、缩放、旋转、坐标换算等细节应放在图像编辑器设计文档中。 + +## 2. 架构目标 + +- 避免所有逻辑集中在一个窗口类或一个入口文件中。 +- 让 UI、图片合成、模板、配置、批量任务、日志职责清晰分离。 +- 让核心图片合成逻辑可以脱离 GUI 单独测试。 +- 让单张合成和批量合成复用同一套核心逻辑。 +- 让后续 AI 执行小任务时能快速定位应修改的模块。 + +## 3. 推荐目录结构 + +```text +src/ + main.py + version.py + app/ + __init__.py + main_window.py + widgets/ + __init__.py + image_canvas.py + image_list_panel.py + template_panel.py + transform_panel.py + export_panel.py + core/ + __init__.py + models.py + composer.py + batch.py + services/ + __init__.py + config_service.py + template_service.py + file_service.py + log_service.py + resources/ + icons/ + styles/ +tests/ + test_composer.py + test_templates.py +docs/ +``` + +说明: + +- `src/main.py` 是程序入口,只负责创建应用和主窗口。 +- `src/version.py` 统一维护软件名称和版本号。 +- `app/` 放 GUI 相关代码。 +- `core/` 放与 GUI 无关的核心模型、合成和批量处理逻辑。 +- `services/` 放配置、模板、文件扫描、日志等服务。 +- `resources/` 放图标、样式等静态资源。 +- `tests/` 放自动化测试。 + +## 4. 模块职责 + +### 4.1 `src/main.py` + +职责: + +- 初始化 Qt 应用。 +- 初始化日志。 +- 创建并显示主窗口。 +- 进入应用事件循环。 + +禁止: + +- 禁止在入口文件中写图片合成逻辑。 +- 禁止在入口文件中写复杂 UI 布局。 +- 禁止在入口文件中直接读写模板和配置细节。 + +### 4.2 `src/version.py` + +职责: + +- 统一定义软件名称。 +- 统一定义当前版本号。 +- 为标题栏、关于信息、打包发布配置提供版本来源。 + +建议字段: + +```text +APP_NAME +APP_VERSION +``` + +禁止: + +- 禁止在其他模块重复硬编码软件名称和版本号。 +- 禁止让 UI 显示版本号与打包发布版本号不一致。 + +### 4.3 `app/main_window.py` + +职责: + +- 组织主界面布局。 +- 协调各 UI 面板。 +- 连接用户操作与服务调用。 +- 管理当前选中的衣服图、印花图和模板状态。 + +禁止: + +- 禁止直接实现底层图片合成算法。 +- 禁止直接写入配置文件细节。 +- 禁止堆叠大量按钮事件中的业务逻辑。 + +### 4.4 `app/widgets/image_canvas.py` + +职责: + +- 显示衣服底图和印花图。 +- 承载印花拖动、缩放、旋转交互。 +- 将 UI 交互结果转换为统一的变换状态。 +- 通知外部坐标、尺寸、角度变化。 + +禁止: + +- 禁止直接导出最终图片文件。 +- 禁止直接管理批量任务。 +- 禁止把屏幕坐标直接作为最终合成坐标。 + +### 4.5 `app/widgets/image_list_panel.py` + +职责: + +- 显示衣服图片列表和印花图片列表。 +- 支持选择文件夹、全选、取消全选。 +- 通知主窗口当前选择变化。 + +禁止: + +- 禁止直接执行图片合成。 +- 禁止直接修改图片文件。 + +### 4.6 `app/widgets/template_panel.py` + +职责: + +- 显示内置模板和自定义模板。 +- 处理模板选择、新增、重命名、删除等 UI 操作。 +- 通过 `template_service` 读取和保存模板。 + +禁止: + +- 禁止在 UI 控件中硬编码所有模板逻辑。 +- 禁止直接绕过 `template_service` 修改模板文件。 + +### 4.7 `app/widgets/transform_panel.py` + +职责: + +- 显示和编辑印花的 X/Y 坐标、宽度、高度、旋转角度。 +- 用户修改参数后通知预览区更新。 +- 预览区交互变化后同步显示最新参数。 + +禁止: + +- 禁止自己维护一套与预览区不一致的状态。 +- 禁止只更新输入框但不更新预览。 + +### 4.8 `app/widgets/export_panel.py` + +职责: + +- 选择输出目录。 +- 设置输出格式和质量。 +- 触发单张合成和批量合成。 +- 显示合成进度和结果摘要。 + +禁止: + +- 禁止直接写底层图像合成算法。 +- 禁止批量处理时阻塞主界面且没有进度反馈。 + +### 4.9 `core/models.py` + +职责: + +- 定义核心数据模型。 +- 统一表示图片文件、印花变换参数、模板、导出选项、批量任务结果。 + +建议模型: + +```text +ImageAsset +TransformState +Template +ExportOptions +BatchOptions +ComposeResult +BatchResult +``` + +禁止: + +- 禁止在模型文件中引入 PySide6 UI 控件。 +- 禁止在模型文件中执行文件扫描或图片导出。 + +### 4.10 `core/composer.py` + +职责: + +- 实现单张图片合成。 +- 根据衣服底图、印花图、变换参数和导出选项生成结果图。 +- 保证透明 PNG 正确叠加。 +- 保证导出结果与预览参数一致。 + +禁止: + +- 禁止依赖 PySide6 UI 控件。 +- 禁止读取 UI 输入框。 +- 禁止处理文件夹批量遍历逻辑。 + +### 4.11 `core/batch.py` + +职责: + +- 实现批量合成任务编排。 +- 支持一一匹配和全组合模式。 +- 复用 `core/composer.py` 的单张合成能力。 +- 汇总成功、失败和错误信息。 + +禁止: + +- 禁止复制一套独立于 `composer.py` 的合成算法。 +- 禁止因单个文件失败中断整个批量任务。 + +### 4.12 `services/config_service.py` + +职责: + +- 读取和保存应用配置。 +- 管理默认配置。 +- 在配置损坏或缺失时提供安全默认值。 + +禁止: + +- 禁止在多个模块中重复解析同一个配置文件。 +- 禁止启动时无提示清空用户配置。 + +### 4.13 `services/template_service.py` + +职责: + +- 加载内置模板。 +- 加载、保存、重命名、删除自定义模板。 +- 校验模板字段是否合法。 + +禁止: + +- 禁止让 UI 直接操作模板 JSON 文件。 +- 禁止单个模板损坏导致全部模板不可用。 + +### 4.14 `services/file_service.py` + +职责: + +- 扫描图片文件夹。 +- 过滤支持的图片格式。 +- 生成安全输出文件名。 +- 处理 Windows 中文路径。 + +禁止: + +- 禁止在 UI 层重复实现文件扫描规则。 +- 禁止默认覆盖已有输出文件,除非用户确认。 + +### 4.15 `services/log_service.py` + +职责: + +- 初始化日志系统。 +- 统一日志格式。 +- 管理日志文件路径。 + +禁止: + +- 禁止业务模块各自创建不一致的日志配置。 +- 禁止用 `print` 代替正式日志。 + +## 5. 依赖方向 + +允许的依赖方向: + +```text +main.py -> app +app -> core +app -> services +core -> services 仅限必要的纯工具能力 +services -> core models 可选 +tests -> core +tests -> services +``` + +推荐依赖关系: + +```text +UI 层负责收集用户输入 +UI 层把输入转换为 core models +core 层执行合成或批量任务 +services 层处理配置、模板、文件和日志 +UI 层展示结果和错误信息 +``` + +禁止的依赖方向: + +```text +core -> app +services -> app +models -> app +composer -> PySide6 UI 控件 +batch -> PySide6 UI 控件 +``` + +## 6. 状态管理 + +主窗口维护当前工作状态: + +- 当前衣服图片。 +- 当前印花图片。 +- 当前印花变换参数。 +- 当前模板。 +- 当前输出配置。 + +印花变换参数必须使用统一模型表示,例如: + +```text +TransformState + x + y + width + height + rotation + keep_aspect_ratio +``` + +规则: + +- 预览区变化后更新 `TransformState`。 +- 参数面板变化后更新同一个 `TransformState`。 +- 单张导出和批量导出都读取同一个 `TransformState` 或模板转换结果。 +- 不允许 UI 面板各自维护互不一致的状态。 + +## 7. 单张合成流程 + +```text +用户选择衣服图和印花图 +-> 主窗口更新当前状态 +-> 预览区显示衣服和印花 +-> 用户拖动、缩放、旋转或输入参数 +-> 更新 TransformState +-> 用户点击单张导出 +-> export_panel 构造 ExportOptions +-> core.composer 执行合成 +-> file_service 生成输出路径 +-> 保存结果 +-> UI 显示成功或失败 +``` + +## 8. 批量合成流程 + +```text +用户选择多张衣服图和印花图 +-> 用户选择批量模式 +-> 用户选择模板或当前参数 +-> export_panel 构造 BatchOptions +-> core.batch 生成任务列表 +-> 每个任务调用 core.composer +-> 失败项记录错误并继续 +-> UI 显示进度 +-> 任务完成后显示成功数量和失败数量 +``` + +## 9. 错误处理 + +错误处理分两层: + +- 用户提示:简洁、可理解,说明用户可以做什么。 +- 日志信息:详细记录异常类型、文件路径、调用阶段和堆栈信息。 + +规则: + +- 文件不存在、格式不支持、输出目录不可写必须提示用户。 +- 单张导出失败应提示用户并记录日志。 +- 批量任务中单个文件失败应记录失败原因并继续处理。 +- 全局不可恢复错误才允许终止批量任务。 + +## 10. 测试边界 + +优先测试: + +- `core/composer.py` 的单张合成。 +- 透明 PNG 印花叠加。 +- 坐标、尺寸、旋转参数应用。 +- 输出文件名生成。 +- 模板 JSON 读写。 +- 批量一一匹配模式。 +- 批量全组合模式。 + +UI 测试可以后置,但核心合成逻辑必须尽量可测试。 + +## 11. 后续扩展点 + +架构应允许后续增加: + +- 图像编辑器专项模块。 +- 局域网版本分发模块。 +- 多版本启动器。 +- 更多输出格式。 +- 更多模板类型。 +- 素材目录记忆。 + +新增扩展时必须遵守现有依赖方向,不得让核心模块依赖 UI。 diff --git a/docs/06-ui-mockup-prompt-claude.md b/docs/06-ui-mockup-prompt-claude.md new file mode 100644 index 0000000..0cea6ac --- /dev/null +++ b/docs/06-ui-mockup-prompt-claude.md @@ -0,0 +1,169 @@ +# UI 效果图生成提示词 + +## 文档定位 + +本文档用于向 Claude 或其他具备设计能力的 AI 生成 UI 效果图。提示词目标是产出 Windows 桌面软件的高保真 UI mockup,用于后续整理 UI 设计文档和指导 PySide6 实现。 + +> 本文档已根据多轮设计讨论更新。原始提示词保留在「## 提示词」一节,所有经讨论确定的设计决策已并入提示词正文,并在文末「## 设计迭代记录」中分主题说明原因,便于追溯。 + +## 提示词 + +```text +你是一名资深桌面软件 UI/UX 设计师,请为一个 Windows 桌面软件设计高保真 UI 效果图。 + +软件名称: +自动合成印花服饰效果图工具 + +软件用途: +用户加载衣服底图和印花图片,把印花叠加到衣服图片上,并通过拖动、缩放、旋转调整印花位置,最后导出单张或批量合成效果图。 +注意:「合成印花」只是整个业务流程中的一个环节,软件还包含「AI 穿搭」「导出上架」等其他环节,界面需要用顶部标签页在多个环节间切换。 + +目标用户: +服饰定制、印花打样、商品上架、批量制图人员。用户不一定是专业设计师,所以界面要清晰、直观、效率优先。 + +设计目标: +1. 保留旧版软件的核心工作流,但界面更现代、更清晰。 +2. 重点突出中间的大预览区,让用户能直接拖动、缩放、旋转印花。 +3. 适合 Windows 桌面端,分辨率优先按 1920x1080 设计。 +4. UI 风格专业、简洁、偏工具软件,必须是浅色 Windows 原生工具风(类似 Office / Qt 默认控件),不要做成网页落地页,也不要做成深色潮酷风。 +5. 不要使用花哨渐变、大面积插画、营销风格卡片。 +6. 布局应适合长时间批量操作。 +7. 整体外观应能较直接地对应到 PySide6 / Qt 控件,方便后续实现。 + +视觉风格细则(浅色 Windows 原生工具风): +- 浅灰窗口底(约 #f0f0f0)+ 白色面板,中性灰画布。 +- 字体:Segoe UI / 微软雅黑,正文 12px 左右。 +- 控件用 1px 实线边框、小圆角(约 3px)、几乎不用阴影。 +- 单一蓝色强调色(约 #0067c0)用于选中、主按钮、激活状态。 +- 带标题的分组框(GroupBox),标题嵌在边框上。 +- 数字输入用带上下微调箭头的 SpinBox 样式。 +- 信息密度偏高、留白克制,适合长时间制图。 + +顶部环节标签页(Tab): +- 顶部放一排「步骤式」标签页,标签带序号,体现推荐顺序,但每个标签都可随时点击切换(基本按顺序、允许来回跳)。 +- 标签共三个:① 添加印花、② AI 穿搭、③ 导出上架。 +- 当前演示的是「① 添加印花」环节,该标签高亮(蓝色序号 + 白底),其余标签为未激活灰色态。 +- 后两个环节(AI 穿搭、导出上架)的具体界面内容待定,本次可只搭好标签框架,内容留作占位。 + +核心功能区(属于「① 添加印花」环节): +- 衣服图片列表 +- 印花图片列表 +- 合成预览区 +- 模板选择 +- 坐标参数:X、Y +- 尺寸参数:宽度、高度,支持锁定比例 +- 旋转参数:角度、左旋、右旋 +- 输出设置:输出目录、格式、质量 +- 操作按钮:单张导出、批量导出、保存模板/另存为、重置参数 +- 合成队列与批量进度显示 + +素材导入与勾选规则: +- 选择衣服文件夹 / 印花文件夹后,递归导入文件夹内(含所有子文件夹)的全部图片。 +- 缩略图采用平铺展示,不做分组折叠树;文件名带上所在子文件夹(如「夏季/白T_圆领.png」)以保留来源信息。 +- 缩略图右上角用小角标显示所在子文件夹名(如「夏季」),缩略图下方显示文件名;默认按「子文件夹 + 文件名」排序,使同一子文件夹的图自然相邻。 +- 每个素材列表顶部提供一个全局「全选 / 取消全选」复选框(支持全选、未全选时的半选态),并显示「已选 N」计数。 +- 支持单张勾选 / 取消勾选。只需一个全局全选即可,不要求按子文件夹分组勾选。 + +批量组合方式(合成队列): +- 用户常用两种以上的组合方式:多件衣服 × 同一个印花、一件衣服 × 多个印花,也包含全组合(勾选项两两相乘)。界面需要能灵活切换这几种组合模式。 +- 底部设「合成队列」面板:顶部一排模式切换按钮(多衣服 × 单印花 / 单衣服 × 多印花 / 全组合矩阵),下方用表格逐行显式列出每一对「衣服 × 印花」组合,而不是只显示一个总数。 +- 队列每行显示:序号、衣服、印花、来源文件夹、参数标记(模板 / 已微调)、状态(待导出 / 导出中 / 已完成 / 失败)、单项进度或输出文件名。 +- 队列顶部汇总:共 N 项 · 完成 · 进行 · 失败 · 待导出。 +- 队列操作按钮:重置全部、导出选中、开始批量导出、折叠/展开。 + +参数与模板的关系(默认走模板、可单项微调): +- 所有组合默认跟随当前选中的模板参数;修改模板即对全体生效。 +- 选中队列中的某一项后,右侧参数栏只调整这一对组合,调整后该项标记为「已微调」并锁定,重跑模板时不被其参数覆盖。 +- 右侧参数栏底部提供与模板相关的操作,明确区分「应用为模板 / 重置为模板」,避免和模板区的保存功能产生混淆。 + +建议布局: +1. 标题栏: + - 左侧软件名称 + - 右侧「设置」入口(齿轮图标) + - 标准的最小化 / 最大化 / 关闭按钮 + - 不再使用传统的「文件 / 编辑 / 模板 / 帮助」菜单栏,也不再单独占一行顶部工具栏。 +2. 顶部标签页栏:① 添加印花、② AI 穿搭、③ 导出上架(步骤式 Tab,详见上文)。 +3. 左侧素材栏: + - 上半部分:衣服图片区。顶部为「打开衣服文件夹」按钮(紧贴衣服列表上方),其下为衣服缩略图列表(支持子文件夹角标、缩略图、勾选、全选)。 + - 下半部分:印花图片区。顶部为「打开印花文件夹」按钮(紧贴印花列表上方),其下为印花缩略图列表(同样支持角标、缩略图、勾选、全选)。 + - 「打开文件夹」按钮采用就近原则,放在各自列表正上方,点击后图片即出现在下方。 +4. 中间主区域: + - 大尺寸合成预览画布,中性灰棋盘格背景衬托衣服底图。 + - 画布里显示衣服底图。 + - 印花图层有选中边框、四角缩放控制点、顶部旋转控制点。 + - 支持拖动、缩放、旋转的视觉暗示(底部可放操作提示条)。 + - 由于衣服图多为竖向长方形,画布不宜过宽:适当加宽左侧素材栏(约为基础宽度的 1.3 倍、缩略图改 4 列),参数栏可保持原宽或仅小幅加宽,使中间画布更贴合竖图比例。 +5. 右侧参数栏: + - 模板选择(下拉),其下紧跟「保存 / 另存为」模板按钮。 + - 位置:X、Y + - 尺寸:宽、高、锁定比例 + - 旋转:角度输入、左旋、右旋 + - 输出设置:目录、格式、质量 + - 提示:当前调整仅作用于队列中选中的那一对组合,并标记为「已微调」。 + - 底部操作按钮:导出当前单张、应用为模板、重置为模板。 +6. 底部合成队列面板:见上文「批量组合方式」。 +7. 最底部状态栏: + - 当前选中衣服图 + - 当前选中印花图 + - 当前模板 + - 批量进度 + - 失败项提示 + - 日志/错误提示简要信息 + +请输出: +1. 一张高保真 UI 效果图设计说明。 +2. 页面布局结构。 +3. 每个区域包含哪些控件。 +4. 推荐的颜色、字体、间距、按钮样式。 +5. 交互状态说明,包括: + - 未加载图片 + - 已加载衣服但未加载印花 + - 选中印花图层 + - 正在批量导出 + - 导出失败 +6. 如果可以生成图片,请生成一张 1920x1080 的桌面软件 UI mockup。 +7. 不要生成网页落地页,不要生成移动端界面,不要使用营销宣传风格;必须是浅色 Windows 原生工具风。 + +如果你支持直接生成图片,请直接生成一张 1920x1080 的高保真桌面软件 UI 效果图,界面文字使用中文。 +``` + +## 设计迭代记录 + +以下为在初版提示词基础上,经多轮讨论确定的设计决策与原因,已并入上方提示词。 + +### 1. 视觉风格:改为浅色 Windows 原生工具风 +- 初版未限定深浅,容易被做成偏网页 / 深色 IDE 的风格。 +- 最终确定为浅色 Windows 原生工具风(类似 Office / Qt 默认):浅灰窗体 + 白色面板、中性灰画布、单一蓝色强调色、带标题的分组框、SpinBox 数字框、1px 实线边框与小圆角。 +- 原因:目标用户多为批量制图人员而非专业设计师,长时间看图、批量操作更适合浅色稳妥风格;且该风格能较直接对应 PySide6 / Qt 控件,便于实现。 + +### 2. 顶部导航:去掉菜单栏 + 工具栏,改为「步骤式标签页」 +- 去掉传统「文件 / 编辑 / 模板 / 帮助」菜单栏,因其与工具栏按钮功能重复、对非专业用户不友好。 +- 「设置」移至标题栏右上角齿轮图标,不再单独占一行工具栏,界面更干净。 +- 因为「合成印花」只是业务流程中的一个环节(共 ① 添加印花 / ② AI 穿搭 / ③ 导出上架),环节数量少(2–3 个)且「基本按顺序、可来回跳」,最终选用顶部「步骤式 Tab」:标签带序号体现顺序,但每个可随时点击切换。对应 Qt 的 QTabWidget。 +- (备选方案曾考虑左侧图标导航栏,适合环节较多的情况;步骤条 Wizard 适合强顺序流程。本场景环节少,顶部 Tab 最合适。) + +### 3. 素材导入与勾选 +- 选择文件夹后递归导入(含子文件夹)全部图片。 +- 缩略图平铺展示,文件名带子文件夹路径;缩略图右上角加子文件夹角标,默认按「子文件夹 + 文件名」排序,使同一子文件夹的图相邻——用排序低成本地获得分组效果,无需做折叠树。 +- 顶部一个全局「全选 / 取消全选」复选框(含半选态)+「已选 N」计数;支持单张勾选。仅需全局全选,不做按子文件夹分组勾选。 + +### 4. 批量组合:新增「合成队列」面板 +- 初版只有一个「批量进度 38/60」的进度条,无法表达 60 是怎么来的、用了哪种组合方式、每对组合各自的参数与状态。 +- 新增底部「合成队列」面板:顶部模式切换(多衣服 × 单印花 / 单衣服 × 多印花 / 全组合矩阵),下方表格逐行列出每一对组合及其来源文件夹、参数标记、状态、单项进度。 +- 面板位置选用底部横向面板(状态栏上方、可折叠),不挤压中间画布,最适合长时间批量操作。对应 Qt 的 QTableWidget。 + +### 5. 参数与模板:默认走模板,可单项微调 +- 所有组合默认跟随当前模板;改模板对全体生效。 +- 选中队列某一项后,右侧参数栏只调这一对,调整后标记「已微调」并锁定,重跑模板不覆盖。 +- 模板相关操作明确区分:模板区下拉框下方放「保存 / 另存为」;右侧参数栏底部放「应用为模板 / 重置为模板」,避免两处存模板造成混淆。 + +### 6. 操作按钮就近放置 +- 「打开衣服文件夹」放在衣服列表正上方、「打开印花文件夹」放在印花列表正上方、「保存 / 另存为」模板放在模板下拉框正下方。 +- 原因:就近原则,操作按钮贴着其作用对象,点击后结果即出现在附近,符合用户心智。 + +### 7. 画布与栏宽比例 +- 衣服图多为竖向长方形,过宽的画布两侧留白浪费。 +- 适当加宽左侧素材栏(约 1.3 倍、缩略图 4 列),让中间画布变窄、更贴合竖图;参数栏因多为小输入框,建议保持原宽或仅小幅加宽,避免拉宽后输入框两侧空旷。 + +### 待办(后续环节) +- 「② AI 穿搭」与「③ 导出上架」两个标签页的具体界面内容尚未确定,目前仅搭好标签框架。待明确各自的功能后再补充对应的提示词与布局。 diff --git a/docs/06-ui-mockup-prompt.md b/docs/06-ui-mockup-prompt.md new file mode 100644 index 0000000..7d9e3c1 --- /dev/null +++ b/docs/06-ui-mockup-prompt.md @@ -0,0 +1,94 @@ +# UI 效果图生成提示词 + +## 文档定位 + +本文档用于向 Claude 或其他具备设计能力的 AI 生成 UI 效果图。提示词目标是产出 Windows 桌面软件的高保真 UI mockup,用于后续整理 UI 设计文档和指导 PySide6 实现。 + +## 提示词 + +```text +你是一名资深桌面软件 UI/UX 设计师,请为一个 Windows 桌面软件设计高保真 UI 效果图。 + +软件名称: +自动合成印花服饰效果图工具 + +软件用途: +用户加载衣服底图和印花图片,把印花叠加到衣服图片上,并通过拖动、缩放、旋转调整印花位置,最后导出单张或批量合成效果图。 + +目标用户: +服饰定制、印花打样、商品上架、批量制图人员。用户不一定是专业设计师,所以界面要清晰、直观、效率优先。 + +设计目标: +1. 保留旧版软件的核心工作流,但界面更现代、更清晰。 +2. 重点突出中间的大预览区,让用户能直接拖动、缩放、旋转印花。 +3. 适合 Windows 桌面端,分辨率优先按 1920x1080 设计。 +4. UI 风格专业、简洁、偏工具软件,不要做成网页落地页。 +5. 不要使用花哨渐变、大面积插画、营销风格卡片。 +6. 布局应适合长时间批量操作。 + +核心功能区: +- 衣服图片列表:选择衣服文件夹后,自动导入文件夹及所有子文件夹里的图片;列表支持缩略图、全选、取消全选、单张勾选/取消勾选 +- 印花图片列表:选择印花文件夹后,自动导入文件夹及所有子文件夹里的图片;列表支持缩略图、全选、取消全选、单张勾选/取消勾选 +- 合成预览区 +- 模板选择 +- 批量合成模式: + - 多个衣服图片使用同一个印花图片合成 + - 一个衣服图片轮流使用多个印花图片合成 + - 多个衣服图片和多个印花图片一一匹配合成 + - 多个衣服图片和多个印花图片全组合合成 +- 坐标参数:X、Y +- 尺寸参数:宽度、高度,支持锁定比例 +- 旋转参数:角度、左旋、右旋 +- 输出设置:输出目录、格式、质量 +- 操作按钮:单张导出、批量导出、保存模板、重置参数 +- 批量进度显示 + +建议布局: +1. 顶部工具栏: + - 软件名称 + - 当前版本号,放在软件名称右侧,例如 v1.0.0,使用低调的小字号 + - 打开衣服文件夹 + - 打开印花文件夹 + - 保存模板 + - 设置 +2. 左侧素材栏: + - 上半部分:衣服图片列表,包含选择文件夹、全选、取消全选、缩略图列表、单张勾选框 + - 下半部分:印花图片列表,包含选择文件夹、全选、取消全选、缩略图列表、单张勾选框 + - 文件夹导入需要表达“包含子文件夹图片”的能力 +3. 中间主区域: + - 大尺寸合成预览画布 + - 画布里显示衣服底图 + - 印花图层有选中边框、四角缩放控制点、顶部旋转控制点 + - 支持拖动、缩放、旋转的视觉暗示 +4. 右侧参数栏: + - 模板选择 + - 批量模式选择:同一印花应用到多件衣服、同一衣服应用多个印花、一一匹配、全组合 + - 位置:X、Y + - 尺寸:宽、高、锁定比例 + - 旋转:角度输入、左旋、右旋 + - 输出设置:目录、格式、质量 + - 操作按钮 +5. 底部状态栏: + - 当前选中衣服图 + - 当前选中印花图 + - 当前模板 + - 当前批量模式 + - 批量进度 + - 日志/错误提示简要信息 + +请输出: +1. 一张高保真 UI 效果图设计说明。 +2. 页面布局结构。 +3. 每个区域包含哪些控件。 +4. 推荐的颜色、字体、间距、按钮样式。 +5. 交互状态说明,包括: + - 未加载图片 + - 已加载衣服但未加载印花 + - 选中印花图层 + - 正在批量导出 + - 导出失败 +6. 如果可以生成图片,请生成一张 1920x1080 的桌面软件 UI mockup。 +7. 不要生成网页落地页,不要生成移动端界面,不要使用营销宣传风格。 + +如果你支持直接生成图片,请直接生成一张 1920x1080 的高保真桌面软件 UI 效果图,界面文字使用中文。 +``` diff --git a/docs/07-ui-design.md b/docs/07-ui-design.md new file mode 100644 index 0000000..6d1095d --- /dev/null +++ b/docs/07-ui-design.md @@ -0,0 +1,511 @@ +# UI 设计文档 + +## 1. 文档定位 + +本文档基于 `docs/ui-v1.html` 和 `docs/ui-v1.png` 整理,用于指导 PySide6 桌面界面实现。 + +本文档定义界面布局、控件区域、视觉风格和交互状态,不定义底层图片合成算法。拖动、缩放、旋转和坐标换算的具体实现,应在图像编辑器设计文档中定义。 + +## 2. 设计目标 + +- 保持 Windows 桌面工具软件风格,清晰、稳定、效率优先。 +- 让中间合成预览区成为主要工作区域。 +- 让素材选择、参数调整、导出队列同时可见。 +- 支持长时间批量操作,减少反复切换窗口。 +- 支持衣服图片和印花图片从文件夹及子文件夹批量导入。 +- 支持多种批量合成模式。 + +## 3. 整体布局 + +主窗口采用固定工具型布局: + +```text +标题栏 +流程页签 +主工作区:左侧素材栏 | 中间预览区 | 右侧参数栏 +底部合成队列 +底部状态栏 +``` + +参考设计尺寸: + +```text +1920 x 1080 +``` + +推荐布局比例: + +```text +左侧素材栏:约 374 px +右侧参数栏:约 318 px +中间预览区:占用剩余宽度 +底部合成队列:约 188 px +状态栏:约 26 px +``` + +## 4. 顶部区域 + +### 4.1 标题栏 + +内容: + +- 软件图标。 +- 软件名称:`自动合成印花服饰效果图工具`。 +- 当前版本号,放在软件名称右侧,例如:`v1.0.0`。 +- 设置入口。 +- Windows 窗口按钮。 + +要求: + +- 标题栏高度紧凑。 +- 版本号使用低于软件名称的视觉层级,例如浅灰小字。 +- 版本号必须常驻显示,便于用户和管理员确认当前运行版本。 +- 设置入口放在右侧。 +- 不放复杂业务按钮。 + +### 4.2 流程页签 + +当前设计包含三个页签: + +- `1 添加印花` +- `2 AI 穿搭` +- `3 导出上架` + +第一阶段只实现 `添加印花` 主流程。 + +要求: + +- 未实现页签可以置灰或隐藏。 +- 若保留未实现页签,点击时应提示“暂未开放”,不能进入空白页面。 + +## 5. 左侧素材栏 + +左侧素材栏分为上下两块: + +- 衣服图片。 +- 印花图片。 + +### 5.1 衣服图片面板 + +控件: + +- `打开衣服文件夹` 按钮。 +- 图片数量说明,例如:`含子文件夹 · 共 12`。 +- 全选复选框。 +- 已选数量。 +- 排序选择,例如:`文件夹+名称`。 +- 缩略图网格。 + +缩略图项显示: + +- 图片预览。 +- 勾选框。 +- 来源子文件夹标签。 +- 文件名。 +- 当前预览选中态。 + +交互规则: + +- 选择文件夹后递归导入该文件夹及所有子文件夹中的支持格式图片。 +- 支持全选和取消全选。 +- 支持单张勾选和取消勾选。 +- 支持点击某张衣服图片作为当前预览底图。 +- 勾选状态表示是否参与批量合成。 +- 当前预览选中态和批量勾选态需要视觉上可区分。 + +### 5.2 印花图片面板 + +控件和交互与衣服图片面板一致。 + +差异: + +- 按钮文案为 `打开印花文件夹`。 +- 图片项代表印花素材。 +- 点击某张印花图片后更新当前预览印花。 + +## 6. 中间预览区 + +中间预览区是主要编辑区域。 + +### 6.1 预览顶部栏 + +显示: + +- 当前底图文件名。 +- 当前印花文件名。 +- 预览操作模式按钮: + - 移动。 + - 缩放。 + - 旋转。 +- 缩放比例控制: + - 减小。 + - 当前比例,例如 `82%`。 + - 放大。 + +要求: + +- 预览缩放只影响画布显示比例,不改变真实合成参数。 +- 当前底图和当前印花需要清晰显示。 + +### 6.2 画布区域 + +画布显示: + +- 灰色棋盘背景。 +- 衣服底图。 +- 印花图层。 +- 印花选中框。 +- 缩放控制点。 +- 旋转控制点。 +- 操作提示条。 + +印花选中态: + +- 选中框使用蓝色线条。 +- 四角和边中点显示缩放控制点。 +- 顶部显示旋转控制点。 +- 印花可以显示轻微旋转状态。 + +操作提示: + +```text +拖动移动 +四角 缩放 +顶部 旋转 +Shift 锁比例 +``` + +要求: + +- 画布区域应尽量大。 +- 鼠标交互反馈必须明确。 +- 未加载图片时显示空状态提示。 +- 加载衣服但未加载印花时,只显示衣服底图。 +- 加载衣服和印花后,默认显示可编辑印花图层。 + +## 7. 右侧参数栏 + +右侧参数栏用于模板、位置、尺寸、旋转和输出设置。 + +### 7.1 模板区域 + +控件: + +- 模板下拉框。 +- `保存` 按钮。 +- `另存为` 按钮。 + +示例模板: + +```text +标准居中印花 · T恤 +``` + +要求: + +- 选择模板后更新当前印花位置、尺寸和旋转。 +- 保存用于覆盖当前自定义模板。 +- 另存为用于创建新模板。 + +### 7.2 位置区域 + +控件: + +- `X 坐标` 数字输入框。 +- `Y 坐标` 数字输入框。 + +要求: + +- 单位为衣服底图原始像素。 +- 用户输入后实时或确认后更新预览。 +- 预览区拖动后同步更新输入框。 + +### 7.3 尺寸区域 + +控件: + +- `宽度` 数字输入框。 +- `高度` 数字输入框。 +- `锁定宽高比例` 复选框。 + +要求: + +- 单位为像素。 +- 默认锁定宽高比例。 +- 预览区缩放后同步更新宽度和高度。 +- 输入框修改后同步更新预览。 + +### 7.4 旋转区域 + +控件: + +- `角度` 数字输入框。 +- `左旋 90°` 按钮。 +- `右旋 90°` 按钮。 + +要求: + +- 角度单位为度。 +- 支持负角度。 +- 顶部旋转控制点拖动后同步角度输入框。 +- 按钮旋转后同步预览和角度输入框。 + +### 7.5 输出设置区域 + +控件: + +- 输出目录输入框。 +- 浏览目录按钮。 +- 输出格式下拉框。 +- 输出质量下拉框。 + +要求: + +- 输出目录显示完整或省略路径。 +- 格式至少支持 PNG 和 JPG。 +- PNG 默认保留透明相关能力;JPG 需要处理背景。 +- 质量设置用于 JPG 或需要压缩的输出格式。 + +### 7.6 提示信息区域 + +用于显示当前操作影响范围。 + +示例: + +```text +当前调整将仅作用于队列中选中的「黑T_短袖 × 花卉_001」,并标记为「已微调」,重跑模板时不被覆盖。 +``` + +要求: + +- 提示语需要明确说明当前参数影响的是当前单张、选中队列项还是批量模板。 +- 警告和错误信息不能只用颜色表达。 + +### 7.7 操作按钮 + +主要按钮: + +- `导出当前单张` +- `应用为模板` +- `重置为模板` + +要求: + +- `导出当前单张` 使用主按钮样式。 +- 其他按钮使用次级按钮样式。 +- 当前缺少衣服图或印花图时,导出按钮应禁用。 + +## 8. 底部合成队列 + +合成队列用于批量任务预览和导出进度。 + +### 8.1 队列头部 + +内容: + +- 标题:`合成队列`。 +- 批量模式切换。 +- 队列统计。 +- 队列操作按钮。 + +批量模式: + +- `多衣服 × 单印花` +- `单衣服 × 多印花` +- `全组合(矩阵)` + +PRD 还要求支持: + +- 多衣服和多印花一一匹配。 + +实现时需要在 UI 中补充 `一一匹配` 模式,或在后续 UI 版本中调整。 + +队列统计示例: + +```text +共 60 项 · 完成 12 · 进行 1 · 失败 1 · 待导出 46 +``` + +操作按钮: + +- `重置全部` +- `导出选中` +- `开始批量导出` +- 折叠/展开队列。 + +### 8.2 队列表格 + +列: + +- 序号。 +- 衣服。 +- 印花。 +- 来源文件夹。 +- 参数。 +- 状态。 +- 进度 / 输出文件。 + +参数状态: + +- `模板` +- `已微调` + +任务状态: + +- `已完成` +- `待导出` +- `导出中` +- `失败` + +要求: + +- 当前选中的队列行需要高亮。 +- 导出中显示进度条。 +- 失败行显示具体原因。 +- 输出文件列显示最终文件名或错误说明。 + +## 9. 底部状态栏 + +显示: + +- 当前衣服。 +- 当前印花。 +- 当前模板。 +- 批量导出状态。 +- 失败数量。 +- 最近日志摘要。 + +示例: + +```text +衣服:白T_圆领.png +印花:花卉_001.png +模板:标准居中印花 · T恤 +批量导出中 12/60 +1 项失败 +已导出 白T_圆领_花卉_001.png +``` + +要求: + +- 状态栏保持单行。 +- 重要失败状态需要明显,但不要遮挡工作区。 + +## 10. 视觉规范 + +整体风格: + +- Windows 桌面工具软件。 +- 浅色背景。 +- 蓝色作为主操作色。 +- 边框、分隔线和面板层级清晰。 +- 不使用营销式大图、渐变背景或装饰性卡片。 + +推荐颜色: + +```text +窗口背景:#f0f0f0 +面板背景:#ffffff +次级面板:#f7f7f7 +边框:#d6d6d6 +主色:#0067c0 / #0078d4 +成功:#107c10 +警告:#b87a00 +错误:#c42b1c +画布背景:#9a9da3 +``` + +字体: + +```text +Segoe UI +Microsoft YaHei +微软雅黑 +``` + +字号: + +- 普通文本:12 px。 +- 重要标签:13 px。 +- 辅助说明:11 px。 + +圆角: + +- 控件圆角保持小半径,约 3 px。 +- 不使用大圆角卡片。 + +## 11. PySide6 控件映射 + +建议映射: + +```text +主窗口:QMainWindow 或 QWidget +整体布局:QVBoxLayout + QSplitter / QGridLayout +左侧素材栏:QWidget + QVBoxLayout +缩略图列表:QListWidget / QTableView + 自定义 delegate +中间预览区:QGraphicsView + QGraphicsScene +右侧参数栏:QScrollArea + QFormLayout / QVBoxLayout +数字输入:QSpinBox / QDoubleSpinBox +下拉框:QComboBox +复选框:QCheckBox +队列表格:QTableView +进度条:QProgressBar +状态栏:QStatusBar 或底部 QWidget +``` + +要求: + +- 预览区必须优先使用 `QGraphicsView / QGraphicsScene`。 +- 批量队列建议使用模型视图结构,避免后续数据多时卡顿。 +- 缩略图列表可先用 `QListWidget` 实现,后续再优化为模型视图。 + +## 12. 状态说明 + +### 12.1 未加载图片 + +- 左侧列表为空。 +- 中间画布显示空状态提示。 +- 参数区禁用。 +- 导出按钮禁用。 + +### 12.2 已加载衣服但未加载印花 + +- 画布显示衣服底图。 +- 印花相关参数禁用。 +- 导出按钮禁用。 + +### 12.3 已加载衣服和印花 + +- 画布显示衣服和印花。 +- 印花默认选中。 +- 位置、尺寸、旋转参数可编辑。 +- 单张导出按钮可用。 + +### 12.4 选中队列项 + +- 队列行高亮。 +- 预览区显示该队列项的衣服、印花和参数。 +- 若该队列项被手动调整,参数状态显示为 `已微调`。 + +### 12.5 正在批量导出 + +- 显示总进度和当前任务进度。 +- `开始批量导出` 按钮应切换为停止或暂停类操作,具体行为后续定义。 +- 允许查看队列状态。 +- 不应阻塞整个界面。 + +### 12.6 导出失败 + +- 队列对应行显示 `失败`。 +- 输出列显示失败原因。 +- 状态栏显示失败数量。 +- 日志记录详细异常信息。 + +## 13. 与当前设计稿的差异和待确认 + +当前 `ui-v1` 设计稿已经覆盖主要工作流,但有以下待确认点: + +- 批量模式缺少 `一一匹配` 明确入口,需要补充。 +- 顶部 `AI 穿搭` 和 `导出上架` 页签不属于第一阶段范围,应确认是隐藏、置灰还是保留为未来入口。 +- 队列中单项微调和模板重跑规则需要在后续需求或交互文档中进一步明确。 +- 批量导出期间是否允许继续调整队列项,需要后续定义。 diff --git a/docs/08-image-editor-design.md b/docs/08-image-editor-design.md new file mode 100644 index 0000000..6d2921e --- /dev/null +++ b/docs/08-image-editor-design.md @@ -0,0 +1,473 @@ +# 图像编辑器设计 + +## 1. 文档定位 + +本文档定义合成预览区的实现规则,包括衣服底图显示、印花图层拖动、缩放、旋转、坐标换算、参数同步,以及预览和最终导出保持一致的规则。 + +后续任何涉及 `image_canvas.py`、印花变换、坐标换算、图片预览或最终合成一致性的任务,都必须阅读本文档。 + +## 2. 设计目标 + +- 用户可以在预览区直接拖动、缩放、旋转印花。 +- 参数面板和预览区双向同步。 +- 预览区缩放不影响真实合成参数。 +- 所有合成参数以衣服底图原始像素坐标系保存。 +- 最终导出结果与预览中的衣服、印花位置、尺寸、角度一致。 +- 图片合成逻辑可以脱离 GUI 单独测试。 + +## 3. 核心原则 + +必须遵守: + +- Qt 预览负责交互和显示。 +- Pillow 负责最终图片合成和导出。 +- 禁止截图预览区作为最终导出图片。 +- 禁止把屏幕坐标直接保存为合成坐标。 +- 禁止把预览缩放比例写入真实合成参数。 +- `TransformState` 是唯一可信的印花变换状态。 + +## 4. 坐标系定义 + +图像编辑器涉及三套坐标系: + +### 4.1 屏幕坐标 + +屏幕坐标是鼠标事件中的窗口坐标或控件坐标。 + +用途: + +- 接收鼠标点击、拖动、滚轮事件。 +- 判断用户操作了哪个控制点。 + +规则: + +- 屏幕坐标只能作为交互输入。 +- 屏幕坐标不能直接用于最终合成。 + +### 4.2 Scene 坐标 + +Scene 坐标是 `QGraphicsScene` 中的坐标。 + +用途: + +- 显示衣服底图。 +- 显示印花图层。 +- 显示选中框、缩放手柄和旋转手柄。 + +推荐规则: + +- `QGraphicsScene` 的坐标应尽量与衣服底图原始像素坐标保持一致。 +- 衣服底图左上角放在 scene 坐标 `(0, 0)`。 +- 衣服底图宽高等于原始图片像素宽高。 +- 画布缩放通过 `QGraphicsView` 的 view transform 实现,不修改 scene 中图片真实尺寸。 + +### 4.3 原图像素坐标 + +原图像素坐标是衣服底图原始图片的像素坐标。 + +用途: + +- 保存印花位置。 +- 保存印花尺寸。 +- 执行最终 Pillow 合成。 +- 保存模板参数。 + +规则: + +- `TransformState.x` 和 `TransformState.y` 使用原图像素坐标。 +- `TransformState.width` 和 `TransformState.height` 使用原图像素尺寸。 +- `TransformState.rotation` 使用角度,单位为度。 + +## 5. 图层结构 + +预览区使用 `QGraphicsView / QGraphicsScene`。 + +图层顺序: + +```text +背景棋盘格 +衣服底图 Item +印花图片 Item +印花选中框 Item +缩放控制点 Item +旋转控制点 Item +操作提示 Overlay +``` + +说明: + +- 背景棋盘格可以由 view 背景或 scene 背景绘制。 +- 衣服底图不可拖动。 +- 印花图片可拖动、缩放、旋转。 +- 选中框和控制点仅用于交互,不参与最终导出。 +- 操作提示仅显示在 UI 中,不参与最终导出。 + +## 6. 状态模型 + +印花变换状态使用统一模型表示: + +```text +TransformState + x: float + y: float + width: float + height: float + rotation: float + keep_aspect_ratio: bool +``` + +字段含义: + +- `x`:印花未旋转包围盒左上角在衣服原图中的 X 坐标。 +- `y`:印花未旋转包围盒左上角在衣服原图中的 Y 坐标。 +- `width`:印花缩放后的宽度。 +- `height`:印花缩放后的高度。 +- `rotation`:围绕印花中心旋转的角度,单位为度。 +- `keep_aspect_ratio`:是否锁定宽高比例。 + +规则: + +- UI 面板、预览区和导出逻辑必须共享同一个 `TransformState`。 +- 预览区交互完成后更新 `TransformState`。 +- 参数面板输入变化后更新 `TransformState`。 +- `TransformState` 变化后统一刷新预览。 + +## 7. 印花 Item 设计 + +建议实现自定义印花图层: + +```text +TransformablePrintItem +``` + +职责: + +- 显示印花图片。 +- 接收拖动、缩放、旋转交互。 +- 绘制或管理选中框。 +- 发出变换状态变化信号。 + +不负责: + +- 不负责最终 Pillow 导出。 +- 不负责批量任务。 +- 不负责读取文件夹。 +- 不负责保存模板文件。 + +## 8. 拖动设计 + +### 8.1 操作行为 + +用户按住印花主体区域并拖动,移动印花位置。 + +要求: + +- 拖动过程中实时移动印花。 +- 拖动过程中可以实时更新状态,也可以拖动结束后统一提交。 +- 拖动结束后必须同步 X/Y 输入框。 +- 拖动不改变宽度、高度和旋转角度。 + +### 8.2 坐标更新 + +拖动位移应在 scene 坐标中计算。 + +推荐公式: + +```text +new_x = old_x + delta_scene_x +new_y = old_y + delta_scene_y +``` + +由于 scene 坐标与原图像素坐标一致,`new_x` 和 `new_y` 可以直接写入 `TransformState`。 + +规则: + +- 不强制限制印花必须完全在衣服底图内。 +- 导出时只保留衣服画布范围内的内容。 +- 如需边界吸附,应作为后续增强功能,不作为第一阶段必需。 + +## 9. 缩放设计 + +### 9.1 控制点 + +选中印花时显示 8 个缩放控制点: + +```text +左上、上中、右上、右中、右下、下中、左下、左中 +``` + +第一阶段至少需要支持四角缩放: + +```text +左上、右上、右下、左下 +``` + +### 9.2 缩放行为 + +要求: + +- 默认锁定宽高比例。 +- 勾选 `锁定宽高比例` 时,拖动四角控制点按原始比例缩放。 +- 取消锁定后,可以独立改变宽度和高度。 +- 缩放后同步宽度和高度输入框。 +- 缩放不改变旋转角度。 + +### 9.3 最小尺寸 + +必须设置最小尺寸,避免印花缩放为 0 或负数。 + +建议: + +```text +min_width = 10 px +min_height = 10 px +``` + +### 9.4 缩放中心 + +第一阶段推荐使用对角固定缩放: + +- 拖动右下角时,左上角保持不动。 +- 拖动左上角时,右下角保持不动。 +- 拖动右上角时,左下角保持不动。 +- 拖动左下角时,右上角保持不动。 + +若实现复杂度过高,可第一阶段使用中心缩放,但必须保证参数和导出一致。 + +### 9.5 旋转状态下缩放 + +旋转后的缩放更复杂。第一阶段推荐规则: + +- 控制点跟随旋转后的选中框显示。 +- 计算缩放时先将鼠标点从 scene 坐标转换到印花本地坐标。 +- 在印花本地坐标中计算新的宽高。 +- 再更新 `TransformState`。 + +如果第一阶段暂不实现旋转状态下的精确控制点缩放,必须在实现说明中明确限制,并保证输入框缩放和导出仍然正确。 + +## 10. 旋转设计 + +### 10.1 控制点 + +旋转控制点位于印花选中框顶部中心外侧,通过一条短线连接选中框。 + +### 10.2 旋转中心 + +旋转中心为印花缩放后矩形的中心点。 + +计算: + +```text +center_x = x + width / 2 +center_y = y + height / 2 +``` + +### 10.3 鼠标旋转 + +用户拖动旋转控制点时,根据鼠标当前位置和中心点计算角度。 + +推荐公式: + +```text +angle = atan2(mouse_y - center_y, mouse_x - center_x) +rotation = angle_to_degrees(angle) + offset +``` + +`offset` 用于让控制点初始方向和视觉方向一致。 + +要求: + +- 拖动时实时更新预览。 +- 旋转结束后同步角度输入框。 +- 角度可以为负数。 +- 内部可以归一化到 `-180..180` 或 `0..360`,但 UI 表示要一致。 + +### 10.4 按钮旋转 + +右侧参数栏包含: + +- `左旋 90°` +- `右旋 90°` + +规则: + +```text +左旋 90°:rotation = rotation - 90 +右旋 90°:rotation = rotation + 90 +``` + +更新后必须刷新预览并同步输入框。 + +## 11. 参数面板同步 + +参数面板包含: + +- X 坐标。 +- Y 坐标。 +- 宽度。 +- 高度。 +- 锁定宽高比例。 +- 角度。 + +同步规则: + +```text +预览区交互 -> 更新 TransformState -> 更新参数面板 +参数面板输入 -> 更新 TransformState -> 刷新预览区 +模板选择 -> 生成 TransformState -> 刷新预览区和参数面板 +队列项选择 -> 加载该项 TransformState -> 刷新预览区和参数面板 +``` + +防循环规则: + +- 参数面板程序化更新输入框时,应避免触发重复变更。 +- 可以使用 `blockSignals(True/False)` 或内部更新标记。 + +## 12. 预览缩放 + +预览区支持画布缩放,例如: + +```text +82% +``` + +规则: + +- 预览缩放只改变 `QGraphicsView` 的显示变换。 +- 预览缩放不改变 `TransformState.x/y/width/height`。 +- 鼠标事件需要通过 Qt 的坐标映射转换回 scene 坐标。 + +推荐: + +```text +view.mapToScene(mouse_pos) +``` + +## 13. 最终导出设计 + +最终导出由 Pillow 执行,不使用 Qt 截图。 + +### 13.1 单张合成输入 + +```text +garment_path +print_path +TransformState +ExportOptions +``` + +### 13.2 合成步骤 + +推荐流程: + +```text +读取衣服底图为 RGBA +读取印花图为 RGBA +将印花缩放到 TransformState.width / height +按 TransformState.rotation 围绕中心旋转 +计算旋转后图片应粘贴到衣服底图上的位置 +使用 alpha_composite 合成 +根据 ExportOptions 保存 PNG 或 JPG +``` + +### 13.3 旋转后粘贴位置 + +Pillow 旋转通常会产生新的包围盒。 + +规则: + +- `TransformState.x/y/width/height` 表示旋转前矩形。 +- 旋转中心为该矩形中心。 +- 旋转后图片粘贴位置应保证旋转中心仍落在同一个中心点。 + +计算: + +```text +center_x = x + width / 2 +center_y = y + height / 2 +paste_x = center_x - rotated_width / 2 +paste_y = center_y - rotated_height / 2 +``` + +### 13.4 输出格式 + +PNG: + +- 保留 RGBA 或按需求输出不透明图。 + +JPG: + +- JPG 不支持透明通道。 +- 保存 JPG 前必须合成到 RGB 背景。 +- 默认背景建议使用白色,除非后续有其他需求。 + +## 14. 预览与导出一致性 + +必须保证: + +- Qt 中印花显示位置来自 `TransformState`。 +- Pillow 导出也使用同一个 `TransformState`。 +- 两边使用相同的旋转中心。 +- 两边使用相同的宽度、高度和角度定义。 +- 预览缩放比例不参与导出。 + +允许存在的差异: + +- Qt 和 Pillow 的抗锯齿细节可能略有不同。 +- 字节级像素不要求完全一致。 + +不允许存在的差异: + +- 印花明显偏移。 +- 印花尺寸明显不同。 +- 旋转中心不同。 +- 透明区域显示异常。 + +## 15. 队列项微调 + +UI 设计稿中存在 `已微调` 状态。 + +定义: + +- 当用户选中队列中的某一项,并手动调整位置、尺寸或旋转后,该队列项标记为 `已微调`。 +- `已微调` 队列项应保存自己的 `TransformState`。 +- 重新应用模板时,是否覆盖 `已微调` 项需要用户确认或遵循明确规则。 + +第一阶段推荐规则: + +- 当前队列项被手动调整后标记为 `已微调`。 +- 批量导出时,已微调项使用自己的 `TransformState`。 +- 未微调项使用当前模板生成的 `TransformState`。 + +## 16. 边界情况 + +必须处理: + +- 印花部分超出衣服画布。 +- 印花完全超出衣服画布。 +- 印花透明 PNG。 +- 衣服图和印花图尺寸很大。 +- 文件路径包含中文。 +- 宽度或高度输入为 0、负数或非数字。 +- 旋转角度超过 360 度。 + +处理规则: + +- 部分超出画布允许导出,只保留画布内内容。 +- 完全超出画布时可以导出空印花效果,但应提示用户确认或标记风险。 +- 非法尺寸输入应阻止提交并提示。 +- 角度可以归一化显示。 + +## 17. 测试建议 + +核心测试: + +- 未旋转印花合成位置正确。 +- 缩放后导出尺寸正确。 +- 旋转 90 度后中心不偏移。 +- 旋转 -8 度后预览和导出大体一致。 +- 印花部分超出画布时不会报错。 +- 透明 PNG 不出现黑底或白底。 +- 预览缩放到 50% 或 200% 后,拖动仍能得到正确原图坐标。 +- 参数面板输入后预览同步。 +- 预览拖动后参数面板同步。 diff --git a/docs/09-packaging-release.md b/docs/09-packaging-release.md new file mode 100644 index 0000000..30d5b0f --- /dev/null +++ b/docs/09-packaging-release.md @@ -0,0 +1,262 @@ +# 打包发布设计 + +## 1. 文档定位 + +本文档定义第一阶段的本地打包和发布规则。当前阶段只考虑将软件打包为可在 Windows 电脑上运行的桌面程序。 + +暂不包含: + +- 局域网分发。 +- 自动更新。 +- 多版本启动器。 +- 强制升级策略。 +- 远程版本策略管理。 + +这些能力后续需要时再单独补充设计文档。 + +## 2. 打包目标 + +- 将 PySide6 桌面程序打包为 Windows 可运行程序。 +- 目标电脑无需预装 Python 环境。 +- 打包产物应包含运行所需依赖、资源文件、默认配置和模板。 +- 打包后的程序应能在 Windows 10 / Windows 11 上启动。 +- 打包结构应方便排查 Qt 插件、资源、配置和日志问题。 + +## 3. 推荐打包方式 + +使用 PyInstaller。 + +第一阶段推荐使用 `onedir` 模式: + +```bash +pyinstaller --onedir --windowed src/main.py +``` + +原因: + +- PySide6 / Qt 依赖文件较多,`onedir` 更容易排查问题。 +- 启动速度通常优于 `onefile`。 +- 资源文件、配置文件、模板文件更容易随程序目录管理。 +- 后续如需局域网分发或增量替换,目录模式更合适。 + +不推荐第一阶段使用 `onefile`: + +- 启动时需要解压临时文件。 +- Qt 插件问题更难排查。 +- 日志、模板和配置路径更容易混乱。 + +## 4. Python 与依赖要求 + +必须遵守: + +- Python 版本:Python 3.7。 +- GUI 框架:PySide6。 +- 推荐 PySide6 版本:`PySide6==6.5.3`。 +- 图片处理:Pillow。 +- 打包工具:PyInstaller。 + +依赖版本应在项目根目录的 `requirements.txt` 中锁定。 + +打包前必须确认依赖可以在 Python 3.7 环境中安装。 + +## 5. 发布目录结构 + +推荐打包后的发布目录: + +```text +AutoPrint/ + AutoPrint.exe + _internal/ + config/ + app_config.json + templates.json + resources/ + icons/ + styles/ + logs/ + output/ + README.txt +``` + +说明: + +- `AutoPrint.exe`:主程序入口。 +- `_internal/`:PyInstaller 依赖目录。 +- `config/`:默认配置和模板配置。 +- `resources/`:图标、样式等资源。 +- `logs/`:运行日志目录,程序启动时可自动创建。 +- `output/`:默认输出目录,可自动创建。 +- `README.txt`:给用户的简短使用说明。 + +## 6. 可写目录规则 + +程序运行时可能写入: + +- 日志文件。 +- 用户配置。 +- 自定义模板。 +- 导出图片。 + +规则: + +- 程序必须确保 `logs/` 目录存在。 +- 程序必须确保默认 `output/` 目录存在。 +- 配置和模板写入前必须确认目录可写。 +- 如果安装目录不可写,应提示用户选择可写目录,或后续改用用户数据目录。 + +第一阶段可以默认将配置、模板、日志和输出放在程序目录下,但必须处理目录不可写的错误。 + +## 7. 资源文件打包 + +需要随程序打包的资源: + +- 应用图标。 +- UI 样式文件。 +- 默认模板文件。 +- 默认配置文件。 + +规则: + +- 资源文件路径不能写死为开发机绝对路径。 +- 程序需要通过统一的资源路径函数获取资源。 +- 打包环境和开发环境应使用同一套路径访问规则。 + +建议提供路径辅助函数: + +```text +get_app_dir() +get_resource_path(relative_path) +get_config_path(relative_path) +get_log_dir() +get_output_dir() +``` + +## 8. PyInstaller 配置 + +第一阶段可以先使用命令行打包;项目稳定后应维护 `.spec` 文件。 + +建议命令: + +```bash +pyinstaller ^ + --onedir ^ + --windowed ^ + --name AutoPrint ^ + --add-data "resources;resources" ^ + --add-data "config;config" ^ + src/main.py +``` + +注意: + +- Windows 下 `--add-data` 的源路径和目标路径使用分号 `;` 分隔。 +- 如果在 PowerShell 中执行,换行符和引号需要按实际环境调整。 +- 打包命令应后续固化到脚本中,例如 `scripts/build.ps1`。 + +## 9. 版本号规则 + +软件需要有明确版本号。 + +规则: + +- 当前版本号必须显示在标题栏软件名称右侧。 +- 版本号应从统一位置读取,不应在多个 UI 文件中硬编码。 +- 打包产物的版本号、窗口显示版本号和发布目录版本号应一致。 + +建议维护: + +```text +src/version.py +``` + +示例: + +```python +APP_NAME = "自动合成印花服饰效果图工具" +APP_VERSION = "1.0.0" +``` + +发布目录建议带版本号: + +```text +AutoPrint-1.0.0/ +``` + +## 10. 打包前检查 + +打包前必须检查: + +- Python 版本是否为 3.7。 +- 依赖是否安装成功。 +- 程序是否可以在开发环境启动。 +- 默认配置文件是否存在。 +- 默认模板文件是否存在。 +- 资源文件路径是否正确。 +- 应用版本号是否正确。 + +建议命令: + +```bash +python --version +pip freeze +python src/main.py +``` + +## 11. 打包后验证 + +打包后必须验证: + +- 双击 `AutoPrint.exe` 可以启动。 +- 标题栏显示正确版本号。 +- 可以打开衣服图片文件夹。 +- 可以打开印花图片文件夹。 +- 可以显示预览区。 +- 可以拖动、缩放、旋转印花。 +- 可以导出当前单张图片。 +- 可以生成日志文件。 +- 程序关闭后再次启动正常。 + +若测试机器没有 Python 环境,应优先在该机器上验证。 + +## 12. 发布产物 + +第一阶段发布产物建议为压缩包: + +```text +AutoPrint-1.0.0.zip +``` + +压缩包内包含: + +```text +AutoPrint-1.0.0/ + AutoPrint.exe + _internal/ + config/ + resources/ + README.txt +``` + +不应包含: + +- 源代码。 +- 测试文件。 +- 开发环境缓存。 +- 临时输出图片。 +- 本机私人配置。 +- 打包中间目录。 + +## 13. 暂不做 + +第一阶段不做: + +- 局域网共享目录发布。 +- 自动检查新版本。 +- 自动下载更新。 +- 多版本共存启动器。 +- 按电脑名控制版本。 +- 安装程序。 +- Windows 注册表写入。 +- 开机自启动。 + +如果后续要增加这些能力,应新增或扩展发布分发文档,不能直接混入当前第一阶段打包规则。 diff --git a/docs/ui-v1.html b/docs/ui-v1.html new file mode 100644 index 0000000..e036e2c --- /dev/null +++ b/docs/ui-v1.html @@ -0,0 +1,345 @@ + + +
+ +| # | 衣服 | 印花 | 来源文件夹 | 参数 | 状态 | 进度 / 输出文件 |
|---|---|---|---|---|---|---|
| 1 | 白T_圆领 | 花卉_001 | 夏季 / 花卉 | 模板 | 已完成 | 白T_圆领_花卉_001.png |
| 2 | 黑T_短袖 | 花卉_001 | 夏季 / 花卉 | 已微调 | 待导出 | — |
| 3 | 藏青_长袖 | 花卉_001 | 夏季 / 花卉 | 模板 | 导出中 | 64% |
| 4 | 酒红_卫衣 | 花卉_001 | 秋季 / 花卉 | 模板 | 失败 | 印花超出画布边界,已跳过 |
| 5 | 军绿_圆领 | 花卉_001 | 秋季 / 花卉 | 模板 | 待导出 | — |
| 6 | 卡其_POLO | 花卉_001 | 秋季 / 花卉 | 已微调 | 待导出 | — |
| 7 | 黑T_宽松 | 花卉_001 | 基础 / 花卉 | 模板 | 待导出 | — |
~sd66-@pV`jwfG!xp1zL)^K761ghx=fl%9O4`T#Fp1Db2a(T00)nnq`_;+woOs4& zIq{$$QK#m1y9AgyDQJ#Qi;qcHL&i8U1Oi%m;v5&Q0~5JTcTRzwrK#)WVUjS6Chdbm z$Fj6v+9FdnuDvH(wzBXXjn%r6#Lzs7!`k${Yau}&zl8`VbW5eGI{qXYn+v)|Mjk6j zU68~uHvI+IRaQD9s)W4N*pxWX7%Tlm_1eJ1iWO0%WHi zqWO2J@g^+O=L|^V(J7xi1W3y&w2D-`ab3X$U1YL<%pwH1M{5)FySgie8F?lo=aFmY ztP_%;;7c%$g-qg}f$qS9=B4G}*2IxH1mf)$K|?!$>874AHN;rf;}6ynSv;*5;I{qE zMjIio0H|eWSIwuHYqwDBspC|Y-M|hTEBz-J_p *y!xZbld=()W?Iy<+X zvU;FF^+kIBcEFUl9hRp@fZc`7ZL8tJ8xbhTzO1rTSxqbGQ%4mra3=FCDjvPbIS_@= zNJvi@2?sM&HF~tzUVJVmg3SgNX*}ERhtYGxcii94E$)deClHPGnCXJ3{iv*Fk_}T% zg%sHbVF;e4IO#CQPm}h`$66v74NW}COiIz{>THrLu-3wF{Ss7^3H@ZQaP|b0gK7M% z;&cg4+=X%dU6+k^tUWaxj3u {b>j;U)>EJT~4lwSMDU(-B0TV#R52Jo4DL8kHgb zUXrpSuwymctfUkOGo!4Dzg1}Km&c`u#fUNl4#{px*^AUTKBXl3B6LgNDf4vM6pRE4 zMAfShYvfA|EoL-zGIUClklo7AK} ;^fA0?F>l$e~hw0=2TZfrx zcF)nA4mB55F8jR#%WAFidc`6aZ;f51p$qBe@88@%gr6KWh<+9M#Ah`CK6lyAoi)NG zqSFALKAt5;uD6|7+kU*B7Nplcp`C0ZAPN86VKsi|(5QLq+o5JO47ik(J$fkWAnqav z`q5#-2CO!i44lpv^;{?W_TobD3`KtvmIy5yyP@Vt3Ff@#5L)4LT{|*H-P@tdwbl*D zW? b>IfG|UG7M_{f3xud z5)$K+<-0bTn&uNB`|KFy^j)-Rw(N y(x)JDTe4GYu%Gt_(9ruF{LgtRWaBBC2dMrJg#SOUjy^d z9FytfUMY>ZkFws_P5Qh7(_*YX6@7ww1J; H(h@-#4Quf60RwP6NoGqoy!;Djk-FFwM@b w5)G>9I`;X8lf zM>lY{_H7=_Nm*vgtxyaGyO C7I1v5s{ zx$KQS+J~s-Eef6(^&9LA+ISdg<+2AI@+FVCDQM*zjuvk_^9Z9bBog}5hK~L38_bF$ zC7rFG=OCwJ?Tj)xe>nsZORbH%Q}^b1Y8T{aXE@(Q*PyA;(#q1JKe-%FZ*gjfisQl* z|Ebx dLae;m7QX;GVGY&e&O4ZQb3Iu~JUfs(nq$ZO{xSc6RmBuUTSVUFS8w z =gF^@*AoPHbb((%xy2hj%G? LW-s9=&2$yoe*-9TF0Np${U-iY>u8#;lhR@_Uu##!<1U*} z18$%(_ORg7?dyWggJ-J^kGz>N{sIZB)9UG|Q~Fjb2>se6IiLFmN~xnppt}L=VbkAR zU{_y7+kl69_HA?)uccYx#h(cB63wT0)+agd^@u-b#^>>(#VYC~pL(OS(q;zJo#{sH zX;!7zg4dnl?1z)Cxknyh6T0%!D3hTG__j9fv@?3XZX8Chjw|625+9gG6Yznb7bJCP z%`QskFDaq8lQWn!sct?sP^7O(JfxKVPbO5~>VFi;H!u$t|G3?zq|8t1FHXJ)IU?Y* zXH1-n#8PFWDg}s8fms 9dx}gQ_#y!Xm_rmWpBjhh{CV{Ed%L>nR6-0ZsrP^WQ_!PM{0BoRDTN5 z8!4!&TI&ZW+U`K;kY-*&b9sYWS9iIjaNoB(j`P4Kf9ORer*{RAf;8lxEL9}juMEzp zygHZ&pO_pG+x}kG2W;5Qki$k%EwAzm*gK*zU6#*}&?1+k8 {C^#C}L`x0ywyO<7?CizwZv!#`I-NfZfT~u)(4_%M>CH()#i)*w;r!F@D8K*U zOScQ+jd$0K=m3ngJm^jCNL&IpkC$6KkLGHIW03Wdv@6u~`9%*aiObf0t&=K-;y5mk zrMp^oriu^;3|p!r{#78jC0n*M1u|<<{OM+?>nhlH2h!jd`w)tw45 w{ZK&1_+Ijp6g0GJ;s`({JhWZ^=A^BATVc1eywOUS!=n}{->4*zlJzI&<$Ia z*1x4X_2% i2VO&j{1N8 z@%OtiNlD4K|MW~`75-}d#PrSH&!7J_M>wZlgLXe$MxKA(gZuwMa{tc}{GSL|BI=D* zvYg~(onDoLjSbnD%gV}%SYBK{jj*sVcW3J6(%f7hfn}DG^e ?c+p{B5-@JaENz}ivptqBSPSC$Ne&qaj3&1%Q!IQEjZoBV| z<@w&82nh+v cK5HmJVT@=D3} |F8uMSyTk>(+3~b90~3(Z$wc4yMX5T)#l%@$m4ziIE0lo1mlvSR%>iu?hZ5 zF8D8sg%{n^+Z* m?RyChshy3D zSf*z$YgiKR&m~H}ER^L){a{|Jp}b9|-|=?HF}=8dU;p({`OSZU{~&D0;;XB9r0pm0 zv@k$x%SemG6T>$g-8WL|7BPThDCbv}_@tY^zrU%eskrzX*V&h=x3@$*45f+2NPb=B zpB+}Wu#B9G&)Hn$E2q7k!nt3C+(B1r^5pB 6hG=#Vm)zriX>GLG~oitqIC_f`(m42hc za7;x-1x8eNp=MIu$v!F)^FT(#K{fr eZ{1mLX>c zcSnRQC2cLOVy%j0)JdCqx@0;Un$qKPPb|sBnWHA`@s5(d^XBA_MZQA!x6@~DJqY &C76>i>5t4Rl+_6L5THl<1S>Zr|n5xBGLgwJ9^3lG<2g>3?qc(P0 zONa_!mx1I%&eL}LkOZ1>BHM{er~+LwErULqWOsLjZ2yfpGSHr>(i8`=lM>#0ei1D6 z7y+K2 j*I!r+Bj5Ivf&eY#|d3jq( zs+h+QF3L6AXEuj)^oO-i9%NIqej!k)*c?_?Ei z{e?if5`50BlviFccD zqPuyc!oroV6`cY9h;Fpb8_vH{q6W6;vS_{%`{BnDi)6x{_VW`$s3X8(qgp>rKV^a0 zOeZPoy&5L4!N$^LqIMgKXkCULIaZsw9mO^Hy|w1$9H=kuSuKGlUQCOGkv5(#^;1nD ziLj9pTBMqt5sxF7Q;LhCXCt9?NsKQ5HgHu}*u}#tX|`xtO}b7SsF#2(&8j~#H{!tJ zaC5+{o@$ruU-@G=GO2?>HVS>5a*=%n8VNfdpiE3#CLxoZYE#U?$HQ}2ZZ-#pGb6=D zoG_j29ZJq>8qv2qL7G)9b#(y%!cU*p4i66lD!Tc>PwtslS?33Ct~8Yp=PPh*tmH_5 zy&r{Tbwi%sIIs80k6wxNgNe)O_O=3k wCPNUM+R3m&z_dHJoGRuQ|Yb%BOHF6SeoC~p 892iWLw-nJ zHa_TzufMlu@(Ra~a8eCYZImV@74jTzBGSr=nV5vgQ0Xh%1L>`}CX|wX>NC-zsfy7t zk+RCggWE+&BZ$$HlTyueJV)nCM!PD+XZK2fqLfE&xByB{62h&D9(K{;xqB+h%B-n^ zv@nS~(D@kBFmq_CWBeKBhGkYq5XB=(VY^vIC$v`KS!aHZ7P{is$*uN>jqubz ^v6bC#ptaWz-fYg6o|u055+o>uvHM7sIiF6mJ|1$tc!3w5o37v$ zpwY6IobtBfFAG344%BhGHWv=m?pZoRe}~-i9l_hWzS*Lf=j>R>VWr}`rJZ;IjbOv! zVCxpG?7C<3nZC2osf75}TZ@*Dk#bdzp7FVqlvDp$8&9bWgs~!&<(&PDfypl4@I%__ zH&)IQ50uRP*a7Wi;^!_q-}`H?ap~LDyOc%^GR0hgmUc`LF#Da2e$|AgY;;v+v@G#& z`K4W55_CD}asZ>)>XR0%%Xapa55&AH)1lb=37OlrNd0jgL&}&KW!`!D6x60W`GQkd z%#CJL4HtOPp5LVkYV`9pr#09SCI&&S5?;BU;-LoSv(rs!T)pY|mPqBDVNwVP$moAi z3Hj7{n_Qa?gLFucK0)7|9Y&(sZ`>raoFH1$2pg?@qOUEibdR)={8^f3wOj0X@NQHm zX$k_nYf#iB&XK|6w6Qu8jL9R6r7#}MZ4u^T;Ty|HF}mA!iS|!9e3F5EM ~X z?)8t|zB7(36aE8s +nyBt5fhS{{8quTD&~LXwPC z5|e{Ho~#;uFQ{Zihm-%e#w*6*fl*DK7*)(lqo$)4zV)~%iH38ImQtmZ7C(oX?9&0+ zzCPii?&Zm=wDs-nqRDyf9rSjZjq!jszWuG;K*-*i5*&N?@H_kgWlRo<*jNkSL w+#p)a)xc5H$)}OB51@ZSgbsQ6ieb7cv^2J5MrL8vMEosO|Tc nLcZY3HWZwUIH|%fkQ~3y_%)K9@{fZ7_ zQ+>L+`aK!3F*G7*2^-Oe-ukY5!T+n;L~Qm%BE8aXcTzqTMe0jc#i=T~4FRG(yTa3~ z 3i0=jqn#ARGkr?)s@5TJ4! z@R%>Ta5!NUSv`H2sp#C+_I(0F*a+fsxY^jj&Wy;Tl}><5?;==qA}2bY{QE@C7R!>W zO3z587uEk@`s#dLVdv;j E*0=IN5s%b~>?Ix4+KqFoXTk|vUS@$R>LusqI zrL8WK>t5dCe+3rA<$?Ozs$Bh-Q-rb~PJ3*S#LfEvEtSA6G4ZNnOi%r!T_&S5VefoW z<&W{Z#0EKv5&O-5wiDlhB8bW8NDFrzfeaRJj6*APzhRfibbXVt)}y%~Auq&YgNQR5 zp%u~ekJ6T+e-Ddufsfw> EHfw$L&D?o{|$#X727rs0BpCb>Q?$mcVy zX;3zC_crWqZ|a&eQYz~h6PTfnnMH$5+0ky*TDGIB{ _dj7?!);RNsnGRuv7k`9UofbF @V!LMrsa>= z_S;u=!|_vS0bjh#yn=w*-UX$Jv5i`Wpc!5T=lc5Z8}{_VLOTzHsTdV2bKQPOGO2A& zQbsNaoS63dkeMgZ(dYSzv9E2g^{r44iY<;{<3A2ebY|z(mArV11!2OtUYqwTW;P}l zc|E7N^^v;WjbZ@kbNhpd;aBVWv5eamW5BYVN=l<3h)#f$U69GwA>e4MACVFsB8Y*$ z=a=T?VsiuoAt9N$m49REr!OV@Zu k_}HsA!|Z z^2FhA?|jth>iB4e7s9vefT=Ozru~`F%}{eXSGR!|fJ)26RSa?7ktlO?bFq|PV0a(x zKXp47!Ojgs@u)}0Z|~4=nBZw^zPk$+>@6oJj@kx(SKMt_JHv9O3&W>7e4P+*+w!}I z3u9XN`OwI6Q8cakF@9=#waHEQ!OE?1g)n2U`M_jf5~3J$dLzU3`#y;>Fz1f|WH (x=DY)X8oVjY6uZAI{F_4=XND^?;QyUjgP?X z;V-JO{8OdOG6g3 QNY!x2-tRPEJn$Mhjqae?Py}-%Q%be*zQRkGPp|zIQa(r7p`2M{h229Sf+6{_zp! za=U8XE%=CNDNg9DTtKsOIX@t XW7~wf=0NVTzn9JWW!2MhtN%*tIj1HeCwhCwus8AscnL_ zkGvgMh&ErZ#|bOOwCujB0|K>Q7@C~Cd +)(F#7WOFZy{AbP;H)C _;($Ga}IpHCR7s ze4~k{H(;dhg+Z`qu+2)~6D^Gmic=W(G;#$n(a|9xBZHUBW56 6 zS6~@11NBg$fBbl~P57ekFRISxw)aKQYm9(E_OF0~7Ts=NTai;)If%}DsFK9})MWo( z{Hn^M@xQ5^m(2f-&i=m&bnV~(s;Z)EE5FY>)SW0!Q3(FA4`-k7MS6RB(3_;4&Q6og z&YWJ`gNk|~rKL1HJgNU4SP(*aMMdYIKkr{^i8n5`2d#epb(1g8?O*>VnTCU_Lwq~- zcQ$ZvV^p6+{=uMdA{ioZAK~DfPC2=_%BO9rwB+@oMt6}z#fXWC|Ai2pn3qNl({7&S zcAk7Zf`fw#kEds5(Ba@v|7V^}y{W5~UIKtV(k7DjJ_VRgv`T%VrE}Ips1M zt-I7_L%jjNfwQtT{1+xap{>_jhPdu7%I4q>xml1875^)W{{`^O09h_T>C+Il=4l6+ z4QRCS9A;R!>62~D&G$nm@CF0QY*Sm3y?7DfJoS+2a-SQX4-Uw;XKl_4-mm4&aH`xb zoc(OkMahClUnmqLN{Ky808??lbdU=yX;=NyE ze;9*ARoHDbw4y~MY&p}HnrgWIWCI=lNFUq9pvM=CO9~pNu^nK}PLwO!rTZ#X+>tfY zoAV;k$dXrAMz==(p2}s-vdVC4>piW=*C*NZK7#$9qdxq*D0?kH&DG(O`dJ{)#L?kN zVjy7PehU;Tj#I5bd}N82>HhvFz_6`b9?pl-if4ZWs}9IElQ^4p*1`()oeoV7$ qyD^+Y*CisS9ot$m zSNWQCqu1BTnL|UF^i?qJyoQKAeT5-((Osh^o#ZM1*8WtImdq1}ttKoQWW(4WzxY%1MNPZH%_=?X(3_3q# zHy=M7?RhV=mYW6|CBT%iVKPk0W~Z&vK!*#Ou@yzNf(zm*U?|3dy{y&0zY!4nc@uTI zRji?3P~NPiS$75eBpkfn9(Yv<0k`0QbYR}0Io`N9?1s~iMMJ^@L=pnODSin9Y2V`$ zI|>$kLX9pSCFHj-pp5i78V^s=T!MwehWb($rJZYj;FBXrb*c1fSw7i$_r1xu!5Q>d z%A#XZKl1UOknYZ`*{~EAzM4-}ak-t&9D({B1{+n!$IS&OcbC%zJb3Q(LU&5mxk>!H ziA^e8gq}8f@uH ^8WvK{MEgnKtHiX`oyzcLm(+pf2 zo31>?zkCeY=tb~Ei{mdX-uu
|M2bu@@qXYJ f6GQhBToHjV8BLA73kz5EZ 7)n5C+eQKY9w zxfp-A%wHi@y+K9Au}#8L^A*)&?^7-<>wDQ;-aE<0rq2nygL%l(l9KSRg2EC+NX{-V zFK=(Jc4=O|j !lH9Nzz5As_8p{0+R}AY8;cInOM2HB%_Uw=82z}`HsY=03M(8@ zJaK@1{h2;_DEzcCDNR mv%xy%hQ$6t8(Y1>m5%1n|AIg>nGgL zgre>K(1JHeBW&kj;_C2Esy%S&5joGb2l^8w%9|(M%uqiVbk|opDQmgetbaW`lpg!T z`{+IPHN(Qup><8beGwI`2umIz@}qR^P9e!w|J9K9o3E)E#E*hGKj#Av@K1~J=yJhA zKxI7+M!h~Q5zqs-x3Q{?XY4Zx^Uu*GWzQd~KhvJexo0$$bZYFx-j*%pKw(riJz>bb z-(W1`fy6ljj@rXM){Tn8m(9Qu5c{$laFRRa*yt8Goq>{B5&fCZv^(td!$)&pFM`D+ zh|T1ax_R&$3x<0Z(+~F}ui05#S6-kqm2#lh-enout)eIDaeS5~b-<{_rT1!P8Wo-s z38a&7={C1C;7#*08A!9f9di}rdr??6PJQdwULvEZKh&Ds#kHq8YXB)nia-&^esMl< zo|(-X_oIy|T_b~j aDky*G?6zV_j&O-1|3|x=|(!UpMuNpfF=`d#Fyz7U%tr1 zjS;xpBw?*S-WL!`c7aUxNu$fl t+=5ySgd@e^GnhYZ aTPaMT70jb@v4Omq$NJ$V-2PqoFw%&8cqW;nHKHyf bf9$O)GYlVa)J*^rD>$s3RDj;jh>aAWd0_`|B6Lu3r)`vE-~ zx*q+(jX%hxq-;p( )O%V}?~gj|X6ZZBF>{I>eDXkZ2umQWQP z8%-pbd-toE`-yFBd{k<-I_t@AN7qGvmbv3zgHty{{?cFkiA&6XtG2-tgaR-F7TbUD z4)oDE 1sTXgP8bi&laPF)o>o_E`bjDxx Tg!&}#!A`uCG@Hjj4p4`VmS-d-W?8bKG?HzzTLvHP{x1sa*LxJ zpVIK_xG6Z5wQ~#i`c;TS{qg-F-C{I1MI&Zht|cl-0( zRh-1?qY79$eK%H0$I|8A&)KI|>q*v&7qt }VkLr1}r0~ME zB|Kk%i#%^~viGjLa$+DA! @ zYgfzM`ag6BKDQiy3FToJQzj5Lhvkw42X6h07w;}=5nl4r h-WrAb5(Y)vs)LVH zFAZ|=4C%s p3FU8}tGPK1g9jiK z!mKs8&Qn;9%Di!v<6FvVj^>`!krvG_Ov9pje-dJROF%sJXX4Y}7bnWnYt4DvGvrTg zaedhvDa)wHE({{{@lkpC+HPa6@d8zNne}q98_c$ &@2cM@HI&Bsb>V6} zct}_hb-5*O;i?B;KdK-Hz+) 6 _MW|D7 a5t{6EP%?5d7I11%z3YPXjS*;%Wotp&7 z;xaj3NCFU0Nuh4p&K{(3vbyKS?s?niE2Ztzd^JpbnEEwfb1QTl$E}Fl9YLs+j637K z^@4vBN2zw-_q)Yr7VqJGLSC##!_=-6ntNeRUm|P4Xf?^Vq{cOqD*$WVon0Inm3~dU zMMu*Y{qZUtqnu|iVZ4vHOsQib@wn+>Y}BV`Ac{+W@ZXv6KFV=|Ui*@gvkt*R#C4Ov z9@4`>-sH^)TJAU+SlZL!QS*b@8U#EB89Z`l%j@YShH_QV+l9=yE1dgPEk+JT0K4;2 zB-v!!DJ*cn@81;xT`jXwo#Zdod|+%z!?~<_{ZH&S*TS(H{mOQWKS7!AIiAiZEn|Ls zK=;%j#N{y8xr;a|NKc<|O%6GWH>n|6iZxK}{y~Tv_J9lLfc&pG3iX3yMh#-!!N9A| z>0^%cg>qZGPQ?vk{_AO+dvK?XvcuzD6n*;1BYFvwe_4l)rk_=B%qW}d9~gZ7&mJ|^ zR)-1lLjs_*u%e#q)m-N7j+*{+U77PAU-kX^v>76T=qrdjbH`H^Yu?MAnw?~S$AXMF z{5Cr{DazxMztrgGN2?E5Jnp{^Of5SEOHvnl&ps )+QjAzF#hKS4v=O&zBd70Z z63c}3Oibx08%1)Z*gsAl9*&GpjlHiq=bemvF{s3*F;FmZK(om~L2&Z@E~P%7h-J(n zus;o8cV{E1u>7!!D$R4VUTllSfd-FCO!`poU1C1LBboW60YL|c$^L|jZO z^z%B2$Nk_}E ?2GwgetzGO2s@ z;XZB^S^illmmZSR;rF_s3q3@3-Ki8S& zTuJcv&`nK3xH|0Ddmc)w5zH!FoSX;O9`t_R0(n4fd-cSYc|XzTY{C#5EN|{56x&yZ zyCVSrJdF}89~b_+sW_qh+(XUPC8q|$c27&;<&UvMA+v4LDvqM2PodvujQEik;G^Zl zA0FTfXF&IR!i8Y%Ms;n1K9~W!wQ4yn0i_)q?wsR kN?=Vm^lY wVGGS!AYD!Hxa3@?a;EJ=z&L2tS@wGQavm|^PV0n zazKi1A*|f3x#rh _3^frBo9*d`%+qQOYm-Ey0fQ-{BdG0l5BP9SXs(CfZN9APaE3_z6CG^coPH&P zk7H^ql)`QA&qscXnUv#EzfKoxv7JE|k7N9_l6Pj6U)uFFB#2A2gA2vdN|eBq+u!o% zO(~IzUcXLrGa4y;9;%gUj@Rv>m>NE{y63&HFa-EqFV*u C z9FfZ%O*F42Pt{9?p%|cbMS=+U3cYO(_FgJOa1f1kHcIE-*b3{>KXopdSriOxyde%u zVwQOj7iRqzjcV$g(v+mO?JBsJj7pnj8-MdI9F>5kTCEpm8`)6#_`T4A!Ds0~PxCVu zi~Ehpj`?w3Y= 2xHQ1kZg~P@d!5{ ^5ct83Wg2?J9SI zX~<=DcbhbRMu*J-H>S@pUf1#4#in&);SP%Wr$r@qMDIWgw+Hk4 #pCWGvQz|7yPDvSO~d6CnO0d9un2%k1ElJ%&gkAh_`Q6$hXpaC*aZbzpld z8aN J9^mV7c%X>$X5Z0s8U?^*DzZl d}7oeeFlGwuk 9VTh z)AKI})iUJ<3V(|`0Y1*n&av77K%kzceye-`fnv$rI|}h#29Cnf=wzHh83_fep`bk0 z T CkLqT{_H|7Rj zTr1Xn+j(u=+BYb@#xBsVUg*rtxo`=z4YVh5$| {ubnjykoNZ0jrz^Ac^d9TdUj`ph_KwOHx|lz#XozM zelhS^{8298X?@U~VX Bi2-KItjaoc&hGdcrgBnGwJ@^w`q`;Y~PsnHT#}B>BLLxWhn<` zY31#rLZ8PsX!v8xikHZq#L=X}gEu_-(? xh>!9RyX9zouv;{vT!T)T#M1^I zZ}KKyIuaI_=bRXVTp~fUl`t0GC&kB;0RM}IiU{xSC2nm8%jpa5UH^M7Ih?eGW)G0b zMJS^8OYIYJ@;9OHKfq9#>4R^OXuFQMPl_gnM I!s04 z%%;iVB>(kq%ukm&;~M4aMX!8xTU`2?XpD>4^)YO>>2SC^&wPPi ze0zGYsu9vYn%FSSQFu_8C#xTN1>o%1^kT+mZOAxXpn{LSf2`&_F#0I9KE;2f&Wpq8 zxuGxty$!hD-?HW< C0ecj;A>m(L^3| Xw zJIw^nm|wv@)fJCw*Aq(nzW#dJ&bG9*d;&wbBLQ$hR9-0coZtL8Yp`Q%Y-39w5%O#o zLh9L+#dAtJ>oe^Ekas`q?CeX>NL_Y|2}*pOV~WOm(y05LDE4whRdbs^5Aw*i`R-g3 z*j7rdkF~@zc5)monFh-8xR&=e!&DLT3T!xTeeEZFWCUt&Yq|;I9&R9H#_oU4X|0w0 z4Q~4}JE|7v8tqKRH!)Cw9OEtho%-V1g5w96Q;?GI3BWL6%ATd!fzAn0H}O6>CuLeH zil?won9{kGAemzxQdFf+&>wi-4TJwaH(pgIQ3rn)W#`zAeyFFj#_d~ak^%wM*Zv+Y zHw7I8>vI&-sa=2B(9IJ0FgEqMXz7`kSl9LIfGh$ma*k2|+QQZ&H|(}E{{rLidT ldtfx4mH;7;;w4VONOZwYEA^f0@~0AUR&NqQd2@f! zk3)6< (e)6u;-o>{`=romOiyMy!>ehA78!Aljz{X-yUfYk>uLwmx>@S&A29 z{-ma#+pAn>%R95!g&sMK{-iVGZXdu1p;gzg-GQP$9;?fF0z33qlpRjR%E0L?>Fn*z z^0x3&SGnQ{I8YtqN+u(h@g}6DKl|VhHeAbTFt>58E@bP8y4C+&dF5bc
z2&*3%T;+@!u?sz)2##nAv$6wqk}Otc5=TdLxCPyM5`pbR=LX`1id$>kePubKV%if( zqWCkQulN=i)U_nz6}u3Sn*kLe=e })uNL6oID0(D`YU6dW!Vh zwNLL`T}-bOE_?g&s(KaVU#5=v$EKIGu29SsWIc86_1!2(_}s-_4TC=KVBO;qSv#Qw zX{-S?AhR6l9Yup0yA?FHx@4PDVgcjGT^{FZcImd6h!xLorR^pfw<4`KEU1UhNZ9tr zOWml!6L%N;Y|SC%CoSvOU~$ 0?$4igJ`CO+iYEmaK4JV7`m?*1 z*q66-!qj^s5U0Jz7?(zZHE${WcmccmHTCBy_w9G=ho|G#TnmrAz~Nq`sv_n-K8P>q zVs}1$mI>i1>3%P~=-YUN {G@~1El VoF5uW&-pJiQi3Nirn0#2%dcelqX96>BA8 zb?Htjay>Bfxq~EK1$=+B^yZy*#@F@6$yaczHKCepI8Y#U|N9R5BK+A>2IG1lKa|5T zL});f%dp1%UEl+?08^_^(OC*%y#*%(xbo1?W5eo^_WXMSZO~3^Tb1x_5id~4XE>}c zu>4r@n3w)L=;_nmkF;V$W`vG;fZKP;izheb;fM(e?#mxX>Nb<#yZaSm({at*3(o_P zuo|Y>vXqbV4l?x6?wy@SE%0)+rFJKjKjt3p7vQ0Q>PGRdpk&sqMJCMKE;T;0S;dOi zI{eJ!%vPe_#+)q*gv_G94;V10TruxxrgBwJz>0fb;(FrKWZqupc46z@{`PLq3Y^?H zw`{2La5`r}Zq6b -?}9 zryn*5Cl8ph95M=ayb@WBI+sxmwfg`prk!vN@To*d(1hM$jWuX9BwXO{OV}ys2irMK zoVul>sB-V=lx6e1mcz$Sra9ZkByA&Wcj!*ux3isdHJj!lSTJ VXB6IM^7mg%||qIg-?wWc!TI54^h5N53B;yTD=j(GoV@<$|;NdLIU4LKG;ouwD{! zTbIySvy?5|jAoo~c# 9Bw+hPF4AN~OB7<= x7iv6-i@8@z)*TpcfCt(d-Z{3N>|;RDZqEDyN0i9Rtfu(h><(cPW04=XcM5S9 z*NoM_M#woW4t*4tZf;)^n{z8N^y6|?A4N0P=!K?`5Ocu!Uj4i^$%o-992^&1ppq!? zRzrs2AA(D5mrZ-R#ZHAW^uSC*SJ(SshOtgd9u6*<0vn0n^$?$kNDfK^Tkm62aEUzH zo&paSG^p|b^ma321ljv>j{oEWG;&~q7k7y%i 6kW_)?G40|N*G##OuG%? zzbQ^^7}SuULv=kLA4NN^GY%SLDbwl-eg=Ad54>yb8f%|_KpDws_xT`Xo?PeyUfZYF z+7Q*d=8;y{+WeuQms>B?@r(KNwDDmChuN;ZR&B v}_(h ?NWO%Gm85y4L;x+ zTjDMmszUzULn8Fi#|IXJ_bB-eh3}2KbRT=7;nA!tsos{ffF#|cbRWoHcdf8*%!_MJ zx~)c9g0FY{9!R{MoI5CVgP&Cnc5(`7TZ#&rv$i_9p$-=R>eu6FKwD)@Il&lxCla z$4#^p>xG&xmZlT96MR$;*e{z_GK6sIx6&$0tBX+sOf&(O(oTg6-)bZ!+HXHA<>o(4 zf@Bi(#>O`LN YbBw*+1SB_b0U*>F8hr}z~431)u--B||jqx7;@+Bff9B-X;HRll+5pGW~z zJjA-o@X!v467dTeVZMgHih}aJiln~XY51S(W{bDCz1~;x2@p56O2C (9#{~^t<|6Uv#HPCW-DOvq{!zRIX9r&_((Fp%W1j)pvPcT1 zftk5Pw?+5o$)2BlP~612pZzvGM;^#>iPBbKesS(9P?8B@X`38Vpl5bP+x(ap-hUPW zSSuRoVca>9MZ=X&z+Y{Gy__hC!U%LW2LYstO`0VtP4uDH=jXjTL;vgacH7RU6#fVY zN5b%b8+y9^>$vH*OLNAS&-gp4MNePl180TW9~&5!Pt?p`YCceCNZM|EM2?qFtzUsU zD&p3*e!Z^_<-B#|*3Oiz-ciuj`#Sd$hWywp5NK#yA-Y4mlE7!!TrKXOxKQBI)kT09 zh!ylxui!c}oqT=&Zh9yD$9R}Uh5R=vZ=BxYk)c!urUBh_GI{M5IyKP&WJe^aF5{{% z*Z{iUgG^F7sUO%-O`{iskk4s?604V~8572(ORmdgFx_jB&mK76gJ*UU-bot{8fz&> zw^%Q<3aaJDk!`@N1NoHe|8k)mwr<+QWK2u!xBWs!PX}-UYzMxjN@_KpK72JwQpQ_b znkgRJDd6xDu3>l*&cjD#ZX#7h@CDol0Z||BQu@ESjlKxqDLI{Fi@PU&)L!%?foC?U zn8F8jYaRhb=D&KZMd>#=;#e6Yl$0%!N+#B6L@}ggw)fR&4HxIDQ(q&Wqc)gZTy=#W zVybdh(Xy@B$^?fQ0I`*&{_#5$(|2dWxm+B*s?s*WguZS%{QcwB3OK_3S%mp_ jH z@Xe4u(~OBimTM&9oQ5ubXGs1j!_TE2*+_ro78JDO!w?BRufb)C0npXkB@E(FC2W$< z*9w@{#8SEp6yE}A=HIyC!#q`)-_N#57wBJi*ICsS`n_>0bV0>_^7)crGJ5gh?|Wkp zVv7~nkq#N$Te84%nq*@)>*q!MA`x^x3D}{LIh!e^eJ2lTWM)^U90h-gj4{UIo)nKy zVYRW2CwrNXQ!VRQY!*8gxuk&mJ|XURZ;eJpI+4VC5A7ixrhz4%l(0~Xy}KsxCI^;$ zO$=?&mqY1rxgeNPlcB3BZ_`;1l`N%lIxby^>g-e3n41 zdNScB6yfIL@?g6ti-OtzyoK}a7v}!Evzf$(vgue(+u^#BxOO`Hn(L0KJQl8&E7eWz zwJGLv`7gDiI;^30jcU+(_Y~}j!@unG4DTiEU<@XC16O$_MkOiCY%R^a(#W`XQGwaJ zKVn~13nEys3n;xx^SJsjFFaVAf1k}%B>3{!R$on#{$E4g)o)pm+p;CagR`ZhvQkk@ z`soxosBLk*f>h+ef$Q59gB-OY@d;8mh ?rL=FW|b@f(D3)yn$@|}{KN~s zZa1BOj84hE1!eUH#Y`IC9w2u5iH}kFOl8_JJ-EfRbst9wG%J2+YWXi!SHK#3qV=VO zh=`){$C w*AS^L1@k zjUXoKe+<=RtlXiZ{TR*zSN_OCjM#0)jq7B?db=#3;ic4v=)zk<;D} fuc8-3FHgQAc~SB<^#?uc%wQ^KY<|N4HasN#m+kB}H%8TTfJ{jngq>Dx z>=PTv^jnW9OvYfA2Ih|PJC0#QE)G9{ag^oNbhfJ{&(1o{`bH+QzSu)=@zV&%3oQ;|XJ3ApwiT}?zJkpkfeLKHc zG_XfIQA#9(nIN*oJgQ1b88nxU3lm`SXsU;ESoov{G7W&E`dMk&;}j*D3VrjpsF0M2 z{s;%0+C&WxvD1WshaNB=T5-ntCWq=DO4)_E3EbA2sXZfAO>fDf@HRIm4WqJBlbQet z+dDGIXSu<-0}iRnd8k%Jjq_I5@=WvXUz1XUy4kB8gT4^qaeG$76oq68a{5#l(pvL) z1gpO=Z=}u&`(OJgO58-Xp#RJ3?i7=Vt#vaZUu5#D45O2AtjSZhIPdKl^Ap8Rh!$l8 z(@KYgzy_%rWo~~E&1}M!ktsNkQbKM(LT;snk8zOb4WWkvzMYDLh3tB&A|4<&pMZ-3 zJUaZmS8$xa)d!%;%gq%JnhVI?YC`TCSrqGkat%7Y`yTuIOQBq^yj~k?o$=!mN|U=R z%wN`Ox0Wh~kza(yLC3JfLlj;h@D+x%g~et$5W?~oyZo -K+*E__5}Hgj59snI6=INs zp%c2fYY~shWe~VTS1|_%$)A--<98laFiq_^oc+d1?&z!kaeYD!OCl}ylB*O;BYWFw z`DDatlm`zvOi|n1U kJ$Y1twC|NS&4$wwZGwEm7
G&AU9CXw@o+61~y-r;St zcbo7|^vpY$Z@(Dh`|nCmBAT5$#3 cMCr zTa-gxvB0Z|NmWnXiIntNfw#W%9TmfTlRrm@dk^CXsRN8D62C|rGMmpp4gA65@D$DC z5KGB 2cHzONr16bWQi!G`z+5qjGVvUz}+F{pk+UlJZp5MtR*X}K& zGqK8I7YJ*y05#&kHUV-l#p_ thacyvPchdwDMc)OotqCdN-9peK;I_MuGLV=n zlY!Uk#-k$li^}qpvh&di@S^k7ubIys-^2o%o((nX6PTDd#y63MK1CO@(kegoEoyj# zy~o)5kDdAIM{;bWKl@|!iXj9zkwj90p$Ig`Pwt6nwBgS->qGK5^~fN%d+t-al;exU z)2$Im4Y#PrHK`nNb|9HC+S@(W-GN_**&hO4{-&B7^L?_jUE--RyNwqf@w#?CettX; zeIl2^pP E zUE?`V(%sqOGshGr7AsFVY-*}8E-Yn@d1p{wHpc1tTO6wRz`bK8od~Q&-CPZAUGg&A zOB~u@w2VC-hVjRAw|@-53iw <>}h)vf=2gRH$t)!iifGntc5B{@Hz}&qDAt z2MdarcbJfKYli(4)n?f8DX7k@9lXpOLda98p#kdVjsY1@Y)XN@mStZQEcvO>`53D( zV-PaRqLVU2PHwwtl}w+Z+cB~(U0vmWzYiyxXHJaA!R2%zCCfJfR1EkmXH4c6P7ZS8 zowhwTqZ7tpOV9nd4<2lMO}&_3aky|1;{Z=SP#GmXjLk+bi*`JkQ1tn*r=py+&U%wR ziFBIEU^9@;oFe(3TmX;N#|DQT 8`;P-Sr{JstS@vFN4S1|5X#%Y5_jM!m}sewp@eeS6JwFORx zPs6i$QT;(D29~6A9`?j>IPRsFrC0HO*Lb&fgeAqNT%q1Ff3E!D;BfDLxriA2Nedr) zoxv#{t@s2dNdd!L+JkBJ=j!LLY7j&U^7% {p(N0 z6R$uPzv{vt?_8$i4 LN&n&hchryt!zyam-+SSYo93rONUR`mvcK444 zdM}o_Im T` z{M2!;@HzD+>-!JE(|e1m&uVE4Dlg vf(JMYcC_1He*K^rD1kgJ~z_}Qf1AW-r=%Q0#534M)YIv~g%ujN nIY5P*S}?d=Hb(ONqC~m{ zyRPwG-=n9z;~i@CEz?^870J$qWA_4gD}UA3)39FlSGvx9?K|yly9>U;i>K*v@ClBN zNhq9SB@TlGc|}ng{E%|p4hHu7(9-_=WM5Aj+h`W6BQYsMHH@AaSe(4inF}GVkla2$ zB1t>r#+J6)>cIW|;F*_+!T)#n;ovy>Cx>Lbn}R*oLCOxO2>gZj>R2+2c7voRb1gloeve0V8uVVC>%F$ zXmi^vUylNTth^V;vyVV6k@05Xr&G`F`n~2e$JhLaWkLD2gthAz%?-jd0JtLrEFMxp zvAgg+t3jVIVy+>pxV(UCJpY1F_nrZM>Zj!m&$M~a4Zx=HK~A*-+iC~xxm-Cq|G-Qt zruBna#0BoMcx`PHLM>Sr$-2H94@{~`9`Jfl+r_fl-AVb=c1!MNXhTB=rGuPGY*XA% z^;2-VZh}WRDP(ja$q#V|okT!Yo||{?g}l2e@qIY{q+DcKY#NW-jY@w3gW|vSK{`1V znMHYS%EI^&CGeggd| hbrzX4g=l2zeQnS;_2CB0*NvfC7+B|DyG|Vxl zuG*6b;hPN|(Jt!N1%0_WxMa*rl#0c>{-(n?ME@5Cf?eI7S19j({s?%LNKc285U#Ht zl8%4h6XVyrEpZ_v69`8DWu^YLd^xR%B=)OjpiK9`2NU)F=!efQMk(W@(z~MegnFNQ z)swNJk!&412hshhR~zsPm%gT+JcPi!ucPV6LqMvaEz_aY{|GhF+Aty4~ndZc|I) zZtJ}2pfhd(RK~I`Rgxoq@1vflCFEbyMcw2Qf&ZG7X1b>rNMW0_a%5%+ORj;gj~42l zRet}_hzVKo1}<2+SUvLr(qr+|qE1%GVk-yPWMNOvRF$^RoYpjG0fh_1#Z`;*2F1q; z41^&ULi*0@H$jsH*N;aL9GBzb=ajd5-hg7!H-hupa&QZTGDI1CK9QUp`4z;&ywX~d zC9ya}j1!VqM|rLG7l6}1GDbA#Bvdb;Bq2W=)!E(JBLpX~A7y`k4#N-gHNQ 0S-+ZO^-In8d9&LDbOSU2Q_E?4pL8~s*>U*7* z_pSlGu#y)?x*(#4kX!7Dj42%G-Hnpk++2`fq;U76>|(CU*n)jPuZ+9&+CZ0z7Mu0Z zD{9vnIvEgOYRzRRrNL^ap6;XI?awK?3h54hw+pR)gQCag;KFi>PlZ> QR-sDuu4Jy2v2D27AlJ4!5n|1Kb@;yO@zxYLD7Cy6?ggdak5==RfZ#QVSN zSAdKBv_sx5PH18CT5ncLf&P&1|2kH+LSC9n-0q$6k)sfDxGvNdVMT9*;l!{%+sTeb z2;Gq}T`ZtUxIM4zKzz<_#at+@MbY=!lrx0Ag(z=F2_gHp-P)js)uTLvj3LA5gGxj@ z# zM;iqvn@eWTt5mXOgz&_Nz@nAJ(N|4a%~#Req=FA>(vVZ5yVu<3yVW_o0kV(xLUCLZ zsa*H6Kjjn0whD4S0gvcNllX>gysPW)y2iVj8!;%#Yzhz|!fX~VpL?EigmvFVgd%h4 zqn%=y0&D=EhL1n}a{=JAG9_Nr-V8i1-z+}|_qyV#<#c5{Y(UbDZBZ64D2XvMq08sj zl&xmgZ*`|YD2$lbhwn$^0+(xnqP!UF+#SM4hUb0=&!+ineKF4s>SWai2p{&J7xu#( z9TYKMGy-8O9Y#kO_G>5lusWosu9TL}jDc8+v=KBYx53YtcL|vNRPrqPZIPp!1DMd* zDa56$XoDCt6sINosmiHBgWTIk^l(BWcgmDuv}BgJe_Oqz$!IM=kJ&;mi|bEFfYyV? zbx9l;hPOC`3Lp^<{XNjPu_3quCqlS(kFYpPb1t#|^<{a{XH?r6m>;Ae)As*9ZEKuf zz(G|RQU06J5mNpa=~*SC{twV|Snh*Xfb?!uAx`~&>__1Y$tChsXrrWH(&dwPITl(> z3GCcDuS0m$It%Aj^ITg0^%oMbhrtLquy|PtBhg8O0hfKpujIu)UsHPVN5b~!IDu0s znGVGM708RVR6E;Q^b!}U7&r6rkG}e!<9`dYxqQoui^>1_6!sU0O(-G@s|y8#9ARSo z|F)oX?E8O3+kcmOGS~L;X}6sz{*3znW;YTS4|N)m&HcXzieta1TpI}b2$s!d)5639 z^dWA-{kutqtJNn6+wS}ZpO=@%muCArl8lv=bx1ThpPrun^~-M^9nZZXc<}J>_t(dY z$_ib=TJbgZAGzZA_&G ^LlF^ c8l0Rk$)=GK-8e@gt8$&R#a zPqn+g+I%^NP~F*B-)C~hNZL7vv)0t_e9^g)+dE($jBKgscL5^JFkcoz97mHa9O4WW z;I8AD){(rwO3}Cw!8)L{V`SFqu@}bW5JikI<5o=1;B;C^=L2YSq~gX<)ehxgiBaOJ zp80{ 6-(s6K2mN~qz^J-`!qzNI?2@jD*oH?R4Y&U`f2K=}gE`961+bD2R%)4{Sso$W+G zI9u EIs94lVG=j%c1Es{}Vv6SxQS3 zQ}yR&so4`C+hTh;^uepi-4Cs93Z3z9CxEeVmJWk@Lp6SVuoF!)ET;D5FVD^vEty_m zirHZro%51@n1~i1@DTlD$kOvuBr?A02rrQ#S>}u5?ipsieE;{L=NQrkM~K!1q%ni# zSbmqY7G!R`V}RvlOG`MFkJZUtZI!49A#|_P(#o@g!W6s$e5{*I$lD`}MhvCQ4?&Nu z?&GDy@zeQQmM^b?aEvhnl$c)c|3vo-{IW8h{07iR@btN(PIw_Kg6R~~)6-jdv*cg@ zT>r;x`OA$OFPR`J5ayUP`fN6{Gjl0gbqk}l+@ARbr+0*bQ|3G|8k9eEMhe?#^7AW* zX9{j$kZc{{VC>rQzDS((t59pskeGeE7_sx{FzWbn6MO0E1duemTVS4?lbom(u#@jA zsLuUk&uBP`3oABOZQ`!r87&+Nt4mg)&W)z6{Gmj-h2R4sCW$vimUd-CN?2i>WmZSz zpK;vj=YJ1g@gpFI5JMv<*DYW`Tp}yXMJmEbSvg8FhwLXkR(@{o$*q^^K%LcWs$AyQ z!~ (6gQo-uR z1oaBZJ1>yA<-|BaS)N)*Lj#`TarucRjKn@3Nz6X(Fw>>DMGG3>;M~aUQSNi 6344T?0#>c9KLPLaRPi#(*J=w>x!bdTT~sB# zxw^BMu8(_m5h0t#V}oZ-Ti+A)vJSJ8C%R;W9fqqYl0T~c^oQhS-lZSI)sohkxss|- z3&qSO-PRX&R08!iheVFM*wZ8h7rDoL%pd*j5~qz~Jh_1KYIv|RD;-Y*{Ci0s({2^G z(zG#BmJ*hx-XN%Nl4J+S$)Etw!n8mZ$0O{58SY$MW8DF*9_F9gK@;pjYxT#NoGYy! zb=ld66VoXVi3ZYwai7rDdcuiUh67-?fX5(I9D@}?Z@X?`JPDhYzn))RLFcL{oHmlb zZNLGZu~n3dO6n b7jPtgbpF^4nJp0W^=%PX)jzUiVPuYq|7|fzoZR%QNG*TM zvFVE(4nqp>>;{$}EQ`#TwN4$=%6V*@5C}A-Ca9+qeO((>@S^tqqG_IW|BT{5h?hJN zn{>XCkxj5nG1A>%(#P<;-pB}V_&w2Q%| zT_hoKaikvGvr $s9&_=Wo{5Ty6+$!0Rz}t4CYbvqhKhOS& z^X?cdMEr( (a6Y+rZQxiYmS_S4og^j;hao?lWJmm z$Q{hRT@^liwTjZk?JyBtC>W%< Pj@@z5(aOG#QI`eB(SQc7+w%4}5iI&7O zve2~cs|EU053qJjEiKo$W(R_xdFi+R~SYUg+W%0U2=PJCQ5tMo^# zsJF-Z_{(9&(wfOBb&lNbuH)IUEbh<%nD#Q|Y*0?s-$Ci&LL>Hoi=^otiPPa~FOta~ zK&DAY^{!7@_@h!rt%g}ILe%0q1i)xylDsVH0;{#NDAyned0}!pZj*wpS6Zr!>5vhn znqP2n1~|zao53Z%y9)^6wycYLZhAi{?S5;S7fp&K)*5Hx{(RfYV!g_qV~D7GjKzP| z)H71vVMPE|^m6))!B1C|U}Rp>=DwZc4WK6+o)!E0UH^%@FE=5q(r=@m?S=iubc|V@ zpsFZg#kPhH WlcdjN3G~>pQsiApMq@K;m@x~sPsU;fV?So~r46+0 zeiXDmxsGisKHc?IRXJ^k8qsI@XLWD4me0hJU{Eq@MPaJP+^@Zvj+KBhlavrpTzG3f z4#!Gt=-7^R6BqI}9A`T2(kFF`q7IAKrv;AUlgO;T_=O%yd};V4G4FN1y%;i@sXs_O zy6AO4eLS%ggl5jWF)}DB?0oJWkNv6>Gc_S{l7)n(rhhg2Ia-R5K8jC4`-a{jn5&EC zEitSjAX8bJ=E-II;0 rF8?|^MtL|srvP02^h0KP)wC=l zgZV!p`O0upytqoy=Q?NTkpR 6h}DDU!U6Ery@7_#cx$;)>uoVyWQzi%O=?2 z4QI&DR3!4plxnr{NElkjh$v&$XE1weE9?aRkTXq-P4s;@@gZA1cVBr4_B=ieAxyS4 zMPmf()_ bbcT_j|sKOu#gB!4aDrRUXgQmHH6jaWRJ%SmMrJ6^+RmXu%oPX9wtaO zd})~~^2UZhgnjK8py_V;{Gnrz;vk-6trQUnlW4Iry!Eb`3=bD62sYCQx>cTJN~#($ ziBLa9ZEO#{_u=+n^{j^f=(oc_Z0)8_5EgwC@Fwf&_C=lz1M^&h_LpBpzCzvcq(Ju4 zJU)N`f7N7A4L5(U^M?K-*|GJPi8)GH!m!Rc>5AUw Uei;^^(%kTtm#H!-g3L_y?GLA;+wu&lxJmjnR%?RKeq^yhz)t =!}&oGa2JVK6h=HumS7m38{uoU7q;j(PAGBpGDP6lSK) =$qgEeJWa^Jff8!fCG6 zS3af8l87)GsKDwzlg(3xyEs07+Ca+2*$!LBlB^25M$Bs`jiD52ySeBj@zwU(!kH;< z%R~)M;s>v`w$`~<3tPxUT6&Q!g8cmaI|YuMh@{UE(=|D}(cfS44pUCtve8+(#s)O8 zrhFaMt!$#)Oy{L5;ztdaYv0f7Yt=9-}}8R?4MN9^Tx*Z7@5IcH0wH;BSOakii?R zp+s`))Fs1ONRNcWU~h3RsoM%^)+BAW!@SVBJE~DbpV%v9nR|$1a6*?J!n1C6BxZZ~ zVmX*tDdBkVrhA+Idty}xZZCze&lm?6T^u*kl}!t){UJMZubyM6?@V$t6L2^FU5TLk zuJA2RT8uu?Y=a47^lh5a27HH*dsV%Y{_{eJux{?1kWn$)Hi9II93lgKYY(~tK6kio zwoCnsjIqQ-LYd|&z4_nhxq)ZY)1F_vc1*8-rH~#%zBm^N?IoEVIvP=HF%^IQtWGj= z% =LqPyugu>9h;z-Udkb#?GZ}M1B)z0?l-o{U5>&gmp$R4 zl?vJtkC_`=$zUu;#i0&Gtu(#m%}t29X;TZx0luq$G$SKAHkkmBEqjI |(sSac_Xo45XYdOu(8QYV1yKQPa=3u#bG zdssXj&c|TQbB&xuUm|ZG6_Hzep45s5Ft?p!i77{?q!ma#OvX>wV96=7!{|rXAhc@K z9DlK=SZsgQ--UTEYOf &|CT87=(BemTWU_8HrJt_H@&?a)28 z1l(a28Kh~I;UXQ2+}3moJ9aTF;u3M
a%Ge-^CP@v!f%NBz{L_nyQ9 zuefHdd~D_S^BK4msSFeW_=#$EufStL*sE7sad$YYe^_F2&tqkg7oT*QF?znYphfp< z!av8I*aIz;@!>9ow04;exvs6h@5{?Ww6(ToPBm;yo2?*X{?QfdDFP-J^TQW5=9rH^ zo(Js0)$y7A+*CBnG6!Hk@;!;v6c72<>)H0Siu>G=nee%N^Wk|5<2W!jf%>wPk DN@%6o3nYuJk#^;omT5EQt)j5uoV7$A7-MgqXbm}gk_x>}ByUW)v ziC4yXDvR^AAfYrYOrsGW@9~p1%q4EtH?!2Q;LN!fY Y>`7v?G9b#V{c^)GV&8D8*z$*GEfi{8@61ahJ3MEA`;AWR# z=S1<|VLea7`r%oP)atvQt?4WH6kb+whZ=4Y8Hyz#!u6>-0H&;rF_H{sKm!Yd76BRO zqw|eU*IN9Lk_L(H^+37GA8V7(g}JiKH;92O&NrzVeG-mVft;NFEnIkS!iL^r#S-F9 z=`6Lbt&eF?Us(D@au?248d$f>@!UE(@n xV52WRN0_^1px8(i{XB5x87LCfDRh8r{^$4`< z{eXTt(A*A$;v3tA?-e2nYA*B2lonmU3^dI+;#d2!1^QApRUbRpimr6$jQfX9zlH+Os& z (#xsz3{60xt}NM-bWg0TUQ-IQMWAC$2R})ozQ@B`*Xlg zqXZa700%(bIeo<$+BIfS!9*;X+OYZ+PeX^HP3dMpKcdj@=WsGRgH&NTq6&_!jz?7F z39Zp0! e&wZ)m>^RZl_IO1xBdh zZau!^+Yd@-ETb~zvM*Fx1Rge$;6S{*ca3*p8^H4QuRZ}Mb6+Oh_OiiY%FCE{Zy*of zY<#@Wf3opVGep{13m#$NZ!5;u#@{3YBN)F!9j;Oq=6VtpXh3h^8WEuGl?MjQrsh`V zD?W|jAQm9PJJ@9wNrADbyWc~9vUD_Fe>hP(JHRL4rSuo!ca2b`blF=eb;&!mFtTzb z{)`reY*u4ou=!)i1uHfVk4c>cKJk9j^=;B~5mWqZA%)Fg-1HeT9_M=1$DT>zcR$f( zJ+CeXJK8 RE?N<~IqWQT0!K zy)m}{=1C0cQGx!Rk%a|WVXvBvkS<&J0F)q3J&sD=g}XEMfU7MvXPIPdQj%RPA=lWm zg9_me0{+2hqjUIsA^rgey`$bG=GiUH;~GXDlpN2#H jST19kQ^~DNeR&E0+cKt$ z1r%_A7VoqjXUa6lwZc}rhW}X;i}x{Zue*zrb=|Lpt3n5cTwO*GaRbWnn&joa2vl?2 zcZcfMdorVhWw5dLb>xq25T8WIX 5{1Kn1EH%KKjt;0jwun$%rwC$)w= zttd#&=<~& 7uKxrt>taAwv>og9&_8?~%Vw`2b)pcVBF&b$Ja)pbOFTs8I0 z$f!txm9vNcD@cZRbImfbT8H#=3iZH)Qjfxo4@hPcvxVmR2U%&`Ya+nQLhaYAcuuf0 zCGEav1HP-POUj9kj?U1?D1}&EMWw5U8rz>f>qVADa?p5oc6Mrc?_c?6946}X)6Jdf z@tjYBWxmkm-s8LAtlYGZcYp3FdT2epSUV|lY8tfW9x#EcI2prtao^_CTBgkl*)shA z>-sc|F+MmrI5wvADg>RES|t_?Gr?W8gj2v_vAp_hL98Q<0=Iy>Pt5XH+m=T!FE1@Z z`b|`X^He0XI1_3gs~m9_>UX?tLz;%pU1Ek-&b?im?a)vDLy@-GandG(d79!sdFhm? zW4Q$)`ioMlaHOLvtbKlwWBPCU+}>{Kba38PE%?CF40@=rV0+}cAFC~Jtq^xnzw>gx zo+e!KBL{4v#I+WIOZHQJy?_RDsez?6TaM67!> TU<|r@A=Cd6&?QX)F^N?P3BZ!Pu{CvlLYEYa_w`RxMazc-e_Wgoc*Oh|FH#k zd*xTP`TzQ-Zem2eV^fbsSN|cKP1mai;@ov6=H^`kA*<`lX8w8T(G+me<`wIo3MZX@ zNPp$uMHZVMr*4=wM-(=|@gvm?)D3lj&8LYOelLWB;YFs+w#RpxcE0$30NmAI-EYR& zl0so`VcrC;DZi>%qbY21wzOv;eL4}OIa`GF(9kEYyH5@F(6kov3 K`EDWYqxPJV+cS5?ByXR>pV%=k}$pL2`g`Y5PdSCPv*+@Z8q* zgTtt)qQKjVWk50T>dc4HeW|`UqWuw{;Qe16fQSFPk{Pt}3QG{82MALaHm`xdD9f-W z*%>E~n1zh!C^mWxHfX$D4(@lsI&?h9IhK0z oqwY+_WiYd18sg7mAWaf zXLGb30vQ>yW?DQ}aYxrsJ^)`{8`ui3Sq})ECqu|mdOaH4s~#~G(?f1=4wjEGuffju zwDu*F#<8M*a1jBxzlixNMFdNwm;dnZy}xcZ2dgiSs?n)!UXjdBi!Bb<5l)V+LT~Tx zJiJPYNET+7g$`2qM}!o%6G=Z7SwGJ1+Ky~V=F_R=EH*9eSY_05HS($pqyu(-mYmG& z&CX(8Hs2AvM_Agvj!F@kABI)9ADtcE_#2iIg8K`M`WwlB`l{n#H)t9mpwIB3Tp5Rc zO1Nha|3I< U`(+?~Z%?h+#F}n8CR)Ct>haVBlS`dKxazpc(D|_7p z2ur4Fjt9kAi;DMlaK5ghAKr~|$SY&NfNR8qHFpS|936qe_m8x>k We=j*r_y66{t;TFR(WNPX-iO~{O -tEhKefom=pKFtjP5G*_5e-G?@tvPhHB7*1sSw}j=R>}CePF_MjgJp ztCj^jq&2KI4u9W`hoZ=2y?_gSKz$1*(h>moXIy3WLbcXvN`i)6Kv!R_TrH>_Ita@f zHdyYtiW5_gN7oFKTk7hKwCYU?oZM{>LYHlmc3677acWBB`707#mjv5V |gP zVlhu4jK7DX_peE~yJpeHK8*y&ylL)zt$E!-C*KcLOm4k)o~{^(1K#D$&37|p+Lm?6 z0=qv%4f?~0{rvovL*1pq-=O*X0|H*ct$uU?B4ub}3$`F?FJC)cS#L0ov zAqFyu+a{?CmQxY92rnkSZ1GO<-7(~MwGzA|U!bm6D6X*~3EC;2S~w-M1{bC=T!@Dl zn$&Lm>Av|h3@({$z_k;k0yKP+F7LS6`>9p3$OZ^s8cmFJfIQh%cPzPXSthO3m7n_E zJ`3DO#Pk2e5LkT81s*0HYPgKof5iY~ApJiyopnG{Z`l4(k&qOmOGQAWM7l*(L{gD1 z0b#%xHM)@!Nkwv`l;nWH$bq!Vg09gqVsyiZ-@fnr{`UX* b{YL_3u)w?`C9f=0lKUD%BvoK-HgwUXEiDx`Kh=t;#JXRMxDA+ ^OxappF vEt?RZH_TMn&;Qq1w zY2nqjiwE5nUefO3Xqp3|FIbT;F0TY7|5MFEkN*+hg!WZeaUQWYMvi5*Rs@205bRRV z+2aML`7QJnM@NkZ%V!hK6o;kOJ zDSBV;T3L;V?7wVBy zpR+0)WKSE#B4bovF$(Tmt875Y@xr9Mn^-WcE$p?g#MJJr7s<@=Z~A7tQ(1O5PWxkb z?;}Au=tk`>;bGq;GSA-1bze(M|6}|sEkd3|g1C!S$dMBpW4%g_?1aI{A5I#z^T4h~ zS4H9qx&Tk>^1z#m9e@xs;x+aa-L27^VzQg6Hod-ViHUpZtniP%3Ee+t{Y?^>0-TR< z;1mmBP#aFfSY;!<9=BIi%Yl^OV!r+YQFA|>yRK^XPnjt0O~opr&z{FMp!%D55zYYj zhAufZtr$hZVE^B{5 rPDvCJl`8;No>NdQt_f>Z12E$0H{hAwLlcs2}`qMR`d- z*lc*yzS53qPu2$K(rsD`GFg;G7w8$g%0QA*N2MBU?) E6LPg}B#I5B;y)=`BN&WxFVEK|PefUwpc{VO=|k+?L# 2_cj#wA~y96O|CWoZNP$>*#!{pGJg$!8=QRi!qq3wChF?{h>L4}Y>rgJTuFTCmf% zmh7_eD|V&f_PM2k-(+?mg26HgkNlMy{HNSV4{=NLb~>pv>M_a6j5=e^oVvmdF(^~C z`08FdVB<>fPeubDvgegzG_-97)Dmt}>f 4UTRx$wtxZ{n zXYf~kei&>zI175Ij0f>F3~IB`T3J~cF~);q@UM}cSS#X-0xU-J}4eKgrbP|#MV!M3sjt_rE#LSgGS-M1oI z_Yn;MfVE>kyiFO7mK3d4F!YY8eqZX5e7EJCTT*9lH(+4>cn?PV-!`2mu8CLzN>eQ& z2Ku{=Z=f1RR3%~?yNFfIWN4&N>XD(d$7SfHNtXE9!9#BiAdCm)2o)5UT=_G-JksB6 z=&Bu0*+4yIegLclO2;2Qw3NpS>!YCJ%5@_bnZR6;2Cr1kR5sUOZ$-3+<$;yL5?Gvn zUlf>&IHc|C_{$3R-t1Xgm-`E??;O8)T%0(zs)@vOCMjUYl_e5?EsZH3#EqhQ0U$qN zB7HCK;2nm}>iTXEbdrsEnt0%~Jf>`x7T`BIq5$5v=|ugYiBXhshjWW-Zo#a9sXr!t zC6OhiX*m#WuoHp{n0tgf%u p4{J(al9_s5 W|DT4FidJ32;~Cyr z#))57jQ4_$Zdxs}XtfIF)YDEC2 3IN0MBAHr)p72hfEUwJ z&;c`LSrZN9jIRaCyREy{S|$;{SkrjWblvM;9TeRpN>@@&UMuWY*`$mX$pyM}js^OH z63Vh&CQsk?9ytr(ov$8vPsVh)$YB-^Yv%lHe`q^P*ci#df<(8E>pQmk270Asxf5Jp z_0o{+CjC-8|Jwrjd#EDDjtg-)7)oIwLl85@H66@LTC4HN@#;d^m!;()P(dh?jq@!D ziD`-5jk@ C3*!2WX}0o)&IKudT!ZOqju`B}wobgd^e20|L3@es zvS=S!hh!dVo8~n9?l=3m=~T%(EktONV@*Y)&ue!FwJ+>vRqV)qe9aAhc|~(#g5Sc+ zzg9c=|Gj`bRMV3Z{Fg59YGB>r%Eyp}0X)76?~)=e2Yxd*D3-9pYr~UzAbEIH0 Ux$>9qROMpI?ReR?gA!T;g9U!K9u94#0Suf4LeCce-2>d~6dUer*%H zcsNgs66A7S7#OO?V@0e4vlCYTI^H00%LNk0&kxonU6N2rX+X}}j{v)R+VZTGe$g&R z;PdrH?Y;p25)Of?hMkL<_p4^$>uC$;$ t{45Q`2^h$qfysR^JduZ4j#+g8d zflE(%#Y;TUsd~TmNTY$1vE|W}3s}0z^FX|AR=UN-ojU$KF|T1vKP^|*SBFbli2n<{ z^>Q97(Z;dVd`4(G{W;L-t=NyUhta*i?j)v=({SB04PJbyy*%K1J!HH3c7>fkotc>X zVCZN 6<#tvP}lrd^8IR$fTH&jvb-%Jc$21Uf?+?e zdW2U7qRinK5f+hL C5Ik0~sd=eX;fp3Gyr6 zJH89+e?mjm$&Ps2vn~u&%f)zR;n!*M{87qq5lHr3MNfB+o+>-M?udI1!@(=3_b{WX zB`Uc~nM}V0GztaL&W*2|4enJRG0TCfan&_3WVt2874bx#-IYrj9D}~mj%+JN>V4BL zLvzov4_Lo`e-cV79!~f^Tp~JFI-0icy`x};;XP6btdGwgcKieBg6>@4pGe0_T`+v| z71jD`(QzzxveSLg$PsfS)ENjoBedw|vRSC9AucY2n_x5QIcj^bVJC;B9Z;&;$MruQ zis9wj+G&|_bIF}LHQ@x6A>LD5NUGgtTRy+Em4H`I4Cvav{rrJ$YpQwh{7eXQI_6H! zf_#Ksl#i@Dv5X+hh;4_w=Gb*`|4#s-Yb?u)D7<<}jfyXOStV{Z{%HJ~!e!sI!L!)R zZL)()YDO<)i#c>`5Z{;H5>(W9)Z(pi{E_Hb(?oTV&ZOw`u@>#}tzrd{K)6f)ag`ba zC}3Q%@AM;gcbl}t8?)Yje~2__;|~@(ZsDUHsn*>Y7Uge{RM1ry6@+q+qxq>2LRaq- z^w0jkArFRo{J&&VefWsB>PfY&9|S34 t2-KU^wRSDv(&JhR!fKyUpm VE7x}NsXN0XKTm7-Sr9d zBSqUC=fx{M |C{|Dl$8SAUfP?94XX(rRkt(-qD8pry4CX~QIy ziv-->%XhY~eF`4iJMLDmzW&uNM!`zhWL)67>3^upJ)QDU|9c5Dr+jdI)%Mg01TF!H zw5A%Tpd7*rf_o5GVHdr`rhWOKgroAl=Qkd#uurxF{;1ocd-EY~)U3-9(Zw%9WllWm znKy{n5coNw+U*`ztygwsRe*U&TjpHKS2rM+Z`t9$!S!}VP_aY*oY*_wIWxBuK#SH5 z7{wM^_N>o#0@~7t(mo>Cr>^?0qG8ozVqi79ZIAK8grWiNeP@Dn_zjZKt?HVEkCFWL zo4bO%U7r)oso5ok`M->qEBe3SR`&eF)4tD0(3v=#qj$rlY6>}#MU&nAZ_%+PK@k}i zmmz7XyRKo-P`}aD9u{8-N TgnVzby}Jk2bx^a *&_3 ztGB<;t}9gTa_jNYaSW==N4G=I79ApcC3}s@D_8a+6)`QB`xTV6CF)(p^wWBqz(zo2 z%<(hB&m06-*5Jw7(i9vgWB(D|(Muz-)r&Hm6UNVZ&~+AGxm IF%HOb0 zTW;fOv$}+8t5_OA5;+5eKNz%zR^gzL Tklfv(PzrJQV#0XlG=DWK790w#Si^8lg0$5Hh=+3BmE{@ zF468hnB&%^CWpliSj!f@nnFy8#k@ +#kv9c=a^ctNx5N-Z=+HEoGdSd }xd#3dF*cchv)>ls-6YWg?3mCZ>_8Wu2cxvd=)z#gcdO25^?{(9P$1>Tp@@R40 z%krl3pk(bjEN#KmtvD@N9@n<64I|&+vyzbG-TCJGcNcGDrK~Y5ovbB=9!r!?Y0tW% zoGxZSZBi(_9xvp}V%gd3fR>;05qN|WW+i8-g5{Smm+6=riLKSnD_{#(OHmOz3Mxtp z`X^6xvgmwm+zqcot{oRbf(;<=o$YxteWPeQqb>)fV07I B;pqvXX=f&o1j@Z< zmL`L3w9QR2&$LSGa;hY_)V!h8CDuVl>mtCp>%C%k^EX30*B6fi9{a0YkCn}Nmo^6$ zDduUuc}`w-b-qW%m2 zUqqVe{|kspqWDjZ3|p) zek?5e{E^-dmK-|P3*1JdHrvkCYU|X6bkx5I+bK`Oy88vhY9UtlSdX6@DDCe(>NW22 z6wq|Q4oG{|SCwp2f~`Zk&Ask&wBKmu`EoFT?GYboxKA QxuVNyiAqpuL7t*uSuB!4Vm%pkn<=j2{HI!~x8WiER1um0`tvwQJeXWl8w zm3{UFmGpv_7no3mZGiGW*4ktlv#1blcdPoq2mM8vn 0Fui z;Mz7A=L`o5x5qbC6q7-|*<#e(hnsk)B6j^ElWs0#wRS2P(wuq{3ct4d*;HUH>>#~7 zgIl!MS9jJgg4NGX4y)w@hsq5UHODYl9z7Syf<^q5e**L``%4#}9Q&0eu3D66T%L(Z z^oBUf3TCK@2hWz5cD%|BI&Ua48pVY>Oxzp oii1yO@z2?k>qGWbB-)=+GkSl` z-ED{QN8Ag!=ul@%8-=%*l_A@k7EWYHe1F$jEWnnYMT=MU1~4co%zCqagrUXnd9zzW z!a+tc@L@skuIaw+bNc|pM(RL_>)e7pB*9NAcD$@fx@Gz5ZIA!3RVkaAr9k;4oeos( zmw;$O>!q@Sa-#)-wq+U8hsp)vEBxR_yq<3|P*{mMxKIN82La44pP0{eY!brqHs7qy zgeVK>6utM$QkJ+_0$j@Np9;i|jXrHZrTpAX{;f0CW7I9aHR`l4!q2peg5+cO#V#cc zjfjvCFqy>k&$x`Emow;VaZTAFAuFqs!)bA41$8|Aan@g=^~VTRGxggbdp(w8l=y5o zvjQaDf5+c%QkdzsX!h*NMULhw!E@as|L{xaUt!x(3GtC*hG8q4sj)M-6QC7wyO|%) zGee>9>X;gTk6|o^GK)!)iGIvS?-bg9jVgq8?v@N&vNH2aQg2*$^J*z`DKAFK 6xn-u9Ye7F>tcM|TzAA=a|UvM;k9+ubbsm01bEZX zuCr@xd}VU7em>r)YTeV kZoDzXzoFeM)BE?n`E)`J=gnb&OA+jhw}HQzsnxHQL`*DF2jKsb-z3;2;(RTJ z o%vqAZW$J^)Xw%j6D zDt09{Q}X?(m 3y5A7Z_~Eg17H$9A zZI7I}Mr+7-Z|QHtM~z2bZ3(SsfjT52)D-x;hCZKqgw%6_1(qgfZ$xC7T>0a|-b)>@ zuFM6uU6^(K3JqHy*=6iE;zl?IzQ!p%>Xl}lji65iTF}H4&cJr(9nzh0QKQ4t*ur|> z59A2(Q2U*y(Lc?6F9+KZ!IDJx0 tF#2@@mC8g& #ZAIt*_c!{08N&IcCxDc|#TRY{DPT=dcl(*tXLE_c!~TY67Vf z; l(Z&PQAbvXq7|6Ty>)5`@ye nLTGv`E1nX9ZK0FH{VGPamZoWPK2%VUHrV0 +S*a@TYsXVH62{UdNeXeM*M#TNBve)F7N(o1BZR zfZy0m^~_V^PNc7v%clJRxm4yn?g7Va!p;bP*72MNHMLfDoSRnxdZi!kj*I1Hq3fx8 zF&h8*ogu7nv(jPf|2_w+ss=CHM4F3=8AQ`k_4VAokr!WIeOvt&$QhdNIwbk)XjG;3 zdKD@e6cD|Z`Qm7<>ayMSFA5ylnISaG%ITc>8#Y4GP+?w_4c;3vyv8?u?d%JnX}7#+ z>s%2Aj~;NnVHF{LVMW~hD*x$BQjbaqmi9RO(cJvCpe1pZ8ZUF$)Lucz)(Z;8{$s)h z+j)!R?MSpk_C2pdt!T?d2=>XU0e)1nOask0?zszd=MXv2v4fjaZai1+&hNq9J8MH9 ze7gJ}`_phsRAJLKj=Y%qe6EeTj@RZ(L(u0+;K$w-!6EAIT#2NIY?p*N XoK6QEq!K(hM z^ut1&+43ZLDW=TN4}t=3zl09|a;#j?B_OD|@=$|WOZ_s4{G`;0>wky$=Ou9~dLwN| z9h7wTGht-_QP;NTYfDfUNyDqL>*h;LJJ!Xbe6DSB_irm*#7Op;JRUh76`3`sgDm}( z)-ccyfyx|(I4gVcrLNkX#7%S4Xd)y|(x;Wh{#nh{K(@!2x)A49KCRb7v zW-^R@j(^W#xSw*7Ld+-JKHY4l4@>m(IN-9I2J}tO{d6vm-xSI-`(Q *$ z15#+8<#So-jf?F(nXZ%;u2i^$zKXt)Z@Zy$=Hw@q`!U@~{L?zskIp6@KbCr S&ZtLC$> z3o@O h zmYbuC0jE^#qG KOGAEh|0k2y|KBvdPPk`LU-#roicWoKmXf;>WdP*e)MH3 zQS*o-Y98Jx6kN?p;)>_dwkn*zwxw Ha)fS%sTdd=;PvgX|}p#J}n!3&&8_9;r{**L{zKb zq?)`gCpp(&rZltl@(s8Z!XBrfJvHx8{i^$Cl_Jl(+v|1PA z>D_!DtY7$!?Wy*g@t?x8-&OG+%J3;E7BAGe$W2_H-TLxj6CE+eBdOSoJyFe=_U@ z^lFv|YdUBi-K~)mtoU2&{@?)TO)dr%wX|$g%t4#q-B~cXK{7x)liUetX`OJ18?Iva zj)9f0O?nqe-G@^`o4f{)x?UmeKfZOfcrr7Zy~zJ~G4W05Vaw&ofDe0os_Y0xo|8J7 z5Or5u8wKC`JZ4J^UBROT`J@A%UE)E#Fu$cRze~aSgOKE`5rL|-C3|w^F)_u8{*G(h zsE|R{daB4lBlc18qbYw&x!wU<5!s4zWQ>Fl r{bYNi+&0!qi;Ic-4~xfi0^?x&6@HPo9q0j3 zrII{ChylRtB6Uozu5Asu$w~`{!}oi0(DGaaY`CC- s4IJru< 5oNIW1CNS {G8R9C3l$ z>nm{)`@#FeUBsb#(ZiU42+D^(tDL`n%+-ml!*xeNMiAExqSrRx@vaBvaSqC_C+My< zvOol=rlz0-k(Yn`$8Kk<%Hl (Q&ILw{m@`@&ilH9%#{{{clgdDn3X0Wu+xHm+ee4DcaT|CEDJv`mwokT=%V? z21c`UHLx~BjUN~69U)fW4}7HO_;b7nqh`%+yS~cWM+oT7gVe8<{b0ZjTaF^8-}nAu zt8^*pNkkKRe8iw@HxQ9tJ$X8fs+7;-sj+X+Lbf+c5*>(4>;Lt;Ehuw~TR0B9f4F5Q zn1(x^Yi#kIhGtt+!lLE-J@g;CZZ%mChumt 7kAYp(w4s;FjmwMvM4AAYeyziC%FXT(h9px(QI{O)m|bZkxm>HCE~b&2!? z4oJ{d9yft4?;Tvov^T@ObgqHGm$Pw(7o^XLOBT^!4p1*iU*8r=<^N~}MP~^0w>v-3 z9RXYiFQ00xN2`S>Nbr@xa(&c~+s$5CRZP71?Z;|bWD+{an~EeIJ7U|t6OvgsV(Q&L z&wijnN{k>)TR_GHueF^S@%imr4UAu!(y^wA$aVG2cSMbT+_oPqP&~&|fyoB=o9nDJ z$^{_=d{dPcidV-F&sHjNE9cCII^sMu4Gm2^a{W8XIfO(w0oq_eOX6nLsIU&}vOrhj zhzG*#X97Vuq?8e-89D2 40 z2BiA}e=Ac*t^aT7IGBtcPYGRZ^|5AmG+o%`|GM?p6&AhSel+b+z;pXQv}!&k)!;ct ztPPF$Jq!1Za<72ks>ato?7LVb$kp4x>TIu%=OplOsTtzAd^(P!rqevbuL?$)BqGl@ zVdVJ8tQz}N;~@ lS-f~W zW9!`)wx)fV2P*H}0{( qPbm~MVw zUbq*^pYUNy8VI9Y7B0;NRcQ;4miiBm9gYd6njr#S(dn}Ht~Q)@hQ3n@i1^w=XqWro zz7^#KIP|{V06+AvG}O#PIn-;X-n1BgsINecXGJt^Kd1n%))a&c=ZC8}KB$rP-S`2> z5W74(=<$yhqR(vpYWAc+JF?%q2i^VttF^LG IaXYs9e#zIDs zN8?K$H0(cY>h~>??HeF;UZk}e;L>kR7@e6k^5=#QfIfOLl8{|`oZ(w@Ei }L* zZeXc7p1$9$3~G%QZhx|`l^rOeQGO=9u>K{Vye?t*mg#B-3nuV><7HkYQ+L#>?hC4@ zG(VB1G~elR;SKF<=;VwIFh+Pr4%rvR>Jm_Av81jkMr&*2pyTQQ!EQZ`Z3vV+^oIva zUh~^GYZLL@dg(H6XVAH`MV`+)dJu~!qh@bO1LPCqHCwFSRso=kJS;T#nTM~rGaf2= zkA7gG=`ZFgJ>=Q-sbIf-jF);ArzBSu{Xi &XEO6b7|^_>a47oU31a7;y|`pP{ac3)xM7k{z$4g&b}32XDdB |Lx!;1|n2)EZ=lf1w!T ztx>mRI%K(vY~NhpI#*Wk?!axfM_*h_Tg1F8a{B#02HOZ2c;@B11aMRMj7rt}(nU1P z3=eGEs2KiYQtDhSTvO^QJx!#6ALwip98Cw|$fo>t^j7h!T?pVKX7WVt6wb$Y?l)}h zb%!x%Oe9zYq#P0Z6g*ZuEg>wG*9rHlfwz!hD**w10kE_WHJo!%QLk+X9-Zys*M*cz z4uiYU6qjUf39@kVttMMK`F2Lc4@4(+ZleW19z<_D5ueikW(s;M98CHu zn=u=wa;R{$lW!H9!G9yYygf3Q#lbUK0p4>2Z{9r#=Kt8CowWa9hYT(6Ve*$C$=9;B z+={6Qz5e~>AcQuZ15o$VP>%8Yk2dFpGSSPu@>cyIaM|{|bLVw}GZ-Y_@BY+9+f~VM z>+!dSZCH~6T#%3Zj|gXjL8eoc0MJ$77qGO6twO`wo?+a {MRD_1=#r($cL#jh4|?GFbPgIoLQ)|8_6O(dsS*rrV%2JIU#iH&YmclJ|CTSP zbI~?GdV$Vi1x=Eeb 8ppTitT7}N+q^I$D@`B<%##k)bazkzP23!RacBZ6$ z)Uo04SDgp#GbgD0uU|Q$e3og%*5WSnwx-lLbd8B?>H!l4s^*!v)kQ}7^%YVU<2H9X z)a8plr~t+s(F7rm{zbq68I~Bw>z}R~1ptihI5Q2AB@6up{I~Lwll##l5*OkKN*a-2 zh84dK2-u@#p7}lwd{gd8OCPtv*lP3sUemRS&=>7jT~X9i(*$GJ(r_05mq45^ys1n| zx6U5wZv*LVp3BQ|Q)l-8Aox