从使用itext创建的PDF中删除HTML和CSS样式

时间:2011-04-20 04:44:10

标签: java itext

我们在应用程序中使用itext动态创建PDF。用户使用富文本编辑器的屏幕在Web应用程序中插入PDF的内容。

以下是具体步骤。

  1. 用户转到添加PDF内容页面。
  2. 添加页面有一个富文本编辑器,可以输入PDF内容。
  3. 有时用户可以复制/粘贴现有​​word文档中的内容并输入RTE。
  4. 一旦他提交了内容,就会创建PDF。
  5. 使用RTE是因为我们有一些其他页面需要以粗体,斜体等方式显示内容。

    但是,我们不希望生成PDF中的RTE内容。

    在生成PDF之前,我们使用了一些java实用程序从内容中删除RTE内容。

    这可以正常工作,但是当从word文档复制内容时,文档应用的html和css样式不会被我们正在使用的java实用程序删除。

    如何在不包含任何HTML或CSS的情况下生成PDF?

    这是代码

    Paragraph paragraph = new Paragraph(Util.removeHTML(content), font);
    

    removeHTML方法如下

    public static String removeHTML(String htmlString) {
        if (htmlString == null)
            return "";
        htmlString.replace("\"", "'");
        htmlString = htmlString.replaceAll("\\<.*?>", "");
        htmlString = htmlString.replaceAll("&nbsp;", "");
        return htmlString;
    }
    

    以下是我从word文档中复制/粘贴时PDF中显示的其他内容。

    <w:LsdException Locked="false" Priority="10" SemiHidden="false
    UnhideWhenUsed="false" QFormat="true" Name="Title" />
    <w:LsdException Locked="false" Priority="11" SemiHidden="false"
    UnhideWhenUsed="false" QFormat="true" Name="Subtitle" />
    <w:LsdException Locked="false" Priority="22" SemiHidden="false"
    

    请帮忙!

    感谢。

2 个答案:

答案 0 :(得分:2)

我们的应用程序类似,我们有一个富文本编辑器(TinyMCE),我们的输出是通过iText PDF生成的PDF。我们希望HTML尽可能干净,理想情况下只使用iText的HTMLWorker支持的HTML标签。 TinyMCE可以做到这一点,但仍有一些情况,最终用户可以提交真正搞砸的HTML,这可能会破坏iText生成PDF的能力。

我们正在使用jSoup和jTidy + CSSParser的组合来过滤掉在HTML“样式”属性中输入的不需要的CSS样式。输入TinyMCE的HTML使用此服务进行清理,该服务可以清除文字标记中的任何粘贴(如果用户没有使用TinyMCE中的“从Word粘贴”按钮),并为我们提供了可以很好地转换为iTextPDFs HTMLWorker的HTML。

如果表格宽度在style属性中,HTMLWorker忽略它并将表格宽度设置为0,我还在iText的HTMLWorker解析器(5.0.6)中发现了表格宽度问题,因此这是修复下面的一些逻辑。我们使用以下库:a

com.itextpdf:itextpdf:5.0.6                 // used to generate PDFs
org.jsoup:jsoup:1.5.2                       // used for cleaning HTML, primary cleaner
net.sf.jtidy:jtidy:r938                     // used for cleaning HTML, secondary cleaner
net.sourceforge.cssparser:cssparser:0.9.5   // used to parse out unwanted HTML "style" attribute values

以下是我们为擦除HTML而构建的Groovy服务中的一些代码,只保留iText +支持的标记和样式属性,修复了表格问题。代码中有一些特定于我们的应用程序的假设。这对我们来说非常有效。

import com.steadystate.css.parser.CSSOMParser
import org.htmlcleaner.CleanerProperties
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.PrettyHtmlSerializer
import org.htmlcleaner.SimpleHtmlSerializer
import org.htmlcleaner.TagNode
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.safety.Cleaner
import org.jsoup.safety.Whitelist
import org.jsoup.select.Elements
import org.w3c.css.sac.InputSource
import org.w3c.dom.css.CSSRule
import org.w3c.dom.css.CSSRuleList
import org.w3c.dom.css.CSSStyleDeclaration
import org.w3c.dom.css.CSSStyleSheet
import org.w3c.tidy.Tidy

class HtmlCleanerService {

    static transactional = true

    def cleanHTML(def html) {

        // clean with JSoup which should filter out most unwanted things and
        // ensure good html syntax
        html = soupClean(html);

        // run through JTidy to remove repeated nested tags, clean anything JSoup left out
        html = tidyClean(html);

        return html;
    }

