我试图在一个同时使用HTML
、和 web表单的ASP.NET
web应用程序中使用iTextSharp将iTextSharp转换成ASP.NET
。<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
);
}
}
因此,对于像这样的示例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的(S)超链接被破坏,并使用文件URI方案 (file///XXX...
)而不是指向正确的网站。
这里有一些答案,还有一些来自Google搜索的建议,建议用绝对URL代替相对URL,这对于一次性的情况来说是完全可以接受的。但是,对于这个问题,全局上用硬编码字符串替换所有<img src>
和<a href>
属性都是<img src>
,所以请不要发布这样的答复,因为这样会被否决。
我正在寻找一种适用于的解决方案--许多不同的web应用程序驻留在测试、开发和生产环境中。
发布于 2016-02-24 05:34:15
开箱即用的XMLWorker
只懂绝对的URI,因此所描述的问题是预期的行为。如果没有其他信息,解析器就无法自动推断URI方案或路径。
实现ILinkProvider修复了中断的超链接问题,而实现IImageProvider则解决了损坏的图像问题。因为这两种实现都必须执行URI解析,所以这是第一步。下面的helper类会这样做,并尝试使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)
是很容易注意的内联注释和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工作文档,将上面的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);
}
}
}
}
作为内联注释,SimpleAjaxImgFix(string xHtml)
专门处理标签,这是有效的 HTML
,但是无效的 XML
将E 233
断开XMLWorker
。关于如何使用XHR和iTextSharp 可以在这里找到接收PDF或其他二进制数据的简单解释和实现。
Regex
在SimpleAjaxImgFix(string xHtml)
中被使用,所以任何人使用(复制/粘贴?)代码不需要添加另一个nuget
包,但是应该使用像HtmlAgilityPack这样的HTML
解析器,因为它的作用是:
<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()
作为一般蓝图来另外实现一个自定义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();
}
或者来自从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();
或者一个简单的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 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>
上面两个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);
}
很长的答案,但看看这里的问题在如此标记的itextsharp
,截至本文撰写(2016-02-23),有776个结果对4063个总标记的itextsharp
-即19%。
发布于 2018-10-13 03:32:40
很有帮助的职位,
我在我的报告html到pdf中渲染图片时遇到了问题。有了你的职位我就能做到。
我和asp.mvc 5一起工作。
我只需要更改ImageProviderClass的这个方法
public virtual string GetImageRootPath() { return null; }
至
public virtual string GetImageRootPath() { HostingEnvironment.MapPath("~/Content/Images/") }
谢谢!
https://stackoverflow.com/questions/35594030
复制相似问题