diff --git a/README.md b/README.md index 41deb15..ac850d4 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ using Gotenberg.Sharp.API.Client; using Gotenberg.Sharp.API.Client.Domain.Builders; using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; using Gotenberg.Sharp.API.Client.Domain.Requests.Facets; // For Cookie, etc. +using Gotenberg.Sharp.API.Client.Domain.ValueObjects; // For ScreenshotFormat, etc. ``` ### HTML To PDF @@ -529,27 +530,41 @@ public async Task FastConversion() } ``` -### Wait For Selector & Emulated Media Features -*Wait for a DOM element and emulate CSS media features like dark mode:* +### Screenshot from HTML +*Capture a screenshot of HTML content as PNG, JPEG, or WebP:* ```csharp -public async Task CreateWithChromiumFeatures() +public async Task ScreenshotHtml() { - var builder = new HtmlRequestBuilder() - .AddDocument(doc => doc.SetBody("
Ready
")) - .SetConversionBehaviors(b => b - .SetWaitForSelector("#app") - .AddEmulatedMediaFeature("prefers-color-scheme", "dark") - .SetFailOnHttpStatusCodes(499, 599) - .FailOnResourceLoadingFailed() - .AddIgnoreResourceHttpStatusDomains("cdn.example.com")) - .WithPageProperties(pp => pp.UseChromeDefaults()); + var builder = new ScreenshotHtmlRequestBuilder() + .AddDocument(doc => doc.SetBody("

Screenshot!

")) + .WithScreenshotProperties(p => p + .SetSize(1280, 720) + .SetFormat(ScreenshotFormat.Png)); var request = builder.Build(); - return await _sharpClient.HtmlToPdfAsync(request); + return await _sharpClient.ScreenshotHtmlAsync(request); } ``` +### Screenshot from URL +*Capture a screenshot of any URL:* + +```csharp +public async Task ScreenshotUrl() +{ + var builder = new ScreenshotUrlRequestBuilder() + .SetUrl("https://example.com") + .WithScreenshotProperties(p => p + .SetSize(1024, 768) + .SetFormat(ScreenshotFormat.Jpeg) + .SetQuality(90) + .SetClip()); + + var request = builder.Build(); + return await _sharpClient.ScreenshotUrlAsync(request); +} +``` ### Custom Page Properties *Fine-tune page dimensions and properties:* diff --git a/examples/Screenshot/Program.cs b/examples/Screenshot/Program.cs new file mode 100644 index 0000000..9ea39b9 --- /dev/null +++ b/examples/Screenshot/Program.cs @@ -0,0 +1,85 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.ValueObjects; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; + +using Microsoft.Extensions.Configuration; + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); + +var destinationDirectory = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationDirectory); + +var sharpClient = CreateClient(options); + +// Screenshot from HTML +var htmlPath = await ScreenshotFromHtml(destinationDirectory, sharpClient); +Console.WriteLine($"HTML screenshot: {htmlPath}"); + +// Screenshot from URL +var urlPath = await ScreenshotFromUrl(destinationDirectory, sharpClient); +Console.WriteLine($"URL screenshot: {urlPath}"); + +static async Task ScreenshotFromHtml(string destinationDirectory, GotenbergSharpClient sharpClient) +{ + var builder = new ScreenshotHtmlRequestBuilder() + .AddDocument(doc => doc.SetBody(@" + + +

Screenshot Demo

+

Captured with Gotenberg + GotenbergSharpApiClient

