我已遵循本教程:https://www.freecodecamp.org/news/convert-html-to-pdf-with-azure-functions-and-wkhtmltopdf/
解决方案是 Azure C# Function App,它将 HTML 转换为 PDF 文档,并将该 PDF 保存在 Blob 存储中。这个特定的函数应用程序接受以下内容,我在通过 Postman 在本地运行时通过请求正文传递这些内容:
{
"PDFFileName": "test.pdf",
"HtmlContent": "<html>\n <head><meta http-equiv=Content-Type content=\"text/html; charset=UTF-8\">\n </head>\n <body>\n <div>test</div>\n </body> \n </html>"
}
本教程使用此库:https://github.com/rdvojmoc/DinkToPdf
发生的事情有点难以调试。我在本地运行函数应用程序,它运行良好,并且 PDF 保存在我的 blob 存储中,没有任何问题,但运行时我确实在控制台中看到以下错误。
Qt: Could not initialize OLE (error 80010106)
然后,当我再次运行此命令时(甚至删除 pdf,以确保那里没有冲突),应用程序只是挂起,直到它返回 503 或我取消请求。
追踪代码,正是这一行崩溃了(来自下面的代码):
var PDFByteArray = BuildPdf(Request.HtmlContent, new MarginSettings(2, 2, 2, 2));
直接在门户中发布和运行时(测试+代码并在逻辑应用程序流程中运行),我观察到相同的行为。
我的应用程序服务计划是基本 (B1: 1)。任何解决此问题的帮助将不胜感激!
完整代码如下:
using System;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using DinkToPdf;
using IPdfConverter = DinkToPdf.Contracts.IConverter;
[assembly: FunctionsStartup(typeof(pdfCreation.Startup))]
namespace pdfCreation
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton(typeof(IPdfConverter), new SynchronizedConverter(new PdfTools()));
}
}
public class Html2Pdf
{
// Read more about converter on: https://github.com/rdvojmoc/DinkToPdf
// For our purposes we are going to use SynchronizedConverter
IPdfConverter pdfConverter = new SynchronizedConverter(new PdfTools());
// A function to convert html content to pdf based on the configuration passed as arguments
// Arguments:
// HtmlContent: the html content to be converted
// Margins: the margis around the content
// DPI: The dpi is very important when you want to print the pdf.
// Returns a byte array of the pdf which can be stored as a file
private byte[] BuildPdf(string HtmlContent, MarginSettings Margins, int? DPI = 180)
{
// Call the Convert method of SynchronizedConverter "pdfConverter"
return pdfConverter.Convert(new HtmlToPdfDocument()
{
// Set the html content
Objects =
{
new ObjectSettings
{
HtmlContent = HtmlContent
}
},
// Set the configurations
GlobalSettings = new GlobalSettings
{
PaperSize = PaperKind.A4,
DPI = DPI,
Margins = Margins
}
});
}
private string ConnectionString(ILogger log, ExecutionContext context)
{
var config = new ConfigurationBuilder()
.SetBasePath(context.FunctionAppDirectory)
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
var connString = config.GetConnectionString("MyConnectionString");
if(connString == null){log.LogInformation("Connection String is null");}
return connString;
}
// The first arugment tells that the functions can be triggerd by a POST HTTP request.
// The second argument is mainly used for logging information, warnings or errors
[FunctionName("Html2Pdf")]
[STAThread]
public async Task<string> Run([HttpTrigger(AuthorizationLevel.Function, "POST")] Html2PdfRequest Request, ILogger Log, ExecutionContext context)
{
Log.LogInformation("C# HTTP trigger function started for HTML2PDF.");
Log.LogInformation(Request.HtmlContent);
// PDFByteArray is a byte array of pdf generated from the HtmlContent
var PDFByteArray = BuildPdf(Request.HtmlContent, new MarginSettings(2, 2, 2, 2));
// The connection string of the Storage Account to which our PDF file will be uploaded
var StorageConnectionString = ConnectionString(Log, context);
// Generate an instance of CloudStorageAccount by parsing the connection string
var StorageAccount = CloudStorageAccount.Parse(StorageConnectionString);
// Create an instance of CloudBlobClient to connect to our storage account
CloudBlobClient BlobClient = StorageAccount.CreateCloudBlobClient();
// Get the instance of CloudBlobContainer which points to a container name "pdf"
// Replace your own container name
CloudBlobContainer BlobContainer = BlobClient.GetContainerReference("pdf");
// Get the instance of the CloudBlockBlob to which the PDFByteArray will be uploaded
CloudBlockBlob Blob = BlobContainer.GetBlockBlobReference(Request.PDFFileName);
Log.LogInformation("Attempting to upload " + Request.PDFFileName);
// Upload the pdf blob
await Blob.UploadFromByteArrayAsync(PDFByteArray, 0, PDFByteArray.Length);
return "Created PDF Packing Slip.";
}
}
}
和
namespace pdfCreation
{
public class Html2PdfRequest
{
// The HTML content that needs to be converted.
public string HtmlContent { get; set; }
// The name of the PDF file to be generated
public string PDFFileName { get; set; }
}
}
.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DinkToPdf" Version="1.0.8" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.16" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.16" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.13" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="4.5.0-preview1-25914-04" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
<None Update="libwkhtmltox.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</None>
<None Update="libwkhtmltox.dylib">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="libwkhtmltox.so">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="wwwroot\**\*">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
好的,所以我似乎已经解决了这个问题。我仍然收到“Qt:无法初始化 OLE(错误 80010106)”错误消息,但我可以连续使用 API 并继续生成 PDF,这样问题就解决了。
如果有人遇到此问题并遇到问题,那么我的解决方案的代码如下:
.csproj 文件:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DinkToPdf" Version="1.0.8" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.13" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.13" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.13" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="4.5.0-preview1-25914-04" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
<None Update="libwkhtmltox.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</None>
<None Update="libwkhtmltox.dylib">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="libwkhtmltox.so">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="wwwroot\**\*">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
功能应用程序文件:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using DinkToPdf;
using IPdfConverter = DinkToPdf.Contracts.IConverter;
[assembly: FunctionsStartup(typeof(pdfCreation.Startup))]
namespace pdfCreation
{
public class Startup : FunctionsStartup
{
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
FunctionsHostBuilderContext context = builder.GetContext();
builder.ConfigurationBuilder.AddEnvironmentVariables();
}
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton(typeof(IPdfConverter), new SynchronizedConverter(new PdfTools()));
}
}
public class Html2Pdf
{
// Read more about converter on: https://github.com/rdvojmoc/DinkToPdf
// For our purposes we are going to use SynchronizedConverter
//IPdfConverter pdfConverter = new SynchronizedConverter(new PdfTools());
//Note we are using SynchronisedConverter / IPDFConverter via dependency injection
private readonly IPdfConverter pdfConverter;
public Html2Pdf(IPdfConverter pdfConverter)
{
this.pdfConverter = pdfConverter;
}
// A function to convert html content to pdf based on the configuration passed as arguments
// Arguments:
// HtmlContent: the html content to be converted
// Margins: the margis around the content
// DPI: The dpi is very important when you want to print the pdf.
// Returns a byte array of the pdf which can be stored as a file
private byte[] BuildPdf(string HtmlContent, MarginSettings Margins, int? DPI = 180)
{
// Call the Convert method of IPdfConverter / SynchronisedConverter "pdfConverter"
return pdfConverter.Convert(new HtmlToPdfDocument()
{
// Set the html content
Objects =
{
new ObjectSettings
{
HtmlContent = HtmlContent,
WebSettings = { DefaultEncoding = "UTF-8", LoadImages = true }
}
},
// Set the configurations
GlobalSettings = new GlobalSettings
{
PaperSize = PaperKind.A4,
DPI = DPI,
Margins = Margins
}
});
}
private string ConnectionString(ILogger log, ExecutionContext context)
{
var config = new ConfigurationBuilder()
.SetBasePath(context.FunctionAppDirectory)
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
var connString = config.GetConnectionString("ConnectionString");
if(connString == null){log.LogInformation("Connection String is null");}
return connString;
}
// The first arugment tells that the functions can be triggerd by a POST HTTP request.
// The second argument is mainly used for logging information, warnings or errors
[FunctionName("Html2Pdf")]
[STAThread]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "POST")] Html2PdfRequest Request, ILogger Log, ExecutionContext Context)
{
try{
Log.LogInformation("C# HTTP trigger function started for HTML2PDF.");
Log.LogInformation(Request.HtmlContent);
// PDFByteArray is a byte array of pdf generated from the HtmlContent
var PDFByteArray = BuildPdf(Request.HtmlContent, new MarginSettings(2, 2, 2, 2));
// The connection string of the Storage Account to which our PDF file will be uploaded
var StorageConnectionString = ConnectionString(Log, Context);
// Generate an instance of CloudStorageAccount by parsing the connection string
var StorageAccount = CloudStorageAccount.Parse(StorageConnectionString);
// Create an instance of CloudBlobClient to connect to our storage account
CloudBlobClient BlobClient = StorageAccount.CreateCloudBlobClient();
// Get the instance of CloudBlobContainer which points to a container name "pdf"
// Replace your own container name
CloudBlobContainer BlobContainer = BlobClient.GetContainerReference("pdf");
// Get the instance of the CloudBlockBlob to which the PDFByteArray will be uploaded
CloudBlockBlob Blob = BlobContainer.GetBlockBlobReference(Request.PDFFileName);
Log.LogInformation("Attempting to upload " + Request.PDFFileName);
// Upload the pdf blob
await Blob.UploadFromByteArrayAsync(PDFByteArray, 0, PDFByteArray.Length);
return (ActionResult)new OkObjectResult(Request.PDFFileName + " was successfully created.");
}
catch(Exception e)
{
Log.LogInformation("Error occurred: " + e);
return (ActionResult)new OkObjectResult("Error" + e.Message);
}
}
}
}
namespace pdfCreation
{
public class Html2PdfRequest
{
// The HTML content that needs to be converted.
public string HtmlContent { get; set; }
// The name of the PDF file to be generated
public string PDFFileName { get; set; }
}
}
我应该强调这里的变化:
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.13" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.13" />
更新了下面这部分代码:
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
FunctionsHostBuilderContext context = builder.GetContext();
builder.ConfigurationBuilder.AddEnvironmentVariables();
}
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton(typeof(IPdfConverter), new SynchronizedConverter(new PdfTools()));
}
}
private readonly IPdfConverter pdfConverter;
public Html2Pdf(IPdfConverter pdfConverter)
{
this.pdfConverter = pdfConverter;
}
另外,不要忘记在函数应用程序内的配置/应用程序设置下添加连接字符串。
希望对某人有帮助!
正如 distantcam 在 github issues #119
中的回答这是由于线程SynchronizedConverter未处于STA模式。 我测试了它,它解决了出现的错误
Qt: Could not initialize OLE (error 80010106)
您可以按如下方式创建 STA 同步转换器:
public class STASynchronizedConverter : BasicConverter
{
Thread conversionThread;
BlockingCollection<Task> conversions = new BlockingCollection<Task>();
bool kill = false;
private readonly object startLock = new object();
public STASynchronizedConverter(ITools tools) : base(tools)
{
}
public override byte[] Convert(IDocument document)
{
return Invoke(() => base.Convert(document));
}
public TResult Invoke<TResult>(Func<TResult> @delegate)
{
StartThread();
Task<TResult> task = new Task<TResult>(@delegate);
lock (task)
{
//add task to blocking collection
conversions.Add(task);
//wait for task to be processed by conversion thread
Monitor.Wait(task);
}
//throw exception that happened during conversion
if (task.Exception != null)
{
throw task.Exception;
}
return task.Result;
}
private void StartThread()
{
lock (startLock)
{
if (conversionThread == null)
{
conversionThread = new Thread(Run)
{
IsBackground = true,
Name = "wkhtmltopdf worker thread"
};
// This is to fix issue https://github.com/rdvojmoc/DinkToPdf/issues/119
conversionThread.SetApartmentState(ApartmentState.STA);
kill = false;
conversionThread.Start();
}
}
}
private void StopThread()
{
lock (startLock)
{
if (conversionThread != null)
{
kill = true;
while (conversionThread.ThreadState == ThreadState.Stopped)
{ }
conversionThread = null;
}
}
}
private void Run()
{
while (!kill)
{
//get next conversion taks from blocking collection
Task task = conversions.Take();
lock (task)
{
//run taks on thread that called RunSynchronously method
task.RunSynchronously();
//notify caller thread that task is completed
Monitor.Pulse(task);
}
}
}
}
并在 DI 中使用此转换器:
builder.Services.AddSingleton(typeof(IConverter), new STaSynchronizedConverter(new PdfTools()));