diff --git a/.gitignore b/.gitignore index 1ce198e..a1a4498 100644 --- a/.gitignore +++ b/.gitignore @@ -289,3 +289,4 @@ __pycache__/ *.xsd.cs settings.local.json +examples/**/output diff --git a/GotenbergSharpApiClient.sln b/GotenbergSharpApiClient.sln index 2e11290..d19cbde 100644 --- a/GotenbergSharpApiClient.sln +++ b/GotenbergSharpApiClient.sln @@ -5,12 +5,32 @@ VisualStudioVersion = 17.0.32112.339 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gotenberg.Sharp.Api.Client", "src\Gotenberg.Sharp.Api.Client\Gotenberg.Sharp.Api.Client.csproj", "{75F783A4-9392-412F-9DFE-00EE89527C10}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FE638D0D-6A76-4BF4-AF06-D8AAB9726723}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GotenbergSharpClient.Tests", "test\GotenbergSharpClient.Tests\GotenbergSharpClient.Tests.csproj", "{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig + examples\appsettings.json = examples\appsettings.json + examples\Directory.Build.props = examples\Directory.Build.props + examples\README.md = examples\README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GotenbergSharpClient.Tests", "test\GotenbergSharpClient.Tests\GotenbergSharpClient.Tests.csproj", "{EEAF9CA2-7962-176A-E851-BF81D8DE31F0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DIExample", "examples\DIExample\DIExample.csproj", "{C022147F-DA63-5B3C-9349-FEB9675362DF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlConvert", "examples\HtmlConvert\HtmlConvert.csproj", "{6113FC36-F04E-B6CA-5C64-EA6156640C94}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlWithMarkdown", "examples\HtmlWithMarkdown\HtmlWithMarkdown.csproj", "{893AD0F6-7E7E-8550-56D7-BBCB77933BD3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OfficeMerge", "examples\OfficeMerge\OfficeMerge.csproj", "{2A1FC5B6-EB87-C86A-71BB-42784913627C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PdfConvert", "examples\PdfConvert\PdfConvert.csproj", "{BB512649-8BE2-38B1-49D1-E8CA8701BA1A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PdfMerge", "examples\PdfMerge\PdfMerge.csproj", "{DDD4236E-F481-4DE9-306C-66E77A8D2CB9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UrlConvert", "examples\UrlConvert\UrlConvert.csproj", "{9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UrlsToMergedPdf", "examples\UrlsToMergedPdf\UrlsToMergedPdf.csproj", "{A4D0B498-A934-2F11-2AA7-18D1D1C9E881}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Webhook", "examples\Webhook\Webhook.csproj", "{01C9F662-6378-3FC4-B629-9FD37A38033D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -46,10 +66,129 @@ Global {EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|x64.Build.0 = Release|Any CPU {EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|x86.ActiveCfg = Release|Any CPU {EEAF9CA2-7962-176A-E851-BF81D8DE31F0}.Release|x86.Build.0 = Release|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|x64.ActiveCfg = Debug|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|x64.Build.0 = Debug|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|x86.ActiveCfg = Debug|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Debug|x86.Build.0 = Debug|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|Any CPU.Build.0 = Release|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|x64.ActiveCfg = Release|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|x64.Build.0 = Release|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|x86.ActiveCfg = Release|Any CPU + {C022147F-DA63-5B3C-9349-FEB9675362DF}.Release|x86.Build.0 = Release|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|x64.ActiveCfg = Debug|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|x64.Build.0 = Debug|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|x86.ActiveCfg = Debug|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Debug|x86.Build.0 = Debug|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|Any CPU.Build.0 = Release|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|x64.ActiveCfg = Release|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|x64.Build.0 = Release|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|x86.ActiveCfg = Release|Any CPU + {6113FC36-F04E-B6CA-5C64-EA6156640C94}.Release|x86.Build.0 = Release|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|x64.ActiveCfg = Debug|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|x64.Build.0 = Debug|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|x86.ActiveCfg = Debug|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Debug|x86.Build.0 = Debug|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|Any CPU.Build.0 = Release|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|x64.ActiveCfg = Release|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|x64.Build.0 = Release|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|x86.ActiveCfg = Release|Any CPU + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3}.Release|x86.Build.0 = Release|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|x64.ActiveCfg = Debug|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|x64.Build.0 = Debug|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|x86.ActiveCfg = Debug|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Debug|x86.Build.0 = Debug|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|Any CPU.Build.0 = Release|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|x64.ActiveCfg = Release|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|x64.Build.0 = Release|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|x86.ActiveCfg = Release|Any CPU + {2A1FC5B6-EB87-C86A-71BB-42784913627C}.Release|x86.Build.0 = Release|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|x64.ActiveCfg = Debug|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|x64.Build.0 = Debug|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|x86.ActiveCfg = Debug|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Debug|x86.Build.0 = Debug|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|Any CPU.Build.0 = Release|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|x64.ActiveCfg = Release|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|x64.Build.0 = Release|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|x86.ActiveCfg = Release|Any CPU + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A}.Release|x86.Build.0 = Release|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|x64.ActiveCfg = Debug|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|x64.Build.0 = Debug|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|x86.ActiveCfg = Debug|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Debug|x86.Build.0 = Debug|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|Any CPU.Build.0 = Release|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|x64.ActiveCfg = Release|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|x64.Build.0 = Release|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|x86.ActiveCfg = Release|Any CPU + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9}.Release|x86.Build.0 = Release|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|x64.ActiveCfg = Debug|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|x64.Build.0 = Debug|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|x86.ActiveCfg = Debug|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Debug|x86.Build.0 = Debug|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|Any CPU.Build.0 = Release|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|x64.ActiveCfg = Release|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|x64.Build.0 = Release|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|x86.ActiveCfg = Release|Any CPU + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17}.Release|x86.Build.0 = Release|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|x64.ActiveCfg = Debug|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|x64.Build.0 = Debug|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|x86.ActiveCfg = Debug|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Debug|x86.Build.0 = Debug|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|Any CPU.Build.0 = Release|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|x64.ActiveCfg = Release|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|x64.Build.0 = Release|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|x86.ActiveCfg = Release|Any CPU + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881}.Release|x86.Build.0 = Release|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|x64.ActiveCfg = Debug|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|x64.Build.0 = Debug|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|x86.ActiveCfg = Debug|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Debug|x86.Build.0 = Debug|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Release|Any CPU.Build.0 = Release|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Release|x64.ActiveCfg = Release|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Release|x64.Build.0 = Release|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Release|x86.ActiveCfg = Release|Any CPU + {01C9F662-6378-3FC4-B629-9FD37A38033D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C022147F-DA63-5B3C-9349-FEB9675362DF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {6113FC36-F04E-B6CA-5C64-EA6156640C94} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {893AD0F6-7E7E-8550-56D7-BBCB77933BD3} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {2A1FC5B6-EB87-C86A-71BB-42784913627C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {BB512649-8BE2-38B1-49D1-E8CA8701BA1A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {DDD4236E-F481-4DE9-306C-66E77A8D2CB9} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {9C18F2D1-E9E9-613A-DE10-4263A2EA8A17} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {A4D0B498-A934-2F11-2AA7-18D1D1C9E881} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {01C9F662-6378-3FC4-B629-9FD37A38033D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E732CE09-693A-4EB7-BC1C-34E0D4E2E19F} EndGlobalSection diff --git a/README.md b/README.md index f6f03b2..9ea6045 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ public void ConfigureServices(IServiceCollection services) ``` # Using GotenbergSharpClient -*See the [linqPad folder](linqpad/)* for complete examples. +*See the [examples folder](examples/)* for complete working examples as console applications. ## Required Using Statements ```csharp diff --git a/examples/DIExample/DIExample.csproj b/examples/DIExample/DIExample.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/DIExample/DIExample.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/DIExample/Program.cs b/examples/DIExample/Program.cs new file mode 100644 index 0000000..06f9dcc --- /dev/null +++ b/examples/DIExample/Program.cs @@ -0,0 +1,70 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Requests; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Extensions; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +// Builds a simple DI container with logging enabled. +// Client retrieved through the service provider is configured with options defined in appsettings.json +// Watch the polly-retry policy in action: +// Turn off gotenberg, run this script and let it fail/retry two or three times. +// Turn gotenberg back on & the request will successfully complete. +// Example builds a 1 page PDF from the specified TargetUrl + +const string TargetUrl = "https://www.cnn.com"; +var saveToPath = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(saveToPath); + +var services = BuildServiceCollection(); +var sp = services.BuildServiceProvider(); + +var sharpClient = sp.GetRequiredService(); +var request = await CreateUrlRequest(); +var response = await sharpClient.UrlToPdfAsync(request); + +var resultPath = Path.Combine(saveToPath, $"GotenbergFromUrl-{DateTime.Now:yyyyMMddHHmmss}.pdf"); + +await using (var destinationStream = File.Create(resultPath)) +{ + await response.CopyToAsync(destinationStream, CancellationToken.None); +} + +Console.WriteLine($"PDF created: {resultPath}"); + +IServiceCollection BuildServiceCollection() +{ + var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + + return new ServiceCollection() + .AddOptions() + .Bind(config.GetSection(nameof(GotenbergSharpClient))).Services + .AddGotenbergSharpClient() + .Services.AddLogging(s => s.AddSimpleConsole(ops => + { + ops.IncludeScopes = true; + ops.SingleLine = false; + ops.TimestampFormat = "hh:mm:ss "; + })); +} + +Task CreateUrlRequest() +{ + var builder = new UrlRequestBuilder() + .SetUrl(TargetUrl) + .ConfigureRequest(b => b.SetPageRanges("1-2")) + .WithPageProperties(b => + { + b.SetPaperSize(PaperSizes.A4) + .SetMargins(Margins.None); + }); + + return builder.BuildAsync(); +} \ No newline at end of file diff --git a/examples/Directory.Build.props b/examples/Directory.Build.props new file mode 100644 index 0000000..8e0da1b --- /dev/null +++ b/examples/Directory.Build.props @@ -0,0 +1,31 @@ + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + diff --git a/examples/HtmlConvert/HtmlConvert.csproj b/examples/HtmlConvert/HtmlConvert.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/HtmlConvert/HtmlConvert.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/HtmlConvert/Program.cs b/examples/HtmlConvert/Program.cs new file mode 100644 index 0000000..91bd776 --- /dev/null +++ b/examples/HtmlConvert/Program.cs @@ -0,0 +1,71 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; + +using Microsoft.Extensions.Configuration; + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); + +var destinationDirectory = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationDirectory); + +var resourcePath = Path.Combine(AppContext.BaseDirectory, "resources", "Html", "ConvertExample"); +var path = await CreateFromHtml(destinationDirectory, resourcePath, options); + +Console.WriteLine($"PDF created: {path}"); + +static async Task CreateFromHtml(string destinationDirectory, string resourcePath, GotenbergSharpClientOptions options) +{ + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut + }; + + var sharpClient = new GotenbergSharpClient(httpClient); + + var builder = new HtmlRequestBuilder() + .AddAsyncDocument(async doc => + doc.SetBody(await GetHtmlFile(resourcePath, "body.html")) + .SetFooter(await GetHtmlFile(resourcePath, "footer.html")) + ).WithPageProperties(dims => dims.UseChromeDefaults()) + .WithAsyncAssets(async assets => + assets.AddItem("ear-on-beach.jpg", await GetImageBytes(resourcePath)) + ) + .SetConversionBehaviors(b => + b.AddAdditionalHeaders("hello", "from-earth") + ) + .ConfigureRequest(b => b.SetPageRanges("1")); + + var request = await builder.BuildAsync(); + + var resultPath = Path.Combine(destinationDirectory, $"GotenbergFromHtml-{DateTime.Now:yyyyMMddHHmmss}.pdf"); + var response = await sharpClient.HtmlToPdfAsync(request); + + await using var destinationStream = File.Create(resultPath); + await response.CopyToAsync(destinationStream, CancellationToken.None); + + return resultPath; +} + +static Task GetImageBytes(string resourcePath) +{ + return File.ReadAllBytesAsync(Path.Combine(resourcePath, "ear-on-beach.jpg")); +} + +static Task GetHtmlFile(string resourcePath, string fileName) +{ + return File.ReadAllBytesAsync(Path.Combine(resourcePath, fileName)); +} \ No newline at end of file diff --git a/examples/HtmlWithMarkdown/HtmlWithMarkdown.csproj b/examples/HtmlWithMarkdown/HtmlWithMarkdown.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/HtmlWithMarkdown/HtmlWithMarkdown.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/HtmlWithMarkdown/Program.cs b/examples/HtmlWithMarkdown/Program.cs new file mode 100644 index 0000000..c0b7476 --- /dev/null +++ b/examples/HtmlWithMarkdown/Program.cs @@ -0,0 +1,82 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; +using Microsoft.Extensions.Configuration; + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); + +var destinationDirectory = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationDirectory); + +var resourcePath = Path.Combine(AppContext.BaseDirectory, "resources", "Markdown"); +var path = await CreateFromMarkdown(destinationDirectory, resourcePath, options); + +Console.WriteLine($"PDF created from Markdown: {path}"); + +static async Task CreateFromMarkdown(string destinationDirectory, string resourcePath, GotenbergSharpClientOptions options) +{ + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut + }; + + var sharpClient = new GotenbergSharpClient(httpClient); + + var builder = new HtmlRequestBuilder() + .AddAsyncDocument(async b => + b.SetHeader(await GetFile(resourcePath, "header.html")) + .SetBody(await GetFile(resourcePath, "index.html")) + .SetContainsMarkdown() + .SetFooter(await GetFile(resourcePath, "footer.html")) + ).WithPageProperties(b => + { + b.UseChromeDefaults() + .SetLandscape() + .SetScale(.90); + }).WithAsyncAssets(async b => + b.AddItems(await GetMarkdownAssets(resourcePath)) + ) + .ConfigureRequest(b => b.SetResultFileName("hello.pdf")) + .SetConversionBehaviors(b => b.SetBrowserWaitDelay(2)); + + var request = await builder.BuildAsync(); + var response = await sharpClient.HtmlToPdfAsync(request); + + var outPath = Path.Combine(destinationDirectory, $"GotenbergFromMarkDown-{DateTime.Now:yyyyMMddHHmmss}.pdf"); + + await using var destinationStream = File.Create(outPath); + await response.CopyToAsync(destinationStream, CancellationToken.None); + + return outPath; +} + +static async Task GetFile(string resourcePath, string fileName) + => await File.ReadAllTextAsync(Path.Combine(resourcePath, fileName)); + +static async Task>> GetMarkdownAssets(string resourcePath) +{ + var bodyAssetNames = new[] { "img.gif", "font.woff", "style.css" }; + var markdownFiles = new[] { "paragraph1.md", "paragraph2.md", "paragraph3.md" }; + + var bodyAssetTasks = bodyAssetNames.Select(ba => GetFile(resourcePath, ba)); + var mdTasks = markdownFiles.Select(md => GetFile(resourcePath, md)); + + var bodyAssets = await Task.WhenAll(bodyAssetTasks); + var mdParagraphs = await Task.WhenAll(mdTasks); + + return bodyAssetNames.Select((name, index) => KeyValuePair.Create(name, bodyAssets[index])) + .Concat(markdownFiles.Select((name, index) => KeyValuePair.Create(name, mdParagraphs[index]))); +} diff --git a/examples/OfficeMerge/OfficeMerge.csproj b/examples/OfficeMerge/OfficeMerge.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/OfficeMerge/OfficeMerge.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/OfficeMerge/Program.cs b/examples/OfficeMerge/Program.cs new file mode 100644 index 0000000..ab07eb0 --- /dev/null +++ b/examples/OfficeMerge/Program.cs @@ -0,0 +1,66 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; + +using Microsoft.Extensions.Configuration; + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); + +var sourceDirectory = args.Length > 0 ? args[0] : Path.Combine(AppContext.BaseDirectory, "resources", "OfficeDocs"); +var destinationDirectory = args.Length > 1 ? args[1] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationDirectory); + +var path = await DoOfficeMerge(sourceDirectory, destinationDirectory, options); +Console.WriteLine($"Merged Office documents PDF created: {path}"); + +static async Task DoOfficeMerge(string sourceDirectory, string destinationDirectory, GotenbergSharpClientOptions options) +{ + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut + }; + + var client = new GotenbergSharpClient(httpClient); + + var builder = new MergeOfficeBuilder() + .ConfigureRequest(c => c.SetTrace("ConsoleExample")) + .WithAsyncAssets(async b => b.AddItems(await GetDocsAsync(sourceDirectory))) + .SetPdfFormat(LibrePdfFormats.A2b) + .SetPageRanges("1-3"); // Only one of the files has more than 1 page. + + var response = await client.MergeOfficeDocsAsync(builder).ConfigureAwait(false); + + var mergeResultPath = Path.Combine(destinationDirectory, $"GotenbergOfficeMerge-{DateTime.Now:yyyyMMddHHmmss}.pdf"); + + await using var destinationStream = File.Create(mergeResultPath); + + await response.CopyToAsync(destinationStream, CancellationToken.None); + + return mergeResultPath; +} + +static async Task>> GetDocsAsync(string sourceDirectory) +{ + var paths = Directory.GetFiles(sourceDirectory, "*.*", SearchOption.TopDirectoryOnly); + var names = paths.Select(p => new FileInfo(p).Name); + var tasks = paths.Select(f => File.ReadAllBytesAsync(f)); + + var docs = await Task.WhenAll(tasks); + + return names.Select((name, index) => KeyValuePair.Create(name, docs[index])) + .Take(10); +} \ No newline at end of file diff --git a/examples/PdfConvert/PdfConvert.csproj b/examples/PdfConvert/PdfConvert.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/PdfConvert/PdfConvert.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/PdfConvert/Program.cs b/examples/PdfConvert/Program.cs new file mode 100644 index 0000000..bf76850 --- /dev/null +++ b/examples/PdfConvert/Program.cs @@ -0,0 +1,71 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; + +using Microsoft.Extensions.Configuration; + +// If you get 1 file, the result is a PDF; get more and the API returns a zip containing the results +// Currently, Gotenberg supports these formats: A2b & A3b + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); + +var sourcePath = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "pdfs"); +var destinationPath = args.Length > 1 ? args[1] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationPath); + +var result = await DoConversion(sourcePath, destinationPath, options); +Console.WriteLine($"Converted PDF created: {result}"); + +static async Task DoConversion(string sourcePath, string destinationPath, GotenbergSharpClientOptions options) +{ + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut + }; + + var sharpClient = new GotenbergSharpClient(httpClient); + + var items = Directory.GetFiles(sourcePath, "*.pdf", SearchOption.TopDirectoryOnly) + .Select(p => new { Info = new FileInfo(p), Path = p }) + .OrderBy(item => item.Info.CreationTime) + .Take(2); + + Console.WriteLine($"Converting {items.Count()} PDFs:"); + foreach (var item in items) + { + Console.WriteLine($" - {item.Info.Name}"); + } + + var toConvert = items.Select(item => KeyValuePair.Create(item.Info.Name, File.ReadAllBytes(item.Path))); + + var builder = new PdfConversionBuilder() + .WithPdfs(b => b.AddItems(toConvert)) + .SetPdfFormat(LibrePdfFormats.A2b); + + var request = builder.Build(); + var response = await sharpClient.ConvertPdfDocumentsAsync(request); + + // If you send one in -- the result is PDF. + var extension = items.Count() > 1 ? "zip" : "pdf"; + var outPath = Path.Combine(destinationPath, $"GotenbergConvertResult.{extension}"); + + await using var destinationStream = File.Create(outPath); + + await response.CopyToAsync(destinationStream, CancellationToken.None); + + return outPath; +} \ No newline at end of file diff --git a/examples/PdfMerge/PdfMerge.csproj b/examples/PdfMerge/PdfMerge.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/PdfMerge/PdfMerge.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/PdfMerge/Program.cs b/examples/PdfMerge/Program.cs new file mode 100644 index 0000000..4b1c093 --- /dev/null +++ b/examples/PdfMerge/Program.cs @@ -0,0 +1,66 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; +using Microsoft.Extensions.Configuration; + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); + +var sourcePath = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "pdfs"); +var destinationPath = args.Length > 1 ? args[1] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationPath); + +var result = await DoMerge(sourcePath, destinationPath, options); +Console.WriteLine($"Merged PDF created: {result}"); + +static async Task DoMerge(string sourcePath, string destinationPath, GotenbergSharpClientOptions options) +{ + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut + }; + + var sharpClient = new GotenbergSharpClient(httpClient); + + var items = Directory.GetFiles(sourcePath, "*.pdf", SearchOption.TopDirectoryOnly) + .Select(p => new { Info = new FileInfo(p), Path = p }) + .Where(item => !item.Info.Name.Contains("GotenbergMergeResult.pdf")) + .OrderBy(item => item.Info.CreationTime) + .Take(2); + + Console.WriteLine($"Merging {items.Count()} PDFs:"); + foreach (var item in items) + { + Console.WriteLine($" - {item.Info.Name}"); + } + + var toMerge = items.Select(item => KeyValuePair.Create(item.Info.Name, File.ReadAllBytes(item.Path))); + + var builder = new MergeBuilder() + .SetPdfFormat(LibrePdfFormats.A2b) + .WithAssets(b => { b.AddItems(toMerge); }); + + var request = builder.Build(); + var response = await sharpClient.MergePdfsAsync(request); + + var outPath = Path.Combine(destinationPath, "GotenbergMergeResult.pdf"); + + await using var destinationStream = File.Create(outPath); + + await response.CopyToAsync(destinationStream, CancellationToken.None); + + return outPath; +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..4273aa9 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,119 @@ +# Gotenberg Sharp Client Examples + +This directory contains console application examples demonstrating various features of the Gotenberg Sharp API Client. + +## Prerequisites + +1. **Gotenberg Server**: You need a running Gotenberg instance with basic authentication. See the [main README](../README.md) for setup instructions. + + The examples are pre-configured to use the docker-compose setup with basic auth: + ```bash + docker-compose -f docker/docker-compose-basic-auth.yml up -d + ``` + +2. **.NET 8.0 SDK**: All examples target .NET 8.0. + +## Configuration + +All examples share a common configuration file: `appsettings.json` + +The default configuration includes: +- **Service URL**: `http://localhost:3000` +- **Basic Auth**: Username `testuser`, Password `testpass` (matching docker-compose-basic-auth.yml) +- **Retry Policy**: Enabled with 4 retries and exponential backoff + +You can modify these settings in `appsettings.json` or update the credentials directly in the example code. + +## Examples + +### HtmlConvert +Converts HTML to PDF with embedded assets. + +```bash +cd HtmlConvert +dotnet run [output-directory] +``` + +### DIExample +Demonstrates dependency injection setup with logging and Polly retry policy. + +```bash +cd DIExample +dotnet run [output-directory] +``` + +### PdfMerge +Merges multiple PDF files into a single PDF. + +```bash +cd PdfMerge +dotnet run [source-directory] [output-directory] +``` + +### UrlsToMergedPdf +Converts multiple URLs to PDFs and merges them into a single document. Creates a news summary from major news sites. + +**Note**: Requires increased Gotenberg timeout (`--api-timeout=1800s`) + +```bash +cd UrlsToMergedPdf +dotnet run [output-directory] +``` + +### HtmlWithMarkdown +Converts HTML containing Markdown to PDF. + +```bash +cd HtmlWithMarkdown +dotnet run [output-directory] +``` + +### OfficeMerge +Merges Office documents (Word, Excel, PowerPoint) into a single PDF. + +```bash +cd OfficeMerge +dotnet run [source-directory] [output-directory] +``` + +### UrlConvert +Converts a URL to PDF with custom header and footer. + +```bash +cd UrlConvert +dotnet run [output-directory] +``` + +### Webhook +Demonstrates webhook functionality for async PDF generation. + +**Note**: Requires a webhook receiver API running on `localhost:5000` + +```bash +cd Webhook +dotnet run +``` + +### PdfConvert +Converts PDF files to PDF/A formats (A1a, A2b, A3b). + +```bash +cd PdfConvert +dotnet run [source-directory] [output-directory] +``` + +## Project Structure + +- **Directory.Build.props**: Shared configuration for all example projects (target framework, dependencies, resources) +- **appsettings.json**: Shared Gotenberg client configuration +- **resources/**: Shared resource files used by the examples (HTML templates, images, Office documents, etc.) + +## Running Examples + +Each example can be run independently. Most examples accept optional command-line arguments for specifying input/output directories. If no arguments are provided, they use sensible defaults (usually `./output` for generated files). + +Example: +```bash +cd PdfMerge +dotnet run C:\MyPdfs C:\Output +``` diff --git a/examples/UrlConvert/Program.cs b/examples/UrlConvert/Program.cs new file mode 100644 index 0000000..4533ba1 --- /dev/null +++ b/examples/UrlConvert/Program.cs @@ -0,0 +1,67 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; + +using Microsoft.Extensions.Configuration; + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); + +var destinationPath = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationPath); + +var resourcePath = Path.Combine(AppContext.BaseDirectory, "resources", "Html"); +var headerPath = Path.Combine(resourcePath, "UrlHeader.html"); +var footerPath = Path.Combine(resourcePath, "UrlFooter.html"); + +var path = await CreateFromUrl(destinationPath, headerPath, footerPath, options); +Console.WriteLine($"PDF created from URL: {path}"); + +static async Task CreateFromUrl(string destinationPath, string headerPath, string footerPath, GotenbergSharpClientOptions options) +{ + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut + }; + + var sharpClient = new GotenbergSharpClient(httpClient); + + var builder = new UrlRequestBuilder() + .SetUrl("https://www.cnn.com") + .SetConversionBehaviors(b => b.SetBrowserWaitDelay(1)) + .ConfigureRequest(b => b.SetTrace("ConsoleExample").SetPageRanges("1-2")) + .AddAsyncHeaderFooter(async b => + b.SetHeader(await File.ReadAllBytesAsync(headerPath)) + .SetFooter(await File.ReadAllBytesAsync(footerPath)) + ) + .WithPageProperties(b => + b.SetPaperSize(PaperSizes.A4) + .UseChromeDefaults() + .SetMarginLeft(0) + .SetMarginRight(0) + ); + + var request = await builder.BuildAsync(); + var response = await sharpClient.UrlToPdfAsync(request); + + var resultPath = Path.Combine(destinationPath, $"GotenbergFromUrl-{DateTime.Now:yyyyMMddHHmmss}.pdf"); + + await using var destinationStream = File.Create(resultPath); + + await response.CopyToAsync(destinationStream, CancellationToken.None); + + return resultPath; +} \ No newline at end of file diff --git a/examples/UrlConvert/UrlConvert.csproj b/examples/UrlConvert/UrlConvert.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/UrlConvert/UrlConvert.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/UrlsToMergedPdf/Program.cs b/examples/UrlsToMergedPdf/Program.cs new file mode 100644 index 0000000..de75d82 --- /dev/null +++ b/examples/UrlsToMergedPdf/Program.cs @@ -0,0 +1,107 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Requests; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; +using Microsoft.Extensions.Configuration; + +// NOTE: You need to increase gotenberg api's timeout for this to work +// by passing --api-timeout=1800s when running the container. + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); + +var destinationDirectory = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output"); +Directory.CreateDirectory(destinationDirectory); + +var path = await CreateWorldNewsSummary(destinationDirectory, options); +Console.WriteLine($"News summary PDF created: {path}"); + +static async Task CreateWorldNewsSummary(string destinationDirectory, GotenbergSharpClientOptions options) +{ + var sites = new[] + { + "https://www.nytimes.com", "https://www.axios.com/", + "https://www.cnn.com", "https://www.csmonitor.com", + "https://www.wsj.com", "https://www.usatoday.com", + "https://www.irishtimes.com", "https://www.lemonde.fr", + "https://calgaryherald.com", "https://www.bbc.com/news/uk", + "https://english.elpais.com/", "https://www.thehindu.com", + "https://www.theaustralian.com.au", "https://www.welt.de", + "https://www.cankaoxiaoxi.com", "https://www.novinky.cz", + "https://www.elobservador.com.uy" + } + .Select(u => new Uri(u)); + + var builders = CreateRequestBuilders(sites); + var requests = builders.Select(b => b.Build()); + + return await ExecuteRequestsAndMerge(requests, destinationDirectory, options); +} + +static IEnumerable CreateRequestBuilders(IEnumerable uris) +{ + foreach (var uri in uris) + { + yield return new UrlRequestBuilder() + .SetUrl(uri) + .ConfigureRequest(b => + { + b.SetPageRanges("1-2"); + }) + .WithPageProperties(b => + { + b.SetMargins(Margins.None) + .SetMarginLeft(.3) + .SetMarginRight(.3); + }); + } +} + +static async Task ExecuteRequestsAndMerge(IEnumerable requests, string destinationDirectory, GotenbergSharpClientOptions options) +{ + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var innerClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl, + Timeout = TimeSpan.FromMinutes(7) + }; + + var sharpClient = new GotenbergSharpClient(innerClient); + + Console.WriteLine("Converting URLs to PDFs..."); + var tasks = requests.Select(r => sharpClient.UrlToPdfAsync(r, CancellationToken.None)); + var results = await Task.WhenAll(tasks); + + Console.WriteLine("Merging PDFs..."); + var mergeBuilder = new MergeBuilder() + .WithAssets(b => + { + b.AddItems(results.Select((r, i) => KeyValuePair.Create($"{i}.pdf", r))); + }); + + var response = await sharpClient.MergePdfsAsync(mergeBuilder.Build(), CancellationToken.None); + + return await WriteFileAndGetPath(response, destinationDirectory); +} + +static async Task WriteFileAndGetPath(Stream responseStream, string destinationDirectory) +{ + var fullPath = Path.Combine(destinationDirectory, $"{DateTime.Now:yyyy-MM-dd}-{DateTime.Now.Ticks}.pdf"); + + await using var destinationStream = File.Create(fullPath); + + await responseStream.CopyToAsync(destinationStream, CancellationToken.None); + + return fullPath; +} diff --git a/examples/UrlsToMergedPdf/UrlsToMergedPdf.csproj b/examples/UrlsToMergedPdf/UrlsToMergedPdf.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/UrlsToMergedPdf/UrlsToMergedPdf.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/Webhook/Program.cs b/examples/Webhook/Program.cs new file mode 100644 index 0000000..60415e1 --- /dev/null +++ b/examples/Webhook/Program.cs @@ -0,0 +1,68 @@ +using Gotenberg.Sharp.API.Client; +using Gotenberg.Sharp.API.Client.Domain.Builders; +using Gotenberg.Sharp.API.Client.Domain.Builders.Faceted; +using Gotenberg.Sharp.API.Client.Domain.Settings; +using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; +using Microsoft.Extensions.Configuration; + +// For this to work you need an API running on localhost:5000 with an endpoint to receive the webhook + +var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json") + .Build(); + +var options = new GotenbergSharpClientOptions(); +config.GetSection(nameof(GotenbergSharpClient)).Bind(options); + +var resourcePath = Path.Combine(AppContext.BaseDirectory, "resources", "Html"); +var headerPath = Path.Combine(resourcePath, "UrlHeader.html"); +var footerPath = Path.Combine(resourcePath, "UrlFooter.html"); + +Console.WriteLine($"Header: {headerPath}"); +Console.WriteLine($"Footer: {footerPath}"); + +await CreateFromUrl(headerPath, footerPath, options); + +Console.WriteLine("Webhook request sent..."); + +static async Task CreateFromUrl(string headerPath, string footerPath, GotenbergSharpClientOptions options) +{ + using var handler = new HttpClientHandler(); + using var authHandler = !string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword) + ? new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler } + : null; + + using var httpClient = new HttpClient(authHandler ?? (HttpMessageHandler)handler) + { + BaseAddress = options.ServiceUrl, + Timeout = options.TimeOut + }; + + var sharpClient = new GotenbergSharpClient(httpClient); + + var builder = new UrlRequestBuilder() + .SetUrl("https://www.newyorker.com") + .ConfigureRequest(b => + { + b.AddWebhook(hook => + { + hook.SetUrl("http://host.docker.internal:5000/api/WebhookReceiver") + .SetErrorUrl("http://host.docker.internal:5000/api/WebhookReceiver") + .AddExtraHeader("custom-header", "value"); + }).SetPageRanges("1-2"); + }) + .AddAsyncHeaderFooter(async b => + b.SetHeader(await File.ReadAllTextAsync(headerPath)) + .SetFooter(await File.ReadAllBytesAsync(footerPath)) + ) + .WithPageProperties(b => + { + b.SetPaperSize(PaperSizes.A4) + .SetMargins(Margins.None); + }); + + var request = await builder.BuildAsync(); + + await sharpClient.FireWebhookAndForgetAsync(request); +} diff --git a/examples/Webhook/Webhook.csproj b/examples/Webhook/Webhook.csproj new file mode 100644 index 0000000..35e3d84 --- /dev/null +++ b/examples/Webhook/Webhook.csproj @@ -0,0 +1,2 @@ + + diff --git a/examples/appsettings.json b/examples/appsettings.json new file mode 100644 index 0000000..94bc1e9 --- /dev/null +++ b/examples/appsettings.json @@ -0,0 +1,14 @@ +{ + "GotenbergSharpClient": { + "ServiceUrl": "http://localhost:3000", + "HealthCheckUrl": "http://localhost:3000/health", + "BasicAuthUsername": "testuser", + "BasicAuthPassword": "testpass", + "RetryPolicy": { + "Enabled": true, + "RetryCount": 4, + "BackoffPower": 1.5, + "LoggingEnabled": true + } + } +} diff --git a/linqpad/Resources/Html/ConvertExample/body.html b/examples/resources/Html/ConvertExample/body.html similarity index 100% rename from linqpad/Resources/Html/ConvertExample/body.html rename to examples/resources/Html/ConvertExample/body.html diff --git a/linqpad/Resources/Html/ConvertExample/ear-on-beach.jpg b/examples/resources/Html/ConvertExample/ear-on-beach.jpg similarity index 100% rename from linqpad/Resources/Html/ConvertExample/ear-on-beach.jpg rename to examples/resources/Html/ConvertExample/ear-on-beach.jpg diff --git a/linqpad/Resources/Html/ConvertExample/footer.html b/examples/resources/Html/ConvertExample/footer.html similarity index 100% rename from linqpad/Resources/Html/ConvertExample/footer.html rename to examples/resources/Html/ConvertExample/footer.html diff --git a/linqpad/Resources/Html/UrlFooter.html b/examples/resources/Html/UrlFooter.html similarity index 100% rename from linqpad/Resources/Html/UrlFooter.html rename to examples/resources/Html/UrlFooter.html diff --git a/linqpad/Resources/Html/UrlHeader.html b/examples/resources/Html/UrlHeader.html similarity index 100% rename from linqpad/Resources/Html/UrlHeader.html rename to examples/resources/Html/UrlHeader.html diff --git a/linqpad/Resources/Html/font.woff b/examples/resources/Html/font.woff similarity index 100% rename from linqpad/Resources/Html/font.woff rename to examples/resources/Html/font.woff diff --git a/linqpad/Resources/Html/footer.html b/examples/resources/Html/footer.html similarity index 100% rename from linqpad/Resources/Html/footer.html rename to examples/resources/Html/footer.html diff --git a/linqpad/Resources/Html/header.html b/examples/resources/Html/header.html similarity index 100% rename from linqpad/Resources/Html/header.html rename to examples/resources/Html/header.html diff --git a/linqpad/Resources/Html/img.gif b/examples/resources/Html/img.gif similarity index 100% rename from linqpad/Resources/Html/img.gif rename to examples/resources/Html/img.gif diff --git a/linqpad/Resources/Html/index.html b/examples/resources/Html/index.html similarity index 100% rename from linqpad/Resources/Html/index.html rename to examples/resources/Html/index.html diff --git a/linqpad/Resources/Html/style.css b/examples/resources/Html/style.css similarity index 100% rename from linqpad/Resources/Html/style.css rename to examples/resources/Html/style.css diff --git a/linqpad/Resources/Markdown/font.woff b/examples/resources/Markdown/font.woff similarity index 100% rename from linqpad/Resources/Markdown/font.woff rename to examples/resources/Markdown/font.woff diff --git a/linqpad/Resources/Markdown/footer.html b/examples/resources/Markdown/footer.html similarity index 100% rename from linqpad/Resources/Markdown/footer.html rename to examples/resources/Markdown/footer.html diff --git a/linqpad/Resources/Markdown/header.html b/examples/resources/Markdown/header.html similarity index 100% rename from linqpad/Resources/Markdown/header.html rename to examples/resources/Markdown/header.html diff --git a/linqpad/Resources/Markdown/img.gif b/examples/resources/Markdown/img.gif similarity index 100% rename from linqpad/Resources/Markdown/img.gif rename to examples/resources/Markdown/img.gif diff --git a/linqpad/Resources/Markdown/index.html b/examples/resources/Markdown/index.html similarity index 100% rename from linqpad/Resources/Markdown/index.html rename to examples/resources/Markdown/index.html diff --git a/linqpad/Resources/Markdown/paragraph1.md b/examples/resources/Markdown/paragraph1.md similarity index 100% rename from linqpad/Resources/Markdown/paragraph1.md rename to examples/resources/Markdown/paragraph1.md diff --git a/linqpad/Resources/Markdown/paragraph2.md b/examples/resources/Markdown/paragraph2.md similarity index 100% rename from linqpad/Resources/Markdown/paragraph2.md rename to examples/resources/Markdown/paragraph2.md diff --git a/linqpad/Resources/Markdown/paragraph3.md b/examples/resources/Markdown/paragraph3.md similarity index 100% rename from linqpad/Resources/Markdown/paragraph3.md rename to examples/resources/Markdown/paragraph3.md diff --git a/linqpad/Resources/Markdown/style.css b/examples/resources/Markdown/style.css similarity index 100% rename from linqpad/Resources/Markdown/style.css rename to examples/resources/Markdown/style.css diff --git a/linqpad/Resources/OfficeDocs/LorumIpsem.txt b/examples/resources/OfficeDocs/LorumIpsem.txt similarity index 100% rename from linqpad/Resources/OfficeDocs/LorumIpsem.txt rename to examples/resources/OfficeDocs/LorumIpsem.txt diff --git a/linqpad/Resources/OfficeDocs/Visual-Studio-NOTICE.docx b/examples/resources/OfficeDocs/Visual-Studio-NOTICE.docx similarity index 100% rename from linqpad/Resources/OfficeDocs/Visual-Studio-NOTICE.docx rename to examples/resources/OfficeDocs/Visual-Studio-NOTICE.docx diff --git a/linqpad/Resources/OfficeDocs/document.docx b/examples/resources/OfficeDocs/document.docx similarity index 100% rename from linqpad/Resources/OfficeDocs/document.docx rename to examples/resources/OfficeDocs/document.docx diff --git a/linqpad/Resources/OfficeDocs/document.rtf b/examples/resources/OfficeDocs/document.rtf similarity index 100% rename from linqpad/Resources/OfficeDocs/document.rtf rename to examples/resources/OfficeDocs/document.rtf diff --git a/linqpad/Resources/OfficeDocs/document2.docx b/examples/resources/OfficeDocs/document2.docx similarity index 100% rename from linqpad/Resources/OfficeDocs/document2.docx rename to examples/resources/OfficeDocs/document2.docx diff --git a/linqpad/Resources/Settings/appsettings.json b/examples/resources/Settings/appsettings.json similarity index 100% rename from linqpad/Resources/Settings/appsettings.json rename to examples/resources/Settings/appsettings.json diff --git a/linqpad/Resources/office/document.docx b/examples/resources/office/document.docx similarity index 100% rename from linqpad/Resources/office/document.docx rename to examples/resources/office/document.docx diff --git a/src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs b/src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs index cdf89b6..beb2a7f 100644 --- a/src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs +++ b/src/Gotenberg.Sharp.Api.Client/Extensions/TypedClientServiceCollectionExtensions.cs @@ -52,19 +52,10 @@ public static IHttpClientBuilder AddGotenbergSharpClient( var ops = GetOptions(sp); client.Timeout = ops.TimeOut; client.BaseAddress = ops.ServiceUrl; - - // Add basic auth header if credentials are provided - if (!string.IsNullOrWhiteSpace(ops.BasicAuthUsername) && - !string.IsNullOrWhiteSpace(ops.BasicAuthPassword)) - { - var credentials = Convert.ToBase64String( - System.Text.Encoding.ASCII.GetBytes($"{ops.BasicAuthUsername}:{ops.BasicAuthPassword}")); - client.DefaultRequestHeaders.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials); - } }); } + /// /// Registers GotenbergSharpClient with dependency injection using a custom HttpClient configuration. /// @@ -82,7 +73,7 @@ public static IHttpClientBuilder AddGotenbergSharpClient( { if (configureClient == null) throw new ArgumentNullException(nameof(configureClient)); - return services + var builder = services .AddHttpClient(nameof(GotenbergSharpClient), configureClient) .AddTypedClient() .ConfigurePrimaryHttpMessageHandler( @@ -92,8 +83,33 @@ public static IHttpClientBuilder AddGotenbergSharpClient( AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate })) + .AddHttpMessageHandler(sp => + { + var ops = GetOptions(sp); + + var hasUsername = !string.IsNullOrWhiteSpace(ops.BasicAuthUsername); + var hasPassword = !string.IsNullOrWhiteSpace(ops.BasicAuthPassword); + + // Validate that both username and password are provided together + if (hasUsername ^ hasPassword) + { + throw new InvalidOperationException( + "BasicAuth configuration is incomplete. Both BasicAuthUsername and BasicAuthPassword must be set, or neither should be set."); + } + + // Add basic auth handler if credentials are configured + if (hasUsername && hasPassword) + { + return new BasicAuthHandler(ops.BasicAuthUsername!, ops.BasicAuthPassword!); + } + + // Return a pass-through handler if no auth is configured + return new PassThroughHandler(); + }) .AddPolicyHandler(PolicyFactory.CreatePolicyFromSettings) .SetHandlerLifetime(TimeSpan.FromMinutes(6)); + + return builder; } private static GotenbergSharpClientOptions GetOptions(IServiceProvider sp) diff --git a/src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/BasicAuthHandler.cs b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/BasicAuthHandler.cs new file mode 100644 index 0000000..3cd7d96 --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/BasicAuthHandler.cs @@ -0,0 +1,49 @@ +// Copyright 2019-2025 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. + +using System.Net.Http.Headers; +using System.Text; + +namespace Gotenberg.Sharp.API.Client.Infrastructure.Pipeline; + +/// +/// HTTP message handler that adds Basic Authentication headers to outgoing requests +/// +public class BasicAuthHandler : DelegatingHandler +{ + private readonly string _username; + private readonly string _password; + + /// + /// Creates a new BasicAuthHandler with the specified credentials + /// + /// Basic auth username + /// Basic auth password + public BasicAuthHandler(string username, string password) + { + _username = username ?? throw new ArgumentNullException(nameof(username)); + _password = password ?? throw new ArgumentNullException(nameof(password)); + } + + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_username}:{_password}")); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); + + return base.SendAsync(request, cancellationToken); + } +} diff --git a/src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/PassThroughHandler.cs b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/PassThroughHandler.cs new file mode 100644 index 0000000..81f830b --- /dev/null +++ b/src/Gotenberg.Sharp.Api.Client/Infrastructure/Pipeline/PassThroughHandler.cs @@ -0,0 +1,29 @@ +// Copyright 2019-2025 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.Infrastructure.Pipeline; + +/// +/// HTTP message handler that passes requests through without modification +/// +internal class PassThroughHandler : DelegatingHandler +{ + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + return base.SendAsync(request, cancellationToken); + } +}