Skip to content

Commit

Permalink
Merge pull request #1 from skylab-tech/version-2
Browse files Browse the repository at this point in the history
Version 2
  • Loading branch information
kev-le authored Jan 31, 2024
2 parents 88bb465 + 7ce66bc commit ace0565
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 5 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ dynamic queuedJob = await apiClient.QueueJob(job.id.Value, new { callback_url =
// We will send a response to the specified callback_url with the output photo download urls
```

```dotnet
// OPTIONAL: If you want this SDK to handle photo downloads to a specified output folder
// FETCH COMPLETED JOB (wait until job status is completed)
dynamic completedJob = await apiClient.GetJob(queuedJob.id.Value);
// DOWNLOAD COMPLETED JOB PHOTOS
JArray photosList = completedJob.photos;
await apiClient.DownloadAllPhotos(photosList, completedJob.profile, "photos/output/");
```

### Error Handling

By default, the API calls return a JSON (JObject) response object no matter the type of response.
Expand Down Expand Up @@ -155,6 +166,22 @@ api.UploadProfilePhoto("/path/to/photo", profileId);

If upload fails, the photo object is deleted for you. If upload succeeds and you later decide you no longer want to include that image, use api.DeletePhoto(photoId) to remove it.

#### Download photo(s)

This function handles downloading the output photos to a specified directory.

```dotnet
JArray photosList = completedJob.photos;
api.DownloadAllPhotos(photosList, completedJob.profile, "/output/folder/");
```

OR

```dotnet
api.DownloadPhoto(photoId, "/path/to/photo");
```

#### Delete photo

This will remove the photo from the job/profile's bucket. Useful for when you've accidentally uploaded an image that you'd like removed.
Expand Down
16 changes: 14 additions & 2 deletions StudioClient.Example/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@

using System.Security.Principal;
using NetVips;
using Newtonsoft.Json.Linq;

namespace SkylabStudio.Example
{
class Program
Expand All @@ -12,7 +16,7 @@ static async Task Main(string[] args)
Guid randomUuid = Guid.NewGuid();

// CREATE PROFILE
dynamic profile = await apiClient.CreateProfile(new { name = $"Test Profile ({randomUuid})", enable_crop = false, enable_color = true });
dynamic profile = await apiClient.CreateProfile(new { name = $"Test Profile ({randomUuid})", enable_crop = false, enable_color = false, enable_extract = true });

// CREATE JOB
var jobName = $"test-job-{randomUuid}";
Expand All @@ -21,9 +25,17 @@ static async Task Main(string[] args)
// UPLOAD PHOTO
string filePath = "/path/to/photo";
dynamic res = await apiClient.UploadJobPhoto(filePath, job.id.Value);

// QUEUE JOB
dynamic queuedJob = await apiClient.QueueJob(job.id.Value, new { callback_url = "YOUR_CALLBACK_ENDPOINT" });

// FETCH COMPLETED JOB (wait until job status is completed)
dynamic completedJob = await apiClient.GetJob(queuedJob.id.Value);

// DOWNLOAD COMPLETED JOB PHOTOS
JArray photosList = completedJob.photos;
await apiClient.DownloadAllPhotos(photosList, completedJob.profile, "/output/folder/");

}
catch (Exception ex)
{
Expand Down
157 changes: 157 additions & 0 deletions StudioClient/DownloadHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
using Newtonsoft.Json.Linq;
using NetVips;


namespace SkylabStudio
{
public partial class StudioClient
{
private async Task<List<Image>?> DownloadBgImages(dynamic profile)
{
var httpClient = new HttpClient();

List<Image> tempBgs = new List<Image>();
List<JToken> bgPhotos = ((JArray) profile!.photos).Where(photo => photo["jobId"] != null).ToList();

foreach (dynamic bg in bgPhotos)
{
byte[] bgBuffer = await DownloadImageAsync(bg.originalUrl.Value);
Image bgImage = Image.NewFromBuffer(bgBuffer);
tempBgs.Add(bgImage);
}

return tempBgs;

}

private static async Task<byte[]?> DownloadImageAsync(string imageUrl)
{
try
{
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<bool> DownloadReplacedBackgroundImage(string fileName, Image inputImage, string outputPath, dynamic? profile = null, List<Image>? 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<bool> DownloadAllPhotos(JArray photosList, dynamic profile, string outputPath)
{
try {
profile = await GetProfile(profile.id.Value);
List<Image> bgs = await DownloadBgImages(profile);

var httpClient = new HttpClient();

List<string> photoIds = photosList.Select(photo => photo?["id"]?.ToString() ?? "").ToList() ?? new List<string>();

// Use a semaphore to control access to the download operation
var semaphore = new SemaphoreSlim(_maxConcurrentDownloads);
List<Task> downloadTasks = new List<Task>();
foreach (string photoId in photoIds)
{
downloadTasks.Add(DownloadPhoto(long.Parse(photoId), outputPath, profile, null, semaphore));
}

// Wait for all download tasks to complete
await Task.WhenAll(downloadTasks);

return true;
} catch (Exception _e) {
Console.WriteLine(_e);
return false;
}
}
public async Task<bool> DownloadPhoto(long photoId, string outputPath, dynamic? profile = null, dynamic? options = null, SemaphoreSlim? semaphore = null)
{
try {
if (semaphore != null) await semaphore.WaitAsync(); // Wait until a slot is available

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<Image>? 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;
}
}
}
}
5 changes: 5 additions & 0 deletions StudioClient/Profile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,10 @@ public async Task<dynamic> UpdateProfile(long profileId, object payload)
{
return await Request($"profiles/{profileId}", HttpMethod.Put, payload);
}

public async Task<dynamic> GetProfileBgs(long profileId)
{
return await Request($"profiles/{profileId}/bg_photos", HttpMethod.Get);
}
}
}
4 changes: 3 additions & 1 deletion StudioClient/StudioClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ 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");

string baseUrl = Environment.GetEnvironmentVariable("SKYLAB_API_URL") ?? "https://studio.skylabtech.ai";
_httpClient = new HttpClient();
_httpClient.BaseAddress = new Uri(baseUrl);
_apiKey = apiKey;
_maxConcurrentDownloads = options?.maxConcurrentDownloads ?? 5;
}

private async Task<dynamic> Request(string endpoint, HttpMethod httpMethod, object? payload = null)
Expand Down
5 changes: 3 additions & 2 deletions StudioClient/StudioClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@
<PackageProjectUrl>https://github.com/skylab-tech/studio_client_dotnet</PackageProjectUrl>
<PackageIconUrl>https://avatars.githubusercontent.com/u/45469060</PackageIconUrl>
<PackageTags>studio skylab skylabtech api library .net nuget</PackageTags>
<TargetFrameworks>net47;net472;net48;net6.0;net7.0;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net5.0;net6.0;net7.0;netstandard2.1;netcoreapp3.0;netcoreapp3.1</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Copyright>Copyright (c) Skylab Technologies Inc. 2023</Copyright>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNet.WebApi.SelfHost" Version="5.3.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="NetVips" Version="2.4.0" />
<PackageReference Include="NetVips.Native" Version="8.15.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<None Include="..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
Expand Down

0 comments on commit ace0565

Please sign in to comment.