Java 富文本转word(支持水印)

发布于:2025-05-01 ⋅ 阅读:(15) ⋅ 点赞:(0)

前言:

本文的目的是将传入的富文本内容(html标签,图片)并且分页导出为word文档。

所使用的为docx4j

一、依赖导入

        <!-- 富文本转word -->
        <dependency>
            <groupId>org.docx4j</groupId>
            <artifactId>docx4j</artifactId>
            <version>6.1.2</version>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-log4j12</artifactId>
                    <groupId>org.slf4j</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>log4j</artifactId>
                    <groupId>log4j</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>commons-io</artifactId>
                    <groupId>commons-io</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>commons-compress</artifactId>
                    <groupId>org.apache.commons</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>guava</artifactId>
                    <groupId>com.google.guava</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>mbassador</artifactId>
                    <groupId>net.engio</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.docx4j</groupId>
            <artifactId>docx4j-ImportXHTML</artifactId>
            <version>8.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.docx4j</groupId>
            <artifactId>docx4j-JAXB-ReferenceImpl</artifactId>
            <version>8.1.0</version>
            <exclusions>
                <exclusion>
                    <artifactId>docx4j-core</artifactId>
                    <groupId>org.docx4j</groupId>
                </exclusion>
            </exclusions>
        </dependency>

二、字体文件

将字体文件上传到子项目resources的static.fonts目录中

三、工具类

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import sun.misc.BASE64Decoder;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;

/** 
 * 编码工具类 
 * 实现aes加密、解密 
 */  
public class AESEncryptUtils {
	
	public static final String aesKey = "this-is-aescrypt";

    private AESEncryptUtils(){
        throw new AssertionError();
    }

    /**
     * 算法 
     */  
    private static final String ALGORITHMSTR = "AES/ECB/PKCS5Padding";  
  
    public static void main(String[] args) throws Exception {
        System.out.println(AESEncryptUtils.aesEncrypt("html2Pdf", "this-is-aescrypt"));
    }
    

    public static String aesEncryptToString(String content) throws Exception {
    	return aesEncrypt(content, aesKey);
    }
    
    public static String aesDecryptToString(String content) throws Exception {
    	return aesDecrypt(content, aesKey);
    }
 
    /** 
     * 将byte[]转为各种进制的字符串 
     * @param bytes byte[] 
     * @param radix 可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制 
     * @return 转换后的字符串 
     */  
    public static String binary(byte[] bytes, int radix){  
        return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数  
    }  
  
    /** 
     * base 64 encode 
     * @param bytes 待编码的byte[] 
     * @return 编码后的base 64 code 
     */  
    public static String base64Encode(byte[] bytes){  
        return Base64.encodeBase64String(bytes);
    }  
  
    /** 
     * base 64 decode 
     * @param base64Code 待解码的base 64 code 
     * @return 解码后的byte[] 
     * @throws Exception 
     */  
    public static byte[] base64Decode(String base64Code) throws Exception{  
        return StringUtils.isEmpty(base64Code) ? null : new BASE64Decoder().decodeBuffer(base64Code);
    }  
  
    /**
     * AES加密 
     * @param content 待加密的内容 
     * @param encryptKey 加密密钥 
     * @return 加密后的byte[] 
     * @throws Exception 
     */  
    public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {  
        KeyGenerator kgen = KeyGenerator.getInstance("AES");  
        kgen.init(128);  
        Cipher cipher = Cipher.getInstance(ALGORITHMSTR);  
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES"));  
  
        return cipher.doFinal(content.getBytes("utf-8"));  
    }  
  
    /**
     * AES加密为base 64 code 
     * @param content 待加密的内容 
     * @param encryptKey 加密密钥 
     * @return 加密后的base 64 code 
     * @throws Exception 
     */  
    public static String aesEncrypt(String content, String encryptKey) throws Exception {  
        return base64Encode(aesEncryptToBytes(content, encryptKey));  
    }  
  
    /** 
     * AES解密 
     * @param encryptBytes 待解密的byte[] 
     * @param decryptKey 解密密钥 
     * @return 解密后的String 
     * @throws Exception 
     */  
     public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception {  
            KeyGenerator kgen = KeyGenerator.getInstance("AES");  
            kgen.init(128);  
  
            Cipher cipher = Cipher.getInstance(ALGORITHMSTR);  
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), "AES"));  
            byte[] decryptBytes = cipher.doFinal(encryptBytes);  
  
            return new String(decryptBytes);  
        }  
  
  
    /** 
     * 将base 64 code AES解密 
     * @param encryptStr 待解密的base 64 code 
     * @param decryptKey 解密密钥 
     * @return 解密后的string 
     * @throws Exception 
     */  
    public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {  
        return StringUtils.isEmpty(encryptStr) ? null : aesDecryptByBytes(base64Decode(encryptStr), decryptKey);  
    }  

}

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "html.convert")
public class HtmlConvertproperties {