+ + ")) + .WithScreenshotProperties(p => p + .SetSize(1280, 720) + .SetFormat(ScreenshotFormat.Png)); + + await using var response = await sharpClient.ScreenshotHtmlAsync(builder); + + var resultPath = Path.Combine(destinationDirectory, $"ScreenshotHtml-{DateTime.Now:yyyyMMddHHmmss}.png"); + await using var file = File.Create(resultPath); + await response.CopyToAsync(file); + return resultPath; +} + +static async Task ScreenshotFromUrl(string destinationDirectory, GotenbergSharpClient sharpClient) +{ + var builder = new ScreenshotUrlRequestBuilder() + .SetUrl("https://example.com") + .WithScreenshotProperties(p => p + .SetSize(1024, 768) + .SetFormat(ScreenshotFormat.Jpeg) + .SetQuality(90) + .SetClip()); + + await using var response = await sharpClient.ScreenshotUrlAsync(builder); + + var resultPath = Path.Combine(destinationDirectory, $"ScreenshotUrl-{DateTime.Now:yyyyMMddHHmmss}.jpg"); + await using var file = File.Create(resultPath); + await response.CopyToAsync(file); + return resultPath; +} + +static GotenbergSharpClient CreateClient(GotenbergSharpClientOptions options) +{ + var handler = new HttpClientHandler(); + HttpMessageHandler effectiveHandler = handler; + + if (!string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword)) + effectiveHandler = new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler }; + + var httpClient = new HttpClient(effectiveHandler) + { + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut + }; + + return new GotenbergSharpClient(httpClient); +} diff --git a/examples/Screenshot/Screenshot.csproj b/examples/Screenshot/Screenshot.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/Screenshot/Screenshot.csproj @@ -0,0 +1,2 @@ + + diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Builders/BaseScreenshotBuilder.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/BaseScreenshotBuilder.cs new file mode 100644 index 0000000..f7cd4e4 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/BaseScreenshotBuilder.cs @@ -0,0 +1,88 @@ +// Copyright 2019-2026 Chris Mohan, Jaben Cargman +// and GotenbergSharpApiClient Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Gotenberg.Sharp.API.Client.Domain.Builders; + +/// +/// Base builder for all Chromium screenshot requests. Provides screenshot properties, +/// conversion behaviors, and asset management shared across URL and HTML screenshot builders. +/// +public abstract class BaseScreenshotBuilder(TRequest request) + : BaseBuilder(request) + where TRequest : ScreenshotRequest + where TBuilder : BaseScreenshotBuilder +{ + /// + /// Configures screenshot-specific properties (device dimensions, format, quality). + /// + public TBuilder WithScreenshotProperties(Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + action(new ScreenshotPropertyBuilder(this.Request.ScreenshotProperties)); + + return (TBuilder)this; + } + + /// + /// Configures Chromium rendering behaviors (wait conditions, cookies, headers, error handling). + /// + public TBuilder SetConversionBehaviors(Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + action(new HtmlConversionBehaviorBuilder(this.Request.ConversionBehaviors)); + + return (TBuilder)this; + } + + /// + /// Sets pre-configured conversion behaviors. + /// + public TBuilder SetConversionBehaviors(HtmlConversionBehaviors behaviors) + { + this.Request.ConversionBehaviors = behaviors ?? throw new ArgumentNullException(nameof(behaviors)); + + return (TBuilder)this; + } + + /// + /// Adds embedded assets (images, fonts, CSS, JS) referenced by the HTML content. + /// + public TBuilder WithAssets(Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + this.Request.Assets ??= new AssetDictionary(); + + action(new AssetBuilder(this.Request.Assets)); + + return (TBuilder)this; + } + + /// + /// Adds embedded assets asynchronously (e.g., from streams or files). + /// + public TBuilder WithAsyncAssets(Func asyncAction) + { + if (asyncAction == null) throw new ArgumentNullException(nameof(asyncAction)); + + this.Request.Assets ??= new AssetDictionary(); + + this.BuildTasks.Add(asyncAction(new AssetBuilder(this.Request.Assets))); + + return (TBuilder)this; + } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/ScreenshotPropertyBuilder.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/ScreenshotPropertyBuilder.cs new file mode 100644 index 0000000..0ffabbe --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/ScreenshotPropertyBuilder.cs @@ -0,0 +1,93 @@ +// Copyright 2019-2026 Chris Mohan, Jaben Cargman +// and GotenbergSharpApiClient Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Gotenberg.Sharp.API.Client.Domain.ValueObjects; + +namespace Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; + +/// +/// Configures screenshot-specific properties (device dimensions, format, quality). +/// +public sealed class ScreenshotPropertyBuilder +{ + private readonly ScreenshotProperties _properties; + + internal ScreenshotPropertyBuilder(ScreenshotProperties properties) + { + _properties = properties; + } + + public ScreenshotPropertyBuilder SetWidth(ScreenDimension width) + { + _properties.Width = width ?? throw new ArgumentNullException(nameof(width)); + return this; + } + + public ScreenshotPropertyBuilder SetWidth(int pixels) + { + return SetWidth(ScreenDimension.Create(pixels)); + } + + public ScreenshotPropertyBuilder SetHeight(ScreenDimension height) + { + _properties.Height = height ?? throw new ArgumentNullException(nameof(height)); + return this; + } + + public ScreenshotPropertyBuilder SetHeight(int pixels) + { + return SetHeight(ScreenDimension.Create(pixels)); + } + + public ScreenshotPropertyBuilder SetSize(int width, int height) + { + return SetWidth(width).SetHeight(height); + } + + public ScreenshotPropertyBuilder SetClip(bool clip = true) + { + _properties.Clip = clip; + return this; + } + + public ScreenshotPropertyBuilder SetFormat(ScreenshotFormat format) + { + _properties.Format = format; + return this; + } + + public ScreenshotPropertyBuilder SetQuality(CompressionQuality quality) + { + _properties.Quality = quality ?? throw new ArgumentNullException(nameof(quality)); + return this; + } + + public ScreenshotPropertyBuilder SetQuality(int quality) + { + return SetQuality(CompressionQuality.Create(quality)); + } + + public ScreenshotPropertyBuilder SetOmitBackground(bool omit = true) + { + _properties.OmitBackground = omit; + return this; + } + + public ScreenshotPropertyBuilder SetOptimizeForSpeed(bool optimize = true) + { + _properties.OptimizeForSpeed = optimize; + return this; + } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Builders/ScreenshotHtmlRequestBuilder.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/ScreenshotHtmlRequestBuilder.cs new file mode 100644 index 0000000..041e5c2 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/ScreenshotHtmlRequestBuilder.cs @@ -0,0 +1,51 @@ +// Copyright 2019-2026 Chris Mohan, Jaben Cargman +// and GotenbergSharpApiClient Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Gotenberg.Sharp.API.Client.Domain.Builders; + +/// +/// Builds requests for capturing screenshots of HTML content using Chromium. +/// +public sealed class ScreenshotHtmlRequestBuilder() + : BaseScreenshotBuilder(new ScreenshotHtmlRequest()) +{ + /// + /// Configures the HTML document content for the screenshot. + /// + public ScreenshotHtmlRequestBuilder AddDocument(Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + this.Request.Content ??= new FullDocument(); + + action(new DocumentBuilder(this.Request.Content, _ => { })); + + return this; + } + + /// + /// Configures the HTML document content asynchronously. + /// + public ScreenshotHtmlRequestBuilder AddAsyncDocument(Func asyncAction) + { + if (asyncAction == null) throw new ArgumentNullException(nameof(asyncAction)); + + this.Request.Content ??= new FullDocument(); + + this.BuildTasks.Add(asyncAction(new DocumentBuilder(this.Request.Content, _ => { }))); + + return this; + } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Builders/ScreenshotUrlRequestBuilder.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/ScreenshotUrlRequestBuilder.cs new file mode 100644 index 0000000..742e5d9 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/ScreenshotUrlRequestBuilder.cs @@ -0,0 +1,40 @@ +// Copyright 2019-2026 Chris Mohan, Jaben Cargman +// and GotenbergSharpApiClient Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Gotenberg.Sharp.API.Client.Domain.Builders; + +/// +/// Builds requests for capturing screenshots of URLs using Chromium. +/// +public sealed class ScreenshotUrlRequestBuilder() + : BaseScreenshotBuilder(new ScreenshotUrlRequest()) +{ + /// + /// Sets the URL to screenshot. + /// + public ScreenshotUrlRequestBuilder SetUrl(string url) + { + return SetUrl(new Uri(url)); + } + + /// + /// Sets the URL to screenshot. + /// + public ScreenshotUrlRequestBuilder SetUrl(Uri url) + { + this.Request.Url = url ?? throw new ArgumentNullException(nameof(url)); + return this; + } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/FacetBase.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/FacetBase.cs index a575b6e..3d23a2f 100644 --- a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/FacetBase.cs +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/FacetBase.cs @@ -81,6 +81,7 @@ public virtual IEnumerable ToHttpContent() ConversionPdfFormats format => format.ToFormDataValue(), PdfPassword password => password.Value, List cookies => JsonConvert.SerializeObject(cookies), + ScreenshotFormat screenshotFormat => screenshotFormat.ToFormValue(), List features => JsonConvert.SerializeObject( features.ToDictionary(f => f.Name, f => f.Value)), List codes => JsonConvert.SerializeObject(codes.Select(c => c.Value)), diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/ScreenshotProperties.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/ScreenshotProperties.cs new file mode 100644 index 0000000..fb7965d --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/ScreenshotProperties.cs @@ -0,0 +1,46 @@ +// Copyright 2019-2026 Chris Mohan, Jaben Cargman +// and GotenbergSharpApiClient Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Gotenberg.Sharp.API.Client.Domain.ValueObjects; + +namespace Gotenberg.Sharp.API.Client.Domain.Requests.Facets; + +/// +/// Screenshot-specific properties for Chromium screenshot routes. +/// Controls device dimensions, image format, quality, and rendering options. +/// +public class ScreenshotProperties : FacetBase +{ + [MultiFormHeader(Constants.Gotenberg.Chromium.Screenshot.Width)] + public ScreenDimension? Width { get; set; } + + [MultiFormHeader(Constants.Gotenberg.Chromium.Screenshot.Height)] + public ScreenDimension? Height { get; set; } + + [MultiFormHeader(Constants.Gotenberg.Chromium.Screenshot.Clip)] + public bool? Clip { get; set; } + + [MultiFormHeader(Constants.Gotenberg.Chromium.Screenshot.Format)] + public ScreenshotFormat? Format { get; set; } + + [MultiFormHeader(Constants.Gotenberg.Chromium.Screenshot.Quality)] + public CompressionQuality? Quality { get; set; } + + [MultiFormHeader(Constants.Gotenberg.Chromium.Screenshot.OmitBackground)] + public bool? OmitBackground { get; set; } + + [MultiFormHeader(Constants.Gotenberg.Chromium.Screenshot.OptimizeForSpeed)] + public bool? OptimizeForSpeed { get; set; } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/ScreenshotHtmlRequest.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/ScreenshotHtmlRequest.cs new file mode 100644 index 0000000..cbbd958 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/ScreenshotHtmlRequest.cs @@ -0,0 +1,49 @@ +// Copyright 2019-2026 Chris Mohan, Jaben Cargman +// and GotenbergSharpApiClient Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Gotenberg.Sharp.API.Client.Domain.Requests; + +/// +/// Captures a screenshot of HTML content using Chromium. +/// +public sealed class ScreenshotHtmlRequest : ScreenshotRequest +{ + protected override string ApiPath => Constants.Gotenberg.Chromium.ApiPaths.ScreenshotHtml; + + public FullDocument? Content { get; set; } + + protected override void Validate() + { + if (this.Content?.Body == null) + throw new InvalidOperationException("HTML body content is required for screenshot."); + + base.Validate(); + } + + protected override IEnumerable ToHttpContent() + { + if (this.Content != null) + { + foreach (var item in this.Content.ToHttpContent()) + yield return item; + } + + foreach (var item in this.Assets.IfNullEmptyContent()) + yield return item; + + foreach (var item in base.ToHttpContent()) + yield return item; + } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/ScreenshotRequest.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/ScreenshotRequest.cs new file mode 100644 index 0000000..3c7fae9 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/ScreenshotRequest.cs @@ -0,0 +1,34 @@ +// Copyright 2019-2026 Chris Mohan, Jaben Cargman +// and GotenbergSharpApiClient Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Gotenberg.Sharp.API.Client.Domain.Requests; + +/// +/// Base class for Chromium screenshot requests. Shares HtmlConversionBehaviors +/// with PDF routes but uses ScreenshotProperties instead of PageProperties. +/// +public abstract class ScreenshotRequest : BuildRequestBase +{ + public ScreenshotProperties ScreenshotProperties { get; set; } = new(); + + public HtmlConversionBehaviors ConversionBehaviors { get; set; } = new(); + + protected override IEnumerable ToHttpContent() + { + return this.ScreenshotProperties.ToHttpContent() + .Concat(this.ConversionBehaviors.ToHttpContent()) + .Concat(this.Config.IfNullEmptyContent()); + } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/ScreenshotUrlRequest.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/ScreenshotUrlRequest.cs new file mode 100644 index 0000000..01cf274 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/ScreenshotUrlRequest.cs @@ -0,0 +1,53 @@ +// Copyright 2019-2026 Chris Mohan, Jaben Cargman +// and GotenbergSharpApiClient Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Gotenberg.Sharp.API.Client.Domain.Requests; + +/// +/// Captures a screenshot of a URL using Chromium. +/// +public sealed class ScreenshotUrlRequest : ScreenshotRequest +{ + protected override string ApiPath => Constants.Gotenberg.Chromium.ApiPaths.ScreenshotUrl; + + private Uri? _url; + + public Uri? Url + { + get => _url; + set + { + if (value != null && !value.IsAbsoluteUri) + throw new ArgumentException("URL must be absolute.", nameof(value)); + _url = value; + } + } + + protected override void Validate() + { + if (this.Url == null || !this.Url.IsAbsoluteUri) + throw new InvalidOperationException("An absolute URL is required for screenshot."); + + base.Validate(); + } + + protected override IEnumerable ToHttpContent() + { + yield return CreateFormDataItem(this.Url!, Constants.Gotenberg.Chromium.Routes.Url.RemoteUrl); + + foreach (var item in base.ToHttpContent()) + yield return item; + } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/CompressionQuality.cs b/src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/CompressionQuality.cs new file mode 100644 index 0000000..f05e670 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/CompressionQuality.cs @@ -0,0 +1,60 @@ +// Copyright 2019-2026 Chris Mohan, Jaben Cargman +// and GotenbergSharpApiClient Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Globalization; + +namespace Gotenberg.Sharp.API.Client.Domain.ValueObjects; + +/// +/// Represents a validated compression quality value (0-100) for screenshot output. +/// Only applies to JPEG format screenshots. +/// +public sealed class CompressionQuality : IEquatable +{ + public const int MinValue = 0; + public const int MaxValue = 100; + + public int Value { get; } + + private CompressionQuality(int value) + { + Value = value; + } + + public static CompressionQuality Create(int quality) + { + if (quality < MinValue || quality > MaxValue) + throw new ArgumentOutOfRangeException( + nameof(quality), + quality, + $"Compression quality must be between {MinValue} and {MaxValue}."); + + return new CompressionQuality(quality); + } + + public override string ToString() => Value.ToString(CultureInfo.InvariantCulture); + + public bool Equals(CompressionQuality? other) => other is not null && Value == other.Value; + + public override bool Equals(object? obj) => Equals(obj as CompressionQuality); + + public override int GetHashCode() => Value; + + public static implicit operator int(CompressionQuality quality) => quality.Value; + + public static bool operator ==(CompressionQuality? left, CompressionQuality? right) => Equals(left, right); + + public static bool operator !=(CompressionQuality? left, CompressionQuality? right) => !Equals(left, right); +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/ScreenDimension.cs b/src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/ScreenDimension.cs new file mode 100644 index 0000000..d3e0f98 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/ScreenDimension.cs @@ -0,0 +1,57 @@ +// Copyright 2019-2026 Chris Mohan, Jaben Cargman +// and GotenbergSharpApiClient Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Globalization; + +namespace Gotenberg.Sharp.API.Client.Domain.ValueObjects; + +/// +/// Represents a validated screen dimension (width or height) in pixels for screenshots. +/// Must be a positive integer. +/// +public sealed class ScreenDimension : IEquatable +{ + public int Value { get; } + + private ScreenDimension(int value) + { + Value = value; + } + + public static ScreenDimension Create(int pixels) + { + if (pixels <= 0) + throw new ArgumentOutOfRangeException( + nameof(pixels), + pixels, + "Screen dimension must be a positive integer."); + + return new ScreenDimension(pixels); + } + + public override string ToString() => Value.ToString(CultureInfo.InvariantCulture); + + public bool Equals(ScreenDimension? other) => other is not null && Value == other.Value; + + public override bool Equals(object? obj) => Equals(obj as ScreenDimension); + + public override int GetHashCode() => Value; + + public static implicit operator int(ScreenDimension dim) => dim.Value; + + public static bool operator ==(ScreenDimension? left, ScreenDimension? right) => Equals(left, right); + + public static bool operator !=(ScreenDimension? left, ScreenDimension? right) => !Equals(left, right); +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/ScreenshotFormat.cs b/src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/ScreenshotFormat.cs new file mode 100644 index 0000000..b80219c --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/ScreenshotFormat.cs @@ -0,0 +1,37 @@ +// Copyright 2019-2026 Chris Mohan, Jaben Cargman +// and GotenbergSharpApiClient Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Gotenberg.Sharp.API.Client.Domain.ValueObjects; + +/// +/// Represents the output image format for Chromium screenshot routes. +/// +public enum ScreenshotFormat +{ + Png, + Jpeg, + Webp +} + +internal static class ScreenshotFormatExtensions +{ + internal static string ToFormValue(this ScreenshotFormat format) => format switch + { + ScreenshotFormat.Png => "png", + ScreenshotFormat.Jpeg => "jpeg", + ScreenshotFormat.Webp => "webp", + _ => throw new ArgumentOutOfRangeException(nameof(format)) + }; +} diff --git a/src/Gotenberg.Sharp.Api.Client/GotenbergSharpClient.cs b/src/Gotenberg.Sharp.Api.Client/GotenbergSharpClient.cs index 5f85e40..e0982c0 100644 --- a/src/Gotenberg.Sharp.Api.Client/GotenbergSharpClient.cs +++ b/src/Gotenberg.Sharp.Api.Client/GotenbergSharpClient.cs @@ -242,6 +242,66 @@ public virtual async Task ConvertPdfDocumentsAsync( .ConfigureAwait(false); } + /// + /// Captures a screenshot of HTML content using Gotenberg's Chromium module. + /// + /// The HTML screenshot request. + /// Cancellation token for the async operation. + /// A stream containing the screenshot image (PNG, JPEG, or WebP). + /// Gotenberg Screenshot HTML Documentation + public virtual Task ScreenshotHtmlAsync( + ScreenshotHtmlRequest request, + CancellationToken cancelToken = default) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + + return this.ExecuteRequestAsync(request.CreateApiRequest(), cancelToken); + } + + /// + /// Captures a screenshot of HTML content using a builder pattern. + /// + public virtual async Task ScreenshotHtmlAsync( + ScreenshotHtmlRequestBuilder builder, + CancellationToken cancelToken = default) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + var request = await builder.BuildAsync().ConfigureAwait(false); + + return await this.ScreenshotHtmlAsync(request, cancelToken).ConfigureAwait(false); + } + + /// + /// Captures a screenshot of a URL using Gotenberg's Chromium module. + /// + /// The URL screenshot request. + /// Cancellation token for the async operation. + /// A stream containing the screenshot image (PNG, JPEG, or WebP). + /// Gotenberg Screenshot URL Documentation + public virtual Task ScreenshotUrlAsync( + ScreenshotUrlRequest request, + CancellationToken cancelToken = default) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + + return this.ExecuteRequestAsync(request.CreateApiRequest(), cancelToken); + } + + /// + /// Captures a screenshot of a URL using a builder pattern. + /// + public virtual async Task ScreenshotUrlAsync( + ScreenshotUrlRequestBuilder builder, + CancellationToken cancelToken = default) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + var request = await builder.BuildAsync().ConfigureAwait(false); + + return await this.ScreenshotUrlAsync(request, cancelToken).ConfigureAwait(false); + } + /// /// Executes a standalone PDF engine operation (flatten, rotate, split, encrypt, metadata). /// diff --git a/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs index 8487b7d..2fd3fb9 100644 --- a/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs +++ b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs @@ -305,6 +305,23 @@ public static class ApiPaths public const string ConvertHtml = $"{Root}/convert/html"; public const string ConvertMarkdown = $"{Root}/convert/markdown"; + + public const string ScreenshotUrl = $"{Root}/screenshot/url"; + + public const string ScreenshotHtml = $"{Root}/screenshot/html"; + + public const string ScreenshotMarkdown = $"{Root}/screenshot/markdown"; + } + + public static class Screenshot + { + public const string Width = "width"; + public const string Height = "height"; + public const string Clip = "clip"; + public const string Format = "format"; + public const string Quality = "quality"; + public const string OmitBackground = "omitBackground"; + public const string OptimizeForSpeed = "optimizeForSpeed"; } public static class Routes diff --git a/test/GotenbergSharpClient.Tests/ScreenshotTests.cs b/test/GotenbergSharpClient.Tests/ScreenshotTests.cs new file mode 100644 index 0000000..58625ee --- /dev/null +++ b/test/GotenbergSharpClient.Tests/ScreenshotTests.cs @@ -0,0 +1,202 @@ +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Requests; +using Gotenberg.Sharp.API.Client.Domain.Requests.Facets; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Domain.ValueObjects; +using Gotenberg.Sharp.API.Client.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace GotenbergSharpClient.Tests; + +[TestFixture] +public class ScreenshotTests +{ + #region Value Object Tests + + [TestCase(1)] + [TestCase(800)] + [TestCase(3840)] + public void ScreenDimension_Create_WithValidValue_Succeeds(int pixels) + { + var dim = ScreenDimension.Create(pixels); + dim.Value.Should().Be(pixels); + } + + [TestCase(0)] + [TestCase(-1)] + public void ScreenDimension_Create_WithInvalidValue_Throws(int pixels) + { + var act = () => ScreenDimension.Create(pixels); + act.Should().ThrowExactly(); + } + + [TestCase(0)] + [TestCase(50)] + [TestCase(100)] + public void CompressionQuality_Create_WithValidValue_Succeeds(int quality) + { + var q = CompressionQuality.Create(quality); + q.Value.Should().Be(quality); + } + + [TestCase(-1)] + [TestCase(101)] + public void CompressionQuality_Create_WithInvalidValue_Throws(int quality) + { + var act = () => CompressionQuality.Create(quality); + act.Should().ThrowExactly(); + } + + #endregion + + #region Builder Tests + + [Test] + public void ScreenshotHtmlBuilder_SetsAllProperties() + { + var builder = new ScreenshotHtmlRequestBuilder() + .AddDocument(doc => doc.SetBody("test")) + .WithScreenshotProperties(p => p + .SetSize(1920, 1080) + .SetClip() + .SetFormat(ScreenshotFormat.Jpeg) + .SetQuality(80) + .SetOmitBackground() + .SetOptimizeForSpeed()) + .SetConversionBehaviors(b => b.SkipNetworkIdleEvent()); + + var request = builder.Build(); + + request.ScreenshotProperties.Width!.Value.Should().Be(1920); + request.ScreenshotProperties.Height!.Value.Should().Be(1080); + request.ScreenshotProperties.Clip.Should().BeTrue(); + request.ScreenshotProperties.Format.Should().Be(ScreenshotFormat.Jpeg); + request.ScreenshotProperties.Quality!.Value.Should().Be(80); + request.ScreenshotProperties.OmitBackground.Should().BeTrue(); + request.ScreenshotProperties.OptimizeForSpeed.Should().BeTrue(); + } + + [Test] + public void ScreenshotUrlBuilder_SetsUrl() + { + var builder = new ScreenshotUrlRequestBuilder() + .SetUrl("https://example.com") + .WithScreenshotProperties(p => p.SetFormat(ScreenshotFormat.Png)); + + var request = builder.Build(); + + request.Url!.ToString().Should().Be("https://example.com/"); + request.ScreenshotProperties.Format.Should().Be(ScreenshotFormat.Png); + } + + [Test] + public void ScreenshotUrlBuilder_WithRelativeUrl_Throws() + { + var builder = new ScreenshotUrlRequestBuilder(); + + var act = () => builder.SetUrl(new Uri("/relative", UriKind.Relative)); + + act.Should().ThrowExactly(); + } + + #endregion + + #region Serialization Tests + + [Test] + public async Task ScreenshotProperties_SerializesCorrectly() + { + var props = new ScreenshotProperties + { + Width = ScreenDimension.Create(1920), + Height = ScreenDimension.Create(1080), + Clip = true, + Format = ScreenshotFormat.Jpeg, + Quality = CompressionQuality.Create(80), + OptimizeForSpeed = true + }; + + var httpContents = props.ToHttpContent().ToList(); + + (await httpContents.FirstOrDefault(c => + c.Headers.ContentDisposition?.Name == "width")! + .ReadAsStringAsync()).Should().Be("1920"); + (await httpContents.FirstOrDefault(c => + c.Headers.ContentDisposition?.Name == "height")! + .ReadAsStringAsync()).Should().Be("1080"); + (await httpContents.FirstOrDefault(c => + c.Headers.ContentDisposition?.Name == "format")! + .ReadAsStringAsync()).Should().Be("jpeg"); + (await httpContents.FirstOrDefault(c => + c.Headers.ContentDisposition?.Name == "quality")! + .ReadAsStringAsync()).Should().Be("80"); + } + + #endregion + + #region Integration Tests + + [Category("Integration")] + [Test] + public async Task ScreenshotHtml_ReturnsImage() + { + var client = CreateAuthenticatedClient(); + + var builder = new ScreenshotHtmlRequestBuilder() + .AddDocument(doc => doc.SetBody( + "

