Skip to content

Commit d45780d

Browse files
authored
Merge pull request #70 from ChangemakerStudios/feature/chromium-missing-fields
Add missing Chromium form fields with DDD value objects
2 parents 3f225a9 + 9b26a91 commit d45780d

17 files changed

Lines changed: 1309 additions & 20 deletions

File tree

README.md

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -529,37 +529,27 @@ public async Task<Stream> FastConversion()
529529
}
530530
```
531531

532-
### Watermark & Rotation
533-
*Add text watermarks and rotate PDF pages — available on all request types:*
532+
### Wait For Selector & Emulated Media Features
533+
*Wait for a DOM element and emulate CSS media features like dark mode:*
534534

535535
```csharp
536-
public async Task<Stream> CreateWatermarkedPdf()
536+
public async Task<Stream> CreateWithChromiumFeatures()
537537
{
538538
var builder = new HtmlRequestBuilder()
539-
.AddDocument(doc => doc.SetBody("<html><body><h1>Report</h1></body></html>"))
540-
.SetWatermarkOptions(w => w.SetTextWatermark("DRAFT", "1-3"))
541-
.SetRotationOptions(r => r.SetAngle(90).SetPages("2"))
539+
.AddDocument(doc => doc.SetBody("<html><body><div id='app'>Ready</div></body></html>"))
540+
.SetConversionBehaviors(b => b
541+
.SetWaitForSelector("#app")
542+
.AddEmulatedMediaFeature("prefers-color-scheme", "dark")
543+
.SetFailOnHttpStatusCodes(499, 599)
544+
.FailOnResourceLoadingFailed()
545+
.AddIgnoreResourceHttpStatusDomains("cdn.example.com"))
542546
.WithPageProperties(pp => pp.UseChromeDefaults());
543547

544548
var request = builder.Build();
545549
return await _sharpClient.HtmlToPdfAsync(request);
546550
}
547551
```
548552

549-
### Split PDFs
550-
*Split generated PDFs into chunks or extract specific pages:*
551-
552-
```csharp
553-
public async Task<Stream> SplitPdf()
554-
{
555-
var builder = new HtmlRequestBuilder()
556-
.AddDocument(doc => doc.SetBody("<html><body>Multi-page content</body></html>"))
557-
.SetSplitOptions(s => s.SplitByPages("1-3,5", unify: true));
558-
559-
var request = builder.Build();
560-
return await _sharpClient.HtmlToPdfAsync(request);
561-
}
562-
```
563553

564554
### Custom Page Properties
565555
*Fine-tune page dimensions and properties:*
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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using Gotenberg.Sharp.API.Client;
2+
using Gotenberg.Sharp.API.Client.Domain.Builders;
3+
using Gotenberg.Sharp.API.Client.Domain.Settings;
4+
using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline;
5+
6+
using Microsoft.Extensions.Configuration;
7+
8+
var config = new ConfigurationBuilder()
9+
.SetBasePath(AppContext.BaseDirectory)
10+
.AddJsonFile("appsettings.json")
11+
.Build();
12+
13+
var options = new GotenbergSharpClientOptions();
14+
config.GetSection(nameof(GotenbergSharpClient)).Bind(options);
15+
16+
var destinationDirectory = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output");
17+
Directory.CreateDirectory(destinationDirectory);
18+
19+
var path = await CreateWithChromiumFeatures(destinationDirectory, options);
20+
Console.WriteLine($"PDF created: {path}");
21+
22+
static async Task<string> CreateWithChromiumFeatures(string destinationDirectory, GotenbergSharpClientOptions options)
23+
{
24+
var handler = new HttpClientHandler();
25+
HttpMessageHandler effectiveHandler = handler;
26+
if (!string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword))
27+
effectiveHandler = new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler };
28+
29+
using var httpClient = new HttpClient(effectiveHandler, disposeHandler: true)
30+
{
31+
BaseAddress = options.ServiceUrl,
32+
Timeout = options.TimeOut
33+
};
34+
35+
var sharpClient = new GotenbergSharpClient(httpClient);
36+
37+
// Demonstrates waitForSelector, emulated media features, and error handling options
38+
var builder = new HtmlRequestBuilder()
39+
.AddDocument(doc => doc.SetBody(@"
40+
<html>
41+
<head>
42+
<style>
43+
@media (prefers-color-scheme: dark) {
44+
body { background: #1a1a2e; color: #eee; }
45+
}
46+
</style>
47+
</head>
48+
<body>
49+
<div id='content'>
50+
<h1>Chromium Feature Demo</h1>
51+
<p>This PDF was generated with dark mode emulation, waitForSelector,
52+
and strict error handling.</p>
53+
</div>
54+
</body>
55+
</html>"))
56+
.SetConversionBehaviors(b => b
57+
// Wait for the #content element before converting
58+
.SetWaitForSelector("#content")
59+
// Emulate dark mode
60+
.AddEmulatedMediaFeature("prefers-color-scheme", "dark")
61+
// Fail if the main page returns 4xx or 5xx
62+
.SetFailOnHttpStatusCodes(499, 599)
63+
// Fail if any resource fails to load
64+
.FailOnResourceLoadingFailed()
65+
// Fail on any console exceptions
66+
.FailOnConsoleExceptions()
67+
// Ignore CDN domains for status code checks
68+
.AddIgnoreResourceHttpStatusDomains("cdn.example.com")
69+
)
70+
.WithPageProperties(pp => pp.UseChromeDefaults());
71+
72+
var request = builder.Build();
73+
var response = await sharpClient.HtmlToPdfAsync(request);
74+
75+
var resultPath = Path.Combine(destinationDirectory, $"ChromiumFeatures-{DateTime.Now:yyyyMMddHHmmss}.pdf");
76+
77+
await using var destinationStream = File.Create(resultPath);
78+
await response.CopyToAsync(destinationStream, CancellationToken.None);
79+
80+
return resultPath;
81+
}

src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/HtmlConversionBehaviorBuilder.cs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16+
using Gotenberg.Sharp.API.Client.Domain.ValueObjects;
17+
1618
using Newtonsoft.Json.Linq;
1719

1820
namespace Gotenberg.Sharp.API.Client.Domain.Builders.Faceted;
@@ -174,6 +176,171 @@ public HtmlConversionBehaviorBuilder SkipNetworkIdleEvent()
174176
return this;
175177
}
176178

179+
/// <summary>
180+
/// Sets a CSS selector to wait for before conversion.
181+
/// Chromium will delay conversion until the specified element appears in the DOM.
182+
/// </summary>
183+
/// <param name="selector">A validated CSS selector.</param>
184+
/// <returns>The builder instance for method chaining.</returns>
185+
public HtmlConversionBehaviorBuilder SetWaitForSelector(CssSelector selector)
186+
{
187+
_htmlConversionBehaviors.WaitForSelector = selector ?? throw new ArgumentNullException(nameof(selector));
188+
189+
return this;
190+
}
191+
192+
/// <summary>
193+
/// Sets a CSS selector to wait for before conversion.
194+
/// Chromium will delay conversion until the specified element appears in the DOM.
195+
/// </summary>
196+
/// <param name="selector">A CSS selector string (e.g., "#content", ".loaded").</param>
197+
/// <returns>The builder instance for method chaining.</returns>
198+
public HtmlConversionBehaviorBuilder SetWaitForSelector(string selector)
199+
{
200+
return SetWaitForSelector(CssSelector.Create(selector));
201+
}
202+
203+
/// <summary>
204+
/// Adds a CSS media feature override for Chromium rendering.
205+
/// </summary>
206+
/// <param name="feature">A validated emulated media feature.</param>
207+
/// <returns>The builder instance for method chaining.</returns>
208+
public HtmlConversionBehaviorBuilder AddEmulatedMediaFeature(EmulatedMediaFeature feature)
209+
{
210+
if (feature == null) throw new ArgumentNullException(nameof(feature));
211+
212+
_htmlConversionBehaviors.EmulatedMediaFeatures ??= new List<EmulatedMediaFeature>();
213+
_htmlConversionBehaviors.EmulatedMediaFeatures.Add(feature);
214+
215+
return this;
216+
}
217+
218+
/// <summary>
219+
/// Adds a CSS media feature override by name and value.
220+
/// </summary>
221+
/// <param name="name">CSS media feature name (e.g., "prefers-color-scheme").</param>
222+
/// <param name="value">CSS media feature value (e.g., "dark").</param>
223+
/// <returns>The builder instance for method chaining.</returns>
224+
public HtmlConversionBehaviorBuilder AddEmulatedMediaFeature(string name, string value)
225+
{
226+
return AddEmulatedMediaFeature(EmulatedMediaFeature.Create(name, value));
227+
}
228+
229+
/// <summary>
230+
/// Sets HTTP status codes that trigger a 409 Conflict when the main page returns them.
231+
/// </summary>
232+
/// <param name="statusCodes">Validated HTTP status codes.</param>
233+
/// <returns>The builder instance for method chaining.</returns>
234+
public HtmlConversionBehaviorBuilder SetFailOnHttpStatusCodes(IEnumerable<GotenbergStatusCode> statusCodes)
235+
{
236+
if (statusCodes == null) throw new ArgumentNullException(nameof(statusCodes));
237+
238+
var codes = statusCodes.ToList();
239+
240+
if (codes.Any(c => c == null))
241+
throw new ArgumentException("Status codes collection must not contain null elements.", nameof(statusCodes));
242+
243+
_htmlConversionBehaviors.FailOnHttpStatusCodes = codes;
244+
245+
return this;
246+
}
247+
248+
/// <summary>
249+
/// Sets HTTP status codes that trigger a 409 Conflict when the main page returns them.
250+
/// </summary>
251+
/// <param name="statusCodes">Raw HTTP status code integers (must be 100-599).</param>
252+
/// <returns>The builder instance for method chaining.</returns>
253+
public HtmlConversionBehaviorBuilder SetFailOnHttpStatusCodes(params int[] statusCodes)
254+
{
255+
if (statusCodes == null) throw new ArgumentNullException(nameof(statusCodes));
256+
257+
return SetFailOnHttpStatusCodes(statusCodes.Select(GotenbergStatusCode.Create));
258+
}
259+
260+
/// <summary>
261+
/// Sets HTTP status codes that trigger a failure when page resources return them.
262+
/// </summary>
263+
/// <param name="statusCodes">Validated HTTP status codes.</param>
264+
/// <returns>The builder instance for method chaining.</returns>
265+
public HtmlConversionBehaviorBuilder SetFailOnResourceHttpStatusCodes(IEnumerable<GotenbergStatusCode> statusCodes)
266+
{
267+
if (statusCodes == null) throw new ArgumentNullException(nameof(statusCodes));
268+
269+
var codes = statusCodes.ToList();
270+
271+
if (codes.Any(c => c == null))
272+
throw new ArgumentException("Status codes collection must not contain null elements.", nameof(statusCodes));
273+
274+
_htmlConversionBehaviors.FailOnResourceHttpStatusCodes = codes;
275+
276+
return this;
277+
}
278+
279+
/// <summary>
280+
/// Sets HTTP status codes that trigger a failure when page resources return them.
281+
/// </summary>
282+
/// <param name="statusCodes">Raw HTTP status code integers (must be 100-599).</param>
283+
/// <returns>The builder instance for method chaining.</returns>
284+
public HtmlConversionBehaviorBuilder SetFailOnResourceHttpStatusCodes(params int[] statusCodes)
285+
{
286+
if (statusCodes == null) throw new ArgumentNullException(nameof(statusCodes));
287+
288+
return SetFailOnResourceHttpStatusCodes(statusCodes.Select(GotenbergStatusCode.Create));
289+
}
290+
291+
/// <summary>
292+
/// Adds a domain to exclude from HTTP status code checks on resources.
293+
/// </summary>
294+
/// <param name="domain">A validated domain name.</param>
295+
/// <returns>The builder instance for method chaining.</returns>
296+
public HtmlConversionBehaviorBuilder AddIgnoreResourceHttpStatusDomain(DomainName domain)
297+
{
298+
if (domain == null) throw new ArgumentNullException(nameof(domain));
299+
300+
_htmlConversionBehaviors.IgnoreResourceHttpStatusDomains ??= new List<DomainName>();
301+
_htmlConversionBehaviors.IgnoreResourceHttpStatusDomains.Add(domain);
302+
303+
return this;
304+
}
305+
306+
/// <summary>
307+
/// Adds a domain to exclude from HTTP status code checks on resources.
308+
/// </summary>
309+
/// <param name="domain">A domain string (e.g., "cdn.example.com").</param>
310+
/// <returns>The builder instance for method chaining.</returns>
311+
public HtmlConversionBehaviorBuilder AddIgnoreResourceHttpStatusDomain(string domain)
312+
{
313+
return AddIgnoreResourceHttpStatusDomain(DomainName.Create(domain));
314+
}
315+
316+
/// <summary>
317+
/// Adds multiple domains to exclude from HTTP status code checks on resources.
318+
/// </summary>
319+
/// <param name="domains">Domain strings to exclude.</param>
320+
/// <returns>The builder instance for method chaining.</returns>
321+
public HtmlConversionBehaviorBuilder AddIgnoreResourceHttpStatusDomains(params string[] domains)
322+
{
323+
if (domains == null) throw new ArgumentNullException(nameof(domains));
324+
325+
var validated = domains.Select(DomainName.Create).ToList();
326+
327+
_htmlConversionBehaviors.IgnoreResourceHttpStatusDomains ??= new List<DomainName>();
328+
_htmlConversionBehaviors.IgnoreResourceHttpStatusDomains.AddRange(validated);
329+
330+
return this;
331+
}
332+
333+
/// <summary>
334+
/// Tells Gotenberg to return a 409 Conflict if any resource fails to load due to network errors.
335+
/// </summary>
336+
/// <returns>The builder instance for method chaining.</returns>
337+
public HtmlConversionBehaviorBuilder FailOnResourceLoadingFailed()
338+
{
339+
_htmlConversionBehaviors.FailOnResourceLoadingFailed = true;
340+
341+
return this;
342+
}
343+
177344
/// <summary>
178345
/// Sets the format of the resulting PDF document.
179346
/// </summary>

src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/FacetBase.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ public virtual IEnumerable<HttpContent> ToHttpContent()
8181
ConversionPdfFormats format => format.ToFormDataValue(),
8282
PdfPassword password => password.Value,
8383
List<Cookie> cookies => JsonConvert.SerializeObject(cookies),
84+
List<EmulatedMediaFeature> features => JsonConvert.SerializeObject(
85+
features.ToDictionary(f => f.Name, f => f.Value)),
86+
List<GotenbergStatusCode> codes => JsonConvert.SerializeObject(codes.Select(c => c.Value)),
87+
List<DomainName> domains => JsonConvert.SerializeObject(domains.Select(d => d.Value)),
88+
CssSelector selector => selector.Value,
8489
OverlaySource overlaySource => overlaySource.ToFormValue(),
8590
SplitMode splitMode => splitMode.ToFormValue(),
8691
float f => f.ToString(cultureInfo),

src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/HtmlConversionBehaviors.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16+
using Gotenberg.Sharp.API.Client.Domain.ValueObjects;
17+
1618
using Newtonsoft.Json.Linq;
1719

1820
namespace Gotenberg.Sharp.API.Client.Domain.Requests.Facets;
@@ -85,4 +87,44 @@ public class HtmlConversionBehaviors : FacetBase
8587
/// </summary>
8688
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.SkipNetworkIdleEvent)]
8789
public bool? SkipNetworkIdleEvent { get; set; }
90+
91+
/// <summary>
92+
/// CSS selector to wait for before conversion. Delays until the element appears in the DOM.
93+
/// </summary>
94+
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.WaitForSelector)]
95+
public CssSelector? WaitForSelector { get; set; }
96+
97+
/// <summary>
98+
/// Overrides CSS media features (e.g., prefers-color-scheme, prefers-reduced-motion).
99+
/// Sent as a JSON array of {name, value} objects.
100+
/// </summary>
101+
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.EmulatedMediaFeatures)]
102+
public List<EmulatedMediaFeature>? EmulatedMediaFeatures { get; set; }
103+
104+
/// <summary>
105+
/// HTTP status codes that trigger a 409 Conflict response from Gotenberg
106+
/// when the main page returns a matching code. Default: [499, 599].
107+
/// </summary>
108+
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.FailOnHttpStatusCodes)]
109+
public List<GotenbergStatusCode>? FailOnHttpStatusCodes { get; set; }
110+
111+
/// <summary>
112+
/// HTTP status codes that trigger a failure when page resources (CSS, images, fonts)
113+
/// return a matching code.
114+
/// </summary>
115+
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.FailOnResourceHttpStatusCodes)]
116+
public List<GotenbergStatusCode>? FailOnResourceHttpStatusCodes { get; set; }
117+
118+
/// <summary>
119+
/// Domains to exclude from HTTP status code checks on resources.
120+
/// Useful for ignoring third-party CDNs or analytics domains.
121+
/// </summary>
122+
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.IgnoreResourceHttpStatusDomains)]
123+
public List<DomainName>? IgnoreResourceHttpStatusDomains { get; set; }
124+
125+
/// <summary>
126+
/// Tells Gotenberg to return a 409 Conflict if any resource fails to load due to network errors.
127+
/// </summary>
128+
[MultiFormHeader(Constants.Gotenberg.Chromium.Shared.HtmlConvert.FailOnResourceLoadingFailed)]
129+
public bool? FailOnResourceLoadingFailed { get; set; }
88130
}

0 commit comments

Comments
 (0)