    /** 生成的文件保存路径 */
    private String fileSavePath;

    /** echarts转换后的图片保存路径 */
    private String echartsImgSavePath;
}
package cn.aotu.sss.module.sss.util.text.word;

import com.aliyuncs.utils.StringUtils;
import org.docx4j.Docx4J;
import org.docx4j.XmlUtils;
import org.docx4j.convert.in.xhtml.XHTMLImporterImpl;
import org.docx4j.fonts.IdentityPlusMapper;
import org.docx4j.fonts.Mapper;
import org.docx4j.fonts.PhysicalFont;
import org.docx4j.fonts.PhysicalFonts;
import org.docx4j.jaxb.Context;
import org.docx4j.model.structure.DocumentModel;
import org.docx4j.model.structure.PageSizePaper;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.PartName;
import org.docx4j.openpackaging.parts.WordprocessingML.*;
import org.docx4j.openpackaging.parts.relationships.RelationshipsPart;
import org.docx4j.relationships.Relationship;
import org.docx4j.wml.Color;
import org.docx4j.wml.*;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Entities;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.*;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLEncoder;

import static cn.aotu.sss.module.sss.util.text.word.HtmlConverter.RemoveTag.*;

/**
 * html转换工具类
 *
 * 图片长宽乘积不能太大,不然会导致内存溢出
 *
 * HtmlConverter
 * @author: huangbing
 * @date: 2020/8/7 2:32 下午
 */
public class HtmlConverter {

    private static final ObjectFactory factory = Context.getWmlObjectFactory();

    /**
     * 页面大小
     */
    public enum PageSize {
        /** 大小*/
        LETTER("letter"),
        LEGAL("legal"),
        A3("A3"),
        A4("A4"),
        A5("A5"),
        B4JIS("B4JIS");

        PageSize(String code){
            this.code = code;
        }
        private String code;

        public String getCode() {
            return code;
        }
    }

    /**
     * 移除的标签
     */
    enum RemoveTag {
        /** 移除的标签*/
        SCRIPT("script"), A("a"), LINK("link"), HREF("href");

        RemoveTag(String code){
            this.code = code;
        }
        private String code;

        public String getCode() {
            return code;
        }
    }

    /**
     * 参数类
     */
    private static class Params {

        /** 默认字体库*/
        private final static String DEFAULT_FONT_FAMILY = "STSongStd-Light";
        /** 默认字体库路径*/
        private final static String DEFAULT_FONT_PATH = "/static/fonts/STSongStd-Light.ttf";
        /** 默认是否横版*/
        private final static boolean DEFAULT_LAND_SCAPE = false;
        /** 默认页面尺寸*/
        private final static String DEFAULT_PAGE_SIZE = PageSize.A4.getCode();
        /** 字体库*/
        private String fontFamily = DEFAULT_FONT_FAMILY;
        /** 字体库路径*/
        private String fontPath = DEFAULT_FONT_PATH;
        /** 页面尺寸*/
        private String pageSize = DEFAULT_PAGE_SIZE;
        /** 是否横版*/
        private boolean isLandScape = DEFAULT_LAND_SCAPE;
        /** 保存的文件的路径 */
        private String saveFilePath = HtmlConverter.class.getResource("/").getPath() + "output/";

    }

    private final Logger logger = LoggerFactory.getLogger(HtmlConverter.class);

    private Builder builder;

    public HtmlConverter(Builder builder) {
        this.builder = builder;
    }

    /**
     * 构建类
     */
    public static class Builder {

        private Params params;

        public Builder() {
            this.params = new Params();
            this.params.fontFamily = Params.DEFAULT_FONT_FAMILY;
            this.params.fontPath = Params.DEFAULT_FONT_PATH;
            this.params.pageSize = Params.DEFAULT_PAGE_SIZE;
            this.params.isLandScape = Params.DEFAULT_LAND_SCAPE;
        }

        public Builder fontFamily(String fontFamily) {
            this.params.fontFamily = fontFamily;
            return this;
        }

