Skip to content

Commit d9da3b5

Browse files
committed
Add standalone PDF engine operations (flatten, rotate, split, encrypt, metadata)
Introduce PdfEngineRequest base class and concrete request types: FlattenPdfRequest, RotatePdfRequest, SplitPdfRequest, EncryptPdfRequest, ReadMetadataRequest, WriteMetadataRequest. Each maps to a standalone /forms/pdfengines/* route. Add PdfEngineBuilders static factory with fluent builder creation for each operation. Add ExecutePdfEngineAsync and ReadPdfMetadataAsync methods to GotenbergSharpClient. Include RotationAngle, PageRanges, and SplitMode value objects for domain validation. Add PdfEngineOperations example and README section.
1 parent 09b6c02 commit d9da3b5

17 files changed

Lines changed: 980 additions & 0 deletions

File tree

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,34 @@ public async Task<Stream> FastConversion()
492492
}
493493
```
494494

495+
### Standalone PDF Engine Operations
496+
*Flatten, rotate, encrypt, and manipulate existing PDFs:*
497+
498+
```csharp
499+
// Flatten form fields
500+
var flattenResult = await _sharpClient.ExecutePdfEngineAsync(
501+
PdfEngineBuilders.Flatten().WithPdfs(a => a.AddItem("form.pdf", pdfBytes)));
502+
503+
// Rotate pages 90 degrees
504+
var rotateResult = await _sharpClient.ExecutePdfEngineAsync(
505+
PdfEngineBuilders.Rotate(90, "1-3").WithPdfs(a => a.AddItem("doc.pdf", pdfBytes)));
506+
507+
// Encrypt with passwords
508+
var encrypted = await _sharpClient.ExecutePdfEngineAsync(
509+
PdfEngineBuilders.Encrypt("reader123", "admin456").WithPdfs(a => a.AddItem("doc.pdf", pdfBytes)));
510+
511+
// Read metadata (returns JSON)
512+
var metadataJson = await _sharpClient.ReadPdfMetadataAsync(
513+
PdfEngineBuilders.ReadMetadata().WithPdfs(a => a.AddItem("doc.pdf", pdfBytes)));
514+
515+
// Write metadata
516+
var result = await _sharpClient.ExecutePdfEngineAsync(
517+
PdfEngineBuilders.WriteMetadata(new Dictionary<string, object>
518+
{
519+
{ "Author", "John Doe" }, { "Title", "My Document" }
520+
}).WithPdfs(a => a.AddItem("doc.pdf", pdfBytes)));
521+
```
522+
495523
### Custom Page Properties
496524
*Fine-tune page dimensions and properties:*
497525

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
</Project>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using Gotenberg.Sharp.API.Client;
2+
using Gotenberg.Sharp.API.Client.Domain.Builders;
3+
using Gotenberg.Sharp.API.Client.Domain.ValueObjects;
4+
using Gotenberg.Sharp.API.Client.Domain.Settings;
5+
using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline;
6+
7+
using Microsoft.Extensions.Configuration;
8+
using Newtonsoft.Json.Linq;
9+
10+
var config = new ConfigurationBuilder()
11+
.SetBasePath(AppContext.BaseDirectory)
12+
.AddJsonFile("appsettings.json")
13+
.Build();
14+
15+
var options = new GotenbergSharpClientOptions();
16+
config.GetSection(nameof(GotenbergSharpClient)).Bind(options);
17+
18+
var destinationDirectory = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output");
19+
Directory.CreateDirectory(destinationDirectory);
20+
21+
var sharpClient = CreateClient(options);
22+
23+
// First generate a test PDF
24+
Console.WriteLine("Generating test PDF...");
25+
var pdfBytes = await GenerateTestPdf(sharpClient);
26+
27+
// Flatten
28+
Console.WriteLine("Flattening PDF...");
29+
var flattenResult = await sharpClient.ExecutePdfEngineAsync(
30+
PdfEngineBuilders.Flatten().WithPdfs(a => a.AddItem("test.pdf", pdfBytes)));
31+
await SaveStream(flattenResult, destinationDirectory, "Flattened.pdf");
32+
33+
// Rotate 90 degrees
34+
Console.WriteLine("Rotating PDF 90 degrees...");
35+
var rotateResult = await sharpClient.ExecutePdfEngineAsync(
36+
PdfEngineBuilders.Rotate(90).WithPdfs(a => a.AddItem("test.pdf", pdfBytes)));
37+
await SaveStream(rotateResult, destinationDirectory, "Rotated.pdf");
38+
39+
// Encrypt
40+
Console.WriteLine("Encrypting PDF...");
41+
var encryptResult = await sharpClient.ExecutePdfEngineAsync(
42+
PdfEngineBuilders.Encrypt("reader123", "admin456").WithPdfs(a => a.AddItem("test.pdf", pdfBytes)));
43+
await SaveStream(encryptResult, destinationDirectory, "Encrypted.pdf");
44+
45+
// Write metadata
46+
Console.WriteLine("Writing metadata...");
47+
var writeResult = await sharpClient.ExecutePdfEngineAsync(
48+
PdfEngineBuilders.WriteMetadata(new Dictionary<string, object>
49+
{
50+
{ "Author", "GotenbergSharpApiClient" },
51+
{ "Title", "PDF Engine Demo" }
52+
}).WithPdfs(a => a.AddItem("test.pdf", pdfBytes)));
53+
await SaveStream(writeResult, destinationDirectory, "WithMetadata.pdf");
54+
55+
// Read metadata
56+
Console.WriteLine("Reading metadata...");
57+
var metadataJson = await sharpClient.ReadPdfMetadataAsync(
58+
PdfEngineBuilders.ReadMetadata().WithPdfs(a => a.AddItem("test.pdf", pdfBytes)));
59+
var parsed = JObject.Parse(metadataJson);
60+
Console.WriteLine($"Metadata: {parsed.ToString(Newtonsoft.Json.Formatting.Indented)}");
61+
62+
Console.WriteLine($"\nAll output saved to: {destinationDirectory}");
63+
64+
static async Task<byte[]> GenerateTestPdf(GotenbergSharpClient client)
65+
{
66+
var builder = new HtmlRequestBuilder()
67+
.AddDocument(doc => doc.SetBody(@"
68+
<html><body>
69+
<h1>PDF Engine Operations Demo</h1>
70+
<p>This PDF is used to demonstrate standalone PDF engine operations.</p>
71+
<form><input type='text' name='field1' value='Form field (will be flattened)'/></form>
72+
</body></html>"));
73+
74+
var stream = await client.HtmlToPdfAsync(builder);
75+
using var ms = new MemoryStream();
76+
await stream.CopyToAsync(ms);
77+
return ms.ToArray();
78+
}
79+
80+
static async Task SaveStream(Stream stream, string directory, string filename)
81+
{
82+
var path = Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(filename)}-{DateTime.Now:yyyyMMddHHmmss}{Path.GetExtension(filename)}");
83+
await using var file = File.Create(path);
84+
await stream.CopyToAsync(file);
85+
Console.WriteLine($" Saved: {path}");
86+
}
87+
88+
static GotenbergSharpClient CreateClient(GotenbergSharpClientOptions options)
89+
{
90+
var handler = new HttpClientHandler();
91+
HttpMessageHandler effectiveHandler = handler;
92+
93+
if (!string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword))
94+
effectiveHandler = new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler };
95+
96+
var httpClient = new HttpClient(effectiveHandler)
97+
{
98+
BaseAddress = options.ServiceUrl,
99+
Timeout = options.TimeOut
100+
};
101+
102+
return new GotenbergSharpClient(httpClient);
103+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright 2019-2026 Chris Mohan, Jaben Cargman
2+
// and GotenbergSharpApiClient Contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
using Gotenberg.Sharp.API.Client.Domain.ValueObjects;
17+
18+
using Newtonsoft.Json.Linq;
19+
20+
namespace Gotenberg.Sharp.API.Client.Domain.Builders;
21+
22+
/// <summary>
23+
/// Builds standalone PDF engine requests. Use the static factory methods to create
24+
/// builders for specific operations (flatten, rotate, split, encrypt, metadata).
25+
/// </summary>
26+
public sealed class PdfEngineBuilder<TRequest>
27+
: BaseBuilder<TRequest, PdfEngineBuilder<TRequest>>
28+
where TRequest : PdfEngineRequest
29+
{
30+
internal PdfEngineBuilder(TRequest request) : base(request)
31+
{
32+
}
33+
34+
/// <summary>
35+
/// Adds PDF files to process.
36+
/// </summary>
37+
public PdfEngineBuilder<TRequest> WithPdfs(Action<AssetBuilder> action)
38+
{
39+
if (action == null) throw new ArgumentNullException(nameof(action));
40+
41+
action(new AssetBuilder(this.Request.Assets ??= new AssetDictionary()));
42+
43+
return this;
44+
}
45+
46+
/// <summary>
47+
/// Adds PDF files asynchronously.
48+
/// </summary>
49+
public PdfEngineBuilder<TRequest> WithPdfsAsync(Func<AssetBuilder, Task> asyncAction)
50+
{
51+
if (asyncAction == null) throw new ArgumentNullException(nameof(asyncAction));
52+
53+
this.BuildTasks.Add(asyncAction(new AssetBuilder(this.Request.Assets ??= new AssetDictionary())));
54+
55+
return this;
56+
}
57+
}
58+
59+
/// <summary>
60+
/// Static factory for creating PDF engine builders for specific operations.
61+
/// </summary>
62+
public static class PdfEngineBuilders
63+
{
64+
/// <summary>
65+
/// Creates a builder for flattening PDF form fields into static content.
66+
/// </summary>
67+
public static PdfEngineBuilder<FlattenPdfRequest> Flatten()
68+
{
69+
return new PdfEngineBuilder<FlattenPdfRequest>(new FlattenPdfRequest());
70+
}
71+
72+
/// <summary>
73+
/// Creates a builder for rotating PDF pages.
74+
/// </summary>
75+
public static PdfEngineBuilder<RotatePdfRequest> Rotate(RotationAngle angle, PageRanges? pages = null)
76+
{
77+
var request = new RotatePdfRequest
78+
{
79+
RotateAngle = angle ?? throw new ArgumentNullException(nameof(angle)),
80+
RotatePages = pages
81+
};
82+
return new PdfEngineBuilder<RotatePdfRequest>(request);
83+
}
84+
85+
/// <summary>
86+
/// Creates a builder for rotating PDF pages.
87+
/// </summary>
88+
public static PdfEngineBuilder<RotatePdfRequest> Rotate(int angleDegrees, string? pages = null)
89+
{
90+
return Rotate(
91+
RotationAngle.Create(angleDegrees),
92+
pages != null ? PageRanges.Create(pages) : null);
93+
}
94+
95+
/// <summary>
96+
/// Creates a builder for splitting PDFs.
97+
/// </summary>
98+
public static PdfEngineBuilder<SplitPdfRequest> Split(SplitMode mode, string span, bool unify = false)
99+
{
100+
var request = new SplitPdfRequest
101+
{
102+
Mode = mode,
103+
Span = span ?? throw new ArgumentNullException(nameof(span)),
104+
Unify = unify
105+
};
106+
return new PdfEngineBuilder<SplitPdfRequest>(request);
107+
}
108+
109+
/// <summary>
110+
/// Creates a builder for encrypting PDFs with passwords.
111+
/// </summary>
112+
public static PdfEngineBuilder<EncryptPdfRequest> Encrypt(string userPassword, string? ownerPassword = null)
113+
{
114+
if (string.IsNullOrWhiteSpace(userPassword))
115+
throw new ArgumentException("User password is required.", nameof(userPassword));
116+
117+
var request = new EncryptPdfRequest
118+
{
119+
UserPassword = userPassword,
120+
OwnerPassword = ownerPassword
121+
};
122+
return new PdfEngineBuilder<EncryptPdfRequest>(request);
123+
}
124+
125+
/// <summary>
126+
/// Creates a builder for reading metadata from PDFs. Returns JSON.
127+
/// </summary>
128+
public static PdfEngineBuilder<ReadMetadataRequest> ReadMetadata()
129+
{
130+
return new PdfEngineBuilder<ReadMetadataRequest>(new ReadMetadataRequest());
131+
}
132+
133+
/// <summary>
134+
/// Creates a builder for writing metadata to PDFs.
135+
/// </summary>
136+
public static PdfEngineBuilder<WriteMetadataRequest> WriteMetadata(JObject metadata)
137+
{
138+
var request = new WriteMetadataRequest
139+
{
140+
Metadata = metadata ?? throw new ArgumentNullException(nameof(metadata))
141+
};
142+
return new PdfEngineBuilder<WriteMetadataRequest>(request);
143+
}
144+
145+
/// <summary>
146+
/// Creates a builder for writing metadata to PDFs.
147+
/// </summary>
148+
public static PdfEngineBuilder<WriteMetadataRequest> WriteMetadata(IDictionary<string, object> metadata)
149+
{
150+
return WriteMetadata(JObject.FromObject(metadata));
151+
}
152+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2019-2026 Chris Mohan, Jaben Cargman
2+
// and GotenbergSharpApiClient Contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0
5+
6+
namespace Gotenberg.Sharp.API.Client.Domain.Requests;
7+
8+
public sealed class EncryptPdfRequest : PdfEngineRequest
9+
{
10+
protected override string ApiPath => Constants.Gotenberg.PdfEngines.ApiPaths.Encrypt;
11+
12+
public string? UserPassword { get; set; }
13+
14+
public string? OwnerPassword { get; set; }
15+
16+
protected override void Validate()
17+
{
18+
if (string.IsNullOrWhiteSpace(this.UserPassword))
19+
throw new InvalidOperationException("User password is required for encryption.");
20+
21+
base.Validate();
22+
}
23+
24+
protected override IEnumerable<HttpContent> ToHttpContent()
25+
{
26+
yield return CreateFormDataItem(this.UserPassword!, "userPassword");
27+
28+
if (!string.IsNullOrWhiteSpace(this.OwnerPassword))
29+
yield return CreateFormDataItem(this.OwnerPassword, "ownerPassword");
30+
31+
foreach (var content in base.ToHttpContent())
32+
yield return content;
33+
}
34+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright 2019-2026 Chris Mohan, Jaben Cargman
2+
// and GotenbergSharpApiClient Contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0
5+
6+
namespace Gotenberg.Sharp.API.Client.Domain.Requests;
7+
8+
public sealed class FlattenPdfRequest : PdfEngineRequest
9+
{
10+
protected override string ApiPath => Constants.Gotenberg.PdfEngines.ApiPaths.Flatten;
11+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2019-2026 Chris Mohan, Jaben Cargman
2+
// and GotenbergSharpApiClient Contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
namespace Gotenberg.Sharp.API.Client.Domain.Requests;
17+
18+
/// <summary>
19+
/// Base class for standalone PDF engine operations (flatten, rotate, split, encrypt,
20+
/// watermark, stamp, metadata). Each takes PDF files as input and returns processed output.
21+
/// </summary>
22+
public abstract class PdfEngineRequest : BuildRequestBase
23+
{
24+
protected override void Validate()
25+
{
26+
if (this.Assets == null || !this.Assets.Any())
27+
throw new InvalidOperationException("At least one PDF file is required.");
28+
29+
base.Validate();
30+
}
31+
32+
protected override IEnumerable<HttpContent> ToHttpContent()
33+
{
34+
foreach (var item in this.Assets.IfNullEmpty().Where(item => item.IsValid()))
35+
{
36+
var contentItem = item.Value.ToHttpContentItem();
37+
38+
contentItem.Headers.ContentDisposition =
39+
new ContentDispositionHeaderValue(Constants.HttpContent.Disposition.Types.FormData)
40+
{
41+
Name = Constants.Gotenberg.SharedFormFieldNames.Files, FileName = item.Key
42+
};
43+
44+
contentItem.Headers.ContentType = new MediaTypeHeaderValue(Constants.HttpContent.MediaTypes.ApplicationPdf);
45+
46+
yield return contentItem;
47+
}
48+
49+
foreach (var item in this.Config.IfNullEmptyContent())
50+
yield return item;
51+
52+
foreach (var content in base.ToHttpContent())
53+
yield return content;
54+
}
55+
}

0 commit comments

Comments
 (0)