iText를 사용하여 이미지 및 하이퍼 링크가있는 HTML을 PDF로 변환하는 방법은 무엇입니까?

html-agility-pack html-parsing itext pdf xmlworker

문제

MVC 웹 양식 을 사용하는 ASP.NET 웹 응용 프로그램에서 iTextSharp를 사용하여 HTMLPDF 로 변환하려고 합니다 . <img><a> 요소는 절대 URL과 상대 URL을 가지며 <img> 요소 중 일부는 base64 입니다. SO 및 Google 검색 결과의 일반적인 해답은 다음과 같은 XMLWorkerHelper 를 사용하여 PDF 코드에 일반 HTML 을 사용합니다.

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이있는 모든 하이퍼 링크가 손상되고 올바른 웹 사이트를 가리키는 대신 파일 URI 스키마 ( file///XXX... )를 사용합니다. .

Google 검색의 SO 및 기타 사용자의 답변 중 일부는 관련 URL을 절대 URL로 대체하는 것이 좋습니다. 일회용 으로 는 절대 허용 됩니다. 그러나 모든 <img src><a href> 속성을 하드 코딩 된 문자열로 전역 대체하는 것은이 질문에 용납 될 수 없으므로 이와 같이 응답을 게시하지 마십시오.

테스트, 개발 및 생산 환경에 거주하는 많은 다른 웹 응용 프로그램 작동하는 솔루션을 찾고 있습니다.

수락 된 답변

XMLWorker 는 절대 URI 만 이해 하므로 설명 된 문제는 예상되는 동작입니다. 파서는 추가 정보없이 URI 스키마 나 경로를 자동으로 추론 할 수 없습니다.

ILinkProvider를 구현하면 끊어진 하이퍼 링크 문제가 해결되고 IImageProvider를 구현하면 깨진 이미지 문제가 해결됩니다. 두 구현 모두 URI 확인을 수행해야하기 때문에 이것이 첫 번째 단계입니다. 다음 도우미 클래스는이를 수행하고 가능한 한 단순한 웹 ( 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 가 기본 URI를 제공하기 때문에 UriHelper 구현 ILinkProvider 것이 매우 간단합니다. 올바른 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 Worker 문서를 기반으로 ILinkProviderIImageProvider 구현을 간단한 파서 클래스에 연결하는 것은 매우 간단합니다.

/* 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 이지만 XMLWorker 깨뜨릴 수있는 유효하지 않은 XML unclosed <img> 태그를 보낼 수있는 XHR을 구체적으로 처리합니다. XHR 및 iTextSharp를 사용하여 PDF 또는 기타 이진 데이터를받는 방법에 대한 간단한 설명 및 구현은 여기에서 찾을 수 있습니다 .

Regex 사용 하였다 SimpleAjaxImgFix(string xHtml) (? 복사 / 붙여 넣기)를 사람이 사용하도록 코드가 다른 추가 할 필요가 없습니다 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() 일반적인 청사진으로 사용 하여 설명서에 설명 된 사용자 지정 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();
}

또는 서버 컨트롤 에서 HTML 을 가져 오는 Web Form 에서 가져옵니다.

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 &lt;img&gt;: <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);
}

꽤 긴 대답하지만, SO 태그 여기 질문에서 살펴 본다 html , pdfitextsharp 이 글을 쓰는 (2016년 2월 23일)의로는, 4063 총에 대한 776 개 결과 태그가 itextsharp - 그 19 %입니다.


인기 답변

매우 유용한 게시물,

나는 pdf에 나의보고 html에있는 심상을 만드는 문제이었다. 당신의 포스트로 나는 그것을 할 수 있었다.

나는 asp.mvc 5와 함께 일하고있다.

ImageProviderClass의이 메서드 만 변경하면됩니다.

public virtual string GetImageRootPath() { return null; }

public virtual string GetImageRootPath() { HostingEnvironment.MapPath("~/Content/Images/") }

감사!



아래 라이선스: CC-BY-SA with attribution
와 제휴하지 않음 Stack Overflow
이 KB는 합법적입니까? 예, 이유를 알아보십시오.
아래 라이선스: CC-BY-SA with attribution
와 제휴하지 않음 Stack Overflow
이 KB는 합법적입니까? 예, 이유를 알아보십시오.