    def tidyClean(def html) {
        Tidy tidy = new Tidy() 
        tidy.setAsciiChars(true)
        tidy.setDropEmptyParas(true)
        tidy.setDropProprietaryAttributes(true)
        tidy.setPrintBodyOnly(true)

        tidy.setEncloseText(true)
        tidy.setJoinStyles(true)
        tidy.setLogicalEmphasis(true)
        tidy.setQuoteMarks(true)
        tidy.setHideComments(true)
        tidy.setWraplen(120)

        // (makeClean || dropFontTags) = replaces presentational markup by style rules
        tidy.setMakeClean(true)     // remove presentational clutter.
        tidy.setDropFontTags(true)  

        // word2000 = drop style & class attributes and empty p, span elements
        // draconian cleaning for Word2000
        tidy.setWord2000(true)      
        tidy.setMakeBare(true)      // remove Microsoft cruft.
        tidy.setRepeatedAttributes(org.w3c.tidy.Configuration.KEEP_FIRST) // keep first or last duplicate attribute

        // TODO ? tidy.setForceOutput(true)

        def reader = new StringReader(html);
        def writer = new StringWriter();

        // hide output from stderr
        tidy.setShowWarnings(false)
        tidy.setErrout(new PrintWriter(new StringWriter()))

        tidy.parse(reader, writer); // run tidy, providing an input and output stream
        return writer.toString()
    }

    def soupClean(def html) {

        // clean the html
        Document dirty = Jsoup.parseBodyFragment(html);
        Cleaner cleaner = new Cleaner(createWhitelist());
        Document clean = cleaner.clean(dirty);

        // now hunt down all style attributes and ensure we only have those that render with iTextPDF
        Elements styledNodes = clean.select("[style]"); // a with href
        styledNodes.each { element ->
            def style = element.attr("style");
            def tag = element.tagName().toLowerCase()
            def newstyle = ""
            CSSOMParser parser = new CSSOMParser();
            InputSource is = new InputSource(new StringReader(style))
            CSSStyleDeclaration styledeclaration = parser.parseStyleDeclaration(is)
            boolean hasProps = false
            for (int i=0; i < styledeclaration.getLength(); i++) {
                def propname = styledeclaration.item(i)
                def propval = styledeclaration.getPropertyValue(propname)
                propval = propval ? propval.trim() : ""

                if (["padding-left", "text-decoration", "text-align", "font-weight", "font-style"].contains(propname)) {
                    newstyle = newstyle + propname + ": " + propval + ";"
                    hasProps = true
                }

                // standardize table widths, itextPDF won't render tables if there is only width in the
                // style attribute.  Here we ensure the width is in its own attribute, and change the value so
                // it is in percentage and no larger than 100% to avoid end users from creating really goofy
                // tables that they can't edit properly becuase they have made the width too large.
                //
                // width of the display area in the editor is about 740px, so let's ensure everything
                // is relative to that
                //
                // TODO could get into trouble with nested tables and widths within as we assume
                // one table (e.g. could have nested tables both with widths of 500)
                if (tag.equals("table") && propname.equals("width")) {
                    if (propval.endsWith("%")) {
                        // ensure it is <= 100%
                        propval = propval.replaceAll(~"[^0-9]", "")
                        propval = Math.min(100, propval.toInteger())
                    }
                    else {
                        // else we have measurement in px or assumed px, clean up and
                        // get integer value, then calculate a percentage
                        propval = propval.replaceAll(~"[^0-9]", "")
                        propval = Math.min(100, (int) (propval.toInteger() / 740)*100)
                    } 
                    element.attr("width", propval + "%")
                }
            }
            if (hasProps) {
                element.attr("style", newstyle)
            } else {
                element.removeAttr("style")
            }

        }

        return clean.body().html();
    }

    /**
     * Returns a JSoup whitelist suitable for sane HTML output and iTextPDF 
     */
    def createWhitelist() {
        Whitelist wl = new Whitelist();

        // iText supported tags
        wl.addTags(
            "br", "div", "p", "pre", "span", "blockquote", "q", "hr",
            "h1", "h2", "h3", "h4", "h5", "h6",
            "u", "strike", "s", "strong", "sub", "sup", "em", "i", "b", 
            "ul", "ol", "li", "ol",
            "table", "tbody", "td", "tfoot", "th", "thead", "tr", 
            );

        // iText attributes recognized which we care about
        // padding-left (div/p/span indentation)
        // text-align (for table right/left align)
        // text-decoration (for span/div/p underline, strikethrough)
        // font-weight (for span/div/p bolder etc)
        // font-style (for span/div/p italic etc)
        // width (for tables)
        // colspan/rowspan (for tables)

        ["span", "div", "p", "table", "ul", "ol", "pre", "td", "th"].each { tag ->
            ["style", "padding-left", "text-decoration", "text-align", "font-weight", "font-style"].each { attr ->
                wl.addAttributes(tag, attr)
            }
        }

        ["td", "th"].each { tag ->
            ["colspan", "rowspan", "width"].each { attr ->
                wl.addAttributes(tag, attr)
            }
        }
        wl.addAttributes("table", "width", "style", "cellpadding")

        // img support
        // wl.addAttributes("img", "align", "alt", "height", "src", "title", "width")


        return wl
    }
}

答案 1 :(得分:0)

如果您只想要HTML文档的文本内容,则使用XML API(如SAX或DOM)仅从文档中发出文本节点。如果您了解DOM的方法,那么使用DocumentTraversal API这是微不足道的。如果我运行了IDE,我会粘贴一个样本......

此外,显示的removeHtml方法效率低下。使用Pattern.compile并将其缓存在静态变量中,并使用Matcher API替换为StringBuffer(或者StringBuilder,如果它使用的话)。这样你就不会创建一堆中间字符串并将它们扔掉。