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..9cf1c81 --- /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($"{nameof(EmbedsData)} 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..5301ca5 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..918aa95 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 Embed_Succeeds() + { + 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)); + + await using 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()