kuu*_*nbo 5 pdf itext html-parsing html-agility-pack xmlworker
我想转换HTML到PDF在使用iTextSharp的ASP.NET同时使用Web应用程序的MVC,和 网页形式.在<img>和<a>元素有绝对和相对 URL和一些的<img>元素的base64.典型的答案在这里SO和谷歌搜索结果中使用泛型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
);
}
}
Run Code Online (Sandbox Code Playgroud)
所以这样的样本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>
Run Code Online (Sandbox Code Playgroud)
生成的PDF:(1)缺少所有图像,(2)所有具有相对URL的超链接都被破坏并使用文件URI方案(file///XXX...)而不是指向正确的网站.
SO的一些答案和谷歌搜索的其他答案建议用绝对URL替换相对URL,这对于一次性案例是完全可以接受的.但是,使用硬编码字符串全局替换所有<img src>和<a href>属性对于这个问题是不可接受的,所以请不要发布这样的答案,因为它会相应地被低估.
我正在寻找适用于测试,开发和生产环境中的许多不同Web应用程序的解决方案.
kuu*_*nbo 11
开箱即用XMLWorker 只能理解绝对URI,因此所描述的问题是预期的行为.如果没有其他信息,解析器无法自动推导URI方案或路径.
实现ILinkProvider可修复损坏的超链接问题,实现IImageProvider可修复损坏的图像问题.由于两种实现都必须执行URI解析,因此这是第一步.下面的帮助器类可以做到这一点,并且还尝试使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");
}
}
Run Code Online (Sandbox Code Playgroud)
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;
}
}
Run Code Online (Sandbox Code Playgroud)
IImageProvider只需要实现一个方法,Retrieve(string src)但Store(string src, Image img)很容易 - 请注意内联注释以及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() { }
}
Run Code Online (Sandbox Code Playgroud)
基于XML Worker文档,将上面的实现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 ICSSResolver 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);
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
作为行内评论,SimpleAjaxImgFix(string xHtml)专门处理XHR是可以发送未关闭的<img>标签,这是有效的 HTML,但无效 XML是将打破XMLWorker.可以在此处找到有关如何使用XHR和iTextSharp接收PDF或其他二进制数据的简单说明和实现.
使用A Regex是为了SimpleAjaxImgFix(string xHtml)使任何使用(复制/粘贴?)代码的人不需要添加另一个nuget包,但是应该使用HTML像HtmlAgilityPack 这样的解析器 ,因为它改变了这个:
<div><img src='a.gif'><br><hr></div>
Run Code Online (Sandbox Code Playgroud)
进入这个:
<div><img src='a.gif' /><br /><hr /></div>
Run Code Online (Sandbox Code Playgroud)
只需几行代码:
var hDocument = new HtmlDocument()
{
OptionWriteEmptyNodes = true,
OptionAutoCloseOnEnd = true
};
hDocument.LoadHtml("<div><img src='a.gif'><br><hr></div>");
var closedTags = hDocument.DocumentNode.WriteTo();
Run Code Online (Sandbox Code Playgroud)
另外值得注意的是 - 将SimpleParser.Parse()上面的内容用作另外实现自定义ICSSResolver或ITagProcessorFactory的一般蓝图,文档中对此进行了解释.
现在应该处理问题中描述的问题.来自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();
}
Run Code Online (Sandbox Code Playgroud)
或从Web Form该得到HTML从服务器控制:
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();
Run Code Online (Sandbox Code Playgroud)
或者在文件系统上包含超链接和图像的简单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>
Run Code Online (Sandbox Code Playgroud)
或来自远程网站的HTML:
<div>
<div>
<img width="200" alt="Wikipedia Logo"
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>
Run Code Online (Sandbox Code Playgroud)
以上两个HTML片段从控制台应用程序运行:
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);
}
Run Code Online (Sandbox Code Playgroud)
相当长的答案,但在SO标记考虑看看问题在这里html,pdf和itextsharp,写这篇文章(2016年2月23日)的有776个结果对4063总标签itextsharp-这是19% .
| 归档时间: |
|
| 查看次数: |
7358 次 |
| 最近记录: |