如何使用 iText 将带有图像和超链接的 HTML 转换为 PDF?
Posted
技术标签:
【中文标题】如何使用 iText 将带有图像和超链接的 HTML 转换为 PDF?【英文标题】:How can I use iText to convert HTML with images and hyperlinks to PDF? 【发布时间】:2016-06-06 06:19:21 【问题描述】:我正在尝试在使用MVC、和 web forms 的ASP.NET
Web 应用程序中使用iTextSharp 将html
转换为PDF
。 <img>
和<a>
元素具有绝对和相对 URL,并且一些<img>
元素是base64。 SO 和 Google 搜索结果中的典型答案使用通用的 HTML
到 PDF
代码和 XMLWorkerHelper
,看起来像这样:
using (var stringReader = new StringReader(xHtml))
using (Document document = new Document())
PdfWriter writer = PdfWriter.GetInstance(document, stream);
document.Open();
XMLWorkerHelper.GetInstance().ParseXHtml(
writer, document, stringReader
);
所以像这样的示例HTML
:
<div>
<h3>HTML Works, but Broken in Converted PDF</h3>
<div>Relative local <img>: <img src='./../content/images/kuujinbo_320-30.gif' /></div>
<div>
Base64 <img>:
<img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' />
</div>
<div><a href='/somePage.html'>Relative local hyperlink, broken in PDF</a></div>
<div>
生成的 PDF:(1) 缺少所有图像,并且 (2) 具有相对 URL 的所有超链接都已损坏并使用 file URI scheme ( file///XXX...
) 而不是指向正确的网站。
SO 的一些答案和 Google 搜索的其他答案建议用绝对 URL 替换相对 URL,对于一次性情况,这完全可以接受。但是,对于这个问题,用硬编码字符串全局替换所有 <img src>
和 <a href>
属性是不可接受的,因此请不要发布这样的答案,因为它会因此被否决。
我正在寻找一种适用于测试、开发和生产环境中的许多不同 Web 应用程序的解决方案。
【问题讨论】:
啊,我没有立即看到这确实是您自己的问题。好的,下次我会好好看看。 【参考方案1】:开箱即用XMLWorker
only understands absolute URIs,因此所描述的问题是预期行为。解析器无法自动推断 URI schemes 或没有一些附加信息的路径。
实施ILinkProvider 修复了损坏的超链接问题,实施IImageProvider 修复了损坏的图像问题。由于两种实现都必须执行URI resolution,这是第一步。下面的帮助类可以做到这一点,并尝试使 web (ASP.NET
) 上下文调用(示例如下)尽可能简单:
// resolve URIs for LinkProvider & ImageProvider
public class UriHelper
/* IsLocal; when running in web context:
* [1] give LinkProvider http[s] scheme; see CreateBase(string baseUri)
* [2] give ImageProvider relative path starting with '/' - see:
* Join(string relativeUri)
*/
public bool IsLocal get; set;
public HttpContext HttpContext get; private set;
public Uri BaseUri get; private set;
public UriHelper(string baseUri) : this(baseUri, true)
public UriHelper(string baseUri, bool isLocal)
IsLocal = isLocal;
HttpContext = HttpContext.Current;
BaseUri = CreateBase(baseUri);
/* get URI for IImageProvider to instantiate iTextSharp.text.Image for
* each <img> element in the HTML.
*/
public string Combine(string relativeUri)
/* when running in a web context, the HTML is coming from a MVC view
* or web form, so convert the incoming URI to a **local** path
*/
if (HttpContext != null && !BaseUri.IsAbsoluteUri && IsLocal)
return HttpContext.Server.MapPath(
// Combine() checks directory traversal exploits
VirtualPathUtility.Combine(BaseUri.ToString(), relativeUri)
);
return BaseUri.Scheme == Uri.UriSchemeFile
? Path.Combine(BaseUri.LocalPath, relativeUri)
// for this example we're assuming URI.Scheme is http[s]
: new Uri(BaseUri, relativeUri).AbsoluteUri;
private Uri CreateBase(string baseUri)
if (HttpContext != null)
// running on a web server; need to update original value
var req = HttpContext.Request;
baseUri = IsLocal
// IImageProvider; absolute virtual path (starts with '/')
// used to convert to local file system path. see:
// Combine(string relativeUri)
? req.ApplicationPath
// ILinkProvider; absolute http[s] URI scheme
: req.Url.GetLeftPart(UriPartial.Authority)
+ HttpContext.Request.ApplicationPath;
Uri uri;
if (Uri.TryCreate(baseUri, UriKind.RelativeOrAbsolute, out uri)) return uri;
throw new InvalidOperationException("cannot create a valid BaseUri");
现在实现ILinkProvider
非常简单,因为UriHelper
提供了基本URI。我们只需要正确的 URI 方案(file
或 http[s]
):
// make hyperlinks with relative URLs absolute
public class LinkProvider : ILinkProvider
// rfc1738 - file URI scheme section 3.10
public const char SEPARATOR = '/';
public string BaseUrl get; private set;
public LinkProvider(UriHelper uriHelper)
var uri = uriHelper.BaseUri;
/* simplified implementation that only takes into account:
* Uri.UriSchemeFile || Uri.UriSchemeHttp || Uri.UriSchemeHttps
*/
BaseUrl = uri.Scheme == Uri.UriSchemeFile
// need trailing separator or file paths break
? uri.AbsoluteUri.TrimEnd(SEPARATOR) + SEPARATOR
// assumes Uri.UriSchemeHttp || Uri.UriSchemeHttps
: BaseUrl = uri.AbsoluteUri;
public string GetLinkRoot()
return BaseUrl;
IImageProvider
仅需要实现单个方法 Retrieve(string src)
,但 Store(string src, Image img)
很容易 - 注意那里的内联 cmets 和 GetImageRootPath()
:
// handle <img> elements in HTML
public class ImageProvider : IImageProvider
private UriHelper _uriHelper;
// see Store(string src, Image img)
private Dictionary<string, Image> _imageCache =
new Dictionary<string, Image>();
public virtual float ScalePercent get; set;
public virtual Regex Base64 get; set;
public ImageProvider(UriHelper uriHelper) : this(uriHelper, 67f)
// hard-coded based on general past experience ^^^
// but call the overload to supply your own
public ImageProvider(UriHelper uriHelper, float scalePercent)
_uriHelper = uriHelper;
ScalePercent = scalePercent;
Base64 = new Regex( // rfc2045, section 6.8 (alphabet/padding)
@"^data:image/[^;]+;base64,(?<data>[a-z0-9+/]+=0,2)$",
RegexOptions.Compiled | RegexOptions.IgnoreCase
);
public virtual Image ScaleImage(Image img)
img.ScalePercent(ScalePercent);
return img;
public virtual Image Retrieve(string src)
if (_imageCache.ContainsKey(src)) return _imageCache[src];
try
if (Regex.IsMatch(src, "^https?://", RegexOptions.IgnoreCase))
return ScaleImage(Image.GetInstance(src));
Match match;
if ((match = Base64.Match(src)).Length > 0)
return ScaleImage(Image.GetInstance(
Convert.FromBase64String(match.Groups["data"].Value)
));
var imgPath = _uriHelper.Combine(src);
return ScaleImage(Image.GetInstance(imgPath));
// not implemented to keep the SO answer (relatively) short
catch (BadElementException ex) return null;
catch (IOException ex) return null;
catch (Exception ex) return null;
/*
* always called after Retrieve(string src):
* [1] cache any duplicate <img> in the HTML source so the image bytes
* are only written to the PDF **once**, which reduces the
* resulting file size.
* [2] the cache can also **potentially** save network IO if you're
* running the parser in a loop, since Image.GetInstance() creates
* a WebRequest when an image resides on a remote server. couldn't
* find a CachePolicy in the source code
*/
public virtual void Store(string src, Image img)
if (!_imageCache.ContainsKey(src)) _imageCache.Add(src, img);
/* XMLWorker documentation for ImageProvider recommends implementing
* GetImageRootPath():
*
* http://demo.itextsupport.com/xmlworker/itextdoc/flatsite.html#itextdoc-menu-10
*
* but a quick run through the debugger never hits the breakpoint, so
* not sure if I'm missing something, or something has changed internally
* with XMLWorker....
*/
public virtual string GetImageRootPath() return null;
public virtual void Reset()
基于XML Worker documentation,将上面的ILinkProvider
和IImageProvider
的实现挂钩到一个简单的解析器类中非常简单:
/* a simple parser that uses XMLWorker and XMLParser to handle converting
* (most) images and hyperlinks internally
*/
public class SimpleParser
public virtual ILinkProvider LinkProvider get; set;
public virtual IImageProvider ImageProvider get; set;
public virtual HtmlPipelineContext HtmlPipelineContext get; set;
public virtual ITagProcessorFactory TagProcessorFactory get; set;
public virtual ICs-s-resolver CssResolver get; set;
/* overloads simplfied to keep SO answer (relatively) short. if needed
* set LinkProvider/ImageProvider after instantiating SimpleParser()
* to override the defaults (e.g. ImageProvider.ScalePercent)
*/
public SimpleParser() : this(null)
public SimpleParser(string baseUri)
LinkProvider = new LinkProvider(new UriHelper(baseUri, false));
ImageProvider = new ImageProvider(new UriHelper(baseUri, true));
HtmlPipelineContext = new HtmlPipelineContext(null);
// another story altogether, and not implemented for simplicity
TagProcessorFactory = Tags.GetHtmlTagProcessorFactory();
CssResolver = XMLWorkerHelper.GetInstance().GetDefaultCssResolver(true);
/*
* when sending XHR via any of the popular javascript frameworks,
* <img> tags are **NOT** always closed, which results in the
* infamous iTextSharp.tool.xml.exceptions.RuntimeWorkerException:
* 'Invalid nested tag a found, expected closing tag img.' a simple
* workaround.
*/
public virtual string SimpleAjaxImgFix(string xHtml)
return Regex.Replace(
xHtml,
"(?<image><img[^>]+)(?<=[^/])>",
new MatchEvaluator(match => match.Groups["image"].Value + " />"),
RegexOptions.IgnoreCase | RegexOptions.Multiline
);
public virtual void Parse(Stream stream, string xHtml)
xHtml = SimpleAjaxImgFix(xHtml);
using (var stringReader = new StringReader(xHtml))
using (Document document = new Document())
PdfWriter writer = PdfWriter.GetInstance(document, stream);
document.Open();
HtmlPipelineContext
.SetTagFactory(Tags.GetHtmlTagProcessorFactory())
.SetLinkProvider(LinkProvider)
.SetImageProvider(ImageProvider)
;
var pdfWriterPipeline = new PdfWriterPipeline(document, writer);
var htmlPipeline = new HtmlPipeline(HtmlPipelineContext, pdfWriterPipeline);
var cssResolverPipeline = new CssResolverPipeline(CssResolver, htmlPipeline);
XMLWorker worker = new XMLWorker(cssResolverPipeline, true);
XMLParser parser = new XMLParser(worker);
parser.Parse(stringReader);
正如内联注释,SimpleAjaxImgFix(string xHtml)
专门处理 XHR that may send unclosed <img>
tags,这是 有效 HTML
,但 无效 XML
将 打破XMLWorker
。如何使用 XHR 和 iTextSharp can be found here 接收 PDF 或其他二进制数据的简单解释和实现。
SimpleAjaxImgFix(string xHtml)
中使用了 Regex
,因此任何使用(复制/粘贴?)代码的人都不需要添加另一个 nuget
包,而是添加一个 HTML
解析器像HtmlAgilityPack 应该使用,因为轮到这个了:
<div><img src='a.gif'><br><hr></div>
进入这个:
<div><img src='a.gif' /><br /><hr /></div>
只有几行代码:
var hDocument = new HtmlDocument()
OptionWriteEmptyNodes = true,
OptionAutoCloseOnEnd = true
;
hDocument.LoadHtml("<div><img src='a.gif'><br><hr></div>");
var closedTags = hDocument.DocumentNode.WriteTo();
还要注意 - 使用上面的SimpleParser.Parse()
作为通用 蓝图来额外实现自定义ICs-s-resolver 或ITagProcessorFactory,即explained in the documentation。
现在应该注意问题中描述的问题。来自MVC Action Method
:
[HttpPost] // some browsers have URL length limits
[ValidateInput(false)] // or throws HttpRequestValidationException
public ActionResult Index(string xHtml)
Response.ContentType = "application/pdf";
Response.AppendHeader(
"Content-Disposition", "attachment; filename=test.pdf"
);
var simpleParser = new SimpleParser();
simpleParser.Parse(Response.OutputStream, xHtml);
return new EmptyResult();
或者从Web Form
得到HTML
从server control:
Response.ContentType = "application/pdf";
Response.AppendHeader("Content-Disposition", "attachment; filename=test.pdf");
using (var stringWriter = new StringWriter())
using (var htmlWriter = new HtmlTextWriter(stringWriter))
ConvertControlToPdf.RenderControl(htmlWriter);
var simpleParser = new SimpleParser();
simpleParser.Parse(Response.OutputStream, stringWriter.ToString());
Response.End();
或文件系统上带有超链接和图像的简单 HTML 文件:
<h1>HTML Page 00 on Local File System</h1>
<div>
<div>
Relative <img>: <img src='Images/alt-gravatar.png' />
</div>
<div>
Hyperlink to file system HTML page:
<a href='file-system-html-01.html'>Page 01</a>
</div>
</div>
或来自远程网站的 HTML:
<div>
<div>
<img
src="portal/wikipedia.org/assets/img/Wikipedia-logo-v2.png">
</div>
<div lang="en">
<a href="https://en.wikipedia.org/">English</a>
</div>
<div lang="en">
<a href="wiki/IText">iText</a>
</div>
</div>
以上两个HTML
sn-ps 从控制台应用程序运行:
var filePaths = Path.Combine(basePath, "file-system-html-00.html");
var htmlFile = File.ReadAllText(filePaths);
var remoteUrl = Path.Combine(basePath, "wikipedia.html");
var htmlRemote = File.ReadAllText(remoteUrl);
var outputFile = Path.Combine(basePath, "filePaths.pdf");
var outputRemote = Path.Combine(basePath, "remoteUrl.pdf");
using (var stream = new FileStream(outputFile, FileMode.Create))
var simpleParser = new SimpleParser(basePath);
simpleParser.Parse(stream, htmlFile);
using (var stream = new FileStream(outputRemote, FileMode.Create))
var simpleParser = new SimpleParser("https://wikipedia.org");
simpleParser.Parse(stream, htmlRemote);
相当长的答案,但请查看标记为 html
, pdf
, and itextsharp
的问题,截至撰写本文时 (2016-02-23),有 776 个结果,总共有 4,063 个标记为 @987654335 @ - 那是 19%。
【讨论】:
不错,在这么短的时间内回答这么长……;) @mkl - 哈哈....过去一周左右来回发布问题,然后自己回答。<sarcasm>
Being the eternal optimist </sarcasm>
我希望这对 tiny 有点帮助 - 即使我已经厌倦了看到版本 1.XX.XXX.XXXX 分拆这个或非常相似的概念。 ;)
很好的@kuujinbo,这将是一个很好的“接近重复”的参考!
@ChrisHaas - 谢谢! :) 希望它有助于清理这里的东西。 (itextsharp
标签)
这是很好的信息,帮助我理解了the iText documentation。像您的 SimpleParser 这样的东西将成为 iTextSharp 的一部分似乎是合乎逻辑的。您是否有机会考虑将此作为pull request 提交?【参考方案2】:
非常有用的帖子,
我在将报告 html 中的图像渲染为 pdf 时遇到问题。有了你的帖子,我可以做到。
我正在使用 asp.mvc 5。
我只需要改变ImageProviderClass的这个方法
public virtual string GetImageRootPath() return null;
到
public virtual string GetImageRootPath() HostingEnvironment.MapPath("~/Content/Images/")
谢谢!
【讨论】:
以上是关于如何使用 iText 将带有图像和超链接的 HTML 转换为 PDF?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 itext7 Java 将多个图像添加到 PDF?