diff --git a/Source/Frameworks/gyroflow/inc/gyroflow.h b/Source/Frameworks/gyroflow/inc/gyroflow.h index bdf2a7a5..78bfb788 100644 --- a/Source/Frameworks/gyroflow/inc/gyroflow.h +++ b/Source/Frameworks/gyroflow/inc/gyroflow.h @@ -35,4 +35,10 @@ const char* processFrame( void *command_queue ); -uint32_t trashCache(); +uint32_t trashCache( + void +); + +const char* importMediaFile( + const char* media_file_path +); diff --git a/Source/Frameworks/gyroflow/src/lib.rs b/Source/Frameworks/gyroflow/src/lib.rs index 913307d1..5cc8e00a 100644 --- a/Source/Frameworks/gyroflow/src/lib.rs +++ b/Source/Frameworks/gyroflow/src/lib.rs @@ -8,7 +8,7 @@ //--------------------------------------------------------- // Local name bindings: //--------------------------------------------------------- -use gyroflow_core::{StabilizationManager, stabilization::*}; +use gyroflow_core::{StabilizationManager, stabilization::*, telemetry_parser::util::VideoMetadata}; use gyroflow_core::gpu::{ BufferDescription, BufferSource, Buffers }; use once_cell::sync::OnceCell; // Provides two new cell-like types, unsync::OnceCell and sync::OnceCell @@ -48,6 +48,66 @@ pub extern "C" fn trashCache() -> u32 { cache.len() as u32 } +//--------------------------------------------------------- +// The "Import Media File" function that gets triggered +// from Objective-C Land: +//--------------------------------------------------------- +#[no_mangle] +pub extern "C" fn importMediaFile( + media_file_path: *const c_char, +) -> *const c_char { + //--------------------------------------------------------- + // Convert the file path to a `&str`: + //--------------------------------------------------------- + let media_file_path_pointer = unsafe { CStr::from_ptr(media_file_path) }; + let media_file_path_string = media_file_path_pointer.to_string_lossy(); + + log::info!("[Gyroflow Toolbox Rust] media_file_path_string: {:?}", media_file_path_string); + + let mut stab = StabilizationManager::default(); + { + //--------------------------------------------------------- + // Find first lens profile database with loaded profiles: + //--------------------------------------------------------- + let lock = MANAGER_CACHE.lock().unwrap(); + for (_, v) in lock.iter() { + if v.lens_profile_db.read().loaded { + stab.lens_profile_db = v.lens_profile_db.clone(); + break; + } + } + } + + // Load video file. For simplicity, I'm passing None as metadata. You can change it as per your need. + match stab.load_video_file(&media_file_path_string, None) { + Ok(_) => { + log::info!("[Gyroflow Toolbox Rust] Video file loaded successfully"); + }, + Err(e) => { + log::error!("[Gyroflow Toolbox Rust] An error occured: {:?}", e); + } + } + + // Export Gyroflow data + let gyroflow_data: String; + match stab.export_gyroflow_data(false, false, "{}") { + Ok(data) => { + gyroflow_data = data; + log::info!("[Gyroflow Toolbox Rust] Gyroflow data exported successfully"); + }, + Err(e) => { + log::error!("[Gyroflow Toolbox Rust] An error occured: {:?}", e); + gyroflow_data = "FAIL".to_string(); + } + } + + //--------------------------------------------------------- + // Return Gyroflow Project data as string: + //--------------------------------------------------------- + let result = CString::new(gyroflow_data).unwrap(); + return result.into_raw() +} + //--------------------------------------------------------- // The "Process Frame" function that gets triggered from // Objective-C Land: @@ -297,7 +357,7 @@ pub extern "C" fn processFrame( manager.process_pixels::(timestamp, &mut buffers) }, _ => { - log::error!("[Gyroflow Toolbox] Unsupported pixel format: {:?}", pixel_format_string); + log::error!("[Gyroflow Toolbox Rust] Unsupported pixel format: {:?}", pixel_format_string); let result = CString::new("FAIL").unwrap(); return result.into_raw() } diff --git a/Source/Gyroflow/Plugin/GyroflowConstants.h b/Source/Gyroflow/Plugin/GyroflowConstants.h index 14f62009..0f82f142 100644 --- a/Source/Gyroflow/Plugin/GyroflowConstants.h +++ b/Source/Gyroflow/Plugin/GyroflowConstants.h @@ -19,6 +19,7 @@ enum { kCB_LaunchGyroflow = 20, kCB_LoadLastGyroflowProject = 25, kCB_ImportGyroflowProject = 30, + kCB_ImportMediaFile = 35, kCB_LoadedGyroflowProject = 40, kCB_ReloadGyroflowProject = 50, diff --git a/Source/Gyroflow/Plugin/GyroflowPlugIn.h b/Source/Gyroflow/Plugin/GyroflowPlugIn.h index 12f5ab3b..2bc45fdb 100644 --- a/Source/Gyroflow/Plugin/GyroflowPlugIn.h +++ b/Source/Gyroflow/Plugin/GyroflowPlugIn.h @@ -14,6 +14,7 @@ //--------------------------------------------------------- NSView* launchGyroflowView; NSView* importGyroflowProjectView; + NSView* importMediaFileView; NSView* reloadGyroflowProjectView; NSView* loadLastGyroflowProjectView; NSView* dropZoneView; diff --git a/Source/Gyroflow/Plugin/GyroflowPlugIn.m b/Source/Gyroflow/Plugin/GyroflowPlugIn.m index 758e00c4..b4e4d08b 100644 --- a/Source/Gyroflow/Plugin/GyroflowPlugIn.m +++ b/Source/Gyroflow/Plugin/GyroflowPlugIn.m @@ -159,6 +159,13 @@ - (NSView*)createViewForParameterID:(UInt32)parameterID buttonTitle:@"Import Gyroflow Project"]; importGyroflowProjectView = view; return view; + } else if (parameterID == kCB_ImportMediaFile) { + NSView* view = [[CustomButtonView alloc] initWithAPIManager:_apiManager + parentPlugin:self + buttonID:kCB_ImportMediaFile + buttonTitle:@"Import Media File"]; + importMediaFileView = view; + return view; } else if (parameterID == kCB_ReloadGyroflowProject) { NSView* view = [[CustomButtonView alloc] initWithAPIManager:_apiManager parentPlugin:self @@ -177,14 +184,14 @@ - (NSView*)createViewForParameterID:(UInt32)parameterID NSView* view = [[CustomButtonView alloc] initWithAPIManager:_apiManager parentPlugin:self buttonID:kCB_LoadLastGyroflowProject - buttonTitle:@"Import Last Saved Project"]; + buttonTitle:@"Import Last Gyroflow Project"]; loadLastGyroflowProjectView = view; return view; } else if (parameterID == kCB_DropZone) { NSView* view = [[CustomDropZoneView alloc] initWithAPIManager:_apiManager parentPlugin:self buttonID:kCB_DropZone - buttonTitle:@"Drop Zone"]; + buttonTitle:@"Import Dropped Clip"]; dropZoneView = view; return view; } else { @@ -230,23 +237,6 @@ - (BOOL)addParametersWithError:(NSError**)error } return NO; } - - //--------------------------------------------------------- - // ADD PARAMETER: Drop Zone - //--------------------------------------------------------- - if (![paramAPI addCustomParameterWithName:@"Drop Zone" - parameterID:kCB_DropZone - defaultValue:@0 - parameterFlags:kFxParameterFlag_CUSTOM_UI | kFxParameterFlag_NOT_ANIMATABLE]) - { - if (error != NULL) { - NSDictionary* userInfo = @{NSLocalizedDescriptionKey : @"[Gyroflow Toolbox Renderer] Unable to add parameter: kCB_DropZone"}; - *error = [NSError errorWithDomain:FxPlugErrorDomain - code:kFxError_InvalidParameter - userInfo:userInfo]; - } - return NO; - } //--------------------------------------------------------- // ADD PARAMETER: 'Loaded Gyroflow Project' Text Box @@ -283,9 +273,9 @@ - (BOOL)addParametersWithError:(NSError**)error } //--------------------------------------------------------- - // ADD PARAMETER: 'Import Last Saved Project' Button + // ADD PARAMETER: 'Import Last Gyroflow Project' Button //--------------------------------------------------------- - if (![paramAPI addCustomParameterWithName:@"Import Last Saved Project" + if (![paramAPI addCustomParameterWithName:@"Import Last Gyroflow Project" parameterID:kCB_LoadLastGyroflowProject defaultValue:@0 parameterFlags:kFxParameterFlag_CUSTOM_UI | kFxParameterFlag_NOT_ANIMATABLE]) @@ -299,6 +289,40 @@ - (BOOL)addParametersWithError:(NSError**)error return NO; } + //--------------------------------------------------------- + // ADD PARAMETER: 'Import Media File' Button + //--------------------------------------------------------- + if (![paramAPI addCustomParameterWithName:@"Import Media File" + parameterID:kCB_ImportMediaFile + defaultValue:@0 + parameterFlags:kFxParameterFlag_CUSTOM_UI | kFxParameterFlag_NOT_ANIMATABLE]) + { + if (error != NULL) { + NSDictionary* userInfo = @{NSLocalizedDescriptionKey : @"[Gyroflow Toolbox Renderer] Unable to add parameter: kCB_ImportMediaFile"}; + *error = [NSError errorWithDomain:FxPlugErrorDomain + code:kFxError_InvalidParameter + userInfo:userInfo]; + } + return NO; + } + + //--------------------------------------------------------- + // ADD PARAMETER: Import Dropped Clip + //--------------------------------------------------------- + if (![paramAPI addCustomParameterWithName:@"Import Dropped Clip" + parameterID:kCB_DropZone + defaultValue:@0 + parameterFlags:kFxParameterFlag_CUSTOM_UI | kFxParameterFlag_NOT_ANIMATABLE]) + { + if (error != NULL) { + NSDictionary* userInfo = @{NSLocalizedDescriptionKey : @"[Gyroflow Toolbox Renderer] Unable to add parameter: kCB_DropZone"}; + *error = [NSError errorWithDomain:FxPlugErrorDomain + code:kFxError_InvalidParameter + userInfo:userInfo]; + } + return NO; + } + //--------------------------------------------------------- // ADD PARAMETER: 'Open in Gyroflow' Button //--------------------------------------------------------- @@ -643,7 +667,7 @@ - (BOOL)addParametersWithError:(NSError**)error if (![paramAPI addStringParameterWithName:@"Unique Identifier" parameterID:kCB_UniqueIdentifier defaultValue:@"" - parameterFlags:kFxParameterFlag_DISABLED]) + parameterFlags:kFxParameterFlag_HIDDEN | kFxParameterFlag_NOT_ANIMATABLE]) { if (error != NULL) { NSDictionary* userInfo = @{NSLocalizedDescriptionKey : @"[Gyroflow Toolbox Renderer] Unable to add parameter: kCB_UniqueIdentifier"}; @@ -1211,9 +1235,92 @@ - (void)customButtonViewPressed:(UInt32)buttonID [self buttonImportGyroflowProject]; } else if (buttonID == kCB_ReloadGyroflowProject) { [self buttonReloadGyroflowProject]; + } else if (buttonID == kCB_ImportMediaFile) { + [self buttonImportMediaFile]; } } +//--------------------------------------------------------- +// BUTTON: 'Launch Gyroflow' +//--------------------------------------------------------- +- (void)buttonImportMediaFile { + NSLog(@"[Gyroflow Toolbox Renderer] Import Media File!"); + + //--------------------------------------------------------- + // Setup an NSOpenPanel: + //--------------------------------------------------------- + NSOpenPanel* panel = [NSOpenPanel openPanel]; + [panel setCanChooseDirectories:NO]; + [panel setCanCreateDirectories:YES]; + [panel setCanChooseFiles:YES]; + [panel setAllowsMultipleSelection:NO]; + //[panel setDirectoryURL:optionalURL]; + + //--------------------------------------------------------- + // Limit the file type to .gyroflow files: + //--------------------------------------------------------- + UTType *mxf = [UTType typeWithFilenameExtension:@"mxf"]; + UTType *braw = [UTType typeWithFilenameExtension:@"braw"]; + UTType *mpFour = [UTType typeWithFilenameExtension:@"mp4"]; + + NSArray *allowedContentTypes = [NSArray arrayWithObjects:mxf, braw, mpFour, nil]; + [panel setAllowedContentTypes:allowedContentTypes]; + + //--------------------------------------------------------- + // Open the panel: + //--------------------------------------------------------- + NSModalResponse result = [panel runModal]; + if (result != NSModalResponseOK) { + return; + } + + //--------------------------------------------------------- + // Start accessing security scoped resource: + //--------------------------------------------------------- + NSURL *url = [panel URL]; + BOOL startedOK = [url startAccessingSecurityScopedResource]; + if (startedOK == NO) { + [self showAlertWithMessage:@"An error has occurred." info:@"Failed to startAccessingSecurityScopedResource. This shouldn't happen."]; + return; + } + + NSString *path = [url path]; + + NSLog(@"[Gyroflow Toolbox Renderer] Import Media File Path: %@", path); + + const char* importResult = importMediaFile([path UTF8String]); + NSString *resultString = [NSString stringWithUTF8String: importResult]; + NSLog(@"[Gyroflow Toolbox Renderer] resultString: %@", resultString); + + + if (resultString == nil || [resultString isEqualToString:@"FAIL"]) { + [self showAlertWithMessage:@"An error has occurred" info:@"Failed to generate a Gyroflow Project from the Media File."]; + return; + } + + //--------------------------------------------------------- + // Create a temporary file in the temporary directory to + // save our Gyroflow Project: + //--------------------------------------------------------- + NSString *randomFilename = [[NSUUID UUID] UUIDString]; + NSURL *tempDirURL = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES]; + NSURL *tempFileURL = [NSURL fileURLWithPath:[tempDirURL.path stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.png", randomFilename]]]; + NSString *tempFilePath = [tempFileURL path]; + + NSError *error = nil; + [resultString writeToFile:tempFilePath + atomically:YES + encoding:NSUTF8StringEncoding + error:&error]; + + if (error) { + NSString *errorMessage = [NSString stringWithFormat:@"Failed to write the temporary Gyroflow project to disk due to:\n\n%@", error.localizedDescription]; + [self showAlertWithMessage:@"An error has occurred" info:errorMessage]; + } + + [self importGyroflowProjectWithOptionalURL:[NSURL fileURLWithPath:tempFilePath]]; +} + //--------------------------------------------------------- // BUTTON: 'Launch Gyroflow' //--------------------------------------------------------- @@ -1381,7 +1488,7 @@ - (void)buttonReloadGyroflowProject { //--------------------------------------------------------- // Trash all the caches in Rust land: - //--------------------------------------------------------- + //--------------------------------------------------------- uint32_t cacheSize = trashCache(); NSLog(@"[Gyroflow Toolbox Renderer]: Rust MANAGER_CACHE size after trashing (should be zero): %u", cacheSize); diff --git a/Source/Gyroflow/Wrapper Application/Resources/Motion Templates/Gyroflow Toolbox/Gyroflow Toolbox.moef b/Source/Gyroflow/Wrapper Application/Resources/Motion Templates/Gyroflow Toolbox/Gyroflow Toolbox.moef index cbf2a832..952f1aa5 100644 --- a/Source/Gyroflow/Wrapper Application/Resources/Motion Templates/Gyroflow Toolbox/Gyroflow Toolbox.moef +++ b/Source/Gyroflow/Wrapper Application/Resources/Motion Templates/Gyroflow Toolbox/Gyroflow Toolbox.moef @@ -73,7 +73,7 @@ 0 Active Camera 0 - + @@ -110,7 +110,7 @@ 1 1.5555555555555556 1.5555555555555556 - + @@ -118,8 +118,8 @@ 0 0 0 - - + + 0 @@ -163,10 +163,11 @@ 2 - - - + + + + @@ -261,52 +262,57 @@ 8589934609 - + 0 *********J2eMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl0GMb-gOLBoA11I*E61-*I4-kdM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh260Jh1RLBoPqoUF43oMM*-cUgAJGFiRKlg2**62FcY8H6rGIlMKZpX*********E2*********1E*******************4I6**U*2E*O*0E*8E*m*1Q*GE-C*3g*PE-j*52*RE-v*F*********0*E*********E*******************-2E** *********J2eMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl0GMb-gOLBoA11I*E61-*I4-kdM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh260Jh1RLBoPqoUF43oMM*-cUgAJGFiRKlg2**62FcY8H6rGIlMKZpX*********E2*********1E*******************4I6**U*2E*O*0E*8E*m*1Q*GE-C*3g*PE-j*52*RE-v*F*********0*E*********E*******************-2E** - + NOTHING LOADED - + + 0 + *********J2eMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl0GMb-gOLBoA11I*E61-*I4-kdM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh260Jh1RLBoPqoUF43oMM*-cUgAJGFiRKlg2**62FcY8H6rGIlMKZpX*********E2*********1E*******************4I6**U*2E*O*0E*8E*m*1Q*GE-C*3g*PE-j*52*RE-v*F*********0*E*********E*******************-2E** + *********J2eMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl0GMb-gOLBoA11I*E61-*I4-kdM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh260Jh1RLBoPqoUF43oMM*-cUgAJGFiRKlg2**62FcY8H6rGIlMKZpX*********E2*********1E*******************4I6**U*2E*O*0E*8E*m*1Q*GE-C*3g*PE-j*52*RE-v*F*********0*E*********E*******************-2E** + + 0 *********J2eMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl0GMb-gOLBoA11I*E61-*I4-kdM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh260Jh1RLBoPqoUF43oMM*-cUgAJGFiRKlg2**62FcY8H6rGIlMKZpX*********E2*********1E*******************4I6**U*2E*O*0E*8E*m*1Q*GE-C*3g*PE-j*52*RE-v*F*********0*E*********E*******************-2E** *********J2eMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl0GMb-gOLBoA11I*E61-*I4-kdM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh260Jh1RLBoPqoUF43oMM*-cUgAJGFiRKlg2**62FcY8H6rGIlMKZpX*********E2*********1E*******************4I6**U*2E*O*0E*8E*m*1Q*GE-C*3g*PE-j*52*RE-v*F*********0*E*********E*******************-2E** - + 0 *********J2eMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl0GMb-gOLBoA11I*E61-*I4-kdM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh260Jh1RLBoPqoUF43oMM*-cUgAJGFiRKlg2**62FcY8H6rGIlMKZpX*********E2*********1E*******************4I6**U*2E*O*0E*8E*m*1Q*GE-C*3g*PE-j*52*RE-v*F*********0*E*********E*******************-2E** *********J2eMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl0GMb-gOLBoA11I*E61-*I4-kdM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh260Jh1RLBoPqoUF43oMM*-cUgAJGFiRKlg2**62FcY8H6rGIlMKZpX*********E2*********1E*******************4I6**U*2E*O*0E*8E*m*1Q*GE-C*3g*PE-j*52*RE-v*F*********0*E*********E*******************-2E** - + 0 *********J2eMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl0GMb-gOLBoA11I*E61-*I4-kdM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh260Jh1RLBoPqoUF43oMM*-cUgAJGFiRKlg2**62FcY8H6rGIlMKZpX*********E2*********1E*******************4I6**U*2E*O*0E*8E*m*1Q*GE-C*3g*PE-j*52*RE-v*F*********0*E*********E*******************-2E** *********J2eMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl0GMb-gOLBoA11I*E61-*I4-kdM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh260Jh1RLBoPqoUF43oMM*-cUgAJGFiRKlg2**62FcY8H6rGIlMKZpX*********E2*********1E*******************4I6**U*2E*O*0E*8E*m*1Q*GE-C*3g*PE-j*52*RE-v*F*********0*E*********E*******************-2E** - + 0 *********J2eMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl0GMb-gOLBoA11I*E61-*I4-kdM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh260Jh1RLBoPqoUF43oMM*-cUgAJGFiRKlg2**62FcY8H6rGIlMKZpX*********E2*********1E*******************4I6**U*2E*O*0E*8E*m*1Q*GE-C*3g*PE-j*52*RE-v*F*********0*E*********E*******************-2E** *********J2eMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl0GMb-gOLBoA11I*E61-*I4-kdM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh260Jh1RLBoPqoUF43oMM*-cUgAJGFiRKlg2**62FcY8H6rGIlMKZpX*********E2*********1E*******************4I6**U*2E*O*0E*8E*m*1Q*GE-C*3g*PE-j*52*RE-v*F*********0*E*********E*******************-2E** - - - + + + - - - - - - - - - + + + + + + + + + - - - - + + + + 1