Skip to content

Commit badcdbe

Browse files
authored
Merge pull request #75 from ChangemakerStudios/feature/standalone-pdf-operations
Add standalone PDF engine operations
2 parents 09b6c02 + b5bbb3a commit badcdbe

17 files changed

Lines changed: 1017 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+
using 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+
using 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+
using 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+
using 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+
using 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 = !string.IsNullOrWhiteSpace(span) ? span : throw new ArgumentException("Span must not be null or whitespace.", 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 ArgumentException("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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
/// <summary>
9+
/// Flattens PDF form fields into static content.
10+
/// </summary>
11+
public sealed class FlattenPdfRequest : PdfEngineRequest
12+
{
13+
protected override string ApiPath => Constants.Gotenberg.PdfEngines.ApiPaths.Flatten;
14+
}

0 commit comments

Comments
 (0)