        public Builder fontPath(String fontPath) {
            this.params.fontPath = fontPath;
            return this;
        }

        public Builder pageSize(String pageSize) {
            this.params.pageSize = pageSize;
            return this;
        }

        public Builder isLandScape(boolean isLandScape) {
            this.params.isLandScape = isLandScape;
            return this;
        }

        public Builder saveFilePath(String saveFilePath) {
            this.params.saveFilePath = saveFilePath;
            return this;
        }

        /**
         * 数据处理完毕之后处理逻辑放在构造函数里面
         *
         * @return
         */
        public HtmlConverter builder() {
            return new HtmlConverter(this);
        }

    }

    /**
     * 将页面保存为 docx
     *
     * @param url
     * @param fileName
     * @return
     * @throws Exception
     */
    public File saveUrlToDocx(String url, String fileName) throws Exception {
        return saveDocx(url2word(url), fileName);
    }

    /**
     * 将页面保存为 pdf
     *
     * @param url
     * @param fileName
     * @return
     * @throws Exception
     */
    public File saveUrlToPdf(String url, String fileName) throws Exception {
        return savePdf(url2word(url), fileName);
    }

    /**
     * 将页面转为 {@link WordprocessingMLPackage}
     *
     * @param url
     * @return
     * @throws Exception
     */
    public WordprocessingMLPackage url2word(String url) throws Exception {
        return xhtml2word(url2xhtml(url), null);
    }

    /**
     * 将 {@link WordprocessingMLPackage} 存为 docx
     *
     * @param wordMLPackage
     * @param fileName
     * @return
     * @throws Exception
     */
    public File saveDocx(WordprocessingMLPackage wordMLPackage, String fileName) throws Exception {
        File file = new File(genFilePath(fileName) + ".docx");
        //保存到 docx 文件
        wordMLPackage.save(file);

        if (logger.isDebugEnabled()) {
            logger.debug("Save to [.docx]: {}", file.getAbsolutePath());
        }

        return file;
    }

    /**
     * 将 {@link WordprocessingMLPackage} 存为 pdf
     *
     * @param wordMLPackage
     * @param fileName
     * @return
     * @throws Exception
     */
    public File savePdf(WordprocessingMLPackage wordMLPackage, String fileName) throws Exception {

        File file = new File(genFilePath(fileName) + ".pdf");

        OutputStream os = new FileOutputStream(file);

        Docx4J.toPDF(wordMLPackage, os);

        os.flush();
        os.close();

        if (logger.isDebugEnabled()) {
//            logger.debug("Save to [.pdf]: {}", file.getAbsolutePath());
        }
        return file;
    }

    /**
     * 将 {@link Document} 对象转为 {@link WordprocessingMLPackage}
     * xhtml to word
     *
     * @param doc
     * @return
     * @throws Exception
     */
    protected WordprocessingMLPackage xhtml2word(Document doc, String watermarkText) throws Exception {
        //A4纸,//横版:true
        WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.createPackage(PageSizePaper.valueOf(this.builder.params.pageSize), this.builder.params.isLandScape);

        //配置中文字体
        configSimSunFont(wordMLPackage);

        if (!StringUtils.isEmpty(watermarkText)) {
            addWatermarkToWord(wordMLPackage, watermarkText);
        }

        XHTMLImporterImpl xhtmlImporter = new XHTMLImporterImpl(wordMLPackage);

        //导入 xhtml
        wordMLPackage.getMainDocumentPart().getContent().addAll(
                xhtmlImporter.convert(doc.html(), doc.baseUri()));

        return wordMLPackage;
    }

    /**
     * 将页面转为{@link Document}对象,xhtml 格式
     *
     * @param url
     * @return
     * @throws Exception
     */
    protected Document url2xhtml(String url) throws Exception {
        // 添加头部授权参数防止被过滤
        String token = AESEncryptUtils.aesEncryptToString("html2File");

        Document doc = Jsoup.connect(url).header("Authorization", token).get();

        if (logger.isDebugEnabled()) {
//            logger.debug("baseUri: {}", doc.baseUri());
        }
        //除去所有 script
        for (Element script : doc.getElementsByTag(SCRIPT.getCode())) {
            script.remove();
        }

        //除去 a 的 onclick,href 属性
        for (Element a : doc.getElementsByTag(A.getCode())) {
            a.removeAttr("onclick");
//            a.removeAttr("href");
        }
        //将link中的地址替换为绝对地址
        Elements links = doc.getElementsByTag(LINK.getCode());
        for (Element element : links) {
            String href = element.absUrl(HREF.getCode());

            if (logger.isDebugEnabled()) {
//                logger.debug("href: {} -> {}", element.attr(HREF.getCode()), href);
            }

            element.attr(HREF.getCode(), href);
        }

        //转为 xhtml 格式
        doc.outputSettings()
                .syntax(Document.OutputSettings.Syntax.xml)
                .escapeMode(Entities.EscapeMode.xhtml);

        if (logger.isDebugEnabled()) {
            String[] split = doc.html().split("\n");
            for (int c = 0; c < split.length; c++) {
//                logger.debug("line {}:\t{}", c + 1, split[c]);
            }
        }
        return doc;
    }

