diff --git a/README.md b/README.md index c8f12fd..3d0b1b3 100644 --- a/README.md +++ b/README.md @@ -529,32 +529,36 @@ public async Task FastConversion() } ``` -### Standalone PDF Engine Operations -*Flatten, rotate, encrypt, and manipulate existing PDFs:* +### Watermark & Rotation +*Add text watermarks and rotate PDF pages — available on all request types:* ```csharp -// Flatten form fields -var flattenResult = await _sharpClient.ExecutePdfEngineAsync( - PdfEngineBuilders.Flatten().WithPdfs(a => a.AddItem("form.pdf", pdfBytes))); +public async Task CreateWatermarkedPdf() +{ + var builder = new HtmlRequestBuilder() + .AddDocument(doc => doc.SetBody("

Report

")) + .SetWatermarkOptions(w => w.SetTextWatermark("DRAFT", "1-3")) + .SetRotationOptions(r => r.SetAngle(90).SetPages("2")) + .WithPageProperties(pp => pp.UseChromeDefaults()); -// Rotate pages 90 degrees -var rotateResult = await _sharpClient.ExecutePdfEngineAsync( - PdfEngineBuilders.Rotate(90, "1-3").WithPdfs(a => a.AddItem("doc.pdf", pdfBytes))); + var request = builder.Build(); + return await _sharpClient.HtmlToPdfAsync(request); +} +``` -// Encrypt with passwords -var encrypted = await _sharpClient.ExecutePdfEngineAsync( - PdfEngineBuilders.Encrypt("reader123", "admin456").WithPdfs(a => a.AddItem("doc.pdf", pdfBytes))); +### Split PDFs +*Split generated PDFs into chunks or extract specific pages:* -// Read metadata (returns JSON) -var metadataJson = await _sharpClient.ReadPdfMetadataAsync( - PdfEngineBuilders.ReadMetadata().WithPdfs(a => a.AddItem("doc.pdf", pdfBytes))); +```csharp +public async Task SplitPdf() +{ + var builder = new HtmlRequestBuilder() + .AddDocument(doc => doc.SetBody("Multi-page content")) + .SetSplitOptions(s => s.SplitByPages("1-3,5", unify: true)); -// Write metadata -var result = await _sharpClient.ExecutePdfEngineAsync( - PdfEngineBuilders.WriteMetadata(new Dictionary - { - { "Author", "John Doe" }, { "Title", "My Document" } - }).WithPdfs(a => a.AddItem("doc.pdf", pdfBytes))); + var request = builder.Build(); + return await _sharpClient.HtmlToPdfAsync(request); +} ``` ### Custom Page Properties diff --git a/examples/WatermarkAndRotate/Program.cs b/examples/WatermarkAndRotate/Program.cs new file mode 100644 index 0000000..db1241c --- /dev/null +++ b/examples/WatermarkAndRotate/Program.cs @@ -0,0 +1,62 @@ +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 path = await CreateWatermarkedAndRotatedPdf(destinationDirectory, options); +Console.WriteLine($"Watermarked & rotated PDF created: {path}"); + +static async Task CreateWatermarkedAndRotatedPdf(string destinationDirectory, 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 }; + + using var httpClient = new HttpClient(effectiveHandler, disposeHandler: true) + { + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut + }; + + var sharpClient = new GotenbergSharpClient(httpClient); + + // Demonstrates watermark, stamp, rotation, and split options + var builder = new HtmlRequestBuilder() + .AddDocument(doc => doc.SetBody(@" + +

Cross-Cutting Features Demo

+

This PDF has a text watermark and is rotated 90 degrees.

+

Page 2 content here...