Screenshot!

")) + .WithScreenshotProperties(p => p + .SetSize(800, 600) + .SetFormat(ScreenshotFormat.Png)); + + using var result = await client.ScreenshotHtmlAsync(builder); + + result.Should().NotBeNull(); + result.Length.Should().BeGreaterThan(0); + + // Verify it's a PNG (starts with PNG magic bytes) + result.Position = 0; + var header = new byte[8]; + await result.ReadAsync(header, 0, 8); + header[0].Should().Be(0x89); // PNG signature + header[1].Should().Be(0x50); // 'P' + header[2].Should().Be(0x4E); // 'N' + header[3].Should().Be(0x47); // 'G' + } + + [Category("Integration")] + [Test] + public async Task ScreenshotUrl_ReturnsImage() + { + var client = CreateAuthenticatedClient(); + + var builder = new ScreenshotUrlRequestBuilder() + .SetUrl("https://example.com") + .WithScreenshotProperties(p => p + .SetSize(1024, 768) + .SetFormat(ScreenshotFormat.Jpeg) + .SetQuality(90)); + + using var result = await client.ScreenshotUrlAsync(builder); + + result.Should().NotBeNull(); + result.Length.Should().BeGreaterThan(0); + } + + private static Gotenberg.Sharp.API.Client.GotenbergSharpClient CreateAuthenticatedClient() + { + var services = new ServiceCollection(); + services.AddOptions() + .Configure(options => + { + options.ServiceUrl = new Uri("http://localhost:3000"); + options.BasicAuthUsername = "testuser"; + options.BasicAuthPassword = "testpass"; + }); + services.AddGotenbergSharpClient(); + return services.BuildServiceProvider() + .GetRequiredService(); + } + + #endregion +}