Giter Site home page Giter Site logo

changemakerstudios / gotenbergsharpapiclient Goto Github PK

View Code? Open in Web Editor NEW
112.0 10.0 17.0 2.13 MB

.NET C# Client for the Gotenberg API

License: Apache License 2.0

C# 37.02% HTML 1.45% CSS 0.17% Rich Text Format 61.37%
pdf pdf-converter csharp netstandard url-to-pdf html-to-pdf pdf-merge typed-clients fluent async

gotenbergsharpapiclient's Introduction

gotenberg icon Gotenberg.Sharp.Api.Client

NuGet version Downloads Build status

⭐ For Gotenberg v7 & v8 ⭐

.NET C# Client for interacting with the Gotenberg v7 & v8 micro-service's API. Gotenberg is a Docker-powered stateless API for converting & merging HTML, Markdown and Office documents to PDF. The client supports a configurable Polly retry policy with exponential backoff for handling transient exceptions.

Getting Started

Pull the image from dockerhub.com

> docker pull gotenberg/gotenberg:latest

Create & start a container

docker run --name gotenbee8x --rm -p 3000:3000 gotenberg/gotenberg:latest gotenberg --api-timeout=1800s --log-level=debug

.NET Core Project Setup

Install nuget package into your project

PM> Install-Package Gotenberg.Sharp.Api.Client

Note: Use v1.x nugets for Gotenberg v6.

AppSettings

  "GotenbergSharpClient": {
    "ServiceUrl": "http://localhost:3000",
    "HealthCheckUrl": "http://localhost:3000/health",
    "RetryPolicy": {
      "Enabled": true,
      "RetryCount": 4,
      "BackoffPower": 1.5,
      "LoggingEnabled": true
    }
  }

Configure Services In Startup.cs

public void ConfigureServices(IServiceCollection services)
{
	.....
    services.AddOptions<GotenbergSharpClientOptions>()
	        .Bind(Configuration.GetSection(nameof(GotenbergSharpClient)));
    services.AddGotenbergSharpClient();
	.....    
}

Using GotenbergSharpClient

See the linqPad folder for complete examples.

Html To Pdf

With embedded assets:

 [HttpGet]
 public async Task<ActionResult> HtmlToPdf([FromServices] GotenbergSharpClient sharpClient)
 {
     var builder = new HtmlRequestBuilder()
         .AddDocument(doc => 
             doc.SetBody(GetBody()).SetFooter(GetFooter())
         ).WithDimensions(dims =>
         {
             dims.SetPaperSize(PaperSizes.A3)
                 .SetMargins(Margins.None)
                 .SetScale(.99);
         }).WithAsyncAssets(async assets => assets.AddItem("some-image.jpg", await GetImageBytes()));

     var req = await builder.BuildAsync();

     var result = await sharpClient.HtmlToPdfAsync(req);

     return this.File(result, "application/pdf", "gotenbergFromHtml.pdf");
 }

Url To Pdf

Url to Pdf with custom page range, header & footer:

public async Task<Stream> CreateFromUrl(string headerPath, string footerPath)
{
	var builder = new UrlRequestBuilder()
		.SetUrl("https://www.cnn.com")
		.ConfigureRequest(config =>
		{
			config.SetPageRanges("1-2");
		})
		.AddAsyncHeaderFooter(async
			doc => doc.SetHeader(await File.ReadAllTextAsync(headerPath))
				  .SetFooter(await File.ReadAllBytesAsync(footerPath)
		)).WithDimensions(dims =>
		{
			dims.SetPaperSize(PaperSizes.A4)
			 .SetMargins(Margins.None)
			 .SetScale(.90)
			 .LandScape();
		});

	var request = await builder.BuildAsync();
	return await _sharpClient.UrlToPdfAsync(request);
}

Merge Office Docs

Merges office documents and configures the request time-out:

public async Task<Stream> DoOfficeMerge(string sourceDirectory)
{
	var builder = new MergeOfficeBuilder()
		.WithAsyncAssets(async a => a.AddItems(await GetDocsAsync(sourceDirectory)));

	var request = await builder.BuildAsync();
	return await _sharpClient.MergeOfficeDocsAsync(request);
}

Markdown to Pdf

Markdown to Pdf conversion with embedded assets:

public async Task<Stream> CreateFromMarkdown()
{
	var builder = new HtmlRequestBuilder()
		.AddAsyncDocument(async
			doc => doc.SetHeader(await this.GetHeaderAsync())
				  .SetBody(await GetBodyAsync())
				  .ContainsMarkdown()
				  .SetFooter(await GetFooterAsync())
		).WithDimensions(dims =>
		{
			dims.UseChromeDefaults().LandScape().SetScale(.90);
		}).WithAsyncAssets(async
			a => a.AddItems(await GetMarkdownAssets())
		));

	var request = await builder.BuildAsync();
	return await _sharpClient.HtmlToPdfAsync(request);
}

Webhook

All request types support webhooks

 public async Task SendUrlToWebhookEndpoint(string headerPath, string footerPath)
 {
     var builder = new UrlRequestBuilder()
         .SetUrl("https://www.cnn.com")
         .ConfigureRequest(reqBuilder =>
         {
             reqBuilder.AddWebhook(hook =>
                 {
                     hook.SetUrl("http://host.docker.internal:5000/api/your/webhookReceiver")
                         .SetErrorUrl("http://host.docker.internal:5000/api/your/webhookReceiver/error")
                         .AddExtraHeader("custom-header", "value");
                 })
                 .SetPageRanges("1-2");
         })
         .AddAsyncHeaderFooter(async
             b => b.SetHeader(await System.IO.File.ReadAllTextAsync(headerPath))
                   .SetFooter(await System.IO.File.ReadAllBytesAsync(footerPath))
         ).WithDimensions(dimBuilder =>
         {
             dimBuilder.SetPaperSize(PaperSizes.A4)
                 .SetMargins(Margins.None)
                 .SetScale(.90)
                 .LandScape();
         });

     var request = await builder.BuildAsync();

     await _sharpClient.FireWebhookAndForgetAsync(request);
 }

Merge 15 Urls to one pdf

Builds a 30 page pdf by merging the front two pages of 15 news sites. Takes about a minute to complete