    /**
     * 为 {@link WordprocessingMLPackage} 配置中文字体
     *
     * @param wordMLPackage
     * @throws Exception
     */
    protected void configSimSunFont(WordprocessingMLPackage wordMLPackage) throws Exception {
        Mapper fontMapper = new IdentityPlusMapper();
        wordMLPackage.setFontMapper(fontMapper);

        //加载字体文件(解决linux环境下无中文字体问题)
        URL simsunUrl = this.getClass().getResource(this.builder.params.fontPath);
        PhysicalFonts.addPhysicalFont(simsunUrl);
        PhysicalFont simsunFont = PhysicalFonts.get(this.builder.params.fontFamily);
        fontMapper.put(this.builder.params.fontFamily, simsunFont);
        //设置文件默认字体
        RFonts rfonts = Context.getWmlObjectFactory().createRFonts();
        rfonts.setAsciiTheme(null);
        rfonts.setAscii(this.builder.params.fontFamily);
        wordMLPackage.getMainDocumentPart().getPropertyResolver()
                .getDocumentDefaultRPr().setRFonts(rfonts);
    }

    /**
     * 直接通过HTML字符串生成Word处理包(核心修改点)
     */
    public WordprocessingMLPackage htmlString2word(String htmlContent, String watermarkText) throws Exception {
        // 解析 HTML 字符串为 Document 对象
        Document doc = Jsoup.parse(htmlContent);

        // 配置输出设置(修正后的关键步骤)
        doc.outputSettings()
                .syntax(Document.OutputSettings.Syntax.xml)
                .escapeMode(Entities.EscapeMode.xhtml);

        // 清理不安全标签(复用原有逻辑)
        cleanHtml(doc);

        // 转换为 Word 处理包
        return xhtml2word(doc, watermarkText);
    }

    /**
     * 清理HTML标签(提取公共方法)
     */
    private void cleanHtml(Document doc) {
        // 移除script标签
        doc.getElementsByTag(RemoveTag.SCRIPT.getCode()).remove();
        // 移除a标签的事件和链接属性
        doc.getElementsByTag(RemoveTag.A.getCode()).forEach(a -> {
            a.removeAttr("onclick");
//            a.removeAttr("href");
        });
        // 处理link标签的绝对路径(如需加载外部资源,可保留此逻辑)
        doc.getElementsByTag(RemoveTag.LINK.getCode()).forEach(link -> {
            String href = link.absUrl(RemoveTag.HREF.getCode());
            link.attr(RemoveTag.HREF.getCode(), href);
        });
    }

    /**
     * 公共文件下载处理方法
     */
    public void handleFileDownload(
            File file,
            String displayFileName,
            HttpServletRequest request,
            HttpServletResponse response
    ) throws Exception {
        // 文件名编码处理
        String encodedFileName = URLEncoder.encode(displayFileName, "UTF-8")
                .replaceAll("\\+", "%20"); // 处理空格问题

        // 设置响应头
        response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
        response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);
        response.setHeader("Content-Length", String.valueOf(file.length()));

