From a61abaa360670b075cf235ca5bae20903ce8fd56 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 01:12:56 +0000 Subject: [PATCH] chore(third-party-sync): update jlc2kicad-lib to master@8df3ad194107 --- .../.github/ISSUE_TEMPLATE/bug_report.md | 24 + .../.github/ISSUE_TEMPLATE/feature_request.md | 19 + .../JLC2KiCadLib/JLC2KiCadLib.py | 217 ++++++ .../JLC2KiCad_lib/JLC2KiCadLib/__init__.py | 0 .../JLC2KiCadLib/footprint/__init__.py | 0 .../JLC2KiCadLib/footprint/footprint.py | 181 +++++ .../footprint/footprint_handlers.py | 617 ++++++++++++++++++ .../JLC2KiCadLib/footprint/model3d.py | 255 ++++++++ .../JLC2KiCad_lib/JLC2KiCadLib/helper.py | 29 + .../JLC2KiCadLib/symbol/__init__.py | 0 .../JLC2KiCadLib/symbol/symbol.py | 234 +++++++ .../JLC2KiCadLib/symbol/symbol_handlers.py | 469 +++++++++++++ .../TousstNicolas/JLC2KiCad_lib/LICENSE | 9 + .../TousstNicolas/JLC2KiCad_lib/README.md | 128 ++++ .../JLC2KiCad_lib/images/JLC_3Dmodel.png | Bin 0 -> 103353 bytes .../JLC2KiCad_lib/images/JLC_Footprint_1.png | Bin 0 -> 95126 bytes .../JLC2KiCad_lib/images/JLC_Symbol_1.png | Bin 0 -> 228112 bytes .../JLC2KiCad_lib/images/KiCad_3Dmodel.png | Bin 0 -> 150050 bytes .../images/KiCad_Footprint_1.png | Bin 0 -> 133369 bytes .../JLC2KiCad_lib/images/KiCad_Symbol_1.png | Bin 0 -> 14271 bytes .../JLC2KiCad_lib/pyproject.toml | 46 ++ tools/third_party_sync/repos.lock.json | 10 +- 22 files changed, 2237 insertions(+), 1 deletion(-) create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/JLC2KiCadLib.py create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/__init__.py create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/__init__.py create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/footprint.py create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/footprint_handlers.py create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/model3d.py create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/helper.py create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/symbol/__init__.py create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/symbol/symbol.py create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/symbol/symbol_handlers.py create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/LICENSE create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/README.md create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/images/JLC_3Dmodel.png create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/images/JLC_Footprint_1.png create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/images/JLC_Symbol_1.png create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/images/KiCad_3Dmodel.png create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/images/KiCad_Footprint_1.png create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/images/KiCad_Symbol_1.png create mode 100644 third_party/TousstNicolas/JLC2KiCad_lib/pyproject.toml diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/.github/ISSUE_TEMPLATE/bug_report.md b/third_party/TousstNicolas/JLC2KiCad_lib/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..55cf6977 --- /dev/null +++ b/third_party/TousstNicolas/JLC2KiCad_lib/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help me improve +title: '' +labels: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. LCSC part # that caused the issue +2. Arguments used for the execution + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/.github/ISSUE_TEMPLATE/feature_request.md b/third_party/TousstNicolas/JLC2KiCad_lib/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..a27874f1 --- /dev/null +++ b/third_party/TousstNicolas/JLC2KiCad_lib/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/JLC2KiCadLib.py b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/JLC2KiCadLib.py new file mode 100644 index 00000000..a747ac43 --- /dev/null +++ b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/JLC2KiCadLib.py @@ -0,0 +1,217 @@ +import argparse +import json +import logging +from importlib.metadata import version as pkg_version + +import requests + +from . import helper + +__version__ = pkg_version("JLC2KiCadLib") + +from .footprint.footprint import create_footprint, get_footprint_info +from .symbol.symbol import create_symbol + + +def add_component(component_id, args): + logging.info(f"creating library for component {component_id}") + data = json.loads( + requests.get( + f"https://easyeda.com/api/products/{component_id}/svgs", + headers={"User-Agent": helper.get_user_agent()}, + ).content.decode() + ) + + if not data["success"]: + logging.error( + f"failed to get component uuid for {component_id}\n" + "The component # is probably wrong. Check a possible typo and that the " + "component exists on easyEDA" + ) + return () + + footprint_component_uuid = data["result"][-1]["component_uuid"] + symbol_component_uuid = [i["component_uuid"] for i in data["result"][:-1]] + + if args.footprint_creation: + footprint_name, datasheet_link = create_footprint( + footprint_component_uuid=footprint_component_uuid, + component_id=component_id, + footprint_lib=args.footprint_lib, + output_dir=args.output_dir, + model_base_variable=args.model_base_variable, + model_dir=args.model_dir, + skip_existing=args.skip_existing, + models=args.models, + ) + else: + _, datasheet_link, _, _ = get_footprint_info(footprint_component_uuid) + footprint_name = "" + + if args.symbol_creation: + create_symbol( + symbol_component_uuid=symbol_component_uuid, + footprint_name=footprint_name.replace( + ".pretty", "" + ), # see https://github.com/TousstNicolas/JLC2KiCad_lib/issues/47 + datasheet_link=datasheet_link, + library_name=args.symbol_lib, + symbol_path=args.symbol_lib_dir, + output_dir=args.output_dir, + component_id=component_id, + skip_existing=args.skip_existing, + ) + + +def main(): + parser = argparse.ArgumentParser( + description=( + "take a JLCPCB part # and create the according component'skicad's library" + ), + epilog=( + "example use : \n" + " JLC2KiCadLib C1337258 C24112 -dir My_lib " + "-symbol_lib My_Symbol_lib --no_footprint" + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument( + "components", + metavar="JLCPCB_part_#", + type=str, + nargs="+", + help="List of JLCPCB part # from the components you want to create", + ) + + parser.add_argument( + "-dir", + dest="output_dir", + type=str, + default="JLC2KiCad_lib", + help="Base directory for output library files", + ) + + parser.add_argument( + "--no_footprint", + dest="footprint_creation", + action="store_false", + help="Use --no_footprint if you do not want to create the footprint", + ) + + parser.add_argument( + "--no_symbol", + dest="symbol_creation", + action="store_false", + help="Use --no_symbol if you do not want to create the symbol", + ) + + parser.add_argument( + "-symbol_lib", + dest="symbol_lib", + type=str, + default=None, + help='Set symbol library name, default is "default_lib"', + ) + + parser.add_argument( + "-symbol_lib_dir", + dest="symbol_lib_dir", + type=str, + default="symbol", + help='Set symbol library path, default is "symbol" (relative to OUTPUT_DIR)', + ) + + parser.add_argument( + "-footprint_lib", + dest="footprint_lib", + type=str, + default="footprint", + help='Set footprint library name, default is "footprint"', + ) + + parser.add_argument( + "-models", + dest="models", + nargs="*", + choices=["STEP", "WRL"], + type=str, + default="STEP", + help=( + "Select the 3D model you want to use. Default is STEP. " + "If both are selected, only the STEP model will be added to the footprint " + "(the WRL model will still be generated alongside the STEP model). " + "If you do not want any model to be generated, use the --models " + "without arguments" + ), + ) + + parser.add_argument( + "-model_dir", + dest="model_dir", + type=str, + default="packages3d", + help=( + 'Set directory for storing 3d models, default is "packages3d" ' + "(relative to FOOTPRINT_LIB)" + ), + ) + + parser.add_argument( # argument to skip already existing files and symbols + "--skip_existing", + dest="skip_existing", + action="store_true", + help=( + "Use --skip_existing if you want do not want to replace already existing " + "footprints and symbols" + ), + ) + + parser.add_argument( + "-model_base_variable", + dest="model_base_variable", + type=str, + default="", + help=( + "Use -model_base_variable if you want to specify the base path of the 3D " + "model using a path variable. If the specified variable starts with '$' it " + "is used 'as-is', otherwise it is encapsulated: $(MODEL_BASE_VARIABLE)" + ), + ) + + parser.add_argument( + "-logging_level", + dest="logging_level", + type=str, + default="INFO", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + help=( + "Set logging level. If DEBUG is used, the debug logs are only written in " + "the log file if the option --log_file is set " + ), + ) + + parser.add_argument( + "--log_file", + dest="log_file", + action="store_true", + help="Use --log_file if you want logs to be written in a file", + ) + + parser.add_argument( + "--version", + action="version", + version=f"%(prog)s {__version__}", + help="Print version number and exit", + ) + + args = parser.parse_args() + + helper.set_logging(args.logging_level, args.log_file) + + for component in args.components: + add_component(component, args) + + +if __name__ == "__main__": + main() diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/__init__.py b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/__init__.py b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/footprint.py b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/footprint.py new file mode 100644 index 00000000..f21d3a4d --- /dev/null +++ b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/footprint.py @@ -0,0 +1,181 @@ +import json +import logging +import os +from dataclasses import dataclass + +import requests +from KicadModTree import Footprint, KicadFileHandler, Pad, Text, Translation + +from .. import helper +from .footprint_handlers import handlers, mil2mm + + +@dataclass +class FootprintInfo: + max_X: float = -10000 + max_Y: float = -10000 + min_X: float = 10000 + min_Y: float = 10000 + footprint_name: str = "" + output_dir: str = "" + footprint_lib: str = "" + model_base_variable: str = "" + model_dir: str = "" + origin: tuple = (0, 0) + models: str = "" + + +def create_footprint( + footprint_component_uuid, + component_id, + footprint_lib, + output_dir, + model_base_variable, + model_dir, + skip_existing, + models, +): + logging.info("Creating footprint ...") + + ( + footprint_name, + datasheet_link, + footprint_shape, + translation, + ) = get_footprint_info(footprint_component_uuid) + + if skip_existing and os.path.isfile( + os.path.join(output_dir, footprint_lib, footprint_name + ".kicad_mod") + ): + logging.info(f"Footprint {footprint_name} already exists, skipping.") + return f"{footprint_lib}:{footprint_name}", datasheet_link + + # init kicad footprint + kicad_mod = Footprint(f'"{footprint_name}"') + kicad_mod.setDescription(f"{footprint_name} footprint") # TODO Set real description + kicad_mod.setTags(f"{footprint_name} footprint {component_id}") + + footprint_info = FootprintInfo( + footprint_name=footprint_name, + output_dir=output_dir, + footprint_lib=footprint_lib, + model_base_variable=model_base_variable, + model_dir=model_dir, + origin=translation, + models=models, + ) + + # for each line in data : use the appropriate handler + for line in footprint_shape: + args = [i for i in line.split("~")] # split and remove empty string in list + model = args[0] + logging.debug(args) + if model not in handlers: + logging.warning(f"footprint : model not in handler : {model}") + else: + handlers.get(model)(args[1:], kicad_mod, footprint_info) + + if any( + isinstance(child, Pad) and child.type == Pad.TYPE_THT + for child in kicad_mod.getAllChilds() + ): + kicad_mod.setAttribute("through_hole") + else: + kicad_mod.setAttribute("smd") + + kicad_mod.insert(Translation(-mil2mm(translation[0]), -mil2mm(translation[1]))) + + # Translate the footprint max and min values to the origin + footprint_info.max_X -= mil2mm(translation[0]) + footprint_info.max_Y -= mil2mm(translation[1]) + footprint_info.min_X -= mil2mm(translation[0]) + footprint_info.min_Y -= mil2mm(translation[1]) + + # set general values + kicad_mod.append( + Text( + type="reference", + text="REF**", + at=[ + (footprint_info.min_X + footprint_info.max_X) / 2, + footprint_info.min_Y - 2, + ], + layer="F.SilkS", + ) + ) + kicad_mod.append( + Text( + type="user", + text="${REFERENCE}", + at=[ + (footprint_info.min_X + footprint_info.max_X) / 2, + (footprint_info.min_Y + footprint_info.max_Y) / 2, + ], + layer="F.Fab", + ) + ) + kicad_mod.append( + Text( + type="value", + text=footprint_name, + at=[ + (footprint_info.min_X + footprint_info.max_X) / 2, + footprint_info.max_Y + 2, + ], + layer="F.Fab", + ) + ) + + if not os.path.exists(f"{output_dir}/{footprint_lib}"): + os.makedirs(f"{output_dir}/{footprint_lib}") + + # output kicad model + file_handler = KicadFileHandler(kicad_mod) + file_handler.writeFile(f"{output_dir}/{footprint_lib}/{footprint_name}.kicad_mod") + logging.info(f"Created '{output_dir}/{footprint_lib}/{footprint_name}.kicad_mod'") + + # return the datasheet link and footprint name to be linked with the symbol + return (f"{footprint_lib}:{footprint_name}", datasheet_link) + + +def get_footprint_info(footprint_component_uuid): + # fetch the component data from easyeda library + response = requests.get( + f"https://easyeda.com/api/components/{footprint_component_uuid}", + headers={"User-Agent": helper.get_user_agent()}, + ) + + if response.status_code == requests.codes.ok: + data = json.loads(response.content.decode()) + else: + logging.error( + "create_footprint error. Requests returned with error code " + f"{response.status_code}" + ) + return ("", None, "", (0, 0)) + + footprint_shape = data["result"]["dataStr"]["shape"] + x = data["result"]["dataStr"]["head"]["x"] + y = data["result"]["dataStr"]["head"]["y"] + try: + datasheet_link = data["result"]["dataStr"]["head"]["c_para"]["link"] + except KeyError: + datasheet_link = "" + logging.warning("Could not retrieve datasheet link from EASYEDA") + + footprint_name = ( + data["result"]["title"] + .replace(" ", "_") + .replace("/", "_") + .replace("(", "_") + .replace(")", "_") + ) + + if not footprint_name: + footprint_name = "NoName" + logging.warning( + "Could not retrieve components information from EASYEDA, default name " + "'NoName'." + ) + + return (footprint_name, datasheet_link, footprint_shape, (x, y)) diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/footprint_handlers.py b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/footprint_handlers.py new file mode 100644 index 00000000..5f910f17 --- /dev/null +++ b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/footprint_handlers.py @@ -0,0 +1,617 @@ +import json +import logging +import re +from math import acos, cos, pi, pow, radians, sin, sqrt + +from KicadModTree import ( + Arc, + Circle, + Line, + Pad, + Polygon, + RectFill, + RectLine, + Text, + Vector2D, +) + +from .model3d import get_StepModel, get_WrlModel + +__all__ = [ + "handlers", + "h_TRACK", + "h_PAD", + "h_ARC", + "h_CIRCLE", + "h_SOLIDREGION", + "h_SVGNODE", + "h_VIA", + "h_RECT", + "h_HOLE", + "h_TEXT", + "mil2mm", + "svg_arc_to_points", +] + +layer_correspondance = { + "1": "F.Cu", + "2": "B.Cu", + "3": "F.SilkS", + "4": "B.Silks", + "5": "F.Paste", + "6": "B.Paste", + "7": "F.Mask", + "8": "B.Mask", + "10": "Edge.Cuts", + "11": "User.Comments", # EasyEDA "Multilayer" + "12": "F.Fab", + "99": "User.Comments", # EasyEDA "Component shape layer" + "100": "User.Comments", # EasyEDA "Pin soldering layer" + "101": "User.Comments", # EasyEDA "Component marking layer" +} + + +def mil2mm(data): + return float(data) / 3.937 + + +def h_TRACK(data, kicad_mod, footprint_info): + """ + Append a line to the footprint + + data : [ + 0 : width + 1 : layer + 2 : + 3 : points list + 4 : id + ] + """ + + width = mil2mm(data[0]) + + points = [mil2mm(p) for p in data[3].split(" ") if p] + + for i in range(int(len(points) / 2) - 1): + start = [points[2 * i], points[2 * i + 1]] + end = [points[2 * i + 2], points[2 * i + 3]] + try: + layer = layer_correspondance[data[1]] + except Exception: + logging.exception("footprint h_TRACK: layer correspondance not found") + layer = "F.SilkS" + + # update footprint borders + footprint_info.max_X = max(footprint_info.max_X, start[0], end[0]) + footprint_info.min_X = min(footprint_info.min_X, start[0], end[0]) + footprint_info.max_Y = max(footprint_info.max_Y, start[1], end[1]) + footprint_info.min_Y = min(footprint_info.min_Y, start[1], end[1]) + + # append line to kicad_mod + kicad_mod.append(Line(start=start, end=end, width=width, layer=layer)) + + +def h_PAD(data, kicad_mod, footprint_info): + """ + Append a pad to the footprint + + data : [ + 0 : shape type + 1 : pad position x + 2 : pad position y + 3 : pad size x + 4 : pad size y + 5 : layer + 6 : + 7 : pad number + 8 : drill size + 9 : Polygon nodes + 10 : rotation + 11 : id + 12 : drill offset + 13 : + 14 : plated + 15 : + 16 : + 17 : + 18 : + ] + """ + + # PAD layer definition + TOPLAYER = "1" + BOTTOMLAYER = "2" + MULTILAYER = "11" + + shape_type = data[0] + at = [mil2mm(data[1]), mil2mm(data[2])] + size = [mil2mm(data[3]), mil2mm(data[4])] + layer = data[5] + pad_number = data[7] + + drill_diameter = float(mil2mm(data[8])) * 2 + drill_size = drill_diameter + + rotation = float(data[10]) + drill_offset = float(mil2mm(data[12])) if data[12] else 0 + + primitives = "" + + if layer == MULTILAYER: + pad_type = Pad.TYPE_THT + pad_layer = Pad.LAYERS_THT + elif layer == TOPLAYER: + pad_type = Pad.TYPE_SMT + pad_layer = Pad.LAYERS_SMT + elif layer == BOTTOMLAYER: + pad_type = Pad.TYPE_SMT + pad_layer = ["B.Cu", "B.Mask", "B.Paste"] + else: + logging.warning( + f"footprint, h_PAD: Unrecognized pad layer. Using default SMT layer for " + f"pad {pad_number}" + ) + pad_type = Pad.TYPE_SMT + pad_layer = Pad.LAYERS_SMT + + if shape_type == "OVAL": + shape = Pad.SHAPE_OVAL + + if drill_offset == 0: + drill_size = drill_diameter + + elif (drill_diameter < drill_offset) ^ ( + size[0] > size[1] + ): # invert the orientation of the drill hole if not in the same orientation + # as the pad shape + drill_size = [drill_diameter, drill_offset] + else: + drill_size = [drill_offset, drill_diameter] + + elif shape_type == "RECT": + shape = Pad.SHAPE_RECT + + if drill_offset == 0: + drill_size = drill_diameter + else: + drill_size = [drill_diameter, drill_offset] + + elif shape_type == "ELLIPSE": + shape = Pad.SHAPE_CIRCLE + + elif shape_type == "POLYGON": + shape = Pad.SHAPE_CUSTOM + points = [] + for i, coord in enumerate(data[9].split(" ")): + points.append(mil2mm(coord) - at[i % 2]) + primitives = [Polygon(nodes=zip(points[::2], points[1::2], strict=True))] + size = [0.1, 0.1] + + drill_size = 1 if drill_offset == 0 else [drill_diameter, drill_offset] + + else: + logging.error( + f"footprint handler, pad : no correspondance found, using default " + f"SHAPE_OVAL for pad {pad_number}" + ) + shape = Pad.SHAPE_OVAL + + # update footprint borders + footprint_info.max_X = max(footprint_info.max_X, at[0]) + footprint_info.min_X = min(footprint_info.min_X, at[0]) + footprint_info.max_Y = max(footprint_info.max_Y, at[1]) + footprint_info.min_Y = min(footprint_info.min_Y, at[1]) + + kicad_mod.append( + Pad( + number=pad_number, + type=pad_type, + shape=shape, + at=at, + size=size, + rotation=rotation, + drill=drill_size, + layers=pad_layer, + primitives=primitives, + ) + ) + + +def h_ARC(data, kicad_mod, footprint_info): + """ + append an Arc to the footprint + data : [ + 0 : width + 1 : layer + 2 : + 3 : nodes + 4 : + 5 : id + ] + """ + + width = data[0] + layer = layer_correspondance[data[1]] + svg_path = data[3] + + # Parse SVG path + pattern = ( + r"M\s*([-\d.]+)[\s,]+([-\d.]+)\s*A\s*([-\d.]+)[\s,]+" + r"([-\d.]+)[\s,]+([-\d.]+)[\s,]+(\d)[\s,]+(\d)[\s,]+([-\d.]+)[\s,]+([-\d.]+)" + ) + + match = re.search(pattern, svg_path) + + if not match: + logging.error("footprint handler, h_ARC: failed to parse ARC") + return + + # Extract values + start_x, start_y = float(match.group(1)), float(match.group(2)) + rx, ry = float(match.group(3)), float(match.group(4)) + _ = float(match.group(5)) # rotation ? + large_arc_flag = int(match.group(6)) + sweep_flag = int(match.group(7)) + end_x, end_y = float(match.group(8)), float(match.group(9)) + + width = mil2mm(width) + start_x = mil2mm(start_x) + start_y = mil2mm(start_y) + radius_x = mil2mm(rx) + radius_y = mil2mm(ry) + end_x = mil2mm(end_x) + end_y = mil2mm(end_y) + + start = [start_x, start_y] + end = [end_x, end_y] + + # Check if this is a full circle (start == end) + if abs(start_x - end_x) < 1e-6 and abs(start_y - end_y) < 1e-6: + # Full circle: center is offset from start by radius + # Direction depends on sweep_flag + radius = radius_x # Assuming circular arc (rx == ry) + # For sweep_flag=1 (clockwise in SVG), center is to the right + # For sweep_flag=0 (counter-clockwise), center is to the left + if sweep_flag == 1: + center = [start_x + radius, start_y] + else: + center = [start_x - radius, start_y] + kicad_mod.append(Circle(center=center, radius=radius, width=width, layer=layer)) + return + + if sweep_flag == 0: + start, end = end, start + + # find the midpoint of start and end + mid = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2] + # create vector from start to mid: + vec1 = Vector2D(mid[0] - start[0], mid[1] - start[1]) + + # create vector that's normal to vec1: + length_squared = radius_x * radius_y - pow(vec1.distance_to((0, 0)), 2) + if length_squared < 0: + length_squared = 0 + large_arc_flag = 1 + + vec2 = vec1.rotate(-90) if large_arc_flag == 1 else vec1.rotate(90) + + magnitude = sqrt(vec2[0] ** 2 + vec2[1] ** 2) + vec2 = Vector2D(vec2[0] / magnitude, vec2[1] / magnitude) + + length = sqrt(length_squared) + cen = Vector2D(mid) + vec2 * length + + kicad_mod.append(Arc(start=start, end=end, width=width, center=cen, layer=layer)) + + +def h_CIRCLE(data, kicad_mod, footprint_info): + # append a Circle to the footprint + + if ( + data[4] == "100" + ): # they want to draw a circle on pads, we don't want that. This is an empirical + # deduction, no idea if this is correct, but it seems to work on my tests + return () + + data[0] = mil2mm(data[0]) + data[1] = mil2mm(data[1]) + data[2] = mil2mm(data[2]) + data[3] = mil2mm(data[3]) + + center = [data[0], data[1]] + radius = data[2] + width = data[3] + + try: + layer = layer_correspondance[data[4]] + except KeyError: + logging.exception( + "footprint handler, h_CIRCLE : layer correspondance not found" + ) + layer = "F.SilkS" + + kicad_mod.append(Circle(center=center, radius=radius, width=width, layer=layer)) + + +def svg_arc_to_points(x1, y1, rx, ry, rotation, large_arc_flag, sweep_flag, x2, y2): + """ + Convert SVG arc to list of points using center parameterization. + Uses SVG arc implementation algorithm from W3C spec F.6.5. + + Args: + x1, y1: Start point + rx, ry: Ellipse radii + rotation: X-axis rotation in degrees + large_arc_flag: 0 or 1 + sweep_flag: 0 or 1 + x2, y2: End point + + Returns: + List of (x, y) tuples representing points along the arc + """ + # Handle degenerate cases + if x1 == x2 and y1 == y2: + return [] + if rx == 0 or ry == 0: + return [(x2, y2)] + + rx = abs(rx) + ry = abs(ry) + + cos_rot = cos(radians(rotation)) + sin_rot = sin(radians(rotation)) + + # Compute (x1', y1') - rotated coordinates + dx = (x1 - x2) / 2 + dy = (y1 - y2) / 2 + x1_prime = cos_rot * dx + sin_rot * dy + y1_prime = -sin_rot * dx + cos_rot * dy + + # Compute center (cx', cy') + rx_sq = rx * rx + ry_sq = ry * ry + x1_prime_sq = x1_prime * x1_prime + y1_prime_sq = y1_prime * y1_prime + + # Correct radii if needed (ensure arc is possible) + lambda_sq = x1_prime_sq / rx_sq + y1_prime_sq / ry_sq + if lambda_sq > 1: + scale = sqrt(lambda_sq) + rx *= scale + ry *= scale + rx_sq = rx * rx + ry_sq = ry * ry + + # Calculate center + denom = rx_sq * y1_prime_sq + ry_sq * x1_prime_sq + if denom == 0: + return [(x2, y2)] + + sign = -1 if large_arc_flag == sweep_flag else 1 + sq = max(0, (rx_sq * ry_sq - rx_sq * y1_prime_sq - ry_sq * x1_prime_sq) / denom) + coef = sign * sqrt(sq) + + cx_prime = coef * rx * y1_prime / ry + cy_prime = -coef * ry * x1_prime / rx + + # Compute center (cx, cy) in original coordinates + cx = cos_rot * cx_prime - sin_rot * cy_prime + (x1 + x2) / 2 + cy = sin_rot * cx_prime + cos_rot * cy_prime + (y1 + y2) / 2 + + # Calculate start angle and delta angle + def angle_between(ux, uy, vx, vy): + n = sqrt(ux * ux + uy * uy) * sqrt(vx * vx + vy * vy) + if n == 0: + return 0 + c = (ux * vx + uy * vy) / n + c = max(-1, min(1, c)) + angle = acos(c) + if ux * vy - uy * vx < 0: + angle = -angle + return angle + + theta1 = angle_between(1, 0, (x1_prime - cx_prime) / rx, (y1_prime - cy_prime) / ry) + dtheta = angle_between( + (x1_prime - cx_prime) / rx, + (y1_prime - cy_prime) / ry, + (-x1_prime - cx_prime) / rx, + (-y1_prime - cy_prime) / ry, + ) + + # Adjust delta angle based on sweep flag + if sweep_flag == 0 and dtheta > 0: + dtheta -= 2 * pi + elif sweep_flag == 1 and dtheta < 0: + dtheta += 2 * pi + + # Generate points along the arc (adaptive resolution) + num_segments = max(8, int(abs(dtheta) / (2 * pi) * 32)) + + points = [] + for i in range(1, num_segments + 1): # Skip first point (it's the current position) + angle = theta1 + dtheta * i / num_segments + x = cx + rx * cos(angle) * cos_rot - ry * sin(angle) * sin_rot + y = cy + rx * cos(angle) * sin_rot + ry * sin(angle) * cos_rot + points.append((x, y)) + + return points + + +def h_SOLIDREGION(data, kicad_mod, footprint_info): + layer = "Edge.Cuts" if data[3] == "npth" else layer_correspondance[data[0]] + + path = data[2] + points = [] + current_pos = (0.0, 0.0) + + # Parse SVG path + command_pattern = re.compile( + r"([MLAZ])\s*" + r"((?:[-+]?\d*\.?\d+[\s,]*)*)", + re.IGNORECASE, + ) + + # Pattern to extract numbers + number_pattern = re.compile(r"[-+]?\d*\.?\d+") + + for match in command_pattern.finditer(path): + cmd = match.group(1).upper() + params_str = match.group(2) + params = [float(n) for n in number_pattern.findall(params_str)] + + if cmd == "M": + # Move to: M x y + if len(params) >= 2: + current_pos = (params[0], params[1]) + points.append(current_pos) + + elif cmd == "L": + # Line to: L x y + if len(params) >= 2: + current_pos = (params[0], params[1]) + points.append(current_pos) + + elif cmd == "A": + # Arc: A rx ry rotation large-arc-flag sweep-flag x y + if len(params) >= 7: + rx = params[0] + ry = params[1] + rotation = params[2] + large_arc_flag = int(params[3]) + sweep_flag = int(params[4]) + end_x = params[5] + end_y = params[6] + + arc_points = svg_arc_to_points( + current_pos[0], + current_pos[1], + rx, + ry, + rotation, + large_arc_flag, + sweep_flag, + end_x, + end_y, + ) + points.extend(arc_points) + current_pos = (end_x, end_y) + + elif cmd == "Z": + # Close path - no action needed, polygon will close automatically + pass + + # Convert from mils to mm + points = [(mil2mm(p[0]), mil2mm(p[1])) for p in points] + + if points: + kicad_mod.append(Polygon(nodes=points, layer=layer)) + + +def h_SVGNODE(data, kicad_mod, footprint_info): + # create 3D model as a WRL file + # parse json data + try: + data = json.loads(data[0]) + except Exception: + logging.exception("footprint handler, h_SVGNODE : failed to parse json data") + return () + + c_origin = data["attrs"]["c_origin"].split(",") + if "STEP" in footprint_info.models: + get_StepModel( + component_uuid=data["attrs"]["uuid"], + footprint_info=footprint_info, + kicad_mod=kicad_mod, + translationX=float(c_origin[0]), + translationY=float(c_origin[1]), + translationZ=data["attrs"]["z"], + rotation=data["attrs"]["c_rotation"], + ) + + if "WRL" in footprint_info.models: + get_WrlModel( + component_uuid=data["attrs"]["uuid"], + footprint_info=footprint_info, + kicad_mod=kicad_mod, + translationX=float(c_origin[0]), + translationY=float(c_origin[1]), + translationZ=data["attrs"]["z"], + rotation=data["attrs"]["c_rotation"], + ) + + +def h_VIA(data, kicad_mod, footprint_info): + logging.warning( + "VIA not supported. Via are often added for better heat dissipation. " + "Be careful and read datasheet if needed." + ) + + +def h_RECT(data, kicad_mod, footprint_info): + Xstart = float(mil2mm(data[0])) + Ystart = float(mil2mm(data[1])) + Xdelta = float(mil2mm(data[2])) + Ydelta = float(mil2mm(data[3])) + start = [Xstart, Ystart] + end = [Xstart + Xdelta, Ystart + Ydelta] + width = mil2mm(data[7]) + + if width == 0: + # filled: + kicad_mod.append( + RectFill( + start=start, + end=end, + layer=layer_correspondance[data[4]], + ) + ) + else: + # not filled: + kicad_mod.append( + RectLine( + start=start, + end=end, + width=width, + layer=layer_correspondance[data[4]], + ) + ) + + +def h_HOLE(data, kicad_mod, footprint_info): + kicad_mod.append( + Pad( + number="", + type=Pad.TYPE_NPTH, + shape=Pad.SHAPE_CIRCLE, + at=[mil2mm(data[0]), mil2mm(data[1])], + size=mil2mm(data[2]) * 2, + rotation=0, + drill=mil2mm(data[2]) * 2, + layers=Pad.LAYERS_NPTH, + ) + ) + + +def h_TEXT(data, kicad_mod, footprint_info): + kicad_mod.append( + Text( + type="user", + text=data[9], + at=[mil2mm(data[1]), mil2mm(data[2])], + layer="F.SilkS", + ) + ) + + +handlers = { + "TRACK": h_TRACK, + "PAD": h_PAD, + "ARC": h_ARC, + "CIRCLE": h_CIRCLE, + "SOLIDREGION": h_SOLIDREGION, + "SVGNODE": h_SVGNODE, + "VIA": h_VIA, + "RECT": h_RECT, + "HOLE": h_HOLE, + "TEXT": h_TEXT, +} diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/model3d.py b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/model3d.py new file mode 100644 index 00000000..c723b988 --- /dev/null +++ b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/footprint/model3d.py @@ -0,0 +1,255 @@ +import logging +import os +import re + +import requests +from KicadModTree import Model + +from .. import helper + +wrl_header = """#VRML V2.0 utf8 +#created by JLC2KiCad_lib using the JLCPCB library +#for more info see https://github.com/TousstNicolas/JLC2KICAD_lib +""" + + +def mil2mm(data): + return float(data) / 3.937 + + +def get_StepModel( + component_uuid, + footprint_info, + kicad_mod, + translationX, + translationY, + translationZ, + rotation, +): + logging.info("Downloading STEP Model ...") + + # `qAxj6KHrDKw4blvCG8QJPs7Y` is a constant in + # https://modules.lceda.cn/smt-gl-engine/0.8.22.6032922c/smt-gl-engine.js + # and points to the bucket containing the step files. + + response = requests.get( + f"https://modules.easyeda.com/qAxj6KHrDKw4blvCG8QJPs7Y/{component_uuid}", + headers={"User-Agent": helper.get_user_agent()}, + ) + + if response.status_code != requests.codes.ok: + logging.error("request error, no Step model found") + return + + ensure_footprint_lib_directories_exist(footprint_info) + filename = ( + f"{footprint_info.output_dir}/" + f"{footprint_info.footprint_lib}/" + f"{footprint_info.model_dir}/" + f"{footprint_info.footprint_name}.step" + ) + with open(filename, "wb") as f: + f.write(response.content) + + logging.info(f"STEP model created at {filename}") + + if footprint_info.model_base_variable: + if footprint_info.model_base_variable.startswith("$"): + path_name = ( + f'"{footprint_info.model_base_variable}/' + f"{footprint_info.model_dir}/" + f'{footprint_info.footprint_name}.step"' + ) + else: + path_name = ( + f'"$({footprint_info.model_base_variable})/' + f"{footprint_info.model_dir}/" + f'{footprint_info.footprint_name}.step"' + ) + else: + path_name = f"{footprint_info.model_dir}/{footprint_info.footprint_name}.step" + + translationX = (translationX - footprint_info.origin[0]) / 100 + translationY = -(translationY - footprint_info.origin[1]) / 100 + translationZ = float(translationZ) / 100 + + kicad_mod.append( + Model( + filename=path_name, + at=[translationX, translationY, translationZ], + rotate=[-float(axis_rotation) for axis_rotation in rotation.split(",")], + ) + ) + logging.info(f"added {path_name} to footprint") + + +def get_WrlModel( + component_uuid, + footprint_info, + kicad_mod, + translationX, + translationY, + translationZ, + rotation, +): + logging.info("Creating WRL model ...") + + response = requests.get( + f"https://easyeda.com/analyzer/api/3dmodel/{component_uuid}", + headers={"User-Agent": helper.get_user_agent()}, + ) + if response.status_code == requests.codes.ok: + text = response.content.decode() + else: + logging.error("request error, no 3D model found") + return () + + wrl_content = wrl_header + + # get material list + pattern = "newmtl .*?endmtl" + matchs = re.findall(pattern=pattern, string=text, flags=re.DOTALL) + + materials = {} + for match in matchs: + material = {} + material_id = "" + for value in match.split("\n"): + if value[0:6] == "newmtl": + material_id = value.split(" ")[1] + elif value[0:2] == "Ka": + material["ambientColor"] = value.split(" ")[1:] + elif value[0:2] == "Kd": + material["diffuseColor"] = value.split(" ")[1:] + elif value[0:2] == "Ks": + material["specularColor"] = value.split(" ")[1:] + elif value[0] == "d": + material["transparency"] = value.split(" ")[1] + + materials[material_id] = material + + # get vertices list + pattern = "v (.*?)\n" + matchs = re.findall(pattern=pattern, string=text, flags=re.DOTALL) + + vertices = [] + for vertice in matchs: + vertices.append( + " ".join( + [str(round(float(coord) / 2.54, 4)) for coord in vertice.split(" ")] + ) + ) + + # get shape list + shapes = text.split("usemtl")[1:] + for shape in shapes: + lines = shape.split("\n") + material = materials[lines[0].replace(" ", "")] + index_counter = 0 + link_dict = {} + coordIndex = [] + points = [] + for line in lines[1:]: + if len(line) > 0: + face = [int(index) for index in line.replace("//", "").split(" ")[1:]] + face_index = [] + for index in face: + if index not in link_dict: + link_dict[index] = index_counter + face_index.append(str(index_counter)) + points.append(vertices[index - 1]) + index_counter += 1 + else: + face_index.append(str(link_dict[index])) + face_index.append("-1") + coordIndex.append(",".join(face_index) + ",") + points.insert(-1, points[-1]) + + shape_str = f""" +Shape{{ + appearance Appearance {{ + material Material {{ + diffuseColor {" ".join(material["diffuseColor"])} + specularColor {" ".join(material["specularColor"])} + ambientIntensity 0.2 + transparency {material["transparency"]} + shininess 0.5 + }} + }} + geometry IndexedFaceSet {{ + ccw TRUE + solid FALSE + coord DEF co Coordinate {{ + point [ + {(", ").join(points)} + ] + }} + coordIndex [ + {"".join(coordIndex)} + ] + }} +}}""" + + wrl_content += shape_str + + ensure_footprint_lib_directories_exist(footprint_info) + + filename = ( + f"{footprint_info.output_dir}/" + f"{footprint_info.footprint_lib}/" + f"{footprint_info.model_dir}/" + f"{footprint_info.footprint_name}.wrl" + ) + with open(filename, "w") as f: + f.write(wrl_content) + + if footprint_info.model_base_variable: + if footprint_info.model_base_variable.startswith("$"): + path_name = ( + f'"{footprint_info.model_base_variable}/' + f"{footprint_info.model_dir}/" + f'{footprint_info.footprint_name}.wrl"' + ) + else: + path_name = ( + f'"$({footprint_info.model_base_variable})/' + f"{footprint_info.model_dir}/" + f'{footprint_info.footprint_name}.wrl"' + ) + else: + path_name = f"{footprint_info.model_dir}/{footprint_info.footprint_name}.wrl" + + translationX = (translationX - footprint_info.origin[0]) / 100 + translationY = -(translationY - footprint_info.origin[1]) / 100 + translationZ = float(translationZ) / 100 + + # Check if a model has already been added to the footprint to prevent duplicates + if any(isinstance(child, Model) for child in kicad_mod.getAllChilds()): + logging.info("WRL model created at {filename}") + logging.info( + "WRL model was not added to the footprint to prevent duplicates with STEP " + "model" + ) + else: + kicad_mod.append( + Model( + filename=path_name, + at=[translationX, translationY, translationZ], + rotate=[-float(axis_rotation) for axis_rotation in rotation.split(",")], + ) + ) + logging.info(f"added {path_name} to footprint") + + +def ensure_footprint_lib_directories_exist(footprint_info): + if not os.path.exists( + f"{footprint_info.output_dir}/{footprint_info.footprint_lib}" + ): + os.makedirs(f"{footprint_info.output_dir}/{footprint_info.footprint_lib}") + + if not os.path.exists( + f"{footprint_info.output_dir}/{footprint_info.footprint_lib}/{footprint_info.model_dir}" + ): + os.makedirs( + f"{footprint_info.output_dir}/{footprint_info.footprint_lib}/{footprint_info.model_dir}" + ) diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/helper.py b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/helper.py new file mode 100644 index 00000000..cfbd02a7 --- /dev/null +++ b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/helper.py @@ -0,0 +1,29 @@ +import logging +import sys +from importlib.metadata import version as pkg_version + + +def get_user_agent(): + """Get the User-Agent header for API requests to EasyEDA.""" + try: + version = pkg_version("JLC2KiCadLib") + except Exception: + version = "unknown" + return f"JLC2KiCadLib/{version} (https://github.com/TousstNicolas/JLC2KiCad_lib)" + + +def set_logging(logging_level, logging_file): + LOGGING_FILE = "JLC2KiCad_lib.log" + + if logging_file: + logging.basicConfig( + filename=LOGGING_FILE, format="%(asctime)s - %(levelname)s - %(message)s" + ) + + root_logger = logging.getLogger() + root_logger.setLevel(logging_level) + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(logging.INFO) + root_logger.addHandler(handler) + formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") + handler.setFormatter(formatter) diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/symbol/__init__.py b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/symbol/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/symbol/symbol.py b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/symbol/symbol.py new file mode 100644 index 00000000..c608f21f --- /dev/null +++ b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/symbol/symbol.py @@ -0,0 +1,234 @@ +import json +import logging +import os +import re + +import requests + +from .. import helper +from .symbol_handlers import handlers + +template_lib_header = """\ +(kicad_symbol_lib (version 20210201) (generator TousstNicolas/JLC2KiCad_lib) +""" + +template_lib_footer = ")\n" + +supported_value_types = [ + "Resistance", + "Capacitance", + "Inductance", + "Frequency", +] # define which attribute/value from JLCPCB/LCSC will be added in the "value" field + + +def create_symbol( + symbol_component_uuid, + footprint_name, + datasheet_link, + library_name, + symbol_path, + output_dir, + component_id, + skip_existing, +): + class kicad_symbol: + drawing = "" + pinNamesHide = "(pin_names hide)" + pinNumbersHide = "(pin_numbers hide)" + + kicad_symbol = kicad_symbol() + + ComponentName = "" + for component_uuid in symbol_component_uuid: + response = requests.get( + f"https://easyeda.com/api/components/{component_uuid}", + headers={"User-Agent": helper.get_user_agent()}, + ) + if response.status_code == requests.codes.ok: + data = json.loads(response.content.decode()) + else: + logging.error( + f"create_symbol error. Requests returned with error code " + f"{response.status_code}" + ) + return () + + symbol_shape = data["result"]["dataStr"]["shape"] + symmbol_prefix = data["result"]["packageDetail"]["dataStr"]["head"]["c_para"][ + "pre" + ].replace("?", "") + component_title = ( + data["result"]["title"] + .replace(" ", "_") + .replace(".", "_") + .replace("/", "{slash}") + .replace("\\", "{backslash}") + .replace("<", "{lt}") + .replace(">", "{gt}") + .replace(":", "{colon}") + .replace('"', "{dblquote}") + ) + + component_types_values = [] + for value_type in supported_value_types: + if value_type in data["result"]["dataStr"]["head"]["c_para"]: + component_types_values.append( + ( + value_type, + data["result"]["dataStr"]["head"]["c_para"][value_type], + ) + ) + + if not ComponentName: + ComponentName = component_title + component_title += "_0" + if ( + len(symbol_component_uuid) >= 2 + and component_uuid == symbol_component_uuid[0] + ): + continue + + # if library_name is not defined, use component_title as library name + if not library_name: + library_name = ComponentName + + filename = f"{output_dir}/{symbol_path}/{library_name}.kicad_sym" + + logging.info(f"Creating symbol {component_title} in {library_name}") + + kicad_symbol.drawing += f'''\n (symbol "{component_title}_1"''' + + for line in symbol_shape: + args = [i for i in line.split("~")] # split arguments + model = args[0] + logging.debug(args) + if model not in handlers: + logging.warning("symbol : parsing model not in handler : " + model) + else: + handlers.get(model)( + data=args[1:], + translation=( + data["result"]["dataStr"]["head"]["x"], + data["result"]["dataStr"]["head"]["y"], + ), + kicad_symbol=kicad_symbol, + ) + kicad_symbol.drawing += """\n )""" + + # ruff: disable [E501] + template_lib_component = f"""\ + (symbol "{ComponentName}" {kicad_symbol.pinNamesHide} {kicad_symbol.pinNumbersHide} (in_bom yes) (on_board yes) + (property "Reference" "{symmbol_prefix}" (id 0) (at 0 1.27 0) + (effects (font (size 1.27 1.27))) + ) + (property "Value" "{ComponentName}" (id 1) (at 0 -2.54 0) + (effects (font (size 1.27 1.27))) + ) + (property "Footprint" "{footprint_name}" (id 2) (at 0 -10.16 0) + (effects (font (size 1.27 1.27) italic) hide) + ) + (property "Datasheet" "{datasheet_link}" (id 3) (at -2.286 0.127 0) + (effects (font (size 1.27 1.27)) (justify left) hide) + ) + (property "ki_keywords" "{component_id}" (id 4) (at 0 0 0) + (effects (font (size 1.27 1.27)) hide) + ) + (property "LCSC" "{component_id}" (id 5) (at 0 0 0) + (effects (font (size 1.27 1.27)) hide) + ) + {get_type_values_properties(6, component_types_values)}{kicad_symbol.drawing} + ) +""" + # ruff: enable [E501] + + if not os.path.exists(f"{output_dir}/{symbol_path}"): + os.makedirs(f"{output_dir}/{symbol_path}") + + if os.path.exists(filename): + update_library( + library_name, + symbol_path, + ComponentName, + template_lib_component, + output_dir, + skip_existing, + ) + else: + with open(filename, "w") as f: + logging.info(f"writing in {filename} file") + f.write(template_lib_header) + f.write(template_lib_footer) + update_library( + library_name, + symbol_path, + ComponentName, + template_lib_component, + output_dir, + skip_existing, + ) + + +def get_type_values_properties(start_index, component_types_values): + # ruff: disable [E501] + return "\n".join( + [ + f"""(property "{type_value[0]}" "{type_value[1]}" (id {start_index + index}) (at 0 0 0) + (effects (font (size 1.27 1.27)) hide) + )""" + for index, type_value in enumerate(component_types_values) + ] + ) + # ruff: enable [E501] + + +def update_library( + library_name, + symbol_path, + component_title, + template_lib_component, + output_dir, + skip_existing, +): + """ + if component is already in library, + the library will be updated, + if not already present in library, + the component will be added at the end + """ + + with open( + f"{output_dir}/{symbol_path}/{library_name}.kicad_sym", "rb+" + ) as lib_file: + pattern = rf' \(symbol "{component_title}" (\n|.)*?\n \)' + file_content = lib_file.read().decode() + + if f'symbol "{component_title}"' in file_content: + if skip_existing: + logging.info( + f"component {component_title} already in symbols library, skipping" + ) + return + # use regex to find the old component template in the file and + # replace it with the new one + logging.info( + f"found component already in {library_name}, updating {library_name}" + ) + sub = re.sub( + pattern=pattern, + repl=template_lib_component, + string=file_content, + flags=re.DOTALL, + count=1, + ) + lib_file.seek(0) + # delete the file content and rewrite it + lib_file.truncate() + lib_file.write(sub.encode()) + else: + # move before the library footer and write the component template + # see https://github.com/TousstNicolas/JLC2KiCad_lib/issues/46 + new_content = file_content[: file_content.rfind(")")] + new_content = new_content + template_lib_component + template_lib_footer + lib_file.seek(0) + lib_file.write(new_content.encode()) diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/symbol/symbol_handlers.py b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/symbol/symbol_handlers.py new file mode 100644 index 00000000..94cc11e1 --- /dev/null +++ b/third_party/TousstNicolas/JLC2KiCad_lib/JLC2KiCadLib/symbol/symbol_handlers.py @@ -0,0 +1,469 @@ +import logging +import math +import re + +RELATIVE_OFFSET = 0.254 +ABSOLUTE_OFFSET_X = 101.6 +ABSOLUTE_OFFSET_Y = -63.5 + +__all__ = [ + "handlers", + "h_R", + "h_E", + "h_P", + "h_T", + "h_PL", + "h_PG", + "h_PT", + "h_A", + "h_AR", +] + + +def mil2mm(data): + return float(data) / 3.937 + + +def h_R(data, translation, kicad_symbol): + """ + Rectangle handler + data = { + 0 : x1 + 1 : y1 + 2 : + 3 : + 4 : width + 5 : length + 6 : stroke color + 7 : ? + 8 : stroke style : 0 = solid, 1 = dashed, 2 = dotted + 9 : fill color + 10 : id + 11 : locked + } + """ + + x1 = float(data[0]) + y1 = float(data[1]) + width = float(data[4]) + length = float(data[5]) + + x2 = x1 + width + y2 = y1 + length + + x1_mm = mil2mm(x1 - translation[0]) + y1_mm = -mil2mm(y1 - translation[1]) + x2_mm = mil2mm(x2 - translation[0]) + y2_mm = -mil2mm(y2 - translation[1]) + + if data[8] == 1: + stroke_style = "dash" + elif data[8] == 2: + stroke_style = "dot" + else: + stroke_style = "default" + + kicad_symbol.drawing += f""" + (rectangle + (start {x1_mm} {y1_mm}) + (end {x2_mm} {y2_mm}) + (stroke (width 0) (type {stroke_style}) (color 0 0 0 0)) + (fill (type background)) + )""" + + +def h_E(data, translation, kicad_symbol): + """ + Circle + """ + + x1 = mil2mm(float(data[0]) - translation[0]) + y1 = -mil2mm(float(data[1]) - translation[1]) + radius = mil2mm(float(data[2])) + + kicad_symbol.drawing += f""" + (circle + (center {x1} {y1}) + (radius {radius}) + (stroke (width 0) (type default) (color 0 0 0 0)) + (fill (type background)) + )""" + + +def h_P(data, translation, kicad_symbol): + """ + Add Pin to the symbol + data = [ + 0 : + 1 : electrical type + 2 : pin number + 3 : x1 + 4 : y1 + 5 : rotation + 6 : id + 7 : + 8 : + 9 : + 10 : + 11 : + 12 : + 13 : + 14 : + 15 : + 16 : + 17 : name size + 18 : + 19 : + 20 : + 21 : + 22 : + 23 : + 24 : number size + 25 : + ] + """ + + if data[1] == "0": + electrical_type = "unspecified" + elif data[1] == "1": + electrical_type = "input" + elif data[1] == "2": + electrical_type = "output" + elif data[1] == "3": + electrical_type = "bidirectional" + elif data[1] == "4": + electrical_type = "power_in" + else: + electrical_type = "unspecified" + + pin_number = data[2] + pin_name = data[13] + + x1 = round(mil2mm(float(data[3]) - translation[0]), 3) + y1 = round(-mil2mm(float(data[4]) - translation[1]), 3) + + rotation = (int(data[5]) + 180) % 360 if data[5] else 180 + + if rotation == 0 or rotation == 180: + length = round(mil2mm(abs(float(data[8].split("h")[-1]))), 3) + elif rotation == 90 or rotation == 270: + length = mil2mm(abs(float(data[8].split("v")[-1]))) + else: + length = 2.54 + logging.warning( + f'symbol : pin number {pin_number} : "{pin_name}" failed to find length.' + "Using Default length" + ) + + if data[9].split("^^")[1] != "0": + kicad_symbol.pinNamesHide = "" + if data[17].split("^^")[1] != "0": + kicad_symbol.pinNumbersHide = "" + + name_size = mil2mm(float(data[16].replace("pt", ""))) if data[16] else 1 + number_size = mil2mm(float(data[24].replace("pt", ""))) if data[24] else 1 + + kicad_symbol.drawing += f""" + (pin {electrical_type} line + (at {x1} {y1} {rotation}) + (length {length}) + (name "{pin_name}" (effects (font (size {name_size} {name_size})))) + (number "{pin_number}" (effects (font (size {number_size} {number_size})))) + )""" + + +def h_T(data, translation, kicad_symbol): + """ + Text handler + data = [ + 0 : + 1 : x1 + 2 : y1 + 3 : rotation + 4 : color + 5 : font + 6 : font size + 7 : + 8 : + 9 : + 10 : + 11 : text + 12 : + 13 : anchor + ] + """ + + x1 = mil2mm(float(data[1]) - translation[0]) + y1 = -mil2mm(float(data[2]) - translation[1]) + + # From https://dev-docs.kicad.org/en/file-formats/sexpr-intro/index.html#_position_identifier + # Symbol text ANGLEs are stored in tenth’s of a degree. All other ANGLEs are stored + # in degrees. + rotation = ((int(data[3]) + 180) % 360) * 10 + + font_size = mil2mm(float(data[6].replace("pt", ""))) if data[6] else 15 + + text = data[11] + + if data[13] == "middle": + justify = "left" + elif data[13] == "end": + justify = "right" + else: + justify = "left" + + kicad_symbol.drawing += f""" + (text + "{text}" + (at {x1} {y1} {rotation}) + (effects + (font (size {font_size} {font_size})) + (justify {justify} bottom) + ) + )""" + + +def h_PL(data, translation, kicad_symbol): + """ + Polygone handler + """ + + path_string = data[0].split(" ") + polypts = [] + for i, _ in enumerate(path_string[::2]): + polypts.append( + f"(xy {mil2mm(float(path_string[2 * i]) - translation[0])} " + f"{-mil2mm(float(path_string[2 * i + 1]) - translation[-1])})" + ) + polystr = "\n ".join(polypts) + + kicad_symbol.drawing += f""" + (polyline + (pts + {polystr} + ) + (stroke (width 0) (type default) (color 0 0 0 0)) + (fill (type none)) + )""" + + +def h_PG(data, translation, kicad_symbol): + """ + Closed polygone handler + """ + + path_string = [i for i in data[0].split(" ") if i] + polypts = [] + for i, _ in enumerate(path_string[::2]): + polypts.append( + f"(xy {mil2mm(float(path_string[2 * i]) - translation[0])} " + f"{-mil2mm(float(path_string[2 * i + 1]) - translation[1])})" + ) + polypts.append(polypts[0]) + polystr = "\n ".join(polypts) + + kicad_symbol.drawing += f""" + (polyline + (pts + {polystr} + ) + (stroke (width 0) (type default) (color 0 0 0 0)) + (fill (type background)) + )""" + + +def h_PT(data, translation, kicad_symbol): + """ + Triangle handler + """ + + data[0] = ( + data[0].replace("M", "").replace("L", "").replace("Z", "").replace("C", "") + ) + h_PG(data, translation, kicad_symbol) + + +def h_A(data, translation, kicad_symbol): + """ + Arc handler + """ + + # Parse SVG path: "M x1 y1 A rx ry rotation large-arc sweep x2 y2" + path = data[0].strip() + + # Split into M and A commands + parts = re.split(r"[MA]", path) + parts = [p.strip() for p in parts if p.strip()] + + # Parse M command (start point) + start_coords = re.split(r"[\s,]+", parts[0]) + x1 = float(start_coords[0]) + y1 = float(start_coords[1]) + + # Parse A command (arc parameters) + arc_params = re.split(r"[\s,]+", parts[1]) + rx = float(arc_params[0]) + ry = float(arc_params[1]) + rotation = float(arc_params[2]) + large_arc_flag = int(arc_params[3]) + sweep_flag = int(arc_params[4]) + x2 = float(arc_params[5]) + y2 = float(arc_params[6]) + + cos_rot = math.cos(math.radians(rotation)) + sin_rot = math.sin(math.radians(rotation)) + + # Step 1: Compute (x1', y1') + dx = (x1 - x2) / 2 + dy = (y1 - y2) / 2 + x1_prime = cos_rot * dx + sin_rot * dy + y1_prime = -sin_rot * dx + cos_rot * dy + + # Step 2: Compute center (cx', cy') + rx_sq = rx * rx + ry_sq = ry * ry + x1_prime_sq = x1_prime * x1_prime + y1_prime_sq = y1_prime * y1_prime + + # Correct radii if needed + lambda_sq = x1_prime_sq / rx_sq + y1_prime_sq / ry_sq + if lambda_sq > 1: + rx *= math.sqrt(lambda_sq) + ry *= math.sqrt(lambda_sq) + rx_sq = rx * rx + ry_sq = ry * ry + + sign = -1 if large_arc_flag == sweep_flag else 1 + + if (rx_sq * y1_prime_sq + ry_sq * x1_prime_sq) == 0: + return + + sq = max( + 0, + (rx_sq * ry_sq - rx_sq * y1_prime_sq - ry_sq * x1_prime_sq) + / (rx_sq * y1_prime_sq + ry_sq * x1_prime_sq), + ) + coef = sign * math.sqrt(sq) + + cx_prime = coef * rx * y1_prime / ry + cy_prime = -coef * ry * x1_prime / rx + + # Step 3: Compute center (cx, cy) + cx = cos_rot * cx_prime - sin_rot * cy_prime + (x1 + x2) / 2 + cy = sin_rot * cx_prime + cos_rot * cy_prime + (y1 + y2) / 2 + + # Calculate angles for finding midpoint + def angle_between(ux, uy, vx, vy): + n = math.sqrt(ux * ux + uy * uy) * math.sqrt(vx * vx + vy * vy) + c = (ux * vx + uy * vy) / n + c = max(-1, min(1, c)) # Clamp to [-1, 1] + angle = math.acos(c) + if ux * vy - uy * vx < 0: + angle = -angle + return angle + + theta1 = angle_between(1, 0, (x1_prime - cx_prime) / rx, (y1_prime - cy_prime) / ry) + dtheta = angle_between( + (x1_prime - cx_prime) / rx, + (y1_prime - cy_prime) / ry, + (-x1_prime - cx_prime) / rx, + (-y1_prime - cy_prime) / ry, + ) + + if sweep_flag == 0 and dtheta > 0: + dtheta -= 2 * math.pi + elif sweep_flag == 1 and dtheta < 0: + dtheta += 2 * math.pi + + # Calculate midpoint angle + mid_angle = theta1 + dtheta / 2 + + # Calculate midpoint coordinates + x_mid = cx + rx * math.cos(mid_angle) * cos_rot - ry * math.sin(mid_angle) * sin_rot + y_mid = cy + rx * math.cos(mid_angle) * sin_rot + ry * math.sin(mid_angle) * cos_rot + + # Convert to KiCad coordinates (mil to mm and apply translation) + x1_mm = mil2mm(x1 - translation[0]) + y1_mm = -mil2mm(y1 - translation[1]) + x2_mm = mil2mm(x2 - translation[0]) + y2_mm = -mil2mm(y2 - translation[1]) + x_mid_mm = mil2mm(x_mid - translation[0]) + y_mid_mm = -mil2mm(y_mid - translation[1]) + + kicad_symbol.drawing += f""" + (arc + (start {x1_mm} {y1_mm}) + (mid {x_mid_mm} {y_mid_mm}) + (end {x2_mm} {y2_mm}) + (stroke (width 0) (type default) (color 0 0 0 0)) + (fill (type none)) + )""" + + +def h_AR(data, translation, kicad_symbol): + """ + Arrowhead handler + + data = { + 0 : type + 1 : x position + 2 : y position + 3 : id + 4 : rotation angle + 5 : SVG path + 6 : stroke color + 7 : ? + 8 : stroke width? + 9 : ? + } + """ + + svg_path = data[5] + + # Remove SVG commands and extract coordinates + path_cleaned = svg_path.replace("M", "").replace("L", "").replace("Z", "").strip() + + # Split into coordinate pairs + coords = re.split(r"[\s,]+", path_cleaned) + coords = [c for c in coords if c] + + polypts = [] + for i in range(0, len(coords) - 1, 2): + x = float(coords[i]) + y = float(coords[i + 1]) + polypts.append( + f"(xy {mil2mm(x - translation[0])} {-mil2mm(y - translation[1])})" + ) + + if not polypts: + return + + # Close the polygon + polypts.append(polypts[0]) + polystr = "\n ".join(polypts) + + kicad_symbol.drawing += f""" + (polyline + (pts + {polystr} + ) + (stroke (width 0) (type default) (color 0 0 0 0)) + (fill (type background)) + )""" + + +handlers = { + "R": h_R, + "E": h_E, + "P": h_P, + "T": h_T, + "PL": h_PL, + "PG": h_PG, + "PT": h_PT, + "A": h_A, + "AR": h_AR, + # "J" : h_NotYetImplemented, + # "N" : h_NotYetImplemented, + # "BE" : h_NotYetImplemented, + # "O" : h_NotYetImplemented, +} diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/LICENSE b/third_party/TousstNicolas/JLC2KiCad_lib/LICENSE new file mode 100644 index 00000000..7d4edf9b --- /dev/null +++ b/third_party/TousstNicolas/JLC2KiCad_lib/LICENSE @@ -0,0 +1,9 @@ + The MIT License (MIT) + +Copyright © 2021 TousstNicolas + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/README.md b/third_party/TousstNicolas/JLC2KiCad_lib/README.md new file mode 100644 index 00000000..44236933 --- /dev/null +++ b/third_party/TousstNicolas/JLC2KiCad_lib/README.md @@ -0,0 +1,128 @@ +# JLC2KiCadLib + +
+ +[](https://badge.fury.io/py/JLC2KiCadLib) + +[](https://pepy.tech/project/jlc2kicadlib) +[](https://github.com/astral-sh/ruff) +[](https://github.com/astral-sh/ruff) +[](https://opensource.org/licenses/MIT) + +
+ +JLC2KiCadLib is a python script that generate a component library (symbol, footprint and 3D model) for KiCad from the JLCPCB/easyEDA library. +This script requires **Python 3.8** or higher. + +## Example + + + +easyEDA origin | KiCad result +---- | ---- + |  + |  + |  + +## Installation + +Install using pip: + +``` +pip install JLC2KiCadLib +``` + +Install from source: + +``` +git clone https://github.com/TousstNicolas/JLC2KiCad_lib.git +cd JLC2KiCad_lib +pip install . +``` + +## Usage + +``` +usage: JLC2KiCadLib [-h] [-dir OUTPUT_DIR] [--no_footprint] [--no_symbol] [-symbol_lib SYMBOL_LIB] [-footprint_lib FOOTPRINT_LIB] + [-models [{STEP,WRL} ...]] [--skip_existing] [-model_base_variable MODEL_BASE_VARIABLE] + [-logging_level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--log_file] [--version] + JLCPCB_part_# [JLCPCB_part_# ...] + +take a JLCPCB part # and create the according component's kicad's library + +positional arguments: + JLCPCB_part_# list of JLCPCB part # from the components you want to create + +options: + -h, --help show this help message and exit + -dir OUTPUT_DIR base directory for output library files + --no_footprint use --no_footprint if you do not want to create the footprint + --no_symbol use --no_symbol if you do not want to create the symbol + -symbol_lib SYMBOL_LIB + set symbol library name, default is "default_lib" + -symbol_lib_dir SYMBOL_LIB_DIR + Set symbol library path, default is "symbol" (relative to OUTPUT_DIR) + -footprint_lib FOOTPRINT_LIB + set footprint library name, default is "footprint" + -models [{STEP,WRL} ...] + Select the 3D model you want to use. Default is STEP. + If both are selected, only the STEP model will be added to the footprint (the WRL model will still be generated alongside the STEP model). + If you do not want any model to be generated, use the --models without arguments + -model_dir MODEL_DIR Set directory for storing 3d models, default is "packages3d" (relative to FOOTPRINT_LIB) + --skip_existing use --skip_existing if you want do not want to replace already existing footprints and symbols + -model_base_variable MODEL_BASE_VARIABLE + use -model_base_variable if you want to specify the base path of the 3D model using a path variable + -logging_level {DEBUG,INFO,WARNING,ERROR,CRITICAL} + set logging level. If DEBUG is used, the debug logs are only written in the log file if the option --log_file is set + --log_file use --log_file if you want logs to be written in a file + --version Print versin number and exit + +exemple use : + JLC2KiCadLib C1337258 C24112 -dir My_lib -symbol_lib My_Symbol_lib --no_footprint +``` + +The only required arguments are the JLCPCP_part number (e.g. Cxxxxx) + +Example usage : +``` +JLC2KiCadLib C1337258 C24112 -dir My_lib \ + -model_dir My_model_dir \ + -footprint_lib My_footprint_lib \ + -symbol_lib_dir My_symbol_lib_dir \ + -symbol_lib My_symbol_lib +``` + +This example will create the symbol, footprint and 3D model for the two components specified and will output the symbol in the `./My_lib/symbol/My_symbol_lib.lib` file, the footprint and 3D model will be created in the `./My_lib/Footprint`. This will result in the following tree to be created : + +``` +My_lib +├── My_footprint_lib +│ ├── My_model_dir +│ │ ├── QFN-24_L4.0-W4.0-P0.50-BL-EP2.7.step +│ │ └── VQFN-48_L7.0-W7.0-P0.50-BL-EP5.5.step +│ ├── QFN-24_L4.0-W4.0-P0.50-BL-EP2.7.kicad_mod +│ └── VQFN-48_L7.0-W7.0-P0.50-BL-EP5.5.kicad_mod +└── My_symbol_lib_dir + └── My_symbol_lib.kicad_sym +``` + +Most of those arguments are optional. The only required argument is the JLCPCB part #. + +The JLCPCB part # is found in the part info section of every component in the JLCPCB part library. + +By default, the library folder will be created in the execution directory. You can specify an absolute path with the -dir option. + +## Dependencies + +JLC2KiCadLib relies on the [KicadModTree](https://gitlab.com/kicad/libraries/kicad-footprint-generator) framework to generate the footprints. + +## Notes + +* Even so I tested the script on a lot of components, be careful and always check the output footprint and symbol. +* I consider this project completed. I will continue to maintain it if a bug report is filed, but I will not develop new functionality in the near future. If you feel that an important feature is missing, please open an issue to discuss it, then you can fork this project with a new branch before submitting a PR. + +## License + +Copyright © 2021 TousstNicolas + +The code is released under the MIT license diff --git a/third_party/TousstNicolas/JLC2KiCad_lib/images/JLC_3Dmodel.png b/third_party/TousstNicolas/JLC2KiCad_lib/images/JLC_3Dmodel.png new file mode 100644 index 0000000000000000000000000000000000000000..139722b75e6bcd7d8772ce9d37e9494d7d4e18a9 GIT binary patch literal 103353 zcmeEt^;=Y5^zI-ENE&o1D5-P|EhsI`&>_+w%}7ZI0@BjbNDVNA0wWSbib&@S(#^oo zF?8I+=X>wpaDVv@&hu=~b7r5l_gd>+YrkuqNG%N|GGcmS5C}x3qWnq+1iB5r`4SNT zS2pjxiwA-5!W>?{)KYo*@{yZ|tF6O38xV*yAs|6ox#I_8LLn4yCb}IeCTGSd9Lye%)JvoyxXDOe7)e`0=mFCi%M}H>`TBod#dFFKfMr*VpUg Ksi@2u4fCui z>l%*aZ3y_n&Hyp{^*v9G#=vV~I`NNj?1E^@{Xa4JoXLmhlM~KPU(P)$=RZH)XsUi@ zkc!#&lDTa@FU0w`;n7A@76UU4$^6=l>N)iYOHqMh27J&cA41|*r@mK;q0{SVBy q)=Q#ell zr1a C1vP>fsqbjwDN6W~PQN5zn7G{2?P~DpC2GK#W1Hn=pBKu7oY+gSy;Z z-R}moP@g_lzWnXjm<>+?;LZoxnJ}hNeuZlZD$tZ^DMelm{2gqut@#E|<6FxXGeAhm zegjLi*@WOEgv5#g*dHp2<|#9P3$m3r>^`>lZYjvDV_7@8d`@LvG?Vj|K7n})c6QQB z*Z@7YIj#YE2>z!RAR+OIbPt>?NZ`{gS?Rk%Itw93{$cMW!}dd-0=_tHVtQ*nqTRAh zVmiP5E;q>hn00~^A$Pq$k%4BB(Q6q`V)(|>BA^(nsLo@z3iX95r152=_Vvs-ekc_t zSYxUB2Q{%5Jt3(yc905vMS8w+#N@;T3S%)nqGTbXw;j6pK#UeAS3S@z9`xzCYKL3L ztAzY0acCDg{@@vC&(g;@7iym4_Yj=Z@L3IXVc&FD&es;Ty^6KbAF~0Od-LA5Axsw) z1*|N<#Q!`yK^`C^MKYlk<YH*n|8nvUC@_780-;Fcst%&iGW1Lvg-|t`WPwp4+Pv;AM#~yo#_Y#xO z8~$P8#oO&zP?W1i!^OU1S2W72?r0H13nVGLaj5=t n(kpA4jM8*?fP^S)&EALP(8-Ej#N$$a4AUA9Rk(+}uz>Y(V7by>PbDghf?_ zQQ)+dm8r!R=i^O(au?t!b>UAC#KO2cad&`Fa@j6+TU^$Z+a8&A{P8(ANcEde?BQv) zS-@GEiLw8$Z% lq =g}Y4q=+@K-0hKtm7F+G* z$(h*^ %Vb+gN0wX6rYGShu z<@M?AW?)J9^~UvS($!#4O99vr!m3-A$EI8+=6JS?O}x4?7{}#a9hm{OHSG#ptJ-lA zIpo!gx}Ay*bhe1J>D2HWt&(9WE;VqUfVnS$_Iz3nhsx^MHcf_VHa)^ncppy@IIw&e zbh{G)@t)TMM+t&fbSh}29bspOz2^-d_R41)-fKU6Xw|` F+#&$Z*K9QIvZ_~_ z9OSRcs3f6GTv6d%>iL}yvzfr|EPEmuoLdhGNqqZny7@0t9q9{zF|%^t4BekD%>*0m zC>RKXiZTl`^5l%y&@<40tIp@yMj4}hW1sTv4<84WqsC4>do!{a$44$|dCU8pEEgVz z%>@*N9=*WdNCrYhhnu`VEn`#6FE++Ne#NrRgt7!}B>l!_d*Q7AMw5)sG1-jIIT_Ly z3W+`tD$|eGKcfso9St QQ_*+ z{Y#PCB*dcfUrdQS%rhK+8=GZ}P4|6b?_%(~&Q@#KJ7Vx#>QOKQ*AdySlA_Avle#qeTp}h2mzPgBpHeL?B+cKJ~H+z2?{{5 ?}GZbUUas<`ttIe
TAs#{r!m8U#bEzlo?R1EuVzepO zCv@ZaNd@JJ_S;H~qeZ65${ itqLBhOVbP3MVhJihWaoUi_b{P_7#x!k+rV4?47<6Q% E4CQBrP*or~7zGL)Tu}vV>hRDfcG;6N%`S6hqe0h8u(J+e<4q`z#(Ehem@Zzo z3do?xuwhTL%`%34q; f1&=CWe;q9Lzn^w-QTiDR!%Jn~z22k9H_ jQly+6W!G zqe1>e^7usP0Kn-ut!(CB2+3H*3Y3jI(mRQaIf_y$tnO;w74#EedVSC|w0hLMJ!3IG zF1E8)pxH8 *Z` zdfeq+4xBz> &~yW z8WN*%)Z=ayV@ibedbq`OC@h&)iYoWS$K(5olS7Vi^@6#n!Nj-7v-}?42FeEdr17i! z0H5s!YE{Yj<@l9rvq;1Lm|~3iL&Xd-?(8xr3(?>M^djAKA#H`ZaDC2Ku3DM^J-&^Q zl%8oo7Rz3ol3cD2TCDL|%#HU)Fm1u~QJkDIcTAqMvQ6AOwBLAfjQ%MMwEid-92K~& z*H@Msw&O`LU_w1MQfxK)!wpiB^nx*ptsiv<8}cqSzRhS<%56iw-NSd2r&CFZ(k2?2 z$!>Ppn$gZF#zGr`8giZ2%Kp=AX`!kg`Y+uXbEBLV1HO1_s3c+!_*}5wE!VGF&X>~8 z)_Nn1r&vlJ(1|Q2HM=bN%d#m}oBP$G(4jacvw(uCmCp40$ECCi+Rt<=lm&Ja@mo%$ z635iC-jfTWHmKBV%Oa-sK~g-o$oapX P4y*lDU05IcQNkgRTfYs&Hsepc-{$nigz2z? zI%>gkk`YTN#d4!LxsFOy7|vr>C<+9Y9&C98iShEb%Ue9Q*f>Hy2fA@WuEwuVU2$k! zuj5bSGL)mE2t(O8H^-uI%)P=V{bTD#!fSxpA>e#1%>{GU3swR9rvWq5n_P9~3F6t3 z^Zk~g!@@N^)uyxc300wyK+9JKeSbJTN_p#5Efpx|=)=Aonu&R1i^X+ku^2Ch5RZ`d z^$m*xP@yi;G`CwzA1Eo-r!`MtH#Yh|qJK(M8|7!t&5kbaSEo{ApjVqstF{~vxc*Zi zseF+rpx?;H$GFGouQ;F6Ow#;h#KPP`J9hX_oXqv2iL|MIVShi qqw2&XGPFuN7~t?5>UKDr?|d~a!SBL>Ce(Tocvs?s&KgH6|V1` zTzATW5` P^uOlUp>k}kJr^An_ CF_KbIbGd{8QIJL zZ)!G% BFE3N-rX;b~f(uz*0NldI`a z`vs6Aoaw%dv;38?KK$pW%=w}TpvBVnSW1W?5>KjEsKTv7SFd`ydooW|>TSHm4U#*D zeom$_-wON|xVkL=CXEfm*kh)ZYPdxfuFekE&fn|Lv1Q&YfmAt`8==y21HN_E-$h|X z`uVblfv!IK{)SYHo!5GMA~gmEfsb2TTgSQk%kRoP_A$cL1`VNR2s8b5-Apn~8Xb3b z;By=HK>0cQ^5aos>3(~u aq&(F}ITjMb z?%@Hss!+@$fC|-^E6X(&-? $?=9+;_czaGaWfgCd(adpuegM zP~{H3fjTq;0}hrhs{KC%L^7U>(8pR$RVR|3(H}PU)VwCl{)k2&AI=ZwphYQa>u2;m zh758fZHRkf6UG U0v*5Ha->|lwr zeE2&rk9E-NdmRiH>l_f^q~3BJ?+W#=;bRD&$^%KMLAj~WK+Qr}xq|% XD-!2imQX>pG5dRpEZZM9N_Cmz3CfZdyxv7N`f%!xb}1x*X#~gk^vh zxiBMjO-;t$_bK;6j=YZ%IJcLf8W!nfSX-A9_^#@fH_Y;K{-0}y(e1(OSTR-8p{>}; zBMKlqT1=x(d+lm#n+hE(&X!zb{E*GtK(= {%6I&M}< h;Jd-?K5oqsLOSm z3utTTEB$V6&L~U{s(PzK+MEjt)Pr&VjmQ&bVWyjUcUwaTFXuiF*o|9!2enQ!>5QRY z*7&%>01*E^>bG}LGUbq@F&+4=LEw-J^Bf`?d4!NyqfsRpoFoHoY0~d>6#IcCC0;>O zXqGRQfm&{E^Xen2M)M3_ey``eO9 !S0V=SQk1zK^5c?-)9GbT6-(RLiKJJjH#gGuOxni_@$5j2`v8c zBZHb7igFU*5f6qv0<>v)cw*{aRqm!*k#?}~duyqrQ*JaiFFn-&j K)CpIl30f6&eSibCb;-_aQH7BEo`^Tb~ zZr ;m$*Kbnjr%03o*jFjM@`6l2yT`ea;&NY2cS{o4W)T>p&z4O zWN5x}K8o{Z36@W~Z+Au@4Bs94(Cr xM9$fkl|TbKmoF z_J`wjsW_o2pCHQQmY`L}od@c`<9)gjUxi~Ha}R$P%eY(6V!YaPRtnJu`?6?wd48A& z-^fo2w)`bx^F^Yl-eBP(Qpy$nKn0 EU=L~h=YA5HON*9%R@10%OaeB=@+FUzVKWgRGJukT zawt`x0zer{@9}kkd)dVirCI(AU=Q_x?Uu$@|ME`2bT%#fd(H1q^0b$V9o{5{H*aI@ zV!w7Fj6_NTY29M<{K6lxv|kU-W{WC$m^tKJ&uysPzGwI#J@WNxyFz}UBfN|kH|CY8 z-jY!zEh>#hXQD(4%0~1PRjnhf6IE8je^cMz4onl`lLE0njbgoz_lR}(qVGjibgQ!F zr@0NyX-{&PdcFfI7tj2?Q+8tT@}D)15^Hw#!q~etIFm _>k7{MnQPV`+$!1`}_# zZ_T|s8bXKn`OrH|e&$-acWMmCrf4s}xT4*4&=L0qUDd`(Fn|-uftEgGypeGHDsLr^ zH@0DZQ5tMZ98=)1d}`Ud;9xjKvH5B>aBls YbjVb|3YS)7hOI zrnC#IF7}yczV51TUcGv?9L_04>SUF{M3cMk_`0$ARndlytQ^G(BWsH$Uc@w!xLt5XykN?(4B%`F`%Wx2mLp=?EKmkNgdvGO`Apm=>x%+Sq>Q zq53(!Kvdi69r{itRL;yNE3{p5dyDknOc@u+b?i)OCa;GuIiu#SF7@TwSYZJxT4uLH z1gpX6zwLEdCbvq9O NYtl+_s9} zUo!{_*d`X2e*W fAq8*y0j6|K~=Yiy20tM z;xzDQ{jsXgsT3&P{`OlADXf+F>}1!S$X*{fGs`yh{#pGzj1~=T12HBmv{AGie^H`1 zrW?btY(2KLWz3H8y^c1J@^^1-YrDTR`jCA|*PMy*V4WsH>hwb7`fN{4=6sV<^ehY` zsAd?$=kKN$W;WTKQ1fQ#*)*7hZ y}l4hOfQ>^y1*{f0NIs+&**8f5~hYS28n@nx0r4mNePi-rm~@ zJQ3YNUQ#P)A5p9{^KtKyi;9W;qb1)BVvQ{!#Q#~R*mQhk@`<*aYXtg{rYi1aty?JP z!%+-N&_11}OkhuojBkGzUsp`ytA#d;k-K3%$3kc|*EB)7r (SPG4PCq#AG>J6{F}hUhHgnlcep!=OOnQ5>wPcG(47$Rk z={l6fW9RDS&!0c*{V~*a$M0839`=MdIwfsY^o!jBEe*^xPPeoS=3Y Z0>tyC!HD$xR)x+ceKBos+_)D0oK|)EmzA+%%2~_-^D*KX zpo84ScNMM?OxbCM3cVe8-KadJ_!ir%*R)TT;CUl~?@m?4Sev*KBxQ4EvbdakduC?I ze`k6LJwVVObZIAZvMHjjOmf#O%1E8#S##T(QMa<$xE=&r3ge#LT(ZpYXTQ-pf-lT6 zg)W(S&SNi>hCqOBl-E-tHbzvaqu~Vv$tn#veQIA!Y%JOKbbW`taKle?as}=0HOTHG z`@cu_K3AY?i>oQk(=G~&w @!j%@)^$zc;4P>S0aTJ+9u}ARF-JRc14P~G^jCtTsow-tnCT{iyzo5D&fu`>u z!4V|KR-QEhleMvo@%ZX#|Gtgfp-$IseeBdN?j5*4FVPV@7vwfK!MtY>bjlXhZb@Ll zc=ZWWrAgGy3#LF)fC+5O?Jrj*RHAAkxz6-n05QNi4oPK~v_mRuXQd@n5Sc!z!ushG zx4%O3*@q|9<|LmzryHQrCUq{_klqw~|DAe7WB>7NKxWHXFUwx;HP-AWchEvuH(uC= zER~@bfqP2Pe{pxC&z7MspFbR-eHxv+$S;(^>n=g|`zsy*_?SVHhTGLHS)T7)U2Bj? zWHO8*%X4${-9g9k+(K8-p7i!Cy|$0{x1#RGK&j3ApvVDnfG-pSM14hF@-;T9V%3MZ z*S3~x6neMU=rsIq=AhF5%tspW66nfKEtXn2c6K54Iee45gFOFoe(X{s!r5}b6<6bn zot-K&Qwn1MjD$6M(gb&2*FGpynDKIsum5wm@`PS7tH)6cpfjiAms`^Q{{GX=0hw)Y zz9(1x;FKyD{vzpp JU2q6 zA(gf`N`ZjAmA*3;CdVLD6^CD?c}rlOGJ*)eSA+?;IqTxb=91z@!{T0t|Ap)IRRHtc zoDHoV995?W!cB01G x}_sP az*607P7vLrOwNWa7_=1e&1~9qe{*YMLdfx0yr=o~hZr!wSzmHKj z*hhegEfXg2Or?v+5~o!v9(^$|Nf8x%i`+;E04qYqUh{m91%(8eOYcgHzqMa)GOO|- z^8q;{TByL(KQpZX#R0`W#n0)5x{R#AGLU+Z&e>V!%*;$D2DcY;-UXcHJIHT{*EdFO zlNB3{^9)p8S);hcqWOT2+=P8pSu}h3OQt%Yl(3pVs!+3 hFAX3g49wI&Vb93 4kyoT(?%5VY462}( +?y94# zed;uDJHIbR)SBJ-$;$M2`RD03z*a~HenHxKdM0IOGXd;m !fO;E3?TW#-}guXT^=`I0w_Aj>ph|tF2oxY-gW`Q< ~2b&?*1y8>G7r6epcpa(SdYkM)E?d95|r6C4=2RqjSioBqFb+Sx?_cq%|kn%eNH zvkxpIvk*#=yL(^}&?jttQO6kJp XQ7Jtcs6-yK` z{icZbW4VM-hj`b~k*N3fv=KI#4bwNg$JKJarW<$^J-6#Ed;WElH( xZc<~ z;k(A|oU#O+tH}gFqs~*KzYL4fR!CFI@XY 9jp2Jb@bU!w${)PUSP#o_Pnp>s!lCrgA_GRC7;U=&}ls7 zzJU;)e!N+_S-OG#afAL~UOOP6bY}QbPSL)+I%1Vr4zx2(OiWlhIXQ7@x@CniSV#qR z@&)1^w_KnIKU{5!{JN-6(;b-mPR>nHWDs`aw10bKWBPmI;l4k=*E~o`gMF-wwZ;Ld z-bK#H(=QMM$~!>5&`_DSjaYeCYqlJXsXCe9yH2@2lD+zPeI?ei+zsb~Ud|LFX|=>@ z^#l6ZHKNo0vp$KbPO4d2@)%3;xn^EFHaoT_Y>J~LhV?(jLSO3pyYpLt8TaSg@Q;p< zxq3ZH%}u7DfPBHZmlt9egDt0bZ%lhpOf!D%!QAN~fhY;MM`nIkqWwR}+CS4t>D-j2 z(6|NoeOcKX&w}3$Eu3O80MJ~rELK|sj rL$%fS?2L7K9cB0=Zg z*X?|=GKEWTp{b(h*Hq-h|JN*?$$LhCY(1&eUCIux+6Vwb@pQ$8uM=ZbshKD5O4J^~ z`}{SHQh}GwDHjxb(u*C$#pw=QTm0Lvan~12ve#)rO9dQ!CnpnJ6?C0<*v>{?8qv;X zj`-X-4;3c3hanScYe *8QAu;co2F6htoxhvm~
x#qx!%XS zJ{g_cPqGN?8>Q(Y&9+>*fT *70I ztYyP=GEqD;W%1yg|Iag625>BI$$iRKFdA-6Bp!wPKd1^XJ0Bc?fTWcIa~{~$*c z`ICf{FwHx$Be-&l=<$d!lO}K~3k~vbx674T!L>Qb1e_%XU7+kPdXrvGit6Z-w*(6> zme<>x(T4VLHmU0UADX^8EXu8WTl4@Toe~2|O3TngD$*$>ASDgb-67pAT{4s?jUe4a zgMff^gLH%Rw}D!2!iPQeBDx>6V?ADAjHJnc@Z3 zxs0 $GT*cVzp4DjwN_M-jjve+tkUZUid!DG?h zdMWxq@^Rb?rR?LKDU>o?+>5^nh1Vxf?+^=jC$v5nZ77=UwnS?4%99b+Zd-n@Q7H12 zzoOrR^XUVet QAX(YrNVG1b{K#WiiA@TlEk3&Z?z#k%t)KEn5`U}mHpe$7u}fH zRr;rNw g)W z@m`M&i!46#xkPy;>W+9itao^HnzF*c38|Cw75qGZ$;A9*30b!$f?Xwb$vgTO*KhPr z64VqKJ{3fzjMuk1zyS*R{M$8^S4CdP*L|SO_dM;zGw(jpo0XStCqqWOd{T=LlchRG z&ye$ZpNNoiTdrw{*UG<=!=Fo4FZB*b63iM!guYmi>pg=UT&`hGvmb13gm|4?KwrjC zD5govUyOPh@?YLk_)Pd3&Ew1<6I!Lt)pR`v+M1zA&qu>`r+D+3K#TX^@BJ2xI~O++ zEeby2iU_U;;OZZMQDWkL%Xwl+3ASBy@xji*{BmYUZJn=ca+JYM6iT<#^x;Ls8xi-* zaE%bc0)_qv@bbeE6ebFVC3B&wzNfC6OTj4l0m>$7`AZqPUa#swstRSL-?&mC-dIof zUM?3|G^e39tt2zWv3|Z*@D}OkSYiY64f133)5gYT^4*jnEf 5$g_E(rb(uUhb3;WpH^29>W229@~& op`$ss zA##4vCs81wYL*smcuhYfhE^fj{a7d9st+S#LDZ(g5aZM74If&*^5>$qB4p~Wx3Sk< z2 aQU~USo|e-xU=kI;xK}sZO*D znVe}X*S>$f&f)0O`UxE_eYqhYn##Z3LG7Rl9BbU`fKQrftR^m&vj%|@;wq_5s@|5* z$Ac{8ah8iYjF-w4&d~kliOaDe>JjI#3}VnDL7L{QSJWvEo)9fVp*1s+z84iwVfOKV z=rv1Qr0-#{`rVfaA(P%ASiDIzBJPkbHr}+W@BWt#7#v)N0~v~hV=0qA#DdyiFJbd} z7Bc%jMWTl>bS|^RuATnj!?vW;{Ml~n++7-&1v(SoUvEGXzA+RGR4d3&i}&%eC}|B- zxUNOj?{o(vQEN|H`e%ko$0UaHXSZa!9H1+?N$|0Yn(q_HB{1EdMv+Z7ccQ;4dNY+# z&25ZoL4(mu#jo!1VrJ&&Vzv2$fn)rvC64IAxMV5H?!woDl53JkUwe^1P%V=oPYX4f zsI!9~c!z~++}$VmAEpmVMeDTxp>iklr34T3a*qaoCxkfAB8}Q`K@k{h-cWre bdnj%CRlxOvCdISz=`jU4wRvB)3PB`n*bmcYW%{~U%buBM3RsP}2=tt19{a!S# zKdezDpP-CYlAZTBzHXEP{@PTHB0r>VJ)0C#8`iNBT@dyc0ISgRSg`-I0lvJ3eUWe4 z*MuxN>$4iGe8R)q??05{i(Rn(tFUk_20E$G)C`4-;MvE3Xs~o(iE*dJZsrc&&R@=d zmBXEf&*vh&g)i)p(n3fF81tIcXhct$zbV+{WMWy1JuLv+9tx`Div5RKhFS7Sd;#=; zQO5r()^!#-BQX(d8(!~^NvX*S$2xn#mVYG<`OX8lKj%Qs?q&M%+i)Yb87WOFuYs z8>~d^{%u`TMSosfs5$OEqWsn6bLYjGs_^`Gh*>29iS0OYwh3M&^jK8gX69hFemN|^ z|B1pTAna;gUJKnQ)D(pZ#zNS}pNo3Z08wD+K!KVvjP?AKvz)`279>qV lD(Ed>Y{ac6( zzR;e*MZe;|qJN988wKZmE*JBnsugJH6;$KyEh07~IcCCOor|5?h*&Y8^iCKl-g(NG zd0$ZL3~(*>`pfFhM@$2MD=Cxl#jzU)K`NW~DRm!e1)<-jL?*~Ax E>Gv7$UF`k9MN!5YUmuX$k`m7_Ib;g3$|c6Of-FjJ4}Xf(@<|4ksh?) zVCot3e?R<<&SU#q-Bk1Rg~v;B8GcjG%~+5s#H25=6eiBS;d0OWhfIA#nZ~;0`%;}n zrr-V)S!8TvxL@V0+7Ow9L87B*1(~U&Y%2SzPkcB5j+QF$%nLG2UNLEIYnr^q2|knk z%b> zk$Q&g!2)Zl6C;X-BAMZSb>so>c@<|m+mxjyzluvtBqM(nyynxXPBA2_HleX;pb3bm z;_Lfr9kr_Cn>#7kG3f43tKE`}Q}I%{;=6Fc_ZPsweq))NQf7CLiPxi0X0za?s~0e@ z>koj>6E_axYxg@oX_{JRQ-#7KQsHB*y>tW#VmMl&U}2lKJ7WxoTR+$A2xQ#7DQvUY zpg{rkekux*NRP4M-ng`b+rARj-@tk_o#^fk;TPHsg>FCWp9aW4?(R>vL&4_d5fPX6 z%q>N96+G12;{QS}y%BSd7z{YxvgbI#@ X@=6%=Jz)2U~wlZ@)?St1+TN8H}!h=3#%DV(*HoW z0~}_i7F3}{cKT3OO^g! iz0j=Dad`BwTcFP zm5KAkl=C+nT&?DYrlWLXYCY835-1_8B^gm;7OQ``N1e94gNICQ5w9^in9q-gPHCSb zBlbj?zmEV7)6Ij63!wnob-!FNR5$pv3eUsYKe2wZ;;^RaPIyH~us!cGCRh~lvfB^& zv7s5Cp}55ZlCV3Ue>-nFSL~m6Vg>iXO}w+dI$;=Fn55K%jmhMt&v{LH96C(6_Dnpa zMZT-0Md{=Q-SC);L3i1SJWM%L(xeG#coS0Hryu}%Pfz`~i=8-FugK-r?z>V)3>!H| zZ!f#5mq=Yx!31Yi3m0eim5K2Ek4*DYET+Y!w=>zFHt)ykCXxOAaF1VoY|HqIBA0;S z2RIc#XyjssB~CWPwal^tw_-{?1ucrWfr(qh%;dYFK2@R|WLWo5$l7A{59E~B^`{O- zW<>D jZ|lgtMGVDp` LJPUSn$z`nQJr;+yo$VFMV!9vg zLG{6pE1QZFfFFc?QVRUA3S<7+U)iZl;JBwGyM5G`25Oxg(+l_}fweSUL#ba!>yWGw z7HeFCDl0E*<LMBhU#0mi(gHD}-lF-g;&@ zK7kptA#G>$sxP&dj>BU3 Cgjg*;&0t+HT%AFwe_hvuZWTI9LDOq#M2zVH%U7P;Rf#)D^3vp zEJ?%WEez)1{8xL5J!6kX|2?RO;`xzCv+~*<=AE&XM~)pLD ^0%B7n7`Xf}BWHc#;3_6`@h~lUcFx-E z#611x#=EalbCG#mDPmIqZowS ew-S4hY=U=S%(gk 9fx zE4zn(#}-!ZRgsw@RiXu5MTNg|s-@L5zc`cLJ9rNK%5uCjSZ~@FUL3yNVVn2d$5D4C zv1485No{k>_;-CAo&3^305xa>>DEUfkPd@ExD-X>@sRh{mQ}J>#UNZu;cswYsG@Dp zG;;gshaW~im7+{w*T3j!)}rZVl1Mvfa^?G1^=xkKYN4h3e7>PS09}xXJz;FQph%Nf zQIUXzq97r7lY1)!*_IY>HqCw_fl}*&F62|*wI1AE7h#*EdeZ3e8Rka$xAM`3bDYT< zGPfiQhB76n)E{wAIke{oH_Wh!UM}hST(8u+pU(;I?AfSzzy+xY3loAG**Bh3R2NF} z@f!AA4k7_woM@$cVB!a_S%-f@M%t-;8<@Uj84z&)#hNgICmos}NX*tCg1)Al;$9{c z?6=vEtJbOZ?WnWuw`d&S?C&4{JG8c$o6akKqB1!W)m ES2Q9Kt z?|(z+BdPOf7{!Zj%Cam4#sW$T^>s0NW}F_Jr o_NH)Wen?8GJ^kt^$*Uwt#z{5l zl<9+pG}~&7jd}&vn8o^P`N}(JKDjs~Sl3Q}%dg%2$qb@_GQPC(J3k0-ZinEM$X_M} z3V9J4I=O~qmb8M}SyC}J_II~99R_zKz0`ONMk E&n(1~$1JIHLlMHM~4J$|28r z)zou~W&1`~GCID$%&bV9XH-ZP4ewe1n`<`ho;z)ejJTfc=?e0|Rr7{;D<5iuK-$HU zuCWjZ%{PFFSffaz#sEAakh$2({^z*mwyoG*(iC6Me! CU1^IgZI{VSzl5p4gx~{!FhM_@D zhm}U!zcg3=poBd^D7+pOKz -(77RT-|HPCGdhbX5cCOlFinv6jFpXwU%BTtQu)8;1 z?c9&}fe|z%`G}9) 3wQ8nYASasm-rMg21H*$C9 z-}anQHx)lwSe2pucrSLM3;y|+oy|XBMfmvlJ=9n2%i?vL{#zaVzeml8QdIXQ#HVN0 zq=a qr3dW`*GJ%=J_wQvBgv;RHm296+?D(84?)` zUwgtq?{4zDQbq$FOlXtens7+hy4`?uo+dy*HOYrpc&$H(wp)B}I*6A{m|`Ga0izDN zM4b5}q9Cg?P#ZChA#_mCd!>1cM?~cprSOQ|dNO;%SqI7+0WjyfO zAF{A9m#~8Ee$~n~7Hs|@MHMWTl{W2}oqbzhGP_lI9F&Mp64_PKQ)7Ytyl#|BNQ=eo zH^=b&jfcy1`7?aYP*B}GU{;PJ&Ih}x{*<#UMo^CmwceOWqlNoHYA#1{aSm(5F%^VK za@knyV*#r`P|wsD!k@jkw+O8CeyPzi+8j2_DrZ0IF6--jlm-IBp;<+ktSmApAv)g@ zBW2S0Q_;lgx8QbYOm8<-gPn!;o9tcuIvp5-UMU(Z6|!RWOvQn@7Wk(J)p_LnSxTyQ zkIy*%ffG3~3yU0h2!+(b0u4g`DHju``jZ53q9G$hsffjQtCOqVZfwgh^xVddXTX zcs!Yx|I0$NGg@K}++Uurc6AaAJ T4uAh-_LN|SA?`?BNeP9uN| Av+ry-(Ise-EX+y`V0>*h#{B*9bn5#cipjtL(5qqjGN&`4x zcnbdEj7c;g7h*ac+}@be6EO7k56Dp=)`9_<(R8PAHT76cm_@SmMR)@bg&M4?0ULLI z+SJoC^c+V}unFHtRKnyFtzlF%wOU{F_fs)s#aK_oHax =zw?B|tBPBlw>#Z^D?$5~=#Hfj%;iZ`2?JF6AqD^u2 zB>jii#KYES+n5z)agiT6?*|!<;k{hDPYjtks_H~}oWx}<8AF*1lRhK&A>{Sr)G-TR z|Blk7f5GSFo(;y;JbweQueM6&U }hFfxKjU@M|E7eTf8ZQ@)8J*F3 zA--f4J-or$%i|W$UiuvZg5BD1K)xW(eg++);qKSrH{Frh6rM?`q)g)7^9coKUszq; z@be2PJIu#44h6R3iQ}cnj-5Ek3+;tYkv#vA=PJaGJG_JVgUUG$AGL&$I{_3}Gd&i` z2Y)_iFWdROl+#f*TJN1YsG{reWc>E9wkgP34fA6EN2Zb2A}ox!w_4}kfv*Qz?W%2F zQMnaoDxjT8oR8!5f0#paA{N)x*J1;YCAUQ&*{})Pq+J1Z+KRc((T;j=q5i96&KcU_ zb-d!r&ZrtDv_}IZBd@3N3sZmpS5NbSE^3Yd8Ma+J-|RJcI<2i$=f09+y=)#E=*6EQ z_iaQi#7X{Ds{StcAx@pcRAhl1pw@I9se`g_N~0gew;dA>AE+vUBqQ)-XW?vTmmiJz z1M#2bd73ec<#G#iLz?uD }ZTczeZ!l(U8{+M~5kB{L_mHb$!cRin*#4X=MUm?z?xSG@ec2ntaGh%xq=6#r ze8ADO_9dgM%^p)VO|mRVH2uMxf#NeJhJBOM5xM)1MxXYqVo5l-?URTc&tU1qG3vhi zNln9dd24I>+j($(odJkWnwkdp_SnY_V}@>I#x@fRm9ib2obnb(@wKwAScA1_@|z)^ zB3XC@3KTL-9ahshJ)FowSJ{@S_(-bolSSk7VY#ooVroj%s@Y08`_jC%`ol_}2jZ)e z^XE =k`j*?M_0&wYk-q0N zd`dWGrvm?93!qG7Wo1 zy1qD;W21y&@&oIhrqj__<{u!HJt$Ptj<&{845nF4d!*LRzn$C4*`2 +{xJ_z>g8JhD2N6P*Ka&vXuut3 zk!*_64Ys?*>XCI4HIHV7AyfHZ< MZ#r#I4n0Cxsc<*C`(PEV!B zpMcRsE>;PBj?)(oz^;{X6R77<(^RIgmC~wSL}O2KT0hbqc*>qI#QgiX4k PzwTu !tB=JVJp6?c zCxHebQ(`~Ue1&U?A2&XwFNhR52nq`Rc8*HL7h60yh}9_51GoE7)`s?&SUF>71k^iV zg%6zk<@w_lJy!y!H2e6hDB^qY#eD+(+%s`L&rgLuol;u(sEUk4P{mPP$VYM)$L$WC z=X`JXZBkahxqBLyldgpN_(u+A8opr1=v+p|@Q3@tO!S74jXpPN;C*q96A}dYb#(LY zyu87UH)m|~J`@?4jysOn`yp5%aT#y)r=t*^eMiXnOJpOp2Cz>&Ew;k)Z<9e%kAeJ1 z(+9I-Vq7Y$i $R*XW j=j=fM(4&pOXRa5~&)u$eg}3U{P-g&rST|4mZ!ZuIhrW1m6KBPSMN@fu?qwuI z<>=wj_kn|7v=`*sfVM@JK?zVi4zFzXDLp-rjS>LBsuDV%P6d#sz;I%MA&ul5RG#t7 zWhP-rXG%~hu%M-0V-amJfZ@#$?oD(z-rWe_UY0GajKGCmY^^zp ISqcvQF1i7MndvooboD2{B#V-?vbI(xLWPfn0@Vl(4i1o0h_8 *JyI)W^=?i-OD)T_0TnoXR;3FGkB20*@Tq<=lvP_~zcX3&~ zz0su-onINsWb5loc#{W%Q7ba6rf9A*MfVaZ;&Lh&Z75`edGTGdtvmx^!59-1;nSg< zgwzZQy6)LIIOGDY7ELmv*~)79(xQdbc p6bi=d~eG_N9r?DE?k mb zj1p&X6GL4vFPkmSKAT;c&k34bfO_BGRQC{Of}CE- GUM7ik3l&Hzai#8&bo$ZiGNl86{i5u@a^@5f|Wf%8 y 6WNlS+#sBamNNNBfV;bF_?BS>_$?5 zMn{YD{rDPZ73y?sDtB1y{9jl5&t)jBoVYd}6K8(K%)>D+&hHo#j5;0sQC yN)a-~KNobk@#PGBOSnUBn7hDO*I?mb<>L zPDXb!Qj*qYuD;8hyWP`wv&@V$)y~eY)k#1d%(u1VZhVNuT=If;r3g`to(P=aXR*=( zhp^yN 7gS<>w~!4tR@bBO<(YnRNJGe+*aNN+uYF5 zR9PQdee87-=z?_{bABmi27?3+(sJMuA2@#gCiq;nPYEuJUJV403MgDFo6qNV%>7?< z8cv+}%wJ`Co1}$ jG4#gO{9%GoArjt `ysJi% zdbHrmacf|(zz)^nFa)DETI)*79zIUZK*-cAr1A%kF$d!nYeytRKxMMxFZ2aWut2v> zMmv`KwS@R8#hc)=s|C=fEV|+EtnMKoe$X+K=#P kq|C@k_wNaY_CNgSpN{X(t z`WF-Z!WPeJ AnGm39U-vWSuo3Pa3vZ`zhTVmDX1S2q(dim%F#`*$ z1g5|;8KMzkZrDp`{FSfQXlJyuJl-zfP^=h0S=Y22M^Mor{L`8o*ghysb)h{^Y`<&d zv;>hyhWB_ZgkIq;q%Edaqc%`fNxD1ErCF2MRfD$gCP-{@aaAMSarT5VFnxFTzNvyA z2dPWHR5iLZ9^nK)9@bf2F~Biz_5}=&s@oEN*pkGh|IBuYE-nV40bI1W_cV zz}Fy%P$_9DYp_D6@Z-&t8}y_)wIf_nq!7O^UL^$r_?u^kq5@ ~i>bgtn%(HhGT+LF#^U$@9$n z1pk@W0SdG@T1R~1z(-zm$##|)qGW0Ku|k0TLst0G<0?})TQ4?|LyrowjwP}gr!m-Z zh5@zaH(zX$sF7bkeSJYSmi+B}v9`_eeCE5jiM`lShP)g@=A@b(e%;7cJ+fi}SnT5J zn(UA{Jw0v6nR?K$aHEJ468fDDo}D+^C6T1m0K^d(S;68Zm~3)jMx`@^fSRN+963Ow zVUBBDhHS4R)$%KvgpIk$-zHI2`S{Cj2{>T=L{%)~x%6A9W=nArXafA#iB?!HJ|Z`# zm>Im5gLL5LW}cP$qO?i)R`Vh(wW6->Vw~YOkO{fl?!CC~+Uc26{&(Tp8XXAlz<2s& zVgYtAsDCQ77o2(Pl()z3=3|wL46YwaG=a`E3Y5!7$MKVatP{jX$|EHoodS*}^$a?Z z2<0XIt&gs;wn#H=9+cHmA?mu&FHDqX@-A#9L!K@mlCY LeS7t@0( zC}TJFj#=H+c@pNh)BMv+Z}#H$nc;LnZE-5|?lR8vxK#WlAAP^Tn)h^2`tr0}E~9>V zqBhpp-N! B_Jmy#VSXVn&UaS2!X{z3j Yv0ObJ)LZ0Af0enORiZDiee z8EVy3WOvLn;D_*$zX>63NhZBFSLai6b6uV>#SShmh~tgnuf QJun92A`64bi()pB7t`5a{|l_TLHLgQJ2I zzmV7M )Aej8}-Ce3-(8{8R>Ir||K0+5^ z*qhrt9OP@**}*!zb;eIll(vpQ{F%b9(1@YglCBh>nb7v;L(Do%X4-x!cH3W=2|ijE zio)God;nN^jr9ViTqJb8CUjXdIFga7IwG_K#eXtf%q;XcG_$~vx9P0Bu+=Gz4csL^ zP~57O@JB5@O{wq9rT6wWN3yib NdCz z1GyhEGGDhky;2{`94PXsYykgyk=6KMe@;7S=8GG#13}g=na4|hp;CsDV;=FEHUIZG zD&>@y!V&p!QG)Ot0o|wH(u2g>;Wf8BeEz7Ag7mj2p)x5iX`ia+Mb%f_DAfftG{6GB zv}80LUXtr3C7jE7z(wL;)Q8e!Jj#Y>G2>@zx$}XKjD+&VFPqcTUWjzHj=Wq;kQ#** zhMMO635kXC-n7m>0KRO)kEdKL!1;}ax)(9O?zusV (vki}LPHJUh zJECRCw_a7}K{CMzlOb?P # dXZ9GHRZRH* z9&E2>K*mKb`bv_P)WqxZIgUiv>v^dpS~_>NR#RBILZx4aQzvzCwY70` G^*#z> zcaa*2WHGDpGztaGD Azfj+g<6OQ1wdpO^e^i^D1&{UIbW0SV|!8V+GpZ)N$ey z5x-t45EV;al- DeKZ1s$lX(LJMezU}BE})aQcW z^j2c2ul@U%QeP_i5oI?6Zmn%vMFte#8xR1tHCgfSJu88S`ZIG!9bB6jPt+I(`8)oV zQjBZZ3V7!n7^|rkN92Ix8Yo2Zi-k*@Fg>aqtpA9i1*83YIBVKqFolMpN>=6zpEM?s zV+fY;m_}A*&T$RFM$Spxmza}`T}3D&g LNY1&8T{ z(L)fVV_)zdDrE^R9j DP!b5b1ZWlkbss zL+ +91sOivuxx$@hsWIG=-6$gF>SP}a9(^lxY`$gGt(MMXTeDTCKG zGpZlzALAGjk@WXiWcJu<8ETvUdYLFxl{XEdMlHz%AU%czjD+66L$?9YI|C qF2#=yclNcd3uuKL>arvQ2i`a$ht?qyoOX~@blzjcB1i_s^qD WSCYY-?E@lh Vgl zYr+)Gt6R3vUhza==v!xhLK@9*N~A-hqYKL~FDOXZmiV8F!v>0_DgUz>$sIvy(&=^V zg0ajXNRKT6R|Adp(@FhffRF)w1^oqZ*k*6^F?G2PyG2?sDJ^hAuc&XY9%&B$W(@bQ zWbHl95E=#@F|eH(j;rp9Eera#-gTFH!&$;cyy+zyj-*Bd)@u*;KeLWmy4p2@yXMps zmUCqxi2gA(p@|(UV4oJ;V%f)HW0HpQeBX?}<&eKFi?-eUIoQqwE8l9YDz=V 4I z@(cTpfUkCh;JTmu#Rt?LfUQbaT7 l|-y*&~# z;|HAXhXk )C1f8F+&Yg1My zrVc!(I^SCT42Q#O1^59wNtI`SP*FL6B~MIDTm~lPGJ+sI@erP_Svxs#nxh2XQ)@@B z6vxu^ojwLrg;=C6;myl!2d&7zYp4>EX1de}%~peE^HzML+>ZnbiG#OdGGeMxfvDt< znAxU>sMRz1G#O)w$=2M`XWcVcc9Ppjjpmm!;K0gmZaqk-J2mxXVV}^*SR~u=e{dJ+ zZ{XWKEkzLNGg3Ylzy6ND8u_;G?Byk&wE z&uS(@RL5Xi)qJ_d0#G^FYP^R4%fC>{I{0^J1V#Zvo107uV~wuuy&-708ihp ?emh|RB0<~4Q{ zX=YY3Do=Pr7l5sOUb1LBex1Tbqs~G%GMMR5;va}=ari_l ra-SOQX7}v=ue~%`I z;CMz#(!0;k@e-(ad+9mYD?c|Jp8?j#rUc{=>D;dS^CtX| yvLxIL?FDQVzU zk$pyN-ncziCdY(tv j^b`+o4L645Edb=We z+XI*KJh}Q-LaUV&w?VaFP$==ZeWjc2?%J%wiXrJOh0y>-UnTO4?%bR%$mLo|VDfQ& zo$<`~8XV8c;>aGJe39!^|Di=lfEUrz+l%3>`@gc*;JRO`q*3R2&TeLA268%~Sh=b? z@aC6`b{yoVfJqO`HPElv+wbyNKc+RUio_#Q*M3fc$v?0!4kn8Dn%opv{M^)EOmm1A ze6)`WMdvndZpbqx-6;!tu^h)4{7i7Xe-6GcaJ|))H)?RfV0;K@he$%Ob9)ZjSwhFk= z$6urJ&d6s_N5-SN7>S0Ie-h-s>M5uA1W>3W{HvI(n66ZkwHR-LZB80v(VvBotMgj- zgAXCQyVUM`ad_tJ?2jW*;{(}aRK8O1`!cC*JRf;d14T6Ycxzh2HcvfHJep217&TD3 zySln0YXSoCkmFI{h@edaBCV$~Qm?GrO^9LGTE;lCF4OX|i^l$_(y&(NEo)iJu7HG6 z&CZA%ty X%K4kwr$gyaiUG~ssqg%&l!tm z(klh7XU~|6RCvOwB=W*M+=a5OcHZL@z &w6DtDrcK1jrpzU+v?>3FIx4slLb zMvp0~LV<(SqnN^xshFx*cye{)T#Ha3UB1}k;omrzFGEk5&QS)=15dWxqI>=@wdHa~ zX#-D)n2ZfaFu%{MjVDwXbzPMt#F;Qv{S&Ws=?A9pB97Em2$f=rqLgu}eh6Oubccgh z&X1Li){)>yUJola+0j>P|6dCLkc?>6Pj6a&4g{%Vz%~zFR|Qtq`qBE_DMspqF}c9z zW9a%D00%|rwVu=GM~TVTr#9#^$C3xLl43z_UV~fFwiUyF*L1ECgzcKtjF>1pGvX*q z5rE2!NO6^`W(FJsi2&z|VprxYwVbJ&Z^8eVS!Z4>dOys$TI5pY;3EEN;>~5$1@bq~ z6#PgvxcCj_DKzm)--l*$@8B{^(kf+{*;*6E;YptoV1GH@r!x6$6H@&|01)BEV*%<) zD#qy=wg*~CBm3u@=T{UMLTaBuxx+%+PSBli)B`y02SognN%}}2*7)AX>cTF|fDXm4 zoch54-X)NPDG qffnd$#Ft})`5dOYT}KA`sc)WXInrdZ0FG8 z`IAZtqbSiK_0K0AImy3Aba-Ka{Hf%ToE;BXi}mbc?%SYTyl_rW&&wv}Gy(!z=DETy z8HmD(c!hBXsgV!pzJqKn`%NHY0|qc}NeNqD<@Iu5lCc&7Typ?GciRU)M;!bw*5E8P z4Z>WM8z7O-oys9r#CEUD_zoa!S?dlB;n3`Cuwr;89cxltu}oh7>#X$0{NkUplxAV9 zKYeW@mdd?uWYp;x@~lUGJ42o6oC8;XZkgAlfG?QGB?KuNK4RWP*$I;Z$QEc;Y@08F z58bZ26xjfZL&v2;PPM=va?hfr0f@>=WCJAg1F{-!z!vs{7z0 3{aTAAW-mQcePO23^SOUo05iUEbW9ShxUdfTPR{-shAL0L61>(fRw4w|l(x z?FQt=CmZ~!+v^QcWN8O-jR#Q4p`jsu0fFV^Wy3iOWCG+}ePHO&xyr;T^^_iFBs1hc zAiy69?8RW8Y`dqbQY-*1qh;#dVw`yS%r>XkSa0(jQ?esIw`bB7MFUg&wZuP2p-O&Y z1r`}p>ro;Uajo<7aL#q%5WEkW+>WnHS`GLi4*T=MjpqP*Qi=Ku03cyKQ)*_Qb2Afz z%~RUU(9elXHBZ1q@M5oF|KH_y8OR4j5}!f=d`9x(;cU@D3-a_fgUE^>&Fz(^16vtF zgndHJ2S4q3%oG1}(fMBC*D=c)UNl-0S1&^s7sdKm9W{@7_ 0zTfpc?nr`Z|?a#~5DkJZ%=m%A{1E;nBQB+WVt z;{BT+E&san(9dU#f^LOH{J#PV8Q|3Q%|Q@kX(4=9_6_TGJV!dZA_@M~@3TZAMw6k5 zq5{@h`&;KYHT>mDnu+m`p0eGis=q7RGGQQ3(RM`TGV=5DFDm(768c`RJOhT>wlk{J zIocX$0gX=J8r!OBfSe(*5GZp1GXU9ECm3b&gZrE<$fs*e>U6;2pQ*vP4Hy{zR r~Iz!ff^L1bQtv_M`aCxZ#qY+ zvo0$=L0k=d6zp*lSOZd?m2}%?ca~XARuUoiqd)*hvccUchlfz)0zj| nW;Kr$JRT(#g?KA=Y?FC3p#??SaG9sH=Qs5`i=^Cs(q{#`|pY zJQCdW^;r*?`ID&OABANycqYk=uzu~IPc1BL?9DZZQrKMY+WM|s?YF!LYz0US;4-9W z2z(SEfs?PrLB0^8A~d2y^nmyTcX?doa5S;Xfn3p~hr55{k@NMO3Lnre(3Junz5yPy z(f!Ofueew#?GzB?yYN}&rJSf}lk~W}el16TS{*cqOY$#(m~39P5vp(T+Kb?_YNL!t zWj?1-PKkWv*+8El^9(!}GpaaV?3LI#7r)vUg)D}DQP#tt6_qZgemFn2eG1aHDL JdYqzGY_B@^| zTPw1UKh;=&-A)618fjyoHX9xp!G}uDB!PsoKjHkY5S5pjRx&2E<7#_CHiWiBQFVkQ zBzLI5jugPrU_u<_d<(*-5tAt02!cQF`2Vo|JDYh#C6Y0^ZBo|2qZWtvPO8(T$!&a` zm-xQR!?S-=Zwb0fGd0&IV@lVjWxi{`P{tLS-?tCd`6M2HsxSRo91c_+NNL~M*_i-i z-SoR78}mcU)u^_~|IM|5)BxFk;7zyp@o5%`pQz^r?W5#3(a+!h$g)2<>37=YXs31~ z7eWcq{AmCRGGERgrs@*Aicr>>>N-Jv0&4CPSp$9$E`93g-4qg968KG61_C;r%vmPd z<>s96fA#}GRsP}U9F6zz>C>k|o@Z9VE6W>tFI8k&NidW8>49@JdYp!X8g^e%LVa44 z$|eE8!WEE_5I0W?F0hbB5)?4w-5Abx1mtk0Cfn=!JD42sLSe DyW|^_kN}dZWk-Z&h``he4eJvQ8ifbR>!Bvc)-&X9Bhv0&H&B@kpp}avLZm0Gq}_ z3s}J7&v5J{OC*l^sc*V9*;sR_iJzpMrAXdhh_sdR`;{kK${t_nJM`-s%$6rnXR zAcan2E*<_B!-%*6?Tjjs8cmb3Tp7T1O{X$pahhz>UpB8VPaHgD38Yk{YB#DtPS{Tl z#S>dRIZ3Rsz5r90UXZn wnJw{aqJ87C+q&pfa-V&YWZi=K5`*@Mot zqn4`kcvoDf>(AVGjl9X)>I8=QQ(g?fHn{R6@3;h2337Xa5w?uX?M=FWruUSSF2=`8 zL{#1n&c`~z;yavA^_5h*IX%ScGfY1o21D1A8lK>YJqFxcdGgf5&s<&o88K@kg?JvA z*KBocpHOHkRkBt+i-sa}-pEJ0tNse?@rd_*!Ls#j#n3t@^TG=W=iaU+7Duygi4%2g z7Q>AQ RSPY=}{U+(vS#G8#6SKI&4?Ow( $Dsnob}V2X2fmfpCzlAL73i>U-F5hTB}{TN^;fY~Tu ziy>1d&) DR?l}{*za`!&<5T={xoGJ zDZ4*xR!_d;J{Du_Jk-Qf4w^7FJi6AZ7`^!M-JV@_+qmSXYDBE&p=XfvVNkV>)5R0( zWd@SNQdUpnzNg~_WoE|T+_*l%iKWY{soPopdq%!i8S$nRXRl^R06 u$>X)Y{D#r7;wwz(}-|)%)2^eDh8XLnN49Jc*T2*VO*06BEz{QsB{Q?FD82W^~ zK}slRW }uMZwoO%3>@AV|M$e%~X0xBD^T%y^NpGMY;3QL6T`h4e{z z)IxzJlG|%uzn1MYBy;1z{6+@xx AW7@Sb-@ zMil4ZQ2p2g`*tz7_yJ6&|E~uK(6P;qbNFMBahAaVV#?}lqE!VLqa;xs9h?xetSMxe z z|B-*n2te-eDx+#nyZp{-c+{ zeojjc-sOiXd0+8jx&h z{S*G;;#|gXXDKi$nsq7uB`Gy#8v3uF z`d0UM_qPN!f|`vPucXruK%6E?{Q4O{1sO=j!0ibp@hJ{>4O?neCL)`!o6ID7!i*ua z$(6BJDL^pMR|dv6UU>G-*V_tIix!V0qoC^>AM4;@V9bi|T(n&<*^b{+O;ea(v5tLA zh9va)jiuG NJy86 zz>dn>+OM$6XQQi9ohvI*8|*Q$Gc8FyR?=r*-sFDfu+F!7V(Ix+1!pJRh%TYVhVMaN zEISoHU^EdeUbx`0PfoXkR-jf-wmw?B{9ODYq$K67$I*~iX{E3N%!S}%du!U@Kye}{ zAl-(Hl3vmu;(ey46P&T~GU&%#b))osm1Mm&)uc*>tr>yK3Yok=5g419uX7uxtO|5a zlj~Ir3Az{D(Ol{2#cR>b%8~kTG=4E+mq?0Q^XCk6*3U 6Zf ze0#0l?)iS@5;rU-woXt>5O!sZlWt)-#+HrKy$^QjGSogRdGzG_SaEdZ$-+v|)?6Lp zw>zmF?SWxDIp(8&JhuFKm&uk7HO jr-(lRb25J_O;LF7Q?pm{9rcueV_WSnGPo5EPp-?a zcLf_s #+1CPnx<#TGqn64TK$Q>*DiS5n`&AAamyL$m`We+$mhzvi}|!S68F$l z;RB&x`Tf8-WZCuI^fJiM7N%_XG--wGL&Micu>x2FQyyOE)#?%WUXf$}AN9{=f=vre zhGF)zf8B+fBc|A>`lMC$joKY!q8lWg)q{Q7!Y<}?zO x@kD&UAqL*SA-Y&gmy(T*ZSQXY@EnkT!AYH}J$>_u zA1p|-&m!8t=H^X*p){4U4;zA<1=ZbSEES*~ )phaBoa!xZ31M8w3PG7Qu)|DMj=)jc_R6|}am+41EO z5#f4cEgOL6fotg8;V oLj-(2^SBF}d2Wt(f?mX`>l z`+P=OzG9QnAqCh4bUr{T2sz|viN5d(B(6a7{nAWWZYe0UKif$Na`EtxPq8js7v*V~ zI) FdX0v{?dq!!TCE2 C^U0m*a(d>UTlxYl(<|WPgbJJ`B_dNXZxz4gi8Uf!*+!0TC zNB#P>$M8&vq%gay^@YQP_`)~03vFdtLlh;Ie!s?l6_Mky6zCcI9E>4IVtbb3-4`Tu zd(N`Dz38TlERn-2(-)P9N>3K&bn|J9<{gL9)@P$XAXb( Zd2e$;rXX(qdy z=yv6=)WkmA0-|^CxF_MXx9b|rx3ulFw~q=0O`ijj(N?ACrTR)Cd@^>- hdViU< zYR{EQ5D~8Lvl+4EVZTZQe2K!!MJ3hv-QCz{&j?7l-obgO{HG8D?fS(3k~w=_-PC-f zuLznpDh;^bK0_RB_+jRb_xl5{FIH7@n)UW&9h57o;T=gOwBLHm8*V}ZaE(>bHdJFY zQ81rSk6MBCb^ldEzYiUM34=~LB+n0=508#FOloVxDV}`tOV}1(^CU`DgreB6%7D9w zvprQ9i7NI*F!IrD f7i>ylIy2|U8jGU z>BU$1UoBZRG>v@=(pA?bd^wP93w~}KI< xNpt 2N(7gdIVZjfwZ6{Hxl#Q2VQ^I}Twpkimk-5G%s*(#LNuca#y00- zLPHC{c{$v7E+Yl)IMtf)?`Cs>b^e)6O{%kDZ9E)QChD-0F^n;Cf|kf#0)U(g^3O@0 z x4!K5UR z4N~tGH6L0TDrT;LEV1 t~q5EEi$SrH%1k@tQhMv+s0ZH zH5swoFt7u{=w%<+yX^MBGL(FYtEb^ iF0#kQS(AW$voC?Q=_ z@oNGOVqQuKUt0g4wSp`t-oLy2*|~-OQdv{;F%*XwV6+RMsRzgbe0>&$o3W=o!by93 zD`&0 XYvpw3rM^dvs`1kngaGWgEYXQ$Rwr5pEcsob~ zrzi5*uLfsm_HAvw%*goe`y;#<#q>0lm267)g-S$E8BAoc4Jp7Q((OYuF`^fhh6O;@ zv)AGjMeKW{o3HiszzAw0K>CDNB6)$i<3HP#I)Aq87Inx__lhsC6WE^fv+eUQXJ@Ua zui**Ite)I7>chVB6gr}YablF>gqc*^D7>_f2rx3vp{HWNjTs}FWaa*-TIacB;tZX& zD4j;ikr7*7A#T!5vAc~ >erMurXn!yJt09L(<_ShAmI0LW`o`{OR?2LD`CXXNvKrT+dHDA%=boqH0c^ zoDol}Nqo1-hKi`eL(Z=QVKXx!p`pCiM~e5g6%@WwYp7%`M`^f*t=i`H&w&%(IBvK~ z!*J~BlX>nC(KTvm8sM+ 0P0++QROKxaJ;|F5E`ya0uX0TXsLfTU{L*+~Wh=25baS1CB2sZ*s_&mLr$&OiL8q zoL64@++*t!DKYTlnZdtb(9IBT#nmY_&LnDjwMF<`dG6M$yJ4o=8D9uVwcxg`%bXjI zw`W{gc3>dJ+e~0W6iqxIhLIF*_)1WW_zifA|JR5l<$F2ikbe7gz$<9N3#5%lUv>ZL zzE33o_~_tEJChZ6w~cG9!bgA9Wnlu8YNV{HZRln3h{7rf*m22@``epaamkUo3dLMH zjp~eJ Iq7V(+=dLSXivdN3j0a&eQ+t8C7R`Vr63JuUzA7tmdhL{DvGP#a>Do@} z8>?U-#3avh(e#O1b84a}Q_QD7qXZF4Gm;(>2!0mxphn?IKRsz$lX4=L;eleei%$Vs z8?d84cVh;wFBy-Bpx>Pv!H=TB!bKz`@@!K_b3Xb3Dh&!KV8y;}>`Qu6MYzYWdGvy7 z0QA)~>#G~36T&D22F6ED_#>gT)KiQRBj@#$WB=ag9uc dMmOfX6booT3P^>Vo?xyqN}p$yIDl?tN<^QecsU#(xz*R>2mTf2UZZU z5qf}gK=liPX9Sm~W`;oia3B+CJe9Q<+j~CSlT%X*-#a3JO>&`gF@AClBoH7-288cX zG_o7dMG3=}3 |z9ZqtkahmqyL z_wR^ `gS@XZT3j!0USO4LRh}Q&8Xk}$Z z09{SQrsH|)49f>i;Ydd+@K=z5d=b%7-5^=!1Wtu|_rCp#pvZ%ohb29$SMbuk_P(zD z?y##m@}_e6X5mN@KR{!efnzZ=x>M0u$*jYO_e}H4+3d?T59t&WCrV8XO|3{}f~>qg z`4J{NYw*QRWdCHD2$=%v0EY8FO5q($KAF{^e$u2s6gg@ANOy7d klWXY$A5~_-OcO(j7H;v)*IGPp?;Tp%k<6LL!}P-mh}*cn-Ne zYPJ&CN?gelswPxP91*mM&RWKor`1i U?7c zKQb@Yu)1GVm;HI8 4KsjPlhDW)(r Rl|{LGmGMp{U!g@Th(xWiu}~ zgt~ya2Wx9 Bu!c$F1|Ru@%4*HmWzDVOoeOy8vbyL3 zv<8lPrU>7h0@_oU(5eiTdwsBuZOWhLvapjaQ?yFqwEXb(b;d%6U)}~!hVTxzD89AT zef0je*ng9d((uW(tfByeQO$^0QEz!rF98j-ZWq`E)(UMxkVtxnTL0?^1rGto60!Ld zYXIPo&Tk~q?{^Sf1j%x~t%mMDBIX`%XEAvbQKmu~FT17EMI7nQjik>t!W5e>n6XFW z(ByvO4WI!F7NkWqE!;F|8?z)TOY&RauKpg?7jtkVoPE`vsK+nN5}5KKe;t $3dx_i~;KW)ZAu3T75YiK0GvXW9NU`)i9VLh_+0fJ#VAYyN z75!D|?Ol(Y@VFh+p_aqf^>VJ=esV)Lpyt%-G<8SuVb`e3U`f#VRu%;GvgfkRw 8(h!S1`rMv$?D5s0xZ3*T30Z(`h6Y(xF3t7B0b0`OfEA+iUwDNP|;W zkZmVXZLOwi(pF>qg!UEigvoVBJY(&y8*jZTRmf7%Z;15&@(If6T&3*YdIxphoT!X7 zolDq3mEYnn#WH(|Oo3oiH?^gxe~pi&U-x!iH7d_4R0&YV-0|V9ea*_L(Wo#Y622D3 zl~{Q-HwnM0e&?#ki(^zMWlda;??Md<3Sr##9J}% d_-*%|CU;d0~;Ae MY>m2)trB2?vHL!%4?}{S@ot;fcjHo4F_v(wHXV27WqnV8} z-&h#we2%DmyjV+_#h7mVu36OHHfKTTxXg4=@uia=y~NbwYzv)l%R(u+j>Ry^1}kUQ z3f6O$%zP6h<%nJT0zV3rDIyNp+}ykt)Z%HvpEZEUo*k#5F9S5o)n$506cHZYN%-tn zDX}l}q1rPa>_Q_^y|w)Fd^B)Hchm05QnJX}u)6`#NKJrW^|@_}T~onQcGw!{(?bJ? zXwjr8JWkN^-` xb5bSWoe@KK3zrI0vddrPfqopE`0O&{Udv0C{0m zybyeoF>dfji*`t(P-z_Rwl7ioWb(rn_6;F0@^E(YYq0R%RbRKZVd)>7d~`|1QRsgD zwk`-LouwG@jYR2RFlDUVaU$52qhl3sbOf{YRn ~E3L22l)l{X*)*}*@VJ)$Cc+#wPXNn&Y?!~0(asHUYvF3#jD zpZv}9ii4m2v$gckaViu@gpC7lVAtM&FDy&)qm;2~Qty_gbBUi z-Sjm6EY7K_JaIq)-+8jH2pR|Y=@78X-zR;ygwox?IYO2SX=8>ON8j&KX%k&iQoy^w z8x|J}jyVFR)wa<{Ebl7NPMVXuLjG!;NN>A$UEuj>x8`M`&(2^-R8cCn_m0jdo2q{Z zmvNg6Z_i40ikq`=sA*-27u#*5X$O5jIge?zvFGA?rlx`teth9#=h85}o|!NSie>q= z6&4BBm}U4~4!ld^n2-M+42+vzzA{Dr@l;LY;$7Lg>Fi;&3YnjMOmt;9%RqM%9oabc zq{I9BEll=&Lo6~3BOG}nKSY3Ug-(~P6MG|jaPf%30`e)4M1pJt+NnMF6%p>-uv8Cx zKB5bflKSLVPD!iEvH$SJ{ %I|1ilkg5vdzlu(!+kF+rL zeX|R9$>WAY7tFg!q@N!%QZX53b;n+GkeymH?;^KgoAG}2#5Keg)&A{GyDCSXV9;iw z*k4g0%4;A!>9P9o{W}vP>W0h))SJJ>Ki!Mry|gV+G8zrr00;OJOkZiJuQzWnCX@a8 zSdJs_EKm5ud{X*Ef6w9T>7(E7MZD7dAGj|mKhJJa9F@ws_=#C`c?4~)DXgc |XkNH=LN3w7WnAE#fUO|WLd6~1CfnQ6WGC}$_EGR0c?kN5MP z;cGl&gWGHqOVy#*C;ucA@oMO1Rx5m{5)dOfV9!fF-d%B%OJV`E81zJA)0$4ljvh@j zo^5eynT)_a!TW%086bw{pmVspI}OSLEn9jl?^A}O?_FOspvus8Ad%82KOqtE 4tPYiUEl{nWQ%h9-qqh{+C@4wJ()@5^bhKt(9Xruyg-}Z(xsH2pWI5OfVeUA(Y zf|r+1h3@HsJ*><)D1gyGK8zsD$kZK3_=eZa9h-i&tU=o{VU}dT!V9y&@1S4k>w}eS z_bYdX_i?lfpnFpCs^B*v!wR8Fij$Sa+ryM2l-6^+;rqQC52G&8t~(DsUQ8q*^^er? zG+$=!A Zi}^@Mx+2f08y -0e0E)q)kUMb-LW9D;QAX~FFdfVY zm^ZZoSgMW6a)kH!*!Zanqz(0rjTQ~Y3+cl`Ivjbmm9@kl{HeSjt+{k=9I=`Cwq4Mj zVdRw|Y7&=PCuME78_hVzgmnogM^p5QYUu<|Dab||L>*3B?d^8Ugmq!xd{M#9JeFGe z9Pe6~8tSfS=3538?Li5DRgRj)SY8nMdM LHkaZ69@eB%61(TfXDz z;gbSWQp}?4LS}ug0rRJcY*c9B5og=?JM-#1Bu~8TRj(GFIuR*2?o!EwFm{ nKail}v*bu*7^ScKe(XKlJfann$XjreJ=l`CupP`PO#h z$jy)&Gfpp{`D=D|wsE}c>`W3G!o&wuhA9wT8FaCO+zDih;75JV&m{Eq_2IHNB2|HJ z8mdKqt>eC`OhrU{g>G!Je<&vUOhq`0Gj iP5lBF>ni2O9rX5=I7;6V8K8GDPbz!(bJw5AR`wJXGgHpXt z>f00WlXMnk<1=H*e_dQH4{yuTVd#?YNWzET+!%GXdB5{s ?WZ@r%&{Wa z$^=VAPfZVJzwTy(Y3$@2`f?$cn?WBJknS$t-u3iH$47e5ya?0k47lr?n_)veB(5&! zTP*{aj0RbAC#URFvaQHl6RUkL4VjvwjRu6t1#!4)F9Szcs}fDTmLTf0$CW PMB-)?!m75ERxI-dK zVRyxq3AN*T3pXyfGh`Hra9f}Iy^>p6ButyV+hG;Q`{n#v=v9&~kjNSc5ubwpK vmgv`68HP z`s#>SrGL!D#0zB8 #*yp`#1r5uRFd9y zMCaRrA{2~h_w3XUTm_VYp#P|w&Kq9)w&p6f;&Ck&-epPjLWaPQI%b1FXLy{)wVEi_ z0k^J ZPUF&J3PJ@pZFUDZ-)=umA$435?4b_yfkoL!GWo*Q-#b^0bdi@qJuG>U= zXF%-LZ2s!!T6Rs)K!P}}@}N^L5Yk#1*3QObY^EfS+I?JJW(Bs#k^n#{klgr*%>HYR zg1$M>7%*X-UWfC4h38UivWD6GExn0bo4=hIH(6r;`kgFKUQ{RcTY9DET!@_J*A`*L zL~o0w-CLwK&$1Np$Y?C88TI(z7={?#U% xEEWn?x|ONq$1 zq3^D4`ZO&IX}5F{hOt)9=?-06+`jA#t4o0;Kv-#uf XOdpO%@gU!Og_L`$3;si6~m)T`a(b*=mK60KuBQ-6AeICI$-Chk9HfAP*2gPIp6 ztMNMSuN~0HI-see))&L^nQC&zU`Mq}I^Ba_nqI>fHZYZ-5YqV`@0)$J*x3n?99- zL1hWiKZ0^c1X!$~4)do~QQ9rM85@-gjrsa*1s6YIQYx_95BM(^8P}hiYoiuY!j`W- z`$>N5d^ D*>FLQQC)yti)Pt#SuYp^4 M6$UgP&R#<$ex;Z=PS>AXKKTS+-Ly?4A_|z;KJ}jaZ-4%xEd~{` zfo17#cjDDnyS(J7Gaoagne`9nv7z^NaBY=*C?Wjm*Zv2oU$-Ao$4xz6>|=O b)qa?VVNC)=%J^K)mLph(>;ugjBP z*|8pU%ANcp();x-hUc11YGKCCb2y? ELDCba)Qx8q86T}X4(9e)9&7taK3=ltdz{H*>ajL=flXlp zGoDSs6SMQc&bHI(YR`c<0dyqOd`a{JUbzLY4=+cDD(d+ttVb9~;3ul6nE?`p;a$OX z&T~oDllrcob#=kq(8H@jnvl;|UNXbJ`Q;APaP-AX_H8G%@}1@zVK*^zt}^poHy>0b z!iK0Rjv QB8vNuJxASb31;2I} zUWj@c&SZ$Co@Fk2{D4au98u3vxBs$1N-*Ol)^k5*$E){lfhzI`Eq(mN^tGoZ18-BP z@jT;h;rlnphFD%(E4k%Y&)MM@GwN9a0S7Nb5dw5V8t;5dg@Jsy3c8%U;sJkf9qZFq zH{(R!V}``X>b~Ymz)_4P$<-JMuEGoR%cMeGKzUSm+q`^cU3V=?aqeLDm+qiqrO|wD z_6`-p?MV%Y4{%h