public async Task<Stream> CreateWorldNewsSummary()
{
    var sites = new[]
        {
            "https://www.nytimes.com", "https://www.axios.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://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 = CreateBuilders(sites);
    var requests = builders.Select(b => b.Build());

    return await ExecuteRequestsAndMerge(requests);
}

IEnumerable<UrlRequestBuilder> CreateBuilders(IEnumerable<Uri> uris)
{
    foreach (var uri in uris)
    {
        yield return new UrlRequestBuilder()
            .SetUrl(uri)
            .ConfigureRequest(req => { req.SetPageRanges("1-2"); })
            .AddHeaderFooter(docBuilder =>
            {
                docBuilder.SetHeader(GetHeadFoot(uri.Host.Replace("www.", string.Empty).ToUpper()))
                   .SetFooter(GetHeadFoot(uri.ToString()));
            })
            .WithDimensions(dimBuilder =>
            {
                dimBuilder.UseChromeDefaults()
                    .SetScale(.90)
                    .LandScape()
                    .MarginLeft(.5)
                    .MarginRight(.5);
            });
    }

    static string GetHeadFoot(string heading)
        => "<html><head> <style> body { font-size: 8rem; } h1 { margin-left: auto; margin-right: auto; } </style></head><body><h1>" +
           heading + "</h1></body></html>";
}

async Task<Stream> ExecuteRequestsAndMerge(IEnumerable<UrlRequest> requests)
{
    var tasks = requests.Select(r => _sharpClient.UrlToPdfAsync(r));
    var results = await Task.WhenAll(tasks);

    var mergeBuilder = new MergeBuilder()
        .WithAssets(b => { 
            b.AddItems(results.Select((r, i) => KeyValuePair.Create($"{i}.pdf", r))); 
        });

    var request = mergeBuilder.Build();
    return await _sharpClient.MergePdfsAsync(request);
}

gotenbergsharpapiclient's People

Contributors

chrismo111 avatar jaben avatar naefp avatar raschmitt avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

gotenbergsharpapiclient's Issues

Parsing error on demensions parse float issue

I started using the example defined in the getting started and had instantly this error:

{"level":"error","msg":"strconv.ParseFloat: parsing "0,5": invalid syntax","op":"xhttp.htmlHandler: xhttp.chromePrinterOptions: resource.MarginArgs: resource.Resource.Float64Arg: xassert.Float64","time":"2020-03-04T09:27:32Z","trace":"RP4I2w1Xa684qWtwV7zeeRktGsi3j9I8"}

Compatibility with netcoreapp2.2

Should it be possible to reference this from a netcoreapp2.2 project?

Gotenberg.Sharp.Api.Client.csproj references Microsoft.Extensions.Http v3.0.0 which appears to prevent this situation

  • Gotenberg.Sharp.Api.Client.csproj
...
    <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Http" Version="3.0.0" />
...

The change from v2.2.0 to v3.0.0 took place here

New version of Gotenberg 🚀

Hello here 👋

I've just released version 7.0.0 of Gotenberg 🎉

A lot of minor changes, the best if you wish to update your C# client is to head to the new documentation.

Also, I've added your repository to the awesome list 😄 currently, I've marked it as only compatible with Gotenberg 6.x, but do not hesitate to open a PR when your client is compatible with version 7 👍

PS: if you have any questions, please ask them!

Add support for the `exportFormFields` form field

Hi,

I have a small feature suggestion:

Gotenberg 8.3.0 introduced the new exportFormFields form field as an option for its forms/libreoffice/convert end point.
It's also documented at https://gotenberg.dev/docs/routes#page-properties-libreoffice.

This option is especially useful when converting Word documents containing content controls as currently, these are automatically transformed into form fields - a transformation that's not always ideal.

Best regards,
Mario

Dimensions.cs ToHttpContent() breaks numeric values due to unawareness of globalization

Used the API on a german Windows. Having this the output of ToString() of 8.3 will be 8,3 which breaks the request to Gotenberg with an unable to convert to float message.

Used this to fix the issue:

StringContent contentItem;
switch (value)
{
  case float f:
    contentItem = new StringContent(f.ToString(System.Globalization.CultureInfo.GetCultureInfo("en-US")));
    break;
   case double d:
     contentItem = new StringContent(d.ToString(System.Globalization.CultureInfo.GetCultureInfo("en-US")));
     break;
    case decimal c:
      contentItem = new StringContent(c.ToString(System.Globalization.CultureInfo.GetCultureInfo("en-US")));
      break;
     case int i:
       contentItem = new StringContent(i.ToString(System.Globalization.CultureInfo.GetCultureInfo("en-US")));
       break;
      case long l:
        contentItem = new StringContent(l.ToString(System.Globalization.CultureInfo.GetCultureInfo("en-US")));
        break;
      case DateTime date:
        contentItem = new StringContent(date.ToString(System.Globalization.CultureInfo.GetCultureInfo("en-US")));
        break;
      default:
        contentItem = new StringContent(value.ToString());  
        break;
}

No emulatedMediaType option for v7

There is currently no emulatedMediaType=screen option.

This option renders page exactly what it looks like on web and not in print mode.

I will be happy to contribute to v7 branch if you need any help.

Release v1.1.0

@chrismo111 Okay to go ahead and release v1.1 with these fixes or do you have other things you want to put in there?

Convert single office file to pdf

As I understand you have to use GotenbergSharpClient.MergeOfficeDocsAsync for converting office files. But there is limitation that only allows 2 or more files to be converted. There is check for that:

foreach (var isValid in new[] { Count > 1, validItems.Value.Count > 1 })
if (!isValid) yield break;

Although Gotengberg docs shows that you could also use only one file.
Is there a reason for this limitation or should I use some other method?

Error handling

It looks like the client returns the result on error without any hints that is response is not 200 OK. Should it not throw an exception at least?

Container returns "Not Found" for all endpoints

I get 404 response for each gotenberg container endpoint. I tried every way to create it, each one returns the same - the string "Not Found" along with the HTTP 404 code. In the logs I get this:
image

I tried to use images, but neither works:

  • thecodingmachine/gotenberg:latest
  • thecodingmachine/gotenberg:7
  • gotenberg/gotenberg:gotenberg:latest
  • thecodingmachine/gotenberg:7

Using from within code also doesn't work. I have no ideas what else to do. I am asking for tips and correction of the log, because the present say nothing.

Cannot select text from PDF result

Hi

The Gotenberg pdf result after converting from HTML to PDF is not containing selectable text, is there a solution to this?

Mitt-f-rste-skjema-UXY-DKU.pdf

    var bodyHtml = await GetResource("body.html");
    var styleCss = await GetResource("style.html");
    
    var fullHtml = bodyHtml.Replace("{{body}}", message.HTML).Replace("{{style}}", styleCss);

    var footerHtml = FooterGenerator.GenerateFooter(message.DialogueName, message.DialogueId, message.ReferenceId);

    var builder = new HtmlRequestBuilder()
        .AddDocument(doc =>
            doc.SetBody(fullHtml)
               .SetFooter(footerHtml)
        )
        .WithDimensions(dims =>
            dims.SetPaperSize(PaperSizes.A4)
                .SetMargins(Margins.Normal)
        )
        .SetConversionBehaviors(c =>
            c.SetPdfFormat(PdfFormats.A2b)
        );
  
    var req = await builder.BuildAsync();
    var result = await _client.HtmlToPdfAsync(req);
    var resultAsByteArray = StreamToByteArray(result);

body.html:

<html>
    <head>
        <link href="https://fonts.googleapis.com/css?family=Roboto&display=swap"
              rel="stylesheet" />
            {{style}}

    </head>
    <body>
        {{body}}
    </body>
</html>

footer.html:

<html>
   <head>
      <style>
         body {
         font-size: 8rem;
         }
         p {
         width: 100%;
         text-align: center;
         line-height: 1.5;
         }
      </style>
   </head>
   <body>
      <p>Mitt første skjema - ACOS-1 - UXY-DKU<br/>
         <span class="pageNumber"></span> / <span class="totalPages"></span>
      </p>
   </body>
</html>

message.HTML:

            <div class='summary-container'>
                <div class='summary-header-container'>
                    
                <div class='summary-header'>
                    <h1 class='summary-title front-hidden'>Mitt første skjema</h1>
                    <div class='summary-submitted front-hidden'>
                        <h2 class='summary-submitted-label'>Innsendt: </h2> 
                        <h2 class='summary-submitted-value'>22.05.2024 08:36</h2>
                    </div>
                    <div class='summary-reference-id front-hidden'>
                        <h2 class='summary-reference-id-label'>Referanse-ID: </h2> 
                        <h2 class='summary-reference-id-value'>UXY-DKU</h2>
                    </div>
                    <div class='summary-submitted front-hidden'>
                        <h2 class='summary-dialogueid-label'>Skjema-ID: </h2> 
                        <h2 class='summary-dialogueid-value'>ACOS-1</h2>
                    </div>
                </div>
            
                    <img class='summary-logo' src='data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI3LjkuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMTUuMzMgNTguMjEiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDIxNS4zMyA1OC4yMTsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMxRjNGNjU7fQo8L3N0eWxlPgo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNNzQuMDgsNDAuODhjMCwwLjExLTAuMDMsMC4yLTAuMDgsMC4yOHMtMC4xNSwwLjE0LTAuMjgsMC4xOXMtMC4zMSwwLjA5LTAuNTMsMC4xMgoJYy0wLjIyLDAuMDMtMC41LDAuMDUtMC44MywwLjA1Yy0wLjMyLDAtMC41OS0wLjAyLTAuODItMC4wNWMtMC4yMy0wLjAzLTAuNDEtMC4wNy0wLjU0LTAuMTJzLTAuMjMtMC4xMS0wLjI4LTAuMTkKCWMtMC4wNS0wLjA4LTAuMDgtMC4xNy0wLjA4LTAuMjhWMTYuMDJjMC0wLjExLDAuMDMtMC4yLDAuMDktMC4yOGMwLjA2LTAuMDgsMC4xNi0wLjE0LDAuMy0wLjE5czAuMzItMC4wOSwwLjU0LTAuMTIKCXMwLjQ4LTAuMDUsMC43OS0wLjA1YzAuMzMsMCwwLjYxLDAuMDIsMC44MywwLjA1czAuNCwwLjA3LDAuNTMsMC4xMnMwLjIzLDAuMTEsMC4yOCwwLjE5YzAuMDUsMC4wOCwwLjA4LDAuMTcsMC4wOCwwLjI4VjQwLjg4eiIvPgo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNOTYuMSw0MC45YzAsMC4xMS0wLjAzLDAuMi0wLjA4LDAuMjdzLTAuMTQsMC4xNC0wLjI2LDAuMTlzLTAuMjksMC4wOS0wLjUsMC4xMmMtMC4yMSwwLjAzLTAuNDgsMC4wNC0wLjgsMC4wNAoJYy0wLjMzLDAtMC42MS0wLjAxLTAuODItMC4wNHMtMC4zOC0wLjA3LTAuNS0wLjEycy0wLjIxLTAuMTItMC4yNi0wLjE5Yy0wLjA1LTAuMDctMC4wOC0wLjE2LTAuMDgtMC4yN1YzMC4zNQoJYzAtMS4wMy0wLjA4LTEuODUtMC4yNC0yLjQ4cy0wLjM5LTEuMTctMC43LTEuNjJzLTAuNy0wLjgtMS4xOS0xLjA0cy0xLjA1LTAuMzYtMS42OS0wLjM2Yy0wLjgzLDAtMS42NiwwLjI5LTIuNDgsMC44OAoJYy0wLjgzLDAuNTktMS43LDEuNDUtMi42LDIuNThWNDAuOWMwLDAuMTEtMC4wMywwLjItMC4wOCwwLjI3cy0wLjE0LDAuMTQtMC4yNiwwLjE5cy0wLjI5LDAuMDktMC41LDAuMTIKCWMtMC4yMSwwLjAzLTAuNDksMC4wNC0wLjgyLDAuMDRjLTAuMzIsMC0wLjU5LTAuMDEtMC44LTAuMDRjLTAuMjEtMC4wMy0wLjM4LTAuMDctMC41MS0wLjEyYy0wLjEzLTAuMDUtMC4yMS0wLjEyLTAuMjYtMC4xOQoJcy0wLjA3LTAuMTYtMC4wNy0wLjI3VjIyLjg4YzAtMC4xMSwwLjAyLTAuMiwwLjA2LTAuMjdzMC4xMi0wLjE0LDAuMjQtMC4yYzAuMTItMC4wNiwwLjI3LTAuMSwwLjQ2LTAuMTJzMC40My0wLjAzLDAuNzQtMC4wMwoJYzAuMjksMCwwLjU0LDAuMDEsMC43MywwLjAzczAuMzQsMC4wNiwwLjQ1LDAuMTJjMC4xMSwwLjA2LDAuMTgsMC4xMywwLjIzLDAuMnMwLjA3LDAuMTYsMC4wNywwLjI3djIuMzgKCWMxLjAxLTEuMTMsMi4wMy0xLjk2LDMuMDMtMi40OWMxLjAxLTAuNTMsMi4wMi0wLjc5LDMuMDUtMC43OWMxLjIsMCwyLjIxLDAuMiwzLjAzLDAuNjFjMC44MiwwLjQxLDEuNDgsMC45NSwxLjk5LDEuNjMKCWMwLjUxLDAuNjgsMC44NywxLjQ4LDEuMDksMi4zOXMwLjMzLDIuMDEsMC4zMywzLjI5TDk2LjEsNDAuOUw5Ni4xLDQwLjl6Ii8+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xMTEuMTEsMzkuNThjMCwwLjM5LTAuMDMsMC42OS0wLjA4LDAuOTJjLTAuMDUsMC4yMy0wLjEzLDAuMzktMC4yNCwwLjVzLTAuMjcsMC4yMS0wLjQ4LDAuMwoJYy0wLjIxLDAuMDktMC40NiwwLjE3LTAuNzMsMC4yM2MtMC4yNywwLjA2LTAuNTYsMC4xMS0wLjg3LDAuMTVzLTAuNjEsMC4wNi0wLjkyLDAuMDZjLTAuOTMsMC0xLjc0LTAuMTItMi40LTAuMzcKCWMtMC42Ny0wLjI1LTEuMjEtMC42Mi0xLjY0LTEuMTJzLTAuNzQtMS4xMy0wLjkzLTEuOXMtMC4yOS0xLjY3LTAuMjktMi43MVYyNS4xaC0yLjUyYy0wLjIsMC0wLjM2LTAuMTEtMC40OC0wLjMyCglzLTAuMTgtMC41Ni0wLjE4LTEuMDRjMC0wLjI1LDAuMDItMC40NywwLjA1LTAuNjRzMC4wOC0wLjMyLDAuMTMtMC40M3MwLjEyLTAuMTksMC4yMS0wLjI0YzAuMDktMC4wNSwwLjE4LTAuMDcsMC4yOS0wLjA3aDIuNQoJdi00LjI4YzAtMC4wOSwwLjAyLTAuMTgsMC4wNy0wLjI2YzAuMDUtMC4wOCwwLjEzLTAuMTUsMC4yNi0wLjIxYzAuMTMtMC4wNiwwLjMtMC4xLDAuNTEtMC4xM2MwLjIxLTAuMDMsMC40OC0wLjA0LDAuOC0wLjA0CgljMC4zMywwLDAuNjEsMC4wMSwwLjgyLDAuMDRjMC4yMSwwLjAzLDAuMzgsMC4wNywwLjUsMC4xM3MwLjIxLDAuMTMsMC4yNiwwLjIxYzAuMDUsMC4wOCwwLjA4LDAuMTcsMC4wOCwwLjI2djQuMjhoNC42MgoJYzAuMTEsMCwwLjIsMC4wMiwwLjI4LDAuMDdzMC4xNSwwLjEzLDAuMjEsMC4yNGMwLjA2LDAuMTEsMC4xLDAuMjYsMC4xMywwLjQzYzAuMDMsMC4xNywwLjA0LDAuMzksMC4wNCwwLjY0CgljMCwwLjQ4LTAuMDYsMC44My0wLjE4LDEuMDRzLTAuMjgsMC4zMi0wLjQ4LDAuMzJoLTQuNjJ2MTAuMDVjMCwxLjI0LDAuMTgsMi4xOCwwLjU1LDIuODFjMC4zNywwLjYzLDEuMDIsMC45NSwxLjk3LDAuOTUKCWMwLjMxLDAsMC41OC0wLjAzLDAuODItMC4wOXMwLjQ1LTAuMTIsMC42NC0wLjE5czAuMzUtMC4xMywwLjQ4LTAuMTljMC4xMy0wLjA2LDAuMjUtMC4wOSwwLjM2LTAuMDljMC4wNywwLDAuMTMsMC4wMiwwLjE5LDAuMDUKCWMwLjA2LDAuMDMsMC4xMSwwLjEsMC4xNCwwLjE5YzAuMDMsMC4wOSwwLjA2LDAuMjIsMC4wOSwwLjM4QzExMS4xLDM5LjE0LDExMS4xMSwzOS4zNCwxMTEuMTEsMzkuNTh6Ii8+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xMzAuNDEsMzEuMTFjMCwwLjUyLTAuMTMsMC44OS0wLjM5LDEuMTFjLTAuMjYsMC4yMi0wLjU2LDAuMzMtMC45LDAuMzNoLTExLjg3YzAsMSwwLjEsMS45LDAuMywyLjcKCXMwLjU0LDEuNDksMS4wMSwyLjA2czEuMDgsMS4wMSwxLjgzLDEuMzJzMS42NywwLjQ2LDIuNzYsMC40NmMwLjg2LDAsMS42Mi0wLjA3LDIuMjktMC4yMXMxLjI1LTAuMywxLjc0LTAuNDdzMC44OS0wLjMzLDEuMjEtMC40NwoJczAuNTUtMC4yMSwwLjcxLTAuMjFjMC4wOSwwLDAuMTgsMC4wMiwwLjI1LDAuMDdzMC4xMywwLjEyLDAuMTcsMC4yMWMwLjA0LDAuMDksMC4wNywwLjIyLDAuMDksMC4zOXMwLjAzLDAuMzcsMC4wMywwLjYxCgljMCwwLjE3LTAuMDEsMC4zMi0wLjAyLDAuNDVjLTAuMDEsMC4xMy0wLjAzLDAuMjQtMC4wNSwwLjM0Yy0wLjAyLDAuMS0wLjA1LDAuMTktMC4xLDAuMjdjLTAuMDUsMC4wOC0wLjExLDAuMTYtMC4xOCwwLjIzCgljLTAuMDcsMC4wNy0wLjI5LDAuMTktMC42NSwwLjM2Yy0wLjM2LDAuMTctMC44MywwLjMzLTEuNCwwLjQ5Yy0wLjU3LDAuMTYtMS4yNCwwLjMtMS45OSwwLjQzcy0xLjU2LDAuMTktMi40MSwwLjE5CgljLTEuNDgsMC0yLjc4LTAuMjEtMy44OS0wLjYycy0yLjA1LTEuMDMtMi44MS0xLjg0cy0xLjMzLTEuODMtMS43Mi0zLjA2Yy0wLjM5LTEuMjMtMC41OC0yLjY2LTAuNTgtNC4yOGMwLTEuNTUsMC4yLTIuOTQsMC42LTQuMTcKCWMwLjQtMS4yMywwLjk4LTIuMjgsMS43My0zLjE0czEuNjYtMS41MiwyLjczLTEuOThjMS4wNy0wLjQ2LDIuMjYtMC42OSwzLjU4LTAuNjljMS40MiwwLDIuNjIsMC4yMywzLjYxLDAuNjhzMS44MSwxLjA2LDIuNDUsMS44MwoJYzAuNjQsMC43NywxLjExLDEuNjcsMS40MSwyLjdjMC4zLDEuMDMsMC40NSwyLjE0LDAuNDUsMy4zMXYwLjZIMTMwLjQxeiBNMTI3LjA5LDMwLjEzYzAuMDQtMS43My0wLjM1LTMuMS0xLjE2LTQuMDgKCWMtMC44MS0wLjk5LTIuMDItMS40OC0zLjYyLTEuNDhjLTAuODIsMC0xLjU0LDAuMTUtMi4xNiwwLjQ2Yy0wLjYyLDAuMzEtMS4xMywwLjcxLTEuNTUsMS4yMmMtMC40MiwwLjUxLTAuNzQsMS4xLTAuOTcsMS43NwoJcy0wLjM2LDEuMzgtMC4zOCwyLjExSDEyNy4wOXoiLz4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTE0NS45NywyNC4wNmMwLDAuMjktMC4wMSwwLjU0LTAuMDIsMC43NHMtMC4wNCwwLjM2LTAuMDgsMC40N2MtMC4wNCwwLjExLTAuMDksMC4yLTAuMTUsMC4yNgoJYy0wLjA2LDAuMDYtMC4xNCwwLjA5LTAuMjUsMC4wOWMtMC4xMSwwLTAuMjQtMC4wMy0wLjM5LTAuMDlzLTAuMzMtMC4xMi0wLjUyLTAuMThzLTAuNDEtMC4xMi0wLjY1LTAuMTdzLTAuNS0wLjA4LTAuNzgtMC4wOAoJYy0wLjMzLDAtMC42NiwwLjA3LTAuOTgsMC4yYy0wLjMyLDAuMTMtMC42NiwwLjM1LTEuMDEsMC42NnMtMC43MiwwLjcxLTEuMTEsMS4yMmMtMC4zOSwwLjUxLTAuODEsMS4xMy0xLjI4LDEuODZ2MTEuODUKCWMwLDAuMTEtMC4wMywwLjItMC4wOCwwLjI3Yy0wLjA1LDAuMDctMC4xNCwwLjE0LTAuMjYsMC4xOXMtMC4yOSwwLjA5LTAuNSwwLjEyYy0wLjIxLDAuMDMtMC40OSwwLjA0LTAuODIsMC4wNAoJYy0wLjMyLDAtMC41OS0wLjAxLTAuOC0wLjA0Yy0wLjIxLTAuMDMtMC4zOC0wLjA3LTAuNTEtMC4xMmMtMC4xMy0wLjA1LTAuMjEtMC4xMi0wLjI2LTAuMTlzLTAuMDctMC4xNi0wLjA3LTAuMjdWMjIuODgKCWMwLTAuMTEsMC4wMi0wLjIsMC4wNi0wLjI3YzAuMDQtMC4wNywwLjEyLTAuMTQsMC4yNC0wLjJjMC4xMi0wLjA2LDAuMjctMC4xLDAuNDYtMC4xMnMwLjQzLTAuMDMsMC43NC0wLjAzCgljMC4yOSwwLDAuNTQsMC4wMSwwLjczLDAuMDNzMC4zNCwwLjA2LDAuNDUsMC4xMmMwLjExLDAuMDYsMC4xOCwwLjEzLDAuMjMsMC4yYzAuMDUsMC4wNywwLjA3LDAuMTYsMC4wNywwLjI3djIuNjIKCWMwLjQ5LTAuNzIsMC45Ni0xLjMxLDEuMzktMS43NmMwLjQzLTAuNDUsMC44NC0wLjgxLDEuMjMtMS4wN2MwLjM5LTAuMjYsMC43Ny0wLjQ0LDEuMTUtMC41NGMwLjM4LTAuMSwwLjc2LTAuMTUsMS4xNS0wLjE1CgljMC4xNywwLDAuMzcsMC4wMSwwLjU5LDAuMDNzMC40NSwwLjA2LDAuNjksMC4xMWMwLjI0LDAuMDUsMC40NiwwLjExLDAuNjUsMC4xOGMwLjE5LDAuMDcsMC4zMywwLjEzLDAuNDEsMC4yCgljMC4wOCwwLjA3LDAuMTMsMC4xMywwLjE2LDAuMTljMC4wMywwLjA2LDAuMDUsMC4xNCwwLjA3LDAuMjNjMC4wMiwwLjA5LDAuMDMsMC4yMywwLjA0LDAuNDEKCUMxNDUuOTYsMjMuNTEsMTQ1Ljk3LDIzLjc2LDE0NS45NywyNC4wNnoiLz4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTE2Mi40Niw0MC45MmMwLDAuMTYtMC4wNSwwLjI4LTAuMTYsMC4zNmMtMC4xMSwwLjA4LTAuMjUsMC4xNC0wLjQ0LDAuMThzLTAuNDYsMC4wNi0wLjgyLDAuMDYKCWMtMC4zNSwwLTAuNjItMC4wMi0wLjgzLTAuMDZjLTAuMjEtMC4wNC0wLjM2LTAuMS0wLjQ1LTAuMThzLTAuMTQtMC4yLTAuMTQtMC4zNnYtMS44Yy0wLjc5LDAuODQtMS42NywxLjUtMi42MywxLjk2CgljLTAuOTcsMC40Ny0xLjk5LDAuNy0zLjA3LDAuN2MtMC45NSwwLTEuODEtMC4xMi0yLjU3LTAuMzdjLTAuNzctMC4yNS0xLjQyLTAuNi0xLjk2LTEuMDdjLTAuNTQtMC40Ny0wLjk2LTEuMDQtMS4yNi0xLjcyCgljLTAuMy0wLjY4LTAuNDUtMS40NS0wLjQ1LTIuMzJjMC0xLjAxLDAuMjEtMS45LDAuNjItMi42NGMwLjQxLTAuNzUsMS4wMS0xLjM3LDEuNzgtMS44NmMwLjc3LTAuNDksMS43Mi0wLjg2LDIuODQtMS4xMQoJczIuMzgtMC4zNywzLjc4LTAuMzdoMi40OHYtMS40YzAtMC42OS0wLjA3LTEuMzEtMC4yMi0xLjg0cy0wLjM4LTAuOTgtMC43MS0xLjMzYy0wLjMzLTAuMzUtMC43NS0wLjYyLTEuMjctMC44CgljLTAuNTItMC4xOC0xLjE2LTAuMjctMS45Mi0wLjI3Yy0wLjgxLDAtMS41NCwwLjEtMi4xOSwwLjI5Yy0wLjY1LDAuMTktMS4yMSwwLjQxLTEuNywwLjY0Yy0wLjQ5LDAuMjMtMC44OSwwLjQ1LTEuMjIsMC42NAoJYy0wLjMzLDAuMTktMC41NywwLjI5LTAuNzMsMC4yOWMtMC4xMSwwLTAuMi0wLjAzLTAuMjgtMC4wOGMtMC4wOC0wLjA1LTAuMTUtMC4xMy0wLjIxLTAuMjRzLTAuMS0wLjI0LTAuMTMtMC40MQoJcy0wLjA0LTAuMzUtMC4wNC0wLjU1YzAtMC4zMywwLjAyLTAuNiwwLjA3LTAuNzljMC4wNS0wLjE5LDAuMTYtMC4zOCwwLjM0LTAuNTVzMC40OS0wLjM4LDAuOTMtMC42MWMwLjQ0LTAuMjMsMC45NS0wLjQ1LDEuNTItMC42NAoJczEuMi0wLjM1LDEuODgtMC40OGMwLjY4LTAuMTMsMS4zNy0wLjE5LDIuMDYtMC4xOWMxLjI5LDAsMi40LDAuMTUsMy4zLDAuNDRjMC45MSwwLjI5LDEuNjQsMC43MiwyLjIsMS4yOXMwLjk3LDEuMjcsMS4yMiwyLjExCglzMC4zOCwxLjgyLDAuMzgsMi45NEwxNjIuNDYsNDAuOTJMMTYyLjQ2LDQwLjkyeiBNMTU5LjE4LDMyLjY5aC0yLjgyYy0wLjkxLDAtMS43LDAuMDgtMi4zNiwwLjIzYy0wLjY3LDAuMTUtMS4yMiwwLjM4LTEuNjYsMC42OAoJcy0wLjc2LDAuNjYtMC45NywxLjA4Yy0wLjIxLDAuNDItMC4zMSwwLjktMC4zMSwxLjQ1YzAsMC45MywwLjMsMS42OCwwLjg5LDIuMjNzMS40MiwwLjgzLDIuNDksMC44M2MwLjg3LDAsMS42Ny0wLjIyLDIuNDEtMC42NgoJYzAuNzQtMC40NCwxLjUyLTEuMTEsMi4zMy0yLjAyVjMyLjY5eiIvPgo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTgxLjQ4LDM4LjE2YzAsMC4yMy0wLjAxLDAuNDItMC4wMiwwLjU5Yy0wLjAxLDAuMTctMC4wNCwwLjMxLTAuMDcsMC40MmMtMC4wMywwLjExLTAuMDcsMC4yMS0wLjEyLDAuMwoJYy0wLjA1LDAuMDktMC4xNSwwLjIxLTAuMzIsMC4zOGMtMC4xNywwLjE3LTAuNDUsMC4zNy0wLjg1LDAuNjJjLTAuNCwwLjI1LTAuODUsMC40Ny0xLjM1LDAuNjZzLTEuMDQsMC4zNS0xLjYzLDAuNDcKCWMtMC41OSwwLjEyLTEuMTksMC4xOC0xLjgyLDAuMThjLTEuMjksMC0yLjQ0LTAuMjEtMy40NC0wLjY0cy0xLjg0LTEuMDUtMi41MS0xLjg3Yy0wLjY3LTAuODItMS4xOS0xLjgzLTEuNTQtMy4wMgoJYy0wLjM1LTEuMTktMC41My0yLjU3LTAuNTMtNC4xM2MwLTEuNzcsMC4yMi0zLjMsMC42NS00LjU3czEuMDMtMi4zMiwxLjc4LTMuMTNzMS42NC0xLjQyLDIuNjYtMS44MWMxLjAyLTAuMzksMi4xMi0wLjU5LDMuMzEtMC41OQoJYzAuNTcsMCwxLjEzLDAuMDUsMS42NywwLjE2YzAuNTQsMC4xMSwxLjA0LDAuMjUsMS40OSwwLjQyczAuODYsMC4zNywxLjIxLDAuNnMwLjYxLDAuNDIsMC43NywwLjU4czAuMjcsMC4yOSwwLjMzLDAuMzgKCXMwLjExLDAuMiwwLjE1LDAuMzNjMC4wNCwwLjEzLDAuMDcsMC4yNywwLjA4LDAuNDNjMC4wMSwwLjE2LDAuMDIsMC4zNiwwLjAyLDAuNmMwLDAuNTItMC4wNiwwLjg4LTAuMTgsMS4wOXMtMC4yNywwLjMxLTAuNDQsMC4zMQoJYy0wLjIsMC0wLjQzLTAuMTEtMC42OS0wLjMzcy0wLjU5LTAuNDYtMC45OS0wLjczYy0wLjQtMC4yNy0wLjg4LTAuNTEtMS40NS0wLjczYy0wLjU3LTAuMjItMS4yNC0wLjMzLTIuMDEtMC4zMwoJYy0xLjU5LDAtMi44MSwwLjYxLTMuNjUsMS44M2MtMC44NSwxLjIyLTEuMjcsMi45OS0xLjI3LDUuMzFjMCwxLjE2LDAuMTEsMi4xOCwwLjMzLDMuMDVzMC41NCwxLjYsMC45NywyLjE5CgljMC40MywwLjU5LDAuOTUsMS4wMiwxLjU3LDEuMzFzMS4zMywwLjQzLDIuMTMsMC40M2MwLjc2LDAsMS40My0wLjEyLDItMC4zNnMxLjA3LTAuNSwxLjQ5LTAuNzljMC40Mi0wLjI5LDAuNzctMC41NSwxLjA2LTAuNzgKCWMwLjI5LTAuMjMsMC41MS0wLjM1LDAuNjctMC4zNWMwLjA5LDAsMC4xNywwLjAzLDAuMjQsMC4wOHMwLjEyLDAuMTQsMC4xNywwLjI3YzAuMDUsMC4xMywwLjA4LDAuMjksMC4xLDAuNDgKCUMxODEuNDcsMzcuNjYsMTgxLjQ4LDM3Ljg5LDE4MS40OCwzOC4xNnoiLz4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTE5NS4yMywzOS41OGMwLDAuMzktMC4wMywwLjY5LTAuMDgsMC45MmMtMC4wNSwwLjIzLTAuMTMsMC4zOS0wLjI0LDAuNWMtMC4xMSwwLjExLTAuMjcsMC4yMS0wLjQ4LDAuMwoJYy0wLjIxLDAuMDktMC40NiwwLjE3LTAuNzMsMC4yM2MtMC4yNywwLjA2LTAuNTYsMC4xMS0wLjg3LDAuMTVzLTAuNjEsMC4wNi0wLjkyLDAuMDZjLTAuOTMsMC0xLjc0LTAuMTItMi40LTAuMzcKCWMtMC42Ny0wLjI1LTEuMjEtMC42Mi0xLjY0LTEuMTJzLTAuNzQtMS4xMy0wLjkzLTEuOWMtMC4xOS0wLjc3LTAuMjktMS42Ny0wLjI5LTIuNzJWMjUuMWgtMi41MmMtMC4yLDAtMC4zNi0wLjExLTAuNDgtMC4zMgoJcy0wLjE4LTAuNTYtMC4xOC0xLjA0YzAtMC4yNSwwLjAyLTAuNDcsMC4wNS0wLjY0czAuMDgtMC4zMiwwLjEzLTAuNDNjMC4wNS0wLjExLDAuMTItMC4xOSwwLjIxLTAuMjQKCWMwLjA5LTAuMDUsMC4xOC0wLjA3LDAuMjktMC4wN2gyLjV2LTQuMjhjMC0wLjA5LDAuMDItMC4xOCwwLjA3LTAuMjZjMC4wNS0wLjA4LDAuMTMtMC4xNSwwLjI2LTAuMjFjMC4xMy0wLjA2LDAuMy0wLjEsMC41MS0wLjEzCgljMC4yMS0wLjAzLDAuNDgtMC4wNCwwLjgtMC4wNGMwLjMzLDAsMC42MSwwLjAxLDAuODIsMC4wNGMwLjIxLDAuMDMsMC4zOCwwLjA3LDAuNSwwLjEzczAuMjEsMC4xMywwLjI2LDAuMjEKCWMwLjA1LDAuMDgsMC4wOCwwLjE3LDAuMDgsMC4yNnY0LjI4aDQuNjJjMC4xMSwwLDAuMiwwLjAyLDAuMjgsMC4wN2MwLjA4LDAuMDUsMC4xNSwwLjEzLDAuMjEsMC4yNGMwLjA2LDAuMTEsMC4xLDAuMjYsMC4xMywwLjQzCgljMC4wMywwLjE3LDAuMDQsMC4zOSwwLjA0LDAuNjRjMCwwLjQ4LTAuMDYsMC44My0wLjE4LDEuMDRzLTAuMjgsMC4zMi0wLjQ4LDAuMzJoLTQuNjJ2MTAuMDVjMCwxLjI0LDAuMTgsMi4xOCwwLjU1LDIuODEKCXMxLjAyLDAuOTUsMS45NywwLjk1YzAuMzEsMCwwLjU4LTAuMDMsMC44Mi0wLjA5YzAuMjQtMC4wNiwwLjQ1LTAuMTIsMC42NC0wLjE5czAuMzUtMC4xMywwLjQ4LTAuMTljMC4xMy0wLjA2LDAuMjUtMC4wOSwwLjM2LTAuMDkKCWMwLjA3LDAsMC4xMywwLjAyLDAuMTksMC4wNWMwLjA2LDAuMDMsMC4xMSwwLjEsMC4xNCwwLjE5YzAuMDMsMC4wOSwwLjA2LDAuMjIsMC4wOSwwLjM4QzE5NS4yMiwzOS4xNCwxOTUuMjMsMzkuMzQsMTk1LjIzLDM5LjU4eiIKCS8+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0yMTUuNTUsMjkuODdjMCwwLjI1LTAuMDIsMC40Ny0wLjA2LDAuNjVjLTAuMDQsMC4xOC0wLjA5LDAuMzMtMC4xNiwwLjQ1Yy0wLjA3LDAuMTItMC4xNSwwLjIxLTAuMjQsMC4yNgoJcy0wLjE5LDAuMDgtMC4zLDAuMDhoLTYuNTd2Ny4yOWMwLDAuMTEtMC4wMywwLjItMC4wOCwwLjI3Yy0wLjA1LDAuMDctMC4xNCwwLjE0LTAuMjUsMC4xOWMtMC4xMSwwLjA1LTAuMjYsMC4xLTAuNDUsMC4xMwoJcy0wLjQyLDAuMDUtMC43LDAuMDVjLTAuMjcsMC0wLjUtMC4wMi0wLjY5LTAuMDVjLTAuMTktMC4wMy0wLjM1LTAuMDgtMC40Ni0wLjEzYy0wLjExLTAuMDUtMC4yLTAuMTItMC4yNS0wLjE5CgljLTAuMDUtMC4wNy0wLjA4LTAuMTYtMC4wOC0wLjI3di03LjI5aC02LjU3Yy0wLjEyLDAtMC4yMi0wLjAzLTAuMzEtMC4wOHMtMC4xNi0wLjE0LTAuMjMtMC4yNnMtMC4xMi0wLjI3LTAuMTYtMC40NQoJYy0wLjA0LTAuMTgtMC4wNi0wLjQtMC4wNi0wLjY1YzAtMC4yNCwwLjAyLTAuNDUsMC4wNi0wLjYzYzAuMDQtMC4xOCwwLjA5LTAuMzMsMC4xNi0wLjQ1czAuMTQtMC4yMSwwLjIzLTAuMjZzMC4xOC0wLjA4LDAuMjktMC4wOAoJaDYuNTl2LTcuMjljMC0wLjExLDAuMDMtMC4yLDAuMDgtMC4yOWMwLjA1LTAuMDksMC4xNC0wLjE2LDAuMjUtMC4yMmMwLjExLTAuMDYsMC4yNy0wLjExLDAuNDYtMC4xNHMwLjQyLTAuMDUsMC42OS0wLjA1CgljMC4yOCwwLDAuNTEsMC4wMiwwLjcsMC4wNXMwLjM0LDAuMDgsMC40NSwwLjE0YzAuMTEsMC4wNiwwLjIsMC4xMywwLjI1LDAuMjJjMC4wNSwwLjA5LDAuMDgsMC4xOCwwLjA4LDAuMjl2Ny4yOWg2LjU5CgljMC4xMSwwLDAuMiwwLjAzLDAuMjksMC4wOGMwLjA5LDAuMDUsMC4xNywwLjE0LDAuMjQsMC4yNmMwLjA3LDAuMTIsMC4xMywwLjI3LDAuMTYsMC40NUMyMTUuNTQsMjkuNDIsMjE1LjU1LDI5LjYzLDIxNS41NSwyOS44N3oiCgkvPgo8cGF0aCBpZD0iSWNvbl9hd2Vzb21lLWNoZWNrLWNpcmNsZV8wMDAwMDA0Nzc1MTgxOTk3Mzg0ODk2MDEwMDAwMDAxMTQ1MzEwODQ2NjkxOTY2MjQ5OV8iIGNsYXNzPSJzdDAiIGQ9Ik01OC4yMSwyOS4xCgljMCwxNi4wNy0xMy4wMywyOS4xMS0yOS4xLDI5LjExUzAsNDUuMTgsMCwyOS4xMVMxMy4wMywwLDI5LjEsMGwwLDBDNDUuMTcsMCw1OC4yLDEzLjAzLDU4LjIxLDI5LjF6IE0yNS43NCw0NC41MWwyMS41OS0yMS41OQoJYzAuNzMtMC43MywwLjczLTEuOTIsMC0yLjY1bC0yLjY1LTIuNjVjLTAuNzMtMC43My0xLjkyLTAuNzMtMi42NSwwbC0xNy42MiwxNy42TDE2LjE5LDI3Yy0wLjczLTAuNzMtMS45Mi0wLjczLTIuNjUsMGwtMi42NiwyLjY2CgljLTAuNzMsMC43My0wLjczLDEuOTIsMCwyLjY1bDEyLjIsMTIuMkMyMy44MSw0NS4yNSwyNSw0NS4yNSwyNS43NCw0NC41MUwyNS43NCw0NC41MXoiLz4KPC9zdmc+Cg=='/>
                </div>
                    
                <div class='summary-table'>
                        <h2 class='summary-step-title' colspan='2'>Dialogue step 1</h2>
                    <div class='summary-element-class'>
                            <div class='summary-element-class-label'>Utfyllingsfelt1</div> 
                    
                        <div class='summary-element-instance front-hidden'><span style=""white-space: pre-wrap;"">dsa</span></div>
                        <div class='summary-element-instance pdf-hidden'><span style="white-space: pre-wrap;">dsa</span></div>
                        </div>
                
            
                </div>
             
                    
            </div>  
            

style.html:

<style>
    * {
        font-family: sans-serif;
        -webkit-filter: opacity(1);
        -webkit-print-color-adjust: exact;
        print-color-adjust: exact;
    }

    body {
    }

    .pdf-hidden {
        display: none !important;
    }

    .summary-container {
        display: flex;
        flex-direction: column;
        justify-content: center;
    }

    .summary-header-container {
        display: flex;
        gap: 1rem;
        margin-bottom: 1rem;
        justify-content: space-between;
        align-items: flex-start;
    }

    .summary-title {
        margin-top: 0;
    }

    .summary-logo {
        max-width: 200px;
    }

    h1 {
        font-size: 1.5rem;
    }

    .summary-reference-id-label,
    .summary-reference-id-value,
    .summary-submitted-label,
    .summary-submitted-value,
    .summary-dialogueid-label,
    .summary-dialogueid-value {
        font-size: 1rem;
        display: inline-flex;
        font-weight: normal;
        margin: 0;
    }

    .summary-reference-id-label,
    .summary-submitted-label,
    .summary-dialogueid-label {
        font-weight: bold;
    }

    .errorlabel {
        color: #f44336;
    }

    caption {
        margin: 1rem 0;
    }

    .summary-table {
        width: 100%;
        margin: 0 auto;
    }


    .summary-step-title {
        font-size: 1.2rem;
        padding: 1rem 0;
        text-align: left;
        margin: 1rem 0 0.5rem;
    }

    span {
        white-space: pre-wrap;
    }

    .summary-element-group-class-label {
        font-weight: bold;
        padding: 10px 0;
        font-size: 1.2rem;
    }

    .summary-group-instance {
        display: flex;
        justify-content: space-between;
        flex-wrap: wrap;
        border-bottom: 1px solid black;
        padding-bottom: 5px;
        margin-bottom: 5px;
    }

    .summary-element-class {
        display: flex;
        width: 100%;
        gap: 1rem;
        font-size: 1.1rem;
    }

    .summary-element-class-label {
        width: 40%;
        font-weight: bold;
        padding: 5px 0;
        text-align: left;
    }

    .summary-element-instance {
        width: 60%;
        padding: 5px 0;
    }

    .summary-element-instance-multiple {
        padding: 5px 0;
    }

    .summary-element-instance-radiobutton-list,
    .summary-element-instance-checkbox-list {
        display: flex;
        flex-direction: column;
        gap: 0.25rem;
    }

    .summary-element-instance-radiobutton-list-item,
    .summary-element-instance-checkbox-list-item {
        display: grid;
        grid-template-columns: 1em auto;
        gap: 0.5em;
        align-items: center;
    }

    .radio-button-item {
        display: grid !important;
        grid-template-columns: 1em auto;
        gap: 0.5em;
        align-items: center;
    }

    input[type="radio"] {
        /* Add if not using autoprefixer */
        -webkit-appearance: none;
        /* Remove most all native input styles */
        appearance: none;
        /* For iOS < 15 */
        background-color: #fff;
        /* Not removed via appearance */
        margin: 0;
        font: inherit;
        color: currentColor;
        width: 18px;
        height: 18px;
        border: 0.15em solid currentColor;
        border-radius: 50%;
        transform: translateY(-0.075em);
        display: grid;
        place-content: center;
    }

        input[type="radio"]::before {
            content: "";
            width: 8px;
            height: 8px;
            border-radius: 50%;
            transition: 120ms transform ease-in-out;
            box-shadow: inset 1em 1em #000;
            /* Windows High Contrast Mode */
            background-color: CanvasText;
        }

        input[type="radio"]:checked::before {
            transform: scale(1);
        }

        input[type="radio"]:focus-visible {
            outline: max(2px, 0.15em) solid currentColor;
            outline-offset: max(2px, 0.15em);
        }

    input[type="checkbox"] {
        position: absolute;
        width: 24px;
        height: 24px;
        appearance: none;
        margin: 0;
        opacity: 1;
    }

        input[type="checkbox"] + span {
            position: relative;
            height: 14px;
            line-height: 14px;
            padding: 0 0 0.25rem 2.5rem !important;
            font-weight: normal !important;
            display: inline-block;
        }

            input[type="checkbox"] + span:before {
                content: "";
                position: absolute;
                top: 0;
                left: 0;
                height: 14px;
                width: 14px;
                border: 2px solid;
                border-radius: 50%;
                background-color: #fff;
            }

        input[type="checkbox"]:checked + span:before {
            display: block;
            background-color: currentColor;
            box-shadow: inset 0 0 0 0.25em #fff;
        }

        input[type="checkbox"] + span:before {
            border-radius: 0;
        }

        input[type="checkbox"] + span:after {
            content: "";
            position: absolute;
            left: 6px;
            top: 41%;
            width: 4px;
            height: 8px;
            border-style: solid;
            border-width: 0px 2px 2px 0px;
            transform: translateY(-50%) rotate(45deg);
            display: none;
        }

        input[type="checkbox"]:checked + span:after {
            display: block;
        }

        input[type="checkbox"]:checked + span:before {
            background-color: #fff;
            box-shadow: none;
        }
</style>

An item with the same key has already been added. Key: Gotenberg-Webhook-Extra-Http-Headers

After updating to v2 it seems impossible to set multiple request headers to the webhook request.

usage:

reqBuilder.AddWebhook(hook =>
  {
      hook.SetUrl(gotenbergWebHookUrl, HttpMethod.Post)
          .SetErrorUrl(gotenbergErrorUrl, HttpMethod.Post)
          .AddRequestHeader("JobId", jobId)
          .AddRequestHeader("DocumentId", documentId)
          .AddRequestHeader("ContentViewType", Enum.GetName(contentViewType));
  })

exception:

System.ArgumentException: An item with the same key has already been added. Key: Gotenberg-Webhook-Extra-Http-Headers
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at Gotenberg.Sharp.API.Client.Domain.Builders.Faceted.WebhookBuilder.AddRequestHeader(String name, IEnumerable`1 values)
   at Gotenberg.Sharp.API.Client.Domain.Builders.Faceted.WebhookBuilder.AddRequestHeader(String name, String value)

It seems like the method AddRequestHeader should either accept an IDictionary<string, string> / IDictionary<string, IEnumerable<string>> or handle multiple calls of the method correctly. I didnt deep dive into the differences between v1 and v2 and not into differences between Gotenberg v6 and v7, but on the first view it seems to be a bug in GotenbergSharpApiClient. Or am I wrong?

How to pass in Retry Option Settings when not using Dependency Injection (DI)?

I have the following Set-up

AppSettings

  "GotenbergSharpClient": {
    "ServiceUrl": "http://localhost:3000",
    "HealthCheckUrl": "http://localhost:3000/ping",
    "RetryPolicy": {
      "Enabled": true,
      "RetryCount": 4,
      "BackoffPower": 1.5,
      "LoggingEnabled": true
    }
  }
public void ConfigureServices(IServiceCollection services)
{
	.....
    services.AddOptions<GotenbergSharpClientOptions>()
	        .Bind(Configuration.GetSection(nameof(GotenbergSharpClient)));
    services.AddGotenbergSharpClient();
	.....    
}

Question 1:

I notice when I have this

var test = Configuration.GetSection(nameof(GotenbergSharpClient));

It is always null?

So not sure if the App settings is getting passed on to the GotenbergSharpClient or not?

Question 2:

When newing up the GotenbergSharpClient

_Instance = new GotenbergSharpClient(Settings.Current.PDFServer);

How does the Retry options get passed to it?

"RetryPolicy": {
      "Enabled": true,
      "RetryCount": 4,
      "BackoffPower": 1.5,
      "LoggingEnabled": true
    }

Not 100% sure when newing it up if the Retry Policies gets passed in to it?

I am thinking and hoping by looking at this

public static IAsyncPolicy<HttpResponseMessage> CreatePolicyFromSettings(IServiceProvider sp,
            HttpRequestMessage request)

it seems like the policy is created on HttpResponseMessage and it gets the settings from the service provider and since that
is provided in Startup.cs that the retry settings should be passed in regardless of how the Client is used?

Question 3:

How do I check to make sure that the retry settings are being passed into the Gotenberg Service correctly?

Question 4:

What is the best way to Unit test Gotenberg Server Errors which results in HttpResonse errors, to trigger the retry policy to kick in?

Thanks 👍

Breaking change in .NET 6 causes this client to throw exception because of Globalization issues

In .Net 6 it is know thrown exception if invariant culture is enabled, but the app is creating a specific culture.

E.g. see this:
https://docs.microsoft.com/en-us/dotnet/core/compatibility/globalization/6.0/culture-creation-invariant-mode

Below is a line from your code where a specific culture info is used. Maybe just set it to Cultureinfo.InvariantCulture?

var cultureInfo = CultureInfo.GetCultureInfo("en-US");

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.