diff --git a/StudioClient/DownloadHelper.cs b/StudioClient/DownloadHelper.cs new file mode 100644 index 0000000..661f2b4 --- /dev/null +++ b/StudioClient/DownloadHelper.cs @@ -0,0 +1,158 @@ +using Newtonsoft.Json.Linq; +using NetVips; + + +namespace SkylabStudio +{ + public partial class StudioClient + { + private async Task?> DownloadBgImages(dynamic profile) + { + var httpClient = new HttpClient(); + SemaphoreSlim semaphore = new SemaphoreSlim(_maxConcurrentDownloads); + + List tempBgs = new List(); + List bgPhotos = ((JArray) profile!.photos).Where(photo => photo["jobId"] != null).ToList(); + + foreach (dynamic bg in bgPhotos) + { + byte[] bgBuffer = await DownloadImageAsync(bg.originalUrl.Value, semaphore); + Image bgImage = Image.NewFromBuffer(bgBuffer); + tempBgs.Add(bgImage); + } + + return tempBgs; + + } + + private static async Task DownloadImageAsync(string imageUrl, SemaphoreSlim? semaphore = null) + { + try + { + if (semaphore != null) await semaphore.WaitAsync(); // Wait until a slot is available + + using (HttpClient httpClient = new HttpClient()) + { + // Download the image into a byte array + byte[] imageBuffer = await httpClient.GetByteArrayAsync(imageUrl); + + return imageBuffer; + } + } + catch (HttpRequestException ex) + { + Console.WriteLine($"Error downloading image: {ex.Message}"); + return null; + } + } + + private async Task DownloadReplacedBackgroundImage(string fileName, Image inputImage, string outputPath, dynamic? profile = null, List? bgs = null) + { + try + { + string outputFileType = profile?.outputFileType?.Value ?? "png"; + + if (bgs == null && profile?.photos?.Count > 0) { + bgs = await DownloadBgImages(profile); + } + + Image alphaChannel = inputImage.ExtractBand(3); + Image rgbChannel = inputImage.ExtractBand(0, 3); + Image rgbCutout = rgbChannel.Bandjoin(alphaChannel); + + if (bgs != null && bgs.Count > 0){ + for (int i = 0; i < bgs.Count; i++) { + string newFileName = i == 0 ? $"{Path.GetFileNameWithoutExtension(fileName)}.{outputFileType}": $"{Path.GetFileNameWithoutExtension(fileName)} ({i+1}).{outputFileType}"; + Image resizedBgImage = bgs[i].ThumbnailImage(inputImage.Width, inputImage.Height, crop: Enums.Interesting.Centre); + Image resultImage = resizedBgImage.Composite2(rgbCutout, Enums.BlendMode.Over); + resultImage.WriteToFile(Path.Combine(outputPath, newFileName)); + } + } + + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Error downloading background image: {ex.Message}"); + return false; + } + } + + public async Task DownloadAllPhotos(JArray photosList, dynamic profile, string outputPath) + { + try { + profile = await GetProfile(profile.id.Value); + List bgs = await DownloadBgImages(profile); + + var httpClient = new HttpClient(); + + List photoIds = photosList.Select(photo => photo?["id"]?.ToString() ?? "").ToList() ?? new List(); + + // Use a semaphore to control access to the download operation + var semaphore = new SemaphoreSlim(_maxConcurrentDownloads); + List downloadTasks = new List(); + foreach (string photoId in photoIds) + { + downloadTasks.Add(DownloadPhoto(long.Parse(photoId), outputPath, profile)); + } + + // Wait for all download tasks to complete + await Task.WhenAll(downloadTasks); + + return true; + } catch (Exception _e) { + Console.WriteLine(_e); + return false; + } + } + public async Task DownloadPhoto(long photoId, string outputPath, dynamic? profile = null, dynamic? options = null) + { + try { + dynamic photo = await GetPhoto(photoId); + long profileId = photo.job.profileId; + + string fileName = photo.name.Value; + + if (profile == null) { + profile = await GetProfile(profileId); + } + bool isExtract = Convert.ToBoolean(profile.enableExtract.Value); + bool replaceBackground = Convert.ToBoolean(profile.enableExtract.Value); + bool isDualFileOutput = Convert.ToBoolean(profile.dualFileOutput.Value); + bool enableStripPngMetadata = Convert.ToBoolean(profile.enableStripPngMetadata.Value); + List? bgs = options?.bgs; + + // Load output image + byte[] imageBuffer = await DownloadImageAsync(photo.retouchedUrl.Value); + Image image = Image.NewFromBuffer(imageBuffer); + + if (isExtract) { // Output extract image + string pngFileName = $"{Path.GetFileNameWithoutExtension(fileName)}.png"; + + // Dual File Output will provide an image in the format specified in the outputFileType field + // and an extracted image as a PNG. + if (isDualFileOutput) { + image.WriteToFile(Path.Combine(outputPath, pngFileName)); + } + + if (replaceBackground) { + await DownloadReplacedBackgroundImage(fileName, image, outputPath, profile, bgs); + } + + // Regular Extract output + if (!isDualFileOutput) image.WriteToFile(Path.Combine(outputPath, pngFileName)); + } else { // Non-extracted regular image output + image.WriteToFile(Path.Combine(outputPath, fileName)); + } + + Console.WriteLine($"Successfully downloaded: {fileName}"); + return true; + } catch (Exception _e) + { + Console.WriteLine($"Failed to download photo id: {photoId}"); + Console.WriteLine(_e); + return false; + } + } + } +} \ No newline at end of file diff --git a/StudioClient/Profile.cs b/StudioClient/Profile.cs index 0d1a86e..7a60530 100644 --- a/StudioClient/Profile.cs +++ b/StudioClient/Profile.cs @@ -21,5 +21,10 @@ public async Task UpdateProfile(long profileId, object payload) { return await Request($"profiles/{profileId}", HttpMethod.Put, payload); } + + public async Task GetProfileBgs(long profileId) + { + return await Request($"profiles/{profileId}/bg_photos", HttpMethod.Get); + } } } diff --git a/StudioClient/StudioClient.cs b/StudioClient/StudioClient.cs index 6661688..b06222c 100644 --- a/StudioClient/StudioClient.cs +++ b/StudioClient/StudioClient.cs @@ -8,8 +8,9 @@ public partial class StudioClient { private readonly HttpClient _httpClient; private readonly string _apiKey; + private readonly int _maxConcurrentDownloads = 5; - public StudioClient(string? apiKey = null) + public StudioClient(string? apiKey = null, dynamic? options = null) { if (apiKey == null) throw new Exception("No API key provided"); @@ -17,6 +18,7 @@ public StudioClient(string? apiKey = null) _httpClient = new HttpClient(); _httpClient.BaseAddress = new Uri(baseUrl); _apiKey = apiKey; + _maxConcurrentDownloads = options?.maxConcurrentDownloads ?? 5; } private async Task Request(string endpoint, HttpMethod httpMethod, object? payload = null) diff --git a/StudioClient/StudioClient.csproj b/StudioClient/StudioClient.csproj index d0c8802..b934af8 100644 --- a/StudioClient/StudioClient.csproj +++ b/StudioClient/StudioClient.csproj @@ -11,7 +11,7 @@ https://github.com/skylab-tech/studio_client_dotnet https://avatars.githubusercontent.com/u/45469060 studio skylab skylabtech api library .net nuget - net47;net472;net48;net6.0;net7.0;netstandard2.0 + net5.0;net6.0;net7.0;netstandard2.1;netcoreapp3.0;netcoreapp3.1 enable enable README.md @@ -19,8 +19,9 @@ - + +