From eeb1c469f3e63d1a7279e6dde221c0ae366b19c6 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 May 2023 17:32:02 +0400 Subject: [PATCH 1/6] Camera and editor improvements --- .../Sources/AccountContext.swift | 21 +- submodules/Camera/Sources/Camera.swift | 3 +- .../Camera/Sources/CameraPreviewView.swift | 2 +- submodules/Camera/Sources/VideoRecorder.swift | 10 +- .../Sources/ChatListController.swift | 53 +- .../Sources/ChatListControllerNode.swift | 9 +- submodules/ContactListUI/BUILD | 1 + .../Sources/ContactListNode.swift | 67 +- .../Source/ContainableController.swift | 1 - .../ContainedViewLayoutTransition.swift | 46 +- submodules/Display/Source/DeviceMetrics.swift | 8 +- submodules/Display/Source/GenerateImage.swift | 8 +- .../Navigation/NavigationController.swift | 10 +- .../Display/Source/ViewController.swift | 6 +- .../Sources/DrawingEntitiesView.swift | 8 +- .../Sources/DrawingMediaEntity.swift | 1 + .../DrawingUI/Sources/DrawingScreen.swift | 26 +- .../Sources/StickerPickerScreen.swift | 16 +- .../Sources/LegacyPaintStickersContext.swift | 2 +- .../LocalizedPeerData/Sources/PeerTitle.swift | 66 + .../Sources/MediaPickerGridItem.swift | 51 +- .../Sources/MediaPickerScreen.swift | 70 +- .../Sources/StickerResources.swift | 8 +- .../Sources/ApiUtils/TelegramUser.swift | 8 +- .../SyncCore/SyncCore_TelegramUser.swift | 5 + .../TelegramEngine/Privacy/CloseFriends.swift | 36 + .../Privacy/TelegramEnginePrivacy.swift | 4 + .../TelegramUI/Components/CameraScreen/BUILD | 1 + .../CameraScreen/Sources/CameraButton.swift | 16 +- .../CameraScreen/Sources/CameraScreen.swift | 524 ++++--- .../Sources/CaptureControlsComponent.swift | 34 +- .../Sources/FocusCrosshairsView.swift | 22 + .../Sources/ShutterBlobView.swift | 100 +- .../CameraScreen/Sources/ZoomComponent.swift | 9 - .../MetalResources/EditorEnhance.metal | 2 +- .../MetalResources/EditorVideo.metal | 42 + .../Sources/AdjustmentsRenderPass.swift | 51 +- .../MediaEditor/Sources/BlurRenderPass.swift | 47 +- .../Sources/EnhanceRenderPass.swift | 33 +- .../Sources/HistogramCalculationPass.swift | 100 +- .../Sources/ImageTextureSource.swift | 78 +- .../MediaEditor/Sources/MediaEditor.swift | 88 +- .../Sources/MediaEditorComposer.swift | 105 +- .../Sources/MediaEditorPreviewView.swift | 29 + .../Sources/MediaEditorRenderer.swift | 107 +- .../Sources/MediaEditorUtils.swift | 41 + .../MediaEditor/Sources/RenderPass.swift | 10 +- .../Sources/SharpenRenderPass.swift | 2 +- .../Sources/VideoTextureSource.swift | 141 +- .../Sources/MediaEditorScreen.swift | 574 +++++-- .../Sources/MediaToolsScreen.swift | 3 + .../Sources/MessageInputPanelComponent.swift | 8 +- .../Components/ShareWithPeersScreen/BUILD | 2 + .../Sources/CategoryListItemComponent.swift | 34 +- .../Sources/ShareWithPeersScreen.swift | 582 ++++--- .../Sources/TextFieldComponent.swift | 2 +- .../FlashOffIcon.imageset/Contents.json | 12 + .../FlashOffIcon.imageset/off shadow.pdf | 1366 +++++++++++++++++ .../SubtitleArrow.imageset/Contents.json | 15 + .../SubtitleArrow.imageset/SubtitleArrow.pdf | 92 ++ .../Media Editor/Apply.imageset/Contents.json | 12 + .../Media Editor/Apply.imageset/check.pdf | 150 ++ .../Sources/SharedAccountContext.swift | 4 +- .../Sources/TelegramRootController.swift | 161 +- 64 files changed, 4037 insertions(+), 1108 deletions(-) create mode 100644 submodules/TelegramCore/Sources/TelegramEngine/Privacy/CloseFriends.swift create mode 100644 submodules/TelegramUI/Components/CameraScreen/Sources/FocusCrosshairsView.swift create mode 100644 submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorVideo.metal create mode 100644 submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off shadow.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/SubtitleArrow.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 0820fe1a67..37c5051f5e 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -774,8 +774,25 @@ public struct StoryCameraTransitionOut { } } +public struct StoryCameraTransitionInCoordinator { + public let animateIn: () -> Void + public let updateTransitionProgress: (CGFloat) -> Void + public let completeWithTransitionProgressAndVelocity: (CGFloat, CGFloat) -> Void + + public init( + animateIn: @escaping () -> Void, + updateTransitionProgress: @escaping (CGFloat) -> Void, + completeWithTransitionProgressAndVelocity: @escaping (CGFloat, CGFloat) -> Void + ) { + self.animateIn = animateIn + self.updateTransitionProgress = updateTransitionProgress + self.completeWithTransitionProgressAndVelocity = completeWithTransitionProgressAndVelocity + } +} + public protocol TelegramRootControllerInterface: NavigationController { - func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) + @discardableResult + func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? } public protocol SharedAccountContext: AnyObject { @@ -874,7 +891,7 @@ public protocol SharedAccountContext: AnyObject { func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController - func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any) -> Void) -> ViewController + func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?) -> Void, dismissed: @escaping () -> Void) -> ViewController func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 2a2daea5b1..0a469b2820 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -46,9 +46,10 @@ private final class CameraContext { let ciContext = CIContext() var ciImage = CIImage(cvImageBuffer: pixelBuffer) ciImage = ciImage.transformed(by: CGAffineTransform(scaleX: 0.33, y: 0.33)) + ciImage = ciImage.clampedToExtent() if let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) { let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) - CameraSimplePreviewView.saveLastState(uiImage) + CameraSimplePreviewView.saveLastStateImage(uiImage) } } } diff --git a/submodules/Camera/Sources/CameraPreviewView.swift b/submodules/Camera/Sources/CameraPreviewView.swift index 974f6668c4..3ed8925847 100644 --- a/submodules/Camera/Sources/CameraPreviewView.swift +++ b/submodules/Camera/Sources/CameraPreviewView.swift @@ -19,7 +19,7 @@ public class CameraSimplePreviewView: UIView { } } - static func saveLastState(_ image: UIImage) { + static func saveLastStateImage(_ image: UIImage) { let imagePath = NSTemporaryDirectory() + "cameraImage.jpg" if let blurredImage = blurredImage(image, radius: 60.0), let data = blurredImage.jpegData(compressionQuality: 0.85) { try? data.write(to: URL(fileURLWithPath: imagePath)) diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index e67f6c8773..234957074c 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -93,7 +93,7 @@ final class VideoRecorder { self.isRecording = true - assetWriter.startWriting() + //assetWriter.startWriting() } } @@ -132,17 +132,15 @@ final class VideoRecorder { } } - private var skippedCount = 0 func appendVideo(sampleBuffer: CMSampleBuffer) { - if self.skippedCount < 2 { - self.skippedCount += 1 - return - } self.queue.async { guard let assetWriter = self.assetWriter, let videoInput = self.videoInput, (self.isRecording || self.isStopping) && !self.finishedWriting else { return } let timestamp = sampleBuffer.presentationTimestamp + if let startTimestamp = self.captureStartTimestamp, timestamp.seconds < startTimestamp { + return + } switch assetWriter.status { case .unknown: diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 1d11364e8d..4b0b2b3faa 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2439,17 +2439,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - var cameraTransitionIn: StoryCameraTransitionIn? - if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { - if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { - cameraTransitionIn = StoryCameraTransitionIn( - sourceView: transitionView, - sourceRect: transitionView.bounds, - sourceCornerRadius: transitionView.bounds.height * 0.5 - ) - } - } - var initialFocusedId: AnyHashable? if let peer { initialFocusedId = AnyHashable(peer.id) @@ -2457,11 +2446,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if initialFocusedId == AnyHashable(self.context.account.peerId), let firstItem = initialContent.first, firstItem.id == initialFocusedId && firstItem.items.isEmpty { if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionOut: { [weak self] _ in + let coordinator = rootController.openStoryCamera(transitionIn: nil, transitionOut: { [weak self] finished in guard let self else { return nil } - if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { + if finished, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { return StoryCameraTransitionOut( destinationView: transitionView, @@ -2472,6 +2461,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } return nil }) + coordinator?.animateIn() } return @@ -4862,6 +4852,43 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.view.addSubview(ConfettiView(frame: self.view.bounds)) } } + + private var storyCameraTransitionInCoordinator: StoryCameraTransitionInCoordinator? + func storyCameraPanGestureChanged(transitionFraction: CGFloat) { + guard let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface else { + return + } + + let coordinator: StoryCameraTransitionInCoordinator? + if let current = self.storyCameraTransitionInCoordinator { + coordinator = current + } else { + coordinator = rootController.openStoryCamera(transitionIn: nil, transitionOut: { [weak self] finished in + guard let self else { + return nil + } + if finished, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { + if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { + return StoryCameraTransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: transitionView.bounds.height * 0.5 + ) + } + } + return nil + }) + self.storyCameraTransitionInCoordinator = coordinator + } + coordinator?.updateTransitionProgress(transitionFraction) + } + + func storyCameraPanGestureEnded(transitionFraction: CGFloat, velocity: CGFloat) { + if let coordinator = self.storyCameraTransitionInCoordinator { + coordinator.completeWithTransitionProgressAndVelocity(transitionFraction, velocity) + self.storyCameraTransitionInCoordinator = nil + } + } } private final class ChatListTabBarContextExtractedContentSource: ContextExtractedContentSource { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index be1c1b7053..ae54b3ca1f 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1081,8 +1081,11 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele } if selectedIndex <= 0 && translation.x > 0.0 { - let overscroll = translation.x - transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width + //let overscroll = translation.x + //transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width + transitionFraction = 0.0 + + self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width) } if selectedIndex >= maxFilterIndex && translation.x < 0.0 { @@ -1159,6 +1162,8 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, transition: .immediate) } } + + self.controller?.storyCameraPanGestureEnded(transitionFraction: translation.x / layout.size.width, velocity: velocity.x) } default: break diff --git a/submodules/ContactListUI/BUILD b/submodules/ContactListUI/BUILD index 7d77adb63e..54e01e9ff0 100644 --- a/submodules/ContactListUI/BUILD +++ b/submodules/ContactListUI/BUILD @@ -36,6 +36,7 @@ swift_library( "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/QrCodeUI:QrCodeUI", + "//submodules/LocalizedPeerData:LocalizedPeerData" ], visibility = [ "//visibility:public", diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 2f5960fe20..7bfa65ab5b 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -22,6 +22,7 @@ import TelegramPermissionsUI import AppBundle import ContextUI import PhoneNumberFormat +import LocalizedPeerData private let dropDownIcon = { () -> UIImage in UIGraphicsBeginImageContextWithOptions(CGSize(width: 12.0, height: 12.0), false, 0.0) @@ -369,72 +370,6 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } } -private extension EnginePeer.IndexName { - func isLessThan(other: EnginePeer.IndexName, ordering: PresentationPersonNameOrder) -> ComparisonResult { - switch self { - case let .title(lhsTitle, _): - let rhsString: String - switch other { - case let .title(title, _): - rhsString = title - case let .personName(first, last, _, _): - switch ordering { - case .firstLast: - if first.isEmpty { - rhsString = last - } else { - rhsString = first + last - } - case .lastFirst: - if last.isEmpty { - rhsString = first - } else { - rhsString = last + first - } - } - } - return lhsTitle.caseInsensitiveCompare(rhsString) - case let .personName(lhsFirst, lhsLast, _, _): - let lhsString: String - switch ordering { - case .firstLast: - if lhsFirst.isEmpty { - lhsString = lhsLast - } else { - lhsString = lhsFirst + lhsLast - } - case .lastFirst: - if lhsLast.isEmpty { - lhsString = lhsFirst - } else { - lhsString = lhsLast + lhsFirst - } - } - let rhsString: String - switch other { - case let .title(title, _): - rhsString = title - case let .personName(first, last, _, _): - switch ordering { - case .firstLast: - if first.isEmpty { - rhsString = last - } else { - rhsString = first + last - } - case .lastFirst: - if last.isEmpty { - rhsString = first - } else { - rhsString = last + first - } - } - } - return lhsString.caseInsensitiveCompare(rhsString) - } - } -} - private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool) -> [ContactListNodeEntry] { var entries: [ContactListNodeEntry] = [] diff --git a/submodules/Display/Source/ContainableController.swift b/submodules/Display/Source/ContainableController.swift index 22da56873a..5b9b269f10 100644 --- a/submodules/Display/Source/ContainableController.swift +++ b/submodules/Display/Source/ContainableController.swift @@ -21,7 +21,6 @@ public protocol ContainableController: AnyObject { func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) - func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? func viewWillAppear(_ animated: Bool) diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 6aaea8af80..14efefd113 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -1106,9 +1106,13 @@ public extension ContainedViewLayoutTransition { } func updateTransform(node: ASDisplayNode, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + self.updateTransform(layer: node.layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion) + } + + func updateTransform(layer: CALayer, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { let transform = CATransform3DMakeAffineTransform(transform) - if CATransform3DEqualToTransform(node.layer.transform, transform) { + if CATransform3DEqualToTransform(layer.transform, transform) { if let completion = completion { completion(true) } @@ -1117,19 +1121,19 @@ public extension ContainedViewLayoutTransition { switch self { case .immediate: - node.layer.transform = transform + layer.transform = transform if let completion = completion { completion(true) } case let .animated(duration, curve): let previousTransform: CATransform3D - if beginWithCurrentState, let presentation = node.layer.presentation() { + if beginWithCurrentState, let presentation = layer.presentation() { previousTransform = presentation.transform } else { - previousTransform = node.layer.transform + previousTransform = layer.transform } - node.layer.transform = transform - node.layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { value in + layer.transform = transform + layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { value in completion?(value) }) } @@ -1262,13 +1266,8 @@ public extension ContainedViewLayoutTransition { } } - func updateSublayerTransformScaleAndOffset(node: ASDisplayNode, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { - if !node.isNodeLoaded { - node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) - completion?(true) - return - } - let t = node.layer.sublayerTransform + func updateSublayerTransformScaleAndOffset(layer: CALayer, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { + let t = layer.sublayerTransform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) let currentOffset = CGPoint(x: t.m41 / currentScale, y: t.m42 / currentScale) if abs(currentScale - scale) <= CGFloat.ulpOfOne && abs(currentOffset.x - offset.x) <= CGFloat.ulpOfOne && abs(currentOffset.y - offset.y) <= CGFloat.ulpOfOne { @@ -1282,21 +1281,21 @@ public extension ContainedViewLayoutTransition { switch self { case .immediate: - node.layer.removeAnimation(forKey: "sublayerTransform") - node.layer.sublayerTransform = transform + layer.removeAnimation(forKey: "sublayerTransform") + layer.sublayerTransform = transform if let completion = completion { completion(true) } case let .animated(duration, curve): let initialTransform: CATransform3D - if beginWithCurrentState, node.isNodeLoaded { - initialTransform = node.layer.presentation()?.sublayerTransform ?? t + if beginWithCurrentState { + initialTransform = layer.presentation()?.sublayerTransform ?? t } else { initialTransform = t } - node.layer.sublayerTransform = transform - node.layer.animate(from: NSValue(caTransform3D: initialTransform), to: NSValue(caTransform3D: node.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { + layer.sublayerTransform = transform + layer.animate(from: NSValue(caTransform3D: initialTransform), to: NSValue(caTransform3D: layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { result in if let completion = completion { completion(result) @@ -1305,6 +1304,15 @@ public extension ContainedViewLayoutTransition { } } + func updateSublayerTransformScaleAndOffset(node: ASDisplayNode, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { + if !node.isNodeLoaded { + node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) + completion?(true) + return + } + return updateSublayerTransformScaleAndOffset(layer: node.layer, scale: scale, offset: offset, beginWithCurrentState: beginWithCurrentState, completion: completion) + } + func updateSublayerTransformScale(node: ASDisplayNode, scale: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if !node.isNodeLoaded { node.subnodeTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0) diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index c2eda7b160..3e58e08d6e 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -185,13 +185,15 @@ public enum DeviceMetrics: CaseIterable, Equatable { case .iPhoneX, .iPhoneXSMax: return 39.0 case .iPhoneXr: - return 41.0 + UIScreenPixel + return 41.5 case .iPhone12Mini: return 44.0 - case .iPhone12, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed: + case .iPhone12, .iPhone13, .iPhone13Pro, .iPhone14ProZoomed: return 47.0 + UIScreenPixel - case .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMaxZoomed: return 53.0 + UIScreenPixel + case .iPhone14Pro, .iPhone14ProMax: + return 55.0 case let .unknown(_, _, onScreenNavigationHeight): if let _ = onScreenNavigationHeight { return 39.0 diff --git a/submodules/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index 114b2929c7..35d1cee10a 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -566,7 +566,7 @@ public class DrawingContext { f(self.context) } - public init?(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false, bytesPerRow: Int? = nil) { + public init?(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false, bytesPerRow: Int? = nil, colorSpace: CGColorSpace? = nil) { if size.width <= 0.0 || size.height <= 0.0 { return nil } @@ -601,7 +601,7 @@ public class DrawingContext { height: Int(self.scaledSize.height), bitsPerComponent: DeviceGraphicsContextSettings.shared.bitsPerComponent, bytesPerRow: self.bytesPerRow, - space: DeviceGraphicsContextSettings.shared.colorSpace, + space: colorSpace ?? DeviceGraphicsContextSettings.shared.colorSpace, bitmapInfo: self.bitmapInfo.rawValue, releaseCallback: nil, releaseInfo: nil @@ -616,7 +616,7 @@ public class DrawingContext { } } - public func generateImage() -> UIImage? { + public func generateImage(colorSpace: CGColorSpace? = nil) -> UIImage? { if self.scaledSize.width.isZero || self.scaledSize.height.isZero { return nil } @@ -633,7 +633,7 @@ public class DrawingContext { bitsPerComponent: self.context.bitsPerComponent, bitsPerPixel: self.context.bitsPerPixel, bytesPerRow: self.context.bytesPerRow, - space: DeviceGraphicsContextSettings.shared.colorSpace, + space: colorSpace ?? DeviceGraphicsContextSettings.shared.colorSpace, bitmapInfo: self.context.bitmapInfo, provider: dataProvider, decode: nil, diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index da9591bf79..6ad2308f96 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -1274,9 +1274,6 @@ open class NavigationController: UINavigationController, ContainableController, self.filterController(controller, animated: false) } - public func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) { - } - private func scrollToTop(_ subject: NavigationSplitContainerScrollToTop) { if let _ = self.inCallStatusBar { self.inCallNavigate?() @@ -1765,4 +1762,11 @@ open class NavigationController: UINavigationController, ContainableController, private func notifyAccessibilityScreenChanged() { UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: nil) } + + public func updateRootContainerTransitionOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition) { + guard let rootContainer = self.rootContainer, case let .flat(container) = rootContainer else { + return + } + transition.updateTransform(node: container, transform: CGAffineTransformMakeTranslation(offset, 0.0)) + } } diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 1444cb8bcf..b7745a3820 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -166,10 +166,12 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { public private(set) var modalStyleOverlayTransitionFactor: CGFloat = 0.0 public var modalStyleOverlayTransitionFactorUpdated: ((ContainedViewLayoutTransition) -> Void)? + public var customModalStyleOverlayTransitionFactorUpdated: ((ContainedViewLayoutTransition) -> Void)? public func updateModalStyleOverlayTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) { if self.modalStyleOverlayTransitionFactor != value { self.modalStyleOverlayTransitionFactor = value self.modalStyleOverlayTransitionFactorUpdated?(transition) + self.customModalStyleOverlayTransitionFactorUpdated?(transition) } } @@ -452,10 +454,6 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { } } - open func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) { - - } - open func navigationStackConfigurationUpdated(next: [ViewController]) { } diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 6e59843601..6f346a965b 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -51,7 +51,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { private let size: CGSize weak var drawingView: DrawingView? - weak var selectionContainerView: DrawingSelectionContainerView? + public weak var selectionContainerView: DrawingSelectionContainerView? private var tapGestureRecognizer: UITapGestureRecognizer! private(set) var selectedEntityView: DrawingEntityView? @@ -220,7 +220,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { return CGSize(width: width, height: width) } - func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) { + public func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) { let center = self.startPosition(relativeTo: relativeTo) let rotation = self.getEntityInitialRotation() let zoomScale = 1.0 / (self.drawingView?.zoomScale ?? 1.0) @@ -485,7 +485,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } } - func selectEntity(_ entity: DrawingEntity?) { + public func selectEntity(_ entity: DrawingEntity?) { if entity?.isMedia == true { return } @@ -596,7 +596,7 @@ public class DrawingEntityView: UIView { let entity: DrawingEntity var isTracking = false - weak var selectionView: DrawingEntitySelectionView? + public weak var selectionView: DrawingEntitySelectionView? weak var containerView: DrawingEntitiesView? var onSnapToXAxis: (Bool) -> Void = { _ in } diff --git a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift index 15384d14c5..b78e163148 100644 --- a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift @@ -22,6 +22,7 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia didSet { if let previewView = self.previewView { previewView.isUserInteractionEnabled = false + previewView.layer.allowsEdgeAntialiasing = true self.addSubview(previewView) } } diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index d791ee91e2..712a2b5077 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -2310,8 +2310,13 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U private var _selectionContainerView: DrawingSelectionContainerView? var selectionContainerView: DrawingSelectionContainerView { - if self._selectionContainerView == nil { - self._selectionContainerView = DrawingSelectionContainerView(frame: .zero) + if self._selectionContainerView == nil, let controller = self.controller { + if let externalSelectionContainerView = controller.externalSelectionContainerView { + self._selectionContainerView = externalSelectionContainerView + } else { + self._selectionContainerView = DrawingSelectionContainerView(frame: .zero) + } + } return self._selectionContainerView! } @@ -3013,6 +3018,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U private let isAvatar: Bool private let externalDrawingView: DrawingView? private let externalEntitiesView: DrawingEntitiesView? + private let externalSelectionContainerView: DrawingSelectionContainerView? private let existingStickerPickerInputData: Promise? public var requestDismiss: () -> Void = {} @@ -3020,7 +3026,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U public var getCurrentImage: () -> UIImage? = { return nil } public var updateVideoPlayback: (Bool) -> Void = { _ in } - public init(context: AccountContext, sourceHint: SourceHint? = nil, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, drawingView: DrawingView?, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?, existingStickerPickerInputData: Promise? = nil) { + public init(context: AccountContext, sourceHint: SourceHint? = nil, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, drawingView: DrawingView?, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?, selectionContainerView: DrawingSelectionContainerView?, existingStickerPickerInputData: Promise? = nil) { self.context = context self.sourceHint = sourceHint self.size = size @@ -3041,6 +3047,12 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.externalEntitiesView = nil } + if let selectionContainerView = selectionContainerView { + self.externalSelectionContainerView = selectionContainerView + } else { + self.externalSelectionContainerView = nil + } + super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .Hide @@ -3081,13 +3093,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U return nil } - let drawingImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in - let bounds = CGRect(origin: .zero, size: size) - context.clear(bounds) - if let cgImage = self.drawingView.drawingImage?.cgImage { - context.draw(cgImage, in: bounds) - } - }, opaque: false, scale: 1.0) + let drawingImage = self.drawingView.drawingImage let _ = self.entitiesView.entitiesData let codableEntities = self.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }.compactMap({ CodableDrawingEntity(entity: $0) }) diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index 47ca066839..a35bd6e844 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -201,7 +201,7 @@ private final class StickerSelectionComponent: Component { } } -class StickerPickerScreen: ViewController { +public class StickerPickerScreen: ViewController { final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { private var presentationData: PresentationData private weak var controller: StickerPickerScreen? @@ -999,9 +999,9 @@ class StickerPickerScreen: ViewController { public var pushController: (ViewController) -> Void = { _ in } public var presentController: (ViewController) -> Void = { _ in } - var completion: (TelegramMediaFile?) -> Void = { _ in } + public var completion: (TelegramMediaFile?) -> Void = { _ in } - init(context: AccountContext, inputData: Signal) { + public init(context: AccountContext, inputData: Signal) { self.context = context self.theme = defaultDarkColorPresentationTheme self.inputData = inputData @@ -1015,14 +1015,14 @@ class StickerPickerScreen: ViewController { fatalError("init(coder:) has not been implemented") } - override func loadDisplayNode() { + public override func loadDisplayNode() { self.displayNode = Node(context: self.context, controller: self, theme: self.theme) self.displayNodeDidLoad() self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) } - override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { self.view.endEditing(true) if flag { self.node.animateOut(completion: { @@ -1035,19 +1035,19 @@ class StickerPickerScreen: ViewController { } } - override func viewDidAppear(_ animated: Bool) { + public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.node.updateIsVisible(isVisible: true) } - override func viewDidDisappear(_ animated: Bool) { + public override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.node.updateIsVisible(isVisible: false) } - override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.currentLayout = layout super.containerLayoutUpdated(layout, transition: transition) diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index e1b3520201..6a475beccb 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -579,7 +579,7 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon let interfaceController: TGPhotoDrawingInterfaceController init(context: AccountContext, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?) { - let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, drawingView: nil, entitiesView: entitiesView) + let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, drawingView: nil, entitiesView: entitiesView, selectionContainerView: nil) self.interfaceController = interfaceController self.drawingView = interfaceController.drawingView self.drawingEntitiesView = interfaceController.entitiesView diff --git a/submodules/LocalizedPeerData/Sources/PeerTitle.swift b/submodules/LocalizedPeerData/Sources/PeerTitle.swift index 0986fe7de3..b30e6cabcd 100644 --- a/submodules/LocalizedPeerData/Sources/PeerTitle.swift +++ b/submodules/LocalizedPeerData/Sources/PeerTitle.swift @@ -59,3 +59,69 @@ public extension EnginePeer { } } } + +public extension EnginePeer.IndexName { + func isLessThan(other: EnginePeer.IndexName, ordering: PresentationPersonNameOrder) -> ComparisonResult { + switch self { + case let .title(lhsTitle, _): + let rhsString: String + switch other { + case let .title(title, _): + rhsString = title + case let .personName(first, last, _, _): + switch ordering { + case .firstLast: + if first.isEmpty { + rhsString = last + } else { + rhsString = first + last + } + case .lastFirst: + if last.isEmpty { + rhsString = first + } else { + rhsString = last + first + } + } + } + return lhsTitle.caseInsensitiveCompare(rhsString) + case let .personName(lhsFirst, lhsLast, _, _): + let lhsString: String + switch ordering { + case .firstLast: + if lhsFirst.isEmpty { + lhsString = lhsLast + } else { + lhsString = lhsFirst + lhsLast + } + case .lastFirst: + if lhsLast.isEmpty { + lhsString = lhsFirst + } else { + lhsString = lhsLast + lhsFirst + } + } + let rhsString: String + switch other { + case let .title(title, _): + rhsString = title + case let .personName(first, last, _, _): + switch ordering { + case .firstLast: + if first.isEmpty { + rhsString = last + } else { + rhsString = first + last + } + case .lastFirst: + if last.isEmpty { + rhsString = first + } else { + rhsString = last + first + } + } + } + return lhsString.caseInsensitiveCompare(rhsString) + } + } +} diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index acd681407a..4f5ec48c76 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -29,30 +29,32 @@ final class MediaPickerGridItem: GridItem { let theme: PresentationTheme let selectable: Bool let enableAnimations: Bool + let stories: Bool let section: GridSection? = nil - init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) { + init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) { self.content = content self.interaction = interaction self.theme = theme self.selectable = selectable self.enableAnimations = enableAnimations + self.stories = stories } func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { switch self.content { case let .asset(fetchResult, index): let node = MediaPickerGridItemNode() - node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations) + node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) return node case let .media(media, index): let node = MediaPickerGridItemNode() - node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations) + node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) return node case let .draft(draft, index): let node = MediaPickerGridItemNode() - node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations) + node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) return node } } @@ -64,11 +66,11 @@ final class MediaPickerGridItem: GridItem { } switch self.content { case let .asset(fetchResult, index): - node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations) + node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) case let .media(media, index): - node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations) + node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) case let .draft(draft, index): - node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations) + node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) } } } @@ -151,7 +153,11 @@ final class MediaPickerGridItemNode: GridItemNode { } var identifier: String { - return self.selectableItem?.uniqueIdentifier ?? "" + if let (draft, _) = self.currentDraftState { + return draft.path + } else { + return self.selectableItem?.uniqueIdentifier ?? "" + } } var selectableItem: TGMediaSelectableItem? { @@ -235,7 +241,7 @@ final class MediaPickerGridItemNode: GridItemNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) } - func setup(interaction: MediaPickerInteraction, draft: MediaEditorDraft, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) { + func setup(interaction: MediaPickerInteraction, draft: MediaEditorDraft, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) { self.interaction = interaction self.theme = theme self.selectable = selectable @@ -262,7 +268,7 @@ final class MediaPickerGridItemNode: GridItemNode { self.updateHiddenMedia() } - func setup(interaction: MediaPickerInteraction, media: MediaPickerScreen.Subject.Media, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) { + func setup(interaction: MediaPickerInteraction, media: MediaPickerScreen.Subject.Media, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) { self.interaction = interaction self.theme = theme self.selectable = selectable @@ -279,7 +285,7 @@ final class MediaPickerGridItemNode: GridItemNode { self.updateHiddenMedia() } - func setup(interaction: MediaPickerInteraction, fetchResult: PHFetchResult, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) { + func setup(interaction: MediaPickerInteraction, fetchResult: PHFetchResult, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) { self.interaction = interaction self.theme = theme self.selectable = selectable @@ -315,7 +321,12 @@ final class MediaPickerGridItemNode: GridItemNode { } let scale = min(2.0, UIScreenScale) - let targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale) + let targetSize: CGSize + if stories { + targetSize = CGSize(width: 128.0 * UIScreenScale, height: 128.0 * UIScreenScale) + } else { + targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale) + } let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .fastFormat, synchronous: true) |> then( @@ -456,10 +467,18 @@ final class MediaPickerGridItemNode: GridItemNode { } } - func transitionView() -> UIView { - let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)! - view.frame = self.convert(self.bounds, to: nil) - return view + func transitionView(snapshot: Bool) -> UIView { + if snapshot { + let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)! + view.frame = self.convert(self.bounds, to: nil) + return view + } else { + return self.imageNode.view + } + } + + func transitionImage() -> UIImage? { + return self.imageNode.image } @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 2ccd53bbd1..54206cdf45 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -53,13 +53,14 @@ private struct MediaPickerGridEntry: Comparable, Identifiable { let stableId: Int let content: MediaPickerGridItemContent let selectable: Bool + let stories: Bool static func <(lhs: MediaPickerGridEntry, rhs: MediaPickerGridEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(context: AccountContext, interaction: MediaPickerInteraction, theme: PresentationTheme) -> MediaPickerGridItem { - return MediaPickerGridItem(content: self.content, interaction: interaction, theme: theme, selectable: self.selectable, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency) + return MediaPickerGridItem(content: self.content, interaction: interaction, theme: theme, selectable: self.selectable, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency, stories: self.stories) } } @@ -583,9 +584,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { var updateLayout = false + var stories = false var selectable = true if case let .assets(_, mode) = controller.subject, mode != .default { selectable = false + if mode == .story { + stories = true + } } switch state { @@ -606,7 +611,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { var draftIndex = 0 for draft in drafts { - entries.append(MediaPickerGridEntry(stableId: stableId, content: .draft(draft, draftIndex), selectable: selectable)) + entries.append(MediaPickerGridEntry(stableId: stableId, content: .draft(draft, draftIndex), selectable: selectable, stories: stories)) stableId += 1 draftIndex += 1 } @@ -618,7 +623,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } else { index = totalCount - i - 1 } - entries.append(MediaPickerGridEntry(stableId: stableId, content: .asset(fetchResult, index), selectable: selectable)) + entries.append(MediaPickerGridEntry(stableId: stableId, content: .asset(fetchResult, index), selectable: selectable, stories: stories)) stableId += 1 } @@ -647,7 +652,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { case let .media(media): let count = media.count for i in 0 ..< count { - entries.append(MediaPickerGridEntry(stableId: stableId, content: .media(media[i], i), selectable: true)) + entries.append(MediaPickerGridEntry(stableId: stableId, content: .media(media[i], i), selectable: true, stories: stories)) stableId += 1 } } @@ -976,7 +981,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } - private func transitionView(for identifier: String, hideSource: Bool = false) -> UIView? { + fileprivate func transitionView(for identifier: String, snapshot: Bool = true, hideSource: Bool = false) -> UIView? { if let selectionNode = self.selectionNode, selectionNode.alpha > 0.0 { return selectionNode.transitionView(for: identifier, hideSource: hideSource) } else { @@ -986,7 +991,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { transitionNode = itemNode } } - let transitionView = transitionNode?.transitionView() + let transitionView = transitionNode?.transitionView(snapshot: snapshot) if hideSource { transitionNode?.isHidden = true } @@ -994,6 +999,16 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } + fileprivate func transitionImage(for identifier: String) -> UIImage? { + var transitionNode: MediaPickerGridItemNode? + self.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? MediaPickerGridItemNode, itemNode.identifier == identifier { + transitionNode = itemNode + } + } + return transitionNode?.transitionImage() + } + private func enqueueTransaction(_ transaction: MediaPickerGridTransaction) { self.enqueuedTransactions.append(transaction) @@ -1189,7 +1204,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { var itemHeight = itemWidth if case let .assets(_, mode) = controller.subject, case .story = mode { - itemHeight = 180.0 + itemHeight = round(itemWidth / 9.0 * 16.0) } self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: itemHeight * 3.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemHeight), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in @@ -1905,6 +1920,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } + fileprivate func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? { + return self.controllerNode.transitionView(for: identifier, snapshot: snapshot, hideSource: hideSource) + } + + fileprivate func transitionImage(for identifier: String) -> UIImage? { + return self.controllerNode.transitionImage(for: identifier) + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) @@ -2112,7 +2135,8 @@ public func wallpaperMediaPickerController( public func storyMediaPickerController( context: AccountContext, - completion: @escaping (Any) -> Void = { _ in } + completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?) -> Void = { _, _, _, _, _ in }, + dismissed: @escaping () -> Void ) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) @@ -2121,9 +2145,37 @@ public func storyMediaPickerController( }) controller.requestController = { _, present in let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .story), mainButtonState: nil, mainButtonAction: nil) - mediaPickerController.customSelection = completion + mediaPickerController.customSelection = { [weak mediaPickerController] result in + guard let controller = mediaPickerController else { + return + } + if let result = result as? MediaEditorDraft { + if let transitionView = controller.transitionView(for: result.path, snapshot: false) { + let transitionOut: () -> (UIView, CGRect)? = { + if let transitionView = controller.transitionView(for: result.path, snapshot: false) { + return (transitionView, transitionView.bounds) + } + return nil + } + completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.path), transitionOut) + } + } else if let result = result as? PHAsset { + if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) { + let transitionOut: () -> (UIView, CGRect)? = { + if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) { + return (transitionView, transitionView.bounds) + } + return nil + } + completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.localIdentifier), transitionOut) + } + } + } present(mediaPickerController, mediaPickerController.mediaPickerContext) } + controller.willDismiss = { + dismissed() + } controller.navigationPresentation = .flatModal controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) return controller diff --git a/submodules/StickerResources/Sources/StickerResources.swift b/submodules/StickerResources/Sources/StickerResources.swift index 815840466b..97735c297f 100644 --- a/submodules/StickerResources/Sources/StickerResources.swift +++ b/submodules/StickerResources/Sources/StickerResources.swift @@ -328,8 +328,8 @@ public func chatMessageLegacySticker(account: Account, userLocation: MediaResour } } -public func chatMessageSticker(account: Account, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return chatMessageSticker(postbox: account.postbox, userLocation: userLocation, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, thumbnail: thumbnail, synchronousLoad: synchronousLoad) +public func chatMessageSticker(account: Account, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false, colorSpace: CGColorSpace? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return chatMessageSticker(postbox: account.postbox, userLocation: userLocation, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, thumbnail: thumbnail, synchronousLoad: synchronousLoad, colorSpace: colorSpace) } public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaResource, animated: Bool = false, synchronousLoad: Bool = false, nilIfEmpty: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { @@ -385,7 +385,7 @@ public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaRes } } -public func chatMessageSticker(postbox: Postbox, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func chatMessageSticker(postbox: Postbox, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false, colorSpace: CGColorSpace? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal: Signal, NoError> if thumbnail { signal = chatMessageStickerThumbnailData(postbox: postbox, userLocation: userLocation, file: file, synchronousLoad: synchronousLoad) @@ -408,7 +408,7 @@ public func chatMessageSticker(postbox: Postbox, userLocation: MediaResourceUser return nil } - guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil) else { + guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil, colorSpace: colorSpace) else { return nil } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift index 5421591c6d..bae9426009 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift @@ -64,6 +64,9 @@ extension TelegramUser { if (flags & (1 << 28)) != 0 { userFlags.insert(.isPremium) } + if (flags2 & (1 << 2)) != 0 { + userFlags.insert(.isCloseFriend) + } var botInfo: BotUserInfo? if (flags & (1 << 14)) != 0 { @@ -96,7 +99,7 @@ extension TelegramUser { static func merge(_ lhs: TelegramUser?, rhs: Api.User) -> TelegramUser? { switch rhs { - case let .user(flags, _, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _): + case let .user(flags, flags2, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _): let isMin = (flags & (1 << 20)) != 0 if !isMin { return TelegramUser(user: rhs) @@ -129,6 +132,9 @@ extension TelegramUser { if (flags & (1 << 28)) != 0 { userFlags.insert(.isPremium) } + if (flags2 & (1 << 2)) != 0 { + userFlags.insert(.isCloseFriend) + } var botInfo: BotUserInfo? if (flags & (1 << 14)) != 0 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index 2830b1724e..f7815f68d2 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -16,6 +16,7 @@ public struct UserInfoFlags: OptionSet { public static let isScam = UserInfoFlags(rawValue: (1 << 2)) public static let isFake = UserInfoFlags(rawValue: (1 << 3)) public static let isPremium = UserInfoFlags(rawValue: (1 << 4)) + public static let isCloseFriend = UserInfoFlags(rawValue: (1 << 5)) } public struct BotUserInfoFlags: OptionSet { @@ -351,4 +352,8 @@ public final class TelegramUser: Peer, Equatable { public func withUpdatedEmojiStatus(_ emojiStatus: PeerEmojiStatus?) -> TelegramUser { return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: emojiStatus, usernames: self.usernames) } + + public func withUpdatedFlags(_ flags: UserInfoFlags) -> TelegramUser { + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: emojiStatus, usernames: self.usernames) + } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/CloseFriends.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/CloseFriends.swift new file mode 100644 index 0000000000..37dee8cde2 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/CloseFriends.swift @@ -0,0 +1,36 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +public func _internal_updateCloseFriends(account: Account, peerIds: [EnginePeer.Id]) -> Signal { + let ids: [Int64] = peerIds.map { $0.id._internalGetInt64Value() } + return account.network.request(Api.functions.contacts.editCloseFriends(id: ids)) + |> retryRequest + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction in + let contactPeerIds = transaction.getContactPeerIds() + var updatedPeers: [Peer] = [] + for peerId in contactPeerIds { + if let peer = transaction.getPeer(peerId) as? TelegramUser { + if peerIds.contains(peerId) { + var updatedFlags = peer.flags + updatedFlags.insert(.isCloseFriend) + let updatedPeer = peer.withUpdatedFlags(updatedFlags) + updatedPeers.append(updatedPeer) + } else if peer.flags.contains(.isCloseFriend) { + var updatedFlags = peer.flags + updatedFlags.remove(.isCloseFriend) + let updatedPeer = peer.withUpdatedFlags(updatedFlags) + updatedPeers.append(updatedPeer) + } + } + } + updatePeers(transaction: transaction, peers: updatedPeers, update: { _, updated in + return updated + }) + } + } + |> ignoreValues +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift index 9f56c06f76..0291b7675d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift @@ -44,5 +44,9 @@ public extension TelegramEngine { public func updateSelectiveAccountPrivacySettings(type: UpdateSelectiveAccountPrivacySettingsType, settings: SelectivePrivacySettings) -> Signal { return _internal_updateSelectiveAccountPrivacySettings(account: self.account, type: type, settings: settings) } + + public func updateCloseFriends(peerIds: [EnginePeer.Id]) -> Signal { + return _internal_updateCloseFriends(account: self.account, peerIds: peerIds) + } } } diff --git a/submodules/TelegramUI/Components/CameraScreen/BUILD b/submodules/TelegramUI/Components/CameraScreen/BUILD index 75c4febdd0..acf39becc8 100644 --- a/submodules/TelegramUI/Components/CameraScreen/BUILD +++ b/submodules/TelegramUI/Components/CameraScreen/BUILD @@ -70,6 +70,7 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/Components/BlurredBackgroundComponent", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", + "//submodules/Components/BundleIconComponent:BundleIconComponent", "//submodules/TooltipUI", "//submodules/TelegramUI/Components/MediaEditor", ], diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraButton.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraButton.swift index a408ca9f43..867f42e71e 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraButton.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraButton.swift @@ -3,14 +3,14 @@ import UIKit import ComponentFlow final class CameraButton: Component { - let content: AnyComponent + let content: AnyComponentWithIdentity let minSize: CGSize? let tag: AnyObject? let isEnabled: Bool let action: () -> Void init( - content: AnyComponent, + content: AnyComponentWithIdentity, minSize: CGSize? = nil, tag: AnyObject? = nil, isEnabled: Bool = true, @@ -50,7 +50,7 @@ final class CameraButton: Component { } final class View: UIButton, ComponentTaggedView { - private let contentView: ComponentHostView + private var contentView: ComponentHostView private var component: CameraButton? private var currentIsHighlighted: Bool = false { @@ -123,9 +123,17 @@ final class CameraButton: Component { } func update(component: CameraButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if let currentId = self.component?.content.id, currentId != component.content.id { + self.contentView.removeFromSuperview() + + self.contentView = ComponentHostView() + self.contentView.isUserInteractionEnabled = false + self.contentView.layer.allowsGroupOpacity = true + self.addSubview(self.contentView) + } let contentSize = self.contentView.update( transition: transition, - component: component.content, + component: component.content.component, environment: {}, containerSize: availableSize ) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 3d78dda0bd..306e91861c 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -17,6 +17,7 @@ import Photos import LottieAnimationComponent import TooltipUI import MediaEditor +import BundleIconComponent let videoRedColor = UIColor(rgb: 0xff3b30) @@ -33,29 +34,31 @@ private struct CameraState { } let mode: CameraMode let flashMode: Camera.FlashMode + let flashModeDidChange: Bool let recording: Recording let duration: Double func updatedMode(_ mode: CameraMode) -> CameraState { - return CameraState(mode: mode, flashMode: self.flashMode, recording: self.recording, duration: self.duration) + return CameraState(mode: mode, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration) } func updatedFlashMode(_ flashMode: Camera.FlashMode) -> CameraState { - return CameraState(mode: self.mode, flashMode: flashMode, recording: self.recording, duration: self.duration) + return CameraState(mode: self.mode, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, recording: self.recording, duration: self.duration) } func updatedRecording(_ recording: Recording) -> CameraState { - return CameraState(mode: self.mode, flashMode: self.flashMode, recording: recording, duration: self.duration) + return CameraState(mode: self.mode, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: recording, duration: self.duration) } func updatedDuration(_ duration: Double) -> CameraState { - return CameraState(mode: self.mode, flashMode: self.flashMode, recording: self.recording, duration: duration) + return CameraState(mode: self.mode, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: duration) } } enum CameraScreenTransition { case animateIn case animateOut + case finishedAnimateIn } private let cancelButtonTag = GenericComponentViewTag() @@ -71,7 +74,7 @@ private final class CameraScreenComponent: CombinedComponent { let context: AccountContext let camera: Camera let changeMode: ActionSlot - let isDismissing: Bool + let hasAppeared: Bool let present: (ViewController) -> Void let push: (ViewController) -> Void let completion: ActionSlot> @@ -80,7 +83,7 @@ private final class CameraScreenComponent: CombinedComponent { context: AccountContext, camera: Camera, changeMode: ActionSlot, - isDismissing: Bool, + hasAppeared: Bool, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: ActionSlot> @@ -88,7 +91,7 @@ private final class CameraScreenComponent: CombinedComponent { self.context = context self.camera = camera self.changeMode = changeMode - self.isDismissing = isDismissing + self.hasAppeared = hasAppeared self.present = present self.push = push self.completion = completion @@ -98,7 +101,7 @@ private final class CameraScreenComponent: CombinedComponent { if lhs.context !== rhs.context { return false } - if lhs.isDismissing != rhs.isDismissing { + if lhs.hasAppeared != rhs.hasAppeared { return false } return true @@ -108,7 +111,6 @@ private final class CameraScreenComponent: CombinedComponent { enum ImageKey: Hashable { case cancel case flip - case flash } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey) -> UIImage { @@ -121,8 +123,6 @@ private final class CameraScreenComponent: CombinedComponent { image = UIImage(bundleImageName: "Camera/CloseIcon")! case .flip: image = UIImage(bundleImageName: "Camera/FlipIcon")! - case .flash: - image = UIImage(bundleImageName: "Camera/FlashIcon")! } cachedImages[key] = image return image @@ -141,7 +141,7 @@ private final class CameraScreenComponent: CombinedComponent { fileprivate var lastGalleryAsset: PHAsset? private var lastGalleryAssetsDisposable: Disposable? - var cameraState = CameraState(mode: .photo, flashMode: .off, recording: .none, duration: 0.0) + var cameraState = CameraState(mode: .photo, flashMode: .off, flashModeDidChange: false, recording: .none, duration: 0.0) var swipeHint: CaptureControlsComponent.SwipeHint = .none init(context: AccountContext, camera: Camera, present: @escaping (ViewController) -> Void, completion: ActionSlot>) { @@ -188,6 +188,9 @@ private final class CameraScreenComponent: CombinedComponent { } func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) { + guard hint != self.swipeHint else { + return + } self.swipeHint = hint self.updated(transition: .easeInOut(duration: 0.2)) } @@ -274,10 +277,15 @@ private final class CameraScreenComponent: CombinedComponent { if case .none = state.cameraState.recording { let cancelButton = cancelButton.update( component: CameraButton( - content: AnyComponent(Image( - image: state.image(.cancel), - size: CGSize(width: 40.0, height: 40.0) - )), + content: AnyComponentWithIdentity( + id: "cancel", + component: AnyComponent( + Image( + image: state.image(.cancel), + size: CGSize(width: 40.0, height: 40.0) + ) + ) + ), action: { guard let controller = controller() as? CameraScreen else { return @@ -294,32 +302,50 @@ private final class CameraScreenComponent: CombinedComponent { .disappear(.default(scale: true)) .cornerRadius(20.0) ) - - let flashIconName: String - switch state.cameraState.flashMode { - case .off: - flashIconName = "flash_off" - case .on: - flashIconName = "flash_on" - case .auto: - flashIconName = "flash_auto" - @unknown default: - flashIconName = "flash_off" - } - - let flashButton = flashButton.update( - component: CameraButton( - content: AnyComponent( + + let flashContentComponent: AnyComponentWithIdentity + if component.hasAppeared { + let flashIconName: String + switch state.cameraState.flashMode { + case .off: + flashIconName = "flash_off" + case .on: + flashIconName = "flash_on" + case .auto: + flashIconName = "flash_auto" + @unknown default: + flashIconName = "flash_off" + } + + flashContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: flashIconName, - mode: .animating(loop: false), + mode: !state.cameraState.flashModeDidChange ? .still(position: .end) : .animating(loop: false), range: nil ), colors: [:], size: CGSize(width: 40.0, height: 40.0) ) - ), + ) + ) + } else { + flashContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + BundleIconComponent( + name: "Camera/FlashOffIcon", + tintColor: nil + ) + ) + ) + } + + let flashButton = flashButton.update( + component: CameraButton( + content: flashContentComponent, action: { [weak state] in guard let state else { return @@ -514,7 +540,7 @@ private final class CameraScreenComponent: CombinedComponent { } } - if case .none = state.cameraState.recording, !component.isDismissing { + if case .none = state.cameraState.recording { let modeControl = modeControl.update( component: ModeComponent( availableModes: [.photo, .video], @@ -638,13 +664,16 @@ public class CameraScreen: ViewController { private let context: AccountContext private let updateState: ActionSlot - private let backgroundEffectView: UIVisualEffectView - private let backgroundDimView: UIView + fileprivate let backgroundView: UIView + fileprivate let containerView: UIView fileprivate let componentHost: ComponentView private let previewContainerView: UIView fileprivate let previewView: CameraPreviewView? fileprivate let simplePreviewView: CameraSimplePreviewView? fileprivate let previewBlurView: BlurView + private var previewSnapshotView: UIView? + fileprivate let transitionDimView: UIView + fileprivate let transitionCornersView: UIImageView fileprivate let camera: Camera private var presentationData: PresentationData @@ -665,7 +694,7 @@ public class CameraScreen: ViewController { } } - private var previewBlurPromise = ValuePromise(false) + fileprivate var previewBlurPromise = ValuePromise(false) init(controller: CameraScreen) { self.controller = controller @@ -674,9 +703,11 @@ public class CameraScreen: ViewController { self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - self.backgroundEffectView = UIVisualEffectView(effect: nil) - self.backgroundDimView = UIView() - self.backgroundDimView.backgroundColor = UIColor(rgb: 0x000000) + self.backgroundView = UIView() + self.backgroundView.backgroundColor = UIColor(rgb: 0x000000) + + self.containerView = UIView() + self.containerView.clipsToBounds = true self.componentHost = ComponentView() @@ -710,17 +741,25 @@ public class CameraScreen: ViewController { #endif } } + + self.transitionDimView = UIView() + self.transitionDimView.backgroundColor = UIColor(rgb: 0x000000) + self.transitionDimView.isUserInteractionEnabled = false + + self.transitionCornersView = UIImageView() super.init() self.backgroundColor = .clear - self.view.addSubview(self.backgroundEffectView) - self.view.addSubview(self.backgroundDimView) + self.view.addSubview(self.backgroundView) + self.view.addSubview(self.containerView) - self.view.addSubview(self.previewContainerView) + self.containerView.addSubview(self.previewContainerView) self.previewContainerView.addSubview(self.effectivePreviewView) self.previewContainerView.addSubview(self.previewBlurView) + self.containerView.addSubview(self.transitionDimView) + self.view.addSubview(self.transitionCornersView) self.changingPositionDisposable = combineLatest( queue: Queue.mainQueue(), @@ -733,7 +772,9 @@ public class CameraScreen: ViewController { self.previewBlurView.effect = UIBlurEffect(style: .dark) }) } else if forceBlur { - self.previewBlurView.effect = UIBlurEffect(style: .dark) + UIView.animate(withDuration: 0.4) { + self.previewBlurView.effect = UIBlurEffect(style: .dark) + } } else { UIView.animate(withDuration: 0.4) { self.previewBlurView.effect = nil @@ -776,7 +817,8 @@ public class CameraScreen: ViewController { self.camera.stopCapture() } } - } + }, + nil ) } } @@ -814,96 +856,43 @@ public class CameraScreen: ViewController { } } - private var panTranslation: CGFloat? private var previewInitialPosition: CGPoint? private var controlsInitialPosition: CGPoint? @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let controller = self.controller else { + return + } + let translation = gestureRecognizer.translation(in: gestureRecognizer.view) switch gestureRecognizer.state { case .began: - self.panTranslation = nil self.previewInitialPosition = self.previewContainerView.center self.controlsInitialPosition = self.componentHost.view?.center case .changed: - let translation = gestureRecognizer.translation(in: gestureRecognizer.view) if !"".isEmpty { } else { - if abs(translation.x) > 50.0 && abs(translation.y) < 50.0, self.panTranslation == nil { - self.changeMode.invoke(translation.x > 0.0 ? .photo : .video) - gestureRecognizer.isEnabled = false - gestureRecognizer.isEnabled = true - } else if translation.y > 10.0 { - let isFirstPanChange = self.panTranslation == nil - self.panTranslation = translation.y - if let previewInitialPosition = self.previewInitialPosition { - self.previewContainerView.center = CGPoint(x: previewInitialPosition.x, y: previewInitialPosition.y + translation.y) - } - if let controlsInitialPosition = self.controlsInitialPosition, let view = self.componentHost.view { - view.center = CGPoint(x: controlsInitialPosition.x, y: controlsInitialPosition.y + translation.y) - } - - if self.backgroundEffectView.isHidden { - self.backgroundEffectView.isHidden = false - - UIView.animate(withDuration: 0.25, animations: { - self.backgroundEffectView.effect = nil - self.backgroundDimView.alpha = 0.0 - }) - } - - if isFirstPanChange { - if let layout = self.validLayout { - self.containerLayoutUpdated(layout: layout, transition: .easeInOut(duration: 0.2)) - } - } + if translation.x < -10.0 { + let transitionFraction = 1.0 - abs(translation.x) / self.frame.width + controller.updateTransitionProgress(transitionFraction, transition: .immediate) } else if translation.y < -10.0 { - self.controller?.presentGallery() + controller.presentGallery() gestureRecognizer.isEnabled = false gestureRecognizer.isEnabled = true } } case .ended: let velocity = gestureRecognizer.velocity(in: self.view) - if velocity.y > 1000.0 { - self.controller?.requestDismiss(animated: true) - } else if let panTranslation = self.panTranslation, abs(panTranslation) > 300.0 { - self.controller?.requestDismiss(animated: true) - } else { - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) - if let previewInitialPosition = self.previewInitialPosition { - transition.setPosition(view: self.previewContainerView, position: previewInitialPosition) - } - if let controlsInitialPosition = self.controlsInitialPosition, let view = self.componentHost.view { - transition.setPosition(view: view, position: controlsInitialPosition) - } - if !self.backgroundEffectView.isHidden { - UIView.animate(withDuration: 0.25, animations: { - self.backgroundEffectView.effect = UIBlurEffect(style: .dark) - self.backgroundDimView.alpha = 1.0 - }, completion: { _ in - self.backgroundEffectView.isHidden = true - }) - } - } - if let _ = self.panTranslation { - self.panTranslation = nil - if let layout = self.validLayout { - self.containerLayoutUpdated(layout: layout, transition: .easeInOut(duration: 0.2)) - } - } + let transitionFraction = 1.0 - abs(translation.x) / self.frame.width + controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true) default: break } } func animateIn() { - self.backgroundDimView.alpha = 0.0 + self.backgroundView.alpha = 0.0 UIView.animate(withDuration: 0.4, animations: { - self.backgroundEffectView.effect = UIBlurEffect(style: .dark) - self.backgroundDimView.alpha = 1.0 - - }, completion: { _ in - self.backgroundEffectView.isHidden = true + self.backgroundView.alpha = 1.0 }) if let transitionIn = self.controller?.transitionIn, let sourceView = transitionIn.sourceView { @@ -926,23 +915,13 @@ public class CameraScreen: ViewController { view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) } } - - if let view = self.componentHost.findTaggedView(tag: flashButtonTag) { - view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) - view.layer.shadowRadius = 4.0 - view.layer.shadowColor = UIColor.black.cgColor - view.layer.shadowOpacity = 0.2 - } } func animateOut(completion: @escaping () -> Void) { self.camera.stopCapture(invalidate: true) - - self.backgroundEffectView.isHidden = false - + UIView.animate(withDuration: 0.25, animations: { - self.backgroundEffectView.effect = nil - self.backgroundDimView.alpha = 0.0 + self.backgroundView.alpha = 0.0 }) if let transitionOut = self.controller?.transitionOut(false), let destinationView = transitionOut.destinationView { @@ -994,13 +973,15 @@ public class CameraScreen: ViewController { } } - func commitTransitionToEditor() { - self.previewContainerView.alpha = 0.0 + func pauseCameraCapture() { + self.simplePreviewView?.isEnabled = false + Queue.mainQueue().after(0.3) { + self.previewBlurPromise.set(true) + } + self.camera.stopCapture() } - private var previewSnapshotView: UIView? - func animateInFromEditor() { - self.previewContainerView.alpha = 1.0 + func resumeCameraCapture() { if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) { self.simplePreviewView?.addSubview(snapshot) self.previewSnapshotView = snapshot @@ -1023,26 +1004,46 @@ public class CameraScreen: ViewController { self.previewBlurPromise.set(false) } } + } + + func animateInFromEditor(toGallery: Bool) { + if !toGallery { + self.resumeCameraCapture() + + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + transition.setAlpha(view: view, alpha: 1.0) + } + if let view = self.componentHost.findTaggedView(tag: flashButtonTag) { + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + transition.setAlpha(view: view, alpha: 1.0) + } + if let view = self.componentHost.findTaggedView(tag: zoomControlTag) { + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + transition.setAlpha(view: view, alpha: 1.0) + } + if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View { + view.animateInFromEditor(transition: transition) + } + if let view = self.componentHost.findTaggedView(tag: modeControlTag) as? ModeComponent.View { + view.animateInFromEditor(transition: transition) + } + } + } + + func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) { + guard let layout = self.validLayout else { + return + } - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) - if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) - transition.setAlpha(view: view, alpha: 1.0) - } - if let view = self.componentHost.findTaggedView(tag: flashButtonTag) { - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) - transition.setAlpha(view: view, alpha: 1.0) - } - if let view = self.componentHost.findTaggedView(tag: zoomControlTag) { - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) - transition.setAlpha(view: view, alpha: 1.0) - } - if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View { - view.animateInFromEditor(transition: transition) - } - if let view = self.componentHost.findTaggedView(tag: modeControlTag) as? ModeComponent.View { - view.animateInFromEditor(transition: transition) - } + let progress = 1.0 - value + let maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width + let maxOffset = -56.0 + + let scale = 1.0 * progress + (1.0 - progress) * maxScale + let offset = (1.0 - progress) * maxOffset + transition.updateSublayerTransformScaleAndOffset(layer: self.containerView.layer, scale: scale, offset: CGPoint(x: 0.0, y: offset), beginWithCurrentState: true) } func presentDraftTooltip() { @@ -1067,8 +1068,22 @@ public class CameraScreen: ViewController { } return result } + + func requestUpdateLayout(hasAppeared: Bool, transition: Transition) { + if let layout = self.validLayout { + self.containerLayoutUpdated(layout: layout, forceUpdate: true, hasAppeared: hasAppeared, transition: transition) + + if let view = self.componentHost.findTaggedView(tag: flashButtonTag) { + view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) + view.layer.shadowRadius = 4.0 + view.layer.shadowColor = UIColor.black.cgColor + view.layer.shadowOpacity = 0.2 + } + } + } - func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { + fileprivate var hasAppeared = false + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: Transition) { guard let _ = self.controller else { return } @@ -1103,8 +1118,9 @@ public class CameraScreen: ViewController { var transition = transition if isFirstTime { transition = transition.withUserData(CameraScreenTransition.animateIn) - } else if animateOut { - transition = transition.withUserData(CameraScreenTransition.animateOut) + } else if hasAppeared && !self.hasAppeared { + self.hasAppeared = hasAppeared + transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn) } let componentSize = self.componentHost.update( @@ -1114,7 +1130,7 @@ public class CameraScreen: ViewController { context: self.context, camera: self.camera, changeMode: self.changeMode, - isDismissing: self.panTranslation != nil, + hasAppeared: self.hasAppeared, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) }, @@ -1127,34 +1143,48 @@ public class CameraScreen: ViewController { environment: { environment }, - forceUpdate: forceUpdate || animateOut, + forceUpdate: forceUpdate, containerSize: layout.size ) if let componentView = self.componentHost.view { if componentView.superview == nil { - self.view.insertSubview(componentView, at: 3) + self.containerView.insertSubview(componentView, belowSubview: transitionDimView) componentView.clipsToBounds = true } - - if self.panTranslation == nil { - let componentFrame = CGRect(origin: .zero, size: componentSize) - transition.setFrame(view: componentView, frame: componentFrame) - } + + let componentFrame = CGRect(origin: .zero, size: componentSize) + transition.setFrame(view: componentView, frame: componentFrame) } - transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size)) - transition.setFrame(view: self.backgroundEffectView, frame: CGRect(origin: .zero, size: layout.size)) + transition.setPosition(view: self.backgroundView, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)) + transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: .zero, size: layout.size)) - if self.panTranslation == nil { - let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize) - transition.setFrame(view: self.previewContainerView, frame: previewFrame) - transition.setFrame(view: self.effectivePreviewView, frame: CGRect(origin: .zero, size: previewFrame.size)) - transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size)) - } + transition.setPosition(view: self.containerView, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)) + transition.setBounds(view: self.containerView, bounds: CGRect(origin: .zero, size: layout.size)) - if isFirstTime { - self.animateIn() + transition.setFrame(view: self.transitionDimView, frame: CGRect(origin: .zero, size: layout.size)) + + + let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize) + transition.setFrame(view: self.previewContainerView, frame: previewFrame) + transition.setFrame(view: self.effectivePreviewView, frame: CGRect(origin: .zero, size: previewFrame.size)) + transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size)) + + let screenCornerRadius = layout.deviceMetrics.screenCornerRadius + if screenCornerRadius > 0.0, self.transitionCornersView.image == nil { + self.transitionCornersView.image = generateImage(CGSize(width: screenCornerRadius, height: screenCornerRadius * 3.0), rotatedContext: { size, context in + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + context.setBlendMode(.clear) + + let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: size.width * 2.0, height: size.height)), cornerRadius: size.width) + context.addPath(path.cgPath) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(screenCornerRadius)) } + + transition.setPosition(view: self.transitionCornersView, position: CGPoint(x: layout.size.width + screenCornerRadius / 2.0, y: layout.size.height / 2.0)) + transition.setBounds(view: self.transitionCornersView, bounds: CGRect(origin: .zero, size: CGSize(width: screenCornerRadius, height: layout.size.height))) } } @@ -1167,15 +1197,36 @@ public class CameraScreen: ViewController { fileprivate let holder: CameraHolder? fileprivate let transitionIn: TransitionIn? fileprivate let transitionOut: (Bool) -> TransitionOut? - fileprivate let completion: (Signal) -> Void + public final class ResultTransition { + public weak var sourceView: UIView? + public let sourceRect: CGRect + public let sourceImage: UIImage? + public let transitionOut: () -> (UIView, CGRect)? + + public init( + sourceView: UIView, + sourceRect: CGRect, + sourceImage: UIImage?, + transitionOut: @escaping () -> (UIView, CGRect)? + ) { + self.sourceView = sourceView + self.sourceRect = sourceRect + self.sourceImage = sourceImage + self.transitionOut = transitionOut + } + } + fileprivate let completion: (Signal, ResultTransition?) -> Void + + private var audioSessionDisposable: Disposable? + public init( context: AccountContext, mode: Mode, holder: CameraHolder? = nil, transitionIn: TransitionIn?, transitionOut: @escaping (Bool) -> TransitionOut?, - completion: @escaping (Signal) -> Void + completion: @escaping (Signal, ResultTransition?) -> Void ) { self.context = context self.mode = mode @@ -1186,15 +1237,21 @@ public class CameraScreen: ViewController { super.init(navigationBarPresentationData: nil) - self.statusBar.statusBarStyle = .White + self.statusBar.statusBarStyle = .Ignore self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.navigationPresentation = .flatModal + + self.requestAudioSession() } required public init(coder: NSCoder) { preconditionFailure() } + + deinit { + self.audioSessionDisposable?.dispose() + } override public func loadDisplayNode() { self.displayNode = Node(controller: self) @@ -1202,31 +1259,61 @@ public class CameraScreen: ViewController { super.displayNodeDidLoad() } - public func returnFromEditor() { - self.node.animateInFromEditor() + private func requestAudioSession() { + self.audioSessionDisposable = self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: .recordWithOthers, activate: { _ in }, deactivate: { _ in + return .single(Void()) + }) } - public func commitTransitionToEditor() { - self.node.commitTransitionToEditor() + private weak var galleryController: ViewController? + public func returnFromEditor() { + self.node.animateInFromEditor(toGallery: self.galleryController != nil) } func presentGallery() { - var dismissGalleryControllerImpl: (() -> Void)? - let controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] result in - dismissGalleryControllerImpl?() + var didStopCameraCapture = false + let stopCameraCapture = { [weak self] in + guard !didStopCameraCapture, let self else { + return + } + didStopCameraCapture = true + + self.node.pauseCameraCapture() + } + + let controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut in if let self { - self.node.animateOutToEditor() + stopCameraCapture() + + let resultTransition = ResultTransition( + sourceView: transitionView, + sourceRect: transitionRect, + sourceImage: transitionImage, + transitionOut: transitionOut + ) if let asset = result as? PHAsset { - self.completion(.single(.asset(asset))) + self.completion(.single(.asset(asset)), resultTransition) } else if let draft = result as? MediaEditorDraft { - self.completion(.single(.draft(draft))) + self.completion(.single(.draft(draft)), resultTransition) } } + }, dismissed: { [weak self] in + if let self { + self.node.resumeCameraCapture() + } }) - dismissGalleryControllerImpl = { [weak controller] in - controller?.dismiss(animated: true) + controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in + if let self, let controller { + let transitionFactor = controller.modalStyleOverlayTransitionFactor + if transitionFactor > 0.1 { + stopCameraCapture() + } + self.node.updateModalTransitionFactor(transitionFactor, transition: transition) + } } - push(controller) + + self.galleryController = controller + self.push(controller) } public func presentDraftTooltip() { @@ -1234,21 +1321,82 @@ public class CameraScreen: ViewController { } private var isDismissed = false - fileprivate func requestDismiss(animated: Bool) { + fileprivate func requestDismiss(animated: Bool, interactive: Bool = false) { guard !self.isDismissed else { return } + self.node.camera.stopCapture(invalidate: true) self.isDismissed = true - self.statusBar.statusBarStyle = .Ignore if animated { - self.node.animateOut(completion: { - self.dismiss(animated: false) + if !interactive { + if let navigationController = self.navigationController as? NavigationController { + navigationController.updateRootContainerTransitionOffset(self.node.frame.width, transition: .immediate) + } + } + self.updateTransitionProgress(0.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in + self?.dismiss(animated: false) }) } else { self.dismiss(animated: false) } } + private var isTransitioning = false + public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { + self.isTransitioning = true + let offsetX = (1.0 - transitionFraction) * self.node.frame.width * -1.0 + transition.updateTransform(layer: self.node.backgroundView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0)) + transition.updateTransform(layer: self.node.containerView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0)) + let scale = max(0.8, min(1.0, 0.8 + 0.2 * transitionFraction)) + transition.updateSublayerTransformScaleAndOffset(layer: self.node.containerView.layer, scale: scale, offset: CGPoint(x: -offsetX * 1.0 / scale * 0.5, y: 0.0), completion: { _ in + completion() + }) + + let dimAlpha = 0.6 * (1.0 - transitionFraction) + transition.updateAlpha(layer: self.node.transitionDimView.layer, alpha: dimAlpha) + transition.updateTransform(layer: self.node.transitionCornersView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0)) + + self.statusBar.updateStatusBarStyle(transitionFraction > 0.45 ? .White : .Ignore, animated: true) + + if let navigationController = self.navigationController as? NavigationController { + let offsetX = transitionFraction * self.node.frame.width + navigationController.updateRootContainerTransitionOffset(offsetX, transition: transition) + } + } + + public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) { + self.isTransitioning = false + if dismissing { + if transitionFraction < 0.7 || velocity > 1000.0 { + self.requestDismiss(animated: true, interactive: true) + } else { + self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in + if let self, let navigationController = self.navigationController as? NavigationController { + navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) + } + }) + } + } else { + if transitionFraction > 0.33 || velocity > 1000.0 { + self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in + if let self, let navigationController = self.navigationController as? NavigationController { + navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) + self.node.requestUpdateLayout(hasAppeared: true, transition: .immediate) + } + }) + } else { + self.requestDismiss(animated: true, interactive: true) + } + } + } + + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + if !flag { + self.galleryController?.dismiss(animated: false) + } + super.dismiss(animated: flag, completion: completion) + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index f8d3623a28..a6bb919642 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -108,7 +108,7 @@ private final class ShutterButtonContentComponent: Component { } self.blobView.updatePrimaryOffset(bandedOffset, transition: transition) } else { - self.blobView.updatePrimaryOffset(0.0, transition: .spring(duration: 0.15)) + self.blobView.updatePrimaryOffset(0.0, transition: .spring(duration: 0.2)) } } } @@ -411,11 +411,11 @@ final class CaptureControlsComponent: Component { if let galleryButton = self.galleryButtonView.view { blobOffset = galleryButton.center.x - self.frame.width / 2.0 } - self.shutterUpdateOffset.invoke((blobOffset, .spring(duration: 0.5))) + self.shutterUpdateOffset.invoke((blobOffset, .spring(duration: 0.35))) } else { self.hapticFeedback.impact(.light) self.component?.shutterReleased() - self.shutterUpdateOffset.invoke((0.0, .spring(duration: 0.3))) + self.shutterUpdateOffset.invoke((0.0, .spring(duration: 0.25))) } default: break @@ -450,7 +450,7 @@ final class CaptureControlsComponent: Component { self.component?.zoomUpdated(1.0) } - if location.x < self.frame.width / 2.0 - 20.0 { + if location.x < self.frame.width / 2.0 - 30.0 { if location.x < self.frame.width / 2.0 - 60.0 { self.component?.swipeHintUpdated(.releaseLock) if location.x < 75.0 { @@ -464,7 +464,7 @@ final class CaptureControlsComponent: Component { blobOffset = rubberBandingOffset(offset: blobOffset, bandingStart: 0.0) isBanding = true } - } else if location.x > self.frame.width / 2.0 + 20.0 { + } else if location.x > self.frame.width / 2.0 + 30.0 { self.component?.swipeHintUpdated(.flip) if location.x > self.frame.width / 2.0 + 60.0 { self.panBlobState = .transientToFlip @@ -488,8 +488,8 @@ final class CaptureControlsComponent: Component { } var transition: Transition = .immediate if let wasBanding = self.wasBanding, wasBanding != isBanding { - self.hapticFeedback.impact(.light) - transition = .spring(duration: 0.3) + //self.hapticFeedback.impact(.light) + transition = .spring(duration: 0.35) } self.wasBanding = isBanding self.shutterUpdateOffset.invoke((blobOffset, transition)) @@ -574,11 +574,14 @@ final class CaptureControlsComponent: Component { transition: .immediate, component: AnyComponent( CameraButton( - content: AnyComponent( - Image( - image: state.cachedAssetImage?.1, - size: CGSize(width: 50.0, height: 50.0), - contentMode: .scaleAspectFill + content: AnyComponentWithIdentity( + id: "gallery", + component: AnyComponent( + Image( + image: state.cachedAssetImage?.1, + size: CGSize(width: 50.0, height: 50.0), + contentMode: .scaleAspectFill + ) ) ), tag: component.galleryButtonTag, @@ -632,8 +635,11 @@ final class CaptureControlsComponent: Component { transition: .immediate, component: AnyComponent( CameraButton( - content: AnyComponent( - FlipButtonContentComponent(action: flipAnimationAction) + content: AnyComponentWithIdentity( + id: "flip", + component: AnyComponent( + FlipButtonContentComponent(action: flipAnimationAction) + ) ), minSize: CGSize(width: 44.0, height: 44.0), action: { diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/FocusCrosshairsView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/FocusCrosshairsView.swift new file mode 100644 index 0000000000..975831c251 --- /dev/null +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/FocusCrosshairsView.swift @@ -0,0 +1,22 @@ +import Foundation +import UIKit + +final class FocusCrosshairsView: UIView { + private let indicatorView: UIImageView + + override init(frame: CGRect) { + self.indicatorView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.indicatorView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(pointOfInterest: CGPoint) { + + } +} diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift index ccc76a634b..7230dff4cd 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift @@ -41,15 +41,15 @@ private final class AnimatableProperty { } func update(value: T, transition: Transition = .immediate) { + let currentTimestamp = CACurrentMediaTime() if case .none = transition.animation { if let animation = self.animation, case let .curve(duration, curve) = animation.animation { self.value = value - let elapsed = duration - (CACurrentMediaTime() - animation.startTimestamp) - if elapsed < 0.1 { - self.presentationValue = value - self.animation = nil + let elapsed = duration - (currentTimestamp - animation.startTimestamp) + if let presentationValue = self.presentationValue as? CGFloat, let newValue = value as? CGFloat, abs(presentationValue - newValue) > 0.56 { + self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed * 0.8, curve: curve), startTimestamp: currentTimestamp) } else { - self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: CACurrentMediaTime()) + self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: currentTimestamp) } } else { self.value = value @@ -58,7 +58,7 @@ private final class AnimatableProperty { } } else { self.value = value - self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: CACurrentMediaTime()) + self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: currentTimestamp) } } @@ -73,13 +73,18 @@ private final class AnimatableProperty { case .easeInOut: t = listViewAnimationCurveEaseInOut(t) case .spring: - t = listViewAnimationCurveSystem(t) + t = listViewAnimationCurveEaseInOut(t) + //t = listViewAnimationCurveSystem(t) case let .custom(x1, y1, x2, y2): t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t) } self.presentationValue = animation.valueAt(t) as! T - return timeFromStart <= duration + if timeFromStart <= duration { + return true + } + self.animation = nil + return false } } @@ -204,26 +209,21 @@ final class ShutterBlobView: MTKView, MTKViewDelegate { super.init(frame: CGRect(), device: device) - self.delegate = self - self.isOpaque = false self.backgroundColor = .clear - self.framebufferOnly = true self.colorPixelFormat = .bgra8Unorm + self.framebufferOnly = true + self.presentsWithTransaction = true + self.isPaused = true + self.delegate = self self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in self?.tick() } self.displayLink?.isPaused = true - - self.isPaused = true } - public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { - self.viewportDimensions = size - } - required public init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -232,6 +232,10 @@ final class ShutterBlobView: MTKView, MTKViewDelegate { self.displayLink?.invalidate() } + public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { + self.viewportDimensions = size + } + func updateState(_ state: BlobState, transition: Transition = .immediate) { guard self.state != state else { return @@ -253,6 +257,7 @@ final class ShutterBlobView: MTKView, MTKViewDelegate { } let mappedOffset = offset / self.frame.height * 2.0 self.primaryOffset.update(value: mappedOffset, transition: transition) + self.tick() } @@ -262,6 +267,7 @@ final class ShutterBlobView: MTKView, MTKViewDelegate { } let mappedOffset = offset / self.frame.height * 2.0 self.secondaryOffset.update(value: mappedOffset, transition: transition) + self.tick() } @@ -288,47 +294,51 @@ final class ShutterBlobView: MTKView, MTKViewDelegate { private func tick() { self.updateAnimations() - self.draw() + autoreleasepool { + self.draw(in: self) + } } - - override public func draw(_ rect: CGRect) { - self.redraw(drawable: self.currentDrawable!) + + override func layoutSubviews() { + super.layoutSubviews() + + self.tick() } - - private func redraw(drawable: MTLDrawable) { + + func draw(in view: MTKView) { guard let commandBuffer = self.commandQueue.makeCommandBuffer() else { return } - - let renderPassDescriptor = self.currentRenderPassDescriptor! + + guard let renderPassDescriptor = self.currentRenderPassDescriptor else { + return + } renderPassDescriptor.colorAttachments[0].loadAction = .clear renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0.0) - + guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { return } - + let viewportDimensions = self.viewportDimensions renderEncoder.setViewport(MTLViewport(originX: 0.0, originY: 0.0, width: viewportDimensions.width, height: viewportDimensions.height, znear: -1.0, zfar: 1.0)) - renderEncoder.setRenderPipelineState(self.drawPassthroughPipelineState) - + let w = Float(1) let h = Float(1) - var vertices: [Float] = [ - w, -h, + w, -h, -w, -h, -w, h, - w, -h, + w, -h, -w, h, - w, h + w, h ] renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0) - + var resolution = simd_uint2(UInt32(viewportDimensions.width), UInt32(viewportDimensions.height)) renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout.size * 2, index: 0) - + var primaryParameters = simd_float4( Float(self.primarySize.presentationValue), Float(self.primaryOffset.presentationValue), @@ -343,23 +353,15 @@ final class ShutterBlobView: MTKView, MTKViewDelegate { Float(self.secondaryRedness.presentationValue) ) renderEncoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout.size, index: 2) - renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1) - renderEncoder.endEncoding() - drawable.present() - //commandBuffer.present(drawable) + + if let currentDrawable = self.currentDrawable { + commandBuffer.present(currentDrawable) + } commandBuffer.commit() - commandBuffer.waitUntilScheduled() - } - - override func layoutSubviews() { - super.layoutSubviews() - - self.tick() - } - - func draw(in view: MTKView) { + //commandBuffer.waitUntilScheduled() + //self.currentDrawable?.present() } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ZoomComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ZoomComponent.swift index bca4ee384c..a1499a7dc7 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ZoomComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ZoomComponent.swift @@ -120,15 +120,6 @@ final class ZoomComponent: Component { let spacing: CGFloat = 3.0 let buttonSize = CGSize(width: 37.0, height: 37.0) let size: CGSize = CGSize(width: buttonSize.width * CGFloat(component.availableValues.count) + spacing * CGFloat(component.availableValues.count - 1) + sideInset * 2.0, height: 43.0) - - if let screenTransition = transition.userData(CameraScreenTransition.self) { - switch screenTransition { - case .animateIn: - self.animateIn() - case .animateOut: - self.animateOut() - } - } var i = 0 var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 3.0), size: buttonSize) diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorEnhance.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorEnhance.metal index 64c16902ae..e8827432bb 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorEnhance.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorEnhance.metal @@ -78,7 +78,7 @@ fragment half4 enhanceColorLookupFragmentShader(RasterizerData in [[stage_in]], constexpr sampler lutSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero); float2 sourceCoord = in.texCoord; - half4 color = sourceTexture.sample(colorSampler,sourceCoord); + half4 color = sourceTexture.sample(colorSampler, sourceCoord); half3 hslColor = rgbToHsl(color.rgb); float txf = sourceCoord.x * tileGridSize.x - 0.5; diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorVideo.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorVideo.metal new file mode 100644 index 0000000000..ac60ab77da --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorVideo.metal @@ -0,0 +1,42 @@ +#include +#include "EditorCommon.h" +#include "EditorUtils.h" + +using namespace metal; + +static inline float4 BT709_decode(const float Y, const float Cb, const float Cr) { + float Yn = Y; + + float Cbn = (Cb - (128.0f/255.0f)); + float Crn = (Cr - (128.0f/255.0f)); + + float3 YCbCr = float3(Yn, Cbn, Crn); + + const float3x3 kColorConversion709 = float3x3(float3(1.0, 1.0, 1.0), + float3(0.0f, -0.1873, 1.8556), + float3(1.5748, -0.4681, 0.0)); + + float3 rgb = kColorConversion709 * YCbCr; + + rgb = saturate(rgb); + + return float4(rgb.r, rgb.g, rgb.b, 1.0f); +} + + +fragment float4 bt709ToRGBFragmentShader(RasterizerData in [[stage_in]], + texture2d inYTexture [[texture(0)]], + texture2d inUVTexture [[texture(1)]] + ) +{ + constexpr sampler textureSampler (mag_filter::nearest, min_filter::nearest); + + float Y = float(inYTexture.sample(textureSampler, in.texCoord).r); + half2 uvSamples = inUVTexture.sample(textureSampler, in.texCoord).rg; + + float Cb = float(uvSamples[0]); + float Cr = float(uvSamples[1]); + + float4 pixel = BT709_decode(Y, Cb, Cr); + return pixel; +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift index d10be35fe2..4224ff1f07 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift @@ -18,6 +18,45 @@ struct MediaEditorAdjustments { var warmth: simd_float1 var grain: simd_float1 var vignette: simd_float1 + + var hasValues: Bool { + let epsilon: simd_float1 = 0.005 + + if abs(self.shadows) > epsilon { + return true + } + if abs(self.highlights) > epsilon { + return true + } + if abs(self.contrast) > epsilon { + return true + } + if abs(self.fade) > epsilon { + return true + } + if abs(self.saturation) > epsilon { + return true + } + if abs(self.shadowsTintIntensity) > epsilon { + return true + } + if abs(self.highlightsTintIntensity) > epsilon { + return true + } + if abs(self.exposure) > epsilon { + return true + } + if abs(self.warmth) > epsilon { + return true + } + if abs(self.grain) > epsilon { + return true + } + if abs(self.vignette) > epsilon { + return true + } + return false + } } final class AdjustmentsRenderPass: DefaultRenderPass { @@ -50,10 +89,14 @@ final class AdjustmentsRenderPass: DefaultRenderPass { return "adjustmentsFragmentShader" } - override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) - - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + guard self.adjustments.hasValues else { + return input + } + self.setupVerticesBuffer(device: device) + + let width = input.width + let height = input.height if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height { self.adjustments.dimensions = simd_float2(Float(width), Float(height)) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/BlurRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/BlurRenderPass.swift index 030fff5640..eda463bb1d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/BlurRenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/BlurRenderPass.swift @@ -29,11 +29,11 @@ private final class BlurGaussianPass: RenderPass { } - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { return nil } - func process(input: MTLTexture, intensity: Float, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process(input: MTLTexture, intensity: Float, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { let radius = round(4.0 + intensity * 26.0) if self.blur?.sigma != radius { self.blur = MPSImageGaussianBlur(device: device, sigma: radius) @@ -67,10 +67,8 @@ private final class BlurLinearPass: DefaultRenderPass { return "blurLinearFragmentShader" } - func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) - - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) let renderPassDescriptor = MTLRenderPassDescriptor() renderPassDescriptor.colorAttachments[0].texture = output @@ -83,7 +81,7 @@ private final class BlurLinearPass: DefaultRenderPass { renderCommandEncoder.setViewport(MTLViewport( originX: 0, originY: 0, - width: Double(width), height: Double(height), + width: Double(input.width), height: Double(input.height), znear: -1.0, zfar: 1.0) ) @@ -105,10 +103,8 @@ private final class BlurRadialPass: DefaultRenderPass { return "blurRadialFragmentShader" } - func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) - - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) let renderPassDescriptor = MTLRenderPassDescriptor() renderPassDescriptor.colorAttachments[0].texture = output @@ -121,7 +117,7 @@ private final class BlurRadialPass: DefaultRenderPass { renderCommandEncoder.setViewport(MTLViewport( originX: 0, originY: 0, - width: Double(width), height: Double(height), + width: Double(input.width), height: Double(input.height), znear: -1.0, zfar: 1.0) ) @@ -145,11 +141,9 @@ private final class BlurPortraitPass: DefaultRenderPass { return "blurPortraitFragmentShader" } - func process(input: MTLTexture, blurredTexture: MTLTexture, maskTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) + func process(input: MTLTexture, blurredTexture: MTLTexture, maskTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) - let renderPassDescriptor = MTLRenderPassDescriptor() renderPassDescriptor.colorAttachments[0].texture = output renderPassDescriptor.colorAttachments[0].loadAction = .dontCare @@ -161,7 +155,7 @@ private final class BlurPortraitPass: DefaultRenderPass { renderCommandEncoder.setViewport(MTLViewport( originX: 0, originY: 0, - width: Double(width), height: Double(height), + width: Double(input.width), height: Double(input.height), znear: -1.0, zfar: 1.0) ) @@ -208,17 +202,18 @@ final class BlurRenderPass: RenderPass { self.portraitPass.setup(device: device, library: library) } - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.process(input: input, maskTexture: self.maskTexture, rotation: rotation, device: device, commandBuffer: commandBuffer) + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.process(input: input, maskTexture: self.maskTexture, device: device, commandBuffer: commandBuffer) } - func process(input: MTLTexture, maskTexture: MTLTexture?, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process(input: MTLTexture, maskTexture: MTLTexture?, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { guard self.intensity > 0.005 && self.mode != .off else { return input } - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) - + let width = input.width + let height = input.height + if self.cachedTexture == nil { self.value.aspectRatio = Float(height) / Float(width) @@ -235,18 +230,18 @@ final class BlurRenderPass: RenderPass { self.cachedTexture = texture } - guard let blurredTexture = self.blurPass.process(input: input, intensity: self.intensity, rotation: rotation, device: device, commandBuffer: commandBuffer), let output = self.cachedTexture else { + guard let blurredTexture = self.blurPass.process(input: input, intensity: self.intensity, device: device, commandBuffer: commandBuffer), let output = self.cachedTexture else { return input } switch self.mode { case .linear: - return self.linearPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer) + return self.linearPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, device: device, commandBuffer: commandBuffer) case .radial: - return self.radialPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer) + return self.radialPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, device: device, commandBuffer: commandBuffer) case .portrait: if let maskTexture { - return self.portraitPass.process(input: input, blurredTexture: blurredTexture, maskTexture: maskTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer) + return self.portraitPass.process(input: input, blurredTexture: blurredTexture, maskTexture: maskTexture, values: self.value, output: output, device: device, commandBuffer: commandBuffer) } else { return input } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/EnhanceRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/EnhanceRenderPass.swift index 5a5f85f3ec..400369f49e 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/EnhanceRenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/EnhanceRenderPass.swift @@ -21,20 +21,12 @@ private final class EnhanceLightnessPass: DefaultRenderPass { return .r8Unorm } - func process(input: MTLTexture, size: TextureSize, scale: simd_float2, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) + func process(input: MTLTexture, size: TextureSize, scale: simd_float2, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) - let width: Int - let height: Int - switch rotation { - case .rotate90Degrees, .rotate270Degrees: - width = size.height - height = size.width - default: - width = size.width - height = size.height - } - + let width = size.width + let height = size.height + if self.cachedTexture == nil { let textureDescriptor = MTLTextureDescriptor() textureDescriptor.textureType = .type2D @@ -130,7 +122,7 @@ private final class EnhanceLUTGeneratorPass: RenderPass { } } - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { return nil } @@ -204,10 +196,11 @@ private final class EnhanceLookupPass: DefaultRenderPass { return "enhanceColorLookupFragmentShader" } - func process(input: MTLTexture, lookupTexture: MTLTexture, value: simd_float1, gridSize: simd_float2, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) + func process(input: MTLTexture, lookupTexture: MTLTexture, value: simd_float1, gridSize: simd_float2, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + let width = input.width + let height = input.height if self.cachedTexture == nil { let textureDescriptor = MTLTextureDescriptor() @@ -269,7 +262,7 @@ final class EnhanceRenderPass: RenderPass { self.lookupPass.setup(device: device, library: library) } - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { guard self.value > 0.005 else { return input } @@ -279,12 +272,12 @@ final class EnhanceRenderPass: RenderPass { let lightnessSize = TextureSize(width: input.width + dX, height: input.height + dY) let lightnessScale = simd_float2(Float(input.width + dX) / Float(input.width), Float(input.height + dY) / Float(input.height)) - let lightness = self.lightnessPass.process(input: input, size: lightnessSize, scale: lightnessScale, rotation: rotation, device: device, commandBuffer: commandBuffer) + let lightness = self.lightnessPass.process(input: input, size: lightnessSize, scale: lightnessScale, device: device, commandBuffer: commandBuffer) let lookupTexture = self.lutGeneratorPass.process(input: lightness!, gridSize: self.tileGridSize, clipLimit: self.clipLimit, device: device, commandBuffer: commandBuffer) let gridSize = simd_float2(Float(self.tileGridSize.width), Float(self.tileGridSize.height)) - let output = self.lookupPass.process(input: input, lookupTexture: lookupTexture!, value: self.value, gridSize: gridSize, rotation: rotation, device: device, commandBuffer: commandBuffer) + let output = self.lookupPass.process(input: input, lookupTexture: lookupTexture!, value: self.value, gridSize: gridSize, device: device, commandBuffer: commandBuffer) return output } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift index 1402227669..db830bd4d2 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift @@ -8,6 +8,7 @@ final class HistogramCalculationPass: DefaultRenderPass { fileprivate var histogramBuffer: MTLBuffer? fileprivate var calculation: MPSImageHistogram? + var isEnabled = false var updated: ((Data) -> Void)? override var fragmentShaderFunctionName: String { @@ -40,58 +41,61 @@ final class HistogramCalculationPass: DefaultRenderPass { super.setup(device: device, library: library) } - override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) - - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) - - if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height { - let textureDescriptor = MTLTextureDescriptor() - textureDescriptor.textureType = .type2D - textureDescriptor.width = width - textureDescriptor.height = height - textureDescriptor.pixelFormat = .r8Unorm - textureDescriptor.storageMode = .shared - textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] - guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + if self.isEnabled { + self.setupVerticesBuffer(device: device) + + let width = input.width + let height = input.height + + if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height { + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.width = width + textureDescriptor.height = height + textureDescriptor.pixelFormat = .r8Unorm + textureDescriptor.storageMode = .shared + textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] + guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + return input + } + self.cachedTexture = texture + texture.label = "lumaTexture" + } + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture! + renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + renderPassDescriptor.colorAttachments[0].storeAction = .store + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { return input } - self.cachedTexture = texture - texture.label = "lumaTexture" - } - - let renderPassDescriptor = MTLRenderPassDescriptor() - renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture! - renderPassDescriptor.colorAttachments[0].loadAction = .dontCare - renderPassDescriptor.colorAttachments[0].storeAction = .store - renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) - guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { - return input - } - - renderCommandEncoder.setViewport(MTLViewport( - originX: 0, originY: 0, - width: Double(width), height: Double(height), - znear: -1.0, zfar: 1.0) - ) - - renderCommandEncoder.setFragmentTexture(input, index: 0) - - var texCoordScales = simd_float2(x: 1.0, y: 1.0) - renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout.stride, index: 0) - - self.encodeDefaultCommands(using: renderCommandEncoder) - - renderCommandEncoder.endEncoding() - - if let histogramBuffer = self.histogramBuffer, let calculation = self.calculation { - calculation.encode(to: commandBuffer, sourceTexture: input, histogram: histogramBuffer, histogramOffset: 0) - let lumaHistogramBufferLength = calculation.histogramSize(forSourceFormat: .r8Unorm) - calculation.encode(to: commandBuffer, sourceTexture: self.cachedTexture!, histogram: histogramBuffer, histogramOffset: histogramBuffer.length - lumaHistogramBufferLength) + renderCommandEncoder.setViewport(MTLViewport( + originX: 0, originY: 0, + width: Double(width), height: Double(height), + znear: -1.0, zfar: 1.0) + ) - let histogramData = Data(bytes: histogramBuffer.contents(), count: histogramBuffer.length) - self.updated?(histogramData) + renderCommandEncoder.setFragmentTexture(input, index: 0) + + var texCoordScales = simd_float2(x: 1.0, y: 1.0) + renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout.stride, index: 0) + + self.encodeDefaultCommands(using: renderCommandEncoder) + + renderCommandEncoder.endEncoding() + + if let histogramBuffer = self.histogramBuffer, let calculation = self.calculation { + calculation.encode(to: commandBuffer, sourceTexture: input, histogram: histogramBuffer, histogramOffset: 0) + + let lumaHistogramBufferLength = calculation.histogramSize(forSourceFormat: .r8Unorm) + calculation.encode(to: commandBuffer, sourceTexture: self.cachedTexture!, histogram: histogramBuffer, histogramOffset: histogramBuffer.length - lumaHistogramBufferLength) + + let histogramData = Data(bytes: histogramBuffer.contents(), count: histogramBuffer.length) + self.updated?(histogramData) + } } return input diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift index ce7b5a7fba..8017410b88 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift @@ -3,45 +3,57 @@ import AVFoundation import Metal import MetalKit import Display +import Accelerate + +func loadTexture(image: UIImage, device: MTLDevice) -> MTLTexture? { + func dataForImage(_ image: UIImage) -> UnsafeMutablePointer { + let imageRef = image.cgImage + let width = Int(image.size.width) + let height = Int(image.size.height) + let colorSpace = CGColorSpaceCreateDeviceRGB() + + let rawData = UnsafeMutablePointer.allocate(capacity: width * height * 4) + let bytePerPixel = 4 + let bytesPerRow = bytePerPixel * Int(width) + let bitsPerComponent = 8 + let bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue + CGImageAlphaInfo.premultipliedFirst.rawValue + let context = CGContext.init(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) + context?.draw(imageRef!, in: CGRect(x: 0, y: 0, width: width, height: height)) + + return rawData + } + + let width = Int(image.size.width * image.scale) + let height = Int(image.size.height * image.scale) + let bytePerPixel = 4 + let bytesPerRow = bytePerPixel * width + + var texture : MTLTexture? + let region = MTLRegionMake2D(0, 0, Int(width), Int(height)) + let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: width, height: height, mipmapped: false) + texture = device.makeTexture(descriptor: textureDescriptor) + + let data = dataForImage(image) + texture?.replace(region: region, mipmapLevel: 0, withBytes: data, bytesPerRow: bytesPerRow) + + return texture +} final class ImageTextureSource: TextureSource { weak var output: TextureConsumer? - var textureLoader: MTKTextureLoader? var texture: MTLTexture? init(image: UIImage, renderTarget: RenderTarget) { - guard let device = renderTarget.mtlDevice, var cgImage = image.cgImage else { - return + if let device = renderTarget.mtlDevice { + self.texture = loadTexture(image: image, device: device) } - let textureLoader = MTKTextureLoader(device: device) - self.textureLoader = textureLoader - - if let bitsPerPixel = image.cgImage?.bitsPerPixel, bitsPerPixel > 32 { - let updatedImage = generateImage(image.size, contextGenerator: { size, context in - context.setFillColor(UIColor.black.cgColor) - context.fill(CGRect(origin: .zero, size: size)) - context.draw(cgImage, in: CGRect(origin: .zero, size: size)) - }, opaque: false) - cgImage = updatedImage?.cgImage ?? cgImage - } - - self.texture = try? textureLoader.newTexture(cgImage: cgImage, options: [.SRGB : false]) - } - - func start() { - - } - - func pause() { - } func connect(to consumer: TextureConsumer) { self.output = consumer - if let texture = self.texture { - self.output?.consumeTexture(texture, rotation: .rotate0Degrees) + self.output?.consumeTexture(texture) } } } @@ -59,3 +71,17 @@ func pixelBufferToMTLTexture(pixelBuffer: CVPixelBuffer, textureCache: CVMetalTe return nil } + +func getTextureImage(device: MTLDevice, texture: MTLTexture) -> UIImage? { + let colorSpace = CGColorSpaceCreateDeviceRGB() + let context = CIContext(mtlDevice: device, options: [:]) + guard var ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: colorSpace]) else { + return nil + } + let transform = CGAffineTransform(1.0, 0.0, 0.0, -1.0, 0.0, ciImage.extent.height) + ciImage = ciImage.transformed(by: transform) + guard let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: CGSize(width: ciImage.extent.width, height: ciImage.extent.height))) else { + return nil + } + return UIImage(cgImage: cgImage) +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index d74d2a80b8..1cdddad3ec 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -76,7 +76,15 @@ public final class MediaEditor { public var histogram: Signal { return self.histogramPromise.get() } - + public var isHistogramEnabled: Bool { + get { + return self.histogramCalculationPass.isEnabled + } + set { + self.histogramCalculationPass.isEnabled = newValue + } + } + private var textureCache: CVMetalTextureCache! public var hasPortraitMask: Bool { @@ -90,7 +98,7 @@ public final class MediaEditor { public var resultImage: UIImage? { return self.renderer.finalRenderedImage() } - + private let playerPromise = Promise() private var playerPlaybackState: (Double, Double, Bool) = (0.0, 0.0, false) { didSet { @@ -250,6 +258,7 @@ public final class MediaEditor { } } + private var volumeFade: SwiftSignalKit.Timer? private func setupSource() { guard let renderTarget = self.previewView else { return @@ -367,14 +376,19 @@ public final class MediaEditor { |> deliverOnMainQueue).start(next: { [weak self] sourceAndColors in if let self { let (source, image, player, topColor, bottomColor) = sourceAndColors + self.renderer.onNextRender = { [weak self] in + self?.previewView?.removeTransitionImage() + } self.renderer.textureSource = source self.player = player self.playerPromise.set(.single(player)) self.gradientColorsValue = (topColor, bottomColor) self.setGradientColors([topColor, bottomColor]) - self.maybeGeneratePersonSegmentation(image) - + if player == nil { + self.maybeGeneratePersonSegmentation(image) + } + if let player { self.timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 10), queue: DispatchQueue.main) { [weak self] time in guard let self, let duration = player.currentItem?.duration.seconds else { @@ -394,6 +408,7 @@ public final class MediaEditor { } }) self.player?.play() + self.volumeFade = self.player?.fadeVolume(from: 0.0, to: 1.0, duration: 0.4) } } }) @@ -431,16 +446,45 @@ public final class MediaEditor { self.values = self.values.withUpdatedVideoIsMuted(videoIsMuted) } - public func seek(_ position: Double, andPlay: Bool) { - if !andPlay { + private var targetTimePosition: (CMTime, Bool)? + private var updatingTimePosition = false + public func seek(_ position: Double, andPlay play: Bool) { + if !play { self.player?.pause() } - self.player?.seek(to: CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0)), toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { _ in }) - if andPlay { + let targetPosition = CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0)) + if self.targetTimePosition?.0 != targetPosition { + self.targetTimePosition = (targetPosition, play) + if !self.updatingTimePosition { + self.updateVideoTimePosition() + } + } + if play { self.player?.play() } } + public func stop() { + self.player?.pause() + } + + private func updateVideoTimePosition() { + guard let (targetPosition, _) = self.targetTimePosition else { + return + } + self.updatingTimePosition = true + self.player?.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { [weak self] _ in + if let self { + if let (currentTargetPosition, _) = self.targetTimePosition, currentTargetPosition == targetPosition { + self.updatingTimePosition = false + self.targetTimePosition = nil + } else { + self.updateVideoTimePosition() + } + } + }) + } + public func setVideoTrimStart(_ trimStart: Double) { let trimEnd = self.values.videoTrimRange?.upperBound ?? self.playerPlaybackState.0 let trimRange = trimStart ..< trimEnd @@ -583,15 +627,23 @@ final class MediaEditorRenderChain { } case .shadowsTint: if let value = value as? TintValue { - let (red, green, blue, _) = value.color.components - self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) - self.adjustmentsPass.adjustments.shadowsTintIntensity = value.intensity + if value.color != .clear { + let (red, green, blue, _) = value.color.components + self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) + self.adjustmentsPass.adjustments.shadowsTintIntensity = value.intensity + } else { + self.adjustmentsPass.adjustments.shadowsTintIntensity = 0.0 + } } case .highlightsTint: if let value = value as? TintValue { - let (red, green, blue, _) = value.color.components - self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) - self.adjustmentsPass.adjustments.highlightsTintIntensity = value.intensity + if value.color != .clear { + let (red, green, blue, _) = value.color.components + self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) + self.adjustmentsPass.adjustments.highlightsTintIntensity = value.intensity + } else { + self.adjustmentsPass.adjustments.highlightsTintIntensity = 0.0 + } } case .blur: if let value = value as? BlurValue { @@ -626,3 +678,11 @@ final class MediaEditorRenderChain { } } } + +public func debugSaveImage(_ image: UIImage, name: String) { + let path = NSTemporaryDirectory() + "debug_\(name)_\(Int64.random(in: .min ... .max)).png" + print(path) + if let data = image.pngData() { + try? data.write(to: URL(fileURLWithPath: path)) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index b73c5a99c3..ccc6a9e539 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -12,8 +12,26 @@ import TelegramAnimatedStickerNode import YuvConversion import StickerResources +func mediaEditorGenerateGradientImage(size: CGSize, colors: [UIColor]) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, false, 1.0) + if let context = UIGraphicsGetCurrentContext() { + let gradientColors = colors.map { $0.cgColor } as CFArray + let colorSpace = CGColorSpaceCreateDeviceRGB() + + var locations: [CGFloat] = [0.0, 1.0] + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } + + let image = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + + return image +} + final class MediaEditorComposer { let device: MTLDevice? + private let colorSpace: CGColorSpace private let values: MediaEditorValues private let dimensions: CGSize @@ -34,27 +52,29 @@ final class MediaEditorComposer { self.dimensions = dimensions self.outputDimensions = outputDimensions + let colorSpace = CGColorSpaceCreateDeviceRGB() + self.colorSpace = colorSpace + self.renderer.addRenderChain(self.renderChain) self.renderer.addRenderPass(ComposerRenderPass()) - if let gradientColors = values.gradientColors { - let image = generateGradientImage(size: dimensions, scale: 1.0, colors: gradientColors, locations: [0.0, 1.0])! - self.gradientImage = CIImage(image: image)!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) + if let gradientColors = values.gradientColors, let image = mediaEditorGenerateGradientImage(size: dimensions, colors: gradientColors) { + self.gradientImage = CIImage(image: image, options: [.colorSpace: self.colorSpace])!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) } else { self.gradientImage = CIImage(color: .black) } - if let drawing = values.drawing, let drawingImage = CIImage(image: drawing) { + if let drawing = values.drawing, let drawingImage = CIImage(image: drawing, options: [.colorSpace: self.colorSpace]) { self.drawingImage = drawingImage.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) } else { self.drawingImage = nil } - self.entities = values.entities.map { $0.entity } .compactMap { composerEntityForDrawingEntity(account: account, entity: $0) } + self.entities = values.entities.map { $0.entity } .compactMap { composerEntityForDrawingEntity(account: account, entity: $0, colorSpace: colorSpace) } self.device = MTLCreateSystemDefaultDevice() if let device = self.device { - self.ciContext = CIContext(mtlDevice: device, options: [.workingColorSpace : NSNull()]) + self.ciContext = CIContext(mtlDevice: device, options: [.workingColorSpace : self.colorSpace]) CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &self.textureCache) } else { @@ -82,10 +102,10 @@ final class MediaEditorComposer { texture = CVMetalTextureGetTexture(textureRef!) } if let texture { - self.renderer.consumeTexture(texture, rotation: .rotate90Degrees) + self.renderer.consumeTexture(texture) self.renderer.renderFrame() - if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture) { + if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) { ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height)) var pixelBuffer: CVPixelBuffer? @@ -116,13 +136,12 @@ final class MediaEditorComposer { completion(nil, time) return } - if self.filteredImage == nil, let device = self.device, let cgImage = inputImage.cgImage { - let textureLoader = MTKTextureLoader(device: device) - if let texture = try? textureLoader.newTexture(cgImage: cgImage, options: [.SRGB : false]) { - self.renderer.consumeTexture(texture, rotation: .rotate0Degrees) + if self.filteredImage == nil, let device = self.device { + if let texture = loadTexture(image: inputImage, device: device) { + self.renderer.consumeTexture(texture) self.renderer.renderFrame() - if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture) { + if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) { ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height)) self.filteredImage = ciImage } @@ -137,7 +156,7 @@ final class MediaEditorComposer { makeEditorImageFrameComposition(inputImage: image, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, values: self.values, entities: self.entities, time: time, completion: { compositedImage in if var compositedImage { let scale = self.outputDimensions.width / self.dimensions.width - compositedImage = compositedImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) + compositedImage = compositedImage.samplingLinear().transformed(by: CGAffineTransform(scaleX: scale, y: scale)) self.ciContext?.render(compositedImage, to: pixelBuffer) completion(pixelBuffer, time) @@ -157,21 +176,21 @@ final class MediaEditorComposer { } public func makeEditorImageComposition(account: Account, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, completion: @escaping (UIImage?) -> Void) { - let inputImage = CIImage(image: inputImage)! + let colorSpace = CGColorSpaceCreateDeviceRGB() + let inputImage = CIImage(image: inputImage, options: [.colorSpace: colorSpace])! let gradientImage: CIImage var drawingImage: CIImage? - if let gradientColors = values.gradientColors { - let image = generateGradientImage(size: dimensions, scale: 1.0, colors: gradientColors, locations: [0.0, 1.0])! - gradientImage = CIImage(image: image)!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) + if let gradientColors = values.gradientColors, let image = mediaEditorGenerateGradientImage(size: dimensions, colors: gradientColors) { + gradientImage = CIImage(image: image, options: [.colorSpace: colorSpace])!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) } else { gradientImage = CIImage(color: .black) } - if let drawing = values.drawing, let image = CIImage(image: drawing) { + if let drawing = values.drawing, let image = CIImage(image: drawing, options: [.colorSpace: colorSpace]) { drawingImage = image.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) } - let entities: [MediaEditorComposerEntity] = values.entities.map { $0.entity }.compactMap { composerEntityForDrawingEntity(account: account, entity: $0) } + let entities: [MediaEditorComposerEntity] = values.entities.map { $0.entity }.compactMap { composerEntityForDrawingEntity(account: account, entity: $0, colorSpace: colorSpace) } makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: gradientImage, drawingImage: drawingImage, dimensions: dimensions, values: values, entities: entities, time: time, completion: { ciImage in if let ciImage { let context = CIContext(options: [.workingColorSpace : NSNull()]) @@ -190,7 +209,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) resultImage = gradientImage.composited(over: resultImage) - var mediaImage = inputImage.transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY)) + var mediaImage = inputImage.samplingLinear().transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY)) var initialScale: CGFloat if mediaImage.extent.height > mediaImage.extent.width { @@ -206,7 +225,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: resultImage = mediaImage.composited(over: resultImage) if let drawingImage { - resultImage = drawingImage.composited(over: resultImage) + resultImage = drawingImage.samplingLinear().composited(over: resultImage) } let frameRate: Float = 30.0 @@ -235,7 +254,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: } let index = i entity.image(for: time, frameRate: frameRate, completion: { image in - if var image = image { + if var image = image?.samplingLinear() { let resetTransform = CGAffineTransform(translationX: -image.extent.width / 2.0, y: -image.extent.height / 2.0) image = image.transformed(by: resetTransform) @@ -266,7 +285,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: maybeFinalize() } -private func composerEntityForDrawingEntity(account: Account, entity: DrawingEntity) -> MediaEditorComposerEntity? { +private func composerEntityForDrawingEntity(account: Account, entity: DrawingEntity, colorSpace: CGColorSpace) -> MediaEditorComposerEntity? { if let entity = entity as? DrawingStickerEntity { let content: MediaEditorComposerStickerEntity.Content switch entity.content { @@ -275,8 +294,8 @@ private func composerEntityForDrawingEntity(account: Account, entity: DrawingEnt case let .image(image): content = .image(image) } - return MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored) - } else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage) { + return MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored, colorSpace: colorSpace) + } else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) { if let entity = entity as? DrawingBubbleEntity { return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false) } else if let entity = entity as? DrawingSimpleShapeEntity { @@ -331,6 +350,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { let rotation: CGFloat let baseSize: CGSize? let mirrored: Bool + let colorSpace: CGColorSpace var isAnimated: Bool var source: AnimatedStickerNodeSource? @@ -349,13 +369,14 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { var imagePixelBuffer: CVPixelBuffer? let imagePromise = Promise() - init(account: Account, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool) { + init(account: Account, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool, colorSpace: CGColorSpace) { self.content = content self.position = position self.scale = scale self.rotation = rotation self.baseSize = baseSize self.mirrored = mirrored + self.colorSpace = colorSpace switch content { case let .file(file): @@ -373,7 +394,6 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { let queue = strongSelf.queue let frameSource = QueueLocalObject(queue: queue, generate: { return AnimatedStickerDirectFrameSource(queue: queue, data: data, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)! - //return AnimatedStickerCachedFrameSource(queue: queue, data: data, complete: complete, notifyUpdated: {})! }) frameSource.syncWith { frameSource in strongSelf.frameCount = frameSource.frameCount @@ -391,13 +411,13 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { } } else { self.isAnimated = false - self.disposables.add((chatMessageSticker(account: account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false) + self.disposables.add((chatMessageSticker(account: account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false, colorSpace: self.colorSpace) |> deliverOn(self.queue)).start(next: { [weak self] generator in - if let strongSelf = self { + if let self { let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: baseSize, boundingSize: baseSize, intrinsicInsets: UIEdgeInsets())) - let image = context?.generateImage() - if let image = image { - strongSelf.imagePromise.set(.single(image)) + let image = context?.generateImage(colorSpace: self.colorSpace) + if let image { + self.imagePromise.set(.single(image)) } } })) @@ -488,7 +508,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { } if let imagePixelBuffer { - let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, tintColor: tintColor) + let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, colorSpace: strongSelf.colorSpace, tintColor: tintColor) strongSelf.image = image } completion(strongSelf.image) @@ -508,9 +528,9 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { let _ = (self.imagePromise.get() |> take(1) |> deliverOn(self.queue)).start(next: { [weak self] image in - if let strongSelf = self { - strongSelf.image = CIImage(image: image) - completion(strongSelf.image) + if let self { + self.image = CIImage(image: image, options: [.colorSpace: self.colorSpace]) + completion(self.image) } }) } @@ -528,7 +548,7 @@ protocol MediaEditorComposerEntity { func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) } -private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, tintColor: UIColor?) -> CIImage? { +private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, colorSpace: CGColorSpace, tintColor: UIColor?) -> CIImage? { //let calculatedBytesPerRow = (4 * Int(width) + 31) & (~31) //assert(bytesPerRow == calculatedBytesPerRow) @@ -557,16 +577,17 @@ private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) - return CIImage(cvPixelBuffer: pixelBuffer) + return CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: colorSpace]) } final class ComposerRenderPass: DefaultRenderPass { fileprivate var cachedTexture: MTLTexture? - override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) + override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + let width = input.width + let height = input.height if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height { let textureDescriptor = MTLTextureDescriptor() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift index adf68aefbe..c3c2de83ae 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift @@ -67,4 +67,33 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge } self.renderer?.renderFrame() } + + private var transitionView: UIImageView? + public func setTransitionImage(_ image: UIImage) { + self.transitionView?.removeFromSuperview() + + let transitionView = UIImageView(image: image) + transitionView.frame = self.bounds + self.addSubview(transitionView) + + self.transitionView = transitionView + } + + public func removeTransitionImage() { + if let transitionView = self.transitionView { +// transitionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak transitionView] _ in +// +// }) + transitionView.removeFromSuperview() + self.transitionView = nil + } + } + + public override func layoutSubviews() { + super.layoutSubviews() + + if let transitionView = self.transitionView { + transitionView.frame = self.bounds + } + } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift index 4cb0e5c7fe..cd06a3af9f 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift @@ -6,7 +6,8 @@ import Photos import SwiftSignalKit protocol TextureConsumer: AnyObject { - func consumeTexture(_ texture: MTLTexture, rotation: TextureRotation) + func consumeTexture(_ texture: MTLTexture) + func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation) } final class RenderingContext { @@ -24,7 +25,7 @@ final class RenderingContext { protocol RenderPass: AnyObject { func setup(device: MTLDevice, library: MTLLibrary) - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? } protocol TextureSource { @@ -51,7 +52,9 @@ final class MediaEditorRenderer: TextureConsumer { var semaphore = DispatchSemaphore(value: 3) private var renderPasses: [RenderPass] = [] - private var outputRenderPass = OutputRenderPass() + + private let videoInputPass = VideoInputPass() + private let outputRenderPass = OutputRenderPass() private weak var renderTarget: RenderTarget? { didSet { self.outputRenderPass.renderTarget = self.renderTarget @@ -59,10 +62,14 @@ final class MediaEditorRenderer: TextureConsumer { } private var device: MTLDevice? - private var commandQueue: MTLCommandQueue? - private var currentTexture: MTLTexture? - private var currentRotation: TextureRotation = .rotate0Degrees private var library: MTLLibrary? + private var commandQueue: MTLCommandQueue? + private var textureCache: CVMetalTextureCache? + + private var currentTexture: MTLTexture? + private var currentPixelBuffer: (CVPixelBuffer, TextureRotation)? + + public var onNextRender: (() -> Void)? var finalTexture: MTLTexture? @@ -94,6 +101,8 @@ final class MediaEditorRenderer: TextureConsumer { return } + CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) + let mainBundle = Bundle(for: MediaEditorRenderer.self) guard let path = mainBundle.path(forResource: "MediaEditorBundle", ofType: "bundle") else { return @@ -102,15 +111,16 @@ final class MediaEditorRenderer: TextureConsumer { return } - guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else { + guard let library = try? device.makeDefaultLibrary(bundle: bundle) else { return } - self.library = defaultLibrary + self.library = library self.commandQueue = device.makeCommandQueue() self.commandQueue?.label = "Media Editor Command Queue" - self.renderPasses.forEach { $0.setup(device: device, library: defaultLibrary) } - self.outputRenderPass.setup(device: device, library: defaultLibrary) + self.videoInputPass.setup(device: device, library: library) + self.renderPasses.forEach { $0.setup(device: device, library: library) } + self.outputRenderPass.setup(device: device, library: library) } func setupForComposer(composer: MediaEditorComposer) { @@ -118,6 +128,7 @@ final class MediaEditorRenderer: TextureConsumer { return } self.device = device + CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) let mainBundle = Bundle(for: MediaEditorRenderer.self) guard let path = mainBundle.path(forResource: "MediaEditorBundle", ofType: "bundle") else { @@ -127,17 +138,17 @@ final class MediaEditorRenderer: TextureConsumer { return } - guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else { + guard let library = try? device.makeDefaultLibrary(bundle: bundle) else { return } - self.library = defaultLibrary + self.library = library self.commandQueue = device.makeCommandQueue() self.commandQueue?.label = "Media Editor Command Queue" - self.renderPasses.forEach { $0.setup(device: device, library: defaultLibrary) } + self.videoInputPass.setup(device: device, library: library) + self.renderPasses.forEach { $0.setup(device: device, library: library) } } - private var currentCommandBuffer: MTLCommandBuffer? func renderFrame() { let device: MTLDevice? if let renderTarget = self.renderTarget { @@ -149,7 +160,7 @@ final class MediaEditorRenderer: TextureConsumer { } guard let device = device, let commandQueue = self.commandQueue, - var texture = self.currentTexture else { + let textureCache = self.textureCache else { return } @@ -157,22 +168,36 @@ final class MediaEditorRenderer: TextureConsumer { return } - var rotation: TextureRotation = self.currentRotation + var texture: MTLTexture + if let currentTexture = self.currentTexture { + texture = currentTexture + } else if let (currentPixelBuffer, textureRotation) = self.currentPixelBuffer, let videoTexture = self.videoInputPass.processPixelBuffer(currentPixelBuffer, rotation: textureRotation, textureCache: textureCache, device: device, commandBuffer: commandBuffer) { + texture = videoTexture + } else { + return + } + for renderPass in self.renderPasses { - if let nextTexture = renderPass.process(input: texture, rotation: rotation, device: device, commandBuffer: commandBuffer) { - if nextTexture !== texture { - rotation = .rotate0Degrees - } + if let nextTexture = renderPass.process(input: texture, device: device, commandBuffer: commandBuffer) { texture = nextTexture } } if self.renderTarget != nil { - let _ = self.outputRenderPass.process(input: texture, rotation: rotation, device: device, commandBuffer: commandBuffer) + let _ = self.outputRenderPass.process(input: texture, device: device, commandBuffer: commandBuffer) } self.finalTexture = texture commandBuffer.addCompletedHandler { [weak self] _ in - self?.semaphore.signal() + if let self { + self.semaphore.signal() + + if let onNextRender = self.onNextRender { + self.onNextRender = nil + Queue.mainQueue().async { + onNextRender() + } + } + } } if let _ = self.renderTarget { @@ -184,18 +209,17 @@ final class MediaEditorRenderer: TextureConsumer { } } - func commit() { - if let commandBuffer = self.currentCommandBuffer { - commandBuffer.commit() - self.currentCommandBuffer = nil - } - } - - func consumeTexture(_ texture: MTLTexture, rotation: TextureRotation) { + func consumeTexture(_ texture: MTLTexture) { self.semaphore.wait() self.currentTexture = texture - self.currentRotation = rotation + self.renderTarget?.scheduleFrame() + } + + func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation) { + self.semaphore.wait() + + self.currentPixelBuffer = (pixelBuffer, rotation) self.renderTarget?.scheduleFrame() } @@ -209,27 +233,10 @@ final class MediaEditorRenderer: TextureConsumer { } func finalRenderedImage() -> UIImage? { - if let finalTexture = self.finalTexture { - return getTextureImage(finalTexture) + if let finalTexture = self.finalTexture, let device = self.renderTarget?.mtlDevice { + return getTextureImage(device: device, texture: finalTexture) } else { return nil } } - - private func getTextureImage(_ texture: MTLTexture) -> UIImage? { - guard let device = self.renderTarget?.mtlDevice else { - return nil - } - let options = [CIImageOption.colorSpace: CGColorSpaceCreateDeviceRGB()] - let context = CIContext(mtlDevice: device) - guard var ciImage = CIImage(mtlTexture: texture, options: options) else { - return nil - } - let transform = CGAffineTransform(1.0, 0.0, 0.0, -1.0, 0.0, ciImage.extent.height) - ciImage = ciImage.transformed(by: transform) - guard let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: CGSize(width: ciImage.extent.width, height: ciImage.extent.height))) else { - return nil - } - return UIImage(cgImage: cgImage) - } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift new file mode 100644 index 0000000000..a93192fc58 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift @@ -0,0 +1,41 @@ +import Foundation +import AVFoundation +import SwiftSignalKit + +extension AVPlayer { + func fadeVolume(from: Float, to: Float, duration: Float, completion: (() -> Void)? = nil) -> SwiftSignalKit.Timer? { + self.volume = from + guard from != to else { return nil } + + let interval: Float = 0.1 + let range = to - from + let step = (range * interval) / duration + + func reachedTarget() -> Bool { + guard self.volume >= 0, self.volume <= 1 else { + self.volume = to + return true + } + + if to > from { + return self.volume >= to + } + return self.volume <= to + } + + var invalidateImpl: (() -> Void)? + let timer = SwiftSignalKit.Timer(timeout: Double(interval), repeat: true, completion: { [weak self] in + if let self, !reachedTarget() { + self.volume += step + } else { + invalidateImpl?() + completion?() + } + }, queue: Queue.mainQueue()) + invalidateImpl = { [weak timer] in + timer?.invalidate() + } + timer.start() + return timer + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift index 7301dabbd2..111c9cae2a 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift @@ -102,7 +102,7 @@ class DefaultRenderPass: RenderPass { } } - func setupVerticesBuffer(device: MTLDevice, rotation: TextureRotation) { + func setupVerticesBuffer(device: MTLDevice, rotation: TextureRotation = .rotate0Degrees) { if self.verticesBuffer == nil || rotation != self.textureRotation { self.textureRotation = rotation let vertices = verticesDataForRotation(rotation) @@ -113,8 +113,8 @@ class DefaultRenderPass: RenderPass { } } - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) return nil } @@ -131,11 +131,11 @@ class DefaultRenderPass: RenderPass { final class OutputRenderPass: DefaultRenderPass { weak var renderTarget: RenderTarget? - override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { guard let renderTarget = self.renderTarget, let renderPassDescriptor = renderTarget.renderPassDescriptor else { return nil } - self.setupVerticesBuffer(device: device, rotation: rotation) + self.setupVerticesBuffer(device: device) let drawableSize = renderTarget.drawableSize diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/SharpenRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/SharpenRenderPass.swift index 07e0e8e651..9730c80d8a 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/SharpenRenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/SharpenRenderPass.swift @@ -11,7 +11,7 @@ final class SharpenRenderPass: RenderPass { } - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { return input } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift index e83c5520fb..33c009a189 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift @@ -13,22 +13,19 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD private var displayLink: CADisplayLink? + private let device: MTLDevice? private var textureRotation: TextureRotation = .rotate0Degrees - + private var forceUpdate: Bool = false weak var output: TextureConsumer? - var textureCache: CVMetalTextureCache! var queue: DispatchQueue! var started: Bool = false init(player: AVPlayer, renderTarget: RenderTarget) { self.player = player - - if let device = renderTarget.mtlDevice, CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) != kCVReturnSuccess { - print("error") - } - + self.device = renderTarget.mtlDevice! + self.queue = DispatchQueue( label: "VideoTextureSource Queue", qos: .userInteractive, @@ -47,7 +44,8 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD } deinit { - print() + self.playerItemObservation?.invalidate() + self.playerItemStatusObservation?.invalidate() } private func updatePlayerItem(_ playerItem: AVPlayerItem?) { @@ -63,7 +61,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD self.playerItemStatusObservation = nil self.playerItem = playerItem - self.playerItemStatusObservation = self.playerItem?.observe(\.status, options: [.initial,.new], changeHandler: { [weak self] item, change in + self.playerItemStatusObservation = self.playerItem?.observe(\.status, options: [.initial, .new], changeHandler: { [weak self] item, change in guard let strongSelf = self else { return } @@ -91,21 +89,10 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD } else if t.b == -1.0 && t.c == 1.0 { self.textureRotation = .rotate270Degrees } else if t.a == -1.0 && t.d == 1.0 { -// if (mirrored != NULL) { -// *mirrored = true; -// } self.textureRotation = .rotate270Degrees } else if t.a == 1.0 && t.d == -1.0 { -// if (mirrored != NULL) { -// *mirrored = true; -// } self.textureRotation = .rotate180Degrees } else { -// if (t.c == 1) { -// if (mirrored != NULL) { -// *mirrored = true; -// } -// } self.textureRotation = .rotate90Degrees } } @@ -115,7 +102,20 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD return } - let output = AVPlayerItemVideoOutput(pixelBufferAttributes: [kCVPixelBufferPixelFormatTypeKey as NSString as String: kCVPixelFormatType_32BGRA]) + let colorProperties: [String: Any] = [ + AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, + AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, + AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2 + ] + + let outputSettings: [String: Any] = [ + kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, + kCVPixelBufferMetalCompatibilityKey as String: true, + AVVideoColorPropertiesKey: colorProperties + ] + + let output = AVPlayerItemVideoOutput(outputSettings: outputSettings) + output.suppressesPlayerRendering = true output.setDelegate(self, queue: self.queue) playerItem.add(output) self.playerItemOutput = output @@ -174,12 +174,10 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD var presentationTime: CMTime = .zero if let pixelBuffer = output.copyPixelBuffer(forItemTime: requestTime, itemTimeForDisplay: &presentationTime) { - if let texture = self.pixelBufferToMTLTexture(pixelBuffer: pixelBuffer) { - self.output?.consumeTexture(texture, rotation: self.textureRotation) - } + self.output?.consumeVideoPixelBuffer(pixelBuffer, rotation: self.textureRotation) } } - + func setNeedsUpdate() { self.displayLink?.isPaused = false self.forceUpdate = true @@ -196,19 +194,88 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD self.output = consumer } - private func pixelBufferToMTLTexture(pixelBuffer: CVPixelBuffer) -> MTLTexture? { - let width = CVPixelBufferGetWidth(pixelBuffer) - let height = CVPixelBufferGetHeight(pixelBuffer) - let format: MTLPixelFormat = .bgra8Unorm - var textureRef : CVMetalTexture? - let status = CVMetalTextureCacheCreateTextureFromImage(nil, self.textureCache, pixelBuffer, nil, format, width, height, 0, &textureRef) - if status == kCVReturnSuccess { - return CVMetalTextureGetTexture(textureRef!) - } - return nil - } - public func outputMediaDataWillChange(_ sender: AVPlayerItemOutput) { self.displayLink?.isPaused = false } } + +final class VideoInputPass: DefaultRenderPass { + private var cachedTexture: MTLTexture? + + override var fragmentShaderFunctionName: String { + return "bt709ToRGBFragmentShader" + } + + func processPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation, textureCache: CVMetalTextureCache, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func textureFromPixelBuffer(_ pixelBuffer: CVPixelBuffer, pixelFormat: MTLPixelFormat, width: Int, height: Int, plane: Int) -> MTLTexture? { + var textureRef : CVMetalTexture? + let status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, pixelBuffer, nil, pixelFormat, width, height, plane, &textureRef) + if status == kCVReturnSuccess, let textureRef { + return CVMetalTextureGetTexture(textureRef) + } + return nil + } + + let width = CVPixelBufferGetWidth(pixelBuffer) + let height = CVPixelBufferGetHeight(pixelBuffer) + guard let inputYTexture = textureFromPixelBuffer(pixelBuffer, pixelFormat: .r8Unorm, width: width, height: height, plane: 0), + let inputCbCrTexture = textureFromPixelBuffer(pixelBuffer, pixelFormat: .rg8Unorm, width: width >> 1, height: height >> 1, plane: 1) else { + return nil + } + return self.process(yTexture: inputYTexture, cbcrTexture: inputCbCrTexture, width: width, height: height, rotation: rotation, device: device, commandBuffer: commandBuffer) + } + + func process(yTexture: MTLTexture, cbcrTexture: MTLTexture, width: Int, height: Int, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device, rotation: rotation) + + func textureDimensionsForRotation(width: Int, height: Int, rotation: TextureRotation) -> (width: Int, height: Int) { + switch rotation { + case .rotate90Degrees, .rotate270Degrees: + return (height, width) + default: + return (width, height) + } + } + + let (outputWidth, outputHeight) = textureDimensionsForRotation(width: width, height: height, rotation: rotation) +// let outputSize = CGSize(width: outputWidth, height: outputHeight).fitted(CGSize(width: 1920.0, height: 1920.0)) +// outputWidth = Int(outputSize.width) +// outputHeight = Int(outputSize.height) + if self.cachedTexture == nil { + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.width = outputWidth + textureDescriptor.height = outputHeight + textureDescriptor.pixelFormat = .bgra8Unorm + textureDescriptor.storageMode = .private + textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] + if let texture = device.makeTexture(descriptor: textureDescriptor) { + self.cachedTexture = texture + } + } + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture! + renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + renderPassDescriptor.colorAttachments[0].storeAction = .store + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + return nil + } + + renderCommandEncoder.setViewport(MTLViewport( + originX: 0, originY: 0, + width: Double(outputWidth), height: Double(outputHeight), + znear: -1.0, zfar: 1.0) + ) + + renderCommandEncoder.setFragmentTexture(yTexture, index: 0) + renderCommandEncoder.setFragmentTexture(cbcrTexture, index: 1) + + self.encodeDefaultCommands(using: renderCommandEncoder) + + renderCommandEncoder.endEncoding() + + return self.cachedTexture + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 5b487994d5..f36fbafd5b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -21,6 +21,7 @@ import BlurredBackgroundComponent import AvatarNode import ShareWithPeersScreen import PresentationDataUtils +import ContextUI enum DrawingScreenType { case drawing @@ -37,23 +38,20 @@ final class MediaEditorScreenComponent: Component { let context: AccountContext let mediaEditor: MediaEditor? - let privacy: EngineStoryPrivacy - let timeout: Bool + let privacy: MediaEditorResultPrivacy let openDrawing: (DrawingScreenType) -> Void let openTools: () -> Void init( context: AccountContext, mediaEditor: MediaEditor?, - privacy: EngineStoryPrivacy, - timeout: Bool, + privacy: MediaEditorResultPrivacy, openDrawing: @escaping (DrawingScreenType) -> Void, openTools: @escaping () -> Void ) { self.context = context self.mediaEditor = mediaEditor self.privacy = privacy - self.timeout = timeout self.openDrawing = openDrawing self.openTools = openTools } @@ -65,9 +63,6 @@ final class MediaEditorScreenComponent: Component { if lhs.privacy != rhs.privacy { return false } - if lhs.timeout != rhs.timeout { - return false - } return true } @@ -186,7 +181,11 @@ final class MediaEditorScreenComponent: Component { fatalError("init(coder:) has not been implemented") } - func animateInFromCamera() { + enum TransitionAnimationSource { + case camera + case gallery + } + func animateIn(from source: TransitionAnimationSource) { if let view = self.cancelButton.view { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) @@ -217,7 +216,7 @@ final class MediaEditorScreenComponent: Component { if let view = self.inputPanel.view { view.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) } if let view = self.saveButton.view { @@ -236,7 +235,7 @@ final class MediaEditorScreenComponent: Component { } } - func animateOutToCamera() { + func animateOut(to source: TransitionAnimationSource) { let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let view = self.cancelButton.view { transition.setAlpha(view: view, alpha: 0.0) @@ -334,7 +333,7 @@ final class MediaEditorScreenComponent: Component { if let view = self.privacyButton.view { transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } if let view = self.scrubber.view { @@ -386,7 +385,7 @@ final class MediaEditorScreenComponent: Component { if let view = self.privacyButton.view { transition.setAlpha(view: view, alpha: 1.0) - transition.setScale(view: view, scale: 1.0) + view.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) } if let view = self.scrubber.view { @@ -637,6 +636,17 @@ final class MediaEditorScreenComponent: Component { } + let timeoutValue: Int32 + let timeoutSelected: Bool + switch component.privacy { + case let .story(_, archive): + timeoutValue = 24 + timeoutSelected = archive + case let .message(_, timeout): + timeoutValue = timeout ?? 1 + timeoutSelected = timeout != nil + } + self.inputPanel.parentState = state let inputPanelSize = self.inputPanel.update( transition: transition, @@ -665,16 +675,25 @@ final class MediaEditorScreenComponent: Component { discardMediaRecordingPreview: nil, attachmentAction: nil, reactionAction: nil, - timeoutAction: { view in - + timeoutAction: { [weak self] view in + guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else { + return + } + switch controller.state.privacy { + case let .story(privacy, archive): + controller.state.privacy = .story(privacy: privacy, archive: !archive) + controller.node.presentStoryArchiveTooltip(sourceView: view) + case .message: + controller.presentTimeoutSetup(sourceView: view) + } }, audioRecorder: nil, videoRecordingStatus: nil, isRecordingLocked: false, recordedAudioPreview: nil, wasRecordingDismissed: false, - timeoutValue: 24, - timeoutSelected: component.timeout, + timeoutValue: timeoutValue, + timeoutSelected: timeoutSelected, displayGradient: false,//component.inputHeight != 0.0, bottomInset: 0.0 //component.inputHeight != 0.0 ? 0.0 : bottomContentInset )), @@ -697,18 +716,26 @@ final class MediaEditorScreenComponent: Component { } let privacyText: String - switch component.privacy.base { - case .everyone: - privacyText = "Everyone" - case .closeFriends: - privacyText = "Close Friends" - case .contacts: - privacyText = "Contacts" - case .nobody: - privacyText = "Selected Contacts" + switch component.privacy { + case let .story(privacy, _): + switch privacy.base { + case .everyone: + privacyText = "Everyone" + case .closeFriends: + privacyText = "Close Friends" + case .contacts: + privacyText = "Contacts" + case .nobody: + privacyText = "Selected Contacts" + } + case let .message(peerIds, _): + if peerIds.count == 1 { + privacyText = "User Test" + } else { + privacyText = "\(peerIds.count) Recipients" + } } - let privacyButtonSize = self.privacyButton.update( transition: transition, component: AnyComponent(Button( @@ -845,21 +872,31 @@ final class MediaEditorScreenComponent: Component { private let storyDimensions = CGSize(width: 1080.0, height: 1920.0) +public enum MediaEditorResultPrivacy: Equatable { + case story(privacy: EngineStoryPrivacy, archive: Bool) + case message(peers: [EnginePeer.Id], timeout: Int32?) +} + public final class MediaEditorScreen: ViewController { - public final class TransitionIn { - public weak var sourceView: UIView? - public let sourceRect: CGRect - public let sourceCornerRadius: CGFloat - - public init( - sourceView: UIView, - sourceRect: CGRect, - sourceCornerRadius: CGFloat - ) { - self.sourceView = sourceView - self.sourceRect = sourceRect - self.sourceCornerRadius = sourceCornerRadius + public enum TransitionIn { + public final class GalleryTransitionIn { + public weak var sourceView: UIView? + public let sourceRect: CGRect + public let sourceImage: UIImage? + + public init( + sourceView: UIView, + sourceRect: CGRect, + sourceImage: UIImage? + ) { + self.sourceView = sourceView + self.sourceRect = sourceRect + self.sourceImage = sourceImage + } } + + case camera + case gallery(GalleryTransitionIn) } public final class TransitionOut { @@ -878,6 +915,16 @@ public final class MediaEditorScreen: ViewController { } } + struct State { + var privacy: MediaEditorResultPrivacy = .story(privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), archive: true) + } + + var state = State() { + didSet { + self.node.requestUpdate() + } + } + fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private weak var controller: MediaEditorScreen? private let context: AccountContext @@ -885,8 +932,6 @@ public final class MediaEditorScreen: ViewController { fileprivate var subject: MediaEditorScreen.Subject? private var subjectDisposable: Disposable? - fileprivate var storyPrivacy: EngineStoryPrivacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []) - fileprivate var timeout: Bool = true private let backgroundDimView: UIView fileprivate let componentHost: ComponentView @@ -898,6 +943,7 @@ public final class MediaEditorScreen: ViewController { fileprivate let entitiesContainerView: UIView fileprivate let entitiesView: DrawingEntitiesView + fileprivate let selectionContainerView: DrawingSelectionContainerView fileprivate let drawingView: DrawingView fileprivate let previewView: MediaEditorPreviewView fileprivate var mediaEditor: MediaEditor? @@ -910,7 +956,7 @@ public final class MediaEditorScreen: ViewController { init(controller: MediaEditorScreen) { self.controller = controller self.context = controller.context - + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.backgroundDimView = UIView() @@ -938,16 +984,20 @@ public final class MediaEditorScreen: ViewController { self.drawingView = DrawingView(size: storyDimensions) self.drawingView.isUserInteractionEnabled = false + self.selectionContainerView = DrawingSelectionContainerView(frame: .zero) + self.entitiesView.selectionContainerView = self.selectionContainerView + super.init() self.backgroundColor = .clear - //self.view.addSubview(self.backgroundDimView) + self.view.addSubview(self.backgroundDimView) self.view.addSubview(self.previewContainerView) self.previewContainerView.addSubview(self.gradientView) self.previewContainerView.addSubview(self.entitiesContainerView) self.entitiesContainerView.addSubview(self.entitiesView) self.previewContainerView.addSubview(self.drawingView) + self.previewContainerView.addSubview(self.selectionContainerView) self.subjectDisposable = ( controller.subject @@ -1035,7 +1085,7 @@ public final class MediaEditorScreen: ViewController { let mediaEntity = DrawingMediaEntity(content: subject.mediaContent, size: fittedSize) mediaEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) if fittedSize.height > fittedSize.width { - mediaEntity.scale = storyDimensions.height / fittedSize.height + mediaEntity.scale = storyDimensions.height / fittedSize.height } else { mediaEntity.scale = storyDimensions.width / fittedSize.width } @@ -1082,10 +1132,10 @@ public final class MediaEditorScreen: ViewController { self.previewContainerView.layer.allowsGroupOpacity = true self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in self.previewContainerView.layer.allowsGroupOpacity = false - self.controller?.onReady() + self.backgroundDimView.alpha = 1.0 }) } else { - self.controller?.onReady() + self.backgroundDimView.alpha = 1.0 } } } @@ -1131,18 +1181,47 @@ public final class MediaEditorScreen: ViewController { } func animateIn() { - if let sourceHint = self.controller?.sourceHint { - switch sourceHint { + if let transitionIn = self.controller?.transitionIn { + switch transitionIn { case .camera: if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateInFromCamera() + view.animateIn(from: .camera) + } + case let .gallery(transitionIn): + if let transitionImage = transitionIn.sourceImage { + self.previewContainerView.alpha = 1.0 + self.previewView.setTransitionImage(transitionImage) + } + if let sourceView = transitionIn.sourceView { + if let view = self.componentHost.view as? MediaEditorScreenComponent.View { + view.animateIn(from: .gallery) + } + + let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view) + let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width + let sourceAspectRatio = sourceLocalFrame.height / sourceLocalFrame.width + + let duration: Double = 0.5 + + self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * sourceAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * sourceAspectRatio)), to: self.previewContainerView.bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + + self.backgroundDimView.alpha = 1.0 + self.backgroundDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + + if let componentView = self.componentHost.view { + componentView.layer.animatePosition(from: sourceLocalFrame.center, to: componentView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + componentView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + } } } } -// Queue.mainQueue().after(0.5) { -// self.presentPrivacyTooltip() -// } + // Queue.mainQueue().after(0.5) { + // self.presentPrivacyTooltip() + // } } func animateOut(finished: Bool, completion: @escaping () -> Void) { @@ -1151,18 +1230,68 @@ public final class MediaEditorScreen: ViewController { } controller.statusBar.statusBarStyle = .Ignore - if let transitionOut = controller.transitionOut(finished), let destinationView = transitionOut.destinationView { - let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view) - - let targetScale = destinationLocalFrame.width / self.previewContainerView.frame.width - self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + self.backgroundDimView.alpha = 0.0 + self.backgroundDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + + if finished, case .message = controller.state.privacy { + if let view = self.componentHost.view as? MediaEditorScreenComponent.View { + view.animateOut(to: .camera) + } + let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in completion() }) - self.previewContainerView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } else if let transitionOut = controller.transitionOut(finished), let destinationView = transitionOut.destinationView { + if !finished, let view = self.componentHost.view as? MediaEditorScreenComponent.View { + if let transitionIn = controller.transitionIn, case let .gallery(galleryTransitionIn) = transitionIn, let sourceImage = galleryTransitionIn.sourceImage { + let transitionOutView = UIImageView(image: sourceImage) + var initialScale: CGFloat + if sourceImage.size.height > sourceImage.size.width { + initialScale = self.previewContainerView.bounds.height / sourceImage.size.height + } else { + initialScale = self.previewContainerView.bounds.width / sourceImage.size.width + } + transitionOutView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0) + transitionOutView.transform = CGAffineTransformMakeScale(initialScale, initialScale) + transitionOutView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.previewContainerView.addSubview(transitionOutView) + } + view.animateOut(to: .gallery) + } + let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view) + let destinatinoScale = destinationLocalFrame.width / self.previewContainerView.frame.width + let destinationAspectRatio = destinationLocalFrame.height / destinationLocalFrame.width + + var destinationSnapshotView: UIView? + if let destinationNode = destinationView.asyncdisplaykit_node, destinationNode is AvatarNode, let snapshotView = destinationView.snapshotView(afterScreenUpdates: false) { + destinationView.isHidden = true + + let snapshotScale = self.previewContainerView.bounds.width / snapshotView.frame.width + snapshotView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0) + snapshotView.transform = CGAffineTransform(scaleX: snapshotScale, y: snapshotScale) + + self.previewContainerView.addSubview(snapshotView) + destinationSnapshotView = snapshotView + } + + self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + destinationView.isHidden = false + destinationSnapshotView?.removeFromSuperview() + completion() + }) + self.previewContainerView.layer.animateScale(from: 1.0, to: destinatinoScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * destinationAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * destinationAspectRatio)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + + let targetCornerRadius: CGFloat + if transitionOut.destinationCornerRadius > 0.0 { + targetCornerRadius = self.previewContainerView.bounds.width + } else { + targetCornerRadius = 0.0 + } + self.previewContainerView.layer.animate( from: self.previewContainerView.layer.cornerRadius as NSNumber, - to: self.previewContainerView.bounds.width / 2.0 as NSNumber, + to: targetCornerRadius / 2.0 as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4, @@ -1172,7 +1301,7 @@ public final class MediaEditorScreen: ViewController { if let componentView = self.componentHost.view { componentView.clipsToBounds = true componentView.layer.animatePosition(from: componentView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - componentView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + componentView.layer.animateScale(from: 1.0, to: destinatinoScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) componentView.layer.animateBounds(from: componentView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (componentView.bounds.height - componentView.bounds.width) / 2.0), size: CGSize(width: componentView.bounds.width, height: componentView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) componentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) componentView.layer.animate( @@ -1184,17 +1313,14 @@ public final class MediaEditorScreen: ViewController { removeOnCompletion: false ) } - } else if let sourceHint = controller.sourceHint { - switch sourceHint { - case .camera: - if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateOutToCamera() - } - let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) - transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in - completion() - }) + } else if let transitionIn = controller.transitionIn, case .camera = transitionIn { + if let view = self.componentHost.view as? MediaEditorScreenComponent.View { + view.animateOut(to: .camera) } + let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in + completion() + }) } else { completion() } @@ -1211,7 +1337,7 @@ public final class MediaEditorScreen: ViewController { view.animateInFromTool() } } - + func presentPrivacyTooltip() { guard let sourceView = self.componentHost.findTaggedView(tag: privacyButtonTag) else { return @@ -1221,10 +1347,10 @@ public final class MediaEditorScreen: ViewController { let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize()) - let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in return .ignore }) - self.controller?.present(controller, in: .current) + self.controller?.present(tooltipController, in: .current) } func presentSaveTooltip() { @@ -1243,11 +1369,40 @@ public final class MediaEditorScreen: ViewController { } else { text = "Image saved to Photos" } - - let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .top), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _ in + + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .top), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _ in return .ignore }) - self.controller?.present(controller, in: .current) + self.controller?.present(tooltipController, in: .current) + } + + private weak var storyArchiveTooltip: ViewController? + func presentStoryArchiveTooltip(sourceView: UIView) { + guard let controller = self.controller, case let .story(_, archive) = controller.state.privacy else { + return + } + + if let storyArchiveTooltip = self.storyArchiveTooltip { + storyArchiveTooltip.dismiss(animated: true) + self.storyArchiveTooltip = nil + } + + let parentFrame = self.view.convert(self.bounds, to: nil) + let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 3.0), size: CGSize()) + + let text: String + if archive { + text = "Story will be kept on your page." + } else { + text = "Story will disappear in 24 hours." + } + + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .bottom), displayDuration: .default, inset: 7.0, shouldDismissOnTouch: { _ in + return .ignore + }) + self.storyArchiveTooltip = tooltipController + self.controller?.present(tooltipController, in: .current) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -1266,9 +1421,24 @@ public final class MediaEditorScreen: ViewController { } } + private func insertDrawingEntity(_ entity: DrawingEntity) { + self.entitiesView.prepareNewEntity(entity) + self.entitiesView.add(entity) + self.entitiesView.selectEntity(entity) + + if let entityView = entitiesView.getView(for: entity.uuid) { + entityView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + entityView.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) + + if let selectionView = entityView.selectionView { + selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2) + } + } + } + private var drawingScreen: DrawingScreen? func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { - guard let _ = self.controller else { + guard let controller = self.controller else { return } let isFirstTime = self.validLayout == nil @@ -1305,19 +1475,31 @@ public final class MediaEditorScreen: ViewController { MediaEditorScreenComponent( context: self.context, mediaEditor: self.mediaEditor, - privacy: self.storyPrivacy, - timeout: self.timeout, + privacy: controller.state.privacy, openDrawing: { [weak self] mode in if let self { - let controller = DrawingScreen(context: self.context, sourceHint: .storyEditor, size: self.previewContainerView.frame.size, originalSize: storyDimensions, isVideo: false, isAvatar: false, drawingView: self.drawingView, entitiesView: self.entitiesView, existingStickerPickerInputData: self.stickerPickerInputData) + switch mode { + case .sticker: + let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get()) + controller.completion = { [weak self] file in + if let self, let file { + let stickerEntity = DrawingStickerEntity(content: .file(file)) + self.insertDrawingEntity(stickerEntity) + } + } + self.controller?.present(controller, in: .current) + return + case .text: + break + default: + break + } + + let controller = DrawingScreen(context: self.context, sourceHint: .storyEditor, size: self.previewContainerView.frame.size, originalSize: storyDimensions, isVideo: false, isAvatar: false, drawingView: self.drawingView, entitiesView: self.entitiesView, selectionContainerView: self.selectionContainerView, existingStickerPickerInputData: self.stickerPickerInputData) self.drawingScreen = controller self.drawingView.isUserInteractionEnabled = true - - let selectionContainerView = controller.selectionContainerView - selectionContainerView.frame = self.previewContainerView.bounds - self.previewContainerView.addSubview(selectionContainerView) - - controller.requestDismiss = { [weak controller, weak self, weak selectionContainerView] in + + controller.requestDismiss = { [weak controller, weak self] in self?.drawingScreen = nil controller?.animateOut({ controller?.dismiss() @@ -1325,9 +1507,9 @@ public final class MediaEditorScreen: ViewController { self?.drawingView.isUserInteractionEnabled = false self?.animateInFromTool() - selectionContainerView?.removeFromSuperview() + self?.entitiesView.selectEntity(nil) } - controller.requestApply = { [weak controller, weak self, weak selectionContainerView] in + controller.requestApply = { [weak controller, weak self] in self?.drawingScreen = nil controller?.animateOut({ controller?.dismiss() @@ -1341,7 +1523,7 @@ public final class MediaEditorScreen: ViewController { self?.mediaEditor?.setDrawingAndEntities(data: nil, image: nil, entities: []) } - selectionContainerView?.removeFromSuperview() + self?.entitiesView.selectEntity(nil) } self.controller?.present(controller, in: .current) @@ -1407,6 +1589,8 @@ public final class MediaEditorScreen: ViewController { transition.setFrame(view: self.gradientView, frame: CGRect(origin: .zero, size: previewFrame.size)) transition.setFrame(view: self.drawingView, frame: CGRect(origin: .zero, size: previewFrame.size)) + transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) + if isFirstTime { self.animateIn() } @@ -1475,22 +1659,16 @@ public final class MediaEditorScreen: ViewController { fileprivate let subject: Signal fileprivate let transitionIn: TransitionIn? fileprivate let transitionOut: (Bool) -> TransitionOut? - - public enum SourceHint { - case camera - } - public var sourceHint: SourceHint? - + public var cancelled: (Bool) -> Void = { _ in } - public var completion: (MediaEditorScreen.Result, @escaping () -> Void, EngineStoryPrivacy) -> Void = { _, _, _ in } - public var onReady: () -> Void = {} + public var completion: (MediaEditorScreen.Result, @escaping () -> Void, MediaEditorResultPrivacy) -> Void = { _, _, _ in } public init( context: AccountContext, subject: Signal, transitionIn: TransitionIn?, transitionOut: @escaping (Bool) -> TransitionOut?, - completion: @escaping (MediaEditorScreen.Result, @escaping () -> Void, EngineStoryPrivacy) -> Void + completion: @escaping (MediaEditorScreen.Result, @escaping () -> Void, MediaEditorResultPrivacy) -> Void ) { self.context = context self.subject = subject @@ -1518,22 +1696,175 @@ public final class MediaEditorScreen: ViewController { } func presentPrivacySettings() { - let stateContext = ShareWithPeersScreen.StateContext(context: self.context) + if case .message(_, _) = self.state.privacy { + self.presentSendAsMessage() + } else { + let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .stories) + let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + + let initialPrivacy: EngineStoryPrivacy + if case let .story(privacy, _) = self.state.privacy { + initialPrivacy = privacy + } else { + initialPrivacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []) + } + + self.push( + ShareWithPeersScreen( + context: self.context, + initialPrivacy: initialPrivacy, + stateContext: stateContext, + completion: { [weak self] privacy in + guard let self else { + return + } + self.state.privacy = .story(privacy: privacy, archive: true) + }, + editCategory: { [weak self] privacy in + guard let self else { + return + } + self.presentEditCategory(privacy: privacy, completion: { [weak self] privacy in + guard let self else { + return + } + self.state.privacy = .story(privacy: privacy, archive: true) + self.presentPrivacySettings() + }) + }, + secondaryAction: { [weak self] in + guard let self else { + return + } + self.presentSendAsMessage() + } + ) + ) + }) + } + } + + private func presentEditCategory(privacy: EngineStoryPrivacy, completion: @escaping (EngineStoryPrivacy) -> Void) { + let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .contacts(privacy.base)) let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { return } - self.push(ShareWithPeersScreen(context: self.context, initialPrivacy: self.node.storyPrivacy, stateContext: stateContext, completion: { [weak self] privacy in - guard let self else { - return - } - self.node.storyPrivacy = privacy - self.node.requestUpdate() - })) + self.push( + ShareWithPeersScreen( + context: self.context, + initialPrivacy: privacy, + stateContext: stateContext, + completion: { [weak self] result in + guard let self else { + return + } + if case .closeFriends = privacy.base { + let _ = self.context.engine.privacy.updateCloseFriends(peerIds: result.additionallyIncludePeers).start() + } + completion(result) + }, + editCategory: { _ in }, + secondaryAction: { [weak self] in + guard let self else { + return + } + self.presentSendAsMessage() + } + ) + ) }) } + private func presentSendAsMessage() { + var initialPeerIds = Set() + if case let .message(peers, _) = self.state.privacy { + initialPeerIds = Set(peers) + } + let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .chats, initialPeerIds: initialPeerIds) + let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + + self.push( + ShareWithPeersScreen( + context: self.context, + initialPrivacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), + stateContext: stateContext, + completion: { [weak self] privacy in + guard let self else { + return + } + self.state.privacy = .message(peers: privacy.additionallyIncludePeers, timeout: nil) + }, + editCategory: { _ in }, + secondaryAction: {} + ) + ) + }) + } + + func presentTimeoutSetup(sourceView: UIView) { + var items: [ContextMenuItem] = [] + + let updateTimeout: (Int32?) -> Void = { [weak self] timeout in + guard let self else { + return + } + if case let .message(peers, _) = self.state.privacy { + self.state.privacy = .message(peers: peers, timeout: timeout) + } + } + + let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil + items.append(.action(ContextMenuActionItem(text: "Choose how long the media will be kept after opening.", textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) + + items.append(.action(ContextMenuActionItem(text: "Until First View", icon: { _ in + return nil + }, action: { _, a in + a(.default) + + updateTimeout(1) + }))) + items.append(.action(ContextMenuActionItem(text: "3 Seconds", icon: { _ in + return nil + }, action: { _, a in + a(.default) + + updateTimeout(3) + }))) + items.append(.action(ContextMenuActionItem(text: "10 Seconds", icon: { _ in + return nil + }, action: { _, a in + a(.default) + + updateTimeout(10) + }))) + items.append(.action(ContextMenuActionItem(text: "1 Minute", icon: { _ in + return nil + }, action: { _, a in + a(.default) + + updateTimeout(60) + }))) + items.append(.action(ContextMenuActionItem(text: "Keep Always", icon: { _ in + return nil + }, action: { _, a in + a(.default) + + updateTimeout(nil) + }))) + + let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) + let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + self.present(contextController, in: .window(.root)) + } + func maybePresentDiscardAlert() { if let subject = self.node.subject, case .asset = subject { self.requestDismiss(saveDraft: false, animated: true) @@ -1606,6 +1937,10 @@ public final class MediaEditorScreen: ViewController { } } + if let mediaEditor = self.node.mediaEditor { + mediaEditor.stop() + } + self.cancelled(saveDraft) self.node.animateOut(finished: false, completion: { [weak self] in @@ -1617,6 +1952,8 @@ public final class MediaEditorScreen: ViewController { guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else { return } + + mediaEditor.stop() if mediaEditor.resultIsVideo { let videoResult: Result.VideoResult @@ -1664,7 +2001,7 @@ public final class MediaEditorScreen: ViewController { self?.node.animateOut(finished: true, completion: { [weak self] in self?.dismiss() }) - }, self.node.storyPrivacy) + }, self.state.privacy) if case let .draft(draft) = subject { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) @@ -1677,7 +2014,7 @@ public final class MediaEditorScreen: ViewController { self?.node.animateOut(finished: true, completion: { [weak self] in self?.dismiss() }) - }, self.node.storyPrivacy) + }, self.state.privacy) if case let .draft(draft) = subject { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) } @@ -1866,3 +2203,20 @@ final class PrivacyButtonComponent: CombinedComponent { } } } + +private final class HeaderContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceView: UIView + var keepInPlace: Bool { + return true + } + + init(controller: ViewController, sourceView: UIView) { + self.controller = controller + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift index cd5bc4560c..a072eb531b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -520,6 +520,7 @@ private final class MediaToolsScreenComponent: Component { }) } + var needsHistogram = false let screenSize: CGSize let optionsSize: CGSize let optionsTransition: Transition = sectionChanged ? .immediate : transition @@ -711,6 +712,7 @@ private final class MediaToolsScreenComponent: Component { containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height) ) case .curves: + needsHistogram = true let internalState: CurvesInternalState if let current = self.curvesState { internalState = current @@ -755,6 +757,7 @@ private final class MediaToolsScreenComponent: Component { containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height) ) } + component.mediaEditor.isHistogramEnabled = needsHistogram let optionsFrame = CGRect(origin: .zero, size: optionsSize) if let optionsView = self.toolOptions.view { diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 5ff550726c..9fdbe2e22a 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -239,7 +239,7 @@ public final class MessageInputPanelComponent: Component { } func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - var insets = UIEdgeInsets(top: 14.0, left: 7.0, bottom: 6.0, right: 7.0) + var insets = UIEdgeInsets(top: 14.0, left: 7.0, bottom: 6.0, right: 41.0) if let _ = component.attachmentAction { insets.left = 41.0 @@ -321,8 +321,8 @@ public final class MessageInputPanelComponent: Component { environment: {}, containerSize: availableTextFieldSize ) - if self.textFieldExternalState.isEditing { - insets.right = 41.0 + if !self.textFieldExternalState.isEditing && component.setMediaRecordingActive == nil { + insets.right = insets.left } let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: textFieldSize.height)) @@ -707,7 +707,7 @@ public final class MessageInputPanelComponent: Component { if timeoutButtonView.superview == nil { self.addSubview(timeoutButtonView) } - let timeoutIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - timeoutButtonSize.width, y: fieldFrame.minY + 1.0 + floor((fieldFrame.height - timeoutButtonSize.height) * 0.5)), size: timeoutButtonSize) + let timeoutIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - timeoutButtonSize.width, y: fieldFrame.maxY - 3.0 - timeoutButtonSize.height), size: timeoutButtonSize) transition.setPosition(view: timeoutButtonView, position: timeoutIconFrame.center) transition.setBounds(view: timeoutButtonView, bounds: CGRect(origin: CGPoint(), size: timeoutIconFrame.size)) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD b/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD index 5bcc1de71f..af39190090 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD @@ -29,9 +29,11 @@ swift_library( "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/AnimatedCounterComponent", "//submodules/TelegramUI/Components/TokenListTextField", + "//submodules/Components/BundleIconComponent", "//submodules/AvatarNode", "//submodules/CheckNode", "//submodules/PeerPresenceStatusManager", + "//submodules/LocalizedPeerData" ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift index 1788bc76f8..8c38a9b484 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift @@ -10,6 +10,7 @@ import MultilineTextComponent import AvatarNode import TelegramPresentationData import CheckNode +import BundleIconComponent final class CategoryListItemComponent: Component { enum SelectionState: Equatable { @@ -27,6 +28,7 @@ final class CategoryListItemComponent: Component { let selectionState: SelectionState let hasNext: Bool let action: () -> Void + let secondaryAction: () -> Void init( context: AccountContext, @@ -38,7 +40,8 @@ final class CategoryListItemComponent: Component { subtitle: String?, selectionState: SelectionState, hasNext: Bool, - action: @escaping () -> Void + action: @escaping () -> Void, + secondaryAction: @escaping () -> Void ) { self.context = context self.theme = theme @@ -50,6 +53,7 @@ final class CategoryListItemComponent: Component { self.selectionState = selectionState self.hasNext = hasNext self.action = action + self.secondaryAction = secondaryAction } static func ==(lhs: CategoryListItemComponent, rhs: CategoryListItemComponent) -> Bool { @@ -88,6 +92,7 @@ final class CategoryListItemComponent: Component { private let title = ComponentView() private let label = ComponentView() + private let labelArrow = ComponentView() private let separatorLayer: SimpleLayer private let iconView: UIImageView @@ -120,7 +125,11 @@ final class CategoryListItemComponent: Component { guard let component = self.component else { return } - component.action() + if case .editing(true, _) = component.selectionState { + component.secondaryAction() + } else { + component.action() + } } func update(component: CategoryListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { @@ -223,7 +232,12 @@ final class CategoryListItemComponent: Component { transition.setFrame(view: self.iconView, frame: avatarFrame) - let labelData: (String, Bool) = ("", false) + let labelData: (String, Bool, Bool) + if let subtitle = component.subtitle { + labelData = (subtitle, true, true) + } else { + labelData = ("", false, false) + } let labelSize = self.label.update( transition: .immediate, @@ -234,6 +248,13 @@ final class CategoryListItemComponent: Component { containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) ) + let labelArrowSize = self.labelArrow.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent(name: "Contact List/SubtitleArrow", tintColor: component.theme.list.itemAccentColor)), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + let previousTitleFrame = self.title.view?.frame var previousTitleContents: UIView? if hasSelectionUpdated && !"".isEmpty { @@ -284,6 +305,13 @@ final class CategoryListItemComponent: Component { } transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing), size: labelSize)) } + if let labelArrowView = self.labelArrow.view, !labelData.0.isEmpty { + if labelArrowView.superview == nil { + labelArrowView.isUserInteractionEnabled = false + self.containerButton.addSubview(labelArrowView) + } + transition.setFrame(view: labelArrowView, frame: CGRect(origin: CGPoint(x: titleFrame.minX + labelSize.width + 5.0, y: titleFrame.maxY + titleSpacing + floorToScreenPixels(labelSize.height / 2.0 - labelArrowSize.height / 2.0)), size: labelArrowSize)) + } if themeUpdated { self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index bf886c3f3c..ee97a5f4fe 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -17,6 +17,7 @@ import PlainButtonComponent import AnimatedCounterComponent import TokenListTextField import AvatarNode +import LocalizedPeerData final class ShareWithPeersScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -26,19 +27,25 @@ final class ShareWithPeersScreenComponent: Component { let initialPrivacy: EngineStoryPrivacy let categoryItems: [CategoryItem] let completion: (EngineStoryPrivacy) -> Void + let editCategory: (EngineStoryPrivacy) -> Void + let secondaryAction: () -> Void init( context: AccountContext, stateContext: ShareWithPeersScreen.StateContext, initialPrivacy: EngineStoryPrivacy, categoryItems: [CategoryItem], - completion: @escaping (EngineStoryPrivacy) -> Void + completion: @escaping (EngineStoryPrivacy) -> Void, + editCategory: @escaping (EngineStoryPrivacy) -> Void, + secondaryAction: @escaping () -> Void ) { self.context = context self.stateContext = stateContext self.initialPrivacy = initialPrivacy self.categoryItems = categoryItems self.completion = completion + self.editCategory = editCategory + self.secondaryAction = secondaryAction } static func ==(lhs: ShareWithPeersScreenComponent, rhs: ShareWithPeersScreenComponent) -> Bool { @@ -204,6 +211,7 @@ final class ShareWithPeersScreenComponent: Component { private let navigationBackgroundView: BlurredBackgroundView private let navigationTitle = ComponentView() private let navigationLeftButton = ComponentView() + private let navigationRightButton = ComponentView() private let navigationSeparatorLayer: SimpleLayer private let navigationTextFieldState = TokenListTextField.ExternalState() private let navigationTextField = ComponentView() @@ -440,7 +448,11 @@ final class ShareWithPeersScreenComponent: Component { if section.id == 0 { sectionTitle = "WHO CAN VIEW FOR 24 HOURS" } else { - sectionTitle = "CONTACTS" + if case .chats = component.stateContext.subject { + sectionTitle = "CHATS" + } else { + sectionTitle = "CONTACTS" + } } let _ = sectionHeader.update( @@ -521,6 +533,26 @@ final class ShareWithPeersScreenComponent: Component { } } self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + }, + secondaryAction: { [weak self] in + guard let self, let environment = self.environment, let controller = environment.controller() else { + return + } + let base: EngineStoryPrivacy.Base? + switch categoryId { + case .everyone: + base = nil + case .contacts: + base = .contacts + case .closeFriends: + base = .closeFriends + case .selectedContacts: + base = .nobody + } + if let base { + component.editCategory(EngineStoryPrivacy(base: base, additionallyIncludePeers: self.selectedPeers)) + controller.dismiss() + } } )), environment: {}, @@ -700,6 +732,8 @@ final class ShareWithPeersScreenComponent: Component { var applyState = false self.defaultStateValue = component.stateContext.stateValue + self.selectedPeers = Array(component.stateContext.initialPeerIds) + self.stateDisposable = (component.stateContext.state |> deliverOnMainQueue).start(next: { [weak self] stateValue in guard let self else { @@ -738,97 +772,85 @@ final class ShareWithPeersScreenComponent: Component { self.bottomSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor } - var tokens: [TokenListTextField.Token] = [] - for categoryId in self.selectedCategories.sorted(by: { $0.rawValue < $1.rawValue }) { - let categoryTitle: String - var categoryImage: UIImage? - switch categoryId { - case .everyone: - categoryTitle = "Everyone" - categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue) - case .contacts: - categoryTitle = "Contacts" - categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 0.6 * 0.9, cornerRadius: 6.0, circleCorners: true, color: .yellow) - case .closeFriends: - categoryTitle = "Close Friends" - categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 0.6 * 1.0, cornerRadius: 6.0, circleCorners: true, color: .green) - case .selectedContacts: - categoryTitle = "Selected Contacts" - categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Group"), color: .white), iconScale: 0.6 * 1.0, cornerRadius: 6.0, circleCorners: true, color: .purple) - } - tokens.append(TokenListTextField.Token( - id: AnyHashable(categoryId), - title: categoryTitle, - fixedPosition: categoryId.rawValue, - content: .category(categoryImage) - )) - } - for peerId in self.selectedPeers { - guard let stateValue = self.defaultStateValue, let peer = stateValue.peers.first(where: { $0.id == peerId }) else { - continue - } - tokens.append(TokenListTextField.Token( - id: AnyHashable(peerId), - title: peer.compactDisplayTitle, - fixedPosition: nil, - content: .peer(peer) - )) - } - - self.navigationTextField.parentState = state - let navigationTextFieldSize = self.navigationTextField.update( - transition: transition, - component: AnyComponent(TokenListTextField( - externalState: self.navigationTextFieldState, - context: component.context, - theme: environment.theme, - placeholder: "Search Contacts", - tokens: tokens, - sideInset: sideInset, - deleteToken: { [weak self] tokenId in - guard let self else { - return - } - if let categoryId = tokenId.base as? CategoryId { - self.selectedCategories.remove(categoryId) - } else if let peerId = tokenId.base as? EnginePeer.Id { - self.selectedPeers.removeAll(where: { $0 == peerId }) - } - if self.selectedCategories.isEmpty { - self.selectedCategories.insert(.everyone) - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + let navigationTextFieldSize: CGSize + if case .stories = component.stateContext.subject { + navigationTextFieldSize = .zero + } else { + var tokens: [TokenListTextField.Token] = [] + for peerId in self.selectedPeers { + guard let stateValue = self.defaultStateValue, let peer = stateValue.peers.first(where: { $0.id == peerId }) else { + continue } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 1000.0) - ) - - if !self.navigationTextFieldState.text.isEmpty { - if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(self.navigationTextFieldState.text) { - } else { - self.searchStateDisposable?.dispose() - let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(self.navigationTextFieldState.text)) - var applyState = false - self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in - guard let self else { - return - } - self.searchStateContext = searchStateContext - if applyState { - self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(contentReloaded: true))) - } - }) - applyState = true + tokens.append(TokenListTextField.Token( + id: AnyHashable(peerId), + title: peer.compactDisplayTitle, + fixedPosition: nil, + content: .peer(peer) + )) } - } else if let _ = self.searchStateContext { - self.searchStateContext = nil - self.searchStateDisposable?.dispose() - self.searchStateDisposable = nil - contentTransition = contentTransition.withUserData(AnimationHint(contentReloaded: true)) + let placeholder: String + switch component.stateContext.subject { + case .chats: + placeholder = "Search Chats" + default: + placeholder = "Search Contacts" + } + self.navigationTextField.parentState = state + navigationTextFieldSize = self.navigationTextField.update( + transition: transition, + component: AnyComponent(TokenListTextField( + externalState: self.navigationTextFieldState, + context: component.context, + theme: environment.theme, + placeholder: placeholder, + tokens: tokens, + sideInset: sideInset, + deleteToken: { [weak self] tokenId in + guard let self else { + return + } + if let categoryId = tokenId.base as? CategoryId { + self.selectedCategories.remove(categoryId) + } else if let peerId = tokenId.base as? EnginePeer.Id { + self.selectedPeers.removeAll(where: { $0 == peerId }) + } + if self.selectedCategories.isEmpty { + self.selectedCategories.insert(.everyone) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + + if !self.navigationTextFieldState.text.isEmpty { + if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(self.navigationTextFieldState.text) { + } else { + self.searchStateDisposable?.dispose() + let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(self.navigationTextFieldState.text)) + var applyState = false + self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + self.searchStateContext = searchStateContext + if applyState { + self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(contentReloaded: true))) + } + }) + applyState = true + } + } else if let _ = self.searchStateContext { + self.searchStateContext = nil + self.searchStateDisposable?.dispose() + self.searchStateDisposable = nil + + contentTransition = contentTransition.withUserData(AnimationHint(contentReloaded: true)) + } } - + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) let categoryItemSize = self.categoryTemplateItem.update( @@ -843,7 +865,8 @@ final class ShareWithPeersScreenComponent: Component { subtitle: nil, selectionState: .editing(isSelected: false, isTinted: false), hasNext: true, - action: {} + action: {}, + secondaryAction: {} )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 1000.0) @@ -870,54 +893,121 @@ final class ShareWithPeersScreenComponent: Component { var sections: [ItemLayout.Section] = [] if let stateValue = self.effectiveStateValue { - if self.searchStateContext == nil { + if case .stories = component.stateContext.subject { sections.append(ItemLayout.Section( id: 0, - insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 00, right: 0.0), + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), itemHeight: categoryItemSize.height, itemCount: component.categoryItems.count )) + } else { + sections.append(ItemLayout.Section( + id: 1, + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), + itemHeight: peerItemSize.height, + itemCount: stateValue.peers.count + )) } - sections.append(ItemLayout.Section( - id: 1, - insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 00, right: 0.0), - itemHeight: peerItemSize.height, - itemCount: stateValue.peers.count - )) } let containerInset: CGFloat = environment.statusBarHeight + 10.0 var navigationHeight: CGFloat = 56.0 - let navigationSideInset: CGFloat = 16.0 - let navigationLeftButtonSize = self.navigationLeftButton.update( + var navigationButtonsWidth: CGFloat = 0.0 + + if case .stories = component.stateContext.subject { + } else { + let navigationLeftButtonSize = self.navigationLeftButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), + action: { [weak self] in + guard let self, let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + ).minSize(CGSize(width: navigationHeight, height: navigationHeight))), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: navigationHeight) + ) + let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: floor((navigationHeight - navigationLeftButtonSize.height) * 0.5)), size: navigationLeftButtonSize) + if let navigationLeftButtonView = self.navigationLeftButton.view { + if navigationLeftButtonView.superview == nil { + self.navigationContainerView.addSubview(navigationLeftButtonView) + } + transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame) + } + navigationButtonsWidth += navigationLeftButtonSize.width + navigationSideInset + } + + let navigationRightButtonSize = self.navigationRightButton.update( transition: transition, component: AnyComponent(Button( - content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), + content: AnyComponent(Text(text: "Done", font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), action: { [weak self] in - guard let self, let environment = self.environment, let controller = environment.controller() else { + guard let self, let component = self.component, let controller = self.environment?.controller() else { return } + + let base: EngineStoryPrivacy.Base + if self.selectedCategories.contains(.everyone) { + base = .everyone + } else if self.selectedCategories.contains(.closeFriends) { + base = .closeFriends + } else if self.selectedCategories.contains(.contacts) { + base = .contacts + } else if self.selectedCategories.contains(.selectedContacts) { + base = .nobody + } else { + base = .nobody + } + + component.completion(EngineStoryPrivacy( + base: base, + additionallyIncludePeers: self.selectedPeers + )) controller.dismiss() } ).minSize(CGSize(width: navigationHeight, height: navigationHeight))), environment: {}, containerSize: CGSize(width: availableSize.width, height: navigationHeight) ) - let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: floor((navigationHeight - navigationLeftButtonSize.height) * 0.5)), size: navigationLeftButtonSize) - if let navigationLeftButtonView = self.navigationLeftButton.view { - if navigationLeftButtonView.superview == nil { - self.navigationContainerView.addSubview(navigationLeftButtonView) + let navigationRightButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - navigationSideInset - navigationRightButtonSize.width, y: floor((navigationHeight - navigationRightButtonSize.height) * 0.5)), size: navigationRightButtonSize) + if let navigationRightButtonView = self.navigationRightButton.view { + if navigationRightButtonView.superview == nil { + self.navigationContainerView.addSubview(navigationRightButtonView) } - transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame) + transition.setFrame(view: navigationRightButtonView, frame: navigationRightButtonFrame) } + navigationButtonsWidth += navigationRightButtonSize.width + navigationSideInset + let title: String + switch component.stateContext.subject { + case .stories: + title = "Share Story" + case .chats: + title = "Send as a Message" + case let .contacts(category): + switch category { + case .closeFriends: + title = "Close Friends" + case .contacts: + title = "Excluded People" + case .nobody: + title = "Selected Contacts" + case .everyone: + title = "" + } + case .search: + title = "" + } let navigationTitleSize = self.navigationTitle.update( transition: .immediate, - component: AnyComponent(Text(text: "Share Story", font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)), + component: AnyComponent(Text(text: title, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)), environment: {}, - containerSize: CGSize(width: availableSize.width - navigationSideInset - navigationLeftButtonFrame.maxX, height: navigationHeight) + containerSize: CGSize(width: availableSize.width - navigationButtonsWidth, height: navigationHeight) ) let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) * 0.5), y: floor((navigationHeight - navigationTitleSize.height) * 0.5)), size: navigationTitleSize) if let navigationTitleView = self.navigationTitle.view { @@ -943,7 +1033,11 @@ final class ShareWithPeersScreenComponent: Component { if environment.inputHeight != 0.0 || !self.navigationTextFieldState.text.isEmpty { topInset = 0.0 } else { - topInset = max(0.0, availableSize.height - containerInset - 600.0) + if case .stories = component.stateContext.subject { + topInset = max(0.0, availableSize.height - containerInset - 410.0) + } else { + topInset = max(0.0, availableSize.height - containerInset - 600.0) + } } self.navigationBackgroundView.update(size: CGSize(width: availableSize.width, height: navigationHeight), cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) @@ -951,84 +1045,47 @@ final class ShareWithPeersScreenComponent: Component { transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - var actionButtonTitle: String = "Post Story" - if self.selectedCategories.contains(.everyone) { - actionButtonTitle = "Post Story" - } else if self.selectedCategories.contains(.closeFriends) { - actionButtonTitle = "Send to Close Friends" - } else if self.selectedCategories.contains(.contacts) { - actionButtonTitle = "Send to Contacts" - } else if self.selectedCategories.contains(.selectedContacts) { - actionButtonTitle = "Send to Selected Contacts" - } - - let actionButtonSize = self.actionButton.update( - transition: transition, - component: AnyComponent(ButtonComponent( - background: ButtonComponent.Background( - color: environment.theme.list.itemCheckColors.fillColor, - foreground: environment.theme.list.itemCheckColors.foregroundColor, - pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) - ), - content: AnyComponentWithIdentity( - id: actionButtonTitle, - component: AnyComponent(ButtonTextContentComponent( - text: actionButtonTitle, - badge: 0, - textColor: environment.theme.list.itemCheckColors.foregroundColor, - badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, - badgeForeground: environment.theme.list.itemCheckColors.fillColor - )) - ), - isEnabled: true, - displaysProgress: false, - action: { [weak self] in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - let base: EngineStoryPrivacy.Base - if self.selectedCategories.contains(.everyone) { - base = .everyone - } else if self.selectedCategories.contains(.closeFriends) { - base = .closeFriends - } else if self.selectedCategories.contains(.contacts) { - base = .contacts - } else if self.selectedCategories.contains(.selectedContacts) { - base = .nobody - } else { - base = .nobody - } - - component.completion(EngineStoryPrivacy( - base: base, - additionallyIncludePeers: self.selectedPeers - )) - controller.dismiss() - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - navigationSideInset * 2.0, height: 50.0) - ) - var bottomPanelHeight: CGFloat = 0.0 - if environment.inputHeight != 0.0 { - bottomPanelHeight += environment.inputHeight + 8.0 + actionButtonSize.height - } else { - bottomPanelHeight += 10.0 + environment.safeInsets.bottom + actionButtonSize.height - } - let actionButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) - if let actionButtonView = self.actionButton.view { - if actionButtonView.superview == nil { - self.addSubview(actionButtonView) + if case .stories = component.stateContext.subject { + let actionButtonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text( + text: "Send as a Message", + font: Font.regular(17.0), + color: environment.theme.list.itemAccentColor + )), + action: { [weak self] in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + component.secondaryAction() + controller.dismiss() + } + ).minSize(CGSize(width: 200.0, height: 44.0))), + environment: {}, + containerSize: CGSize(width: availableSize.width - navigationSideInset * 2.0, height: 44.0) + ) + + if environment.inputHeight != 0.0 { + bottomPanelHeight += environment.inputHeight + 8.0 + actionButtonSize.height + } else { + bottomPanelHeight += environment.safeInsets.bottom + actionButtonSize.height } - transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + let actionButtonFrame = CGRect(origin: CGPoint(x: (availableSize.width - actionButtonSize.width) / 2.0, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight + 8.0))) + self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) + transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) } - transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight + 8.0))) - self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) - transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - let itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: bottomPanelHeight + environment.safeInsets.bottom, topInset: topInset, sideInset: sideInset, navigationHeight: navigationHeight, sections: sections) let previousItemLayout = self.itemLayout self.itemLayout = itemLayout @@ -1099,13 +1156,16 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { public final class StateContext { public enum Subject: Equatable { - case contacts + case stories + case chats + case contacts(EngineStoryPrivacy.Base) case search(String) } fileprivate var stateValue: State? public let subject: Subject + public private(set) var initialPeerIds: Set = Set() private var stateDisposable: Disposable? private let stateSubject = Promise() @@ -1119,12 +1179,51 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { public init( context: AccountContext, - subject: Subject = .contacts + subject: Subject = .chats, + initialPeerIds: Set = Set() ) { self.subject = subject + self.initialPeerIds = initialPeerIds switch subject { - case .contacts: + case .stories: + let state = State(peers: [], presences: [:]) + self.stateValue = state + self.stateSubject.set(.single(state)) + self.readySubject.set(true) + case .chats: + self.stateDisposable = (context.engine.messages.chatList(group: .root, count: 200) + |> deliverOnMainQueue).start(next: { [weak self] chatList in + guard let self else { + return + } + + var selectedPeers: [EnginePeer] = [] + for item in chatList.items.reversed() { + if self.initialPeerIds.contains(item.renderedPeer.peerId), let peer = item.renderedPeer.peer { + selectedPeers.append(peer) + } + } + + var presences: [EnginePeer.Id: EnginePeer.Presence] = [:] + for item in chatList.items { + presences[item.renderedPeer.peerId] = item.presence + } + + var peers: [EnginePeer] = [] + peers = chatList.items.filter { !self.initialPeerIds.contains($0.renderedPeer.peerId) }.reversed().compactMap { $0.renderedPeer.peer } + peers.insert(contentsOf: selectedPeers, at: 0) + + let state = State( + peers: peers, + presences: presences + ) + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) + }) + case let .contacts(base): self.stateDisposable = (context.engine.data.subscribe( TelegramEngine.EngineData.Item.Contacts.List(includePresences: true) ) @@ -1133,23 +1232,40 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { return } - let state = State( - peers: contactList.peers.sorted(by: { lhs, rhs in - let lhsPresence = contactList.presences[lhs.id] - let rhsPresence = contactList.presences[rhs.id] - - if let lhsPresence, let rhsPresence { - return lhsPresence.status > rhsPresence.status - } else if lhsPresence != nil { - return true - } else if rhsPresence != nil { - return false - } else { - return lhs.id < rhs.id + var selectedPeers: [EnginePeer] = [] + if case .closeFriends = base { + for peer in contactList.peers { + if case let .user(user) = peer, user.flags.contains(.isCloseFriend) { + selectedPeers.append(peer) } - }), + } + selectedPeers = selectedPeers.sorted(by: { lhs, rhs in + let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast) + if result == .orderedSame { + return lhs.id < rhs.id + } else { + return result == .orderedAscending + } + }) + self.initialPeerIds = Set(selectedPeers.map { $0.id }) + } + + var peers: [EnginePeer] = [] + peers = contactList.peers.filter { !self.initialPeerIds.contains($0.id) }.sorted(by: { lhs, rhs in + let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast) + if result == .orderedSame { + return lhs.id < rhs.id + } else { + return result == .orderedAscending + } + }) + peers.insert(contentsOf: selectedPeers, at: 0) + + let state = State( + peers: peers, presences: contactList.presences ) + self.stateValue = state self.stateSubject.set(.single(state)) @@ -1183,45 +1299,49 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { private var isDismissed: Bool = false - public init(context: AccountContext, initialPrivacy: EngineStoryPrivacy, stateContext: StateContext, completion: @escaping (EngineStoryPrivacy) -> Void) { + public init(context: AccountContext, initialPrivacy: EngineStoryPrivacy, stateContext: StateContext, completion: @escaping (EngineStoryPrivacy) -> Void, editCategory: @escaping (EngineStoryPrivacy) -> Void, secondaryAction: @escaping () -> Void) { self.context = context var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = [] - categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( - id: .everyone, - title: "Everyone", - icon: "Chat List/Filters/Channel", - iconColor: .blue, - actionTitle: nil - )) - categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( - id: .contacts, - title: "Contacts", - icon: "Chat List/Tabs/IconContacts", - iconColor: .yellow, - actionTitle: nil - )) - categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( - id: .closeFriends, - title: "Close Friends", - icon: "Call/StarHighlighted", - iconColor: .green, - actionTitle: nil - )) - categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( - id: .selectedContacts, - title: "Selected Contacts", - icon: "Chat List/Filters/Group", - iconColor: .purple, - actionTitle: nil - )) + if case .stories = stateContext.subject { + categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( + id: .everyone, + title: "Everyone", + icon: "Chat List/Filters/Channel", + iconColor: .blue, + actionTitle: nil + )) + categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( + id: .contacts, + title: "Contacts", + icon: "Chat List/Tabs/IconContacts", + iconColor: .yellow, + actionTitle: "exclude people" + )) + categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( + id: .closeFriends, + title: "Close Friends", + icon: "Call/StarHighlighted", + iconColor: .green, + actionTitle: "edit list" + )) + categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( + id: .selectedContacts, + title: "Selected Contacts", + icon: "Chat List/Filters/Group", + iconColor: .violet, + actionTitle: "edit list" + )) + } super.init(context: context, component: ShareWithPeersScreenComponent( context: context, stateContext: stateContext, initialPrivacy: initialPrivacy, categoryItems: categoryItems, - completion: completion + completion: completion, + editCategory: editCategory, + secondaryAction: secondaryAction ), navigationBarAppearance: .none, theme: .dark) self.statusBar.statusBarStyle = .Ignore diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 230bbdf369..a191096c7c 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -134,7 +134,7 @@ public final class TextFieldComponent: Component { self.layoutManager.ensureLayout(for: self.textContainer) let boundingRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: self.textStorage.length), in: self.textContainer) - let size = CGSize(width: availableSize.width, height: min(100.0, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom)) + let size = CGSize(width: availableSize.width, height: min(200.0, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom)) let refreshScrolling = self.textView.bounds.size != size self.textView.frame = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/Contents.json new file mode 100644 index 0000000000..ac6650008b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "off shadow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off shadow.pdf b/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off shadow.pdf new file mode 100644 index 0000000000..297a8ab85d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off shadow.pdf @@ -0,0 +1,1366 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 187.691406 139.141357 cm +1.000000 1.000000 1.000000 scn +112.318077 230.817093 m +114.451881 236.621597 106.507004 239.951080 102.709404 235.070602 c +70.894165 194.178711 37.289165 151.643723 1.607707 108.848343 c +-1.328853 105.327103 -0.157611 101.418564 4.426768 101.506927 c +20.839167 101.823715 57.499165 100.883713 57.172985 100.331924 c +57.264164 100.648712 34.854565 40.328918 24.311525 11.312057 c +22.424946 6.120438 29.099884 2.524002 31.687706 5.647614 c +67.369164 48.713715 102.736664 92.306213 134.258621 130.929871 c +138.408722 136.015289 136.225098 141.255768 130.969559 141.267990 c +115.661659 141.303711 79.236656 141.303711 79.280838 141.150482 c +79.236656 141.303711 103.559158 206.986221 112.318077 230.817093 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 187.691406 134.211060 cm +1.000000 1.000000 1.000000 scn +112.318077 235.747391 m +108.788986 237.044724 l +108.788910 237.044525 l +112.318077 235.747391 l +h +102.709404 240.000900 m +99.741928 242.309937 l +99.741806 242.309784 l +102.709404 240.000900 l +h +1.607707 113.778641 m +4.495335 111.370483 l +4.495601 111.370804 l +1.607707 113.778641 l +h +4.426768 106.437225 m +4.499225 102.677933 l +4.499327 102.677933 l +4.426768 106.437225 l +h +57.172985 105.262222 m +53.559685 106.302261 l +53.036144 104.483368 53.946251 102.558853 55.684322 101.809479 c +57.422398 101.060104 59.446564 101.719513 60.409737 103.348846 c +57.172985 105.262222 l +h +24.311525 16.242355 m +27.845428 14.958176 l +27.845484 14.958313 l +24.311525 16.242355 l +h +31.687706 10.577911 m +28.792362 12.976776 l +28.792278 12.976685 l +31.687706 10.577911 l +h +134.258621 135.860168 m +137.171616 133.482788 l +137.171692 133.482880 l +134.258621 135.860168 l +h +130.969559 146.198288 m +130.960785 142.438293 l +130.960815 142.438293 l +130.969559 146.198288 l +h +75.668030 145.039062 m +76.243362 143.043762 78.327271 141.892639 80.322571 142.467987 c +82.317871 143.043304 83.468979 145.127228 82.893646 147.122528 c +75.668030 145.039062 l +h +115.847176 234.450058 m +116.701851 236.775009 116.619583 239.131058 115.641846 241.171753 c +114.692940 243.152252 113.039856 244.550491 111.229118 245.332703 c +107.622391 246.890778 102.796585 246.235626 99.741928 242.309937 c +105.676872 237.691849 l +106.419823 238.646637 107.465248 238.766983 108.246910 238.429321 c +108.630371 238.263672 108.800774 238.046219 108.860069 237.922440 c +108.890541 237.858856 109.001205 237.622040 108.788986 237.044724 c +115.847176 234.450058 l +h +99.741806 242.309784 m +67.937111 201.431458 34.361767 158.934479 -1.280186 116.186478 c +4.495601 111.370804 l +40.216564 154.213562 73.851219 196.786575 105.676994 237.692001 c +99.741806 242.309784 l +h +-1.279920 116.186798 m +-3.258407 113.814377 -4.442426 110.586319 -3.346803 107.497406 c +-2.147628 104.116547 1.122458 102.612839 4.499225 102.677933 c +4.354311 110.196533 l +3.731298 110.184525 3.509113 110.314011 3.518988 110.308304 c +3.525570 110.304504 3.570336 110.276199 3.624385 110.211884 c +3.678738 110.147202 3.717865 110.075287 3.740574 110.011261 c +3.787205 109.879791 3.727518 109.904022 3.795444 110.156403 c +3.863918 110.410828 4.051676 110.838486 4.495335 111.370483 c +-1.279920 116.186798 l +h +4.499327 102.677933 m +12.622606 102.834717 25.825699 102.680542 36.987919 102.416489 c +42.562939 102.284607 47.599640 102.125992 51.211418 101.966797 c +53.024452 101.886887 54.443344 101.808319 55.382313 101.735413 c +55.867409 101.697754 56.148544 101.667206 56.272243 101.649170 c +56.369865 101.634933 56.239033 101.647369 56.024120 101.711349 c +55.987492 101.722260 55.526577 101.847763 55.024200 102.194839 c +54.785992 102.359406 54.143162 102.837708 53.738750 103.746353 c +53.220856 104.909973 53.351353 106.186203 53.936230 107.175613 c +60.409737 103.348846 l +60.999710 104.346878 61.130787 105.631775 60.609016 106.804108 c +60.200726 107.721466 59.549557 108.208527 59.298603 108.381912 c +58.770741 108.746582 58.266712 108.889893 58.169785 108.918747 c +57.834274 109.018631 57.514095 109.067612 57.357121 109.090500 c +56.971634 109.146698 56.482571 109.192627 55.964401 109.232849 c +54.896851 109.315735 53.376972 109.398651 51.542557 109.479507 c +47.859436 109.641846 42.766521 109.801895 37.165764 109.934387 c +25.976433 110.199081 12.643328 110.356522 4.354208 110.196533 c +4.499327 102.677933 l +h +60.786282 104.222198 m +60.809654 104.307068 60.897423 104.745392 60.929855 105.103455 c +58.742435 108.680466 54.195972 107.559952 53.865387 107.051224 c +53.814571 106.953873 53.747608 106.813766 53.728859 106.771698 c +53.718666 106.748245 53.702507 106.710175 53.696033 106.694595 c +53.676624 106.647614 53.659908 106.604416 53.655502 106.593063 c +53.640240 106.553726 53.620911 106.502960 53.600471 106.449005 c +53.557949 106.336761 53.495045 106.169464 53.413906 105.952957 c +53.251022 105.518311 53.008900 104.869934 52.696381 104.031525 c +52.071083 102.354004 51.161480 99.908997 50.034843 96.876465 c +47.781475 90.811142 44.658810 82.392624 41.203846 73.057480 c +34.294983 54.390076 26.053322 32.046478 20.777569 17.526382 c +27.845484 14.958313 l +33.112766 29.455093 41.347427 51.779816 48.256329 70.447327 c +51.710243 79.779648 54.831825 88.195236 57.084080 94.257553 c +58.210258 97.288849 59.118813 99.731049 59.742771 101.404984 c +60.054882 102.242294 60.295158 102.885712 60.455673 103.314026 c +60.536228 103.528992 60.595272 103.685959 60.632847 103.785156 c +60.652458 103.836929 60.662716 103.863693 60.666313 103.872971 c +60.671341 103.885925 60.661373 103.859802 60.646000 103.822601 c +60.640533 103.809448 60.625214 103.773361 60.615692 103.751434 c +60.597614 103.710892 60.531147 103.571854 60.480659 103.475113 c +60.150402 102.966995 55.604095 101.846634 53.416664 105.423340 c +53.449085 105.781082 53.536671 106.218628 53.559685 106.302261 c +60.786282 104.222198 l +h +20.777622 17.526535 m +19.123594 12.974869 21.347857 9.066910 24.252548 7.152466 c +26.852655 5.438782 31.562777 4.533432 34.583130 8.179138 c +28.792278 12.976685 l +28.944626 13.160568 29.138533 13.175171 29.096033 13.174240 c +28.995413 13.172043 28.716915 13.216492 28.390881 13.431366 c +28.077776 13.637741 27.886385 13.893036 27.802916 14.105087 c +27.740448 14.263763 27.684525 14.515381 27.845428 14.958176 c +20.777622 17.526535 l +h +34.583050 8.179047 m +70.276260 51.259323 105.658882 94.870407 137.171616 133.482788 c +131.345612 138.237579 l +99.814445 99.602615 64.462067 56.028702 28.792362 12.976776 c +34.583050 8.179047 l +h +137.171692 133.482880 m +139.813660 136.720261 140.928268 140.649261 139.688675 144.177887 c +138.396896 147.855072 134.944427 149.949066 130.978302 149.958282 c +130.960815 142.438293 l +131.647614 142.436707 132.038910 142.268188 132.232162 142.142303 c +132.415222 142.023071 132.526093 141.877991 132.593735 141.685471 c +132.722305 141.319458 132.853668 140.085495 131.345535 138.237488 c +137.171692 133.482880 l +h +130.978333 149.958282 m +123.319649 149.976151 110.381828 149.985077 99.363159 149.970398 c +93.854500 149.963043 88.820534 149.949799 85.164253 149.928741 c +83.337914 149.918243 81.845848 149.905746 80.807587 149.890915 c +80.292084 149.883545 79.869400 149.875366 79.568008 149.865906 c +79.424759 149.861420 79.269722 149.855499 79.134247 149.846558 c +79.081985 149.843109 78.947746 149.834045 78.793877 149.812927 c +78.748192 149.806656 78.519775 149.776611 78.242943 149.696655 c +78.151802 149.670319 77.732826 149.552322 77.269096 149.258408 c +77.008621 149.077545 76.415733 148.516479 76.117638 148.114105 c +75.779449 147.451843 75.526001 145.882538 75.668030 145.039062 c +82.893646 147.122528 l +83.035507 146.279633 82.782158 144.710938 82.444359 144.049255 c +82.146645 143.647491 81.554413 143.087036 81.294876 142.906754 c +80.833023 142.614014 80.417023 142.497208 80.329964 142.472061 c +80.061295 142.394440 79.845428 142.366760 79.816643 142.362793 c +79.744957 142.352966 79.691727 142.348022 79.672020 142.346252 c +79.647842 142.344101 79.632370 142.343079 79.629318 142.342865 c +79.623993 142.342514 79.635231 142.343292 79.669975 142.344788 c +79.702553 142.346176 79.746620 142.347809 79.803734 142.349609 c +80.038528 142.356964 80.408836 142.364441 80.914993 142.371674 c +81.920067 142.386047 83.386047 142.398407 85.207527 142.408875 c +88.846886 142.429810 93.867622 142.443054 99.373184 142.450394 c +110.382942 142.465088 123.311569 142.456146 130.960785 142.438293 c +130.978333 149.958282 l +h +82.893646 147.122528 m +83.006897 146.584534 83.032082 145.826370 83.012100 145.618286 c +82.997322 145.512054 82.967239 145.342422 82.954086 145.279816 c +82.929825 145.169250 82.905975 145.085785 82.899849 145.064331 c +82.890221 145.030609 82.882523 145.005676 82.879196 144.994995 c +82.875359 144.982666 82.872734 144.974579 82.872002 144.972321 c +82.871216 144.969894 82.878197 144.991119 82.898605 145.049866 c +82.937004 145.160400 82.999245 145.335785 83.087128 145.580521 c +83.261513 146.066177 83.522041 146.783676 83.860504 147.710892 c +84.536697 149.563309 85.515999 152.230911 86.721222 155.506134 c +89.131294 162.055588 92.440521 171.023193 96.025543 180.732605 c +103.194412 200.148285 111.466515 222.531342 115.847252 234.450256 c +108.788910 237.044525 l +104.410728 225.132568 96.142120 202.758942 88.971054 183.337341 c +85.386108 173.628113 82.075554 164.656921 79.663872 158.103104 c +78.458214 154.826691 77.476135 152.151535 76.796440 150.289520 c +76.456955 149.359528 76.190903 148.626892 76.009583 148.121918 c +75.919609 147.871368 75.846710 147.666321 75.795128 147.517853 c +75.770538 147.447083 75.743820 147.369186 75.720924 147.298798 c +75.711311 147.269257 75.690361 147.204407 75.668648 147.128326 c +75.659912 147.097717 75.634186 147.007111 75.608788 146.891342 c +75.595062 146.826141 75.564598 146.654419 75.549614 146.546570 c +75.529434 146.336914 75.554611 145.577637 75.668030 145.039062 c +82.893646 147.122528 l +h +f +n +Q + +endstream +endobj + +2 0 obj + 9809 +endobj + +3 0 obj + << /Type /XObject + /Subtype /Image + /BitsPerComponent 8 + /Length 4 0 R + /Height 676 + /Width 544 + /ColorSpace /DeviceGray + /Filter [ /FlateDecode ] + >> +stream +xì݉šìÆumkITgõ–D[¶d™|ÿ—¼ÿX+€DmÒ¾çó!7K>5@ÄjfFfU±ùÑ>øàƒ>øàƒ>øàƒ>øàƒ>øàƒ>øàƒ>øàƒ>øàƒ>øàƒ>øàƒ>øàƒ>øàƒ>øàƒÿÝüxxtU蔩š:u?öØÿ Õ›ðÁh.%‚í_»[Uùà?¥]ÊÕ˜Þ5×ë1£›ëLïøà?’½ÌHҵà ç\S>ôñÿ›hE½x çdËÔáxüƒÿíÈóðu›¡ÑOj¢þ>æ1Oð¿É¥»aÄuÅ…ké@Ö ÚÃnعQgžþ™Ômµ²8½V·í‹Í¾K?™îGu¡?£ÃãlV©ýà‰;;›Vo¤>ø1è@?iGy\®.Mí…£|ð‰ÔIŸ¦ú†}é†ì#…i†×(îÛv`"k}ð†ì…Áê6×?:ý'ÝÁvû‡¶~ëyfšÓ&µÄÿxLÚd/dw†“XÍœ4À7ÛðÄ9ÑX¦ìƒWÂ2üƒp'ð5Jå$|w̦Ól‰ŸÌKªra¬Äôꋽ8í6Vþà½"?rT³åÁœ=s{Ieºiæ–¡Ñ\ZœŽjîÒ•¥A’ÐÁHëôƒ÷ˆÔ”IÚÆÙ§”ÜÓö±1ƒ‹=¹tsñ:m¾9Ó\» U[4,üÁ»Drdçð&ÏûZ´hŸÒ-¥½WæáŸÔ>ïœg«qõ<¬2äƒwIÙ©¼E"¯k†ÊpõöåUÃá| É„l´oH[kÄï““hÉ;åp._8•^ƒ’í‚2¯þÿ0·é¦×(7ÙñÁ;B:äD^jJЋ×?±6ìÂâ4¦›T«õ”¢½ºÚék•å\¸àÆ`„54¼dEFê¦)YQw’¶¼{Ź¢N;z1–ÖLé8\ÃZUaƒ ûà= ²!º“£oÏ'¡´ñêµÆšÐM‰½jP³/¹cØŠ®!¬íLYœ2ìƒw€lÈ…ŒÌèbNú¨UÏø˜)Óð´ê—3žæ´w¥¶fFö†}ð(•©/J䕯é§$òbÇ_è*U§ñãŸ|Qƒi–¹!w„öZÙpÐw‰qüð”Ùø6$J¹8C¹ì+ÿîùâ‹Êצ1Ã5“‘§»`V†}ð2i¨Lv¦œ(%ï\ÓÍ…>\”Ó:Ì9=T©{e0ƒ +÷k_´Ðum[h]Í? r þê[JÐùêxçlºs&Ñ‹¼+1²ØÆŒ«Úaú¬¢YÔ ä¬i¼Õ¥`è? ¢ÛËö[xäÓhŽGÆk;ù‚ k ôÂÜp½0Ït§›FW_½”B2•~ ÊÀP·)‰{$Ó,õW½˜ÔT!ýjè/=yÌ÷ŸVK›GÅ E½ÊÐ~„~S››Wÿ`SªyO'ã®)ÏàÇ5ýd㑆/º&V4ÿ5Œcè?Å^†6AåK«g }•Ci®®¡üŠ«.“k.w˜ûS|®ÜwÀ>†~ð¹)ìþ©›Œ'{åNhÃÕvªÐÊ´NóÅO¿øéOkÑ…}õÁëü~qW¹j¶8×h§KYüÁçDØ_\‰¸˜ór&G²ä8ȪÜjÚÓ$õ0‰^Óªµn^³šºbTÛö·¶ejåƒÏŠÈ‹¹&®þ‰Ü §NšåûêU× ^²À\Ðî1£îÞêdÛ×ÜõlÑÊÔ«ã>6ÏŽ¨/;òÃÄɇ¼ÉØoöjPv¥¼fKMÇ`0B9§_ütÓÀ«8Ý`Ö}ðÉi62ùƒÏGøA6ûqó©‘o’Fdù°Bˆëâ};®½^]L³ÌpV™F÷â˜æ’â´úÁçDÌ‹º¸«›m¿‡˜±FƆò;Ýà¬/¡oé‚/§Z·ª7{éqÑ ¦¶si°øƒL¸;¯0úƒÏ` wèÄþ¦ /&JØ@bR«LSY:×Ö×(q:›¥Ù žXwyèd.ž=å|DÚ¡W Æ40 &ƒ“QU1^öK…~…0Ía®t}˜áœLƒ™E£Sk”°"ZyZÜV-Lÿà3 ÔúI…rNáÊP§úNªé¾9”Þ›É÷I¿¡:E£íÝ©ÓÎÍ¥š§C&¬-3œï Ó?økGœ­{Ú²*¢®´U!£ò©J·œËû0gN§{ Œ qç4«zèw©­Ê‚i±Æœ³ûƒÏCÑVçze¥KJüådúN äó§eU².ïÃÏÔ×Ùžk»0C¯UÞbV“Y,Š•ïuvz–ðêש¾ü¨xd¬±|¾EÖ‡Ÿ%€êͼôE׿µeF{K3?˜3ë³å`XQ¦ð˜@‹wè'ü' ›%UÑh+xew-Ý“ûÁˆ&´S5n›‘»˜)›¸E˜ÀŒìœD£?ø<\á/ìÛ„ì¼6\ýEI–iÇEÔ¼º8´çìà™Š gÚ¦wf˜=/}„kÃ?ø \Á/ìÕÓÈLHÑ YŠfЗ[i–Ý É'ݶ˜s­µ®;ÝçLU=˜ßİޒ kϧ0ûƒÏDÑ. Š ¤åP¶ª¥®:íɪãt¥]þqº‹ë´×sL«˜Ð\µê,¶?À@W™ªUìþàs&”CP¥C¦ªCì…ß§k%µR²uó£N¼bðÀmËŽÌd¶ÜkÀLÖTczÀê>¢ý ¿¡ŒÈŠ|9ÙtÉÚ‹I§ÔNŠ @»üügŽŸSÁÒÐ…FŽÁMÜw—ff ëBʃÁœ±˜á|߈²¨WŸHÉfG® * ƒ¥×è¨×HöÂÖ·ŒPT¸qîoXïq“™n» ‹S& fM™ŠŒæÁßÅX£•‚‹FRDᘂÉäP9®q]¬ +RƒÑχN§vì3Ð:f¢nçE«µèÅš¤^0,×|ðý1áéeã¦ÉUÝæNmr¬¨Ø–0M8½Ê^ÓߘdhZÓÕjÀºS4t|ã?ø^)ÈêD^£”’êäE¢†{ …£µ¢°¼FñæǹÝxÌih³ 1ޱˆqÃm±‘ãƒï ñ'H„THŒìĶKé“C©œÜÊñ[¿P·Á¹vã®a&yƒù/,ÏlÇ®EÝ ^|ðý!Â'ÚZE¤fSR’ÊAL«FYU4g$û—,Nw.ÍÙ/¦uí…9.|Ñþt~bÕúl“N£[NoóøÐÇ÷„ðŠî· 7Ò©ÓBþªò*·Ž“vÙÿ($¡Á¶¸.¬@êf0Óu\´Œ:ú¸„ÙµºÐc=O>øî)°\/È‹° ½4 %ç@É¢ÜiäT^C‚¹Öè F«ƒÓÅc8¸Õ½µ]˜XµZkh[Ÿ, }–±òfÏøòÁ÷€Ð&Ìa äH¶ªÛ!ÄH¶,C¶‡Ý>ª3ֽ޹àžëf]t¬×±°á Œ\ˆ¥æCß×¾Ñ?„miQ¨ã$Jw!2z!Çå @EWjo:^XÌ0˜U³Ìb-yÁ¬Q‚‘‡æÇßb+¾ØN¸½'_”u’¥¹˜lNý)u(gø‹_ü’ðKÃoâE7 Muaî)ó‹÷ÑG•Ð(Ô¡,WϾ|ð]#ª¢{â\ÓiñßD虪y “—&^P ÁÔa®EWªáÖÁ˜ÆÄfGë:å2™ÇÌN9óÁwJ1ÛëEi@ø_©–RT‚¼™QöJâ¤SfUé_LWÎ/1•(W¯tY?¸5æALo }Xú‚5`Ÿ¦Ã@«?ðèƒï’DõB´Å¼f('jÊPåJÚ:J]êˆ2«L®GUÐÀƒú¦½^æ™J³œ)gzË-–>ÐêE¶Î÷¥ÛzÎ|ð#¬óÏí)~ØC8‘¨“6 ù|QŽ%YsAÕZ}cNè„IcVØ¥HÒÒYå÷¹ÊXîë?ø®)ªê +¾­£>]èÂ`r©[æ ¿È¯$_”wz@Ý/~ñOÚaN®Õݘdæ3ïOkˆã(ÄÒ7LcY3™ú úà»DH…5l'à‚9 ýˆCÂ¤î › ca\ —zÉÿåèbOðO çR_wã—6³€ê.¬eÉnõQi }pNyôÁw…h¦%N„ &2Qy!Kò%iêæP*‡:)~!ó· \õÐë7n‡k³ƒ˜s™UtË,?d±ŽleuÝUùóÁw…hV–¢õrP¤áüx7˘㠟ó¦—Y)†CÒ_Œ,þiÇø”8O ~Œ9ø³ÿH…8¢å¦-ceÖÆÕóêƒï½pMU«W“Lr*¶[èƒ>ªëóK ¤ Ö@§¨.(;‚G<£™‰@ÒP‡ÂÀt,bØÂÖØvÝáÕß ¢)žª@c¥FÜ!’É‘ ¥ŒiäN1I­@¦íUi—ûi+†Ngs:Ìs0£¹¶m+ÎÚÔ‘YÙ¥ÏÔ±Û×&ãÅ–øÁw…pjt4¡hª‚~ Õ›Vbd’UÖ:.}$ͬþ…ܯ*¿úUºAíÞåæèi³˜Æ¬ÍÿÂÂ`cHcªca4›;r„;|G¥xn€aàÇ­ ‡”’MŒVŠìõWâ$qÑ‘‡*É‹äoæI3NÔqÉã.̳^« MZÌY–y7¼Ëƒc|7KÔ¢-⹸ÈÕ¾·¥pNY­Ì'ļ¿LtA¿úÕ¯U׺0m¥[Ü7]˜8‰l!ñg:Ç0›ëا‚Ù—:¸óÁw…hÆ„¶fê YY¡Œ*i›” ’IòZ#Á“é[‰‚$ÎqëÃÉÁ`nöÌ0³ý­³ ¥9ÀÊÐ\Ê6³›0ÔàØßBÙ'Ê2½h¿ ¥Ä¨Éƒ.Ê›c‘OªP7Óìmå>IL£¤_ÿºÆxèGôà0s æ¶HëÕÇX1*Èm-Ù_7>}ð]!¢C¡U•s „EBHC9”¸)ô]R*µR\¦Õò®À’"Ô_ÓGÅ U«‡g cÊ4&µ,7F™3°rI(TÁö…_|7ˆfï Ñ>HBHJÈÒPÖ!Rª˜*Õ(í UÄÈÂæqpÅ%7u›¶¯±£m$Ý™ÿZ,RÆEÄQ¹,eõŽ}ðÝpÞvÚ ò­ I(ú“<¤j26•4V]PFEÒ_¬.N‡¹>¯hÃÑ$*}ÀÖT±`‹Zþ‚9lCæ-Ì~­02yjnJM]õEg·u«ZÇ)ß­$ºâkÖä1eI6;2“µò“ÊÞûÒKê6R_òé!~£â7¿þÕ…W§À#Ô±ÇÁ´°¬ +,B!·8ÀbŽÄôüúÇ‚ÍË5ÜN«¨Zå~Õ¨±‚ituKµò?Äóðë * +u?¦)Ä‘C¯jµ~ëÿ ž_v$¼•mн,H…÷*uäk¹_ü¼Ê$¤ÒŒéd?¨Ä@ jò˜‚®ªè^ÕcØ™F"¦Ž‚•“h­Â¢`CæÇéùö“8Uª }qÎ\ÖÆŒ¶Ü\7m üOð`šà…ø +ö"!ðÖ…L-27¬:JjÈò"㫤0å‰S¥J$Ýæ ˜`g2áBԡЇ– t»öe§Ênl»nýc‘ÍŽlÞœ¼a}ôŠ2Õ¥ù«Ânmlô?ÃÃ&o†0f¡Ž‹ºèC2 5eéÔ# tHi5uüÓdZÂ'ë“»)ÌÀQ;Ì«µƒ‡0ß^ÐöaZêZÍ’+–°ÆöA ¬,ŒçÃÕrî 6ÇZ¯Ub®OsÚ赪kÛݼîybúÿ=ªžµêÅùPä!²©)CòÔÛYÖ¨£¢„B‚ùŽI~Po~«N«„;¨Ã±¿94©J!£k’G0ƒ-ŶƲ=¦Í1Þ½D_»}v«ãA]\½ÌÔT$Çð[ñà·)sÑ"-ÛþáQ7ê¦^oH¤cäA ƒ|M‘:ù+‘2JÊ­ Œ‡4~ûÛßüú·T‘.\®[º‘:”hšÑÇ:¬eUÐ#^ÙBY<¶_ðŠ{ï—¬«Ö:†ùòé„T8†o^¸°>fT¡;ßÖµ5pYÝñƒÛºkŠin&Ðb_dCVÐî!U% i#eTIƒ8þ©˵ô˹£2‡ãÂ8êºÇ3=zëì‡Ö™éÃúLQüî-¬d)³Á›N¾gÈÈA²‘§va“ýäuþEwO#{šjç nZÓ5Ý þ{Üáž)1ó˜(@Â^7âqÊF¿ôKlF%2L’]ÎUù_9PÅÍœxm®jÝ=8ÀÇw —ùدQ‡Çuõ nÌ_蔋dzQå5ÿÝàî›9±ê¨ƒ:ü6ê’ˆ$¥Èy(‹Ì’†Ë2…”îI¼ô_ü5êJ¤W+í.î‡9Ðl¦$%y¤ÆÄA {˜•@8v²<8Žhøøža¤„©53ºØ+ߤËÕOØK=¤œ&TáKÉ'XŸ ÿnZŒLC‘š‚^ð%bÅ!;2D~#&_Wöä²#]TÉ–ðrU]¼Á /ÜJU˜Ë„Ô1Ó·R⨂L¹’} {Á™ þññýÂÄ‹§ÙCª“š©ÐUæšò†— >}å ó"¦„³ÕÍ ºGuûvË[ÌE¥BB$&¤)ˆCîò¹Ò˜©CÖ/h ü^}(„:ècð°YLw$bbL GG +y\â‹ÁnlÃÃ÷ +ãxctîs n‘IÂ1¯ÔV:Wº8åLÞÈ‹šÿ‚½m[ÝJc4êxa*k¡`‹øOûòwÙ!%¤>RH‰,£“Ø,ËÒ-íÃê@;üž2*¿ÿýouí¸¹Gz’´Ì³˜VW ‘5Lbiƒ­ '^äë;¥°×}“\¨.{Ǥ1<çLSõjã˜ö¦[Î…¹G=§³ÈÍtÓ ¦º‡ÌÅ[Ô_ÂV^‚6 cGÄ‘:ÈC‘`®–ð¬>R„JÉ$\ïµ¹¥G<9ó˜&J ÁG#/Öøq‚+¹$ïæáê/‹§{žì¸¶Þ踓ªÆ¸#ά3Šk‘OØ_¸kËÔ?íÉgú /øRÒ™yÊcÔ‘<üÄq +‡åœ†#‡t¡^¸|^˜›=GÊ¥“,í«–°…M ËÆ1”>ž Ü“Š÷»˜È¼ÌÄ´Ó`úW3ü+7ê똷hõ•ÓÜ]4¬Þ¬/îó3xÝÜÈì‡ùy@ØÅÿ #äQz$)y´w(#›‡÷<Êî ÛRîXh§ûƒzXyÔ¸Ÿ°F"Í3S°ܑǠ²ì +¦Ff G.–‡w ó–ó'5vN.Éž'¼T.Þå‹/z@yq¾ÆÌÌo^9cbnÍù +7q(‹O—¾JLò-úu@‰CYq”gÙ–òð5Cû»KΕ7x=&€©V³üÊjC+§ŽùgÚɃ@ü•™`qŒ¯ƒhhÞ+cÞØªi<ƒAO5Oð0%h¨ç¹¶F ·]+‡×礮Zs>—ôŠÆbª~§g…Vñ‚ràq¨«_<Ä!•Þò#H2u(T¡&‡jÛÆŠDû‡?üî: wy&qÐVêh ‰G #E‹˜‘! ¢°ÏÉl_W8£B&ÞŲå˜y æÃÍäÓ´SÖMõrØàæ {нšÓ¹0]ãé5 +¼€O¶®'^pc±´‹½àrQRæ—uoòÔÑòœ6⃋‘á4Š:ú@¢ +êPLG °DX’(­ú`ÌÂ6F‚Õgbß òñÎ`T–í`¬­ÎÖjW”muµ2£÷à锃»»_§ïÙGÝŸ•gÔËg´¼ÆgÔ­Ý5%(#m¸¸‹=fû ŽÙÈC äàí/ÍR=òú&þYM ÿì¼³ÅMáyœÈ“&ˆEA daQ°v;ö2?oÔñV2ÞL[˜Ù‰nk¶+‹ ßJ^V?a/ÕN­ öÄ„×zô^ +/*NÕÁY)5:ÓÐÎ’>$à #»}ü¢t©2¤¡,´¡„„ƒf„ REm'‹ÞIt_ú Wm «ˆµZñÖG©2Šlíô!Sò©“@â8ò˜ÜÊ2ä´àÐnÓý±Î+†½B!°}¼42I!¶§#ô‘:Ø‘-cÓCÁøœáÓ…|¼7X•…ê–å Næt•×· Yé—ù¨Ì¨kCêtŠêD¦;'f6©F[¯Ñ~ÂãÊÓÔ°Îq·{L3â€ìÈѨ£Ï…BF2ªÒGù•ì´òŸÒÓüñ+‘ªëp¿ÇÄ1£PÇ` ^ò Q– Õ²`ï#z !ï‰û0¶N3å bO/‘LWæãt7{Þ skÏ¿0ß™ùàÌ•il{uØõ¯¦‰Q?Ú¨HˆÜÜò H¤2mô'XòæY8Hà TÔÁËnP»y>eÒ‡ƒ@.‰X&}XÕÒ,À*„Qý †‘`/ó};Ý»”ëürt¹úFÙî\ªU9×ÑàÆX™Ü(~#¡Ù‘[; ³j [(,×@wLÙö‰ïÏ^¯Tû 3ml/è~Y¸Ô1ú(UR6ÈŸ,Ž<ˆCyL®Ëû0]Y$Œ?Ö«‹ž>P‡ fÿHv¦8Ÿ0V¦…>V  ^ +ÊO&NyÁÎÝHÈ;Cà_Œ­Ó°oÞð›RºÜã$oµö›ÌUOõÄÔm˜N žæ|ƒnRc4oX8ÚÀGHŒôØ9ÐG‹ÄAGÒr,ÓéÇÒ! :=\ryòWÒG“’‡²ê¸ô1°…=‰yöÖó!¯ÔA:ÞY¥ŠúšYçBã¡Sðk1àä ?ÚØ~ÛáM0¢þªogvÞÊ•e¬98uÎlfÅÌ/æØ?dErdh"e©cJ™$)µþ'Ïjâ(õR ‡?þiÊàÜ+/Üî©vœýƒDˆ³àS G#F²·ÊþVŸÈÈ;‚A,ÒÜF>L—uÚ¾;èâîøX3ÎBÏûmOƒnP àì4¸úÜ´°•oú®ºÃ\x¼2XŒ:Vr‘>nuŒ>F!2xÔ‘<èCŽ%Y²%=R-TiãO§‡Ën<'1˜“ä´Üʰ<“Ç„H@]…6ê/¹kñG=Òþaþ°y(YÐ6Æ `ÛÀPÖ2Ÿàÿ²òŽ`ÐÂÊËHÚ: Ú8´JU{hìlò3äõ0£ŠAå4?韭SO¿ê6C«]¬)lÓÞxvî6šÚb_Ì{AÔ!;}5]u$Þß#"¯r+Å!ß¶´à€a½jP9ÌécÔa˜GbQ+<èƒZÙD~|ydsæó!ßx¹„ÈÊ»1 :¶ r±ÿá1ä’m'Ÿ KŸàâ¡d”ß,·À®îàú·2Ï™A?XÆR³yÈ@È}@ŽèC²VEêúfJ $RºU™§RЪ‡3|äèCñôêcbz«P¬Š´AÔaï˜†Š™ÊjÑÈÿuÉ–wƒŽ…ÓL{Ì–‡©µÁ-~MU.fÈ]Ž/…`põÅsæÃé–¶ªûÂ=È–è5Ç\÷~~©#d-$8H,q,Ò=9—úQÇ@Ú?«C/x5Ü8Å“æ0—+GŸ0ñ5‡D¬kñÙ<„M` } lϪh?xwQÇD½:î¬í7Ç^wgônËuœ  {½ >ž8WìÃ=X>9=4‡Æ@SYL׊ÄA Ä¡„íãüÉVºhcÕ±âø†6_\ãÍa…O2¢^”U«ïOnzyoÕ 4qSï*ÃéOw[âŠKnûcfƒE!Rƒô¡\ú ŽK!½£GÈ?I…š,´õ5u®t‹ˆg‰‹DF!fF +Q,ia°1 „:Ž>&+˜³Ÿ.`$}ôßbgø"Þ‡rñ®È–±i +Ø{Õ#ê2¾i8)1ÕøÅ™ró_gfxpÏÝ`Äé†Ë*ߊ¼>P¨YLeÊbNŠT„´ yuüš6àmNAÔñY–êGPÿü¥zøÓùå¾t‰Ä#TE©#L<Œ<.Œ<À ÚÈ>–¾ñŸKœz1É—÷@†T–ÛJ—å¢á••rršÉÍz;iãÄ÷ œ(§¹C«C‹p2ìÕ0zÝk*±Ä?ˆ” GIJGÞéÒ¹òd:ä_Þ €Rˆ„q ‡¹‹œFaÓÑÇQÈÈcôA+£úX\þs„7˜àW©ydÉqŒËÔ‹×(xr²¢-'jí²_ÙVE 3Òv W4w½ÚB¦Õ72Üþ¢óÁ$ƒe +¼øK„·ª|H +¨£T©Ô‘<ä>DR<¤rNJ _:@{T0-q$Áƒ7 Ž>ˆ…Zr䡤&y`éÃpH¨g 9ï,qÜf ?bîÍ%‘üPKɇþIiç:çÕÁ5b S§}a³ì¤-3 Ž&T¥Ú¹Îàpî7ÏN<òèeH‹äÈ’T­<.}ÈeòÛ§8’Çò%¦Áœí5iiÄc0E +!8eäѯÈöç—g‚¿À8À¶Q‡(àw¸$Î7Ò!3ïv°…tËšÉ\¯Ý‡9‘Œ%ÏpxJl·íÅ9;Mˆv­¢Ñš=¬¥ ÖêŽí_¥›§˜ íʨƒpåFVÞÂá'´°ì`Û¹©¦SÏ|æÆé¾ -hãù“KsBäC"z»J qGHØoÚ`.}üÊ¿Ðýò¨ƒÿ9ñ…¨¿œ÷C3dßÈA?]šX~Ìn,œâXð±Ôé ûöÀC¯F½OGÌS×Tý]>œ[b)ýS_\g?ñz·¦:ÓÆŠ’2úh÷ Žv¹ƒ,&Ii¨£ŽÒþV¤±4º5â·º=jy¤þçÙ>Âvµd$S#x…xÂþBnÞ yÚÕX­|J^¨\‚¬ÈKES5xÎ}!ÐCkôjºr1OhL n1lŸ™á‚‹ÜÞÃa¹…äÑÅArR}ˆ·ô +DSÇè#dY¦G+?'…±ü«ºg®Nq#ú‚š:ÌT¦4ñèƒBF +yÒ…0(uT'`yN䯸¶±Ÿ²óÃc÷Ð —] u >Cö3à‘wûú§ãêÕrTŠC#‡rŸkêÜOM1ÑrF¢†–:­æü³‰ ÕnQÁ¤Â’Ò²›q¬:ò HnÈóÈc´‘F Óüë¿V†./n…gÂóBo&6=,dEêHô¡°‘ùÏg~çw„‚.öê"7ï +‰mc,e0U8 áCØÖ¹4µTæ£:MU’øþ†û|^R¶që·`ÖC+A½%1 XÐ/ßx0Ìz"£ò øY‚Bȃ@d±¯¦éCf¥WŠa7}€.4IÓº2tÙ Däf‰˜…ÒìfN! +…XÐÂÔA `!ß÷E#œ‚0“È¢— ©y0c ZŒçŒ­‡5\Ën¼M '?óËÕs2Í3·{`ßJ(\Ÿ2­ºÜ#¯\x0LsÄô!'}¶Œ>ä*uÐäð¿÷vy$Úˆ²ŽK +ä1Ò ŽŠs¯ôšê6¤#“%~b‹¼ôóýƒ]GlÍ[u(®B<ïÊeæ=%XãÎÐñ ë%%Ê—Žsã$I ¾“ñÿ¥Ê™ºSc_8¸8·Ï 06ÝÅs¼XwÙ‘+á.ÏzØŒ‹ÈDÛ‡wîŠCñé’@$Ž<ä±Íã–Gi&e°b ‰ÎÕy¨žñ¤çý +u01íYÂ.e±—@(•AÌ”aLxÂãMAÌHnÞ ~4ÿ® ²S}2ö;^]ðq\½ö€¥Ä†b"RœñÏ~Þ?èï‰7˜Ñœj˜Òq°ì‹‚ +÷;ðSšÑôƒTGê8ú)ù’³Ô!‹‰#ÈslÖiPþæ/ÎW;ƒ[G=j2kóHxéƒ8¥Þú0yËå˜Ð +ó¨âPbdçpdñ„©ä h2ÝQV**¯†ñPzÀcÕ1ˆÁ Äéð«ÛÌž«Ìd&mbê•°¬:¸^߇<¾X$ä‚>¤¥üP‡"_ÔA²¸úÔýê!Ç#ŽAò‰ÃqhkFU7ÒÇ(f²ƒ˜–>`‹>RØ’@X–ÓŒe1¸0pJ„S‡R6ê 7ï€ Q6Ö1–¹Ù ­Qmž” àoò¹rØH´[è5‡x¡^;¡ê)Å(ÌÚ¼Z#½V]¬ÌÇàÅ)áis÷€mø´—‰‘ Ìöá=í‹ôIãªÒ°¿ôu©OË¿ü%ÈCuš:ŽL?Œ@¬Jm Ù¦2^WùÁ%¿züeó`†cØnÌÔŽ2®p*6?·6¼{/Nþ¿‰ëÅèç5pãÅÌ`*Ì´7–9¿U…Õµ‹WNaÀ®Ù´#Ž#¶)rô«y@.i#uüQ‚ÓÇQÈìé¢zÓ˜(à5¯O¡¥äÑæúØo V Ç>X1¹>_¸ÍØŒ>nåT!i±·¿œ÷adyÔiM8jËN[frójäè ¦ÿî*­™Lf¿˜ÅÞ`}×—]y&3¥$øl9òðæM ‘yÉ#4¯èƒB,hYû‡ÍcH ì›ûÇ3Q]Ä9ÄþÆÛö=ÀŠl¹a#C×àZÊ^…‡rà^ÉQËŸV9=ÈÓaRÊ´‡N:½z󄊺Q1œÖ:Ê©– ½Sû³×/ÌdBÓ¾ÔAÞÁÄAræ°}H¢Oê PÇèÃn™ ÿuüÛÑÇô.{5Üž#…B$…X‚­G ­~ôÁ ú8ó3Ï8Q^1D^–ÉÊ»€1ŸÀÔÙÙ>òøTÁÉ)Ü|™:ì¨V©×)F†ÃÞ?½‡aºSäPÛª§*CëNãIS`&— ò $´äA>Ve¶˲\ÃÇ *è*ù7—G îTSG'Ó<kX +µ}€%}¾¬>XŒÏ!ØA°:uœtÈË{5ß k; KjÔrnÉÓIPŒ÷’$M!YF;~2W¶Yö=®¹bÇâ¨jÕØÕ]êõ-;ÁÌ)QGòÈ—ÄÉŸcÔñÜ=ä j¸0šâyœcÜŸ><<ú8I~Án-?KÛµ’‡m lJ 9n²šb)ªƒ0ãìÛBnÞLY£fŒÅt¬a¨ ÉÐ&Œ«dI™zòöü>s{÷? °b·ÌìÖØ•°B ÷]³ª+æ\…y¤Ä1/ðpÉ£Ýã¨C‘ôÙ@iPþÝÄ1¸gõ‘Bp $y(Äší)„@X4ò`${Áˆ¤È‡r#øÉ›žÌ`ÎmY?Ë…càÀÀ¼ì¼àìÀqY‚ÈÒ…¨\¼¹¼×Ý>íÓ&Zf4 ˆáºàò²ÏîüòïØˆõßÛ§Y“yóä$O^绨"±ˆIßµê·án·k¯Ò,¦‹3²F ú˜±3¯{`Zšmƒ„ÈLê$âH6þˆLöá"­’›4ˆ£”Ë=Ø7¶D™ZãâB& ŽÕ‡o½£ŽÑ‡ò¸·Ž‚ÝàÂpä!Ä•8á?Hλ€)c×=`éEæƒ8¨Ýž-ýùÏÆiµ^¢„aþ_!(is8œun€Z·Š›WÂlM7²X –4püìgûJ·«=k*38 /~zxèã‡BÉcô1!ŽUG•–…îô Ä]•£O§ +9ú°†•è£ÿê¥ÆØ]þuNc¶wìWTêPÒ‡éÒÇlôAj0,èÿDG!¬b˜;¡hBpEW=ˆ=dÄŸÁÞ zˆÌfú0êàÎ-à*w%ÛžD ‚",s`šZEs®ÂcϪæx3#´Ð…¡2Õ‹ÏÏ?©>SJ‡wmòH÷îá‡PGú Ž#íÈ€6h¢ráÒâ†pûl b3™±íƒ>|ŠQ¬œ@2ê`[°uâÒQˆÐŠ2¦ œ„¼—¯ì`M†yhl¿àKp ù§¾’tõò¼ˆÈ²Ñ)uÔ—2·ºýÂMUÅLo‘‹Üf_Q§9«ÎÄ£ï[sL‘‡´<èã!ô!Ó²-é € ‡§:èÕÁn#[ˆÇ ‘>Ì›<í)„BÙb;cVæeiö ÞQyr‡"ÞBÿ@bÞcÊ f²7sµ ?V 9Ö¯4ƒ§Þ ›¡Eˆ + +Îá §{5ƒÜîÓfù‹œo˜U/õb\‹QGú ŽÊêCÂ(ÄOÔ1ÿSòPèƒ<@G!’Oñï%‹·@à·ÓÇì4BH Ö¤êH¢ñ/ØEQ0…T\꘸¿ÈûÑÇq?:vGuàO8Ä+Ç ®JŽô¨5"p#Yè‹{íÖ'Î ÜÒíw¿…š2µÖ5×ݽº2¬–<0âxɃ>’q$‡·½—fêp ²O¶åß“Èô‡^ ú°Ø<`Ú¥[éƒBˆtô±ÿrcÁz‘FáؘP_ÈÁ`óx'ò`ÇjƒQFGlføþŒ8¼ƒ÷wù\®x_ª&:½t•¯iõq¸p%TïfxÌ£ÅqKU¬Ôa9ÅÈRÕ˜ÎãÍ5òð¶…É“„8úÏ{È!y´wÌîA!6êH2?PÁò׿RÀÅníêX…˜kÔáóÅ")Äªéƒ ÔN®_,e:‘BBPEV¬²0HÎ{€%lÄœ>”öÍÂ/¾!}ä¬ÜITe³;týRÞ‹ŒnW[QõÛ»âfOmÄiaÞfÕ+­hMWÜž]Ì—:æ=[rÞÈ#ˆãÈCZËîÈ#q )„Êý¡ëÄáwŽ<¥âÓ+uŒ<üÐÔÇ‹£œä!O˜Ÿý«Ç2¡ù}“ÚÁ/¤æ=À’»jØ{`ü%qÎp‰c /.KUu¹Óµù*83«i…l{Í‹6‰ÉµyËcµy­¾'”&S‚BbÔa«—°£)”Èô!©Gÿr>Zä8¨@² Ž¡1¼Úª» ¤gGéƒ@Lnûøg_„­9ú¸6ñ7ìÇwï+á£WòH ù ?](ªæ]À’˲`êÂrºPO$pôAô!·äQ’gÿö.Hc¸ôQqyø;Ì`ê}˜›>ì»Gêmä. ™ +¦ › +â ¬k›ÇÉ@éP+ïE²“¥,g>yp„7àYoÂXø;ÙšFºn„EÆ”£7쫇²ë'fÂtý`SZôO&ºxãÞ³Ô¬äý*/H%[r&s"‰£ +‘ZÈqâpy¿Ä‘TÊŒæÂ_çå¹ÓÄêãÏ_š³ ÄÖ²"}ìÇ k‚iãÑú`!ާGò é=úo9i€ hâEgª«^Dw{Ž<èƒ8`Vº#Ù@¬i!6lâT±uôA!b'€‰„>‘¦Ž'Ró.`ÊC³Æ^eh¦<£ [çªfõÁùrµˆJür.¼Z'ƒA§S4• ]LŽ:5Úž2à 'qëƒ<.}Ø>HúYê¸õaãù…¾/ì=n†§¨C1×è#qŒ<ì ÔáÃ>„E9ÅDf&ú5G†à†PCìc²"9ï€ Ùª,¶9ºP¶çˆzxm<Íß·Ú€`JÏ|c%£m.\|’:ú7ͳ¼ ™·‚ÀãÜsÊ0ß }3,=+)ƒä¥úÏùXx‰ã°ùo¯æ£å…—£ŽÝ?$…¤>_Ò9Z–6ÀA…gÌ'ˆÕÈ£ã’ÈKEþ¦=ý=ÀrP•ú:'óqlŠä,u¤yLù•fɦ IW/e {ým¤âõ’ˆÇSÇÈcb +ñ³ôȃ>ØÁSN±•¢$V‚&tÞbŽA|qÞŽ¿¼uŒ>ªW{`kÒÈìZ~Ð… +žÍ_5×W  ù‚ȈN&É;ÇÅŒ§‰ìßæ=lš™H»=º]šQ‹ž˜¥HCN(Cñ£-uÌ_ô½­ûá"}ÈåÙ=Ž>¨ƒ>RÇüøzïa†vðââ š#¤0=u|ºäó(YxP¸„MüB(é#…ˆ³ƒ:&ýû­âäÀ]º5TF°úÀ©à`^ªÄž¿Ä±YŠ“Ôc=´Fƒ‡FƒÙMéd{#-:q¦¹2y@#?¿³w(~£9Û‡Ïú ÑÇüÖ”>¤:YPEÙŒFqº^˜—ÖC£Ž[f¿ôA¾žþŽ1àK}I#‚'†+ŽUÇAØÅ~3ð^äq D½ÚzÖÆÈƒ \Æ+þц¦7ÃQGúP&嬰È„Hœ¦UP;×õFƺZ—žô|˜QÛ´š9¶ß¢i&±¡ÓmØ;@ÑŸô"…:¼óò°u¬@”QF§, ½¼Ôþ„ÊÒG¡ŽëZ}Ø>Ø2.1‘¥"D!p?GE@…·kÁ_Þ—<(â C163<¡ N-ä$èï3¹9„h\i«8 óg3C¥¦ãE÷¹ÿUˆ¬OSžRmÞZåàN+ÍLÄ1ê¸>\F mÄA~kÚ±ò°HôKä𠺬vcò°ÐÝúH Ö±š/ ÔAô‘O¬cìÈ#}ˆ„>Ð{oC¼Ñ>ÙHêû€%)bkB±{Î ®¯ˆãhÆ +¤íÃûdªŒMÎdmò† —ö:ÿV<ÓOâ——.\QÜqnÖzü¥ +I«öŸ-äaó8¿ø}ü¥d¯6F †sVSݤ‡¢ /Â.ü¼HÎ;€cÐ&^°õñ…ýc²ú˜–» Ž•D#qLêb$T…ëÅëÌH©q£[—ûyÓ™ÐyzÑØË7f0äÆ?ê ò°}È‘ÒÆ¨Cž“‡].äþ4ßä?Ô^s'<äa"3—)W#«YÕâ—<Æo± "fáM&šB»$þ¼ï8”•÷A¦ ·mTQ½`úHU¤‹ +¸Œ—©' +1¤Žª­¯À½ŠÇˆãþx1ó¥eôA ŒaÇ`±IÔAâFô!¦Á>HÀ -ró`{.ö'þŸ•gRÇBSÀçF ”èÛiúðÛ®dûCdħ0í›J G—@„“ê4Ò"7ïæ<¬›Ž½ô¡,óárëüT ©ƒßÔáH"ò‡"Â5(#‰Õ~Š +¬:Ó|/-Mâ¡Ô1ú¸âÍì»G‘@Y<úÙ +yô‹9¿t¡btqpz.׺±»Ó}QÙlÒüÉ–MécÜðÇ}ŠàPH1µ Žä!Â8Ô!+2ó>`ÎèUwÁ\¾I\Ò€‹/È+ {GÇ …¢IžnЖj¯?)áŽ74—º D§óŒé!7G¯QÇèCéÛ%wÕ!Ý!ù1ýKáĵm(}½™•@¨C±_ ÖUæA(D†8Èç ˆ`¡\F!*uqœ.¤æÀŒc•†Á^vWˆ#¡SûÀ· Ò¨¤ åÈ/Å#Dr±‚˜mÜ=˜3o}­fnñŒ§Ô‹f¢…JÜä~zn>Zâhû°ß—·•‡LJ§7½Ì~òá’.(aš7t%¦uß@/}ü+yŒ>lÔáÛGaÍïX¶ÖÞú)¡¿< 1]„¢.zeš÷[ÎÞ±íÍþ.7 +É™¡o^‚³ÞAéC$HãúnZ|Ô%ƒzYì>ãp¦htÎj5=³¹×7ø6\®Øjæ)'ʹ}`äÙܽƒ>®ÝC!Uuh¨BÕn÷w§†ÔA!ð”í#˜ŒàÈnôáÏsíVNóÄïsð+q<ú !?܈ÒFèwíIBi‘›÷kØ¢^°4g7è#¸CACÞr™:ècx}¸øÙBpè£ Æfp¹O†Ó@_¹ð´c¹úì‹^<é]{mXy¤•‡LJèèƒ:V¥{OdÛþ]5ÒQF%ÜMN¹õa[2¿UÚ@¬iåô1xo@4F„RG#êDUxxÂMʹy¼Õ†ZËÞ…áM⨮@x:.—<¨C!êð 9¹œ,Ššc0ø}Ãê6opó>£*¡SÔ®ñ…Û=C¿“šù£ m<Õ‘>D>¥•<D’G³y€ PGÑpH +}(äAäE é#…˜>…XŽ@¬ÎŽqé7ý§ Ó‡¸ˆPŒ<Hû0yt$ŽEÈE>äDV¶{0‰5ꕌçÁ@ Ô¡ØÁUo‡àø|ºŒ4ˆã׿*.òXEÉü´ÐoY~+ž½ù‡¹*çáO§\ )O`¤Aá,[v| +¡ØýcÔA·>ä½ôÓÁ‹£áŒÝÔͪÇÒ‡‰L7ò Ž‡>ÐÇËø +‚!0ý_oSGP‡(Ò‡‹²€ {/›íûU`ç6,>¸BÔê_ƒã ŽAX¼wH# +e^™ØÝ<ǬêÝ×íÃ>|t€sù‰×<ᩈô‘@úrúç?y{’ÉSˆµ,I¬»>ÄBÓê3¡‹Q‡â,Ò‚>E½–wsž°˜0Tp>ÈÖÈ5êÞ.£ú(¢qB+ŽmËè"|aTk·¿ºÁëîîçf¨¼ÅÍ>WÒÆªC–æŸ&Ä|ºÈßdå‘:’‡\‡¼ìÊè#¬DF #¿ü°.ØÎôñZÚ>eö§>ŠdXAž0sõ‰¬¼'X´°“Á‡õ€/àç¸|å0·¹î Ú@Aic!‡B£Ži0!”Ö'ó^t×bÛÐÖ(UßÒ³uÐFxÛéÓqP‡ Ž:FòJÿ6ò˜”Ë>F ßÄ *Ü +χBÉã/æ%ú VMôÁ ߨ!äÑ?”<ĪA’8”dûô•ƒ|8ÞLKÇF&ßpáèƒ@\ |O } £¡ÑYRÆT© Á+€ª¤jŸœóéÜz$=»8gÓ¹SJ¤ÄA![·>Dé#H,}@ªe|7þþ÷ÿTª†‹ƒ§‡ÒæÑöA VyŒ>Ø“í<æ;y@Q¤Ô2]EâP–r««7šaìën®Q—ë ;ÕƒA +|ºÈ™¼%ÔAÄñ‰:”ÕG* …‹—:/Ž>Üî1›Á4¦3­é­BâùÀU^ûöAŠÈ€Dà +£`®8ÐB‹þEBdåýÀ¶©Á`fì?ßNÁµ:ú4mÿ8P@\ÄF|Ècô‚6ˆÞ+µz¸r\²](Ð5#ýM/Þx&$‡:ú°Ä1ÿ2ÃÊãèCrý ⋦lC꥟ HáðŸqÊñ:}TG—>ÌfRò°}Ø>Ò3ÄZñÇ9QH3ú A£%y(Gb-êaH!-ï¶°¬L¿¶©áÅÇÚ@T}ðYå<ˆEˆ +„”1ˆ˜¨¡è á[Äõ-®=uÔèb_wŸ’“4`ï ðÖ~©£*y@–G#PÂ…ÍCÁL絹ɭ$å1â24¡ß¹Y"u¤ÓXÙ†Éê ꀉ“ˆ<%Ž@A?Ìp"-ïæ<ÉbuàW‚g 7¹Ê]âèŽ@HäÈÃî±Û‡Ëu —ùpDÑ.»{ dCn÷”gûw¸bÞˆ©#}°„i|Dúu P‰™È: +hò(Ââìí(ôºéaG—’ÁÉ‹†±±`¶rëƒWä"mȃ>u¤q¡Žä1û‡7“€Q†²ÛM¸@¤øÅóÌ}¯®¹H±ÍtG²$U±.ÄòXÈ+ÈC¦AA‡ò¨Ú¤¡t«‚@ì¦2á­ˆ¥‹˜ÊU.û€™—²úh!"7al!†ÐãNƒ$¨KÉ‘¡–L8&éü‚ÀÉão +h#x>¤h¨ÔA‰CÑ1E•å…SŽÁUkà ó²ÎÙ'ÈD¹psÄÈü\[²RÇõÕTGgÿ ŽUÇßü9*x‘(þº w]òððêïÙLlþù‡ƒ,iiV€¹éƒÓ0 +!êðñÒþ1ˆ\J„xøBï8”Ý E?(LèƒåØfp›Íƒ…Cô‘sÁÕçG»}$¡I"«t±žCÊ HôW.ž¯Þ—%B*ÔCÒ  êHÔñg™“=›z³{Ë}ôAáóÂAGô@¸º®Luƒû<±ê}4•)Ílkч „Lâ7éƒëb bBámtöB(’«ŽWœ åbÓR+E?(,`Å($y,bùÈþüò®z½zá•)sää|+…$:èıêIowHìè#äœ:žPhBzWÃÄä¡‘ˆI̵ú ŽôA•ä9kéc¿ÞL +”vô!V+TdiD”ÕÕÅÕ£Œ¨÷÷fÐó¥›mƒå\ ©äqÂYðú¥Áð–I £ï"âaˆH¢X$·"ÀSg¸7 ÿòÑFÕö>FÇÈãÒ‡t†ï£©&Žhw€o£Ä@øJùª“ʾL ð y\?Þ}´ôäÒÇüŸ³yiὈ…œ dvÈH„<ЍýC€/ +¼z8Ã2Sz~X²ã²HÏØ%Æ~PGðn|ä-ø H!âC7Dåû‡ˆ-¢wt¡Û¼¿å÷i¢Jšä0ȃZòRr’iÀW#âyˆœÊ+}øx)Û!ó7”±ÐÇÁÕƒ{éã?F·>|³±u€4™ÀVæ‡û€ƒÝ>ú{@D ;aÄDVoD¾èÃÓ”™•‡‘ö Ìa¢1wàÂÀ.{ÁÕÔ¡ä}ïÁŽ×òÜ>jï®>ª|Š“·~”„¨—’H´ÑQGâøò_éûœ>iM}ûíAêí ›G]¸¾¸•>‚¾Ì2ú8û‡“dC{$‡@:¼uÈ"&rä1¡ŠÇÕ‹þ'\ª02Ôü0#æ—¨ÁÌ0^p‰[ü[x; +éóE¡ùïý˨#DÊv«â·¢,S-õ§U·ÝÚEUqB¢0Ò:9HƒB‚6F³yÈ!u(Uê ŽoÊ#q(¿ôa4¯Ž@l¤e1‰Ïþ1ú`‹˜ ‚×£ïï˜ä!:½“DÜF×ÿ×ô‹ŸN”5‡b¿\ý…$M¢¤ë³cYŠP·ggÔS8x3âbÝ“:f„ALDfý¡Úm Ž.(C­ÍE`E7ôÊ[ºa‘yp,£Ñ:æ—¦ççÚ䱑̾yôÝ#’‡?PÂÅW×öQ /§Žž¡Èèƒ@Ú>ú†Ú ± y\áÿê£Cpèv)D0…uãê„[™F̦¡?£€Y:¶1“Ý뇖4éó0G“‡š AG EI¨DlvÛ‡‚ô±ˆj•QžŠºêòHHÉ2#âHÄAÞ×¾=ÇüMnõ!­r+ÅtÔT€6ŒÙ<Ô^}x£Ž£çç +.}pŽŸ|öîð)K¶$’B `"êÀW1è¶~±ßzØü ’õƒ`éµhš}°ý'ㆆKä‘>V}ÆPÇoy‘Â2úèX}„O“‡`NÕ>çi”áô %B ãÕÆªCž¤”•7<Ê¥„&Å7ÂP$Üá» .]|å¨ îˆy€<èG³ø*XÉÎÙ*G"à‡9ˆŠðˆ‘8õ·<ï± ¦°×b¼è ºÐ/ÓojBÉúìXc@csZ1ù—„ô¡´}D»çC «Ž(PòV"ä¡ÜìÈ ú \,öjL"@¹q@¢è#…yôs-¤SJ©£ÿî-&Ùrî@¸xI£k´¡½â9ú0Q˜9Xͪ–§Tf‚_äqïAôAB"&n䱈¨˜ª „4†MÂâ7ÚQ.ÂY‰™S9ûÌX”S§a)¾è¹$®Ž>ü†}…äq d¾¢®:^ú>!GºXU`·¦0Kÿô5ŠÆ9eH‚Ú±ò–­á\¶ÇÊcôá{i%yàõ»H{òXR…–-s¶[àîÔaû0ɨŽ>^ÿ|!ØÄPÞ/Éãˆ@Ê|¡Á6±#…@äöAô…(-N¶»‘®ÏŒ%™héαæƒ+Üáy ä‘@ÀyÄÊC`èã@Jh©ãhC8_Ó¿íê¨àÆ+Ó®"¦9-¼qÃH’d +R†ý×°åpôI%™íc¥qıíˆA»Ge°‡,J 6S¥óûx±¢¥³€…Ìð»+" Â"8ä1Áò¦¢l,7®EX¨ß GÜŸV®ªŸ‹.cÁ6«UÈÀŸ£.zŒ>”£þû…Ô¡ + ÚADªB!z¾B$QLiBM#b¬¾e.ˆ¿$ g )SvBžˆcäá{ÁÙ?B6!­icÕÑ'ËJDþo-|uhàôºì®ÔQ¥óP‡bú7ú`"Ósf|ã-¨ƒ>æÈê£Ï—4räAˉ®0ð(òRps\yÒKÚg¤/Æö±lf;xÂqéƒBÀyØD‘:ècâ-Áò–jû°ùžL¼.Aæ> 8+k•`ÍH//}¸ËëˆwIuÄu¤ªXæ‘ 1>q¥ª¨rÐûTíÐK mÔB¢¨#y¤ŽKr)¡²º»m„l‡Ühâf7Qà¾6O¥iB±F;ˆ…-Î&°’öÔÁWðœ>¼S@ +‘@QD¯0ÇKªs* +$æb.t~ª¬}6¬ø`,a$cUoHðïú¢`ü§Žä!("säA $B$£e"˜Óë Â\Ô—Fdñ¢qêÚPµ —>$ñ¯½þê’:Ú:BÆiCIu_Åè¢Æ©k1½›&Áìb)KZŸ)Ìc1}pèRHÇ|À‡Ð„( X<ò@lb=Lì7 ›™>HÊçÂRÇfXxÛËtúP‚c9®’p^F £ë%Kˆ6MÌ@è/äA.$CCÄÑ_ãˆ#e(Uâ@òpôvȃ@Bª©_è_«5]qùà{lâ ®£ûíH?À´` "$o#±±Ä¡Ü€Š-„9ôJÈ€4ÜìI×”Sˆ¤}6,زl2ˆz6"Ë™?är4rv…ÿ#~Á..Ô‘ •ò¹â@ÿ… ÐÆC³_8.¾6vÜ’'pÒŒGôHÿB¤ÌÉ\.${ÿÈsH£?ß>Bà †Å1Db4|_•†Yø&½2HÜg"9ÂÂJèXˆí˜OXyP‡ÂMÞRÈÑÈêÂ"4äQ”F!´Ñ+‚âxq~~é¿ÓÒú:ª«u\ÈHe"Æ4ÔA +u@*“‡RrIcR-é †'¶ôrâóg!+Ì›>ú~jQ{2’Ñ6Îä?gÿà¿$¢B"$ê·Â'Œ‹ VFØÁübA$âs´?>û<ik½a YHz=Fõ|ä'u¬<Ú=èc~IFC!:òx t!–úA|‡ µˆ'-yT‡ÍÆ´—8$I¦|¶HØÊC•CGÚXq ©cå±H ÃŽªµ½p Žd5Û‡Ù(ÎÄb›²*جM¹Ã;n®@èƒBVq/}ÆE\!Ð*´z2„ƒS¼šSˆ¤i¾,ö†5€…`¬ÊüŸÖèÀ3î¯H!ܧï” ŽAŒV éCè„o?]Š%•8„VC!ì)cš“]úê€4ÈÞÛô!r9)•XúP¤z´Qú4@ øúô_}ýµfèu÷Ñ’gÄó&jû0±¯6T˜>Ú?… Ï q@¼úˆ¡Žä±BÁ$©b+Ö…ZÄoäA&Ôa2uŸÁùûþ±TXzLZáª,D&S?Ú씫Nƒÿ¶öAhè#…ˆU…Œ>#*!ê}@œouܼ†²±PdH–dêVGúB›GHÃæ1ú€´T¾A_AzÅ‘8þÓsžÄ2—>Rìêƒ<ùù»þ£œ|¿ÿÆO!Âq"_¼ð.;І§áŽB>”…Å@ WQÒ¦~¬3‹½Èö-Ìå@ŒDøÅ½§”ÑGQi!¡J£L©d)ªêÈ£(kQÈþ‰LHljÜ@’B²d,ˆc"“òYRûd±8J¶”F嫨vù:eL­qGxÄ£0y˜·`­…dê[}ðÔ›á|¼ˆƒ’>„&¨ƒ>ñ[m¼ b¬)ØêA~üc¹Á4%¥V+’÷Y°’î|Leypcä1QÏ‚Ù>ÒF¢4±Aü.ˆC@…UpQŒc”1 þ”.ª€„¨´AU´é8d‘0”£Èrâ8ꈯ¾A² ‹éºÉÍ=˜‹>’G±’U}¼1†eŒÌö‘ÝóŒD¸|+„@îq*Z‚B8"9UtÂ~ˆ¡„ÍÄÐÐ1”ÉÜçÁjØe¯ž-¬Qƒåóñ—ä1âˆ\Îyú gB"#ù‚ +AÉ…@F"ÅÞŒ!ì˜ðkÒ˜J+Ž[¤¥oô1òuL#Ë¥{)û4ð¢³t¡~­Âá1zÞ\fµ4ÿĺŒ` ™Û7¨²ƒá)y\ú ‘G±}W@ï2¡¼Ý  ¡vTâ¤bù E<(mêgÃ’o`Ü ›É#}äÏôG#|N!Ôá'\Þ8"ꀘ‰›è‰Ÿ(®>Hƒ8:BœµA(²P+S¦Jær!g2Gr(“:ÂXä¹½`RC ¡ú/(šDÔôáH+¡õþÂV1kôÁfú ;ÿÀcúð µ0ˆ†7è@˜D+}¬B@WÁíŸöê˜~¡ +Mœn‘¯Ï§ëE¨þ`ˆÈLÒ&¼ ú¼"òÐòvô¡@(¤¸„@Fô¡`åqB"qtA¿ƒ’ ]uHi(YV¥ÓÎqIDŽK6aDýWo½¸ÁM‚çG äAÊêcö/ùH œã'Ž@ñ®‰ø¦Bâ1-ªƒ O¤CÐcÓ§«W2÷y°”å+ƒy7,^Üð?ÿw+ôaÿ€UT1*PÂ%dÄ¡ B´A +}¼ð™$éäâæKâi‚­C3x{Ëâ|¼Hjø½úßR‡*ãAŒ$ögÙá%ŒtQ M“<Gò°œÅéƒYld+}ôã«6}PÈïúJ>F!ä1;Hõè£7•ð ÞÑÇAŒˆú…d|7ܳm]øLXj­X[oyòŒ{|%Å›c°}„ÀéÈÙA.}ˆ#uÐìÍôq D¸iâ¿4’ ÒˆÙ<¨cõ![R†~|9úã: å¤p0¾ÕÑÈK½®®><>ò8!‘V…0…>ì ¦~ô óœ$^_úH £…> j‰#„Q81~!ôýgFÔm2÷y°TË©7,£ UÌ?Ì€oà(¸üF¢B "T+ +D:´¡ lÀ{oÆðÜ>¤&V²IƒÜ­BˆãÁ‘Ç-40õ›¸êåèóˆ:”H0û¬”<B² „­LæOF <yxW8¨ãÈã%‡BV"éC;ˆòʼn<6"%ŠzجIÞ÷UfqÇf DÎpëg?å!/í–ÔAŠ—K!qVP‡àq BJ«Q.Òu\ò ÇQˆÝcB©Ã›ùBò¼¿¥qô1e²KsHlûÀÎ1ÝãNÇa7Z…¬<èƒ@RãÊ\ÆÓ…¤òèãeཤòm#â8ò>AtP}ˆ²7bˆzèebø$;«m¿o¬òd’b³’Å™¯NÃ3ŸŸðœ§þwA)DX„ÆþAÊÈc"ŒÄ±êHbK!CñV |ú( ²  -vó òðÁBÒFR(“ÒYVeW†eå;y _«ÛèÎ5¯ßxÌÓ&‚™ÍSØ’qlãyÀ^½6»¦Í3ă:B€‚FÄŒ::ˆc•1­8‹õD\àe`‘‰O¢-ŸC $2èÞ²QGí΃ԡ郷ÁuA úH!#‘-â€ð‰á”UHâ`!Ž‘G½ðKAyPâÀêƒ&9+qô‘BÒ‡œRHû‡—éÔ1ò¸ÄñmÌËè^m˜G ÁÈK#äA\â'9Ëc~ó_ ÄBHFxŠÑ%ˆß@"G$ãðŠ&q¼’²£i%«*yß;VŠYvš!Ã.˜|@.!÷ò•8x®Œ@H$Dg)u,âò ]!ŽK d‘©II‰ƒ,ž¬:’‡\Ò† n ˜„—ù#íl÷wÓÝ=4îè®Á³=í uÇ¥ù *ØOè „sÂáà;yØH#}ø!F„|À(‚"(˜J[µ7Å\äɨÀX£^Èß÷NBŒçºÎ‚}Êd,Ö‰‘ï£ôîx»±¡åÒÇa·Ñ‡À¦ 1 -A‹÷«œHŒì„–a?_IL]uŒ>J³*ÝR.÷_;nÎ2VÚ"Ô1ú £NL3œ ó‡@xËñýR(VA ÞHÂu)‚)¢bªBŒ/Ä>¦-5JœîäLö¾w¬b¥]XÓIõ268€/¸…ä¼M ³üÒwYyüÚÛhõñú ˆ0þóï…3öHû‡ÚHÓŒ8*B +}\Ø;Fe0yØ?”EŽQº+ õkÇòyÅk˜VãÁÔa.úKX‰"­ÏeSÙ}É#ÿçVs‚@ÕÞ6ÂC d"&p+ÂØB‰C×@Ô¡S$E¹yåNþ¾g¬³z@Kßæ0ìæˆÂ§HJŸ0y=úˆ•ȧy„p†ÐÈB"ÞwåË?ˤ‚6Ž:i¢Ò•8$Ï¡õN—Ð>_dw‘mÉVA‡—,ž¸Á]î›ûûpY‰¬B,d=‹ƒ<”Qg=þøG.ñË»À{#8…ÐÈ%…<Äo  ™N€c¢]ÔMHÈÍì'‹~ÏXäâ^u`Ôk‡äÎGýç¦8.!":ÄQéí4A£‡@¼íÄBì­H!îç»é ÒÞ»)CY¤ R×GÀµP‡:¹”±ÀaõñŸÛ³ÑFMuö™fbn뤋“‡’<ØÈZ¦'ß?À­ôþŽ@H$}ø)&l±#Ÿ0bÑó&«†rSœ‡âž>¤Bn:deÊi>ƒ<èãG–±+®ßá²§f`_VV¨cËêƒoý’=ð:xŸTÄ#‘ò1ƒý÷d*²aQlÚØr«#h#dEvd¨4©ô¡Pi(Î(µê¥—þ)iá믾® œxqÕôÕWoéq³˜‘<:¬cò,ØB³ „Å ÷I )„_ü[‰ÐÇd7‰…$*m€<@oµA5:þa³ÒÇv eÆ!ß7V±Žú) ËÂé³z*4œÿ¯6þs— +òøÕªC¥ÛÇo}0Ó†ò aZâPÎþ!æäA /‰ÈäEnÈâBʆùtI’I³xûCšUYÇW/vçøº¶F ¢ ’ÅsfQÌJ ÷þ¡°RS‡ýãü‹Ú\à 8E vIúà/øáoeäA #¡ +%eàt)cùCÂP.fø£ŽÏƒ•Ìêc˶^p $ñSFŽjg‡E`„‡D T}ÀxoÙBèƒ8pÉcqtx²¸ Ô±ÐEµLÉ˜Ì ’+Œòª#˜”oó†N¿þ:q(pà ›'ú†úˆUÉc`Xv’3ÓId¼áÁÕÞéCd$â­‚DÕÂ6$ÊPH£ÿ¦²vÁ¿”áü +SýñçÒ‡kvX„é˜ÉÔóyÂ'ò°…ÈÊã܇XŒ< >«Žù„:1üƒ_ö¿óÈÙ?Îîq#ðž  5q,Rwô!›þf+¯ÛFÈ´l`¤0Ír þóïxÖDËèä1ú0ÌÂÒѸG1I‘[!öYXa#AŒ>A f¿øqÄõÑòãR÷Y°,OŠŠLRÇÄLe/8Nåy€³ÒçË„A0ä±/Ø>F"‚9í%AÈ!ö>êK‰Znèƒ8Ji$Ž¥oÞé’R;ÈóÅlÕøš}×tá67î·eõ‘îLNauFŒ@Æ:–ÒÇŸç HúÈ/Ûã­ßýŽï†{ QF0[Äñ¨cºb¬BÔkŠþfãä©Á™Ô},whí*ƒªCV^´ æ §èƒ8”à4÷!"}œ/ û+TA¬& e Ž‘‡h@„~ Ž’Anh££B +qÐFÈ¥„’Lj½C®o(àá kîq_òð¤IÌ3[„@l `F=õœàÍ(„<à@³ŒV $é#…$‘~QF$+ÙŠìÑì¿#îƒTT.v3‘ºÏ‚¥,¦¾± 2ldóÂrJç çÀGú8 +é;ê'ú Ôqé#yTéc´Qž¿ÝŠ8²0H‰ÍCr$(ˆcÔ1ÐqP‡©EÚ¸~™6UÜ$¯¿žî žê¡0ƒ©ÂäÒbÖ´4Ò‡­ mÏcý +äþ™_£’‡ÂmÞ Â`¹B‚%bAÉãg?ÝŸtwÙ^رñ—ŒIÑ©û HçSoùŠV_ãÒF—·,=k†£›Ý +éÃÂÔAªÌb dõ‘'m é#…Ø>Ç-9ú ¡‚  "H !²\ˆicóêP>®4m/sŸk}ö(mc#yîplá$¸ HÞ0É#¨ƒ8 rô!…Ì' uTGmÛBO ‹ŒÈ‰Ì$Œ(âPdnHÚù<ØùßË4Q¨ Ž20£ièÄ=šén}œ‰“ÇßlX–WR«Øìe6¸1¡oNR‡2ÿÂeú˜f"21(£ÔqÉ‚»l/è˜'F"~¼u›øQÇçÀZ8ëŽ5Ɗʾ‹ìm íœGG¹Mä¡ m(ô±œðí§‹x’ÇA˜éc2òƒA:Þª£Ó…@´åm‘EÚ‰•^Y~0y§‚áëQÄ}zFÝ5HŠç³<è#¨rL`Ï(„D˜š<Ò… G!1úày +u¬>ðK"&úè8¹%R`{ÿÑÆD[ÑPˆDHGÙÐîx;ùû¾9:´Þsm6 ¬SªŒeÈê#…Œ2 +éÍA!&B ¶)hÁH‡w +±@SÈî¢ïðƒ‚\ÐGEj G2UÜz§§ ß+GõFÊ +–çxØ{Í<&$[ ÔAÿÎêèW¨ +KW!ÙOÉ#x8…>V!"à£6‘ˆ‰Ý#Š6Ò…Œ8ªH!Æ"#%±-&c÷}c‘)S¯Õ·g]\5i¨Zð‡cÔÁGަp_ F‚":³ƒtŒ>ˆ0ŠçÈCYqP} ÞŸƒ\ǪC‚èB¹‘»A&™ 9þ„¯@_}­Ç|©º´Å=K«£‘‡uÈCK’}tÐò—ÿÂlp/ôN"¼ËGúPÒÿ è#DG„DI¨ÄŒ:–[!‹_Läé˜ò ü¾±Šux®Ï&¬L]˜œAž‘ÇdÂ{a¸ÿ“Sä1A1»•‡‚"{"Ü/}Te#y¤‡:J•Œ ²W +%²|Êê&WšYWT--|µ’8|ýõ9uÇ¡¡ÇÃt07,„‘Ç„>|Â(šág¸µ!¾òx""¢j“‡xAð†¢(œéCå'þ%CJ”.Uþ¾wZ§•4;4|±-C/x#öÃü:úðºòØ ØB +MêX}Œ<èƒBÈ#ˆ+Ä8y$/¾èGÿUu"‘™!êP_H^9”Iu„Š[ å\{a”*¾ÖœSÊ‹nÀvM`2³‘‡É?¾Œ<Â'“ê`mò¸’BèãÈèƒBøOJïAu¤U¾Êèƒ87~¢+âBÆÒñ)2&{Ÿ«Y1ž†0 Ìt\ð€©ƒsUîñ•DÒ…„`ËÑÇDÐÒ‡ &ŽÖ%¹ÍC9H…|P‡7¯ì ek!˜\Ê(N~ÆÉ7Ò€† >e.{ÓÕ˜e  “šþ^ÏÚTšB²+ ¯@ü–ìÀ[ }ª&êxm!x}¾$Bm‡?Óцºy P¥E¹i,c2÷yh±©Kæh–1/SÁvNð"} ︙:Vm óv),/}yø ]Ìþ‘>¼ýüì’>ì ä2” ù±…\U÷¯.˜6”R+¿s|ÂWq6ŒÅ^´^}‹YLwhöÝ@n}d}()9‰0¼è£’>þH²! ô¡þê×>xG bEÞWEŽ8ЮâíçP;„~Ñ`’¢ž¶¬}6Ž8fᚬªÆ8Æ®Õ|qŒ:T®‰xg +Æl )„<"}´FUP!©~ô! Š÷+^»‰Ø7dKÖ"‰ ’º©¥Ž7úPDzXæìൿEÕÎ,gOJ ŠÅ†#`à¨#}$ÔAAúäA ˜ï œHï‘(e(ÁLUñ†:}(C¹™t’öùØÕ, k+ªã•ÌOFp…:Èãgy<¦ûÇ›W›>„I°ÔáicÔч}Úæ‘>D>$!dDZèâBšnuŒ8¦yéC'ÉoH´ðõýÌàR¯LñÈüDêHUꨃ­Ú§ ÎvYÉ`vÓvò¯xÆ;o†K!bo!o¡$Òö‘@@üú#qŒ<äl«RÙdfÒ#K ŽÑ‡ô…„ª‡ùë«_ìôñÿ1w§‹­,ǵ­¯-Élõ¶lËÍ•ßÿ%Ï7"2 ×>ç'¹gef@ 2bƨ,\MBG}è>ÂöIÊA.[l´W$MµHáÔP«WSÔøƒ‚„‰—PâóHÚqôWow‚Nå¬;˜edRs ‚F§€EÞ-å2?!€ÀC“7B¡»Z¸âºIVÄ5Öñ0qsÐHsÀt~³ †v¯õ ‡Êö…jÆ3õîe‹ Ãßnt¬ä#-Úú…µ„B `DbM —ÂÃò± È ñÁo¦Ä#.– £Vµt„Œì´æ±õUë­û CÆŽÿ +;lÔ<;·–Ô ¼så\äÜ4ó˜1Ň Š,>H´¢;BJEÇB\ø€G|È>BÆ +†¸nþ Ä+–ua­Z‹_€³ß íTÃvä 8´¯”‰iþ,Ù‘¸¨'áZ÷lä·Ò}æ–8 Bò=€¸JþÙ +’)®|X@Và 6†Wø †3žÿUø8„Ô­uEÓ"ƒª§¦¼ +­ÿÏû<ÃQ`ÔÉÓ¾äv=!£ÓÙF¦5¯„“ "‘õãÏ>=IB*Ôbq <|,!L¨a#D84x CcÞ‹G;ß官žñ:@ÞëbCÈwðq©“¸&¼aXÀ!dÈã. + ï fF Œ>Äub#;]sÌÇÒ‰|¸HGŠ’Z>q–¾¹©¢®¤+~WEo•ÕãƒG‚R¯퓌:m2ߪÉW±ÄI¢.x„Èf$3„Ì +’€;xBÈÂ3Æ ¼ä'qvÔuÈm¾k + 3Øé#S´/”9Ó°ÃÕCÄ‚B¤”vñX>ÂCúƒG†wÐqùˆ3[>ð1tàãÂûD=‚c„ðX)UD¥LJk«M©µtv¨ 70’§GÏAr&ê¬G¦ +>’ØÄ·|HxKëA"1|h2•1@>ð"@XÓb×ò1„0“£ÙÊÝ4^70~~Œ½%ù 5ûJ™Ï¤/Í#Ñ=0 ~øÐduù°X†‡%ÄòÁ…Å,òùôââbáàÁTB‡–ø¸¯*1|$õ‰Ž³vÒ©â)z²GÄè¡ÄÓt¾³õõŽmGΩ™õDLTt„KÞòq‘Ó"r ÁGt0„)ŒaÏøôàÁBth+öRhŒvùX>´—”GѾPæL„€H\4 +ÑQò°$_j2Òµ5„ð’©Ý_r/>4u¸t>”I¹>˜’:NKÕW‘Zx ‡ GŸ^Û£èÐVÎN¦"SÓYÈ„•&F±µèe!õ"RDI ˆì`J|°ˆU#έƒ¬d©‹Ž¹‰Ñã7±žTC=ô«®ç/ÕÌ9²[^5tˆQ¨´±K%ÉJnI®$ñèÀ?ˆ5ððíío™Å±]@8ù§?²”¯Ü…G€ðúÐAŠ@ØPå #2TLéFʨ–#åMúÑÛƒ¿bb4»ÿ}~R×ôÎAv#gß¹ÈôñQ$‡ñ¥p- !tW !"e|øö-˜âD²Ša‡6×CÙ;sšfÇyøå°ÑÿMõR´/TXŒf¿ßÇ,àt=Ê¡.|´ÁC[>VÀƒ8âûW`СÑ.«4Ë·" Ñá“© +é§dvdé0Aa\™§þú¦ÿµ‘ç|<õ.ý*Æf£Nm[2yË…Btƒ mY=xÈ. ¹`lñÛ\êcZˆdÛŃ›Ùª§|^¹.9¯« +öµ2§yw¸’ØÄ'Ò4t<ßàÆG©J:õ)Œ6®ÀÃú~áçç§å+6V¼FßùŸeQš¤P§^j‡‰j¸Õ>†4Çäñ_‡ŠÅᧆ79S58±)F¦³vÄ,eÑAð¨÷Ï*ãƒ@ ü“4#DÊñ!uW DváÒòAqsiÓ²–¸Ììó+@RÑßþmph_)ó¾ôKÇ)T€HŸ ¼°¢Ú–„dY|¹‰Ž84Wd¾ÛÀ‘P1r+0 ¡©¡rNA—‹7Í€0¢Bû¬ýÒêuèL«Nÿ?3—iÿO*6=FÀÍú' é CHë¡íwóLj%œé¯L ˇ«ëB|}¿­¼¿R}¤`Šö•jRíüMO\IX#‘Z>òM9Y%(Q’2>výhù€w’¬ø¸€psÖðHáèPrÁ‚ã‘B‘’¬/©¬ê^©>=ì—<ü OÍ“çË©#ç†a¦ S’é­ #±‰/ŠÅÓ"½DäsAˆ<] ­äH¿«>mÔ ðX@˜ÊÝGðX:¦jñÔD¡þF©¾Vð0ï ´¡4ŠÄ:š¸%2’<$i“¬œ]ÔÒúð-1‘›\…@\‰¼N̯*U¹R%R±‘"êSMÝöI[ÿUÇHHÿ«„Øj­!¦%!ÃHá¨ë‡f ÁÇüÛByˆœç³+Hv,Ä(‚G|>V]|Û9Íîd¤ªAg§d_-³Ž +&µs -‰;‰?Ø ¥(ÏDîá±€p n0¿å”ë‰kKÇÃGx°øà1| 3ƒú„ªHðP.uS8])«e]acàÑ4%o‰ÙF>©'êN匫ácñ°€hCÇÈêA¢µ~D"™õCH’¯. ñ1„€ƒƸÄFf†GÎŽ¸ëÀ¨+„RÔ\öï¹Gû Œpê$Fñ‚"š$dr ’0I~a†ë…;èètàƒua"7çþøà4ÃÇø[õ‰•R®£©M9vª[OžÐ^õ§×'Ïyò£œjÆ9#™Ã<9³‹£€Ä…Üøx- ÉEN¹|ˆ¼eßEâJ@˜Ãž½ÁðŒÐÁ¬ԗÄd†§¼?RŒd¯Rªõõ2û¢aÕ°‘°Hœ$æiý9TtHùòëþBrˆ Äu €té°)8à‘–ŽÁ]Šü&x ª¡z„hðP,R7åSBdØ”4©·6}ô¤,%Á¡Ñü£R?ª3N7™ÈÌ$ ÑKp"<| ÄGdèø—ýC©r„ÇpÔá¡q†?‰U9Æ<²~`cLuíé:<,àJ°x4Ôÿæ[ø0- ÈøHdñ†‡& y€^rðHòEãâ|¯Â#@8ÈÊÁcq ·‡ŽåC5VŠs¤X¤v4;uœ’>ªô=ÜîŒÉ=täØöÒœçʉ“©ÀQGG[¡cYF„"rDZBö§d YHáã!„k»~¬Jœ=bw€¼T1tlhß!S6ŠäH`$Ô ØîW‘ ÉJ~Kˆ+ ²ÿ >Â/„<|D8Œf:õ·*Õ`™Ò©:©Ö€A•[ØG- ï¥O`ðTÝágyÆ[ÞäÄÉTØhK‰¬:tˆœ. È2€¸&:fÉöÐàá‚b÷Ü`tŠ .œfüÊ‘JL;R®/WÓÃ+ + +Nxâ==¡#<ˇŸ¯ó¢õ„má7˜b1¯#¹¯ +jAÊ2Úa´km]Õ÷¶ÕÝ¿Ýcã¶£9ÍKÎLÍó&„ˆeð@H¼$rÑã#@$(ÏAˆÌ™š¤«'<†Ä=âcâ)@VÖ¶k;(„jœonI¹¾Zf—6¢¢ ‹Gš„Hvˇ6€ŒX —>†Ëo„@˜Ëcj`;)A|hðxI‘Tê¡£ºj£ ÆÜTÒìî³ôzá»zvŸ™7îÏCœ;- f5;B2Ë@´Ä(~YÀãíç Ò”¬œ‡„hÁAø@|x#?AÇò±€pœß¬¿RŠGª¤^ß!“Sÿ*êжCÇèˆ^‘ÓâQ¦òõÁü…‡ëì!N1lðˆ‰¿±1š+“ù?ð±u)Þ-bõ<º0¼žÚÇižÛê·µ›[ÎÕ¿ÎZB¦A(bÈÀaØ%¤Ðã#@däs7¹d +|D8q¡%„_‡ÄÉË{~ÛF}JUZP¾çûÓÅÇj‚ÓSQ‹]ô-$)¹µ|P|!¸|,!‡?ð-@´ÃÇx›ÇzëG¾+B CO®_:ÕR¹)¢AAOÕß+íP{×óðýe£—1ç¦04Û‘ H8Q+8€ì"v:Hb¥'Íå#<ƃå£îêa…@Ø·x¤è¨ç3³s~þ„qÁP¢öJõõ2k3Ÿ@F M¤ÇÈg'Š 68à!}|ظ b¯àᢚ›‹æRë§c¬]ñ™åÚò¡ +á±€ÄGªbê C£r>EßêÒ<ÛãºßÄA©£—<ôÄŽúy‚ÚwbÝTu‚§mEŒ5ñ®>JËOR)<†àA<Á‡_ã"„UB}<õ“«<w™­aBS¤ª£Û+”z}½Lnvz}mÊ@ È®î1äªxèX<†í¡ƒ–‡&»™s<㇠püH‡¢Ñü{As \¿úL A$=_ÙÃû`ö3-Çt³$R51 ºB·ðä B.|D84b„dqýœ„aÇ@&r2>øÚ¥Üæû›”cåJþ™ÚúÛ$ÐYBŠ˜À! I Ö‘ü  øˆåãõý kÑãw +HÕ*T×TLåhvй…í¨ã}´ã‘jJ^¦]=‡·Ö &|$Ôg8„|>€ CÉÎE UK¼ü/ž‹Ç¯ñp´|dÅ_|Œ¸ÏhÔ”ãh +õõ2³‰uÚhExèÐtÁ“<¤$³$Kxh—Ñð‘=è¸|p!)C—L6°{èèß“EEÆ­UUÓI)mO]?ÊÓÚ‘#¯>ò@ë®tžë,iG{šÁ<&D†NÂ>T¬ˆà}ŽÒ°A~Œúâ åß—Ë;à ý^ƇÏ+æç>>Þ¤FÊõ 2÷H4ZÞˆ ·#IØÊIn èÍò!ìˆÝ¿¾ýÃ…ÄIžrÖõÇc:x(:´-q©j#…TÌÑPrWG´ŸTÏÿx¿=œÜ© ËÇ ƒh%,#‚%wÅøˆ´“»K÷—ˆKCþ}D_@Ø!ø@H‚‡ #¼d(¥TÕ§¬?¼Ž>³ðÒ_÷sêÊ‹<š“7ÏiÐЯ„áQ7º„Š q%lÁO³~ Ú"oh ¼€G|B¸…?úÞŒtÏÎTâ°¥àÐI1B¤‚TåÑ8¦|ªX9ÃAKg7RkÃŒvíë£sà½ç(uX9‘ F&41‰d%²Z‘E/¹Øà¡#>"døàÃÐ1|X@Â+†ë¬ÂCHŒä,:M¬_©DÕx¤Z_/\½"› …z"Žlt”G€ÀcàHr•¸Û :<}™»‹„…ø Ž‚#ƒsšáà­ÂŒ¤8´u2(Új +ùÔ´ò¶µ§9ì%v?©Ž9Ñê>åΟ̶³âC‘ÄF‚]@HÇbýtàÃG/ $@ÜaС¹Žà û ÌD_¹›Ç¬æ;÷¯#Ù+ÔwÉÜï**¡ ²ðIѱ|¸Ç #<‚ €øBøˆ²~0na刱 VÁ£Œ¦"–ˆT¢¤fIß54ŒîþèÓÃ}¼·‡OóC¦ùAÈ‘@ô•I°BîÖ¸|\<×At¤ßî·¸¿íˆëÇUË:†pÔ!BY¯ã‘)Õ×ËÔ´p PG}z)´€à!å'Óýë·øX@À‘'ÃÇïÏ*~Á ÃÇ⚃‡‹ÐFñq9ˆLITçMª¦lÔ ’US_Dn¿ê+õá™÷—¾ÉyûŒš³a IèIç§diÒ"{pbn0œ F- éŸUú»\^x¥ oR®/W`>šxºï ­@ç #Á'‰È ³vŒ$+k€Ç?[L‚þ´| ãà >~$x°–‚õè˜:LEj«ê¤^*6u3(åÑûµµË„/Ÿx8´ÕFíu¯2~’Ów~šY“@Ú621 +U³è‘ø!"EG’©„å-{|Pë_¢ƒÅ/tX>úüÁËY@8Ûà2´ÁCU‹£ê¤Z_.S¯&œ#Ç Œ#±O$¥F’->¢#C‹·rìð±x\>ÖæEd¥ †£-–@áoý:öHWæ +퉾„‡‘ŸT/:òºyÇ‘)gÞAd$¼ðH‚né“Â:BÜFÿäÛy|Dâc>~ćvÁ‡åã Žñ±ž3Ÿû#€T–:F¾A¦]mÔ8'RÑ’Èm² 9Y?>òáû—!„ ¹|p è àˆbóàᦞTd¤4+Ū`Q‘¦ŒIMÔ|i¿9–‰9ôl{ðÍÕ³aãÒ¡'ñµÂ‰‡)ÈEJàÐâ#@–>¡¢cî/œ‡>Ÿ¶~àƒ¬K8ê¼fú#•˜r„†nü™=‰C‰,8ŽD=››KkáÁÃæ'¨ñ!å’ç+†ŸÛ@øÎÁƒ‹þW–.<æôâ¡‘(ER–ô_ó—«×ê”P-éì*ùŒwzpÙiG¯£^õ<š7ÜÓ_™U $ N á1|üG߃ °——+`èX>d¿€ÄÇBð`Ëâƒ.5vòtø nÓ\› +€ýéÇvß!Óš:-…MˆèÀÇ‘4J%‡$ßY?'øÁ“~Ì¢Ác~ÂÌ×Âbb8D’*Œ*GXíx4%¬¥G5j¬¯ÎÑR0ãeæžcÎáÔ`Ò£"Ï2í2ËÇëó©&G™¾uƒG€ dñ°v«Ðá²b`|ð³knøÐùM¯§jq¥LÊõ jfÚPf™W$–’ M~šT­ð  ‰/9äxì§€¤w<4NÓ]>VŠÒpUµ”ì¨ê¥©g}Ö=<]û)Í-eø¨æÑK½À‰Kó‹ÆÒ!$ +T¸³|HàY@ýRć ¾ ­–Is5 ÄKáƒX2ß¶tÔ¯ëËeZš?|J3Н.R´ÈáÑú1€„:dØòA¾³aˆÛ @Ðñûèà>4p„- <¦®Etüe + #CO*óH©ªÛT¯æ@oXÍ‘ÇÚhïõ«Žë©ýyaê!zf×)F¦\=ñ ƒÇðnIHEB’%Iy~>F®V$ÐA"U<Ü`B$>4x,"ÑQ· _<@ï©"àÂŽ”æ”H­Ô]#GWx"ÍÞ+Œä+m÷SIÚcÚƒÆ^à•öÐè½;#Íä¸,"C‡…Mè8|”– ±v>8áÛ—¹¿ð'>ø•ác 8–Ž_r~õ Ř6CR«¯–Y©éëw<4:KIæÒŽÁcÖË8èà +whùàÙÂC^â!ð8€p<>úí¾:D‡FUfUtõ:RCÍþªCÝ—R{OZ WïÇW=w»óôö#“IƒðH¨¯; @$9W@t¸§â!.’±| ,bBˆwn/\ä'_)“ä—™Î|öë +Q5*Rý;ø¸„¼©¨Dâv|h’‘ެ¬úÈõðÎÇÁÃò nÍa!'ÑÜå0§¹¤ +¤-êjC³S©‘âU=m¤”äÀÞ0r°O{JÅuÚÂïh÷éS‡g^òg¡Ù-£bšøh«ÍíE&hO2ƒÇ$>¤ß‚ q¬rE@x˜Ÿ 6ļFÕ¨Wªõ 2ñjã),]˜#¿Ø¸ð¡E=IO’ÿØþuɇ +ÄêîYAðÁÑ Ÿùoê =)Õ‘²©¡ƒ¤žÉÞÏÚi+Ïÿ”þú‚ã'^â§®N @¨ +jùhñÐà±€ DBCGË<âCÞçˆ+eøÐ|±zð +ùÉUΉߔù:½±Ñ +¢X_-“šõçà¢Ýõ£<äC#’T]ÈŽ6bO|´|à ,ÄBðñoÌå0q›ç#U¸õ)Ÿ€ÀC)öHRcý ¾ò®BñØfÿè¶û«wÚgN!°F"$Ñ>¢ãßÃ#@౓ÿ~ƒKLÀ‡‹†3ø N œ³ó2ñ61™ÑCïò‹q¥xäÍ$Â#ŽÝÝ…fý¸| Ä÷ið°‚€>‰Èoû·-|,..†Gtø`ÔÈo¶Ã#@VÕ'”JÅÎXñF=Úz6h:Vo_L•>õ-Š6Ã~}?k¿WJf†GÁ6F- .yHGVšüfùˆßÀÌúáBñ  òð1KÚ(‰Ý#ö¯Ôd„ÆoÐLLg7 Œ„i‹Ñ6R?þ<†¡#qûËýÁ‹ÇðŽá l1¹{yxp}踵À†¶²S+š²5ÔGÏÁQÕ—–†¨}Ý70íFsÐk§_™Y7&1‰Õ4|Ȉäö¯Òĵzhøhýèþ2âÑáƒqèØûËñ:Ï}ßÂø•bÝ”G¹¾\f53ù ˆM(ÂêÆ>|Úâ Ódøxî.ñáz‰æ°(@ƒsÃ#£C'3z6pÀCKJ¢6GJE·nOï>½wX÷ÂvF®ö>Œ†Ù¥–ò¦GÐ ¬ +ðÏñQºdd%7Š€X6eÎ>À£õ1 ŒK\ ä»6b!ñ VèÇ®iø8ˇFñá‚d<ûG +Rû$õR.õ®‚ûc5õÑ~UOíÕ¼¢Oñ!`ÛCG}Á¨MO^Þ{tB`Ë]<¢ÃuÄ.¦=x0sñÀG$BvýP…¤$ÚGBBã]j¨­æÀ Ý[ÐHÁêÓh0X@fø$ð½eÔÞÄž_xÄÖ£¾¡Á£´åOذ~p†xtÖBçO !ÝÂW,Ïù-@ù õúráòH4âJbk°! H›Ä¤‡|Px¸»^<.L¢¥£û‹ ÄÏ> ²–xÜú±|üE Ž*‰Â|Ò'•6¶OíÁ£yøß3´º¢'¿p1Öþ”γû&2mÍA–º•È ’"ðKQ®ðxøhñÐvý@Gxï¢ÄÖåC{£ãJYVS©o™Í¾`u$Ø¡ƒä+©IÑaÙ¼|hì<†~ »~p‘“àøˆÇˆ÷]Ÿ*q¤,˜X g7Úò­öب”wNÓ=¥µ|hoz}­/xÇUš}ø¸„õÀ±tœd–†„Èœ}JdžƛáƒWèøÇ_óŽƒ-ʼn±‰É¼æø¡ÃNS”Ñ(×7èÌÞ  ¢šÐH°Bxñãã‚|ÈT®IêÑ‘àa£fýØÄ70Œd(BX«3!{Mòþà¡ J¢.SúŒµGÕîÿ­×+©~šÓGçùçñ<ã ;­hV\< +ÙÐ Rƒ‡ähî/‘v|„äºá“XE³~ ƒx‰%„ËÄp€¬ûí¦*Gß³~˜ÕvUDÛ“X££.€¤M KHŒàƒ ðˆÅ#@,ø@;á1€°—Ó6´¤ +/ê€e¥`ê¶R=ÍcíÈã7õCÐQHl/ÎÎ'×{DóJoò®dÒ‘`‰yš^|̯çäJË,Äth؈Žá!ÄKž2–xL GÅ«Qq¥Nß¡æ5ûQ1é±+TxجHÉÄäÒµÁ ø&ÁcáÜðчS|ìÚ¡øÍs„TƒVŽ+H(Λz8¥SFãè9}|DÏÞ²ú+=~”Îí&(±‰o!døˆÉIÑu€ŽðÀ@˜1xˆõc˜ ËúËE^cÙË`N×’Tõx©2)×—ËÔ/ +"k¨ˆd!ÄK ÙÚøènûÆ:>¤»‹†äúc3«ÙíÊÿ¢b$…!Åê7t« +¶…s¤æÑ Få5yhu öÉjc;šã’—yiC§7™IñABÞ]A=tàã…‡åøè·/ÄtháÁ¡7>ˆ‹®4x,5«Gïjª‡ß™¶§ +¥\_.³š¹±l¿ø…îðóÀ1«‡ ò:¬Ú#ÆäðÁ±á#:†C‡Õcø@H€Œ÷ê@³«(J³ú@ˆÂUÀÕ<Ú‡3<š/Tð4‚þÕ|­—vs²#3’PhéHÐâ'édøè÷/ÿÒÇS Èþüƒ<éÆË&±Šaľ‡Î:ˆá|ç½J¼´„¨’j}‡LN‚H6¾‘xEÚð€H)Él‘'@âÃ÷/”CHxðǧ?€cÿtòùöv á+q8q;:FÊ0ø#) ݃©{µ»}†žõày¤ØošÚ¿tŒòõ9C}dÂ$ñ„Gnp [ûûýÁ#¹à ðéôw¿û_ßòåà~¥ÁCžröð‘Ø¾Rõ8r NŠõåjÞ—Äuù°ÐíêAJÉ@$:¡ã®Ð8tW8VÝèxçƒ×ü¶v€ã€¡ºº<Úõèfe|Š8OæÙ«ûàìFHÝWê«yvy5h+'7¯™GB$Ñ<â£õCV ]è~x Ã%öø!€_LÃ87ù +Žðàµë‘xÏ|Å8zÖïácfavÚH˜b%¡Ka$ŸÃ‡,áÁ€®“A„/Ë<ÐAŒã`OããÏ¢e1ñ;±ž”a´%QšÏR0õ«š?­)t:»¤Ìä]ÃÍ~ænضòº—L—"Á‘@m«Áãð9.äÒ`Üa˜Ò–‚ëÓéâa-&®Äëã•áG)Ö7(<´« öb=Kˆ fù’Ìà!Çd>Rx€cè0À#@8–Üšã#1ÔB¡¡Á1x¸D•‚~ü ]\$µÞj“ߨ¡ª°>5¼ë>êKIõS(Ü>ã³–œWÎÞLN`B Ÿ~ùËÀò¡£ïmÒB9t€ãâÑýÅòÁ |ϲnø@-ü%vSÎOTƒ¦6Ž«Ô·¨ÙÍ¿ÖÄ—„*ÜKÈY=⣿X ÙJÙuáò ‹)CÀÑò ÌBˆ ‹.ÂÙ„ â5@–ÃÇJi*Žáª’U@šá£zJÚG)ûjq0jW¾éË^äüÈðnÓY<:HĈ\úÆ?"Ó7>öþÒò1„üÓ?gÔðŽË@ØÊ\hh4ßGÊñRu2|½šÕÔvQ"*‘„)`2È"«]?,”áÑúá +ùuƒ7áÑÖ5zøà'¹îRphèÐ ìW‰¤"4ê¤zGj©Œ«{ ÄG^鸊Óìj`ÐúW^Ћ›ˆÎ‘P.!Âƨñ!‡~·ïãVÿÑiN@ÐÁEÇåƒ\„‡Ÿ%ÿâ—Ì^qžû +R©*)××ËÜ/‰l«oÄB'Y$ˆÈK~…G€H«:ðaù˜?œKá ÙÊÛýøÁjvƒ#ßÐU•±)¶R0PIíFÕr´Fí zt;¢Cw±ÅÇ\€|îŸZÑrÞO†7¹»|ï€ÌêaÝhA£›£4Èò!'¹%|„‡Ä‰ŒàÇÒõcùHLcµ~pðH,B\®JkÇn*¡'•QOR¼ÏÚ’Ž^‡Õ€Âb×ä5/™éQ¤ + +!Q“ ¤‚ t¤î.‡þ›d.ÄS\9 â¯XƹიÉÔ ‹Q®óž¢r¼I”ëëeê Ä 0dÔl)¶“4øøûî-IÊrw{™Ï§ø°¦&xÌúÁ±ù`,s$1<ñ^ >HYT'94ý°€¤ó\šÄg)?^ú„HñÅ+z¡Þél+sS¡ Q ÃG·—ð@Ü\Z?x@ø@Çð!ÃëN&+ò‹Ý©ÀJIVs ×¾Cæ¿êˆ¸Rqêb¡£LÐ1|¸–9K½»K€¸`¸ââÁGr?^<\]ÿÂÉÁcþãì¡£ ò{Ä|š:¼KTˆ”É·ü*yGÓ)3/¶ƒ |….»7ÞåÄë¼ÌH&Jx$8aZéâCìñ!>›>|>±øD ÇÛò-ÏÙÌëuœû¤GS¡Eä;ÔÄF¤¡˜ðhC‡|ðQŠ2:àñk«q#á! »\Uœc¡åƒðÿþ>€Œ¸½®/†Ñ)Í"Ýšu¤‚Gjn\¡À`[íÇ×籃ÕàpÌñ#¯¢Ý5 ‰a%<³xÑ!r”K¦åãüÑ1<¬ðÀG„ÄžÕãÞ_ø˜ÄÞƒ,ÚšC9lÔI¹¾Z¦5±© ©– +R¼3DG}ø¸ËG‰ê’î“>4€@„3Ã@.éˆõƒµÄã¼GÎc£"(Å#åQ¢-Ôw8µ£n9/ûhå¡1Ù+ÿ*$ôšnœÍ5ê<´Ó ) %b+Ê"ƆÉćô?þIÊnµï|ÃØÆ½\d&>þŽ³Ù m g¿RhWç¨:©×—Ë´5r{I"é„Kb'œã!Ñ!Ã!$>‚èxÿù@ø„a-ÄÌăøÜÚ‘Z?æÿñ"¥PŽGª¤)Ø#5TE퓦¾æëª>£â+¿îsF0ü¤æ•¶šÅ´dúÞ$|| !à°zìò!SùZ>üú~¦p†ˆÄG€àÃ'Br78º™>RU±lØÈP£-Ûh Sà9¦-÷>R›§hÆ÷åƒúÒ¼Àk““ÛŽ@bòÙHˆ‚ÿ®WðCG‚}€ìâýt}>œòÃýe阘ŽaèH—„071:±?ý ÚŽž©Ö×Ë´¦>“à>25Éúçö2’´ìYà*y[?â#@ø!ìCLj¡ÃÇ ìWtà㣔I©®W>•üA•X×> ?è"âËtväìŸL6&ÁÄL¢_>|ê&Ÿ?\e‹Ž»z„‡õ#w2Ë<„‰<—fm¥WUI‘ì¾Af6¬„• +u¦ ~‘P H€H5µ~  û dþù1x$×|FM|$žÞõcÈê|ŸKj’ªÏv¥R®4µó@Õ´vû“n½«sE·4ÔôxøŸýVöÀað::»•YÒ|Ö1;>"Dh"e÷ÄBLJTHN³~L¦R x0|üæ7ØˆŽƒÇå##gù óøoýæac¥jBÊ£Pß óÎÌbX‰J¤#€ˆÙžðxX@öÿ“,É;>´ô¬ðèÏZa’¨²ËÇâ±|HTÊ>~÷›ß£õ#ñ‰[‰u 7±Aì…Çh¼W‚´lÌPyéËeZjWß`DTLŸ‘¢cøI n.+€#Ác™ïóð¡YCfá#?Gø@HFó{ÙHª@[zþ¶ƒjí¡ÖÎ@ʬ>ïØír>‚ÌóóŠ=‘c&HMµX@ "¾ÃG‚ø"+¹Iq‘óÒaõ˜åcø˜ĸèÀGâ)ñwFG\LW +ÅДëdÚe£ Èñ!`ë]‚G€Èh‘#H òœ‡ΠáÃ7/çör>Ÿžû˯2™ÕÄû#ÅPýH±¾^¦mê÷8#äƒD‘‘¤â#BБº6À‡n/Zx ³‚üo> ¤?b8†²•µ»€ÄÓs><4ª†— U,9¨)ãéïRå©ôÿ¿Ò#ÅGÃ]5êZêns^säĦ ³š™Är$È‘À£ã|s{ð(Ï]?æþ‡¡Ã•cùðÝ˯ǰ…ƒúøA,uéqwùÀÅhÊð.µúAã Ä! Z²„ˆ[ø$’ÒBÐÐŽø@‰öðá™õƒ˜A„±Äb>³›_ õpÁ<¦Dõ¢S9{G;ЖøMó„F34ÃèuÔ½$ÍÉÈÉÄf¶Shhø¥ I¡áãOÿ"Íà<~o ½|À ð8ˆÜäèðñ+.gºå#F¦&·ÏÉÔêdò´ñBŸèDšì.å@óãÎçSÉ®dÎY@º¿Àƒ9‚ŠmøˆŽø<È2 ÈùGr_ßÀ¤‹È¨BÂínWLýƒT¼á‘Çd& O;Ú×ïÉ:7™Uí†ñ Â%ÑSt ~úù‡¹óŠo»z01¹Ü˜Šw˜±{èØb¼Ëuü-j⢠Œ#tˆò]ȰÃCR»~\>à!}xð‚!-Ȭ¾ñ"Lä%B˜zá3³9%¬G‡6šodªŒ¡*©×‚á@?RÍÿ‹æö¢u º–þzþ!~ƒ¶hÌ GC3£@- ‘PEL¢G‡THfçþ"[‹¦ÔYÀˆ|D‡Æ<;ùÊ[xÌú1x¤)Ë‘ê¨Õ÷Èô/y«G–iû€$xh&×EbB ÈC‹(<Úx7ëG„p”­£dÄutÐ壊”gà0FÅAdFƒrV~ÖF³o¨|¹–ú¬ax´‡^óhNkÔfN]“Ø–Žƒ‡àe€ô7:vù˜_ݺ@ò!èà —˜døpµ¢ƒ ¬Îy ÈÒ¡Ô±ëø;dÞ´cq4‘ÄJýüCì$*+|Ìú!ÒFG,}D% ñ¡1:?Bejl¬x}ð°€ÄGÅ8£ +Q;…R±‘Ú=RÒ8šƒRÓÓ¢Ðè#ªf¯Ó¼©%Ƕ2_2¹8Ä‚¥pá1«rðxñ$ëð`Ä,Ö«›,¸èp‰x låmròš"<êÃÇTH¹¾^¦&ñ¼bšEºš ÙÄÉO¢ðØßÐIËÇha<¬ Ö\? ;ëBX:Î.#¬¾÷„¬”cT]ªÐ–JÉ*ÛÕò½¶µ#…?r‚ÚÇCˆÃó¢~â„sJ2ݪÕCL#!ŠTü%ðÀÇŸ¬”K‡ĉa >žåcðøû¿ç$;‰³<ÖŽ˜kÑîöïÁcøhz!Ùi†dáx“Ü%%s‘£<’än9ß¿0…3ñ1„¸œ–÷—ù×òAC—Ä÷šT \( +4À±ê'ª¶š®—ÎN™ý «Šo›a!fXÍ¡ç ½È‘áýÄf3-Ée}+Dýêð˜ôä(×ÃÇ2| £Ú<Œ\­±8å9çIú`ØV«NŠõõÆ‘(V¢{ð8££4бxìò1|HÛ2ëG€0"¶añŒuáÁH:7™a1<4â|Þ«j¼I ³S¸SÃÝ)ê¨ê¾ä‘FªŽ€—P¡½ÔƒûR!«æY$ì›l Có»Û~<æâÕ>8ÉN¾‹Ï_.Þ4•2|½LžÞÃ^1ð‰\ +Ñ¡- ”çðñ¢ƒ}ü€cXóCvÆÅGÂBŽØHÝ`FÌŽÔš>½ú@À1eC†2Î'U݃ÿ·ÔP‡Ã~óò’×ôª•“™«‰Í/‰LxFG|ˆŸ¤"¡%¤4ãÕ&piø`Xb^|„›œ%ÆøÆá£ÚØíŸ¿CfþQâìKâ>HV$?‰$y@Z<ð1€ÌúÉGfâcï/Lf4·/ S—¡_L*Ü£tœ?r5ÂC»tŒöгµw9ß›LfRtg$:tàc—À éÉ1:$ðçö‚ŽÑÜ–áAÇ’ÁG¬5P‡—¦NÊõ23 ±i‚£`GE/‡Å #ùMª¥ >Îß¡ˆ†ð> ÄMwˆ $——ŽÄ|+¸*T à˜Q¥FÆ]RJõ|Ó”zŸ¹e·Âêu„%„z‘ÞØÞy© Ì52}éeÞåcðÄáãÞ]†¥#˜á¡Ë⃃œ´BØœxÎø¤´»I­¾C'€ @ÍŽ$‹RIr |üýù;tÝaÐý€¤lKà n.üEŸÙÍsp)CêºMJ4ÃÔJÕloRÓ«pñ¸2Ÿ®æ§½éAÃó6šáŒNA¦0]ݤ¡1ƒØðL<4pX>fõˆtà ðHybqäŠÊ7FHŽvñ1øˆõd§÷ÂíØ£ïRsë7Ž9(ÂÅ#?È%³tåéZ0e¢Ú; \Ë:þqq‰a28íT€ãác¤rjG~^%+èÑÖ7õG†T¼}Ѧû:Í7¶í NéüÍóÈä¢!à&x$‹Ÿà1|Ü_¾ c~yëãW÷vÄÕ3x°*ËxÇA^Ä\ç7ת©‘b}¹ÌŒsÛÍa$X¤p$§!ÄχéÞa,yÐúÁ<|øˆVj4|0™ÕI.Í¿èI-à Ê£FʦzŠˆ ÔTig;ڧȧ‘ôñãèyÐnÞglïÅ£=í̳Dš:ÁãÒçè8÷—ÖÂR:ëG€ä[?X̱ƒŃgÑѽe±2g0›Ññ‹¸Hª`3´•r}ƒL¼ºqˆŒÄ;š ÈC:r’6ü† ðФ壎«ÇზphKHv†ÇðÁçC0¦c#•dYQȦ©!µ›’*ì'©8Uÿ7ÅÊ‘æËó¢+gš“ÖšÄ|&&QF@é¤ÛKx”“䟹à \–à!Ä/bÞˆ™YÊZb2>à€‘Æ[Œ+ß¾|gêv+¡‰P¸‹Æjð´àQGG|DGxH+GøbmíçËŒr=-!øÐ|ÌOÚáÁf|h+îãB”ƒ* ͨXÊUÍÔŽª¥{LEUÚ«W±É¬àpî,šÁWúêö^9çKî\fÑ’‰Í¡ è°|àÃJ ³~HÒj#Npc׋‡ çø«¬u{a÷Ñ(„Rè¤2šá;4S× +¯(Å)ê"OàH2É!#™/"œhýàKA¢C‹6ö±ð‚—¹ ›qÜu©NQ(ÕºU›*’AaW»Ð=Px ¤¿"£ã!ãÊ+æ5WÎälÛ æ$?^OE#2AZ?†pÐâ!/ùEÇáÃRÛ7ˆ5ÔZ‹Ž¬càà#ÓØN Ê S†vªõå2qS›\?1 N€„šßÐÉC>Ij$GëÇ‚ŽÒñ1t|ºÅÔ$<BÌerâ7ýQÕGB²ŽýƶîKçÅ;"#iLeÒ$ŽZ!‰Ö'tGG¬/Cˆë!>–^p„3býð½ @Xœ´eìÅ#,FìW‰­È¨:}ŸÌ/†áb´ƒ•£ž¤$©$Ã8´Ô'0„W")<†¡cZ·˜A„ß­ œ'U8<´¤bpÕ‘Š*®¯îÑTHøQžíu>À¯æ²€HŸ˜Á’¡cñÀG€ Dë£Y?ˆ­ð —âŸÃÚòª:†ªe«pSÁ°8RZ•~×þ†Ž °²bh«=ðU_ßwÚ9˜3œÝÉŒðH"!Á Q¤/>’dàMªÃ@´aÉ£øçÈÆÄ×¹­:ëÕ@N]¾Ž‘ +i‚àÑâáþrñ2 +|´jJ!Ä‹" aS|,!øÀF|,!Œ½€ „åà`~|Ô“ÂŒT)U°•Z>ªÂÚg!@ÙðÓòŠ£yÍ©8ÂcᨋC0…EBLðÀÇâÓT€DÛ 44tàƒ/CGx0 ÚáãÂåtð˜Q!”BG‡ö­É…:L…« [):´Y?¤(S’3Iç¿)d >à1€D8„!„µ Ã)ÀÂ1)MW°2©—-©^%TJª°ªK[æ—¾FV <ŒG¾¦{Amä¶=§S™!¦E[ +VÄ>~H€¤âSÕð!Í>H Ã<†渎ƒoìKàHœõùŽÍüµ?ÕP p¾YByIxã<€ D‡ABI’}ø@ùøÝoð!‡>ƒG€h×3¿o*…-uñ*‘BůޤŠz%­+ðÈÁûG +$¼ }S‹Œé¾žÚwŠ+§µ5&xŒ!‘ámŸ!ɹ +ä:€ÈøØßí»tÐAîÈa^âen⃽ùÌo†§ÃÇ•}¯Ä ¤ Êïc$ö4™ÈEJ»‚ä â&#}½!]2li}„hÉÅÅÁlt¹ñ”¯ø€Çþ¥~x@cºzTåBTk計3*'U\E¦­z@ðƒ<ùÖ¼hÕ lÿý?ÎŒç¯ì™o$€â>FÂŇÈŽûz š¤Â:ëGtÔñÁ7‡œSó—Ïùý¦S‰Ù};FÁw%ê Ó e&ÁÉS¾²NðˆåÃ-—/ìÈEðˆbfŽ2 ý•©q-¾R!ª†KÖ +²R§%dQBU]©ð•*§ô ‹ÆhÍ×¼âÊ)¼/¶“¯LZxjñ˜Õ#Ð’±Å“ Ñýñ±Áƒ[®¨¬ãሡüå21ü%¥¸R›o– è‰It"L¢H)<’MŽ:I›@¼È;!Ì¢G—#¾âc ‰bþðQ‡ÈJuÔèHÕhʨ ÓþUu¿¡»løÂ»|±AkßΫû‘)ácÐ0Kmdî4tˆK„A‡Ñ“d@/A‚Ácn/Ì€‡åƒx4pÔ©«,q4±™ÝGÜW…7©íU :4vK¾ËõH£,‰ÑQ/×Á܈>‚há8ƒ¸ - @þˆ w“a9ñ^¶!DqÐ1±r)›¦j9­¾ÔÏ6”žf ƒÊ}ÂkzÅ#'¡Îh°ÌeÖ¦E>„c| #>Êú$Oùúáz|ŒºoxðÊFœË@Ûò‘Á—%©…B$O^7bÛØÇÌȊ¹%„ûðH*¢,¤DøX@îMÈPUƒNÅýüTýÑÑÒQ7DНøjºûä œ3ÑÄ$„Å´$FqŠw–Ã@Ü;)6êà˜å"ü˜ –X>¥áã²ân+ÈKJv¤ï¤#>HÆ":Ñ SÔÈ®Kb%)™Iðï~5y:6H|PF øÐˆ“ÄSG¯îð‘ñ€hðÐ\½Gê¥h¨¨“jŽ¢C›á);$^Bà¡ÏWvl·opŠ6f˜y.ÃRÅ‚xh„Žùæ>GÅIn]=†bG|0(£t–%>&†ò–ʼnã«-@·Û{ôŠHÞTPn1b&áw$’Ôò1’¯´>ˆËÇ‹v- L¬Ä‡ tAUÁCE !¢Zø d(<´©knPæÚ”þ“f‰_o×~º·¯æçd’ÚÎjö!AaCKÃǬ¯_ÎíODƒÇð .›!v1‘\pÄÚ¬¯U„{@ +¤Nß$óOÅÖ'FžF’HËG«ãÉ4>–pÀcþ0êÂ±Š¸Æ;ÂÇ"‡ùÃBÄ÷‡BjDà õ;RÑѹNSø"Ò®ÉS¶—žc§pŽçtà3 ™±IJ÷ñÑ¿ö/&Dì‹IP¦ðÐú›Ù‰>1+ˈƒ£è€Gb¸èÆÑÙ½B}Ìlú¨ý…#Dñ6ŽuˆŒ¤*ëÁ‰‰1n¿bïå#Wgmfñòqˆ_è*>FàP¬óÕõ ŽŠ;Š€—<ÂÄ'y~uŽÎ;í…œ5™Ã<ÍgÚà„G€,|X?ŽD_&$±ËÇÐq–º| !2Z#]rì^åþª}]mê»dn1GêX„­¨E¾’År$Ç$ãVmñ¸€ $ÇtîY‚Ùx 8,Ñn0ƒë•@ HE>!—#åœÂNkÊ~ +?z_<ŽzVOó 9“´~tÆNŒÑð‘H¬"¡åãáCøø@¤uùpA ä €ð‡IœâXâ ¹‰è·“½¦WŽ5R¨o”®„#‰s$úRˆläô ™%i.ËÇBüàË‹„„Çò1b§oqKf4:÷ÕPxhª„íH ÕQIÕÖö’c?Iu£‡OxÒz 9œýÐŽèè´N¿ëÇNÓ[>Hxýsü—±ãC2ðƒG€HüðñO.Š9„¸¿ „§ðÈ]&?ê' Úê9P¤ï‘¹G˜hÐ Â\‰|TGØH%IR–ù¤Ï†è1"ÃÈ"I¼ä(_Ññ&ÿUA)ðñoj£ !®çšâm!ÕT]WÊLJ®1ßÝÂbvÉÓo_:êÆ£NÛùõ[2»Õ»|r$d;Â%BƒÇ~ü1|pÂòâÔŃwè˜ÎÐä!D F¾¹Ç­ñß&³¿´‰2MÈ"'Yl.v²‚ÇÐ1|ôãN<>¢#¾ùé":Ò!„·!Âê½Ãà!à ËG…‰M¹Ä€x|¦ã›bÁöY=ç««Ž¼sÚë;—ú22§™ÑXm AïÄX¼´x r +IJ•\ ȘqˆQ>£2-‚ƒù¶|_á¡ÉÍÍ%IWÒò޹¿pfñ:âCƒ‡_ðó•¹C·u@|–’¨Ó·I…0ÚèDYÄ}p²­äŽº¼F< ²÷3Bxh,ñgáÇXgùH¬ÌOxhçCŒGHŒ`£Ve’J%EKJIŠ + ­2På‡ÄN4ò4ÝÈRgƒÇÀ1c3™š ®n|bk¡ÛÕãð!‰Y åô{h !’]>x CÈüxŒW#æe"/ùÉ[&³™ëI ¦ +3©ÒwÉì/?Y/Ê•Ÿ“Ѥ`×ÿäÊ-ÍM&<$1‚°‘3 Bq+<·oa¢¯žñ!åŸ@"D©HíFÊ© ç£ÊWSùÖ -¡¢‹Ù‘/P¿Ü]yØ»õN22•Ét3_<„ÂïG€t!ŸPKYö‰}£q‰W¶lãñò’ÏýPø¿ò ÄHj¢PߤŸN0´ U#qŒ’J3Iy„€ Äú1Œ¸¤8 ÄJâj„øbá86FÊøR«‘â¹¼UR=ÿû¿ÞÖ ÕNí «acOù"Ùi´pĆ~äüøÐÀ±xàC „GB:†iàî/Iª$géÃãŸØÁÞ°hÇÁ#ñ›ù?!uú.™Üö.1¾$ê—¤s%=|h!ëçˆ iQeNzñ<¼x a4³ñÁö¤ © <4R6Õ#…LêZ‘•ºŠÏÞöOšÚëÏÐ ´Õ¼Þûɉ’“:±“ÏÊAИÉ$¬ƒG€üÀGÿeöÞ`F“¶ì]'ôÆGx°mÅF|\6¦sœý¶dÜCeú65½n‰6%mAÓIâEH÷˜ò”-I\þLýgcxà âÓ‚ßÄá!è˜o`.Ý^ð¡ê¡.êCªE*WmIeGçÖ†Ï:ßê^uTñQOý“xd®ðÐ<âC@ðxk¿\‡"tìò!I·—Ö©ˆåc<áÕcùxá§vÍe÷бêöb;Uê›dòÏš0Gb^@FRYEG’)IZòøB˜A|È"Â,–Y>¸wøÐ"„í ‰Ö[>’TF}ÔèBA¢’jJUX[;¬Ì’–Šwùêj_éÍɹ®°‘L“LKb€©`¡"døÎå"¥I¯<£ãâAœ€G<ê‡NG8Øo›Qsºñ;dâÚh£IŽüFÀµÒd“$'AKˆd ”—¦$µÖâ#1 èÀ_Ùkqœó–uPU™ÛK€(ײ±ptÅ+­«óüqS;`Øãà§å£9ðVÃÊéœÏ‰=™Ç|&%sÇÇj!’ˆÁ±t憄HQž!ã;ÜxùÃ&V±‹mÌ#NRî6øŒ +ÊýÚG©Ö—Ë´«9Ù,köúÂQN ’¦fõ*@v ɘ7@x†dý€Hâ,D ~Q7„¨‚_ѩȡZê¥l³‘‚†GRë-¹> -õ—zä5´o°GÈmÉ™Gf5·H(ðHgý@ò"tB8I¢ox<€ì +Â!|,!Ù‡ìäiâ4ÓÇzE †ºÚìuüÚùWs 8AÕìE,pÁ“,j#éýR‚ÃÆHÚᑸ1ÂG„äÕð¡‘ËløÈXþöd:ÏѤ$JS‘– ©ãHQ©zoé „—Þy—¬¼©¶r®äÔ+ ®LÛìV1M0 ˆè@,N± +ÚÈ‚øÎÇ‚x$ËÇðÁ!bÖˆ…ÉÍš« ìN¼Wˆ£Neëë53¯Ä ©(õ‘¨…ß_ô’ 8(2j‘êHæÑ–.r "2| „â#1#ä#ãÒJ® K‡®Zè>¨{>D±qq:@¦Ýo_FÃHƒ¯{…~ÈØ‘œÌ 7íÞlá¡ áÐ!¤$:ŠS°BÆ6I¢DNˆ,©|ålàDò\ʬ¼[8ކ4£(ÂÏN¹¾^ ˆÎÑ•Å)jƒ½d"™­äi#yß$>ö,kñ!\c‡€ ¤ 01;<â n0n1J£L +Eÿ¡n!¢œIaI±WCÂÑý€zv½¢*Þƒ Ç{ +§"§væÎo¦dNphÅ ÃC+2ƒ*^|ÄFCw) KˆÕC;|pƒø@Ȉ[lË723C9Ëâ£>‡l V}oy¥d_ª‚Ю +S?ýJ*«InÉ’¼¯Á¾°çá#@¿%dá/‡]‰/>*ƒz(L%‚‡fY)gR[RçÀxø8ЉúK¾~åMÞ¹rtØÀ±ZDƒÌ/tŒ\á…Ç‹äBÇ$e*Ýáca>xÂît%ñ+BÀ¡ã³ŸhŒªTåúr5uÚñî&¸-@´Q¹ÈË +bø;=áC“y6è,!ψO‡þ1Ñgwl|dïY?„ûø˜õcQ¤¡ãÁCA6©³r+=> Óçмì%ËG ÛB£¾lhË2öÃÇÞ_4äÁ ”DËaþw%'Epì¯påÎ>LGt° KHâðã7÷Õ@Vs¤2_ÎGÓ>ãÂã†ÛüH:$- ÎbLÒ.y #Þ:´!„X8+6›‡| !ê $4€¢)]²tTTå­ÆÊ½tÌPO}9p¬zW’wõ¾#ç¢C2êdÂdòUtèIpàHÂ=|D88|B}ÿªd¥œ$B8’Þñ˜“KGæ2™Ñtvª@gwj¤f_ª™Q75Í??Z,ú†ŒµGr"Ùɰۨm$}âÃÃ<Òß»$Îu{<2•µüå3»ñ¡©€B !jã"~ÃC5- *û +¿¨Ð:Nû Ý´ÑÆ½éÔiõdsA#55‰B$i–ÑEHŸNM³Êb$)+#Dæ'd»€H,ÂBV\càþÖ“§ðh÷¸ÐV¨MeR®/• Mž +"õ¸'Ø×0|̰|H.%JR¾€,"\É|Ãà Z„p4Bhðë¢ />T,)ŸBVÕ)¯¦«¹ä%DèÆ÷'{äe†}qädC]€ÄÇ’ÑÝeؘ!8hèÀGB‰ÞÚ è€Çï¤'QÛ]?Îoè Âtü—bC_qrñH¹œõG +¡ž°«*Iɾ^¦5µXnŸ Œ£Â6‚Ã0@ Ò’œI¾¶–NO –bØdÄM¦!®B„¸*I–…AÇ¿ÿ{×4):â£>eVt@ºûäøÄ ¦‘wxWšSÀƒœ79¿IÐ258–ŒQlÔ$ü>l-2$ÉŽ$.>ðƒ'’ЄðÄУ3ÙH +aSœ†¤RJžT;é b$ºÕ„º7».|H «> Èþ Úˆ?lb±nG|¸ôðA B|ü«JBy[@TSe§¶J}Ñ9¦à˜îGN4rÞàЃcéÀÆêEˆ€Àñ†‡ï¹âZôI&³~ÜÐ!Ë~tìíECÇáƒE+öñpÔÏ—òÖ¶šC9ô†J5Û—jç~W…—n¼l#é¬pqåjxb ј‚Ž]?柿LHKq„07@\Lg=>B9èßÿM­TMÝCHRéJ¯ê ƒm†°¨Ú`ñ¨w¥9…sÝϦNÞMcBtÔÑQ#ÀZ>þõ_þ,<*R$….~˜' É+ÉÒÒQ[@ yçƒgÄD×OW¹|§©ÃKqaçàkeÆ£·CÁÕ£á bÑ¥&Ç„ I>eOˆ=‡†¹²HÜä)gyœø ø .Xå©T—õÜÊN•û + íHñâëWÞäÝäLȈàÕÈTæjÆÁ£ÕcøN—è"N±Š84v ‘<>[ÉÑòáêHöç:¦s‡M¬"€h#v.ÛIÒì UÑ"}¹š³j#±4þ„äØ@½IR–{b-ÇkªÕ㟳0+VV'|¤:¼ã1t(¡‹]A+,)÷ÒÝ_<ÏQ½¦×é~lj{É)GÎñ¨-¦‚0…GBÇHÈéà¾ß’$)€Žä‚9|DHâXö±sGgÚîÅk§d_¨¦3/9¼Á4 +ð“.‰+iÉîMR^:3, óéc!¶q‹¼¤ðpgò¬‰Ë@ú{€Ìú¡Š +zé8|Tsü„– òŠ+oL¨Àˆ}GNKÎ?X¦;4’x„A +uè(öQË \à¡%9†ˆ„Iî~$Îpç® Ë>4ÊáQæ+Ýý#UúZ5gƒXl«¢òhtbn'Z’ÅÿKÒ•ò0âJ>, ôkw|´òºÆ8Âf‹Çÿ‚ íQLRÕ¤ÚÃHåÁ ‡×:âÃÇ>ë^Õ˽Ñûå +:öÌ`¦+ó®ÀQà8x¼øñJô¥ ù€C:ÂZëGbÄ. ÁAËËÐÑ6tä­ÖùJP›~õåxÐÌjûA‚½>6•ÃHF¨ßô®¤LÒÇq!#NÁ# ù-:2—ËË÷ÁÕª(-îÃGÂÇJ‰•Z±GpˆÁ¨}’=ò^r’:FN?†í¡Q#Q .¥hûH  ‘Ô"M|h—ðÈÐQ‡Gfõ©ƒ#–§Ï%3ü€ññ{KeR²/TÓ5ëh÷;ž(ß4¬À±kˆ Ó¤j4±xrùðCÔá$>Îr>¡2þOèP %Qx(×áÕ¾Rá¤Ü+€ BÂþƒ¼„¼UÉ)Þ„|˜ÃDWØ †-‰Š|8µzÔ‡Id. ©œ!$œ`¡£ uı®.&O™Ë^>å¾2|’b}­fFûŠKà»Ä®É!>4Û(,ô_Êy” ‹>´÷õ#<™ËgšõC”Ãú‘”I½ÞðP]%Vï— ð®å¨ã¸HÞå©“Œº·6Ì`29“ÙÅ ŽiáEG*^ÛÐQ +# ù"µð¨'YƒC‹á oZ?FL#r“fä.‹WüW‡um¤f_*S>*†ºQXÙ¿TøI*%%3’#:$j1  CsR|ø—usƒASÉEHóW¥\£Ö¥µà+^|BâC©U\égXÞ5ϧ/Xõ®4xt.g9½Ï8/£Þ_Éž0À±Ÿ?D&@|\BÂ$‡¡cC7:d!!ˆþއ4q’«G|Nãü”à]ª¥d_,sÖüèhãê¡鯼’ÉHf$Ç•Œ÷þÒÿ"4t  |rAeBØÉPb/:Ømù˜OHA¶6ª¤tƒ‡‚&Vhš!A ?þÖvÆO¯õ¦£Ác'%t˜Gfl^“‹À+„îð#tH`Õê¡/Ò”ìJâÒ‡:°Ä"âVÖaƒ‹ôÁÃð’Ä9pÙ‚< <äåÄu¸| cø¨:ÕÉ‚ùáÙ)qÚÂmo‚Äé;z…NÞ2” ÝY´>z\5:ð±p0^Ú„¢c%d ˆà»Ã@D6àÐHz$WšõcV$Æ ãðqGˆ°s…”ÓŽá +ñ§RÕ¾Z¦FCÝæ@;šÇ®±ÝDNu¹äW¿ì÷ ^1CoˆIü<ˆL>€¨‚j |¸Î¡1Rcë‚ëFôù4>É‚Ý˼ÚûÐÉéœòðaŠ•ÙLéc±m"Ǩ ttèÂÔ°Q'lÔ¤ !R#Iv!Hwø@H|DWXC, ++.ëlð`ç‹™<~³ÿ¥9V*€|-"fkÖGRoâêîOø Ò2”g’ó¯¤ß:êj!®p§d~ ùž2—Ï®ÈÌGH|ÜÿZÛâ?x,®þ¤â/`ð˜áè9öe/Y:ÈÛ%>RlÔ ±26̼d؉ITàájè°zÌúA–A™Hˆ"¤üŽèøçà ðåK ÃGÎ1%ó¯6Íl¯Ôà{Ý*EŠö•2ãg‰g‚Kölþj“2â£&×RžÜ—®œ„UÌzø8„ ü­Dµ!åR½È!ddáPõ­= +yf᨟‡^r /§8t„G'7Ð €¤°0ù ²²vP‹Gÿlòe è¨'i|ð!/Ž‘ôŸ€£ÿoláÕ ”½™lo×EÉÿŸ¢}©LyWèVˆ%pÁËCB<.±‘¬ l±…?Ë„Läfx`Cã<>HiÈ*ñÓ±¥C}+¶m¤öp +}¥Wή| Ô…Æp1ác¿oE=ö%æŒ íH$Ç<´‰0\x}ÿÂõ¹B«8HɬÊ™”—*ôec…†GÏÏ?y¯S8ÏÇâ訛q… +Çl.FÐGBGB‡þÆÇ°Aè ]@Hæ‰ œÈ’øH ‡q¬ â&oWpýG©Ù×ÊœfÕ?HpOtj=Eˆ’Ä ÇÈe]?4Î ¬‚ñÐÕÖòÑ%Èq€(…š2)Ùð1€¨®+ô,sÛÀFý“,"8ñ…‘×F:ÈiÐÑFˇ BƒLgN›üH(£vB;Z> Œºè“Dl² "ÈQl £ŒÄ â +84ÂcØ1‘ÄáG g?ùéGÃHɾ\¦éëô +륂M% HIÉMv’•±-@ð±@´ç÷Ëc]€¼f<<”C‘¬ôÊ……TÔð8Rð»€D>ªgµ3דw:Éâ:X:LŽÚhvB¡ö¨hiK òéáQ'h´–V9cDòáÁzáAüzã#qqÜe¿>büg)×—«YMMwô¢€¯p1š¤ä&Wphrg€+%@þ¡Ÿ¡ò(§XøgñÁr„¨Ç¿VÅBGÅs©[A”×úaK>T¡!õv?P_yú%¯ö&o†9Õ(ì°±ßÒŽÌGf;ÇHP‡Ž‡T ÞF–ø "Ë "’—=òknXXm„4€¸Åìß§ëbsáÑ £\×U¿XMjЦ'ëEÃU_~ø˜ÅcÇ$7YŽä<|>æöÂ#×ÍSfãðÁ`|p^)šª)¢j*¬úŽ”›þá±{lL·]ym|x'‰º³:³ó'5:ÈÜI  ‡ÆšÃ ~û‚7Bb„†­¬Â##™Ë?ñÂåòK±‰[6tGbå“Wœg|rH[¬Ù¥N;®oHí !ÓyÙÈ{@† meù€…n$s&óOFE4hèš^¨/:üUôRHé ièp”*<â!ĈðhÑÑáHegùpáhö2Ø6v3>ë•ÀN©fP³/TSž9M¿ŒaÆb‰ôJÒ˜œBD’䪘ÕÃúñ÷ðÐâÃ%dù'–ñ/+Yjý˜o`¢C‹õR6Œ®|&ÕþƒÒâaÝaf{äuèˆÎ@ðHÎ ç ³€Ä|ƒ8ÌßHV £=Y5 o‚Æ>º·hð¸²8®f™¿ #wâ+\1Á‘¸Äªáãþ˜,#G|µÙÓøýöçs®êkeF³šùh¸˜¡ß^ u´Á· ¤*Ù$€O˜Ã£ä’²|P|¸ò8lùÐ*>¢lj¨”ñ¡¾G*ŽRü¹„ù‚§t/Xy‡· GaQ7‚£)ÿüoŒ#Ñh Y>ÒwG-jx С½ø ù dýˆe„ÁÇdð žYBFà¦òˆïGªT ª•’}¹L:A ½“‡‚Þøe"!l´%ËØÀêÚÁˆ„e–^²”·Ã:”ÃEL]×.ðÑ‹Š`Ðù&׊qäØO9ðå‘·`£Nç³Grêàˆh€c„‰„ ,dƒG2j£c¾$ƒàHó äÿÒ-fñ}°‚ð‘ØÃ(fׯбÜ=ÇOSeú6™{ƒÖOJ¼I©Ä ’®”e΂Áƒ3Ä&|Dóâcyø@T UQEñQ‰5Õ¶]@¢ÀpäÀ£UGàÍûÀ‘œkø˜s‡0ôdÒ‡„ŠýØa—„÷.LÔWa"‰,¿ù­t—K/ðp<ËÇÒÁ:þeå{3x®ó]O³W!µúrͤ¦Áhbyv<õH&²’–ô$IRîæJ‹Çòáó‡ûË|kù˜_ߺy *£NŠæÂVDrÑ«lE&õƆv +lì‹Àõ:lhÞ /r¢¥cبÃÐÐ’É£B³Ù“¨È„÷Rph$vñ¯$³|80:ð1:€DGðX1ˆQƒG@â#D˜;p$ž§»¯B +õMjj!|Ðl¾ à‘$R 鲓¥d“ä-CcБZ=N¶|0šë*C +Ñ¡«i Èð¡èG!¢–ö8EF²³ÜŒœ`ð>† phðx“ɯ@1*¨Æ‘Åy¶bîŠOŽáa )X¬à/EˆP뇖x0|p¬!>%x¤³#éÂaÇq¶¿¤ +S¥o’Ùõ8Ðí_šGaS9<’Øò1€È?Þ`†rôÅÇ‚…‹~| +mîj¾Á£‹…Á¡X„ÆÊ»¼wäTû} BÂc ês鎖Ž+ñhÓ«âGãHäÀ0JâÜ`Fð@Çð1+>ÂÃOÉ–yã#@¸Å2t¬òQç'oYlhÇõL7¤)Ž}›LÞfÿY'Ĉ¦~,ù”‘ôH®ð¸€°ƒÐ¡Á ¼ —]3_%†ŽøPB…Œt>ô-=ÎÙ@¨,ÄFˇÝÈ{z×¼Î7l2È< 2áÚ¨€à!°@Ñ&ÖÓÓÞ_dñ’UK84€„ˆ+c×[¬¬èˆ¥Ãý…u«ñ’€Á^&ï6ºßæVuúV CX7¢—6T§Ò@†Q^à¨Kzðˆ»|Àãòx>uû³Ï»| È Á +£ÁÚªÇ^rå]ÞÝ9œÉò:t2ɤG"8ºH”c›*!Ã0’ L&#BôIrv‹‘ü!„„î,‰ü\q—Ã3fºöHy¾[‚(Âêàƒ<'î7<*¹‹G àÑŸ"c ƒXµ€X‚YÉÛs%¿Ö*«/ ¤æ!°*fxvä­ ^êõÃÇÊé6¢Ãº¹ Œ•Žú“ÈozýL½þ¦bŸ6x¬fýHà ™Z= 1á…G€°ˆUÎÑîx‰»:Ã?è<¡Fß*!ü„îÊì„^‘$FR ¥ƒØÁrq‹\_- ¼e´" ÊbýhQÚÇÂ@ðIn2Àp¼"yy/œ:ÃÐñ°á®ò£PÄ#."(VY:¤¢ÃCÿÝoûIûÒ‘\ÒŽ$v¸^øÂBñÌú_e¢ÎMÆŽ»L&†”ò(Ñ7K#GslÿHÐ#©HB"c—±á¬ðhýp=ùàfY@8Þ×iðˆBºÖÃCq•X¥ûf¤Uáb€2Ç}²•£Þë¼~ð9ÛòÑ6phË2f@&4€!t”<:Úˆ õ.ޤ†Érøsx›,<4®´€„£>ˆy+^r“©ÙËcv¿t)É7K…1q8r ŸèHÜ×Ña€}<âã… ! bËtðxëER4—7©§º*o/Õºš+=@ñA牾:ú÷>p âœÐ€ °X™wè qÌv°ØÐŒ‚¤‹× ðÕ=žõCwƒ  Ž\ +$éðxh+Öð‡I¼"®%öÁcùàë/Ù;ÊömWJ¢,ß-Hµ«¹ËØÒ ‚„Œ~õ˾esa$|h\¡øð :ƹÀ2”ø­"øX<UVVftЖ=ÐðA7y±÷ÑðµÃÇÐÉ|ˆÐµQplÇ ¦·ÙïŽ&`‰þáâ¥øh‘ct@£îÎJˆ}è þä<î1¶_h«Ù³wùø(µPŸŸÄ#¢#ñ +Ø8dß>¡Ãò¡IWâ] äbá +<ðÁ/„0 ¬e7û¢^Q¾ +©¢| !Êž,4HÌpåK°Ðÿš÷‘Søã\éÌ—à°nÌœÈÁbU<‚²³Õ® +5 Ú¡ÁNO¸¸²zHné0¬¢£ÎNpÃGö%„Eð@˜ÆºðhÍnýeú£}PI~FÑg ;. í¨Äd˜ä,óá\qí0 ¼³~¸Ð–%Qœª5x(ãs@â£Úÿ×ã*>ê G½ðàáýðHΈ:th#S4¬\ê@£ÄŽ?%LŸ"„‡Ù9ÿˆª¨’‹YáúéÇà%NÊMJ?‚ “ƒGOyÉ}#€ ƒ‡<ðaÂÆÒqdúÀ‡.¢d‰¸ÚC#2lÚc™\I+:°¡…Gƒœ“ü»D8Á–0è`TjùøUÿïGâfv®Ø›xþ.µP–ŸD#(!N¨•€4.³¡ CC†ë<˜ÆÂdðPžá!wù>4Rnµßê£áÄÓ¼¡#>æçbN‹„‡ó[ÚÖ®$ ¡¨¾ˆ’ÈM3è³O;ÞÝb(']nò“¤DÝe’¼„Œ Žp&{–^±ŒoîÍ,|È œeoVogØ®:Š•ÂŠª´ ÇÜ^Hí¯^€y޼èÊÛÂ#:‚ƒœ×¹Ñ¡›dÚCÇÀAÀ€ f³o }´ÃhöiYHE2àÐRùÍ +Ñ,!–ÏÄŠ È°Ž¶ƒÇòÍÀXÖ2W_6nÚ«ÉÏG…SL~Þ.êTÒ}*¯Œúï5O´ÍõQ±‘.IŠ‘ëÐ1|Äïérâ׿æÉzÃ¥à ¾ùh£Òølˆm¤ªòó’°èD¸!#{À²‘ôrø> äÚA¯úÿÖ[@8û¿)ï:n ðPÖÁcVå&•ÀQL<òxÔK޼ 9>‡x” ¦GF‚â¿éãç-y‡q„ é¼Ñ Ý#€ „8ñ¬C¡ƒeèpÑG]ñùŠë/ý ù  liI«Ñ1|, ¼ðmgБxÆ@|¸(U§BYðñ!ððS©œpät?|Ô{Ö«FÞ⽃‡S%'}£#¸eÍ@˜ „:ð¡®ðÐСأx鵂x0_´uoéõ::ÂãðáœØH&‰ŽÅÃýDÓWbyS·“‘²ÛÚë3ØŽ:˜‡˜XÉê%iv ˆ» eD€:êóùƒiœ›$óv ¦>3þ]ª¡(? GT+?’ÄHb,×áCö|È?öoõ3(>XféýýðQ…æÓ]>Òú¡Î[íÿ ‚H¨'ûšþB4/÷NphÐÇ¿ïÇÂÆÐqøx¡ñ"C­ƒ`ºÑ®½±}m}¾F¾ü’E;º$Jn§ð:@Xàc˜kÅUÀ«ˆ8Fà.²2?y›µ ¶g9ßG•¡r²ü|$žÑ*æ œd0’™ e*e’>+Âî\>˜ÇN|üA™fùP¿½½ÄB¬j]ÁÕÝò@3à–¬äÀ Žzy€Äç@|ºÁ‡™ 2ì¦Òjmå±G5[rxŽîÞWzò¥AãHšIÆøXB\$ÄFlJ<;|„Lj›k+{¡Qt+¡&?‰ç]&±K •Ðð Cˆô9ðpõ a_ÖªŽZ©@|Ñ÷jŠçö‚`@äM ÐȈ +_´%¯>xàÉèÞ[Ì}×ÒÃðáö2`àƒ”úQ<ó¦óà¿;´Gõ#x<€@ƒd::†× +eÊÐ6bÙåCc%?±9¼âú#¿ƒ‰²ü|$ OØšá’‹÷Ôèv>.¿ÍE5¨Jj¦tJîø˜å>ª¿žq4{è+¾ºƒW»¹Ôœ"@,GÃG«‡Å#Å@̼vtè@Ãþ°Ûé”Ý#muðQžÖhG/Ùu•iˆü6:ð1bFâ‹õcY<6€hðX:ˆµü½âújʰ•åg#ጊL¸À°í°¡‘ôð 7¢#<„e³~0êeýPCx@,øPirËH8ø¨žZ:ÞäõÐH9]|à¯Ïf24´°Øq$˜GEö“RuÛhÞ`ðN£~ž÷‚GÒ$p$™@xÁt„‡øñãà ²Œð—ljë«*Ñ#{UùÙh±È¬nv…^úÐ!7ÙIS¶á1°ãáƒUÑÁHÎ*Ðácù³r**<ð‘Ô{ø°Zà!ÍŽòuò:/>|<7§Å‡9ÈdÖŽÔÔ° Šû®B[í~ÇÑçbáÓ“Þü&t|Âcÿ­ Z +NéL#d`Cƒ†¶âr-qþ£”åg$] â¶M.ø@HÉ…‡„ø4Æ‘åƒUᑱ¬U$S¸å£zÒà¡ÐªmýPxt„ÇEc¾—¹|Å zAƒ¼ÿð޶V‘ÉLhV\Ðëê'¥5ÒÆf?Ý£ΑÑCíèœáÈiò³á#B¤žú +|DHâ6´ ƒÇ„‹åiÖí¸½pô±ƒûzrÍþŒ$ —æu¡¯ÀÑAn݈õÑAðÈ?f*Ðàá÷ëðÐüòÝ]ð¡EˆÒë88:TÐ<«Ó!Þ>x8×Ü\>-}*=|(î¨hŒ[ý4<ÜýÛæáêÉyíKrÅÆ phËǬÌaPFqìN¶€D2¦s:ñý%õøyñ! 4»ÁCè$éè/@â#Á |áOË@ºÂâÃ… …<ÎòU¦ý¢ðƒÂš…£Aƒ‡WzÏÒQ|˜b?|˜0!#4꣧¨÷ø÷BLóà¥yØË½ißÞpÖ_ú yu¹SË8þ)KÐA ²~Ìò±t,ÄJÆÆU~³ý©ÊÏFÈ0ÜßÐŇöâC^ÁQ?€t‰taŒŸpŠgýpŒŸ—ås>D©DÉIícGw ñtúϼ.>:“ņÅã,-µÅckZSWCìü§uay´ï{Ô™ö4$Q<.ÉÅ2«Gn{øøGF1‹¸Æ@7g»b3·‡UŸ‘DD΢>’ùì#²Œ¹Ä—³€°ŒwyªH +x(äÖ¥>б|h˜Øáö™3€Cë ´‹Gtt>g t #Ó¢ã‘`.í¦7ø 3h½ËÒŽWžÍ«½Gž;|„ñ‡EÁ¡3 ®-ÒÂW[ât~óžÎŽÐñóâãRŒ5‘‹>-‘1’4@˜ðί8¶ë‡Ÿ<´|ÄÇóý­Ê:4¥bvìutÔ½R½ÑœÆ¹†ŽC áÜðX>„!Ž©&‰‹ž'¦ÍÑ ä=ÞH;’'á|1•­Ó<þöo}8½ÈR’Ÿ•„4‘½­N’Ý!¤¬ñÑ*úàq–Þ±T• ó!ÿ6ÿôØð±RõÃÇ`±»Ð˜mžI^ä­N@àˆêÜðˆt¼ØPÒ-*ML£·îK}ñÿ´w/Z²äÈy¥¥¾²)ÍH\œEêýŸt¾m÷ˆÈsšj-ª*£jÅvÀ¿0˜ýá‘™U]½•U§kh%3ÍtxbSS‡[·au¤ Òúè8ê} ‰ˆ©ØÜëëõ!ôD„Œ¼N‡Ë°Ò°%KJÿì¶÷‡`}$‘ºÂš:Ò‡@°? úèË1äzå±úi ¶š­ Ï„AG „6¨ƒêyø{Ö+S½×Æ·Œlê {Ó±Ö…/ì<«*¶ñdeim¬:às¡¬b6¡BÁ,¤TñŒH ùHËÛ7Gœåñùzi;as6h£bßô!b".â#ð…^øJQú8ú ɤß ~€j/ƒ}Ì×%¤íw6UY<æi£@#˜±™½DD <£¢×ÇþtJ~BÀÑG/¹öÓ„Jûû 4w9Ô£ yÚ8˜`ôÑëäÔa~‹Ð‡©26“GÎ86‹1?"9ÖÑ‹þ‹XCùJs™´_sêz“Ó4)…(¢ Ô1âXÈcô!b´¡ ESPö²âLÊ“<¦'-ïO–»Çß…ï¤N!v5Øiÿí±ùÓ …„(‰zÿn¡¥^ Ò'©ã!¢Œ>RÈ!5 ]R÷–'FKóÿõcüª#¨ƒ<8’‡UHâdQú‚[øxãÆ4ªáêƒ=[[Û3§6³Æ" +>-±ï¢ŽÑG›àQ}@\sâ?£Æ•Éù~øÁ¡iV¶—ÙÓ`ß +Da›Ñ…„ uD±Ò"« +±Hã„WGjÞÎ\ŒŸÜÍáÔ‘6о’ÿŸ‰#ì\DCTÈC˜ŠwùÙ·‡?V‘‡,ÒFÈlꀄK»Üƒ .úwA–.»·xÚo¶”E]æè«¥·GÒè°LâÀÊr·pˆSÒ)xzuž9¤ÞððÅA +7« —ˆ£C;xòz{˜!LgÒUHy¬>x9#q¼ã"«§­£yxÚµÆï$Šîáôæ±êŒ1ÕÎ*¾`ˆ¸ˆ yøz™²âø«ð ¡ZS‘„bG"êKÎ;+‹–«áïc’Fì„:”‘uøD¤ý§·—>"yÐG_/Rçý1òX}xÇꃕ|2 âð+Œ^]¸N!虞¥ŽH!æyÇȃ­•8ú7ŽpFÚþß40©«Êß©®ºî¢ÆC=~°Îmc«2Ô6õe/Œ^L +ÚPpä‘@ X£ +yˆ%qU¡=ˆ³ÏåpLZÞ¾ Ç5®òû ß/ûw€m'±ð¡!1*°$M dô!‡r)£ÔAÊ¥y'z ‰¨píÂ3 ÒGÚ€©ü-…<žâÕ‘6F“=®\Hט›×“xz^ Wm„y0gÌÜœ85b¦¯ +|^’Ç +bµø‚;ß/«Q¸ +oÐÄþ™ò";o7¸ÂÄq½?†k´acíÐ^aë"pë£P Xò¸¾_üöÒûCF!·Ô±ú …ÒÇIĵ¥û´áѕǥԑ8hoÕñ¢.”1䯤-£Äi.œz +Æèùí×hÇ+}Å0?0Ïl–ØŸO "CõhDäæí±êp€¡éAžÉy8sCæO¦Òµ C±!q,®æ´y­®¹h5®–8ÎÜk=å¹­Êb ú—EˆˆãŸŠ×ÔK)BëD{}œ2óp„37¾_‚Û뿽¬>RG[µéÔA«>£N‘yЇìù˜û°K*u¬>RG•n…tÐ…ºúpkñ”‡ j¤ þ}¦"Åì©ÃB³¤Ÿ{,“3þ\PÇÖL‰9'ÇÕbgP›k˜©Ýd«Ñ2& !),+"†  ÄP,ET)¶Õíúk*z±¿<á +Ƭ£ë4f+É#uØ ÝRˆ@ÌeñKôáµ/éÃï·>ø’ ò©Oc‚0•Á]ÂPS‡_]Œ3Ä1ê0­¯êu¬>|Êekòşȯ1jNN«cœ35y_i Õ‘F%ƒkæe.y¨{ÌÒpÓy{¤Ðut*ñJâ'ˆ^*D63Ð…€IDì¼|¡‹¸=õö8ØG»²A[´×öìý!ÉC˜…uÞB)Wôñüþð©—ܸô1ɧ„U¤:ÕH‡wšÊ’Ǩc^Ô‘<Î/.VNXH?ßÔΔ'ã1ìóö€Ç$ƒY¿âh. R‡êÄiÔÁÑÆ‰C¨µô±iˆ·ÇÈC?íN],v@ôA°U<éCd lqóõB ’æýqòÿù‰¡Ÿ>†ˆôc~½Xa n éƒBúr1²·¡]òH +êø«JÛü9"ø#UqšÓáëWºîiPYƒÍR{²oÞ6öŒ«Ãv²g´©,"$¾aHCÁ| Z_0>_É£ú@„WáFrÞŽò—<Îí¥-Q}ؤ½Úqúœ‹yˆ³Œ‘}Ȥ¼¬®@¼ .ø ;…DÍ¥ŽZç—8h£×‡‘©&3eÚ¸ÿjÚÛâéc2u¥ª£ò€§Ù›ã 2Hõ2=%žõáäÆm#2ƒ™šBBD‚•:ŠÜ¥ôQ…芯PÇØÉÀ0Y‘œw€'ÇF G¶`veg6h£XyЇˆ[A*†>Ç·>äðèƒ@¼A(D®G ›vÔ°ïFaÂÝžËA£3aßÔô«Rf‰ãˆÄJUGpP¾ÔiëÜÆqáYƒž˜iÌÛú‚‹Ý¯faĽ YGŒ(¥ o£úC±QQ­¤oWE?&!¹yräÔKÜ·ò ?ÙyŒ@ì[èC46¶RšVé£÷Ç¿ž¿®CŠ!Û^ 2?êHÇf4ʨÃÛÃS^q\ò ·}{Їwu¤«îëÃßKqK¢¦ò’ŸêÓNçÜMŒi„jð ó{˜ÔΚ»¦Voñ˜ûÿýÿéÿö×Ðo6$%u4ÑKËA`UAN1‘—€>©“ +™y¸Âüb^óÜÈc±¿#{¶w‘a¦ }¤ò˜ÿrÈC“êQÇÿ*ÿ„0¬(ØÓ*äã¡¥a'û·Ô‘>®/ê° ,͹⠤i²N );8¬\ì½yª2êXv.³š|§§ E'º¥ÎÑ £gsyŒ@ê*¬Åv˜FÀ} 9¸‘›w`åqÁGÞ‚6@ñÌʃ@`Ͻ@&â)R"–’Çè#lÂî,mšÀËWž®¸-“×£^„†%˜Èdac¯¸Ô5·ž0d1 Zåüè‘4æ¿Âž>ȃ@ÄsÞ(Ð>q‡,ʊܼ¹á /9Ëoò¶{ºõa§v Ý(j>VôQ¾RGòÀ¿Ê©ÄÊ/(G!òO)&FìâF¬<¨£Açíá›Ê×K?z$Œ<¬Šñà¤IŠ&Iñ·ù׸ä*X^Ïù>ÉÚk¨‘‹”›mJó6{åë‡yº +3˜þðaµ‰]L(…T\'ºƒH‹¸€«}ìÿFrÞ®\žyçÚÓ%K  Æ>p0I¢áåýA^éãRÇÑGªÈ(sè‡âyiyPG¼¨Cñ­–<¤O6%¨ñ+rPuwõxï¶G;*“߃=™­24¹²«èvç¾Æ˜0‹©L,@Q¨Äk<äêƒBí +c£¾IXRóýðƒX/§8|iØ‹MQHØ©…ˆ‚hŠ‰Üˆ´=äA2K«/ñé£Ýø’„V‰±îxŽ>ˆ#V Þf£Ž‡>¬v«£|qeÔ\ã_ðS¢^pÁånxÈ£†·ˆ šAÛdjå»N¶öÐCÑ0Ó˜ÈÄŠbRØâ(¢X¦-ÞEœ>þàëXó.ðåJ8;Þr8Vèb£vÝæÅA4&‚BöÇ„BèÃ?à}Ì÷D—wù%(ÓÞT÷n<;l +/âÀÈ£ù­b9‹¦ùÇôÈ"á‡þgÒŽ(aqµÐí‘C#g¸D·)³]¶Þ´Tœ‹îzjš–‡¹Ì¼ˆ•xÁ{·"~Â览m!F½›•:ªïOÐWÞ¸Xå4ß{¢ŽÁfm\dW‘<èC²d$âçG?(øÀËí­y¯R ‡Ÿàî’<ŒB/u‡™‰ãȃ8¨“¨ò*= +’FpõÁxWÇ›tšij݃Ð:è› sæÞÐÒƒ•[Ú仈x‰˜zÿ˧£ê‘òª¸ËÅļ|y‚“—¶ÛÈÊãÖ‡M‹¬ˆ†Èu¤êyÈßü},Pˆ$Ë4q@ê©À±ôÖè«fq³Øäñ¬ó˜Œ>LÜü Ää1úàÂ&ŠGÒsÁM‡ŸpAiêbÜ æêTW7ÎÇÉÃÊV¾¸E}»ˆéÈ#SMU_yŒõi}¼7–ñ£ü‰ØlŠ<”ùë‡*ÔbCɃ> uô!}¿P‡’B$‰È»ü¯HÒUhõ¶™›ñdyÌOðåâkyÐ媣TåÍÉä” /ð{ñyëÁ£ Û2¡)5uÚe’P2WÝÌÕÚÕYÚkAä„Ï#¥c¥!âä!öâ/ƒ¬HÎÀî¬_ã!où}ëãO´¡x{ÀæÅàŠ£HÝ‘7¹“A?}¤‰í 2}Œ@ [}8àQ#FäæZmŒ:ȃ:`áÞóóŸ†+CÜšîÔaÜÝFQõ¶ W‡”QÚmÄ¡\´H…(Ô9tÏu¦¢ï6‡5†~ðêè Apo„âßù-Éyxò€¯à÷ʃ:†UGµ}‡°(i’,I“º K¼úó ‚C‚P©ct_õðbèȃ8´Ñ«Éü«kZxR6p‡W|s€«¡W_Ý2Ú¸®{pþú +¦x¤(ÈÙùzQdóèC‘ëÕˆÔ¯@<Ÿº™4ª7Ìx“ô“)}Pݨ#}È/ÈK™™ÔKMÉ©£ÐCœæÂcnˆœöçÁD¾bØŒ^B8è]+bžø[}­ëë寅 GcEU\—>…uÈB ¸ðsá{À—7‡¼VmäzؤíNHES<ŠN¤úðò8Ù›¯òÚç‚~®ðBÄpst=^«ˆ/¼ªG £‹µ u0oo†¼¤ùýVK«$’<Û.ññõB2‰óúH «©¦y—ü1Äv²ÕôÑCa MO55ûÿì{ÌbiÕQ¸2ÉáÛÀIž¾ O,SSµ“óô3˜ÅT3ŸŒ#SµLBQ²©\×v½0û lä! +¦ˆ +­~AOÈÀ ò"9ßOŽ$ÜÊq ¶bC^Œ66^‹˜P)# ó#Á¼?(äÒ‡üBªˆNçå ÃbšªhÃs +u(ñüö KXŠ<`eúÈõŸûzà÷ò‘§“9¬Ùª·?ôü¡ñ£*Ì8û<$ +kÕèÀ—KÌ3š3Ê&¾°–Ø  ß £ÃŸ×é#q¤QBÓ¿³ËÎ÷Ã;ò°%êPÄT0}aCHDJ‘$é?Â#wÔñ?¼@’‡2 +IŠwƒ#V +WK½QG4Ä@˜¯êPÂ’Ä¡”¤IÌ@JªÁaÔ*ÕºìT÷±ÚR±1ú¨²êNÊV†VKWu£›qšþ±íb^ EÂP†‰&Hƒ>èBeÊGŸYÇ›Àê¸õ1ð¶mpcjç’‡·½×Gé#ïyƒÈìÑGòèK„ ­ž#æÔ#£%Φ0Sñ½µú …:>d ’Q®•馀‹GO÷œÈcžÖaÌ3MöL+¨q­8¸Ózhð·Rƒ4‚8 +éJä Ö¢­^™ž¹ùvxáx¶®uØ…ýØ}ت7²‡ˆ†à¤Éûƒ>BGóçSUŠˆÄ£ü'ƒCB9Œjþíß)#mÀ°‡’:‡:ò Ū/Y\©ÈèSj&9œþ»tߎ.iolñûm»Vg®¿``35ÿƒÑÆQ†  +털Hn¡Ÿ/”•Å"5Òó½PŽÑ'/8ß.li°Å‰¦ýoìhB£ˆ_/FÞRëgËäá ƒ2äž^é§¡iôê £ÃË#}GS'¯5y$Nœ$%²Žòö?”GÌsk¤ñcì¦|Á2Öº¨ïâpwø0ƒM3sf¬C™ dñT)„FÈc™¶ËCh CyBâjCz¾“

ð‹¾!ÈCuÐ!¨kB‡h†yf¥Ã>L8ò8¯Qy$ üáßx7L“Çã9îöê {âÁ‹Æ™£yT"Y®6R„[ÕÁ¾bŒq˜¹­Ô‘-¦Z²H?‡¸Úï—}\š9I$ïíb¶µQ´q€Èȼ>®//~?Œ@ÈCfW“p©‡ì“B%QD'SÝc†F9$ÑÇQ‡eh#uŒ>VrÄ+)‘?tÈOÙGwÓŽ¶ž_cP“<eT™i†V´¬Ž¶[–wVG5 ¦º°Âetå1®` ½ØSFÇRô½¼ªcLNs}°£Ë;a(6ÒCÊÊcôA—>d7ˆ#ä}ôAddÁ`ŒïŸÁ£ƒ‘ágÏäVõ,š>¨WVJ.K4gÿü'?p gÊPëLOw˜çK¤º˜ÌlgZ¶e2t÷3GÅHÅqa‰h9kZâzCrðŠä|?ü8žå¢×ÿ¾’>þhgA;‚œQˆ4¥BH eôˆ\SG‡’†Q†ªÌ5Ê8 +ñ4 $Žy{˜ÌœÔ«xxa £¾”ù˜„𓣪ö‘vKl¢œhnˆƒ5t˜©Lz1+ÔèXÏ[ÃÂp}¹û;|1£™gî–Π˜Æ¦ñòo½\\} úV¸0p/;‡{ÚŒm bóñ9òòØ¿†RyHl9N! +y¬èae°®¸ºôÔ û7éd1*‰®¹ƒžUWf€Ù@ÔAýða=‹Ò†÷ÇàÙ¨ÎGt³Q•†Œcíî*ãŠâ©x¶&1Ó6å|*ëÌ¡3̺ÔÑ¿fâÞ)†h§Ì2ÅÜkŽ„û þ?âãûÍp 7ˆCeƒß° [‚ Ú'l^L¼>ˆ,¥ŽK~D%r²ß0å]òG¤Pyàšs&’ÆþFˆyLÖ¤¦n…S-›@Ò‡…THƒ²®æ²šÿ¶µÕ$GxÄ€Œ:=ƒÃLLLÖ•ƒÞ@UfqöFšÁx3¨Íб«Zyh\í|Fÿ åsæDо¾Ts†—çû¨ûüö/%D`ÒÇ«B¼> •TÙ•gò€Ì—}•@Ê€f.Tÿ•4ȃéݡƈ££—uôårÄáËe^|á×P6ÆÙ­ƒ-Üœ³ìéjã<­™_äM3˜Ò~×L3‡bEëBã^°®;nŒ7’œÙ­ÈÔ܈roê©éb…gÞC—[$üVa;Ø£-od&4}H˜´ƒ>D.%uä©.í²Õ‚cX§Þµ~P! %iTÇ@r „:Òi ¾]¼?¨ƒO²Á9ùàèÀåú6À}•i?šíÄœhöÑÃ$fÌûÄuÞše®]£4µ¹5Vtܶ¨k|4‹þä`8 …|3\x†“ÁwÛ87°»y\!!HPÙ_`‚>ȪüJ3¼(€¨ v8Ú¨ÆþØAªQrô‘Þh#ÌOô¡þéo<áM~Ñîd„«âÏ]Öþ#ºï©ž|Å4¶kÃô`Úe:> ì_þlÑÑȘ½v豆iýÌf˜þŠdÅ-C1NZGq/úÓÐ +3U‚¾~<à.o¹.j*»ûkǯUâ€É”7ˆÌ‡ùLô!ËvªPÔ=Ym¸à¾JÞ5ƒ‘Gf2˜ÚäA-š8xÀÙÙüHÄIÅx=ÍEÛ‰i™ÙßìŽyeRzÕM4t§ã[ÒÅÐòŠ{n©uÎ ƒyÃrò%ÿPßÁ‰¨© +ýòèцÊ~3p½;Ll'µ­`¡hý›é +ºØýÊóéýnxqûÅYØ„ÝØ’6Ùæ1ò¨’‡\ù0HïøœH¥_M½BÒiD!EW˜3§‰âàyò0ÄÞ Ô¡ZpôÑklR$we…—ù v6°Í&àÂù¹0·ž0z؉X9VšúÂß:·¤Š[ ëÓÃÌÁõ5¬cu sÀêLzEðUœæ¤’<@?áVGõ CÃ<Ñœ©ƒ6ΫÞéƒ'Á1ˆ½òBÛP™iê¼à´[5;XmÃõ†¦UÇT7 +Ø5×Ý=1È3Ž&SÔ¥å†ÓŽ^…}¸;æàGpn”ìà»­`¶e“°{†Ï-uH‘L­: ƒô‘d2ÐÇ(¤\ˆ¬Ë<‘:²@gázŃ=ܨ…:ÒÇþ›Èf·iL±°ß\F¼‘%ø9e*ÿ…}8iÆ\Ìc]"«N° ©Î¦Àbþh~óÔ]<¡ânÊMl-ˆ²u§ A/ðò°íAjÞž\ès7Ï£­ÕµMØ~ñ+uŒ>Rˆ´IÝD.éƒ÷…¯ûß~+Xg˜EçÒßÁóØÆèJµcXU®OâjÃo3yyøÁ¥ÃÖöiÏ¢"#!;´ÙJ ²7ìÿüÑB¢%|HTp±Ò`çªûÃ>ëÕA —:ÓŽB,5Œ>æÝ¡rjáfˆüI¨?â>nî~ .±_0›ÉÀ(CЍ‰=q¸ât:šÁ3Q§¡g²e¾Rò2Z˜'€îäA;”õ-XÏ‚Ó{óçHÛì8úŸÞ‹>Ñ2çÒè¡O»ˆ"Í +äÜ;¡AÔ=Ѓ +o3È#ytøÍ…8ƒ+àոƜ>ñ‡ÖvŽ}bÎÏÕ±õ{ú0³„ÝKì´aSÀv©VíØshÇŽ«Ifʧ…¦¯Xþæ>Ù|ÈÌ;°ÞŒAÏVìÉÆ;0òè !MÔq½@ä8”Ez/y”wJ ƒ-´¡0Y§ƒ{4¼9.i˜nŽ£JôêˆG5wƱîr:·óßFbÚÛôF•©Î÷©1Ycºf©˜Ò1ÌcÖN\4ͩѥ±Æ$•,hÝ5k3UlNäæ àÈx3ð›ï°Ýœ­ÊÂuÐÇ eäÑ!‹åsBÿ½ÿ¿äH!¸µ}~Z¨ +4ASš‘:Ž8ânŒ6”ÜäîÅeè)S±ÍØ1¶ÝÓÓ× ~a3gJͬ Äi~`®É—Ó3cúVšbÍàİçɉ¼¼|âÞ:hí¢Ö!!)}n{ytÈ—´Aþ¼@F#²J^-ãrÿ$„IDãæƒU‡¡aª¥‰ÿöÏÖHae> \‚†ƒò sV«±‘V±'ØsOe^ébCU;ß¶9›úAïÔåjŸ¸.!6\½ +\´Rë=Ðß²ðRRÞåQ¬‡6Qµ‘vj—åa™ìÈS™"þ=!É#Ñü”ê’@RHŒÈa :Õw+nð/ìâ3%¦aìÏG´~'Îêê„£Ú;3˜þB–[Í­Œº¶:L§›=fèÅœTÍÝa›§ÿˆ.¿ÞIüQx©F[áòlÆ^“‡\(“›ó;.}€DˆCqLf½=Ⱥü“Bv˜ŽR'zÆCž=<ò@3ªGÉêVÎn$Ž }{ŒÉíö±6¦™í±N¶ûʹf'È´ÿĄ̊Q·ìÍéÝFqyMÇÆÜX-vá9ËÜ×!-own8ulÉ^Ƀ@4Í _ô±HcÉœÄÊ0äz’_ú©(tãjOT–‘•¡æ€o“yŒ:Kÿù¯<8pŠ@–¼]lâöâÓF÷±3ƒ¾²Ø¼ÉÆÌ'FwlÔ©h•:ªÒé©ÙLw+#ÝÅä0kÆå9Y¶-#Òò6ðÆ‘wùgaW¶7êHÒqA2È]І\î+d"Ûƒä@ªP0vŒËf€a«Ž fž7Ç`ÍYš§ˆC Î^Ø‚M8ÚIÆé ¶Ú~»^¿ŽÌK}5ÀþÙÝu(Lau÷Л&«ù¯¬½èLUЛÙýkGõp®"ï¤åm—.lÓ>ªHRÀJÈä!SiD +á+FN¥R,×’ž8@·,à¼ëdáyƒF3 m¤Xc´aµ‘†»ò)›ì(C!V¦®îÜû‘Ÿ_\šü'XŽtæ!e!çVÇîl:Êñ^vÛ.;ÜQ˜w"ÔÅ.ì{ å¦ ©©RH8‰U˳´_ŒFz®£ +è̸™Álécb _.­iiË/”1pO2ÔÅ.ýÍǵɽ¾ö?Àd÷¬§Jµ»çö¬=ÕÉxàêΧ¢¥Ùåjut•ñRNÞ NñsJÆ&6?oȈ´„Ÿ¨ƒ>V!RH +J.$z2¾ÐBz¨ÔêdÑôä`œ±f0 ³[Âbé£jý…K#ꘃÛã¸Ä݉Ùaß(ÞÛz8Í¡g `îídqw„C…ï<]¿™OÌù˜s›ü…¦Ò,VWÞK"ù£yjKÕbŸ@F!¥D ÉSÈÜÊãˆÔN’U —ý0%Y<ã’'<£3Êð´1˜uåa!‹ÁÊ­>äQxþw°;ex’Èé O}ñ0•ų̀^}‰¬ªL½,¦3îÜê¨ÊTSý„ûjúµ¼Ê‡7‚K’ªM uì5dâBrB®@óÓ£<Æ*d4"ÝHÇhŽ<ô˜¹Ä‚*f1¼’:fòÁbGéâÈ‘…sà©TŒÇmÀ>^±»bÎ8ýÚØmÏ eêMfÕéšý+V}æ>ÝÎZ#M¢ÙŽÂÔæÇã§ÓuMVÞþ<°Gk!Cƒ|͇š8”eõQ†K¶”GuFÚ-×CZC[5Ò´¤Ñ*1kzohóCuðñ†ïü[õàBgjÛœÆé°`íOÌæ­e¾r.qByâõÌðÐ*Ç`¦T:Êž¾ãÆØNãIxè£üHÒÓ÷‹ú ‡¤JkÈ5ÜÐVOÌ?jñàh#qPGhÌcBSÃ:–sP† +êÈ!Žm"Öò8ïg1§{¶“ݽ®'Ÿ®Ýݽgþ/gWªÊOh°Ênó`Ï-´}Ö)ßÞ n=°{f°á´¡N>.dH¦Îëµ²%ûoð)<ã´òB3ÔaªT瀵(d™Å5yµäehx¿\ߘÝÙ㸼ÌÃÊÍþpñ¸c‘¸Ú?ÞW–׳N¹ôý„‹¾Tf‘1pé†sïŸ.ÚP[« ÚƒUÇZÙŒAòù<”]¤ïJø;¸å‰Å³ÿ͈3tÔÓÒF*„©ÂNp…WÄks9Îvælº›üBUœf¹fy\ž ÌÜaZÈ—íúAOÁØÌŽjªp‹}°g­É·÷ƒ[—б±  Ee.$hþILëÉ uhe4¤WÂÕ ²«…ÇÚÎÕ´ê0ÈX3ÄÌfR¤‹©ÖÀ£>ÈŒtœ v%àÊÏØ[=¡{ÆÄ\‰&R`…‹Î¦t±ÊLuæÆX78eËÕ.}1*|zG8ö`÷ñ§ i€¤ÐE)ªJ—O5m(iÃ7|J*$Z¶Ë"-Lç´KÏUÔl#o"Ç:ZË’øq\âù™ÃÊÔ×­œ3;üWÝÂi~BV‡É¬Vœ2®Ÿ;îMͺYÍt¢þÀ|»M­<{3rÊq‘³ö‡0iXy,“¥~D$rÓaÒ’~X=¬­Õ›{Œ§XíJ#YduX%t,ÜÿX.Ôu0ò—ÓÙ>[ÒsèeBO0UÖñ3ÌÚ͘®%†VY®+.è)O<™ãqvz׈6€ñ‰y?8‡ÜG¡7ÈB™ÈJ 6I,ä ò(››TGH¶¬«R¯ 4¡tZwGJÕÁ8cÍqcvXiÞYì?8Ú(¸ÜåôWæšíý”7vøé$_/¾ž…+gªØ w«Qâ4ÑãÕ;oÊøX}ö]dV›iyB²æ "}£An¹–ï ƒ‹¿],ƶ+˜M¼Ð…•Ks§ÿÛzGåpüÕÜÌ~þ± Ï³Õì¶š:f|õ ï)6Χ©ózç¶ +²ó@ØÖŠo +çnøª0a®äk2&sÒ7úø ihØCé~@%d5ØRÅ]ÌðD_/»Ì`e⨼»XwÙálG¸;þ<ý•ǵ{ÞŽÅã!æ +®ö™½9ÿyÓí5T»\yOøãû" +‡ý¤ˆÜ0‹t-ä‘@âP/…È|©_)ÐDhµËnj•5ÆÃ„ŽÁ„aXœ¸ç„þ>ìãi t›ûðàRÏiEï0Ù¬ùß0Œ‰Ó©ÉGÅ¿~ðÕ";ñ±Ú÷…s| î6A•¡¼”žE¾B +eó¢û’9ɉ§€2 ãš‹ÏO7Õ0ãÐÆ8ËY6'ÆŒ:rõ*cÚÊ2[û‡ð䔌FɘOft>7tÏ¥½5&¦3×ÎsuŸy=·nèÕ­¾)—ƒÚ٠ئÈ€*Ø´ÈÑùzäq‘X”b‰&G6(b^#L}W÷:œ…Q†LFa~´`Ö/.\Pÿ ®oœ çóVYÔØMØÕ?Â<¹Õx¥cæ Ý›FõðÔûôî y”à;˜U9¦¥ùñÖðò8‹+ôä±”HVHØ…<‡r%X¾€>UP‚šL´®¢>KOF6³˜Ì„ƒÉëaðd~<%³‡íN +ìÃŽþÏhˆÚ¡£»œ%ÆhNûŠ‹¦sñ³~3NÌ`¡Vbð7ß;N¥AR#S-S²vãK@Jeø y†NnöÄÓ+ ŠSƒ(žáxÄ+žqU™ºŒ:jþ“A7ƒ9ž™yn=Nõ”©ŸÞk+ýfàò¡íˆ½èo¤äBЉóÚÇ⳿ÉJ{\-¼5R {¸ÒpÑÿ‹ÅÚÑFp3gs½ª»íÿð›GL˜y\°RìÚµ‡ÓqgK¦ÓL1“%~Kpúà‡v²ãC|˜ÿÖ㥒ÒYŽIÿ—KOò˜®1‹ï“]˜¿E”YÕâ‡ÑG•{\ô;±‹Ì/Å#DÓscùŸ`Ôï±›k“b/ø!ed‘¤aYdew¨#å1¹Â{do=߸UäÁ æ~`å«ŒÃ¸ÊøI‘ûz¬ÍüB˜ÛZ8Íi³–žú5Æ÷»`¶6›:ò˜|ÈËd)#aK),•™‡Bh`˸õ¸<=O?AÓUp-eÑeþÉ\eœËðs¸;åäc&·ÄaOÖ¨ËöÆ®ùݰQóFø#‰<4iÂ&±ÊHlœ†NaŽ=‚pŒÕ¸|0œ0´ÓkbæbV³0O†:U~æäÁY? ´ ;ù…h™¾E,:¶ÓŽaš.œ2õ÷A{»Ã=]!_¤C‚.JML¹Llz¥»ôW“-dÁjõC#V!‹Š ¸´hUð$;íøÆMÔTÃ~ÁŒ˜Ûì»Ó¡ÝÞ0ר©Ïþ.°›Gœ§'rQ™2¥ ü)U¹¤·"Ñe^'êRÎ…'<ÎÄZ˜,VVÒËXkµ:œã ?1ÿà|RQó‹búŠeÔéMqÔª¡Ñe§ù`#6ôŠ\ Û£ŠRUÊ6‹q2)µ£‰Ûzk\7†®wá(cìs :ÞIc|€³àÞ+v`~!Š-¨7'%åG‚G:Œ*>ù›à²þÊ}¥N¸èüÏé_†¿I1KXÉ».?pµ<ãßóõÛ>ü2]µØ3zp"'jHÒ…â4Ä¡”ÚEÊ+˜îWzbÑqÒÀ†ª•,µ´8púx‘ròf?übˆ®8_ÂP‘˜ÉIc)u§ÜÈñRêUÉßvz`;\fèj¤¾§XÅìê.ʃ%Ñòk½{†û¶ñá¢øž¨ ©ˆM[žÔÉšTJ£¾f‘ß²cþü§m»FZêÚ¡qÔ‘3¡fh9+·öUxŵ §,i|ÄñK#Æ'ا‘ˆÕ…,ÍÿLäs¢'‹<lþÇŽA­ªL½0¨róÇþï¹Ì;èXĺê2Nq¹nƒµ‡¿b,Þñ ¹ùQ‡Òõ„üÝê(É¥{Ä¡„ èÐ-w=Ô°¯˜§iQŒ¸åÈnøÿá—¥Oàʼn»Düáú×@âEW_B/¤;ÖÞtzÕg:W»!Š#‹ «ÌÂ7¼R_Èqøð‹"Æ!üêR‚;A%,Nò:L¦i—uÎÙ›/'ûü…‰f2SƬ`Å‹¾à~§Çó¿4B}C(÷9ELb ['eWýã²ÍhF•o0[$?ãXêÕßêéFÍ8F©,q´*N‡+dzš«ÿÑǯ€0C +Uз‘•gÊRÉärê@ ŽS)™$ééº{alh‰Dç`¡a^VÔðFQµÙà¸ïÆ¿4B-ìWÔŸqQ†.äMò •ÊP^ÿ Ó›vFQ³L Ià¹?]JZªy23;ÓB# +gg¸ãx"ǹÿáF”Eúdc“Ã*Sß,$!“šÙFéW0¶szЯtÓÀéÁ,qqõ¸‚qrûU>ü*´h/Eþ>“ çåI¦Ê]²ÐÿJ9EœÞKSÑ*[ãÉ;Æ +*C_àJè8Íí¿ +ÅZÌDf^)yepkLfeW§;ÍO®ƺw³3Zd˜Îñ`ÔQ= žø †•‰©q7òs²ù»Ê"ÁÊÔR?iœDgúÐc:f¦æ…ÆÏ´øV®¼’Ïãõ‡_QvTp§â¿lbXœåP•ØÍê Ñ`¥}Ð’ÀÒu¿;GœË‹©šËÄ1Ká +£¯áÐí ]>øU(ÐBÿBÉ¢›+‰²éxf²=f + \f8Í 3Ù˜šW­_ù>øòa“±6dêY"Òx!·Ï”ûê ú3LçP¯úLç¦ÚþxÿO;»bmèx¨ï;púÿ É­¸s7:‘Û¥,ûøO3†õö  Ç^x¡Ñ‡“ŸÐ²1ÍÃpR9ðùïF¿Ò¢‹íO#c?a²+¿2¬ùJÊPõp´óFÛ\Ûž•4³n|m¹Ç[h>üzˆøW6%¬”ÅtÿTFFeuš*ü myä0;1†i@=Ãv›½5£¦z be:àó‡_!ž²™ÊN£=?!ô×oEÆ*ªã$¦’ÇpšÏ*L“MG™jþÖP.tÏÕq0ÃWÞª~MÊÁV)ØwGW_ +¥³ +)Ö«\¤ˆs¦»o‘jôàruž.Y£ÊXðàR†W +j§óá×CÈÿ>ç®Ì6Ÿ¬ò@žGþ{=A2CMÛ˜{:VyÅÌÀKž~ø„^ +ÈÉüÁßVk)tloSêD +ð£G¢p'³5N롯í»eØ©\U,ò÷ïøùá{}áß4\ÌIfR'‡Žú( ¦e¶ «*D¡ñèpwnš¾U–iÇøùá{ý¸Ú‹—|­,æTs²;ù è*S×*LW_¸ÎM&6ù01ãU.:>|›…ÌɉF¹Ø®42leÙ_§d਀¶SwûW=4Lmæà€‚ùmE¹™>/?| ò€RÆj”cB§‹Óê}Eê ™¹_gðºé¨0îé(=e¾¸Zn ÿÁ?%ÔœKœüð­”õ‰>ÔË$êb3û TweÚÁIgÊOH Cnk:õÁëÙÀ·ßÏCr¤\ìå¹ óòûó$+j§©czµö!ÆgºÈ·ßΤ"äê‰9›Ÿ n$¶«ìƒsFÊ…/•s}¨¯šmãïp~²h~}x $¤|lgò×™‚½¹g²­©hÕŸâ†GŽ©zœê‚é’ÞaºRÀ¡ïÀä‚yA®”cî¦V™º&+å_˜wI7ìck\bç͇·Cb(dëÅ2Ü¿fÈøu}Z§úèf/=·¾Ð%µ‰4¨W{ø¼;ÞY9å‚"J£ªDÍU»Ë¨¤ aˆÂ¡ìkϤÌwÊv«çÎÀ‡œ`¶ÿὕÍKŠ úó·´­l§î+£ 7µ¡‘.¸y=¾ý«Áô2Õ¡[]‡Þ 9‘™E†p'n˜KثǦLskBYvˆÓs]ÚÞƒ9wã,|øðnÈ ´½›I j«Øæzª3Uù“ÇÏi‚릾ÂXýÃ;³yº7½+ÇócÇÜ:·ÝTœªƒ³y¯Ì…ÆN'îŽÛ~×uËÊ~#H¬Œ=ñzvq%ùjïç® W»$ŸÒu«~øm Y¶¶Š×D?ñ“O—Ýiõ -3$E'ÖýðAÖâkw¦÷KCUÀ*X{5Ã>ºf5°Ï¸òá7ƒ„mþ2j—&¿3øøù¯⢂4bžØN3êh˜i>üf±-uu^(áÕ›¯¼ÐŸÓéd¶,VüðFeqüHêÒ%Uyºuwž˜'nLúáwƒ„nFµCª™œëŸ¬‹,æÖE'×è¬êôÃï…*¥*;͘ '•êÛžrLÔ™;ªY?ü^(¡*3å2{ÝP3šŒÞƒsZÍšóÃï)U‡Ÿ´ +³8a0§uÑ¡èOûáw‡ÄJ.¤·N.:qQ«SëxÐEW²>|øðáÇ>|øðáÇ>|øðáÇ>|øðáÇ>|øðáÇ>|øðáÇ>|øðáÇ>ü#üÿ™r8˜ +endstream +endobj + +4 0 obj + 74637 +endobj + +5 0 obj + << /Type /XObject + /Subtype /Image + /BitsPerComponent 8 + /Length 6 0 R + /Height 676 + /SMask 3 0 R + /Width 544 + /ColorSpace /DeviceRGB + /Filter [ /FlateDecode ] + >> +stream +xìÝM²-K²Ui­'$?‚Ba¨Q¤Þ‰èõ<óKi*¶}ïs3ùÞãÜ6 +S†ªÙZ{¹¹õˆ„ªËår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëåòûò}ãïo¾ÈždˆO‚|gú?Êår¹\þ,˜ÛˆÈA‰ˆ |P)'Ãé'g?ž ßår¹\.¿?Ú2Ä“ƒ2Ä%F†ï«ßÑAäLpúår¹\~gLì_‘Õ31r¢ òØâòWÌj$‰ˆ¼\.—ËoÎŒk8ȯÈjrx•˜Î8F”‘$FËår¹üþ˜Øáåk­::J9œK˜UýÕT“"ëÛ¶øä0eD†Ó/—ËåòÏ‚Ñz2‘¿‡ï›ÏÎ8 dX&F`Iž0%È:VeøÞ J“錀_.—Ëå1-d–F”/‘ ¦3VO~2øêÿΉ„rm>"«ûD¯òÄFNÒ” _.—ËåG IŒüˆU¼D†øÿg†¸ Þu”AëëÒøˆ IÔnŽ„,…µ]_†øäÉtNÁw¹\.—Ëw2$“ƒ2pA òB3ð_q®ŽgìGä̆j JÎ~øÕêx$‰ˆ  r2r¹\.—ïdBJŒü1¶d†Ç‘«ûàˆ¬ýv8SSÆA?Ig6ƒ¤LB$Žõí®^”ÕKˆèÈ–™¹\.—ËÌ$ˆH“ ^}DÖÎI¥ éL®½3ðul:õÓrær5Õ¤“\ýYúVFdHG†q²z3 §_.—Ë?&™„ò;Ó$AIbõ€ D$eHd5qµ‡|PJ|—ÍÀóññÕ_8hbΦ´$¤¯2(‡WùÂ*HàáôËårù³c¦a#/¦O™”ëì:±MÆ_GS®]>¤Lž¤3œÄȉã%>²¶HÀA@%F0IÊËårùSc” )“¿ÂjˆOâ%2pËÕðd:$(“Õ3¼Ž%(|õGÈ Ô¬œÆ#ëØ” CJ;å ¬¯ý”“ áôáÇ&Ò——Ëåòg'ÓLbä;¤Ù A@@†³<Ý7€¤)W—²vgòÅ4åêïA¼6:SÆ“µËêýœ€„¸\M\òÚ(™>GD‚€€„q‚Hòr¹\þ2d¬ÉŸ ñd_=~_èƒÀ†jÖ–`)èËá\âk¯r‰‘Y"Õð$"–$RÊA‰‘2‰H«©fõ÷KT£Î2.Aì—ñÀ1r¹\. αÆgú­›|Dž¤#ãâIß9™ŽÄHP;©î$%Hx9Ny‘æ$ÈÉùGA  2LIjo SŽØ#•áôËårùm1¬ùž X=CšÉš >%yà!}Úý‘AgPžœ„$Œ“j¸Äjª; XM}kòäI:ÙPýâòûRJD’ÁG$j£3(A‘—Ëåò;cR…ÓÄè“Áf4ÇA†ô“CöœÍÓƒVï”Õˆ<=©ƒÕ¤”§øÚß,±$p«k–œYÝ |X½´:g‰èÈ×_[@~…ÕËårùÝÈtš"YMS¬½sµ¥DD‚€\.—Ë4¼Ê  xà?ràS?íŸæøêýkçPMö„øj8"ÕKÕ‰Ú¬Þ©N¥DuYû›Ã:¨fmAvNZ’P®&¢¬D¾'"6®ÆG‚òr¹\~2”’ CÊ$"23ðLýôƒÎ:&í ƒ‘õk²gmªêoû›f<‚‘êV³ZÖZ>™m¨ã«ªWëHTo9Ñ ñd5duŸH¬½‘äê~PJËårù­0šÂ¯)“ÇÈêÑwJàa܆j¸ «›: &¿ˆ«©–:Õ¬¯²šj©fµøþˆo— ¨½mu†ÕÔþì Ô\-!eõÎaJK :«eБšÕ‚‘! r¢s¹\.ÿ[0‚Âé'&›üqU¶U;‘£ºxàÃì‘ 8›k£”Õ¬¦úu צÏþÁNDlQV¤6«©fmª±?©ó#þ"HõGD®n®NhÊpúêé¬þæ¤2ðt RJËårù×!3G¾Ð9™ÎøÚ#.¤ÔGdõœ\ÐY»èLK«·­¯TwÌù‰Ú¬n"’ÔI)«wÖFdõDdu³:ƒþYúµ2pKÃj¦¹ÚÁC<‰ˆœAY%È:öœn §€\.—Ë¿f"ß«—¬†KDdx9"2ðµIùJÕh}Í`ªKT7_û¡#gª_«ËÕÀ%t0‚êß3¹º{äÚX´’r¨Þ?MM”˜d5q¬þÚ”:òd–‚òr¹\þu˜™C¾£r.Áj}í –&‘mʵûÕc“¬¯ª½šÕéñ:2Ô~ ²vsPbõ÷ ¢ƒÕ›Ãj__©Ý©ƒÕ?ª—P-ÕW øÚ«IDä:–@9ù±s¹\.ÿ +8ßI_~Î4øÚ}Œ„³ŒObõ×NX”Áœ—u¼” Õ¬–Õ‰ÕTj\‚„úi©?FIVÃuHPZ’¼6«©–jVSým‘µuô±ú’Jœ8"ID&1~¹\.ÿ:˜9‡ÕT/IÀu°¾íAÄ’ä'«›D'¬¯Tm¹š8V/­#±zƒ|QûKdˆKAÆW|5šÕjÖWYM5ëÀ5JT³šê?ž r°*12L'âƒÕßÉ_èËs)$Èår¹üK`Â`$¼Êá5¦@AD‚äSñ ¢‰Õã«› ¨Íjª%Ó¸Ú«3¤Ÿ¬îW£#Gäyž$jwª%éPÛ«¿µµ½šuP_KT³߉HšÕ¬>ä>¸¬}’\¦”ˆ¬¦6šÊÀ‡,`är¹\þY0Uð’$"ríy¥ üD'¬Þ¯²z²‘aõ†µûI¬¦šÕ˜À¯~2T¿ $V/a$Íê=': ˜W Hˆ¯†C)_%tªY-ë z\íX-«ó\ÊQHaõêê—8;ˆHŒ„”ß§\.—Ë? Fʯ° +xàÔ‘3_=eµ×Áú‰úÊê1 JèHeí¥j¦™¼D¤ ÔdípŒ .í©ÍÚ¾« ¨½µ]òÕœî âÕ¬–Õ}Y›Õ0±ŠÕ”XÝ!ˆH¼D^.—Ë? FJ8ýŹdL!"a5p¤/«;‘“uPÍÚ˜´ù™~µè€ÔŽ\Ýä§È!ådèÈÕ͵ÖÓ¯žAGöHضú#<üèdõ¶u ?Y›Õ¾6!%‘Õ^NrP‚¼ð9Kâ‘$Èår¹ü³`¤Žˆ |õpã š2ðÚ(AŸÕµ©Ã‡jÖÁ|vuÂX§Ã†g¿,a«?®Æç#'VGöLIž£3ß|fàY_©Ý©Þƒ8ΫÆYÖNMŒàôAsPú¬Ä²4(/—Ëåÿ?3O^"A0ID§Žq'×á2%ÖA5«ÅPMBÕ%y‘¦4ÛQ29üXÊúí<É÷Û$ȉÎ÷¯Þ09¬^Z%‰Úh*±6:ˆè¿Xýq'ŒÄ¿§ý’¯–0>{.—ËåÌÉpz0sªáÔ{¡Y=ÍP_¥6«‡ç$ªY_ñmšö€œ¤#Až=ùI}ëGªû‘§3<í–@ž¦ZªûÏ.G‘!.ƒK ¡šÕ¤Ÿ¬ƒµ©ÃgòÂ6_­JulPJÚ(A.—Ë埄ё0åˆ#™þw, â#Õ¬ýÙµ;™¢¡ºÕ;ñjÍ—«w¾0öC<ý~‘ÕŸý«ˆTólÑLýäP½*ñ4Ê #áׂꫨÞ#×F)«û‘ÕTËœ*Çúº*ã!äÉÙyùår¹ü“0:%È:HõjrP‚„¸\MµÔѬÃMË8V—à2¥<©FÿEš“OÏs‚§§ýÓÔ̆ð4š²ŽòåÉð4µÕnDõ~ÁµHèƒ@#'iVŸXí kß,Ä5eà«áAç{b5ÊAór¹\þ©œÓƒÿÈ9m²M§Z‚YÍÚ¢)WcHž.CÍ“éØDDbæ¹ÄsÌyTw†êà§)ŸÞ€§E“HšöV“¦ä¨vdV¯Þ€8ø ‰ð|ðé ÏóüÏÿù?#°‡K^ÍóUžÞ/Otæ#O—Ow²$‘2‰—œ9äºd5«MV{°A‰µûu$ê¸)ò;úXýåDžLx•—Ëåò÷`t "1‚q‚ˆ”a5:²v‰ê)*±º\MmV{ö¬Ã™|1MSÕsþÄüAUqž¦š§©æé„ ÕBú¬¤šçVÏ~õ7 2âI8©VËjª¥šµw®î¬>ÿHõmJʰö†SPíÉÀ/—ËåŸÊL‚pÁð‘ÓT¢º” :0ñ$ª¦\ÝŒ%j£3˜Ìú ':ÏÞ^ÍÓòlj{}T{íwGµ þ윞]ú£J¤ Õ¿S>íx¶èƒøêÓ Jh®¦Ztd5«eýD5«ïF^Ø#­VË™šÊËårù_&c$‰ˆ4dª%(åÙ, Õ“ µÕ^†$‘P""ìât×2pìáuÌó§åi¼@BõKAÏn†¸|š¸ä§“ÔA}åÙä·!RýkƒR¦#W”ÃêÕõS"›%GnJõÍ"2¤ƒH;A‚&F.—ËåïÇèø‘2ö¬v’\=µ†µËÕÔ&ƒ®¾öWÁ$"²z?F0}C¸Z@ðt']¥Ä) Áæjž¦š§ÅªÄÓ%òƒ'õQí aõ™€'1²šœ9©&ŽjÖÆ¶·$q +"IŒ\.—ËÿfÈ0Ã'ŒÕ¯NTO¶¤U™Rbí=ˆÈÀ1l%”kÔrPZ’<Ô1Ãñ<ÏÿøÿƒL‚øÈwô­‚(É$4ãIå$ÈÓÔþ©ÐW¯ÖN<[ð´Û8žÍt£T®¦Ž~ÍÕT³ú^$ƒæ ´T[&Ó ür¹\þ˜ÌŠ$"ÉA8"dXÉÆ‡Õ$Hõ”ëpÁ­æù:N“ fµÕ“ÚûeV«yâ‘3‘?âµ2«)“ðG‡³äÙ—Õ<ûÇKè€è€@ø‹Õg»væN­¾ ²Ú«Ó¸LÉWK8}°är¹\¾“ù ‘ˆ¬¦ºD$² +.×A5h“:«E‚ ¡²úS$Ä«›!åÓ˜ÏO7•‘  §óF@Dj‚Kåð*méD<(ŸþIùôO*]”¨]BâÒêrõisðdu?ÔWw³ˆ q}A9(C<‰‘Ëår91†³<SF %DªÇW‰j1ëÖáDYÝ|zf<=`Ÿƒ:Êl3“¹äO ^.‘Ždð +€fà ;A4#$ð “Ôœ<Ñy¾R»ãêG}åi¬†¸¾ ÎÕ‹:îT5öÈÕT“ ÉêòÌÀAI‚\.—˯0%B~¢ƒ‘ç º¬#‘s2áÙÿoa¸ š2¹ö]¨NTKn\µC9^-µïo˜Ò’T""A0r¹\þ‘1 +eàˆ$1RNbõ8Â:d¨&ãn‘ÉšúZš±õu2×Q’Oâ5çS&½@@ÒQF@ Ã¿“¥¤ª¥:5Dt’ OOȧÿó¶<ÑAVQý^8ód:æ6H:Ó9ÑôRA:Ðʓ׆¸ ùÓI%¸Tâé÷ ϦګöÈPƒyš8F°ú.€ º3L?w3TßÜOžØ2Kär¹üƒc JD’«ÇËw²*AÖ¦Ú«Yß0¾ô7«E> µû0N¡#}Ì’A=h}¤Ÿ ^ASê€Ë)ɠ̪L)_‚t¤ŽœŸ1Lÿ…æÓW÷ÊP›§©ø«tªoMuµËÚÜß /1’Õ¤æÚ‚‘ËåòŒQðB‘ÕJ(å”dõ†u!b «÷(ÁÉ!¥ 'O̧ÉP­= ÈI:uPþ˜3çA‚fДÐAÄÒ äÄÎ4“ƒdurPº„Oºöd5OSÏ>@ÉA0‚¬ÊÀÝ”A©‰ÈúŠ=X}¥ Õ’ qy¹\þ‘1@0ŽS°Ža‚µ©ƒõµ„©¥)£$pTÊP¦k˜þéO_óJO‚d'™„™Ïç#5%"š6Äå0åHP†¸üô×"߯G²~¡R8ÈÓŒY ŸÃŒH}<{¦¹6:‘ÚKn´D훞Ñ—ä$y¹\þ19'|õÜÇ:Ê$FVMrírmA–ÂÓ¤“|ªy6Õ<=EO4'ƒÉ¬”8/™1Ø%>ý"ÕMp¦|VþˆOÕÞÌeÊdðq%Hp!òEšI§©ÃD:D>Í8©fí·I¨îÔN¸Å§ƒà%Éaí§ÊËåòƒõaœ€d2ðA 2(‘2ªšµe°š4ÓPíˆãé¡'CmžöLQ‰HFqúƒÄ$Ÿ H‡å‰%O¿@ªK¤” µûµ%}‰4«3å™ð×1K¯Ÿª”Aüén¤ºRâiœí‰þ+sËVS-n+Hí’$ÃjtÎÔ¹\.ÿ˜˜'¯Ž$Ä%Öž9ÕÍÈÚT³úx5äé‰Gðô$|ŽžÝ¬ÍÓnrÎÈ=ÑI?pMƒ:”R‡àOSÝI‚ 2ùÛ‚ ¨î$%üªIœ}îBâ/4ñ4J‰jž–gçoDž¸GRmê««ß# ÕFNÎ&¿\.ÿ8øWNÇ”?Èäê=aõä!&Õ  ql–:ÃÓ’ãP¾°jØJš3¢Á%HàŽÏþ/ŸYÛIòsPŸŸÊOç|°:•É é§’29(]þÒ„iÖæÙo–µVõù'¿£¿úÊêû«)•¡~"äf2ðËåò…øƒ9(Rš*¨Ã#Y=¦8¸TÖ1èžž~ãA)kw I.I"“‘dà0Ÿ¥’œ9LiÂ×1íëàs4?øoÿí¿Å?ß°YŸöÉ:þÖIí>xˆKWΫæÁ)iÍð4:“CõŸ|ïä6aµè€`í7GDžè€`ür¹ü£‘û“8eí‘Bj÷—«©C°s ÍÀAV÷aî%CFbm”ƒ2pCuxÍáqý “üì‘.¥~¨Þ£CòÒüì=CõN9R~¾QÇÔ~φ\EäÄ h&A¸3ÄrÈ“v>-/Ü—á¼Å¨¾é!þ¹z?”ˆÈËåòBþÉËÀÃøê)1å)–©Æt’«_‘j”ˆÈÀ‘'Q{6b<2uPj¾Ð´Œå_‘ÕÉáÓ#Ÿ¦šO¿/d¯ýZù|¥ºSùþÏ.«ÕR°-è€hV¯râÒ’¹À$Nq€’#"5_Ô>yLy¦»ªY}7eí›^-“«;«š\^Ý”¹\.Iòo\~GX=þ+Cö¬¦ZjÏ(åÉÓó OÏFY{*Nê„xÒ¤EÄ–§€`d0½Cm>M|ºütâÓï"yíHu‰)G5Ÿcõsd~Oµ€„xÒe’œÀ$¬ÊçøŸË$tP½4ä^ÈOºwCõ=EµäŽ£º„Π)‘¾øårùËã{8S’ÕƒBV7%"rõêÚ;Cu§v†§ÇZ2eõ@‹ Fâ$"SÔ°$qJVùìÿ. ÿôHÿôÌǧIùiA¼ú}èKh®YýLI’JÉçñ“e5®H*ÁeÈQHä­r"Ó!òiª¥'Hš$™»A5kSÛ ë§UpËåòWÅ¿q!¥4PÛ«Yýît’ë Ì%À/Ô1Üžý–QVOB§©F'3S¾ÈÈ•qDÒ 5ªg¸2R½ò²zæKtði¦óéTžœPš2¼<;?;‡êß&?M¸:iÕiŒÛdšI8Ï“Ú<ín›À5AÜô  ÕOAµ'$Kòr¹üUñoü$ÉÕ¯’‘$tY›jÖ–“§ÿ?©PPNj>Õ¥HòL« Cú&m:Ÿý‘x-U'jË`Úk""A@02èü×ÿú_%'I×b:’£Ú“JTóij_,W‡‹ ú/~l>}ìs/j;"2ä&®~¿DP[GB‚¬îœ©)A.—Ë_˜ü3Ÿ„!Peˆ[’!®ÈÚ\#( ý§×Óè@‰§©æiªÇ 9Gå`Ußż…RšÃÕÄ%È‹ê^=ØÏDÄj^'š2d[öLKŸ½“>(‘6ux¨Æµœ¸öÁØ|òôñÂ17eÐ s+Iu_®Mj?“¯&Èårù “æ2Ä“át(_hft¬¦¶ Õã(ðÀ‘Ïž{OS=‘™„>OžèÀ¤…!,1b ‘OSͧ©æ³©ÆœRZ’ykL‚Žÿò_þ ön)© ñi’ä§ÿ¬ÝÁ§K|šêË9ÑÉ9Hó¸8‘9Gí.ÄŸN噚k¿_ÀCõ½þÞ©~*$ˆŽä ':—Ë寊ã á5”µY½„x¨fmjcòH<ÇÃÓTóÿ˘yˆ”IèóI³ã³çpR™¬æÓòiê`FzPÂ+J‰tR&ÓÁ¼_¸æÉ«c"òÓ?†%ªû¨æÓBä¸Cp2$pÄAÂÓ¤3©3¸5 Õ¬µª?¥_Gb5µÉ³‚Èä  ür¹üõÈ¿îISB¾°´ºO@”õ•ÕT³6ÆQД Aü٣̠K†ŒG”ÉtBÜŒ ÊOSͧÿ»Á§©žÞÉ·$C:^àCJ ÆóN‰¢~¢ãã )eÊ$>RÖ.C.‰SàLà¸ø ƒ³ùô‹Æ] Cíû8V£”ó$¬¦Zj§Õ¤r2À/—Ë_›üK—':kO p‰KXM5뛘B!žœñE`¸Éj8ȉy˜´¥A*‘ú#FqíÉ<ƒ:|ºCdÈ„GD{ä ôÖˆàô “ñUÒžtB:D"%"~gÈå úLJê+%¨v8AÎ § òôáÇÃÓ§ÿÛJrÐO†uP”žY Obqy¹\þªø7>¤”«GDPž ²MVÕR;M8ÈsP]šoà§ ™–¯4N_è6æ3Hm” ƒ!ÿBÓk‚€ƒ MeD"erPÛ0Z™TÊqâwòˆÔüô/ÿ|+áªeõHeD:Cè€èÀõÝ‘Oû³Ñ ¼úžB'¹û¨ƒµK«v^.—¿*þ¿8›3.Â:Þ)Ãj œA‰ê¥êñeFIþd¾Iý“t CÉ ^n&aÀÕ|Z>Ád†¹Í™”Ÿ÷BP¦)Sbú“°t¤‘œ$÷#“!.?}-œœT‚DNFBS*ƒ£ƒŽDŽ]¢¾ÞÔ¾e AßemêÀCrrvâ“—Ë‹óÁx9ÈÉtFBÊd8ý±$ðg“ƒ„Ó‘2‰‘¿"ãùIɹ¨OS[‚ˇ¦q\aÜ‘¾pBõ[¦š§q³N‡Ûš¬fý„§"KÕONˆËÕ}(A.—Á#ñ÷pîä xÉ+‘/¾7ÓI‚ S’0N@0‚Ó‘òÌA8NùSà§þùçO’‡x«7cm1|_G3iLUÏ«j 7 ÓKtÀ¥)9RÊO[|zƒTÏdœN2ÃÎ’c^Á#ƒNàç~â;y%"ÒÏó³eÐüt9YÝ©ÎÂ0¥ ãηCŽÿío#Jr&ÜPÔ&êç!²~Ê=RS‚\.?’ÇãÇ §Ÿ¤/G$9LIB|2œ%"d˜ò»€þ}D’ +òS%"ùÂX¨=(ê°6Õnæ$žžW—Õ£ìÇ4ÃéPš™$™‰xõŒ­ÆïÒ¸— ¯¦Ìð—ašCÞ!®)_\\éËÀóG'ýÈ ü4µI3ùi—ƒ‡Gr/ž¯ï—Ú‚Ú¸qCî2ô±ÖªNTKO%© ür<ÔùB$œŽ”ÉÀÿŽ>È Äâ2Œ¿$‰¿G”$ðÀüÎðòAù#–Öž«©f5uÈLLªP=ͪ'˜”™~/‘†$^’¡*Q=f«G."I˜Øaœ˜êDGÆ¥×Aà úrHf³&4ã$©œôÀ÷ルÑQ†j>[àX¤£#’H§‘‘Üš§©Cþö·ÿçʹ¿ a5ÓYçʰúƒ:¿\^äÁCÊd8=è gùrD’ˆL®oOo\‚€`$ø ª›‰ˆ ã$ðAùýß¿3ð°ú<‰ <¬}\ÕÔ–`ƒV/5':'ÏþOË0â@ÒáàrJb6žÌ ýôû…À¼…ÈtŒhÓ[“‡”ÊxÒØQ ‚Y"É”’p4¡3œåé~ "I¿œ¼.³69„OSF#4¹Dă<ý6 Ïö¿5S÷Z‚„Ú<HI’Б!My¹¼ð` ’Ì“O¦ƒX 8Fp~'¯2hÊÁÒ Áâ“ë„NˆKß¿3pðý¸’«Ïv5Õ¬ƒj ™dˆËg¥§åé Ì·IM2c02|z~~6µÝ¼åÃY ‚SŒtD’˜AP"« ‚Hò?ý§ÿ$ãÉt0K ¾$? \*ÏKà .u¢x°~Hȯ•Á¥X§QJTóijãÄwÂÃ-¤”aÜÎñï>È‰Ž ˆ$çG¿ß5jJ®Cªùlq€‘ïM÷î—A7nëÜ}>hJÍÕT?Brµ¯ý,E’ú2ðË_÷߯'a$XÑÇêgé$K2K$ «Ó SD$|Õ4T/šÕ¬^% )×Þ p:”X?í„2¬þ[A xà ÿrøþ?&{Î<Ñ ëT"9"Os:ªß)kÁ¸Ñ>{ZâÓC•€ foЗrq‰×ç¯9¯<™Îˆ—È Ô—'Y’ ó夃tüž ôËAq™à˜üÓÇœaàÕð9ä‘;’ ϦÚÝÊ ÄÓý  Ïj³~çÿò¿·díÇCÉAeõ’¦´Ó )“ )åÚ \†4“ˆ$‡”rõ‰ :¨fõ*8Öî§sæÚ;Á«û!®)ƒ"¯üVÿåðýÁéÁï¯n’du)Wûjj³ÚWc°œèKÃÕSµWÒd#)Í=‚OOE’ülŒMåIìd0–A ÏÃéf8ˆfÈ„Hï D$ˆ¥$Èð*ƒ¦O )“ ÁŸVJ(óÃÆWÍɜɤSQÎéEd9è@nÇKž¾kÏN÷4ðê[Æ= ¡šÕ’GhòWX½üõpg‡”d퇑3‘ð™2þÂRà°§ö +kŠO¦ÉWïä R½$‘ƒrPÚ/_d ³šNrP‚à» .C\úÚjRJD’ƒù#Ï q¹ö5,…êmÁQÊÀ‡§1£duÉOÌ4ýIãî…~øôêþt›!ž4c "†ð #5Äô†©>h&A@æAäLD$þÃørJøø‰¥Àóâ2hžKpE|2äd°ôéãBNòÓ¯˜ œˆ©XͧÅä$2˜¨ –Ù ¸ ¢’a.¿£ïø‰Î ͼMp +,ù¶ˆæ¤æ)~L|Dæ×b\òÁÕMÂ9€ž³’ÕGŠˆ¦GD‚Àr óì»FÂßþöÿü—`CXMméWØvæå/ƒŠï‚Ó1%ùŽ~8¤ÓÏ=¿òì—õMÕK«Ñ‘ Ó é$WdXMm±J¬îCJ9Li"“!.צº8"«—V'4OR&%ÈX”Ÿ¤#ÿV_¾¬öê µYíç Oϥ砺¬=Ä"ò$sÄDíÙX9­Ð4xI˜Rr˜ÌáìÄMr §cJò¯†à "ANÎ=q_%9GDóÄCDúÁÈuhÊÁèœ9(s€Ÿ~³%FÜ‹ÁÛä$w OKîµ ñdàylPR®.W'4A@@.ÜÊ0N@IÂó0© ²ö£‚Õdm”k7×±‡€ŸYÝ—a5ÕRCõÎÀ±ö— J}‰)±úã«©Æ—H ²ö†Õ«dPþÈk)å+Ap~- ñ3C|§r2H‘3á'¡ú’QÝ'¨ÝÌ Y[d&OxÚŸÁ˜R‚n”nÐ…¸Iˆê‘XsrPªAiä–Ép6ÀIR)9¸œÙ>>9xYHMMxàaü”œœÌŸ&2ø s-$(]xà Î!rtŸƒj>-nAÈ}‘ :J7±Üô /Kë º¬~Š&A`I*A@.vr%"Éà¦WÃAÖ~ ’ko@:au_d}û JÍjÖ×U ÍPý<“Àulñõ•jÖ¦Ú«?ÁjªE?RÇ"±ö‰HšH 8l¨&.¡ |P†q‚H2ðO"’æg„×êYr¬¾„µ©íÎ\V~ÈÑÁÓÔ–¡zL… ®Ì7Žxølª1*å§ÿ×’4Hñƒ7(‘,‘R"b’ƒd°K¥„Ä‘Épú¿ÿ÷ÿ^êà%Iß–T‚( ¸¿”ÒLBGº–Àu\¯TJt@àܪO¯úÀ#à äUºMäij rÓ%HXÍt†ñˆ\M|R$¥ q¹ÖªvD’ƒØ&¡ÎòôÕT‹'V"RݬÎP›|C¶¡zO5ëÀ¤YÍjñqDd'°S*ÇH8÷— ÙÒAšÊÕ‚8ª÷$9Ñ ã$œ>ø+¨ýQ›9..Í™Ú2Tó4™N™]':™lø4ÕRÇ„”SF¤q2cAñ+7¨%'3ɉ¦DDø°ÊÓ‘:ÞR™É'2.Iû¡òÃé~3F ”šI'x˜óqt¨}žø4µß,ˆÌm"2Œ?ýZ‘¨Ž§¢Zæ× q r¢sùk»)Ãù$`íR¾ÈRȉÕOV£¦¾ §#e>[ý…Õ™Ž8%j ôeˆ'‡)GPÍê/'$œ¾z8ȉNXý:ˆLž¬þ62‰4¡³ú{ „Rb$KIèW³zƒ¬^’«©fõ*It°6ç)¡ztœ bÔœdÉñ7»@”˜l0îBÜœ„ÕI3“`³I³wP‚˜ØAâ†|ÈÌ× JMr’MÒ†$t$|PZMêËpºmʤŸ¿)“J(¡äqIä”ä0'I' ß’!A‚$w7Qͳß2:!¾šj©~ÒäÉêL‚\þŒ¸wxI2ÄsÓƒNˆ¯~NMŒ "“Hó{ÚP_YM–PÝ©N-â'I“tª·ÕfØN5k‹²öIÖÁêæø+-¡¾í |õjk—D"%t@N¬®n‰SpJˆ'OtÖþ*¬íÉÕÌqƒTÃÍ™A©ùlª1‚ñ…ˆ±V=Ü’†^5Ÿó§“S‚ F+2xÏDĈF$Ã|2Ä üA'xAL6€è å8âi~O?ù…Rg¹\&Æepƒ³JÚ– ŸÍœ6‘q_q›áiªåo O®µª3T» \¾z5Ž‘ËŸ 7îÅ4OYMíN$Ä%ì‘A«?¥Œ :rP"{ÖN Äjj?·X›j¯^Š`5µ³ºŽœ&‘XM5«eõ9«Õ:®E*±v_®î)õ%”µQÊÕpD4%FõõIZ•CšCJ¹z›¬îÔfõIyzRçi¸ÎÉÓÿåEV¿_$ˆÁ”ÆZàŸžxŸÁR 73 "Ò\˜´à:™±œ4´Á¥U bÎ`$¯p·J$"šøwÿîßI%È`”!žÌo“JŒ'á*B<éz ˆs 9.©“üôIòó´¡Ì½@DÇm"ʈ[‰Hõý­~‚'¡.W—«ñP…¸¾ <œ~ùáÆ‚HÒM¯ òôå ÄwÉ·eà«©£4‡”Õ¬¦6«}ý‚jÖWêèT³ê[™¿.ùêÎjt$HšÕ(yˆËô“ 'éL""Ãjt’:XMÊäÚ¿:)×ÞƒÕåÉt"öHœª¿Õ¬ÑL˜ð´?;ƒY4V–7ÄBݧ3˜#sRr¤§ÉaØrD$ˆù8ˆÑM’;HJ(¯‰¤~rÐÑÇH^1Áp ¢9ø£2ð`üNwEÁõžX‚ÓŽ :ÒÁ¢§=|/sÀ'1÷‘TßÜj<PbÏÆêÇfõCENÒ™DD^þ¤¸}Î ²úÁàƒŽ²š&<¤œ\ý…ëàìc}¥šÕ²:=±‘Ò—åÚK«e}}ÈC5k VSkSÍ:¨.ýEYí¨fí¾TÊ8H5<©#ã k£<±²!DD¾Ð\û{V3^›Õœýœ¤|• áôgOž!CIS)¥i¦”Ÿ¦¾òéŽQ)2Q#0o•™ÃJnbcdÈH×—3ÿÉp–V¥7ˆä§ƒ”ßsðméH0?&â÷K ÜE—,¡#HàÞ,cNdÈín”ÒMDõ-F5Oÿ—Ö ”ð¨€è`õ“ã¡"2²º“ô/:Ü8Œ`<÷W9¤” «7€ öÒ KÙ ¬†ËÕT?„Ó¯Íjl¨Ý ÓGõR5«—ðr¬Þ³¾1ˆ²šÕ²:±ZVgXīդÖ> +(ÃúÖ$ŽøTu9‚—Lâù8 šë+Õ:Ò¥ :Y½GiÔ µ'RènvþÙÿgÁ<±$3s˜Ò\ æí Y_Ô#àÒTÊÁÌO‚Ì»ƒ@‡Ã§dÐ<8O""áã“ÁŽ_{âr‚¥$\{àŽ%p8·¤ƒ}QͧŸ ·#‰HîWà?}sŸ¦šg <ŒP›õõ© üòçÅÄ8È÷›Žµ›ñêàcö JKµåEõw¢Zjç°º\X'öG¯-¡º¬#Q-µûê2LqT;VwÖ‘YZýqÄC5ëÛÉD^‰sg˜Ah‚ "AÖþ” ˆH¬Þ#Q]éËô碪/ùôáƒèÃð šID¤‘”kÉOO9c)#0*ñ³¬Ñÿž²)-Atdà0ÉMûIàÕ ƒ¸ädPbö ú údHyþiäçùÍÓti’'0(ϤcD$él«OØ#¢âÖ`œÀû1ŸMõ8ˆgÉó#•ˆ¼òG,]þ\¸kŸœžç!©xà«—$ªQ¾ ;¥rõÔ’JYÝA}euÇN¸¦ŒËÕ%¸„"IV!RÊ0ý°~A5«¿M¢º¬Î¡šÕÔ¾v‚È™áûþ°vrPþˆH0‚Õ_ôAY½º:ç¿c 3L’ˆè£zæÔ•Y$Atñ%aÄUO¼ê4‰”ƒšÉI&Yœ’ œ™ øÉ9Øú¼’:!%âƒÓŸÄ4I2ePBG‚œä'%ƒŸír$'Ò5‡s'l˜Œ|êÀí€{$¥%¸¹Rùô]~¾þc «©Æc†È™Ã”äògĽ nº ¼zéù«kï «÷'GõÒ©.ù<{uä`ÕM¢¬p ‚H¬.õ%F†ÕŸÂ¸DmV7I2pýêÕ!ÍP½ ¢¹¶`µËê/Sr(å0}dI>}€Ï‘xúECL$¸ ”¦jÏ=ÉÄðr“SãtPš´Ã á2®¥ÑˆÔ$Èœ—ˆœM™·Æ¤NP‚¤)•2èM ¢/ùü‰ˆô“’ðã¥U’q]I”Òá€K81©$ÈÁ~ªoÁˆ ¹M)‘n+Üßjx<Õ÷}í‡$"=Q ÁæÀ_h^þt¸qCÊ$Fà¨o%xÐAmCJ*idAóÓÃÔWÌ@#‘À”ˆHX•J³¦+”&ðˆ„›êʉŽIxbT&MQ b›Ñ +ã—Ÿ™Ã¬FJ vŸÌ«áD–æå™D¶I(ã>(¡ÃOtà÷„q?J\".6pÌ™çvâxa›„ó‡;Ü,Xå¹}H'LÜíþ[ÿãª~ xðÀÔF)×îäñ“ Û ãòòûãN¼:nhugD®Ã'‘>VS-µñ)‡êUM¢ªW«V¹9Ñ9I'éaFm¯&ž”!nÛsì?IŸ$A?9¶Ѭ-/ª/ÖQ¢ºª½:Q-sò#a5iN¦Ibítêè¯.ý$Dd$qž.Íœ¡$7²ˆ„ò³1å}i  bH‚˜Ÿ8°ˆ¼!Óx2Øchì™ùŸœNàðÖ@DnÈ&|¡Ô!É0NNüHøåAÇå$w.œ€Ãùœ8ÃÁÙÚF$ÜçâÕMéNž{*ý$žã×êGYý˜¡šÕϸ ñ$Èå÷Ç +§wÕ·»öê)«áA'¬n’°úã«ñ8Á*¯n*G¯Î”ƒ‘döÈêÒ£+¡ <ð§çÛÓTó4ÕÛHµ—tI³%R’d~ÆsüóAÄžÕTK5Ó¯v2¤¬^=©î8ÉHuÕ¬Æ(ÃêÁê«3¬¦6«"éÔÎt’xö þôÑeÈL)pƒ+p|zšás¼b”¦ß‰Ž!™4<#æ*ˆy+gG$Li.Ã<èÏÌQ‚x/€'uÂ÷2©)Ñ<%Ä“ ƒ¿âç%ýò ”p]P×âÒÍùÇÇ+¡ÃáäCnÊgߥ[&9Iæž7>x`ÏÚÏR‘µVuz>‡”úòDçò›3·)’Äê­ÄKäjªKŒ@¨ÞYÙPÝGµÔ×<Ÿ´8ˆŽä«— <™NäÌ'\¢šçÕ» eˆKÌ<]úò§¿áéÄÓò4þ9Œ£šg |HàƒOÿ• ”aœ€€¬¾Øu /CõRíVoCΖHT/¡6¯¥:µïQu§:ÓñAå$4k£Aäé 'ëØ†§ÉÁ&‘¦xF–D:Ÿþï,dÒô1 F‚ù)ƒk®"ž4„ƒRÎà&yÈ„'A DòjƒÒI*eà ':vÊÓåàï¦#At0¿.Dºº!W¸c‘J"‘sËI"žsæà2¸; »MA â¶¥t÷QûI¨Æ£²úiYý¼Éà –Î”':—ß·é…]»?‚q¤2pD$Îg¦šõ•êŽm µK‚ˆyúáäöÔN gÉ÷…ž(eJþü‚|¤2¬ý«^ý é³Éê$ðuPkSÍÚT³ZV§ï‰¬N¬ƒjVËjª¥:Qý IèHå0ed^"],æTIæ2©ÄŒ)Žˆ—Kõ+™r}†!™ 6À,ŒYiJ˜Ò/Œn}‰S=N¼¤2(‘ ƒääÕQú¸TF’þ4Èà§""q¥AéÎ + +)s’œ N;‡/C5îNî׊W?<Íßú‘¨~6@@_=R‚rPøåOAnÖ+_û¾st’:a5éx`xàc$;Ãj,ÅÉ 3‰ðÀ‘ fT?Þ)k{¤:Q-u$ª¥:Q-óÙžvŒDžýÁÚÀƒ‹Eõ©N ®VoHGN ‚Õûeµ£Z¦‰Õø+2Ôn†Úø”\›j¯Ýy‘æ\&™#Q{þH`óJ™ Ÿãí7ß`Ü)Ï4¥Q‰ˆY +.3c% ^hÂpþ1ñÀÍöÀAŒýA'ðpúcgàát:“ ~UàÁov]$ âs$‡&Ar¤“ Ž=é.Tß‹Ü â‚˹¿‘äÓÏÀÓ<$Á£…ê矀€ÔFy&N¹üæ¸MˆL®}£ƒHàX›j¯Íj_ý TKuêçѪ.ùH: KÒ”öÔ×øéOÉê òùŠ'_‚^›gûÓòT—Õøæ “¬nJ<iÊÀ‡)GV‚¬¯¬î¬^„Ú®/ë`í&ªKT‹» C5«©ŸGDâ%I§árp°°:i +¸%ù|¥zÄ™xˆè³1dfJƒ4(“0ZA2~'e"A2±“&ù‰L~â2Ä%^’¦t@à™_8øñé\KÏ9ÈÁAÁ¹aä<Øq.?›Ü $ܾaî,‘ ƒ'¡ú ñx€S®µªñd~’ŽDD^~sܦš pÓAÒáõ“¬ŸðäÈP½§:ó©Á¶!ªQrOžx\A,=‡ ¶œÔæiêàùZâéγ3äO®S_yöžç öfT{ÈÕ^}íœHòl€ ÕN^TªYµ?R›µ©f5ÕØ ¥ü¶ O_ã`¶¼2SH¤ +:ˆ|úà fT‚Ëf£! “3h"Ó#0x¥QŒóùD¿z’ÃxÑ!øù²'ùdƒñw%'Iø…'~¿%™K“ˆ¸p8 œÈà åIÎ<ùÙä·äD'wY†<Ϧö…y¨ªç F~Ål —ß·iP‚€„Õopøê>ÁêÇcuž¬¦šuÈñ PÖv¢ùôE=:$T7]x7v,OšQ$ã+|“ 4e0•&$73A`¢"¥+‡i ¥4–#’ÃèæÄxq6ãÿæßü9¤™ìÑ\üD?ŒG’~H~ªô㓚®‘¤s 9 +9(sn0  ¥„“çÉϾ5Ÿ¦ú~É[)såà@5Ï/žÿÕÔ!ð"2ø‰ÎåwÃ}ÁHPbÄ—)%H°TO‹D‰Õ«kS›lXÝá#ˆ$=Õpùì‡J<›êG:òì„f¼ö†$ªû¡zü¤zOõ*ùcª·É§ÿJ¤š”xšj\"/‘xúÚÉ  szÄá«©–ê´J¾3ýµwV³šjVù Дƒþ ´úìkÌ…K“\"’4¦`dýHíùfÜ%aBiBffÐ7TA@ŒY"A2Š¥±L0ÌmŒìP&Ae˜7Q‚'1MÄ5“xI ¿JøÙ!îŠHÒ•î’gÐqhŽ\ÂÙ9öI÷bp¿àÞØ–ÌíŽHŒ{5T{5O_KÓ Ê!åÓäqŒÕp¼D®½!¬.צ6«©ƒïOÏ7ªÉ%O5IeÈhŠÈÏžW¤ºª_(ÒèK*%?1' LpST"bÀ‚K¥Ù;(ÍdD$ŒîÀ óÀ¥·ƒD$™%Ƴš„ŽÄˆ¥ø™t‚Ÿ7?8¸œÀAr½Òµ:N Ä¡M:Ò¬Tï””D~šê»öB37T‚¸ãˆÈ§ŸgSíÕxœ‚g Õ (‡”«÷pœrùÝp_B\î&J¤”CÊÕ·{5µYí«±M¢šÕ²úµ‚x5)¥g#µŸÆ§› Õe¨¯žÃ=ä)‰ O¿¾“¥êÍaJjï O‘?½áé ÏAuY}½ §.Ÿ]éKÕ¸:±úlIà:¨Íê ²z5©”«I)AyöïI™ÄÓT‘ƒ™K™ER)‘ÒàÂ̱jR” 3‰Á8蘙 0Hƒé:¹–äLf³š'ƒI>|/‡”y;È”A™„>ˆxRq‰)_¤™ô;ƒRÂ%p Wǃ˗Áå§œ#‚sSʦDŽ7'ÏÁ¡”¹SR™ ¹›·<ùô#žƒê²öÃV›ÕTctLž¤#A.¿¹/2pwViVkwHXÝ ú¦N¨f5uÎmP†,%CB"ñ4Õ<[N<Õú)Ayöê 9âIŒøl5$Ä“ ‡¸|%«ËêÔy ª/ä$«s†#°ôbõù¯Þ#«?‹ˆÉVï— ÏþI‘êK{‘¦™ƒˆN™HÒÈB5Ÿ£,i¾¥é C¥ †$td¦¨qÌ[ Mb#b>ƒƒd€OjÊáœÿ$¤ô•ƒNý':–HrPú=Êñ‘×èz“J ‚ŽSœ4“Î9ídnA¹;øì·LˆÏ%n7 <ð§•§›<]ÊäjªÅ$!’WKPÊ«—ß·fx•˜û8Žê öGÈÚT»§E¢6«ÑçˆÈ4ƒHž=¡ú™D<»|Zž¦¶ úÙ–ˆ$C\b$Ÿzö;ˆ'AÉ¿#|ßoI³º J9×UÍsP¿(}„ËxHçd:#ð‘úŠ&œ|mÎ’O߉êŸWÐçÁeJ8„ “$•2.?Õ¥9f¦è £O‚˜‡0!•2˜ŸÐ‘†ª4f•\ Ä|N*¥Ñýǘÿ°y#8ÊÀGDÎwBéÈYüf W‘œë q §1œ˜tŒ N9mxî>›jܾà†ú“žŠê‡!(¥ç¼ŸT®ƒêÒ„—'g‡_~+Ü”pzÐA$‰Õ·[‰‘ \M}«r¨Ý¯–ÚO×HH9ùô£ø‰§©ƒçÀcèÈ`Oxú{ž&ýgwª~Þ‰¾ GÄä9ÑüôkåÓÔÆ ÆšÒ”{e0A—˜Ä›Eš±'¦î`ÕX~‘‰ý=ú¤RžèƒüÈÿÑ{F$F^œ}ôÅ«© ¹.™‹å9‡dÈùHçî$qÈÁùC39w$|úå’Tº¿’G$æˆ`çStài Õ‡È0ž¥3A.¿!¹5“xÉ™ƒ{] Ç”«ñœðdX½a5ÕK/ô¥¥áéQ&‡<ŸOKàÕpŒ€#"‘G]êLbä;ög5‰S ˆèƒ¤„’ƒƒœè€€ 2 ‚§¯ôéÄÓTKu¢öÁŽå <›‘Ü«¨&eí?jž¯âw"’qù›?ƒ‘OS=ÄŸifÖž(ãÆc0-u‚Y +bÆ&e¦.æmBd\â%2£^žè€/‘—!nF¾ciPúëÉA_žèÀU€¸ÀÀ‘£ÁAAgÒé‡opþȽ˜ù4Õä"·X‚äîƒާÿñJx´t@@YkU'ª1d&A²D’ —ß7hnÙp–ãdõ–ˆT³6µY=¬äIu–âd˜§îÙÔá¨.=¢$ðÀA<ùI}øðcy¦KR‚‡¸<ÑÉÎHPJðg_”Ò”¨æù5u¬VI"§â2X­£ä[B5Ϧš§ñýßqE°çWd(}zF}:3¸$Œµ™oš$˜0¡ÄȉY:/"92„12˜Õ1Ø¡”aü%ÄKDò  šD_†ÓOÎ>÷c†)Ià˜Ëq¥ÃµÃix•$¡/¬rþg~úf}7JwHÐ\õ-Æ:ÕR;Cžœ3«›CJù|¥šg‹§ôÄœfäù:÷± +¢$É ˜¦4¹ŒËA™¥Y’ùsö%4A q‰g_õÓÔ–PÍÓ—|fš¡w¼Asò$씵?û³c7ÉÑ$Ë™¥mHr =ðym"6‘ûñ¥÷ÓvÚU,ø.KX¼uAœÁ+GÕ@„ýh|™k¿%äŸýW¢ÒG#¡©„þ£)ÖSkÿ—‹4Ç&¯ “:f£äÍÏÒD•,Lc|x•Fz胄ï…àõ%t$’Éà^¢)‡ÊòDÞ•y¡é+ÄǼ5i‰@*K ÖšÖ¾_@PS¶M׃­T–/l½t ŒõˆÁÚ8cÁqï±€|mš3  %Hð_~íÈ$FbJ2(ï}î}*FÊJäƒ2:c‰_{X­X›Ï‡Sâ%%œê—O¢fp·#*%*¥¸D"=ÿžzYLI$NwÈ%éƒKõ¬¦3|þþÊk?ÂÚ+,ñÙ%t@âóô?[>;ñÙ¬ÍçùCàkÃá³py¢ÓÇ—HL'¬=¬šZ“F ·hè%à†ai<&§¦q:øB ^xA4%’3ÒcJ‚¾&*¥#q–|Pú)e‚I ‚¤Ä¼Ïd>ña ¬@X™Ð”Ö +ÖpP¶¼°ÚÒ.·/Häµ±ƒŽŽ¼,t@>Ïiù‚N 4.ÖæÞbÈ€KÔ‘¸w“ ¯ò—Ÿ€M*%9ØÐµ;IÜ›µÅ#'$t†Ê{¿Œ#Ñ‘§8ok¼õd‡sP2Tž‡œSºPB)+¥²T†I^#§C )_%’Ò›$’õA`)°4'?›µKäÿlÖ–õ¤Õ–H¤¾äŸý‚Ïfø…òs4ëÀ{ŸºÔĵ¿G*¯ƒõ`d4Ê ·šFH“°4'˜R9ª2Œ\‰DšÉ¥RÊà‰<©ÓwAp$= +¡rI¯çàÉ+o5xpŸˆ }Ëœë`‰Z(©”Ö0”hyKknGl”¡ƒDÚИ½– èT”ø<æ³å³ÓI‹\Æý°6÷ž-¡*'A~ùQØŒD¥ ~nqè`íÝ_›û‘ŽÊ½Ë{gÔ—5É$>ÏdNÍõ  îHKŽy ×—œ ™Né +„Rþ§x™Ÿ’|¨ÔQ¼¦ ®9Ü{“¨,A @†W‰Ï^}Ï.?;ñÙ¬‡Ï^g¹6Êk¿ý6)%’y·I âã¿ö?z%Ö3©$Œ2cM)£Y'‘Hó0LÈÐ46aœ–:,ÌÞÁ(}¦·*ep_§ÀÓ/93réÇ“Iúƒ$xpo»„ÅAЧ–­ƒARFëfÃÚV>ì¼Ìî”J‡õle(Ûëà NHðÏs´ÖÃçï§ŽË{³îÍ:0|äIò—…M y Û\óެͽY[ÖNg¦|á©0 ²¾Ðù”ƒ—Í–P‚Àçàï)'å·œr‰D¢ßƒk߸áz® ?y ŸEV–:D‚ Ê?ÑSëSZÒë ž–žž¬ÍgK?¾6ŸýkÁ%Hx·èc–׳Èå°ö¼š„Ù%5˜rü¤(y³Ñ”Æé`ºêHó aÜSMlœyé[àDS‚€ )ÿÄ<ßI@ˆÏn8F`é"—–Öv°þðÈŽŸmJ®=å ¥Ã0Çãt|ÖöµÓ!lP(ïû^;›0dP~‹G¿üLÚi[×–M âÞ¬G†utœ–§h=g)øzæÛz•ÊèdJ8¨<Î’Ë)ÁA@e¸Á#¿ž[#ѳ_J“:gö«$*K†é-Î’KX+©9©ãk/lŽõw>ëáó¬ÿgÿ9Ññ–B9ï984-Ö^[ÓI®gLI4Áši2 :¥ŒA¦¢ìkE¢j¨–JÉü5~ N‰ÜÇÈ`ì‡GÁ12|íüÇü‡Œ¯O§“ô·¤2r<¼a$>¦ +ë3(â% +\Z|bS`w¸¿öö]û:„v|p0t^é8­}¨Â+AÖÁ½GGMž{ ¸”ùå§ak"/%Hä÷—“°ö÷Erï ÍrP~6ë‘pu‚ÇŸ•N5”pø'A<ºöjJýDóÚ•k§R®Çãtx +y oJJ9%HôIe~&æÑçYÆÏßY»ã‘Œµù^3èý $2z4o{б2ÃÚ\û‹†”f—4Ö"7èÀK“0LÅ¡)›¥“¾\`ÞB§4„A@‚ÃÜŽ¼”0öA"—¾>’à:I¹¦t‚{àÒ—ƒÒL‹‹ÐâÈð¨,©´ÈƒÅoGäP)m™íÃÚûhÑÖËÁS8!‘ËÏÆõ‡$îû^OâÞ¬c'ò3A~ùQØàÁƒc¤½³‘ßÓŸ”á¤!YŸÍ:èd–]¢)çœW– áÁ‘”¸öÅ®MOK$2øõüÔõˆ~~=©Þˆ&Aâ:Hô%¦ ¨ƒÄ#™C ¢³ö‚¯¡¯D2é‘ß ådððY@p=|m®G ¨,M"‘¦xÓ‰Ùèk…”ƒ +ã”¶rPž³7/ahƒ&2ˆ/-¸T†µ’a ƒÃòZp[ö%4¥- åõœü¯}h副‰Ïfm>[L ‚ÄÄÀÚœ¥óBä—‹ z¡‰äg‚Ø}8 J™Ëam4ÏùlÖæó|ãD'SGâè‚C)•¡Œ¼3/óWƵ©##×Ö~åÚ‰õp¬ýS×S¬ÝQŒÀ;”gJýP¾>£ŒüLœ‚Ïþ¿KŒ€ƒÀ+A†~^:%êé — ×þ°kÿ»7ÆVRÇ+A¬ sO)MÂ0$¡ir6?'5›±$LàAiD— vMŒ ?Ä7E(åIOõ‘è”8¤?=hêG"áƒ@)ãü¼-…´,à¥Õ› nm­3—¶ÜÖ:ö« » »,•$N‘¹Óµ6nºŒ¼îF)å½ûà_󗟆}‰¼íã ': ¸ï{ùêwlÊøì3¦òyXÏ ŒuP_æph¹<ÑÁHÇ\âÚO?¹žÄzä+^ßÓµë¬ík緯ĵÃûგ^/áãË)Ièƒ@ŸÿìÕ åð9¾z‚‡GRÇ Jà˜w˜\ûc&k'ÖC£IÂÔ‚r0Ð@ :˜{Ü”¹¯¥ŒÍ0KÑŒ4uC Òd–Й n°9ÑAÒׇ¬eä^†œåàÅW÷YøÔ Vò€ÃZI$–‰´¼ –va°G°_žN¶§6šHÞyH¤#âPam\ÿ¡Y{³6÷–{'îGš0%È Í_~vdP¶›d8K~?_%÷ó,R‰{³6šßÒ1û<¬ýJR:Š¡/¿¢g$8:í ÁAº ±6×f¸Ar¸öÓëH¿D^ksø%Юýˆ*=ÂÚŸBIdè¼ð´œ§\Væ‘[±24#—ƒ#¡ìÇ9xpôGåPÓljõg .iˆMn%Œ>˜{ úÍÆa†g˜¨²éZ6uepË_ÑŒwŒÄYr_#ƒä+~žÊ÷Ndp}ù}øD á#‡ÕÀ,±z°’%ôayA†öbh›:Ìåµ·õÚç€ä…æðÙ¬}Ù‡³ä÷f=sæ…§åý<åä—…MÁÈðµÓnê÷ÃÚÌ YÛɤ’ÏÃÚÌàJ¤G2t@œUMãçQ¹ÈõqmÖ–µs}H ²öÍ?YûÅ/ÖÑ\Û×N¬÷À‘HèJôHÙ‡*A†J/›„&,”ĈG\†tNêx +.¡ÃO4Ñ_”¡/½íaínÝÀ%šW²9&á %Ì:èK30”¦bÙ¨”aŠF£UÂà ÒXÆÆ s.O¾vújÐt‚#™<ÑA"ýp¼DzKƒÈ MŸ+rŸ:”ƒõåZL´¼‰´òÁmGØ#Ø2M™ÛPØhèKgàDÓ!2/A`œœ#"Ñ$Èðµüåbk@îgC_h‚Çý°¶¯Íýˆ3¥ Íø¿M)A4qzTzçëÁçL§Á#˜`‘KwHdŒ¥i¾P i¢‚K˜´š_ÓLùSIùB¾‚ƒD.qÊ Íà~牦”ÞíI Ò‡’ƒÏÍ–‚X¥à–.|§H´¼“¾YÂF„ml¼²¼ö_;í8È·8Brø—ýÞ“ƒ(±6÷Ã:˜±C¾2}ò˦ )‘HŒÜû?acmî-÷>0Ht&§3|:Ÿ-'“ h¦•鄟%Ü…rm®ý}!׆•="'uztíT†#ƒŽ—]ûÅ×ßK\‡ÄÚî­JM†¾²T‚šÑ¢%'eä!—HæO(A@ôÑ;ÔÁõ°ŽÅ “*riŽ™iaÊé¼Ò$‡Q9TJ˜¨ÒP5fÁ%’ÒL>i†O")‘HŒø^ˆq‚DžÔ)ƒJ¿DI‚¿ÞžI ŸK"±VÉ¢oä’Ã:[ÿPÊÙÛk³¶¬¶C8BÃ\äÏþŸŸÍÚAâ˜k¬Í½1pNô'{ÊùÉ´G%’É{o¥\Û±îÍÚç„Ȩ#‘”ŸÍÚ¢#ׯiÄK&^9(álóà:dБ׾ qm¿6kËz&áKJ/PG"¯ç‰^Râ%×þë×ÃÚ¾®çò’µ? |@TJ™G© ‚—LÎò¼¦ô«@†Ê2®ý&ᣅŽDMÊV2—F)M9$2ŒAsùÐ5W ØR‰4“O¦CÐ$/AȾdŒ“àêH$“ ~pœÒ;ÏMwë'ï‡õtâ>X{ + •$ø/?»¶RÖ™ ÞS(ï½é÷f=8 HÎŒÏ>cŸÍÚ²ŽH0ç\‡€Ã1èsðÈ»¸¶\O­ää¼Yá5×þY>O•ã=½vB'¾º¼ž—á:X»\;±öG(û€D¾Ð爐 bU%ÊP‚øU8=tJ\û}^;q=òÂB5µds¬Ôi¸I˜{ƒ¦/ã±TJƒ4 Õ0iáÑdËЙ4ºÁå‰Nó¿ >T– ÁAœ¹<ÿP<¸÷L@Ntà3 +pX´JÖP¶ªrPÂ^„ ûN,¼æÚ;{í‹c÷QélÀùQ– ®¶äŸ‡õŒ‚û¾×ƒŽR¢RÞ›µÇN‰D¾Ð þËO£})A@‚¿÷ŽßÇ øÚMN@@†Ïþï”àÃÚç\:¥ <ô'j"‘Ô” ¸ÖãëázXÛ×û‘Ÿ×JF.áRÓ,•J¼JyéÍxÿr=oìÚ"ANtâtT¶>Òz&ðh¨ô´TžhbÄoÐáÞØÚMk—>Bðð©ƒÃàjˆÉJ™GOš‡¡Ds²É)¹‰:"ÍØÈ%£8Ɖ¹-‘” aþG^âßÿýßeL“€€ Êo™Gþ"FàÑО -4KX +XknAÂò·9oñep›[¥ÝDû{íĵÅÖÃyàH$ùÙYbm>€ƒD^#F\{¤¬:D‚œèD.ùQØÈ¥m][BYÆ:èT¬}x(%:Q HÖsØ:RGb$”'0i¼£.¯§ÄµY›ë`ír=wÄš<;_½^eÔ”~PrŒ Þ ˆGõY[úh“ ƒ䤎E#2G¥Œ\‚€xñ)ƒrÞ®7å+}pp#K‚ QÖˆ“ 0÷¤aÄœ cS§)*­2ÆM`^‚Úà 8%riÔù- }Ñ9¼ÊçÓù[IïV*Ot‚‡O +«1–ËÒXX.Q)-{p´/íš„ý]{£m¨\û ”á(¥;;¸Úkßôõ¤ë44d¬ƒ{³¶>ddPþòÓ°/ ¸÷>‚OJÜ›õàlÈ{"A"/#ÿ¬ç …t>'ÏNèt˜‘Ç”þáÚt)ÊX{ôœè¸MDByº¬,ÏÎW—Ð }%t ‰ëy“×f=\{¼ƒ—ÁÑÓ)G¬R©#ó¨â)ü*¬ý$®]^M‰µ;½Û>` ó*Œ/Íà0Ü lâ•&aiB‚÷µ2nŠ X9TJ£8rinEæ<—y9(A@—‹ÔI$'‘—‘Ë¡Òßt¼ç“éôédX¥,WXIXÛðH¶ì'6#6ŽK{j±öF¯íþÚgãm¸ta|Öæ³ÿ©JÜ›õpo¿wÂü ¹üåÇbƒ"/ƒÇ½Yœ§bmtòD†ƒ´vsrø<8‡ñH†ãª,c¹ArííY'±v¹vÆÚxÎrî”<ËàÓL4 rÉyz9P"ñf°6×þ×nú€u®‡µ}íG‰ÔÁH¾ÒòF®é§Ö!דÑ;™ Ÿ.8Œ)øáà˜É¦ÏaÜ)K“°4Á}¡H˜¢P–ƒI ã——Hdd8˼4êÁƒcÄ÷Ç)úD¾¨99(Ñß’ƒGÒ噑û° h)&­’¥ƒÒJJkIJ[ŽäÜ59¥ý…}_ûHH$Ò™/][$]jþÙsàó|­ð¸7k23‡(׆—uHä“¿ü@Ú ÷ß7z÷f=ë)ŽÏ>HDD‰µqð¸äH&AœX$¥S-AÎcmÖ–µs5By¦K.ƒƒÄøH?U)+Ë'ðTJŽDG‚`$øµ?…OJ°®ý B‚Ÿd¤e '×ód¥w¢\‡€ËŸ§4²`”…ùw0 ¥’Hã1LËÐ4<ƒ£+£Á+eÚå Í&< %’ÄWÉH(ep/t@‚ƒGo`²w>‚äLøÈ -ˆl•,Zi%e«yiýa/¸ÄlA{gÛëkã$ȵ˵Ï\ØòÄÇÚ|ö(GRâÞ¬-¦ 8 ä—Ÿ†}ÁH»Y)1‚û9ëàÞ¬s– ùÙg,/?›µÅ9$ S:«HœgÉ‘è|¥Ó>\»t5"?s‰$HÙ‹:%HpôÊJ‰D‚G"ãzÞª” qí/Jõpm¿vz$¯ÍÚ\´8“¨×ó<Öî¯ÍõÐÂxŸHÂj ¦–¾ši ºÒ0 ƒ18 Ï“æª #·Ôœ ³:²²lÂË¡Rby9ø* îeh~›H¼ãÄŽDÂç « ­›´¤à-²5/A`/¤]“ v- k†k³·²Tv‘|6ëÁõG"ãÞäÞ9˜< Áeäå/?»sRÇ^KŽ{ûäÚÜ›µ¹·Ü;q?ÒúìÓõÙß8Ÿ§ƒµÏapG”€(ep'¹„skw$®ÍÚâ^€ã%Òõ!ÁçZ‘Èeô´ÄòÒoN%HpxY.9ARbäz>ÝÚ\ûcJ¬§$äåÊÁ’Öç¸ÖæÚríÄõü6o§X1O@ÂpƒŽ¡×ô S1ú׸ >ã”|ÅÔ…—M‚4«å‰Ù®)OÎÇùeÁu@@”ð2‰¤¾¬D"½‡¹”ƒOyéó:ÖDÂBX@XOK-‘´ $lF`7ÃFÃî¯}ÖÎÎŒ„SÄ]Û²{ý9X 8îçÿxH¾6† ’/)ùØš¯èÊvœ€cí#>§"© |p؆õBg2'ƒs« §7tJècäzŽ}¬KžÊÐ —ä[×N‡§t%áÂJhò÷zíÛ½vDÂÐÀz Ö3sFN41òËO£­‘1>;KdÔ9ñëàÞ_+rmxðÏ>Kȱ>kãJt>¡ÃÑÖ‘¹Ärä¸öP•ë@ÇГ‘ëƒ(I銙ŸèO“ŸÔ''š²~^êå0ïdЦT™Kø˜X»,•e¬½ ks=‚k³¶¬‡ë`í²_µžßŒõw¼%’ù8J&êpi¸w f`˜‡0!¡/ÍÌP6EeŒ›·ƒ¦„ù|¢Äì…­!¥Òæ$l1ÚýëÁ¥ƒË}é†Kõ÷;þÙi$àatÈfË:Ц¬<ä—ÅkS¾– Á»Œµ¹Öæ~XŒq¢ÎÄÚÌQD¾v3¸Cû§ž^û—ÿφƒx*A®M.ç¾(9Èàfé JèƒÔÉ¥RæåðÊ|~–€ƒD>oRæ×fí&)×QžÔ‡GXÏ¿X›^V®ýÊk£”ÞØàƒ„>tL*úÊ0Ù`ÄÁ#iú…ÒœŒF¥™)Ã,ÌX©INLæš2?s˜©Jà})€@$8HðaJâÇ1}ð®JM¹ð-žúà/¬RxjÝ@,läÜúC)í씽 »\êƒ8XûH¬ýO»ISÝßᳯvgšX›{•€K$g‚`ä—ŸƒM”÷ÞÙ\")‘HŒÜëñµ¹·83Ž’õœ®ÏÑ>Hé bœÌAkrTâÚA‚€€ û"u@ºP$xÈÉt’rð#剎—É‘H¼¤ôb$Þ9FÐç”á5'k?J®'§C@"—H$zW¥÷#)Ã0‡Î+‘” ¾$N‰q‚Džèô{dðA9(½·à>’É|ðY™ÄŠI+I`y­6,¾ÔG›»vÖ.DçÄéJrw6ÜèkO†ò,1rß÷zÒ´A"1Ê_~vääì¼<ò3ïMGâÞ'!îÂ)е¿GÖæ³åó$Öþ*!’ƒ8«D"‘ޱ¹œ|¥”C¥k"cœ„›šn܈äóep$å M?žÈÈÏ îOLÖ‰ñqBYÇH¯,§‰ñd28ZÒ˜æ‰_«ßûÌ£|öf”\¢á¦7÷BFbð03ƒÃP\¢y+e˜ÕH¤1~rv^y_“‘ËàÂÓ~’I$Þ8’>Ë™ }v9(­¬”CË[†Å—°;¶ #š²=-m·óëÁD"ÝÐ®í¤¦t¯×sÓ±¶¯ç;EÂÐX{€¬cȬ‰”¿ü(l +›ˆµ¹ /P–÷ó:Ž„¬y?‰µLÙ)ÂgËçH|6ëGqÑŽnpMïƒk;¸{AB)Ax„«|.Nïé™óT9Þo&Édä2rég%M‚¤ìýŸÔ‘ 8_–èH(ƒƒ´n<ø Äµ_Ðo • ½ÕÒ:H$Ä@ þõûÍFiT4B'ALÚoñÈp.à ¯LdpŒø:*'ƒÃëee‰¤Aâõ“ x‰ôö$Hðð¡@à³s:°D‘[@ÿñÒÂJXsÙúƒ´/aaOí2Fà鵿_ܾ®$”Ò…E×ù³Ÿ-Ÿ'ÿÚð2òû`Æ AR‚üò3±;x‰DR‚ ~ï­¿7Δå½áÞœ(¬}´ÖþúH>š:r˜³J<œçI\:ríÎÚ¸ ˜;’ƒ„«„¯‚îݔЩ,¡#•Á‡)½†ƒ£’ ÊšexÃð´·§“Èú’“¯/¡< +îÑIÙÓAóÄS$Þˆ2|„áœ]Æš2‘ 3ô`JŒ“æ¤ù)Atʦ«Ì‘H3™È\"1Ãc<‘HJ_ à ‘—Cå$ÈPYúµDžLÇû Ä£AgV fqˆ„Õ…µÎàÖ$ìˆ ²ƒ<øìfr=w +×fw0qOáÂòaJâšcm>û[† ÷ž¥ CdxZÞ»I$Nùåç`G"·k ¡ùâ¾ïµ_J8ë æÚ2LéP­}´6µáƒ#Z'‘9”äà訇1ˆSº¡âQ(]¨28H÷n2øŽ„2‘<”R‡HNJ¥4uÀQIÀƒ‡7†Dú8ƒÙ¥#¡ä0Ü‚Ãè“>‰:ÊÒðüŠ1^ Ã@.5ep£»* |$Ró…~ù- JŒD¥ß,óÐQ‚„÷)uŸ%”eøøÁƒƒX1«Gʰ °Öߎ@9iÁa‹aßw .§r¼»éž‚K%>û¦Ëøk£_Æý°oȬƒé8ý—Ÿƒ}‰ÜÞI(ãþÒᨯĽY›Ÿ}¢H|Öß™H”Î'‰Ž±ÌÃñÖþg•+²6n‡<Ò —H†GJ$2¸G$ÎGg‡G^³D %—PB $8zç Á}F eðšrJ2©CÀ'5$åà ÀË@$L0Fc :¼ï‚†¤I4%oÌŽŸÈšr¨”Fw|õ¦=”¡<ø ‰¼ üÄ*{·2æC‘ðTÂ*Y4%FÂ:‡õ‡} òhÄʰËHì>Áµqû ,ÑU%’wAÖ¾ûkç_~ï ‚Kè`m1p‚ƒÿåbkbÜVÊ)‘—ÁãÞ¬Gœ Ž{³žC@œ«aí£89(ÃùĈG2:Ì’wȕõY{0"‘nÇPY7kÊ×]Sr™Ë)‰»™HM"sy–§—‘O"™÷Cü,A2.}ÞD‰¦äDBÉe¼ŸõHBI‰Dz1F4Ã;–#0Öd#.‘C_1àæ¤„™YF«ÈP)39¸¡-‘È7ö¡”‘—Áƒc§€œÔ™¼x·åà‘Ï5œ¥¥PJKÒJJ k©¡låmJð°e³‰Í•p$Ü,¬}ËÖ¾ƒ nâHéæ‚—ŸÍÚò×_ÿß(X{&”‘Oâ¾ïµ'A"qÊ/? +›rbCA@°6••“¸7ë¬Í½¿tÀãó°¿àèdJŽd¤“yyí3A.ˆN(%Ü}e(ÏtˈD"A\F9hÖ9ªü==/‘ ~-A"¡ŸOú,D <8ÈP)%F,—¬ô'HxÊPÊ:Rf‰£/rÃ0”¦es2”HLWp ¹±\›Î¿Ô‘ C7¥‹#ó3ã[wéÂe”ú¯ Ž×+‘Hxš'ಲ¿X*C }ŠP‚€xyMŒœMŽððeð:Cå™Þí 3ô¡Ì4fÌ=øB”cy̼„;(âoñ¨¡-A‚7Þ¥òLaÊDF^JàÁ‡ÊÒ(‘ôžA|4É FÀkr¢cA†–]Úðö¥Í’°‰\¢ë#»SHäÚß,“®$Hð®­T~ö5Ç_{,È'¸ï{íŽ4d”åzP‚”¿ü(lÊ‹šòÞ;Jà÷†ËX›{³6÷–Ž +F>û¤uêðÙ%t¥“éÜJž„rÐ~m:ù2ôKwƒ$N × $\FèœYŸ„R‚ ‘8ÅH>LéÑ$ô½%š½|òÄ'ÎrÜÈJ 2Íà:Ht&ƒ{'ÁáýK$¦ˆáV"ñý“pЇ!éË…€— F+Hð0‡¿ÅÄö²IŒ˜ðƒæä ‰ÓM$%^R áÝVp ŸÊAi5Àå‹–nÓ +[j"mx[»b+AÂvƒ„ÛW I(]a$Ÿý/L"‘È{ÿCdí2š-åz†y¡ òËÄÖ¼øÍÒvcë9š|Ž +”Ÿç€}ž/”ÏÇ/œL©C¥&)á$k‡£¾žï—µ¹q܉D“¸A’ƒEnÈÉÙq=Q'A¢.• ‘ËžN‚Ô—H¼Cp ‚¤A"£ÏžÈÐQ‚L)9N9Ññ~Ho˜‡²9&‘˜oƒŒ>‰†¡ÁˆD•‰ÑJJ¿òOôÔÄ~¥!OäPóEÍò+g|$¦L$é= ё𞡔ðIͰ1NкÀªZa´à6‚ÃÁ®… …>“ë¹\pûàVr ÷4'pÇ¥>û¾ ‘ˆÆý$Ö#X{ø`ãä—H[#[)OzÁÙ?;åAg8KþypðBdPÂÑ•qnƒOùµÿË…H޵_¯‰N÷HgÉ]7éöÅ·~Š×“R)9¸„ΠùÊèeu8”_ÑïÝBÉKè€ ‘øV@0–Ké7ñHòo>̱ÁX¼ †^˜Ð‘(c3riÆ•¥9 .#7®‰<1Ï¡/ÿ__ðµúƒr8ËÓMxKƒ#>‚„ϼ¥ˆ\‚X@´žV˜Ÿ´5‰ý +¥ µ×à“p‰¸t­°öuÃÚ·¯+)áþB)]s¬}ë]ÿÍò¾ïµS)cíŽÙB†{w”úå/?ö¥î½¹ƒ§q:¼,ÖæÞÇ$ø0åç¡(¡/;Ÿgê“8Ý©:ç¸6n—Ý $®Ò‰¾+†<”á†æ$zTù5AâtTJ¿!A¢#s  rRÇÛæÑ‹y™€`ã^™œžxŸRŸ4µ‡/”R¿Y'¥h$’ÒœŒ\Â\å291‡%Œh.£ŽlªŸ9|-ãô}à Á‡) ¼“2ôåàm‡¾ôaÁe´2<Ak(ayCm½°;aãB_žòpM\Ÿp³°öм6ÝD%™ìÚJ·{=|Ç_{H£c킵¥ñ²öÀ‘8¥G'šò—ˆ­Ú¸{ï/ÆÉ$yrïï¹ö™ §Kê|Ös%јsK$÷J%ˆC¾öi_×.] +$îKðî‘*]1ð8Ý}”HûDñä³Y[ÖN§ñ…>œØI8ÒR‰k³®ë:]͵}PÂ=’ƒÒ´ e wðÄä” ^¹‰\ïg1%‘œD¥ o ž–uH‰SÂG›2‘š# š¥òDþ\NÀ%¼C.›c28FŒ»I˜ CÙxääLãtÐiö– hyâ‘I^ß–§ ‘ñò¡²DRâ%ex‡ƒ>|„Iø°PJ%ˆ•!Á-]XRi‘[ö³S#aÓå\7.ÑàbÄk¢›+]êµùlþÚs  ÍŠ{³¶¼†ŒRâx:‰Dþò£°)Á‡)“$xpû9ÎÌëDIuÈçam>ôÅá|&rpŒ%<=¹ö7H·@Æ\A§»##—ÁMNs’hJNÊp7+GRÊIM"¡ ŽDz8¾–Ò»Š)IðàIT‚|¥¾*K¿„`D‰&¼½\æù ‚ѧ˜á‰D¥ >L™” Áƒ#)‡Ê_ó–ŸbäÄG<=i•¤,­ªÔ<ñå‚ÄÖ´Ye8öÑMq‰4%ȵoY\ûË…H·’ •n±{µùl1ä}ߟ="Èz¸÷)ï G% ¥¦$¿ü4ìËð*CóŽOŽñ‰N Ö&HðÏ>lŸÎáI‡S"‘pŒ¥Êrp @‚»&$x¸>• Iè`JâêƒÀ­ä_ÑîeJTðš¥N9勚2¸W‚Ë¡ÒÛ& •P‚ޤô#‰ŒW§²ÄHxoÍ.ß)PN“‰ñ8è”Ñ\•†-‘/4Mf$e4Ìåéåð*CóEMù- JÈÏì-ÉД}œ‰Ï>(ÏÅ$|Å€ÀjÃ^ð°qH$þàp;àq\ë;ÜJ¸¿¼Äçamw÷‡ŽS)ï=Lp$šeð8Ê_~틌ñDƽ7¤ŽÄÚÜ›õб‘ŽÖzkã”pPyéä‰9äytä‰ ‚ÄS ÷ÈÌDRÂ#7.”šàÒü—J>hJM7z\*A”$” ƒ‰Dâ§Hãó>‰’”¡)QSBókzZB§Œ\¢7(%|jÞ@ ®_ÂÜóUBdðs6¢²lœNΰM^i8: x('ã[Odðà ': ‘KŒ€ƒ¼ÏÈ¥ƒ¤Ï+¡‹ÃeèXÉÁ"ÚÏúûB@¢îüKä\“¸ÖßÝt+ Ü_¸æRSr¬Í_ÏL¹ÖæÞ4XÖ†—x‰ŒÓùiØÈË5Ût‚DÞ›õÈœŽD‚|?$úƒƒZv€A ‰‘ëÁ7H©ù•.‹[Þ=‚fÔ!¡ ·ONéJN"){%M"s‰Üw¢.¡ã$¸Ÿ +ŽÄ{ë)ÑtpÊ ôb?•Ëx¥×+CGj’Iø3Ð8 =3p¤Q) ÏPN‚À° cÊ7É#—Hdäå‹i¾¤Ä^ŽDbdð&¡?Tú8/41+`‰^h~Åš—¶£=’Ð [yžîšÌmJÎK·Ž2qCåÜâÏþBù<¬íkóyäÞ#‚H”¿w\GRþòó±S/l.ˆ§¸7k¿2îÍÚÜûð¼ÐçÐ1+׆'dè$Kk³6×s5ˆ”áú f¦ô4”“.] rÒÝ,»­¥#àC¯‰y42èx¥ä#ƒ7M÷Î%tÂh’èQ Oe¥ >(_Ôìw¿D .½mk“áûI_(%LÈÁ•š¤Ä)Í[ÜX‰\šá¡”CeùŸâe 8eP‚€|E?ø ñ>ÁÃ'úÔ§ …’V2”å`G`ƒ»†¶2rwäL“k_·®¡tOC‰óþŽ!Ý{D$† ’:kOŒœhÆé¿ü|Ú¯÷±Ñ'HîýšûaÜû9WkóÙLbm>û[†8œ㉌Žq‚Nøµÿ›<®ër¾Å÷HxYé6Ô‘ƒÒ$%ˆ‹I@àÎB: Áá²RÊà^#ù  ~>M¼2žBY‚èƒËÈ=" ÊÈ5%¦ ¥Ô!28Hœ_(Þ|¥/”ÄèãÄH,_Ô4?CiØ&à%ˆ±üâ×|¨,A‚•‰Œq‚d2ø  2(áæ’ƒË>W¼/ƒ[¢àaaÃò†‡-ˆöhÒ>Â9‡N÷bp³\œDb®ø  7$>¹öªÈáÞó¡1B$”Á½rmÁK&ùù´SäÞÿ¢ˆ³$ksaíf§e¨” øìóöÂ7H8²ŽÓ .åõœs‡ŸK.ÅI 7#®•¬”Ð)Ý;2 Ò•”á‚ô‰ô¹D2 ‚ä~0*A@”ð®$’I/œHžø€\†§Ð™¬É‘Kޤœ7i”I$f]iú‘¦¢ ÃS*O4ÍØPN‚„ÉüÂÜ~áe“øß‘øZ‚¼˜&*%FN¼áð¡ ##·¡œ%"–TòPZçAÇIØ&[N‚rÎâ¾€t­¤+¶ö¥sû$H¸¤¹³ H>kß}‰ä~Öæ~&Œ¼*A@”ùå'Ó6É[¼6¼¬s?y⩦SÄ×–ÁI“ÐÿìS×9AâÐéW’ÒÙ~±ö(AÐ\œà nxðÁ½Ó‘ƒ2ºžH$HWx<¦C”dPj†$8¼@†¦R"‘8ßÞéMÖçD râ‘&ˆr$”8½1ÙWIŽÄ”L¿ÐŸ©8Òðüš0`1•f2)c|&yòmF^¾ÐD"‘”/¦9ò¢þ$¼[ø8å ´_±há¥UA+?ØØ/Û<{WfБnS(ãÚ× n¢t%‘Hw¶„;޹ïXhJÄýLàu@°vS"‘ÁùÍúJý3aÇOœ†{£ %>û€}vâóü‡³„œR¡cŒSbŽ:\Jâ^$àPJß)à'®ÕàºAS*A 7tД Áç“rÊà:DrR‚¼šÉä¼&)eï3ÿ–I$“8eP‚ø»Á 1~â{Ä£ÄÄ“PÂ$Ô‘¨ÉÉ%N1iñcyБhnËê”yQÿÌoñhPJ$%¼ÛÐ|F‰ +ÊàV•XçÄúG»cïˆä'ŽŠKq¢é²¸;Htp=¸z¡é¶‚„‹ —zm—Ÿã+†€ß{2ȵ}íÄÚ³EÞ‡#™ÄKä/ÿƒxmY¥lßqo¹Ööµ¹™S4|öÙûì¯ Ôޱ„—×þGW@w/΄‹ƒJ_D ·L³D¢?踛Á¡: p…K:qzœ|r¨ìNj¾Ä#å ôè”ð«Î¯r8û~ +u_R‰Äˆ;1î /+y˜Š'Æfx$¥1ãÉ9sC["‘/j~M8•‰Œ\â”àC¥„7ŒxT§ +3-WÜ +ó6¢´AƒÒ°qÄ ‹nH÷h¸6.]tƒƒÀ-.Ýëa=˜òä>†Ã½ý~Ò´Y{ò¼¨y&È/?{ôÂþ‚xz{ßGâ~XÛç8%Ÿ‡µ}=çpRçÄ¡…ÃÌ_ rí/šWý‰„‹ƒ®•üŠÛç‘tåé:rèÎ&1®ï‚“òOx™<éõúHä4%ü>9oo¨?¯”ß2‰ùq %‡7#•I b¾—&Þ 4 òA‰&§<Ñ1c%HpÅÁãts["‘¾vðoÿöoÄˈä‘O‚Ä8A"#/½áAçÄÇ Þ:L¶b剅µìà°AH$l¢£î´‡qâ‘ûB$w¡à®ÅÚÐÅtd¸Åƒ¾tµaœHÖ1RH}$Rëq™— ¿üplSŒÜ{CÁOî½ã÷“ òõPG‚|6kŸ=ùÙŽÊÒAêÀ©†Nî¨Çz¾hÜ÷‚H¸&: ÁÃÍ”ÑÕCÍJébÉ“3çþbÊÉÁø-A¥W¾R<ò2xpø»/^ˆ¾äD¢: uÊÐ‡Ž MïóEÃí5è 3i0žÔ16‰üŠ;x šÆò…‰­Džè¼<™o~Rg2r¹ÄK¤÷,‘Lú¤áã뀷\C¥´¶-¸´ƒì—½'n„T†R :äÚÿ CÊ.£‰ÏóoÅòów Á”Ö34î'MðÈïý4t‚ÿò?…ö«Œ{ï©N¹7ëá>XOéAŸÍÚ²6BtD•/)gœ28ùŽýÚp ß,¨$Á]ŸàèfMöݼËHdÒ…•¹TJþ¢æ<•Hºþã2t¢f.û…å”_Ñ— æ”d¨ôɃÞI.A 7‰‘h"‘8ÅäŒqbÀÊàÁMàrPÂÐþÇxMð¯èû*‘yLßJðák‰Ä¼óD‚ø˜H¤ÕÀ)ƒÕóÍB0kN`ƒ¸„ÝäÒyvÔ9’®ƒ ’„¤Kt=WÌíCân‚—Ÿ}©Ý\ðaÿ¼,‘”÷ÃÚãE‡¾TÞ[äz%“¿ü|f§’{ï))ƒë¯ýt})ï“#×>BΘäølÖƒ£x& çs’}"‡kþ.ˆ«1èȹ;Ý)$Óšî`ðp1ãå'_;îõä0¥ÌÁ¡ ^pfx:Ùß% S–• ÊPʯô'<=™N"½ ~v’=’ÞÏ8xä$òÒ/™Ô!à2¦9£ # ¾_Âôesòd:Ä8— 0~ËЗF4 þ-óhdðM!Ï~^?t@‚ƒ S&‰ îýƒOú€'šÖɬ)[a »2ØD‡s¤áØ» ®€ M—äÚ¬»âÊ¡²KZâ³Y[L€3ãûa=ã…Ľ 8’{¿Êà ¿üdÚ£䤎-– ë¡Ó‚{3%AòÙL:‡gztâÄj‚L)9ˆsJ\û+†€¸#Áuº;‰t¡H(KÍàá2Î5ä“1w– :0o¥2ò3A¼ŒÈ\š Dr"9ˆ’ȼ„’—SžhJ}$%’^^†þàOcšd¨/aÄAY†érp´D¥5”aÍѦHDì{âL– p¶¥ó‚îHéâÈkß©kËÚ¸zp1÷´üì«MJ|žUÊÐĽY¦ +^"A@Hôã$N‡ä—-v!øýlœDrïfÜëïÜ»sïĽ¿hÈg•|ÖæsH§ñót*£s+ƒÃñž¼žcßupGB)ëHt›ú*[6N”àrn"NN@àþB_Æ8‰Ü5OJèH 2—•ƒ2øü*p%8F0Np +üE$šàÃ4e ðICOžè )J$Ò ÓM¿Á çI$§œè >(ϯT–áQð“:%’28ú, óaGÐIKޤ5/aGJØ&‡:Ϊ„£; gîˆÛáÊp â~uѤ øBßm þyîr|öþì/RbäÞ¬CLR®/e(A‚JŒ€ÿò/¤-(qïݼ÷Ά#èÜks?_+HJŒÄg?’HÜþŠsî̯‡ëaíë—E*Á'Ý .‘¨” èöIî¦ ×:¥x—º¬ƒSxÍ𵌼YèŸèà”¡RF¿äìpÀKàÁc 3è¸*eã1AR‚4Q% [ (_‰¦´DRbä+Ôé‹ãL$rPb¹_2(A@”‰>Ž ›µùlÖ!FA ³k—òÞ¬=a$4c=ŒŒ _;¿üKh#$f7 Öö:‰ë8=ºŸ×HÏóŸÉøl—p2Kx$ƒ£“,AÂ× +‡kãRÀ5Ée.Ý ðWÌE ¥„ËÈËn(¸T‚#1“G@‚ë#QóL2rpJð^Êð甡„— ‘ çkä M³ :Üp“<ø  n0þ OabĘåeä¥É§ÇÙ9=t@à ¢Ñ/ã彌 %ñ¤Ä·âã|Å#+­’ b‚ÃÙMɉtVËà'n„“O\èÖHwJòk_®kÿ_2p718Ù• ñÙ7\6@°ö XG†Á¹G8%ø½_Êàƒ#¿üKx­ÿ½·{ðɽ_p¬M'GòÏñ/–á~^ùyXÛIpÙY }ŽúàðÇÚ¸Áá¹82êH7ëÄŠÔ'n¢ä¡ŒñDÂ0G×9‘¹TÊáÛ²Œ\úñsPàÀ‡J¾Å#/ÀHMú|R'ÆëK“Mž.} ã‰Î`Š•ÍØà‘—&s(%ȉΉHp$}GÈàÃ×I¿AJÈ'A‚ÄË-BX«Aß’Å}ØîàpfÇUB§sî .º2’ÏmµqѸì– ÝSNà:¯}©×“÷ÓàÞ÷ö{kÈõ œâÖÁ½K¾Å£“:å/ÿ°ÔƒòÞŒ'2—ë)׿Þré E^:l$Ö>p,ù „C;h:Û’'×s”.9‰ËâItËJ÷n¨tÁÑ %¡,O¦C\íDò8žJ$¥^Ö‘'õË“³ã§ sÒ8"HÊú¡ôA“ ‰ ŽF¢ÄWixÊOØÓCǸŽoý«„‰¯‰R)OÎÎøH(ý8’WF^žø8ЗÁ «-ì$ ûÕiß%:À^2g˜8öà.B鎠+#ÇA\1¹kî)t$7z=WÛ}‡± ‘xzoÖ–µS_®ík'Ö3jäIýòäÛΉ’ò—VøOœ;Û+å½÷ý~X›ûçDÿŠS‡d=8– HJgXbÄ Ï‰¼ž/\û¿ëÑqq49AâfA_‚ ®žtÁA¢¾qg¡”˜Î Ó5ç$”M© M.c: N‰úÊ™ƒ ùŽ)¡D™£R4Ë逿á‹ic³*°DærPšÉ øVb<9$|GT’ò+^ Ozå™^ޝþ'|.xMøì HZ±³Î¤õ?3ìþàìAÓA §7:öîÈàÊ”p•0²öu›{Ž.©„ëkw¤;>TÊÏçs?è@ó~¦îí÷fmŒñБ¸w_‰d$8HðàÁù'ayû…µ¹·hÊ{3¾î¾ F®}œ@>ûÈá³å³YŸ ¥c,ãPJ§èKŽõà: JøN î6Áå*uÜ59Lé2N‚¸ª2ø‰»ì5¡„ŽT‚ƒ`Äç ¨ƒDŸ„òOxI?[BgPšEH¤f‚D"1ÇÈIMpãç$äà$šœHd˜´/<-b§C‰d#ƒŽ/…2N?ÑG2¹ ¿ö[æQ"‘Hð1Ã:(% «z¢ßvH6ÔÖcŽqbÁ¥Ž9ÿDç­‘×s³®ë6¸†'^n®»Œ¿öÅÁ)Ftîí‰aBâ>З ð48^"A†)“ä—6³ÎI $xp;Žçd˜ µËäsdgrmxtheŽóTsOO®}\Œ€Ÿ¸P îWp7NG7QI$O†Ê²ë,9ˆkžÇ4%”aPTN•~¤28^Rú…“x‰D +u&ᑬäHt$Ç)f`"sà¦eðà aÆâŒ g ‚DÆË1â«aûŽWÊAÜôXû;eí ƒE)å½ûàxÉ×ÄKä‰Î/ÿ$,oäöäÄ£šH¼~XŸ}¨àð •2œF/“á¸ÖLJœâÀƒ_ûØ_Oº¡ñï±8¯’+ÜÕ %\O¸§ã¨ÄHt£e臋â)9Ä'‘k–¡)uÀeL“€ôT*A"/‘H˜] C¥$g‚Àôã ÁA`Zš2¸Ñ:LIpáA3ò28È·ô¥Pñm3û+¦|áé½ÇÅ8Ö.ÉÉÚCFÆýtÀeL“€ JÈK_þ©Xä»¶v³TÊ$ø<½êÞ‰Ïs¨8>»$ñÙ8|PïËJ›K~í/]‰ÄM î6ir9ôr&ôÝGùB‰› .Aà^ÇyÍOG¢‰‘˜2it€KåKÊàúÉ ¬)£“š¤§g)ƒ¦Ÿ ”‰Q™€•&*ÁWùбübš‰ bø'¡<_å¤fã$ü~¥äHJà Á}^ðÁ²oé$‹ ^ÚÁvCÓq,Ãquª¥¦C.—BÇ¥ 2ôÝù¢»¶¶w%Aàž¢Kí¦“õ\s%G"Aîý¿;$Ö.±¶è4^îÍÚR§ ®¿¶ •¥§'k7%È/ÿ$Îåå°rJäå‰Î½Yt0äP)‘|œÆRèÐj’pž¡)A@œùëÁ+ƒÄ=‚;šá*ÁCÝÍÓt…áRC ‚äÍ0¤¸DÉà':‘K?>\\¦S úÃYr“éFŸj–h`‚`†jRoðŽƒ(ÁƒƒG"Aø +À?ðàÔó{$'%t¤’È8•}yr®IèHK7XahJØ>»<è„ Äy†C.‚fp·<\Ÿ¹P×#'®¤„K:(ñÙ¬Gºò%>ûQÔ”Í ‰µ'Éz2ŒšÈe/&':‰ûïÔ¿ü·ca£-ˆÜÓ¸w|ÜGé´¼ðTz„ÏÁÚ8œ 'Î0|‰€ègëáÚ~m\ Žó¾@ W)ºbž† ¹‹y¢./êéë#àƒ AzD†)=š4@"—á)ôóR fÑYæ%zŠ<8ùbšÄÜ;Ñ ŽFefi% >(ÃLô#å ÍÈÍÿÓLdð?áið¡?ü[<ñC rb A`Á1ʰïÒ +ŽNæà<;êe8ÿáip×\v›d¬Ð5ìÂ’²ŸçRã³qñ%Hxtï0Ù<ÁÚÖÎ^ž‚D^{ÿ‰¼‰—ÿòßK«*mÇÚ›²6:eŒ¯”ü~X÷><dY›Ïf=ß/''°SM@@®}毃_%åàÜ IW줎Dß.# %ê—ÉÜhptßÑÓ)¹lžC5C9({¥ÄÈ Óˤ<8ˆ)§¼^%‰D†¡'AàFB ÒŠAÒP•ƒÒø•œ„òkâ”ÈMþPNS’È%‰DÆ8AHæÁ%Hô¹tÀ‘XpiÅȉÕiËÁq +g/œXçÖ#tÂ'á.„Ž+#‘\k3×P‚耄›]v¬R~žòÞóA®‡û`íríÄz$;“ÁƒcÄë׆KÜÇo” ¿ü·`1q +’{ïB|-ƒë¿plàé‰>Ï‹¤tV%ˆŽD'¤2tp=¬«áÛ¤ ÍîFº_JRvGJ7‰tg‡.rM©,ƒþ2‡J‰†A.•'Ó!HJô#¨3ó‡€ËAù§G¨St’ƒ‡Gg"‘ƒQ ‚Dš¥d$8Œ_© ¥A"A‚1ÿücA"‡¯eø+ÂÓù\#aYʰŒÐ‘ÁañÃöA¶>8-8ƒfÇÄ wòKeD‚¸> ×¾V×þ¿Ð¤¼vÙM”ÐtgÏtÁ±ž›þÙ 3" yor)R ýXûe=½;S¾dÒËd>œåéQGþò߂ŌÓ_ئµŸ–J%ŒµepƤ&>Ïy ‰CK$*æÁ µO;\‡šè¦ 9®Èà®… ¾M$ˆÙ…E"Ã¥V–pßyTbäD3LŒW‰Dš*2GR¾~JF?r&HðøÖ“#æ—9iHJ$ÒDÅôKŒÂßfär¨”ú"øš‘Ë8ý+=-%üݓ錜hb>oX$–qPZ篴×v?œ´Ð„3)Ñé-;ÃpÎ_ç_éî¼p§b=_.%sPº³Äõ”k"£)÷f=ÜÏ7ν›§ãÞÒ´)㫟 ÷Yà2ø/ÿ]´ž³Úñ*{Y^ùÚÜÀ©˜’Çgãà…:±¤óŒ¤®Í:þé%á E‚¸AÒ£Ag. îéVB‰Ë ^FW[‚¸ò’÷¢#9 Þ”€2x͉‘RVJ$ò¤Žôâoñ´Ä8ˆ±FÀq–<^~bHâ%F+’‰ñJ$ò[ÎGyiìƒËàH&#/ÿSzY‰ù[$<Â8|Lr¢«‚VU"‘hl®›Ž–qJáì9Æ' 9:ÿ®Ü.Ý&:pã0ÞÝŒÜýERºæk_"scAæ÷Ãzš÷vÜ[¦‰õL¡DžÔ‘¢§#àH&ù?¡5,‡{ïàÐÓ2îý‚ûamŸ°6JŽõ ò9èJxTÂq…c,¡Ã_y=¬íûÂ%\"h‚èpÑæö„ÒU—Ááþ†ròD' yRçÌ™†‰„§u¼àDG?‡²Ô‘|døZšc§èӣބ!yÒü”ÁѰER'8’É?áipÓ‰Äry9|[–‘Kø»\"‘ Áƒ÷Iå·xd¹$,fØèœÙVÊàN:oÎdäŽk8Éι&þqœ×ËázXûÞq7CYºÂs£¡ŸýEsÒ4(ïû^OâÞx$aøÈà^p¢£¿¶ •“÷óƒ÷ÁÚ¥~(åWôù/c‡sµáé½;Éúû‹¡sï>îïXÏù‘sÀ>[>'°Ô9Ñq\ÁK‡YrÊk{\Ç÷Kp¹/nPŒ']4 âvO—Êà®m(]ê¯è»ï’¿Ð¼@”8Ñ SuˆÌKÔ ®/ƒÊo™GÄLCrvÀ‘HÁlA"ÍPÊà F.‰\ž|í@$8ˆiObœ ¯_;'ó4)Q§ùJý>xV :'Ö6rÙ^ÈAi»%!8]<N•8½$¸£âüƒ¸&éî\wM®å t%ÃôËÏ!ðÂ î —HÖæ~ÐÄÙY{•_9û|PúYù¢Gø“È_þ ´tòOœOù½w6Îν³3pòê|Ö.׿ó°ö±ŒN¬8æ<“aNþµq/×DnPNJ¸_eèK$ÒMŒîé\XRÆy©¹kN¢R‚ ÉhDü)O)Á%?EB |˜’4Ê‚ƒ€¼8›Ü„Œ—›Ÿ¯4fÁAtÈ?áéP)aÎcD_‚L3t@†ÊòOœOs¹¿òÊ“WG9(­€„µŠÜ’:°51n£M8c'N&ôáÜ:ÆdÒ Gç_v;$ˆ»#¯ƒµK×-YûÊpaÏò³ÿé(_¬=†ÊWÞ{¤œÜGçÞArmx9(qï—àÔ$8È/ÿeZ@öbíRâÞ¬ÝÁKzçkso<âÁçh%Ÿp%’Ï>–¤tbñ‰k³þŽÛqâÊ@ß=BÞåâ$þ_öî%Y’#Y“´.Åçµ ÛDíÇ–Þ)“ )ÌOàâf%ztxð‹˜2ÒMDYý( (‘XO 2;Kl1)OêHœâפD"»å‰º$ÊDr‚Dj"‘‘Ë' /œ2 OsÉe8€•ãD"‘ÃÒÄuE"Ab<‘‰) HtðåŸð4øðc)Að÷â¯1è€âK@"1Ò÷æûDRzD"‘ ÍÀ‰aƒYfŒÉÌ9Aó/AÂÖH$ŸgÅü¾€ÀæDž´¶Ò"¯½ÎV>Y;±öxåɽїk€`=Ö¾Bò~~AÊÈeðûùt@02OÁùÄ×üÞo ÷–{ç¤ ®¿¶È{;î-÷ µ§"8F®=`×Κ×vK‰Äô94Õ%’ϳŸ-ö"òÙb³$O0«G‚‡ÅAâÇEž|wìø$ú5™tJ%:à1DjJY þ H$FN±ðtP‚ 93:‰#L"¡æÆJŽD‚ŽD]u™— ƒ2^‚ä‰D"ƒ#‘þ‰Î’£ÿÖ¨SYÎw5Ò—,Ñ ’¡_¢(ѼI nbA`ªgÈ%·¤õ’°5‰¼ô¥…Ò7< /§À$D³$ “ ãÊepC.‘4ÿ’Û—hk$>ÏfÙµà°2ì,t$êXçaíeÇÚ²ö(Aîû^O™ãÞ¢#áæè`m*%‰ñÄg°¶—?–àCe‰DþòOè»’‘KÈËóLÞO÷ó³roÖCÍkÿ_`·v9hb}q=ÿF‚„é&\b†ÿ³±¡cYX¢ÍÒ~?+ h+˰¶aËðÈŽKš'OKGƒ”БJ"Ä£à ¥ëDÀ#—ð#ð(ø tñepŒD¥ 73œV%N9Ñ ÜIŽ—ÈaJ2(A0‚\‚D^‚ ñ÷™ ŽS"—¾ +"_h¾ðÍã”ð¾¥!™A‚‘ c Ùèšg"£i/m,Þú ù<+†Ïþ7º°‰ƒG¥… }Òék§Î s?ÇDBùb=Ü›µ/Ò ¼÷ïxäòEŸô(øð]þò¿Å÷öb¾ð{ÃQ¹žœy±6÷Æ„ðë™%èȵ¹ž_“àhVq:¸Á/ñÙ¬½#Ö„LG %9ˆ]·}¥}$’“ÁΖ-òä4IÜt@æ,€‡»!ëp‚—È' ƒ#Ñ¥’ ~2hJÈ%œÊ2ô#w`#— åŸð4rGþ¤Ž”'Ó!/4äõ¨rrþäG<‰¼„ïJô¥‘Pú¶Áá%ŽCÜH'ÃbA p‰&¤™çhd.g}H 5|viõÂ>‚hΪ×ÞnksmÖƒkš÷}¯}7”÷îàÞ¢üެͽoQ©ÄHÜû“š ¸Œ{svø‹šò—Žo,8’I9(½‹µá÷O¬=eð¹žy»1– †vPF#]â³Çþóüï—´)‘Û&Ë%1%¸œ+)+9ì,”m±T†RêhåeMrž…\"q:JŒÀÓàHÊA ‚‘®“Á)ÁËGïGæéHÊAyéº")ƒ#‘‘;ã‰Ìå”ÃYž:ÁƒƒGR")ÿÑÓáä›ú%ú~¤ïóÍ/qP†Ù˜„Y’& æp0´ð(æ†\š|¥ nA@`wäçÙ,»VV®½}°•PJhye}mÖ–µq r‚‘Á™ô4îçWà¾ïµïQâ~\B¹'¸w: ÷ÃzšàƒrPþò¿¢/­ñUƒ„æýU®ýÉPbí>ÖæÞr?y ¦¼6ëE ¥YÓa¤¡‰Ïx¹6Ÿý³"׆ËöEÎIŒÔ‡¥+AÂz‚ØÖ2,²2*KèH$í>¸¬”ŽÃàtHýI¿Ág‚Ÿè S— +¯’ Êpî@‚SàpB#—îªARbäD3œq%xèH$%F¾ñ(rù$È‹W³Rú‹L‡ )}É #Aúödô"dp¯ì…&̉A‡ƒ9ähnK#øY‘˜±‰½ KÔB}ž„¥ƒ2ÚJ‰–W‚\›µ¹¶\O¾è Üû\ðPbùz¸ÿÊ:p—$Hpà þ`¬‡{Ÿ;|Pþòà«ÃÈIÍÒ7¿”Hä½ÍÜk£/1r=CuíŸ$¸¹L2ˆGD~ŽÉ_K!A, ¸iJ´q¥5ìc©b#ŸÄˆ5·õr<”nÙ­àH$ôO¦3‚q‚¤ë4(‘”':H\¹Odp¸¼Õ?1O ‚d2œô¿Á†)O‰¼<™ÎÈIMéï“„É™ÇÈ|EÄwH$HðÞBx•ß4'eseÌ@0b Ñ f$²G‹0iGÐâHèŸXºÈ'ÑV‚ÀærÖ|Ë~ír¼SP‚œèÜ›õHM—‡¯ÍË'e òÞ?"2ÖFGÞû¸D"ùñF>_2ê„þÚåä½Y[Ön&÷fƒµö\ÁŒÉàá©-A‚7ÏÉü¦ä­Ãg7s ËÂO¬’v­´}DòS†UUJ$³Î¤N(ƒ;']†È%dÐÁ) p…x(ƒ×—S‚#‘H$F‰+O· 9(ƒwTÿ„§ ?qÏ5eþ'Χß.A0SŽàôxu”þVg")ñ’¾È}™åà]ÀÓIðBAš"p3hò†°4·ÁO 9ˆùG"mÊì )-T¬½\XK7 b+IØbXêõlúÉz¸¶» ‰9÷!úDë¡ã#Q‰ô™µEb$*åIùË_iä“ Áqÿ•õW†µÅDI(¯=]æP^û÷%‡²œÑ%0Ìà <>ϯ ¬I%A Û4X4©Iü²€‡ÅÄt¸ls‘ØkɉDíþÐe(£»!5e𚯃SIÀ%<âêL†§ŸÔï*ÊÐqQK¼äLàpÉ¥D ¥êOÎòåÂÓÈåüG“à ':':}3rPú'½…‘AÙK,AÂlÀ\A?L̤‰tf’z˜üRSbvv–hm>[>;ñÙbe+™€´¶Ã«¼öâ_;ãÚî,8×s"øÚ·emî>û)ï¿â.E9h®Ý™ §š¿üøê0~ïW%—©éµÿ楯=3C“#מ¨ñáÚ˜FÔåÄ$H|öÿ±°\›Ï«2œëÃKÌŠ9kh+LJÖVFK]BgÒî¿Ðw0Ò­¡/1ò¢~ â(C 2x¼ÜY#•‰é<"™™sJ0òGƒ2ÆôòÄSY?G"‘HŒDeß.ANüGC_¾8›y _Fô¥/|ð"Nz•Ò#‰ÄÀÄh™7£XT2(gžuiòCYÎŽVéäóìš„Ž„•,uJ ûþµY[º'ë¹äÞ—ä…~yoÖ!®P(e÷n‚Ëà÷Ó'k?’ ùå?À·÷Bó~¾v(ÿ”¸÷{ÁýGäz†jm®‡µ¹ÖAã +Òˆ9ó næá7¥Ô”°&eë<,š´}$ZFèçÄÎB)OzÔ‚OG@¢2¸s!#/ƒƒ géø×'à ÃYòàè¾Éoê—ÃR‡D^†:èƒÄé'gÿt¸ç¡‚¤¦LÊA$ø¼UNú‹Mâ[ÀÃw¢”‡ï38¼‘àÔ^1H“# Iã3ФY5½š0ÞÁ£]°)ƒÅÑ—vjm‡<²‰ •ö48H´Ú×ÃÚ¾®íîƒ 2œåý°þê¸7n”2xpøÌÚæ':k¦ þËFßÞäùõÞ‡ã”è÷ÃÚ’†áÞÿB®=?q=¼¦nm4¡  O¥1”ÆkóÙXnSNt^X+ ›«'+%ì&ˆt@`¯¥’´ïdn:‘Ëg“‡ËƒSæ©28F07 S’teðšà .*¸‰ñDþ=ó™Nz9L™” C¥Œ\F~&’WÂ_I‚ g9žÈ¾R†fßž„ŽoóO%LÌ•y‡94¨7Ì¡óbæŸÀŽ@Ùúà³Y›Ï¥Ó »©”°³‰^ ëáÚî\9Ô ÷fâòHÜÏOÁ|`m8”rJr¢òËL_àä  Þ˰ö HÜ»¼Ÿ_$מŸë`m®->CCøÂÐúØ 4ÒÉgoÁgg´)Äú`ÜOÉHpkÈA”ƒ=•am'aµ1bß¹D"Ý„p*%Gr&FbJLJK$ßy¢38e'ž– Á@epÔAR‚¸¥ñòxyŒ'%\xŒ /A”':Á#/‘LÊàó—QS"‘ èk™Œù>ïå…wýÂÇó‚ÌàÉ™IäÆ#›KNÌüÐjH?.¡>Çf}¶vÂ"±˜àÄò–hµ•×Þzž\G:‰³Ì]˜µ¹·Ü_yâ^ žJ¬ýT‚Äø)¿üoé{“ðUK$rýÍo| ëàÞå½1üL\_³é¯-ksm1¥> QÙKòríñ&°³&Ä#Ø#‹VêÀÒÅé~Y SöÄjƒœØú×@ꎒr¨”éìÈýà':ÁA:eò,A@š²r$8ˆ[*9)OÎÎé8Ëq÷Êà'gçôéd˜räNüÅþ„§C¥ Þ7S‚Àw{¢ï¥ ‘ð®ËAi~ˆ„‰*á‘!4ƒq `’ƒÃÌ7ù: vÜîœè|ö– Ÿ½zD‚œØÐ°¼Ñ:_;ãÚ¬ýC#¯ƒõ׋±6:á¶ ëaúît‚ƒÜûÃ÷þ)A@åP9ùË?§oL‚ y%ȽßN^â>š˜W¼6•’ãzxu”ríría1œ“ a˜¡s&>ÖAZ“ðh°A¨I@`Ñ@0b[OÜæ‚ÀFC)•aÙƒƒ8ñ]º8‰qp’241•%.(K %FðrŒÀ=ÄÈPG¢[Z¦$xIù#§£RÆ8 Žð“霂ä•HdÕI Ä÷óÝx/RIJ$r^:±‘ÐáÒDIÂ0fU‚x4i4áÖG‚`ü³Y›ÏÆÊõ ´¤à°Ë\ByퟔX»Ä:‚¼öe8¥ r?t…îû^;‡µËuœ)‚¼#àÿåŸà»ú=õjdÔ)1;Y÷f=ópŽ +”ëÁÔÉkß‹æÓSðX*å0Cnø))C3lSð°kVï”{ +bs-2”:ãà³øîÀx—Ajʨ3ü¤kƒD")_h"‘ îHä%ˆëGJaÊ‘èŠj†r¨œDRït×IÊA ‚D‚€ ¯g‡ƒüÈü†:“ùÆ£ðÍ`$”¾êà‘{M'Þûà) ”ÒhÁÈA¤ilP 3Ñ nÎÏùç ÖäÄÍ~µk±ö¯ŒŽ•ä x 2û›È6½Ô¹ö)¸vƵoʼn&’ûù×{Ÿ—{—rX›{‹…Sâ~žJïÎ0%ùåâë:ÑÁ½SHÜ»t02Ü`m¼ýA‰k³®í [¼•‡I.a±ö¨¯u(+sâiز2¸Õ“°’r°ª:• °ÑgÂÊC)A@@œ…²[ÁIù#…›y9TNF‡« îâO•%Fb)‰Ó¡‰—¿ÐtƉŒÓñ7%\¾˜f"ƒŸè€€‡¿^è`dÐyq~E|PÂ7ùùâN7Á£±‘&ªT<‰&SšXéžF3ï—%r‰sM`‰ìW¹ö®am>[lbiOC)m.ÁµY[ÖNt®ÝáäD¼ŒNÍ}dÜÛï¸÷5#Áu°¶Ë{ ”“8å—‚ï +‰D"ûžs9(OtNîý/Dz*u@4qïÿ©Öž(M¹¶cýc‰d¦”ÈFÚ„›ü‘Ò#‰Ä¾ õ!J+†–n¤´›ÁaaËàÁgµ Úw$.€q%$å‹iº0#q ‚d2x÷ +§à[¢»Wb¤G/ôA@\Ñ“:‰Œ9Úš\Ž‚D¾¨YS’È%‰Dï/3(OtNtørÊÐDß'‘\zeäD§7.#—/ÌX¼0ðH6½æ™„ÒœK4ÿJ{1RZp$Vl=´wkã…-a‹q.ûu0®ƒ®„„G%Ƚîqÿõà¬Íý´û9h®Ý‘8åý_þ ¾+œ‚ûøþå½ËPÊo|fmî-^nro”Ñ—06±öD­Ý7c/ gyâÃhŒ%HcµÖJé‘´Da¹tˆŸlenU+%¬³Ž´àJN%ü  JW‚ȼ|q6¹k§ŸèÃ¥’HôI ù#Î]ø ^"‡JéxJž ‘1N@‚#‘ Ž9¸DR¿A"‘”qzÔ)%NAR†¿’RþˆGH$ˆ/\ú¾ð“:Ò›záícx|‰1CbÁ¥i4®¦wI˜üÐ1ÿÁm‡ä J‹¢ü<ËE†µw´˜<*A`‹¡”h剼ž—pjŒËµJ'eXM‰äÞ8P ëçkè3à‰”¿üú®dœ>øÂe8’úò~Þ˽Y›ûamï#‘ ×¹6Æ M¦¬#s˜alŒÀüÃ$'òI»›µeXÆIÛŠÄþ¾°øXûTJåpmœ>9÷DB¹¶$RóÞî÷*4K¸÷ÇÊñ™_þG|QHÊ÷ ¯ÇO*Ë{³6÷–û w-A^\›ufÌ gè˜["9>ûÇdíÒü‡¶c°G°\eØ8‰Äl‰äàÒÚ‚—vù…5‹Ÿ‘':®Dp$2ÆÝ™3#/ã¼N¼ Ü­+A4ɉÌ\rŒÄ« Mà'Ž6’3ÿ†ù9™ÁÆIäeüèÄßd¨œA"}QƒŽyá ™×7bNê!p“Jã 'Lì`’OÌù‰uh;¤?8b}‚ö~ɵ}íD (ÛÍŸÑD‹,ëp\Ï/Ëõ\\_[®n‘y‰î tÜûgB®‡ÊIhÞ': ʵáÁù'ø®‚ƒœøb‘¬ãûçR |XûÃk¿_¢¼w÷#õã:&gíÒ°h–¥¡%H¤Ù^{Ô×N+ÛQ‚xD&­D›•H´zDÚJ"‘ØÜs¹¬¹Ž„.À '#ƒÃmA"õÉŸðyâLiNþˆGn݈2‘Á¸³ ¥9щ|®÷ÿÈ|lyd¨”H&%ü}ø‹šžJ(¹üú@ ß”%|É#ÈçÝ‘ÁÛ‡ædc#ØxJ¤ñƒù<1·•Æ{†¤-iGlM"At–­ —ƒe”°¡'ö·lÍå°6×fmYÏYHÎì®Jë¹3÷¦%‡½Dâ”{?—Áù‡øº~äõèU~ãE`=ï:ôupï·O¤©X[äµ'dífT^;ÃJc ‚¤æÉgÿT/«úh}¤µ’:Dr«WjJ[)1{j¡”S‚—š#á>ø@J¡Û¢I†W à nFbÊĹ %’3Æ»™š$8ã#èhG} } ò'zZJ¿a>0þÓe¼)=ýy}Ì¥‚—H/bPz}C¥Œ&A‚ÀðLš.$3~Ä|ÂÄJ}Ã<è˜viþ MYÙɚĺIåçÀʵw3¬ç7>píß—Pâ:Дë¯Íµ¯D÷$t‚ë¯}m°‰µ/Ø)J$gÞÏ'ãå¿üoñ½Ç)H$î¿~óJ$Ò£{÷fmñö ÈIyíá¹ÖƘÁ@J%¸¡Jä÷gg‹H[– #'mˆ­ ¥m¶X¶;t&»N”/™«ÂKAgòd:¤c%ÿ÷-Æý‰D‚¸“/ô勚åßà p·CGâ<^‚‘o<Î’ãa‰DþH$ˆ/HðÍCç•^b(AÂ$(A¥éŠÜ‚7–‰„6ÌDF}°aM¤O‚Ã*A)‡ Ÿýサ÷QZOŒ„ÒFƒ\û×ä\û8:œ Ny¢s?G¦¼ÿ—ê…?"=Z[$ˆŽ<Ñÿå?àõÕ)}·k¿©\*‘H¸ŸC_ÆÚoãeäeSį‡FKjšI‚: 0Èg¹±ç“ °#¥Åya³àiié[©Y¶‚ØåPÚnpéwDFîÃàyÜU)#?sP:M^—MGB 2}2(#w9Ï ~òê¸ÕòlòðèG›õì)NÁl4ÈɵÿÝR®íp+"×—Áï}a†J}¸WPÞ>h– ¯§'>ðË@_]‰D¾óõtÆ%îdmî‡õ¼_‚d‰™Y{ÒäµY›: h,e?%I(ñÙ¬½8ÅFDû"#·Y’~\@ÂJÚÓ:–7ÎÒjs»Ó1îJ„Î$º$àrÊd2xi28È‹¹iÊg“ƒ€8•“ƒ§ÿ¦¾„[]êH(ƒóê+‘”HÊàq:” /jJ$Òß0¸‰\‚œè€DÞ׈¤· ñéå‚`ÄHÄ81KѤB"A 'Ó ¢$ÁÍ9¬¬HpX%$u´qŸ‡õx[)ƒ[Ø“:Òjǵ×ÿU^;ÝŠà×Ó)ƒƒ8>ܦ{'îÝ<Ñ”ÁALþòÐW'#/á]¬Ç ôK¬ç•••x½èµ¹wŸ`äÚÓrmÖ3]%šC‰fUò¤4ÒX›Ïó¿b ” °eØ¥E±tÑ·¡àV¤½–¡ƒDº„$Æ9'%‘Á#—è"ÉÓcœtÖÊ¡RÇ)c"9FðòàßLŸt¨‡J|P•åŸx=­œÁÈ‹³Ÿ—ÁýýA@^¼šgé ô᥀à%Ò«$g"1!‘›+R³à0ƒÒ¸‚ 1ƘÙ&Í<,FBVm“D2GÖÆJJKJj M«-ÇÉu,>’ëa=t1$ê_×u?'EŽß›µe=9Ü»t¬‚ã”ù@ òË€¯.N‡òÞ¿¡”÷ó.¡³¶/ºÔ¹7 õsTBJá,_îû$ ¨s¢ã÷N‰Ìeó Oê7`“fÏà„~ƒ*Mo‰0áÒ̇E€Ž”HÚš¶Iòà6kÿ ÞÚ+)ƒG;k©‘”ö½G׳ø×>å _^ûÇÊp^b×f2Ü%ø€ÄxpŒ :¿ügøö‚#‘HJxGëqLYVÞ;絮à ÈÚ»öèÈëÁt•5 Ì^h–Á -)?{°aÚKhÊJybeÚ)^ZºPJ›¢DëÙÚʰ×áÒ¾×q þ†ùqL‚ƒ¼pˆôËA .tä?¡?RºŠDþ ó‚—”ÁáJsaJäEM r22(A0•ep$ÒßœLÆé¨,‘”ð}ryâ½hJž”^„Á4Ë0]f,™„,aJ¡,OŒ1 ›u`þ%¬F+#Ãf½è×Äö”˜=%ööú…§ÒîƒËó,ä$r÷äGÀ#$²>îãÎܛʹH‰ŽQ®-å7õå/ÿߨPé{–äÞM‰dí§DޝýɵóÅú+÷Ó¹ž”µ¹1c‰ÁHs+A>{¶ ¹ü<Ü"HžÀâÀÁŠé •°ìfèƒXêv™Hûw`Ð þ¢¦tR"Ÿì +Éé‘Û§{$ãtgD3”ÁË‘L"‘ áDKýIœ‚d28ȉÈ  ã§ ý qö9<ÂÈÉÙOÊð݆扷#‘”ñÆAÐTHŒ˜®‰l›IiDÁËf˜€›óAg°gÂÖœèã³ÿÿ%k§òµkóÙ¿2%fmÁ ÍI¸k»¼¶_;ýpœ¸$¯GúHÖs dep͸ŸRÆË1òËÿˆï*òòÅù— ž®ý^ÖÎXû]ËûxÅ÷ã ДÆFŽcíYj´"/ d³Z‚èKf¬ý;"?ÛQiNt,Np¿2“Vï•m%OJXa {ÖüL× ôàwU¾ñÄý‘œÄépÊ$OÁË“¹~‰2Né$åPY•Ž\_gÉ‘È8ý¤~ù÷¼>£ Ž‘ó¯WžxM‚€ )#—¾Ø÷RMÜ›%2‰Üx”ƒ¹ +}iÞBYΔ&f•ÀCG¢—²ùÿÎvÄÖ€´P8ź!±Œ1î3ø?-uð¡ ±6ׯ•à2NçeX›{Ë}Ðá’CXû¦Åé1òË?¤¯K~£÷ó.x ¢¿6|2îýÈë–¸÷oJ²6J˜µ]^Û¯ÍzÄŒ š¥Y%¥<øçùMùl1ía/NìKXé“àr°q¶ ·¡%l®,{ÙJ¼Dï†ÈA‰ÇG‚Ä·KŒÀM{áôIxÊcü%%F†:g¾¨éJŸhN~£ß2Ô)äõH rR§<ñ׆~ðÈ%’2æë•P"‘ðŽ"?³7.ÍÉA 3†~SˆD3)MiŒc ³í“xÍ¿½˜Ž•‰¼´V#QùÙØJ(1 ›ÈÈ¥5”¸ö5ÀõˆsÁe¹ÄýÜ?áRÁS õøKJŒ„ò—‚ïêd:Iéû—CM rq¾îÐ M$ædãdÒ@`¡”atKO'M5Öžp£J!¡´;•DFÛd݈„ÒJ$¥Ý « b»Á¥2¸S€‘Îø Dâì —9Hð¹]àƒÝ= M $/™DR‚8щä 8åOx¹Œ?ùÿˆƒ ‘HäIþæ“/jJŒ¼Ð‡oø…&¼5Œ„÷+5‰9ùsåiÙìIî%šÕf˜Ë\Foø-…„’K$m´D$” š°wXV2¬ê7>0ÍÍŽÀµÿ=S®}+&A"—qïk#±6÷>b÷n*OtH¼Dþòéë’ ¼9L™øü½Yï·&îFà S„díÑZ{Ò$È9„¼¡åMõçÁ¨CÄ +„rÒÖØ£2lÙ‰ÙÁÁbê€Àòr KÝj—ú.À $§¦Ñ$%t¤$x¸c GO| $øx„S‚ƒÀ•æ“HJ>J9(AeðA 2¼Ê"—HJø‹aä2Ÿ' ¾d2xG%óO`/|,iY epËubõ°ž­¬*l±GD‡€(£§ p†µ¯D —äÚMÜÏUáqoÖ#MºK/| $øxôËÆ|{‰ÄÈ÷«™G§à~ÞìÚà ïçåÊA_‚ĵ\f®ÖñÃéPJ#JJ|6FJŒØ›B@lPè[®«‡Äb‚KØY(ÃFÛnp$Òú“¿É¹!d˜k£Ï_Ô,AÂùмüÆôH•“ ë<é‘t¢A”D‚è*%È‹›?2ŸÁ8 ~RgþÚ%F¾™G§ 9Ó~â}I}Üë– :%äÔĤI£JÌ”Z$æy0êRŸX„AGÚ"ƒ[œAŸ¿.Ýçø•I¤m´È’ƒ@çÄúcí>qJh^ûѽ¯ + >û¾×Óœ)õ\­¨œÄý|Jü—ÿ€ó«ã1>~orɱž>Ö¦—»”¨CœHÁDù F›IÌÐi¬g¶:ȉi)ÎõáökÐlŸ•<±³Ò +‡Ï”ƒ5—hý%ÈÜ +ÉãåîLäå‰3%AàòOxêÜ: ƒ2Æ_R:Ë$8¦IJe9œ%‰¼<ÑA"%NÁȉ&È ÎÒ_;ÆGðÄN‚ÃûÊÉxûÒgÀ‘˜p9LiÞ$tESJNtÌ-‘a¶•þÄ.h¶š6HZ.Éá3J9|öÂbÊÿ³—”€€h–Á¯½ûà:k÷×ÎÿûŠD> áÂÄœ&¬ý” kÓgÖ†Çt ”ùå?ÃøM_5‰³Ìï28ÖÓG¯u=(²¦¼6k‹#'æ0zdP1ÒTvxä°H$F,i¿ÊV‰„•„=Õ)a£­3‘yØwèÿ˜Ž)ƒŽLèƒ`$:SMÆé˜ÒÝ4A@@¾™~RÆy™qvrpœã#Qù/¦I‚Ç·—HÊáUú›Éá,O?yõ}ç:Á½²A'8ˆWSNŒÍ0%1l‘KsˆQbz% ³1ó²- ð;RZ™I"A@Ú¸[©oaåé‰ K ‹ùÚ\›õ +$2z$ïÍé±v97 +\D¹6|Д˜>ùå?Æ8¼JÔñ¯¿ +îݹŸŒõø¼ñµ'‰Îõ̉®(A@ *‘>,Aðy~M>ÇÿZoòÃ^|£?«”X4¢l%ËÁ¶Â +si©³þJŒtF$ÈÐÁAræà^I$#'ÓtñrɃŸè`s§ƒÜ#"¿Ñ”ÿŽäLA 2±$” š¡”'ÓùQL÷UG^ö剈IøÆfða ‚‘Ræ2LlæAߨK4ÿÒäƒTNÖ*Æ­¬ÞÚ 8¬ãGÄªŽ¿Ä^ƒ S^û—å:r¸ž²ëqwæÅÚMÇJ®}¸J¥ä':ÿåÿßaœŽJy/‚ ¯:÷óá{¿qkw°öH 1' Êk³ž+ !G¢‰fØèFågcà¹á”ð³Â%·8DrX+‰‘°ƒ–qÐå n£Á¥MÑ/Otœ‹n‹þ$‰¤œE@HÌq“¨” H$È0åÈ ý's¢§$äÅ4GþÄ|`døîDý2NÇ«ôw®CB9 2¼JÔé›'à¡#çÅ%r¨4 ä•f‰ n؆J£.ý¦„Ä ƒK³ ŽûÙˆ°#aq %*%Z: ˨DÆÿÙèH‹,ƒC¿¼6ë)qír®D"ApïÃrojÞ»ƒ)±6÷s£Jœ}Œ€ÿòÿ‚ïðOôTÊoôïçÝû-ƒ k¿ëÁœ`m®MMCú &0¸Nƒ*ƒéõü²¬Q“ÌFØ –KêKKí ô­'—°Ñƒ2Ú}ànE9L™¸0#¡œ ×)4eäå7úÝ:9h¾ÐŒ¼”ƒ2Nwœ•Á1¯gçôálrŒœœÍ|2ÆIøk+eÔyÑä üfú#ðú^â  ‚ó½s}¡)’'¦NêÀ/Ë0ƒJšÞ³Dš|óãDz +ÞáØ5(ãólbŠ\ò¡²½– ×ÁÚåÚOÉàVhÞÏÉ×N¸B ±v_çDó…fËËà¿üWðeÊ{áßè¯MŽDjÞïkR:Òœœ¬g–®çW†¿IQ^‚4ÉŸÍ:Ä´ƒÀüKŒØ”à°A°SPÂÆ·ŒÒb6÷ÄvûÌ`Ó1‚\­Žî ÈP?ÆI8S ‘K8kPJ%N©ã$^þÍÙï—õù¸ ã§àŸNÌ_#ô‘HŸ9yu^å OA^œMßyhz}$NGŸ‘3 ù$ˆ)z¡/AÌ!cI`\%̰!ç0êƒÖáܩ쑱eá3Ãg³¶XRp +A‚ƒXöÁúcí;ëñõàz€D~o8”RŽÈÚãà$îýД¿üWðeJ_»|Ñ#9Ü›µß¬Œû`\{Z®'c=ÿ¢bäK}eðWŽÏžd‚Ïžm{Ø» $–E*‰Ô$a¹JÍVOZÃol.|@‚„¥Aû.Aà8€ •Ý$¥s$‡³ä Hæ¸å'ßoúL‰¤Œ¹Ï$”š Љ\‚`ã‰D"ãÛ%ÈÌßä…?2øü ù‘ÉA‰D†W0(‡ÊÞ¬ŒÓaB_$ÉIdfoÄpúq‘P]¥ ã­)Ã䇦E(¡#a_ÐúÀ¸ý"ñÙ¬Íg‹•LÀA+,-8®‡µ©í2¾ÌÚÜ[<%X»Ǫԑ?ÒäÚŸ)A~ùoÑ÷)á«F²vSÞG.ï/W¹Ô_[Nt®ós&<ýƈ‚€€|ö vø$”vAÚ $VfÐ)m“]# °‰a7£m-‡Yj›¤³ ‡ÊÉŽ‰Ì'¥‹t¢)ãtÇmR?¸Î S‚àÈ%^RõÉÙá‘gðoê—‰ÊòÄßM†§ÁA@¾Ñ‡?[¾¨yfäå ŒÜ÷o3täŸðôs£…äÌEÌ”šÛÒ$&:&¿ˆYž–Ã,Úçam>›õl+úY)[sNÀÏ®wC®ÝäàÒ…‘Häzp  kw$î}߈‰\þò_¡/s2¼‚µáPÊA9¬ý¾Ö“=]» ïcmòsUÂÂø• §|áég³¶¬=íH¤؈Бö¼l­¤u/í <±¤2f—IXðÐ,1‚îCŽ täÉ“3cœ7øÍÚïwí„—¥ ³µ‡¤¡’P® 7~Äd‰J$eól¼C)ý² Ö!|ÌúœèÀ–iÑzZU–wÒv³PF^Â=?ŠCDœ ‚Äq ¥<™AR"ùÓY>Ñ”y¡ròÝù‘ùÁ) H$È÷ßð›>#û¼DrfŒ“à ?âux:9Tz­Ht$FLÁH(’‰i|abÃÃǤ©t`Úý €Ã^€´2Hj†µš´n°wkóÙ¬õ,#—6º‡J‰‘k³6×–k§ë!A@î}a$Ö.£ë´ö¥JÀA”HÊ_þ[ø>OêxJ¬íÁïݹæ-ÇÚ4Ãx”kÊ9HkSÇÈ™HÉIÍðg³¶¬Ó+2;‚DÎN[^Îö ö´åà ƒŸt ˆÌ_tI¤§ƒ2òÒ!ú{| ΚÁÈPGb¹D"‡Jg\¢N ‚S¾©ÿJ¯æ«¦Ÿ”': ƒ¿¿ŽDå0ýP‚Ä·—'^Jé‘üfúÞ²D"g6$Ÿ4W¡” ç(rW¿)d¤ñ.aþƒ·å‰ Ò”Öj¶,>{?Ožr [ ž¶Þ)üÚ7á:èt€Dîc¹7kãFaD_¢ŽÄr òË_æ‹ï¦Î½ßŽ\ûi÷~D¯ú C FŒªðÁø˜Æ8ÝôÆz†y=¿,0üJX +« b›¥Ÿë6Ù‚·­aaµ‡JŸépR¾p:àÑä‹iÎ!J&±|ДøQ@†³<=t‚G×XBÿ;Aâô8;$NÑ£3¿ù±?Íøû”ƒË“³3N‚ƒ Jx//<’áÍ~ãƒ!Ársu¢)‘˜C%Ì*Lo©#MøˆQ‡Ò«ABDÓqvêÄÆY@é3ø<¬íëX[}¦G¥Ýw bíß—NÄÚð3ë“pj@4ïÍÚ— +Ip(±ž/ÿå¿‚//‘ 7"§Cî‡õàWÞOÆÚ³öH¹öh$ú0oòEŽZ2ïðÆI ÍÖ'¸ýAÛ—„ …å• žÆ¬sâ€`Ž —ßÌññ1.‡³ä ²äo˜¼¤ÄÈŸðp1òB$ø0%ÁH(ƒc$þ¦ôw“ëÄ|x¤ÏTr"y"A02Lg<8H/EF^Âû•Jò>sbºHYù#Á|šRÒèÊA³Ù1óà¶àµDíΤ‚§1ÞöIXRí/·æ‘Û}¬çb ×QþßMÒyI +‰\ÇÕ’HJŒ€ÿòßÂ÷ùÿ˜‘ßûõ¬Ý$áuë ‘hB®Íz¤‰’¨™¼a&Ÿýû‚Ï#h¤'Ñ"Hi5ˆämÚ/kX–4l.|­³2™ìÈàÁ‘¸àò…&ÜŸ“WÇÑ^%ΔÁ1Ò¥êO“€€ÌÓà:\‚ÄËãô¡fùB§ ÿÐ)ÄSŒø#eðåÊÈå ô:ð2Ɖw .C³Ù ’:1nÒdä¥7®Ði€ƒM0bì%l” °/'v'|öKÚ5$%Èça=Xd NdŽJ»µ¹ænoôq?8AgkŸ¦àƒÒS¬Rþò_ÄWúB3øà€Äù”ßû‘\»ï¥óà'>pmÖ#h®ÊõÌ[˜ºOýИX9¾öHËÏþ_1¤ »b;$H‹SêØ&ÌÒË‹)¡É¥å•H,2Nqð GC"‘ƒ;£.¡s¢ãj •$xð¿çÇÏhbsrÁ¡DRF.AÐ!ß<8^2‰¤Œq‚¤”ÁA%ˆ¿ä‰fð?q>}¹×DäôŒ„7ú2ÌŒ OepƤD"È‚4º0ɼñnì%,…¬ 4[¥-š_™D"±Œk¯$Ö^U‰‘ÖYZóA×¾¸þÊ::“ĵÁz¸¿pš@@|F¾Ðüå¿‹o5ÆyïŽ{¿¾{gÍIM¬ýë¡÷.a*Öî¬Íõü²Lj‚4{噟¿b¤5%ˆ™·eXx+#+mÖ ´wƒ2l"l%vV‚ØèÈ%šNFµ ýrp¸äP)¿9û§‡NÁr9(âSdŒ : ?Ò£þ¨Œ\b#/¦?2ÿðñ1ødž”•àH~Ì'á žœŽÞø$4A‚#il0NN4%F 'Ìíà ³lÂe4ÿ2ÚŽàÁÃBIûÕºÉÓ%ˆÅ\;–:4e .Ñúƒ`íã°®¿–.Iéœ8Gú÷ÃÚ*”R#÷Ó”Áù¯Ó+OêLÞ}_üÞo'x¬Ý‡·Ï%’É¡Ò\ ) aNågÃåÚec ¢SF[bG@¢X¨I»V‚ ³ªʰÈÊÖJyâP¼ð82P‚D.ávå䤎Œ\ÆË‘H“N«>¸Œ|?ÊÉÙùöï¾ËàÁ1⯠2úD|²¦Tʨóqú‰¾×DÀ¥’œ9xïR3A29c£äƒ²I“QG†á”0´Húq‰ñ&ÿL«2(íN{?(ƒröŽ„•\{Caa_Øh´ã ƒÄ +Öv‰ëù_.×#'÷qy°¶¯‡ûpÇ +ä~šÊAùË¿„¯7ÆÉ½ÉA@îýv88¯#'ãdíQ1?rƒJ£ˆsP9’µùìŸxèÀ:€ v$ì‹RÂNÁÊä¬ ­­´Ëà1Nl=ød¸ƒ> GJù#óÈÉâ%HÍ¡æ ÍàÃYŽwc•àRI¾ñ(8 ‚¤ü‘þɯ¼J|wbúI ‚¤ùFý5dÔÁ8’òoð^þ\7rP"ixä‰Iƒ€œèÀ”úM!ƒI”MûÐ.”ž‚[ðh¡$fÑ`õ$ìãÚ.?{UO¬³>ZpÝkSy²ö¿Ž¿÷ýÁý /±ž»4ÔŒ{£ ¥Œ\þòïáÆ8H/%~MyoÖý_z('M ’µ'ªqZ{ØJ£h 1b\OÌð‰?bìqJX´8öJ´k‰í“œ´¡e ‹•°ø28Nq%þ„#ó?«äÌáUÓ' ÁA@@Çöì(_xŠäþG¢3T–'¯Î«ÄÙá y1}‚¤ô·"¥² MŒ€ƒÄž” á•)A*Ëè¥ë$2/1óó'ÌÞàH#jVIpl^*aø-E‚\‚X–ÒB̯L‰v0>›õü‘%-²íÁˆ;0¬‡kŸ‹díR•΋µ¯ÖÆ™ +Žû9_ ¿ü«ô%K_þÚÊrí÷²þúh=ÜïšKA9’\Ïä„“HŒ¢4{$” F7ÖžêÓnòõ‰¥àPòIØY%‹¥´wДa1aa¡_Â"ƒÀ¦— áJàAéÎHÁ™*ᑜry§ÿ‰ó3ÝÛ$NÇ”#x9’äE¤|Ñ_[zÊ3ñ<8ú‡È:äEÍrPÜ›"ƒ/éÕÇxRš""¡ÙƒRN9èT](Kó,aÔa湄]¡©´,~Ûû‚Äö€€„±Î °æá¬]J\ûß9Épí¸8/ _¸E¹¼Öq© #c|>C~ù—ðõF.ïýýËõt&_hÞ^±\Ï»¾öòõ 3åµ'ªTʆÐL‚€øAÁÚÿv´žoÈà i/@@¬Œ„Uu n AÐb–C+,‘HkIéD€€€€Àá8eP½:Ñ”Áÿ‰q‚îjäpœ‚‘ðá:òåd˜ò%‰ÄÈŸèþ˜§š}LF~æ‹¿i–^"xç{ÏËJœƒÄe^¢Ù“ Áý¸ 3 2³mÚA@츜ieÊÁZ•-ݤ5Çg/¦µ-A`µA@,;8Nq bm®w: NM¬‡û 7*øÚð'¿ü{¼¾aex#r8ñûyšc=âÕ‡‘X^"1E²p9(Í‘&3”åg³öÏJ 3KÁ%” '”ö6‹— Ü>G«jyËÈKûÊÈ]†8Ý19é‘äÁÚÁr 2T–‘KtT1þ8Ÿ¾˜æù‰¤Dòcâ%gF>yã$ú‹…>HpŒ„ÏÿÈùèG9ÑD/Tb$*å`0‚i‰4x &6”0É;”F}æŸHX .¹} ŽÄN•Áý²H;íf|¶[Û8ÝjöÝ£ë9Dò¸vÿÚ‰kÿÊûam_›{‰µïU‰³ü—_2’Ò»˜¬#1JŸ‰^«Ô¿ïûÚóP)%®‡õøLÔˆÁ ÓšŸ=±Ÿ0ÏaÈC³„ù·'6(f¹À¡”- }„õ„2¬í Ͱï Hd8/Î’G^"‘'uæ¿ñD÷áò¤ŽÄH(¿™~"½ÇA ¹yÇ)/4ÍÒ$ á YÂÜ&y0Þ0çð1Ã?(-’VF*£’°hÐ k‚ÏÞÐÏ×ÿ#~ØhxŠDÎÖ'×sÖ‘]{ÿL„Nå½9ËõàFMÞû J_þ |·tå‰×´Ž¦²7<ŒÄÚÜûó¸6kF…S`ä39˜X¬=Ìò³=uiòI9XŠÒ¾ÒB…E {Â2† UÂòæD"±æepáoðçåG<•p£@tHð5_yRGbN+t¤’Èל—/<½\ ‰¹''÷nÞG:M'u$FÀA~ù—èë•':¸÷/¾\ûDž.±6^}¼ü~~t8ÌÌÚ#´ž £eÞˆ jÓ‹µ‡¹4á%~|è7¥œ%"vj°wз ðƒvö…§¥}ÿáÄ'_ Ç$tdðè"ÅxR"‘‘KŒ€ŸsÊ'A< +ò7ø@¼þlœ—JVæCÍ?áà qúIýòÅ4ýqy¢òЉ—{›ÈÁ#Ÿ ó2œ³Ä=„!,¡)abùPÙT7ä0óC»0 ’ÀoÊ7– žZºÒžXÒÁò†O–V»lß%t$΃pînH$%î}‚îMM§Ibܰ¶G.ù—èë-ãåÞˆü¸÷ûòZ“PMµ'GgƉLÂÔÁ4†Îgÿ¬|Œ1šmÀaò1[@àiiq¬O(íT{b ål+‘ H,8H¸$NÜ–>V¦IŒàô¯G•ÒQ•9¦LdÔ&Hä$Æ È‰’óäo:§€ ‘Áƒ#‘Hþ&Ãqüoè%Hð NƒÊIhžÔ‘fìG<‚Q<1´H0Íý\æR)¡Ärù#=*A‚#‘ÞæP9æaPb¦ëL˜Cy–Ä›dR6Þ3ö‰„¥@kbqÀ%›e㤒”°‰ÑzF?+#’ûáDòú×fí£.¡3Ü›®Ðzʵemîý4¾Ý0òË¿Ä|äo~8K¸ŸòåR9ƒh*| D"Ï)â¡#xÙ@~6k—Ä ; <šÿIIJëƒÄBÌ–Á* ¸´žÒª•vyä…>: #È”R‰SÂ9‚ŽÄ8Hðà?r>âß'T‡È?á)§¿è‘ô—CÍžŽ“¡²A"¿Ñ?Ñ9©Sžè`¤¿Œü‘>òéI/79Ñi$Ÿ bºþGÌäàÀ ƒéÆÛÀ'à ° °5gÂNA)AìÝ`CVµ±Î9Zv%®ýk‚k„ɵÏÅ\˜µ¹ÖÃýWÜ¥ðHÆËù·é{– ÷ó^ÈÚŒ“É{³6^ºD"A®=¨” ׃ +¾ö‹œ‘o ¥Ÿ•“µÿ÷K˜me/Ã:(%ü¸„ŽõZ+Ù®Ik¶:%ìì‰uÖ”H&;“ HÜŒ€ƒ€D‡h¨”Ô#Q)‘”Á1§U)ãôy:TÊ8=t@"—Ñ?Pr§£²Ä8NÁ)ÁãåAî/¢$ Jp$g‚à%‰Dⵂ(ÁOôÏ £b¢HÔ*¥™*n˜giÈÃäëHNlË2‹£Ã%lVXºÐ”Ö0‘Ÿ½žŸ6·´Ë³æ Êà ]ƒ¸Gä>X»™8M¤R")ƒ#‘¿üKôõÊûywÁïýƒ^tÌT\_ÿ×fm®çW†£‘kå‰qæÏ#Mµ™Ÿ„”Ѳ„µš´_hõ”¡´•¡„m*%¬vŒ;¡#_h:#/¦IÜŒ`<)ãôÐ9ÑARFî")ÃSŒ Ó! ßœýÓ¿ñôüM8ýDþ,^³¬#Q|ú¡‚¤ŒÓÿA"Ã+ ŽD’ •ñ1‚ä•0œ1Ó #-£QŸù÷›Â¥2,KÌY®AÙÞØDpÙbJ¬½Îa©mºÔ”P"Ñ”Â:ÄÝ 2G";¡Äú+Î’2rùË¿Gß°<©Ó›J$”Á5O¼\}˜Š{ÿñ{g\›µÅ!‘k•4l0PÊÁ¸ëÀxxÉÁm„ä¡”6E¢%²MÖ +‰<·ƒXO´§rð#ö:ì>t䋳ɑ?áÁg&_ÔœD"‡Ê3‘”pW‘h‚ Jd._h” 9Ñ9™ÎN‡¿Œ„>’3AÈà?Ò£þù1¢”Á¿Ñ*epŒÀ»ÍI˜©$a´0âÑ  3<Œî$L{&–Bê‡eAb}Nl<Â,”Ïþylk¯³í·ïàòÄ5ˆµÅzNJ¸'²æ$î}gîM×ií3u&’2ø/ÿ}Ã2¼‰i&åÐÇJæ-'2N‡i îѵ‡çÚi®$Ì[iO̧¹‰&\ÂØš°$”öJ‹æEr´Œ“°°ƒ]†GrP"qÊAébJŒ wvÈ™HdüÉM$‰œã)Q 2¼Jœüd:§ ùNàx‰Œ—#‘qzÿÕÈ™H¾þH M‰¤ü‘I$2N?ñ–=úN$†äo0c'>/Kš a€•&|PÂÀ7ÿ~e8Ë"-Ñ`³$ˆG–.l¢IùÙ?. ëøqR¶õ2øµÿ…“”×>¸¶¸pvÖóS‚uø°6®‰D"‘¿üKÌ×K"— èe‘35±ö›]ï]Þ»¼6:PâÞÿÖQ³ÉÁµg&j¹hu>›uðù+ͶôÈÌ#)-±,àöH¶AÄ¢Éö®„­ä’ÞžèØèšèȉŒÎ—DI$9(ƒƒÄ89Ñ 'ú|R‡€¿r8Ë>v yáꃟ耜|w~äOÓ÷—HÈ%ü)T’3#—H&%ȉzёˡi‘ ƒ¹ú‘yDNs›€Ã0çrÆ>9³Õ€}iq88¬•uƒÎx;(5Aì&Ö^XëLÊh»añ1‚Α¸vyíÿó×C®‡{³îÇï‡.ÕÚÜ»|¡ÿË¿Gß°Ä|ÿÄ#$%ȽYÀÛG"מ +¹&Hó#õ'MšÃÏ3–ñÙ¬-kc†‘˜mbÚe+@$'°#°8R «bÅ@‚ M,ѶN¶Ë2NŸ›@t4 „.O Mù#=:#ß¼U¾2ÜÒ¡²*'ƒÃ?mœ|S_GRF^b$Î2ŸD2 +Lyr6}FAsßòÍ4Ñ[&2—'ÉpÓ%¡Ì%i2q +ˆ1ÆÌ6iìmHØ$³2m“µ’6É, •J|ž-–°ÝP¾ƒ€t$ˆ¦+ÁAN<ºï{íÈ{³vIÂ*á‘*'ù·é{>d^ЉG÷~›÷ÃÚoV‚ ÊO¤™1Bkÿ ­ÍÌ^b A@Ìê`€¡³ bò­’-ˆ±5°S¡cÑØ;Ò&‚ õÔ'¶¸„Τ}O$” HJwc¨ì¼Œ:“A.勚%Ç3 eœŸ‘šÉ #krŒ¼ÐŽD‚œè )A0~¢s¢ü¤NÉ™ ðßñ…G“‘KAã‰<ÑñÞ%F  ‰>F ç7ž`$¦:¶¤½H,ˉ=Ò—°Y6Nêd¶’€à³ÿp–š„RÂ5à×sʵoE‰‘nνs¸7T‰škŸ2$ä—_2’òÄÛ‘õ%FpoÖ–^º„ŽüðkÿÛHbŠˆ<ݰ˜Ãh8?em 0 ¶Q/5KXXÁÖèKKT*­¬*%*í&Hp+üGÒÖ$%×#Î2‘pm0rr6óW‚ Ê“:“ÔnipMòþP)ƒƒ`$ÎÒ?\~ã3 ÁAH8}ÐÄ)þã@ #ÈË¡?(õKŒ€cd˜NRÆøK ¸!^%t`ä^hQ 2øM }i¶Í¹TÚ(¥Ò^H$Òâ°\v +ÄÆ µ‰|¶¸´Ý°æJR‰k³v‡À­(5%Ü™µoÎúkâ>X¥Ãuï>”¿ü«ø’¤Gäd^“¼¼}Œõ8‰k7¯)‚R°A)"8ég%Ö3º%l3/ ÿwž;‚VIÂfq»Fdè ±6”Èh‘L$sÀÃõM$îÌßãcr¨”‘ËÈËñÈý,e|ûä7¯þ”I‰d$ø‰NäIÿA§¬_Bä›?õáÑüsÈÉùTžè€Ä8‰\• €TJ˜ŽD•Ò˜I$gšÒ3Í0liæÁ-‚Ÿ.¹ÚšYbÅBÙÒ•m¢ùìÿ–7¬vh‚X|"ÃYº!N Ö.eÜOÇ]Ÿü‘É_þ=|Ã1~ +F¼D ^¨\:äÞ3$ks=STžMS'a aVehš[¬=Û‘{ðAæGÚvj°†–žÂzJXa$Äâ”n8”u$–\†NärPF> ‚#ޤ Mcœ|S_‚ü OAÐ?-”ÔəèO‘Èå üFÿD#Ãt’þ³@0Ò£o|`˜’`#Ƚz7-/¦93FÀeäf¼Dbª7x{Ø «.=…Ø#(Aì½+-&‘HJ´­˜Õ¶ò%j^ǸÖ>#rPÂIYû°¬'qoÖÆ’8#ƒÎ/ÿ?ÐW=‰oM•¥Ï Þ»¼÷+¾÷ÿE0:':Ãz¦È€Éµg/¸Ÿ’àŸÍÚ²v†¹ ³ ¢É·–DiY8r K$íTÌÒÙÁÁVI+,#— ¶¾ÄH8ДáÂHhòA‰drø.ƒŸL‡ÇÜI(åYb$r ‚‘x•áŸ/¿ÕOƒŸL‡œè JaþÉÓ åP9éωÊñ™¡ròï9?3oŸ€ËÈM™„IÃ<ŠæS•†|æÜÌ—° P‚ØØ—í‘l¿Êhûl"’Ò’®½ËDZíRSÂî—мö5%®}®'ݸ-k_ëáÞG ‰Ž ~¢óË¿‡o#ƒ·k£##—žbíwŠ^·<ñ48LÖ¬ç_`N1o“¦†sm>‡˜ÞAÇ„7ðæ?8ˆÕ ³&¤RÚ#i¹X=p›ˆÙJhú6ú¤­eäEpœçåD?ryR§DRâ[¢;9 â3DÆ«CâOýA$8yÙ?ÊIÍà ÁA@âôAd˜r §£O†GБ‘Ë8ýOƒS&^=‘HÎ49’ƒK˜´Ð‘‡AEb†Í¶T’Icb‚[°2–šƒ]Ó‘aûÂ#û8(ÃËÏçc¯ÁѾƒœ¸ Xÿz[꬯ë9Pkwxœ>øÌ/ÿ¾[$2¼ˆu”¨”‘{³>YÇúÈÉuü»Ê`öšÉrøìŸ˜ÏO¿/0êÆ^ vÖDê¿h›ìˆC;x¦õ,ƒ[áolýà3 H +pŒÀ‘9I‰‘“š‰ŒÓ‡³ÉOr¢ÜÀO¦3‚Sþæ‚Döá|dP"™*ýÓäßàcHä ôg‡J¹ü™‘Á0¼ð3ST†‘ƒŽ”0¥ßè¿0íÒð·Òš€—GÚ£¶,'ƒí³‰%lèÚ|6k/rpXvyymÖWé€ Î ÖþíX›{˽é4ITÆ4ñò_þmúž%Èý¼²Ž¾¹wÿÞ¬ýöeääÚ¬-ëI4?§ôãM£4¢¤é•ðÄ`r4öšƒ ne¢=’a¿¢uƒŸû(Ad[,‡J+O‚Ê…|á¼è—•¡9©s&F¢²Dr&ÜÒÿ;È$YÒ mG‰}_"ŽæGŸ&p…¥™õVÍ‚²ø!ªnd6ÂU-XoÑr¨¯ .Aj’¡r2Æ;š ƒ²Dþ9óü&ȉqšä¤Ž‰Ä”)‘ÈÈ%2‰D‚xõHdä¦èyóäæ`\aŒ¹„!Ìü`àllÍ`³à‘Db×`û”­! ¥=µ·XÚî°ì2ô•¥û!Öæ½ÿ71rrí;ç:Žå.("QY"9ä—¢W~I¯ÉáÚ¹öOŒ„fcmÞÊûÆ8 Æ ž–xmŒ%Ⱥ;ƒ6Ï¥§¥™/Ã^@Dzà!m“Í + X"±žƒÎ¤Et$,>—ƒrpQ •%ˆ«æK>}×)å üWè$FüÈPS>èQ8P92èHè?ð=%‰%ÎŽ_¢ãt(Ñ„—½±³ÄâX¢ú –‹HX:´‰RxmÖíõG⮈µo’àxïÿ/—±n®ÍÚ‡¥{I^÷WL"׿Ú剿/?Ïv¨kg¬ ª„¦ÄtbJòËáãA"cüS¼²µ_ëÚo$òÒT`mÞ{r@ÖÆ€ J¤ñ“xÝó©3Ž×ÆsÙ`KCÆš²½ö‰´Jðµ-lbØÍò3¶É™¶>” Ý Ðáàgº[ÀåI#È%9Tž<Ü™¨“€(Á%‚CD‡€ƒ =ÒL$*%¦‰\s#ôƒ¢ròe‰Œq2ÿ'‘È¥~"#/ƒƒ Jðö•gbÄÀŒC “Æ%FN4 *FL2GbÚaìOôa)lGØ{4Ø/8#•ÄêM¶J¼6kc‘Ѳ£}Ÿ þþ“µoyíï"¡\»¹6:òÚ¸£ ,¯}†òËÏáF"‘Hx5 ¡í—5\»¼ö«ŸDãalHð:Ò¤!?1„ƒáib_û›E6Æ:æ\*%‡±‡r°#Ò²LÚ ­•]Z@i+9›+9FÚz M$çåÀA@åàÂùŽóé§Ÿ r¢ƒ¤º$‘”p $¸Gr¨,ÑYGr$:òGƒ#ÈKŒ|ÉùôôÇ?4Ì™D"‘C?.ëË¡¦Œ¼ŒÓ£N‰Çۇ΃$="ƒaÓyä Wó"]ƒòl¹adèD^>Ð þ ¦D"O¦C‚wmrœ‚•ŸýÁ#t¦R*c<938ˆ”¨üg@àXp|'28’~[.C#¡ ‚ŒÏÛ'‘˜P1x1n>%È0Ãl¼¥R¶ƒÒFXrV&”°PöK‰Óí ´ŒHÚV"_[Ü‚·û<yßwÞ7kß$åÉ\;¸þ¤kªD"OêÈ_~Ž>á$NÞÚàÀµY·xãùÚ©DãñÞ¬c`À%ˆ!$H^{JñúSš[SmÂÁ%Hpóo#x ‹#AÂNAV­ìãƒvÖ#‚S´õH$’ÒuJn˜Ð”ƒ$Æ“r¨<3ÆIp¸$%t ”•œ€Ëàøò” CåœÇép@DIÀqÊ0%”ÍùÍÃH&AàGN—Á$F—Ô§€ JÃ@”1B“F.ÆIÔÈ%f¶Í¹±G¢b#ÀáËvÇx$í‘ °Œ²ÅDòÚ¬{-xt Țyÿù/ ]AÜUk_;ëN"¯ÊAùËáã*eðÞÈu'ÖÝ*×~ÑHšk)×.¥1ÃCÌaäšåkO)^· ‘–††PÂŽ´)Riƒ@,TðY42kh1hÖ‰}tä Œ® ãî–OôqÊII‰‘{2ÖAõ•H$ÈàŒÔŒqÒä’ƒTJeTJ$‰ dP†ß¦üÄ#9TÊè§È$ˆþ'Ó9y4+Kœbh6B%F¿à 1ãJ`ª¡4áiìeK![[«šƒ±€Ãùµë¦E–V›DWH¸"J¬ËDâÚµJ:kçYº”èÓáØ/?ÏvP‚\‚ ¹î÷(qíÒ+&r0!0-ž¾· é"¥òÄø çkcnåÚp4ÌRFV ÔAbAlŠÅájZ.9»f ƒ[Ì2¸ÍÓmú—8FÀ12tÏH$%È D"?9ûŸ.Ý™D šJÈ%’rPG"A%ü|¨S‚h’G‚œè€€ ‘óKÔôT†HðÈ;IÀ‰D~ÇãieybÊÁ3N¤‘“Ð9ÑA+ƒ?hàK»`Mäv§´Jàí—Ì-à‰• ÛŠµyíÿqŒXðà°þò¤Î{³¶¬}±€€À=ƒµ¹¶\t;][Àeä“¿ü4>çà×ýFuŸIäu°¼}9¼÷„Ä{c„ „I‹?É‘Èס­$&JFÝØGëàéв”vv*t$|¿p‰¾\¤­¬-ôA@ûM©ŒÜå A‚#‘n29(#— ‘ËàjJ$%È'Ý«‘è€ÑGRJ$rP‚À/‘¨£ü ÎÈæYr$$xœýŸ¡O”ƒrøKÉ%È 4 %’I*É 4o_b\=Lò¤ñŒ½ŽunG‚Û#b•B)õaÝ@[‰ÖS¯ýóÚ É{ß ¤|¬›n•kÿùJpm—kûº¹6æÚT–ùå'è㕃äf\›usí· ¢)‡†ä½1?¡sfcv⛥™L†uñ`È}¹èŸ´“vúÒ :ö ‰uG›hIA@Âò*iÙÁAH×BJŒ`¼KF~¢2(A>ùìvp6]¡¨#9 eðA9gN‡R"ñ:\æI}œN”DòA‰þRòù%\ÆéP‚Äù#Á5IpÔŒißuyb0#¦‰þ‰ŽA} oªaÈaàu$ˆuŽÄ¾€À* Ê–n–‘ƒ´ª¯;m1 Ëb÷#ï~àï?q‡@dP‚ÀÍn*èË“ÏjÊ_~ïÉÙéeɵ›Éµ³2Ö~ÅòÚ_.¡|o¸Œµ§è¤1“0RIäkO&''FÄ<Ãß2à†ßŠ9±)mMÙZ•1«M‹¹Ít>Ó¾—H\D‚Ô—H¤{É™ ŸÔ—'u$’É¡²D"1×&Q’ÈKdŽ)‰ä1åHŒëCYÖId§€ÄéQç‘ þÞ$ý#å<eüc³xä`l>q/Æ êÃÎïÉsþ¹¬Œ¦´Jà-tJ (-æ™–tÝ+¼¶€´øàRI&»(Þ&ÖÆ}W FÖîËwT ‚drPþò£ô!— ×þŠy°îwzm¿î×½¶¯Ni<ÐðHÍÉLB†äuÿÙóÚó9K$F 9Ì؋ДöÜ—KØ&e9X:XC(-æ™6#C›>‰DbÄ-‚D‚¸dN‘HÎ*KA9œe.‘H7d©—¨3%©#At@†³äá°ŒúgÊéŒ'àHdxªÄéP~‡§1ž” yà_AOe.#—H䃚%zÝR)¤O‚€€ šÒ<ƒKÊÇüÛ k¢$ÒúH;vmðÍ”¶²=}íD[ 2kN,>)ñ¾/‡÷!Xû™¼î«k—k'Ö[k8Ë\bä—ŸÀÇ ääºßšG ¸6ë–Á{‡þ¤ñˆµ16‘Kc&ANt` %^÷¹@Y6Ï ¹Ìå`/¬ƒä6%,tÚ¬²]“¶Èq»¹µ%“ƒÒâJ$n‰ÉÈK÷ F>ñd¨,A"—HJŒ<кáäDÇËB²îw'×öaýÉ{OÈûþ–YÛׯ÷  ØP)b˜Òµyß2àÁl3oþ}­ ÑiM¤%Gkv ¶¯Ô±_bsÉ–HþI—ƒÄÆ ˆk'”ò¤N ¹ ¹D"ñ)CnNå 3ù@'ÁñÒIÔ‘ ž–JùIO‘œy2rþÂ:ädÔ‰Ó¡<©##— ^}('%fBiäÀ'ƒ +MŒ4Þ0ê2,EØèHذPhÅ@œ±wÁÛD «zb‹=’Hìxë?ÝïˆNymÖ͵ýÚ¬·Sä‰Ä)¿ü>a$eða^—×]z餲4k 73\*A cºyòÚß,¯¡fR´ž†¥ì‹´D“}ÅX4$Ò†eTJŒXÞ}iÙ‰| éNHÀ%ÈP)Ý9q:”‘O~2ýOꔃ²»´„’Ò#ùIO%'e.A>ÑîG’òÄ#©ON>;ŽI|>‚fp$eðè·é€OBŸ‡r#8=êLöÞOêÈÁ AS¢Á“'ž7®d0Ò²!/›Ù:´ 2Z âk%lìô%+‰V®½¿¾PfÇ \áfXûÆX»éÞÈ eä×¾…ÜH$xÔǧ uä/?‡O§¼8¬Íu•\ûmJÔ‘Áñþ`íæÚóƒ™1ÒNËX{DabyÙ$—JxÁR ±,akåiðdÖ3‘asO츧DæIÙåÀÉ_pÀmó æd𓳓—1ž”ÁƒwUæŸè#qRB ¥œ’è”ÐÁpऎ“ÔääÑ©üLœþE$š Êääìp5Ëà Í€* ø¤y+• ƒAÅŒtør1öð´ KaG0+£ ¥œo–Äö ž¶‰åk³¶¬½Î°ã9\ÃÜ ¤D—É{?zïŒëOÖ¯J¹îÛ,)ƒÿòsô K$ex5±î—E¼qŒËaÊ‘xoÌ /- e˜=‰‘ׯÐB)Ñ<1ð0ù\ÚpX“K}œËeé$, ìã¤-tìø$Hp¸@tî<ÊäÌ_6ãñèQâ쌻B¥#à8˳äàã„Î?$’:1}2(A"/ñÒï”Ã4ŒË¡&FBy9(c^ú)ƒÒÞÊàM©ä ͳŒõÌRØ»ƒäÜ&‹:m_ ë¯ý/ѦÀ PqW`mÞ÷WLyÝwÑIw‘ü’u •':¿ü}“ ×~¸ö{‘k;¼hënž8fc06¿·0©,•8ƒÄkÿô žÁ6í&ŸH°°)¼´GÁí—¯X:%tlbÔ$“ –—€€À¦K$åà~¾,¥«#8ý/üåØùhœD.#?ïL®ƒ‡ã,K|ŠcrÊœîêLJ8Æ‘ŸhNSž‚‡L"‘ ýCœ€Ë˜æÿÂãpeH"yý¹³¯û‹¦·øà¨,AÞ7ëð¸¾bí[È55L鑌ܣu£”¿ü}ÂrPžè€\7¾8Îr¨/Áú ¬=B%HÓU‚€˜C9˜Øð¨Ä 6xÙ”vdPÂâ@)a§ oË&­’¶RJ[<"à ð‰>ÈÐÕ!ƒÃõ"A†) F†:“xˆA"¿Ä#÷'t¸äƒdPžøèC)OêHg‡&t"/Á§<ДHüu剓 %ùdú#'5K$“0  Ã#A<*1‚FQ‚4ŸrPÂHÜÃä‡~i#¤íè E"±P³\¤Õ+ƒÃnÖvmì2ÈÐŽK—8FÞ7n’Êä:¨ÓUâ!Òɵáå üå‡ðñâ$½"¯ýWÁµû×½åk—Ä„D#49(aÞÂ4Â|bí/”È1¸ š°°¡[3i@ÐfY7X½¡•”1Þþv\dÊäAM·8F†î9LI@%ÈwœO?½º?%”P‚D.‘Lbį +®òh– ‘—áGJŒ udp?+ó“霂Sü†É$’ä¤N9(a>9ûÜŸy4aJOt`¤Ï4íÍ¿´ƒ5¾õmâ2ø`é¬$¬¡’´³¯‹š­¶ä ³ûïýÇçÉ{wÜÔ×¾s$ÖÆÕôçÓO—¿ü>^àÁA0ҫ쵮ݔ׾rx”ï=0MÔ{»+ÑàhJà «|í¡å1ÃLL8”2¬€¥€¦5 »R3·kƒÕC[)#·¿šŸØ÷5¥û!8Hp÷L©œ >TJŒÄ”$øII9|WÎ5‹‡Lv&xM9(#—':ƒ~Éø‰~x*%ȃiœùk¿/ðA Ö>°6Þo~h‚¬ GC"×=3¦(Ìôeðh×µ¾P$t 4ê¥ù‡fp«aSÊÁy*­Õ cï`M´ž–78 ‹: é®8©#Ã%ƒ‘/q>xŒ ÿKgÓå©—JÊàÃYžÞÖÿRd.OÎÎ8éGdp$eð¡òÌá,sÿŠäƒNäÎäò}“ÏNè{éÁ#— æçDœ4øž˜ç0äžšùÁ.èȰ#°5šH¤…" 6‰• ÛŠµyíÿ~!2ø‰@¾ÿ¤»d˜Òùàî¬û +Z÷M5ü/hþòÓøœ‡)Éuãškç°ö—×íFbÝÃS‰™¨µ§ëüB þÚ˜Ø<‘0Òƒ¦/ bøÁmÄ`_ZœIÌfÙ¸ÁZFŒ„c2¸EŽ\Zó¿$H…ĸK&)§$g‚€|R_ž|×™Dr&Né.Q’ÈËó>4ƒcs&)ˆÊR\çDSÆéQ瑃r¨ôûG"ל*ÏÄÈIÍdðÒãá†#C³W‚ •0Ʊ7ç Ím‡´5°JÒZ9Æ ¸´z७œ|íï—ÙâµQ‚XùÄúGÞð¾óº®µ¯‘3Apí?ee¸”PýIý“:“ ¿üOxJâÝ ór×~ïòºQB§|ï/—dÝiœ”¦®lö@NC‹Dšç Ó.A`8¬†´,ƒÝ }kvMYZÀÒJ†$r[LÎ´ì“ 'Ý%F†n RgÐÁHTJŒ€Ÿ|vP³ ãîU9å)ÉÐŽTŽH(ó2øÍNbœÈñAç“óŒò;z:9Lé÷€€èË=*Oê”HÎD"{Ñ2t@!2ÙÈ•8Åp‚#‘æ9 yè›üAÙRØ‘ÖJØ)X.nïFÙbâµ±ÂÁ×Þh bÓ‰•‡;n ¹ö½r]×{÷AÖ†ë¯ý”w/­]®Ž ~òÙæ/?‡O8xp¡²Äµß,‘|ÝPJ ¬ÍLÑÚ(ƒ7i“fÈf2^Û›^i˜ÁKÓ‚6Â.p›2(íQX+©Cl¸,a1CGbÄ"C)ÛwhÊA‰¤‹Bæ“‘—nŒ|âÈPY"9‰D"?ѦL.Þ)“2ø˜<é)<â2?щܱ#à 8#Ÿxdè-Að)ñY‚‰\öÞ%N cs&ÌŸAbD#7Ì¡4çh曼‘v'¬’ŽlŤ¥QXÉhO_7¶X‰DZpðv¿Œ÷¾Ü%FpÞ0|m®›®¦k÷ùÈ™H$ òËÏÑ',Ñ«Ge‚u¿ÄyËÉ$4ã½ÿöH†u•DÆp0±ú ƒ¦ZF£.g ìËBZ‰D¢E#²,ÛÊ$8Úb$$¬¿D"ƒÃ½ñ‰þƒ®Œ„d¨,?yô•ÁãtL9Ò•K$’3OêL†ß£ å$ÈPY"‘~œ ‘HJÞÈ%¼;©¼öK¼¼q™‡Îð>&䜥¦+‘Í¡D¥|mÖAÓk°A`ÚAlÁ`A Ù¾Hë3Ø/©-„Mœ­ e´¿ÁÃiß‘K~â¢$FÀ1âΔ Á‘H>TJ¡RÆ8Û•:“ƒrè< ®^B_ +ˆŸ’ C¥òÁgs:I9TJ¿™€`:“'_v†³|x/½¬ å¤ù!à Xb,Ô”æÆ{°úÑ.Ø+ƒD'l“ý±t°’ÁgmÉÛí‘Dbýçrxw” Áq]×Ú‰uÓí„ðàC¥ùåGñ!J$¤WyôºËk—1³ð¾1NJ9nÆJ¥ä0Šæsí)…¹=i¤ÃÌCÇä ýöE>h§Ú2ë6 ›(-æPÙKŒ„ÒÊc$æ~ 8dn%/ãt(#ŸüdúŸ2Ô9#ƒ’î[ÉÁåOëKT–ÓŒJÈPYzz¢)õÉÉtF“¨SbäD‰_‚DSFòK<Î2—Þxp“C‚…ÁÌgè?hž4ùmA9kÒ—‹´M¡l¿ˆuCbC)mëº×íõ,»q¬Í{ãÒ€r²»…_[Je\û^rA•¡99ýOt~ù úl'1âí€à”XûÍzÑgÆ<)ÖFG¹È !¸4Ÿ0´Ð ƒf¾ óâð‰M‘°G'V,<:±öô~i—ãt´øDæ‰tK€€ .™2¸äK<:Ñ” 'Ó! HÊàÁãô}$.[ eèÈÐW–Ф3åƒyš`$¦$ ¡Ÿ‰Ä?$A¾Ä1ù%ç#|P"‘èË76:PÆìÄdž8|28‘0óa àщ} Û4è—öËÒ[Æ›+_û‹¦NJWÜ C÷Æ{Ëðy¥Ä\JD r¢s}(1à ó?Øý2úf!°_ƒ¥ƒ¦lÏ|í%}m¬pèXj›‰;ëÆRFWÄÚ—[Hk7±þ¼¦Nt"?ä—ÂÇûÉ£¯¼6kÓë^:esrí§J¼oŒtd˜=¥1'æúÒH#7êàr°Є±5 H,”´b2úf±ƒD†­„~vY)¡ »¯Q‚K%F»@B)A02¸‚ÊaJròÙ99ŸžuÊ/ñ_Jäe7v\䤎 Ò‚„¾ÄN‡2úA"QYžè8#‡) ÈP)‘” 'u¼îPJ$&ä¾ nºˆÌa&M3L$Œ·lòËh#l‡•)íQè—aËì`(ƒ[Iß)R ›[¶Ô2¸•”ïÍÚÌu1Ô¹Ö]®Íuãš‚ŽŒiçÓ‡ÿòCøxh^÷«á ÐÄÚ/HT–ƒ2¸áÁÚ¼÷÷K©”hêE#J@ÖþChío$¥y†QWi*iGlЬÏ8”Hd‹fõûª/#o—KAG‚Øý/qu N"‘Á‘¸0ÊÈ%Fâ,OÍÊr¨œy0ÍdÒ-C#Qérœ2tàLȉŽ>ù ü¸©™€ƒ _–å  Þ¯T’È?'D§D" ØPY6M/t 4Õ$ |“ßRL†ÅA¤o›‚Û5ÒJaÊ×E¶ÔfåñÞ7Ãûfm\%F\8X7×v—’GäÚ÷U©”qúðh*ù9| ”˜—èÍòµåK<Š÷žsEd^š4p£X6–üµ¿\^;Mï‰ñÖ,aæKØ (ÃŽX™›žJ»- ´ŒÑĬªE– ÁåÐm ƒwi”‘O†ËGÖÉå‰G':H$HäåÉg¦rP”_Ò£²‹útŒèŸÔ™Œq?Å'h")ƒG2é׆#à ƒr˜rä"—‘Ko6ò2rÙäÈ0W¡Y"1¢ƒ2 p“,aÎÃä{*a)BǾ„%‚…ÒÄÈ,5 >¼n¬0t¤?3Üò½ï‡÷ƽQêH¸dJ$ëO®ÍÚ”‰¼<™ùåGéC~dŒ“ëfݾöÈkÿ¿ Zw9 }¬]JãTI&Í[äòÄÇ81ä0óƒ¦uÀ¸M±8¥U*-š3%,`pK +‹ú °Ô<*Ý‘OÂ¥O9©)ÝBɉ&Hð¡ròäÑ™2‘Cå$È\È#áÑ#ƒƒÀ€@$NŸ3‰q†HNB)ÏNÔÓCÇ<òÑñŒ'%’3‡Ê²Jd.ƒÏœ úšà¦ #'šf#àFFz0í0üáŒ] 6\Âf¡]“ú ÖðÄ +Öþ~Y;}­€·Ñ9¬|‚Äp²ö•²ö7εåäÚ¬›ë`íûJ^ÛOêÿòïàÓÎ’c<¼²ðMQ>0 kô”ƒÔoÖ–u§Ñ’hä !¸4¢X{V ð SrÁ +Høf nGl ±V°e\†5TZ@™ËAÖI{ý™nƒ5ÝdP")%é::ÑD"åP9ùt¬ y‰/Åý,s‰Dz”HTB•#g\g¨œ‰óðé#È= ¥ Žðÿó<Á)Þæ#‘H4' šrhº&A)AÐÐJr$æ\šH%™´Av*”aé@¬!’É×½³°Åa£5%È`ýAÞû6xoÜP"¹ö}R‚¬ÍµåÚtA­ƒk—úå‰Î/?„əãì5ŵý:Òë–ï=ïírí§àCãT†‘1‡å |mÖMƒ=¹%Z ß,ƒÒ6øB±nIð°˜a[}$I.„A âÞ@¢óÙ¥ßyÔ)‘œ‰D"‘ŸLÿ”à1N\ÚD\×—ù™ú\ÊMžôôÌá,Ç“~O.•’“R)MP~— qúI}^1’Òl„R‚DÞ€Éà¦qP"1º 0Ø0ê½DÒFHËç÷K+&õÑ·‰D"_7kc…£õò½¯‹÷N7Æ™ ¸6n¤O'Cu_bÉ™ ¿ü}¼%HäòºYM(‡µß{.ß÷×Êuÿ™¡‰÷ÍÚ³DʵKiÞ‚‡„)š^˜9ù/í¸e±>Ä6• Ö ‰í#²•”ƒÒ‚GÛ 2(£Û@bnŒ :‘Ï]4J¡R"‘ÃYòAù%óèЉÓ{³“Áƒ’ú ,”,1Ckž‰D" ?N± 6ÄâD+f­@¢Gxí%•X7¯íV#m=FÜ®¬IŒÄµYÔ ¼Ž~9TÊ_~Ÿpä%¼Y)A@\ûŒÄº_ºŒÓCgxÃc¢dpƒGJ_.HäkO©é ¹™Ÿ!ç –"lŠ­f‰J‚øf‘ÑöÀzòAù í.AàÈ%'rÐ쑨Œqî"L‡Êà 1NâáÁ1¹Œñ‘A‰<Ñ9Ñ™›€hJNB)‘x¢ ròÙÁ£9¥ßÉ'u@*%9T–qzè gyº÷‹¤4ä‘0KèX:ë[É¡_£µ½´ÔehJ+JéfÀº}ݸ@ÐUƒë`Ýt­¥‰‡ÿòÓô9Kx5òÁ<=×fí÷.¯ã?U&G ²61K 0r%L#Çk³¶˜al ‚‡°2<ìѬO›­[i‰„Òb†……NäÄŽ•.$g‚ÀM"A¾¤GÒu§£r28ȉÈ—ôèLA‰¤;Yr2‰¤ ‡'5G&uÈP)ãáÁ%¾“2ü‹<•8Iyù%x›à H¤ÙÁH˜®Á£0‡ƒq<Œ´Ù&½˜´ °8PZ¥ÁréDn -æÚëmn b©eËÎŒ¼7J׸t½¬}¥”JɯKI®}AÉk{\û)© _~ò Œk¿k'z$1âiŒ{ÝጄæÚÞ„@yb„<AÒ(‚ƒ×µG·1–Ÿ8Ш›Üv„eÑZ"ø~ɉ]tNÚÐvVFÒŽƒË®ý3‘H7 ™¬3ÔéRJ$"ãô˜ARÆé¡ò`šdPƧw“— õAð¥86(5Áeð“éŒ _v"/Ñ?$1òÎ3§Cy¢ãm:“Áa0rb´J£(ì†/©3èHõ™›«TÚ2$:¥„Ý\{OÛY9è—¾S$ bë˹Ü|í;d.“äÚ‰k_A‘qzL‡üòsô KxSkÃ#‘™ÃÞx dP¢i19¥2Ö.e“&‘˜Ið×Ý™ap$†|Ð1üe_+RI$Ú .Û,b׸ä\¶›ÒòJ$2<µÝ'u&áBAâ&‘œ€àì„Ò‰t†œhÊgót(A02|Ù9ÑÁ)H$F— ]é¹ ŽD:ÊI|Jô#ÓL$’9‰Ä8 dPF^Â+ƒò‘ f òæ +#Ñ(N†AU– ¦š ‘Í¿á·9Ý  +†Äê…§¯ý‡ŸD;[Úb ‚‘¹¸.JÙ…C@âÚ¬-k_MD5œMþËOãs>©39(A®ý6¯›ós$ ¿ö™AÿÄP9¦NQ4± f 9—: al¸´2à V ÜŠ•ƒ¯•Ò>z.-,4%ê”°ã<8‰¤ ×ÅI×K(ãá®&"A@>ù®ÿÉœ$ ': H¤Ë™H¢\_æ²2¸‰Ó=šœ¾ÉÊ)%ˆŸtΔHÊA#8½»àá Hƒr06Ä#Áì退„A n€ƒÃ·Iä¥u˜½°/Pz„ÖÊB¡ƒ/­'Ö…E"ÑRŸkηÁº¯ŽX7ïïÍÚ'\~³„¾¾;óËÑÇ+¿£§½>T^7ëvoœË¡æŒÇûOÖ=`‰4 ƒá„¹…Öym Ž8+ aA‚#±;Áa­ß)ƒG–±ÝQp[Þ“éÌ‹4h^7½kôè}LËdÍ÷þf‘kÛHiÁ¥)^{z´™‡ù*íEX©)9)a›Z4iïtˆ´•JöÉ,28F‹ÿ‰cÝ%ÈÉ£ãÂÑA"A"—sw僎”ÁA%Hðà Ô#O>38È'ú HÜóD>ðäDÇað}Œ ÎC_B‡Ÿh>ø²ó(™ìՌȉìË<øÐ• aê¥A†Vji†&ÖAB‡—–ÅX·p ^›u ìì‰EÛíØ™x¸"t䉮}))“¸¶»Žð¡&—Êà ¿ü>á8S’OôÃ+ /ýÄ_ºA2]¥æ‰Ž9$¥á7´±öwÊIyÎÀe»)í)x+,ãáhñÏ™ƒ€€ J—L9(ANº¦àÑ ü¤Žü ç¼ yyRG•Ò¥”'Ó!ç1hB)cšq–dP‚ø Ÿ.‡)É'úç<“{A’“š'Þ>’Í ˆ¦<ÑiäÊAÙ JskŒ¡Äˆ!Ão)ʘei‰J+†Öm2¸Uµ¡Xå#aÁA@\kóÞ_.¥rrPâºÑ‘pÁ£Aü¤Ž<ÑùåGñ!#)Ñ‹+5A@®ëZ7ׯ»æ1>òÞ3#×öÁt…~¾MH“Ííëfm 67ó'ú–JXik ¢$ÌöÁžÂ†äP9Ù%‚¹(À#?Ý?gâ”à .±AG")‘œ9L9‚/ý”È'î‡;€\×”Á‡³tR!™Îrœ ß“”ЙÔA"ƒñS‚{Gàr¨/Ã{GRF3#ƒƒ˜±c©/ø*Á¥yi‡±ŸùG"¡o_È`§lF£×ÁÚk+[áĦ—J¼÷= ×öuß2øµ/œ×.qmº”@bšäÚ?‘Lþò£ø£Wr¯I^÷Ô‰Þ»ÄCä{¹„ï=Q¤•0~¡cJ‡sni9xôµ2 ûbw@Ú&i×H kˆ¤%•–4”Ò:“àÁAf÷•¡3(cÜ‚Dºs‚G^")Ñ­EÀ¿ãñô,9§ãQB#ã£tiKŽ<¸8”R9"9>%” C¥Dâ÷$¥2*ÁAt’3ãÓ%RÆãÕh•^} y¢Ó´Lš®A†“—æ†Äœ#‘椥iSìHk%¡´t ñÚLÇžÂC¶;lýº™› )ƒÇµ¯ k'ztÝ¥K dp$O•¿ü}Â2òÒK©” Ð_Ûå¼e¬»”ÁAL”„rxcfêd˜I$¦4L¯Ô|í/š×ͺi¶Ïà6e°8Ð,ÑfÉv­Œ–6”[X(ƒƒ´ÑrÐ{€— î +$:rPž7 GRæÖ" 1Nâôç#¹Œñ‘Aîçqð€€hJ.§LÊA Ÿ^>¨é÷“R‰|R¤R‚D~&’28¼#(‡³<ݫǩ9i´À#—F¼DÓ ³kr‰Ä:XD†}‘°Pƒ2l\ Hì&® û|ü}ÿII°vÇÍ ±îrîk—×ÍÚ\‘ÄÚ(AbœÄÃù!|¼ qí7E䃅×ý*¯[Ì”r˜>øû`íríÄÚ94¥A%•¦w_.{0ùð"ƒ[ 2ø °G Ö +$N·‰Áa7amK°Ô%’Éw’Ò¥1è” pç”Á="ept}ɼ9Ñù’ òÀm¬/ÇCYâOü,<-ëŒ Ÿ” ƒ2ü*©C$ˆNL)ÿB$Nùäì{MPN‚Àû¼œ!QrŒ°òÃ9˜Õ0ÆèÛ¤Q— 1 nw¬iÑ$t"Ýß,¥Í•':œ”ï —sE¬2\)HÎËçºY›k?•k__òڬ͵Ÿ_~Ÿð‰Nä×ý‚BäÚïKbíÒœh–×>Ë™ŸÁtÅÌ)M)ÆMïÚ£KÂTK0ùpÌRÈàƒ}Ñék#ö ÄÒ –Qg°¡ í¬Ì¥.ãá.È'ѽ!‡)]; ž|)8Åm>y2‚¤ŒÓCäÁÙä _Ò£Ò­NÀKÉ  òIýI$‘~9‘§‡Näå ÄH<ʨùÈÞ9Ñ<Ñé-Ëঅ ?1`Ÿ8ÖX†Y…až‘&#†¿]°&¡3Ø,$v ¼|íÝ|ݬ½§Ö\F.ß÷=Ðʿ־(ÈàÎé)_ûn©sÝ‚S"/OêÈ_~Ÿ0FÀA@"—ð6‘¬7^‚ Ÿ%¡æJ*%”0~ æSBÉÁ_{t_›µ1ÞHŒý ãÛDæ6¥’HX"$vmĺØG¥´¡Ð™´Â J´×“'Ýú$xtiÈÓLÜ<D§ ޤt‰•¡)§SYÆÃƒcyy¢s¢¹™k¼2ò2ÎcP‚ ‘ÁøÈ¿MzJ@”# Jäà ‘ËšH$Èð(½‚ÈËðf•/AÐÌL‚À\adÐ1œ²ù”f˜”0Õ3ä‰l;lAbq,Q©SZ·87Q¾vi[ñ-¸ ù{_²ûAjÊn˜ëOÖ~$ãÚw™¬3Ô*K_~Žù„É'õå ¹6ë–ÇW‚¿i9Ñ7Q'ÎÀ(‚DnVOÌp¬ÍëøïèXà ¶#”V&ìQ%·t%¬›´RÓ†JèpË‹ýÒj''š²;A"‘á9љۆLÇ)Hd¸ß¤ÁÈwÌr¢ùä࢞ô(8jÊ:2t0$xgxp$#àð#H¤>H%È'õå Ä§€qâEügàuƒDnT¾Ä#˜®¡Ù“Áab¥y®4ùDr"9,‹ÕÑ´DV J´zrí5\{UÛÙ“:í¸yï¿3ã}àrÐ ~í«&k_2 ÖæÚâ‚:™Îȉæ/?DïäwôÔ»“<®Ž\»\wöÞåPÿ}O”ã ÐARH0´Ði†_;ñÚòÚC.aò9F`M@»cÀa¹¥ n1Aà;…KTZä°à:Hdä]òK<‚ûä¤Îd·D"‘Èš.7Œ —Á#?I ßyèÄy‡srRÇIüDÇJ_úˆŸŠ\‰Žäßq>åÁ‡ÊÏz%ȉzÅ“ ÁMH(Kª2¸! ŽÚ†y0çÁAìì…RÚp{bÅBùÚÛ'm¨\.-,‘Ü^ƒ(IÙ ±6:òÚÈu'Ö¾”$ù`š#Èå/ÿ&}æ#ð*±ŽNhÂÛ—Xû½+ ®ÝQ‚¼ù‘ﻄ;Ñ1{Có)áQ‰×Æ`ç‰l$¬ ô1„DGŠM†•´˜à¾JJœûËeØúA â~â¦<s%“8ÄÍ6r¢)Oêœy¢ƒ‡È“éAßy¸áåàˆ>¦$ÁA>ÑŸß@b\_*‡J‰‘á³ó`$‰„w1<¼bèȳ•8§ˆGã'Mé`€¥>¸©&hÔgþ“–"lMp{T‚`¯ýÊàö×^K%Þû;<Þ›µÅýÊë¾O®ÍÚ\Š‹¨Ä) sæAMùË¿‰ÏI ï(8FàÌpÃ@0} ò¾gi4o%ˆoDeòÚÃ,±v¹öwJÙØO‚ØØÐ… +n×@´†¥­,í)¸„Eæ •P¢•¤;ᓹFœ” HÜE % 7—¨üdúI‰äÌà jJ$r¨œì†—PrèàSÔÿÌ“:~38Èt Óä“Ç™A‰‘Á‡}‰drèÍ– ÁMEŒƒ4TN?Ó(ybt‡Fæ3ÿ ZÒÖLZ¥à-8^¯W{*•Hδ׃à;ÖKÃM²6Ý9×fÝ4¡A"Aå/ÿÀ»~í7X*‘\û_;qm¹6½÷µ ”Rç½YbØ&Ã4F^]ð׆Kã}&(Ñ"HÛÊÁú„µ*aË<±†¡,íæ „åµÂDrÒÊOFW„ºOΔƒ« `ï3ÿ‘Ž•A.%>åAýÒÝ>"¡4‘œ‰‡L‚?99w`R'xœ%’3‘Èè'à¡#AN¼ÇÉàÁãéKhöÐ@ʘ¡ 3™â, —PZ¢Òr…¯Í:þöƒmE2iÁ‡)ÉðÞ7CE9\ûþ‘'ÓY¥ŒënBä—ÿoaP^ûqàƒÃÚ#޵K"sùÞóó>0`ría1å`n¥§ƒŽ|íÁÆë–ÁüÛäv„KeØ$2Z·dðÍ­g{\Ii»O¬ü'޹"@‚‡[: ÁƒÃeä‰+.?Ñ/‡/Ë/óDéÒ%Hp$2:/k& qúP³ ¿Jà=ùŽž– Á¿äñ¨²œ“3O¼MM‰‘2x:4B%F£hö$oP¥V†ÁF"Ãð·ö%ìNx$a§,ÔAû8 û‰½Ç$H¼7.ŠRÄ‚usmꯛk» _Ê/ÿ!^Hpœâ…½ëà X÷xȆgm—ïíïã[fxï&1“e˜ÛÐ|ýÉÚß#„]@26HÂZ…]+Ã>*t¤U-­-¸„æä¤`sct±HhÊÁT‡D^z4(1â¢+ä?òݱïú's&‘.ö¤” ù?(ˆ³Ì%*ƒ×'2ÆIpœò¿à¤<>}Ò‹ >ôöI©<1<šò¤ŽlöJKã*à0á†|°'ç¾ ²åzݬí¶R®ýýbUÉIÒj±ïkóÞ_.òÚ÷4¯ƒÏNב„Gƒ#¡üå¿¥· Ñ‹“ëF ríæµñÒK'¦Eò÷fm1TH&±ö¼I£8R†éUÊ௠—k?N·RI¬ ñ'™ ÖJ*e{7i%%'8ÅæZ[–Z·ï%]²&/],':1>âšâ—CMtûMò¤G_&¼;\V‚Ë)Á5ãÓeðá,ý©3‚¤Y3ÆG—':Á‘”':1Nà3ç#èeÉÁÓÉ&arP˜oÒ”&KóJC;"ÃlÃØ[¥D"m +,— ¯ÍÚûE¢”ÿ·! ápXêÁ¾KM÷ÇÚ^­ëæºý:]J2t@¦I~ùÏ™Ab^S\÷kŵY›k˵Y÷x$ò½¿b$Öæ}ˆ2Ö.qÎ$ mùÚCþú +“â˜Õݱ5 f¹ˆÄˆÕ“ m¥´¤°¼Ê‘à°àÄÖK(CgPƸ $Ü0J<òI ÷ȃšn¼/ñ´D"c<)ÿNgΔ]øÁë”Á‘H‡%A"‘H‡Kå$Fâ³9щ‡ƒ ‘8ŧ-9 %¼»¡rÞ~(Otš–É0cƒ!„á ǤÑ% æ™H£^*Û´±;¹mB+¦|mÖõ {Ê¥Vö=ÜdP^7ëæÚ~íĵeî(ò@ó—ÿ–Þ‚ ¯O¢¦Gk»¼¶_›µÅxœh⽿h’uóÞ¬Íû˜À0ŸaVõ‰Ì%Î ï)sv$¬4'­˜Ñ)Ã"±›ä¤E±Ý'ú¥ ÄÈн2·M"ƒº¾ÊwÝ •r¨”ƒ28’rx”.vh–:ÁOt:€‡HÌ9L™H‡ 8*‰„’#‘Ñ£Ï|PS‚D~&F|àƒ&Èù²8FàÕ¢oZÎ ~bäŒ"¸1º4´4í0ü¡cGdX{ròÚÿÙB¤Ý”Á¡¦uƈ}Öæ}_¸6kËÚ\[®M7Op¡²Œñ‘_þ|þ_Ò;-;&qmÖþÛƒèÔ9ÑÁûfÝî‘\›¦nmšI©)9ˆé%Æ{˜Ò”ŽYÁÖDî©mJÀeX7v0ZIÖ:áûev¼ î6Ÿ„«£Ô)A@àæ*Kte£Râ7È ¯Jù%=*¿£§ò¤ŽŒñ¹Þur9U~¢oŠËA o‰ s‚‘0W0{283¢abÃ$‡§rÆ~³àöÅ6I%±\aãtâµÿÌ›Ž=-A¢E–¹|߸Pgm®ÍÚ\w *×ÎXPIð¥üòŸàÂDÆu¿â¨ƒµ1<¸ÎµñàïÍú“¦ ‚:¥i—çèò2Ìv‰æíE Vc°/š¥ _.ƒÔ/a=AZXvúÒvà.ÈKŒtHpí@gÓO$ˆ \"‘î=$2Æ“2r‰Dgyz·: ¥Œú:§HŒ`üS†~üLgÈ0yúdp$2øÉtHø¨¥)cœœx_':Þuã¦åc†ÆO:V¢A•0ÉR“H†?lÇ _Â*!™K¤¾±¤àÒ:ƒOZùµÅåàpÕ¸IJ¬ÝO4%æ’#¿ü;øÀA‚ƒD.áÝ!Yû{ÄŸlr†é¼ïÖ¶A†3¸¡-A@˜s~èH_(ehβØ&> b鈄M,-æàŒ´¶¤D]Zsð²@ŸœL‡tÈA w”RI.1àpé•:ß%Ÿ^‰‡£›:\¢24¡3 â‘„IåˆOdpè—:ø”A'8HäòÁ4GÀ}Ô%Èà5!)Ãû’CçpF˘è˜LT3¬É%” 6'ÒÌۈIJðà-‘ gZ·uÿ— ^[ôK v¹Ô—°ìåÚw™Ô—.“µ¹6sÛĵ;×îO ü¤ŽüåßÄgþI}9(A®û_ûý^GG‚D.ñþ“µç ‰4™å‰é…>HïrmlHäv¤´5¡” +¼E“C¥5‡ ……±ËDŸ”•hñ‘H¸ ”qu@‰D—Ï™ n°dÐù¤{ïýOýÊ28>y·z(CSê€Ëà Á‡³äý¸DðIÔWâ!%>åA}9(ñIŸó— â5— C/zä‘&ä>ˆ¡ +³MBhz'Ã`+M>‘yi)`S”28¬’-+Û8©?iC FÚe9(ß›u æZH®›usm¿îÄÚÒE4\»ó‰G¿üWøü‡/ËÞǵ½„Nyí7ÞxH46ÐyïqÂû`ݼ·›=˜L (¡4½•‰&ÌùÚDÅÚØ|vÖgPÂZ Ê–Næö‘Àw +i‹Û]£ÒâËÁ…úrðÈ5]5àgêã!%ÜcPN7!‰DÊÈ%¹ÄCä\òepx‚‘8K¹ôHT"—œ€JýPJ$“Ã_Jr¢Ó'üÈÁ{©#AtB¹W|¢bBBY•Á\p˜F$¸©%L;¬”Ò¾€K¼6kï]©”a%ã3[öÄå@Ö}càèJ¹þL¬}猔Qóºš<8È/ÿ&}æ%®ýRB rRÇ1åµÿ÷1‚D‚¼ïùqfèx„µodPšÏàÁa˜¡”0äë~ø6y0 B@«$a¿ä Ø2J¥¯Øß8}¶;,¾Žä îp$¥k‰”è’HJW9(‡nB$‘×ey2OÁwþ@@†J‰~P þ G#8ÊàH$È 99;}Î’ð‰ôŽ@%z¹#rPš "yðAi¢‚›´Á(Ô2€16Ìd˜±9±,˜ "ökm^7k¯¡­ å ´Èà hÓñÞò¾Ó…@®û–¸ÖæÚâÂ)A®Ý¼6:rèHŒüò¯á3~í·<8Fàdp}¬ý³†Iµ¿JÌ Å{³¶¬ „Éä’›Þ‰Ô™|mÖ^i)âÛ„ðà–+—­á™aCmë #-ò`»5#oý%ˆ›d.‚pîšrPFW™Œ|Ä5x¢)1rR³ >TÊàH\õ•gÂS(%F0>~ +šPÊÊGw†€ƒœèÓÿŽ“á#EDF²×$ƒ÷fI ¹Ùœ )§²I“ æ°4œDš["A@ 6¸œùOÊ–¥Œ×ƒŽ5$ š¡œ´È8®‹µ#]¸6sÏ`í¦N(ãÚt"—×îCü—‡óÓÎ%RÂË’•|mi@t@N¦C†÷þodR\šÏ,AÇ⵿_ЊŒv¤%оSJ7èHû>X[ ‹,A,8øIWk!A.árPS’n$: ®²àH$ˆÛI‰‘/éi‰ä‰ûÉ8ÝIÙ+8]BG^¨ÎpŽA僳iŠš4¹D£ˆ&¶1ÒàÆ{hþe´#¶†è¯½_¯ÍÚ«7‰ÿÛZF.m18Þ÷ÖÛw 2¸.Öq‡ k7ç@—„rPbÅôÁùwðiG^¢·#׿Ú%rpX7×íÆàL$F #kÖ–u§$%Ì'”gšêhÔ_e¾ŽïÒš”‘[¢X4$š°‰\rŒØÓ¾\$H{]⃻ÐÍ 5epW +FB㮦AS‚¸Çp:ùØûƒ\Ë‘$ÍÖÕ¼ÆÂî×$8‰;ý~Ð +Ô¸{ÕEyDFg5~,ÒÌí"JsϪL8yYJù#]ú1A@‚cv2ͨÔ— p5—Á¥”1NÜ >hÊ8}¨Yžè€Äé=ÀR?”IéuŒÄ¼AMޤi*äP)ÍO s¥DƒWšF ³ŠDF“l¶M;‘ƒ€&ˆõÇgo,NÅD2×þûdä5cíÃ#N¬‡ûqÇ΋i’{“Kà¿ü;ñ̃#‘ÁãÞï·Ž¹7kËÚi0îÝQJ\k;’ëIhækßSZêÈà¦úEÃÿÙi#B)_ظÍ6•H$ì„5A«*ÛbÙR—˜et Jg—9é„ù¦C I â@;Ñ”p–H¤«D"‘1ž”O÷œÎ|¥Ì˨‚—”~—ƒ>F\••dP”HdðÈ'qJÏP‚ ‘ÞE(A"ï-OŸ1 ƒŽ43àfihÞ&Í$ŒhÝn+}O$F¬ >ÏßßðÙ[¦DÒV– Ö– Êh»¯ý÷Lp rïsã>N’÷¾A%ˆÛ‚£fð_þýÌ“''Ó¿÷‹¾ŸÄz$Î ¹ö,‘ázX›k³¶4 :Ò¬&2ož%”ÁûÏ“X[|S²@s²U’ -Údð6Vu°¿:³ËD9TZüÄ™Ð)ÁÁqŠSer¨”ÁÃ28ÓäÙ¬â<<ÑD29(ƒ#)‡ïÒÙ>©üDÇ à%‰n˜:•I ùÜ0(¤K2ÆÉÉtˆˆüLMŒ :ÁÏ7/18FLBèH$“æÇ8…2Ì[%‘ÄèšÕ“šÒlƒ~$Ò^ÈöEæŸë ûåp.,¹ö^˵¹6ëøÄ”÷>O†{—÷¦ó'8ÈPYÆ8ùåßL]žè ‘ófKÜÏK7 š“uPüÚäqúL ‘ÁÍê`¤Ã%¥äÆDi#8ˆò³¿5¡cqˆ Þf•aõ`¡œ + ‚¤í>©ãL@2 'Hp8dxðÈ¥Ã*8HäN3‚¤DR‚t<ÊéR9üXJœr2èp™K$x¿IpM‰ÊA9TN‚D.OtÈàž›Ä”/jJïb8ËÜ«/]†~e‘¤r¨9¸:/?ÑÁ) ðèNt\"å ô$FN¼ÍaJï½|¡ilJ˜+^6u $‡A‰Ü¬§´#P‚ØpÛTJX=h+­ª [ ¢I,8”‘KëJép Ž‹A‰äÌèä™ìŒ’ ƒ3Mä¤KòÄQùBd¨,ãåpªKå Mn8K <ø¸ä—Ÿè )qÞ9S~Ë‹é“ÄHTJÏ\ž^‚€ Þà¸ô f¦„KÒ …r²!äÒL†ÒèJr³-9‘6Jø<«´¬ˆM”ѪʰÎk/5Ö>ˆ„É}ßëñ¸wçÞÙá³vÉÉpï’.•¿ü;é™—ƒ2¼µK"9È ¿ö;]O6÷fír¤<Çi$®Íz¸ö÷E®Í9ºPF.Ñ´K|žuðA‘ø|>mMùÚ)è€Ø;» ‚Y[ ^"±àr°ûèXÒ% 2(;p^ÙI52o“Áƒw`")ñ¹D"#— p°ËJ9èÁHTNF.ý¶ƒÄ òM9T¾rPbž• ý“:¯ô"ð’Þ”“ ÁAÂÀœpŒ„Ò,ϰ5{²ùÄ·˜js›”&<ì[c_ÖæôöËÞäI9Xä°ï6ÉÚkÎÉ ¹ï{=9gARFWï'¡Éƒƒ`ä—)ž3ÁÚ(1ŽD¢Ñ’kb‰ãZ"‘®’Ò„ ‚—|6ëYØ .9ˆ-t@f-¦´ª%-A¬<™lý%\:@B9(8’ƒcŽ,è€ q¾ã%Ò!I&u’8J$#àìPrÉp©D¢I9TJ$ÒoRâ%gžè€‰\F.12xVáÒ œgŽ\Ÿœè€xqDG"ÃK‡Î$ˆQyaŠ`®\1{ h,ÑÜJ™gb¼ÁÑüKØ |öú|žl­`û&A@ÚPYy=¬½õD9(1GǽY[ÎgmthJŽû¸¡DR‚üò¯ÃþæìŸÞšÔ¿·€Kè`íÒxàú’ƒiÀ@b=sh,ùЈ•?¦±%>k/‹<ѱSƒÒ¢Ù>Œë k+AÂ%«–=œpU:ÀD× eéÀ ¥ ެà èˆù§e¸ç¯èê >j*IðA‰‘MŒ€ã[†ù‘á,Ç_"A‚ÿæìóž˜<©ÜcÇ”$xpxƒÁA¢×-¡/ƒçÀ8½ÒÔ•0ŠP‚˜UðòÄ`o"aø¡”Á?û_^’Á~ .ÁêýH‹,#oÁ%¬¿fä÷>4ÊÊXÎ¥ä ÁAàoÜóË¿O‰ Žûy•÷–ûë5iJýÈ×¾3Öã&D®CuïKDBÿzXÛÌ'NAâ6ò¾™O>å8ÖóY ’:dhѤþ`aC¡,-¯Õæ >.ÄîÇÈŽ‘“³ãÌ M阒œ ÊÁ§#OêwNJèÈ¿á¯n¨ïœ—q:”sO"9‘¨” /4A"—~ù‰¦¬_‚Q³Ä)Hdp¼Dzz'šrðØ¡)ãt(/ñ}o,…ÔtìN¹öŠM‚Ø8É1N¬*8®½Ý¸9Yû|èp±öaRgm‡3ÊA‰¹õÏfùË¿Žžp÷ñ +BS•åàW­Íýˆ1€2ø4ɽÉ%£¢¼ö˜™I"‡š0´H4Ï4ÕDBùÙ»ðÂŽ”]µG\"Ñ„u ËûÈKŒØb«=(añèXš2¸£ãEMéä9©ã¼*•Ä)§£Ò I¢ò‹;ÿ*G½ÔÉ¡òLŒ€JÿˆAGBä›ú§ ™ =©)‡Ê2ûã‚|mÚ2Û7(ÑžNBózX‡[ÿмï{í[¬¸„NÛåxXÕ°Ñk—k¯¹€”¹7ëáì¯'á Ñ®§GùË¿‚ží$FÀq +’ÉûyËqofpoÖÁ½ËóŽsÒ8Ì¡Iit»ep}b¶%'Ht>›µùl±2#¶éÌà³q°ŒÖ–J(MiÓK§ÃArp’W¢ÃGwR• CGÜÙ¬çä M$“ƒ28éÌ'Hä4“3åp–㉄ßJ*ÉP)‘È“éàt†ÊI$ž8NA"{Îù ò^G.qŠ·\giB“#Aà¶>(R)›7bCyN)1ÉÁ öd~ë ¡äò³YŸÍ:°\òä\ORyíǵYNG„ä¸7JëhrÜëÏc*¹w•%FÀùñHÿŠ×UïH†K/îýï·õöï} ÷1D‰µQÊàÁ¯=u×ÉDâ’4±2—Ð)ƒã5󟇵±8 Á-xë&¡#•í£±ª ƒ†5›ÖßUÉç| áÒ Ké:ÑDâÔA‡ž ŽÎÌÉ¿¡&‘HG=ø‰N—òr˜²{0oÜÓU"¡„$xäå‹i$å_áªÇõ#.½¨éÉ¿¨)A@@¼;pŒÀ»Ž\ŒÍŒK$¸‰’H¤ÁÇHNÛôâtòàf¾´öE*?{q>û_[ˆìÝ76TºJZaiÓ¬/îû^;Ý&‡µqÈÈ{³6:¨$ Á1—佩ú¿üƒôHeœ¯`=‘Pƽï¹7Æ€Ç}”÷¾gJÜ›µÑ‡R*ƒ9¬sXb¤ˆÑ•y©JX“:òó, Öþ¸ÀBEÞ® –ÑJ9([[p©Ä¬¶•t$ˆãÂaÊàá8Bâ¤"':w ƒ²cS‰o/#/£c_žÔ)1‚¼œ_§„_RÖ$?âRpŒ ߓ󪇉¤Ç+•‘ËàÁA”áÝ€À+†2xð0$%ˆÉqâòtiêÂ*AÌd`W6ä2{óîÒð9>41«dÝÀ%ZC%øIö÷ÚXy(OtàpX÷þ.$Xr?WA ƒµ/­§ ¹üå_‡'›õ`³‚[·3ÛJ©¼ö"''kPˆ ŒÜ:Á×Á½O!$%\’Êõ'š': ¿ü+ðlÃë¡d¸÷[»ÿL¬=ɽS Žõ4ïÍzèÉ‘”h,eäIÃ,s‰&:íÅg§}!X: – 9ì$H¤…t`¥Õ–Hì;œe8.\’p˜@ ‚¤t…Y“áÄSN‡'’/‘‘K$Ò^"‘ färP‚D^Âï§€ÌS MàHä ô‚eä2¸—ûÊàF\šp9%t U F.šÃ扄Ñ͘gM~9X +X¬½G$Ö^ð6n§à:VøÚ¬GÚt80‚ñõpowøD.ï£Oî}@¬]BÉå‰Î/ÿ =R‰džü½%”ýûríû‘ß²6÷f=‚µ¹13òÞÃC&ÍÛzÏ4•rí¯I‰äÒT#‘ÆŸ‡µùìÿ`­BdV/™•|‰m•°ÚaÁ%Z‰Né¬åàl ΢¡ãkÄ鎗8H‘” ‘ËA9(1ÒWà•õÿ†n+%ÈIÙï)_r¢‚dÉ™ÑS’ÁéPxü•{YÐÁÿo3ŽyˉDR‚˜†fÉÕIÃÜø•Mædã +b¤ÁAc¯‚¤ÄçYœªu;Ñ·Á¯½Â ë ­—<î?YûyoÖæ~X›ûç’¼÷aE&A"—ƒò—óäìœçkŠ:¡{³¹ÖæÞ ú ÃõçšÌ¿ÊA ³ ?hÊÖä³±;PJ[n×yb=ÃκZZg´à2¸õ‡cwDÈàšÑ‘"c΢‘p‚ašs’“ÎR9(ÿŠ®žél—üD¿~,Ìà ðûŸèJŒÄYŽ„$<™®Êà=Ò2xýoôA@ +Þï7ú ƒ ¯²™1Q“3f†P rÒ”¦Ä<ÊfÜ.`6åÅÚ«$?þ‹ ‘yäÖs8WûÚn÷%îû^÷我(cÜØDƒDÀäA_š@ŒhšÌÈ¥éyæ2Œ:ôeK!•Ÿ½8Ÿø<Ÿ(%ìZ M´‰8Å.—X[?8Ös2ȵÎ~¢ƒ.!©ŸGøÑÉ/ÿ8ì‹WóU¢Ž¹¿X»i<ˆ{8Ön®‚¬=]e˜:‰DšF$îü1A~ÄÌËá³Yû¯gDrX¥à ö|°’ÄÎZä2ô[p»Ž©/àÒ©òK¢2rÇW©” ÎÀ8•sœ‚ÿˆK ‘K8íó24ËÓüNñÏBR¾Ð ¹Ä)ð@Íàáy–5I(åI ‚/hД ^kœ>˜„ú¥2L Fbf‰H“v&ÈQ9ݦ:‡{Á%>{q`§ ”ñ¿ö7EÚD¹> ~ý‰—Ê{Ÿrí$ÇÚ—tär¨ti=(Ä%_þq<Øá,Ç“I‰‘{¿Ó{'îÍÚܦE÷vp0rk!O†æS"‘˜ S&Ý&-‚¥XǦÌÙ¦'°€³‰-,‘Ü|°ìP:ˆä $2œ-Ò‰N-‰¤óðDNÔ8=ÎN^žÔ9ÓGdPº*Q)c<)¿ÑïwHJ$§Ä8A"OêH'ø Î’{ÂHJ$%æ}%%ˆW<è€1¡”30F:ĘIN`cyâ¶ÆU"‘1Þhì%,ìËÉÚ«T*%ìZX@¥„ý]Û“kg´éŽ…µá÷îÜ;qoÖÆáÃë€ÄéqvòÉ_þ)zž#à xÉäý¼ÙÐÄÙ¼7ëàÞå½3îíæ'¹w*A®ã˲v*%ÖÃõ|k̰äàúR/™øì퀭9ÓB—aã¢5”ƒ²Í…†ç¶ špPàGÊ+D/4;Ê$ÈÐIXFg)È‹i&g")‘Èð!ÀøP)ep$剎 +’É™1N‚cdЙG”L"ñ`%ÇK^9xA8›JaÞ¬Œ:¤a•’“²*•Á‘Hó3„àMi‰F‰Ù.ÑäöbP~6V ÊÄ–INN,¦&ȵY[Ö^üÉk/û0'ƒ¼7ëàÞ¥óGÆø½Q_ûêÈ™ ¿üƒôH%’Iô.âÞ/‹€@'ÖF'8î‡õøÚÜ[î'MN(AfÀH¬=ë™ÕP6Ƙ«úÁ ?‘¹ü<XØ/('AÚ8û˜ÌªžXçàv¼Å?N ‡8:I$î +%œ]q::K$§y¡‘Aç<ð¡”ÐäÔ#ßt©<©Sü³NêL*Kà Hz8eäÉ$<^L %¼>è@38¼Í;_Ø)û.AÚÄ×þk¡\›k/¸\ÊA gÅÚ'ÆÉÚµ¹·Ü;1Ñ }Œ :¿üãx° ïH†KCå½ßà}Üš%îÍÚ4Xöq?¬íkß¹Á“P^kûÚ˜Õ28^ƒ­ %>ûo_rm¬LðVIBGVJëb ANì¬nÓeXy¥„“ĉ. Ž”3G'š™8Á%HÇ© <8ÈP9¹ô %Ù¥I¹D"ƒÏ/'I <òòÅ4 HÏdPþÈ\òH1—†ÊIoäD³×Ê%Hp4 “1nl ”0B¼DbØ`9¸¬l2%” $riÎaì¡,­C+óÙ¬?±PÁ-ÚPÙJk+Ã¥áÚë|ïà>þΚ¸÷UÜ8m8Ön*“õç¥Ð âªD¥üå_„Çã#¡<Ñ ÜûZ›{˽3îÍÚÜ÷£rí4Hà aðdñkO©\Û×þˆ ¯24M¸l$Ègÿ÷‡Êj°k ¶‰lO x[,•°ã°þ¡ãpqh J'Œ Q â(;eP: ÁAàP…ò•‘KàsÔƒ ŽÔ‘ä¤N‰|» ⟹ÄÈ7. J‚Då_q^íaꌄòÄ»I^1ÈPY‡Á€i)u$$9nÆÊhü’ÁXjžin1^†Kƒu±dí­Y;["X1©?©Ó&JØ\¬g…£— ÷>"ÈpïŽ\ç ”XûêÚÍ2r y òË?KOU‚ÜÏù¾žŽ™¸*]]÷¦a@7`íùIN4A®ÍËåÚåÚ³J$7À§: °Pâ³?(¥¤mŠ|6΂„m n‘eXp8¤’€8¹GʉŽs ÄáVB§DâH<Ñq´"‘Hä ÍàCåäþü•/^ÍÊ~^*ÁËЉ¼” ‘K$=ÈËÈ%X»\û‚µ¹ÖæÞrï„ù™Äõ°þl®=ºDæeS-¹içåg/x|6ëÙ 6 í´•ÄJðrÙv—V‰!\rhÀÁòÂ¥p•αS&áT„²t¨‚$8ÈPy¦Ox 2TÊA¹D"Aàw†RN‰¼ #à8¥§!_Ôœ”&FÂ%‰yøDæQG‚ ™ŒÞìdpޤ!Á«$ÁM”„é*#—¦qÄ|Ê\*M/x˜jM23O@`/äçùËØça=kUÂÆ•:ÒöI¸$ÛV9ÌF#—úk×y?T–÷f=8g0Ž¿/£RþòOáy‚ ™Ä)HÊûyA÷f=}‰¤„1螸·ßOº +.ƒƒ-¬í ׆˵ÇU^ÏÄÊÁT#qšyÉñÙ òÙ »b­‚ÃÒE.m¢=Ë ¶;8::œN•D†Ž³\âÇÚ ì0L@àt•':x‰ò:àPÊJ©|q69ºSê„RÊÈåÜL )A‚ƒ )A0J"8ȉHpOIéɃ(APçL$%ˆwZBG*#Ÿ n$‚ƒ ±‘HŒS(K“vbcÜ=0±.Ÿ„ÙFbøÁAìxùyXŸý†;x&ȵ÷7®¥æ‰ÃamÇÚåKp?28vð r¢2(ù¿§')A@b<ñú$Groø —†{¿ôûam_GbmŒŠ¼ŸòÞ"<µ;òÚ¬‡ëø²(K4á$|_@>.ÈzÖg°Y aï,c(£mm…­³ä Z8œp’Hhòàp +8Ö¤>‘ á`Tb¼“V~óWýèªìÌ'¡ òâoš%Høe—8F¢Rby9øÁA‚¿¨ùJ§€xærœÈq$òG\*¥W þJ$†D‚4!‰Œ:Ò\%¡4r¥²„Q,AÂÄ6Àe˜íÉÁðƒÄgÿûK(ã³çs¤åÁˆ„Å,¡yíµ•ksm,xäú%ˆ“D‰û¾×ƒÃ§òÞç’Y»:/\ùåÿO#à÷ñü•ƒ2øý¼/¥Ñ)A¸Šµ¹ÿ”{³îgr$ ]•×fm™)%%ˆa”0ðÁAZ ++2kÕÆ•˜•L¬ª„ý”ܲtHgE(gKGŽÄù&A@àœDÒ‘+ñ’ä¤NG=xè”З‘Ï¥¿¢ÛJ¿¤T‚€Ô<9;äDžÀ$^2y>:©ÄÆ=v(eäåI âµÇKLBJŒDc#ÃÕI£E$LŸlå „‰5º’ƒ#™!7ü Hl„ÄÈgoÍg'>Û¥±†Vït åpû‹ëYvޤĽYÍüÞ¬¿µO!“:ÝòËÿ==I§Ç뱿ÐÇȽoVÇý°6÷ÆU¬‡{³þ³„D^{ǃ +Mi˜O4›s ò9X»´>³P‰´wƒ5ÔÄ,oä¶\Z|§p¤HMpÇÑ 鸓yg£ ŽÄyûM}yRg2Æ;öA‚ÿ'åwúm‘ƒ£?(1•~XŒ9œ%þ“„K2xÏY×ÿF/‘ÁA¼_pša$@~Äð ™lº$ [pMrb,5K—¦WÊÆ[rc؈ÐÄg¯L|Ö³\à–d°›Vum®Íz-5F0N¹·ÜŽ—A¿<¹÷ýõeŒ“_þïñ$Að£„w!ëäÁ×nÑ—|¸7šÁ±6•kß³öü™¿¨ÙbDÿzhz¥¦4Ïà“ ?>ûo\§Ô¬lœTiËöT*Û\ ë2ø@_•eœŽ~¡&Lj>”¥ŽÌãôøî„þàg?: 38¾Åó?ùîàlràƒ·¬#OôaH}.Oš檄¾4oa¡)#—ÆÄÆúƒ±*e;"ae°ö*­ããvM³„e”Á‘ȸ6ÖJY™?¹7k‹«ë9UdÔ‰}$”¿ü#x˜'gçôÞÝÚï”Yû׎ ¿7JL¹î?13Ðǵ‡ ùÚS'¯í×Îáœ[Ì#/?Ï'fm¬LpÛÒÒMÚDbOa¥’Àš[ùpèK§D($/Aú ƒÒ‰7(‘8-Gn胀ÿ‘×¥ÊÉð˜ þbš~¡ M¥<½¾Kà/41?/‡2ø0%'V†>HxÎeM.ÿŠÿëěÓ1%ÌF˜$úÈÁK£õÊÁøÉÁ@ÊhJgtÏ‘æ‘7ÿgZ |6ëX%¥´\2r}‹ ¥,o¬Íµ7½TÊA‰{JpŒ¬ãeÊ®býy©RþòOáyg9ž” ¸ŸW#ñ’÷þ¬L®Íý°6÷f=43kËÉtyýÉL, ƒí6"y_â³×íÎ÷*Ùµ²KöqPÂþ‚ ÜÊ„Ò%Gг")D’pÖ•H$ˆÓ¼tØ•ÿ'‰‘P‚€ø4œè€ ÊîD¥ î’ò¤Î™HJ$#~ä'Hdô”êH%òAg8KîE )A”èݡΔäD$8LHð†€—:Ò8‘I¹Á4¾p›¯I 2Óû’r¦‰uÀÎÅËÖ V/\ª$×þ \{OåÚŽõpm·ãH$}íKàXOÓQó÷‰‘PþòãÁÁK&A0‚^è½QB9TÞûž¨ƒ{ß/‡µ§e¤TJ(ƒ7~×ÎÊ0®Ð”1ؼ4ùò³ÿÒ%×öõ¤}‘°Y ÖMÂöA)•­jØb +G4%Ü‘‚d2œQ/4x/:'ÏŒ9„ÉP)‘Èð!ЩÃC$r‰DÊð[!)OÎÎË#¥ŸWr"ƒŸôˆÂU¥D2‰ÄsÁK&A0Sz›P•eŒ#‘i’ÒØ Êh´HiÞ 9Y³”Urr¦17Ò“u lþKÛÊ2>{›ˆÏÇ-Z ×Ãz¸ÖÆFs¬}>`í¦„“$¿wÿÞ‰{‹«øœeœþË?ŽÇ{¢3T– 'u$îç[Ã%HðàqoÖæÞrï ƒ„DâÚSwm¦3Ãf›ƒÃäëáó`SJ¬½SÒ–%öŽÙbFk bÍC çÀà €s.8”JÇš ®y¢ƒÄáÊpö¾ÐA"A@b<)}Nê”n9©¹D"c<‘HJàð3F^•Ò3‘ Ce‰‘AÇ£þ—†Ê$¼J©C$\âà$8Ì‚ÄÌÈЩ,‘ø¸üÿ÷÷…S3 rÎ-”0Ø%̶i‡«¼¬”¶ƒHNâ³—ö‹K(aݾ™}®½Ë¶Ø%JÍ΄{3eÜ2gËýx ãÉ™¿üƒx¤‰JÙi pž@GŽ&hÊ¡£OR¸ dpö.IAßî[@ÀM9M%?³&Fj•eü•Ckï—œukõ&ëÀªNj^í²DâXpˆÎP)]ržðupï£F®}5òšà/jÊ_þ<Ì8ßå ¹÷$å½áPN‚{¿è{'gIÞÏüÜþ/¾“¨s=Ù¸Ji^šöÈåg¯ì‹ü¥…"Hdëf‰Å·ªÐ‡už´õp8”D:7dp§F4áô›D"á8-¡äA‰ä‰”ÁûL ‘': Ñ/)e䉌Óq–y‰ógäAâÉ“:“ƒþ#¯K•½#©¦#‘”Ceí¦laåÚ\[®°ÝeXÿr=(¡ƒµ™S÷nÞû8"å Œ\")ùgé©J$2ò3qï×7Üû=†R¢¦2òIW×þ…X[ÖÆÌ@9¬£)•×BNNŒk¸ZšäÄÌ#‘ølÖÆÊHÛ„¶ì•>+öJXØàaëA†N GÊ qLƒ#áüÎÒ9¬ ‚Dâ”8Êùp„#'Ñý2ºD†ÊòïqÈ ô3N‚`žUðàÁƒÃƒåÈÁ‘L"‘áMMN$\*½w eäeW‰ä‰„rðA¦4W J"}AþŠ®Ê†S‚]$F&¼Œ\Zi;%>ëÁZ 팶ä:X{©AâÚÿS*%æˆ{³6÷fíFâ”8Êû§“ê—=Þ3%z˜*ï?ï93~t¿*Ì\û÷YOj‚ĵ1¥rírmtJó næA|öRpËÂ¥2lÓ«´e˜5´›H¬m‹,Ûzt€8% ‡ÕàôÓ‘pŠJ%9áÐ*% 2(Ï%Ht/‡.I|¨œ”ýh‰Œ¼<àÁ‡JOõLœâuȤŽäC:d¨”‘Oš"s.¡<©?iÀ@‚7uRY"‘ÁgJÁ'´–P†Q7ó$ì‚D òyX¯ÍçÛgÅ8» ÜòÂvËõlzTNÞûpÁ:¸wyoÖ–µ¹÷YD@†Ê$Nÿåß@\^H} ‚ä§Ä½Y÷žŸÉX{º†ÊF×–k³ö¸‚˜dÉI(%¬€É—hY$È ë‚Ö°<±­öצC)Ñ™ Ž(KÇxpçá Ó‰Ê ¸ ŽŽb$I‰‘úðEOtpJp¸ç¯pÕm¤ÄÈIÍrP‚ ŸN¾Ð'3 P¢N‰é“á,O‡rðšbÜ r¨/A0‚\‚€˜à ÁÑØH˜%ý$”f¯ÄK¤á”>(àÆ˜Hèr¾, hAʓϳMR)­¸”–Q‚ …kc¯AÖÞñ2Æ;À%îÝQ&÷N‡ y¢{÷  òË¿¸Á)½£álroôA@Nî=q?¬=?Á¯ Y›k‹¹‰»™OJËãÄBE>{×&–°¤ hÙ¥s ”Ž8UBG‚8¦Îq$‚œ8NA@æ%¾eÐÁK$’I>DG¢ò¯è¶óÅ_5û¡ä7õ=Œ9TöK$òd:^„T&gj†2Æ Nyq69H4 ú ' OðÈ X_ :H$HäÕׄLb¤‘6ç1ùÜF$à“ ¾)kóÙòyh¹¤¥+¡c1A*¯ga“k³þÄ‚ƒ€{ÿE4Ázè`äÞ÷(‘L‚€üòo¦Ç.1‚—Ÿèœo3Î2—÷ÃúÓM^b%”ןÆ54ãI£>è|žE¬Ò‰-ƒ~ Ë8ØYXaËNJ8 +$æôp°€à<Áø tHGg©ä':Žå¿Çm ': ßɉ䃲¦T"ñ‘ÕÜÐ#£ÎÐúƒr¨ôÁå MA‰oñŽ"—‘KŒàå'^}M™K¥42—]—1NÜbÀÈ9o\¾¨)TF:²/Ë 4ê'¶@¿ñYÝ‘:ølÖ–µ™µú_{õ@@0{ +kûÚâµ¹vüäÞŸùbš, ': eäò—'ž9Hð{ÃAÖ†—ßÔw§äqï$Ön®ý5—•ß ×3–an¥aFÒØK´ Ÿ˜e!–ˆœX4iû`+ê†Kö=p8„àAâ@—Á’1Þ¹*Cß9 òMýr˜2)#—ðíà’—4K%ÈIsø.ÑO'¿Ñ‡ç$øÐcùæÕï-H}§¿p $N$^:8©OÇL Ñ,•hœ”¥òL8²lD%Ó nž%FŒúøÐFX p9´AŸ›ubéà‹üzXº½& Hêƒà>Ð,ë¯}Ô Ê{³v¿Œ\‚ÿåßÌë±WJ$$òr¨ô¢%Óï=¸·¸43“H$M]ð pi°M;ARâ³ÿ&v¢ {d×$ˆN{'í&ì©ò…C`ÖœKG„tª ʧ\p83Aæh%Áá4~¡ òW¸ +‚ù:ÈoêËnÇK$ ‚—”'ggœôÓI$òÄ3‰qϯ/”Þˆ2‘ù A"‡J‰D‚xã2—JpY9?zÃc–Fð-á#R†Éq)A`‰æYF£Ã:¥‰ÊŸý‰!H¬^iCyr=¬ÍµY[ÖÆvé4˜\›{Ÿ*ß WAPsÊõå/ÿƒxÁïý¾ÊÐDRF.‡ùUúHî?Y÷'ðkÏÞ½;\*cøÚŸƒ åÐØK»°öF`íÅ‘awZ.©”V”ƒUE‹ ; p\ÀyÂå t¦M‚tBJè€8iÁÃi ‚Ä•®–V dÐJJØ_Ø_©´Ú ­¿tPgH8å”'ŽÊȳàÎa.+ep¼¤Árùú‚(¥ˆ¦ÌA~¤Kåðc)q +H?Zœîi ÊG"5ñ’ò…‡¯¤Áëà'Ó™·LJ%¸ô‡J Óã#K‚€Àò2x4™ÉL/¸©– aÚáêd‹P‚HX¬½Mö‹”m¶rPž\›ú×ÃÚ{÷}¯Ý”÷&ǽ/ÁI2 ‚äòÞ¿„Ÿèüò?ˆWüD#ÈeðÁ›•ÓLdÜ›µ)®Ÿ¦îÚÍëISÊ0á Íüç`=›‚–HÊ–Ž„e n…%Fá|ˆÎ™rpŽ çä ‰ÄI¹ì4ÆxðàÁA¼ò¤ŽD_–R ‚‰D"ƒSöã”ðórðÐAR•ң÷€cž|%F~ÄÕ“:ò…æÉù®9x3‰¬‰DÆyÕ„ :R38’†ÐGDɉ2x;f»’r©”Í¿ k|úŸ½G°_'–úåÐn^{a9±Ñdm¸¼ö¿Ñ€ßû(Èqï2î‡õà< ‚ä•'ß_þGð"@@‚G"A‚ƒÁ9'&*r üúbí¹• v£ž„ˆµWF†• nÅÂÆ)[CngC m¯ÁÃ)âÀ)‘ÈèÐ+Ñ™YâT–Áƒc$æ«!c:‰ä'šI9œe^¯ÒOÍ5=qiPÎs#‘Ÿž¹„&G"‘È¡RJà è]G"Ề)Ip¸„)_¸„F«‰Üø Íd©”ÜÄ&àÑ<—Á <¸„]J2›B`}°6í”äÁ[ºI3òk/,®-×N\„e×¹ŸSâÞh¢r=' xpŒÄÜ,#/ùÄ+øfúäG\ºŸs”ÁÇ)‡rRóÚ·‘kÃåµÿ"D¤y% |X„µùl±) ³ADFË«g%CiO¥ýµÝá€f‰N ‡Ï™N¹:e83K8iC#çoêKŒ€û^ȡ扤_BÎ<Ñ*Ëx¹ŸbD?û‰HäFÀA‚žyä%Fxw|¨>(Aæ*Ñyè’’ Êw—œ€ “¨IÀaKó9(A`z¥\l$M{³ VƒƒT–a•b=k»&[ÀIØÍÐ)ãÚ»|´Ô8Å píͰöѱ6N’<‘üÞ¡æ_å/ÿ9ôFä Íà Áƒ#)qï1¸wâÞb¢ä½÷Íà×K\‡c"aÎKŸÕXÛ­8»fïûë ´Ë°æ°òPJç¸sæDN?8-¹Œ:·“èdF"ƒƒGR‚ )‡Ê}bÀeŒ“È%yég9Ñ” ~p$%FÀ=«àHdÏVæÑ*å MŒxw¡#¡ùM}÷€—Ð nHT‚#19®ÊšHʹ'‘‘KŒ˜Ï&Sò0º:ÑHC“ƒ›p"9[Ìv€ë`äs°ö~™Ã†È|Yˆ…Ô±×#ep\{Í•Áq?¬MGJ ‚¤*K_þÃñšeägÞ›õ'÷îÜ; )׿~úaÞ†ÆòzÄG®óÙóÿÙ¬G¬IØMknéû¨-/‰¶»Ó¥c'øàô ÇfhJçí Dâ|ARÇK$ÈàÃ!kʳDRÆ8‰\?–åàGø·ô³KÁSyq>XeL“2TzY ÊI²Rš/Œ„«‘ëOꀜÔé9(ÑŒ?1á’±¡-ëHŽ˜mÓMàvmtBÇÏöÏN|¶Ø;iõˆäƒ Õ‘°°X{sm4‘9”H®Ý$HʸŸa‘H$ÈpÞV"‘¿ü'Ó;’'uä D"A¼÷µ¹7&ʸ7ëÁ%̼•ƒ¡ n¶ËAŸýq!Ä!‘VÌêÃzžØb«­t 8…e8ú&ÑA*Ot¶28H8¨epü•”/^Må|_À¥#?òºZY‚‡?¿T‰D¢Y‚ é)q>(ƒƒxÈ¡™K\"‘Á;ÕeäÓþ¿¥ÛÊFbP"‘0?ãdP†@‚à RiåÐpJN$‡O ”ÁAL83OJ[d°/¡Ïþ D+öÙÿ +ÊðM™Dz=(åÚõ`µAâô{Ÿ Ö'I²v†fhÊaJòË^ÖP)‘H$%Ƚg@†±YûÖF³DCH$’k7 *f˜Ñ¨6VC® Ý‘íF¬žl­ªµoÁ¥eO)àΨ“:Ò¡ŽÐàpäž ÒùŒD‚ ‘HJàƒòD'æãR‚ÿßÒm%’þðßâG†RJϧ M 2Tzª 9Ñ ïEžq— ,A¼SɃŸè¸AB ¢C@*%t¤I‰÷€ .™($2xCˆÄdJž`3½àPÂlƒ ™±o”–…È\~6k¯‘yØAè”v“Hnm±6×–k§¥¹v‰J ‚{÷fmYÇÁ|Pžè Ê_þÃñš‚?ѹ÷×$”qïÁÀ½åÞ¬=Kò~|¨smÖ{¢$>V#4KXi¿‚Ç,#¬§´Å“–(áÈz¡ G"FàUbÄÙ¹ƒz‰ü¦¾ÄK¾¤‹ä¡”HdŒ¿äÇ î_~X© ^")áù š‰Gãúg‚€ÀÑ-•Dé}ñò…¦H ‚‘P¢›Áƒwuxu”¡)QS"‘¦«TJœƒG”DF#$L²Rš}V˜|´ ñí­ÌgV>û›"±6ŸÝf-fN%®½È¸ö——ǽ×_ â„Ò 6(‰ßè;?Oê8„C‰Ä‰ýÍÙçÁäué,ÇO‰¼D"_ÔœìÏ,AâtøIK$ž ‘¹Äˆç|¨”ðüeä. Mï :ßé"y"s©”Jc0¢O ÃKWõÍšÁAøµ':Ñøœ¼4±3ÃJÞ`Oj–Í¿¬´¡#Ãî¬ý×3;E>ko\´Œrð7ÀЗ°Â±öš“rm¸¼vó…¾tn8ÖC% ²ö¥á,ù/ÿuxqC¥·,óAsíJ3S‡€€@ÿÚ“V‚ ffØm&<Œ=Öæ³ÅjÀ¦pÜ•°_hû¤¾´­àƒàÄñå0‘.I$ŽJ$RptüJ$Ä¡ >9œåéP"‘'u$Hpàñ£'#á¬#¿ñÃbÄm“Á=–Iàž!yÙ×Ð9ñv4‘Ÿ‰Dž¸Y‰ÛÆ_Ì%"óASGòAG†¯ÉPŸÀþnóG"õ‘˜ä7ä¼4ü'Vú‡Ÿ‹vâ’´†¤±¤DÂÚ® —×fmY;y gÖ>(b޵qª”'u$È/ÿ¥ÌëK&AîýeIÖ‘FˆÄý”“0ƒá p¸ÓxO~ÖCkb_dX¥à°kƒ­ }XXXöÎîd“8¥ãñÌp¢¾Ðtü¾phëËà‘OžLgäGú»¸‰üF‰<Ñ Z>9ô£Ià'=ŸÉ࣌\â%Ò»H&‡Þ¼>.y('12è€>(cÜ?BVJùó¼$üÂÊRyb%ȉ;Ï4ºÁÃ7e2ÌypXXŒèÃî€à³ÿ;€\Ûצ¥i%m¨ Ë;XjÍk F /ƒÇ}ßëI}2¬Ýï`‘{H_þóñ¦@‚{ékË$Hý†DæÒ%yïæ‰¦¹ž¿ê4º†¶Dƒ]ÆgïB(íKŒ«$mÈ7VI§HtBÊàátÂu¸qnKŒÄ”‰Ä·`<‘Á#/%ÈßãžàþÀ/ôK?šŒ\‚x&8%8<@(å  çs%‘x‰ñvFJœ2¸m:§À1Îå8A.+9Hp$“ps.O—™æ M—H$“ÁÏo +>+2¸iiþ%È7Ö¤õ±SkÓ¢}޳†Ä†JJØ_ ­ÄÚrí÷}¯ƒ®âÞ}er?_–{ ¸D"ƒG^ÿå¿‚^– f`íIˆõ'÷WÇM×3¦× Jƒ<>ûûR¦€ÀÕÊ–´z¥­”v6w„Ò¹ÇÝàx WO:W‘HtGîÄÆr rR§D"A‚ƒ`kÓFX bÀAl ›»Égsm·£¼t 9ýˆ '$t¤sµ M‰ÄQ\ž¸48Æ¿™>9™‰qòÍôIð™KÄŸó}?NœA$8HxnJ‰Db/‘PŽÃ[@R¢7%¡)1î†R)At$È Dâ6.CÓHÍД8‰üZY³Aâ÷?3\’Á+Hp3 ޤ„ió¶#r Ä%ÿìµúlÖþ É:°’ƒ¦mÅÈ,2iÁ׎£@žèß»INîçÐp¼Ä8Ü,Q_þòŸ7<8’2’{O‹ŒµÑ)#—מÀk%‘ht s C¾ö𯽠V#¸U"ʶL¶}Ö3Úh}pÙi€N6‡^p§¥ ì )A†g 8”eðàñí%’ääìpàHJ$%ü '¿›èG+Aà!„Žzb1N‚Ã>½ô¼‚PJ¸t¢ã6/N*å8òP‚ø%|pgéNʹ<8’2ü&R‡ÈÐŽTŸ„ä™fib%'%H#-¹Q”m±ßè£UúìÍÂgËgcïxÜn–¡9 ;\ÏRÛñRYǸ¿üÞ‰N˜䤎›%ùå?ŸÞÔ+½ÍµäÞ%îçËRbíY’÷n’kÏ®?1¥š ç¸fÏfmY;‚öH¢“Áí M %,,ÈàXÃ}ı9(Ã[•dŽk¥Då  ãÉdð8S$“‘ËA9ø.½ðýˆKžÆ KºDÊω«pä×à áà Ž¤ô6I(ƒÇøKJ$ýV\VNöG" • Ð,QSú}*Kš24A@ÂÄâÃ,A@МËà!¸5—Jø¬€ ÊàølÖ#vÊhC%,,”×±Åà:±ö9@$”qïcA®íX[Ö>^dð8ÊûÏ/Ëä/ÿxY‘{›kÃßûƒ`†àt×¹&p=%Œ«iž%Ègÿ'âR)-K(ÄE³€¡„ÍEbµÛ}ép#pJ861>8oÃ}â'yy¢ƒdI§š ‘O‚ ‘8%üñ $üPC¥œ?)à ~Òã-1¼T‡÷"AHŒ —½JŽäL$2æþI ’qã~«œ€`~¹Œq¹ô«H‰SôY‘0ÉRIex°ÖD b}p:ÁÈgS>K‹ >Øâ»,áÚúk/8?ÑÁ}ßëIÍä~._h‚D>ùË>Þ‰¤4ëÁx@§\fLê\›u®-ÆÒôÂ$+“PF+`5ˆl}@ôO¬ˆ•¬v ‚ƒ:ÎÆà ÎOðŽÙàÁË'Žný\•%^R")Otœ‰D")1°—Nü8?&éi„”Œ\ÂS•Jpxæ“ /Ejp8i¹ääD®¯yºT"Ñ)Aj"‘ ˜’Ið†N©rRÇïC$šOå Ô”‘K³¾)¡¢ÓRØ¡Òè|ÖÆÞq™K‹ Öö}ûn¯eÇ‚¬,uÖ{—÷ñY)Q:¸÷•%’ò—ÿ|¼©oôA@NîýÒï L ‚k³žÉL¤¡”&Y~ž ÿÞÔp–¹¼÷_*äÚnHÀ‘\›uH\kg˜ÕÐ4Æ“Ÿ“û"]—°YÁíÝ  +i[ÑRÃÉæÐ#Hœ‡H$ÎStpÞžè8œƒƒ c|PÆé߸ +‚ä2òÉ8=Î÷§:ÑñS”ƒ>Ü ÞS’C¥‡ .‘HÄ+Q& '½)é’ }© 2/šƒ`~É™ÁOtææ¨”§Ëþðಒƒ—u$Hø}âô¡¦ôƒ”a˜#w .¡ +cYJ%8>û;òÙ¬gõ0rbIÃÚBGÇõ°œ ù7®:O\Árùdäò—ÿp¼¦¡ò|ûàÁïÍÚ²6¦ Øõ°¶¯=r0¨r¨<û³gÞ.™K̲X%k5{6±„µãe¸äÜCâ` ‡§„>ŸtÀÆËÌqz8Õ¡/A‚Ÿè ÊaJ<òr˜’œèœèÀ/8úY&ƒ=„ÁóARΓ$J'-xÙü }TJ(s"{Q?8È7ú Ááwûþ­Êoêû%’‡2Æ ?&—èמ9üX–áZoɉ„™— ÁK!íÈ`qJ«´ö%N>:¶ú¯Dë)m1¸ ~mÚýI} |˜C#îƒJ©\ÊÈË¡RþòŸ75TzÑX›{ω{÷MËI3¶žÁ[[Œâ™ab]1Éòó°žoŠ´D -WiaCǦ·û:'ÎC$§rPÎ^ÁA rà‰Ä·üˆ«Áãô8;?zâCä é‘ƒÒÏK‚׉¼§$¥O OûLO|òÄ ‚¾ôÖ$”5ƒ×”¤J¿!)ϾT"‘ ‘—C¥ß +D‡„²<™NÒýrð‡‘ƒ«u’R)sÙT'aàýÒ:„ŽDë#uNtÚ2|¶Ì´•ƒd°¹H$Ú}Éq=ë_G‚Üî«To ÃÂ.V{qI"‘h_$HØ2èÈ- 4¥ò¦¬¿ÊØfŽ<”²Ž\¶ì ¿K(7¿pO¸º(A@às%t8tòäýSÂg^‚ÀÕðR ñ²Þ¦èZùLzõðÒ[I$ÈïoðëõCÓÀ”¨$ qÏ`Á=b°üÄ%ÙUyÏQç—ÿ·xï_*KåžM¿'¯ùoy†k‘ÁÑq­äd©Œ=üDzG–-½A½Y½e’Ã[)ÑKZöâ›{;’Fhè¼ßÒ”Æ +8VLx$› ñåÁA‚ã-XYêH$Ò÷—ÁÑS€€,•ûÔI KÖ0xXÞ°ÚK¥«6bQÚ¦ä+õ ˆ=ù*A‚»ú¦Îþ)‚üŽ>‘¹ ¾T– KeéI‰äX'Á±Í>]*ƒëMxX(ö¥Ò»ŽïÈ?&z­ð™·ìó¢×°ôVJ$ûÂ&Ò Ž3xñ%’Møò0U@‚ã-XYꔿü¿‚ýZ” èG$‰3¼Ï ®çW&΃Qpµü¼ü™ÁK^h–áå’Þ;ôJhzIAànÐI% F£¼4HW$ÈÒ–ëw‚Àlç ÁA¾x7ߎ- ’rQF^Âwà _øæáj<B*ÁAtH(ƒƒï6©A@¢\.Ê7:Áá_Iô÷%\-u$ÇŠ\4;ó‘K/EšÒ[CÐ{$¡ÄgþcòŒŸÉ~VJÞÊ¥Ò›‹d×Ã9ÿ—†Àýš_hÞƒ‘’“Ø’ )eð_þrlÓOêË/îáŒ8'à׃³WjÊàHœX$Îs(?sþ‰ôj$^/ÞG$ÞÖ^gePî/Kh– Æ)1u—J&öÒ`—”‰Y¶$ø)?qé M𔡉·D°‰¤ç•‹æfpËÖÙÚJޤ•‡’ƒƒØ•’/fiäeíf.s©_4%VÂm}—‹RÿÐ/¿ÐÄ +xÏHÀ¡”ýµšú%VbË· Ç÷_ÒëÄËÞ åæšŸù}W½ ¼­RIdpxÁ‘˜p©DrÍ%²(ïá ÷ù‰‰äª\4ùÛ°/ot‚‚ûŸöý~š œ%œáΈ^¢Ÿ9äŸù(ÇžH/Èfx}–ޯ𺕽ÂÒ½èÀ ”‹Q‰·„Ѧ®R‡Yýf;+†ü¢¹$8’2x¼Jà }./÷;'oñŒX ²lÙoJi1amCG"±þà†$”%ì”T¾Ñ”ú‹Ž´Ñä¦ ÿ:ÁA@°òF3Þ_5êpùÅ6‰;%” :Iä+ÈÝ .QGz.–âNx¤7eeéUÂJ(½hg~Y’P.Jïip¯0z©u®áŒ˜o47ƒã¶l”ÁÝ óE ü—¿ ûòßp5îÙ\p å<8!¨Cä5œázŽââˆ"qI:ÉpÈ%μÒ;â­Y¼V%\…Л^zIM9ð0ÍC¦å&ŒÖør4Še˜Û¡)AÌvpàHÊàX_”?Ѽô¡å_U3rù¦\)ƒ[™ŸXCWƒ[OXdè,_‡MYÚ,—@BS òFç½× :àPF.áÏr¬„²«%V\¾³Ì¿Ð wæ%VÀßø'áÒ~zTÊ.•K,á-p$¸—¥ ¼w*>ãŸI|žWKx+9 ïì¢){Çò ¹äHʸ‡3†H¾(£K¡yùË_H[³ù¦ŽÄý:q†:g Çé<\ƒƒÇq;¢\:Æ$xøe ï…²ôx¹àE Ò‹é剑hHr¬4K“E bü’ævù…9/]ÇJ(Að$›Ke¹T~%öÓIp_›HT–=—TJh)$ KWÂJr ¿/hÙe𞀀À~è€c¹]&o4Aà*HrñÇÿ—ÐÍ•òM‰DIJ%Ö%Þ|ïÇ»”ï}¥PBÊÍžW.½¥×y(A¼JÈÁ%HïÝçù‰ÞÖœHôÚJ¯3¾ä æVB‰ûùß,’ŸÁ$‘÷”÷CÍÍ¥ò+ù ±5X‰Ê‰í>³õggäÌ9Y0YS^sÞ®‡3Çrm8Ãp¼Ïr8ó‰÷åŽ× +^ÃM¬ôò6¥Ánf.•,¸Œ¿¤lPsðàHJE.J$I |QÆ:AâCA‚û¶à otàñbM@H«+©”‹2,8¬ùÆÕ7æ$V¢RÂÇvÀ¡¹l“|•>ºŒšd;àRIJOñ%%HM šÊw_•Ø«ÈáªÔ!áuVô^`A’Ò{DdoÎü§ÁyÞ>x1uJ﬎ä ÁqÍÛ}=ù~ýM$÷pFÎÌà‹2Ö V~ù«Ø}! ‹r±û_œ¹AvNÈèh]“p9ˆÒ‰EâÐb´f8ÿ áÕèõ){¡Jxû¾2üµ ƒqÓ¨” a´7„‰Ì‘H£o nà#ÙD"R_âK~f¼?‹CY¢¯ú¦NéÑ6#·ñv‹¶ø5‘®i…‰„#ìÌ@/‰\+a[·ÃKû.¡\7‡KJðšðqIýdq›Ž\ö¡Ð RS¢RF}"¡äQ ¢üB38Hø2H6=£DbY¤7EÆOï*ñÎÃçõJ¯'¸éý• ¸žwüšŸyf¬l"‘a’” “+÷kÔ¼3r òË߃ ¾(m¨ÌƒßÓ”tpçuZr×°k8s&Ë-Xp'9øç9íŸùŸ0ཽ>Hz§¼h ^FÞÜ7>cÐ` 3ó«MW¬À(Æ +8Ìðøò÷œ£/‘ HJ¬„‰„ØqIï«Êørô˜›h6¿°h°†‹ÛZX ˾ظ„v'8HpØM(At@@H·IT–‹ÄmXñ­ä—e7p‚·Ã£AJ¬ —Ñ: [~ýÍ/ï6(±7Ëûh’ƒx)È7½DÒ{$¯Xœy%VÀ‘xU½³ù¾Èȯù‰sMŸH$¤1rOš3X %ù¸ôË_…M ¼ÇýHÜçOÇ5IâLyæ:á”Ê7ޱüÌwþ7áÕðâ¼ñZÁ[7¼ñ’šx+†!”&䦙‰Ät ¥\”òOôÃ0·øÐ)A¾x79È›w‡#ñ÷%ÿ‰«á{G"{.¹ «±¹´bK‹iaeäV C ²»C`Ëxp¬´§ÝÒ% M‰:?3ònæ$Ö}7‰nQ’àî!X e|9z|‰J©óF'Þu~&|Ÿð=¿°nnx£â­‰Þ£òó¼_+à8sÕkJé=]¼¿ÐîÕÆ™_ +Râ<¯?®é¿Ñ”æÎp¿0|tdp$›_hþò·a_býåžÿå"Ïxè(ƒ‡ƒtæ8½q¡ï|–J¹8ÃálŸ9ápæÃÛ}xkà‚wMGz%'ÒK†Þ¢o*š“à‹Ajº–o\ÚQLðc<8 ²ø A"—wÉA@"—ð—CGâ« â냀DϲTzüEG¾±b‹«–‰´Îañ¡S¦|a×ôK%êÈ6·ÔySüÛ‡²ñ÷%-KèÄzÒ#HžÄº¾Të ”¡³Ô—|ñwÞ踀K$ú ot< ,ßô¾”ðA__>ó¢Iôö>º$y¢ôæÉ5œÿyx¹(ï‡3ÜÃyhøHMu¾Ðüåo£}ÙüÉöy¿8sZ’kN‘2*ãšßÜ) ‡¶„KÒñ†¯,½ ›^ù¦W,¼†Ñ[Œ~V¤IJ˜™ 0cù²¥ù †ùï&7ÿã§ÿÌ7:Hü©ÿ†«K¥ï.—íÏõF=>He ?(°nXqÕ’F.Ãâÿc¾1 å¢\*%ˆíƺÑÎúR¤ŽJ©‚¤ü‰¾ïŸlÖ2OÊØˆ,å.ùSàPþLWåO$Ó.JôÖxH(¡‚ÏÃyáM”èõ$¡ñ.{»ã?ó{qÈó8¾ü÷}ŸqbÂHœAYþ7\]¾Ê_þuÚ$ÖIÜϾß#Ž„Äýú2ÄA:sœÎŸ8ŠÒ™DR§Ó+A>sÎ?“û:,^x§ ôº™ƒô&z…›{e˜¥É .A@ÌXÅ$\•Á“|©ô£€D‰D.[&‰AD"‘X‰-}CÈ%<ÑÙÕà‹Ò*-Ê~V@@,,ˆ¥nÍ%ˆí{¤<8HÛJJ|ÉÞ@$O$ —@ÞèÀ=û­p(QI@b=)A<,±¤|ãR‰Ä?LÊÊwjÊ7úK¥¯ +îÑ@zkxRF¿)2òÞ5x¡Y¼ª®J/¬ä‹Ž7]By=œ ×p¦#—-2÷pFÎpÏ/¸D"±_å/¶äžå ¸g‹“8ŽÇ–|óÍ5çêMç°„ÓûN;ÎàÕÞšðNÁû¥¹ÒûèÕNÐ,›Š¦%¤ļ}c /®.&9’w†ß…+È%È›:% 9xp$rñU#/= Y”Á=xp$V ¼D ([Ï2,8Ùv ‘a§”28ì¦DÊwâKÜI°‚úÿ­ŒÜ÷!à u$®F.—ž¢\ü”¼ÙùÂr¡¥“ÑòJXv(ßÙ¦È7툫 °³¹äDB ¢CBÎÉ:¸›‘K(s¢LJ_ 9rÉA"/ï’“¥Ç×'‘o“”QS†Høè€èøzœ 4¥¹ô¤¤µ’J¬Dåg8þàø?¯7×K½xµ¥æ5œ‡küzøÏþïXÐ{ÈýpfàHÜS.ú:ò'®þòwòÞ\î>n‰3;Ž3tHî)9:<ÊkΕ#·lI¢³º‡–€|žãýy~e¼ Þ—ÞÞ²p5¼­ F_i†~VŒÊÄD— ØQL@Léøò¥9/¿èé„H(CgSEr"9éãÀ%’wî7'¡”èIå°,h‰@°«—XU‰–º\Ú.AlÜbC¥þ¦ø?ÒIÀ[º_.ÊÈK$>%”‰K‘Kßä]r ‚ž±RBÊDFM úeM‰D¢?˜ ÑÙì‹•®†ÒC%’/JKGdðÏp†Ï#ÞG¬€{[½Åà‹Ò+ä?ÿùc$›ØfÜ÷}žÔOî'ã fQë‰üåo£}‘H$~Ê=»|çÁy¹æ7åzpÒ”qM ‡:IçÉgŽ÷g~\Âáe>—^(x˰o+vúÁ<\ÌIe‡* bƒƒ˜Ò¡ ÜÌ¥ Ž~,o‚ ‘îÁÊÍð¹Ác€øþ%ˆ§ûG\ +?.ÒZE.- „å KšÒú‡²´eaävÚÊMžK7²2¸N(C>‹ƒƒÔáHtJ_2‘ø)Ý A‚ƒôȲ¥Ê¨Ü¬ƒ·àýwøMY¿\|½2r÷‡' ÅrI|Îà5ÄWÊðÚ‚„WÛ;gÞwœá?×ü²€¿ä¾ï3‰3ógÑ‘šÁƒÿòwbwð–ûÙV‰{ÊûÉÅa(õß\gÞ&VNÇUrâ0Ç™ÓÞÉ/{)$ˆ—EÂ{ï]èHï/‘¦_óp¤Q‰¦è{®BÙ.Íg"C‰ nà#‘Á#— ýv,uäOºŠD‚€ôÑeðÅ×þ ‹Ç”x‹/-TË%¡c%‘Xá7š_¶#$l’w¶•28VÚw¼%8Hð7þˆKä ð$H¬O!s$¥K?Ñc"ßÄ[¬›|;*AúËëH$Èß+àÖäí›Ëg8óJÊÏëÿôÎJô:Kk^ù¸ÎøÌŠõÐ “ä‹óL›óšEÉfp_þ*lJð7:»¹Éý¤Ž#Á7£Ãs gèà•Ø3Êà0éxŸü^ +Ù+#½P‹z áU5 ÆEf¦J£58ʼn„¡-—JùFÇð²(ƒÇú?þĸ +‚ð7>7Þîkc®J .ƒ· ‡ÈhÝ$ˆ… nÃÊCÇ^,JÛôN$ÒVâKvÇI¼ý'®úW’ƒGâpTîx'ê¿ÑÙA"5‰ÌËh$’$¸…u ÍÈ%VÂ7ÑÁ[@@s¤—ÎÞ—^Ãð–i Ïl7*‰„)ú…KÍÞd1ŸëÈJ¿)á—%ô7A Éf¦4K£[† _nn/•͉D"ÙÄÿŸ¸´ø,™ÿÄ·…«2òž±D"-eÁŠå‚rW€KK nñ%ÚÙIèKí£¦ ®Y*å›ú΃䋲f¬»ô'‰D_rp©L_‰\”žn¤þM´ ²)Q§TÊEY‰¿/ßè×L~¢ïÑÊP‚|þäLç<|æ\FîU e o4®ù¹†3r†ÿÌïÅf}Òܸ‡3bÂÈ/΋nùOôù«°)olßxð¸çÒýÂ! } â¼Á) +]êdn:·àpþo‰Êà^½>½\á•lÜ-f  ³f&—&*8 ØEis›Ë/Œúú’/_¼ü‰>H¼=ü5©Oâíoôá #)=]©” -‚„e2¸#a1a…KXp¸$a/ lkÞ¹·Ò†_*€óÎðy8Ãçá¼ðO$H—>“[‚ûïÔ”Q§¯ Ž-‘{RëúDóÙKÄ¢Iš¼TÊÈeô)epÅ7—Á=øçõ~èÄ™«^Fy”½¤ë_è{©Kô¾7®!ߌú¡{¨”¡#Hž?'’ü‰þ/ö+‹ÎÛ´õ°é²R‚89g¸æ¤-: p,¥’8´àø ç9ög$¼#Љ^7ée Cšf`ð0*AŒM$&mpÂXÁm²lIÞøˆ/w'‰uëþIù¿yßÃ}a¹lÙ– Ø—]"u¬$vmeÞš»°¹Sn7‹ T’X']úÌè#e|ótΠ åOÎÜé*ø¢ Ü—÷M/¶éaÝÊÄm‹ÈRi%AâŸ'ot¢«RS.5}OððtxËgÖŸ‡3|æ÷…ÈE ¯*”RéýMÞ\ƒKë¿gP€ƒÀÕ3îêy¸¿GîÁØ)ãíî‘‘Ë_þBÚ r?›˜ãÌU\§ã¢\®9<Ñy+áXbʼnåÒÙ>sÂÏó" ×¤ôâ ‘Þ,éõ1ë¥Iˆ&¤Aó³lÆ–oŒåÅÜ.ÃÕ$¸ß…·HŽ· )±‚/Ju⻕ Xï%ü¬ÈÖᎵË([U´Ú›v!”awÚ¯hABS)Q)Að™ðyr9Ãg¤Óòì¥óâ3?4É;£¯$±Ò ÄC­×I@”Ë»lqJë–D^³¨Œš¤ìêW"ñ5B)#ïA|f}>¯|c Wåâê¾È$ò^ùk~Sä¸AR‚ÜÃO{ÆN8±ž” ¿ümØ—/4A‚ƒÀv—gpâíŽΜ´ó ñ›òæó'Þyž×!¼A ^´ÒÛ‡F_‰æ! I.£É©&­»©2iŒ¯HÅ/4eå&Hp¸9/ßè¸oÇ»Ì}·RüާƒÇ\‡Ò‚”–Ä D¢¥“PZ^ ¾Ø‚Å¥7fÚb߸ºT~Îø™'ñù'ÎÓ?#gèŸ|fJœ)}„ .Éð ߸dé‚DÞ¥¨#K„·¼Ñ ÿJ‚Ô—¡R“G¥O—xKO'ýà– :Á‘”½­%È⽆÷]*¯yñ¯I˜ x—\æò~PÊótŒšœ„RÖɱå/'vçÎr5%?Ã=œá~Äñprέ8ÏÙ#p,¡”Êø<œáócxABémñNÀ “¦ˆyˆD†QÙü”h®*MÚ¦1”ïi’Ë<8Và7+.E‰¤ ·I%Á—”?ñÝ"÷D¡,óK´X¥ƒõ,u,ò +Ú‚²}±M •¶oÉçÙk%Ç>gø¼8Ãg0ñä™òL⌜'ñºÙé¼ä¯.-J¸ºNàé@°MRSÆ:…‚R*ƒGWcK‰J¼>W* Ò×òyq¦<ó#B@”Ÿ§)ámE²/¯äà¸æek8üg~5¤N‰¤Œû¡æ×´9Ã=â’ü‰>È/6+°šq_NÂýÈ›k8Ã5ÿaã@B)ƒ;ÃÁ?sªáEAoŠ„7(t¼t›aú-¦¢M$²ùYš«FnšH¤ùL$H4ØK$~ P¹¾©Û|'’2|Ÿ¯IA ‚ÄS· a}”V/t,fpøee/a;¤Ýq•ƒ‡Nû(ÛÜÏCå<|^#n9Ï9Éã3^SâK\ÅçÅ™o¢‚Ä×.A4C‰¯Ž‡}ËX%(%t~º©.•oêl÷MàKJhBYÖùÌ:|æñ%¬4‘Èÿ3¯§‰¼D/u"q½8óó•7šq¿8Sž'ÑÀ)ƒïU(‘lþò·Ñ¾Èÿ†«qÏæÞgÄ5‡jqêt$*Aà¿Î9μ 8H¯†ì%âDzû º0£Á¦eäÒ,5`CÙÈ-ÃpÞ ³=òr©,Aúá®.s©/ÿ· +Ö=”2zXÉ­]²´€Òbk+ƒÃ²‡ìHØ °wÐÁçÙÓå3Øú/\ÂçÅ>ÃyèÎÏNp¼¯~Æ?ƒoe‰Dþ7\žÔÓ¯ƒ•PºA¢rW,Ñ)—- Ù_“y©²¼¿-¼ ëpfAÎ,—Œ¼ù?ó#Ϩ‰u‚D"q„$‰½ÈŸÁ—Y”pƒTž¹óÃyñ™ÒvËÏÌ4‚D¾ÙùB‰SDÀ%tø¢ó™Ï’gø¼8?J|Ox@ ‰H@”_ÞrIèH¬,_÷È:‰ Ÿ%Q³Ô þ™gü ­‰ÔA"- xðÅ[ MùF׌yžˆk:ä~qþ,ã FMð7:HÞü—¿‡vD†Í•H\ºg»ïWâÎóŸ H¤3Ç ç9uN%œa¹|æKœÁ»oÇâåŠAn†1ˆ¤4-Ãü\ÌU¸ÁÈ C847Ãô~ã’1ùA”‰Ä +ÞŽ-ÉÈK$öK’/ÜÖÓ%ž|„Ã*Á¢AYÂoʦ¥‹¯„½€R.vPºjû8Îl7A²ÝPÂ9‘J"±R?r—:¡YÖÜOL$Μ½3ùEßÿÌ¥ó¤¦A"á©sɉÄJ(aÑä¢tIB ¢ó¦N\.n†&HÏ|ÿ/jÊ3kòEÍÒr-ÞÙÒ%ÒMê\óŸŽDò3²€€KåõȸïûÌ8*A@@pÏhZ¾J7üò÷`Gâí°kq†ûÁag:gRçæõÂy“gÎ$”²ÒI†_žï…Wfñ~¹!A§1£!©I¢*yUê„ñkö&X¡­”XA“\V~e¼}Ù& ŽDúãà+}±àú%Þ²x:Ï»¸ZZX(A`õÞXÒpƒ5Ç +ìÅ{\R.Êσý·‡Û¤Ã°è@DgEr¢$K¥ŒÜm H6A>ó•ä:žBž!Gr¦#?Óñø24åvM²TZÃÈk‚Ëx»ü©¨_4åWSùyž(ÎÃgúD¶\rQîÛšH(‘”×¼àëqÆ „24ï™D‰3Ü#÷ó{Á%V¾¨/±òË߆­A"ïÙßä ÷pyã„|á8ázqæøuD÷0ƒÃ93gþ4Δ.ÉÃyž+_¸dQZ(p °¤¨Sb%”þ9¾¤Kàò3ÿ’œéœIxÆÐ‘H¤5—?ñæ~áŸ\óÄ‹/qMG.]’_hÞ÷}&¡##(Üý8ÓYÁ[~ù°XA^‚àžM¼ÿ<¹„«åõBGžùY‘ N)é—<¸Ü7‚„›áe‘è”0èxi ÂTäÍLÜ Ý4]¥9 b“EGšÞ‹Ž\vþësE{Û;A‚Ãmu”o4eðð%¿èA\—ž4ZpYi‰¬¸´ŒoiyKk¾(aƒvkB‰}Œ3Øâ$rÇØ&ò2ò2Þ¾húû$x¬ôU‰üÌ™$øLÿóOxL¸ºùEM bÑB) +߸_â_E^F^ÿ<ßÿÌÓ-ÊÏó˜­Œ:oêØ#x¯Kœy¯\^eÜ÷}ž±Y3* ‰3ÿD¹¦ ü'ú¿ü‹|mÉnåýìò™¾¼ÇïÉŸ8!×ÙƒWwDA‘ùG¼ ^+xe˜{Á7f£™ .MÑæ*ô¥Ù . ä/ôa’C)·\üh– ÿ›îñI©,¡%HpßDB¹N°î¡@ùWÆO'HütëïÏ]‰½Dú¶ü3ß?/?ÓùLþÄ#KÈËÈ- òìÍ’¬€»DÞhÿÌ^øþd9óDÁ=;Y*õÁáÍÅ»ôvKh–¸¦)ß %Ü# ÷ŸYg0pÞ¹Á +ø/ÿ­¿\” xËýú­ Ç@)ß|¹†óàÔu,K8t¶ìä{"÷ú€Kn¡câ•&!8?(ÐM˜´¼Ù[ÈÁA¾0ÌõKÅï4epW“ÍE‰¤Û‚ÿ¤¾Dâ+-Êà+Ð÷¼Dzv%’]"¿&²u  Ë¥Õ%ìÂ{¤Yâ3›ø™)½èÈÈ%ˆàH€è€Ä:AâÎDI ‚·¸Yæ>tEB‰Êoù¼8¯òŒ{vy^耇{$tZ·²2þ‡»3”²NŸù&[&Ÿi~Îø™,=¸ä Êe—ŽH(½ÝJçyåq†k~SÈ;¥ržÁrî&Oè—¸çfå¢ ¾(ù±HÊà ö1Îì)Îà`Èû¡R‚€€\Ρ2¸Ã îƒ|fÉ8Cï…ô65ÙJàÆ Ù¨DÃSF«a S—7e뤑^.•28üXHèä¤D¢uɃÿDß×@"—J|ñtáÑ*K kµé*±†¡ô³² «m#VÞØ#}iïpfCϳ¿ÈÛtðÐ)5egC¾ËàÐÁŠ{Ê:d‰ þþÜR[B)Að™Gû̉9Sâ ŸùY e|¦)Ϭ˜Õ#2ÑA"5ÁKhú 2ù<S"‘ú8¾³ô€È%ǺäÉæRiµ½à8Ãõˆ “+à÷Lyî?1… /‘È¥RÿåïÁŽ )¢?(et0pæ†3©¹è\ƒ%:“›Ž1x|†:Ÿy#¼2ŒµeKÒ4aBr bl.†jè³²Á b8cES.†÷hòˆ>σ“åÌéÇ”ï„eäÁuÀßü}Ãg8#g®É?/yãBÿð˜_ülZ4¬¸Az»Ï#8×p^¿&ñîgàò~8Ã=œg(­@_bå‹n–¿ü Ø $›¸Ÿý%åyšolŸ—Xœ9‹*œð7^8ójxGÐ;¥c¸½iúI†n †¤™ b¨¾iÞ6~Ã$øÓþãŸlb~A6—½àKÊðéÁA–-Ix(ô¤ šVCÂ*½©ÓªZÞP‚Ø "£}ù<û›È‘H»L6u/ʆ|—œàíÉž+eR†fùn.5eðî|'úžš²R¾ù Ûÿ¼V#ΔÖJâ •òMKŠn€H|ñ–3Ÿ‚3w"Çgð ËÐ/Ï'þ›Ã^àÌÏʙę!ÊMƒÄ ÷p†ûO¹'Ãð âªD%Hp_þEl¾¤ ²ÜϾßsHJ¬¼q¢Î¼¿+¥ó âœÃáÿâgÜn »ü3; ›Ð—Hô ÞN”D~áÒöWj–¨#±%ÙÜȲODzR‰•/Î,Îfùy8Ãçù•±¤ ‹«2ÖI|æ/|æ£I|žf¸_Â=p =‘2øâZä’“Eçšÿë„<Ã5œ‘3¿/$xÔŽû¡Î=¿+eh‚„K8¯NÔ‘¿ü»ì.$äž]»'ëØze~?ý÷œp–΋kJ‡°Ü#J°Ç›÷ðϼ#^ xï`¾-ša†Áš†gi„‚­›\ 3y1«¥Käé’.¿Ð Ž|9’M$>J¨_‚ ï ≙‡Å X7XF(Ã:ÃÊs¹Ûñ›öNâü˜]6+ú24; ’_3¯ÈfÔ‡æ›kNºTâ- H$V|‰ô=å:Ö%O@àÙã¼Ð—Ÿi~þ‰3|žß‚3«Z‚€¸$ƒŸ‡ÏÎ4Ï$Þ_ ßìä—äb‰¸$#·þ2øy¸æ…HŽ3É}ßggä ÷ £&ô7õÏÀ%ùË¿‹]øÉW_yÏ>渧t6Èr¦ƒüzŽ“tð6Òw‚8á+ŸÁ{ôÆ|kÖI÷€†’Ãx”ÁMN4H%vÀ¼‘›Æà&6‘?q‰„±•Å ›ñöØNR”:?©ß÷”J‰çB,a‚@¿´V°’o¬êâË6BŸÙ£ÄÞ…²´¹dÓèR W‘”‘Ëë™c\Bïæyn–çqù¦/° ¢/s(}™£R¾©Ó:ðÏk˜Æ?ÿ”8#gøÊgþ‚<ãgRä \b¥//óÈ%Z‡àñ^LŽó,2Îãg†’+0=pf¶œW.¦ 4å5ËŸèÿòïbÞ|íf©‰órœ9gÐ962òkþ‹Å9„²D¸tÂAâ3œç +e˜r&V Ã0Ý OðÅ,1iÍ^™K¥„á¼é|Q‚€ _ùF‰Á[ú;9~–HJô=Aà)°®FÏ.- ¸„µâ­§üÂjÃͰmÐgmŸ´¡DG‚´ïR RS‚8-ç‘7:ú J¬œáΈK‘×<ƒä ß§ìj©—K¥ñ¼àeäJVJ$› ñyþŠÄ–ñyðO~âé*Î M¾­Ì7A"—Ñ"H(q g¨yýÉ®¯?îû¾þlÞ÷}¦”¸§¼'qg†L ‚·¸GþÄÕ_þEÞ[Àƒß³_ò<}g¸ç³Hœ)Ϥ¦ó#‘H'‰Ûö¬G]$øçÁkµè›o0ë¾pÉH4! ›2t`´ú5)C³ ,¡c8+¡äàaªc%ü +Èà‹Û@@‚¿ñO@\— ÁA"/AúzÄw^*=Q(áÙ­F(£u %¬§¦´Ô•Äú—‹Kñ™9fãH; •Å 2:É™éÄ ˆ³Dp=hÊå ×Èõƒ3Í3J‚€Þw“œ€‡ˆ'•¨”:° 2ôå»ã¶Ï«„Hðσu–q^ýp3|„KDç Ÿ¹TBDÇÍDs=£]r7n@}òF³ôD ¡©”üšÕS’2ìšÒ¥kPBùNeKã‹3SåLâÌ "2t‚o¿R‚ÿå_Ä`öëüYâÌ^Ÿ9ÉMkŽV©”|é.N¬CþÆû¢)㈙¼F·4ÏáÒf¼_¥h¢’€/šÛOÊE¹(Aà»ÇŠGˆuâyáÁ¡|gKdÅÂ2¢…• Á[yNì γSÒ&’/•v¼|ã’\”×Ãy9œ‰ógçáú“óp g!È5œ?ñ¢K+à¡Dp$+V¤N©‚D¼Ô)ñΈ¦\”úÿ˜Hdpøû‘냀x"‚d³KeðëÅ™ÒÚÊå 5ÏpÏÜX”8ÓÇ9“Ëy±S<8Þëä—»°ØÁò<}‚{:¸ÎÎRgÞ®‘ëù¯ÁN©s¾¹(½>8óZ™f‹Ž4å`*‰„ñ‚•æ§lœ. ^$åNéð#ÑT—Xyónæe?"üVÀA@@Âw@.ûª2|ÿEñ³²XÅ‚„…qsX϶TJk‚Ï‹3´_çŸ&” ¶^_ž9<øy: Îs¢®é\OÆwCcM†q·è£1hH¾Ñ13‰4NÃŒÕßÙœ”;À‰ÁNÀ¥2r‰D‚€ ‘ Ë–+_èG +߇˾¡äDrÏ%A4¥g_Ùiq@ZCÉXáБ?lJœÙ£3Ã*¸­„ý•ÊD¢c@åõ4\Ïù!çÏkn 2: ×ô‰ÌQ rÍ¿&ûtœá¹—Þ¥ ¾O%t‚["9V4墄«:ø<œÇÝ ƒë#‘ðoCG†>’ú%‰¾©™H\ÿ´,gØg¸fÍI Í{8#š Ëyhà€èKTâ-¸ŸV4Ë_þElÁ›íì6ŸÙAœáŒÐ¹†ó’¡TJg8ûÒU|^ïÎg^.k‹2 = Ã0xRšœ0H•‰1»JÓX6«% shJ4ó‰äàrÙþÍ/4#/åòþÊè«&ѳÈèëppk²X%hZºà°°X±æDó3´GŸÙ /ÎÃ^u𚜔JɉÄJ(ûþœ¼Ñ¼fŸIX8Ó×y£ùÅýL ^Þ—ç…™oߣÄJ|•Ðùåߥ](q¿~VBçLç%ïSÁ%ÞÒ‘ã¸Jtž ¼ex~Ò|[ÌC$æ¤Ì%ÌÏàØK}c9ÖIæ ðhþËå«\Þýu_îS@}ŸKôÁ%êƒøž9zðÈ+ÖÄr bÑ@üˆ`ܲ:øÌOŒ” ÐGbg“ëOÎÓq0ä™;Ih.Ê3ä èÈlš÷LªŸìmîÙÔÄ™/ ¯q\?艷xÀ?ÅÕà ‹ä}s-© âžE‰7ð$8ºá }‰¾† ù5p(¥2,)È™Û8¸‰·ßϨ!úgfùÊퟑÈ5%‰·ÿò¯` °‚¼ Žûµ÷l4Œp¨Î”NTÆ™ó.•švéðÿÄÙQ†ó`Ä ©(›“ KsÕPM@°CØp–•M쥩ÜÌ'àR‰·/?;?鞉ô¥¯D@*á;#Ñ„ïÜÓIyKTÂJ?"x;ø®ög~âm ¸ ;¹Í•ðO Yöll¿Žü¢KrQ"‘ÁåRY^Ïß_—‘ŸéÇ?“q†k8Ã~y¢){ä¤ M¹t©¦¬léM¸Aêx—¼ÜY*ýÁ[&×}%D.5}ùPzºò]†¥@Ë"wÑÈb…åg~â?ó›R†f´ƒ¥ ä TžI%ÈøÿÀ‘;sðÎÜ)ïçÞ#2¸&îñ7>K‚œe©Y~qæYVä5œ‘3ÍeWƒÈ\*K|uä›wgW›¼éž+xÿñàЗ áÒ5¿,+X'%ˆ&Î,TT‚/ïÒúŸ)eÜÃvî¹áÎÈyê|¡y?7@¹‰Dþò/²[@@îÙ2‰óêoÖw<Ȧc†/9sJßœÁñâ•‘Hšo‘—~Y‚ÃxÉ)¤ÈK˜·P¢ Œ³Ú'HŒ÷M~’rK$Û! ÐßDòÎ÷çæ2úzRS"ñýãIèC‰žZêHkV,,#VÜf‘|æÇŸá íŽ-1vÈb¯%ÈâT¸¿Ä5çášÜ&Ö °3Ü5A¢«gr9Ï_X¶C®ù>ÝrM_âL‰3r&qæ©É¢\v)ܶN ¢‚D"i#ßôÀA@H¹äàPB)±rÍS\Ï3Êk×pf­HœWIP÷}Ÿ'qÜÿ%šrÑ—Û\ùBä—»ù{ËÐ?³õ8#çÁù¹æì]Ã9ωqÔÁKo|ó™ÿƒ¿<Yƒfcil‚Ë¥fl¸º(McðÒ¸6Ãsþ'nÃû'+#±‚¼JäR)±_L‡K_+žhñÔ¡_Z i¡$¬á¢)-r(?/l’ú‰l7eè€ØtŽ„„Î;Ïô‰„$øýp¦/ï¥Dr^—PSêKq.‘\¯¾†õ3žbË3r®á ×sgiY@¶”PF¾ùÆÍV[r¼=*7ƒûSo4Aàê¢Üo{Í÷¿ É®G¬È=p‰33ÜÀ òÎŒ¹TJW%È™N ‚·D.ù{°# ÷³¡\†fœ¹gNHìy»æõÁ™ΜçEéÌ¿ñúhâ3T~Œ;˜~K¥{`NoŠJ˜«%Œ\¸TêËfµÑ”ÁÑÑÒ±ÈK¥•·PÚ/p»&9Í•‹òÌŽãŒ8 ¡”H\:#÷*"9’3—pÁyxßs&;¨g.Éûù;R)QYBÉ5_˜H(q¦$oöaq gDÿf ¹›ÏÐb:_¸ë‰(ƒƒ€ø,à 8óåÏ‹ëU^Ùu¹f¡‹‰ëºîYU7ÜÃùSÎäû†ö+øâ†¸‡óŒ ùFä—¿‡vDb%ìãy¸¿A'dŽVyæìáŒh\Ó#-á½€aõ“†ÜŒƒ‰9Y"1EÁƒ›ºDš½r½á ¢ £ûM³}ñ£ õA*¡äo긾(õÁ¡”X Ÿ» ßðí¾3‘Á=ÞN +Ȱ>Ö \*­$¸ÔÜlÙ¥€&Vll¢T’Å/»ï+÷œeä›÷suóžNÜã÷à|vQ.gèÒ=¬'ò ÍûŸ®úþçázüz×øõ$z|©#‘hžK¼–‰ Ž7#©I6AꃇÈ5_LžÇAâ ×Ã>~R× «w^eèÜçÅ=K Žû¹z?¶UÞOS¹~Ïî_þËßC;"ß´ƒÑÕ:ëgŽ“¼ç´2®áŒœášó¼y^Çû: ï±ö™ÿY'‘˜‡x‹Q‰¤‰Zb‡mãW.fµÞb’ÃÀ‡Ž\*%üFpðÐùÂU÷È7>H\FîRøJJ$¾¶ŒÜ‚ÀS#ùʰ\–J‰¶ü gäÌÏJp{´—”Ë5lÓÁPJ(AÞ8Bç¡28Hœ9lqww&‰ÔùÉ™?‹•7ï–ýÂèjõ3y~”áÙ‘H¸§ˆ /%ˆïâÛÆ:ñ8otÐSKXÅ%kÕÒI¥Œ\bvÄî|ÑöI$ö×îcå ·Ä:¹ç¼q‚3(7ïiÞOjÊ3ŽópÿÉ™ÎÞŒû)KœAÙ׿¸æ¬V–×\ Ž3MœyüPÊÅ%$×;Ï«„2xðÈ%[€\B¹Nà#–3Íäú'ÎôÏ‹ëÏÒƒ—_´†÷Ãyù^Åýˆæ{¤äå¢téŒ`ë+‹Î/»#IyÏŽƒœ)ñ. ¢SÆõHMk8N¸DRz/¤9²t~bpeÊæ'”Ò\ c6LãîÑ %ÌsŽ\bçÿÊÍÒïruB¿N$”$úhp5}ÕÒSüÏ»XÜEþ•V5Zmb/B¶ mV®áŒœázFq‡DyÏáróžæ=÷È8Ó9Oº$ï‡3òFGÿ~8Ï=gšçÉÅa–îAR^Ï—'çG™H37œ‡Ÿk¢“„Râšæ5YS‚,­68HèWJ¥Ò Ò—‹2Îp=¸¤Äõ{d¹:É2G/B¹£ŒÏ3ôÐ<ŒubŠ‚›®e¿xOf^Âô–Jƒ½Dâ’Œ\¢ßéGD¢æÒUÍ࡯”Pæ}–T‚K_ šòKá‰H÷"ð]²XF¸A¢Žòó`ñ噽°;aJ}iψ¼†3´û×S^s6b]÷œg:8#]:Ã=œçßJùÿ·?óÏ׉\”Xé »SFî7,g¸†óâzqþ,­Þ¦µuÊà‘»€Ç:‰¼„? r†käýåÏ4—.I¸$#— -Eð3 »¼Üœ×çÏQ£Äý£¹KÁ‘È_þ6ì ’+oììGårÍ$Ò çáÎp Ž:”è•Ieœ4Ü>O.Æ ÌC¸Tbf)ˆ bêÂd–ÁÃôþ‰æ|%‘ýÈå«\¶OàŸñÁu|«Ð ®bžEzLôÔHd´8e«W¢ÎRiaÚö +6t±õúIy?T–qçœaÞùóÒýÌ™ò΃¦2Î\=“ÑÕ3ßAžçËÜÓ!2êD¹ñO®á<ÿ–Ä5œáš›[´3\Ãy-æy<)õƒ/]’XÁ5·ÉÏ#Ò§ƒÿ7\…×tHyÍ_ãK¥UŠõóôÏp?œá}Év,5I(¿ÒUÜs[Ô”Áù Ù­IîÙA¾(A\Š3çä‹=DByMóš„sŽJS Ji AS†)·œù¡9ƒ‚†¤4H LZà;„ÔRi†K†üR)±W%ˆ_¬—¨#—šËûOqJ à¾gômWz"™‡ÕÅÙ±zo,ìÒ²ËÅ?±AÒ‘°}›ír\³­u$¾DÆ=lçžÕ;S~q¦y&±wÆýêÜãdQöA÷p^÷ãÝVÞóõB‰{:gnó¼28Ϋ/ÏŸk‚k¨´z$4¯å¢_âÎPS‚€€Ä5œù 8”Ry½úë¡SÂSËJ~F°.ï¹Gžñ¥f¬Ÿ¹Ç)Ï ƒJ.•÷#2ÖÉ/6e©´}絉 JœéŸ9KXŸI%’í_Ã9ƒWWp ÉçùY ó°1-¥YÙJn¼Ý”Ž#=vÔ“7:è÷¢T¯#Aú;áÔ‘|+È}¥àØoN<È +<)tÊ]i•”à ЄR‚|æ7…”HlJÛ$ߨÇwžÙn‰ë™Z¸ÿ&o¿¾šg2Μ½Í{ÎdšréRMyO)Ï|´ Í-ïç È-‰Á=Mä繊Ü#Ÿ‡ëy|ÉÏÀA®§s g°†±NpÍ ×äÒUYÿšTGrÍ¥k8ƒï‚k¾§<—¸¦Jl_ÄR”çá~ÐY”nÀýpÆíÁ=¸Ç] +Ž·€ÜóÏ_þB¾¶F‰•¨”÷p怕 ¸†3\s%Îp N>”Æ/AŒ5ÉIp|æWæ3i$Âœä›Æi(Ãà-Íap³šÈ/º$ vbìóÈK¬€û)‘Pr‰ÊÒŸ•¹„Np$Òw؄ž¨Ô$ðà²RBGZ‰Äºm~žÅL>þÄ´ vŠ¿3®ÙÜk2lºÄ5N‹9Ã=èpÜO?³ÐG"÷’òÌ¿âà+{‰œá¹‡3âÓ%Îë;Ç=œG\•gÊ3‰óü+ ‚k8Ã5/q†ëÏÎõpÆÏpÍ "ÏxœA_^S^sxy=r®ñëá¼üoåRyM)ÏÃ5®)qÍ ä~8¯«÷³\8SžIèçg¸ΰ›E¾+o¾šÊ_þ*lJp¼e±õRóž3pO™yãìiJ7ÎÿƒËmJÙ|“0ôp>óÿ‚)C3ÌÏ&*F.LãÅU4À%’²Qß/G"£Ÿ"ñ.et©ï/ç‹2\ +^Ÿ÷U7=/uÀ=fä¥5Y¬ôe (—ó,¸õçË»´M‘˶øŒÈ7uî!û9<÷ÈýpæÈ•ÊM{®¢rQ†œé¯Èû¹„3ÜçOÇý’/Î4Ïóh o¾–‚@äMkx½8Oyq”qMgçáÎp\ÿ„¯WqÍmJ$ï¼þ¼çžÊ{x—çá¿'ÝÐFœ÷³;2øyúà› ¸ÿé_þ*l +Þ‚{6®\”qæ¨È{<êÈ=r¹¦y ç™N2 ±0ßàRðE‰ÏpfHJ$æ§4Qƒ›ºàFñ¢t8Œñ7;\•ð£ Q§’¡ÿ•n{SGb>Êw†oXúÚåèj.‘X–°JR“€à3œ‘óü”´Ôm +Á +ôå5›ø…qÏ}ßçá~pI‚èw®ÎÜÀEùÆçÕT‚Ä:9—¸|(tò{8CÍò'gn÷xÔ‘üŒÈ¥R"‘ןhZ^r®g¸†3r†käz2ÎË¿ð)ûµùc¯öO°ýçoîiÞæ›33ÜàvGˆ¼Çc;¤üÂUü—¿‡v¤ òÆæ:] g[ Û_¼;_ b|-†›Ô' 0Ãx„i WË¥;oÁÃpvçWÂ0/udð0üe¬÷ R¢¾2x¬ÀŸU‚K%xé;€( 8ˆ/¿¼KOç†ðìR¸´5oÌ ‚3\gÜŽ¡Ïi¹p怙¿ÑÁ=W噈Ìc;¸‡Jë.£ò~eÜÃîýOræ—БQSçÅõ¬Òyן´¼çéŸáš…•?9sõLþÿ™ïöNèKlçýÍ•2Ö“÷Ü5-—Ä=—â~8wÛ&È=}pŸ|݃·ÿò—`S"ßy£s¿F8K‹K Ëž[‚3oÁ™ô…R‚ É–,ÊÏpFÎ`N‚ á)AÂø• Á gã—“7Fúâ¶rñ[ðN¬€ûM!oê¸äO£ò+‘H¬øVû=5AtÀ=…‰ub5bê3«÷ù‘V>t¤M ®ClÜ™íîá̾ËàÁq¿ŽÍ9ÿ”±ør7Ȥä=÷à‹Kg:d9Ï?)+Ï“¸ÿäLçL.žqq $öêJWKX7©¼Á5œGôåyÊ3r†k®\Ó¼^\ÓÄõpÆÝó}¸­\*Ë{8àÌ%kÈyfÙÁq¦”÷Ëq?%È=ðE÷PY‚ÿåï¤Ý)ƒ/JÜs⿟ì,I¥ì r噃3cê+/$¦Ü¢ÜœlpÃS¹¹C•@3ÌäJƒ:Od˜êÑoäD¢Rê,JècËD‚¸}q\’‘— ðUá¤&x䛞ÝÊHTJhÊà–Î’Êó,»\4aw‚Çõж¾ÑùÂý÷ƒÒA"çižGôó3§®T‚(±‚\~±÷—÷p4‘”pΔ›q¿8ƒGqOó~Ò¥R)¡<ÃýgŸ\óäå5«—,u@‚_Oÿ¸{#ÊÈ(Qbúù@9üsòµÌÍYº¡º ¬¾_V/§ÈµUob>““%)ÉHw¿äU\Eaʰ¹þÎ|G§l’á[®cÄ&t—†ßUè(ó‹§O!G“$E:ÙPÂYgºKÊgÿ‰‘9*£šDq¢IÎdÇ5–ÒF¦ãTÿæ/šå$öI›dÆ3Qü£RW$ +–(áu½x9]™ô–-CÇ#Æ|JŸÍ–(ø}òØH¦îë ßiÆjO—§Ó%ÉjÕ¥¶õ“ßO¸4yÊÆµ‹Äú,ÃXmõ>iLÆjÕK…™eÒ&j?Ä%t—†÷Ž}µµÕæê$)2Ü™|ú“ŒlªKåp‰)ë몌¹”¥‘Úýé¯ý¥úi«÷ Pýp¹º“ÏDÚ$«Ç3IIâ6,QFF¹ö³ ãõ7—¯éLÖ×Wö~Q(ÃÆý«éÃ’§ÿ/JíÃ*gšŨÈÐqôINH‰Ã—ä0’6™sXbÌ¡’%'¼LÏŸ†¤Q’Q†îӕбDAñü’§HÎ^§Qâ] Ø(¡‡îí›O«.>4…”|ÂgG™/]òô·ölÕ§Š"uÖZÕË8»‰qõ éeõruf#ÃR2Ë”ä˜ÛÈ%IŠŒ³¿äÒ+YûU¡Ÿ9ÎÑ;uÿúRÍÕ‘Ñ%‰…)®VÓåêq5›äpI#)“‚ò§±v‘úÚ%¹úfŒúCLJƒ% +)’éÎê"Y=b££d<ÍF¹þæÎ¯)]žlV«Ãê¯?})R¯æ˜FÎN?}“|úÀüì¤öÑê°NFõ0âGAaŽ}%KÅ_É—’6RG1*/žÄ%92NÎëÁÆƤ×2¼Á3ÉGÏêì/ù`“¡£ä+‘.ŸVým¢ËÑ×þT[]ÖNü0J²IZ®Íˆ‚â)I¾Ë7—V?O¤[¢« ºÆÚUbí1ÉjÕ›|“Ù¯ý?(e5eµÚVߦ°z¿:É~õ¸ZõR‘újózX­ºXJª­VmýäÎYê’)Õ—ªÿiéŸË¸úŸ3º$GÆWÆÚ!cõ (ã5^O¾&R$¯"QP|Ý1½ÚjµÏ"üð&£ú¼bʘMJŽ»†Ãˆýç§ê6™CX‘Q˜ƒZæ —düþ+pn’6ŠÌ‘t9ŒäNI–RG!E¢Äëõà ÆÒ›RF6Þ¾Ôù´êâƒ:ùôìC·Q|ÎÅ2tRžþÖxZíâkÕO6~¥¶Õòk‘ÙTÿ–HG%Šä‹K«¸öU=‰2Œ1=Eò§‡KR$ +ÊÉé¶ZõçÆ—:n[?ÕÞTW¤þRû‰b“\{\}«UË%/[êöÉêÛγÉCV«.siuwÚÈa¹ú…”Õw*¡ÛPûR2,åÉæúûË7%cº‚¾ëêïýTm5?¹a™äi+‰‚½3:ßÈ(Éè­úS}˜øl“LÁ=(¡ó´êRíé?.èòüêõjë(~<Ã&tRrOzòôºÊtRrg̦º$cúwaº‚2^#6¬æí£P½¬¶~Sû£C1®¦ËêH²z´G‰Õw†ŽÂ\RXû±’ÕãÚòO¬}ÿꫬV»Œ¹Ÿéõ3q #õ“½\½×ÇŒ.MZ®./ö×?ˆ¯Œ)¾SªGEê¤H˨þÔNV—ü\Ÿ>ŽžŸIrˆÉÐsÜÍIhCŠ´WdäýtætýìNÊy ëáÐKœçéÊ‹¿Åm)(e¤KWÑ'ý2ôÈ%©cœÝ{‘^|ÒÞrT¿}æ“QP|tIŸ°LGÉ&ô“M<ýÝùrIÉ×=läÚ¿™$«U³<ÙS_û‘Kå4LžlPbºé2æ©Ð©©“"‡qõ«“Õeý9©Ã|bQÝ=³½B5¹ZíÕc}%ÕÅS)±¾T[­º¸_Ž×Hõm£z¬m^ª‚>ù+—V?|5ŒñÝ'C¿þþòMI”˜®0ß>–ùá]ê« úsüݧO'rdI©GN¹H—á*ŽÊaäÓª}ºä¼­.(Žâ$s\ë’{朗¡[¢'‡1Ò'QPð ’³0åd‰—„b£ä]0]¾ä-:ù4Ÿ>=¦¸Š‚OX}dôíÔþÖžNßc¼úêóa¬ÍocØŸl˜2fs¦ü‰ÆŒ +SЙBºD‰éÊ‹åÚo0=ªoV¨ÞI¯¶ºøèj› ÓSª¥Gµlª­.«ÕqiuC'/‰ÕWWËÕ¹„nOõ=uX[õm“((üZ˜âdX¢ ¬}IGa +úõ÷ç›Æu|­ëg§ºT'.ÉðûÄ¥HuŒÕ½šÓ,9Œ8‡r¸$Q>ûPåÓý³ÿ7E“¤äˆ–Žn,eø‹Y*a$%9ÿÝ£$1†1,ål”Ðó2$6åŒ +)yÙ:ŠýÓoŸFº2fô/.ɰÄס£ÔöüTýµ¢œV³—ÕÖÁ¥ömJGÆI¦ü÷ŒŒ“¿òb$sO6ÆÕ…W—¤$Gn“Ôf“´D1®.²ºW[]Ö¡Úú2ŸðjÕ¥¶Õ}µêoJ®V»„K²Že6Ô±¤Úê²v†7’QA©Í–r6Jèä*6¬~†¦ £„~ý#ø²x•3É·Ï:Ê©z㇪<[5ËôçP=VtI¦8ñtÉyBZ{‡ê·êƒ× ÆHwŒ;¢Ñ™b?òwûH—¹$É&ùIÉ^êøçä7÷$#/ÕÍKï«öU”óCÐ-Q|Vè¤H,éRÏW€ni £e<ýßC•X­ÚÚêèøÁ`‰ò’¥D‰ôyìÉ%¦.ãìß\ ü3*ëkóÊÐ9Ë)OUý„QíÜë(ÕcIíBÂr¬þ7ɪ/ÚV÷¹³Ú:ÊËùÏ¥ËìQª7É×HJök?ŠôI”ÜS[6I”‘Q^ÿ¾¬?ÉÕɱú—`©Œê=~ÀN¤jéÉQ_ršÉ\uÐé2}Ò±™œ³ôÓEžœº“ÕtRÔèL!Çû$Š¿ R'Å%²”¤È˜žûý[ÒR ÝFóòÐÝ€³üô»K‰j> +|b¤ø0•$ +ÙcƒBŠô•ɧ¿Žt‰ÂÚÎM‰Ÿ•?q•jÆäÈ(ÿƒæfå4ådã_—¿Ê “#£<ÏYM›Q_V/W¿’”¨îõ•>vYý¥(1} +ÕV«]¨¶vau_^Êú9J”ÈR’"cú÷ý¡¯ãRØ„î”H—×?E¾/É«ÈH—ùÒQNÕVóÛÆ¹$-‡Ñ’êSëWN¶á6œLz¸$s®~uìÃ9<2&Õ(Nø°ŸF”S6g¢ xrŨH”3_¼*{©“ŽòÙYƧßìg§·.}b>I–Ø ãÓ»"‡%¾²“{$Êê/:¥¶ÕK‰ –(/–«ïQNÙH¦ ÿçx,¯2Éê×oœBŠŒtéùÍU”‘;-‡1ôõ—䨶ú#u }T_¢v‰ê(%Ö¡z¬ÎÈÍøW²¯~†aDA9es&«ŸS‘édL’r&ssÌžéÊõÏâ[û–½d5#Jí 6(£ú’OÿÑÃHõžú­8ÖB'ç¡$Å9l&q®Fõµ»ã0:·“ÆÉü¿ú¾ô§M2OˆqXžòp[ºŒ¼rFËoß%>(£d +>XRìŸþ³’Æjùʪ +J¾âÕI6Q½¿ +lä‹eè¹3=ù'®þ'x`輊ü“\•¬f ù'¹*W z¥Íjµ/)ëç¥tVß¶ZõýʨÞ$±‰Ú¡ºÏÕ:¬–KÕcõS¥¬¾: +.ÉÈ2›ôÈF®^&Ãø«ÜùÍC®4_bLÏw=£‚B.¡PmmM%ôQ}ÄÕÎÈ—œ“0EêÎÏ0J›OÿIOòùɱl)Þ((–’éW’Ƥ%,‘ny&ögf¯œ:^OÒFbDñú©öÙª{uâcD!Eú |z(F%é–y¶ê^ý·ƒ”W®µªÓè— ³¶ênŸ<¹„âJdy& +SþÓÃCŸƒ}ظ$1†Ž¿’).¹Yêç’WÇù¯ërF¦+(î‰ê×d1tR$çGäs“(ç§Šbµ=Íw„QFºd­UÔ¶º¯Îü6’¤L¾ÌÒcåŒÊ¿Hž\¢ ¼x1ÕoGG—¤LFz%”u°I’"Qr§’$åÌaŒµuª{2t”xõÕ9Ùc/¡“%Sr)y.I‘§×æ§+×ÿ¾Ð‘q’µ~6(¡Gzn[ýqÃIEõÑDŠ Õž.Ïoª9Ù)Cîq®’rfNcYíÓª9·9Ç3ýQ@—§l¤{üQŒ‘.I‘¸cÒFꡇ½]âµI#Ÿ~#aä|ãèX2Ÿ˜ÏSŽ,‡ñÙ¸/#«¿Ê,åÚãúMmù%$Q°G9ýºaÊ¿qdœä,(¡Çô”y× +ÕK¦ “"¿Ù£„ç‘ßr«ÕåLK‰…õuéÌ\Ms“´©]dºdíK#£\?/é±úIÐå0^ÿø*C?ÙŒù (u\2Rý#©¢a“OŸiÏΨ>ú”Élp`JltR¤3–§1JXòé?%ŸÎø´jŸ>ÌÃ8ŒÃŸ€“«—t©“.§£œlH‘Ÿþ×%ßÅ[Àm(‘.GÆ$ù”d>ådóüä‘¡»A’â;¥ö—»Z킟AèQÇ/äì2÷¤È'ÿbè'›_}_ÊæL|Ò82þš¸¿úSÕC¯}uÌ&%IЇ „%)«Ÿ-lPP†q‡Ç¢„««¥Ë³ËÕÿ–Žr²!%ù'®^ÿ3ø6II’ z¤'qƒ4²Zr4%QÂ!FJmO÷çP}0JRd䨔‘sU‘éò<œ¥±Ÿæד(–ÕÅú7·IrCƤ'±|õO?3ŸÞDº%)ŸCm^¿Œy›JòÅçÃ7KR&ÇÓ5O)J蜅Õߣ ß~Ò%©Wß“.R¯.(LAGAAù7ó’’ %ғÈ‚2Œxïòe–n‹t92JRäøu<óü'¤±6¡¯}ÉHŠ¥ W©~ «Ó(kwô$Ê‹%)r¯ÿIæ;=Kœc¬V}Uqøè’ŒßœcÕ÷ÈçèãÙœ„Ã>œœÃˆc…‰òé¿2)Õé—TÕIµOßR›M¤K8NY†î±((u<Û§Ëç§êM^gèçFAÁ»æ,¸ªû”δ—ø$Q†K<[>ÿäÓ#ºDYkUr‡ +YW—ñÃm¡ókùÿbþé³ÄÙ1†Îï·º+ÉÐO¹G®}¿ÆpU†}¬fƒ‘Õ£BŠDYûRr5ã$ +Šª‹ =Ò“ÃýÒ2ô—,%)ò4%Î~ýÓù6‡…”Õ¿:2’’\ûO‰.µ­Ÿ—$OÿYÁ&l¨æÔ¥ŽòâüÄ%ÉYpØNòéÜHŠü´óül¨½´ÁHþ” K£òé›è2£üô¥ÏVÝs)>›W… Ÿ¾3£Œtïs”ød0¢àc¤öˆe>sÅ O«þ¾žV-WYÍ×-kup)ì%–k÷aƒ‚$)“ÿfþÑaDA!%É«LzSTw”Ð#]r–ðXiÃꎎò’æÒYHI²~¾ªI\’3žf骭¾]’"×¾„ñ•1] ýdÔë*_ñK–“Laõ/pmÕMÈâà’dDy¶Ú½ö‘¨ ¼XG«Ãv +Žhi#‡1>}€;çSâ³ûç{¨f”|úR䯌"?½O©Nª‹}è‘î…%#7SÍ;’Ù“bIŠDñ™œ,ãé“§U˜ +ÕËúùETçêÿT «¯JV«ÕÏ€:Š{$ÆÐ™ýpéoh^Ø«$™Bº µÍFÉHF”ÕÒ%gAy±F¯]&³ÆH_ûž˜®ŒÜƒ=gî©Þ$_#JœŒåúßÃ7~Êofõ-iduŸ¬¶šCIR͘tvQ_\’O_zZõIéÉHϹª;xu”_¹…g8Õ>»ŒOo>­öŸ‰‚2Œ|úÎÏoªïW¨}× '?=†%¼)#J¤KRò! £jO—§ÕáÙêà‹°Yý ¢T/åê¾:Yý½‡^MG!E¢¬ãþ°LþCyñ'”“ )ŸC}±?“7Ëôd¤ÏÕÕ%Î>܃Ât¦Œl$Êê׬£  #)ò›ýuü$$)É8l:Õ¿@lœEè¤ÔÁæÌp Q?O¼§9*Ó¤]Ú££ cŽeiŒô$Ÿ­Ú§‹«)Ÿ£¶O÷Oÿ·…OwYÝkû4ωBõ²š¥ü# +ÊÉAqÉ»Ö_E’Qfó´êRýQ+QÍ&9Œ(¾26ëÏ|ݲöc²~.ëuR’ÿD^ùx×®ŽŒ“§sóÝóT‘n‚2ŒÃéI”ÐI‘ž¹º$‡1Ò% +J¤ËЇñº¾ùmÌOî”K2œEÆúÉòÛ÷>›})TËi)QPÂYšqÒ†Å((–RG =Ç»ú§ÿ"Èê½’Ì_ªÍHµÏOEöʘ͙–^¡’ÄfÒ[³Çºed$]ãÓj—Q½©þ´“œ…ÕeVÑŠÔQ°a­U6’ê;Ç9~wùO䕳?Ù|ûÞÏf +:k˜)è#㙡£"WAJèLáOýd2Œ(ßf¯\×)¿ŠÕ¿jýOÜ@µµÕ>©t”1cî‘OtOg8‘^}F]žl°òìIç3Š1E:Õ«KèŸ}Î'??Çä§UûüäY½¯Ÿ\J¢29Œ^d%Koìåt%úÓtY}I>ýö’TòSâìd”«¿¯”Ú¾}”u\Š‘êF™Í+ÿ¹¼þaD‰³ÿIîye¬þ¸N–2ÎÙüš«ŸJ‘zý,1=%IÊ™((ß²—§ïÍuá‡²ŽŸheõ¯”Œ¬VmmN§¤¥<{ÌA'dzåx|¥£UãpƒÓ8™1É—PPH‘(ù3ñk‘ŸÝ•¨“(Ã8ÎQŸ×“"í“FtIŠw*Q²‘(6(ãéO5žþã‚‚‚ýêÿ§b:º$ûV«]¨–/]ÚHŒ((¡Ï /®þÏ0ïEAAy9—g'£ÆðéUgZÊoöÕßTuÚ(I¦¬} …‰ÂŸÊ™#cåÛŸö×ÿB~ ¡s–‘Q®ý{ŽÚûá°²—é‘“Íæé“ðù™T÷Èêu§“8i1JœÒÒHŠDÁÕÉHŸd +(‡K(#c2q˜$E~ú¿ò(O~²®J¼AérŸƒDÉ¥|Œ#£ ÷ÈÐWY2tËXkÕÎù–•áÕKEê(¤Èkäy%Søµ§$ÇŒ¯?‰BJ2Ò'Q®ë¿ŸÖ˜1?WlHY}’„‘j«Ï¥0ŽŒI]õHu©N—Rj'L]ê¤<½Aw>ó]rÝ¡.Cÿ"ûäÉ÷'ó„ÙK>} ]2ex”¥Œ¿î^¼Ä6(O2O«]¢z¬ä#å,ÃHÊZ«:©.µ3|ÝrµÚ£ÄFb³ú*“×ðð*IR&QB'E†¾úÃW$SÐQ^÷ ¯ýMì¯ë¿W~W“1??%ôÐWß³šcŠKÖîÒ‰'u¦ »4ªÏÆáD•¡GîQГŽâ_¹älgÊk‰2^#Ù$óØ0&QB'·)’Œ’ï2l¼—#y³6O>º2|€–<­6{^%é6ÖþvÖZÕÖ>…VoV2ôáªDY}sºûxL>‡êÑYš‚ŽÕ'-Š£øt.΂‚âO€$£|mŒaŒt—’ÆH—ö +Š‘)Ã&/Rb#)IlH9óÛÓȳ?Ïg«ýÙ²~SÛúùçf5_q¤ÛË“Íjzr¸$Q®‘d’”ä˜qÊËì§œf©p”‘Q^׿B~]’3’ždn@1ÖaµÚÅ©õÍÕd<ÍF2§hʳå0ºúJ‡sŠÔCwà+1]A™$ +YN†½‘yöälR¤¦ ‡Ž}ºyƒØóô‹sÕ£O/¥:±‰ù\b­Uýù£'O¾S7œ²‘Ì R¯¦ŸÎ~ )2ôKRäŸäꙤ$ã쿚”ëúï5¿«)«Ï0ÊÐCGYÍ•®Èjé(Þ§Á˜¥‚KNQIJõýé(6g¢äpÖQ°A±qÚë +ÙC'7 “QI¾2~]†K¥$3žÎÍÙ1òô{§ÿ^¤Píé’=O߃>Öþ:Æúâ!(øZ%Õß²"õa´D1¢   ãõ«óÙ®üêûR6“(¡£œf£"CgÊuý÷šŸ–‚gY&Y­Žn ›Õé ýÙ'¡RMw®£úžÚ‰¦?­šGIR’ÃÁ>f|ýí6)¡Û(2<­ñÅmòäN‰KóéJ¥‚.J½Aj¿Yª—V°øp$¯’Äw!cíGá²Y;ÝF”ÐQ^²L¢\×u…3)œ‡ ¹$™ÂÚ'Õ¥ö‘EFVÿ'ää°?óé¿8)T”ÚcJm[l$Žhi‚2\M†ÎY"RPPP\RŸe’WÉý +º|úÿßýÅ%‰ÂÓïHÖáÙŸÀ0žÎ¦ŒÕ_ÇÚßBzT[ý¥'±‰ôI”ÕjoN–×u]áLx±\[õ +Õý”%«9¸$ÕV—Õé +)r8£ú År:uÈÙ‹2cuyÉÒ‘ŽŽ‚‚Kò”Kr̨  #žDÿfïj +º$/õé4¢KÎ Õž­ºW{º<>(¦ ³úë½ÚjÕ7§°º¬þË¢Dz2Ò%SF6òº®ë”“A†£¦zLÆ,ã{tda)µÍ¸Ž´|9OËéŠD¡~óìóyXN:áÑQB'W“ÆI,õa#-Sd:QÂ¥S6I/µúדÕcd|úËÚŸCý,“OoÂæõñfŒÕ}mùÖê`C–ÊŸ¼®QP®ëºÆy,¤¯>|IFlP†KÔ¶Z޵“M®*¡×>2,yúÌ|Zµ§ï‘Õž.ÏÁÉlƒÂ”óäl\ +£ô +z褸?ôÐç’‚.™ÍÈÆ?‚Rûõ1òì}Þ¬4ÆÓcÒ(C__ìꬾó»ÈÕ}5_eØÈ˜®¬}³”äu]ׯLqŒÔîS,ed)Q\¢úàRB'eîÑeõ'Ç&ÊxÝ6ÝmÕ‡mý9ç¸Vû¯ºMJò5NÚ+¤K¦Øëò”ýééötºšòüLê(nó~ɨDzr.åãb5j[­ú+CG!%é†X=&¡¿ÌR¹®ëz™ÃAqžÔ>[”IR$SVß)«­.s¸ª¯æ’4¾rQœŸrzõñ[?Ó9,yöHɱŸD±TäȳTžþ/2:Š ŠQ‘éuIÆÐ‡g«~ÎôQ½¬Î¨íÙýéòtâ(–2lHYûƒ]ýicL_±6_JõRgº"§cè×u]-g…D‰ôÉ9g&QX}@ÏÓlÔ1Î J¤'ÇyÏœ«ÕGîKíeŽñjzÒF¦OFú$SÈC¯¦ŒIRÎDyöcŸ.ÏoêðìÑ[þÎ1£²ö'¦PÝåê¾:OkoVóÝéòd铜庮ë/8(†Ó¦ZºÄÈÒeè«ÕQ戣ºT[ý‡F‘¤$]ª.¡£„ÎÓÇïÓjËIÎÓK£B5cØÈ°™å”0¢ðü|†É·Uß6Æx¶:zÞÝs”¨~ûeíO)iµ¯®ÎpïȆڗlPFÆ$)…‰r]×õå¸ %‰22®þs£ 0Wc²úÎê<ÍÕ0ʵ=ûh =ìW?=ªoP¨îò98χK’)n¨¦K”8/…NÊÓϬÄôg?Чï‘uÈ+”¯ýÓcöTo¨öôgB>›Õ ®HŒÃh_Û:îI²þü%ž%7ÿÊ ×u]ÿN ¦8XPB¯Í–2ì_Öñ±z¹:sâ­îd”#£3¶º0%fTž–ƒ=Y}þ§ð…jϾ™ê±ö£¤þt c¤?ûæa“¬¾J}¼`lÎ|z?%‰ )2tÖþèN®æëX͘œ¥‚RýU¦ŒÕãêtõ۹ׯëºþ‚ƒ‚)8^¨fürói6ëê'gà)¬ýÓ‡³Óí“ÕÿDí;Ÿ>äSª3#Š1Ò“Qû†dè¸tzz/©6cÔÞT{Zõ+O’M<ý^ÐåŒÞ—ÄžÕoóÃÆ§­Pû‰ ÕV«æ~”Õq’/®J~-×u]ÿAshœ…)#›$)rõÙµú¸“ÕV—Ù¤PÝåê¾v>û¦Èt\•YF6“(n ÚÓœó(õuæ××CrÕžV]ªo–Ï_š{Yý¨ói¡¯~×zœãËjõUFõ3(Ô¶Zu©ýe¡0ÅÕIì×¾?ô뺮ÿWN˜® â´‘é(¡GºÛj3’s/iDAAq‰Õw>}8§+“((1]!erÌñž½Ô£¶§ÕQ˜röá©Üö4]žlP¨ýïVÓÃFÚ k3Æ«£ø|¨mí¾ZµÕeu²ö7ˆ‚Âj“(‘>y]×õÿÄÑgR$gA }¬­¶õ›ê}m96«­þ»é–’ùôiœ.£úª%z<­v¡¶§û³y”Œê'Gµo®]Ü Ÿž­ºW?OÒ(_r …µßõiµjks›¤z9ßÅêMd™dõ%ã0²úáIldè×u]ÿ¯œß²wÔP_ý”%:)³\]V§“PÆêÍjöL±O†>Œñì#Z©¦'Iq‰Ú]b|ZµlNO_²G§zIµgs62£F\}ZºLÇû­ÞÈÕô“MµÕª­ßJT÷ê¤öº +Ê):Ê0#Êu]×_sVDz’”d8¦ªÇ$Sp•jë8ÐX­úf¹º¯/ÕÙOW&ë`#QbŽî1¥šž ”§Õ.Q[îYý$ª¾ÇFRÇX}ç°¡z©H2&ÃÓV?yõR²¾T€)«Ó(W÷°AY[F©SÝ9ËX}ÃêÌ%‰r]×õŸä•§óÀ!Ecè¸ÿ̨>¸Nµ7UÕÖ..IV߆>Œ§lr’§Ë§OøÈf2Ü@õd“DqédCŠD‰ôäé{ƒåj:ÆaÌ^ëª?v¹ZºLgŠý)›Éøîòº®ë¿…#…ï2ÇYÚT—8;®Ž«Ájk«>QÑOÕVsõÅ¥ÉH—Oÿ½À˜®0%\¢z)ýßñSN6(¡ÇùÀ§û)û1cÊӶ¸úúˆ$«Ußl‰b3ªï©#ÝƒŽ‚% +–(qö¹'E¢äždè×u]ÿN’S6I”1£Ó”µÕîöÎÌŒu$.ÉÚÜ–qõŸå×,ùSIò]Яëºþ[8RBôµÏ¦„¨¾³Úê’KÕEÆê –8Qõa´tSª÷’Õ2®}I™$Ëa|ö™®ÄÓf£„emF”˜®„¾ú_—µÍèL£\ÇCV÷”ð(lHY{6'7T[­z)×ñ@£dÊÉ庮ë¿ÎyçY¤×f K9\«/)L!=¹ú”êñTÛyØÖ.Q=VsÊêÍê´Aá,ø«!ÃÍ2—’¸¡º+ÉêQ>ÝÃ…ÕÏ#£ÚÚÎKT_ª~†dFÖ¡Úê²¶:>Ãj“¡ã†I˵Ë$ÊÈ(C¿®ëúoáHù“\ ýd3fT˜2lN«ÊµÍy»vÎ&å¥úžÚ‰Û’ùs $Ýð4YVÿáЕ$ + +¯"=Jêë§ÚW«M_;±œó™¸dÙK”aŒïžG%Qª÷(|—뺮µ9p¦Œl’8»¤qõ!ÆÚRÖÞ¬.œ]¡úf%‰‚BõýçÁ[½©~”dµj«oV¤¿«÷ÏþóÁêÍêÛPªoF ¹öÍ#ËS^ŒK:Õ/ïY­úzT_šbs²á,¤H¦ž$åÌЯëºþ=œ9(('”ÐãÕQB_Çù‰‚‚RM—«é’êMuFm«U—j«Ëê gûÉf&ŒqöÕ7(%r¥ö^'%ùRÇÓ²~ª¶ZµÕªKuæ©¶zDôÕ7¤ËŒaD‰³_×uýÛ8|þ‚8K褬>eµÕã·\¢ÚêÛlPr2¯V]ªÏö¤‘”dõ=QW_«o³AG ûÕtR,IY½‘õÓÚª{™û×£ÚÚ…õÕ}1]Yý¡³zT’¡‡éÉ뺮NüÚsv½XV[}UaJd\ûNlÈfõQª­]X‡Ú戦ZNm«U—Ü/gTX}Ã)—Ö¡zÌžÕ]ÆŸzTß_;çE¢Û„ŽåÉf.¡“²ú*ºŒ?õa‰‚r]×õïçü!%92ÊÕGœ"Æj«¯¢³ö>›3×¾S¡zÏt%IµÕeýÌ_ͯ;«§{~¹ZíKQÛêîNWi“¬=R}§Ñ +ú,%ÕÏ#×î(‘.¿ûéÊÚÏ#×ÏKGA!E^×uýÿå,ŠtIŠÆat¸É—\b®f“F·Õ>0S¢º×¶º¯>ÀSb5ûÚ9Vß&©ãÏDJµµËX½Y­ÚÚª{õSÍ3S½—¤«–2lê`ú0âN9ãKöÉË뺮¿ó\ÒGÆœu¤dIŠD9e#W?dQ ¡£¬ÿ€êÛj'ë§úmÃê’“¿¶Õ}µj^‰NJõÕtiŒÕê°Zm«ï—ÕÒ%ÆÕ} +º$å;CGùf]×õ÷‘sé̱úèSä0rrç˜Ñ ¤LFº\[õ†tV—u$«U[­ÚjóçC‘ëPmmnHžÿb¬­~Z½Éý¤$I©¾mº’ =Ò“§läÈ(W?­’<}o®ëºþœN/¯¥‘µL”1£²úNÖ>‡ñWç%ª~lõR®î¬-{ÖVÝëȨuæ©”Œ’êŒ)µo¨.ru_‡Úcõ=ŠLÇXÝSV«Íþ̘ôøë.¯ëºþ>œK¡£Dz…³¬>õ)’õótMÉ’Œ(¡ã†jécèÃÓW«.Õµ­î«ÿåE®=zû“Mäê8GÕOhŒ³sŽî”6(L¯MÆdèqö뺮¿•P2Ò%Êp*R½”¬=’"cº‚;¥1t¦Äyé%÷ T¿€tªÿp(Tw”ÕãÚª­v>–Œµ%Jœ}xˆ½Ãëk©„:Sð¨êq + +¯2ÉYPæá +JdyÊæÌ“ ¯"™§2ŒñÝåu]×ÿ*RÎ\?Ïêê:«FR^ù«y¬B,QH‘(k? +# +)’Õ÷Hj#£½Ô«K’¹ö¥ÐQÂÕ¨^J¦\×uý/ä ü•K(#crQ8 +)gÆÅ(cíýä0þÊC¤˜Âê§ +{RÎ\ý Ím“L!]#Êu]×ÿNÎ@RdèLùæÒúy Û2ù•Û\½®ëú_Îa8fLIÆô³¬Ÿçó_Œn=rUV/™žÌ&lbúYâì‹b¹vAÁ¦z_{3º$O¹ôk¢"¯ëº®œ‡’ɯ…)1£:ÎçjécèÃÓ•qŽz¤'II¢  ø×C'ËÓ\"W%)2¦§$_,¯ëº®ÿ§(Êiõ­ÈoÙŸIJzµÕK…IŠ =t”‘1Ó•ü+d”#£Æëº®ë_ÇIË”aéIR$Jè9ÛÓ“§l$ + +ÊȘÄVÓcúYÖþw‡q¼Æ“K×u]׿šó–)/³WPâ»'™Ât%ôo³O‘(((1=%‰BŠŒé)I”뺮ë_ÇI{šMŠ$eòÛ÷~6)ÉaDÆÐQHI†Žú)›_“‰r]×uýK9lQP¾eŸDáUä˜qJI‘¡Gú$gÆ_¹ôÍþ[öòº®ëú·qð’’üU.Mã¯æ’z¤ŸÉ«$GÆdè¡£"Q®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëº®ëßéÿ!º‚ü +endstream +endobj + +6 0 obj + 134207 +endobj + +7 0 obj + << /Type /XObject + /Length 8 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << /XObject << /X1 5 0 R >> >> + /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +272.226562 0.000000 -0.000000 338.357361 119.886719 90.744507 cm +/X1 Do +Q + +endstream +endobj + +8 0 obj + 104 +endobj + +9 0 obj + << /Type /XObject + /Length 10 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 15.562500 15.437378 cm +0.000000 0.000000 0.000000 scn +0.000000 0.000031 m +0.000000 451.298706 l +303.743164 147.476624 l +311.959686 139.257233 325.283234 139.257233 333.499786 147.476624 c +341.716339 155.695953 341.716339 169.021393 333.499786 177.240784 c +29.756639 481.062866 l +480.937866 481.062866 l +480.937866 0.000031 l +0.000000 0.000031 l +h +f +n +Q + +endstream +endobj + +10 0 obj + 420 +endobj + +11 0 obj + << /Type /XObject + /Length 12 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 187.691406 139.141357 cm +0.000000 0.000000 0.000000 scn +112.318077 230.817093 m +114.451881 236.621597 106.507004 239.951080 102.709404 235.070602 c +70.894165 194.178711 37.289165 151.643723 1.607707 108.848343 c +-1.328853 105.327103 -0.157611 101.418564 4.426768 101.506927 c +20.839167 101.823715 57.499165 100.883713 57.172985 100.331924 c +57.264164 100.648712 34.854565 40.328918 24.311525 11.312057 c +22.424946 6.120438 29.099884 2.524002 31.687706 5.647614 c +67.369164 48.713715 102.736664 92.306213 134.258621 130.929871 c +138.408722 136.015289 136.225098 141.255768 130.969559 141.267990 c +115.661659 141.303711 79.236656 141.303711 79.280838 141.150482 c +79.236656 141.303711 103.559158 206.986221 112.318077 230.817093 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 187.691406 139.141357 cm +0.000000 0.000000 0.000000 scn +112.318077 230.817093 m +114.451881 236.621597 106.507004 239.951080 102.709404 235.070602 c +70.894165 194.178711 37.289165 151.643723 1.607707 108.848343 c +-1.328853 105.327103 -0.157611 101.418564 4.426768 101.506927 c +20.839167 101.823715 57.499165 100.883713 57.172985 100.331924 c +57.264164 100.648712 34.854565 40.328918 24.311525 11.312057 c +22.424946 6.120438 29.099884 2.524002 31.687706 5.647614 c +67.369164 48.713715 102.736664 92.306213 134.258621 130.929871 c +138.408722 136.015289 136.225098 141.255768 130.969559 141.267990 c +115.661659 141.303711 79.236656 141.303711 79.280838 141.150482 c +79.236656 141.303711 103.559158 206.986221 112.318077 230.817093 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 187.691406 134.211060 cm +0.000000 0.000000 0.000000 scn +112.318077 235.747391 m +108.788986 237.044724 l +108.788910 237.044525 l +112.318077 235.747391 l +h +102.709404 240.000900 m +99.741928 242.309937 l +99.741806 242.309784 l +102.709404 240.000900 l +h +1.607707 113.778641 m +4.495335 111.370483 l +4.495601 111.370804 l +1.607707 113.778641 l +h +4.426768 106.437225 m +4.499225 102.677933 l +4.499327 102.677933 l +4.426768 106.437225 l +h +57.172985 105.262222 m +53.559685 106.302261 l +53.036144 104.483368 53.946251 102.558853 55.684322 101.809479 c +57.422398 101.060104 59.446564 101.719513 60.409737 103.348846 c +57.172985 105.262222 l +h +24.311525 16.242355 m +27.845428 14.958176 l +27.845484 14.958313 l +24.311525 16.242355 l +h +31.687706 10.577911 m +28.792362 12.976776 l +28.792278 12.976685 l +31.687706 10.577911 l +h +134.258621 135.860168 m +137.171616 133.482788 l +137.171692 133.482880 l +134.258621 135.860168 l +h +130.969559 146.198288 m +130.960785 142.438293 l +130.960815 142.438293 l +130.969559 146.198288 l +h +75.668030 145.039062 m +76.243362 143.043762 78.327271 141.892639 80.322571 142.467987 c +82.317871 143.043304 83.468979 145.127228 82.893646 147.122528 c +75.668030 145.039062 l +h +115.847176 234.450058 m +116.701851 236.775009 116.619583 239.131058 115.641846 241.171753 c +114.692940 243.152252 113.039856 244.550491 111.229118 245.332703 c +107.622391 246.890778 102.796585 246.235626 99.741928 242.309937 c +105.676872 237.691849 l +106.419823 238.646637 107.465248 238.766983 108.246910 238.429321 c +108.630371 238.263672 108.800774 238.046219 108.860069 237.922440 c +108.890541 237.858856 109.001205 237.622040 108.788986 237.044724 c +115.847176 234.450058 l +h +99.741806 242.309784 m +67.937111 201.431458 34.361767 158.934479 -1.280186 116.186478 c +4.495601 111.370804 l +40.216564 154.213562 73.851219 196.786575 105.676994 237.692001 c +99.741806 242.309784 l +h +-1.279920 116.186798 m +-3.258407 113.814377 -4.442426 110.586319 -3.346803 107.497406 c +-2.147628 104.116547 1.122458 102.612839 4.499225 102.677933 c +4.354311 110.196533 l +3.731298 110.184525 3.509113 110.314011 3.518988 110.308304 c +3.525570 110.304504 3.570336 110.276199 3.624385 110.211884 c +3.678738 110.147202 3.717865 110.075287 3.740574 110.011261 c +3.787205 109.879791 3.727518 109.904022 3.795444 110.156403 c +3.863918 110.410828 4.051676 110.838486 4.495335 111.370483 c +-1.279920 116.186798 l +h +4.499327 102.677933 m +12.622606 102.834717 25.825699 102.680542 36.987919 102.416489 c +42.562939 102.284607 47.599640 102.125992 51.211418 101.966797 c +53.024452 101.886887 54.443344 101.808319 55.382313 101.735413 c +55.867409 101.697754 56.148544 101.667206 56.272243 101.649170 c +56.369865 101.634933 56.239033 101.647369 56.024120 101.711349 c +55.987492 101.722260 55.526577 101.847763 55.024200 102.194839 c +54.785992 102.359406 54.143162 102.837708 53.738750 103.746353 c +53.220856 104.909973 53.351353 106.186203 53.936230 107.175613 c +60.409737 103.348846 l +60.999710 104.346878 61.130787 105.631775 60.609016 106.804108 c +60.200726 107.721466 59.549557 108.208527 59.298603 108.381912 c +58.770741 108.746582 58.266712 108.889893 58.169785 108.918747 c +57.834274 109.018631 57.514095 109.067612 57.357121 109.090500 c +56.971634 109.146698 56.482571 109.192627 55.964401 109.232849 c +54.896851 109.315735 53.376972 109.398651 51.542557 109.479507 c +47.859436 109.641846 42.766521 109.801895 37.165764 109.934387 c +25.976433 110.199081 12.643328 110.356522 4.354208 110.196533 c +4.499327 102.677933 l +h +60.786282 104.222198 m +60.809654 104.307068 60.897423 104.745392 60.929855 105.103455 c +58.742435 108.680466 54.195972 107.559952 53.865387 107.051224 c +53.814571 106.953873 53.747608 106.813766 53.728859 106.771698 c +53.718666 106.748245 53.702507 106.710175 53.696033 106.694595 c +53.676624 106.647614 53.659908 106.604416 53.655502 106.593063 c +53.640240 106.553726 53.620911 106.502960 53.600471 106.449005 c +53.557949 106.336761 53.495045 106.169464 53.413906 105.952957 c +53.251022 105.518311 53.008900 104.869934 52.696381 104.031525 c +52.071083 102.354004 51.161480 99.908997 50.034843 96.876465 c +47.781475 90.811142 44.658810 82.392624 41.203846 73.057480 c +34.294983 54.390076 26.053322 32.046478 20.777569 17.526382 c +27.845484 14.958313 l +33.112766 29.455093 41.347427 51.779816 48.256329 70.447327 c +51.710243 79.779648 54.831825 88.195236 57.084080 94.257553 c +58.210258 97.288849 59.118813 99.731049 59.742771 101.404984 c +60.054882 102.242294 60.295158 102.885712 60.455673 103.314026 c +60.536228 103.528992 60.595272 103.685959 60.632847 103.785156 c +60.652458 103.836929 60.662716 103.863693 60.666313 103.872971 c +60.671341 103.885925 60.661373 103.859802 60.646000 103.822601 c +60.640533 103.809448 60.625214 103.773361 60.615692 103.751434 c +60.597614 103.710892 60.531147 103.571854 60.480659 103.475113 c +60.150402 102.966995 55.604095 101.846634 53.416664 105.423340 c +53.449085 105.781082 53.536671 106.218628 53.559685 106.302261 c +60.786282 104.222198 l +h +20.777622 17.526535 m +19.123594 12.974869 21.347857 9.066910 24.252548 7.152466 c +26.852655 5.438782 31.562777 4.533432 34.583130 8.179138 c +28.792278 12.976685 l +28.944626 13.160568 29.138533 13.175171 29.096033 13.174240 c +28.995413 13.172043 28.716915 13.216492 28.390881 13.431366 c +28.077776 13.637741 27.886385 13.893036 27.802916 14.105087 c +27.740448 14.263763 27.684525 14.515381 27.845428 14.958176 c +20.777622 17.526535 l +h +34.583050 8.179047 m +70.276260 51.259323 105.658882 94.870407 137.171616 133.482788 c +131.345612 138.237579 l +99.814445 99.602615 64.462067 56.028702 28.792362 12.976776 c +34.583050 8.179047 l +h +137.171692 133.482880 m +139.813660 136.720261 140.928268 140.649261 139.688675 144.177887 c +138.396896 147.855072 134.944427 149.949066 130.978302 149.958282 c +130.960815 142.438293 l +131.647614 142.436707 132.038910 142.268188 132.232162 142.142303 c +132.415222 142.023071 132.526093 141.877991 132.593735 141.685471 c +132.722305 141.319458 132.853668 140.085495 131.345535 138.237488 c +137.171692 133.482880 l +h +130.978333 149.958282 m +123.319649 149.976151 110.381828 149.985077 99.363159 149.970398 c +93.854500 149.963043 88.820534 149.949799 85.164253 149.928741 c +83.337914 149.918243 81.845848 149.905746 80.807587 149.890915 c +80.292084 149.883545 79.869400 149.875366 79.568008 149.865906 c +79.424759 149.861420 79.269722 149.855499 79.134247 149.846558 c +79.081985 149.843109 78.947746 149.834045 78.793877 149.812927 c +78.748192 149.806656 78.519775 149.776611 78.242943 149.696655 c +78.151802 149.670319 77.732826 149.552322 77.269096 149.258408 c +77.008621 149.077545 76.415733 148.516479 76.117638 148.114105 c +75.779449 147.451843 75.526001 145.882538 75.668030 145.039062 c +82.893646 147.122528 l +83.035507 146.279633 82.782158 144.710938 82.444359 144.049255 c +82.146645 143.647491 81.554413 143.087036 81.294876 142.906754 c +80.833023 142.614014 80.417023 142.497208 80.329964 142.472061 c +80.061295 142.394440 79.845428 142.366760 79.816643 142.362793 c +79.744957 142.352966 79.691727 142.348022 79.672020 142.346252 c +79.647842 142.344101 79.632370 142.343079 79.629318 142.342865 c +79.623993 142.342514 79.635231 142.343292 79.669975 142.344788 c +79.702553 142.346176 79.746620 142.347809 79.803734 142.349609 c +80.038528 142.356964 80.408836 142.364441 80.914993 142.371674 c +81.920067 142.386047 83.386047 142.398407 85.207527 142.408875 c +88.846886 142.429810 93.867622 142.443054 99.373184 142.450394 c +110.382942 142.465088 123.311569 142.456146 130.960785 142.438293 c +130.978333 149.958282 l +h +82.893646 147.122528 m +83.006897 146.584534 83.032082 145.826370 83.012100 145.618286 c +82.997322 145.512054 82.967239 145.342422 82.954086 145.279816 c +82.929825 145.169250 82.905975 145.085785 82.899849 145.064331 c +82.890221 145.030609 82.882523 145.005676 82.879196 144.994995 c +82.875359 144.982666 82.872734 144.974579 82.872002 144.972321 c +82.871216 144.969894 82.878197 144.991119 82.898605 145.049866 c +82.937004 145.160400 82.999245 145.335785 83.087128 145.580521 c +83.261513 146.066177 83.522041 146.783676 83.860504 147.710892 c +84.536697 149.563309 85.515999 152.230911 86.721222 155.506134 c +89.131294 162.055588 92.440521 171.023193 96.025543 180.732605 c +103.194412 200.148285 111.466515 222.531342 115.847252 234.450256 c +108.788910 237.044525 l +104.410728 225.132568 96.142120 202.758942 88.971054 183.337341 c +85.386108 173.628113 82.075554 164.656921 79.663872 158.103104 c +78.458214 154.826691 77.476135 152.151535 76.796440 150.289520 c +76.456955 149.359528 76.190903 148.626892 76.009583 148.121918 c +75.919609 147.871368 75.846710 147.666321 75.795128 147.517853 c +75.770538 147.447083 75.743820 147.369186 75.720924 147.298798 c +75.711311 147.269257 75.690361 147.204407 75.668648 147.128326 c +75.659912 147.097717 75.634186 147.007111 75.608788 146.891342 c +75.595062 146.826141 75.564598 146.654419 75.549614 146.546570 c +75.529434 146.336914 75.554611 145.577637 75.668030 145.039062 c +82.893646 147.122528 l +h +f +n +Q + +endstream +endobj + +12 0 obj + 10585 +endobj + +13 0 obj + << /Type /XObject + /Length 14 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 15.562500 15.437378 cm +0.000000 0.000000 0.000000 scn +0.000000 0.000031 m +0.000000 451.298706 l +303.743164 147.476624 l +311.959686 139.257233 325.283234 139.257233 333.499786 147.476624 c +341.716339 155.695953 341.716339 169.021393 333.499786 177.240784 c +29.756639 481.062866 l +480.937866 481.062866 l +480.937866 0.000031 l +0.000000 0.000031 l +h +f +n +Q + +endstream +endobj + +14 0 obj + 420 +endobj + +15 0 obj + << /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + /Resources << /XObject << /X1 11 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 13 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> + /Subtype /Form + /Length 16 0 R + /Group << /Type /Group + /S /Transparency + >> + /Type /XObject + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q +q +1.000000 0.000000 -0.000000 1.000000 177.791992 177.791992 cm +0.000000 0.000000 0.000000 scn +156.416016 0.000000 m +0.000000 156.416016 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 177.791992 157.981934 cm +0.000000 0.000000 0.000000 scn +150.035095 13.429123 m +153.559174 9.905029 159.272858 9.905029 162.796951 13.429123 c +166.321045 16.953217 166.321045 22.666901 162.796951 26.190979 c +150.035095 13.429123 l +h +6.380932 182.607010 m +2.856840 186.131104 -2.856840 186.131104 -6.380932 182.607010 c +-9.905023 179.082916 -9.905023 173.369232 -6.380932 169.845139 c +6.380932 182.607010 l +h +162.796951 26.190979 m +6.380932 182.607010 l +-6.380932 169.845139 l +150.035095 13.429123 l +162.796951 26.190979 l +h +f +n +Q + +endstream +endobj + +16 0 obj + 761 +endobj + +17 0 obj + << /Length 18 0 R + /FunctionType 4 + /Domain [ 0.000000 1.000000 ] + /Range [ 0.000000 1.000000 ] + >> +stream +{ 0 gt { 0 } { 1 } ifelse } +endstream +endobj + +18 0 obj + 27 +endobj + +19 0 obj + << /XObject << /X2 1 0 R + /X1 7 0 R + >> + /ExtGState << /E2 << /SMask << /Type /Mask + /G 9 0 R + /S /Alpha + >> + /Type /ExtGState + >> + /E1 << /SMask << /Type /Mask + /G 15 0 R + /S /Alpha + /TR 17 0 R + >> + /Type /ExtGState + >> + >> + >> +endobj + +20 0 obj + << /Length 21 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q +q +/E2 gs +/X2 Do +Q +q +1.000000 0.000000 -0.000000 1.000000 177.791992 157.981934 cm +1.000000 1.000000 1.000000 scn +150.035095 13.429123 m +153.559174 9.905029 159.272858 9.905029 162.796951 13.429123 c +166.321045 16.953217 166.321045 22.666901 162.796951 26.190979 c +150.035095 13.429123 l +h +6.380932 182.607010 m +2.856840 186.131104 -2.856840 186.131104 -6.380932 182.607010 c +-9.905023 179.082916 -9.905023 173.369232 -6.380932 169.845139 c +6.380932 182.607010 l +h +162.796951 26.190979 m +6.380932 182.607010 l +-6.380932 169.845139 l +150.035095 13.429123 l +162.796951 26.190979 l +h +f +n +Q + +endstream +endobj + +21 0 obj + 632 +endobj + +22 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 512.000000 512.000000 ] + /Resources 19 0 R + /Contents 20 0 R + /Parent 23 0 R + >> +endobj + +23 0 obj + << /Kids [ 22 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +24 0 obj + << /Pages 23 0 R + /Type /Catalog + >> +endobj + +xref +0 25 +0000000000 65535 f +0000000010 00000 n +0000010069 00000 n +0000010092 00000 n +0000084945 00000 n +0000084969 00000 n +0000219409 00000 n +0000219434 00000 n +0000219813 00000 n +0000219835 00000 n +0000220506 00000 n +0000220529 00000 n +0000231366 00000 n +0000231391 00000 n +0000232063 00000 n +0000232086 00000 n +0000233473 00000 n +0000233496 00000 n +0000233673 00000 n +0000233695 00000 n +0000234343 00000 n +0000235033 00000 n +0000235056 00000 n +0000235235 00000 n +0000235311 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 24 0 R + /Size 25 +>> +startxref +235372 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/Contents.json new file mode 100644 index 0000000000..bd16466e97 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "SubtitleArrow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/SubtitleArrow.pdf b/submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/SubtitleArrow.pdf new file mode 100644 index 0000000000..cecbd934c3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/SubtitleArrow.pdf @@ -0,0 +1,92 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +-1.000000 0.000000 -0.000000 -1.000000 4.664993 10.125000 cm +0.000000 0.478431 1.000000 scn +4.470226 8.989735 m +4.729925 9.249434 4.729925 9.670488 4.470226 9.930187 c +4.210527 10.189886 3.789473 10.189886 3.529774 9.930187 c +4.470226 8.989735 l +h +0.000000 5.459961 m +-0.470226 5.930187 l +-0.729925 5.670488 -0.729925 5.249434 -0.470226 4.989735 c +0.000000 5.459961 l +h +3.529774 0.989735 m +3.789473 0.730036 4.210527 0.730036 4.470226 0.989735 c +4.729925 1.249434 4.729925 1.670488 4.470226 1.930187 c +3.529774 0.989735 l +h +3.529774 9.930187 m +-0.470226 5.930187 l +0.470226 4.989735 l +4.470226 8.989735 l +3.529774 9.930187 l +h +-0.470226 4.989735 m +3.529774 0.989735 l +4.470226 1.930187 l +0.470226 5.930187 l +-0.470226 4.989735 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 767 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 5.329987 9.330078 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000857 00000 n +0000000879 00000 n +0000001050 00000 n +0000001124 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1183 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json new file mode 100644 index 0000000000..37accc5389 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "check.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf new file mode 100644 index 0000000000..c4dfcbde1b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf @@ -0,0 +1,150 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 11.669922 11.450928 cm +0.000000 0.000000 0.000000 scn +16.557842 15.662290 m +17.172628 15.260314 17.345146 14.436065 16.943171 13.821279 c +8.443170 0.821279 l +8.223975 0.486040 7.865371 0.267438 7.466962 0.226191 c +7.068553 0.184944 6.672771 0.325444 6.389548 0.608668 c +0.389548 6.608668 l +-0.129849 7.128065 -0.129849 7.970175 0.389548 8.489573 c +0.908945 9.008969 1.751055 9.008969 2.270452 8.489573 c +7.112782 3.647242 l +14.716830 15.276961 l +15.118806 15.891748 15.943055 16.064266 16.557842 15.662290 c +h +f* +n +Q + +endstream +endobj + +2 0 obj + 584 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 20.000000 m +0.000000 31.045694 8.954306 40.000000 20.000000 40.000000 c +20.000000 40.000000 l +31.045694 40.000000 40.000000 31.045694 40.000000 20.000000 c +40.000000 20.000000 l +40.000000 8.954306 31.045694 0.000000 20.000000 0.000000 c +20.000000 0.000000 l +8.954306 0.000000 0.000000 8.954306 0.000000 20.000000 c +0.000000 20.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 472 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 40.000000 40.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000000842 00000 n +0000000864 00000 n +0000001584 00000 n +0000001606 00000 n +0000001904 00000 n +0000002006 00000 n +0000002027 00000 n +0000002200 00000 n +0000002274 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +2334 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 85a01c0c16..be58331f0a 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1828,8 +1828,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, parentNavigationController: parentNavigationController, sendSticker: sendSticker) } - public func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any) -> Void) -> ViewController { - return storyMediaPickerController(context: context, completion: completion) + public func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?) -> Void, dismissed: @escaping () -> Void) -> ViewController { + return storyMediaPickerController(context: context, completion: completion, dismissed: dismissed) } public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 46737ce2ab..c5e1f9464f 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -26,6 +26,7 @@ import AvatarNode import LocalMediaResources import ShareWithPeersScreen import ImageCompression +import TextFormat private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private var presentationData: PresentationData @@ -196,16 +197,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon guard let self else { return } - var transitionIn: StoryCameraTransitionIn? - if let cameraItemView = self.rootTabController?.viewForCameraItem() { - transitionIn = StoryCameraTransitionIn( - sourceView: cameraItemView, - sourceRect: cameraItemView.bounds, - sourceCornerRadius: cameraItemView.bounds.height / 2.0 - ) - } - self.openStoryCamera( - transitionIn: transitionIn, + let coordinator = self.openStoryCamera( + transitionIn: nil, transitionOut: { [weak self] finished in guard let self else { return nil @@ -218,18 +211,11 @@ public final class TelegramRootController: NavigationController, TelegramRootCon destinationCornerRadius: transitionView.bounds.height / 2.0 ) } - } else { - if let cameraItemView = self.rootTabController?.viewForCameraItem() { - return StoryCameraTransitionOut( - destinationView: cameraItemView, - destinationRect: cameraItemView.bounds, - destinationCornerRadius: cameraItemView.bounds.height / 2.0 - ) - } } return nil } ) + coordinator?.animateIn() } ) @@ -288,9 +274,10 @@ public final class TelegramRootController: NavigationController, TelegramRootCon presentedLegacyShortcutCamera(context: self.context, saveCapturedMedia: false, saveEditedPhotos: false, mediaGrouping: true, parentController: controller) } - public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) { + @discardableResult + public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? { guard let controller = self.viewControllers.last as? ViewController else { - return + return nil } controller.view.endEditing(true) @@ -299,7 +286,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon var presentImpl: ((ViewController) -> Void)? var returnToCameraImpl: (() -> Void)? var dismissCameraImpl: (() -> Void)? - var hideCameraImpl: (() -> Void)? var showDraftTooltipImpl: (() -> Void)? let cameraController = CameraScreen( context: context, @@ -326,7 +312,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return nil } }, - completion: { result in + completion: { result, resultTransition in let subject: Signal = result |> map { value -> MediaEditorScreen.Subject? in switch value { @@ -342,17 +328,37 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return .draft(draft) } } + + var transitionIn: MediaEditorScreen.TransitionIn? + if let resultTransition, let sourceView = resultTransition.sourceView { + transitionIn = .gallery( + MediaEditorScreen.TransitionIn.GalleryTransitionIn( + sourceView: sourceView, + sourceRect: resultTransition.sourceRect, + sourceImage: resultTransition.sourceImage + ) + ) + } else { + transitionIn = .camera + } + let controller = MediaEditorScreen( context: context, subject: subject, - transitionIn: nil, + transitionIn: transitionIn, transitionOut: { finished in - if finished, let transitionOut = transitionOut(true), let destinationView = transitionOut.destinationView { + if finished, let transitionOut = transitionOut(finished), let destinationView = transitionOut.destinationView { return MediaEditorScreen.TransitionOut( destinationView: destinationView, destinationRect: transitionOut.destinationRect, destinationCornerRadius: transitionOut.destinationCornerRadius ) + } else if !finished, let resultTransition, let (destinationView, destinationRect) = resultTransition.transitionOut() { + return MediaEditorScreen.TransitionOut( + destinationView: destinationView, + destinationRect: destinationRect, + destinationCornerRadius: 0.0 + ) } else { return nil } @@ -367,10 +373,71 @@ public final class TelegramRootController: NavigationController, TelegramRootCon switch mediaResult { case let .image(image, dimensions, caption): if let imageData = compressImageToJPEG(image, quality: 0.6) { - storyListContext.upload(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: privacy) - Queue.mainQueue().after(0.2, { [weak chatListController] in - chatListController?.animateStoryUploadRipple() - }) + switch privacy { + case let .story(storyPrivacy, _): + storyListContext.upload(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: storyPrivacy) + Queue.mainQueue().after(0.2, { [weak chatListController] in + chatListController?.animateStoryUploadRipple() + }) + case let .message(peerIds, timeout): + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let tempFilePath = NSTemporaryDirectory() + "\(randomId).jpg" + let _ = try? imageData.write(to: URL(fileURLWithPath: tempFilePath)) + + var representations: [TelegramMediaImageRepresentation] = [] + let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) + + var attributes: [MessageAttribute] = [] + let imageFlags: TelegramMediaImageFlags = [] +// var stickerFiles: [TelegramMediaFile] = [] +// if !stickers.isEmpty { +// for fileReference in stickers { +// stickerFiles.append(fileReference.media) +// } +// } +// if !stickerFiles.isEmpty { +// attributes.append(EmbeddedMediaStickersMessageAttribute(files: stickerFiles)) +// imageFlags.insert(.hasStickers) +// } + + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: imageFlags) + if let timeout, timeout > 0 && timeout <= 60 { + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: timeout, countdownBeginTime: nil)) + } + + let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString())) + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + var bubbleUpEmojiOrStickersetsById: [Int64: ItemCollectionId] = [:] + text.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: text.length), using: { value, _, _ in + if let value = value as? ChatTextInputTextCustomEmojiAttribute { + if let file = value.file { + if let packId = value.interactivelySelectedFromPackId { + bubbleUpEmojiOrStickersetsById[file.fileId.id] = packId + } + } + } + }) + var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] + for entity in entities { + if case let .CustomEmoji(_, fileId) = entity.type { + if let packId = bubbleUpEmojiOrStickersetsById[fileId] { + if !bubbleUpEmojiOrStickersets.contains(packId) { + bubbleUpEmojiOrStickersets.append(packId) + } + } + } + } + + let _ = enqueueMessagesToMultiplePeers( + account: self.context.account, + peerIds: peerIds, threadIds: [:], + messages: [.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)]).start() + } } case let .video(content, _, values, duration, dimensions, caption): let adjustments: VideoMediaResourceAdjustments @@ -388,10 +455,14 @@ public final class TelegramRootController: NavigationController, TelegramRootCon case let .asset(localIdentifier): resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) } - storyListContext.upload(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: privacy) - Queue.mainQueue().after(0.2, { [weak chatListController] in - chatListController?.animateStoryUploadRipple() - }) + if case let .story(storyPrivacy, _) = privacy { + storyListContext.upload(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: storyPrivacy) + Queue.mainQueue().after(0.2, { [weak chatListController] in + chatListController?.animateStoryUploadRipple() + }) + } else { + + } } } } @@ -400,16 +471,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon commit() } ) - controller.sourceHint = .camera controller.cancelled = { showDraftTooltip in if showDraftTooltip { showDraftTooltipImpl?() } returnToCameraImpl?() } - controller.onReady = { - hideCameraImpl?() - } presentImpl?(controller) } ) @@ -429,16 +496,28 @@ public final class TelegramRootController: NavigationController, TelegramRootCon cameraController.returnFromEditor() } } - hideCameraImpl = { [weak cameraController] in - if let cameraController { - cameraController.commitTransitionToEditor() - } - } showDraftTooltipImpl = { [weak cameraController] in if let cameraController { cameraController.presentDraftTooltip() } } + return StoryCameraTransitionInCoordinator( + animateIn: { [weak cameraController] in + if let cameraController { + cameraController.updateTransitionProgress(0.0, transition: .immediate) + cameraController.completeWithTransitionProgress(1.0, velocity: 0.0, dismissing: false) + } + }, + updateTransitionProgress: { [weak cameraController] transitionFraction in + if let cameraController { + cameraController.updateTransitionProgress(transitionFraction, transition: .immediate) + } + }, + completeWithTransitionProgressAndVelocity: { [weak cameraController] transitionFraction, velocity in + if let cameraController { + cameraController.completeWithTransitionProgress(transitionFraction, velocity: velocity, dismissing: false) + } + }) } public func openSettings() { From 2d738fbfacf3f52f124619773a53dfb683de5d9b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 27 May 2023 02:29:23 +0400 Subject: [PATCH 2/6] Camera and editor improvements --- submodules/Camera/BUILD | 1 + submodules/Camera/Sources/Camera.swift | 23 +- submodules/Camera/Sources/CameraDevice.swift | 9 + submodules/Camera/Sources/CameraMetrics.swift | 44 + submodules/Camera/Sources/CameraOutput.swift | 1 - .../Camera/Sources/CameraPreviewView.swift | 2 +- .../DrawingUI/Sources/ColorPickerScreen.swift | 12 +- .../Sources/DrawingEntitiesView.swift | 6 +- .../Sources/DrawingMediaEntity.swift | 2 +- .../DrawingUI/Sources/DrawingScreen.swift | 1147 +++++++++-------- .../DrawingUI/Sources/EyedropperView.swift | 2 +- .../CameraScreen/Sources/CameraScreen.swift | 46 +- .../MediaEditor/Sources/MediaEditor.swift | 4 + .../Sources/MediaEditorComposer.swift | 57 +- .../Sources/MediaEditorPreviewView.swift | 2 +- .../Sources/MediaEditorVideoExport.swift | 21 +- .../Sources/VideoTextureSource.swift | 39 +- .../Sources/MediaEditorScreen.swift | 190 +-- .../Sources/VideoScrubberComponent.swift | 47 +- .../Sources/TelegramRootController.swift | 8 +- 20 files changed, 960 insertions(+), 703 deletions(-) diff --git a/submodules/Camera/BUILD b/submodules/Camera/BUILD index eb09de4aa3..07377c40c1 100644 --- a/submodules/Camera/BUILD +++ b/submodules/Camera/BUILD @@ -57,6 +57,7 @@ swift_library( "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", "//submodules/ImageBlur:ImageBlur", + "//submodules/TelegramCore:TelegramCore", ], visibility = [ "//visibility:public", diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 0a469b2820..e57823a79a 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -40,14 +40,14 @@ private final class CameraContext { } } + private let previewSnapshotContext = CIContext() private var lastSnapshotTimestamp: Double = CACurrentMediaTime() private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer) { Queue.concurrentDefaultQueue().async { - let ciContext = CIContext() var ciImage = CIImage(cvImageBuffer: pixelBuffer) - ciImage = ciImage.transformed(by: CGAffineTransform(scaleX: 0.33, y: 0.33)) - ciImage = ciImage.clampedToExtent() - if let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) { + let size = ciImage.extent.size + ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: 40.0).cropped(to: CGRect(origin: .zero, size: size)) + if let cgImage = self.previewSnapshotContext.createCGImage(ciImage, from: ciImage.extent) { let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) CameraSimplePreviewView.saveLastStateImage(uiImage) } @@ -67,7 +67,7 @@ private final class CameraContext { self.input.configure(for: self.session, device: self.device, audio: configuration.audio) self.output.configure(for: self.session, configuration: configuration) - self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: 60) + self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: self.preferredMaxFrameRate) self.output.configureVideoStabilization() } @@ -115,6 +115,15 @@ private final class CameraContext { } } + private var preferredMaxFrameRate: Double { + switch DeviceModel.current { + case .iPhone14ProMax, .iPhone13ProMax: + return 60.0 + default: + return 30.0 + } + } + func startCapture() { guard !self.session.isRunning else { return @@ -160,7 +169,7 @@ private final class CameraContext { self.changingPosition = true self.device.configure(for: self.session, position: targetPosition) self.input.configure(for: self.session, device: self.device, audio: self.initialConfiguration.audio) - self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: 60) + self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: self.preferredMaxFrameRate) self.output.configureVideoStabilization() self.queue.after(0.5) { self.changingPosition = false @@ -173,7 +182,7 @@ private final class CameraContext { self.input.invalidate(for: self.session) self.device.configure(for: self.session, position: position) self.input.configure(for: self.session, device: self.device, audio: self.initialConfiguration.audio) - self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: 60) + self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: self.preferredMaxFrameRate) self.output.configureVideoStabilization() } } diff --git a/submodules/Camera/Sources/CameraDevice.swift b/submodules/Camera/Sources/CameraDevice.swift index 182017f167..816ce061d9 100644 --- a/submodules/Camera/Sources/CameraDevice.swift +++ b/submodules/Camera/Sources/CameraDevice.swift @@ -1,6 +1,7 @@ import Foundation import AVFoundation import SwiftSignalKit +import TelegramCore private let defaultFPS: Double = 30.0 @@ -68,6 +69,14 @@ final class CameraDevice { if let bestFormat = candidates.last { device.activeFormat = bestFormat + + Logger.shared.log("Camera", "Available formats:") + for format in device.formats { + Logger.shared.log("Camera", format.description) + } + + Logger.shared.log("Camera", "Selected format:") + Logger.shared.log("Camera", bestFormat.description) } if let targetFPS = device.actualFPS(maxFramerate) { diff --git a/submodules/Camera/Sources/CameraMetrics.swift b/submodules/Camera/Sources/CameraMetrics.swift index cc7c0e20bf..9c296c8528 100644 --- a/submodules/Camera/Sources/CameraMetrics.swift +++ b/submodules/Camera/Sources/CameraMetrics.swift @@ -23,6 +23,8 @@ public extension Camera { self = .iPhone14ProMax case .unknown: self = .unknown + default: + self = .unknown } } @@ -70,6 +72,16 @@ enum DeviceModel: CaseIterable { case iPodTouch6 case iPodTouch7 + case iPhone12 + case iPhone12Mini + case iPhone12Pro + case iPhone12ProMax + + case iPhone13 + case iPhone13Mini + case iPhone13Pro + case iPhone13ProMax + case iPhone14 case iPhone14Plus case iPhone14Pro @@ -93,6 +105,22 @@ enum DeviceModel: CaseIterable { return "iPod7,1" case .iPodTouch7: return "iPod9,1" + case .iPhone12: + return "iPhone13,2" + case .iPhone12Mini: + return "iPhone13,1" + case .iPhone12Pro: + return "iPhone13,3" + case .iPhone12ProMax: + return "iPhone13,4" + case .iPhone13: + return "iPhone14,5" + case .iPhone13Mini: + return "iPhone14,4" + case .iPhone13Pro: + return "iPhone14,2" + case .iPhone13ProMax: + return "iPhone14,3" case .iPhone14: return "iPhone14,7" case .iPhone14Plus: @@ -122,6 +150,22 @@ enum DeviceModel: CaseIterable { return "iPod touch 6G" case .iPodTouch7: return "iPod touch 7G" + case .iPhone12: + return "iPhone 12" + case .iPhone12Mini: + return "iPhone 12 mini" + case .iPhone12Pro: + return "iPhone 12 Pro" + case .iPhone12ProMax: + return "iPhone 12 Pro Max" + case .iPhone13: + return "iPhone 13" + case .iPhone13Mini: + return "iPhone 13 mini" + case .iPhone13Pro: + return "iPhone 13 Pro" + case .iPhone13ProMax: + return "iPhone 13 Pro Max" case .iPhone14: return "iPhone 14" case .iPhone14Plus: diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index 5735944198..de12b7f7d6 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -65,7 +65,6 @@ final class CameraOutput: NSObject { super.init() self.videoOutput.alwaysDiscardsLateVideoFrames = false - //self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32BGRA] as [String : Any] self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] as [String : Any] self.faceLandmarksOutput.outputFaceObservations = { [weak self] observations in diff --git a/submodules/Camera/Sources/CameraPreviewView.swift b/submodules/Camera/Sources/CameraPreviewView.swift index 3ed8925847..3361f257be 100644 --- a/submodules/Camera/Sources/CameraPreviewView.swift +++ b/submodules/Camera/Sources/CameraPreviewView.swift @@ -21,7 +21,7 @@ public class CameraSimplePreviewView: UIView { static func saveLastStateImage(_ image: UIImage) { let imagePath = NSTemporaryDirectory() + "cameraImage.jpg" - if let blurredImage = blurredImage(image, radius: 60.0), let data = blurredImage.jpegData(compressionQuality: 0.85) { + if let data = image.jpegData(compressionQuality: 0.6) { try? data.write(to: URL(fileURLWithPath: imagePath)) } } diff --git a/submodules/DrawingUI/Sources/ColorPickerScreen.swift b/submodules/DrawingUI/Sources/ColorPickerScreen.swift index fe314eec12..5745ca248b 100644 --- a/submodules/DrawingUI/Sources/ColorPickerScreen.swift +++ b/submodules/DrawingUI/Sources/ColorPickerScreen.swift @@ -937,7 +937,7 @@ final class ColorSpectrumComponent: Component { } } -final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate { +public final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate { private var validSize: CGSize? private var selectedColor: DrawingColor? @@ -950,7 +950,7 @@ final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate { private var circleMaskView = UIView() private let maskCircle = SimpleShapeLayer() - var selected: (DrawingColor) -> Void = { _ in } + public var selected: (DrawingColor) -> Void = { _ in } private var bitmapData: UnsafeMutableRawPointer? @@ -1048,7 +1048,7 @@ final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate { private var animatingIn = false private var scheduledAnimateOut: (() -> Void)? - func animateIn() { + public func animateIn() { self.animatingIn = true Queue.mainQueue().after(0.15) { @@ -1107,7 +1107,7 @@ final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate { }) } - func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize { + public func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize { let previousSize = self.validSize let imageSize = size @@ -2413,10 +2413,10 @@ private final class ColorPickerSheetComponent: CombinedComponent { } } -class ColorPickerScreen: ViewControllerComponentContainer { +public final class ColorPickerScreen: ViewControllerComponentContainer { private var dismissed: () -> Void - init(context: AccountContext, initialColor: DrawingColor, updated: @escaping (DrawingColor) -> Void, openEyedropper: @escaping () -> Void, dismissed: @escaping () -> Void = {}) { + public init(context: AccountContext, initialColor: DrawingColor, updated: @escaping (DrawingColor) -> Void, openEyedropper: @escaping () -> Void, dismissed: @escaping () -> Void = {}) { self.dismissed = dismissed super.init(context: context, component: ColorPickerSheetComponent(context: context, initialColor: initialColor, updated: updated, openEyedropper: openEyedropper, dismissed: dismissed), navigationBarAppearance: .none) diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 77ac1d044a..3b7e0f164a 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -54,7 +54,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { public weak var selectionContainerView: DrawingSelectionContainerView? private var tapGestureRecognizer: UITapGestureRecognizer! - private(set) var selectedEntityView: DrawingEntityView? + public private(set) var selectedEntityView: DrawingEntityView? public var getEntityCenterPosition: () -> CGPoint = { return .zero } public var getEntityInitialRotation: () -> CGFloat = { return 0.0 } @@ -593,7 +593,7 @@ protocol DrawingEntityMediaView: DrawingEntityView { public class DrawingEntityView: UIView { let context: AccountContext - let entity: DrawingEntity + public let entity: DrawingEntity var isTracking = false public weak var selectionView: DrawingEntitySelectionView? @@ -645,7 +645,7 @@ public class DrawingEntityView: UIView { } - func update(animated: Bool = false) { + public func update(animated: Bool = false) { self.updateSelectionView() } diff --git a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift index b78e163148..bc73d07eb8 100644 --- a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift @@ -89,7 +89,7 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia } public var updated: (() -> Void)? - override func update(animated: Bool) { + public override func update(animated: Bool) { self.center = self.mediaEntity.position let size = self.mediaEntity.baseSize diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 712a2b5077..8aaf79a8a2 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import CoreServices +import AsyncDisplayKit import Display import ComponentFlow import LegacyComponents @@ -511,6 +512,7 @@ private final class DrawingScreenComponent: CombinedComponent { let insertText: ActionSlot let updateEntityView: ActionSlot<(UUID, Bool)> let endEditingTextEntityView: ActionSlot<(UUID, Bool)> + let entityViewForEntity: (DrawingEntity) -> DrawingEntityView? let apply: ActionSlot let dismiss: ActionSlot @@ -544,6 +546,7 @@ private final class DrawingScreenComponent: CombinedComponent { insertText: ActionSlot, updateEntityView: ActionSlot<(UUID, Bool)>, endEditingTextEntityView: ActionSlot<(UUID, Bool)>, + entityViewForEntity: @escaping (DrawingEntity) -> DrawingEntityView?, apply: ActionSlot, dismiss: ActionSlot, presentColorPicker: @escaping (DrawingColor) -> Void, @@ -575,6 +578,7 @@ private final class DrawingScreenComponent: CombinedComponent { self.insertText = insertText self.updateEntityView = updateEntityView self.endEditingTextEntityView = endEditingTextEntityView + self.entityViewForEntity = entityViewForEntity self.apply = apply self.dismiss = dismiss self.presentColorPicker = presentColorPicker @@ -652,6 +656,7 @@ private final class DrawingScreenComponent: CombinedComponent { private let insertText: ActionSlot private let updateEntityView: ActionSlot<(UUID, Bool)> private let endEditingTextEntityView: ActionSlot<(UUID, Bool)> + private let entityViewForEntity: (DrawingEntity) -> DrawingEntityView? private let present: (ViewController) -> Void var currentMode: Mode @@ -678,6 +683,7 @@ private final class DrawingScreenComponent: CombinedComponent { insertText: ActionSlot, updateEntityView: ActionSlot<(UUID, Bool)>, endEditingTextEntityView: ActionSlot<(UUID, Bool)>, + entityViewForEntity: @escaping (DrawingEntity) -> DrawingEntityView?, present: @escaping (ViewController) -> Void) { self.context = context @@ -692,6 +698,7 @@ private final class DrawingScreenComponent: CombinedComponent { self.insertText = insertText self.updateEntityView = updateEntityView self.endEditingTextEntityView = endEditingTextEntityView + self.entityViewForEntity = entityViewForEntity self.present = present self.currentMode = .drawing @@ -1015,7 +1022,23 @@ private final class DrawingScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, existingStickerPickerInputData: self.existingStickerPickerInputData, updateToolState: self.updateToolState, insertEntity: self.insertEntity, deselectEntity: self.deselectEntity, updateEntitiesPlayback: self.updateEntitiesPlayback, dismissEyedropper: self.dismissEyedropper, toggleWithEraser: self.toggleWithEraser, toggleWithPreviousTool: self.toggleWithPreviousTool, insertSticker: self.insertSticker, insertText: self.insertText, updateEntityView: self.updateEntityView, endEditingTextEntityView: self.endEditingTextEntityView, present: self.present) + return State( + context: self.context, + existingStickerPickerInputData: self.existingStickerPickerInputData, + updateToolState: self.updateToolState, + insertEntity: self.insertEntity, + deselectEntity: self.deselectEntity, + updateEntitiesPlayback: self.updateEntitiesPlayback, + dismissEyedropper: self.dismissEyedropper, + toggleWithEraser: self.toggleWithEraser, + toggleWithPreviousTool: self.toggleWithPreviousTool, + insertSticker: self.insertSticker, + insertText: self.insertText, + updateEntityView: self.updateEntityView, + endEditingTextEntityView: self.endEditingTextEntityView, + entityViewForEntity: self.entityViewForEntity, + present: self.present + ) } static var body: Body { @@ -1632,9 +1655,9 @@ private final class DrawingScreenComponent: CombinedComponent { var sizeSliderVisible = false var isEditingText = false var sizeValue: CGFloat? - if let textEntity = state.selectedEntity as? DrawingTextEntity, !"".isEmpty {//} let entityView = textEntity.currentEntityView as? DrawingTextEntityView { + if let textEntity = state.selectedEntity as? DrawingTextEntity, let entityView = component.entityViewForEntity(textEntity) as? DrawingTextEntityView { sizeSliderVisible = true - isEditingText = false//entityView.isEditing + isEditingText = entityView.isEditing sizeValue = textEntity.fontSize } else { if state.selectedEntity == nil || !(state.selectedEntity is DrawingStickerEntity) { @@ -2041,6 +2064,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U fileprivate final class Node: ViewControllerTracingNode { private weak var controller: DrawingScreen? private let context: AccountContext + private var interaction: DrawingToolsInteraction? private let updateState: ActionSlot private let updateColor: ActionSlot private let performAction: ActionSlot @@ -2064,10 +2088,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U private let dismiss: ActionSlot fileprivate let componentHost: ComponentView - - private let textEditAccessoryView: UIInputView - private let textEditAccessoryHost: ComponentView - + private var presentationData: PresentationData private let hapticFeedback = HapticFeedback() private var validLayout: (ContainerViewLayout, UIInterfaceOrientation?)? @@ -2098,68 +2119,67 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U } } self._drawingView?.requestedColorPicker = { [weak self] in - if let strongSelf = self { - if let _ = strongSelf.colorPickerScreen { - strongSelf.dismissColorPicker() + if let self, let interaction = self.interaction { + if let _ = interaction.colorPickerScreen { + interaction.dismissColorPicker() } else { - strongSelf.requestPresentColorPicker.invoke(Void()) + self.requestPresentColorPicker.invoke(Void()) } } } self._drawingView?.requestedEraserToggle = { [weak self] in - if let strongSelf = self { - strongSelf.toggleWithEraser.invoke(Void()) + if let self { + self.toggleWithEraser.invoke(Void()) } } self._drawingView?.requestedToolsToggle = { [weak self] in - if let strongSelf = self { - strongSelf.toggleWithPreviousTool.invoke(Void()) + if let self { + self.toggleWithPreviousTool.invoke(Void()) } } self.performAction.connect { [weak self] action in - if let strongSelf = self { - if action == .clear { - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) + if let self { + if case .clear = action { + let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) actionSheet.setItemGroups([ ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in + ActionSheetButtonItem(title: self.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in actionSheet?.dismissAnimated() self?._drawingView?.performAction(action) }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) ]) - strongSelf.controller?.present(actionSheet, in: .window(.root)) + self.controller?.present(actionSheet, in: .window(.root)) } else { - strongSelf._drawingView?.performAction(action) + self._drawingView?.performAction(action) } } } self.updateToolState.connect { [weak self] state in - if let strongSelf = self { - strongSelf._drawingView?.updateToolState(state) + if let self { + self._drawingView?.updateToolState(state) } } self.previewBrushSize.connect { [weak self] size in - if let strongSelf = self { - strongSelf._drawingView?.setBrushSizePreview(size) + if let self { + self._drawingView?.setBrushSizePreview(size) } } self.dismissEyedropper.connect { [weak self] in - if let strongSelf = self { - strongSelf.dismissCurrentEyedropper() + if let self { + self.interaction?.dismissCurrentEyedropper() } } } return self._drawingView! } - private weak var currentMenuController: ContextMenuController? var _entitiesView: DrawingEntitiesView? var entitiesView: DrawingEntitiesView { if self._entitiesView == nil, let controller = self.controller { @@ -2206,76 +2226,9 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U strongSelf.updateSelectedEntity.invoke(entity) } } - self._entitiesView?.requestedMenuForEntityView = { [weak self] entityView, isTopmost in - guard let strongSelf = self else { - return - } - if strongSelf.currentMenuController != nil { - if let entityView = entityView as? DrawingTextEntityView { - entityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) - } - return - } - var actions: [ContextMenuAction] = [] - actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Delete, accessibilityLabel: strongSelf.presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in - if let strongSelf = self, let entityView = entityView { - strongSelf.entitiesView.remove(uuid: entityView.entity.uuid, animated: true) - } - })) - if let entityView = entityView as? DrawingTextEntityView { - actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Edit, accessibilityLabel: strongSelf.presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in - if let strongSelf = self, let entityView = entityView { - entityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) - strongSelf.entitiesView.selectEntity(entityView.entity) - } - })) - } - if !isTopmost { - actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_MoveForward, accessibilityLabel: strongSelf.presentationData.strings.Paint_MoveForward), action: { [weak self, weak entityView] in - if let strongSelf = self, let entityView = entityView { - strongSelf.entitiesView.bringToFront(uuid: entityView.entity.uuid) - } - })) - } - actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Duplicate, accessibilityLabel: strongSelf.presentationData.strings.Paint_Duplicate), action: { [weak self, weak entityView] in - if let strongSelf = self, let entityView = entityView { - let newEntity = strongSelf.entitiesView.duplicate(entityView.entity) - strongSelf.entitiesView.selectEntity(newEntity) - } - })) - let entityFrame = entityView.convert(entityView.selectionBounds, to: strongSelf.view).offsetBy(dx: 0.0, dy: -6.0) - let controller = ContextMenuController(actions: actions) - strongSelf.currentMenuController = controller - strongSelf.controller?.present( - controller, - in: .window(.root), - with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in - if let strongSelf = self { - return (strongSelf, entityFrame, strongSelf, strongSelf.bounds.insetBy(dx: 0.0, dy: 160.0)) - } else { - return nil - } - }) - ) - } self.insertEntity.connect { [weak self] entity in - if let strongSelf = self, let entitiesView = strongSelf._entitiesView { - entitiesView.prepareNewEntity(entity) - entitiesView.add(entity) - entitiesView.selectEntity(entity) - - if let entityView = entitiesView.getView(for: entity.uuid) { - if let textEntityView = entityView as? DrawingTextEntityView { - textEntityView.beginEditing(accessoryView: strongSelf.textEditAccessoryView) - } else { - entityView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - entityView.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) - - if let selectionView = entityView.selectionView { - selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2) - } - } - } + if let self, let interaction = self.interaction { + interaction.insertEntity(entity) } } self.deselectEntity.connect { [weak self] in @@ -2355,10 +2308,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.componentHost = ComponentView() - - self.textEditAccessoryView = UIInputView(frame: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 44.0)), inputViewStyle: .keyboard) - self.textEditAccessoryHost = ComponentView() - + super.init() self.apply.connect { [weak self] _ in @@ -2397,198 +2347,48 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.view.disablesInteractiveKeyboardGestureRecognizer = true self.view.disablesInteractiveTransitionGestureRecognizer = true - } - - private var currentEyedropperView: EyedropperView? - func presentEyedropper(retryLaterForVideo: Bool = true, dismissed: @escaping () -> Void) { - guard let controller = self.controller else { - return - } - self.entitiesView.pause() - - if controller.isVideo && retryLaterForVideo { - controller.updateVideoPlayback(false) - Queue.mainQueue().after(0.1) { - self.presentEyedropper(retryLaterForVideo: false, dismissed: dismissed) - } - return - } - - guard let currentImage = controller.getCurrentImage() else { - self.entitiesView.play() - controller.updateVideoPlayback(true) - return - } - - let sourceImage = generateImage(controller.drawingView.imageSize, contextGenerator: { size, context in - let bounds = CGRect(origin: .zero, size: size) - if let cgImage = currentImage.cgImage { - context.draw(cgImage, in: bounds) - } - if let cgImage = controller.drawingView.drawingImage?.cgImage { - context.draw(cgImage, in: bounds) - } - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - controller.entitiesView.layer.render(in: context) - }, opaque: true, scale: 1.0) - guard let sourceImage = sourceImage else { - return - } - - let eyedropperView = EyedropperView(containerSize: controller.contentWrapperView.frame.size, drawingView: controller.drawingView, sourceImage: sourceImage) - eyedropperView.completed = { [weak self, weak controller] color in - if let strongSelf = self, let controller = controller { - strongSelf.updateColor.invoke(color) - controller.entitiesView.play() - controller.updateVideoPlayback(true) - dismissed() - } - } - eyedropperView.dismissed = { - controller.entitiesView.play() - controller.updateVideoPlayback(true) - } - eyedropperView.frame = controller.contentWrapperView.convert(controller.contentWrapperView.bounds, to: controller.view) - controller.view.addSubview(eyedropperView) - self.currentEyedropperView = eyedropperView - } - - func dismissCurrentEyedropper() { - if let currentEyedropperView = self.currentEyedropperView { - self.currentEyedropperView = nil - currentEyedropperView.dismiss() - } - } - - private weak var colorPickerScreen: ColorPickerScreen? - func presentColorPicker(initialColor: DrawingColor, dismissed: @escaping () -> Void = {}) { - self.dismissCurrentEyedropper() - self.dismissFontPicker() guard let controller = self.controller else { return } - self.hapticFeedback.impact(.medium) - var didDismiss = false - let colorController = ColorPickerScreen(context: self.context, initialColor: initialColor, updated: { [weak self] color in - self?.updateColor.invoke(color) - }, openEyedropper: { [weak self] in - self?.presentEyedropper(dismissed: dismissed) - }, dismissed: { - if !didDismiss { - didDismiss = true - dismissed() - } - }) - controller.present(colorController, in: .window(.root)) - self.colorPickerScreen = colorController - } - - func dismissColorPicker() { - if let colorPickerScreen = self.colorPickerScreen { - self.colorPickerScreen = nil - colorPickerScreen.dismiss() - } - } - - private var fastColorPickerView: ColorSpectrumPickerView? - func presentFastColorPicker(sourceView: UIView) { - self.dismissCurrentEyedropper() - self.dismissFontPicker() - - guard self.fastColorPickerView == nil, let superview = sourceView.superview else { - return - } - - self.hapticFeedback.impact(.medium) - - let size = CGSize(width: min(350.0, superview.frame.width - 8.0 - 24.0), height: 296.0) - - let fastColorPickerView = ColorSpectrumPickerView(frame: CGRect(origin: CGPoint(x: sourceView.frame.minX + 5.0, y: sourceView.frame.maxY - size.height - 6.0), size: size)) - fastColorPickerView.selected = { [weak self] color in - self?.updateColor.invoke(color) - } - let _ = fastColorPickerView.updateLayout(size: size, selectedColor: nil) - sourceView.superview?.addSubview(fastColorPickerView) - - fastColorPickerView.animateIn() - - self.fastColorPickerView = fastColorPickerView - } - - func updateFastColorPickerPan(_ point: CGPoint) { - guard let fastColorPickerView = self.fastColorPickerView else { - return - } - fastColorPickerView.handlePan(point: point) - } - - func dismissFastColorPicker() { - guard let fastColorPickerView = self.fastColorPickerView else { - return - } - self.fastColorPickerView = nil - fastColorPickerView.animateOut(completion: { [weak fastColorPickerView] in - fastColorPickerView?.removeFromSuperview() - }) - } - - private weak var currentFontPicker: ContextController? - func presentFontPicker(sourceView: UIView) { - guard !self.dismissFontPicker(), let validLayout = self.validLayout?.0 else { - return - } - - if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView { - entityView.textChanged = { [weak self] in - self?.dismissFontPicker() - } - } - - let fonts: [DrawingTextFont] = [ - .sanFrancisco, - .other("AmericanTypewriter", "Typewriter"), - .other("AvenirNext-DemiBoldItalic", "Avenir Next"), - .other("CourierNewPS-BoldMT", "Courier New"), - .other("Noteworthy-Bold", "Noteworthy"), - .other("Georgia-Bold", "Georgia"), - .other("Papyrus", "Papyrus"), - .other("SnellRoundhand-Bold", "Snell Roundhand") - ] - - var items: [ContextMenuItem] = [] - for font in fonts { - items.append(.action(ContextMenuActionItem(text: font.title, textFont: .custom(font: font.uiFont(size: 17.0), height: 42.0, verticalOffset: font.title == "Noteworthy" ? -6.0 : nil), icon: { _ in return nil }, animationName: nil, action: { [weak self] f in - f.dismissWithResult(.default) - guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { - return + self.interaction = DrawingToolsInteraction( + context: self.context, + drawingView: self.drawingView, + entitiesView: self.entitiesView, + selectionContainerView: self.selectionContainerView, + isVideo: controller.isVideo, + updateSelectedEntity: { [weak self] entity in + if let self { + self.updateSelectedEntity.invoke(entity) } - textEntity.font = font.font - entityView.update() - - if let (layout, orientation) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, forceUpdate: true, transition: .easeInOut(duration: 0.2)) + }, + updateVideoPlayback: { [weak controller] isPlaying in + if let controller { + controller.updateVideoPlayback(isPlaying) } - }))) - } - - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) - let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: CGRect(origin: .zero, size: CGSize(width: validLayout.size.width, height: validLayout.size.height - (validLayout.inputHeight ?? 0.0))), customPosition: CGPoint(x: 0.0, y: 1.0))), items: .single(ContextController.Items(content: .list(items)))) - self.controller?.present(contextController, in: .window(.root)) - self.currentFontPicker = contextController - contextController.view.disablesInteractiveKeyboardGestureRecognizer = true - } - - @discardableResult - func dismissFontPicker() -> Bool { - if let currentFontPicker = self.currentFontPicker { - self.currentFontPicker = nil - currentFontPicker.dismiss() - return true - } - return false + }, + updateColor: { [weak self] color in + if let self { + self.updateColor.invoke(color) + } + }, + getCurrentImage: { [weak controller] in + return controller?.getCurrentImage() + }, + getControllerNode: { [weak self] in + return self + }, + present: { [weak self] c, i, a in + if let self { + self.controller?.present(c, in: i, with: a) + } + }, + addSubview: { [weak self] view in + if let self { + self.view.addSubview(view) + } + } + ) } func animateIn() { @@ -2776,22 +2576,29 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U insertText: self.insertText, updateEntityView: self.updateEntityView, endEditingTextEntityView: self.endEditingTextEntityView, + entityViewForEntity: { [weak self] entity in + if let self, let entityView = self.entitiesView.getView(for: entity.uuid) { + return entityView + } else { + return nil + } + }, apply: self.apply, dismiss: self.dismiss, presentColorPicker: { [weak self] initialColor in - self?.presentColorPicker(initialColor: initialColor) + self?.interaction?.presentColorPicker(initialColor: initialColor) }, presentFastColorPicker: { [weak self] sourceView in - self?.presentFastColorPicker(sourceView: sourceView) + self?.interaction?.presentFastColorPicker(sourceView: sourceView) }, updateFastColorPickerPan: { [weak self] point in - self?.updateFastColorPickerPan(point) + self?.interaction?.updateFastColorPickerPan(point) }, dismissFastColorPicker: { [weak self] in - self?.dismissFastColorPicker() + self?.interaction?.dismissFastColorPicker() }, presentFontPicker: { [weak self] sourceView in - self?.presentFontPicker(sourceView: sourceView) + self?.interaction?.presentFontPicker(sourceView: sourceView) } ) ), @@ -2815,190 +2622,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U } } - if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity { - var isFirstTime = true - if let componentView = self.textEditAccessoryHost.view, componentView.superview != nil { - isFirstTime = false - } - UIView.performWithoutAnimation { - let accessorySize = self.textEditAccessoryHost.update( - transition: isFirstTime ? .immediate : .easeInOut(duration: 0.2), - component: AnyComponent( - TextSettingsComponent( - color: textEntity.color, - style: DrawingTextStyle(style: textEntity.style), - animation: DrawingTextAnimation(animation: textEntity.animation), - alignment: DrawingTextAlignment(alignment: textEntity.alignment), - font: DrawingTextFont(font: textEntity.font), - isEmojiKeyboard: entityView.textView.inputView != nil, - tag: nil, - fontTag: fontTag, - presentColorPicker: { [weak self] in - guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { - return - } - entityView.suspendEditing() - self?.presentColorPicker(initialColor: textEntity.color, dismissed: { - entityView.resumeEditing() - }) - }, - presentFastColorPicker: { [weak self] buttonTag in - if let buttonView = self?.textEditAccessoryHost.findTaggedView(tag: buttonTag) { - self?.presentFastColorPicker(sourceView: buttonView) - } - }, - updateFastColorPickerPan: { [weak self] point in - self?.updateFastColorPickerPan(point) - }, - dismissFastColorPicker: { [weak self] in - self?.dismissFastColorPicker() - }, - toggleStyle: { [weak self] in - self?.dismissFontPicker() - guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { - return - } - var nextStyle: DrawingTextEntity.Style - switch textEntity.style { - case .regular: - nextStyle = .filled - case .filled: - nextStyle = .semi - case .semi: - nextStyle = .stroke - case .stroke: - nextStyle = .regular - } - textEntity.style = nextStyle - entityView.update() - - if let (layout, orientation) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate) - } - }, - toggleAnimation: { [weak self] in - self?.dismissFontPicker() - guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { - return - } - var nextAnimation: DrawingTextEntity.Animation - switch textEntity.animation { - case .none: - nextAnimation = .typing - case .typing: - nextAnimation = .wiggle - case .wiggle: - nextAnimation = .zoomIn - case .zoomIn: - nextAnimation = .none - } - textEntity.animation = nextAnimation - entityView.update() - - if let (layout, orientation) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate) - } - }, - toggleAlignment: { [weak self] in - self?.dismissFontPicker() - guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { - return - } - var nextAlignment: DrawingTextEntity.Alignment - switch textEntity.alignment { - case .left: - nextAlignment = .center - case .center: - nextAlignment = .right - case .right: - nextAlignment = .left - } - textEntity.alignment = nextAlignment - entityView.update() - - if let (layout, orientation) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate) - } - }, - presentFontPicker: { [weak self] in - if let buttonView = self?.textEditAccessoryHost.findTaggedView(tag: fontTag) { - self?.presentFontPicker(sourceView: buttonView) - } - }, - toggleKeyboard: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.dismissFontPicker() - strongSelf.toggleInputMode() - } - ) - ), - environment: {}, - forceUpdate: true, - containerSize: CGSize(width: layout.size.width, height: 44.0) - ) - if let componentView = self.textEditAccessoryHost.view { - if componentView.superview == nil { - self.textEditAccessoryView.addSubview(componentView) - } - - self.textEditAccessoryView.frame = CGRect(origin: .zero, size: accessorySize) - componentView.frame = CGRect(origin: .zero, size: accessorySize) - } - } - } - } - - private func toggleInputMode() { - guard let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView else { - return - } - - let textView = entityView.textView - var shouldHaveInputView = false - if textView.isFirstResponder { - if textView.inputView == nil { - shouldHaveInputView = true - } - } else { - shouldHaveInputView = true - } - - if shouldHaveInputView { - let inputView = EntityInputView( - context: self.context, - isDark: true, - areCustomEmojiEnabled: true, - hideBackground: true, - forceHasPremium: true - ) - inputView.insertText = { [weak entityView] text in - entityView?.insertText(text) - } - inputView.deleteBackwards = { [weak textView] in - textView?.deleteBackward() - } - inputView.switchToKeyboard = { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.toggleInputMode() - } - textView.inputView = inputView - } else { - textView.inputView = nil - } - - if textView.isFirstResponder { - textView.reloadInputViews() - } else { - textView.becomeFirstResponder() - } - - if let (layout, orientation) = self.validLayout { - self.containerLayoutUpdated(layout: layout, orientation: orientation, animateOut: false, transition: .immediate) - } + self.interaction?.containerLayoutUpdated(layout: layout, transition: transition) } } @@ -3178,35 +2802,12 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.node.animateOut(completion: { completion() }) - - Queue.mainQueue().after(0.4) { - self.node.isHidden = true - } +// +// Queue.mainQueue().after(0.4) { +// self.node.isHidden = true +// } } - - public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { - super.dismiss(animated: flag, completion: completion) - - self.node._drawingView?.entitiesView = nil - self.node._entitiesView?.drawingView = nil - self.node._entitiesView?.entityAdded = { _ in } - self.node._entitiesView?.entityRemoved = { _ in } - self.node._drawingView?.getFullImage = { return nil } - self.node._entitiesView?.selectionContainerView = nil - self.node._entitiesView?.selectionChanged = { _ in } - self.node._entitiesView?.requestedMenuForEntityView = { _, _ in } - self.node._drawingView = nil - self.node._entitiesView = nil - } - - public func presentStickerSelection() { - self.node.insertSticker.invoke(Void()) - } - - public func addTextEntity() { - self.node.insertText.invoke(Void()) - } - + private var orientation: UIInterfaceOrientation? override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) @@ -3271,3 +2872,539 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U //self.chatDisplayNode.updateDropInteraction(isActive: false) } } + +public final class DrawingToolsInteraction { + private let context: AccountContext + private let drawingView: DrawingView + private let entitiesView: DrawingEntitiesView + private let selectionContainerView: DrawingSelectionContainerView + private let isVideo: Bool + private let updateSelectedEntity: (DrawingEntity?) -> Void + private let updateVideoPlayback: (Bool) -> Void + private let updateColor: (DrawingColor) -> Void + + private let getCurrentImage: () -> UIImage? + private let getControllerNode: () -> ASDisplayNode? + private let present: (ViewController, PresentationContextType, Any?) -> Void + private let addSubview: (UIView) -> Void + + private let textEditAccessoryView: UIInputView + private let textEditAccessoryHost: ComponentView + + private var currentEyedropperView: EyedropperView? + private weak var currentMenuController: ContextMenuController? + + private let hapticFeedback = HapticFeedback() + + private var isActive = false + private var validLayout: ContainerViewLayout? + + public init( + context: AccountContext, + drawingView: DrawingView, + entitiesView: DrawingEntitiesView, + selectionContainerView: DrawingSelectionContainerView, + isVideo: Bool, + updateSelectedEntity: @escaping (DrawingEntity?) -> Void, + updateVideoPlayback: @escaping (Bool) -> Void, + updateColor: @escaping (DrawingColor) -> Void, + getCurrentImage: @escaping () -> UIImage?, + getControllerNode: @escaping () -> ASDisplayNode?, + present: @escaping (ViewController, PresentationContextType, Any?) -> Void, + addSubview: @escaping (UIView) -> Void + ) { + self.context = context + self.drawingView = drawingView + self.entitiesView = entitiesView + self.selectionContainerView = selectionContainerView + self.isVideo = isVideo + self.updateSelectedEntity = updateSelectedEntity + self.updateVideoPlayback = updateVideoPlayback + self.updateColor = updateColor + self.getCurrentImage = getCurrentImage + self.getControllerNode = getControllerNode + self.present = present + self.addSubview = addSubview + + self.textEditAccessoryView = UIInputView(frame: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 44.0)), inputViewStyle: .keyboard) + self.textEditAccessoryHost = ComponentView() + + self.activate() + } + + func activate() { + self.isActive = true + + self.entitiesView.selectionContainerView = self.selectionContainerView + self.entitiesView.selectionChanged = { [weak self] entity in + if let self { + self.updateSelectedEntity(entity) + } + } + + self.entitiesView.requestedMenuForEntityView = { [weak self] entityView, isTopmost in + guard let self, let node = self.getControllerNode() else { + return + } + if self.currentMenuController != nil { + if let entityView = entityView as? DrawingTextEntityView { + entityView.beginEditing(accessoryView: self.textEditAccessoryView) + } + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + var actions: [ContextMenuAction] = [] + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Delete, accessibilityLabel: presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in + if let self, let entityView { + self.entitiesView.remove(uuid: entityView.entity.uuid, animated: true) + } + })) + if let entityView = entityView as? DrawingTextEntityView { + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Edit, accessibilityLabel: presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in + if let self, let entityView { + entityView.beginEditing(accessoryView: self.textEditAccessoryView) + self.entitiesView.selectEntity(entityView.entity) + } + })) + } + if !isTopmost { + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_MoveForward, accessibilityLabel: presentationData.strings.Paint_MoveForward), action: { [weak self, weak entityView] in + if let self, let entityView { + self.entitiesView.bringToFront(uuid: entityView.entity.uuid) + } + })) + } + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Duplicate, accessibilityLabel: presentationData.strings.Paint_Duplicate), action: { [weak self, weak entityView] in + if let self, let entityView { + let newEntity = self.entitiesView.duplicate(entityView.entity) + self.entitiesView.selectEntity(newEntity) + } + })) + let entityFrame = entityView.convert(entityView.selectionBounds, to: node.view).offsetBy(dx: 0.0, dy: -6.0) + let controller = ContextMenuController(actions: actions) + let bounds = node.bounds.insetBy(dx: 0.0, dy: 160.0) + self.present( + controller, + .window(.root), + ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak node] in + if let node { + return (node, entityFrame, node, bounds) + } else { + return nil + } + }) + ) + self.currentMenuController = controller + } + } + + public func deactivate() { + self.isActive = false + } + + public func insertEntity(_ entity: DrawingEntity) { + self.entitiesView.prepareNewEntity(entity) + self.entitiesView.add(entity) + self.entitiesView.selectEntity(entity) + + if let entityView = self.entitiesView.getView(for: entity.uuid) { + if let textEntityView = entityView as? DrawingTextEntityView { + textEntityView.beginEditing(accessoryView: self.textEditAccessoryView) + } else { + entityView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + entityView.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) + + if let selectionView = entityView.selectionView { + selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2) + } + } + } + } + + func presentEyedropper(retryLaterForVideo: Bool = true, dismissed: @escaping () -> Void) { +// self.entitiesView.pause() +// +// if self.isVideo && retryLaterForVideo { +// self.updateVideoPlayback(false) +// Queue.mainQueue().after(0.1) { +// self.presentEyedropper(retryLaterForVideo: false, dismissed: dismissed) +// } +// return +// } +// +// guard let currentImage = self.getCurrentImage() else { +// self.entitiesView.play() +// self.updateVideoPlayback(true) +// return +// } +// +// let sourceImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in +// let bounds = CGRect(origin: .zero, size: size) +// if let cgImage = currentImage.cgImage { +// context.draw(cgImage, in: bounds) +// } +// if let cgImage = self.drawingView.drawingImage?.cgImage { +// context.draw(cgImage, in: bounds) +// } +// context.translateBy(x: size.width / 2.0, y: size.height / 2.0) +// context.scaleBy(x: 1.0, y: -1.0) +// context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) +// self.entitiesView.layer.render(in: context) +// }, opaque: true, scale: 1.0) +// guard let sourceImage = sourceImage else { +// return +// } +// +// let eyedropperView = EyedropperView(containerSize: controller.contentWrapperView.frame.size, drawingView: self.drawingView, sourceImage: sourceImage) +// eyedropperView.completed = { [weak self] color in +// if let self { +// self.updateColor(color) +// self.entitiesView.play() +// self.updateVideoPlayback(true) +// +// dismissed() +// } +// } +// eyedropperView.dismissed = { [weak self] in +// if let self { +// self.entitiesView.play() +// self.updateVideoPlayback(true) +// } +// } +// eyedropperView.frame = controller.contentWrapperView.convert(controller.contentWrapperView.bounds, to: controller.view) +// self.addSubview(eyedropperView) +// self.currentEyedropperView = eyedropperView + } + + func dismissCurrentEyedropper() { + if let currentEyedropperView = self.currentEyedropperView { + self.currentEyedropperView = nil + currentEyedropperView.dismiss() + } + } + + weak var colorPickerScreen: ColorPickerScreen? + func presentColorPicker(initialColor: DrawingColor, dismissed: @escaping () -> Void = {}) { + self.dismissCurrentEyedropper() + self.dismissFontPicker() + + self.hapticFeedback.impact(.medium) + var didDismiss = false + let colorController = ColorPickerScreen(context: self.context, initialColor: initialColor, updated: { [weak self] color in + if let self { + self.updateColor(color) + } + }, openEyedropper: { [weak self] in + if let self { + self.presentEyedropper(dismissed: dismissed) + } + }, dismissed: { + if !didDismiss { + didDismiss = true + dismissed() + } + }) + self.present(colorController, .window(.root), nil) + self.colorPickerScreen = colorController + } + + func dismissColorPicker() { + if let colorPickerScreen = self.colorPickerScreen { + self.colorPickerScreen = nil + colorPickerScreen.dismiss() + } + } + + private var fastColorPickerView: ColorSpectrumPickerView? + func presentFastColorPicker(sourceView: UIView) { + self.dismissCurrentEyedropper() + self.dismissFontPicker() + + guard self.fastColorPickerView == nil, let superview = sourceView.superview else { + return + } + + self.hapticFeedback.impact(.medium) + + let size = CGSize(width: min(350.0, superview.frame.width - 8.0 - 24.0), height: 296.0) + + let fastColorPickerView = ColorSpectrumPickerView(frame: CGRect(origin: CGPoint(x: sourceView.frame.minX + 5.0, y: sourceView.frame.maxY - size.height - 6.0), size: size)) + fastColorPickerView.selected = { [weak self] color in + if let self { + self.updateColor(color) + } + } + let _ = fastColorPickerView.updateLayout(size: size, selectedColor: nil) + sourceView.superview?.addSubview(fastColorPickerView) + + fastColorPickerView.animateIn() + + self.fastColorPickerView = fastColorPickerView + } + + func updateFastColorPickerPan(_ point: CGPoint) { + guard let fastColorPickerView = self.fastColorPickerView else { + return + } + fastColorPickerView.handlePan(point: point) + } + + func dismissFastColorPicker() { + guard let fastColorPickerView = self.fastColorPickerView else { + return + } + self.fastColorPickerView = nil + fastColorPickerView.animateOut(completion: { [weak fastColorPickerView] in + fastColorPickerView?.removeFromSuperview() + }) + } + + private weak var currentFontPicker: ContextController? + func presentFontPicker(sourceView: UIView) { + guard !self.dismissFontPicker(), let validLayout = self.validLayout else { + return + } + + if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView { + entityView.textChanged = { [weak self] in + self?.dismissFontPicker() + } + } + + let fonts: [DrawingTextFont] = [ + .sanFrancisco, + .other("AmericanTypewriter", "Typewriter"), + .other("AvenirNext-DemiBoldItalic", "Avenir Next"), + .other("CourierNewPS-BoldMT", "Courier New"), + .other("Noteworthy-Bold", "Noteworthy"), + .other("Georgia-Bold", "Georgia"), + .other("Papyrus", "Papyrus"), + .other("SnellRoundhand-Bold", "Snell Roundhand") + ] + + var items: [ContextMenuItem] = [] + for font in fonts { + items.append(.action(ContextMenuActionItem(text: font.title, textFont: .custom(font: font.uiFont(size: 17.0), height: 42.0, verticalOffset: font.title == "Noteworthy" ? -6.0 : nil), icon: { _ in return nil }, animationName: nil, action: { [weak self] f in + f.dismissWithResult(.default) + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + textEntity.font = font.font + entityView.update() + + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, transition: .easeInOut(duration: 0.2)) + } + }))) + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: CGRect(origin: .zero, size: CGSize(width: validLayout.size.width, height: validLayout.size.height - (validLayout.inputHeight ?? 0.0))), customPosition: CGPoint(x: 0.0, y: 1.0))), items: .single(ContextController.Items(content: .list(items)))) + self.present(contextController, .window(.root), nil) + self.currentFontPicker = contextController + contextController.view.disablesInteractiveKeyboardGestureRecognizer = true + } + + @discardableResult + func dismissFontPicker() -> Bool { + if let currentFontPicker = self.currentFontPicker { + self.currentFontPicker = nil + currentFontPicker.dismiss() + return true + } + return false + } + + private func toggleInputMode() { + guard let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView else { + return + } + + let textView = entityView.textView + var shouldHaveInputView = false + if textView.isFirstResponder { + if textView.inputView == nil { + shouldHaveInputView = true + } + } else { + shouldHaveInputView = true + } + + if shouldHaveInputView { + let inputView = EntityInputView( + context: self.context, + isDark: true, + areCustomEmojiEnabled: true, + hideBackground: true, + forceHasPremium: true + ) + inputView.insertText = { [weak entityView] text in + entityView?.insertText(text) + } + inputView.deleteBackwards = { [weak textView] in + textView?.deleteBackward() + } + inputView.switchToKeyboard = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.toggleInputMode() + } + textView.inputView = inputView + } else { + textView.inputView = nil + } + + if textView.isFirstResponder { + textView.reloadInputViews() + } else { + textView.becomeFirstResponder() + } + + if let layout = self.validLayout { + self.containerLayoutUpdated(layout: layout, transition: .immediate) + } + } + + public func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) { + self.validLayout = layout + + guard self.isActive else { + return + } + + if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity { + var isFirstTime = true + if let componentView = self.textEditAccessoryHost.view, componentView.superview != nil { + isFirstTime = false + } + UIView.performWithoutAnimation { + let accessorySize = self.textEditAccessoryHost.update( + transition: isFirstTime ? .immediate : .easeInOut(duration: 0.2), + component: AnyComponent( + TextSettingsComponent( + color: textEntity.color, + style: DrawingTextStyle(style: textEntity.style), + animation: DrawingTextAnimation(animation: textEntity.animation), + alignment: DrawingTextAlignment(alignment: textEntity.alignment), + font: DrawingTextFont(font: textEntity.font), + isEmojiKeyboard: entityView.textView.inputView != nil, + tag: nil, + fontTag: fontTag, + presentColorPicker: { [weak self] in + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + entityView.suspendEditing() + self?.presentColorPicker(initialColor: textEntity.color, dismissed: { + entityView.resumeEditing() + }) + }, + presentFastColorPicker: { [weak self] buttonTag in + if let buttonView = self?.textEditAccessoryHost.findTaggedView(tag: buttonTag) { + self?.presentFastColorPicker(sourceView: buttonView) + } + }, + updateFastColorPickerPan: { [weak self] point in + self?.updateFastColorPickerPan(point) + }, + dismissFastColorPicker: { [weak self] in + self?.dismissFastColorPicker() + }, + toggleStyle: { [weak self] in + self?.dismissFontPicker() + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + var nextStyle: DrawingTextEntity.Style + switch textEntity.style { + case .regular: + nextStyle = .filled + case .filled: + nextStyle = .semi + case .semi: + nextStyle = .stroke + case .stroke: + nextStyle = .regular + } + textEntity.style = nextStyle + entityView.update() + + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate) + } + }, + toggleAnimation: { [weak self] in + self?.dismissFontPicker() + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + var nextAnimation: DrawingTextEntity.Animation + switch textEntity.animation { + case .none: + nextAnimation = .typing + case .typing: + nextAnimation = .wiggle + case .wiggle: + nextAnimation = .zoomIn + case .zoomIn: + nextAnimation = .none + } + textEntity.animation = nextAnimation + entityView.update() + + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate) + } + }, + toggleAlignment: { [weak self] in + self?.dismissFontPicker() + guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else { + return + } + var nextAlignment: DrawingTextEntity.Alignment + switch textEntity.alignment { + case .left: + nextAlignment = .center + case .center: + nextAlignment = .right + case .right: + nextAlignment = .left + } + textEntity.alignment = nextAlignment + entityView.update() + + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate) + } + }, + presentFontPicker: { [weak self] in + if let buttonView = self?.textEditAccessoryHost.findTaggedView(tag: fontTag) { + self?.presentFontPicker(sourceView: buttonView) + } + }, + toggleKeyboard: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.dismissFontPicker() + strongSelf.toggleInputMode() + } + ) + ), + environment: {}, + forceUpdate: true, + containerSize: CGSize(width: layout.size.width, height: 44.0) + ) + if let componentView = self.textEditAccessoryHost.view { + if componentView.superview == nil { + self.textEditAccessoryView.addSubview(componentView) + } + + self.textEditAccessoryView.frame = CGRect(origin: .zero, size: accessorySize) + componentView.frame = CGRect(origin: .zero, size: accessorySize) + } + } + } + } +} diff --git a/submodules/DrawingUI/Sources/EyedropperView.swift b/submodules/DrawingUI/Sources/EyedropperView.swift index e38470da8c..62ebd7e774 100644 --- a/submodules/DrawingUI/Sources/EyedropperView.swift +++ b/submodules/DrawingUI/Sources/EyedropperView.swift @@ -38,7 +38,7 @@ private func generateGridImage(size: CGSize, light: Bool) -> UIImage? { }) } -final class EyedropperView: UIView { +public final class EyedropperView: UIView { private weak var drawingView: DrawingView? private let containerView: UIView diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 306e91861c..d58748b840 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -872,7 +872,7 @@ public class CameraScreen: ViewController { } else { if translation.x < -10.0 { - let transitionFraction = 1.0 - abs(translation.x) / self.frame.width + let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width controller.updateTransitionProgress(transitionFraction, transition: .immediate) } else if translation.y < -10.0 { controller.presentGallery() @@ -882,7 +882,7 @@ public class CameraScreen: ViewController { } case .ended: let velocity = gestureRecognizer.velocity(in: self.view) - let transitionFraction = 1.0 - abs(translation.x) / self.frame.width + let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true) default: break @@ -982,26 +982,28 @@ public class CameraScreen: ViewController { } func resumeCameraCapture() { - if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) { - self.simplePreviewView?.addSubview(snapshot) - self.previewSnapshotView = snapshot - } - self.simplePreviewView?.isEnabled = true - self.camera.startCapture() - - if #available(iOS 13.0, *), let isPreviewing = self.simplePreviewView?.isPreviewing { - let _ = (isPreviewing - |> filter { - $0 + if self.simplePreviewView?.isEnabled == false { + if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) { + self.simplePreviewView?.addSubview(snapshot) + self.previewSnapshotView = snapshot } - |> take(1)).start(next: { [weak self] _ in - if let self { + self.simplePreviewView?.isEnabled = true + self.camera.startCapture() + + if #available(iOS 13.0, *), let isPreviewing = self.simplePreviewView?.isPreviewing { + let _ = (isPreviewing + |> filter { + $0 + } + |> take(1)).start(next: { [weak self] _ in + if let self { + self.previewBlurPromise.set(false) + } + }) + } else { + Queue.mainQueue().after(1.0) { self.previewBlurPromise.set(false) } - }) - } else { - Queue.mainQueue().after(1.0) { - self.previewBlurPromise.set(false) } } } @@ -1344,7 +1346,7 @@ public class CameraScreen: ViewController { private var isTransitioning = false public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { self.isTransitioning = true - let offsetX = (1.0 - transitionFraction) * self.node.frame.width * -1.0 + let offsetX = floorToScreenPixels((1.0 - transitionFraction) * self.node.frame.width * -1.0) transition.updateTransform(layer: self.node.backgroundView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0)) transition.updateTransform(layer: self.node.containerView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0)) let scale = max(0.8, min(1.0, 0.8 + 0.2 * transitionFraction)) @@ -1359,7 +1361,7 @@ public class CameraScreen: ViewController { self.statusBar.updateStatusBarStyle(transitionFraction > 0.45 ? .White : .Ignore, animated: true) if let navigationController = self.navigationController as? NavigationController { - let offsetX = transitionFraction * self.node.frame.width + let offsetX = floorToScreenPixels(transitionFraction * self.node.frame.width) navigationController.updateRootContainerTransitionOffset(offsetX, transition: transition) } } @@ -1367,7 +1369,7 @@ public class CameraScreen: ViewController { public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) { self.isTransitioning = false if dismissing { - if transitionFraction < 0.7 || velocity > 1000.0 { + if transitionFraction < 0.7 || velocity < -1000.0 { self.requestDismiss(animated: true, interactive: true) } else { self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index a61e8ad0d7..74692b08db 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -464,6 +464,10 @@ public final class MediaEditor { } } + public func play() { + self.player?.play() + } + public func stop() { self.player?.pause() } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index ccc6a9e539..e3d1fd400d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -85,46 +85,35 @@ final class MediaEditorComposer { self.renderChain.update(values: self.values) } - func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, pool: CVPixelBufferPool?, completion: @escaping (CVPixelBuffer?) -> Void) { - guard let textureCache = self.textureCache, let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let pool = pool else { + func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, pool: CVPixelBufferPool?, textureRotation: TextureRotation, completion: @escaping (CVPixelBuffer?) -> Void) { + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let pool = pool else { completion(nil) return } let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) - let width = CVPixelBufferGetWidth(imageBuffer) - let height = CVPixelBufferGetHeight(imageBuffer) - let format: MTLPixelFormat = .bgra8Unorm - var textureRef : CVMetalTexture? - let status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, imageBuffer, nil, format, width, height, 0, &textureRef) - var texture: MTLTexture? - if status == kCVReturnSuccess { - texture = CVMetalTextureGetTexture(textureRef!) - } - if let texture { - self.renderer.consumeTexture(texture) - self.renderer.renderFrame() + self.renderer.consumeVideoPixelBuffer(imageBuffer, rotation: textureRotation) + self.renderer.renderFrame() + + if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) { + ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height)) - if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) { - ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height)) - - var pixelBuffer: CVPixelBuffer? - CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer) - - if let pixelBuffer { - processImage(inputImage: ciImage, time: time, completion: { compositedImage in - if var compositedImage { - let scale = self.outputDimensions.width / self.dimensions.width - compositedImage = compositedImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) - - self.ciContext?.render(compositedImage, to: pixelBuffer) - completion(pixelBuffer) - } else { - completion(nil) - } - }) - return - } + var pixelBuffer: CVPixelBuffer? + CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer) + + if let pixelBuffer { + processImage(inputImage: ciImage, time: time, completion: { compositedImage in + if var compositedImage { + let scale = self.outputDimensions.width / self.dimensions.width + compositedImage = compositedImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) + + self.ciContext?.render(compositedImage, to: pixelBuffer) + completion(pixelBuffer) + } else { + completion(nil) + } + }) + return } } completion(nil) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift index c3c2de83ae..1e8cc11c16 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift @@ -50,7 +50,7 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge } func scheduleFrame() { - Queue.mainQueue().async { + Queue.mainQueue().justDispatch { self.draw() } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 0337bed811..2e5ca44e09 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -247,10 +247,7 @@ public final class MediaEditorVideoExport { private let subject: Subject private let configuration: Configuration private let outputPath: String - - private var previousSampleTime: CMTime = .zero - private var processedPixelBuffer: CVPixelBuffer? - + private var reader: AVAssetReader? private var videoOutput: AVAssetReaderOutput? @@ -260,6 +257,7 @@ public final class MediaEditorVideoExport { private var writer: MediaEditorVideoExportWriter? private var composer: MediaEditorComposer? + private var textureRotation: TextureRotation = .rotate0Degrees private let duration = ValuePromise() private let pauseDispatchGroup = DispatchGroup() @@ -320,16 +318,23 @@ public final class MediaEditorVideoExport { return } + self.textureRotation = textureRotatonForAVAsset(asset) + writer.setup(configuration: self.configuration, outputPath: self.outputPath) let videoTracks = asset.tracks(withMediaType: .video) if (videoTracks.count > 0) { - let outputSettings: [String : Any] var sourceFrameRate: Float = 0.0 + let outputSettings: [String: Any] = [ + kCVPixelBufferPixelFormatTypeKey as String: [kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange], + AVVideoColorPropertiesKey: [ + AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, + AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, + AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2 + ] + ] if let videoTrack = videoTracks.first, videoTrack.preferredTransform.isIdentity && !self.configuration.values.requiresComposing { - outputSettings = [kCVPixelBufferPixelFormatTypeKey as String: [kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange]] } else { - outputSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] self.setupComposer() } let videoOutput = AVAssetReaderTrackOutput(track: videoTracks.first!, outputSettings: outputSettings) @@ -516,7 +521,7 @@ public final class MediaEditorVideoExport { if let buffer = output.copyNextSampleBuffer() { if let composer = self.composer { let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer) - composer.processSampleBuffer(buffer, pool: writer.pixelBufferPool, completion: { pixelBuffer in + composer.processSampleBuffer(buffer, pool: writer.pixelBufferPool, textureRotation: self.textureRotation, completion: { pixelBuffer in if let pixelBuffer { if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) { writer.markVideoAsFinished() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift index 33c009a189..f8f2e18558 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift @@ -3,6 +3,28 @@ import AVFoundation import Metal import MetalKit +func textureRotatonForAVAsset(_ asset: AVAsset) -> TextureRotation { + for track in asset.tracks { + if track.mediaType == .video { + let t = track.preferredTransform + if t.a == -1.0 && t.d == -1.0 { + return .rotate180Degrees + } else if t.a == 1.0 && t.d == 1.0 { + return .rotate0Degrees + } else if t.b == -1.0 && t.c == 1.0 { + return .rotate270Degrees + } else if t.a == -1.0 && t.d == 1.0 { + return .rotate270Degrees + } else if t.a == 1.0 && t.d == -1.0 { + return .rotate180Degrees + } else { + return .rotate90Degrees + } + } + } + return .rotate0Degrees +} + final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullDelegate { private let player: AVPlayer private var playerItem: AVPlayerItem? @@ -80,23 +102,10 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD for track in playerItem.asset.tracks { if track.mediaType == .video { hasVideoTrack = true - - let t = track.preferredTransform - if t.a == -1.0 && t.d == -1.0 { - self.textureRotation = .rotate180Degrees - } else if t.a == 1.0 && t.d == 1.0 { - self.textureRotation = .rotate0Degrees - } else if t.b == -1.0 && t.c == 1.0 { - self.textureRotation = .rotate270Degrees - } else if t.a == -1.0 && t.d == 1.0 { - self.textureRotation = .rotate270Degrees - } else if t.a == 1.0 && t.d == -1.0 { - self.textureRotation = .rotate180Degrees - } else { - self.textureRotation = .rotate90Degrees - } + break } } + self.textureRotation = textureRotatonForAVAsset(playerItem.asset) if !hasVideoTrack { assertionFailure("No video track found.") return diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 4a3866b39e..85ab785831 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -496,7 +496,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let drawButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 - 3.0 - drawButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 - 3.0 - drawButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), size: drawButtonSize ) if let drawButtonView = self.drawButton.view { @@ -521,7 +521,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let textButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width / 2.5 + 5.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + origin: CGPoint(x: floorToScreenPixels(availableSize.width / 2.5 + 5.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), size: textButtonSize ) if let textButtonView = self.textButton.view { @@ -546,7 +546,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let stickerButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width - availableSize.width / 2.5 - 5.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + origin: CGPoint(x: floorToScreenPixels(availableSize.width - availableSize.width / 2.5 - 5.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), size: stickerButtonSize ) if let stickerButtonView = self.stickerButton.view { @@ -571,7 +571,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let toolsButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 * 3.0 + 3.0 - toolsButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset), + origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 * 3.0 + 3.0 - toolsButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), size: toolsButtonSize ) if let toolsButtonView = self.toolsButton.view { @@ -592,24 +592,19 @@ final class MediaEditorScreenComponent: Component { context: component.context, duration: playerState.duration, startPosition: playerState.timeRange?.lowerBound ?? 0.0, - endPosition: playerState.timeRange?.upperBound ?? playerState.duration, + endPosition: playerState.timeRange?.upperBound ?? min(playerState.duration, storyMaxVideoDuration), position: playerState.position, + maxDuration: storyMaxVideoDuration, frames: playerState.frames, framesUpdateTimestamp: playerState.framesUpdateTimestamp, - startPositionUpdated: { [weak mediaEditor] position, done in + trimUpdated: { [weak mediaEditor] start, end, updatedEnd, done in if let mediaEditor { - mediaEditor.setVideoTrimStart(position) - mediaEditor.seek(position, andPlay: done) - } - }, - endPositionUpdated: { [weak mediaEditor] position, done in - if let mediaEditor { - mediaEditor.setVideoTrimEnd(position) + mediaEditor.setVideoTrimStart(start) + mediaEditor.setVideoTrimEnd(end) if done { - let start = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0 mediaEditor.seek(start, andPlay: true) } else { - mediaEditor.seek(position, andPlay: false) + mediaEditor.seek(updatedEnd ? end : start, andPlay: false) } } }, @@ -730,7 +725,7 @@ final class MediaEditorScreenComponent: Component { } case let .message(peerIds, _): if peerIds.count == 1 { - privacyText = "User Test" + privacyText = "1 Recipient" } else { privacyText = "\(peerIds.count) Recipients" } @@ -871,6 +866,7 @@ final class MediaEditorScreenComponent: Component { } private let storyDimensions = CGSize(width: 1080.0, height: 1920.0) +private let storyMaxVideoDuration: Double = 60.0 public enum MediaEditorResultPrivacy: Equatable { case story(privacy: EngineStoryPrivacy, archive: Bool) @@ -928,6 +924,7 @@ public final class MediaEditorScreen: ViewController { fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private weak var controller: MediaEditorScreen? private let context: AccountContext + private var interaction: DrawingToolsInteraction? private let initializationTimestamp = CACurrentMediaTime() fileprivate var subject: MediaEditorScreen.Subject? @@ -1162,6 +1159,51 @@ public final class MediaEditorScreen: ViewController { let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(self.handleRotate(_:))) rotateGestureRecognizer.delegate = self self.previewContainerView.addGestureRecognizer(rotateGestureRecognizer) + + let tapGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.previewContainerView.addGestureRecognizer(tapGestureRecognizer) + + self.interaction = DrawingToolsInteraction( + context: self.context, + drawingView: self.drawingView, + entitiesView: self.entitiesView, + selectionContainerView: self.selectionContainerView, + isVideo: false, + updateSelectedEntity: { _ in + + }, + updateVideoPlayback: { [weak self] isPlaying in + if let self, let mediaEditor = self.mediaEditor { + if isPlaying { + mediaEditor.play() + } else { + mediaEditor.stop() + } + } + }, + updateColor: { [weak self] color in + if let self, let selectedEntityView = self.entitiesView.selectedEntityView { + selectedEntityView.entity.color = color + selectedEntityView.update(animated: false) + } + }, + getCurrentImage: { + return nil + }, + getControllerNode: { [weak self] in + return self + }, + present: { [weak self] c, i, a in + if let self { + self.controller?.present(c, in: i, with: a) + } + }, + addSubview: { [weak self] view in + if let self { + self.view.addSubview(view) + } + } + ) } @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { @@ -1180,6 +1222,12 @@ public final class MediaEditorScreen: ViewController { self.entitiesView.handleRotate(gestureRecognizer) } + @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + if self.entitiesView.hasSelection { + self.entitiesView.selectEntity(nil) + } + } + func animateIn() { if let transitionIn = self.controller?.transitionIn { switch transitionIn { @@ -1435,21 +1483,6 @@ public final class MediaEditorScreen: ViewController { } } - private func insertDrawingEntity(_ entity: DrawingEntity) { - self.entitiesView.prepareNewEntity(entity) - self.entitiesView.add(entity) - self.entitiesView.selectEntity(entity) - - if let entityView = entitiesView.getView(for: entity.uuid) { - entityView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - entityView.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) - - if let selectionView = entityView.selectionView { - selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2) - } - } - } - private var drawingScreen: DrawingScreen? func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { guard let controller = self.controller else { @@ -1492,71 +1525,65 @@ public final class MediaEditorScreen: ViewController { privacy: controller.state.privacy, openDrawing: { [weak self] mode in if let self { + if self.entitiesView.hasSelection { + self.entitiesView.selectEntity(nil) + } switch mode { case .sticker: let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get()) controller.completion = { [weak self] file in if let self, let file { let stickerEntity = DrawingStickerEntity(content: .file(file)) - self.insertDrawingEntity(stickerEntity) + self.interaction?.insertEntity(stickerEntity) } } self.controller?.present(controller, in: .current) return case .text: - break - default: - break - } - - let controller = DrawingScreen(context: self.context, sourceHint: .storyEditor, size: self.previewContainerView.frame.size, originalSize: storyDimensions, isVideo: false, isAvatar: false, drawingView: self.drawingView, entitiesView: self.entitiesView, selectionContainerView: self.selectionContainerView, existingStickerPickerInputData: self.stickerPickerInputData) - self.drawingScreen = controller - self.drawingView.isUserInteractionEnabled = true - - controller.requestDismiss = { [weak controller, weak self] in - self?.drawingScreen = nil - controller?.animateOut({ - controller?.dismiss() - }) - self?.drawingView.isUserInteractionEnabled = false - self?.animateInFromTool() + let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .regular, animation: .none, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white)) + self.interaction?.insertEntity(textEntity) + return + case .drawing: + let controller = DrawingScreen(context: self.context, sourceHint: .storyEditor, size: self.previewContainerView.frame.size, originalSize: storyDimensions, isVideo: false, isAvatar: false, drawingView: self.drawingView, entitiesView: self.entitiesView, selectionContainerView: self.selectionContainerView, existingStickerPickerInputData: self.stickerPickerInputData) + self.drawingScreen = controller + self.drawingView.isUserInteractionEnabled = true - self?.entitiesView.selectEntity(nil) - } - controller.requestApply = { [weak controller, weak self] in - self?.drawingScreen = nil - controller?.animateOut({ - controller?.dismiss() - }) - self?.drawingView.isUserInteractionEnabled = false - self?.animateInFromTool() - - if let result = controller?.generateDrawingResultData() { - self?.mediaEditor?.setDrawingAndEntities(data: result.data, image: result.drawingImage, entities: result.entities) - } else { - self?.mediaEditor?.setDrawingAndEntities(data: nil, image: nil, entities: []) + controller.requestDismiss = { [weak controller, weak self] in + self?.drawingScreen = nil + controller?.animateOut({ + controller?.dismiss() + }) + self?.drawingView.isUserInteractionEnabled = false + self?.animateInFromTool() + + self?.entitiesView.selectEntity(nil) } - - self?.entitiesView.selectEntity(nil) + controller.requestApply = { [weak controller, weak self] in + self?.drawingScreen = nil + controller?.animateOut({ + controller?.dismiss() + }) + self?.drawingView.isUserInteractionEnabled = false + self?.animateInFromTool() + + if let result = controller?.generateDrawingResultData() { + self?.mediaEditor?.setDrawingAndEntities(data: result.data, image: result.drawingImage, entities: result.entities) + } else { + self?.mediaEditor?.setDrawingAndEntities(data: nil, image: nil, entities: []) + } + + self?.entitiesView.selectEntity(nil) + } + self.controller?.present(controller, in: .current) + self.animateOutToTool() } - self.controller?.present(controller, in: .current) - - switch mode { - case .sticker: - controller.presentStickerSelection() - case .text: - Queue.mainQueue().after(0.05, { - controller.addTextEntity() - }) - default: - break - } - - self.animateOutToTool() } }, openTools: { [weak self] in if let self, let mediaEditor = self.mediaEditor { + if self.entitiesView.hasSelection { + self.entitiesView.selectEntity(nil) + } let controller = MediaToolsScreen(context: self.context, mediaEditor: mediaEditor) controller.dismissed = { [weak self] in if let self { @@ -1605,6 +1632,8 @@ public final class MediaEditorScreen: ViewController { transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) + self.interaction?.containerLayoutUpdated(layout: layout, transition: transition) + if isFirstTime { self.animateIn() } @@ -1968,7 +1997,10 @@ public final class MediaEditorScreen: ViewController { } mediaEditor.stop() - + + let codableEntities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }.compactMap({ CodableDrawingEntity(entity: $0) }) + mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) + if mediaEditor.resultIsVideo { let videoResult: Result.VideoResult let duration: Double diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift index 0b798a9943..ef5a75bc8a 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/VideoScrubberComponent.swift @@ -23,10 +23,10 @@ final class VideoScrubberComponent: Component { let startPosition: Double let endPosition: Double let position: Double + let maxDuration: Double let frames: [UIImage] let framesUpdateTimestamp: Double - let startPositionUpdated: (Double, Bool) -> Void - let endPositionUpdated: (Double, Bool) -> Void + let trimUpdated: (Double, Double, Bool, Bool) -> Void let positionUpdated: (Double, Bool) -> Void init( @@ -35,10 +35,10 @@ final class VideoScrubberComponent: Component { startPosition: Double, endPosition: Double, position: Double, + maxDuration: Double, frames: [UIImage], framesUpdateTimestamp: Double, - startPositionUpdated: @escaping (Double, Bool) -> Void, - endPositionUpdated: @escaping (Double, Bool) -> Void, + trimUpdated: @escaping (Double, Double, Bool, Bool) -> Void, positionUpdated: @escaping (Double, Bool) -> Void ) { self.context = context @@ -46,10 +46,10 @@ final class VideoScrubberComponent: Component { self.startPosition = startPosition self.endPosition = endPosition self.position = position + self.maxDuration = maxDuration self.frames = frames self.framesUpdateTimestamp = framesUpdateTimestamp - self.startPositionUpdated = startPositionUpdated - self.endPositionUpdated = endPositionUpdated + self.trimUpdated = trimUpdated self.positionUpdated = positionUpdated } @@ -69,6 +69,9 @@ final class VideoScrubberComponent: Component { if lhs.position != rhs.position { return false } + if lhs.maxDuration != rhs.maxDuration { + return false + } if lhs.framesUpdateTimestamp != rhs.framesUpdateTimestamp { return false } @@ -165,22 +168,28 @@ final class VideoScrubberComponent: Component { let end = self.frame.width - handleWidth let length = end - start let fraction = (location.x - start) / length - var value = max(0.0, component.duration * fraction) - if value > component.endPosition - minumumDuration { - value = max(0.0, component.endPosition - minumumDuration) + + var startValue = max(0.0, component.duration * fraction) + if startValue > component.endPosition - minumumDuration { + startValue = max(0.0, component.endPosition - minumumDuration) + } + var endValue = component.endPosition + if endValue - startValue > component.maxDuration { + let delta = (endValue - startValue) - component.maxDuration + endValue -= delta } var transition: Transition = .immediate switch gestureRecognizer.state { case .began, .changed: self.isPanningHandle = true - component.startPositionUpdated(value, false) + component.trimUpdated(startValue, endValue, false, false) if case .began = gestureRecognizer.state { transition = .easeInOut(duration: 0.25) } case .ended, .cancelled: self.isPanningHandle = false - component.startPositionUpdated(value, true) + component.trimUpdated(startValue, endValue, false, true) transition = .easeInOut(duration: 0.25) default: break @@ -197,22 +206,28 @@ final class VideoScrubberComponent: Component { let end = self.frame.width - handleWidth let length = end - start let fraction = (location.x - start) / length - var value = min(component.duration, component.duration * fraction) - if value < component.startPosition + minumumDuration { - value = min(component.duration, component.startPosition + minumumDuration) + + var endValue = min(component.duration, component.duration * fraction) + if endValue < component.startPosition + minumumDuration { + endValue = min(component.duration, component.startPosition + minumumDuration) + } + var startValue = component.startPosition + if endValue - startValue > component.maxDuration { + let delta = (endValue - startValue) - component.maxDuration + startValue += delta } var transition: Transition = .immediate switch gestureRecognizer.state { case .began, .changed: self.isPanningHandle = true - component.endPositionUpdated(value, false) + component.trimUpdated(startValue, endValue, true, false) if case .began = gestureRecognizer.state { transition = .easeInOut(duration: 0.25) } case .ended, .cancelled: self.isPanningHandle = false - component.endPositionUpdated(value, true) + component.trimUpdated(startValue, endValue, true, true) transition = .easeInOut(duration: 0.25) default: break diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 254e0a6e6e..1017c1ab4d 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -376,7 +376,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon switch privacy { case let .story(storyPrivacy, _): let _ = self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: storyPrivacy).start() - Queue.mainQueue().after(0.2, { [weak chatListController] in + Queue.mainQueue().after(0.3, { [weak chatListController] in chatListController?.animateStoryUploadRipple() }) case let .message(peerIds, timeout): @@ -457,7 +457,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } if case let .story(storyPrivacy, _) = privacy { let _ = self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: storyPrivacy).start() - Queue.mainQueue().after(0.2, { [weak chatListController] in + Queue.mainQueue().after(0.3, { [weak chatListController] in chatListController?.animateStoryUploadRipple() }) } else { @@ -468,7 +468,9 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } dismissCameraImpl?() - commit() + Queue.mainQueue().after(0.1) { + commit() + } } ) controller.cancelled = { showDraftTooltip in From 57eceb0aefc44165d5f89bdf845860b1afc67f90 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 27 May 2023 19:39:09 +0400 Subject: [PATCH 3/6] Camera and editor improvements --- submodules/Camera/Sources/Camera.swift | 20 +- submodules/Camera/Sources/CameraDevice.swift | 26 +- submodules/Camera/Sources/CameraOutput.swift | 25 +- .../Camera/Sources/CameraPreviewView.swift | 4 + submodules/Camera/Sources/VideoRecorder.swift | 595 +++++++++++++----- .../CameraScreen/Sources/CameraScreen.swift | 19 +- .../Sources/MediaEditorScreen.swift | 2 +- 7 files changed, 495 insertions(+), 196 deletions(-) diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index e57823a79a..3ac78d7fe3 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -142,8 +142,17 @@ private final class CameraContext { self.session.stopRunning() } - func focus(at point: CGPoint) { - self.device.setFocusPoint(point, focusMode: .continuousAutoFocus, exposureMode: .continuousAutoExposure, monitorSubjectAreaChange: true) + func focus(at point: CGPoint, autoFocus: Bool) { + let focusMode: AVCaptureDevice.FocusMode + let exposureMode: AVCaptureDevice.ExposureMode + if autoFocus { + focusMode = .continuousAutoFocus + exposureMode = .continuousAutoExposure + } else { + focusMode = .autoFocus + exposureMode = .autoExpose + } + self.device.setFocusPoint(point, focusMode: focusMode, exposureMode: exposureMode, monitorSubjectAreaChange: true) } func setFps(_ fps: Float64) { @@ -276,6 +285,9 @@ public final class Camera { self.metrics = Camera.Metrics(model: DeviceModel.current) let session = AVCaptureSession() + session.usesApplicationAudioSession = true + session.automaticallyConfiguresApplicationAudioSession = false + session.automaticallyConfiguresCaptureDeviceForWideColor = false if let previewView { previewView.session = session } @@ -373,10 +385,10 @@ public final class Camera { } } - public func focus(at point: CGPoint) { + public func focus(at point: CGPoint, autoFocus: Bool = true) { self.queue.async { if let context = self.contextRef?.takeUnretainedValue() { - context.focus(at: point) + context.focus(at: point, autoFocus: autoFocus) } } } diff --git a/submodules/Camera/Sources/CameraDevice.swift b/submodules/Camera/Sources/CameraDevice.swift index 816ce061d9..880e4f35b4 100644 --- a/submodules/Camera/Sources/CameraDevice.swift +++ b/submodules/Camera/Sources/CameraDevice.swift @@ -8,9 +8,21 @@ private let defaultFPS: Double = 30.0 final class CameraDevice { var position: Camera.Position = .back + deinit { + if let videoDevice = self.videoDevice { + self.unsubscribeFromChanges(videoDevice) + } + } + public private(set) var videoDevice: AVCaptureDevice? = nil { didSet { + if let previousVideoDevice = oldValue { + self.unsubscribeFromChanges(previousVideoDevice) + } self.videoDevicePromise.set(.single(self.videoDevice)) + if let videoDevice = self.videoDevice { + self.subscribeForChanges(videoDevice) + } } } private var videoDevicePromise = Promise() @@ -93,12 +105,12 @@ final class CameraDevice { } } - private func subscribeForChanges() { - NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaChanged), name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: self.videoDevice) + private func subscribeForChanges(_ device: AVCaptureDevice) { + NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaChanged), name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: device) } - private func unsubscribeFromChanges() { - NotificationCenter.default.removeObserver(self, name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: self.videoDevice) + private func unsubscribeFromChanges(_ device: AVCaptureDevice) { + NotificationCenter.default.removeObserver(self, name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: device) } @objc private func subjectAreaChanged() { @@ -171,6 +183,12 @@ final class CameraDevice { device.focusPointOfInterest = point device.focusMode = focusMode } + + device.isSubjectAreaChangeMonitoringEnabled = monitorSubjectAreaChange + + if abs(device.exposureTargetBias) > 0.0 { + device.setExposureTargetBias(0.0) + } } } diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index de12b7f7d6..6f3b2d0e58 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -109,11 +109,12 @@ final class CameraOutput: NSObject { func configureVideoStabilization() { if let videoDataOutputConnection = self.videoOutput.connection(with: .video), videoDataOutputConnection.isVideoStabilizationSupported { - if #available(iOS 13.0, *) { - videoDataOutputConnection.preferredVideoStabilizationMode = .cinematicExtended - } else { - videoDataOutputConnection.preferredVideoStabilizationMode = .cinematic - } + videoDataOutputConnection.preferredVideoStabilizationMode = .standard +// if #available(iOS 13.0, *) { +// videoDataOutputConnection.preferredVideoStabilizationMode = .cinematicExtended +// } else { +// videoDataOutputConnection.preferredVideoStabilizationMode = .cinematic +// } } } @@ -178,7 +179,7 @@ final class CameraOutput: NSObject { let outputFileName = NSUUID().uuidString let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4" let outputFileURL = URL(fileURLWithPath: outputFilePath) - let videoRecorder = VideoRecorder(preset: MediaPreset(videoSettings: videoSettings, audioSettings: audioSettings), videoTransform: CGAffineTransform(rotationAngle: .pi / 2.0), fileUrl: outputFileURL, completion: { [weak self] result in + let videoRecorder = VideoRecorder(configuration: VideoRecorder.Configuration(videoSettings: videoSettings, audioSettings: audioSettings), videoTransform: CGAffineTransform(rotationAngle: .pi / 2.0), fileUrl: outputFileURL, completion: { [weak self] result in if case .success = result { self?.recordingCompletionPipe.putNext(outputFilePath) } else { @@ -186,7 +187,8 @@ final class CameraOutput: NSObject { } }) - videoRecorder.start() + + videoRecorder?.start() self.videoRecorder = videoRecorder return Signal { subscriber in @@ -244,13 +246,8 @@ extension CameraOutput: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureA // self.processSampleBuffer?(finalVideoPixelBuffer, connection) // } - if let videoRecorder = self.videoRecorder, videoRecorder.isRecording || videoRecorder.isStopping { - let mediaType = sampleBuffer.type - if mediaType == kCMMediaType_Video { - videoRecorder.appendVideo(sampleBuffer: sampleBuffer) - } else if mediaType == kCMMediaType_Audio { - videoRecorder.appendAudio(sampleBuffer: sampleBuffer) - } + if let videoRecorder = self.videoRecorder, videoRecorder.isRecording { + videoRecorder.appendSampleBuffer(sampleBuffer) } } diff --git a/submodules/Camera/Sources/CameraPreviewView.swift b/submodules/Camera/Sources/CameraPreviewView.swift index 3361f257be..74ae7b8b3b 100644 --- a/submodules/Camera/Sources/CameraPreviewView.swift +++ b/submodules/Camera/Sources/CameraPreviewView.swift @@ -108,6 +108,10 @@ public class CameraSimplePreviewView: UIView { } |> distinctUntilChanged } + + public func cameraPoint(for location: CGPoint) -> CGPoint { + return self.videoPreviewLayer.captureDevicePointConverted(fromLayerPoint: location) + } } public class CameraPreviewView: MTKView { diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index 234957074c..cd67763227 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -1,129 +1,439 @@ import Foundation import AVFoundation import SwiftSignalKit +import TelegramCore -struct MediaPreset { - var videoSettings: [String: Any] - var audioSettings: [String: Any] - - init(videoSettings: [String: Any], audioSettings: [String: Any]) { - self.videoSettings = videoSettings - self.audioSettings = audioSettings - } - - var hasAudio: Bool { - return !self.audioSettings.isEmpty +private extension CMSampleBuffer { + var endTime: CMTime { + let presentationTime = CMSampleBufferGetPresentationTimeStamp(self) + let duration = CMSampleBufferGetDuration(self) + return presentationTime + duration } } -final class VideoRecorder { +private final class VideoRecorderImpl { + public enum RecorderError: LocalizedError { + case generic + case avError(Error) + + public var errorDescription: String? { + switch self { + case .generic: + return "Error" + case let .avError(error): + return error.localizedDescription + } + } + } + + private let queue = DispatchQueue(label: "VideoRecorder") + + private var assetWriter: AVAssetWriter + private var videoInput: AVAssetWriterInput? + private var audioInput: AVAssetWriterInput? + + private var pendingAudioSampleBuffers: [CMSampleBuffer] = [] + + private var _duration: CMTime = .zero + public var duration: CMTime { + self.queue.sync { _duration } + } + + private var lastVideoSampleTime: CMTime = .invalid + private var recordingStartSampleTime: CMTime = .invalid + private var recordingStopSampleTime: CMTime = .invalid + + private let configuration: VideoRecorder.Configuration + private let videoTransform: CGAffineTransform + private let url: URL + fileprivate var completion: (Bool) -> Void = { _ in } + + private let error = Atomic(value: nil) + + private var stopped = false + private var hasAllVideoBuffers = false + private var hasAllAudioBuffers = false + + public init?(configuration: VideoRecorder.Configuration, videoTransform: CGAffineTransform, fileUrl: URL) { + self.configuration = configuration + self.videoTransform = videoTransform + self.url = fileUrl + + try? FileManager.default.removeItem(at: url) + guard let assetWriter = try? AVAssetWriter(url: url, fileType: .mp4) else { + return nil + } + self.assetWriter = assetWriter + self.assetWriter.shouldOptimizeForNetworkUse = false + } + + private func hasError() -> Error? { + return self.error.with { $0 } + } + + public func start() { + self.queue.async { + self.recordingStartSampleTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + } + } + + public func appendVideoSampleBuffer(_ sampleBuffer: CMSampleBuffer) { + if let _ = self.hasError() { + return + } + + guard let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer), CMFormatDescriptionGetMediaType(formatDescription) == kCMMediaType_Video else { + return + } + + let presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + self.queue.async { + guard !self.stopped && self.error.with({ $0 }) == nil else { + return + } + + var failed = false + if self.videoInput == nil { + let videoSettings = self.configuration.videoSettings + if self.assetWriter.canApply(outputSettings: videoSettings, forMediaType: .video) { + let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings, sourceFormatHint: formatDescription) + videoInput.expectsMediaDataInRealTime = true + videoInput.transform = self.videoTransform + if self.assetWriter.canAdd(videoInput) { + self.assetWriter.add(videoInput) + self.videoInput = videoInput + } else { + failed = true + } + } else { + failed = true + } + } + + if failed { + print("error") + return + } + + if self.assetWriter.status == .unknown { + if sampleBuffer.presentationTimestamp < self.recordingStartSampleTime { + return + } + if !self.assetWriter.startWriting() { + if let error = self.assetWriter.error { + self.transitionToFailedStatus(error: .avError(error)) + return + } + } + + self.assetWriter.startSession(atSourceTime: presentationTime) + self.recordingStartSampleTime = presentationTime + self.lastVideoSampleTime = presentationTime + } + + if self.assetWriter.status == .writing { + if self.recordingStopSampleTime != .invalid && sampleBuffer.presentationTimestamp > self.recordingStopSampleTime { + self.hasAllVideoBuffers = true + self.maybeFinish() + return + } + + if let videoInput = self.videoInput, videoInput.isReadyForMoreMediaData { + if videoInput.append(sampleBuffer) { + self.lastVideoSampleTime = presentationTime + let startTime = self.recordingStartSampleTime + let duration = presentationTime - startTime + self._duration = duration + } else { + print("error") + } + if !self.tryAppendingPendingAudioBuffers() { + self.transitionToFailedStatus(error: .generic) + } + } + } + } + } + + public func appendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) { + if let _ = self.hasError() { + return + } + + guard let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer), CMFormatDescriptionGetMediaType(formatDescription) == kCMMediaType_Audio else { + return + } + + self.queue.async { + guard !self.stopped && self.error.with({ $0 }) == nil else { + return + } + + var failed = false + if self.audioInput == nil { + var audioSettings = self.configuration.audioSettings + if let currentAudioStreamBasicDescription = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) { + audioSettings[AVSampleRateKey] = currentAudioStreamBasicDescription.pointee.mSampleRate + audioSettings[AVNumberOfChannelsKey] = currentAudioStreamBasicDescription.pointee.mChannelsPerFrame + } + + var audioChannelLayoutSize: Int = 0 + let currentChannelLayout = CMAudioFormatDescriptionGetChannelLayout(formatDescription, sizeOut: &audioChannelLayoutSize) + let currentChannelLayoutData: Data + if let currentChannelLayout = currentChannelLayout, audioChannelLayoutSize > 0 { + currentChannelLayoutData = Data(bytes: currentChannelLayout, count: audioChannelLayoutSize) + } else { + currentChannelLayoutData = Data() + } + audioSettings[AVChannelLayoutKey] = currentChannelLayoutData + + if self.assetWriter.canApply(outputSettings: audioSettings, forMediaType: .audio) { + let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings, sourceFormatHint: formatDescription) + audioInput.expectsMediaDataInRealTime = true + if self.assetWriter.canAdd(audioInput) { + self.assetWriter.add(audioInput) + self.audioInput = audioInput + } else { + failed = true + } + } else { + failed = true + } + } + + if failed { + print("error") + return + } + + if self.assetWriter.status == .writing { + if sampleBuffer.presentationTimestamp < self.recordingStartSampleTime { + return + } + if self.recordingStopSampleTime != .invalid && sampleBuffer.presentationTimestamp > self.recordingStopSampleTime { + self.hasAllAudioBuffers = true + self.maybeFinish() + return + } + var result = false + if self.tryAppendingPendingAudioBuffers() { + if self.tryAppendingAudioSampleBuffer(sampleBuffer) { + result = true + } + } + if !result { + self.transitionToFailedStatus(error: .generic) + } + } + } + } + + public func cancelRecording(completion: @escaping () -> Void) { + self.queue.async { + if self.stopped { + DispatchQueue.main.async { + completion() + } + return + } + self.stopped = true + self.pendingAudioSampleBuffers = [] + if self.assetWriter.status == .writing { + self.assetWriter.cancelWriting() + } + let fileManager = FileManager() + try? fileManager.removeItem(at: self.url) + DispatchQueue.main.async { + completion() + } + } + } + + public var isRecording: Bool { + self.queue.sync { !(self.hasAllVideoBuffers && self.hasAllAudioBuffers) } + } + + public func stopRecording() { + self.queue.async { + self.recordingStopSampleTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + } + } + + public func maybeFinish() { + self.queue.async { + guard self.hasAllVideoBuffers && self.hasAllVideoBuffers else { + return + } + self.stopped = true + self.finish() + } + } + + public func finish() { + self.queue.async { + let completion = self.completion + if self.recordingStopSampleTime == .invalid { + DispatchQueue.main.async { + completion(false) + } + return + } + + if let _ = self.error.with({ $0 }) { + DispatchQueue.main.async { + completion(false) + } + return + } + + if !self.tryAppendingPendingAudioBuffers() { + DispatchQueue.main.async { + completion(false) + } + return + } + + if self.assetWriter.status == .writing { + self.assetWriter.finishWriting { + if let _ = self.assetWriter.error { + DispatchQueue.main.async { + completion(false) + } + } else { + DispatchQueue.main.async { + completion(true) + } + } + } + } else if let _ = self.assetWriter.error { + DispatchQueue.main.async { + completion(true) + } + } else { + DispatchQueue.main.async { + completion(true) + } + } + } + } + + private func tryAppendingPendingAudioBuffers() -> Bool { + dispatchPrecondition(condition: .onQueue(self.queue)) + guard self.pendingAudioSampleBuffers.count > 0 else { + return true + } + + var result = true + let (sampleBuffersToAppend, pendingSampleBuffers) = self.pendingAudioSampleBuffers.stableGroup(using: { $0.endTime <= self.lastVideoSampleTime }) + for sampleBuffer in sampleBuffersToAppend { + if !self.internalAppendAudioSampleBuffer(sampleBuffer) { + result = false + break + } + } + self.pendingAudioSampleBuffers = pendingSampleBuffers + return result + } + + private func tryAppendingAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) -> Bool { + dispatchPrecondition(condition: .onQueue(self.queue)) + + var result = true + if sampleBuffer.endTime > self.lastVideoSampleTime { + self.pendingAudioSampleBuffers.append(sampleBuffer) + } else { + result = self.internalAppendAudioSampleBuffer(sampleBuffer) + } + return result + } + + private func internalAppendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) -> Bool { + if let audioInput = self.audioInput, audioInput.isReadyForMoreMediaData { + if !audioInput.append(sampleBuffer) { + if let _ = self.assetWriter.error { + return false + } + } + } else { + + } + return true + } + + private func transitionToFailedStatus(error: RecorderError) { + let _ = self.error.modify({ _ in return error }) + } +} + +private extension Sequence { + func stableGroup(using predicate: (Element) throws -> Bool) rethrows -> ([Element], [Element]) { + var trueGroup: [Element] = [] + var falseGroup: [Element] = [] + for element in self { + if try predicate(element) { + trueGroup.append(element) + } else { + falseGroup.append(element) + } + } + return (trueGroup, falseGroup) + } +} + +public final class VideoRecorder { + var duration: Double? { + return self.impl.duration.seconds + } + enum Result { enum Error { case generic } case success + case initError(Error) case writeError(Error) case finishError(Error) } + struct Configuration { + var videoSettings: [String: Any] + var audioSettings: [String: Any] + + init(videoSettings: [String: Any], audioSettings: [String: Any]) { + self.videoSettings = videoSettings + self.audioSettings = audioSettings + } + + var hasAudio: Bool { + return !self.audioSettings.isEmpty + } + } + + private let impl: VideoRecorderImpl + fileprivate let configuration: Configuration + fileprivate let videoTransform: CGAffineTransform + fileprivate let fileUrl: URL private let completion: (Result) -> Void - private let queue = Queue() - private var assetWriter: AVAssetWriter? + public var isRecording: Bool { + return self.impl.isRecording + } - private var videoInput: AVAssetWriterInput? - private var audioInput: AVAssetWriterInput? - - private let preset: MediaPreset - private let videoTransform: CGAffineTransform - private let fileUrl: URL - - private (set) var isRecording = false - private (set) var isStopping = false - private var finishedWriting = false - - private var captureStartTimestamp: Double? - private var firstVideoTimestamp: CMTime? - private var lastVideoTimestamp: CMTime? - private var lastAudioTimestamp: CMTime? - - private var pendingAudioBuffers: [CMSampleBuffer] = [] - - init(preset: MediaPreset, videoTransform: CGAffineTransform, fileUrl: URL, completion: @escaping (Result) -> Void) { - self.preset = preset + init?(configuration: Configuration, videoTransform: CGAffineTransform, fileUrl: URL, completion: @escaping (Result) -> Void) { + self.configuration = configuration self.videoTransform = videoTransform self.fileUrl = fileUrl self.completion = completion - } - - func start() { - self.queue.async { - guard self.assetWriter == nil else { - return - } - - self.captureStartTimestamp = CFAbsoluteTimeGetCurrent() - - guard let assetWriter = try? AVAssetWriter(url: self.fileUrl, fileType: .mp4) else { - return - } - - let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: self.preset.videoSettings) - videoInput.expectsMediaDataInRealTime = true - videoInput.transform = self.videoTransform - if assetWriter.canAdd(videoInput) { - assetWriter.add(videoInput) - } - - let audioInput: AVAssetWriterInput? - if self.preset.hasAudio { - audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: self.preset.audioSettings) - audioInput!.expectsMediaDataInRealTime = true - if assetWriter.canAdd(audioInput!) { - assetWriter.add(audioInput!) - } - } else { - audioInput = nil - } - - self.assetWriter = assetWriter - self.videoInput = videoInput - self.audioInput = audioInput - - self.isRecording = true - - //assetWriter.startWriting() - } - } - - func stop() { - self.queue.async { - guard let captureStartTimestamp = self.captureStartTimestamp, abs(CFAbsoluteTimeGetCurrent() - captureStartTimestamp) > 0.5 else { - return - } - - self.isStopping = true - - if self.audioInput == nil { - self.finish() - } - } - } - - private func finish() { - guard let assetWriter = self.assetWriter else { - return - } - self.queue.async { - self.isRecording = false - self.isStopping = false - - assetWriter.finishWriting { - self.finishedWriting = true - - if case .completed = assetWriter.status { + guard let impl = VideoRecorderImpl(configuration: configuration, videoTransform: videoTransform, fileUrl: fileUrl) else { + completion(.initError(.generic)) + return nil + } + self.impl = impl + impl.completion = { [weak self] success in + if let self { + if success { self.completion(.success) } else { self.completion(.finishError(.generic)) @@ -132,76 +442,25 @@ final class VideoRecorder { } } - func appendVideo(sampleBuffer: CMSampleBuffer) { - self.queue.async { - guard let assetWriter = self.assetWriter, let videoInput = self.videoInput, (self.isRecording || self.isStopping) && !self.finishedWriting else { - return - } - let timestamp = sampleBuffer.presentationTimestamp - if let startTimestamp = self.captureStartTimestamp, timestamp.seconds < startTimestamp { - return - } - - switch assetWriter.status { - case .unknown: - break - case .writing: - if self.firstVideoTimestamp == nil { - self.firstVideoTimestamp = timestamp - assetWriter.startSession(atSourceTime: timestamp) - } - while !videoInput.isReadyForMoreMediaData { - RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) - } - - if videoInput.append(sampleBuffer) { - self.lastVideoTimestamp = timestamp - } - - if self.audioInput != nil && self.isStopping, let lastVideoTimestamp = self.lastAudioTimestamp, let lastAudioTimestamp = self.lastAudioTimestamp, lastVideoTimestamp >= lastAudioTimestamp { - self.finish() - } - case .failed: - self.isRecording = false - self.completion(.writeError(.generic)) - default: - break - } - } + func start() { + self.impl.start() } - func appendAudio(sampleBuffer: CMSampleBuffer) { - self.queue.async { - guard let _ = self.assetWriter, let audioInput = self.audioInput, !self.isStopping && !self.finishedWriting else { - return - } - let timestamp = sampleBuffer.presentationTimestamp - - if let _ = self.firstVideoTimestamp { - if !self.pendingAudioBuffers.isEmpty { - for buffer in self.pendingAudioBuffers { - audioInput.append(buffer) - } - self.pendingAudioBuffers.removeAll() - } - - while !audioInput.isReadyForMoreMediaData { - RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) - } - - if audioInput.append(sampleBuffer) { - self.lastAudioTimestamp = timestamp - } - } else { - self.pendingAudioBuffers.append(sampleBuffer) - } - } + func stop() { + self.impl.stopRecording() } - var duration: Double? { - guard let firstTimestamp = self.firstVideoTimestamp, let lastTimestamp = self.lastVideoTimestamp else { - return nil + func appendSampleBuffer(_ sampleBuffer: CMSampleBuffer) { + guard let formatDescriptor = CMSampleBufferGetFormatDescription(sampleBuffer) else { + return + } + let type = CMFormatDescriptionGetMediaType(formatDescriptor) + if type == kCMMediaType_Video { + self.impl.appendVideoSampleBuffer(sampleBuffer) + } else if type == kCMMediaType_Audio { + if self.configuration.hasAudio { + self.impl.appendAudioSampleBuffer(sampleBuffer) + } } - return (lastTimestamp - firstTimestamp).seconds } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index d58748b840..b7598ed110 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -840,7 +840,10 @@ public class CameraScreen: ViewController { let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) self.effectivePreviewView.addGestureRecognizer(panGestureRecognizer) - self.camera.focus(at: CGPoint(x: 0.5, y: 0.5)) + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.effectivePreviewView.addGestureRecognizer(tapGestureRecognizer) + + self.camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true) self.camera.startCapture() } @@ -856,8 +859,6 @@ public class CameraScreen: ViewController { } } - private var previewInitialPosition: CGPoint? - private var controlsInitialPosition: CGPoint? @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { guard let controller = self.controller else { return @@ -865,8 +866,7 @@ public class CameraScreen: ViewController { let translation = gestureRecognizer.translation(in: gestureRecognizer.view) switch gestureRecognizer.state { case .began: - self.previewInitialPosition = self.previewContainerView.center - self.controlsInitialPosition = self.componentHost.view?.center + break case .changed: if !"".isEmpty { @@ -888,6 +888,15 @@ public class CameraScreen: ViewController { break } } + + @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + guard let previewView = self.simplePreviewView else { + return + } + let location = gestureRecognizer.location(in: previewView) + let point = previewView.cameraPoint(for: location) + self.camera.focus(at: point, autoFocus: false) + } func animateIn() { self.backgroundView.alpha = 0.0 diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 85ab785831..8f60ae7242 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1160,7 +1160,7 @@ public final class MediaEditorScreen: ViewController { rotateGestureRecognizer.delegate = self self.previewContainerView.addGestureRecognizer(rotateGestureRecognizer) - let tapGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) self.previewContainerView.addGestureRecognizer(tapGestureRecognizer) self.interaction = DrawingToolsInteraction( From 0ef81e29280dc5da27c67039f5e1bac0a80fffde Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 27 May 2023 20:06:12 +0400 Subject: [PATCH 4/6] Fix build --- .../Sources/MediaEditorScreen.swift | 32 +++++++++++++++---- .../Sources/TelegramRootController.swift | 2 +- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 8f60ae7242..d4f2d9e3c0 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -39,6 +39,7 @@ final class MediaEditorScreenComponent: Component { let context: AccountContext let mediaEditor: MediaEditor? let privacy: MediaEditorResultPrivacy + let selectedEntity: DrawingEntity? let openDrawing: (DrawingScreenType) -> Void let openTools: () -> Void @@ -46,12 +47,14 @@ final class MediaEditorScreenComponent: Component { context: AccountContext, mediaEditor: MediaEditor?, privacy: MediaEditorResultPrivacy, + selectedEntity: DrawingEntity?, openDrawing: @escaping (DrawingScreenType) -> Void, openTools: @escaping () -> Void ) { self.context = context self.mediaEditor = mediaEditor self.privacy = privacy + self.selectedEntity = selectedEntity self.openDrawing = openDrawing self.openTools = openTools } @@ -63,6 +66,9 @@ final class MediaEditorScreenComponent: Component { if lhs.privacy != rhs.privacy { return false } + if lhs.selectedEntity?.uuid != rhs.selectedEntity?.uuid { + return false + } return true } @@ -696,6 +702,8 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: availableSize.width, height: 200.0) ) + var isEditingTextEntity = false + var inputPanelAlpha: CGFloat = 1.0 var inputPanelOffset: CGFloat = 0.0 var inputPanelBottomInset: CGFloat = scrubberBottomInset if environment.inputHeight > 0.0 { @@ -708,6 +716,12 @@ final class MediaEditorScreenComponent: Component { self.addSubview(inputPanelView) } transition.setFrame(view: inputPanelView, frame: inputPanelFrame) + + if inputPanelOffset > 0.0 && component.selectedEntity != nil { + isEditingTextEntity = true + inputPanelAlpha = 0.0 + } + transition.setAlpha(view: inputPanelView, alpha: inputPanelAlpha) } let privacyText: String @@ -759,8 +773,8 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: privacyButtonView, position: privacyButtonFrame.center) transition.setBounds(view: privacyButtonView, bounds: CGRect(origin: .zero, size: privacyButtonFrame.size)) - transition.setScale(view: privacyButtonView, scale: self.inputPanelExternalState.isEditing ? 0.01 : 1.0) - transition.setAlpha(view: privacyButtonView, alpha: self.inputPanelExternalState.isEditing ? 0.0 : 1.0) + transition.setScale(view: privacyButtonView, scale: self.inputPanelExternalState.isEditing || isEditingTextEntity ? 0.01 : 1.0) + transition.setAlpha(view: privacyButtonView, alpha: self.inputPanelExternalState.isEditing || isEditingTextEntity ? 0.0 : 1.0) } let saveButtonSize = self.saveButton.update( @@ -803,8 +817,8 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: saveButtonView, position: saveButtonFrame.center) transition.setBounds(view: saveButtonView, bounds: CGRect(origin: .zero, size: saveButtonFrame.size)) - transition.setScale(view: saveButtonView, scale: self.inputPanelExternalState.isEditing ? 0.01 : 1.0) - transition.setAlpha(view: saveButtonView, alpha: self.inputPanelExternalState.isEditing ? 0.0 : 1.0) + transition.setScale(view: saveButtonView, scale: self.inputPanelExternalState.isEditing || isEditingTextEntity ? 0.01 : 1.0) + transition.setAlpha(view: saveButtonView, alpha: self.inputPanelExternalState.isEditing || isEditingTextEntity ? 0.0 : 1.0) } if let playerState = state.playerState, playerState.hasAudio { @@ -1161,6 +1175,7 @@ public final class MediaEditorScreen: ViewController { self.previewContainerView.addGestureRecognizer(rotateGestureRecognizer) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + tapGestureRecognizer.delegate = self self.previewContainerView.addGestureRecognizer(tapGestureRecognizer) self.interaction = DrawingToolsInteraction( @@ -1523,6 +1538,7 @@ public final class MediaEditorScreen: ViewController { context: self.context, mediaEditor: self.mediaEditor, privacy: controller.state.privacy, + selectedEntity: self.entitiesView.selectedEntityView?.entity, openDrawing: { [weak self] mode in if let self { if self.entitiesView.hasSelection { @@ -1613,7 +1629,11 @@ public final class MediaEditorScreen: ViewController { var bottomInputOffset: CGFloat = 0.0 if let inputHeight = layout.inputHeight, inputHeight > 0.0 { - bottomInputOffset = inputHeight - topInset - 17.0 + if self.entitiesView.selectedEntityView != nil { + bottomInputOffset = inputHeight / 2.0 + } else { + bottomInputOffset = inputHeight - topInset - 17.0 + } } transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size)) @@ -1629,7 +1649,7 @@ public final class MediaEditorScreen: ViewController { transition.setFrame(view: self.entitiesContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) transition.setFrame(view: self.gradientView, frame: CGRect(origin: .zero, size: previewFrame.size)) transition.setFrame(view: self.drawingView, frame: CGRect(origin: .zero, size: previewFrame.size)) - + transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) self.interaction?.containerLayoutUpdated(layout: layout, transition: transition) diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 1017c1ab4d..63a6fe1e41 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -436,7 +436,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon let _ = enqueueMessagesToMultiplePeers( account: self.context.account, peerIds: peerIds, threadIds: [:], - messages: [.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)]).start() + messages: [.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)]).start() } } case let .video(content, _, values, duration, dimensions, caption): From 69676bae7c1d121743f2cfb3051a5eae3ba0f5b6 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 28 May 2023 05:45:12 +0400 Subject: [PATCH 5/6] Camera and editor improvements --- .../AccountContext/Sources/AccountContext.swift | 2 +- .../ChatListUI/Sources/ChatListController.swift | 4 ++-- .../CameraScreen/Sources/CameraScreen.swift | 4 +++- .../Sources/MediaEditorScreen.swift | 2 +- .../MessageInputActionButtonComponent.swift | 14 ++++++++++++++ .../Sources/TelegramRootController.swift | 12 +++++++++++- 6 files changed, 32 insertions(+), 6 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 37c5051f5e..16080fa8ed 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -792,7 +792,7 @@ public struct StoryCameraTransitionInCoordinator { public protocol TelegramRootControllerInterface: NavigationController { @discardableResult - func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? + func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? } public protocol SharedAccountContext: AnyObject { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 06e99869bf..43ed04b506 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2523,7 +2523,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if let peer, peer.id == self.context.account.peerId, storyContentState.slice == nil { if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - let coordinator = rootController.openStoryCamera(transitionIn: nil, transitionOut: { [weak self] finished in + let coordinator = rootController.openStoryCamera(transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] finished in guard let self else { return nil } @@ -4951,7 +4951,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let current = self.storyCameraTransitionInCoordinator { coordinator = current } else { - coordinator = rootController.openStoryCamera(transitionIn: nil, transitionOut: { [weak self] finished in + coordinator = rootController.openStoryCamera(transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] finished in guard let self else { return nil } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index b7598ed110..4b242cfb85 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1228,7 +1228,8 @@ public class CameraScreen: ViewController { } } fileprivate let completion: (Signal, ResultTransition?) -> Void - + public var transitionedIn: () -> Void = {} + private var audioSessionDisposable: Disposable? public init( @@ -1393,6 +1394,7 @@ public class CameraScreen: ViewController { if let self, let navigationController = self.navigationController as? NavigationController { navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) self.node.requestUpdateLayout(hasAppeared: true, transition: .immediate) + self.transitionedIn() } }) } else { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index d4f2d9e3c0..c3b4d9f6cc 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1241,6 +1241,7 @@ public final class MediaEditorScreen: ViewController { if self.entitiesView.hasSelection { self.entitiesView.selectEntity(nil) } + self.view.endEditing(true) } func animateIn() { @@ -1485,7 +1486,6 @@ public final class MediaEditorScreen: ViewController { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) if result == self.componentHost.view { - self.controller?.view.endEditing(true) let point = self.view.convert(point, to: self.previewContainerView) return self.previewContainerView.hitTest(point, with: event) } diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift index a0da688d27..65ed6be20c 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift @@ -223,6 +223,20 @@ public final class MessageInputActionButtonComponent: Component { if self.sendIconView.image == nil || previousComponent?.mode.iconName != component.mode.iconName { if let iconName = component.mode.iconName { self.sendIconView.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: .white) + } else if case .apply = component.mode { + self.sendIconView.image = generateImage(CGSize(width: 33.0, height: 33.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + if let image = UIImage(bundleImageName: "Media Editor/Apply"), let cgImage = image.cgImage { + context.setBlendMode(.clear) + context.clip(to: CGRect(origin: CGPoint(x: -4.0 + UIScreenPixel, y: -3.0 - UIScreenPixel), size: CGSize(width: 40.0, height: 40.0)), mask: cgImage) + context.fill(CGRect(origin: .zero, size: size)) + } + }) + } else if case .none = component.mode { + self.sendIconView.image = nil } else { self.sendIconView.image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 63a6fe1e41..1b634103f0 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -199,6 +199,14 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } let coordinator = self.openStoryCamera( transitionIn: nil, + transitionedIn: { [weak self] in + guard let self, let rootTabController = self.rootTabController else { + return + } + if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController}) { + rootTabController.selectedIndex = index + } + }, transitionOut: { [weak self] finished in guard let self else { return nil @@ -275,7 +283,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } @discardableResult - public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? { + public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? { guard let controller = self.viewControllers.last as? ViewController else { return nil } @@ -370,6 +378,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } if let chatListController = self.chatListController as? ChatListControllerImpl { + chatListController.scrollToTop?() switch mediaResult { case let .image(image, dimensions, caption): if let imageData = compressImageToJPEG(image, quality: 0.6) { @@ -482,6 +491,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon presentImpl?(controller) } ) + cameraController.transitionedIn = transitionedIn controller.push(cameraController) presentImpl = { [weak cameraController] c in if let navigationController = cameraController?.navigationController as? NavigationController { From f11c45070e5e533f6f4077cc9f7708afd443538b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 29 May 2023 16:27:26 +0400 Subject: [PATCH 6/6] Camera and editor improvements --- .../MetalResources/EditorDefault.metal | 21 +----- .../Sources/HistogramCalculationPass.swift | 5 +- .../Sources/MediaEditorComposer.swift | 55 +-------------- .../Sources/MediaEditorVideoExport.swift | 2 +- .../MediaEditor/Sources/RenderPass.swift | 19 +----- .../Sources/VideoTextureSource.swift | 68 +++++++++++++++++-- 6 files changed, 71 insertions(+), 99 deletions(-) diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal index 1e63810031..44039cab78 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal @@ -21,34 +21,17 @@ vertex RasterizerData defaultVertexShader(uint vertexID [[vertex_id]], } fragment half4 defaultFragmentShader(RasterizerData in [[stage_in]], - constant float2 &texCoordScales [[buffer(0)]], texture2d texture [[texture(0)]]) { constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear); - - float scaleX = texCoordScales.x; - float scaleY = texCoordScales.y; - float x = (in.texCoord.x - (1.0 - scaleX) / 2.0) / scaleX; - float y = (in.texCoord.y - (1.0 - scaleY) / 2.0) / scaleY; - if (x < 0 || x > 1 || y < 0 || y > 1) { - return half4(0.0, 0.0, 0.0, 1.0); - } - half3 color = texture.sample(samplr, float2(x, y)).rgb; + half3 color = texture.sample(samplr, in.texCoord).rgb; return half4(color, 1.0); } fragment half histogramPrepareFragmentShader(RasterizerData in [[stage_in]], - constant float2 &texCoordScales [[buffer(0)]], texture2d texture [[texture(0)]]) { constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear); - float scaleX = texCoordScales.x; - float scaleY = texCoordScales.y; - float x = (in.texCoord.x - (1.0 - scaleX) / 2.0) / scaleX; - float y = (in.texCoord.y - (1.0 - scaleY) / 2.0) / scaleY; - if (x < 0 || x > 1 || y < 0 || y > 1) { - return 0.0; - } - half3 color = texture.sample(samplr, float2(x, y)).rgb; + half3 color = texture.sample(samplr, in.texCoord).rgb; half luma = color.r * 0.3 + color.g * 0.59 + color.b * 0.11; return luma; } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift index db830bd4d2..2fc2302d39 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift @@ -79,10 +79,7 @@ final class HistogramCalculationPass: DefaultRenderPass { ) renderCommandEncoder.setFragmentTexture(input, index: 0) - - var texCoordScales = simd_float2(x: 1.0, y: 1.0) - renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout.stride, index: 0) - + self.encodeDefaultCommands(using: renderCommandEncoder) renderCommandEncoder.endEncoding() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index e3d1fd400d..191124d303 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -56,7 +56,6 @@ final class MediaEditorComposer { self.colorSpace = colorSpace self.renderer.addRenderChain(self.renderChain) - self.renderer.addRenderPass(ComposerRenderPass()) if let gradientColors = values.gradientColors, let image = mediaEditorGenerateGradientImage(size: dimensions, colors: gradientColors) { self.gradientImage = CIImage(image: image, options: [.colorSpace: self.colorSpace])!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) @@ -106,7 +105,7 @@ final class MediaEditorComposer { if var compositedImage { let scale = self.outputDimensions.width / self.dimensions.width compositedImage = compositedImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) - + self.ciContext?.render(compositedImage, to: pixelBuffer) completion(pixelBuffer) } else { @@ -568,55 +567,3 @@ private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: return CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: colorSpace]) } - -final class ComposerRenderPass: DefaultRenderPass { - fileprivate var cachedTexture: MTLTexture? - - override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device) - - let width = input.width - let height = input.height - - if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height { - let textureDescriptor = MTLTextureDescriptor() - textureDescriptor.textureType = .type2D - textureDescriptor.width = width - textureDescriptor.height = height - textureDescriptor.pixelFormat = input.pixelFormat - textureDescriptor.storageMode = .shared - textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] - guard let texture = device.makeTexture(descriptor: textureDescriptor) else { - return input - } - self.cachedTexture = texture - texture.label = "composerTexture" - } - - let renderPassDescriptor = MTLRenderPassDescriptor() - renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture! - renderPassDescriptor.colorAttachments[0].loadAction = .dontCare - renderPassDescriptor.colorAttachments[0].storeAction = .store - renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) - guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { - return input - } - - renderCommandEncoder.setViewport(MTLViewport( - originX: 0, originY: 0, - width: Double(width), height: Double(height), - znear: -1.0, zfar: 1.0) - ) - - renderCommandEncoder.setFragmentTexture(input, index: 0) - - var texCoordScales = simd_float2(x: 1.0, y: 1.0) - renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout.stride, index: 0) - - self.encodeDefaultCommands(using: renderCommandEncoder) - - renderCommandEncoder.endEncoding() - - return self.cachedTexture! - } -} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 2e5ca44e09..f48230a443 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -326,7 +326,7 @@ public final class MediaEditorVideoExport { if (videoTracks.count > 0) { var sourceFrameRate: Float = 0.0 let outputSettings: [String: Any] = [ - kCVPixelBufferPixelFormatTypeKey as String: [kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange], + kCVPixelBufferPixelFormatTypeKey as String: [kCVPixelFormatType_420YpCbCr8BiPlanarFullRange], AVVideoColorPropertiesKey: [ AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift index 111c9cae2a..45344dabd0 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift @@ -147,23 +147,8 @@ final class OutputRenderPass: DefaultRenderPass { width: Double(drawableSize.width), height: Double(drawableSize.height), znear: -1.0, zfar: 1.0)) - do { - var texCoordScales = simd_float2(x: 1.0, y: 1.0) - var scaleFactor = drawableSize.width / CGFloat(input.width) - let textureFitHeight = CGFloat(input.height) * scaleFactor - if textureFitHeight > drawableSize.height { - scaleFactor = drawableSize.height / CGFloat(input.height) - let textureFitWidth = CGFloat(input.width) * scaleFactor - let texCoordsScaleX = textureFitWidth / drawableSize.width - texCoordScales.x = Float(texCoordsScaleX) - } else { - let texCoordsScaleY = textureFitHeight / drawableSize.height - texCoordScales.y = Float(texCoordsScaleY) - } - - renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout.stride, index: 0) - renderCommandEncoder.setFragmentTexture(input, index: 0) - } + + renderCommandEncoder.setFragmentTexture(input, index: 0) self.encodeDefaultCommands(using: renderCommandEncoder) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift index f8f2e18558..508e30210e 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift @@ -210,11 +210,17 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD final class VideoInputPass: DefaultRenderPass { private var cachedTexture: MTLTexture? + private let scalePass = VideoInputScalePass() override var fragmentShaderFunctionName: String { return "bt709ToRGBFragmentShader" } + override func setup(device: MTLDevice, library: MTLLibrary) { + super.setup(device: device, library: library) + self.scalePass.setup(device: device, library: library) + } + func processPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation, textureCache: CVMetalTextureCache, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { func textureFromPixelBuffer(_ pixelBuffer: CVPixelBuffer, pixelFormat: MTLPixelFormat, width: Int, height: Int, plane: Int) -> MTLTexture? { var textureRef : CVMetalTexture? @@ -247,9 +253,6 @@ final class VideoInputPass: DefaultRenderPass { } let (outputWidth, outputHeight) = textureDimensionsForRotation(width: width, height: height, rotation: rotation) -// let outputSize = CGSize(width: outputWidth, height: outputHeight).fitted(CGSize(width: 1920.0, height: 1920.0)) -// outputWidth = Int(outputSize.width) -// outputHeight = Int(outputSize.height) if self.cachedTexture == nil { let textureDescriptor = MTLTextureDescriptor() textureDescriptor.textureType = .type2D @@ -285,6 +288,63 @@ final class VideoInputPass: DefaultRenderPass { renderCommandEncoder.endEncoding() - return self.cachedTexture + var outputTexture = self.cachedTexture + if let texture = outputTexture { + outputTexture = self.scalePass.process(input: texture, device: device, commandBuffer: commandBuffer) + } + return outputTexture + } +} + +final class VideoInputScalePass: DefaultRenderPass { + private var cachedTexture: MTLTexture? + + override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + guard max(input.width, input.height) > 1920 else { + return input + } + self.setupVerticesBuffer(device: device) + + let scaledSize = CGSize(width: input.width, height: input.height).fitted(CGSize(width: 1920.0, height: 1920.0)) + let width = Int(scaledSize.width) + let height = Int(scaledSize.height) + + if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height { + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.width = width + textureDescriptor.height = height + textureDescriptor.pixelFormat = input.pixelFormat + textureDescriptor.storageMode = .private + textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] + guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + return input + } + self.cachedTexture = texture + texture.label = "scaledVideoTexture" + } + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture! + renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + renderPassDescriptor.colorAttachments[0].storeAction = .store + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) + guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + return input + } + + renderCommandEncoder.setViewport(MTLViewport( + originX: 0, originY: 0, + width: Double(width), height: Double(height), + znear: -1.0, zfar: 1.0) + ) + + renderCommandEncoder.setFragmentTexture(input, index: 0) + + self.encodeDefaultCommands(using: renderCommandEncoder) + + renderCommandEncoder.endEncoding() + + return self.cachedTexture! } }