+ ")) + // Add a text watermark behind the content + .SetWatermarkOptions(w => w.SetTextWatermark("CONFIDENTIAL")) + // Rotate all pages 90 degrees + .SetRotationOptions(r => r.SetAngle(RotationAngle.Degrees90)) + .WithPageProperties(pp => pp.UseChromeDefaults()); + + var request = builder.Build(); + var response = await sharpClient.HtmlToPdfAsync(request); + + var resultPath = Path.Combine(destinationDirectory, $"WatermarkRotate-{DateTime.Now:yyyyMMddHHmmss}.pdf"); + + await using var destinationStream = File.Create(resultPath); + await response.CopyToAsync(destinationStream, CancellationToken.None); + + return resultPath; +} diff --git a/examples/WatermarkAndRotate/WatermarkAndRotate.csproj b/examples/WatermarkAndRotate/WatermarkAndRotate.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/WatermarkAndRotate/WatermarkAndRotate.csproj @@ -0,0 +1,2 @@ + + diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Builders/BaseBuilder.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/BaseBuilder.cs index c232fad..a1ba337 100644 --- a/src/Gotenberg.Sharp.Api.Client/Domain/Builders/BaseBuilder.cs +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/BaseBuilder.cs @@ -88,6 +88,63 @@ public TBuilder SetPdfOutputOptions(PdfOutputOptions options) return (TBuilder)this; } + /// + /// Configures rotation options for the resulting PDF. + /// + public TBuilder SetRotationOptions(Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + this.Request.RotationOptions ??= new RotationOptions(); + + action(new RotationOptionsBuilder(this.Request.RotationOptions)); + + return (TBuilder)this; + } + + /// + /// Configures split options for the resulting PDF. + /// When splitting returns multiple files, Gotenberg returns a ZIP. + /// + public TBuilder SetSplitOptions(Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + this.Request.SplitOptions ??= new SplitOptions(); + + action(new SplitOptionsBuilder(this.Request.SplitOptions)); + + return (TBuilder)this; + } + + /// + /// Configures watermark options (background overlay) for the resulting PDF. + /// + public TBuilder SetWatermarkOptions(Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + this.Request.WatermarkOptions ??= new WatermarkOptions(); + + action(new WatermarkOptionsBuilder(this.Request.WatermarkOptions)); + + return (TBuilder)this; + } + + /// + /// Configures stamp options (foreground overlay) for the resulting PDF. + /// + public TBuilder SetStampOptions(Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + + this.Request.StampOptions ??= new StampOptions(); + + action(new StampOptionsBuilder(this.Request.StampOptions)); + + return (TBuilder)this; + } + /// /// Builds the request synchronously. Use when all content is already in memory (no async operations). /// diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/RotationOptionsBuilder.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/RotationOptionsBuilder.cs new file mode 100644 index 0000000..ce72b74 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/RotationOptionsBuilder.cs @@ -0,0 +1,50 @@ +// 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; + +public sealed class RotationOptionsBuilder +{ + private readonly RotationOptions _options; + + internal RotationOptionsBuilder(RotationOptions options) + { + _options = options; + } + + public RotationOptionsBuilder SetAngle(RotationAngle angle) + { + _options.RotateAngle = angle ?? throw new ArgumentNullException(nameof(angle)); + return this; + } + + public RotationOptionsBuilder SetAngle(int degrees) + { + return SetAngle(RotationAngle.Create(degrees)); + } + + public RotationOptionsBuilder SetPages(PageRanges pages) + { + _options.RotatePages = pages ?? throw new ArgumentNullException(nameof(pages)); + return this; + } + + public RotationOptionsBuilder SetPages(string pages) + { + return SetPages(PageRanges.Create(pages)); + } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/SplitOptionsBuilder.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/SplitOptionsBuilder.cs new file mode 100644 index 0000000..312da36 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/SplitOptionsBuilder.cs @@ -0,0 +1,65 @@ +// 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; + +public sealed class SplitOptionsBuilder +{ + private readonly SplitOptions _options; + + internal SplitOptionsBuilder(SplitOptions options) + { + _options = options; + } + + public SplitOptionsBuilder SetMode(SplitMode mode) + { + _options.Mode = mode; + return this; + } + + public SplitOptionsBuilder SetSpan(string span) + { + if (string.IsNullOrWhiteSpace(span)) + throw new ArgumentException("Split span must not be null or empty.", nameof(span)); + + _options.Span = span; + return this; + } + + public SplitOptionsBuilder SetUnify(bool unify = true) + { + _options.Unify = unify; + return this; + } + + /// + /// Configures interval-based splitting (e.g., split every N pages). + /// + public SplitOptionsBuilder SplitByIntervals(string span) + { + return SetMode(SplitMode.Intervals).SetSpan(span); + } + + /// + /// Configures page-based splitting (e.g., extract specific page ranges). + /// + public SplitOptionsBuilder SplitByPages(string span, bool unify = false) + { + return SetMode(SplitMode.Pages).SetSpan(span).SetUnify(unify); + } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/StampOptionsBuilder.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/StampOptionsBuilder.cs new file mode 100644 index 0000000..8f7c053 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/StampOptionsBuilder.cs @@ -0,0 +1,76 @@ +// 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; + +using Newtonsoft.Json.Linq; + +namespace Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; + +public sealed class StampOptionsBuilder +{ + private readonly StampOptions _options; + + internal StampOptionsBuilder(StampOptions options) + { + _options = options; + } + + public StampOptionsBuilder SetSource(OverlaySource source) + { + _options.Source = source; + return this; + } + + public StampOptionsBuilder SetExpression(string expression) + { + if (string.IsNullOrWhiteSpace(expression)) + throw new ArgumentException("Expression must not be null or empty.", nameof(expression)); + + _options.Expression = expression; + return this; + } + + public StampOptionsBuilder SetPages(PageRanges pages) + { + _options.Pages = pages ?? throw new ArgumentNullException(nameof(pages)); + return this; + } + + public StampOptionsBuilder SetPages(string pages) + { + return SetPages(PageRanges.Create(pages)); + } + + public StampOptionsBuilder SetOptions(JObject options) + { + _options.Options = options ?? throw new ArgumentNullException(nameof(options)); + return this; + } + + /// + /// Convenience method for a text stamp. + /// + public StampOptionsBuilder SetTextStamp(string text, string? pages = null) + { + SetSource(OverlaySource.Text); + SetExpression(text); + + if (pages != null) + SetPages(pages); + + return this; + } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/WatermarkOptionsBuilder.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/WatermarkOptionsBuilder.cs new file mode 100644 index 0000000..a137231 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/WatermarkOptionsBuilder.cs @@ -0,0 +1,76 @@ +// 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; + +using Newtonsoft.Json.Linq; + +namespace Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; + +public sealed class WatermarkOptionsBuilder +{ + private readonly WatermarkOptions _options; + + internal WatermarkOptionsBuilder(WatermarkOptions options) + { + _options = options; + } + + public WatermarkOptionsBuilder SetSource(OverlaySource source) + { + _options.Source = source; + return this; + } + + public WatermarkOptionsBuilder SetExpression(string expression) + { + if (string.IsNullOrWhiteSpace(expression)) + throw new ArgumentException("Expression must not be null or empty.", nameof(expression)); + + _options.Expression = expression; + return this; + } + + public WatermarkOptionsBuilder SetPages(PageRanges pages) + { + _options.Pages = pages ?? throw new ArgumentNullException(nameof(pages)); + return this; + } + + public WatermarkOptionsBuilder SetPages(string pages) + { + return SetPages(PageRanges.Create(pages)); + } + + public WatermarkOptionsBuilder SetOptions(JObject options) + { + _options.Options = options ?? throw new ArgumentNullException(nameof(options)); + return this; + } + + /// + /// Convenience method for a text watermark. + /// + public WatermarkOptionsBuilder SetTextWatermark(string text, string? pages = null) + { + SetSource(OverlaySource.Text); + SetExpression(text); + + if (pages != null) + SetPages(pages); + + return this; + } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/BuildRequestBase.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/BuildRequestBase.cs index 5c88f90..efb487f 100644 --- a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/BuildRequestBase.cs +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/BuildRequestBase.cs @@ -28,6 +28,26 @@ public abstract class BuildRequestBase /// public PdfOutputOptions? PdfOutputOptions { get; set; } + /// + /// Cross-cutting rotation options (angle and page ranges). + /// + public RotationOptions? RotationOptions { get; set; } + + /// + /// Cross-cutting split options (mode, span, and unify). + /// + public SplitOptions? SplitOptions { get; set; } + + /// + /// Cross-cutting watermark options (background overlay). + /// + public WatermarkOptions? WatermarkOptions { get; set; } + + /// + /// Cross-cutting stamp options (foreground overlay). + /// + public StampOptions? StampOptions { get; set; } + protected abstract string ApiPath { get; } private const string _dispositionType = Constants.HttpContent.Disposition.Types.FormData; @@ -43,7 +63,11 @@ internal static StringContent CreateFormDataItem(T value, string fieldName) protected virtual IEnumerable ToHttpContent() { - return this.PdfOutputOptions.IfNullEmptyContent(); + return this.PdfOutputOptions.IfNullEmptyContent() + .Concat(this.RotationOptions.IfNullEmptyContent()) + .Concat(this.SplitOptions.IfNullEmptyContent()) + .Concat(this.WatermarkOptions.IfNullEmptyContent()) + .Concat(this.StampOptions.IfNullEmptyContent()); } protected virtual void Validate() 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 dda01d1..4ebbab4 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,8 @@ public virtual IEnumerable ToHttpContent() ConversionPdfFormats format => format.ToFormDataValue(), PdfPassword password => password.Value, List cookies => JsonConvert.SerializeObject(cookies), + OverlaySource overlaySource => overlaySource.ToFormValue(), + SplitMode splitMode => splitMode.ToFormValue(), float f => f.ToString(cultureInfo), double d => d.ToString(cultureInfo), decimal c => c.ToString(cultureInfo), diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/RotationOptions.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/RotationOptions.cs new file mode 100644 index 0000000..98898f4 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/RotationOptions.cs @@ -0,0 +1,30 @@ +// 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; + +/// +/// Cross-cutting rotation options. Applies to Chromium, LibreOffice, and PDF engine routes. +/// +public class RotationOptions : FacetBase +{ + [MultiFormHeader(Constants.Gotenberg.CrossCuttingOptions.RotateAngle)] + public RotationAngle? RotateAngle { get; set; } + + [MultiFormHeader(Constants.Gotenberg.CrossCuttingOptions.RotatePages)] + public PageRanges? RotatePages { get; set; } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/SplitOptions.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/SplitOptions.cs new file mode 100644 index 0000000..f5c8555 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/SplitOptions.cs @@ -0,0 +1,33 @@ +// 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; + +/// +/// Cross-cutting split options. When splitting returns multiple files, Gotenberg returns a ZIP. +/// +public class SplitOptions : FacetBase +{ + [MultiFormHeader(Constants.Gotenberg.CrossCuttingOptions.SplitMode)] + public SplitMode? Mode { get; set; } + + [MultiFormHeader(Constants.Gotenberg.CrossCuttingOptions.SplitSpan)] + public string? Span { get; set; } + + [MultiFormHeader(Constants.Gotenberg.CrossCuttingOptions.SplitUnify)] + public bool? Unify { get; set; } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/StampOptions.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/StampOptions.cs new file mode 100644 index 0000000..0f663c5 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/StampOptions.cs @@ -0,0 +1,39 @@ +// 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; + +using Newtonsoft.Json.Linq; + +namespace Gotenberg.Sharp.API.Client.Domain.Requests.Facets; + +/// +/// Cross-cutting stamp options. Applies a foreground overlay to generated PDFs. +/// Same structure as watermark but rendered in front of content. +/// +public class StampOptions : FacetBase +{ + [MultiFormHeader(Constants.Gotenberg.CrossCuttingOptions.StampSource)] + public OverlaySource? Source { get; set; } + + [MultiFormHeader(Constants.Gotenberg.CrossCuttingOptions.StampExpression)] + public string? Expression { get; set; } + + [MultiFormHeader(Constants.Gotenberg.CrossCuttingOptions.StampPages)] + public PageRanges? Pages { get; set; } + + [MultiFormHeader(Constants.Gotenberg.CrossCuttingOptions.StampOptionsJson)] + public JObject? Options { get; set; } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/WatermarkOptions.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/WatermarkOptions.cs new file mode 100644 index 0000000..0a9c979 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/WatermarkOptions.cs @@ -0,0 +1,38 @@ +// 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; + +using Newtonsoft.Json.Linq; + +namespace Gotenberg.Sharp.API.Client.Domain.Requests.Facets; + +/// +/// Cross-cutting watermark options. Applies a background overlay to generated PDFs. +/// +public class WatermarkOptions : FacetBase +{ + [MultiFormHeader(Constants.Gotenberg.CrossCuttingOptions.WatermarkSource)] + public OverlaySource? Source { get; set; } + + [MultiFormHeader(Constants.Gotenberg.CrossCuttingOptions.WatermarkExpression)] + public string? Expression { get; set; } + + [MultiFormHeader(Constants.Gotenberg.CrossCuttingOptions.WatermarkPages)] + public PageRanges? Pages { get; set; } + + [MultiFormHeader(Constants.Gotenberg.CrossCuttingOptions.WatermarkOptionsJson)] + public JObject? Options { get; set; } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/OverlaySource.cs b/src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/OverlaySource.cs new file mode 100644 index 0000000..2ca61eb --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/ValueObjects/OverlaySource.cs @@ -0,0 +1,38 @@ +// 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 source type for a watermark or stamp overlay. +/// Used for both watermarkSource and stampSource form fields. +/// +public enum OverlaySource +{ + Text, + Image, + Pdf +} + +internal static class OverlaySourceExtensions +{ + internal static string ToFormValue(this OverlaySource source) => source switch + { + OverlaySource.Text => "text", + OverlaySource.Image => "image", + OverlaySource.Pdf => "pdf", + _ => throw new ArgumentOutOfRangeException(nameof(source)) + }; +} diff --git a/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs index 360ca04..3636f5b 100644 --- a/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs +++ b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs @@ -100,6 +100,33 @@ internal static class FileNames } } + /// + /// Cross-cutting options that apply across multiple modules (Chromium, LibreOffice, PDF Engines). + /// + public static class CrossCuttingOptions + { + // Rotation + public const string RotateAngle = "rotateAngle"; + public const string RotatePages = "rotatePages"; + + // Split + public const string SplitMode = "splitMode"; + public const string SplitSpan = "splitSpan"; + public const string SplitUnify = "splitUnify"; + + // Watermark + public const string WatermarkSource = "watermarkSource"; + public const string WatermarkExpression = "watermarkExpression"; + public const string WatermarkPages = "watermarkPages"; + public const string WatermarkOptionsJson = "watermarkOptions"; + + // Stamp + public const string StampSource = "stampSource"; + public const string StampExpression = "stampExpression"; + public const string StampPages = "stampPages"; + public const string StampOptionsJson = "stampOptions"; + } + /// /// PDF output options shared across all modules (Chromium, LibreOffice, PDF Engines). /// diff --git a/test/GotenbergSharpClient.Tests/CrossCuttingOptionsTests.cs b/test/GotenbergSharpClient.Tests/CrossCuttingOptionsTests.cs new file mode 100644 index 0000000..3b40628 --- /dev/null +++ b/test/GotenbergSharpClient.Tests/CrossCuttingOptionsTests.cs @@ -0,0 +1,295 @@ +using Gotenberg.Sharp.API.Client.Domain.Builders; +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 CrossCuttingOptionsTests +{ + #region Value Object Tests + + [TestCase(90)] + [TestCase(180)] + [TestCase(270)] + public void RotationAngle_Create_WithValidAngle_Succeeds(int angle) + { + var result = RotationAngle.Create(angle); + result.Value.Should().Be(angle); + } + + [TestCase(0)] + [TestCase(45)] + [TestCase(360)] + public void RotationAngle_Create_WithInvalidAngle_Throws(int angle) + { + var act = () => RotationAngle.Create(angle); + act.Should().ThrowExactly(); + } + + [TestCase("1-3")] + [TestCase("5")] + [TestCase("1-3,5,8-10")] + [TestCase("1")] + public void PageRanges_Create_WithValidFormat_Succeeds(string ranges) + { + var result = PageRanges.Create(ranges); + result.Value.Should().Be(ranges); + } + + [TestCase(null)] + [TestCase("")] + [TestCase("abc")] + [TestCase("1-")] + [TestCase("-3")] + public void PageRanges_Create_WithInvalidFormat_Throws(string? ranges) + { + var act = () => PageRanges.Create(ranges!); + act.Should().Throw(); + } + + #endregion + + #region Rotation Builder Tests + + [Test] + public void SetRotationOptions_WithAngleAndPages_SetsProperties() + { + var builder = new HtmlRequestBuilder() + .AddDocument(doc => doc.SetBody("test")) + .SetRotationOptions(r => r + .SetAngle(90) + .SetPages("1-3")); + + var request = builder.Build(); + + request.RotationOptions!.RotateAngle!.Value.Should().Be(90); + request.RotationOptions.RotatePages!.Value.Should().Be("1-3"); + } + + [Test] + public void SetRotationOptions_WithStaticAngle_Works() + { + var builder = new HtmlRequestBuilder() + .AddDocument(doc => doc.SetBody("test")) + .SetRotationOptions(r => r.SetAngle(RotationAngle.Degrees180)); + + var request = builder.Build(); + + request.RotationOptions!.RotateAngle!.Value.Should().Be(180); + } + + #endregion + + #region Split Builder Tests + + [Test] + public void SetSplitOptions_SplitByIntervals_SetsProperties() + { + var builder = new HtmlRequestBuilder() + .AddDocument(doc => doc.SetBody("test")) + .SetSplitOptions(s => s.SplitByIntervals("2")); + + var request = builder.Build(); + + request.SplitOptions!.Mode.Should().Be(SplitMode.Intervals); + request.SplitOptions.Span.Should().Be("2"); + } + + [Test] + public void SetSplitOptions_SplitByPages_WithUnify_SetsProperties() + { + var builder = new HtmlRequestBuilder() + .AddDocument(doc => doc.SetBody("test")) + .SetSplitOptions(s => s.SplitByPages("1-3,5", unify: true)); + + var request = builder.Build(); + + request.SplitOptions!.Mode.Should().Be(SplitMode.Pages); + request.SplitOptions.Span.Should().Be("1-3,5"); + request.SplitOptions.Unify.Should().BeTrue(); + } + + #endregion + + #region Watermark Builder Tests + + [Test] + public void SetWatermarkOptions_TextWatermark_SetsProperties() + { + var builder = new HtmlRequestBuilder() + .AddDocument(doc => doc.SetBody("test")) + .SetWatermarkOptions(w => w.SetTextWatermark("DRAFT", "1-3")); + + var request = builder.Build(); + + request.WatermarkOptions!.Source.Should().Be(OverlaySource.Text); + request.WatermarkOptions.Expression.Should().Be("DRAFT"); + request.WatermarkOptions.Pages!.Value.Should().Be("1-3"); + } + + #endregion + + #region Stamp Builder Tests + + [Test] + public void SetStampOptions_TextStamp_SetsProperties() + { + var builder = new HtmlRequestBuilder() + .AddDocument(doc => doc.SetBody("test")) + .SetStampOptions(s => s.SetTextStamp("CONFIDENTIAL")); + + var request = builder.Build(); + + request.StampOptions!.Source.Should().Be(OverlaySource.Text); + request.StampOptions.Expression.Should().Be("CONFIDENTIAL"); + } + + #endregion + + #region Serialization Tests + + [Test] + public async Task RotationOptions_SerializesCorrectly() + { + var options = new RotationOptions + { + RotateAngle = RotationAngle.Degrees90, + RotatePages = PageRanges.Create("1-3") + }; + + var httpContents = options.ToHttpContent().ToList(); + + var angleContent = httpContents.FirstOrDefault(c => + c.Headers.ContentDisposition?.Name == "rotateAngle")!; + (await angleContent.ReadAsStringAsync()).Should().Be("90"); + + var pagesContent = httpContents.FirstOrDefault(c => + c.Headers.ContentDisposition?.Name == "rotatePages")!; + (await pagesContent.ReadAsStringAsync()).Should().Be("1-3"); + } + + [Test] + public async Task SplitOptions_SerializesCorrectly() + { + var options = new SplitOptions + { + Mode = SplitMode.Intervals, + Span = "2", + Unify = false + }; + + var httpContents = options.ToHttpContent().ToList(); + + var modeContent = httpContents.FirstOrDefault(c => + c.Headers.ContentDisposition?.Name == "splitMode")!; + (await modeContent.ReadAsStringAsync()).Should().Be("intervals"); + + var spanContent = httpContents.FirstOrDefault(c => + c.Headers.ContentDisposition?.Name == "splitSpan")!; + (await spanContent.ReadAsStringAsync()).Should().Be("2"); + } + + [Test] + public async Task WatermarkOptions_SerializesCorrectly() + { + var options = new WatermarkOptions + { + Source = OverlaySource.Text, + Expression = "DRAFT" + }; + + var httpContents = options.ToHttpContent().ToList(); + + var sourceContent = httpContents.FirstOrDefault(c => + c.Headers.ContentDisposition?.Name == "watermarkSource")!; + (await sourceContent.ReadAsStringAsync()).Should().Be("text"); + + var exprContent = httpContents.FirstOrDefault(c => + c.Headers.ContentDisposition?.Name == "watermarkExpression")!; + (await exprContent.ReadAsStringAsync()).Should().Be("DRAFT"); + } + + [Test] + public async Task StampOptions_SerializesCorrectly() + { + var options = new StampOptions + { + Source = OverlaySource.Image, + Expression = "logo.png", + Pages = PageRanges.Create("1") + }; + + var httpContents = options.ToHttpContent().ToList(); + + var sourceContent = httpContents.FirstOrDefault(c => + c.Headers.ContentDisposition?.Name == "stampSource")!; + (await sourceContent.ReadAsStringAsync()).Should().Be("image"); + + var exprContent = httpContents.FirstOrDefault(c => + c.Headers.ContentDisposition?.Name == "stampExpression")!; + (await exprContent.ReadAsStringAsync()).Should().Be("logo.png"); + + var pagesContent = httpContents.FirstOrDefault(c => + c.Headers.ContentDisposition?.Name == "stampPages")!; + (await pagesContent.ReadAsStringAsync()).Should().Be("1"); + } + + #endregion + + #region Integration Tests + + [Category("Integration")] + [Test] + public async Task HtmlToPdf_WithRotation_Succeeds() + { + var client = CreateAuthenticatedClient(); + + var builder = new HtmlRequestBuilder() + .AddDocument(doc => doc.SetBody( + "

Rotated PDF

")) + .SetRotationOptions(r => r.SetAngle(90)); + + var result = await client.HtmlToPdfAsync(builder); + + result.Should().NotBeNull(); + result.Length.Should().BeGreaterThan(0); + } + + [Category("Integration")] + [Test] + public async Task HtmlToPdf_WithWatermark_Succeeds() + { + var client = CreateAuthenticatedClient(); + + var builder = new HtmlRequestBuilder() + .AddDocument(doc => doc.SetBody( + "

Watermarked PDF

")) + .SetWatermarkOptions(w => w.SetTextWatermark("DRAFT")); + + var result = await client.HtmlToPdfAsync(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 +}