        // 流传输
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
             BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())) {
            byte[] buffer = new byte[1024 * 8];
            int bytesRead;
            while ((bytesRead = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, bytesRead);
            }
            bos.flush();
        }
    }

    /**
     * 生成文件位置
     *
     * @return
     */
    protected String genFilePath(String fileName) {
        return this.builder.params.saveFilePath + fileName;
    }

    /**
     * 在 Word 文档中添加多水印
     */
    private void addWatermarkToWord(WordprocessingMLPackage wordprocessingMLPackage, String watermarkText) throws Exception {
        MainDocumentPart mainDocumentPart = wordprocessingMLPackage.getMainDocumentPart();
        DocumentModel documentModel = wordprocessingMLPackage.getDocumentModel();

        SectPr sectPr = wordprocessingMLPackage.getDocumentModel().getSections().get(wordprocessingMLPackage.getDocumentModel().getSections().size() - 1).getSectPr();

        if (sectPr == null) {
            sectPr = factory.createSectPr();
            mainDocumentPart.addObject(sectPr);
            documentModel.getSections().get(documentModel.getSections().size() - 1).setSectPr(sectPr);
        }

        // 修改值为 first even default 测试
        String hdrFtrRef = "even";

        // header footer 判断规则 规则为: first even default
        if (hdrFtrRef.equals("first")) {
            // first:设置 first、default两个header
            createHeader(sectPr, "first", watermarkText, mainDocumentPart);
            createFooter(sectPr, "first", mainDocumentPart, wordprocessingMLPackage);
            //首页不同时
            sectPr.setTitlePg(new BooleanDefaultTrue());

        } else if (hdrFtrRef.equals("even")) {
            // even: 设置even default 两个header 并在 setting.xml 设置 evenAndOddHeaders
            createHeader(sectPr, "even", watermarkText, mainDocumentPart);
            createFooter(sectPr, "even", mainDocumentPart, wordprocessingMLPackage);
            DocumentSettingsPart documentSettingsPart = mainDocumentPart.getDocumentSettingsPart();
            CTSettings contents = documentSettingsPart.getContents();
            //奇偶不同时设置
            contents.setEvenAndOddHeaders(new BooleanDefaultTrue());
        }

        // default,增加一个header footer 设置为rels为default
        createHeader(sectPr, "default", watermarkText, mainDocumentPart);
        createFooter(sectPr, "default", mainDocumentPart, wordprocessingMLPackage);


          // 设置首页空白页demo,不用可以屏蔽
//        mainDocumentPart.addObject(makePageBr());

//        mainDocumentPart.addStyledParagraphOfText("Heading1", "页面内容");
//        mainDocumentPart.addObject(makePageBr());
//        mainDocumentPart.addStyledParagraphOfText("Normal", "页面内容11111");
//        mainDocumentPart.addObject(makePageBr());

    }

    private void createFooter(SectPr sectPr, String type, MainDocumentPart mainDocumentPart, WordprocessingMLPackage wordprocessingMLPackage) throws Exception {
        FooterPart footerPart = new FooterPart(new PartName("/word/footer-" + type + ".xml"));
        Ftr ftr = factory.createFtr();

        // Bind the header JAXB elements as representing their header parts
        footerPart.setJaxbElement(ftr);
        Relationship relationship = mainDocumentPart.addTargetPart(footerPart);
        wordprocessingMLPackage.getParts().put(footerPart);

        FooterReference footerReference = factory.createFooterReference();
        footerReference.setType(HdrFtrRef.fromValue(type));
        footerReference.setId(relationship.getId());
        P paragraph = factory.createP();
        if ("first".equals(type)) {
            createHeaderFooterThreePart1(paragraph);
        } else {
            createHeaderFooterThreePart(paragraph);
        }
        ftr.getContent().add(paragraph);
        sectPr.getEGHdrFtrReferences().add(footerReference);

    }


    private void createHeader(SectPr sectPr, String type, String watermark, MainDocumentPart mainDocumentPart) throws Exception {

        HeaderPart headerPart = new HeaderPart(new PartName("/word/heade-" + type + ".xml"));
        Relationship relationship = mainDocumentPart.addTargetPart(headerPart);
        Hdr hdr = null;
        if (org.apache.commons.lang3.StringUtils.isNoneBlank(watermark)) {
            setWatermarkHdr(headerPart, watermark);
            hdr = headerPart.getJaxbElement();
        } else {
            hdr = factory.createHdr();
        }

        // Bind the header JAXB elements as representing their header parts
        headerPart.setJaxbElement(hdr);
        P paragraph = factory.createP();
        if ("first".equals(type)) {
            createHeaderFooterThreePart1(paragraph);
        } else {
            createHeaderFooterThreePart(paragraph);
        }
        hdr.getContent().add(paragraph);

        // headerPart.getJaxbElement().getContent().add(e)

        // Add the reference to both header parts to the Main Document Part
        HeaderReference headerReference = factory.createHeaderReference();
        headerReference.setType(HdrFtrRef.fromValue(type));
        headerReference.setId(relationship.getId());
        sectPr.getEGHdrFtrReferences().add(headerReference);

    }

    /**
     * 添加页眉页脚,左中右 三部分内容
     *
     * @return 页脚对象
     */
    private void createHeaderFooterThreePart(P paragraph) {
        RPr fontRPr = getRPr("宋体", "000000", "22", STHint.EAST_ASIA, true, false, false, false);
        R run = factory.createR();
        run.setRPr(fontRPr);
        paragraph.getContent().add(run);

        // tab
//        paragraph.getContent().add(getTextField("left少时诵诗书"));
        R r1 = factory.createR();
        R.Ptab rPtab = factory.createRPtab();
        rPtab.setAlignment(STPTabAlignment.CENTER);
        rPtab.setRelativeTo(STPTabRelativeTo.MARGIN);
        rPtab.setLeader(STPTabLeader.NONE);
        r1.getContent().add(rPtab);
        paragraph.getContent().add(r1);
        // 中间内容
        SdtContentBlock sdtContentBlock = factory.createSdtContentBlock();
        sdtContentBlock.getContent().add(getTextField("第"));
        sdtContentBlock.getContent().add(getFieldBegin());
        sdtContentBlock.getContent().add(getPageNumberField());
        sdtContentBlock.getContent().add(getFieldEnd());
        sdtContentBlock.getContent().add(getTextField("页"));
        sdtContentBlock.getContent().add(getTextField(" 总共"));
        sdtContentBlock.getContent().add(getFieldBegin());
        sdtContentBlock.getContent().add(getTotalPageNumberField());
        sdtContentBlock.getContent().add(getFieldEnd());
        sdtContentBlock.getContent().add(getTextField("页"));
        paragraph.getContent().add(sdtContentBlock);
        // tab
        R r2 = factory.createR();
        R.Ptab rPtab1 = factory.createRPtab();
        rPtab1.setAlignment(STPTabAlignment.RIGHT);
        rPtab1.setRelativeTo(STPTabRelativeTo.MARGIN);
        rPtab1.setLeader(STPTabLeader.NONE);
        r2.getContent().add(rPtab1);

        // 右边内容
        paragraph.getContent().add(r2);
//        paragraph.getContent().add(getTextField("right塑料袋"));
    }

    /**
     * 添加页眉页脚,左中右 三部分内容
     *
     * @return 页脚对象
     */
    private void createHeaderFooterThreePart1(P paragraph) {
        RPr fontRPr = getRPr("宋体", "000000", "22", STHint.EAST_ASIA, true, false, false, false);
        R run = factory.createR();
        run.setRPr(fontRPr);
        paragraph.getContent().add(run);

        // tab
        paragraph.getContent().add(getTextField("9990090"));
        R r1 = factory.createR();
        R.Ptab rPtab = factory.createRPtab();
        rPtab.setAlignment(STPTabAlignment.CENTER);
        rPtab.setRelativeTo(STPTabRelativeTo.MARGIN);
        rPtab.setLeader(STPTabLeader.NONE);
        r1.getContent().add(rPtab);
        paragraph.getContent().add(r1);
        // 中间内容
        SdtContentBlock sdtContentBlock = factory.createSdtContentBlock();
        sdtContentBlock.getContent().add(getTextField("第"));
        sdtContentBlock.getContent().add(getFieldBegin());
        sdtContentBlock.getContent().add(getPageNumberField());
        sdtContentBlock.getContent().add(getFieldEnd());
        sdtContentBlock.getContent().add(getTextField("页"));
        sdtContentBlock.getContent().add(getTextField(" 总共"));
        sdtContentBlock.getContent().add(getFieldBegin());
        sdtContentBlock.getContent().add(getTotalPageNumberField());
        sdtContentBlock.getContent().add(getFieldEnd());
        sdtContentBlock.getContent().add(getTextField("页"));
        paragraph.getContent().add(sdtContentBlock);
        // tab
        R r2 = factory.createR();
        R.Ptab rPtab1 = factory.createRPtab();
        rPtab1.setAlignment(STPTabAlignment.RIGHT);
        rPtab1.setRelativeTo(STPTabRelativeTo.MARGIN);
        rPtab1.setLeader(STPTabLeader.NONE);
        r2.getContent().add(rPtab1);

        // 右边内容
        paragraph.getContent().add(r2);
        paragraph.getContent().add(getTextField("uuuuuuuu"));
    }

    private R getTextField(String content) {
        Text text = factory.createText();
        R run = factory.createR();
        text.setValue(content);
        run.getContent().add(text);
        return run;
    }

    private static R getPageNumberField() {
        R run = factory.createR();
        Text txt = new Text();
        txt.setSpace("preserve");
        txt.setValue("PAGE \\* MERGEFORMAT");
        run.getContent().add(factory.createRInstrText(txt));
        return run;
    }

    private static R getTotalPageNumberField() {
        R run = factory.createR();
        Text txt = new Text();
        txt.setSpace("preserve");
        txt.setValue(" NUMPAGES \\* MERGEFORMAT ");
        run.getContent().add(factory.createRInstrText(txt));
        return run;
    }

    private static R getFieldBegin() {
        R run = factory.createR();
        FldChar fldchar = factory.createFldChar();
        fldchar.setFldCharType(STFldCharType.BEGIN);
        run.getContent().add(fldchar);
        return run;
    }

    private R getFieldEnd() {
        FldChar fldcharend = factory.createFldChar();
        fldcharend.setFldCharType(STFldCharType.END);
        R run = factory.createR();
        run.getContent().add(fldcharend);
        return run;
    }

    public RPr getRPr(String fontFamily, String colorVal, String fontSize, STHint sTHint, boolean isBlod,
                      boolean isUnderLine, boolean isItalic, boolean isStrike) {
        RPr rPr = factory.createRPr();
        RFonts rf = new RFonts();
        rf.setHint(sTHint);
        rf.setAscii(fontFamily);
        rf.setHAnsi(fontFamily);
        rPr.setRFonts(rf);

        BooleanDefaultTrue bdt = factory.createBooleanDefaultTrue();
        rPr.setBCs(bdt);
        if (isBlod) {
            rPr.setB(bdt);
        }
        if (isItalic) {
            rPr.setI(bdt);
        }
        if (isStrike) {
            rPr.setStrike(bdt);
        }
        if (isUnderLine) {
            U underline = new U();
            underline.setVal(UnderlineEnumeration.SINGLE);
            rPr.setU(underline);
        }

        Color color = new Color();
        color.setVal(colorVal);
        rPr.setColor(color);

        HpsMeasure sz = new HpsMeasure();
        sz.setVal(new BigInteger(fontSize));
        rPr.setSz(sz);
        rPr.setSzCs(sz);
        return rPr;
    }


    private void setWatermarkHdr(HeaderPart headerPart, String text) throws Exception {

        ImagePngPart imagePart = new ImagePngPart(new PartName("/media/background.png"));
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ImageIO.write(createWaterMark(text), "png", out);
        byte[] imagebytes = out.toByteArray();
        imagePart.setBinaryData(imagebytes);
        Relationship rel = headerPart.addTargetPart(imagePart, RelationshipsPart.AddPartBehaviour.REUSE_EXISTING);

        String openXML = "<w:hdr mc:Ignorable=\"w14 wp14\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" xmlns:o=\"urn:schemas-microsoft-com:office:office\" xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">"
                + "<w:p>"
                + "<w:pPr>"
                + "<w:pStyle w:val=\"Header\"/>"
                + "</w:pPr>"
                + "<w:r>"
                + "<w:rPr>"
                + "<w:noProof/>"
                + "</w:rPr>"
                + "<w:pict>"
                + "<v:shapetype coordsize=\"21600,21600\" filled=\"f\" id=\"_x0000_t75\" o:preferrelative=\"t\" o:spt=\"75\" path=\"m@4@5l@4@11@9@11@9@5xe\" stroked=\"f\">"
                + "<v:stroke joinstyle=\"miter\"/>"
                + "<v:formulas>"
                + "<v:f eqn=\"if lineDrawn pixelLineWidth 0\"/>"
                + "<v:f eqn=\"sum @0 1 0\"/>"
                + "<v:f eqn=\"sum 0 0 @1\"/>"
                + "<v:f eqn=\"prod @2 1 2\"/>"
                + "<v:f eqn=\"prod @3 21600 pixelWidth\"/>"
                + "<v:f eqn=\"prod @3 21600 pixelHeight\"/>"
                + "<v:f eqn=\"sum @0 0 1\"/>"
                + "<v:f eqn=\"prod @6 1 2\"/>"
                + "<v:f eqn=\"prod @7 21600 pixelWidth\"/>"
                + "<v:f eqn=\"sum @8 21600 0\"/>"
                + "<v:f eqn=\"prod @7 21600 pixelHeight\"/>"
                + "<v:f eqn=\"sum @10 21600 0\"/>"
                + "</v:formulas>"
                + "<v:path gradientshapeok=\"t\" o:connecttype=\"rect\" o:extrusionok=\"f\"/>"
                + "<o:lock aspectratio=\"t\" v:ext=\"edit\"/>"
                + "</v:shapetype>"
                + "<v:shape id=\"WordPictureWatermark835936646\" o:allowincell=\"f\" o:spid=\"_x0000_s2050\" style=\"position:absolute;margin-left:0;margin-top:0;width:467.95pt;height:615.75pt;z-index:-251657216;mso-position-horizontal:center;mso-position-horizontal-relative:margin;mso-position-vertical:center;mso-position-vertical-relative:margin\" type=\"#_x0000_t75\">"
                + "<v:imagedata blacklevel=\"22938f\" gain=\"19661f\" o:title=\"docx4j-logo\" r:id=\"" + rel.getId() + "\"/>"
                + "</v:shape>"
                + "</w:pict>"
                + "</w:r>"
                + "</w:p>"
                + "</w:hdr>";

        Hdr hdr = (Hdr) XmlUtils.unmarshalString(openXML);
        headerPart.setJaxbElement(hdr);
    }

    private static BufferedImage createWaterMark(String content) {
        Integer width = 1000;
        Integer height = 1360;
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);// 获取bufferedImage对象
        String fontType = "宋体";
        Integer fontStyle = Font.PLAIN;
        Integer fontSize = 30;
        Font font = new Font(fontType, fontStyle, fontSize);
        Graphics2D g2d = image.createGraphics(); // 获取Graphics2d对象
        image = g2d.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
        g2d.dispose();
        for (int i = 1; i <= 20; i += 2) {
            for (int j = 1; j <= 10; j += 2) {
                int px = j * 100;
                int py = i * 100;
                g2d = image.createGraphics();
                g2d.setColor(java.awt.Color.black);
                g2d.setStroke(new BasicStroke(1)); // 设置字体
                g2d.setFont(font); // 设置字体类型 加粗 大小
                g2d.translate(px, py);// 设置原点
                g2d.rotate(Math.toRadians(-30));// 设置倾斜度
                FontRenderContext context = g2d.getFontRenderContext();
                Rectangle2D bounds = font.getStringBounds(content, context);
                g2d.drawString(content, 0, 0);
                g2d.dispose();
            }
        }
        return image;
    }

    private static P makePageBr() throws Exception {
        P p = factory.createP();
        R r = factory.createR();
        Br br = factory.createBr();
        br.setType(STBrType.PAGE);
        r.getContent().add(br);
        p.getContent().add(r);
        return p;
    }

}

