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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -152,4 +153,18 @@ public static PdfEngineBuilder<WriteMetadataRequest> WriteMetadata(IDictionary<s
{
return WriteMetadata(JObject.FromObject(metadata));
}

/// <summary>
/// Creates a builder for embedding files into PDFs.
/// </summary>
/// <param name="embedsMetadata">A dictionary from file names to their data</param>
public static PdfEngineBuilder<EmbedRequest> Embed(IDictionary<string, Entry> embedsMetadata)
{
var request = new EmbedRequest
{
EmbedsData = embedsMetadata,
};

return new PdfEngineBuilder<EmbedRequest>(request);
}
}
24 changes: 24 additions & 0 deletions src/Gotenberg.Sharp.Api.Client/Domain/Embed/Entry.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
72 changes: 72 additions & 0 deletions src/Gotenberg.Sharp.Api.Client/Domain/Requests/EmbedRequest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <inheritdoc />
protected override string ApiPath => Constants.Gotenberg.PdfEngines.ApiPaths.Embed;

public IDictionary<string, Entry>? EmbedsData { get; set; }

/// <inheritdoc />
protected override void Validate()
{
if (this.EmbedsData == null || !this.EmbedsData.Any())
throw new InvalidOperationException($"{nameof(EmbedsData)} is required and cannot be empty");

base.Validate();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// <inheritdoc />
protected override IEnumerable<HttpContent> 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;
}
}
}
}
11 changes: 11 additions & 0 deletions src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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";
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// <summary>
Expand Down
67 changes: 67 additions & 0 deletions test/GotenbergSharpClient.Tests/PdfEngineOperationsTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -93,6 +96,33 @@ public void PdfEngineBuilders_WriteMetadata_CreatesRequest()
writeRequest.Metadata!["Author"]!.Value<string>().Should().Be("Test");
}

[Test]
public void PdfEngineBuilders_Embed_CreatesRequest()
{
const string xml = """
<?xml version="1.0" encoding="UTF-8"?>
""";

var entries = new Dictionary<string, Entry>
{
{
"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()
{
Expand Down Expand Up @@ -198,6 +228,43 @@ public async Task ReadMetadata_ReturnsJson()
parsed.Should().NotBeEmpty();
}

[Category("Integration")]
[Test]
public async Task Embed_Succeeds()
{
const string xml = """
<?xml version="1.0" encoding="UTF-8"?>
<data id="test">
Important data
</data>
""";

var entries = new Dictionary<string, Entry>
{
{
"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);
}

Comment thread
kokoISnoTarget marked this conversation as resolved.
#endregion

private static Gotenberg.Sharp.API.Client.GotenbergSharpClient CreateAuthenticatedClient()
Expand Down