Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<Version>0.7.0</Version>
<Version>0.8.0</Version>
<Company>Code Happy, LLC</Company>
<Authors>Code Happy, LLC</Authors>
<Product>HTML/CSS To Image API</Product>
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var html_request = new CreateHtmlCssImageRequest()
ViewportHeight = 200
};

var html_image = await client.CreateImageAsync(html_request);
using var html_image = await client.CreateImageAsync(html_request);

if(html_image.Success)
{
Expand All @@ -58,4 +58,4 @@ Check out the package READMEs for more details on integrating:
> Check out the [HTML/CSS To Image Docs](https://docs.htmlcsstoimage.com) for more details on the API's capabilities.

> [!TIP]
> Get started for free at [htmlcsstoimage.com](https://htmlcsstoimage.com).
> Get started for free at [htmlcsstoimage.com](https://htmlcsstoimage.com).
57 changes: 54 additions & 3 deletions src/HtmlCssToImage/Client/HtmlCssToImageClient.Images.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,57 @@ namespace HtmlCssToImage;

public partial class HtmlCssToImageClient
{
/// <inheritdoc />
public async Task<ApiResult<bool?>> DeleteImageAsync(string imageId, CancellationToken cancellationToken = default)
{
var response = await _client.DeleteAsync(
$"{CREATE_OR_GET_PATH}/{Uri.EscapeDataString(imageId)}",
cancellationToken).ConfigureAwait(false);

return await CreateDeleteImageResultAsync(response, cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc />
public async Task<ApiResult<bool?>> DeleteImageBatchAsync(IEnumerable<string> imageIds,
CancellationToken cancellationToken = default)
{
var request = new DeleteImageBatchRequest
{
Ids = imageIds.ToArray()
};
var request_message = new HttpRequestMessage(HttpMethod.Delete, BATCH_URL)
{
Content = JsonContent.Create(request, JsonContext.Default.DeleteImageBatchRequest)
};
var response = await _client.SendAsync(request_message, cancellationToken).ConfigureAwait(false);

return await CreateDeleteImageResultAsync(response, cancellationToken).ConfigureAwait(false);
}

private static async Task<ApiResult<bool?>> CreateDeleteImageResultAsync(HttpResponseMessage response,
CancellationToken cancellationToken)
{
var result = new ApiResult<bool?>
{
HttpResponseMessage = response,
StatusCode = (int)response.StatusCode,
Success = response.IsSuccessStatusCode
};

if (response.IsSuccessStatusCode)
{
result.Response = true;
}
else
{
result.ErrorDetails = await response.Content.ReadFromJsonAsync<ErrorDetails>(
JsonContext.Default.ErrorDetails,
cancellationToken).ConfigureAwait(false);
}

return result;
}

/// <inheritdoc />
public async Task<ApiResult<CreateImageResponse[]?>> CreateImageBatchAsync<T>(T? defaultOptions, IEnumerable<T> variations,
CancellationToken cancellationToken = default) where T : IBatchAllowedImageRequest
Expand All @@ -27,11 +78,11 @@ public partial class HtmlCssToImageClient
HttpResponseMessage response;
if (request is CreateImageBatchRequest<CreateUrlImageRequest> url_request)
{
response = await _client.PostAsJsonAsync(CREATE_BATCH_URL, url_request, JsonContext.Default.CreateImageBatchRequestCreateUrlImageRequest, cancellationToken).ConfigureAwait(false);
response = await _client.PostAsJsonAsync(BATCH_URL, url_request, JsonContext.Default.CreateImageBatchRequestCreateUrlImageRequest, cancellationToken).ConfigureAwait(false);
}
else if (request is CreateImageBatchRequest<CreateHtmlCssImageRequest> html_request)
{
response = await _client.PostAsJsonAsync(CREATE_BATCH_URL, html_request, JsonContext.Default.CreateImageBatchRequestCreateHtmlCssImageRequest, cancellationToken).ConfigureAwait(false);
response = await _client.PostAsJsonAsync(BATCH_URL, html_request, JsonContext.Default.CreateImageBatchRequestCreateHtmlCssImageRequest, cancellationToken).ConfigureAwait(false);
}
else
{
Expand Down Expand Up @@ -100,4 +151,4 @@ await response.Content.ReadFromJsonAsync<CreateImageBatchResponse>(

return result;
}
}
}
2 changes: 1 addition & 1 deletion src/HtmlCssToImage/Client/HtmlCssToImageClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal HtmlCssToImageClient(HttpClient client, HtmlCssToImageOptions options,
internal const string CREATE_OR_GET_PATH = "/v1/image";
private const string CREATE_AND_RENDER_PATH = $"{CREATE_OR_GET_PATH}/create-and-render";
private const string CREATE_URL = $"{CREATE_OR_GET_PATH}?includeId=true";
private const string CREATE_BATCH_URL = $"{CREATE_OR_GET_PATH}/batch";
private const string BATCH_URL = $"{CREATE_OR_GET_PATH}/batch";

private const string TEMPLATE_VERSION_QUERY_PARAM = "template_version";
}
16 changes: 16 additions & 0 deletions src/HtmlCssToImage/IHtmlCssToImageClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ public interface IHtmlCssToImageClient
/// <returns>An <see cref="ApiResult{T}"/> object containing the response with metadata such as the image's URL and ID, as well as the status of the operation.</returns>
public Task<ApiResult<CreateImageResponse?>> CreateImageAsync<T>(T request, CancellationToken cancellationToken = default) where T : ICreateImageRequestBase;

/// <summary>
/// Deletes an existing image and clears it from the CDN.
/// </summary>
/// <param name="imageId">The ID of the image to delete.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>An <c>ApiResult&lt;bool?&gt;</c> whose response is <see langword="true"/> when the delete request is accepted.</returns>
public Task<ApiResult<bool?>> DeleteImageAsync(string imageId, CancellationToken cancellationToken = default);

/// <summary>
/// Deletes multiple existing images in one API call and clears them from the CDN.
/// </summary>
/// <param name="imageIds">The IDs of the images to delete.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>An <c>ApiResult&lt;bool?&gt;</c> whose response is <see langword="true"/> when the batch delete request is accepted.</returns>
public Task<ApiResult<bool?>> DeleteImageBatchAsync(IEnumerable<string> imageIds, CancellationToken cancellationToken = default);

/// <summary>
/// Sends a batch request to create multiple images using the specified parameters.
/// </summary>
Expand Down
4 changes: 3 additions & 1 deletion src/HtmlCssToImage/JsonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ namespace HtmlCssToImage;
/// - <see cref="CreateUrlImageRequest"/>
/// - <see cref="CreateTemplatedImageRequest"/>
/// - <see cref="CreateImageBatchRequest{T}"/> (for specific batch image types)
/// - <see cref="DeleteImageBatchRequest"/>
/// - <see cref="CreateImageBatchResponse"/>
/// - <see cref="CreateImageResponse"/>
/// - <see cref="ErrorDetails"/>
Expand All @@ -46,11 +47,12 @@ namespace HtmlCssToImage;
[JsonSerializable(typeof(CreateTemplatedImageRequest))]
[JsonSerializable(typeof(CreateImageBatchRequest<CreateUrlImageRequest>))]
[JsonSerializable(typeof(CreateImageBatchRequest<CreateHtmlCssImageRequest>))]
[JsonSerializable(typeof(DeleteImageBatchRequest))]
[JsonSerializable(typeof(CreateImageBatchResponse))]
[JsonSerializable(typeof(CreateImageResponse))]
[JsonSerializable(typeof(ErrorDetails))]
[JsonSerializable(typeof(PdfValueWithUnits[]))]
[JsonSerializable(typeof(CreateTemplateRequest))]
[JsonSerializable(typeof(CreateTemplateResponse))]
[JsonSerializable(typeof(PaginatedResponse<Template>))]
public partial class JsonContext : JsonSerializerContext { }
public partial class JsonContext : JsonSerializerContext { }
12 changes: 12 additions & 0 deletions src/HtmlCssToImage/Models/Requests/DeleteImageBatchRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace HtmlCssToImage.Models.Requests;

/// <summary>
/// Represents a request to delete multiple images in one API call.
/// </summary>
public sealed class DeleteImageBatchRequest
{
/// <summary>
/// Gets or sets the IDs of the images to delete.
/// </summary>
public string[] Ids { get; set; } = [];
}
28 changes: 25 additions & 3 deletions src/HtmlCssToImage/Models/Results/ApiResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ namespace HtmlCssToImage.Models.Results;
/// the associated response data, and additional metadata related to the HTTP response.
/// </summary>
/// <typeparam name="T">The type of the response data returned by the API if the operation is successful.</typeparam>
public class ApiResult<T>
/// <remarks>
/// Dispose the result after reading it to release its owned <see cref="System.Net.Http.HttpResponseMessage"/>.
/// </remarks>
public sealed class ApiResult<T> : IDisposable
{
private bool _disposed;

/// <summary>
/// The response item, when successful
/// </summary>
Expand All @@ -32,7 +37,24 @@ public class ApiResult<T>
public int StatusCode { get; set; }

/// <summary>
/// The raw HTTP response message returned by the API.
/// The raw HTTP response message returned by the API. Use this for advanced scenarios that
/// need response headers, the originating <see cref="System.Net.Http.HttpResponseMessage.RequestMessage"/>,
/// the HTTP version, reason phrase, or other transport-level details. This message is disposed
/// when the <see cref="ApiResult{T}"/> is disposed.
/// </summary>
public HttpResponseMessage? HttpResponseMessage { get; internal set; } = null!;
}

/// <summary>
/// Releases the owned HTTP response message.
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}

HttpResponseMessage?.Dispose();
_disposed = true;
}
}
36 changes: 31 additions & 5 deletions src/HtmlCssToImage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ var client = new HtmlCssToImageClient(http, options);

If you're using ASP.NET Core or similar frameworks supporting Microsoft DI, check out the [HtmlCssToImage.DependencyInjection Package Docs](https://github.com/htmlcsstoimage/dotnet-client/blob/main/src/HtmlCssToImage.DependencyInjection/README.md) for how to inject the client into your application.

## Response Shape

Most client methods that call the HCTI API return an [`ApiResult<T>`](https://github.com/htmlcsstoimage/dotnet-client/blob/main/src/HtmlCssToImage/Models/Results/ApiResult.cs). Signed URL helpers return a `string` instead because they do not make an HTTP request.

An `ApiResult<T>` provides:

- `Response`: The typed response body when the request succeeds.
- `ErrorDetails`: The typed API error when the request fails.
- `Success` and `StatusCode`: A quick way to inspect the outcome.
- `HttpResponseMessage`: The raw HTTP response for advanced cases.

The raw `HttpResponseMessage` is included so you can inspect response headers, the originating `RequestMessage`, the HTTP version, reason phrase, and other transport-level details that are not part of the typed response.

`ApiResult<T>` owns that raw response and implements `IDisposable`. Use `using` or `using var` for every result so the underlying response and content are released promptly.

## Creating Images
You can generate images using your HCTI client with strongly typed parameters:
Expand All @@ -51,7 +65,7 @@ var html_request = new CreateHtmlCssImageRequest()
ViewportHeight = 400
};

var html_image = await client.CreateImageAsync(html_request);
using var html_image = await client.CreateImageAsync(html_request);

if(html_image.Success)
{
Expand All @@ -61,7 +75,21 @@ if(html_image.Success)
}
```

Image creation responds with an [`ApiResult<CreateImageResponse>`](https://github.com/htmlcsstoimage/dotnet-client/blob/main/src/HtmlCssToImage/Models/Responses/CreateImageResponse.cs). [`ApiResult<T>`](https://github.com/htmlcsstoimage/dotnet-client/blob/main/src/HtmlCssToImage/Models/Results/ApiResult.cs) is a simple wrapper around Api responses that provides an indicator of `Success` and potentially `ErrorDetails` if the request failed.
## Deleting Images

Delete one or more existing images from HCTI and clear them from the CDN using their image IDs:

```csharp
using var result = await client.DeleteImageAsync("your-image-id");

if (!result.Success)
{
Console.WriteLine(result.ErrorDetails.Message);
}

using var batchResult = await client.DeleteImageBatchAsync(
["first-image-id", "second-image-id"]);
```

### Template Helpers
When creating a templated image, you can use static helper methods `FromObject<T>` on the `CreateTemplatedImageRequest` class to generate a template, providing serialization options for AOT/serialization control. See more below in the [Performance & Native AOT](#performance--native-aot) section.
Expand All @@ -84,9 +112,7 @@ Call `CreateImageBatchAsync<T>` on the client to create a batch of images from a
You can construct a `CreateImageBatchRequest` object directly or use the overload on `HtmlCssToImageClient` that accepts `defaultOptions` and `variations` as parameters.


Batch creation responds with an [`ApiResult<CreateImageResponse[]>`](https://github.com/htmlcsstoimage/dotnet-client/blob/main/src/HtmlCssToImage/Models/Responses/CreateImageResponse.cs). [`ApiResult<T>`](https://github.com/htmlcsstoimage/dotnet-client/blob/main/src/HtmlCssToImage/Models/Results/ApiResult.cs) is a simple wrapper around Api responses that provides an indicator of `Success` and potentially `ErrorDetails` if the request failed.

If your request is successful, the `Response` property will be an array in the order of your `variations`.
Batch creation responds with an [`ApiResult<CreateImageResponse[]>`](https://github.com/htmlcsstoimage/dotnet-client/blob/main/src/HtmlCssToImage/Models/Responses/CreateImageResponse.cs). If your request is successful, the `Response` property will be an array in the order of your `variations`.

## Creating Image URLs

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public async Task ListTemplates_ReturnsSuccessResult()
Content = JsonContent.Create(expectedResponse, JsonContext.Default.PaginatedResponseTemplate)
});

var result = await client.ListTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
using var result = await client.ListTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);

Assert.True(result.Success);
Assert.NotNull(result.Response);
Expand All @@ -95,7 +95,7 @@ public async Task ListTemplates_UsesConfiguredHost()

var client = CreateClient("https://example.test");

await client.ListTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
using var result = await client.ListTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);

Assert.NotNull(capturedRequest);
Assert.Equal(new Uri("https://example.test/v1/template?count=10"), capturedRequest.RequestUri);
Expand Down Expand Up @@ -123,7 +123,7 @@ public async Task ListTemplateVersions_ReturnsSuccessResult()
Content = JsonContent.Create(expectedResponse, JsonContext.Default.PaginatedResponseTemplate)
});

var result = await client.ListTemplateVersionsAsync(templateId, count: 20, cancellationToken: TestContext.Current.CancellationToken);
using var result = await client.ListTemplateVersionsAsync(templateId, count: 20, cancellationToken: TestContext.Current.CancellationToken);

Assert.True(result.Success);
Assert.NotNull(result.Response);
Expand All @@ -147,7 +147,7 @@ public async Task ListTemplatesCore_HandlesErrorResponse()
Content = JsonContent.Create(errorDetails, JsonContext.Default.ErrorDetails)
});

var result = await client.ListTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
using var result = await client.ListTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);

Assert.False(result.Success);
Assert.Equal(401, result.StatusCode);
Expand All @@ -173,7 +173,7 @@ public async Task ListTemplates_ClampsCountValue(int inputCount, uint expectedUs
Content = JsonContent.Create(new PaginatedResponse<Template> { Data = [], Pagination = new PaginatedResponse<Template>.PaginationInfo(null) }, JsonContext.Default.PaginatedResponseTemplate)
});

await client.ListTemplatesAsync(count: inputCount, cancellationToken: TestContext.Current.CancellationToken);
using var result = await client.ListTemplatesAsync(count: inputCount, cancellationToken: TestContext.Current.CancellationToken);

_handlerMock.Protected().Verify(
"SendAsync",
Expand Down
Loading
Loading