四、controller

    /**
     * 直接接收HTML富文本内容生成Word文档
     * @param htmlContent 富文本HTML代码(如:<p>富文本内容</p>)
     */
    @PostMapping("/export")
    @Operation(summary = "导出word")
    @Parameters({
            @Parameter(name = "htmlContent", description = "富文本内容", required = true),
            @Parameter(name = "watermarkText", description = "水印内容", required = false)
    })
    public void generateWord(
            @RequestParam(value = "htmlContent", required = true) String htmlContent,
            @RequestParam(value = "watermarkText", required = false)  String watermarkText,
            HttpServletRequest request,
            HttpServletResponse response
    ) throws Exception {
        // 1. 初始化HtmlConverter(使用配置中的文件保存路径)
        HtmlConverter htmlConverter = new HtmlConverter.Builder()
                .saveFilePath(htmlConvertproperties.getFileSavePath()) // 从配置获取路径
                .builder();

        // 2. 转换HTML字符串为Word处理包
        WordprocessingMLPackage wordMLPackage = htmlConverter.htmlString2word(htmlContent, watermarkText);

        // 3. 生成临时文件并设置响应
        String fileName = "report_" + System.currentTimeMillis();
        File tempFile = htmlConverter.saveDocx(wordMLPackage, fileName); // 调用原有保存逻辑

        // 4. 处理文件下载(兼容不同浏览器)
        htmlConverter.handleFileDownload(tempFile, "报告.docx", request, response);

        // 5. 清理临时文件(根据需求可选,生产环境建议异步清理或设置过期策略)
        tempFile.deleteOnExit();
    }

五、引用说明

工具类参考github上的文章,但是对于工具类中的的具体逻辑作了修改。

https://github.com/FTOLs/report-demo

六、测试


网站公告

今日签到

点亮在社区的每一天
去签到