From 36e829a7030a0fcc85c19e49e229eb22b9382e47 Mon Sep 17 00:00:00 2001 From: "weinrowsky.konrad" Date: Tue, 21 Jul 2026 11:09:09 +0200 Subject: [PATCH 1/4] feat: add embed on pdfengine --- .../Application/Builders/PdfEngineBuilder.cs | 15 ++++ .../Domain/Embed/Entry.cs | 24 +++++++ .../Domain/Requests/EmbedRequest.cs | 72 +++++++++++++++++++ .../Infrastructure/Constants.cs | 11 +++ .../PdfEngineOperationsTests.cs | 67 +++++++++++++++++ 5 files changed, 189 insertions(+) create mode 100644 src/Gotenberg.Sharp.Api.Client/Domain/Embed/Entry.cs create mode 100644 src/Gotenberg.Sharp.Api.Client/Domain/Requests/EmbedRequest.cs diff --git a/src/Gotenberg.Sharp.Api.Client/Application/Builders/PdfEngineBuilder.cs b/src/Gotenberg.Sharp.Api.Client/Application/Builders/PdfEngineBuilder.cs index e619077..3e80652 100644 --- a/src/Gotenberg.Sharp.Api.Client/Application/Builders/PdfEngineBuilder.cs +++ b/src/Gotenberg.Sharp.Api.Client/Application/Builders/PdfEngineBuilder.cs @@ -14,6 +14,7 @@ // limitations under the License. using Gotenberg.Sharp.API.Client.Application.Requests; +using Gotenberg.Sharp.API.Client.Domain.Embed; using Gotenberg.Sharp.API.Client.Domain.Rotation; using Gotenberg.Sharp.API.Client.Domain.Shared; using Gotenberg.Sharp.API.Client.Domain.Split; @@ -152,4 +153,18 @@ public static PdfEngineBuilder WriteMetadata(IDictionary + /// Creates a builder for embedding files into PDFs. + /// + /// A dictionary from file names to their data + public static PdfEngineBuilder Embed(IDictionary embedsMetadata) + { + var request = new EmbedRequest + { + EmbedsData = embedsMetadata, + }; + + return new PdfEngineBuilder(request); + } } diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Embed/Entry.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Embed/Entry.cs new file mode 100644 index 0000000..4c77c74 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Embed/Entry.cs @@ -0,0 +1,24 @@ +// 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.Embed +{ + public class Entry + { + public required string MimeType { get; set; } + public required string Relationship { get; set; } + public required ContentItem Content { get; set; } + } +} \ No newline at end of file diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/EmbedRequest.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/EmbedRequest.cs new file mode 100644 index 0000000..970dba8 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/EmbedRequest.cs @@ -0,0 +1,72 @@ +using System.Net.Http.Headers; +using Gotenberg.Sharp.API.Client.Domain.Embed; +using Gotenberg.Sharp.API.Client.Extensions; +using Gotenberg.Sharp.API.Client.Infrastructure; +using Newtonsoft.Json.Linq; + +namespace Gotenberg.Sharp.API.Client.Domain.Requests +{ + public sealed class EmbedRequest : PdfEngineRequest + { + /// + protected override string ApiPath => Constants.Gotenberg.PdfEngines.ApiPaths.Embed; + + public IDictionary? EmbedsData { get; set; } + + /// + protected override void Validate() + { + if (this.EmbedsData == null || !this.EmbedsData.Any()) + throw new InvalidOperationException("EmbedsMetadata is required and cannot be empty"); + + base.Validate(); + } + + /// + protected override IEnumerable ToHttpContent() + { + var metadataObject = new JObject(); + foreach (var kvp in EmbedsData!) + { + metadataObject.Add(kvp.Key, JObject.FromObject(new + { + kvp.Value.MimeType, + kvp.Value.Relationship + })); + } + + var metadataContent = new StringContent(metadataObject.ToString()); + metadataContent.Headers.ContentType = new MediaTypeHeaderValue(Constants.HttpContent.MediaTypes.ApplicationJson); + metadataContent.Headers.ContentDisposition = new ContentDispositionHeaderValue(Constants.HttpContent.Disposition.Types.FormData) + { + Name = "embedsMetadata", + }; + metadataContent.Headers.ContentType = new MediaTypeHeaderValue(Constants.HttpContent.MediaTypes.ApplicationJson); + + yield return metadataContent; + + foreach (var kvp in EmbedsData) + { + var contentItem = kvp.Value.Content.ToHttpContentItem(); + + contentItem.Headers.ContentDisposition = new ContentDispositionHeaderValue(Constants.HttpContent.Disposition.Types.FormData) + { + Name = "embeds", + FileName = kvp.Key, + }; + + if (!string.IsNullOrEmpty(kvp.Value.MimeType)) + { + contentItem.Headers.ContentType = new MediaTypeHeaderValue(kvp.Value.MimeType); + } + + yield return contentItem; + } + + foreach (var content in base.ToHttpContent()) + { + yield return content; + } + } + } +} \ No newline at end of file diff --git a/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs index 2fd3fb9..09369d9 100644 --- a/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs +++ b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs @@ -256,6 +256,8 @@ public static class ApiPaths public const string Watermark = $"{Root}/watermark"; public const string Stamp = $"{Root}/stamp"; + + public const string Embed = $"{Root}/embed"; } public static class Routes @@ -270,6 +272,15 @@ public static class Convert public const string PdfFormat = CrossCutting.PdfFormat; } } + + public static class EmbedRelation + { + public const string Source = "source"; + public const string Data = "data"; + public const string Alternative = "alternative"; + public const string Supplement = "supplement"; + public const string Unspecified = "unspecified"; + } } /// diff --git a/test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs b/test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs index 2011b6d..6aef12c 100644 --- a/test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs +++ b/test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs @@ -1,9 +1,12 @@ using Gotenberg.Sharp.API.Client.Application.Builders; +using Gotenberg.Sharp.API.Client.Domain.Embed; using Gotenberg.Sharp.API.Client.Domain.Requests; using Gotenberg.Sharp.API.Client.Domain.Settings; using Gotenberg.Sharp.API.Client.Domain.Split; using Gotenberg.Sharp.API.Client.Extensions; +using Gotenberg.Sharp.API.Client.Infrastructure; using Microsoft.Extensions.DependencyInjection; +using MimeMapping; using Newtonsoft.Json.Linq; namespace GotenbergSharpClient.Tests; @@ -93,6 +96,33 @@ public void PdfEngineBuilders_WriteMetadata_CreatesRequest() writeRequest.Metadata!["Author"]!.Value().Should().Be("Test"); } + [Test] + public void PdfEngineBuilders_Embed_CreatesRequest() + { + const string xml = """ + + """; + + var entries = new Dictionary + { + { + "test.xml", new Entry + { + MimeType = KnownMimeTypes.Xml, + Relationship = Constants.Gotenberg.PdfEngines.EmbedRelation.Data, + Content = new ContentItem(xml), + } + }, + }; + + var builder = PdfEngineBuilders.Embed(entries) + .WithPdfs(a => a.AddItem("test.pdf", new byte[] { 1, 2, 3 })); + var request = builder.Build(); + + var writeRequest = (EmbedRequest)request; + writeRequest.EmbedsData.Should().NotBeNull(); + } + [Test] public void PdfEngineBuilders_Rotate_WithInvalidAngle_Throws() { @@ -198,6 +228,43 @@ public async Task ReadMetadata_ReturnsJson() parsed.Should().NotBeEmpty(); } + [Category("Integration")] + [Test] + public async Task Emend() + { + const string xml = """ + + + Important data + + """; + + var entries = new Dictionary + { + { + "test.xml", new Entry + { + MimeType = KnownMimeTypes.Xml, + Relationship = Constants.Gotenberg.PdfEngines.EmbedRelation.Data, + Content = new ContentItem(xml), + } + }, + }; + + var client = CreateAuthenticatedClient(); + var pdfBytes = await GenerateTestPdf(client); + + var builder = PdfEngineBuilders.Embed(entries) + .WithPdfs(a => a.AddItem("test.pdf", pdfBytes)); + + var result = await client.ExecutePdfEngineAsync(builder); + + var file = File.Create("result.pdf"); + await result.CopyToAsync(file); + + result.Length.Should().BeGreaterThan(0); + } + #endregion private static Gotenberg.Sharp.API.Client.GotenbergSharpClient CreateAuthenticatedClient() From 2503386e8af7b888844685d2b3919cf42238b689 Mon Sep 17 00:00:00 2001 From: "weinrowsky.konrad" Date: Mon, 3 Aug 2026 10:43:43 +0200 Subject: [PATCH 2/4] fix: ensure exception mentions the correct field. --- src/Gotenberg.Sharp.Api.Client/Domain/Requests/EmbedRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/EmbedRequest.cs b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/EmbedRequest.cs index 970dba8..9cf1c81 100644 --- a/src/Gotenberg.Sharp.Api.Client/Domain/Requests/EmbedRequest.cs +++ b/src/Gotenberg.Sharp.Api.Client/Domain/Requests/EmbedRequest.cs @@ -17,7 +17,7 @@ public sealed class EmbedRequest : PdfEngineRequest protected override void Validate() { if (this.EmbedsData == null || !this.EmbedsData.Any()) - throw new InvalidOperationException("EmbedsMetadata is required and cannot be empty"); + throw new InvalidOperationException($"{nameof(EmbedsData)} is required and cannot be empty"); base.Validate(); } From a8f1a9b03f4f6b07146715a2d6e07f242570da5f Mon Sep 17 00:00:00 2001 From: "weinrowsky.konrad" Date: Mon, 3 Aug 2026 10:45:32 +0200 Subject: [PATCH 3/4] fix: Capitalize EmbedRelation strings to match specs --- .../Infrastructure/Constants.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs index 09369d9..5301ca5 100644 --- a/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs +++ b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs @@ -275,11 +275,11 @@ public static class Convert public static class EmbedRelation { - public const string Source = "source"; - public const string Data = "data"; - public const string Alternative = "alternative"; - public const string Supplement = "supplement"; - public const string Unspecified = "unspecified"; + public const string Source = "Source"; + public const string Data = "Data"; + public const string Alternative = "Alternative"; + public const string Supplement = "Supplement"; + public const string Unspecified = "Unspecified"; } } From 1f84d5b2440c19c3d2c15921a94ddc23dabaa630 Mon Sep 17 00:00:00 2001 From: "weinrowsky.konrad" Date: Mon, 3 Aug 2026 10:47:34 +0200 Subject: [PATCH 4/4] fix: rename test + dispose of result --- test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs b/test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs index 6aef12c..918aa95 100644 --- a/test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs +++ b/test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs @@ -230,7 +230,7 @@ public async Task ReadMetadata_ReturnsJson() [Category("Integration")] [Test] - public async Task Emend() + public async Task Embed_Succeeds() { const string xml = """ @@ -257,7 +257,7 @@ Important data var builder = PdfEngineBuilders.Embed(entries) .WithPdfs(a => a.AddItem("test.pdf", pdfBytes)); - var result = await client.ExecutePdfEngineAsync(builder); + await using var result = await client.ExecutePdfEngineAsync(builder); var file = File.Create("result.pdf"); await result.CopyToAsync(file);